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-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')

View File

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

View File

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

View File

@@ -341,6 +341,10 @@ public class ModelRenderPanel extends JPanel {
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();
if (panel.getToolManagement().hasActiveTool() &&
!panel.getToolManagement().getCurrentTool().getToolName().equals("选择工具")) {
// 如果有激活的工具且不是选择工具,切换到选择工具
panel.switchTool("选择工具");
panel.switchToDefaultTool();
logger.info("按ESC键切换到选择工具");
} else {
// 否则取消所有选择
panel.clearSelectedMeshes();
logger.info("按ESC键取消所有选择");
}
break;

View File

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

View File

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

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.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() {