diff --git a/build.gradle b/build.gradle index f8d1001..8de67fc 100644 --- a/build.gradle +++ b/build.gradle @@ -135,6 +135,7 @@ dependencies { implementation 'com.googlecode.soundlibs:jorbis:0.0.17-2' // ogg 依赖 implementation 'cn.dev33:sa-token-spring-boot-starter:1.44.0' + implementation 'com.twelvemonkeys.imageio:imageio-psd:3.12.0' } configurations.all { 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 9bbbb9b..8c21c67 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelLayerPanel.java @@ -1,16 +1,18 @@ package com.chuangzhou.vivid2D.render.awt; +import com.chuangzhou.vivid2D.render.awt.util.OperationHistoryManager; +import com.chuangzhou.vivid2D.render.awt.util.PsdParser; 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.Texture; +import org.joml.Vector2f; import org.lwjgl.system.MemoryUtil; import javax.imageio.ImageIO; import javax.swing.*; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; -import javax.swing.plaf.basic.BasicListUI; import java.awt.*; import java.awt.datatransfer.StringSelection; import java.awt.datatransfer.Transferable; @@ -22,7 +24,6 @@ import java.lang.reflect.*; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; @@ -54,6 +55,9 @@ public class ModelLayerPanel extends JPanel { private JSlider opacitySlider; private JLabel opacityValueLabel; + private boolean isDragging = false; + private ModelPart draggedPart = null; + private Vector2f dragStartPosition = null; // 程序性设置滑块时忽略事件,避免错误写回 private volatile boolean ignoreSliderEvents = false; @@ -78,6 +82,380 @@ public class ModelLayerPanel extends JPanel { this.renderPanel = panel; } + // ============== 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) { + importPSDFile(chooser.getSelectedFile()); + } + } + + private void importPSDFile(File psdFile) { + try { + // 使用工具类解析PSD文件 + PsdParser.PSDImportResult result = PsdParser.parsePSDFile(psdFile); + if (result != null && !result.layers.isEmpty()) { + int choice = JOptionPane.showConfirmDialog(this, + String.format("PSD文件包含 %d 个图层,是否全部导入?", result.layers.size()), + "导入PSD图层", JOptionPane.YES_NO_OPTION); + + if (choice == JOptionPane.YES_OPTION) { + importPSDLayers(result); + } + } else { + JOptionPane.showMessageDialog(this, "未找到可导入的PSD图层或解析失败", "提示", JOptionPane.INFORMATION_MESSAGE); + } + } catch (Exception ex) { + ex.printStackTrace(); + JOptionPane.showMessageDialog(this, + "解析PSD文件失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); + } + } + + /** + * 导入PSD图层到模型 + */ + private void importPSDLayers(PsdParser.PSDImportResult result) { + if (renderPanel != null) { + // 使用更可靠的方式在GL上下文中创建纹理 + try { + // 在GL上下文中同步执行所有图层的创建 + renderPanel.executeInGLContext(() -> { + try { + List createdParts = new ArrayList<>(); + + for (PsdParser.PSDLayerInfo layerInfo : result.layers) { + try { + ModelPart part = createPartFromPSDLayer(layerInfo); + if (part != null) { + createdParts.add(part); + } else { + System.err.println("创建图层失败: " + layerInfo.name); + } + } catch (Exception e) { + System.err.println("创建PSD图层异常: " + layerInfo.name + " - " + e.getMessage()); + e.printStackTrace(); + } + } + + // 确保模型更新 + if (model != null) { + model.markNeedsUpdate(); + System.out.println("模型标记为需要更新,已创建 " + createdParts.size() + " 个图层"); + } + + // 在GL线程中立即更新UI + SwingUtilities.invokeLater(() -> { + reloadFromModel(); + if (!createdParts.isEmpty()) { + selectPart(createdParts.get(0)); + } + }); + } catch (Exception e) { + e.printStackTrace(); + SwingUtilities.invokeLater(() -> { + JOptionPane.showMessageDialog(ModelLayerPanel.this, + "导入PSD图层失败: " + e.getMessage(), + "错误", JOptionPane.ERROR_MESSAGE); + }); + } + }); + + } catch (Exception e) { + e.printStackTrace(); + JOptionPane.showMessageDialog(this, + "执行GL上下文任务失败: " + e.getMessage(), + "错误", JOptionPane.ERROR_MESSAGE); + } + + } else { + // 无GL上下文的情况 - 直接创建 + System.out.println("无GL上下文,直接创建PSD图层"); + List createdParts = new ArrayList<>(); + + for (PsdParser.PSDLayerInfo layerInfo : result.layers) { + try { + ModelPart part = createPartFromPSDLayer(layerInfo); + if (part != null) { + createdParts.add(part); + } else { + System.err.println("创建图层失败: " + layerInfo.name); + } + } catch (Exception e) { + System.err.println("创建PSD图层异常: " + layerInfo.name + " - " + e.getMessage()); + e.printStackTrace(); + } + } + + if (model != null) { + model.markNeedsUpdate(); + System.out.println("模型标记为需要更新,已创建 " + createdParts.size() + " 个图层"); + } + + reloadFromModel(); + if (!createdParts.isEmpty()) { + selectPart(createdParts.get(0)); + } + + JOptionPane.showMessageDialog(this, + "成功导入 " + createdParts.size() + " 个PSD图层", + "导入完成", JOptionPane.INFORMATION_MESSAGE); + } + } + + private String ensureUniquePartName(String baseName) { + if (model == null) return baseName; + Map partMap = getModelPartMap(); + if (partMap == null) return baseName; + String name = baseName; + int idx = 1; + while (partMap.containsKey(name)) { + name = baseName + "_" + idx++; + } + return name; + } + + /** + * 从PSD图层信息创建部件 - 返回创建的部件 + */ + private ModelPart createPartFromPSDLayer(PsdParser.PSDLayerInfo layerInfo) { + try { + System.out.println("正在创建PSD图层: " + layerInfo.name + " [" + + layerInfo.width + "x" + layerInfo.height + "]" + "[x=" + layerInfo.x + ",y=" + layerInfo.y + "]"); + + // 确保部件名唯一,避免覆盖已有部件导致“合并成一个图层”的问题 + String uniqueName = ensureUniquePartName(layerInfo.name); + + // 创建部件 + ModelPart part = model.createPart(uniqueName); + if (part == null) { + System.err.println("创建部件失败: " + uniqueName); + return null; + } + + // 如果 model 有 partMap,更新映射(防止老实现以 name 为 key 覆盖或冲突) + try { + Map partMap = getModelPartMap(); + if (partMap != null) { + partMap.put(uniqueName, part); + } + } catch (Exception ignored) {} + + part.setVisible(layerInfo.visible); + + // 设置不透明度(优先使用公开方法) + try { + part.setOpacity(layerInfo.opacity); + } catch (Throwable t) { + // 如果没有公开方法,尝试通过反射备用(保持兼容) + try { + Field f = part.getClass().getDeclaredField("opacity"); + f.setAccessible(true); + f.setFloat(part, layerInfo.opacity); + } catch (Throwable ignored) { + System.err.println("设置不透明度失败: " + uniqueName); + } + } + part.setPosition(layerInfo.x, layerInfo.y); + + // 创建网格(使用唯一 mesh 名避免工厂复用同一实例) + long uniq = System.nanoTime(); + Mesh2D mesh = createQuadForImage(layerInfo.image, uniqueName + "_mesh_" + uniq); + + // 把 mesh 加入 part(注意部分实现可能复制或包装 mesh) + part.addMesh(mesh); + + // 创建纹理(使用唯一名称,防止按 name 在内部被复用或覆盖) + String texName = uniqueName + "_tex_" + uniq; + Texture texture = createTextureFromBufferedImage(layerInfo.image, texName); + if (texture != null) { + // 尝试把纹理设置到实际被 part 持有的 mesh 上(取最后一个元素作为刚刚添加的 mesh) + try { + java.util.List partMeshes = part.getMeshes(); + Mesh2D actualMesh = null; + if (partMeshes != null && !partMeshes.isEmpty()) { + actualMesh = partMeshes.get(partMeshes.size() - 1); + } + + if (actualMesh != null) { + actualMesh.setTexture(texture); + } else { + // 兜底:如果没拿到实际 mesh,仍然设置在原始 mesh 上以避免丢失 + mesh.setTexture(texture); + } + + // 把纹理加入 model 管理 + model.addTexture(texture); + + // 强制尝试上传/初始化(若纹理对象需要) + try { + tryCallTextureUpload(texture); + } catch (Throwable ignored) {} + + // 标记模型需要更新 + model.markNeedsUpdate(); + } catch (Throwable e) { + System.err.println("在绑定纹理到 mesh 时出错: " + uniqueName + " - " + e.getMessage()); + e.printStackTrace(); + } + + // 触发 UI/渲染刷新(使用 EDT) + SwingUtilities.invokeLater(() -> { + try { + reloadFromModel(); + } catch (Throwable ignored) {} + try { + if (renderPanel != null) renderPanel.repaint(); + } catch (Throwable ignored) {} + }); + } else { + System.err.println("创建纹理失败: " + uniqueName); + } + + return part; + + } catch (Exception e) { + System.err.println("创建PSD图层部件失败: " + layerInfo.name + " - " + e.getMessage()); + e.printStackTrace(); + return null; + } + } + + private Texture createTextureFromBufferedImage(BufferedImage img, String texName) { + // 在创建纹理前翻转图片 + BufferedImage flippedImage = flipImageVertically(img); + return Texture.createFromBufferedImage(texName, flippedImage); + } + + private BufferedImage flipImageVertically(BufferedImage img) { + int width = img.getWidth(); + int height = img.getHeight(); + BufferedImage flipped = new BufferedImage(width, height, img.getType()); + Graphics2D g = flipped.createGraphics(); + g.drawImage(img, 0, height, width, -height, null); + g.dispose(); + return flipped; + } + + /** + * 将BufferedImage转换为字节数组 + */ + private byte[] bufferedImageToByteArray(BufferedImage img) { + if (img == null) return null; + try { + final int width = img.getWidth(); + final int height = img.getHeight(); + final int len = width * height; + + // 尽量直接取得 int[] 像素数据(避免 getRGB 每像素的开销) + final int[] pixels; + int imgType = img.getType(); + if (imgType == BufferedImage.TYPE_INT_ARGB && img.getRaster().getDataBuffer() instanceof java.awt.image.DataBufferInt) { + pixels = ((java.awt.image.DataBufferInt) img.getRaster().getDataBuffer()).getData(); + } else { + // 转换为 TYPE_INT_ARGB(非预乘),尽量用最近邻以加快绘制 + BufferedImage converted = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = converted.createGraphics(); + try { + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); + g.drawImage(img, 0, 0, null); + } finally { + g.dispose(); + } + pixels = ((java.awt.image.DataBufferInt) converted.getRaster().getDataBuffer()).getData(); + } + + // 输出数组 RGBA 顺序(每像素 4 字节) + byte[] bytes = new byte[len * 4]; + int outIndex = 0; + + // 局部变量加速 + for (int i = 0; i < len; i++) { + int p = pixels[i]; + bytes[outIndex++] = (byte) ((p >> 16) & 0xFF); // R + bytes[outIndex++] = (byte) ((p >> 8) & 0xFF); // G + bytes[outIndex++] = (byte) (p & 0xFF); // B + bytes[outIndex++] = (byte) ((p >> 24) & 0xFF); // A + } + + return bytes; + } catch (Exception e) { + System.err.println("转换BufferedImage到字节数组失败: " + e.getMessage()); + return null; + } + } + + + /** + * 通过构造函数创建纹理 - 增强版本 + */ + private Texture createTextureViaConstructor(BufferedImage img, String texName) { + try { + int w = img.getWidth(); + int h = img.getHeight(); + + // 将BufferedImage转换为ByteBuffer + ByteBuffer buffer = bufferedImageToByteBuffer(img); + if (buffer == null) { + System.err.println("创建ByteBuffer失败: " + texName); + return null; + } + + // 使用Texture类的构造函数 + Texture texture = new Texture(texName, w, h, Texture.TextureFormat.RGBA, buffer); + + // 设置纹理参数 + texture.setMinFilter(Texture.TextureFilter.LINEAR); + texture.setMagFilter(Texture.TextureFilter.LINEAR); + texture.setWrapS(Texture.TextureWrap.CLAMP_TO_EDGE); + texture.setWrapT(Texture.TextureWrap.CLAMP_TO_EDGE); + + // 缓存像素数据以便后续使用 + texture.ensurePixelDataCached(); + + return texture; + + } catch (Exception e) { + System.err.println("通过构造函数创建纹理失败: " + texName + " - " + e.getMessage()); + e.printStackTrace(); + return null; + } + } + + /** + * 将BufferedImage转换为ByteBuffer + */ + private ByteBuffer bufferedImageToByteBuffer(BufferedImage img) { + try { + int width = img.getWidth(); + int height = img.getHeight(); + int[] pixels = new int[width * height]; + img.getRGB(0, 0, width, height, pixels, 0, width); + + ByteBuffer buffer = MemoryUtil.memAlloc(width * height * 4); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int pixel = pixels[y * width + x]; + // RGBA格式 + buffer.put((byte) ((pixel >> 16) & 0xFF)); // R + buffer.put((byte) ((pixel >> 8) & 0xFF)); // G + buffer.put((byte) (pixel & 0xFF)); // B + buffer.put((byte) ((pixel >> 24) & 0xFF)); // A + } + } + buffer.flip(); + return buffer; + + } catch (Exception e) { + System.err.println("转换BufferedImage到ByteBuffer失败: " + e.getMessage()); + return null; + } + } + private void initComponents() { setLayout(new BorderLayout()); listModel = new DefaultListModel<>(); @@ -166,14 +544,18 @@ public class ModelLayerPanel extends JPanel { JMenuItem addBlank = new JMenuItem("创建空图层 (无贴图)"); JMenuItem addWithTexture = new JMenuItem("从文件选择贴图并创建图层"); JMenuItem addTransparent = new JMenuItem("创建透明贴图图层"); + JMenuItem addPSD = new JMenuItem("从PSD文件导入图层"); addMenu.add(addBlank); addMenu.add(addWithTexture); addMenu.add(addTransparent); + addMenu.add(new JSeparator()); + addMenu.add(addPSD); addButton.addActionListener(e -> addMenu.show(addButton, 0, addButton.getHeight())); addBlank.addActionListener(e -> createEmptyPart()); addWithTexture.addActionListener(e -> createPartWithTextureFromFile()); addTransparent.addActionListener(e -> createPartWithTransparentTexture()); + addPSD.addActionListener(e -> importPSDFile()); removeButton = new JButton("-"); removeButton.setToolTipText("删除选中图层"); @@ -265,7 +647,7 @@ public class ModelLayerPanel extends JPanel { // 先创建部件与 Mesh(基于图片尺寸) ModelPart part = model.createPart(name); - part.setPivot(0,0); + //part.setPivot(0,0); Mesh2D mesh = createQuadForImage(img, name + "_mesh"); part.addMesh(mesh); @@ -762,12 +1144,21 @@ public class ModelLayerPanel extends JPanel { if (visualTo < 0) visualTo = 0; if (visualTo > size - 1) visualTo = size - 1; + ModelPart moved = listModel.get(visualFrom); + + // 如果是拖拽操作,设置拖拽状态 + if (!isDragging) { + isDragging = true; + draggedPart = moved; + dragStartPosition = new Vector2f(moved.getPosition()); + } + // 构造新的视觉顺序(arraylist) List visual = new ArrayList<>(size); for (int i = 0; i < size; i++) visual.add(listModel.get(i)); // 移动元素 - ModelPart moved = visual.remove(visualFrom); + moved = visual.remove(visualFrom); visual.add(visualTo, moved); // 更新 listModel(程序性更新,期间设置 ignoreSliderEvents 防止滑块回写) @@ -794,6 +1185,29 @@ public class ModelLayerPanel extends JPanel { } } + private void endDragOperation() { + if (isDragging && draggedPart != null && dragStartPosition != null) { + // 记录拖拽操作 + Vector2f endPosition = draggedPart.getPosition(); + if (!endPosition.equals(dragStartPosition)) { + // 只有在位置确实发生变化时才记录操作 + recordDragOperation(draggedPart, dragStartPosition, endPosition); + } + + // 重置拖拽状态 + isDragging = false; + draggedPart = null; + dragStartPosition = null; + } + } + + private void recordDragOperation(ModelPart part, Vector2f startPos, Vector2f endPos) { + OperationHistoryManager manager = OperationHistoryManager.getInstance(); + if (manager != null) { + manager.recordOperation("DRAG_PART", part, startPos, endPos); + } + } + // ============== 反射读写 Model2D 内部 ============== @SuppressWarnings("unchecked") @@ -944,12 +1358,24 @@ public class ModelLayerPanel extends JPanel { int srcIdx = Integer.parseInt(s); if (srcIdx == dropIndex || srcIdx + 1 == dropIndex) return false; performVisualReorder(srcIdx, dropIndex); + + // 拖拽结束时记录操作 + endDragOperation(); return true; } catch (Exception ex) { ex.printStackTrace(); } return false; } + + @Override + protected void exportDone(JComponent source, Transferable data, int action) { + // 如果拖拽被取消,也结束拖拽操作 + if (action == TransferHandler.NONE) { + endDragOperation(); + } + super.exportDone(source, data, action); + } } // ============== 小工具 ============== @@ -986,4 +1412,72 @@ public class ModelLayerPanel extends JPanel { e.printStackTrace(); } } + + private void debugPrintModelState() { + if (model == null) { + System.out.println("模型为 null"); + return; + } + try { + List parts = model.getParts(); + System.out.println("=== Model Parts: " + (parts == null ? 0 : parts.size()) + " ==="); + if (parts != null) { + for (int i = 0; i < parts.size(); i++) { + ModelPart p = parts.get(i); + String name = p.getName(); + boolean visible = true; + float px = 0, py = 0; + try { + Method gm = p.getClass().getMethod("getPivotX"); + Method gm2 = p.getClass().getMethod("getPivotY"); + px = ((Number) gm.invoke(p)).floatValue(); + py = ((Number) gm2.invoke(p)).floatValue(); + } catch (Exception ignored) { + try { + Field fx = p.getClass().getDeclaredField("pivotX"); + Field fy = p.getClass().getDeclaredField("pivotY"); + fx.setAccessible(true); fy.setAccessible(true); + px = ((Number) fx.get(p)).floatValue(); + py = ((Number) fy.get(p)).floatValue(); + } catch (Exception ignored2) {} + } + try { + Method vm = p.getClass().getMethod("isVisible"); + visible = (Boolean) vm.invoke(p); + } catch (Exception ignored) {} + System.out.println(String.format("Part[%d] name=%s visible=%s pivot=(%.1f, %.1f)", i, name, visible, px, py)); + + // meshes + try { + Method gmsh = p.getClass().getMethod("getMeshes"); + Object list = gmsh.invoke(p); + if (list instanceof List) { + List meshes = (List) list; + System.out.println(" meshes count = " + meshes.size()); + for (int m = 0; m < meshes.size(); m++) { + Object mesh = meshes.get(m); + Object tex = null; + try { + Method gtex = mesh.getClass().getMethod("getTexture"); + tex = gtex.invoke(mesh); + } catch (Exception e) { + try { + Field f = mesh.getClass().getDeclaredField("texture"); + f.setAccessible(true); + tex = f.get(mesh); + } catch (Exception ignored) {} + } + System.out.println(" mesh[" + m + "] texture = " + (tex == null ? "null" : tex.getClass().getSimpleName())); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } \ No newline at end of file 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 0f02ae7..e410d71 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ModelRenderPanel.java @@ -1,6 +1,8 @@ package com.chuangzhou.vivid2D.render.awt; import com.chuangzhou.vivid2D.render.ModelRender; +import com.chuangzhou.vivid2D.render.awt.util.OperationHistoryGlobal; +import com.chuangzhou.vivid2D.render.awt.util.OperationHistoryManager; import com.chuangzhou.vivid2D.render.model.Model2D; import com.chuangzhou.vivid2D.render.model.ModelPart; import com.chuangzhou.vivid2D.render.model.util.BoundingBox; @@ -23,6 +25,8 @@ import javax.swing.*; import java.awt.*; import java.awt.image.BufferedImage; import java.nio.ByteBuffer; +import java.util.*; +import java.util.List; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicReference; @@ -34,7 +38,7 @@ import java.util.concurrent.atomic.AtomicReference; * * * @author tzdwindows 7 - * @version 1.0 + * @version 1.1 * @since 2025-10-13 * @see com.chuangzhou.vivid2D.test.TestModelGLPanel */ @@ -64,7 +68,9 @@ public class ModelRenderPanel extends JPanel { private final CopyOnWriteArrayList clickListeners = new CopyOnWriteArrayList<>(); private volatile Mesh2D hoveredMesh = null; - private volatile Mesh2D selectedMesh = null; + // 修改:将单个选中改为集合,支持多选 + private final Set selectedMeshes = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private volatile Mesh2D lastSelectedMesh = null; // 用于Shift多选的最后一个选中项 private volatile ModelPart draggedPart = null; private volatile float dragStartX, dragStartY; private volatile float partStartX, partStartY; @@ -89,6 +95,7 @@ public class ModelRenderPanel extends JPanel { private volatile float resizeStartWidth, resizeStartHeight; private volatile float resizeStartX, resizeStartY; private volatile boolean shiftPressed = false; + private volatile boolean ctrlPressed = false; // 新增:Ctrl键状态 // 新增:选择框边框厚度和角点大小 public static final float BORDER_THICKNESS = 6.0f; @@ -108,6 +115,17 @@ public class ModelRenderPanel extends JPanel { private volatile float partInitialRotation = 0.0f; private volatile Vector2f rotationCenter = new Vector2f(); private static final float ROTATION_HANDLE_DISTANCE = 30.0f; + private OperationHistoryManager historyManager; + + // 新增:操作历史管理器 + private OperationHistoryGlobal operationHistory; + + // 新增:拖拽操作的状态记录字段 + private Map dragStartPositions = new HashMap<>(); + private Map dragStartScales = new HashMap<>(); + private Map dragStartRotations = new HashMap<>(); + private Map dragStartPivots = new HashMap<>(); + /** * 构造函数:使用模型路径 */ @@ -115,7 +133,9 @@ public class ModelRenderPanel extends JPanel { this.modelPath = modelPath; this.width = width; this.height = height; + this.operationHistory = OperationHistoryGlobal.getInstance(); initialize(); + initKeyboardShortcuts(); } /** @@ -126,7 +146,196 @@ public class ModelRenderPanel extends JPanel { this.width = width; this.height = height; this.modelRef.set(model); + this.operationHistory = OperationHistoryGlobal.getInstance(); initialize(); + initKeyboardShortcuts(); + } + + /** + * 初始化键盘快捷键 + */ + private void initKeyboardShortcuts() { + // 获取输入映射和动作映射 + InputMap inputMap = getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW); + ActionMap actionMap = getActionMap(); + + // 撤回快捷键:Ctrl+Z + KeyStroke undoKey = KeyStroke.getKeyStroke(KeyEvent.VK_Z, KeyEvent.CTRL_DOWN_MASK); + inputMap.put(undoKey, "undo"); + actionMap.put("undo", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + undo(); + } + }); + + // 重做快捷键:Ctrl+Y 或 Ctrl+Shift+Z + KeyStroke redoKey1 = KeyStroke.getKeyStroke(KeyEvent.VK_Y, KeyEvent.CTRL_DOWN_MASK); + KeyStroke redoKey2 = KeyStroke.getKeyStroke(KeyEvent.VK_Z, KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK); + inputMap.put(redoKey1, "redo"); + inputMap.put(redoKey2, "redo"); + actionMap.put("redo", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + redo(); + } + }); + + // 清除历史记录:Ctrl+Shift+Delete + KeyStroke clearHistoryKey = KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK); + inputMap.put(clearHistoryKey, "clearHistory"); + actionMap.put("clearHistory", new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + clearHistory(); + } + }); + } + + // ============ 新增:操作历史记录方法 ============ + + /** + * 记录位置变化操作 + */ + private void recordPositionChange(ModelPart part, Vector2f oldPosition, Vector2f newPosition) { + if (operationHistory != null && part != null) { + operationHistory.recordOperation("SET_POSITION", part, oldPosition, newPosition); + } + } + + /** + * 记录缩放变化操作 + */ + private void recordScaleChange(ModelPart part, Vector2f oldScale, Vector2f newScale) { + if (operationHistory != null && part != null) { + operationHistory.recordOperation("SET_SCALE", part, oldScale, newScale); + } + } + + /** + * 记录旋转变化操作 + */ + private void recordRotationChange(ModelPart part, float oldRotation, float newRotation) { + if (operationHistory != null && part != null) { + operationHistory.recordOperation("SET_ROTATION", part, oldRotation, newRotation); + } + } + + /** + * 记录中心点变化操作 + */ + private void recordPivotChange(ModelPart part, Vector2f oldPivot, Vector2f newPivot) { + if (operationHistory != null && part != null) { + operationHistory.recordOperation("SET_PIVOT", part, oldPivot, newPivot); + } + } + + /** + * 记录拖拽结束操作 + */ + private void recordDragEnd(List parts, Map startPositions) { + if (operationHistory != null && parts != null && !parts.isEmpty()) { + List params = new ArrayList<>(); + params.add(parts); + params.add(startPositions); + // 添加当前位置 + for (ModelPart part : parts) { + params.add(part.getPosition()); + } + operationHistory.recordOperation("DRAG_PART_END", params.toArray()); + } + } + + /** + * 记录调整大小结束操作 + */ + private void recordResizeEnd(List parts, Map startScales) { + if (operationHistory != null && parts != null && !parts.isEmpty()) { + List params = new ArrayList<>(); + params.add(parts); + params.add(startScales); + // 添加当前缩放 + for (ModelPart part : parts) { + params.add(part.getScale()); + } + operationHistory.recordOperation("RESIZE_PART_END", params.toArray()); + } + } + + /** + * 记录旋转结束操作 + */ + private void recordRotateEnd(List parts, Map startRotations) { + if (operationHistory != null && parts != null && !parts.isEmpty()) { + List params = new ArrayList<>(); + params.add(parts); + params.add(startRotations); + // 添加当前旋转 + for (ModelPart part : parts) { + params.add(part.getRotation()); + } + operationHistory.recordOperation("ROTATE_PART_END", params.toArray()); + } + } + + /** + * 记录移动中心点结束操作 + */ + private void recordMovePivotEnd(List parts, Map startPivots) { + if (operationHistory != null && parts != null && !parts.isEmpty()) { + List params = new ArrayList<>(); + params.add(parts); + params.add(startPivots); + // 添加当前中心点 + for (ModelPart part : parts) { + params.add(part.getPivot()); + } + operationHistory.recordOperation("MOVE_PIVOT_END", params.toArray()); + } + } + + /** + * 撤回操作 + */ + public void undo() { + if (operationHistory != null && operationHistory.canUndo()) { + executeInGLContext(() -> { + boolean success = operationHistory.undo(); + if (success) { + repaint(); + System.out.println("撤回: " + operationHistory.getUndoDescription()); + } + }); + } else { + System.out.println("没有可撤回的操作"); + } + } + + /** + * 重做操作 + */ + public void redo() { + if (operationHistory != null && operationHistory.canRedo()) { + executeInGLContext(() -> { + boolean success = operationHistory.redo(); + if (success) { + repaint(); + System.out.println("重做: " + operationHistory.getRedoDescription()); + } + }); + } else { + System.out.println("没有可重做的操作"); + } + } + + /** + * 清除操作历史 + */ + public void clearHistory() { + if (operationHistory != null) { + operationHistory.clearHistory(); + System.out.println("操作历史已清除"); + } } /** @@ -147,34 +356,228 @@ public class ModelRenderPanel extends JPanel { * 获取当前选中的网格 */ public Mesh2D getSelectedMesh() { - return selectedMesh; + return selectedMeshes.isEmpty() ? null : selectedMeshes.iterator().next(); } /** - * 设置选中的网格 + * 获取当前选中的所有网格 + */ + public Set getSelectedMeshes() { + return Collections.unmodifiableSet(selectedMeshes); + } + + /** + * 设置选中的网格(单选) */ public void setSelectedMesh(Mesh2D mesh) { - executeInGLContext(() -> { - // 清除之前选中的网格 - if (selectedMesh != null) { + //executeInGLContext(() -> { + // 清除之前选中的所有网格 + for (Mesh2D selectedMesh : selectedMeshes) { selectedMesh.setSelected(false); + // 清除多选列表 + selectedMesh.clearMultiSelection(); } + selectedMeshes.clear(); // 设置新的选中网格 - selectedMesh = mesh; - if (selectedMesh != null) { - selectedMesh.setSelected(true); + if (mesh != null) { + mesh.setSelected(true); + selectedMeshes.add(mesh); + lastSelectedMesh = mesh; // 更新最后选中的网格 + + // 通知其他选中网格添加到多选列表 + updateMultiSelectionInMeshes(); + } else { + lastSelectedMesh = null; + } + + logger.debug("设置选中网格: {}, 当前选中数量: {}", + mesh != null ? mesh.getName() : "null", selectedMeshes.size()); + //}); + } + + /** + * 添加选中的网格(多选) + */ + public void addSelectedMesh(Mesh2D mesh) { + executeInGLContext(() -> { + if (mesh != null && !selectedMeshes.contains(mesh)) { + mesh.setSelected(true); + selectedMeshes.add(mesh); + lastSelectedMesh = mesh; // 更新最后选中的网格 + + // 更新所有选中网格的多选列表 + updateMultiSelectionInMeshes(); + + logger.debug("添加选中网格: {}, 当前选中数量: {}", mesh.getName(), selectedMeshes.size()); } }); } + /** + * 移除选中的网格 + */ + public void removeSelectedMesh(Mesh2D mesh) { + executeInGLContext(() -> { + if (mesh != null && selectedMeshes.contains(mesh)) { + mesh.setSelected(false); + selectedMeshes.remove(mesh); + + // 更新所有选中网格的多选列表 + updateMultiSelectionInMeshes(); + + // 如果移除的是最后选中的网格,更新lastSelectedMesh + if (mesh == lastSelectedMesh) { + lastSelectedMesh = selectedMeshes.isEmpty() ? null : selectedMeshes.iterator().next(); + } + + logger.debug("移除选中网格: {}, 当前选中数量: {}", mesh.getName(), selectedMeshes.size()); + } + }); + } + + /** + * 清空所有选中的网格 + */ + public void clearSelectedMeshes() { + executeInGLContext(() -> { + for (Mesh2D mesh : selectedMeshes) { + mesh.setSelected(false); + mesh.clearMultiSelection(); + } + selectedMeshes.clear(); + lastSelectedMesh = null; + + logger.debug("清空所有选中网格"); + }); + } + + /** + * 全选所有网格 + */ + public void selectAllMeshes() { + executeInGLContext(() -> { + Model2D model = modelRef.get(); + if (model == null) return; + + // 清除之前的选择 + for (Mesh2D mesh : selectedMeshes) { + mesh.setSelected(false); + mesh.clearMultiSelection(); + } + selectedMeshes.clear(); + + // 获取所有网格并选中 + List allMeshes = getAllMeshesFromModel(model); + for (Mesh2D mesh : allMeshes) { + if (mesh.isVisible()) { + mesh.setSelected(true); + selectedMeshes.add(mesh); + } + } + + // 设置最后选中的网格 + if (!selectedMeshes.isEmpty()) { + lastSelectedMesh = selectedMeshes.iterator().next(); + } + + // 更新所有选中网格的多选列表 + updateMultiSelectionInMeshes(); + + logger.info("已全选 {} 个网格", selectedMeshes.size()); + }); + } + + /** + * 更新所有选中网格的多选列表 + */ + private void updateMultiSelectionInMeshes() { + if (selectedMeshes.size() <= 1) { + // 单选或没有选中,清除所有多选列表 + for (Mesh2D mesh : getAllMeshesFromModel(modelRef.get())) { + mesh.clearMultiSelection(); + } + return; + } + + // 多选状态,更新每个选中网格的多选列表 + for (Mesh2D selectedMesh : selectedMeshes) { + selectedMesh.clearMultiSelection(); + for (Mesh2D otherMesh : selectedMeshes) { + if (otherMesh != selectedMesh) { + selectedMesh.addToMultiSelection(otherMesh); + } + } + } + } + + /** + * 获取模型中的所有网格 + */ + private List getAllMeshesFromModel(Model2D model) { + List allMeshes = new ArrayList<>(); + if (model == null) return allMeshes; + + try { + java.util.List parts = model.getParts(); + if (parts == null) return allMeshes; + + for (ModelPart part : parts) { + if (part != null && part.isVisible()) { + addMeshesFromPart(part, allMeshes); + } + } + } catch (Exception e) { + logger.error("获取模型网格时出错", e); + } + + return allMeshes; + } + + /** + * 递归从部件中获取所有网格 + */ + private void addMeshesFromPart(ModelPart part, List meshList) { + if (part == null) return; + + // 添加当前部件的网格 + java.util.List meshes = part.getMeshes(); + if (meshes != null) { + for (Mesh2D mesh : meshes) { + if (mesh != null && mesh.isVisible()) { + meshList.add(mesh); + } + } + } + + // 递归处理子部件 + for (ModelPart child : part.getChildren()) { + addMeshesFromPart(child, meshList); + } + } + /** * 获取当前选中的部件 */ public ModelPart getSelectedPart() { + Mesh2D selectedMesh = getSelectedMesh(); return selectedMesh != null ? findPartByMesh(selectedMesh) : null; } + /** + * 获取所有选中的部件 + */ + public List getSelectedParts() { + List selectedParts = new ArrayList<>(); + for (Mesh2D mesh : selectedMeshes) { + ModelPart part = findPartByMesh(mesh); + if (part != null && !selectedParts.contains(part)) { + selectedParts.add(part); + } + } + return selectedParts; + } + /** * 获取鼠标悬停的网格 */ @@ -242,12 +645,18 @@ public class ModelRenderPanel extends JPanel { } }); - // 新增:键盘监听器用于检测Shift键 + // 新增:键盘监听器用于检测Shift和Ctrl键 addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_SHIFT) { shiftPressed = true; + } else if (e.getKeyCode() == KeyEvent.VK_CONTROL) { + ctrlPressed = true; + } else if (e.getKeyCode() == KeyEvent.VK_A && ctrlPressed) { + // Ctrl+A 全选 + e.consume(); // 阻止默认行为 + selectAllMeshes(); } } @@ -255,10 +664,47 @@ public class ModelRenderPanel extends JPanel { public void keyReleased(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_SHIFT) { shiftPressed = false; + } else if (e.getKeyCode() == KeyEvent.VK_CONTROL) { + ctrlPressed = false; } } }); + addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_SHIFT) { + shiftPressed = true; + } else if (e.getKeyCode() == KeyEvent.VK_CONTROL) { + ctrlPressed = true; + } else if (e.getKeyCode() == KeyEvent.VK_A && ctrlPressed) { + // Ctrl+A 全选 + e.consume(); // 阻止默认行为 + selectAllMeshes(); + } else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { + // ESC 键取消所有选择 + e.consume(); + clearSelectedMeshes(); + logger.info("按ESC键取消所有选择"); + } else if (e.getKeyCode() == KeyEvent.VK_D && ctrlPressed) { + // Ctrl+D 取消选择(可选,保留作为备选方案) + e.consume(); + clearSelectedMeshes(); + logger.info("按Ctrl+D取消所有选择"); + } + } + + @Override + public void keyReleased(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_SHIFT) { + shiftPressed = false; + } else if (e.getKeyCode() == KeyEvent.VK_CONTROL) { + ctrlPressed = false; + } + } + }); + + addMouseWheelListener(new MouseWheelListener() { @Override public void mouseWheelMoved(MouseWheelEvent e) { @@ -308,16 +754,29 @@ public class ModelRenderPanel extends JPanel { float modelY = modelCoords[1]; // 首先检查是否点击了选择框的调整手柄 - DragMode dragMode = checkResizeHandleHit(modelX, modelY); + // 多选时只对最后一个选中的网格进行操作 + Mesh2D targetMeshForHandle = selectedMeshes.isEmpty() ? null : lastSelectedMesh; + DragMode dragMode = targetMeshForHandle != null ? + checkResizeHandleHit(modelX, modelY, targetMeshForHandle) : DragMode.NONE; - if (dragMode == DragMode.ROTATE && selectedMesh != null) { - // 开始旋转 + // 清空之前的状态记录 + dragStartPositions.clear(); + dragStartScales.clear(); + dragStartRotations.clear(); + dragStartPivots.clear(); + + if (dragMode == DragMode.ROTATE) { + // 开始旋转 - 记录初始旋转状态 + List selectedParts = getSelectedParts(); + for (ModelPart part : selectedParts) { + dragStartRotations.put(part, part.getRotation()); + } currentDragMode = DragMode.ROTATE; dragStartX = modelX; dragStartY = modelY; // 获取边界框和中心点 - BoundingBox bounds = selectedMesh.getBounds(); + BoundingBox bounds = targetMeshForHandle.getBounds(); rotationCenter.set((bounds.getMinX() + bounds.getMaxX()) / 2.0f, (bounds.getMinY() + bounds.getMaxY()) / 2.0f); @@ -325,104 +784,180 @@ public class ModelRenderPanel extends JPanel { rotationStartAngle = (float) Math.atan2(dragStartY - rotationCenter.y, dragStartX - rotationCenter.x); - // 记录部件的初始旋转 - ModelPart selPart = findPartByMesh(selectedMesh); - if (selPart != null) { - partInitialRotation = selPart.getRotation(); + } else if (dragMode == DragMode.MOVE_PIVOT && targetMeshForHandle != null) { + // 开始移动中心点 - 记录初始中心点状态 + List selectedParts = getSelectedParts(); + for (ModelPart part : selectedParts) { + dragStartPivots.put(part, new Vector2f(part.getPivot())); } - - }else if (dragMode == DragMode.MOVE_PIVOT && selectedMesh != null) { - // 开始移动中心点 currentDragMode = DragMode.MOVE_PIVOT; dragStartX = modelX; dragStartY = modelY; // 记录初始中心点位置 - BoundingBox bounds = selectedMesh.getBounds(); + BoundingBox bounds = targetMeshForHandle.getBounds(); rotationCenter.set((bounds.getMinX() + bounds.getMaxX()) / 2.0f, (bounds.getMinY() + bounds.getMaxY()) / 2.0f); - } else if (dragMode != DragMode.NONE && selectedMesh != null) { - // 开始调整大小 + } else if (dragMode != DragMode.NONE && targetMeshForHandle != null) { + // 开始调整大小 - 记录初始缩放状态 + List selectedParts = getSelectedParts(); + for (ModelPart part : selectedParts) { + dragStartScales.put(part, new Vector2f(part.getScale())); + } currentDragMode = dragMode; dragStartX = modelX; // 记录拖拽起始位置 dragStartY = modelY; - BoundingBox bounds = selectedMesh.getBounds(); + BoundingBox bounds = targetMeshForHandle.getBounds(); resizeStartWidth = bounds.getWidth(); resizeStartHeight = bounds.getHeight(); - Vector2f center = bounds.getCenter(); - resizeStartX = center.x; - resizeStartY = center.y; - // 记录被调整部件的当前缩放(不使用反射,直接调用 ModelPart 的 getter) - ModelPart selPart = findPartByMesh(selectedMesh); - if (selPart != null) { - partInitialScaleX = selPart.getScaleX(); - partInitialScaleY = selPart.getScaleY(); - } else { - partInitialScaleX = 1.0f; - partInitialScaleY = 1.0f; - } - - //logger.info("开始调整大小,模式: {}", dragMode); } else { // 检查是否点击了网格(移动操作) Mesh2D clickedMesh = findMeshAtPosition(modelX, modelY); + if (clickedMesh != null) { - setSelectedMesh(clickedMesh); - ModelPart clickedPart = findPartByMesh(clickedMesh); - if (clickedPart != null) { - draggedPart = clickedPart; - dragStartX = modelX; - dragStartY = modelY; - partStartX = clickedPart.getPosition().x; - partStartY = clickedPart.getPosition().y; - currentDragMode = DragMode.MOVE; - //logger.info("开始移动部件: {}", clickedPart.getName()); + handleMultiSelect(clickedMesh, e.isShiftDown(), e.isControlDown()); + + // 记录初始位置状态 + List selectedParts = getSelectedParts(); + + for (ModelPart part : selectedParts) { + dragStartPositions.put(part, new Vector2f(part.getPosition())); } + + // 设置拖拽目标(如果点击了已选中的网格) + draggedPart = findPartByMesh(clickedMesh); + dragStartX = modelX; + dragStartY = modelY; + currentDragMode = DragMode.MOVE; + logger.debug("开始移动网格: {}", clickedMesh.getName()); } else { - // 点击空白区域,取消选中 - setSelectedMesh(null); - currentDragMode = DragMode.NONE; + // 点击空白区域 + if (e.isControlDown() || e.isShiftDown()) { + logger.info("按住Ctrl/Shift点击空白区域,保持当前选择状态 - Ctrl: {}, Shift: {}, 当前选中数量: {}", + e.isControlDown(), e.isShiftDown(), selectedMeshes.size()); + currentDragMode = DragMode.NONE; + } else { + currentDragMode = DragMode.NONE; + logger.info("点击空白区域,保持当前选择状态 - 当前选中数量: {}, 点击位置: ({}, {})", + selectedMeshes.size(), modelX, modelY); + } } } - } catch (Exception ex) { logger.error("处理鼠标按下时出错", ex); } }); } + + /** + * 处理多选逻辑 + */ + private void handleMultiSelect(Mesh2D clickedMesh, boolean isShiftDown, boolean isCtrlDown) { + if (isCtrlDown) { + if (selectedMeshes.contains(clickedMesh)) { + removeSelectedMesh(clickedMesh); + } else { + addSelectedMesh(clickedMesh); + } + } else if (isShiftDown && lastSelectedMesh != null) { + selectRange(lastSelectedMesh, clickedMesh); + } else if (!isInMultiSelection()) { + if (!selectedMeshes.contains(clickedMesh)) { + setSelectedMesh(clickedMesh); + } + } + + logger.debug("多选处理完成 - Ctrl: {}, Shift: {}, 选中数量: {}", + isCtrlDown, isShiftDown, selectedMeshes.size()); + } + + /** + * 检查是否处于多选状态 + */ + private boolean isInMultiSelection() { + return selectedMeshes.size() > 1; + } + + /** + * 选择两个网格之间的所有网格 + */ + private void selectRange(Mesh2D fromMesh, Mesh2D toMesh) { + Model2D model = modelRef.get(); + if (model == null) return; + + List allMeshes = getAllMeshesFromModel(model); + int fromIndex = allMeshes.indexOf(fromMesh); + int toIndex = allMeshes.indexOf(toMesh); + + if (fromIndex == -1 || toIndex == -1) { + // 如果找不到网格,回退到单选 + setSelectedMesh(toMesh); + return; + } + + // 确定选择范围 + int start = Math.min(fromIndex, toIndex); + int end = Math.max(fromIndex, toIndex); + + // 先清除当前选择 + //clearSelectedMeshes(); + + // 添加范围内的所有网格到选中集合 + for (int i = start; i <= end; i++) { + Mesh2D mesh = allMeshes.get(i); + if (mesh.isVisible()) { + mesh.setSelected(true); + selectedMeshes.add(mesh); + } + } + + // 更新最后选中的网格和多选列表 + lastSelectedMesh = toMesh; + updateMultiSelectionInMeshes(); + + logger.debug("范围选择: 从 {} 到 {}, 选中 {} 个网格", + fromMesh.getName(), toMesh.getName(), selectedMeshes.size()); + } + /** * 检查是否点击了选择框的调整手柄 */ - private DragMode checkResizeHandleHit(float modelX, float modelY) { - if (selectedMesh == null) return DragMode.NONE; + private DragMode checkResizeHandleHit(float modelX, float modelY, Mesh2D targetMesh) { + if (targetMesh == null) return DragMode.NONE; + + BoundingBox bounds; + Vector2f center; + + // 在多选状态下使用多选边界框 + if (targetMesh.isInMultiSelection()) { + bounds = targetMesh.getMultiSelectionBounds(); + center = bounds.getCenter(); + } else { + targetMesh.updateBounds(); + bounds = targetMesh.getBounds(); + center = targetMesh.getPivot(); + } - selectedMesh.updateBounds(); - BoundingBox bounds = selectedMesh.getBounds(); float minX = bounds.getMinX(); float minY = bounds.getMinY(); float maxX = bounds.getMaxX(); float maxY = bounds.getMaxY(); - // 使用 Mesh2D 的实际中心点 - Vector2f actualPivot = selectedMesh.getPivot(); - float centerX = actualPivot.x; - float centerY = actualPivot.y; - // 动态计算检测阈值,基于面板缩放比例 float scaleFactor = calculateScaleFactor(); float borderThickness = BORDER_THICKNESS / scaleFactor; float cornerSize = CORNER_SIZE / scaleFactor; // 首先检查是否点击了中心点(移动中心点) - if (isPointInCenterHandle(modelX, modelY, centerX, centerY, cornerSize)) { + if (isPointInCenterHandle(modelX, modelY, center.x, center.y, cornerSize)) { return DragMode.MOVE_PIVOT; } // 检查是否点击了旋转手柄 - if (isPointInRotationHandle(modelX, modelY, centerX, centerY, minY, cornerSize)) { + if (isPointInRotationHandle(modelX, modelY, center.x, center.y, minY, cornerSize)) { return DragMode.ROTATE; } @@ -514,7 +1049,6 @@ public class ModelRenderPanel extends JPanel { final int screenX = e.getX(); final int screenY = e.getY(); - executeInGLContext(() -> { try { if (currentDragMode == DragMode.NONE) { @@ -539,7 +1073,6 @@ public class ModelRenderPanel extends JPanel { handleMovePivotDrag(modelX, modelY); break; default: - // 调整大小逻辑 handleResizeDrag(modelX, modelY); break; } @@ -550,14 +1083,60 @@ public class ModelRenderPanel extends JPanel { }); } + + /** + * 处理移动中心点拖拽 + */ + private void handleMovePivotDrag(float modelX, float modelY) { + if (lastSelectedMesh == null) return; + + float deltaX = modelX - dragStartX; + float deltaY = modelY - dragStartY; + + // 只移动主选中网格的中心点 + ModelPart selectedPart = findPartByMesh(lastSelectedMesh); + if (selectedPart == null) return; + + Vector2f currentPivot = selectedPart.getPivot(); + float newPivotX = currentPivot.x + deltaX; + float newPivotY = currentPivot.y + deltaY; + + if (selectedPart.setPivot(newPivotX, newPivotY)) { + dragStartX = modelX; + dragStartY = modelY; + rotationCenter.set(newPivotX, newPivotY); + } + } + + /** + * 处理移动拖拽 + */ + private void handleMoveDrag(float modelX, float modelY) { + if (selectedMeshes.isEmpty()) return; + + float deltaX = modelX - dragStartX; + float deltaY = modelY - dragStartY; + + // 移动所有选中的部件 - 使用 ModelPart 的多选移动功能 + List selectedParts = getSelectedParts(); + for (ModelPart part : selectedParts) { + Vector2f pos = part.getPosition(); + part.setPosition(pos.x + deltaX, pos.y + deltaY); + } + + // 更新拖拽起始位置 + dragStartX = modelX; + dragStartY = modelY; + + // 强制更新多选边界框 + updateMultiSelectionBoundsForSelectedMeshes(); + } + /** * 处理旋转拖拽 */ private void handleRotateDrag(float modelX, float modelY) { - if (selectedMesh == null) return; - - ModelPart selectedPart = findPartByMesh(selectedMesh); - if (selectedPart == null) return; + if (lastSelectedMesh == null) return; // 计算当前角度 float currentAngle = (float) Math.atan2(modelY - rotationCenter.y, @@ -566,63 +1145,35 @@ public class ModelRenderPanel extends JPanel { // 计算旋转增量 float deltaAngle = currentAngle - rotationStartAngle; - // 应用旋转(基于初始旋转加上增量) - float newRotation = partInitialRotation + deltaAngle; - // 如果按住Shift键,以15度为步长进行约束旋转 if (shiftPressed || shiftDuringDrag) { float constraintStep = (float) (Math.PI / 12); // 15度 - newRotation = Math.round(newRotation / constraintStep) * constraintStep; + deltaAngle = Math.round(deltaAngle / constraintStep) * constraintStep; } - selectedPart.setRotation(newRotation); - - logger.debug("旋转角度: {} 度", Math.toDegrees(newRotation)); - } - - /** - * 处理移动中心点拖拽 - */ - private void handleMovePivotDrag(float modelX, float modelY) { - if (selectedMesh == null) return; - ModelPart selectedPart = findPartByMesh(selectedMesh); - if (selectedPart == null) return; - - float deltaX = modelX - dragStartX; - float deltaY = modelY - dragStartY; - Vector2f currentPivot = selectedPart.getPivot(); - float newPivotX = currentPivot.x + deltaX; - float newPivotY = currentPivot.y + deltaY; - if (!selectedPart.setPivot(newPivotX, newPivotY)) { - return; + // 应用旋转到所有选中的部件 - 使用 ModelPart 的多选旋转功能 + List selectedParts = getSelectedParts(); + for (ModelPart part : selectedParts) { + part.rotate(deltaAngle); } - dragStartX = modelX; - dragStartY = modelY; - rotationCenter.set(newPivotX, newPivotY); + + // 更新旋转起始角度 + rotationStartAngle = currentAngle; + + // 强制更新多选边界框 + updateMultiSelectionBoundsForSelectedMeshes(); + + logger.debug("旋转角度增量: {} 度", Math.toDegrees(deltaAngle)); } - /** - * 处理移动拖拽 - */ - private void handleMoveDrag(float modelX, float modelY) { - if (draggedPart == null) return; - - float deltaX = modelX - dragStartX; - float deltaY = modelY - dragStartY; - - float newX = partStartX + deltaX; - float newY = partStartY + deltaY; - - draggedPart.setPosition(newX, newY); - } /** * 处理调整大小拖拽 */ private void handleResizeDrag(float modelX, float modelY) { - if (selectedMesh == null) return; + if (lastSelectedMesh == null) return; - ModelPart selectedPart = findPartByMesh(selectedMesh); + ModelPart selectedPart = findPartByMesh(lastSelectedMesh); if (selectedPart == null) return; float deltaX = modelX - dragStartX; @@ -631,7 +1182,7 @@ public class ModelRenderPanel extends JPanel { float relScaleX = 1.0f; float relScaleY = 1.0f; - // 根据拖拽模式计算相对缩放比例(基于 resizeStartWidth/resizeStartHeight) + // 根据拖拽模式计算相对缩放比例 switch (currentDragMode) { case RESIZE_LEFT: relScaleX = (resizeStartWidth - deltaX) / Math.max(1e-6f, resizeStartWidth); @@ -670,34 +1221,87 @@ public class ModelRenderPanel extends JPanel { relScaleY = uniform; } - // 将相对缩放转换为基于部件初始缩放的绝对缩放值(不使用反射) - float finalScaleX = partInitialScaleX * relScaleX; - float finalScaleY = partInitialScaleY * relScaleY; + // 应用缩放到所有选中的部件 - 使用 ModelPart 的多选缩放功能 + List selectedParts = getSelectedParts(); + for (ModelPart part : selectedParts) { + Vector2f currentScale = part.getScale(); + part.setScale(currentScale.x * relScaleX, currentScale.y * relScaleY); + } - // 防止缩放变为零或负值 - finalScaleX = Math.max(finalScaleX, 0.01f); - finalScaleY = Math.max(finalScaleY, 0.01f); + // 更新拖拽起始位置和初始尺寸 + dragStartX = modelX; + dragStartY = modelY; + resizeStartWidth *= relScaleX; + resizeStartHeight *= relScaleY; - // 应用缩放(ModelPart 提供 setScale 方法) - selectedPart.setScale(finalScaleX, finalScaleY); - - // 可选:实时更新选择框基准(使得在同一次拖拽中感觉更自然) - // 注意:如果开启此项,dragStartX/dragStartY 与 resizeStartWidth/height 将被更新, - // 这样拖拽的 delta 计算会以当前帧为基础(增量更新),而不是始终相对于按下时的初始值。 - // 取消注释下面三行以启用增量模式: - // dragStartX = modelX; - // dragStartY = modelY; - // resizeStartWidth = Math.max(1e-6f, resizeStartWidth * relScaleX); resizeStartHeight = Math.max(1e-6f, resizeStartHeight * relScaleY); + // 强制更新多选边界框 + updateMultiSelectionBoundsForSelectedMeshes(); } /** - * 处理鼠标释放事件(结束拖拽) + * 更新所有选中网格的多选边界框 + */ + private void updateMultiSelectionBoundsForSelectedMeshes() { + if (selectedMeshes.size() <= 1) return; + + for (Mesh2D mesh : selectedMeshes) { + if (mesh.isInMultiSelection()) { + mesh.updateBounds(); + mesh.forceUpdateMultiSelectionBounds(); + } + } + } + + + /** + * 处理鼠标释放事件(结束拖拽并记录操作历史) */ private void handleMouseReleased(MouseEvent e) { + if (currentDragMode != DragMode.NONE) { + // 记录操作历史 + //executeInGLContext(() -> { + + try { + List selectedParts = getSelectedParts(); + switch (currentDragMode) { + case MOVE: + if (!dragStartPositions.isEmpty() && !selectedParts.isEmpty()) { + recordDragEnd(selectedParts, new HashMap<>(dragStartPositions)); + } + break; + case ROTATE: + if (!dragStartRotations.isEmpty() && !selectedParts.isEmpty()) { + recordRotateEnd(selectedParts, new HashMap<>(dragStartRotations)); + } + break; + case MOVE_PIVOT: + if (!dragStartPivots.isEmpty() && !selectedParts.isEmpty()) { + recordMovePivotEnd(selectedParts, new HashMap<>(dragStartPivots)); + } + break; + default: + if (!dragStartScales.isEmpty() && !selectedParts.isEmpty()) { + recordResizeEnd(selectedParts, new HashMap<>(dragStartScales)); + } + break; + } + } catch (Exception ex) { + logger.error("记录操作历史时出错", ex); + } + //}); + } + + // 重置状态 isDragging = false; draggedPart = null; currentDragMode = DragMode.NONE; shiftDuringDrag = false; + + // 清空状态记录 + dragStartPositions.clear(); + dragStartScales.clear(); + dragStartRotations.clear(); + dragStartPivots.clear(); } /** @@ -763,9 +1367,6 @@ public class ModelRenderPanel extends JPanel { // 检测点击的网格 Mesh2D clickedMesh = findMeshAtPosition(modelX, modelY); - // 更新选中状态 - setSelectedMesh(clickedMesh); - // 触发点击事件 for (ModelClickListener listener : clickListeners) { try { @@ -774,7 +1375,6 @@ public class ModelRenderPanel extends JPanel { logger.error("点击事件监听器执行出错", ex); } } - } catch (Exception ex) { logger.error("处理鼠标点击时出错", ex); } @@ -828,18 +1428,32 @@ public class ModelRenderPanel extends JPanel { */ private float[] screenToModelCoordinates(int screenX, int screenY) { if (width <= 0 || height <= 0) return null; + int panelWidth = getWidth(); int panelHeight = getHeight(); if (panelWidth <= 0 || panelHeight <= 0) return null; + + // 1. 屏幕坐标转换为离屏缓冲坐标 float scaleX = (float) width / panelWidth; float scaleY = (float) height / panelHeight; float bufferX = screenX * scaleX; - float bufferY = screenY * scaleY; + float bufferY = screenY * scaleY; // Y轴不反转 + + // 2. 缓冲坐标转换为标准化设备坐标 (NDC) float ndcX = (bufferX / width) * 2.0f - 1.0f; float ndcY = (bufferY / height) * 2.0f - 1.0f; - float modelX = ndcX * (width / 2.0f); - float modelY = ndcY * (height / 2.0f); - return new float[]{modelX, modelY}; + + // 3. NDC 转换为模型坐标(考虑当前显示缩放) + float modelX = ndcX * (width / 2.0f) / displayScale; + float modelY = ndcY * (height / 2.0f) / displayScale; + + float[] result = new float[]{modelX, modelY}; + + // 调试日志 + logger.debug("坐标转换: 屏幕({}, {}) -> 缓冲({}, {}) -> NDC({}, {}) -> 模型({}, {}), 缩放: {}", + screenX, screenY, bufferX, bufferY, ndcX, ndcY, modelX, modelY, displayScale); + + return result; } /** @@ -1448,4 +2062,25 @@ public class ModelRenderPanel extends JPanel { logger.info("OpenGL 资源已清理"); } + + /** + * 设置操作历史管理器(兼容性方法) + */ + public void setHistoryManager(OperationHistoryManager manager) { + this.historyManager = manager; + } + + /** + * 获取操作历史管理器(兼容性方法) + */ + public OperationHistoryManager getHistoryManager() { + return historyManager; + } + + /** + * 获取全局操作历史管理器 + */ + public OperationHistoryGlobal getOperationHistory() { + return operationHistory; + } } \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/TransformPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/TransformPanel.java index a898562..960c126 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/TransformPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/TransformPanel.java @@ -1,5 +1,6 @@ package com.chuangzhou.vivid2D.render.awt; +import com.chuangzhou.vivid2D.render.awt.util.OperationHistoryGlobal; import com.chuangzhou.vivid2D.render.model.ModelPart; import com.chuangzhou.vivid2D.render.model.ModelEvent; import org.joml.Vector2f; @@ -10,13 +11,16 @@ import javax.swing.event.DocumentListener; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.util.*; +import java.util.List; /** * @author tzdwindows 7 */ public class TransformPanel extends JPanel implements ModelEvent { private ModelRenderPanel renderPanel; - private ModelPart selectedPart; + private List selectedParts = new ArrayList<>(); + private boolean isMultiSelection = false; // 位置控制 private JTextField positionXField; @@ -43,8 +47,11 @@ public class TransformPanel extends JPanel implements ModelEvent { private boolean updatingUI = false; // 防止UI更新时触发事件 private javax.swing.Timer transformTimer; // 用于延迟处理变换输入 + private OperationHistoryGlobal operationHistory; + public TransformPanel(ModelRenderPanel renderPanel) { this.renderPanel = renderPanel; + this.operationHistory = OperationHistoryGlobal.getInstance(); initComponents(); setupListeners(); updateUIState(); @@ -226,55 +233,137 @@ public class TransformPanel extends JPanel implements ModelEvent { pivotXField.addActionListener(enterListener); pivotYField.addActionListener(enterListener); - // 按钮监听器 + // 旋转按钮监听器修改(支持多选) rotate90CWButton.addActionListener(e -> { - if (selectedPart != null) { + if (!selectedParts.isEmpty()) { renderPanel.executeInGLContext(() -> { - float currentRotation = (float) Math.toDegrees(selectedPart.getRotation()); - float newRotation = normalizeAngle(currentRotation + 90.0f); - selectedPart.setRotation((float) Math.toRadians(newRotation)); + Map oldRotations = new HashMap<>(); + Map newRotations = new HashMap<>(); + + for (ModelPart part : selectedParts) { + float oldRotation = part.getRotation(); + oldRotations.put(part, oldRotation); + + float currentRotation = (float) Math.toDegrees(oldRotation); + float newRotation = normalizeAngle(currentRotation + 90.0f); + part.setRotation((float) Math.toRadians(newRotation)); + + newRotations.put(part, part.getRotation()); + } + + // 记录多选操作历史 + recordMultiPartOperation("ROTATION", + new HashMap<>(oldRotations), + new HashMap<>(newRotations)); + renderPanel.repaint(); }); } }); rotate90CCWButton.addActionListener(e -> { - if (selectedPart != null) { + if (!selectedParts.isEmpty()) { renderPanel.executeInGLContext(() -> { - float currentRotation = (float) Math.toDegrees(selectedPart.getRotation()); - float newRotation = normalizeAngle(currentRotation - 90.0f); - selectedPart.setRotation((float) Math.toRadians(newRotation)); + Map oldRotations = new HashMap<>(); + Map newRotations = new HashMap<>(); + + for (ModelPart part : selectedParts) { + float oldRotation = part.getRotation(); + oldRotations.put(part, oldRotation); + + float currentRotation = (float) Math.toDegrees(oldRotation); + float newRotation = normalizeAngle(currentRotation - 90.0f); + part.setRotation((float) Math.toRadians(newRotation)); + + newRotations.put(part, part.getRotation()); + } + + // 记录多选操作历史 + recordMultiPartOperation("ROTATION", + new HashMap<>(oldRotations), + new HashMap<>(newRotations)); + renderPanel.repaint(); }); } }); + // 翻转按钮监听器修改(支持多选) flipXButton.addActionListener(e -> { - if (selectedPart != null) { + if (!selectedParts.isEmpty()) { renderPanel.executeInGLContext(() -> { - float currentScaleX = selectedPart.getScaleX(); - float currentScaleY = selectedPart.getScaleY(); - selectedPart.setScale(currentScaleX * -1, currentScaleY); + Map oldScales = new HashMap<>(); + Map newScales = new HashMap<>(); + + for (ModelPart part : selectedParts) { + Vector2f oldScale = new Vector2f(part.getScale()); + oldScales.put(part, oldScale); + + float currentScaleX = part.getScaleX(); + float currentScaleY = part.getScaleY(); + part.setScale(currentScaleX * -1, currentScaleY); + + newScales.put(part, new Vector2f(part.getScale())); + } + + // 记录多选操作历史 + recordMultiPartOperation("SCALE", + new HashMap<>(oldScales), + new HashMap<>(newScales)); + renderPanel.repaint(); }); } }); flipYButton.addActionListener(e -> { - if (selectedPart != null) { + if (!selectedParts.isEmpty()) { renderPanel.executeInGLContext(() -> { - float currentScaleX = selectedPart.getScaleX(); - float currentScaleY = selectedPart.getScaleY(); - selectedPart.setScale(currentScaleX, currentScaleY * -1); + Map oldScales = new HashMap<>(); + Map newScales = new HashMap<>(); + + for (ModelPart part : selectedParts) { + Vector2f oldScale = new Vector2f(part.getScale()); + oldScales.put(part, oldScale); + + float currentScaleX = part.getScaleX(); + float currentScaleY = part.getScaleY(); + part.setScale(currentScaleX, currentScaleY * -1); + + newScales.put(part, new Vector2f(part.getScale())); + } + + // 记录多选操作历史 + recordMultiPartOperation("SCALE", + new HashMap<>(oldScales), + new HashMap<>(newScales)); + renderPanel.repaint(); }); } }); + // 重置缩放按钮监听器修改(支持多选) resetScaleButton.addActionListener(e -> { - if (selectedPart != null) { + if (!selectedParts.isEmpty()) { renderPanel.executeInGLContext(() -> { - selectedPart.setScale(1.0f, 1.0f); + Map oldScales = new HashMap<>(); + Map newScales = new HashMap<>(); + + for (ModelPart part : selectedParts) { + Vector2f oldScale = new Vector2f(part.getScale()); + oldScales.put(part, oldScale); + + part.setScale(1.0f, 1.0f); + + newScales.put(part, new Vector2f(part.getScale())); + } + + // 记录多选操作历史 + recordMultiPartOperation("SCALE", + new HashMap<>(oldScales), + new HashMap<>(newScales)); + renderPanel.repaint(); }); } @@ -282,42 +371,101 @@ public class TransformPanel extends JPanel implements ModelEvent { } /** - * 事件监听器实现 - 当ModelPart的属性变化时自动更新UI + * 记录多部件操作历史 + */ + private void recordMultiPartOperation(String operationType, Map oldValues, Map newValues) { + if (operationHistory != null && !selectedParts.isEmpty()) { + List params = new ArrayList<>(); + params.add(new ArrayList<>(selectedParts)); + params.add(oldValues); + params.add(newValues); + operationHistory.recordOperation("MULTI_" + operationType, params.toArray()); + } + } + + /** + * 批量应用变换到所有选中部件 + */ + private void applyTransformToAllParts(float posX, float posY, float rotationDegrees, + float scaleX, float scaleY, float pivotX, float pivotY) { + // 记录变换前的状态 + Map oldStates = new HashMap<>(); + Map newStates = new HashMap<>(); + + for (ModelPart part : selectedParts) { + // 记录旧状态 + Object[] oldState = new Object[]{ + new Vector2f(part.getPosition()), + part.getRotation(), + new Vector2f(part.getScale()), + new Vector2f(part.getPivot()) + }; + oldStates.put(part, oldState); + + // 应用变换 + part.setPosition(posX, posY); + part.setRotation((float) Math.toRadians(rotationDegrees)); + part.setScale(scaleX, scaleY); + part.setPivot(pivotX, pivotY); + + // 记录新状态 + Object[] newState = new Object[]{ + new Vector2f(part.getPosition()), + part.getRotation(), + new Vector2f(part.getScale()), + new Vector2f(part.getPivot()) + }; + newStates.put(part, newState); + } + + // 记录批量操作历史 + recordMultiPartOperation("BATCH_TRANSFORM", oldStates, newStates); + } + + /** + * 事件监听器实现 - 当任何选中部件的属性变化时更新UI */ @Override public void trigger(String eventName, Object source) { - if (!(source instanceof ModelPart) || source != selectedPart) return; + if (!(source instanceof ModelPart) || !selectedParts.contains(source)) return; SwingUtilities.invokeLater(() -> { - updatingUI = true; - try { - ModelPart part = (ModelPart) source; - switch (eventName) { - case "position": - Vector2f position = part.getPosition(); - positionXField.setText(String.format("%.2f", position.x)); - positionYField.setText(String.format("%.2f", position.y)); - break; - case "rotation": - float currentRotation = (float) Math.toDegrees(part.getRotation()); - currentRotation = normalizeAngle(currentRotation); - rotationField.setText(String.format("%.2f", currentRotation)); - break; - case "scale": - Vector2f scale = part.getScale(); - scaleXField.setText(String.format("%.2f", scale.x)); - scaleYField.setText(String.format("%.2f", scale.y)); - break; - case "pivot": - Vector2f pivot = part.getPivot(); - pivotXField.setText(String.format("%.2f", pivot.x)); - pivotYField.setText(String.format("%.2f", pivot.y)); - break; + // 如果是多选,只更新UI但不记录历史(避免循环触发) + if (selectedParts.size() > 1) { + updatingUI = true; + updateUIForMultiSelection(); + updatingUI = false; + } else if (selectedParts.size() == 1) { + updatingUI = true; + try { + ModelPart part = (ModelPart) source; + switch (eventName) { + case "position": + Vector2f position = part.getPosition(); + positionXField.setText(String.format("%.2f", position.x)); + positionYField.setText(String.format("%.2f", position.y)); + break; + case "rotation": + float currentRotation = (float) Math.toDegrees(part.getRotation()); + currentRotation = normalizeAngle(currentRotation); + rotationField.setText(String.format("%.2f", currentRotation)); + break; + case "scale": + Vector2f scale = part.getScale(); + scaleXField.setText(String.format("%.2f", scale.x)); + scaleYField.setText(String.format("%.2f", scale.y)); + break; + case "pivot": + Vector2f pivot = part.getPivot(); + pivotXField.setText(String.format("%.2f", pivot.x)); + pivotYField.setText(String.format("%.2f", pivot.y)); + break; + } + } catch (Exception ex) { + ex.printStackTrace(); } - } catch (Exception ex) { - ex.printStackTrace(); + updatingUI = false; } - updatingUI = false; }); } @@ -325,7 +473,7 @@ public class TransformPanel extends JPanel implements ModelEvent { * 调度变换更新(延迟处理) */ private void scheduleTransformUpdate() { - if (updatingUI || selectedPart == null) return; + if (updatingUI || selectedParts.isEmpty()) return; transformTimer.stop(); transformTimer.start(); } @@ -342,94 +490,186 @@ public class TransformPanel extends JPanel implements ModelEvent { } /** - * 应用所有变换更改 + * 应用所有变换更改(支持多选) */ private void applyTransformChanges() { - if (updatingUI || selectedPart == null) return; + if (updatingUI || selectedParts.isEmpty()) return; renderPanel.executeInGLContext(() -> { try { - // 应用位置变化 float posX = Float.parseFloat(positionXField.getText()); float posY = Float.parseFloat(positionYField.getText()); - selectedPart.setPosition(posX, posY); - - // 应用旋转变化 float rotationDegrees = Float.parseFloat(rotationField.getText()); rotationDegrees = normalizeAngle(rotationDegrees); - selectedPart.setRotation((float) Math.toRadians(rotationDegrees)); - - // 应用缩放变化 float scaleX = Float.parseFloat(scaleXField.getText()); float scaleY = Float.parseFloat(scaleYField.getText()); - selectedPart.setScale(scaleX, scaleY); - - // 应用中心点变化 float pivotX = Float.parseFloat(pivotXField.getText()); float pivotY = Float.parseFloat(pivotYField.getText()); - selectedPart.setPivot(pivotX, pivotY); + + // 批量应用到所有选中部件 + applyTransformToAllParts(posX, posY, rotationDegrees, scaleX, scaleY, pivotX, pivotY); renderPanel.repaint(); } catch (NumberFormatException ex) { // 输入无效时恢复之前的值 - SwingUtilities.invokeLater(this::updateUIFromSelectedPart); + SwingUtilities.invokeLater(this::updateUIFromSelectedParts); } }); } /** - * 从选中的部件更新UI + * 从选中的部件更新UI(支持多选) */ - private void updateUIFromSelectedPart() { - if (selectedPart == null) return; + private void updateUIFromSelectedParts() { + if (selectedParts.isEmpty()) return; updatingUI = true; try { - // 更新位置 - Vector2f position = selectedPart.getPosition(); - positionXField.setText(String.format("%.2f", position.x)); - positionYField.setText(String.format("%.2f", position.y)); - - // 更新旋转 - float currentRotation = (float) Math.toDegrees(selectedPart.getRotation()); - currentRotation = normalizeAngle(currentRotation); - rotationField.setText(String.format("%.2f", currentRotation)); - - // 更新缩放 - Vector2f scale = selectedPart.getScale(); - scaleXField.setText(String.format("%.2f", scale.x)); - scaleYField.setText(String.format("%.2f", scale.y)); - - // 更新中心点 - Vector2f pivot = selectedPart.getPivot(); - pivotXField.setText(String.format("%.2f", pivot.x)); - pivotYField.setText(String.format("%.2f", pivot.y)); + if (selectedParts.size() == 1) { + // 单选:显示具体值 + ModelPart part = selectedParts.get(0); + updateUIFromSinglePart(part); + isMultiSelection = false; + } else { + // 多选:显示特殊标识或平均值 + updateUIForMultiSelection(); + isMultiSelection = true; + } } catch (Exception ex) { ex.printStackTrace(); } updatingUI = false; } - public void setSelectedPart(ModelPart part) { + /** + * 从单个部件更新UI + */ + private void updateUIFromSinglePart(ModelPart part) { + // 更新位置 + Vector2f position = part.getPosition(); + positionXField.setText(String.format("%.2f", position.x)); + positionYField.setText(String.format("%.2f", position.y)); + + // 更新旋转 + float currentRotation = (float) Math.toDegrees(part.getRotation()); + currentRotation = normalizeAngle(currentRotation); + rotationField.setText(String.format("%.2f", currentRotation)); + + // 更新缩放 + Vector2f scale = part.getScale(); + scaleXField.setText(String.format("%.2f", scale.x)); + scaleYField.setText(String.format("%.2f", scale.y)); + + // 更新中心点 + Vector2f pivot = part.getPivot(); + pivotXField.setText(String.format("%.2f", pivot.x)); + pivotYField.setText(String.format("%.2f", pivot.y)); + } + + /** + * 多选时的UI显示 + */ + private void updateUIForMultiSelection() { + // 多选时显示特殊值或平均值 + positionXField.setText("[多选]"); + positionYField.setText("[多选]"); + rotationField.setText("[多选]"); + scaleXField.setText("[多选]"); + scaleYField.setText("[多选]"); + pivotXField.setText("[多选]"); + pivotYField.setText("[多选]"); + + // 或者计算平均值(可选) + // calculateAndDisplayAverageValues(); + } + + /** + * 设置选中的部件(支持多选) + */ + public void setSelectedParts(List parts) { // 移除旧部件的事件监听 - if (this.selectedPart != null) { - this.selectedPart.removeEvent(this); + for (ModelPart oldPart : selectedParts) { + oldPart.removeEvent(this); } - this.selectedPart = part; + this.selectedParts.clear(); + if (parts != null) { + this.selectedParts.addAll(parts); - // 添加新部件的事件监听 - if (this.selectedPart != null) { - this.selectedPart.addEvent(this); + // 添加新部件的事件监听 + for (ModelPart newPart : selectedParts) { + newPart.addEvent(this); + } } updateUIState(); } + /** + * 添加选中部件 + */ + public void addSelectedPart(ModelPart part) { + if (part != null && !selectedParts.contains(part)) { + selectedParts.add(part); + part.addEvent(this); + updateUIState(); + } + } + + /** + * 移除选中部件 + */ + public void removeSelectedPart(ModelPart part) { + if (part != null && selectedParts.contains(part)) { + selectedParts.remove(part); + part.removeEvent(this); + updateUIState(); + } + } + + /** + * 清空选中部件 + */ + public void clearSelectedParts() { + for (ModelPart part : selectedParts) { + part.removeEvent(this); + } + selectedParts.clear(); + updateUIState(); + } + + /** + * 获取当前选中部件 + */ + public ModelPart getSelectedPart() { + return selectedParts.isEmpty() ? null : selectedParts.get(0); + } + + /** + * 获取所有选中部件 + */ + public List getSelectedParts() { + return new ArrayList<>(selectedParts); + } + + /** + * 获取当前选中部件数量 + */ + public int getSelectedPartsCount() { + return selectedParts.size(); + } + + /** + * 检查是否是多选状态 + */ + public boolean isMultiSelection() { + return isMultiSelection; + } + private void updateUIState() { updatingUI = true; - if (selectedPart != null) { - updateUIFromSelectedPart(); + if (!selectedParts.isEmpty()) { + updateUIFromSelectedParts(); setControlsEnabled(true); } else { // 清空所有字段 @@ -441,6 +681,7 @@ public class TransformPanel extends JPanel implements ModelEvent { pivotXField.setText("0.00"); pivotYField.setText("0.00"); setControlsEnabled(false); + isMultiSelection = false; } updatingUI = false; } @@ -467,8 +708,9 @@ public class TransformPanel extends JPanel implements ModelEvent { if (transformTimer != null) { transformTimer.stop(); } - if (selectedPart != null) { - selectedPart.removeEvent(this); + // 移除所有部件的事件监听 + for (ModelPart part : selectedParts) { + part.removeEvent(this); } } } \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationHistoryGlobal.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationHistoryGlobal.java new file mode 100644 index 0000000..fec31f4 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationHistoryGlobal.java @@ -0,0 +1,1507 @@ +package com.chuangzhou.vivid2D.render.awt.util; + +import com.chuangzhou.vivid2D.render.model.ModelPart; +import com.chuangzhou.vivid2D.render.model.util.Mesh2D; +import org.joml.Vector2f; + +import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * 全局操作历史管理器 + * 负责统一注册、管理和发布所有操作记录 + * + * @author tzdwindows 7 + */ +public class OperationHistoryGlobal { + + // 单例实例 + private static final OperationHistoryGlobal INSTANCE = new OperationHistoryGlobal(); + + // 操作记录管理器 + private final OperationHistoryManager historyManager; + + // 操作监听器列表 + private final List listeners; + + // 已注册的操作类型 + private final Set registeredOperations; + + // 操作记录器映射 + private final Map recorderMap; + + // 默认监听器 + private final DefaultOperationListener defaultListener; + + private OperationHistoryGlobal() { + this.historyManager = OperationHistoryManager.getInstance(); + this.listeners = new CopyOnWriteArrayList<>(); + this.registeredOperations = new HashSet<>(); + this.recorderMap = new HashMap<>(); + this.defaultListener = new DefaultOperationListener(); + + // 添加默认监听器 + addOperationListener(defaultListener); + + // 初始化基础操作记录器 + initializeBasicRecorders(); + } + + /** + * 获取全局实例 + */ + public static OperationHistoryGlobal getInstance() { + return INSTANCE; + } + + /** + * 初始化基础操作记录器 + */ + private void initializeBasicRecorders() { + System.out.println("开始初始化基础操作记录器..."); + + // 基础变换操作 + registerOperationRecorder("SET_POSITION", new OperationRecorder() { + @Override + public void execute(Object... params) { + notifyListeners("SET_POSITION", "execute", params); + } + + @Override + public void undo(Object... params) { + notifyListeners("SET_POSITION", "undo", params); + } + + @Override + public String getDescription() { + return "移动图层"; + } + }); + + registerOperationRecorder("DRAG_PART_END", new OperationRecorder() { + @Override + public void execute(Object... params) { + notifyListeners("DRAG_PART_END", "execute", params); + } + + @Override + public void undo(Object... params) { + notifyListeners("DRAG_PART_END", "undo", params); + } + + @Override + public String getDescription() { + return "拖拽图层结束"; + } + }); + + registerOperationRecorder("RESIZE_PART_END", new OperationRecorder() { + @Override + public void execute(Object... params) { + notifyListeners("RESIZE_PART_END", "execute", params); + } + + @Override + public void undo(Object... params) { + notifyListeners("RESIZE_PART_END", "undo", params); + } + + @Override + public String getDescription() { + return "调整大小结束"; + } + }); + + registerOperationRecorder("ROTATE_PART_END", new OperationRecorder() { + @Override + public void execute(Object... params) { + notifyListeners("ROTATE_PART_END", "execute", params); + } + + @Override + public void undo(Object... params) { + notifyListeners("ROTATE_PART_END", "undo", params); + } + + @Override + public String getDescription() { + return "旋转图层结束"; + } + }); + + registerOperationRecorder("MOVE_PIVOT_END", new OperationRecorder() { + @Override + public void execute(Object... params) { + notifyListeners("MOVE_PIVOT_END", "execute", params); + } + + @Override + public void undo(Object... params) { + notifyListeners("MOVE_PIVOT_END", "undo", params); + } + + @Override + public String getDescription() { + return "移动中心点结束"; + } + }); + + registerOperationRecorder("SET_ROTATION", new OperationRecorder() { + @Override + public void execute(Object... params) { + notifyListeners("SET_ROTATION", "execute", params); + } + + @Override + public void undo(Object... params) { + notifyListeners("SET_ROTATION", "undo", params); + } + + @Override + public String getDescription() { + return "旋转图层"; + } + }); + + registerOperationRecorder("BATCH_TRANSFORM", new OperationRecorder() { + @Override + public void execute(Object... params) { + notifyListeners("BATCH_TRANSFORM", "execute", params); + } + + @Override + public void undo(Object... params) { + notifyListeners("BATCH_TRANSFORM", "undo", params); + } + + @Override + public String getDescription() { + return "批量变换"; + } + }); + + registerOperationRecorder("SET_SCALE", new OperationRecorder() { + @Override + public void execute(Object... params) { + notifyListeners("SET_SCALE", "execute", params); + } + + @Override + public void undo(Object... params) { + notifyListeners("SET_SCALE", "undo", params); + } + + @Override + public String getDescription() { + return "缩放图层"; + } + }); + + registerOperationRecorder("SET_OPACITY", new OperationRecorder() { + @Override + public void execute(Object... params) { + notifyListeners("SET_OPACITY", "execute", params); + } + + @Override + public void undo(Object... params) { + notifyListeners("SET_OPACITY", "undo", params); + } + + @Override + public String getDescription() { + return "调整不透明度"; + } + }); + + registerOperationRecorder("SET_VISIBLE", new OperationRecorder() { + @Override + public void execute(Object... params) { + notifyListeners("SET_VISIBLE", "execute", params); + } + + @Override + public void undo(Object... params) { + notifyListeners("SET_VISIBLE", "undo", params); + } + + @Override + public String getDescription() { + return "显示/隐藏图层"; + } + }); + + registerOperationRecorder("SET_PIVOT", new OperationRecorder() { + @Override + public void execute(Object... params) { + notifyListeners("SET_PIVOT", "execute", params); + } + + @Override + public void undo(Object... params) { + notifyListeners("SET_PIVOT", "undo", params); + } + + @Override + public String getDescription() { + return "设置中心点"; + } + }); + + // 图层操作 + registerOperationRecorder("ADD_PART", new OperationRecorder() { + @Override + public void execute(Object... params) { + notifyListeners("ADD_PART", "execute", params); + } + + @Override + public void undo(Object... params) { + notifyListeners("ADD_PART", "undo", params); + } + + @Override + public String getDescription() { + return "添加图层"; + } + }); + + registerOperationRecorder("REMOVE_PART", new OperationRecorder() { + @Override + public void execute(Object... params) { + notifyListeners("REMOVE_PART", "execute", params); + } + + @Override + public void undo(Object... params) { + notifyListeners("REMOVE_PART", "undo", params); + } + + @Override + public String getDescription() { + return "删除图层"; + } + }); + + registerOperationRecorder("RENAME_PART", new OperationRecorder() { + @Override + public void execute(Object... params) { + notifyListeners("RENAME_PART", "execute", params); + } + + @Override + public void undo(Object... params) { + notifyListeners("RENAME_PART", "undo", params); + } + + @Override + public String getDescription() { + return "重命名图层"; + } + }); + + // 拖拽操作 + registerOperationRecorder("DRAG_PART", new OperationRecorder() { + @Override + public void execute(Object... params) { + notifyListeners("DRAG_PART", "execute", params); + } + + @Override + public void undo(Object... params) { + notifyListeners("DRAG_PART", "undo", params); + } + + @Override + public String getDescription() { + return "拖拽图层"; + } + }); + + // 网格操作 + registerOperationRecorder("ADD_MESH", new OperationRecorder() { + @Override + public void execute(Object... params) { + notifyListeners("ADD_MESH", "execute", params); + } + + @Override + public void undo(Object... params) { + notifyListeners("ADD_MESH", "undo", params); + } + + @Override + public String getDescription() { + return "添加网格"; + } + }); + + registerOperationRecorder("REMOVE_MESH", new OperationRecorder() { + @Override + public void execute(Object... params) { + notifyListeners("REMOVE_MESH", "execute", params); + } + + @Override + public void undo(Object... params) { + notifyListeners("REMOVE_MESH", "undo", params); + } + + @Override + public String getDescription() { + return "移除网格"; + } + }); + + // 液化操作 + registerOperationRecorder("LIQUIFY", new OperationRecorder() { + @Override + public void execute(Object... params) { + notifyListeners("LIQUIFY", "execute", params); + } + + @Override + public void undo(Object... params) { + notifyListeners("LIQUIFY", "undo", params); + } + + @Override + public String getDescription() { + return "液化操作"; + } + }); + + // 层级操作 + registerOperationRecorder("ADD_CHILD", new OperationRecorder() { + @Override + public void execute(Object... params) { + notifyListeners("ADD_CHILD", "execute", params); + } + + @Override + public void undo(Object... params) { + notifyListeners("ADD_CHILD", "undo", params); + } + + @Override + public String getDescription() { + return "添加子部件"; + } + }); + + registerOperationRecorder("REMOVE_CHILD", new OperationRecorder() { + @Override + public void execute(Object... params) { + notifyListeners("REMOVE_CHILD", "execute", params); + } + + @Override + public void undo(Object... params) { + notifyListeners("REMOVE_CHILD", "undo", params); + } + + @Override + public String getDescription() { + return "移除子部件"; + } + }); + + // 纹理操作 + registerOperationRecorder("BIND_TEXTURE", new OperationRecorder() { + @Override + public void execute(Object... params) { + notifyListeners("BIND_TEXTURE", "execute", params); + } + + @Override + public void undo(Object... params) { + notifyListeners("BIND_TEXTURE", "undo", params); + } + + @Override + public String getDescription() { + return "绑定纹理"; + } + }); + + // PSD导入操作 + registerOperationRecorder("IMPORT_PSD", new OperationRecorder() { + @Override + public void execute(Object... params) { + notifyListeners("IMPORT_PSD", "execute", params); + } + + @Override + public void undo(Object... params) { + notifyListeners("IMPORT_PSD", "undo", params); + } + + @Override + public String getDescription() { + return "导入PSD"; + } + }); + + System.out.println("基础操作记录器初始化完成,共注册 " + registeredOperations.size() + " 个操作类型"); + } + + /** + * 默认操作监听器 + * 用于处理基础操作记录器的初始化和基本事件处理 + */ + private static class DefaultOperationListener implements OperationListener { + + private final Map operationCounts = new HashMap<>(); + private final Set handledOperations = new HashSet<>(); + private final Map operationContext = new HashMap<>(); + + @Override + public void onOperationEvent(String operationType, String action, Object... params) { + // 记录操作统计 + operationCounts.put(operationType, operationCounts.getOrDefault(operationType, 0) + 1); + + // 根据操作类型和动作进行处理 + switch (action) { + case "record": + handleRecordEvent(operationType, params); + break; + case "execute": + handleExecuteEvent(operationType, params); + break; + case "undo": + handleUndoEvent(operationType, params); + break; + case "redo": + handleRedoEvent(operationType, params); + break; + case "clear": + handleClearEvent(operationType, params); + break; + default: + handleUnknownEvent(operationType, action, params); + break; + } + + // 标记已处理的操作类型 + handledOperations.add(operationType); + + // 输出调试信息 + if (isDebugEnabled()) { + System.out.printf("默认监听器处理: %s - %s (参数数: %d)%n", + operationType, action, params != null ? params.length : 0); + } + } + + private void handleRecordEvent(String operationType, Object... params) { + // 处理记录操作事件 - 保存操作状态用于撤回 + switch (operationType) { + case "SET_POSITION": + handlePositionRecord(params); + break; + case "SET_ROTATION": + handleRotationRecord(params); + break; + case "SET_SCALE": + handleScaleRecord(params); + break; + case "SET_OPACITY": + handleOpacityRecord(params); + break; + case "SET_VISIBLE": + handleVisibleRecord(params); + break; + case "SET_PIVOT": + handlePivotRecord(params); + break; + case "ADD_PART": + handleAddPartRecord(params); + break; + case "REMOVE_PART": + handleRemovePartRecord(params); + break; + case "RENAME_PART": + handleRenamePartRecord(params); + break; + case "DRAG_PART": + handleDragPartRecord(params); + break; + case "DRAG_PART_END": + handleDragPartEndRecord(params); + break; + case "RESIZE_PART_END": + handleResizePartEndRecord(params); + break; + case "ROTATE_PART_END": + handleRotatePartEndRecord(params); + break; + case "MOVE_PIVOT_END": + handleMovePivotEndRecord(params); + break; + case "ADD_MESH": + handleAddMeshRecord(params); + break; + case "REMOVE_MESH": + handleRemoveMeshRecord(params); + break; + case "BIND_TEXTURE": + handleBindTextureRecord(params); + break; + case "BATCH_TRANSFORM": + handleBatchTransformRecord(params); + break; + default: + System.out.println("记录操作: " + operationType); + break; + } + } + + private void handleExecuteEvent(String operationType, Object... params) { + // 处理执行操作事件(重做) + switch (operationType) { + case "SET_POSITION": + executePositionChange(params); + break; + case "SET_ROTATION": + executeRotationChange(params); + break; + case "SET_SCALE": + executeScaleChange(params); + break; + case "SET_OPACITY": + executeOpacityChange(params); + break; + case "SET_VISIBLE": + executeVisibleChange(params); + break; + case "SET_PIVOT": + executePivotChange(params); + break; + case "DRAG_PART_END": + executeDragPartEnd(params); + break; + case "RESIZE_PART_END": + executeResizePartEnd(params); + break; + case "ROTATE_PART_END": + executeRotatePartEnd(params); + break; + case "MOVE_PIVOT_END": + executeMovePivotEnd(params); + break; + case "BATCH_TRANSFORM": + executeBatchTransform(params); + break; + default: + System.out.println("执行操作: " + operationType); + break; + } + } + + private void handleUndoEvent(String operationType, Object... params) { + // 处理撤回操作事件 + switch (operationType) { + case "SET_POSITION": + undoPositionChange(params); + break; + case "SET_ROTATION": + undoRotationChange(params); + break; + case "SET_SCALE": + undoScaleChange(params); + break; + case "SET_OPACITY": + undoOpacityChange(params); + break; + case "SET_VISIBLE": + undoVisibleChange(params); + break; + case "SET_PIVOT": + undoPivotChange(params); + break; + case "DRAG_PART_END": + undoDragPartEnd(params); + break; + case "RESIZE_PART_END": + undoResizePartEnd(params); + break; + case "ROTATE_PART_END": + undoRotatePartEnd(params); + break; + case "MOVE_PIVOT_END": + undoMovePivotEnd(params); + break; + case "BATCH_TRANSFORM": + undoBatchTransform(params); + break; + default: + //System.out.println("撤回操作: " + operationType); + break; + } + } + + // ============ 批量变换操作方法 ============ + + private void handleBatchTransformRecord(Object... params) { + if (params.length >= 3 && params[0] instanceof ModelPart) { + ModelPart part = (ModelPart) params[0]; + Object[] oldValues = (Object[]) params[1]; + Object[] newValues = (Object[]) params[2]; + + System.out.printf("记录批量变换: %s (位置、旋转、缩放、中心点)%n", part.getName()); + } + } + + private void executeBatchTransform(Object... params) { + if (params.length >= 3 && params[0] instanceof ModelPart) { + ModelPart part = (ModelPart) params[0]; + Object[] newValues = (Object[]) params[2]; + + // 应用新的变换值 + if (newValues.length >= 4) { + Vector2f newPosition = (Vector2f) newValues[0]; + Float newRotation = (Float) newValues[1]; + Vector2f newScale = (Vector2f) newValues[2]; + Vector2f newPivot = (Vector2f) newValues[3]; + + part.setPosition(newPosition.x, newPosition.y); + part.setRotation(newRotation); + part.setScale(newScale.x, newScale.y); + part.setPivot(newPivot.x, newPivot.y); + } + + System.out.println("重做批量变换: " + part.getName()); + } + } + + private void undoBatchTransform(Object... params) { + if (params.length >= 2 && params[0] instanceof ModelPart) { + ModelPart part = (ModelPart) params[0]; + Object[] oldValues = (Object[]) params[1]; + + // 恢复旧的变换值 + if (oldValues.length >= 4) { + Vector2f oldPosition = (Vector2f) oldValues[0]; + Float oldRotation = (Float) oldValues[1]; + Vector2f oldScale = (Vector2f) oldValues[2]; + Vector2f oldPivot = (Vector2f) oldValues[3]; + + part.setPosition(oldPosition.x, oldPosition.y); + part.setRotation(oldRotation); + part.setScale(oldScale.x, oldScale.y); + part.setPivot(oldPivot.x, oldPivot.y); + } + + System.out.println("撤回批量变换: " + part.getName()); + } + } + + private void handleRedoEvent(String operationType, Object... params) { + // 处理重做操作事件 - 与 execute 相同 + handleExecuteEvent(operationType, params); + } + + private void handleClearEvent(String operationType, Object... params) { + // 处理清空历史事件 + operationCounts.clear(); + handledOperations.clear(); + operationContext.clear(); + System.out.println("操作历史已清空"); + } + + private void handleUnknownEvent(String operationType, String action, Object... params) { + System.out.println("未知操作事件: " + operationType + " - " + action); + } + + // ============ 新增的拖拽结束操作方法 ============ + + private void handleDragPartEndRecord(Object... params) { + //if (params.length >= 2) { + // List parts = (List) params[0]; + // Map startPositions = (Map) params[1]; +// + // System.out.printf("记录拖拽结束: %d 个部件从起始位置拖拽%n", parts.size()); + // for (ModelPart part : parts) { + // Vector2f startPos = startPositions.get(part); + // Vector2f currentPos = part.getPosition(); + // System.out.printf(" 部件 %s: (%.1f,%.1f) -> (%.1f,%.1f)%n", + // part.getName(), startPos.x, startPos.y, currentPos.x, currentPos.y); + // } + //} + } + + private void handleResizePartEndRecord(Object... params) { + if (params.length >= 2) { + List parts = (List) params[0]; + Map startScales = (Map) params[1]; + + System.out.printf("记录调整大小结束: %d 个部件从起始缩放调整%n", parts.size()); + for (ModelPart part : parts) { + Vector2f startScale = startScales.get(part); + Vector2f currentScale = part.getScale(); + System.out.printf(" 部件 %s: (%.2f,%.2f) -> (%.2f,%.2f)%n", + part.getName(), startScale.x, startScale.y, currentScale.x, currentScale.y); + } + } + } + + private void handleRotatePartEndRecord(Object... params) { + if (params.length >= 2) { + List parts = (List) params[0]; + Map startRotations = (Map) params[1]; + + System.out.printf("记录旋转结束: %d 个部件从起始旋转角度调整%n", parts.size()); + for (ModelPart part : parts) { + float startRotation = startRotations.get(part); + float currentRotation = part.getRotation(); + System.out.printf(" 部件 %s: %.1f° -> %.1f°%n", + part.getName(), Math.toDegrees(startRotation), Math.toDegrees(currentRotation)); + } + } + } + + private void handleMovePivotEndRecord(Object... params) { + if (params.length >= 2) { + List parts = (List) params[0]; + Map startPivots = (Map) params[1]; + + System.out.printf("记录移动中心点结束: %d 个部件从起始中心点移动%n", parts.size()); + for (ModelPart part : parts) { + Vector2f startPivot = startPivots.get(part); + Vector2f currentPivot = part.getPivot(); + System.out.printf(" 部件 %s: (%.1f,%.1f) -> (%.1f,%.1f)%n", + part.getName(), startPivot.x, startPivot.y, currentPivot.x, currentPivot.y); + } + } + } + + private void executeDragPartEnd(Object... params) { + if (params.length >= 2) { + List parts = (List) params[0]; + // 从参数中获取当前位置(参数索引从2开始) + int paramIndex = 2; + for (ModelPart part : parts) { + if (paramIndex < params.length && params[paramIndex] instanceof Vector2f) { + Vector2f targetPosition = (Vector2f) params[paramIndex]; + part.setPosition(targetPosition.x, targetPosition.y); + paramIndex++; + } + } + System.out.println("重做拖拽结束操作"); + } + } + + private void executeResizePartEnd(Object... params) { + if (params.length >= 2) { + List parts = (List) params[0]; + // 从参数中获取当前缩放(参数索引从2开始) + int paramIndex = 2; + for (ModelPart part : parts) { + if (paramIndex < params.length && params[paramIndex] instanceof Vector2f) { + Vector2f targetScale = (Vector2f) params[paramIndex]; + part.setScale(targetScale.x, targetScale.y); + paramIndex++; + } + } + System.out.println("重做调整大小结束操作"); + } + } + + private void executeRotatePartEnd(Object... params) { + if (params.length >= 2) { + List parts = (List) params[0]; + // 从参数中获取当前旋转(参数索引从2开始) + int paramIndex = 2; + for (ModelPart part : parts) { + if (paramIndex < params.length && params[paramIndex] instanceof Float) { + float targetRotation = (Float) params[paramIndex]; + part.setRotation(targetRotation); + paramIndex++; + } + } + System.out.println("重做旋转结束操作"); + } + } + + private void executeMovePivotEnd(Object... params) { + if (params.length >= 2) { + List parts = (List) params[0]; + // 从参数中获取当前中心点(参数索引从2开始) + int paramIndex = 2; + for (ModelPart part : parts) { + if (paramIndex < params.length && params[paramIndex] instanceof Vector2f) { + Vector2f targetPivot = (Vector2f) params[paramIndex]; + part.setPivot(targetPivot.x, targetPivot.y); + paramIndex++; + } + } + System.out.println("重做移动中心点结束操作"); + } + } + + private void undoDragPartEnd(Object... params) { + if (params.length >= 2) { + List parts = (List) params[0]; + Map startPositions = (Map) params[1]; + + for (ModelPart part : parts) { + Vector2f startPosition = startPositions.get(part); + if (startPosition != null) { + part.setPosition(startPosition.x, startPosition.y); + } + } + // System.out.println("撤回拖拽结束操作"); + } + } + + private void undoResizePartEnd(Object... params) { + if (params.length >= 2) { + List parts = (List) params[0]; + Map startScales = (Map) params[1]; + + for (ModelPart part : parts) { + Vector2f startScale = startScales.get(part); + if (startScale != null) { + part.setScale(startScale.x, startScale.y); + } + } + System.out.println("撤回调整大小结束操作"); + } + } + + private void undoRotatePartEnd(Object... params) { + if (params.length >= 2) { + List parts = (List) params[0]; + Map startRotations = (Map) params[1]; + + for (ModelPart part : parts) { + Float startRotation = startRotations.get(part); + if (startRotation != null) { + part.setRotation(startRotation); + } + } + System.out.println("撤回旋转结束操作"); + } + } + + private void undoMovePivotEnd(Object... params) { + if (params.length >= 2) { + List parts = (List) params[0]; + Map startPivots = (Map) params[1]; + + for (ModelPart part : parts) { + Vector2f startPivot = startPivots.get(part); + if (startPivot != null) { + part.setPivot(startPivot.x, startPivot.y); + } + } + System.out.println("撤回移动中心点结束操作"); + } + } + + + // ============ 具体操作处理方法 ============ + + private void handlePositionRecord(Object... params) { + if (params.length >= 3 && params[0] instanceof ModelPart) { + ModelPart part = (ModelPart) params[0]; + Vector2f oldPosition = (Vector2f) params[1]; + Vector2f newPosition = (Vector2f) params[2]; + + // 保存撤回信息 + String key = "position_undo_" + part.getName(); + operationContext.put(key, oldPosition); + + System.out.printf("记录位置变化: %s (%.1f,%.1f) -> (%.1f,%.1f)%n", + part.getName(), oldPosition.x, oldPosition.y, newPosition.x, newPosition.y); + } + } + + private void executePositionChange(Object... params) { + if (params.length >= 3 && params[0] instanceof ModelPart) { + ModelPart part = (ModelPart) params[0]; + Vector2f newPosition = (Vector2f) params[2]; + part.setPosition(newPosition.x, newPosition.y); + System.out.println("重做位置变化: " + part.getName()); + } + } + + private void undoPositionChange(Object... params) { + if (params.length >= 2 && params[0] instanceof ModelPart) { + ModelPart part = (ModelPart) params[0]; + Vector2f oldPosition = (Vector2f) params[1]; + part.setPosition(oldPosition.x, oldPosition.y); + System.out.println("撤回位置变化: " + part.getName()); + } + } + + private void handleRotationRecord(Object... params) { + if (params.length >= 3 && params[0] instanceof ModelPart) { + ModelPart part = (ModelPart) params[0]; + float oldRotation = (Float) params[1]; + float newRotation = (Float) params[2]; + + String key = "rotation_undo_" + part.getName(); + operationContext.put(key, oldRotation); + + System.out.printf("记录旋转变化: %s %.1f° -> %.1f°%n", + part.getName(), Math.toDegrees(oldRotation), Math.toDegrees(newRotation)); + } + } + + private void executeRotationChange(Object... params) { + if (params.length >= 3 && params[0] instanceof ModelPart) { + ModelPart part = (ModelPart) params[0]; + float newRotation = (Float) params[2]; + part.setRotation(newRotation); + System.out.println("重做旋转变化: " + part.getName()); + } + } + + private void undoRotationChange(Object... params) { + if (params.length >= 2 && params[0] instanceof ModelPart) { + ModelPart part = (ModelPart) params[0]; + float oldRotation = (Float) params[1]; + part.setRotation(oldRotation); + System.out.println("撤回旋转变化: " + part.getName()); + } + } + + private void handleScaleRecord(Object... params) { + if (params.length >= 3 && params[0] instanceof ModelPart) { + ModelPart part = (ModelPart) params[0]; + Vector2f oldScale = (Vector2f) params[1]; + Vector2f newScale = (Vector2f) params[2]; + + String key = "scale_undo_" + part.getName(); + operationContext.put(key, oldScale); + + System.out.printf("记录缩放变化: %s (%.2f,%.2f) -> (%.2f,%.2f)%n", + part.getName(), oldScale.x, oldScale.y, newScale.x, newScale.y); + } + } + + private void executeScaleChange(Object... params) { + if (params.length >= 3 && params[0] instanceof ModelPart) { + ModelPart part = (ModelPart) params[0]; + Vector2f newScale = (Vector2f) params[2]; + part.setScale(newScale.x, newScale.y); + System.out.println("重做缩放变化: " + part.getName()); + } + } + + private void undoScaleChange(Object... params) { + if (params.length >= 2 && params[0] instanceof ModelPart) { + ModelPart part = (ModelPart) params[0]; + Vector2f oldScale = (Vector2f) params[1]; + part.setScale(oldScale.x, oldScale.y); + System.out.println("撤回缩放变化: " + part.getName()); + } + } + + private void handleOpacityRecord(Object... params) { + if (params.length >= 3 && params[0] instanceof ModelPart) { + ModelPart part = (ModelPart) params[0]; + float oldOpacity = (Float) params[1]; + float newOpacity = (Float) params[2]; + + String key = "opacity_undo_" + part.getName(); + operationContext.put(key, oldOpacity); + + System.out.printf("记录不透明度变化: %s %.1f -> %.1f%n", + part.getName(), oldOpacity, newOpacity); + } + } + + private void executeOpacityChange(Object... params) { + if (params.length >= 3 && params[0] instanceof ModelPart) { + ModelPart part = (ModelPart) params[0]; + float newOpacity = (Float) params[2]; + part.setOpacity(newOpacity); + System.out.println("重做不透明度变化: " + part.getName()); + } + } + + private void undoOpacityChange(Object... params) { + if (params.length >= 2 && params[0] instanceof ModelPart) { + ModelPart part = (ModelPart) params[0]; + float oldOpacity = (Float) params[1]; + part.setOpacity(oldOpacity); + System.out.println("撤回不透明度变化: " + part.getName()); + } + } + + private void handleVisibleRecord(Object... params) { + if (params.length >= 3 && params[0] instanceof ModelPart) { + ModelPart part = (ModelPart) params[0]; + boolean oldVisible = (Boolean) params[1]; + boolean newVisible = (Boolean) params[2]; + + String key = "visible_undo_" + part.getName(); + operationContext.put(key, oldVisible); + + System.out.printf("记录可见性变化: %s %s -> %s%n", + part.getName(), oldVisible ? "显示" : "隐藏", newVisible ? "显示" : "隐藏"); + } + } + + private void executeVisibleChange(Object... params) { + if (params.length >= 3 && params[0] instanceof ModelPart) { + ModelPart part = (ModelPart) params[0]; + boolean newVisible = (Boolean) params[2]; + part.setVisible(newVisible); + System.out.println("重做可见性变化: " + part.getName()); + } + } + + private void undoVisibleChange(Object... params) { + if (params.length >= 2 && params[0] instanceof ModelPart) { + ModelPart part = (ModelPart) params[0]; + boolean oldVisible = (Boolean) params[1]; + part.setVisible(oldVisible); + System.out.println("撤回可见性变化: " + part.getName()); + } + } + + private void handlePivotRecord(Object... params) { + if (params.length >= 3 && params[0] instanceof ModelPart) { + ModelPart part = (ModelPart) params[0]; + Vector2f oldPivot = (Vector2f) params[1]; + Vector2f newPivot = (Vector2f) params[2]; + + String key = "pivot_undo_" + part.getName(); + operationContext.put(key, oldPivot); + + System.out.printf("记录中心点变化: %s (%.1f,%.1f) -> (%.1f,%.1f)%n", + part.getName(), oldPivot.x, oldPivot.y, newPivot.x, newPivot.y); + } + } + + private void executePivotChange(Object... params) { + if (params.length >= 3 && params[0] instanceof ModelPart) { + ModelPart part = (ModelPart) params[0]; + Vector2f newPivot = (Vector2f) params[2]; + part.setPivot(newPivot.x, newPivot.y); + System.out.println("重做中心点变化: " + part.getName()); + } + } + + private void undoPivotChange(Object... params) { + if (params.length >= 2 && params[0] instanceof ModelPart) { + ModelPart part = (ModelPart) params[0]; + Vector2f oldPivot = (Vector2f) params[1]; + part.setPivot(oldPivot.x, oldPivot.y); + System.out.println("撤回中心点变化: " + part.getName()); + } + } + + private void handleAddPartRecord(Object... params) { + if (params.length >= 1 && params[0] instanceof ModelPart) { + ModelPart part = (ModelPart) params[0]; + System.out.println("记录添加图层: " + part.getName()); + } + } + + private void handleRemovePartRecord(Object... params) { + if (params.length >= 1 && params[0] instanceof MeshState) { + MeshState state = (MeshState) params[0]; + System.out.println("记录删除图层: " + state.name); + } + } + + private void handleRenamePartRecord(Object... params) { + if (params.length >= 3 && params[0] instanceof ModelPart) { + ModelPart part = (ModelPart) params[0]; + String oldName = (String) params[1]; + String newName = (String) params[2]; + System.out.printf("记录重命名: %s -> %s%n", oldName, newName); + } + } + + private void handleDragPartRecord(Object... params) { + if (params.length >= 3 && params[0] instanceof ModelPart) { + ModelPart part = (ModelPart) params[0]; + Vector2f startPos = (Vector2f) params[1]; + Vector2f endPos = (Vector2f) params[2]; + System.out.printf("记录拖拽: %s (%.1f,%.1f) -> (%.1f,%.1f)%n", + part.getName(), startPos.x, startPos.y, endPos.x, endPos.y); + } + } + + private void handleAddMeshRecord(Object... params) { + if (params.length >= 1 && params[0] instanceof Mesh2D) { + Mesh2D mesh = (Mesh2D) params[0]; + System.out.println("记录添加网格: " + mesh.getName()); + } + } + + private void handleRemoveMeshRecord(Object... params) { + if (params.length >= 1 && params[0] instanceof MeshState) { + MeshState state = (MeshState) params[0]; + System.out.println("记录删除网格: " + state.name); + } + } + + private void handleBindTextureRecord(Object... params) { + System.out.println("记录绑定纹理操作"); + } + + private boolean isDebugEnabled() { + return Boolean.getBoolean("operation.history.debug"); + } + + /** + * 获取操作统计信息 + */ + public Map getOperationStatistics() { + return new HashMap<>(operationCounts); + } + + /** + * 获取详细统计报告 + */ + public String getDetailedStatistics() { + StringBuilder sb = new StringBuilder(); + sb.append("操作统计报告:\n"); + sb.append("==============\n"); + + int totalOperations = operationCounts.values().stream().mapToInt(Integer::intValue).sum(); + sb.append("总操作次数: ").append(totalOperations).append("\n"); + sb.append("处理的操作类型数: ").append(handledOperations.size()).append("\n\n"); + + sb.append("各操作类型统计:\n"); + operationCounts.entrySet().stream() + .sorted(Map.Entry.comparingByValue().reversed()) + .forEach(entry -> { + sb.append(String.format(" %-20s: %d 次\n", entry.getKey(), entry.getValue())); + }); + + return sb.toString(); + } + + /** + * 获取已处理的操作类型 + */ + public Set getHandledOperations() { + return new HashSet<>(handledOperations); + } + + /** + * 重置统计信息 + */ + public void resetStatistics() { + operationCounts.clear(); + handledOperations.clear(); + operationContext.clear(); + } + + /** + * 获取操作上下文(用于调试) + */ + public Map getOperationContext() { + return new HashMap<>(operationContext); + } + + /** + * 检查特定操作类型的统计 + */ + public int getOperationCount(String operationType) { + return operationCounts.getOrDefault(operationType, 0); + } + + /** + * 获取最频繁的操作类型 + */ + public String getMostFrequentOperation() { + return operationCounts.entrySet().stream() + .max(Map.Entry.comparingByValue()) + .map(Map.Entry::getKey) + .orElse("无操作"); + } + } + + public static class MeshState { + String name; + float[] vertices; + float[] originalVertices; + Vector2f originalPivot; + Object texture; + + MeshState(String name, float[] vertices, float[] originalVertices, + Vector2f originalPivot, Object texture) { + this.name = name; + this.vertices = vertices != null ? vertices.clone() : null; + this.originalVertices = originalVertices != null ? originalVertices.clone() : null; + this.originalPivot = originalPivot != null ? new Vector2f(originalPivot) : null; + this.texture = texture; + } + } + + /** + * 注册操作记录器 + */ + public void registerOperationRecorder(String operationType, OperationRecorder recorder) { + if (operationType == null || recorder == null) { + throw new IllegalArgumentException("Operation type and recorder cannot be null"); + } + + registeredOperations.add(operationType); + recorderMap.put(operationType, recorder); + + // 同时注册到历史管理器 + if (historyManager != null) { + historyManager.registerRecorder(operationType, recorder); + } + + System.out.println("已注册操作类型: " + operationType); + } + + /** + * 注销操作记录器 + */ + public void unregisterOperationRecorder(String operationType) { + if (operationType != null) { + registeredOperations.remove(operationType); + recorderMap.remove(operationType); + + // 注意:这里不从历史管理器中移除,因为历史管理器可能需要处理已有的操作记录 + System.out.println("已注销操作类型: " + operationType); + } + } + + /** + * 记录操作 + */ + public void recordOperation(String operationType, Object... params) { + if (!isOperationRegistered(operationType)) { + System.err.println("未注册的操作类型: " + operationType); + return; + } + + if (historyManager != null) { + historyManager.recordOperation(operationType, params); + notifyListeners(operationType, "record", params); + } + } + + /** + * 执行撤回操作 + */ + public boolean undo() { + if (historyManager != null && historyManager.canUndo()) { + boolean success = historyManager.undo(); + if (success) { + String description = historyManager.getUndoDescription(); + notifyListeners("SYSTEM", "undo", description); + //System.out.println("撤回操作: " + description); + } + return success; + } + return false; + } + + /** + * 执行重做操作 + */ + public boolean redo() { + if (historyManager != null && historyManager.canRedo()) { + boolean success = historyManager.redo(); + if (success) { + String description = historyManager.getRedoDescription(); + notifyListeners("SYSTEM", "redo", description); + System.out.println("重做操作: " + description); + } + return success; + } + return false; + } + + /** + * 添加操作监听器 + */ + public void addOperationListener(OperationListener listener) { + if (listener != null && !listeners.contains(listener)) { + listeners.add(listener); + System.out.println("已添加操作监听器: " + listener.getClass().getSimpleName()); + } + } + + /** + * 移除操作监听器 + */ + public void removeOperationListener(OperationListener listener) { + if (listeners.remove(listener)) { + System.out.println("已移除操作监听器: " + listener.getClass().getSimpleName()); + } + } + + /** + * 移除默认监听器(谨慎使用) + */ + public void removeDefaultListener() { + removeOperationListener(defaultListener); + } + + /** + * 通知所有监听器 + */ + private void notifyListeners(String operationType, String action, Object... params) { + for (OperationListener listener : listeners) { + try { + listener.onOperationEvent(operationType, action, params); + } catch (Exception e) { + System.err.println("操作监听器执行失败: " + e.getMessage()); + e.printStackTrace(); + } + } + } + + /** + * 检查操作类型是否已注册 + */ + public boolean isOperationRegistered(String operationType) { + return registeredOperations.contains(operationType); + } + + /** + * 获取所有已注册的操作类型 + */ + public Set getRegisteredOperations() { + return new HashSet<>(registeredOperations); + } + + /** + * 获取操作记录器 + */ + public OperationRecorder getOperationRecorder(String operationType) { + return recorderMap.get(operationType); + } + + /** + * 清空操作历史 + */ + public void clearHistory() { + if (historyManager != null) { + historyManager.clear(); + notifyListeners("SYSTEM", "clear", "操作历史已清空"); + } + } + + /** + * 获取操作历史管理器 + */ + public OperationHistoryManager getHistoryManager() { + return historyManager; + } + + /** + * 检查是否可以撤回 + */ + public boolean canUndo() { + return historyManager != null && historyManager.canUndo(); + } + + /** + * 检查是否可以重做 + */ + public boolean canRedo() { + return historyManager != null && historyManager.canRedo(); + } + + /** + * 获取撤回操作描述 + */ + public String getUndoDescription() { + return historyManager != null ? historyManager.getUndoDescription() : ""; + } + + /** + * 获取重做操作描述 + */ + public String getRedoDescription() { + return historyManager != null ? historyManager.getRedoDescription() : ""; + } + + /** + * 批量注册操作记录器 + */ + public void registerOperationRecorders(Map recorders) { + if (recorders != null) { + for (Map.Entry entry : recorders.entrySet()) { + registerOperationRecorder(entry.getKey(), entry.getValue()); + } + } + } + + /** + * 获取默认监听器的统计信息 + */ + public Map getDefaultListenerStatistics() { + return defaultListener.getOperationStatistics(); + } + + /** + * 获取默认监听器处理的操作用户 + */ + public Set getDefaultListenerHandledOperations() { + return defaultListener.getHandledOperations(); + } + + /** + * 重置默认监听器统计信息 + */ + public void resetDefaultListenerStatistics() { + defaultListener.resetStatistics(); + } + + /** + * 获取操作统计信息 + */ + public OperationStatistics getStatistics() { + return new OperationStatistics( + registeredOperations.size(), + historyManager != null ? getHistorySize() : 0, + canUndo(), + canRedo() + ); + } + + /** + * 获取历史记录大小(估算) + */ + private int getHistorySize() { + // 这里可以通过反射或其他方式获取历史记录的实际大小 + // 暂时返回估算值 + return 0; + } + + /** + * 操作统计信息类 + */ + public static class OperationStatistics { + private final int registeredOperationCount; + private final int historySize; + private final boolean canUndo; + private final boolean canRedo; + + public OperationStatistics(int registeredOperationCount, int historySize, + boolean canUndo, boolean canRedo) { + this.registeredOperationCount = registeredOperationCount; + this.historySize = historySize; + this.canUndo = canUndo; + this.canRedo = canRedo; + } + + public int getRegisteredOperationCount() { return registeredOperationCount; } + public int getHistorySize() { return historySize; } + public boolean canUndo() { return canUndo; } + public boolean canRedo() { return canRedo; } + + @Override + public String toString() { + return String.format( + "OperationStatistics{注册操作数=%d, 历史记录=%d, 可撤回=%s, 可重做=%s}", + registeredOperationCount, historySize, canUndo, canRedo + ); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationHistoryManager.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationHistoryManager.java new file mode 100644 index 0000000..183b152 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationHistoryManager.java @@ -0,0 +1,211 @@ +package com.chuangzhou.vivid2D.render.awt.util; + +import java.util.*; + +/** + * 操作记录管理器 + * 负责管理操作的撤回和重做 + * @author tzdwindows 7 + */ +public class OperationHistoryManager { + private static OperationHistoryManager instance = new OperationHistoryManager(); + // 操作记录栈 + private final LinkedList undoStack; + private final LinkedList redoStack; + + // 最大记录数量 + private final int maxHistorySize; + + // 操作记录器映射 + private final Map recorderMap; + + // 是否启用记录 + private boolean enabled = true; + + public OperationHistoryManager() { + this(1000); + } + + public OperationHistoryManager(int maxHistorySize) { + this.maxHistorySize = maxHistorySize; + this.undoStack = new LinkedList<>(); + this.redoStack = new LinkedList<>(); + this.recorderMap = new HashMap<>(); + } + + /** + * 获取操作记录管理器实例 + * @return 操作记录管理器实例 + */ + public static OperationHistoryManager getInstance() { + return instance; + } + + /** + * 注册操作记录器 + * @param operationType 操作类型标识 + * @param recorder 操作记录器 + */ + public void registerRecorder(String operationType, OperationRecorder recorder) { + recorderMap.put(operationType, recorder); + } + + /** + * 记录操作 + * @param operationType 操作类型 + * @param params 操作参数 + */ + public void recordOperation(String operationType, Object... params) { + if (!enabled) return; + + OperationRecorder recorder = recorderMap.get(operationType); + if (recorder == null) { + System.err.println("未注册的操作类型: " + operationType); + return; + } + + // 创建操作记录 + OperationRecord record = new OperationRecord(operationType, params, recorder.getDescription()); + + // 添加到撤回栈 + undoStack.push(record); + + // 限制栈大小 + if (undoStack.size() > maxHistorySize) { + undoStack.removeLast(); + } + + // 清空重做栈(新操作后重做栈无效) + redoStack.clear(); + + //System.out.println("记录操作: " + record.getDescription()); + } + + /** + * 撤回操作 + */ + public boolean undo() { + if (undoStack.isEmpty()) { + System.out.println("没有可撤回的操作"); + return false; + } + + OperationRecord record = undoStack.pop(); + OperationRecorder recorder = recorderMap.get(record.getOperationType()); + + if (recorder != null) { + try { + // 禁用记录,避免撤回操作被记录 + enabled = false; + recorder.undo(record.getParams()); + // 添加到重做栈 + redoStack.push(record); + //System.out.println("撤回操作: " + record.getDescription()); + return true; + } catch (Exception e) { + System.err.println("撤回操作失败: " + record.getDescription()); + e.printStackTrace(); + // 操作失败,放回撤回栈 + undoStack.push(record); + return false; + } finally { + enabled = true; + } + } + + return false; + } + + /** + * 重做操作 + */ + public boolean redo() { + if (redoStack.isEmpty()) { + System.out.println("没有可重做的操作"); + return false; + } + + OperationRecord record = redoStack.pop(); + OperationRecorder recorder = recorderMap.get(record.getOperationType()); + + if (recorder != null) { + try { + // 禁用记录,避免重做操作被记录 + enabled = false; + recorder.execute(record.getParams()); + // 放回撤回栈 + undoStack.push(record); + System.out.println("重做操作: " + record.getDescription()); + return true; + } catch (Exception e) { + System.err.println("重做操作失败: " + record.getDescription()); + e.printStackTrace(); + // 操作失败,放回重做栈 + redoStack.push(record); + return false; + } finally { + enabled = true; + } + } + + return false; + } + + /** + * 清空所有记录 + */ + public void clear() { + undoStack.clear(); + redoStack.clear(); + } + + /** + * 是否可以撤回 + */ + public boolean canUndo() { + return !undoStack.isEmpty(); + } + + /** + * 是否可以重做 + */ + public boolean canRedo() { + return !redoStack.isEmpty(); + } + + /** + * 获取撤回操作描述 + */ + public String getUndoDescription() { + return undoStack.isEmpty() ? "" : undoStack.peek().getDescription(); + } + + /** + * 获取重做操作描述 + */ + public String getRedoDescription() { + return redoStack.isEmpty() ? "" : redoStack.peek().getDescription(); + } + + /** + * 操作记录内部类 + */ + private static class OperationRecord { + private final String operationType; + private final Object[] params; + private final String description; + private final long timestamp; + + public OperationRecord(String operationType, Object[] params, String description) { + this.operationType = operationType; + this.params = params != null ? params.clone() : new Object[0]; + this.description = description; + this.timestamp = System.currentTimeMillis(); + } + + public String getOperationType() { return operationType; } + public Object[] getParams() { return params; } + public String getDescription() { return description; } + public long getTimestamp() { return timestamp; } + } +} diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationListener.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationListener.java new file mode 100644 index 0000000..ce1fbf5 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationListener.java @@ -0,0 +1,16 @@ +package com.chuangzhou.vivid2D.render.awt.util; + +/** + * 操作监听器接口 + * 用于监听操作历史事件 + */ +public interface OperationListener { + + /** + * 操作事件回调 + * @param operationType 操作类型 + * @param action 动作类型(record, execute, undo, redo, clear) + * @param params 操作参数 + */ + void onOperationEvent(String operationType, String action, Object... params); +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationRecorder.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationRecorder.java new file mode 100644 index 0000000..59f3503 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/OperationRecorder.java @@ -0,0 +1,27 @@ +package com.chuangzhou.vivid2D.render.awt.util; + +/** + * 操作记录接口 + * 用于注册需要支持撤回/重做的操作 + * @author tzdwindows 7 + */ +public interface OperationRecorder { + + /** + * 执行操作(用于重做) + * @param params 操作参数 + */ + void execute(Object... params); + + /** + * 撤销操作 + * @param params 操作参数 + */ + void undo(Object... params); + + /** + * 获取操作描述(用于UI显示) + * @return 操作描述 + */ + String getDescription(); +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/PSD_Structure_Dumper.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/PSD_Structure_Dumper.java new file mode 100644 index 0000000..612bc83 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/PSD_Structure_Dumper.java @@ -0,0 +1,167 @@ +package com.chuangzhou.vivid2D.render.awt.util; + +import java.io.*; +import java.nio.charset.StandardCharsets; + +/** + * PSD文件结构诊断工具 + * 目的:打印出“图层和蒙版信息区段”的详细结构,用于分析非标准PSD文件。 + */ +public class PSD_Structure_Dumper { + + private static final int PREVIEW_BYTES = 16; // 预览的字节数 + + public static void dump(File file) { + System.out.println("=========================================================="); + System.out.println("开始诊断PSD文件: " + file.getName()); + System.out.println("=========================================================="); + + try (FileInputStream fis = new FileInputStream(file); + DataInputStream dis = new DataInputStream(new BufferedInputStream(fis))) { + + // 1. 跳过文件头、颜色模式区、图像资源区 + if (!"8BPS".equals(readString(dis, 4))) throw new IOException("非法的PSD文件签名"); + skipFully(dis, 22); + skipFully(dis, readUInt32(dis)); // Color mode data + skipFully(dis, readUInt32(dis)); // Image resources + + // 2. 进入“图层和蒙版信息区段” + long layerAndMaskLength = readUInt32(dis); + if (layerAndMaskLength == 0) { + System.out.println("文件不包含“图层和蒙版信息区段”。"); + return; + } + System.out.printf("发现“图层和蒙版信息区段”,总长度: %d%n", layerAndMaskLength); + long sectionEndPos = fis.getChannel().position() + layerAndMaskLength; + + long layerInfoLength = readUInt32(dis); + System.out.printf(" - 图层信息块长度: %d%n", layerInfoLength); + if (layerInfoLength == 0) return; + + int layerCount = dis.readShort(); + System.out.printf(" - 文件报告的图层数量: %d%n", layerCount); + if (layerCount < 0) layerCount = -layerCount; + + // 3. 逐一打印每个图层记录的结构 + for (int i = 0; i < layerCount; i++) { + System.out.println("\n--- 开始解析图层记录 " + i + " ---"); + long layerRecordStartPos = fis.getChannel().position(); + System.out.printf("[偏移: %d] 图层坐标 (Top, Left, Bottom, Right): %d, %d, %d, %d%n", + layerRecordStartPos, dis.readInt(), dis.readInt(), dis.readInt(), dis.readInt()); + + int channels = dis.readShort(); + System.out.printf("[偏移: %d] 通道数量: %d. 跳过 %d 字节的通道信息.%n", fis.getChannel().position(), channels, channels * 6); + skipFully(dis, (long) channels * 6); + + String blendSig = readString(dis, 4); + System.out.printf("[偏移: %d] 混合模式签名: '%s'%n", fis.getChannel().position() - 4, blendSig); + if (!"8BIM".equals(blendSig)) { + System.out.println("!!! 错误: 此处签名不是 '8BIM',解析可能已出错。"); + } + + String blendMode = readString(dis, 4); + System.out.printf("[偏移: %d] 混合模式Key: '%s'%n", fis.getChannel().position() - 4, blendMode); + skipFully(dis, 4); // Opacity, Clipping, Flags + + int extraDataLen = dis.readInt(); + System.out.printf("[偏移: %d] 额外数据总长度: %d%n", fis.getChannel().position() - 4, extraDataLen); + long extraDataEndPos = fis.getChannel().position() + extraDataLen; + + // 4. 遍历额外数据中的所有附加信息块 (这是关键) + System.out.println(" --- 遍历额外数据块 ---"); + while (fis.getChannel().position() < extraDataEndPos) { + long blockStartPos = fis.getChannel().position(); + String sig = readString(dis, 4); + if (!"8BIM".equals(sig) && !"8B64".equals(sig)) { + System.out.printf("[偏移: %d] !!! 发现未知签名 '%s',可能已错位,停止解析此图层。%n", blockStartPos, sig); + break; + } + + String key = readString(dis, 4); + long len = readUInt32(dis); + + System.out.printf(" [偏移: %d] 发现数据块: 签名='%s', Key='%s', 长度=%d%n", blockStartPos, sig, key, len); + + // 特别关注图层名称块 'luni' + if ("luni".equals(key)) { + int nameLen = dis.readInt(); + byte[] nameBytes = new byte[nameLen * 2]; + dis.readFully(nameBytes); + String name = new String(nameBytes, StandardCharsets.UTF_16BE); + System.out.printf(" >> 解码为 'luni' (Unicode图层名称): '%s'%n", name); + // 跳过剩余部分 + long alreadyRead = 4 + nameBytes.length; + if (len - alreadyRead > 0) skipFully(dis, len - alreadyRead); + } else { + // 打印其他块的少量预览数据 + byte[] preview = new byte[(int) Math.min(len, PREVIEW_BYTES)]; + dis.readFully(preview); + System.out.printf(" 预览数据: %s ...%n", bytesToHex(preview)); + if (len - preview.length > 0) { + skipFully(dis, len - preview.length); + } + } + // 确保长度是偶数,Photoshop有时会填充一个字节 + if (len % 2 != 0) { + System.out.println(" 检测到奇数长度,跳过1个填充字节。"); + skipFully(dis, 1); + } + } + System.out.println(" --- 额外数据块遍历结束 ---"); + // 确保指针移动到下一个图层记录的开始 + if(fis.getChannel().position() != extraDataEndPos) { + long diff = extraDataEndPos - fis.getChannel().position(); + System.out.printf("!!! 指针与预期不符,强制跳过 %d 字节以对齐下一个图层%n", diff); + skipFully(dis, diff); + } + } + System.out.println("\n--- 所有图层记录解析完毕 ---"); + + + } catch (Exception e) { + System.out.println("\n!!!!!! 在诊断过程中发生严重错误 !!!!!!"); + e.printStackTrace(); + } finally { + System.out.println("=========================================================="); + System.out.println("诊断结束"); + System.out.println("=========================================================="); + } + } + + // --- 辅助方法 --- + private static String readString(DataInputStream dis, int len) throws IOException { + byte[] bytes = new byte[len]; + dis.readFully(bytes); + return new String(bytes, StandardCharsets.US_ASCII); + } + + private static long readUInt32(DataInputStream dis) throws IOException { + return dis.readInt() & 0xFFFFFFFFL; + } + + private static void skipFully(DataInputStream dis, long bytes) throws IOException { + if (bytes <= 0) return; + long remaining = bytes; + while (remaining > 0) { + long skipped = dis.skip(remaining); + if (skipped <= 0) throw new IOException("Skip failed"); + remaining -= skipped; + } + } + + private static String bytesToHex(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + sb.append(String.format("%02X ", b)); + } + return sb.toString(); + } + + public static void main(String[] args) { + File fileToDiagnose = new File("G:\\鬼畜素材\\工作间\\川普-风催雨\\川普-风催雨.psd"); + System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8)); + System.setErr(new PrintStream(System.err, true, StandardCharsets.UTF_8)); + PSD_Structure_Dumper.dump(fileToDiagnose); + } +} + diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/PsdParser.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/PsdParser.java new file mode 100644 index 0000000..0756ce0 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/PsdParser.java @@ -0,0 +1,448 @@ +package com.chuangzhou.vivid2D.render.awt.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.stream.ImageInputStream; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileInputStream; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * PSD文件解析工具类 - 基于 TwelveMonkeys PSDMetadata(修复版) + */ +public class PsdParser { + private static final Logger logger = LoggerFactory.getLogger(PsdParser.class); + + public static class PSDLayerInfo { + public String name; + public BufferedImage image; + public float opacity; + public boolean visible; + public int x, y; + public int width, height; + public int left, top, right, bottom; + + public PSDLayerInfo(String name, BufferedImage image, float opacity, boolean visible, + int left, int top, int right, int bottom) { + this.name = name; + this.image = image; + this.opacity = opacity; + this.visible = visible; + this.left = left; + this.top = top; + this.right = right; + this.bottom = bottom; + this.x = left; + this.y = top; + this.width = right - left; + this.height = bottom - top; + } + } + + public static class PSDImportResult { + public List layers = new ArrayList<>(); + public int documentWidth = -1; + public int documentHeight = -1; + public BufferedImage mergedImage; + } + + /** + * 主解析方法 + */ + public static PSDImportResult parsePSDFile(File psdFile) throws Exception { + PSDImportResult result = new PSDImportResult(); + + ImageReader reader = findPSDImageReader(); + if (reader == null) { + throw new RuntimeException("系统不支持PSD文件格式,请安装 TwelveMonkeys imageio-psd 插件"); + } + + try (FileInputStream fis = new FileInputStream(psdFile); + ImageInputStream iis = ImageIO.createImageInputStream(fis)) { + + reader.setInput(iis); + + // 读取文档尺寸 + result.documentWidth = reader.getWidth(0); + result.documentHeight = reader.getHeight(0); + logger.info("文档尺寸: {}x{}", result.documentWidth, result.documentHeight); + + // 获取元数据 + IIOMetadata metadata = reader.getImageMetadata(0); + + // 尝试从元数据中提取图层信息 + if (metadata != null) { + extractLayerInfoFromMetadata(metadata, result, reader); + } else { + logger.warn("无法获取元数据,使用备用解析方法"); + parseUsingImageIndices(reader, result); + } + + // 读取合并图像 + try { + result.mergedImage = reader.read(0); + } catch (Exception e) { + logger.warn("无法读取合并图像: {}", e.getMessage()); + } + + } finally { + try { reader.dispose(); } catch (Exception ignored) {} + } + + return result; + } + + /** + * 从元数据中提取图层信息 + */ + private static void extractLayerInfoFromMetadata(IIOMetadata metadata, + PSDImportResult result, + ImageReader reader) { + try { + // 尝试访问 TwelveMonkeys 的 PSDMetadata + if (metadata.getClass().getName().equals("com.twelvemonkeys.imageio.plugins.psd.PSDMetadata")) { + extractFromPSDMetadata(metadata, result, reader); + } else { + // 尝试从标准元数据格式中提取 + extractFromStandardMetadata(metadata, result, reader); + } + } catch (Exception e) { + logger.error("从元数据提取图层信息失败: {}", e.getMessage()); + parseUsingImageIndices(reader, result); + } + } + + /** + * 从 TwelveMonkeys 的 PSDMetadata 中提取图层信息 + */ + private static void extractFromPSDMetadata(IIOMetadata metadata, + PSDImportResult result, + ImageReader reader) { + try { + // 使用反射访问 PSDMetadata 的私有字段 + Class psdMetadataClass = metadata.getClass(); + + // 获取 layerInfo 字段 + Field layerInfoField = psdMetadataClass.getDeclaredField("layerInfo"); + layerInfoField.setAccessible(true); + + @SuppressWarnings("unchecked") + List layerInfos = (List) layerInfoField.get(metadata); + + if (layerInfos != null && !layerInfos.isEmpty()) { + logger.info("从 PSDMetadata 中找到 {} 个图层", layerInfos.size()); + + for (int i = 0; i < layerInfos.size(); i++) { + try { + Object twelveMonkeysLayer = layerInfos.get(i); + PSDLayerInfo layer = createLayerInfoFromTwelveMonkeys(twelveMonkeysLayer, reader, i); + if (layer != null) { + result.layers.add(layer); + } + } catch (Exception e) { + logger.error("处理图层 {} 失败: {}", i, e.getMessage()); + } + } + } else { + logger.info("PSDMetadata 中没有图层信息,使用图像索引方式"); + parseUsingImageIndices(reader, result); + } + + } catch (Exception e) { + logger.error("访问 PSDMetadata 失败: {}", e.getMessage()); + parseUsingImageIndices(reader, result); + } + } + + /** + * 从 TwelveMonkeys 的图层对象创建我们的图层信息 + */ + private static PSDLayerInfo createLayerInfoFromTwelveMonkeys(Object twelveMonkeysLayer, + ImageReader reader, + int layerIndex) { + try { + Class layerClass = twelveMonkeysLayer.getClass(); + + // 提取基本几何信息 + int top = getIntField(layerClass, twelveMonkeysLayer, "top"); + int left = getIntField(layerClass, twelveMonkeysLayer, "left"); + int bottom = getIntField(layerClass, twelveMonkeysLayer, "bottom"); + int right = getIntField(layerClass, twelveMonkeysLayer, "right"); + + // 提取图层名称 + String layerName = extractLayerName(layerClass, twelveMonkeysLayer); + + // 提取可见性和不透明度 + boolean visible = extractVisibility(layerClass, twelveMonkeysLayer); + float opacity = extractOpacity(layerClass, twelveMonkeysLayer); + + // 读取图层图像 + BufferedImage layerImage = readLayerImage(reader, layerIndex); + + // 如果无法读取图像,创建占位符 + if (layerImage == null) { + int width = right - left; + int height = bottom - top; + if (width > 0 && height > 0) { + layerImage = createPlaceholderImage(width, height); + } + } + + return new PSDLayerInfo(layerName, layerImage, opacity, visible, left, top, right, bottom); + + } catch (Exception e) { + logger.error("创建图层信息失败: {}", e.getMessage()); + return null; + } + } + + /** + * 提取图层名称 + */ + private static String extractLayerName(Class layerClass, Object layer) { + try { + // 先尝试获取 unicodeLayerName + Field unicodeNameField = layerClass.getDeclaredField("unicodeLayerName"); + unicodeNameField.setAccessible(true); + String unicodeName = (String) unicodeNameField.get(layer); + if (unicodeName != null && !unicodeName.trim().isEmpty()) { + return unicodeName.trim(); + } + + // 然后尝试获取 layerName + Field nameField = layerClass.getDeclaredField("layerName"); + nameField.setAccessible(true); + String name = (String) nameField.get(layer); + if (name != null && !name.trim().isEmpty()) { + return name.trim(); + } + } catch (Exception e) { + logger.debug("无法提取图层名称: {}", e.getMessage()); + } + + return "Layer_" + System.identityHashCode(layer); + } + + /** + * 提取可见性 + */ + private static boolean extractVisibility(Class layerClass, Object layer) { + try { + Field blendModeField = layerClass.getDeclaredField("blendMode"); + blendModeField.setAccessible(true); + Object blendMode = blendModeField.get(layer); + + if (blendMode != null) { + Class blendModeClass = blendMode.getClass(); + Field flagsField = blendModeClass.getDeclaredField("flags"); + flagsField.setAccessible(true); + int flags = flagsField.getInt(blendMode); + // 第2位为0表示可见 + return (flags & 0x02) == 0; + } + } catch (Exception e) { + logger.debug("无法提取可见性: {}", e.getMessage()); + } + return true; // 默认可见 + } + + /** + * 提取不透明度 + */ + private static float extractOpacity(Class layerClass, Object layer) { + try { + Field blendModeField = layerClass.getDeclaredField("blendMode"); + blendModeField.setAccessible(true); + Object blendMode = blendModeField.get(layer); + + if (blendMode != null) { + Class blendModeClass = blendMode.getClass(); + Field opacityField = blendModeClass.getDeclaredField("opacity"); + opacityField.setAccessible(true); + int opacity = opacityField.getInt(blendMode); + return opacity / 255.0f; // 转换为 0.0-1.0 范围 + } + } catch (Exception e) { + logger.debug("无法提取不透明度: {}", e.getMessage()); + } + return 1.0f; // 默认不透明度 + } + + /** + * 获取整数字段值 + */ + private static int getIntField(Class clazz, Object obj, String fieldName) { + try { + Field field = clazz.getDeclaredField(fieldName); + field.setAccessible(true); + return field.getInt(obj); + } catch (Exception e) { + logger.debug("无法获取字段 {}: {}", fieldName, e.getMessage()); + return 0; + } + } + + /** + * 读取图层图像 + */ + private static BufferedImage readLayerImage(ImageReader reader, int layerIndex) { + try { + // 图层索引从1开始(0是合并图像) + int imageIndex = layerIndex + 1; + if (imageIndex < reader.getNumImages(true)) { + return reader.read(imageIndex); + } + } catch (Exception e) { + logger.debug("无法读取图层 {} 的图像: {}", layerIndex, e.getMessage()); + } + return null; + } + + /** + * 从标准元数据格式中提取图层信息 + */ + private static void extractFromStandardMetadata(IIOMetadata metadata, + PSDImportResult result, + ImageReader reader) { + try { + // 尝试从标准元数据节点中提取图层信息 + org.w3c.dom.Node tree = metadata.getAsTree("com_twelvemonkeys_imageio_psd_image_1.0"); + if (tree != null) { + extractFromMetadataTree(tree, result, reader); + } else { + parseUsingImageIndices(reader, result); + } + } catch (Exception e) { + logger.error("从标准元数据提取失败: {}", e.getMessage()); + parseUsingImageIndices(reader, result); + } + } + + /** + * 从元数据树中提取图层信息 + */ + private static void extractFromMetadataTree(org.w3c.dom.Node tree, + PSDImportResult result, + ImageReader reader) { + // 这里可以添加从 DOM 树中解析图层信息的逻辑 + // 由于比较复杂,暂时使用备用方法 + parseUsingImageIndices(reader, result); + } + + /** + * 备用方法:使用图像索引解析图层 + */ + private static void parseUsingImageIndices(ImageReader reader, PSDImportResult result) { + try { + int numImages = reader.getNumImages(true); + logger.info("使用图像索引方式,找到 {} 个图像", numImages); + + // 从索引1开始读取图层(索引0是合并图像) + for (int i = 1; i < numImages; i++) { + try { + BufferedImage layerImage = reader.read(i); + if (layerImage != null) { + String layerName = "Layer_" + i; + PSDLayerInfo layer = new PSDLayerInfo( + layerName, layerImage, 1.0f, true, + 0, 0, layerImage.getWidth(), layerImage.getHeight() + ); + result.layers.add(layer); + + logger.info("读取图层 {}: '{}' 尺寸 {}x{}", i, layerName, layerImage.getWidth(), layerImage.getHeight()); + } + } catch (Exception e) { + logger.error("读取图层 {} 失败: {}", i, e.getMessage()); + } + } + } catch (Exception e) { + logger.error("使用图像索引方式解析失败: {}", e.getMessage()); + } + } + + /** + * 创建占位符图像 + */ + private static BufferedImage createPlaceholderImage(int width, int height) { + if (width <= 0 || height <= 0) { + return null; + } + + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int alpha = 128; + int red = (x * 255 / width) & 0xFF; + int green = (y * 255 / height) & 0xFF; + int blue = 128; + + int rgb = (alpha << 24) | (red << 16) | (green << 8) | blue; + image.setRGB(x, y, rgb); + } + } + + return image; + } + + /** + * 查找 PSD ImageReader + */ + private static ImageReader findPSDImageReader() { + try { + Iterator it = ImageIO.getImageReadersByFormatName("psd"); + if (it.hasNext()) return it.next(); + it = ImageIO.getImageReadersByMIMEType("image/vnd.adobe.photoshop"); + if (it.hasNext()) return it.next(); + it = ImageIO.getImageReadersBySuffix("psd"); + if (it.hasNext()) return it.next(); + } catch (Exception e) { + logger.debug("查找 PSD ImageReader 失败: {}", e.getMessage()); + } + return null; + } + + public static boolean isPSDSupported() { + return findPSDImageReader() != null; + } + + public static String getPSDSupportInfo() { + ImageReader r = findPSDImageReader(); + return (r != null) ? ("PSD 支持: " + r.getClass().getName()) : "PSD 不支持(请安装 TwelveMonkeys imageio-psd 插件)"; + } + + /** + * 简单测试方法 + */ + public static void main(String[] args) { + try { + File psdFile = new File("test.psd"); + if (!psdFile.exists()) { + System.out.println("测试文件不存在: " + psdFile.getAbsolutePath()); + return; + } + + PSDImportResult result = parsePSDFile(psdFile); + + System.out.println("文档尺寸: " + result.documentWidth + "x" + result.documentHeight); + System.out.println("图层数量: " + result.layers.size()); + + for (PSDLayerInfo layer : result.layers) { + System.out.printf("图层: %s, 位置: (%d, %d), 尺寸: %dx%d, 可见: %s, 不透明度: %.2f%n", + layer.name, layer.x, layer.y, layer.width, layer.height, + layer.visible, layer.opacity); + } + + } catch (Exception e) { + e.printStackTrace(); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java index 033013e..e453391 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java @@ -53,6 +53,7 @@ public class ModelPart { private boolean pivotInitialized; private final List events = new ArrayList<>(); + private boolean inMultiSelectionOperation = false; // ====== 液化模式枚举 ====== public enum LiquifyMode { @@ -66,6 +67,7 @@ public class ModelPart { TURBULENCE // 湍流(噪声扰动) } + // ==================== 构造器 ==================== public ModelPart() { @@ -113,6 +115,268 @@ public class ModelPart { } } + // ==================== 多选支持 ==================== + + /** + * 标记多选状态需要更新 + */ + public void markMultiSelectionDirty() { + List selectedMeshes = getSelectedMeshes(); + for (Mesh2D mesh : selectedMeshes) { + mesh.markDirty(); + if (mesh.isInMultiSelection()) { + mesh.forceUpdateMultiSelectionBounds(); + } + } + } + + /** + * 更新所有选中网格的多选列表 + */ + private void updateMultiSelectionInMeshes() { + List selectedMeshes = getSelectedMeshes(); + if (selectedMeshes.size() <= 1) { + // 单选或没有选中,清除所有多选列表 + for (Mesh2D mesh : meshes) { + mesh.clearMultiSelection(); + } + return; + } + + // 多选状态,更新每个选中网格的多选列表 + for (Mesh2D selectedMesh : selectedMeshes) { + selectedMesh.clearMultiSelection(); + for (Mesh2D otherMesh : selectedMeshes) { + if (otherMesh != selectedMesh) { + selectedMesh.addToMultiSelection(otherMesh); + } + } + } + } + + /** + * 获取当前选中的所有网格(从所有网格中筛选出选中的) + */ + public List getSelectedMeshes() { + List selected = new ArrayList<>(); + for (Mesh2D mesh : meshes) { + if (mesh.isSelected()) { + selected.add(mesh); + } + } + return selected; + } + + /** + * 获取多选状态下的组合边界框 + */ + public BoundingBox getMultiSelectionBounds() { + BoundingBox bounds = new BoundingBox(); + List selectedMeshes = getSelectedMeshes(); + + if (selectedMeshes.isEmpty()) { + return bounds; + } + + // 使用第一个选中网格的多选边界框(如果处于多选状态) + Mesh2D firstSelected = selectedMeshes.get(0); + if (firstSelected.isInMultiSelection()) { + return firstSelected.getMultiSelectionBounds(); + } + + // 否则计算所有选中网格的组合边界框 + for (Mesh2D mesh : selectedMeshes) { + BoundingBox meshBounds = mesh.getBounds(); + if (meshBounds.isValid()) { + bounds.expand(meshBounds); + } + } + + return bounds; + } + + /** + * 检查是否处于多选状态 + */ + public boolean isInMultiSelection() { + List selectedMeshes = getSelectedMeshes(); + if (selectedMeshes.isEmpty()) { + return false; + } + + // 如果任意选中的网格处于多选状态,则认为整个部件处于多选状态 + for (Mesh2D mesh : selectedMeshes) { + if (mesh.isInMultiSelection()) { + return true; + } + } + + return selectedMeshes.size() > 1; + } + + /** + * 获取多选状态下的中心点 + */ + public Vector2f getMultiSelectionCenter() { + BoundingBox bounds = getMultiSelectionBounds(); + return bounds.getCenter(); + } + + /** + * 移动所有选中的网格(整体移动) + */ + public void moveSelectedMeshes(float dx, float dy) { + List selectedMeshes = getSelectedMeshes(); + if (selectedMeshes.isEmpty()) return; + + // 如果是多选状态,整体移动 + if (isInMultiSelection()) { + // 整体移动:所有选中网格使用相同的位移 + for (Mesh2D mesh : selectedMeshes) { + ModelPart meshPart = findPartByMesh(mesh); + if (meshPart != null) { + Vector2f pos = meshPart.getPosition(); + meshPart.setPosition(pos.x + dx, pos.y + dy); + } + } + } else { + // 单选状态,只移动选中的网格 + for (Mesh2D mesh : selectedMeshes) { + ModelPart meshPart = findPartByMesh(mesh); + if (meshPart != null) { + Vector2f pos = meshPart.getPosition(); + meshPart.setPosition(pos.x + dx, pos.y + dy); + } + } + } + + triggerEvent("multiSelectionMove"); + } + + /** + * 旋转所有选中的网格(整体旋转) + */ + public void rotateSelectedMeshes(float deltaAngle) { + List selectedMeshes = getSelectedMeshes(); + if (selectedMeshes.isEmpty()) return; + + // 如果是多选状态,整体旋转 + if (isInMultiSelection()) { + Vector2f center = getMultiSelectionCenter(); + + for (Mesh2D mesh : selectedMeshes) { + ModelPart meshPart = findPartByMesh(mesh); + if (meshPart != null) { + // 计算相对于中心点的旋转 + Vector2f meshPos = meshPart.getPosition(); + Vector2f relativePos = new Vector2f(meshPos.x - center.x, meshPos.y - center.y); + + // 应用旋转 + float cos = (float) Math.cos(deltaAngle); + float sin = (float) Math.sin(deltaAngle); + float newX = center.x + relativePos.x * cos - relativePos.y * sin; + float newY = center.y + relativePos.x * sin + relativePos.y * cos; + + meshPart.setPosition(newX, newY); + meshPart.rotate(deltaAngle); + } + } + } else { + // 单选状态,各自绕自己的中心旋转 + for (Mesh2D mesh : selectedMeshes) { + ModelPart meshPart = findPartByMesh(mesh); + if (meshPart != null) { + meshPart.rotate(deltaAngle); + } + } + } + + triggerEvent("multiSelectionRotate"); + } + + /** + * 缩放所有选中的网格(整体缩放) + */ + public void scaleSelectedMeshes(float scaleX, float scaleY) { + List selectedMeshes = getSelectedMeshes(); + if (selectedMeshes.isEmpty()) return; + + // 如果是多选状态,整体缩放 + if (isInMultiSelection()) { + Vector2f center = getMultiSelectionCenter(); + + for (Mesh2D mesh : selectedMeshes) { + ModelPart meshPart = findPartByMesh(mesh); + if (meshPart != null) { + // 计算相对于中心点的缩放 + Vector2f meshPos = meshPart.getPosition(); + Vector2f relativePos = new Vector2f(meshPos.x - center.x, meshPos.y - center.y); + + // 应用缩放 + float newX = center.x + relativePos.x * scaleX; + float newY = center.y + relativePos.y * scaleY; + + meshPart.setPosition(newX, newY); + meshPart.scale(scaleX, scaleY); + } + } + } else { + // 单选状态,各自绕自己的中心缩放 + for (Mesh2D mesh : selectedMeshes) { + ModelPart meshPart = findPartByMesh(mesh); + if (meshPart != null) { + meshPart.scale(scaleX, scaleY); + } + } + } + + triggerEvent("multiSelectionScale"); + } + + /** + * 通过网格查找对应的 ModelPart(递归查找) + */ + private ModelPart findPartByMesh(Mesh2D targetMesh) { + // 先检查当前部件的网格 + for (Mesh2D mesh : meshes) { + if (mesh == targetMesh) { + return this; + } + } + + // 递归检查子部件 + for (ModelPart child : children) { + ModelPart found = child.findPartByMeshRecursive(targetMesh); + if (found != null) { + return found; + } + } + + return null; + } + + /** + * 递归查找包含指定网格的部件 + */ + private ModelPart findPartByMeshRecursive(Mesh2D targetMesh) { + // 检查当前部件的网格 + for (Mesh2D mesh : meshes) { + if (mesh == targetMesh) { + return this; + } + } + + // 递归检查子部件 + for (ModelPart child : children) { + ModelPart found = child.findPartByMeshRecursive(targetMesh); + if (found != null) { + return found; + } + } + + return null; + } + // ==================== 层级管理 ==================== /** @@ -492,13 +756,46 @@ public class ModelPart { * 设置位置 */ public void setPosition(float x, float y) { - position.set(x, y); + // 防止递归调用 + if (inMultiSelectionOperation) { + // 直接执行单选择辑,避免递归 + position.set(x, y); + markTransformDirty(); + updateLocalTransform(); + recomputeWorldTransformRecursive(); + for (Mesh2D mesh : meshes) { + Vector2f worldPivot = Matrix3fUtils.transformPoint(worldTransform, mesh.getOriginalPivot()); + mesh.setPivot(worldPivot.x, worldPivot.y); + } + + updateMeshVertices(); + triggerEvent("position"); + return; + } + + // 如果是多选状态下的移动,使用多选移动方法 + if (isInMultiSelection() && !getSelectedMeshes().isEmpty()) { + Vector2f currentPos = getPosition(); + float dx = x - currentPos.x; + float dy = y - currentPos.y; + + // 设置标志防止递归 + inMultiSelectionOperation = true; + try { + moveSelectedMeshes(dx, dy); + } finally { + inMultiSelectionOperation = false; + } + return; + } + + // 原有单选择辑 + position.set(x, y); markTransformDirty(); updateLocalTransform(); recomputeWorldTransformRecursive(); - // 不修改 originalPivot,只同步 mesh world pivot for (Mesh2D mesh : meshes) { Vector2f worldPivot = Matrix3fUtils.transformPoint(worldTransform, mesh.getOriginalPivot()); mesh.setPivot(worldPivot.x, worldPivot.y); @@ -508,6 +805,7 @@ public class ModelPart { triggerEvent("position"); } + /** * 更新所有网格的顶点位置以反映当前变换 */ @@ -621,23 +919,25 @@ public class ModelPart { * 设置旋转(弧度) */ public void setRotation(float radians) { - // 记录旧的世界变换,用于计算 pivot 的相对位置 - Matrix3f oldWorldTransform = new Matrix3f(this.worldTransform); + // 如果是多选状态下的旋转,使用多选旋转方法 + if (isInMultiSelection() && !getSelectedMeshes().isEmpty()) { + float currentRotation = getRotation(); + float deltaAngle = radians - currentRotation; + rotateSelectedMeshes(deltaAngle); + return; + } + // 原有单选择辑 + Matrix3f oldWorldTransform = new Matrix3f(this.worldTransform); this.rotation = radians; markTransformDirty(); updateLocalTransform(); recomputeWorldTransformRecursive(); - // 旋转操作会改变部件的局部坐标系,因此需要更新网格的 originalPivot for (Mesh2D mesh : meshes) { - // 将 mesh 的原始局部 pivot 变换到旧的世界坐标系 Vector2f oldWorldPivot = Matrix3fUtils.transformPoint(oldWorldTransform, mesh.getOriginalPivot()); - // 将旧的世界 pivot 逆变换回新的局部坐标系(即新的 originalPivot) Vector2f newLocalOriginalPivot = Matrix3fUtils.transformPointInverse(this.worldTransform, oldWorldPivot); - mesh.setOriginalPivot(newLocalOriginalPivot); - // 同时更新 mesh 的当前 pivot 到新的世界坐标 mesh.setPivot(Matrix3fUtils.transformPoint(this.worldTransform, newLocalOriginalPivot)); } @@ -653,6 +953,7 @@ public class ModelPart { markTransformDirty(); updateLocalTransform(); recomputeWorldTransformRecursive(); + updateMeshVertices(); triggerEvent("rotation"); } @@ -660,9 +961,17 @@ public class ModelPart { * 设置缩放 */ public void setScale(float sx, float sy) { - // 记录旧的世界变换,用于计算 pivot 的相对位置 - Matrix3f oldWorldTransform = new Matrix3f(this.worldTransform); + // 如果是多选状态下的缩放,使用多选缩放方法 + if (isInMultiSelection() && !getSelectedMeshes().isEmpty()) { + Vector2f currentScale = getScale(); + float scaleX = sx / currentScale.x; + float scaleY = sy / currentScale.y; + scaleSelectedMeshes(scaleX, scaleY); + return; + } + // 原有单选择辑 + Matrix3f oldWorldTransform = new Matrix3f(this.worldTransform); this.scaleX = sx; this.scaleY = sy; scale.set(sx, sy); @@ -670,21 +979,18 @@ public class ModelPart { updateLocalTransform(); recomputeWorldTransformRecursive(); - // 缩放操作会改变部件的局部坐标系,因此需要更新网格的 originalPivot for (Mesh2D mesh : meshes) { - // 将 mesh 的原始局部 pivot 变换到旧的世界坐标系 Vector2f oldWorldPivot = Matrix3fUtils.transformPoint(oldWorldTransform, mesh.getOriginalPivot()); - // 将旧的世界 pivot 逆变换回新的局部坐标系(即新的 originalPivot) Vector2f newLocalOriginalPivot = Matrix3fUtils.transformPointInverse(this.worldTransform, oldWorldPivot); - mesh.setOriginalPivot(newLocalOriginalPivot); - // 同时更新 mesh 的当前 pivot 到新的世界坐标 mesh.setPivot(Matrix3fUtils.transformPoint(this.worldTransform, newLocalOriginalPivot)); } + updateMeshVertices(); triggerEvent("scale"); } + public void setScale(float uniformScale) { // 记录旧的世界变换,用于计算 pivot 的相对位置 Matrix3f oldWorldTransform = new Matrix3f(this.worldTransform); @@ -1114,6 +1420,7 @@ public class ModelPart { @Override public String toString() { + List selectedMeshes = getSelectedMeshes(); return "ModelPart{" + "name='" + name + '\'' + ", position=" + position + @@ -1122,6 +1429,8 @@ public class ModelPart { ", visible=" + visible + ", children=" + children.size() + ", meshes=" + meshes.size() + + ", selectedMeshes=" + selectedMeshes.size() + + ", inMultiSelection=" + isInMultiSelection() + '}'; } } \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java index f99320c..ddbd818 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java @@ -8,6 +8,8 @@ import org.joml.Vector2f; import java.nio.FloatBuffer; import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import org.joml.Vector4f; @@ -53,6 +55,11 @@ public class Mesh2D { private Vector2f pivot = new Vector2f(0, 0); private Vector2f originalPivot = new Vector2f(0, 0); + // ==================== 多选支持 ==================== + private final List multiSelectedParts = new ArrayList<>(); + private final BoundingBox multiSelectionBounds = new BoundingBox(); + private boolean multiSelectionDirty = true; + // ==================== 常量 ==================== public static final int POINTS = 0; public static final int LINES = 1; @@ -60,7 +67,6 @@ public class Mesh2D { public static final int TRIANGLES = 3; public static final int TRIANGLE_STRIP = 4; public static final int TRIANGLE_FAN = 5; - private static final float ROTATION_HANDLE_DISTANCE = 30.0f; // ==================== 构造器 ==================== @@ -81,6 +87,8 @@ public class Mesh2D { setMeshData(vertices, uvs, indices); } + + // ==================== 网格数据设置 ==================== /** @@ -402,8 +410,119 @@ public class Mesh2D { * 检查点是否在网格内(使用边界框近似) */ public boolean containsPoint(float x, float y) { + return containsPoint(x, y, false); + } + + /** + * 检查点是否在网格内(可选择精确检测) + */ + public boolean containsPoint(float x, float y, boolean precise) { + if (isInMultiSelection()) { + BoundingBox multiBounds = getMultiSelectionBounds(); + boolean inBounds = x >= multiBounds.getMinX() && x <= multiBounds.getMaxX() && + y >= multiBounds.getMinY() && y <= multiBounds.getMaxY(); + + if (precise && inBounds) { + // 在多选边界框内时,进一步检查是否在任意选中的网格几何形状内 + return isPointInAnySelectedMesh(x, y); + } + return inBounds; + } + BoundingBox b = getBounds(); - return x >= b.getMinX() && x <= b.getMaxX() && y >= b.getMinY() && y <= b.getMaxY(); + boolean inBounds = x >= b.getMinX() && x <= b.getMaxX() && y >= b.getMinY() && y <= b.getMaxY(); + + if (precise && inBounds) { + // 精确检测点是否在网格几何形状内 + return isPointInMeshGeometry(x, y); + } + return inBounds; + } + + /** + * 检查点是否在任意选中的网格几何形状内 + */ + private boolean isPointInAnySelectedMesh(float x, float y) { + // 检查自己 + if (isPointInMeshGeometry(x, y)) { + return true; + } + + // 检查多选列表中的其他网格 + for (Mesh2D mesh : multiSelectedParts) { + if (mesh.isPointInMeshGeometry(x, y)) { + return true; + } + } + return false; + } + + /** + * 精确检测点是否在网格几何形状内(使用射线法) + */ + private boolean isPointInMeshGeometry(float x, float y) { + // 简单的边界框检测先过滤掉明显不在的 + BoundingBox b = getBounds(); + if (x < b.getMinX() || x > b.getMaxX() || y < b.getMinY() || y > b.getMaxY()) { + return false; + } + + // 使用射线法进行精确检测 + return isPointInPolygon(x, y, vertices); + } + + /** + * 使用射线法判断点是否在多边形内 + */ + private boolean isPointInPolygon(float x, float y, float[] vertices) { + if (vertices.length < 6) { // 至少需要3个点组成三角形 + return false; + } + + boolean inside = false; + int n = vertices.length / 2; + + for (int i = 0, j = n - 1; i < n; j = i++) { + float xi = vertices[i * 2]; + float yi = vertices[i * 2 + 1]; + float xj = vertices[j * 2]; + float yj = vertices[j * 2 + 1]; + + // 检查点是否在多边形的边上 + if (isPointOnLineSegment(x, y, xi, yi, xj, yj)) { + return true; + } + + // 射线法核心逻辑 + if (((yi > y) != (yj > y)) && + (x < (xj - xi) * (y - yi) / (yj - yi) + xi)) { + inside = !inside; + } + } + + return inside; + } + + /** + * 检查点是否在线段上 + */ + private boolean isPointOnLineSegment(float x, float y, float x1, float y1, float x2, float y2) { + float cross = (x - x1) * (y2 - y1) - (y - y1) * (x2 - x1); + if (Math.abs(cross) > 1e-6) { + return false; // 不在直线上 + } + + float dot = (x - x1) * (x2 - x1) + (y - y1) * (y2 - y1); + if (dot < 0) { + return false; // 在线段起点之前 + } + + float squaredLength = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1); + if (dot > squaredLength) { + return false; // 在线段终点之后 + } + + return true; } public boolean containsPoint(Vector2f point) { @@ -488,7 +607,6 @@ public class Mesh2D { } // ==================== 状态管理 ==================== - /** * 标记数据已修改 */ @@ -496,6 +614,7 @@ public class Mesh2D { deleteGPU(); this.dirty = true; this.boundsDirty = true; + this.multiSelectionDirty = true; // 新增:标记多选边界框需要更新 } /** @@ -624,7 +743,12 @@ public class Mesh2D { RenderSystem.uniform4f(colorLoc, 1.0f, 1.0f, 1.0f, 1.0f); } } - drawSelectBox(); + + if (isInMultiSelection()) { + drawMultiSelectionBox(); + } else { + drawSelectBox(); + } } finally { if (currentProgram != 0) { RenderSystem.useProgram(currentProgram); @@ -689,6 +813,302 @@ public class Mesh2D { drawRotationHandle(bb, minX, minY, maxX, maxY); } + /** + * 添加网格到多选列表 + */ + public void addToMultiSelection(Mesh2D mesh) { + if (mesh != null && !multiSelectedParts.contains(mesh)) { + multiSelectedParts.add(mesh); + multiSelectionDirty = true; + markDirty(); + } + } + + /** + * 从多选列表移除网格 + */ + public void removeFromMultiSelection(Mesh2D mesh) { + if (multiSelectedParts.remove(mesh)) { + multiSelectionDirty = true; + markDirty(); + } + } + + /** + * 清空多选列表 + */ + public void clearMultiSelection() { + if (!multiSelectedParts.isEmpty()) { + multiSelectedParts.clear(); + multiSelectionDirty = true; + markDirty(); + } + } + + /** + * 获取多选列表 + */ + public List getMultiSelectedParts() { + return new ArrayList<>(multiSelectedParts); + } + + /** + * 检查是否在多选状态 + */ + public boolean isInMultiSelection() { + return !multiSelectedParts.isEmpty(); + } + + /** + * 获取多选状态下的组合边界框 + */ + public BoundingBox getMultiSelectionBounds() { + if (multiSelectionDirty) { + updateMultiSelectionBounds(); + } + return multiSelectionBounds; + } + + /** + * 更新多选边界框 + */ + private void updateMultiSelectionBounds() { + multiSelectionBounds.reset(); + + // 首先包含自己的边界(应用变换后的边界) + BoundingBox selfBounds = getBounds(); + if (selfBounds.isValid()) { + multiSelectionBounds.expand(selfBounds); + } + + // 然后包含所有多选部分的边界(应用它们各自的变换) + for (Mesh2D mesh : multiSelectedParts) { + // 确保其他网格的边界也是最新的 + mesh.updateBounds(); + BoundingBox meshBounds = mesh.getBounds(); + if (meshBounds.isValid()) { + multiSelectionBounds.expand(meshBounds); + } + } + + multiSelectionDirty = false; + } + + /** + * 强制更新多选边界框(在外部变换操作后调用) + */ + public void forceUpdateMultiSelectionBounds() { + multiSelectionDirty = true; + updateMultiSelectionBounds(); + } + + /** + * 检查点是否在多选边界框内 + */ + public boolean multiSelectionContainsPoint(float x, float y) { + if (!isInMultiSelection()) { + return containsPoint(x, y); + } + + BoundingBox multiBounds = getMultiSelectionBounds(); + return multiBounds.contains(x, y); + } + + public boolean multiSelectionContainsPoint(Vector2f point) { + return multiSelectionContainsPoint(point.x, point.y); + } + + /** + * 在多选状态下绘制组合边界框 + */ + private void drawMultiSelectionBox() { + if (!isInMultiSelection()) { + drawSelectBox(); + return; + } + BoundingBox multiBounds = getMultiSelectionBounds(); + if (!multiBounds.isValid()) return; + float minX = multiBounds.getMinX(); + float minY = multiBounds.getMinY(); + float maxX = multiBounds.getMaxX(); + float maxY = multiBounds.getMaxY(); + BufferBuilder bb = new BufferBuilder(); + RenderSystem.enableBlend(); + RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + drawDashedBorder(bb, minX, minY, maxX, maxY); + final float CORNER_SIZE = 8.0f; + final float BORDER_THICKNESS = 6.0f; + drawMultiSelectionResizeHandles(bb, minX, minY, maxX, maxY, CORNER_SIZE, BORDER_THICKNESS); + drawMultiSelectionCenterPoint(bb, minX, minY, maxX, maxY); + drawMultiSelectionRotationHandle(bb, minX, minY, maxX, maxY); + } + + /** + * 绘制虚线边框 + */ + private void drawDashedBorder(BufferBuilder bb, float minX, float minY, float maxX, float maxY) { + final float DASH_LENGTH = 8.0f; // 虚线段的长度 + final float GAP_LENGTH = 4.0f; // 间隔的长度 + final Vector4f BORDER_COLOR = new Vector4f(1.0f, 1.0f, 0.0f, 1.0f); // 黄色虚线 + + float width = maxX - minX; + float height = maxY - minY; + + // 绘制上边虚线 + drawDashedLine(bb, minX, minY, maxX, minY, DASH_LENGTH, GAP_LENGTH, BORDER_COLOR); + + // 绘制右边虚线 + drawDashedLine(bb, maxX, minY, maxX, maxY, DASH_LENGTH, GAP_LENGTH, BORDER_COLOR); + + // 绘制下边虚线 + drawDashedLine(bb, maxX, maxY, minX, maxY, DASH_LENGTH, GAP_LENGTH, BORDER_COLOR); + + // 绘制左边虚线 + drawDashedLine(bb, minX, maxY, minX, minY, DASH_LENGTH, GAP_LENGTH, BORDER_COLOR); + } + + /** + * 绘制虚线线段 + */ + private void drawDashedLine(BufferBuilder bb, float startX, float startY, float endX, float endY, + float dashLength, float gapLength, Vector4f color) { + float dx = endX - startX; + float dy = endY - startY; + float lineLength = (float) Math.sqrt(dx * dx + dy * dy); + float dashCount = lineLength / (dashLength + gapLength); + + float dirX = dx / lineLength; + float dirY = dy / lineLength; + + int segmentCount = (int) Math.ceil(dashCount); + + for (int i = 0; i < segmentCount; i++) { + float segmentStart = i * (dashLength + gapLength); + float segmentEnd = segmentStart + dashLength; + + // 确保不超过线段总长度 + if (segmentStart > lineLength) break; + if (segmentEnd > lineLength) segmentEnd = lineLength; + + float segStartX = startX + dirX * segmentStart; + float segStartY = startY + dirY * segmentStart; + float segEndX = startX + dirX * segmentEnd; + float segEndY = startY + dirY * segmentEnd; + + // 绘制虚线段 + bb.begin(GL11.GL_LINES, 2); + bb.setColor(color); + bb.vertex(segStartX, segStartY, 0.0f, 0.0f); + bb.vertex(segEndX, segEndY, 0.0f, 0.0f); + bb.endImmediate(); + } + } + + /** + * 绘制多选状态下的调整手柄 + */ + private void drawMultiSelectionResizeHandles(BufferBuilder bb, float minX, float minY, float maxX, float maxY, + float cornerSize, float borderThickness) { + Vector4f handleColor = new Vector4f(1.0f, 1.0f, 0.0f, 1.0f); // 黄色手柄 + + // 绘制四个角点 + drawCornerHandle(bb, minX, minY, handleColor, cornerSize); // 左上 + drawCornerHandle(bb, maxX, minY, handleColor, cornerSize); // 右上 + drawCornerHandle(bb, minX, maxY, handleColor, cornerSize); // 左下 + drawCornerHandle(bb, maxX, maxY, handleColor, cornerSize); // 右下 + + // 绘制边线中点 + drawEdgeHandle(bb, (minX + maxX) / 2, minY, handleColor, borderThickness); // 上边中点 + drawEdgeHandle(bb, (minX + maxX) / 2, maxY, handleColor, borderThickness); // 下边中点 + drawEdgeHandle(bb, minX, (minY + maxY) / 2, handleColor, borderThickness); // 左边中点 + drawEdgeHandle(bb, maxX, (minY + maxY) / 2, handleColor, borderThickness); // 右边中点 + } + + /** + * 绘制多选中心点 + */ + private void drawMultiSelectionCenterPoint(BufferBuilder bb, float minX, float minY, float maxX, float maxY) { + BoundingBox multiBounds = getMultiSelectionBounds(); + Vector2f center = multiBounds.getCenter(); + + float centerX = center.x; + float centerY = center.y; + float pointSize = 6.0f; + Vector4f centerColor = new Vector4f(1.0f, 0.0f, 0.0f, 1.0f); // 红色中心点 + + // 绘制中心点(十字形) + bb.begin(GL11.GL_LINES, 4); + bb.setColor(centerColor); + + // 水平线 + bb.vertex(centerX - pointSize, centerY, 0.0f, 0.0f); + bb.vertex(centerX + pointSize, centerY, 0.0f, 0.0f); + + // 垂直线 + bb.vertex(centerX, centerY - pointSize, 0.0f, 0.0f); + bb.vertex(centerX, centerY + pointSize, 0.0f, 0.0f); + + bb.endImmediate(); + + // 绘制中心点圆圈 + bb.begin(RenderSystem.GL_LINE_LOOP, 12); + bb.setColor(centerColor); + + float radius = pointSize * 0.8f; + for (int i = 0; i < 12; i++) { + float angle = (float) (i * 2 * Math.PI / 12); + float x = centerX + (float) Math.cos(angle) * radius; + float y = centerY + (float) Math.sin(angle) * radius; + bb.vertex(x, y, 0.0f, 0.0f); + } + bb.endImmediate(); + } + + /** + * 绘制多选旋转手柄 + */ + private void drawMultiSelectionRotationHandle(BufferBuilder bb, float minX, float minY, float maxX, float maxY) { + BoundingBox multiBounds = getMultiSelectionBounds(); + Vector2f center = multiBounds.getCenter(); + + float centerX = center.x; + float centerY = center.y; + float rotationHandleY = minY - ROTATION_HANDLE_DISTANCE; + + Vector4f rotationColor = new Vector4f(0.0f, 1.0f, 0.0f, 1.0f); // 绿色旋转手柄 + + // 绘制连接线(从中心点到旋转手柄) + bb.begin(GL11.GL_LINES, 2); + bb.setColor(rotationColor); + bb.vertex(centerX, minY, 0.0f, 0.0f); + bb.vertex(centerX, rotationHandleY, 0.0f, 0.0f); + bb.endImmediate(); + + // 绘制旋转手柄(圆圈) + float handleRadius = 6.0f; + bb.begin(RenderSystem.GL_LINE_LOOP, 12); + bb.setColor(rotationColor); + + for (int i = 0; i < 12; i++) { + float angle = (float) (i * 2 * Math.PI / 12); + float x = centerX + (float) Math.cos(angle) * handleRadius; + float y = rotationHandleY + (float) Math.sin(angle) * handleRadius; + bb.vertex(x, y, 0.0f, 0.0f); + } + bb.endImmediate(); + + // 绘制旋转箭头 + bb.begin(GL11.GL_LINES, 4); + bb.setColor(rotationColor); + + float arrowSize = 4.0f; + bb.vertex(centerX - arrowSize, rotationHandleY - arrowSize, 0.0f, 0.0f); + bb.vertex(centerX + arrowSize, rotationHandleY + arrowSize, 0.0f, 0.0f); + + bb.vertex(centerX + arrowSize, rotationHandleY - arrowSize, 0.0f, 0.0f); + bb.vertex(centerX - arrowSize, rotationHandleY + arrowSize, 0.0f, 0.0f); + bb.endImmediate(); + } private void drawCenterPoint(BufferBuilder bb, float minX, float minY, float maxX, float maxY) { // 使用 Mesh2D 的 pivot 作为中心点位置,但当 pivot 不在 bounds 内时回退为 bounds 中心(避免渲染时跳回 0,0 的情况) float centerX = pivot.x; @@ -1087,11 +1507,18 @@ public class Mesh2D { .append(", vertices=").append(getVertexCount()) .append(", indices=").append(indices.length) .append(", pivot=(").append(String.format("%.2f", pivot.x)) - .append(", ").append(String.format("%.2f", pivot.y)).append(")") // 新增这行 + .append(", ").append(String.format("%.2f", pivot.y)).append(")") .append(", visible=").append(visible) + .append(", selected=").append(selected) + .append(", inMultiSelection=").append(isInMultiSelection()) + .append(", multiSelectionCount=").append(multiSelectedParts.size()) .append(", drawMode=").append(getDrawModeString()) .append(", bounds=").append(getBounds()); + if (isInMultiSelection()) { + sb.append(", multiSelectionBounds=").append(getMultiSelectionBounds()); + } + if (vertices != null && vertices.length > 0) { sb.append(", coordinates=["); for (int i = 0; i < vertices.length; i += 2) { diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Texture.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Texture.java index 31593b4..3d9263b 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Texture.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Texture.java @@ -11,6 +11,8 @@ import org.lwjgl.stb.STBImage; import org.lwjgl.stb.STBImageWrite; import org.lwjgl.system.MemoryUtil; +import java.awt.*; +import java.awt.image.BufferedImage; import java.io.File; import java.nio.ByteBuffer; import java.nio.IntBuffer; @@ -224,17 +226,27 @@ public class Texture { createTextureObject(); } - // 上传纹理数据 - GL11.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, width, height, - format.getGLFormat(), type.getGLType(), pixelData); + // 关键:确保以 1 字节对齐上传,防止行对齐导致的数据错位(很多白图/花屏来自此) + int prevUnpack = GL11.glGetInteger(GL11.GL_UNPACK_ALIGNMENT); + GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1); - // 检查OpenGL错误 - checkGLError("glTexSubImage2D"); + try { + // 上传纹理数据 + GL11.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, width, height, + format.getGLFormat(), type.getGLType(), pixelData); - // 缓存像素数据 - cachePixelDataFromBuffer(pixelData); + // 检查OpenGL错误 + checkGLError("glTexSubImage2D"); - unbind(); + // 缓存像素数据 + cachePixelDataFromBuffer(pixelData); + } finally { + // 恢复原先的 UNPACK_ALIGNMENT + try { + GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, prevUnpack); + } catch (Exception ignored) {} + unbind(); + } } /** @@ -983,6 +995,83 @@ public class Texture { // ==================== 新的静态工厂方法 ==================== + public static Texture createFromBufferedImage(String name, BufferedImage img) { + return createFromBufferedImage(name, img, TextureFilter.LINEAR, TextureFilter.LINEAR); + } + + public static Texture createFromBufferedImage(String name, BufferedImage img, TextureFilter minFilter, TextureFilter magFilter) { + if (img == null) throw new IllegalArgumentException("BufferedImage cannot be null"); + + final int width = img.getWidth(); + final int height = img.getHeight(); + final int len = width * height; + + // 获取或转换为 TYPE_INT_ARGB 的 int[] 像素数据以提高性能 + final int[] pixels; + BufferedImage working = img; + if (img.getType() != BufferedImage.TYPE_INT_ARGB) { + BufferedImage conv = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = conv.createGraphics(); + try { + g.setRenderingHint(java.awt.RenderingHints.KEY_INTERPOLATION, java.awt.RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); + g.drawImage(img, 0, 0, null); + } finally { + g.dispose(); + } + working = conv; + } + pixels = ((java.awt.image.DataBufferInt) working.getRaster().getDataBuffer()).getData(); + + // 检测是否为预乘(alpha premultiplied) + boolean isPremultiplied = working.isAlphaPremultiplied(); + + // 分配本地 ByteBuffer 并填充为 非预乘 RGBA 顺序(R,G,B,A) + ByteBuffer buffer = MemoryUtil.memAlloc(len * 4); + try { + for (int i = 0; i < len; i++) { + int p = pixels[i]; + int a = (p >> 24) & 0xFF; + int r = (p >> 16) & 0xFF; + int g = (p >> 8) & 0xFF; + int b = (p) & 0xFF; + + // 如果是预乘 alpha,则反预乘(避免颜色被 alpha 缩小导致看起来发白或透明) + if (isPremultiplied && a != 0) { + // 反预乘:原色 = premultipliedColor * 255 / alpha + float invA = 255.0f / (float) a; + r = Math.min(255, Math.round(r * invA)); + g = Math.min(255, Math.round(g * invA)); + b = Math.min(255, Math.round(b * invA)); + } + + buffer.put((byte) (r & 0xFF)); + buffer.put((byte) (g & 0xFF)); + buffer.put((byte) (b & 0xFF)); + buffer.put((byte) (a & 0xFF)); + } + buffer.flip(); + + // 创建纹理并上传(uploadData 内已处理 GL_UNPACK_ALIGNMENT) + Texture texture = new Texture(name, width, height, TextureFormat.RGBA, buffer); + texture.setMinFilter(minFilter); + texture.setMagFilter(magFilter); + + // 若为 POT 尺寸且需要,则生成 mipmaps + if (texture.isPowerOfTwo(width) && texture.isPowerOfTwo(height)) { + texture.generateMipmaps(); + } + + // 缓存像素数据(可选,确保后续 crop/copy 等可用) + texture.ensurePixelDataCached(); + + return texture; + } finally { + MemoryUtil.memFree(buffer); + } + } + + + /** * 从字节数组创建纹理 */ diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelLayerPanelTest.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelLayerPanelTest.java index 8a0b80c..f3f55fc 100644 --- a/src/main/java/com/chuangzhou/vivid2D/test/ModelLayerPanelTest.java +++ b/src/main/java/com/chuangzhou/vivid2D/test/ModelLayerPanelTest.java @@ -10,6 +10,9 @@ import com.chuangzhou.vivid2D.render.model.util.Mesh2D; import javax.swing.*; import java.awt.*; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.List; /** * 简单的测试示例:创建一个 Model2D,添加几层(部件), @@ -19,6 +22,8 @@ import java.awt.*; public class ModelLayerPanelTest { public static void main(String[] args) { SwingUtilities.invokeLater(() -> { + System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8)); + System.setErr(new PrintStream(System.err, true, StandardCharsets.UTF_8)); // 创建示例模型并添加图层 Model2D model = new Model2D("示例模型"); @@ -92,13 +97,8 @@ public class ModelLayerPanelTest { JButton updateSelectionBtn = new JButton("更新选中部件"); updateSelectionBtn.addActionListener(e -> { renderPanel.executeInGLContext(() -> { - ModelPart selectedPart = renderPanel.getSelectedPart(); - transformPanel.setSelectedPart(selectedPart); - if (selectedPart != null) { - System.out.println("已选中部件: " + selectedPart.getName()); - } else { - System.out.println("未选中任何部件"); - } + List selectedPart = renderPanel.getSelectedParts(); + transformPanel.setSelectedParts(selectedPart); }); }); bottom.add(updateSelectionBtn); @@ -113,8 +113,8 @@ public class ModelLayerPanelTest { System.out.println("点击了模型:" + mesh.getName() + ",模型坐标:" + modelX + ", " + modelY + ",屏幕坐标:" + screenX + ", " + screenY); // 自动更新变换面板的选中部件 - ModelPart selectedPart = renderPanel.getSelectedPart(); - transformPanel.setSelectedPart(selectedPart); + List selectedPart = renderPanel.getSelectedParts(); + transformPanel.setSelectedParts(selectedPart); // 切换到变换控制选项卡 rightTabbedPane.setSelectedIndex(1);