/* ===========================================================
 * Orson Charts : a 3D chart library for the Java(tm) platform
 * ===========================================================
 * 
 * (C)opyright 2013-2016, by Object Refinery Limited.  All rights reserved.
 * 
 * http://www.object-refinery.com/orsoncharts/index.html
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
 * Other names may be trademarks of their respective owners.]
 * 
 * If you do not wish to be bound by the terms of the GPL, an alternative
 * commercial license can be purchased.  For details, please see visit the
 * Orson Charts home page:
 * 
 * http://www.object-refinery.com/orsoncharts/index.html
 * 
 */

package com.orsoncharts.renderer.category;

import java.awt.Color;
import java.io.Serializable;

import com.orsoncharts.Chart3D;
import com.orsoncharts.Chart3DFactory;
import com.orsoncharts.Range;
import com.orsoncharts.axis.CategoryAxis3D;
import com.orsoncharts.axis.ValueAxis3D;
import com.orsoncharts.data.KeyedValues3DItemKey;
import com.orsoncharts.data.category.CategoryDataset3D;
import com.orsoncharts.graphics3d.Dimension3D;
import com.orsoncharts.graphics3d.Object3D;
import com.orsoncharts.graphics3d.Offset3D;
import com.orsoncharts.graphics3d.World;
import com.orsoncharts.label.ItemLabelPositioning;
import com.orsoncharts.plot.CategoryPlot3D;
import com.orsoncharts.renderer.Renderer3DChangeEvent;
import com.orsoncharts.util.ObjectUtils;

/**
 * A renderer that can be used with the {@link CategoryPlot3D} class to create
 * 3D lines charts from data in a {@link CategoryDataset3D}.  The 
 * {@code createLineChart()} method in the {@link Chart3DFactory} class 
 * will construct a chart that uses this renderer.  Here is a sample:
 * <div>
 * <object id="ABC" data="../../../../doc-files/LineChart3DDemo1.svg"  
 * type="image/svg+xml" width="500" height="359"></object>
 * </div>
 * (refer to {@code LineChart3DDemo1.java} for the code to generate the
 * above chart).
 * <br><br>
 * Some attributes in the renderer are specified in "world units" - see the
 * {@link Chart3D} class description for more information about world units.
 * <br><br>
 * There is a factory method to create a chart using this renderer - see
 * {@link Chart3DFactory#createLineChart(String, String, CategoryDataset3D, 
 * String, String, String)}.
 * <br><br> 
 * NOTE: This class is serializable, but the serialization format is subject 
 * to change in future releases and should not be relied upon for persisting 
 * instances of this class.
 */
@SuppressWarnings("serial")
public class LineRenderer3D extends AbstractCategoryRenderer3D 
        implements Serializable {

    /** The line width (in world units). */
    private double lineWidth;
    
    /** The line height (in world units). */
    private double lineHeight;

    /** 
     * For isolated data values this attribute controls the width (x-axis) of 
     * the box representing the data item, it is expressed as a percentage of
     * the category width.
     */
    private double isolatedItemWidthPercent;
    
    /**
     * The color source that determines the color used to highlight clipped
     * items in the chart.
     */
    private CategoryColorSource clipColorSource;
    
    /**
     * Creates a new instance with default attribute values.
     */
    public LineRenderer3D() {
        this.lineWidth = 0.4;
        this.lineHeight = 0.2;
        this.isolatedItemWidthPercent = 0.25;
        this.clipColorSource = new StandardCategoryColorSource(Color.RED);
    }
    
    /**
     * Returns the line width in world units.  The default value is 
     * {@code 0.4}.
     * 
     * @return The line width in world units. 
     */
    public double getLineWidth() {
        return this.lineWidth;
    }
    
    /**
     * Sets the line width (in world units) and sends a 
     * {@link Renderer3DChangeEvent} to all registered listeners.
     * 
     * @param width  the width (in world units). 
     */
    public void setLineWidth(double width) {
        this.lineWidth = width;
        fireChangeEvent(true);
    }

    /**
     * Returns the line height in world units.  The default value is 
     * {@code 0.2}.
     * 
     * @return The line height in world units. 
     */
    public double getLineHeight() {
        return this.lineHeight;
    }
    
    /**
     * Sets the line height (in world units) and sends a 
     * {@link Renderer3DChangeEvent} to all registered listeners.
     * 
     * @param height  the height (in world units). 
     */
    public void setLineHeight(double height) {
        this.lineHeight = height;
        fireChangeEvent(true);
    }

    /**
     * Returns the width for isolated data items as a percentage of the
     * category width.  The default value is 0.25 (twenty five percent).
     * 
     * @return The width percentage.
     * 
     * @since 1.3
     */
    public double getIsolatedItemWidthPercent() {
        return this.isolatedItemWidthPercent;
    }
    
    /**
     * Sets the width for isolated data items as a percentage of the category
     * width and sends a change event to all registered listeners.
     * 
     * @param percent  the new percentage.
     * 
     * @since 1.3
     */
    public void setIsolatedItemWidthPercent(double percent) {
        this.isolatedItemWidthPercent = percent;
        fireChangeEvent(true);
    }
    
    /**
     * Returns the color source used to determine the color used to highlight
     * clipping in the chart elements.  If the source is {@code null},
     * then the regular series color is used instead.
     * 
     * @return The color source (possibly {@code null}). 
     */
    public CategoryColorSource getClipColorSource() {
        return this.clipColorSource;
    }
    
    /**
     * Sets the color source that determines the color used to highlight
     * clipping in the chart elements, and sends a {@link Renderer3DChangeEvent}
     * to all registered listeners.
     * 
     * @param source  the source ({@code null} permitted). 
     */
    public void setClipColorSource(CategoryColorSource source) {
        this.clipColorSource = source;
        fireChangeEvent(true);
    }
    
    /**
     * Constructs and places one item from the specified dataset into the given 
     * world.  This method will be called by the {@link CategoryPlot3D} class
     * while iterating over the items in the dataset.
     * 
     * @param dataset  the dataset ({@code null} not permitted).
     * @param series  the series index.
     * @param row  the row index.
     * @param column  the column index.
     * @param world  the world ({@code null} not permitted).
     * @param dimensions  the plot dimensions ({@code null} not permitted).
     * @param xOffset  the x-offset.
     * @param yOffset  the y-offset.
     * @param zOffset  the z-offset.
     */
    @Override
    public void composeItem(CategoryDataset3D dataset, int series, int row, 
            int column, World world, Dimension3D dimensions, 
            double xOffset, double yOffset, double zOffset) {
        
        // there is a lot of brute force code underneath this compose method
        // because I haven't seen the pattern yet that will let me reduce it
        // to something more elegant...probably I'm not smart enough.
        Number y = dataset.getValue(series, row, column);
        Number yprev = null;
        if (column > 0) {
            yprev = dataset.getValue(series, row, column - 1);
        }
        Number ynext = null;
        if (column < dataset.getColumnCount() - 1) {
            ynext = dataset.getValue(series, row, column + 1);
        }

        CategoryPlot3D plot = getPlot();
        CategoryAxis3D rowAxis = plot.getRowAxis();
        CategoryAxis3D columnAxis = plot.getColumnAxis();
        ValueAxis3D valueAxis = plot.getValueAxis();
        Range r = valueAxis.getRange();
        
        Comparable<?> seriesKey = dataset.getSeriesKey(series);
        Comparable<?> rowKey = dataset.getRowKey(row);
        Comparable<?> columnKey = dataset.getColumnKey(column);
        double rowValue = rowAxis.getCategoryValue(rowKey);
        double columnValue = columnAxis.getCategoryValue(columnKey);
        double ww = dimensions.getWidth();
        double hh = dimensions.getHeight();
        double dd = dimensions.getDepth();

        // for any data value, we'll try to create two line segments, one to
        // the left of the value and one to the right of the value (each 
        // halfway to the adjacent data value).  If the adjacent data values
        // are null (or don't exist, as in the case of the first and last data
        // items, then we can create an isolated segment to represent the data
        // item.  The final consideration is whether the opening and closing
        // faces of each segment are filled or not (if the segment connects to
        // another segment, there is no need to fill the end face)
        boolean createLeftSegment, createRightSegment, createIsolatedSegment;
        boolean leftOpen = false;
        boolean leftClose = false;
        boolean rightOpen = false;
        boolean rightClose = false;
        if (column == 0) { // first column is a special case
            createLeftSegment = false;  // never for first item
            if (dataset.getColumnCount() == 1) {
                createRightSegment = false; 
                createIsolatedSegment = (y != null);
            } else {
                createRightSegment = (y != null && ynext != null);
                rightOpen = true;
                rightClose = false;
                createIsolatedSegment = (y != null && ynext == null);
            }
        } else if (column == dataset.getColumnCount() - 1) { // last column
            createRightSegment = false; // never for the last item
            createLeftSegment = (y != null && yprev != null);
            leftOpen = false;
            leftClose = true;
            createIsolatedSegment = (y != null && yprev == null);
        } else { // this is the general case
            createLeftSegment = (y != null && yprev != null);
            leftOpen = false;
            leftClose = (createLeftSegment && ynext == null);
            createRightSegment = (y != null && ynext != null);
            rightOpen = (createRightSegment && yprev == null);
            rightClose = false;
            createIsolatedSegment = (y != null 
                    && yprev == null && ynext == null);
        }

        // now that we know what we have to create, we'll need some info 
        // for the construction
        double xw = columnAxis.translateToWorld(columnValue, ww) + xOffset;
        double yw = Double.NaN;
        if (y != null) {
            yw = valueAxis.translateToWorld(y.doubleValue(), hh) + yOffset; 
        }
        double zw = rowAxis.translateToWorld(rowValue, dd) + zOffset;
        double ywmin = valueAxis.translateToWorld(r.getMin(), hh) + yOffset;
        double ywmax = valueAxis.translateToWorld(r.getMax(), hh) + yOffset;
        Color color = getColorSource().getColor(series, row, column);
        Color clipColor = color;  
        if (getClipColorSource() != null) {
            Color c = getClipColorSource().getColor(series, row, column);
            if (c != null) {
                clipColor = c;
            }
        }
        KeyedValues3DItemKey itemKey = new KeyedValues3DItemKey(seriesKey, 
                rowKey, columnKey);
        if (createLeftSegment) {
            Comparable<?> prevColumnKey = dataset.getColumnKey(column - 1);
            double prevColumnValue = columnAxis.getCategoryValue(prevColumnKey);
            double prevColumnX = columnAxis.translateToWorld(prevColumnValue, 
                    ww) + xOffset;
            double xl = (prevColumnX + xw) / 2.0;
            double yprevw = valueAxis.translateToWorld(yprev.doubleValue(), hh) 
                    + yOffset; 
            double yl = (yprevw + yw) / 2.0;
            Object3D left = createSegment(xl, yl, xw, yw, zw, this.lineWidth, 
                    this.lineHeight, ywmin, ywmax, color, clipColor, leftOpen, 
                    leftClose);
            if (left != null) {
                left.setProperty(Object3D.ITEM_KEY, itemKey);
                world.add(left);
            }
        }
        if (createRightSegment) {
            Comparable<?> nextColumnKey = dataset.getColumnKey(column + 1);
            double nextColumnValue = columnAxis.getCategoryValue(nextColumnKey);
            double nextColumnX = columnAxis.translateToWorld(nextColumnValue, 
                    ww) + xOffset;
            double xr = (nextColumnX + xw) / 2.0;
            double ynextw = valueAxis.translateToWorld(ynext.doubleValue(), hh) 
                    + yOffset; 
            double yr = (ynextw + yw) / 2.0;
            Object3D right = createSegment(xw, yw, xr, yr, zw, this.lineWidth, 
                    this.lineHeight, ywmin, ywmax, color, clipColor, rightOpen, 
                    rightClose);
            if (right != null) {
                right.setProperty(Object3D.ITEM_KEY, itemKey);
                world.add(right);
            }
        }
        if (createIsolatedSegment) {
            double cw = columnAxis.getCategoryWidth() 
                    * this.isolatedItemWidthPercent;
            double cww = columnAxis.translateToWorld(cw, ww);
            Object3D isolated = Object3D.createBox(xw, cww, yw, this.lineHeight, 
                    zw, this.lineWidth, color);
            if (isolated != null) {
                isolated.setProperty(Object3D.ITEM_KEY, itemKey);
                world.add(isolated);
            }
        }
        
        if (getItemLabelGenerator() != null && !Double.isNaN(yw) 
                && yw >= ywmin && yw <= ywmax) {
            String label = getItemLabelGenerator().generateItemLabel(dataset, 
                    seriesKey, rowKey, columnKey);
            if (label != null) {
                ItemLabelPositioning positioning = getItemLabelPositioning();
                Offset3D offsets = getItemLabelOffsets();
                double dy = offsets.getDY() * dimensions.getHeight();
                if (positioning.equals(ItemLabelPositioning.CENTRAL)) {
                    Object3D labelObj = Object3D.createLabelObject(label, 
                            getItemLabelFont(), getItemLabelColor(), 
                            getItemLabelBackgroundColor(),
                            xw, yw + dy, zw, false, true);
                    labelObj.setProperty(Object3D.ITEM_KEY, itemKey);
                    world.add(labelObj);
                } else if (positioning.equals(
                        ItemLabelPositioning.FRONT_AND_BACK)) {
                    double dz = this.lineWidth ;
                    Object3D labelObj1 = Object3D.createLabelObject(label, 
                            getItemLabelFont(), getItemLabelColor(), 
                            getItemLabelBackgroundColor(),
                            xw, yw, zw - dz, false, false);
                    labelObj1.setProperty(Object3D.ITEM_KEY, itemKey);
                    world.add(labelObj1);
                    Object3D labelObj2 = Object3D.createLabelObject(label, 
                            getItemLabelFont(), getItemLabelColor(), 
                            getItemLabelBackgroundColor(),
                            xw, yw, zw + dz, true, false);
                    labelObj2.setProperty(Object3D.ITEM_KEY, itemKey);
                    world.add(labelObj2);
                }
            }
        }
    }

    /**
     * Creates a segment of a line between (x0, y0, z) and (x1, y1, z), with
     * the specified line width and height, taking into account the minimum
     * and maximum world coordinates (in the y-direction, because it is assumed
     * that we have the full x and z-range required).
     * 
     * @param x0  the starting x-coordinate.
     * @param y0  the starting x-coordinate.
     * @param x1  the ending x-coordinate.
     * @param y1  the ending y-coordinate.
     * @param z  the z-coordinate.
     * @param lineWidth  the line width (z-axis).
     * @param lineHeight  the line height (y-axis).
     * @param ymin  the lower bound for y-values.
     * @param ymax  the upper bound for y-values.
     * @param color  the segment color.
     * @param clipColor  the clip color (for the faces in the segment that are
     *     clipped against the edge of the world).
     * @param openingFace  is an opening face required?
     * @param closingFace  is a closing face required?
     * 
     * @return A 3D object that is a segment in a line.
     */
    private Object3D createSegment(double x0, double y0, double x1, double y1,
            double z, double lineWidth, double lineHeight, double ymin, 
            double ymax, Color color, Color clipColor, boolean openingFace,
            boolean closingFace) {
        double wdelta = lineWidth / 2.0;
        double hdelta = lineHeight / 2.0;
        double y0b = y0 - hdelta;
        double y0t = y0 + hdelta;
        double y1b = y1 - hdelta;
        double y1t = y1 + hdelta;
        double zf = z - wdelta;
        double zb = z + wdelta;
        double[] xpts = calcCrossPoints(x0, x1, y0b, y0t, y1b, y1t, ymin, ymax);
        Object3D seg = null;
        if (y0b >= ymax) {  // CASE A 
            seg = createSegmentA(x0, x1, xpts, y0b, y0t, y1b, y1t, 
            ymin, ymax, zf, zb, color, clipColor, false, closingFace);
        } else if (y0t > ymax && y0b > ymin) {  // CASE B
            seg = createSegmentB(x0, x1, xpts, y0b, y0t, y1b, y1t, ymin, ymax, 
                    zf, zb, color, clipColor, openingFace, closingFace);
        } else if (y0t > ymax && y0b <= ymin) {  // CASE C
            seg = createSegmentC(x0, x1, xpts, y0b, y0t, y1b, y1t, ymin, ymax, 
                    zf, zb, color, clipColor, openingFace, closingFace);
        } else if (y0t > ymin && y0b >= ymin) { // CASE D
            seg = createSegmentD(x0, x1, xpts, y0b, y0t, y1b, y1t, ymin, ymax, 
                    zf, zb, color, clipColor, openingFace, closingFace);                    
        } else if (y0t > ymin && y0b < ymin) { // CASE E
            seg = createSegmentE(x0, x1, xpts, y0b, y0t, y1b, y1t, ymin, ymax, 
                    zf, zb, color, clipColor, openingFace, closingFace);                    
        } else if (y0t <= ymin) {  // CASE F
            seg = createSegmentF(x0, x1, xpts, y0b, y0t, y1b, y1t, ymin, ymax, 
                    zf, zb, color, clipColor, false, closingFace);                   
        }
        return seg;
    }
    
    /**
     * Calculates the four intersection points between two horizontal lines
     * (ymin and ymax) and the lines (x0, y0b, x1, y1b) and (x0, y1t, x1, y1t)
     * and returns the x-coordinates in an array.
     * 
     * @param x0
     * @param x1
     * @param y0b
     * @param y0t
     * @param y1b
     * @param y1t
     * @param ymin
     * @param ymax
     * 
     * @return An array of 4 x-coordinates. 
     */
    private double[] calcCrossPoints(double x0, double x1, double y0b, 
            double y0t, double y1b, double y1t, double ymin, double ymax) {
        double[] xpts = new double[] { Double.NaN, Double.NaN, Double.NaN, 
            Double.NaN };
        double factor = (y0b - ymin) / (y0b - y1b);
        xpts[0] = x0 + factor * (x1 - x0);
        factor = (y0t - ymin) / (y0t - y1t);
        xpts[1] = x0 + factor * (x1 - x0);
        factor = (y0b - ymax) / (y0b - y1b);
        xpts[2] = x0 + factor * (x1 - x0);
        factor = (y0t - ymax) / (y0t - y1t);
        xpts[3] = x0 + factor * (x1 - x0);
        return xpts;
    }
    
    /**
     * Creates a segment for the case where the start of the segment is 
     * completely above the upper bound of the axis at the left side of the
     * chart.
     * 
     * @param x0
     * @param x1
     * @param xpts
     * @param y0b
     * @param y0t
     * @param y1b
     * @param y1t
     * @param wmin
     * @param wmax
     * @param zf
     * @param zb
     * @param color
     * @param clipColor
     * @param openingFace  ignored because there is no opening face for this
     *     case.
     * @param closingFace
     * 
     * @return A segment ({@code null} if the segment is entirely clipped). 
     */
    private Object3D createSegmentA(double x0, double x1, double[] xpts, 
            double y0b, double y0t, double y1b, double y1t, double wmin, 
            double wmax, double zf, double zb, Color color, Color clipColor, 
            boolean openingFace, boolean closingFace) {
        if (y1b > wmax) {
            return null;  // nothing is visible
        }
        if (y1t > wmax) {
            if (y1b >= wmin) {
                // create a triangle with the top and right
                Object3D seg = new Object3D(color, true);
                seg.setProperty(Object3D.COLOR_PREFIX + "clip", clipColor);
                seg.addVertex(xpts[2], wmax, zf);
                seg.addVertex(xpts[2], wmax, zb);
                seg.addVertex(x1, wmax, zf);
                seg.addVertex(x1, wmax, zb);
                seg.addVertex(x1, y1b, zf);
                seg.addVertex(x1, y1b, zb);
                seg.addFace(new int[] {0, 2, 4}); // front
                seg.addFace(new int[] {1, 5, 3}); // rear
                seg.addFace(new int[] {0, 1, 3, 2}, "clip"); // clip top
                seg.addFace(new int[] {4, 5, 1, 0}); // bottom
                if (closingFace) {
                    seg.addFace(new int[] {2, 3, 5, 4});
                }
                return seg;
            } else {
                Object3D seg = new Object3D(color, true);
                seg.setProperty(Object3D.COLOR_PREFIX + "clip", clipColor);
                seg.addVertex(xpts[2], wmax, zf);
                seg.addVertex(xpts[2], wmax, zb);
                seg.addVertex(x1, wmax, zf);
                seg.addVertex(x1, wmax, zb);
                seg.addVertex(x1, wmin, zf);
                seg.addVertex(x1, wmin, zb);
                seg.addVertex(xpts[0], wmin, zf);
                seg.addVertex(xpts[0], wmin, zb);
                seg.addFace(new int[] {0, 2, 4, 6}); // front
                seg.addFace(new int[] {1, 7, 5, 3}); // rear
                seg.addFace(new int[] {0, 1, 3, 2}); // clip top
                seg.addFace(new int[] {4, 5, 7, 6}, "clip"); // clip bottom
                seg.addFace(new int[] {6, 7, 1, 0}); // bottom
                if (closingFace) {
                    seg.addFace(new int[] {2, 3, 5, 4});
                }
                return seg;
            }
        } else if (y1t >= wmin) {
            if (y1b >= wmin) {
                Object3D seg = new Object3D(color, true);
                seg.setProperty(Object3D.COLOR_PREFIX + "clip", clipColor);
                seg.addVertex(xpts[2], wmax, zf);
                seg.addVertex(xpts[2], wmax, zb);
                seg.addVertex(xpts[3], wmax, zf);
                seg.addVertex(xpts[3], wmax, zb);
                seg.addVertex(x1, y1t, zf);
                seg.addVertex(x1, y1t, zb);
                seg.addVertex(x1, y1b, zf);
                seg.addVertex(x1, y1b, zb);
                seg.addFace(new int[] {0, 2, 4, 6}); // front
                seg.addFace(new int[] {1, 7, 5, 3}); // rear
                seg.addFace(new int[] {0, 1, 3, 2}, "clip"); // clip top
                seg.addFace(new int[] {2, 3, 5, 4}); // top
                seg.addFace(new int[] {6, 7, 1, 0}); // bottom
                if (closingFace) {
                    seg.addFace(new int[] {4, 5, 7, 6});
                }
                return seg;
            } else {
                Object3D seg = new Object3D(color, true);
                seg.setProperty(Object3D.COLOR_PREFIX + "clip", clipColor);
                seg.addVertex(xpts[2], wmax, zf);
                seg.addVertex(xpts[2], wmax, zb);
                seg.addVertex(xpts[3], wmax, zf);
                seg.addVertex(xpts[3], wmax, zb);
                seg.addVertex(x1, y1t, zf);
                seg.addVertex(x1, y1t, zb);
                seg.addVertex(x1, wmin, zf);
                seg.addVertex(x1, wmin, zb);
                seg.addVertex(xpts[0], wmin, zf);
                seg.addVertex(xpts[0], wmin, zb);
                seg.addFace(new int[] {0, 2, 4, 6, 8}); // front
                seg.addFace(new int[] {1, 9, 7, 5, 3}); // rear
                seg.addFace(new int[] {2, 3, 5, 4});
                seg.addFace(new int[] {0, 1, 3, 2}, "clip"); // clip top
                seg.addFace(new int[] {6, 7, 9, 8}, "clip"); // clip bottom
                seg.addFace(new int[] {8, 9, 1, 0});
                // there is no opening face in this case
                if (closingFace) {
                    seg.addFace(new int[] {4, 5, 7, 6});
                }
                return seg;
            }
        } else {
            Object3D seg = new Object3D(color, true);
            seg.setProperty(Object3D.COLOR_PREFIX + "clip", clipColor);
            seg.addVertex(xpts[2], wmax, zf);
            seg.addVertex(xpts[2], wmax, zb);
            seg.addVertex(xpts[3], wmax, zf);
            seg.addVertex(xpts[3], wmax, zb);
            seg.addVertex(xpts[1], wmin, zf);
            seg.addVertex(xpts[1], wmin, zb);
            seg.addVertex(xpts[0], wmin, zf);
            seg.addVertex(xpts[0], wmin, zb);
            seg.addFace(new int[] {0, 2, 4, 6}); // front
            seg.addFace(new int[] {1, 7, 5, 3}); // rear
            seg.addFace(new int[] {4, 2, 3, 5}); // top
            seg.addFace(new int[] {0, 6, 7, 1}); // bottom
            seg.addFace(new int[] {0, 1, 3, 2}, "clip"); // clip top
            seg.addFace(new int[] {4, 5, 7, 6}, "clip"); // clip bottom
            // there are no opening or closing faces in this case
            return seg;
        }
    }
    
    /**
     * Creates a segment for the case where the left end of the line spans
     * the axis maximum on the left side of the chart.
     * 
     * @param x0
     * @param x1
     * @param xpts
     * @param y0b
     * @param y0t
     * @param y1b
     * @param y1t
     * @param wmin
     * @param wmax
     * @param zf
     * @param zb
     * @param color
     * @param clipColor
     * @param openingFace
     * @param closingFace
     * 
     * @return A segment. 
     */
    private Object3D createSegmentB(double x0, double x1, double[] xpts, 
            double y0b, double y0t, double y1b, double y1t, double wmin, 
            double wmax, double zf, double zb, Color color, Color clipColor, 
            boolean openingFace, boolean closingFace) {
        
        if (y1b >= wmax) {
            Object3D seg = new Object3D(color, true);
            seg.setProperty(Object3D.COLOR_PREFIX + "clip", clipColor);
            seg.addVertex(x0, y0b, zf);
            seg.addVertex(x0, y0b, zb);
            seg.addVertex(x0, wmax, zf);
            seg.addVertex(x0, wmax, zb);
            seg.addVertex(xpts[2], wmax, zf);
            seg.addVertex(xpts[2], wmax, zb);
            seg.addFace(new int[] {0, 2, 4}); // front
            seg.addFace(new int[] {1, 5, 3});  // rear
            seg.addFace(new int[] {0, 4, 5, 1}); // bottom
            seg.addFace(new int[] {2, 3, 5, 4}, "clip"); // clip top
            if (openingFace) {
                seg.addFace(new int[] {0, 1, 3, 2}); 
            }
            // there is no closing face in this case
            return seg; 
        }
        if (y1t > wmax) {
            if (y1b >= wmin) {
                Object3D seg = new Object3D(color, true);
                seg.setProperty(Object3D.COLOR_PREFIX + "clip", clipColor);
                seg.addVertex(x0, y0b, zf);
                seg.addVertex(x0, y0b, zb);
                seg.addVertex(x0, wmax, zf);
                seg.addVertex(x0, wmax, zb);
                seg.addVertex(x1, wmax, zf);
                seg.addVertex(x1, wmax, zb);
                seg.addVertex(x1, y1b, zf);
                seg.addVertex(x1, y1b, zb);
                seg.addFace(new int[] {0, 2, 4, 6});  // front
                seg.addFace(new int[] {1, 7, 5, 3}); // rear
                seg.addFace(new int[] {2, 3, 5, 4}, "clip"); // clip top
                seg.addFace(new int[] {0, 6, 7, 1}); // bottom
                if (openingFace) {
                    seg.addFace(new int[] {0, 1, 3, 2});
                }
                if (closingFace) {
                    seg.addFace(new int[] {4, 5, 7, 6});
                }
                return seg;
            } else {
                Object3D seg = new Object3D(color, true);
                seg.setProperty(Object3D.COLOR_PREFIX + "clip", clipColor);
                seg.addVertex(x0, y0b, zf);
                seg.addVertex(x0, y0b, zb);
                seg.addVertex(x0, wmax, zf);
                seg.addVertex(x0, wmax, zb);
                seg.addVertex(x1, wmax, zf);
                seg.addVertex(x1, wmax, zb);
                seg.addVertex(x1, wmin, zf);
                seg.addVertex(x1, wmin, zb);
                seg.addVertex(xpts[0], wmin, zf);
                seg.addVertex(xpts[0], wmin, zb);
                seg.addFace(new int[] {0, 2, 4, 6, 8});  // front
                seg.addFace(new int[] {1, 9, 7, 5, 3}); // rear
                seg.addFace(new int[] {2, 3, 5, 4}, "clip"); // clip top
                seg.addFace(new int[] {8, 6, 7, 9}, "clip"); // clip bottom
                seg.addFace(new int[] {0, 8, 9, 1});
                if (openingFace) {
                    seg.addFace(new int[] {0, 1, 3, 2});
                }
                if (closingFace) {
                    seg.addFace(new int[] {4, 5, 7, 6});
                }
                return seg;
            }
        }
        if (y1t > wmin) {
            if (y1b >= wmin) {
                Object3D seg = new Object3D(color, true);
                seg.setProperty(Object3D.COLOR_PREFIX + "clip", clipColor);
                seg.addVertex(x0, y0b, zf);
                seg.addVertex(x0, y0b, zb);
                seg.addVertex(x0, wmax, zf);
                seg.addVertex(x0, wmax, zb);
                seg.addVertex(xpts[3], wmax, zf);
                seg.addVertex(xpts[3], wmax, zb);
                seg.addVertex(x1, y1t, zf);
                seg.addVertex(x1, y1t, zb);
                seg.addVertex(x1, y1b, zf);
                seg.addVertex(x1, y1b, zb);
                seg.addFace(new int[] {0, 2, 4, 6, 8}); // front
                seg.addFace(new int[] {1, 9, 7, 5, 3}); // rear
                seg.addFace(new int[] {2, 3, 5, 4}, "clip"); // clip top
                seg.addFace(new int[] {4, 5, 7, 6}); // top
                seg.addFace(new int[] {0, 8, 9, 1}); // bottom
                if (openingFace) {
                    seg.addFace(new int[] {0, 1, 3, 2});
                }
                if (closingFace) {
                    seg.addFace(new int[] {6, 7, 9, 8});
                }
                return seg;
            } else {
                Object3D seg = new Object3D(color, true);
                seg.setProperty(Object3D.COLOR_PREFIX + "clip", clipColor);
                seg.addVertex(x0, y0b, zf);
                seg.addVertex(x0, y0b, zb);
                seg.addVertex(x0, wmax, zf);
                seg.addVertex(x0, wmax, zb);
                seg.addVertex(xpts[3], wmax, zf);
                seg.addVertex(xpts[3], wmax, zb);
                seg.addVertex(x1, y1t, zf);
                seg.addVertex(x1, y1t, zb);
                seg.addVertex(x1, wmin, zf);
                seg.addVertex(x1, wmin, zb);
                seg.addVertex(xpts[0], wmin, zf);
                seg.addVertex(xpts[0], wmin, zb);
                seg.addFace(new int[] {0, 2, 4, 6, 8, 10}); // front
                seg.addFace(new int[] {1, 11, 9, 7, 5, 3}); // rear
                seg.addFace(new int[] {2, 3, 5, 4}, "clip"); // clip top
                seg.addFace(new int[] {4, 5, 7, 6}); // top
                seg.addFace(new int[] {8, 9, 11, 10}, "clip"); // clip bottom
                seg.addFace(new int[] {10, 11, 1, 0}); // bottom
                if (openingFace) {
                    seg.addFace(new int[] {0, 1, 3, 2});
                }
                if (closingFace) {
                    seg.addFace(new int[] {6, 7, 9, 8});
                }
                return seg;
            }
        }
        Object3D seg = new Object3D(color, true);
        seg.setProperty(Object3D.COLOR_PREFIX + "clip", clipColor);
        seg.addVertex(x0, y0b, zf);
        seg.addVertex(x0, y0b, zb);
        seg.addVertex(x0, wmax, zf);
        seg.addVertex(x0, wmax, zb);
        seg.addVertex(xpts[3], wmax, zf);
        seg.addVertex(xpts[3], wmax, zb);
        seg.addVertex(xpts[1], wmin, zf);
        seg.addVertex(xpts[1], wmin, zb);
        seg.addVertex(xpts[0], wmin, zf);
        seg.addVertex(xpts[0], wmin, zb);
        seg.addFace(new int[] {0, 2, 4, 6, 8}); // front
        seg.addFace(new int[] {1, 9, 7, 5, 3}); // rear
        seg.addFace(new int[] {2, 3, 5, 4}, "clip"); // clip top
        seg.addFace(new int[] {4, 5, 7, 6}); // top
        seg.addFace(new int[] {6, 7, 9, 8}, "clip"); // clip bottom
        seg.addFace(new int[] {8, 9, 1, 0}); // bottom
        if (openingFace) {
            seg.addFace(new int[] {0, 1, 3, 2});
        }
        // there is no closing face in this case
        return seg;
    }

    /**
     * Creates a segment for the case where the line end spans the entire axis
     * range at the left side of the chart.
     * 
     * @param x0
     * @param x1
     * @param xpts
     * @param y0b
     * @param y0t
     * @param y1b
     * @param y1t
     * @param wmin
     * @param wmax
     * @param zf
     * @param zb
     * @param color
     * @param clipColor
     * @param openingFace
     * @param closingFace
     * 
     * @return A segment. 
     */
    private Object3D createSegmentC(double x0, double x1, double[] xpts, 
            double y0b, double y0t, double y1b, double y1t, double wmin, 
            double wmax, double zf, double zb, Color color, Color clipColor, 
            boolean openingFace, boolean closingFace) {

        // the first 4 vertices and the opening face are common to all 
        // segments in this case
        Object3D seg = new Object3D(color, true);
        seg.setProperty(Object3D.COLOR_PREFIX + "clip", clipColor);
        seg.addVertex(x0, wmin, zf);
        seg.addVertex(x0, wmin, zb);
        seg.addVertex(x0, wmax, zf);
        seg.addVertex(x0, wmax, zb);
        if (openingFace) {
            seg.addFace(new int[] {0, 1, 3, 2});
        }
        
        if (y1b >= wmax) {
            seg.addVertex(xpts[2], wmax, zf);
            seg.addVertex(xpts[2], wmax, zb);
            seg.addVertex(xpts[0], wmin, zf);
            seg.addVertex(xpts[0], wmin, zb);
            seg.addFace(new int[] {0, 2, 4, 6}); // front
            seg.addFace(new int[] {1, 7, 5, 3}); // rear
            seg.addFace(new int[] {2, 3, 5, 4}, "clip"); // clip top
            seg.addFace(new int[] {4, 5, 7, 6}); // bottom
            seg.addFace(new int[] {7, 1, 0, 6}, "clip"); // bottom clip
            return seg;
        }
        if (y1t > wmax) {
            if (y1b >= wmin) {
                seg.addVertex(x1, wmax, zf);
                seg.addVertex(x1, wmax, zb);
                seg.addVertex(x1, y1b, zf);
                seg.addVertex(x1, y1b, zb);
                seg.addVertex(xpts[0], wmin, zf);
                seg.addVertex(xpts[0], wmin, zb);
                seg.addFace(new int[] {0, 2, 4, 6, 8}); // front
                seg.addFace(new int[] {1, 9, 7, 5, 3}); // rear
                seg.addFace(new int[] {2, 3, 5, 4}, "clip"); // top clip
                seg.addFace(new int[] {6, 7, 9, 8}); // bottom
                seg.addFace(new int[] {8, 9, 1, 0}, "clip"); // clip bottom
                if (closingFace) {
                    seg.addFace(new int[] {4, 5, 7, 6});
                }
                return seg;
            } else {
                seg.addVertex(x1, wmax, zf);
                seg.addVertex(x1, wmax, zb);
                seg.addVertex(x1, wmin, zf);
                seg.addVertex(x1, wmin, zb);
                seg.addFace(new int[] {0, 2, 4, 6}); // front
                seg.addFace(new int[] {1, 7, 5, 3}); // rear
                seg.addFace(new int[] {2, 3, 5, 4}, "clip"); // clip top
                seg.addFace(new int[] {4, 5, 7, 6}); // bottom
                seg.addFace(new int[] {7, 1, 0, 6}, "clip"); // bottom clip
                return seg;
            }
        }
        if (y1t > wmin) {
            if (y1b >= wmin) {
                return null; // in practice I don't think this case
                             // can occur
            } else {
                seg.addVertex(xpts[3], wmax, zf);
                seg.addVertex(xpts[3], wmax, zb);
                seg.addVertex(x1, y1t, zf);
                seg.addVertex(x1, y1t, zb);
                seg.addVertex(x1, wmin, zf);
                seg.addVertex(x1, wmin, zb);
                seg.addFace(new int[] {0, 2, 4, 6, 8}); // front
                seg.addFace(new int[] {1, 9, 7, 5, 3}); // rear
                seg.addFace(new int[] {2, 3, 5, 4}, "clip"); // clip top
                seg.addFace(new int[] {4, 5, 7, 6}); // top
                seg.addFace(new int[] {9, 1, 0, 8}, "clip"); // clip bottom
                if (closingFace) {
                    seg.addFace(new int[] {6, 7, 9, 8});
                }
                return seg; 
            }
        }
        seg.addVertex(xpts[3], wmax, zf);
        seg.addVertex(xpts[3], wmax, zb);
        seg.addVertex(xpts[1], wmin, zf);
        seg.addVertex(xpts[1], wmin, zb);
        seg.addFace(new int[] {0, 2, 4, 6}); // front
        seg.addFace(new int[] {1, 7, 5, 3}); // rear
        seg.addFace(new int[] {2, 3, 5, 4}, "clip"); // clip top
        seg.addFace(new int[] {4, 5, 7, 6}); // top
        seg.addFace(new int[] {6, 7, 1, 0}, "clip"); // clip bottom
        return seg;
    }
    
    /**
     * Creates a segment for the case where the segment is contained within
     * the axis range at the left side.
     * 
     * @param x0
     * @param x1
     * @param xpts
     * @param y0b
     * @param y0t
     * @param y1b
     * @param y1t
     * @param wmin
     * @param wmax
     * @param zf
     * @param zb
     * @param color
     * @param clipColor
     * @param openingFace
     * @param closingFace
     * 
     * @return A segment. 
     */
    private Object3D createSegmentD(double x0, double x1, double[] xpts, 
            double y0b, double y0t, double y1b, double y1t, double wmin, 
            double wmax, double zf, double zb, Color color, Color clipColor, 
            boolean openingFace, boolean closingFace) {

        Object3D seg = new Object3D(color, true);
        seg.setProperty(Object3D.COLOR_PREFIX + "clip", clipColor);
        seg.addVertex(x0, y0b, zf);
        seg.addVertex(x0, y0b, zb);
        seg.addVertex(x0, y0t, zf);
        seg.addVertex(x0, y0t, zb);
        if (y1b >= wmax) {
            seg.addVertex(xpts[3], wmax, zf);
            seg.addVertex(xpts[3], wmax, zb);
            seg.addVertex(xpts[2], wmax, zf);
            seg.addVertex(xpts[2], wmax, zb);
            seg.addFace(new int[] {0, 2, 4, 6});  // front
            seg.addFace(new int[] {1, 7, 5, 3});  // rear
            seg.addFace(new int[] {2, 3, 5, 4});  // top
            seg.addFace(new int[] {4, 5, 7, 6}, "clip"); // clip top
            seg.addFace(new int[] {0, 6, 7, 1}); // bottom
            if (openingFace) {
                seg.addFace(new int[] {0, 1, 3, 2});
            }
            // there is no closing face in this case
            return seg;
        }
        if (y1t > wmax) {
            if (y1b >= wmin) {
                seg.addVertex(xpts[3], wmax, zf);
                seg.addVertex(xpts[3], wmax, zb);
                seg.addVertex(x1, wmax, zf);
                seg.addVertex(x1, wmax, zb);
                seg.addVertex(x1, y1b, zf);
                seg.addVertex(x1, y1b, zb);
                seg.addFace(new int[] {0, 2, 4, 6, 8}); // front
                seg.addFace(new int[] {1, 9, 7, 5, 3}); // rear
                seg.addFace(new int[] {2, 3, 5, 4}); // top
                seg.addFace(new int[] {4, 5, 7, 6}, "clip"); // clip top
                seg.addFace(new int[] {0, 8, 9, 1});
                if (openingFace) {
                    seg.addFace(new int[] {0, 1, 3, 2});
                }
                if (closingFace) {
                    seg.addFace(new int[] {6, 7, 9, 8});
                }
                return seg;  
            } else {
                return null;  // this case should not be possible
            }
        }
        if (y1t > wmin) {
            if (y1b >= wmin) {
                // this is the regular segment, no clipping
                seg.addVertex(x1, y1t, zf);
                seg.addVertex(x1, y1t, zb);
                seg.addVertex(x1, y1b, zf);
                seg.addVertex(x1, y1b, zb);
                seg.addFace(new int[] {0, 2, 4, 6});  // front
                seg.addFace(new int[] {1, 7, 5, 3});  // rear
                seg.addFace(new int[] {2, 3, 5, 4});  // top
                seg.addFace(new int[] {0, 6, 7, 1});  // bottom
                if (openingFace) {
                    seg.addFace(new int[] {0, 1, 3, 2});
                }
                if (closingFace) {
                    seg.addFace(new int[] {4, 5, 7, 6});
                }
                return seg;
            } else {
                seg.addVertex(x1, y1t, zf);
                seg.addVertex(x1, y1t, zb);
                seg.addVertex(x1, wmin, zf);
                seg.addVertex(x1, wmin, zb);
                seg.addVertex(xpts[0], wmin, zf);
                seg.addVertex(xpts[0], wmin, zb);
                seg.addFace(new int[] {0, 2, 4, 6, 8}); // front
                seg.addFace(new int[] {1, 9, 7, 5, 3}); // rear
                seg.addFace(new int[] {2, 3, 5, 4}); // top
                seg.addFace(new int[] {0, 8, 9, 1});  // bottom
                seg.addFace(new int[] {6, 7, 9, 8}, "clip"); // clip bottom
                if (openingFace) {
                    seg.addFace(new int[] {0, 1, 3, 2});
                }
                if (closingFace) {
                    seg.addFace(new int[] {4, 5, 7, 6});
                }
                return seg;
            }
        } else {
            seg.addVertex(xpts[1], wmin, zf);
            seg.addVertex(xpts[1], wmin, zb);
            seg.addVertex(xpts[0], wmin, zf);
            seg.addVertex(xpts[0], wmin, zb);
            seg.addFace(new int[] {0, 2, 4, 6}); // front
            seg.addFace(new int[] {1, 7, 5, 3}); // rear
            seg.addFace(new int[] {2, 3, 5, 4}); // top
            seg.addFace(new int[] {0, 6, 7, 1}); // bottom
            seg.addFace(new int[] {4, 5, 7, 6}, "clip"); // clip bottom
            if (openingFace) {
                seg.addFace(new int[] {0, 1, 3, 2});
            }
            // there is no closing face in this case
            return seg;
        }
    }
    
    /**
     * Returns a segment for the case where the line height spans the lower 
     * bound of the axis range at the left side of the chart.
     * 
     * @param x0
     * @param x1
     * @param xpts
     * @param y0b
     * @param y0t
     * @param y1b
     * @param y1t
     * @param wmin
     * @param wmax
     * @param zf
     * @param zb
     * @param color
     * @param clipColor
     * @param openingFace
     * @param closingFace
     * 
     * @return The segment. 
     */
    private Object3D createSegmentE(double x0, double x1, double[] xpts, 
            double y0b, double y0t, double y1b, double y1t, double wmin, 
            double wmax, double zf, double zb, Color color, Color clipColor, 
            boolean openingFace, boolean closingFace) {
        if (y1b > wmax) {
            Object3D seg = new Object3D(color, true);
            seg.setProperty(Object3D.COLOR_PREFIX + "clip", clipColor);
            seg.addVertex(x0, wmin, zf);
            seg.addVertex(x0, wmin, zb);
            seg.addVertex(x0, y0t, zf);
            seg.addVertex(x0, y0t, zb);
            seg.addVertex(xpts[3], wmax, zf);
            seg.addVertex(xpts[3], wmax, zb);
            seg.addVertex(xpts[2], wmax, zf);
            seg.addVertex(xpts[2], wmax, zb);
            seg.addVertex(xpts[0], wmin, zf);
            seg.addVertex(xpts[0], wmin, zb);
            seg.addFace(new int[] {0, 2, 4, 6, 8}); // front
            seg.addFace(new int[] {1, 9, 7, 5, 3}); // rear
            seg.addFace(new int[] {2, 3, 5, 4}); // top
            seg.addFace(new int[] {4, 5, 7, 6}, "clip"); // clip top
            seg.addFace(new int[] {6, 7, 9, 8}); // bottom
            seg.addFace(new int[] {0, 8, 9, 1}, "clip"); // clip bottom
            if (openingFace) {
                seg.addFace(new int[] {0, 1, 3, 2});
            }
            return seg;
        }
        if (y1t > wmax) {
            if (y1b >= wmin) {
                Object3D seg = new Object3D(color, true);
                seg.setProperty(Object3D.COLOR_PREFIX + "clip", clipColor);
                seg.addVertex(x0, wmin, zf);
                seg.addVertex(x0, wmin, zb);
                seg.addVertex(x0, y0t, zf);
                seg.addVertex(x0, y0t, zb);
                seg.addVertex(xpts[3], wmax, zf);
                seg.addVertex(xpts[3], wmax, zb);
                seg.addVertex(x1, wmax, zf);
                seg.addVertex(x1, wmax, zb);
                seg.addVertex(x1, y1b, zf);
                seg.addVertex(x1, y1b, zb);
                seg.addVertex(xpts[0], wmin, zf);
                seg.addVertex(xpts[0], wmin, zb);
                seg.addFace(new int[] {0, 2, 4, 6, 8, 10}); // front
                seg.addFace(new int[] {1, 11, 9, 7, 5, 3}); // rear
                seg.addFace(new int[] {2, 3, 5, 4}); // top
                seg.addFace(new int[] {5, 7, 6, 4}, "clip"); // clip top
                seg.addFace(new int[] {8, 9, 11, 10}); // bottom
                seg.addFace(new int[] {1, 0, 10, 11}, "clip");
                if (openingFace) {
                    seg.addFace(new int[] {0, 1, 3, 2}); 
                }
                if (closingFace) {
                    seg.addFace(new int[] {6, 7, 9, 8}); 
                }
                return seg;
            } else {
                Object3D seg = new Object3D(color, true);
                seg.setProperty(Object3D.COLOR_PREFIX + "clip", clipColor);
                seg.addVertex(x0, wmin, zf);
                seg.addVertex(x0, wmin, zb);
                seg.addVertex(x0, y0t, zf);
                seg.addVertex(x0, y0t, zb);
                seg.addVertex(xpts[3], wmax, zf);
                seg.addVertex(xpts[3], wmax, zb);
                seg.addVertex(x1, wmax, zf);
                seg.addVertex(x1, wmax, zb);
                seg.addVertex(x1, wmin, zf);
                seg.addVertex(x1, wmin, zb);
                seg.addFace(new int[] {0, 2, 4, 6, 8}); // front
                seg.addFace(new int[] {1, 9, 7, 5, 3}); // rear
                seg.addFace(new int[] {2, 3, 5, 4}); // top
                seg.addFace(new int[] {5, 7, 6, 4}, "clip"); // clip top
                seg.addFace(new int[] {0, 8, 9, 1}, "clip"); // clip bottom
                if (openingFace) {
                    seg.addFace(new int[] {0, 1, 3, 2}); 
                }
                if (closingFace) {
                    seg.addFace(new int[] {6, 7, 9, 8}); 
                }
                return seg;  
            }
        }
        if (y1t > wmin) {
            if (y1b >= wmin) {
                Object3D seg = new Object3D(color, true);
                seg.setProperty(Object3D.COLOR_PREFIX + "clip", clipColor);
                seg.addVertex(x0, wmin, zf);
                seg.addVertex(x0, wmin, zb);
                seg.addVertex(x0, y0t, zf);
                seg.addVertex(x0, y0t, zb);
                seg.addVertex(x1, y1t, zf);                
                seg.addVertex(x1, y1t, zb);
                seg.addVertex(x1, y1b, zf);                
                seg.addVertex(x1, y1b, zb);
                seg.addVertex(xpts[0], wmin, zf);
                seg.addVertex(xpts[0], wmin, zb);
                seg.addFace(new int[] {0, 2, 4, 6, 8}); // front
                seg.addFace(new int[] {1, 9, 7, 5, 3}); // rear
                seg.addFace(new int[] {2, 3, 5, 4}); // top
                seg.addFace(new int[] {6, 7, 9, 8}); // bottom
                seg.addFace(new int[] {0, 8, 9, 1}, "clip"); // clip bottom
                if (openingFace) {
                    seg.addFace(new int[] {0, 1, 3, 2});
                }
                if (closingFace) {
                    seg.addFace(new int[] {4, 5, 7, 6});
                }
                return seg;
            } else {
                Object3D seg = new Object3D(color, true);
                seg.setProperty(Object3D.COLOR_PREFIX + "clip", clipColor);
                seg.addVertex(x0, wmin, zf);
                seg.addVertex(x0, wmin, zb);
                seg.addVertex(x0, y0t, zf);
                seg.addVertex(x0, y0t, zb);
                seg.addVertex(x1, y1t, zf);                
                seg.addVertex(x1, y1t, zb);
                seg.addVertex(x1, wmin, zf);
                seg.addVertex(x1, wmin, zb);
                seg.addFace(new int[] {0, 2, 4, 6}); // front
                seg.addFace(new int[] {1, 7, 5, 3}); // rear
                seg.addFace(new int[] {2, 3, 5, 4}); // top
                seg.addFace(new int[] {0, 6, 7, 1}, "clip"); // clip bottom
                if (openingFace) {
                    seg.addFace(new int[] {0, 1, 3, 2});
                }
                if (closingFace) {
                    seg.addFace(new int[] {4, 5, 7, 6});
                }
                return seg;
            }
        }
        Object3D seg = new Object3D(color, true);
        seg.setProperty(Object3D.COLOR_PREFIX + "clip", clipColor);
        seg.addVertex(x0, wmin, zf);
        seg.addVertex(x0, wmin, zb);
        seg.addVertex(x0, y0t, zf);
        seg.addVertex(x0, y0t, zb);
        seg.addVertex(xpts[1], wmin, zf);
        seg.addVertex(xpts[1], wmin, zb);
        seg.addFace(new int[] {0, 2, 4}); // front
        seg.addFace(new int[] {1, 5, 3}); // rear
        seg.addFace(new int[] {2, 3, 5, 4}); // top
        seg.addFace(new int[] {0, 4, 5, 1}, "clip"); // clip bottom
        if (openingFace) {
            seg.addFace(new int[] {0, 1, 3, 2});
        }
        // there is no closing face in this case
        return seg;
    }
    
    /**
     * Creates and returns a segment for the case where the line is completely
     * below the axis range at the left side.
     * 
     * @param x0
     * @param x1
     * @param xpts
     * @param y0b
     * @param y0t
     * @param y1b
     * @param y1t
     * @param wmin
     * @param wmax
     * @param zf
     * @param zb
     * @param color
     * @param clipColor
     * @param openingFace  ignored because there is no opening face in this 
     *     case.
     * @param closingFace
     * 
     * @return A segment. 
     */
    private Object3D createSegmentF(double x0, double x1, double[] xpts, 
            double y0b, double y0t, double y1b, double y1t, double wmin, 
            double wmax, double zf, double zb, Color color, Color clipColor, 
            boolean openingFace, boolean closingFace) {

        if (y1b > wmax) {
            Object3D seg = new Object3D(color, true);
            seg.setProperty(Object3D.COLOR_PREFIX + "clip", clipColor);
            seg.addVertex(xpts[1], wmin, zf);
            seg.addVertex(xpts[1], wmin, zb);
            seg.addVertex(xpts[3], wmax, zf);
            seg.addVertex(xpts[3], wmax, zb);
            seg.addVertex(xpts[2], wmax, zf);
            seg.addVertex(xpts[2], wmax, zb);
            seg.addVertex(xpts[0], wmin, zf);
            seg.addVertex(xpts[0], wmin, zb);
            seg.addFace(new int[] {0, 2, 4, 6}); // front
            seg.addFace(new int[] {1, 7, 5, 3}); // rear
            seg.addFace(new int[] {0, 1, 3, 2}); // top
            seg.addFace(new int[] {2, 3, 5, 4}, "clip"); // clip top
            seg.addFace(new int[] {4, 5, 7, 6}); // bottom
            seg.addFace(new int[] {0, 6, 7, 1}, "clip"); // clip bottom
            // there are no opening and closing faces for this case
            return seg;
        }
        if (y1t > wmax) {
            if (y1b > wmin) {
                Object3D seg = new Object3D(color, true);
                seg.setProperty(Object3D.COLOR_PREFIX + "clip", clipColor);
                seg.addVertex(xpts[1], wmin, zf);
                seg.addVertex(xpts[1], wmin, zb);
                seg.addVertex(xpts[3], wmax, zf);
                seg.addVertex(xpts[3], wmax, zb);
                seg.addVertex(x1, wmax, zf);
                seg.addVertex(x1, wmax, zb);
                seg.addVertex(x1, y1b, zf);
                seg.addVertex(x1, y1b, zb);
                seg.addVertex(xpts[0], wmin, zf);
                seg.addVertex(xpts[0], wmin, zb);
                seg.addFace(new int[] {0, 2, 4, 6, 8}); // front
                seg.addFace(new int[] {1, 9, 7, 5, 3}); // rear
                seg.addFace(new int[] {2, 3, 5, 4}); //clip top
                seg.addFace(new int[] {0, 1, 3, 2}); // top
                seg.addFace(new int[] {0, 8, 9, 1}, "clip"); // clip bottom
                seg.addFace(new int[] {6, 7, 9, 8}); // bottom
                // there is no opening face in this case
                if (closingFace) {
                    seg.addFace(new int[] {4, 5, 7, 6});
                }
                return seg;
            } else {
                Object3D seg = new Object3D(color, true);
                seg.setProperty(Object3D.COLOR_PREFIX + "clip", clipColor);
                seg.addVertex(xpts[1], wmin, zf);
                seg.addVertex(xpts[1], wmin, zb);
                seg.addVertex(xpts[3], wmax, zf);
                seg.addVertex(xpts[3], wmax, zb);
                seg.addVertex(x1, wmax, zf);
                seg.addVertex(x1, wmax, zb);
                seg.addVertex(x1, wmin, zf);
                seg.addVertex(x1, wmin, zb);
                seg.addFace(new int[] {0, 2, 4, 6}); // front
                seg.addFace(new int[] {1, 7, 5, 3}); // rear
                seg.addFace(new int[] {0, 1, 3, 2}); // top
                seg.addFace(new int[] {2, 3, 5, 4}, "clip"); // clip top
                seg.addFace(new int[] {6, 7, 1, 0}, "clip"); // clip bottom
                if (closingFace) {
                    seg.addFace(new int[] {4, 5, 7, 6});
                }
                return seg;
            }
        }
        if (y1t > wmin) {
            if (y1b >= wmin) {
                Object3D seg = new Object3D(color, true);
                seg.setProperty(Object3D.COLOR_PREFIX + "clip", clipColor);
                seg.addVertex(xpts[1], wmin, zf);
                seg.addVertex(xpts[1], wmin, zb);
                seg.addVertex(x1, y1t, zf);
                seg.addVertex(x1, y1t, zb);
                seg.addVertex(x1, y1b, zf);
                seg.addVertex(x1, y1b, zb);
                seg.addVertex(xpts[0], wmin, zf);
                seg.addVertex(xpts[0], wmin, zb);                
                seg.addFace(new int[] {0, 2, 4, 6}); // front
                seg.addFace(new int[] {1, 7, 5, 3}); // rear
                seg.addFace(new int[] {0, 1, 3, 2}); // top
                seg.addFace(new int[] {4, 5, 7, 6}); // bottom
                seg.addFace(new int[] {0, 6, 7, 1}, "clip"); // clip bottom
                if (closingFace) {
                    seg.addFace(new int[] {2, 3, 5, 4});
                }
                return seg;
            } else {
                Object3D seg = new Object3D(color, true);
                seg.setProperty(Object3D.COLOR_PREFIX + "clip", clipColor);
                seg.addVertex(xpts[1], wmin, zf);
                seg.addVertex(xpts[1], wmin, zb);
                seg.addVertex(x1, y1t, zf);
                seg.addVertex(x1, y1t, zb);
                seg.addVertex(x1, wmin, zf);
                seg.addVertex(x1, wmin, zb);
                seg.addFace(new int[] {0, 2, 4}); // front
                seg.addFace(new int[] {1, 5, 3}); // rear
                seg.addFace(new int[] {0, 1, 3, 2}); // top
                seg.addFace(new int[] {0, 4, 5, 1}, "clip"); // clip bottom
                // there is no opening face in this case
                if (closingFace) {
                    seg.addFace(new int[] {2, 3, 5, 4});
                }
                return seg;
            }
        }
        return null;  // nothing to see
    }

    /**
     * Tests this renderer for equality with an arbitrary object.
     * 
     * @param obj  the object ({@code null} not permitted).
     * 
     * @return A boolean. 
     */
    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (!(obj instanceof LineRenderer3D)) {
            return false;
        }
        LineRenderer3D that = (LineRenderer3D) obj;
        if (this.lineWidth != that.lineWidth) {
            return false;
        }
        if (this.lineHeight != that.lineHeight) {
            return false;
        }
        if (this.isolatedItemWidthPercent != that.isolatedItemWidthPercent) {
            return false;
        }
        if (!ObjectUtils.equals(this.clipColorSource, that.clipColorSource)) {
            return false;
        }
        return super.equals(obj);
    }
}

