001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.geometry.euclidean.twod;
018
019import java.util.ArrayList;
020import java.util.Collections;
021import java.util.List;
022import java.util.stream.Collectors;
023import java.util.stream.Stream;
024import java.util.stream.StreamSupport;
025
026import org.apache.commons.geometry.core.partitioning.Hyperplane;
027import org.apache.commons.geometry.core.partitioning.Split;
028import org.apache.commons.geometry.core.partitioning.bsp.AbstractBSPTree;
029import org.apache.commons.geometry.core.partitioning.bsp.AbstractPartitionedRegionBuilder;
030import org.apache.commons.geometry.core.partitioning.bsp.AbstractRegionBSPTree;
031import org.apache.commons.geometry.core.partitioning.bsp.BSPTreeVisitor;
032import org.apache.commons.geometry.core.partitioning.bsp.RegionCutBoundary;
033import org.apache.commons.geometry.euclidean.twod.path.InteriorAngleLinePathConnector;
034import org.apache.commons.geometry.euclidean.twod.path.LinePath;
035import org.apache.commons.numbers.core.Precision;
036
037/** Binary space partitioning (BSP) tree representing a region in two dimensional
038 * Euclidean space.
039 */
040public final class RegionBSPTree2D extends AbstractRegionBSPTree<Vector2D, RegionBSPTree2D.RegionNode2D>
041    implements BoundarySource2D {
042
043    /** List of line subset paths comprising the region boundary. */
044    private List<LinePath> boundaryPaths;
045
046    /** Create a new, empty region.
047     */
048    public RegionBSPTree2D() {
049        this(false);
050    }
051
052    /** Create a new region. If {@code full} is true, then the region will
053     * represent the entire 2D space. Otherwise, it will be empty.
054     * @param full whether or not the region should contain the entire
055     *      2D space or be empty
056     */
057    public RegionBSPTree2D(final boolean full) {
058        super(full);
059    }
060
061    /** Return a deep copy of this instance.
062     * @return a deep copy of this instance.
063     * @see #copy(org.apache.commons.geometry.core.partitioning.bsp.BSPTree)
064     */
065    public RegionBSPTree2D copy() {
066        final RegionBSPTree2D result = RegionBSPTree2D.empty();
067        result.copy(this);
068
069        return result;
070    }
071
072    /** {@inheritDoc} */
073    @Override
074    public Iterable<LineConvexSubset> boundaries() {
075        return createBoundaryIterable(LineConvexSubset.class::cast);
076    }
077
078    /** {@inheritDoc} */
079    @Override
080    public Stream<LineConvexSubset> boundaryStream() {
081        return StreamSupport.stream(boundaries().spliterator(), false);
082    }
083
084    /** {@inheritDoc} */
085    @Override
086    public List<LineConvexSubset> getBoundaries() {
087        return createBoundaryList(LineConvexSubset.class::cast);
088    }
089
090    /** Get the boundary of the region as a list of connected line subset paths.
091     * The line subset are oriented such that their minus (left) side lies on the
092     * interior of the region.
093     * @return line subset paths representing the region boundary
094     */
095    public List<LinePath> getBoundaryPaths() {
096        if (boundaryPaths == null) {
097            boundaryPaths = Collections.unmodifiableList(computeBoundaryPaths());
098        }
099        return boundaryPaths;
100    }
101
102    /** Add a convex area to this region. The resulting region will be the
103     * union of the convex area and the region represented by this instance.
104     * @param area the convex area to add
105     */
106    public void add(final ConvexArea area) {
107        union(area.toTree());
108    }
109
110    /** Return a list of {@link ConvexArea}s representing the same region
111     * as this instance. One convex area is returned for each interior leaf
112     * node in the tree.
113     * @return a list of convex areas representing the same region as this
114     *      instance
115     */
116    public List<ConvexArea> toConvex() {
117        final List<ConvexArea> result = new ArrayList<>();
118
119        toConvexRecursive(getRoot(), ConvexArea.full(), result);
120
121        return result;
122    }
123
124    /** Recursive method to compute the convex areas of all inside leaf nodes in the subtree rooted at the given
125     * node. The computed convex areas are added to the given list.
126     * @param node root of the subtree to compute the convex areas for
127     * @param nodeArea the convex area for the current node; this will be split by the node's cut hyperplane to
128     *      form the convex areas for any child nodes
129     * @param result list containing the results of the computation
130     */
131    private void toConvexRecursive(final RegionNode2D node, final ConvexArea nodeArea,
132            final List<? super ConvexArea> result) {
133        if (node.isLeaf()) {
134            // base case; only add to the result list if the node is inside
135            if (node.isInside()) {
136                result.add(nodeArea);
137            }
138        } else {
139            // recurse
140            final Split<ConvexArea> split = nodeArea.split(node.getCutHyperplane());
141
142            toConvexRecursive(node.getMinus(), split.getMinus(), result);
143            toConvexRecursive(node.getPlus(), split.getPlus(), result);
144        }
145    }
146
147    /** {@inheritDoc} */
148    @Override
149    public Split<RegionBSPTree2D> split(final Hyperplane<Vector2D> splitter) {
150        return split(splitter, RegionBSPTree2D.empty(), RegionBSPTree2D.empty());
151    }
152
153    /** {@inheritDoc} */
154    @Override
155    public Vector2D project(final Vector2D pt) {
156        // use our custom projector so that we can disambiguate points that are
157        // actually equidistant from the target point
158        final BoundaryProjector2D projector = new BoundaryProjector2D(pt);
159        accept(projector);
160
161        return projector.getProjected();
162    }
163
164    /** Return the current instance.
165     */
166    @Override
167    public RegionBSPTree2D toTree() {
168        return this;
169    }
170
171    /** {@inheritDoc} */
172    @Override
173    public List<LinecastPoint2D> linecast(final LineConvexSubset subset) {
174        final LinecastVisitor visitor = new LinecastVisitor(subset, false);
175        accept(visitor);
176
177        return visitor.getResults();
178    }
179
180    /** {@inheritDoc} */
181    @Override
182    public LinecastPoint2D linecastFirst(final LineConvexSubset subset) {
183        final LinecastVisitor visitor = new LinecastVisitor(subset, true);
184        accept(visitor);
185
186        return visitor.getFirstResult();
187    }
188
189    /** Compute the line subset paths comprising the region boundary.
190     * @return the line subset paths comprising the region boundary
191     */
192    private List<LinePath> computeBoundaryPaths() {
193        final InteriorAngleLinePathConnector connector = new InteriorAngleLinePathConnector.Minimize();
194        connector.connect(boundaries());
195
196        return connector.connectAll().stream()
197                .map(LinePath::simplify).collect(Collectors.toList());
198    }
199
200    /** {@inheritDoc} */
201    @Override
202    protected RegionSizeProperties<Vector2D> computeRegionSizeProperties() {
203        // handle simple cases
204        if (isFull()) {
205            return new RegionSizeProperties<>(Double.POSITIVE_INFINITY, null);
206        } else if (isEmpty()) {
207            return new RegionSizeProperties<>(0, null);
208        }
209
210        // compute the size based on the boundary line subsets
211        double quadrilateralAreaSum = 0.0;
212
213        double scaledSumX = 0.0;
214        double scaledSumY = 0.0;
215
216        Vector2D startPoint;
217        Vector2D endPoint;
218        double signedArea;
219
220        for (final LineConvexSubset boundary : boundaries()) {
221
222            if (boundary.isInfinite()) {
223                // at least on boundary is infinite, meaning that
224                // the size is also infinite
225                quadrilateralAreaSum = Double.POSITIVE_INFINITY;
226
227                break;
228            }
229
230            startPoint = boundary.getStartPoint();
231            endPoint = boundary.getEndPoint();
232
233            // compute the area
234            signedArea = startPoint.signedArea(endPoint);
235
236            quadrilateralAreaSum += signedArea;
237
238            // compute scaled coordinate values for the centroid
239            scaledSumX += signedArea * (startPoint.getX() + endPoint.getX());
240            scaledSumY += signedArea * (startPoint.getY() + endPoint.getY());
241        }
242
243        double size = Double.POSITIVE_INFINITY;
244        Vector2D centroid = null;
245
246        // The area is finite only if the computed quadrilateral area is finite and non-negative.
247        // Negative areas indicate that the region is inside-out, with a finite outside surrounded
248        // by an infinite inside.
249        if (quadrilateralAreaSum >= 0 && Double.isFinite(quadrilateralAreaSum)) {
250            size = 0.5 * quadrilateralAreaSum;
251
252            if (quadrilateralAreaSum > 0) {
253                centroid = Vector2D.of(scaledSumX, scaledSumY).multiply(1.0 / (3.0 * quadrilateralAreaSum));
254            }
255        }
256
257        return new RegionSizeProperties<>(size, centroid);
258    }
259
260    /** {@inheritDoc} */
261    @Override
262    protected void invalidate() {
263        super.invalidate();
264
265        boundaryPaths = null;
266    }
267
268    /** {@inheritDoc} */
269    @Override
270    protected RegionNode2D createNode() {
271        return new RegionNode2D(this);
272    }
273
274    /** Return a new {@link RegionBSPTree2D} instance containing the entire space.
275     * @return a new {@link RegionBSPTree2D} instance containing the entire space
276     */
277    public static RegionBSPTree2D full() {
278        return new RegionBSPTree2D(true);
279    }
280
281    /** Return a new, empty {@link RegionBSPTree2D} instance.
282     * @return a new, empty {@link RegionBSPTree2D} instance
283     */
284    public static RegionBSPTree2D empty() {
285        return new RegionBSPTree2D(false);
286    }
287
288    /** Construct a new tree from the given boundaries. If no boundaries
289     * are present, the returned tree is empty.
290     * @param boundaries boundaries to construct the tree from
291     * @return a new tree instance constructed from the given boundaries
292     * @see #from(Iterable, boolean)
293     */
294    public static RegionBSPTree2D from(final Iterable<? extends LineConvexSubset> boundaries) {
295        return from(boundaries, false);
296    }
297
298    /** Construct a new tree from the given boundaries. If {@code full} is true, then
299     * the initial tree before boundary insertion contains the entire space. Otherwise,
300     * it is empty.
301     * @param boundaries boundaries to construct the tree from
302     * @param full if true, the initial tree will contain the entire space
303     * @return a new tree instance constructed from the given boundaries
304     */
305    public static RegionBSPTree2D from(final Iterable<? extends LineConvexSubset> boundaries, final boolean full) {
306        final RegionBSPTree2D tree = new RegionBSPTree2D(full);
307        tree.insert(boundaries);
308
309        return tree;
310    }
311
312    /** Create a new {@link PartitionedRegionBuilder2D} instance which can be used to build balanced
313     * BSP trees from region boundaries.
314     * @return a new {@link PartitionedRegionBuilder2D} instance
315     */
316    public static PartitionedRegionBuilder2D partitionedRegionBuilder() {
317        return new PartitionedRegionBuilder2D();
318    }
319
320    /** BSP tree node for two dimensional Euclidean space.
321     */
322    public static final class RegionNode2D extends AbstractRegionBSPTree.AbstractRegionNode<Vector2D, RegionNode2D> {
323        /** Simple constructor.
324         * @param tree the owning tree instance
325         */
326        private RegionNode2D(final AbstractBSPTree<Vector2D, RegionNode2D> tree) {
327            super(tree);
328        }
329
330        /** Get the region represented by this node. The returned region contains
331         * the entire area contained in this node, regardless of the attributes of
332         * any child nodes.
333         * @return the region represented by this node
334         */
335        public ConvexArea getNodeRegion() {
336            ConvexArea area = ConvexArea.full();
337
338            RegionNode2D child = this;
339            RegionNode2D parent;
340
341            while ((parent = child.getParent()) != null) {
342                final Split<ConvexArea> split = area.split(parent.getCutHyperplane());
343
344                area = child.isMinus() ? split.getMinus() : split.getPlus();
345
346                child = parent;
347            }
348
349            return area;
350        }
351
352        /** {@inheritDoc} */
353        @Override
354        protected RegionNode2D getSelf() {
355            return this;
356        }
357    }
358
359    /** Class used to build regions in Euclidean 2D space by inserting boundaries into a BSP
360     * tree containing "partitions", i.e. structural cuts where both sides of the cut have the same region location.
361     * When partitions are chosen that effectively divide the region boundaries at each partition level, the
362     * constructed tree is shallower and more balanced than one constructed from the region boundaries alone,
363     * resulting in improved performance. For example, consider a line segment approximation of a circle. The region is
364     * convex so each boundary has all of the other boundaries on its minus side; the plus sides are all empty.
365     * When these boundaries are inserted directly into a tree, the tree degenerates into a simple linked list of
366     * nodes with a height directly proportional to the number of boundaries. This means that many operations on the
367     * tree, such as inside/outside testing of points, involve iterating through each and every region boundary. In
368     * contrast, if a partition is first inserted that passes through the circle center, the first BSP tree node
369     * contains region nodes on its plus <em>and</em> minus sides, cutting the height of the tree in half. Operations
370     * such as inside/outside testing are then able to skip half of the tree nodes with a single test on the
371     * root node, resulting in drastically improved performance. Insertion of additional partitions (using a grid
372     * layout, for example) can produce even shallower trees, although there is a point unique to each boundary set at
373     * which the addition of more partitions begins to decrease instead of increase performance.
374     *
375     * <h2>Usage</h2>
376     * <p>Usage of this class consists of two phases: (1) <em>partition insertion</em> and (2) <em>boundary
377     * insertion</em>. Instances begin in the <em>partition insertion</em> phase. Here, partitions can be inserted
378     * into the empty tree using {@link PartitionedRegionBuilder2D#insertPartition(LineConvexSubset) insertPartition}
379     * or similar methods. The {@link org.apache.commons.geometry.core.partitioning.bsp.RegionCutRule#INHERIT INHERIT}
380     * cut rule is used internally to insert the cut so the represented region remains empty even as partitions are
381     * inserted.
382     * </p>
383     *
384     * <p>The instance moves into the <em>boundary insertion</em> phase when the caller inserts the first region
385     * boundary, using {@link PartitionedRegionBuilder2D#insertBoundary(LineConvexSubset) insertBoundary} or
386     * similar methods. Attempting to insert a partition after this point results in an {@code IllegalStateException}.
387     * This ensures that partitioning cuts are always located higher up the tree than boundary cuts.</p>
388     *
389     * <p>After all boundaries are inserted, the {@link PartitionedRegionBuilder2D#build() build} method is used
390     * to perform final processing and return the computed tree.</p>
391     */
392    public static final class PartitionedRegionBuilder2D
393        extends AbstractPartitionedRegionBuilder<Vector2D, RegionNode2D> {
394
395        /** Construct a new builder instance.
396         */
397        private PartitionedRegionBuilder2D() {
398            super(RegionBSPTree2D.empty());
399        }
400
401        /** Insert a partition line.
402         * @param partition partition to insert
403         * @return this instance
404         * @throws IllegalStateException if a boundary has previously been inserted
405         */
406        public PartitionedRegionBuilder2D insertPartition(final Line partition) {
407            return insertPartition(partition.span());
408        }
409
410        /** Insert a line convex subset as a partition.
411         * @param partition partition to insert
412         * @return this instance
413         * @throws IllegalStateException if a boundary has previously been inserted
414         */
415        public PartitionedRegionBuilder2D insertPartition(final LineConvexSubset partition) {
416            insertPartitionInternal(partition);
417
418            return this;
419        }
420
421        /** Insert two axis aligned lines intersecting at the given point as partitions.
422         * The lines each contain the {@code center} point and have the directions {@code +x} and {@code +y}
423         * in that order. If inserted into an empty tree, this will partition the space
424         * into 4 sections.
425         * @param center center point for the partitions; the inserted lines intersect at this point
426         * @param precision precision context used to construct the lines
427         * @return this instance
428         * @throws IllegalStateException if a boundary has previously been inserted
429         */
430        public PartitionedRegionBuilder2D insertAxisAlignedPartitions(final Vector2D center,
431                final Precision.DoubleEquivalence precision) {
432
433            insertPartition(Lines.fromPointAndDirection(center, Vector2D.Unit.PLUS_X, precision));
434            insertPartition(Lines.fromPointAndDirection(center, Vector2D.Unit.PLUS_Y, precision));
435
436            return this;
437        }
438
439        /** Insert a grid of partitions. The partitions are constructed recursively: at each level two axis-aligned
440         * partitioning lines are inserted using
441         * {@link #insertAxisAlignedPartitions(Vector2D, Precision.DoubleEquivalence) insertAxisAlignedPartitions}.
442         * The algorithm then recurses using bounding boxes from the min point to the center and from the center
443         * point to the max. Note that this means no partitions are ever inserted directly on the boundaries of
444         * the given bounding box. This is intentional and done to allow this method to be called directly with the
445         * bounding box from a set of boundaries to be inserted without unnecessarily adding partitions that will
446         * never have region boundaries on both sides.
447         * @param bounds bounding box for the grid
448         * @param level recursion level for the grid; each level subdivides each grid cube into 4 sections, making the
449         *      total number of grid cubes equal to {@code 4 ^ level}
450         * @param precision precision context used to construct the partition lines
451         * @return this instance
452         * @throws IllegalStateException if a boundary has previously been inserted
453         */
454        public PartitionedRegionBuilder2D insertAxisAlignedGrid(final Bounds2D bounds, final int level,
455                final Precision.DoubleEquivalence precision) {
456
457            insertAxisAlignedGridRecursive(bounds.getMin(), bounds.getMax(), level, precision);
458
459            return this;
460        }
461
462        /** Recursively insert axis-aligned grid partitions.
463         * @param min min point for the grid square to partition
464         * @param max max point for the grid square to partition
465         * @param level current recursion level
466         * @param precision precision context used to construct the partition planes
467         */
468        private void insertAxisAlignedGridRecursive(final Vector2D min, final Vector2D max, final int level,
469                final Precision.DoubleEquivalence precision) {
470            if (level > 0) {
471                final Vector2D center = min.lerp(max, 0.5);
472
473                insertAxisAlignedPartitions(center, precision);
474
475                final int nextLevel = level - 1;
476                insertAxisAlignedGridRecursive(min, center, nextLevel, precision);
477                insertAxisAlignedGridRecursive(center, max, nextLevel, precision);
478            }
479        }
480
481        /** Insert a region boundary.
482         * @param boundary region boundary to insert
483         * @return this instance
484         */
485        public PartitionedRegionBuilder2D insertBoundary(final LineConvexSubset boundary) {
486            insertBoundaryInternal(boundary);
487
488            return this;
489        }
490
491        /** Insert a collection of region boundaries.
492         * @param boundaries boundaries to insert
493         * @return this instance
494         */
495        public PartitionedRegionBuilder2D insertBoundaries(final Iterable<? extends LineConvexSubset> boundaries) {
496            for (final LineConvexSubset boundary : boundaries) {
497                insertBoundaryInternal(boundary);
498            }
499
500            return this;
501        }
502
503        /** Insert all boundaries from the given source.
504         * @param boundarySrc source of boundaries to insert
505         * @return this instance
506         */
507        public PartitionedRegionBuilder2D insertBoundaries(final BoundarySource2D boundarySrc) {
508            try (Stream<LineConvexSubset> stream = boundarySrc.boundaryStream()) {
509                stream.forEach(this::insertBoundaryInternal);
510            }
511
512            return this;
513        }
514
515        /** Build and return the region BSP tree.
516         * @return the region BSP tree
517         */
518        public RegionBSPTree2D build() {
519            return (RegionBSPTree2D) buildInternal();
520        }
521    }
522
523    /** Class used to project points onto the 2D region boundary.
524     */
525    private static final class BoundaryProjector2D extends BoundaryProjector<Vector2D, RegionNode2D> {
526        /** Simple constructor.
527         * @param point the point to project onto the region's boundary
528         */
529        BoundaryProjector2D(final Vector2D point) {
530            super(point);
531        }
532
533        /** {@inheritDoc} */
534        @Override
535        protected Vector2D disambiguateClosestPoint(final Vector2D target, final Vector2D a, final Vector2D b) {
536            // return the point with the smallest coordinate values
537            final int cmp = Vector2D.COORDINATE_ASCENDING_ORDER.compare(a, b);
538            return cmp < 0 ? a : b;
539        }
540    }
541
542    /** BSP tree visitor that performs a linecast operation against the boundaries of the visited tree.
543     */
544    private static final class LinecastVisitor implements BSPTreeVisitor<Vector2D, RegionNode2D> {
545
546        /** The line subset to intersect with the boundaries of the BSP tree. */
547        private final LineConvexSubset linecastSubset;
548
549        /** If true, the visitor will stop visiting the tree once the first linecast
550         * point is determined.
551         */
552        private final boolean firstOnly;
553
554        /** The minimum abscissa found during the search. */
555        private double minAbscissa = Double.POSITIVE_INFINITY;
556
557        /** List of results from the linecast operation. */
558        private final List<LinecastPoint2D> results = new ArrayList<>();
559
560        /** Create a new instance with the given intersecting line subset.
561         * @param linecastSubset line subset to intersect with the BSP tree region boundary
562         * @param firstOnly if true, the visitor will stop visiting the tree once the first
563         *      linecast point is determined
564         */
565        LinecastVisitor(final LineConvexSubset linecastSubset, final boolean firstOnly) {
566            this.linecastSubset = linecastSubset;
567            this.firstOnly = firstOnly;
568        }
569
570        /** Get the first {@link LinecastPoint2D} resulting from the linecast operation.
571         * @return the first linecast result point
572         */
573        public LinecastPoint2D getFirstResult() {
574            final List<LinecastPoint2D> sortedResults = getResults();
575
576            return sortedResults.isEmpty() ?
577                    null :
578                    sortedResults.get(0);
579        }
580
581        /** Get a list containing the results of the linecast operation. The list is
582         * sorted and filtered.
583         * @return list of sorted and filtered results from the linecast operation
584         */
585        public List<LinecastPoint2D> getResults() {
586            LinecastPoint2D.sortAndFilter(results);
587
588            return results;
589        }
590
591        /** {@inheritDoc} */
592        @Override
593        public Order visitOrder(final RegionNode2D internalNode) {
594            final Line cut = (Line) internalNode.getCutHyperplane();
595            final Line line = linecastSubset.getLine();
596
597            final boolean plusIsNear = line.getDirection().dot(cut.getOffsetDirection()) < 0;
598
599            return plusIsNear ?
600                    Order.PLUS_NODE_MINUS :
601                    Order.MINUS_NODE_PLUS;
602        }
603
604        /** {@inheritDoc} */
605        @Override
606        public Result visit(final RegionNode2D node) {
607            if (node.isInternal()) {
608                // check if the line subset intersects the node cut
609                final Line line = linecastSubset.getLine();
610                final Vector2D pt = ((Line) node.getCutHyperplane()).intersection(line);
611
612                if (pt != null) {
613                    if (firstOnly && !results.isEmpty() &&
614                            line.getPrecision().compare(minAbscissa, line.abscissa(pt)) < 0) {
615                        // we have results and we are now sure that no other intersection points will be
616                        // found that are closer or at the same position on the intersecting line.
617                        return Result.TERMINATE;
618                    } else if (linecastSubset.contains(pt)) {
619                        // we've potentially found a new linecast point; add it to the list of potential
620                        // results
621                        final LinecastPoint2D potentialResult = computeLinecastPoint(pt, node);
622                        if (potentialResult != null) {
623                            results.add(potentialResult);
624
625                            // update the min abscissa
626                            minAbscissa = Math.min(minAbscissa, potentialResult.getAbscissa());
627                        }
628                    }
629                }
630            }
631
632            return Result.CONTINUE;
633        }
634
635        /** Compute the linecast point for the given intersection point and tree node, returning null
636         * if the point does not actually lie on the region boundary.
637         * @param pt intersection point
638         * @param node node containing the cut that the linecast line intersected with
639         * @return a new linecast point instance or null if the intersection point does not lie
640         *      on the region boundary
641         */
642        private LinecastPoint2D computeLinecastPoint(final Vector2D pt, final RegionNode2D node) {
643            final Line cut = (Line) node.getCutHyperplane();
644            final RegionCutBoundary<Vector2D> boundary = node.getCutBoundary();
645
646            boolean onBoundary = false;
647            boolean negateNormal = false;
648
649            if (boundary.containsInsideFacing(pt)) {
650                // on inside-facing boundary
651                onBoundary = true;
652                negateNormal = true;
653            } else  if (boundary.containsOutsideFacing(pt)) {
654                // on outside-facing boundary
655                onBoundary = true;
656            }
657
658            if (onBoundary) {
659                Vector2D normal = cut.getOffsetDirection();
660                if (negateNormal) {
661                    normal = normal.negate();
662                }
663
664                return new LinecastPoint2D(pt, normal, linecastSubset.getLine());
665            }
666
667            return null;
668        }
669    }
670}