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.model.Land
import net.hareworks.hcu.lands.model.Shape
import net.hareworks.hcu.lands.model.normalizeToBlockInt
import net.hareworks.hcu.lands.task.LandsVisualizer
import net.hareworks.kommand_lib.kommand
import net.kyori.adventure.text.Component
@ -88,7 +89,14 @@ class LandsCommand(
executes {
val p1: Position = argument("pos1")
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")
updateLandData(sender, name) { parts ->
@ -102,14 +110,26 @@ class LandsCommand(
literal("cylinder") {
blockCoordinates("center") {
float("radius") {
float("height") {
literal("center") {
integer("totalHeight", min = 1) {
executes {
val c: Position = argument("center")
val r: Double = argument("radius")
val h: Double = argument("height")
val shape = Shape.Cylinder(c.x(), c.y(), c.z(), r, h)
val total: Int = argument("totalHeight")
val name: String = argument("name")
// Subtract 1 for center block, distribute remaining
val remaining = total - 1
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))
@ -117,6 +137,76 @@ class LandsCommand(
}
}
}
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 p1: Position = argument("pos1")
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")
updateLandData(sender, name) { parts ->
@ -148,15 +245,26 @@ class LandsCommand(
literal("cylinder") {
blockCoordinates("center") {
float("radius") {
float("height") {
literal("center") {
integer("totalHeight", min = 1) {
executes {
val index: Int = argument("index")
val c: Position = argument("center")
val r: Double = argument("radius")
val h: Double = argument("height")
val shape = Shape.Cylinder(c.x(), c.y(), c.z(), r, h)
val total: Int = argument("totalHeight")
val name: String = argument("name")
val remaining = total - 1
val topH = (remaining + 1) / 2
val bottomH = remaining - topH
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
@ -168,6 +276,89 @@ class LandsCommand(
}
}
}
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
@SerialName("cuboid")
data class Cuboid(
val x1: Double, val y1: Double, val z1: Double,
val x2: Double, val y2: Double, val z2: Double
val x1: Int, val y1: Int, val z1: Int,
val x2: Int, val y2: Int, val z2: Int
) : Shape() {
fun minX() = minOf(x1, x2)
fun maxX() = maxOf(x1, x2)
@ -32,24 +32,63 @@ sealed class Shape {
fun maxZ() = maxOf(z1, z2)
fun contains(x: Double, y: Double, z: Double): Boolean {
return x >= minX() && x <= maxX() &&
y >= minY() && y <= maxY() &&
z >= minZ() && z <= maxZ()
// Add 0.5 to block coordinates for center-based comparison
return x >= minX() + 0.5 && x <= maxX() + 0.5 &&
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
@SerialName("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 height: Double // Assuming vertical cylinder
val bottomHeight: Int, // Height extending downward from center block
val topHeight: Int // Height extending upward from center block
) : Shape() {
fun contains(tx: Double, ty: Double, tz: Double): Boolean {
if (ty < y || ty > y + height) return false
val dx = tx - x
val dz = tz - z
// Add 0.5 to block coordinates for center-based comparison
val centerX = x + 0.5
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)
}
/**
* 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) {
// Draw edges
val minX = cuboid.minX()
val maxX = cuboid.maxX()
val minY = cuboid.minY()
val maxY = cuboid.maxY()
val minZ = cuboid.minZ()
val maxZ = cuboid.maxZ()
val (minCorner, maxCorner) = cuboid.getCornerBlocks()
// Just corners for now or simple lines
// A simple way to visualize is to spawn particles at corners and periodically along edges
// Doing full wireframe every tick is heavy.
// Let's do corners.
val corners = listOf(
Triple(minX, minY, minZ), Triple(maxX, minY, minZ),
Triple(minX, maxY, minZ), Triple(maxX, maxY, minZ),
Triple(minX, minY, maxZ), Triple(maxX, minY, maxZ),
Triple(minX, maxY, maxZ), Triple(maxX, maxY, maxZ)
)
corners.forEach { (x, y, z) ->
player.spawnParticle(Particle.DUST, x, y, z, 1, 0.0, 0.0, 0.0, Particle.DustOptions(Color.LIME, 1.0f))
}
// Draw outline of min corner block
drawBlockOutline(player, minCorner.first, minCorner.second, minCorner.third, Color.LIME)
// Draw outline of max corner block
drawBlockOutline(player, maxCorner.first, maxCorner.second, maxCorner.third, Color.YELLOW)
// Draw edges of the cuboid using outer corners
// Min corner is at block position, max corner extends to outer edge (+1.0)
val minX = cuboid.minX().toDouble()
val maxX = cuboid.maxX() + 1.0
val minY = cuboid.minY().toDouble()
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) {
val segments = 20
val step = (Math.PI * 2) / segments
// Top and Bottom circles
for (i in 0 until segments) {
val angle = i * step
val dx = cos(angle) * cylinder.radius
val dz = sin(angle) * cylinder.radius
val center = cylinder.getCenterBlock()
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))
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))
// 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 edgeColor = Particle.DustOptions(Color.fromRGB(100, 200, 255), 0.75f)
// Bottom 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, bottomY, z1, x2, bottomY, z2, edgeColor, 0.25)
}
// 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)
}
}
}