96900ae749bbfbee461ee0ffa6f35adf433120dd
[sixth-3d.git] /
1 /*
2  * Sixth 3D engine. Author: Svjatoslav Agejenko.
3  * This project is released under Creative Commons Zero (CC0) license.
4  */
5 package eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.wireframe;
6
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;
12
13 /**
14  * A 3D wireframe arrow shape composed of a cylindrical body and a conical tip.
15  *
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>
18  * <ul>
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>
21  * </ul>
22  *
23  * <p><b>Usage example:</b></p>
24  * <pre>{@code
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
30  *     8,                         // body radius
31  *     20,                        // tip radius
32  *     40,                        // tip length
33  *     16,                        // segments
34  *     appearance
35  * );
36  * shapeCollection.addShape(arrow);
37  * }</pre>
38  *
39  * @see WireframeCone
40  * @see WireframeCylinder
41  * @see eu.svjatoslav.sixth.e3d.renderer.raster.shapes.composite.solid.SolidPolygonArrow
42  */
43 public class WireframeArrow extends AbstractCompositeShape {
44
45     /**
46      * Constructs a 3D wireframe arrow pointing from start to end.
47      *
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
51      * is rendered.</p>
52      *
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
61      */
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) {
66         super();
67
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);
73
74         // Handle degenerate case: start and end are the same point
75         if (distance < 0.001) {
76             return;
77         }
78
79         // Normalize direction vector
80         final double nx = dx / distance;
81         final double ny = dy / distance;
82         final double nz = dz / distance;
83
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();
89
90         // Calculate body length (distance minus tip)
91         final double bodyLength = Math.max(0, distance - tipLength);
92
93         // Build the arrow components
94         if (bodyLength > 0) {
95             addCylinderBody(startPoint, bodyRadius, bodyLength, segments, appearance, rotMatrix, nx, ny, nz);
96         }
97         addConeTip(endPoint, tipRadius, tipLength, segments, appearance, rotMatrix, nx, ny, nz);
98     }
99
100     /**
101      * Creates a quaternion that rotates from the -Y axis to the given direction.
102      *
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>
105      *
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
110      */
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;
116
117         // Check for parallel vectors
118         if (dot > 0.9999) {
119             // Direction is nearly -Y, no rotation needed
120             return Quaternion.identity();
121         }
122         if (dot < -0.9999) {
123             // Direction is nearly +Y, rotate 180° around X axis
124             return Quaternion.fromAxisAngle(new Point3D(1, 0, 0), Math.PI);
125         }
126
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;
135
136         // Angle from dot product
137         final double angle = Math.acos(dot);
138
139         return Quaternion.fromAxisAngle(
140                 new Point3D(normalizedAxisX, 0, normalizedAxisZ), angle);
141     }
142
143     /**
144      * Adds the cylindrical body of the arrow.
145      *
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>
148      *
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
158      */
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;
167
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];
174
175         final double halfLength = length / 2.0;
176
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);
181
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;
189
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;
197         }
198
199         // Create the circular rings
200         for (int i = 0; i < segments; i++) {
201             final int next = (i + 1) % segments;
202
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)));
207
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)));
212         }
213
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)));
219         }
220     }
221
222     /**
223      * Adds the conical tip of the arrow.
224      *
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>
227      *
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
237      */
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;
247
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];
251
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);
256
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;
263             baseRing[i] = local;
264         }
265
266         // Apex point (the arrow tip)
267         final Point3D apex = new Point3D(endPoint.x, endPoint.y, endPoint.z);
268
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)));
275         }
276
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)));
282         }
283     }
284 }