Commit 7eb83cde authored by Matthias Betz's avatar Matthias Betz
Browse files

fixes

parent 0a760af4
Pipeline #12384 failed with stage
in 1 minute and 35 seconds
package de.hft.stuttgart.citydoctor2.datastructure.bht;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.ArrayList;
import java.util.function.Function;
import java.util.Objects;
import java.util.function.Function;
import de.hft.stuttgart.citydoctor2.datastructure.ConcretePolygon;
/**
* Generic Bounding Volume Hierarchy (BVH) for AABBs.
*
* Supports:
* - binary BVH (degree 2)
* - octonary / 8-ary BVH (degree 8)
* Supports: - binary BVH (degree 2) - octonary / 8-ary BVH (degree 8)
*
* Split strategies:
* - binary: object median, object mean, spatial median
* - octonary: object median, object mean, spatial median
* Split strategies: - binary: object median, object mean, spatial median -
* octonary: object median, object mean, spatial median
*
* Notes:
* - This implementation caches AABBs and centers during build time via BuildItem.
* - The input list is NOT reordered.
* - The octonary variant is implemented as an 8-ary BVH with tight child AABBs
* built from bucket contents, not as a strict spatial octree with fixed cell boxes.
* Notes: - This implementation caches AABBs and centers during build time via
* BuildItem. - The input list is NOT reordered. - The octonary variant is
* implemented as an 8-ary BVH with tight child AABBs built from bucket
* contents, not as a strict spatial octree with fixed cell boxes.
*
* @param <E> element type
*/
public class BoundingVolumeHierarchyTree<E> {
private Node<E> root;
private final Function<E, AABB> aabbFunction;
private static final int DEFAULT_MIN_DEPTH = 4;
private static final int DEFAULT_MAX_DEPTH = 32;
private static final int DEFAULT_BINARY_LEAF_SIZE = 1;
private static final int DEFAULT_OCTONARY_LEAF_SIZE = 8;
private static final double DEFAULT_DEGENERATE_TOL = 1e-12;
public enum SplitStrategy {
AUTO,
BINARY_OBJECT_MEDIAN,
BINARY_OBJECT_MEAN,
BINARY_SPATIAL_MEDIAN,
OCTONARY_OBJECT_MEDIAN,
OCTONARY_OBJECT_MEAN,
OCTONARY_SPATIAL_MEDIAN
}
public static final class BuildConfig {
private final int degree;
private final SplitStrategy splitStrategy;
private final int maxLeafSize;
private final int maxDepth;
private final double degenerateTolerance;
private BuildConfig(Builder b) {
this.degree = b.degree;
this.splitStrategy = Objects.requireNonNull(b.splitStrategy, "splitStrategy");
this.maxLeafSize = b.maxLeafSize;
this.maxDepth = b.maxDepth;
this.degenerateTolerance = b.degenerateTolerance;
}
public int getDegree() {
return degree;
}
public SplitStrategy getSplitStrategy() {
return splitStrategy;
}
public int getMaxLeafSize() {
return maxLeafSize;
}
public int getMaxDepth() {
return maxDepth;
}
public double getDegenerateTolerance() {
return degenerateTolerance;
}
public static Builder builder() {
return new Builder();
}
public static BuildConfig binaryDefault() {
return builder()
.degree(2)
.splitStrategy(SplitStrategy.AUTO)
.maxLeafSize(DEFAULT_BINARY_LEAF_SIZE)
.maxDepth(DEFAULT_MAX_DEPTH)
.degenerateTolerance(DEFAULT_DEGENERATE_TOL)
.build();
}
public static BuildConfig octonaryDefault() {
return builder()
.degree(8)
.splitStrategy(SplitStrategy.AUTO)
.maxLeafSize(DEFAULT_OCTONARY_LEAF_SIZE)
.maxDepth(DEFAULT_MAX_DEPTH)
.degenerateTolerance(DEFAULT_DEGENERATE_TOL)
.build();
}
public static final class Builder {
private int degree = 2;
private SplitStrategy splitStrategy = SplitStrategy.AUTO;
private int maxLeafSize = DEFAULT_BINARY_LEAF_SIZE;
private int maxDepth = DEFAULT_MAX_DEPTH;
private double degenerateTolerance = DEFAULT_DEGENERATE_TOL;
public Builder degree(int degree) {
this.degree = degree;
return this;
}
public Builder splitStrategy(SplitStrategy splitStrategy) {
this.splitStrategy = splitStrategy;
return this;
}
public Builder maxLeafSize(int maxLeafSize) {
this.maxLeafSize = maxLeafSize;
return this;
}
public Builder maxDepth(int maxDepth) {
this.maxDepth = maxDepth;
return this;
}
public Builder degenerateTolerance(double degenerateTolerance) {
this.degenerateTolerance = degenerateTolerance;
return this;
}
public BuildConfig build() {
if (degree != 2 && degree != 8) {
throw new IllegalArgumentException("Only degree 2 and 8 are supported.");
}
if (maxLeafSize < 1) {
throw new IllegalArgumentException("maxLeafSize must be >= 1.");
}
if (maxDepth < 1) {
throw new IllegalArgumentException("maxDepth must be >= 1.");
}
if (degenerateTolerance < 0.0) {
throw new IllegalArgumentException("degenerateTolerance must be >= 0.");
}
return new BuildConfig(this);
}
}
}
/**
* Internal build-time cache.
*/
private static final class BuildItem<E> {
final E element;
final AABB aabb;
final double centerX;
final double centerY;
final double centerZ;
BuildItem(E element, AABB aabb) {
this.element = element;
this.aabb = aabb;
this.centerX = 0.5 * (aabb.getMinX() + aabb.getMaxX());
this.centerY = 0.5 * (aabb.getMinY() + aabb.getMaxY());
this.centerZ = 0.5 * (aabb.getMinZ() + aabb.getMaxZ());
}
double center(int axis) {
switch (axis) {
case 0: return centerX;
case 1: return centerY;
case 2: return centerZ;
default: throw new IllegalArgumentException("axis must be 0, 1, or 2");
}
}
}
/**
* Creates an empty BVH.
*/
public BoundingVolumeHierarchyTree(Function<E, AABB> aabbFunction) {
this.root = null;
this.aabbFunction = Objects.requireNonNull(aabbFunction, "aabbFunction");
}
/**
* Creates a binary BVH with default config (AUTO -> binary spatial median).
*/
public BoundingVolumeHierarchyTree(List<E> elements, Function<E, AABB> aabbFunction) {
this(elements, aabbFunction, BuildConfig.binaryDefault());
}
/**
* Creates a BVH with a full config.
*/
public BoundingVolumeHierarchyTree(List<E> elements, Function<E, AABB> aabbFunction, BuildConfig config) {
this.aabbFunction = Objects.requireNonNull(aabbFunction, "aabbFunction");
Objects.requireNonNull(config, "config");
if (elements == null || elements.isEmpty()) {
this.root = null;
return;
}
List<BuildItem<E>> items = toBuildItems(elements);
SplitStrategy resolvedStrategy = resolveSplitStrategy(config);
if (config.getDegree() == 2) {
this.root = buildBinaryRecursive(items, 0, resolvedStrategy, config);
} else {
this.root = buildOctonaryRecursive(items, 0, resolvedStrategy, config);
}
}
/**
* Convenience factory: binary BVH with AUTO/defaults.
*/
public static <E> BoundingVolumeHierarchyTree<E> newBinary(
List<E> elements,
Function<E, AABB> aabbFunction) {
return new BoundingVolumeHierarchyTree<>(elements, aabbFunction, BuildConfig.binaryDefault());
}
/**
* Convenience factory: octonary BVH with AUTO/defaults.
*/
public static <E> BoundingVolumeHierarchyTree<E> newOctonary(
List<E> elements,
Function<E, AABB> aabbFunction) {
return new BoundingVolumeHierarchyTree<>(elements, aabbFunction, BuildConfig.octonaryDefault());
}
/**
* Convenience factory: octonary BVH with a custom maxLeafSize.
*/
public static <E> BoundingVolumeHierarchyTree<E> newOctonary(
List<E> elements,
Function<E, AABB> aabbFunction,
int maxLeafSize) {
BuildConfig cfg = BuildConfig.builder()
.degree(8)
.splitStrategy(SplitStrategy.AUTO)
.maxLeafSize(maxLeafSize)
.maxDepth(computeDefaultMaxDepth(elements != null ? elements.size() : 0))
.degenerateTolerance(DEFAULT_DEGENERATE_TOL)
.build();
return new BoundingVolumeHierarchyTree<>(elements, aabbFunction, cfg);
}
public Node<E> getRoot() {
return root;
}
public void setRoot(Node<E> root) {
this.root = root;
}
/**
* NOTE: generic version already exists.
*/
public static AABB getAggregateAABB(List<ConcretePolygon> polygons) {
if (polygons.size() == 1) {
return AABB.of(polygons.get(0));
}
return getAggregateAABB(polygons, AABB::of);
}
public static <E> AABB getAggregateAABB(List<E> elements, Function<E, AABB> aabbFunc) {
double minX = Double.POSITIVE_INFINITY;
double minY = Double.POSITIVE_INFINITY;
double minZ = Double.POSITIVE_INFINITY;
double maxX = Double.NEGATIVE_INFINITY;
double maxY = Double.NEGATIVE_INFINITY;
double maxZ = Double.NEGATIVE_INFINITY;
for (E e : elements) {
AABB aabb = aabbFunc.apply(e);
minX = Math.min(minX, aabb.getMinX());
minY = Math.min(minY, aabb.getMinY());
minZ = Math.min(minZ, aabb.getMinZ());
maxX = Math.max(maxX, aabb.getMaxX());
maxY = Math.max(maxY, aabb.getMaxY());
maxZ = Math.max(maxZ, aabb.getMaxZ());
}
return new AABB(minX, minY, minZ, maxX, maxY, maxZ);
}
public List<E> getAllIntersectingElements(AABB query) {
List<E> result = new ArrayList<>();
getAllIntersectingElementsRecursive(root, query, result);
return result;
}
private void getAllIntersectingElementsRecursive(Node<E> node, AABB query, List<E> result) {
if (node == null) {
return;
}
if (!node.getAabb().overlaps(query)) {
return;
}
if (node.isLeaf()) {
result.add(node.getElement());
return;
}
for (Node<E> child : node.getChildren()) {
getAllIntersectingElementsRecursive(child, query, result);
}
}
// ---------------------------------------------------------------------
// Build
// ---------------------------------------------------------------------
private List<BuildItem<E>> toBuildItems(List<E> elements) {
List<BuildItem<E>> items = new ArrayList<>(elements.size());
for (E e : elements) {
AABB aabb = Objects.requireNonNull(aabbFunction.apply(e), "aabbFunction returned null");
items.add(new BuildItem<>(e, aabb));
}
return items;
}
private SplitStrategy resolveSplitStrategy(BuildConfig config) {
if (config.getSplitStrategy() != SplitStrategy.AUTO) {
validateStrategyMatchesDegree(config.getDegree(), config.getSplitStrategy());
return config.getSplitStrategy();
}
return (config.getDegree() == 2)
? SplitStrategy.BINARY_SPATIAL_MEDIAN
: SplitStrategy.OCTONARY_OBJECT_MEAN;
}
private void validateStrategyMatchesDegree(int degree, SplitStrategy strategy) {
boolean binary = strategy == SplitStrategy.BINARY_OBJECT_MEDIAN
|| strategy == SplitStrategy.BINARY_OBJECT_MEAN
|| strategy == SplitStrategy.BINARY_SPATIAL_MEDIAN;
boolean octonary = strategy == SplitStrategy.OCTONARY_OBJECT_MEDIAN
|| strategy == SplitStrategy.OCTONARY_OBJECT_MEAN
|| strategy == SplitStrategy.OCTONARY_SPATIAL_MEDIAN;
if (degree == 2 && !binary) {
throw new IllegalArgumentException("Strategy " + strategy + " does not match degree 2.");
}
if (degree == 8 && !octonary) {
throw new IllegalArgumentException("Strategy " + strategy + " does not match degree 8.");
}
}
private Node<E> buildBinaryRecursive(
List<BuildItem<E>> items,
int depth,
SplitStrategy strategy,
BuildConfig config) {
AABB totalAabb = getAggregateAABBFromItems(items);
if (shouldStop(items, depth, config, totalAabb)) {
return packLeafFromItems(items, totalAabb);
}
int axis = totalAabb.findLongestAxis();
SplitResult<E> split;
switch (strategy) {
case BINARY_OBJECT_MEDIAN:
split = splitBinaryObjectMedian(items, axis);
break;
case BINARY_OBJECT_MEAN:
split = splitBinaryObjectMean(items, axis);
break;
case BINARY_SPATIAL_MEDIAN:
split = splitBinarySpatialMedian(items, axis, totalAabb);
break;
default:
throw new IllegalStateException("Unexpected binary strategy: " + strategy);
}
if (!split.valid()) {
return packLeafFromItems(items, totalAabb);
}
Node<E> node = new Node<>(null, totalAabb);
node.getChildren().add(buildBinaryRecursive(split.left, depth + 1, strategy, config));
node.getChildren().add(buildBinaryRecursive(split.right, depth + 1, strategy, config));
return node;
}
private Node<E> buildOctonaryRecursive(
List<BuildItem<E>> items,
int depth,
SplitStrategy strategy,
BuildConfig config) {
AABB totalAabb = getAggregateAABBFromItems(items);
if (shouldStop(items, depth, config, totalAabb)) {
return packLeafFromItems(items, totalAabb);
}
OctSplit<E> split;
switch (strategy) {
case OCTONARY_OBJECT_MEDIAN:
split = splitOctonaryObjectMedian(items);
break;
case OCTONARY_OBJECT_MEAN:
split = splitOctonaryObjectMean(items);
break;
case OCTONARY_SPATIAL_MEDIAN:
split = splitOctonarySpatialMedian(items, totalAabb);
break;
default:
throw new IllegalStateException("Unexpected octonary strategy: " + strategy);
}
if (!split.valid()) {
return packLeafFromItems(items, totalAabb);
}
Node<E> node = new Node<>(null, totalAabb);
for (int i = 0; i < 8; i++) {
List<BuildItem<E>> bucket = split.buckets[i];
if (bucket.isEmpty()) {
continue;
}
node.getChildren().add(buildOctonaryRecursive(bucket, depth + 1, strategy, config));
}
return node;
}
private boolean shouldStop(List<BuildItem<E>> items, int depth, BuildConfig config, AABB totalAabb) {
return items.size() <= config.getMaxLeafSize()
|| depth >= config.getMaxDepth()
|| totalAabb.isDegenerate(config.getDegenerateTolerance());
}
private Node<E> packLeafFromItems(List<BuildItem<E>> items, AABB totalAabb) {
if (items.size() == 1) {
BuildItem<E> item = items.get(0);
return new Node<>(item.element, item.aabb);
}
Node<E> leaf = new Node<>(null, totalAabb);
for (BuildItem<E> item : items) {
leaf.getChildren().add(new Node<>(item.element, item.aabb));
}
return leaf;
}
private static <E> AABB getAggregateAABBFromItems(List<BuildItem<E>> items) {
double minX = Double.POSITIVE_INFINITY;
double minY = Double.POSITIVE_INFINITY;
double minZ = Double.POSITIVE_INFINITY;
double maxX = Double.NEGATIVE_INFINITY;
double maxY = Double.NEGATIVE_INFINITY;
double maxZ = Double.NEGATIVE_INFINITY;
for (BuildItem<E> item : items) {
AABB aabb = item.aabb;
minX = Math.min(minX, aabb.getMinX());
minY = Math.min(minY, aabb.getMinY());
minZ = Math.min(minZ, aabb.getMinZ());
maxX = Math.max(maxX, aabb.getMaxX());
maxY = Math.max(maxY, aabb.getMaxY());
maxZ = Math.max(maxZ, aabb.getMaxZ());
}
return new AABB(minX, minY, minZ, maxX, maxY, maxZ);
}
// ---------------------------------------------------------------------
// Binary splits
// ---------------------------------------------------------------------
private SplitResult<E> splitBinaryObjectMedian(List<BuildItem<E>> items, int axis) {
items.sort(Comparator.comparingDouble(it -> it.center(axis)));
int mid = items.size() / 2;
if (mid <= 0 || mid >= items.size()) {
return SplitResult.invalid();
}
return SplitResult.of(items.subList(0, mid), items.subList(mid, items.size()));
}
private SplitResult<E> splitBinaryObjectMean(List<BuildItem<E>> items, int axis) {
double sum = 0.0;
for (BuildItem<E> item : items) {
sum += item.center(axis);
}
double splitValue = sum / items.size();
List<BuildItem<E>> left = new ArrayList<>();
List<BuildItem<E>> right = new ArrayList<>();
for (BuildItem<E> item : items) {
if (item.center(axis) < splitValue) {
left.add(item);
} else {
right.add(item);
}
}
if (left.isEmpty() || right.isEmpty()) {
return splitBinaryObjectMedian(items, axis);
}
return SplitResult.of(left, right);
}
private SplitResult<E> splitBinarySpatialMedian(List<BuildItem<E>> items, int axis, AABB totalAabb) {
double splitValue;
switch (axis) {
case 0: splitValue = totalAabb.getCenterX(); break;
case 1: splitValue = totalAabb.getCenterY(); break;
case 2: splitValue = totalAabb.getCenterZ(); break;
default: throw new IllegalArgumentException("axis must be 0, 1, or 2");
}
List<BuildItem<E>> left = new ArrayList<>();
List<BuildItem<E>> right = new ArrayList<>();
for (BuildItem<E> item : items) {
if (item.center(axis) < splitValue) {
left.add(item);
} else {
right.add(item);
}
}
if (left.isEmpty() || right.isEmpty()) {
return splitBinaryObjectMean(items, axis);
}
return SplitResult.of(left, right);
}
private static final class SplitResult<E> {
final List<BuildItem<E>> left;
final List<BuildItem<E>> right;
private SplitResult(List<BuildItem<E>> left, List<BuildItem<E>> right) {
this.left = left;
this.right = right;
}
static <E> SplitResult<E> of(List<BuildItem<E>> left, List<BuildItem<E>> right) {
return new SplitResult<>(left, right);
}
static <E> SplitResult<E> invalid() {
return new SplitResult<>(null, null);
}
boolean valid() {
return left != null && right != null && !left.isEmpty() && !right.isEmpty();
}
}
// ---------------------------------------------------------------------
// Octonary / 8-ary splits
// ---------------------------------------------------------------------
private OctSplit<E> splitOctonaryObjectMean(List<BuildItem<E>> items) {
double sumX = 0.0;
double sumY = 0.0;
double sumZ = 0.0;
for (BuildItem<E> item : items) {
sumX += item.centerX;
sumY += item.centerY;
sumZ += item.centerZ;
}
double sx = sumX / items.size();
double sy = sumY / items.size();
double sz = sumZ / items.size();
return bucketizeOctonary(items, sx, sy, sz, true);
}
private OctSplit<E> splitOctonarySpatialMedian(List<BuildItem<E>> items, AABB totalAabb) {
double sx = totalAabb.getCenterX();
double sy = totalAabb.getCenterY();
double sz = totalAabb.getCenterZ();
OctSplit<E> split = bucketizeOctonary(items, sx, sy, sz, false);
if (!split.valid()) {
return splitOctonaryObjectMean(items);
}
return split;
}
private OctSplit<E> splitOctonaryObjectMedian(List<BuildItem<E>> items) {
items.sort(Comparator.comparingDouble(it -> it.centerX));
double sx = medianValue(items, 0);
items.sort(Comparator.comparingDouble(it -> it.centerY));
double sy = medianValue(items, 1);
items.sort(Comparator.comparingDouble(it -> it.centerZ));
double sz = medianValue(items, 2);
return bucketizeOctonary(items, sx, sy, sz, true);
}
private double medianValue(List<BuildItem<E>> items, int axis) {
int n = items.size();
int mid = n / 2;
if ((n & 1) == 1) {
return items.get(mid).center(axis);
}
return 0.5 * (items.get(mid - 1).center(axis) + items.get(mid).center(axis));
}
@SuppressWarnings("unchecked")
private OctSplit<E> bucketizeOctonary(List<BuildItem<E>> items, double sx, double sy, double sz, boolean fallbackToMeanOnFailure) {
List<BuildItem<E>>[] buckets = new List[8];
for (int i = 0; i < 8; i++) {
buckets[i] = new ArrayList<>();
}
int nonEmptyCount = 0;
boolean[] seen = new boolean[8];
for (BuildItem<E> item : items) {
int idx = octantIndex(item.centerX, item.centerY, item.centerZ, sx, sy, sz);
buckets[idx].add(item);
if (!seen[idx]) {
seen[idx] = true;
nonEmptyCount++;
}
}
if (nonEmptyCount <= 1) {
if (fallbackToMeanOnFailure) {
// caller may already be object mean, so just mark invalid here
return OctSplit.invalid();
}
return OctSplit.invalid();
}
return OctSplit.of(buckets);
}
private int octantIndex(double x, double y, double z, double sx, double sy, double sz) {
int idx = 0;
if (x >= sx) idx |= 1;
if (y >= sy) idx |= 2;
if (z >= sz) idx |= 4;
return idx;
}
private static final class OctSplit<E> {
final List<BuildItem<E>>[] buckets;
private OctSplit(List<BuildItem<E>>[] buckets) {
this.buckets = buckets;
}
static <E> OctSplit<E> of(List<BuildItem<E>>[] buckets) {
return new OctSplit<>(buckets);
}
@SuppressWarnings("unchecked")
static <E> OctSplit<E> invalid() {
return new OctSplit<>(null);
}
boolean valid() {
if (buckets == null) {
return false;
}
int nonEmpty = 0;
for (List<BuildItem<E>> bucket : buckets) {
if (bucket != null && !bucket.isEmpty()) {
nonEmpty++;
}
}
return nonEmpty > 1;
}
}
// ---------------------------------------------------------------------
// helpers: to be continued with more heuristics
// ---------------------------------------------------------------------
private static int computeDefaultMaxDepth(int n) {
if (n <= 1) {
return DEFAULT_MIN_DEPTH;
}
double log2n = Math.log(n) / Math.log(2.0);
int depth = (int) Math.ceil(2.0 * log2n);
return clamp(depth, DEFAULT_MIN_DEPTH, DEFAULT_MAX_DEPTH);
}
private static int clamp(int value, int min, int max) {
return Math.max(min, Math.min(max, value));
}
private Node<E> root;
private final Function<E, AABB> aabbFunction;
private static final int DEFAULT_MIN_DEPTH = 4;
private static final int DEFAULT_MAX_DEPTH = 32;
private static final int DEFAULT_BINARY_LEAF_SIZE = 1;
private static final int DEFAULT_OCTONARY_LEAF_SIZE = 8;
private static final double DEFAULT_DEGENERATE_TOL = 1e-12;
public enum SplitStrategy {
AUTO,
BINARY_OBJECT_MEDIAN, BINARY_OBJECT_MEAN, BINARY_SPATIAL_MEDIAN,
OCTONARY_OBJECT_MEDIAN, OCTONARY_OBJECT_MEAN, OCTONARY_SPATIAL_MEDIAN
}
public static final class Builder<E> {
private int degree = 2;
private SplitStrategy splitStrategy = SplitStrategy.AUTO;
private int maxLeafSize = DEFAULT_BINARY_LEAF_SIZE;
private int maxDepth = DEFAULT_MAX_DEPTH;
private double degenerateTolerance = DEFAULT_DEGENERATE_TOL;
private List<E> elements;
private Function<E, AABB> aabbFunction;
public Builder() {
}
public Builder<E> elements(List<E> elements) {
this.elements = elements;
return this;
}
public Builder<E> function(Function<E, AABB> aabbFunction) {
this.aabbFunction = aabbFunction;
return this;
}
public Builder<E> degree(int degree) {
this.degree = degree;
return this;
}
public Builder<E> splitStrategy(SplitStrategy splitStrategy) {
this.splitStrategy = splitStrategy;
return this;
}
public Builder<E> maxLeafSize(int maxLeafSize) {
this.maxLeafSize = maxLeafSize;
return this;
}
public Builder<E> maxDepth(int maxDepth) {
this.maxDepth = maxDepth;
return this;
}
public Builder<E> degenerateTolerance(double degenerateTolerance) {
this.degenerateTolerance = degenerateTolerance;
return this;
}
public Function<E, AABB> getAabbFunction() {
return aabbFunction;
}
public double getDegenerateTolerance() {
return degenerateTolerance;
}
public int getDegree() {
return degree;
}
public List<E> getElements() {
return elements;
}
public int getMaxDepth() {
return maxDepth;
}
public int getMaxLeafSize() {
return maxLeafSize;
}
public SplitStrategy getSplitStrategy() {
return splitStrategy;
}
public Builder<E> binaryDefault() {
return new Builder<E>().degree(2).splitStrategy(SplitStrategy.AUTO).maxLeafSize(DEFAULT_BINARY_LEAF_SIZE)
.maxDepth(DEFAULT_MAX_DEPTH).degenerateTolerance(DEFAULT_DEGENERATE_TOL);
}
public Builder<E> octonaryDefault() {
return new Builder<E>().degree(8).splitStrategy(SplitStrategy.AUTO).maxLeafSize(DEFAULT_OCTONARY_LEAF_SIZE)
.maxDepth(DEFAULT_MAX_DEPTH).degenerateTolerance(DEFAULT_DEGENERATE_TOL);
}
public BoundingVolumeHierarchyTree<E> build() {
if (degree != 2 && degree != 8) {
throw new IllegalArgumentException("Only degree 2 and 8 are supported.");
}
if (maxLeafSize < 1) {
throw new IllegalArgumentException("maxLeafSize must be >= 1.");
}
if (maxDepth < 1) {
throw new IllegalArgumentException("maxDepth must be >= 1.");
}
if (degenerateTolerance < 0.0) {
throw new IllegalArgumentException("degenerateTolerance must be >= 0.");
}
return new BoundingVolumeHierarchyTree<E>(this);
}
}
/**
* Internal build-time cache.
*/
private static final class BuildItem<E> {
final E element;
final AABB aabb;
final double centerX;
final double centerY;
final double centerZ;
BuildItem(E element, AABB aabb) {
this.element = element;
this.aabb = aabb;
this.centerX = 0.5 * (aabb.getMinX() + aabb.getMaxX());
this.centerY = 0.5 * (aabb.getMinY() + aabb.getMaxY());
this.centerZ = 0.5 * (aabb.getMinZ() + aabb.getMaxZ());
}
double center(int axis) {
switch (axis) {
case 0:
return centerX;
case 1:
return centerY;
case 2:
return centerZ;
default:
throw new IllegalArgumentException("axis must be 0, 1, or 2");
}
}
}
private BoundingVolumeHierarchyTree(Builder<E> builder) {
Objects.requireNonNull(builder, "config");
this.aabbFunction = Objects.requireNonNull(builder.aabbFunction);
List<E> elements = Objects.requireNonNull(builder.getElements());
if (elements == null || elements.isEmpty()) {
this.root = null;
return;
}
List<BuildItem<E>> items = toBuildItems(elements);
SplitStrategy resolvedStrategy = resolveSplitStrategy(builder);
if (builder.getDegree() == 2) {
this.root = buildBinaryRecursive(items, 0, resolvedStrategy, builder);
} else {
this.root = buildOctonaryRecursive(items, 0, resolvedStrategy, builder);
}
}
/**
* Convenience factory: binary BVH with AUTO/defaults.
*/
public static <E> BoundingVolumeHierarchyTree<E> newBinary(List<E> elements, Function<E, AABB> aabbFunction) {
Builder<E> binaryDefault = new Builder<E>().binaryDefault();
binaryDefault.elements(elements);
binaryDefault.function(aabbFunction);
return new BoundingVolumeHierarchyTree<>(binaryDefault);
}
/**
* Convenience factory: octonary BVH with AUTO/defaults.
*/
public static <E> BoundingVolumeHierarchyTree<E> newOctonary(List<E> elements, Function<E, AABB> aabbFunction) {
Builder<E> octonaryDefault = new Builder<E>().octonaryDefault();
octonaryDefault.elements(elements);
octonaryDefault.function(aabbFunction);
return new BoundingVolumeHierarchyTree<>(octonaryDefault);
}
/**
* Convenience factory: octonary BVH with a custom maxLeafSize.
*/
public static <E> BoundingVolumeHierarchyTree<E> newOctonary(List<E> elements, Function<E, AABB> aabbFunction,
int maxLeafSize) {
Builder<E> octonaryDefault = new Builder<E>().octonaryDefault();
octonaryDefault.elements(elements);
octonaryDefault.function(aabbFunction);
octonaryDefault.maxLeafSize(maxLeafSize);
octonaryDefault.maxDepth(computeDefaultMaxDepth(elements != null ? elements.size() : 0));
return new BoundingVolumeHierarchyTree<>(octonaryDefault);
}
public Node<E> getRoot() {
return root;
}
public void setRoot(Node<E> root) {
this.root = root;
}
/**
* NOTE: generic version already exists.
*/
public static AABB getAggregateAABB(List<ConcretePolygon> polygons) {
if (polygons.size() == 1) {
return AABB.of(polygons.get(0));
}
return getAggregateAABB(polygons, AABB::of);
}
public static <E> AABB getAggregateAABB(List<E> elements, Function<E, AABB> aabbFunc) {
double minX = Double.POSITIVE_INFINITY;
double minY = Double.POSITIVE_INFINITY;
double minZ = Double.POSITIVE_INFINITY;
double maxX = Double.NEGATIVE_INFINITY;
double maxY = Double.NEGATIVE_INFINITY;
double maxZ = Double.NEGATIVE_INFINITY;
for (E e : elements) {
AABB aabb = aabbFunc.apply(e);
minX = Math.min(minX, aabb.getMinX());
minY = Math.min(minY, aabb.getMinY());
minZ = Math.min(minZ, aabb.getMinZ());
maxX = Math.max(maxX, aabb.getMaxX());
maxY = Math.max(maxY, aabb.getMaxY());
maxZ = Math.max(maxZ, aabb.getMaxZ());
}
return new AABB(minX, minY, minZ, maxX, maxY, maxZ);
}
public List<E> getAllIntersectingElements(AABB query) {
List<E> result = new ArrayList<>();
getAllIntersectingElementsRecursive(root, query, result);
return result;
}
private void getAllIntersectingElementsRecursive(Node<E> node, AABB query, List<E> result) {
if (node == null) {
return;
}
if (!node.getAabb().overlaps(query)) {
return;
}
if (node.isLeaf()) {
result.add(node.getElement());
return;
}
for (Node<E> child : node.getChildren()) {
getAllIntersectingElementsRecursive(child, query, result);
}
}
// ---------------------------------------------------------------------
// Build
// ---------------------------------------------------------------------
private List<BuildItem<E>> toBuildItems(List<E> elements) {
List<BuildItem<E>> items = new ArrayList<>(elements.size());
for (E e : elements) {
AABB aabb = Objects.requireNonNull(aabbFunction.apply(e), "aabbFunction returned null");
items.add(new BuildItem<>(e, aabb));
}
return items;
}
private SplitStrategy resolveSplitStrategy(Builder<E> config) {
if (config.getSplitStrategy() != SplitStrategy.AUTO) {
validateStrategyMatchesDegree(config.getDegree(), config.getSplitStrategy());
return config.getSplitStrategy();
}
return (config.getDegree() == 2) ? SplitStrategy.BINARY_SPATIAL_MEDIAN : SplitStrategy.OCTONARY_OBJECT_MEAN;
}
private void validateStrategyMatchesDegree(int degree, SplitStrategy strategy) {
boolean binary = strategy == SplitStrategy.BINARY_OBJECT_MEDIAN || strategy == SplitStrategy.BINARY_OBJECT_MEAN
|| strategy == SplitStrategy.BINARY_SPATIAL_MEDIAN;
boolean octonary = strategy == SplitStrategy.OCTONARY_OBJECT_MEDIAN
|| strategy == SplitStrategy.OCTONARY_OBJECT_MEAN || strategy == SplitStrategy.OCTONARY_SPATIAL_MEDIAN;
if (degree == 2 && !binary) {
throw new IllegalArgumentException("Strategy " + strategy + " does not match degree 2.");
}
if (degree == 8 && !octonary) {
throw new IllegalArgumentException("Strategy " + strategy + " does not match degree 8.");
}
}
private Node<E> buildBinaryRecursive(List<BuildItem<E>> items, int depth, SplitStrategy strategy,
Builder<E> config) {
AABB totalAabb = getAggregateAABBFromItems(items);
if (shouldStop(items, depth, config, totalAabb)) {
return packLeafFromItems(items, totalAabb);
}
int axis = totalAabb.findLongestAxis();
SplitResult<E> split;
switch (strategy) {
case BINARY_OBJECT_MEDIAN:
split = splitBinaryObjectMedian(items, axis);
break;
case BINARY_OBJECT_MEAN:
split = splitBinaryObjectMean(items, axis);
break;
case BINARY_SPATIAL_MEDIAN:
split = splitBinarySpatialMedian(items, axis, totalAabb);
break;
default:
throw new IllegalStateException("Unexpected binary strategy: " + strategy);
}
if (!split.valid()) {
return packLeafFromItems(items, totalAabb);
}
Node<E> node = new Node<>(null, totalAabb);
node.getChildren().add(buildBinaryRecursive(split.left, depth + 1, strategy, config));
node.getChildren().add(buildBinaryRecursive(split.right, depth + 1, strategy, config));
return node;
}
private Node<E> buildOctonaryRecursive(List<BuildItem<E>> items, int depth, SplitStrategy strategy,
Builder<E> config) {
AABB totalAabb = getAggregateAABBFromItems(items);
if (shouldStop(items, depth, config, totalAabb)) {
return packLeafFromItems(items, totalAabb);
}
OctSplit<E> split;
switch (strategy) {
case OCTONARY_OBJECT_MEDIAN:
split = splitOctonaryObjectMedian(items);
break;
case OCTONARY_OBJECT_MEAN:
split = splitOctonaryObjectMean(items);
break;
case OCTONARY_SPATIAL_MEDIAN:
split = splitOctonarySpatialMedian(items, totalAabb);
break;
default:
throw new IllegalStateException("Unexpected octonary strategy: " + strategy);
}
if (!split.valid()) {
return packLeafFromItems(items, totalAabb);
}
Node<E> node = new Node<>(null, totalAabb);
for (int i = 0; i < 8; i++) {
List<BuildItem<E>> bucket = split.buckets[i];
if (bucket.isEmpty()) {
continue;
}
node.getChildren().add(buildOctonaryRecursive(bucket, depth + 1, strategy, config));
}
return node;
}
private boolean shouldStop(List<BuildItem<E>> items, int depth, Builder<E> config, AABB totalAabb) {
return items.size() <= config.getMaxLeafSize() || depth >= config.getMaxDepth()
|| totalAabb.isDegenerate(config.getDegenerateTolerance());
}
private Node<E> packLeafFromItems(List<BuildItem<E>> items, AABB totalAabb) {
if (items.size() == 1) {
BuildItem<E> item = items.get(0);
return new Node<>(item.element, item.aabb);
}
Node<E> leaf = new Node<>(null, totalAabb);
for (BuildItem<E> item : items) {
leaf.getChildren().add(new Node<>(item.element, item.aabb));
}
return leaf;
}
private static <E> AABB getAggregateAABBFromItems(List<BuildItem<E>> items) {
double minX = Double.POSITIVE_INFINITY;
double minY = Double.POSITIVE_INFINITY;
double minZ = Double.POSITIVE_INFINITY;
double maxX = Double.NEGATIVE_INFINITY;
double maxY = Double.NEGATIVE_INFINITY;
double maxZ = Double.NEGATIVE_INFINITY;
for (BuildItem<E> item : items) {
AABB aabb = item.aabb;
minX = Math.min(minX, aabb.getMinX());
minY = Math.min(minY, aabb.getMinY());
minZ = Math.min(minZ, aabb.getMinZ());
maxX = Math.max(maxX, aabb.getMaxX());
maxY = Math.max(maxY, aabb.getMaxY());
maxZ = Math.max(maxZ, aabb.getMaxZ());
}
return new AABB(minX, minY, minZ, maxX, maxY, maxZ);
}
// ---------------------------------------------------------------------
// Binary splits
// ---------------------------------------------------------------------
private SplitResult<E> splitBinaryObjectMedian(List<BuildItem<E>> items, int axis) {
items.sort(Comparator.comparingDouble(it -> it.center(axis)));
int mid = items.size() / 2;
if (mid <= 0 || mid >= items.size()) {
return SplitResult.invalid();
}
return SplitResult.of(items.subList(0, mid), items.subList(mid, items.size()));
}
private SplitResult<E> splitBinaryObjectMean(List<BuildItem<E>> items, int axis) {
double sum = 0.0;
for (BuildItem<E> item : items) {
sum += item.center(axis);
}
double splitValue = sum / items.size();
List<BuildItem<E>> left = new ArrayList<>();
List<BuildItem<E>> right = new ArrayList<>();
for (BuildItem<E> item : items) {
if (item.center(axis) < splitValue) {
left.add(item);
} else {
right.add(item);
}
}
if (left.isEmpty() || right.isEmpty()) {
return splitBinaryObjectMedian(items, axis);
}
return SplitResult.of(left, right);
}
private SplitResult<E> splitBinarySpatialMedian(List<BuildItem<E>> items, int axis, AABB totalAabb) {
double splitValue;
switch (axis) {
case 0:
splitValue = totalAabb.getCenterX();
break;
case 1:
splitValue = totalAabb.getCenterY();
break;
case 2:
splitValue = totalAabb.getCenterZ();
break;
default:
throw new IllegalArgumentException("axis must be 0, 1, or 2");
}
List<BuildItem<E>> left = new ArrayList<>();
List<BuildItem<E>> right = new ArrayList<>();
for (BuildItem<E> item : items) {
if (item.center(axis) < splitValue) {
left.add(item);
} else {
right.add(item);
}
}
if (left.isEmpty() || right.isEmpty()) {
return splitBinaryObjectMean(items, axis);
}
return SplitResult.of(left, right);
}
private static final class SplitResult<E> {
final List<BuildItem<E>> left;
final List<BuildItem<E>> right;
private SplitResult(List<BuildItem<E>> left, List<BuildItem<E>> right) {
this.left = left;
this.right = right;
}
static <E> SplitResult<E> of(List<BuildItem<E>> left, List<BuildItem<E>> right) {
return new SplitResult<>(left, right);
}
static <E> SplitResult<E> invalid() {
return new SplitResult<>(null, null);
}
boolean valid() {
return left != null && right != null && !left.isEmpty() && !right.isEmpty();
}
}
// ---------------------------------------------------------------------
// Octonary / 8-ary splits
// ---------------------------------------------------------------------
private OctSplit<E> splitOctonaryObjectMean(List<BuildItem<E>> items) {
double sumX = 0.0;
double sumY = 0.0;
double sumZ = 0.0;
for (BuildItem<E> item : items) {
sumX += item.centerX;
sumY += item.centerY;
sumZ += item.centerZ;
}
double sx = sumX / items.size();
double sy = sumY / items.size();
double sz = sumZ / items.size();
return bucketizeOctonary(items, sx, sy, sz, true);
}
private OctSplit<E> splitOctonarySpatialMedian(List<BuildItem<E>> items, AABB totalAabb) {
double sx = totalAabb.getCenterX();
double sy = totalAabb.getCenterY();
double sz = totalAabb.getCenterZ();
OctSplit<E> split = bucketizeOctonary(items, sx, sy, sz, false);
if (!split.valid()) {
return splitOctonaryObjectMean(items);
}
return split;
}
private OctSplit<E> splitOctonaryObjectMedian(List<BuildItem<E>> items) {
items.sort(Comparator.comparingDouble(it -> it.centerX));
double sx = medianValue(items, 0);
items.sort(Comparator.comparingDouble(it -> it.centerY));
double sy = medianValue(items, 1);
items.sort(Comparator.comparingDouble(it -> it.centerZ));
double sz = medianValue(items, 2);
return bucketizeOctonary(items, sx, sy, sz, true);
}
private double medianValue(List<BuildItem<E>> items, int axis) {
int n = items.size();
int mid = n / 2;
if ((n & 1) == 1) {
return items.get(mid).center(axis);
}
return 0.5 * (items.get(mid - 1).center(axis) + items.get(mid).center(axis));
}
@SuppressWarnings("unchecked")
private OctSplit<E> bucketizeOctonary(List<BuildItem<E>> items, double sx, double sy, double sz,
boolean fallbackToMeanOnFailure) {
List<BuildItem<E>>[] buckets = new List[8];
for (int i = 0; i < 8; i++) {
buckets[i] = new ArrayList<>();
}
int nonEmptyCount = 0;
boolean[] seen = new boolean[8];
for (BuildItem<E> item : items) {
int idx = octantIndex(item.centerX, item.centerY, item.centerZ, sx, sy, sz);
buckets[idx].add(item);
if (!seen[idx]) {
seen[idx] = true;
nonEmptyCount++;
}
}
if (nonEmptyCount <= 1) {
if (fallbackToMeanOnFailure) {
// caller may already be object mean, so just mark invalid here
return OctSplit.invalid();
}
return OctSplit.invalid();
}
return OctSplit.of(buckets);
}
private int octantIndex(double x, double y, double z, double sx, double sy, double sz) {
int idx = 0;
if (x >= sx)
idx |= 1;
if (y >= sy)
idx |= 2;
if (z >= sz)
idx |= 4;
return idx;
}
private static final class OctSplit<E> {
final List<BuildItem<E>>[] buckets;
private OctSplit(List<BuildItem<E>>[] buckets) {
this.buckets = buckets;
}
static <E> OctSplit<E> of(List<BuildItem<E>>[] buckets) {
return new OctSplit<>(buckets);
}
static <E> OctSplit<E> invalid() {
return new OctSplit<>(null);
}
boolean valid() {
if (buckets == null) {
return false;
}
int nonEmpty = 0;
for (List<BuildItem<E>> bucket : buckets) {
if (bucket != null && !bucket.isEmpty()) {
nonEmpty++;
}
}
return nonEmpty > 1;
}
}
// ---------------------------------------------------------------------
// helpers: to be continued with more heuristics
// ---------------------------------------------------------------------
private static int computeDefaultMaxDepth(int n) {
if (n <= 1) {
return DEFAULT_MIN_DEPTH;
}
double log2n = Math.log(n) / Math.log(2.0);
int depth = (int) Math.ceil(2.0 * log2n);
return clamp(depth, DEFAULT_MIN_DEPTH, DEFAULT_MAX_DEPTH);
}
private static int clamp(int value, int min, int max) {
return Math.max(min, Math.min(max, value));
}
}
\ No newline at end of file
......@@ -88,8 +88,11 @@ public class SolidSelfIntCheckAABB extends Check {
CheckResult cr;
// Build BVH on polygons, but compute AABBs from the *original* polygons
BoundingVolumeHierarchyTree<Polygon> tree =
new BoundingVolumeHierarchyTree<>(polys, p -> AABB.of(p.getOriginal()));
BoundingVolumeHierarchyTree<Polygon> tree =
new BoundingVolumeHierarchyTree.Builder<Polygon>()
.elements(polys)
.function(p -> AABB.of(p.getOriginal()))
.build();
// TODO: comparison with older version without tree
List<PolygonIntersection> intersections =
SelfIntersectionUtil.calculateSolidSelfIntersection(g, 0.001, tree);
......
......@@ -84,7 +84,7 @@ public class NestedRingsCheck extends Check {
@Override
public void check(Polygon p) {
if (useAabbFilter) {
if (p.getInnerRings().size() > 3) {
checkWithBoundingBoxFilter(p);
} else {
checkOriginal(p);
......
......@@ -204,10 +204,8 @@ public class RingSelfIntCheck extends Check {
}
}
BoundingVolumeHierarchyTree<Edge> edgeTree = new BoundingVolumeHierarchyTree<>(
edges,
e -> AABB.of(e.getFrom(), e.getTo(), epsilon),
BoundingVolumeHierarchyTree.BuildConfig.binaryDefault());
BoundingVolumeHierarchyTree<Edge> edgeTree = BoundingVolumeHierarchyTree.newBinary(
edges, e -> AABB.of(e.getFrom(), e.getTo(), epsilon));
for (int i = 0; i < edges.size(); i++) {
Edge e1 = edges.get(i);
......@@ -263,11 +261,9 @@ public class RingSelfIntCheck extends Check {
}
List<Edge> edges = getEdgesForRing(lr);
BoundingVolumeHierarchyTree<Vertex> vertexTree = new BoundingVolumeHierarchyTree<>(
vertices,
v -> AABB.of(v, epsilon),
BoundingVolumeHierarchyTree.BuildConfig.binaryDefault());
BoundingVolumeHierarchyTree<Vertex> vertexTree =
BoundingVolumeHierarchyTree.newBinary(vertices, v -> AABB.of(v, epsilon));
for (Edge e : edges) {
if (checkForPointsTouchingEdgeTree(lr, e, vertexTree)) {
......@@ -275,10 +271,8 @@ public class RingSelfIntCheck extends Check {
}
}
BoundingVolumeHierarchyTree<Edge> edgeTree = new BoundingVolumeHierarchyTree<>(
edges,
e -> AABB.of(e.getFrom(), e.getTo(), epsilon),
BoundingVolumeHierarchyTree.BuildConfig.binaryDefault());
BoundingVolumeHierarchyTree<Edge> edgeTree = BoundingVolumeHierarchyTree.newBinary(
edges, e -> AABB.of(e.getFrom(), e.getTo(), epsilon));
for (int i = 0; i < edges.size(); i++) {
Edge e1 = edges.get(i);
......
......@@ -133,21 +133,18 @@ public class SelfIntersectionUtil {
public static List<PolygonIntersection> calculateSolidSelfIntersectionWithTree(
Geometry g,
double delta,
BoundingVolumeHierarchyTree.BuildConfig treeConfig) {
BoundingVolumeHierarchyTree.Builder<Integer> treeConfig) {
List<TesselatedPolygon> tesselatedPolygons = tesselateAndFilter(g, delta);
List<Integer> indices = new ArrayList<>(tesselatedPolygons.size());
for (int i = 0; i < tesselatedPolygons.size(); i++) {
indices.add(i);
}
treeConfig.elements(indices).function(index -> AABB.of(tesselatedPolygons.get(index).getOriginal()));
// Build BVH on polygon indices, while computing AABBs from the original polygons
BoundingVolumeHierarchyTree<Integer> tree =
new BoundingVolumeHierarchyTree<>(
indices,
index -> AABB.of(tesselatedPolygons.get(index).getOriginal()),
treeConfig
);
BoundingVolumeHierarchyTree<Integer> tree = treeConfig.build();
List<PolygonIntersection> intersections = new ArrayList<>();
......@@ -187,7 +184,7 @@ public class SelfIntersectionUtil {
return calculateSolidSelfIntersectionWithTree(
g,
delta,
BoundingVolumeHierarchyTree.BuildConfig.binaryDefault()
new BoundingVolumeHierarchyTree.Builder<Integer>().binaryDefault()
);
}
......
package de.hft.stuttgart.citydoctor2.checks.util;
import static org.junit.Assert.*;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.util.List;
import org.junit.Test;
import org.citygml4j.core.model.CityGMLVersion;
import org.citygml4j.core.model.core.CityModel;
import org.junit.jupiter.api.Test;
import de.hft.stuttgart.citydoctor2.check.ValidationConfiguration;
import de.hft.stuttgart.citydoctor2.database.UnconnectedCache;
import de.hft.stuttgart.citydoctor2.datastructure.Building;
import de.hft.stuttgart.citydoctor2.datastructure.CityDoctorModel;
import de.hft.stuttgart.citydoctor2.datastructure.Geometry;
......@@ -15,13 +20,30 @@ import de.hft.stuttgart.citydoctor2.datastructure.Lod;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.bht.AABB;
import de.hft.stuttgart.citydoctor2.datastructure.bht.BoundingVolumeHierarchyTree;
import de.hft.stuttgart.citydoctor2.exceptions.CityDoctorWriteException;
import de.hft.stuttgart.citydoctor2.parser.CityGmlParseException;
import de.hft.stuttgart.citydoctor2.parser.CityGmlParser;
import de.hft.stuttgart.citydoctor2.parser.InvalidGmlFileException;
import de.hft.stuttgart.citydoctor2.parser.ParserConfiguration;
import de.hft.stuttgart.citydoctor2.utils.PolygonIntersection;
public class SolidSelfIntersectionBVHUtilTest {
@Test
public void testWriteModel() throws CityDoctorWriteException {
Building b = new Building();
b.addGeometry(GeometryTestUtils.createGoodGeometry());
b.setGmlObject(new org.citygml4j.core.model.building.Building());
UnconnectedCache unconnectedCache = new UnconnectedCache();
CityDoctorModel model = new CityDoctorModel(
new ParserConfiguration(8, false), new File("test.gml"), unconnectedCache);
model.setParsedCityGMLVersion(CityGMLVersion.v2_0);
model.setCityModel(new CityModel());
model.addBuilding(b);
model.saveAs("test.gml", false);
}
@Test
public void testBVHCalculateOnKnownGoodModel() throws CityGmlParseException, InvalidGmlFileException {
......@@ -42,7 +64,7 @@ public class SolidSelfIntersectionBVHUtilTest {
assertTrue("Expected at least 2 polygons", polys.size() > 1);
BoundingVolumeHierarchyTree<Polygon> tree =
new BoundingVolumeHierarchyTree<>(polys, p -> AABB.of(p.getOriginal()));
BoundingVolumeHierarchyTree.newBinary(polys, p -> AABB.of(p.getOriginal()));
double delta = 0.001;
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment