Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/en/04-extension-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,8 @@ This is a danger box (red).

### Row Component — `<row>` / `::: row`

Horizontal rows wrap whole components to the available width, keeping recipe proportions and applying alignment per line. Height and hit areas follow the wrapped layout, so guide zoom does not hide recipes on the right.

Renders child components side by side in a horizontal layout.

**Smart Width Allocation:**
Expand Down Expand Up @@ -462,11 +464,17 @@ Blocks without a corresponding item (technical blocks) will not render.
> **Note:** Unlike `<item>`, `<block>` looks up the **block** registry and automatically finds the
> matching block item. You do not need to know the block item's ID separately.

### Key Binding — `<key>`

`<key id="key.anvilcraft.switch_phase"/>` shows the currently bound key in paragraphs and lists. Rebinding updates the displayed name without editing the document.

### Entity Display — `<entity>`

Renders an entity preview with `InventoryScreen.renderEntityInInventoryFollowsMouse`.
The entity rotates to follow the cursor.

The entity follows its row and guide transforms and is clipped to its preview frame. Its size is measured before the first layout to keep following content in place.

```markdown
<entity id="minecraft:zombie"/>
<entity id="minecraft:villager"/>
Expand Down
3 changes: 2 additions & 1 deletion docs/en/10-structure-preview-rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,5 @@ Left-click a block in a structure or machine preview to show its localized name,
- Left-click to inspect the block name, registry ID, structure coordinates and block state. The hover hint is simply “Click to inspect”.
- Right-click a block with a menu to open its native read-only GUI. Its saved contents, mode, timing and progress are shown by the native menu. Player inventory and hotbar slots stay empty. No menu input or close/configuration packets are sent.
- Hold the left button to drag the popup; right-click it to close. A white connector ends at the popup edge and points to the block center. Backgrounds, text and items fade together; `reduced_motion` reduces scaling.
- Drag the structure with the left/right button to pan/rotate. Ctrl + scroll to zoom. Layer buttons and PageUp / PageDown change visible layers.
- Drag the structure with the left/right button to pan/rotate. Scroll to zoom. Layer buttons and PageUp / PageDown change visible layers.
- A wheel gesture stays with the region where it starts: outside a structure it scrolls the page, inside it zooms that same structure. Crossing regions or moving a structure under the pointer does not change the target. At least 0.5 seconds without wheel input releases it; reversing direction does not.
8 changes: 8 additions & 0 deletions docs/zh/04-extension-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,8 @@ Ageratum 自带若干内置扩展组件,可在任意文档中直接使用。

### 行组件 — `<row>` / `::: row`

横向排列会按当前可用宽度自动换行,整项配方移到下一行后仍保留自身比例;每行独立应用水平和垂直对齐。行高与交互命中随换行同步更新,手册缩放不会丢失右侧配方。

将内部的子组件以水平方式(并排)渲染。

**智能宽度分配:**
Expand Down Expand Up @@ -455,10 +457,16 @@ Ageratum 自带若干内置扩展组件,可在任意文档中直接使用。
> **注意:** 与 `<item>` 不同,`<block>` 查询的是**方块**注册表,自动匹配对应的方块物品。
> 你不需要知道方块物品的独立 ID。

### 按键名称 — `<key>`

`<key id="key.anvilcraft.switch_phase"/>` 显示当前客户端绑定的按键名称,可用于段落和列表;修改按键绑定后会同步更新,无需修改手册文本。

### 实体展示 — `<entity>`

使用 `InventoryScreen.renderEntityInInventoryFollowsMouse` 渲染一个实体预览,实体会随鼠标移动而转向。

实体会按所在行和手册缩放定位,并裁剪在预览框内;首次布局前即测量实体尺寸,避免打开页面时模型和后续段落错位。

```markdown
<entity id="minecraft:zombie"/>
<entity id="minecraft:villager"/>
Expand Down
3 changes: 2 additions & 1 deletion docs/zh/10-structure-preview-rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,5 @@
- 左键单击查看方块名称、注册 ID、结构内坐标和方块状态。悬停提示简写为“点击查看”。
- 右键单击有菜单的方块打开原生只读 GUI 浮窗,保存的库存、模式、延时和进度由对应菜单显示,玩家物品栏与快捷栏固定为空。浮窗不接收菜单操作,也不会发送关闭或修改配置的数据包。
- 按住左键拖动浮窗,右键关闭。浮窗以白线连接到方块中心,线段在窗口边缘结束;背景、文字和物品同步淡入淡出。配置 `reduced_motion` 可减少缩放动画。
- 左键拖动结构进行平移,右键拖动旋转;Ctrl+滚轮缩放。层数按钮与 PageUp / PageDown 调整显示层数。
- 左键拖动结构进行平移,右键拖动旋转;滚轮缩放。层数按钮与 PageUp / PageDown 调整显示层数。
- 连续滚轮操作按起始位置锁定:从结构外开始时持续滚动页面,从结构内开始时持续缩放同一个结构;即使鼠标跨区域或滚动使结构移到鼠标下,也不会中途切换。停滚至少 0.5 秒后重新判断,反向滚动不会结束当前操作。
1 change: 1 addition & 0 deletions gradle/scripts/structure-test.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ neoForge {
runs {
structureTest {
client()
systemProperty 'ageratum.scrollEntityTest', providers.gradleProperty('scrollEntityTest').isPresent().toString()
systemProperty 'ageratum.structureGuiReview', providers.gradleProperty('structureGuiReview').isPresent().toString()
systemProperty 'ageratum.structureGuiTest', providers.gradleProperty('structureGuiTest').isPresent().toString()
systemProperty 'ageratum.blockDetailsTest', providers.gradleProperty('blockDetailsTest').isPresent().toString()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package dev.anvilcraft.resource.ageratum.test;

import com.mojang.blaze3d.platform.InputConstants;
import dev.anvilcraft.resource.ageratum.client.feat.markdown.MDRenderContext;
import dev.anvilcraft.resource.ageratum.client.feat.markdown.component.MDComponent;
import dev.anvilcraft.resource.ageratum.client.feat.markdown.component.MDListComponent;
import dev.anvilcraft.resource.ageratum.client.feat.markdown.component.extend.MDRowComponent;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.resources.ResourceKey;

import java.util.ArrayList;
import java.util.List;

/** 检查列表首行对齐、实时按键绑定,以及实际配方的宽窄排版。 */
final class ListRecipeClientScreen extends Screen {
private final ListProbe list;
private final MDRowComponent recipes;

ListRecipeClientScreen() {
super(Component.literal("Lists and wrapped recipes"));
Minecraft mc = Minecraft.getInstance();
var keyList = new MDListComponent(List.of(MDListComponent.unordered(0, "按住 <key id=\"key.sneak\"/> 操作")));
var key = mc.options.keyShift;
var original = key.getKey();
try {
key.setKey(InputConstants.UNKNOWN);
ScrollEntityClientTest.check(keyList.getText().getString().equals("按住 " + key.getTranslatedKeyMessage().getString() + " 操作"),
"inline key resolves inside lists and follows rebinding");
} finally {
key.setKey(original);
}
this.list = new ListProbe(List.of(
MDListComponent.unordered(0, "圆点与首行对齐,长文本自动换行后仍然保持统一的正文左边缘,不再显示灰色背景。"),
MDListComponent.unordered(1, "嵌套列表保留层级缩进。"),
MDListComponent.ordered(0, 9, "编号在标记列内右对齐。"),
MDListComponent.ordered(0, 10, "两位和三位编号都不会碰到正文,也不会挤压数字。"),
MDListComponent.ordered(0, 100, "更长的编号仍保持同一文字起点。"),
MDListComponent.task(0, false, "未完成任务"),
MDListComponent.task(0, true, "已完成任务"),
MDListComponent.unordered(0, "按住 <key id=\"key.sneak\"/> 操作")));
var simple = new ListProbe(List.of(MDListComponent.unordered(0, "parent"), MDListComponent.unordered(1, "child")));
ScrollEntityClientTest.check(this.list.textX(1) - this.list.textX(0) >= simple.textX(1) - simple.textX(0),
"wide parent numbering preserves nested indentation");
ScrollEntityClientTest.check(this.list.textX(2) == this.list.textX(3) && this.list.textX(3) == this.list.textX(4),
"ordered entries share their text origin");
List<MDComponent> items = new ArrayList<>();
for (String name : List.of("crafting_table", "chest", "furnace", "hopper")) {
ResourceLocation id = ResourceLocation.parse("minecraft:" + name);
var recipe = mc.getSingleplayerServer().getRecipeManager().byKey(id).orElseThrow();
var component = new dev.anvilcraft.resource.ageratum.client.feat.markdown.component.extend.recipe.MDCraftingTableRecipeComponent(
(net.minecraft.world.item.crafting.CraftingRecipe) recipe.value(), false);
items.add(component);
}
this.recipes = new MDRowComponent(items, MDRowComponent.Direction.HORIZONTAL,
MDRowComponent.HorizontalAlign.LEFT, MDRowComponent.VerticalAlign.TOP);
ScrollEntityClientTest.check(this.recipes.getHeight(mc, 150, Integer.MAX_VALUE)
> this.recipes.getHeight(mc, 440, Integer.MAX_VALUE), "recipes gain lines in a narrower viewport");
}

private static final class ListProbe extends MDListComponent {
private ListProbe(List<ListItem> items) { super(items); }
private int textX(int index) { return this.getTextX(this.cachedItems.get(index)); }
}

@Override
public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
graphics.fill(0, 0, this.width, this.height, 0xFFF6EDD5);
this.draw(graphics, this.list, 20, 20, 190, 1.5f);
this.draw(graphics, this.recipes, 340, 20, 440, 1);
this.draw(graphics, this.recipes, 340, 180, 150, 1);
}

private void draw(GuiGraphics graphics, MDComponent component, int x, int y, int width, float scale) {
graphics.pose().pushPose();
graphics.pose().translate(x, y, 0);
graphics.pose().scale(scale, scale, 1);
component.render(new MDRenderContext(null, Minecraft.getInstance(), graphics, new ArrayList<>(),
this.width, this.height, width, 1000, -10000, -10000, Math.round(x / scale), Math.round(y / scale), 1 / scale,
0, 0, new ArrayList<>()));
graphics.pose().popPose();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package dev.anvilcraft.resource.ageratum.test;

import dev.anvilcraft.resource.ageratum.client.feat.markdown.MDDocument;
import dev.anvilcraft.resource.ageratum.client.feat.markdown.MDRenderContext;
import dev.anvilcraft.resource.ageratum.client.feat.markdown.component.MDComponent;
import dev.anvilcraft.resource.ageratum.client.feat.markdown.component.extend.MDEntityComponent;
import dev.anvilcraft.resource.ageratum.client.gui.GuideScreen;
import net.minecraft.client.Minecraft;
import net.minecraft.client.Screenshot;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/** 在真实手册上验证滚轮目标,并截取不同 GUI 变换下的实体预览。 */
public final class ScrollEntityClientTest {
private static int stage;
private static long due;
private static ScrollScreen guide;

static boolean tick(Minecraft mc) {
if (System.nanoTime() < due) return false;
switch (stage++) {
case 0 -> {
dev.anvilcraft.resource.ageratum.client.AgeratumClient.CONFIG.scale = 1;
mc.options.guiScale().set(1);
mc.resizeDisplay();
guide = new ScrollScreen();
mc.setScreen(guide);
}
case 1 -> guide.pageBurst();
case 2 -> guide.structureBurst();
case 3 -> {
guide.pageAfterPause();
mc.setScreen(new EntityScreen());
}
case 4 -> {
Screenshot.grab(mc.gameDirectory, "entity-scroll-regression.png", mc.getMainRenderTarget(), message -> {});
mc.setScreen(new ListRecipeClientScreen());
}
default -> {
Screenshot.grab(mc.gameDirectory, "list-recipe-regression.png", mc.getMainRenderTarget(), message -> {});
return true;
}
}
due = System.nanoTime() + 650_000_000;
return false;
}

static void check(boolean value, String message) {
if (!value) throw new AssertionError(message);
}

private static final class ScrollScreen extends GuideScreen {
private final ScrollBlock structure;
private ScrollScreen() { this(new ScrollBlock()); }
private ScrollScreen(ScrollBlock structure) {
super(ResourceLocation.parse("ageratum:scroll-test"), new MDDocument(null, Map.of(),
List.of(new Space(40), structure, new Space(800))), List.of(), false);
this.structure = structure;
}
private boolean wheel(double localY, double delta) {
return this.wheelAt(this.getContentWidth() / 2.0, localY, delta);
}
private boolean wheelAt(double localX, double localY, double delta) {
return this.mouseScrolled((this.leftPos + this.getContentStartX() + localX) / this.scale,
(this.topPos + this.getContentStartY() + localY) / this.scale, 0, delta);
}
private void pageBurst() {
this.contentScroll = 0;
check(this.wheelAt(-1, 5, -1), "content left edge wins over overlapping sidebar labels");
float first = this.contentScroll;
check(first > 0, "page moved; maximum=" + this.maxContentScroll + ", components=" + this.parsedComponents.size());
check(this.wheel(70 - first, -1), "wheel over structure remains captured by page");
check(this.contentScroll > first && this.structure.delta == 0, "crossing structure does not interrupt page");
}
private void structureBurst() {
this.contentScroll = 0;
check(this.wheel(70, -1), "structure accepts first wheel after pause");
check(this.structure.delta == -1, "structure zooms");
check(this.mouseScrolled(0, 0, 0, 1), "structure retains wheel outside page");
check(this.structure.delta == 0 && this.contentScroll == 0, "reversal stays with structure");
}
private void pageAfterPause() {
this.contentScroll = 70;
check(this.wheel(-1, -1), "paper top margin starts page gesture after idle");
check(this.contentScroll > 70 && this.structure.delta == 0, "clipped structure cannot capture paper margin");
}
}

private static class Space extends MDComponent {
private final int height;
private Space(int height) { super(""); this.height = height; }
@Override public int getHeight(Minecraft minecraft, int maxX, int maxY) { return this.height; }
}

private static final class ScrollBlock extends Space {
private double delta;
private ScrollBlock() { super(90); }
@Override public boolean mouseScrolled(Minecraft minecraft, double x, double y, double delta, int maxX) {
this.delta += delta;
return true;
}
}

private static final class EntityScreen extends Screen {
private final MDEntityComponent villager = new MDEntityComponent(ResourceLocation.parse("minecraft:villager"), new CompoundTag(), false);
private final MDEntityComponent trader = new MDEntityComponent(ResourceLocation.parse("minecraft:wandering_trader"), new CompoundTag(), false);
private EntityScreen() { super(Component.literal("Entity preview regression")); }
@Override public void render(GuiGraphics graphics, int mouseX, int mouseY, float partialTick) {
graphics.fill(0, 0, this.width, this.height, 0xFFF6EDD5);
this.preview(graphics, this.villager, 40, 35, 1);
this.preview(graphics, this.trader, 240, 35, 1.5f);
this.preview(graphics, this.villager, 440, 35, 0.75f);
}
private void preview(GuiGraphics graphics, MDEntityComponent entity, int x, int y, float scale) {
int height = entity.getHeight(Minecraft.getInstance(), 128, 200);
check(height >= 72, "entity size is available before its first render");
graphics.pose().pushPose();
graphics.pose().translate(x, y, 0);
graphics.pose().scale(scale, scale, 1);
entity.render(new MDRenderContext(null, Minecraft.getInstance(), graphics, new ArrayList<>(),
this.width, this.height, 128, 200, 64, 36, Math.round(x / scale), Math.round(y / scale), 1 / scale,
0, 0, new ArrayList<>()));
graphics.pose().popPose();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ public final class StructureTest {
public StructureTest(IEventBus bus) {
bus.addListener((ModelEvent.BakingCompleted event) -> this.ready = true);
NeoForge.EVENT_BUS.addListener(this::tick);
NeoForge.EVENT_BUS.addListener(this::render);
if (Boolean.getBoolean("ageratum.scrollEntityTest")) {
NeoForge.EVENT_BUS.addListener((net.neoforged.neoforge.client.event.RenderFrameEvent.Post event) -> this.scrollFrame());
} else {
NeoForge.EVENT_BUS.addListener(this::render);
}
}

private void tick(ClientTickEvent.Post event) {
Expand All @@ -77,6 +81,20 @@ private void tick(ClientTickEvent.Post event) {
.value().createWorldDimensions(), mc.screen);
}

private void scrollFrame() {
Minecraft mc = Minecraft.getInstance();
if (this.stage == 5 || mc.level == null || mc.player == null || mc.getOverlay() != null) return;
try {
if (ScrollEntityClientTest.tick(mc)) {
LogUtils.getLogger().info("AGERATUM_STRUCTURE_TEST PASS: scroll ownership, idle release and entity viewport");
this.stage = 5;
mc.stop();
}
} catch (Throwable exception) {
this.fail(exception);
}
}

private void render(RenderLevelStageEvent event) {
Minecraft mc = Minecraft.getInstance();
if (event.getStage() != RenderLevelStageEvent.Stage.AFTER_TRANSLUCENT_BLOCKS || mc.level == null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,12 @@ public boolean mouseScrolled(Minecraft minecraft, double mouseX, double mouseY,
return false;
}

/** 固定滚动起点的组件及局部坐标,供连续滚轮操作复用。 */
public java.util.function.DoublePredicate scrollHandler(Minecraft minecraft, double mouseX, double mouseY, int maxX) {
return delta -> this.mouseScrolled(minecraft, mouseX, mouseY, delta, maxX);
}


/**
* 处理组件内部鼠标按下事件。
*
Expand Down
Loading
Loading