feat: 最適化とアウトライン表示
This commit is contained in:
parent
0ed06e789f
commit
576005aada
|
|
@ -21,16 +21,7 @@ class LandsAPIImpl(private val landService: LandService) : LandsAPI {
|
|||
}
|
||||
|
||||
override fun getLandAt(world: String, x: Double, y: Double, z: Double): Land? {
|
||||
return landService.getAllLands()
|
||||
.filter { it.world == world }
|
||||
.firstOrNull { land ->
|
||||
land.data.parts.any { shape ->
|
||||
when (shape) {
|
||||
is net.hareworks.hcu.lands.model.Shape.Cuboid -> shape.contains(x, y, z)
|
||||
is net.hareworks.hcu.lands.model.Shape.Cylinder -> shape.contains(x, y, z)
|
||||
}
|
||||
}
|
||||
}
|
||||
return landService.getLandAt(world, x, y, z)
|
||||
}
|
||||
|
||||
override fun getLand(name: String): Land? {
|
||||
|
|
|
|||
198
src/main/kotlin/net/hareworks/hcu/lands/index/LandIndex.kt
Normal file
198
src/main/kotlin/net/hareworks/hcu/lands/index/LandIndex.kt
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
package net.hareworks.hcu.lands.index
|
||||
|
||||
import net.hareworks.hcu.lands.model.Land
|
||||
import net.hareworks.hcu.lands.model.Shape
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* Chunk-based spatial index for fast land lookups.
|
||||
*
|
||||
* This index maintains a mapping from chunk coordinates to lands that overlap those chunks.
|
||||
* This allows for O(1) chunk lookup followed by O(k) land checks where k is the number of
|
||||
* lands in that chunk (typically 1-10).
|
||||
*/
|
||||
class LandIndex {
|
||||
|
||||
// Chunk -> Set of lands that overlap this chunk
|
||||
private val chunkIndex = ConcurrentHashMap<ChunkKey, MutableSet<Land>>()
|
||||
|
||||
// Land -> its bounding box (cached)
|
||||
private val boundingBoxCache = ConcurrentHashMap<String, BoundingBox>()
|
||||
|
||||
/**
|
||||
* Rebuilds the entire index from a collection of lands.
|
||||
* This should be called when the plugin initializes or when bulk changes occur.
|
||||
*/
|
||||
fun rebuild(lands: Collection<Land>) {
|
||||
chunkIndex.clear()
|
||||
boundingBoxCache.clear()
|
||||
|
||||
for (land in lands) {
|
||||
addLand(land)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a land to the index.
|
||||
*/
|
||||
fun addLand(land: Land) {
|
||||
val bbox = calculateBoundingBox(land)
|
||||
boundingBoxCache[land.name] = bbox
|
||||
|
||||
val chunks = bbox.getAffectedChunks(land.world)
|
||||
for (chunk in chunks) {
|
||||
chunkIndex.getOrPut(chunk) { ConcurrentHashMap.newKeySet() }.add(land)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a land from the index.
|
||||
*/
|
||||
fun removeLand(land: Land) {
|
||||
val bbox = boundingBoxCache.remove(land.name) ?: return
|
||||
|
||||
val chunks = bbox.getAffectedChunks(land.world)
|
||||
for (chunk in chunks) {
|
||||
// Use removeIf to ensure we remove the land even if its data has changed
|
||||
// (since equals/hashCode would be different for modified data classes)
|
||||
chunkIndex[chunk]?.removeIf { it.name == land.name }
|
||||
|
||||
if (chunkIndex[chunk]?.isEmpty() == true) {
|
||||
chunkIndex.remove(chunk)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a land in the index (removes old, adds new).
|
||||
*/
|
||||
fun updateLand(land: Land) {
|
||||
removeLand(land)
|
||||
addLand(land)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the land at the given coordinates.
|
||||
*
|
||||
* @return The land containing the coordinates, or null if none found
|
||||
*/
|
||||
fun getLandAt(world: String, x: Double, y: Double, z: Double): Land? {
|
||||
val chunkKey = ChunkKey.fromCoords(world, x, z)
|
||||
val candidates = chunkIndex[chunkKey] ?: return null
|
||||
|
||||
// Check each candidate land in this chunk
|
||||
for (land in candidates) {
|
||||
// Early exit: check bounding box first
|
||||
val bbox = boundingBoxCache[land.name]
|
||||
if (bbox != null && !bbox.contains(x, y, z)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Detailed shape check
|
||||
if (land.data.parts.any { shape -> checkShapeContains(shape, x, y, z) }) {
|
||||
return land
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all lands that overlap with the given chunk.
|
||||
*/
|
||||
fun getLandsInChunk(world: String, chunkX: Int, chunkZ: Int): Set<Land> {
|
||||
val chunkKey = ChunkKey(world, chunkX, chunkZ)
|
||||
return chunkIndex[chunkKey]?.toSet() ?: emptySet()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all lands in the given world.
|
||||
*/
|
||||
fun getLandsInWorld(world: String): Set<Land> {
|
||||
return chunkIndex.entries
|
||||
.filter { it.key.world == world }
|
||||
.flatMap { it.value }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the bounding box for a land.
|
||||
*/
|
||||
private fun calculateBoundingBox(land: Land): BoundingBox {
|
||||
if (land.data.parts.isEmpty()) {
|
||||
// Empty land, use center block as bounding box
|
||||
return BoundingBox(0, 0, 0, 0, 0, 0)
|
||||
}
|
||||
|
||||
var minX = Int.MAX_VALUE
|
||||
var minY = Int.MAX_VALUE
|
||||
var minZ = Int.MAX_VALUE
|
||||
var maxX = Int.MIN_VALUE
|
||||
var maxY = Int.MIN_VALUE
|
||||
var maxZ = Int.MIN_VALUE
|
||||
|
||||
for (shape in land.data.parts) {
|
||||
when (shape) {
|
||||
is Shape.Cuboid -> {
|
||||
minX = minOf(minX, shape.minX())
|
||||
maxX = maxOf(maxX, shape.maxX())
|
||||
minY = minOf(minY, shape.minY())
|
||||
maxY = maxOf(maxY, shape.maxY())
|
||||
minZ = minOf(minZ, shape.minZ())
|
||||
maxZ = maxOf(maxZ, shape.maxZ())
|
||||
}
|
||||
is Shape.Cylinder -> {
|
||||
// Use actual radius (radius + 0.5) for bounding box
|
||||
val actualRadius = shape.radius + 0.5
|
||||
val r = actualRadius.toInt() + 1
|
||||
minX = minOf(minX, shape.x - r)
|
||||
maxX = maxOf(maxX, shape.x + r)
|
||||
minY = minOf(minY, shape.y - shape.bottomHeight)
|
||||
maxY = maxOf(maxY, shape.y + shape.topHeight)
|
||||
minZ = minOf(minZ, shape.z - r)
|
||||
maxZ = maxOf(maxZ, shape.z + r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return BoundingBox(minX, minY, minZ, maxX, maxY, maxZ)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a shape contains the given coordinates.
|
||||
*/
|
||||
private fun checkShapeContains(shape: Shape, x: Double, y: Double, z: Double): Boolean {
|
||||
return when (shape) {
|
||||
is Shape.Cuboid -> shape.contains(x, y, z)
|
||||
is Shape.Cylinder -> shape.contains(x, y, z)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets statistics about the index.
|
||||
*/
|
||||
fun getStats(): IndexStats {
|
||||
val totalChunks = chunkIndex.size
|
||||
val totalLands = boundingBoxCache.size
|
||||
val avgLandsPerChunk = if (totalChunks > 0) {
|
||||
chunkIndex.values.sumOf { it.size }.toDouble() / totalChunks
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
|
||||
return IndexStats(
|
||||
totalChunks = totalChunks,
|
||||
totalLands = totalLands,
|
||||
avgLandsPerChunk = avgLandsPerChunk
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistics about the land index.
|
||||
*/
|
||||
data class IndexStats(
|
||||
val totalChunks: Int,
|
||||
val totalLands: Int,
|
||||
val avgLandsPerChunk: Double
|
||||
)
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package net.hareworks.hcu.lands.index
|
||||
|
||||
import net.hareworks.hcu.lands.model.Land
|
||||
|
||||
/**
|
||||
* Represents a chunk coordinate key for indexing.
|
||||
*/
|
||||
data class ChunkKey(
|
||||
val world: String,
|
||||
val chunkX: Int,
|
||||
val chunkZ: Int
|
||||
) {
|
||||
companion object {
|
||||
/**
|
||||
* Creates a ChunkKey from block coordinates.
|
||||
*/
|
||||
fun fromBlockCoords(world: String, blockX: Int, blockZ: Int): ChunkKey {
|
||||
return ChunkKey(world, blockX shr 4, blockZ shr 4)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a ChunkKey from double coordinates.
|
||||
*/
|
||||
fun fromCoords(world: String, x: Double, z: Double): ChunkKey {
|
||||
return ChunkKey(world, x.toInt() shr 4, z.toInt() shr 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a 3D bounding box for spatial queries.
|
||||
*/
|
||||
data class BoundingBox(
|
||||
val minX: Int,
|
||||
val minY: Int,
|
||||
val minZ: Int,
|
||||
val maxX: Int,
|
||||
val maxY: Int,
|
||||
val maxZ: Int
|
||||
) {
|
||||
/**
|
||||
* Checks if this bounding box contains the given coordinates.
|
||||
*/
|
||||
fun contains(x: Double, y: Double, z: Double): Boolean {
|
||||
return x >= minX && x <= maxX + 1.0 &&
|
||||
y >= minY && y <= maxY + 1.0 &&
|
||||
z >= minZ && z <= maxZ + 1.0
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all chunk keys that this bounding box overlaps.
|
||||
*/
|
||||
fun getAffectedChunks(world: String): Set<ChunkKey> {
|
||||
val chunks = mutableSetOf<ChunkKey>()
|
||||
val minChunkX = minX shr 4
|
||||
val maxChunkX = maxX shr 4
|
||||
val minChunkZ = minZ shr 4
|
||||
val maxChunkZ = maxZ shr 4
|
||||
|
||||
for (chunkX in minChunkX..maxChunkX) {
|
||||
for (chunkZ in minChunkZ..maxChunkZ) {
|
||||
chunks.add(ChunkKey(world, chunkX, chunkZ))
|
||||
}
|
||||
}
|
||||
|
||||
return chunks
|
||||
}
|
||||
}
|
||||
|
|
@ -67,9 +67,13 @@ sealed class Shape {
|
|||
val maxY = centerY + 1.0 + topHeight
|
||||
|
||||
if (ty < minY || ty > maxY) return false
|
||||
|
||||
// Add 0.5 to radius for block-aligned judgment
|
||||
// This ensures that a radius of 4 includes all blocks within 4 blocks from center
|
||||
val actualRadius = radius + 0.5
|
||||
val dx = tx - centerX
|
||||
val dz = tz - centerZ
|
||||
return (dx * dx + dz * dz) <= (radius * radius)
|
||||
return (dx * dx + dz * dz) <= (actualRadius * actualRadius)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -92,13 +96,16 @@ sealed class Shape {
|
|||
val outline = mutableSetOf<Triple<Int, Int, Int>>()
|
||||
val centerX = x + 0.5
|
||||
val centerZ = z + 0.5
|
||||
val radiusSquared = radius * radius
|
||||
|
||||
// Use actual radius (radius + 0.5) for block-aligned judgment
|
||||
val actualRadius = radius + 0.5
|
||||
val radiusSquared = actualRadius * actualRadius
|
||||
|
||||
// Calculate the bounding box for the cylinder
|
||||
val minBlockX = (x - radius - 1).toInt()
|
||||
val maxBlockX = (x + radius + 1).toInt()
|
||||
val minBlockZ = (z - radius - 1).toInt()
|
||||
val maxBlockZ = (z + radius + 1).toInt()
|
||||
val minBlockX = (x - actualRadius - 1).toInt()
|
||||
val maxBlockX = (x + actualRadius + 1).toInt()
|
||||
val minBlockZ = (z - actualRadius - 1).toInt()
|
||||
val maxBlockZ = (z + actualRadius + 1).toInt()
|
||||
|
||||
// For each Y level in the cylinder
|
||||
val minY = y - bottomHeight
|
||||
|
|
@ -108,10 +115,8 @@ sealed class Shape {
|
|||
// Check each block in the XZ plane
|
||||
for (blockX in minBlockX..maxBlockX) {
|
||||
for (blockZ in minBlockZ..maxBlockZ) {
|
||||
// Check if this block is inside the cylinder
|
||||
val dx = (blockX + 0.5) - centerX
|
||||
val dz = (blockZ + 0.5) - centerZ
|
||||
val isInside = (dx * dx + dz * dz) <= radiusSquared
|
||||
// Check if this block center is inside the cylinder
|
||||
val isInside = contains(blockX + 0.5, blockY + 0.5, blockZ + 0.5)
|
||||
|
||||
if (isInside) {
|
||||
// Check if any adjacent block is outside (making this an edge block)
|
||||
|
|
@ -121,9 +126,7 @@ sealed class Shape {
|
|||
Pair(blockX, blockZ - 1),
|
||||
Pair(blockX, blockZ + 1)
|
||||
).any { (nx, nz) ->
|
||||
val ndx = (nx + 0.5) - centerX
|
||||
val ndz = (nz + 0.5) - centerZ
|
||||
(ndx * ndx + ndz * ndz) > radiusSquared
|
||||
!contains(nx + 0.5, blockY + 0.5, nz + 0.5)
|
||||
}
|
||||
|
||||
if (hasOutsideNeighbor) {
|
||||
|
|
@ -136,6 +139,70 @@ sealed class Shape {
|
|||
|
||||
return outline
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a set of blocks that form the outline of the blockified cylinder.
|
||||
* Optimized algorithm: O(R) instead of O(R^2).
|
||||
* Calculates boundary blocks by scanning X/Z axes instead of checking all blocks area.
|
||||
*/
|
||||
fun getOptimizedFaceOutline(): Set<Pair<Int, Int>> {
|
||||
val outline = mutableSetOf<Pair<Int, Int>>()
|
||||
val actualRadius = radius + 0.5
|
||||
val radiusSq = actualRadius * actualRadius
|
||||
val rInt = actualRadius.toInt() + 1
|
||||
|
||||
// 1. Scan X axis to find Z boundaries (Top/Bottom edges)
|
||||
for (dx in -rInt..rInt) {
|
||||
val bx = x + dx
|
||||
// Calculate distance from center X
|
||||
// Block center is at bx + 0.5. Relative to cylinder center (x + 0.5)
|
||||
// dist = (bx + 0.5) - (x + 0.5) = bx - x = dx
|
||||
// Wait, logic check:
|
||||
// Cylinder center: cx = x + 0.5
|
||||
// Block center: bcx = bx + 0.5
|
||||
// diff = bcx - cx = bx - x = dx
|
||||
// So (dx)^2 + (dz)^2 <= R^2
|
||||
// We need to verify if this dx is valid geometrically with +0.5 offset considerations handled by contains()
|
||||
// Let's rely on calculation consistent with contains():
|
||||
// (bx + 0.5 - (x + 0.5))^2 + ... = dx^2 + dz^2
|
||||
|
||||
// Correction: The contains() logic uses:
|
||||
// val dx = tx - centerX
|
||||
// tx is block coord + 0.5, centerX is x + 0.5.
|
||||
// So dx = (bx + 0.5) - (x + 0.5) = bx - x.
|
||||
// It works simply with integers.
|
||||
|
||||
val term = radiusSq - dx * dx
|
||||
if (term < 0) continue
|
||||
|
||||
val maxDistZ = kotlin.math.sqrt(term)
|
||||
// We want largest integer dz such that dz^2 <= term
|
||||
// Since dz is integer difference (bz - z), dz can be simply floor(maxDistZ) and ceil(-maxDistZ)
|
||||
// Let's verify: if dz = 4.5, integer dz can be 4.
|
||||
|
||||
val maxDz = kotlin.math.floor(maxDistZ).toInt()
|
||||
val minDz = -maxDz // Symmetry
|
||||
|
||||
outline.add(Pair(bx, z + maxDz))
|
||||
outline.add(Pair(bx, z + minDz))
|
||||
}
|
||||
|
||||
// 2. Scan Z axis to find X boundaries (Left/Right edges)
|
||||
for (dz in -rInt..rInt) {
|
||||
val term = radiusSq - dz * dz
|
||||
if (term < 0) continue
|
||||
|
||||
val maxDistX = kotlin.math.sqrt(term)
|
||||
val maxDx = kotlin.math.floor(maxDistX).toInt()
|
||||
val minDx = -maxDx
|
||||
|
||||
val bz = z + dz
|
||||
outline.add(Pair(x + maxDx, bz))
|
||||
outline.add(Pair(x + minDx, bz))
|
||||
}
|
||||
|
||||
return outline
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package net.hareworks.hcu.lands.service
|
||||
|
||||
import net.hareworks.hcu.lands.database.LandsTable
|
||||
import net.hareworks.hcu.lands.index.LandIndex
|
||||
import net.hareworks.hcu.lands.model.Land
|
||||
import net.hareworks.hcu.lands.model.LandData
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
|
|
@ -16,6 +17,7 @@ import org.bukkit.Bukkit
|
|||
|
||||
class LandService(private val database: Database) {
|
||||
private val cache = ConcurrentHashMap<String, Land>()
|
||||
private val spatialIndex = LandIndex()
|
||||
|
||||
fun init() {
|
||||
transaction(database) {
|
||||
|
|
@ -35,6 +37,9 @@ class LandService(private val database: Database) {
|
|||
)
|
||||
cache[land.name] = land
|
||||
}
|
||||
|
||||
// Rebuild spatial index
|
||||
spatialIndex.rebuild(cache.values)
|
||||
}
|
||||
|
||||
fun createLand(name: String, actorId: Int, world: String): Boolean {
|
||||
|
|
@ -48,8 +53,10 @@ class LandService(private val database: Database) {
|
|||
it[LandsTable.data] = LandData()
|
||||
}
|
||||
}
|
||||
// Update cache efficiently
|
||||
cache[name] = Land(name, actorId, world, LandData())
|
||||
// Update cache and index
|
||||
val land = Land(name, actorId, world, LandData())
|
||||
cache[name] = land
|
||||
spatialIndex.addLand(land)
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -65,20 +72,52 @@ class LandService(private val database: Database) {
|
|||
it[LandsTable.data] = newData
|
||||
}
|
||||
}
|
||||
cache[name] = land.copy(data = newData)
|
||||
|
||||
val updatedLand = land.copy(data = newData)
|
||||
cache[name] = updatedLand
|
||||
|
||||
// Update spatial index
|
||||
spatialIndex.updateLand(updatedLand)
|
||||
return true
|
||||
}
|
||||
|
||||
fun deleteLand(name: String): Boolean {
|
||||
if (!cache.containsKey(name)) return false
|
||||
val land = cache[name]!!
|
||||
|
||||
transaction(database) {
|
||||
LandsTable.deleteWhere { LandsTable.name eq name }
|
||||
}
|
||||
|
||||
cache.remove(name)
|
||||
|
||||
// Remove from spatial index
|
||||
spatialIndex.removeLand(land)
|
||||
return true
|
||||
}
|
||||
|
||||
fun getLand(name: String): Land? = cache[name]
|
||||
|
||||
fun getAllLands(): Collection<Land> = cache.values
|
||||
|
||||
/**
|
||||
* Gets the land at the specified coordinates using the spatial index.
|
||||
* This is much faster than iterating through all lands.
|
||||
*/
|
||||
fun getLandAt(world: String, x: Double, y: Double, z: Double): Land? {
|
||||
return spatialIndex.getLandAt(world, x, y, z)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all lands in the specified chunk.
|
||||
*/
|
||||
fun getLandsInChunk(world: String, chunkX: Int, chunkZ: Int): Set<Land> {
|
||||
return spatialIndex.getLandsInChunk(world, chunkX, chunkZ)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets statistics about the spatial index.
|
||||
*/
|
||||
fun getIndexStats() = spatialIndex.getStats()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,10 +30,22 @@ class VisualizerTask(private val landService: LandService) : Runnable {
|
|||
override fun run() {
|
||||
Bukkit.getOnlinePlayers().forEach { p ->
|
||||
if (LandsVisualizer.isEnabled(p)) {
|
||||
landService.getAllLands().forEach { land ->
|
||||
if (land.world == p.world.name) {
|
||||
// Optimization: Check distance to Land center/bounds?
|
||||
// For now basic iteration
|
||||
val pChunkX = p.location.blockX shr 4
|
||||
val pChunkZ = p.location.blockZ shr 4
|
||||
val radiusInChunks = 2 // 32 blocks radius approx
|
||||
|
||||
// Collect lands from nearby chunks using spatial index
|
||||
val nearbyLands = mutableSetOf<net.hareworks.hcu.lands.model.Land>()
|
||||
|
||||
for (cx in (pChunkX - radiusInChunks)..(pChunkX + radiusInChunks)) {
|
||||
for (cz in (pChunkZ - radiusInChunks)..(pChunkZ + radiusInChunks)) {
|
||||
nearbyLands.addAll(landService.getLandsInChunk(p.world.name, cx, cz))
|
||||
}
|
||||
}
|
||||
|
||||
nearbyLands.forEach { land ->
|
||||
// Further distance check could be added here if needed,
|
||||
// but chunk-based filtering is already efficient enough for visualization logic
|
||||
land.data.parts.forEach { shape ->
|
||||
drawShape(p, shape)
|
||||
}
|
||||
|
|
@ -41,7 +53,6 @@ class VisualizerTask(private val landService: LandService) : Runnable {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawShape(player: Player, shape: Shape) {
|
||||
when (shape) {
|
||||
|
|
@ -101,6 +112,9 @@ class VisualizerTask(private val landService: LandService) : Runnable {
|
|||
val centerX = cylinder.x + 0.5
|
||||
val centerZ = cylinder.z + 0.5
|
||||
|
||||
// Use actual radius (radius + 0.5) for visualization
|
||||
val actualRadius = cylinder.radius + 0.5
|
||||
|
||||
// Y coordinates use block boundaries (no +0.5 offset)
|
||||
val bottomY = cylinder.y.toDouble() - cylinder.bottomHeight
|
||||
val topY = cylinder.y.toDouble() + 1.0 + cylinder.topHeight
|
||||
|
|
@ -113,10 +127,10 @@ class VisualizerTask(private val landService: LandService) : Runnable {
|
|||
for (i in 0 until segments) {
|
||||
val angle1 = i * step
|
||||
val angle2 = (i + 1) * step
|
||||
val x1 = centerX + cos(angle1) * cylinder.radius
|
||||
val z1 = centerZ + sin(angle1) * cylinder.radius
|
||||
val x2 = centerX + cos(angle2) * cylinder.radius
|
||||
val z2 = centerZ + sin(angle2) * cylinder.radius
|
||||
val x1 = centerX + cos(angle1) * actualRadius
|
||||
val z1 = centerZ + sin(angle1) * actualRadius
|
||||
val x2 = centerX + cos(angle2) * actualRadius
|
||||
val z2 = centerZ + sin(angle2) * actualRadius
|
||||
|
||||
drawLine(player, x1, bottomY, z1, x2, bottomY, z2, edgeColor, 0.25)
|
||||
}
|
||||
|
|
@ -125,10 +139,10 @@ class VisualizerTask(private val landService: LandService) : Runnable {
|
|||
for (i in 0 until segments) {
|
||||
val angle1 = i * step
|
||||
val angle2 = (i + 1) * step
|
||||
val x1 = centerX + cos(angle1) * cylinder.radius
|
||||
val z1 = centerZ + sin(angle1) * cylinder.radius
|
||||
val x2 = centerX + cos(angle2) * cylinder.radius
|
||||
val z2 = centerZ + sin(angle2) * cylinder.radius
|
||||
val x1 = centerX + cos(angle1) * actualRadius
|
||||
val z1 = centerZ + sin(angle1) * actualRadius
|
||||
val x2 = centerX + cos(angle2) * actualRadius
|
||||
val z2 = centerZ + sin(angle2) * actualRadius
|
||||
|
||||
drawLine(player, x1, topY, z1, x2, topY, z2, edgeColor, 0.25)
|
||||
}
|
||||
|
|
@ -137,19 +151,69 @@ class VisualizerTask(private val landService: LandService) : Runnable {
|
|||
val verticalSegments = 8
|
||||
for (i in 0 until verticalSegments) {
|
||||
val angle = i * (Math.PI * 2) / verticalSegments
|
||||
val x = centerX + cos(angle) * cylinder.radius
|
||||
val z = centerZ + sin(angle) * cylinder.radius
|
||||
val x = centerX + cos(angle) * actualRadius
|
||||
val z = centerZ + sin(angle) * actualRadius
|
||||
|
||||
drawLine(player, x, bottomY, z, x, topY, z, edgeColor, 0.5)
|
||||
}
|
||||
|
||||
// Draw blockified outline
|
||||
val blockifiedOutline = cylinder.getBlockifiedOutline()
|
||||
val blockColor = Particle.DustOptions(Color.fromRGB(150, 220, 255), 0.5f)
|
||||
// Draw top face outline in red (outer edges only)
|
||||
// Optimized: Calculate outline once for both faces as they share the same XZ shape
|
||||
val faceOutline = cylinder.getOptimizedFaceOutline()
|
||||
val redColor = Particle.DustOptions(Color.RED, 1.0f)
|
||||
val topFaceY = cylinder.y + cylinder.topHeight
|
||||
|
||||
for ((bx, by, bz) in blockifiedOutline) {
|
||||
// Draw a subtle outline for each block in the blockified cylinder
|
||||
drawBlockOutline(player, bx, by, bz, Color.fromRGB(150, 220, 255))
|
||||
drawFaceOutline(player, faceOutline, topFaceY + 1.0, redColor, cylinder)
|
||||
|
||||
// Draw bottom face outline in red (outer edges only)
|
||||
val bottomFaceY = cylinder.y - cylinder.bottomHeight
|
||||
|
||||
drawFaceOutline(player, faceOutline, bottomFaceY.toDouble(), redColor, cylinder)
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws only the outer edges of a face outline (excluding inner holes).
|
||||
*/
|
||||
private fun drawFaceOutline(
|
||||
player: Player,
|
||||
faceBlocks: Set<Pair<Int, Int>>,
|
||||
y: Double,
|
||||
dustOptions: Particle.DustOptions,
|
||||
cylinder: Shape.Cylinder
|
||||
) {
|
||||
// Use the cylinder's vertical center for the 'contains' check to ensure we are within Y bounds
|
||||
// This effectively checks if the neighbor is outside the radius laterally
|
||||
val checkY = cylinder.y + 0.5
|
||||
|
||||
// For each block in the face, check which edges face outward (not toward inner holes)
|
||||
for ((bx, bz) in faceBlocks) {
|
||||
// Check each of the 4 edges
|
||||
// An edge should be drawn if the adjacent block is NOT in the face
|
||||
// AND is outside the cylinder (not an inner hole)
|
||||
|
||||
// Top edge (z- direction)
|
||||
val topNeighbor = Pair(bx, bz - 1)
|
||||
if (!faceBlocks.contains(topNeighbor) && !cylinder.contains(topNeighbor.first + 0.5, checkY, topNeighbor.second + 0.5)) {
|
||||
drawLine(player, bx.toDouble(), y, bz.toDouble(), bx + 1.0, y, bz.toDouble(), dustOptions, 0.25)
|
||||
}
|
||||
|
||||
// Right edge (x+ direction)
|
||||
val rightNeighbor = Pair(bx + 1, bz)
|
||||
if (!faceBlocks.contains(rightNeighbor) && !cylinder.contains(rightNeighbor.first + 0.5, checkY, rightNeighbor.second + 0.5)) {
|
||||
drawLine(player, bx + 1.0, y, bz.toDouble(), bx + 1.0, y, bz + 1.0, dustOptions, 0.25)
|
||||
}
|
||||
|
||||
// Bottom edge (z+ direction)
|
||||
val bottomNeighbor = Pair(bx, bz + 1)
|
||||
if (!faceBlocks.contains(bottomNeighbor) && !cylinder.contains(bottomNeighbor.first + 0.5, checkY, bottomNeighbor.second + 0.5)) {
|
||||
drawLine(player, bx + 1.0, y, bz + 1.0, bx.toDouble(), y, bz + 1.0, dustOptions, 0.25)
|
||||
}
|
||||
|
||||
// Left edge (x- direction)
|
||||
val leftNeighbor = Pair(bx - 1, bz)
|
||||
if (!faceBlocks.contains(leftNeighbor) && !cylinder.contains(leftNeighbor.first + 0.5, checkY, leftNeighbor.second + 0.5)) {
|
||||
drawLine(player, bx.toDouble(), y, bz + 1.0, bx.toDouble(), y, bz.toDouble(), dustOptions, 0.25)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user