This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit dbb51dd2fda67940aae241e0019cae11595b94ef Author: jsorel <[email protected]> AuthorDate: Fri Oct 23 08:58:50 2026 +0200 feat(Geometry): add flag to control separation of walls and roof in terrain extruder --- .../operation/extrude/TerrainExtruder.java | 216 +++++++++++++++++---- .../operation/spatialanalysis2d/Centerline.java | 4 +- .../operation/extrude/TerrainExtruderTest.java | 90 ++++++++- 3 files changed, 265 insertions(+), 45 deletions(-) diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/extrude/TerrainExtruder.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/extrude/TerrainExtruder.java index a84936f1e3..1291054e4c 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/extrude/TerrainExtruder.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/extrude/TerrainExtruder.java @@ -18,6 +18,7 @@ package org.apache.sis.geometries.operation.extrude; import java.util.ArrayList; import java.util.Arrays; +import java.util.BitSet; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; @@ -36,6 +37,7 @@ import org.apache.sis.geometries.internal.shared.Planar.IntList; import org.apache.sis.geometries.internal.shared.Planar.XY; import org.apache.sis.geometries.mesh.MeshPrimitive; import org.apache.sis.geometries.mesh.MeshPrimitiveIndexes; +import org.apache.sis.geometries.mesh.MultiMeshPrimitive; import org.apache.sis.geometries.operation.Intersection; import org.apache.sis.geometries.operation.OperationException; import org.apache.sis.geometries.operation.ProcessorUtils; @@ -53,14 +55,17 @@ import org.apache.sis.util.ArgumentChecks; * Embeds flat areas, such as building footprints or roads, in a terrain mesh. * * <p>The terrain is a height field: a triangle mesh in which each (x,y) location has a single elevation. - * Each {@linkplain #appendFlatConstraint(Polygon, double, double, boolean, boolean) appended polygon} cuts - * the terrain along its rings and replaces the part inside by a flat top:</p> + * Each {@linkplain #appendFlatConstraint(Polygon, double, double, HeightReference, boolean) appended polygon} + * cuts the terrain along its rings and replaces the part inside by a flat top, depending on the + * {@linkplain HeightReference height reference}:</p> * <ul> * <li>with an absolute height, the top is horizontal at the given elevation;</li> * <li>relative to the ground, the top has no sideways tilt but follows the terrain along the * {@linkplain Centerline centerline} of the polygon, like a road climbing a mountain. * Each point of the top is at the elevation of the ground at its nearest point on the centerline, - * plus the given height.</li> + * plus the given height;</li> + * <li>at the centroid, the top is horizontal at the elevation of the ground at the polygon centroid, + * plus the given height, like a building on a slope.</li> * </ul> * <p>The top is joined to the terrain either by vertical walls, or by moving the terrain vertices * on the polygon rings to the elevation of the top, in which case the neighboring terrain triangles slope @@ -82,6 +87,28 @@ import org.apache.sis.util.ArgumentChecks; */ public final class TerrainExtruder { + /** + * How the height of an appended polygon is measured. + */ + public enum HeightReference { + /** + * The height is an elevation, the top is horizontal. + */ + ABSOLUTE, + + /** + * The height is above the ground along the polygon centerline. The top has no sideways tilt + * but follows the terrain, like a road climbing a mountain. + */ + RELATIVE_TO_GROUND, + + /** + * The height is above the ground at the polygon centroid, the top is horizontal. + * If the centroid is outside the terrain, the mean ground elevation on the polygon vertices is used. + */ + POINT_AT_CENTROID + } + /** * The axes of the (x,y) plane. */ @@ -112,6 +139,17 @@ public final class TerrainExtruder { private int[] triangles; private int nbTriangle; + /** + * Identifiers of the surface triangles which are tops or roofs of the appended areas. + * Pieces of a cut triangle keep its kind. + */ + private final BitSet roofs = new BitSet(); + + /** + * Whether the terrain, the roofs and the walls are returned in separated mesh primitives. + */ + private boolean separateParts; + /** * Index of the surface triangles by their (x,y) envelope. */ @@ -146,7 +184,7 @@ public final class TerrainExtruder { double length = 0; for (int i = 0; i + 2 < indices.length; i += 3) { final int a = indices[i], b = indices[i+1], c = indices[i+2]; - addTriangle(a, b, c); + addTriangle(a, b, c, false); length += distance2D(a, b) + distance2D(b, c) + distance2D(c, a); } densification = (length > 0) ? length / (indices.length / 3 * 3) : 1; @@ -174,6 +212,26 @@ public final class TerrainExtruder { this.densification = densification; } + /** + * Returns whether the terrain, the roofs and the walls are returned in separated mesh primitives. + * + * @return whether the result parts are separated, {@code false} by default + */ + public boolean getSeparateParts() { + return separateParts; + } + + /** + * Sets whether the terrain, the roofs and the walls are returned in separated mesh primitives. + * Separated primitives do not share vertices, so normals computed on each primitive are not averaged + * across the sharp edges between the terrain, the walls and the roofs. + * + * @param separateParts whether the result parts are separated + */ + public void setSeparateParts(final boolean separateParts) { + this.separateParts = separateParts; + } + /** * Appends a flat area, possibly covered by a hipped roof. The terrain is cut along the polygon rings * and the part inside the polygon is replaced by the top. Tops of previously appended polygons @@ -184,42 +242,51 @@ public final class TerrainExtruder { * with a roof, this is the elevation of the eaves * @param roofHeight elevation of the highest roof ridge, in the same way than {@code height}. * If equal to {@code height} the top is flat, otherwise it is a hipped roof - * @param relativeToGround whether the top follows the ground along the polygon centerline + * @param reference how the heights are measured * @param walls {@code true} to join the top to the terrain with vertical walls, * {@code false} to move the terrain vertices on the rings to the top elevation * @throws OperationException if the polygon CRS differs from the terrain one, or a triangulation * or the straight skeleton computation failed */ public void appendFlatConstraint(final Polygon polygon, final double height, final double roofHeight, - final boolean relativeToGround, final boolean walls) throws OperationException + final HeightReference reference, final boolean walls) throws OperationException { ArgumentChecks.ensureNonNull("polygon", polygon); + ArgumentChecks.ensureNonNull("reference", reference); ProcessorUtils.ensureSameCRS2D(terrain, polygon); final List<double[]> rings = Planar.rings(polygon); if (!rings.isEmpty()) { - new Append(polygon, rings, height, roofHeight, relativeToGround, walls).run(); + new Append(polygon, rings, height, roofHeight, reference, walls).run(); } } /** * Returns the terrain with the appended flat areas. + * By default the result contains a single mesh primitive. If {@linkplain #setSeparateParts(boolean) + * parts are separated}, the result contains the terrain, the roofs and the walls in that order, + * without the empty ones, each with its own vertices. * - * @return a new mesh, with positions only + * @return new meshes, with positions only */ - public MeshPrimitive.Triangles getResult() { - final IntList faces = new IntList(); - for (int i = 0; i < nbTriangle * 3; i += 3) { - if (triangles[i] >= 0) faces.add(triangles[i], triangles[i+1], triangles[i+2]); + public MultiMeshPrimitive<MeshPrimitive.Triangles> getResult() { + final IntList terrainFaces = new IntList(), roofFaces = new IntList(), wallFaces = new IntList(); + for (int i = 0; i < nbTriangle; i++) { + if (triangles[i*3] >= 0) { + (roofs.get(i) ? roofFaces : terrainFaces).add(triangles[i*3], triangles[i*3+1], triangles[i*3+2]); + } } /* * A surface edge without neighbor is on the terrain border or on a cut made by a flat area. * On a cut, another edge without neighbor goes in the reverse direction at the same location * on the other side. Those two edges are joined by a vertical wall, with the same orientation. */ - final long[] edges = new long[faces.size]; - for (int i = 0; i < faces.size; i += 3) { - for (int k = 0; k < 3; k++) { - edges[i+k] = ((long) faces.values[i+k] << 32) | faces.values[i + (k+1) % 3]; + final long[] edges = new long[terrainFaces.size + roofFaces.size]; + int n = 0; + for (IntList faces : List.of(terrainFaces, roofFaces)) { + for (int i = 0; i < faces.size; i += 3) { + for (int k = 0; k < 3; k++) { + edges[n++] = ((long) faces.values[i+k] << 32) | faces.values[i + (k+1) % 3]; + } } } Arrays.sort(edges); @@ -229,13 +296,33 @@ public final class TerrainExtruder { if (Arrays.binarySearch(edges, ((long) b << 32) | a) < 0) { final int[] other = open.remove(List.of(key(b), key(a))); if (other != null) { - faces.add(b, a, other[1], b, other[1], other[0]); + wallFaces.add(b, a, other[1], b, other[1], other[0]); } else { open.put(List.of(key(a), key(b)), new int[] {a, b}); } } } - //keep the triangles not degenerated in 3D, and only their vertices + final MultiMeshPrimitive<MeshPrimitive.Triangles> result = new MultiMeshPrimitive<>(terrain.getCoordinateReferenceSystem()); + if (separateParts) { + for (IntList faces : List.of(terrainFaces, roofFaces, wallFaces)) { + final MeshPrimitive.Triangles part = toPrimitive(faces); + if (part != null) result.append(List.of(part)); + } + } else { + terrainFaces.add(roofFaces.toArray()); + terrainFaces.add(wallFaces.toArray()); + final MeshPrimitive.Triangles part = toPrimitive(terrainFaces); + if (part != null) result.append(List.of(part)); + } + return result; + } + + /** + * Creates a mesh primitive with the given triangles not degenerated in 3D, and only their vertices. + * + * @return the primitive, or null if there is no triangle + */ + private MeshPrimitive.Triangles toPrimitive(final IntList faces) { final int[] remap = new int[nbVertex]; Arrays.fill(remap, -1); final DoubleList positions = new DoubleList(); @@ -254,10 +341,13 @@ public final class TerrainExtruder { indices.add(remap[v]); } } - final MeshPrimitive.Triangles result = new MeshPrimitive.Triangles(); - result.setPositions(NDArrays.of(terrain.getCoordinateReferenceSystem(), positions.toArray())); - result.setIndex(NDArrays.ofUnsigned(1, indices.toArray())); - return result; + if (indices.size == 0) { + return null; + } + final MeshPrimitive.Triangles primitive = new MeshPrimitive.Triangles(); + primitive.setPositions(NDArrays.of(terrain.getCoordinateReferenceSystem(), positions.toArray())); + primitive.setIndex(NDArrays.ofUnsigned(1, indices.toArray())); + return primitive; } // --------------------------------------------------------------------------------------------- @@ -276,8 +366,10 @@ public final class TerrainExtruder { /** * Adds a surface triangle, made counterclockwise. Flat triangles are ignored. + * + * @param roof whether the triangle is part of a top or a roof */ - private void addTriangle(final int a, int b, int c) { + private void addTriangle(final int a, int b, int c, final boolean roof) { final double side = side(a, b, xyz[c*3], xyz[c*3+1]); if (side == 0) { return; @@ -292,6 +384,7 @@ public final class TerrainExtruder { triangles[id*3] = a; triangles[id*3+1] = b; triangles[id*3+2] = c; + roofs.set(id, roof); index.insert(envelope(id), id); } @@ -424,6 +517,7 @@ public final class TerrainExtruder { private final Polygon polygon; private final List<double[]> rings; private final double height, roofHeight; + private final HeightReference reference; private final boolean relative, withWalls; private final Envelope extent = new Envelope(); @@ -444,17 +538,18 @@ public final class TerrainExtruder { /** Centerline sampled on the ground, (x,y,z) for each polyline, for relative tops. */ private final List<double[]> profiles = new ArrayList<>(); - /** Elevation of the ground used when there is no centerline. */ - private double fallbackGround; + /** Elevation of the ground below a top at the centroid, or used for relative tops without centerline. */ + private double ground; Append(final Polygon polygon, final List<double[]> rings, final double height, final double roofHeight, - final boolean relative, final boolean withWalls) + final HeightReference reference, final boolean withWalls) { this.polygon = polygon; this.rings = rings; this.height = height; this.roofHeight = roofHeight; - this.relative = relative; + this.reference = reference; + this.relative = (reference == HeightReference.RELATIVE_TO_GROUND); this.withWalls = withWalls; final double[] exterior = rings.get(0); for (int i = 0; i < exterior.length; i += 2) { @@ -463,9 +558,16 @@ public final class TerrainExtruder { } void run() throws OperationException { - if (relative) { - //sampled before the surface is modified - buildProfiles(); + //ground sampled before the surface is modified + switch (reference) { + case RELATIVE_TO_GROUND: buildProfiles(); break; + case POINT_AT_CENTROID: { + final double[] c = centroid(); + ground = groundZ(c[0], c[1]); + if (Double.isNaN(ground)) ground = ringGround(); + break; + } + default: break; } buildChain(); cutSurface(); @@ -509,6 +611,13 @@ public final class TerrainExtruder { } } //used if there is no centerline or the centerline is outside the terrain + ground = ringGround(); + } + + /** + * Returns the mean elevation of the ground on the ring vertices inside the terrain, or 0 if none. + */ + private double ringGround() { double sum = 0; int count = 0; for (double[] ring : rings) { @@ -520,7 +629,30 @@ public final class TerrainExtruder { } } } - fallbackGround = (count > 0) ? sum / count : 0; + return (count > 0) ? sum / count : 0; + } + + /** + * Returns the area centroid of the polygon, holes excluded, or the center of its extent if the area is zero. + * Coordinates are relative to the first vertex for accuracy. + */ + private double[] centroid() { + final double x0 = rings.get(0)[0], y0 = rings.get(0)[1]; + double area = 0, cx = 0, cy = 0; + for (double[] ring : rings) { + for (int i = 0; i < ring.length; i += 2) { + final double xa = ring[i] - x0, ya = ring[i+1] - y0; + final double xb = ring[(i+2) % ring.length] - x0, yb = ring[(i+3) % ring.length] - y0; + final double cross = xa*yb - xb*ya; + area += cross; + cx += (xa + xb) * cross; + cy += (ya + yb) * cross; + } + } + if (area == 0) { + return new double[] {extent.centre().x, extent.centre().y}; + } + return new double[] {x0 + cx / (3 * area), y0 + cy / (3 * area)}; } /** @@ -545,10 +677,10 @@ public final class TerrainExtruder { */ private double topZ(final double x, final double y) { if (!relative) { - return height; + return ground + height; } double best = Double.POSITIVE_INFINITY; - double z = fallbackGround; + double z = ground; for (double[] p : profiles) { final int n = p.length / 3; for (int i = 0; i < Math.max(1, n - 1); i++) { @@ -730,15 +862,18 @@ public final class TerrainExtruder { addLocal(inside.values[i], vertices, local); } - //ring segments inside the triangle + //ring segments in the triangle, as constraints if inside it, all of them as boundary + final IntList boundary = new IntList(); for (int i = 0, n = vertices.size; i < n; i++) { final IntList points = chainAt.get(key(vertices.values[i])); for (int j = 0; points != null && j < points.size; j++) { final int k = points.values[j]; final Integer other = local.get(location(next[k])); + if (other == null) continue; + boundary.add(i, other); final double mx = (chain[k*2] + chain[next[k]*2]) / 2; final double my = (chain[k*2+1] + chain[next[k]*2+1]) / 2; - if (other != null && side(a, b, mx, my) > 0 && side(b, c, mx, my) > 0 && side(c, a, mx, my) > 0) { + if (side(a, b, mx, my) > 0 && side(b, c, mx, my) > 0 && side(c, a, mx, my) > 0) { constraints.add(i, other); } } @@ -749,10 +884,12 @@ public final class TerrainExtruder { coords[i*2] = xyz[vertices.values[i]*3]; coords[i*2+1] = xyz[vertices.values[i]*3+1]; } - final int[] parts = Planar.keep(coords, Delaunay.triangulate(coords, constraints.toArray()), rings, false); + final int[] parts = Planar.keep(coords, Delaunay.triangulate(coords, constraints.toArray()), + boundary.toArray(), rings, false); removeTriangle(id); + final boolean roof = roofs.get(id); for (int i = 0; i < parts.length; i += 3) { - addTriangle(vertices.values[parts[i]], vertices.values[parts[i+1]], vertices.values[parts[i+2]]); + addTriangle(vertices.values[parts[i]], vertices.values[parts[i+1]], vertices.values[parts[i+2]], roof); } } @@ -779,6 +916,7 @@ public final class TerrainExtruder { for (int k = 0; k < next.length; k++) { constraints.add(addTop(location(k), 0, local, points), addTop(location(next[k]), 0, local, points)); } + final int[] boundary = constraints.toArray(); double roofScale = 0; if (roofHeight != height) { final StraightSkeleton skeleton = new StraightSkeleton(polygon); @@ -822,9 +960,9 @@ public final class TerrainExtruder { for (int l = 0; l < nbLocal; l++) { if (topVertex[l] < 0) topVertex[l] = addVertex(xy[l*2], xy[l*2+1], z[l]); } - final int[] tops = Planar.keep(xy, Delaunay.triangulate(xy, constraints.toArray()), rings, true); + final int[] tops = Planar.keep(xy, Delaunay.triangulate(xy, constraints.toArray()), boundary, rings, true); for (int i = 0; i < tops.length; i += 3) { - addTriangle(topVertex[tops[i]], topVertex[tops[i+1]], topVertex[tops[i+2]]); + addTriangle(topVertex[tops[i]], topVertex[tops[i+1]], topVertex[tops[i+2]], true); } } diff --git a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/spatialanalysis2d/Centerline.java b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/spatialanalysis2d/Centerline.java index 4e238e3681..8b438be9eb 100644 --- a/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/spatialanalysis2d/Centerline.java +++ b/incubator/src/org.apache.sis.geometry/main/org/apache/sis/geometries/operation/spatialanalysis2d/Centerline.java @@ -145,7 +145,9 @@ public final class Centerline { } } final double[] xy = points.toArray(); - final int[] triangles = Planar.keep(xy, Delaunay.triangulate(xy, segments.toArray()), rings, true); + //segments follow the rings, which have the polygon interior on their left + final int[] boundary = segments.toArray(); + final int[] triangles = Planar.keep(xy, Delaunay.triangulate(xy, boundary), boundary, rings, true); //chords are the edges shared by two triangles, edges on the rings have a single triangle inside final Map<Long,Integer> edgeCount = new HashMap<>(); diff --git a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/extrude/TerrainExtruderTest.java b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/extrude/TerrainExtruderTest.java index abaa39b025..e6dfc22cc0 100644 --- a/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/extrude/TerrainExtruderTest.java +++ b/incubator/src/org.apache.sis.geometry/test/org/apache/sis/geometries/operation/extrude/TerrainExtruderTest.java @@ -28,6 +28,8 @@ import org.apache.sis.geometries.Geometries; import org.apache.sis.geometries.GeometryFactory; import org.apache.sis.geometries.curve.LinearRing; import org.apache.sis.geometries.mesh.MeshPrimitive; +import org.apache.sis.geometries.mesh.MultiMeshPrimitive; +import org.apache.sis.geometries.operation.extrude.TerrainExtruder.HeightReference; import org.apache.sis.geometries.surface.Polygon; import org.apache.sis.maths.Maths; import org.apache.sis.maths.NDArrays; @@ -78,13 +80,65 @@ public class TerrainExtruderTest { mesh.assertWatertight(); } + /** + * Rotated footprints on a grid terrain, whose edges cross many terrain edges at interpolated points + * slightly off the footprint lines. The walls shall have no hole. + */ + @Test + public void testWallsOnGrid() { + final int n = 10; + final double[] xyz = new double[(n + 1) * (n + 1) * 3]; + for (int i = 0, k = 0; i <= n; i++) { + for (int j = 0; j <= n; j++) { + xyz[k++] = i; xyz[k++] = j; xyz[k++] = i; + } + } + final int[] index = new int[n * n * 6]; + for (int i = 0, k = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + final int a = i * (n + 1) + j, b = a + 1, d = a + n + 1, e = d + 1; + index[k++] = a; index[k++] = d; index[k++] = e; + index[k++] = a; index[k++] = e; index[k++] = b; + } + } + final MeshPrimitive.Triangles grid = new MeshPrimitive.Triangles(); + grid.setPositions(NDArrays.of(CRS3D, xyz)); + grid.setIndex(NDArrays.ofUnsigned(1, index)); + final TerrainExtruder extruder = new TerrainExtruder(grid); + extruder.appendFlatConstraint(polygon(new double[] {1.3,2.1, 6.7,1.2, 7.9,5.3, 2.4,6.6}), 15, 15, HeightReference.ABSOLUTE, true); + extruder.appendFlatConstraint(polygon(new double[] {6.1,6.2, 8.8,7.1, 8.1,9.3, 5.7,8.4}), 15, 18, HeightReference.ABSOLUTE, true); + new Mesh(extruder.getResult()).assertWatertight(); + } + + /** + * Terrain, roof and walls in separated primitives, without shared vertices. + */ + @Test + public void testSeparateParts() { + final TerrainExtruder extruder = new TerrainExtruder(terrain()); + extruder.setSeparateParts(true); + extruder.appendFlatConstraint(polygon(FOOTPRINT), 20, 23, HeightReference.ABSOLUTE, true); + final MultiMeshPrimitive<MeshPrimitive.Triangles> result = extruder.getResult(); + assertEquals(3, result.getNumGeometries()); + final Mesh ground = new Mesh(result.getGeometryN(0)); + final Mesh roof = new Mesh(result.getGeometryN(1)); + final Mesh walls = new Mesh(result.getGeometryN(2)); + assertEquals(0, ground.countVertical(), "No wall expected in the terrain."); + assertEquals(0, roof.countVertical(), "No wall expected in the roof."); + assertEquals(walls.index.length / 3, walls.countVertical(), "Only walls expected."); + ground.assertElevations((x, y) -> x); + for (int i = 2; i < roof.positions.length; i += 3) { + assertTrue(roof.positions[i] >= 20, "Roof vertex below the eaves."); + } + } + /** * Absolute top joined to the terrain by moving the terrain vertices on the rings. */ @Test public void testAbsoluteFlattened() { final TerrainExtruder extruder = new TerrainExtruder(terrain()); - extruder.appendFlatConstraint(polygon(FOOTPRINT), 20, 20, false, false); + extruder.appendFlatConstraint(polygon(FOOTPRINT), 20, 20, HeightReference.ABSOLUTE, false); final Mesh mesh = new Mesh(extruder.getResult()); assertEquals(0, mesh.countVertical(), "No wall expected."); final Set<List<Double>> xy = new HashSet<>(); @@ -104,7 +158,7 @@ public class TerrainExtruderTest { public void testRelative() { final TerrainExtruder extruder = new TerrainExtruder(terrain()); extruder.setDensification(0.5); - extruder.appendFlatConstraint(polygon(new double[] {1,4, 9,4, 9,6, 1,6}), 2, 2, true, true); + extruder.appendFlatConstraint(polygon(new double[] {1,4, 9,4, 9,6, 1,6}), 2, 2, HeightReference.RELATIVE_TO_GROUND, true); final Mesh mesh = new Mesh(extruder.getResult()); //top vertices on the long sides, by x final Map<Double,Double> south = new HashMap<>(), north = new HashMap<>(); @@ -122,6 +176,23 @@ public class TerrainExtruderTest { mesh.assertWatertight(); } + /** + * Top relative to the ground at the centroid (3.5,4.5) where the ground is at 3.5: + * horizontal at 5.5, with walls down to the ground on all sides. + */ + @Test + public void testPointAtCentroid() { + final TerrainExtruder extruder = new TerrainExtruder(terrain()); + extruder.appendFlatConstraint(polygon(FOOTPRINT), 2, 2, HeightReference.POINT_AT_CENTROID, true); + final Mesh mesh = new Mesh(extruder.getResult()); + for (double[] p : new double[][] {{2,3},{5,3},{5,6},{2,6},{3,3},{5,5}}) { + assertTrue(mesh.hasVertex(p[0], p[1], 5.5), "Missing top vertex at " + p[0] + " " + p[1]); + assertTrue(mesh.hasVertex(p[0], p[1], p[0]), "Missing ground vertex at " + p[0] + " " + p[1]); + } + mesh.assertElevations((x, y) -> inside(FOOTPRINT, x, y) ? 5.5 : x); + mesh.assertWatertight(); + } + /** * Two overlapping footprints, the last appended one wins. * Not watertight: where the second rings cross a wall of the first footprint, the new walls on each @@ -132,8 +203,8 @@ public class TerrainExtruderTest { final double[] first = {2,2, 6,2, 6,6, 2,6}; final double[] second = {4,4, 8,4, 8,8, 4,8}; final TerrainExtruder extruder = new TerrainExtruder(terrain()); - extruder.appendFlatConstraint(polygon(first), 15, 15, false, true); - extruder.appendFlatConstraint(polygon(second), 25, 25, false, true); + extruder.appendFlatConstraint(polygon(first), 15, 15, HeightReference.ABSOLUTE, true); + extruder.appendFlatConstraint(polygon(second), 25, 25, HeightReference.ABSOLUTE, true); final Mesh mesh = new Mesh(extruder.getResult()); mesh.assertElevations((x, y) -> inside(second, x, y) ? 25 : inside(first, x, y) ? 15 : x); for (int t = 0; t < mesh.index.length / 3; t++) { @@ -185,7 +256,7 @@ public class TerrainExtruderTest { */ private static Mesh extrude(final double height, final double roofHeight, final Polygon polygon) { final TerrainExtruder extruder = new TerrainExtruder(terrain()); - extruder.appendFlatConstraint(polygon, height, roofHeight, false, true); + extruder.appendFlatConstraint(polygon, height, roofHeight, HeightReference.ABSOLUTE, true); return new Mesh(extruder.getResult()); } @@ -251,11 +322,20 @@ public class TerrainExtruderTest { final double[] positions; final int[] index; + Mesh(final MultiMeshPrimitive<MeshPrimitive.Triangles> result) { + this(single(result)); + } + Mesh(final MeshPrimitive.Triangles mesh) { positions = mesh.getPositions().toArrayDouble(); index = mesh.getIndex().toArrayInt(); } + private static MeshPrimitive.Triangles single(final MultiMeshPrimitive<MeshPrimitive.Triangles> result) { + assertEquals(1, result.getNumGeometries(), "Parts are not separated by default."); + return result.getGeometryN(0); + } + double[] vertex(final int triangle, final int k) { final int v = index[triangle*3 + k]; return new double[] {positions[v*3], positions[v*3+1], positions[v*3+2]};
