Phase 2: Core math (BezierCurve, Vec3d) and road data structures (RoadProfile, RoadSegment)
This commit is contained in:
201
src/main/kotlin/com/bnair/roadrunner/math/BezierCurve.kt
Normal file
201
src/main/kotlin/com/bnair/roadrunner/math/BezierCurve.kt
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
package com.bnair.roadrunner.math
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Cubic Bezier Curve in 3D space.
|
||||||
|
*
|
||||||
|
* The curve is defined by 4 control points:
|
||||||
|
* - p0: Start point (the curve passes through this)
|
||||||
|
* - p1: First control point (influences curve direction from start)
|
||||||
|
* - p2: Second control point (influences curve direction towards end)
|
||||||
|
* - p3: End point (the curve passes through this)
|
||||||
|
*
|
||||||
|
* The curve is parametrized by t in [0, 1].
|
||||||
|
*/
|
||||||
|
data class BezierCurve(
|
||||||
|
val p0: Vec3d,
|
||||||
|
val p1: Vec3d,
|
||||||
|
val p2: Vec3d,
|
||||||
|
val p3: Vec3d
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Evaluates the curve at parameter t (0 to 1).
|
||||||
|
* Uses the cubic Bezier formula: B(t) = (1-t)³P0 + 3(1-t)²tP1 + 3(1-t)t²P2 + t³P3
|
||||||
|
*/
|
||||||
|
fun evaluate(t: Double): Vec3d {
|
||||||
|
val u = 1.0 - t
|
||||||
|
val tt = t * t
|
||||||
|
val uu = u * u
|
||||||
|
val uuu = uu * u
|
||||||
|
val ttt = tt * t
|
||||||
|
|
||||||
|
return p0 * uuu +
|
||||||
|
p1 * (3.0 * uu * t) +
|
||||||
|
p2 * (3.0 * u * tt) +
|
||||||
|
p3 * ttt
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the tangent (first derivative) at parameter t.
|
||||||
|
* This gives the direction of the curve at that point.
|
||||||
|
* B'(t) = 3(1-t)²(P1-P0) + 6(1-t)t(P2-P1) + 3t²(P3-P2)
|
||||||
|
*/
|
||||||
|
fun tangent(t: Double): Vec3d {
|
||||||
|
val u = 1.0 - t
|
||||||
|
val uu = u * u
|
||||||
|
val tt = t * t
|
||||||
|
|
||||||
|
return (p1 - p0) * (3.0 * uu) +
|
||||||
|
(p2 - p1) * (6.0 * u * t) +
|
||||||
|
(p3 - p2) * (3.0 * tt)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes a normalized tangent (unit direction vector) at parameter t.
|
||||||
|
*/
|
||||||
|
fun direction(t: Double): Vec3d = tangent(t).normalize()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the second derivative (acceleration) at parameter t.
|
||||||
|
* Useful for calculating curvature and banking.
|
||||||
|
* B''(t) = 6(1-t)(P2-2P1+P0) + 6t(P3-2P2+P1)
|
||||||
|
*/
|
||||||
|
fun acceleration(t: Double): Vec3d {
|
||||||
|
val u = 1.0 - t
|
||||||
|
return (p2 - p1 * 2.0 + p0) * (6.0 * u) +
|
||||||
|
(p3 - p2 * 2.0 + p1) * (6.0 * t)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the curvature at parameter t.
|
||||||
|
* Curvature = |B'(t) × B''(t)| / |B'(t)|³
|
||||||
|
* Higher curvature = sharper turn = more banking needed.
|
||||||
|
*/
|
||||||
|
fun curvature(t: Double): Double {
|
||||||
|
val d1 = tangent(t)
|
||||||
|
val d2 = acceleration(t)
|
||||||
|
val cross = d1.cross(d2)
|
||||||
|
val d1Len = d1.length()
|
||||||
|
return if (d1Len > 0.0) cross.length() / (d1Len * d1Len * d1Len) else 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Samples the curve into discrete points.
|
||||||
|
* @param segments Number of segments (returns segments + 1 points)
|
||||||
|
* @return List of points along the curve
|
||||||
|
*/
|
||||||
|
fun sample(segments: Int): List<Vec3d> {
|
||||||
|
require(segments > 0) { "Segments must be positive" }
|
||||||
|
return (0..segments).map { i ->
|
||||||
|
evaluate(i.toDouble() / segments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Samples the curve adaptively based on curvature.
|
||||||
|
* Straighter sections get fewer samples, curves get more.
|
||||||
|
* @param minSegments Minimum number of segments
|
||||||
|
* @param maxSegments Maximum number of segments
|
||||||
|
* @param curvatureThreshold How sensitive to curvature changes
|
||||||
|
*/
|
||||||
|
fun sampleAdaptive(
|
||||||
|
minSegments: Int = 10,
|
||||||
|
maxSegments: Int = 100,
|
||||||
|
curvatureThreshold: Double = 0.1
|
||||||
|
): List<Vec3d> {
|
||||||
|
val points = mutableListOf<Vec3d>()
|
||||||
|
points.add(p0)
|
||||||
|
|
||||||
|
fun subdivide(tStart: Double, tEnd: Double, depth: Int) {
|
||||||
|
if (depth > 10) { // Prevent infinite recursion
|
||||||
|
points.add(evaluate(tEnd))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val tMid = (tStart + tEnd) / 2.0
|
||||||
|
val curveMid = curvature(tMid)
|
||||||
|
|
||||||
|
// If curvature is high or segment is large, subdivide
|
||||||
|
val segmentLength = (evaluate(tEnd) - evaluate(tStart)).length()
|
||||||
|
if (curveMid > curvatureThreshold && segmentLength > 1.0 && points.size < maxSegments) {
|
||||||
|
subdivide(tStart, tMid, depth + 1)
|
||||||
|
subdivide(tMid, tEnd, depth + 1)
|
||||||
|
} else {
|
||||||
|
points.add(evaluate(tEnd))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val initialSegments = minSegments.coerceAtLeast(4)
|
||||||
|
for (i in 0 until initialSegments) {
|
||||||
|
val tStart = i.toDouble() / initialSegments
|
||||||
|
val tEnd = (i + 1).toDouble() / initialSegments
|
||||||
|
subdivide(tStart, tEnd, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return points.distinct()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Approximates the total arc length of the curve.
|
||||||
|
* Uses numerical integration (sampling).
|
||||||
|
*/
|
||||||
|
fun arcLength(segments: Int = 100): Double {
|
||||||
|
var length = 0.0
|
||||||
|
var prev = p0
|
||||||
|
for (i in 1..segments) {
|
||||||
|
val curr = evaluate(i.toDouble() / segments)
|
||||||
|
length += (curr - prev).length()
|
||||||
|
prev = curr
|
||||||
|
}
|
||||||
|
return length
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new curve that smoothly continues from this one.
|
||||||
|
* The new curve starts at this curve's end with matching tangent.
|
||||||
|
* @param endPoint Where the new curve should end
|
||||||
|
* @param endTangentScale How far the control point extends (affects curve shape)
|
||||||
|
*/
|
||||||
|
fun continueTo(endPoint: Vec3d, endTangentScale: Double = 0.3): BezierCurve {
|
||||||
|
val startTangent = tangent(1.0).normalize()
|
||||||
|
val newP1 = p3 + startTangent * (p3 - p2).length() // Match incoming tangent
|
||||||
|
val curveLength = (endPoint - p3).length()
|
||||||
|
val newP2 = endPoint - (endPoint - newP1).normalize() * (curveLength * endTangentScale)
|
||||||
|
return BezierCurve(p3, newP1, newP2, endPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Creates a simple straight line as a degenerate Bezier curve.
|
||||||
|
*/
|
||||||
|
fun line(start: Vec3d, end: Vec3d): BezierCurve {
|
||||||
|
val third = (end - start) / 3.0
|
||||||
|
return BezierCurve(
|
||||||
|
start,
|
||||||
|
start + third,
|
||||||
|
end - third,
|
||||||
|
end
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a curve from start to end with specified start and end tangents.
|
||||||
|
* This is useful for connecting road segments with specific angles.
|
||||||
|
*/
|
||||||
|
fun fromTangents(
|
||||||
|
start: Vec3d,
|
||||||
|
startTangent: Vec3d,
|
||||||
|
end: Vec3d,
|
||||||
|
endTangent: Vec3d,
|
||||||
|
tangentScale: Double = 0.3
|
||||||
|
): BezierCurve {
|
||||||
|
val distance = (end - start).length()
|
||||||
|
val scale = distance * tangentScale
|
||||||
|
return BezierCurve(
|
||||||
|
start,
|
||||||
|
start + startTangent.normalize() * scale,
|
||||||
|
end - endTangent.normalize() * scale,
|
||||||
|
end
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/main/kotlin/com/bnair/roadrunner/math/Vec3d.kt
Normal file
63
src/main/kotlin/com/bnair/roadrunner/math/Vec3d.kt
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package com.bnair.roadrunner.math
|
||||||
|
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple 3D vector class for road calculations.
|
||||||
|
* Using our own to avoid loader-specific dependencies in core math.
|
||||||
|
*/
|
||||||
|
data class Vec3d(
|
||||||
|
val x: Double,
|
||||||
|
val y: Double,
|
||||||
|
val z: Double
|
||||||
|
) {
|
||||||
|
operator fun plus(other: Vec3d) = Vec3d(x + other.x, y + other.y, z + other.z)
|
||||||
|
operator fun minus(other: Vec3d) = Vec3d(x - other.x, y - other.y, z - other.z)
|
||||||
|
operator fun times(scalar: Double) = Vec3d(x * scalar, y * scalar, z * scalar)
|
||||||
|
operator fun div(scalar: Double) = Vec3d(x / scalar, y / scalar, z / scalar)
|
||||||
|
|
||||||
|
fun dot(other: Vec3d): Double = x * other.x + y * other.y + z * other.z
|
||||||
|
|
||||||
|
fun cross(other: Vec3d) = Vec3d(
|
||||||
|
y * other.z - z * other.y,
|
||||||
|
z * other.x - x * other.z,
|
||||||
|
x * other.y - y * other.x
|
||||||
|
)
|
||||||
|
|
||||||
|
fun length(): Double = sqrt(x * x + y * y + z * z)
|
||||||
|
|
||||||
|
fun lengthSquared(): Double = x * x + y * y + z * z
|
||||||
|
|
||||||
|
fun normalize(): Vec3d {
|
||||||
|
val len = length()
|
||||||
|
return if (len > 0.0) this / len else this
|
||||||
|
}
|
||||||
|
|
||||||
|
fun lerp(other: Vec3d, t: Double): Vec3d {
|
||||||
|
return this + (other - this) * t
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a perpendicular vector in the XZ plane (horizontal).
|
||||||
|
* Useful for calculating road width offsets.
|
||||||
|
*/
|
||||||
|
fun perpendicularXZ(): Vec3d {
|
||||||
|
return Vec3d(-z, 0.0, x).normalize()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts to block coordinates (floored integers).
|
||||||
|
*/
|
||||||
|
fun toBlockPos(): Triple<Int, Int, Int> {
|
||||||
|
return Triple(
|
||||||
|
kotlin.math.floor(x).toInt(),
|
||||||
|
kotlin.math.floor(y).toInt(),
|
||||||
|
kotlin.math.floor(z).toInt()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val ZERO = Vec3d(0.0, 0.0, 0.0)
|
||||||
|
val UP = Vec3d(0.0, 1.0, 0.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
131
src/main/kotlin/com/bnair/roadrunner/road/RoadProfile.kt
Normal file
131
src/main/kotlin/com/bnair/roadrunner/road/RoadProfile.kt
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
package com.bnair.roadrunner.road
|
||||||
|
|
||||||
|
import com.bnair.roadrunner.math.Vec3d
|
||||||
|
|
||||||
|
enum class LaneType {
|
||||||
|
DRIVING,
|
||||||
|
PUBLIC_TRANSPORT,
|
||||||
|
PEDESTRIAN,
|
||||||
|
SHOULDER,
|
||||||
|
MEDIAN_BARRIER,
|
||||||
|
MEDIAN_GRASS
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class TerrainMode {
|
||||||
|
FOLLOW_TERRAIN,
|
||||||
|
FLAT_CUT_FILL
|
||||||
|
}
|
||||||
|
|
||||||
|
data class LaneDefinition(
|
||||||
|
val type: LaneType,
|
||||||
|
val width: Double,
|
||||||
|
val material: String = when (type) {
|
||||||
|
LaneType.DRIVING -> "minecraft:gray_concrete"
|
||||||
|
LaneType.PUBLIC_TRANSPORT -> "minecraft:yellow_concrete"
|
||||||
|
LaneType.PEDESTRIAN -> "minecraft:stone_bricks"
|
||||||
|
LaneType.SHOULDER -> "minecraft:gravel"
|
||||||
|
LaneType.MEDIAN_BARRIER -> "minecraft:stone_brick_wall"
|
||||||
|
LaneType.MEDIAN_GRASS -> "minecraft:grass_block"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RoadProfile(
|
||||||
|
val lanesLeft: List<LaneDefinition>,
|
||||||
|
val lanesRight: List<LaneDefinition>,
|
||||||
|
val median: LaneDefinition?,
|
||||||
|
val terrainMode: TerrainMode = TerrainMode.FLAT_CUT_FILL,
|
||||||
|
val elevationOffset: Double = 0.0,
|
||||||
|
val bankingEnabled: Boolean = true,
|
||||||
|
val maxBankingAngle: Double = 15.0
|
||||||
|
) {
|
||||||
|
val totalWidth: Double
|
||||||
|
get() {
|
||||||
|
val leftWidth = lanesLeft.sumOf { it.width }
|
||||||
|
val rightWidth = lanesRight.sumOf { it.width }
|
||||||
|
val medianWidth = median?.width ?: 0.0
|
||||||
|
return leftWidth + rightWidth + medianWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getOffsetForLane(side: Side, laneIndex: Int): Double {
|
||||||
|
val medianHalfWidth = (median?.width ?: 0.0) / 2.0
|
||||||
|
|
||||||
|
return when (side) {
|
||||||
|
Side.LEFT -> {
|
||||||
|
var offset = -medianHalfWidth
|
||||||
|
for (i in 0 until laneIndex) {
|
||||||
|
offset -= lanesLeft[i].width
|
||||||
|
}
|
||||||
|
offset - lanesLeft[laneIndex].width / 2.0
|
||||||
|
}
|
||||||
|
Side.RIGHT -> {
|
||||||
|
var offset = medianHalfWidth
|
||||||
|
for (i in 0 until laneIndex) {
|
||||||
|
offset += lanesRight[i].width
|
||||||
|
}
|
||||||
|
offset + lanesRight[laneIndex].width / 2.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Side { LEFT, RIGHT }
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun singleLaneEachWay(laneWidth: Double = 3.5): RoadProfile {
|
||||||
|
return RoadProfile(
|
||||||
|
lanesLeft = listOf(LaneDefinition(LaneType.DRIVING, laneWidth)),
|
||||||
|
lanesRight = listOf(LaneDefinition(LaneType.DRIVING, laneWidth)),
|
||||||
|
median = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun highway(
|
||||||
|
lanesPerSide: Int = 2,
|
||||||
|
laneWidth: Double = 3.5,
|
||||||
|
shoulderWidth: Double = 2.0,
|
||||||
|
medianWidth: Double = 3.0,
|
||||||
|
medianType: LaneType = LaneType.MEDIAN_BARRIER
|
||||||
|
): RoadProfile {
|
||||||
|
val sideLanes = buildList {
|
||||||
|
add(LaneDefinition(LaneType.SHOULDER, shoulderWidth))
|
||||||
|
repeat(lanesPerSide) {
|
||||||
|
add(LaneDefinition(LaneType.DRIVING, laneWidth))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return RoadProfile(
|
||||||
|
lanesLeft = sideLanes.reversed(),
|
||||||
|
lanesRight = sideLanes,
|
||||||
|
median = LaneDefinition(medianType, medianWidth)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun withPublicTransport(
|
||||||
|
drivingLanes: Int = 1,
|
||||||
|
laneWidth: Double = 3.5,
|
||||||
|
busLaneWidth: Double = 3.5,
|
||||||
|
pedestrianWidth: Double = 2.0
|
||||||
|
): RoadProfile {
|
||||||
|
val leftLanes = buildList {
|
||||||
|
add(LaneDefinition(LaneType.PEDESTRIAN, pedestrianWidth))
|
||||||
|
add(LaneDefinition(LaneType.PUBLIC_TRANSPORT, busLaneWidth))
|
||||||
|
repeat(drivingLanes) {
|
||||||
|
add(LaneDefinition(LaneType.DRIVING, laneWidth))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val rightLanes = buildList {
|
||||||
|
repeat(drivingLanes) {
|
||||||
|
add(LaneDefinition(LaneType.DRIVING, laneWidth))
|
||||||
|
}
|
||||||
|
add(LaneDefinition(LaneType.PUBLIC_TRANSPORT, busLaneWidth))
|
||||||
|
add(LaneDefinition(LaneType.PEDESTRIAN, pedestrianWidth))
|
||||||
|
}
|
||||||
|
|
||||||
|
return RoadProfile(
|
||||||
|
lanesLeft = leftLanes.reversed(),
|
||||||
|
lanesRight = rightLanes,
|
||||||
|
median = LaneDefinition(LaneType.MEDIAN_GRASS, 2.0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
149
src/main/kotlin/com/bnair/roadrunner/road/RoadSegment.kt
Normal file
149
src/main/kotlin/com/bnair/roadrunner/road/RoadSegment.kt
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
package com.bnair.roadrunner.road
|
||||||
|
|
||||||
|
import com.bnair.roadrunner.math.BezierCurve
|
||||||
|
import com.bnair.roadrunner.math.Vec3d
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.atan2
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.sin
|
||||||
|
|
||||||
|
data class RoadSegment(
|
||||||
|
val curve: BezierCurve,
|
||||||
|
val profile: RoadProfile,
|
||||||
|
val id: String = java.util.UUID.randomUUID().toString()
|
||||||
|
) {
|
||||||
|
fun generateBlocks(samplesPerBlock: Double = 0.5): List<RoadBlock> {
|
||||||
|
val blocks = mutableListOf<RoadBlock>()
|
||||||
|
val arcLen = curve.arcLength()
|
||||||
|
val numSamples = (arcLen / samplesPerBlock).toInt().coerceAtLeast(10)
|
||||||
|
|
||||||
|
for (i in 0..numSamples) {
|
||||||
|
val t = i.toDouble() / numSamples
|
||||||
|
val centerPoint = curve.evaluate(t)
|
||||||
|
val direction = curve.direction(t)
|
||||||
|
val perpendicular = direction.perpendicularXZ()
|
||||||
|
|
||||||
|
val curvature = curve.curvature(t)
|
||||||
|
val bankingAngle = if (profile.bankingEnabled) {
|
||||||
|
calculateBankingAngle(curvature, profile.maxBankingAngle)
|
||||||
|
} else 0.0
|
||||||
|
|
||||||
|
profile.median?.let { median ->
|
||||||
|
val medianBlocks = generateLaneBlocks(
|
||||||
|
centerPoint,
|
||||||
|
perpendicular,
|
||||||
|
0.0,
|
||||||
|
median,
|
||||||
|
bankingAngle
|
||||||
|
)
|
||||||
|
blocks.addAll(medianBlocks)
|
||||||
|
}
|
||||||
|
|
||||||
|
profile.lanesLeft.forEachIndexed { index, lane ->
|
||||||
|
val offset = profile.getOffsetForLane(RoadProfile.Side.LEFT, index)
|
||||||
|
val laneBlocks = generateLaneBlocks(
|
||||||
|
centerPoint,
|
||||||
|
perpendicular,
|
||||||
|
offset,
|
||||||
|
lane,
|
||||||
|
bankingAngle
|
||||||
|
)
|
||||||
|
blocks.addAll(laneBlocks)
|
||||||
|
}
|
||||||
|
|
||||||
|
profile.lanesRight.forEachIndexed { index, lane ->
|
||||||
|
val offset = profile.getOffsetForLane(RoadProfile.Side.RIGHT, index)
|
||||||
|
val laneBlocks = generateLaneBlocks(
|
||||||
|
centerPoint,
|
||||||
|
perpendicular,
|
||||||
|
offset,
|
||||||
|
lane,
|
||||||
|
bankingAngle
|
||||||
|
)
|
||||||
|
blocks.addAll(laneBlocks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return blocks.distinctBy { Triple(it.x, it.y, it.z) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateLaneBlocks(
|
||||||
|
centerPoint: Vec3d,
|
||||||
|
perpendicular: Vec3d,
|
||||||
|
offset: Double,
|
||||||
|
lane: LaneDefinition,
|
||||||
|
bankingAngle: Double
|
||||||
|
): List<RoadBlock> {
|
||||||
|
val blocks = mutableListOf<RoadBlock>()
|
||||||
|
val halfWidth = lane.width / 2.0
|
||||||
|
|
||||||
|
val bankingOffset = if (bankingAngle != 0.0) {
|
||||||
|
abs(offset) * sin(Math.toRadians(bankingAngle))
|
||||||
|
} else 0.0
|
||||||
|
|
||||||
|
val widthSteps = (lane.width).toInt().coerceAtLeast(1)
|
||||||
|
for (w in 0 until widthSteps) {
|
||||||
|
val widthOffset = -halfWidth + (w + 0.5) * (lane.width / widthSteps)
|
||||||
|
val totalOffset = offset + widthOffset
|
||||||
|
|
||||||
|
val blockPos = centerPoint + perpendicular * totalOffset
|
||||||
|
val adjustedY = blockPos.y + profile.elevationOffset + bankingOffset
|
||||||
|
|
||||||
|
val (bx, by, bz) = Vec3d(blockPos.x, adjustedY, blockPos.z).toBlockPos()
|
||||||
|
|
||||||
|
blocks.add(
|
||||||
|
RoadBlock(
|
||||||
|
x = bx,
|
||||||
|
y = by,
|
||||||
|
z = bz,
|
||||||
|
material = lane.material,
|
||||||
|
laneType = lane.type
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return blocks
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun calculateBankingAngle(curvature: Double, maxAngle: Double): Double {
|
||||||
|
val normalizedCurvature = (curvature * 100).coerceIn(0.0, 1.0)
|
||||||
|
return normalizedCurvature * maxAngle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class RoadBlock(
|
||||||
|
val x: Int,
|
||||||
|
val y: Int,
|
||||||
|
val z: Int,
|
||||||
|
val material: String,
|
||||||
|
val laneType: LaneType
|
||||||
|
)
|
||||||
|
|
||||||
|
data class RoadNetwork(
|
||||||
|
val segments: MutableList<RoadSegment> = mutableListOf()
|
||||||
|
) {
|
||||||
|
fun addSegment(segment: RoadSegment) {
|
||||||
|
segments.add(segment)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeSegment(id: String) {
|
||||||
|
segments.removeIf { it.id == id }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findSegmentAt(pos: Vec3d, tolerance: Double = 5.0): RoadSegment? {
|
||||||
|
return segments.find { segment ->
|
||||||
|
val samples = segment.curve.sample(20)
|
||||||
|
samples.any { (it - pos).length() < tolerance }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAllBlocks(): List<RoadBlock> {
|
||||||
|
return segments.flatMap { it.generateBlocks() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun estimateMaterialCost(): Map<String, Int> {
|
||||||
|
return getAllBlocks()
|
||||||
|
.groupBy { it.material }
|
||||||
|
.mapValues { it.value.size }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
modLoader = "javafml"
|
modLoader = "javafml"
|
||||||
loaderVersion = "*"
|
loaderVersion = "*"
|
||||||
issueTrackerURL = "https://github.com/ThePandaOliver/Multiloader-Template/issues"
|
issueTrackerURL = "https://gitea.thefetagroup.com/bnair/RoadBuildMC/issues"
|
||||||
license = "${mod_license}"
|
license = "${mod_license}"
|
||||||
|
|
||||||
[[mods]]
|
[[mods]]
|
||||||
|
|||||||
@@ -6,15 +6,15 @@
|
|||||||
"description": "${mod_description}",
|
"description": "${mod_description}",
|
||||||
"authors": [${fabric_mod_authors}],
|
"authors": [${fabric_mod_authors}],
|
||||||
"contact": {
|
"contact": {
|
||||||
"issues": "https://github.com/ThePandaOliver/Multiloader-Template/issues",
|
"issues": "https://gitea.thefetagroup.com/bnair/RoadBuildMC/issues",
|
||||||
"sources": "https://github.com/ThePandaOliver/Multiloader-Template"
|
"sources": "https://gitea.thefetagroup.com/bnair/RoadBuildMC"
|
||||||
},
|
},
|
||||||
"license": "${mod_license}",
|
"license": "${mod_license}",
|
||||||
"icon": "assets/${mod_id}/icon.png",
|
"icon": "assets/${mod_id}/icon.png",
|
||||||
"environment": "*",
|
"environment": "*",
|
||||||
"entrypoints": {
|
"entrypoints": {
|
||||||
"main": [
|
"main": [
|
||||||
"com.example.template.TemplateModFabric"
|
"com.bnair.roadrunner.RoadRunnerFabric"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"mixins": [
|
"mixins": [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"required": true,
|
"required": true,
|
||||||
"package": "com.example.template.mixin",
|
"package": "com.bnair.roadrunner.mixin",
|
||||||
"compatibilityLevel": "JAVA_21",
|
"compatibilityLevel": "JAVA_21",
|
||||||
"mixins": [
|
"mixins": [
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user