2 * Sixth 3D engine. Author: Svjatoslav Agejenko.
3 * This project is released under Creative Commons Zero (CC0) license.
5 package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.wireframe;
7 import eu.svjatoslav.sixth.e3d.geometry.Point3D;
8 import eu.svjatoslav.sixth.e3d.math.Matrix3x3;
9 import eu.svjatoslav.sixth.e3d.math.Quaternion;
10 import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.basic.line.LineAppearance;
11 import eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.base.AbstractCompositeShape;
14 * A 3D wireframe arrow shape composed of a cylindrical body and a conical tip.
16 * <p>The arrow points from a start point to an end point, with the tip
17 * located at the end point. The wireframe consists of:</p>
19 * <li><b>Body:</b> Two circular rings connected by lines between corresponding vertices</li>
20 * <li><b>Tip:</b> A circular ring at the cone base with lines to the apex</li>
23 * <p><b>Usage example:</b></p>
25 * // Create a red arrow pointing from origin to (100, -50, 200)
26 * LineAppearance appearance = new LineAppearance(2, Color.RED);
27 * WireframeArrow arrow = new WireframeArrow(
28 * new Point3D(0, 0, 0), // start point
29 * new Point3D(100, -50, 200), // end point
36 * shapeCollection.addShape(arrow);
40 * @see WireframeCylinder
41 * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonArrow
43 public class WireframeArrow extends AbstractCompositeShape {
46 * Constructs a 3D wireframe arrow pointing from start to end.
48 * <p>The arrow consists of a cylindrical body extending from the start point
49 * towards the end, and a conical tip at the end point. If the distance between
50 * start and end is less than or equal to the tip length, only the cone tip
53 * @param startPoint the origin point of the arrow (where the body starts)
54 * @param endPoint the destination point of the arrow (where the tip points to)
55 * @param bodyRadius the radius of the cylindrical body
56 * @param tipRadius the radius of the cone base at the tip
57 * @param tipLength the length of the conical tip
58 * @param segments the number of segments for cylinder and cone smoothness.
59 * Higher values create smoother arrows. Minimum is 3.
60 * @param appearance the line appearance (color, width) used for all lines
62 public WireframeArrow(final Point3D startPoint, final Point3D endPoint,
63 final double bodyRadius, final double tipRadius,
64 final double tipLength, final int segments,
65 final LineAppearance appearance) {
68 // Calculate direction and distance
69 final double dx = endPoint.x - startPoint.x;
70 final double dy = endPoint.y - startPoint.y;
71 final double dz = endPoint.z - startPoint.z;
72 final double distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
74 // Handle degenerate case: start and end are the same point
75 if (distance < 0.001) {
79 // Normalize direction vector
80 final double nx = dx / distance;
81 final double ny = dy / distance;
82 final double nz = dz / distance;
84 // Calculate rotation to align Y-axis with direction
85 // Default arrow points in -Y direction (apex at lower Y)
86 // We need to rotate from (0, -1, 0) to (nx, ny, nz)
87 final Quaternion rotation = createRotationFromYAxis(nx, ny, nz);
88 final Matrix3x3 rotMatrix = rotation.toMatrix();
90 // Calculate body length (distance minus tip)
91 final double bodyLength = Math.max(0, distance - tipLength);
93 // Build the arrow components
95 addCylinderBody(startPoint, bodyRadius, bodyLength, segments, appearance, rotMatrix, nx, ny, nz);
97 addConeTip(endPoint, tipRadius, tipLength, segments, appearance, rotMatrix, nx, ny, nz);
101 * Creates a quaternion that rotates from the -Y axis to the given direction.
103 * <p>The arrow by default points in the -Y direction. This method computes
104 * the rotation needed to align the arrow with the target direction vector.</p>
106 * @param nx normalized direction X component
107 * @param ny normalized direction Y component
108 * @param nz normalized direction Z component
109 * @return quaternion representing the rotation
111 private Quaternion createRotationFromYAxis(final double nx, final double ny, final double nz) {
112 // Default direction is -Y (0, -1, 0)
113 // Target direction is (nx, ny, nz)
114 // Dot product: 0*nx + (-1)*ny + 0*nz = -ny
115 final double dot = -ny;
117 // Check for parallel vectors
119 // Direction is nearly -Y, no rotation needed
120 return Quaternion.identity();
123 // Direction is nearly +Y, rotate 180° around X axis
124 return Quaternion.fromAxisAngle(new Point3D(1, 0, 0), Math.PI);
127 // Cross product: (0, -1, 0) x (nx, ny, nz) = (-nz, 0, nx)
128 // This gives the rotation axis
129 final double axisX = -nz;
130 final double axisY = 0;
131 final double axisZ = nx;
132 final double axisLength = Math.sqrt(axisX * axisX + axisY * axisY + axisZ * axisZ);
133 final double normalizedAxisX = axisX / axisLength;
134 final double normalizedAxisZ = axisZ / axisLength;
136 // Angle from dot product
137 final double angle = Math.acos(dot);
139 return Quaternion.fromAxisAngle(
140 new Point3D(normalizedAxisX, 0, normalizedAxisZ), angle);
144 * Adds the cylindrical body of the arrow.
146 * <p><b>Local coordinate system:</b> The arrow points in -Y direction in local space.
147 * Therefore, local -Y is toward the tip (front), and local +Y is toward the start (back).</p>
149 * @param startPoint the origin of the arrow body
150 * @param radius the radius of the cylinder
151 * @param length the length of the cylinder
152 * @param segments the number of segments around the circumference
153 * @param appearance the line appearance
154 * @param rotMatrix the rotation matrix to apply
155 * @param dirX direction X component (for translation calculation)
156 * @param dirY direction Y component
157 * @param dirZ direction Z component
159 private void addCylinderBody(final Point3D startPoint, final double radius,
160 final double length, final int segments,
161 final LineAppearance appearance, final Matrix3x3 rotMatrix,
162 final double dirX, final double dirY, final double dirZ) {
163 // Cylinder center is at startPoint + (length/2) * direction
164 final double centerX = startPoint.x + (length / 2.0) * dirX;
165 final double centerY = startPoint.y + (length / 2.0) * dirY;
166 final double centerZ = startPoint.z + (length / 2.0) * dirZ;
168 // Generate ring vertices in local space, then rotate and translate
169 // Arrow points in -Y direction, so:
170 // - tipSideRing is at local -Y (toward arrow tip, front of cylinder)
171 // - startSideRing is at local +Y (toward arrow start, back of cylinder)
172 final Point3D[] tipSideRing = new Point3D[segments];
173 final Point3D[] startSideRing = new Point3D[segments];
175 final double halfLength = length / 2.0;
177 for (int i = 0; i < segments; i++) {
178 final double angle = 2.0 * Math.PI * i / segments;
179 final double localX = radius * Math.cos(angle);
180 final double localZ = radius * Math.sin(angle);
182 // Tip-side ring (at -halfLength in local Y = toward arrow tip)
183 final Point3D tipSideLocal = new Point3D(localX, -halfLength, localZ);
184 rotMatrix.transform(tipSideLocal, tipSideLocal);
185 tipSideLocal.x += centerX;
186 tipSideLocal.y += centerY;
187 tipSideLocal.z += centerZ;
188 tipSideRing[i] = tipSideLocal;
190 // Start-side ring (at +halfLength in local Y = toward arrow start)
191 final Point3D startSideLocal = new Point3D(localX, halfLength, localZ);
192 rotMatrix.transform(startSideLocal, startSideLocal);
193 startSideLocal.x += centerX;
194 startSideLocal.y += centerY;
195 startSideLocal.z += centerZ;
196 startSideRing[i] = startSideLocal;
199 // Create the circular rings
200 for (int i = 0; i < segments; i++) {
201 final int next = (i + 1) % segments;
203 // Tip-side ring line segment
204 addShape(appearance.getLine(
205 new Point3D(tipSideRing[i].x, tipSideRing[i].y, tipSideRing[i].z),
206 new Point3D(tipSideRing[next].x, tipSideRing[next].y, tipSideRing[next].z)));
208 // Start-side ring line segment
209 addShape(appearance.getLine(
210 new Point3D(startSideRing[i].x, startSideRing[i].y, startSideRing[i].z),
211 new Point3D(startSideRing[next].x, startSideRing[next].y, startSideRing[next].z)));
214 // Create vertical lines connecting the two rings
215 for (int i = 0; i < segments; i++) {
216 addShape(appearance.getLine(
217 new Point3D(tipSideRing[i].x, tipSideRing[i].y, tipSideRing[i].z),
218 new Point3D(startSideRing[i].x, startSideRing[i].y, startSideRing[i].z)));
223 * Adds the conical tip of the arrow.
225 * <p><b>Local coordinate system:</b> In local space, the cone points in -Y direction
226 * (apex at lower Y). The base ring is at Y=0, and the apex is at Y=-length.</p>
228 * @param endPoint the position of the arrow tip (cone apex)
229 * @param radius the radius of the cone base
230 * @param length the length of the cone
231 * @param segments the number of segments around the circumference
232 * @param appearance the line appearance
233 * @param rotMatrix the rotation matrix to apply
234 * @param dirX direction X component
235 * @param dirY direction Y component
236 * @param dirZ direction Z component
238 private void addConeTip(final Point3D endPoint, final double radius,
239 final double length, final int segments,
240 final LineAppearance appearance, final Matrix3x3 rotMatrix,
241 final double dirX, final double dirY, final double dirZ) {
242 // Apex is at endPoint (the arrow tip)
243 // Base center is at endPoint - length * direction (toward arrow start)
244 final double baseCenterX = endPoint.x - length * dirX;
245 final double baseCenterY = endPoint.y - length * dirY;
246 final double baseCenterZ = endPoint.z - length * dirZ;
248 // Generate base ring vertices
249 // In local space, cone points in -Y direction, so base is at Y=0
250 final Point3D[] baseRing = new Point3D[segments];
252 for (int i = 0; i < segments; i++) {
253 final double angle = 2.0 * Math.PI * i / segments;
254 final double localX = radius * Math.cos(angle);
255 final double localZ = radius * Math.sin(angle);
257 // Base ring vertices at local Y=0
258 final Point3D local = new Point3D(localX, 0, localZ);
259 rotMatrix.transform(local, local);
260 local.x += baseCenterX;
261 local.y += baseCenterY;
262 local.z += baseCenterZ;
266 // Apex point (the arrow tip)
267 final Point3D apex = new Point3D(endPoint.x, endPoint.y, endPoint.z);
269 // Create the circular base ring
270 for (int i = 0; i < segments; i++) {
271 final int next = (i + 1) % segments;
272 addShape(appearance.getLine(
273 new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z),
274 new Point3D(baseRing[next].x, baseRing[next].y, baseRing[next].z)));
277 // Create lines from apex to each base vertex
278 for (int i = 0; i < segments; i++) {
279 addShape(appearance.getLine(
280 new Point3D(apex.x, apex.y, apex.z),
281 new Point3D(baseRing[i].x, baseRing[i].y, baseRing[i].z)));