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 a078517..495e249 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelClickListener.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelClickListener.java @@ -31,4 +31,12 @@ public interface ModelClickListener { default void onLiquifyModeExited(){}; default void onLiquifyModeEntered(Mesh2D targetMesh, ModelPart liquifyTargetPart){}; + + default void onSecondaryVertexModeEntered(Mesh2D secondaryVertexTargetMesh){}; + + default void onSecondaryVertexModeExited(){}; + + default void onPuppetModeEntered(Mesh2D puppetTargetMesh){}; + + default void onPuppetModeExited(){}; } 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 db4567a..9c80653 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java @@ -7,6 +7,8 @@ import com.chuangzhou.vivid2D.render.model.Model2D; import com.chuangzhou.vivid2D.render.model.ModelPart; import com.chuangzhou.vivid2D.render.model.util.BoundingBox; import com.chuangzhou.vivid2D.render.model.util.Mesh2D; +import com.chuangzhou.vivid2D.render.model.util.PuppetPin; +import com.chuangzhou.vivid2D.render.model.util.SecondaryVertex; import com.chuangzhou.vivid2D.render.systems.Camera; import com.chuangzhou.vivid2D.render.systems.RenderSystem; import com.chuangzhou.vivid2D.test.TestModelGLPanel; @@ -92,7 +94,9 @@ public class ModelRenderPanel extends JPanel { RESIZE_BOTTOM_LEFT, // 调整左下角 RESIZE_BOTTOM_RIGHT, // 调整右下角 ROTATE, // 新增:旋转 - MOVE_PIVOT // 新增:移动中心点 + MOVE_PIVOT, // 新增:移动中心点 + MOVE_SECONDARY_VERTEX, // 新增:移动二级顶点 + MOVE_PUPPET_PIN // 新增:移动 puppetPin } // 新增:拖拽相关字段 @@ -159,6 +163,20 @@ public class ModelRenderPanel extends JPanel { private float liquifyBrushStrength = 2.0f; private ModelPart.LiquifyMode currentLiquifyMode = ModelPart.LiquifyMode.PUSH; + // ================== 二级顶点变形器相关字段 ================== + private volatile boolean secondaryVertexMode = false; // 二级顶点变形模式 + private volatile Mesh2D secondaryVertexTargetMesh = null; // 当前操作的二级顶点网格 + private volatile SecondaryVertex selectedSecondaryVertex = null; // 选中的二级顶点 + private volatile SecondaryVertex hoveredSecondaryVertex = null; // 悬停的二级顶点 + private static final float SECONDARY_VERTEX_TOLERANCE = 8.0f; // 二级顶点选择容差 + + // ================== 木偶工具相关字段 ================== + private volatile boolean puppetMode = false; // 木偶工具模式 + private volatile Mesh2D puppetTargetMesh = null; // 当前操作的木偶工具网格 + private volatile PuppetPin selectedPuppetPin = null; // 选中的木偶控制点 + private volatile PuppetPin hoveredPuppetPin = null; // 悬停的木偶控制点 + private static final float PUPPET_PIN_TOLERANCE = 8.0f; // 木偶控制点选择容差 + // ================== 摄像机控制方法 ================== /** @@ -210,7 +228,6 @@ public class ModelRenderPanel extends JPanel { executeInGLContext(() -> ModelRender.resetCamera()); } - /** * 构造函数:使用模型路径 */ @@ -246,12 +263,711 @@ public class ModelRenderPanel extends JPanel { doubleClickTimer.setRepeats(false); } + /** + * 初始化键盘快捷键 + */ + private void initKeyboardShortcuts() { + // 获取输入映射和动作映射 + InputMap inputMap = getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); + ActionMap actionMap = getActionMap(); + // 撤回快捷键:Ctrl+Z + KeyStroke undoKey = KeyStroke.getKeyStroke(KeyEvent.VK_Z, KeyEvent.CTRL_DOWN_MASK); + inputMap.put(undoKey, "undo"); + actionMap.put("undo", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + undo(); + } + }); + + // 木偶工具快捷键 Ctrl+P + KeyStroke puppetModeKey = KeyStroke.getKeyStroke(KeyEvent.VK_P, KeyEvent.CTRL_DOWN_MASK); + inputMap.put(puppetModeKey, "puppetMode"); + actionMap.put("puppetMode", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + togglePuppetMode(); + } + }); + + // 重做快捷键:Ctrl+Y 或 Ctrl+Shift+Z + KeyStroke redoKey1 = KeyStroke.getKeyStroke(KeyEvent.VK_Y, KeyEvent.CTRL_DOWN_MASK); + KeyStroke redoKey2 = KeyStroke.getKeyStroke(KeyEvent.VK_Z, KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK); + inputMap.put(redoKey1, "redo"); + inputMap.put(redoKey2, "redo"); + actionMap.put("redo", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + redo(); + } + }); + + // 清除历史记录:Ctrl+Shift+Delete + KeyStroke clearHistoryKey = KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK); + inputMap.put(clearHistoryKey, "clearHistory"); + actionMap.put("clearHistory", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + clearHistory(); + } + }); + + // 添加摄像机重置快捷键:Ctrl+R + KeyStroke resetCameraKey = KeyStroke.getKeyStroke(KeyEvent.VK_R, KeyEvent.CTRL_DOWN_MASK); + inputMap.put(resetCameraKey, "resetCamera"); + actionMap.put("resetCamera", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + resetCamera(); + logger.info("重置摄像机"); + } + }); + + // 添加摄像机启用/禁用快捷键:Ctrl+E + KeyStroke toggleCameraKey = KeyStroke.getKeyStroke(KeyEvent.VK_E, KeyEvent.CTRL_DOWN_MASK); + inputMap.put(toggleCameraKey, "toggleCamera"); + actionMap.put("toggleCamera", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + Camera camera = ModelRender.getCamera(); + boolean newState = !camera.isEnabled(); + camera.setEnabled(newState); + logger.info("{}摄像机", newState ? "启用" : "禁用"); + } + }); + + // 新增:二级顶点变形器快捷键 Ctrl+T + KeyStroke secondaryVertexKey = KeyStroke.getKeyStroke(KeyEvent.VK_T, KeyEvent.CTRL_DOWN_MASK); + inputMap.put(secondaryVertexKey, "secondaryVertex"); + actionMap.put("secondaryVertex", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + toggleSecondaryVertexMode(); + } + }); + } + + /** + * 切换木偶工具模式 + */ + private void togglePuppetMode() { + if (puppetMode) { + exitPuppetMode(); + } else { + executeInGLContext(this::enterPuppetMode); + } + } + + /** + * 进入木偶工具模式 + */ + private void enterPuppetMode() { + if (selectedMeshes.isEmpty()) { + logger.warn("请先选择一个网格以进入木偶工具模式"); + return; + } + + puppetMode = true; + puppetTargetMesh = lastSelectedMesh; + + if (puppetTargetMesh != null) { + // 显示木偶控制点 + puppetTargetMesh.setShowPuppetPins(true); + + // 如果没有木偶控制点,创建默认的四个角点 + if (puppetTargetMesh.getPuppetPinCount() == 0) { + createDefaultPuppetPins(); + } + + // 预计算权重 + puppetTargetMesh.precomputeAllPuppetWeights(); + } + + // 设置特殊光标 + setCursor(createPuppetCursor()); + + logger.info("进入木偶工具模式: {}", + puppetTargetMesh != null ? puppetTargetMesh.getName() : "null"); + + // 通知监听器 + for (ModelClickListener listener : clickListeners) { + try { + listener.onPuppetModeEntered(puppetTargetMesh); + } catch (Exception ex) { + logger.error("木偶工具模式进入事件监听器执行出错", ex); + } + } + + repaint(); + } + + /** + * 退出木偶工具模式 + */ + private void exitPuppetMode() { + executeInGLContext(() -> { + puppetMode = false; + if (puppetTargetMesh != null) { + puppetTargetMesh.setShowPuppetPins(false); + } + puppetTargetMesh = null; + selectedPuppetPin = null; + hoveredPuppetPin = null; + + // 恢复默认光标 + updateCursorForHoverState(); + + logger.info("退出木偶工具模式"); + + // 通知监听器 + for (ModelClickListener listener : clickListeners) { + try { + listener.onPuppetModeExited(); + } catch (Exception ex) { + logger.error("木偶工具模式退出事件监听器执行出错", ex); + } + } + + repaint(); + }); + } + + /** + * 创建默认的四个角点木偶控制点 + */ + private void createDefaultPuppetPins() { + if (puppetTargetMesh == null) return; + + BoundingBox bounds = puppetTargetMesh.getBounds(); + if (bounds == null || !bounds.isValid()) return; + + float minX = bounds.getMinX(); + float minY = bounds.getMinY(); + float maxX = bounds.getMaxX(); + float maxY = bounds.getMaxY(); + + // 创建四个角点 + puppetTargetMesh.addPuppetPin(minX, minY, 0.0f, 1.0f); // 左下 + puppetTargetMesh.addPuppetPin(maxX, minY, 1.0f, 1.0f); // 右下 + puppetTargetMesh.addPuppetPin(maxX, maxY, 1.0f, 0.0f); // 右上 + puppetTargetMesh.addPuppetPin(minX, maxY, 0.0f, 0.0f); // 左上 + + logger.debug("为网格 {} 创建了4个默认木偶控制点", puppetTargetMesh.getName()); + } + + /** + * 创建木偶工具模式光标 + */ + private Cursor createPuppetCursor() { + 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; + + // 外圈(蓝色) + g2d.setColor(Color.BLUE); + g2d.setStroke(new BasicStroke(2f)); + g2d.drawOval(center - 6, center - 6, 12, 12); + + // 内圈 + g2d.setColor(new Color(0, 0, 200, 150)); + g2d.setStroke(new BasicStroke(1f)); + g2d.drawOval(center - 3, center - 3, 6, 6); + + // 中心点 + g2d.setColor(Color.BLUE); + g2d.fillOval(center - 1, center - 1, 2, 2); + + g2d.dispose(); + + return Toolkit.getDefaultToolkit().createCustomCursor(cursorImg, new Point(center, center), "PuppetCursor"); + } + + /** + * 在木偶工具模式下处理双击 + */ + private void handlePuppetDoubleClick(MouseEvent e) { + 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]; + + // 检查是否双击了木偶控制点 + PuppetPin clickedPin = findPuppetPinAtPosition(modelX, modelY); + if (clickedPin != null) { + // 双击木偶控制点:删除该控制点 + deletePuppetPin(clickedPin); + } else { + // 双击空白处:创建新的木偶控制点 + createPuppetPinAt(modelX, modelY); + } + + } catch (Exception ex) { + logger.error("处理木偶工具双击时出错", ex); + } + }); + } + + /** + * 在指定位置创建木偶控制点 + */ + private void createPuppetPinAt(float x, float y) { + if (puppetTargetMesh == null) return; + + // 计算UV坐标(基于边界框) + BoundingBox bounds = puppetTargetMesh.getBounds(); + if (bounds == null || !bounds.isValid()) return; + + float u = (x - bounds.getMinX()) / bounds.getWidth(); + float v = (y - bounds.getMinY()) / bounds.getHeight(); + + // 限制UV在0-1范围内 + u = Math.max(0.0f, Math.min(1.0f, u)); + v = Math.max(0.0f, Math.min(1.0f, v)); + + PuppetPin newPin = puppetTargetMesh.addPuppetPin(x, y, u, v); + logger.info("创建木偶控制点: ID={}, 位置({}, {}), UV({}, {})", + newPin.getId(), x, y, u, v); + + repaint(); + } + + /** + * 删除木偶控制点 + */ + private void deletePuppetPin(PuppetPin pin) { + if (puppetTargetMesh == null || pin == null) return; + + boolean removed = puppetTargetMesh.removePuppetPin(pin); + if (removed) { + if (selectedPuppetPin == pin) { + selectedPuppetPin = null; + } + if (hoveredPuppetPin == pin) { + hoveredPuppetPin = null; + } + logger.info("删除木偶控制点: ID={}", pin.getId()); + } + + repaint(); + } + + /** + * 在指定位置查找木偶控制点 + */ + private PuppetPin findPuppetPinAtPosition(float x, float y) { + if (puppetTargetMesh == null) return null; + + float tolerance = PUPPET_PIN_TOLERANCE / calculateScaleFactor(); + + return puppetTargetMesh.selectPuppetPinAt(x, y, tolerance); + } + + /** + * 处理木偶工具模式下的鼠标移动 + */ + private void handlePuppetMouseMove(float modelX, float modelY) { + if (!puppetMode || puppetTargetMesh == null) return; + + // 更新悬停的木偶控制点 + PuppetPin newHoveredPin = findPuppetPinAtPosition(modelX, modelY); + + if (newHoveredPin != hoveredPuppetPin) { + hoveredPuppetPin = newHoveredPin; + + // 更新光标 + if (hoveredPuppetPin != null) { + setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + setCursor(createPuppetCursor()); + } + } + } + + /** + * 处理木偶工具模式下的鼠标按下 + */ + private void handlePuppetMousePressed(float modelX, float modelY) { + if (!puppetMode || puppetTargetMesh == null) return; + + // 选择木偶控制点 + PuppetPin clickedPin = findPuppetPinAtPosition(modelX, modelY); + if (clickedPin != null) { + puppetTargetMesh.setSelectedPuppetPin(clickedPin); + selectedPuppetPin = clickedPin; + + // 开始拖拽 + currentDragMode = DragMode.MOVE_PUPPET_PIN; + dragStartX = modelX; + dragStartY = modelY; + + logger.debug("开始移动木偶控制点: ID={}", clickedPin.getId()); + } else { + // 点击空白处,取消选择 + puppetTargetMesh.setSelectedPuppetPin(null); + selectedPuppetPin = null; + } + } + + /** + * 处理木偶工具模式下的鼠标拖拽 + */ + private void handlePuppetMouseDragged(float modelX, float modelY) { + if (!puppetMode || selectedPuppetPin == null) return; + + if (currentDragMode == DragMode.MOVE_PUPPET_PIN) { + float deltaX = modelX - dragStartX; + float deltaY = modelY - dragStartY; + + if (ctrlPressed) { + // Ctrl键按下:精确移动木偶控制点 + selectedPuppetPin.move(deltaX, deltaY); + } else { + // 普通移动:直接设置位置 + selectedPuppetPin.setPosition(modelX, modelY); + } + + // 更新拖拽起始位置 + dragStartX = modelX; + dragStartY = modelY; + + // 更新网格变形 + puppetTargetMesh.updateVerticesFromPuppetPins(); + puppetTargetMesh.markDirty(); + + repaint(); + } + } + + // ================== 二级顶点变形器方法 ================== + + /** + * 切换二级顶点变形模式 + */ + private void toggleSecondaryVertexMode() { + if (secondaryVertexMode) { + exitSecondaryVertexMode(); + } else { + executeInGLContext(this::enterSecondaryVertexMode); + } + } + + /** + * 进入二级顶点变形模式 + */ + private void enterSecondaryVertexMode() { + if (selectedMeshes.isEmpty()) { + logger.warn("请先选择一个网格以进入二级顶点变形模式"); + return; + } + + secondaryVertexMode = true; + secondaryVertexTargetMesh = lastSelectedMesh; + + if (secondaryVertexTargetMesh != null) { + // 显示二级顶点 + secondaryVertexTargetMesh.setShowSecondaryVertices(true); + + // 如果没有二级顶点,创建默认的四个角点 + if (secondaryVertexTargetMesh.getSecondaryVertexCount() == 0) { + createDefaultSecondaryVertices(); + } + } + + // 设置特殊光标 + setCursor(createSecondaryVertexCursor()); + + logger.info("进入二级顶点变形模式: {}", + secondaryVertexTargetMesh != null ? secondaryVertexTargetMesh.getName() : "null"); + + // 通知监听器 + for (ModelClickListener listener : clickListeners) { + try { + listener.onSecondaryVertexModeEntered(secondaryVertexTargetMesh); + } catch (Exception ex) { + logger.error("二级顶点模式进入事件监听器执行出错", ex); + } + } + + repaint(); + } + + /** + * 退出二级顶点变形模式 + */ + private void exitSecondaryVertexMode() { + executeInGLContext(() -> { + secondaryVertexMode = false; + if (secondaryVertexTargetMesh != null) { + secondaryVertexTargetMesh.setShowSecondaryVertices(false); + } + secondaryVertexTargetMesh = null; + selectedSecondaryVertex = null; + hoveredSecondaryVertex = null; + + // 恢复默认光标 + updateCursorForHoverState(); + + logger.info("退出二级顶点变形模式"); + + // 通知监听器 + for (ModelClickListener listener : clickListeners) { + try { + listener.onSecondaryVertexModeExited(); + } catch (Exception ex) { + logger.error("二级顶点模式退出事件监听器执行出错", ex); + } + } + + repaint(); + }); + } + + /** + * 创建默认的四个角点二级顶点 + */ + private void createDefaultSecondaryVertices() { + if (secondaryVertexTargetMesh == null) return; + + BoundingBox bounds = secondaryVertexTargetMesh.getBounds(); + if (bounds == null || !bounds.isValid()) return; + + float minX = bounds.getMinX(); + float minY = bounds.getMinY(); + float maxX = bounds.getMaxX(); + float maxY = bounds.getMaxY(); + + // 创建四个角点 + secondaryVertexTargetMesh.addSecondaryVertex(minX, minY, 0.0f, 1.0f); // 左下 + secondaryVertexTargetMesh.addSecondaryVertex(maxX, minY, 1.0f, 1.0f); // 右下 + secondaryVertexTargetMesh.addSecondaryVertex(maxX, maxY, 1.0f, 0.0f); // 右上 + secondaryVertexTargetMesh.addSecondaryVertex(minX, maxY, 0.0f, 0.0f); // 左上 + + logger.debug("为网格 {} 创建了4个默认二级顶点", secondaryVertexTargetMesh.getName()); + } + + /** + * 创建二级顶点模式光标 + */ + private Cursor createSecondaryVertexCursor() { + 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; + + // 外圈 + g2d.setColor(Color.GREEN); + g2d.setStroke(new BasicStroke(2f)); + g2d.drawOval(center - 6, center - 6, 12, 12); + + // 内圈 + g2d.setColor(new Color(0, 200, 0, 150)); + g2d.setStroke(new BasicStroke(1f)); + g2d.drawOval(center - 3, center - 3, 6, 6); + + // 中心点 + g2d.setColor(Color.GREEN); + g2d.fillOval(center - 1, center - 1, 2, 2); + + g2d.dispose(); + + return Toolkit.getDefaultToolkit().createCustomCursor(cursorImg, new Point(center, center), "SecondaryVertexCursor"); + } + + /** + * 在二级顶点模式下处理双击 + */ + private void handleSecondaryVertexDoubleClick(MouseEvent e) { + 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]; + + // 检查是否双击了二级顶点 + SecondaryVertex clickedVertex = findSecondaryVertexAtPosition(modelX, modelY); + if (clickedVertex != null) { + // 双击二级顶点:删除该顶点 + deleteSecondaryVertex(clickedVertex); + } else { + // 双击空白处:创建新的二级顶点 + createSecondaryVertexAt(modelX, modelY); + } + + } catch (Exception ex) { + logger.error("处理二级顶点双击时出错", ex); + } + }); + } + + /** + * 在指定位置创建二级顶点 + */ + private void createSecondaryVertexAt(float x, float y) { + if (secondaryVertexTargetMesh == null) return; + + // 计算UV坐标(基于边界框) + BoundingBox bounds = secondaryVertexTargetMesh.getBounds(); + if (bounds == null || !bounds.isValid()) return; + + float u = (x - bounds.getMinX()) / bounds.getWidth(); + float v = (y - bounds.getMinY()) / bounds.getHeight(); + + // 限制UV在0-1范围内 + u = Math.max(0.0f, Math.min(1.0f, u)); + v = Math.max(0.0f, Math.min(1.0f, v)); + + SecondaryVertex newVertex = secondaryVertexTargetMesh.addSecondaryVertex(x, y, u, v); + logger.info("创建二级顶点: ID={}, 位置({}, {}), UV({}, {})", + newVertex.getId(), x, y, u, v); + + repaint(); + } + + /** + * 删除二级顶点 + */ + private void deleteSecondaryVertex(SecondaryVertex vertex) { + if (secondaryVertexTargetMesh == null || vertex == null) return; + + boolean removed = secondaryVertexTargetMesh.removeSecondaryVertex(vertex); + if (removed) { + if (selectedSecondaryVertex == vertex) { + selectedSecondaryVertex = null; + } + if (hoveredSecondaryVertex == vertex) { + hoveredSecondaryVertex = null; + } + logger.info("删除二级顶点: ID={}", vertex.getId()); + } + + repaint(); + } + + /** + * 在指定位置查找二级顶点 + */ + private SecondaryVertex findSecondaryVertexAtPosition(float x, float y) { + if (secondaryVertexTargetMesh == null) return null; + + float tolerance = SECONDARY_VERTEX_TOLERANCE / calculateScaleFactor(); + + return secondaryVertexTargetMesh.selectSecondaryVertexAt(x, y, tolerance); + } + + /** + * 处理二级顶点模式下的鼠标移动 + */ + private void handleSecondaryVertexMouseMove(float modelX, float modelY) { + if (!secondaryVertexMode || secondaryVertexTargetMesh == null) return; + + // 更新悬停的二级顶点 + SecondaryVertex newHoveredVertex = findSecondaryVertexAtPosition(modelX, modelY); + + if (newHoveredVertex != hoveredSecondaryVertex) { + hoveredSecondaryVertex = newHoveredVertex; + + // 更新光标 + if (hoveredSecondaryVertex != null) { + setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + setCursor(createSecondaryVertexCursor()); + } + } + } + + /** + * 处理二级顶点模式下的鼠标按下 + */ + private void handleSecondaryVertexMousePressed(float modelX, float modelY) { + if (!secondaryVertexMode || secondaryVertexTargetMesh == null) return; + + // 选择二级顶点 + SecondaryVertex clickedVertex = findSecondaryVertexAtPosition(modelX, modelY); + if (clickedVertex != null) { + secondaryVertexTargetMesh.setSelectedSecondaryVertex(clickedVertex); + selectedSecondaryVertex = clickedVertex; + + // 开始拖拽 + currentDragMode = DragMode.MOVE_SECONDARY_VERTEX; + dragStartX = modelX; + dragStartY = modelY; + + logger.debug("开始移动二级顶点: ID={}", clickedVertex.getId()); + } else { + // 点击空白处,取消选择 + secondaryVertexTargetMesh.setSelectedSecondaryVertex(null); + selectedSecondaryVertex = null; + } + } + + /** + * 处理二级顶点模式下的鼠标拖拽 + */ + private void handleSecondaryVertexMouseDragged(float modelX, float modelY) { + if (!secondaryVertexMode || selectedSecondaryVertex == null) return; + + if (currentDragMode == DragMode.MOVE_SECONDARY_VERTEX) { + float deltaX = modelX - dragStartX; + float deltaY = modelY - dragStartY; + + if (ctrlPressed) { + // Ctrl键按下:精确移动二级顶点 + selectedSecondaryVertex.move(deltaX, deltaY); + } else { + // 普通移动:直接设置位置 + selectedSecondaryVertex.setPosition(modelX, modelY); + } + + // 更新拖拽起始位置 + dragStartX = modelX; + dragStartY = modelY; + + secondaryVertexTargetMesh.markDirty(); + + repaint(); + } + } /** * 处理双击事件 */ private void handleDoubleClick(MouseEvent e) { + if (secondaryVertexMode) { + handleSecondaryVertexDoubleClick(e); + return; + } + if (liquifyMode) { // 如果在液化模式下双击,退出液化模式 exitLiquifyMode(); @@ -270,6 +986,11 @@ public class ModelRenderPanel extends JPanel { float modelX = modelCoords[0]; float modelY = modelCoords[1]; + if (puppetMode) { + handlePuppetDoubleClick(e); + return; + } + // 检测双击的网格 Mesh2D clickedMesh = findMeshAtPosition(modelX, modelY); if (clickedMesh != null) { @@ -282,7 +1003,6 @@ public class ModelRenderPanel extends JPanel { }); } - /** * 进入液化模式 */ @@ -368,10 +1088,6 @@ public class ModelRenderPanel extends JPanel { liquifyTargetPart.applyLiquify(brushCenter, liquifyBrushSize, liquifyBrushStrength, currentLiquifyMode, 1, createVertices); - //logger.info("应用液化效果: 位置({}, {}), 大小{}, 强度{}, 模式{}, 创建顶点{}", - // modelX, modelY, liquifyBrushSize, liquifyBrushStrength, - // currentLiquifyMode, createVertices); - } catch (Exception ex) { logger.error("应用液化效果时出错", ex); } @@ -456,79 +1172,31 @@ public class ModelRenderPanel extends JPanel { } /** - * 处理单单击事件 + * 获取当前二级顶点模式状态 */ - private void handleSingleClick() { - if (liquifyMode) { - handleLiquifyClick(); - } + public boolean isInSecondaryVertexMode() { + return secondaryVertexMode; } /** - * 初始化键盘快捷键 + * 获取二级顶点目标网格 */ - private void initKeyboardShortcuts() { - // 获取输入映射和动作映射 - InputMap inputMap = getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); - ActionMap actionMap = getActionMap(); + public Mesh2D getSecondaryVertexTargetMesh() { + return secondaryVertexTargetMesh; + } - // 撤回快捷键:Ctrl+Z - KeyStroke undoKey = KeyStroke.getKeyStroke(KeyEvent.VK_Z, KeyEvent.CTRL_DOWN_MASK); - inputMap.put(undoKey, "undo"); - actionMap.put("undo", new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - undo(); - } - }); + /** + * 处理单单击事件 + */ + private void handleSingleClick() { + if (secondaryVertexMode) { + // 二级顶点模式下单击无特殊操作 + return; + } - // 重做快捷键:Ctrl+Y 或 Ctrl+Shift+Z - KeyStroke redoKey1 = KeyStroke.getKeyStroke(KeyEvent.VK_Y, KeyEvent.CTRL_DOWN_MASK); - KeyStroke redoKey2 = KeyStroke.getKeyStroke(KeyEvent.VK_Z, KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK); - inputMap.put(redoKey1, "redo"); - inputMap.put(redoKey2, "redo"); - actionMap.put("redo", new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - redo(); - } - }); - - // 清除历史记录:Ctrl+Shift+Delete - KeyStroke clearHistoryKey = KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK); - inputMap.put(clearHistoryKey, "clearHistory"); - actionMap.put("clearHistory", new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - clearHistory(); - } - }); - - // 添加摄像机重置快捷键:Ctrl+R - KeyStroke resetCameraKey = KeyStroke.getKeyStroke(KeyEvent.VK_R, KeyEvent.CTRL_DOWN_MASK); - inputMap.put(resetCameraKey, "resetCamera"); - actionMap.put("resetCamera", new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - resetCamera(); - logger.info("重置摄像机"); - } - }); - - // 添加摄像机启用/禁用快捷键:Ctrl+E - KeyStroke toggleCameraKey = KeyStroke.getKeyStroke(KeyEvent.VK_E, KeyEvent.CTRL_DOWN_MASK); - inputMap.put(toggleCameraKey, "toggleCamera"); - actionMap.put("toggleCamera", new AbstractAction() { - @Override - public void actionPerformed(ActionEvent e) { - //executeInGLContext(() -> { - Camera camera = ModelRender.getCamera(); - boolean newState = !camera.isEnabled(); - camera.setEnabled(newState); - logger.info("{}摄像机", newState ? "启用" : "禁用"); - //}); - } - }); + if (liquifyMode) { + handleLiquifyClick(); + } } // ============ 新增:操作历史记录方法 ============ @@ -709,7 +1377,6 @@ public class ModelRenderPanel extends JPanel { * 设置选中的网格(单选) */ public void setSelectedMesh(Mesh2D mesh) { - //executeInGLContext(() -> { // 清除之前选中的所有网格 for (Mesh2D selectedMesh : selectedMeshes) { selectedMesh.setSelected(false); @@ -732,7 +1399,6 @@ public class ModelRenderPanel extends JPanel { logger.debug("设置选中网格: {}, 当前选中数量: {}", mesh != null ? mesh.getName() : "null", selectedMeshes.size()); - //}); } /** @@ -785,6 +1451,15 @@ public class ModelRenderPanel extends JPanel { exitLiquifyMode(); } + if (puppetMode) { + exitPuppetMode(); + } + + // 如果当前在二级顶点模式,先退出二级顶点模式 + if (secondaryVertexMode) { + exitSecondaryVertexMode(); + } + for (Mesh2D mesh : selectedMeshes) { mesh.setSelected(false); mesh.setSuspension(false); @@ -1040,7 +1715,6 @@ public class ModelRenderPanel extends JPanel { } }); - addMouseMotionListener(new MouseMotionAdapter() { @Override public void mouseMoved(MouseEvent e) { @@ -1120,7 +1794,6 @@ public class ModelRenderPanel extends JPanel { } }); - addMouseWheelListener(new MouseWheelListener() { @Override public void mouseWheelMoved(MouseWheelEvent e) { @@ -1173,6 +1846,41 @@ public class ModelRenderPanel extends JPanel { return; } + // 二级顶点模式下的左键处理 + if (secondaryVertexMode && SwingUtilities.isLeftMouseButton(e)) { + executeInGLContext(() -> { + try { + float[] modelCoords = screenToModelCoordinates(screenX, screenY); + if (modelCoords == null) return; + + float modelX = modelCoords[0]; + float modelY = modelCoords[1]; + + handleSecondaryVertexMousePressed(modelX, modelY); + } catch (Exception ex) { + logger.error("处理二级顶点鼠标按下时出错", ex); + } + }); + return; + } + + if (puppetMode && SwingUtilities.isLeftMouseButton(e)) { + executeInGLContext(() -> { + try { + float[] modelCoords = screenToModelCoordinates(screenX, screenY); + if (modelCoords == null) return; + + float modelX = modelCoords[0]; + float modelY = modelCoords[1]; + + handlePuppetMousePressed(modelX, modelY); + } catch (Exception ex) { + logger.error("处理木偶工具鼠标按下时出错", ex); + } + }); + return; + } + // 液化模式下的左键处理 if (liquifyMode && SwingUtilities.isLeftMouseButton(e)) { // 在液化模式下,左键按下直接开始液化操作 @@ -1335,6 +2043,12 @@ public class ModelRenderPanel extends JPanel { case MOVE_PIVOT: // 使用十字光标表示移动中心点 return Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR); + case MOVE_SECONDARY_VERTEX: + // 移动二级顶点时使用手型光标 + return Cursor.getPredefinedCursor(Cursor.HAND_CURSOR); + case MOVE_PUPPET_PIN: + // 移动木偶控制点时使用手型光标 + return Cursor.getPredefinedCursor(Cursor.HAND_CURSOR); case NONE: default: return Cursor.getDefaultCursor(); @@ -1471,7 +2185,6 @@ public class ModelRenderPanel extends JPanel { return worldBounds; } - /** * 检查是否点击了选择框的调整手柄 */ @@ -1566,7 +2279,6 @@ public class ModelRenderPanel extends JPanel { return result; } - /** * 检查点是否在中心点区域内 */ @@ -1607,7 +2319,6 @@ public class ModelRenderPanel extends JPanel { return base * displayScale; } - /** * 检查点是否在角点区域内 */ @@ -1649,6 +2360,46 @@ public class ModelRenderPanel extends JPanel { return; } + // 二级顶点模式下的拖拽处理 + if (secondaryVertexMode && SwingUtilities.isLeftMouseButton(e)) { + 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]; + + handleSecondaryVertexMouseDragged(modelX, modelY); + } catch (Exception ex) { + logger.error("处理二级顶点拖拽时出错", ex); + } + }); + return; + } + + // 木偶工具模式下的拖拽处理 + if (puppetMode && SwingUtilities.isLeftMouseButton(e)) { + 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]; + + handlePuppetMouseDragged(modelX, modelY); + } catch (Exception ex) { + logger.error("处理木偶工具拖拽时出错", ex); + } + }); + return; + } + // 液化模式下的拖拽处理(优先于其他模式) if (liquifyMode && SwingUtilities.isLeftMouseButton(e)) { handleLiquifyDrag(e); @@ -1720,10 +2471,6 @@ public class ModelRenderPanel extends JPanel { liquifyTargetPart.applyLiquify(brushCenter, liquifyBrushSize, liquifyBrushStrength, currentLiquifyMode, 1, createVertices); - //logger.info("拖拽应用液化效果: 位置({}, {}), 大小{}, 强度{}, 模式{}, 创建顶点{}", - // modelX, modelY, liquifyBrushSize, liquifyBrushStrength, - // currentLiquifyMode, createVertices); - } catch (Exception ex) { logger.error("应用液化拖拽效果时出错", ex); } @@ -1768,6 +2515,9 @@ public class ModelRenderPanel extends JPanel { for (ModelPart part : selectedParts) { Vector2f pos = part.getPosition(); part.setPosition(pos.x + deltaX, pos.y + deltaY); + + // 同步移动该部件下的所有网格的二级顶点 + syncSecondaryVerticesForPart(part, deltaX, deltaY); } // 更新拖拽起始位置 @@ -1778,6 +2528,29 @@ public class ModelRenderPanel extends JPanel { updateMultiSelectionBoundsForSelectedMeshes(); } + /** + * 同步部件下所有网格的二级顶点位置 + */ + private void syncSecondaryVerticesForPart(ModelPart part, float deltaX, float deltaY) { + if (part == null) return; + + // 获取部件下的所有网格 + List meshes = part.getMeshes(); + if (meshes == null) return; + + for (Mesh2D mesh : meshes) { + if (mesh != null && mesh.getSecondaryVertexCount() > 0) { + // 移动二级顶点 + mesh.moveSecondaryVertices(deltaX, deltaY); + } + } + + // 递归处理子部件 + for (ModelPart child : part.getChildren()) { + syncSecondaryVerticesForPart(child, deltaX, deltaY); + } + } + /** * 处理旋转拖拽 */ @@ -1812,7 +2585,6 @@ public class ModelRenderPanel extends JPanel { logger.debug("旋转角度增量: {} 度", Math.toDegrees(deltaAngle)); } - /** * 处理调整大小拖拽 */ @@ -1872,6 +2644,9 @@ public class ModelRenderPanel extends JPanel { for (ModelPart part : selectedParts) { Vector2f currentScale = part.getScale(); part.setScale(currentScale.x * relScaleX, currentScale.y * relScaleY); + + // 同步缩放该部件下的所有网格的二级顶点 + syncSecondaryVerticesScaleForPart(part, relScaleX, relScaleY); } // 更新拖拽起始位置和初始尺寸 @@ -1884,6 +2659,26 @@ public class ModelRenderPanel extends JPanel { updateMultiSelectionBoundsForSelectedMeshes(); } + private void syncSecondaryVerticesScaleForPart(ModelPart part, float scaleX, float scaleY) { + if (part == null) return; + + // 获取部件下的所有网格 + List meshes = part.getMeshes(); + if (meshes == null) return; + + for (Mesh2D mesh : meshes) { + if (mesh != null && mesh.getSecondaryVertexCount() > 0) { + // 同步二级顶点到新的边界 + mesh.syncSecondaryVerticesToBounds(); + } + } + + // 递归处理子部件 + for (ModelPart child : part.getChildren()) { + syncSecondaryVerticesScaleForPart(child, scaleX, scaleY); + } + } + /** * 更新所有选中网格的多选边界框 */ @@ -1899,7 +2694,6 @@ public class ModelRenderPanel extends JPanel { //} } - /** * 处理鼠标释放事件(结束拖拽并记录操作历史) */ @@ -1912,6 +2706,14 @@ public class ModelRenderPanel extends JPanel { return; } + // 二级顶点模式下的释放处理 + if (secondaryVertexMode && SwingUtilities.isLeftMouseButton(e)) { + // 二级顶点模式下不需要记录操作历史,直接重置状态 + currentDragMode = DragMode.NONE; + logger.debug("二级顶点拖拽结束"); + return; + } + // 液化模式下的释放处理 if (liquifyMode && SwingUtilities.isLeftMouseButton(e)) { // 液化模式下不需要记录操作历史,直接重置状态 @@ -1922,8 +2724,6 @@ public class ModelRenderPanel extends JPanel { if (currentDragMode != DragMode.NONE) { // 记录操作历史 - //executeInGLContext(() -> { - try { List selectedParts = getSelectedParts(); switch (currentDragMode) { @@ -1951,7 +2751,6 @@ public class ModelRenderPanel extends JPanel { } catch (Exception ex) { logger.error("记录操作历史时出错", ex); } - //}); } // 重置状态 @@ -2085,6 +2884,18 @@ public class ModelRenderPanel extends JPanel { float modelX = modelCoords[0]; float modelY = modelCoords[1]; + // 木偶工具模式下的鼠标移动处理 + if (puppetMode) { + handlePuppetMouseMove(modelX, modelY); + return; + } + + // 二级顶点模式下的鼠标移动处理 + if (secondaryVertexMode) { + handleSecondaryVertexMouseMove(modelX, modelY); + return; + } + // 检测悬停的网格 Mesh2D newHoveredMesh = findMeshAtPosition(modelX, modelY); @@ -2142,6 +2953,26 @@ public class ModelRenderPanel extends JPanel { return; } + // 二级顶点模式下的光标处理 + if (secondaryVertexMode) { + if (hoveredSecondaryVertex != null) { + setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + setCursor(createSecondaryVertexCursor()); + } + return; + } + + // 木偶工具模式下的光标处理 + if (puppetMode) { + if (hoveredPuppetPin != null) { + setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else { + setCursor(createPuppetCursor()); + } + return; + } + Cursor newCursor = Cursor.getDefaultCursor(); isOverSelection = false; @@ -2185,6 +3016,20 @@ public class ModelRenderPanel extends JPanel { } } + /** + * 获取当前木偶工具模式状态 + */ + public boolean isInPuppetMode() { + return puppetMode; + } + + /** + * 获取木偶工具目标网格 + */ + public Mesh2D getPuppetTargetMesh() { + return puppetTargetMesh; + } + /** * 将屏幕坐标转换为模型坐标 */ @@ -2210,7 +3055,6 @@ public class ModelRenderPanel extends JPanel { return new float[]{modelX, modelY}; } - /** * 在指定位置查找网格 */ @@ -2268,8 +3112,6 @@ public class ModelRenderPanel extends JPanel { } } - - /** * 获取模型的边界框 */ @@ -2285,21 +3127,6 @@ public class ModelRenderPanel extends JPanel { } } - /** - * 查找模型中的第一个网格 - */ - private Mesh2D findFirstMesh(Model2D model) { - if (model == null) return null; - - try { - Method getMeshesMethod = model.getClass().getMethod("getMeshes"); - List meshes = (List) getMeshesMethod.invoke(model); - return meshes.isEmpty() ? null : meshes.get(0); - } catch (Exception e) { - return null; - } - } - /** * 创建离屏 OpenGL 上下文 */ 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 fed5627..c25313c 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java @@ -3,6 +3,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.PuppetPin; import com.chuangzhou.vivid2D.render.systems.Matrix3fUtils; import com.chuangzhou.vivid2D.render.model.util.Mesh2D; import org.joml.Matrix3f; @@ -1668,11 +1669,45 @@ public class ModelPart { logger.warn("更新网格pivot时出错: {}", e.getMessage()); } + updatePuppetPinsPosition(mesh); + // 标记网格需要更新 mesh.markDirty(); mesh.setBakedToWorld(true); } + /** + * 更新木偶控制点的位置 + */ + private void updatePuppetPinsPosition(Mesh2D mesh) { + if (mesh == null) return; + + List puppetPins = mesh.getPuppetPins(); + if (puppetPins.isEmpty()) return; + + // 确保世界变换是最新的 + if (transformDirty) { + updateLocalTransform(); + recomputeWorldTransformRecursive(); + } + + for (PuppetPin pin : puppetPins) { + // 获取控制点的原始局部位置 + Vector2f originalLocalPos = pin.getOriginalPosition(); + + // 将原始局部位置变换到世界坐标 + Vector2f worldPos = Matrix3fUtils.transformPoint(worldTransform, originalLocalPos); + + // 更新控制点的当前位置 + pin.setPosition(worldPos.x, worldPos.y); + + logger.trace("更新木偶控制点位置: 局部({}, {}) -> 世界({}, {})", + originalLocalPos.x, originalLocalPos.y, worldPos.x, worldPos.y); + } + + logger.debug("同步更新了 {} 个木偶控制点的位置", puppetPins.size()); + } + public void setPosition(Vector2f pos) { // 记录旧世界变换和旧位置,用于计算位移 Matrix3f oldWorldTransform = new Matrix3f(this.worldTransform); @@ -1701,12 +1736,46 @@ public class ModelPart { mesh.setOriginalPivot(newLocalOriginalPivot); mesh.setPivot(movedWorldPivot.x, movedWorldPivot.y); + + // ==================== 新增:同步更新木偶控制点的原始位置 ==================== + updatePuppetPinsOriginalPosition(mesh, oldWorldTransform, dx, dy); } // 更新网格顶点位置 updateMeshVertices(); } + /** + * 更新木偶控制点的原始位置 + */ + private void updatePuppetPinsOriginalPosition(Mesh2D mesh, Matrix3f oldWorldTransform, float dx, float dy) { + if (mesh == null) return; + + List puppetPins = mesh.getPuppetPins(); + if (puppetPins.isEmpty()) return; + + for (PuppetPin pin : puppetPins) { + // 获取控制点的当前原始位置(局部坐标) + Vector2f currentOriginalPos = pin.getOriginalPosition(); + + // 将原始位置变换到旧的世界坐标系 + Vector2f oldWorldPos = Matrix3fUtils.transformPoint(oldWorldTransform, currentOriginalPos); + + // 在世界坐标系中应用位移 + Vector2f newWorldPos = new Vector2f(oldWorldPos.x + dx, oldWorldPos.y + dy); + + // 将新的世界位置逆变换回新的局部坐标系 + Vector2f newLocalOriginalPos = Matrix3fUtils.transformPointInverse(this.worldTransform, newWorldPos); + + // 更新控制点的原始位置 + pin.setOriginalPosition(newLocalOriginalPos.x, newLocalOriginalPos.y); + + logger.trace("更新木偶控制点原始位置: 旧局部({}, {}) -> 新局部({}, {})", + currentOriginalPos.x, currentOriginalPos.y, newLocalOriginalPos.x, newLocalOriginalPos.y); + } + } + + /** * 移动部件 */ @@ -1748,6 +1817,9 @@ public class ModelPart { Vector2f newLocalOriginalPivot = Matrix3fUtils.transformPointInverse(this.worldTransform, oldWorldPivot); mesh.setOriginalPivot(newLocalOriginalPivot); mesh.setPivot(Matrix3fUtils.transformPoint(this.worldTransform, newLocalOriginalPivot)); + + // ==================== 新增:同步更新木偶控制点的原始位置 ==================== + updatePuppetPinsOriginalPositionForTransform(mesh, oldWorldTransform); } updateMeshVertices(); @@ -1793,12 +1865,40 @@ public class ModelPart { Vector2f newLocalOriginalPivot = Matrix3fUtils.transformPointInverse(this.worldTransform, oldWorldPivot); mesh.setOriginalPivot(newLocalOriginalPivot); mesh.setPivot(Matrix3fUtils.transformPoint(this.worldTransform, newLocalOriginalPivot)); + + // ==================== 新增:同步更新木偶控制点的原始位置 ==================== + updatePuppetPinsOriginalPositionForTransform(mesh, oldWorldTransform); } updateMeshVertices(); triggerEvent("scale"); } + /** + * 为变换操作更新木偶控制点的原始位置 + */ + private void updatePuppetPinsOriginalPositionForTransform(Mesh2D mesh, Matrix3f oldWorldTransform) { + if (mesh == null) return; + + List puppetPins = mesh.getPuppetPins(); + if (puppetPins.isEmpty()) return; + + for (PuppetPin pin : puppetPins) { + // 获取控制点的当前原始位置(局部坐标) + Vector2f currentOriginalPos = pin.getOriginalPosition(); + + // 将原始位置变换到旧的世界坐标系 + Vector2f oldWorldPos = Matrix3fUtils.transformPoint(oldWorldTransform, currentOriginalPos); + + // 将旧的世界位置逆变换回新的局部坐标系 + Vector2f newLocalOriginalPos = Matrix3fUtils.transformPointInverse(this.worldTransform, oldWorldPos); + + // 更新控制点的原始位置 + pin.setOriginalPosition(newLocalOriginalPos.x, newLocalOriginalPos.y); + } + } + + public void setScale(float uniformScale) { // 记录旧的世界变换,用于计算 pivot 的相对位置 @@ -1894,6 +1994,9 @@ public class ModelPart { mesh.setPivot(worldPivot.x, worldPivot.y); } catch (Exception ignored) { } + // ==================== 新增:初始化木偶控制点的位置 ==================== + initializePuppetPinsPosition(mesh); + // 标记为已烘焙到世界坐标(语义上明确),并确保 bounds/dirty 状态被正确刷新 mesh.setBakedToWorld(true); @@ -1905,6 +2008,63 @@ public class ModelPart { boundsDirty = true; } + /** + * 更新木偶控制点的位置(修复移动问题) + */ + private void updatePuppetPinsForMovement(Mesh2D mesh, Matrix3f oldWorldTransform, float dx, float dy) { + if (mesh == null) return; + + List puppetPins = mesh.getPuppetPins(); + if (puppetPins.isEmpty()) return; + + for (PuppetPin pin : puppetPins) { + // 获取控制点的当前位置(世界坐标) + Vector2f currentWorldPos = pin.getPosition(); + + // 应用相同的位移到控制点 + Vector2f newWorldPos = new Vector2f(currentWorldPos.x + dx, currentWorldPos.y + dy); + + // 更新控制点的位置 + pin.setPosition(newWorldPos.x, newWorldPos.y); + + // 同时更新控制点的原始位置,保持一致性 + Vector2f currentOriginalPos = pin.getOriginalPosition(); + Vector2f newOriginalPos = new Vector2f(currentOriginalPos.x + dx, currentOriginalPos.y + dy); + pin.setOriginalPosition(newOriginalPos.x, newOriginalPos.y); + + logger.trace("移动木偶控制点: ({}, {}) -> ({}, {})", + currentWorldPos.x, currentWorldPos.y, newWorldPos.x, newWorldPos.y); + } + + logger.debug("移动时同步更新了 {} 个木偶控制点", puppetPins.size()); + } + + /** + * 初始化木偶控制点的位置 + */ + private void initializePuppetPinsPosition(Mesh2D mesh) { + if (mesh == null) return; + + List puppetPins = mesh.getPuppetPins(); + if (puppetPins.isEmpty()) return; + + for (PuppetPin pin : puppetPins) { + // 获取控制点的原始局部位置 + Vector2f originalLocalPos = pin.getOriginalPosition(); + + // 将原始局部位置变换到世界坐标 + Vector2f worldPos = Matrix3fUtils.transformPoint(this.worldTransform, originalLocalPos); + + // 设置控制点的当前位置 + pin.setPosition(worldPos.x, worldPos.y); + + // 同时保存当前位置为原始位置(确保一致性) + pin.saveAsOriginal(); + } + + logger.debug("初始化了 {} 个木偶控制点的位置", puppetPins.size()); + } + /** * 设置中心点 */ diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/data/MeshData.java b/src/main/java/com/chuangzhou/vivid2D/render/model/data/MeshData.java index 01da532..de44c6b 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/data/MeshData.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/data/MeshData.java @@ -1,12 +1,24 @@ package com.chuangzhou.vivid2D.render.model.data; import com.chuangzhou.vivid2D.render.model.util.Mesh2D; +import com.chuangzhou.vivid2D.render.model.util.PuppetPin; +import com.chuangzhou.vivid2D.render.model.util.SecondaryVertex; +import org.joml.Vector2f; +import org.joml.Vector4f; import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +/** + * @author tzdwindows 7 + */ public class MeshData implements Serializable { private static final long serialVersionUID = 1L; + // 基础网格数据 public String name; public float[] vertices; public float[] uvs; @@ -15,9 +27,45 @@ public class MeshData implements Serializable { public boolean visible; public int drawMode; + // 原始顶点数据(用于变形恢复) + public float[] originalVertices; + + // 变换相关 + public Vector2f pivot; + public Vector2f originalPivot; + + // 选择状态 + public boolean selected; + public boolean bakedToWorld; + + // ==================== 二级顶点支持 ==================== + public List secondaryVertices = new ArrayList<>(); + public boolean showSecondaryVertices = false; + public Vector4f secondaryVertexColor = new Vector4f(0.0f, 1.0f, 0.0f, 1.0f); + public Vector4f selectedSecondaryVertexColor = new Vector4f(1.0f, 0.0f, 0.0f, 1.0f); + public float secondaryVertexSize = 6.0f; + public Integer selectedSecondaryVertexId = null; + + // ==================== 木偶工具支持 ==================== + public List puppetPins = new ArrayList<>(); + public Integer selectedPuppetPinId = null; + public boolean showPuppetPins = true; + public Vector4f puppetPinColor = new Vector4f(1.0f, 0.0f, 0.0f, 1.0f); + public Vector4f selectedPuppetPinColor = new Vector4f(1.0f, 1.0f, 0.0f, 1.0f); + public float puppetPinSize = 8.0f; + + // ==================== 液化状态支持 ==================== + public boolean showLiquifyOverlay = false; + public Vector4f liquifyOverlayColor = new Vector4f(1.0f, 0.5f, 0.0f, 0.3f); + + // ==================== 渲染状态 ==================== + public boolean isRenderVertices = false; + public MeshData() { this.visible = true; this.drawMode = Mesh2D.TRIANGLES; + this.pivot = new Vector2f(0, 0); + this.originalPivot = new Vector2f(0, 0); } public MeshData(Mesh2D mesh) { @@ -28,19 +76,153 @@ public class MeshData implements Serializable { this.indices = mesh.getIndices(); this.visible = mesh.isVisible(); this.drawMode = mesh.getDrawMode(); + this.selected = mesh.isSelected(); + this.bakedToWorld = mesh.isBakedToWorld(); + this.isRenderVertices = mesh.isRenderVertices(); + + // 保存原始顶点数据 + this.originalVertices = mesh.getOriginalVertices(); + + // 保存变换数据 + this.pivot = new Vector2f(mesh.getPivot()); + this.originalPivot = new Vector2f(mesh.getOriginalPivot()); + + // 保存二级顶点数据 + saveSecondaryVertices(mesh); + + // 保存木偶控制点数据 + savePuppetPins(mesh); + + // 保存液化状态 + this.showLiquifyOverlay = mesh.showLiquifyOverlay; + this.liquifyOverlayColor = new Vector4f(mesh.getLiquifyOverlayColor()); if (mesh.getTexture() != null) { this.textureName = mesh.getTexture().getName(); } } + /** + * 保存二级顶点数据 + */ + private void saveSecondaryVertices(Mesh2D mesh) { + List vertices = mesh.getSecondaryVertices(); + SecondaryVertex selectedVertex = mesh.getSelectedSecondaryVertex(); + + for (SecondaryVertex vertex : vertices) { + SecondaryVertexData data = new SecondaryVertexData(vertex); + secondaryVertices.add(data); + + // 记录选中的二级顶点ID + if (selectedVertex != null && selectedVertex.getId() == vertex.getId()) { + selectedSecondaryVertexId = vertex.getId(); + } + } + + this.showSecondaryVertices = mesh.isShowSecondaryVertices(); + this.secondaryVertexColor = new Vector4f(mesh.secondaryVertexColor); + this.selectedSecondaryVertexColor = new Vector4f(mesh.selectedSecondaryVertexColor); + this.secondaryVertexSize = mesh.secondaryVertexSize; + } + + /** + * 保存木偶控制点数据 + */ + private void savePuppetPins(Mesh2D mesh) { + List pins = mesh.getPuppetPins(); + PuppetPin selectedPin = mesh.getSelectedPuppetPin(); + + for (PuppetPin pin : pins) { + PuppetPinData data = new PuppetPinData(pin); + puppetPins.add(data); + + // 记录选中的木偶控制点ID + if (selectedPin != null && selectedPin.getId() == pin.getId()) { + selectedPuppetPinId = pin.getId(); + } + } + + this.showPuppetPins = mesh.getShowPuppetPins(); + this.puppetPinColor = new Vector4f(mesh.getPuppetPinColor()); + this.selectedPuppetPinColor = new Vector4f(mesh.getSelectedPuppetPinColor()); + this.puppetPinSize = mesh.getPuppetPinSize(); + } + public Mesh2D toMesh2D() { Mesh2D mesh = new Mesh2D(name, vertices, uvs, indices); mesh.setVisible(visible); mesh.setDrawMode(drawMode); + mesh.setSelected(selected); + mesh.setBakedToWorld(bakedToWorld); + mesh.setRenderVertices(isRenderVertices); + + // 恢复原始顶点数据 + if (originalVertices != null) { + mesh.setOriginalVertices(originalVertices); + } + + // 恢复变换数据 + mesh.setPivot(pivot); + mesh.setOriginalPivot(originalPivot); + + // 恢复二级顶点 + restoreSecondaryVertices(mesh); + + // 恢复木偶控制点 + restorePuppetPins(mesh); + + // 恢复液化状态 + mesh.setShowLiquifyOverlay(showLiquifyOverlay); + mesh.getLiquifyOverlayColor().set(liquifyOverlayColor); + return mesh; } + /** + * 恢复二级顶点数据 + */ + private void restoreSecondaryVertices(Mesh2D mesh) { + for (SecondaryVertexData data : secondaryVertices) { + SecondaryVertex vertex = mesh.addSecondaryVertex(data.position.x, data.position.y, data.uv.x, data.uv.y); + vertex.setId(data.id); + vertex.setOriginalPosition(data.originalPosition); + + // 恢复选中状态 + if (selectedSecondaryVertexId != null && data.id == selectedSecondaryVertexId) { + mesh.setSelectedSecondaryVertex(vertex); + } + } + + mesh.setShowSecondaryVertices(showSecondaryVertices); + mesh.setSecondaryVertexColor(secondaryVertexColor); + mesh.setSelectedSecondaryVertexColor(selectedSecondaryVertexColor); + mesh.setSecondaryVertexSize(secondaryVertexSize); + } + + /** + * 恢复木偶控制点数据 + */ + private void restorePuppetPins(Mesh2D mesh) { + for (PuppetPinData data : puppetPins) { + PuppetPin pin = mesh.addPuppetPin(data.position.x, data.position.y, data.uv.x, data.uv.y); + pin.setId(data.id); + pin.setOriginalPosition(data.originalPosition); + pin.setInfluenceRadius(data.influenceRadius); + pin.setName(data.name); + + // 恢复权重映射 + pin.getWeightMap().putAll(data.weightMap); + + // 恢复选中状态 + if (selectedPuppetPinId != null && data.id == selectedPuppetPinId) { + mesh.setSelectedPuppetPin(pin); + } + } + + mesh.setShowPuppetPins(showPuppetPins); + // 注意:木偶控制点颜色等设置需要在Mesh2D中添加相应的setter方法 + } + public MeshData copy() { MeshData copy = new MeshData(); copy.name = this.name; @@ -50,6 +232,123 @@ public class MeshData implements Serializable { copy.textureName = this.textureName; copy.visible = this.visible; copy.drawMode = this.drawMode; + copy.selected = this.selected; + copy.bakedToWorld = this.bakedToWorld; + copy.isRenderVertices = this.isRenderVertices; + + // 复制原始顶点数据 + copy.originalVertices = this.originalVertices != null ? this.originalVertices.clone() : null; + + // 复制变换数据 + copy.pivot = new Vector2f(this.pivot); + copy.originalPivot = new Vector2f(this.originalPivot); + + // 复制二级顶点数据 + copy.secondaryVertices = new ArrayList<>(); + for (SecondaryVertexData data : this.secondaryVertices) { + copy.secondaryVertices.add(data.copy()); + } + copy.showSecondaryVertices = this.showSecondaryVertices; + copy.secondaryVertexColor = new Vector4f(this.secondaryVertexColor); + copy.selectedSecondaryVertexColor = new Vector4f(this.selectedSecondaryVertexColor); + copy.secondaryVertexSize = this.secondaryVertexSize; + copy.selectedSecondaryVertexId = this.selectedSecondaryVertexId; + + // 复制木偶控制点数据 + copy.puppetPins = new ArrayList<>(); + for (PuppetPinData data : this.puppetPins) { + copy.puppetPins.add(data.copy()); + } + copy.selectedPuppetPinId = this.selectedPuppetPinId; + copy.showPuppetPins = this.showPuppetPins; + copy.puppetPinColor = new Vector4f(this.puppetPinColor); + copy.selectedPuppetPinColor = new Vector4f(this.selectedPuppetPinColor); + copy.puppetPinSize = this.puppetPinSize; + + // 复制液化状态 + copy.showLiquifyOverlay = this.showLiquifyOverlay; + copy.liquifyOverlayColor = new Vector4f(this.liquifyOverlayColor); + return copy; } -} + + // ==================== 内部数据类 ==================== + + /** + * 二级顶点数据类(可序列化) + */ + public static class SecondaryVertexData implements Serializable { + private static final long serialVersionUID = 1L; + + public int id; + public Vector2f position; + public Vector2f originalPosition; + public Vector2f uv; + public boolean selected; + + public SecondaryVertexData() {} + + public SecondaryVertexData(SecondaryVertex vertex) { + this.id = vertex.getId(); + this.position = new Vector2f(vertex.getPosition()); + this.originalPosition = new Vector2f(vertex.getOriginalPosition()); + this.uv = new Vector2f(vertex.getUV()); + this.selected = vertex.isSelected(); + } + + public SecondaryVertexData copy() { + SecondaryVertexData copy = new SecondaryVertexData(); + copy.id = this.id; + copy.position = new Vector2f(this.position); + copy.originalPosition = new Vector2f(this.originalPosition); + copy.uv = new Vector2f(this.uv); + copy.selected = this.selected; + return copy; + } + } + + /** + * 木偶控制点数据类(可序列化) + */ + public static class PuppetPinData implements Serializable { + private static final long serialVersionUID = 1L; + + public int id; + public Vector2f position; + public Vector2f originalPosition; + public Vector2f uv; + public float influenceRadius; + public boolean selected; + public String name; + public Map weightMap; + + public PuppetPinData() { + this.weightMap = new HashMap<>(); + } + + public PuppetPinData(PuppetPin pin) { + this(); + this.id = pin.getId(); + this.position = new Vector2f(pin.getPosition()); + this.originalPosition = new Vector2f(pin.getOriginalPosition()); + this.uv = new Vector2f(pin.getUV()); + this.influenceRadius = pin.getInfluenceRadius(); + this.selected = pin.isSelected(); + this.name = pin.getName(); + this.weightMap = new HashMap<>(pin.getWeightMap()); + } + + public PuppetPinData copy() { + PuppetPinData copy = new PuppetPinData(); + copy.id = this.id; + copy.position = new Vector2f(this.position); + copy.originalPosition = new Vector2f(this.originalPosition); + copy.uv = new Vector2f(this.uv); + copy.influenceRadius = this.influenceRadius; + copy.selected = this.selected; + copy.name = this.name; + copy.weightMap = new HashMap<>(this.weightMap); + return copy; + } + } +} \ No newline at end of file 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 d1f67c6..e77e559 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 @@ -16,6 +16,7 @@ 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 org.joml.Vector4f; @@ -40,6 +41,14 @@ public class Mesh2D { private float[] originalVertices; // 原始顶点数据(用于变形恢复) private ModelPart modelPart; + // ==================== 二级顶点支持 ==================== + private List secondaryVertices = new ArrayList<>(); + private boolean showSecondaryVertices = false; + public Vector4f secondaryVertexColor = new Vector4f(0.0f, 1.0f, 0.0f, 1.0f); // 绿色二级顶点 + public Vector4f selectedSecondaryVertexColor = new Vector4f(1.0f, 0.0f, 0.0f, 1.0f); // 红色选中的二级顶点 + public float secondaryVertexSize = 6.0f; + private SecondaryVertex selectedSecondaryVertex = null; + // ==================== 渲染属性 ==================== private Texture texture; private boolean visible = true; @@ -67,9 +76,17 @@ public class Mesh2D { private boolean multiSelectionDirty = true; // ==================== 液化状态渲染 ==================== - private boolean showLiquifyOverlay = false; + public boolean showLiquifyOverlay = false; private Vector4f liquifyOverlayColor = new Vector4f(1.0f, 0.5f, 0.0f, 0.3f); // 半透明橙色 + // ==================== 木偶工具 ==================== + private List puppetPins = new ArrayList<>(); + private PuppetPin selectedPuppetPin = null; + private boolean showPuppetPins = true; + private Vector4f puppetPinColor = new Vector4f(1.0f, 0.0f, 0.0f, 1.0f); // 红色控制点 + private Vector4f selectedPuppetPinColor = new Vector4f(1.0f, 1.0f, 0.0f, 1.0f); // 黄色选中的控制点 + private float puppetPinSize = 8.0f; + // ==================== 常量 ==================== public static final int POINTS = 0; public static final int LINES = 1; @@ -97,6 +114,430 @@ public class Mesh2D { setMeshData(vertices, uvs, indices); } + public Vector4f getPuppetPinColor() { + return puppetPinColor; + } + + public PuppetPin getSelectedPuppetPin() { + return selectedPuppetPin; + } + + public Vector4f getSelectedPuppetPinColor() { + return selectedPuppetPinColor; + } + + public float getPuppetPinSize() { + return puppetPinSize; + } + + public Vector4f getLiquifyOverlayColor() { + return liquifyOverlayColor; + } + + /** + * 设置是否显示木偶控制点 + */ + public void setShowPuppetPins(boolean show) { + this.showPuppetPins = show; + markDirty(); + } + + /** + * 设置木偶控制点颜色 + */ + public void setPuppetPinColor(Vector4f color) { + this.puppetPinColor.set(color); + markDirty(); + } + + /** + * 设置选中的木偶控制点颜色 + */ + public void setSelectedPuppetPinColor(Vector4f color) { + this.selectedPuppetPinColor.set(color); + markDirty(); + } + + /** + * 设置木偶控制点大小 + */ + public void setPuppetPinSize(float size) { + this.puppetPinSize = size; + markDirty(); + } + + /** + * 获取是否渲染顶点模式 + */ + public boolean isRenderVertices() { + return isRenderVertices; + } + + /** + * 添加木偶控制点 + */ + public PuppetPin addPuppetPin(float x, float y, float u, float v) { + PuppetPin pin = new PuppetPin(x, y, u, v); + puppetPins.add(pin); + + // 预计算权重 + pin.precomputeWeights(this); + + markDirty(); + return pin; + } + + /** + * 移除木偶控制点 + */ + public boolean removePuppetPin(PuppetPin pin) { + boolean removed = puppetPins.remove(pin); + if (removed) { + if (selectedPuppetPin == pin) { + selectedPuppetPin = null; + } + markDirty(); + updateVerticesFromPuppetPins(); // 更新顶点位置 + } + return removed; + } + + /** + * 设置选中的木偶控制点 + */ + public void setSelectedPuppetPin(PuppetPin pin) { + if (selectedPuppetPin != null) { + selectedPuppetPin.setSelected(false); + } + + selectedPuppetPin = pin; + + if (selectedPuppetPin != null) { + selectedPuppetPin.setSelected(true); + } + } + + /** + * 通过位置选择木偶控制点 + */ + public PuppetPin selectPuppetPinAt(float x, float y, float tolerance) { + for (int i = puppetPins.size() - 1; i >= 0; i--) { + PuppetPin pin = puppetPins.get(i); + Vector2f pos = pin.getPosition(); + + if (Math.abs(pos.x - x) <= tolerance && Math.abs(pos.y - y) <= tolerance) { + setSelectedPuppetPin(pin); + return pin; + } + } + setSelectedPuppetPin(null); + return null; + } + + /** + * 移动选中的木偶控制点 - 修复版 + */ + public boolean moveSelectedPuppetPin(float dx, float dy) { + if (selectedPuppetPin != null) { + // 如果有活跃的二级顶点变形,提示用户或自动解决冲突 + if (hasActiveSecondaryVertexDeformation()) { + logger.warn("检测到变形冲突:木偶控制点移动时存在活跃的二级顶点变形"); + // 可以选择自动解决冲突或提示用户 + resolveDeformationConflict(true); // 优先使用木偶变形 + } + + selectedPuppetPin.move(dx, dy); + updateVerticesFromPuppetPins(); + return true; + } + return false; + } + + /** + * 保存当前的木偶变形状态 + */ + public void savePuppetDeformationState() { + if (originalVertices == null || originalVertices.length != vertices.length) { + originalVertices = vertices.clone(); + } else { + // 更新原始顶点为当前状态,这样后续的变形会基于当前状态 + System.arraycopy(vertices, 0, originalVertices, 0, vertices.length); + } + + // 同时保存控制点的原始位置 + for (PuppetPin pin : puppetPins) { + pin.saveAsOriginal(); + } + } + + /** + * 检查是否有活跃的木偶变形 + */ + public boolean hasActivePuppetDeformation() { + for (PuppetPin pin : puppetPins) { + Vector2f currentPos = pin.getPosition(); + Vector2f originalPos = pin.getOriginalPosition(); + if (currentPos.distanceSquared(originalPos) > 0.001f) { + return true; + } + } + return false; + } + + /** + * 检查是否有活跃的二级顶点变形 + */ + public boolean hasActiveSecondaryVertexDeformation() { + for (SecondaryVertex vertex : secondaryVertices) { + Vector2f currentPos = vertex.getPosition(); + Vector2f originalPos = vertex.getOriginalPosition(); + if (currentPos.distanceSquared(originalPos) > 0.001f) { + return true; + } + } + return false; + } + + /** + * 基于木偶控制点更新顶点位置(核心算法) + */ + public void updateVerticesFromPuppetPins() { + if (puppetPins.isEmpty() || originalVertices == null) { + return; + } + + // 保存原始顶点位置(如果还没有保存) + if (originalVertices.length != vertices.length) { + originalVertices = vertices.clone(); + } + + // 重置顶点到原始位置 + System.arraycopy(originalVertices, 0, vertices, 0, vertices.length); + + // 应用所有控制点的变形 + boolean hasDeformation = false; + for (PuppetPin pin : puppetPins) { + if (applyPuppetPinDeformation(pin)) { + hasDeformation = true; + } + } + + if (hasDeformation) { + markDirtyForPuppet(); // 使用专门的方法标记脏状态 + } + } + + /** + * 应用单个控制点的变形 - 返回是否实际应用了变形 + */ + private boolean applyPuppetPinDeformation(PuppetPin pin) { + Vector2f pinDelta = new Vector2f(pin.getPosition()).sub(pin.getOriginalPosition()); + + // 如果控制点没有移动,跳过 + if (pinDelta.lengthSquared() < 0.001f) { + return false; + } + + Map weightMap = pin.getWeightMap(); + + // 应用变形到每个受影响的顶点 + for (Map.Entry entry : weightMap.entrySet()) { + int vertexIndex = entry.getKey(); + float weight = entry.getValue(); + + // 计算该顶点应该移动的距离 + Vector2f vertexDelta = new Vector2f(pinDelta).mul(weight); + + // 应用变形 + int baseIndex = vertexIndex * 2; + vertices[baseIndex] += vertexDelta.x; + vertices[baseIndex + 1] += vertexDelta.y; + } + + return true; + } + + private void markDirtyForPuppet() { + // 不调用 updateVerticesFromSecondaryVertices(),避免覆盖木偶变形 + deleteGPU(); + this.dirty = true; + this.boundsDirty = true; + this.multiSelectionDirty = true; + } + + /** + * 预计算所有控制点的权重 + */ + public void precomputeAllPuppetWeights() { + for (PuppetPin pin : puppetPins) { + pin.precomputeWeights(this); + } + } + + public boolean getShowPuppetPins() { + return showPuppetPins; + } + /** + * 绘制木偶控制点 + */ + private void drawPuppetPins(Matrix3f modelMatrix) { + if (!showPuppetPins || puppetPins.isEmpty()) 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); + } + } + + RenderSystem.enableBlend(); + RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + + Tesselator t = Tesselator.getInstance(); + BufferBuilder bb = t.getBuilder(); + + // 绘制控制点影响范围 + drawPuppetPinInfluenceRanges(bb); + + // 绘制控制点 + for (PuppetPin pin : puppetPins) { + Vector2f position = pin.getPosition(); + Vector4f color = pin.isSelected() ? selectedPuppetPinColor : puppetPinColor; + + drawPuppetPin(bb, position.x, position.y, color, puppetPinSize, pin.isSelected()); + + // 为选中的控制点绘制信息 + if (pin.isSelected()) { + drawPuppetPinInfo(bb, pin, position.x, position.y); + } + } + + } finally { + RenderSystem.popState(); + } + } + + /** + * 绘制控制点影响范围 + */ + private void drawPuppetPinInfluenceRanges(BufferBuilder bb) { + for (PuppetPin pin : puppetPins) { + Vector2f position = pin.getPosition(); + float radius = pin.getInfluenceRadius(); + Vector4f rangeColor = new Vector4f(0.3f, 0.3f, 1.0f, 0.2f); // 半透明蓝色 + + // 绘制影响范围圆圈 + bb.begin(GL11.GL_TRIANGLE_FAN, 32); + bb.setColor(rangeColor); + bb.vertex(position.x, position.y, 0f, 0f); // 中心点 + + for (int i = 0; i <= 32; i++) { + float angle = (float) (i * 2 * Math.PI / 32); + float x = position.x + (float) Math.cos(angle) * radius; + float y = position.y + (float) Math.sin(angle) * radius; + bb.vertex(x, y, 0f, 0f); + } + bb.endImmediate(); + + // 绘制范围边框 + bb.begin(GL11.GL_LINE_LOOP, 32); + bb.setColor(new Vector4f(0.1f, 0.1f, 0.8f, 0.6f)); + + for (int i = 0; i < 32; i++) { + float angle = (float) (i * 2 * Math.PI / 32); + float x = position.x + (float) Math.cos(angle) * radius; + float y = position.y + (float) Math.sin(angle) * radius; + bb.vertex(x, y, 0f, 0f); + } + bb.endImmediate(); + } + } + + /** + * 绘制木偶控制点(更醒目的样式) + */ + private void drawPuppetPin(BufferBuilder bb, float x, float y, Vector4f color, float size, boolean selected) { + float halfSize = size / 2; + + if (selected) { + // 选中的控制点:带圆圈的十字 + bb.begin(GL11.GL_LINES, 4); + bb.setColor(color); + bb.vertex(x - halfSize, y, 0f, 0f); + bb.vertex(x + halfSize, y, 0f, 0f); + bb.vertex(x, y - halfSize, 0f, 0f); + bb.vertex(x, y + halfSize, 0f, 0f); + bb.endImmediate(); + + // 外圈圆圈 + bb.begin(GL11.GL_LINE_LOOP, 16); + bb.setColor(color); + float circleSize = size * 1.5f; + for (int i = 0; i < 16; i++) { + float angle = (float) (i * 2 * Math.PI / 16); + float cx = x + (float) Math.cos(angle) * circleSize; + float cy = y + (float) Math.sin(angle) * circleSize; + bb.vertex(cx, cy, 0f, 0f); + } + bb.endImmediate(); + } else { + // 普通控制点:实心圆圈 + bb.begin(GL11.GL_TRIANGLE_FAN, 16); + bb.setColor(color); + bb.vertex(x, y, 0f, 0f); // 中心点 + for (int i = 0; i <= 16; i++) { + float angle = (float) (i * 2 * Math.PI / 16); + float cx = x + (float) Math.cos(angle) * halfSize; + float cy = y + (float) Math.sin(angle) * halfSize; + bb.vertex(cx, cy, 0f, 0f); + } + bb.endImmediate(); + + // 边框 + bb.begin(GL11.GL_LINE_LOOP, 16); + bb.setColor(new Vector4f(1.0f, 1.0f, 1.0f, 1.0f)); + for (int i = 0; i < 16; i++) { + float angle = (float) (i * 2 * Math.PI / 16); + float cx = x + (float) Math.cos(angle) * halfSize; + float cy = y + (float) Math.sin(angle) * halfSize; + bb.vertex(cx, cy, 0f, 0f); + } + bb.endImmediate(); + } + } + + /** + * 绘制控制点信息 + */ + private void drawPuppetPinInfo(BufferBuilder bb, PuppetPin pin, float x, float y) { + String infoText = pin.getName() + " (R:" + (int)pin.getInfluenceRadius() + ")"; + TextRenderer textRenderer = ModelRender.getTextRenderer(); + if (textRenderer != null) { + float textWidth = textRenderer.getTextWidth(infoText); + float textX = x + puppetPinSize + 5.0f; + float textY = y - 6.0f; + + // 绘制文字背景 + bb.begin(GL11.GL_TRIANGLES, 6); + bb.setColor(new Vector4f(0.1f, 0.1f, 0.1f, 0.8f)); + bb.vertex(textX - 3, textY - 10, 0f, 0f); + bb.vertex(textX + textWidth + 3, textY - 10, 0f, 0f); + bb.vertex(textX + textWidth + 3, textY + 4, 0f, 0f); + bb.vertex(textX + textWidth + 3, textY + 4, 0f, 0f); + bb.vertex(textX - 3, textY + 4, 0f, 0f); + bb.vertex(textX - 3, textY - 10, 0f, 0f); + bb.endImmediate(); + + // 绘制文字 + ModelRender.renderText(infoText, textX, textY, new Vector4f(1.0f, 1.0f, 1.0f, 1.0f)); + } + } // ==================== 网格数据设置 ==================== @@ -716,6 +1157,753 @@ public class Mesh2D { return new Mesh2D(name, vertices, uvs, indices); } + // ==================== 二级顶点操作方法 ==================== + + /** + * 添加二级顶点 + */ + public SecondaryVertex addSecondaryVertex(float x, float y, float u, float v) { + SecondaryVertex vertex = new SecondaryVertex(x, y, u, v); + secondaryVertices.add(vertex); + markDirty(); + return vertex; + } + + public SecondaryVertex addSecondaryVertex(Vector2f position, Vector2f uv) { + return addSecondaryVertex(position.x, position.y, uv.x, uv.y); + } + + /** + * 在指定位置插入二级顶点 + */ + public SecondaryVertex insertSecondaryVertex(int index, float x, float y, float u, float v) { + if (index < 0 || index > secondaryVertices.size()) { + throw new IndexOutOfBoundsException("Secondary vertex index out of bounds: " + index); + } + SecondaryVertex vertex = new SecondaryVertex(x, y, u, v); + secondaryVertices.add(index, vertex); + markDirty(); + return vertex; + } + + /** + * 移除二级顶点 + */ + public boolean removeSecondaryVertex(SecondaryVertex vertex) { + boolean removed = secondaryVertices.remove(vertex); + if (removed) { + if (selectedSecondaryVertex == vertex) { + selectedSecondaryVertex = null; + } + markDirty(); + } + return removed; + } + + /** + * 通过ID移除二级顶点 + */ + public boolean removeSecondaryVertex(int id) { + return secondaryVertices.removeIf(vertex -> { + if (vertex.id == id) { + if (selectedSecondaryVertex != null && selectedSecondaryVertex.id == id) { + selectedSecondaryVertex = null; + } + return true; + } + return false; + }); + } + + /** + * 通过索引移除二级顶点 + */ + public SecondaryVertex removeSecondaryVertexAt(int index) { + if (index < 0 || index >= secondaryVertices.size()) { + throw new IndexOutOfBoundsException("Secondary vertex index out of bounds: " + index); + } + SecondaryVertex removed = secondaryVertices.remove(index); + if (selectedSecondaryVertex == removed) { + selectedSecondaryVertex = null; + } + markDirty(); + return removed; + } + + /** + * 清空所有二级顶点 + */ + public void clearSecondaryVertices() { + secondaryVertices.clear(); + selectedSecondaryVertex = null; + markDirty(); + } + + /** + * 获取二级顶点列表 + */ + public List getSecondaryVertices() { + return new ArrayList<>(secondaryVertices); + } + + /** + * 获取二级顶点数量 + */ + public int getSecondaryVertexCount() { + return secondaryVertices.size(); + } + + /** + * 通过ID查找二级顶点 + */ + public SecondaryVertex getSecondaryVertex(int id) { + return secondaryVertices.stream() + .filter(vertex -> vertex.id == id) + .findFirst() + .orElse(null); + } + + /** + * 通过索引获取二级顶点 + */ + public SecondaryVertex getSecondaryVertexAt(int index) { + if (index < 0 || index >= secondaryVertices.size()) { + throw new IndexOutOfBoundsException("Secondary vertex index out of bounds: " + index); + } + return secondaryVertices.get(index); + } + + /** + * 设置选中的二级顶点 + */ + public void setSelectedSecondaryVertex(SecondaryVertex vertex) { + // 取消之前选中的顶点 + if (selectedSecondaryVertex != null) { + selectedSecondaryVertex.setSelected(false); + } + + selectedSecondaryVertex = vertex; + + if (selectedSecondaryVertex != null) { + selectedSecondaryVertex.setSelected(true); + } + } + + /** + * 获取选中的二级顶点 + */ + public SecondaryVertex getSelectedSecondaryVertex() { + return selectedSecondaryVertex; + } + + /** + * 通过位置选择二级顶点 + */ + public SecondaryVertex selectSecondaryVertexAt(float x, float y, float tolerance) { + for (int i = secondaryVertices.size() - 1; i >= 0; i--) { + SecondaryVertex vertex = secondaryVertices.get(i); + Vector2f pos = vertex.getPosition(); + + if (Math.abs(pos.x - x) <= tolerance && Math.abs(pos.y - y) <= tolerance) { + setSelectedSecondaryVertex(vertex); + return vertex; + } + } + setSelectedSecondaryVertex(null); + return null; + } + + public SecondaryVertex selectSecondaryVertexAt(Vector2f position, float tolerance) { + return selectSecondaryVertexAt(position.x, position.y, tolerance); + } + + /** + * 移动选中的二级顶点 + */ + public boolean moveSelectedSecondaryVertex(float dx, float dy) { + if (selectedSecondaryVertex != null) { + selectedSecondaryVertex.move(dx, dy); + markDirty(); + return true; + } + return false; + } + + /** + * 设置是否显示二级顶点 + */ + public void setShowSecondaryVertices(boolean show) { + this.showSecondaryVertices = show; + markDirty(); + } + + /** + * 移动所有二级顶点(与网格同步移动) + */ + public void moveSecondaryVertices(float dx, float dy) { + for (SecondaryVertex vertex : secondaryVertices) { + vertex.move(dx, dy); + } + markDirty(); + } + + /** + * 设置所有二级顶点的位置(绝对位置) + */ + public void setSecondaryVerticesPosition(float x, float y) { + for (SecondaryVertex vertex : secondaryVertices) { + vertex.setPosition(x, y); + } + markDirty(); + } + + /** + * 同步二级顶点到网格边界(当网格移动或变形时调用) + */ + public void syncSecondaryVerticesToBounds() { + if (secondaryVertices.isEmpty()) return; + + BoundingBox bounds = getBounds(); + if (bounds == null || !bounds.isValid()) return; + + // 如果有4个二级顶点(默认的四个角点),将它们同步到边界框的四个角 + if (secondaryVertices.size() == 4) { + float minX = bounds.getMinX(); + float minY = bounds.getMinY(); + float maxX = bounds.getMaxX(); + float maxY = bounds.getMaxY(); + + // 更新四个角点的位置 + secondaryVertices.get(0).setPosition(minX, minY); // 左下 + secondaryVertices.get(1).setPosition(maxX, minY); // 右下 + secondaryVertices.get(2).setPosition(maxX, maxY); // 右上 + secondaryVertices.get(3).setPosition(minX, maxY); // 左上 + } else { + // 对于其他数量的二级顶点,使用相对位置保持 + syncSecondaryVerticesByRelativePosition(); + } + markDirty(); + } + + /** + * 根据相对位置同步二级顶点 + */ + private void syncSecondaryVerticesByRelativePosition() { + BoundingBox currentBounds = getBounds(); + BoundingBox originalBounds = calculateOriginalBounds(); + + if (currentBounds == null || originalBounds == null || + !currentBounds.isValid() || !originalBounds.isValid()) { + return; + } + + float currentWidth = currentBounds.getWidth(); + float currentHeight = currentBounds.getHeight(); + float originalWidth = originalBounds.getWidth(); + float originalHeight = originalBounds.getHeight(); + + if (originalWidth <= 0 || originalHeight <= 0) return; + + // 计算缩放比例 + float scaleX = currentWidth / originalWidth; + float scaleY = currentHeight / originalHeight; + + // 计算平移量 + float translateX = currentBounds.getMinX() - originalBounds.getMinX(); + float translateY = currentBounds.getMinY() - originalBounds.getMinY(); + + // 应用变换到所有二级顶点 + for (SecondaryVertex vertex : secondaryVertices) { + Vector2f originalPos = vertex.getOriginalPosition(); + float newX = originalPos.x * scaleX + translateX; + float newY = originalPos.y * scaleY + translateY; + vertex.setPosition(newX, newY); + } + } + + /** + * 保存当前二级顶点位置为原始位置(在网格移动后调用) + */ + public void saveCurrentPositionsAsOriginal() { + for (SecondaryVertex vertex : secondaryVertices) { + vertex.saveAsOriginal(); + } + } + + /** + * 根据二级顶点位置更新网格顶点(实现变形效果) + */ + public void updateVerticesFromSecondaryVertices() { + if (secondaryVertices.isEmpty() || vertices == null || originalVertices == null) { + return; + } + + // 确保顶点数组长度匹配 + if (originalVertices.length != vertices.length) { + logger.warn("原始顶点和当前顶点数组长度不匹配: {} != {}", + originalVertices.length, vertices.length); + return; + } + + // 使用重心坐标插值或双线性插值 + if (secondaryVertices.size() == 4) { + updateVerticesUsingBilinearInterpolationStable(); + } else { + updateVerticesUsingInverseDistanceWeighting(); + } + } + + /** + * 使用稳定的双线性插值更新顶点 + */ + private void updateVerticesUsingBilinearInterpolationStable() { + try { + // 获取四个角点的二级顶点 + if (secondaryVertices.size() < 4) return; + + SecondaryVertex bottomLeft = secondaryVertices.get(0); // 左下 + SecondaryVertex bottomRight = secondaryVertices.get(1); // 右下 + SecondaryVertex topRight = secondaryVertices.get(2); // 右上 + SecondaryVertex topLeft = secondaryVertices.get(3); // 左上 + + // 计算原始边界框 + BoundingBox originalBounds = calculateOriginalBounds(); + if (originalBounds == null || !originalBounds.isValid()) return; + + float minX = originalBounds.getMinX(); + float minY = originalBounds.getMinY(); + float maxX = originalBounds.getMaxX(); + float maxY = originalBounds.getMaxY(); + float width = maxX - minX; + float height = maxY - minY; + + if (width <= 0 || height <= 0) { + logger.warn("无效的边界框尺寸: {} x {}", width, height); + return; + } + + // 对每个顶点进行双线性插值 + for (int i = 0; i < originalVertices.length; i += 2) { + float origX = originalVertices[i]; + float origY = originalVertices[i + 1]; + + // 计算UV坐标(在原始边界框中的相对位置) + float u = (origX - minX) / width; + float v = (origY - minY) / height; + + // 限制UV在[0,1]范围内 + u = Math.max(0.0f, Math.min(1.0f, u)); + v = Math.max(0.0f, Math.min(1.0f, v)); + + // 双线性插值计算新位置 + Vector2f newPos = bilinearInterpolationStable( + bottomLeft.getPosition(), bottomRight.getPosition(), + topLeft.getPosition(), topRight.getPosition(), + u, v + ); + + // 更新顶点位置 + vertices[i] = newPos.x; + vertices[i + 1] = newPos.y; + } + + logger.debug("应用双线性插值变形,更新了 {} 个顶点", originalVertices.length / 2); + + } catch (Exception e) { + logger.error("双线性插值变形失败", e); + } + } + + /** + * 稳定的双线性插值计算 + */ + private Vector2f bilinearInterpolationStable(Vector2f p00, Vector2f p10, + Vector2f p01, Vector2f p11, + float u, float v) { + // 水平插值(底部和顶部) + Vector2f bottom = new Vector2f(); + bottom.x = p00.x + u * (p10.x - p00.x); + bottom.y = p00.y + u * (p10.y - p00.y); + + Vector2f top = new Vector2f(); + top.x = p01.x + u * (p11.x - p01.x); + top.y = p01.y + u * (p11.y - p01.y); + + // 垂直插值 + Vector2f result = new Vector2f(); + result.x = bottom.x + v * (top.x - bottom.x); + result.y = bottom.y + v * (top.y - bottom.y); + + return result; + } + + /** + * 使用反距离加权插值(适用于任意数量的控制点) + */ + private void updateVerticesUsingInverseDistanceWeighting() { + try { + float power = 2.0f; // 距离的幂次,通常为2 + float epsilon = 0.0001f; // 避免除零的小值 + + for (int i = 0; i < originalVertices.length; i += 2) { + float origX = originalVertices[i]; + float origY = originalVertices[i + 1]; + + float weightSum = 0.0f; + float newX = 0.0f; + float newY = 0.0f; + + for (SecondaryVertex secVertex : secondaryVertices) { + Vector2f secOrigPos = secVertex.getOriginalPosition(); + Vector2f secCurrPos = secVertex.getPosition(); + + // 计算原始位置到控制点原始位置的距离 + float dx = origX - secOrigPos.x; + float dy = origY - secOrigPos.y; + float distance = (float) Math.sqrt(dx * dx + dy * dy); + + // 避免除零 + if (distance < epsilon) { + // 如果顶点正好在控制点上,直接使用控制点的新位置 + newX = secCurrPos.x; + newY = secCurrPos.y; + weightSum = 1.0f; + break; + } + + // 计算权重:1 / distance^power + float weight = 1.0f / (float) Math.pow(distance, power); + + // 计算该控制点对顶点位置的贡献 + float deltaX = secCurrPos.x - secOrigPos.x; + float deltaY = secCurrPos.y - secOrigPos.y; + + newX += weight * (origX + deltaX); + newY += weight * (origY + deltaY); + weightSum += weight; + } + + if (weightSum > 0) { + vertices[i] = newX / weightSum; + vertices[i + 1] = newY / weightSum; + } else { + // 保持原始位置 + vertices[i] = origX; + vertices[i + 1] = origY; + } + } + + logger.debug("应用反距离加权变形,使用 {} 个控制点", secondaryVertices.size()); + + } catch (Exception e) { + logger.error("反距离加权变形失败", e); + } + } + + /** + * 计算原始顶点的边界框 + */ + private BoundingBox calculateOriginalBounds() { + if (originalVertices == null || originalVertices.length < 2) { + return null; + } + + BoundingBox bounds = new BoundingBox(); + for (int i = 0; i < originalVertices.length; i += 2) { + bounds.expand(originalVertices[i], originalVertices[i + 1]); + } + return bounds; + } + + /** + * 使用双线性插值更新顶点(适用于4个角点的情况) + */ + private void updateVerticesUsingBilinearInterpolation() { + // 获取四个角点的二级顶点 + SecondaryVertex bottomLeft = secondaryVertices.get(0); // 左下 + SecondaryVertex bottomRight = secondaryVertices.get(1); // 右下 + SecondaryVertex topRight = secondaryVertices.get(2); // 右上 + SecondaryVertex topLeft = secondaryVertices.get(3); // 左上 + + // 获取原始边界框(用于UV计算) + BoundingBox originalBounds = new BoundingBox(); + for (int i = 0; i < originalVertices.length; i += 2) { + originalBounds.expand(originalVertices[i], originalVertices[i + 1]); + } + + float minX = originalBounds.getMinX(); + float minY = originalBounds.getMinY(); + float maxX = originalBounds.getMaxX(); + float maxY = originalBounds.getMaxY(); + float width = maxX - minX; + float height = maxY - minY; + + if (width <= 0 || height <= 0) return; + + // 对每个顶点进行双线性插值 + for (int i = 0; i < originalVertices.length; i += 2) { + float origX = originalVertices[i]; + float origY = originalVertices[i + 1]; + + // 计算UV坐标(在原始边界框中的相对位置) + float u = (origX - minX) / width; + float v = (origY - minY) / height; + + // 双线性插值计算新位置 + Vector2f newPos = bilinearInterpolation( + bottomLeft.getPosition(), bottomRight.getPosition(), + topRight.getPosition(), topLeft.getPosition(), + u, v + ); + + // 更新顶点位置 + vertices[i] = newPos.x; + vertices[i + 1] = newPos.y; + } + } + + /** + * 双线性插值计算 + */ + private Vector2f bilinearInterpolation(Vector2f p00, Vector2f p10, Vector2f p11, Vector2f p01, + float u, float v) { + // 水平插值 + Vector2f bottom = new Vector2f( + p00.x + u * (p10.x - p00.x), + p00.y + u * (p10.y - p00.y) + ); + + Vector2f top = new Vector2f( + p01.x + u * (p11.x - p01.x), + p01.y + u * (p11.y - p01.y) + ); + + // 垂直插值 + return new Vector2f( + bottom.x + v * (top.x - bottom.x), + bottom.y + v * (top.y - bottom.y) + ); + } + + /** + * 使用复杂插值方法(适用于任意数量的二级顶点) + */ + private void updateVerticesUsingComplexInterpolation() { + // 这里可以实现更复杂的插值算法,如重心坐标插值 + // 目前先使用简单的最近邻插值 + + for (int i = 0; i < originalVertices.length; i += 2) { + float origX = originalVertices[i]; + float origY = originalVertices[i + 1]; + + // 找到最近的二级顶点 + SecondaryVertex closest = findClosestSecondaryVertex(origX, origY); + if (closest != null) { + Vector2f closestPos = closest.getPosition(); + // 简单地将顶点移动到最近的二级顶点位置 + vertices[i] = closestPos.x; + vertices[i + 1] = closestPos.y; + } + } + } + + /** + * 找到距离指定位置最近的二级顶点 + */ + private SecondaryVertex findClosestSecondaryVertex(float x, float y) { + if (secondaryVertices.isEmpty()) return null; + + SecondaryVertex closest = secondaryVertices.get(0); + float minDistance = distance(x, y, closest.getPosition().x, closest.getPosition().y); + + for (int i = 1; i < secondaryVertices.size(); i++) { + SecondaryVertex vertex = secondaryVertices.get(i); + float dist = distance(x, y, vertex.getPosition().x, vertex.getPosition().y); + if (dist < minDistance) { + minDistance = dist; + closest = vertex; + } + } + + return closest; + } + + private float distance(float x1, float y1, float x2, float y2) { + float dx = x2 - x1; + float dy = y2 - y1; + return (float) Math.sqrt(dx * dx + dy * dy); + } + + /** + * 获取是否显示二级顶点 + */ + public boolean isShowSecondaryVertices() { + return showSecondaryVertices; + } + + /** + * 设置二级顶点颜色 + */ + public void setSecondaryVertexColor(Vector4f color) { + this.secondaryVertexColor.set(color); + markDirty(); + } + + /** + * 设置选中的二级顶点颜色 + */ + public void setSelectedSecondaryVertexColor(Vector4f color) { + this.selectedSecondaryVertexColor.set(color); + markDirty(); + } + + /** + * 设置二级顶点大小 + */ + public void setSecondaryVertexSize(float size) { + this.secondaryVertexSize = size; + markDirty(); + } + + // ==================== 二级顶点变形支持 ==================== + + /** + * 对二级顶点应用变形 + */ + public void transformSecondaryVertices(VertexTransformer transformer) { + for (SecondaryVertex vertex : secondaryVertices) { + Vector2f position = vertex.getPosition(); + transformer.transform(position, -1); // 使用负索引标识二级顶点 + vertex.setPosition(position); + } + markDirty(); + } + + /** + * 重置所有二级顶点到原始位置 + */ + public void resetSecondaryVerticesToOriginal() { + for (SecondaryVertex vertex : secondaryVertices) { + vertex.resetToOriginal(); + } + markDirty(); + } + + /** + * 保存当前二级顶点位置为原始位置 + */ + public void saveSecondaryVerticesAsOriginal() { + for (SecondaryVertex vertex : secondaryVertices) { + vertex.saveAsOriginal(); + } + } + + // ==================== 二级顶点渲染 ==================== + + /** + * 绘制二级顶点 + */ + private void drawSecondaryVertices(Matrix3f modelMatrix) { + if (!showSecondaryVertices || secondaryVertices.isEmpty()) 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); + } + } + + RenderSystem.enableBlend(); + RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + + Tesselator t = Tesselator.getInstance(); + BufferBuilder bb = t.getBuilder(); + + // 绘制所有二级顶点 + for (SecondaryVertex vertex : secondaryVertices) { + Vector2f position = vertex.getPosition(); + Vector4f color = vertex.isSelected() ? selectedSecondaryVertexColor : secondaryVertexColor; + + drawVertexPoint(bb, position.x, position.y, color, secondaryVertexSize); + + // 为选中的顶点绘制编号 + if (vertex.isSelected()) { + drawVertexId(bb, vertex.getId(), position.x, position.y); + } + } + + } finally { + RenderSystem.popState(); + } + } + + /** + * 绘制顶点标记点 + */ + private void drawVertexPoint(BufferBuilder bb, float x, float y, Vector4f color, float size) { + float halfSize = size / 2; + + // 绘制方形顶点标记 + bb.begin(GL11.GL_TRIANGLES, 6); + bb.setColor(color); + + 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(); + + // 绘制边框 + bb.begin(GL11.GL_LINE_LOOP, 4); + bb.setColor(new Vector4f(1.0f, 1.0f, 1.0f, 1.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(); + } + + /** + * 绘制顶点ID编号 + */ + private void drawVertexId(BufferBuilder bb, int id, float x, float y) { + String idText = String.valueOf(id); + TextRenderer textRenderer = ModelRender.getTextRenderer(); + if (textRenderer != null) { + float textWidth = textRenderer.getTextWidth(idText); + float textX = x + secondaryVertexSize + 2.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(idText, textX, textY, new Vector4f(1.0f, 1.0f, 1.0f, 1.0f)); + } + } // ==================== 顶点操作 ==================== /** @@ -957,6 +2145,13 @@ public class Mesh2D { markDirty(); } + /** + * 获取木偶控制点数量 + */ + public int getPuppetPinCount() { + return puppetPins.size(); + } + /** * 顶点变换器接口 */ @@ -1194,10 +2389,27 @@ public class Mesh2D { * 标记数据已修改 */ public void markDirty() { + if (puppetPins.isEmpty() || !hasMovedPuppetPins()) { + updateVerticesFromSecondaryVertices(); + } deleteGPU(); this.dirty = true; this.boundsDirty = true; - this.multiSelectionDirty = true; // 新增:标记多选边界框需要更新 + this.multiSelectionDirty = true; + } + + /** + * 检查是否有移动过的木偶控制点 + */ + private boolean hasMovedPuppetPins() { + for (PuppetPin pin : puppetPins) { + Vector2f currentPos = pin.getPosition(); + Vector2f originalPos = pin.getOriginalPosition(); + if (currentPos.distanceSquared(originalPos) > 0.001f) { + return true; + } + } + return false; } /** @@ -1221,6 +2433,24 @@ public class Mesh2D { return dirty; } + /** + * 获取当前活跃的变形类型 + */ + public String getActiveDeformationType() { + boolean hasPuppet = hasActivePuppetDeformation(); + boolean hasSecondary = hasActiveSecondaryVertexDeformation(); + + if (hasPuppet && hasSecondary) { + return "CONFLICT"; + } else if (hasPuppet) { + return "PUPPET"; + } else if (hasSecondary) { + return "SECONDARY"; + } else { + return "NONE"; + } + } + /** * 将网格数据上传到 GPU(生成 VAO/VBO/EBO) */ @@ -1279,10 +2509,57 @@ public class Mesh2D { } } + /** + * 解决变形冲突 - 优先使用木偶变形 + */ + public void resolveDeformationConflict(boolean preferPuppet) { + if (preferPuppet) { + // 保存当前二级顶点位置,然后重置它们 + for (SecondaryVertex vertex : secondaryVertices) { + vertex.saveAsOriginal(); + } + } else { + // 保存当前木偶控制点位置,然后重置它们 + for (PuppetPin pin : puppetPins) { + pin.saveAsOriginal(); + } + updateVerticesFromPuppetPins(); + } + markDirty(); + } + + /** + * 重置所有变形到原始状态 + */ + public void resetAllDeformations() { + // 重置木偶控制点 + for (PuppetPin pin : puppetPins) { + pin.resetToOriginal(); + } + + // 重置二级顶点 + for (SecondaryVertex vertex : secondaryVertices) { + vertex.resetToOriginal(); + } + + // 重置顶点到原始状态 + if (originalVertices != null && originalVertices.length == vertices.length) { + System.arraycopy(originalVertices, 0, vertices, 0, vertices.length); + } + + markDirty(); + } + /** * 绘制网格(会在第一次绘制时自动上传到 GPU) */ public void draw(int shaderProgram, Matrix3f modelMatrix) { + String deformationType = getActiveDeformationType(); + + if ("PUPPET".equals(deformationType) || "CONFLICT".equals(deformationType)) { + updateVerticesFromPuppetPins(); + } + if (!visible) return; if (indices == null || indices.length == 0) return; @@ -1375,6 +2652,14 @@ public class Mesh2D { drawLiquifyOverlay(shaderProgram, modelMatrix); } + if (showSecondaryVertices && !secondaryVertices.isEmpty()) { + drawSecondaryVertices(modelMatrix); + } + + if (showPuppetPins && !puppetPins.isEmpty()) { + drawPuppetPins(modelMatrix); + } + if (isSuspension && !selected) { RenderSystem.pushState(); @@ -1994,6 +3279,56 @@ public class Mesh2D { } } + // ==================== 木偶工具API方法 ==================== + + + /** + * 获取木偶控制点列表 + */ + public List getPuppetPins() { + return new ArrayList<>(puppetPins); + } + + /** + * 通过ID获取木偶控制点 + */ + public PuppetPin getPuppetPin(int id) { + return puppetPins.stream() + .filter(pin -> pin.getId() == id) + .findFirst() + .orElse(null); + } + + /** + * 清空所有木偶控制点 + */ + public void clearPuppetPins() { + puppetPins.clear(); + selectedPuppetPin = null; + markDirty(); + } + + /** + * 重置所有木偶控制点到原始位置 + */ + public void resetPuppetPinsToOriginal() { + for (PuppetPin pin : puppetPins) { + pin.resetToOriginal(); + } + updateVerticesFromPuppetPins(); + } + + /** + * 保存当前木偶控制点位置为原始位置 + */ + public void savePuppetPinsAsOriginal() { + for (PuppetPin pin : puppetPins) { + pin.saveAsOriginal(); + } + // 同时保存当前顶点为原始顶点 + saveAsOriginal(); + } + // ==================== Getter/Setter ==================== public String getName() { @@ -2089,6 +3424,25 @@ public class Mesh2D { copy.bounds = new BoundingBox(); copy.selected = this.selected; + for (SecondaryVertex vertex : this.secondaryVertices) { + Vector2f pos = vertex.getPosition(); + Vector2f uv = vertex.getUV(); + Vector2f origPos = vertex.getOriginalPosition(); + + SecondaryVertex copiedVertex = copy.addSecondaryVertex(pos, uv); + copiedVertex.setPosition(origPos); // 设置原始位置 + copiedVertex.saveAsOriginal(); // 保存为原始位置 + + if (vertex.isSelected()) { + copy.setSelectedSecondaryVertex(copiedVertex); + } + } + + copy.showSecondaryVertices = this.showSecondaryVertices; + copy.secondaryVertexColor = new Vector4f(this.secondaryVertexColor); + copy.selectedSecondaryVertexColor = new Vector4f(this.selectedSecondaryVertexColor); + copy.secondaryVertexSize = this.secondaryVertexSize; + return copy; } @@ -2153,6 +3507,7 @@ public class Mesh2D { sb.append("Mesh2D{") .append("name='").append(name).append('\'') .append(", vertices=").append(getVertexCount()) + .append(", secondaryVertices=").append(secondaryVertices.size()) .append(", indices=").append(indices.length) .append(", pivot=(").append(String.format("%.2f", pivot.x)) .append(", ").append(String.format("%.2f", pivot.y)).append(")") diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/PuppetPin.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/PuppetPin.java new file mode 100644 index 0000000..a37ed52 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/PuppetPin.java @@ -0,0 +1,128 @@ +package com.chuangzhou.vivid2D.render.model.util; + +import org.joml.Vector2f; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author tzdwindows 7 + */ +public class PuppetPin { + private static int NEXT_ID = 0; + + private int id; + private Vector2f position; + private Vector2f originalPosition; + private Vector2f uv; + private float influenceRadius = 100.0f; + private boolean selected = false; + private String name; + + // 权重贴图(顶点索引 -> 权重值) + private Map weightMap = new HashMap<>(); + + public PuppetPin(float x, float y, float u, float v) { + this.id = NEXT_ID++; + this.position = new Vector2f(x, y); + this.originalPosition = new Vector2f(x, y); + this.uv = new Vector2f(u, v); + this.name = "Pin_" + id; + } + + // 计算顶点权重(基于距离的衰减) + public float calculateWeight(Vector2f vertexPos, int vertexIndex) { + float distance = position.distance(vertexPos); + + if (distance > influenceRadius) { + return 0.0f; + } + + // 使用平滑的衰减函数 + float normalizedDistance = distance / influenceRadius; + float weight = (float) (Math.cos(normalizedDistance * Math.PI) + 1) / 2.0f; + + return weight; + } + + // 预计算所有顶点权重 + public void precomputeWeights(Mesh2D mesh) { + weightMap.clear(); + for (int i = 0; i < mesh.getVertexCount(); i++) { + Vector2f vertexPos = mesh.getVertex(i); + float weight = calculateWeight(vertexPos, i); + if (weight > 0.01f) { // 只存储有影响的权重 + weightMap.put(i, weight); + } + } + } + + // ==================== 新增方法 ==================== + + /** + * 设置原始位置 + */ + public void setOriginalPosition(float x, float y) { + this.originalPosition.set(x, y); + } + + /** + * 设置原始位置 + */ + public void setOriginalPosition(Vector2f pos) { + this.originalPosition.set(pos); + } + + /** + * 获取UV坐标 + */ + public Vector2f getUV() { + return new Vector2f(uv); + } + + /** + * 设置UV坐标 + */ + public void setUV(float u, float v) { + this.uv.set(u, v); + } + + /** + * 设置UV坐标 + */ + public void setUV(Vector2f uv) { + this.uv.set(uv); + } + + // ==================== 原有方法 ==================== + + // Getters and Setters + public Vector2f getPosition() { return new Vector2f(position); } + public void setPosition(float x, float y) { this.position.set(x, y); } + public void setPosition(Vector2f pos) { this.position.set(pos); } + public Vector2f getOriginalPosition() { return new Vector2f(originalPosition); } + public float getInfluenceRadius() { return influenceRadius; } + public void setInfluenceRadius(float radius) { this.influenceRadius = radius; } + public boolean isSelected() { return selected; } + public void setSelected(boolean selected) { this.selected = selected; } + public int getId() { return id; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public Map getWeightMap() { return new HashMap<>(weightMap); } + + public void move(float dx, float dy) { + position.add(dx, dy); + } + + public void saveAsOriginal() { + originalPosition.set(position); + } + + public void resetToOriginal() { + position.set(originalPosition); + } + + public void setId(int id) { + this.id = id; + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..51b8cec --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/SecondaryVertex.java @@ -0,0 +1,90 @@ +package com.chuangzhou.vivid2D.render.model.util; + +import org.joml.Vector2f; + +import java.util.Objects; + +/** + * @author tzdwindows 7 + */ +public class SecondaryVertex { + Vector2f position; + Vector2f originalPosition; + Vector2f uv; + boolean selected = false; + int id; + static int nextId = 0; + + public SecondaryVertex(float x, float y, float u, float v) { + this.position = new Vector2f(x, y); + this.originalPosition = new Vector2f(x, y); + this.uv = new Vector2f(u, v); + this.id = nextId++; + } + + public SecondaryVertex(Vector2f position, Vector2f uv) { + this(position.x, position.y, uv.x, uv.y); + } + + // Getter和Setter方法 + public Vector2f getPosition() { return new Vector2f(position); } + public Vector2f getOriginalPosition() { return new Vector2f(originalPosition); } + public Vector2f getUV() { return new Vector2f(uv); } + public boolean isSelected() { return selected; } + public int getId() { return id; } + + public void setPosition(float x, float y) { + this.position.set(x, y); + } + + public void setPosition(Vector2f position) { + this.position.set(position); + } + + public void setId(int id) { + this.id = id; + } + + public void setOriginalPosition(Vector2f originalPosition) { + this.originalPosition.set(originalPosition); + } + + public void setUV(float u, float v) { + this.uv.set(u, v); + } + + public void setSelected(boolean selected) { + this.selected = selected; + } + + public void resetToOriginal() { + this.position.set(originalPosition); + } + + public void saveAsOriginal() { + this.originalPosition.set(position); + } + + public void move(float dx, float dy) { + this.position.add(dx, dy); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SecondaryVertex that = (SecondaryVertex) o; + return id == that.id; + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return String.format("SecondaryVertex{id=%d, position=(%.2f, %.2f), uv=(%.2f, %.2f)}", + id, position.x, position.y, uv.x, uv.y); + } +}