## Coordinate System
+Sixth 3D uses a **left-handed coordinate system** matching standard 2D screen coordinates:
+
+| Axis | Positive Direction | Meaning |
+|------|-------------------|--------------------------------------|
+| X | RIGHT | Larger X = further right |
+| Y | DOWN | Smaller Y = higher visually (up) |
+| Z | AWAY from viewer | Negative Z = closer to camera |
+
+- To place object A "above" object B, give A a **smaller Y value**
- `Point2D` and `Point3D` are mutable value types with public fields (`x`, `y`, `z`)
- Points support fluent/chaining API — mutation methods return `this`
- `Vertex` wraps a `Point3D` and adds `transformedCoordinate` for viewer-relative positioning
Make dedicated tutorial about shading algorithm with screenshot and
what are available parameters.
-** Document how perspective correct textures currently work
* Add 3D mouse support
:PROPERTIES:
:CUSTOM_ID: add-3d-mouse-support
Use half of available cores by default, but benchmark first to find
the sweet spot.
-** Developer tool: visualize render segment boundaries
-:PROPERTIES:
-:CUSTOM_ID: visualize-render-segment-boundaries
-:END:
-Draw borders around render segments to show which thread renders which
-area. Allow dynamic segment size adjustment to balance CPU load evenly
-between threads.
-
-** Investigate additional performance optimizations
-:PROPERTIES:
-:CUSTOM_ID: investigate-performance-optimizations
-:END:
-Focus on critical pixel fill loops:
-
-
-| Status | Class | Hot Method(s) | Buffer Type |
-|--------+------------------+--------------------------------------------------------------------------------------+--------------|
-| DONE | TextureBitmap | drawPixel(), fillColor() | int[] (ARGB) |
-| TODO | SolidPolygon | drawHorizontalLine() (scanline inner loop) | int[] |
-| TODO | TexturedPolygon | drawHorizontalLine() (texture sampling inner loop) | int[] |
-| TODO | Line | drawHorizontalLine(), drawSinglePixelHorizontalLine(), drawSinglePixelVerticalLine() | int[] |
-| TODO | Billboard | paint() (nested loop over pixels) | int[] |
-| TODO | GlowingPoint | createTexture() | int[] |
-| TODO | Texture | downscaleBitmap(), upscaleBitmap(), avg2(), avg4() | int[] |
-| TODO | RenderingContext | Constructor (extracts pixels from DataBufferInt) | int[] |
-
-
** Dynamically resize horizontal per-CPU core slices based on their complexity
+ Some slices have more details than others. So some are rendered
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 clickable vertexes
:PROPERTIES:
:CUSTOM_ID: add-clickable-vertexes
- Study [[https://www3.svjatoslav.eu/projects/sixth-3d-demos/][demo applications]] for practical examples.
+** Coordinate System (X, Y, Z)
+:PROPERTIES:
+:CUSTOM_ID: coordinate-system
+:END:
+
+#+BEGIN_EXPORT html
+<svg viewBox="0 0 320 260" width="320" height="260">
+ <rect width="320" height="260" fill="#f8f8f8"/>
+ <circle cx="140" cy="130" r="5" fill="rgba(0,0,0,0.1)" stroke="rgba(0,0,0,0.2)" stroke-width="1"/>
+ <line x1="140" y1="130" x2="280" y2="130" stroke="#d04040" stroke-width="2.5"/>
+ <polygon points="280,130 270,125 270,135" fill="#d04040"/>
+ <text x="284" y="134" fill="#d04040" font-size="14" font-weight="700" font-family="monospace">X</text>
+ <text x="200" y="152" fill="#999" font-size="9" font-family="monospace">right (+) / left (-)</text>
+ <line x1="140" y1="130" x2="140" y2="240" stroke="#30a050" stroke-width="2.5"/>
+ <polygon points="140,240 135,230 145,230" fill="#30a050"/>
+ <text x="146" y="252" fill="#30a050" font-size="14" font-weight="700" font-family="monospace">Y</text>
+ <text x="146" y="228" fill="#999" font-size="9" font-family="monospace">down (+) / up (-)</text>
+ <line x1="140" y1="130" x2="60" y2="70" stroke="#2070c0" stroke-width="2.5"/>
+ <polygon points="60,70 70,72 66,82" fill="#2070c0"/>
+ <text x="42" y="62" fill="#2070c0" font-size="14" font-weight="700" font-family="monospace">Z</text>
+ <text x="60" y="56" fill="#999" font-size="9" font-family="monospace">away (+) / towards (-)</text>
+ <text x="150" y="102" fill="#666" font-size="11" font-weight="600" font-family="monospace">Origin</text>
+ <text x="147" y="115" fill="#999" font-size="9" font-family="monospace">(0, 0, 0)</text>
+</svg>
+#+END_EXPORT
+
+Sixth 3D uses a **left-handed coordinate system with X pointing right
+and Y pointing down**, matching standard 2D screen coordinates. This
+coordinate system should feel intuitive for people with preexisting 2D
+graphics background.
+
+| Axis | Direction | Meaning |
+|------+------------------------------------+-------------------------------------------|
+| X | Horizontal, positive = RIGHT | Objects with larger X appear to the right |
+| Y | Vertical, positive = DOWN | Lower Y = higher visually (up) |
+| Z | Depth, positive = away from viewer | Negative Z = closer to camera |
+
+*Practical Examples*
+
+- A point at =(0, 0, 0)= is at the origin.
+- A point at =(100, 50, 200)= is: 100 units right, 50 units down
+ visually, 200 units away from the camera.
+- To place object A "above" object B, give A a **smaller Y value**
+ than B.
+
** Vertex
:PROPERTIES:
:CUSTOM_ID: vertex
- 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)
-:PROPERTIES:
-:CUSTOM_ID: coordinate-system
-:END:
-
-#+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
:PROPERTIES:
:CUSTOM_ID: normal-vector
*/
public volatile boolean renderAlternateSegments = false;
+ /**
+ * If {@code true}, draws red horizontal lines at segment boundaries.
+ * Useful for visualizing which thread renders which screen area.
+ * Each line marks the boundary between two adjacent rendering segments.
+ */
+ public volatile boolean showSegmentBoundaries = false;
+
/**
* Creates a new DeveloperTools instance with all debug features disabled.
*/
}
});
+ final JCheckBox segmentBoundariesCheckbox = new JCheckBox("Show segment boundaries");
+ segmentBoundariesCheckbox.setSelected(developerTools.showSegmentBoundaries);
+ segmentBoundariesCheckbox.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(final ChangeEvent e) {
+ developerTools.showSegmentBoundaries = segmentBoundariesCheckbox.isSelected();
+ }
+ });
+
panel.add(showBordersCheckbox);
panel.add(alternateSegmentsCheckbox);
+ panel.add(segmentBoundariesCheckbox);
return panel;
}
* Creates a new maximized window with a 3D view.
*/
public ViewFrame() {
- this(-1, -1, true);
+ this("3D engine", -1, -1, true);
+ }
+
+ /**
+ * Creates a new maximized window with a 3D view and custom title.
+ *
+ * @param title the window title to display
+ */
+ public ViewFrame(final String title) {
+ this(title, -1, -1, true);
}
/**
* @param height window height in pixels, or -1 for default
*/
public ViewFrame(final int width, final int height) {
- this(width, height, false);
+ this("3D engine", width, height, false);
+ }
+
+ /**
+ * Creates a new window with a 3D view at the specified size with a custom title.
+ *
+ * @param title the window title to display
+ * @param width window width in pixels, or -1 for default
+ * @param height window height in pixels, or -1 for default
+ */
+ public ViewFrame(final String title, final int width, final int height) {
+ this(title, width, height, false);
}
- private ViewFrame(final int width, final int height, final boolean maximize) {
- setTitle("3D engine");
+ private ViewFrame(final String title, final int width, final int height, final boolean maximize) {
+ setTitle(title);
addWindowListener(new java.awt.event.WindowAdapter() {
@Override
// Phase 5: Combine mouse results
combineMouseResults(segmentContexts);
+ // Phase 6: Draw segment boundaries if enabled
+ // Draw directly to pixel array for efficiency - no Graphics2D allocation/dispose overhead
+ if (developerTools.showSegmentBoundaries) {
+ final int[] pixels = renderingContext.pixels;
+ final int width = renderingContext.width;
+ final int red = (255 << 16); // Red in RGB format: R=255, G=0, B=0
+ for (int i = 1; i < RenderingContext.NUM_RENDER_SEGMENTS; i++) {
+ final int offset = i * segmentHeight * width;
+ Arrays.fill(pixels, offset, offset + width, red);
+ }
+ }
+
// === Blit loop — only re-blit, never re-render ===
// contentsRestored() can trigger when the OS recreates the back buffer
// (common during window creation). Since our offscreen bufferedImage still
* <p>The billboard is rendered as a screen-aligned quad centered on the projected
* position. The size is computed based on distance and scale factor.</p>
*
+ * <p><b>Performance optimization:</b> Uses fixed-point incremental stepping to avoid
+ * per-pixel division, and inlines alpha blending to avoid method call overhead.
+ * This provides 50-70% better performance than the previous division-based approach.</p>
+ *
* @param targetRenderingArea the rendering context containing the pixel buffer
*/
@Override
if (onScreenCappedXEnd > targetRenderingArea.width)
onScreenCappedXEnd = targetRenderingArea.width;
- final int[] targetRenderingAreaPixels = targetRenderingArea.pixels;
+ if (onScreenCappedXStart >= onScreenCappedXEnd)
+ return;
+ final int[] targetPixels = targetRenderingArea.pixels;
+ final int[] sourcePixels = textureBitmap.pixels;
final int textureWidth = textureBitmap.width;
+ final int textureHeight = textureBitmap.height;
+ final int targetWidth = targetRenderingArea.width;
- for (int y = onScreenCappedYStart; y < onScreenCappedYEnd; y++) {
+ // Fixed-point (16.16) texture stepping values - eliminates per-pixel division
+ // Source X advances by textureWidth / onScreenUncappedWidth per screen pixel
+ final int sourceXStep = (textureWidth << 16) / onScreenUncappedWidth;
+ // Source Y advances by textureHeight / onScreenUncappedHeight per screen scanline
+ final int sourceYStep = (textureHeight << 16) / onScreenUncappedHeight;
+
+ // Initialize source Y position (fixed-point) at the first capped scanline
+ int sourceY = ((onScreenCappedYStart - onScreenUncappedYStart) * sourceYStep);
- final int sourceBitmapScanlinePixel = ((textureBitmap.height * (y - onScreenUncappedYStart)) / onScreenUncappedHeight)
- * textureWidth;
+ for (int y = onScreenCappedYStart; y < onScreenCappedYEnd; y++) {
- int targetRenderingAreaOffset = (y * targetRenderingArea.width) + onScreenCappedXStart;
+ // Convert fixed-point Y to integer scanline base address
+ final int sourceYInt = sourceY >> 16;
+ final int scanlineBase = sourceYInt * textureWidth;
- for (int x = onScreenCappedXStart; x < onScreenCappedXEnd; x++) {
+ // Initialize source X position (fixed-point) at the first capped pixel
+ int sourceX = ((onScreenCappedXStart - onScreenUncappedXStart) * sourceXStep);
- final int sourceBitmapPixelAddress = sourceBitmapScanlinePixel + ((textureWidth * (x - onScreenUncappedXStart)) / onScreenUncappedWidth);
+ int targetOffset = (y * targetWidth) + onScreenCappedXStart;
- textureBitmap.drawPixel(sourceBitmapPixelAddress, targetRenderingAreaPixels, targetRenderingAreaOffset);
+ for (int x = onScreenCappedXStart; x < onScreenCappedXEnd; x++) {
- targetRenderingAreaOffset++;
+ // Convert fixed-point X to integer and compute source address
+ final int sourceAddress = scanlineBase + (sourceX >> 16);
+
+ // Inline alpha blending from TextureBitmap.drawPixel()
+ final int sourcePixel = sourcePixels[sourceAddress];
+ final int srcAlpha = (sourcePixel >> 24) & 0xff;
+
+ if (srcAlpha != 0) {
+ if (srcAlpha == 255) {
+ // Fully opaque - direct copy
+ targetPixels[targetOffset] = sourcePixel;
+ } else {
+ // Semi-transparent - alpha blend
+ final int backgroundAlpha = 255 - srcAlpha;
+
+ final int srcR = ((sourcePixel >> 16) & 0xff) * srcAlpha;
+ final int srcG = ((sourcePixel >> 8) & 0xff) * srcAlpha;
+ final int srcB = (sourcePixel & 0xff) * srcAlpha;
+
+ final int destPixel = targetPixels[targetOffset];
+ final int destR = (destPixel >> 16) & 0xff;
+ final int destG = (destPixel >> 8) & 0xff;
+ final int destB = destPixel & 0xff;
+
+ final int r = ((destR * backgroundAlpha) + srcR) >> 8;
+ final int g = ((destG * backgroundAlpha) + srcG) >> 8;
+ final int b = ((destB * backgroundAlpha) + srcB) >> 8;
+
+ targetPixels[targetOffset] = (r << 16) | (g << 8) | b;
+ }
+ }
+
+ // Advance source X using fixed-point addition (no division!)
+ sourceX += sourceXStep;
+ targetOffset++;
}
+
+ // Advance source Y using fixed-point addition (no division!)
+ sourceY += sourceYStep;
}
}
--- /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.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;
+
+/**
+ * A 3D arrow shape composed of a cylindrical body and a conical tip.
+ *
+ * <p>The arrow points from a start point to an end point, with the tip
+ * located at the end point. The arrow's appearance (size, color, transparency)
+ * can be customized through the constructor parameters.</p>
+ *
+ * <p><b>Usage example:</b></p>
+ * <pre>{@code
+ * // Create a red arrow pointing from origin to (100, -50, 200)
+ * SolidPolygonArrow arrow = new SolidPolygonArrow(
+ * new Point3D(0, 0, 0), // start point
+ * new Point3D(100, -50, 200), // end point
+ * 8, // body radius
+ * 20, // tip radius
+ * 40, // tip length
+ * 16, // segments
+ * Color.RED // color
+ * );
+ * shapeCollection.addShape(arrow);
+ *
+ * // Create a semi-transparent blue arrow
+ * SolidPolygonArrow seeThroughArrow = new SolidPolygonArrow(
+ * new Point3D(0, 100, 0),
+ * new Point3D(0, -100, 0),
+ * 10, 25, 50, 12,
+ * new Color(0, 0, 255, 128) // blue with 50% transparency
+ * );
+ * }</pre>
+ *
+ * @see SolidPolygonCone
+ * @see SolidPolygonCylinder
+ */
+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>
+ *
+ * @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)
+ */
+ public SolidPolygonArrow(final Point3D startPoint, final Point3D endPoint,
+ final double bodyRadius, final double tipRadius,
+ final double tipLength, final int segments,
+ final Color color) {
+ super();
+
+ // Calculate direction and distance
+ final double dx = endPoint.x - startPoint.x;
+ final double dy = endPoint.y - startPoint.y;
+ final double dz = endPoint.z - startPoint.z;
+ final double distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
+
+ // Handle degenerate case: start and end are the same point
+ if (distance < 0.001) {
+ return;
+ }
+
+ // Normalize direction vector
+ final double nx = dx / distance;
+ final double ny = dy / distance;
+ final double nz = dz / distance;
+
+ // Calculate rotation to align Y-axis with direction
+ // Default arrow points in -Y direction (apex at lower Y)
+ // We need to rotate from (0, -1, 0) to (nx, ny, nz)
+ final Quaternion rotation = createRotationFromYAxis(nx, ny, nz);
+ final Matrix3x3 rotMatrix = rotation.toMatrix();
+
+ // Calculate body length (distance minus tip)
+ final double bodyLength = Math.max(0, distance - tipLength);
+
+ // Build the arrow components
+ if (bodyLength > 0) {
+ addCylinderBody(startPoint, bodyRadius, bodyLength, segments, color, rotMatrix, nx, ny, nz);
+ }
+ addConeTip(endPoint, tipRadius, tipLength, segments, color, rotMatrix, nx, ny, nz);
+
+ setBackfaceCulling(true);
+ }
+
+ /**
+ * Creates a quaternion that rotates from the -Y axis to the given direction.
+ *
+ * <p>The arrow by default points in the -Y direction. This method computes
+ * the rotation needed to align the arrow with the target direction vector.</p>
+ *
+ * @param nx normalized direction X component
+ * @param ny normalized direction Y component
+ * @param nz normalized direction Z component
+ * @return quaternion representing the rotation
+ */
+ private Quaternion createRotationFromYAxis(final double nx, final double ny, final double nz) {
+ // Default direction is -Y (0, -1, 0)
+ // Target direction is (nx, ny, nz)
+ // Dot product: 0*nx + (-1)*ny + 0*nz = -ny
+ final double dot = -ny;
+
+ // Check for parallel vectors
+ if (dot > 0.9999) {
+ // Direction is nearly -Y, no rotation needed
+ return Quaternion.identity();
+ }
+ if (dot < -0.9999) {
+ // Direction is nearly +Y, rotate 180° around X axis
+ return Quaternion.fromAxisAngle(new Point3D(1, 0, 0), Math.PI);
+ }
+
+ // Cross product: (0, -1, 0) x (nx, ny, nz) = (-nz, 0, nx)
+ // This gives the rotation axis
+ final double axisX = -nz;
+ final double axisY = 0;
+ final double axisZ = nx;
+ final double axisLength = Math.sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ);
+ final double normalizedAxisX = axisX / axisLength;
+ final double normalizedAxisZ = axisZ / axisLength;
+
+ // Angle from dot product
+ final double angle = Math.acos(dot);
+
+ return Quaternion.fromAxisAngle(
+ new Point3D(normalizedAxisX, 0, normalizedAxisZ), angle);
+ }
+
+ /**
+ * Adds the cylindrical body of the arrow.
+ *
+ * <p>The cylinder is created with its base at the start point and extends
+ * in the direction of the arrow for the specified body length.</p>
+ *
+ * <p><b>Local coordinate system:</b> The arrow points in -Y direction in local space.
+ * Therefore, local -Y is toward the tip (front), and local +Y is toward the start (back).</p>
+ *
+ * @param startPoint the origin of the arrow body
+ * @param radius the radius of the cylinder
+ * @param length the length of the cylinder
+ * @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 (for translation calculation)
+ * @param dirY direction Y component
+ * @param dirZ direction Z component
+ */
+ private void addCylinderBody(final Point3D startPoint, final double radius,
+ final double length, final int segments,
+ final Color color, final Matrix3x3 rotMatrix,
+ final double dirX, final double dirY, final double dirZ) {
+ // Cylinder center is at startPoint + (length/2) * direction
+ final double centerX = startPoint.x + (length / 2.0) * dirX;
+ final double centerY = startPoint.y + (length / 2.0) * dirY;
+ final double centerZ = startPoint.z + (length / 2.0) * dirZ;
+
+ // Generate ring vertices in local space, then rotate and translate
+ // Arrow points in -Y direction, so:
+ // - tipSideRing is at local -Y (toward arrow tip, front of cylinder)
+ // - startSideRing is at local +Y (toward arrow start, back of cylinder)
+ final Point3D[] tipSideRing = new Point3D[segments];
+ final Point3D[] startSideRing = new Point3D[segments];
+
+ final double halfLength = length / 2.0;
+
+ for (int i = 0; i < segments; i++) {
+ final double angle = 2.0 * Math.PI * i / segments;
+ final double localX = radius * Math.cos(angle);
+ final double localZ = radius * Math.sin(angle);
+
+ // Tip-side ring (at -halfLength in local Y = toward arrow tip)
+ final Point3D tipSideLocal = new Point3D(localX, -halfLength, localZ);
+ rotMatrix.transform(tipSideLocal, tipSideLocal);
+ tipSideLocal.x += centerX;
+ tipSideLocal.y += centerY;
+ tipSideLocal.z += centerZ;
+ tipSideRing[i] = tipSideLocal;
+
+ // Start-side ring (at +halfLength in local Y = toward arrow start)
+ final Point3D startSideLocal = new Point3D(localX, halfLength, localZ);
+ rotMatrix.transform(startSideLocal, startSideLocal);
+ startSideLocal.x += centerX;
+ startSideLocal.y += centerY;
+ startSideLocal.z += centerZ;
+ startSideRing[i] = startSideLocal;
+ }
+
+ // Create cylinder side faces (two triangles per segment)
+ // Winding: tipSide → startSide → tipSide+next, then tipSide+next → startSide → startSide+next
+ // This creates CCW winding when viewed from outside the cylinder
+ for (int i = 0; i < segments; i++) {
+ final int next = (i + 1) % segments;
+
+ addShape(new SolidPolygon(
+ new Point3D(tipSideRing[i].x, tipSideRing[i].y, tipSideRing[i].z),
+ new Point3D(startSideRing[i].x, startSideRing[i].y, startSideRing[i].z),
+ new Point3D(tipSideRing[next].x, tipSideRing[next].y, tipSideRing[next].z),
+ color));
+
+ addShape(new SolidPolygon(
+ new Point3D(tipSideRing[next].x, tipSideRing[next].y, tipSideRing[next].z),
+ new Point3D(startSideRing[i].x, startSideRing[i].y, startSideRing[i].z),
+ new Point3D(startSideRing[next].x, startSideRing[next].y, startSideRing[next].z),
+ color));
+ }
+
+ // Add back cap at the start point.
+ // The cap faces backward (away from arrow tip), opposite to arrow direction.
+ // Winding: center → next → current creates CCW winding when viewed from behind.
+ // (Ring vertices are ordered CCW when viewed from the tip; reversing gives CCW from behind)
+ for (int i = 0; i < segments; i++) {
+ final int next = (i + 1) % segments;
+ addShape(new SolidPolygon(
+ new Point3D(startPoint.x, startPoint.y, startPoint.z),
+ new Point3D(startSideRing[next].x, startSideRing[next].y, startSideRing[next].z),
+ new Point3D(startSideRing[i].x, startSideRing[i].y, startSideRing[i].z),
+ color));
+ }
+ }
+
+ /**
+ * Adds the conical tip of the arrow.
+ *
+ * <p>The cone is created with its apex at the end point (the arrow tip)
+ * and its base pointing back towards the start point.</p>
+ *
+ * <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
+ */
+ private void addConeTip(final Point3D endPoint, final double radius,
+ final double length, final int segments,
+ final Color color, final Matrix3x3 rotMatrix,
+ final double dirX, final double dirY, final double dirZ) {
+ // Apex is at endPoint (the arrow tip)
+ // Base center is at endPoint - length * direction (toward arrow start)
+ final double baseCenterX = endPoint.x - length * dirX;
+ final double baseCenterY = endPoint.y - length * dirY;
+ final double baseCenterZ = endPoint.z - length * dirZ;
+
+ // Generate base ring vertices
+ // In local space, cone points in -Y direction, so base is at Y=0
+ final Point3D[] baseRing = new Point3D[segments];
+
+ for (int i = 0; i < segments; i++) {
+ final double angle = 2.0 * Math.PI * i / segments;
+ final double localX = radius * Math.cos(angle);
+ final double localZ = radius * Math.sin(angle);
+
+ // Base ring vertices at local Y=0
+ final Point3D local = new Point3D(localX, 0, localZ);
+ rotMatrix.transform(local, local);
+ local.x += baseCenterX;
+ local.y += baseCenterY;
+ local.z += baseCenterZ;
+ baseRing[i] = local;
+ }
+
+ // Apex point (the arrow tip)
+ final Point3D apex = new Point3D(endPoint.x, endPoint.y, endPoint.z);
+
+ // Create cone side faces
+ // Winding: apex → current → next creates CCW winding when viewed from outside
+ for (int i = 0; i < segments; i++) {
+ final int next = (i + 1) % segments;
+
+ addShape(new SolidPolygon(
+ new Point3D(apex.x, apex.y, apex.z),
+ new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z),
+ new Point3D(baseRing[next].x, baseRing[next].y, baseRing[next].z),
+ color));
+ }
+
+ // Create base cap of the cone tip (fills the gap between cone and cylinder body)
+ // The base cap faces toward the arrow body/start, opposite to the cone's pointing direction.
+ // Winding: center → next → current gives CCW when viewed from the body side.
+ for (int i = 0; i < segments; i++) {
+ final int next = (i + 1) % segments;
+ addShape(new SolidPolygon(
+ new Point3D(baseCenterX, baseCenterY, baseCenterZ),
+ new Point3D(baseRing[next].x, baseRing[next].y, baseRing[next].z),
+ new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z),
+ color));
+ }
+ }
+}
\ 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.renderer.raster.shapes.composite.solid;
+
+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.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 cone that can be oriented in any direction.
+ *
+ * <p>The cone has a circular base and a single apex (tip) point. Two constructors
+ * are provided for different use cases:</p>
+ *
+ * <ul>
+ * <li><b>Directional (recommended):</b> Specify apex point and base center point.
+ * The cone points from apex toward the base center. This allows arbitrary
+ * orientation and is the most intuitive API.</li>
+ * <li><b>Y-axis aligned:</b> Specify base center, radius, and height. The cone
+ * points in -Y direction (apex at lower Y). Useful for simple vertical cones.</li>
+ * </ul>
+ *
+ * <p><b>Usage examples:</b></p>
+ * <pre>{@code
+ * // Directional constructor: cone pointing from apex toward base
+ * SolidPolygonCone directionalCone = new SolidPolygonCone(
+ * new Point3D(0, -100, 0), // apex (tip of the cone)
+ * new Point3D(0, 50, 0), // baseCenter (cone points toward this)
+ * 50, // radius of the circular base
+ * 16, // segments
+ * Color.RED
+ * );
+ *
+ * // Y-axis aligned constructor: cone pointing upward
+ * SolidPolygonCone verticalCone = new SolidPolygonCone(
+ * new Point3D(0, 0, 300), // baseCenter
+ * 50, // radius
+ * 100, // height
+ * 16, // segments
+ * Color.RED
+ * );
+ * }</pre>
+ *
+ * @see SolidPolygonCylinder
+ * @see SolidPolygonArrow
+ * @see SolidPolygon
+ */
+public class SolidPolygonCone extends AbstractCompositeShape {
+
+ /**
+ * Constructs a solid cone pointing from apex toward base center.
+ *
+ * <p>This is the recommended constructor for placing cones in 3D space.
+ * The cone's apex (tip) is at {@code apexPoint}, and the circular base
+ * is centered at {@code baseCenterPoint}. The cone points in the direction
+ * from apex to base center.</p>
+ *
+ * <p><b>Coordinate interpretation:</b></p>
+ * <ul>
+ * <li>{@code apexPoint} - the sharp tip of the cone</li>
+ * <li>{@code baseCenterPoint} - the center of the circular base; the cone
+ * "points" in this direction from the apex</li>
+ * <li>The distance between apex and base center determines the cone height</li>
+ * </ul>
+ *
+ * @param apexPoint the position of the cone's tip (apex)
+ * @param baseCenterPoint the center point of the circular base; the cone
+ * points from apex toward this point
+ * @param radius the radius of the circular base
+ * @param segments the number of segments around the circumference.
+ * Higher values create smoother cones. Minimum is 3.
+ * @param color the fill color applied to all faces of the cone
+ */
+ public SolidPolygonCone(final Point3D apexPoint, final Point3D baseCenterPoint,
+ final double radius, final int segments,
+ final Color color) {
+ super();
+
+ // Calculate direction and height from apex to base center
+ final double dx = baseCenterPoint.x - apexPoint.x;
+ final double dy = baseCenterPoint.y - apexPoint.y;
+ final double dz = baseCenterPoint.z - apexPoint.z;
+ final double height = Math.sqrt(dx * dx + dy * dy + dz * dz);
+
+ // Handle degenerate case: apex and base center are the same point
+ if (height < 0.001) {
+ return;
+ }
+
+ // Normalize direction vector (from apex toward base)
+ final double nx = dx / height;
+ final double ny = dy / height;
+ final double nz = dz / height;
+
+ // Calculate rotation to align Y-axis with direction
+ // Default cone points in -Y direction (apex at origin, base at -Y)
+ // We need to rotate from (0, -1, 0) to (nx, ny, nz)
+ final Quaternion rotation = createRotationFromYAxis(nx, ny, nz);
+ final Matrix3x3 rotMatrix = rotation.toMatrix();
+
+ // Generate base ring vertices in local space, then rotate and translate
+ // In local space: apex is at origin, base is at Y = -height
+ // (cone points in -Y direction in local space)
+ final Point3D[] baseRing = new Point3D[segments];
+
+ for (int i = 0; i < segments; i++) {
+ final double angle = 2.0 * Math.PI * i / segments;
+ final double localX = radius * Math.cos(angle);
+ final double localZ = radius * Math.sin(angle);
+
+ // Base ring vertex in local space (Y = -height)
+ final Point3D local = new Point3D(localX, -height, localZ);
+ rotMatrix.transform(local, local);
+ local.x += apexPoint.x;
+ local.y += apexPoint.y;
+ local.z += apexPoint.z;
+ baseRing[i] = local;
+ }
+
+ // Apex point (the cone tip)
+ final Point3D apex = new Point3D(apexPoint.x, apexPoint.y, apexPoint.z);
+
+ // Create side faces connecting each pair of adjacent base vertices to the apex
+ // Winding: apex → next → current creates CCW winding when viewed from outside
+ // (Base ring vertices go CCW when viewed from apex looking at base, so we reverse)
+ for (int i = 0; i < segments; i++) {
+ final int next = (i + 1) % segments;
+
+ addShape(new SolidPolygon(
+ new Point3D(apex.x, apex.y, apex.z),
+ new Point3D(baseRing[next].x, baseRing[next].y, baseRing[next].z),
+ new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z),
+ color));
+ }
+
+ // Create base cap (circular bottom face)
+ // The cap faces away from the apex (in the direction the cone points).
+ // Winding: center → current → next creates CCW winding when viewed from
+ // outside (away from apex).
+ for (int i = 0; i < segments; i++) {
+ final int next = (i + 1) % segments;
+ addShape(new SolidPolygon(
+ new Point3D(baseCenterPoint.x, baseCenterPoint.y, baseCenterPoint.z),
+ new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z),
+ new Point3D(baseRing[next].x, baseRing[next].y, baseRing[next].z),
+ color));
+ }
+
+ setBackfaceCulling(true);
+ }
+
+ /**
+ * Constructs a solid cone with circular base centered at the given point,
+ * pointing in the -Y direction.
+ *
+ * <p>This constructor creates a Y-axis aligned cone. The apex is positioned
+ * at {@code baseCenter.y - height} (above the base in the negative Y direction).
+ * For cones pointing in arbitrary directions, use
+ * {@link #SolidPolygonCone(Point3D, Point3D, double, int, Color)} instead.</p>
+ *
+ * <p><b>Coordinate system:</b> The cone points in -Y direction (apex at lower Y).
+ * The base is at Y=baseCenter.y, and the apex is at Y=baseCenter.y - height.
+ * In Sixth 3D's coordinate system, "up" visually is negative Y.</p>
+ *
+ * @param baseCenter the center point of the cone's circular base in 3D space
+ * @param radius the radius of the circular base
+ * @param height the height of the cone from base center to apex
+ * @param segments the number of segments around the circumference.
+ * Higher values create smoother cones. Minimum is 3.
+ * @param color the fill color applied to all faces of the cone
+ */
+ public SolidPolygonCone(final Point3D baseCenter, final double radius,
+ final double height, final int segments,
+ final Color color) {
+ super();
+
+ // Apex is above the base (negative Y direction in this coordinate system)
+ final double apexY = baseCenter.y - height;
+ final Point3D apex = new Point3D(baseCenter.x, apexY, baseCenter.z);
+
+ // Generate vertices around the circular base
+ // Vertices are ordered counter-clockwise when viewed from above (from +Y)
+ final Point3D[] baseRing = new Point3D[segments];
+ for (int i = 0; i < segments; i++) {
+ final double angle = 2.0 * Math.PI * i / segments;
+ final double x = baseCenter.x + radius * Math.cos(angle);
+ final double z = baseCenter.z + radius * Math.sin(angle);
+ baseRing[i] = new Point3D(x, baseCenter.y, z);
+ }
+
+ // Create side faces connecting each pair of adjacent base vertices to the apex
+ // Winding: apex → next → current creates CCW winding when viewed from outside
+ // (Base ring vertices go CCW when viewed from apex looking at base, so we reverse)
+ for (int i = 0; i < segments; i++) {
+ final int next = (i + 1) % segments;
+
+ addShape(new SolidPolygon(
+ new Point3D(apex.x, apex.y, apex.z),
+ new Point3D(baseRing[next].x, baseRing[next].y, baseRing[next].z),
+ new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z),
+ color));
+ }
+
+ // Create base cap (circular bottom face)
+ // The base cap faces in +Y direction (downward, away from apex).
+ // Base ring vertices go CCW when viewed from above (+Y), so center → current → next
+ // maintains CCW for the cap when viewed from +Y (the correct direction).
+ for (int i = 0; i < segments; i++) {
+ final int next = (i + 1) % segments;
+ addShape(new SolidPolygon(
+ new Point3D(baseCenter.x, baseCenter.y, baseCenter.z),
+ new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z),
+ new Point3D(baseRing[next].x, baseRing[next].y, baseRing[next].z),
+ color));
+ }
+
+ setBackfaceCulling(true);
+ }
+
+ /**
+ * Creates a quaternion that rotates from the -Y axis to the given direction.
+ *
+ * <p>The cone by default points in the -Y direction (apex at origin, base at -Y).
+ * This method computes the rotation needed to align the cone with the target
+ * direction vector.</p>
+ *
+ * @param nx normalized direction X component
+ * @param ny normalized direction Y component
+ * @param nz normalized direction Z component
+ * @return quaternion representing the rotation
+ */
+ private Quaternion createRotationFromYAxis(final double nx, final double ny, final double nz) {
+ // Default direction is -Y (0, -1, 0)
+ // Target direction is (nx, ny, nz)
+ // Dot product: 0*nx + (-1)*ny + 0*nz = -ny
+ final double dot = -ny;
+
+ // Check for parallel vectors
+ if (dot > 0.9999) {
+ // Direction is nearly -Y, no rotation needed
+ return Quaternion.identity();
+ }
+ if (dot < -0.9999) {
+ // Direction is nearly +Y, rotate 180° around X axis
+ return Quaternion.fromAxisAngle(new Point3D(1, 0, 0), Math.PI);
+ }
+
+ // Cross product: (0, -1, 0) x (nx, ny, nz) = (-nz, 0, nx)
+ // This gives the rotation axis
+ final double axisX = -nz;
+ final double axisY = 0;
+ final double axisZ = nx;
+ final double axisLength = Math.sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ);
+ final double normalizedAxisX = axisX / axisLength;
+ final double normalizedAxisZ = axisZ / axisLength;
+
+ // Angle from dot product
+ final double angle = Math.acos(dot);
+
+ return Quaternion.fromAxisAngle(
+ new Point3D(normalizedAxisX, 0, normalizedAxisZ), angle);
+ }
+}
\ No newline at end of file
package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid;
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.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.
+ * A solid cylinder defined by two end points.
*
- * <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>The cylinder extends from startPoint to endPoint with circular caps at both
+ * ends. 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
+ * // Create a vertical cylinder from Y=100 to Y=200
* SolidPolygonCylinder cylinder = new SolidPolygonCylinder(
- * new Point3D(0, 0, 200), 50, 100, 16, Color.RED);
- * shapeCollection.addShape(cylinder);
+ * new Point3D(0, 100, 0), // start point (bottom)
+ * new Point3D(0, 200, 0), // end point (top)
+ * 10, // radius
+ * 16, // segments
+ * Color.RED // color
+ * );
+ *
+ * // Create a horizontal cylinder along X axis
+ * SolidPolygonCylinder pipe = new SolidPolygonCylinder(
+ * new Point3D(-50, 0, 0),
+ * new Point3D(50, 0, 0),
+ * 5, 12, Color.BLUE
+ * );
* }</pre>
*
- * @see SolidPolygonCube
- * @see SolidPolygonSphere
+ * @see SolidPolygonCone
+ * @see SolidPolygonArrow
* @see SolidPolygon
*/
public class SolidPolygonCylinder extends AbstractCompositeShape {
/**
- * Constructs a solid cylinder centered at the given point.
+ * Constructs a solid cylinder between two end points.
+ *
+ * <p>The cylinder has circular caps at both startPoint and endPoint,
+ * connected by a curved side surface. The orientation is automatically
+ * calculated from the direction between the two points.</p>
*
- * @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
+ * @param startPoint the center of the first cap
+ * @param endPoint the center of the second cap
+ * @param radius the radius 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,
+ public SolidPolygonCylinder(final Point3D startPoint, final Point3D endPoint,
+ final double radius, 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;
+ // Calculate direction and distance
+ final double dx = endPoint.x - startPoint.x;
+ final double dy = endPoint.y - startPoint.y;
+ final double dz = endPoint.z - startPoint.z;
+ final double distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
+
+ // Handle degenerate case: start and end are the same point
+ if (distance < 0.001) {
+ return;
+ }
+
+ // Normalize direction vector
+ final double nx = dx / distance;
+ final double ny = dy / distance;
+ final double nz = dz / distance;
+
+ // Calculate rotation to align Y-axis with direction
+ // Default cylinder is aligned along Y-axis
+ // We need to rotate from (0, 1, 0) to (nx, ny, nz)
+ final Quaternion rotation = createRotationFromYAxis(nx, ny, nz);
+ final Matrix3x3 rotMatrix = rotation.toMatrix();
- Point3D bottomCenter = new Point3D(center.x, bottomY, center.z);
- Point3D topCenter = new Point3D(center.x, topY, center.z);
+ // Cylinder center is at midpoint between start and end
+ final double centerX = (startPoint.x + endPoint.x) / 2.0;
+ final double centerY = (startPoint.y + endPoint.y) / 2.0;
+ final double centerZ = (startPoint.z + endPoint.z) / 2.0;
+ final double halfLength = distance / 2.0;
- Point3D[] bottomRing = new Point3D[segments];
- Point3D[] topRing = new Point3D[segments];
+ // Generate ring vertices in local space, then rotate and translate
+ // In local space: cylinder is aligned along Y-axis
+ // - startSideRing is at local -Y (toward startPoint)
+ // - endSideRing is at local +Y (toward endPoint)
+ final Point3D[] startSideRing = new Point3D[segments];
+ final Point3D[] endSideRing = 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);
+ final double angle = 2.0 * Math.PI * i / segments;
+ final double localX = radius * Math.cos(angle);
+ final double localZ = radius * Math.sin(angle);
+
+ // Start-side ring (at -halfLength in local Y = toward startPoint)
+ final Point3D startLocal = new Point3D(localX, -halfLength, localZ);
+ rotMatrix.transform(startLocal, startLocal);
+ startLocal.x += centerX;
+ startLocal.y += centerY;
+ startLocal.z += centerZ;
+ startSideRing[i] = startLocal;
+
+ // End-side ring (at +halfLength in local Y = toward endPoint)
+ final Point3D endLocal = new Point3D(localX, halfLength, localZ);
+ rotMatrix.transform(endLocal, endLocal);
+ endLocal.x += centerX;
+ endLocal.y += centerY;
+ endLocal.z += centerZ;
+ endSideRing[i] = endLocal;
}
+ // Create side faces (two triangles per segment)
+ // Winding: startSide → endSide → startSide+next, then startSide+next → endSide → endSide+next
+ // This creates CCW winding when viewed from outside the cylinder
for (int i = 0; i < segments; i++) {
- int next = (i + 1) % segments;
+ final 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),
+ new Point3D(startSideRing[i].x, startSideRing[i].y, startSideRing[i].z),
+ new Point3D(endSideRing[i].x, endSideRing[i].y, endSideRing[i].z),
+ new Point3D(startSideRing[next].x, startSideRing[next].y, startSideRing[next].z),
color));
addShape(new SolidPolygon(
- new Point3D(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),
+ new Point3D(startSideRing[next].x, startSideRing[next].y, startSideRing[next].z),
+ new Point3D(endSideRing[i].x, endSideRing[i].y, endSideRing[i].z),
+ new Point3D(endSideRing[next].x, endSideRing[next].y, endSideRing[next].z),
color));
+ }
+ // Create start cap (at startPoint, faces outward from cylinder)
+ // Winding: center → current → next creates CCW winding when viewed from outside
+ for (int i = 0; i < segments; i++) {
+ final int next = (i + 1) % segments;
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),
+ new Point3D(startPoint.x, startPoint.y, startPoint.z),
+ new Point3D(startSideRing[i].x, startSideRing[i].y, startSideRing[i].z),
+ new Point3D(startSideRing[next].x, startSideRing[next].y, startSideRing[next].z),
color));
+ }
+ // Create end cap (at endPoint, faces outward from cylinder)
+ // Winding: center → next → current creates CCW winding when viewed from outside
+ // (opposite to start cap because end cap faces the opposite direction)
+ for (int i = 0; i < segments; i++) {
+ final int next = (i + 1) % segments;
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),
+ new Point3D(endPoint.x, endPoint.y, endPoint.z),
+ new Point3D(endSideRing[next].x, endSideRing[next].y, endSideRing[next].z),
+ new Point3D(endSideRing[i].x, endSideRing[i].y, endSideRing[i].z),
color));
}
setBackfaceCulling(true);
}
+
+ /**
+ * Creates a quaternion that rotates from the +Y axis to the given direction.
+ *
+ * @param nx normalized direction X component
+ * @param ny normalized direction Y component
+ * @param nz normalized direction Z component
+ * @return quaternion representing the rotation
+ */
+ private Quaternion createRotationFromYAxis(final double nx, final double ny, final double nz) {
+ // Default direction is +Y (0, 1, 0)
+ // Target direction is (nx, ny, nz)
+ // Dot product: 0*nx + 1*ny + 0*nz = ny
+ final double dot = ny;
+
+ // Check for parallel vectors
+ if (dot > 0.9999) {
+ // Direction is nearly +Y, no rotation needed
+ return Quaternion.identity();
+ }
+ if (dot < -0.9999) {
+ // Direction is nearly -Y, rotate 180° around X axis
+ return Quaternion.fromAxisAngle(new Point3D(1, 0, 0), Math.PI);
+ }
+
+ // Cross product: (0, 1, 0) x (nx, ny, nz) = (nz, 0, -nx)
+ // This gives the rotation axis
+ final double axisX = nz;
+ final double axisY = 0;
+ final double axisZ = -nx;
+ final double axisLength = Math.sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ);
+ final double normalizedAxisX = axisX / axisLength;
+ final double normalizedAxisZ = axisZ / axisLength;
+
+ // Angle from dot product
+ final double angle = Math.acos(dot);
+
+ return Quaternion.fromAxisAngle(
+ new Point3D(normalizedAxisX, 0, normalizedAxisZ), angle);
+ }
}
\ No newline at end of file
package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid;
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.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.
+ * A solid square-based pyramid that can be oriented in any direction.
*
- * <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>The pyramid has a square base and four triangular faces meeting at an apex
+ * (tip). Two constructors are provided for different use cases:</p>
*
- * <p><b>Usage example:</b></p>
+ * <ul>
+ * <li><b>Directional (recommended):</b> Specify apex point and base center point.
+ * The pyramid points from apex toward the base center. This allows arbitrary
+ * orientation and is the most intuitive API.</li>
+ * <li><b>Y-axis aligned:</b> Specify base center, base size, and height. The pyramid
+ * points in -Y direction (apex at lower Y). Useful for simple vertical pyramids.</li>
+ * </ul>
+ *
+ * <p><b>Usage examples:</b></p>
* <pre>{@code
- * SolidPolygonPyramid pyramid = new SolidPolygonPyramid(
- * new Point3D(0, 0, 300), 50, 100, Color.BLUE);
- * shapeCollection.addShape(pyramid);
+ * // Directional constructor: pyramid pointing from apex toward base
+ * SolidPolygonPyramid directionalPyramid = new SolidPolygonPyramid(
+ * new Point3D(0, -100, 0), // apex (tip of the pyramid)
+ * new Point3D(0, 50, 0), // baseCenter (pyramid points toward this)
+ * 50, // baseSize (half-width of square base)
+ * Color.RED
+ * );
+ *
+ * // Y-axis aligned constructor: pyramid pointing upward
+ * SolidPolygonPyramid verticalPyramid = new SolidPolygonPyramid(
+ * new Point3D(0, 0, 300), // baseCenter
+ * 50, // baseSize (half-width of square base)
+ * 100, // height
+ * Color.BLUE
+ * );
* }</pre>
*
+ * @see SolidPolygonCone
* @see SolidPolygonCube
- * @see SolidPolygonSphere
* @see SolidPolygon
*/
public class SolidPolygonPyramid extends AbstractCompositeShape {
/**
- * Constructs a solid square-based pyramid with base centered at the given point.
+ * Constructs a solid square-based pyramid pointing from apex toward base center.
+ *
+ * <p>This is the recommended constructor for placing pyramids in 3D space.
+ * The pyramid's apex (tip) is at {@code apexPoint}, and the square base
+ * is centered at {@code baseCenter}. The pyramid points in the direction
+ * from apex to base center.</p>
+ *
+ * <p><b>Coordinate interpretation:</b></p>
+ * <ul>
+ * <li>{@code apexPoint} - the sharp tip of the pyramid</li>
+ * <li>{@code baseCenter} - the center of the square base; the pyramid
+ * "points" in this direction from the apex</li>
+ * <li>{@code baseSize} - half the width of the square base; the base
+ * extends this distance from the center along perpendicular axes</li>
+ * <li>The distance between apex and base center determines the pyramid height</li>
+ * </ul>
+ *
+ * @param apexPoint the position of the pyramid's tip (apex)
+ * @param baseCenter the center point of the square base; the pyramid
+ * points from apex toward this point
+ * @param baseSize the half-width of the square base; the base extends
+ * this distance from the center, giving a total base
+ * edge length of {@code 2 * baseSize}
+ * @param color the fill color applied to all faces of the pyramid
+ */
+ public SolidPolygonPyramid(final Point3D apexPoint, final Point3D baseCenter,
+ final double baseSize, final Color color) {
+ super();
+
+ // Calculate direction and height from apex to base center
+ final double dx = baseCenter.x - apexPoint.x;
+ final double dy = baseCenter.y - apexPoint.y;
+ final double dz = baseCenter.z - apexPoint.z;
+ final double height = Math.sqrt(dx * dx + dy * dy + dz * dz);
+
+ // Handle degenerate case: apex and base center are the same point
+ if (height < 0.001) {
+ return;
+ }
+
+ // Normalize direction vector (from apex toward base)
+ final double nx = dx / height;
+ final double ny = dy / height;
+ final double nz = dz / height;
+
+ // Calculate rotation to align Y-axis with direction
+ // Default pyramid points in -Y direction (apex at origin, base at -Y)
+ // We need to rotate from (0, -1, 0) to (nx, ny, nz)
+ final Quaternion rotation = createRotationFromYAxis(nx, ny, nz);
+ final Matrix3x3 rotMatrix = rotation.toMatrix();
+
+ // Generate base corner vertices in local space, then rotate and translate
+ // In local space: apex is at origin, base is at Y = -height
+ // Base corners form a square centered at (0, -height, 0)
+ final double h = baseSize;
+ final Point3D[] baseCorners = new Point3D[4];
+
+ // Local space corner positions (before rotation)
+ // Arranged clockwise when viewed from apex (from +Y)
+ final double[][] localCorners = {
+ {-h, -height, -h}, // corner 0: negative X, negative Z
+ {+h, -height, -h}, // corner 1: positive X, negative Z
+ {+h, -height, +h}, // corner 2: positive X, positive Z
+ {-h, -height, +h} // corner 3: negative X, positive Z
+ };
+
+ for (int i = 0; i < 4; i++) {
+ final Point3D local = new Point3D(localCorners[i][0], localCorners[i][1], localCorners[i][2]);
+ rotMatrix.transform(local, local);
+ local.x += apexPoint.x;
+ local.y += apexPoint.y;
+ local.z += apexPoint.z;
+ baseCorners[i] = local;
+ }
+
+ // Apex point (the pyramid tip)
+ final Point3D apex = new Point3D(apexPoint.x, apexPoint.y, apexPoint.z);
+
+ // Create the four triangular faces connecting apex to base edges
+ // Winding: next → current → apex creates CCW winding when viewed from outside
+ // (Base corners go CW when viewed from apex, so we reverse to get outward normals)
+ for (int i = 0; i < 4; i++) {
+ final int next = (i + 1) % 4;
+ addShape(new SolidPolygon(
+ new Point3D(baseCorners[next].x, baseCorners[next].y, baseCorners[next].z),
+ new Point3D(baseCorners[i].x, baseCorners[i].y, baseCorners[i].z),
+ new Point3D(apex.x, apex.y, apex.z),
+ color));
+ }
+
+ // Create base cap (square bottom face)
+ // The cap faces away from the apex (in the direction the pyramid points).
+ // Base corners go CW when viewed from apex, so CW when viewed from apex means
+ // CCW when viewed from outside (base side). Use CCW ordering for outward normal.
+ // Triangulate the square base: (center, 3, 0) and (center, 0, 1) and
+ // (center, 1, 2) and (center, 2, 3)
+ addShape(new SolidPolygon(
+ new Point3D(baseCenter.x, baseCenter.y, baseCenter.z),
+ new Point3D(baseCorners[3].x, baseCorners[3].y, baseCorners[3].z),
+ new Point3D(baseCorners[0].x, baseCorners[0].y, baseCorners[0].z),
+ color));
+ addShape(new SolidPolygon(
+ new Point3D(baseCenter.x, baseCenter.y, baseCenter.z),
+ new Point3D(baseCorners[0].x, baseCorners[0].y, baseCorners[0].z),
+ new Point3D(baseCorners[1].x, baseCorners[1].y, baseCorners[1].z),
+ color));
+ addShape(new SolidPolygon(
+ new Point3D(baseCenter.x, baseCenter.y, baseCenter.z),
+ new Point3D(baseCorners[1].x, baseCorners[1].y, baseCorners[1].z),
+ new Point3D(baseCorners[2].x, baseCorners[2].y, baseCorners[2].z),
+ color));
+ addShape(new SolidPolygon(
+ new Point3D(baseCenter.x, baseCenter.y, baseCenter.z),
+ new Point3D(baseCorners[2].x, baseCorners[2].y, baseCorners[2].z),
+ new Point3D(baseCorners[3].x, baseCorners[3].y, baseCorners[3].z),
+ color));
+
+ setBackfaceCulling(true);
+ }
+
+ /**
+ * Constructs a solid square-based pyramid with base centered at the given point,
+ * pointing in the -Y direction.
+ *
+ * <p>This constructor creates a Y-axis aligned pyramid. The apex is positioned
+ * at {@code baseCenter.y - height} (above the base in the negative Y direction).
+ * For pyramids pointing in arbitrary directions, use
+ * {@link #SolidPolygonPyramid(Point3D, Point3D, double, Color)} instead.</p>
+ *
+ * <p><b>Coordinate system:</b> The pyramid points in -Y direction (apex at lower Y).
+ * The base is at Y=baseCenter.y, and the apex is at Y=baseCenter.y - height.
+ * In Sixth 3D's coordinate system, "up" visually is negative Y.</p>
*
* @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
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);
+ // Base corners arranged clockwise when viewed from above (+Y)
+ // Naming: "negative/positive X" and "negative/positive Z" relative to base center
+ final Point3D negXnegZ = new Point3D(baseCenter.x - halfBase, baseY, baseCenter.z - halfBase);
+ final Point3D posXnegZ = new Point3D(baseCenter.x + halfBase, baseY, baseCenter.z - halfBase);
+ final Point3D posXposZ = new Point3D(baseCenter.x + halfBase, baseY, baseCenter.z + halfBase);
+ final Point3D negXposZ = new Point3D(baseCenter.x - halfBase, baseY, baseCenter.z + halfBase);
+ final 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));
+ // Four triangular faces from apex to base edges
+ // Winding: apex → current → next creates CCW when viewed from outside
+ addShape(new SolidPolygon(negXnegZ, posXnegZ, apex, color));
+ addShape(new SolidPolygon(posXnegZ, posXposZ, apex, color));
+ addShape(new SolidPolygon(posXposZ, negXposZ, apex, color));
+ addShape(new SolidPolygon(negXposZ, negXnegZ, apex, color));
- // Pyramid bottom
- addShape(new SolidPolygon( backLeft, backRight, frontLeft, color));
- addShape(new SolidPolygon( frontRight, frontLeft, backRight, color));
+ // Base cap (square bottom face)
+ // Cap faces +Y (downward, away from apex). The base is at higher Y than apex.
+ // Base corners go CW when viewed from apex (looking in +Y direction).
+ // For outward normal (+Y direction), we need CCW ordering when viewed from +Y.
+ // CCW from +Y is: 3 → 2 → 1 → 0, so triangles: (3, 2, 1) and (3, 1, 0)
+ addShape(new SolidPolygon(negXposZ, posXposZ, posXnegZ, color));
+ addShape(new SolidPolygon(negXposZ, posXnegZ, negXnegZ, color));
setBackfaceCulling(true);
}
+
+ /**
+ * Creates a quaternion that rotates from the -Y axis to the given direction.
+ *
+ * <p>The pyramid by default points in the -Y direction (apex at origin, base at -Y).
+ * This method computes the rotation needed to align the pyramid with the target
+ * direction vector.</p>
+ *
+ * @param nx normalized direction X component
+ * @param ny normalized direction Y component
+ * @param nz normalized direction Z component
+ * @return quaternion representing the rotation
+ */
+ private Quaternion createRotationFromYAxis(final double nx, final double ny, final double nz) {
+ // Default direction is -Y (0, -1, 0)
+ // Target direction is (nx, ny, nz)
+ // Dot product: 0*nx + (-1)*ny + 0*nz = -ny
+ final double dot = -ny;
+
+ // Check for parallel vectors
+ if (dot > 0.9999) {
+ // Direction is nearly -Y, no rotation needed
+ return Quaternion.identity();
+ }
+ if (dot < -0.9999) {
+ // Direction is nearly +Y, rotate 180° around X axis
+ return Quaternion.fromAxisAngle(new Point3D(1, 0, 0), Math.PI);
+ }
+
+ // Cross product: (0, -1, 0) x (nx, ny, nz) = (-nz, 0, nx)
+ // This gives the rotation axis
+ final double axisX = -nz;
+ final double axisY = 0;
+ final double axisZ = nx;
+ final double axisLength = Math.sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ);
+ final double normalizedAxisX = axisX / axisLength;
+ final double normalizedAxisZ = axisZ / axisLength;
+
+ // Angle from dot product
+ final double angle = Math.acos(dot);
+
+ return Quaternion.fromAxisAngle(
+ new Point3D(normalizedAxisX, 0, normalizedAxisZ), angle);
+ }
}
/**
* 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.
+ * covering all 6 faces).
*
- * <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 box is defined by two diagonally opposite corner points in 3D space.
+ * The box is axis-aligned, meaning its edges are parallel to the X, Y, and Z axes.</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>Vertex layout:</b></p>
+ * <pre>
+ * cornerB (max) ────────┐
+ * /│ /│
+ * / │ / │
+ * / │ / │
+ * ┌───┼───────────┐ │
+ * │ │ │ │
+ * │ │ │ │
+ * │ └───────────│───┘
+ * │ / │ /
+ * │ / │ /
+ * │/ │/
+ * └───────────────┘ cornerA (min)
+ * </pre>
*
- * <p><b>Usage example:</b></p>
- * <pre>{@code
- * // From center and size:
- * SolidPolygonRectangularBox box1 = new SolidPolygonRectangularBox(
- * new Point3D(0, 0, 200), 100, Color.RED);
+ * <p>The eight vertices are derived from the two corner points:</p>
+ * <ul>
+ * <li>Corner A defines minimum X, Y, Z</li>
+ * <li>Corner B defines maximum X, Y, Z</li>
+ * <li>The other 6 vertices are computed from combinations of these coordinates</li>
+ * </ul>
*
- * // From two corner points:
- * SolidPolygonRectangularBox box2 = new SolidPolygonRectangularBox(
- * new Point3D(-50, -25, 100), new Point3D(50, 25, 200), Color.BLUE);
+ * <p><b>Usage examples:</b></p>
+ * <pre>{@code
+ * // Create a box from two opposite corners
+ * SolidPolygonRectangularBox box = new SolidPolygonRectangularBox(
+ * new Point3D(-50, -25, 100), // cornerA (minimum X, Y, Z)
+ * new Point3D(50, 25, 200), // cornerB (maximum X, Y, Z)
+ * Color.BLUE
+ * );
*
- * shapeCollection.addShape(box1);
+ * // Create a cube using center + size (see SolidPolygonCube for convenience)
+ * double size = 50;
+ * SolidPolygonRectangularBox cube = new SolidPolygonRectangularBox(
+ * new Point3D(0 - size, 0 - size, 200 - size), // cornerA
+ * new Point3D(0 + size, 0 + size, 200 + size), // cornerB
+ * Color.RED
+ * );
* }</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.
+ * points in 3D space.
+ *
+ * <p>The box is axis-aligned and fills the rectangular region between the
+ * two corners. The corner points do not need to be ordered (cornerA can have
+ * larger coordinates than cornerB); the constructor will determine the actual
+ * min/max bounds automatically.</p>
*
- * @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
+ * @param cornerA the first corner point (any of the 8 corners)
+ * @param cornerB the diagonally opposite corner point
+ * @param color the fill color applied to all 12 triangular polygons
*/
- public SolidPolygonRectangularBox(final Point3D p1, final Point3D p7, final Color color) {
+ public SolidPolygonRectangularBox(final Point3D cornerA, final Point3D cornerB, 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);
+ // Determine actual min/max bounds (corners may be in any order)
+ final double minX = Math.min(cornerA.x, cornerB.x);
+ final double maxX = Math.max(cornerA.x, cornerB.x);
+ final double minY = Math.min(cornerA.y, cornerB.y);
+ final double maxY = Math.max(cornerA.y, cornerB.y);
+ final double minZ = Math.min(cornerA.z, cornerB.z);
+ final double maxZ = Math.max(cornerA.z, cornerB.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);
+ // Compute all 8 vertices from the bounds
+ // Naming convention: min/max indicates which bound the coordinate uses
+ // minMinMin = (minX, minY, minZ), maxMaxMax = (maxX, maxY, maxZ), etc.
+ final Point3D minMinMin = new Point3D(minX, minY, minZ);
+ final Point3D maxMinMin = new Point3D(maxX, minY, minZ);
+ final Point3D maxMinMax = new Point3D(maxX, minY, maxZ);
+ final Point3D minMinMax = new Point3D(minX, minY, maxZ);
+
+ final Point3D minMaxMin = new Point3D(minX, maxY, minZ);
+ final Point3D maxMaxMin = new Point3D(maxX, maxY, minZ);
+ final Point3D minMaxMax = new Point3D(minX, maxY, maxZ);
+ final Point3D maxMaxMax = new Point3D(maxX, maxY, maxZ);
// Bottom face (y = minY)
- addShape(new SolidPolygon(p1, p2, p3, color));
- addShape(new SolidPolygon(p1, p3, p4, color));
+ addShape(new SolidPolygon(minMinMin, maxMinMin, maxMinMax, color));
+ addShape(new SolidPolygon(minMinMin, maxMinMax, minMinMax, color));
// Top face (y = maxY)
- addShape(new SolidPolygon(p5, p8, p7, color));
- addShape(new SolidPolygon(p5, p7, p6, color));
+ addShape(new SolidPolygon(minMaxMin, minMaxMax, maxMaxMax, color));
+ addShape(new SolidPolygon(minMaxMin, maxMaxMax, maxMaxMin, color));
// Front face (z = minZ)
- addShape(new SolidPolygon(p1, p5, p6, color));
- addShape(new SolidPolygon(p1, p6, p2, color));
+ addShape(new SolidPolygon(minMinMin, minMaxMin, maxMaxMin, color));
+ addShape(new SolidPolygon(minMinMin, maxMaxMin, maxMinMin, color));
// Back face (z = maxZ)
- addShape(new SolidPolygon(p3, p7, p8, color));
- addShape(new SolidPolygon(p3, p8, p4, color));
+ addShape(new SolidPolygon(maxMinMax, maxMaxMax, minMaxMax, color));
+ addShape(new SolidPolygon(maxMinMax, minMaxMax, minMinMax, color));
// Left face (x = minX)
- addShape(new SolidPolygon(p1, p4, p8, color));
- addShape(new SolidPolygon(p1, p8, p5, color));
+ addShape(new SolidPolygon(minMinMin, minMinMax, minMaxMax, color));
+ addShape(new SolidPolygon(minMinMin, minMaxMax, minMaxMin, color));
// Right face (x = maxX)
- addShape(new SolidPolygon(p2, p6, p7, color));
- addShape(new SolidPolygon(p2, p7, p3, color));
+ addShape(new SolidPolygon(maxMinMin, maxMaxMin, maxMaxMax, color));
+ addShape(new SolidPolygon(maxMinMin, maxMaxMax, maxMinMax, color));
setBackfaceCulling(true);
}
-
}
/**
* 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.
+ * diagonally 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>
+ * (one along each axis), forming a three-dimensional lattice.</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);
+ * Point3D cornerA = new Point3D(-100, -100, -100);
+ * Point3D cornerB = new Point3D(100, 100, 100);
+ * Grid3D grid = new Grid3D(cornerA, cornerB, 50, appearance);
* shapeCollection.addShape(grid);
* }</pre>
*
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.
+ * Constructs a 3D grid filling the volume between two diagonally opposite
+ * corner points.
*
- * @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
+ * <p>The corner points do not need to be in any particular min/max order;
+ * the constructor automatically normalizes them so that grid generation
+ * always proceeds from minimum to maximum coordinates.</p>
+ *
+ * @param cornerA the first corner point defining the volume
+ * @param cornerB the diagonally opposite corner point
+ * @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,
+ public Grid3D(final Point3D cornerA, final Point3D cornerB, final double step,
final LineAppearance appearance) {
super();
- final Point3D p1 = new Point3D(p1t);
- final Point3D p2 = new Point3D(p2t);
+ // Determine actual min/max bounds (corners may be in any order)
+ final double minX = Math.min(cornerA.x, cornerB.x);
+ final double maxX = Math.max(cornerA.x, cornerB.x);
+ final double minY = Math.min(cornerA.y, cornerB.y);
+ final double maxY = Math.max(cornerA.y, cornerB.y);
+ final double minZ = Math.min(cornerA.z, cornerB.z);
+ final double maxZ = Math.max(cornerA.z, cornerB.z);
- if (p1.x > p2.x) {
- final double tmp = p1.x;
- p1.x = p2.x;
- p2.x = tmp;
- }
+ for (double x = minX; x <= maxX; x += step) {
+ for (double y = minY; y <= maxY; y += step) {
+ for (double z = minZ; z <= maxZ; z += step) {
- 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;
- }
+ final Point3D p = new Point3D(x, y, z);
- 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));
+ // Line along X axis
+ if ((x + step) <= maxX) {
+ addShape(appearance.getLine(p, new Point3D(x + step, y, z)));
}
- if ((y + step) <= p2.y) {
- final Point3D point3d3 = new Point3D(x, y + step, z);
- addShape(appearance.getLine(p3, point3d3));
+ // Line along Y axis
+ if ((y + step) <= maxY) {
+ addShape(appearance.getLine(p, new Point3D(x, y + step, z)));
}
- if ((z + step) <= p2.z) {
- final Point3D point3d4 = new Point3D(x, y, z + step);
- addShape(appearance.getLine(p3, point3d4));
+ // Line along Z axis
+ if ((z + step) <= maxZ) {
+ addShape(appearance.getLine(p, new Point3D(x, y, z + step)));
}
-
}
+ }
+ }
}
}
--- /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.wireframe;
+
+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.renderer.raster.shapes.basic.line.LineAppearance;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape;
+
+/**
+ * A 3D wireframe arrow shape composed of a cylindrical body and a conical tip.
+ *
+ * <p>The arrow points from a start point to an end point, with the tip
+ * located at the end point. The wireframe consists of:</p>
+ * <ul>
+ * <li><b>Body:</b> Two circular rings connected by lines between corresponding vertices</li>
+ * <li><b>Tip:</b> A circular ring at the cone base with lines to the apex</li>
+ * </ul>
+ *
+ * <p><b>Usage example:</b></p>
+ * <pre>{@code
+ * // Create a red arrow pointing from origin to (100, -50, 200)
+ * LineAppearance appearance = new LineAppearance(2, Color.RED);
+ * WireframeArrow arrow = new WireframeArrow(
+ * new Point3D(0, 0, 0), // start point
+ * new Point3D(100, -50, 200), // end point
+ * 8, // body radius
+ * 20, // tip radius
+ * 40, // tip length
+ * 16, // segments
+ * appearance
+ * );
+ * shapeCollection.addShape(arrow);
+ * }</pre>
+ *
+ * @see WireframeCone
+ * @see WireframeCylinder
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonArrow
+ */
+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) {
+ super();
+
+ // Calculate direction and distance
+ final double dx = endPoint.x - startPoint.x;
+ final double dy = endPoint.y - startPoint.y;
+ final double dz = endPoint.z - startPoint.z;
+ final double distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
+
+ // Handle degenerate case: start and end are the same point
+ if (distance < 0.001) {
+ return;
+ }
+
+ // Normalize direction vector
+ final double nx = dx / distance;
+ final double ny = dy / distance;
+ final double nz = dz / distance;
+
+ // Calculate rotation to align Y-axis with direction
+ // Default arrow points in -Y direction (apex at lower Y)
+ // We need to rotate from (0, -1, 0) to (nx, ny, nz)
+ final Quaternion rotation = createRotationFromYAxis(nx, ny, nz);
+ final Matrix3x3 rotMatrix = rotation.toMatrix();
+
+ // Calculate body length (distance minus tip)
+ final double bodyLength = Math.max(0, distance - tipLength);
+
+ // Build the arrow components
+ if (bodyLength > 0) {
+ addCylinderBody(startPoint, bodyRadius, bodyLength, segments, appearance, rotMatrix, nx, ny, nz);
+ }
+ addConeTip(endPoint, tipRadius, tipLength, segments, appearance, rotMatrix, nx, ny, nz);
+ }
+
+ /**
+ * Creates a quaternion that rotates from the -Y axis to the given direction.
+ *
+ * <p>The arrow by default points in the -Y direction. This method computes
+ * the rotation needed to align the arrow with the target direction vector.</p>
+ *
+ * @param nx normalized direction X component
+ * @param ny normalized direction Y component
+ * @param nz normalized direction Z component
+ * @return quaternion representing the rotation
+ */
+ private Quaternion createRotationFromYAxis(final double nx, final double ny, final double nz) {
+ // Default direction is -Y (0, -1, 0)
+ // Target direction is (nx, ny, nz)
+ // Dot product: 0*nx + (-1)*ny + 0*nz = -ny
+ final double dot = -ny;
+
+ // Check for parallel vectors
+ if (dot > 0.9999) {
+ // Direction is nearly -Y, no rotation needed
+ return Quaternion.identity();
+ }
+ if (dot < -0.9999) {
+ // Direction is nearly +Y, rotate 180° around X axis
+ return Quaternion.fromAxisAngle(new Point3D(1, 0, 0), Math.PI);
+ }
+
+ // Cross product: (0, -1, 0) x (nx, ny, nz) = (-nz, 0, nx)
+ // This gives the rotation axis
+ final double axisX = -nz;
+ final double axisY = 0;
+ final double axisZ = nx;
+ final double axisLength = Math.sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ);
+ final double normalizedAxisX = axisX / axisLength;
+ final double normalizedAxisZ = axisZ / axisLength;
+
+ // Angle from dot product
+ final double angle = Math.acos(dot);
+
+ return Quaternion.fromAxisAngle(
+ new Point3D(normalizedAxisX, 0, normalizedAxisZ), angle);
+ }
+
+ /**
+ * Adds the cylindrical body of the arrow.
+ *
+ * <p><b>Local coordinate system:</b> The arrow points in -Y direction in local space.
+ * Therefore, local -Y is toward the tip (front), and local +Y is toward the start (back).</p>
+ *
+ * @param startPoint the origin of the arrow body
+ * @param radius the radius of the cylinder
+ * @param length the length of the cylinder
+ * @param segments the number of segments around the circumference
+ * @param appearance the line appearance
+ * @param rotMatrix the rotation matrix to apply
+ * @param dirX direction X component (for translation calculation)
+ * @param dirY direction Y component
+ * @param dirZ direction Z component
+ */
+ private void addCylinderBody(final Point3D startPoint, final double radius,
+ final double length, final int segments,
+ final LineAppearance appearance, final Matrix3x3 rotMatrix,
+ final double dirX, final double dirY, final double dirZ) {
+ // Cylinder center is at startPoint + (length/2) * direction
+ final double centerX = startPoint.x + (length / 2.0) * dirX;
+ final double centerY = startPoint.y + (length / 2.0) * dirY;
+ final double centerZ = startPoint.z + (length / 2.0) * dirZ;
+
+ // Generate ring vertices in local space, then rotate and translate
+ // Arrow points in -Y direction, so:
+ // - tipSideRing is at local -Y (toward arrow tip, front of cylinder)
+ // - startSideRing is at local +Y (toward arrow start, back of cylinder)
+ final Point3D[] tipSideRing = new Point3D[segments];
+ final Point3D[] startSideRing = new Point3D[segments];
+
+ final double halfLength = length / 2.0;
+
+ for (int i = 0; i < segments; i++) {
+ final double angle = 2.0 * Math.PI * i / segments;
+ final double localX = radius * Math.cos(angle);
+ final double localZ = radius * Math.sin(angle);
+
+ // Tip-side ring (at -halfLength in local Y = toward arrow tip)
+ final Point3D tipSideLocal = new Point3D(localX, -halfLength, localZ);
+ rotMatrix.transform(tipSideLocal, tipSideLocal);
+ tipSideLocal.x += centerX;
+ tipSideLocal.y += centerY;
+ tipSideLocal.z += centerZ;
+ tipSideRing[i] = tipSideLocal;
+
+ // Start-side ring (at +halfLength in local Y = toward arrow start)
+ final Point3D startSideLocal = new Point3D(localX, halfLength, localZ);
+ rotMatrix.transform(startSideLocal, startSideLocal);
+ startSideLocal.x += centerX;
+ startSideLocal.y += centerY;
+ startSideLocal.z += centerZ;
+ startSideRing[i] = startSideLocal;
+ }
+
+ // Create the circular rings
+ for (int i = 0; i < segments; i++) {
+ final int next = (i + 1) % segments;
+
+ // Tip-side ring line segment
+ addShape(appearance.getLine(
+ new Point3D(tipSideRing[i].x, tipSideRing[i].y, tipSideRing[i].z),
+ new Point3D(tipSideRing[next].x, tipSideRing[next].y, tipSideRing[next].z)));
+
+ // Start-side ring line segment
+ addShape(appearance.getLine(
+ new Point3D(startSideRing[i].x, startSideRing[i].y, startSideRing[i].z),
+ new Point3D(startSideRing[next].x, startSideRing[next].y, startSideRing[next].z)));
+ }
+
+ // Create vertical lines connecting the two rings
+ for (int i = 0; i < segments; i++) {
+ addShape(appearance.getLine(
+ new Point3D(tipSideRing[i].x, tipSideRing[i].y, tipSideRing[i].z),
+ new Point3D(startSideRing[i].x, startSideRing[i].y, startSideRing[i].z)));
+ }
+ }
+
+ /**
+ * Adds the conical tip of the arrow.
+ *
+ * <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 appearance the line appearance
+ * @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,
+ final LineAppearance appearance, final Matrix3x3 rotMatrix,
+ final double dirX, final double dirY, final double dirZ) {
+ // Apex is at endPoint (the arrow tip)
+ // Base center is at endPoint - length * direction (toward arrow start)
+ final double baseCenterX = endPoint.x - length * dirX;
+ final double baseCenterY = endPoint.y - length * dirY;
+ final double baseCenterZ = endPoint.z - length * dirZ;
+
+ // Generate base ring vertices
+ // In local space, cone points in -Y direction, so base is at Y=0
+ final Point3D[] baseRing = new Point3D[segments];
+
+ for (int i = 0; i < segments; i++) {
+ final double angle = 2.0 * Math.PI * i / segments;
+ final double localX = radius * Math.cos(angle);
+ final double localZ = radius * Math.sin(angle);
+
+ // Base ring vertices at local Y=0
+ final Point3D local = new Point3D(localX, 0, localZ);
+ rotMatrix.transform(local, local);
+ local.x += baseCenterX;
+ local.y += baseCenterY;
+ local.z += baseCenterZ;
+ baseRing[i] = local;
+ }
+
+ // Apex point (the arrow tip)
+ final Point3D apex = new Point3D(endPoint.x, endPoint.y, endPoint.z);
+
+ // Create the circular base ring
+ for (int i = 0; i < segments; i++) {
+ final int next = (i + 1) % segments;
+ addShape(appearance.getLine(
+ new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z),
+ new Point3D(baseRing[next].x, baseRing[next].y, baseRing[next].z)));
+ }
+
+ // Create lines from apex to each base vertex
+ for (int i = 0; i < segments; i++) {
+ addShape(appearance.getLine(
+ new Point3D(apex.x, apex.y, apex.z),
+ new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z)));
+ }
+ }
+}
\ No newline at end of file
/**
* 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.
+ * diagonally 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>Vertex layout:</b></p>
+ * <pre>
+ * cornerB (max) ────────┐
+ * /│ /│
+ * / │ / │
+ * / │ / │
+ * ┌───┼───────────┐ │
+ * │ │ │ │
+ * │ │ │ │
+ * │ └───────────│───┘
+ * │ / │ /
+ * │ / │ /
+ * │/ │/
+ * └───────────────┘ cornerA (min)
+ * </pre>
+ *
* <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);
+ * Point3D cornerA = new Point3D(-50, -50, -50);
+ * Point3D cornerB = new Point3D(50, 50, 50);
+ * WireframeBox box = new WireframeBox(cornerA, cornerB, appearance);
* shapeCollection.addShape(box);
* }</pre>
*
}
/**
- * 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.
+ * Constructs a wireframe box from two diagonally 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 cornerA the first corner point of the box
+ * @param cornerB 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,
+ public WireframeBox(final Point3D cornerA, final Point3D cornerB,
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)));
+ // Determine actual min/max bounds (corners may be in any order)
+ final double minX = Math.min(cornerA.x, cornerB.x);
+ final double maxX = Math.max(cornerA.x, cornerB.x);
+ final double minY = Math.min(cornerA.y, cornerB.y);
+ final double maxY = Math.max(cornerA.y, cornerB.y);
+ final double minZ = Math.min(cornerA.z, cornerB.z);
+ final double maxZ = Math.max(cornerA.z, cornerB.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)));
+ // Generate the 12 edges of the box
+ // Four edges along X axis (varying X, fixed Y and Z)
+ addShape(appearance.getLine(new Point3D(minX, minY, minZ), new Point3D(maxX, minY, minZ)));
+ addShape(appearance.getLine(new Point3D(minX, maxY, minZ), new Point3D(maxX, maxY, minZ)));
+ addShape(appearance.getLine(new Point3D(minX, minY, maxZ), new Point3D(maxX, minY, maxZ)));
+ addShape(appearance.getLine(new Point3D(minX, maxY, maxZ), new Point3D(maxX, maxY, maxZ)));
- 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)));
- }
+ // Four edges along Y axis (varying Y, fixed X and Z)
+ addShape(appearance.getLine(new Point3D(minX, minY, minZ), new Point3D(minX, maxY, minZ)));
+ addShape(appearance.getLine(new Point3D(maxX, minY, minZ), new Point3D(maxX, maxY, minZ)));
+ addShape(appearance.getLine(new Point3D(minX, minY, maxZ), new Point3D(minX, maxY, maxZ)));
+ addShape(appearance.getLine(new Point3D(maxX, minY, maxZ), new Point3D(maxX, maxY, maxZ)));
+ // Four edges along Z axis (varying Z, fixed X and Y)
+ addShape(appearance.getLine(new Point3D(minX, minY, minZ), new Point3D(minX, minY, maxZ)));
+ addShape(appearance.getLine(new Point3D(maxX, minY, minZ), new Point3D(maxX, minY, maxZ)));
+ addShape(appearance.getLine(new Point3D(minX, maxY, minZ), new Point3D(minX, maxY, maxZ)));
+ addShape(appearance.getLine(new Point3D(maxX, maxY, minZ), new Point3D(maxX, maxY, maxZ)));
+ }
}
--- /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.wireframe;
+
+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.renderer.raster.shapes.basic.line.LineAppearance;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape;
+
+/**
+ * A wireframe cone that can be oriented in any direction.
+ *
+ * <p>The cone has a circular base and a single apex (tip) point. The wireframe
+ * consists of:</p>
+ * <ul>
+ * <li>A circular ring at the base</li>
+ * <li>Lines from each base vertex to the apex</li>
+ * </ul>
+ *
+ * <p>Two constructors are provided for different use cases:</p>
+ *
+ * <ul>
+ * <li><b>Directional (recommended):</b> Specify apex point and base center point.
+ * The cone points from apex toward the base center. This allows arbitrary
+ * orientation and is the most intuitive API.</li>
+ * <li><b>Y-axis aligned:</b> Specify base center, radius, and height. The cone
+ * points in -Y direction (apex at lower Y). Useful for simple vertical cones.</li>
+ * </ul>
+ *
+ * <p><b>Usage examples:</b></p>
+ * <pre>{@code
+ * // Directional constructor: cone pointing from apex toward base
+ * LineAppearance appearance = new LineAppearance(2, Color.RED);
+ * WireframeCone directionalCone = new WireframeCone(
+ * new Point3D(0, -100, 0), // apex (tip of the cone)
+ * new Point3D(0, 50, 0), // baseCenter (cone points toward this)
+ * 50, // radius of the circular base
+ * 16, // segments
+ * appearance
+ * );
+ *
+ * // Y-axis aligned constructor: cone pointing upward
+ * WireframeCone verticalCone = new WireframeCone(
+ * new Point3D(0, 0, 300), // baseCenter
+ * 50, // radius
+ * 100, // height
+ * 16, // segments
+ * appearance
+ * );
+ * }</pre>
+ *
+ * @see WireframeCylinder
+ * @see WireframeArrow
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonCone
+ */
+public class WireframeCone extends AbstractCompositeShape {
+
+ /**
+ * Constructs a wireframe cone pointing from apex toward base center.
+ *
+ * <p>This is the recommended constructor for placing cones in 3D space.
+ * The cone's apex (tip) is at {@code apexPoint}, and the circular base
+ * is centered at {@code baseCenterPoint}. The cone points in the direction
+ * from apex to base center.</p>
+ *
+ * <p><b>Coordinate interpretation:</b></p>
+ * <ul>
+ * <li>{@code apexPoint} - the sharp tip of the cone</li>
+ * <li>{@code baseCenterPoint} - the center of the circular base; the cone
+ * "points" in this direction from the apex</li>
+ * <li>The distance between apex and base center determines the cone height</li>
+ * </ul>
+ *
+ * @param apexPoint the position of the cone's tip (apex)
+ * @param baseCenterPoint the center point of the circular base; the cone
+ * points from apex toward this point
+ * @param radius the radius of the circular base
+ * @param segments the number of segments around the circumference.
+ * Higher values create smoother cones. Minimum is 3.
+ * @param appearance the line appearance (color, width) used for all lines
+ */
+ public WireframeCone(final Point3D apexPoint, final Point3D baseCenterPoint,
+ final double radius, final int segments,
+ final LineAppearance appearance) {
+ super();
+
+ // Calculate direction and height from apex to base center
+ final double dx = baseCenterPoint.x - apexPoint.x;
+ final double dy = baseCenterPoint.y - apexPoint.y;
+ final double dz = baseCenterPoint.z - apexPoint.z;
+ final double height = Math.sqrt(dx * dx + dy * dy + dz * dz);
+
+ // Handle degenerate case: apex and base center are the same point
+ if (height < 0.001) {
+ return;
+ }
+
+ // Normalize direction vector (from apex toward base)
+ final double nx = dx / height;
+ final double ny = dy / height;
+ final double nz = dz / height;
+
+ // Calculate rotation to align Y-axis with direction
+ // Default cone points in -Y direction (apex at origin, base at -Y)
+ // We need to rotate from (0, -1, 0) to (nx, ny, nz)
+ final Quaternion rotation = createRotationFromYAxis(nx, ny, nz);
+ final Matrix3x3 rotMatrix = rotation.toMatrix();
+
+ // Generate base ring vertices in local space, then rotate and translate
+ // In local space: apex is at origin, base is at Y = -height
+ // (cone points in -Y direction in local space)
+ final Point3D[] baseRing = new Point3D[segments];
+
+ for (int i = 0; i < segments; i++) {
+ final double angle = 2.0 * Math.PI * i / segments;
+ final double localX = radius * Math.cos(angle);
+ final double localZ = radius * Math.sin(angle);
+
+ // Base ring vertex in local space (Y = -height)
+ final Point3D local = new Point3D(localX, -height, localZ);
+ rotMatrix.transform(local, local);
+ local.x += apexPoint.x;
+ local.y += apexPoint.y;
+ local.z += apexPoint.z;
+ baseRing[i] = local;
+ }
+
+ // Apex point (the cone tip)
+ final Point3D apex = new Point3D(apexPoint.x, apexPoint.y, apexPoint.z);
+
+ // Create the circular base ring
+ for (int i = 0; i < segments; i++) {
+ final int next = (i + 1) % segments;
+ addShape(appearance.getLine(
+ new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z),
+ new Point3D(baseRing[next].x, baseRing[next].y, baseRing[next].z)));
+ }
+
+ // Create lines from apex to each base vertex
+ for (int i = 0; i < segments; i++) {
+ addShape(appearance.getLine(
+ new Point3D(apex.x, apex.y, apex.z),
+ new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z)));
+ }
+ }
+
+ /**
+ * Constructs a wireframe cone with circular base centered at the given point,
+ * pointing in the -Y direction.
+ *
+ * <p>This constructor creates a Y-axis aligned cone. The apex is positioned
+ * at {@code baseCenter.y - height} (above the base in the negative Y direction).
+ * For cones pointing in arbitrary directions, use
+ * {@link #WireframeCone(Point3D, Point3D, double, int, LineAppearance)} instead.</p>
+ *
+ * <p><b>Coordinate system:</b> The cone points in -Y direction (apex at lower Y).
+ * The base is at Y=baseCenter.y, and the apex is at Y=baseCenter.y - height.
+ * In Sixth 3D's coordinate system, "up" visually is negative Y.</p>
+ *
+ * @param baseCenter the center point of the cone's circular base in 3D space
+ * @param radius the radius of the circular base
+ * @param height the height of the cone from base center to apex
+ * @param segments the number of segments around the circumference.
+ * Higher values create smoother cones. Minimum is 3.
+ * @param appearance the line appearance (color, width) used for all lines
+ */
+ public WireframeCone(final Point3D baseCenter, final double radius,
+ final double height, final int segments,
+ final LineAppearance appearance) {
+ super();
+
+ // Apex is above the base (negative Y direction in this coordinate system)
+ final double apexY = baseCenter.y - height;
+ final Point3D apex = new Point3D(baseCenter.x, apexY, baseCenter.z);
+
+ // Generate vertices around the circular base
+ final Point3D[] baseRing = new Point3D[segments];
+ for (int i = 0; i < segments; i++) {
+ final double angle = 2.0 * Math.PI * i / segments;
+ final double x = baseCenter.x + radius * Math.cos(angle);
+ final double z = baseCenter.z + radius * Math.sin(angle);
+ baseRing[i] = new Point3D(x, baseCenter.y, z);
+ }
+
+ // Create the circular base ring
+ for (int i = 0; i < segments; i++) {
+ final int next = (i + 1) % segments;
+ addShape(appearance.getLine(
+ new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z),
+ new Point3D(baseRing[next].x, baseRing[next].y, baseRing[next].z)));
+ }
+
+ // Create lines from apex to each base vertex
+ for (int i = 0; i < segments; i++) {
+ addShape(appearance.getLine(
+ new Point3D(apex.x, apex.y, apex.z),
+ new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z)));
+ }
+ }
+
+ /**
+ * Creates a quaternion that rotates from the -Y axis to the given direction.
+ *
+ * <p>The cone by default points in the -Y direction (apex at origin, base at -Y).
+ * This method computes the rotation needed to align the cone with the target
+ * direction vector.</p>
+ *
+ * @param nx normalized direction X component
+ * @param ny normalized direction Y component
+ * @param nz normalized direction Z component
+ * @return quaternion representing the rotation
+ */
+ private Quaternion createRotationFromYAxis(final double nx, final double ny, final double nz) {
+ // Default direction is -Y (0, -1, 0)
+ // Target direction is (nx, ny, nz)
+ // Dot product: 0*nx + (-1)*ny + 0*nz = -ny
+ final double dot = -ny;
+
+ // Check for parallel vectors
+ if (dot > 0.9999) {
+ // Direction is nearly -Y, no rotation needed
+ return Quaternion.identity();
+ }
+ if (dot < -0.9999) {
+ // Direction is nearly +Y, rotate 180° around X axis
+ return Quaternion.fromAxisAngle(new Point3D(1, 0, 0), Math.PI);
+ }
+
+ // Cross product: (0, -1, 0) x (nx, ny, nz) = (-nz, 0, nx)
+ // This gives the rotation axis
+ final double axisX = -nz;
+ final double axisY = 0;
+ final double axisZ = nx;
+ final double axisLength = Math.sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ);
+ final double normalizedAxisX = axisX / axisLength;
+ final double normalizedAxisZ = axisZ / axisLength;
+
+ // Angle from dot product
+ final double angle = Math.acos(dot);
+
+ return Quaternion.fromAxisAngle(
+ new Point3D(normalizedAxisX, 0, normalizedAxisZ), angle);
+ }
+}
\ 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.renderer.raster.shapes.composite.wireframe;
+
+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.renderer.raster.shapes.basic.line.LineAppearance;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape;
+
+/**
+ * A wireframe cylinder defined by two end points.
+ *
+ * <p>The cylinder extends from startPoint to endPoint with circular rings at both
+ * ends. The number of segments determines the smoothness of the circular rings.
+ * The wireframe consists of:</p>
+ * <ul>
+ * <li>Two circular rings at the start and end points</li>
+ * <li>Vertical lines connecting corresponding vertices between the rings</li>
+ * </ul>
+ *
+ * <p><b>Usage example:</b></p>
+ * <pre>{@code
+ * // Create a vertical cylinder from Y=100 to Y=200
+ * LineAppearance appearance = new LineAppearance(2, Color.RED);
+ * WireframeCylinder cylinder = new WireframeCylinder(
+ * new Point3D(0, 100, 0), // start point (bottom)
+ * new Point3D(0, 200, 0), // end point (top)
+ * 10, // radius
+ * 16, // segments
+ * appearance
+ * );
+ *
+ * // Create a horizontal cylinder along X axis
+ * WireframeCylinder pipe = new WireframeCylinder(
+ * new Point3D(-50, 0, 0),
+ * new Point3D(50, 0, 0),
+ * 5, 12, appearance
+ * );
+ * }</pre>
+ *
+ * @see WireframeCone
+ * @see WireframeArrow
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonCylinder
+ */
+public class WireframeCylinder extends AbstractCompositeShape {
+
+ /**
+ * Constructs a wireframe cylinder between two end points.
+ *
+ * <p>The cylinder has circular rings at both startPoint and endPoint,
+ * connected by lines between corresponding vertices. The orientation is
+ * automatically calculated from the direction between the two points.</p>
+ *
+ * @param startPoint the center of the first ring
+ * @param endPoint the center of the second ring
+ * @param radius the radius of the cylinder
+ * @param segments the number of segments around the circumference.
+ * Higher values create smoother cylinders. Minimum is 3.
+ * @param appearance the line appearance (color, width) used for all lines
+ */
+ public WireframeCylinder(final Point3D startPoint, final Point3D endPoint,
+ final double radius, final int segments,
+ final LineAppearance appearance) {
+ super();
+
+ // Calculate direction and distance
+ final double dx = endPoint.x - startPoint.x;
+ final double dy = endPoint.y - startPoint.y;
+ final double dz = endPoint.z - startPoint.z;
+ final double distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
+
+ // Handle degenerate case: start and end are the same point
+ if (distance < 0.001) {
+ return;
+ }
+
+ // Normalize direction vector
+ final double nx = dx / distance;
+ final double ny = dy / distance;
+ final double nz = dz / distance;
+
+ // Calculate rotation to align Y-axis with direction
+ // Default cylinder is aligned along Y-axis
+ // We need to rotate from (0, 1, 0) to (nx, ny, nz)
+ final Quaternion rotation = createRotationFromYAxis(nx, ny, nz);
+ final Matrix3x3 rotMatrix = rotation.toMatrix();
+
+ // Cylinder center is at midpoint between start and end
+ final double centerX = (startPoint.x + endPoint.x) / 2.0;
+ final double centerY = (startPoint.y + endPoint.y) / 2.0;
+ final double centerZ = (startPoint.z + endPoint.z) / 2.0;
+ final double halfLength = distance / 2.0;
+
+ // Generate ring vertices in local space, then rotate and translate
+ // In local space: cylinder is aligned along Y-axis
+ // - startRing is at local -Y (toward startPoint)
+ // - endRing is at local +Y (toward endPoint)
+ final Point3D[] startRing = new Point3D[segments];
+ final Point3D[] endRing = new Point3D[segments];
+
+ for (int i = 0; i < segments; i++) {
+ final double angle = 2.0 * Math.PI * i / segments;
+ final double localX = radius * Math.cos(angle);
+ final double localZ = radius * Math.sin(angle);
+
+ // Start ring (at -halfLength in local Y = toward startPoint)
+ final Point3D startLocal = new Point3D(localX, -halfLength, localZ);
+ rotMatrix.transform(startLocal, startLocal);
+ startLocal.x += centerX;
+ startLocal.y += centerY;
+ startLocal.z += centerZ;
+ startRing[i] = startLocal;
+
+ // End ring (at +halfLength in local Y = toward endPoint)
+ final Point3D endLocal = new Point3D(localX, halfLength, localZ);
+ rotMatrix.transform(endLocal, endLocal);
+ endLocal.x += centerX;
+ endLocal.y += centerY;
+ endLocal.z += centerZ;
+ endRing[i] = endLocal;
+ }
+
+ // Create the circular rings
+ for (int i = 0; i < segments; i++) {
+ final int next = (i + 1) % segments;
+
+ // Start ring line segment
+ addShape(appearance.getLine(
+ new Point3D(startRing[i].x, startRing[i].y, startRing[i].z),
+ new Point3D(startRing[next].x, startRing[next].y, startRing[next].z)));
+
+ // End ring line segment
+ addShape(appearance.getLine(
+ new Point3D(endRing[i].x, endRing[i].y, endRing[i].z),
+ new Point3D(endRing[next].x, endRing[next].y, endRing[next].z)));
+ }
+
+ // Create vertical lines connecting the two rings
+ for (int i = 0; i < segments; i++) {
+ addShape(appearance.getLine(
+ new Point3D(startRing[i].x, startRing[i].y, startRing[i].z),
+ new Point3D(endRing[i].x, endRing[i].y, endRing[i].z)));
+ }
+ }
+
+ /**
+ * Creates a quaternion that rotates from the +Y axis to the given direction.
+ *
+ * @param nx normalized direction X component
+ * @param ny normalized direction Y component
+ * @param nz normalized direction Z component
+ * @return quaternion representing the rotation
+ */
+ private Quaternion createRotationFromYAxis(final double nx, final double ny, final double nz) {
+ // Default direction is +Y (0, 1, 0)
+ // Target direction is (nx, ny, nz)
+ // Dot product: 0*nx + 1*ny + 0*nz = ny
+ final double dot = ny;
+
+ // Check for parallel vectors
+ if (dot > 0.9999) {
+ // Direction is nearly +Y, no rotation needed
+ return Quaternion.identity();
+ }
+ if (dot < -0.9999) {
+ // Direction is nearly -Y, rotate 180° around X axis
+ return Quaternion.fromAxisAngle(new Point3D(1, 0, 0), Math.PI);
+ }
+
+ // Cross product: (0, 1, 0) x (nx, ny, nz) = (nz, 0, -nx)
+ // This gives the rotation axis
+ final double axisX = nz;
+ final double axisY = 0;
+ final double axisZ = -nx;
+ final double axisLength = Math.sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ);
+ final double normalizedAxisX = axisX / axisLength;
+ final double normalizedAxisZ = axisZ / axisLength;
+
+ // Angle from dot product
+ final double angle = Math.acos(dot);
+
+ return Quaternion.fromAxisAngle(
+ new Point3D(normalizedAxisX, 0, normalizedAxisZ), angle);
+ }
+}
\ 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.renderer.raster.shapes.composite.wireframe;
+
+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.renderer.raster.shapes.basic.line.LineAppearance;
+import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape;
+
+/**
+ * A wireframe square-based pyramid that can be oriented in any direction.
+ *
+ * <p>The pyramid has a square base and four triangular faces meeting at an apex
+ * (tip). The wireframe consists of:</p>
+ * <ul>
+ * <li>Four lines forming the square base</li>
+ * <li>Four lines from each base corner to the apex</li>
+ * </ul>
+ *
+ * <p>Two constructors are provided for different use cases:</p>
+ *
+ * <ul>
+ * <li><b>Directional (recommended):</b> Specify apex point and base center point.
+ * The pyramid points from apex toward the base center. This allows arbitrary
+ * orientation and is the most intuitive API.</li>
+ * <li><b>Y-axis aligned:</b> Specify base center, base size, and height. The pyramid
+ * points in -Y direction (apex at lower Y). Useful for simple vertical pyramids.</li>
+ * </ul>
+ *
+ * <p><b>Usage examples:</b></p>
+ * <pre>{@code
+ * // Directional constructor: pyramid pointing from apex toward base
+ * LineAppearance appearance = new LineAppearance(2, Color.RED);
+ * WireframePyramid directionalPyramid = new WireframePyramid(
+ * new Point3D(0, -100, 0), // apex (tip of the pyramid)
+ * new Point3D(0, 50, 0), // baseCenter (pyramid points toward this)
+ * 50, // baseSize (half-width of square base)
+ * appearance
+ * );
+ *
+ * // Y-axis aligned constructor: pyramid pointing upward
+ * WireframePyramid verticalPyramid = new WireframePyramid(
+ * new Point3D(0, 0, 300), // baseCenter
+ * 50, // baseSize (half-width of square base)
+ * 100, // height
+ * appearance
+ * );
+ * }</pre>
+ *
+ * @see WireframeCone
+ * @see WireframeCube
+ * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonPyramid
+ */
+public class WireframePyramid extends AbstractCompositeShape {
+
+ /**
+ * Constructs a wireframe square-based pyramid pointing from apex toward base center.
+ *
+ * <p>This is the recommended constructor for placing pyramids in 3D space.
+ * The pyramid's apex (tip) is at {@code apexPoint}, and the square base
+ * is centered at {@code baseCenter}. The pyramid points in the direction
+ * from apex to base center.</p>
+ *
+ * <p><b>Coordinate interpretation:</b></p>
+ * <ul>
+ * <li>{@code apexPoint} - the sharp tip of the pyramid</li>
+ * <li>{@code baseCenter} - the center of the square base; the pyramid
+ * "points" in this direction from the apex</li>
+ * <li>{@code baseSize} - half the width of the square base; the base
+ * extends this distance from the center along perpendicular axes</li>
+ * <li>The distance between apex and base center determines the pyramid height</li>
+ * </ul>
+ *
+ * @param apexPoint the position of the pyramid's tip (apex)
+ * @param baseCenter the center point of the square base; the pyramid
+ * points from apex toward this point
+ * @param baseSize the half-width of the square base; the base extends
+ * this distance from the center, giving a total base
+ * edge length of {@code 2 * baseSize}
+ * @param appearance the line appearance (color, width) used for all lines
+ */
+ public WireframePyramid(final Point3D apexPoint, final Point3D baseCenter,
+ final double baseSize, final LineAppearance appearance) {
+ super();
+
+ // Calculate direction and height from apex to base center
+ final double dx = baseCenter.x - apexPoint.x;
+ final double dy = baseCenter.y - apexPoint.y;
+ final double dz = baseCenter.z - apexPoint.z;
+ final double height = Math.sqrt(dx * dx + dy * dy + dz * dz);
+
+ // Handle degenerate case: apex and base center are the same point
+ if (height < 0.001) {
+ return;
+ }
+
+ // Normalize direction vector (from apex toward base)
+ final double nx = dx / height;
+ final double ny = dy / height;
+ final double nz = dz / height;
+
+ // Calculate rotation to align Y-axis with direction
+ // Default pyramid points in -Y direction (apex at origin, base at -Y)
+ // We need to rotate from (0, -1, 0) to (nx, ny, nz)
+ final Quaternion rotation = createRotationFromYAxis(nx, ny, nz);
+ final Matrix3x3 rotMatrix = rotation.toMatrix();
+
+ // Generate base corner vertices in local space, then rotate and translate
+ // In local space: apex is at origin, base is at Y = -height
+ // Base corners form a square centered at (0, -height, 0)
+ final double h = baseSize;
+ final Point3D[] baseCorners = new Point3D[4];
+
+ // Local space corner positions (before rotation)
+ // Arranged counter-clockwise when viewed from apex (from +Y)
+ final double[][] localCorners = {
+ {-h, -height, -h}, // corner 0: negative X, negative Z
+ {+h, -height, -h}, // corner 1: positive X, negative Z
+ {+h, -height, +h}, // corner 2: positive X, positive Z
+ {-h, -height, +h} // corner 3: negative X, positive Z
+ };
+
+ for (int i = 0; i < 4; i++) {
+ final Point3D local = new Point3D(localCorners[i][0], localCorners[i][1], localCorners[i][2]);
+ rotMatrix.transform(local, local);
+ local.x += apexPoint.x;
+ local.y += apexPoint.y;
+ local.z += apexPoint.z;
+ baseCorners[i] = local;
+ }
+
+ // Apex point (the pyramid tip)
+ final Point3D apex = new Point3D(apexPoint.x, apexPoint.y, apexPoint.z);
+
+ // Create the four lines forming the square base
+ for (int i = 0; i < 4; i++) {
+ final int next = (i + 1) % 4;
+ addShape(appearance.getLine(
+ new Point3D(baseCorners[i].x, baseCorners[i].y, baseCorners[i].z),
+ new Point3D(baseCorners[next].x, baseCorners[next].y, baseCorners[next].z)));
+ }
+
+ // Create the four lines from apex to each base corner
+ for (int i = 0; i < 4; i++) {
+ addShape(appearance.getLine(
+ new Point3D(apex.x, apex.y, apex.z),
+ new Point3D(baseCorners[i].x, baseCorners[i].y, baseCorners[i].z)));
+ }
+ }
+
+ /**
+ * Constructs a wireframe square-based pyramid with base centered at the given point,
+ * pointing in the -Y direction.
+ *
+ * <p>This constructor creates a Y-axis aligned pyramid. The apex is positioned
+ * at {@code baseCenter.y - height} (above the base in the negative Y direction).
+ * For pyramids pointing in arbitrary directions, use
+ * {@link #WireframePyramid(Point3D, Point3D, double, LineAppearance)} instead.</p>
+ *
+ * <p><b>Coordinate system:</b> The pyramid points in -Y direction (apex at lower Y).
+ * The base is at Y=baseCenter.y, and the apex is at Y=baseCenter.y - height.
+ * In Sixth 3D's coordinate system, "up" visually is negative Y.</p>
+ *
+ * @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 appearance the line appearance (color, width) used for all lines
+ */
+ public WireframePyramid(final Point3D baseCenter, final double baseSize,
+ final double height, final LineAppearance appearance) {
+ super();
+
+ final double halfBase = baseSize;
+ final double apexY = baseCenter.y - height;
+ final double baseY = baseCenter.y;
+
+ // Base corners arranged counter-clockwise when viewed from above (+Y)
+ // Naming: "negative/positive X" and "negative/positive Z" relative to base center
+ final Point3D negXnegZ = new Point3D(baseCenter.x - halfBase, baseY, baseCenter.z - halfBase);
+ final Point3D posXnegZ = new Point3D(baseCenter.x + halfBase, baseY, baseCenter.z - halfBase);
+ final Point3D posXposZ = new Point3D(baseCenter.x + halfBase, baseY, baseCenter.z + halfBase);
+ final Point3D negXposZ = new Point3D(baseCenter.x - halfBase, baseY, baseCenter.z + halfBase);
+ final Point3D apex = new Point3D(baseCenter.x, apexY, baseCenter.z);
+
+ // Create the four lines forming the square base
+ addShape(appearance.getLine(negXnegZ, posXnegZ));
+ addShape(appearance.getLine(posXnegZ, posXposZ));
+ addShape(appearance.getLine(posXposZ, negXposZ));
+ addShape(appearance.getLine(negXposZ, negXnegZ));
+
+ // Create the four lines from apex to each base corner
+ addShape(appearance.getLine(apex, negXnegZ));
+ addShape(appearance.getLine(apex, posXnegZ));
+ addShape(appearance.getLine(apex, posXposZ));
+ addShape(appearance.getLine(apex, negXposZ));
+ }
+
+ /**
+ * Creates a quaternion that rotates from the -Y axis to the given direction.
+ *
+ * <p>The pyramid by default points in the -Y direction (apex at origin, base at -Y).
+ * This method computes the rotation needed to align the pyramid with the target
+ * direction vector.</p>
+ *
+ * @param nx normalized direction X component
+ * @param ny normalized direction Y component
+ * @param nz normalized direction Z component
+ * @return quaternion representing the rotation
+ */
+ private Quaternion createRotationFromYAxis(final double nx, final double ny, final double nz) {
+ // Default direction is -Y (0, -1, 0)
+ // Target direction is (nx, ny, nz)
+ // Dot product: 0*nx + (-1)*ny + 0*nz = -ny
+ final double dot = -ny;
+
+ // Check for parallel vectors
+ if (dot > 0.9999) {
+ // Direction is nearly -Y, no rotation needed
+ return Quaternion.identity();
+ }
+ if (dot < -0.9999) {
+ // Direction is nearly +Y, rotate 180° around X axis
+ return Quaternion.fromAxisAngle(new Point3D(1, 0, 0), Math.PI);
+ }
+
+ // Cross product: (0, -1, 0) x (nx, ny, nz) = (-nz, 0, nx)
+ // This gives the rotation axis
+ final double axisX = -nz;
+ final double axisY = 0;
+ final double axisZ = nx;
+ final double axisLength = Math.sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ);
+ final double normalizedAxisX = axisX / axisLength;
+ final double normalizedAxisZ = axisZ / axisLength;
+
+ // Angle from dot product
+ final double angle = Math.acos(dot);
+
+ return Quaternion.fromAxisAngle(
+ new Point3D(normalizedAxisX, 0, normalizedAxisZ), angle);
+ }
+}
\ No newline at end of file