feat(render): 添加二级顶点编辑面板与相关工具支持

- 新增 SecondaryVertexPanel 用于显示和编辑二级顶点属性
- 在 MainWindow 中集成 SecondaryVertexPanel 并调整右侧面板布局- 添加顶点变形工具的 ChangeListener 支持,实现顶点操作回调
- 引入 ToolManagement 工具切换监听器机制
- 实现 ModelRenderPanel 的 switchToDefaultTool 方法
- 添加加载/保存模型时的进度条提示
- 优化顶点拖拽逻辑并移除冗余的坐标记录
- 更新 build.gradle 添加 swingx 依赖
- 清理无用导入和代码格式优化
This commit is contained in:
tzdwindows 7
2025-11-08 12:57:55 +08:00
parent b17bd500f2
commit 3b4b1b1b26
9 changed files with 584 additions and 63 deletions

View File

@@ -50,6 +50,9 @@ dependencies {
implementation 'com.github.steos.jnafilechooser:jnafilechooser-api:1.1.2' implementation 'com.github.steos.jnafilechooser:jnafilechooser-api:1.1.2'
implementation 'com.github.steos.jnafilechooser:jnafilechooser-win32: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/JNC-1.0-jnc.jar')
implementation files('libs/dog api 1.3.jar') implementation files('libs/dog api 1.3.jar')

View File

@@ -74,7 +74,6 @@ public final class TextRenderer {
shader.use(); shader.use();
try { try {
// 烘焙 ASCII
asciiCharData = STBTTBakedChar.malloc(charCount); asciiCharData = STBTTBakedChar.malloc(charCount);
ByteBuffer asciiBitmap = ByteBuffer.allocateDirect(bitmapWidth * bitmapHeight); ByteBuffer asciiBitmap = ByteBuffer.allocateDirect(bitmapWidth * bitmapHeight);
int asciiRes = stbtt_BakeFontBitmap(fontData, fontHeight, asciiBitmap, int asciiRes = stbtt_BakeFontBitmap(fontData, fontHeight, asciiBitmap,

View File

@@ -1417,9 +1417,9 @@ public class ModelLayerPanel extends JPanel {
} }
if (confirm == JOptionPane.YES_OPTION) { if (confirm == JOptionPane.YES_OPTION) {
mainWindow.saveData(false); mainWindow.saveData(false);
mainWindow.loadModel(file.getAbsolutePath());
} }
} }
mainWindow.loadModel(file.getAbsolutePath());
} else { } else {
JOptionPane.showMessageDialog(SwingUtilities.getWindowAncestor(ModelLayerPanel.this), "无法获取主窗口引用,无法加载模型文件。", "导入失败", JOptionPane.ERROR_MESSAGE); JOptionPane.showMessageDialog(SwingUtilities.getWindowAncestor(ModelLayerPanel.this), "无法获取主窗口引用,无法加载模型文件。", "导入失败", JOptionPane.ERROR_MESSAGE);
} }

View File

@@ -341,6 +341,10 @@ public class ModelRenderPanel extends JPanel {
glContextManager.executeInGLContext(() -> toolManagement.switchTool(toolName)); glContextManager.executeInGLContext(() -> toolManagement.switchTool(toolName));
} }
public void switchToDefaultTool() {
glContextManager.executeInGLContext(toolManagement::switchToDefaultTool);
}
/** /**
* 切换到液化工具 * 切换到液化工具
*/ */

View File

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

View File

@@ -270,13 +270,10 @@ public class KeyboardManager {
e.consume(); e.consume();
if (panel.getToolManagement().hasActiveTool() && if (panel.getToolManagement().hasActiveTool() &&
!panel.getToolManagement().getCurrentTool().getToolName().equals("选择工具")) { !panel.getToolManagement().getCurrentTool().getToolName().equals("选择工具")) {
// 如果有激活的工具且不是选择工具,切换到选择工具 panel.switchToDefaultTool();
panel.switchTool("选择工具");
logger.info("按ESC键切换到选择工具"); logger.info("按ESC键切换到选择工具");
} else { } else {
// 否则取消所有选择
panel.clearSelectedMeshes(); panel.clearSelectedMeshes();
logger.info("按ESC键取消所有选择"); logger.info("按ESC键取消所有选择");
} }
break; break;

View File

@@ -13,6 +13,7 @@ import java.awt.event.MouseEvent;
import java.util.*; import java.util.*;
import java.util.List; import java.util.List;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList; // 用于线程安全的监听器列表
/** /**
* 工具管理器 * 工具管理器
@@ -27,6 +28,9 @@ public class ToolManagement {
private Tool currentTool = null; private Tool currentTool = null;
private Tool previousTool = null; private Tool previousTool = null;
// 【新增】工具切换监听器列表
private final List<ToolChangeListener> listeners;
// 默认工具(选择工具) // 默认工具(选择工具)
private final Tool defaultTool; private final Tool defaultTool;
@@ -34,6 +38,8 @@ public class ToolManagement {
this.renderPanel = renderPanel; this.renderPanel = renderPanel;
this.registeredTools = new ConcurrentHashMap<>(); this.registeredTools = new ConcurrentHashMap<>();
this.randerToolsManager = randerToolsManager; this.randerToolsManager = randerToolsManager;
// 【新增】初始化监听器列表
this.listeners = new CopyOnWriteArrayList<>();
// 创建默认选择工具 // 创建默认选择工具
this.defaultTool = new SelectionTool(renderPanel); this.defaultTool = new SelectionTool(renderPanel);
@@ -43,6 +49,52 @@ public class ToolManagement {
switchTool(defaultTool.getToolName()); 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(); updateCursor();
logger.info("切换到工具: {}", currentTool.getToolName()); logger.info("切换到工具: {}", currentTool.getToolName());
// 【新增】触发工具切换回调
fireToolChanged(currentTool);
return true; return true;
} }
@@ -195,6 +251,14 @@ public class ToolManagement {
return currentTool; return currentTool;
} }
// 【新增】提供 getActiveTool() 方法以解决 MainWindow 中的编译问题
/**
* 获取当前活动的工具(与 getCurrentTool 相同,提供别名以提高兼容性)
*/
public Tool getActiveTool() {
return currentTool;
}
/** /**
* 获取上一个工具 * 获取上一个工具
*/ */
@@ -308,6 +372,7 @@ public class ToolManagement {
tool.dispose(); tool.dispose();
} }
registeredTools.clear(); registeredTools.clear();
listeners.clear(); // 【新增】清理监听器
logger.info("工具管理器已清理"); logger.info("工具管理器已清理");
} }
@@ -326,4 +391,4 @@ public class ToolManagement {
registeredTools.size(), activeCount, registeredTools.size(), activeCount,
currentTool != null ? currentTool.getToolName() : ""); currentTool != null ? currentTool.getToolName() : "");
} }
} }

View File

@@ -13,6 +13,7 @@ import org.slf4j.LoggerFactory;
import java.awt.*; import java.awt.*;
import java.awt.event.MouseEvent; import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.util.ArrayList; // 新增导入
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -29,15 +30,60 @@ public class VertexDeformationTool extends Tool {
private static final float VERTEX_TOLERANCE = 8.0f; private static final float VERTEX_TOLERANCE = 8.0f;
private ModelRenderPanel.DragMode currentDragMode = ModelRenderPanel.DragMode.NONE; private ModelRenderPanel.DragMode currentDragMode = ModelRenderPanel.DragMode.NONE;
private float dragStartX, dragStartY;
private float savedCameraRotation = Float.NaN; private float savedCameraRotation = Float.NaN;
private Vector2f savedCameraScale = new Vector2f(1,1); private Vector2f savedCameraScale = new Vector2f(1,1);
private boolean cameraStateSaved = false; private boolean cameraStateSaved = false;
private final List<SecondaryVertexChangeListener> changeListeners = new ArrayList<>();
public VertexDeformationTool(ModelRenderPanel renderPanel) { public VertexDeformationTool(ModelRenderPanel renderPanel) {
super(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 @Override
public void activate() { public void activate() {
if (isActive) return; if (isActive) return;
@@ -96,10 +142,7 @@ public class VertexDeformationTool extends Tool {
@Override @Override
public void deactivate() { public void deactivate() {
if (!isActive) return; if (!isActive) return;
isActive = false; isActive = false;
// 恢复相机之前的旋转/缩放状态(如果已保存)
try { try {
if (cameraStateSaved && renderPanel.getCameraManagement() != null && targetMesh != null && targetMesh.getModelPart() != null) { if (cameraStateSaved && renderPanel.getCameraManagement() != null && targetMesh != null && targetMesh.getModelPart() != null) {
renderPanel.getGlContextManager().executeInGLContext(() -> { renderPanel.getGlContextManager().executeInGLContext(() -> {
@@ -122,19 +165,17 @@ public class VertexDeformationTool extends Tool {
if (targetMesh != null) { if (targetMesh != null) {
associatedRanderTools.setAlgorithmEnabled("showSecondaryVertices", false); associatedRanderTools.setAlgorithmEnabled("showSecondaryVertices", false);
renderPanel.getGlContextManager().executeInGLContext(() -> { try {
try { targetMesh.setShowSecondaryVertices(false);
targetMesh.setShowSecondaryVertices(false); targetMesh.setRenderVertices(false);
targetMesh.setRenderVertices(false); // 标记脏,触发必要的刷新
// 标记脏,触发必要的刷新 if (targetMesh.getModelPart() != null) {
if (targetMesh.getModelPart() != null) { targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition());
targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition()); targetMesh.getModelPart().updateMeshVertices();
targetMesh.getModelPart().updateMeshVertices();
}
} catch (Throwable t) {
logger.debug("停用时清理失败: {}", t.getMessage());
} }
}); } catch (Throwable t) {
logger.debug("停用时清理失败: {}", t.getMessage());
}
} }
targetMesh = null; targetMesh = null;
selectedVertex = null; selectedVertex = null;
@@ -158,8 +199,6 @@ public class VertexDeformationTool extends Tool {
// 开始拖拽 // 开始拖拽
currentDragMode = ModelRenderPanel.DragMode.MOVE_SECONDARY_VERTEX; currentDragMode = ModelRenderPanel.DragMode.MOVE_SECONDARY_VERTEX;
dragStartX = modelX;
dragStartY = modelY;
logger.debug("开始移动二级顶点: ID={}, 位置({}, {})", logger.debug("开始移动二级顶点: ID={}, 位置({}, {})",
clickedVertex.getId(), modelX, modelY); clickedVertex.getId(), modelX, modelY);
@@ -182,6 +221,8 @@ public class VertexDeformationTool extends Tool {
// 记录操作历史(可在这里添加撤销记录) // 记录操作历史(可在这里添加撤销记录)
if (currentDragMode == ModelRenderPanel.DragMode.MOVE_SECONDARY_VERTEX && selectedVertex != null) { if (currentDragMode == ModelRenderPanel.DragMode.MOVE_SECONDARY_VERTEX && selectedVertex != null) {
logger.debug("完成移动二级顶点: ID={}", selectedVertex.getId()); logger.debug("完成移动二级顶点: ID={}", selectedVertex.getId());
// 顶点移动完成,触发回调
notifyListeners(selectedVertex, ChangeType.MOVE); // 新增:移动回调
} }
currentDragMode = ModelRenderPanel.DragMode.NONE; currentDragMode = ModelRenderPanel.DragMode.NONE;
@@ -198,6 +239,9 @@ public class VertexDeformationTool extends Tool {
// 移动顶点到新位置 // 移动顶点到新位置
selectedVertex.setPosition(modelX, modelY); selectedVertex.setPosition(modelX, modelY);
// 持续拖拽,触发回调(使用 MOVE 类型)
notifyListeners(selectedVertex, ChangeType.MOVE); // 新增:持续移动回调
// 广播secondaryVertex -> { id, pos:[x,y] } // 广播secondaryVertex -> { id, pos:[x,y] }
try { try {
if (targetMesh != null && targetMesh.getModelPart() != null) { 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) { if (targetMesh != null && targetMesh.getModelPart() != null) {
@@ -344,6 +386,9 @@ public class VertexDeformationTool extends Tool {
logger.info("创建二级顶点: ID={}, 位置({}, {}), UV({}, {})", logger.info("创建二级顶点: ID={}, 位置({}, {}), UV({}, {})",
newVertex.getId(), x, y, u, v); newVertex.getId(), x, y, u, v);
// 触发回调
notifyListeners(newVertex, ChangeType.CREATE); // 新增:创建回调
// 广播创建GL 线程内) // 广播创建GL 线程内)
try { try {
if (targetMesh.getModelPart() != null) { if (targetMesh.getModelPart() != null) {
@@ -387,6 +432,9 @@ public class VertexDeformationTool extends Tool {
} }
logger.info("删除二级顶点: ID={}", vertex.getId()); logger.info("删除二级顶点: ID={}", vertex.getId());
// 触发回调
notifyListeners(vertex, ChangeType.DELETE); // 新增:删除回调
// 广播删除(将 pos 设为 null 表示删除,可由 FrameInterpolator 识别) // 广播删除(将 pos 设为 null 表示删除,可由 FrameInterpolator 识别)
try { try {
if (targetMesh.getModelPart() != null) { if (targetMesh.getModelPart() != null) {
@@ -485,4 +533,4 @@ public class VertexDeformationTool extends Tool {
public boolean isDragging() { public boolean isDragging() {
return currentDragMode == ModelRenderPanel.DragMode.MOVE_SECONDARY_VERTEX; return currentDragMode == ModelRenderPanel.DragMode.MOVE_SECONDARY_VERTEX;
} }
} }

View File

@@ -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.ParametersManagement;
import com.chuangzhou.vivid2D.render.awt.manager.data.LayerOperationManagerData; import com.chuangzhou.vivid2D.render.awt.manager.data.LayerOperationManagerData;
import com.chuangzhou.vivid2D.render.awt.manager.data.ParametersManagementData; 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.Model2D;
import com.chuangzhou.vivid2D.render.model.ModelPart; import com.chuangzhou.vivid2D.render.model.ModelPart;
import com.chuangzhou.vivid2D.render.model.util.Mesh2D; import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
import com.chuangzhou.vivid2D.render.model.util.SecondaryVertex;
import jnafilechooser.api.JnaFileChooser; import jnafilechooser.api.JnaFileChooser;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -19,8 +21,6 @@ import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.ObjectOutputStream; import java.io.ObjectOutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@@ -35,12 +35,18 @@ public class MainWindow extends JFrame {
private final TransformPanel transformPanel; private final TransformPanel transformPanel;
private final ParametersPanel parametersPanel; private final ParametersPanel parametersPanel;
private final ModelPartInfoPanel partInfoPanel; private final ModelPartInfoPanel partInfoPanel;
private final SecondaryVertexPanel secondaryVertexPanel;
private final KeyBindingManager keyBindingManager; private final KeyBindingManager keyBindingManager;
public String currentModelPath = null; public String currentModelPath = null;
private JLabel statusBarLabel; private JLabel statusBarLabel;
private JMenuBar menuBar; private JMenuBar menuBar;
private boolean isModelModified = false; private boolean isModelModified = false;
// 【新增】进度条
private JProgressBar loadingProgressBar;
/** /**
* 构造主窗口。 * 构造主窗口。
*/ */
@@ -53,10 +59,14 @@ public class MainWindow extends JFrame {
this.transformPanel = new TransformPanel(renderPanel); this.transformPanel = new TransformPanel(renderPanel);
this.parametersPanel = new ParametersPanel(renderPanel); this.parametersPanel = new ParametersPanel(renderPanel);
this.partInfoPanel = new ModelPartInfoPanel(renderPanel); this.partInfoPanel = new ModelPartInfoPanel(renderPanel);
// 【新增】初始化 SecondaryVertexPanel
this.secondaryVertexPanel = new SecondaryVertexPanel();
createMenuBar(); createMenuBar();
createToolBar(); createToolBar();
createMainLayout(); createMainLayout();
createStatusBar(); createStatusBar(); // 确保在构造函数中调用,以便进度条被初始化
setEditComponentsEnabled(false); setEditComponentsEnabled(false);
setupInitialListeners(); setupInitialListeners();
setSize(1600, 900); setSize(1600, 900);
@@ -106,10 +116,13 @@ public class MainWindow extends JFrame {
if (modelName != null && !modelName.trim().isEmpty()) { if (modelName != null && !modelName.trim().isEmpty()) {
modelName = modelName.trim(); modelName = modelName.trim();
String finalModelName = modelName; String finalModelName = modelName;
// 【进度条】开始
showProgressBar(true, "正在创建并加载新模型: " + finalModelName);
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
Model2D newModel = new Model2D(finalModelName); Model2D newModel = new Model2D(finalModelName);
setEditComponentsEnabled(false); setEditComponentsEnabled(false);
statusBarLabel.setText("正在创建并加载新模型: " + finalModelName);
try { try {
renderPanel.loadModel(newModel); renderPanel.loadModel(newModel);
renderPanel.setParametersManagement(ParametersManagement.getInstance(parametersPanel)); renderPanel.setParametersManagement(ParametersManagement.getInstance(parametersPanel));
@@ -130,6 +143,9 @@ public class MainWindow extends JFrame {
"加载错误", "加载错误",
JOptionPane.ERROR_MESSAGE); JOptionPane.ERROR_MESSAGE);
setEditComponentsEnabled(false); setEditComponentsEnabled(false);
} finally {
// 【进度条】结束
showProgressBar(false, "");
} }
}); });
} else if (modelName != null) { } else if (modelName != null) {
@@ -158,21 +174,41 @@ public class MainWindow extends JFrame {
JScrollPane layerScroll = new JScrollPane(layerPanel); JScrollPane layerScroll = new JScrollPane(layerPanel);
layerScroll.setMinimumSize(new Dimension(240, 100)); layerScroll.setMinimumSize(new Dimension(240, 100));
layerScroll.setPreferredSize(new Dimension(260, 600)); layerScroll.setPreferredSize(new Dimension(260, 600));
JPanel centerPanelWrapper = new JPanel(new BorderLayout()); JPanel centerPanelWrapper = new JPanel(new BorderLayout());
centerPanelWrapper.add(renderPanel, BorderLayout.CENTER); centerPanelWrapper.add(renderPanel, BorderLayout.CENTER);
centerPanelWrapper.setMinimumSize(new Dimension(400, 300)); centerPanelWrapper.setMinimumSize(new Dimension(400, 300));
JScrollPane transformScroll = new JScrollPane(transformPanel); JScrollPane transformScroll = new JScrollPane(transformPanel);
transformScroll.setBorder(BorderFactory.createTitledBorder("变换控制")); transformScroll.setBorder(BorderFactory.createTitledBorder("变换控制"));
transformScroll.setPreferredSize(new Dimension(300, 200)); transformScroll.setPreferredSize(new Dimension(300, 200));
JScrollPane paramScroll = new JScrollPane(parametersPanel); JScrollPane paramScroll = new JScrollPane(parametersPanel);
paramScroll.setBorder(BorderFactory.createTitledBorder("参数管理")); paramScroll.setBorder(BorderFactory.createTitledBorder("参数管理"));
paramScroll.setPreferredSize(new Dimension(300, 200)); 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 mainSplit = getjSplitPane(new JSplitPane(
JSplitPane.HORIZONTAL_SPLIT, JSplitPane.HORIZONTAL_SPLIT,
centerPanelWrapper, centerPanelWrapper,
rightPanelSplit rightPanelSplit
), 0.75, JSplitPane.HORIZONTAL_SPLIT, layerScroll, 0.2); ), 0.75, JSplitPane.HORIZONTAL_SPLIT, layerScroll, 0.2);
add(mainSplit, BorderLayout.CENTER); add(mainSplit, BorderLayout.CENTER);
} }
@@ -189,13 +225,26 @@ public class MainWindow extends JFrame {
return mainSplit; 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, JSplitPane.VERTICAL_SPLIT,
paramScroll, paramScroll,
partInfoPanel transformScroll
), 0.5, JSplitPane.VERTICAL_SPLIT, transformScroll, 0.33); );
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)); rightPanelSplit.setPreferredSize(new Dimension(300, 600));
return rightPanelSplit; return rightPanelSplit;
} }
@@ -203,13 +252,41 @@ public class MainWindow extends JFrame {
* 创建底部状态栏。 * 创建底部状态栏。
*/ */
private void createStatusBar() { 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)); 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("未加载模型。请通过 [文件] -> [打开模型...] 启动编辑。"); this.statusBarLabel = new JLabel("未加载模型。请通过 [文件] -> [打开模型...] 启动编辑。");
statusBar.add(statusBarLabel); statusBar.add(statusBarLabel);
statusBar.add(loadingProgressBar);
add(statusBar, BorderLayout.SOUTH); 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 { } else {
partInfoPanel.updatePanel(null); partInfoPanel.updatePanel(null);
} }
// 当点击模型,但未选中顶点时,清空面板 (依赖 ToolChangeListener)
// 如果 ToolManagement 中没有 getActiveTool(),此处的逻辑依赖 ToolChangeListener
}); });
} }
@@ -259,6 +339,34 @@ public class MainWindow extends JFrame {
onModelClicked(mesh, modelX, modelY, screenX, screenY); 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); transformPanel.setEnabled(enabled);
parametersPanel.setEnabled(enabled); parametersPanel.setEnabled(enabled);
partInfoPanel.setEnabled(enabled); partInfoPanel.setEnabled(enabled);
secondaryVertexPanel.setEnabled(enabled); // 【新增】
renderPanel.setEnabled(enabled); renderPanel.setEnabled(enabled);
for (Component comp : menuBar.getComponents()) { for (Component comp : menuBar.getComponents()) {
if (comp instanceof JMenu) { if (comp instanceof JMenu menu) {
JMenu menu = (JMenu) comp;
if ("编辑".equals(menu.getName())) { if ("编辑".equals(menu.getName())) {
menu.setEnabled(enabled); menu.setEnabled(enabled);
} }
@@ -313,7 +421,10 @@ public class MainWindow extends JFrame {
*/ */
public void loadModel(String modelPath) { public void loadModel(String modelPath) {
setEditComponentsEnabled(false); setEditComponentsEnabled(false);
statusBarLabel.setText("正在加载模型: " + modelPath);
// 【进度条】开始
showProgressBar(true, "正在加载模型: " + modelPath);
CompletableFuture.runAsync(() -> { CompletableFuture.runAsync(() -> {
Model2D model = null; Model2D model = null;
try { try {
@@ -324,6 +435,9 @@ public class MainWindow extends JFrame {
} }
Model2D finalModel = model; Model2D finalModel = model;
SwingUtilities.invokeLater(() -> { SwingUtilities.invokeLater(() -> {
// 【进度条】结束
showProgressBar(false, "");
if (finalModel == null || !renderPanel.getGlContextManager().isRunning()) { if (finalModel == null || !renderPanel.getGlContextManager().isRunning()) {
currentModelPath = null; currentModelPath = null;
setTitle("Vivid2D Editor - [加载失败]"); setTitle("Vivid2D Editor - [加载失败]");
@@ -367,28 +481,58 @@ public class MainWindow extends JFrame {
return; return;
} }
} }
statusBarLabel.setText("正在保存...");
if (renderPanel.getModel() != null) { // 【进度条】开始
System.out.println("正在保存模型: " + currentModelPath); showProgressBar(true, "正在保存模型...");
renderPanel.getModel().saveToFile(currentModelPath);
} // 将保存操作放入后台线程,以避免阻塞 EDT
LayerOperationManager layerManager = layerPanel.getLayerOperationManager(); new SwingWorker<Void, Void>() {
LayerOperationManagerData layerData = new LayerOperationManagerData(layerManager.layerMetadata); @Override
//System.out.println("正在保存参数: " + renderPanel.getParametersManagement()); protected Void doInBackground() throws Exception {
ParametersManagementData managementData = new ParametersManagementData(renderPanel.getParametersManagement()); if (renderPanel.getModel() != null) {
String managementFilePath = currentModelPath + ".data"; System.out.println("正在保存模型: " + currentModelPath);
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(managementFilePath))) { renderPanel.getModel().saveToFile(currentModelPath);
oos.writeObject(layerData); }
oos.writeObject(managementData); LayerOperationManager layerManager = layerPanel.getLayerOperationManager();
} catch (IOException ex) { LayerOperationManagerData layerData = new LayerOperationManagerData(layerManager.layerMetadata);
ex.printStackTrace(System.err); ParametersManagementData managementData = new ParametersManagementData(renderPanel.getParametersManagement());
statusBarLabel.setText("保存参数失败!"); String managementFilePath = currentModelPath + ".data";
} try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(managementFilePath))) {
statusBarLabel.setText("保存成功。"); oos.writeObject(layerData);
setModelModified(false); oos.writeObject(managementData);
if (exitOnComplete) { } catch (IOException ex) {
shutdown(); // 必须在 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() { private @NotNull JnaFileChooser getJnaFileChooser() {