From b4afcbb5c55caef6cf489c48ad1d305c120d96c4 Mon Sep 17 00:00:00 2001 From: HyperPigeon <58276133+HyperPigeon@users.noreply.github.com> Date: Sat, 10 Oct 2026 19:12:59 -0400 Subject: [PATCH 1/9] Added ability to tweak the rotation and offset of individual model parts for entity being displayed by entity display block --- .../entity/EntityDisplayBlockEntity.java | 34 ++++ .../screen/ingame/DisplayBlockEditScreen.java | 12 +- .../ingame/EntityDisplayEditScreen.java | 150 ++++++++++++++++++ .../EntityDisplayBlockEntityRenderer.java | 8 + .../glowcase/client/util/ModelPartUtil.java | 64 ++++++++ .../mixin/client/EntityRenderStateMixin.java | 25 +++ .../client/LivingEntityRendererMixin.java | 18 +++ .../client/ModelFeatureRendererMixin.java | 18 +++ .../mixinsupport/PartPosedRenderState.java | 10 ++ .../packet/C2SEditEntityDisplayBlock.java | 21 ++- .../glowcase/util/PartPoseInfo.java | 90 +++++++++++ .../resources/assets/glowcase/lang/en_us.json | 4 +- src/main/resources/glowcase.classtweaker | 2 + src/main/resources/glowcase.mixins.json | 3 + 14 files changed, 450 insertions(+), 9 deletions(-) create mode 100644 src/main/java/dev/hephaestus/glowcase/client/util/ModelPartUtil.java create mode 100644 src/main/java/dev/hephaestus/glowcase/mixin/client/EntityRenderStateMixin.java create mode 100644 src/main/java/dev/hephaestus/glowcase/mixin/client/LivingEntityRendererMixin.java create mode 100644 src/main/java/dev/hephaestus/glowcase/mixin/client/ModelFeatureRendererMixin.java create mode 100644 src/main/java/dev/hephaestus/glowcase/mixinsupport/PartPosedRenderState.java create mode 100644 src/main/java/dev/hephaestus/glowcase/util/PartPoseInfo.java diff --git a/src/main/java/dev/hephaestus/glowcase/block/entity/EntityDisplayBlockEntity.java b/src/main/java/dev/hephaestus/glowcase/block/entity/EntityDisplayBlockEntity.java index 6db86155..c000bd8f 100644 --- a/src/main/java/dev/hephaestus/glowcase/block/entity/EntityDisplayBlockEntity.java +++ b/src/main/java/dev/hephaestus/glowcase/block/entity/EntityDisplayBlockEntity.java @@ -1,6 +1,8 @@ package dev.hephaestus.glowcase.block.entity; +import com.mojang.serialization.Codec; import dev.hephaestus.glowcase.Glowcase; +import dev.hephaestus.glowcase.util.PartPoseInfo; import net.minecraft.core.BlockPos; import net.minecraft.core.HolderLookup; import net.minecraft.core.registries.Registries; @@ -19,6 +21,10 @@ import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + @NullMarked public class EntityDisplayBlockEntity extends DisplayBlockEntity implements StackInteractable { public static final TagKey> TICK = TagKey.create(Registries.ENTITY_TYPE, Glowcase.id("tick_in_display")); @@ -26,6 +32,7 @@ public class EntityDisplayBlockEntity extends DisplayBlockEntity implements Stac protected int entityId; protected @Nullable Entity displayEntity = null; protected @Nullable EntityType entityType = null; + protected Map partPoses = new HashMap<>(); public EntityDisplayBlockEntity(BlockPos pos, BlockState state) { super(Glowcase.ENTITY_DISPLAY_BLOCK_ENTITY.get(), pos, state); @@ -82,12 +89,35 @@ public static void tick(Level world, BlockPos blockPos, BlockState state, Entity } } + public Map getPartPoses(){ + return partPoses; + } + + public void loadPartPoses(Map newPartPoses) { + this.partPoses.putAll(newPartPoses); + } + + public PartPoseInfo getPartPostInfo(String name) { + if(partPoses.containsKey(name)) { + return partPoses.get(name); + } + return new PartPoseInfo(name,new Vector3f(0), new Vector3f(0)); + } + + public void setPartPoseInfo(String name, PartPoseInfo partPoseInfo) { + this.partPoses.put(name,partPoseInfo); + } + + @Override protected void saveAdditional(ValueOutput view) { super.saveAdditional(view); if (entityType != null) { view.store("type", EntityType.CODEC, entityType); } + if(!partPoses.isEmpty()) { + view.store("partPoses", Codec.unboundedMap(Codec.STRING, PartPoseInfo.CODEC), partPoses); + } } @Override @@ -97,6 +127,10 @@ protected void loadAdditional(ValueInput view) { this.entityId = view.getIntOr("entityId", 0); this.entityType = view.read("type", EntityType.CODEC).orElse(null); this.displayEntity = null; + + this.partPoses.clear(); + Optional> optionalStringPartPoseInfoMap = view.read("partPoses", Codec.unboundedMap(Codec.STRING, PartPoseInfo.CODEC)); + optionalStringPartPoseInfoMap.ifPresent(stringPartPoseInfoMap -> this.partPoses.putAll(stringPartPoseInfoMap)); } @Override diff --git a/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/DisplayBlockEditScreen.java b/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/DisplayBlockEditScreen.java index 85c1429e..c80c5eef 100644 --- a/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/DisplayBlockEditScreen.java +++ b/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/DisplayBlockEditScreen.java @@ -197,12 +197,12 @@ public void init() { @Override public void extractRenderState(GuiGraphicsExtractor graphics, int mouseX, int mouseY, float delta) { super.extractRenderState(graphics, mouseX, mouseY, delta); - graphics.text(minecraft.font, Component.translatable("gui.glowcase.scale_label"), 20, 17, 0xFFFFFFFF); - graphics.text(minecraft.font, Component.translatable("gui.glowcase.x_offset_label"), 20, 47, 0xFFFFFFFF); - graphics.text(minecraft.font, Component.translatable("gui.glowcase.y_offset_label"), 20, 77, 0xFFFFFFFF); - graphics.text(minecraft.font, Component.translatable("gui.glowcase.z_offset_label"), 20, 107, 0xFFFFFFFF); - graphics.text(minecraft.font, Component.translatable("gui.glowcase.pitch_value"), 20, 137, 0xFFFFFFFF); - graphics.text(minecraft.font, Component.translatable("gui.glowcase.yaw_value"), 20, 167, 0xFFFFFFFF); + graphics.text(minecraft.font, Component.translatable("gui.glowcase.scale_label"), 20, this.scaleField.getY() + 7, 0xFFFFFFFF); + graphics.text(minecraft.font, Component.translatable("gui.glowcase.x_offset_label"), 20, this.xOffsetField.getY() + 7, 0xFFFFFFFF); + graphics.text(minecraft.font, Component.translatable("gui.glowcase.y_offset_label"), 20, this.yOffsetField.getY() + 7, 0xFFFFFFFF); + graphics.text(minecraft.font, Component.translatable("gui.glowcase.z_offset_label"), 20, this.zOffsetField.getY() + 7, 0xFFFFFFFF); + graphics.text(minecraft.font, Component.translatable("gui.glowcase.pitch_value"), 20, this.pitchField.getY() + 7, 0xFFFFFFFF); + graphics.text(minecraft.font, Component.translatable("gui.glowcase.yaw_value"), 20, this.yawField.getY() + 7, 0xFFFFFFFF); } protected final void editDisplayBlock() { diff --git a/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/EntityDisplayEditScreen.java b/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/EntityDisplayEditScreen.java index 21321f2d..6ba6e484 100644 --- a/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/EntityDisplayEditScreen.java +++ b/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/EntityDisplayEditScreen.java @@ -1,16 +1,166 @@ package dev.hephaestus.glowcase.client.gui.screen.ingame; +import com.google.common.primitives.Floats; import dev.hephaestus.glowcase.block.entity.DisplayBlockEntity; +import dev.hephaestus.glowcase.block.entity.EntityDisplayBlockEntity; +import dev.hephaestus.glowcase.client.util.ModelPartUtil; import dev.hephaestus.glowcase.packet.C2SEditEntityDisplayBlock; +import dev.hephaestus.glowcase.util.PartPoseInfo; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.StringWidget; +import net.minecraft.client.gui.components.events.GuiEventListener; +import net.minecraft.client.model.geom.ModelPart; +import net.minecraft.network.chat.Component; import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.EntityType; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; public class EntityDisplayEditScreen extends DisplayBlockEditScreen { + + private final EntityDisplayBlockEntity entityDisplay; + private final Set expandedParts = new HashSet<>(); + private boolean expanded = false; + private int STARTING_X = 10; + private int STARTING_Y = 200; + private int scrollOffset = 0; + private int maxScroll = 0; + + public EntityDisplayEditScreen(DisplayBlockEntity displayBlock) { super(displayBlock); + this.entityDisplay = (EntityDisplayBlockEntity) displayBlock; + } + + @Override + public void init() { + super.init(); + + this.addRenderableWidget(Button.builder( + Component.literal(this.expanded ? "▼ " : "▶ ").append(Component.translatable("gui.glowcase.model_parts")), + _ -> { + this.expanded = !this.expanded; + this.rebuildWidgets(); + } + ).bounds(STARTING_X, STARTING_Y, 200, 20).build()); + + int bottom = STARTING_Y + 22; + if(this.entityDisplay.getDisplayEntity() != null) { + EntityType type = this.entityDisplay.getDisplayEntity().getType(); + ModelPart root = ModelPartUtil.getModelRoot(type); + if (this.expanded && root != null) { + bottom = this.addPartRows(root); + } + } + + this.maxScroll = Math.max(0, bottom + 10 - this.height); + this.scrollOffset = Mth.clamp(this.scrollOffset, 0, this.maxScroll); + this.offsetWidgets(-this.scrollOffset); + + + } + + private int addPartRows(ModelPart parent) { + int y = STARTING_Y + 22; + for (Map.Entry entry : parent.children.entrySet()) { + String name = entry.getKey(); + boolean partExpanded = this.expandedParts.contains(name); + String displayName = name.replace("_", " "); + displayName = displayName.substring(0, 1).toUpperCase() + displayName.substring(1); + + this.addRenderableWidget(Button.builder(Component.literal((partExpanded ? "▼ " : "▶ ") + displayName), _ -> { + if (!this.expandedParts.remove(name)) this.expandedParts.add(name); + this.rebuildWidgets(); + }).bounds(STARTING_X + 10, y, 190, 20).build()); + + y += 22; + if (!partExpanded) continue; + + for (int i = 0; i <= 1; i++) { + boolean isRotation = i == 0; + Component label = Component.translatable(isRotation ? "gui.glowcase.rotation" : "gui.glowcase.offset"); + this.addRenderableWidget(new StringWidget(STARTING_X + 20, y + 5, this.font.width(label), 9, label, this.font)); + + float step = isRotation ? 5F : 0.25F; + for (int axis = 0; axis < 3; axis++) { + int column = STARTING_X + 70 + axis * 84; + int a = axis; + PartPoseInfo partPoseInfo = this.entityDisplay.getPartPostInfo(name); + + EditBox field = new EditBox(this.font, column, y, 44, 18, Component.empty()); + field.setValue(String.valueOf((isRotation ? partPoseInfo.rotation() : partPoseInfo.offset()).get(a))); + field.setResponder(string -> { + if (Floats.tryParse(string) instanceof Float parsed) { + if(isRotation) { + entityDisplay.setPartPoseInfo(name, entityDisplay.getPartPostInfo(name).updateRotationAtAxis(a,parsed)); + } + else { + entityDisplay.setPartPoseInfo(name, entityDisplay.getPartPostInfo(name).updateOffsetAtAxis(a,parsed)); + } + editDisplayBlock(); + } + }); + + this.addRenderableWidget(field); + this.addRenderableWidget(Button.builder(Component.literal("-"), _ -> { + PartPoseInfo newValue = this.entityDisplay.getPartPostInfo(name); + if(isRotation) { + newValue = newValue.updateRotationAtAxis(a,newValue.getRotationAtAxis(a)-step); + this.entityDisplay.setPartPoseInfo(name,newValue); + } + else { + newValue = newValue.updateOffsetAtAxis(a,newValue.getOffsetAtAxis(a)-step); + this.entityDisplay.setPartPoseInfo(name,newValue); + } + editDisplayBlock(); + field.setValue(String.valueOf((isRotation ? newValue.getRotationAtAxis(a) : newValue.getOffsetAtAxis(a)))); + }).bounds(column + 46, y, 18, 18).build()); + this.addRenderableWidget(Button.builder(Component.literal("+"), _ -> { + PartPoseInfo newValue = this.entityDisplay.getPartPostInfo(name); + if(isRotation) { + newValue = newValue.updateRotationAtAxis(a,newValue.getRotationAtAxis(a)+step); + this.entityDisplay.setPartPoseInfo(name,newValue); + } + else { + newValue = newValue.updateOffsetAtAxis(a,newValue.getOffsetAtAxis(a)+step); + this.entityDisplay.setPartPoseInfo(name,newValue); + } + editDisplayBlock(); + field.setValue(String.valueOf((isRotation ? newValue.getRotationAtAxis(a) : newValue.getOffsetAtAxis(a)))); + }).bounds(column + 64, y, 18, 18).build()); + } + y += 22; + } + } + return y; + + } + + @Override + public boolean mouseScrolled(double x, double y, double scrollX, double scrollY) { + int newOffset = Mth.clamp(this.scrollOffset - (int) (scrollY * 20), 0, this.maxScroll); + if (newOffset != this.scrollOffset) { + this.offsetWidgets(this.scrollOffset - newOffset); + this.scrollOffset = newOffset; + return true; + } + return super.mouseScrolled(x, y, scrollX, scrollY); + } + + private void offsetWidgets(int scrollOffset) { + for (GuiEventListener child : this.children()) { + if (child instanceof AbstractWidget widget) widget.setY(widget.getY() + scrollOffset); + } } @Override public CustomPacketPayload getUpdatePayload() { return C2SEditEntityDisplayBlock.of(blockEntity); } + } diff --git a/src/main/java/dev/hephaestus/glowcase/client/render/block/entity/EntityDisplayBlockEntityRenderer.java b/src/main/java/dev/hephaestus/glowcase/client/render/block/entity/EntityDisplayBlockEntityRenderer.java index 5d773373..55b8faef 100644 --- a/src/main/java/dev/hephaestus/glowcase/client/render/block/entity/EntityDisplayBlockEntityRenderer.java +++ b/src/main/java/dev/hephaestus/glowcase/client/render/block/entity/EntityDisplayBlockEntityRenderer.java @@ -5,6 +5,8 @@ import dev.hephaestus.glowcase.Glowcase; import dev.hephaestus.glowcase.block.entity.EntityDisplayBlockEntity; import dev.hephaestus.glowcase.client.util.BlockEntityRenderUtil; +import dev.hephaestus.glowcase.mixinsupport.PartPosedRenderState; +import dev.hephaestus.glowcase.util.PartPoseInfo; import net.minecraft.client.renderer.SubmitNodeCollector; import net.minecraft.client.renderer.blockentity.BlockEntityRenderer; import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider; @@ -19,6 +21,8 @@ import org.joml.Vector3f; import org.jspecify.annotations.Nullable; +import java.util.Map; + public record EntityDisplayBlockEntityRenderer(BlockEntityRendererProvider.Context context) implements BlockEntityRenderer { public static Identifier ITEM_TEXTURE = Glowcase.id("textures/item/entity_display_block.png"); @@ -30,6 +34,8 @@ public static class EntityDisplayRenderState extends BlockEntityRenderState { public Vector3f scale; public Entity entity; + + public Map partPoseInfoMap; } @Override @@ -47,6 +53,7 @@ public void extractRenderState(EntityDisplayBlockEntity blockEntity, EntityDispl // TODO: hmm, yes, shuttle the *entire entity* to the render state // Perhaps shuttle the entity renderer & its state instead? state.entity = blockEntity.getDisplayEntity(); + state.partPoseInfoMap = blockEntity.getPartPoses(); } @Override @@ -62,6 +69,7 @@ public void submit(EntityDisplayRenderState state, PoseStack poseStack, SubmitNo //noinspection unchecked EntityRenderer entityRenderer = (EntityRenderer) context.entityRenderer().getRenderer(renderEntity); EntityRenderState entityRenderState = entityRenderer.createRenderState(renderEntity, 0); + ((PartPosedRenderState) entityRenderState).glowcase$setPartPoses(state.partPoseInfoMap.isEmpty() ? null : state.partPoseInfoMap); entityRenderer.submit(entityRenderState, poseStack, submitNodeCollector, camera); } diff --git a/src/main/java/dev/hephaestus/glowcase/client/util/ModelPartUtil.java b/src/main/java/dev/hephaestus/glowcase/client/util/ModelPartUtil.java new file mode 100644 index 00000000..5fb0bc58 --- /dev/null +++ b/src/main/java/dev/hephaestus/glowcase/client/util/ModelPartUtil.java @@ -0,0 +1,64 @@ +package dev.hephaestus.glowcase.client.util; + +import dev.hephaestus.glowcase.mixinsupport.PartPosedRenderState; +import dev.hephaestus.glowcase.util.PartPoseInfo; +import net.minecraft.client.Minecraft; +import net.minecraft.client.model.Model; +import net.minecraft.client.model.geom.ModelPart; +import net.minecraft.client.multiplayer.ClientLevel; +import net.minecraft.client.renderer.entity.EntityRenderer; +import net.minecraft.client.renderer.entity.LivingEntityRenderer; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import org.jetbrains.annotations.Nullable; +import org.joml.Vector3fc; + +import java.util.Map; + +public class ModelPartUtil { + + @Nullable + public static ModelPart getModelRoot(EntityType type) { + Minecraft minecraft = Minecraft.getInstance(); + ClientLevel level = minecraft.level; + if (level == null) return null; + + Entity entity = type.create(level, EntitySpawnReason.EVENT); + if (entity == null) return null; + + EntityRenderer renderer = minecraft.getEntityRenderDispatcher().getRenderer(entity); + if (renderer instanceof LivingEntityRenderer living) { + return living.getModel().root(); + } + return null; + } + + public static void applyPartPoses(Model model, Object state) { + if (!(state instanceof PartPosedRenderState posed)) { + return; + } + Map partPoses = posed.glowcase$getPartPoses(); + if (partPoses == null) { + return; + } + + Map children = model.root().children; + for (PartPoseInfo partPoseInfo : partPoses.values()) { + ModelPart part = children.get(partPoseInfo.name()); + if (part == null) { + continue; + } + + Vector3fc offset = partPoseInfo.offset(); + Vector3fc rotation = partPoseInfo.rotation(); + part.x += offset.x(); + part.y += offset.y(); + part.z += offset.z(); + part.xRot += rotation.x() * Mth.DEG_TO_RAD; + part.yRot += rotation.y() * Mth.DEG_TO_RAD; + part.zRot += rotation.z() * Mth.DEG_TO_RAD; + } + } +} diff --git a/src/main/java/dev/hephaestus/glowcase/mixin/client/EntityRenderStateMixin.java b/src/main/java/dev/hephaestus/glowcase/mixin/client/EntityRenderStateMixin.java new file mode 100644 index 00000000..af053f50 --- /dev/null +++ b/src/main/java/dev/hephaestus/glowcase/mixin/client/EntityRenderStateMixin.java @@ -0,0 +1,25 @@ +package dev.hephaestus.glowcase.mixin.client; + +import dev.hephaestus.glowcase.mixinsupport.PartPosedRenderState; +import dev.hephaestus.glowcase.util.PartPoseInfo; +import net.minecraft.client.renderer.entity.state.EntityRenderState; +import org.jspecify.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; + +import java.util.Map; + +@Mixin(EntityRenderState.class) +public class EntityRenderStateMixin implements PartPosedRenderState { + @Unique private @Nullable Map glowcase$partPoses; + + @Override + public void glowcase$setPartPoses(@Nullable Map partPoses) { + this.glowcase$partPoses = partPoses; + } + + @Override + public @Nullable Map glowcase$getPartPoses() { + return glowcase$partPoses; + } +} diff --git a/src/main/java/dev/hephaestus/glowcase/mixin/client/LivingEntityRendererMixin.java b/src/main/java/dev/hephaestus/glowcase/mixin/client/LivingEntityRendererMixin.java new file mode 100644 index 00000000..ef66a627 --- /dev/null +++ b/src/main/java/dev/hephaestus/glowcase/mixin/client/LivingEntityRendererMixin.java @@ -0,0 +1,18 @@ +package dev.hephaestus.glowcase.mixin.client; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import dev.hephaestus.glowcase.client.util.ModelPartUtil; +import net.minecraft.client.model.EntityModel; +import net.minecraft.client.renderer.entity.LivingEntityRenderer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(LivingEntityRenderer.class) +public class LivingEntityRendererMixin { + @WrapOperation(method = "submit", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/model/EntityModel;setupAnim(Ljava/lang/Object;)V")) + private void applyPartPoses(EntityModel model, Object state, Operation original) { + original.call(model, state); + ModelPartUtil.applyPartPoses(model, state); + } +} diff --git a/src/main/java/dev/hephaestus/glowcase/mixin/client/ModelFeatureRendererMixin.java b/src/main/java/dev/hephaestus/glowcase/mixin/client/ModelFeatureRendererMixin.java new file mode 100644 index 00000000..d046507e --- /dev/null +++ b/src/main/java/dev/hephaestus/glowcase/mixin/client/ModelFeatureRendererMixin.java @@ -0,0 +1,18 @@ +package dev.hephaestus.glowcase.mixin.client; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import dev.hephaestus.glowcase.client.util.ModelPartUtil; +import net.minecraft.client.model.Model; +import net.minecraft.client.renderer.feature.ModelFeatureRenderer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(ModelFeatureRenderer.class) +public class ModelFeatureRendererMixin { + @WrapOperation(method = "prepareModel", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/model/Model;setupAnim(Ljava/lang/Object;)V")) + private void applyPartPoses(Model model, Object state, Operation original) { + original.call(model, state); + ModelPartUtil.applyPartPoses(model, state); + } +} diff --git a/src/main/java/dev/hephaestus/glowcase/mixinsupport/PartPosedRenderState.java b/src/main/java/dev/hephaestus/glowcase/mixinsupport/PartPosedRenderState.java new file mode 100644 index 00000000..4e6585b4 --- /dev/null +++ b/src/main/java/dev/hephaestus/glowcase/mixinsupport/PartPosedRenderState.java @@ -0,0 +1,10 @@ +package dev.hephaestus.glowcase.mixinsupport; + +import dev.hephaestus.glowcase.util.PartPoseInfo; +import org.jspecify.annotations.Nullable; +import java.util.Map; + +public interface PartPosedRenderState { + void glowcase$setPartPoses(@Nullable Map partPoses); + @Nullable Map glowcase$getPartPoses(); +} diff --git a/src/main/java/dev/hephaestus/glowcase/packet/C2SEditEntityDisplayBlock.java b/src/main/java/dev/hephaestus/glowcase/packet/C2SEditEntityDisplayBlock.java index bce1d00d..750727a7 100644 --- a/src/main/java/dev/hephaestus/glowcase/packet/C2SEditEntityDisplayBlock.java +++ b/src/main/java/dev/hephaestus/glowcase/packet/C2SEditEntityDisplayBlock.java @@ -2,24 +2,38 @@ import dev.hephaestus.glowcase.Glowcase; import dev.hephaestus.glowcase.block.entity.DisplayBlockEntity; +import dev.hephaestus.glowcase.block.entity.EntityDisplayBlockEntity; import dev.hephaestus.glowcase.util.DisplayBlockSettings; +import dev.hephaestus.glowcase.util.PartPoseInfo; +import io.netty.buffer.ByteBuf; import net.minecraft.core.BlockPos; import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; import net.minecraft.network.codec.StreamCodec; import net.minecraft.network.protocol.common.custom.CustomPacketPayload; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.level.block.entity.BlockEntity; -public record C2SEditEntityDisplayBlock(BlockPos pos, DisplayBlockSettings settings) implements C2SEditBlockEntity, C2SEditDisplayBlock { +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public record C2SEditEntityDisplayBlock(BlockPos pos, DisplayBlockSettings settings, Optional> partPoses) implements C2SEditBlockEntity, C2SEditDisplayBlock { public static final Type ID = new Type<>(Glowcase.id("channel.entity_display")); + + private static final StreamCodec> PART_POSES_CODEC = + ByteBufCodecs.map(HashMap::new, ByteBufCodecs.stringUtf8(64), PartPoseInfo.PACKET_CODEC, 256); + public static final StreamCodec PACKET_CODEC = StreamCodec.composite( BlockPos.STREAM_CODEC, C2SEditEntityDisplayBlock::pos, DisplayBlockSettings.PACKET_CODEC, C2SEditEntityDisplayBlock::settings, + ByteBufCodecs.optional(PART_POSES_CODEC), C2SEditEntityDisplayBlock::partPoses, C2SEditEntityDisplayBlock::new ); public static C2SEditEntityDisplayBlock of(DisplayBlockEntity be) { - return new C2SEditEntityDisplayBlock(be.getBlockPos(), be.toSettings()); + EntityDisplayBlockEntity entityDisplayBlock = (EntityDisplayBlockEntity)be; + return new C2SEditEntityDisplayBlock(be.getBlockPos(), be.toSettings(), Optional.of(entityDisplayBlock.getPartPoses())); } @Override @@ -29,6 +43,9 @@ public Type type() { @Override public void receive(ServerLevel world, BlockEntity blockEntity) { + if(blockEntity instanceof EntityDisplayBlockEntity entityDisplayBlock) { + partPoses().ifPresent(entityDisplayBlock::loadPartPoses); + } C2SEditDisplayBlock.super.receive(world, blockEntity); } } diff --git a/src/main/java/dev/hephaestus/glowcase/util/PartPoseInfo.java b/src/main/java/dev/hephaestus/glowcase/util/PartPoseInfo.java new file mode 100644 index 00000000..ad461ad5 --- /dev/null +++ b/src/main/java/dev/hephaestus/glowcase/util/PartPoseInfo.java @@ -0,0 +1,90 @@ +package dev.hephaestus.glowcase.util; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import io.netty.buffer.ByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.util.ExtraCodecs; +import org.joml.Vector3f; +import org.joml.Vector3fc; + +public record PartPoseInfo(String name, Vector3fc offset, Vector3fc rotation) { + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + Codec.STRING.fieldOf("name").forGetter(PartPoseInfo::name), + ExtraCodecs.VECTOR3F.lenientOptionalFieldOf("offset", new Vector3f()).forGetter(PartPoseInfo::offset), + ExtraCodecs.VECTOR3F.lenientOptionalFieldOf("rotation", new Vector3f()).forGetter(PartPoseInfo::rotation) + ).apply(instance, PartPoseInfo::new)); + + public static final StreamCodec PACKET_CODEC = StreamCodec.composite( + ByteBufCodecs.STRING_UTF8, PartPoseInfo::name, + ByteBufCodecs.VECTOR3F, PartPoseInfo::offset, + ByteBufCodecs.VECTOR3F, PartPoseInfo::rotation, + PartPoseInfo::new + ); + + public float getRotationAtAxis(int axis){ + if(axis == 0) { + return rotation().x(); + } + else if (axis == 1) { + return rotation().y(); + } + else { + return rotation().z(); + } + } + + public float getOffsetAtAxis(int axis){ + if(axis == 0) { + return offset().x(); + } + else if (axis == 1) { + return offset().y(); + } + else { + return offset().z(); + } + } + + public PartPoseInfo updateRotationAtAxis(int axis, float newRotation) { + Vector3f newRotationVec; + if(axis == 0) { + newRotationVec = new Vector3f(newRotation, rotation.y(), rotation.z()); + return new PartPoseInfo(name(), offset(), newRotationVec); + } + else if (axis == 1) { + newRotationVec = new Vector3f(rotation.x(), newRotation, rotation.z()); + return new PartPoseInfo(name(), offset(), newRotationVec); + } + else { + newRotationVec = new Vector3f(rotation.x(), rotation().y(), newRotation); + return new PartPoseInfo(name(), offset(), newRotationVec); + } + } + + public PartPoseInfo updateOffsetAtAxis(int axis, float newOffset) { + Vector3f newOffsetVec; + if(axis == 0) { + newOffsetVec = new Vector3f(newOffset, offset().y(), offset().z()); + return new PartPoseInfo(name(), newOffsetVec, rotation()); + } + else if (axis == 1) { + newOffsetVec = new Vector3f(offset().x(), newOffset, offset().z()); + return new PartPoseInfo(name(), newOffsetVec, rotation()); + } + else { + newOffsetVec = new Vector3f(offset().x(), offset().y(), newOffset); + return new PartPoseInfo(name(), newOffsetVec, rotation()); + } + } + + public PartPoseInfo updateRotation(Vector3f newRotation) { + return new PartPoseInfo(name(), offset(), newRotation); + } + + public PartPoseInfo updateOffset(Vector3f newOffset) { + return new PartPoseInfo(name(), newOffset, rotation()); + } + +} diff --git a/src/main/resources/assets/glowcase/lang/en_us.json b/src/main/resources/assets/glowcase/lang/en_us.json index 6b0a901c..cb042823 100644 --- a/src/main/resources/assets/glowcase/lang/en_us.json +++ b/src/main/resources/assets/glowcase/lang/en_us.json @@ -198,5 +198,7 @@ "gui.glowcase.underline": "Underline (Ctrl+U)", "gui.glowcase.obfuscate": "Obfuscate (Ctrl+O)", "gui.glowcase.color_text": "Color Text (Via QuickText Tags)", - "gui.glowcase.insert_font": "Insert Font (Via QuickText Tags)" + "gui.glowcase.insert_font": "Insert Font (Via QuickText Tags)", + "gui.glowcase.model_parts": "Model Parts", + "gui.glowcase.rotation": "Rotation" } diff --git a/src/main/resources/glowcase.classtweaker b/src/main/resources/glowcase.classtweaker index 579e1447..fa8d2f40 100644 --- a/src/main/resources/glowcase.classtweaker +++ b/src/main/resources/glowcase.classtweaker @@ -40,3 +40,5 @@ accessible class net/minecraft/client/gui/components/MultilineTextField$StringVi accessible field net/minecraft/client/gui/components/EditBox displayPos I accessible field net/minecraft/client/gui/components/EditBox highlightPos I + +accessible field net/minecraft/client/model/geom/ModelPart children Ljava/util/Map; diff --git a/src/main/resources/glowcase.mixins.json b/src/main/resources/glowcase.mixins.json index e7cec271..00443509 100644 --- a/src/main/resources/glowcase.mixins.json +++ b/src/main/resources/glowcase.mixins.json @@ -13,12 +13,15 @@ ], "client": [ "AbstractContainerScreenInvoker", + "client.EntityRenderStateMixin", "client.FirstPersonHandsAndItemsRendererMixin", "client.FontAccessor", "client.GameRendererAccessor", "client.GuiRendererMixin", "client.HudMixin", "client.KeyboardHandlerMixin", + "client.LivingEntityRendererMixin", + "client.ModelFeatureRendererMixin", "client.TextureManagerAccessor", "client.bakedbe.BlockEntityRenderDispatcherMixin", "client.bakedbe.BlockEntityRenderStateMixin", From d149e1f0c87de5da148fe8bfacf26c707a4fe8b8 Mon Sep 17 00:00:00 2001 From: HyperPigeon <58276133+HyperPigeon@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:08:04 -0400 Subject: [PATCH 2/9] - fixed issue where part pose data was not clearing when clearing entity display block - recurse into child parts - updated getModelRoot to take entity instead of entity type as parameter --- .../entity/EntityDisplayBlockEntity.java | 5 +++ .../ingame/EntityDisplayEditScreen.java | 33 +++++++-------- .../glowcase/client/util/ModelPartUtil.java | 40 +++++++++++-------- 3 files changed, 46 insertions(+), 32 deletions(-) diff --git a/src/main/java/dev/hephaestus/glowcase/block/entity/EntityDisplayBlockEntity.java b/src/main/java/dev/hephaestus/glowcase/block/entity/EntityDisplayBlockEntity.java index c000bd8f..4cbbaada 100644 --- a/src/main/java/dev/hephaestus/glowcase/block/entity/EntityDisplayBlockEntity.java +++ b/src/main/java/dev/hephaestus/glowcase/block/entity/EntityDisplayBlockEntity.java @@ -60,6 +60,7 @@ public void setFromStack(ItemStack stack) { @Override public void unsetFromStack() { + clearPartPoses(); setDisplayEntity(null); } @@ -97,6 +98,10 @@ public void loadPartPoses(Map newPartPoses) { this.partPoses.putAll(newPartPoses); } + public void clearPartPoses(){ + this.partPoses.clear(); + } + public PartPoseInfo getPartPostInfo(String name) { if(partPoses.containsKey(name)) { return partPoses.get(name); diff --git a/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/EntityDisplayEditScreen.java b/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/EntityDisplayEditScreen.java index 6ba6e484..fdf7eeba 100644 --- a/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/EntityDisplayEditScreen.java +++ b/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/EntityDisplayEditScreen.java @@ -15,7 +15,7 @@ import net.minecraft.network.chat.Component; import net.minecraft.network.protocol.common.custom.CustomPacketPayload; import net.minecraft.util.Mth; -import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Entity; import java.util.HashSet; import java.util.Map; @@ -41,20 +41,20 @@ public EntityDisplayEditScreen(DisplayBlockEntity displayBlock) { public void init() { super.init(); - this.addRenderableWidget(Button.builder( - Component.literal(this.expanded ? "▼ " : "▶ ").append(Component.translatable("gui.glowcase.model_parts")), - _ -> { - this.expanded = !this.expanded; - this.rebuildWidgets(); - } - ).bounds(STARTING_X, STARTING_Y, 200, 20).build()); - int bottom = STARTING_Y + 22; - if(this.entityDisplay.getDisplayEntity() != null) { - EntityType type = this.entityDisplay.getDisplayEntity().getType(); - ModelPart root = ModelPartUtil.getModelRoot(type); + Entity displayEntity = this.entityDisplay.getDisplayEntity(); + if(displayEntity != null) { + this.addRenderableWidget(Button.builder( + Component.literal(this.expanded ? "▼ " : "▶ ").append(Component.translatable("gui.glowcase.model_parts")), + _ -> { + this.expanded = !this.expanded; + this.rebuildWidgets(); + } + ).bounds(STARTING_X, STARTING_Y, 200, 20).build()); + + ModelPart root = ModelPartUtil.getModelRoot(displayEntity); if (this.expanded && root != null) { - bottom = this.addPartRows(root); + bottom = this.addPartRows(root, bottom, 0); } } @@ -65,8 +65,8 @@ public void init() { } - private int addPartRows(ModelPart parent) { - int y = STARTING_Y + 22; + private int addPartRows(ModelPart parent, int y, int depth) { + int indent = Math.min(depth, 6) * 10; for (Map.Entry entry : parent.children.entrySet()) { String name = entry.getKey(); boolean partExpanded = this.expandedParts.contains(name); @@ -76,7 +76,7 @@ private int addPartRows(ModelPart parent) { this.addRenderableWidget(Button.builder(Component.literal((partExpanded ? "▼ " : "▶ ") + displayName), _ -> { if (!this.expandedParts.remove(name)) this.expandedParts.add(name); this.rebuildWidgets(); - }).bounds(STARTING_X + 10, y, 190, 20).build()); + }).bounds(STARTING_X + 10 + indent, y, 190 - indent, 20).build()); y += 22; if (!partExpanded) continue; @@ -136,6 +136,7 @@ private int addPartRows(ModelPart parent) { } y += 22; } + y = this.addPartRows(entry.getValue(), y, depth + 1); } return y; diff --git a/src/main/java/dev/hephaestus/glowcase/client/util/ModelPartUtil.java b/src/main/java/dev/hephaestus/glowcase/client/util/ModelPartUtil.java index 5fb0bc58..300abd52 100644 --- a/src/main/java/dev/hephaestus/glowcase/client/util/ModelPartUtil.java +++ b/src/main/java/dev/hephaestus/glowcase/client/util/ModelPartUtil.java @@ -20,12 +20,10 @@ public class ModelPartUtil { @Nullable - public static ModelPart getModelRoot(EntityType type) { + public static ModelPart getModelRoot(Entity entity) { Minecraft minecraft = Minecraft.getInstance(); ClientLevel level = minecraft.level; if (level == null) return null; - - Entity entity = type.create(level, EntitySpawnReason.EVENT); if (entity == null) return null; EntityRenderer renderer = minecraft.getEntityRenderDispatcher().getRenderer(entity); @@ -40,25 +38,35 @@ public static void applyPartPoses(Model model, Object state) { return; } Map partPoses = posed.glowcase$getPartPoses(); - if (partPoses == null) { + if (partPoses == null || partPoses.isEmpty()) { return; } - Map children = model.root().children; - for (PartPoseInfo partPoseInfo : partPoses.values()) { - ModelPart part = children.get(partPoseInfo.name()); - if (part == null) { + walk(model.root(), partPoses); + } + + private static void walk(ModelPart parent, Map partPoses) { + Map children = parent.children; + for (Map.Entry entry : children.entrySet()) { + ModelPart part = entry.getValue(); + String name = entry.getKey(); + if (part == null || name == null) { continue; } + if(partPoses.containsKey(name)) { + PartPoseInfo partPoseInfo = partPoses.get(name); - Vector3fc offset = partPoseInfo.offset(); - Vector3fc rotation = partPoseInfo.rotation(); - part.x += offset.x(); - part.y += offset.y(); - part.z += offset.z(); - part.xRot += rotation.x() * Mth.DEG_TO_RAD; - part.yRot += rotation.y() * Mth.DEG_TO_RAD; - part.zRot += rotation.z() * Mth.DEG_TO_RAD; + Vector3fc offset = partPoseInfo.offset(); + Vector3fc rotation = partPoseInfo.rotation(); + + part.x += offset.x(); + part.y += offset.y(); + part.z += offset.z(); + part.xRot += rotation.x() * Mth.DEG_TO_RAD; + part.yRot += rotation.y() * Mth.DEG_TO_RAD; + part.zRot += rotation.z() * Mth.DEG_TO_RAD; + } + walk(part, partPoses); } } } From 956d33c1efbff8932e673bfaf1225ec9523c3b5f Mon Sep 17 00:00:00 2001 From: HyperPigeon <58276133+HyperPigeon@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:46:34 -0400 Subject: [PATCH 3/9] - use Vec3FieldsWidget in EntityDisplayEditScreen model part tweaking --- .../ingame/EntityDisplayEditScreen.java | 58 +++---------------- 1 file changed, 8 insertions(+), 50 deletions(-) diff --git a/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/EntityDisplayEditScreen.java b/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/EntityDisplayEditScreen.java index fdf7eeba..51e72af4 100644 --- a/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/EntityDisplayEditScreen.java +++ b/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/EntityDisplayEditScreen.java @@ -1,14 +1,13 @@ package dev.hephaestus.glowcase.client.gui.screen.ingame; -import com.google.common.primitives.Floats; import dev.hephaestus.glowcase.block.entity.DisplayBlockEntity; import dev.hephaestus.glowcase.block.entity.EntityDisplayBlockEntity; +import dev.hephaestus.glowcase.client.gui.widget.ingame.number.Vec3FieldsWidget; import dev.hephaestus.glowcase.client.util.ModelPartUtil; import dev.hephaestus.glowcase.packet.C2SEditEntityDisplayBlock; import dev.hephaestus.glowcase.util.PartPoseInfo; import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.client.gui.components.Button; -import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.components.StringWidget; import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.model.geom.ModelPart; @@ -16,6 +15,7 @@ import net.minecraft.network.protocol.common.custom.CustomPacketPayload; import net.minecraft.util.Mth; import net.minecraft.world.entity.Entity; +import net.minecraft.world.phys.Vec3; import java.util.HashSet; import java.util.Map; @@ -86,54 +86,12 @@ private int addPartRows(ModelPart parent, int y, int depth) { Component label = Component.translatable(isRotation ? "gui.glowcase.rotation" : "gui.glowcase.offset"); this.addRenderableWidget(new StringWidget(STARTING_X + 20, y + 5, this.font.width(label), 9, label, this.font)); - float step = isRotation ? 5F : 0.25F; - for (int axis = 0; axis < 3; axis++) { - int column = STARTING_X + 70 + axis * 84; - int a = axis; - PartPoseInfo partPoseInfo = this.entityDisplay.getPartPostInfo(name); - - EditBox field = new EditBox(this.font, column, y, 44, 18, Component.empty()); - field.setValue(String.valueOf((isRotation ? partPoseInfo.rotation() : partPoseInfo.offset()).get(a))); - field.setResponder(string -> { - if (Floats.tryParse(string) instanceof Float parsed) { - if(isRotation) { - entityDisplay.setPartPoseInfo(name, entityDisplay.getPartPostInfo(name).updateRotationAtAxis(a,parsed)); - } - else { - entityDisplay.setPartPoseInfo(name, entityDisplay.getPartPostInfo(name).updateOffsetAtAxis(a,parsed)); - } - editDisplayBlock(); - } - }); - - this.addRenderableWidget(field); - this.addRenderableWidget(Button.builder(Component.literal("-"), _ -> { - PartPoseInfo newValue = this.entityDisplay.getPartPostInfo(name); - if(isRotation) { - newValue = newValue.updateRotationAtAxis(a,newValue.getRotationAtAxis(a)-step); - this.entityDisplay.setPartPoseInfo(name,newValue); - } - else { - newValue = newValue.updateOffsetAtAxis(a,newValue.getOffsetAtAxis(a)-step); - this.entityDisplay.setPartPoseInfo(name,newValue); - } - editDisplayBlock(); - field.setValue(String.valueOf((isRotation ? newValue.getRotationAtAxis(a) : newValue.getOffsetAtAxis(a)))); - }).bounds(column + 46, y, 18, 18).build()); - this.addRenderableWidget(Button.builder(Component.literal("+"), _ -> { - PartPoseInfo newValue = this.entityDisplay.getPartPostInfo(name); - if(isRotation) { - newValue = newValue.updateRotationAtAxis(a,newValue.getRotationAtAxis(a)+step); - this.entityDisplay.setPartPoseInfo(name,newValue); - } - else { - newValue = newValue.updateOffsetAtAxis(a,newValue.getOffsetAtAxis(a)+step); - this.entityDisplay.setPartPoseInfo(name,newValue); - } - editDisplayBlock(); - field.setValue(String.valueOf((isRotation ? newValue.getRotationAtAxis(a) : newValue.getOffsetAtAxis(a)))); - }).bounds(column + 64, y, 18, 18).build()); - } + PartPoseInfo partPoseInfo = this.entityDisplay.getPartPostInfo(name); + this.addRenderableWidget(Vec3FieldsWidget.builder(this.font, new Vec3(isRotation ? partPoseInfo.rotation() : partPoseInfo.offset())).setPos(STARTING_X + 70, y).setWidth(246).setHeight(18).setRotation(isRotation).setOnValueChange(vec3 -> { + PartPoseInfo current = this.entityDisplay.getPartPostInfo(name); + this.entityDisplay.setPartPoseInfo(name, isRotation ? current.updateRotation(vec3.toVector3f()) : current.updateOffset(vec3.toVector3f())); + editDisplayBlock(); + }).build()); y += 22; } y = this.addPartRows(entry.getValue(), y, depth + 1); From 44ec2593834affd7f595d0d48a206b4c483f3798 Mon Sep 17 00:00:00 2001 From: HyperPigeon <58276133+HyperPigeon@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:55:41 -0400 Subject: [PATCH 4/9] - undid formatting for better readability --- .../client/gui/screen/ingame/EntityDisplayEditScreen.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/EntityDisplayEditScreen.java b/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/EntityDisplayEditScreen.java index 51e72af4..f11a8d3b 100644 --- a/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/EntityDisplayEditScreen.java +++ b/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/EntityDisplayEditScreen.java @@ -87,7 +87,11 @@ private int addPartRows(ModelPart parent, int y, int depth) { this.addRenderableWidget(new StringWidget(STARTING_X + 20, y + 5, this.font.width(label), 9, label, this.font)); PartPoseInfo partPoseInfo = this.entityDisplay.getPartPostInfo(name); - this.addRenderableWidget(Vec3FieldsWidget.builder(this.font, new Vec3(isRotation ? partPoseInfo.rotation() : partPoseInfo.offset())).setPos(STARTING_X + 70, y).setWidth(246).setHeight(18).setRotation(isRotation).setOnValueChange(vec3 -> { + this.addRenderableWidget(Vec3FieldsWidget.builder(this.font, new Vec3(isRotation ? partPoseInfo.rotation() : partPoseInfo.offset())) + .setPos(STARTING_X + 70, y). + setWidth(246).setHeight(18) + .setRotation(isRotation) + .setOnValueChange(vec3 -> { PartPoseInfo current = this.entityDisplay.getPartPostInfo(name); this.entityDisplay.setPartPoseInfo(name, isRotation ? current.updateRotation(vec3.toVector3f()) : current.updateOffset(vec3.toVector3f())); editDisplayBlock(); From 8374604abd3f67cb37a6a82e2fe8eb3dd351fbea Mon Sep 17 00:00:00 2001 From: HyperPigeon <58276133+HyperPigeon@users.noreply.github.com> Date: Wed, 14 Oct 2026 23:21:23 -0400 Subject: [PATCH 5/9] - swapped to path based keys --- .../ingame/EntityDisplayEditScreen.java | 17 +++++++++-------- .../glowcase/client/util/ModelPartUtil.java | 19 ++++++++++++++----- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/EntityDisplayEditScreen.java b/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/EntityDisplayEditScreen.java index f11a8d3b..e83ab8dc 100644 --- a/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/EntityDisplayEditScreen.java +++ b/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/EntityDisplayEditScreen.java @@ -54,7 +54,7 @@ public void init() { ModelPart root = ModelPartUtil.getModelRoot(displayEntity); if (this.expanded && root != null) { - bottom = this.addPartRows(root, bottom, 0); + bottom = this.addPartRows("root", root, bottom, 0); } } @@ -65,16 +65,17 @@ public void init() { } - private int addPartRows(ModelPart parent, int y, int depth) { + private int addPartRows(String path, ModelPart parent, int y, int depth) { int indent = Math.min(depth, 6) * 10; for (Map.Entry entry : parent.children.entrySet()) { String name = entry.getKey(); - boolean partExpanded = this.expandedParts.contains(name); + String partPath = path + "." + name; + boolean partExpanded = this.expandedParts.contains(partPath); String displayName = name.replace("_", " "); displayName = displayName.substring(0, 1).toUpperCase() + displayName.substring(1); this.addRenderableWidget(Button.builder(Component.literal((partExpanded ? "▼ " : "▶ ") + displayName), _ -> { - if (!this.expandedParts.remove(name)) this.expandedParts.add(name); + if (!this.expandedParts.remove(partPath)) this.expandedParts.add(partPath); this.rebuildWidgets(); }).bounds(STARTING_X + 10 + indent, y, 190 - indent, 20).build()); @@ -86,19 +87,19 @@ private int addPartRows(ModelPart parent, int y, int depth) { Component label = Component.translatable(isRotation ? "gui.glowcase.rotation" : "gui.glowcase.offset"); this.addRenderableWidget(new StringWidget(STARTING_X + 20, y + 5, this.font.width(label), 9, label, this.font)); - PartPoseInfo partPoseInfo = this.entityDisplay.getPartPostInfo(name); + PartPoseInfo partPoseInfo = this.entityDisplay.getPartPostInfo(partPath); this.addRenderableWidget(Vec3FieldsWidget.builder(this.font, new Vec3(isRotation ? partPoseInfo.rotation() : partPoseInfo.offset())) .setPos(STARTING_X + 70, y). setWidth(246).setHeight(18) .setRotation(isRotation) .setOnValueChange(vec3 -> { - PartPoseInfo current = this.entityDisplay.getPartPostInfo(name); - this.entityDisplay.setPartPoseInfo(name, isRotation ? current.updateRotation(vec3.toVector3f()) : current.updateOffset(vec3.toVector3f())); + PartPoseInfo current = this.entityDisplay.getPartPostInfo(partPath); + this.entityDisplay.setPartPoseInfo(partPath, isRotation ? current.updateRotation(vec3.toVector3f()) : current.updateOffset(vec3.toVector3f())); editDisplayBlock(); }).build()); y += 22; } - y = this.addPartRows(entry.getValue(), y, depth + 1); + y = this.addPartRows(partPath, entry.getValue(), y, depth + 1); } return y; diff --git a/src/main/java/dev/hephaestus/glowcase/client/util/ModelPartUtil.java b/src/main/java/dev/hephaestus/glowcase/client/util/ModelPartUtil.java index 300abd52..0401dbfe 100644 --- a/src/main/java/dev/hephaestus/glowcase/client/util/ModelPartUtil.java +++ b/src/main/java/dev/hephaestus/glowcase/client/util/ModelPartUtil.java @@ -42,19 +42,20 @@ public static void applyPartPoses(Model model, Object state) { return; } - walk(model.root(), partPoses); + walk("root", model.root(), partPoses); } - private static void walk(ModelPart parent, Map partPoses) { + private static void walk(String path, ModelPart parent, Map partPoses) { Map children = parent.children; for (Map.Entry entry : children.entrySet()) { ModelPart part = entry.getValue(); String name = entry.getKey(); + String partPath = path + "." + name; if (part == null || name == null) { continue; } - if(partPoses.containsKey(name)) { - PartPoseInfo partPoseInfo = partPoses.get(name); + if(partPoses.containsKey(partPath)) { + PartPoseInfo partPoseInfo = partPoses.get(partPath); Vector3fc offset = partPoseInfo.offset(); Vector3fc rotation = partPoseInfo.rotation(); @@ -66,7 +67,15 @@ private static void walk(ModelPart parent, Map partPoses) part.yRot += rotation.y() * Mth.DEG_TO_RAD; part.zRot += rotation.z() * Mth.DEG_TO_RAD; } - walk(part, partPoses); + walk(partPath,part, partPoses); } } + + private static String getPartName(String partPath){ + String[] parts = partPath.split("\\."); + if (parts.length > 0){ + return parts[parts.length-1]; + } + return ""; + } } From 04c53c3b166a34cfe4a8ab0952d8294685b32617 Mon Sep 17 00:00:00 2001 From: HyperPigeon <58276133+HyperPigeon@users.noreply.github.com> Date: Wed, 14 Oct 2026 23:24:21 -0400 Subject: [PATCH 6/9] - removed unused method --- .../hephaestus/glowcase/client/util/ModelPartUtil.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/main/java/dev/hephaestus/glowcase/client/util/ModelPartUtil.java b/src/main/java/dev/hephaestus/glowcase/client/util/ModelPartUtil.java index 0401dbfe..00269e52 100644 --- a/src/main/java/dev/hephaestus/glowcase/client/util/ModelPartUtil.java +++ b/src/main/java/dev/hephaestus/glowcase/client/util/ModelPartUtil.java @@ -10,8 +10,6 @@ import net.minecraft.client.renderer.entity.LivingEntityRenderer; import net.minecraft.util.Mth; import net.minecraft.world.entity.Entity; -import net.minecraft.world.entity.EntitySpawnReason; -import net.minecraft.world.entity.EntityType; import org.jetbrains.annotations.Nullable; import org.joml.Vector3fc; @@ -71,11 +69,4 @@ private static void walk(String path, ModelPart parent, Map 0){ - return parts[parts.length-1]; - } - return ""; - } } From 810cc3d9aaf1c835ee5bb15e5b83f41ee5943a00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ampflower=20=F0=9F=8C=BA?= Date: Thu, 15 Oct 2026 05:22:17 -0700 Subject: [PATCH 7/9] api: Use Direction.Axis instead of integer --- .../glowcase/util/PartPoseInfo.java | 75 +++++++------------ 1 file changed, 27 insertions(+), 48 deletions(-) diff --git a/src/main/java/dev/hephaestus/glowcase/util/PartPoseInfo.java b/src/main/java/dev/hephaestus/glowcase/util/PartPoseInfo.java index ad461ad5..f1e81bbe 100644 --- a/src/main/java/dev/hephaestus/glowcase/util/PartPoseInfo.java +++ b/src/main/java/dev/hephaestus/glowcase/util/PartPoseInfo.java @@ -3,6 +3,7 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; import io.netty.buffer.ByteBuf; +import net.minecraft.core.Direction; import net.minecraft.network.codec.ByteBufCodecs; import net.minecraft.network.codec.StreamCodec; import net.minecraft.util.ExtraCodecs; @@ -23,60 +24,38 @@ public record PartPoseInfo(String name, Vector3fc offset, Vector3fc rotation) { PartPoseInfo::new ); - public float getRotationAtAxis(int axis){ - if(axis == 0) { - return rotation().x(); - } - else if (axis == 1) { - return rotation().y(); - } - else { - return rotation().z(); - } + public float getRotationAtAxis(Direction.Axis axis) { + return switch (axis) { + case X -> rotation().x(); + case Y -> rotation().y(); + case Z -> rotation().z(); + }; } - public float getOffsetAtAxis(int axis){ - if(axis == 0) { - return offset().x(); - } - else if (axis == 1) { - return offset().y(); - } - else { - return offset().z(); - } + public float getOffsetAtAxis(Direction.Axis axis) { + return switch (axis) { + case X -> offset().x(); + case Y -> offset().y(); + case Z -> offset().z(); + }; } - public PartPoseInfo updateRotationAtAxis(int axis, float newRotation) { - Vector3f newRotationVec; - if(axis == 0) { - newRotationVec = new Vector3f(newRotation, rotation.y(), rotation.z()); - return new PartPoseInfo(name(), offset(), newRotationVec); - } - else if (axis == 1) { - newRotationVec = new Vector3f(rotation.x(), newRotation, rotation.z()); - return new PartPoseInfo(name(), offset(), newRotationVec); - } - else { - newRotationVec = new Vector3f(rotation.x(), rotation().y(), newRotation); - return new PartPoseInfo(name(), offset(), newRotationVec); - } + public PartPoseInfo updateRotationAtAxis(Direction.Axis axis, float newRotation) { + final Vector3f newRotationVec = switch (axis) { + case X -> new Vector3f(newRotation, rotation.y(), rotation.z()); + case Y -> new Vector3f(rotation.x(), newRotation, rotation.z()); + case Z -> new Vector3f(rotation.x(), rotation().y(), newRotation); + }; + return new PartPoseInfo(name(), offset(), newRotationVec); } - public PartPoseInfo updateOffsetAtAxis(int axis, float newOffset) { - Vector3f newOffsetVec; - if(axis == 0) { - newOffsetVec = new Vector3f(newOffset, offset().y(), offset().z()); - return new PartPoseInfo(name(), newOffsetVec, rotation()); - } - else if (axis == 1) { - newOffsetVec = new Vector3f(offset().x(), newOffset, offset().z()); - return new PartPoseInfo(name(), newOffsetVec, rotation()); - } - else { - newOffsetVec = new Vector3f(offset().x(), offset().y(), newOffset); - return new PartPoseInfo(name(), newOffsetVec, rotation()); - } + public PartPoseInfo updateOffsetAtAxis(Direction.Axis axis, float newOffset) { + Vector3f newOffsetVec = switch (axis) { + case X -> new Vector3f(newOffset, offset().y(), offset().z()); + case Y -> new Vector3f(offset().x(), newOffset, offset().z()); + case Z -> new Vector3f(offset().x(), offset().y(), newOffset); + }; + return new PartPoseInfo(name(), newOffsetVec, rotation()); } public PartPoseInfo updateRotation(Vector3f newRotation) { From 8b1df0f184aa0c7089825400b102576ffedefac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ampflower=20=F0=9F=8C=BA?= Date: Thu, 15 Oct 2026 05:22:48 -0700 Subject: [PATCH 8/9] api: Records are immutable, use `with` instead of `update` --- .../gui/screen/ingame/EntityDisplayEditScreen.java | 9 ++++++--- .../java/dev/hephaestus/glowcase/util/PartPoseInfo.java | 8 ++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/EntityDisplayEditScreen.java b/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/EntityDisplayEditScreen.java index e83ab8dc..48368274 100644 --- a/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/EntityDisplayEditScreen.java +++ b/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/EntityDisplayEditScreen.java @@ -26,8 +26,8 @@ public class EntityDisplayEditScreen extends DisplayBlockEditScreen { private final EntityDisplayBlockEntity entityDisplay; private final Set expandedParts = new HashSet<>(); private boolean expanded = false; - private int STARTING_X = 10; - private int STARTING_Y = 200; + private final int STARTING_X = 10; + private final int STARTING_Y = 200; private int scrollOffset = 0; private int maxScroll = 0; @@ -94,7 +94,10 @@ private int addPartRows(String path, ModelPart parent, int y, int depth) { .setRotation(isRotation) .setOnValueChange(vec3 -> { PartPoseInfo current = this.entityDisplay.getPartPostInfo(partPath); - this.entityDisplay.setPartPoseInfo(partPath, isRotation ? current.updateRotation(vec3.toVector3f()) : current.updateOffset(vec3.toVector3f())); + this.entityDisplay.setPartPoseInfo( + partPath, + isRotation ? current.withRotation(vec3.toVector3f()) : current.withOffset(vec3.toVector3f()) + ); editDisplayBlock(); }).build()); y += 22; diff --git a/src/main/java/dev/hephaestus/glowcase/util/PartPoseInfo.java b/src/main/java/dev/hephaestus/glowcase/util/PartPoseInfo.java index f1e81bbe..6a19b536 100644 --- a/src/main/java/dev/hephaestus/glowcase/util/PartPoseInfo.java +++ b/src/main/java/dev/hephaestus/glowcase/util/PartPoseInfo.java @@ -40,7 +40,7 @@ public float getOffsetAtAxis(Direction.Axis axis) { }; } - public PartPoseInfo updateRotationAtAxis(Direction.Axis axis, float newRotation) { + public PartPoseInfo withRotationAtAxis(Direction.Axis axis, float newRotation) { final Vector3f newRotationVec = switch (axis) { case X -> new Vector3f(newRotation, rotation.y(), rotation.z()); case Y -> new Vector3f(rotation.x(), newRotation, rotation.z()); @@ -49,7 +49,7 @@ public PartPoseInfo updateRotationAtAxis(Direction.Axis axis, float newRotation) return new PartPoseInfo(name(), offset(), newRotationVec); } - public PartPoseInfo updateOffsetAtAxis(Direction.Axis axis, float newOffset) { + public PartPoseInfo withOffsetAtAxis(Direction.Axis axis, float newOffset) { Vector3f newOffsetVec = switch (axis) { case X -> new Vector3f(newOffset, offset().y(), offset().z()); case Y -> new Vector3f(offset().x(), newOffset, offset().z()); @@ -58,11 +58,11 @@ public PartPoseInfo updateOffsetAtAxis(Direction.Axis axis, float newOffset) { return new PartPoseInfo(name(), newOffsetVec, rotation()); } - public PartPoseInfo updateRotation(Vector3f newRotation) { + public PartPoseInfo withRotation(Vector3f newRotation) { return new PartPoseInfo(name(), offset(), newRotation); } - public PartPoseInfo updateOffset(Vector3f newOffset) { + public PartPoseInfo withOffset(Vector3f newOffset) { return new PartPoseInfo(name(), newOffset, rotation()); } From 6d4b814d8e9453bd0cd4f91699f7aad6c0de1bd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ampflower=20=F0=9F=8C=BA?= Date: Thu, 15 Oct 2026 08:15:54 -0700 Subject: [PATCH 9/9] refactor: Move scrolling code into GlowcaseScreen for reusability. This could be frankly better, but, it's fine. Has scaffolding to handle X scrolling as well as Y. Future considerations is to add a scrollbar when it is scrollable. --- .../ingame/EntityDisplayEditScreen.java | 29 +------ .../gui/screen/ingame/GlowcaseScreen.java | 83 +++++++++++++++++++ 2 files changed, 84 insertions(+), 28 deletions(-) diff --git a/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/EntityDisplayEditScreen.java b/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/EntityDisplayEditScreen.java index 48368274..487dbb0b 100644 --- a/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/EntityDisplayEditScreen.java +++ b/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/EntityDisplayEditScreen.java @@ -6,14 +6,11 @@ import dev.hephaestus.glowcase.client.util.ModelPartUtil; import dev.hephaestus.glowcase.packet.C2SEditEntityDisplayBlock; import dev.hephaestus.glowcase.util.PartPoseInfo; -import net.minecraft.client.gui.components.AbstractWidget; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.StringWidget; -import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.model.geom.ModelPart; import net.minecraft.network.chat.Component; import net.minecraft.network.protocol.common.custom.CustomPacketPayload; -import net.minecraft.util.Mth; import net.minecraft.world.entity.Entity; import net.minecraft.world.phys.Vec3; @@ -28,9 +25,6 @@ public class EntityDisplayEditScreen extends DisplayBlockEditScreen { private boolean expanded = false; private final int STARTING_X = 10; private final int STARTING_Y = 200; - private int scrollOffset = 0; - private int maxScroll = 0; - public EntityDisplayEditScreen(DisplayBlockEntity displayBlock) { super(displayBlock); @@ -58,11 +52,7 @@ public void init() { } } - this.maxScroll = Math.max(0, bottom + 10 - this.height); - this.scrollOffset = Mth.clamp(this.scrollOffset, 0, this.maxScroll); - this.offsetWidgets(-this.scrollOffset); - - + this.setMaxScroll(0, bottom + 10 - this.height, false); } private int addPartRows(String path, ModelPart parent, int y, int depth) { @@ -108,23 +98,6 @@ private int addPartRows(String path, ModelPart parent, int y, int depth) { } - @Override - public boolean mouseScrolled(double x, double y, double scrollX, double scrollY) { - int newOffset = Mth.clamp(this.scrollOffset - (int) (scrollY * 20), 0, this.maxScroll); - if (newOffset != this.scrollOffset) { - this.offsetWidgets(this.scrollOffset - newOffset); - this.scrollOffset = newOffset; - return true; - } - return super.mouseScrolled(x, y, scrollX, scrollY); - } - - private void offsetWidgets(int scrollOffset) { - for (GuiEventListener child : this.children()) { - if (child instanceof AbstractWidget widget) widget.setY(widget.getY() + scrollOffset); - } - } - @Override public CustomPacketPayload getUpdatePayload() { return C2SEditEntityDisplayBlock.of(blockEntity); diff --git a/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/GlowcaseScreen.java b/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/GlowcaseScreen.java index 0f541f4b..4ca5c78f 100644 --- a/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/GlowcaseScreen.java +++ b/src/main/java/dev/hephaestus/glowcase/client/gui/screen/ingame/GlowcaseScreen.java @@ -1,10 +1,21 @@ package dev.hephaestus.glowcase.client.gui.screen.ingame; import net.minecraft.client.gui.GuiGraphicsExtractor; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.events.GuiEventListener; import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.chat.Component; +import net.minecraft.util.Mth; +import net.minecraft.util.Util; public abstract class GlowcaseScreen extends Screen { + // Scroll handler + private long lastScroll = Util.getMillis(); + private int scrollOffsetX = 0; + private int scrollOffsetY = 0; + private int maxScrollX = 0; + private int maxScrollY = 0; + protected GlowcaseScreen() { super(Component.empty()); } @@ -23,4 +34,76 @@ public void extractBackground(GuiGraphicsExtractor graphics, int mouseX, int mou public boolean isPauseScreen() { return false; } + + + /** + * Sets the max scroll of the UI, adjusting every element when necessary. + * + * @param maxScrollX The maximum the pane can be scrolled on X + * @param maxScrollY The maximum the pane can be scrolled on Y + * @param useDeltaOffset Whether to use the offset to adjust elements. + * If this is called from init, pass {@code false}. + */ + protected final void setMaxScroll(int maxScrollX, int maxScrollY, boolean useDeltaOffset) { + this.maxScrollX = Math.max(maxScrollX, 0); + this.maxScrollY = Math.max(maxScrollY, 0); + + final int newOffsetX = Mth.clamp(this.scrollOffsetX, 0, this.maxScrollX); + final int newOffsetY = Mth.clamp(this.scrollOffsetY, 0, this.maxScrollY); + + if (useDeltaOffset) { + this.offsetWidgets( + newOffsetX - this.scrollOffsetX, + newOffsetY - this.scrollOffsetY + ); + } else { + this.offsetWidgets( + -newOffsetX, + -newOffsetY + ); + } + + this.scrollOffsetX = newOffsetX; + this.scrollOffsetY = newOffsetY; + } + + @Override + public boolean mouseScrolled(double x, double y, double scrollX, double scrollY) { + return (!this.scrollPaneFirst() && super.mouseScrolled(x, y, scrollX, scrollY)) || scrollElements( + scrollX, + scrollY + ); + } + + private boolean scrollPaneFirst() { + return Util.getMillis() - this.lastScroll < 500L; + } + + protected boolean scrollElements(double scrollX, double scrollY) { + int newOffsetX = Mth.clamp(this.scrollOffsetX - (int) (scrollX * 20), 0, this.maxScrollX); + int newOffsetY = Mth.clamp(this.scrollOffsetY - (int) (scrollY * 20), 0, this.maxScrollY); + if (newOffsetX != this.scrollOffsetX || newOffsetY != this.scrollOffsetY) { + // TODO: consider using absolutes rather than relative? + this.offsetWidgets(this.scrollOffsetX - newOffsetX, this.scrollOffsetY - newOffsetY); + this.scrollOffsetX = newOffsetX; + this.scrollOffsetY = newOffsetY; + this.lastScroll = Util.getMillis(); + return true; + } + return false; + } + + /** + * Adjusts every element to be scrolled. By default, this adjusts every abstract widget. + */ + protected void offsetWidgets(int deltaX, int deltaY) { + for (GuiEventListener child : this.children()) { + if (child instanceof AbstractWidget widget) { + widget.setPosition( + widget.getX() + deltaX, + widget.getY() + deltaY + ); + } + } + } }