diff --git a/src/main/java/com/listmore/config/ListMoreConfigGui.java b/src/main/java/com/listmore/config/ListMoreConfigGui.java index e2af650..2847c71 100644 --- a/src/main/java/com/listmore/config/ListMoreConfigGui.java +++ b/src/main/java/com/listmore/config/ListMoreConfigGui.java @@ -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; } diff --git a/src/main/java/com/listmore/schematic/preview/SchematicPreviewLoader.java b/src/main/java/com/listmore/schematic/preview/SchematicPreviewLoader.java index 6764d85..f47f917 100644 --- a/src/main/java/com/listmore/schematic/preview/SchematicPreviewLoader.java +++ b/src/main/java/com/listmore/schematic/preview/SchematicPreviewLoader.java @@ -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> CACHE = new ConcurrentHashMap<>(); + //TODO:目前来看4已经够用了,但真的够吗? + private static final int MAX_CACHE_ENTRIES = 4; + private static final ConcurrentMap CACHE = new ConcurrentHashMap<>(); + private static final ConcurrentLinkedQueue CACHE_ORDER = new ConcurrentLinkedQueue<>(); private SchematicPreviewLoader() { } - public static CompletableFuture load(Path file) { + public static CompletableFuture load(Path file) { Path normalizedFile = file.toAbsolutePath().normalize(); - CompletableFuture 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 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 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 task) { + } + + private record CacheReference(Path file, CacheEntry entry) { } } diff --git a/src/main/java/com/listmore/schematic/preview/SchematicPreviewModel.java b/src/main/java/com/listmore/schematic/preview/SchematicPreviewModel.java index ce68a54..5e194ad 100644 --- a/src/main/java/com/listmore/schematic/preview/SchematicPreviewModel.java +++ b/src/main/java/com/listmore/schematic/preview/SchematicPreviewModel.java @@ -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; @@ -20,17 +23,15 @@ public final class SchematicPreviewModel { private final float centerZ; private final List blocks; private final Map statesByPosition; - private final Map blockEntitiesByPosition; private SchematicPreviewModel(Vec3i size, float centerX, float centerY, float centerZ, List blocks, - Map statesByPosition, Map blockEntitiesByPosition) { + Map 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; } @@ -39,10 +40,6 @@ private SchematicPreviewModel(Vec3i size, float centerX, float centerY, float ce public float centerZ() { return this.centerZ; } public List 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()) { @@ -94,7 +91,6 @@ public static SchematicPreviewModel from(LitematicaSchematic schematic) { // 从每个区域中提取非空气方块,转换为相对坐标 List blocks = new ArrayList<>(); Map statesByPosition = new HashMap<>(); - Map blockEntitiesByPosition = new HashMap<>(); for (Map.Entry entry : areas.entrySet()) { LitematicaBlockStateContainer container = schematic.getSubRegionContainer(entry.getKey()); BlockPos first = entry.getValue().getPos1(); @@ -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; @@ -125,31 +129,27 @@ public static SchematicPreviewModel from(LitematicaSchematic schematic) { } } } - // 收集方块实体数据(如箱子、告示牌等) - Map blockEntities = schematic.getBlockEntityMapForRegion(entry.getKey()); - if (blockEntities != null) { - for (Map.Entry 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) { diff --git a/src/main/java/com/listmore/schematic/preview/SchematicPreviewSession.java b/src/main/java/com/listmore/schematic/preview/SchematicPreviewSession.java index bd44ab2..de83daa 100644 --- a/src/main/java/com/listmore/schematic/preview/SchematicPreviewSession.java +++ b/src/main/java/com/listmore/schematic/preview/SchematicPreviewSession.java @@ -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; // 单个原理图浏览器的预览状态 @@ -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 loadingTask; + private CompletableFuture loadingTask; private SchematicPreviewModel model; private Throwable loadFailure; // 后台加载线程写入,GUI绘制线程读取 @@ -64,7 +64,7 @@ public void setFile(Path file) { this.transform.reset(); // 递增 generation 使旧请求的回调失效 long requestGeneration = this.generation.incrementAndGet(); - CompletableFuture task = SchematicPreviewLoader.load(normalizedFile); + CompletableFuture task = SchematicPreviewLoader.load(normalizedFile); this.loadingTask = task; task.whenComplete((loaded, throwable) -> { // 检查:未关闭 + generation 匹配 + 未被新任务替换 @@ -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()) { @@ -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; @@ -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) { } }