001/* ===========================================================
002 * Orson Charts : a 3D chart library for the Java(tm) platform
003 * ===========================================================
004 * 
005 * (C)opyright 2013-2022, by David Gilbert.  All rights reserved.
006 * 
007 * https://github.com/jfree/orson-charts
008 * 
009 * This program is free software: you can redistribute it and/or modify
010 * it under the terms of the GNU General Public License as published by
011 * the Free Software Foundation, either version 3 of the License, or
012 * (at your option) any later version.
013 *
014 * This program is distributed in the hope that it will be useful,
015 * but WITHOUT ANY WARRANTY; without even the implied warranty of
016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
017 * GNU General Public License for more details.
018 *
019 * You should have received a copy of the GNU General Public License
020 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
021 * 
022 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
023 * Other names may be trademarks of their respective owners.]
024 * 
025 * If you do not wish to be bound by the terms of the GPL, an alternative
026 * commercial license can be purchased.  For details, please see visit the
027 * Orson Charts home page:
028 * 
029 * http://www.object-refinery.com/orsoncharts/index.html
030 * 
031 */
032
033package org.jfree.chart3d.graphics3d;
034
035import java.awt.Color;
036import java.awt.Font;
037import java.awt.geom.Point2D;
038import java.util.ArrayList;
039import java.util.HashMap;
040import java.util.List;
041import java.util.Map;
042import org.jfree.chart3d.graphics3d.internal.TaggedFace;
043import org.jfree.chart3d.internal.Args;
044
045/**
046 * An object defined in 3D space by (a) a list of coordinates, and (b) a list
047 * of faces.  This class has methods to calculate projected points in 2D when
048 * a {@link ViewPoint3D} is provided.
049 * <br><br>
050 * This class also contains a collection of static methods for constructing
051 * common 3D objects.
052 */
053public class Object3D {
054
055    /**
056     * The key for storing the object class as an optional property for this 
057     * object.
058     * 
059     * @since 1.4
060     */
061    public static final String CLASS_KEY = "class";
062    
063    /**
064     * The key for storing item keys as property values.
065     * 
066     * @since 1.3
067     */
068    public static final String ITEM_KEY = "key";
069
070    /** 
071     * A prefix used for setting color properties for an object.
072     * 
073     * @since 1.3
074     */
075    public static final String COLOR_PREFIX = "color/";
076    
077    /** World coordinates. */
078    private List<Point3D> vertices;
079
080    /** Faces for the object, specified by indices to the world coords. */
081    private List<Face> faces;
082    
083    /** The primary color for the object. */
084    private Color color;
085    
086    /** 
087     * A flag that indicates whether or not faces for this object have their
088     * outlines drawn (that is, the shape is filled then drawn versus just 
089     * filled only).
090     */
091    private boolean outline;
092 
093    /**
094     * A map containing properties for the object.  If there are no properties
095     * defined, then we leave this as {@code null} as an empty map would 
096     * consume memory unnecessarily.
097     */
098    private Map<String, Object> properties;
099    
100    /**
101     * Creates a new object, initially with no vertices or faces.
102     * 
103     * @param color  the default face color ({@code null} not permitted).
104     * 
105     * @since 1.3
106     */
107    public Object3D(Color color) {
108        this(color, false);
109    }
110    
111    /**
112     * Creates a new object, initially with no vertices or faces.
113     * 
114     * @param color  the default face color ({@code null} not permitted).
115     * @param outline  the default flag that determines whether face outlines
116     *     are drawn.
117     * 
118     * @since 1.3
119     */
120    public Object3D(Color color, boolean outline) {
121        Args.nullNotPermitted(color, "color");
122        this.color = color;
123        this.outline = outline;
124        this.vertices = new java.util.ArrayList<>();
125        this.faces = new java.util.ArrayList<>();
126    }
127
128    /**
129     * Returns the default face color as specified in the constructor.
130     * 
131     * @return The color (never {@code null}).
132     * 
133     * @since 1.3
134     */
135    public Color getColor() {
136        return this.color;
137    }
138    
139    /**
140     * Returns the outline flag.
141     * 
142     * @return The outline flag.
143     * 
144     * @since 1.3
145     */
146    public boolean getOutline() {
147        return this.outline;
148    }
149    
150    /**
151     * Sets the outline flag.  This determines the default setting for whether
152     * or not the faces of this object have their outlines drawn when rendered.
153     * 
154     * @param outline  the new flag value. 
155     * 
156     * @since 1.3
157     */
158    public void setOutline(boolean outline) {
159        this.outline = outline;
160    }
161    
162    /**
163     * Returns the value of the property with the specified key, or 
164     * {@code null} if there is no property defined for that key.
165     * 
166     * @param key  the property key ({@code null} not permitted).
167     * 
168     * @return The value (possibly {@code null}).
169     * 
170     * @since 1.3
171     */
172    public Object getProperty(String key) {
173        Args.nullNotPermitted(key, "key");
174        if (this.properties == null) {
175            return null;
176        } else {
177            return this.properties.get(key);
178        }
179    }
180
181    /**
182     * Sets the value of a property, overwriting any existing value.  One 
183     * application for this is storing item key references to link a 3D object
184     * back to the data item that it represents (the key for this is
185     * {@link Object3D#ITEM_KEY}).
186     * 
187     * @param key  the key ({@code null} not permitted).
188     * @param value  the value ({@code null} permitted).
189     * 
190     * @since 1.3
191     */
192    public void setProperty(String key, Object value) {
193        Args.nullNotPermitted(key, "key");
194        if (this.properties == null) {
195            this.properties = new HashMap<>();
196        }
197        this.properties.put(key, value);
198    }
199    
200    /**
201     * Returns the color for a specific face.  If the face has a tag, then
202     * this method will look for a property with the key COLOR_PREFIX + tag
203     * and return that color, otherwise it returns the default color for the
204     * object.
205     * 
206     * @param face  the face ({@code null} not permitted).
207     * 
208     * @return The color for the specified face (never {@code null}).
209     * 
210     * @since 1.3
211     */
212    public Color getColor(Face face) {
213        if (face.getTag() != null) {
214            // see if there is a custom color defined for the tag
215            Object obj = getProperty(COLOR_PREFIX + face.getTag());
216            if (obj != null) {
217                return (Color) obj;
218            }
219        }
220        return this.color;
221    }
222    
223    /**
224     * Returns {@code true} if an outline should be drawn for the 
225     * specified face, and {@code false} otherwise.
226     * 
227     * @param face  the face ({@code null} not permitted).
228     * 
229     * @return A boolean.
230     * 
231     * @since 1.3
232     */
233    public boolean getOutline(Face face) {
234        return this.outline;
235    }
236    
237    /**
238     * Returns the number of vertices for this object.
239     *
240     * @return The number of vertices.
241     */
242    public int getVertexCount() {
243        return this.vertices.size();
244    }
245
246    /**
247     * Adds a new object vertex with the specified coordinates.
248     * 
249     * @param x  the x-coordinate.
250     * @param y  the y-coordinate.
251     * @param z  the z-coordinate.
252     */
253    public void addVertex(double x, double y, double z) {
254        addVertex(new Point3D(x, y, z));    
255    }
256    
257    /**
258     * Adds a new object vertex.
259     *
260     * @param vertex  the vertex ({@code null} not permitted).
261     */
262    public void addVertex(Point3D vertex) {
263        Args.nullNotPermitted(vertex, "vertex");
264        this.vertices.add(vertex);
265    }
266
267    /**
268     * Returns the number of faces.
269     *
270     * @return The number of faces.
271     */
272    public int getFaceCount() {
273        return this.faces.size();
274    }
275
276    /**
277     * Adds a face for the given vertices (specified by index value).
278     * 
279     * @param vertices  the vertices (all should lie in a plane).
280     * 
281     * @since 1.3
282     */
283    public void addFace(int[] vertices) {
284        // defer the arg checks...
285        addFace(new Face(this, vertices));
286    }
287    
288    /**
289     * Adds a tagged face for the given vertices (specified by index value).
290     * 
291     * @param vertices  the vertices (all should lie in a plane).
292     * @param tag  the tag ({@code null} not permitted).
293     * 
294     * @since 1.3
295     */
296    public void addFace(int[] vertices, String tag) {
297        addFace(new TaggedFace(this, vertices, tag));
298    }
299    
300    /**
301     * Adds a double-sided face for the given vertices (specified by index 
302     * value) and color.
303     * 
304     * @param vertices  the vertices (all should lie in a plane).
305     * 
306     * @since 1.3
307     */
308    public void addDoubleSidedFace(int[] vertices) {
309        addFace(new DoubleSidedFace(this, vertices));
310    }
311    
312    /**
313     * Adds a face for this object.
314     *
315     * @param face  the face ({@code null} not permitted).
316     */
317    public void addFace(Face face) {
318        Args.nullNotPermitted(face, "face");
319        this.faces.add(face);
320    }
321
322    /**
323     * Returns the faces for this object.  Note that the list returned is a 
324     * direct reference to the internal storage for this {@code Object3D} 
325     * instance, so callers should take care not to modify this list 
326     * unintentionally.
327     *
328     * @return The faces.
329     */
330    public List<Face> getFaces() {
331        return this.faces;
332    }
333
334    /**
335     * Calculates the projected points for the object's vertices, for the
336     * given viewpoint.
337     *
338     * @param viewPoint  the view point ({@code null} not permitted).
339     * @param d  the projection distance.
340     *
341     * @return The projected points.
342     */
343    public Point2D[] calculateProjectedPoints(ViewPoint3D viewPoint, double d) {
344        Args.nullNotPermitted(viewPoint, "viewPoint");
345        Point2D[] result = new Point2D[this.vertices.size()];
346        int vertexCount = this.vertices.size();
347        for (int i = 0; i < vertexCount; i++) {
348            Point3D p = this.vertices.get(i);
349            result[i] = viewPoint.worldToScreen(p, d);
350        }
351        return result;
352    }
353
354    /**
355     * Returns the eye coordinates of the object's vertices.
356     *
357     * @param viewPoint  the view point ({@code null} not permitted).
358     *
359     * @return The eye coordinates.
360     */
361    public Point3D[] calculateEyeCoordinates(ViewPoint3D viewPoint) {
362        Args.nullNotPermitted(viewPoint, "viewPoint");
363        Point3D[] result = new Point3D[this.vertices.size()];
364        int i = 0;
365        for (Point3D vertex : this.vertices) {
366            result[i] = viewPoint.worldToEye(vertex);
367            i++;
368        }
369        return result;
370    }
371    
372    /**
373     * Creates a square flat surface in the x-z plane (constant y) with a 
374     * single face.
375     * 
376     * @param size  the sheet size.
377     * @param x  the x-coordinate for the center of the square.
378     * @param y  the y-coordinate.
379     * @param z  the z-coordinate for the center of the square.
380     * @param color  the color ({@code null} not permitted).
381     * @param invert  invert the order of the face
382     * 
383     * @return  The sheet.
384     */
385    public static Object3D createYSheet(double size, double x, double y, 
386            double z, Color color, boolean invert) {
387        Args.nullNotPermitted(color, "color");
388        Object3D sheet = new Object3D(color);
389        double delta = size / 2.0;
390        sheet.addVertex(new Point3D(x + delta, y, z - delta));
391        sheet.addVertex(new Point3D(x + delta, y, z + delta));
392        sheet.addVertex(new Point3D(x - delta, y, z + delta));
393        sheet.addVertex(new Point3D(x - delta, y, z - delta));
394        if (invert) {
395            sheet.addFace(new Face(sheet, new int[] {3, 2, 1, 0}));   
396        } else {
397            sheet.addFace(new Face(sheet, new int[] {0, 1, 2, 3}));
398        }
399        return sheet;
400    }
401  
402    /**
403     * Creates a square flat surface in the x-y plane (constant z).
404     * 
405     * @param size  the sheet size.
406     * @param x  the x-coordinate of a point on the surface.
407     * @param y  the y-coordinate of a point on the surface.
408     * @param z  the z-coordinate of a point on the surface.
409     * @param color  the color.
410     * 
411     * @return The sheet. 
412     */
413    public static Object3D createZSheet(double size, double x, double y, 
414            double z, Color color) {
415        Object3D sheet = new Object3D(color);
416        double delta = size / 2.0;
417        sheet.addVertex(new Point3D(x + delta, y - delta, z));
418        sheet.addVertex(new Point3D(x + delta, y + delta, z));
419        sheet.addVertex(new Point3D(x - delta, y + delta, z));
420        sheet.addVertex(new Point3D(x - delta, y - delta, z));
421        sheet.addFace(new Face(sheet, new int[] {0, 1, 2, 3}));
422        return sheet;
423    }
424
425    /**  
426     * Creates a cube centered on {@code (x, y, z)} with the specified 
427     * {@code size}.
428     *
429     * @param size  the size.
430     * @param x  the x-offset.
431     * @param y  the y-offset.
432     * @param z  the z-offset.
433     * @param color  the color ({@code null} not permitted).
434     *
435     * @return The cube (never {@code null}).
436     */
437    public static Object3D createCube(double size, double x, 
438            double y, double z, Color color) {
439        return createBox(x, size, y, size, z, size, color);
440    }
441
442    /**  
443     * Creates a box centered on {@code (x, y, z)} with the specified 
444     * dimensions.  
445     *
446     * @param x  the x-coordinate.
447     * @param xdim  the length of the box in the x-dimension.
448     * @param y  the y-coordinate.
449     * @param ydim  the length of the box in the y-dimension.
450     * @param z  the z-coordinate.
451     * @param zdim  the length of the box in the y-dimension.
452     * @param color  the color ({@code null} not permitted).
453     *
454     * @return The box (never {@code null}).
455     * 
456     * @see #createCube(double, double, double, double, java.awt.Color) 
457     */
458    public static Object3D createBox(double x, double xdim, 
459            double y, double ydim, double z, double zdim, 
460            Color color) {
461        Args.nullNotPermitted(color, "color");
462        Object3D box = new Object3D(color);
463        double xdelta = xdim / 2.0;
464        double ydelta = ydim / 2.0;
465        double zdelta = zdim / 2.0;
466        box.addVertex(new Point3D(x - xdelta, y - ydelta, z - zdelta));
467        box.addVertex(new Point3D(x + xdelta, y - ydelta, z - zdelta));
468        box.addVertex(new Point3D(x + xdelta, y - ydelta, z + zdelta));
469        box.addVertex(new Point3D(x - xdelta, y - ydelta, z + zdelta));
470        box.addVertex(new Point3D(x - xdelta, y + ydelta, z - zdelta));
471        box.addVertex(new Point3D(x + xdelta, y + ydelta, z - zdelta));
472        box.addVertex(new Point3D(x + xdelta, y + ydelta, z + zdelta));
473        box.addVertex(new Point3D(x - xdelta, y + ydelta, z + zdelta));
474        box.addFace(new Face(box, new int[] {4, 5, 1, 0}));
475        box.addFace(new Face(box, new int[] {5, 6, 2, 1}));
476        box.addFace(new Face(box, new int[] {6, 7, 3, 2}));
477        box.addFace(new Face(box, new int[] {3, 7, 4, 0}));
478        box.addFace(new Face(box, new int[] {7, 6, 5, 4}));
479        box.addFace(new Face(box, new int[] {0, 1, 2, 3}));
480        return box;
481    }
482
483    /**
484     * Creates a tetrahedron.
485     * 
486     * @param size  the size.
487     * @param xOffset  the x-offset.
488     * @param yOffset  the y-offset.
489     * @param zOffset  the z-offset.
490     * @param color  the color ({@code null} not permitted).
491     * 
492     * @return A tetrahedron.
493     */
494    public static Object3D createTetrahedron(double size, double xOffset,
495            double yOffset, double zOffset, Color color) {
496        Args.nullNotPermitted(color, "color");
497        Object3D tetra = new Object3D(color);
498        tetra.addVertex(new Point3D(size + xOffset, -size + yOffset, 
499                -size + zOffset));
500        tetra.addVertex(new Point3D(-size + xOffset, size + yOffset, 
501                -size + zOffset));
502        tetra.addVertex(new Point3D(size + xOffset, size + yOffset, 
503                size + zOffset));
504        tetra.addVertex(new Point3D(-size + xOffset, -size + yOffset, 
505                size + zOffset));
506        tetra.addFace(new Face(tetra, new int[] {0, 1, 2}));
507        tetra.addFace(new Face(tetra, new int[] {1, 3, 2}));
508        tetra.addFace(new Face(tetra, new int[] {0, 3, 1}));
509        tetra.addFace(new Face(tetra, new int[] {0, 2, 3}));
510        return tetra;
511    }
512
513    /**
514     * Creates an octahedron.
515     * 
516     * @param size  the size.
517     * @param xOffset  the x-offset.
518     * @param yOffset  the y-offset.
519     * @param zOffset  the z-offset.
520     * @param color  the color ({@code null} not permitted).
521     * 
522     * @return An octahedron.
523     */
524    public static Object3D createOctahedron(double size, double xOffset,
525            double yOffset, double zOffset, Color color) {
526        Args.nullNotPermitted(color, "color");
527        Object3D octa = new Object3D(color);
528        octa.addVertex(new Point3D(size + xOffset, 0 + yOffset, 0 + zOffset));
529        octa.addVertex(new Point3D(0 + xOffset, size + yOffset, 0 + zOffset));
530        octa.addVertex(new Point3D(-size + xOffset, 0 + yOffset, 0 + zOffset));
531        octa.addVertex(new Point3D(0 + xOffset, -size + yOffset, 0 + zOffset));
532        octa.addVertex(new Point3D(0 + xOffset, 0 + yOffset, -size + zOffset));
533        octa.addVertex(new Point3D(0 + xOffset, 0 + yOffset, size + zOffset));
534
535        octa.addFace(new Face(octa, new int[] {0, 1, 5}));
536        octa.addFace(new Face(octa, new int[] {1, 2, 5}));
537        octa.addFace(new Face(octa, new int[] {2, 3, 5}));
538        octa.addFace(new Face(octa, new int[] {3, 0, 5}));
539        octa.addFace(new Face(octa, new int[] {1, 0, 4}));
540        octa.addFace(new Face(octa, new int[] {2, 1, 4}));
541        octa.addFace(new Face(octa, new int[] {3, 2, 4}));
542        octa.addFace(new Face(octa, new int[] {0, 3, 4}));
543        return octa;
544    }
545
546    /**
547     * Creates an approximation of a sphere.
548     * 
549     * @param radius  the radius of the sphere (in world units).
550     * @param n  the number of layers.
551     * @param x  the x-coordinate of the center of the sphere. 
552     * @param y  the y-coordinate of the center of the sphere.
553     * @param z  the z-coordinate of the center of the sphere.
554     * @param extColor  the exterior color ({@code null} not permitted).
555     * @param intColor  the interior color ({@code null} not permitted).
556     * 
557     * @return A sphere. 
558     */
559    public static Object3D createSphere(double radius, int n,
560            double x, double y, double z, Color extColor, Color intColor) {
561        Object3D sphere = new Object3D(extColor);
562        sphere.setProperty(COLOR_PREFIX + "interior", intColor);
563        double theta = Math.PI / n;
564        Point3D[] prevLayer = new Point3D[n * 2 + 1];
565        for (int i = 0; i <= n * 2; i++) {
566            prevLayer[i] = new Point3D(x, y + radius, z);
567            if (i != n * 2) {
568                sphere.addVertex(prevLayer[i]);
569            }
570        }
571
572        for (int layer = 1; layer < n; layer++) {
573            Point3D[] currLayer = new Point3D[n * 2 + 1];
574            for (int i = 0; i <= n * 2; i++) {
575                double xx = radius * Math.cos(i * theta) 
576                        * Math.sin(layer * theta);
577                double yy = radius * Math.cos(layer * theta);
578                double zz = radius * Math.sin(i * theta) 
579                        * Math.sin(layer * theta);
580                currLayer[i] = new Point3D(x + xx, y + yy, z + zz);
581                if (i != n * 2) {
582                    sphere.addVertex(currLayer[i]);
583                }
584                if (i > 0 && layer > 1) {
585                    if (i != n * 2) {
586                        Face f = new Face(sphere, new int[] {
587                            (layer - 1) * n * 2 + i - 1, 
588                            (layer - 1) * n * 2 + i, layer * n * 2 + i, 
589                            layer * n * 2 + i - 1});
590                        sphere.addFace(f);
591                        f = new TaggedFace(sphere, new int[] {
592                            layer * n * 2 + i - 1, layer * n * 2 + i, 
593                            (layer - 1) * n * 2 + i, 
594                            (layer - 1) * n * 2 + i - 1}, "interior");
595                        sphere.addFace(f);
596                    } else {
597                        sphere.addFace(new Face(sphere, new int[] {
598                            (layer - 1) * n * 2 + i - 1, (layer - 1) * n * 2, 
599                            layer * n * 2, layer * n * 2 + i - 1}));
600                        sphere.addFace(new TaggedFace(sphere, new int[] {
601                            layer * n * 2 + i - 1, layer * n * 2, 
602                            (layer - 1) * n * 2, (layer - 1) * n * 2 + i - 1}, 
603                            "interior"));
604                    }
605                }
606            }
607        }
608        return sphere;
609    }
610 
611    /**
612     * Creates a pie segment with the specified attributes.
613     * 
614     * @param radius  the radius.
615     * @param explodeRadius  the explode radius (0.0 if not exploded).
616     * @param base  the base.
617     * @param height  the height.
618     * @param angle1  the start angle (radians).
619     * @param angle2  the end angle (radians).
620     * @param inc  the increment.
621     * @param color  the color ({@code null} not permitted).
622     * 
623     * @return  A pie segment object. 
624     */
625    public static Object3D createPieSegment(double radius, double explodeRadius, 
626            double base, double height, double angle1, double angle2, 
627            double inc, Color color) {
628        Args.nullNotPermitted(color, "color");
629        Object3D segment = new Object3D(color, true);
630        double angleCentre = (angle1 + angle2) / 2.0;
631        Point3D centre = new Point3D(explodeRadius * Math.cos(angleCentre), 
632                base, explodeRadius * Math.sin(angleCentre));
633        float cx = (float) centre.x;
634        float cz = (float) centre.z;
635        segment.addVertex(new Point3D(cx + 0.0, base, cz + 0.0));
636        segment.addVertex(new Point3D(cx + 0.0, base + height, cz + 0.0));
637        Point3D v0 = new Point3D(cx + radius * Math.cos(angle1), base, 
638                cz + radius * Math.sin(angle1));
639        Point3D v1 = new Point3D(cx + radius * Math.cos(angle1), base + height, 
640                cz + radius * Math.sin(angle1));
641        segment.addVertex(v0);
642        segment.addVertex(v1);
643        segment.addFace(new Face(segment, new int[] {1, 3, 2, 0}));
644        int vc = 4; // vertex count
645        double theta = angle1 + inc;
646        while (theta < angle2) {
647            Point3D v2 = new Point3D(cx + radius * Math.cos(theta), base, 
648                    cz + radius * Math.sin(theta));
649            Point3D v3 = new Point3D(cx + radius * Math.cos(theta), 
650                    base + height, cz + radius * Math.sin(theta));
651            segment.addVertex(v2);
652            segment.addVertex(v3);
653            vc = vc + 2;
654
655            // outside edge
656            segment.addFace(new Face(segment, 
657                    new int[] {vc - 2, vc - 4, vc - 3, vc - 1}));
658
659            // top and bottom
660            segment.addFace(new Face(segment, 
661                    new int[] {0,  vc - 4, vc - 2, 0}));
662            segment.addFace(new Face(segment, 
663                    new int[] {1,  vc - 1, vc - 3, 1}));
664            theta = theta + inc;
665        }
666        v0 = new Point3D(cx + radius * Math.cos(angle2), base, 
667                cz + radius * Math.sin(angle2));
668        v1 = new Point3D(cx + radius * Math.cos(angle2), base + height, 
669                cz + radius * Math.sin(angle2));
670        segment.addVertex(v0);
671        segment.addVertex(v1);
672        vc = vc + 2;
673        segment.addFace(new Face(segment, 
674                new int[] {vc - 2, vc - 4, vc - 3, vc - 1}));
675
676        // top and bottom
677        segment.addFace(new Face(segment, new int[] {0,  vc - 4, vc - 2, 0}));
678        segment.addFace(new Face(segment, new int[] {1,  vc - 1, vc - 3, 1}));
679
680        // closing side
681        segment.addFace(new Face(segment, new int[] {1, 0, vc-2, vc-1}));
682        return segment;
683    }
684
685    /**
686     * Returns two 3D objects (sheets in the y-plane) that can be used as
687     * alignment anchors for the labels of a pie segment.  One sheet is on the
688     * front face of the segment, and the other is on the back face.  Depending
689     * on the viewing point, only one of the sheets will be showing, and this
690     * is the one that the pie segment label will be attached to.
691     * 
692     * @param radius  the pie segment radius (in world units).
693     * @param explodeRadius  the pie segment explode radius (in world units).
694     * @param base  the base of the pie segment.
695     * @param height  the height of the pie segment.
696     * @param angle1  the start angle of the segment (in radians).
697     * @param angle2  the end angle of the segment (in radians).
698     * 
699     * @return A list containing the two 3D objects to be used as pie label
700     *     markers.
701     */
702    public static List<Object3D> createPieLabelMarkers(double radius, 
703            double explodeRadius, double base, double height, 
704            double angle1, double angle2) {
705        List<Object3D> result = new ArrayList<>();
706        double angle = (angle1 + angle2) / 2.0;
707        Point3D centre = new Point3D(explodeRadius * Math.cos(angle), 
708                base, explodeRadius * Math.sin(angle));
709        float cx = (float) centre.x;
710        float cz = (float) centre.z;
711        double r = radius * 0.9;
712        Point3D v0 = new Point3D(cx + r * Math.cos(angle), base, 
713                cz + r * Math.sin(angle));
714        Point3D v1 = new Point3D(cx + r * Math.cos(angle), base + height, 
715                cz + r * Math.sin(angle));
716        result.add(Object3D.createYSheet(2.0, v0.x, v0.y, v0.z, Color.RED, 
717                false));
718        result.add(Object3D.createYSheet(2.0, v1.x, v1.y, v1.z, Color.BLUE, 
719                true));
720        return result;
721    }
722 
723    /**
724     * Creates a bar with the specified dimensions and color.
725     * 
726     * @param xWidth  the x-width of the bar.
727     * @param zWidth  the z-width (or depth) of the bar.
728     * @param x  the x-coordinate for the center of the bar.
729     * @param y  the y-coordinate for the top of the bar.
730     * @param z  the z-coordinate for the center of the bar.
731     * @param zero  the y-coordinate for the bottom of the bar.
732     * @param barColor  the color for the bar ({@code null} not permitted).
733     * @param baseColor  the color for the base of the bar (if {@code null}, 
734     *     the {@code color} is used instead).
735     * @param topColor  the color for the top of the bar (if 
736     *     {@code null}, the {@code color} is used instead).
737     * @param inverted  a flag that determines whether the baseColor and 
738     *     topColor should be swapped in their usage.
739     * 
740     * @return A 3D object that can represent a bar in a bar chart. 
741     */
742    public static Object3D createBar(double xWidth, double zWidth, double x, 
743            double y, double z, double zero, Color barColor, Color baseColor,
744            Color topColor, boolean inverted) {
745        Args.nullNotPermitted(barColor, "barColor");
746        Color c0 = baseColor;
747        Color c1 = topColor;
748        if (inverted) {
749            Color cc = c1;
750            c1 = c0;
751            c0 = cc;
752        }
753        Object3D bar = new Object3D(barColor);
754        if (c0 != null) {
755            bar.setProperty(COLOR_PREFIX + "c0", c0);
756        }
757        if (c1 != null) {
758            bar.setProperty(COLOR_PREFIX + "c1", c1);
759        }
760        double xdelta = xWidth / 2.0;
761        double zdelta = zWidth / 2.0;
762        bar.addVertex(new Point3D(x - xdelta, zero, z - zdelta));
763        bar.addVertex(new Point3D(x + xdelta, zero, z - zdelta));
764        bar.addVertex(new Point3D(x + xdelta, zero, z + zdelta));
765        bar.addVertex(new Point3D(x - xdelta, zero, z + zdelta));
766        bar.addVertex(new Point3D(x - xdelta, y, z - zdelta));
767        bar.addVertex(new Point3D(x + xdelta, y, z - zdelta));
768        bar.addVertex(new Point3D(x + xdelta, y, z + zdelta));
769        bar.addVertex(new Point3D(x - xdelta, y, z + zdelta));
770
771        bar.addFace(new Face(bar, new int[] {0, 1, 5, 4}));
772        bar.addFace(new Face(bar, new int[] {4, 5, 1, 0}));
773        bar.addFace(new Face(bar, new int[] {1, 2, 6, 5}));
774        bar.addFace(new Face(bar, new int[] {5, 6, 2, 1}));
775        bar.addFace(new Face(bar, new int[] {2, 3, 7, 6}));
776        bar.addFace(new Face(bar, new int[] {6, 7, 3, 2}));
777        bar.addFace(new Face(bar, new int[] {0, 4, 7, 3}));
778        bar.addFace(new Face(bar, new int[] {3, 7, 4, 0}));
779        bar.addFace(new Face(bar, new int[] {4, 5, 6, 7}));
780        bar.addFace(new Face(bar, new int[] {3, 2, 1, 0}));
781        if (c1 != null) {
782            bar.addFace(new TaggedFace(bar, new int[] {7, 6, 5, 4}, "c1"));
783        } else {
784            bar.addFace(new Face(bar, new int[] {7, 6, 5, 4}));
785        }
786        if (c0 != null) {
787            bar.addFace(new TaggedFace(bar, new int[] {0, 1, 2, 3}, "c0"));    
788        } else {
789            bar.addFace(new Face(bar, new int[] {0, 1, 2, 3}));                
790        }
791        
792        return bar;      
793    }
794    
795    /**
796     * Creates a label object, which has a single transparent face in the 
797     * Z-plane plus associated label attributes.  These faces are used to 
798     * track the location and visibility of labels in a 3D scene.
799     * 
800     * @param label  the label ({@code null} not permitted).
801     * @param font  the font ({@code null} not permitted).
802     * @param fgColor  the label foreground color ({@code null} not permitted).
803     * @param bgColor  the label background color ({@code null} not permitted).
804     * @param x  the x-coordinate in 3D space.
805     * @param y  the y-coordinate in 3D space.
806     * @param z  the z-coordinate in 3D space.
807     * @param reversed  reverse the order of the vertices?
808     * @param doubleSided  is the face double-sided (visible from either side)?
809     * 
810     * @return A new label object (never {@code null}).
811     * 
812     * @since 1.3
813     */
814    public static Object3D createLabelObject(String label, Font font, 
815            Color fgColor, Color bgColor, double x, double y, double z, 
816            boolean reversed, boolean doubleSided) {
817        Object3D labelObj = new Object3D(bgColor);
818        labelObj.setProperty(Object3D.CLASS_KEY, "ItemLabel");
819        labelObj.addVertex(x - 0.1, y, z);
820        labelObj.addVertex(x + 0.1, y, z);
821        labelObj.addVertex(x + 0.1, y + 0.1, z);
822        labelObj.addVertex(x - 0.1, y + 0.1, z);
823        
824        if (!reversed || doubleSided) {
825            labelObj.addFace(new LabelFace(labelObj, new int[] {0, 1, 2, 3}, 
826                    label, font, fgColor, bgColor));
827        }
828        if (reversed || doubleSided) {
829            labelObj.addFace(new LabelFace(labelObj, new int[] {3, 2, 1, 0}, 
830                    label, font, fgColor, bgColor));
831        }
832        return labelObj;
833    }
834
835}