Skip to content

Commit

Permalink
Add Canvas
Browse files Browse the repository at this point in the history
  • Loading branch information
NichtStudioCode committed Dec 14, 2024
1 parent ce664e5 commit 812dcb8
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package xyz.xenondevs.nova.resources.builder.layout.item

import org.joml.Vector3d
import xyz.xenondevs.commons.collections.enumMapOf
import xyz.xenondevs.nova.registry.RegistryElementBuilderDsl
import xyz.xenondevs.nova.resources.ResourcePath
import xyz.xenondevs.nova.resources.ResourceType
Expand All @@ -9,9 +11,14 @@ import xyz.xenondevs.nova.resources.builder.data.EmptyItemModel
import xyz.xenondevs.nova.resources.builder.data.ItemModel
import xyz.xenondevs.nova.resources.builder.data.ItemModelDefinition
import xyz.xenondevs.nova.resources.builder.data.SpecialItemModel.SpecialModel
import xyz.xenondevs.nova.resources.builder.data.TintSource
import xyz.xenondevs.nova.resources.builder.layout.ModelSelectorScope
import xyz.xenondevs.nova.resources.builder.layout.block.BlockModelSelectorScope
import xyz.xenondevs.nova.resources.builder.model.Model
import xyz.xenondevs.nova.resources.builder.model.ModelBuilder
import xyz.xenondevs.nova.resources.builder.task.model.ModelContent
import xyz.xenondevs.nova.ui.menu.Canvas
import java.awt.Color

@RegistryElementBuilderDsl
class ItemModelDefinitionBuilder<S : ModelSelectorScope> internal constructor(
Expand Down Expand Up @@ -195,4 +202,80 @@ sealed class ItemModelCreationScope<S : ModelSelectorScope>(
}
}

/**
* Creates a canvas model, which is an item model with a flat texture where each pixel is individually
* addressable using the `colors` part of the `minecraft:custom_model_data` component, where
* the pixel at (x, y) is found under the index `y * width + x` and (0, 0) is the top-left pixel.
*
* The maximum [width] and [height] are `161 - |offsetX|` and `161 - |offsetY|` respectively.
* (Consider using multiple smaller canvases instead of a single large one to reduce the resource pack size.)
*
* @param width The width of the canvas, in pixels.
* @param height The height of the canvas, in pixels.
* @param offsetX The x offset of the canvas texture to the item's center, pixels.
* @param offsetY The y offset of the canvas texture to the item's center, pixels.
* @param scale The size of the pixels in the canvas texture.
* A scale of 2 means each pixel is 2x2 pixels in game (assuming a client-side gui-scale of 1).
* Defaults to 1.
*
* @see Canvas
*/
fun canvasModel(
width: Int, height: Int,
offsetX: Double = 0.0, offsetY: Double = 0.0,
scale: Double = 1.0
): ItemModel = select(SelectItemModelProperty.DisplayContext) {
// the actual width and height of the canvas, in pixel models needed, takes scale into account
val actualWidth = (width / scale).toInt()
val actualHeight = (height / scale).toInt()

// the individual pixel models apply a display scale of 4, so actualScale counteracts this with 0.25
val actualScale = 0.25 * scale

// parent model for all pixels with this scale
val parentModel = Model(
parent = ResourcePath(ResourceType.Model, "nova", "item/canvas_pixel"),
elements = listOf(
Model.Element(
from = Vector3d(8.0, 8.0 - actualScale, 0.0),
to = Vector3d(8.0 + actualScale, 8.0, 0.0),
faces = enumMapOf(
Model.Direction.SOUTH to Model.Element.Face(
texture = "#0",
tintIndex = 0
)
)
)
)
)
val parentModelId = resourcePackBuilder.getHolder<ModelContent>().getOrPutGenerated(parentModel)

fallback = empty()
case[DisplayContext.GUI] = composite {
for (y in 0..<actualHeight) {
for (x in 0..<actualWidth) {
val i = y * actualWidth + x
models += model {
tintSource[0] = TintSource.CustomModelData(Color.WHITE, i)
model = {
ModelBuilder(Model(
parent = parentModelId,
display = mapOf(
Model.Display.Position.GUI to Model.Display(
scale = Vector3d(4.0, 4.0, 4.0),
translation = Vector3d(
(width / -2.0) + offsetX + x * scale,
-((height / -2.0) + offsetY + y * scale),
0.0
)
)
)
))
}
}
}
}
}
}

}
118 changes: 118 additions & 0 deletions nova/src/main/kotlin/xyz/xenondevs/nova/ui/menu/Canvas.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package xyz.xenondevs.nova.ui.menu

import it.unimi.dsi.fastutil.ints.IntArrayList
import net.minecraft.core.component.DataComponents
import net.minecraft.world.item.component.CustomModelData
import org.bukkit.entity.Player
import org.bukkit.event.inventory.ClickType
import xyz.xenondevs.invui.item.AbstractItem
import xyz.xenondevs.invui.item.Click
import xyz.xenondevs.invui.item.Item
import xyz.xenondevs.invui.item.ItemBuilder
import xyz.xenondevs.invui.item.ItemProvider
import xyz.xenondevs.invui.item.notifyWindows
import xyz.xenondevs.nova.resources.builder.layout.item.ItemModelCreationScope
import xyz.xenondevs.nova.util.unwrap
import xyz.xenondevs.nova.world.item.DefaultGuiItems
import xyz.xenondevs.nova.world.item.NovaItem
import java.awt.image.BufferedImage
import java.util.function.Supplier

/**
* An [Item] supplier for the [canvasItem] that splits [image] into square parts of [itemResolution]x[itemResolution] px.
*
* @param canvasItem The canvas item, which uses a model created via [ItemModelCreationScope.canvasModel].
* This model should be square and should completely fill an entire slot (18x18 px or multiples of that at non-1 scales).
* @param itemResolution The size of the canvas item in pixels (18 px or multiples of that at non-1 scales).
* @param image The image that is used to fill the canvas. Will be read from every time [notifyWindows] is called.
*
* @see ItemModelCreationScope.canvasModel
*/
open class Canvas(
private val canvasItem: NovaItem,
private val itemResolution: Int,
private val image: BufferedImage
) : Supplier<Item> {

private val items = ArrayList<Item>()
private var supplierIndex = 0

/**
* Creates a new [Canvas] with the [DefaultGuiItems.CANVAS] (18x18 px) item.
*
* @param image The image that is used to fill the canvas. Will be read from every time [notifyWindows] is called.
*/
constructor(image: BufferedImage) : this(DefaultGuiItems.CANVAS, 18, image)

init {
require(image.type == BufferedImage.TYPE_INT_ARGB) { "Image needs to be TYPE_INT_ARGB" }
require(image.height % itemResolution == 0) { "Image height needs to be divisible by $itemResolution" }
require(image.width % itemResolution == 0) { "Image width needs to be divisible by $itemResolution" }

for (y in 0..<(image.height / itemResolution)) {
for (x in 0..<(image.width / itemResolution)) {
items += CanvasPart(x, y)
}
}
}

override fun get(): Item {
return items[supplierIndex++]
}

/**
* [Notifies][Item.notifyWindows] all windows of all items of this canvas.
*/
fun notifyWindows() {
items.notifyWindows()
}

/**
* Modifies the [itemBuilder] for the canvas part at the given [x] and [y] coordinates,
* which will be displayed to [viewer].
*/
open fun modifyItemBuilder(x: Int, y: Int, viewer: Player, itemBuilder: ItemBuilder) = Unit

/**
* Handles a [click] on the canvas part at the given [x] and [y] coordinates.
*/
open fun handleClick(x: Int, y: Int, click: Click) = Unit

private inner class CanvasPart(private val x: Int, private val y: Int) : AbstractItem() {

private val colors = IntArray(itemResolution * itemResolution)

override fun getItemProvider(viewer: Player): ItemProvider {
// read colors from image
image.getRGB(
x * itemResolution, y * itemResolution,
itemResolution, itemResolution,
colors,
0,
itemResolution
)

// write colors to item stack
val itemStack = canvasItem.clientsideProvider.get().unwrap().copy()
itemStack.set(
DataComponents.CUSTOM_MODEL_DATA,
CustomModelData(
emptyList(),
emptyList(),
emptyList(),
IntArrayList(colors)
)
)

val builder = ItemBuilder(itemStack.asBukkitMirror())
modifyItemBuilder(x, y, viewer, builder)
return builder
}

override fun handleClick(clickType: ClickType, player: Player, click: Click) {
handleClick(x, y, click)
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ internal object ImageUtils {
@JvmStatic
fun createImageFromArgbRaster(width: Int, raster: IntArray): BufferedImage {
// https://stackoverflow.com/questions/14416107/int-array-to-bufferedimage
val sm = SinglePixelPackedSampleModel(DataBuffer.TYPE_INT, width, 16, ARGB_BIT_MASKS)
val sm = SinglePixelPackedSampleModel(DataBuffer.TYPE_INT, width, raster.size / width, ARGB_BIT_MASKS)
val db = DataBufferInt(raster, raster.size)
val wr = Raster.createWritableRaster(sm, db, Point())
return BufferedImage(ColorModel.getRGBdefault(), wr, false, null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import net.kyori.adventure.key.Key
import xyz.xenondevs.nova.initialize.InternalInit
import xyz.xenondevs.nova.initialize.InternalInitStage
import xyz.xenondevs.nova.resources.builder.ResourcePackBuilder
import xyz.xenondevs.nova.resources.builder.layout.item.ItemModelCreationScope
import xyz.xenondevs.nova.resources.builder.layout.item.ItemModelDefinitionBuilder
import xyz.xenondevs.nova.resources.builder.layout.item.ItemModelSelectorScope
import xyz.xenondevs.nova.util.data.writeImage
Expand Down Expand Up @@ -97,6 +98,17 @@ object DefaultGuiItems {
val STOPWATCH = guiItem("stopwatch")
//</editor-fold>

/**
* An 18x18 scale 1 [canvas][ItemModelCreationScope.canvasModel].
*/
val CANVAS = item("gui/canvas") {
hidden(true)
name(null)
modelDefinition {
model = canvasModel(18, 18)
}
}

//<editor-fold desc="without background">
// legacy InvUI gui items
val TP_LINE_CORNER_BOTTOM_LEFT = tpGuiItem("line/corner_bottom_left", null, true)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"parent": "nova:item/gui_item",
"textures": {
"0": "nova:item/white"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 812dcb8

Please sign in to comment.