feat: データ形式の修正とvisualize

This commit is contained in:
Keisuke Hirata 2025-12-09 07:09:29 +09:00
parent c33cd859fe
commit 8fcacf37ff
4 changed files with 405 additions and 65 deletions

1
hcu-core Submodule

@ -0,0 +1 @@
Subproject commit 520a378cd5c3da6321305d07cc0b402b73292add

View File

@ -4,6 +4,7 @@ import io.papermc.paper.math.Position
import net.hareworks.hcu.lands.App import net.hareworks.hcu.lands.App
import net.hareworks.hcu.lands.model.Land import net.hareworks.hcu.lands.model.Land
import net.hareworks.hcu.lands.model.Shape import net.hareworks.hcu.lands.model.Shape
import net.hareworks.hcu.lands.model.normalizeToBlockInt
import net.hareworks.hcu.lands.task.LandsVisualizer import net.hareworks.hcu.lands.task.LandsVisualizer
import net.hareworks.kommand_lib.kommand import net.hareworks.kommand_lib.kommand
import net.kyori.adventure.text.Component import net.kyori.adventure.text.Component
@ -88,7 +89,14 @@ class LandsCommand(
executes { executes {
val p1: Position = argument("pos1") val p1: Position = argument("pos1")
val p2: Position = argument("pos2") val p2: Position = argument("pos2")
val shape = Shape.Cuboid(p1.x(), p1.y(), p1.z(), p2.x(), p2.y(), p2.z()) val shape = Shape.Cuboid(
normalizeToBlockInt(p1.x()),
normalizeToBlockInt(p1.y()),
normalizeToBlockInt(p1.z()),
normalizeToBlockInt(p2.x()),
normalizeToBlockInt(p2.y()),
normalizeToBlockInt(p2.z())
)
val name: String = argument("name") val name: String = argument("name")
updateLandData(sender, name) { parts -> updateLandData(sender, name) { parts ->
@ -102,17 +110,99 @@ class LandsCommand(
literal("cylinder") { literal("cylinder") {
blockCoordinates("center") { blockCoordinates("center") {
float("radius") { float("radius") {
float("height") { literal("center") {
executes { integer("totalHeight", min = 1) {
val c: Position = argument("center") executes {
val r: Double = argument("radius") val c: Position = argument("center")
val h: Double = argument("height") val r: Double = argument("radius")
val shape = Shape.Cylinder(c.x(), c.y(), c.z(), r, h) val total: Int = argument("totalHeight")
val name: String = argument("name") val name: String = argument("name")
updateLandData(sender, name) { parts -> // Subtract 1 for center block, distribute remaining
parts.add(shape) val remaining = total - 1
sender.sendMessage(Component.text("Shape added.", NamedTextColor.GREEN)) val topH = (remaining + 1) / 2 // Prefer top
val bottomH = remaining - topH
val shape = Shape.Cylinder(
normalizeToBlockInt(c.x()),
normalizeToBlockInt(c.y()),
normalizeToBlockInt(c.z()),
r, bottomH, topH
)
updateLandData(sender, name) { parts ->
parts.add(shape)
sender.sendMessage(Component.text("Shape added.", NamedTextColor.GREEN))
}
}
}
}
literal("top") {
integer("topHeight", min = 1) {
literal("bottom") {
integer("bottomHeight", min = 0) {
executes {
val c: Position = argument("center")
val r: Double = argument("radius")
val topH: Int = argument("topHeight")
val bottomH: Int = argument("bottomHeight")
val name: String = argument("name")
val shape = Shape.Cylinder(
normalizeToBlockInt(c.x()),
normalizeToBlockInt(c.y()),
normalizeToBlockInt(c.z()),
r, bottomH, topH
)
updateLandData(sender, name) { parts ->
parts.add(shape)
sender.sendMessage(Component.text("Shape added.", NamedTextColor.GREEN))
}
}
}
}
executes {
val c: Position = argument("center")
val r: Double = argument("radius")
val total: Int = argument("topHeight")
val name: String = argument("name")
// total - 1 goes to top, 0 to bottom
val shape = Shape.Cylinder(
normalizeToBlockInt(c.x()),
normalizeToBlockInt(c.y()),
normalizeToBlockInt(c.z()),
r, 0, total - 1
)
updateLandData(sender, name) { parts ->
parts.add(shape)
sender.sendMessage(Component.text("Shape added.", NamedTextColor.GREEN))
}
}
}
}
literal("bottom") {
integer("bottomHeight", min = 1) {
executes {
val c: Position = argument("center")
val r: Double = argument("radius")
val total: Int = argument("bottomHeight")
val name: String = argument("name")
// total - 1 goes to bottom, 0 to top
val shape = Shape.Cylinder(
normalizeToBlockInt(c.x()),
normalizeToBlockInt(c.y()),
normalizeToBlockInt(c.z()),
r, total - 1, 0
)
updateLandData(sender, name) { parts ->
parts.add(shape)
sender.sendMessage(Component.text("Shape added.", NamedTextColor.GREEN))
}
} }
} }
} }
@ -130,7 +220,14 @@ class LandsCommand(
val index: Int = argument("index") val index: Int = argument("index")
val p1: Position = argument("pos1") val p1: Position = argument("pos1")
val p2: Position = argument("pos2") val p2: Position = argument("pos2")
val shape = Shape.Cuboid(p1.x(), p1.y(), p1.z(), p2.x(), p2.y(), p2.z()) val shape = Shape.Cuboid(
normalizeToBlockInt(p1.x()),
normalizeToBlockInt(p1.y()),
normalizeToBlockInt(p1.z()),
normalizeToBlockInt(p2.x()),
normalizeToBlockInt(p2.y()),
normalizeToBlockInt(p2.z())
)
val name: String = argument("name") val name: String = argument("name")
updateLandData(sender, name) { parts -> updateLandData(sender, name) { parts ->
@ -148,21 +245,115 @@ class LandsCommand(
literal("cylinder") { literal("cylinder") {
blockCoordinates("center") { blockCoordinates("center") {
float("radius") { float("radius") {
float("height") { literal("center") {
executes { integer("totalHeight", min = 1) {
val index: Int = argument("index") executes {
val c: Position = argument("center") val index: Int = argument("index")
val r: Double = argument("radius") val c: Position = argument("center")
val h: Double = argument("height") val r: Double = argument("radius")
val shape = Shape.Cylinder(c.x(), c.y(), c.z(), r, h) val total: Int = argument("totalHeight")
val name: String = argument("name") val name: String = argument("name")
updateLandData(sender, name) { parts -> val remaining = total - 1
if (index in parts.indices) { val topH = (remaining + 1) / 2
parts[index] = shape val bottomH = remaining - topH
sender.sendMessage(Component.text("Shape modified at index $index.", NamedTextColor.GREEN))
} else { val shape = Shape.Cylinder(
sender.sendMessage(Component.text("Index out of bounds.", NamedTextColor.RED)) normalizeToBlockInt(c.x()),
normalizeToBlockInt(c.y()),
normalizeToBlockInt(c.z()),
r, bottomH, topH
)
updateLandData(sender, name) { parts ->
if (index in parts.indices) {
parts[index] = shape
sender.sendMessage(Component.text("Shape modified at index $index.", NamedTextColor.GREEN))
} else {
sender.sendMessage(Component.text("Index out of bounds.", NamedTextColor.RED))
}
}
}
}
}
literal("top") {
integer("topHeight", min = 1) {
literal("bottom") {
integer("bottomHeight", min = 0) {
executes {
val index: Int = argument("index")
val c: Position = argument("center")
val r: Double = argument("radius")
val topH: Int = argument("topHeight")
val bottomH: Int = argument("bottomHeight")
val name: String = argument("name")
val shape = Shape.Cylinder(
normalizeToBlockInt(c.x()),
normalizeToBlockInt(c.y()),
normalizeToBlockInt(c.z()),
r, bottomH, topH
)
updateLandData(sender, name) { parts ->
if (index in parts.indices) {
parts[index] = shape
sender.sendMessage(Component.text("Shape modified at index $index.", NamedTextColor.GREEN))
} else {
sender.sendMessage(Component.text("Index out of bounds.", NamedTextColor.RED))
}
}
}
}
}
executes {
val index: Int = argument("index")
val c: Position = argument("center")
val r: Double = argument("radius")
val total: Int = argument("topHeight")
val name: String = argument("name")
val shape = Shape.Cylinder(
normalizeToBlockInt(c.x()),
normalizeToBlockInt(c.y()),
normalizeToBlockInt(c.z()),
r, 0, total - 1
)
updateLandData(sender, name) { parts ->
if (index in parts.indices) {
parts[index] = shape
sender.sendMessage(Component.text("Shape modified at index $index.", NamedTextColor.GREEN))
} else {
sender.sendMessage(Component.text("Index out of bounds.", NamedTextColor.RED))
}
}
}
}
}
literal("bottom") {
integer("bottomHeight", min = 1) {
executes {
val index: Int = argument("index")
val c: Position = argument("center")
val r: Double = argument("radius")
val total: Int = argument("bottomHeight")
val name: String = argument("name")
val shape = Shape.Cylinder(
normalizeToBlockInt(c.x()),
normalizeToBlockInt(c.y()),
normalizeToBlockInt(c.z()),
r, total - 1, 0
)
updateLandData(sender, name) { parts ->
if (index in parts.indices) {
parts[index] = shape
sender.sendMessage(Component.text("Shape modified at index $index.", NamedTextColor.GREEN))
} else {
sender.sendMessage(Component.text("Index out of bounds.", NamedTextColor.RED))
}
} }
} }
} }

View File

@ -21,8 +21,8 @@ sealed class Shape {
@Serializable @Serializable
@SerialName("cuboid") @SerialName("cuboid")
data class Cuboid( data class Cuboid(
val x1: Double, val y1: Double, val z1: Double, val x1: Int, val y1: Int, val z1: Int,
val x2: Double, val y2: Double, val z2: Double val x2: Int, val y2: Int, val z2: Int
) : Shape() { ) : Shape() {
fun minX() = minOf(x1, x2) fun minX() = minOf(x1, x2)
fun maxX() = maxOf(x1, x2) fun maxX() = maxOf(x1, x2)
@ -32,24 +32,63 @@ sealed class Shape {
fun maxZ() = maxOf(z1, z2) fun maxZ() = maxOf(z1, z2)
fun contains(x: Double, y: Double, z: Double): Boolean { fun contains(x: Double, y: Double, z: Double): Boolean {
return x >= minX() && x <= maxX() && // Add 0.5 to block coordinates for center-based comparison
y >= minY() && y <= maxY() && return x >= minX() + 0.5 && x <= maxX() + 0.5 &&
z >= minZ() && z <= maxZ() y >= minY() + 0.5 && y <= maxY() + 0.5 &&
z >= minZ() + 0.5 && z <= maxZ() + 0.5
}
/**
* Returns the two corner blocks (min and max corners) for visualization
*/
fun getCornerBlocks(): Pair<Triple<Int, Int, Int>, Triple<Int, Int, Int>> {
val minCorner = Triple(minX(), minY(), minZ())
val maxCorner = Triple(maxX(), maxY(), maxZ())
return minCorner to maxCorner
} }
} }
@Serializable @Serializable
@SerialName("cylinder") @SerialName("cylinder")
data class Cylinder( data class Cylinder(
val x: Double, val y: Double, val z: Double, val x: Int, val y: Int, val z: Int,
val radius: Double, val radius: Double,
val height: Double // Assuming vertical cylinder val bottomHeight: Int, // Height extending downward from center block
val topHeight: Int // Height extending upward from center block
) : Shape() { ) : Shape() {
fun contains(tx: Double, ty: Double, tz: Double): Boolean { fun contains(tx: Double, ty: Double, tz: Double): Boolean {
if (ty < y || ty > y + height) return false // Add 0.5 to block coordinates for center-based comparison
val dx = tx - x val centerX = x + 0.5
val dz = tz - z val centerY = y + 0.5
val centerZ = z + 0.5
// Total height includes center block (1) + bottomHeight + topHeight
val minY = centerY - bottomHeight
val maxY = centerY + 1.0 + topHeight
if (ty < minY || ty > maxY) return false
val dx = tx - centerX
val dz = tz - centerZ
return (dx * dx + dz * dz) <= (radius * radius) return (dx * dx + dz * dz) <= (radius * radius)
} }
/**
* Returns the center block for visualization
*/
fun getCenterBlock(): Triple<Int, Int, Int> {
return Triple(x, y, z)
}
/**
* Total height of the cylinder including center block
*/
fun totalHeight(): Int = 1 + bottomHeight + topHeight
} }
} }
/**
* Normalizes a coordinate to block integer (floor)
*/
fun normalizeToBlockInt(coord: Double): Int {
return kotlin.math.floor(coord).toInt()
}

View File

@ -51,40 +51,149 @@ class VisualizerTask(private val landService: LandService) : Runnable {
} }
private fun drawCuboid(player: Player, cuboid: Shape.Cuboid) { private fun drawCuboid(player: Player, cuboid: Shape.Cuboid) {
// Draw edges val (minCorner, maxCorner) = cuboid.getCornerBlocks()
val minX = cuboid.minX()
val maxX = cuboid.maxX()
val minY = cuboid.minY()
val maxY = cuboid.maxY()
val minZ = cuboid.minZ()
val maxZ = cuboid.maxZ()
// Just corners for now or simple lines // Draw outline of min corner block
// A simple way to visualize is to spawn particles at corners and periodically along edges drawBlockOutline(player, minCorner.first, minCorner.second, minCorner.third, Color.LIME)
// Doing full wireframe every tick is heavy.
// Let's do corners. // Draw outline of max corner block
val corners = listOf( drawBlockOutline(player, maxCorner.first, maxCorner.second, maxCorner.third, Color.YELLOW)
Triple(minX, minY, minZ), Triple(maxX, minY, minZ),
Triple(minX, maxY, minZ), Triple(maxX, maxY, minZ), // Draw edges of the cuboid using outer corners
Triple(minX, minY, maxZ), Triple(maxX, minY, maxZ), // Min corner is at block position, max corner extends to outer edge (+1.0)
Triple(minX, maxY, maxZ), Triple(maxX, maxY, maxZ) val minX = cuboid.minX().toDouble()
) val maxX = cuboid.maxX() + 1.0
corners.forEach { (x, y, z) -> val minY = cuboid.minY().toDouble()
player.spawnParticle(Particle.DUST, x, y, z, 1, 0.0, 0.0, 0.0, Particle.DustOptions(Color.LIME, 1.0f)) val maxY = cuboid.maxY() + 1.0
} val minZ = cuboid.minZ().toDouble()
val maxZ = cuboid.maxZ() + 1.0
val edgeColor = Particle.DustOptions(Color.fromRGB(100, 255, 100), 0.75f)
val step = 0.5
// Bottom face edges
drawLine(player, minX, minY, minZ, maxX, minY, minZ, edgeColor, step)
drawLine(player, maxX, minY, minZ, maxX, minY, maxZ, edgeColor, step)
drawLine(player, maxX, minY, maxZ, minX, minY, maxZ, edgeColor, step)
drawLine(player, minX, minY, maxZ, minX, minY, minZ, edgeColor, step)
// Top face edges
drawLine(player, minX, maxY, minZ, maxX, maxY, minZ, edgeColor, step)
drawLine(player, maxX, maxY, minZ, maxX, maxY, maxZ, edgeColor, step)
drawLine(player, maxX, maxY, maxZ, minX, maxY, maxZ, edgeColor, step)
drawLine(player, minX, maxY, maxZ, minX, maxY, minZ, edgeColor, step)
// Vertical edges
drawLine(player, minX, minY, minZ, minX, maxY, minZ, edgeColor, step)
drawLine(player, maxX, minY, minZ, maxX, maxY, minZ, edgeColor, step)
drawLine(player, maxX, minY, maxZ, maxX, maxY, maxZ, edgeColor, step)
drawLine(player, minX, minY, maxZ, minX, maxY, maxZ, edgeColor, step)
} }
private fun drawCylinder(player: Player, cylinder: Shape.Cylinder) { private fun drawCylinder(player: Player, cylinder: Shape.Cylinder) {
val segments = 20 val center = cylinder.getCenterBlock()
// Draw outline of center block
drawBlockOutline(player, center.first, center.second, center.third, Color.AQUA)
// Draw cylinder edges (top and bottom circles + vertical lines)
// Add 0.5 to block coordinates for center on X/Z axis
val centerX = cylinder.x + 0.5
val centerZ = cylinder.z + 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
val segments = 32
val step = (Math.PI * 2) / segments val step = (Math.PI * 2) / segments
// Top and Bottom circles val edgeColor = Particle.DustOptions(Color.fromRGB(100, 200, 255), 0.75f)
// Bottom circle
for (i in 0 until segments) { for (i in 0 until segments) {
val angle = i * step val angle1 = i * step
val dx = cos(angle) * cylinder.radius val angle2 = (i + 1) * step
val dz = sin(angle) * cylinder.radius 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
player.spawnParticle(Particle.DUST, cylinder.x + dx, cylinder.y, cylinder.z + dz, 1, 0.0, 0.0, 0.0, Particle.DustOptions(Color.AQUA, 1.0f)) drawLine(player, x1, bottomY, z1, x2, bottomY, z2, edgeColor, 0.25)
player.spawnParticle(Particle.DUST, cylinder.x + dx, cylinder.y + cylinder.height, cylinder.z + dz, 1, 0.0, 0.0, 0.0, Particle.DustOptions(Color.AQUA, 1.0f)) }
// Top circle
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
drawLine(player, x1, topY, z1, x2, topY, z2, edgeColor, 0.25)
}
// Vertical lines (8 cardinal directions)
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
drawLine(player, x, bottomY, z, x, topY, z, edgeColor, 0.5)
}
}
/**
* Draws an outline of a block using particles
*/
private fun drawBlockOutline(player: Player, x: Int, y: Int, z: Int, color: Color) {
val dustOptions = Particle.DustOptions(color, 1.0f)
val step = 0.25 // Particle spacing along edges
// Bottom face edges
drawLine(player, x.toDouble(), y.toDouble(), z.toDouble(), x + 1.0, y.toDouble(), z.toDouble(), dustOptions, step)
drawLine(player, x + 1.0, y.toDouble(), z.toDouble(), x + 1.0, y.toDouble(), z + 1.0, dustOptions, step)
drawLine(player, x + 1.0, y.toDouble(), z + 1.0, x.toDouble(), y.toDouble(), z + 1.0, dustOptions, step)
drawLine(player, x.toDouble(), y.toDouble(), z + 1.0, x.toDouble(), y.toDouble(), z.toDouble(), dustOptions, step)
// Top face edges
drawLine(player, x.toDouble(), y + 1.0, z.toDouble(), x + 1.0, y + 1.0, z.toDouble(), dustOptions, step)
drawLine(player, x + 1.0, y + 1.0, z.toDouble(), x + 1.0, y + 1.0, z + 1.0, dustOptions, step)
drawLine(player, x + 1.0, y + 1.0, z + 1.0, x.toDouble(), y + 1.0, z + 1.0, dustOptions, step)
drawLine(player, x.toDouble(), y + 1.0, z + 1.0, x.toDouble(), y + 1.0, z.toDouble(), dustOptions, step)
// Vertical edges
drawLine(player, x.toDouble(), y.toDouble(), z.toDouble(), x.toDouble(), y + 1.0, z.toDouble(), dustOptions, step)
drawLine(player, x + 1.0, y.toDouble(), z.toDouble(), x + 1.0, y + 1.0, z.toDouble(), dustOptions, step)
drawLine(player, x + 1.0, y.toDouble(), z + 1.0, x + 1.0, y + 1.0, z + 1.0, dustOptions, step)
drawLine(player, x.toDouble(), y.toDouble(), z + 1.0, x.toDouble(), y + 1.0, z + 1.0, dustOptions, step)
}
/**
* Draws a line of particles between two points
*/
private fun drawLine(
player: Player,
x1: Double, y1: Double, z1: Double,
x2: Double, y2: Double, z2: Double,
dustOptions: Particle.DustOptions,
step: Double
) {
val dx = x2 - x1
val dy = y2 - y1
val dz = z2 - z1
val distance = kotlin.math.sqrt(dx * dx + dy * dy + dz * dz)
val steps = (distance / step).toInt()
if (steps == 0) return
for (i in 0..steps) {
val t = i.toDouble() / steps
val x = x1 + dx * t
val y = y1 + dy * t
val z = z1 + dz * t
player.spawnParticle(Particle.DUST, x, y, z, 1, 0.0, 0.0, 0.0, dustOptions)
} }
} }
} }