变形我甚至是搞不定了谁来搞搞啊,有关变形的类和方法在
类 VertexDeformationRander.java、SecondaryVertexPanel.java、VertexDeformationTool.java、Mesh2D.java 方法 VertexDeformationRander全部、SecondaryVertexPanel全部VertexDeformationTool全部、Mesh2D中的Mesh2D.updateVerticesFromSecondaryVertices()方法, Mesh2D的顶点管理很乱,我支持你完全重写Mesh2D中的顶点操作,删除二级顶点什么的,变形的时候注解操作原始顶点
This commit is contained in:
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<String, Object> payload = (Map<String, Object>) 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<AnimationParameter> newAnimationParameters = new ArrayList<>(existingParameter.animationParameter());
|
||||
List<String> newParamIds = new ArrayList<>(existingParameter.paramId());
|
||||
List<Object> newValues = new ArrayList<>(existingParameter.value());
|
||||
List<Float> newKeyframes = new ArrayList<>(existingParameter.keyframe());
|
||||
List<Boolean> 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<String, Object> oldPayload = (Map<String, Object>) 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),
|
||||
|
||||
@@ -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){};
|
||||
|
||||
/**
|
||||
* 获取工具光标
|
||||
*/
|
||||
|
||||
@@ -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<SecondaryVertexChangeListener> changeListeners = new ArrayList<>();
|
||||
private final List<Vector2f> 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<SecondaryVertex> 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<String, Object> 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<SecondaryVertex> 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<Vector2f> 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<SecondaryVertex> 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<Vector2f> 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<Vector2f> 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<Vector2f> convexHull(java.util.List<Vector2f> points) {
|
||||
java.util.List<Vector2f> pts = new java.util.ArrayList<>();
|
||||
if (points == null || points.isEmpty()) return pts;
|
||||
java.util.Set<String> 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<Vector2f>() {
|
||||
@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<Vector2f> 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<Vector2f> 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<String, Object> 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<SecondaryVertex> 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<String, Object> 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;
|
||||
}
|
||||
}
|
||||
|
||||
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<SecondaryVertex> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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<Vector2f> allP_list = new java.util.ArrayList<>();
|
||||
java.util.List<Vector2f> allQ_list = new java.util.ArrayList<>();
|
||||
java.util.List<Boolean> 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<Integer> 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<Vector2f> ptsQ = new java.util.ArrayList<>();
|
||||
java.util.List<Integer> idxMap = new java.util.ArrayList<>();
|
||||
java.util.Set<String> 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<Vector2f> poly = new java.util.ArrayList<>();
|
||||
java.util.List<Integer> 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<Integer> 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<Vector2f> hull = convexHull(ptsQ);
|
||||
java.util.List<Integer> 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<Vector2f> mlsPList = new java.util.ArrayList<>();
|
||||
java.util.List<Vector2f> 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<Vector2f> 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<Vector2f> 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<Vector2f> 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<Vector2f> convexHull(java.util.List<Vector2f> points) {
|
||||
java.util.List<Vector2f> 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<Vector2f> 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<Vector2f> 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<Integer> inRange = new java.util.ArrayList<>();
|
||||
java.util.List<Integer> 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<Integer> comp = (a, b) -> Float.compare(dists[a], dists[b]);
|
||||
inRange.sort(comp);
|
||||
others.sort(comp);
|
||||
|
||||
java.util.List<Integer> 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用反距离加权插值(适用于任意数量的控制点)
|
||||
*/
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
package com.chuangzhou.vivid2D.render.model.util;
|
||||
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* RegionOptimizer
|
||||
* - 处理当新点靠近已有点时的 controlRadius 重新分配(避免两个点控制区完全相同且重叠)
|
||||
* - 提供针对特征区域(例如嘴巴、尾巴)的简单优化算法接口
|
||||
* <p>
|
||||
* 算法思想(简述):
|
||||
* - 当新点插入或靠近已有点时,对两点及其邻域进行局部半径重分配,保证半径不相等且满足最小/最大约束。
|
||||
* - 对于特征(MOUTH/TAIL),使用基于位置的权重缩放半径(例如嘴巴中间半径较小以保证细节,边缘半径较大)
|
||||
*/
|
||||
public class RegionOptimizer {
|
||||
|
||||
public enum FeatureType { MOUTH, TAIL, OTHER }
|
||||
|
||||
// 新点插入时处理(调用 resolveNewAndNeighbor 或 resolveForInsertedVertex)
|
||||
public static void resolveForInsertedVertex(SecondaryVertex newV, List<SecondaryVertex> 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<SecondaryVertex> 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<SecondaryVertex> 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<SecondaryVertex> 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<SecondaryVertex> 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<SecondaryVertex> all, int radiusNeighbors) {
|
||||
if (v == null || all == null) return;
|
||||
// 取最近 couple 个(这里用固定 4 个邻居作为平滑范围)
|
||||
java.util.List<SecondaryVertex> 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) );
|
||||
}
|
||||
}
|
||||
@@ -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<SecondaryVertex> 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<SecondaryVertex> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<String, Boolean> 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<SecondaryVertex> 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<Vector2f> 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<String> seen = new java.util.HashSet<>();
|
||||
java.util.List<Vector2f> 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<Vector2f> 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<Vector2f> buildOrderedPolygon(java.util.List<Vector2f> pts) {
|
||||
java.util.List<Vector2f> out = new ArrayList<>();
|
||||
if (pts == null || pts.isEmpty()) return out;
|
||||
// 去重
|
||||
java.util.Set<String> seen = new java.util.HashSet<>();
|
||||
java.util.List<Vector2f> 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<Vector2f> 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<Vector2f> convexHull(java.util.List<Vector2f> points) {
|
||||
java.util.List<Vector2f> pts = new java.util.ArrayList<>();
|
||||
if (points == null || points.isEmpty()) return pts;
|
||||
|
||||
java.util.Set<String> 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<Vector2f> 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<Vector2f> 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<Vector2f> 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<Vector2f> 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<SecondaryVertex> 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<SecondaryVertex> 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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user