diff --git a/build.gradle b/build.gradle index 2f1936f..f81f607 100644 --- a/build.gradle +++ b/build.gradle @@ -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') diff --git a/src/main/java/com/chuangzhou/vivid2D/render/TextRenderer.java b/src/main/java/com/chuangzhou/vivid2D/render/TextRenderer.java index 19d5df4..21915d2 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/TextRenderer.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/TextRenderer.java @@ -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, diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java index d11f780..00b80d5 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java @@ -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); } diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java index ef9bfaa..c7fbc8f 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java @@ -341,6 +341,10 @@ public class ModelRenderPanel extends JPanel { glContextManager.executeInGLContext(() -> toolManagement.switchTool(toolName)); } + public void switchToDefaultTool() { + glContextManager.executeInGLContext(toolManagement::switchToDefaultTool); + } + /** * 切换到液化工具 */ diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/SecondaryVertexPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/SecondaryVertexPanel.java new file mode 100644 index 0000000..9a1190d --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/SecondaryVertexPanel.java @@ -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())); + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/KeyboardManager.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/KeyboardManager.java index ccdcb35..8d9a52a 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/KeyboardManager.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/KeyboardManager.java @@ -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; diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ToolManagement.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ToolManagement.java index fcb5b98..6af39d2 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ToolManagement.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ToolManagement.java @@ -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 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() : "无"); } -} +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/VertexDeformationTool.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/VertexDeformationTool.java index d2ce4e4..d1e3464 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/VertexDeformationTool.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/VertexDeformationTool.java @@ -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 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; } -} +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/window/MainWindow.java b/src/main/java/com/chuangzhou/vivid2D/window/MainWindow.java index 784bc7f..c4b94c0 100644 --- a/src/main/java/com/chuangzhou/vivid2D/window/MainWindow.java +++ b/src/main/java/com/chuangzhou/vivid2D/window/MainWindow.java @@ -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() { + @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() {