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 extends ModelPart> 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 extends ModelPart> 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));
// 创建示例模型并添加图层