feat(render): 实现模型部件变换控制面板

- 新增 TransformPanel 类,提供图形界面控制模型部件的位移、旋转、缩放和中心点
- 在 ModelLayerPanelTest 中集成变换面板,支持自动更新选中部件
- 为 ModelPart 添加事件系统,支持变换属性变更通知
- 实现 Mesh2D 的 pivot 和 originalPivot 分离,支持更精确的变换控制- 添加 ModelEvent 接口,用于模型部件事件触发机制
- 优化 ModelRenderPanel 的选中部件获取逻辑
- 完善模型点击监听器,支持自动切换到变换控制选项卡
-修复拖拽移动中心点时的边界检查问题
- 增强各变换操作的边界验证和错误处理
- 改进中心点绘制逻辑,增加边界检查和回退机制

重要更新
- 修复上个版本的所有问题,并且增加新的面板观测图层的各种信息
This commit is contained in:
tzdwindows 7
2025-10-18 15:27:04 +08:00
parent b3c50ca794
commit 6a3eb89aaf
7 changed files with 815 additions and 85 deletions

View File

@@ -986,4 +986,4 @@ public class ModelLayerPanel extends JPanel {
e.printStackTrace();
}
}
}
}

View File

@@ -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 资源已清理");
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -0,0 +1,9 @@
package com.chuangzhou.vivid2D.render.model;
/**
* 模型事件
* @author tzdwindows 7
*/
public interface ModelEvent {
void trigger(String eventName,Object eventBus);
}

View File

@@ -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) 因为这个为非00时后面如果想要热变换就会出问题
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 的世界变换来计算
// 这里只是设置了 originalPivotMesh2D 的实际 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() +
'}';
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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再把它传给 layerPanelModelRenderPanel 构造需要尺寸)
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);
});
}
}
}