diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelClickListener.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelClickListener.java index 618954e..a078517 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelClickListener.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelClickListener.java @@ -1,5 +1,6 @@ package com.chuangzhou.vivid2D.render.awt; +import com.chuangzhou.vivid2D.render.model.ModelPart; import com.chuangzhou.vivid2D.render.model.util.Mesh2D; /** @@ -27,4 +28,7 @@ public interface ModelClickListener { * @param screenY 屏幕坐标系中的 Y 坐标 */ default void onModelHover(Mesh2D mesh, float modelX, float modelY, int screenX, int screenY) {} + default void onLiquifyModeExited(){}; + + default void onLiquifyModeEntered(Mesh2D targetMesh, ModelPart liquifyTargetPart){}; } 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 8c21c67..a74c75d 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java @@ -824,12 +824,84 @@ public class ModelLayerPanel extends JPanel { 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) return (Mesh2D) o; + if (o instanceof Mesh2D) { + Mesh2D mesh = (Mesh2D) o; + // 对基础四边形进行细分以增加顶点密度 + 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()) { @@ -841,27 +913,157 @@ public class ModelLayerPanel extends JPanel { } if (cons != null) { cons.setAccessible(true); - float[] vertices = new float[]{ - -w / 2f, -h / 2f, - w / 2f, -h / 2f, - w / 2f, h / 2f, - -w / 2f, h / 2f - }; - float[] uvs = new float[]{ - 0f, 1f, - 1f, 1f, - 1f, 0f, - 0f, 0f - }; - int[] indices = new int[]{0, 1, 2, 2, 3, 0}; - Object meshObj = cons.newInstance(meshName, vertices, uvs, indices); - if (meshObj instanceof Mesh2D) return (Mesh2D) meshObj; + Object meshObj = cons.newInstance(name, vertices, uvs, indices); + if (meshObj instanceof Mesh2D) { + Mesh2D mesh = (Mesh2D) meshObj; + + // 设置合适的pivot(中心点) + mesh.setPivot(0, 0); + if (mesh.getOriginalPivot() != null) { + mesh.setOriginalPivot(new Vector2f(0, 0)); + } + + return mesh; + } } - } catch (Exception ex) { - ex.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); } - throw new RuntimeException("无法创建 Mesh2D(没有合适的工厂或构造函数)"); + 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; + } } /** diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java index bd61b3c..db4567a 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java @@ -9,6 +9,7 @@ import com.chuangzhou.vivid2D.render.model.util.BoundingBox; import com.chuangzhou.vivid2D.render.model.util.Mesh2D; import com.chuangzhou.vivid2D.render.systems.Camera; import com.chuangzhou.vivid2D.render.systems.RenderSystem; +import com.chuangzhou.vivid2D.test.TestModelGLPanel; import org.joml.Matrix3f; import org.joml.Vector2f; import org.lwjgl.glfw.*; @@ -19,11 +20,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.event.*; +import java.lang.reflect.Method; import java.nio.IntBuffer; import java.nio.ByteOrder; import java.util.concurrent.locks.LockSupport; import javax.swing.*; +import javax.swing.Timer; import java.awt.*; import java.awt.image.BufferedImage; import java.nio.ByteBuffer; @@ -42,7 +45,7 @@ import java.util.concurrent.atomic.AtomicReference; * @author tzdwindows 7 * @version 1.1 * @since 2025-10-13 - * @see com.chuangzhou.vivid2D.test.TestModelGLPanel + * @see TestModelGLPanel */ public class ModelRenderPanel extends JPanel { private static final Logger logger = LoggerFactory.getLogger(ModelRenderPanel.class); @@ -145,6 +148,17 @@ public class ModelRenderPanel extends JPanel { private static final float CAMERA_Z_MIN = -5.0f; private static final float CAMERA_Z_MAX = 5.0f; + private volatile boolean liquifyMode = false; + private volatile ModelPart liquifyTargetPart = null; + private volatile Mesh2D liquifyTargetMesh = null; + private final Timer doubleClickTimer; + private volatile long lastClickTime = 0; + private static final int DOUBLE_CLICK_INTERVAL = 300; // 双击间隔(毫秒) + + private float liquifyBrushSize = 50.0f; + private float liquifyBrushStrength = 2.0f; + private ModelPart.LiquifyMode currentLiquifyMode = ModelPart.LiquifyMode.PUSH; + // ================== 摄像机控制方法 ================== /** @@ -207,6 +221,11 @@ public class ModelRenderPanel extends JPanel { this.operationHistory = OperationHistoryGlobal.getInstance(); initialize(); initKeyboardShortcuts(); + + doubleClickTimer = new Timer(DOUBLE_CLICK_INTERVAL, e -> { + handleSingleClick(); + }); + doubleClickTimer.setRepeats(false); } /** @@ -220,6 +239,229 @@ public class ModelRenderPanel extends JPanel { this.operationHistory = OperationHistoryGlobal.getInstance(); initialize(); initKeyboardShortcuts(); + doubleClickTimer = new Timer(DOUBLE_CLICK_INTERVAL, e -> { + // 单单击超时处理 + handleSingleClick(); + }); + doubleClickTimer.setRepeats(false); + } + + + + /** + * 处理双击事件 + */ + private void handleDoubleClick(MouseEvent e) { + if (liquifyMode) { + // 如果在液化模式下双击,退出液化模式 + exitLiquifyMode(); + return; + } + + final int screenX = e.getX(); + final int screenY = e.getY(); + + executeInGLContext(() -> { + try { + // 转换屏幕坐标到模型坐标 + float[] modelCoords = screenToModelCoordinates(screenX, screenY); + if (modelCoords == null) return; + + float modelX = modelCoords[0]; + float modelY = modelCoords[1]; + + // 检测双击的网格 + Mesh2D clickedMesh = findMeshAtPosition(modelX, modelY); + if (clickedMesh != null) { + enterLiquifyMode(clickedMesh); + } + + } catch (Exception ex) { + logger.error("处理双击时出错", ex); + } + }); + } + + + /** + * 进入液化模式 + */ + private void enterLiquifyMode(Mesh2D targetMesh) { + liquifyMode = true; + liquifyTargetMesh = targetMesh; + liquifyTargetPart = findPartByMesh(targetMesh); + + if (liquifyTargetPart != null) { + liquifyTargetPart.setStartLiquefy(true); + } + + // 设置液化模式光标 + setCursor(createLiquifyCursor()); + + logger.info("进入液化模式: {}", targetMesh.getName()); + + // 通知监听器 + for (ModelClickListener listener : clickListeners) { + try { + listener.onLiquifyModeEntered(targetMesh, liquifyTargetPart); + } catch (Exception ex) { + logger.error("液化模式进入事件监听器执行出错", ex); + } + } + + repaint(); + } + + /** + * 退出液化模式 + */ + private void exitLiquifyMode() { + executeInGLContext(()->{ + liquifyMode = false; + if (liquifyTargetPart != null) { + liquifyTargetPart.setStartLiquefy(false); + } + liquifyTargetMesh = null; + liquifyTargetPart = null; + + // 恢复默认光标 + updateCursorForHoverState(); + + logger.info("退出液化模式"); + + // 通知监听器 + for (ModelClickListener listener : clickListeners) { + try { + listener.onLiquifyModeExited(); + } catch (Exception ex) { + logger.error("液化模式退出事件监听器执行出错", ex); + } + } + + repaint(); + }); + } + + /** + * 液化模式下的单击处理(应用液化效果) + */ + private void handleLiquifyClick() { + Point mousePos = getMousePosition(); + if (mousePos == null || liquifyTargetPart == null) return; + + logger.debug("液化模式单击: {}", mousePos); + + executeInGLContext(() -> { + try { + float[] modelCoords = screenToModelCoordinates(mousePos.x, mousePos.y); + if (modelCoords == null) return; + + float modelX = modelCoords[0]; + float modelY = modelCoords[1]; + + // 应用液化效果 + Vector2f brushCenter = new Vector2f(modelX, modelY); + + // 判断是否按住Ctrl键,决定是否创建顶点 + boolean createVertices = ctrlPressed; + + liquifyTargetPart.applyLiquify(brushCenter, liquifyBrushSize, + liquifyBrushStrength, currentLiquifyMode, 1, createVertices); + + //logger.info("应用液化效果: 位置({}, {}), 大小{}, 强度{}, 模式{}, 创建顶点{}", + // modelX, modelY, liquifyBrushSize, liquifyBrushStrength, + // currentLiquifyMode, createVertices); + + } catch (Exception ex) { + logger.error("应用液化效果时出错", ex); + } + }); + } + + /** + * 创建液化模式光标 + */ + private Cursor createLiquifyCursor() { + // 创建自定义液化光标(圆圈) + int size = 32; + BufferedImage cursorImg = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = cursorImg.createGraphics(); + + // 设置抗锯齿 + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + + // 绘制透明背景 + g2d.setColor(new Color(0, 0, 0, 0)); + g2d.fillRect(0, 0, size, size); + + // 绘制圆圈 + int center = size / 2; + int radius = (int)(liquifyBrushSize * 0.1f); // 根据画笔大小缩放光标 + + // 外圈 + g2d.setColor(Color.RED); + g2d.setStroke(new BasicStroke(2f)); + g2d.drawOval(center - radius, center - radius, radius * 2, radius * 2); + + // 内圈 + g2d.setColor(new Color(255, 100, 100, 150)); + g2d.setStroke(new BasicStroke(1f)); + g2d.drawOval(center - radius / 2, center - radius / 2, radius, radius); + + // 中心点 + g2d.setColor(Color.RED); + g2d.fillOval(center - 2, center - 2, 4, 4); + + g2d.dispose(); + + return Toolkit.getDefaultToolkit().createCustomCursor(cursorImg, new Point(center, center), "LiquifyCursor"); + } + + /** + * 设置液化画笔大小 + */ + public void setLiquifyBrushSize(float size) { + this.liquifyBrushSize = Math.max(1.0f, Math.min(500.0f, size)); + if (liquifyMode) { + setCursor(createLiquifyCursor()); + } + } + + /** + * 设置液化画笔强度 + */ + public void setLiquifyBrushStrength(float strength) { + this.liquifyBrushStrength = Math.max(0.0f, Math.min(2.0f, strength)); + } + + /** + * 设置液化模式 + */ + public void setLiquifyMode(ModelPart.LiquifyMode mode) { + this.currentLiquifyMode = mode; + } + + /** + * 获取当前液化状态 + */ + public boolean isInLiquifyMode() { + return liquifyMode; + } + + /** + * 获取液化目标网格 + */ + public Mesh2D getLiquifyTargetMesh() { + return liquifyTargetMesh; + } + + /** + * 处理单单击事件 + */ + private void handleSingleClick() { + if (liquifyMode) { + handleLiquifyClick(); + } } /** @@ -538,8 +780,14 @@ public class ModelRenderPanel extends JPanel { */ public void clearSelectedMeshes() { executeInGLContext(() -> { + // 如果当前在液化模式,先退出液化模式 + if (liquifyMode) { + exitLiquifyMode(); + } + for (Mesh2D mesh : selectedMeshes) { mesh.setSelected(false); + mesh.setSuspension(false); mesh.clearMultiSelection(); } selectedMeshes.clear(); @@ -616,7 +864,7 @@ public class ModelRenderPanel extends JPanel { if (model == null) return allMeshes; try { - java.util.List parts = model.getParts(); + List parts = model.getParts(); if (parts == null) return allMeshes; for (ModelPart part : parts) { @@ -638,7 +886,7 @@ public class ModelRenderPanel extends JPanel { if (part == null) return; // 添加当前部件的网格 - java.util.List meshes = part.getMeshes(); + List meshes = part.getMeshes(); if (meshes != null) { for (Mesh2D mesh : meshes) { if (mesh != null && mesh.isVisible()) { @@ -697,9 +945,9 @@ public class ModelRenderPanel extends JPanel { // 创建渲染线程 startRendering(); - this.addComponentListener(new java.awt.event.ComponentAdapter() { + this.addComponentListener(new ComponentAdapter() { @Override - public void componentResized(java.awt.event.ComponentEvent e) { + public void componentResized(ComponentEvent e) { int w = getWidth(); int h = getHeight(); if (w <= 0 || h <= 0) return; @@ -813,6 +1061,10 @@ public class ModelRenderPanel extends JPanel { shiftPressed = true; } else if (e.getKeyCode() == KeyEvent.VK_CONTROL) { ctrlPressed = true; + if (liquifyMode && liquifyTargetMesh != null) { + liquifyTargetMesh.setRenderVertices(true); + logger.debug("液化模式下按住Ctrl,开启顶点渲染"); + } } else if (e.getKeyCode() == KeyEvent.VK_A && ctrlPressed) { // Ctrl+A 全选 e.consume(); // 阻止默认行为 @@ -826,6 +1078,10 @@ public class ModelRenderPanel extends JPanel { shiftPressed = false; } else if (e.getKeyCode() == KeyEvent.VK_CONTROL) { ctrlPressed = false; + if (liquifyMode && liquifyTargetMesh != null) { + liquifyTargetMesh.setRenderVertices(false); + logger.debug("液化模式下松开Ctrl,关闭顶点渲染"); + } } } }); @@ -897,11 +1153,11 @@ public class ModelRenderPanel extends JPanel { */ private void handleMousePressed(MouseEvent e) { if (!contextInitialized) return; - final int screenX = e.getX(); final int screenY = e.getY(); requestFocusInWindow(); + // 首先处理中键拖拽(摄像机控制),在任何模式下都可用 if (SwingUtilities.isMiddleMouseButton(e)) { cameraDragging = true; lastCameraDragX = screenX; @@ -917,6 +1173,23 @@ public class ModelRenderPanel extends JPanel { return; } + // 液化模式下的左键处理 + if (liquifyMode && SwingUtilities.isLeftMouseButton(e)) { + // 在液化模式下,左键按下直接开始液化操作 + currentDragMode = DragMode.NONE; // 液化模式使用特殊拖拽逻辑 + logger.debug("液化模式下开始拖拽"); + + // 立即应用一次液化效果 + handleLiquifyClick(); + return; + } + + // 非液化模式的正常处理 + if (liquifyMode) { + // 液化模式下只处理左键和中键,其他按钮忽略 + return; + } + shiftDuringDrag = e.isShiftDown(); executeInGLContext(() -> { @@ -1375,6 +1648,14 @@ public class ModelRenderPanel extends JPanel { }); return; } + + // 液化模式下的拖拽处理(优先于其他模式) + if (liquifyMode && SwingUtilities.isLeftMouseButton(e)) { + handleLiquifyDrag(e); + return; + } + + // 普通模式下的拖拽处理 if (currentDragMode == DragMode.NONE) return; final int screenX = e.getX(); @@ -1413,6 +1694,41 @@ public class ModelRenderPanel extends JPanel { }); } + /** + * 液化模式下的拖拽处理(连续应用液化效果) + */ + private void handleLiquifyDrag(MouseEvent e) { + if (!liquifyMode || liquifyTargetPart == null) return; + + final int screenX = e.getX(); + final int screenY = e.getY(); + + executeInGLContext(() -> { + try { + float[] modelCoords = screenToModelCoordinates(screenX, screenY); + if (modelCoords == null) return; + + float modelX = modelCoords[0]; + float modelY = modelCoords[1]; + + // 应用液化效果 + Vector2f brushCenter = new Vector2f(modelX, modelY); + + // 判断是否按住Ctrl键,决定是否创建顶点 + boolean createVertices = ctrlPressed; + + liquifyTargetPart.applyLiquify(brushCenter, liquifyBrushSize, + liquifyBrushStrength, currentLiquifyMode, 1, createVertices); + + //logger.info("拖拽应用液化效果: 位置({}, {}), 大小{}, 强度{}, 模式{}, 创建顶点{}", + // modelX, modelY, liquifyBrushSize, liquifyBrushStrength, + // currentLiquifyMode, createVertices); + + } catch (Exception ex) { + logger.error("应用液化拖拽效果时出错", ex); + } + }); + } /** * 处理移动中心点拖拽 @@ -1588,12 +1904,22 @@ public class ModelRenderPanel extends JPanel { * 处理鼠标释放事件(结束拖拽并记录操作历史) */ private void handleMouseReleased(MouseEvent e) { - if (cameraDragging && SwingUtilities.isMiddleMouseButton(e)) { + // 首先处理摄像机拖拽释放 + if (cameraDragging && (SwingUtilities.isMiddleMouseButton(e) || liquifyMode)) { cameraDragging = false; // 恢复悬停状态的光标 updateCursorForHoverState(); return; } + + // 液化模式下的释放处理 + if (liquifyMode && SwingUtilities.isLeftMouseButton(e)) { + // 液化模式下不需要记录操作历史,直接重置状态 + currentDragMode = DragMode.NONE; + logger.debug("液化拖拽结束"); + return; + } + if (currentDragMode != DragMode.NONE) { // 记录操作历史 //executeInGLContext(() -> { @@ -1693,37 +2019,47 @@ public class ModelRenderPanel extends JPanel { final int screenX = e.getX(); final int screenY = e.getY(); - // 在 GL 上下文线程中执行点击检测 - executeInGLContext(() -> { - try { - // 转换屏幕坐标到模型坐标 - float[] modelCoords = screenToModelCoordinates(screenX, screenY); - if (modelCoords == null) return; + long currentTime = System.currentTimeMillis(); + boolean isDoubleClick = (currentTime - lastClickTime) < DOUBLE_CLICK_INTERVAL; + lastClickTime = currentTime; - float modelX = modelCoords[0]; - float modelY = modelCoords[1]; + if (isDoubleClick) { + // 取消单单击计时器 + doubleClickTimer.stop(); + handleDoubleClick(e); + } else { + executeInGLContext(() -> { + try { + // 转换屏幕坐标到模型坐标 + float[] modelCoords = screenToModelCoordinates(screenX, screenY); + if (modelCoords == null) return; - logger.debug("点击位置:({}, {})", modelX, modelY); - // 检测点击的网格 - Mesh2D clickedMesh = findMeshAtPosition(modelX, modelY); + float modelX = modelCoords[0]; + float modelY = modelCoords[1]; - if (clickedMesh == null && !e.isControlDown() && !e.isShiftDown()) { - clearSelectedMeshes(); - logger.debug("点击空白处,取消所有选择"); - } + logger.debug("点击位置:({}, {})", modelX, modelY); + // 检测点击的网格 + Mesh2D clickedMesh = findMeshAtPosition(modelX, modelY); - // 触发点击事件 - for (ModelClickListener listener : clickListeners) { - try { - listener.onModelClicked(clickedMesh, modelX, modelY, screenX, screenY); - } catch (Exception ex) { - logger.error("点击事件监听器执行出错", ex); + if (clickedMesh == null && !e.isControlDown() && !e.isShiftDown()) { + clearSelectedMeshes(); + logger.debug("点击空白处,取消所有选择"); } + + // 触发点击事件 + for (ModelClickListener listener : clickListeners) { + try { + listener.onModelClicked(clickedMesh, modelX, modelY, screenX, screenY); + } catch (Exception ex) { + logger.error("点击事件监听器执行出错", ex); + } + } + } catch (Exception ex) { + logger.error("处理鼠标点击时出错", ex); } - } catch (Exception ex) { - logger.error("处理鼠标点击时出错", ex); - } - }); + }); + doubleClickTimer.restart(); + } } /** @@ -1756,6 +2092,7 @@ public class ModelRenderPanel extends JPanel { if (newHoveredMesh != hoveredMesh) { if (hoveredMesh != null) { hoveredMesh.setSuspension(false); + hoveredMesh.setRenderVertices(false); } hoveredMesh = newHoveredMesh; @@ -1891,7 +2228,7 @@ public class ModelRenderPanel extends JPanel { float checkX = modelX; float checkY = modelY; - java.util.List parts = model.getParts(); + List parts = model.getParts(); if (parts == null || parts.isEmpty()) { return null; } @@ -1901,7 +2238,7 @@ public class ModelRenderPanel extends JPanel { ModelPart part = parts.get(i); if (part == null || !part.isVisible()) continue; - java.util.List meshes = part.getMeshes(); + List meshes = part.getMeshes(); if (meshes == null || meshes.isEmpty()) continue; for (int m = meshes.size() - 1; m >= 0; m--) { @@ -1940,7 +2277,7 @@ public class ModelRenderPanel extends JPanel { if (model == null) return null; try { - java.lang.reflect.Method getBoundsMethod = model.getClass().getMethod("getBounds"); + Method getBoundsMethod = model.getClass().getMethod("getBounds"); return (BoundingBox) getBoundsMethod.invoke(model); } catch (Exception e) { logger.debug("无法获取模型边界", e); @@ -1955,8 +2292,8 @@ public class ModelRenderPanel extends JPanel { if (model == null) return null; try { - java.lang.reflect.Method getMeshesMethod = model.getClass().getMethod("getMeshes"); - java.util.List meshes = (java.util.List) getMeshesMethod.invoke(model); + Method getMeshesMethod = model.getClass().getMethod("getMeshes"); + List meshes = (List) getMeshesMethod.invoke(model); return meshes.isEmpty() ? null : meshes.get(0); } catch (Exception e) { return null; diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationHistoryGlobal.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationHistoryGlobal.java index 1dc57f3..0e68d13 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationHistoryGlobal.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationHistoryGlobal.java @@ -589,6 +589,9 @@ public class OperationHistoryGlobal { case "BATCH_TRANSFORM": executeBatchTransform(params); break; + case "LIQUIFY": + executeLiquify(params); + break; default: LOGGER.debug("执行操作: {}", operationType); break; @@ -631,6 +634,9 @@ public class OperationHistoryGlobal { case "BATCH_TRANSFORM": undoBatchTransform(params); break; + case "LIQUIFY": + undoLiquify(params); + break; default: //System.out.println("撤回操作: " + operationType); break; @@ -639,6 +645,64 @@ public class OperationHistoryGlobal { // ============ 批量变换操作方法 ============ + private void executeLiquify(Object... params) { + // 重做液化操作:应用液化后的状态 + if (params.length >= 2) { + @SuppressWarnings("unchecked") + Map afterStates = + (Map) params[1]; + applyLiquifyStates(afterStates); + } + } + + private void undoLiquify(Object... params) { + // 撤回液化操作:恢复液化前的状态 + if (params.length >= 1) { + @SuppressWarnings("unchecked") + Map beforeStates = + (Map) params[0]; + applyLiquifyStates(beforeStates); + } + } + + private void applyLiquifyStates(Map states) { + if (states == null || states.isEmpty()) { + return; + } + + for (Map.Entry entry : states.entrySet()) { + Mesh2D mesh = entry.getKey(); + OperationHistoryGlobal.MeshState state = entry.getValue(); + + if (mesh == null) { + continue; + } + + try { + // 恢复顶点数据 + if (state.vertices != null && state.vertices.length > 0) { + mesh.setVertices(state.vertices, true); // true表示同时更新originalVertices + } else { + } + + // 恢复原始顶点数据(作为备份) + if (state.originalVertices != null && state.originalVertices.length > 0) { + mesh.setOriginalVertices(state.originalVertices); + } + + // 恢复原始中心点 + if (state.originalPivot != null) { + mesh.setOriginalPivot(state.originalPivot); + } + + // 强制更新边界 + mesh.updateBounds(); + + } catch (Exception e) { + } + } + } + private void handleBatchTransformRecord(Object... params) { if (params.length >= 3 && params[0] instanceof ModelPart) { ModelPart part = (ModelPart) params[0]; @@ -1126,14 +1190,14 @@ public class OperationHistoryGlobal { } public static class MeshState { - String name; - float[] vertices; - float[] originalVertices; - Vector2f originalPivot; + public String name; + public float[] vertices; + public float[] originalVertices; + public Vector2f originalPivot; Object texture; - MeshState(String name, float[] vertices, float[] originalVertices, - Vector2f originalPivot, Object texture) { + public MeshState(String name, float[] vertices, float[] originalVertices, + Vector2f originalPivot, Object texture) { this.name = name; this.vertices = vertices != null ? vertices.clone() : null; this.originalVertices = originalVertices != null ? originalVertices.clone() : null; 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 227aeab..fed5627 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java @@ -1,5 +1,6 @@ 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.systems.Matrix3fUtils; @@ -9,9 +10,7 @@ import org.joml.Vector2f; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; +import java.util.*; import javax.swing.tree.DefaultMutableTreeNode; /** @@ -54,6 +53,7 @@ public class ModelPart { private final List events = new ArrayList<>(); private boolean inMultiSelectionOperation = false; + private boolean startLiquefy =false; // ====== 液化模式枚举 ====== public enum LiquifyMode { @@ -115,6 +115,20 @@ public class ModelPart { } } + /** + * 设置液化状态 + */ + public void setStartLiquefy(boolean startLiquefy) { + this.startLiquefy = startLiquefy; + + // 同步到所有网格 + for (Mesh2D mesh : meshes) { + mesh.setShowLiquifyOverlay(startLiquefy); + } + + triggerEvent("liquifyModeChanged"); + } + // ==================== 多选支持 ==================== /** @@ -512,7 +526,7 @@ public class ModelPart { if (stroke == null || stroke.points == null) return; LiquifyMode mode = stroke.mode != null ? stroke.mode : LiquifyMode.PUSH; for (LiquifyPoint p : stroke.points) { - applyLiquify(new Vector2f(p.x, p.y), stroke.radius, stroke.strength, mode, stroke.iterations); + applyLiquify(new Vector2f(p.x, p.y), stroke.radius, stroke.strength, mode, stroke.iterations,true); } } @@ -528,15 +542,31 @@ public class ModelPart { * * 该方法会直接修改 mesh 的顶点并更新其边界(mesh.updateBounds)。 */ - public void applyLiquify(Vector2f brushCenter, float radius, float strength, LiquifyMode mode, int iterations) { + public void applyLiquify(Vector2f brushCenter, float radius, float strength, LiquifyMode mode, int iterations, boolean createVertices) { if (radius <= 0f || strength == 0f || iterations <= 0) return; - // 限制 strength 到合理范围,避免过大造成畸变 - float s = Math.max(-5f, Math.min(5f, strength)); + logger.debug("应用液化效果: 中心({}, {}), 半径{}, 强度{}, 模式{}, 迭代{}, 创建顶点{}", + brushCenter.x, brushCenter.y, radius, strength, mode, iterations, createVertices); + + // 限制 strength 到合理范围 + float s = Math.max(-2f, Math.min(2f, strength)); // 随机用于 Turbulence java.util.Random rand = new java.util.Random(); + // 记录液化前的网格状态用于操作历史 + Map beforeStates = new HashMap<>(); + for (Mesh2D mesh : meshes) { + if (mesh.getVertexCount() > 0) { + beforeStates.put(mesh, createMeshState(mesh)); + } + } + + // 只在真正需要时添加顶点,避免破坏网格结构 + //if (createVertices) { + // addMinimalVerticesIfNeeded(brushCenter, radius); + //} + // 迭代多个小步以获得平滑结果 for (int iter = 0; iter < iterations; iter++) { // 对每个网格执行液化 @@ -544,132 +574,895 @@ public class ModelPart { int vc = mesh.getVertexCount(); if (vc <= 0) continue; - // 预取顶点副本,以便在单次迭代中使用原始邻域数据(避免串联影响) - Vector2f[] original = new Vector2f[vc]; - for (int i = 0; i < vc; i++) { - Vector2f v = mesh.getVertex(i); - original[i] = new Vector2f(v); + // 获取原始顶点数据 + float[] originalVertices = mesh.getOriginalVertices(); + if (originalVertices == null || originalVertices.length == 0) { + logger.warn("网格 {} 没有原始顶点数据", mesh.getName()); + continue; } - // 对每个顶点计算影响 - for (int i = 0; i < vc; i++) { - Vector2f vOrig = original[i]; - float dx = vOrig.x - brushCenter.x; - float dy = vOrig.y - brushCenter.y; - float dist = (float) Math.hypot(dx, dy); + // 创建顶点副本用于处理 + float[] processedVertices = originalVertices.clone(); - if (dist > radius) continue; + // 找到画笔区域内需要影响的顶点 + List influencedVertices = findVerticesToInfluence(mesh, brushCenter, radius); - // falloff 使用平滑步进(smoothstep) - float t = 1.0f - (dist / radius); // 0..1, 1 在中心 - float falloff = t * t * (3f - 2f * t); // smoothstep + // 对每个需要影响的顶点计算影响 + for (VertexInfluence vi : influencedVertices) { + int i = vi.vertexIndex; + Vector2f vertexPos = new Vector2f(processedVertices[i * 2], processedVertices[i * 2 + 1]); + + // 将局部坐标转换为世界坐标 + Vector2f worldPos = Matrix3fUtils.transformPoint(worldTransform, vertexPos); + + float dx = worldPos.x - brushCenter.x; + float dy = worldPos.y - brushCenter.y; + float dist = (float) Math.sqrt(dx * dx + dy * dy); + + // 使用软边界 + float softRadius = radius * 1.3f; + if (dist > softRadius) continue; + + // 改进的falloff计算,支持软边界 + float t; + if (dist <= radius) { + t = 1.0f - (dist / radius); + } else { + t = 1.0f - ((dist - radius) / (softRadius - radius)); + t = Math.max(0.01f, t); + } + float falloff = t * t * (3f - 2f * t); // 基本影响量 - float influence = falloff * s; + float influence = falloff * s * 10.0f; - // 目标点(工作副本) - Vector2f vNew = new Vector2f(vOrig); + // 目标点(局部坐标) + Vector2f newLocalPos = new Vector2f(vertexPos); switch (mode) { case PUSH -> { - // 推开:沿着从中心到点的方向推动(与 PS push 含义一致) if (dist < 1e-6f) { - // 随机方向避免除0 float ang = rand.nextFloat() * (float) (2.0 * Math.PI); - vNew.x += (float) Math.cos(ang) * influence; - vNew.y += (float) Math.sin(ang) * influence; + newLocalPos.x += (float) Math.cos(ang) * influence * 0.1f; + newLocalPos.y += (float) Math.sin(ang) * influence * 0.1f; } else { - vNew.x += (dx / dist) * influence * (radius * 0.02f); - vNew.y += (dy / dist) * influence * (radius * 0.02f); + float dirX = dx / dist; + float dirY = dy / dist; + Vector2f worldDisplacement = new Vector2f(dirX * influence * 0.1f, dirY * influence * 0.1f); + Vector2f localDisplacement = Matrix3fUtils.transformVectorInverse(worldTransform, worldDisplacement); + newLocalPos.add(localDisplacement); } } case PULL -> { - // 拉近:沿着从点到中心的方向拉动(和 PUSH 相反) if (dist < 1e-6f) { float ang = rand.nextFloat() * (float) (2.0 * Math.PI); - vNew.x -= (float) Math.cos(ang) * influence; - vNew.y -= (float) Math.sin(ang) * influence; + newLocalPos.x -= (float) Math.cos(ang) * influence * 0.1f; + newLocalPos.y -= (float) Math.sin(ang) * influence * 0.1f; } else { - vNew.x -= (dx / dist) * influence * (radius * 0.02f); - vNew.y -= (dy / dist) * influence * (radius * 0.02f); + float dirX = -dx / dist; + float dirY = -dy / dist; + Vector2f worldDisplacement = new Vector2f(dirX * influence * 0.1f, dirY * influence * 0.1f); + Vector2f localDisplacement = Matrix3fUtils.transformVectorInverse(worldTransform, worldDisplacement); + newLocalPos.add(localDisplacement); } } case SWIRL_CW, SWIRL_CCW -> { - // 旋转:绕画笔中心旋转一定角度,距离越近角度越大 float dir = (mode == LiquifyMode.SWIRL_CW) ? -1f : 1f; - // 角度基于 influence(建议 strength 为接近 1 时产生 0.5-1 弧度级别) - float maxAngle = 1.0f * s; // 可调:s 控制总体角度 - float angle = dir * maxAngle * falloff; - vNew = rotateAround(vOrig, brushCenter, angle); + float maxAngle = 1.0f * s * falloff; + float angle = dir * maxAngle; + Vector2f rotatedWorldPos = rotateAround(worldPos, brushCenter, angle); + Vector2f newPos = Matrix3fUtils.transformPointInverse(worldTransform, rotatedWorldPos); + newLocalPos.set(newPos); } case BLOAT -> { - // 鼓起:远离中心(按比例放大) if (dist < 1e-6f) { - // 随机方向扩大 float ang = rand.nextFloat() * (float) (2.0 * Math.PI); - vNew.x += (float) Math.cos(ang) * Math.abs(influence); - vNew.y += (float) Math.sin(ang) * Math.abs(influence); + newLocalPos.x += (float) Math.cos(ang) * Math.abs(influence) * 0.05f; + newLocalPos.y += (float) Math.sin(ang) * Math.abs(influence) * 0.05f; } else { - float factor = 1.0f + Math.abs(influence) * 0.08f; // scale multiplier - vNew.x = brushCenter.x + (vOrig.x - brushCenter.x) * factor * (1.0f * falloff + (1.0f - falloff)); - vNew.y = brushCenter.y + (vOrig.y - brushCenter.y) * factor * (1.0f * falloff + (1.0f - falloff)); + float factor = 1.0f + Math.abs(influence) * 0.1f; + Vector2f scaledWorldPos = new Vector2f( + brushCenter.x + (worldPos.x - brushCenter.x) * factor, + brushCenter.y + (worldPos.y - brushCenter.y) * factor + ); + Vector2f newPos = Matrix3fUtils.transformPointInverse(worldTransform, scaledWorldPos); + newLocalPos.set(newPos); } } case PINCH -> { - // 收缩:向中心缩放 if (dist < 1e-6f) { float ang = rand.nextFloat() * (float) (2.0 * Math.PI); - vNew.x -= (float) Math.cos(ang) * Math.abs(influence); - vNew.y -= (float) Math.sin(ang) * Math.abs(influence); + newLocalPos.x -= (float) Math.cos(ang) * Math.abs(influence) * 0.05f; + newLocalPos.y -= (float) Math.sin(ang) * Math.abs(influence) * 0.05f; } else { - float factor = 1.0f - Math.abs(influence) * 0.08f; - factor = Math.max(0.01f, factor); - vNew.x = brushCenter.x + (vOrig.x - brushCenter.x) * factor * (1.0f * falloff + (1.0f - falloff)); - vNew.y = brushCenter.y + (vOrig.y - brushCenter.y) * factor * (1.0f * falloff + (1.0f - falloff)); + float factor = 1.0f - Math.abs(influence) * 0.1f; + factor = Math.max(0.1f, factor); + Vector2f scaledWorldPos = new Vector2f( + brushCenter.x + (worldPos.x - brushCenter.x) * factor, + brushCenter.y + (worldPos.y - brushCenter.y) * factor + ); + Vector2f newPos = Matrix3fUtils.transformPointInverse(worldTransform, scaledWorldPos); + newLocalPos.set(newPos); } } case SMOOTH -> { - // 平滑:计算邻域点的平均并向平均点移动 Vector2f avg = new Vector2f(0f, 0f); int count = 0; - float neighborRadius = Math.max(1.0f, radius * 0.25f); + float neighborRadius = Math.max(5.0f, radius * 0.3f); + for (int j = 0; j < vc; j++) { - Vector2f vj = original[j]; - if (vOrig.distance(vj) <= neighborRadius) { - avg.add(vj); + if (j == i) continue; + Vector2f otherVertex = new Vector2f(processedVertices[j * 2], processedVertices[j * 2 + 1]); + Vector2f otherWorldPos = Matrix3fUtils.transformPoint(worldTransform, otherVertex); + if (worldPos.distance(otherWorldPos) <= neighborRadius) { + avg.add(otherVertex); count++; } } + if (count > 0) { avg.mul(1.0f / count); - // 向平均位置移动,强度受 influence 控制 - vNew.x = vOrig.x + (avg.x - vOrig.x) * (Math.abs(influence) * 0.5f * falloff); - vNew.y = vOrig.y + (avg.y - vOrig.y) * (Math.abs(influence) * 0.5f * falloff); + float blendFactor = Math.abs(influence) * 0.3f * falloff; + newLocalPos.x = newLocalPos.x + (avg.x - newLocalPos.x) * blendFactor; + newLocalPos.y = newLocalPos.y + (avg.y - newLocalPos.y) * blendFactor; } } case TURBULENCE -> { - // 湍流:基于噪声/随机的小位移叠加 - float jitter = (rand.nextFloat() * 2f - 1f) * Math.abs(influence) * radius * 0.005f; - float jitter2 = (rand.nextFloat() * 2f - 1f) * Math.abs(influence) * radius * 0.005f; - vNew.x += jitter * falloff; - vNew.y += jitter2 * falloff; + float jitter = (rand.nextFloat() * 2f - 1f) * Math.abs(influence) * 0.5f; + float jitter2 = (rand.nextFloat() * 2f - 1f) * Math.abs(influence) * 0.5f; + newLocalPos.x += jitter * falloff; + newLocalPos.y += jitter2 * falloff; } default -> { /* no-op */ } } - // 写回顶点(直接覆盖) - mesh.setVertex(i, vNew.x, vNew.y); - } // end for vertices + // 更新处理后的顶点 + processedVertices[i * 2] = newLocalPos.x; + processedVertices[i * 2 + 1] = newLocalPos.y; + } + + // 更新网格的原始顶点数据 + mesh.setOriginalVertices(processedVertices); + updateMeshVertices(mesh); // 更新网格边界 try { mesh.updateBounds(); - } catch (Exception ignored) { } - } // end for meshes + logger.debug("网格 {} 边界已更新", mesh.getName()); + } catch (Exception e) { + logger.warn("更新网格边界时出错: {}", e.getMessage()); + } + } // 标记需要重新计算边界 this.boundsDirty = true; - } // end iterations + } + + // 记录液化操作到历史记录 + if (!beforeStates.isEmpty()) { + recordLiquifyOperation(beforeStates, brushCenter, radius, strength, mode, createVertices); + } + + logger.debug("液化效果应用完成,影响 {} 个网格", meshes.size()); + } + + /** + * 检查画笔区域内是否有足够的顶点 + */ + private boolean hasSufficientVerticesInArea(Mesh2D mesh, Vector2f brushCenter, float radius) { + int verticesInArea = 0; + float[] vertices = mesh.getOriginalVertices(); + + for (int i = 0; i < vertices.length / 2; i++) { + Vector2f vertexPos = new Vector2f(vertices[i * 2], vertices[i * 2 + 1]); + Vector2f worldPos = Matrix3fUtils.transformPoint(worldTransform, vertexPos); + float dist = worldPos.distance(brushCenter); + + if (dist <= radius) { + verticesInArea++; + if (verticesInArea >= 3) { // 至少有3个顶点在区域内 + return true; + } + } + } + + return false; + } + + /** + * 在画笔中心添加单个顶点 + */ + private void addSingleVertexAtBrushCenter(Mesh2D mesh, Vector2f brushCenter) { + float[] originalVertices = mesh.getOriginalVertices(); + float[] originalUVs = mesh.getUVs(); + int[] originalIndices = mesh.getIndices(); + + if (originalVertices == null || originalUVs == null || originalIndices == null) { + return; + } + + // 转换画笔中心到局部坐标 + Vector2f localBrushCenter = Matrix3fUtils.transformPointInverse(worldTransform, brushCenter); + + // 检查是否已经有顶点在附近 + if (hasVertexNearby(originalVertices, localBrushCenter, 5.0f)) { + return; // 已经有顶点在附近,不需要添加 + } + + // 创建新的顶点数组 + int newVertexCount = originalVertices.length / 2 + 1; + float[] newVertices = new float[newVertexCount * 2]; + float[] newUVs = new float[newVertexCount * 2]; + + // 复制原有数据 + System.arraycopy(originalVertices, 0, newVertices, 0, originalVertices.length); + System.arraycopy(originalUVs, 0, newUVs, 0, originalUVs.length); + + // 添加新顶点 + newVertices[originalVertices.length] = localBrushCenter.x; + newVertices[originalVertices.length + 1] = localBrushCenter.y; + + // 估算新顶点的UV坐标 + Vector2f newUV = estimateUVForPosition(localBrushCenter.x, localBrushCenter.y, mesh); + newUVs[originalUVs.length] = newUV.x; + newUVs[originalUVs.length + 1] = newUV.y; + + // 创建新的索引数组 - 保持原有索引不变 + // 注意:这里不自动连接新顶点,避免破坏网格结构 + // 新顶点将在后续的液化操作中被移动,但不会影响整体网格拓扑 + int[] newIndices = originalIndices.clone(); + + // 设置新的网格数据 + mesh.setMeshData(newVertices, newUVs, newIndices); + logger.debug("在画笔中心添加单个顶点: ({}, {})", localBrushCenter.x, localBrushCenter.y); + } + + /** + * 检查指定位置附近是否已有顶点 + */ + private boolean hasVertexNearby(float[] vertices, Vector2f position, float threshold) { + for (int i = 0; i < vertices.length / 2; i++) { + float dx = vertices[i * 2] - position.x; + float dy = vertices[i * 2 + 1] - position.y; + if (dx * dx + dy * dy < threshold * threshold) { + return true; + } + } + return false; + } + + /** + * 为指定位置估算UV坐标 + */ + private Vector2f estimateUVForPosition(float x, float y, Mesh2D mesh) { + BoundingBox bounds = mesh.getBounds(); + if (bounds != null && bounds.isValid()) { + float u = (x - bounds.getMinX()) / bounds.getWidth(); + float v = (y - bounds.getMinY()) / bounds.getHeight(); + // 限制在[0,1]范围内 + u = Math.max(0, Math.min(1, u)); + v = Math.max(0, Math.min(1, v)); + return new Vector2f(u, v); + } + return new Vector2f(0.5f, 0.5f); + } + + /** + * 在画笔区域内全面添加顶点,确保整个区域都可以被液化 + */ + private void addComprehensiveVerticesToBrushArea(Vector2f brushCenter, float radius) { + for (Mesh2D mesh : meshes) { + if (mesh.getVertexCount() <= 0) continue; + + float[] originalVertices = mesh.getOriginalVertices(); + float[] originalUVs = mesh.getUVs(); + int[] originalIndices = mesh.getIndices(); + + // 检查数组长度是否有效 + if (originalVertices == null || originalUVs == null || originalIndices == null) { + continue; + } + + // 确保顶点和UV数组长度匹配 + if (originalVertices.length / 2 != originalUVs.length / 2) { + logger.warn("网格 {} 的顶点和UV数量不匹配: 顶点={}, UV={}", + mesh.getName(), originalVertices.length / 2, originalUVs.length / 2); + continue; + } + + List newVertices = new ArrayList<>(); + List newUVs = new ArrayList<>(); + List newIndices = new ArrayList<>(); + + // 将现有数据添加到列表 + int vertexCount = originalVertices.length / 2; + for (int i = 0; i < vertexCount; i++) { + if (i * 2 + 1 < originalVertices.length) { + newVertices.add(new Vector2f(originalVertices[i * 2], originalVertices[i * 2 + 1])); + } + if (i * 2 + 1 < originalUVs.length) { + newUVs.add(new Vector2f(originalUVs[i * 2], originalUVs[i * 2 + 1])); + } + } + + // 复制索引数组 + for (int index : originalIndices) { + if (index < vertexCount) { + newIndices.add(index); + } + } + + // 转换画笔中心到局部坐标 + Vector2f localBrushCenter = Matrix3fUtils.transformPointInverse(worldTransform, brushCenter); + + // 获取网格边界 + BoundingBox meshBounds = mesh.getBounds(); + if (meshBounds == null || !meshBounds.isValid()) { + logger.warn("网格 {} 的边界无效,无法添加顶点", mesh.getName()); + continue; + } + + // 计算画笔区域在局部坐标系中的边界 + float localRadius = radius / Math.max(scale.x, scale.y); // 粗略估算局部半径 + float brushMinX = localBrushCenter.x - localRadius; + float brushMaxX = localBrushCenter.x + localRadius; + float brushMinY = localBrushCenter.y - localRadius; + float brushMaxY = localBrushCenter.y + localRadius; + + // 与网格边界求交 + float effectiveMinX = Math.max(brushMinX, meshBounds.getMinX()); + float effectiveMaxX = Math.min(brushMaxX, meshBounds.getMaxX()); + float effectiveMinY = Math.max(brushMinY, meshBounds.getMinY()); + float effectiveMaxY = Math.min(brushMaxY, meshBounds.getMaxY()); + + // 如果画笔区域与网格没有交集,跳过 + if (effectiveMinX >= effectiveMaxX || effectiveMinY >= effectiveMaxY) { + logger.debug("画笔区域与网格 {} 无交集,跳过添加顶点", mesh.getName()); + continue; + } + + // 计算合理的顶点密度 + float brushAreaWidth = effectiveMaxX - effectiveMinX; + float brushAreaHeight = effectiveMaxY - effectiveMinY; + float brushArea = brushAreaWidth * brushAreaHeight; + + // 根据画笔大小和网格复杂度计算顶点密度 + int targetVertexCount = Math.max(4, Math.min(20, (int)(brushArea / (localRadius * localRadius * 0.5f)))); + + // 在画笔区域内均匀添加顶点 + boolean addedVertices = addUniformVerticesInArea(mesh, newVertices, newUVs, newIndices, + effectiveMinX, effectiveMinY, effectiveMaxX, effectiveMaxY, targetVertexCount, localBrushCenter); + + // 如果添加了顶点,更新网格数据 + if (addedVertices) { + updateMeshWithNewVertices(mesh, newVertices, newUVs, newIndices); + logger.info("网格 {} 在画笔区域添加顶点完成,总顶点数: {}", mesh.getName(), newVertices.size()); + } + } + } + + /** + * 在指定区域内均匀添加顶点 + */ + private boolean addUniformVerticesInArea(Mesh2D mesh, List vertices, List uvs, + List indices, float minX, float minY, float maxX, float maxY, + int targetCount, Vector2f brushCenter) { + boolean addedAny = false; + + // 计算网格步长 + float width = maxX - minX; + float height = maxY - minY; + int gridX = (int) Math.sqrt(targetCount * (width / height)); + int gridY = (int) Math.sqrt(targetCount * (height / width)); + + gridX = Math.max(2, gridX); + gridY = Math.max(2, gridY); + + float stepX = width / (gridX - 1); + float stepY = height / (gridY - 1); + + // 在网格交点处添加顶点 + for (int i = 0; i < gridX; i++) { + for (int j = 0; j < gridY; j++) { + float x = minX + i * stepX; + float y = minY + j * stepY; + + // 检查该位置是否已经有顶点(在一定阈值内) + if (!hasVertexNearby(vertices, x, y, Math.min(stepX, stepY) * 0.3f)) { + int newIndex = vertices.size(); + vertices.add(new Vector2f(x, y)); + + // 估算UV坐标 + Vector2f newUV = estimateUVForPosition(x, y, mesh); + uvs.add(newUV); + + // 连接到最近的三角形 + connectNewVertexToMesh(newIndex, vertices, indices); + addedAny = true; + + logger.debug("在位置 ({}, {}) 添加顶点", x, y); + } + } + } + + return addedAny; + } + + /** + * 检查指定位置附近是否已有顶点 + */ + private boolean hasVertexNearby(List vertices, float x, float y, float threshold) { + for (Vector2f vertex : vertices) { + float dx = vertex.x - x; + float dy = vertex.y - y; + if (dx * dx + dy * dy < threshold * threshold) { + return true; + } + } + return false; + } + + /** + * 将新顶点连接到网格 + */ + private void connectNewVertexToMesh(int vertexIndex, List vertices, List indices) { + if (vertices.size() <= 1 || indices.isEmpty()) return; + + // 找到距离最近的三角形 + Vector2f newVertex = vertices.get(vertexIndex); + int closestTriangle = findClosestTriangle(newVertex, vertices, indices); + + if (closestTriangle != -1) { + // 将三角形细分为三个小三角形 + subdivideTriangle(closestTriangle, vertexIndex, indices); + } else { + // 如果没有找到合适的三角形,尝试连接到边界 + connectToMeshBoundary(vertexIndex, vertices, indices); + } + } + + /** + * 将三角形细分为三个小三角形 + */ + private void subdivideTriangle(int triangleStart, int newVertexIndex, List indices) { + int i1 = indices.get(triangleStart); + int i2 = indices.get(triangleStart + 1); + int i3 = indices.get(triangleStart + 2); + + // 修改原三角形为第一个小三角形 + indices.set(triangleStart, newVertexIndex); + indices.set(triangleStart + 1, i1); + indices.set(triangleStart + 2, i2); + + // 添加另外两个小三角形 + indices.add(newVertexIndex); + indices.add(i2); + indices.add(i3); + + indices.add(newVertexIndex); + indices.add(i3); + indices.add(i1); + } + + /** + * 将顶点连接到网格边界(备用方法) + */ + private void connectToMeshBoundary(int vertexIndex, List vertices, List indices) { + // 简单的边界连接策略:找到两个最近的顶点,形成一个三角形 + if (vertices.size() < 3) return; + + Vector2f newVertex = vertices.get(vertexIndex); + + // 找到两个最近的顶点 + int closest1 = -1, closest2 = -1; + float minDist1 = Float.MAX_VALUE, minDist2 = Float.MAX_VALUE; + + for (int i = 0; i < vertices.size() - 1; i++) { // 排除新顶点自己 + if (i == vertexIndex) continue; + + float dist = vertices.get(i).distance(newVertex); + if (dist < minDist1) { + minDist2 = minDist1; + closest2 = closest1; + minDist1 = dist; + closest1 = i; + } else if (dist < minDist2) { + minDist2 = dist; + closest2 = i; + } + } + + if (closest1 != -1 && closest2 != -1) { + // 添加新的三角形 + indices.add(vertexIndex); + indices.add(closest1); + indices.add(closest2); + } + } + + /** + * 使用新的顶点数据更新网格 + */ + private void updateMeshWithNewVertices(Mesh2D mesh, List vertices, List uvs, List indices) { + // 创建新的数组 + float[] finalVertices = new float[vertices.size() * 2]; + float[] finalUVs = new float[uvs.size() * 2]; + int[] finalIndices = new int[indices.size()]; + + // 填充顶点数据 + for (int i = 0; i < vertices.size(); i++) { + finalVertices[i * 2] = vertices.get(i).x; + finalVertices[i * 2 + 1] = vertices.get(i).y; + } + + // 填充UV数据 + for (int i = 0; i < uvs.size(); i++) { + finalUVs[i * 2] = uvs.get(i).x; + finalUVs[i * 2 + 1] = uvs.get(i).y; + } + + // 填充索引数据 + for (int i = 0; i < indices.size(); i++) { + finalIndices[i] = indices.get(i); + } + + // 设置新的网格数据 + mesh.setMeshData(finalVertices, finalUVs, finalIndices); + } + + /** + * 创建网格状态快照 + */ + private OperationHistoryGlobal.MeshState createMeshState(Mesh2D mesh) { + return new OperationHistoryGlobal.MeshState( + mesh.getName(), + mesh.getVertices(), + mesh.getOriginalVertices(), + mesh.getOriginalPivot(), + mesh.getTexture() + ); + } + + /** + * 记录液化操作到历史记录 + */ + private void recordLiquifyOperation(Map beforeStates, + Vector2f brushCenter, float radius, float strength, + LiquifyMode mode, boolean createVertices) { + try { + // 创建液化后的状态 + Map afterStates = new HashMap<>(); + for (Mesh2D mesh : meshes) { + if (mesh.getVertexCount() > 0) { + afterStates.put(mesh, createMeshState(mesh)); + } + } + + // 记录操作 - 确保参数顺序正确 + // params[0] = beforeStates (撤回时使用) + // params[1] = afterStates (重做时使用) + Object[] params = new Object[]{ + beforeStates, // 索引0: 液化前状态(撤回时恢复这个) + afterStates, // 索引1: 液化后状态(重做时恢复这个) + brushCenter, // 索引2: 画笔中心 + radius, // 索引3: 画笔半径 + strength, // 索引4: 画笔强度 + mode, // 索引5: 液化模式 + createVertices // 索引6: 是否创建顶点 + }; + + OperationHistoryGlobal.getInstance().recordOperation("LIQUIFY", params); + logger.debug("已记录液化操作到历史记录,影响 {} 个网格", beforeStates.size()); + + } catch (Exception e) { + logger.error("记录液化操作时出错: {}", e.getMessage(), e); + } + } + + /** + * 应用液化操作历史记录 + */ + public void applyLiquifyFromHistory(OperationHistoryGlobal.MeshState beforeState, OperationHistoryGlobal.MeshState afterState) { + if (beforeState == null || afterState == null) return; + + // 查找对应的网格 + for (Mesh2D mesh : meshes) { + if (mesh.getName().equals(beforeState.name)) { + // 恢复网格状态 + if (afterState.vertices != null) { + mesh.setOriginalVertices(afterState.vertices); + updateMeshVertices(mesh); + } + if (afterState.originalVertices != null) { + mesh.setOriginalVertices(afterState.originalVertices); + } + if (afterState.originalPivot != null) { + mesh.setOriginalPivot(afterState.originalPivot); + } + // 注意:纹理恢复需要根据具体实现调整 + + // 更新边界 + try { + mesh.updateBounds(); + } catch (Exception e) { + logger.warn("更新网格边界时出错: {}", e.getMessage()); + } + break; + } + } + } + + /** + * 撤回液化操作 + */ + public void undoLiquifyFromHistory(OperationHistoryGlobal.MeshState beforeState, OperationHistoryGlobal.MeshState afterState) { + if (beforeState == null) return; + + // 查找对应的网格 + for (Mesh2D mesh : meshes) { + if (mesh.getName().equals(beforeState.name)) { + // 恢复到液化前的状态 + if (beforeState.vertices != null) { + mesh.setOriginalVertices(beforeState.vertices); + updateMeshVertices(mesh); + } + if (beforeState.originalVertices != null) { + mesh.setOriginalVertices(beforeState.originalVertices); + } + if (beforeState.originalPivot != null) { + mesh.setOriginalPivot(beforeState.originalPivot); + } + + // 更新边界 + try { + mesh.updateBounds(); + } catch (Exception e) { + logger.warn("更新网格边界时出错: {}", e.getMessage()); + } + break; + } + } + } + + /** + * 顶点影响信息 + */ + private static class VertexInfluence { + int vertexIndex; + float distance; + + VertexInfluence(int vertexIndex, float distance) { + this.vertexIndex = vertexIndex; + this.distance = distance; + } + } + + /** + * 找到需要影响的顶点(只找最近的几个) + */ + private List findVerticesToInfluence(Mesh2D mesh, Vector2f brushCenter, float radius) { + List influencedVertices = new ArrayList<>(); + float[] vertices = mesh.getOriginalVertices(); + + // 收集画笔区域内的所有顶点 + for (int i = 0; i < vertices.length / 2; i++) { + Vector2f vertexPos = new Vector2f(vertices[i * 2], vertices[i * 2 + 1]); + Vector2f worldPos = Matrix3fUtils.transformPoint(worldTransform, vertexPos); + float dist = worldPos.distance(brushCenter); + + if (dist <= radius * 1.3f) { // 软边界 + influencedVertices.add(new VertexInfluence(i, dist)); + } + } + + // 按距离排序,只保留最近的几个顶点 + influencedVertices.sort((v1, v2) -> Float.compare(v1.distance, v2.distance)); + + // 限制影响顶点的数量,避免过度变形 + int maxVerticesToInfluence = Math.min(8, influencedVertices.size()); + return influencedVertices.subList(0, maxVerticesToInfluence); + } + + /** + * 在画笔区域内稀疏地添加顶点 + */ + private void addSparseVerticesToBrushArea(Vector2f brushCenter, float radius) { + for (Mesh2D mesh : meshes) { + if (mesh.getVertexCount() <= 0) continue; + + float[] originalVertices = mesh.getOriginalVertices(); + float[] originalUVs = mesh.getUVs(); + int[] originalIndices = mesh.getIndices(); + + // 检查数组长度是否有效 + if (originalVertices == null || originalUVs == null || originalIndices == null) { + continue; + } + + // 确保顶点和UV数组长度匹配 + if (originalVertices.length / 2 != originalUVs.length / 2) { + logger.warn("网格 {} 的顶点和UV数量不匹配: 顶点={}, UV={}", + mesh.getName(), originalVertices.length / 2, originalUVs.length / 2); + continue; + } + + List newVertices = new ArrayList<>(); + List newUVs = new ArrayList<>(); + List newIndices = new ArrayList<>(); + + // 将现有数据添加到列表 - 添加边界检查 + int vertexCount = originalVertices.length / 2; + for (int i = 0; i < vertexCount; i++) { + if (i * 2 + 1 < originalVertices.length) { + newVertices.add(new Vector2f(originalVertices[i * 2], originalVertices[i * 2 + 1])); + } + if (i * 2 + 1 < originalUVs.length) { + newUVs.add(new Vector2f(originalUVs[i * 2], originalUVs[i * 2 + 1])); + } + } + + // 复制索引数组 + for (int index : originalIndices) { + if (index < vertexCount) { // 确保索引有效 + newIndices.add(index); + } + } + + boolean addedVertices = false; + + // 检查画笔中心区域是否缺少顶点 + Vector2f localBrushCenter = Matrix3fUtils.transformPointInverse(worldTransform, brushCenter); + + // 找到距离画笔中心最近的顶点 + float minDistance = Float.MAX_VALUE; + int closestVertexIndex = -1; + + for (int i = 0; i < newVertices.size(); i++) { + Vector2f vertex = newVertices.get(i); + Vector2f worldVertex = Matrix3fUtils.transformPoint(worldTransform, vertex); + float dist = worldVertex.distance(brushCenter); + + if (dist < minDistance) { + minDistance = dist; + closestVertexIndex = i; + } + } + + // 如果最近的顶点距离超过阈值,才添加新顶点 + float addVertexThreshold = radius * 0.6f; // 只有距离超过这个阈值才添加顶点 + if (minDistance > addVertexThreshold && closestVertexIndex != -1) { + // 添加一个顶点在画笔中心 + int centerIndex = newVertices.size(); + newVertices.add(new Vector2f(localBrushCenter)); + + // 估算UV坐标 + Vector2f centerUV = estimateUVForNewVertex(localBrushCenter.x, localBrushCenter.y, mesh); + newUVs.add(centerUV); + + // 找到最近的三角形来连接这个新顶点 + int closestTriangle = findClosestTriangle(localBrushCenter, newVertices, newIndices); + if (closestTriangle != -1) { + connectVertexToTriangle(centerIndex, closestTriangle, newIndices); + } + + addedVertices = true; + logger.debug("在画笔中心添加顶点: ({}, {}), 最近顶点距离: {}", + localBrushCenter.x, localBrushCenter.y, minDistance); + } + + // 如果添加了顶点,更新网格数据 + if (addedVertices) { + // 创建新的数组 + 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++) { + if (i * 2 + 1 < finalVertices.length) { + finalVertices[i * 2] = newVertices.get(i).x; + finalVertices[i * 2 + 1] = newVertices.get(i).y; + } + } + + // 填充UV数据 + for (int i = 0; i < newUVs.size(); i++) { + if (i * 2 + 1 < finalUVs.length) { + finalUVs[i * 2] = newUVs.get(i).x; + finalUVs[i * 2 + 1] = newUVs.get(i).y; + } + } + + // 填充索引数据 + for (int i = 0; i < newIndices.size(); i++) { + if (i < finalIndices.length) { + finalIndices[i] = newIndices.get(i); + } + } + + // 设置新的网格数据 + mesh.setMeshData(finalVertices, finalUVs, finalIndices); + logger.info("网格 {} 已添加顶点: {} -> {}", mesh.getName(), originalVertices.length / 2, newVertices.size()); + } + } + } + + /** + * 找到距离给定点最近的三角形 + */ + private int findClosestTriangle(Vector2f point, List vertices, List indices) { + int closestTriangle = -1; + float minDistance = Float.MAX_VALUE; + + for (int i = 0; i < indices.size(); i += 3) { + // 检查是否有足够的索引 + if (i + 2 >= indices.size()) break; + + int i1 = indices.get(i); + int i2 = indices.get(i + 1); + int i3 = indices.get(i + 2); + + // 检查索引是否有效 + if (i1 < 0 || i1 >= vertices.size() || + i2 < 0 || i2 >= vertices.size() || + i3 < 0 || i3 >= vertices.size()) { + continue; + } + + Vector2f v1 = vertices.get(i1); + Vector2f v2 = vertices.get(i2); + Vector2f v3 = vertices.get(i3); + + // 计算三角形中心 + Vector2f center = new Vector2f( + (v1.x + v2.x + v3.x) / 3f, + (v1.y + v2.y + v3.y) / 3f + ); + + float distance = center.distance(point); + if (distance < minDistance) { + minDistance = distance; + closestTriangle = i; + } + } + + return closestTriangle; + } + + /** + * 将顶点连接到三角形(将三角形细分为3个小三角形) + */ + private void connectVertexToTriangle(int vertexIndex, int triangleStart, List indices) { + int i1 = indices.get(triangleStart); + int i2 = indices.get(triangleStart + 1); + int i3 = indices.get(triangleStart + 2); + + // 修改原三角形为第一个小三角形 + indices.set(triangleStart, vertexIndex); + indices.set(triangleStart + 1, i1); + indices.set(triangleStart + 2, i2); + + // 添加另外两个小三角形 + indices.add(vertexIndex); + indices.add(i2); + indices.add(i3); + + indices.add(vertexIndex); + indices.add(i3); + indices.add(i1); + } + + /** + * 为新顶点估算UV坐标 + */ + private Vector2f estimateUVForNewVertex(float x, float y, Mesh2D mesh) { + BoundingBox bounds = mesh.getBounds(); + if (bounds != null && bounds.isValid()) { + float u = (x - bounds.getMinX()) / bounds.getWidth(); + float v = (y - bounds.getMinY()) / bounds.getHeight(); + return new Vector2f(u, v); + } + return new Vector2f(0.5f, 0.5f); } /** @@ -828,7 +1621,7 @@ public class ModelPart { } /** - * 更新单个网格的顶点位置 + * 强制更新单个网格的顶点位置 */ private void updateMeshVertices(Mesh2D mesh) { if (mesh == null) return; @@ -846,22 +1639,38 @@ public class ModelPart { recomputeWorldTransformRecursive(); } - // 应用当前世界变换到每个顶点(将局部 original 顶点烘焙为 world 顶点) - for (int i = 0; i < originalVertices.length; i += 2) { - Vector2f localPoint = new Vector2f(originalVertices[i], originalVertices[i + 1]); + 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); - mesh.setVertex(i / 2, worldPoint.x, worldPoint.y); + + // 检查目标索引是否有效 + if (i < mesh.getVertexCount()) { + mesh.setVertex(i, worldPoint.x, worldPoint.y); + } else { + logger.warn("顶点索引 {} 超出网格顶点范围 (总顶点数: {})", i, mesh.getVertexCount()); + } } - // 同步 mesh 的原始局部 pivot -> 当前世界 pivot(保持可视中心一致) + // 同步 mesh 的原始局部 pivot -> 当前世界 pivot try { Vector2f origPivot = mesh.getOriginalPivot(); Vector2f worldPivot = Matrix3fUtils.transformPoint(worldTransform, origPivot); mesh.setPivot(worldPivot.x, worldPivot.y); - } catch (Exception ignored) { } + } catch (Exception e) { + logger.warn("更新网格pivot时出错: {}", e.getMessage()); + } // 标记网格需要更新 mesh.markDirty(); + mesh.setBakedToWorld(true); } public void setPosition(Vector2f pos) { diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/data/PartData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/data/PartData.java index d4b9500..5471b9e 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/data/PartData.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/data/PartData.java @@ -242,19 +242,8 @@ public class PartData implements Serializable { if (stroke.points != null) { for (LiquifyPointData p : stroke.points) { try { - part.applyLiquify(new Vector2f(p.x, p.y), stroke.radius, stroke.strength, modeEnum, stroke.iterations); + part.applyLiquify(new Vector2f(p.x, p.y), stroke.radius, stroke.strength, modeEnum, stroke.iterations, true); } catch (Exception e) { - // 如果 applyLiquify 不存在或签名不匹配,则尝试通过反射调用名为 applyLiquify 的方法 - try { - java.lang.reflect.Method am = part.getClass().getMethod("applyLiquify", Vector2f.class, float.class, float.class, ModelPart.LiquifyMode.class, int.class); - am.invoke(part, new Vector2f(p.x, p.y), stroke.radius, stroke.strength, modeEnum, stroke.iterations); - } catch (NoSuchMethodException nsme) { - // 无法恢复液化(该 ModelPart 可能不支持液化存储/播放),跳过 - break; - } catch (Exception ex) { - ex.printStackTrace(); - break; - } } } } 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 59174cb..d1f67c6 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 @@ -59,12 +59,17 @@ public class Mesh2D { private Vector2f pivot = new Vector2f(0, 0); private Vector2f originalPivot = new Vector2f(0, 0); private boolean isSuspension = false; + private boolean isRenderVertices = false; // ==================== 多选支持 ==================== private final List multiSelectedParts = new ArrayList<>(); private final BoundingBox multiSelectionBounds = new BoundingBox(); private boolean multiSelectionDirty = true; + // ==================== 液化状态渲染 ==================== + private boolean showLiquifyOverlay = false; + private Vector4f liquifyOverlayColor = new Vector4f(1.0f, 0.5f, 0.0f, 0.3f); // 半透明橙色 + // ==================== 常量 ==================== public static final int POINTS = 0; public static final int LINES = 1; @@ -121,10 +126,347 @@ public class Mesh2D { markDirty(); } + /** + * 设置是否为渲染顶点 + */ + public void setRenderVertices(boolean renderVertices) { + isRenderVertices = renderVertices; + } + public void setModelPart(ModelPart modelPart) { this.modelPart = modelPart; } + /** + * 设置是否显示液化覆盖层 + */ + public void setShowLiquifyOverlay(boolean show) { + this.showLiquifyOverlay = show; + markDirty(); + } + + /** + * 绘制液化状态覆盖层 - 渲染所有顶点 + */ + private void drawLiquifyOverlay(int shaderProgram, Matrix3f modelMatrix) { + if (!showLiquifyOverlay) return; + + RenderSystem.pushState(); + try { + // 使用固色着色器 + ShaderProgram solidShader = ShaderManagement.getShaderProgram("Solid Color Shader"); + if (solidShader != null && solidShader.programId != 0) { + solidShader.use(); + + // 设置模型矩阵 + int modelLoc = solidShader.getUniformLocation("uModelMatrix"); + if (modelLoc != -1) { + RenderSystem.uniformMatrix3(modelLoc, modelMatrix); + } + + // 设置液化覆盖层颜色 + int colorLoc = solidShader.getUniformLocation("uColor"); + if (colorLoc != -1) { + RenderSystem.uniform4f(colorLoc, liquifyOverlayColor); + } + } + + RenderSystem.enableBlend(); + RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + + Tesselator t = Tesselator.getInstance(); + BufferBuilder bb = t.getBuilder(); + + if (isRenderVertices && vertices != null && vertices.length >= 6) { + // ============ 显示顶点模式 ============ + // 1. 绘制所有顶点组成的多边形填充 + bb.begin(GL11.GL_TRIANGLES, vertices.length / 2 * 3); + bb.setColor(liquifyOverlayColor); + + // 使用三角形扇绘制填充 + Vector2f center = new Vector2f(vertices[0], vertices[1]); + for (int i = 1; i < vertices.length / 2 - 1; i++) { + int baseIndex1 = i * 2; + int baseIndex2 = (i + 1) * 2; + + bb.vertex(center.x, center.y, 0f, 0f); + bb.vertex(vertices[baseIndex1], vertices[baseIndex1 + 1], 0f, 0f); + bb.vertex(vertices[baseIndex2], vertices[baseIndex2 + 1], 0f, 0f); + } + t.end(); + + // 2. 绘制顶点连线(边框) + drawLiquifyVertexLines(bb); + + // 3. 绘制顶点标记 + drawLiquifyVertexPoints(bb); + + // 4. 绘制液化提示文字 + drawLiquifyTextAtCenter(bb); + + } else { + // 4. 绘制液化状态指示器 + drawLiquifyStatusIndicator(bb); + } + + } finally { + RenderSystem.popState(); + } + } + + /** + * 绘制液化状态指示器(在不显示顶点时) + */ + private void drawLiquifyStatusIndicator(BufferBuilder bb) { + BoundingBox bounds = getBounds(); + if (bounds == null || !bounds.isValid()) return; + + float centerX = (bounds.getMinX() + bounds.getMaxX()) / 2.0f; + float centerY = (bounds.getMinY() + bounds.getMaxY()) / 2.0f; + float width = bounds.getWidth(); + float height = bounds.getHeight(); + + // 计算指示器位置(放在网格右上方,不遮挡内容) + float indicatorX = bounds.getMaxX() + Math.max(width, height) * 0.2f; + float indicatorY = bounds.getMaxY() + Math.max(width, height) * 0.1f; + + // 1. 绘制简洁的液化状态圆点 + float dotRadius = Math.max(width, height) * 0.08f; + + // 外圈圆点 + bb.begin(GL11.GL_TRIANGLE_FAN, 16); + bb.setColor(new Vector4f(1.0f, 0.6f, 0.0f, 0.8f)); // 橙色圆点 + bb.vertex(indicatorX, indicatorY, 0f, 0f); // 中心点 + for (int i = 0; i <= 16; i++) { + float angle = (float) (i * 2 * Math.PI / 16); + float x = indicatorX + (float) Math.cos(angle) * dotRadius; + float y = indicatorY + (float) Math.sin(angle) * dotRadius; + bb.vertex(x, y, 0f, 0f); + } + bb.endImmediate(); + + // 内圈白色圆点 + float innerRadius = dotRadius * 0.5f; + bb.begin(GL11.GL_TRIANGLE_FAN, 12); + bb.setColor(new Vector4f(1.0f, 1.0f, 1.0f, 0.9f)); // 白色内圆 + bb.vertex(indicatorX, indicatorY, 0f, 0f); // 中心点 + for (int i = 0; i <= 12; i++) { + float angle = (float) (i * 2 * Math.PI / 12); + float x = indicatorX + (float) Math.cos(angle) * innerRadius; + float y = indicatorY + (float) Math.sin(angle) * innerRadius; + bb.vertex(x, y, 0f, 0f); + } + bb.endImmediate(); + + // 2. 绘制简洁的画笔图标 + drawSimpleBrushIcon(bb, indicatorX, indicatorY, dotRadius); + + // 3. 绘制简洁的提示文字 + String liquifyText = "Liquify"; + String hintText = "Ctrl: Show Vertices"; + + TextRenderer textRenderer = ModelRender.getTextRenderer(); + if (textRenderer != null) { + float textY = indicatorY + dotRadius + 15f; + + // 主标题 + float titleWidth = textRenderer.getTextWidth(liquifyText); + float titleX = indicatorX - titleWidth / 2.0f; + + // 绘制主标题背景(简洁的圆角效果) + bb.begin(GL11.GL_TRIANGLES, 6); + bb.setColor(new Vector4f(0.0f, 0.0f, 0.0f, 0.6f)); // 半透明黑色背景 + bb.vertex(titleX - 6, textY - 12, 0f, 0f); + bb.vertex(titleX + titleWidth + 6, textY - 12, 0f, 0f); + bb.vertex(titleX + titleWidth + 6, textY + 2, 0f, 0f); + bb.vertex(titleX + titleWidth + 6, textY + 2, 0f, 0f); + bb.vertex(titleX - 6, textY + 2, 0f, 0f); + bb.vertex(titleX - 6, textY - 12, 0f, 0f); + bb.endImmediate(); + + // 绘制主标题 + ModelRender.renderText(liquifyText, titleX, textY, new Vector4f(1.0f, 0.8f, 0.0f, 1.0f)); + + // 提示文字(小字号) + float hintY = textY + 15f; + float hintWidth = textRenderer.getTextWidth(hintText); + float hintX = indicatorX - hintWidth / 2.0f; + + ModelRender.renderText(hintText, hintX, hintY, new Vector4f(0.8f, 0.8f, 0.8f, 0.7f)); + } + } + + /** + * 绘制简洁的画笔图标 + */ + private void drawSimpleBrushIcon(BufferBuilder bb, float centerX, float centerY, float size) { + float iconSize = size * 0.6f; + + // 画笔柄(简单的线条) + bb.begin(GL11.GL_LINES, 2); + bb.setColor(new Vector4f(1.0f, 1.0f, 1.0f, 0.9f)); + bb.vertex(centerX - iconSize * 0.3f, centerY - iconSize * 0.5f, 0f, 0f); + bb.vertex(centerX, centerY + iconSize * 0.3f, 0f, 0f); + bb.endImmediate(); + + // 画笔头(小三角形) + bb.begin(GL11.GL_TRIANGLES, 3); + bb.setColor(new Vector4f(1.0f, 0.3f, 0.0f, 0.9f)); + bb.vertex(centerX, centerY + iconSize * 0.3f, 0f, 0f); + bb.vertex(centerX - iconSize * 0.2f, centerY + iconSize * 0.6f, 0f, 0f); + bb.vertex(centerX + iconSize * 0.2f, centerY + iconSize * 0.6f, 0f, 0f); + bb.endImmediate(); + } + + /** + * 绘制顶点连线(边框) + */ + private void drawLiquifyVertexLines(BufferBuilder bb) { + final Vector4f LINE_COLOR = new Vector4f(1.0f, 0.8f, 0.0f, 1.0f); // 黄色边框 + + if (vertices == null || vertices.length < 4) return; + + // 绘制闭合的多边形边框 + bb.begin(GL11.GL_LINE_LOOP, vertices.length / 2); + bb.setColor(LINE_COLOR); + + for (int i = 0; i < vertices.length / 2; i++) { + int baseIndex = i * 2; + bb.vertex(vertices[baseIndex], vertices[baseIndex + 1], 0f, 0f); + } + bb.endImmediate(); + + // 如果网格有三角形索引,也绘制内部连线 + if (indices != null && indices.length >= 3) { + bb.begin(GL11.GL_LINES, indices.length * 2); + bb.setColor(new Vector4f(1.0f, 0.6f, 0.0f, 0.6f)); // 半透明橙色内部线 + + for (int i = 0; i < indices.length; i += 3) { + int i1 = indices[i]; + int i2 = indices[i + 1]; + int i3 = indices[i + 2]; + + // 三条边 + drawLineBetweenVertices(bb, i1, i2); + drawLineBetweenVertices(bb, i2, i3); + drawLineBetweenVertices(bb, i3, i1); + } + bb.endImmediate(); + } + } + + /** + * 在两个顶点之间绘制线段 + */ + private void drawLineBetweenVertices(BufferBuilder bb, int index1, int index2) { + if (index1 < 0 || index1 >= vertices.length / 2 || + index2 < 0 || index2 >= vertices.length / 2) { + return; + } + + int base1 = index1 * 2; + int base2 = index2 * 2; + + bb.vertex(vertices[base1], vertices[base1 + 1], 0f, 0f); + bb.vertex(vertices[base2], vertices[base2 + 1], 0f, 0f); + } + + /** + * 绘制顶点标记 + */ + private void drawLiquifyVertexPoints(BufferBuilder bb) { + final float POINT_SIZE = 4.0f; + final Vector4f POINT_COLOR = new Vector4f(1.0f, 0.4f, 0.0f, 1.0f); // 橙色顶点 + + if (vertices == null) return; + + // 绘制所有顶点 + for (int i = 0; i < vertices.length / 2; i++) { + int baseIndex = i * 2; + float x = vertices[baseIndex]; + float y = vertices[baseIndex + 1]; + + // 绘制小方块表示顶点 + bb.begin(GL11.GL_TRIANGLES, 6); + bb.setColor(POINT_COLOR); + + float halfSize = POINT_SIZE / 2; + bb.vertex(x - halfSize, y - halfSize, 0f, 0f); + bb.vertex(x + halfSize, y - halfSize, 0f, 0f); + bb.vertex(x + halfSize, y + halfSize, 0f, 0f); + + bb.vertex(x + halfSize, y + halfSize, 0f, 0f); + bb.vertex(x - halfSize, y + halfSize, 0f, 0f); + bb.vertex(x - halfSize, y - halfSize, 0f, 0f); + + bb.endImmediate(); + + // 在顶点旁边显示编号(可选) + drawVertexNumber(bb, i, x, y); + } + } + + /** + * 在顶点旁边显示编号 + */ + private void drawVertexNumber(BufferBuilder bb, int vertexIndex, float x, float y) { + String numberText = String.valueOf(vertexIndex); + TextRenderer textRenderer = ModelRender.getTextRenderer(); + if (textRenderer != null) { + float textWidth = textRenderer.getTextWidth(numberText); + float textX = x + 6.0f; // 在顶点右侧显示 + float textY = y - 4.0f; + + // 绘制文字背景 + bb.begin(GL11.GL_TRIANGLES, 6); + bb.setColor(new Vector4f(0.1f, 0.1f, 0.1f, 0.8f)); + bb.vertex(textX - 2, textY - 8, 0f, 0f); + bb.vertex(textX + textWidth + 2, textY - 8, 0f, 0f); + bb.vertex(textX + textWidth + 2, textY + 2, 0f, 0f); + bb.vertex(textX + textWidth + 2, textY + 2, 0f, 0f); + bb.vertex(textX - 2, textY + 2, 0f, 0f); + bb.vertex(textX - 2, textY - 8, 0f, 0f); + bb.endImmediate(); + + // 绘制文字 + ModelRender.renderText(numberText, textX, textY, new Vector4f(1.0f, 1.0f, 1.0f, 1.0f)); + } + } + + /** + * 在网格中心绘制液化提示文字 + */ + private void drawLiquifyTextAtCenter(BufferBuilder bb) { + String liquifyText = "LIQUIFY MODE"; + BoundingBox bounds = getBounds(); + if (bounds == null || !bounds.isValid()) return; + + float centerX = (bounds.getMinX() + bounds.getMaxX()) / 2.0f; + float textY = bounds.getMaxY() + 20.0f; + + // 使用文本渲染器绘制提示文字 + Vector4f textColor = new Vector4f(1.0f, 0.8f, 0.0f, 1.0f); + TextRenderer textRenderer = ModelRender.getTextRenderer(); + if (textRenderer != null) { + float textWidth = textRenderer.getTextWidth(liquifyText); + float textX = centerX - textWidth / 2.0f; + + // 绘制文字背景 + bb.begin(GL11.GL_TRIANGLES, 6); + bb.setColor(new Vector4f(0.1f, 0.1f, 0.1f, 0.8f)); + bb.vertex(textX - 5, textY - 15, 0f, 0f); + bb.vertex(textX + textWidth + 5, textY - 15, 0f, 0f); + bb.vertex(textX + textWidth + 5, textY + 5, 0f, 0f); + bb.vertex(textX + textWidth + 5, textY + 5, 0f, 0f); + bb.vertex(textX - 5, textY + 5, 0f, 0f); + bb.vertex(textX - 5, textY - 15, 0f, 0f); + bb.endImmediate(); + + // 绘制文字 + ModelRender.renderText(liquifyText, textX, textY, textColor); + } + } + /** * 设置中心点 */ @@ -233,6 +575,106 @@ public class Mesh2D { this.originalVertices = originalVertices != null ? originalVertices.clone() : null; } + /** + * 设置顶点数据(支持顶点数量变化) + */ + public void setVertices(float[] vertices) { + if (vertices == null) { + throw new IllegalArgumentException("Vertices array cannot be null"); + } + if (vertices.length % 2 != 0) { + throw new IllegalArgumentException("Vertices array must have even length (x,y pairs)"); + } + + // 允许顶点数量变化,但需要相应地调整UV数组 + if (this.vertices.length != vertices.length) { + // 顶点数量发生了变化,需要调整UV数组 + adjustUVsForNewVertexCount(vertices.length / 2); + } + + this.vertices = vertices.clone(); + markDirty(); + } + + /** + * 根据新的顶点数量调整UV数组 + */ + private void adjustUVsForNewVertexCount(int newVertexCount) { + int currentVertexCount = getVertexCount(); + + if (newVertexCount == currentVertexCount) { + return; // 顶点数量没有变化 + } + + float[] newUVs = new float[newVertexCount * 2]; + + if (newVertexCount > currentVertexCount) { + // 顶点数量增加,复制现有UV并估算新增顶点的UV + System.arraycopy(uvs, 0, newUVs, 0, uvs.length); + + // 为新增的顶点估算UV坐标 + for (int i = currentVertexCount; i < newVertexCount; i++) { + int uvIndex = i * 2; + // 使用边界框来估算UV + BoundingBox bounds = getBounds(); + if (bounds != null && bounds.isValid()) { + float u = 0.5f; // 默认值 + float v = 0.5f; // 默认值 + newUVs[uvIndex] = u; + newUVs[uvIndex + 1] = v; + } else { + newUVs[uvIndex] = 0.5f; + newUVs[uvIndex + 1] = 0.5f; + } + } + } else { + // 顶点数量减少,截断UV数组 + System.arraycopy(uvs, 0, newUVs, 0, newUVs.length); + } + + this.uvs = newUVs; + + // 如果顶点数量变化很大,可能需要重新生成索引 + if (Math.abs(newVertexCount - currentVertexCount) > 2) { + regenerateIndicesForNewVertexCount(newVertexCount); + } + } + + /** + * 为新的顶点数量重新生成索引 + */ + private void regenerateIndicesForNewVertexCount(int newVertexCount) { + // 简单的三角形扇形索引生成 + if (newVertexCount >= 3) { + List newIndices = new ArrayList<>(); + + // 使用三角形扇形(适用于凸多边形) + for (int i = 1; i < newVertexCount - 1; i++) { + newIndices.add(0); + newIndices.add(i); + newIndices.add(i + 1); + } + + this.indices = new int[newIndices.size()]; + for (int i = 0; i < newIndices.size(); i++) { + this.indices[i] = newIndices.get(i); + } + } else { + // 顶点太少,清空索引 + this.indices = new int[0]; + } + } + + /** + * 设置顶点数据(带原始顶点同步) + */ + public void setVertices(float[] vertices, boolean updateOriginal) { + setVertices(vertices); + if (updateOriginal && originalVertices != null && originalVertices.length == vertices.length) { + this.originalVertices = vertices.clone(); + } + } + /** * 创建圆形网格 */ @@ -276,6 +718,138 @@ public class Mesh2D { // ==================== 顶点操作 ==================== + /** + * 添加新顶点到网格末尾 + */ + public void addVertex(float x, float y, float u, float v) { + // 扩展顶点数组 + float[] newVertices = new float[vertices.length + 2]; + System.arraycopy(vertices, 0, newVertices, 0, vertices.length); + newVertices[vertices.length] = x; + newVertices[vertices.length + 1] = y; + + // 扩展UV数组 + float[] newUVs = new float[uvs.length + 2]; + System.arraycopy(uvs, 0, newUVs, 0, uvs.length); + newUVs[uvs.length] = u; + newUVs[uvs.length + 1] = v; + + // 更新数据 + this.vertices = newVertices; + this.uvs = newUVs; + + // 注意:需要手动更新索引数组来包含新顶点 + markDirty(); + } + + /** + * 在指定位置插入顶点 + */ + public void insertVertex(int index, float x, float y, float u, float v) { + if (index < 0 || index > getVertexCount()) { + throw new IndexOutOfBoundsException("Vertex index out of bounds: " + index); + } + + // 扩展顶点数组 + float[] newVertices = new float[vertices.length + 2]; + System.arraycopy(vertices, 0, newVertices, 0, index * 2); + newVertices[index * 2] = x; + newVertices[index * 2 + 1] = y; + System.arraycopy(vertices, index * 2, newVertices, index * 2 + 2, vertices.length - index * 2); + + // 扩展UV数组 + float[] newUVs = new float[uvs.length + 2]; + System.arraycopy(uvs, 0, newUVs, 0, index * 2); + newUVs[index * 2] = u; + newUVs[index * 2 + 1] = v; + System.arraycopy(uvs, index * 2, newUVs, index * 2 + 2, uvs.length - index * 2); + + // 更新数据 + this.vertices = newVertices; + this.uvs = newUVs; + + // 注意:需要更新索引数组来反映顶点位置的变化 + updateIndicesAfterVertexInsertion(index); + + markDirty(); + } + + /** + * 移除指定顶点 + */ + public void removeVertex(int index) { + if (index < 0 || index >= getVertexCount()) { + throw new IndexOutOfBoundsException("Vertex index out of bounds: " + index); + } + + // 缩减顶点数组 + float[] newVertices = new float[vertices.length - 2]; + System.arraycopy(vertices, 0, newVertices, 0, index * 2); + System.arraycopy(vertices, index * 2 + 2, newVertices, index * 2, vertices.length - index * 2 - 2); + + // 缩减UV数组 + float[] newUVs = new float[uvs.length - 2]; + System.arraycopy(uvs, 0, newUVs, 0, index * 2); + System.arraycopy(uvs, index * 2 + 2, newUVs, index * 2, uvs.length - index * 2 - 2); + + // 更新数据 + this.vertices = newVertices; + this.uvs = newUVs; + + // 更新索引数组 + updateIndicesAfterVertexRemoval(index); + + markDirty(); + } + + /** + * 在插入顶点后更新索引数组 + */ + private void updateIndicesAfterVertexInsertion(int insertedIndex) { + if (indices == null) return; + + for (int i = 0; i < indices.length; i++) { + if (indices[i] >= insertedIndex) { + indices[i]++; + } + } + } + + /** + * 在移除顶点后更新索引数组 + */ + private void updateIndicesAfterVertexRemoval(int removedIndex) { + if (indices == null) return; + + // 创建新索引数组,移除引用被删除顶点的三角形 + List newIndicesList = new ArrayList<>(); + for (int i = 0; i < indices.length; i += 3) { + int i1 = indices[i]; + int i2 = indices[i + 1]; + int i3 = indices[i + 2]; + + // 如果三角形包含被删除的顶点,跳过这个三角形 + if (i1 == removedIndex || i2 == removedIndex || i3 == removedIndex) { + continue; + } + + // 调整索引编号 + if (i1 > removedIndex) i1--; + if (i2 > removedIndex) i2--; + if (i3 > removedIndex) i3--; + + newIndicesList.add(i1); + newIndicesList.add(i2); + newIndicesList.add(i3); + } + + // 转换回数组 + this.indices = new int[newIndicesList.size()]; + for (int i = 0; i < newIndicesList.size(); i++) { + this.indices[i] = newIndicesList.get(i); + } + } + /** * 获取顶点数量 */ @@ -768,7 +1342,7 @@ public class Mesh2D { } // 选中框绘制(需要切换到固色 shader) - if (selected) { + if (selected && !isRenderVertices) { RenderSystem.enableBlend(); RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); @@ -797,6 +1371,10 @@ public class Mesh2D { } } + if (showLiquifyOverlay) { + drawLiquifyOverlay(shaderProgram, modelMatrix); + } + if (isSuspension && !selected) { RenderSystem.pushState(); diff --git a/src/main/java/com/chuangzhou/vivid2D/render/systems/Matrix3fUtils.java b/src/main/java/com/chuangzhou/vivid2D/render/systems/Matrix3fUtils.java index 017c81e..e37ec2a 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/systems/Matrix3fUtils.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/systems/Matrix3fUtils.java @@ -25,4 +25,46 @@ public class Matrix3fUtils { public static Vector2f transformPointInverse(Matrix3f matrix, Vector2f point) { return transformPointInverse(matrix, point, new Vector2f()); } -} + + /** + * 变换向量(不考虑平移,只考虑旋转和缩放) + */ + public static Vector2f transformVector(Matrix3f matrix, Vector2f vector, Vector2f dest) { + float x = matrix.m00() * vector.x + matrix.m01() * vector.y; + float y = matrix.m10() * vector.x + matrix.m11() * vector.y; + return dest.set(x, y); + } + + public static Vector2f transformVector(Matrix3f matrix, Vector2f vector) { + return transformVector(matrix, vector, new Vector2f()); + } + + /** + * 逆变换向量(不考虑平移,只考虑旋转和缩放的逆) + */ + public static Vector2f transformVectorInverse(Matrix3f matrix, Vector2f vector, Vector2f dest) { + // 计算2x2子矩阵的行列式 + float det = matrix.m00() * matrix.m11() - matrix.m01() * matrix.m10(); + + if (Math.abs(det) < 1e-6f) { + return dest.set(vector); + } + + float invDet = 1.0f / det; + + // 计算2x2子矩阵的逆 + float m00 = matrix.m11() * invDet; + float m01 = -matrix.m01() * invDet; + float m10 = -matrix.m10() * invDet; + float m11 = matrix.m00() * invDet; + + float x = vector.x * m00 + vector.y * m01; + float y = vector.x * m10 + vector.y * m11; + + return dest.set(x, y); + } + + public static Vector2f transformVectorInverse(Matrix3f matrix, Vector2f vector) { + return transformVectorInverse(matrix, vector, new Vector2f()); + } +} \ No newline at end of file