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.renderer.category; 034 035import java.awt.Color; 036import java.io.Serializable; 037import java.util.ArrayList; 038import java.util.List; 039 040import com.orsoncharts.axis.CategoryAxis3D; 041import com.orsoncharts.Chart3DFactory; 042import com.orsoncharts.Range; 043import com.orsoncharts.axis.ValueAxis3D; 044import com.orsoncharts.data.category.CategoryDataset3D; 045import com.orsoncharts.data.DataUtils; 046import com.orsoncharts.data.Values3D; 047import com.orsoncharts.data.KeyedValues3DItemKey; 048import com.orsoncharts.graphics3d.Dimension3D; 049import com.orsoncharts.graphics3d.Object3D; 050import com.orsoncharts.graphics3d.Offset3D; 051import com.orsoncharts.graphics3d.Utils2D; 052import com.orsoncharts.graphics3d.World; 053import com.orsoncharts.label.ItemLabelPositioning; 054import com.orsoncharts.plot.CategoryPlot3D; 055import com.orsoncharts.renderer.Renderer3DChangeEvent; 056import com.orsoncharts.util.ObjectUtils; 057 058/** 059 * A renderer for creating 3D area charts from data in a 060 * {@link CategoryDataset3D} (for use with a {@link CategoryPlot3D}). For 061 * example: 062 * <div> 063 * <object id="ABC" data="../../../../doc-files/AreaChart3DDemo1.svg" 064 * type="image/svg+xml" width="500" height="359"> 065 * </object> 066 * </div> 067 * (refer to {@code AreaChart3DDemo1.java} for the code to generate the 068 * above chart). 069 * <br><br> 070 * There is a factory method to create a chart using this renderer - see 071 * {@link Chart3DFactory#createAreaChart(String, String, CategoryDataset3D, 072 * String, String, String)}. 073 * <br><br> 074 * NOTE: This class is serializable, but the serialization format is subject 075 * to change in future releases and should not be relied upon for persisting 076 * instances of this class. 077 */ 078@SuppressWarnings("serial") 079public class AreaRenderer3D extends AbstractCategoryRenderer3D 080 implements Serializable { 081 082 /** The base for the areas (defaults to 0.0). */ 083 private double base; 084 085 /** 086 * The color used to paint the underside of the area object (if 087 * {@code null}, the regular series color is used). 088 */ 089 private Color baseColor; 090 091 /** The depth of the area. */ 092 private double depth; 093 094 /** 095 * For isolated data values this attribute controls the width (x-axis) of 096 * the box representing the data item, it is expressed as a percentage of 097 * the category width. 098 */ 099 private double isolatedItemWidthPercent; 100 101 /** 102 * The color source that determines the color used to highlight clipped 103 * items in the chart. 104 */ 105 private CategoryColorSource clipColorSource; 106 107 /** 108 * A flag that controls whether or not outlines are drawn for the faces 109 * making up the area segments. 110 */ 111 private boolean drawFaceOutlines; 112 113 /** 114 * Default constructor. 115 */ 116 public AreaRenderer3D() { 117 this.base = 0.0; 118 this.baseColor = null; 119 this.depth = 0.6; 120 this.isolatedItemWidthPercent = 0.25; 121 this.clipColorSource = new StandardCategoryColorSource(Color.RED); 122 this.drawFaceOutlines = true; 123 } 124 125 /** 126 * Returns the y-value for the base of the area. The default value is 127 * {@code 0.0}. 128 * 129 * @return The base value. 130 */ 131 public double getBase() { 132 return this.base; 133 } 134 135 /** 136 * Sets the base value and sends a change event to all registered listeners. 137 * 138 * @param base the base value. 139 */ 140 public void setBase(double base) { 141 this.base = base; 142 fireChangeEvent(true); 143 } 144 145 /** 146 * Returns the color used to paint the underside of the area polygons. 147 * The default value is {@code null} (which means the undersides are 148 * painted using the regular series color). 149 * 150 * @return The color (possibly {@code null}). 151 * 152 * @see #setBaseColor(java.awt.Color) 153 */ 154 public Color getBaseColor() { 155 return this.baseColor; 156 } 157 158 /** 159 * Sets the color for the underside of the area shapes and sends a 160 * change event to all registered listeners. If you set 161 * this to {@code null} the base will be painted with the regular 162 * series color. 163 * 164 * @param color the color ({@code null} permitted). 165 */ 166 public void setBaseColor(Color color) { 167 this.baseColor = color; 168 fireChangeEvent(true); 169 } 170 171 /** 172 * Returns the depth (in 3D) for the area (in world units). The 173 * default value is {@code 0.6}. 174 * 175 * @return The depth. 176 */ 177 public double getDepth() { 178 return this.depth; 179 } 180 181 /** 182 * Sets the depth (in 3D) and sends a change event to all registered 183 * listeners. 184 * 185 * @param depth the depth. 186 */ 187 public void setDepth(double depth) { 188 this.depth = depth; 189 fireChangeEvent(true); 190 } 191 192 /** 193 * Returns the color source used to determine the color used to highlight 194 * clipping in the chart elements. If the source is {@code null}, 195 * then the regular series color is used instead. 196 * 197 * @return The color source (possibly {@code null}). 198 * 199 * @since 1.3 200 */ 201 public CategoryColorSource getClipColorSource() { 202 return this.clipColorSource; 203 } 204 205 /** 206 * Sets the color source that determines the color used to highlight 207 * clipping in the chart elements, and sends a {@link Renderer3DChangeEvent} 208 * to all registered listeners. 209 * 210 * @param source the source ({@code null} permitted). 211 * 212 * @since 1.3 213 */ 214 public void setClipColorSource(CategoryColorSource source) { 215 this.clipColorSource = source; 216 fireChangeEvent(true); 217 } 218 219 /** 220 * Returns the flag that controls whether or not the faces making up area 221 * segments will be drawn with outlines. The default value is 222 * {@code true}. When anti-aliasing is on, the fill area for the 223 * faces will have some gray shades around the edges, and these will show 224 * up on the chart as thin lines (usually not visible if you turn off 225 * anti-aliasing). To mask this, the rendering engine can draw an outline 226 * around each face in the same color (this usually results in cleaner 227 * output, but it is slower and can introduce some minor visual artifacts 228 * as well depending on the output target). 229 * 230 * @return A boolean. 231 * 232 * @since 1.3 233 */ 234 public boolean getDrawFaceOutlines() { 235 return this.drawFaceOutlines; 236 } 237 238 /** 239 * Sets the flag that controls whether or not outlines are drawn for the 240 * faces making up the area segments and sends a change event to all 241 * registered listeners. 242 * 243 * @param outline the new flag value. 244 * 245 * @since 1.3 246 */ 247 public void setDrawFaceOutlines(boolean outline) { 248 this.drawFaceOutlines = outline; 249 fireChangeEvent(true); 250 } 251 252 /** 253 * Returns the range (for the value axis) that is required for this 254 * renderer to show all the values in the specified data set. This method 255 * is overridden to ensure that the range includes the base value (normally 256 * 0.0) set for the renderer. 257 * 258 * @param data the data ({@code null} not permitted). 259 * 260 * @return The range. 261 */ 262 @Override 263 public Range findValueRange(Values3D<? extends Number> data) { 264 return DataUtils.findValueRange(data, this.base); 265 } 266 267 /** 268 * Constructs and places one item from the specified dataset into the given 269 * world. This method will be called by the {@link CategoryPlot3D} class 270 * while iterating over the items in the dataset. 271 * 272 * @param dataset the dataset ({@code null} not permitted). 273 * @param series the series index. 274 * @param row the row index. 275 * @param column the column index. 276 * @param world the world ({@code null} not permitted). 277 * @param dimensions the plot dimensions ({@code null} not permitted). 278 * @param xOffset the x-offset. 279 * @param yOffset the y-offset. 280 * @param zOffset the z-offset. 281 */ 282 @Override 283 public void composeItem(CategoryDataset3D dataset, int series, int row, 284 int column, World world, Dimension3D dimensions, 285 double xOffset, double yOffset, double zOffset) { 286 287 Number y = dataset.getValue(series, row, column); 288 Number yprev = null; 289 if (column > 0) { 290 yprev = dataset.getValue(series, row, column - 1); 291 } 292 Number ynext = null; 293 if (column < dataset.getColumnCount() - 1) { 294 ynext = dataset.getValue(series, row, column + 1); 295 } 296 297 CategoryPlot3D plot = getPlot(); 298 CategoryAxis3D rowAxis = plot.getRowAxis(); 299 CategoryAxis3D columnAxis = plot.getColumnAxis(); 300 ValueAxis3D valueAxis = plot.getValueAxis(); 301 Range r = valueAxis.getRange(); 302 303 Comparable<?> seriesKey = dataset.getSeriesKey(series); 304 Comparable<?> rowKey = dataset.getRowKey(row); 305 Comparable<?> columnKey = dataset.getColumnKey(column); 306 double rowValue = rowAxis.getCategoryValue(rowKey); 307 double columnValue = columnAxis.getCategoryValue(columnKey); 308 double ww = dimensions.getWidth(); 309 double hh = dimensions.getHeight(); 310 double dd = dimensions.getDepth(); 311 312 // for any data value, we'll try to create two area segments, one to 313 // the left of the value and one to the right of the value (each 314 // halfway to the adjacent data value). If the adjacent data values 315 // are null (or don't exist, as in the case of the first and last data 316 // items), then we can create an isolated segment to represent the data 317 // item. The final consideration is whether the opening and closing 318 // faces of each segment are filled or not (if the segment connects to 319 // another segment, there is no need to fill the end face) 320 boolean createLeftSegment, createRightSegment, createIsolatedSegment; 321 boolean leftOpen = false; 322 boolean leftClose = false; 323 boolean rightOpen = false; 324 boolean rightClose = false; 325 326 // for the first column there is no left segment, we also handle the 327 // special case where there is just one column of data in which case 328 // the renderer can only show an isolated data value 329 if (column == 0) { 330 createLeftSegment = false; // never for first item 331 if (dataset.getColumnCount() == 1) { 332 createRightSegment = false; 333 createIsolatedSegment = (y != null); 334 } else { 335 createRightSegment = (y != null && ynext != null); 336 rightOpen = true; 337 rightClose = false; 338 createIsolatedSegment = (y != null && ynext == null); 339 } 340 } 341 342 // for the last column there is no right segment 343 else if (column == dataset.getColumnCount() - 1) { // last column 344 createRightSegment = false; // never for the last item 345 createLeftSegment = (y != null && yprev != null); 346 leftOpen = false; 347 leftClose = true; 348 createIsolatedSegment = (y != null && yprev == null); 349 } 350 351 // for the general case we handle left and right segments or an 352 // isolated segment if the surrounding data values are null 353 else { 354 createLeftSegment = (y != null && yprev != null); 355 leftOpen = false; 356 leftClose = (createLeftSegment && ynext == null); 357 createRightSegment = (y != null && ynext != null); 358 rightOpen = (createRightSegment && yprev == null); 359 rightClose = false; 360 createIsolatedSegment = (y != null 361 && yprev == null && ynext == null); 362 } 363 364 // now that we know what we have to create, we'll need some info 365 // for the construction...world coordinates are required 366 double xw = columnAxis.translateToWorld(columnValue, ww) + xOffset; 367 double yw = Double.NaN; 368 if (y != null) { 369 yw = valueAxis.translateToWorld(y.doubleValue(), hh) + yOffset; 370 } 371 double zw = rowAxis.translateToWorld(rowValue, dd) + zOffset; 372 double ywmin = valueAxis.translateToWorld(r.getMin(), hh) + yOffset; 373 double ywmax = valueAxis.translateToWorld(r.getMax(), hh) + yOffset; 374 double basew = valueAxis.translateToWorld(this.base, hh) + yOffset; 375 Color color = getColorSource().getColor(series, row, column); 376 Color clipColor = color; 377 if (getClipColorSource() != null) { 378 Color c = getClipColorSource().getColor(series, row, column); 379 if (c != null) { 380 clipColor = c; 381 } 382 } 383 KeyedValues3DItemKey itemKey = new KeyedValues3DItemKey(seriesKey, 384 rowKey, columnKey); 385 386 if (createLeftSegment) { 387 Comparable<?> prevColumnKey = dataset.getColumnKey(column - 1); 388 double prevColumnValue = columnAxis.getCategoryValue(prevColumnKey); 389 double prevColumnX = columnAxis.translateToWorld(prevColumnValue, 390 ww) + xOffset; 391 double xl = (prevColumnX + xw) / 2.0; 392 assert yprev != null; // we know this because createLeftSegment is 393 // not 'true' otherwise 394 double yprevw = valueAxis.translateToWorld(yprev.doubleValue(), hh) 395 + yOffset; 396 double yl = (yprevw + yw) / 2.0; 397 List<Object3D> leftObjs = createSegment(xl, yl, xw, yw, zw, 398 basew, ywmin, ywmax, color, this.baseColor, clipColor, 399 leftOpen, leftClose); 400 for (Object3D obj : leftObjs) { 401 obj.setProperty(Object3D.ITEM_KEY, itemKey); 402 obj.setOutline(this.drawFaceOutlines); 403 world.add(obj); 404 } 405 } 406 407 if (createRightSegment) { 408 Comparable<?> nextColumnKey = dataset.getColumnKey(column + 1); 409 double nextColumnValue = columnAxis.getCategoryValue(nextColumnKey); 410 double nextColumnX = columnAxis.translateToWorld(nextColumnValue, 411 ww) + xOffset; 412 double xr = (nextColumnX + xw) / 2.0; 413 assert ynext != null; // we know this because createRightSegment is 414 // not 'true' otherwise 415 double ynextw = valueAxis.translateToWorld(ynext.doubleValue(), hh) 416 + yOffset; 417 double yr = (ynextw + yw) / 2.0; 418 List<Object3D> rightObjs = createSegment(xw, yw, xr, yr, zw, 419 basew, ywmin, ywmax, color, this.baseColor, clipColor, 420 rightOpen, rightClose); 421 for (Object3D obj : rightObjs) { 422 obj.setProperty(Object3D.ITEM_KEY, itemKey); 423 obj.setOutline(this.drawFaceOutlines); 424 world.add(obj); 425 } 426 } 427 428 if (createIsolatedSegment) { 429 double cw = columnAxis.getCategoryWidth() 430 * this.isolatedItemWidthPercent; 431 double cww = columnAxis.translateToWorld(cw, ww); 432 double h = yw - basew; 433 Object3D isolated = Object3D.createBox(xw, cww, yw - h / 2, h, 434 zw, this.depth, color); 435 isolated.setOutline(this.drawFaceOutlines); 436 isolated.setProperty(Object3D.ITEM_KEY, itemKey); 437 world.add(isolated); 438 } 439 440 if (getItemLabelGenerator() != null && !Double.isNaN(yw) 441 && yw >= ywmin && yw <= ywmax) { 442 String label = getItemLabelGenerator().generateItemLabel(dataset, 443 seriesKey, rowKey, columnKey); 444 ItemLabelPositioning positioning = getItemLabelPositioning(); 445 Offset3D offsets = getItemLabelOffsets(); 446 double ydelta = dimensions.getHeight() * offsets.getDY(); 447 if (yw < basew) { 448 ydelta = -ydelta; 449 } 450 if (positioning.equals(ItemLabelPositioning.CENTRAL)) { 451 Object3D labelObj = Object3D.createLabelObject(label, 452 getItemLabelFont(), getItemLabelColor(), 453 getItemLabelBackgroundColor(), xw, yw + ydelta, zw, 454 false, true); 455 456 labelObj.setProperty(Object3D.ITEM_KEY, itemKey); 457 world.add(labelObj); 458 } else if (positioning.equals( 459 ItemLabelPositioning.FRONT_AND_BACK)) { 460 double zdelta = this.depth / 2 * offsets.getDZ(); 461 Object3D labelObj1 = Object3D.createLabelObject(label, 462 getItemLabelFont(), getItemLabelColor(), 463 getItemLabelBackgroundColor(), xw, yw + ydelta, 464 zw - zdelta, false, false); 465 labelObj1.setProperty(Object3D.CLASS_KEY, "ItemLabel"); 466 labelObj1.setProperty(Object3D.ITEM_KEY, itemKey); 467 world.add(labelObj1); 468 Object3D labelObj2 = Object3D.createLabelObject(label, 469 getItemLabelFont(), getItemLabelColor(), 470 getItemLabelBackgroundColor(), xw, yw + ydelta, 471 zw + zdelta, true, false); 472 labelObj2.setProperty(Object3D.CLASS_KEY, "ItemLabel"); 473 labelObj2.setProperty(Object3D.ITEM_KEY, itemKey); 474 world.add(labelObj2); 475 } 476 } 477 } 478 479 /** 480 * Creates objects to represent the area segment between (x0, y0) and 481 * (x1, y1). 482 * 483 * @param x0 484 * @param y0 485 * @param x1 486 * @param y1 487 * @param z 488 * @param lineWidth 489 * @param lineHeight 490 * @param ymin 491 * @param ymax 492 * @param color 493 * @param clipColor 494 * @param openingFace 495 * @param closingFace 496 * 497 * @return A list of objects making up the segment. 498 */ 499 private List<Object3D> createSegment(double x0, double y0, double x1, 500 double y1, double z, double base, double ymin, double ymax, 501 Color color, Color baseColor, Color clipColor, boolean openingFace, 502 boolean closingFace) { 503 504 List<Object3D> result = new ArrayList<Object3D>(2); 505 // either there is a crossing or there is not 506 if (!isBaselineCrossed(y0, y1, base)) { 507 Object3D segment = createSegmentWithoutCrossing(x0, y0, x1, y1, 508 z, base, ymin, ymax, color, baseColor, clipColor, 509 openingFace, closingFace); 510 result.add(segment); 511 } else { 512 result.addAll(createSegmentWithCrossing(x0, y0, x1, y1, 513 z, base, ymin, ymax, color, baseColor, clipColor, 514 openingFace, closingFace)); 515 } 516 return result; 517 } 518 519 /** 520 * Returns {@code true} if the two values are on opposite sides of 521 * the baseline. If the data values cross the baseline, then we need 522 * to construct two 3D objects to represent the data, whereas if there is 523 * no crossing, a single 3D object will be sufficient. 524 * 525 * @param y0 the first value. 526 * @param y1 the second value. 527 * @param baseline the baseline. 528 * 529 * @return A boolean. 530 */ 531 private boolean isBaselineCrossed(double y0, double y1, double baseline) { 532 return (y0 > baseline && y1 < baseline) 533 || (y0 < baseline && y1 > baseline); 534 } 535 536 private Object3D createSegmentWithoutCrossing(double x0, double y0, 537 double x1, double y1, double z, double base, double ymin, 538 double ymax, Color color, Color baseColor, Color clipColor, 539 boolean openingFace, boolean closingFace) { 540 541 boolean positive = y0 > base || y1 > base; 542 if (positive) { 543 Object3D pos = createPositiveArea(x0, y0, x1, y1, base, 544 z, new Range(ymin, ymax), color, openingFace, 545 closingFace); 546 return pos; 547 } else { 548 Object3D neg = createNegativeArea(x0, y0, x1, y1, base, z, 549 new Range(ymin, ymax), color, openingFace, closingFace); 550 return neg; 551 } 552 } 553 554 private List<Object3D> createSegmentWithCrossing(double x0, double y0, 555 double x1, double y1, double z, double base, double ymin, 556 double ymax, Color color, Color baseColor, Color clipColor, 557 boolean openingFace, boolean closingFace) { 558 List<Object3D> result = new ArrayList<Object3D>(2); 559 Range range = new Range(ymin, ymax); 560 // find the crossing point 561 double ydelta = Math.abs(y1 - y0); 562 double factor = 0; 563 if (ydelta != 0.0) { 564 factor = Math.abs(y0 - base) / ydelta; 565 } 566 double xcross = x0 + factor * (x1 - x0); 567 if (y0 > base) { 568 Object3D pos = createPositiveArea(x0, y0, xcross, base, base, z, 569 range, color, openingFace, closingFace); 570 if (pos != null) { 571 result.add(pos); 572 } 573 Object3D neg = createNegativeArea(xcross, base, x1, y1, 574 base, z, range, color, openingFace, closingFace); 575 if (neg != null) { 576 result.add(neg); 577 } 578 } else { 579 Object3D neg = createNegativeArea(x0, y0, xcross, base, 580 base, z, range, color, openingFace, closingFace); 581 if (neg != null) { 582 result.add(neg); 583 } 584 Object3D pos = createPositiveArea(xcross, base, x1, y1, base, 585 z, range, color, openingFace, closingFace); 586 if (pos != null) { 587 result.add(pos); 588 } 589 } 590 return result; 591 } 592 593 /** 594 * A utility method that returns the fraction (x - x0) / (x1 - x0), which 595 * is used for some interpolation calculations. 596 * 597 * @param x the x-value. 598 * @param x0 the start of a range. 599 * @param x1 the end of a range. 600 * 601 * @return The fractional value of x along the range x0 to x1. 602 */ 603 private double fraction(double x, double x0, double x1) { 604 double dist = x - x0; 605 double length = x1 - x0; 606 return dist / length; 607 } 608 609 /** 610 * A value in world units that is considered small enough to be not 611 * significant. We use this to check if two coordinates are "more or less" 612 * the same. 613 */ 614 private static final double EPSILON = 0.001; 615 616 /** 617 * Creates a 3D object to represent a positive "area", taking into account 618 * that the visible range can be restricted. 619 * 620 * @param color the color ({@code null} not permitted). 621 * @param wx0 622 * @param wy0 623 * @param wx1 624 * @param wy1 625 * @param wbase 626 * @param wz 627 * @param range 628 * @param openingFace 629 * @param closingFace 630 * 631 * @return A 3D object or {@code null}. 632 */ 633 private Object3D createPositiveArea(double wx0, double wy0, 634 double wx1, double wy1, double wbase, double wz, Range range, 635 Color color, boolean openingFace, boolean closingFace) { 636 637 if (!range.intersects(wy0, wbase) && !range.intersects(wy1, wbase)) { 638 return null; 639 } 640 double wy00 = range.peggedValue(wy0); 641 double wy11 = range.peggedValue(wy1); 642 double wbb = range.peggedValue(wbase); 643 644 double wx00 = wx0; 645 if (wy0 < range.getMin()) { 646 wx00 = wx0 + (wx1 - wx0) * fraction(wy00, wy0, wy1); 647 } 648 double wx11 = wx1; 649 if (wy1 < range.getMin()) { 650 wx11 = wx1 - (wx1 - wx0) * fraction(wy11, wy1, wy0); 651 } 652 double wx22 = Double.NaN; // bogus 653 boolean p2required = Utils2D.spans(range.getMax(), wy0, wy1); 654 if (p2required) { 655 wx22 = wx0 + (wx1 - wx0) * fraction(range.getMax(), wy0, wy1); 656 } 657 658 double delta = this.depth / 2.0; 659 660 // create an area shape 661 Object3D obj = new Object3D(color, true); 662 obj.addVertex(wx00, wbb, wz - delta); 663 obj.addVertex(wx00, wbb, wz + delta); 664 boolean leftSide = false; 665 if (Math.abs(wy00 - wbb) > EPSILON) { 666 leftSide = true; 667 obj.addVertex(wx00, wy00, wz - delta); 668 obj.addVertex(wx00, wy00, wz + delta); 669 } 670 if (p2required) { 671 obj.addVertex(wx22, range.getMax(), wz - delta); 672 obj.addVertex(wx22, range.getMax(), wz + delta); 673 } 674 obj.addVertex(wx11, wy11, wz - delta); 675 obj.addVertex(wx11, wy11, wz + delta); 676 boolean rightSide = false; 677 if (Math.abs(wy11 - wbb) > EPSILON) { 678 rightSide = true; 679 obj.addVertex(wx11, wbb, wz - delta); 680 obj.addVertex(wx11, wbb, wz + delta); 681 } 682 int vertices = obj.getVertexCount(); 683 684 if (vertices == 10) { 685 obj.addFace(new int[] {0, 2, 4, 6, 8}); // front 686 obj.addFace(new int[] {1, 9, 7, 5, 3}); // rear 687 obj.addFace(new int[] {0, 8, 9, 1}); // base 688 obj.addFace(new int[] {2, 3, 5, 4}); // top 1 689 obj.addFace(new int[] {4, 5, 7, 6}); // top 2 690 if (openingFace) { 691 obj.addFace(new int[] {0, 1, 3, 2}); 692 } 693 if (closingFace) { 694 obj.addFace(new int[] {6, 7, 9, 8}); 695 } 696 } else if (vertices == 8) { 697 obj.addFace(new int[] {0, 2, 4, 6}); // front 698 obj.addFace(new int[] {7, 5, 3, 1}); // rear 699 if (!leftSide) { 700 obj.addFace(new int[] {0, 1, 3, 2}); // top left 701 } 702 obj.addFace(new int[] {2, 3, 5, 4}); // top 1 703 if (!rightSide) { 704 obj.addFace(new int[] {4, 5, 7, 6}); // top 2 705 } 706 obj.addFace(new int[] {1, 0, 6, 7}); // base 707 if (openingFace) { 708 obj.addFace(new int[] {0, 1, 3, 2}); 709 } 710 if (closingFace) { 711 obj.addFace(new int[] {4, 5, 7, 6}); 712 } 713 } else if (vertices == 6) { 714 obj.addFace(new int[] {0, 2, 4}); // front 715 obj.addFace(new int[] {5, 3, 1}); // rear 716 if (leftSide) { 717 obj.addFace(new int[] {3, 5, 4, 2}); // top 718 if (openingFace) { 719 obj.addFace(new int[] {0, 1, 3, 2}); 720 } 721 } else { 722 obj.addFace(new int[] {0, 1, 3, 2}); // top 723 if (closingFace) { 724 obj.addFace(new int[] {2, 3, 5, 4}); 725 } 726 } 727 obj.addFace(new int[] {0, 4, 5, 1}); // base 728 } else { 729 obj.addFace(new int[] {0, 1, 3, 2}); 730 obj.addFace(new int[] {2, 3, 1, 0}); 731 } 732 return obj; 733 } 734 735 /** 736 * Creates a negative area shape from (wx0, wy0) to (wx1, wy1) with the 737 * base at wbase (it is assumed that both wy0 and wy1 are less than wbase). 738 * 739 * @param wx0 740 * @param wy0 741 * @param wx1 742 * @param wy1 743 * @param wbase 744 * @param wz 745 * @param range 746 * @param color 747 * @param openingFace 748 * @param closingFace 749 * 750 * @return An object representing the area shape (or {@code null}). 751 */ 752 private Object3D createNegativeArea(double wx0, double wy0, 753 double wx1, double wy1, double wbase, double wz, Range range, 754 Color color, boolean openingFace, boolean closingFace) { 755 756 if (!range.intersects(wy0, wbase) && !range.intersects(wy1, wbase)) { 757 return null; 758 } 759 double wy00 = range.peggedValue(wy0); 760 double wy11 = range.peggedValue(wy1); 761 double wbb = range.peggedValue(wbase); 762 763 double wx00 = wx0; 764 if (wy0 > range.getMax()) { 765 wx00 = wx0 + (wx1 - wx0) * fraction(wy00, wy0, wy1); 766 } 767 double wx11 = wx1; 768 if (wy1 > range.getMax()) { 769 wx11 = wx1 - (wx1 - wx0) * fraction(wy11, wy1, wy0); 770 } 771 double wx22 = (wx00 + wx11) / 2.0; // bogus 772 boolean p2required = Utils2D.spans(range.getMin(), wy0, wy1); 773 if (p2required) { 774 wx22 = wx0 + (wx1 - wx0) * fraction(range.getMin(), wy0, wy1); 775 } 776 777 double delta = this.depth / 2.0; 778 779 // create an area shape 780 Object3D obj = new Object3D(color, true); 781 obj.addVertex(wx00, wbb, wz - delta); 782 obj.addVertex(wx00, wbb, wz + delta); 783 boolean leftSide = false; 784 if (Math.abs(wy00 - wbb) > EPSILON) { 785 leftSide = true; 786 obj.addVertex(wx00, wy00, wz - delta); 787 obj.addVertex(wx00, wy00, wz + delta); 788 } 789 if (p2required) { 790 obj.addVertex(wx22, range.getMin(), wz - delta); 791 obj.addVertex(wx22, range.getMin(), wz + delta); 792 } 793 obj.addVertex(wx11, wy11, wz - delta); 794 obj.addVertex(wx11, wy11, wz + delta); 795 boolean rightSide = false; 796 if (Math.abs(wy11 - wbb) > EPSILON) { 797 obj.addVertex(wx11, wbb, wz - delta); 798 obj.addVertex(wx11, wbb, wz + delta); 799 } 800 int vertices = obj.getVertexCount(); 801 if (vertices == 10) { 802 obj.addFace(new int[] {8, 6, 4, 2, 0}); // front 803 obj.addFace(new int[] {1, 3, 5, 7, 9}); // rear 804 obj.addFace(new int[] {1, 9, 8, 0}); // base 805 obj.addFace(new int[] {4, 5, 3, 2}); // top 1 806 obj.addFace(new int[] {6, 7, 5, 4}); // top 2 807 if (openingFace) { 808 obj.addFace(new int[] {2, 3, 1, 0}); 809 } 810 if (closingFace) { 811 obj.addFace(new int[] {8, 9, 7, 6}); 812 } 813 } else if (vertices == 8) { 814 obj.addFace(new int[] {2, 0, 6, 4}); // front 815 obj.addFace(new int[] {1, 3, 5, 7}); // rear 816 obj.addFace(new int[] {0, 1, 7, 6}); // base 817 if (!leftSide) { 818 obj.addFace(new int[] {2, 3, 1, 0}); 819 } 820 obj.addFace(new int[] {3, 2, 4, 5}); // negative top 821 if (!rightSide) { 822 obj.addFace(new int[] {6, 7, 5, 4}); 823 } 824 if (openingFace) { 825 obj.addFace(new int[] {1, 0, 2, 3}); 826 } 827 if (closingFace) { 828 obj.addFace(new int[] {5, 4, 6, 7}); 829 } 830 } else if (vertices == 6) { 831 obj.addFace(new int[] {4, 2, 0}); // front 832 obj.addFace(new int[] {1, 3, 5}); // rear 833 if (leftSide) { 834 obj.addFace(new int[] {4, 5, 3, 2}); // negative top 835 if (openingFace) { 836 obj.addFace(new int[] {1, 0, 2, 3}); 837 } 838 } else { 839 obj.addFace(new int[] {2, 3, 1, 0}); // negative top 840 if (closingFace) { 841 obj.addFace(new int[] {3, 2, 4, 5}); 842 } 843 } 844 obj.addFace(new int[] {0, 1, 5, 4}); // base 845 } else { 846 obj.addFace(new int[] {0, 1, 3, 2}); 847 obj.addFace(new int[] {2, 3, 1, 0}); 848 } 849 return obj; 850 } 851 852 /** 853 * Tests this renderer for equality with an arbitrary object. 854 * 855 * @param obj the object ({@code null} permitted). 856 * 857 * @return A boolean. 858 */ 859 @Override 860 public boolean equals(Object obj) { 861 if (obj == this) { 862 return true; 863 } 864 if (!(obj instanceof AreaRenderer3D)) { 865 return false; 866 } 867 AreaRenderer3D that = (AreaRenderer3D) obj; 868 if (this.base != that.base) { 869 return false; 870 } 871 if (!ObjectUtils.equals(this.baseColor, that.baseColor)) { 872 return false; 873 } 874 if (this.depth != that.depth) { 875 return false; 876 } 877 return super.equals(obj); 878 } 879}