feat(render): 实现PSD文件导入和多选支持功能
- 添加PSD文件解析和图层导入功能- 实现多选状态下网格选择和边界框绘制 - 增加虚线边框和多选操作手柄显示 - 支持多选状态下点精确检测算法 - 添加拖拽操作历史记录功能 - 实现模型部件唯一命名避免冲突- 增加纹理垂直翻转和像素数据转换- 支持可见PSD图层性和不透明度设置 - 添加模型状态调试打印功能 -优化网格包含点检测逻辑和性能 重要更新 - 支持多选图层 - 支持导入psd文件 - 支持撤回和重做操作
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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<ModelPart> 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<ModelPart> 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<String, ModelPart> 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<String, ModelPart> 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<Mesh2D> 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<ModelPart> 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<ModelPart> 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<ModelPart> 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<ModelPart, Float> oldRotations = new HashMap<>();
|
||||
Map<ModelPart, Float> 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<ModelPart, Float> oldRotations = new HashMap<>();
|
||||
Map<ModelPart, Float> 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<ModelPart, Vector2f> oldScales = new HashMap<>();
|
||||
Map<ModelPart, Vector2f> 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<ModelPart, Vector2f> oldScales = new HashMap<>();
|
||||
Map<ModelPart, Vector2f> 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<ModelPart, Vector2f> oldScales = new HashMap<>();
|
||||
Map<ModelPart, Vector2f> 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<ModelPart, Object> oldValues, Map<ModelPart, Object> newValues) {
|
||||
if (operationHistory != null && !selectedParts.isEmpty()) {
|
||||
List<Object> 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<ModelPart, Object> oldStates = new HashMap<>();
|
||||
Map<ModelPart, Object> 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<ModelPart> 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<ModelPart> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<OperationRecord> undoStack;
|
||||
private final LinkedList<OperationRecord> redoStack;
|
||||
|
||||
// 最大记录数量
|
||||
private final int maxHistorySize;
|
||||
|
||||
// 操作记录器映射
|
||||
private final Map<String, OperationRecorder> 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; }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PSDLayerInfo> 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<Object> layerInfos = (List<Object>) 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<ImageReader> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,7 @@ public class ModelPart {
|
||||
private boolean pivotInitialized;
|
||||
|
||||
private final List<ModelEvent> 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<Mesh2D> selectedMeshes = getSelectedMeshes();
|
||||
for (Mesh2D mesh : selectedMeshes) {
|
||||
mesh.markDirty();
|
||||
if (mesh.isInMultiSelection()) {
|
||||
mesh.forceUpdateMultiSelectionBounds();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新所有选中网格的多选列表
|
||||
*/
|
||||
private void updateMultiSelectionInMeshes() {
|
||||
List<Mesh2D> 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<Mesh2D> getSelectedMeshes() {
|
||||
List<Mesh2D> selected = new ArrayList<>();
|
||||
for (Mesh2D mesh : meshes) {
|
||||
if (mesh.isSelected()) {
|
||||
selected.add(mesh);
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取多选状态下的组合边界框
|
||||
*/
|
||||
public BoundingBox getMultiSelectionBounds() {
|
||||
BoundingBox bounds = new BoundingBox();
|
||||
List<Mesh2D> 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<Mesh2D> 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<Mesh2D> 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<Mesh2D> 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<Mesh2D> 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<Mesh2D> 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() +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -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<Mesh2D> 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<Mesh2D> 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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 从字节数组创建纹理
|
||||
*/
|
||||
|
||||
@@ -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<ModelPart> 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<ModelPart> selectedPart = renderPanel.getSelectedParts();
|
||||
transformPanel.setSelectedParts(selectedPart);
|
||||
|
||||
// 切换到变换控制选项卡
|
||||
rightTabbedPane.setSelectedIndex(1);
|
||||
|
||||
Reference in New Issue
Block a user