/* ===========================================================
* 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 java.util.ArrayList;
import java.util.List;
import com.orsoncharts.axis.CategoryAxis3D;
import com.orsoncharts.Chart3DFactory;
import com.orsoncharts.Range;
import com.orsoncharts.axis.ValueAxis3D;
import com.orsoncharts.data.category.CategoryDataset3D;
import com.orsoncharts.data.DataUtils;
import com.orsoncharts.data.Values3D;
import com.orsoncharts.data.KeyedValues3DItemKey;
import com.orsoncharts.graphics3d.Dimension3D;
import com.orsoncharts.graphics3d.Object3D;
import com.orsoncharts.graphics3d.Offset3D;
import com.orsoncharts.graphics3d.Utils2D;
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 for creating 3D area charts from data in a
* {@link CategoryDataset3D} (for use with a {@link CategoryPlot3D}). For
* example:
* <div>
* <object id="ABC" data="../../../../doc-files/AreaChart3DDemo1.svg"
* type="image/svg+xml" width="500" height="359">
* </object>
* </div>
* (refer to {@code AreaChart3DDemo1.java} for the code to generate the
* above chart).
* <br><br>
* There is a factory method to create a chart using this renderer - see
* {@link Chart3DFactory#createAreaChart(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 AreaRenderer3D extends AbstractCategoryRenderer3D
implements Serializable {
/** The base for the areas (defaults to 0.0). */
private double base;
/**
* The color used to paint the underside of the area object (if
* {@code null}, the regular series color is used).
*/
private Color baseColor;
/** The depth of the area. */
private double depth;
/**
* 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;
/**
* A flag that controls whether or not outlines are drawn for the faces
* making up the area segments.
*/
private boolean drawFaceOutlines;
/**
* Default constructor.
*/
public AreaRenderer3D() {
this.base = 0.0;
this.baseColor = null;
this.depth = 0.6;
this.isolatedItemWidthPercent = 0.25;
this.clipColorSource = new StandardCategoryColorSource(Color.RED);
this.drawFaceOutlines = true;
}
/**
* Returns the y-value for the base of the area. The default value is
* {@code 0.0}.
*
* @return The base value.
*/
public double getBase() {
return this.base;
}
/**
* Sets the base value and sends a change event to all registered listeners.
*
* @param base the base value.
*/
public void setBase(double base) {
this.base = base;
fireChangeEvent(true);
}
/**
* Returns the color used to paint the underside of the area polygons.
* The default value is {@code null} (which means the undersides are
* painted using the regular series color).
*
* @return The color (possibly {@code null}).
*
* @see #setBaseColor(java.awt.Color)
*/
public Color getBaseColor() {
return this.baseColor;
}
/**
* Sets the color for the underside of the area shapes and sends a
* change event to all registered listeners. If you set
* this to {@code null} the base will be painted with the regular
* series color.
*
* @param color the color ({@code null} permitted).
*/
public void setBaseColor(Color color) {
this.baseColor = color;
fireChangeEvent(true);
}
/**
* Returns the depth (in 3D) for the area (in world units). The
* default value is {@code 0.6}.
*
* @return The depth.
*/
public double getDepth() {
return this.depth;
}
/**
* Sets the depth (in 3D) and sends a change event to all registered
* listeners.
*
* @param depth the depth.
*/
public void setDepth(double depth) {
this.depth = depth;
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}).
*
* @since 1.3
*/
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).
*
* @since 1.3
*/
public void setClipColorSource(CategoryColorSource source) {
this.clipColorSource = source;
fireChangeEvent(true);
}
/**
* Returns the flag that controls whether or not the faces making up area
* segments will be drawn with outlines. The default value is
* {@code true}. When anti-aliasing is on, the fill area for the
* faces will have some gray shades around the edges, and these will show
* up on the chart as thin lines (usually not visible if you turn off
* anti-aliasing). To mask this, the rendering engine can draw an outline
* around each face in the same color (this usually results in cleaner
* output, but it is slower and can introduce some minor visual artifacts
* as well depending on the output target).
*
* @return A boolean.
*
* @since 1.3
*/
public boolean getDrawFaceOutlines() {
return this.drawFaceOutlines;
}
/**
* Sets the flag that controls whether or not outlines are drawn for the
* faces making up the area segments and sends a change event to all
* registered listeners.
*
* @param outline the new flag value.
*
* @since 1.3
*/
public void setDrawFaceOutlines(boolean outline) {
this.drawFaceOutlines = outline;
fireChangeEvent(true);
}
/**
* Returns the range (for the value axis) that is required for this
* renderer to show all the values in the specified data set. This method
* is overridden to ensure that the range includes the base value (normally
* 0.0) set for the renderer.
*
* @param data the data ({@code null} not permitted).
*
* @return The range.
*/
@Override
public Range findValueRange(Values3D<? extends Number> data) {
return DataUtils.findValueRange(data, this.base);
}
/**
* 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 @SuppressWarnings("unchecked")
public void composeItem(CategoryDataset3D dataset, int series, int row,
int column, World world, Dimension3D dimensions,
double xOffset, double yOffset, double zOffset) {
Number y = (Number) dataset.getValue(series, row, column);
Number yprev = null;
if (column > 0) {
yprev = (Number) dataset.getValue(series, row, column - 1);
}
Number ynext = null;
if (column < dataset.getColumnCount() - 1) {
ynext = (Number) 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 area 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;
// for the first column there is no left segment, we also handle the
// special case where there is just one column of data in which case
// the renderer can only show an isolated data value
if (column == 0) {
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);
}
}
// for the last column there is no right segment
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);
}
// for the general case we handle left and right segments or an
// isolated segment if the surrounding data values are null
else {
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...world coordinates are required
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;
double basew = valueAxis.translateToWorld(this.base, 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;
assert yprev != null; // we know this because createLeftSegment is
// not 'true' otherwise
double yprevw = valueAxis.translateToWorld(yprev.doubleValue(), hh)
+ yOffset;
double yl = (yprevw + yw) / 2.0;
List<Object3D> leftObjs = createSegment(xl, yl, xw, yw, zw,
basew, ywmin, ywmax, color, this.baseColor, clipColor,
leftOpen, leftClose);
for (Object3D obj : leftObjs) {
obj.setProperty(Object3D.ITEM_KEY, itemKey);
obj.setOutline(this.drawFaceOutlines);
world.add(obj);
}
}
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;
assert ynext != null; // we know this because createRightSegment is
// not 'true' otherwise
double ynextw = valueAxis.translateToWorld(ynext.doubleValue(), hh)
+ yOffset;
double yr = (ynextw + yw) / 2.0;
List<Object3D> rightObjs = createSegment(xw, yw, xr, yr, zw,
basew, ywmin, ywmax, color, this.baseColor, clipColor,
rightOpen, rightClose);
for (Object3D obj : rightObjs) {
obj.setProperty(Object3D.ITEM_KEY, itemKey);
obj.setOutline(this.drawFaceOutlines);
world.add(obj);
}
}
if (createIsolatedSegment) {
double cw = columnAxis.getCategoryWidth()
* this.isolatedItemWidthPercent;
double cww = columnAxis.translateToWorld(cw, ww);
double h = yw - basew;
Object3D isolated = Object3D.createBox(xw, cww, yw - h / 2, h,
zw, this.depth, color);
isolated.setOutline(this.drawFaceOutlines);
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);
ItemLabelPositioning positioning = getItemLabelPositioning();
Offset3D offsets = getItemLabelOffsets();
double ydelta = dimensions.getHeight() * offsets.getDY();
if (yw < basew) {
ydelta = -ydelta;
}
if (positioning.equals(ItemLabelPositioning.CENTRAL)) {
Object3D labelObj = Object3D.createLabelObject(label,
getItemLabelFont(), getItemLabelColor(),
getItemLabelBackgroundColor(), xw, yw + ydelta, zw,
false, true);
labelObj.setProperty(Object3D.ITEM_KEY, itemKey);
world.add(labelObj);
} else if (positioning.equals(
ItemLabelPositioning.FRONT_AND_BACK)) {
double zdelta = this.depth / 2 * offsets.getDZ();
Object3D labelObj1 = Object3D.createLabelObject(label,
getItemLabelFont(), getItemLabelColor(),
getItemLabelBackgroundColor(), xw, yw + ydelta,
zw - zdelta, false, false);
labelObj1.setProperty(Object3D.CLASS_KEY, "ItemLabel");
labelObj1.setProperty(Object3D.ITEM_KEY, itemKey);
world.add(labelObj1);
Object3D labelObj2 = Object3D.createLabelObject(label,
getItemLabelFont(), getItemLabelColor(),
getItemLabelBackgroundColor(), xw, yw + ydelta,
zw + zdelta, true, false);
labelObj2.setProperty(Object3D.CLASS_KEY, "ItemLabel");
labelObj2.setProperty(Object3D.ITEM_KEY, itemKey);
world.add(labelObj2);
}
}
}
/**
* Creates objects to represent the area segment between (x0, y0) and
* (x1, y1).
*
* @param x0
* @param y0
* @param x1
* @param y1
* @param z
* @param lineWidth
* @param lineHeight
* @param ymin
* @param ymax
* @param color
* @param clipColor
* @param openingFace
* @param closingFace
*
* @return A list of objects making up the segment.
*/
private List<Object3D> createSegment(double x0, double y0, double x1,
double y1, double z, double base, double ymin, double ymax,
Color color, Color baseColor, Color clipColor, boolean openingFace,
boolean closingFace) {
List<Object3D> result = new ArrayList<Object3D>(2);
// either there is a crossing or there is not
if (!isBaselineCrossed(y0, y1, base)) {
Object3D segment = createSegmentWithoutCrossing(x0, y0, x1, y1,
z, base, ymin, ymax, color, baseColor, clipColor,
openingFace, closingFace);
result.add(segment);
} else {
result.addAll(createSegmentWithCrossing(x0, y0, x1, y1,
z, base, ymin, ymax, color, baseColor, clipColor,
openingFace, closingFace));
}
return result;
}
/**
* Returns {@code true} if the two values are on opposite sides of
* the baseline. If the data values cross the baseline, then we need
* to construct two 3D objects to represent the data, whereas if there is
* no crossing, a single 3D object will be sufficient.
*
* @param y0 the first value.
* @param y1 the second value.
* @param baseline the baseline.
*
* @return A boolean.
*/
private boolean isBaselineCrossed(double y0, double y1, double baseline) {
return (y0 > baseline && y1 < baseline)
|| (y0 < baseline && y1 > baseline);
}
private Object3D createSegmentWithoutCrossing(double x0, double y0,
double x1, double y1, double z, double base, double ymin,
double ymax, Color color, Color baseColor, Color clipColor,
boolean openingFace, boolean closingFace) {
boolean positive = y0 > base || y1 > base;
if (positive) {
Object3D pos = createPositiveArea(x0, y0, x1, y1, base,
z, new Range(ymin, ymax), color, openingFace,
closingFace);
return pos;
} else {
Object3D neg = createNegativeArea(x0, y0, x1, y1, base, z,
new Range(ymin, ymax), color, openingFace, closingFace);
return neg;
}
}
private List<Object3D> createSegmentWithCrossing(double x0, double y0,
double x1, double y1, double z, double base, double ymin,
double ymax, Color color, Color baseColor, Color clipColor,
boolean openingFace, boolean closingFace) {
List<Object3D> result = new ArrayList<Object3D>(2);
Range range = new Range(ymin, ymax);
// find the crossing point
double ydelta = Math.abs(y1 - y0);
double factor = 0;
if (ydelta != 0.0) {
factor = Math.abs(y0 - base) / ydelta;
}
double xcross = x0 + factor * (x1 - x0);
if (y0 > base) {
Object3D pos = createPositiveArea(x0, y0, xcross, base, base, z,
range, color, openingFace, closingFace);
if (pos != null) {
result.add(pos);
}
Object3D neg = createNegativeArea(xcross, base, x1, y1,
base, z, range, color, openingFace, closingFace);
if (neg != null) {
result.add(neg);
}
} else {
Object3D neg = createNegativeArea(x0, y0, xcross, base,
base, z, range, color, openingFace, closingFace);
if (neg != null) {
result.add(neg);
}
Object3D pos = createPositiveArea(xcross, base, x1, y1, base,
z, range, color, openingFace, closingFace);
if (pos != null) {
result.add(pos);
}
}
return result;
}
/**
* A utility method that returns the fraction (x - x0) / (x1 - x0), which
* is used for some interpolation calculations.
*
* @param x the x-value.
* @param x0 the start of a range.
* @param x1 the end of a range.
*
* @return The fractional value of x along the range x0 to x1.
*/
private double fraction(double x, double x0, double x1) {
double dist = x - x0;
double length = x1 - x0;
return dist / length;
}
/**
* A value in world units that is considered small enough to be not
* significant. We use this to check if two coordinates are "more or less"
* the same.
*/
private static final double EPSILON = 0.001;
/**
* Creates a 3D object to represent a positive "area", taking into account
* that the visible range can be restricted.
*
* @param color the color ({@code null} not permitted).
* @param wx0
* @param wy0
* @param wx1
* @param wy1
* @param wbase
* @param wz
* @param range
* @param openingFace
* @param closingFace
*
* @return A 3D object or {@code null}.
*/
private Object3D createPositiveArea(double wx0, double wy0,
double wx1, double wy1, double wbase, double wz, Range range,
Color color, boolean openingFace, boolean closingFace) {
if (!range.intersects(wy0, wbase) && !range.intersects(wy1, wbase)) {
return null;
}
double wy00 = range.peggedValue(wy0);
double wy11 = range.peggedValue(wy1);
double wbb = range.peggedValue(wbase);
double wx00 = wx0;
if (wy0 < range.getMin()) {
wx00 = wx0 + (wx1 - wx0) * fraction(wy00, wy0, wy1);
}
double wx11 = wx1;
if (wy1 < range.getMin()) {
wx11 = wx1 - (wx1 - wx0) * fraction(wy11, wy1, wy0);
}
double wx22 = Double.NaN; // bogus
boolean p2required = Utils2D.spans(range.getMax(), wy0, wy1);
if (p2required) {
wx22 = wx0 + (wx1 - wx0) * fraction(range.getMax(), wy0, wy1);
}
double delta = this.depth / 2.0;
// create an area shape
Object3D obj = new Object3D(color, true);
obj.addVertex(wx00, wbb, wz - delta);
obj.addVertex(wx00, wbb, wz + delta);
boolean leftSide = false;
if (Math.abs(wy00 - wbb) > EPSILON) {
leftSide = true;
obj.addVertex(wx00, wy00, wz - delta);
obj.addVertex(wx00, wy00, wz + delta);
}
if (p2required) {
obj.addVertex(wx22, range.getMax(), wz - delta);
obj.addVertex(wx22, range.getMax(), wz + delta);
}
obj.addVertex(wx11, wy11, wz - delta);
obj.addVertex(wx11, wy11, wz + delta);
boolean rightSide = false;
if (Math.abs(wy11 - wbb) > EPSILON) {
rightSide = true;
obj.addVertex(wx11, wbb, wz - delta);
obj.addVertex(wx11, wbb, wz + delta);
}
int vertices = obj.getVertexCount();
if (vertices == 10) {
obj.addFace(new int[] {0, 2, 4, 6, 8}); // front
obj.addFace(new int[] {1, 9, 7, 5, 3}); // rear
obj.addFace(new int[] {0, 8, 9, 1}); // base
obj.addFace(new int[] {2, 3, 5, 4}); // top 1
obj.addFace(new int[] {4, 5, 7, 6}); // top 2
if (openingFace) {
obj.addFace(new int[] {0, 1, 3, 2});
}
if (closingFace) {
obj.addFace(new int[] {6, 7, 9, 8});
}
} else if (vertices == 8) {
obj.addFace(new int[] {0, 2, 4, 6}); // front
obj.addFace(new int[] {7, 5, 3, 1}); // rear
if (!leftSide) {
obj.addFace(new int[] {0, 1, 3, 2}); // top left
}
obj.addFace(new int[] {2, 3, 5, 4}); // top 1
if (!rightSide) {
obj.addFace(new int[] {4, 5, 7, 6}); // top 2
}
obj.addFace(new int[] {1, 0, 6, 7}); // base
if (openingFace) {
obj.addFace(new int[] {0, 1, 3, 2});
}
if (closingFace) {
obj.addFace(new int[] {4, 5, 7, 6});
}
} else if (vertices == 6) {
obj.addFace(new int[] {0, 2, 4}); // front
obj.addFace(new int[] {5, 3, 1}); // rear
if (leftSide) {
obj.addFace(new int[] {3, 5, 4, 2}); // top
if (openingFace) {
obj.addFace(new int[] {0, 1, 3, 2});
}
} else {
obj.addFace(new int[] {0, 1, 3, 2}); // top
if (closingFace) {
obj.addFace(new int[] {2, 3, 5, 4});
}
}
obj.addFace(new int[] {0, 4, 5, 1}); // base
} else {
obj.addFace(new int[] {0, 1, 3, 2});
obj.addFace(new int[] {2, 3, 1, 0});
}
return obj;
}
/**
* Creates a negative area shape from (wx0, wy0) to (wx1, wy1) with the
* base at wbase (it is assumed that both wy0 and wy1 are less than wbase).
*
* @param wx0
* @param wy0
* @param wx1
* @param wy1
* @param wbase
* @param wz
* @param range
* @param color
* @param openingFace
* @param closingFace
*
* @return An object representing the area shape (or {@code null}).
*/
private Object3D createNegativeArea(double wx0, double wy0,
double wx1, double wy1, double wbase, double wz, Range range,
Color color, boolean openingFace, boolean closingFace) {
if (!range.intersects(wy0, wbase) && !range.intersects(wy1, wbase)) {
return null;
}
double wy00 = range.peggedValue(wy0);
double wy11 = range.peggedValue(wy1);
double wbb = range.peggedValue(wbase);
double wx00 = wx0;
if (wy0 > range.getMax()) {
wx00 = wx0 + (wx1 - wx0) * fraction(wy00, wy0, wy1);
}
double wx11 = wx1;
if (wy1 > range.getMax()) {
wx11 = wx1 - (wx1 - wx0) * fraction(wy11, wy1, wy0);
}
double wx22 = (wx00 + wx11) / 2.0; // bogus
boolean p2required = Utils2D.spans(range.getMin(), wy0, wy1);
if (p2required) {
wx22 = wx0 + (wx1 - wx0) * fraction(range.getMin(), wy0, wy1);
}
double delta = this.depth / 2.0;
// create an area shape
Object3D obj = new Object3D(color, true);
obj.addVertex(wx00, wbb, wz - delta);
obj.addVertex(wx00, wbb, wz + delta);
boolean leftSide = false;
if (Math.abs(wy00 - wbb) > EPSILON) {
leftSide = true;
obj.addVertex(wx00, wy00, wz - delta);
obj.addVertex(wx00, wy00, wz + delta);
}
if (p2required) {
obj.addVertex(wx22, range.getMin(), wz - delta);
obj.addVertex(wx22, range.getMin(), wz + delta);
}
obj.addVertex(wx11, wy11, wz - delta);
obj.addVertex(wx11, wy11, wz + delta);
boolean rightSide = false;
if (Math.abs(wy11 - wbb) > EPSILON) {
obj.addVertex(wx11, wbb, wz - delta);
obj.addVertex(wx11, wbb, wz + delta);
}
int vertices = obj.getVertexCount();
if (vertices == 10) {
obj.addFace(new int[] {8, 6, 4, 2, 0}); // front
obj.addFace(new int[] {1, 3, 5, 7, 9}); // rear
obj.addFace(new int[] {1, 9, 8, 0}); // base
obj.addFace(new int[] {4, 5, 3, 2}); // top 1
obj.addFace(new int[] {6, 7, 5, 4}); // top 2
if (openingFace) {
obj.addFace(new int[] {2, 3, 1, 0});
}
if (closingFace) {
obj.addFace(new int[] {8, 9, 7, 6});
}
} else if (vertices == 8) {
obj.addFace(new int[] {2, 0, 6, 4}); // front
obj.addFace(new int[] {1, 3, 5, 7}); // rear
obj.addFace(new int[] {0, 1, 7, 6}); // base
if (!leftSide) {
obj.addFace(new int[] {2, 3, 1, 0});
}
obj.addFace(new int[] {3, 2, 4, 5}); // negative top
if (!rightSide) {
obj.addFace(new int[] {6, 7, 5, 4});
}
if (openingFace) {
obj.addFace(new int[] {1, 0, 2, 3});
}
if (closingFace) {
obj.addFace(new int[] {5, 4, 6, 7});
}
} else if (vertices == 6) {
obj.addFace(new int[] {4, 2, 0}); // front
obj.addFace(new int[] {1, 3, 5}); // rear
if (leftSide) {
obj.addFace(new int[] {4, 5, 3, 2}); // negative top
if (openingFace) {
obj.addFace(new int[] {1, 0, 2, 3});
}
} else {
obj.addFace(new int[] {2, 3, 1, 0}); // negative top
if (closingFace) {
obj.addFace(new int[] {3, 2, 4, 5});
}
}
obj.addFace(new int[] {0, 1, 5, 4}); // base
} else {
obj.addFace(new int[] {0, 1, 3, 2});
obj.addFace(new int[] {2, 3, 1, 0});
}
return obj;
}
/**
* Tests this renderer for equality with an arbitrary object.
*
* @param obj the object ({@code null} permitted).
*
* @return A boolean.
*/
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof AreaRenderer3D)) {
return false;
}
AreaRenderer3D that = (AreaRenderer3D) obj;
if (this.base != that.base) {
return false;
}
if (!ObjectUtils.equals(this.baseColor, that.baseColor)) {
return false;
}
if (this.depth != that.depth) {
return false;
}
return super.equals(obj);
}
}