diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/SecondaryVertexPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/SecondaryVertexPanel.java index 9a1190d..153a9f5 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/SecondaryVertexPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/SecondaryVertexPanel.java @@ -5,17 +5,10 @@ import org.joml.Vector2f; import javax.swing.*; import javax.swing.border.EmptyBorder; -import javax.swing.event.ChangeEvent; -import javax.swing.event.ChangeListener; import java.awt.*; import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; import java.text.DecimalFormat; -/** - * SecondaryVertex 属性编辑面板 - * 用于显示和设置当前选中的二级顶点的信息。 - */ public class SecondaryVertexPanel extends JPanel { private SecondaryVertex currentVertex; @@ -24,35 +17,25 @@ public class SecondaryVertexPanel extends JPanel { private final CardLayout cardLayout; private final JPanel vertexInfoPanel; - // 顶点信息组件 private JLabel idValue; private JLabel posValue; private JLabel uvValue; private JCheckBox pinnedCheckBox; private JCheckBox lockedCheckBox; - // 控制半径组件 - private JLabel radiusValue; - private JSlider radiusSlider; - private JCheckBox fixedRadiusCheckBox; - - // 格式化器 private static final DecimalFormat DF = new DecimalFormat("0.00"); public SecondaryVertexPanel() { - // 设置布局为 CardLayout,用于在状态消息和顶点详细信息之间切换 cardLayout = new CardLayout(); this.setLayout(new BorderLayout()); contentPanel = new JPanel(cardLayout); - // 1. "当前无顶点" 状态面板 JPanel emptyPanel = new JPanel(new GridBagLayout()); statusLabel = new JLabel("当前无顶点", SwingConstants.CENTER); statusLabel.setFont(new Font("SansSerif", Font.ITALIC, 14)); emptyPanel.add(statusLabel); - // 2. 顶点详细信息面板 vertexInfoPanel = createVertexInfoPanel(); contentPanel.add(emptyPanel, "EMPTY"); @@ -60,13 +43,9 @@ public class SecondaryVertexPanel extends JPanel { this.add(contentPanel, BorderLayout.CENTER); - // 默认显示空状态 cardLayout.show(contentPanel, "EMPTY"); } - /** - * 创建显示和编辑 SecondaryVertex 属性的面板 - */ private JPanel createVertexInfoPanel() { JPanel panel = new JPanel(new GridBagLayout()); panel.setBorder(new EmptyBorder(10, 10, 10, 10)); @@ -77,7 +56,6 @@ public class SecondaryVertexPanel extends JPanel { int row = 0; - // --- 基本信息 (非编辑) --- gbc.gridx = 0; gbc.weightx = 0; panel.add(new JLabel("ID:"), gbc); @@ -106,7 +84,6 @@ public class SecondaryVertexPanel extends JPanel { uvValue = new JLabel("N/A"); panel.add(uvValue, gbc); - // --- 状态控制 (编辑) --- row++; gbc.gridy = row; gbc.gridx = 0; @@ -128,56 +105,16 @@ public class SecondaryVertexPanel extends JPanel { lockedCheckBox.addActionListener(this::handleCheckboxChange); panel.add(lockedCheckBox, gbc); - // --- 控制半径 (编辑) --- row++; gbc.gridy = row; gbc.gridx = 0; gbc.gridwidth = 2; - panel.add(new JSeparator(), gbc); - - row++; - gbc.gridy = row; - gbc.gridx = 0; - gbc.gridwidth = 1; - panel.add(new JLabel("Control Radius:"), gbc); - - gbc.gridx = 1; - radiusValue = new JLabel("N/A"); - panel.add(radiusValue, gbc); - - row++; - gbc.gridy = row; - gbc.gridx = 0; - gbc.gridwidth = 2; - radiusSlider = new JSlider(JSlider.HORIZONTAL, 0, 1000, 100); // 暂定 0-1000 映射到实际范围 - radiusSlider.setMajorTickSpacing(250); - radiusSlider.setPaintTicks(true); - radiusSlider.setPaintLabels(true); - radiusSlider.addChangeListener(this::handleRadiusSliderChange); - panel.add(radiusSlider, gbc); - - row++; - gbc.gridy = row; - gbc.gridx = 0; - gbc.gridwidth = 2; - fixedRadiusCheckBox = new JCheckBox("Fixed Radius (固定半径)"); - fixedRadiusCheckBox.setToolTipText("锁定半径值,不允许修改"); - fixedRadiusCheckBox.addActionListener(this::handleCheckboxChange); - panel.add(fixedRadiusCheckBox, gbc); - - // 填充剩余空间 - row++; - gbc.gridy = row; - gbc.weighty = 1.0; // 将垂直空间推到这里 + gbc.weighty = 1.0; panel.add(new JPanel(), gbc); return panel; } - /** - * 公开 API:设置要显示的二级顶点。 - * @param vertex 要显示的 SecondaryVertex 对象,如果为 null 则显示默认状态。 - */ public void setSecondaryVertex(SecondaryVertex vertex) { this.currentVertex = vertex; @@ -189,40 +126,17 @@ public class SecondaryVertexPanel extends JPanel { } } - /** - * 根据 SecondaryVertex 对象更新面板内容。 - */ private void updatePanelContent(SecondaryVertex vertex) { - // 1. 基本信息 idValue.setText(String.valueOf(vertex.getId())); Vector2f pos = vertex.getPosition(); posValue.setText(String.format("(%s, %s)", DF.format(pos.x), DF.format(pos.y))); Vector2f uv = vertex.getUV(); uvValue.setText(String.format("(%s, %s)", DF.format(uv.x), DF.format(uv.y))); - // 2. 状态控制 pinnedCheckBox.setSelected(vertex.isPinned()); lockedCheckBox.setSelected(vertex.isLocked()); - - // 3. 半径控制 - // 根据实际 min/maxControlRadius 调整 Slider 的值域,这里使用固定 0-200 范围,以简化演示 - float minR = vertex.getMinControlRadius(); - float maxR = vertex.getMaxControlRadius(); - float currentR = vertex.getControlRadius(); - - radiusValue.setText(DF.format(currentR)); - - // 将实际半径映射到滑块 0-1000 的范围 - int sliderValue = (int) (((currentR - minR) / (maxR - minR)) * 1000.0f); - radiusSlider.setValue(sliderValue); - - fixedRadiusCheckBox.setSelected(vertex.isFixedRadius()); - radiusSlider.setEnabled(!vertex.isFixedRadius()); } - /** - * 处理 JCheckBox 状态变更 - */ private void handleCheckboxChange(ActionEvent e) { if (currentVertex == null) return; @@ -230,32 +144,8 @@ public class SecondaryVertexPanel extends JPanel { currentVertex.setPinned(pinnedCheckBox.isSelected()); } else if (e.getSource() == lockedCheckBox) { currentVertex.setLocked(lockedCheckBox.isSelected()); - } else if (e.getSource() == fixedRadiusCheckBox) { - currentVertex.setFixedRadius(fixedRadiusCheckBox.isSelected()); - radiusSlider.setEnabled(!fixedRadiusCheckBox.isSelected()); } - // 更改后自动刷新一次 (虽然状态已在 SecondaryVertex 中更改,但显示可能需要同步) updatePanelContent(currentVertex); } - - /** - * 处理 JSlider 半径值变更 - */ - private void handleRadiusSliderChange(ChangeEvent e) { - if (currentVertex == null || radiusSlider.getValueIsAdjusting()) return; // 仅在停止拖动时更新 - - // 将滑块 0-1000 的值重新映射到实际的 min/maxControlRadius 范围 - float minR = currentVertex.getMinControlRadius(); - float maxR = currentVertex.getMaxControlRadius(); - int sliderValue = radiusSlider.getValue(); - - float newRadius = minR + (sliderValue / 1000.0f) * (maxR - minR); - - // 通过 setter 更新 SecondaryVertex,setter 内部会处理范围限制 - currentVertex.setControlRadius(newRadius); - - // 更新显示的数值 - radiusValue.setText(DF.format(currentVertex.getControlRadius())); - } } \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/GLContextManager.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/GLContextManager.java index bb8830b..ce73ede 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/GLContextManager.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/GLContextManager.java @@ -229,7 +229,8 @@ public class GLContextManager { // 读取像素数据到 BufferedImage readPixelsToImage(); } catch (Exception e) { - System.err.println("渲染错误: " + e.getMessage()); + e.printStackTrace(); + logger.error("渲染错误", e); renderErrorFrame(e.getMessage()); } } else { diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ParametersManagement.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ParametersManagement.java index b3c6a49..b1db6f4 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ParametersManagement.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ParametersManagement.java @@ -11,10 +11,7 @@ import org.slf4j.LoggerFactory; import java.io.File; import java.io.FileInputStream; import java.io.ObjectInputStream; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; +import java.util.*; public class ParametersManagement { private static final Logger logger = LoggerFactory.getLogger(ParametersManagement.class); @@ -144,57 +141,56 @@ public class ParametersManagement { if (getSelectParameter() == null) { return; } - // 获取当前正在编辑的 AnimationParameter 实例 AnimationParameter currentAnimParam = getSelectParameter(); - - // 获取当前的关键帧时间点 (Float) 和是否为关键帧 (Boolean) Float currentKeyframe = getSelectedKeyframe(false); - - // 如果当前没有选中的关键帧,通常我们不应该记录,但为了安全,先检查 null if (currentKeyframe == null) { return; } - // 重新判断是否为关键帧,确保 isKeyframe 准确 boolean isKeyframe = currentAnimParam.getKeyframes().contains(currentKeyframe); - - // 查找是否已存在该ModelPart的记录 + Integer newId = null; + if (paramId.equals("secondaryVertex") && value instanceof Map) { + @SuppressWarnings("unchecked") + Map payload = (Map) value; + Object idObj = payload.get("id"); + if (idObj instanceof Integer) { + newId = (Integer) idObj; + } + } for (int i = 0; i < oldValues.size(); i++) { Parameter existingParameter = oldValues.get(i); - - // 步骤 1: 找到对应的 ModelPart if (existingParameter.modelPart().equals(modelPart)) { - // 步骤 2: 复制所有列表(保持不可变性) List newAnimationParameters = new ArrayList<>(existingParameter.animationParameter()); List newParamIds = new ArrayList<>(existingParameter.paramId()); List newValues = new ArrayList<>(existingParameter.value()); List newKeyframes = new ArrayList<>(existingParameter.keyframe()); List newIsKeyframes = new ArrayList<>(existingParameter.isKeyframe()); - - // 步骤 3: 查找相同 keyframe + paramId + AnimationParameter 的记录 int existingIndex = -1; for (int j = 0; j < newKeyframes.size(); j++) { - // 检查 keyframe 是否相同 (使用 Objects.equals 比较 Float) boolean keyframeMatches = Objects.equals(newKeyframes.get(j), currentKeyframe); - - // 检查 paramId 是否相同 boolean paramIdMatches = paramId.equals(newParamIds.get(j)); - - // 检查 AnimationParameter 是否相同 (使用 equals) AnimationParameter recordAnimParam = newAnimationParameters.get(j); boolean animParamMatches = recordAnimParam != null && recordAnimParam.equals(currentAnimParam); - - // 如果时间点、参数ID和参数实例都匹配,则找到了现有记录 - if (keyframeMatches && paramIdMatches && animParamMatches) { + boolean idMatches = true; + if (paramIdMatches && paramId.equals("secondaryVertex")) { + Object oldValue = newValues.get(j); + if (oldValue instanceof Map) { + @SuppressWarnings("unchecked") + Map oldPayload = (Map) oldValue; + Object oldIdObj = oldPayload.get("id"); + Integer oldId = (oldIdObj instanceof Integer) ? (Integer) oldIdObj : null; + idMatches = Objects.equals(newId, oldId); + } else { + idMatches = false; + } + } + if (keyframeMatches && paramIdMatches && animParamMatches && idMatches) { existingIndex = j; break; } } - if (existingIndex != -1) { - // 找到了相同的记录位置: 执行 UPDATE (设置) 操作 newValues.set(existingIndex, value); } else { - // 没有找到相同的记录: 执行 ADD (新增) 操作 newAnimationParameters.add(currentAnimParam); newParamIds.add(paramId); newValues.add(value); @@ -203,11 +199,9 @@ public class ParametersManagement { } Parameter updatedParameter = new Parameter(modelPart, newAnimationParameters, newParamIds, newValues, newKeyframes, newIsKeyframes); oldValues.set(i, updatedParameter); - return; // ModelPart 记录已处理 + return; } } - - // 如果没有找到 ModelPart 的现有记录,创建新记录 Parameter parameter = new Parameter( modelPart, Collections.singletonList(currentAnimParam), diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/Tool.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/Tool.java index bc5055b..4bb19ed 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/Tool.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/Tool.java @@ -4,6 +4,7 @@ import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel; import com.chuangzhou.vivid2D.render.model.util.tools.RanderTools; import java.awt.*; +import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; /** @@ -92,6 +93,8 @@ public abstract class Tool { return toolDescription; } + public void onKeyPressed(KeyEvent e){}; + /** * 获取工具光标 */ diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/VertexDeformationTool.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/VertexDeformationTool.java index d1e3464..99d5d55 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/VertexDeformationTool.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/VertexDeformationTool.java @@ -13,13 +13,16 @@ import org.slf4j.LoggerFactory; import java.awt.*; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; -import java.util.ArrayList; // 新增导入 +import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.awt.event.KeyEvent; +import java.util.HashSet; +import java.util.Set; +import java.util.Comparator; /** - * 顶点变形工具 - * 用于通过二级顶点对网格进行精细变形 + * 顶点变形工具(完整) */ public class VertexDeformationTool extends Tool { private static final Logger logger = LoggerFactory.getLogger(VertexDeformationTool.class); @@ -35,41 +38,32 @@ public class VertexDeformationTool extends Tool { private boolean cameraStateSaved = false; private final List changeListeners = new ArrayList<>(); + private final List drawPath = new ArrayList<>(); + private boolean isDrawingPath = false; + private static final float SNAP_TOLERANCE_MODEL = 8.0f; public VertexDeformationTool(ModelRenderPanel renderPanel) { super(renderPanel, "顶点变形工具", "通过二级顶点对网格进行精细变形操作"); } - // 新增:二级顶点变化监听器接口 public interface SecondaryVertexChangeListener { - /** - * 当二级顶点发生变化(创建、移动、删除)时调用。 - * @param part 发生变化的 ModelPart - * @param mesh 发生变化的 Mesh2D - * @param vertex 发生变化的 SecondaryVertex - * @param type 变化类型:CREATE, MOVE, DELETE - */ void onSecondaryVertexChange(ModelPart part, Mesh2D mesh, SecondaryVertex vertex, ChangeType type); } - // 新增:变化类型枚举 public enum ChangeType { CREATE, MOVE, DELETE } - // 新增:添加监听者 public void addChangeListener(SecondaryVertexChangeListener listener) { if (listener != null && !changeListeners.contains(listener)) { changeListeners.add(listener); } } - // 新增:移除监听者 public void removeChangeListener(SecondaryVertexChangeListener listener) { changeListeners.remove(listener); } - // 新增:通知监听者 private void notifyListeners(SecondaryVertex vertex, ChangeType type) { if (targetMesh == null || targetMesh.getModelPart() == null) return; ModelPart part = targetMesh.getModelPart(); @@ -90,23 +84,18 @@ public class VertexDeformationTool extends Tool { isActive = true; - // 尝试获取选中的网格,如果没有选中则使用第一个可见网格 if (!renderPanel.getSelectedMeshes().isEmpty()) { targetMesh = renderPanel.getSelectedMesh(); } else { - // 如果没有选中的网格,尝试获取第一个可见网格 targetMesh = findFirstVisibleMesh(); } - // 记录并重置相机(如果可用)到默认状态:旋转 = 0,缩放 = 1 try { if (renderPanel.getCameraManagement() != null && targetMesh != null && targetMesh.getModelPart() != null) { - // 备份 savedCameraRotation = targetMesh.getModelPart().getRotation(); savedCameraScale = new Vector2f(targetMesh.getModelPart().getScale().x, targetMesh.getModelPart().getScale().y); cameraStateSaved = true; - // 设置为默认(在 GL 线程中执行变更) renderPanel.getGlContextManager().executeInGLContext(() -> { try { targetMesh.getModelPart().setRotation(0f); @@ -122,7 +111,6 @@ public class VertexDeformationTool extends Tool { } if (targetMesh != null) { - // 显示二级顶点 associatedRanderTools.setAlgorithmEnabled("showSecondaryVertices", true); renderPanel.getGlContextManager().executeInGLContext(() -> { try { @@ -168,7 +156,6 @@ public class VertexDeformationTool extends Tool { try { targetMesh.setShowSecondaryVertices(false); targetMesh.setRenderVertices(false); - // 标记脏,触发必要的刷新 if (targetMesh.getModelPart() != null) { targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition()); targetMesh.getModelPart().updateMeshVertices(); @@ -187,42 +174,48 @@ public class VertexDeformationTool extends Tool { @Override public void onMousePressed(MouseEvent e, float modelX, float modelY) { - if (!isActive || targetMesh == null) return; - - // 选择二级顶点(select 操作不需要 GL 线程来 read,但为一致性在GL线程处理选择标记) + if (!isActive || targetMesh == null || isDrawingPath) return; renderPanel.getGlContextManager().executeInGLContext(() -> { try { SecondaryVertex clickedVertex = findSecondaryVertexAtPosition(modelX, modelY); + + if (!e.isShiftDown()) { + List svs = targetMesh.getSecondaryVertices(); + for (SecondaryVertex sv : svs) { + sv.setSelected(false); + } + selectedVertex = null; + } + if (clickedVertex != null) { - targetMesh.setSelectedSecondaryVertex(clickedVertex); - selectedVertex = clickedVertex; - - // 开始拖拽 + clickedVertex.setSelected(!clickedVertex.isSelected() || e.isShiftDown()); + if (clickedVertex.isSelected()) { + selectedVertex = clickedVertex; + } else if (selectedVertex == clickedVertex) { + selectedVertex = null; + } currentDragMode = ModelRenderPanel.DragMode.MOVE_SECONDARY_VERTEX; - logger.debug("开始移动二级顶点: ID={}, 位置({}, {})", clickedVertex.getId(), modelX, modelY); } else { - // 点击空白处,取消选择 - targetMesh.setSelectedSecondaryVertex(null); - selectedVertex = null; currentDragMode = ModelRenderPanel.DragMode.NONE; } } catch (Throwable t) { logger.error("onMousePressed (VertexDeformationTool) 处理失败", t); + } finally { + renderPanel.repaint(); } }); } + @Override public void onMouseReleased(MouseEvent e, float modelX, float modelY) { if (!isActive) return; - // 记录操作历史(可在这里添加撤销记录) if (currentDragMode == ModelRenderPanel.DragMode.MOVE_SECONDARY_VERTEX && selectedVertex != null) { logger.debug("完成移动二级顶点: ID={}", selectedVertex.getId()); - // 顶点移动完成,触发回调 - notifyListeners(selectedVertex, ChangeType.MOVE); // 新增:移动回调 + notifyListeners(selectedVertex, ChangeType.MOVE); } currentDragMode = ModelRenderPanel.DragMode.NONE; @@ -233,16 +226,12 @@ public class VertexDeformationTool extends Tool { if (!isActive || selectedVertex == null) return; if (currentDragMode == ModelRenderPanel.DragMode.MOVE_SECONDARY_VERTEX) { - // 在 GL 线程中修改顶点与部件状态,保持线程安全与渲染同步 renderPanel.getGlContextManager().executeInGLContext(() -> { try { - // 移动顶点到新位置 selectedVertex.setPosition(modelX, modelY); - // 持续拖拽,触发回调(使用 MOVE 类型) - notifyListeners(selectedVertex, ChangeType.MOVE); // 新增:持续移动回调 + notifyListeners(selectedVertex, ChangeType.MOVE); - // 广播:secondaryVertex -> { id, pos:[x,y] } try { if (targetMesh != null && targetMesh.getModelPart() != null) { Map payload = Map.of( @@ -250,15 +239,11 @@ public class VertexDeformationTool extends Tool { "pos", List.of(modelX, modelY) ); renderPanel.getParametersManagement().broadcast(targetMesh.getModelPart(), "secondaryVertex", payload); - //logger.info("广播 secondaryVertex: {}", payload); } } catch (Throwable bx) { logger.debug("广播 secondaryVertex 失败: {}", bx.getMessage()); } - // 更新拖拽起始位置 - - // 标记网格为脏状态,需要重新计算边界等 if (targetMesh != null && targetMesh.getModelPart() != null) { targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition()); targetMesh.updateBounds(); @@ -267,7 +252,6 @@ public class VertexDeformationTool extends Tool { } catch (Throwable t) { logger.error("onMouseDragged (VertexDeformationTool) 处理失败", t); } finally { - // 请求 UI 重绘(在 UI 线程) renderPanel.repaint(); } }); @@ -277,16 +261,17 @@ public class VertexDeformationTool extends Tool { @Override public void onMouseMoved(MouseEvent e, float modelX, float modelY) { if (!isActive || targetMesh == null) return; - - // 更新悬停的二级顶点(仅读取,不进行写入) —— 在主线程做轻量检测(容忍略微延迟) + if (isDrawingPath) { + targetMesh.setPreviewPoint(new Vector2f(modelX, modelY)); + renderPanel.repaint(); + } SecondaryVertex newHoveredVertex = findSecondaryVertexAtPosition(modelX, modelY); - if (newHoveredVertex != hoveredVertex) { hoveredVertex = newHoveredVertex; - - // 更新光标(在 UI 线程) if (hoveredVertex != null) { renderPanel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } else if (isDrawingPath) { + renderPanel.setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR)); } else { renderPanel.setCursor(createVertexCursor()); } @@ -297,9 +282,17 @@ public class VertexDeformationTool extends Tool { public void onMouseClicked(MouseEvent e, float modelX, float modelY) { if (!isActive || targetMesh == null) return; - // 如果点击了空白处且没有顶点被选中,可以创建新顶点 - if (selectedVertex == null && findSecondaryVertexAtPosition(modelX, modelY) == null) { - // 这里选择不在单击时自动创建顶点,保留为可选功能 + if (isDrawingPath) { + renderPanel.getGlContextManager().executeInGLContext(() -> { + try { + addPointToPath(modelX, modelY); + } catch (Throwable t) { + logger.error("onMouseClicked(addPointToPath) 失败", t); + } finally { + renderPanel.repaint(); + } + }); + return; } } @@ -307,35 +300,336 @@ public class VertexDeformationTool extends Tool { public void onMouseDoubleClicked(MouseEvent e, float modelX, float modelY) { if (!isActive || targetMesh == null) return; - // 双击需要修改模型,放到 GL 线程中 renderPanel.getGlContextManager().executeInGLContext(() -> { try { SecondaryVertex clickedVertex = findSecondaryVertexAtPosition(modelX, modelY); - if (clickedVertex != null) { - // 双击二级顶点:删除该顶点 - deleteSecondaryVertex(clickedVertex); + + if (!isDrawingPath) { + if (clickedVertex != null) { + drawPath.clear(); + Vector2f p = clickedVertex.getPosition(); + drawPath.add(new Vector2f(p.x, p.y)); + isDrawingPath = true; + renderPanel.repaint(); + logger.info("开始绘制路径(起点为顶点 ID={})", clickedVertex.getId()); + } else { + createSecondaryVertexAt(modelX, modelY); + } } else { - // 双击空白处:创建新的二级顶点 - createSecondaryVertexAt(modelX, modelY); + if (clickedVertex != null) { + addPointToPath(clickedVertex.getPosition().x, clickedVertex.getPosition().y); + commitPath(); + } else { + SnapResult snap = snapToVertexOrSegment(modelX, modelY); + if (snap.type == SnapResult.Type.VERTEX) { + // 如果吸附到顶点,并且该顶点不是路径最后一个点 + Vector2f last = drawPath.get(drawPath.size() - 1); + if (last.distance(new Vector2f((float)snap.px, (float)snap.py)) > 1e-6f) { + addPointToPath((float) snap.px, (float) snap.py); + } + commitPath(); + } else if (snap.type == SnapResult.Type.SEGMENT) { + addPointToPath((float) snap.px, (float) snap.py); + commitPath(); + } else { + if (drawPath.size() >= 2) { + Vector2f first = drawPath.get(0); + float dx = modelX - first.x; + float dy = modelY - first.y; + float tol = SNAP_TOLERANCE_MODEL / calculateScaleFactor(); + if (dx * dx + dy * dy <= tol * tol) { + commitPath(); + } else { + addPointToPath(modelX, modelY); + commitPath(); + } + } else { + drawPath.clear(); + isDrawingPath = false; + renderPanel.repaint(); + } + } + } } } catch (Throwable t) { - logger.error("onMouseDoubleClicked (VertexDeformationTool) 处理失败", t); - } finally { + logger.error("onMouseDoubleClicked 处理失败", t); + drawPath.clear(); + isDrawingPath = false; renderPanel.repaint(); } }); } + @Override + public void onKeyPressed(KeyEvent e) { + if (!isActive) return; + + if (selectedVertex != null) { + int kc = e.getKeyCode(); + if (kc == KeyEvent.VK_BACK_SPACE || kc == KeyEvent.VK_DELETE || kc == KeyEvent.VK_LEFT) { + renderPanel.getGlContextManager().executeInGLContext(() -> { + try { + deleteSecondaryVertex(selectedVertex); + selectedVertex = null; + renderPanel.repaint(); + } catch (Throwable t) { + logger.error("onKeyPressed 删除顶点失败", t); + } + }); + } + } + + if (e.getKeyCode() == KeyEvent.VK_ESCAPE && isDrawingPath) { + drawPath.clear(); + isDrawingPath = false; + renderPanel.repaint(); + } + } + + private void addPointToPath(float x, float y) { + if (targetMesh == null || !isDrawingPath) return; + float tol = SNAP_TOLERANCE_MODEL / calculateScaleFactor(); + SecondaryVertex nearest = targetMesh.selectSecondaryVertexAt(x, y, tol); + if (nearest != null) { + Vector2f p = nearest.getPosition(); + if (drawPath.size() >= 2 && drawPath.get(0).distance(p) <= 1e-6f) { + commitPath(); + return; + } + drawPath.add(new Vector2f(p.x, p.y)); + logger.debug("路径吸附到顶点 ID={} at ({},{})", nearest.getId(), p.x, p.y); + targetMesh.setPreviewPoint(null); + return; + } + List svs = targetMesh.getSecondaryVertices(); + float bestD = Float.POSITIVE_INFINITY; + Vector2f bestProj = null; + for (int i = 0; i < svs.size(); i++) { + for (int j = i + 1; j < svs.size(); j++) { + Vector2f a = svs.get(i).getPosition(); + Vector2f b = svs.get(j).getPosition(); + double proj[] = projectPointToSegment(x, y, a.x, a.y, b.x, b.y); + double px = proj[0], py = proj[1], dist = proj[2]; + if (dist < bestD) { + bestD = (float) dist; + bestProj = new Vector2f((float) px, (float) py); + } + } + } + if (bestProj != null && bestD <= tol) { + drawPath.add(new Vector2f(bestProj.x, bestProj.y)); + logger.debug("路径吸附到线段投影 at ({},{}) dist={}", bestProj.x, bestProj.y, bestD); + targetMesh.setPreviewPoint(null); + return; + } + drawPath.add(new Vector2f(x, y)); + targetMesh.setPreviewPoint(null); + } + + private boolean pointsAreEqual(Vector2f a, Vector2f b) { + return a.distanceSquared(b) < 1e-12f; + } + + private void commitPath() { + if (targetMesh == null) return; + + if (drawPath.size() < 3) { + logger.info("路径闭合失败:点数不足"); + drawPath.clear(); + isDrawingPath = false; + targetMesh.setPreviewPoint(null); + renderPanel.setCursor(createVertexCursor()); + renderPanel.repaint(); + return; + } + + // 保留原始顺序,不去重 + List poly = new ArrayList<>(drawPath); + + // 确保首尾闭合 + Vector2f first = poly.get(0); + Vector2f last = poly.get(poly.size() - 1); + if (!pointsAreEqual(first, last)) { + poly.add(new Vector2f(first.x, first.y)); + } + + List verticesInPolygon = new ArrayList<>(); + for (Vector2f p : poly) { + SecondaryVertex v = targetMesh.addSecondaryVertex(p.x, p.y, 0f, 0f); + if (v != null) { + v.setSelected(true); + verticesInPolygon.add(v); + } + } + + if (!verticesInPolygon.isEmpty()) { + // 第一个顶点作为 master + SecondaryVertex masterVertex = verticesInPolygon.get(0); + SecondaryVertex.ControlShape masterShape = masterVertex.getControlShape(); + + // 清理 masterShape + masterShape.clearControlVertices(); + + float minX = Float.MAX_VALUE, minY = Float.MAX_VALUE; + float maxX = Float.MIN_VALUE, maxY = Float.MIN_VALUE; + + for (SecondaryVertex sv : verticesInPolygon) { + Vector2f pos = sv.getPosition(); + masterShape.addControlVertex(sv); + + minX = Math.min(minX, pos.x); + minY = Math.min(minY, pos.y); + maxX = Math.max(maxX, pos.x); + maxY = Math.max(maxY, pos.y); + } + + masterShape.setMinControlPoint(new Vector2f(minX, minY)); + masterShape.setMaxControlPoint(new Vector2f(maxX, maxY)); + } + + selectedVertex = null; + drawPath.clear(); + isDrawingPath = false; + targetMesh.setPreviewPoint(null); + renderPanel.setCursor(createVertexCursor()); + + if (targetMesh.getModelPart() != null) { + targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition()); + targetMesh.getModelPart().updateMeshVertices(); + } + renderPanel.repaint(); + + logger.info("路径闭合完成:总顶点数量={}", verticesInPolygon.size()); + } + + private double[] projectPointToSegment(double px, double py, double x1, double y1, double x2, double y2) { + double vx = x2 - x1, vy = y2 - y1; + double wx = px - x1, wy = py - y1; + double c1 = vx * wx + vy * wy; + if (c1 <= 0) { + double d = Math.hypot(px - x1, py - y1); + return new double[]{x1, y1, d}; + } + double c2 = vx * vx + vy * vy; + if (c2 <= c1) { + double d = Math.hypot(px - x2, py - y2); + return new double[]{x2, y2, d}; + } + double t = c1 / c2; + double projx = x1 + t * vx; + double projy = y1 + t * vy; + double d = Math.hypot(px - projx, py - projy); + return new double[]{projx, projy, d}; + } + + private boolean pointInPolygonRayCast(float x, float y, java.util.List poly) { + boolean inside = false; + int n = poly.size(); + for (int i = 0, j = n - 1; i < n; j = i++) { + Vector2f vi = poly.get(i); + Vector2f vj = poly.get(j); + boolean intersect = ((vi.y > y) != (vj.y > y)) && + (x < (vj.x - vi.x) * (y - vi.y) / (vj.y - vi.y + 1e-12f) + vi.x); + if (intersect) inside = !inside; + } + return inside; + } + + private boolean polygonIsSelfIntersecting(java.util.List poly) { + if (poly == null) return false; + int n = poly.size(); + if (n < 4) return false; + for (int i = 0; i < n; i++) { + Vector2f a1 = poly.get(i); + Vector2f a2 = poly.get((i + 1) % n); + for (int j = i + 1; j < n; j++) { + if (Math.abs(i - j) <= 1 || (i == 0 && j == n - 1)) continue; + Vector2f b1 = poly.get(j); + Vector2f b2 = poly.get((j + 1) % n); + if (segmentsIntersect(a1.x, a1.y, a2.x, a2.y, b1.x, b1.y, b2.x, b2.y)) return true; + } + } + return false; + } + + private boolean segmentsIntersect(double x1, double y1, double x2, double y2, + double x3, double y3, double x4, double y4) { + if (Math.max(x1, x2) < Math.min(x3, x4) || Math.max(x3, x4) < Math.min(x1, x2) || + Math.max(y1, y2) < Math.min(y3, y4) || Math.max(y3, y4) < Math.min(y1, y2)) { + return false; + } + double d1 = orient(x3, y3, x4, y4, x1, y1); + double d2 = orient(x3, y3, x4, y4, x2, y2); + double d3 = orient(x1, y1, x2, y2, x3, y3); + double d4 = orient(x1, y1, x2, y2, x4, y4); + + if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && + ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) { + return true; + } + if (Math.abs(d1) < 1e-10 && onSegment(x3,y3,x4,y4,x1,y1)) return true; + if (Math.abs(d2) < 1e-10 && onSegment(x3,y3,x4,y4,x2,y2)) return true; + if (Math.abs(d3) < 1e-10 && onSegment(x1,y1,x2,y2,x3,y3)) return true; + if (Math.abs(d4) < 1e-10 && onSegment(x1,y1,x2,y2,x4,y4)) return true; + return false; + } + private double orient(double ax,double ay,double bx,double by,double cx,double cy) { + return (bx-ax)*(cy-ay) - (by-ay)*(cx-ax); + } + private boolean onSegment(double ax,double ay,double bx,double by,double px,double py) { + return px >= Math.min(ax,bx) - 1e-8 && px <= Math.max(ax,bx) + 1e-8 && + py >= Math.min(ay,by) - 1e-8 && py <= Math.max(ay,by) + 1e-8 && + Math.abs(orient(ax,ay,bx,by,px,py)) < 1e-8; + } + + private java.util.List convexHull(java.util.List points) { + java.util.List pts = new java.util.ArrayList<>(); + if (points == null || points.isEmpty()) return pts; + java.util.Set seen = new java.util.HashSet<>(); + for (Vector2f p : points) { + String k = String.format("%.6f_%.6f", p.x, p.y); + if (!seen.contains(k)) { seen.add(k); pts.add(new Vector2f(p.x, p.y)); } + } + if (pts.size() <= 1) return new java.util.ArrayList<>(pts); + pts.sort(new Comparator() { + @Override + public int compare(Vector2f a, Vector2f b) { + int c = Float.compare(a.x, b.x); + if (c != 0) return c; + return Float.compare(a.y, b.y); + } + }); + java.util.List lower = new java.util.ArrayList<>(); + for (Vector2f p : pts) { + while (lower.size() >= 2) { + Vector2f p1 = lower.get(lower.size()-2), p2 = lower.get(lower.size()-1); + if (orient(p1.x,p1.y,p2.x,p2.y,p.x,p.y) <= 0) lower.remove(lower.size()-1); + else break; + } + lower.add(p); + } + java.util.List upper = new java.util.ArrayList<>(); + for (int i = pts.size()-1; i >= 0; i--) { + Vector2f p = pts.get(i); + while (upper.size() >= 2) { + Vector2f p1 = upper.get(upper.size()-2), p2 = upper.get(upper.size()-1); + if (orient(p1.x,p1.y,p2.x,p2.y,p.x,p.y) <= 0) upper.remove(upper.size()-1); + else break; + } + upper.add(p); + } + lower.remove(lower.size()-1); + upper.remove(upper.size()-1); + lower.addAll(upper); + return lower; + } + + @Override public Cursor getToolCursor() { return createVertexCursor(); } - // ================== 工具特定方法 ================== - - /** - * 查找第一个可见的网格 - */ private Mesh2D findFirstVisibleMesh() { Model2D model = renderPanel.getModel(); if (model == null) return null; @@ -358,14 +652,10 @@ public class VertexDeformationTool extends Tool { return null; } - /** - * 在指定位置创建二级顶点 - */ private void createSecondaryVertexAt(float x, float y) { if (targetMesh == null) return; try { - // 确保边界框是最新的 targetMesh.updateBounds(); BoundingBox bounds = targetMesh.getBounds(); if (bounds == null || !bounds.isValid()) { @@ -373,11 +663,9 @@ public class VertexDeformationTool extends Tool { return; } - // 计算UV坐标(基于边界框) 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)); @@ -386,10 +674,8 @@ public class VertexDeformationTool extends Tool { logger.info("创建二级顶点: ID={}, 位置({}, {}), UV({}, {})", newVertex.getId(), x, y, u, v); - // 触发回调 - notifyListeners(newVertex, ChangeType.CREATE); // 新增:创建回调 + notifyListeners(newVertex, ChangeType.CREATE); - // 广播创建(GL 线程内) try { if (targetMesh.getModelPart() != null) { Map payload = Map.of( @@ -415,9 +701,6 @@ public class VertexDeformationTool extends Tool { } } - /** - * 删除二级顶点 - */ private void deleteSecondaryVertex(SecondaryVertex vertex) { if (targetMesh == null || vertex == null) return; @@ -432,10 +715,14 @@ public class VertexDeformationTool extends Tool { } logger.info("删除二级顶点: ID={}", vertex.getId()); - // 触发回调 - notifyListeners(vertex, ChangeType.DELETE); // 新增:删除回调 + List svs = targetMesh.getSecondaryVertices(); + for (SecondaryVertex sv : svs) { + // 清理所有ControlShape中对被删除顶点的引用 + sv.getControlShape().removeControlVertex(vertex); + } + + notifyListeners(vertex, ChangeType.DELETE); - // 广播删除(将 pos 设为 null 表示删除,可由 FrameInterpolator 识别) try { if (targetMesh.getModelPart() != null) { Map payload = Map.of( @@ -448,7 +735,6 @@ public class VertexDeformationTool extends Tool { logger.debug("广播 secondaryVertex(删除) 失败: {}", bx.getMessage()); } - // 标记网格为脏状态 if (targetMesh.getModelPart() != null) { targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition()); targetMesh.getModelPart().updateMeshVertices(); @@ -462,9 +748,6 @@ public class VertexDeformationTool extends Tool { } } - /** - * 在指定位置查找二级顶点 - */ private SecondaryVertex findSecondaryVertexAtPosition(float x, float y) { if (targetMesh == null) return null; @@ -472,42 +755,30 @@ public class VertexDeformationTool extends Tool { return targetMesh.selectSecondaryVertexAt(x, y, tolerance); } - /** - * 计算当前缩放因子 - */ private float calculateScaleFactor() { return renderPanel.getCameraManagement().calculateScaleFactor(); } - /** - * 创建顶点工具光标 - */ private Cursor createVertexCursor() { 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); @@ -516,8 +787,6 @@ public class VertexDeformationTool extends Tool { return Toolkit.getDefaultToolkit().createCustomCursor(cursorImg, new Point(center, center), "VertexCursor"); } - // ================== 获取工具状态 ================== - public Mesh2D getTargetMesh() { return targetMesh; } @@ -533,4 +802,60 @@ public class VertexDeformationTool extends Tool { public boolean isDragging() { return currentDragMode == ModelRenderPanel.DragMode.MOVE_SECONDARY_VERTEX; } -} \ No newline at end of file + + private static class SnapResult { + enum Type { NONE, VERTEX, SEGMENT } + final Type type; + final double px; + final double py; + final SecondaryVertex vertex; + + SnapResult(Type type, double px, double py, SecondaryVertex vertex) { + this.type = type; + this.px = px; + this.py = py; + this.vertex = vertex; + } + + static SnapResult none() { return new SnapResult(Type.NONE, 0, 0, null); } + static SnapResult vertex(SecondaryVertex v) { Vector2f p = v.getPosition(); return new SnapResult(Type.VERTEX, p.x, p.y, v); } + static SnapResult segment(double x, double y) { return new SnapResult(Type.SEGMENT, x, y, null); } + } + + private SnapResult snapToVertexOrSegment(float x, float y) { + if (targetMesh == null) return SnapResult.none(); + + float tol = SNAP_TOLERANCE_MODEL / calculateScaleFactor(); + + SecondaryVertex nearest = targetMesh.selectSecondaryVertexAt(x, y, tol); + if (nearest != null) { + return SnapResult.vertex(nearest); + } + + List svs = targetMesh.getSecondaryVertices(); + if (svs == null || svs.size() < 2) return SnapResult.none(); + + double bestDist = Double.POSITIVE_INFINITY; + double bestPx = 0, bestPy = 0; + + for (int i = 0; i < svs.size(); i++) { + Vector2f a = svs.get(i).getPosition(); + for (int j = i + 1; j < svs.size(); j++) { + Vector2f b = svs.get(j).getPosition(); + double[] proj = projectPointToSegment(x, y, a.x, a.y, b.x, b.y); + double px = proj[0], py = proj[1], dist = proj[2]; + if (dist < bestDist) { + bestDist = dist; + bestPx = px; + bestPy = py; + } + } + } + + if (bestDist <= tol) { + return SnapResult.segment(bestPx, bestPy); + } + return SnapResult.none(); + } + +} 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 a328cc0..75c1f37 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 @@ -307,31 +307,25 @@ public class MeshData implements Serializable { public Vector2f originalPosition; public Vector2f uv; public boolean selected; - public boolean pinned; - public boolean locked; - public float controlRadius; - public float minControlRadius; - public float maxControlRadius; - public boolean fixedRadius; + public ControlShapeData controlShape; public SecondaryVertexData() { + this.controlShape = new ControlShapeData(); } - 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.position = vertex.getPosition(); + this.originalPosition = vertex.getOriginalPosition(); + this.uv = vertex.getUV(); this.selected = vertex.isSelected(); - - this.pinned = vertex.isPinned(); - this.locked = vertex.isLocked(); - this.controlRadius = vertex.getControlRadius(); - this.minControlRadius = vertex.getMinControlRadius(); - this.maxControlRadius = vertex.getMaxControlRadius(); - this.fixedRadius = vertex.isFixedRadius(); + this.controlShape = createControlShapeData(vertex.getControlShape()); } + private static ControlShapeData createControlShapeData(SecondaryVertex.ControlShape controlShape) { + return new ControlShapeData(controlShape); + } + + public SecondaryVertexData copy() { SecondaryVertexData copy = new SecondaryVertexData(); copy.id = this.id; @@ -339,17 +333,57 @@ public class MeshData implements Serializable { copy.originalPosition = new Vector2f(this.originalPosition); copy.uv = new Vector2f(this.uv); copy.selected = this.selected; - - // 复制新增字段 - copy.pinned = this.pinned; - copy.locked = this.locked; - copy.controlRadius = this.controlRadius; - copy.minControlRadius = this.minControlRadius; - copy.maxControlRadius = this.maxControlRadius; - copy.fixedRadius = this.fixedRadius; - + copy.controlShape = this.controlShape.copy(); return copy; } + + /** + * 控制点形状和约束数据类(可序列化),用于封装二级顶点控制属性。 + */ + public static class ControlShapeData implements Serializable { + @Serial + private static final long serialVersionUID = 1L; + public int shapeId; + public boolean pinned; + public boolean locked; + public Vector2f minControlPoint; + public Vector2f maxControlPoint; + + public ControlShapeData() { + this.minControlPoint = new Vector2f(0, 0); + this.maxControlPoint = new Vector2f(0, 0); + } + + /** + * 构造函数:从 SecondaryVertex.ControlShape 对象创建数据 + */ + public ControlShapeData(SecondaryVertex.ControlShape controlShape) { + this.shapeId = controlShape.getShapeId(); + this.pinned = controlShape.isPinned(); + this.locked = controlShape.isLocked(); + this.minControlPoint = controlShape.getMinControlPoint(); + this.maxControlPoint = controlShape.getMaxControlPoint(); + } + + /** + * 创建 ControlShapeData 的深拷贝 + */ + public ControlShapeData copy() { + ControlShapeData copy = new ControlShapeData(); + copy.shapeId = this.shapeId; + copy.pinned = this.pinned; + copy.locked = this.locked; + copy.minControlPoint = new Vector2f(this.minControlPoint); + copy.maxControlPoint = new Vector2f(this.maxControlPoint); + return copy; + } + + @Override + public String toString() { + return String.format("ControlShapeData{id=%d, pinned=%s, locked=%s, min=(%.2f, %.2f), max=(%.2f, %.2f)}", + shapeId, pinned, locked, minControlPoint.x, minControlPoint.y, maxControlPoint.x, maxControlPoint.y); + } + } } /** diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java index 4e8d520..b82765f 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 @@ -2,7 +2,6 @@ package com.chuangzhou.vivid2D.render.model.util; import com.chuangzhou.vivid2D.render.ModelRender; import com.chuangzhou.vivid2D.render.MultiSelectionBoxRenderer; -import com.chuangzhou.vivid2D.render.TextRenderer; import com.chuangzhou.vivid2D.render.model.ModelPart; import com.chuangzhou.vivid2D.render.model.util.manager.RanderToolsManager; import com.chuangzhou.vivid2D.render.systems.Matrix3fUtils; @@ -88,7 +87,6 @@ public class Mesh2D { private boolean showPuppetPins = true; private final Vector4f puppetPinColor = new Vector4f(1.0f, 0.0f, 0.0f, 1.0f); // 红色控制点 private final Vector4f selectedPuppetPinColor = new Vector4f(1.0f, 1.0f, 0.0f, 1.0f); // 黄色选中的控制点 - private float puppetPinSize = 8.0f; // ==================== 常量 ==================== public static final int POINTS = 0; @@ -141,7 +139,7 @@ public class Mesh2D { } public float getPuppetPinSize() { - return puppetPinSize; + return 8.0f; } public Vector4f getLiquifyOverlayColor() { @@ -156,30 +154,6 @@ public class Mesh2D { 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(); - } - /** * 获取渲染用顶点(返回副本以防外部修改) */ @@ -329,108 +303,6 @@ public class Mesh2D { return removed; } - /** - * 预测:在不修改当前 mesh 的情况下,如果在 tempPos 放一个控制点(半径 tempRadius) - * 将会得到的顶点数组(与 vertices 长度相同的新数组)。 - * 使用与 updateVerticesFromSecondaryVertices 类似的三角分配策略进行预测(回退到 IDW)。 - */ - public float[] predictVerticesWithTemporarySecondary(org.joml.Vector2f tempPos, float tempRadius) { - if (originalVertices == null || originalVertices.length == 0) return null; - - int secCount = (secondaryVertices == null) ? 0 : secondaryVertices.size(); - // 构建临时控制点数组(原始位置 + 当前位置) - int tmpCount = secCount + 1; // 包含临时点 - org.joml.Vector2f[] secOrig = new org.joml.Vector2f[tmpCount]; - org.joml.Vector2f[] secCurr = new org.joml.Vector2f[tmpCount]; - float[] secRadius = new float[tmpCount]; - - for (int i = 0; i < secCount; i++) { - SecondaryVertex sv = secondaryVertices.get(i); - secOrig[i] = sv.getOriginalPosition(); - secCurr[i] = sv.getPosition(); - secRadius[i] = sv.getControlRadius(); - } - // 最后一个为临时点 - secOrig[tmpCount - 1] = (previewPoint != null && previewPoint.equals(tempPos)) ? tempPos : new org.joml.Vector2f(tempPos); - secCurr[tmpCount - 1] = new org.joml.Vector2f(tempPos); - secRadius[tmpCount - 1] = Math.max(4f, tempRadius); - - // 结果数组(不修改实际 vertices) - float[] pred = new float[originalVertices.length]; - - // 辅助:重用类内的 findNearestNIndices/pointInTriangle/barycentricCoordinates(如果存在) - // 若这些方法是 private 且在同一类中可以直接调用;若不存在则使用简单回退 IDW(这里假设存在) - try { - for (int i = 0; i < originalVertices.length; i += 2) { - float ox = originalVertices[i]; - float oy = originalVertices[i + 1]; - - // 找到最近三个控制点(优先考虑 controlRadius 内的点) - int[] nearest = findNearestNIndices(ox, oy, 3, secOrig); - if (nearest == null || nearest.length < 3) { - // 回退到反距离加权预测(简单实现,不依赖外部私有方法) - float power = 2.0f; - float eps = 1e-4f; - float wsum = 0f; - float nx = 0f, ny = 0f; - for (int k = 0; k < tmpCount; k++) { - float dx = ox - secOrig[k].x; - float dy = oy - secOrig[k].y; - float d = (float)Math.sqrt(dx*dx + dy*dy); - if (d < eps) { nx = secCurr[k].x; ny = secCurr[k].y; wsum = 1f; break; } - float w = 1.0f / (float)Math.pow(d, power); - float deltaX = secCurr[k].x - secOrig[k].x; - float deltaY = secCurr[k].y - secOrig[k].y; - nx += w * (ox + deltaX); - ny += w * (oy + deltaY); - wsum += w; - } - if (wsum > 0f) { - pred[i] = nx / wsum; - pred[i + 1] = ny / wsum; - } else { - pred[i] = ox; pred[i + 1] = oy; - } - continue; - } - - int ia = nearest[0], ib = nearest[1], ic = nearest[2]; - org.joml.Vector2f A = secOrig[ia], B = secOrig[ib], C = secOrig[ic]; - // 检测退化 - float area2 = Math.abs((B.x - A.x) * (C.y - A.y) - (C.x - A.x) * (B.y - A.y)); - if (area2 < 1e-6f) { - // 退化:取最近控制点位移 - int nearestIdx = nearest[0]; - org.joml.Vector2f sOrig = secOrig[nearestIdx]; - org.joml.Vector2f sCurr = secCurr[nearestIdx]; - pred[i] = ox + (sCurr.x - sOrig.x); - pred[i + 1] = oy + (sCurr.y - sOrig.y); - continue; - } - - if (pointInTriangle(ox, oy, A, B, C)) { - float[] bary = barycentricCoordinates(A, B, C, ox, oy); - org.joml.Vector2f Acur = secCurr[ia], Bcur = secCurr[ib], Ccur = secCurr[ic]; - float nx = bary[0] * Acur.x + bary[1] * Bcur.x + bary[2] * Ccur.x; - float ny = bary[0] * Acur.y + bary[1] * Bcur.y + bary[2] * Ccur.y; - pred[i] = nx; pred[i + 1] = ny; - } else { - // 点不在三角形内:使用最近控制点位移 - int nearestIdx = nearest[0]; - org.joml.Vector2f sOrig = secOrig[nearestIdx]; - org.joml.Vector2f sCurr = secCurr[nearestIdx]; - pred[i] = ox + (sCurr.x - sOrig.x); - pred[i + 1] = oy + (sCurr.y - sOrig.y); - } - } - } catch (Exception ex) { - // 出错时直接返回当前顶点数组的拷贝(不变) - pred = java.util.Arrays.copyOf(vertices, vertices.length); - } - - return pred; - } - /** * 设置选中的木偶控制点 */ @@ -1091,9 +963,6 @@ public class Mesh2D { public void addSecondaryVertex(SecondaryVertex newV) { if (secondaryVertices == null) return; secondaryVertices.add(newV); - // 初始插入后用优化器处理与邻域的半径冲突 - RegionOptimizer.resolveForInsertedVertex(newV, secondaryVertices); - // 可选:立刻触发一次网格更新 updateVerticesFromSecondaryVertices(); } @@ -1117,8 +986,8 @@ public class Mesh2D { */ public boolean removeSecondaryVertex(int id) { return secondaryVertices.removeIf(vertex -> { - if (vertex.id == id) { - if (selectedSecondaryVertex != null && selectedSecondaryVertex.id == id) { + if (vertex.getId() == id) { + if (selectedSecondaryVertex != null && selectedSecondaryVertex.getId() == id) { selectedSecondaryVertex = null; } return true; @@ -1170,7 +1039,7 @@ public class Mesh2D { */ public SecondaryVertex getSecondaryVertex(int id) { return secondaryVertices.stream() - .filter(vertex -> vertex.id == id) + .filter(vertex -> vertex.getId() == id) .findFirst() .orElse(null); } @@ -1366,11 +1235,7 @@ public class Mesh2D { origCy /= vertCount; // 根据控制点数量选择策略 - if (secondaryVertices.size() < 3) { - updateVerticesUsingInverseDistanceWeighting(); - } else { - updateVerticesUsingTriangularPartition(); - } + updateVerticesUsingMLS(); // 计算变形后顶点的质心 float newCx = 0f, newCy = 0f; @@ -1395,108 +1260,399 @@ public class Mesh2D { } } - /** - * 使用“最近三点形成三角形 + 重心坐标映射”的方法更新顶点 - * 若三角形退化或点不在三角形内,则回退到最近点位移(nearest pin displacement)或最终回退到 IDW。 - */ - private void updateVerticesUsingTriangularPartition() { + private void updateVerticesUsingMLS() { try { int secCount = secondaryVertices.size(); if (secCount == 0) return; - // 预取控制点的原始位置与当前位置,并计算每个控制点的 delta = current - original - Vector2f[] secOrig = new Vector2f[secCount]; - Vector2f[] deltas = new Vector2f[secCount]; - boolean[] isPinned = new boolean[secCount]; - float[] controlRadiusSq = new float[secCount]; // 存储半径平方,避免重复开方 + // 收集合格控制点:p(original)来自 originalPosition,q(current)来自 position + java.util.List allP_list = new java.util.ArrayList<>(); + java.util.List allQ_list = new java.util.ArrayList<>(); + java.util.List isSelected_list = new java.util.ArrayList<>(); for (int i = 0; i < secCount; i++) { SecondaryVertex sv = secondaryVertices.get(i); - Vector2f secCurr = sv.getPosition(); - secOrig[i] = sv.getOriginalPosition(); - deltas[i] = new Vector2f(secCurr.x - secOrig[i].x, secCurr.y - secOrig[i].y); - isPinned[i] = sv.isPinned(); - controlRadiusSq[i] = sv.getControlRadius() * sv.getControlRadius(); // 预计算平方 + // 仅允许未锁定且未 pinned 的点参与变形(视为合格) + if (sv == null) continue; + if (sv.isLocked() || sv.isPinned()) continue; + + // 注意:getOriginalPosition() 和 getPosition() 返回新的 Vector2f 副本,不要修改它们 + allP_list.add(sv.getOriginalPosition()); + allQ_list.add(sv.getPosition()); + isSelected_list.add(sv.isSelected()); } - for (int i = 0; i < originalVertices.length; i += 2) { - float ox = originalVertices[i]; - float oy = originalVertices[i + 1]; + int eligibleCount = allP_list.size(); + if (eligibleCount == 0) return; - Vector2f finalDelta = null; + Vector2f[] allP = allP_list.toArray(new Vector2f[0]); + Vector2f[] allQ = allQ_list.toArray(new Vector2f[0]); - // --- 1) 优先检查 pinned 控制点(钉子) - // 找到距离最近且覆盖当前顶点的 Pinned 点 - int pinnedMatch = -1; - float bestPinnedDistSq = Float.MAX_VALUE; - for (int p = 0; p < secCount; p++) { - if (!isPinned[p]) continue; - float dx = ox - secOrig[p].x; - float dy = oy - secOrig[p].y; - float distSq = dx * dx + dy * dy; - if (distSq <= controlRadiusSq[p] && distSq < bestPinnedDistSq) { - pinnedMatch = p; - bestPinnedDistSq = distSq; - } + // 构造用户选区索引(基于当前位置 allQ):优先使用被选中的合格点 + java.util.List selIdx = new java.util.ArrayList<>(); + for (int i = 0; i < eligibleCount; i++) if (isSelected_list.get(i)) selIdx.add(i); + + boolean explicitSelection = !selIdx.isEmpty(); + boolean applyGlobal = !explicitSelection; + if (!explicitSelection) { + for (int i = 0; i < eligibleCount; i++) selIdx.add(i); + } + + // 用当前位置 allQ 构造去重后的 ptsQ 与索引映射 + java.util.List ptsQ = new java.util.ArrayList<>(); + java.util.List idxMap = new java.util.ArrayList<>(); + java.util.Set seen = new java.util.HashSet<>(); + for (int idx : selIdx) { + Vector2f cur = allQ[idx]; + String key = String.format("%.6f_%.6f", cur.x, cur.y); + if (seen.contains(key)) continue; + seen.add(key); + ptsQ.add(new Vector2f(cur.x, cur.y)); + idxMap.add(idx); + } + + // 若显式选择且点小于3,尽量扩充到可用合格点(基于当前位置) + if (explicitSelection && ptsQ.size() < 3) { + for (int i = 0; i < eligibleCount && ptsQ.size() < 3; i++) { + String key = String.format("%.6f_%.6f", allQ[i].x, allQ[i].y); + if (seen.contains(key)) continue; + seen.add(key); + ptsQ.add(new Vector2f(allQ[i].x, allQ[i].y)); + idxMap.add(i); } - if (pinnedMatch != -1) { - // 使用该 pinned 的位移,保证“钉子周围点被固定” - finalDelta = deltas[pinnedMatch]; - } else { - // --- 2) 尝试三角分配(最近 3 个控制点) - int[] nearest = findNearestNIndices(ox, oy, 3, secOrig); - if (nearest != null && nearest.length == 3) { - int ia = nearest[0], ib = nearest[1], ic = nearest[2]; - Vector2f A = secOrig[ia], B = secOrig[ib], C = secOrig[ic]; + } - // 检测三角形是否退化或点在内部 - // 注意:这里我们使用一个更严格的条件:点必须在三角形内部 - if (pointInTriangle(ox, oy, A, B, C)) { - // 面积计算用于判断退化和重心坐标 - float areaABC = (B.x - A.x) * (C.y - A.y) - (C.x - A.x) * (B.y - A.y); + // 构建 polygon(按极角排序),若自交则退到凸包(均基于当前位置) + java.util.List poly = new java.util.ArrayList<>(); + java.util.List polyIdx = new java.util.ArrayList<>(); + if (explicitSelection && ptsQ.size() >= 3) { + Vector2f cen = new Vector2f(0f, 0f); + for (Vector2f v : ptsQ) { cen.x += v.x; cen.y += v.y; } + cen.x /= ptsQ.size(); cen.y /= ptsQ.size(); - // 如果三角形面积足够大 - if (Math.abs(areaABC) >= 1e-6f) { - float[] bary = barycentricCoordinates(A, B, C, ox, oy); + java.util.List order = new java.util.ArrayList<>(); + for (int i = 0; i < ptsQ.size(); i++) order.add(i); + order.sort((a, b) -> { + double anga = Math.atan2(ptsQ.get(a).y - cen.y, ptsQ.get(a).x - cen.x); + double angb = Math.atan2(ptsQ.get(b).y - cen.y, ptsQ.get(b).x - cen.x); + return Double.compare(anga, angb); + }); - // 按重心系数混合控制点的 delta - float dx = bary[0] * deltas[ia].x + bary[1] * deltas[ib].x + bary[2] * deltas[ic].x; - float dy = bary[0] * deltas[ia].y + bary[1] * deltas[ib].y + bary[2] * deltas[ic].y; + for (int oi : order) { + poly.add(ptsQ.get(oi)); + polyIdx.add(idxMap.get(oi)); // 这些索引对应 allP/allQ 数组 + } - finalDelta = new Vector2f(dx, dy); + if (polygonIsSelfIntersecting(poly)) { + java.util.List hull = convexHull(ptsQ); + java.util.List hullIdx = new java.util.ArrayList<>(); + for (Vector2f hp : hull) { + int found = -1; + for (int i = 0; i < ptsQ.size(); i++) { + if (Math.abs(ptsQ.get(i).x - hp.x) < 1e-6f && Math.abs(ptsQ.get(i).y - hp.y) < 1e-6f) { + found = idxMap.get(i); + break; } } + hullIdx.add(found); } - - // --- 3) 回退到 IDW(基于 deltas) - if (finalDelta == null) { - finalDelta = computeIDWForPointUsingDeltas(ox, oy, secOrig, deltas); - // IDW 方法返回的是最终位置,需要减去原始位置以获得 delta - finalDelta.x -= ox; - finalDelta.y -= oy; - } - } - - // 应用最终的 delta - if (finalDelta != null) { - vertices[i] = ox + finalDelta.x; - vertices[i + 1] = oy + finalDelta.y; - } else { - // 如果所有方法都失败,保持原始位置 - vertices[i] = ox; - vertices[i + 1] = oy; + poly = hull; + polyIdx = hullIdx; } } - logger.debug("应用三角分配变形(Live2D风格的基于 delta 的插值与 pinned 修正),使用了 {} 个控制点", secondaryVertices.size()); + // 构造 MLS 控制点集合:p 必须来自 allP(original),q 来自 allQ(current) + java.util.List mlsPList = new java.util.ArrayList<>(); + java.util.List mlsQList = new java.util.ArrayList<>(); + if (applyGlobal) { + for (int i = 0; i < eligibleCount; i++) { + mlsPList.add(new Vector2f(allP[i].x, allP[i].y)); + mlsQList.add(new Vector2f(allQ[i].x, allQ[i].y)); + } + } else { + for (Integer id : polyIdx) { + if (id == null || id < 0) continue; + mlsPList.add(new Vector2f(allP[id].x, allP[id].y)); + mlsQList.add(new Vector2f(allQ[id].x, allQ[id].y)); + } + // 防护:若意外为空则退回全局 + if (mlsPList.isEmpty()) { + for (int i = 0; i < eligibleCount; i++) { + mlsPList.add(new Vector2f(allP[i].x, allP[i].y)); + mlsQList.add(new Vector2f(allQ[i].x, allQ[i].y)); + } + applyGlobal = true; + } + } + Vector2f[] p = mlsPList.toArray(new Vector2f[0]); + Vector2f[] q = mlsQList.toArray(new Vector2f[0]); + // 固定羽化宽度(可调) + final float feather = 20f; + + // 如果没有实际位移且是显式选择模式,则直接返回(避免把 rest 覆盖成 q) + boolean anyDelta = false; + for (int i = 0; i < Math.min(p.length, q.length); i++) { + float dx = q[i].x - p[i].x, dy = q[i].y - p[i].y; + if (Math.abs(dx) > 1e-5f || Math.abs(dy) > 1e-5f) { anyDelta = true; break; } + } + if (!anyDelta && !applyGlobal) return; + + // MLS 参数 + final float alpha = 1.0f; + final float eps = 1e-6f; + + // 主循环:遍历原始顶点并应用 MLS(或按羽化混合) + if (originalVertices == null || vertices == null) return; + int vertCount = originalVertices.length / 2; + if (vertCount <= 0) return; + + for (int vi = 0; vi < originalVertices.length; vi += 2) { + float ox = originalVertices[vi]; + float oy = originalVertices[vi + 1]; + + float blend = 1f; + if (!applyGlobal) { + boolean inside = pointInPolygonRayCast(ox, oy, poly); + blend = inside ? 1f : 0f; + if (!inside && feather > 1e-6f) { + float distToEdge = (float) pointToPolygonDistance(ox, oy, poly); + if (distToEdge <= feather) { + blend = 1f - (distToEdge / feather); + blend = Math.max(0f, Math.min(1f, blend)); + } + } + } + + if (blend <= 0f) { + vertices[vi] = ox; + vertices[vi + 1] = oy; + continue; + } + + int n = p.length; + if (n == 0) { + vertices[vi] = ox; + vertices[vi + 1] = oy; + continue; + } + + // 计算权重与加权质心 + float wSum = 0f; + float px = 0f, py = 0f, qx = 0f, qy = 0f; + float[] weights = new float[n]; + boolean directMapped = false; + for (int k = 0; k < n; k++) { + float dx = ox - p[k].x; + float dy = oy - p[k].y; + float distSq = dx * dx + dy * dy; + if (distSq < 1e-8f) { + vertices[vi] = q[k].x; + vertices[vi + 1] = q[k].y; + directMapped = true; + break; + } + float w = 1.0f / (float) Math.pow(distSq + eps, alpha); + weights[k] = w; + wSum += w; + px += w * p[k].x; + py += w * p[k].y; + qx += w * q[k].x; + qy += w * q[k].y; + } + + if (directMapped) { + if (blend < 1f) { + float dx = vertices[vi] - ox; + float dy = vertices[vi + 1] - oy; + vertices[vi] = ox + dx * blend; + vertices[vi + 1] = oy + dy * blend; + } + continue; + } + + if (wSum <= 0f) { + vertices[vi] = ox; + vertices[vi + 1] = oy; + continue; + } + + px /= wSum; py /= wSum; qx /= wSum; qy /= wSum; + + // mu + float mu = 0f; + for (int k = 0; k < n; k++) { + float wk = weights[k]; + if (wk == 0f) continue; + float rx = p[k].x - px, ry = p[k].y - py; + mu += wk * (rx * rx + ry * ry); + } + if (mu < eps) { + // 退化 -> 最近点回退 + int nearest = 0; float nd = Float.POSITIVE_INFINITY; + for (int k = 0; k < n; k++) { + float dx = ox - p[k].x; float dy = oy - p[k].y; + float d = dx * dx + dy * dy; + if (d < nd) { nd = d; nearest = k; } + } + float tx = q[nearest].x - p[nearest].x; + float ty = q[nearest].y - p[nearest].y; + float nx = ox + tx, ny = oy + ty; + vertices[vi] = ox * (1f - blend) + nx * blend; + vertices[vi + 1] = oy * (1f - blend) + ny * blend; + continue; + } + + // 计算 a,b 并应用相似变换 + float a = 0f, b = 0f; + for (int k = 0; k < n; k++) { + float wk = weights[k]; + if (wk == 0f) continue; + float pxk = p[k].x - px, pyk = p[k].y - py; + float qxk = q[k].x - qx, qyk = q[k].y - qy; + a += wk * (pxk * qxk + pyk * qyk); + b += wk * (pxk * qyk - pyk * qxk); + } + + float vxRel = ox - px, vyRel = oy - py; + float tx = (a * vxRel - b * vyRel) / mu; + float ty = (b * vxRel + a * vyRel) / mu; + float dx = qx + tx, dy = qy + ty; + + // 混合结果(羽化) + vertices[vi] = ox * (1f - blend) + dx * blend; + vertices[vi + 1] = oy * (1f - blend) + dy * blend; + } // end for vertices + + logger.debug("MLS 应用完成 (explicitSelection={}, applyGlobal={}), eligibleControl={}, mlsControl={}", + explicitSelection, applyGlobal, eligibleCount, p.length); } catch (Exception e) { - logger.error("三角分配变形失败,回退到反距离加权", e); - // 确保有一个 IDW 回退方法 (需要您实现 updateVerticesUsingInverseDistanceWeighting()) + logger.error("区域化 MLS 变形失败,回退 IDW", e); updateVerticesUsingInverseDistanceWeighting(); } } + + /* --------------------- 辅助函数:多边形/几何 --------------------- */ + + /** 射线法点在多边形内判定,poly 为顺序顶点(闭合不需要重复首点) */ + private boolean pointInPolygonRayCast(float x, float y, java.util.List poly) { + boolean inside = false; + int n = poly.size(); + for (int i = 0, j = n - 1; i < n; j = i++) { + Vector2f vi = poly.get(i); + Vector2f vj = poly.get(j); + boolean intersect = ((vi.y > y) != (vj.y > y)) && + (x < (vj.x - vi.x) * (y - vi.y) / (vj.y - vi.y + 1e-12f) + vi.x); + if (intersect) inside = !inside; + } + return inside; + } + + /** 计算点到多边形边界的最小距离(若 poly 为空返回 +inf) */ + private double pointToPolygonDistance(float x, float y, java.util.List poly) { + if (poly == null || poly.isEmpty()) return Double.POSITIVE_INFINITY; + double best = Double.POSITIVE_INFINITY; + int n = poly.size(); + for (int i = 0; i < n; i++) { + Vector2f a = poly.get(i); + Vector2f b = poly.get((i + 1) % n); + double dist = pointToSegmentDistance(x, y, a.x, a.y, b.x, b.y); + if (dist < best) best = dist; + } + return best; + } + + private double pointToSegmentDistance(double px, double py, double x1, double y1, double x2, double y2) { + double vx = x2 - x1, vy = y2 - y1; + double wx = px - x1, wy = py - y1; + double c1 = vx * wx + vy * wy; + if (c1 <= 0) return Math.hypot(px - x1, py - y1); + double c2 = vx * vx + vy * vy; + if (c2 <= c1) return Math.hypot(px - x2, py - y2); + double t = c1 / c2; + double projx = x1 + t * vx; + double projy = y1 + t * vy; + return Math.hypot(px - projx, py - projy); + } + + /** 判断多边形是否自交(朴素 O(n^2) 检查边相交) */ + private boolean polygonIsSelfIntersecting(java.util.List poly) { + int n = poly.size(); + if (n < 4) return false; + for (int i = 0; i < n; i++) { + Vector2f a1 = poly.get(i); + Vector2f a2 = poly.get((i + 1) % n); + for (int j = i + 1; j < n; j++) { + // skip adjacent edges + if (Math.abs(i - j) <= 1 || (i == 0 && j == n - 1) || (j == 0 && i == n - 1)) continue; + Vector2f b1 = poly.get(j); + Vector2f b2 = poly.get((j + 1) % n); + if (segmentsIntersect(a1.x, a1.y, a2.x, a2.y, b1.x, b1.y, b2.x, b2.y)) return true; + } + } + return false; + } + + /** 线段相交测试(闭包含端点) */ + private boolean segmentsIntersect(double x1, double y1, double x2, double y2, + double x3, double y3, double x4, double y4) { + // 快速边界排除 + if (Math.max(x1,x2) < Math.min(x3,x4) || Math.max(x3,x4) < Math.min(x1,x2) || + Math.max(y1,y2) < Math.min(y3,y4) || Math.max(y3,y4) < Math.min(y1,y2)) + return false; + double d1 = orient(x3,y3,x4,y4,x1,y1); + double d2 = orient(x3,y3,x4,y4,x2,y2); + double d3 = orient(x1,y1,x2,y2,x3,y3); + double d4 = orient(x1,y1,x2,y2,x4,y4); + if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) + return true; + if (Math.abs(d1) < 1e-10 && onSegment(x3,y3,x4,y4,x1,y1)) return true; + if (Math.abs(d2) < 1e-10 && onSegment(x3,y3,x4,y4,x2,y2)) return true; + if (Math.abs(d3) < 1e-10 && onSegment(x1,y1,x2,y2,x3,y3)) return true; + if (Math.abs(d4) < 1e-10 && onSegment(x1,y1,x2,y2,x4,y4)) return true; + return false; + } + private double orient(double ax,double ay,double bx,double by,double cx,double cy) { + return (bx-ax)*(cy-ay) - (by-ay)*(cx-ax); + } + private boolean onSegment(double ax,double ay,double bx,double by,double px,double py) { + return Math.min(ax,bx)-1e-8 <= px && px <= Math.max(ax,bx)+1e-8 && + Math.min(ay,by)-1e-8 <= py && py <= Math.max(ay,by)+1e-8; + } + + /** 凸包(Monotone chain),返回逆时针 hull 顶点 */ + private java.util.List convexHull(java.util.List points) { + java.util.List pts = new java.util.ArrayList<>(points); + pts.sort((a,b) -> { + if (a.x == b.x) return Float.compare(a.y,b.y); + return Float.compare(a.x,b.x); + }); + java.util.List lower = new java.util.ArrayList<>(); + for (Vector2f p : pts) { + while (lower.size() >= 2) { + Vector2f p1 = lower.get(lower.size()-2), p2 = lower.get(lower.size()-1); + if (orient(p1.x,p1.y,p2.x,p2.y,p.x,p.y) <= 0) lower.remove(lower.size()-1); + else break; + } + lower.add(p); + } + java.util.List upper = new java.util.ArrayList<>(); + for (int i = pts.size()-1; i >= 0; i--) { + Vector2f p = pts.get(i); + while (upper.size() >= 2) { + Vector2f p1 = upper.get(upper.size()-2), p2 = upper.get(upper.size()-1); + if (orient(p1.x,p1.y,p2.x,p2.y,p.x,p.y) <= 0) upper.remove(upper.size()-1); + else break; + } + upper.add(p); + } + lower.remove(lower.size()-1); + upper.remove(upper.size()-1); + lower.addAll(upper); + return lower; + } + /** * 基于反距离加权(IDW)但对“位移 delta”加权计算结果。 * 输入为控制点原始位置 secOrig 和每点的 delta(current - original)。 @@ -1592,189 +1748,6 @@ public class Mesh2D { } } - - /** - * 在给定的控制点数组中,返回距离 (x,y) 最近的 N 个索引(按距离升序) - * 如果可用控制点少于 n,返回实际找到的索引数组。 - */ - private int[] findNearestNIndices(float x, float y, int n, Vector2f[] controlOrig) { - int secCount = controlOrig.length; - // 先收集所有在自己 controlRadius 内的点 - float[] dists = new float[secCount]; - for (int i = 0; i < secCount; i++) { - float dx = x - controlOrig[i].x; - float dy = y - controlOrig[i].y; - dists[i] = dx * dx + dy * dy; - } - - // 首先把满足 controlRadius 条件的点挑出来(并按距离排序) - java.util.List inRange = new java.util.ArrayList<>(); - java.util.List others = new java.util.ArrayList<>(); - for (int i = 0; i < secCount; i++) { - float r = secondaryVertices.get(i).getControlRadius(); - if (dists[i] <= r * r) inRange.add(i); - else others.add(i); - } - - // 排序辅助 - java.util.Comparator comp = (a, b) -> Float.compare(dists[a], dists[b]); - inRange.sort(comp); - others.sort(comp); - - java.util.List chosen = new java.util.ArrayList<>(); - for (int idx : inRange) { - if (chosen.size() >= n) break; - chosen.add(idx); - } - for (int idx : others) { - if (chosen.size() >= n) break; - chosen.add(idx); - } - - int[] result = new int[chosen.size()]; - for (int i = 0; i < chosen.size(); i++) result[i] = chosen.get(i); - return result; - } - - /** - * 检查点 (px,py) 是否位于由 A,B,C 三点形成的三角形内部(含边界) - * 使用重心 / 符号面积法 - */ - private boolean pointInTriangle(float px, float py, Vector2f A, Vector2f B, Vector2f C) { - float ax = A.x, ay = A.y; - float bx = B.x, by = B.y; - float cx = C.x, cy = C.y; - - float v0x = cx - ax, v0y = cy - ay; - float v1x = bx - ax, v1y = by - ay; - float v2x = px - ax, v2y = py - ay; - - float dot00 = v0x * v0x + v0y * v0y; - float dot01 = v0x * v1x + v0y * v1y; - float dot02 = v0x * v2x + v0y * v2y; - float dot11 = v1x * v1x + v1y * v1y; - float dot12 = v1x * v2x + v1y * v2y; - - float denom = (dot00 * dot11 - dot01 * dot01); - if (Math.abs(denom) < 1e-8f) return false; // 退化三角形 - - float invDenom = 1.0f / denom; - float u = (dot11 * dot02 - dot01 * dot12) * invDenom; - float v = (dot00 * dot12 - dot01 * dot02) * invDenom; - - // 在三角形内或边上时 u>=0, v>=0, u+v<=1 - return u >= -1e-6f && v >= -1e-6f && (u + v) <= 1.000001f; - } - - /** - * 计算点在三角形 ABC 的重心坐标(返回长度为3的数组 [wA, wB, wC]) - * 对退化情况不做保护,调用方应先检测面积。 - */ - private float[] barycentricCoordinates(Vector2f A, Vector2f B, Vector2f C, float px, float py) { - float x1 = A.x, y1 = A.y; - float x2 = B.x, y2 = B.y; - float x3 = C.x, y3 = C.y; - - float denom = (y2 - y3) * (x1 - x3) + (x3 - x2) * (y1 - y3); - if (Math.abs(denom) < 1e-8f) { - // 退化时返回把权重全部交给最近的顶点(Fallback) - float da = (px - x1) * (px - x1) + (py - y1) * (py - y1); - float db = (px - x2) * (px - x2) + (py - y2) * (py - y2); - float dc = (px - x3) * (px - x3) + (py - y3) * (py - y3); - if (da <= db && da <= dc) return new float[]{1f, 0f, 0f}; - if (db <= da && db <= dc) return new float[]{0f, 1f, 0f}; - return new float[]{0f, 0f, 1f}; - } - float w1 = ((y2 - y3) * (px - x3) + (x3 - x2) * (py - y3)) / denom; - float w2 = ((y3 - y1) * (px - x3) + (x1 - x3) * (py - y3)) / denom; - float w3 = 1.0f - w1 - w2; - return new float[]{w1, w2, w3}; - } - - /** - * 使用稳定的双线性插值更新顶点 - */ - 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; - } - /** * 使用反距离加权插值(适用于任意数量的控制点) */ diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/RegionOptimizer.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/RegionOptimizer.java deleted file mode 100644 index 660bf61..0000000 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/RegionOptimizer.java +++ /dev/null @@ -1,191 +0,0 @@ -package com.chuangzhou.vivid2D.render.model.util; - -import org.joml.Vector2f; - -import java.util.List; - -/** - * RegionOptimizer - * - 处理当新点靠近已有点时的 controlRadius 重新分配(避免两个点控制区完全相同且重叠) - * - 提供针对特征区域(例如嘴巴、尾巴)的简单优化算法接口 - *

- * 算法思想(简述): - * - 当新点插入或靠近已有点时,对两点及其邻域进行局部半径重分配,保证半径不相等且满足最小/最大约束。 - * - 对于特征(MOUTH/TAIL),使用基于位置的权重缩放半径(例如嘴巴中间半径较小以保证细节,边缘半径较大) - */ -public class RegionOptimizer { - - public enum FeatureType { MOUTH, TAIL, OTHER } - - // 新点插入时处理(调用 resolveNewAndNeighbor 或 resolveForInsertedVertex) - public static void resolveForInsertedVertex(SecondaryVertex newV, List all) { - // 与最近一个点进行冲突检测并调整 - SecondaryVertex nearest = findNearest(newV.getPosition().x, newV.getPosition().y, all, newV); - if (nearest != null) { - resolveNewAndNeighbor(newV, nearest, all); - } else { - // 无邻点,只需保证半径在范围内 - clampRadius(newV); - } - } - - /** - * 当新点 A 想进入 B 的控制区时:对 A、B 以及两者周围若干点做局部重分配 - * 目标:避免 A/B 的 controlRadius 完全相同或产生不可分配的覆盖(保证每个点都有独立控制区) - */ - public static void resolveNewAndNeighbor(SecondaryVertex A, SecondaryVertex B, List all) { - if (A == null || B == null) return; - - // 如果任一为 fixedRadius,则优先尊重 fixed,另一方调整 - if (A.isFixedRadius() && B.isFixedRadius()) { - // 都固定:不允许完全重合,若重合则微调 A 的半径少量 - if (Math.abs(A.getControlRadius() - B.getControlRadius()) < 1e-3f) { - if (!A.isFixedRadius()) { - A.setControlRadius(A.getControlRadius() * 0.95f + 0.1f); - } else if (!B.isFixedRadius()) { - B.setControlRadius(B.getControlRadius() * 0.95f + 0.1f); - } else { - // 两个都固定且相等,强制对 A 做微小扰动(尽量不破坏 fixed 标记) - A.setControlRadius(A.getControlRadius() + 0.5f); - } - } - return; - } - - // 否则对两者进行比例缩放:较远的一方保持或略增,靠近的一方减小 - float dist = A.getPosition().distance(B.getPosition()); - float sum = A.getControlRadius() + B.getControlRadius(); - // 如果重叠(距离 < sum),则调整半径 - if (dist < sum) { - // 按距离比重分配空间(保持最小阈值) - float minR = Math.min(Math.max(A.getMinControlRadius(), B.getMinControlRadius()), 4.0f); - - // 计算比例(避免完全相等) - float aPref = Math.max(minR, (A.getControlRadius() * (dist / (sum + 1e-6f))) * 0.9f); - float bPref = Math.max(minR, (B.getControlRadius() * (dist / (sum + 1e-6f))) * 0.9f); - - // 防止 aPref == bPref - if (Math.abs(aPref - bPref) < 1e-2f) { - aPref *= 0.92f; - bPref *= 1.08f; - } - - if (!A.isFixedRadius()) A.setControlRadius(aPref); - if (!B.isFixedRadius()) B.setControlRadius(bPref); - } else { - // 无重叠时,微调避免完全相等 - if (!A.isFixedRadius() && !B.isFixedRadius() && Math.abs(A.getControlRadius() - B.getControlRadius()) < 1e-3f) { - A.setControlRadius(A.getControlRadius() * 0.95f + 0.1f); - } - } - - // 可选:对二者邻域做平滑(简单缓和) - smoothNeighborhood(A, all, 2); - smoothNeighborhood(B, all, 2); - } - - /** - * 移动后调整邻域(简单的重分配与平滑) - */ - public static void adjustRegionsAfterMove(SecondaryVertex moved, List all) { - if (moved == null) return; - // 对周围一定范围内的点进行重平衡,防止刚好重叠或半径完全一致 - for (SecondaryVertex other : all) { - if (other == moved) continue; - float d = moved.getPosition().distance(other.getPosition()); - float influence = moved.getControlRadius() + other.getControlRadius(); - if (d < influence * 1.15f) { - // 近邻则 resolve pair - resolveNewAndNeighbor(moved, other, all); - } - } - } - - /** - * 对特征区域(如嘴巴/尾巴)进行优化:这里给出简单策略, - * 真实项目可替换为更复杂的曲线导向分配(例如沿曲线做非均匀采样、Laplacian 平滑等) - */ - public static void optimizeFeatureRegion(List featureVerts, FeatureType type) { - if (featureVerts == null || featureVerts.isEmpty()) return; - - // 计算质心 - Vector2f center = new Vector2f(0,0); - for (SecondaryVertex v : featureVerts) center.add(v.getPosition()); - center.div(featureVerts.size()); - - // 基于距离做半径缩放:靠近中心半径较小,远离中心半径较大(嘴巴中间精细) - float maxRadius = 0f; - for (SecondaryVertex v : featureVerts) maxRadius = Math.max(maxRadius, v.getControlRadius()); - - for (SecondaryVertex v : featureVerts) { - float d = v.getPosition().distance(center); - // 归一化距离 - float maxD = 1e-6f; - for (SecondaryVertex vv : featureVerts) maxD = Math.max(maxD, vv.getPosition().distance(center)); - float norm = (maxD > 1e-6f) ? d / maxD : 0f; - - if (type == FeatureType.MOUTH) { - // 嘴巴:中心小半径、边缘稍大 - float target = Math.max(v.getMinControlRadius(), maxRadius * (0.5f + 0.7f * norm)); - if (!v.isFixedRadius()) v.setControlRadius(target); - } else if (type == FeatureType.TAIL) { - // 尾巴:从基部到末端半径逐渐减小(假设 featureVerts 顺序已沿尾巴方向) - int idx = featureVerts.indexOf(v); - float t = (float) idx / (featureVerts.size() - 1.0f); - float target = Math.max(v.getMinControlRadius(), maxRadius * (1.0f - 0.7f * t)); - if (!v.isFixedRadius()) v.setControlRadius(target); - } else { - // 默认平滑:靠近中心略小 - float target = Math.max(v.getMinControlRadius(), maxRadius * (0.6f + 0.4f * norm)); - if (!v.isFixedRadius()) v.setControlRadius(target); - } - } - - // 最后做一次局部平滑避免跳变 - for (SecondaryVertex v : featureVerts) smoothNeighborhood(v, featureVerts, 1); - } - - // ----------------- 辅助方法 ----------------- - - private static SecondaryVertex findNearest(float x, float y, List all, SecondaryVertex exclude) { - SecondaryVertex best = null; - float bestD = Float.POSITIVE_INFINITY; - for (SecondaryVertex v : all) { - if (v == exclude) continue; - float dx = x - v.getPosition().x; - float dy = y - v.getPosition().y; - float d2 = dx*dx + dy*dy; - if (d2 < bestD) { - bestD = d2; - best = v; - } - } - return best; - } - - private static void clampRadius(SecondaryVertex v) { - if (v == null) return; - v.setControlRadius(v.getControlRadius()); // 利用 setter 做 clamp - } - - // 对邻域做简单平滑:把邻居半径平均到当前点附近(radiusNeighbors = k hop) - private static void smoothNeighborhood(SecondaryVertex v, List all, int radiusNeighbors) { - if (v == null || all == null) return; - // 取最近 couple 个(这里用固定 4 个邻居作为平滑范围) - java.util.List neighbors = new java.util.ArrayList<>(); - for (SecondaryVertex other : all) { - if (other == v) continue; - neighbors.add(other); - } - neighbors.sort((a,b) -> Float.compare(a.getPosition().distance(v.getPosition()), b.getPosition().distance(v.getPosition()))); - int k = Math.min(4, neighbors.size()); - float sum = v.getControlRadius(); - int cnt = 1; - for (int i = 0; i < k; i++) { - sum += neighbors.get(i).getControlRadius(); - cnt++; - } - float avg = sum / cnt; - if (!v.isFixedRadius()) v.setControlRadius( (v.getControlRadius() * 0.6f) + (avg * 0.4f) ); - } -} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/SecondaryVertex.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/SecondaryVertex.java index ac06df8..444a75c 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/SecondaryVertex.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/SecondaryVertex.java @@ -2,90 +2,120 @@ package com.chuangzhou.vivid2D.render.model.util; import org.joml.Vector2f; -import java.util.Objects; +import java.util.ArrayList; +import java.util.List; -/** - * SecondaryVertex 增加 pinned/locked 支持 - */ public class SecondaryVertex { - Vector2f position; - Vector2f originalPosition; - Vector2f uv; - boolean selected = false; - int id; - static int nextId = 0; - // 新增状态 - boolean pinned = false; // 可以被……当作拖动整块) - boolean locked = false; // 锁定(不能移动) - private float controlRadius = 20.0f; // 控制区域半径(单位与你的坐标系一致),默认值可调整 - private float minControlRadius = 4.0f; // 最小允许半径 - private float maxControlRadius = 200.0f; // 最大允许半径 - private boolean fixedRadius = false; // 是否锁定半径(固定区域) - transient Vector2f worldPosition = new Vector2f(); + private final Vector2f position; + private final Vector2f originalPosition; + private final Vector2f uv; - // 【新增字段】用于存储渲染时的世界坐标,通常由 ModelPart 的世界变换计算而来 - transient Vector2f renderPosition = new Vector2f(); + private int id; + private static int nextId = 0; + private boolean selected = false; - 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++; + private final transient Vector2f worldPosition = new Vector2f(); + private final transient Vector2f renderPosition = new Vector2f(); + + private final ControlShape controlShape; + + public void setId(int id) { + this.id = id; } - 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 getWorldPosition() { - return new Vector2f(worldPosition); - } - - public void setWorldPosition(float x, float y) { - this.worldPosition.set(x, y); - } - - public void setWorldPosition(Vector2f p) { - if (p == null) return; - this.worldPosition.set(p); - } - - // 【新增 Getter】 - public Vector2f getRenderPosition() { - return new Vector2f(renderPosition); - } - - // 【新增 Setter】 - public void setRenderPosition(float x, float y) { - this.renderPosition.set(x, y); - } - - // 【新增 Setter】 - public void setRenderPosition(Vector2f p) { - if (p == null) return; - this.renderPosition.set(p); - } - - public Vector2f getUV() { - return new Vector2f(uv); + public void setSelected(boolean b) { + this.selected = b; } public boolean isSelected() { return selected; } - public int getId() { - return id; + public static class ControlShape { + private final int shapeId; + private static int nextShapeId = 0; + private boolean pinned = false; + private boolean locked = false; + private final Vector2f minControlPoint = new Vector2f(0, 0); + private final Vector2f maxControlPoint = new Vector2f(0, 0); + private final List controlVertices = new ArrayList<>(); + + public ControlShape() { + this.shapeId = nextShapeId++; + } + + public int getShapeId() { + return shapeId; + } + + public boolean isPinned() { + return pinned; + } + + public void setPinned(boolean pinned) { + this.pinned = pinned; + } + + public boolean isLocked() { + return locked; + } + + public void setLocked(boolean locked) { + this.locked = locked; + } + + public Vector2f getMinControlPoint() { + return new Vector2f(minControlPoint); + } + + public void setMinControlPoint(Vector2f minControlPoint) { + this.minControlPoint.set(minControlPoint); + } + + public Vector2f getMaxControlPoint() { + return new Vector2f(maxControlPoint); + } + + public void setMaxControlPoint(Vector2f maxControlPoint) { + this.maxControlPoint.set(maxControlPoint); + } + + public List getControlVertices() { + return controlVertices; + } + + public void addControlVertex(SecondaryVertex vertex) { + if (vertex != null && !controlVertices.contains(vertex)) { + controlVertices.add(vertex); + } + } + + public void removeControlVertex(SecondaryVertex vertex) { + controlVertices.remove(vertex); + } + + public void clearControlVertices() { + controlVertices.clear(); + } + + @Override + public String toString() { + return String.format("ControlShape{id=%d, pinned=%s, locked=%s, min=(%.2f, %.2f), max=(%.2f, %.2f), vertCount=%d}", + shapeId, pinned, locked, minControlPoint.x, minControlPoint.y, maxControlPoint.x, maxControlPoint.y, controlVertices.size()); + } + } + + 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++; + this.controlShape = new ControlShape(); + } + + public Vector2f getPosition() { + return new Vector2f(position); } public void setPosition(float x, float y) { @@ -96,22 +126,18 @@ public class SecondaryVertex { this.position.set(position); } - public void setId(int id) { - this.id = id; + public void move(float dx, float dy) { + this.position.add(dx, dy); + } + + public Vector2f getOriginalPosition() { + return new Vector2f(originalPosition); } 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); } @@ -120,61 +146,55 @@ public class SecondaryVertex { this.originalPosition.set(position); } - public void move(float dx, float dy) { - this.position.add(dx, dy); + + public Vector2f getUV() { + return new Vector2f(uv); + } + + public void setUV(float u, float v) { + this.uv.set(u, v); + } + + public void setWorldPosition(Vector2f p) { + if (p == null) return; + this.worldPosition.set(p); + } + + public Vector2f getRenderPosition() { + return new Vector2f(renderPosition); + } + + public void setRenderPosition(float x, float y) { + this.renderPosition.set(x, y); + } + + public void setRenderPosition(Vector2f p) { + if (p == null) return; + this.renderPosition.set(p); + } + + public int getId() { + return id; } - // 新增: pinned / locked public boolean isPinned() { - return pinned; + return controlShape.isPinned(); } public void setPinned(boolean pinned) { - this.pinned = pinned; + this.controlShape.setPinned(pinned); } public boolean isLocked() { - return locked; + return controlShape.isLocked(); } public void setLocked(boolean locked) { - this.locked = locked; + this.controlShape.setLocked(locked); } - public float getControlRadius() { - return controlRadius; - } - - public void setControlRadius(float controlRadius) { - // 如果固定则不允许修改 - if (this.fixedRadius) return; - this.controlRadius = Math.max(minControlRadius, Math.min(maxControlRadius, controlRadius)); - } - - public float getMinControlRadius() { - return minControlRadius; - } - - public void setMinControlRadius(float minControlRadius) { - this.minControlRadius = Math.max(0f, minControlRadius); - if (this.controlRadius < this.minControlRadius) this.controlRadius = this.minControlRadius; - } - - public float getMaxControlRadius() { - return maxControlRadius; - } - - public void setMaxControlRadius(float maxControlRadius) { - this.maxControlRadius = Math.max(this.minControlRadius, maxControlRadius); - if (this.controlRadius > this.maxControlRadius) this.controlRadius = this.maxControlRadius; - } - - public boolean isFixedRadius() { - return fixedRadius; - } - - public void setFixedRadius(boolean fixedRadius) { - this.fixedRadius = fixedRadius; + public ControlShape getControlShape() { + return controlShape; } @Override @@ -187,12 +207,16 @@ public class SecondaryVertex { @Override public int hashCode() { - return Objects.hash(id); + return id; } @Override public String toString() { - return String.format("SecondaryVertex{id=%d, position=(%.2f, %.2f), uv=(%.2f, %.2f), pinned=%s, locked=%s}", - id, position.x, position.y, uv.x, uv.y, pinned, locked); + return String.format("SecondaryVertex{id=%d, position=(%.2f, %.2f), uv=(%.2f, %.2f), pinned=%s, locked=%s, shape=%s}", + id, position.x, position.y, uv.x, uv.y, isPinned(), isLocked(), controlShape.toString()); + } + + public static void resetNextId(int newNextId) { + nextId = newNextId; } } \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/tools/VertexDeformationRander.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/tools/VertexDeformationRander.java index ca7f478..ce75774 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/tools/VertexDeformationRander.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/tools/VertexDeformationRander.java @@ -14,17 +14,14 @@ import org.joml.Vector2f; import org.joml.Vector4f; import org.lwjgl.opengl.GL11; +import java.util.Comparator; import java.util.Map; +import java.util.*; -/** - * 改进:使二级顶点渲染更现代化(圆形渐变点、阴影、高光),并在选中时显示影响范围(半透明环) - * 目标风格:类似 Live2D 编辑器里点和影响范围的视觉呈现 - */ public class VertexDeformationRander extends RanderTools { @Override public void init(Map algorithmEnabled) { algorithmEnabled.put("showSecondaryVertices", false); - // 可选项:是否显示影响范围 algorithmEnabled.put("showSecondaryVertexInfluence", true); } @@ -37,99 +34,323 @@ public class VertexDeformationRander extends RanderTools { return false; } - /** - * 绘制二级顶点(现代化样式) - */ private void drawSecondaryVertices(Mesh2D mesh2D, Matrix3f modelMatrix) { if (!isAlgorithmEnabled("showSecondaryVertices") || mesh2D.getSecondaryVertices().isEmpty() || !mesh2D.isShowSecondaryVertices()) return; - RenderSystem.pushState(); try { + ShaderProgram solidShader = ShaderManagement.getShaderProgram("Solid Color Shader"); if (solidShader != null && solidShader.programId != 0) { solidShader.use(); - - // 设置模型矩阵(如果 shader 支持) int modelLoc = solidShader.getUniformLocation("uModelMatrix"); - if (modelLoc != -1) { - RenderSystem.uniformMatrix3(modelLoc, modelMatrix); - } + if (modelLoc != -1) RenderSystem.uniformMatrix3(modelLoc, modelMatrix); } - RenderSystem.enableBlend(); RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - - // 启用线段平滑(如果驱动支持) GL11.glEnable(GL11.GL_LINE_SMOOTH); GL11.glHint(GL11.GL_LINE_SMOOTH_HINT, GL11.GL_NICEST); - Tesselator t = Tesselator.getInstance(); BufferBuilder bb = t.getBuilder(); + java.util.List verts = mesh2D.getSecondaryVertices(); - // 1) 先绘制细线连接所有控制点(在点之后绘制也可以,这里画在下层) - drawConnectionLines(bb, mesh2D); - - // 2) 为每个点绘制局部三角分配(用两最近邻构成三角形)并填充半透明三角形,表示该点的控制区域示意 - for (SecondaryVertex vertex : mesh2D.getSecondaryVertices()) { - drawLocalTriangle(bb, vertex, mesh2D); + // 1) 绘制所有控制点连线(按存储顺序),但使用 LINE_LOOP 以保证闭合(如果需要闭合) + if (verts.size() >= 2) { + // 如果顶点存储顺序不能保证多边形闭合,仍能作为参考线显示 + bb.begin(GL11.GL_LINE_STRIP, verts.size()); + bb.setColor(new Vector4f(1f, 1f, 1f, 0.12f)); + for (SecondaryVertex v : verts) { + Vector2f p = v.getPosition(); + bb.vertex(p.x, p.y, 0f, 0f); + } + bb.endImmediate(); } - // 3) 绘制所有二级顶点(点本体 + 高光 + 边框 + pin/lock 标识 + 编号) - for (SecondaryVertex vertex : mesh2D.getSecondaryVertices()) { - Vector2f position = vertex.getPosition(); - Vector4f baseColor = vertex.isSelected() ? mesh2D.selectedSecondaryVertexColor : mesh2D.secondaryVertexColor; - float size = mesh2D.secondaryVertexSize; + // 2) 收集被选中的点(以及可能的 preview),并构建一个**按极角排序的简单多边形** + java.util.List selPts = new java.util.ArrayList<>(); + for (SecondaryVertex sv : verts) if (sv.isSelected()) selPts.add(sv.getPosition()); + Vector2f preview = mesh2D.getPreviewPoint(); + if ((selPts.isEmpty() || selPts.size() < 3) && preview != null) { + // 当选中点少于3时,把 preview 当作临时点 + selPts.add(preview); + } - // 阴影(轻微偏移) - drawCircleSolid(bb, position.x + 2f, position.y - 2f, size * 0.8f, new Vector4f(0f, 0f, 0f, 0.22f), 20); - - // 如果开启显示影响范围且顶点被选中,则绘制半透明环表示影响范围(使用 controlRadius) - if (vertex.isSelected() && isAlgorithmEnabled("showSecondaryVertexInfluence")) { - float influenceRadius = vertex.getControlRadius(); // 使用 SecondaryVertex 的 controlRadius - drawInfluenceRing(bb, position.x, position.y, influenceRadius, baseColor); - } - - // 圆形渐变主点(中心较亮、边缘柔化) - Vector4f centerCol = new Vector4f(baseColor.x, baseColor.y, baseColor.z, Math.min(1.0f, baseColor.w + 0.15f)); - Vector4f outerCol = new Vector4f(baseColor.x, baseColor.y, baseColor.z, baseColor.w * 0.9f); - drawCircleGradient(bb, position.x, position.y, size * 0.9f, centerCol, outerCol, 28); - - // 内部高光(小白点) - drawCircleSolid(bb, position.x - size * 0.12f, position.y + size * 0.12f, size * 0.22f, - new Vector4f(1f, 1f, 1f, 0.75f), 12); - - // 边框(细) - drawCircleOutline(bb, position.x, position.y, size * 0.95f, new Vector4f(1f, 1f, 1f, 0.9f), 28); - - // 绘制 pin / lock 图标(在点旁边) - drawPinLockIcon(bb, vertex, position.x, position.y, size); - - Vector2f preview = mesh2D.getPreviewPoint(); - if (preview != null) { - // 使用 mesh2D 提供的预测方法(临时点半径使用默认 secondaryVertexSize*2) - float[] predicted = mesh2D.predictVerticesWithTemporarySecondary(preview, mesh2D.secondaryVertexSize * 3.0f); - if (predicted != null) { - drawPredictedOutline(bb, predicted, mesh2D); + if (!selPts.isEmpty()) { + // 去重 + java.util.Set seen = new java.util.HashSet<>(); + java.util.List uniq = new java.util.ArrayList<>(); + for (Vector2f v : selPts) { + String k = String.format("%.6f_%.6f", v.x, v.y); + if (!seen.contains(k)) { + seen.add(k); + uniq.add(new Vector2f(v.x, v.y)); } } - // 为选中的顶点绘制编号(更多现代化:带阴影的半透明小标签) - if (vertex.isSelected()) { - drawVertexId(mesh2D, bb, vertex.getId(), position.x, position.y, size); + // 生成一个按极角排序的多边形(相对于重心) + java.util.List poly = buildOrderedPolygon(uniq); + + // 如果自交则替换为凸包(保证无自交) + if (polygonIsSelfIntersecting(poly)) { + poly = convexHull(poly); + } + + // 需要至少3点才能绘制填充多边形 + if (poly.size() >= 3) { + drawSelectionPolygon(bb, poly); + } else if (poly.size() == 2) { + // 两点时绘制一条闭合的短线(可视化) + bb.begin(GL11.GL_LINES, 2); + bb.setColor(new Vector4f(0.95f, 0.6f, 0.15f, 0.28f)); + bb.vertex(poly.get(0).x, poly.get(0).y, 0f, 0f); + bb.vertex(poly.get(1).x, poly.get(1).y, 0f, 0f); + bb.endImmediate(); + } else if (poly.size() == 1) { + // 单点:画一个小圆做提示(在 draw loop 后会再次画点) } } + // 3) 如果启用影响范围显示,显示 controlShape 的包围盒(针对被选中的顶点) + if (isAlgorithmEnabled("showSecondaryVertexInfluence")) { + for (SecondaryVertex vertex : verts) { + if (vertex.isSelected()) { + Vector2f min = vertex.getControlShape().getMinControlPoint(); + Vector2f max = vertex.getControlShape().getMaxControlPoint(); + Vector4f baseColor = mesh2D.selectedSecondaryVertexColor; + if (max.x > min.x || max.y > min.y) { + drawInfluenceBox(bb, min, max, baseColor); + } + } + } + } + + // 4) 绘制二级顶点的点样式(阴影、渐变、高光、边框、pin/lock、编号) + for (SecondaryVertex vertex : verts) { + Vector2f position = vertex.getPosition(); + Vector4f baseColor = vertex.isSelected() ? mesh2D.selectedSecondaryVertexColor : mesh2D.secondaryVertexColor; + float size = mesh2D.secondaryVertexSize; + drawCircleSolid(bb, position.x + 2f, position.y - 2f, size * 0.8f, new Vector4f(0f, 0f, 0f, 0.22f), 20); + Vector4f centerCol = new Vector4f(baseColor.x, baseColor.y, baseColor.z, Math.min(1.0f, baseColor.w + 0.15f)); + Vector4f outerCol = new Vector4f(baseColor.x, baseColor.y, baseColor.z, baseColor.w * 0.9f); + drawCircleGradient(bb, position.x, position.y, size * 0.9f, centerCol, outerCol, 28); + drawCircleSolid(bb, position.x - size * 0.12f, position.y + size * 0.12f, size * 0.22f, + new Vector4f(1f, 1f, 1f, 0.75f), 12); + drawCircleOutline(bb, position.x, position.y, size * 0.95f, new Vector4f(1f, 1f, 1f, 0.9f), 28); + drawPinLockIcon(bb, vertex, position.x, position.y, size); + if (vertex.isSelected()) drawVertexId(mesh2D, bb, vertex.getId(), position.x, position.y, size); + } + } finally { - // 恢复状态 GL11.glDisable(GL11.GL_LINE_SMOOTH); RenderSystem.popState(); } } + /** + * 根据输入点(任意顺序),生成一个“简单多边形”顺序: + * - 先计算重心 + * - 按相对于重心的 atan2 排序(极角排序) + * 该方法不会尝试消除自交,调用方可以在需要时再用 polygonIsSelfIntersecting() 检查并 fallback 到 convexHull() + */ + private java.util.List buildOrderedPolygon(java.util.List pts) { + java.util.List out = new ArrayList<>(); + if (pts == null || pts.isEmpty()) return out; + // 去重 + java.util.Set seen = new java.util.HashSet<>(); + java.util.List uniq = new ArrayList<>(); + for (Vector2f p : pts) { + String k = String.format("%.6f_%.6f", p.x, p.y); + if (!seen.contains(k)) { seen.add(k); uniq.add(new Vector2f(p.x, p.y)); } + } + if (uniq.size() <= 1) return new ArrayList<>(uniq); + + // 计算重心 + Vector2f cen = new Vector2f(0f, 0f); + for (Vector2f v : uniq) { cen.x += v.x; cen.y += v.y; } + cen.x /= uniq.size(); cen.y /= uniq.size(); + + // 按角度排序 + uniq.sort(Comparator.comparingDouble(a -> Math.atan2(a.y - cen.y, a.x - cen.x))); + + // 如果排序后首尾非常接近但方向不一致,尝试修正:确保多边形非自交的简单方式是返回它并由调用方处理 + return uniq; + } + + private boolean polygonIsSelfIntersecting(java.util.List poly) { + if (poly == null) return false; + int n = poly.size(); + if (n < 4) return false; + for (int i = 0; i < n; i++) { + Vector2f a1 = poly.get(i); + Vector2f a2 = poly.get((i + 1) % n); + for (int j = i + 1; j < n; j++) { + if (Math.abs(i - j) <= 1 || (i == 0 && j == n - 1)) continue; + Vector2f b1 = poly.get(j); + Vector2f b2 = poly.get((j + 1) % n); + if (segmentsIntersect(a1.x, a1.y, a2.x, a2.y, b1.x, b1.y, b2.x, b2.y)) { + return true; + } + } + } + return false; + } + + private java.util.List convexHull(java.util.List points) { + java.util.List pts = new java.util.ArrayList<>(); + if (points == null || points.isEmpty()) return pts; + + java.util.Set seen = new java.util.HashSet<>(); + for (Vector2f p : points) { + String k = String.format("%.6f_%.6f", p.x, p.y); + if (!seen.contains(k)) { + seen.add(k); + pts.add(new Vector2f(p.x, p.y)); + } + } + if (pts.size() <= 1) return new java.util.ArrayList<>(pts); + + pts.sort((a, b) -> { + int cmp = Float.compare(a.x, b.x); + if (cmp != 0) return cmp; + return Float.compare(a.y, b.y); + }); + + java.util.List lower = new java.util.ArrayList<>(); + for (Vector2f p : pts) { + while (lower.size() >= 2) { + Vector2f p1 = lower.get(lower.size() - 2); + Vector2f p2 = lower.get(lower.size() - 1); + if (orient(p1.x, p1.y, p2.x, p2.y, p.x, p.y) <= 0) { + lower.remove(lower.size() - 1); + } else break; + } + lower.add(p); + } + + java.util.List upper = new java.util.ArrayList<>(); + for (int i = pts.size() - 1; i >= 0; i--) { + Vector2f p = pts.get(i); + while (upper.size() >= 2) { + Vector2f p1 = upper.get(upper.size() - 2); + Vector2f p2 = upper.get(upper.size() - 1); + if (orient(p1.x, p1.y, p2.x, p2.y, p.x, p.y) <= 0) { + upper.remove(upper.size() - 1); + } else break; + } + upper.add(p); + } + + lower.remove(lower.size() - 1); + upper.remove(upper.size() - 1); + lower.addAll(upper); + return lower; + } + + private boolean segmentsIntersect(double x1, double y1, double x2, double y2, + double x3, double y3, double x4, double y4) { + if (Math.max(x1, x2) < Math.min(x3, x4) || Math.max(x3, x4) < Math.min(x1, x2) || + Math.max(y1, y2) < Math.min(y3, y4) || Math.max(y3, y4) < Math.min(y1, y2)) { + return false; + } + double d1 = orient(x3, y3, x4, y4, x1, y1); + double d2 = orient(x3, y3, x4, y4, x2, y2); + double d3 = orient(x1, y1, x2, y2, x3, y3); + double d4 = orient(x1, y1, x2, y2, x4, y4); + + if (((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && + ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0))) { + return true; + } + + if (Math.abs(d1) < 1e-10 && onSegment(x3, y3, x4, y4, x1, y1)) return true; + if (Math.abs(d2) < 1e-10 && onSegment(x3, y3, x4, y4, x2, y2)) return true; + if (Math.abs(d3) < 1e-10 && onSegment(x1, y1, x2, y2, x3, y3)) return true; + if (Math.abs(d4) < 1e-10 && onSegment(x1, y1, x2, y2, x4, y4)) return true; + + return false; + } + + private double orient(double ax, double ay, double bx, double by, double cx, double cy) { + return (bx - ax) * (cy - ay) - (by - ay) * (cx - ax); + } + + private boolean onSegment(double ax, double ay, double bx, double by, double px, double py) { + double minx = Math.min(ax, bx) - 1e-8; + double maxx = Math.max(ax, bx) + 1e-8; + double miny = Math.min(ay, by) - 1e-8; + double maxy = Math.max(ay, by) + 1e-8; + return px >= minx && px <= maxx && py >= miny && py <= maxy && + Math.abs(orient(ax, ay, bx, by, px, py)) < 1e-8; + } + + + private void drawSelectionPolygon(BufferBuilder bb, java.util.List poly) { + if (poly == null || poly.size() < 3) return; + + Vector4f fillCol = new Vector4f(0.95f, 0.6f, 0.15f, 0.06f); + Vector4f edgeCol = new Vector4f(0.95f, 0.6f, 0.15f, 0.28f); + + // 填充(使用 TRIANGLE_FAN) + bb.begin(GL11.GL_TRIANGLE_FAN, poly.size() + 2); + bb.setColor(fillCol); + float cx = 0f, cy = 0f; + for (Vector2f v : poly) { cx += v.x; cy += v.y; } + cx /= poly.size(); cy /= poly.size(); + bb.vertex(cx, cy, 0f, 0f); + for (Vector2f v : poly) { + bb.setColor(fillCol); + bb.vertex(v.x, v.y, 0f, 0f); + } + // 闭合回首点 + Vector2f first = poly.get(0); + bb.setColor(fillCol); + bb.vertex(first.x, first.y, 0f, 0f); + bb.endImmediate(); + + // 额外绘制多个渐变环用于视觉加强(可选) + int rings = 3; + float ringStep = 8f; + for (int r = 1; r <= rings; r++) { + float out = r * ringStep; + java.util.List expanded = new java.util.ArrayList<>(); + int n = poly.size(); + for (int i = 0; i < n; i++) { + Vector2f prev = poly.get((i - 1 + n) % n); + Vector2f curr = poly.get(i); + Vector2f next = poly.get((i + 1) % n); + float ex = next.x - prev.x; + float ey = next.y - prev.y; + float nx = -ey; + float ny = ex; + float len = (float)Math.sqrt(nx*nx + ny*ny) + 1e-9f; + nx /= len; ny /= len; + expanded.add(new Vector2f(curr.x + nx * out, curr.y + ny * out)); + } + float alpha = 0.06f * (1f - (float)r / (rings + 1)); + Vector4f ringCol = new Vector4f(fillCol.x, fillCol.y, fillCol.z, alpha); + bb.begin(GL11.GL_TRIANGLE_FAN, expanded.size() + 2); + bb.setColor(ringCol); + bb.vertex(cx, cy, 0f, 0f); + for (Vector2f v : expanded) { + bb.setColor(ringCol); + bb.vertex(v.x, v.y, 0f, 0f); + } + bb.setColor(ringCol); + bb.vertex(expanded.get(0).x, expanded.get(0).y, 0f, 0f); + bb.endImmediate(); + } + + // 边框(使用 LINE_LOOP 保证闭合) + bb.begin(GL11.GL_LINE_LOOP, poly.size()); + bb.setColor(edgeCol); + for (Vector2f v : poly) { + bb.vertex(v.x, v.y, 0f, 0f); + } + bb.endImmediate(); + } + private void drawPredictedOutline(BufferBuilder bb, float[] predictedVertices, Mesh2D mesh2D) { if (predictedVertices == null || predictedVertices.length < 4) return; - // 1) 绘制网格轮廓(按顶点顺序连线)——半透明橙色 bb.begin(GL11.GL_LINE_LOOP, predictedVertices.length / 2); bb.setColor(new Vector4f(0.95f, 0.6f, 0.15f, 0.28f)); for (int i = 0; i < predictedVertices.length; i += 2) { @@ -137,8 +358,7 @@ public class VertexDeformationRander extends RanderTools { } bb.endImmediate(); - // 2) 绘制细线网(每隔若干顶点连线,提升可读性) - int step = Math.max(1, (predictedVertices.length / 2) / 40); // 控制线密度 + int step = Math.max(1, (predictedVertices.length / 2) / 40); for (int i = 0; i < predictedVertices.length; i += 2 * step) { int j = (i + 2 * step) % predictedVertices.length; bb.begin(GL11.GL_LINES, 2); @@ -148,7 +368,6 @@ public class VertexDeformationRander extends RanderTools { bb.endImmediate(); } - // 3) 绘制预测顶点的小圆点(半透明,便于与真实点区分) float psize = mesh2D.secondaryVertexSize * 0.6f; for (int i = 0; i < predictedVertices.length; i += 2) { drawCircleSolid(bb, predictedVertices[i], predictedVertices[i + 1], psize, new Vector4f(0.95f, 0.6f, 0.15f, 0.9f), 10); @@ -156,14 +375,10 @@ public class VertexDeformationRander extends RanderTools { } } - /** - * 绘制细线连接控制点(按 secondaryVertices 列表顺序连接,线微透明) - */ private void drawConnectionLines(BufferBuilder bb, Mesh2D mesh2D) { java.util.List verts = mesh2D.getSecondaryVertices(); if (verts.size() < 2) return; - // 细线:连接顺序(通常用于可视化控制点序列) GL11.glLineWidth(1.0f); bb.begin(GL11.GL_LINE_STRIP, verts.size()); bb.setColor(new Vector4f(1f, 1f, 1f, 0.12f)); @@ -173,7 +388,6 @@ public class VertexDeformationRander extends RanderTools { } bb.endImmediate(); - // 另外绘制每对近邻间的微型链接(更明显的细虚线效果:用短段组合实现) for (int i = 0; i < verts.size(); i++) { SecondaryVertex a = verts.get(i); SecondaryVertex b = verts.get((i + 1) % verts.size()); @@ -181,48 +395,6 @@ public class VertexDeformationRander extends RanderTools { } } - /** - * 绘制点的局部三角形(用两最近邻构成)并填充半透明,用于直观展示三角分配(非严格 Delaunay) - */ - private void drawLocalTriangle(BufferBuilder bb, SecondaryVertex v, Mesh2D mesh2D) { - java.util.List verts = mesh2D.getSecondaryVertices(); - if (verts.size() < 3) return; - - // 找两个最近邻 - SecondaryVertex n1 = null, n2 = null; - float best1 = Float.POSITIVE_INFINITY, best2 = Float.POSITIVE_INFINITY; - Vector2f pv = v.getPosition(); - for (SecondaryVertex other : verts) { - if (other == v) continue; - float d2 = pv.distanceSquared(other.getPosition()); - if (d2 < best1) { best2 = best1; n2 = n1; best1 = d2; n1 = other; } - else if (d2 < best2) { best2 = d2; n2 = other; } - } - if (n1 == null || n2 == null) return; - - // 半透明填充三角形(颜色基于 v 的颜色且带 alpha) - Vector4f triFill = new Vector4f(0.9f, 0.6f, 0.2f, 0.06f); // 示意色,可按需替换 - bb.begin(GL11.GL_TRIANGLES, 3); - bb.setColor(triFill); - bb.vertex(pv.x, pv.y, 0f, 0f); - bb.vertex(n1.getPosition().x, n1.getPosition().y, 0f, 0f); - bb.vertex(n2.getPosition().x, n2.getPosition().y, 0f, 0f); - bb.endImmediate(); - - // 三角形边框(细线,颜色取 controlRadius 是否固定的提示) - Vector4f edgeCol = v.isFixedRadius() ? new Vector4f(0.9f,0.4f,0.2f,0.9f) : new Vector4f(1f,1f,1f,0.12f); - bb.begin(GL11.GL_LINE_LOOP, 3); - bb.setColor(edgeCol); - bb.vertex(pv.x, pv.y, 0f, 0f); - bb.vertex(n1.getPosition().x, n1.getPosition().y, 0f, 0f); - bb.vertex(n2.getPosition().x, n2.getPosition().y, 0f, 0f); - bb.endImmediate(); - } - - /** - * 绘制虚线(通过分段短线模拟) - * segmentLen: 实线段长度, gapLen: 间隔长度 - */ private void drawDashedLine(BufferBuilder bb, float x1, float y1, float x2, float y2, float segmentLen, float gapLen, Vector4f color) { float dx = x2 - x1; float dy = y2 - y1; @@ -250,19 +422,14 @@ public class VertexDeformationRander extends RanderTools { } } - /** - * 在点旁边绘制 pin / lock 小图标(用简单几何表示) - */ private void drawPinLockIcon(BufferBuilder bb, SecondaryVertex v, float px, float py, float size) { float iconSize = size * 0.9f; float ix = px + size * 0.9f; float iy = py + size * 0.2f; if (v.isPinned()) { - // 绘制一个小“钉子”样式(矩形竖条 + 圆头) bb.begin(GL11.GL_TRIANGLES, 6); bb.setColor(new Vector4f(0.95f, 0.75f, 0.2f, 0.95f)); - // 矩形竖条 bb.vertex(ix - iconSize*0.12f, iy - iconSize*0.3f, 0f, 0f); bb.vertex(ix + iconSize*0.12f, iy - iconSize*0.3f, 0f, 0f); bb.vertex(ix + iconSize*0.12f, iy + iconSize*0.15f, 0f, 0f); @@ -272,17 +439,14 @@ public class VertexDeformationRander extends RanderTools { bb.vertex(ix - iconSize*0.12f, iy - iconSize*0.3f, 0f, 0f); bb.endImmediate(); - // 圆头 drawCircleSolid(bb, ix, iy + iconSize*0.25f, iconSize*0.18f, new Vector4f(1f,1f,1f,0.9f), 12); } if (v.isLocked()) { - // 绘制一个小“锁”样式(圆角矩形 + 环) float lx = ix + iconSize * 0.6f; float ly = iy; float w = iconSize * 0.8f; float h = iconSize * 0.6f; - // 背景 bb.begin(GL11.GL_TRIANGLES, 6); bb.setColor(new Vector4f(0.16f,0.16f,0.16f,0.95f)); bb.vertex(lx - w/2, ly - h/2, 0f, 0f); @@ -294,22 +458,16 @@ public class VertexDeformationRander extends RanderTools { bb.vertex(lx - w/2, ly - h/2, 0f, 0f); bb.endImmediate(); - // 锁环(用半圆表现) drawCircleGradient(bb, lx, ly - h*0.15f, w*0.35f, new Vector4f(1f,1f,1f,0.95f), new Vector4f(1f,1f,1f,0.6f), 10); } } - /** - * 绘制实心圆(单色) - */ private void drawCircleSolid(BufferBuilder bb, float cx, float cy, float radius, Vector4f color, int segments) { if (radius <= 0f) return; segments = Math.max(6, segments); bb.begin(GL11.GL_TRIANGLE_FAN, segments + 2); - // 中心 bb.setColor(color); bb.vertex(cx, cy, 0f, 0f); - // 外环 for (int i = 0; i <= segments; i++) { double ang = 2.0 * Math.PI * i / segments; float x = cx + (float) (Math.cos(ang) * radius); @@ -320,18 +478,13 @@ public class VertexDeformationRander extends RanderTools { bb.endImmediate(); } - /** - * 绘制带渐变的圆(中心颜色到边缘颜色) - */ private void drawCircleGradient(BufferBuilder bb, float cx, float cy, float radius, Vector4f centerColor, Vector4f outerColor, int segments) { if (radius <= 0f) return; segments = Math.max(8, segments); bb.begin(GL11.GL_TRIANGLE_FAN, segments + 2); - // 中心点使用中心颜色 bb.setColor(centerColor); bb.vertex(cx, cy, 0f, 0f); - // 外环每个顶点使用 outerColor(可以按需对每个顶点略微调整颜色以获得更平滑的效果) for (int i = 0; i <= segments; i++) { double ang = 2.0 * Math.PI * i / segments; float x = cx + (float) (Math.cos(ang) * radius); @@ -342,9 +495,6 @@ public class VertexDeformationRander extends RanderTools { bb.endImmediate(); } - /** - * 绘制圆形边框(线框) - */ private void drawCircleOutline(BufferBuilder bb, float cx, float cy, float radius, Vector4f color, int segments) { if (radius <= 0f) return; segments = Math.max(8, segments); @@ -359,33 +509,36 @@ public class VertexDeformationRander extends RanderTools { bb.endImmediate(); } - /** - * 绘制影响范围(半透明填充 + 边缘渐变) - */ - private void drawInfluenceRing(BufferBuilder bb, float cx, float cy, float radius, Vector4f baseColor) { - if (radius <= 0f) return; - // 内外颜色,外部更透明 - Vector4f inner = new Vector4f(baseColor.x, baseColor.y, baseColor.z, 0.12f); - Vector4f outer = new Vector4f(baseColor.x, baseColor.y, baseColor.z, 0.02f); - // 大致用两个同心渐变圆叠加表现柔和的影响范围 - drawCircleGradient(bb, cx, cy, radius, inner, outer, 48); - // 用一圈更明显的边界帮助辨识范围(细) - drawCircleOutline(bb, cx, cy, radius, new Vector4f(baseColor.x, baseColor.y, baseColor.z, 0.28f), 64); + private void drawInfluenceBox(BufferBuilder bb, Vector2f min, Vector2f max, Vector4f baseColor) { + Vector4f fillCol = new Vector4f(baseColor.x, baseColor.y, baseColor.z, 0.08f); + bb.begin(GL11.GL_QUADS, 4); + bb.setColor(fillCol); + bb.vertex(min.x, min.y, 0f, 0f); + bb.vertex(max.x, min.y, 0f, 0f); + bb.vertex(max.x, max.y, 0f, 0f); + bb.vertex(min.x, max.y, 0f, 0f); + bb.endImmediate(); + + Vector4f edgeCol = new Vector4f(baseColor.x, baseColor.y, baseColor.z, 0.35f); + GL11.glLineWidth(2.0f); + bb.begin(GL11.GL_LINE_LOOP, 4); + bb.setColor(edgeCol); + bb.vertex(min.x, min.y, 0f, 0f); + bb.vertex(max.x, min.y, 0f, 0f); + bb.vertex(max.x, max.y, 0f, 0f); + bb.vertex(min.x, max.y, 0f, 0f); + bb.endImmediate(); + GL11.glLineWidth(1.0f); } - /** - * 绘制顶点ID编号(更现代的标签:阴影 + 半透明背景 + 白色文字) - */ private void drawVertexId(Mesh2D mesh2D, BufferBuilder bb, int id, float x, float y, float size) { String idText = String.valueOf(id); TextRenderer textRenderer = ModelRender.getTextRenderer(); if (textRenderer != null) { float textWidth = textRenderer.getTextWidth(idText); - // 标签位置:点的右上方 float textX = x + size + 6.0f; float textY = y - size * 0.2f; - // 阴影(矩形偏移) float padX = 6f; float padY = 4f; float left = textX - padX; @@ -393,7 +546,6 @@ public class VertexDeformationRander extends RanderTools { float top = textY - 12f - padY; float bottom = textY + 4f + padY; - // 阴影背景(偏移) bb.begin(GL11.GL_TRIANGLES, 6); bb.setColor(new Vector4f(0f, 0f, 0f, 0.25f)); float sx = 2f, sy = -2f; @@ -405,7 +557,6 @@ public class VertexDeformationRander extends RanderTools { bb.vertex(left + sx, top + sy, 0f, 0f); bb.endImmediate(); - // 背景(半透明) bb.begin(GL11.GL_TRIANGLES, 6); bb.setColor(new Vector4f(0.06f, 0.06f, 0.06f, 0.88f)); bb.vertex(left, top, 0f, 0f); @@ -416,7 +567,6 @@ public class VertexDeformationRander extends RanderTools { bb.vertex(left, top, 0f, 0f); bb.endImmediate(); - // 边框 bb.begin(GL11.GL_LINE_LOOP, 4); bb.setColor(new Vector4f(1f, 1f, 1f, 0.1f)); bb.vertex(left, top, 0f, 0f); @@ -425,7 +575,6 @@ public class VertexDeformationRander extends RanderTools { bb.vertex(left, bottom, 0f, 0f); bb.endImmediate(); - // 文字(白色) ModelRender.renderText(idText, textX, textY, new Vector4f(1.0f, 1.0f, 1.0f, 1.0f)); } }