From 5c66838b3e8d07faf461ab003aaf769213ef78de Mon Sep 17 00:00:00 2001 From: tzdwindows 7 <3076584115@qq.com> Date: Sat, 1 Nov 2025 18:33:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(render):=20=E5=AE=9E=E7=8E=B0=E5=9B=BE?= =?UTF-8?q?=E5=B1=82=E7=AE=A1=E7=90=86=E5=92=8C=E6=B8=B2=E6=9F=93=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E5=8A=9F=E8=83=BD-=20=E6=96=B0=E5=A2=9E=20LayerCellRe?= =?UTF-8?q?nderer=20=E7=B1=BB=EF=BC=8C=E7=94=A8=E4=BA=8E=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=9B=BE=E5=B1=82=E5=88=97=E8=A1=A8=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=8F=AF=E8=A7=81=E6=80=A7=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E5=92=8C=E7=BC=A9=E7=95=A5=E5=9B=BE=E6=98=BE=E7=A4=BA-=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20LayerOperationManager=20=E7=B1=BB=EF=BC=8C?= =?UTF-8?q?=E6=8F=90=E4=BE=9B=E5=9B=BE=E5=B1=82=E7=9A=84=E5=A2=9E=E5=88=A0?= =?UTF-8?q?=E6=94=B9=E6=9F=A5=E5=92=8C=E8=A7=86=E8=A7=89=E9=A1=BA=E5=BA=8F?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=8A=9F=E8=83=BD=20-=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=20LayerReorderTransferHandler=20=E7=B1=BB=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E9=80=9A=E8=BF=87=E6=8B=96=E6=8B=BD=E6=96=B9=E5=BC=8F?= =?UTF-8?q?=E9=87=8D=E6=96=B0=E6=8E=92=E5=88=97=E5=9B=BE=E5=B1=82=E9=A1=BA?= =?UTF-8?q?=E5=BA=8F-=20=E4=BC=98=E5=8C=96=20Mesh2D=20=E7=B1=BB=EF=BC=8C?= =?UTF-8?q?=E5=BC=95=E5=85=A5=20renderVertices=20=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E7=BC=93=E5=AD=98=E6=9C=BA=E5=88=B6=EF=BC=8C=E6=8F=90=E5=8D=87?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E6=80=A7=E8=83=BD=20-=20=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E4=BA=8C=E7=BA=A7=E9=A1=B6=E7=82=B9=E7=B3=BB=E7=BB=9F=EF=BC=8C?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E7=BD=91=E6=A0=BC=E5=8F=98=E5=BD=A2=E7=AE=97?= =?UTF-8?q?=E6=B3=95=EF=BC=8C=E4=BF=AE=E5=A4=8D=E9=A1=B6=E7=82=B9=E7=A7=BB?= =?UTF-8?q?=E5=8A=A8=E5=92=8C=E5=B9=B3=E7=A7=BB=E7=9B=B8=E5=85=B3=E9=97=AE?= =?UTF-8?q?=E9=A2=98=20-=20=E6=94=B9=E8=BF=9B=E4=B8=89=E8=A7=92=E5=88=86?= =?UTF-8?q?=E9=85=8D=E5=8F=98=E5=BD=A2=E7=AE=97=E6=B3=95=EF=BC=8C=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=20pinned=20=E6=8E=A7=E5=88=B6=E7=82=B9=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=92=8C=E6=95=B4=E4=BD=93=E4=BD=8D=E7=A7=BB=E6=A0=A1?= =?UTF-8?q?=E6=AD=A3=20-=20=E6=9B=B4=E6=96=B0=20GLContextManager=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E9=98=9F=E5=88=97=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E5=A2=9E=E5=8A=A0=E8=B6=85=E6=97=B6=E5=92=8C=E4=B8=AD?= =?UTF-8?q?=E6=96=AD=E5=A4=84=E7=90=86=E6=9C=BA=E5=88=B6-=20=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=E6=A8=A1=E5=9E=8B=E5=8C=85=E8=A3=85=E5=99=A8=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E6=B3=A8=E9=87=8A=E6=A0=BC=E5=BC=8F=EF=BC=8C=E6=8F=90?= =?UTF-8?q?=E9=AB=98=E4=BB=A3=E7=A0=81=E5=8F=AF=E8=AF=BB=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Anime2VividModelWrapper.java | 2 +- .../vivid2D/render/ModelRender.java | 304 +++ .../vivid2D/render/awt/ModelAIPanel.java | 4 + .../vivid2D/render/awt/ModelLayerPanel.java | 2227 +++++------------ .../render/awt/manager/GLContextManager.java | 11 +- .../awt/manager/LayerOperationManager.java | 70 + .../render/awt/manager/ThumbnailManager.java | 244 ++ .../render/awt/tools/SelectionTool.java | 86 +- .../awt/tools/VertexDeformationTool.java | 54 +- .../render/awt/util/MeshTextureUtil.java | 267 ++ .../vivid2D/render/awt/util/PSDImporter.java | 187 ++ .../awt/util/renderer/LayerCellRenderer.java | 120 + .../renderer/LayerReorderTransferHandler.java | 68 + .../vivid2D/render/model/Model2D.java | 6 +- .../vivid2D/render/model/ModelPart.java | 176 +- .../vivid2D/render/model/util/Mesh2D.java | 754 ++++-- .../render/model/util/SecondaryVertex.java | 35 +- .../com/chuangzhou/vivid2D/test/AI3Test.java | 2 +- .../vivid2D/test/ModelLayerPanelTest.java | 8 + 19 files changed, 2811 insertions(+), 1814 deletions(-) create mode 100644 src/main/java/com/chuangzhou/vivid2D/render/awt/ModelAIPanel.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/render/awt/manager/LayerOperationManager.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ThumbnailManager.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/render/awt/util/MeshTextureUtil.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/render/awt/util/PSDImporter.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/render/awt/util/renderer/LayerCellRenderer.java create mode 100644 src/main/java/com/chuangzhou/vivid2D/render/awt/util/renderer/LayerReorderTransferHandler.java diff --git a/src/main/java/com/chuangzhou/vivid2D/ai/anime_segmentation/Anime2VividModelWrapper.java b/src/main/java/com/chuangzhou/vivid2D/ai/anime_segmentation/Anime2VividModelWrapper.java index 786f6e2..b76a9be 100644 --- a/src/main/java/com/chuangzhou/vivid2D/ai/anime_segmentation/Anime2VividModelWrapper.java +++ b/src/main/java/com/chuangzhou/vivid2D/ai/anime_segmentation/Anime2VividModelWrapper.java @@ -14,7 +14,7 @@ import java.util.List; /** * Anime2VividModelWrapper - 对之前 Anime2Segmenter 的封装,提供更便捷的API - * + *

* 用法示例: * Anime2VividModelWrapper wrapper = Anime2VividModelWrapper.load(Paths.get("/path/to/modelDir")); * Map out = wrapper.segmentAndSave( diff --git a/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java b/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java index 999040b..61654bd 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java @@ -14,6 +14,7 @@ import com.chuangzhou.vivid2D.render.systems.sources.ShaderManagement; import com.chuangzhou.vivid2D.render.systems.sources.ShaderProgram; import org.joml.Matrix3f; import org.joml.Vector2f; +import org.joml.Vector3f; import org.joml.Vector4f; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL15; @@ -674,6 +675,309 @@ public final class ModelRender { RenderSystem.checkGLError("render_end"); } + // ================== 缩略图渲染方法 ================== + + /** + * 渲染模型缩略图(图层式渲染,不受摄像机控制) + * + *

该方法提供类似PS图层预览的缩略图渲染功能:

+ * + * + * @param model 要渲染的模型 + * @param x 缩略图左上角X坐标(屏幕坐标) + * @param y 缩略图左上角Y坐标(屏幕坐标) + * @param width 缩略图宽度 + * @param height 缩略图高度 + */ + public static void renderThumbnail(Model2D model, float x, float y, float width, float height) { + if (!initialized) throw new IllegalStateException("ModelRender not initialized"); + if (model == null) return; + + RenderSystem.assertOnRenderThread(); + RenderSystem.checkGLError("renderThumbnail_start"); + + // 保存原始状态以便恢复 + boolean originalRenderColliders = renderColliders; + boolean originalRenderLightPositions = renderLightPositions; + int originalViewportWidth = viewportWidth; + int originalViewportHeight = viewportHeight; + + try { + // 设置缩略图专用状态 + renderColliders = false; + renderLightPositions = false; + + // 设置缩略图视口(屏幕坐标) + RenderSystem.viewport((int)x, (int)y, (int)width, (int)height); + + // 清除缩略图区域 + RenderSystem.clear(GL11.GL_COLOR_BUFFER_BIT | (enableDepthTest ? GL11.GL_DEPTH_BUFFER_BIT : 0)); + RenderSystem.checkGLError("thumbnail_after_clear"); + + // 简化版的模型更新(跳过物理系统) + model.update(0.016f); // 使用固定时间步长 + + // 计算模型边界和缩放比例 + ThumbnailBounds bounds = calculateThumbnailBounds(model, width, height); + + // 设置缩略图专用的正交投影(固定位置,不受摄像机影响) + Matrix3f proj = buildThumbnailProjection(width, height); + Matrix3f view = new Matrix3f().identity(); + + // 使用默认着色器 + defaultProgram.use(); + RenderSystem.checkGLError("thumbnail_after_use_program"); + + // 设置基础变换矩阵 + setUniformMatrix3(defaultProgram, "uProjectionMatrix", proj); + setUniformMatrix3(defaultProgram, "uViewMatrix", view); + setUniformFloatInternal(defaultProgram, "uCameraZ", 0f); // 固定Z位置 + RenderSystem.checkGLError("thumbnail_after_set_matrices"); + + // 简化光源:只使用环境光 + setupThumbnailLighting(defaultProgram, model); + RenderSystem.checkGLError("thumbnail_after_setup_lighting"); + + // 应用缩放和平移确保模型完全可见 + Matrix3f thumbnailTransform = new Matrix3f( + bounds.scale, 0, bounds.offsetX, + 0, bounds.scale, bounds.offsetY, + 0, 0, 1 + ); + + // 递归渲染所有根部件(应用缩略图专用变换) + for (ModelPart p : model.getParts()) { + if (p.getParent() != null) continue; + renderPartForThumbnail(p, thumbnailTransform); + } + RenderSystem.checkGLError("thumbnail_after_render_parts"); + + } finally { + // 恢复原始状态 + renderColliders = originalRenderColliders; + renderLightPositions = originalRenderLightPositions; + RenderSystem.viewport(0, 0, originalViewportWidth, originalViewportHeight); + } + + RenderSystem.checkGLError("renderThumbnail_end"); + } + + /** + * 缩略图边界计算结果 + */ + private static class ThumbnailBounds { + public float minX, maxX, minY, maxY; + public float scale; + public float offsetX, offsetY; + } + + /** + * 计算模型的边界和合适的缩放比例 + */ + private static ThumbnailBounds calculateThumbnailBounds(Model2D model, float thumbWidth, float thumbHeight) { + ThumbnailBounds bounds = new ThumbnailBounds(); + + // 初始化为极值 + bounds.minX = Float.MAX_VALUE; + bounds.maxX = Float.MIN_VALUE; + bounds.minY = Float.MAX_VALUE; + bounds.maxY = Float.MIN_VALUE; + + // 计算模型的世界坐标边界(递归遍历所有部件) + calculateModelBounds(model, bounds, new Matrix3f().identity()); + + // 如果模型没有有效边界,使用默认值 + if (bounds.minX > bounds.maxX) { + bounds.minX = -50f; + bounds.maxX = 50f; + bounds.minY = -50f; + bounds.maxY = 50f; + } + + // 计算模型宽度和高度 + float modelWidth = bounds.maxX - bounds.minX; + float modelHeight = bounds.maxY - bounds.minY; + + // 计算中心点 + float centerX = (bounds.minX + bounds.maxX) * 0.5f; + float centerY = (bounds.minY + bounds.maxY) * 0.5f; + + // 计算缩放比例(考虑边距) + float margin = 0.1f; // 10%边距 + float scaleX = (thumbWidth * (1 - margin)) / modelWidth; + float scaleY = (thumbHeight * (1 - margin)) / modelHeight; + bounds.scale = Math.min(scaleX, scaleY); + + // 计算偏移量(将模型中心对齐到缩略图中心) + bounds.offsetX = -centerX; + bounds.offsetY = -centerY; + + return bounds; + } + + /** + * 递归计算模型的边界 + */ + private static void calculateModelBounds(Model2D model, ThumbnailBounds bounds, Matrix3f parentTransform) { + for (ModelPart part : model.getParts()) { + if (part.getParent() != null) continue; // 只处理根部件 + + // 计算部件的世界变换 + part.updateWorldTransform(parentTransform, false); + Matrix3f worldTransform = part.getWorldTransform(); + + // 计算部件的边界 + calculatePartBounds(part, bounds, worldTransform); + + // 递归处理子部件 + for (ModelPart child : part.getChildren()) { + calculateModelBoundsForPart(child, bounds, worldTransform); + } + } + } + + /** + * 递归计算部件及其子部件的边界 + */ + private static void calculateModelBoundsForPart(ModelPart part, ThumbnailBounds bounds, Matrix3f parentTransform) { + part.updateWorldTransform(parentTransform, false); + Matrix3f worldTransform = part.getWorldTransform(); + + calculatePartBounds(part, bounds, worldTransform); + + for (ModelPart child : part.getChildren()) { + calculateModelBoundsForPart(child, bounds, worldTransform); + } + } + + /** + * 计算单个部件的边界 + */ + private static void calculatePartBounds(ModelPart part, ThumbnailBounds bounds, Matrix3f worldTransform) { + for (Mesh2D mesh : part.getMeshes()) { + if (!mesh.isVisible()) continue; + + // 获取网格的顶点数据 + float[] vertices = mesh.getVertices(); // 假设有这个方法获取原始顶点 + if (vertices == null) continue; + + // 变换顶点并更新边界 + for (int i = 0; i < vertices.length; i += 3) { // 假设顶点格式:x, y, z + float x = vertices[i]; + float y = vertices[i + 1]; + + // 应用世界变换 + Vector3f transformed = new Vector3f(x, y, 1.0f); + worldTransform.transform(transformed); + + // 更新边界 + bounds.minX = Math.min(bounds.minX, transformed.x); + bounds.maxX = Math.max(bounds.maxX, transformed.x); + bounds.minY = Math.min(bounds.minY, transformed.y); + bounds.maxY = Math.max(bounds.maxY, transformed.y); + } + } + } + + /** + * 构建缩略图专用的正交投影矩阵 + */ + private static Matrix3f buildThumbnailProjection(float width, float height) { + Matrix3f m = new Matrix3f(); + // 标准正交投影,不受摄像机影响 + m.set( + 2.0f / width, 0.0f, -1.0f, + 0.0f, -2.0f / height, 1.0f, + 0.0f, 0.0f, 1.0f + ); + return m; + } + + /** + * 缩略图专用的部件渲染 + */ + public static void renderPartForThumbnail(ModelPart part, Matrix3f parentTransform) { + part.updateWorldTransform(parentTransform, false); + Matrix3f world = part.getWorldTransform(); + + setPartUniforms(defaultProgram, part); + setUniformMatrix3(defaultProgram, "uModelMatrix", world); + + for (Mesh2D mesh : part.getMeshes()) { + renderMeshForThumbnail(mesh, world); + } + + for (ModelPart child : part.getChildren()) { + renderPartForThumbnail(child, world); + } + } + + /** + * 缩略图专用的网格渲染 + */ + private static void renderMeshForThumbnail(Mesh2D mesh, Matrix3f modelMatrix) { + if (!mesh.isVisible()) return; + + Matrix3f matToUse = mesh.isBakedToWorld() ? new Matrix3f().identity() : new Matrix3f(modelMatrix); + + if (mesh.getTexture() != null) { + mesh.getTexture().bind(0); + setUniformIntInternal(defaultProgram, "uTexture", 0); + } else { + RenderSystem.bindTexture(defaultTextureId); + setUniformIntInternal(defaultProgram, "uTexture", 0); + } + + setUniformMatrix3(defaultProgram, "uModelMatrix", matToUse); + mesh.draw(defaultProgram.programId, matToUse); + + RenderSystem.checkGLError("renderMeshForThumbnail"); + } + + /** + * 设置缩略图专用的简化光照 + */ + private static void setupThumbnailLighting(ShaderProgram sp, Model2D model) { + List lights = model.getLights(); + int ambientLightCount = 0; + + // 查找环境光 + for (int i = 0; i < lights.size() && ambientLightCount < 1; i++) { + LightSource light = lights.get(i); + if (light.isEnabled() && light.isAmbient()) { + setUniformVec2Internal(sp, "uLightsPos[0]", new Vector2f(0f, 0f)); + setUniformVec3Internal(sp, "uLightsColor[0]", light.getColor()); + setUniformFloatInternal(sp, "uLightsIntensity[0]", light.getIntensity()); + setUniformIntInternal(sp, "uLightsIsAmbient[0]", 1); + setUniformIntInternal(sp, "uLightsIsGlow[0]", 0); + ambientLightCount++; + } + } + + // 如果没有环境光,创建一个默认的环境光 + if (ambientLightCount == 0) { + setUniformVec2Internal(sp, "uLightsPos[0]", new Vector2f(0f, 0f)); + setUniformVec3Internal(sp, "uLightsColor[0]", new Vector3f(0.8f, 0.8f, 0.8f)); + setUniformFloatInternal(sp, "uLightsIntensity[0]", 1.0f); + setUniformIntInternal(sp, "uLightsIsAmbient[0]", 1); + setUniformIntInternal(sp, "uLightsIsGlow[0]", 0); + ambientLightCount = 1; + } + + setUniformIntInternal(sp, "uLightCount", ambientLightCount); + + // 禁用所有其他光源槽位 + for (int i = ambientLightCount; i < MAX_LIGHTS; i++) { + setUniformFloatInternal(sp, "uLightsIntensity[" + i + "]", 0f); + setUniformIntInternal(sp, "uLightsIsAmbient[" + i + "]", 0); + } + } + /** * 设置所有非默认着色器的顶点坐标相关uniform diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelAIPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelAIPanel.java new file mode 100644 index 0000000..17b3c82 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelAIPanel.java @@ -0,0 +1,4 @@ +package com.chuangzhou.vivid2D.render.awt; + +public class ModelAIPanel { +} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java index 2998e25..4ed40aa 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java @@ -1,61 +1,47 @@ +// ModelLayerPanel.java (现代化重构) package com.chuangzhou.vivid2D.render.awt; -import com.chuangzhou.vivid2D.render.awt.util.OperationHistoryManager; -import com.chuangzhou.vivid2D.render.awt.util.PsdParser; +import com.chuangzhou.vivid2D.render.awt.manager.LayerOperationManager; +import com.chuangzhou.vivid2D.render.awt.manager.ThumbnailManager; +import com.chuangzhou.vivid2D.render.awt.util.*; +import com.chuangzhou.vivid2D.render.awt.util.renderer.LayerCellRenderer; +import com.chuangzhou.vivid2D.render.awt.util.renderer.LayerReorderTransferHandler; import com.chuangzhou.vivid2D.render.model.Model2D; import com.chuangzhou.vivid2D.render.model.ModelPart; import com.chuangzhou.vivid2D.render.model.util.Mesh2D; import com.chuangzhou.vivid2D.render.model.util.Texture; import org.joml.Vector2f; -import org.lwjgl.system.MemoryUtil; import javax.imageio.ImageIO; import javax.swing.*; -import javax.swing.event.ListSelectionEvent; -import javax.swing.event.ListSelectionListener; +import javax.swing.border.EmptyBorder; +import javax.swing.border.TitledBorder; import java.awt.*; -import java.awt.datatransfer.StringSelection; -import java.awt.datatransfer.Transferable; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; +import java.awt.geom.RoundRectangle2D; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; -import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.concurrent.Callable; -/** - * ModelLayerPanel(完整实现) - *

- * - 列表显示“从上到下”的图层(listModel[0] 为最上层) - * - 在任何修改后都会把 model.parts 同步为列表的反序(保证渲染顺序与 UI 一致) - * - 支持添加空层 / 从文件创建带贴图的层(在有 renderPanel 时在 GL 线程使用 Texture.createFromFile) - * - 支持为选中部件绑定贴图、创建透明图层 - * - 支持拖拽重排、上下按钮移动,并在重排后正确恢复选中与不触发滑块事件 - *

- * 使用: - * new ModelLayerPanel(model, optionalModelRenderPanel) - */ public class ModelLayerPanel extends JPanel { private Model2D model; - private ModelRenderPanel renderPanel; // 可选 GL 渲染面板(用于在其 GL 上下文创建纹理) + private ModelRenderPanel renderPanel; private DefaultListModel listModel; private JList layerList; - private JButton addButton; - private JButton removeButton; - private JButton upButton; - private JButton downButton; - private JButton bindTextureButton; + // 现代化UI组件 + private ModernButton addButton; + private ModernButton removeButton; + private ModernButton upButton; + private ModernButton downButton; + private ModernButton bindTextureButton; private JSlider opacitySlider; private JLabel opacityValueLabel; @@ -63,9 +49,20 @@ public class ModelLayerPanel extends JPanel { private ModelPart draggedPart = null; private Vector2f dragStartPosition = null; - // 程序性设置滑块时忽略事件,避免错误写回 private volatile boolean ignoreSliderEvents = false; + // 使用重构后的工具类 + private ThumbnailManager thumbnailManager; + private PSDImporter psdImporter; + private LayerOperationManager operationManager; + + // 现代化颜色方案 + private static final Color BACKGROUND_COLOR = new Color(45, 45, 48); + private static final Color SURFACE_COLOR = new Color(62, 62, 66); + private static final Color ACCENT_COLOR = new Color(0, 122, 204); + private static final Color TEXT_COLOR = new Color(241, 241, 241); + private static final Color BORDER_COLOR = new Color(87, 87, 87); + public ModelLayerPanel(Model2D model) { this(model, null); } @@ -73,267 +70,402 @@ public class ModelLayerPanel extends JPanel { public ModelLayerPanel(Model2D model, ModelRenderPanel renderPanel) { this.model = model; this.renderPanel = renderPanel; + + // 设置现代化外观 + setupModernLookAndFeel(); + + // 初始化工具类 + this.thumbnailManager = new ThumbnailManager(renderPanel); + this.psdImporter = new PSDImporter(model, renderPanel, this); + this.operationManager = new LayerOperationManager(model); + initComponents(); reloadFromModel(); + generateAllThumbnails(); } + private void setupModernLookAndFeel() { + setBackground(BACKGROUND_COLOR); + setBorder(new EmptyBorder(10, 10, 10, 10)); + + // 设置现代化UI默认值 + UIManager.put("List.background", SURFACE_COLOR); + UIManager.put("List.foreground", TEXT_COLOR); + UIManager.put("List.selectionBackground", ACCENT_COLOR); + UIManager.put("List.selectionForeground", Color.WHITE); + UIManager.put("ScrollPane.background", SURFACE_COLOR); + UIManager.put("ScrollPane.border", BorderFactory.createLineBorder(BORDER_COLOR)); + UIManager.put("Slider.background", SURFACE_COLOR); + UIManager.put("Slider.foreground", ACCENT_COLOR); + } + + // ============== 缩略图相关方法 ============== + private void generateAllThumbnails() { + if (model == null) return; + + thumbnailManager.clearCache(); + for (int i = 0; i < listModel.getSize(); i++) { + ModelPart part = listModel.get(i); + thumbnailManager.generateThumbnail(part); + } + layerList.repaint(); + } + + private void refreshSelectedThumbnail() { + ModelPart selected = layerList.getSelectedValue(); + if (selected != null) { + thumbnailManager.generateThumbnail(selected); + layerList.repaint(); + } + } + + // ============== 现代化组件初始化 ============== + private void initComponents() { + setLayout(new BorderLayout(10, 10)); + listModel = new DefaultListModel<>(); + layerList = createModernList(); + + // 创建现代化布局 + createHeaderPanel(); + createCenterPanel(); + createControlPanel(); + } + + private JList createModernList() { + JList list = new JList<>(listModel); + list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + list.setBackground(SURFACE_COLOR); + list.setForeground(TEXT_COLOR); + list.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + list.setFixedCellHeight(70); // 增加行高以显示缩略图 + + // 使用独立的渲染器 + LayerCellRenderer cellRenderer = new LayerCellRenderer(this, thumbnailManager); + cellRenderer.attachMouseListener(list, listModel); + list.setCellRenderer(cellRenderer); + + // 使用独立的拖拽处理器 + list.setDragEnabled(true); + list.setTransferHandler(new LayerReorderTransferHandler(this)); + list.setDropMode(DropMode.INSERT); + + // 双击重命名 + list.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 2) { + int idx = list.locationToIndex(e.getPoint()); + if (idx >= 0) { + showRenameDialog(listModel.get(idx)); + } + } + } + }); + + // 选择变更监听器 + list.addListSelectionListener(e -> updateUIState()); + + return list; + } + + private void createHeaderPanel() { + JPanel headerPanel = new JPanel(new BorderLayout()); + headerPanel.setBackground(BACKGROUND_COLOR); + headerPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0)); + + JLabel titleLabel = new JLabel("图层管理"); + titleLabel.setForeground(TEXT_COLOR); + titleLabel.setFont(titleLabel.getFont().deriveFont(Font.BOLD, 16f)); + + headerPanel.add(titleLabel, BorderLayout.WEST); + add(headerPanel, BorderLayout.NORTH); + } + + private void createCenterPanel() { + JScrollPane scrollPane = new JScrollPane(layerList); + scrollPane.setBorder(createModernBorder("图层列表")); + scrollPane.getViewport().setBackground(SURFACE_COLOR); + + // 自定义滚动条 + JScrollBar verticalScrollBar = scrollPane.getVerticalScrollBar(); + //verticalScrollBar.setUI(new ModernScrollBarUI()); + + add(scrollPane, BorderLayout.CENTER); + } + + private void createControlPanel() { + JPanel controlPanel = new JPanel(new BorderLayout(10, 10)); + controlPanel.setBackground(BACKGROUND_COLOR); + + // 顶部按钮面板 + controlPanel.add(createButtonPanel(), BorderLayout.NORTH); + // 底部设置面板 + controlPanel.add(createSettingsPanel(), BorderLayout.SOUTH); + + add(controlPanel, BorderLayout.SOUTH); + } + + private JPanel createButtonPanel() { + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 8)); + buttonPanel.setBackground(BACKGROUND_COLOR); + buttonPanel.setBorder(createModernBorder("操作")); + + // 创建现代化按钮 + addButton = createIconButton("⊕", "添加图层", this::showAddMenu); + removeButton = createIconButton("⊖", "删除选中图层", this::onRemoveLayer); + upButton = createIconButton("↑", "上移图层", this::moveSelectedUp); + downButton = createIconButton("↓", "下移图层", this::moveSelectedDown); + bindTextureButton = createIconButton("📷", "绑定贴图", this::bindTextureToSelectedPart); + + // 初始禁用状态 + removeButton.setEnabled(false); + upButton.setEnabled(false); + downButton.setEnabled(false); + bindTextureButton.setEnabled(false); + + buttonPanel.add(addButton); + buttonPanel.add(removeButton); + buttonPanel.add(upButton); + buttonPanel.add(downButton); + buttonPanel.add(bindTextureButton); + + return buttonPanel; + } + + private JPanel createSettingsPanel() { + JPanel settingsPanel = new JPanel(new BorderLayout(10, 5)); + settingsPanel.setBackground(BACKGROUND_COLOR); + settingsPanel.setBorder(createModernBorder("图层设置")); + + // 不透明度控制 + JPanel opacityPanel = new JPanel(new BorderLayout(8, 0)); + opacityPanel.setBackground(BACKGROUND_COLOR); + + JLabel opacityLabel = new JLabel("不透明度:"); + opacityLabel.setForeground(TEXT_COLOR); + + opacitySlider = createModernSlider(); + opacityValueLabel = new JLabel("100%"); + opacityValueLabel.setForeground(TEXT_COLOR); + opacityValueLabel.setPreferredSize(new Dimension(40, 20)); + + opacitySlider.addChangeListener(e -> { + if (ignoreSliderEvents) return; + onOpacityChanged(); + }); + + opacityPanel.add(opacityLabel, BorderLayout.WEST); + opacityPanel.add(opacitySlider, BorderLayout.CENTER); + opacityPanel.add(opacityValueLabel, BorderLayout.EAST); + + settingsPanel.add(opacityPanel, BorderLayout.CENTER); + return settingsPanel; + } + + private JSlider createModernSlider() { + JSlider slider = new JSlider(0, 100, 100); + slider.setBackground(BACKGROUND_COLOR); + slider.setForeground(ACCENT_COLOR); + + // 自定义滑块UI + //slider.setUI(new ModernSliderUI()); + + return slider; + } + + private ModernButton createIconButton(String icon, String tooltip, Runnable action) { + ModernButton button = new ModernButton(icon); + button.setToolTipText(tooltip); + button.addActionListener(e -> action.run()); + return button; + } + + private void showAddMenu() { + JPopupMenu addMenu = new ModernPopupMenu(); + + String[] menuItems = { + "📄 创建空图层", + "🖼️ 从文件创建图层", + "🎨 创建透明图层", + "---", + "📂 从PSD文件导入" + }; + + Runnable[] actions = { + this::createEmptyPart, + this::createPartWithTextureFromFile, + this::createPartWithTransparentTexture, + null, + this::importPSDFile + }; + + for (int i = 0; i < menuItems.length; i++) { + if (menuItems[i].equals("---")) { + addMenu.add(new JSeparator()); + } else { + JMenuItem item = new ModernMenuItem(menuItems[i]); + if (actions[i] != null) { + int finalI = i; + item.addActionListener(e -> actions[finalI].run()); + } + addMenu.add(item); + } + } + + addMenu.show(addButton, 0, addButton.getHeight()); + } + + private void createEmptyPart() { + String name = JOptionPane.showInputDialog(this, "新图层名称:", "新图层"); + if (name == null || name.trim().isEmpty()) return; + + operationManager.addLayer(name); + reloadFromModel(); + + // 选中新创建的部件 + ModelPart newPart = findPartByName(name); + if (newPart != null) { + selectPart(newPart); + thumbnailManager.generateThumbnail(newPart); + } + } + + private ModelPart findPartByName(String name) { + if (model == null) return null; + Map partMap = model.getPartMap(); + return partMap != null ? partMap.get(name) : null; + } + + public Map getModelPartMap() { + if (model == null) return null; + return model.getPartMap(); + } + + // ============== 现代化对话框方法 ============== + private void showRenameDialog(ModelPart part) { + String newName = (String) JOptionPane.showInputDialog( + this, + "输入新名称:", + "重命名图层", + JOptionPane.PLAIN_MESSAGE, + null, + null, + part.getName() + ); + + if (newName != null && !newName.trim().isEmpty()) { + renamePart(part, newName); + reloadFromModel(); + refreshSelectedThumbnail(); + } + } + + // ============== 原有业务方法(保持不变) ============== public void setModel(Model2D model) { this.model = model; + this.psdImporter = new PSDImporter(model, renderPanel, this); + this.operationManager = new LayerOperationManager(model); reloadFromModel(); + generateAllThumbnails(); } public void setRenderPanel(ModelRenderPanel panel) { this.renderPanel = panel; + this.thumbnailManager = new ThumbnailManager(panel); + this.psdImporter = new PSDImporter(model, panel, this); } - // ============== PSD文件导入 ============== - private void importPSDFile() { JFileChooser chooser = new JFileChooser(); chooser.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("PSD文件", "psd")); int r = chooser.showOpenDialog(this); if (r == JFileChooser.APPROVE_OPTION) { - importPSDFile(chooser.getSelectedFile()); + psdImporter.importPSDFile(chooser.getSelectedFile()); } } - private void importPSDFile(File psdFile) { + private void updateUIState() { + ModelPart sel = layerList.getSelectedValue(); + boolean hasSelection = sel != null; + + if (hasSelection) { + updateOpacitySlider(sel); + } + + removeButton.setEnabled(hasSelection); + upButton.setEnabled(hasSelection); + downButton.setEnabled(hasSelection); + bindTextureButton.setEnabled(hasSelection); + } + + private void updateOpacitySlider(ModelPart part) { + float opacity = extractOpacity(part); + int value = Math.round(opacity * 100); + + ignoreSliderEvents = true; try { - // 使用工具类解析PSD文件 - PsdParser.PSDImportResult result = PsdParser.parsePSDFile(psdFile); - if (result != null && !result.layers.isEmpty()) { - int choice = JOptionPane.showConfirmDialog(this, - String.format("PSD文件包含 %d 个图层,是否全部导入?", result.layers.size()), - "导入PSD图层", JOptionPane.YES_NO_OPTION); - - if (choice == JOptionPane.YES_OPTION) { - importPSDLayers(result); - } - } else { - JOptionPane.showMessageDialog(this, "未找到可导入的PSD图层或解析失败", "提示", JOptionPane.INFORMATION_MESSAGE); - } - } catch (Exception ex) { - ex.printStackTrace(); - JOptionPane.showMessageDialog(this, - "解析PSD文件失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); + opacitySlider.setValue(value); + opacityValueLabel.setText(value + "%"); + } finally { + ignoreSliderEvents = false; } } - /** - * 导入PSD图层到模型 - */ - private void importPSDLayers(PsdParser.PSDImportResult result) { - if (renderPanel != null) { - // 使用更可靠的方式在GL上下文中创建纹理 - try { - // 在GL上下文中同步执行所有图层的创建 - renderPanel.getGlContextManager().executeInGLContext(() -> { - try { - List createdParts = new ArrayList<>(); - - for (PsdParser.PSDLayerInfo layerInfo : result.layers) { - try { - ModelPart part = createPartFromPSDLayer(layerInfo); - if (part != null) { - createdParts.add(part); - } else { - System.err.println("创建图层失败: " + layerInfo.name); - } - } catch (Exception e) { - System.err.println("创建PSD图层异常: " + layerInfo.name + " - " + e.getMessage()); - e.printStackTrace(); - } - } - - // 确保模型更新 - if (model != null) { - model.markNeedsUpdate(); - System.out.println("模型标记为需要更新,已创建 " + createdParts.size() + " 个图层"); - } - - // 在GL线程中立即更新UI - SwingUtilities.invokeLater(() -> { - reloadFromModel(); - if (!createdParts.isEmpty()) { - selectPart(createdParts.get(0)); - } - }); - } catch (Exception e) { - e.printStackTrace(); - SwingUtilities.invokeLater(() -> { - JOptionPane.showMessageDialog(ModelLayerPanel.this, - "导入PSD图层失败: " + e.getMessage(), - "错误", JOptionPane.ERROR_MESSAGE); - }); - } - }); - - } catch (Exception e) { - e.printStackTrace(); - JOptionPane.showMessageDialog(this, - "执行GL上下文任务失败: " + e.getMessage(), - "错误", JOptionPane.ERROR_MESSAGE); - } - - } else { - // 无GL上下文的情况 - 直接创建 - System.out.println("无GL上下文,直接创建PSD图层"); - List createdParts = new ArrayList<>(); - - for (PsdParser.PSDLayerInfo layerInfo : result.layers) { - try { - ModelPart part = createPartFromPSDLayer(layerInfo); - if (part != null) { - createdParts.add(part); - } else { - System.err.println("创建图层失败: " + layerInfo.name); - } - } catch (Exception e) { - System.err.println("创建PSD图层异常: " + layerInfo.name + " - " + e.getMessage()); - e.printStackTrace(); - } - } - - if (model != null) { - model.markNeedsUpdate(); - System.out.println("模型标记为需要更新,已创建 " + createdParts.size() + " 个图层"); - } - - reloadFromModel(); - if (!createdParts.isEmpty()) { - selectPart(createdParts.get(0)); - } - - JOptionPane.showMessageDialog(this, - "成功导入 " + createdParts.size() + " 个PSD图层", - "导入完成", JOptionPane.INFORMATION_MESSAGE); - } - } - - private String ensureUniquePartName(String baseName) { - if (model == null) return baseName; - Map partMap = getModelPartMap(); - if (partMap == null) return baseName; - String name = baseName; - int idx = 1; - while (partMap.containsKey(name)) { - name = baseName + "_" + idx++; - } - return name; - } - - /** - * 从PSD图层信息创建部件 - 返回创建的部件 - */ - private ModelPart createPartFromPSDLayer(PsdParser.PSDLayerInfo layerInfo) { + private float extractOpacity(ModelPart part) { try { - System.out.println("正在创建PSD图层: " + layerInfo.name + " [" + - layerInfo.width + "x" + layerInfo.height + "]" + "[x=" + layerInfo.x + ",y=" + layerInfo.y + "]"); - - // 确保部件名唯一,避免覆盖已有部件导致“合并成一个图层”的问题 - String uniqueName = ensureUniquePartName(layerInfo.name); - - // 创建部件 - ModelPart part = model.createPart(uniqueName); - if (part == null) { - System.err.println("创建部件失败: " + uniqueName); - return null; - } - - // 如果 model 有 partMap,更新映射(防止老实现以 name 为 key 覆盖或冲突) - try { - Map partMap = getModelPartMap(); - if (partMap != null) { - partMap.put(uniqueName, part); - } - } catch (Exception ignored) { - } - - part.setVisible(layerInfo.visible); - - // 设置不透明度(优先使用公开方法) - try { - part.setOpacity(layerInfo.opacity); - } catch (Throwable t) { - // 如果没有公开方法,尝试通过反射备用(保持兼容) - try { - Field f = part.getClass().getDeclaredField("opacity"); - f.setAccessible(true); - f.setFloat(part, layerInfo.opacity); - } catch (Throwable ignored) { - System.err.println("设置不透明度失败: " + uniqueName); - } - } - part.setPosition(layerInfo.x, layerInfo.y); - - // 创建网格(使用唯一 mesh 名避免工厂复用同一实例) - long uniq = System.nanoTime(); - Mesh2D mesh = createQuadForImage(layerInfo.image, uniqueName + "_mesh_" + uniq); - - // 把 mesh 加入 part(注意部分实现可能复制或包装 mesh) - part.addMesh(mesh); - - // 创建纹理(使用唯一名称,防止按 name 在内部被复用或覆盖) - String texName = uniqueName + "_tex_" + uniq; - Texture texture = createTextureFromBufferedImage(layerInfo.image, texName); - if (texture != null) { - // 尝试把纹理设置到实际被 part 持有的 mesh 上(取最后一个元素作为刚刚添加的 mesh) - try { - java.util.List partMeshes = part.getMeshes(); - Mesh2D actualMesh = null; - if (partMeshes != null && !partMeshes.isEmpty()) { - actualMesh = partMeshes.get(partMeshes.size() - 1); - } - - if (actualMesh != null) { - actualMesh.setTexture(texture); - } else { - // 兜底:如果没拿到实际 mesh,仍然设置在原始 mesh 上以避免丢失 - mesh.setTexture(texture); - } - - // 把纹理加入 model 管理 - model.addTexture(texture); - - // 强制尝试上传/初始化(若纹理对象需要) - try { - tryCallTextureUpload(texture); - } catch (Throwable ignored) { - } - - // 标记模型需要更新 - model.markNeedsUpdate(); - } catch (Throwable e) { - System.err.println("在绑定纹理到 mesh 时出错: " + uniqueName + " - " + e.getMessage()); - e.printStackTrace(); - } - - // 触发 UI/渲染刷新(使用 EDT) - SwingUtilities.invokeLater(() -> { - try { - reloadFromModel(); - } catch (Throwable ignored) { - } - try { - if (renderPanel != null) renderPanel.repaint(); - } catch (Throwable ignored) { - } - }); - } else { - System.err.println("创建纹理失败: " + uniqueName); - } - - return part; - + Method method = part.getClass().getMethod("getOpacity"); + Object value = method.invoke(part); + if (value instanceof Float) return (Float) value; } catch (Exception e) { - System.err.println("创建PSD图层部件失败: " + layerInfo.name + " - " + e.getMessage()); - e.printStackTrace(); - return null; + try { + Field field = part.getClass().getDeclaredField("opacity"); + field.setAccessible(true); + Object value = field.get(part); + if (value instanceof Float) return (Float) value; + } catch (Exception ignored) {} + } + return 1.0f; + } + + private void onOpacityChanged() { + ModelPart sel = layerList.getSelectedValue(); + if (sel == null) return; + + int value = opacitySlider.getValue(); + opacityValueLabel.setText(value + "%"); + + setPartOpacity(sel, value / 100.0f); + + if (model != null) model.markNeedsUpdate(); + layerList.repaint(); + refreshSelectedThumbnail(); + } + + private void setPartOpacity(ModelPart part, float opacity) { + try { + Method method = part.getClass().getMethod("setOpacity", float.class); + method.invoke(part, opacity); + } catch (Exception e) { + try { + Field field = part.getClass().getDeclaredField("opacity"); + field.setAccessible(true); + field.setFloat(part, opacity); + } catch (Exception ignored) {} } } - private Texture createTextureFromBufferedImage(BufferedImage img, String texName) { - // 在创建纹理前翻转图片 + // 现代化边框 + private TitledBorder createModernBorder(String title) { + TitledBorder border = BorderFactory.createTitledBorder( + BorderFactory.createLineBorder(BORDER_COLOR, 1, true), + title + ); + border.setTitleColor(TEXT_COLOR); + border.setTitleFont(border.getTitleFont().deriveFont(Font.BOLD, 12f)); + return border; + } + + public Texture createTextureFromBufferedImage(BufferedImage img, String texName) { BufferedImage flippedImage = flipImageVertically(img); return Texture.createFromBufferedImage(texName, flippedImage); } @@ -348,300 +480,258 @@ public class ModelLayerPanel extends JPanel { return flipped; } - /** - * 将BufferedImage转换为字节数组 - */ - private byte[] bufferedImageToByteArray(BufferedImage img) { - if (img == null) return null; - try { - final int width = img.getWidth(); - final int height = img.getHeight(); - final int len = width * height; + public void refreshCurrentThumbnail() { + refreshSelectedThumbnail(); + } - // 尽量直接取得 int[] 像素数据(避免 getRGB 每像素的开销) - final int[] pixels; - int imgType = img.getType(); - if (imgType == BufferedImage.TYPE_INT_ARGB && img.getRaster().getDataBuffer() instanceof java.awt.image.DataBufferInt) { - pixels = ((java.awt.image.DataBufferInt) img.getRaster().getDataBuffer()).getData(); - } else { - // 转换为 TYPE_INT_ARGB(非预乘),尽量用最近邻以加快绘制 - BufferedImage converted = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - Graphics2D g = converted.createGraphics(); + // 现代化按钮类 + private class ModernButton extends JButton { + public ModernButton(String text) { + super(text); + setupModernStyle(); + } + + private void setupModernStyle() { + setBackground(SURFACE_COLOR); + setForeground(TEXT_COLOR); + setBorder(BorderFactory.createCompoundBorder( + BorderFactory.createLineBorder(BORDER_COLOR, 1, true), + BorderFactory.createEmptyBorder(8, 12, 8, 12) + )); + setFocusPainted(false); + setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + + addMouseListener(new MouseAdapter() { + @Override + public void mouseEntered(MouseEvent e) { + if (isEnabled()) { + setBackground(ACCENT_COLOR); + setForeground(Color.WHITE); + } + } + + @Override + public void mouseExited(MouseEvent e) { + setBackground(SURFACE_COLOR); + setForeground(TEXT_COLOR); + } + }); + } + } + + // 现代化菜单项类 + private class ModernMenuItem extends JMenuItem { + public ModernMenuItem(String text) { + super(text); + setBackground(SURFACE_COLOR); + setForeground(TEXT_COLOR); + setBorder(BorderFactory.createEmptyBorder(8, 12, 8, 12)); + + addMouseListener(new MouseAdapter() { + @Override + public void mouseEntered(MouseEvent e) { + setBackground(ACCENT_COLOR); + setForeground(Color.WHITE); + } + + @Override + public void mouseExited(MouseEvent e) { + setBackground(SURFACE_COLOR); + setForeground(TEXT_COLOR); + } + }); + } + } + + // 现代化弹出菜单 + private class ModernPopupMenu extends JPopupMenu { + public ModernPopupMenu() { + setBackground(SURFACE_COLOR); + setBorder(BorderFactory.createLineBorder(BORDER_COLOR)); + } + } + + // 其余原有方法保持不变... + // (reloadFromModel, performVisualReorder, bindTextureToSelectedPart等) + + public void reloadFromModel() { + ModelPart selected = layerList.getSelectedValue(); + + listModel.clear(); + if (model == null) return; + try { + List parts = model.getParts(); + if (parts != null) { + for (int i = parts.size() - 1; i >= 0; i--) { + listModel.addElement(parts.get(i)); + } + } + } catch (Exception ex) { + ex.printStackTrace(); + } + + if (selected != null) { + for (int i = 0; i < listModel.getSize(); i++) { + if (listModel.get(i) == selected) { + layerList.setSelectedIndex(i); + break; + } + } + } + } + + public void performVisualReorder(int visualFrom, int visualTo) { + if (model == null) return; + try { + int size = listModel.getSize(); + if (visualFrom < 0 || visualFrom >= size) return; + if (visualTo < 0) visualTo = 0; + if (visualTo > size - 1) visualTo = size - 1; + + ModelPart moved = listModel.get(visualFrom); + if (!isDragging) { + isDragging = true; + draggedPart = moved; + dragStartPosition = new Vector2f(moved.getPosition()); + } + + List visual = new ArrayList<>(size); + for (int i = 0; i < size; i++) visual.add(listModel.get(i)); + moved = visual.remove(visualFrom); + visual.add(visualTo, moved); + + ignoreSliderEvents = true; + try { + listModel.clear(); + for (ModelPart p : visual) listModel.addElement(p); + } finally { + ignoreSliderEvents = false; + } + + operationManager.moveLayer(visual); + selectPart(moved); + } catch (Exception ex) { + ex.printStackTrace(); + } + } + + private void selectPart(ModelPart part) { + if (part == null) return; + for (int i = 0; i < listModel.getSize(); i++) { + if (listModel.get(i) == part) { + layerList.setSelectedIndex(i); + layerList.ensureIndexIsVisible(i); + return; + } + } + } + + private void renamePart(ModelPart part, String newName) { + if (part == null) return; + part.setName(newName); + } + + public Model2D getModel() { + return model; + } + + private void bindTextureToSelectedPart() { + ModelPart sel = layerList.getSelectedValue(); + if (sel == null) return; + + JFileChooser chooser = new JFileChooser(); + int r = chooser.showOpenDialog(this); + if (r != JFileChooser.APPROVE_OPTION) return; + File f = chooser.getSelectedFile(); + try { + BufferedImage img = null; + try { + img = ImageIO.read(f); + } catch (Exception ignored) { + } + + Mesh2D targetMesh = null; + try { + List list = sel.getMeshes(); + if (!list.isEmpty() && list.get(0) != null) { + targetMesh = list.get(0); + } + } catch (Exception ignored) { + } + + if (targetMesh == null) { + if (img == null) { + img = new BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB); + } + targetMesh = MeshTextureUtil.createQuadForImage(img, sel.getName() + "_mesh"); try { - g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); - g.drawImage(img, 0, 0, null); - } finally { - g.dispose(); - } - pixels = ((java.awt.image.DataBufferInt) converted.getRaster().getDataBuffer()).getData(); - } - - // 输出数组 RGBA 顺序(每像素 4 字节) - byte[] bytes = new byte[len * 4]; - int outIndex = 0; - - // 局部变量加速 - for (int i = 0; i < len; i++) { - int p = pixels[i]; - bytes[outIndex++] = (byte) ((p >> 16) & 0xFF); // R - bytes[outIndex++] = (byte) ((p >> 8) & 0xFF); // G - bytes[outIndex++] = (byte) (p & 0xFF); // B - bytes[outIndex++] = (byte) ((p >> 24) & 0xFF); // A - } - - return bytes; - } catch (Exception e) { - System.err.println("转换BufferedImage到字节数组失败: " + e.getMessage()); - return null; - } - } - - - /** - * 通过构造函数创建纹理 - 增强版本 - */ - private Texture createTextureViaConstructor(BufferedImage img, String texName) { - try { - int w = img.getWidth(); - int h = img.getHeight(); - - // 将BufferedImage转换为ByteBuffer - ByteBuffer buffer = bufferedImageToByteBuffer(img); - if (buffer == null) { - System.err.println("创建ByteBuffer失败: " + texName); - return null; - } - - // 使用Texture类的构造函数 - Texture texture = new Texture(texName, w, h, Texture.TextureFormat.RGBA, buffer); - - // 设置纹理参数 - texture.setMinFilter(Texture.TextureFilter.LINEAR); - texture.setMagFilter(Texture.TextureFilter.LINEAR); - texture.setWrapS(Texture.TextureWrap.CLAMP_TO_EDGE); - texture.setWrapT(Texture.TextureWrap.CLAMP_TO_EDGE); - - // 缓存像素数据以便后续使用 - texture.ensurePixelDataCached(); - - return texture; - - } catch (Exception e) { - System.err.println("通过构造函数创建纹理失败: " + texName + " - " + e.getMessage()); - e.printStackTrace(); - return null; - } - } - - /** - * 将BufferedImage转换为ByteBuffer - */ - private ByteBuffer bufferedImageToByteBuffer(BufferedImage img) { - try { - int width = img.getWidth(); - int height = img.getHeight(); - int[] pixels = new int[width * height]; - img.getRGB(0, 0, width, height, pixels, 0, width); - - ByteBuffer buffer = MemoryUtil.memAlloc(width * height * 4); - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - int pixel = pixels[y * width + x]; - // RGBA格式 - buffer.put((byte) ((pixel >> 16) & 0xFF)); // R - buffer.put((byte) ((pixel >> 8) & 0xFF)); // G - buffer.put((byte) (pixel & 0xFF)); // B - buffer.put((byte) ((pixel >> 24) & 0xFF)); // A - } - } - buffer.flip(); - return buffer; - - } catch (Exception e) { - System.err.println("转换BufferedImage到ByteBuffer失败: " + e.getMessage()); - return null; - } - } - - private void initComponents() { - setLayout(new BorderLayout()); - listModel = new DefaultListModel<>(); - layerList = new JList<>(listModel); - layerList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - layerList.setCellRenderer(new LayerCellRenderer()); - layerList.setDragEnabled(true); - layerList.setTransferHandler(new LayerReorderTransferHandler()); - layerList.setDropMode(DropMode.INSERT); - - // 双击重命名 - layerList.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - if (e.getClickCount() == 2) { - int idx = layerList.locationToIndex(e.getPoint()); - if (idx >= 0) { - ModelPart part = listModel.get(idx); - String newName = JOptionPane.showInputDialog( - ModelLayerPanel.this, - "输入新名称:", - part.getName() - ); - if (newName != null && !newName.trim().isEmpty()) { - renamePart(part, newName); - reloadFromModel(); - } - } - } - } - }); - - // 选择变更 -> 更新滑块显示(但程序性更新时要忽略事件) - layerList.addListSelectionListener(new ListSelectionListener() { - @Override - public void valueChanged(ListSelectionEvent e) { - ModelPart sel = layerList.getSelectedValue(); - if (sel != null) { - float op = 1.0f; - try { - Method gm = sel.getClass().getMethod("getOpacity"); - Object v = gm.invoke(sel); - if (v instanceof Float) op = (Float) v; - } catch (Exception ex) { - try { - Field f = sel.getClass().getDeclaredField("opacity"); - f.setAccessible(true); - Object v = f.get(sel); - if (v instanceof Float) op = (Float) v; - } catch (Exception ignored) { - } - } - int val = Math.round(op * 100); - - // 程序性更新滑块时阻止 ChangeListener 响应 - ignoreSliderEvents = true; - try { - opacitySlider.setValue(val); - opacityValueLabel.setText(val + "%"); - } finally { - ignoreSliderEvents = false; - } - - removeButton.setEnabled(true); - upButton.setEnabled(true); - downButton.setEnabled(true); - bindTextureButton.setEnabled(true); - } else { - removeButton.setEnabled(false); - upButton.setEnabled(false); - downButton.setEnabled(false); - bindTextureButton.setEnabled(false); - } - } - }); - - JScrollPane scroll = new JScrollPane(layerList); - add(scroll, BorderLayout.CENTER); - - // 按钮区 - JPanel controls = new JPanel(new BorderLayout()); - JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 4)); - - addButton = new JButton("+"); - addButton.setToolTipText("添加图层(点击箭头选择创建方式)"); - JPopupMenu addMenu = new JPopupMenu(); - JMenuItem addBlank = new JMenuItem("创建空图层 (无贴图)"); - JMenuItem addWithTexture = new JMenuItem("从文件选择贴图并创建图层"); - JMenuItem addTransparent = new JMenuItem("创建透明贴图图层"); - JMenuItem addPSD = new JMenuItem("从PSD文件导入图层"); - addMenu.add(addBlank); - addMenu.add(addWithTexture); - addMenu.add(addTransparent); - addMenu.add(new JSeparator()); - addMenu.add(addPSD); - addButton.addActionListener(e -> addMenu.show(addButton, 0, addButton.getHeight())); - - addBlank.addActionListener(e -> createEmptyPart()); - addWithTexture.addActionListener(e -> createPartWithTextureFromFile()); - addTransparent.addActionListener(e -> createPartWithTransparentTexture()); - addPSD.addActionListener(e -> importPSDFile()); - - removeButton = new JButton("-"); - removeButton.setToolTipText("删除选中图层"); - removeButton.addActionListener(e -> onRemoveLayer()); - removeButton.setEnabled(false); - - upButton = new JButton("\u25B2"); - upButton.setToolTipText("上移图层"); - upButton.addActionListener(e -> moveSelectedUp()); - upButton.setEnabled(false); - - downButton = new JButton("\u25BC"); - downButton.setToolTipText("下移图层"); - downButton.addActionListener(e -> moveSelectedDown()); - downButton.setEnabled(false); - - bindTextureButton = new JButton("绑定贴图"); - bindTextureButton.setToolTipText("为选中部件绑定贴图(选择文件)"); - bindTextureButton.addActionListener(e -> bindTextureToSelectedPart()); - bindTextureButton.setEnabled(false); - - btnPanel.add(addButton); - btnPanel.add(removeButton); - btnPanel.add(upButton); - btnPanel.add(downButton); - btnPanel.add(bindTextureButton); - controls.add(btnPanel, BorderLayout.NORTH); - - // 不透明度面板 - JPanel opacityPanel = new JPanel(new BorderLayout(6, 6)); - opacityPanel.setBorder(BorderFactory.createTitledBorder("不透明度")); - opacitySlider = new JSlider(0, 100, 100); - opacityValueLabel = new JLabel("100%"); - opacitySlider.addChangeListener(e -> { - if (ignoreSliderEvents) return; - - ModelPart sel = layerList.getSelectedValue(); - int val = opacitySlider.getValue(); - opacityValueLabel.setText(val + "%"); - if (sel != null) { - try { - Method sm = sel.getClass().getMethod("setOpacity", float.class); - sm.invoke(sel, val / 100.0f); + sel.addMesh(targetMesh); } catch (Exception ex) { - try { - Field f = sel.getClass().getDeclaredField("opacity"); - f.setAccessible(true); - f.setFloat(sel, val / 100.0f); - } catch (Exception ignored) { - } + ex.printStackTrace(); } - if (model != null) model.markNeedsUpdate(); - layerList.repaint(); } - }); - opacityPanel.add(opacitySlider, BorderLayout.CENTER); - opacityPanel.add(opacityValueLabel, BorderLayout.EAST); + final Mesh2D meshToBind = targetMesh; + final String filePath = f.getAbsolutePath(); + final String texName = sel.getName() + "_tex"; - controls.add(opacityPanel, BorderLayout.SOUTH); + if (renderPanel != null) { + renderPanel.getGlContextManager().executeInGLContext(() -> { + try { + Texture texture = Texture.createFromFile(texName, filePath); + meshToBind.setTexture(texture); + model.addTexture(texture); + model.markNeedsUpdate(); + } catch (Throwable ex) { + ex.printStackTrace(); + } + }); + } else { + if (img == null) img = ImageIO.read(f); + Texture mem = MeshTextureUtil.tryCreateTextureFromImageMemory(img, texName); + if (mem != null) { + meshToBind.setTexture(mem); + model.addTexture(mem); + model.markNeedsUpdate(); + } else { + System.err.println("无法在无 GL 上下文中创建纹理: " + filePath); + } + } - add(controls, BorderLayout.SOUTH); + reloadFromModel(); + selectPart(sel); + refreshSelectedThumbnail(); + } catch (Exception ex) { + JOptionPane.showMessageDialog(this, "绑定贴图失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); + ex.printStackTrace(); + } } - // ============== 部件创建 / 贴图绑定 ============== + private void onRemoveLayer() { + ModelPart sel = layerList.getSelectedValue(); + if (sel == null) return; + int r = JOptionPane.showConfirmDialog(this, "确认删除图层:" + sel.getName() + " ?", "确认删除", JOptionPane.YES_NO_OPTION); + if (r != JOptionPane.YES_OPTION) return; - private void createEmptyPart() { - String name = JOptionPane.showInputDialog(this, "新图层名称:", "新图层"); - if (name == null || name.trim().isEmpty()) return; + try { + operationManager.removeLayer(sel); + thumbnailManager.removeThumbnail(sel); + reloadFromModel(); + } catch (Exception ex) { + JOptionPane.showMessageDialog(this, "删除失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); + } + } - // 使用 model.createPart 创建(会加入 model.parts 的末尾 -> 视为底层) - ModelPart part = model.createPart(name); - model.markNeedsUpdate(); + private void moveSelectedUp() { + int idx = layerList.getSelectedIndex(); + if (idx <= 0) return; + performVisualReorder(idx, idx - 1); + } - // reload 并把新创建的部件选中(列表显示从上到下,所以新部件在底部/最后,需要在 reload 后定位) - reloadFromModel(); - selectPart(part); + private void moveSelectedDown() { + int idx = layerList.getSelectedIndex(); + if (idx < 0 || idx >= listModel.getSize() - 1) return; + performVisualReorder(idx, idx + 1); } private void createPartWithTextureFromFile() { @@ -655,58 +745,70 @@ public class ModelLayerPanel extends JPanel { String name = JOptionPane.showInputDialog(this, "新图层名称:", f.getName()); if (name == null || name.trim().isEmpty()) name = f.getName(); - // 先创建部件与 Mesh(基于图片尺寸) + // 创建部件与 Mesh ModelPart part = model.createPart(name); - //part.setPivot(0,0); - Mesh2D mesh = createQuadForImage(img, name + "_mesh"); + Mesh2D mesh = MeshTextureUtil.createQuadForImage(img, name + "_mesh"); + mesh.createDefaultSecondaryVertices(); part.addMesh(mesh); - // 在有 GL 上下文时优先使用 Texture.createFromFile 在 GL 线程创建 + // 创建纹理 if (renderPanel != null) { final String texName = name + "_tex"; final String filePath = f.getAbsolutePath(); renderPanel.getGlContextManager().executeInGLContext(() -> { try { Texture texture = Texture.createFromFile(texName, filePath); - if (texture != null) { - // 找到实际被加入到 part 的 mesh(通常为最后一个) - java.util.List partMeshes = part.getMeshes(); - Mesh2D actualMesh = null; - if (partMeshes != null && !partMeshes.isEmpty()) { - actualMesh = partMeshes.get(partMeshes.size() - 1); - } - - if (actualMesh != null) { - actualMesh.setTexture(texture); - } else { - // 兜底:如果没找到(极少数情况),仍然设置在原始 mesh 上以避免丢失 - mesh.setTexture(texture); - } - - model.addTexture(texture); - model.markNeedsUpdate(); + List partMeshes = part.getMeshes(); + Mesh2D actualMesh = null; + if (partMeshes != null && !partMeshes.isEmpty()) { + actualMesh = partMeshes.get(partMeshes.size() - 1); } + + if (actualMesh != null) { + actualMesh.setTexture(texture); + } else { + mesh.setTexture(texture); + } + + model.addTexture(texture); + model.markNeedsUpdate(); } catch (Throwable ex) { ex.printStackTrace(); } }); } else { - // 无 GL:尝试内存构造 - Texture memTex = tryCreateTextureFromImageMemory(img, name + "_tex"); + Texture memTex = MeshTextureUtil.tryCreateTextureFromImageMemory(img, name + "_tex"); if (memTex != null) { mesh.setTexture(memTex); model.addTexture(memTex); model.markNeedsUpdate(); - } else { - System.err.println("未找到可用的 GL 上下文,也无法创建内存纹理: " + f.getAbsolutePath()); } } reloadFromModel(); selectPart(part); + thumbnailManager.generateThumbnail(part); } catch (Exception ex) { JOptionPane.showMessageDialog(this, "创建带贴图图层失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); - ex.printStackTrace(); + } + } + + public void endDragOperation() { + if (isDragging && draggedPart != null && dragStartPosition != null) { + Vector2f endPosition = draggedPart.getPosition(); + if (!endPosition.equals(dragStartPosition)) { + recordDragOperation(draggedPart, dragStartPosition, endPosition); + } + isDragging = false; + draggedPart = null; + dragStartPosition = null; + } + } + + private void recordDragOperation(ModelPart part, Vector2f startPos, Vector2f endPos) { + OperationHistoryManager manager = OperationHistoryManager.getInstance(); + if (manager != null) { + manager.recordOperation("DRAG_PART", part, startPos, endPos); } } @@ -726,993 +828,18 @@ public class ModelLayerPanel extends JPanel { BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); ModelPart part = model.createPart(name); - Mesh2D mesh = createQuadForImage(img, name + "_mesh"); + Mesh2D mesh = MeshTextureUtil.createQuadForImage(img, name + "_mesh"); part.addMesh(mesh); - if (renderPanel != null) { - renderPanel.getGlContextManager().executeInGLContext(() -> { - try { - Texture tex = createTextureFromBufferedImageInGL(img, name + "_tex"); - if (tex != null) { - mesh.setTexture(tex); - model.addTexture(tex); - } - } catch (Exception e) { - e.printStackTrace(); - } - }); - } else { - Texture memTex = tryCreateTextureFromImageMemory(img, name + "_tex"); - if (memTex != null) { - mesh.setTexture(memTex); - model.addTexture(memTex); - } + Texture memTex = MeshTextureUtil.tryCreateTextureFromImageMemory(img, name + "_tex"); + if (memTex != null) { + mesh.setTexture(memTex); + model.addTexture(memTex); } model.markNeedsUpdate(); reloadFromModel(); selectPart(part); + thumbnailManager.generateThumbnail(part); } - - private void bindTextureToSelectedPart() { - ModelPart sel = layerList.getSelectedValue(); - if (sel == null) return; - - JFileChooser chooser = new JFileChooser(); - int r = chooser.showOpenDialog(this); - if (r != JFileChooser.APPROVE_OPTION) return; - File f = chooser.getSelectedFile(); - try { - BufferedImage img = null; - try { - img = ImageIO.read(f); - } catch (Exception ignored) { - } - - // 获取第一个 mesh - Mesh2D targetMesh = null; - try { - Method getMeshes = sel.getClass().getMethod("getMeshes"); - Object list = getMeshes.invoke(sel); - if (list instanceof List meshes) { - if (!meshes.isEmpty() && meshes.get(0) instanceof Mesh2D) { - targetMesh = (Mesh2D) meshes.get(0); - } - } - } catch (Exception ignored) { - } - - if (targetMesh == null) { - if (img == null) { - img = new BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB); - } - targetMesh = createQuadForImage(img, sel.getName() + "_mesh"); - try { - sel.getClass().getMethod("addMesh", Mesh2D.class).invoke(sel, targetMesh); - } catch (Exception ex) { - ex.printStackTrace(); - } - } - - final Mesh2D meshToBind = targetMesh; - final String filePath = f.getAbsolutePath(); - final String texName = sel.getName() + "_tex"; - - if (renderPanel != null) { - renderPanel.getGlContextManager().executeInGLContext(() -> { - try { - Texture texture = Texture.createFromFile(texName, filePath); - if (texture != null) { - meshToBind.setTexture(texture); - model.addTexture(texture); - model.markNeedsUpdate(); - } else { - System.err.println("Texture.createFromFile 返回 null: " + filePath); - } - } catch (Throwable ex) { - ex.printStackTrace(); - } - }); - } else { - if (img == null) img = ImageIO.read(f); - Texture mem = tryCreateTextureFromImageMemory(img, texName); - if (mem != null) { - meshToBind.setTexture(mem); - model.addTexture(mem); - model.markNeedsUpdate(); - } else { - System.err.println("无法在无 GL 上下文中创建纹理: " + filePath); - } - } - - reloadFromModel(); - selectPart(sel); - } catch (Exception ex) { - JOptionPane.showMessageDialog(this, "绑定贴图失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); - ex.printStackTrace(); - } - } - - // ============== 辅助:Mesh/Texture 创建 ============== - - private Mesh2D createQuadForImage(BufferedImage img, String meshName) { - float w = img.getWidth(); - float h = img.getHeight(); - - try { - Method m = Mesh2D.class.getMethod("createQuad", String.class, float.class, float.class); - Object o = m.invoke(null, meshName, w, h); - if (o instanceof Mesh2D mesh) { - // 对基础四边形进行细分以增加顶点密度 - return subdivideMeshForLiquify(mesh, 3); // 3级细分 - } - } catch (Exception ignored) { - } - - try { - // 创建高密度网格(细分网格) - return createSubdividedQuad(meshName, w, h, 3); // 3级细分 - } catch (Exception ex) { - ex.printStackTrace(); - } - - throw new RuntimeException("无法创建 Mesh2D(没有合适的工厂或构造函数)"); - } - - /** - * 创建细分四边形网格以支持更好的液化效果 - */ - private Mesh2D createSubdividedQuad(String name, float width, float height, int subdivisionLevel) { - // 计算细分后的网格参数 - int segments = (int) Math.pow(2, subdivisionLevel); // 每边分段数 - int vertexCount = (segments + 1) * (segments + 1); // 顶点总数 - int triangleCount = segments * segments * 2; // 三角形总数 - - float[] vertices = new float[vertexCount * 2]; - float[] uvs = new float[vertexCount * 2]; - int[] indices = new int[triangleCount * 3]; - - float halfW = width / 2f; - float halfH = height / 2f; - - // 生成顶点和UV坐标 - int vertexIndex = 0; - for (int y = 0; y <= segments; y++) { - for (int x = 0; x <= segments; x++) { - // 顶点坐标(从中心点开始) - float xPos = -halfW + (x * width) / segments; - float yPos = -halfH + (y * height) / segments; - - vertices[vertexIndex * 2] = xPos; - vertices[vertexIndex * 2 + 1] = yPos; - - // UV坐标 - uvs[vertexIndex * 2] = (float) x / segments; - uvs[vertexIndex * 2 + 1] = 1f - (float) y / segments; // 翻转V坐标 - - vertexIndex++; - } - } - - // 生成三角形索引 - int index = 0; - for (int y = 0; y < segments; y++) { - for (int x = 0; x < segments; x++) { - int topLeft = y * (segments + 1) + x; - int topRight = topLeft + 1; - int bottomLeft = (y + 1) * (segments + 1) + x; - int bottomRight = bottomLeft + 1; - - // 第一个三角形 (topLeft -> topRight -> bottomLeft) - indices[index++] = topLeft; - indices[index++] = topRight; - indices[index++] = bottomLeft; - - // 第二个三角形 (topRight -> bottomRight -> bottomLeft) - indices[index++] = topRight; - indices[index++] = bottomRight; - indices[index++] = bottomLeft; - } - } - - // 使用反射创建Mesh2D实例 - try { - Constructor cons = null; - for (Constructor c : Mesh2D.class.getDeclaredConstructors()) { - Class[] params = c.getParameterTypes(); - if (params.length >= 4 && params[0] == String.class) { - cons = c; - break; - } - } - if (cons != null) { - cons.setAccessible(true); - Object meshObj = cons.newInstance(name, vertices, uvs, indices); - if (meshObj instanceof Mesh2D mesh) { - - // 设置合适的pivot(中心点) - mesh.setPivot(0, 0); - if (mesh.getOriginalPivot() != null) { - mesh.setOriginalPivot(new Vector2f(0, 0)); - } - - return mesh; - } - } - } catch (Exception e) { - e.printStackTrace(); - } - - throw new RuntimeException("无法创建细分网格"); - } - - /** - * 对现有网格进行细分以增加顶点密度 - */ - private Mesh2D subdivideMeshForLiquify(Mesh2D originalMesh, int subdivisionLevel) { - if (subdivisionLevel <= 0) return originalMesh; - - try { - // 获取原始网格数据 - float[] origVertices = originalMesh.getVertices(); - float[] origUVs = originalMesh.getUVs(); - int[] origIndices = originalMesh.getIndices(); - - // 简单的循环细分算法 - List newVertices = new ArrayList<>(); - List newUVs = new ArrayList<>(); - List newIndices = new ArrayList<>(); - - // 添加原始顶点 - for (int i = 0; i < origVertices.length / 2; i++) { - newVertices.add(new Vector2f(origVertices[i * 2], origVertices[i * 2 + 1])); - newUVs.add(new Vector2f(origUVs[i * 2], origUVs[i * 2 + 1])); - } - - // 对每个三角形进行细分 - for (int i = 0; i < origIndices.length; i += 3) { - int i1 = origIndices[i]; - int i2 = origIndices[i + 1]; - int i3 = origIndices[i + 2]; - - Vector2f v1 = newVertices.get(i1); - Vector2f v2 = newVertices.get(i2); - Vector2f v3 = newVertices.get(i3); - - Vector2f uv1 = newUVs.get(i1); - Vector2f uv2 = newUVs.get(i2); - Vector2f uv3 = newUVs.get(i3); - - // 计算边的中点 - Vector2f mid12 = new Vector2f(v1).add(v2).mul(0.5f); - Vector2f mid23 = new Vector2f(v2).add(v3).mul(0.5f); - Vector2f mid31 = new Vector2f(v3).add(v1).mul(0.5f); - - Vector2f uvMid12 = new Vector2f(uv1).add(uv2).mul(0.5f); - Vector2f uvMid23 = new Vector2f(uv2).add(uv3).mul(0.5f); - Vector2f uvMid31 = new Vector2f(uv3).add(uv1).mul(0.5f); - - // 添加新顶点 - int mid12Idx = newVertices.size(); - newVertices.add(mid12); - newUVs.add(uvMid12); - - int mid23Idx = newVertices.size(); - newVertices.add(mid23); - newUVs.add(uvMid23); - - int mid31Idx = newVertices.size(); - newVertices.add(mid31); - newUVs.add(uvMid31); - - // 创建4个小三角形 - // 三角形1: v1, mid12, mid31 - newIndices.add(i1); - newIndices.add(mid12Idx); - newIndices.add(mid31Idx); - - // 三角形2: v2, mid23, mid12 - newIndices.add(i2); - newIndices.add(mid23Idx); - newIndices.add(mid12Idx); - - // 三角形3: v3, mid31, mid23 - newIndices.add(i3); - newIndices.add(mid31Idx); - newIndices.add(mid23Idx); - - // 三角形4: mid12, mid23, mid31 - newIndices.add(mid12Idx); - newIndices.add(mid23Idx); - newIndices.add(mid31Idx); - } - - // 转换回数组 - float[] finalVertices = new float[newVertices.size() * 2]; - float[] finalUVs = new float[newUVs.size() * 2]; - int[] finalIndices = new int[newIndices.size()]; - - for (int i = 0; i < newVertices.size(); i++) { - finalVertices[i * 2] = newVertices.get(i).x; - finalVertices[i * 2 + 1] = newVertices.get(i).y; - - finalUVs[i * 2] = newUVs.get(i).x; - finalUVs[i * 2 + 1] = newUVs.get(i).y; - } - - for (int i = 0; i < newIndices.size(); i++) { - finalIndices[i] = newIndices.get(i); - } - - // 创建新的细分网格 - Mesh2D subdividedMesh = originalMesh.copy(); - subdividedMesh.setMeshData(finalVertices, finalUVs, finalIndices); - - // 递归细分直到达到指定级别 - if (subdivisionLevel > 1) { - return subdivideMeshForLiquify(subdividedMesh, subdivisionLevel - 1); - } - - return subdividedMesh; - - } catch (Exception e) { - e.printStackTrace(); - return originalMesh; // 如果细分失败,返回原始网格 - } - } - - /** - * 根据图像尺寸智能计算细分级别 - */ - private int calculateOptimalSubdivisionLevel(float width, float height) { - float area = width * height; - - // 根据面积决定细分级别 - if (area < 10000) { // 小图像 - return 2; - } else if (area < 50000) { // 中等图像 - return 3; - } else if (area < 200000) { // 大图像 - return 4; - } else { // 超大图像 - return 5; - } - } - - /** - * 在 GL 上下文中创建并上传 Texture(返回已上传的 Texture) - * 该方法仅在 renderPanel 可用时被调用(renderPanel.executeInGLContext) - */ - private Texture createTextureFromBufferedImageInGL(BufferedImage img, String texName) { - if (renderPanel == null) throw new IllegalStateException("需要 renderPanel 才能在 GL 上下文创建纹理"); - - try { - return renderPanel.getGlContextManager().executeInGLContext(() -> { - // 静态工厂尝试 - try { - Method factory = findStaticMethod(Texture.class, "createFromBufferedImage", BufferedImage.class); - if (factory == null) - factory = findStaticMethod(Texture.class, "createFromImage", BufferedImage.class); - if (factory != null) { - Object texObj = factory.invoke(null, img); - if (texObj instanceof Texture) { - tryCallTextureUpload((Texture) texObj); - return (Texture) texObj; - } - } - } catch (Throwable ignored) { - } - - // 构造 ByteBuffer 并尝试构造器 - try { - int w = img.getWidth(); - int h = img.getHeight(); - ByteBuffer buf = imageToRGBAByteBuffer(img); - Constructor suit = null; - for (Constructor c : Texture.class.getDeclaredConstructors()) { - Class[] ps = c.getParameterTypes(); - if (ps.length >= 4 && ps[0] == String.class) { - suit = c; - break; - } - } - if (suit != null) { - suit.setAccessible(true); - Object texObj = null; - Class[] ps = suit.getParameterTypes(); - if (ps.length >= 5 && ps[3].getSimpleName().toLowerCase().contains("format")) { - Object formatEnum = null; - try { - Class formatCls = null; - for (Class inner : Texture.class.getDeclaredClasses()) { - if (inner.getSimpleName().toLowerCase().contains("format")) { - formatCls = inner; - break; - } - } - if (formatCls != null) { - for (Field f : formatCls.getFields()) { - if (f.getName().toUpperCase().contains("RGBA")) { - formatEnum = f.get(null); - break; - } - } - } - } catch (Throwable ignored) { - } - if (formatEnum != null) { - try { - texObj = suit.newInstance(texName, w, h, formatEnum, buf); - } catch (Exception ignored) { - } - } - } - if (texObj == null) { - try { - texObj = suit.newInstance(texName, img.getWidth(), img.getHeight(), buf); - } catch (Exception ignored) { - } - } - if (texObj instanceof Texture) { - tryCallTextureUpload((Texture) texObj); - return (Texture) texObj; - } - } - } catch (Throwable t) { - t.printStackTrace(); - } - - throw new RuntimeException("无法在 GL 上下文中创建 Texture(缺少兼容的构造器/工厂)"); - }).get(); - } catch (Exception e) { - throw new RuntimeException("创建 GL 纹理失败: " + e.getMessage(), e); - } - } - - private Texture tryCreateTextureFromImageMemory(BufferedImage img, String texName) { - try { - int w = img.getWidth(); - int h = img.getHeight(); - ByteBuffer buf = imageToRGBAByteBuffer(img); - - Constructor suit = null; - for (Constructor c : Texture.class.getDeclaredConstructors()) { - Class[] ps = c.getParameterTypes(); - if (ps.length >= 4 && ps[0] == String.class) { - suit = c; - break; - } - } - if (suit != null) { - suit.setAccessible(true); - Object texObj = null; - Class[] ps = suit.getParameterTypes(); - if (ps.length >= 5 && ps[3].getSimpleName().toLowerCase().contains("format")) { - Object formatEnum = null; - try { - Class formatCls = null; - for (Class inner : Texture.class.getDeclaredClasses()) { - if (inner.getSimpleName().toLowerCase().contains("format")) { - formatCls = inner; - break; - } - } - if (formatCls != null) { - for (Field f : formatCls.getFields()) { - if (f.getName().toUpperCase().contains("RGBA")) { - formatEnum = f.get(null); - break; - } - } - } - } catch (Throwable ignored) { - } - if (formatEnum != null) { - try { - texObj = suit.newInstance(texName, w, h, formatEnum, buf); - } catch (Throwable ignored) { - } - } - } - if (texObj == null) { - try { - texObj = suit.newInstance(texName, w, h, buf); - } catch (Throwable ignored) { - } - } - if (texObj instanceof Texture) return (Texture) texObj; - } - } catch (Throwable t) { - t.printStackTrace(); - } - return null; - } - - private ByteBuffer imageToRGBAByteBuffer(BufferedImage img) { - final int w = img.getWidth(); - final int h = img.getHeight(); - final int[] pixels = new int[w * h]; - img.getRGB(0, 0, w, h, pixels, 0, w); - ByteBuffer buffer = MemoryUtil.memAlloc(w * h * 4).order(ByteOrder.nativeOrder()); - for (int y = 0; y < h; y++) { - for (int x = 0; x < w; x++) { - int argb = pixels[y * w + x]; - int a = (argb >> 24) & 0xFF; - int r = (argb >> 16) & 0xFF; - int g = (argb >> 8) & 0xFF; - int b = (argb) & 0xFF; - buffer.put((byte) r); - buffer.put((byte) g); - buffer.put((byte) b); - buffer.put((byte) a); - } - } - buffer.flip(); - return buffer; - } - - private void tryCallTextureUpload(Texture tex) { - if (tex == null) return; - String[] candidates = new String[]{"upload", "uploadToGPU", "initGL", "initTexture", "createGLTexture", "bind"}; - for (String name : candidates) { - try { - Method m = tex.getClass().getMethod(name); - if (m != null) { - m.invoke(tex); - return; - } - } catch (NoSuchMethodException ignored) { - } catch (Throwable t) { - t.printStackTrace(); - } - } - } - - private static Method findStaticMethod(Class cls, String name, Class param) { - try { - Method m = cls.getMethod(name, param); - if (Modifier.isStatic(m.getModifiers())) return m; - } catch (Exception ignored) { - } - try { - Method m = cls.getDeclaredMethod(name, param); - m.setAccessible(true); - if (Modifier.isStatic(m.getModifiers())) return m; - } catch (Exception ignored) { - } - return null; - } - - // ============== 列表操作(核心:保持 model.parts 与 listModel 一致) ============== - - private void onRemoveLayer() { - ModelPart sel = layerList.getSelectedValue(); - if (sel == null) return; - int r = JOptionPane.showConfirmDialog(this, "确认删除图层:" + sel.getName() + " ?", "确认删除", JOptionPane.YES_NO_OPTION); - if (r != JOptionPane.YES_OPTION) return; - - try { - List parts = getModelPartsList(); - if (parts != null) parts.remove(sel); - Map partMap = getModelPartMap(); - if (partMap != null) partMap.remove(sel.getName()); - try { - ModelPart root = model.getRootPart(); - if (root != null && root == sel) { - List remaining = getModelPartsList(); - if (remaining != null && !remaining.isEmpty()) { - model.setRootPart(remaining.get(0)); - } else { - model.setRootPart(null); - } - } - } catch (Exception ignored) { - } - model.markNeedsUpdate(); - reloadFromModel(); - } catch (Exception ex) { - JOptionPane.showMessageDialog(this, "删除失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); - ex.printStackTrace(); - } - } - - private void moveSelectedUp() { - int idx = layerList.getSelectedIndex(); - if (idx <= 0) return; - performVisualReorder(idx, idx - 1); - } - - private void moveSelectedDown() { - int idx = layerList.getSelectedIndex(); - if (idx < 0 || idx >= listModel.getSize() - 1) return; - performVisualReorder(idx, idx + 1); - } - - /** - * 重新加载列表(列表显示顺序为从上到下) - * 列表中的顺序与用户看到的顺序一致(listModel[0] = 最上层) - */ - private void reloadFromModel() { - // 记录对象选中以便恢复 - ModelPart selected = layerList.getSelectedValue(); - - listModel.clear(); - if (model == null) return; - try { - List parts = model.getParts(); - // 我们希望列表从上到下显示,因此把 model.parts 反序加入 listModel - if (parts != null) { - for (int i = parts.size() - 1; i >= 0; i--) { - listModel.addElement(parts.get(i)); - } - } - } catch (Exception ex) { - ex.printStackTrace(); - } - - // 恢复选中(按对象引用) - if (selected != null) { - for (int i = 0; i < listModel.getSize(); i++) { - if (listModel.get(i) == selected) { - layerList.setSelectedIndex(i); - break; - } - } - } - } - - /** - * 执行视觉(列表)层级的重排:先在 visualList 上进行操作,然后把 model.parts 重建为 visualList 的反序, - * 保证 model.parts 与 UI 显示顺序一致(rendering 与 UI 保持一致)。 - * - * @param visualFrom 源 visual 索引(listModel) - * @param visualTo 目标 visual 索引(listModel) - */ - private void performVisualReorder(int visualFrom, int visualTo) { - if (model == null) return; - try { - int size = listModel.getSize(); - if (visualFrom < 0 || visualFrom >= size) return; - if (visualTo < 0) visualTo = 0; - if (visualTo > size - 1) visualTo = size - 1; - - ModelPart moved = listModel.get(visualFrom); - - // 如果是拖拽操作,设置拖拽状态 - if (!isDragging) { - isDragging = true; - draggedPart = moved; - dragStartPosition = new Vector2f(moved.getPosition()); - } - - // 构造新的视觉顺序(arraylist) - List visual = new ArrayList<>(size); - for (int i = 0; i < size; i++) visual.add(listModel.get(i)); - - // 移动元素 - moved = visual.remove(visualFrom); - visual.add(visualTo, moved); - - // 更新 listModel(程序性更新,期间设置 ignoreSliderEvents 防止滑块回写) - ignoreSliderEvents = true; - try { - listModel.clear(); - for (ModelPart p : visual) listModel.addElement(p); - } finally { - ignoreSliderEvents = false; - } - - // 根据视觉顺序重建 model.parts(model.parts = reverse(visual)) - List newModelParts = new ArrayList<>(visual.size()); - for (int i = visual.size() - 1; i >= 0; i--) newModelParts.add(visual.get(i)); - // 替换 model.parts 字段(通过反射) - replaceModelPartsList(newModelParts); - - model.markNeedsUpdate(); - - // 恢复选中:按对象引用找到索引 - selectPart(moved); - } catch (Exception ex) { - ex.printStackTrace(); - } - } - - private void endDragOperation() { - if (isDragging && draggedPart != null && dragStartPosition != null) { - // 记录拖拽操作 - Vector2f endPosition = draggedPart.getPosition(); - if (!endPosition.equals(dragStartPosition)) { - // 只有在位置确实发生变化时才记录操作 - recordDragOperation(draggedPart, dragStartPosition, endPosition); - } - - // 重置拖拽状态 - isDragging = false; - draggedPart = null; - dragStartPosition = null; - } - } - - private void recordDragOperation(ModelPart part, Vector2f startPos, Vector2f endPos) { - OperationHistoryManager manager = OperationHistoryManager.getInstance(); - if (manager != null) { - manager.recordOperation("DRAG_PART", part, startPos, endPos); - } - } - - // ============== 反射读写 Model2D 内部 ============== - - @SuppressWarnings("unchecked") - private List getModelPartsList() { - if (model == null) return null; - try { - Field partsField = model.getClass().getDeclaredField("parts"); - partsField.setAccessible(true); - Object o = partsField.get(model); - if (o instanceof List) return (List) o; - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } - - @SuppressWarnings("unchecked") - private Map getModelPartMap() { - if (model == null) return null; - try { - Field mapField = model.getClass().getDeclaredField("partMap"); - mapField.setAccessible(true); - Object o = mapField.get(model); - if (o instanceof Map) return (Map) o; - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } - - /** - * 用新的 parts 列表替换 model.parts(保持同一 List 对象或直接 set) - */ - private void replaceModelPartsList(List newParts) { - if (model == null) return; - try { - Field partsField = model.getClass().getDeclaredField("parts"); - partsField.setAccessible(true); - Object old = partsField.get(model); - if (old instanceof @SuppressWarnings("rawtypes")List rawList) { - rawList.clear(); - rawList.addAll(newParts); - } else { - partsField.set(model, newParts); - } - } catch (Exception e) { - e.printStackTrace(); - } - } - - // ============== 列表渲染/拖拽辅助 ============== - - private class LayerCellRenderer extends JPanel implements ListCellRenderer { - private final JCheckBox visibleBox = new JCheckBox(); - private final JLabel nameLabel = new JLabel(); - private final JLabel opacityLabel = new JLabel(); - - LayerCellRenderer() { - setLayout(new BorderLayout(6, 6)); - JPanel left = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0)); - left.setOpaque(false); - visibleBox.setOpaque(false); - left.add(visibleBox); - left.add(nameLabel); - add(left, BorderLayout.CENTER); - add(opacityLabel, BorderLayout.EAST); - - addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - int idx = layerList.locationToIndex(e.getPoint()); - if (idx >= 0) { - ModelPart part = listModel.get(idx); - Rectangle cbBounds = new Rectangle(0, 0, 20, getHeight()); - if (cbBounds.contains(e.getPoint())) { - boolean newVis = !part.isVisible(); - part.setVisible(newVis); - if (model != null) model.markNeedsUpdate(); - reloadFromModel(); - } else { - layerList.setSelectedIndex(idx); - } - } - } - }); - } - - @Override - public Component getListCellRendererComponent(JList list, ModelPart value, int index, boolean isSelected, boolean cellHasFocus) { - nameLabel.setText(value.getName()); - try { - Method gm = value.getClass().getMethod("getOpacity"); - Object v = gm.invoke(value); - if (v instanceof Float) opacityLabel.setText(((int) (((Float) v) * 100)) + "%"); - } catch (Exception ex) { - try { - Field f = value.getClass().getDeclaredField("opacity"); - f.setAccessible(true); - Object v = f.get(value); - if (v instanceof Float) opacityLabel.setText(Math.round((Float) v * 100) + "%"); - else opacityLabel.setText(""); - } catch (Exception ignored) { - opacityLabel.setText(""); - } - } - visibleBox.setSelected(value.isVisible()); - - if (isSelected) { - setBackground(list.getSelectionBackground()); - setForeground(list.getSelectionForeground()); - nameLabel.setForeground(list.getSelectionForeground()); - } else { - setBackground(list.getBackground()); - setForeground(list.getForeground()); - nameLabel.setForeground(list.getForeground()); - } - setOpaque(true); - setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3)); - return this; - } - } - - private class LayerReorderTransferHandler extends TransferHandler { - @Override - protected Transferable createTransferable(JComponent c) { - int src = layerList.getSelectedIndex(); - if (src < 0) return null; - return new StringSelection(Integer.toString(src)); - } - - @Override - public int getSourceActions(JComponent c) { - return MOVE; - } - - @Override - public boolean canImport(TransferSupport support) { - return support.isDrop() && support.isDataFlavorSupported(java.awt.datatransfer.DataFlavor.stringFlavor); - } - - @Override - public boolean importData(TransferSupport support) { - if (!canImport(support)) return false; - try { - javax.swing.JList.DropLocation dl = (javax.swing.JList.DropLocation) support.getDropLocation(); - int dropIndex = dl.getIndex(); - String s = (String) support.getTransferable().getTransferData(java.awt.datatransfer.DataFlavor.stringFlavor); - int srcIdx = Integer.parseInt(s); - if (srcIdx == dropIndex || srcIdx + 1 == dropIndex) return false; - performVisualReorder(srcIdx, dropIndex); - - // 拖拽结束时记录操作 - endDragOperation(); - return true; - } catch (Exception ex) { - ex.printStackTrace(); - } - return false; - } - - @Override - protected void exportDone(JComponent source, Transferable data, int action) { - // 如果拖拽被取消,也结束拖拽操作 - if (action == TransferHandler.NONE) { - endDragOperation(); - } - super.exportDone(source, data, action); - } - } - - // ============== 小工具 ============== - - private void selectPart(ModelPart part) { - if (part == null) return; - for (int i = 0; i < listModel.getSize(); i++) { - if (listModel.get(i) == part) { - layerList.setSelectedIndex(i); - layerList.ensureIndexIsVisible(i); - return; - } - } - } - - private void renamePart(ModelPart part, String newName) { - if (part == null) return; - try { - try { - Method m = part.getClass().getMethod("setName", String.class); - m.invoke(part, newName); - } catch (NoSuchMethodException ex) { - Field nameField = part.getClass().getDeclaredField("name"); - nameField.setAccessible(true); - String oldName = (String) nameField.get(part); - nameField.set(part, newName); - Map partMap = getModelPartMap(); - if (partMap != null) { - partMap.remove(oldName); - partMap.put(newName, part); - } - } - } catch (Exception e) { - e.printStackTrace(); - } - } - - private void debugPrintModelState() { - if (model == null) { - System.out.println("模型为 null"); - return; - } - try { - List parts = model.getParts(); - System.out.println("=== Model Parts: " + (parts == null ? 0 : parts.size()) + " ==="); - if (parts != null) { - for (int i = 0; i < parts.size(); i++) { - ModelPart p = parts.get(i); - String name = p.getName(); - boolean visible = true; - float px = 0, py = 0; - try { - Method gm = p.getClass().getMethod("getPivotX"); - Method gm2 = p.getClass().getMethod("getPivotY"); - px = ((Number) gm.invoke(p)).floatValue(); - py = ((Number) gm2.invoke(p)).floatValue(); - } catch (Exception ignored) { - try { - Field fx = p.getClass().getDeclaredField("pivotX"); - Field fy = p.getClass().getDeclaredField("pivotY"); - fx.setAccessible(true); - fy.setAccessible(true); - px = ((Number) fx.get(p)).floatValue(); - py = ((Number) fy.get(p)).floatValue(); - } catch (Exception ignored2) { - } - } - try { - Method vm = p.getClass().getMethod("isVisible"); - visible = (Boolean) vm.invoke(p); - } catch (Exception ignored) { - } - System.out.printf("Part[%d] name=%s visible=%s pivot=(%.1f, %.1f)%n", i, name, visible, px, py); - - // meshes - try { - Method gmsh = p.getClass().getMethod("getMeshes"); - Object list = gmsh.invoke(p); - if (list instanceof List meshes) { - System.out.println(" meshes count = " + meshes.size()); - for (int m = 0; m < meshes.size(); m++) { - Object mesh = meshes.get(m); - Object tex = null; - try { - Method gtex = mesh.getClass().getMethod("getTexture"); - tex = gtex.invoke(mesh); - } catch (Exception e) { - try { - Field f = mesh.getClass().getDeclaredField("texture"); - f.setAccessible(true); - tex = f.get(mesh); - } catch (Exception ignored) { - } - } - System.out.println(" mesh[" + m + "] texture = " + (tex == null ? "null" : tex.getClass().getSimpleName())); - } - } - } catch (Exception e) { - e.printStackTrace(); - } - } - } - } catch (Exception e) { - e.printStackTrace(); - } - } - } \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/GLContextManager.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/GLContextManager.java index 24a7f2c..98443ff 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/GLContextManager.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/GLContextManager.java @@ -493,14 +493,21 @@ public class GLContextManager { contextReady.thenRun(() -> { try { - glTaskQueue.put(() -> { + boolean offered = glTaskQueue.offer(() -> { try { T result = task.call(); future.complete(result); } catch (Exception e) { future.completeExceptionally(e); } - }); + }, 5, TimeUnit.SECONDS); + + if (!offered) { + future.completeExceptionally(new TimeoutException("任务队列已满,无法在5秒内添加任务")); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + future.completeExceptionally(new IllegalStateException("任务提交被中断", e)); } catch (Exception e) { future.completeExceptionally(e); } diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/LayerOperationManager.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/LayerOperationManager.java new file mode 100644 index 0000000..633030d --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/LayerOperationManager.java @@ -0,0 +1,70 @@ +package com.chuangzhou.vivid2D.render.awt.manager; + +import com.chuangzhou.vivid2D.render.model.Model2D; +import com.chuangzhou.vivid2D.render.model.ModelPart; +import org.joml.Vector2f; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class LayerOperationManager { + private final Model2D model; + + public LayerOperationManager(Model2D model) { + this.model = model; + } + + public void addLayer(String name) { + model.createPart(name); + model.markNeedsUpdate(); + } + + public void removeLayer(ModelPart part) { + if (part == null) return; + + List parts = model.getParts(); + if (parts != null) parts.remove(part); + + Map partMap = model.getPartMap(); + if (partMap != null) partMap.remove(part.getName()); + + model.markNeedsUpdate(); + } + + public void moveLayer(List visualOrder) { + List newModelParts = new ArrayList<>(visualOrder.size()); + for (int i = visualOrder.size() - 1; i >= 0; i--) { + newModelParts.add(visualOrder.get(i)); + } + replaceModelPartsList(newModelParts); + model.markNeedsUpdate(); + } + + public void setLayerOpacity(ModelPart part, float opacity) { + part.setOpacity(opacity); + model.markNeedsUpdate(); + } + + public void setLayerVisibility(ModelPart part, boolean visible) { + part.setVisible(visible); + model.markNeedsUpdate(); + } + + private void replaceModelPartsList(List newParts) { + if (model == null) return; + try { + java.lang.reflect.Field partsField = model.getClass().getDeclaredField("parts"); + partsField.setAccessible(true); + Object old = partsField.get(model); + if (old instanceof List) { + ((List) old).clear(); + ((List) old).addAll(newParts); + } else { + partsField.set(model, newParts); + } + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ThumbnailManager.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ThumbnailManager.java new file mode 100644 index 0000000..df95a87 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ThumbnailManager.java @@ -0,0 +1,244 @@ +package com.chuangzhou.vivid2D.render.awt.manager; + +import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel; +import com.chuangzhou.vivid2D.render.model.ModelPart; +import com.chuangzhou.vivid2D.render.model.util.Mesh2D; +import com.chuangzhou.vivid2D.render.model.util.Texture; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ThumbnailManager { + private static final int THUMBNAIL_WIDTH = 48; + private static final int THUMBNAIL_HEIGHT = 48; + + private final Map thumbnailCache = new HashMap<>(); + private ModelRenderPanel renderPanel; + + public ThumbnailManager(ModelRenderPanel renderPanel) { + this.renderPanel = renderPanel; + } + + public BufferedImage getThumbnail(ModelPart part) { + return thumbnailCache.get(part); + } + + public void generateThumbnail(ModelPart part) { + if (renderPanel == null) return; + + try { + BufferedImage thumbnail = renderPanel.getGlContextManager() + .executeInGLContext(() -> renderPartThumbnail(part)) + .get(); + + if (thumbnail != null) { + thumbnailCache.put(part, thumbnail); + } + } catch (Exception e) { + thumbnailCache.put(part, createDefaultThumbnail()); + } + } + + public void removeThumbnail(ModelPart part) { + thumbnailCache.remove(part); + } + + public void clearCache() { + thumbnailCache.clear(); + } + + /** + * 渲染单个部件的缩略图 + */ + private BufferedImage renderPartThumbnail(ModelPart part) { + if (renderPanel == null) return createDefaultThumbnail(); + + try { + return createThumbnailForPart(part); + } catch (Exception e) { + e.printStackTrace(); + return createDefaultThumbnail(); + } + } + + private BufferedImage createThumbnailForPart(ModelPart part) { + BufferedImage thumbnail = new BufferedImage(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = thumbnail.createGraphics(); + + // 设置抗锯齿 + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + + // 绘制背景 + g2d.setColor(new Color(40, 40, 40)); + g2d.fillRect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT); + + try { + // 尝试获取部件的纹理 + Texture texture = null; + List meshes = part.getMeshes(); + if (meshes != null && !meshes.isEmpty()) { + for (Mesh2D mesh : meshes) { + texture = mesh.getTexture(); + if (texture != null) break; + } + } + + if (texture != null && !texture.isDisposed()) { + // 获取纹理的 BufferedImage + BufferedImage textureImage = textureToBufferedImage(texture); + if (textureImage != null) { + // 计算缩放比例以保持宽高比 + int imgWidth = textureImage.getWidth(); + int imgHeight = textureImage.getHeight(); + + if (imgWidth > 0 && imgHeight > 0) { + float scale = Math.min( + (float)(THUMBNAIL_WIDTH - 8) / imgWidth, + (float)(THUMBNAIL_HEIGHT - 8) / imgHeight + ); + + int scaledWidth = (int)(imgWidth * scale); + int scaledHeight = (int)(imgHeight * scale); + int x = (THUMBNAIL_WIDTH - scaledWidth) / 2; + int y = (THUMBNAIL_HEIGHT - scaledHeight) / 2; + + // 绘制纹理图片 + g2d.drawImage(textureImage, x, y, scaledWidth, scaledHeight, null); + + // 绘制边框 + g2d.setColor(Color.WHITE); + g2d.drawRect(x, y, scaledWidth - 1, scaledHeight - 1); + } + } + } + + } catch (Exception e) { + System.err.println("生成缩略图失败: " + part.getName() + " - " + e.getMessage()); + } + + // 如果部件不可见,绘制红色斜线覆盖 + if (!part.isVisible()) { + g2d.setColor(new Color(255, 0, 0, 128)); // 半透明红色 + g2d.setStroke(new BasicStroke(3)); + g2d.drawLine(2, 2, THUMBNAIL_WIDTH - 2, THUMBNAIL_HEIGHT - 2); + g2d.drawLine(THUMBNAIL_WIDTH - 2, 2, 2, THUMBNAIL_HEIGHT - 2); + } + + g2d.dispose(); + return thumbnail; + } + + /** + * 将Texture转换为BufferedImage + */ + private BufferedImage textureToBufferedImage(Texture texture) { + try { + // 确保纹理有像素数据缓存 + texture.ensurePixelDataCached(); + + if (!texture.hasPixelData()) { + System.err.println("纹理没有像素数据: " + texture.getName()); + return null; + } + + byte[] pixelData = texture.getPixelData(); + if (pixelData == null || pixelData.length == 0) { + return null; + } + + int width = texture.getWidth(); + int height = texture.getHeight(); + Texture.TextureFormat format = texture.getFormat(); + int components = format.getComponents(); + + // 创建BufferedImage + BufferedImage image; + switch (components) { + case 1: // 单通道 + image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY); + break; + case 3: // RGB + image = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR); + break; + case 4: // RGBA + image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + break; + default: + System.err.println("不支持的纹理格式组件数量: " + components); + return null; + } + + // 将像素数据复制到BufferedImage,同时翻转垂直方向 + if (components == 4) { + // RGBA格式 - 垂直翻转 + int[] pixels = new int[width * height]; + for (int y = 0; y < height; y++) { + int srcY = height - 1 - y; // 翻转Y坐标 + for (int x = 0; x < width; x++) { + int srcIndex = (srcY * width + x) * 4; + int dstIndex = y * width + x; + + int r = pixelData[srcIndex] & 0xFF; + int g = pixelData[srcIndex + 1] & 0xFF; + int b = pixelData[srcIndex + 2] & 0xFF; + int a = pixelData[srcIndex + 3] & 0xFF; + pixels[dstIndex] = (a << 24) | (r << 16) | (g << 8) | b; + } + } + image.setRGB(0, 0, width, height, pixels, 0, width); + } else if (components == 3) { + // RGB格式 - 垂直翻转 + for (int y = 0; y < height; y++) { + int srcY = height - 1 - y; // 翻转Y坐标 + for (int x = 0; x < width; x++) { + int srcIndex = (srcY * width + x) * 3; + int r = pixelData[srcIndex] & 0xFF; + int g = pixelData[srcIndex + 1] & 0xFF; + int b = pixelData[srcIndex + 2] & 0xFF; + int rgb = (r << 16) | (g << 8) | b; + image.setRGB(x, y, rgb); + } + } + } else if (components == 1) { + // 单通道格式 - 垂直翻转 + for (int y = 0; y < height; y++) { + int srcY = height - 1 - y; // 翻转Y坐标 + for (int x = 0; x < width; x++) { + int srcIndex = srcY * width + x; + int gray = pixelData[srcIndex] & 0xFF; + int rgb = (gray << 16) | (gray << 8) | gray; + image.setRGB(x, y, rgb); + } + } + } + + return image; + + } catch (Exception e) { + System.err.println("转换纹理到BufferedImage失败: " + texture.getName() + " - " + e.getMessage()); + e.printStackTrace(); + return null; + } + } + + private BufferedImage createDefaultThumbnail() { + BufferedImage thumbnail = new BufferedImage(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = thumbnail.createGraphics(); + + g2d.setColor(new Color(60, 60, 60)); + g2d.fillRect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT); + + g2d.setColor(Color.GRAY); + g2d.drawRect(2, 2, THUMBNAIL_WIDTH - 5, THUMBNAIL_HEIGHT - 5); + + g2d.setColor(Color.WHITE); + g2d.drawString("?", THUMBNAIL_WIDTH/2 - 4, THUMBNAIL_HEIGHT/2 + 4); + + g2d.dispose(); + return thumbnail; + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/SelectionTool.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/SelectionTool.java index be1c59b..6fd719b 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/SelectionTool.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/SelectionTool.java @@ -5,6 +5,7 @@ import com.chuangzhou.vivid2D.render.model.Model2D; import com.chuangzhou.vivid2D.render.model.ModelPart; import com.chuangzhou.vivid2D.render.model.util.Mesh2D; import com.chuangzhou.vivid2D.render.model.util.BoundingBox; +import com.chuangzhou.vivid2D.render.model.util.SecondaryVertex; import org.joml.Vector2f; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -89,6 +90,7 @@ public class SelectionTool extends Tool { dragStartY = modelY; // 获取边界框和中心点 + // 确保使用世界坐标下的边界框 BoundingBox bounds = targetMeshForHandle.getBounds(); renderPanel.getCameraManagement().getRotationCenter().set( (bounds.getMinX() + bounds.getMaxX()) / 2.0f, @@ -112,6 +114,7 @@ public class SelectionTool extends Tool { dragStartY = modelY; // 记录初始中心点位置 + // 确保使用世界坐标下的边界框 BoundingBox bounds = targetMeshForHandle.getBounds(); renderPanel.getCameraManagement().getRotationCenter().set( (bounds.getMinX() + bounds.getMaxX()) / 2.0f, @@ -127,6 +130,7 @@ public class SelectionTool extends Tool { dragStartX = modelX; dragStartY = modelY; + // 确保使用世界坐标下的边界框 BoundingBox bounds = targetMeshForHandle.getBounds(); resizeStartWidth = bounds.getWidth(); resizeStartHeight = bounds.getHeight(); @@ -242,7 +246,6 @@ public class SelectionTool extends Tool { handleResizeDrag(modelX, modelY); break; } - } catch (Exception ex) { logger.error("选择工具处理鼠标拖拽时出错", ex); } @@ -447,7 +450,7 @@ public class SelectionTool extends Tool { part.setScale(currentScale.x * relScaleX, currentScale.y * relScaleY); // 同步缩放该部件下的所有网格的二级顶点 - syncSecondaryVerticesScaleForPart(part, relScaleX, relScaleY); + //syncSecondaryVerticesScaleForPart(part, relScaleX, relScaleY); } // 更新拖拽起始位置和初始尺寸 @@ -467,10 +470,32 @@ public class SelectionTool extends Tool { if (meshes == null) return; for (Mesh2D mesh : meshes) { - if (mesh != null && mesh.getSecondaryVertexCount() > 0) { - mesh.moveSecondaryVertices(deltaX, deltaY); + if (mesh != null && mesh.isVisible() && mesh.getSecondaryVertexCount() > 0) { + + List secondaryVertices = mesh.getSecondaryVertices(); + if (secondaryVertices != null) { + + // 遍历所有顶点,逐个调用 moveSecondaryVertex + for (SecondaryVertex vertex : secondaryVertices) { + + // 【修正 1:避免双重平移和状态冲突】 + // 仅对未锁定/未固定的顶点执行局部坐标平移。 + // 锁定的顶点不应被工具的同步逻辑移动,它们应该随 ModelPart 的世界变换移动。 + if (!vertex.isLocked() && !vertex.isPinned()) { + + // 计算顶点的新局部坐标 (position + delta) + float newX = vertex.getPosition().x + deltaX; + float newY = vertex.getPosition().y + deltaY; + + // 使用 moveSecondaryVertex 方法 + mesh.moveSecondaryVertex(vertex, newX, newY); + // 注意:mesh.moveSecondaryVertex 内部会触发形变计算和 markDirty + } + } + } } } + part.setPosition(part.getPosition()); // 递归处理子部件 for (ModelPart child : part.getChildren()) { @@ -526,10 +551,10 @@ public class SelectionTool extends Tool { return ModelRenderPanel.DragMode.NONE; } + // 统一使用世界坐标边界框 BoundingBox bounds; Vector2f center; - // 多选状态下使用多选边界框 if (targetMesh.isInMultiSelection()) { bounds = targetMesh.getMultiSelectionBounds(); center = bounds.getCenter(); @@ -566,6 +591,7 @@ public class SelectionTool extends Tool { float expandedMaxX = maxX + borderThickness; float expandedMaxY = maxY + borderThickness; + // 如果不在扩展边界内,直接返回NONE if (result == ModelRenderPanel.DragMode.NONE) { if (modelX < expandedMinX || modelX > expandedMaxX || modelY < expandedMinY || modelY > expandedMaxY) { @@ -600,9 +626,13 @@ public class SelectionTool extends Tool { } } + logger.debug("手柄检测: 位置({}, {}), 边界[{}, {}, {}, {}], 结果: {}", + modelX, modelY, minX, minY, maxX, maxY, result); + return result; } + // 辅助方法:检查点是否在中心点、旋转手柄、角点区域内 private boolean isPointInCenterHandle(float x, float y, float centerX, float centerY, float handleSize) { return Math.abs(x - centerX) <= handleSize && Math.abs(y - centerY) <= handleSize; @@ -732,14 +762,11 @@ public class SelectionTool extends Tool { */ public void setSelectedMesh(Mesh2D mesh) { renderPanel.getGlContextManager().executeInGLContext(() -> { - // 清除之前选中的所有网格 for (Mesh2D selectedMesh : selectedMeshes) { selectedMesh.setSelected(false); selectedMesh.clearMultiSelection(); } selectedMeshes.clear(); - - // 设置新的选中网格 if (mesh != null) { mesh.setSelected(true); selectedMeshes.add(mesh); @@ -760,7 +787,14 @@ public class SelectionTool extends Tool { mesh.setSelected(true); selectedMeshes.add(mesh); lastSelectedMesh = mesh; + ModelPart part = findPartByMesh(mesh); + if (part != null) { + part.updateMeshVertices(); + } updateMultiSelectionInMeshes(); + for (ModelPart selectedPart : getSelectedParts()) { + selectedPart.updateMeshVertices(); + } } }); } @@ -787,13 +821,34 @@ public class SelectionTool extends Tool { */ public void clearSelectedMeshes() { renderPanel.getGlContextManager().executeInGLContext(() -> { + // 记录所有受影响的 ModelPart,以便在清除选中状态后更新它们的网格顶点 + // Use a Set to collect unique ModelParts + Set affectedParts = new HashSet<>(); + + // 1. 清除网格的选中状态并收集父 ModelPart for (Mesh2D mesh : selectedMeshes) { mesh.setSelected(false); mesh.setSuspension(false); mesh.clearMultiSelection(); + + // 查找并记录父 ModelPart + ModelPart part = findPartByMesh(mesh); + if (part != null) { + affectedParts.add(part); + } } + + // 2. 清除选择集 selectedMeshes.clear(); lastSelectedMesh = null; + + // 3. 强制更新所有受影响 ModelPart 的网格顶点。 + // 这将确保网格的渲染顶点(renderVertices)从 ModelPart 的世界变换中重新同步, + // 从而修复多选结束后位置重置的错误。 + for (ModelPart part : affectedParts) { + // 关键的修复:强制 ModelPart 重新同步其网格顶点,恢复正确的世界位置 + part.updateMeshVertices(); + } }); } @@ -805,14 +860,19 @@ public class SelectionTool extends Tool { Model2D model = renderPanel.getModel(); if (model == null) return; - // 清除之前的选择 + // 1. 清除之前的选择 for (Mesh2D mesh : selectedMeshes) { mesh.setSelected(false); mesh.clearMultiSelection(); + // 在清除前获取并更新 ModelPart 也是一个好习惯,确保状态一致性 + ModelPart part = findPartByMesh(mesh); + if (part != null) { + part.updateMeshVertices(); + } } selectedMeshes.clear(); - // 获取所有网格并选中 + // 2. 获取所有网格并选中 List allMeshes = getAllMeshesFromModel(model); for (Mesh2D mesh : allMeshes) { if (mesh.isVisible()) { @@ -821,12 +881,16 @@ public class SelectionTool extends Tool { } } - // 设置最后选中的网格 + // 3. 设置最后选中的网格 if (!selectedMeshes.isEmpty()) { lastSelectedMesh = selectedMeshes.iterator().next(); } updateMultiSelectionInMeshes(); + + for (ModelPart selectedPart : getSelectedParts()) { + selectedPart.updateMeshVertices(); + } }); } diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/VertexDeformationTool.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/VertexDeformationTool.java index 7c294ef..061d35a 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/VertexDeformationTool.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/VertexDeformationTool.java @@ -6,6 +6,7 @@ import com.chuangzhou.vivid2D.render.model.ModelPart; import com.chuangzhou.vivid2D.render.model.util.Mesh2D; import com.chuangzhou.vivid2D.render.model.util.SecondaryVertex; import com.chuangzhou.vivid2D.render.model.util.BoundingBox; +import org.joml.Vector2f; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,7 +29,9 @@ public class VertexDeformationTool extends Tool { private static final float VERTEX_TOLERANCE = 8.0f; private ModelRenderPanel.DragMode currentDragMode = ModelRenderPanel.DragMode.NONE; private float dragStartX, dragStartY; - + private float savedCameraRotation = Float.NaN; + private Vector2f savedCameraScale = new Vector2f(1,1); + private boolean cameraStateSaved = false; public VertexDeformationTool(ModelRenderPanel renderPanel) { super(renderPanel, "顶点变形工具", "通过二级顶点对网格进行精细变形操作"); } @@ -47,17 +50,28 @@ public class VertexDeformationTool extends Tool { targetMesh = findFirstVisibleMesh(); } + // 记录并重置相机(如果可用)到默认状态:旋转 = 0,缩放 = 1 + try { + if (renderPanel.getCameraManagement() != null) { + // 备份 + savedCameraRotation = targetMesh.getModelPart().getRotation(); + savedCameraScale = targetMesh.getModelPart().getScale(); + cameraStateSaved = true; + + // 设置为默认 + targetMesh.getModelPart().setRotation(0f); + targetMesh.getModelPart().setScale(1f); + } + } catch (Throwable t) { + // 若没有这些方法或发生异常则记录但不阻塞工具激活 + logger.debug("无法备份/设置相机状态: {}", t.getMessage()); + } + if (targetMesh != null) { // 显示二级顶点 associatedRanderTools.setAlgorithmEnabled("showSecondaryVertices", true); targetMesh.setShowSecondaryVertices(true); targetMesh.setRenderVertices(true); - - // 如果没有二级顶点,创建默认的四个角点 - //if (targetMesh.getSecondaryVertexCount() == 0) { - // createDefaultSecondaryVertices(); - //} - logger.info("激活顶点变形工具: {}", targetMesh.getName()); } else { logger.warn("没有找到可用的网格用于顶点变形"); @@ -69,10 +83,26 @@ public class VertexDeformationTool extends Tool { if (!isActive) return; isActive = false; + + // 恢复相机之前的旋转/缩放状态(如果已保存) + try { + if (cameraStateSaved && renderPanel.getCameraManagement() != null) { + targetMesh.getModelPart().setRotation(savedCameraRotation); + targetMesh.getModelPart().setScale(savedCameraScale); + } + } catch (Throwable t) { + logger.debug("无法恢复相机状态: {}", t.getMessage()); + } finally { + cameraStateSaved = false; + savedCameraRotation = Float.NaN; + savedCameraScale = new Vector2f(1,1); + } + if (targetMesh != null) { associatedRanderTools.setAlgorithmEnabled("showSecondaryVertices", false); targetMesh.setShowSecondaryVertices(false); targetMesh.setRenderVertices(false); + targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition()); } targetMesh = null; selectedVertex = null; @@ -124,8 +154,6 @@ public class VertexDeformationTool extends Tool { if (!isActive || selectedVertex == null) return; if (currentDragMode == ModelRenderPanel.DragMode.MOVE_SECONDARY_VERTEX) { - float deltaX = modelX - dragStartX; - float deltaY = modelY - dragStartY; // 移动顶点到新位置 selectedVertex.setPosition(modelX, modelY); @@ -135,8 +163,7 @@ public class VertexDeformationTool extends Tool { dragStartY = modelY; // 标记网格为脏状态,需要重新计算边界等 - targetMesh.markDirty(); - targetMesh.updateBounds(); + targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition()); // 强制重绘 renderPanel.repaint(); @@ -247,8 +274,7 @@ public class VertexDeformationTool extends Tool { logger.info("创建二级顶点: ID={}, 位置({}, {}), UV({}, {})", newVertex.getId(), x, y, u, v); - // 标记网格为脏状态 - targetMesh.markDirty(); + targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition()); renderPanel.repaint(); } else { logger.warn("创建二级顶点失败"); @@ -272,7 +298,7 @@ public class VertexDeformationTool extends Tool { logger.info("删除二级顶点: ID={}", vertex.getId()); // 标记网格为脏状态 - targetMesh.markDirty(); + targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition()); renderPanel.repaint(); } else { logger.warn("删除二级顶点失败: ID={}", vertex.getId()); diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/MeshTextureUtil.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/MeshTextureUtil.java new file mode 100644 index 0000000..b33bc5a --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/MeshTextureUtil.java @@ -0,0 +1,267 @@ +package com.chuangzhou.vivid2D.render.awt.util; + +import com.chuangzhou.vivid2D.render.model.util.Mesh2D; +import com.chuangzhou.vivid2D.render.model.util.Texture; +import org.joml.Vector2f; +import org.lwjgl.system.MemoryUtil; + +import java.awt.image.BufferedImage; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.List; + +public class MeshTextureUtil { + + public static Mesh2D createQuadForImage(BufferedImage img, String meshName) { + float w = img.getWidth(); + float h = img.getHeight(); + + try { + Mesh2D o = Mesh2D.createQuad(meshName, w, h); + return subdivideMeshForLiquify(o, 3); + } catch (Exception ignored) { + } + + try { + return createSubdividedQuad(meshName, w, h, 3); + } catch (Exception ex) { + ex.printStackTrace(); + } + + throw new RuntimeException("无法创建 Mesh2D"); + } + + private static Mesh2D createSubdividedQuad(String name, float width, float height, int subdivisionLevel) { + int segments = (int) Math.pow(2, subdivisionLevel); + int vertexCount = (segments + 1) * (segments + 1); + int triangleCount = segments * segments * 2; + + float[] vertices = new float[vertexCount * 2]; + float[] uvs = new float[vertexCount * 2]; + int[] indices = new int[triangleCount * 3]; + + float halfW = width / 2f; + float halfH = height / 2f; + int vertexIndex = 0; + for (int y = 0; y <= segments; y++) { + for (int x = 0; x <= segments; x++) { + float xPos = -halfW + (x * width) / segments; + float yPos = -halfH + (y * height) / segments; + + vertices[vertexIndex * 2] = xPos; + vertices[vertexIndex * 2 + 1] = yPos; + + uvs[vertexIndex * 2] = (float) x / segments; + uvs[vertexIndex * 2 + 1] = 1f - (float) y / segments; + + vertexIndex++; + } + } + + int index = 0; + for (int y = 0; y < segments; y++) { + for (int x = 0; x < segments; x++) { + int topLeft = y * (segments + 1) + x; + int topRight = topLeft + 1; + int bottomLeft = (y + 1) * (segments + 1) + x; + int bottomRight = bottomLeft + 1; + + indices[index++] = topLeft; + indices[index++] = topRight; + indices[index++] = bottomLeft; + + indices[index++] = topRight; + indices[index++] = bottomRight; + indices[index++] = bottomLeft; + } + } + + try { + Constructor cons = null; + for (Constructor c : Mesh2D.class.getDeclaredConstructors()) { + Class[] params = c.getParameterTypes(); + if (params.length >= 4 && params[0] == String.class) { + cons = c; + break; + } + } + if (cons != null) { + cons.setAccessible(true); + Object meshObj = cons.newInstance(name, vertices, uvs, indices); + if (meshObj instanceof Mesh2D mesh) { + mesh.setPivot(0, 0); + if (mesh.getOriginalPivot() != null) { + mesh.setOriginalPivot(new Vector2f(0, 0)); + } + return mesh; + } + } + } catch (Exception e) { + e.printStackTrace(); + } + throw new RuntimeException("无法创建细分网格"); + } + + private static Mesh2D subdivideMeshForLiquify(Mesh2D originalMesh, int subdivisionLevel) { + if (subdivisionLevel <= 0) return originalMesh; + + try { + float[] origVertices = originalMesh.getVertices(); + float[] origUVs = originalMesh.getUVs(); + int[] origIndices = originalMesh.getIndices(); + List newVertices = new ArrayList<>(); + List newUVs = new ArrayList<>(); + List newIndices = new ArrayList<>(); + + for (int i = 0; i < origVertices.length / 2; i++) { + newVertices.add(new Vector2f(origVertices[i * 2], origVertices[i * 2 + 1])); + newUVs.add(new Vector2f(origUVs[i * 2], origUVs[i * 2 + 1])); + } + + for (int i = 0; i < origIndices.length; i += 3) { + int i1 = origIndices[i]; + int i2 = origIndices[i + 1]; + int i3 = origIndices[i + 2]; + Vector2f v1 = newVertices.get(i1); + Vector2f v2 = newVertices.get(i2); + Vector2f v3 = newVertices.get(i3); + Vector2f uv1 = newUVs.get(i1); + Vector2f uv2 = newUVs.get(i2); + Vector2f uv3 = newUVs.get(i3); + + Vector2f mid12 = new Vector2f(v1).add(v2).mul(0.5f); + Vector2f mid23 = new Vector2f(v2).add(v3).mul(0.5f); + Vector2f mid31 = new Vector2f(v3).add(v1).mul(0.5f); + Vector2f uvMid12 = new Vector2f(uv1).add(uv2).mul(0.5f); + Vector2f uvMid23 = new Vector2f(uv2).add(uv3).mul(0.5f); + Vector2f uvMid31 = new Vector2f(uv3).add(uv1).mul(0.5f); + + int mid12Idx = newVertices.size(); + newVertices.add(mid12); + newUVs.add(uvMid12); + int mid23Idx = newVertices.size(); + newVertices.add(mid23); + newUVs.add(uvMid23); + int mid31Idx = newVertices.size(); + newVertices.add(mid31); + newUVs.add(uvMid31); + + newIndices.add(i1); newIndices.add(mid12Idx); newIndices.add(mid31Idx); + newIndices.add(i2); newIndices.add(mid23Idx); newIndices.add(mid12Idx); + newIndices.add(i3); newIndices.add(mid31Idx); newIndices.add(mid23Idx); + newIndices.add(mid12Idx); newIndices.add(mid23Idx); newIndices.add(mid31Idx); + } + + float[] finalVertices = new float[newVertices.size() * 2]; + float[] finalUVs = new float[newUVs.size() * 2]; + int[] finalIndices = new int[newIndices.size()]; + + for (int i = 0; i < newVertices.size(); i++) { + finalVertices[i * 2] = newVertices.get(i).x; + finalVertices[i * 2 + 1] = newVertices.get(i).y; + finalUVs[i * 2] = newUVs.get(i).x; + finalUVs[i * 2 + 1] = newUVs.get(i).y; + } + + for (int i = 0; i < newIndices.size(); i++) { + finalIndices[i] = newIndices.get(i); + } + + Mesh2D subdividedMesh = originalMesh.copy(); + subdividedMesh.setMeshData(finalVertices, finalUVs, finalIndices); + + if (subdivisionLevel > 1) { + return subdivideMeshForLiquify(subdividedMesh, subdivisionLevel - 1); + } + return subdividedMesh; + } catch (Exception e) { + e.printStackTrace(); + return originalMesh; + } + } + + public static Texture tryCreateTextureFromImageMemory(BufferedImage img, String texName) { + try { + int w = img.getWidth(); + int h = img.getHeight(); + ByteBuffer buf = imageToRGBAByteBuffer(img); + + Constructor suit = null; + for (Constructor c : Texture.class.getDeclaredConstructors()) { + Class[] ps = c.getParameterTypes(); + if (ps.length >= 4 && ps[0] == String.class) { + suit = c; + break; + } + } + if (suit != null) { + suit.setAccessible(true); + Object texObj = null; + Class[] ps = suit.getParameterTypes(); + if (ps.length >= 5 && ps[3].getSimpleName().toLowerCase().contains("format")) { + Object formatEnum = null; + try { + Class formatCls = null; + for (Class inner : Texture.class.getDeclaredClasses()) { + if (inner.getSimpleName().toLowerCase().contains("format")) { + formatCls = inner; + break; + } + } + if (formatCls != null) { + for (Field f : formatCls.getFields()) { + if (f.getName().toUpperCase().contains("RGBA")) { + formatEnum = f.get(null); + break; + } + } + } + } catch (Throwable ignored) { + } + if (formatEnum != null) { + try { + texObj = suit.newInstance(texName, w, h, formatEnum, buf); + } catch (Throwable ignored) { + } + } + } + if (texObj == null) { + try { + texObj = suit.newInstance(texName, w, h, buf); + } catch (Throwable ignored) { + } + } + if (texObj instanceof Texture) return (Texture) texObj; + } + } catch (Throwable t) { + t.printStackTrace(); + } + return null; + } + + private static ByteBuffer imageToRGBAByteBuffer(BufferedImage img) { + final int w = img.getWidth(); + final int h = img.getHeight(); + final int[] pixels = new int[w * h]; + img.getRGB(0, 0, w, h, pixels, 0, w); + ByteBuffer buffer = MemoryUtil.memAlloc(w * h * 4).order(ByteOrder.nativeOrder()); + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + int argb = pixels[y * w + x]; + int a = (argb >> 24) & 0xFF; + int r = (argb >> 16) & 0xFF; + int g = (argb >> 8) & 0xFF; + int b = (argb) & 0xFF; + buffer.put((byte) r); + buffer.put((byte) g); + buffer.put((byte) b); + buffer.put((byte) a); + } + } + buffer.flip(); + return buffer; + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/PSDImporter.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/PSDImporter.java new file mode 100644 index 0000000..ee78c6d --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/PSDImporter.java @@ -0,0 +1,187 @@ +package com.chuangzhou.vivid2D.render.awt.util; + +import com.chuangzhou.vivid2D.render.awt.ModelLayerPanel; +import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel; +import com.chuangzhou.vivid2D.render.awt.util.PsdParser; +import com.chuangzhou.vivid2D.render.model.Model2D; +import com.chuangzhou.vivid2D.render.model.ModelPart; +import com.chuangzhou.vivid2D.render.model.util.Mesh2D; +import com.chuangzhou.vivid2D.render.model.util.Texture; + +import javax.swing.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class PSDImporter { + private final Model2D model; + private final ModelRenderPanel renderPanel; + private final ModelLayerPanel layerPanel; + + public PSDImporter(Model2D model, ModelRenderPanel renderPanel, ModelLayerPanel layerPanel) { + this.model = model; + this.renderPanel = renderPanel; + this.layerPanel = layerPanel; + } + + public void importPSDFile(File psdFile) { + try { + PsdParser.PSDImportResult result = PsdParser.parsePSDFile(psdFile); + if (result != null && !result.layers.isEmpty()) { + int choice = JOptionPane.showConfirmDialog(null, + String.format("PSD文件包含 %d 个图层,是否全部导入?", result.layers.size()), + "导入PSD图层", JOptionPane.YES_NO_OPTION); + + if (choice == JOptionPane.YES_OPTION) { + importPSDLayers(result); + } + } + } catch (Exception ex) { + JOptionPane.showMessageDialog(null, + "解析PSD文件失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); + } + } + + private void importPSDLayers(PsdParser.PSDImportResult result) { + if (renderPanel != null) { + renderPanel.getGlContextManager().executeInGLContext(() -> { + try { + List createdParts = createPartsFromPSDLayers(result.layers); + SwingUtilities.invokeLater(() -> notifyImportComplete(createdParts)); + } catch (Exception e) { + SwingUtilities.invokeLater(() -> + showError("导入PSD图层失败: " + e.getMessage())); + } + }); + } else { + List createdParts = createPartsFromPSDLayers(result.layers); + notifyImportComplete(createdParts); + } + } + + private List createPartsFromPSDLayers(List layers) { + List createdParts = new ArrayList<>(); + for (PsdParser.PSDLayerInfo layerInfo : layers) { + ModelPart part = createPartFromPSDLayer(layerInfo); + if (part != null) { + createdParts.add(part); + } + } + return createdParts; + } + + private ModelPart createPartFromPSDLayer(PsdParser.PSDLayerInfo layerInfo) { + try { + System.out.println("正在创建PSD图层: " + layerInfo.name + " [" + + layerInfo.width + "x" + layerInfo.height + "]" + "[x=" + layerInfo.x + ",y=" + layerInfo.y + "]"); + + // 确保部件名唯一,避免覆盖已有部件导致"合并成一个图层"的问题 + String uniqueName = ensureUniquePartName(layerInfo.name); + + // 创建部件 + ModelPart part = model.createPart(uniqueName); + if (part == null) { + System.err.println("创建部件失败: " + uniqueName); + return null; + } + + // 如果 model 有 partMap,更新映射(防止老实现以 name 为 key 覆盖或冲突) + try { + Map partMap = layerPanel.getModelPartMap(); + if (partMap != null) { + partMap.put(uniqueName, part); + } + } catch (Exception ignored) { + } + + part.setVisible(layerInfo.visible); + + // 设置不透明度(优先使用公开方法) + try { + part.setOpacity(layerInfo.opacity); + } catch (Throwable t) { + // 如果没有公开方法,尝试通过反射备用(保持兼容) + try { + Field f = part.getClass().getDeclaredField("opacity"); + f.setAccessible(true); + f.setFloat(part, layerInfo.opacity); + } catch (Throwable ignored) { + System.err.println("设置不透明度失败: " + uniqueName); + } + } + part.setPosition(layerInfo.x, layerInfo.y); + + // 创建网格(使用唯一 mesh 名避免工厂复用同一实例) + long uniq = System.nanoTime(); + Mesh2D mesh = MeshTextureUtil.createQuadForImage(layerInfo.image, uniqueName + "_mesh_" + uniq); + + // 把 mesh 加入 part(注意部分实现可能复制或包装 mesh) + part.addMesh(mesh); + + // 创建纹理(使用唯一名称,防止按 name 在内部被复用或覆盖) + String texName = uniqueName + "_tex_" + uniq; + Texture texture = layerPanel.createTextureFromBufferedImage(layerInfo.image, texName); + try { + List partMeshes = part.getMeshes(); + Mesh2D actualMesh = null; + if (partMeshes != null && !partMeshes.isEmpty()) { + actualMesh = partMeshes.get(partMeshes.size() - 1); + } + + if (actualMesh != null) { + actualMesh.setTexture(texture); + } else { + mesh.setTexture(texture); + } + model.addTexture(texture); + model.markNeedsUpdate(); + } catch (Throwable e) { + System.err.println("在绑定纹理到 mesh 时出错: " + uniqueName + " - " + e.getMessage()); + e.printStackTrace(); + } + SwingUtilities.invokeLater(() -> { + try { + layerPanel.reloadFromModel(); + } catch (Throwable ignored) { + } + try { + if (renderPanel != null) renderPanel.repaint(); + } catch (Throwable ignored) { + } + }); + + return part; + + } catch (Exception e) { + System.err.println("创建PSD图层部件失败: " + layerInfo.name + " - " + e.getMessage()); + e.printStackTrace(); + return null; + } + } + + private String ensureUniquePartName(String baseName) { + if (model == null) return baseName; + Map partMap = layerPanel.getModelPartMap(); + if (partMap == null) return baseName; + String name = baseName; + int idx = 1; + while (partMap.containsKey(name)) { + name = baseName + "_" + idx++; + } + return name; + } + + private void notifyImportComplete(List createdParts) { + if (model != null) { + model.markNeedsUpdate(); + } + // 通知监听器导入完成 + } + + private void showError(String message) { + JOptionPane.showMessageDialog(null, message, "错误", JOptionPane.ERROR_MESSAGE); + } +} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/renderer/LayerCellRenderer.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/renderer/LayerCellRenderer.java new file mode 100644 index 0000000..94d3a08 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/renderer/LayerCellRenderer.java @@ -0,0 +1,120 @@ +package com.chuangzhou.vivid2D.render.awt.util.renderer; + +import com.chuangzhou.vivid2D.render.awt.ModelLayerPanel; +import com.chuangzhou.vivid2D.render.awt.manager.ThumbnailManager; +import com.chuangzhou.vivid2D.render.model.ModelPart; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.image.BufferedImage; + +public class LayerCellRenderer extends JPanel implements ListCellRenderer { + private static final int THUMBNAIL_WIDTH = 48; + private static final int THUMBNAIL_HEIGHT = 48; + + private final JCheckBox visibleBox = new JCheckBox(); + private final JLabel nameLabel = new JLabel(); + private final JLabel opacityLabel = new JLabel(); + private final JLabel thumbnailLabel = new JLabel(); + + private final ModelLayerPanel layerPanel; + private final ThumbnailManager thumbnailManager; + + public LayerCellRenderer(ModelLayerPanel layerPanel, ThumbnailManager thumbnailManager) { + this.layerPanel = layerPanel; + this.thumbnailManager = thumbnailManager; + initComponents(); + } + + private void initComponents() { + setLayout(new BorderLayout(6, 6)); + + // 左侧:缩略图 + thumbnailLabel.setPreferredSize(new Dimension(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)); + thumbnailLabel.setOpaque(true); + thumbnailLabel.setBackground(new Color(60, 60, 60)); + thumbnailLabel.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1)); + + // 中间:可见性复选框和名称 + JPanel centerPanel = new JPanel(new BorderLayout(4, 0)); + centerPanel.setOpaque(false); + + JPanel leftPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0)); + leftPanel.setOpaque(false); + visibleBox.setOpaque(false); + leftPanel.add(visibleBox); + leftPanel.add(nameLabel); + + centerPanel.add(leftPanel, BorderLayout.CENTER); + centerPanel.add(opacityLabel, BorderLayout.EAST); + + add(thumbnailLabel, BorderLayout.WEST); + add(centerPanel, BorderLayout.CENTER); + } + + public void attachMouseListener(JList layerList, javax.swing.ListModel listModel) { + addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + int idx = layerList.locationToIndex(e.getPoint()); + if (idx >= 0) { + ModelPart part = listModel.getElementAt(idx); + Rectangle cbBounds = visibleBox.getBounds(); + // 调整点击区域检测,考虑缩略图的存在 + cbBounds.x += thumbnailLabel.getWidth() + 6; // 缩略图宽度 + 间距 + if (cbBounds.contains(e.getPoint())) { + boolean newVis = !part.isVisible(); + part.setVisible(newVis); + if (layerPanel.getModel() != null) { + layerPanel.getModel().markNeedsUpdate(); + } + layerPanel.reloadFromModel(); + layerPanel.refreshCurrentThumbnail(); + } else { + layerList.setSelectedIndex(idx); + } + } + } + }); + } + + @Override + public Component getListCellRendererComponent(JList list, ModelPart value, + int index, boolean isSelected, boolean cellHasFocus) { + nameLabel.setText(value.getName()); + opacityLabel.setText(((int) (value.getOpacity() * 100)) + "%"); + visibleBox.setSelected(value.isVisible()); + + // 设置缩略图 + BufferedImage thumbnail = thumbnailManager.getThumbnail(value); + if (thumbnail != null) { + thumbnailLabel.setIcon(new ImageIcon(thumbnail)); + } else { + thumbnailLabel.setIcon(null); + // 如果没有缩略图,生成一个 + SwingUtilities.invokeLater(() -> { + thumbnailManager.generateThumbnail(value); + list.repaint(); + }); + } + + if (isSelected) { + setBackground(list.getSelectionBackground()); + setForeground(list.getSelectionForeground()); + nameLabel.setForeground(list.getSelectionForeground()); + opacityLabel.setForeground(list.getSelectionForeground()); + thumbnailLabel.setBorder(BorderFactory.createLineBorder(list.getSelectionForeground(), 2)); + } else { + setBackground(list.getBackground()); + setForeground(list.getForeground()); + nameLabel.setForeground(list.getForeground()); + opacityLabel.setForeground(list.getForeground()); + thumbnailLabel.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1)); + } + setOpaque(true); + setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3)); + return this; + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/renderer/LayerReorderTransferHandler.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/renderer/LayerReorderTransferHandler.java new file mode 100644 index 0000000..084443d --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/renderer/LayerReorderTransferHandler.java @@ -0,0 +1,68 @@ +package com.chuangzhou.vivid2D.render.awt.util.renderer; + +import com.chuangzhou.vivid2D.render.awt.ModelLayerPanel; + +import javax.swing.*; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.StringSelection; +import java.awt.datatransfer.Transferable; + +public class LayerReorderTransferHandler extends TransferHandler { + private final ModelLayerPanel layerPanel; + + public LayerReorderTransferHandler(ModelLayerPanel layerPanel) { + this.layerPanel = layerPanel; + } + + @Override + protected Transferable createTransferable(JComponent c) { + if (!(c instanceof JList)) return null; + + JList list = (JList) c; + int src = list.getSelectedIndex(); + if (src < 0) return null; + return new StringSelection(Integer.toString(src)); + } + + @Override + public int getSourceActions(JComponent c) { + return MOVE; + } + + @Override + public boolean canImport(TransferSupport support) { + return support.isDrop() && support.isDataFlavorSupported(DataFlavor.stringFlavor); + } + + @Override + public boolean importData(TransferSupport support) { + if (!canImport(support)) return false; + + try { + if (!(support.getComponent() instanceof JList)) return false; + + JList.DropLocation dl = (JList.DropLocation) support.getDropLocation(); + int dropIndex = dl.getIndex(); + + String s = (String) support.getTransferable().getTransferData(DataFlavor.stringFlavor); + int srcIdx = Integer.parseInt(s); + + if (srcIdx == dropIndex || srcIdx + 1 == dropIndex) return false; + + layerPanel.performVisualReorder(srcIdx, dropIndex); + layerPanel.endDragOperation(); + return true; + } catch (Exception ex) { + ex.printStackTrace(); + } + return false; + } + + @Override + protected void exportDone(JComponent source, Transferable data, int action) { + if (action == TransferHandler.NONE) { + layerPanel.endDragOperation(); + } + super.exportDone(source, data, action); + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java b/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java index 5306b55..70d91e4 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java @@ -209,8 +209,12 @@ public class Model2D { return partMap.get(name); } + public Map getPartMap() { + return partMap; + } + public List getParts() { - return Collections.unmodifiableList(parts); + return parts; } // ==================== 参数管理 ==================== diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java index 61dbbb2..794db77 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java @@ -1,10 +1,7 @@ package com.chuangzhou.vivid2D.render.model; import com.chuangzhou.vivid2D.render.awt.util.OperationHistoryGlobal; -import com.chuangzhou.vivid2D.render.model.util.BoundingBox; -import com.chuangzhou.vivid2D.render.model.util.Deformer; -import com.chuangzhou.vivid2D.render.model.util.Mesh2D; -import com.chuangzhou.vivid2D.render.model.util.PuppetPin; +import com.chuangzhou.vivid2D.render.model.util.*; import com.chuangzhou.vivid2D.render.systems.Matrix3fUtils; import org.joml.Matrix3f; import org.joml.Vector2f; @@ -1595,6 +1592,7 @@ public class ModelPart { mesh.setPivot(worldPivot.x, worldPivot.y); } + updateMeshVertices(); triggerEvent("position"); } @@ -1626,56 +1624,72 @@ public class ModelPart { */ private void updateMeshVertices(Mesh2D mesh) { if (mesh == null) return; - - // 获取原始顶点数据(局部坐标) - float[] originalVertices = mesh.getOriginalVertices(); - if (originalVertices == null || originalVertices.length == 0) { - logger.warn("网格 {} 没有原始顶点数据,无法更新变换", mesh.getName()); - return; - } - - // 确保世界变换是最新的 + // 确保 worldTransform 是最新的 if (transformDirty) { updateLocalTransform(); recomputeWorldTransformRecursive(); } - - int vertexCount = originalVertices.length / 2; - - // 应用当前世界变换到每个顶点 - 添加边界检查 - for (int i = 0; i < vertexCount; i++) { - if (i * 2 + 1 >= originalVertices.length) { - logger.warn("顶点索引 {} 超出原始顶点数组范围", i); - continue; - } - - Vector2f localPoint = new Vector2f(originalVertices[i * 2], originalVertices[i * 2 + 1]); - Vector2f worldPoint = Matrix3fUtils.transformPoint(worldTransform, localPoint); - - // 检查目标索引是否有效 - if (i < mesh.getVertexCount()) { - mesh.setVertex(i, worldPoint.x, worldPoint.y); - } else { - logger.warn("顶点索引 {} 超出网格顶点范围 (总顶点数: {})", i, mesh.getVertexCount()); - } - } - - // 同步 mesh 的原始局部 pivot -> 当前世界 pivot + // 1) 让 mesh 自己把局部顶点一次性转换成渲染缓存(世界坐标) + mesh.syncRenderVerticesFromLocal(this.worldTransform); + // 2) 同步 pivot(不改原始局部数据) try { Vector2f origPivot = mesh.getOriginalPivot(); - Vector2f worldPivot = Matrix3fUtils.transformPoint(worldTransform, origPivot); - mesh.setPivot(worldPivot.x, worldPivot.y); + if (origPivot != null) { + Vector2f worldPivot = Matrix3fUtils.transformPoint(this.worldTransform, origPivot); + mesh.setPivot(worldPivot.x, worldPivot.y); + } } catch (Exception e) { logger.warn("更新网格pivot时出错: {}", e.getMessage()); } - - updatePuppetPinsPosition(mesh); - - // 标记网格需要更新 + // 3) 更新木偶控制点显示位置(仅 display/world pos) + //updatePuppetPinsPosition(mesh); + // 4) 更新二级顶点的 worldPosition 缓存(仅 display/world pos,不修改局部变形数据) + updateSecondaryVerticesWorldPosition(mesh); + // 5) 标记 mesh 需要重新渲染(渲染器应使用 mesh.getVerticesForUpload() 来上传 VBO) mesh.markDirty(); mesh.setBakedToWorld(true); } + /** + * 更新二级顶点的原始局部位置与当前局部位置(当 ModelPart 的 worldTransform 改变时调用) + * 保证 SecondaryVertex 在变换后仍然用局部坐标表示(用于变形计算),同时更新 worldPosition 缓存用于显示。 + */ + private void updateSecondaryVerticesWorldPosition(Mesh2D mesh) { + if (mesh == null) return; + List secondaryVertices = mesh.getSecondaryVertices(); + if (secondaryVertices == null || secondaryVertices.isEmpty()) return; + if (transformDirty) { + updateLocalTransform(); + recomputeWorldTransformRecursive(); + } + boolean hasMirror = hasMirrorTransform(this.worldTransform); + for (SecondaryVertex vertex : secondaryVertices) { + Vector2f localPos = vertex.getPosition(); + Vector2f adjustedPos = localPos; + if (hasMirror) { + adjustedPos = new Vector2f(-localPos.x, localPos.y); + } + Vector2f worldPos = Matrix3fUtils.transformPoint(this.worldTransform, adjustedPos); + vertex.setWorldPosition(worldPos); + vertex.setRenderPosition(worldPos.x, worldPos.y); + } + + logger.debug("更新了 {} 个二级顶点的位置(处理镜像:{})", + secondaryVertices.size(), hasMirror); + } + + /** + * 检查变换矩阵是否包含镜像(负缩放) + */ + private boolean hasMirrorTransform(Matrix3f transform) { + // 检查X轴缩放因子的符号 + float scaleX = (float)Math.sqrt(transform.m00 * transform.m00 + transform.m10 * transform.m10); + + // 通过行列式检查镜像 + float determinant = transform.m00 * transform.m11 - transform.m01 * transform.m10; + return determinant < 0; + } + /** * 更新木偶控制点的位置 */ @@ -1733,12 +1747,8 @@ public class ModelPart { Vector2f movedWorldPivot = new Vector2f(oldWorldPivot.x + dx, oldWorldPivot.y + dy); // 将位移后的世界 pivot 逆变换回新的局部坐标系(即新的 originalPivot) Vector2f newLocalOriginalPivot = Matrix3fUtils.transformPointInverse(this.worldTransform, movedWorldPivot); - mesh.setOriginalPivot(newLocalOriginalPivot); mesh.setPivot(movedWorldPivot.x, movedWorldPivot.y); - - // ==================== 新增:同步更新木偶控制点的原始位置 ==================== - updatePuppetPinsOriginalPosition(mesh, oldWorldTransform, dx, dy); } // 更新网格顶点位置 @@ -1805,7 +1815,7 @@ public class ModelPart { return; } - // 原有单选择辑 + // 原有单选择辑 - 修复:确保网格顶点被更新 Matrix3f oldWorldTransform = new Matrix3f(this.worldTransform); this.rotation = radians; markTransformDirty(); @@ -1818,8 +1828,9 @@ public class ModelPart { mesh.setOriginalPivot(newLocalOriginalPivot); mesh.setPivot(Matrix3fUtils.transformPoint(this.worldTransform, newLocalOriginalPivot)); - // ==================== 新增:同步更新木偶控制点的原始位置 ==================== + // 更新木偶控制点和二级顶点 updatePuppetPinsOriginalPositionForTransform(mesh, oldWorldTransform); + updateSecondaryVerticesWorldPosition(mesh); } updateMeshVertices(); @@ -1831,9 +1842,16 @@ public class ModelPart { */ public void rotate(float deltaRadians) { this.rotation += deltaRadians; + Matrix3f oldWorldTransform = new Matrix3f(this.worldTransform); markTransformDirty(); updateLocalTransform(); recomputeWorldTransformRecursive(); + for (Mesh2D mesh : meshes) { + Vector2f oldWorldPivot = Matrix3fUtils.transformPoint(oldWorldTransform, mesh.getOriginalPivot()); + Vector2f newLocalOriginalPivot = Matrix3fUtils.transformPointInverse(this.worldTransform, oldWorldPivot); + mesh.setOriginalPivot(newLocalOriginalPivot); + mesh.setPivot(Matrix3fUtils.transformPoint(this.worldTransform, newLocalOriginalPivot)); + } updateMeshVertices(); triggerEvent("rotation"); } @@ -1851,7 +1869,7 @@ public class ModelPart { return; } - // 原有单选择辑 + // 原有单选择辑 - 修复:确保网格顶点被更新 Matrix3f oldWorldTransform = new Matrix3f(this.worldTransform); this.scaleX = sx; this.scaleY = sy; @@ -1866,8 +1884,9 @@ public class ModelPart { mesh.setOriginalPivot(newLocalOriginalPivot); mesh.setPivot(Matrix3fUtils.transformPoint(this.worldTransform, newLocalOriginalPivot)); - // ==================== 新增:同步更新木偶控制点的原始位置 ==================== + // 更新木偶控制点和二级顶点 updatePuppetPinsOriginalPositionForTransform(mesh, oldWorldTransform); + updateSecondaryVerticesWorldPosition(mesh); } updateMeshVertices(); @@ -1918,6 +1937,7 @@ public class ModelPart { mesh.setOriginalPivot(newLocalOriginalPivot); // 同时更新 mesh 的当前 pivot 到新的世界坐标 mesh.setPivot(Matrix3fUtils.transformPoint(this.worldTransform, newLocalOriginalPivot)); + updateSecondaryVerticesWorldPosition(mesh); } updateMeshVertices(); @@ -1968,29 +1988,33 @@ public class ModelPart { public void addMesh(Mesh2D mesh) { if (mesh == null) return; - // 确保拷贝保留原始的纹理引用(copy() 已处理) - //mesh.setTexture(mesh.getTexture()); mesh.setModelPart(this); - // 确保本节点的 worldTransform 是最新的 recomputeWorldTransformRecursive(); - // 保存拷贝的原始(局部)顶点供后续重算 world 顶点使用 - float[] originalVertices = mesh.getVertices().clone(); - mesh.setOriginalVertices(originalVertices); - // 把 originalPivot 保存在 mesh 中(setMeshData 已经初始化 originalPivot) - // 将每个顶点从本地空间变换到世界空间(烘焙到 world) + // 1. 保存局部顶点到 originalVertices + float[] localVertices = mesh.getVertices().clone(); + mesh.setOriginalVertices(localVertices); + + // 2. 确保 renderVertices 数组已初始化 + // (您需要 Mesh2D.java 中有这个方法) + // mesh.ensureRenderVerticesInitialized(); + + // 3. 计算世界坐标并写入 *renderVertices*,而不是 int vc = mesh.getVertexCount(); for (int i = 0; i < vc; i++) { - Vector2f local = new Vector2f(originalVertices[i * 2], originalVertices[i * 2 + 1]); + Vector2f local = new Vector2f(localVertices[i * 2], localVertices[i * 2 + 1]); Vector2f worldPt = Matrix3fUtils.transformPoint(this.worldTransform, local); - mesh.setVertex(i, worldPt.x, worldPt.y); + + // 错误:mesh.setVertex(i, worldPt.x, worldPt.y); + // 正确: + mesh.setRenderVertex(i, worldPt.x, worldPt.y); // 假设 setRenderVertex 存在 } - // 同步 originalPivot -> world pivot(如果 originalPivot 有意义) + // 4. 同步 pivot try { Vector2f origPivot = mesh.getOriginalPivot(); Vector2f worldPivot = Matrix3fUtils.transformPoint(this.worldTransform, origPivot); - mesh.setPivot(worldPivot.x, worldPivot.y); + mesh.setPivot(worldPivot.x, worldPivot.y); // 现在这个会成功(因为步骤1的修复) } catch (Exception ignored) { } @@ -2193,19 +2217,27 @@ public class ModelPart { * 获取世界空间中的包围盒 */ public BoundingBox getWorldBounds() { - if (boundsDirty) { - updateBounds(); + BoundingBox worldBounds = new BoundingBox(); + + for (Mesh2D mesh : meshes) { + // 确保网格的世界边界是最新的 + BoundingBox meshWorldBounds = mesh.getWorldBounds(); + if (meshWorldBounds != null && meshWorldBounds.isValid()) { + worldBounds.expand(meshWorldBounds); + } } - BoundingBox worldBounds = new BoundingBox(); - for (Mesh2D mesh : meshes) { - BoundingBox meshBounds = mesh.getBounds(); - if (meshBounds != null) { - // 变换到世界空间 - Vector2f min = localToWorld(new Vector2f(meshBounds.getMinX(), meshBounds.getMinY())); - Vector2f max = localToWorld(new Vector2f(meshBounds.getMaxX(), meshBounds.getMaxY())); - worldBounds.expand(min.x, min.y); - worldBounds.expand(max.x, max.y); + // 如果没有有效边界,使用局部边界作为备选 + if (!worldBounds.isValid()) { + for (Mesh2D mesh : meshes) { + BoundingBox meshBounds = mesh.getBounds(); + if (meshBounds != null && meshBounds.isValid()) { + // 变换到世界空间 + Vector2f min = localToWorld(new Vector2f(meshBounds.getMinX(), meshBounds.getMinY())); + Vector2f max = localToWorld(new Vector2f(meshBounds.getMaxX(), meshBounds.getMaxY())); + worldBounds.expand(min.x, min.y); + worldBounds.expand(max.x, max.y); + } } } diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java index 4fdf5e6..7a94ae0 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java @@ -5,6 +5,7 @@ import com.chuangzhou.vivid2D.render.MultiSelectionBoxRenderer; import com.chuangzhou.vivid2D.render.TextRenderer; import com.chuangzhou.vivid2D.render.model.ModelPart; import com.chuangzhou.vivid2D.render.model.util.manager.RanderToolsManager; +import com.chuangzhou.vivid2D.render.systems.Matrix3fUtils; import com.chuangzhou.vivid2D.render.systems.RenderSystem; import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder; import com.chuangzhou.vivid2D.render.systems.buffer.Tesselator; @@ -23,10 +24,7 @@ import org.slf4j.LoggerFactory; import java.nio.FloatBuffer; import java.nio.IntBuffer; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; /** * 2D网格类,用于存储和管理2D模型的几何数据 @@ -43,6 +41,8 @@ public class Mesh2D { private int[] indices; // 索引数据 private float[] originalVertices; // 原始顶点数据(用于变形恢复) private ModelPart modelPart; + private float[] renderVertices; + // ==================== 二级顶点支持 ==================== private final List secondaryVertices = new ArrayList<>(); @@ -104,7 +104,17 @@ public class Mesh2D { private static final float SNAP_THRESHOLD = 0.01f; // 靠近判定阈值(按需要调大,比如 0.1f) private SecondaryVertex pinnedController = null; // 当前作为“钉子”的控制点(若有) public Mesh2D() { - this("unnamed"); + this.name = "unnamed"; + this.vertices = new float[0]; + this.uvs = new float[0]; + this.indices = new int[0]; + this.originalVertices = new float[0]; + this.renderVertices = null; + this.bounds = new BoundingBox(); + this.pivot = new Vector2f(0f, 0f); + this.originalPivot = new Vector2f(0f, 0f); + this.bakedToWorld = false; + this.dirty = true; } public Mesh2D(String name) { @@ -172,6 +182,110 @@ public class Mesh2D { markDirty(); } + /** + * 获取渲染用顶点(返回副本以防外部修改) + */ + public float[] getRenderVertices() { + ensureRenderVerticesInitialized(); + return renderVertices != null ? renderVertices.clone() : null; + } + + /** + * 渲染上传器/渲染线程调用此接口获取用于上传到 GPU 的顶点数组。 + * 优先返回 renderVertices(已是世界坐标),否则返回局部顶点副本(兼容旧渲染路径)。 + */ + public float[] getVerticesForUpload() { + ensureRenderVerticesInitialized(); + if (renderVertices != null) return renderVertices; + return vertices != null ? vertices.clone() : null; + } + + /** + * 确保 renderVertices 已初始化并与局部 vertices 长度一致 + */ + private void ensureRenderVerticesInitialized() { + if (this.vertices == null) return; + if (this.renderVertices == null || this.renderVertices.length != this.vertices.length) { + this.renderVertices = this.vertices.clone(); + } + } + + /** + * 设置渲染用顶点(索引以顶点序号计) + * 注意:此方法仅修改渲染缓存,不触碰局部 vertices 或 originalVertices。 + */ + public void setRenderVertex(int index, float x, float y) { + ensureRenderVerticesInitialized(); + if (renderVertices == null) return; + if (index < 0 || index >= getVertexCount()) { + throw new IndexOutOfBoundsException("Render vertex index out of bounds: " + index); + } + int base = index * 2; + renderVertices[base] = x; + renderVertices[base + 1] = y; + markDirty(); + } + + /** + * 将局部顶点数组一次性转换为世界坐标并写入 renderVertices(由 ModelPart 调用) + * 注意:不修改局部 vertices/originalVertices,只修改 renderVertices(渲染缓存)。 + */ + // Mesh2D.java + + public void syncRenderVerticesFromLocal(Matrix3f worldTransform) { + if (this.vertices == null || this.vertices.length == 0) return; + ensureRenderVerticesInitialized(); + + // 【关键新增】获取 ModelPart 的 Pivot。这是解决偏移的关键。 + ModelPart part = getModelPart(); // 假设 Mesh2D 提供了 getModelPart() 方法 + if (part == null) { + logger.warn("Mesh {} 找不到关联的 ModelPart,无法获取 Pivot。", this.name); + // 如果找不到 Part,则继续使用默认 (0,0) 局部坐标 + } + // 默认 Pivot 补偿为 (0, 0) + Vector2f pivotCompensation = (part != null) ? part.getPivot() : new Vector2f(0, 0); // 假设 ModelPart 有 getPivot() + + try { + int vc = this.vertices.length / 2; + for (int i = 0; i < vc; i++) { + int ix = i * 2; + Vector2f local = new Vector2f(this.vertices[ix], this.vertices[ix + 1]); + + // ---------------------------------------------------------------------------------- + // 【修正 1:形变网格顶点】 + // 如果 ModelPart 变换是绕 Pivot 进行的,那么网格顶点需要相对于 Pivot 进行平移补偿 + Vector2f adjustedLocal = local.sub(pivotCompensation.x, pivotCompensation.y, new Vector2f()); + + Vector2f world = Matrix3fUtils.transformPoint(worldTransform, adjustedLocal); + // ---------------------------------------------------------------------------------- + + renderVertices[ix] = world.x; + renderVertices[ix + 1] = world.y; + } + this.bakedToWorld = true; + this.boundsDirty = false; + + // 额外同步 SecondaryVertex 的世界坐标 + for (SecondaryVertex sv : secondaryVertices) { + Vector2f local = sv.getPosition(); + + // ---------------------------------------------------------------------------------- + // 【修正 2:SecondaryVertex 渲染位置】 + // SecondaryVertex 的局部位置也需要相对于 Pivot 进行平移补偿 + Vector2f adjustedLocal = local.sub(pivotCompensation.x, pivotCompensation.y, new Vector2f()); + + Vector2f world = Matrix3fUtils.transformPoint(worldTransform, adjustedLocal); + // ---------------------------------------------------------------------------------- + + sv.setWorldPosition(world); + } + + markDirty(); + } catch (Exception e) { + logger.error("syncRenderVerticesFromLocal failed for mesh {}: {}", this.name, e.getMessage(), e); + } + } + /** * 获取是否渲染顶点模式 */ @@ -510,10 +624,13 @@ public class Mesh2D { throw new IllegalArgumentException("Vertices and UVs must have same number of points"); } - this.vertices = vertices.clone(); + this.vertices = vertices.clone(); // 局部顶点(用于变形) this.uvs = uvs.clone(); this.indices = indices.clone(); - this.originalVertices = vertices.clone(); + this.originalVertices = vertices.clone(); // 记录 original + + // 初始化渲染缓存(初始与局部顶点一致) + this.renderVertices = vertices.clone(); // 将当前 pivot 视为原始(局部)pivot 的初始值 this.originalPivot.set(this.pivot); @@ -638,7 +755,7 @@ public class Mesh2D { } public float[] getOriginalVertices() { - return originalVertices != null ? originalVertices.clone() : vertices.clone(); + return (originalVertices != null) ? originalVertices.clone() : (vertices != null ? vertices.clone() : new float[0]); } /** @@ -646,6 +763,13 @@ public class Mesh2D { */ public void setOriginalVertices(float[] originalVertices) { this.originalVertices = originalVertices != null ? originalVertices.clone() : null; + + // 修改原始数据意味着之前的 renderVertices/烘焙不再可信 + this.renderVertices = null; + this.bakedToWorld = false; + + // 标记脏,触发后续重新计算/上传 + markDirty(); } /** @@ -796,8 +920,14 @@ public class Mesh2D { */ public SecondaryVertex addSecondaryVertex(float x, float y, float u, float v) { SecondaryVertex vertex = new SecondaryVertex(x, y, u, v); + // 初始化当前位置为原始位置,避免新建时 position 默认为 (0,0) 导致网格瞬移至 (0,0) + Vector2f orig = vertex.getOriginalPosition(); + if (orig != null) { + vertex.setPosition(new Vector2f(orig.x, orig.y)); + } secondaryVertices.add(vertex); markDirty(); + updateVerticesFromSecondaryVertices(); return vertex; } @@ -805,6 +935,29 @@ public class Mesh2D { return addSecondaryVertex(position.x, position.y, uv.x, uv.y); } + public void createDefaultSecondaryVertices() { + updateBounds(); + BoundingBox bounds = getBounds(); + if (bounds == null || !bounds.isValid()) { + logger.warn("无法创建默认二级顶点:边界框无效"); + return; + } + float minX = bounds.getMinX(); + float minY = bounds.getMinY(); + float maxX = bounds.getMaxX(); + float maxY = bounds.getMaxY(); + // 1. 左上角 (0, 0) + addSecondaryVertex(minX, minY, 0.0f, 0.0f); + // 2. 右上角 (1, 0) + addSecondaryVertex(maxX, minY, 1.0f, 0.0f); + // 3. 右下角 (1, 1) + addSecondaryVertex(maxX, maxY, 1.0f, 1.0f); + // 4. 左下角 (0, 1) + addSecondaryVertex(minX, maxY, 0.0f, 1.0f); + logger.info("为网格 {} 创建了四个默认二级顶点", getName()); + markDirty(); + } + /** * 在指定位置插入二级顶点 */ @@ -823,25 +976,29 @@ public class Mesh2D { */ public void moveSecondaryVertex(SecondaryVertex v, float newX, float newY) { if (v == null) return; - if (v.isLocked()) { - // 已锁定,不能移动 - logger.debug("secondary vertex {} is locked, move ignored", v.getId()); + logger.debug("Secondary vertex {} is locked, move ignored", v.getId()); return; } - // 如果 v 已经被 pin,则将整个网格按 delta 平移 - Vector2f oldPos = v.getPosition(); + // 【关键修改:移除或禁用 pinned 状态下的 applyDeltaToMesh 调用】 + // 在工具中,如果拖拽的钉子是 pinned 状态,我们应该阻止它进行任何操作, + // 或者允许它移动但不再触发整个网格的平移。 if (v.isPinned()) { - float dx = newX - oldPos.x; - float dy = newY - oldPos.y; - applyDeltaToMesh(dx, dy); - logger.debug("moved pinned vertex {} by delta ({}, {}) and translated whole mesh", v.getId(), dx, dy); + // 允许 pinned 钉子移动,但不再触发 applyDeltaToMesh。 + // ModelPart 的平移应该由 SelectionTool/ModelPart 自身更新其 position 字段来驱动。 + v.setPosition(newX, newY); + logger.debug("Moved pinned vertex {} to ({}, {}) without translating whole mesh", v.getId(), newX, newY); + // 即使 pinned 移动,也需要更新形变网格,因为 position 变了 + updateVerticesFromSecondaryVertices(); + markDirty(); return; } // 否则我们尝试正常移动,同时检查是否碰撞到其他顶点(snap) // 优先检测是否靠近其他 existing 顶点位置 + Vector2f oldPos = v.getPosition(); // 用于计算 delta,但在 pin 状态已处理 + for (SecondaryVertex other : secondaryVertices) { if (other == v) continue; Vector2f otherPos = other.getPosition(); @@ -851,9 +1008,12 @@ public class Mesh2D { // 把被靠近的那个当作钉子 other.setPinned(true); pinnedController = other; - // 把移动到其上的顶点锁定,不能再单独移动 + // 把移动到其上的顶点锁定 v.setLocked(true); logger.info("SecondaryVertex {} snapped to {}. {} pinned, {} locked.", v.getId(), other.getId(), other.getId(), v.getId()); + + updateVerticesFromSecondaryVertices(); + markDirty(); return; } } @@ -861,6 +1021,10 @@ public class Mesh2D { // 没有 snap,正常移动该点(只移动当前顶点,不影响整块) v.setPosition(newX, newY); logger.debug("Moved secondary vertex {} to ({}, {})", v.getId(), newX, newY); + + // 确保非 snap 移动后,形变网格和渲染状态立即更新 + updateVerticesFromSecondaryVertices(); + markDirty(); } /** @@ -894,16 +1058,14 @@ public class Mesh2D { if (dx == 0f && dy == 0f) return; // 移动 secondary vertices(当前 pos 和 original pos 都平移) - if (secondaryVertices != null) { - for (SecondaryVertex sv : secondaryVertices) { - Vector2f p = sv.getPosition(); - p.add(dx, dy); - sv.setPosition(p); + for (SecondaryVertex sv : secondaryVertices) { + Vector2f p = sv.getPosition(); + p.add(dx, dy); + sv.setPosition(p); - Vector2f op = sv.getOriginalPosition(); - op.add(dx, dy); - sv.setOriginalPosition(op); - } + Vector2f op = sv.getOriginalPosition(); + op.add(dx, dy); + sv.setOriginalPosition(op); } // 移动顶点数组(当前和原始) @@ -1063,10 +1225,6 @@ public class Mesh2D { return null; } - public SecondaryVertex selectSecondaryVertexAt(Vector2f position, float tolerance) { - return selectSecondaryVertexAt(position.x, position.y, tolerance); - } - /** * 移动选中的二级顶点 */ @@ -1162,7 +1320,8 @@ public class Mesh2D { float translateX = currentBounds.getMinX() - originalBounds.getMinX(); float translateY = currentBounds.getMinY() - originalBounds.getMinY(); - // 应用变换到所有二级顶点 + // 应用变换到所有二级顶点(**注意:不要修改 secondary 的 originalPosition**, + // 否则会改变用于计算 delta 的基准位置,导致一级顶点整体偏移) for (SecondaryVertex vertex : secondaryVertices) { Vector2f originalPos = vertex.getOriginalPosition(); float newX = originalPos.x * scaleX + translateX; @@ -1171,6 +1330,7 @@ public class Mesh2D { } } + /** * 保存当前二级顶点位置为原始位置(在网格移动后调用) */ @@ -1194,14 +1354,45 @@ public class Mesh2D { return; } - // 如果控制点太少,直接使用反距离加权(兼容性) + // 计算原始顶点质心(用于后续抵消整体平移) + int vertCount = originalVertices.length / 2; + if (vertCount == 0) return; + float origCx = 0f, origCy = 0f; + for (int i = 0; i < originalVertices.length; i += 2) { + origCx += originalVertices[i]; + origCy += originalVertices[i + 1]; + } + origCx /= vertCount; + origCy /= vertCount; + + // 根据控制点数量选择策略 if (secondaryVertices.size() < 3) { updateVerticesUsingInverseDistanceWeighting(); - return; + } else { + updateVerticesUsingTriangularPartition(); } - // 主要使用三角分配策略 - updateVerticesUsingTriangularPartition(); + // 计算变形后顶点的质心 + float newCx = 0f, newCy = 0f; + for (int i = 0; i < vertices.length; i += 2) { + newCx += vertices[i]; + newCy += vertices[i + 1]; + } + newCx /= vertCount; + newCy /= vertCount; + + // 抵消全局平移:把新质心移回原始质心位置 + float tx = origCx - newCx; + float ty = origCy - newCy; + + // 只有在有显著偏移时才应用修正,避免数值抖动 + if (Math.abs(tx) > 1e-6f || Math.abs(ty) > 1e-6f) { + for (int i = 0; i < vertices.length; i += 2) { + vertices[i] += tx; + vertices[i + 1] += ty; + } + logger.debug("纠正整体位移,平移量 ({}, {})", tx, ty); + } } /** @@ -1211,85 +1402,149 @@ public class Mesh2D { private void updateVerticesUsingTriangularPartition() { try { int secCount = secondaryVertices.size(); + if (secCount == 0) return; - // 预取控制点的原始位置与当前位置(副本) + // 预取控制点的原始位置与当前位置,并计算每个控制点的 delta = current - original Vector2f[] secOrig = new Vector2f[secCount]; - Vector2f[] secCurr = new Vector2f[secCount]; + Vector2f[] deltas = new Vector2f[secCount]; + boolean[] isPinned = new boolean[secCount]; + float[] controlRadiusSq = new float[secCount]; // 存储半径平方,避免重复开方 + for (int i = 0; i < secCount; i++) { - secOrig[i] = secondaryVertices.get(i).getOriginalPosition(); // 原始局部坐标 - secCurr[i] = secondaryVertices.get(i).getPosition(); // 当前局部/世界坐标(视实现而定) + SecondaryVertex sv = secondaryVertices.get(i); + Vector2f secCurr = sv.getPosition(); + secOrig[i] = sv.getOriginalPosition(); + deltas[i] = new Vector2f(secCurr.x - secOrig[i].x, secCurr.y - secOrig[i].y); + isPinned[i] = sv.isPinned(); + controlRadiusSq[i] = sv.getControlRadius() * sv.getControlRadius(); // 预计算平方 } for (int i = 0; i < originalVertices.length; i += 2) { - float origX = originalVertices[i]; - float origY = originalVertices[i + 1]; + float ox = originalVertices[i]; + float oy = originalVertices[i + 1]; - // 找到距离该点最近的三个控制点索引(优先考虑 controlRadius 的实现由 findNearestNIndices 决定) - int[] nearest = findNearestNIndices(origX, origY, 3, secOrig); + Vector2f finalDelta = null; - // 如果未能找到 3 个点,回退到 IDW(基于位移 delta) - if (nearest == null || nearest.length < 3) { - Vector2f idw = computeIDWForPoint(origX, origY, secOrig, secCurr); - vertices[i] = idw.x; - vertices[i + 1] = idw.y; - continue; + // --- 1) 优先检查 pinned 控制点(钉子) + // 找到距离最近且覆盖当前顶点的 Pinned 点 + int pinnedMatch = -1; + float bestPinnedDistSq = Float.MAX_VALUE; + for (int p = 0; p < secCount; p++) { + if (!isPinned[p]) continue; + float dx = ox - secOrig[p].x; + float dy = oy - secOrig[p].y; + float distSq = dx * dx + dy * dy; + if (distSq <= controlRadiusSq[p] && distSq < bestPinnedDistSq) { + pinnedMatch = p; + bestPinnedDistSq = distSq; + } } - - int ia = nearest[0], ib = nearest[1], ic = nearest[2]; - Vector2f A = secOrig[ia]; - Vector2f B = secOrig[ib]; - Vector2f C = secOrig[ic]; - - // 检测三角形是否退化(面积接近 0) - float area2 = Math.abs((B.x - A.x) * (C.y - A.y) - (C.x - A.x) * (B.y - A.y)); - if (area2 < 1e-6f) { - // 退化:回退到 IDW(基于位移 delta) - Vector2f idw = computeIDWForPoint(origX, origY, secOrig, secCurr); - vertices[i] = idw.x; - vertices[i + 1] = idw.y; - continue; - } - - // 点在三角形内部则使用重心坐标映射,但映射的是“位移 delta”,以保持整体位置不变 - if (pointInTriangle(origX, origY, A, B, C)) { - float[] bary = barycentricCoordinates(A, B, C, origX, origY); - - // 计算每个控制点的 delta(current - original) - Vector2f Acur = secCurr[ia]; - Vector2f Bcur = secCurr[ib]; - Vector2f Ccur = secCurr[ic]; - - float dAx = Acur.x - A.x; - float dAy = Acur.y - A.y; - float dBx = Bcur.x - B.x; - float dBy = Bcur.y - B.y; - float dCx = Ccur.x - C.x; - float dCy = Ccur.y - C.y; - - // 按重心系数混合 delta - float dx = bary[0] * dAx + bary[1] * dBx + bary[2] * dCx; - float dy = bary[0] * dAy + bary[1] * dBy + bary[2] * dCy; - - // 新位置 = 原始顶点位置 + 混合位移(不会把整个网格移动到原点) - vertices[i] = origX + dx; - vertices[i + 1] = origY + dy; + if (pinnedMatch != -1) { + // 使用该 pinned 的位移,保证“钉子周围点被固定” + finalDelta = deltas[pinnedMatch]; } else { - // 不在三角形内:回退到 IDW(基于位移 delta) - Vector2f idw = computeIDWForPoint(origX, origY, secOrig, secCurr); - vertices[i] = idw.x; - vertices[i + 1] = idw.y; + // --- 2) 尝试三角分配(最近 3 个控制点) + int[] nearest = findNearestNIndices(ox, oy, 3, secOrig); + if (nearest != null && nearest.length == 3) { + int ia = nearest[0], ib = nearest[1], ic = nearest[2]; + Vector2f A = secOrig[ia], B = secOrig[ib], C = secOrig[ic]; + + // 检测三角形是否退化或点在内部 + // 注意:这里我们使用一个更严格的条件:点必须在三角形内部 + if (pointInTriangle(ox, oy, A, B, C)) { + // 面积计算用于判断退化和重心坐标 + float areaABC = (B.x - A.x) * (C.y - A.y) - (C.x - A.x) * (B.y - A.y); + + // 如果三角形面积足够大 + if (Math.abs(areaABC) >= 1e-6f) { + float[] bary = barycentricCoordinates(A, B, C, ox, oy); + + // 按重心系数混合控制点的 delta + float dx = bary[0] * deltas[ia].x + bary[1] * deltas[ib].x + bary[2] * deltas[ic].x; + float dy = bary[0] * deltas[ia].y + bary[1] * deltas[ib].y + bary[2] * deltas[ic].y; + + finalDelta = new Vector2f(dx, dy); + } + } + } + + // --- 3) 回退到 IDW(基于 deltas) + if (finalDelta == null) { + finalDelta = computeIDWForPointUsingDeltas(ox, oy, secOrig, deltas); + // IDW 方法返回的是最终位置,需要减去原始位置以获得 delta + finalDelta.x -= ox; + finalDelta.y -= oy; + } + } + + // 应用最终的 delta + if (finalDelta != null) { + vertices[i] = ox + finalDelta.x; + vertices[i + 1] = oy + finalDelta.y; + } else { + // 如果所有方法都失败,保持原始位置 + vertices[i] = ox; + vertices[i + 1] = oy; } } - logger.debug("应用三角分配变形(使用位移 delta 保持全局位置),使用了 {} 个控制点", secondaryVertices.size()); + logger.debug("应用三角分配变形(Live2D风格的基于 delta 的插值与 pinned 修正),使用了 {} 个控制点", secondaryVertices.size()); } catch (Exception e) { logger.error("三角分配变形失败,回退到反距离加权", e); - // 出错回退 + // 确保有一个 IDW 回退方法 (需要您实现 updateVerticesUsingInverseDistanceWeighting()) updateVerticesUsingInverseDistanceWeighting(); } } + /** + * 基于反距离加权(IDW)但对“位移 delta”加权计算结果。 + * 输入为控制点原始位置 secOrig 和每点的 delta(current - original)。 + * 返回最终的绝对坐标(orig + weighted_delta) + */ + private Vector2f computeIDWForPointUsingDeltas(float ox, float oy, Vector2f[] secOrig, Vector2f[] deltas) { + // Live2D 弯曲变形的影响力衰减可能更快,使用 power = 3.0f 或 4.0f 可能会更自然地模拟局部性。 + // 这里采用 power = 3.0f 作为改进尝试。 + final float power = 3.0f; // 增加衰减速度 + final float eps = 1e-5f; + + float sumWX = 0f; + float sumWY = 0f; + float weightSum = 0f; + + for (int k = 0; k < secOrig.length; k++) { + Vector2f sO = secOrig[k]; + + float dx = ox - sO.x; + float dy = oy - sO.y; + float distSq = dx * dx + dy * dy; + + // 如果非常接近某个控制点,直接使用该控制点的 delta,避免数值不稳定 + if (distSq < eps * eps) { + return new Vector2f(ox + deltas[k].x, oy + deltas[k].y); + } + + // --- 核心改进:权重计算 --- + float dist = (float) Math.sqrt(distSq); + // Live2D 可能使用径向基函数(RBF)或平滑的权重, + // 但这里保持 IDW 结构,仅增加 power 使影响更局部。 + float w = 1.0f / (float) Math.pow(dist, power); + + sumWX += w * deltas[k].x; + sumWY += w * deltas[k].y; + weightSum += w; + } + + if (weightSum > 0f) { + float dxFinal = sumWX / weightSum; + float dyFinal = sumWY / weightSum; + return new Vector2f(ox + dxFinal, oy + dyFinal); + } else { + return new Vector2f(ox, oy); + } + } + + /** * 基于反距离加权(IDW)但对“位移 delta”加权计算结果,保证不会把整个网格搬到 (0,0)。 * 返回最终的绝对坐标(orig + weighted_delta) @@ -1298,8 +1553,8 @@ public class Mesh2D { final float power = 2.0f; final float eps = 1e-5f; - float nx = ox; - float ny = oy; + float sumWX = 0f; + float sumWY = 0f; float weightSum = 0f; for (int k = 0; k < secOrig.length; k++) { @@ -1308,62 +1563,36 @@ public class Mesh2D { float dx = ox - sO.x; float dy = oy - sO.y; - float dist = (float) Math.sqrt(dx * dx + dy * dy); + float distSq = dx * dx + dy * dy; - // 如果原始顶点非常靠近某个控制点的原始位置,直接使用该控制点的 delta(避免数值不稳定) - if (dist < eps) { + // 如果非常接近某个控制点,直接使用该控制点的位移,避免数值不稳定 + if (distSq < eps * eps) { float deltaX = sC.x - sO.x; float deltaY = sC.y - sO.y; return new Vector2f(ox + deltaX, oy + deltaY); } + float dist = (float) Math.sqrt(distSq); float w = 1.0f / (float) Math.pow(dist, power); + float deltaX = sC.x - sO.x; float deltaY = sC.y - sO.y; - nx += w * deltaX; - ny += w * deltaY; + sumWX += w * deltaX; + sumWY += w * deltaY; weightSum += w; } if (weightSum > 0f) { - nx = (ox * 1.0f + (nx - ox) / 1.0f * 1.0f); // 保持结构:ox + (sum(w*delta)/sum(w)) - // 计算正确的加权和:我们已经把 ox added multiple times, 修正如下: - // 实际上应为 ox + ( sum(w*delta) / sum(w) ) - // 为此重新计算 sum(w*deltaX) 与 sum(w*deltaY) - float sumWX = 0f; - float sumWY = 0f; - weightSum = 0f; - for (int k = 0; k < secOrig.length; k++) { - Vector2f sO = secOrig[k]; - Vector2f sC = secCurr[k]; - float dx = ox - sO.x; - float dy = oy - sO.y; - float dist = (float) Math.sqrt(dx * dx + dy * dy); - if (dist < eps) { - float deltaX = sC.x - sO.x; - float deltaY = sC.y - sO.y; - return new Vector2f(ox + deltaX, oy + deltaY); - } - float w = 1.0f / (float) Math.pow(dist, power); - float deltaX = sC.x - sO.x; - float deltaY = sC.y - sO.y; - sumWX += w * deltaX; - sumWY += w * deltaY; - weightSum += w; - } - if (weightSum > 0f) { - float dxFinal = sumWX / weightSum; - float dyFinal = sumWY / weightSum; - return new Vector2f(ox + dxFinal, oy + dyFinal); - } else { - return new Vector2f(ox, oy); - } + float dxFinal = sumWX / weightSum; + float dyFinal = sumWY / weightSum; + return new Vector2f(ox + dxFinal, oy + dyFinal); } else { return new Vector2f(ox, oy); } } + /** * 在给定的控制点数组中,返回距离 (x,y) 最近的 N 个索引(按距离升序) * 如果可用控制点少于 n,返回实际找到的索引数组。 @@ -1945,7 +2174,7 @@ public class Mesh2D { * 获取顶点数量 */ public int getVertexCount() { - return vertices.length / 2; + return (vertices != null) ? (vertices.length / 2) : 0; } /** @@ -1970,11 +2199,14 @@ public class Mesh2D { throw new IndexOutOfBoundsException("Vertex index out of bounds: " + index); } int baseIndex = index * 2; + // 只写局部顶点(供变形使用) vertices[baseIndex] = x; vertices[baseIndex + 1] = y; + // 不直接把局部坐标写到 renderVertices(render 由 ModelPart 更新世界变换时写入) markDirty(); } + public void setVertex(int index, Vector2f position) { setVertex(index, position.x, position.y); } @@ -2065,26 +2297,173 @@ public class Mesh2D { // ==================== 边界计算 ==================== /** - * 更新边界框 + * 更新边界 */ public void updateBounds() { - bounds.reset(); + // 优先使用渲染顶点(世界坐标) + if (this.renderVertices != null && this.bakedToWorld && this.renderVertices.length >= 2) { + float minX = Float.POSITIVE_INFINITY, minY = Float.POSITIVE_INFINITY; + float maxX = Float.NEGATIVE_INFINITY, maxY = Float.NEGATIVE_INFINITY; + boolean hasValidPoints = false; - for (int i = 0; i < vertices.length; i += 2) { - bounds.expand(vertices[i], vertices[i + 1]); + for (int i = 0; i < renderVertices.length; i += 2) { + float x = renderVertices[i]; + float y = renderVertices[i + 1]; + if (Float.isNaN(x) || Float.isNaN(y) || Float.isInfinite(x) || Float.isInfinite(y)) continue; + + hasValidPoints = true; + if (x < minX) minX = x; + if (y < minY) minY = y; + if (x > maxX) maxX = x; + if (y > maxY) maxY = y; + } + + if (hasValidPoints) { + if (this.bounds == null) this.bounds = new BoundingBox(); + this.bounds.set(minX, minY, maxX, maxY); + this.boundsDirty = false; + return; + } } - boundsDirty = false; + // 回退到局部顶点计算 - 关键修复:只使用原始网格顶点,不包含二级顶点 + float[] src = null; + String srcType = "null"; + + if (this.vertices != null && this.vertices.length > 0) { + src = this.vertices; + srcType = "vertices"; + } else if (this.originalVertices != null) { + src = this.originalVertices; + srcType = "originalVertices"; + } else { + srcType = "null"; + } + + logger.debug("Mesh2D.updateBounds [{}] - 使用的数据源: {}, 长度: {}", + this.name, srcType, (src != null ? src.length : 0)); + + if (src == null || src.length < 2) { + if (this.bounds == null) this.bounds = new BoundingBox(); + this.bounds.set(0f, 0f, 0f, 0f); + this.boundsDirty = false; + return; + } + + float minX = Float.POSITIVE_INFINITY, minY = Float.POSITIVE_INFINITY; + float maxX = Float.NEGATIVE_INFINITY, maxY = Float.NEGATIVE_INFINITY; + boolean hasValidPoints = false; + + // 关键:只计算原始网格顶点的边界,不包含二级顶点 + for (int i = 0; i < src.length; i += 2) { + float x = src[i]; + float y = src[i + 1]; + if (Float.isNaN(x) || Float.isNaN(y) || Float.isInfinite(x) || Float.isInfinite(y)) continue; + + hasValidPoints = true; + if (x < minX) minX = x; + if (y < minY) minY = y; + if (x > maxX) maxX = x; + if (y > maxY) maxY = y; + } + + if (!hasValidPoints || minX == Float.POSITIVE_INFINITY) { + minX = minY = maxX = maxY = 0f; + } + + if (this.bounds == null) this.bounds = new BoundingBox(); + this.bounds.set(minX, minY, maxX, maxY); + this.boundsDirty = false; + + logger.debug("边界更新完成 - 使用{}坐标: [{}, {}, {}, {}] (不包含二级顶点)", + (this.renderVertices != null && this.bakedToWorld) ? "世界" : "局部", + minX, minY, maxX, maxY); } /** * 获取边界框 */ public BoundingBox getBounds() { - if (boundsDirty) { + if (this.boundsDirty) { updateBounds(); } - return bounds; + return this.bounds; + } + + + public BoundingBox getWorldBounds() { + BoundingBox wb = new BoundingBox(); + + // 1) 优先使用 renderVertices(已是世界坐标) + if (this.renderVertices != null && this.bakedToWorld && this.renderVertices.length >= 2) { + float minX = Float.POSITIVE_INFINITY, minY = Float.POSITIVE_INFINITY; + float maxX = Float.NEGATIVE_INFINITY, maxY = Float.NEGATIVE_INFINITY; + for (int i = 0; i + 1 < renderVertices.length; i += 2) { + float x = renderVertices[i]; + float y = renderVertices[i + 1]; + if (Float.isNaN(x) || Float.isInfinite(x) || Float.isNaN(y) || Float.isInfinite(y)) continue; + if (x < minX) minX = x; + if (y < minY) minY = y; + if (x > maxX) maxX = x; + if (y > maxY) maxY = y; + } + if (minX == Float.POSITIVE_INFINITY) { // no valid points + wb.set(0f, 0f, 0f, 0f); + } else { + wb.set(minX, minY, maxX, maxY); + } + return wb; + } + + // 2) 否则使用局部 vertices(不包含二级顶点) + float[] src = (this.vertices != null && this.vertices.length > 0) ? this.vertices + : (this.originalVertices != null ? this.originalVertices : null); + + if (src == null || src.length < 2) { + wb.set(0f, 0f, 0f, 0f); + return wb; + } + + // 如果没有 modelPart,直接把局部 bounds 返回(视为 world) + if (this.modelPart == null) { + // 直接使用局部 bounds + float minX = Float.POSITIVE_INFINITY, minY = Float.POSITIVE_INFINITY; + float maxX = Float.NEGATIVE_INFINITY, maxY = Float.NEGATIVE_INFINITY; + for (int i = 0; i + 1 < src.length; i += 2) { + float x = src[i], y = src[i + 1]; + if (Float.isNaN(x) || Float.isNaN(y) || Float.isInfinite(x) || Float.isInfinite(y)) continue; + if (x < minX) minX = x; + if (y < minY) minY = y; + if (x > maxX) maxX = x; + if (y > maxY) maxY = y; + } + if (minX == Float.POSITIVE_INFINITY) wb.set(0f, 0f, 0f, 0f); + else wb.set(minX, minY, maxX, maxY); + return wb; + } + + // 确保 modelPart 的 worldTransform 是最新的 + Matrix3f wt = new Matrix3f(modelPart.getWorldTransform()); + + float minX = Float.POSITIVE_INFINITY, minY = Float.POSITIVE_INFINITY; + float maxX = Float.NEGATIVE_INFINITY, maxY = Float.NEGATIVE_INFINITY; + + Vector2f tmp = new Vector2f(); + for (int i = 0; i + 1 < src.length; i += 2) { + tmp.set(src[i], src[i + 1]); + Vector2f worldPt = Matrix3fUtils.transformPoint(wt, tmp); + float x = worldPt.x, y = worldPt.y; + if (Float.isNaN(x) || Float.isNaN(y) || Float.isInfinite(x) || Float.isInfinite(y)) continue; + if (x < minX) minX = x; + if (y < minY) minY = y; + if (x > maxX) maxX = x; + if (y > maxY) maxY = y; + } + + if (minX == Float.POSITIVE_INFINITY) wb.set(0f, 0f, 0f, 0f); + else wb.set(minX, minY, maxX, maxY); + + return wb; } /** @@ -2238,16 +2617,28 @@ public class Mesh2D { if (index < 0 || index >= getVertexCount()) { throw new IndexOutOfBoundsException("Vertex index out of bounds: " + index); } - return vertices[index * 2]; + int base = index * 2; + // 使用 renderVertices(已由 syncRenderVerticesFromLocal 填充并标记 bakedToWorld) + if (renderVertices != null && bakedToWorld && renderVertices.length > base + 1) { + return renderVertices[base]; + } + // 回退到局部顶点(变形/编辑写入这里) + return vertices[base]; } + public float getY(int index) { if (index < 0 || index >= getVertexCount()) { throw new IndexOutOfBoundsException("Vertex index out of bounds: " + index); } - return vertices[index * 2 + 1]; + int base = index * 2; + if (renderVertices != null && bakedToWorld && renderVertices.length > base + 1) { + return renderVertices[base + 1]; + } + return vertices[base + 1]; } + /** * 获取索引缓冲区数据 */ @@ -2289,15 +2680,18 @@ public class Mesh2D { * 标记数据已修改 */ public void markDirty() { - if (puppetPins.isEmpty() || !hasMovedPuppetPins()) { - updateVerticesFromSecondaryVertices(); - } + // 删除旧 GPU 对象(若有),并标记脏 deleteGPU(); this.dirty = true; this.boundsDirty = true; this.multiSelectionDirty = true; + + // 渲染缓存(renderVertices)不再可信,清除烘焙标志 + this.bakedToWorld = false; + // 注意:不立即清除 renderVertices 数组(保留以供 debug),但 bakedToWorld=false 可确保上传使用局部 vertices } + /** * 检查是否有移动过的木偶控制点 */ @@ -2458,6 +2852,8 @@ public class Mesh2D { if ("PUPPET".equals(deformationType) || "CONFLICT".equals(deformationType)) { updateVerticesFromPuppetPins(); + } else if ("SECONDARY".equals(deformationType)) { + updateVerticesFromSecondaryVertices(); } if (!visible) return; @@ -2657,11 +3053,21 @@ public class Mesh2D { * 添加网格到多选列表 */ public void addToMultiSelection(Mesh2D mesh) { - if (mesh != null && !multiSelectedParts.contains(mesh)) { - multiSelectedParts.add(mesh); - multiSelectionDirty = true; - markDirty(); + if (mesh == null || mesh == this || multiSelectedParts.contains(mesh)) { + return; } + + multiSelectedParts.add(mesh); + multiSelectionDirty = true; + markDirty(); + + if (!mesh.multiSelectedParts.contains(this)) { + mesh.multiSelectedParts.add(this); + mesh.multiSelectionDirty = true; + mesh.markDirty(); + } + + logger.debug("网格 {} 添加到 {} 的多选列表", mesh.getName(), this.getName()); } /** @@ -2671,6 +3077,13 @@ public class Mesh2D { if (multiSelectedParts.remove(mesh)) { multiSelectionDirty = true; markDirty(); + + if (mesh.multiSelectedParts.remove(this)) { + mesh.multiSelectionDirty = true; + mesh.markDirty(); + } + + logger.debug("网格 {} 从 {} 的多选列表移除", mesh.getName(), this.getName()); } } @@ -2678,18 +3091,28 @@ public class Mesh2D { * 清空多选列表 */ public void clearMultiSelection() { - if (!multiSelectedParts.isEmpty()) { - multiSelectedParts.clear(); - multiSelectionDirty = true; - markDirty(); + if (multiSelectedParts.isEmpty()) { + return; } + + List toRemove = new ArrayList<>(multiSelectedParts); + + for (Mesh2D mesh : toRemove) { + removeFromMultiSelection(mesh); + } + + multiSelectedParts.clear(); + multiSelectionDirty = true; + markDirty(); + + logger.debug("清空网格 {} 的多选列表", this.getName()); } /** * 获取多选列表 */ public List getMultiSelectedParts() { - return new ArrayList<>(multiSelectedParts); + return List.copyOf(multiSelectedParts); } /** @@ -2706,7 +3129,7 @@ public class Mesh2D { if (multiSelectionDirty) { updateMultiSelectionBounds(); } - return multiSelectionBounds; + return new BoundingBox(multiSelectionBounds); } /** @@ -2715,15 +3138,12 @@ public class Mesh2D { private void updateMultiSelectionBounds() { multiSelectionBounds.reset(); - // 首先包含自己的边界(应用变换后的边界) BoundingBox selfBounds = getBounds(); if (selfBounds.isValid()) { multiSelectionBounds.expand(selfBounds); } - // 然后包含所有多选部分的边界(应用它们各自的变换) for (Mesh2D mesh : multiSelectedParts) { - // 确保其他网格的边界也是最新的 mesh.updateBounds(); BoundingBox meshBounds = mesh.getBounds(); if (meshBounds.isValid()) { @@ -2732,6 +3152,10 @@ public class Mesh2D { } multiSelectionDirty = false; + + logger.debug("更新多选边界: [{}, {}, {}, {}]", + multiSelectionBounds.getMinX(), multiSelectionBounds.getMinY(), + multiSelectionBounds.getMaxX(), multiSelectionBounds.getMaxY()); } /** @@ -2740,6 +3164,10 @@ public class Mesh2D { public void forceUpdateMultiSelectionBounds() { multiSelectionDirty = true; updateMultiSelectionBounds(); + + for (Mesh2D mesh : multiSelectedParts) { + mesh.multiSelectionDirty = true; + } } /** @@ -3234,7 +3662,7 @@ public class Mesh2D { } public float[] getVertices() { - return vertices.clone(); + return (vertices != null) ? vertices.clone() : new float[0]; } public float[] getUVs() { @@ -3364,6 +3792,10 @@ public class Mesh2D { } } + public ModelPart getModelPart() { + return modelPart; + } + /** * 标记或查询网格顶点是否已经被烘焙到世界坐标 */ diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/SecondaryVertex.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/SecondaryVertex.java index 4262554..ac06df8 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/SecondaryVertex.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/SecondaryVertex.java @@ -22,6 +22,10 @@ public class SecondaryVertex { private float minControlRadius = 4.0f; // 最小允许半径 private float maxControlRadius = 200.0f; // 最大允许半径 private boolean fixedRadius = false; // 是否锁定半径(固定区域) + transient Vector2f worldPosition = new Vector2f(); + + // 【新增字段】用于存储渲染时的世界坐标,通常由 ModelPart 的世界变换计算而来 + transient Vector2f renderPosition = new Vector2f(); public SecondaryVertex(float x, float y, float u, float v) { this.position = new Vector2f(x, y); @@ -43,6 +47,35 @@ public class SecondaryVertex { return new Vector2f(originalPosition); } + public Vector2f getWorldPosition() { + return new Vector2f(worldPosition); + } + + public void setWorldPosition(float x, float y) { + this.worldPosition.set(x, y); + } + + public void setWorldPosition(Vector2f p) { + if (p == null) return; + this.worldPosition.set(p); + } + + // 【新增 Getter】 + public Vector2f getRenderPosition() { + return new Vector2f(renderPosition); + } + + // 【新增 Setter】 + public void setRenderPosition(float x, float y) { + this.renderPosition.set(x, y); + } + + // 【新增 Setter】 + public void setRenderPosition(Vector2f p) { + if (p == null) return; + this.renderPosition.set(p); + } + public Vector2f getUV() { return new Vector2f(uv); } @@ -162,4 +195,4 @@ public class SecondaryVertex { return String.format("SecondaryVertex{id=%d, position=(%.2f, %.2f), uv=(%.2f, %.2f), pinned=%s, locked=%s}", id, position.x, position.y, uv.x, uv.y, pinned, locked); } -} +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/test/AI3Test.java b/src/main/java/com/chuangzhou/vivid2D/test/AI3Test.java index 73ec954..42f3f28 100644 --- a/src/main/java/com/chuangzhou/vivid2D/test/AI3Test.java +++ b/src/main/java/com/chuangzhou/vivid2D/test/AI3Test.java @@ -19,7 +19,7 @@ public class AI3Test { Set faceLabels = Set.of("foreground"); wrapper.segmentAndSave( - Paths.get("C:\\Users\\Administrator\\Desktop\\b_7a8349adece17d1e4bebd20cb2387cf6.jpg").toFile(), + Paths.get("C:\\Users\\Administrator\\Desktop\\b_e15c587fab8a7291740d44e4ce57599f.jpg").toFile(), faceLabels, Paths.get("C:\\models\\out") ); diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelLayerPanelTest.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelLayerPanelTest.java index d62acee..887adb8 100644 --- a/src/main/java/com/chuangzhou/vivid2D/test/ModelLayerPanelTest.java +++ b/src/main/java/com/chuangzhou/vivid2D/test/ModelLayerPanelTest.java @@ -5,6 +5,8 @@ import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel; import com.chuangzhou.vivid2D.render.awt.TransformPanel; import com.chuangzhou.vivid2D.render.model.Model2D; import com.chuangzhou.vivid2D.render.model.ModelPart; +import com.formdev.flatlaf.themes.FlatMacDarkLaf; +import com.formdev.flatlaf.themes.FlatMacLightLaf; import javax.swing.*; import java.awt.*; @@ -20,6 +22,12 @@ import java.util.List; public class ModelLayerPanelTest { public static void main(String[] args) { SwingUtilities.invokeLater(() -> { + //LookAndFeel defaultLaf = isDarkMode ? : new FlatMacLightLaf(); + try { + UIManager.setLookAndFeel(new FlatMacDarkLaf()); + } catch (UnsupportedLookAndFeelException e) { + throw new RuntimeException(e); + } System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8)); System.setErr(new PrintStream(System.err, true, StandardCharsets.UTF_8)); // 创建示例模型并添加图层