From bec9ccf64f35c6540ee71baac6fe57ab12b10eb3 Mon Sep 17 00:00:00 2001 From: tzdwindows 7 <3076584115@qq.com> Date: Sat, 8 Nov 2025 10:34:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(vivid2D):=20=E5=AE=9E=E7=8E=B0=E5=A4=9A?= =?UTF-8?q?=E9=80=89=E5=9B=BE=E5=B1=82=E4=B8=8E=E6=96=87=E4=BB=B6=E6=8B=96?= =?UTF-8?q?=E6=94=BE=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 JnaFileChooser 库支持,替换原有 JFileChooser - 实现图层面板的多选功能与批量操作 - 支持通过拖放方式导入 PSD 和图片文件 - 新增新建模型功能,完善文件菜单选项 -优化模型加载逻辑,支持直接加载 Model2D 对象 - 重构图层重排序逻辑,支持多图层块移动- 改进鼠标点击与悬停事件处理机制 - 修复图层操作后选中状态与缩略图刷新问题 - 添加命令行启动任务 runBoxClient与 runVivid2DClient - 升级主窗口初始化流程与界面组件配置 --- build.gradle | 16 +- .../java/com/chuangzhou/vivid2D/Main.java | 16 +- .../java/com/chuangzhou/vivid2D/Vivid2D.java | 15 + .../render/awt/ModelClickListener.java | 19 - .../vivid2D/render/awt/ModelLayerPanel.java | 349 ++++++++++++++---- .../vivid2D/render/awt/ModelRenderPanel.java | 25 +- .../render/awt/manager/GLContextManager.java | 42 +-- .../render/awt/tools/SelectionTool.java | 22 +- .../renderer/LayerReorderTransferHandler.java | 38 +- .../chuangzhou/vivid2D/window/MainWindow.java | 163 +++++--- 10 files changed, 517 insertions(+), 188 deletions(-) create mode 100644 src/main/java/com/chuangzhou/vivid2D/Vivid2D.java diff --git a/build.gradle b/build.gradle index c03c720..2f1936f 100644 --- a/build.gradle +++ b/build.gradle @@ -46,6 +46,10 @@ dependencies { // === 开发工具 === developmentOnly 'org.springframework.boot:spring-boot-devtools' + // === JnaFileChooser 库 === + implementation 'com.github.steos.jnafilechooser:jnafilechooser-api:1.1.2' + implementation 'com.github.steos.jnafilechooser:jnafilechooser-win32:1.1.2' + // === 本地库文件 === implementation files('libs/JNC-1.0-jnc.jar') implementation files('libs/dog api 1.3.jar') @@ -247,7 +251,7 @@ application { mainClass = 'com.axis.innovators.box.Main' } -tasks.register('runClient', JavaExec) { +tasks.register('runBoxClient', JavaExec) { group = "run-toolboxProgram" description = "执行工具箱程序" classpath = sourceSets.main.runtimeClasspath @@ -258,6 +262,16 @@ tasks.register('runClient', JavaExec) { ] } +tasks.register('runVivid2DClient', JavaExec) { + group = "run-vivid2D" + description = "执行工具箱程序" + classpath = sourceSets.main.runtimeClasspath + mainClass = "com.chuangzhou.vivid2D.Main" + jvmArgs = [ + "-Dfile.encoding=UTF-8" + ] +} + tasks.register('test2DModelLayerPanel', JavaExec) { group = "test-model" description = "运行 2D Model Layer Panel 测试" diff --git a/src/main/java/com/chuangzhou/vivid2D/Main.java b/src/main/java/com/chuangzhou/vivid2D/Main.java index 944a222..ff667bd 100644 --- a/src/main/java/com/chuangzhou/vivid2D/Main.java +++ b/src/main/java/com/chuangzhou/vivid2D/Main.java @@ -1,8 +1,20 @@ package com.chuangzhou.vivid2D; +import com.chuangzhou.vivid2D.window.MainWindow; +import com.formdev.flatlaf.themes.FlatMacDarkLaf; + +import javax.swing.*; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; + public class Main { - public static void main(String[] args) { - + FlatMacDarkLaf.setup(); + System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8)); + System.setErr(new PrintStream(System.err, true, StandardCharsets.UTF_8)); + SwingUtilities.invokeLater(() -> { + MainWindow mainWin = new MainWindow(); + mainWin.setVisible(true); + }); } } diff --git a/src/main/java/com/chuangzhou/vivid2D/Vivid2D.java b/src/main/java/com/chuangzhou/vivid2D/Vivid2D.java new file mode 100644 index 0000000..ad1d80a --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/Vivid2D.java @@ -0,0 +1,15 @@ +package com.chuangzhou.vivid2D; + +import com.chuangzhou.vivid2D.window.MainWindow; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class Vivid2D { + private static final Logger logger = LogManager.getLogger(Vivid2D.class); + private static final String VERSIONS = "0.0.1"; + private static final String[] AUTHOR = new String[]{ + "tzdwindows 7" + }; + + private MainWindow mainWindow; +} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelClickListener.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelClickListener.java index 3b85cf1..4989614 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelClickListener.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelClickListener.java @@ -31,23 +31,4 @@ public interface ModelClickListener { */ default void onModelHover(Mesh2D mesh, float modelX, float modelY, int screenX, int screenY) { } - - default void onLiquifyModeExited() { - } - - default void onLiquifyModeEntered(Mesh2D targetMesh, ModelPart liquifyTargetPart) { - } - - default void onSecondaryVertexModeEntered(Mesh2D secondaryVertexTargetMesh) { - } - - default void onSecondaryVertexModeExited() { - } - - default void onPuppetModeEntered(Mesh2D puppetTargetMesh) { - } - - default void onPuppetModeExited() { - } - } 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 7ed0082..eaa68bb 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java @@ -29,8 +29,16 @@ import java.io.ObjectInputStream; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; +// 引入 JnaFileChooser +import jnafilechooser.api.JnaFileChooser; +// 引入拖放相关的类 +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; +import javax.swing.TransferHandler; + public class ModelLayerPanel extends JPanel { private Model2D model; @@ -90,6 +98,11 @@ public class ModelLayerPanel extends JPanel { setupModernLookAndFeel(); this.thumbnailManager = new ThumbnailManager(renderPanel); + + // --- 新增:设置外部文件拖放处理器 --- + this.setTransferHandler(new FileDropTransferHandler()); + // --------------------------------- + if (this.model != null) { this.psdImporter = new PSDImporter(model, renderPanel, this); this.operationManager = new LayerOperationManager(model); @@ -143,10 +156,11 @@ public class ModelLayerPanel extends JPanel { layerList.repaint(); } + // 修正:支持多选,刷新第一个选中项的缩略图 private void refreshSelectedThumbnail() { - ModelPart selected = layerList.getSelectedValue(); - if (selected != null) { - thumbnailManager.generateThumbnail(selected); + List selected = layerList.getSelectedValuesList(); + if (!selected.isEmpty()) { + thumbnailManager.generateThumbnail(selected.get(0)); layerList.repaint(); } } @@ -181,7 +195,8 @@ public class ModelLayerPanel extends JPanel { private JList createModernList() { JList list = new JList<>(listModel); - list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + // 【修正 1:启用多选】 + list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); list.setBackground(SURFACE_COLOR); list.setForeground(TEXT_COLOR); list.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4)); @@ -190,6 +205,7 @@ public class ModelLayerPanel extends JPanel { cellRenderer.attachMouseListener(list, listModel); list.setCellRenderer(cellRenderer); list.setDragEnabled(true); + // 【修正 2:使用多选 TransferHandler】 list.setTransferHandler(new LayerReorderTransferHandler(this)); list.setDropMode(DropMode.INSERT); list.addMouseListener(new MouseAdapter() { @@ -367,23 +383,9 @@ public class ModelLayerPanel extends JPanel { addMenu.show(addButton, 0, addButton.getHeight()); } - // ... (createEmptyPart, findPartByName, getModelPartMap, showRenameDialog, setModel, setRenderPanel, importPSDFile... 逻辑不变) ... - // ... (这些方法的核心逻辑与UI无关,保留原样) ... - - // [逻辑代码... 从第 303 行到 816 行,保留您原始文件中的所有逻辑方法] - // [例如: createEmptyPart, findPartByName, ... , createPartWithTransparentTexture] - - // ==================================================================== - // 您的所有业务逻辑方法 (createEmptyPart, onRemoveLayer, bindTexture... 等) - // 都应该在这里,保持不变。 - // 为了简洁,我只复制了UI重构相关的部分和几个关键方法, - // 您需要将您文件中的所有业务逻辑方法复制回这个类中。 - // ==================================================================== - - // --- 示例:复制几个关键方法 --- - public void reloadFromModel() { - ModelPart selected = layerList.getSelectedValue(); + // 修正:记录所有选中项 + List selectedParts = layerList.getSelectedValuesList(); listModel.clear(); if (model == null) return; @@ -399,16 +401,11 @@ public class ModelLayerPanel extends JPanel { ex.printStackTrace(); } - if (selected != null) { - for (int i = 0; i < listModel.getSize(); i++) { - if (listModel.get(i) == selected) { - layerList.setSelectedIndex(i); - break; - } - } - } + // 修正:重新选中之前选中的图层块 + setSelectedLayers(selectedParts); } + // 原始的单选拖拽逻辑 (为兼容老版本保留,但现在应主要使用 performBlockReorder) public void performVisualReorder(int visualFrom, int visualTo) { if (model == null) return; try { @@ -429,42 +426,144 @@ public class ModelLayerPanel extends JPanel { moved = visual.remove(visualFrom); visual.add(visualTo, moved); - ignoreSliderEvents = true; - try { - listModel.clear(); - for (ModelPart p : visual) listModel.addElement(p); - } finally { - ignoreSliderEvents = false; - } + // 使用新的辅助方法更新 UI 和模型 + updateModelAndUIFromVisualList(visual, List.of(moved)); - operationManager.moveLayer(visual); - selectPart(moved); } catch (Exception ex) { ex.printStackTrace(); } } - // (请确保您原始文件中的所有其他方法, - // 如 onRemoveLayer, moveSelectedUp, createPartWithTextureFromFile, - // endDragOperation, bindTextureToSelectedPart 等,都复制到这里) + /** + * 【新增方法】执行多选拖拽后的图层块重排序操作。 + * 供 LayerReorderTransferHandler 调用。 + * @param srcIndices 列表中的视觉源索引数组(从上到下,已排序)。 + * @param dropIndex 列表中的视觉目标插入索引。 + */ + public void performBlockReorder(int[] srcIndices, int dropIndex) { + if (model == null || srcIndices.length == 0) return; - // ... (所有其他逻辑方法) ... + // 1. 获取当前的视觉图层列表 + List visualList = new ArrayList<>(listModel.size()); + for (int i = 0; i < listModel.size(); i++) visualList.add(listModel.get(i)); + // 2. 识别并提取要移动的 ModelPart 块 + List partsToMove = new ArrayList<>(srcIndices.length); + for (int index : srcIndices) { + partsToMove.add(listModel.getElementAt(index)); + } + + // 3. 从列表中移除要移动的块 + visualList.removeAll(partsToMove); + + // 4. 计算实际插入点 (新的列表大小) + int newDropIndex = dropIndex; + + // newDropIndex 不超过新的列表大小 + newDropIndex = Math.min(newDropIndex, visualList.size()); + + // 5. 将块插入到新的位置 + visualList.addAll(newDropIndex, partsToMove); + + // 6. 更新模型和UI + updateModelAndUIFromVisualList(visualList, partsToMove); + } private void updateUIState() { - ModelPart sel = layerList.getSelectedValue(); - boolean hasSelection = sel != null; + // 修正:支持多选 + List selected = layerList.getSelectedValuesList(); + boolean hasSelection = !selected.isEmpty(); + boolean singleSelection = selected.size() == 1; - if (hasSelection) { - updateOpacitySlider(sel); + if (singleSelection) { + updateOpacitySlider(selected.get(0)); + } else { + // 多选或未选中时,重置不透明度滑块UI + ignoreSliderEvents = true; + opacitySlider.setValue(100); + opacityValueLabel.setText("---"); + ignoreSliderEvents = false; } removeButton.setEnabled(hasSelection); + // 【修正 3:多选时启用上下移动按钮】 upButton.setEnabled(hasSelection); downButton.setEnabled(hasSelection); - bindTextureButton.setEnabled(hasSelection); + // 绑定贴图仍然只在单选时有意义 + bindTextureButton.setEnabled(singleSelection); } + /** + * 【新增辅助方法】更新模型和UI,并重新选中块。 + */ + private void updateModelAndUIFromVisualList(List visualList, List selectedParts) { + // 刷新模型:这一步是关键,它更新了 model.getParts() 的内部顺序 + operationManager.moveLayer(visualList); + + // 刷新列表模型 (UI) + ignoreSliderEvents = true; + listModel.clear(); + for (ModelPart p : visualList) { + listModel.addElement(p); + } + ignoreSliderEvents = false; + + // 重新选中块 + setSelectedLayers(selectedParts); + + // 刷新缩略图 + if (!selectedParts.isEmpty()) { + refreshSelectedThumbnail(); + } + } + + /** + * 将指定的图层块作为整体重新选中。 + * 供外部调用,用于在模型操作后设置当前选中的多图层。 + * @param parts 要选中的 ModelPart 列表。 + */ + public void setSelectedLayers(List parts) { + if (!SwingUtilities.isEventDispatchThread()) { + SwingUtilities.invokeLater(() -> setSelectedLayers(parts)); + return; + } + if (parts.isEmpty()) { + layerList.clearSelection(); + return; + } + List indicesList = new ArrayList<>(parts.size()); + for (int i = 0; i < listModel.getSize(); i++) { + if (parts.contains(listModel.getElementAt(i))) { + indicesList.add(i); + } + } + if (!indicesList.isEmpty()) { + int[] indices = indicesList.stream().mapToInt(i->i).toArray(); + int[] currentIndices = layerList.getSelectedIndices(); + if (Arrays.equals(currentIndices, indices)) { + return; + } + layerList.setIgnoreRepaint(true); + try { + ListSelectionModel selectionModel = layerList.getSelectionModel(); + selectionModel.setValueIsAdjusting(true); + try { + selectionModel.clearSelection(); + for (int index : indices) { + selectionModel.addSelectionInterval(index, index); + } + } finally { + selectionModel.setValueIsAdjusting(false); + } + layerList.ensureIndexIsVisible(indices[0]); + } finally { + layerList.setIgnoreRepaint(false); + layerList.repaint(); + } + } + } + + private void updateOpacitySlider(ModelPart part) { float opacity = extractOpacity(part); int value = Math.round(opacity * 100); @@ -508,9 +607,6 @@ public class ModelLayerPanel extends JPanel { refreshSelectedThumbnail(); } - // (其他所有逻辑方法... setPartOpacity, createEmptyPart, etc.) - // (确保从您的原始文件中复制所有剩余的方法) - private void createEmptyPart() { String name = JOptionPane.showInputDialog(this, "新图层名称:", "新图层"); if (name == null || name.trim().isEmpty()) return; @@ -568,11 +664,17 @@ public class ModelLayerPanel extends JPanel { this.psdImporter = new PSDImporter(model, panel, this); } + /** + * 【JnaFileChooser 替换】从文件选择器导入 PSD 文件。 + */ private void importPSDFile() { - JFileChooser chooser = new JFileChooser(); - chooser.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("PSD文件", "psd")); - int r = chooser.showOpenDialog(this); - if (r == JFileChooser.APPROVE_OPTION) { + JnaFileChooser chooser = new JnaFileChooser(); + chooser.setTitle("选择 PSD 文件 (*.psd)"); + chooser.addFilter("PSD文件 (*.psd)", "psd"); + chooser.setMultiSelectionEnabled(false); + chooser.setMode(JnaFileChooser.Mode.Files); + + if (chooser.showOpenDialog(SwingUtilities.getWindowAncestor(this))) { psdImporter.importPSDFile(chooser.getSelectedFile()); } } @@ -639,14 +741,22 @@ public class ModelLayerPanel extends JPanel { return model; } + /** + * 【JnaFileChooser 替换】打开文件选择器,绑定贴图到选中部件。 + */ private void bindTextureToSelectedPart() { ModelPart sel = layerList.getSelectedValue(); if (sel == null) return; - JFileChooser chooser = new JFileChooser(); - int r = chooser.showOpenDialog(this); - if (r != JFileChooser.APPROVE_OPTION) return; + JnaFileChooser chooser = new JnaFileChooser(); + chooser.setTitle("选择贴图文件"); + chooser.addFilter("图片文件 (*.png, *.jpg, *.jpeg)", "png", "jpg", "jpeg"); + chooser.setMode(JnaFileChooser.Mode.Files); + + if (!chooser.showOpenDialog(SwingUtilities.getWindowAncestor(this))) return; + File f = chooser.getSelectedFile(); + try { BufferedImage img = null; try { @@ -712,14 +822,24 @@ public class ModelLayerPanel extends JPanel { } private void onRemoveLayer() { - ModelPart sel = layerList.getSelectedValue(); - if (sel == null) return; - int r = JOptionPane.showConfirmDialog(this, "确认删除图层:" + sel.getName() + " ?", "确认删除", JOptionPane.YES_NO_OPTION); + // 修正:支持删除多个选中的图层 + List selectedParts = layerList.getSelectedValuesList(); + if (selectedParts.isEmpty()) return; + + String names = selectedParts.stream().map(ModelPart::getName).collect(java.util.stream.Collectors.joining("、")); + int r = JOptionPane.showConfirmDialog(this, "确认删除图层:" + names + " ?", "确认删除", JOptionPane.YES_NO_OPTION); if (r != JOptionPane.YES_OPTION) return; try { - operationManager.removeLayer(sel); - thumbnailManager.removeThumbnail(sel); + for(ModelPart part : selectedParts) { + operationManager.removeLayer(part); + thumbnailManager.removeThumbnail(part); + // 仅移除第一个选中项的参数管理(这是一个简化,实际应用中可能需要遍历移除) + if (part == selectedParts.get(0)) { + renderPanel.getParametersManagement().removeParameter(part, "all"); + renderPanel.getGlContextManager().executeInGLContext(() -> renderPanel.getParametersManagement().removeParameter(part, "all")); + } + } reloadFromModel(); } catch (Exception ex) { JOptionPane.showMessageDialog(this, "删除失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); @@ -727,22 +847,56 @@ public class ModelLayerPanel extends JPanel { } private void moveSelectedUp() { - int idx = layerList.getSelectedIndex(); - if (idx <= 0) return; - performVisualReorder(idx, idx - 1); + moveSelectedBlock(-1); } private void moveSelectedDown() { - int idx = layerList.getSelectedIndex(); - if (idx < 0 || idx >= listModel.getSize() - 1) return; - performVisualReorder(idx, idx + 1); + moveSelectedBlock(1); } + /** + * 【新增方法】将选中的图层块作为一个整体上移/下移一位。 + * @param direction -1 (上移) 或 1 (下移) + */ + private void moveSelectedBlock(int direction) { + List selectedParts = layerList.getSelectedValuesList(); + if (selectedParts.isEmpty()) return; + + int minIndex = layerList.getMinSelectionIndex(); + int maxIndex = layerList.getMaxSelectionIndex(); + + if (direction == -1) { // 向上移动 + if (minIndex <= 0) return; + // 目标位置是 minIndex - 1 + performBlockReorder(layerList.getSelectedIndices(), minIndex - 1); + } else { // 向下移动 + if (maxIndex >= listModel.getSize() - 1) return; + // 目标位置是 maxIndex + 1 (即在 maxIndex 所在的块后插入) + performBlockReorder(layerList.getSelectedIndices(), maxIndex + 1); + } + } + + + /** + * 【JnaFileChooser 替换】打开文件选择器,从文件创建图层。 + */ private void createPartWithTextureFromFile() { - JFileChooser chooser = new JFileChooser(); - int r = chooser.showOpenDialog(this); - if (r != JFileChooser.APPROVE_OPTION) return; + JnaFileChooser chooser = new JnaFileChooser(); + chooser.setTitle("选择图片文件创建图层"); + chooser.addFilter("图片文件 (*.png, *.jpg, *.jpeg)", "png", "jpg", "jpeg"); + chooser.setMode(JnaFileChooser.Mode.Files); + + if (!chooser.showOpenDialog(SwingUtilities.getWindowAncestor(this))) return; File f = chooser.getSelectedFile(); + + createPartWithTextureFromFile(f); + } + + /** + * 【重构核心逻辑】从指定文件创建图层的核心逻辑。供文件选择器和拖放使用。 + * @param f 图片文件 + */ + private void createPartWithTextureFromFile(File f) { try { BufferedImage img = ImageIO.read(f); if (img == null) throw new IOException("无法读取图片:" + f.getAbsolutePath()); @@ -795,6 +949,7 @@ public class ModelLayerPanel extends JPanel { } } + public void endDragOperation() { if (isDragging && draggedPart != null && dragStartPosition != null) { Vector2f endPosition = draggedPart.getPosition(); @@ -854,6 +1009,60 @@ public class ModelLayerPanel extends JPanel { // 现代化的内部UI类 // ==================================================================== + /** + * 【新增】处理外部文件拖放的 TransferHandler。 + * 支持拖放单个 .psd 或图片文件来创建图层。 + */ + private class FileDropTransferHandler extends TransferHandler { + private final List IMAGE_EXTENSIONS = List.of("png", "jpg", "jpeg"); + private static final String PSD_EXTENSION = "psd"; + + @Override + public boolean canImport(TransferSupport support) { + // 检查是否支持文件列表数据格式 + return support.isDataFlavorSupported(DataFlavor.javaFileListFlavor); + } + + @Override + public boolean importData(TransferSupport support) { + if (!canImport(support)) return false; + + try { + @SuppressWarnings("unchecked") + List files = (List) support.getTransferable().getTransferData(DataFlavor.javaFileListFlavor); + + if (files.size() != 1) { + // 仅支持拖放单个文件 + JOptionPane.showMessageDialog(ModelLayerPanel.this, "仅支持拖放单个文件。", "导入失败", JOptionPane.WARNING_MESSAGE); + return false; + } + + File file = files.get(0); + String fileName = file.getName().toLowerCase(); + String extension = fileName.substring(fileName.lastIndexOf('.') + 1); + + if (PSD_EXTENSION.equals(extension)) { + // 导入 PSD 文件 + psdImporter.importPSDFile(file); + return true; + } else if (IMAGE_EXTENSIONS.contains(extension)) { + // 创建带贴图的图层 + createPartWithTextureFromFile(file); + return true; + } else { + JOptionPane.showMessageDialog(ModelLayerPanel.this, "不支持的文件类型: ." + extension, "导入失败", JOptionPane.WARNING_MESSAGE); + return false; + } + + } catch (Exception ex) { + JOptionPane.showMessageDialog(ModelLayerPanel.this, "文件导入失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); + ex.printStackTrace(); + return false; + } + } + } + + /** * 现代化的圆角按钮 */ 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 51c359f..d2fc74b 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java @@ -443,10 +443,9 @@ public class ModelRenderPanel extends JPanel { logger.debug("点击位置:({}, {})", modelX, modelY); - // 触发点击事件 for (ModelClickListener listener : clickListeners) { try { - listener.onModelClicked(null, modelX, modelY, screenX, screenY); + listener.onModelClicked(getSelectedMesh(), modelX, modelY, screenX, screenY); } catch (Exception ex) { logger.error("点击事件监听器执行出错", ex); } @@ -461,7 +460,6 @@ public class ModelRenderPanel extends JPanel { toolManagement.handleMouseClicked(e, modelCoords[0], modelCoords[1]); doubleClickTimer.restart(); } - doubleClickTimer.restart(); } } @@ -479,11 +477,19 @@ public class ModelRenderPanel extends JPanel { } float[] modelCoords = worldManagement.screenToModelCoordinates(screenX, screenY); + float modelX = modelCoords[0]; + float modelY = modelCoords[1]; + for (ModelClickListener listener : clickListeners) { + try { + listener.onModelHover(getSelectedMesh(), modelX, modelY, screenX, screenY); + } catch (Exception ex) { + logger.error("点击事件监听器执行出错", ex); + } + } // 如果有激活的工具,优先交给工具处理 if (toolManagement.hasActiveTool() && modelCoords != null) { toolManagement.handleMouseMoved(e, modelCoords[0], modelCoords[1]); - return; } } @@ -577,6 +583,17 @@ public class ModelRenderPanel extends JPanel { }); } + /** + * 加载模型 + */ + public void loadModel(Model2D model) { + glContextManager.loadModel(model); + this.modelRef.set(model); + resetPostLoadState(model); + modelsUpdate(model); + logger.info("ModelRenderPanel 模型更新完成,工具状态已重置。"); + } + /** * 重置加载新模型后需要清理或初始化的状态。 */ diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/GLContextManager.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/GLContextManager.java index d9f9a61..bb8830b 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/GLContextManager.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/GLContextManager.java @@ -592,59 +592,51 @@ public class GLContextManager { * @return 包含加载完成的模型对象的 CompletableFuture,可用于获取加载结果或处理错误。 */ public CompletableFuture loadModel(String newModelPath) { - // 使用 executeInGLContext(Callable) 确保模型加载在 GL 线程上进行,并返回结果 return executeInGLContext(() -> { Model2D model; - try { if (newModelPath != null && !newModelPath.isEmpty()) { - // 尝试从文件中加载模型 model = Model2D.loadFromFile(newModelPath); logger.info("动态加载模型成功: {}", newModelPath); } else { - // 如果路径为空,创建一个默认空模型 model = new Model2D("新的空项目"); logger.info("创建新的空模型项目"); } - - // 1. 更新上下文中的模型路径和模型引用 - this.modelPath = newModelPath; // 更新 modelPath - modelRef.set(model); // 设置新的 Model2D 实例 - - // 2. 确保如果外部调用者正在等待初始模型(通过 waitForModel),它能得到结果 - // 注意:这里我们假设外部主要依赖于这个 loadModel 返回的 Future, - // 但如果 ModelReady 尚未完成,我们让它完成(通常在空启动时发生)。 + this.modelPath = newModelPath; + modelRef.set(model); if (!modelReady.isDone()) { modelReady.complete(model); } - - // 3. 请求重绘,以便立即显示新模型 if (repaintCallback != null) { - // 确保 repaint() 调用返回到 Swing EDT SwingUtilities.invokeLater(repaintCallback::repaint); } - - return model; // 返回加载成功的模型 - + return model; } catch (Throwable e) { logger.error("动态加载模型失败: {}", e.getMessage(), e); - - // 加载失败时,设置一个空的模型以清除渲染画面,避免崩溃 Model2D emptyModel = new Model2D("加载失败"); modelRef.set(emptyModel); - this.modelPath = null; // 清除路径 - - // 确保通知外部调用者加载失败 + this.modelPath = null; if (repaintCallback != null) { SwingUtilities.invokeLater(repaintCallback::repaint); } - - // 抛出异常,让 CompletableFuture 携带失败信息 throw new Exception("模型加载失败: " + e.getMessage(), e); } }); } + public void loadModel(Model2D newModel) { + executeInGLContext(() -> { + modelRef.set(newModel); + if (!modelReady.isDone()) { + modelReady.complete(newModel); + } + if (repaintCallback != null) { + SwingUtilities.invokeLater(repaintCallback::repaint); + } + return newModel; + }); + } + public interface RepaintCallback { void repaint(); } diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/SelectionTool.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/SelectionTool.java index 12a8c70..a5993cf 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/SelectionTool.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/tools/SelectionTool.java @@ -26,6 +26,7 @@ public class SelectionTool extends Tool { // 选择工具专用字段 private volatile Mesh2D hoveredMesh = null; private final Set selectedMeshes = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private volatile List callQueue = new LinkedList<>(); private volatile Mesh2D lastSelectedMesh = null; private volatile ModelPart draggedPart = null; private volatile float dragStartX, dragStartY; @@ -56,10 +57,23 @@ public class SelectionTool extends Tool { @Override public void deactivate() { isActive = false; - // 清理选择状态 clearSelectedMeshes(); } + public void addCall(Call call){ + callQueue.add(call); + } + + public void removeCall(Call call){ + callQueue.remove(call); + } + + private void runCall(List meshes){ + for (Call call : callQueue) { + call.call(meshes); + } + } + @Override public void onMousePressed(MouseEvent e, float modelX, float modelY) { if (!renderPanel.getGlContextManager().isContextInitialized()) return; @@ -717,6 +731,7 @@ public class SelectionTool extends Tool { selectedMeshes.clear(); if (mesh != null) { mesh.setSelected(true); + runCall(List.of(mesh)); selectedMeshes.add(mesh); lastSelectedMesh = mesh; updateMultiSelectionInMeshes(); @@ -734,6 +749,7 @@ public class SelectionTool extends Tool { if (mesh != null && !selectedMeshes.contains(mesh)) { mesh.setSelected(true); selectedMeshes.add(mesh); + runCall(new ArrayList<>(selectedMeshes)); lastSelectedMesh = mesh; ModelPart part = findPartByMesh(mesh); if (part != null) { @@ -1066,4 +1082,8 @@ public class SelectionTool extends Tool { public Mesh2D getHoveredMesh() { return hoveredMesh; } + + public interface Call { + void call(List mesh); + } } \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/renderer/LayerReorderTransferHandler.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/renderer/LayerReorderTransferHandler.java index 084443d..72d6a0f 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/renderer/LayerReorderTransferHandler.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/renderer/LayerReorderTransferHandler.java @@ -6,6 +6,8 @@ import javax.swing.*; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; +import java.util.Arrays; +import java.util.stream.Collectors; public class LayerReorderTransferHandler extends TransferHandler { private final ModelLayerPanel layerPanel; @@ -15,13 +17,20 @@ public class LayerReorderTransferHandler extends TransferHandler { } @Override - protected Transferable createTransferable(JComponent c) { + public Transferable createTransferable(JComponent c) { if (!(c instanceof JList)) return null; JList list = (JList) c; - int src = list.getSelectedIndex(); - if (src < 0) return null; - return new StringSelection(Integer.toString(src)); + // 【修正 1:获取所有选中索引】 + int[] srcIndices = list.getSelectedIndices(); + if (srcIndices.length == 0) return null; + + // 将所有选中索引打包成一个逗号分隔的字符串 + String indexString = Arrays.stream(srcIndices) + .mapToObj(String::valueOf) + .collect(Collectors.joining(",")); + + return new StringSelection(indexString); } @Override @@ -44,14 +53,29 @@ public class LayerReorderTransferHandler extends TransferHandler { JList.DropLocation dl = (JList.DropLocation) support.getDropLocation(); int dropIndex = dl.getIndex(); + // 【修正 2:解析索引字符串,获取所有被拖拽的源索引】 String s = (String) support.getTransferable().getTransferData(DataFlavor.stringFlavor); - int srcIdx = Integer.parseInt(s); + int[] srcIndices = Arrays.stream(s.split(",")) + .mapToInt(Integer::parseInt) + .toArray(); - if (srcIdx == dropIndex || srcIdx + 1 == dropIndex) return false; + if (srcIndices.length == 0) return false; + + // 检查目标位置是否在拖拽的块内 (minSrc < dropIndex <= maxSrc) + int minSrc = srcIndices[0]; + int maxSrc = srcIndices[srcIndices.length - 1]; + + // 如果 dropIndex 落在 (minSrc, maxSrc] 区间内,则阻止拖拽到自身或内部 + if (dropIndex > minSrc && dropIndex <= maxSrc) { + return false; + } + + // 【修正 3:调用 ModelLayerPanel 中的块重排方法】 + layerPanel.performBlockReorder(srcIndices, dropIndex); - layerPanel.performVisualReorder(srcIdx, dropIndex); layerPanel.endDragOperation(); return true; + } catch (Exception ex) { ex.printStackTrace(); } diff --git a/src/main/java/com/chuangzhou/vivid2D/window/MainWindow.java b/src/main/java/com/chuangzhou/vivid2D/window/MainWindow.java index 32b5458..d5cbd1e 100644 --- a/src/main/java/com/chuangzhou/vivid2D/window/MainWindow.java +++ b/src/main/java/com/chuangzhou/vivid2D/window/MainWindow.java @@ -1,17 +1,15 @@ package com.chuangzhou.vivid2D.window; -import com.chuangzhou.vivid2D.render.awt.ModelLayerPanel; -import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel; -import com.chuangzhou.vivid2D.render.awt.ParametersPanel; -import com.chuangzhou.vivid2D.render.awt.TransformPanel; +import com.chuangzhou.vivid2D.render.awt.*; 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.ModelPartInfoPanel; import com.chuangzhou.vivid2D.render.model.Model2D; import com.chuangzhou.vivid2D.render.model.ModelPart; +import com.chuangzhou.vivid2D.render.model.util.Mesh2D; import com.formdev.flatlaf.themes.FlatMacDarkLaf; +import jnafilechooser.api.JnaFileChooser; import org.jetbrains.annotations.NotNull; import javax.swing.*; @@ -43,28 +41,6 @@ public class MainWindow extends JFrame { private JLabel statusBarLabel; private JMenuBar menuBar; - /** - * 启动器。 - */ - public static void main(String[] args) { - // 设置 Look and Feel - try { - UIManager.setLookAndFeel(new FlatMacDarkLaf()); - } catch (UnsupportedLookAndFeelException e) { - throw new RuntimeException(e); - } - - // 确保控制台输出使用 UTF-8 - System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8)); - System.setErr(new PrintStream(System.err, true, StandardCharsets.UTF_8)); - - // 在 EDT (Event Dispatch Thread) 上创建和显示 GUI - SwingUtilities.invokeLater(() -> { - MainWindow mainWin = new MainWindow(); - mainWin.setVisible(true); - }); - } - /** * 构造主窗口。 */ @@ -72,30 +48,17 @@ public class MainWindow extends JFrame { setTitle("Vivid2D Editor - [未加载文件]"); setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); setLayout(new BorderLayout()); - - // 1. 初始化核心渲染器和面板 - // ModelRenderPanel 传入空路径 "" this.renderPanel = new ModelRenderPanel("", 1024, 768); this.layerPanel = new ModelLayerPanel(renderPanel); this.transformPanel = new TransformPanel(renderPanel); this.parametersPanel = new ParametersPanel(renderPanel); - // 【重要】使用我们新实现的 ModelPartInfoPanel this.partInfoPanel = new ModelPartInfoPanel(renderPanel); - - // 关联参数管理器 - //renderPanel.setParametersManagement(ParametersManagement.getInstance(parametersPanel)); - - // 2. 构建模块化的 UI createMenuBar(); createToolBar(); createMainLayout(); createStatusBar(); - - // 3. 设置初始状态:所有编辑功能禁用 setEditComponentsEnabled(false); setupInitialListeners(); - - // 4. 设置窗口 setSize(1600, 900); setLocationRelativeTo(null); } @@ -106,17 +69,26 @@ public class MainWindow extends JFrame { private void createMenuBar() { menuBar = new JMenuBar(); JMenu fileMenu = new JMenu("文件"); + + // 新增:新建模型菜单项 + JMenuItem newItem = new JMenuItem("新建模型..."); + newItem.addActionListener(e -> createNewModel()); + fileMenu.add(newItem); + JMenuItem openItem = new JMenuItem("打开模型..."); openItem.addActionListener(e -> openModelFile()); fileMenu.add(openItem); fileMenu.addSeparator(); + JMenuItem saveItem = new JMenuItem("保存"); saveItem.setName("saveItem"); saveItem.addActionListener(e -> saveData(false)); fileMenu.add(saveItem); + JMenuItem exitItem = new JMenuItem("退出"); exitItem.addActionListener(e -> dispatchEvent(new WindowEvent(this, WindowEvent.WINDOW_CLOSING))); fileMenu.add(exitItem); + menuBar.add(fileMenu); JMenu editMenu = new JMenu("编辑"); editMenu.setName("editMenu"); @@ -125,6 +97,45 @@ public class MainWindow extends JFrame { setJMenuBar(menuBar); } + /** + * 处理新建模型的操作。 + */ + private void createNewModel() { + String modelName = JOptionPane.showInputDialog(this, "请输入新模型的名称:", "新建模型", JOptionPane.PLAIN_MESSAGE); + if (modelName != null && !modelName.trim().isEmpty()) { + modelName = modelName.trim(); + String finalModelName = modelName; + SwingUtilities.invokeLater(() -> { + Model2D newModel = new Model2D(finalModelName); + setEditComponentsEnabled(false); + statusBarLabel.setText("正在创建并加载新模型: " + finalModelName); + try { + renderPanel.loadModel(newModel); + renderPanel.setParametersManagement(ParametersManagement.getInstance(parametersPanel)); + layerPanel.loadMetadata(); + currentModelPath = null; + setTitle("Vivid2D Editor - " + finalModelName + " [新建]"); + statusBarLabel.setText("新模型 " + finalModelName + " 创建并加载完毕。"); + setEditComponentsEnabled(true); + layerPanel.setModel(newModel); + } catch (Exception e) { + System.err.println("新建模型加载失败: " + e.getMessage()); + currentModelPath = null; + setTitle("Vivid2D Editor - [加载失败]"); + statusBarLabel.setText("新模型加载失败!无法加载: " + finalModelName); + JOptionPane.showMessageDialog(this, + "无法加载新模型: " + finalModelName + "\n错误: " + e.getMessage(), + "加载错误", + JOptionPane.ERROR_MESSAGE); + setEditComponentsEnabled(false); + } + }); + } else if (modelName != null) { + JOptionPane.showMessageDialog(this, "模型名称不能为空。", "输入错误", JOptionPane.WARNING_MESSAGE); + } + } + + /** * 创建顶部工具栏。 */ @@ -225,16 +236,25 @@ public class MainWindow extends JFrame { } }); - renderPanel.addModelClickListener((mesh, modelX, modelY, screenX, screenY) -> { - List selectedPart = renderPanel.getSelectedParts(); - SwingUtilities.invokeLater(() -> { - transformPanel.setSelectedParts(selectedPart); - if (!selectedPart.isEmpty()) { - partInfoPanel.updatePanel(selectedPart.get(0)); - } else { - partInfoPanel.updatePanel(null); - } - }); + renderPanel.addModelClickListener(new ModelClickListener() { + @Override + public void onModelClicked(Mesh2D mesh, float modelX, float modelY, int screenX, int screenY) { + List selectedPart = renderPanel.getSelectedParts(); + SwingUtilities.invokeLater(() -> { + layerPanel.setSelectedLayers(selectedPart); + transformPanel.setSelectedParts(selectedPart); + if (!selectedPart.isEmpty()) { + partInfoPanel.updatePanel(selectedPart.get(0)); + } else { + partInfoPanel.updatePanel(null); + } + }); + } + + @Override + public void onModelHover(Mesh2D mesh, float modelX, float modelY, int screenX, int screenY) { + onModelClicked(mesh, modelX, modelY, screenX, screenY); + } }); } @@ -274,13 +294,13 @@ public class MainWindow extends JFrame { * 打开文件对话框并加载模型。 */ private void openModelFile() { - JFileChooser fileChooser = new JFileChooser(); - fileChooser.setDialogTitle("选择 Vivid2D 模型文件 (*.model)"); - FileNameExtensionFilter filter = new FileNameExtensionFilter("Vivid2D 模型文件 (*.model)", "model"); - fileChooser.setFileFilter(filter); - fileChooser.setAcceptAllFileFilterUsed(false); // 这一行可选,用于禁用 "All Files" 选项 - if (fileChooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) { - File file = fileChooser.getSelectedFile(); + JnaFileChooser jnaFileChooser = new JnaFileChooser(); + jnaFileChooser.setTitle("选择 Vivid2D 模型文件 (*.model)"); + jnaFileChooser.addFilter("Vivid2D 模型文件 (*.model)", "model"); + jnaFileChooser.setMultiSelectionEnabled(false); + jnaFileChooser.setMode(JnaFileChooser.Mode.Files); + if (jnaFileChooser.showOpenDialog(this)) { + File file = jnaFileChooser.getSelectedFile(); loadModel(file.getAbsolutePath()); } } @@ -294,6 +314,7 @@ public class MainWindow extends JFrame { CompletableFuture.runAsync(() -> { Model2D model = null; try { + // 假设 renderPanel.loadModel(String modelPath) 返回一个 CompletableFuture model = renderPanel.loadModel(modelPath).get(); } catch (InterruptedException | ExecutionException e) { System.err.println("模型异步加载失败: " + e.getMessage()); @@ -327,8 +348,32 @@ public class MainWindow extends JFrame { */ private void saveData(boolean exitOnComplete) { if (currentModelPath == null) { - statusBarLabel.setText("没有加载模型,无法保存。"); - return; + JnaFileChooser jnaFileChooser = new JnaFileChooser(); + jnaFileChooser.setTitle("另存为 Vivid2D 模型文件 (*.model)"); + + // JnaFileChooser 使用 addFilter() 来添加过滤器 + jnaFileChooser.addFilter("Vivid2D 模型文件 (*.model)", "model"); + jnaFileChooser.setMultiSelectionEnabled(false); + jnaFileChooser.setOpenButtonText("保存"); + jnaFileChooser.setMode(JnaFileChooser.Mode.Files); + + // 弹出保存对话框 + if (jnaFileChooser.showSaveDialog(this)) { + File fileToSave = jnaFileChooser.getSelectedFile(); + String path = fileToSave.getAbsolutePath(); + + // 确保文件以 .model 结尾 (原生对话框可能已经处理,但 Swing 风格代码保留以防万一) + if (!path.toLowerCase().endsWith(".model")) { + path += ".model"; + fileToSave = new File(path); + } + + this.currentModelPath = path; + setTitle("Vivid2D Editor - " + fileToSave.getName()); + } else { + statusBarLabel.setText("保存操作已取消。"); + return; + } } statusBarLabel.setText("正在保存..."); if (renderPanel.getModel() != null) {