Skip to content
Merged

Dev #13

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
4 changes: 0 additions & 4 deletions src/main/java/com/listmore/config/ListMoreConfigGui.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,6 @@ private int createButton(int x, int y, int width, ConfigTab configTab) {

@Override
protected int getConfigWidth() {
if (tab == ConfigTab.GENERIC) {
return 200;
}

return 200;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,80 @@
package com.listmore.schematic.preview;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap;

import fi.dy.masa.litematica.schematic.LitematicaSchematic;

// 后台读取 litematic 文件
// 后台读取 litematic 文件并提取预览模型
public final class SchematicPreviewLoader {
private static final ConcurrentMap<Path, CompletableFuture<LitematicaSchematic>> CACHE = new ConcurrentHashMap<>();
//TODO:目前来看4已经够用了,但真的够吗?
private static final int MAX_CACHE_ENTRIES = 4;
private static final ConcurrentMap<Path, CacheEntry> CACHE = new ConcurrentHashMap<>();
private static final ConcurrentLinkedQueue<CacheReference> CACHE_ORDER = new ConcurrentLinkedQueue<>();

private SchematicPreviewLoader() {
}

public static CompletableFuture<LitematicaSchematic> load(Path file) {
public static CompletableFuture<LoadedPreview> load(Path file) {
Path normalizedFile = file.toAbsolutePath().normalize();
CompletableFuture<LitematicaSchematic> task = CACHE.computeIfAbsent(normalizedFile, SchematicPreviewLoader::startLoad);
task.whenComplete((schematic, throwable) -> {
if (throwable != null || schematic == null) {
CACHE.remove(normalizedFile, task);
FileStamp stamp;
try {
stamp = new FileStamp(Files.getLastModifiedTime(normalizedFile).toMillis(), Files.size(normalizedFile));
} catch (IOException exception) {
return CompletableFuture.failedFuture(exception);
}

CacheEntry entry = CACHE.compute(normalizedFile, (path, cached) -> {
if (cached != null && cached.stamp().equals(stamp)) {
return cached;
}
CacheEntry created = new CacheEntry(stamp, startLoad(path));
CACHE_ORDER.add(new CacheReference(path, created));
return created;
});
trimCache();
entry.task().whenComplete((preview, throwable) -> {
if (throwable != null || preview == null) {
CACHE.remove(normalizedFile, entry);
}
});
return entry.task();
}

private static CompletableFuture<LoadedPreview> startLoad(Path file) {
return CompletableFuture.supplyAsync(() -> {
LitematicaSchematic schematic = LitematicaSchematic.createFromFile(file.getParent(), file.getFileName().toString());
if (schematic == null) {
throw new IllegalStateException("Litematica returned no schematic");
}
return new LoadedPreview(schematic, SchematicPreviewModel.from(schematic));
});
return task;
}

private static CompletableFuture<LitematicaSchematic> startLoad(Path file) {
return CompletableFuture.supplyAsync(() ->
LitematicaSchematic.createFromFile(file.getParent(), file.getFileName().toString()));
private static void trimCache() {
while (CACHE.size() > MAX_CACHE_ENTRIES) {
CacheReference oldest = CACHE_ORDER.poll();
if (oldest == null) {
return;
}
CACHE.remove(oldest.file(), oldest.entry());
}
}

public record LoadedPreview(LitematicaSchematic schematic, SchematicPreviewModel model) {
}

private record FileStamp(long modifiedTime, long size) {
}

private record CacheEntry(FileStamp stamp, CompletableFuture<LoadedPreview> task) {
}

private record CacheReference(Path file, CacheEntry entry) {
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.listmore.schematic.preview;

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

import fi.dy.masa.litematica.schematic.LitematicaSchematic;
import fi.dy.masa.litematica.schematic.container.ILitematicaBlockStatePalette;
import fi.dy.masa.litematica.schematic.container.LitematicaBitArray;
import fi.dy.masa.litematica.schematic.container.LitematicaBlockStateContainer;
import fi.dy.masa.litematica.selection.Box;
import net.minecraft.core.BlockPos;
Expand All @@ -20,17 +23,15 @@ public final class SchematicPreviewModel {
private final float centerZ;
private final List<Block> blocks;
private final Map<Long, BlockState> statesByPosition;
private final Map<Long, Object> blockEntitiesByPosition;

private SchematicPreviewModel(Vec3i size, float centerX, float centerY, float centerZ, List<Block> blocks,
Map<Long, BlockState> statesByPosition, Map<Long, Object> blockEntitiesByPosition) {
Map<Long, BlockState> statesByPosition) {
this.size = size;
this.centerX = centerX;
this.centerY = centerY;
this.centerZ = centerZ;
this.blocks = List.copyOf(blocks);
this.statesByPosition = Map.copyOf(statesByPosition);
this.blockEntitiesByPosition = Map.copyOf(blockEntitiesByPosition);
this.blocks = Collections.unmodifiableList(blocks);
this.statesByPosition = Collections.unmodifiableMap(statesByPosition);
}

public Vec3i size() { return this.size; }
Expand All @@ -39,10 +40,6 @@ private SchematicPreviewModel(Vec3i size, float centerX, float centerY, float ce
public float centerZ() { return this.centerZ; }
public List<Block> blocks() { return this.blocks; }

public Object blockEntityDataAt(int x, int y, int z) {
return this.blockEntitiesByPosition.get(packPosition(x, y, z));
}

// 查询预览坐标中的方块状态
public BlockState blockStateAt(int x, int y, int z) {
if (x < 0 || y < 0 || z < 0 || x >= this.size.getX() || y >= this.size.getY() || z >= this.size.getZ()) {
Expand Down Expand Up @@ -94,7 +91,6 @@ public static SchematicPreviewModel from(LitematicaSchematic schematic) {
// 从每个区域中提取非空气方块,转换为相对坐标
List<Block> blocks = new ArrayList<>();
Map<Long, BlockState> statesByPosition = new HashMap<>();
Map<Long, Object> blockEntitiesByPosition = new HashMap<>();
for (Map.Entry<String, Box> entry : areas.entrySet()) {
LitematicaBlockStateContainer container = schematic.getSubRegionContainer(entry.getKey());
BlockPos first = entry.getValue().getPos1();
Expand All @@ -107,11 +103,19 @@ public static SchematicPreviewModel from(LitematicaSchematic schematic) {
int originY = Math.min(first.getY(), second.getY());
int originZ = Math.min(first.getZ(), second.getZ());
// 遍历容器内所有方块,只保留非空气方块
LitematicaBitArray storage = container.getArray();
ILitematicaBlockStatePalette palette = container.getPalette();
boolean[] airIds = findAirIds(palette);
long storageIndex = 0L;
for (int y = 0; y < regionSize.getY(); y++) {
for (int z = 0; z < regionSize.getZ(); z++) {
for (int x = 0; x < regionSize.getX(); x++) {
BlockState state = container.get(x, y, z);
if (!state.isAir()) {
int paletteId = storage.getAt(storageIndex++);
if (paletteId < 0 || paletteId >= airIds.length || !airIds[paletteId]) {
BlockState state = palette.getBlockState(paletteId);
if (state == null || state.isAir()) {
continue;
}
// 绝对坐标 -> 相对坐标:减去全局包围盒原点
int relativeX = x + originX - minX;
int relativeY = y + originY - minY;
Expand All @@ -125,31 +129,27 @@ public static SchematicPreviewModel from(LitematicaSchematic schematic) {
}
}
}
// 收集方块实体数据(如箱子、告示牌等)
Map<BlockPos, ?> blockEntities = schematic.getBlockEntityMapForRegion(entry.getKey());
if (blockEntities != null) {
for (Map.Entry<BlockPos, ?> blockEntity : blockEntities.entrySet()) {
if (blockEntity.getValue() == null) {
continue;
}
BlockPos position = blockEntity.getKey();
int relativeX = position.getX() + originX - minX;
int relativeY = position.getY() + originY - minY;
int relativeZ = position.getZ() + originZ - minZ;
blockEntitiesByPosition.putIfAbsent(packPosition(relativeX, relativeY, relativeZ), blockEntity.getValue());
}
}
}

// 包围盒尺寸 = 远端 - 原点,中心 = 尺寸 * 0.5(几何中心)
Vec3i size = new Vec3i(maxXExclusive - minX, maxYExclusive - minY, maxZExclusive - minZ);
return new SchematicPreviewModel(size, size.getX() * 0.5F, size.getY() * 0.5F, size.getZ() * 0.5F,
blocks, statesByPosition, blockEntitiesByPosition);
blocks, statesByPosition);
}

private static SchematicPreviewModel empty() {
return new SchematicPreviewModel(BlockPos.ZERO, 0.0F, 0.0F, 0.0F, List.of(), Map.of(), Map.of());
return new SchematicPreviewModel(BlockPos.ZERO, 0.0F, 0.0F, 0.0F, List.of(), Map.of());
}

private static boolean[] findAirIds(ILitematicaBlockStatePalette palette) {
boolean[] airIds = new boolean[palette.getPaletteSize()];
for (int id = 0; id < airIds.length; id++) {
BlockState state = palette.getBlockState(id);
airIds[id] = state == null || state.isAir();
}
return airIds;
}
//突然想到一个很神的点子,如果用c/c++或者rust去写计算部分呢?真神人了

// 将坐标打包为 long
private static long packPosition(int x, int y, int z) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicLong;

import com.listmore.schematic.preview.SchematicPreviewLoader.LoadedPreview;
import com.listmore.schematic.preview.render.SchematicPreviewRenderManager;
import fi.dy.masa.litematica.schematic.LitematicaSchematic;
import net.minecraft.core.Vec3i;

// 单个原理图浏览器的预览状态
Expand All @@ -14,7 +14,7 @@ public final class SchematicPreviewSession {
private final SchematicPreviewTransform transform = new SchematicPreviewTransform();
private final SchematicPreviewRenderManager renderer = new SchematicPreviewRenderManager();
private Path file;
private CompletableFuture<LitematicaSchematic> loadingTask;
private CompletableFuture<LoadedPreview> loadingTask;
private SchematicPreviewModel model;
private Throwable loadFailure;
// 后台加载线程写入,GUI绘制线程读取
Expand Down Expand Up @@ -64,7 +64,7 @@ public void setFile(Path file) {
this.transform.reset();
// 递增 generation 使旧请求的回调失效
long requestGeneration = this.generation.incrementAndGet();
CompletableFuture<LitematicaSchematic> task = SchematicPreviewLoader.load(normalizedFile);
CompletableFuture<LoadedPreview> task = SchematicPreviewLoader.load(normalizedFile);
this.loadingTask = task;
task.whenComplete((loaded, throwable) -> {
// 检查:未关闭 + generation 匹配 + 未被新任务替换
Expand All @@ -77,7 +77,7 @@ public void setFile(Path file) {

// 在 GUI 绘制线程调用,消费后台加载线程写入的 pendingResult
// 检查 generation 确保只处理最新请求的结果,忽略已过期的加载任务
// 加载成功后从 LitematicaSchematic 提取 SchematicPreviewModel 并提交给渲染器
// 加载成功后将后台生成的模型提交给渲染器
public void update() {
LoadResult result = this.pendingResult;
if (result == null || result.generation() != this.generation.get()) {
Expand All @@ -89,15 +89,15 @@ public void update() {
this.loadFailure = result.throwable();
return;
}
if (result.schematic() == null) {
this.loadFailure = new IllegalStateException("Litematica returned no schematic");
if (result.preview() == null) {
this.loadFailure = new IllegalStateException("Litematica returned no preview");
return;
}

try {
// 加载成功 -> 提取模型并提交给渲染器
this.model = SchematicPreviewModel.from(result.schematic());
this.renderer.setSchematic(result.schematic());
// 加载成功 -> 提交模型给渲染器
this.model = result.preview().model();
this.renderer.setSchematic(result.preview().schematic());
this.renderer.setModel(this.model);
} catch (Throwable throwable) {
this.loadFailure = throwable;
Expand All @@ -113,6 +113,6 @@ public void close() {
this.renderer.close();
}

private record LoadResult(long generation, LitematicaSchematic schematic, Throwable throwable) {
private record LoadResult(long generation, LoadedPreview preview, Throwable throwable) {
}
}