变形我甚至是搞不定了谁来搞搞啊,有关变形的类和方法在

类 VertexDeformationRander.java、SecondaryVertexPanel.java、VertexDeformationTool.java、Mesh2D.java
方法 VertexDeformationRander全部、SecondaryVertexPanel全部VertexDeformationTool全部、Mesh2D中的Mesh2D.updateVerticesFromSecondaryVertices()方法,
Mesh2D的顶点管理很乱,我支持你完全重写Mesh2D中的顶点操作,删除二级顶点什么的,变形的时候注解操作原始顶点
This commit is contained in:
tzdwindows 7
2025-11-09 14:45:11 +08:00
parent c72fc19602
commit 69b1acf121
10 changed files with 1342 additions and 1140 deletions

View File

@@ -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 更新 SecondaryVertexsetter 内部会处理范围限制
currentVertex.setControlRadius(newRadius);
// 更新显示的数值
radiusValue.setText(DF.format(currentVertex.getControlRadius()));
}
}

View File

@@ -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 {

View File

@@ -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),

View File

@@ -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){};
/**
* 获取工具光标
*/

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}
/**

View File

@@ -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]; // 存储半径平方,避免重复开方
// 收集合格控制点poriginal来自 originalPositionqcurrent来自 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 必须来自 allPoriginalq 来自 allQcurrent
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 和每点的 deltacurrent - 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;
}
/**
* 使用反距离加权插值(适用于任意数量的控制点)
*/

View File

@@ -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) );
}
}

View File

@@ -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;
}
}

View File

@@ -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));
}
}