    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.xyz;
    034
    035import java.awt.Color;
    036import java.io.Serializable;
    037import java.util.ArrayList;
    038import java.util.List;
    039
    040import org.jfree.chart3d.axis.ValueAxis3D;
    041import org.jfree.chart3d.data.Range;
    042import org.jfree.chart3d.data.function.Function3D;
    043import org.jfree.chart3d.data.function.Function3DUtils;
    044import org.jfree.chart3d.data.xyz.XYZDataset;
    045import org.jfree.chart3d.graphics3d.Dimension3D;
    046import org.jfree.chart3d.graphics3d.Object3D;
    047import org.jfree.chart3d.graphics3d.Point3D;
    048import org.jfree.chart3d.graphics3d.World;
    049import org.jfree.chart3d.internal.Args;
    050import org.jfree.chart3d.plot.XYZPlot;
    051import org.jfree.chart3d.renderer.ColorScale;
    052import org.jfree.chart3d.renderer.ColorScaleRenderer;
    053import org.jfree.chart3d.renderer.ComposeType;
    054import org.jfree.chart3d.renderer.FixedColorScale;
    055import org.jfree.chart3d.renderer.Renderer3DChangeEvent;
    056
    057/**
    058 * A renderer that plots a surface based on a function (any implementation
    059 * of {@link Function3D}).  This renderer is different to others in that it
    060 * does not plot data from a dataset, instead it samples a function and plots
    061 * those values.  By default 900 samples are taken (30 x-values by 30 z-values)
    062 * although this can be modified.
    063 * <br><br>
    064 * For the fastest rendering, the {@code drawFaceOutlines} flag can be set
    065 * to {@code false} (the default is {@code true}) but this may
    066 * cause slight rendering artifacts if anti-aliasing is on (note that switching
    067 * off anti-aliasing as well also improves rendering performance).
    068 * <br><br>
    069 * NOTE: This class is serializable, but the serialization format is subject
    070 * to change in future releases and should not be relied upon for persisting
    071 * instances of this class.
    072 *
    073 * @since 1.1
    074 */
    075@SuppressWarnings("serial")
    076public class SurfaceRenderer extends AbstractXYZRenderer implements XYZRenderer,
    077        ColorScaleRenderer, Serializable {
    078
    079    /** The function. */
    080    private Function3D function;
    081
    082    /** The number of samples along the x-axis (minimum 2). */
    083    private int xSamples;
    084
    085    /** The number of samples along the z-axis (minimum 2). */
    086    private int zSamples;
    087
    088    /** The color scale. */
    089    private ColorScale colorScale;
    090
    091    /**
    092     * A flag that controls whether the faces that make up the surface have
    093     * their outlines drawn (in addition to the shape being filled).  The
    094     * default value is {@code true} which renders a solid surface but
    095     * is slower.
    096     */
    097    private boolean drawFaceOutlines;
    098
    099    /**
    100     * Creates a new renderer for the specified function.  By default, the
    101     * renderer will take 30 samples along the x-axis and 30 samples along the
    102     * z-axis (this is configurable).
    103     *
    104     * @param function  the function ({@code null} not permitted).
    105     */
    106    public SurfaceRenderer(Function3D function) {
    107        Args.nullNotPermitted(function, "function");
    108        this.function = function;
    109        this.xSamples = 30;
    110        this.zSamples = 30;
    111        this.colorScale = new FixedColorScale(Color.YELLOW);
    112        this.drawFaceOutlines = true;
    113    }
    114
    115    /**
    116     * Returns the number of samples the renderer will take along the
    117     * x-axis when plotting the function.  The default value is 30.
    118     *
    119     * @return The number of samples.
    120     */
    121    public int getXSamples() {
    122        return this.xSamples;
    123    }
    124
    125    /**
    126     * Sets the number of samples the renderer will take along the x-axis when
    127     * plotting the function and sends a {@link Renderer3DChangeEvent} to all
    128     * registered listeners.  The default value is 30, setting this to higher
    129     * values will result in smoother looking plots, but they will take
    130     * longer to draw.
    131     *
    132     * @param count  the count.
    133     *
    134     * @see #setZSamples(int)
    135     */
    136    public void setXSamples(int count) {
    137        this.xSamples = count;
    138        fireChangeEvent(true);
    139    }
    140
    141    /**
    142     * Returns the number of samples the renderer will take along the z-axis
    143     * when plotting the function and sends a {@link Renderer3DChangeEvent} to
    144     * all registered listeners.  The default value is 30.
    145     *
    146     * @return The number of samples.
    147     */
    148    public int getZSamples() {
    149        return this.zSamples;
    150    }
    151
    152    /**
    153     * Sets the number of samples the renderer will take along the z-axis when
    154     * plotting the function and sends a {@link Renderer3DChangeEvent} to all
    155     * registered listeners.  The default value is 30, setting this to higher
    156     * values will result in smoother looking plots, but they will take
    157     * longer to draw.
    158     *
    159     * @param count  the count.
    160     *
    161     * @see #setXSamples(int)
    162     */
    163    public void setZSamples(int count) {
    164        this.zSamples = count;
    165    }
    166
    167    /**
    168     * Returns the compose-type for the renderer.  Here the value is
    169     * {@code ComposeType.ALL} which means the plot will call the
    170     * {@link #composeAll(org.jfree.chart3d.plot.XYZPlot,
    171     * org.jfree.chart3d.graphics3d.World, org.jfree.chart3d.graphics3d.Dimension3D,
    172     * double, double, double)} method for composing the chart.
    173     *
    174     * @return The compose type (never {@code null}).
    175     */
    176    @Override
    177    public ComposeType getComposeType() {
    178        return ComposeType.ALL;
    179    }
    180
    181    /**
    182     * Returns the color scale.  This determines the color of the surface
    183     * according to the y-value.
    184     *
    185     * @return The color scale (never {@code null}).
    186     */
    187    @Override
    188    public ColorScale getColorScale() {
    189        return this.colorScale;
    190    }
    191
    192    /**
    193     * Sets the color scale and sends a {@link Renderer3DChangeEvent} to all
    194     * registered listeners.
    195     *
    196     * @param colorScale  the color scale ({@code null} not permitted).
    197     */
    198    public void setColorScale(ColorScale colorScale) {
    199        Args.nullNotPermitted(colorScale, "colorScale");
    200        this.colorScale = colorScale;
    201        fireChangeEvent(true);
    202    }
    203
    204    /**
    205     * Returns the flag that controls whether or not the faces that make
    206     * up the surface have their outlines drawn during rendering.  The
    207     * default value is {@code true}.
    208     *
    209     * @return A boolean.
    210     */
    211    public boolean getDrawFaceOutlines() {
    212        return this.drawFaceOutlines;
    213    }
    214
    215    /**
    216     * Sets a flag that controls whether or not the faces that make up the
    217     * surface are drawn (as well as filled) and sends a
    218     * {@link Renderer3DChangeEvent} to all registered listeners.  If the face
    219     * outlines are drawn (the default), the surface is solid (but takes longer
    220     * to draw).  If the face outlines are not drawn, Java2D can leave small
    221     * gaps that you can "see" through (if you don't mind this, then the
    222     * performance is better).
    223     *
    224     * @param draw  the new flag value.
    225     */
    226    public void setDrawFaceOutlines(boolean draw) {
    227        this.drawFaceOutlines = draw;
    228        fireChangeEvent(true);
    229    }
    230
    231    /**
    232     * Composes the entire representation of the function in the supplied
    233     * {@code world}.
    234     *
    235     * @param plot  the plot.
    236     * @param world  the world.
    237     * @param dimensions  the plot dimensions.
    238     * @param xOffset  the x-offset.
    239     * @param yOffset  the y-offset.
    240     * @param zOffset  the z-offset.
    241     */
    242    @Override
    243    public void composeAll(XYZPlot plot, World world, Dimension3D dimensions,
    244            double xOffset, double yOffset, double zOffset) {
    245
    246        // need to know the x-axis range and the z-axis range
    247        ValueAxis3D xAxis = plot.getXAxis();
    248        ValueAxis3D yAxis = plot.getYAxis();
    249        ValueAxis3D zAxis = plot.getZAxis();
    250        Dimension3D dim = plot.getDimensions();
    251        double xlen = dim.getWidth();
    252        double ylen = dim.getHeight();
    253        double zlen = dim.getDepth();
    254        Range yRange = new Range(yOffset, -yOffset);
    255        for (int xIndex = 0; xIndex < this.xSamples; xIndex++) {
    256            double xfrac0 = xIndex / (double) this.xSamples;
    257            double xfrac1 = (xIndex + 1) / (double) this.xSamples;
    258            for (int zIndex = 0; zIndex < this.zSamples; zIndex++) {
    259                double zfrac0 = zIndex / (double) this.zSamples;
    260                double zfrac1 = (zIndex + 1) / (double) this.zSamples;
    261
    262                double x0 = xAxis.getRange().value(xfrac0);
    263                double x1 = xAxis.getRange().value(xfrac1);
    264                double xm = x0 / 2.0 + x1 / 2.0;
    265                double z0 = zAxis.getRange().value(zfrac0);
    266                double z1 = zAxis.getRange().value(zfrac1);
    267                double zm = z0 / 2.0 + z1 / 2.0;
    268                double y00 = this.function.getValue(x0, z0);
    269                double y01 = this.function.getValue(x0, z1);
    270                double y10 = this.function.getValue(x1, z0);
    271                double y11 = this.function.getValue(x1, z1);
    272                double ymm = this.function.getValue(xm, zm);
    273
    274                double wx0 = xAxis.translateToWorld(x0, xlen) + xOffset;
    275                double wx1 = xAxis.translateToWorld(x1, xlen) + xOffset;
    276                double wy00 = yAxis.translateToWorld(y00, ylen) + yOffset;
    277                double wy01 = yAxis.translateToWorld(y01, ylen) + yOffset;
    278                double wy10 = yAxis.translateToWorld(y10, ylen) + yOffset;
    279                double wy11 = yAxis.translateToWorld(y11, ylen) + yOffset;
    280                double wz0 = zAxis.translateToWorld(z0, zlen) + zOffset;
    281                double wz1 = zAxis.translateToWorld(z1, zlen) + zOffset;
    282
    283                Color color = this.colorScale.valueToColor(ymm);
    284                Object3D obj = new Object3D(color, this.drawFaceOutlines);
    285                List<Point3D> pts1 = facePoints1(wx0, wx1, wz0, wz1, wy00, wy01,
    286                        wy11, yRange);
    287                int count1 = pts1.size();
    288                for (Point3D pt : pts1) {
    289                    obj.addVertex(pt);
    290                }
    291                if (count1 == 3) {
    292                    obj.addDoubleSidedFace(new int[] {0, 1, 2});
    293                } else if (count1 == 4) {
    294                    obj.addDoubleSidedFace(new int[] {0, 1, 2, 3});
    295                } else if (count1 == 5) {
    296                    obj.addDoubleSidedFace(new int[] {0, 1, 2, 3, 4});
    297                }
    298                List<Point3D> pts2 = facePoints2(wx0, wx1, wz0, wz1, wy00, wy11,
    299                        wy10, yRange);
    300                int count2 = pts2.size();
    301                for (Point3D pt : pts2) {
    302                    obj.addVertex(pt);
    303                }
    304                if (count2 == 3) {
    305                    obj.addDoubleSidedFace(new int[] {count1, count1 + 1,
    306                        count1 + 2});
    307                } else if (count2 == 4) {
    308                    obj.addDoubleSidedFace(new int[] {count1, count1 + 1,
    309                        count1 + 2, count1 + 3});
    310                } else if (count2 == 5) {
    311                    obj.addDoubleSidedFace(new int[] {count1, count1 + 1,
    312                        count1 + 2, count1 + 3, count1 + 4});
    313                }
    314                world.add(obj);
    315            }
    316
    317        }
    318    }
    319
    320    private Point3D intersectPoint(double x0, double y0, double z0, double x1,
    321                double y1, double z1, double yy) {
    322        double p = (yy - y0) / (y1 - y0);
    323        double x = x0 + p * (x1 - x0);
    324        double y = y0 + p * (y1 - y0);
    325        double z = z0 + p * (z1 - z0);
    326        return new Point3D(x, y, z);
    327    }
    328
    329    private List<Point3D> facePoints1(double x0, double x1, double z0,
    330            double z1, double y00, double y01, double y11, Range yRange) {
    331
    332        List<Point3D> pts = new ArrayList<>(4);
    333        double ymin = yRange.getMin();
    334        double ymax = yRange.getMax();
    335
    336        // handle y00
    337        if (y00 > yRange.getMax()) {
    338            if (yRange.contains(y01)) {
    339                pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymax));
    340            } else if (y01 < yRange.getMin()) {
    341                pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymax));
    342                pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymin));
    343            }
    344        } else if (yRange.contains(y00)) {
    345            pts.add(new Point3D(x0, y00, z0));
    346            if (y01 > yRange.getMax()) {
    347                pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymax));
    348            } else if (y01 < yRange.getMin()) {
    349                pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymin));
    350            }
    351        } else { // below the range
    352            if (yRange.contains(y01)) {
    353                pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymin));
    354            } else if (y01 > yRange.getMax()) {
    355                pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymin));
    356                pts.add(intersectPoint(x0, y00, z0, x0, y01, z1, ymax));
    357            }
    358        }
    359
    360        // handle y01
    361        if (y01 > yRange.getMax()) {
    362            if (yRange.contains(y11)) {
    363                pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymax));
    364            } else if (y11 < yRange.getMin()) {
    365                pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymax));
    366                pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymin));
    367            }
    368        } else if (yRange.contains(y01)) {
    369            pts.add(new Point3D(x0, y01, z1));
    370            if (y11 > yRange.getMax()) {
    371                pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymax));
    372            } else if (y11 < yRange.getMin()) {
    373                pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymin));
    374            }
    375        } else {
    376            if (y11 > yRange.getMax()) {
    377                pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymin));
    378                pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymax));
    379            } else if (yRange.contains(y11)) {
    380                pts.add(intersectPoint(x0, y01, z1, x1, y11, z1, ymin));
    381            }
    382        }
    383
    384        // handle y11
    385        if (y11 > yRange.getMax()) {
    386            if (yRange.contains(y00)) {
    387                pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymax));
    388            } else if (y00 < yRange.getMin()) {
    389                pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymax));
    390                pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymin));
    391            }
    392        } else if (yRange.contains(y11)) {
    393            pts.add(new Point3D(x1, y11, z1));
    394            if (y00 > yRange.getMax()) {
    395                pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymax));
    396            } else if (y00 < yRange.getMin()) {
    397                pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymin));
    398            }
    399        } else {
    400            if (y00 > yRange.getMax()) {
    401                pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymin));
    402                pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymax));
    403            } else if (yRange.contains(y00)) {
    404                pts.add(intersectPoint(x1, y11, z1, x0, y00, z0, ymin));
    405            }
    406        }
    407        return pts;
    408    }
    409
    410    private List<Point3D> facePoints2(double x0, double x1, double z0,
    411            double z1, double y00, double y11, double y10, Range yRange) {
    412
    413        List<Point3D> pts = new ArrayList<>(4);
    414        double ymin = yRange.getMin();
    415        double ymax = yRange.getMax();
    416        // handle y00
    417        if (y00 > yRange.getMax()) {
    418            if (yRange.contains(y11)) {
    419                pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymax));
    420            } else if (y11 < yRange.getMin()) {
    421                pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymax));
    422                pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymin));
    423            }
    424        } else if (yRange.contains(y00)) {
    425            pts.add(new Point3D(x0, y00, z0));
    426            if (y11 > yRange.getMax()) {
    427                pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymax));
    428            } else if (y11 < yRange.getMin()) {
    429                pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymin));
    430            }
    431        } else { // below the range
    432            if (yRange.contains(y11)) {
    433                pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymin));
    434            } else if (y11 > yRange.getMax()) {
    435                pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymin));
    436                pts.add(intersectPoint(x0, y00, z0, x1, y11, z1, ymax));
    437            }
    438        }
    439
    440        // handle y11
    441        if (y11 > yRange.getMax()) {
    442            if (yRange.contains(y10)) {
    443                pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymax));
    444            } else if (y10 < yRange.getMin()) {
    445                pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymax));
    446                pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymin));
    447            }
    448        } else if (yRange.contains(y11)) {
    449            pts.add(new Point3D(x1, y11, z1));
    450            if (y10 > yRange.getMax()) {
    451                pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymax));
    452            } else if (y10 < yRange.getMin()) {
    453                pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymin));
    454            }
    455        } else {
    456            if (y10 > yRange.getMax()) {
    457                pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymin));
    458                pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymax));
    459            } else if (yRange.contains(y10)) {
    460                pts.add(intersectPoint(x1, y11, z1, x1, y10, z0, ymin));
    461            }
    462        }
    463
    464        // handle y10
    465        if (y10 > yRange.getMax()) {
    466            if (yRange.contains(y00)) {
    467                pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymax));
    468            } else if (y00 < yRange.getMin()) {
    469                pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymax));
    470                pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymin));
    471            }
    472        } else if (yRange.contains(y10)) {
    473            pts.add(new Point3D(x1, y10, z0));
    474            if (y00 > yRange.getMax()) {
    475                pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymax));
    476            } else if (y00 < yRange.getMin()) {
    477                pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymin));
    478            }
    479        } else {
    480            if (y00 > yRange.getMax()) {
    481                pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymin));
    482                pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymax));
    483            } else if (yRange.contains(y00)) {
    484                pts.add(intersectPoint(x1, y10, z0, x0, y00, z0, ymin));
    485            }
    486        }
    487
    488        return pts;
    489    }
    490
    491    /**
    492     * Throws an {@code UnsupportedOperationException} because this
    493     * renderer does not support per-item rendering.
    494     *
    495     * @param dataset the dataset ({@code null} not permitted).
    496     * @param series  the series index.
    497     * @param item  the item index.
    498     * @param world  the world ({@code null} not permitted).
    499     * @param dimensions  the dimensions ({@code null} not permitted).
    500     * @param xOffset  the x-offset.
    501     * @param yOffset  the y-offset.
    502     * @param zOffset  the z-offset.
    503     */
    504    @Override
    505    public void composeItem(XYZDataset dataset, int series, int item,
    506            World world, Dimension3D dimensions, double xOffset,
    507            double yOffset, double zOffset) {
    508        throw new UnsupportedOperationException(
    509                "Not supported by this renderer.");
    510    }
    511
    512    /**
    513     * Returns the current range for the x-axis - the method is overridden
    514     * because this renderer does not use a dataset (it samples and plots a
    515     * function directly).
    516     *
    517     * @param dataset  the dataset (ignored).
    518     *
    519     * @return The x-range (never {@code null}).
    520     */
    521    @Override
    522    public Range findXRange(XYZDataset dataset) {
    523        return getPlot().getXAxis().getRange();
    524    }
    525
    526    /**
    527     * Returns the range that the renderer requires on the y-axis to display
    528     * all the data in the function.
    529     *
    530     * @param dataset  the dataset (ignored).
    531     *
    532     * @return The range.
    533     */
    534    @Override
    535    public Range findYRange(XYZDataset dataset) {
    536        return Function3DUtils.findYRange(this.function,
    537                getPlot().getXAxis().getRange(),
    538                getPlot().getZAxis().getRange(),
    539                this.xSamples, this.zSamples, true);
    540    }
    541
    542    /**
    543     * Returns the current range for the z-axis - the method is overridden
    544     * because this renderer does not use a dataset (it samples and plots a
    545     * function directly).
    546     *
    547     * @param dataset  the dataset (ignored).
    548     *
    549     * @return The z-range (never {@code null}).
    550     */
    551    @Override
    552    public Range findZRange(XYZDataset dataset) {
    553        return getPlot().getZAxis().getRange();
    554    }
    555
    556    /**
    557     * Tests this renderer for equality with an arbitrary object.
    558     *
    559     * @param obj  the object ({@code null} not permitted).
    560     *
    561     * @return A boolean.
    562     */
    563    @Override
    564    public boolean equals(Object obj) {
    565        if (obj == this) {
    566            return true;
    567        }
    568        if (!(obj instanceof SurfaceRenderer)) {
    569            return false;
    570        }
    571        SurfaceRenderer that = (SurfaceRenderer) obj;
    572        if (!this.function.equals(that.function)) {
    573            return false;
    574        }
    575        if (this.xSamples != that.xSamples) {
    576            return false;
    577        }
    578        if (this.zSamples != that.zSamples) {
    579            return false;
    580        }
    581        if (!this.colorScale.equals(that.colorScale)) {
    582            return false;
    583        }
    584        if (this.drawFaceOutlines != that.drawFaceOutlines) {
    585            return false;
    586        }
    587        return super.equals(obj);
    588    }
    589}