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