feat(render): 添加二级顶点编辑面板与相关工具支持
- 新增 SecondaryVertexPanel 用于显示和编辑二级顶点属性 - 在 MainWindow 中集成 SecondaryVertexPanel 并调整右侧面板布局- 添加顶点变形工具的 ChangeListener 支持,实现顶点操作回调 - 引入 ToolManagement 工具切换监听器机制 - 实现 ModelRenderPanel 的 switchToDefaultTool 方法 - 添加加载/保存模型时的进度条提示 - 优化顶点拖拽逻辑并移除冗余的坐标记录 - 更新 build.gradle 添加 swingx 依赖 - 清理无用导入和代码格式优化
This commit is contained in:
@@ -50,6 +50,9 @@ dependencies {
|
||||
implementation 'com.github.steos.jnafilechooser:jnafilechooser-api:1.1.2'
|
||||
implementation 'com.github.steos.jnafilechooser:jnafilechooser-win32:1.1.2'
|
||||
|
||||
// === Swing 组件 ===
|
||||
implementation 'org.swinglabs:swingx:1.6.1'
|
||||
|
||||
// === 本地库文件 ===
|
||||
implementation files('libs/JNC-1.0-jnc.jar')
|
||||
implementation files('libs/dog api 1.3.jar')
|
||||
|
||||
@@ -74,7 +74,6 @@ public final class TextRenderer {
|
||||
shader.use();
|
||||
|
||||
try {
|
||||
// 烘焙 ASCII
|
||||
asciiCharData = STBTTBakedChar.malloc(charCount);
|
||||
ByteBuffer asciiBitmap = ByteBuffer.allocateDirect(bitmapWidth * bitmapHeight);
|
||||
int asciiRes = stbtt_BakeFontBitmap(fontData, fontHeight, asciiBitmap,
|
||||
|
||||
@@ -1417,9 +1417,9 @@ public class ModelLayerPanel extends JPanel {
|
||||
}
|
||||
if (confirm == JOptionPane.YES_OPTION) {
|
||||
mainWindow.saveData(false);
|
||||
mainWindow.loadModel(file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
mainWindow.loadModel(file.getAbsolutePath());
|
||||
} else {
|
||||
JOptionPane.showMessageDialog(SwingUtilities.getWindowAncestor(ModelLayerPanel.this), "无法获取主窗口引用,无法加载模型文件。", "导入失败", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
@@ -341,6 +341,10 @@ public class ModelRenderPanel extends JPanel {
|
||||
glContextManager.executeInGLContext(() -> toolManagement.switchTool(toolName));
|
||||
}
|
||||
|
||||
public void switchToDefaultTool() {
|
||||
glContextManager.executeInGLContext(toolManagement::switchToDefaultTool);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到液化工具
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
package com.chuangzhou.vivid2D.render.awt;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.util.SecondaryVertex;
|
||||
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;
|
||||
private final JLabel statusLabel;
|
||||
private final JPanel contentPanel;
|
||||
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");
|
||||
contentPanel.add(vertexInfoPanel, "INFO");
|
||||
|
||||
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));
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
gbc.insets = new Insets(3, 5, 3, 5);
|
||||
gbc.anchor = GridBagConstraints.WEST;
|
||||
|
||||
int row = 0;
|
||||
|
||||
// --- 基本信息 (非编辑) ---
|
||||
gbc.gridx = 0;
|
||||
gbc.weightx = 0;
|
||||
panel.add(new JLabel("ID:"), gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1;
|
||||
idValue = new JLabel("N/A");
|
||||
panel.add(idValue, gbc);
|
||||
|
||||
row++;
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = row;
|
||||
gbc.weightx = 0;
|
||||
panel.add(new JLabel("Position (X, Y):"), gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1;
|
||||
posValue = new JLabel("N/A");
|
||||
panel.add(posValue, gbc);
|
||||
|
||||
row++;
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = row;
|
||||
gbc.weightx = 0;
|
||||
panel.add(new JLabel("UV (U, V):"), gbc);
|
||||
gbc.gridx = 1;
|
||||
gbc.weightx = 1;
|
||||
uvValue = new JLabel("N/A");
|
||||
panel.add(uvValue, gbc);
|
||||
|
||||
// --- 状态控制 (编辑) ---
|
||||
row++;
|
||||
gbc.gridy = row;
|
||||
gbc.gridx = 0;
|
||||
gbc.gridwidth = 2;
|
||||
panel.add(new JSeparator(), gbc);
|
||||
|
||||
row++;
|
||||
gbc.gridy = row;
|
||||
gbc.gridwidth = 1;
|
||||
gbc.gridx = 0;
|
||||
pinnedCheckBox = new JCheckBox("Pinned (钉住)");
|
||||
pinnedCheckBox.setToolTipText("是否作为固定点,可用于拖动整个网格");
|
||||
pinnedCheckBox.addActionListener(this::handleCheckboxChange);
|
||||
panel.add(pinnedCheckBox, gbc);
|
||||
|
||||
gbc.gridx = 1;
|
||||
lockedCheckBox = new JCheckBox("Locked (锁定)");
|
||||
lockedCheckBox.setToolTipText("锁定顶点,不允许移动");
|
||||
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; // 将垂直空间推到这里
|
||||
panel.add(new JPanel(), gbc);
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 公开 API:设置要显示的二级顶点。
|
||||
* @param vertex 要显示的 SecondaryVertex 对象,如果为 null 则显示默认状态。
|
||||
*/
|
||||
public void setSecondaryVertex(SecondaryVertex vertex) {
|
||||
this.currentVertex = vertex;
|
||||
|
||||
if (vertex == null) {
|
||||
cardLayout.show(contentPanel, "EMPTY");
|
||||
} else {
|
||||
updatePanelContent(vertex);
|
||||
cardLayout.show(contentPanel, "INFO");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 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;
|
||||
|
||||
if (e.getSource() == pinnedCheckBox) {
|
||||
currentVertex.setPinned(pinnedCheckBox.isSelected());
|
||||
} else if (e.getSource() == lockedCheckBox) {
|
||||
currentVertex.setLocked(lockedCheckBox.isSelected());
|
||||
} else if (e.getSource() == fixedRadiusCheckBox) {
|
||||
currentVertex.setFixedRadius(fixedRadiusCheckBox.isSelected());
|
||||
radiusSlider.setEnabled(!fixedRadiusCheckBox.isSelected());
|
||||
}
|
||||
|
||||
// 更改后自动刷新一次 (虽然状态已在 SecondaryVertex 中更改,但显示可能需要同步)
|
||||
updatePanelContent(currentVertex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 JSlider 半径值变更
|
||||
*/
|
||||
private void handleRadiusSliderChange(ChangeEvent e) {
|
||||
if (currentVertex == null || radiusSlider.getValueIsAdjusting()) return; // 仅在停止拖动时更新
|
||||
|
||||
// 将滑块 0-1000 的值重新映射到实际的 min/maxControlRadius 范围
|
||||
float minR = currentVertex.getMinControlRadius();
|
||||
float maxR = currentVertex.getMaxControlRadius();
|
||||
int sliderValue = radiusSlider.getValue();
|
||||
|
||||
float newRadius = minR + (sliderValue / 1000.0f) * (maxR - minR);
|
||||
|
||||
// 通过 setter 更新 SecondaryVertex,setter 内部会处理范围限制
|
||||
currentVertex.setControlRadius(newRadius);
|
||||
|
||||
// 更新显示的数值
|
||||
radiusValue.setText(DF.format(currentVertex.getControlRadius()));
|
||||
}
|
||||
}
|
||||
@@ -270,13 +270,10 @@ public class KeyboardManager {
|
||||
e.consume();
|
||||
if (panel.getToolManagement().hasActiveTool() &&
|
||||
!panel.getToolManagement().getCurrentTool().getToolName().equals("选择工具")) {
|
||||
// 如果有激活的工具且不是选择工具,切换到选择工具
|
||||
panel.switchTool("选择工具");
|
||||
panel.switchToDefaultTool();
|
||||
logger.info("按ESC键切换到选择工具");
|
||||
} else {
|
||||
// 否则取消所有选择
|
||||
panel.clearSelectedMeshes();
|
||||
|
||||
logger.info("按ESC键取消所有选择");
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -13,6 +13,7 @@ import java.awt.event.MouseEvent;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList; // 用于线程安全的监听器列表
|
||||
|
||||
/**
|
||||
* 工具管理器
|
||||
@@ -27,6 +28,9 @@ public class ToolManagement {
|
||||
private Tool currentTool = null;
|
||||
private Tool previousTool = null;
|
||||
|
||||
// 【新增】工具切换监听器列表
|
||||
private final List<ToolChangeListener> listeners;
|
||||
|
||||
// 默认工具(选择工具)
|
||||
private final Tool defaultTool;
|
||||
|
||||
@@ -34,6 +38,8 @@ public class ToolManagement {
|
||||
this.renderPanel = renderPanel;
|
||||
this.registeredTools = new ConcurrentHashMap<>();
|
||||
this.randerToolsManager = randerToolsManager;
|
||||
// 【新增】初始化监听器列表
|
||||
this.listeners = new CopyOnWriteArrayList<>();
|
||||
|
||||
// 创建默认选择工具
|
||||
this.defaultTool = new SelectionTool(renderPanel);
|
||||
@@ -43,6 +49,52 @@ public class ToolManagement {
|
||||
switchTool(defaultTool.getToolName());
|
||||
}
|
||||
|
||||
// 【新增】工具切换监听器接口
|
||||
/**
|
||||
* 工具切换监听器接口
|
||||
*/
|
||||
public interface ToolChangeListener {
|
||||
/**
|
||||
* 当当前工具发生变化时调用
|
||||
* @param newTool 切换后的新工具
|
||||
*/
|
||||
void onToolChanged(Tool newTool);
|
||||
}
|
||||
|
||||
// 【新增】添加监听器方法
|
||||
/**
|
||||
* 注册工具切换监听器
|
||||
*/
|
||||
public void addToolChangeListener(ToolChangeListener listener) {
|
||||
if (listener != null) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
// 【新增】移除监听器方法
|
||||
/**
|
||||
* 移除工具切换监听器
|
||||
*/
|
||||
public void removeToolChangeListener(ToolChangeListener listener) {
|
||||
if (listener != null) {
|
||||
listeners.remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
// 【新增】触发监听器方法
|
||||
/**
|
||||
* 触发所有监听器的 onToolChanged 方法
|
||||
*/
|
||||
private void fireToolChanged(Tool newTool) {
|
||||
for (ToolChangeListener listener : listeners) {
|
||||
try {
|
||||
listener.onToolChanged(newTool);
|
||||
} catch (Exception e) {
|
||||
logger.error("工具切换监听器回调失败: {}", e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ================== 工具注册管理 ==================
|
||||
|
||||
/**
|
||||
@@ -167,6 +219,10 @@ public class ToolManagement {
|
||||
updateCursor();
|
||||
|
||||
logger.info("切换到工具: {}", currentTool.getToolName());
|
||||
|
||||
// 【新增】触发工具切换回调
|
||||
fireToolChanged(currentTool);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -195,6 +251,14 @@ public class ToolManagement {
|
||||
return currentTool;
|
||||
}
|
||||
|
||||
// 【新增】提供 getActiveTool() 方法以解决 MainWindow 中的编译问题
|
||||
/**
|
||||
* 获取当前活动的工具(与 getCurrentTool 相同,提供别名以提高兼容性)
|
||||
*/
|
||||
public Tool getActiveTool() {
|
||||
return currentTool;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上一个工具
|
||||
*/
|
||||
@@ -308,6 +372,7 @@ public class ToolManagement {
|
||||
tool.dispose();
|
||||
}
|
||||
registeredTools.clear();
|
||||
listeners.clear(); // 【新增】清理监听器
|
||||
logger.info("工具管理器已清理");
|
||||
}
|
||||
|
||||
@@ -326,4 +391,4 @@ public class ToolManagement {
|
||||
registeredTools.size(), activeCount,
|
||||
currentTool != null ? currentTool.getToolName() : "无");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import org.slf4j.LoggerFactory;
|
||||
import java.awt.*;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.ArrayList; // 新增导入
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -29,15 +30,60 @@ public class VertexDeformationTool extends Tool {
|
||||
|
||||
private static final float VERTEX_TOLERANCE = 8.0f;
|
||||
private ModelRenderPanel.DragMode currentDragMode = ModelRenderPanel.DragMode.NONE;
|
||||
private float dragStartX, dragStartY;
|
||||
private float savedCameraRotation = Float.NaN;
|
||||
private Vector2f savedCameraScale = new Vector2f(1,1);
|
||||
private boolean cameraStateSaved = false;
|
||||
|
||||
private final List<SecondaryVertexChangeListener> changeListeners = new ArrayList<>();
|
||||
|
||||
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();
|
||||
EventQueue.invokeLater(() -> {
|
||||
for (SecondaryVertexChangeListener listener : changeListeners) {
|
||||
try {
|
||||
listener.onSecondaryVertexChange(part, targetMesh, vertex, type);
|
||||
} catch (Exception e) {
|
||||
logger.error("SecondaryVertexChangeListener 通知失败", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void activate() {
|
||||
if (isActive) return;
|
||||
@@ -96,10 +142,7 @@ public class VertexDeformationTool extends Tool {
|
||||
@Override
|
||||
public void deactivate() {
|
||||
if (!isActive) return;
|
||||
|
||||
isActive = false;
|
||||
|
||||
// 恢复相机之前的旋转/缩放状态(如果已保存)
|
||||
try {
|
||||
if (cameraStateSaved && renderPanel.getCameraManagement() != null && targetMesh != null && targetMesh.getModelPart() != null) {
|
||||
renderPanel.getGlContextManager().executeInGLContext(() -> {
|
||||
@@ -122,19 +165,17 @@ public class VertexDeformationTool extends Tool {
|
||||
|
||||
if (targetMesh != null) {
|
||||
associatedRanderTools.setAlgorithmEnabled("showSecondaryVertices", false);
|
||||
renderPanel.getGlContextManager().executeInGLContext(() -> {
|
||||
try {
|
||||
targetMesh.setShowSecondaryVertices(false);
|
||||
targetMesh.setRenderVertices(false);
|
||||
// 标记脏,触发必要的刷新
|
||||
if (targetMesh.getModelPart() != null) {
|
||||
targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition());
|
||||
targetMesh.getModelPart().updateMeshVertices();
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
logger.debug("停用时清理失败: {}", t.getMessage());
|
||||
try {
|
||||
targetMesh.setShowSecondaryVertices(false);
|
||||
targetMesh.setRenderVertices(false);
|
||||
// 标记脏,触发必要的刷新
|
||||
if (targetMesh.getModelPart() != null) {
|
||||
targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition());
|
||||
targetMesh.getModelPart().updateMeshVertices();
|
||||
}
|
||||
});
|
||||
} catch (Throwable t) {
|
||||
logger.debug("停用时清理失败: {}", t.getMessage());
|
||||
}
|
||||
}
|
||||
targetMesh = null;
|
||||
selectedVertex = null;
|
||||
@@ -158,8 +199,6 @@ public class VertexDeformationTool extends Tool {
|
||||
|
||||
// 开始拖拽
|
||||
currentDragMode = ModelRenderPanel.DragMode.MOVE_SECONDARY_VERTEX;
|
||||
dragStartX = modelX;
|
||||
dragStartY = modelY;
|
||||
|
||||
logger.debug("开始移动二级顶点: ID={}, 位置({}, {})",
|
||||
clickedVertex.getId(), modelX, modelY);
|
||||
@@ -182,6 +221,8 @@ public class VertexDeformationTool extends Tool {
|
||||
// 记录操作历史(可在这里添加撤销记录)
|
||||
if (currentDragMode == ModelRenderPanel.DragMode.MOVE_SECONDARY_VERTEX && selectedVertex != null) {
|
||||
logger.debug("完成移动二级顶点: ID={}", selectedVertex.getId());
|
||||
// 顶点移动完成,触发回调
|
||||
notifyListeners(selectedVertex, ChangeType.MOVE); // 新增:移动回调
|
||||
}
|
||||
|
||||
currentDragMode = ModelRenderPanel.DragMode.NONE;
|
||||
@@ -198,6 +239,9 @@ public class VertexDeformationTool extends Tool {
|
||||
// 移动顶点到新位置
|
||||
selectedVertex.setPosition(modelX, modelY);
|
||||
|
||||
// 持续拖拽,触发回调(使用 MOVE 类型)
|
||||
notifyListeners(selectedVertex, ChangeType.MOVE); // 新增:持续移动回调
|
||||
|
||||
// 广播:secondaryVertex -> { id, pos:[x,y] }
|
||||
try {
|
||||
if (targetMesh != null && targetMesh.getModelPart() != null) {
|
||||
@@ -213,8 +257,6 @@ public class VertexDeformationTool extends Tool {
|
||||
}
|
||||
|
||||
// 更新拖拽起始位置
|
||||
dragStartX = modelX;
|
||||
dragStartY = modelY;
|
||||
|
||||
// 标记网格为脏状态,需要重新计算边界等
|
||||
if (targetMesh != null && targetMesh.getModelPart() != null) {
|
||||
@@ -344,6 +386,9 @@ public class VertexDeformationTool extends Tool {
|
||||
logger.info("创建二级顶点: ID={}, 位置({}, {}), UV({}, {})",
|
||||
newVertex.getId(), x, y, u, v);
|
||||
|
||||
// 触发回调
|
||||
notifyListeners(newVertex, ChangeType.CREATE); // 新增:创建回调
|
||||
|
||||
// 广播创建(GL 线程内)
|
||||
try {
|
||||
if (targetMesh.getModelPart() != null) {
|
||||
@@ -387,6 +432,9 @@ public class VertexDeformationTool extends Tool {
|
||||
}
|
||||
logger.info("删除二级顶点: ID={}", vertex.getId());
|
||||
|
||||
// 触发回调
|
||||
notifyListeners(vertex, ChangeType.DELETE); // 新增:删除回调
|
||||
|
||||
// 广播删除(将 pos 设为 null 表示删除,可由 FrameInterpolator 识别)
|
||||
try {
|
||||
if (targetMesh.getModelPart() != null) {
|
||||
@@ -485,4 +533,4 @@ public class VertexDeformationTool extends Tool {
|
||||
public boolean isDragging() {
|
||||
return currentDragMode == ModelRenderPanel.DragMode.MOVE_SECONDARY_VERTEX;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,11 @@ import com.chuangzhou.vivid2D.render.awt.manager.LayerOperationManager;
|
||||
import com.chuangzhou.vivid2D.render.awt.manager.ParametersManagement;
|
||||
import com.chuangzhou.vivid2D.render.awt.manager.data.LayerOperationManagerData;
|
||||
import com.chuangzhou.vivid2D.render.awt.manager.data.ParametersManagementData;
|
||||
import com.chuangzhou.vivid2D.render.awt.tools.VertexDeformationTool;
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.util.SecondaryVertex;
|
||||
import jnafilechooser.api.JnaFileChooser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@@ -19,8 +21,6 @@ import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
@@ -35,12 +35,18 @@ public class MainWindow extends JFrame {
|
||||
private final TransformPanel transformPanel;
|
||||
private final ParametersPanel parametersPanel;
|
||||
private final ModelPartInfoPanel partInfoPanel;
|
||||
|
||||
private final SecondaryVertexPanel secondaryVertexPanel;
|
||||
|
||||
private final KeyBindingManager keyBindingManager;
|
||||
public String currentModelPath = null;
|
||||
private JLabel statusBarLabel;
|
||||
private JMenuBar menuBar;
|
||||
private boolean isModelModified = false;
|
||||
|
||||
// 【新增】进度条
|
||||
private JProgressBar loadingProgressBar;
|
||||
|
||||
/**
|
||||
* 构造主窗口。
|
||||
*/
|
||||
@@ -53,10 +59,14 @@ public class MainWindow extends JFrame {
|
||||
this.transformPanel = new TransformPanel(renderPanel);
|
||||
this.parametersPanel = new ParametersPanel(renderPanel);
|
||||
this.partInfoPanel = new ModelPartInfoPanel(renderPanel);
|
||||
|
||||
// 【新增】初始化 SecondaryVertexPanel
|
||||
this.secondaryVertexPanel = new SecondaryVertexPanel();
|
||||
|
||||
createMenuBar();
|
||||
createToolBar();
|
||||
createMainLayout();
|
||||
createStatusBar();
|
||||
createStatusBar(); // 确保在构造函数中调用,以便进度条被初始化
|
||||
setEditComponentsEnabled(false);
|
||||
setupInitialListeners();
|
||||
setSize(1600, 900);
|
||||
@@ -106,10 +116,13 @@ public class MainWindow extends JFrame {
|
||||
if (modelName != null && !modelName.trim().isEmpty()) {
|
||||
modelName = modelName.trim();
|
||||
String finalModelName = modelName;
|
||||
|
||||
// 【进度条】开始
|
||||
showProgressBar(true, "正在创建并加载新模型: " + finalModelName);
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
Model2D newModel = new Model2D(finalModelName);
|
||||
setEditComponentsEnabled(false);
|
||||
statusBarLabel.setText("正在创建并加载新模型: " + finalModelName);
|
||||
try {
|
||||
renderPanel.loadModel(newModel);
|
||||
renderPanel.setParametersManagement(ParametersManagement.getInstance(parametersPanel));
|
||||
@@ -130,6 +143,9 @@ public class MainWindow extends JFrame {
|
||||
"加载错误",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
setEditComponentsEnabled(false);
|
||||
} finally {
|
||||
// 【进度条】结束
|
||||
showProgressBar(false, "");
|
||||
}
|
||||
});
|
||||
} else if (modelName != null) {
|
||||
@@ -158,21 +174,41 @@ public class MainWindow extends JFrame {
|
||||
JScrollPane layerScroll = new JScrollPane(layerPanel);
|
||||
layerScroll.setMinimumSize(new Dimension(240, 100));
|
||||
layerScroll.setPreferredSize(new Dimension(260, 600));
|
||||
|
||||
JPanel centerPanelWrapper = new JPanel(new BorderLayout());
|
||||
centerPanelWrapper.add(renderPanel, BorderLayout.CENTER);
|
||||
centerPanelWrapper.setMinimumSize(new Dimension(400, 300));
|
||||
|
||||
JScrollPane transformScroll = new JScrollPane(transformPanel);
|
||||
transformScroll.setBorder(BorderFactory.createTitledBorder("变换控制"));
|
||||
transformScroll.setPreferredSize(new Dimension(300, 200));
|
||||
|
||||
JScrollPane paramScroll = new JScrollPane(parametersPanel);
|
||||
paramScroll.setBorder(BorderFactory.createTitledBorder("参数管理"));
|
||||
paramScroll.setPreferredSize(new Dimension(300, 200));
|
||||
JSplitPane rightPanelSplit = getjSplitPane(paramScroll, transformScroll);
|
||||
|
||||
// 【修改布局】将 partInfoPanel 和 secondaryVertexPanel 放在一个垂直分割面板中
|
||||
secondaryVertexPanel.setBorder(BorderFactory.createTitledBorder("顶点信息"));
|
||||
secondaryVertexPanel.setPreferredSize(new Dimension(300, 200));
|
||||
|
||||
JSplitPane infoSplit = new JSplitPane(
|
||||
JSplitPane.VERTICAL_SPLIT,
|
||||
partInfoPanel,
|
||||
secondaryVertexPanel // 【新增】加入二级顶点面板
|
||||
);
|
||||
infoSplit.setResizeWeight(0.5);
|
||||
infoSplit.setOneTouchExpandable(true);
|
||||
infoSplit.setPreferredSize(new Dimension(300, 300));
|
||||
|
||||
// 右侧面板从上到下:参数、变换控制、顶点信息
|
||||
JSplitPane rightPanelSplit = getjSplitPane(paramScroll, transformScroll, infoSplit);
|
||||
|
||||
JSplitPane mainSplit = getjSplitPane(new JSplitPane(
|
||||
JSplitPane.HORIZONTAL_SPLIT,
|
||||
centerPanelWrapper,
|
||||
rightPanelSplit
|
||||
), 0.75, JSplitPane.HORIZONTAL_SPLIT, layerScroll, 0.2);
|
||||
|
||||
add(mainSplit, BorderLayout.CENTER);
|
||||
}
|
||||
|
||||
@@ -189,13 +225,26 @@ public class MainWindow extends JFrame {
|
||||
return mainSplit;
|
||||
}
|
||||
|
||||
private @NotNull JSplitPane getjSplitPane(JScrollPane paramScroll, JScrollPane transformScroll) {
|
||||
JSplitPane rightPanelSplit = getjSplitPane(new JSplitPane(
|
||||
// 【修改】调整右侧面板的布局逻辑
|
||||
private @NotNull JSplitPane getjSplitPane(JScrollPane paramScroll, JScrollPane transformScroll, JSplitPane infoSplit) {
|
||||
// 上层分割:参数面板 (上) + 下层分割 (下)
|
||||
JSplitPane upperSplit = new JSplitPane(
|
||||
JSplitPane.VERTICAL_SPLIT,
|
||||
paramScroll,
|
||||
partInfoPanel
|
||||
), 0.5, JSplitPane.VERTICAL_SPLIT, transformScroll, 0.33);
|
||||
transformScroll
|
||||
);
|
||||
upperSplit.setResizeWeight(0.5); // 参数和变换面板各占一半
|
||||
|
||||
// 整个右侧面板: upperSplit (上) + infoSplit (下)
|
||||
JSplitPane rightPanelSplit = new JSplitPane(
|
||||
JSplitPane.VERTICAL_SPLIT,
|
||||
upperSplit,
|
||||
infoSplit // 包含 ModelPartInfoPanel 和 SecondaryVertexPanel 的分割面板
|
||||
);
|
||||
rightPanelSplit.setResizeWeight(0.66); // 调整上下比例
|
||||
rightPanelSplit.setOneTouchExpandable(true);
|
||||
rightPanelSplit.setPreferredSize(new Dimension(300, 600));
|
||||
|
||||
return rightPanelSplit;
|
||||
}
|
||||
|
||||
@@ -203,13 +252,41 @@ public class MainWindow extends JFrame {
|
||||
* 创建底部状态栏。
|
||||
*/
|
||||
private void createStatusBar() {
|
||||
JPanel statusBar = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
JPanel statusBar = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 0)); // 调整间距
|
||||
statusBar.setBorder(BorderFactory.createMatteBorder(1, 0, 0, 0, Color.GRAY));
|
||||
|
||||
// 【新增】进度条初始化
|
||||
this.loadingProgressBar = new JProgressBar();
|
||||
loadingProgressBar.setIndeterminate(true); // 不确定进度条,表示正在进行中
|
||||
loadingProgressBar.setVisible(false);
|
||||
loadingProgressBar.setPreferredSize(new Dimension(150, 18));
|
||||
|
||||
this.statusBarLabel = new JLabel("未加载模型。请通过 [文件] -> [打开模型...] 启动编辑。");
|
||||
|
||||
statusBar.add(statusBarLabel);
|
||||
statusBar.add(loadingProgressBar);
|
||||
|
||||
add(statusBar, BorderLayout.SOUTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* 【新增方法】控制进度条的显示和隐藏,并更新状态标签。
|
||||
*/
|
||||
private void showProgressBar(boolean show, String statusText) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (show) {
|
||||
loadingProgressBar.setVisible(true);
|
||||
loadingProgressBar.setIndeterminate(true);
|
||||
if (statusText != null && !statusText.isEmpty()) {
|
||||
statusBarLabel.setText(statusText);
|
||||
}
|
||||
} else {
|
||||
loadingProgressBar.setVisible(false);
|
||||
loadingProgressBar.setIndeterminate(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置初始的监听器,特别是窗口关闭监听。
|
||||
*/
|
||||
@@ -251,6 +328,9 @@ public class MainWindow extends JFrame {
|
||||
} else {
|
||||
partInfoPanel.updatePanel(null);
|
||||
}
|
||||
|
||||
// 当点击模型,但未选中顶点时,清空面板 (依赖 ToolChangeListener)
|
||||
// 如果 ToolManagement 中没有 getActiveTool(),此处的逻辑依赖 ToolChangeListener
|
||||
});
|
||||
}
|
||||
|
||||
@@ -259,6 +339,34 @@ public class MainWindow extends JFrame {
|
||||
onModelClicked(mesh, modelX, modelY, screenX, screenY);
|
||||
}
|
||||
});
|
||||
|
||||
VertexDeformationTool vertexDeformationTool = (VertexDeformationTool) renderPanel.getToolManagement().getTool("顶点变形工具");
|
||||
|
||||
vertexDeformationTool.addChangeListener((part, mesh, vertex, type) -> {
|
||||
// 确保在 AWT 事件分发线程 (EDT) 更新 UI
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (vertex != null) {
|
||||
secondaryVertexPanel.setSecondaryVertex(vertex);
|
||||
}
|
||||
if (type == VertexDeformationTool.ChangeType.DELETE) {
|
||||
SecondaryVertex newSelected = vertexDeformationTool.getSelectedVertex();
|
||||
secondaryVertexPanel.setSecondaryVertex(newSelected);
|
||||
}
|
||||
if (type == VertexDeformationTool.ChangeType.CREATE ||
|
||||
type == VertexDeformationTool.ChangeType.MOVE ||
|
||||
type == VertexDeformationTool.ChangeType.DELETE) {
|
||||
setModelModified(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
renderPanel.getToolManagement().addToolChangeListener(newTool -> SwingUtilities.invokeLater(() -> {
|
||||
if (newTool instanceof VertexDeformationTool) {
|
||||
SecondaryVertex selectedVertex = ((VertexDeformationTool) newTool).getSelectedVertex();
|
||||
secondaryVertexPanel.setSecondaryVertex(selectedVertex);
|
||||
} else {
|
||||
secondaryVertexPanel.setSecondaryVertex(null);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -269,10 +377,10 @@ public class MainWindow extends JFrame {
|
||||
transformPanel.setEnabled(enabled);
|
||||
parametersPanel.setEnabled(enabled);
|
||||
partInfoPanel.setEnabled(enabled);
|
||||
secondaryVertexPanel.setEnabled(enabled); // 【新增】
|
||||
renderPanel.setEnabled(enabled);
|
||||
for (Component comp : menuBar.getComponents()) {
|
||||
if (comp instanceof JMenu) {
|
||||
JMenu menu = (JMenu) comp;
|
||||
if (comp instanceof JMenu menu) {
|
||||
if ("编辑".equals(menu.getName())) {
|
||||
menu.setEnabled(enabled);
|
||||
}
|
||||
@@ -313,7 +421,10 @@ public class MainWindow extends JFrame {
|
||||
*/
|
||||
public void loadModel(String modelPath) {
|
||||
setEditComponentsEnabled(false);
|
||||
statusBarLabel.setText("正在加载模型: " + modelPath);
|
||||
|
||||
// 【进度条】开始
|
||||
showProgressBar(true, "正在加载模型: " + modelPath);
|
||||
|
||||
CompletableFuture.runAsync(() -> {
|
||||
Model2D model = null;
|
||||
try {
|
||||
@@ -324,6 +435,9 @@ public class MainWindow extends JFrame {
|
||||
}
|
||||
Model2D finalModel = model;
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
// 【进度条】结束
|
||||
showProgressBar(false, "");
|
||||
|
||||
if (finalModel == null || !renderPanel.getGlContextManager().isRunning()) {
|
||||
currentModelPath = null;
|
||||
setTitle("Vivid2D Editor - [加载失败]");
|
||||
@@ -367,28 +481,58 @@ public class MainWindow extends JFrame {
|
||||
return;
|
||||
}
|
||||
}
|
||||
statusBarLabel.setText("正在保存...");
|
||||
if (renderPanel.getModel() != null) {
|
||||
System.out.println("正在保存模型: " + currentModelPath);
|
||||
renderPanel.getModel().saveToFile(currentModelPath);
|
||||
}
|
||||
LayerOperationManager layerManager = layerPanel.getLayerOperationManager();
|
||||
LayerOperationManagerData layerData = new LayerOperationManagerData(layerManager.layerMetadata);
|
||||
//System.out.println("正在保存参数: " + renderPanel.getParametersManagement());
|
||||
ParametersManagementData managementData = new ParametersManagementData(renderPanel.getParametersManagement());
|
||||
String managementFilePath = currentModelPath + ".data";
|
||||
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(managementFilePath))) {
|
||||
oos.writeObject(layerData);
|
||||
oos.writeObject(managementData);
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace(System.err);
|
||||
statusBarLabel.setText("保存参数失败!");
|
||||
}
|
||||
statusBarLabel.setText("保存成功。");
|
||||
setModelModified(false);
|
||||
if (exitOnComplete) {
|
||||
shutdown();
|
||||
}
|
||||
|
||||
// 【进度条】开始
|
||||
showProgressBar(true, "正在保存模型...");
|
||||
|
||||
// 将保存操作放入后台线程,以避免阻塞 EDT
|
||||
new SwingWorker<Void, Void>() {
|
||||
@Override
|
||||
protected Void doInBackground() throws Exception {
|
||||
if (renderPanel.getModel() != null) {
|
||||
System.out.println("正在保存模型: " + currentModelPath);
|
||||
renderPanel.getModel().saveToFile(currentModelPath);
|
||||
}
|
||||
LayerOperationManager layerManager = layerPanel.getLayerOperationManager();
|
||||
LayerOperationManagerData layerData = new LayerOperationManagerData(layerManager.layerMetadata);
|
||||
ParametersManagementData managementData = new ParametersManagementData(renderPanel.getParametersManagement());
|
||||
String managementFilePath = currentModelPath + ".data";
|
||||
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(managementFilePath))) {
|
||||
oos.writeObject(layerData);
|
||||
oos.writeObject(managementData);
|
||||
} catch (IOException ex) {
|
||||
// 必须在 doInBackground 中处理或抛出
|
||||
throw ex;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void done() {
|
||||
// 【进度条】结束
|
||||
showProgressBar(false, "");
|
||||
|
||||
try {
|
||||
get(); // 检查是否有异常抛出
|
||||
statusBarLabel.setText("保存成功。");
|
||||
setModelModified(false);
|
||||
if (exitOnComplete) {
|
||||
shutdown();
|
||||
}
|
||||
} catch (InterruptedException ignore) {
|
||||
// 线程中断,通常发生在关闭时
|
||||
} catch (ExecutionException e) {
|
||||
// 实际异常被包装在 ExecutionException 中
|
||||
Throwable cause = e.getCause();
|
||||
System.err.println("保存失败: " + cause.getMessage());
|
||||
statusBarLabel.setText("保存失败!错误: " + cause.getMessage());
|
||||
JOptionPane.showMessageDialog(MainWindow.this,
|
||||
"保存操作失败: " + cause.getMessage(),
|
||||
"保存错误",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
private @NotNull JnaFileChooser getJnaFileChooser() {
|
||||
|
||||
Reference in New Issue
Block a user