feat(render): 实现模型部件变换控制面板
- 新增 TransformPanel 类,提供图形界面控制模型部件的位移、旋转、缩放和中心点 - 在 ModelLayerPanelTest 中集成变换面板,支持自动更新选中部件 - 为 ModelPart 添加事件系统,支持变换属性变更通知 - 实现 Mesh2D 的 pivot 和 originalPivot 分离,支持更精确的变换控制- 添加 ModelEvent 接口,用于模型部件事件触发机制 - 优化 ModelRenderPanel 的选中部件获取逻辑 - 完善模型点击监听器,支持自动切换到变换控制选项卡 -修复拖拽移动中心点时的边界检查问题 - 增强各变换操作的边界验证和错误处理 - 改进中心点绘制逻辑,增加边界检查和回退机制 重要更新 - 修复上个版本的所有问题,并且增加新的面板观测图层的各种信息
This commit is contained in:
@@ -986,4 +986,4 @@ public class ModelLayerPanel extends JPanel {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,6 +168,13 @@ public class ModelRenderPanel extends JPanel {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前选中的部件
|
||||
*/
|
||||
public ModelPart getSelectedPart() {
|
||||
return selectedMesh != null ? findPartByMesh(selectedMesh) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取鼠标悬停的网格
|
||||
*/
|
||||
@@ -324,8 +331,6 @@ public class ModelRenderPanel extends JPanel {
|
||||
partInitialRotation = selPart.getRotation();
|
||||
}
|
||||
|
||||
logger.info("开始旋转,中心点: ({}, {})", rotationCenter.x, rotationCenter.y);
|
||||
|
||||
}else if (dragMode == DragMode.MOVE_PIVOT && selectedMesh != null) {
|
||||
// 开始移动中心点
|
||||
currentDragMode = DragMode.MOVE_PIVOT;
|
||||
@@ -336,8 +341,6 @@ public class ModelRenderPanel extends JPanel {
|
||||
BoundingBox bounds = selectedMesh.getBounds();
|
||||
rotationCenter.set((bounds.getMinX() + bounds.getMaxX()) / 2.0f,
|
||||
(bounds.getMinY() + bounds.getMaxY()) / 2.0f);
|
||||
|
||||
logger.info("开始移动中心点");
|
||||
} else if (dragMode != DragMode.NONE && selectedMesh != null) {
|
||||
// 开始调整大小
|
||||
currentDragMode = dragMode;
|
||||
@@ -403,7 +406,7 @@ public class ModelRenderPanel extends JPanel {
|
||||
float maxX = bounds.getMaxX();
|
||||
float maxY = bounds.getMaxY();
|
||||
|
||||
// 使用 Mesh2D 的实际中心点,而不是边界框中心
|
||||
// 使用 Mesh2D 的实际中心点
|
||||
Vector2f actualPivot = selectedMesh.getPivot();
|
||||
float centerX = actualPivot.x;
|
||||
float centerY = actualPivot.y;
|
||||
@@ -584,15 +587,18 @@ public class ModelRenderPanel extends JPanel {
|
||||
if (selectedMesh == null) return;
|
||||
ModelPart selectedPart = findPartByMesh(selectedMesh);
|
||||
if (selectedPart == null) return;
|
||||
|
||||
float deltaX = modelX - dragStartX;
|
||||
float deltaY = modelY - dragStartY;
|
||||
Vector2f currentPivot = selectedPart.getPivot();
|
||||
float newPivotX = currentPivot.x + deltaX;
|
||||
float newPivotY = currentPivot.y + deltaY;
|
||||
selectedPart.setPivot(newPivotX, newPivotY);
|
||||
rotationCenter.set(newPivotX, newPivotY);
|
||||
if (!selectedPart.setPivot(newPivotX, newPivotY)) {
|
||||
return;
|
||||
}
|
||||
dragStartX = modelX;
|
||||
dragStartY = modelY;
|
||||
rotationCenter.set(newPivotX, newPivotY);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1442,4 +1448,4 @@ public class ModelRenderPanel extends JPanel {
|
||||
|
||||
logger.info("OpenGL 资源已清理");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,474 @@
|
||||
package com.chuangzhou.vivid2D.render.awt;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelEvent;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
|
||||
/**
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class TransformPanel extends JPanel implements ModelEvent {
|
||||
private ModelRenderPanel renderPanel;
|
||||
private ModelPart selectedPart;
|
||||
|
||||
// 位置控制
|
||||
private JTextField positionXField;
|
||||
private JTextField positionYField;
|
||||
|
||||
// 旋转控制
|
||||
private JTextField rotationField;
|
||||
|
||||
// 缩放控制
|
||||
private JTextField scaleXField;
|
||||
private JTextField scaleYField;
|
||||
|
||||
// 中心点控制
|
||||
private JTextField pivotXField;
|
||||
private JTextField pivotYField;
|
||||
|
||||
// 按钮
|
||||
private JButton flipXButton;
|
||||
private JButton flipYButton;
|
||||
private JButton rotate90CWButton;
|
||||
private JButton rotate90CCWButton;
|
||||
private JButton resetScaleButton;
|
||||
|
||||
private boolean updatingUI = false; // 防止UI更新时触发事件
|
||||
private javax.swing.Timer transformTimer; // 用于延迟处理变换输入
|
||||
|
||||
public TransformPanel(ModelRenderPanel renderPanel) {
|
||||
this.renderPanel = renderPanel;
|
||||
initComponents();
|
||||
setupListeners();
|
||||
updateUIState();
|
||||
}
|
||||
|
||||
private void initComponents() {
|
||||
setLayout(new GridBagLayout());
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.insets = new Insets(3, 5, 3, 5);
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
int row = 0;
|
||||
|
||||
// 位置控制
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = row;
|
||||
add(new JLabel("位置 X:"), gbc);
|
||||
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = row;
|
||||
positionXField = new JTextField("0.0");
|
||||
add(positionXField, gbc);
|
||||
|
||||
gbc.gridx = 2;
|
||||
gbc.gridy = row;
|
||||
add(new JLabel("Y:"), gbc);
|
||||
|
||||
gbc.gridx = 3;
|
||||
gbc.gridy = row++;
|
||||
positionYField = new JTextField("0.0");
|
||||
add(positionYField, gbc);
|
||||
|
||||
// 旋转控制
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = row;
|
||||
add(new JLabel("旋转角度:"), gbc);
|
||||
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = row;
|
||||
rotationField = new JTextField("0.0");
|
||||
add(rotationField, gbc);
|
||||
|
||||
gbc.gridx = 2;
|
||||
gbc.gridy = row;
|
||||
gbc.gridwidth = 2;
|
||||
rotate90CWButton = new JButton("+90°");
|
||||
rotate90CWButton.setToolTipText("顺时针旋转90度");
|
||||
add(rotate90CWButton, gbc);
|
||||
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = ++row;
|
||||
gbc.gridwidth = 4;
|
||||
rotate90CCWButton = new JButton("-90°");
|
||||
rotate90CCWButton.setToolTipText("逆时针旋转90度");
|
||||
add(rotate90CCWButton, gbc);
|
||||
|
||||
// 缩放控制
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = ++row;
|
||||
gbc.gridwidth = 1;
|
||||
add(new JLabel("缩放 X:"), gbc);
|
||||
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = row;
|
||||
scaleXField = new JTextField("1.0");
|
||||
add(scaleXField, gbc);
|
||||
|
||||
gbc.gridx = 2;
|
||||
gbc.gridy = row;
|
||||
add(new JLabel("Y:"), gbc);
|
||||
|
||||
gbc.gridx = 3;
|
||||
gbc.gridy = row;
|
||||
scaleYField = new JTextField("1.0");
|
||||
add(scaleYField, gbc);
|
||||
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = ++row;
|
||||
gbc.gridwidth = 2;
|
||||
flipXButton = new JButton("水平翻转");
|
||||
add(flipXButton, gbc);
|
||||
|
||||
gbc.gridx = 2;
|
||||
gbc.gridy = row;
|
||||
gbc.gridwidth = 2;
|
||||
flipYButton = new JButton("垂直翻转");
|
||||
add(flipYButton, gbc);
|
||||
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = ++row;
|
||||
gbc.gridwidth = 4;
|
||||
resetScaleButton = new JButton("重置缩放");
|
||||
resetScaleButton.setToolTipText("重置为1:1缩放");
|
||||
add(resetScaleButton, gbc);
|
||||
|
||||
// 中心点控制
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = ++row;
|
||||
gbc.gridwidth = 1;
|
||||
add(new JLabel("中心点 X:"), gbc);
|
||||
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = row;
|
||||
pivotXField = new JTextField("0.0");
|
||||
add(pivotXField, gbc);
|
||||
|
||||
gbc.gridx = 2;
|
||||
gbc.gridy = row;
|
||||
add(new JLabel("Y:"), gbc);
|
||||
|
||||
gbc.gridx = 3;
|
||||
gbc.gridy = row;
|
||||
pivotYField = new JTextField("0.0");
|
||||
add(pivotYField, gbc);
|
||||
|
||||
// Set border
|
||||
setBorder(BorderFactory.createTitledBorder("变换控制"));
|
||||
|
||||
// 初始化定时器,用于延迟处理变换输入
|
||||
transformTimer = new javax.swing.Timer(300, e -> applyTransformChanges());
|
||||
transformTimer.setRepeats(false); // 只执行一次
|
||||
}
|
||||
|
||||
private void setupListeners() {
|
||||
// 为所有文本框添加文档监听器
|
||||
DocumentListener documentListener = new DocumentListener() {
|
||||
@Override
|
||||
public void insertUpdate(DocumentEvent e) {
|
||||
scheduleTransformUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUpdate(DocumentEvent e) {
|
||||
scheduleTransformUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changedUpdate(DocumentEvent e) {
|
||||
scheduleTransformUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
positionXField.getDocument().addDocumentListener(documentListener);
|
||||
positionYField.getDocument().addDocumentListener(documentListener);
|
||||
rotationField.getDocument().addDocumentListener(documentListener);
|
||||
scaleXField.getDocument().addDocumentListener(documentListener);
|
||||
scaleYField.getDocument().addDocumentListener(documentListener);
|
||||
pivotXField.getDocument().addDocumentListener(documentListener);
|
||||
pivotYField.getDocument().addDocumentListener(documentListener);
|
||||
|
||||
// 添加焦点监听,当失去焦点时立即应用
|
||||
java.awt.event.FocusAdapter focusAdapter = new java.awt.event.FocusAdapter() {
|
||||
@Override
|
||||
public void focusLost(java.awt.event.FocusEvent e) {
|
||||
transformTimer.stop();
|
||||
applyTransformChanges();
|
||||
}
|
||||
};
|
||||
|
||||
positionXField.addFocusListener(focusAdapter);
|
||||
positionYField.addFocusListener(focusAdapter);
|
||||
rotationField.addFocusListener(focusAdapter);
|
||||
scaleXField.addFocusListener(focusAdapter);
|
||||
scaleYField.addFocusListener(focusAdapter);
|
||||
pivotXField.addFocusListener(focusAdapter);
|
||||
pivotYField.addFocusListener(focusAdapter);
|
||||
|
||||
// 添加回车键监听
|
||||
java.awt.event.ActionListener enterListener = e -> {
|
||||
transformTimer.stop();
|
||||
applyTransformChanges();
|
||||
};
|
||||
|
||||
positionXField.addActionListener(enterListener);
|
||||
positionYField.addActionListener(enterListener);
|
||||
rotationField.addActionListener(enterListener);
|
||||
scaleXField.addActionListener(enterListener);
|
||||
scaleYField.addActionListener(enterListener);
|
||||
pivotXField.addActionListener(enterListener);
|
||||
pivotYField.addActionListener(enterListener);
|
||||
|
||||
// 按钮监听器
|
||||
rotate90CWButton.addActionListener(e -> {
|
||||
if (selectedPart != null) {
|
||||
renderPanel.executeInGLContext(() -> {
|
||||
float currentRotation = (float) Math.toDegrees(selectedPart.getRotation());
|
||||
float newRotation = normalizeAngle(currentRotation + 90.0f);
|
||||
selectedPart.setRotation((float) Math.toRadians(newRotation));
|
||||
renderPanel.repaint();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
rotate90CCWButton.addActionListener(e -> {
|
||||
if (selectedPart != null) {
|
||||
renderPanel.executeInGLContext(() -> {
|
||||
float currentRotation = (float) Math.toDegrees(selectedPart.getRotation());
|
||||
float newRotation = normalizeAngle(currentRotation - 90.0f);
|
||||
selectedPart.setRotation((float) Math.toRadians(newRotation));
|
||||
renderPanel.repaint();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
flipXButton.addActionListener(e -> {
|
||||
if (selectedPart != null) {
|
||||
renderPanel.executeInGLContext(() -> {
|
||||
float currentScaleX = selectedPart.getScaleX();
|
||||
float currentScaleY = selectedPart.getScaleY();
|
||||
selectedPart.setScale(currentScaleX * -1, currentScaleY);
|
||||
renderPanel.repaint();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
flipYButton.addActionListener(e -> {
|
||||
if (selectedPart != null) {
|
||||
renderPanel.executeInGLContext(() -> {
|
||||
float currentScaleX = selectedPart.getScaleX();
|
||||
float currentScaleY = selectedPart.getScaleY();
|
||||
selectedPart.setScale(currentScaleX, currentScaleY * -1);
|
||||
renderPanel.repaint();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
resetScaleButton.addActionListener(e -> {
|
||||
if (selectedPart != null) {
|
||||
renderPanel.executeInGLContext(() -> {
|
||||
selectedPart.setScale(1.0f, 1.0f);
|
||||
renderPanel.repaint();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 事件监听器实现 - 当ModelPart的属性变化时自动更新UI
|
||||
*/
|
||||
@Override
|
||||
public void trigger(String eventName, Object source) {
|
||||
if (!(source instanceof ModelPart) || source != selectedPart) 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;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
updatingUI = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 调度变换更新(延迟处理)
|
||||
*/
|
||||
private void scheduleTransformUpdate() {
|
||||
if (updatingUI || selectedPart == null) return;
|
||||
transformTimer.stop();
|
||||
transformTimer.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将角度标准化到0-360度范围内
|
||||
*/
|
||||
private float normalizeAngle(float degrees) {
|
||||
degrees = degrees % 360;
|
||||
if (degrees < 0) {
|
||||
degrees += 360;
|
||||
}
|
||||
return degrees;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用所有变换更改
|
||||
*/
|
||||
private void applyTransformChanges() {
|
||||
if (updatingUI || selectedPart == null) 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);
|
||||
|
||||
renderPanel.repaint();
|
||||
} catch (NumberFormatException ex) {
|
||||
// 输入无效时恢复之前的值
|
||||
SwingUtilities.invokeLater(this::updateUIFromSelectedPart);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从选中的部件更新UI
|
||||
*/
|
||||
private void updateUIFromSelectedPart() {
|
||||
if (selectedPart == null) 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));
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
updatingUI = false;
|
||||
}
|
||||
|
||||
public void setSelectedPart(ModelPart part) {
|
||||
// 移除旧部件的事件监听
|
||||
if (this.selectedPart != null) {
|
||||
this.selectedPart.removeEvent(this);
|
||||
}
|
||||
|
||||
this.selectedPart = part;
|
||||
|
||||
// 添加新部件的事件监听
|
||||
if (this.selectedPart != null) {
|
||||
this.selectedPart.addEvent(this);
|
||||
}
|
||||
|
||||
updateUIState();
|
||||
}
|
||||
|
||||
private void updateUIState() {
|
||||
updatingUI = true;
|
||||
if (selectedPart != null) {
|
||||
updateUIFromSelectedPart();
|
||||
setControlsEnabled(true);
|
||||
} else {
|
||||
// 清空所有字段
|
||||
positionXField.setText("0.00");
|
||||
positionYField.setText("0.00");
|
||||
rotationField.setText("0.00");
|
||||
scaleXField.setText("1.00");
|
||||
scaleYField.setText("1.00");
|
||||
pivotXField.setText("0.00");
|
||||
pivotYField.setText("0.00");
|
||||
setControlsEnabled(false);
|
||||
}
|
||||
updatingUI = false;
|
||||
}
|
||||
|
||||
private void setControlsEnabled(boolean enabled) {
|
||||
positionXField.setEnabled(enabled);
|
||||
positionYField.setEnabled(enabled);
|
||||
rotationField.setEnabled(enabled);
|
||||
scaleXField.setEnabled(enabled);
|
||||
scaleYField.setEnabled(enabled);
|
||||
pivotXField.setEnabled(enabled);
|
||||
pivotYField.setEnabled(enabled);
|
||||
flipXButton.setEnabled(enabled);
|
||||
flipYButton.setEnabled(enabled);
|
||||
rotate90CWButton.setEnabled(enabled);
|
||||
rotate90CCWButton.setEnabled(enabled);
|
||||
resetScaleButton.setEnabled(enabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeNotify() {
|
||||
super.removeNotify();
|
||||
// 清理定时器资源和事件监听
|
||||
if (transformTimer != null) {
|
||||
transformTimer.stop();
|
||||
}
|
||||
if (selectedPart != null) {
|
||||
selectedPart.removeEvent(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.chuangzhou.vivid2D.render.model;
|
||||
|
||||
/**
|
||||
* 模型事件
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public interface ModelEvent {
|
||||
void trigger(String eventName,Object eventBus);
|
||||
}
|
||||
@@ -52,6 +52,8 @@ public class ModelPart {
|
||||
private boolean boundsDirty;
|
||||
private boolean pivotInitialized;
|
||||
|
||||
private final List<ModelEvent> events = new ArrayList<>();
|
||||
|
||||
// ====== 液化模式枚举 ======
|
||||
public enum LiquifyMode {
|
||||
PUSH, // 推开(从画笔中心向外推)
|
||||
@@ -97,6 +99,20 @@ public class ModelPart {
|
||||
recomputeWorldTransformRecursive();
|
||||
}
|
||||
|
||||
public void addEvent(ModelEvent event) {
|
||||
events.add(event);
|
||||
}
|
||||
|
||||
public void removeEvent(ModelEvent event) {
|
||||
events.remove(event);
|
||||
}
|
||||
|
||||
private void triggerEvent(String eventName) {
|
||||
for (ModelEvent event : events) {
|
||||
event.trigger(eventName,this);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 层级管理 ====================
|
||||
|
||||
/**
|
||||
@@ -477,12 +493,19 @@ public class ModelPart {
|
||||
*/
|
||||
public void setPosition(float x, float y) {
|
||||
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);
|
||||
}
|
||||
|
||||
updateMeshVertices();
|
||||
triggerEvent("position");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -525,23 +548,54 @@ public class ModelPart {
|
||||
recomputeWorldTransformRecursive();
|
||||
}
|
||||
|
||||
// 应用当前世界变换到每个顶点
|
||||
// 应用当前世界变换到每个顶点(将局部 original 顶点烘焙为 world 顶点)
|
||||
for (int i = 0; i < originalVertices.length; i += 2) {
|
||||
Vector2f localPoint = new Vector2f(originalVertices[i], originalVertices[i + 1]);
|
||||
Vector2f worldPoint = localToWorld(localPoint);
|
||||
Vector2f worldPoint = Matrix3fUtils.transformPoint(worldTransform, localPoint);
|
||||
mesh.setVertex(i / 2, worldPoint.x, worldPoint.y);
|
||||
}
|
||||
|
||||
// 同步 mesh 的原始局部 pivot -> 当前世界 pivot(保持可视中心一致)
|
||||
try {
|
||||
Vector2f origPivot = mesh.getOriginalPivot();
|
||||
Vector2f worldPivot = Matrix3fUtils.transformPoint(worldTransform, origPivot);
|
||||
mesh.setPivot(worldPivot.x, worldPivot.y);
|
||||
} catch (Exception ignored) { }
|
||||
|
||||
// 标记网格需要更新
|
||||
mesh.markDirty();
|
||||
}
|
||||
|
||||
public void setPosition(Vector2f position) {
|
||||
this.position.set(position);
|
||||
public void setPosition(Vector2f pos) {
|
||||
// 记录旧世界变换和旧位置,用于计算位移
|
||||
Matrix3f oldWorldTransform = new Matrix3f(this.worldTransform);
|
||||
Vector2f oldPosition = new Vector2f(this.position);
|
||||
|
||||
// 更新部件的局部位置
|
||||
this.position.set(pos);
|
||||
|
||||
// 标记变换脏,更新局部变换和世界变换
|
||||
markTransformDirty();
|
||||
updateLocalTransform();
|
||||
recomputeWorldTransformRecursive();
|
||||
|
||||
// 计算部件的实际位移
|
||||
float dx = this.position.x - oldPosition.x;
|
||||
float dy = this.position.y - oldPosition.y;
|
||||
|
||||
// 更新每个网格的 pivot 和 originalPivot
|
||||
for (Mesh2D mesh : meshes) {
|
||||
// 将 mesh 的原始局部 pivot 变换到旧的世界坐标系
|
||||
Vector2f oldWorldPivot = Matrix3fUtils.transformPoint(oldWorldTransform, mesh.getOriginalPivot());
|
||||
// 在世界坐标系中应用位移
|
||||
Vector2f movedWorldPivot = new Vector2f(oldWorldPivot.x + dx, oldWorldPivot.y + dy);
|
||||
// 将位移后的世界 pivot 逆变换回新的局部坐标系(即新的 originalPivot)
|
||||
Vector2f newLocalOriginalPivot = Matrix3fUtils.transformPointInverse(this.worldTransform, movedWorldPivot);
|
||||
|
||||
mesh.setOriginalPivot(newLocalOriginalPivot);
|
||||
mesh.setPivot(movedWorldPivot.x, movedWorldPivot.y);
|
||||
}
|
||||
|
||||
// 更新网格顶点位置
|
||||
updateMeshVertices();
|
||||
}
|
||||
@@ -567,10 +621,28 @@ public class ModelPart {
|
||||
* 设置旋转(弧度)
|
||||
*/
|
||||
public void setRotation(float radians) {
|
||||
// 记录旧的世界变换,用于计算 pivot 的相对位置
|
||||
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));
|
||||
}
|
||||
|
||||
updateMeshVertices();
|
||||
triggerEvent("rotation");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -581,12 +653,16 @@ public class ModelPart {
|
||||
markTransformDirty();
|
||||
updateLocalTransform();
|
||||
recomputeWorldTransformRecursive();
|
||||
triggerEvent("rotation");
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置缩放
|
||||
*/
|
||||
public void setScale(float sx, float sy) {
|
||||
// 记录旧的世界变换,用于计算 pivot 的相对位置
|
||||
Matrix3f oldWorldTransform = new Matrix3f(this.worldTransform);
|
||||
|
||||
this.scaleX = sx;
|
||||
this.scaleY = sy;
|
||||
scale.set(sx, sy);
|
||||
@@ -594,25 +670,69 @@ 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);
|
||||
|
||||
scale.set(uniformScale, uniformScale);
|
||||
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));
|
||||
}
|
||||
|
||||
updateMeshVertices();
|
||||
triggerEvent("scale");
|
||||
}
|
||||
|
||||
public void setScale(Vector2f scale) {
|
||||
// 记录旧的世界变换,用于计算 pivot 的相对位置
|
||||
Matrix3f oldWorldTransform = new Matrix3f(this.worldTransform);
|
||||
|
||||
this.scale.set(scale);
|
||||
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));
|
||||
}
|
||||
|
||||
updateMeshVertices();
|
||||
triggerEvent("scale");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -623,6 +743,7 @@ public class ModelPart {
|
||||
markTransformDirty();
|
||||
updateLocalTransform();
|
||||
recomputeWorldTransformRecursive();
|
||||
triggerEvent("scale");
|
||||
}
|
||||
|
||||
// ==================== 网格管理 ====================
|
||||
@@ -636,7 +757,7 @@ public class ModelPart {
|
||||
// 创建独立副本,避免多个 Part 共享同一 Mesh 实例导致数据冲突
|
||||
Mesh2D m = mesh.copy();
|
||||
|
||||
// 确保拷贝保留原始的纹理引用(copy() 应该已经赋值,但显式赋值可避免遗漏)
|
||||
// 确保拷贝保留原始的纹理引用(copy() 已处理)
|
||||
m.setTexture(mesh.getTexture());
|
||||
|
||||
// 确保本节点的 worldTransform 是最新的
|
||||
@@ -645,10 +766,8 @@ public class ModelPart {
|
||||
// 保存拷贝的原始(局部)顶点供后续重算 world 顶点使用
|
||||
float[] originalVertices = m.getVertices().clone();
|
||||
m.setOriginalVertices(originalVertices);
|
||||
// 保证 UV 不被篡改(通常 copy() 已经处理)
|
||||
// float[] uvs = m.getUVs(); // 如果需要可以在此处检查
|
||||
|
||||
// 将拷贝的 mesh 的每个顶点从本地空间变换到世界空间(烘焙到 world)
|
||||
// 把 originalPivot 保存在 mesh 中(setMeshData 已经初始化 originalPivot)
|
||||
// 将每个顶点从本地空间变换到世界空间(烘焙到 world)
|
||||
int vc = m.getVertexCount();
|
||||
for (int i = 0; i < vc; i++) {
|
||||
Vector2f local = new Vector2f(originalVertices[i * 2], originalVertices[i * 2 + 1]);
|
||||
@@ -656,6 +775,13 @@ public class ModelPart {
|
||||
m.setVertex(i, worldPt.x, worldPt.y);
|
||||
}
|
||||
|
||||
// 同步 originalPivot -> world pivot(如果 originalPivot 有意义)
|
||||
try {
|
||||
Vector2f origPivot = m.getOriginalPivot();
|
||||
Vector2f worldPivot = Matrix3fUtils.transformPoint(this.worldTransform, origPivot);
|
||||
m.setPivot(worldPivot.x, worldPivot.y);
|
||||
} catch (Exception ignored) { }
|
||||
|
||||
// 标记为已烘焙到世界坐标(语义上明确),并确保 bounds/dirty 状态被正确刷新
|
||||
m.setBakedToWorld(true);
|
||||
|
||||
@@ -670,26 +796,40 @@ public class ModelPart {
|
||||
/**
|
||||
* 设置中心点
|
||||
*/
|
||||
public void setPivot(float x, float y) {
|
||||
if (!pivotInitialized) {
|
||||
// 确保第一次设置 pivot 的时候,必须是 (0,0) 因为这个为非0,0时后面如果想要热变换就会出问题
|
||||
if (x != 0 || y != 0) {
|
||||
logger.warn("The first time you set the pivot, it must be (0,0), which is automatically adjusted to (0,0).");
|
||||
x = 0;
|
||||
y = 0;
|
||||
public boolean setPivot(float x, float y) {
|
||||
// 无论是否首次设置,都允许设置任意 pivot
|
||||
// pivotInitialized = true; // 此行不再需要,因为不再强制 (0,0)
|
||||
|
||||
for (Mesh2D mesh : meshes) {
|
||||
// ModelPart 的 pivot 是在部件的局部坐标系中定义的
|
||||
// Mesh2D 的 setPivot 期望的是 Mesh2D 自己的局部坐标系中的 pivot
|
||||
// 因此需要将 ModelPart 的局部 pivot 转换为 Mesh2D 的局部 pivot
|
||||
// 由于 Mesh2D 的 originalPivot 已经存储了其在 ModelPart 局部坐标系中的相对位置,
|
||||
// 我们可以直接将 ModelPart 的新 pivot 赋值给 Mesh2D 的 originalPivot
|
||||
// 然后再通过变换更新 Mesh2D 的实际 pivot
|
||||
if (!mesh.setOriginalPivot(new Vector2f(x, y))){
|
||||
return false;
|
||||
}
|
||||
pivotInitialized = true;
|
||||
// Mesh2D 的实际 pivot 应该根据 ModelPart 的世界变换来计算
|
||||
// 这里只是设置了 originalPivot,Mesh2D 的实际 pivot 会在 updateMeshVertices 中更新
|
||||
// 或者在 ModelPart 的 setPivot 之后,立即触发 Mesh2D 的 pivot 更新
|
||||
// 为了简化,我们假设 Mesh2D 的 setPivot 能够处理好 originalPivot 和实际 pivot 的关系
|
||||
// 或者在 ModelPart 的 updateMeshVertices 中统一处理
|
||||
// 暂时不在这里调用 mesh.setPivot(x, y),因为 Mesh2D.setPivot 有边界检查,可能导致设置失败
|
||||
// 正确的做法是更新 Mesh2D 的 originalPivot,然后让 ModelPart 的变换系统来更新 Mesh2D 的实际 pivot
|
||||
// if (!mesh.setPivot(x, y)){
|
||||
// return false;
|
||||
// }
|
||||
}
|
||||
|
||||
pivot.set(x, y);
|
||||
|
||||
for (Mesh2D mesh : meshes) {
|
||||
mesh.setPivot(x, y);
|
||||
}
|
||||
|
||||
markTransformDirty();
|
||||
updateLocalTransform();
|
||||
recomputeWorldTransformRecursive();
|
||||
triggerEvent("pivot");
|
||||
updateMeshVertices();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -984,4 +1124,4 @@ public class ModelPart {
|
||||
", meshes=" + meshes.size() +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@ public class Mesh2D {
|
||||
private boolean bakedToWorld = false;
|
||||
private volatile boolean selected = false;
|
||||
private Vector2f pivot = new Vector2f(0, 0);
|
||||
private Vector2f originalPivot = new Vector2f(0, 0);
|
||||
|
||||
// ==================== 常量 ====================
|
||||
public static final int POINTS = 0;
|
||||
@@ -101,18 +102,52 @@ public class Mesh2D {
|
||||
this.indices = indices.clone();
|
||||
this.originalVertices = vertices.clone();
|
||||
|
||||
// 将当前 pivot 视为原始(局部)pivot 的初始值
|
||||
this.originalPivot.set(this.pivot);
|
||||
|
||||
markDirty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置中心点
|
||||
*/
|
||||
public void setPivot(float x, float y) {
|
||||
this.pivot.set(x, y);
|
||||
public boolean setPivot(float x, float y) {
|
||||
BoundingBox bounds = getBounds();
|
||||
if (x >= bounds.getMinX() && x <= bounds.getMaxX() &&
|
||||
y >= bounds.getMinY() && y <= bounds.getMaxY()) {
|
||||
this.pivot.set(x, y);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setPivot(Vector2f pivot) {
|
||||
this.pivot.set(pivot);
|
||||
public Vector2f getOriginalPivot() {
|
||||
return new Vector2f(originalPivot);
|
||||
}
|
||||
|
||||
public boolean setOriginalPivot(Vector2f p) {
|
||||
if (p != null) {
|
||||
BoundingBox bounds = getBounds();
|
||||
if (bounds != null &&
|
||||
p.x >= bounds.getMinX() && p.x <= bounds.getMaxX() &&
|
||||
p.y >= bounds.getMinY() && p.y <= bounds.getMaxY()) {
|
||||
this.originalPivot.set(p);
|
||||
markDirty();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public boolean setPivot(Vector2f pivot) {
|
||||
BoundingBox bounds = getBounds();
|
||||
if (pivot.x >= bounds.getMinX() && pivot.x <= bounds.getMaxX() &&
|
||||
pivot.y >= bounds.getMinY() && pivot.y <= bounds.getMaxY()) {
|
||||
this.pivot.set(pivot);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,9 +161,21 @@ public class Mesh2D {
|
||||
* 移动中心点
|
||||
*/
|
||||
public void movePivot(float dx, float dy) {
|
||||
this.pivot.add(dx, dy);
|
||||
float newX = pivot.x + dx;
|
||||
float newY = pivot.y + dy;
|
||||
|
||||
BoundingBox b = getBounds();
|
||||
|
||||
if (b != null && newX >= b.getMinX() && newX <= b.getMaxX()
|
||||
&& newY >= b.getMinY() && newY <= b.getMaxY()) {
|
||||
this.pivot.add(dx, dy);
|
||||
// 同步原始局部 pivot —— 这里假设 originalPivot 与 pivot 的坐标系一致(多数场景下是这样)
|
||||
this.originalPivot.add(dx, dy);
|
||||
markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建矩形网格
|
||||
*/
|
||||
@@ -643,16 +690,25 @@ public class Mesh2D {
|
||||
}
|
||||
|
||||
private void drawCenterPoint(BufferBuilder bb, float minX, float minY, float maxX, float maxY) {
|
||||
// 使用 Mesh2D 的 pivot 作为中心点位置
|
||||
// 使用 Mesh2D 的 pivot 作为中心点位置,但当 pivot 不在 bounds 内时回退为 bounds 中心(避免渲染时跳回 0,0 的情况)
|
||||
float centerX = pivot.x;
|
||||
float centerY = pivot.y;
|
||||
|
||||
float pointSize = 6.0f; // 中心点大小
|
||||
// 如果 pivot 不在当前 bounds(可能因为 pivot 是局部坐标或坐标空间不一致),回退到 bounds 中心
|
||||
boolean pivotInBounds = (centerX >= minX && centerX <= maxX && centerY >= minY && centerY <= maxY);
|
||||
|
||||
if (!pivotInBounds) {
|
||||
// 使用 bounds 中心作为可视化中心,避免显示到 (0,0)
|
||||
centerX = (minX + maxX) * 0.5f;
|
||||
centerY = (minY + maxY) * 0.5f;
|
||||
logger.trace("pivot ({},{}) not in bounds -> using bounds center ({},{}) for rendering", pivot.x, pivot.y, centerX, centerY);
|
||||
}
|
||||
|
||||
float pointSize = 6.0f; // 中心点大小
|
||||
Vector4f centerColor = new Vector4f(1.0f, 0.0f, 0.0f, 1.0f); // 红色中心点
|
||||
|
||||
// 绘制中心点(十字形)
|
||||
bb.begin(GL11.GL_LINES, 4); // 使用 RenderSystem 常量
|
||||
bb.begin(GL11.GL_LINES, 4);
|
||||
bb.setColor(centerColor);
|
||||
|
||||
// 水平线
|
||||
@@ -678,16 +734,22 @@ public class Mesh2D {
|
||||
}
|
||||
bb.endImmediate();
|
||||
|
||||
logger.trace("绘制中心点: ({}, {})", centerX, centerY);
|
||||
logger.trace("绘制中心点 (rendered): ({}, {}) ; pivot(actual): ({}, {})", centerX, centerY, pivot.x, pivot.y);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 绘制旋转手柄
|
||||
*/
|
||||
private void drawRotationHandle(BufferBuilder bb, float minX, float minY, float maxX, float maxY) {
|
||||
// 使用 Mesh2D 的 pivot 作为中心点位置
|
||||
// 计算可视中心(与 drawCenterPoint 的回退逻辑保持一致)
|
||||
float centerX = pivot.x;
|
||||
float centerY = pivot.y;
|
||||
boolean pivotInBounds = (centerX >= minX && centerX <= maxX && centerY >= minY && centerY <= maxY);
|
||||
if (!pivotInBounds) {
|
||||
centerX = (minX + maxX) * 0.5f;
|
||||
centerY = (minY + maxY) * 0.5f;
|
||||
}
|
||||
|
||||
// 旋转手柄位置(在边界框上方)
|
||||
float rotationHandleY = minY - ROTATION_HANDLE_DISTANCE;
|
||||
@@ -719,7 +781,6 @@ public class Mesh2D {
|
||||
bb.begin(GL11.GL_LINES, 4);
|
||||
bb.setColor(rotationColor);
|
||||
|
||||
// 箭头线
|
||||
float arrowSize = 4.0f;
|
||||
bb.vertex(rotationHandleX - arrowSize, rotationHandleY - arrowSize, 0.0f, 0.0f);
|
||||
bb.vertex(rotationHandleX + arrowSize, rotationHandleY + arrowSize, 0.0f, 0.0f);
|
||||
@@ -926,41 +987,42 @@ public class Mesh2D {
|
||||
/**
|
||||
* 创建网格的深拷贝
|
||||
*/
|
||||
public Mesh2D copy() {
|
||||
Mesh2D copy = new Mesh2D(name + "_copy");
|
||||
public Mesh2D copy() {
|
||||
Mesh2D copy = new Mesh2D(name + "_copy");
|
||||
|
||||
// 深拷贝数组(保证互不影响)
|
||||
copy.vertices = this.vertices != null ? this.vertices.clone() : new float[0];
|
||||
copy.uvs = this.uvs != null ? this.uvs.clone() : new float[0];
|
||||
copy.indices = this.indices != null ? this.indices.clone() : new int[0];
|
||||
// 深拷贝数组(保证互不影响)
|
||||
copy.vertices = this.vertices != null ? this.vertices.clone() : new float[0];
|
||||
copy.uvs = this.uvs != null ? this.uvs.clone() : new float[0];
|
||||
copy.indices = this.indices != null ? this.indices.clone() : new int[0];
|
||||
|
||||
// 保留 originalVertices(如果有),否则把当前 vertices 作为原始数据
|
||||
copy.originalVertices = this.originalVertices != null ? this.originalVertices.clone() : copy.vertices.clone();
|
||||
// 保留 originalVertices(如果有),否则把当前 vertices 作为原始数据
|
||||
copy.originalVertices = this.originalVertices != null ? this.originalVertices.clone() : copy.vertices.clone();
|
||||
|
||||
// 复制中心点
|
||||
copy.pivot = new Vector2f(this.pivot);
|
||||
// 复制 pivot 与 originalPivot
|
||||
copy.pivot = new Vector2f(this.pivot);
|
||||
copy.originalPivot = new Vector2f(this.originalPivot);
|
||||
|
||||
// 复制渲染/状态字段(保留纹理引用,但重置 GPU 句柄)
|
||||
copy.texture = this.texture;
|
||||
copy.visible = this.visible;
|
||||
copy.drawMode = this.drawMode;
|
||||
copy.bakedToWorld = this.bakedToWorld;
|
||||
// 复制渲染/状态字段(保留纹理引用,但重置 GPU 句柄)
|
||||
copy.texture = this.texture;
|
||||
copy.visible = this.visible;
|
||||
copy.drawMode = this.drawMode;
|
||||
copy.bakedToWorld = this.bakedToWorld;
|
||||
|
||||
// 重置 GPU 相关句柄,强制重新 uploadToGPU() 在渲染线程执行
|
||||
copy.vaoId = -1;
|
||||
copy.vboId = -1;
|
||||
copy.eboId = -1;
|
||||
copy.indexCount = this.indices != null ? this.indices.length : 0;
|
||||
copy.uploaded = false;
|
||||
// 重置 GPU 相关句柄,强制重新 uploadToGPU() 在渲染线程执行
|
||||
copy.vaoId = -1;
|
||||
copy.vboId = -1;
|
||||
copy.eboId = -1;
|
||||
copy.indexCount = this.indices != null ? this.indices.length : 0;
|
||||
copy.uploaded = false;
|
||||
|
||||
// 状态标记
|
||||
copy.dirty = true;
|
||||
copy.boundsDirty = true;
|
||||
copy.bounds = new BoundingBox();
|
||||
copy.selected = this.selected;
|
||||
// 状态标记
|
||||
copy.dirty = true;
|
||||
copy.boundsDirty = true;
|
||||
copy.bounds = new BoundingBox();
|
||||
copy.selected = this.selected;
|
||||
|
||||
return copy;
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
|
||||
public int getVaoId() {
|
||||
@@ -1046,4 +1108,4 @@ public class Mesh2D {
|
||||
sb.append('}');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.chuangzhou.vivid2D.test;
|
||||
import com.chuangzhou.vivid2D.render.awt.ModelClickListener;
|
||||
import com.chuangzhou.vivid2D.render.awt.ModelLayerPanel;
|
||||
import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel;
|
||||
import com.chuangzhou.vivid2D.render.awt.TransformPanel;
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
@@ -30,20 +31,13 @@ public class ModelLayerPanelTest {
|
||||
}
|
||||
|
||||
// 创建 UI
|
||||
JFrame frame = new JFrame("ModelLayerPanel 测试(含渲染面板)");
|
||||
JFrame frame = new JFrame("ModelLayerPanel 测试(含渲染面板和变换面板)");
|
||||
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
||||
frame.setLayout(new BorderLayout());
|
||||
|
||||
// 左侧:图层面板(传入 renderPanel 后可在面板中绑定贴图到 GL 上下文)
|
||||
// 先创建一个占位 renderPanel,再把它传给 layerPanel(ModelRenderPanel 构造需要尺寸)
|
||||
ModelRenderPanel renderPanel = new ModelRenderPanel(model, 640, 480);
|
||||
//renderPanel.addModelClickListener(new ModelClickListener() {
|
||||
// @Override
|
||||
// public void onModelClicked(Mesh2D mesh, float modelX, float modelY, int screenX, int screenY) {
|
||||
// if (mesh == null) return;
|
||||
// System.out.println("点击了模型:" + mesh.getName() + ",模型坐标:" + modelX + ", " + modelY + ",屏幕坐标:" + screenX + ", " + screenY);
|
||||
// }
|
||||
//});
|
||||
ModelLayerPanel layerPanel = new ModelLayerPanel(model, renderPanel);
|
||||
layerPanel.setPreferredSize(new Dimension(260, 600));
|
||||
frame.add(layerPanel, BorderLayout.WEST);
|
||||
@@ -52,11 +46,25 @@ public class ModelLayerPanelTest {
|
||||
renderPanel.setPreferredSize(new Dimension(640, 480));
|
||||
frame.add(renderPanel, BorderLayout.CENTER);
|
||||
|
||||
// 右侧:显示模型树(用于观察当前模型部件结构)
|
||||
// 创建变换面板
|
||||
TransformPanel transformPanel = new TransformPanel(renderPanel);
|
||||
|
||||
// 右侧:创建选项卡面板,包含模型树和变换面板
|
||||
JTabbedPane rightTabbedPane = new JTabbedPane();
|
||||
|
||||
// 模型树选项卡
|
||||
JTree tree = new JTree(model.toTreeNode());
|
||||
JScrollPane treeScroll = new JScrollPane(tree);
|
||||
treeScroll.setPreferredSize(new Dimension(240, 600));
|
||||
frame.add(treeScroll, BorderLayout.EAST);
|
||||
treeScroll.setPreferredSize(new Dimension(280, 600));
|
||||
rightTabbedPane.addTab("模型结构", treeScroll);
|
||||
|
||||
// 变换面板选项卡
|
||||
JScrollPane transformScroll = new JScrollPane(transformPanel);
|
||||
transformScroll.setPreferredSize(new Dimension(280, 600));
|
||||
rightTabbedPane.addTab("变换控制", transformScroll);
|
||||
|
||||
rightTabbedPane.setPreferredSize(new Dimension(300, 600));
|
||||
frame.add(rightTabbedPane, BorderLayout.EAST);
|
||||
|
||||
// 底部:演示按钮(刷新树以反映面板中对模型的更改)
|
||||
JPanel bottom = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
@@ -80,8 +88,39 @@ public class ModelLayerPanelTest {
|
||||
});
|
||||
bottom.add(printOrderBtn);
|
||||
|
||||
// 添加选中部件更新按钮
|
||||
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("未选中任何部件");
|
||||
}
|
||||
});
|
||||
});
|
||||
bottom.add(updateSelectionBtn);
|
||||
|
||||
frame.add(bottom, BorderLayout.SOUTH);
|
||||
|
||||
// 添加模型点击监听器,自动更新变换面板的选中部件
|
||||
renderPanel.addModelClickListener(new ModelClickListener() {
|
||||
@Override
|
||||
public void onModelClicked(Mesh2D mesh, float modelX, float modelY, int screenX, int screenY) {
|
||||
if (mesh == null) return;
|
||||
System.out.println("点击了模型:" + mesh.getName() + ",模型坐标:" + modelX + ", " + modelY + ",屏幕坐标:" + screenX + ", " + screenY);
|
||||
|
||||
// 自动更新变换面板的选中部件
|
||||
ModelPart selectedPart = renderPanel.getSelectedPart();
|
||||
transformPanel.setSelectedPart(selectedPart);
|
||||
|
||||
// 切换到变换控制选项卡
|
||||
rightTabbedPane.setSelectedIndex(1);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听窗口关闭,确保释放 GL 资源
|
||||
frame.addWindowListener(new java.awt.event.WindowAdapter() {
|
||||
@Override
|
||||
@@ -104,9 +143,9 @@ public class ModelLayerPanelTest {
|
||||
}
|
||||
});
|
||||
|
||||
frame.setSize(1200, 700);
|
||||
frame.setSize(1300, 700);
|
||||
frame.setLocationRelativeTo(null);
|
||||
frame.setVisible(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user