Skip to content

Commit f4a63e5

Browse files
authored
Merge pull request #4577 from kwvanderlinde/refactor/4506-visibility-cleanup
Simplify AreaMeta and move vision blocking algorithm into a dedicated class
2 parents 5b424c3 + b1a5a35 commit f4a63e5

10 files changed

+565
-786
lines changed

src/main/java/net/rptools/lib/GeometryUtil.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.locationtech.jts.geom.Coordinate;
2626
import org.locationtech.jts.geom.Geometry;
2727
import org.locationtech.jts.geom.GeometryFactory;
28+
import org.locationtech.jts.geom.Polygon;
2829
import org.locationtech.jts.geom.PrecisionModel;
2930
import org.locationtech.jts.noding.NodableSegmentString;
3031
import org.locationtech.jts.noding.NodedSegmentString;
@@ -68,7 +69,7 @@ public static GeometryFactory getGeometryFactory() {
6869
return geometryFactory;
6970
}
7071

71-
public static Geometry toJts(Area area) {
72+
private static Polygonizer toPolygonizer(Area area) {
7273
final var pathIterator = area.getPathIterator(null);
7374
final var polygonizer = new Polygonizer(true);
7475
final var coords = (List<Coordinate[]>) ShapeReader.toCoordinates(pathIterator);
@@ -78,6 +79,7 @@ public static Geometry toJts(Area area) {
7879
for (var string : coords) {
7980
strings.add(new NodedSegmentString(string, null));
8081
}
82+
8183
final var noder = new SnapRoundingNoder(precisionModel);
8284
noder.computeNodes(strings);
8385
final Collection<? extends SegmentString> nodedStrings = noder.getNodedSubstrings();
@@ -99,6 +101,14 @@ public static Geometry toJts(Area area) {
99101
invalidRings);
100102
}
101103

102-
return polygonizer.getGeometry();
104+
return polygonizer;
105+
}
106+
107+
public static Geometry toJts(Area area) {
108+
return toPolygonizer(area).getGeometry();
109+
}
110+
111+
public static Collection<Polygon> toJtsPolygons(Area area) {
112+
return toPolygonizer(area).getPolygons();
103113
}
104114
}

src/main/java/net/rptools/maptool/client/ui/zone/FogUtil.java

Lines changed: 21 additions & 199 deletions
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,20 @@
1818
import java.awt.Rectangle;
1919
import java.awt.geom.Area;
2020
import java.util.ArrayList;
21-
import java.util.Collections;
22-
import java.util.Comparator;
23-
import java.util.HashMap;
21+
import java.util.EnumMap;
2422
import java.util.HashSet;
25-
import java.util.IdentityHashMap;
2623
import java.util.List;
2724
import java.util.Map;
2825
import java.util.Set;
2926
import java.util.function.Consumer;
30-
import java.util.function.Function;
3127
import javax.annotation.Nonnull;
32-
import javax.annotation.Nullable;
3328
import net.rptools.lib.CodeTimer;
3429
import net.rptools.lib.GeometryUtil;
3530
import net.rptools.maptool.client.AppUtil;
3631
import net.rptools.maptool.client.MapTool;
3732
import net.rptools.maptool.client.ui.zone.renderer.ZoneRenderer;
3833
import net.rptools.maptool.client.ui.zone.vbl.AreaTree;
39-
import net.rptools.maptool.client.ui.zone.vbl.VisibilitySweepEndpoint;
34+
import net.rptools.maptool.client.ui.zone.vbl.VisibilityProblem;
4035
import net.rptools.maptool.client.ui.zone.vbl.VisionBlockingAccumulator;
4136
import net.rptools.maptool.model.AbstractPoint;
4237
import net.rptools.maptool.model.CellPoint;
@@ -51,18 +46,11 @@
5146
import net.rptools.maptool.model.player.Player.Role;
5247
import org.apache.logging.log4j.LogManager;
5348
import org.apache.logging.log4j.Logger;
54-
import org.locationtech.jts.algorithm.Orientation;
5549
import org.locationtech.jts.awt.ShapeWriter;
5650
import org.locationtech.jts.geom.Coordinate;
5751
import org.locationtech.jts.geom.Geometry;
5852
import org.locationtech.jts.geom.GeometryFactory;
59-
import org.locationtech.jts.geom.LineSegment;
60-
import org.locationtech.jts.geom.LineString;
61-
import org.locationtech.jts.geom.Polygon;
62-
import org.locationtech.jts.geom.prep.PreparedGeometry;
6353
import org.locationtech.jts.geom.prep.PreparedGeometryFactory;
64-
import org.locationtech.jts.geom.util.LineStringExtracter;
65-
import org.locationtech.jts.operation.union.UnaryUnionOp;
6654

6755
public class FogUtil {
6856
private static final Logger log = LogManager.getLogger(FogUtil.class);
@@ -73,13 +61,13 @@ public class FogUtil {
7361
*
7462
* @param origin the vision origin.
7563
* @param vision the lightSourceArea.
76-
* @param topology the VBL topology.
64+
* @param wallVbl the VBL topology.
7765
* @return the visible area.
7866
*/
7967
public static @Nonnull Area calculateVisibility(
8068
Point origin,
8169
Area vision,
82-
AreaTree topology,
70+
AreaTree wallVbl,
8371
AreaTree hillVbl,
8472
AreaTree pitVbl,
8573
AreaTree coverVbl) {
@@ -94,26 +82,31 @@ public class FogUtil {
9482
* cannot handle. These cases do not exist within a single type of topology, but can arise when
9583
* we combine them.
9684
*/
85+
86+
var topologies = new EnumMap<Zone.TopologyType, AreaTree>(Zone.TopologyType.class);
87+
topologies.put(Zone.TopologyType.WALL_VBL, wallVbl);
88+
topologies.put(Zone.TopologyType.HILL_VBL, hillVbl);
89+
topologies.put(Zone.TopologyType.PIT_VBL, pitVbl);
90+
topologies.put(Zone.TopologyType.COVER_VBL, coverVbl);
91+
9792
List<Geometry> visibleAreas = new ArrayList<>();
98-
final List<Function<VisionBlockingAccumulator, Boolean>> topologyConsumers = new ArrayList<>();
99-
topologyConsumers.add(acc -> acc.addWallBlocking(topology));
100-
topologyConsumers.add(acc -> acc.addHillBlocking(hillVbl));
101-
topologyConsumers.add(acc -> acc.addPitBlocking(pitVbl));
102-
topologyConsumers.add(acc -> acc.addCoverBlocking(coverVbl));
103-
for (final var consumer : topologyConsumers) {
93+
for (final var topology : topologies.entrySet()) {
94+
final var solver =
95+
new VisibilityProblem(
96+
geometryFactory, new Coordinate(origin.getX(), origin.getY()), visionGeometry);
10497
final var accumulator =
10598
new VisionBlockingAccumulator(geometryFactory, origin, visionGeometry);
106-
final var isVisionCompletelyBlocked = consumer.apply(accumulator);
99+
final var isVisionCompletelyBlocked = accumulator.add(topology.getKey(), topology.getValue());
107100
if (!isVisionCompletelyBlocked) {
108101
// Vision has been completely blocked by this topology. Short circuit.
109102
return new Area();
110103
}
111104

112-
final var visibleArea =
113-
calculateVisibleArea(
114-
new Coordinate(origin.getX(), origin.getY()),
115-
accumulator.getVisionBlockingSegments(),
116-
visionGeometry);
105+
for (var string : accumulator.getVisionBlockingSegments()) {
106+
solver.add(string);
107+
}
108+
109+
final var visibleArea = solver.solve();
117110
if (visibleArea != null) {
118111
visibleAreas.add(visibleArea);
119112
}
@@ -134,177 +127,6 @@ public class FogUtil {
134127
return vision;
135128
}
136129

137-
private record NearestWallResult(LineSegment wall, Coordinate point, double distance) {}
138-
139-
private static NearestWallResult findNearestOpenWall(
140-
Set<LineSegment> openWalls, LineSegment ray) {
141-
assert !openWalls.isEmpty();
142-
143-
@Nullable LineSegment currentNearest = null;
144-
@Nullable Coordinate currentNearestPoint = null;
145-
double nearestDistance = Double.MAX_VALUE;
146-
for (final var openWall : openWalls) {
147-
final var intersection = ray.lineIntersection(openWall);
148-
if (intersection == null) {
149-
continue;
150-
}
151-
152-
final var distance = ray.p0.distance(intersection);
153-
if (distance < nearestDistance) {
154-
currentNearest = openWall;
155-
currentNearestPoint = intersection;
156-
nearestDistance = distance;
157-
}
158-
}
159-
160-
assert currentNearest != null;
161-
return new NearestWallResult(currentNearest, currentNearestPoint, nearestDistance);
162-
}
163-
164-
/**
165-
* Builds a list of endpoints for the sweep algorithm to consume.
166-
*
167-
* <p>The endpoints will be unique (i.e., no coordinate is represented more than once) and in a
168-
* consistent orientation (i.e., counterclockwise around the origin). In addition, all endpoints
169-
* will have their starting and ending walls filled according to which walls are incident to the
170-
* corresponding point.
171-
*
172-
* @param origin The center of vision, by which orientation can be determined.
173-
* @param visionBlockingSegments The "walls" that are able to block vision. All points in these
174-
* walls will be present in the returned list.
175-
* @return A list of all endpoints in counterclockwise order.
176-
*/
177-
private static List<VisibilitySweepEndpoint> getSweepEndpoints(
178-
Coordinate origin, List<LineString> visionBlockingSegments) {
179-
final Map<Coordinate, VisibilitySweepEndpoint> endpointsByPosition = new HashMap<>();
180-
for (final var segment : visionBlockingSegments) {
181-
VisibilitySweepEndpoint current = null;
182-
for (final var coordinate : segment.getCoordinates()) {
183-
final var previous = current;
184-
current =
185-
endpointsByPosition.computeIfAbsent(
186-
coordinate, c -> new VisibilitySweepEndpoint(c, origin));
187-
if (previous == null) {
188-
// We just started this segment; still need a second point.
189-
continue;
190-
}
191-
192-
final var isForwards =
193-
Orientation.COUNTERCLOCKWISE
194-
== Orientation.index(origin, previous.getPoint(), current.getPoint());
195-
// Make sure the wall always goes in the counterclockwise direction.
196-
final LineSegment wall =
197-
isForwards
198-
? new LineSegment(previous.getPoint(), coordinate)
199-
: new LineSegment(coordinate, previous.getPoint());
200-
if (isForwards) {
201-
previous.startsWall(wall);
202-
current.endsWall(wall);
203-
} else {
204-
previous.endsWall(wall);
205-
current.startsWall(wall);
206-
}
207-
}
208-
}
209-
final List<VisibilitySweepEndpoint> endpoints = new ArrayList<>(endpointsByPosition.values());
210-
211-
endpoints.sort(
212-
Comparator.comparingDouble(VisibilitySweepEndpoint::getPseudoangle)
213-
.thenComparing(VisibilitySweepEndpoint::getDistance));
214-
215-
return endpoints;
216-
}
217-
218-
private static @Nullable Geometry calculateVisibleArea(
219-
Coordinate origin, List<LineString> visionBlockingSegments, PreparedGeometry visionGeometry) {
220-
if (visionBlockingSegments.isEmpty()) {
221-
// No topology, apparently.
222-
return null;
223-
}
224-
225-
/*
226-
* Unioning all the line segments has the nice effect of noding any intersections between line
227-
* segments. Without this, it may not be valid.
228-
* Note: if the geometry were only composed of one topology, it would certainly be valid due to
229-
* its "flat" nature. But even in that case, it is more robust to due the union in case this
230-
* flatness assumption ever changes.
231-
*/
232-
final var allWallGeometry = new UnaryUnionOp(visionBlockingSegments).union();
233-
// Replace the original geometry with the well-defined geometry.
234-
visionBlockingSegments = new ArrayList<>();
235-
LineStringExtracter.getLines(allWallGeometry, visionBlockingSegments);
236-
237-
/*
238-
* The algorithm requires walls in every direction. The easiest way to accomplish this is to add
239-
* the boundary of the bounding box.
240-
*/
241-
final var envelope = allWallGeometry.getEnvelopeInternal();
242-
envelope.expandToInclude(visionGeometry.getGeometry().getEnvelopeInternal());
243-
// Exact expansion distance doesn't matter, we just don't want the boundary walls to overlap
244-
// endpoints from real walls.
245-
envelope.expandBy(1.0);
246-
// Because we definitely have geometry, the envelope will always be a non-trivial rectangle.
247-
visionBlockingSegments.add(((Polygon) geometryFactory.toGeometry(envelope)).getExteriorRing());
248-
249-
// Now that we have valid geometry and a bounding box, we can continue with the sweep.
250-
251-
final var endpoints = getSweepEndpoints(origin, visionBlockingSegments);
252-
Set<LineSegment> openWalls = Collections.newSetFromMap(new IdentityHashMap<>());
253-
254-
// This initial sweep just makes sure we have the correct open set to start.
255-
for (final var endpoint : endpoints) {
256-
openWalls.addAll(endpoint.getStartsWalls());
257-
openWalls.removeAll(endpoint.getEndsWalls());
258-
}
259-
260-
// Now for the real sweep. Make sure to process the first point once more at the end to ensure
261-
// the sweep covers the full 360 degrees.
262-
endpoints.add(endpoints.get(0));
263-
List<Coordinate> visionPoints = new ArrayList<>();
264-
for (final var endpoint : endpoints) {
265-
assert !openWalls.isEmpty();
266-
267-
final var ray = new LineSegment(origin, endpoint.getPoint());
268-
final var nearestWallResult = findNearestOpenWall(openWalls, ray);
269-
270-
openWalls.addAll(endpoint.getStartsWalls());
271-
openWalls.removeAll(endpoint.getEndsWalls());
272-
273-
// Find a new nearest wall.
274-
final var newNearestWallResult = findNearestOpenWall(openWalls, ray);
275-
276-
if (newNearestWallResult.wall != nearestWallResult.wall) {
277-
// Implies we have changed which wall we are at. Need to figure out projections.
278-
279-
if (openWalls.contains(nearestWallResult.wall())) {
280-
// The previous nearest wall is still open. I.e., we didn't fall of its end but
281-
// encountered a new closer wall. So we project the current point to the previous
282-
// nearest wall, then step to the current point.
283-
visionPoints.add(nearestWallResult.point());
284-
visionPoints.add(endpoint.getPoint());
285-
} else {
286-
// The previous nearest wall is now closed. I.e., we "fell off" it and therefore have
287-
// encountered a different wall. So we step from the current point (which is on the
288-
// previous wall) to the projection on the new wall.
289-
visionPoints.add(endpoint.getPoint());
290-
// Special case: if the two walls are adjacent, they share the current point. We don't
291-
// need to add the point twice, so just skip in that case.
292-
if (!endpoint.getStartsWalls().contains(newNearestWallResult.wall())) {
293-
visionPoints.add(newNearestWallResult.point());
294-
}
295-
}
296-
}
297-
}
298-
if (visionPoints.size() < 3) {
299-
// This shouldn't happen, but just in case.
300-
log.warn("Sweep produced too few points: {}", visionPoints);
301-
return null;
302-
}
303-
visionPoints.add(visionPoints.get(0)); // Ensure a closed loop.
304-
305-
return geometryFactory.createPolygon(visionPoints.toArray(Coordinate[]::new));
306-
}
307-
308130
/**
309131
* Expose visible area and previous path of all tokens in the token set. Server and clients are
310132
* updated.

src/main/java/net/rptools/maptool/client/ui/zone/vbl/AreaContainer.java

Lines changed: 0 additions & 58 deletions
This file was deleted.

0 commit comments

Comments
 (0)