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