Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
CityDoctor
CityDoctor2
Commits
cd89dc71
Commit
cd89dc71
authored
Apr 28, 2026
by
Numanoglu
Browse files
Add BVH performance metrics and exploration tests
parent
b073d9bc
Pipeline
#12396
passed with stage
in 2 minutes and 1 second
Changes
11
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/bht/BvhInputMetricsCollector.java
0 → 100644
View file @
cd89dc71
package
de.hft.stuttgart.citydoctor2.checks.bht
;
import
java.util.ArrayList
;
import
java.util.List
;
import
java.util.Locale
;
import
java.util.function.Function
;
import
de.hft.stuttgart.citydoctor2.datastructure.LinearRing
;
import
de.hft.stuttgart.citydoctor2.datastructure.Polygon
;
import
de.hft.stuttgart.citydoctor2.datastructure.bht.AABB
;
/**
* Collects cheap broad-phase shape metrics for BVH strategy experiments.
*
* These metrics are intentionally geometry-agnostic: the same collector can be
* used for solid polygons, nested-ring rings, ring-self-intersection edges later,
* synthetic fixtures, and parsed CityGML models.
*
* @author Numanoglu
*/
final
class
BvhInputMetricsCollector
{
private
static
final
double
DEGENERATE_TOLERANCE
=
1
e
-
12
;
private
static
final
long
MAX_PAIR_SAMPLES
=
20_000L
;
private
BvhInputMetricsCollector
()
{
}
static
Metrics
forPolygons
(
String
scenario
,
List
<?
extends
Polygon
>
polygons
)
{
return
collect
(
scenario
,
polygons
,
polygon
->
AABB
.
of
(
polygon
.
getOriginal
()));
}
static
Metrics
forRings
(
String
scenario
,
List
<?
extends
LinearRing
>
rings
)
{
return
collect
(
scenario
,
rings
,
AABB:
:
of
);
}
/**
* Collects metrics from any element type that can be converted to an AABB.
*/
static
<
E
>
Metrics
collect
(
String
scenario
,
List
<
E
>
elements
,
Function
<
E
,
AABB
>
aabbFunction
)
{
List
<
AABB
>
boxes
=
new
ArrayList
<>(
elements
.
size
());
for
(
E
element
:
elements
)
{
AABB
aabb
=
aabbFunction
.
apply
(
element
);
if
(
aabb
!=
null
)
{
boxes
.
add
(
aabb
);
}
}
return
collectBoxes
(
scenario
,
boxes
);
}
/**
* Collects metrics from precomputed AABBs. Pairwise overlap and containment
* are sampled when the full pairs set would be too large.
*/
static
Metrics
collectBoxes
(
String
scenario
,
List
<
AABB
>
boxes
)
{
int
count
=
boxes
.
size
();
if
(
count
==
0
)
{
return
Metrics
.
empty
(
scenario
);
}
Aggregate
aggregate
=
aggregate
(
boxes
);
PairSample
pairSample
=
samplePairs
(
boxes
);
int
degenerateCount
=
0
;
double
totalAspectRatio
=
0.0
;
double
totalDx
=
0.0
;
double
totalDy
=
0.0
;
double
totalDz
=
0.0
;
double
totalCx
=
0.0
;
double
totalCy
=
0.0
;
double
totalCz
=
0.0
;
for
(
AABB
box
:
boxes
)
{
if
(
box
.
isDegenerate
(
DEGENERATE_TOLERANCE
))
{
degenerateCount
++;
}
double
dx
=
extentX
(
box
);
double
dy
=
extentY
(
box
);
double
dz
=
extentZ
(
box
);
totalDx
+=
dx
;
totalDy
+=
dy
;
totalDz
+=
dz
;
totalAspectRatio
+=
aspectRatio
(
dx
,
dy
,
dz
);
totalCx
+=
box
.
getCenterX
();
totalCy
+=
box
.
getCenterY
();
totalCz
+=
box
.
getCenterZ
();
}
double
meanCx
=
totalCx
/
count
;
double
meanCy
=
totalCy
/
count
;
double
meanCz
=
totalCz
/
count
;
double
varianceX
=
0.0
;
double
varianceY
=
0.0
;
double
varianceZ
=
0.0
;
for
(
AABB
box
:
boxes
)
{
varianceX
+=
square
(
box
.
getCenterX
()
-
meanCx
);
varianceY
+=
square
(
box
.
getCenterY
()
-
meanCy
);
varianceZ
+=
square
(
box
.
getCenterZ
()
-
meanCz
);
}
return
new
Metrics
(
scenario
,
count
,
countPairs
(
count
),
pairSample
.
sampledPairs
,
pairSample
.
overlapRate
(),
pairSample
.
containmentRate
(),
(
double
)
degenerateCount
/
count
,
totalAspectRatio
/
count
,
totalDx
/
count
,
totalDy
/
count
,
totalDz
/
count
,
Math
.
sqrt
(
varianceX
/
count
)
/
positiveOrOne
(
aggregate
.
dx
),
Math
.
sqrt
(
varianceY
/
count
)
/
positiveOrOne
(
aggregate
.
dy
),
Math
.
sqrt
(
varianceZ
/
count
)
/
positiveOrOne
(
aggregate
.
dz
));
}
/**
* Emits one compact metrics line that can be read next to a performance table.
*/
static
void
print
(
Metrics
metrics
)
{
System
.
out
.
println
(
metrics
.
toSummaryLine
());
}
private
static
Aggregate
aggregate
(
List
<
AABB
>
boxes
)
{
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
(
AABB
box
:
boxes
)
{
minX
=
Math
.
min
(
minX
,
box
.
getMinX
());
minY
=
Math
.
min
(
minY
,
box
.
getMinY
());
minZ
=
Math
.
min
(
minZ
,
box
.
getMinZ
());
maxX
=
Math
.
max
(
maxX
,
box
.
getMaxX
());
maxY
=
Math
.
max
(
maxY
,
box
.
getMaxY
());
maxZ
=
Math
.
max
(
maxZ
,
box
.
getMaxZ
());
}
return
new
Aggregate
(
maxX
-
minX
,
maxY
-
minY
,
maxZ
-
minZ
);
}
private
static
PairSample
samplePairs
(
List
<
AABB
>
boxes
)
{
long
totalPairs
=
countPairs
(
boxes
.
size
());
long
sampleEvery
=
Math
.
max
(
1L
,
totalPairs
/
MAX_PAIR_SAMPLES
);
long
pairIndex
=
0L
;
long
sampledPairs
=
0L
;
long
overlappingPairs
=
0L
;
long
containedPairs
=
0L
;
for
(
int
i
=
0
;
i
<
boxes
.
size
()
-
1
;
i
++)
{
AABB
a
=
boxes
.
get
(
i
);
for
(
int
j
=
i
+
1
;
j
<
boxes
.
size
();
j
++)
{
if
(
pairIndex
%
sampleEvery
==
0L
)
{
AABB
b
=
boxes
.
get
(
j
);
sampledPairs
++;
if
(
a
.
overlaps
(
b
))
{
overlappingPairs
++;
}
if
(
a
.
contains
(
b
)
||
b
.
contains
(
a
))
{
containedPairs
++;
}
}
pairIndex
++;
}
}
return
new
PairSample
(
sampledPairs
,
overlappingPairs
,
containedPairs
);
}
private
static
long
countPairs
(
int
count
)
{
return
(
long
)
count
*
(
count
-
1
)
/
2L
;
}
private
static
double
extentX
(
AABB
box
)
{
return
box
.
getMaxX
()
-
box
.
getMinX
();
}
private
static
double
extentY
(
AABB
box
)
{
return
box
.
getMaxY
()
-
box
.
getMinY
();
}
private
static
double
extentZ
(
AABB
box
)
{
return
box
.
getMaxZ
()
-
box
.
getMinZ
();
}
private
static
double
aspectRatio
(
double
dx
,
double
dy
,
double
dz
)
{
double
max
=
Math
.
max
(
dx
,
Math
.
max
(
dy
,
dz
));
double
min
=
Math
.
min
(
positiveOrMax
(
dx
),
Math
.
min
(
positiveOrMax
(
dy
),
positiveOrMax
(
dz
)));
if
(
min
==
Double
.
MAX_VALUE
)
{
return
1.0
;
}
return
max
/
min
;
}
private
static
double
positiveOrMax
(
double
value
)
{
return
value
>
DEGENERATE_TOLERANCE
?
value
:
Double
.
MAX_VALUE
;
}
private
static
double
positiveOrOne
(
double
value
)
{
return
value
>
DEGENERATE_TOLERANCE
?
value
:
1.0
;
}
private
static
double
square
(
double
value
)
{
return
value
*
value
;
}
private
static
final
class
Aggregate
{
final
double
dx
;
final
double
dy
;
final
double
dz
;
Aggregate
(
double
dx
,
double
dy
,
double
dz
)
{
this
.
dx
=
dx
;
this
.
dy
=
dy
;
this
.
dz
=
dz
;
}
}
private
static
final
class
PairSample
{
final
long
sampledPairs
;
final
long
overlappingPairs
;
final
long
containedPairs
;
PairSample
(
long
sampledPairs
,
long
overlappingPairs
,
long
containedPairs
)
{
this
.
sampledPairs
=
sampledPairs
;
this
.
overlappingPairs
=
overlappingPairs
;
this
.
containedPairs
=
containedPairs
;
}
double
overlapRate
()
{
return
sampledPairs
==
0L
?
0.0
:
(
double
)
overlappingPairs
/
sampledPairs
;
}
double
containmentRate
()
{
return
sampledPairs
==
0L
?
0.0
:
(
double
)
containedPairs
/
sampledPairs
;
}
}
static
final
class
Metrics
{
final
String
scenario
;
final
int
elementCount
;
final
long
totalPairs
;
final
long
sampledPairs
;
final
double
sampledOverlapRate
;
final
double
sampledContainmentRate
;
final
double
degenerateRate
;
final
double
averageAspectRatio
;
final
double
averageExtentX
;
final
double
averageExtentY
;
final
double
averageExtentZ
;
final
double
normalizedCenterSpreadX
;
final
double
normalizedCenterSpreadY
;
final
double
normalizedCenterSpreadZ
;
private
Metrics
(
String
scenario
,
int
elementCount
,
long
totalPairs
,
long
sampledPairs
,
double
sampledOverlapRate
,
double
sampledContainmentRate
,
double
degenerateRate
,
double
averageAspectRatio
,
double
averageExtentX
,
double
averageExtentY
,
double
averageExtentZ
,
double
normalizedCenterSpreadX
,
double
normalizedCenterSpreadY
,
double
normalizedCenterSpreadZ
)
{
this
.
scenario
=
scenario
;
this
.
elementCount
=
elementCount
;
this
.
totalPairs
=
totalPairs
;
this
.
sampledPairs
=
sampledPairs
;
this
.
sampledOverlapRate
=
sampledOverlapRate
;
this
.
sampledContainmentRate
=
sampledContainmentRate
;
this
.
degenerateRate
=
degenerateRate
;
this
.
averageAspectRatio
=
averageAspectRatio
;
this
.
averageExtentX
=
averageExtentX
;
this
.
averageExtentY
=
averageExtentY
;
this
.
averageExtentZ
=
averageExtentZ
;
this
.
normalizedCenterSpreadX
=
normalizedCenterSpreadX
;
this
.
normalizedCenterSpreadY
=
normalizedCenterSpreadY
;
this
.
normalizedCenterSpreadZ
=
normalizedCenterSpreadZ
;
}
static
Metrics
empty
(
String
scenario
)
{
return
new
Metrics
(
scenario
,
0
,
0L
,
0L
,
0.0
,
0.0
,
0.0
,
0.0
,
0.0
,
0.0
,
0.0
,
0.0
,
0.0
,
0.0
);
}
String
toSummaryLine
()
{
return
String
.
format
(
Locale
.
ROOT
,
"[BVH-METRICS] %s n=%d pairs=%d sampled=%d overlap=%.4f containment=%.4f"
+
" degenerate=%.4f aspectAvg=%.2f avgExtent=(%.2f, %.2f, %.2f)"
+
" centerSpread=(%.3f, %.3f, %.3f)"
,
scenario
,
elementCount
,
totalPairs
,
sampledPairs
,
sampledOverlapRate
,
sampledContainmentRate
,
degenerateRate
,
averageAspectRatio
,
averageExtentX
,
averageExtentY
,
averageExtentZ
,
normalizedCenterSpreadX
,
normalizedCenterSpreadY
,
normalizedCenterSpreadZ
);
}
}
}
CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/bht/BvhPerformanceTestSupport.java
0 → 100644
View file @
cd89dc71
package
de.hft.stuttgart.citydoctor2.checks.bht
;
import
java.io.OutputStream
;
import
java.io.PrintStream
;
import
java.util.List
;
import
java.util.function.Supplier
;
import
org.apache.logging.log4j.Level
;
import
org.apache.logging.log4j.LogManager
;
import
org.apache.logging.log4j.core.config.Configurator
;
import
de.hft.stuttgart.citydoctor2.datastructure.bht.SplitStrategy
;
/**
* Shared support for manual BVH performance probes.
*
* It performs a warmup, repeats the measured operation, validates stable
* result counts, suppresses noisy debug output during the measured operation,
* and prints one compact table per scenario. Timings are intended for local
* comparison of variants, not as deterministic CI assertions.
*
* @author Numanoglu
*/
final
class
BvhPerformanceTestSupport
{
static
final
int
DEFAULT_WARMUP_RUNS
=
2
;
static
final
int
DEFAULT_MEASURE_RUNS
=
5
;
private
static
final
boolean
SUPPRESS_MEASURED_STDOUT
=
true
;
private
static
final
PrintStream
SILENT_OUT
=
new
PrintStream
(
OutputStream
.
nullOutputStream
());
private
BvhPerformanceTestSupport
()
{
}
static
SplitStrategy
[]
concreteStrategies
()
{
return
new
SplitStrategy
[]
{
SplitStrategy
.
BINARY_OBJECT_MEDIAN
,
SplitStrategy
.
BINARY_OBJECT_MEAN
,
SplitStrategy
.
BINARY_SPATIAL_MEDIAN
,
SplitStrategy
.
OCTONARY_OBJECT_MEDIAN
,
SplitStrategy
.
OCTONARY_OBJECT_MEAN
,
SplitStrategy
.
OCTONARY_SPATIAL_MEDIAN
};
}
static
Measurement
measure
(
String
scenario
,
String
variant
,
int
inputSize
,
Supplier
<
Integer
>
measuredOperation
)
{
return
measure
(
scenario
,
variant
,
inputSize
,
DEFAULT_WARMUP_RUNS
,
DEFAULT_MEASURE_RUNS
,
measuredOperation
);
}
/**
* Measures one variant. The supplied operation must return a deterministic
* result count so correctness can still be checked while timing is collected.
*/
static
Measurement
measure
(
String
scenario
,
String
variant
,
int
inputSize
,
int
warmupRuns
,
int
measureRuns
,
Supplier
<
Integer
>
measuredOperation
)
{
for
(
int
i
=
0
;
i
<
warmupRuns
;
i
++)
{
runMeasuredOperation
(
measuredOperation
);
}
long
totalNanos
=
0L
;
int
resultCount
=
-
1
;
for
(
int
i
=
0
;
i
<
measureRuns
;
i
++)
{
long
start
=
System
.
nanoTime
();
int
currentResultCount
=
runMeasuredOperation
(
measuredOperation
);
totalNanos
+=
System
.
nanoTime
()
-
start
;
if
(
resultCount
<
0
)
{
resultCount
=
currentResultCount
;
}
else
if
(
resultCount
!=
currentResultCount
)
{
throw
new
AssertionError
(
"Unstable result count for "
+
scenario
+
" / "
+
variant
+
": expected "
+
resultCount
+
" but was "
+
currentResultCount
);
}
}
Measurement
measurement
=
new
Measurement
(
scenario
,
variant
,
inputSize
,
resultCount
,
totalNanos
/
measureRuns
);
return
measurement
;
}
private
static
int
runMeasuredOperation
(
Supplier
<
Integer
>
measuredOperation
)
{
Level
originalRootLevel
=
LogManager
.
getRootLogger
().
getLevel
();
Configurator
.
setRootLevel
(
Level
.
WARN
);
if
(!
SUPPRESS_MEASURED_STDOUT
)
{
try
{
return
measuredOperation
.
get
();
}
finally
{
Configurator
.
setRootLevel
(
originalRootLevel
);
}
}
PrintStream
originalOut
=
System
.
out
;
try
{
System
.
setOut
(
SILENT_OUT
);
return
measuredOperation
.
get
();
}
finally
{
System
.
setOut
(
originalOut
);
Configurator
.
setRootLevel
(
originalRootLevel
);
}
}
/**
* Prints a scenario-level table and highlights the fastest measured variant.
* The first measurement is treated as the baseline for the speedup column.
*/
static
void
printScenarioSummary
(
String
scenario
,
List
<
Measurement
>
measurements
)
{
if
(
measurements
.
isEmpty
())
{
return
;
}
Measurement
baseline
=
measurements
.
get
(
0
);
Measurement
winner
=
baseline
;
for
(
Measurement
measurement
:
measurements
)
{
if
(
measurement
.
averageNanos
<
winner
.
averageNanos
)
{
winner
=
measurement
;
}
}
System
.
out
.
println
();
System
.
out
.
println
(
"[BVH-PERFORMANCE] "
+
scenario
);
System
.
out
.
println
(
"--------------------------------------------------------------------------"
);
System
.
out
.
println
(
String
.
format
(
"%-34s %10s %10s %12s %10s"
,
"variant"
,
"input"
,
"result"
,
"avg ms"
,
"vs first"
));
System
.
out
.
println
(
"--------------------------------------------------------------------------"
);
for
(
Measurement
measurement
:
measurements
)
{
System
.
out
.
println
(
String
.
format
(
"%-34s %10d %10d %12.3f %10.2fx"
,
measurement
.
variant
,
measurement
.
inputSize
,
measurement
.
resultCount
,
measurement
.
averageMillis
(),
speedup
(
baseline
,
measurement
)));
}
System
.
out
.
println
(
"--------------------------------------------------------------------------"
);
System
.
out
.
println
(
String
.
format
(
"*** WINNER: %s avgMs=%.3f result=%d ***"
,
winner
.
variant
,
winner
.
averageMillis
(),
winner
.
resultCount
));
System
.
out
.
println
();
}
private
static
double
speedup
(
Measurement
baseline
,
Measurement
measurement
)
{
if
(
measurement
.
averageNanos
==
0L
)
{
return
0.0
;
}
return
(
double
)
baseline
.
averageNanos
/
measurement
.
averageNanos
;
}
static
final
class
Measurement
{
final
String
scenario
;
final
String
variant
;
final
int
inputSize
;
final
int
resultCount
;
final
long
averageNanos
;
Measurement
(
String
scenario
,
String
variant
,
int
inputSize
,
int
resultCount
,
long
averageNanos
)
{
this
.
scenario
=
scenario
;
this
.
variant
=
variant
;
this
.
inputSize
=
inputSize
;
this
.
resultCount
=
resultCount
;
this
.
averageNanos
=
averageNanos
;
}
double
averageMillis
()
{
return
averageNanos
/
1_000_000.0
;
}
}
}
CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/bht/BvhStrategyHeuristicExplorationTest.java
0 → 100644
View file @
cd89dc71
package
de.hft.stuttgart.citydoctor2.checks.bht
;
import
static
org
.
junit
.
Assert
.
assertEquals
;
import
java.util.ArrayList
;
import
java.util.List
;
import
org.junit.jupiter.api.Tag
;
import
org.junit.jupiter.api.Test
;
import
de.hft.stuttgart.citydoctor2.checks.util.SelfIntersectionUtil
;
import
de.hft.stuttgart.citydoctor2.datastructure.Geometry
;
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.datastructure.bht.SplitStrategy
;
/**
* Collects SSI timing observations together with BVH input metrics.
*
* The summary is meant as a working note for deriving conservative strategy
* rules after the fixture set changes.
*
* @author Numanoglu
*/
@Tag
(
"performance"
)
public
class
BvhStrategyHeuristicExplorationTest
{
private
static
final
double
DELTA
=
0.001
;
@Test
public
void
exploreSsiStrategyCandidatesFromMetricsAndTiming
()
{
List
<
Scenario
>
scenarios
=
new
ArrayList
<>();
scenarios
.
add
(
new
Scenario
(
"ssi-separated-grid"
,
SyntheticSolidGeometryFactory
.
separatedBoxGrid
(
Lod
.
LOD2
,
12
,
12
)));
scenarios
.
add
(
new
Scenario
(
"ssi-overlapping-grid"
,
SyntheticSolidGeometryFactory
.
overlappingBoxGrid
(
Lod
.
LOD2
,
12
,
12
)));
scenarios
.
add
(
new
Scenario
(
"ssi-dense-clusters"
,
SyntheticSolidGeometryFactory
.
denseBoxClusters
(
Lod
.
LOD2
,
8
,
20
)));
scenarios
.
add
(
new
Scenario
(
"ssi-long-thin-slabs"
,
SyntheticSolidGeometryFactory
.
longThinSlabs
(
Lod
.
LOD2
,
160
)));
scenarios
.
add
(
new
Scenario
(
"ssi-flat-box-grid"
,
SyntheticSolidGeometryFactory
.
flatBoxGrid
(
Lod
.
LOD2
,
200
)));
scenarios
.
add
(
new
Scenario
(
"ssi-citylike-mixed"
,
SyntheticCityGmlLikeGeometryFactory
.
mixedUrbanDistrict
(
Lod
.
LOD2
,
5
,
4
)));
scenarios
.
add
(
new
Scenario
(
"ssi-citylike-courtyard"
,
SyntheticCityGmlLikeGeometryFactory
.
courtyardDistrict
(
Lod
.
LOD2
,
16
)));
scenarios
.
add
(
new
Scenario
(
"ssi-citylike-roofs"
,
SyntheticCityGmlLikeGeometryFactory
.
variedRoofDistrict
(
Lod
.
LOD2
,
180
)));
List
<
Observation
>
observations
=
new
ArrayList
<>();
for
(
Scenario
scenario
:
scenarios
)
{
observations
.
add
(
measureScenario
(
scenario
));
}
printObservationSummary
(
observations
);
}
private
static
Observation
measureScenario
(
Scenario
scenario
)
{
List
<
Polygon
>
polygons
=
scenario
.
geometry
.
getPolygons
();
BvhInputMetricsCollector
.
Metrics
metrics
=
BvhInputMetricsCollector
.
forPolygons
(
scenario
.
name
,
polygons
);
List
<
BvhPerformanceTestSupport
.
Measurement
>
measurements
=
new
ArrayList
<>();
BvhPerformanceTestSupport
.
Measurement
bruteForce
=
BvhPerformanceTestSupport
.
measure
(
scenario
.
name
,
"BRUTE_FORCE"
,
polygons
.
size
(),
()
->
SelfIntersectionUtil
.
calculateSolidSelfIntersection0
(
scenario
.
geometry
,
DELTA
).
size
());
measurements
.
add
(
bruteForce
);
for
(
SplitStrategy
strategy
:
BvhPerformanceTestSupport
.
concreteStrategies
())
{
BvhPerformanceTestSupport
.
Measurement
bvhMeasurement
=
BvhPerformanceTestSupport
.
measure
(
scenario
.
name
,
strategy
.
name
(),
polygons
.
size
(),
()
->
calculateWithTree
(
scenario
.
geometry
,
strategy
));
measurements
.
add
(
bvhMeasurement
);
assertEquals
(
"SSI result count differs for "
+
scenario
.
name
+
" / "
+
strategy
,
bruteForce
.
resultCount
,
bvhMeasurement
.
resultCount
);
}
BvhPerformanceTestSupport
.
printScenarioSummary
(
scenario
.
name
,
measurements
);
return
new
Observation
(
metrics
,
fastest
(
measurements
),
fastestBvh
(
measurements
),
bruteForce
);
}
private
static
int
calculateWithTree
(
Geometry
geometry
,
SplitStrategy
strategy
)
{
BoundingVolumeHierarchyTree
<
Polygon
>
tree
=
BoundingVolumeHierarchyTree
.
newWithStrategy
(
geometry
.
getPolygons
(),
polygon
->
AABB
.
of
(
polygon
.
getOriginal
()),
strategy
);
return
SelfIntersectionUtil
.
calculateSolidSelfIntersection
(
geometry
,
DELTA
,
tree
).
size
();
}
private
static
BvhPerformanceTestSupport
.
Measurement
fastest
(
List
<
BvhPerformanceTestSupport
.
Measurement
>
measurements
)
{
BvhPerformanceTestSupport
.
Measurement
fastest
=
measurements
.
get
(
0
);
for
(
BvhPerformanceTestSupport
.
Measurement
measurement
:
measurements
)
{
if
(
measurement
.
averageNanos
<
fastest
.
averageNanos
)
{
fastest
=
measurement
;
}
}
return
fastest
;
}
private
static
BvhPerformanceTestSupport
.
Measurement
fastestBvh
(
List
<
BvhPerformanceTestSupport
.
Measurement
>
measurements
)
{
BvhPerformanceTestSupport
.
Measurement
fastest
=
null
;
for
(
BvhPerformanceTestSupport
.
Measurement
measurement
:
measurements
)
{
if
(
"BRUTE_FORCE"
.
equals
(
measurement
.
variant
))
{
continue
;
}
if
(
fastest
==
null
||
measurement
.
averageNanos
<
fastest
.
averageNanos
)
{
fastest
=
measurement
;
}
}
return
fastest
;
}
private
static
void
printObservationSummary
(
List
<
Observation
>
observations
)
{
System
.
out
.
println
();
System
.
out
.
println
(
"[BVH-HEURISTIC-EXPLORATION] SSI observations"
);
System
.
out
.
println
(
"---------------------------------------------------------------------------------------------------------------"
);
System
.
out
.
println
(
String
.
format
(
"%-24s %7s %9s %9s %9s %10s %12s %22s %22s"
,
"scenario"
,
"n"
,
"overlap"
,
"contain"
,
"degen"
,
"aspect"
,
"winner"
,
"fastest BVH"
,
"initial candidate rule"
));
System
.
out
.
println
(
"---------------------------------------------------------------------------------------------------------------"
);
for
(
Observation
observation
:
observations
)
{
BvhInputMetricsCollector
.
Metrics
metrics
=
observation
.
metrics
;
System
.
out
.
println
(
String
.
format
(
"%-24s %7d %9.4f %9.4f %9.4f %10.2f %12s %28s %28s"
,
metrics
.
scenario
,
metrics
.
elementCount
,
metrics
.
sampledOverlapRate
,
metrics
.
sampledContainmentRate
,
metrics
.
degenerateRate
,
metrics
.
averageAspectRatio
,
observation
.
fastest
.
variant
,
observation
.
fastestBvh
.
variant
,
candidateRule
(
observation
)));
}
System
.
out
.
println
(
"---------------------------------------------------------------------------------------------------------------"
);
System
.
out
.
println
(
"Candidate rules are prelim; check them against real CityGML models."
);
System
.
out
.
println
();
}
private
static
String
candidateRule
(
Observation
observation
)
{
BvhInputMetricsCollector
.
Metrics
metrics
=
observation
.
metrics
;
if
(
"BRUTE_FORCE"
.
equals
(
observation
.
fastest
.
variant
))
{
return
"consider brute below/near this n"
;
}
if
(
metrics
.
sampledOverlapRate
<
0.01
&&
metrics
.
elementCount
>
200
)
{
return
"low overlap: BVH likely"
;
}
if
(
metrics
.
degenerateRate
>
0.50
||
metrics
.
averageAspectRatio
>
50.0
)
{
return
"flat/skinny: prefer measured stable BVH"
;
}
if
(
metrics
.
sampledOverlapRate
>
0.10
)
{
return
"high overlap: compare BVH vs brute"
;
}
return
"citylike/mixed: prefer fastest BVH candidate"
;
}
private
static
final
class
Scenario
{
final
String
name
;
final
Geometry
geometry
;
Scenario
(
String
name
,
Geometry
geometry
)
{
this
.
name
=
name
;
this
.
geometry
=
geometry
;
}
}
private
static
final
class
Observation
{
final
BvhInputMetricsCollector
.
Metrics
metrics
;
final
BvhPerformanceTestSupport
.
Measurement
fastest
;
final
BvhPerformanceTestSupport
.
Measurement
fastestBvh
;
final
BvhPerformanceTestSupport
.
Measurement
bruteForce
;
Observation
(
BvhInputMetricsCollector
.
Metrics
metrics
,
BvhPerformanceTestSupport
.
Measurement
fastest
,
BvhPerformanceTestSupport
.
Measurement
fastestBvh
,
BvhPerformanceTestSupport
.
Measurement
bruteForce
)
{
this
.
metrics
=
metrics
;
this
.
fastest
=
fastest
;
this
.
fastestBvh
=
fastestBvh
;
this
.
bruteForce
=
bruteForce
;
}
}
}
CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/bht/NestedRingCheckBvhPerformanceTest.java
0 → 100644
View file @
cd89dc71
package
de.hft.stuttgart.citydoctor2.checks.bht
;
import
static
org
.
junit
.
Assert
.
assertEquals
;
import
java.util.ArrayList
;
import
java.util.List
;
import
org.junit.jupiter.api.Tag
;
import
org.junit.jupiter.api.Test
;
import
de.hft.stuttgart.citydoctor2.check.CheckResult
;
import
de.hft.stuttgart.citydoctor2.check.ResultStatus
;
import
de.hft.stuttgart.citydoctor2.checks.geometry.NestedRingsCheck
;
import
de.hft.stuttgart.citydoctor2.datastructure.ConcretePolygon
;
import
de.hft.stuttgart.citydoctor2.datastructure.bht.SplitStrategy
;
@Tag
(
"performance"
)
public
class
NestedRingCheckBvhPerformanceTest
{
@Test
public
void
compareOldAabbAndBvhVariantsOnSyntheticNestedRings
()
{
measureScenario
(
"nested-disjoint"
,
SyntheticNestedRingGeometryFactory
.
manyDisjointInnerRings
(
1_000
));
measureScenario
(
"nested-one-pair"
,
SyntheticNestedRingGeometryFactory
.
oneNestedPairAmongMany
(
1_000
));
measureScenario
(
"nested-concentric"
,
SyntheticNestedRingGeometryFactory
.
concentricNestedRings
(
300
));
measureScenario
(
"nested-overlap-no-error"
,
SyntheticNestedRingGeometryFactory
.
overlappingAabbsButNotNested
(
1_000
));
measureScenario
(
"nested-clustered"
,
SyntheticNestedRingGeometryFactory
.
clusteredInnerRings
(
16
,
80
));
measureScenario
(
"nested-clustered-pair"
,
SyntheticNestedRingGeometryFactory
.
clusteredInnerRingsWithNestedPair
(
16
,
80
));
}
private
static
void
measureScenario
(
String
scenario
,
ConcretePolygon
polygon
)
{
int
ringCount
=
polygon
.
getInnerRings
().
size
();
BvhInputMetricsCollector
.
print
(
BvhInputMetricsCollector
.
forRings
(
scenario
,
polygon
.
getInnerRings
()));
List
<
BvhPerformanceTestSupport
.
Measurement
>
measurements
=
new
ArrayList
<>();
BvhPerformanceTestSupport
.
Measurement
oldMeasurement
=
BvhPerformanceTestSupport
.
measure
(
scenario
,
NestedRingsCheck
.
Variant
.
OLD
.
name
(),
ringCount
,
()
->
runCheck
(
polygon
,
NestedRingsCheck
.
Variant
.
OLD
));
measurements
.
add
(
oldMeasurement
);
BvhPerformanceTestSupport
.
Measurement
aabbMeasurement
=
BvhPerformanceTestSupport
.
measure
(
scenario
,
NestedRingsCheck
.
Variant
.
AABB_FILTER
.
name
(),
ringCount
,
()
->
runCheck
(
polygon
,
NestedRingsCheck
.
Variant
.
AABB_FILTER
));
measurements
.
add
(
aabbMeasurement
);
assertEquals
(
"Nested result differs for "
+
scenario
+
" / AABB_FILTER"
,
oldMeasurement
.
resultCount
,
aabbMeasurement
.
resultCount
);
for
(
SplitStrategy
strategy
:
BvhPerformanceTestSupport
.
concreteStrategies
())
{
NestedRingsCheck
.
Variant
variant
=
variantFor
(
strategy
);
BvhPerformanceTestSupport
.
Measurement
bvhMeasurement
=
BvhPerformanceTestSupport
.
measure
(
scenario
,
variant
.
name
(),
ringCount
,
()
->
runCheck
(
polygon
,
variant
));
measurements
.
add
(
bvhMeasurement
);
assertEquals
(
"Nested result differs for "
+
scenario
+
" / "
+
variant
,
oldMeasurement
.
resultCount
,
bvhMeasurement
.
resultCount
);
}
BvhPerformanceTestSupport
.
printScenarioSummary
(
scenario
,
measurements
);
}
private
static
int
runCheck
(
ConcretePolygon
polygon
,
NestedRingsCheck
.
Variant
variant
)
{
NestedRingsCheck
check
=
new
NestedRingsCheck
(
variant
);
check
.
check
(
polygon
);
CheckResult
result
=
polygon
.
getCheckResult
(
check
);
return
result
.
getResultStatus
()
==
ResultStatus
.
ERROR
?
1
:
0
;
}
private
static
NestedRingsCheck
.
Variant
variantFor
(
SplitStrategy
strategy
)
{
switch
(
strategy
)
{
case
BINARY_OBJECT_MEDIAN:
return
NestedRingsCheck
.
Variant
.
BVH_BINARY_OBJECT_MEDIAN
;
case
BINARY_OBJECT_MEAN:
return
NestedRingsCheck
.
Variant
.
BVH_BINARY_OBJECT_MEAN
;
case
BINARY_SPATIAL_MEDIAN:
return
NestedRingsCheck
.
Variant
.
BVH_BINARY_SPATIAL_MEDIAN
;
case
OCTONARY_OBJECT_MEDIAN:
return
NestedRingsCheck
.
Variant
.
BVH_OCTONARY_OBJECT_MEDIAN
;
case
OCTONARY_OBJECT_MEAN:
return
NestedRingsCheck
.
Variant
.
BVH_OCTONARY_OBJECT_MEAN
;
case
OCTONARY_SPATIAL_MEDIAN:
return
NestedRingsCheck
.
Variant
.
BVH_OCTONARY_SPATIAL_MEDIAN
;
case
AUTO:
default
:
throw
new
IllegalArgumentException
(
"Unsupported nested-ring BVH strategy: "
+
strategy
);
}
}
}
CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/bht/RingSelfIntCheckBvhPerformanceTest.java
0 → 100644
View file @
cd89dc71
package
de.hft.stuttgart.citydoctor2.checks.bht
;
import
static
org
.
junit
.
Assert
.
assertEquals
;
import
java.util.ArrayList
;
import
java.util.Collections
;
import
java.util.List
;
import
org.junit.jupiter.api.Tag
;
import
org.junit.jupiter.api.Test
;
import
de.hft.stuttgart.citydoctor2.check.CheckResult
;
import
de.hft.stuttgart.citydoctor2.check.ResultStatus
;
import
de.hft.stuttgart.citydoctor2.checks.geometry.RingSelfIntCheck
;
import
de.hft.stuttgart.citydoctor2.datastructure.Geometry
;
import
de.hft.stuttgart.citydoctor2.datastructure.LinearRing
;
import
de.hft.stuttgart.citydoctor2.datastructure.Polygon
;
import
de.hft.stuttgart.citydoctor2.datastructure.bht.SplitStrategy
;
@Tag
(
"performance"
)
public
class
RingSelfIntCheckBvhPerformanceTest
{
private
static
final
double
EPSILON
=
0.001
;
@Test
public
void
compareOldAndBvhVariantsOnSyntheticRings
()
{
measureScenario
(
"rsi-small"
,
SyntheticRingGeometryFactory
.
performanceRingGeometry
(
200
,
100
,
EPSILON
));
measureScenario
(
"rsi-medium"
,
SyntheticRingGeometryFactory
.
performanceRingGeometry
(
1_000
,
500
,
EPSILON
));
measureScenario
(
"rsi-large"
,
SyntheticRingGeometryFactory
.
performanceRingGeometry
(
5_000
,
2_000
,
EPSILON
));
}
private
static
void
measureScenario
(
String
scenario
,
Geometry
geometry
)
{
int
edgeCount
=
countEdges
(
geometry
);
BvhInputMetricsCollector
.
print
(
BvhInputMetricsCollector
.
forRings
(
scenario
,
collectExteriorRings
(
geometry
)));
List
<
BvhPerformanceTestSupport
.
Measurement
>
measurements
=
new
ArrayList
<>();
BvhPerformanceTestSupport
.
Measurement
oldMeasurement
=
BvhPerformanceTestSupport
.
measure
(
scenario
,
RingSelfIntCheck
.
Variant
.
OLD
.
name
(),
edgeCount
,
()
->
runCheck
(
geometry
,
RingSelfIntCheck
.
Variant
.
OLD
));
measurements
.
add
(
oldMeasurement
);
for
(
SplitStrategy
strategy
:
BvhPerformanceTestSupport
.
concreteStrategies
())
{
RingSelfIntCheck
.
Variant
variant
=
variantFor
(
strategy
);
BvhPerformanceTestSupport
.
Measurement
bvhMeasurement
=
BvhPerformanceTestSupport
.
measure
(
scenario
,
variant
.
name
(),
edgeCount
,
()
->
runCheck
(
geometry
,
variant
));
measurements
.
add
(
bvhMeasurement
);
assertEquals
(
"RSI result differs for "
+
scenario
+
" / "
+
variant
,
oldMeasurement
.
resultCount
,
bvhMeasurement
.
resultCount
);
}
BvhPerformanceTestSupport
.
printScenarioSummary
(
scenario
,
measurements
);
}
private
static
int
runCheck
(
Geometry
geometry
,
RingSelfIntCheck
.
Variant
variant
)
{
int
errorCount
=
0
;
for
(
Polygon
polygon
:
geometry
.
getPolygons
())
{
RingSelfIntCheck
check
=
new
RingSelfIntCheck
(
variant
);
check
.
init
(
Collections
.
singletonMap
(
"minVertexDistance"
,
String
.
valueOf
(
EPSILON
)),
null
);
check
.
check
(
polygon
.
getExteriorRing
());
CheckResult
result
=
polygon
.
getExteriorRing
().
getCheckResult
(
check
);
if
(
result
.
getResultStatus
()
==
ResultStatus
.
ERROR
)
{
errorCount
++;
}
}
return
errorCount
;
}
private
static
int
countEdges
(
Geometry
geometry
)
{
int
edgeCount
=
0
;
for
(
Polygon
polygon
:
geometry
.
getPolygons
())
{
LinearRing
ring
=
polygon
.
getExteriorRing
();
edgeCount
+=
Math
.
max
(
0
,
ring
.
getVertices
().
size
()
-
1
);
}
return
edgeCount
;
}
private
static
List
<
LinearRing
>
collectExteriorRings
(
Geometry
geometry
)
{
List
<
LinearRing
>
rings
=
new
ArrayList
<>();
for
(
Polygon
polygon
:
geometry
.
getPolygons
())
{
rings
.
add
(
polygon
.
getExteriorRing
());
}
return
rings
;
}
private
static
RingSelfIntCheck
.
Variant
variantFor
(
SplitStrategy
strategy
)
{
switch
(
strategy
)
{
case
BINARY_OBJECT_MEDIAN:
return
RingSelfIntCheck
.
Variant
.
BVH_BINARY_OBJECT_MEDIAN
;
case
BINARY_OBJECT_MEAN:
return
RingSelfIntCheck
.
Variant
.
BVH_BINARY_OBJECT_MEAN
;
case
BINARY_SPATIAL_MEDIAN:
return
RingSelfIntCheck
.
Variant
.
BVH_BINARY_SPATIAL_MEDIAN
;
case
OCTONARY_OBJECT_MEDIAN:
return
RingSelfIntCheck
.
Variant
.
BVH_OCTONARY_OBJECT_MEDIAN
;
case
OCTONARY_OBJECT_MEAN:
return
RingSelfIntCheck
.
Variant
.
BVH_OCTONARY_OBJECT_MEAN
;
case
OCTONARY_SPATIAL_MEDIAN:
return
RingSelfIntCheck
.
Variant
.
BVH_OCTONARY_SPATIAL_MEDIAN
;
case
AUTO:
default
:
throw
new
IllegalArgumentException
(
"Unsupported ring-self-intersection BVH strategy: "
+
strategy
);
}
}
}
CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/bht/RingSelfIntCheckBvhVariantSyntheticTest.java
View file @
cd89dc71
...
...
@@ -13,15 +13,9 @@ import de.hft.stuttgart.citydoctor2.check.CheckError;
import
de.hft.stuttgart.citydoctor2.check.CheckResult
;
import
de.hft.stuttgart.citydoctor2.check.ResultStatus
;
import
de.hft.stuttgart.citydoctor2.checks.geometry.RingSelfIntCheck
;
import
de.hft.stuttgart.citydoctor2.datastructure.ConcretePolygon
;
import
de.hft.stuttgart.citydoctor2.datastructure.Geometry
;
import
de.hft.stuttgart.citydoctor2.datastructure.Geometry.Orientation
;
import
de.hft.stuttgart.citydoctor2.datastructure.GeometryType
;
import
de.hft.stuttgart.citydoctor2.datastructure.LinearRing
;
import
de.hft.stuttgart.citydoctor2.datastructure.LinearRing.LinearRingType
;
import
de.hft.stuttgart.citydoctor2.datastructure.Lod
;
import
de.hft.stuttgart.citydoctor2.datastructure.Polygon
;
import
de.hft.stuttgart.citydoctor2.datastructure.Vertex
;
public
class
RingSelfIntCheckBvhVariantSyntheticTest
{
...
...
@@ -29,7 +23,7 @@ public class RingSelfIntCheckBvhVariantSyntheticTest {
@Test
public
void
bvhVariantsMatchOriginalOnSyntheticRings
()
{
Geometry
oldGeometry
=
s
yntheticRingGeometry
(
);
Geometry
oldGeometry
=
S
yntheticRingGeometry
Factory
.
correctnessRingGeometry
(
EPSILON
);
List
<
LinearRing
>
oldRings
=
collectExteriorRings
(
oldGeometry
);
List
<
CheckSummary
>
oldSummaries
=
new
ArrayList
<>();
...
...
@@ -38,7 +32,7 @@ public class RingSelfIntCheckBvhVariantSyntheticTest {
}
for
(
RingSelfIntCheck
.
Variant
bvhVariant
:
bvhVariants
())
{
Geometry
bvhGeometry
=
s
yntheticRingGeometry
(
);
Geometry
bvhGeometry
=
S
yntheticRingGeometry
Factory
.
correctnessRingGeometry
(
EPSILON
);
List
<
LinearRing
>
bvhRings
=
collectExteriorRings
(
bvhGeometry
);
assertEquals
(
oldRings
.
size
(),
bvhRings
.
size
());
...
...
@@ -78,17 +72,6 @@ public class RingSelfIntCheckBvhVariantSyntheticTest {
return
new
CheckSummary
(
result
.
getResultStatus
(),
errorType
);
}
private
static
Geometry
syntheticRingGeometry
()
{
Geometry
geometry
=
new
Geometry
(
GeometryType
.
SOLID
,
Lod
.
LOD2
,
Orientation
.
OUTWARD
);
addRingPolygon
(
geometry
,
rectangle
());
addRingPolygon
(
geometry
,
bowTie
());
addRingPolygon
(
geometry
,
pointNearEdge
());
addRingPolygon
(
geometry
,
largeConvexRing
(
80
,
20.0
,
40.0
,
0.0
));
addRingPolygon
(
geometry
,
zigZagCorridor
(
30
));
geometry
.
updateEdgesAndVertices
();
return
geometry
;
}
private
static
List
<
LinearRing
>
collectExteriorRings
(
Geometry
geometry
)
{
List
<
LinearRing
>
rings
=
new
ArrayList
<>();
for
(
Polygon
polygon
:
geometry
.
getPolygons
())
{
...
...
@@ -97,91 +80,6 @@ public class RingSelfIntCheckBvhVariantSyntheticTest {
return
rings
;
}
private
static
void
addRingPolygon
(
Geometry
geometry
,
double
[][]
coordinates
)
{
ConcretePolygon
polygon
=
new
ConcretePolygon
();
LinearRing
ring
=
new
LinearRing
(
LinearRingType
.
EXTERIOR
);
polygon
.
setExteriorRing
(
ring
);
geometry
.
addPolygon
(
polygon
);
Vertex
firstVertex
=
null
;
for
(
int
i
=
0
;
i
<
coordinates
.
length
;
i
++)
{
double
[]
coordinate
=
coordinates
[
i
];
if
(
i
==
coordinates
.
length
-
1
&&
sameCoordinate
(
coordinate
,
coordinates
[
0
]))
{
ring
.
addVertex
(
firstVertex
);
continue
;
}
Vertex
vertex
=
new
Vertex
(
coordinate
[
0
],
coordinate
[
1
],
coordinate
[
2
]);
if
(
i
==
0
)
{
firstVertex
=
vertex
;
}
ring
.
addVertex
(
vertex
);
}
}
private
static
boolean
sameCoordinate
(
double
[]
a
,
double
[]
b
)
{
return
Double
.
compare
(
a
[
0
],
b
[
0
])
==
0
&&
Double
.
compare
(
a
[
1
],
b
[
1
])
==
0
&&
Double
.
compare
(
a
[
2
],
b
[
2
])
==
0
;
}
private
static
double
[][]
rectangle
()
{
return
new
double
[][]
{
{
0.0
,
0.0
,
0.0
},
{
10.0
,
0.0
,
0.0
},
{
10.0
,
10.0
,
0.0
},
{
0.0
,
10.0
,
0.0
},
{
0.0
,
0.0
,
0.0
}
};
}
private
static
double
[][]
bowTie
()
{
return
new
double
[][]
{
{
20.0
,
0.0
,
0.0
},
{
30.0
,
10.0
,
0.0
},
{
30.0
,
0.0
,
0.0
},
{
20.0
,
10.0
,
0.0
},
{
20.0
,
0.0
,
0.0
}
};
}
private
static
double
[][]
pointNearEdge
()
{
return
new
double
[][]
{
{
40.0
,
0.0
,
0.0
},
{
50.0
,
0.0
,
0.0
},
{
50.0
,
10.0
,
0.0
},
{
45.0
,
EPSILON
*
0.5
,
0.0
},
{
40.0
,
10.0
,
0.0
},
{
40.0
,
0.0
,
0.0
}
};
}
private
static
double
[][]
largeConvexRing
(
int
vertexCount
,
double
centerX
,
double
centerY
,
double
z
)
{
double
[][]
coordinates
=
new
double
[
vertexCount
+
1
][
3
];
for
(
int
i
=
0
;
i
<
vertexCount
;
i
++)
{
double
angle
=
2.0
*
Math
.
PI
*
i
/
vertexCount
;
coordinates
[
i
][
0
]
=
centerX
+
Math
.
cos
(
angle
)
*
8.0
;
coordinates
[
i
][
1
]
=
centerY
+
Math
.
sin
(
angle
)
*
5.0
;
coordinates
[
i
][
2
]
=
z
;
}
coordinates
[
vertexCount
][
0
]
=
coordinates
[
0
][
0
];
coordinates
[
vertexCount
][
1
]
=
coordinates
[
0
][
1
];
coordinates
[
vertexCount
][
2
]
=
coordinates
[
0
][
2
];
return
coordinates
;
}
private
static
double
[][]
zigZagCorridor
(
int
segments
)
{
double
[][]
coordinates
=
new
double
[(
segments
*
2
)
+
3
][
3
];
int
index
=
0
;
for
(
int
i
=
0
;
i
<=
segments
;
i
++)
{
coordinates
[
index
++]
=
new
double
[]
{
60.0
+
i
,
i
%
2
==
0
?
0.0
:
1.0
,
0.0
};
}
for
(
int
i
=
segments
;
i
>=
0
;
i
--)
{
coordinates
[
index
++]
=
new
double
[]
{
60.0
+
i
,
i
%
2
==
0
?
4.0
:
5.0
,
0.0
};
}
coordinates
[
index
]
=
new
double
[]
{
60.0
,
0.0
,
0.0
};
return
coordinates
;
}
private
static
final
class
CheckSummary
{
final
ResultStatus
status
;
final
Class
<?>
errorType
;
...
...
CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/bht/SolidSelfIntersectionBVHUtilTest.java
View file @
cd89dc71
...
...
@@ -58,8 +58,8 @@ public class SolidSelfIntersectionBVHUtilTest {
);
Building
building
=
m
.
getBuildings
().
findFirst
().
orElseThrow
();
Geometry
g
=
building
.
getGeometry
(
GeometryType
.
SOLID
,
Lod
.
LOD2
);
assertNotNull
(
"Expected SOLID
LOD2
geometry in test model"
,
g
);
Geometry
g
=
getSolidGeometry
(
building
);
assertNotNull
(
"Expected SOLID geometry in test model"
,
g
);
List
<
Polygon
>
polys
=
g
.
getPolygons
();
assertNotNull
(
polys
);
...
...
@@ -78,4 +78,12 @@ public class SolidSelfIntersectionBVHUtilTest {
assertTrue
(
"No self-intersections expected for SolidSelfIntTest1.gml"
,
intersections
.
isEmpty
());
}
private
static
Geometry
getSolidGeometry
(
Building
building
)
{
Geometry
lod2
=
building
.
getGeometry
(
GeometryType
.
SOLID
,
Lod
.
LOD2
);
if
(
lod2
!=
null
)
{
return
lod2
;
}
return
building
.
getGeometry
(
GeometryType
.
SOLID
,
Lod
.
LOD1
);
}
}
CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/bht/SolidSelfIntersectionBvhPerformanceTest.java
0 → 100644
View file @
cd89dc71
package
de.hft.stuttgart.citydoctor2.checks.bht
;
import
static
org
.
junit
.
Assert
.
assertEquals
;
import
java.util.ArrayList
;
import
java.util.List
;
import
org.junit.jupiter.api.Tag
;
import
org.junit.jupiter.api.Test
;
import
de.hft.stuttgart.citydoctor2.checks.util.SelfIntersectionUtil
;
import
de.hft.stuttgart.citydoctor2.datastructure.Geometry
;
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.datastructure.bht.SplitStrategy
;
@Tag
(
"performance"
)
public
class
SolidSelfIntersectionBvhPerformanceTest
{
private
static
final
double
DELTA
=
0.001
;
@Test
public
void
compareBruteForceAndBvhVariantsOnSyntheticSolids
()
{
measureScenario
(
"ssi-separated-grid"
,
SyntheticSolidGeometryFactory
.
separatedBoxGrid
(
Lod
.
LOD2
,
12
,
12
));
measureScenario
(
"ssi-overlapping-grid"
,
SyntheticSolidGeometryFactory
.
overlappingBoxGrid
(
Lod
.
LOD2
,
12
,
12
));
measureScenario
(
"ssi-dense-clusters"
,
SyntheticSolidGeometryFactory
.
denseBoxClusters
(
Lod
.
LOD2
,
8
,
20
));
measureScenario
(
"ssi-long-thin-slabs"
,
SyntheticSolidGeometryFactory
.
longThinSlabs
(
Lod
.
LOD2
,
160
));
measureScenario
(
"ssi-flat-box-grid"
,
SyntheticSolidGeometryFactory
.
flatBoxGrid
(
Lod
.
LOD2
,
200
));
measureScenario
(
"ssi-citylike-mixed"
,
SyntheticCityGmlLikeGeometryFactory
.
mixedUrbanDistrict
(
Lod
.
LOD2
,
5
,
4
));
measureScenario
(
"ssi-citylike-courtyard"
,
SyntheticCityGmlLikeGeometryFactory
.
courtyardDistrict
(
Lod
.
LOD2
,
16
));
measureScenario
(
"ssi-citylike-roofs"
,
SyntheticCityGmlLikeGeometryFactory
.
variedRoofDistrict
(
Lod
.
LOD2
,
180
));
}
private
static
void
measureScenario
(
String
scenario
,
Geometry
geometry
)
{
int
polygonCount
=
geometry
.
getPolygons
().
size
();
BvhInputMetricsCollector
.
print
(
BvhInputMetricsCollector
.
forPolygons
(
scenario
,
geometry
.
getPolygons
()));
List
<
BvhPerformanceTestSupport
.
Measurement
>
measurements
=
new
ArrayList
<>();
BvhPerformanceTestSupport
.
Measurement
bruteForce
=
BvhPerformanceTestSupport
.
measure
(
scenario
,
"BRUTE_FORCE"
,
polygonCount
,
()
->
SelfIntersectionUtil
.
calculateSolidSelfIntersection0
(
geometry
,
DELTA
).
size
());
measurements
.
add
(
bruteForce
);
for
(
SplitStrategy
strategy
:
BvhPerformanceTestSupport
.
concreteStrategies
())
{
BvhPerformanceTestSupport
.
Measurement
bvhMeasurement
=
BvhPerformanceTestSupport
.
measure
(
scenario
,
strategy
.
name
(),
polygonCount
,
()
->
calculateWithTree
(
geometry
,
strategy
));
measurements
.
add
(
bvhMeasurement
);
assertEquals
(
"SSI result count differs for "
+
scenario
+
" / "
+
strategy
,
bruteForce
.
resultCount
,
bvhMeasurement
.
resultCount
);
}
BvhPerformanceTestSupport
.
printScenarioSummary
(
scenario
,
measurements
);
}
private
static
int
calculateWithTree
(
Geometry
geometry
,
SplitStrategy
strategy
)
{
BoundingVolumeHierarchyTree
<
Polygon
>
tree
=
BoundingVolumeHierarchyTree
.
newWithStrategy
(
geometry
.
getPolygons
(),
polygon
->
AABB
.
of
(
polygon
.
getOriginal
()),
strategy
);
return
SelfIntersectionUtil
.
calculateSolidSelfIntersection
(
geometry
,
DELTA
,
tree
).
size
();
}
}
CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/bht/SolidSelfIntersectionOldVsNewTest.java
View file @
cd89dc71
...
...
@@ -90,10 +90,6 @@ public class SolidSelfIntersectionOldVsNewTest {
System
.
out
.
println
(
"oldTreeRes.size() = "
+
oldTreeRes
.
size
());
System
.
out
.
println
(
"newRes.size() = "
+
newRes
.
size
());
assertFalse
(
"oldRes is empty for: "
+
gmlPath
,
oldRes
.
isEmpty
());
assertFalse
(
"oldTreeRes is empty for: "
+
gmlPath
,
oldTreeRes
.
isEmpty
());
assertFalse
(
"newRes is empty for: "
+
gmlPath
,
newRes
.
isEmpty
());
assertEquals
(
"Old vs oldTree differs for: "
+
gmlPath
,
oldRes
.
size
(),
oldTreeRes
.
size
());
...
...
CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/bht/SyntheticCityGmlLikeGeometryFactory.java
0 → 100644
View file @
cd89dc71
package
de.hft.stuttgart.citydoctor2.checks.bht
;
import
de.hft.stuttgart.citydoctor2.datastructure.ConcretePolygon
;
import
de.hft.stuttgart.citydoctor2.datastructure.Geometry
;
import
de.hft.stuttgart.citydoctor2.datastructure.Geometry.Orientation
;
import
de.hft.stuttgart.citydoctor2.datastructure.GeometryType
;
import
de.hft.stuttgart.citydoctor2.datastructure.LinearRing
;
import
de.hft.stuttgart.citydoctor2.datastructure.LinearRing.LinearRingType
;
import
de.hft.stuttgart.citydoctor2.datastructure.Lod
;
import
de.hft.stuttgart.citydoctor2.datastructure.Vertex
;
/**
* Synthetic but citygml-like geometry fixtures for BVH metric and performance
* experiments.
*
* The goal is not to serialize valid CityGML, but to mimic typical model
* structure more closely than pure grids: mixed building footprints, varying
* heights, roof-like tilted faces, block spacing, and courtyard arrangements.
*
* @author Numanoglu
*/
final
class
SyntheticCityGmlLikeGeometryFactory
{
private
SyntheticCityGmlLikeGeometryFactory
()
{
}
/**
* Creates several blocks with mixed compact, elongated, and roof-like buildings.
* Useful as a balanced city-district fixture with moderate spatial separation.
*/
static
Geometry
mixedUrbanDistrict
(
Lod
lod
,
int
blockColumns
,
int
blockRows
)
{
Geometry
geometry
=
newSolid
(
lod
);
for
(
int
blockX
=
0
;
blockX
<
blockColumns
;
blockX
++)
{
for
(
int
blockY
=
0
;
blockY
<
blockRows
;
blockY
++)
{
double
originX
=
blockX
*
55.0
;
double
originY
=
blockY
*
45.0
;
addMixedBlock
(
geometry
,
originX
,
originY
,
blockX
+
blockY
*
blockColumns
);
}
}
geometry
.
updateEdgesAndVertices
();
return
geometry
;
}
/**
* Creates block-perimeter buildings around open courtyards. This produces many
* nearby but not necessarily intersecting AABBs, closer to dense urban blocks.
*/
static
Geometry
courtyardDistrict
(
Lod
lod
,
int
blockCount
)
{
Geometry
geometry
=
newSolid
(
lod
);
for
(
int
block
=
0
;
block
<
blockCount
;
block
++)
{
double
originX
=
(
block
%
4
)
*
62.0
;
double
originY
=
(
block
/
4
)
*
58.0
;
double
height
=
9.0
+
(
block
%
3
)
*
2.5
;
addBox
(
geometry
,
originX
,
originY
,
0.0
,
44.0
,
7.0
,
height
);
addBox
(
geometry
,
originX
,
originY
+
30.0
,
0.0
,
44.0
,
7.0
,
height
+
1.5
);
addBox
(
geometry
,
originX
,
originY
+
7.0
,
0.0
,
7.0
,
23.0
,
height
-
1.0
);
addBox
(
geometry
,
originX
+
37.0
,
originY
+
7.0
,
0.0
,
7.0
,
23.0
,
height
+
0.5
);
}
geometry
.
updateEdgesAndVertices
();
return
geometry
;
}
/**
* Creates many small buildings with alternating flat and gabled roof shapes.
* Useful for a larger polygon count with city-like height and footprint jitter.
*/
static
Geometry
variedRoofDistrict
(
Lod
lod
,
int
buildingCount
)
{
Geometry
geometry
=
newSolid
(
lod
);
for
(
int
i
=
0
;
i
<
buildingCount
;
i
++)
{
double
x
=
(
i
%
12
)
*
13.0
+
((
i
/
12
)
%
2
)
*
4.0
;
double
y
=
(
i
/
12
)
*
11.0
;
double
width
=
7.0
+
(
i
%
4
)
*
1.3
;
double
depth
=
6.0
+
(
i
%
5
)
*
0.9
;
double
height
=
5.0
+
(
i
%
7
)
*
1.2
;
if
((
i
&
1
)
==
0
)
{
addGabledBuilding
(
geometry
,
x
,
y
,
0.0
,
width
,
depth
,
height
,
2.2
);
}
else
{
addBox
(
geometry
,
x
,
y
,
0.0
,
width
,
depth
,
height
);
}
}
geometry
.
updateEdgesAndVertices
();
return
geometry
;
}
private
static
void
addMixedBlock
(
Geometry
geometry
,
double
originX
,
double
originY
,
int
seed
)
{
addBox
(
geometry
,
originX
,
originY
,
0.0
,
9.0
+
seed
%
3
,
11.0
,
8.0
+
seed
%
4
);
addGabledBuilding
(
geometry
,
originX
+
13.0
,
originY
+
2.0
,
0.0
,
12.0
,
8.0
,
7.0
+
seed
%
5
,
2.5
);
addBox
(
geometry
,
originX
+
30.0
,
originY
,
0.0
,
7.0
,
18.0
,
10.0
+
seed
%
6
);
addGabledBuilding
(
geometry
,
originX
+
4.0
,
originY
+
24.0
,
0.0
,
18.0
,
9.0
,
6.0
+
seed
%
4
,
2.0
);
addBox
(
geometry
,
originX
+
28.0
,
originY
+
24.0
,
0.0
,
14.0
,
12.0
,
5.0
+
seed
%
5
);
}
private
static
Geometry
newSolid
(
Lod
lod
)
{
return
new
Geometry
(
GeometryType
.
SOLID
,
lod
,
Orientation
.
OUTWARD
);
}
private
static
void
addGabledBuilding
(
Geometry
geometry
,
double
x
,
double
y
,
double
z
,
double
width
,
double
depth
,
double
wallHeight
,
double
roofHeight
)
{
Vertex
v000
=
new
Vertex
(
x
,
y
,
z
);
Vertex
v100
=
new
Vertex
(
x
+
width
,
y
,
z
);
Vertex
v110
=
new
Vertex
(
x
+
width
,
y
+
depth
,
z
);
Vertex
v010
=
new
Vertex
(
x
,
y
+
depth
,
z
);
Vertex
v001
=
new
Vertex
(
x
,
y
,
z
+
wallHeight
);
Vertex
v101
=
new
Vertex
(
x
+
width
,
y
,
z
+
wallHeight
);
Vertex
v111
=
new
Vertex
(
x
+
width
,
y
+
depth
,
z
+
wallHeight
);
Vertex
v011
=
new
Vertex
(
x
,
y
+
depth
,
z
+
wallHeight
);
Vertex
ridge0
=
new
Vertex
(
x
+
width
*
0.5
,
y
,
z
+
wallHeight
+
roofHeight
);
Vertex
ridge1
=
new
Vertex
(
x
+
width
*
0.5
,
y
+
depth
,
z
+
wallHeight
+
roofHeight
);
addQuad
(
geometry
,
v000
,
v100
,
v110
,
v010
);
addQuad
(
geometry
,
v000
,
v001
,
v101
,
v100
);
addQuad
(
geometry
,
v100
,
v101
,
v111
,
v110
);
addQuad
(
geometry
,
v110
,
v111
,
v011
,
v010
);
addQuad
(
geometry
,
v010
,
v011
,
v001
,
v000
);
addQuad
(
geometry
,
v001
,
ridge0
,
ridge1
,
v011
);
addQuad
(
geometry
,
ridge0
,
v101
,
v111
,
ridge1
);
addTriangle
(
geometry
,
v001
,
v101
,
ridge0
);
addTriangle
(
geometry
,
v011
,
ridge1
,
v111
);
}
private
static
void
addBox
(
Geometry
geometry
,
double
x
,
double
y
,
double
z
,
double
width
,
double
depth
,
double
height
)
{
Vertex
v000
=
new
Vertex
(
x
,
y
,
z
);
Vertex
v100
=
new
Vertex
(
x
+
width
,
y
,
z
);
Vertex
v110
=
new
Vertex
(
x
+
width
,
y
+
depth
,
z
);
Vertex
v010
=
new
Vertex
(
x
,
y
+
depth
,
z
);
Vertex
v001
=
new
Vertex
(
x
,
y
,
z
+
height
);
Vertex
v101
=
new
Vertex
(
x
+
width
,
y
,
z
+
height
);
Vertex
v111
=
new
Vertex
(
x
+
width
,
y
+
depth
,
z
+
height
);
Vertex
v011
=
new
Vertex
(
x
,
y
+
depth
,
z
+
height
);
addQuad
(
geometry
,
v000
,
v100
,
v110
,
v010
);
addQuad
(
geometry
,
v001
,
v011
,
v111
,
v101
);
addQuad
(
geometry
,
v000
,
v001
,
v101
,
v100
);
addQuad
(
geometry
,
v100
,
v101
,
v111
,
v110
);
addQuad
(
geometry
,
v110
,
v111
,
v011
,
v010
);
addQuad
(
geometry
,
v010
,
v011
,
v001
,
v000
);
}
private
static
void
addQuad
(
Geometry
geometry
,
Vertex
a
,
Vertex
b
,
Vertex
c
,
Vertex
d
)
{
ConcretePolygon
polygon
=
new
ConcretePolygon
();
LinearRing
ring
=
new
LinearRing
(
LinearRingType
.
EXTERIOR
);
polygon
.
setExteriorRing
(
ring
);
geometry
.
addPolygon
(
polygon
);
ring
.
addVertex
(
a
);
ring
.
addVertex
(
b
);
ring
.
addVertex
(
c
);
ring
.
addVertex
(
d
);
ring
.
addVertex
(
a
);
}
private
static
void
addTriangle
(
Geometry
geometry
,
Vertex
a
,
Vertex
b
,
Vertex
c
)
{
ConcretePolygon
polygon
=
new
ConcretePolygon
();
LinearRing
ring
=
new
LinearRing
(
LinearRingType
.
EXTERIOR
);
polygon
.
setExteriorRing
(
ring
);
geometry
.
addPolygon
(
polygon
);
ring
.
addVertex
(
a
);
ring
.
addVertex
(
b
);
ring
.
addVertex
(
c
);
ring
.
addVertex
(
a
);
}
}
CityDoctorParent/CityDoctorValidation/src/test/java/de/hft/stuttgart/citydoctor2/checks/bht/SyntheticRingGeometryFactory.java
0 → 100644
View file @
cd89dc71
package
de.hft.stuttgart.citydoctor2.checks.bht
;
import
de.hft.stuttgart.citydoctor2.datastructure.ConcretePolygon
;
import
de.hft.stuttgart.citydoctor2.datastructure.Geometry
;
import
de.hft.stuttgart.citydoctor2.datastructure.Geometry.Orientation
;
import
de.hft.stuttgart.citydoctor2.datastructure.GeometryType
;
import
de.hft.stuttgart.citydoctor2.datastructure.LinearRing
;
import
de.hft.stuttgart.citydoctor2.datastructure.LinearRing.LinearRingType
;
import
de.hft.stuttgart.citydoctor2.datastructure.Lod
;
import
de.hft.stuttgart.citydoctor2.datastructure.Vertex
;
/**
* Test-fixture factory for RingSelfIntCheck BVH comparison tests.
*
* Scenarios:
* - rectangle:
* simple valid reference ring.
* - bowTie:
* direct segment crossing and therefore a clear self-intersection.
* - pointNearEdge:
* vertex close to a non-adjacent edge, useful for epsilon-sensitive checks.
* - largeConvexRing:
* many edges without self-intersection, useful for candidate-pruning cost.
* - zigZagCorridor:
* many nearby but non-crossing segments, useful as AABB broad-phase stress.
*
* @author Numanoglu
*/
final
class
SyntheticRingGeometryFactory
{
private
SyntheticRingGeometryFactory
()
{
}
static
Geometry
correctnessRingGeometry
(
double
epsilon
)
{
Geometry
geometry
=
new
Geometry
(
GeometryType
.
SOLID
,
Lod
.
LOD2
,
Orientation
.
OUTWARD
);
addRingPolygon
(
geometry
,
rectangle
());
addRingPolygon
(
geometry
,
bowTie
());
addRingPolygon
(
geometry
,
pointNearEdge
(
epsilon
));
addRingPolygon
(
geometry
,
largeConvexRing
(
80
,
20.0
,
40.0
,
0.0
));
addRingPolygon
(
geometry
,
zigZagCorridor
(
30
));
geometry
.
updateEdgesAndVertices
();
return
geometry
;
}
static
Geometry
performanceRingGeometry
(
int
convexVertexCount
,
int
zigZagSegments
,
double
epsilon
)
{
Geometry
geometry
=
new
Geometry
(
GeometryType
.
SOLID
,
Lod
.
LOD2
,
Orientation
.
OUTWARD
);
addRingPolygon
(
geometry
,
largeConvexRing
(
convexVertexCount
,
0.0
,
0.0
,
0.0
));
addRingPolygon
(
geometry
,
zigZagCorridor
(
zigZagSegments
));
addRingPolygon
(
geometry
,
pointNearEdge
(
epsilon
));
addRingPolygon
(
geometry
,
bowTie
());
geometry
.
updateEdgesAndVertices
();
return
geometry
;
}
private
static
void
addRingPolygon
(
Geometry
geometry
,
double
[][]
coordinates
)
{
ConcretePolygon
polygon
=
new
ConcretePolygon
();
LinearRing
ring
=
new
LinearRing
(
LinearRingType
.
EXTERIOR
);
polygon
.
setExteriorRing
(
ring
);
geometry
.
addPolygon
(
polygon
);
Vertex
firstVertex
=
null
;
for
(
int
i
=
0
;
i
<
coordinates
.
length
;
i
++)
{
double
[]
coordinate
=
coordinates
[
i
];
if
(
i
==
coordinates
.
length
-
1
&&
sameCoordinate
(
coordinate
,
coordinates
[
0
]))
{
ring
.
addVertex
(
firstVertex
);
continue
;
}
Vertex
vertex
=
new
Vertex
(
coordinate
[
0
],
coordinate
[
1
],
coordinate
[
2
]);
if
(
i
==
0
)
{
firstVertex
=
vertex
;
}
ring
.
addVertex
(
vertex
);
}
}
private
static
boolean
sameCoordinate
(
double
[]
a
,
double
[]
b
)
{
return
Double
.
compare
(
a
[
0
],
b
[
0
])
==
0
&&
Double
.
compare
(
a
[
1
],
b
[
1
])
==
0
&&
Double
.
compare
(
a
[
2
],
b
[
2
])
==
0
;
}
private
static
double
[][]
rectangle
()
{
return
new
double
[][]
{
{
0.0
,
0.0
,
0.0
},
{
10.0
,
0.0
,
0.0
},
{
10.0
,
10.0
,
0.0
},
{
0.0
,
10.0
,
0.0
},
{
0.0
,
0.0
,
0.0
}
};
}
private
static
double
[][]
bowTie
()
{
return
new
double
[][]
{
{
20.0
,
0.0
,
0.0
},
{
30.0
,
10.0
,
0.0
},
{
30.0
,
0.0
,
0.0
},
{
20.0
,
10.0
,
0.0
},
{
20.0
,
0.0
,
0.0
}
};
}
private
static
double
[][]
pointNearEdge
(
double
epsilon
)
{
return
new
double
[][]
{
{
40.0
,
0.0
,
0.0
},
{
50.0
,
0.0
,
0.0
},
{
50.0
,
10.0
,
0.0
},
{
45.0
,
epsilon
*
0.5
,
0.0
},
{
40.0
,
10.0
,
0.0
},
{
40.0
,
0.0
,
0.0
}
};
}
private
static
double
[][]
largeConvexRing
(
int
vertexCount
,
double
centerX
,
double
centerY
,
double
z
)
{
double
[][]
coordinates
=
new
double
[
vertexCount
+
1
][
3
];
for
(
int
i
=
0
;
i
<
vertexCount
;
i
++)
{
double
angle
=
2.0
*
Math
.
PI
*
i
/
vertexCount
;
coordinates
[
i
][
0
]
=
centerX
+
Math
.
cos
(
angle
)
*
8.0
;
coordinates
[
i
][
1
]
=
centerY
+
Math
.
sin
(
angle
)
*
5.0
;
coordinates
[
i
][
2
]
=
z
;
}
coordinates
[
vertexCount
][
0
]
=
coordinates
[
0
][
0
];
coordinates
[
vertexCount
][
1
]
=
coordinates
[
0
][
1
];
coordinates
[
vertexCount
][
2
]
=
coordinates
[
0
][
2
];
return
coordinates
;
}
private
static
double
[][]
zigZagCorridor
(
int
segments
)
{
double
[][]
coordinates
=
new
double
[(
segments
*
2
)
+
3
][
3
];
int
index
=
0
;
for
(
int
i
=
0
;
i
<=
segments
;
i
++)
{
coordinates
[
index
++]
=
new
double
[]
{
60.0
+
i
,
i
%
2
==
0
?
0.0
:
1.0
,
0.0
};
}
for
(
int
i
=
segments
;
i
>=
0
;
i
--)
{
coordinates
[
index
++]
=
new
double
[]
{
60.0
+
i
,
i
%
2
==
0
?
4.0
:
5.0
,
0.0
};
}
coordinates
[
index
]
=
new
double
[]
{
60.0
,
0.0
,
0.0
};
return
coordinates
;
}
}
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment