feat(animation): 添加关键帧编辑与插值功能
- 为 AnimationParameter 类添加关键帧管理功能,包括添加、删除、查找和清空关键帧 - 实现关键帧的复制方法,支持完整状态复制 - 添加关键帧吸附与最近关键帧查找逻辑 - 实现 FrameInterpolator 类,支持 position、scale、pivot、rotation 和 secondaryVertex 的插值计算 - 添加 KeyframeEditorDialog 图形界面,用于可视化编辑关键帧- 支持鼠标交互添加/删除关键帧,并提供悬浮提示功能 - 实现标尺组件,用于显示关键帧分布与当前值位置 - 添加对角度单位的自动识别与归一化处理- 支持 secondary vertex 的插值与删除标记处理-优化插值性能,减少不必要的中间更新,提升渲染效率
This commit is contained in:
@@ -0,0 +1,677 @@
|
||||
package com.chuangzhou.vivid2D.render.awt;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.AnimationParameter;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.Border;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.table.AbstractTableModel;
|
||||
import javax.swing.table.DefaultTableCellRenderer;
|
||||
import javax.swing.table.TableCellRenderer;
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
|
||||
public class KeyframeEditorDialog extends JDialog {
|
||||
|
||||
// --- 现代UI颜色定义 ---
|
||||
private static final Color COLOR_BACKGROUND = new Color(50, 50, 50);
|
||||
private static final Color COLOR_FOREGROUND = new Color(220, 220, 220);
|
||||
private static final Color COLOR_HEADER = new Color(70, 70, 70);
|
||||
private static final Color COLOR_ACCENT_1 = new Color(230, 80, 80); // 关键帧 (红色)
|
||||
private static final Color COLOR_ACCENT_2 = new Color(80, 150, 230); // 当前值 (蓝色)
|
||||
private static final Color COLOR_GRID = new Color(60, 60, 60);
|
||||
private static final Border DIALOG_PADDING = new EmptyBorder(10, 10, 10, 10);
|
||||
|
||||
private final AnimationParameter parameter;
|
||||
private final EditorRuler ruler;
|
||||
private final KeyframeTableModel tableModel;
|
||||
private final JTable keyframeTable;
|
||||
private final JTextField addField = new JTextField(8);
|
||||
|
||||
/**
|
||||
* 临时存储编辑,直到用户点击 "OK"
|
||||
*/
|
||||
private final TreeSet<Float> tempKeyframes;
|
||||
|
||||
// 用于跟踪 OK/Cancel 状态
|
||||
private boolean confirmed = false;
|
||||
|
||||
// 内部类,用于显示和编辑的标尺
|
||||
private class EditorRuler extends JComponent {
|
||||
private static final int RULER_HEIGHT = 25;
|
||||
private static final int MARKER_SIZE = 8;
|
||||
private static final int TICK_HEIGHT = 5;
|
||||
private static final int PADDING = 15; // 左右内边距
|
||||
private static final int LABEL_VMARGIN = 3; // 标签垂直边距
|
||||
|
||||
// --- 用于跟踪鼠标悬浮 ---
|
||||
private int mouseHoverX = -1;
|
||||
private float mouseHoverValue = 0.0f;
|
||||
// --------------------------
|
||||
|
||||
EditorRuler() {
|
||||
setPreferredSize(new Dimension(100, RULER_HEIGHT + 35));
|
||||
setBackground(COLOR_BACKGROUND);
|
||||
setForeground(COLOR_FOREGROUND);
|
||||
setOpaque(true);
|
||||
|
||||
addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
float value = xToValue(e.getX());
|
||||
float range = parameter.getMaxValue() - parameter.getMinValue();
|
||||
float snapThresholdPx = 4; // 4 像素
|
||||
float snapThreshold = (range > 0) ? (xToValue(getPadding() + (int)snapThresholdPx) - xToValue(getPadding())) : 0;
|
||||
|
||||
Float nearest = getNearestTempKeyframe(value, snapThreshold);
|
||||
|
||||
if (e.isShiftDown() || SwingUtilities.isRightMouseButton(e)) { // 按住 Shift 或右键删除
|
||||
if (nearest != null) {
|
||||
tempKeyframes.remove(nearest);
|
||||
}
|
||||
} else if (SwingUtilities.isLeftMouseButton(e)) {
|
||||
if (nearest != null) {
|
||||
// 选中已有的
|
||||
int row = tableModel.getRowForValue(nearest);
|
||||
if (row != -1) {
|
||||
keyframeTable.setRowSelectionInterval(row, row);
|
||||
keyframeTable.scrollRectToVisible(keyframeTable.getCellRect(row, 0, true));
|
||||
}
|
||||
} else {
|
||||
// 添加新的 (钳位)
|
||||
float clampedValue = Math.max(parameter.getMinValue(), Math.min(parameter.getMaxValue(), value));
|
||||
tempKeyframes.add(clampedValue);
|
||||
}
|
||||
}
|
||||
updateAllUI();
|
||||
}
|
||||
|
||||
// --- [修复] mouseExited 移到这里 (MouseAdapter) ---
|
||||
@Override
|
||||
public void mouseExited(MouseEvent e) {
|
||||
mouseHoverX = -1; // 鼠标离开,清除悬浮位置
|
||||
repaint(); // 触发重绘以隐藏提示
|
||||
}
|
||||
// ----------------------------------------------------
|
||||
});
|
||||
|
||||
// --- 鼠标移动监听器 ---
|
||||
addMouseMotionListener(new MouseMotionAdapter() {
|
||||
@Override
|
||||
public void mouseMoved(MouseEvent e) {
|
||||
mouseHoverX = e.getX();
|
||||
mouseHoverValue = xToValue(mouseHoverX);
|
||||
repaint(); // 触发重绘以显示悬浮提示
|
||||
}
|
||||
// 注意:这里不再需要 @Override public void mouseExited(MouseEvent e)
|
||||
});
|
||||
// ----------------------------------
|
||||
}
|
||||
|
||||
private int getPadding() {
|
||||
return PADDING;
|
||||
}
|
||||
|
||||
private float xToValue(int x) {
|
||||
int padding = getPadding();
|
||||
int trackWidth = getWidth() - padding * 2;
|
||||
if (trackWidth <= 0) return parameter.getMinValue();
|
||||
|
||||
float percent = Math.max(0f, Math.min(1f, (float) (x - padding) / trackWidth));
|
||||
return parameter.getMinValue() + percent * (parameter.getMaxValue() - parameter.getMinValue());
|
||||
}
|
||||
|
||||
private int valueToX(float value) {
|
||||
int padding = getPadding();
|
||||
int trackWidth = getWidth() - padding * 2;
|
||||
float range = parameter.getMaxValue() - parameter.getMinValue();
|
||||
float percent = 0;
|
||||
if (range > 0) {
|
||||
// [修复] 确保钳位
|
||||
percent = (Math.max(parameter.getMinValue(), Math.min(parameter.getMaxValue(), value)) - parameter.getMinValue()) / range;
|
||||
}
|
||||
return padding + (int) (percent * trackWidth);
|
||||
}
|
||||
|
||||
private Float getNearestTempKeyframe(float value, float snapThreshold) {
|
||||
if (snapThreshold <= 0) return null;
|
||||
|
||||
Float lower = tempKeyframes.floor(value);
|
||||
Float higher = tempKeyframes.ceiling(value);
|
||||
|
||||
float distToLower = (lower != null) ? Math.abs(value - lower) : Float.MAX_VALUE;
|
||||
float distToHigher = (higher != null) ? Math.abs(value - higher) : Float.MAX_VALUE;
|
||||
|
||||
if (distToLower < snapThreshold && distToLower <= distToHigher) {
|
||||
return lower;
|
||||
}
|
||||
if (distToHigher < snapThreshold && distToHigher < distToLower) {
|
||||
return higher;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void paintComponent(Graphics g) {
|
||||
super.paintComponent(g); // 绘制不透明背景
|
||||
Graphics2D g2 = (Graphics2D) g.create(); // 使用 g.create() 防止 g 被修改
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
|
||||
// [修复] 强制清除背景,防止渲染残留
|
||||
g2.setColor(getBackground());
|
||||
g2.fillRect(0, 0, getWidth(), getHeight());
|
||||
|
||||
int padding = getPadding();
|
||||
int w = getWidth() - padding * 2;
|
||||
int h = getHeight();
|
||||
|
||||
int topOffset = 15;
|
||||
int trackY = topOffset + (h - topOffset) / 2; // 垂直居中于剩余空间
|
||||
|
||||
// 1. 轨道
|
||||
g2.setColor(COLOR_GRID);
|
||||
g2.setStroke(new BasicStroke(2));
|
||||
g2.drawLine(padding, trackY, padding + w, trackY);
|
||||
|
||||
// 2. 绘制刻度和标签 (min, max, mid)
|
||||
float min = parameter.getMinValue();
|
||||
float max = parameter.getMaxValue();
|
||||
|
||||
drawTick(g2, min, trackY, true); // 强制绘制 min
|
||||
drawTick(g2, max, trackY, true); // 强制绘制 max
|
||||
|
||||
// 仅在 min 和 max 不太近时绘制 mid
|
||||
if (Math.abs(max-min) > 0.1) {
|
||||
float mid = min + (max - min) / 2;
|
||||
drawTick(g2, mid, trackY, false); // 不强制绘制 mid
|
||||
}
|
||||
|
||||
// 3. 绘制关键帧 (来自 tempKeyframes)
|
||||
g2.setColor(COLOR_ACCENT_1);
|
||||
for (float f : tempKeyframes) {
|
||||
int x = valueToX(f);
|
||||
g2.fillOval(x - MARKER_SIZE / 2, trackY - MARKER_SIZE / 2, MARKER_SIZE, MARKER_SIZE);
|
||||
}
|
||||
|
||||
// --- 4. 绘制鼠标悬浮值 (Hover Value) ---
|
||||
if (mouseHoverX != -1) {
|
||||
if (mouseHoverX >= padding && mouseHoverX <= padding + w) {
|
||||
g2.setColor(COLOR_ACCENT_1);
|
||||
g2.drawLine(mouseHoverX, trackY - TICK_HEIGHT - 2, mouseHoverX, trackY + TICK_HEIGHT + 2);
|
||||
String hoverLabel = String.format("%.2f", mouseHoverValue);
|
||||
FontMetrics fm = g2.getFontMetrics();
|
||||
int labelWidth = fm.stringWidth(hoverLabel);
|
||||
int labelY = topOffset + (RULER_HEIGHT / 2) - fm.getAscent() / 2;
|
||||
int labelX = mouseHoverX - labelWidth / 2;
|
||||
labelX = Math.max(padding, Math.min(labelX, getWidth() - padding - labelWidth));
|
||||
g2.drawString(hoverLabel, labelX, labelY);
|
||||
}
|
||||
}
|
||||
// ---------------------------------------------
|
||||
|
||||
// 5. 绘制当前值 (来自 parameter)
|
||||
g2.setColor(COLOR_ACCENT_2);
|
||||
int x = valueToX(parameter.getValue());
|
||||
g2.fillOval(x - MARKER_SIZE / 2, trackY - MARKER_SIZE / 2, MARKER_SIZE, MARKER_SIZE);
|
||||
|
||||
g2.dispose(); // 释放 g.create() 创建的 Graphics
|
||||
}
|
||||
|
||||
private void drawTick(Graphics2D g2, float value, int y, boolean forceLabel) {
|
||||
int x = valueToX(value);
|
||||
g2.setColor(COLOR_GRID.brighter());
|
||||
g2.drawLine(x, y - TICK_HEIGHT, x, y + TICK_HEIGHT);
|
||||
|
||||
// [修复] 仅在空间足够或被强制时绘制标签
|
||||
FontMetrics fm = g2.getFontMetrics();
|
||||
// [修复] 格式化为 2 位小数
|
||||
String label = String.format("%.2f", value);
|
||||
int labelWidth = fm.stringWidth(label);
|
||||
|
||||
// 简单的防重叠
|
||||
boolean fits = (labelWidth < (getWidth() - getPadding()*2) / 5);
|
||||
|
||||
if (forceLabel || fits) {
|
||||
g2.setColor(COLOR_FOREGROUND);
|
||||
g2.drawString(label, x - labelWidth / 2, y + TICK_HEIGHT + fm.getAscent() + LABEL_VMARGIN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义 TableModel 以显示 "No." 和 "Value"
|
||||
*/
|
||||
private class KeyframeTableModel extends AbstractTableModel {
|
||||
private final String[] columnNames = {"序", "值"};
|
||||
private java.util.List<Float> keyframes = new ArrayList<>();
|
||||
|
||||
public void setData(SortedSet<Float> data) {
|
||||
this.keyframes.clear();
|
||||
this.keyframes.addAll(data);
|
||||
fireTableDataChanged();
|
||||
}
|
||||
|
||||
public int getRowForValue(float value) {
|
||||
int index = Collections.binarySearch(keyframes, value);
|
||||
return (index < 0) ? -1 : index;
|
||||
}
|
||||
|
||||
public Float getValueAtRow(int row) {
|
||||
if (row >= 0 && row < keyframes.size()) {
|
||||
return keyframes.get(row);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRowCount() {
|
||||
return keyframes.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnCount() {
|
||||
return columnNames.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColumnName(int column) {
|
||||
return columnNames[column];
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValueAt(int rowIndex, int columnIndex) {
|
||||
if (columnIndex == 0) {
|
||||
return rowIndex + 1; // "No" (序号)
|
||||
}
|
||||
if (columnIndex == 1) {
|
||||
return keyframes.get(rowIndex); // "Value" (值) - [修复] 返回 Float 对象
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- 允许 "值" 列被编辑 ---
|
||||
@Override
|
||||
public boolean isCellEditable(int rowIndex, int columnIndex) {
|
||||
return columnIndex == 1; // 只有 "值" 列可以编辑
|
||||
}
|
||||
|
||||
// --- 处理单元格编辑后的值 ---
|
||||
@Override
|
||||
public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
|
||||
if (columnIndex != 1) return;
|
||||
|
||||
float newValue;
|
||||
try {
|
||||
newValue = Float.parseFloat(aValue.toString());
|
||||
} catch (NumberFormatException e) {
|
||||
JOptionPane.showMessageDialog(KeyframeEditorDialog.this,
|
||||
"请输入有效的浮点数", "格式错误", JOptionPane.ERROR_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
// 钳位
|
||||
newValue = Math.max(parameter.getMinValue(), Math.min(parameter.getMaxValue(), newValue));
|
||||
|
||||
// 获取旧值
|
||||
Float oldValue = getValueAtRow(rowIndex);
|
||||
if (oldValue == null) return;
|
||||
|
||||
// 更新临时的 Set
|
||||
tempKeyframes.remove(oldValue);
|
||||
tempKeyframes.add(newValue);
|
||||
|
||||
// 彻底刷新UI (因为 Set 排序可能已改变)
|
||||
updateAllUI();
|
||||
|
||||
// 刷新后,重新定位并选中新值的行
|
||||
int newRow = tableModel.getRowForValue(newValue);
|
||||
if (newRow != -1) {
|
||||
keyframeTable.setRowSelectionInterval(newRow, newRow);
|
||||
}
|
||||
}
|
||||
// ------------------------------------
|
||||
|
||||
@Override
|
||||
public Class<?> getColumnClass(int columnIndex) {
|
||||
if (columnIndex == 0) return Integer.class;
|
||||
if (columnIndex == 1) return Float.class;
|
||||
return Object.class;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public KeyframeEditorDialog(Window owner, AnimationParameter parameter) {
|
||||
super(owner, "关键帧编辑器: " + parameter.getId(), ModalityType.APPLICATION_MODAL);
|
||||
this.parameter = parameter;
|
||||
|
||||
this.tempKeyframes = new TreeSet<>(parameter.getKeyframes());
|
||||
|
||||
this.ruler = new EditorRuler();
|
||||
this.tableModel = new KeyframeTableModel();
|
||||
this.keyframeTable = new JTable(tableModel);
|
||||
|
||||
initUI();
|
||||
updateAllUI();
|
||||
}
|
||||
|
||||
private void initUI() {
|
||||
setSize(500, 400);
|
||||
setMinimumSize(new Dimension(450, 350));
|
||||
setResizable(true);
|
||||
|
||||
setLocationRelativeTo(getOwner());
|
||||
getContentPane().setBackground(COLOR_BACKGROUND);
|
||||
setLayout(new BorderLayout(5, 5));
|
||||
((JPanel) getContentPane()).setBorder(DIALOG_PADDING);
|
||||
|
||||
// 1. 顶部标尺
|
||||
ruler.setBorder(BorderFactory.createTitledBorder(
|
||||
BorderFactory.createLineBorder(COLOR_GRID), "标尺 (点击添加, Shift/右键 删除)",
|
||||
0, 0, getFont(), COLOR_FOREGROUND
|
||||
));
|
||||
add(ruler, BorderLayout.NORTH);
|
||||
|
||||
// 2. 中间列表
|
||||
configureTableAppearance();
|
||||
JScrollPane scroll = new JScrollPane(keyframeTable);
|
||||
configureScrollPaneAppearance(scroll);
|
||||
|
||||
JPanel centerPanel = new JPanel(new BorderLayout(5, 5));
|
||||
centerPanel.setBackground(COLOR_BACKGROUND);
|
||||
centerPanel.add(scroll, BorderLayout.CENTER);
|
||||
centerPanel.add(createListActionsPanel(), BorderLayout.EAST);
|
||||
|
||||
add(centerPanel, BorderLayout.CENTER);
|
||||
|
||||
// 3. 底部操作栏 (OK/Cancel)
|
||||
add(createBottomPanel(), BorderLayout.SOUTH);
|
||||
|
||||
// --- 为 JTable 添加双击监听器 ---
|
||||
keyframeTable.addMouseListener(new MouseAdapter() {
|
||||
public void mousePressed(MouseEvent e) {
|
||||
if (e.getClickCount() == 2) {
|
||||
int row = keyframeTable.rowAtPoint(e.getPoint());
|
||||
int col = keyframeTable.columnAtPoint(e.getPoint());
|
||||
// 确保是 "值" 列 (索引 1)
|
||||
if (row >= 0 && col == 1) {
|
||||
// 启动编辑
|
||||
if (keyframeTable.editCellAt(row, col)) {
|
||||
// 尝试选中编辑器中的文本
|
||||
Component editor = keyframeTable.getEditorComponent();
|
||||
if (editor instanceof JTextField) {
|
||||
((JTextField)editor).selectAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// ------------------------------------
|
||||
}
|
||||
|
||||
private JPanel createListActionsPanel() {
|
||||
JPanel actionsPanel = new JPanel();
|
||||
actionsPanel.setBackground(COLOR_BACKGROUND);
|
||||
actionsPanel.setLayout(new GridBagLayout());
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.insets = new Insets(0, 5, 0, 5);
|
||||
gbc.anchor = GridBagConstraints.NORTH;
|
||||
gbc.weighty = 1.0;
|
||||
|
||||
JButton delButton = new JButton("删除");
|
||||
styleButton(delButton);
|
||||
delButton.addActionListener(e -> removeSelectedKeyframe());
|
||||
|
||||
actionsPanel.add(delButton, gbc);
|
||||
|
||||
return actionsPanel;
|
||||
}
|
||||
|
||||
private JPanel createBottomPanel() {
|
||||
JPanel bottomPanel = new JPanel(new BorderLayout(5, 5));
|
||||
bottomPanel.setBackground(COLOR_BACKGROUND);
|
||||
|
||||
// 左侧:添加新帧
|
||||
JPanel addPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0));
|
||||
addPanel.setBackground(COLOR_BACKGROUND);
|
||||
|
||||
JLabel addLabel = new JLabel("添加值:");
|
||||
addLabel.setForeground(COLOR_FOREGROUND);
|
||||
styleTextField(addField);
|
||||
|
||||
JButton addButton = new JButton("添加");
|
||||
styleButton(addButton);
|
||||
|
||||
addPanel.add(addLabel);
|
||||
addPanel.add(addField);
|
||||
addPanel.add(addButton);
|
||||
|
||||
// 右侧:OK / Cancel
|
||||
JPanel okCancelPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0));
|
||||
okCancelPanel.setBackground(COLOR_BACKGROUND);
|
||||
|
||||
JButton okButton = new JButton("确定");
|
||||
JButton cancelButton = new JButton("取消");
|
||||
styleButton(okButton);
|
||||
styleButton(cancelButton);
|
||||
|
||||
okCancelPanel.add(okButton);
|
||||
okCancelPanel.add(cancelButton);
|
||||
|
||||
bottomPanel.add(addPanel, BorderLayout.CENTER);
|
||||
bottomPanel.add(okCancelPanel, BorderLayout.EAST);
|
||||
|
||||
// 事件绑定
|
||||
addButton.addActionListener(e -> addKeyframeFromField());
|
||||
addField.addActionListener(e -> addKeyframeFromField());
|
||||
|
||||
okButton.addActionListener(e -> onOK());
|
||||
cancelButton.addActionListener(e -> onCancel());
|
||||
|
||||
// Esc 键关闭 = Cancel
|
||||
getRootPane().registerKeyboardAction(e -> onCancel(),
|
||||
KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
|
||||
JComponent.WHEN_IN_FOCUSED_WINDOW);
|
||||
|
||||
return bottomPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认更改,应用到原始 parameter
|
||||
* [修复] 恢复为 private 访问权限,因为它不覆盖任何父类方法。
|
||||
*/
|
||||
private void onOK() {
|
||||
// 停止任何可能的单元格编辑
|
||||
if (keyframeTable.isEditing()) {
|
||||
keyframeTable.getCellEditor().stopCellEditing();
|
||||
}
|
||||
|
||||
parameter.clearKeyframes();
|
||||
for (Float f : tempKeyframes) {
|
||||
parameter.addKeyframe(f);
|
||||
}
|
||||
this.confirmed = true; // 标记为已确认
|
||||
dispose();
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消更改
|
||||
* [修复] 恢复为 private 访问权限,因为它不覆盖任何父类方法。
|
||||
*/
|
||||
private void onCancel() {
|
||||
// 停止任何可能的单元格编辑,但丢弃结果
|
||||
if (keyframeTable.isEditing()) {
|
||||
keyframeTable.getCellEditor().cancelCellEditing();
|
||||
}
|
||||
|
||||
this.confirmed = false; // 标记为未确认
|
||||
dispose();
|
||||
}
|
||||
|
||||
private void addKeyframeFromField() {
|
||||
try {
|
||||
float val = Float.parseFloat(addField.getText().trim());
|
||||
// 钳位
|
||||
val = Math.max(parameter.getMinValue(), Math.min(parameter.getMaxValue(), val));
|
||||
|
||||
tempKeyframes.add(val);
|
||||
updateAllUI();
|
||||
|
||||
// 添加后自动选中
|
||||
int row = tableModel.getRowForValue(val);
|
||||
if (row != -1) {
|
||||
keyframeTable.setRowSelectionInterval(row, row);
|
||||
keyframeTable.scrollRectToVisible(keyframeTable.getCellRect(row, 0, true));
|
||||
}
|
||||
|
||||
addField.setText("");
|
||||
} catch (NumberFormatException e) {
|
||||
JOptionPane.showMessageDialog(this, "无效的数值", "错误", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeSelectedKeyframe() {
|
||||
int selectedRow = keyframeTable.getSelectedRow();
|
||||
if (selectedRow != -1) {
|
||||
Float val = tableModel.getValueAtRow(selectedRow);
|
||||
if (val != null) {
|
||||
tempKeyframes.remove(val);
|
||||
updateAllUI();
|
||||
|
||||
// 重新选中删除后的下一行
|
||||
if (tableModel.getRowCount() > 0) {
|
||||
int newSel = Math.min(selectedRow, tableModel.getRowCount() - 1);
|
||||
keyframeTable.setRowSelectionInterval(newSel, newSel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAllUI() {
|
||||
// 更新列表
|
||||
tableModel.setData(tempKeyframes);
|
||||
// 重绘标尺
|
||||
ruler.repaint();
|
||||
}
|
||||
|
||||
// --- 辅助方法:设置UI风格 ---
|
||||
|
||||
private void configureTableAppearance() {
|
||||
keyframeTable.setBackground(COLOR_BACKGROUND);
|
||||
keyframeTable.setForeground(COLOR_FOREGROUND);
|
||||
keyframeTable.setGridColor(COLOR_GRID);
|
||||
keyframeTable.setSelectionBackground(COLOR_ACCENT_2);
|
||||
keyframeTable.setSelectionForeground(Color.WHITE);
|
||||
keyframeTable.getTableHeader().setBackground(COLOR_HEADER);
|
||||
keyframeTable.getTableHeader().setForeground(COLOR_FOREGROUND);
|
||||
keyframeTable.setFont(getFont().deriveFont(14f));
|
||||
keyframeTable.setRowHeight(20);
|
||||
|
||||
// 居中 "No" 列
|
||||
DefaultTableCellRenderer centerRenderer = new DefaultTableCellRenderer();
|
||||
centerRenderer.setHorizontalAlignment(JLabel.CENTER);
|
||||
centerRenderer.setBackground(COLOR_BACKGROUND);
|
||||
centerRenderer.setForeground(COLOR_FOREGROUND);
|
||||
keyframeTable.getColumnModel().getColumn(0).setMaxWidth(60);
|
||||
keyframeTable.getColumnModel().getColumn(0).setCellRenderer(centerRenderer);
|
||||
|
||||
// "Value" 列,格式化浮点数
|
||||
TableCellRenderer floatRenderer = new DefaultTableCellRenderer() {
|
||||
{ // Instance initializer
|
||||
setHorizontalAlignment(JLabel.RIGHT); // 数字右对齐
|
||||
setBorder(new EmptyBorder(0, 5, 0, 5)); // 增加内边距
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
|
||||
super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
|
||||
|
||||
if (value instanceof Float) {
|
||||
setText(String.format("%.6f", (Float) value));
|
||||
}
|
||||
|
||||
if (!isSelected) {
|
||||
setBackground(COLOR_BACKGROUND);
|
||||
setForeground(COLOR_FOREGROUND);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
};
|
||||
keyframeTable.getColumnModel().getColumn(1).setCellRenderer(floatRenderer);
|
||||
|
||||
// 为 "值" 列设置一个暗黑风格的编辑器
|
||||
JTextField editorTextField = new JTextField();
|
||||
styleTextField(editorTextField); // 复用暗黑风格
|
||||
editorTextField.setBorder(BorderFactory.createLineBorder(COLOR_ACCENT_2)); // 编辑时高亮
|
||||
keyframeTable.getColumnModel().getColumn(1).setCellEditor(new DefaultCellEditor(editorTextField));
|
||||
}
|
||||
|
||||
private void configureScrollPaneAppearance(JScrollPane scroll) {
|
||||
scroll.setBackground(COLOR_BACKGROUND);
|
||||
scroll.getViewport().setBackground(COLOR_BACKGROUND);
|
||||
scroll.setBorder(BorderFactory.createLineBorder(COLOR_GRID));
|
||||
scroll.getVerticalScrollBar().setUI(new javax.swing.plaf.basic.BasicScrollBarUI() {
|
||||
@Override
|
||||
protected void configureScrollBarColors() {
|
||||
this.thumbColor = COLOR_HEADER;
|
||||
this.trackColor = COLOR_BACKGROUND;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JButton createDecreaseButton(int orientation) {
|
||||
return createZeroButton();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JButton createIncreaseButton(int orientation) {
|
||||
return createZeroButton();
|
||||
}
|
||||
|
||||
private JButton createZeroButton() {
|
||||
JButton b = new JButton();
|
||||
b.setPreferredSize(new Dimension(0, 0));
|
||||
return b;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void styleButton(JButton button) {
|
||||
button.setBackground(COLOR_HEADER);
|
||||
button.setForeground(COLOR_FOREGROUND);
|
||||
button.setFocusPainted(false);
|
||||
button.setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createLineBorder(COLOR_GRID),
|
||||
new EmptyBorder(5, 10, 5, 10)
|
||||
));
|
||||
}
|
||||
|
||||
private void styleTextField(JTextField field) {
|
||||
field.setBackground(COLOR_HEADER);
|
||||
field.setForeground(COLOR_FOREGROUND);
|
||||
field.setCaretColor(COLOR_FOREGROUND);
|
||||
field.setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createLineBorder(COLOR_GRID),
|
||||
new EmptyBorder(4, 4, 4, 4)
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
public boolean isConfirmed() {
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示对话框。
|
||||
*/
|
||||
public static boolean showEditor(Window owner, AnimationParameter parameter) {
|
||||
if (parameter == null) return false;
|
||||
KeyframeEditorDialog dialog = new KeyframeEditorDialog(owner, parameter);
|
||||
dialog.setVisible(true);
|
||||
return dialog.isConfirmed();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package com.chuangzhou.vivid2D.render.awt;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.AnimationParameter;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ChangeListener;
|
||||
import java.awt.*;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.MouseMotionAdapter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 一个自定义的滑块控件,用于显示和操作 AnimationParameter。
|
||||
* 它可以显示关键帧标记,并实现拖拽时的吸附功能。
|
||||
*/
|
||||
public class KeyframeSlider extends JComponent {
|
||||
|
||||
private static final int TRACK_HEIGHT = 6;
|
||||
private static final int THUMB_SIZE = 14;
|
||||
private static final int KEYFRAME_MARKER_SIZE = 8;
|
||||
private static final int PADDING = 8;
|
||||
|
||||
private AnimationParameter parameter;
|
||||
private boolean isDragging = false;
|
||||
private final List<ChangeListener> listeners = new ArrayList<>();
|
||||
|
||||
// 吸附阈值(占总宽度的百分比)
|
||||
private final float snapThresholdPercent = 0.02f; // 2%
|
||||
|
||||
public KeyframeSlider() {
|
||||
setPreferredSize(new Dimension(100, THUMB_SIZE + PADDING * 2));
|
||||
setMinimumSize(new Dimension(50, THUMB_SIZE + PADDING * 2));
|
||||
|
||||
addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
if (parameter == null) return;
|
||||
isDragging = true;
|
||||
updateValueFromMouse(e.getX());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
isDragging = false;
|
||||
}
|
||||
});
|
||||
|
||||
addMouseMotionListener(new MouseMotionAdapter() {
|
||||
@Override
|
||||
public void mouseDragged(MouseEvent e) {
|
||||
if (isDragging && parameter != null) {
|
||||
updateValueFromMouse(e.getX());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置此滑块绑定的参数。
|
||||
*/
|
||||
public void setParameter(AnimationParameter p) {
|
||||
this.parameter = p;
|
||||
repaint();
|
||||
}
|
||||
|
||||
public AnimationParameter getParameter() {
|
||||
return parameter;
|
||||
}
|
||||
|
||||
private void updateValueFromMouse(int mouseX) {
|
||||
if (parameter == null) return;
|
||||
|
||||
int trackStart = PADDING;
|
||||
int trackWidth = getWidth() - PADDING * 2;
|
||||
float percent = Math.max(0f, Math.min(1f, (float) (mouseX - trackStart) / trackWidth));
|
||||
|
||||
float min = parameter.getMinValue();
|
||||
float max = parameter.getMaxValue();
|
||||
float range = max - min;
|
||||
float newValue = min + percent * range;
|
||||
|
||||
// --- 吸附逻辑 ---
|
||||
float snapThreshold = range * snapThresholdPercent;
|
||||
Float nearestKeyframe = parameter.getNearestKeyframe(newValue, snapThreshold);
|
||||
if (nearestKeyframe != null) {
|
||||
newValue = nearestKeyframe;
|
||||
}
|
||||
// ----------------
|
||||
|
||||
if (parameter.getValue() != newValue) {
|
||||
parameter.setValue(newValue); // setValue 会自动钳位
|
||||
fireStateChanged();
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintComponent(Graphics g) {
|
||||
super.paintComponent(g);
|
||||
Graphics2D g2 = (Graphics2D) g;
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
|
||||
if (parameter == null) {
|
||||
paintDisabled(g2);
|
||||
return;
|
||||
}
|
||||
|
||||
int trackY = (getHeight() - TRACK_HEIGHT) / 2;
|
||||
int trackStart = PADDING;
|
||||
int trackWidth = getWidth() - PADDING * 2;
|
||||
|
||||
// 1. 绘制轨道
|
||||
g2.setColor(getBackground().darker());
|
||||
g2.fillRoundRect(trackStart, trackY, trackWidth, TRACK_HEIGHT, TRACK_HEIGHT, TRACK_HEIGHT);
|
||||
|
||||
// 2. 绘制关键帧标记
|
||||
g2.setColor(Color.CYAN.darker());
|
||||
int markerY = (getHeight() - KEYFRAME_MARKER_SIZE) / 2;
|
||||
for (float keyframeValue : parameter.getKeyframes()) {
|
||||
float percent = (keyframeValue - parameter.getMinValue()) / (parameter.getMaxValue() - parameter.getMinValue());
|
||||
int markerX = trackStart + (int) (percent * trackWidth) - KEYFRAME_MARKER_SIZE / 2;
|
||||
// 绘制菱形
|
||||
Polygon diamond = new Polygon();
|
||||
diamond.addPoint(markerX + KEYFRAME_MARKER_SIZE / 2, markerY);
|
||||
diamond.addPoint(markerX + KEYFRAME_MARKER_SIZE, markerY + KEYFRAME_MARKER_SIZE / 2);
|
||||
diamond.addPoint(markerX + KEYFRAME_MARKER_SIZE / 2, markerY + KEYFRAME_MARKER_SIZE);
|
||||
diamond.addPoint(markerX, markerY + KEYFRAME_MARKER_SIZE / 2);
|
||||
g2.fill(diamond);
|
||||
}
|
||||
|
||||
// 3. 绘制滑块 (Thumb)
|
||||
float currentPercent = parameter.getNormalizedValue();
|
||||
int thumbX = trackStart + (int) (currentPercent * trackWidth) - THUMB_SIZE / 2;
|
||||
int thumbY = (getHeight() - THUMB_SIZE) / 2;
|
||||
|
||||
g2.setColor(isEnabled() ? getForeground() : Color.GRAY);
|
||||
g2.fillOval(thumbX, thumbY, THUMB_SIZE, THUMB_SIZE);
|
||||
g2.setColor(getBackground());
|
||||
g2.drawOval(thumbX, thumbY, THUMB_SIZE, THUMB_SIZE);
|
||||
}
|
||||
|
||||
private void paintDisabled(Graphics2D g2) {
|
||||
int trackY = (getHeight() - TRACK_HEIGHT) / 2;
|
||||
int trackStart = PADDING;
|
||||
int trackWidth = getWidth() - PADDING * 2;
|
||||
g2.setColor(Color.GRAY.brighter());
|
||||
g2.fillRoundRect(trackStart, trackY, trackWidth, TRACK_HEIGHT, TRACK_HEIGHT, TRACK_HEIGHT);
|
||||
}
|
||||
|
||||
// --- ChangeEvent 支持 (与 JSlider 保持一致) ---
|
||||
public void addChangeListener(ChangeListener l) {
|
||||
listeners.add(l);
|
||||
}
|
||||
|
||||
public void removeChangeListener(ChangeListener l) {
|
||||
listeners.remove(l);
|
||||
}
|
||||
|
||||
protected void fireStateChanged() {
|
||||
ChangeEvent e = new ChangeEvent(this);
|
||||
for (ChangeListener l : new ArrayList<>(listeners)) {
|
||||
l.stateChanged(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,9 @@ import com.chuangzhou.vivid2D.render.awt.tools.SelectionTool;
|
||||
import com.chuangzhou.vivid2D.render.awt.tools.Tool;
|
||||
import com.chuangzhou.vivid2D.render.awt.tools.VertexDeformationTool;
|
||||
import com.chuangzhou.vivid2D.render.awt.tools.LiquifyTool;
|
||||
import com.chuangzhou.vivid2D.render.awt.util.FrameInterpolator;
|
||||
import com.chuangzhou.vivid2D.render.awt.util.OperationHistoryGlobal;
|
||||
import com.chuangzhou.vivid2D.render.model.AnimationParameter;
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
@@ -53,6 +55,7 @@ public class ModelRenderPanel extends JPanel {
|
||||
private final CopyOnWriteArrayList<ModelClickListener> clickListeners = new CopyOnWriteArrayList<>();
|
||||
private final StatusRecordManagement statusRecordManagement;
|
||||
private final RanderToolsManager randerToolsManager = RanderToolsManager.getInstance();
|
||||
private final AtomicReference<ParametersManagement> parametersManagement = new AtomicReference<>();
|
||||
public static final float BORDER_THICKNESS = 6.0f;
|
||||
public static final float CORNER_SIZE = 12.0f;
|
||||
// ================== 摄像机控制相关字段 ==================
|
||||
@@ -90,8 +93,8 @@ public class ModelRenderPanel extends JPanel {
|
||||
this.toolManagement = new ToolManagement(this, randerToolsManager);
|
||||
|
||||
// 注册所有工具
|
||||
toolManagement.registerTool(new PuppetDeformationTool(this),new PuppetDeformationRander());
|
||||
toolManagement.registerTool(new VertexDeformationTool(this),new VertexDeformationRander());
|
||||
toolManagement.registerTool(new PuppetDeformationTool(this), new PuppetDeformationRander());
|
||||
toolManagement.registerTool(new VertexDeformationTool(this), new VertexDeformationRander());
|
||||
toolManagement.registerTool(new LiquifyTool(this), new LiquifyTargetPartRander());
|
||||
initialize();
|
||||
|
||||
@@ -103,6 +106,35 @@ public class ModelRenderPanel extends JPanel {
|
||||
doubleClickTimer.setRepeats(false);
|
||||
|
||||
modelsUpdate(getModel());
|
||||
|
||||
ParametersPanel.ParameterEventBroadcaster.getInstance().addListener(new ParametersPanel.ParameterEventListener() {
|
||||
@Override
|
||||
public void onParameterUpdated(AnimationParameter p) {
|
||||
// 1. 获取参数管理器
|
||||
ParametersManagement pm = getParametersManagement();
|
||||
if (pm == null) {
|
||||
logger.warn("ParametersManagement 未初始化,无法应用参数更新。");
|
||||
return;
|
||||
}
|
||||
final List<ModelPart> selectedParts = getSelectedParts();
|
||||
if (selectedParts.isEmpty()) {
|
||||
logger.debug("没有选中的模型部件,跳过应用参数。");
|
||||
return;
|
||||
}
|
||||
|
||||
// 必须在 GL 上下文线程中执行模型操作
|
||||
glContextManager.executeInGLContext(() -> {
|
||||
try {
|
||||
FrameInterpolator.applyFrameInterpolations(pm, selectedParts, p, logger);
|
||||
for (ModelPart selectedPart : selectedParts) {
|
||||
selectedPart.updateMeshVertices();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
logger.error("在GL上下文线程中应用关键字动画参数时出错", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,7 +216,6 @@ public class ModelRenderPanel extends JPanel {
|
||||
* 获取当前选中的网格
|
||||
*/
|
||||
public Mesh2D getSelectedMesh() {
|
||||
// 委托给工具管理系统的当前工具
|
||||
Tool currentTool = toolManagement.getCurrentTool();
|
||||
if (currentTool instanceof SelectionTool) {
|
||||
return ((SelectionTool) currentTool).getSelectedMesh();
|
||||
@@ -662,6 +693,21 @@ public class ModelRenderPanel extends JPanel {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取参数管理器
|
||||
*/
|
||||
public ParametersManagement getParametersManagement() {
|
||||
return parametersManagement.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置参数管理器
|
||||
*/
|
||||
public void setParametersManagement(ParametersManagement parametersManagement) {
|
||||
this.parametersManagement.set(parametersManagement);
|
||||
glContextManager.executeInGLContext(() -> ModelRenderPanel.this.parametersManagement.set(parametersManagement));
|
||||
}
|
||||
|
||||
public enum DragMode {
|
||||
NONE, // 无拖拽
|
||||
MOVE, // 移动部件
|
||||
|
||||
@@ -0,0 +1,605 @@
|
||||
package com.chuangzhou.vivid2D.render.awt;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.awt.tools.VertexDeformationTool;
|
||||
import com.chuangzhou.vivid2D.render.model.AnimationParameter;
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.Timer;
|
||||
import javax.swing.event.ListSelectionEvent;
|
||||
import javax.swing.event.ListSelectionListener;
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* 窗口参数管理面板(使用你给定的结构)
|
||||
* 功能:
|
||||
* - 当没有选中网格时显示“未选择网格”的占位
|
||||
* - 当选中网格时,找到对应的 ModelPart,列出其所有 AnimationParameter
|
||||
* - [!] 使用 KeyframeSlider 替换 JSlider,以显示/吸附关键帧
|
||||
* - [!] 双击参数列表项可打开 KeyframeEditorDialog
|
||||
* - 支持新增、删除、重命名、修改参数值
|
||||
* - 广播参数相关事件
|
||||
* - 按下 ESC 取消选择并广播取消事件
|
||||
*/
|
||||
public class ParametersPanel extends JPanel {
|
||||
private final ModelRenderPanel renderPanel;
|
||||
private final Model2D model;
|
||||
private AnimationParameter selectParameter;
|
||||
|
||||
// UI
|
||||
private final CardLayout cardLayout = new CardLayout();
|
||||
private final JPanel cardRoot = new JPanel(cardLayout);
|
||||
|
||||
private final DefaultListModel<AnimationParameter> listModel = new DefaultListModel<>();
|
||||
private final JList<AnimationParameter> parameterList = new JList<>(listModel);
|
||||
|
||||
private final JButton addBtn = new JButton("新建");
|
||||
private final JButton delBtn = new JButton("删除");
|
||||
private final JButton renameBtn = new JButton("重命名");
|
||||
|
||||
// --- 修改:使用 KeyframeSlider 替换 JSlider ---
|
||||
private final KeyframeSlider valueSlider = new KeyframeSlider();
|
||||
// ------------------------------------------
|
||||
|
||||
private final JLabel valueLabel = new JLabel("值: -");
|
||||
private final Timer pollTimer;
|
||||
|
||||
// 当前绑定的 ModelPart(对应选中网格)
|
||||
private volatile ModelPart currentPart = null;
|
||||
|
||||
public ParametersPanel(ModelRenderPanel renderPanel) {
|
||||
this.renderPanel = renderPanel;
|
||||
this.model = renderPanel.getModel();
|
||||
|
||||
setLayout(new BorderLayout());
|
||||
setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
|
||||
|
||||
// emptyPanel
|
||||
JPanel emptyPanel = new JPanel(new BorderLayout());
|
||||
emptyPanel.add(new JLabel("未选择网格", SwingConstants.CENTER), BorderLayout.CENTER);
|
||||
|
||||
// paramPanel 构建
|
||||
parameterList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
parameterList.setCellRenderer((list, value, index, isSelected, cellHasFocus) -> {
|
||||
JLabel l = new JLabel(value == null ? "<null>" : value.getId());
|
||||
l.setOpaque(true);
|
||||
l.setBackground(isSelected ? UIManager.getColor("List.selectionBackground") : UIManager.getColor("List.background"));
|
||||
l.setForeground(isSelected ? UIManager.getColor("List.selectionForeground") : UIManager.getColor("List.foreground"));
|
||||
return l;
|
||||
});
|
||||
|
||||
JScrollPane scroll = new JScrollPane(parameterList);
|
||||
scroll.setPreferredSize(new Dimension(260, 160));
|
||||
|
||||
JPanel topBar = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 6));
|
||||
topBar.add(addBtn);
|
||||
topBar.add(delBtn);
|
||||
topBar.add(renameBtn);
|
||||
|
||||
JPanel bottomBar = new JPanel(new BorderLayout(6, 6));
|
||||
JPanel sliderPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 6, 6));
|
||||
sliderPanel.add(new JLabel("参数值:"));
|
||||
valueSlider.setPreferredSize(new Dimension(150, 25)); // 给自定义滑块一个合适的大小
|
||||
sliderPanel.add(valueSlider);
|
||||
sliderPanel.add(valueLabel);
|
||||
bottomBar.add(sliderPanel, BorderLayout.CENTER);
|
||||
|
||||
JPanel paramPanel = new JPanel(new BorderLayout(6, 6));
|
||||
paramPanel.add(topBar, BorderLayout.NORTH);
|
||||
paramPanel.add(scroll, BorderLayout.CENTER);
|
||||
paramPanel.add(bottomBar, BorderLayout.SOUTH);
|
||||
|
||||
cardRoot.add(emptyPanel, "EMPTY");
|
||||
cardRoot.add(paramPanel, "PARAM");
|
||||
add(cardRoot, BorderLayout.CENTER);
|
||||
|
||||
// 按键 ESC 绑定:取消选择
|
||||
setupEscBinding();
|
||||
|
||||
// 事件绑定
|
||||
bindActions();
|
||||
|
||||
// 定期轮询选中网格(200ms)
|
||||
pollTimer = new Timer(200, e -> pollSelectedMesh());
|
||||
pollTimer.start();
|
||||
|
||||
// 初次展示
|
||||
updateCard();
|
||||
}
|
||||
|
||||
private void setupEscBinding() {
|
||||
InputMap im = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
|
||||
ActionMap am = getActionMap();
|
||||
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "cancelSelection");
|
||||
am.put("cancelSelection", new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
clearSelection();
|
||||
ParameterEventBroadcaster.getInstance().fireCancelEvent();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void bindActions() {
|
||||
addBtn.addActionListener(e -> onAddParameter());
|
||||
delBtn.addActionListener(e -> onDeleteParameter());
|
||||
renameBtn.addActionListener(e -> onRenameParameter());
|
||||
|
||||
parameterList.addListSelectionListener(new ListSelectionListener() {
|
||||
@Override
|
||||
public void valueChanged(ListSelectionEvent e) {
|
||||
if (!e.getValueIsAdjusting()) {
|
||||
selectParameter = parameterList.getSelectedValue();
|
||||
updateSliderForSelected();
|
||||
ParameterEventBroadcaster.getInstance().fireSelectEvent(selectParameter);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// --- 新增:双击打开关键帧编辑器 ---
|
||||
parameterList.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
if (e.getClickCount() == 2) {
|
||||
AnimationParameter p = parameterList.getSelectedValue();
|
||||
if (p != null) {
|
||||
// 弹出编辑器
|
||||
KeyframeEditorDialog.showEditor(SwingUtilities.getWindowAncestor(ParametersPanel.this), p);
|
||||
// 编辑器关闭后,刷新滑块的显示
|
||||
valueSlider.repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// --------------------------------
|
||||
valueSlider.addChangeListener(e -> {
|
||||
if (selectParameter == null) return;
|
||||
valueLabel.setText(String.format("%.3f", selectParameter.getValue()));
|
||||
ParameterEventBroadcaster.getInstance().fireUpdateEvent(selectParameter);
|
||||
markModelNeedsUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
private void pollSelectedMesh() {
|
||||
Mesh2D mesh = getSelectedMesh();
|
||||
if (mesh == null) {
|
||||
if (currentPart != null) {
|
||||
currentPart = null;
|
||||
listModel.clear();
|
||||
selectParameter = null;
|
||||
updateCard();
|
||||
}
|
||||
return;
|
||||
}
|
||||
ModelPart part = renderPanel.findPartByMesh(mesh);
|
||||
if (part == null) {
|
||||
if (currentPart != null) {
|
||||
currentPart = null;
|
||||
listModel.clear();
|
||||
selectParameter = null;
|
||||
updateCard();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (currentPart == part) return; // 未变更
|
||||
// 切换到新部件
|
||||
currentPart = part;
|
||||
loadParametersFromCurrentPart();
|
||||
updateCard();
|
||||
}
|
||||
|
||||
private void updateCard() {
|
||||
if (currentPart == null) {
|
||||
cardLayout.show(cardRoot, "EMPTY");
|
||||
addBtn.setEnabled(false);
|
||||
delBtn.setEnabled(false);
|
||||
renameBtn.setEnabled(false);
|
||||
valueSlider.setEnabled(false);
|
||||
} else {
|
||||
cardLayout.show(cardRoot, "PARAM");
|
||||
addBtn.setEnabled(true);
|
||||
delBtn.setEnabled(!listModel.isEmpty());
|
||||
renameBtn.setEnabled(!listModel.isEmpty());
|
||||
valueSlider.setEnabled(!listModel.isEmpty() && selectParameter != null);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadParametersFromCurrentPart() {
|
||||
listModel.clear();
|
||||
selectParameter = null;
|
||||
if (currentPart == null) return;
|
||||
try {
|
||||
Map<String, AnimationParameter> map = currentPart.getParameters();
|
||||
if (map != null) {
|
||||
for (AnimationParameter p : map.values()) {
|
||||
listModel.addElement(p);
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
if (!listModel.isEmpty()) {
|
||||
parameterList.setSelectedIndex(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void onAddParameter() {
|
||||
if (currentPart == null) return;
|
||||
JPanel panel = new JPanel(new GridLayout(4, 2, 4, 4));
|
||||
JTextField idField = new JTextField();
|
||||
JTextField minField = new JTextField("0.0");
|
||||
JTextField maxField = new JTextField("1.0");
|
||||
JTextField defField = new JTextField("0.0");
|
||||
panel.add(new JLabel("参数ID:"));
|
||||
panel.add(idField);
|
||||
panel.add(new JLabel("最小值:"));
|
||||
panel.add(minField);
|
||||
panel.add(new JLabel("最大值:"));
|
||||
panel.add(maxField);
|
||||
panel.add(new JLabel("默认值:"));
|
||||
panel.add(defField);
|
||||
|
||||
int res = JOptionPane.showConfirmDialog(this, panel, "新建参数", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
|
||||
if (res != JOptionPane.OK_OPTION) return;
|
||||
|
||||
String id = idField.getText().trim();
|
||||
if (id.isEmpty()) {
|
||||
JOptionPane.showMessageDialog(this, "参数ID不能为空", "错误", JOptionPane.ERROR_MESSAGE);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
float min = Float.parseFloat(minField.getText().trim());
|
||||
float max = Float.parseFloat(maxField.getText().trim());
|
||||
float def = Float.parseFloat(defField.getText().trim());
|
||||
|
||||
// 使用 ModelPart.createParameter 如果可用
|
||||
try {
|
||||
AnimationParameter newP = currentPart.createParameter(id, min, max, def);
|
||||
// 如果 createParameter 返回了对象,直接使用;否则通过 getParameter 获取
|
||||
if (newP == null) newP = currentPart.getParameter(id);
|
||||
|
||||
// 插入 UI 列表
|
||||
if (newP != null) {
|
||||
listModel.addElement(newP);
|
||||
parameterList.setSelectedValue(newP, true);
|
||||
ParameterEventBroadcaster.getInstance().fireAddEvent(newP);
|
||||
markModelNeedsUpdate();
|
||||
} else {
|
||||
JOptionPane.showMessageDialog(this, "新参数创建失败", "错误", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
} catch (NoSuchMethodError | NoClassDefFoundError ignore) {
|
||||
// 兜底:通过反射直接修改 internal map(风险自负)
|
||||
try {
|
||||
Method m = currentPart.getClass().getMethod("createParameter", String.class, float.class, float.class, float.class);
|
||||
AnimationParameter newP = (AnimationParameter) m.invoke(currentPart, id, min, max, def);
|
||||
if (newP != null) {
|
||||
listModel.addElement(newP);
|
||||
parameterList.setSelectedValue(newP, true);
|
||||
ParameterEventBroadcaster.getInstance().fireAddEvent(newP);
|
||||
markModelNeedsUpdate();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
JOptionPane.showMessageDialog(this, "创建参数失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
} catch (NumberFormatException nfe) {
|
||||
JOptionPane.showMessageDialog(this, "数值格式错误", "错误", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
updateCard();
|
||||
}
|
||||
|
||||
private void onDeleteParameter() {
|
||||
if (currentPart == null) return;
|
||||
AnimationParameter sel = parameterList.getSelectedValue();
|
||||
if (sel == null) return;
|
||||
int r = JOptionPane.showConfirmDialog(this, "确认删除参数: " + sel.getId() + " ?", "确认删除", JOptionPane.YES_NO_OPTION);
|
||||
if (r != JOptionPane.YES_OPTION) return;
|
||||
|
||||
try {
|
||||
Map<String, AnimationParameter> map = currentPart.getParameters();
|
||||
if (map != null) {
|
||||
map.remove(sel.getId());
|
||||
// 如果 ModelPart 提供删除方法可以使用之;此处直接移除
|
||||
} else {
|
||||
// 反射尝试
|
||||
Field f = currentPart.getClass().getDeclaredField("parameters");
|
||||
f.setAccessible(true);
|
||||
Object o = f.get(currentPart);
|
||||
if (o instanceof Map) {
|
||||
((Map) o).remove(sel.getId());
|
||||
}
|
||||
}
|
||||
listModel.removeElement(sel);
|
||||
selectParameter = null;
|
||||
ParameterEventBroadcaster.getInstance().fireRemoveEvent(sel);
|
||||
markModelNeedsUpdate();
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
JOptionPane.showMessageDialog(this, "删除失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
updateCard();
|
||||
}
|
||||
|
||||
private void onRenameParameter() {
|
||||
if (currentPart == null) return;
|
||||
AnimationParameter sel = parameterList.getSelectedValue();
|
||||
if (sel == null) return;
|
||||
String newId = JOptionPane.showInputDialog(this, "输入新 ID:", sel.getId());
|
||||
if (newId == null || newId.trim().isEmpty()) return;
|
||||
newId = newId.trim();
|
||||
try {
|
||||
Map<String, AnimationParameter> map = currentPart.getParameters();
|
||||
if (map != null) {
|
||||
// 创建新 entry,移除旧 entry,保留值范围和值
|
||||
AnimationParameter old = map.remove(sel.getId());
|
||||
if (old != null) {
|
||||
AnimationParameter copy = new AnimationParameter(newId, old.getMinValue(), old.getMaxValue(), old.getValue());
|
||||
// 复制关键帧
|
||||
old.getKeyframes().forEach(copy::addKeyframe);
|
||||
map.put(newId, copy);
|
||||
// 刷新 UI
|
||||
loadParametersFromCurrentPart();
|
||||
ParameterEventBroadcaster.getInstance().fireRenameEvent(old, copy);
|
||||
markModelNeedsUpdate();
|
||||
}
|
||||
} else {
|
||||
// 反射处理
|
||||
Field f = currentPart.getClass().getDeclaredField("parameters");
|
||||
f.setAccessible(true);
|
||||
Object o = f.get(currentPart);
|
||||
if (o instanceof Map) {
|
||||
Map<String, AnimationParameter> pm = (Map<String, AnimationParameter>) o;
|
||||
AnimationParameter old = pm.remove(sel.getId());
|
||||
if (old != null) {
|
||||
AnimationParameter copy = new AnimationParameter(newId, old.getMinValue(), old.getMaxValue(), old.getValue());
|
||||
// 复制关键帧
|
||||
old.getKeyframes().forEach(copy::addKeyframe);
|
||||
pm.put(newId, copy);
|
||||
loadParametersFromCurrentPart();
|
||||
ParameterEventBroadcaster.getInstance().fireRenameEvent(old, copy);
|
||||
markModelNeedsUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
JOptionPane.showMessageDialog(this, "重命名失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
updateCard();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 修改:更新滑块以显示新选中的参数。
|
||||
*/
|
||||
private void updateSliderForSelected() {
|
||||
AnimationParameter p = selectParameter;
|
||||
if (p == null) {
|
||||
valueSlider.setEnabled(false);
|
||||
valueSlider.setParameter(null); // 清空滑块的参数
|
||||
valueLabel.setText("值: -");
|
||||
} else {
|
||||
valueSlider.setEnabled(true);
|
||||
valueSlider.setParameter(p); // 将参数设置给自定义滑块
|
||||
valueLabel.setText(String.format("%.3f", p.getValue()));
|
||||
}
|
||||
valueSlider.repaint();
|
||||
}
|
||||
|
||||
private void setParameterValue(AnimationParameter param, float value) {
|
||||
if (param == null) return;
|
||||
// 先尝试 param.setValue
|
||||
try {
|
||||
Method m = param.getClass().getMethod("setValue", float.class);
|
||||
m.invoke(param, value);
|
||||
return;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
// 兜底:反射写字段
|
||||
try {
|
||||
Field f = param.getClass().getDeclaredField("value");
|
||||
f.setAccessible(true);
|
||||
f.setFloat(param, value);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
// 如果 ModelPart 有 setParameterValue 方法,调用之以标记 dirty
|
||||
if (currentPart != null) {
|
||||
try {
|
||||
Method m2 = currentPart.getClass().getMethod("setParameterValue", String.class, float.class);
|
||||
m2.invoke(currentPart, param.getId(), value);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clearSelection() {
|
||||
parameterList.clearSelection();
|
||||
selectParameter = null;
|
||||
updateSliderForSelected();
|
||||
}
|
||||
|
||||
/**
|
||||
* 外部可调用,获取当前选中的网格(基于 renderPanel)
|
||||
*/
|
||||
private Mesh2D getSelectedMesh() {
|
||||
if (renderPanel.getSelectedMesh() == null
|
||||
&& renderPanel.getToolManagement().getCurrentTool() instanceof VertexDeformationTool){
|
||||
return ((VertexDeformationTool) renderPanel.getToolManagement().getCurrentTool()).getTargetMesh();
|
||||
}
|
||||
return renderPanel.getSelectedMesh();
|
||||
}
|
||||
|
||||
public AnimationParameter getSelectParameter() {
|
||||
return selectParameter;
|
||||
}
|
||||
|
||||
private void markModelNeedsUpdate() {
|
||||
try {
|
||||
if (model == null) return;
|
||||
Method m = model.getClass().getMethod("markNeedsUpdate");
|
||||
m.invoke(model);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
if (pollTimer != null) pollTimer.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前选中参数上被“选中”的关键帧。
|
||||
* “选中”定义为:滑块的当前值正好(或非常接近)一个关键帧的值。
|
||||
*
|
||||
* @param isPreciseCheck 如果为 true,则只有当 currentValue 几乎精确等于关键帧值时才返回;
|
||||
* 否则允许在 epsilon 阈值内的吸附。
|
||||
* @return 如果当前值命中了关键帧,则返回该帧的值;否则返回 null。
|
||||
*/
|
||||
public Float getSelectedKeyframe(boolean isPreciseCheck) {
|
||||
if (selectParameter == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
float currentValue = selectParameter.getValue();
|
||||
float range = selectParameter.getMaxValue() - selectParameter.getMinValue();
|
||||
if (range <= 0) return null;
|
||||
|
||||
// 设置吸附/命中阈值,例如范围的 0.5%
|
||||
// 注意:这个阈值应该和 KeyframeSlider 中的吸附逻辑保持一致
|
||||
float epsilon = range * 0.005f;
|
||||
// 用于判断浮点数是否"相等"的极小值
|
||||
final float EQUALITY_TOLERANCE = 1e-5f;
|
||||
|
||||
// 1. 检查是否有精确匹配的关键帧
|
||||
Float nearest = selectParameter.getNearestKeyframe(currentValue, epsilon);
|
||||
if (nearest != null) {
|
||||
// 检查是否在吸附阈值内 (旧逻辑)
|
||||
if (Math.abs(currentValue - nearest) <= epsilon) {
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// 2. 新增逻辑:精确检查判断
|
||||
if (isPreciseCheck) {
|
||||
// 如果要求精确检查,则只有当它们几乎相等时才返回
|
||||
if (Math.abs(currentValue - nearest) <= EQUALITY_TOLERANCE) {
|
||||
return nearest;
|
||||
} else {
|
||||
// 如果不相等,则不认为是“选中”的关键帧
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// -------------------------------------------------------------
|
||||
|
||||
// 3. 原有吸附逻辑 (仅在非精确检查时执行,或精确检查通过时执行)
|
||||
// 如果差值大于 EQUALITY_TOLERANCE,说明发生了吸附,需要更新参数值
|
||||
if (Math.abs(currentValue - nearest) > EQUALITY_TOLERANCE) {
|
||||
setParameterValue(selectParameter, nearest);
|
||||
valueLabel.setText(String.format("%.3f", nearest));
|
||||
ParameterEventBroadcaster.getInstance().fireUpdateEvent(selectParameter);
|
||||
}
|
||||
|
||||
// 返回吸附后的值 (或精确匹配的值)
|
||||
return nearest;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
// =================== 简单事件广播器与监听器 (未修改) ===================
|
||||
|
||||
public interface ParameterEventListener {
|
||||
default void onParameterAdded(AnimationParameter p) {}
|
||||
default void onParameterRemoved(AnimationParameter p) {}
|
||||
default void onParameterUpdated(AnimationParameter p) {}
|
||||
default void onParameterRenamed(AnimationParameter oldP, AnimationParameter newP) {}
|
||||
default void onParameterSelected(AnimationParameter p) {}
|
||||
default void onCancelSelection() {}
|
||||
}
|
||||
|
||||
public static class ParameterEventBroadcaster {
|
||||
private static final ParameterEventBroadcaster INSTANCE = new ParameterEventBroadcaster();
|
||||
private final List<ParameterEventListener> listeners = Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
public static ParameterEventBroadcaster getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public void addListener(ParameterEventListener l) {
|
||||
if (l == null) return;
|
||||
listeners.add(l);
|
||||
}
|
||||
|
||||
public void removeListener(ParameterEventListener l) {
|
||||
listeners.remove(l);
|
||||
}
|
||||
|
||||
public void fireAddEvent(AnimationParameter p) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
synchronized (listeners) {
|
||||
for (ParameterEventListener l : new ArrayList<>(listeners)) {
|
||||
try { l.onParameterAdded(p); } catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void fireRemoveEvent(AnimationParameter p) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
synchronized (listeners) {
|
||||
for (ParameterEventListener l : new ArrayList<>(listeners)) {
|
||||
try { l.onParameterRemoved(p); } catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void fireUpdateEvent(AnimationParameter p) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
synchronized (listeners) {
|
||||
for (ParameterEventListener l : new ArrayList<>(listeners)) {
|
||||
try { l.onParameterUpdated(p); } catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void fireRenameEvent(AnimationParameter oldP, AnimationParameter newP) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
synchronized (listeners) {
|
||||
for (ParameterEventListener l : new ArrayList<>(listeners)) {
|
||||
try { l.onParameterRenamed(oldP, newP); } catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void fireSelectEvent(AnimationParameter p) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
synchronized (listeners) {
|
||||
for (ParameterEventListener l : new ArrayList<>(listeners)) {
|
||||
try { l.onParameterSelected(p); } catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void fireCancelEvent() {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
synchronized (listeners) {
|
||||
for (ParameterEventListener l : new ArrayList<>(listeners)) {
|
||||
try { l.onCancelSelection(); } catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package com.chuangzhou.vivid2D.render.awt.manager;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.awt.ParametersPanel;
|
||||
import com.chuangzhou.vivid2D.render.model.AnimationParameter;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ParametersManagement {
|
||||
private final ParametersPanel parametersPanel;
|
||||
private final List<Parameter> oldValues = new ArrayList<>();
|
||||
|
||||
public ParametersManagement(ParametersPanel parametersPanel) {
|
||||
this.parametersPanel = parametersPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取ModelPart的所有参数
|
||||
* @param modelPart 部件
|
||||
* @return 该部件的所有参数
|
||||
*/
|
||||
public Parameter getModelPartParameters(ModelPart modelPart) {
|
||||
for (Parameter parameter : oldValues) {
|
||||
if (parameter.modelPart().equals(modelPart)) {
|
||||
return parameter;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前选中的帧
|
||||
* position List.of(float modelX, float modelY)
|
||||
* rotate float modelAngle
|
||||
* @param isPreciseCheck 是否精确检查
|
||||
* @return 当前选中的帧
|
||||
*/
|
||||
public Float getSelectedKeyframe(boolean isPreciseCheck) {
|
||||
return parametersPanel.getSelectedKeyframe(isPreciseCheck);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前选中的参数
|
||||
* @return 当前选中的参数
|
||||
*/
|
||||
public AnimationParameter getSelectParameter() {
|
||||
if (parametersPanel.getSelectParameter() == null){
|
||||
// System.out.println("getSelectParameter() is null");
|
||||
return null;
|
||||
}
|
||||
return parametersPanel.getSelectParameter().copy();
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听参数变化 (强制添加新记录,即使 paramId 已存在)
|
||||
* 如果列表中已存在相同 modelPart 的记录,则添加新参数到该记录的列表尾部;否则添加新记录。
|
||||
* @param modelPart 变化的部件
|
||||
* @param paramId 参数id
|
||||
* @param value 最终值
|
||||
*/
|
||||
public void broadcast(ModelPart modelPart, String paramId, Object value) {
|
||||
if (getSelectParameter() == null){
|
||||
return;
|
||||
}
|
||||
boolean isKeyframe = getSelectedKeyframe(false) != null;
|
||||
Float currentKeyframe = getSelectedKeyframe(false);
|
||||
|
||||
// 查找是否已存在该ModelPart的记录
|
||||
for (int i = 0; i < oldValues.size(); i++) {
|
||||
Parameter existingParameter = oldValues.get(i);
|
||||
if (existingParameter.modelPart().equals(modelPart)) {
|
||||
// 更新现有记录(复制所有列表以确保记录的不可变性)
|
||||
List<AnimationParameter> newAnimationParameters = new ArrayList<>(existingParameter.animationParameter()); // NEW
|
||||
List<String> newParamIds = new ArrayList<>(existingParameter.paramId());
|
||||
List<Object> newValues = new ArrayList<>(existingParameter.value());
|
||||
List<Float> newKeyframes = new ArrayList<>(existingParameter.keyframe());
|
||||
List<Boolean> newIsKeyframes = new ArrayList<>(existingParameter.isKeyframe());
|
||||
|
||||
newAnimationParameters.add(getSelectParameter());
|
||||
newParamIds.add(paramId);
|
||||
newValues.add(value);
|
||||
newKeyframes.add(currentKeyframe);
|
||||
newIsKeyframes.add(isKeyframe);
|
||||
|
||||
Parameter updatedParameter = new Parameter(modelPart, newAnimationParameters, newParamIds, newValues, newKeyframes, newIsKeyframes); // NEW
|
||||
oldValues.set(i, updatedParameter);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到现有记录,创建新记录
|
||||
Parameter parameter = new Parameter(
|
||||
modelPart,
|
||||
java.util.Collections.singletonList(getSelectParameter()), // NEW
|
||||
java.util.Collections.singletonList(paramId),
|
||||
java.util.Collections.singletonList(value),
|
||||
java.util.Collections.singletonList(currentKeyframe),
|
||||
java.util.Collections.singletonList(isKeyframe)
|
||||
);
|
||||
oldValues.add(parameter);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除特定参数
|
||||
* @param modelPart 部件
|
||||
* @param paramId 参数id
|
||||
*/
|
||||
public void removeParameter(ModelPart modelPart, String paramId) {
|
||||
for (int i = 0; i < oldValues.size(); i++) {
|
||||
Parameter existingParameter = oldValues.get(i);
|
||||
if (existingParameter.modelPart().equals(modelPart)) {
|
||||
List<AnimationParameter> newAnimationParameters = new ArrayList<>(existingParameter.animationParameter());
|
||||
List<String> newParamIds = new ArrayList<>(existingParameter.paramId());
|
||||
List<Object> newValues = new ArrayList<>(existingParameter.value());
|
||||
List<Float> newKeyframes = new ArrayList<>(existingParameter.keyframe());
|
||||
List<Boolean> newIsKeyframes = new ArrayList<>(existingParameter.isKeyframe());
|
||||
|
||||
int paramIndex = newParamIds.indexOf(paramId);
|
||||
if (paramIndex != -1) {
|
||||
newAnimationParameters.remove(paramIndex); // NEW
|
||||
newParamIds.remove(paramIndex);
|
||||
newValues.remove(paramIndex);
|
||||
newKeyframes.remove(paramIndex);
|
||||
newIsKeyframes.remove(paramIndex);
|
||||
|
||||
if (newParamIds.isEmpty()) {
|
||||
oldValues.remove(i);
|
||||
} else {
|
||||
// 更新记录
|
||||
Parameter updatedParameter = new Parameter(
|
||||
existingParameter.modelPart(),
|
||||
newAnimationParameters,
|
||||
newParamIds,
|
||||
newValues,
|
||||
newKeyframes,
|
||||
newIsKeyframes
|
||||
);
|
||||
oldValues.set(i, updatedParameter);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取参数值 (返回 ModelPart 的所有参数的防御性副本)
|
||||
* @param modelPart 部件
|
||||
* @param paramId 参数id (该参数在此方法中将被忽略,因为返回的是所有参数)
|
||||
* @return 该部件所有参数的 Parameter 记录的副本
|
||||
*/
|
||||
public Parameter getValue(ModelPart modelPart, String paramId) {
|
||||
for (Parameter parameter : oldValues) {
|
||||
if (parameter.modelPart().equals(modelPart)) {
|
||||
List<Integer> indices = new ArrayList<>();
|
||||
for (int i = 0; i < parameter.paramId().size(); i++) {
|
||||
if (parameter.paramId().get(i).equals(paramId)) indices.add(i);
|
||||
}
|
||||
if (indices.isEmpty()) return null;
|
||||
List<AnimationParameter> anims = new ArrayList<>();
|
||||
List<String> ids = new ArrayList<>();
|
||||
List<Object> values = new ArrayList<>();
|
||||
List<Float> keyframes = new ArrayList<>();
|
||||
List<Boolean> isKeyframes = new ArrayList<>();
|
||||
for (int idx : indices) {
|
||||
anims.add(parameter.animationParameter().get(idx));
|
||||
ids.add(parameter.paramId().get(idx));
|
||||
values.add(parameter.value().get(idx));
|
||||
keyframes.add(parameter.keyframe().get(idx));
|
||||
isKeyframes.add(parameter.isKeyframe().get(idx));
|
||||
}
|
||||
return new Parameter(parameter.modelPart(), anims, ids, values, keyframes, isKeyframes);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public record Parameter(
|
||||
ModelPart modelPart,
|
||||
List<AnimationParameter> animationParameter,
|
||||
List<String> paramId,
|
||||
List<Object> value,
|
||||
List<Float> keyframe,
|
||||
List<Boolean> isKeyframe
|
||||
) {
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String partName = (modelPart != null) ? modelPart.getName() : "[NULL ModelPart]";
|
||||
sb.append("Parameter[Part=").append(partName).append(", ");
|
||||
sb.append("Details=[");
|
||||
int size = paramId.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
String id = paramId.get(i);
|
||||
Object val = (value != null && value.size() > i) ? value.get(i) : null;
|
||||
Float kf = (keyframe != null && keyframe.size() > i) ? keyframe.get(i) : null;
|
||||
Boolean isKf = (isKeyframe != null && isKeyframe.size() > i) ? isKeyframe.get(i) : false;
|
||||
if (i > 0) {
|
||||
sb.append("; ");
|
||||
}
|
||||
sb.append(String.format("{ID=%s, V=%s, KF=%s, IsKF=%b}",
|
||||
id,
|
||||
String.valueOf(val),
|
||||
kf != null ? String.valueOf(kf) : "null",
|
||||
isKf));
|
||||
}
|
||||
sb.append("]]");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("ParametersManagement State:\n");
|
||||
|
||||
if (oldValues.isEmpty()) {
|
||||
sb.append(" No recorded parameters (oldValues is empty).\n");
|
||||
} else {
|
||||
for (int i = 0; i < oldValues.size(); i++) {
|
||||
Parameter p = oldValues.get(i);
|
||||
sb.append(String.format(" --- Record %d ---\n", i));
|
||||
String partName;
|
||||
if (p.modelPart() != null) {
|
||||
partName = p.modelPart().getName();
|
||||
} else {
|
||||
partName = "[NULL]";
|
||||
}
|
||||
sb.append(String.format(" ModelPart: Part: %s\n", partName));
|
||||
int numParams = p.paramId().size();
|
||||
for (int j = 0; j < numParams; j++) {
|
||||
String id = p.paramId().get(j);
|
||||
Object val = (p.value() != null && p.value().size() > j) ? p.value().get(j) : "[MISSING_VALUE]";
|
||||
Float kf = (p.keyframe() != null && p.keyframe().size() > j) ? p.keyframe().get(j) : null;
|
||||
Boolean isKf = (p.isKeyframe() != null && p.isKeyframe().size() > j) ? p.isKeyframe().get(j) : false;
|
||||
sb.append(String.format(" - Param ID: %s, Value: %s, Keyframe: %s, IsKeyframe: %b\n",
|
||||
id,
|
||||
val != null ? String.valueOf(val) : "[NULL_VALUE]",
|
||||
kf != null ? String.valueOf(kf) : "[NULL_KEYFRAME]",
|
||||
isKf));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.chuangzhou.vivid2D.render.awt.tools;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel;
|
||||
import com.chuangzhou.vivid2D.render.awt.manager.ParametersManagement;
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
@@ -318,15 +319,14 @@ public class SelectionTool extends Tool {
|
||||
|
||||
float deltaX = modelX - dragStartX;
|
||||
float deltaY = modelY - dragStartY;
|
||||
|
||||
// 移动所有选中的部件
|
||||
List<ModelPart> selectedParts = getSelectedParts();
|
||||
for (ModelPart part : selectedParts) {
|
||||
Vector2f pos = part.getPosition();
|
||||
part.setPosition(pos.x + deltaX, pos.y + deltaY);
|
||||
Vector2f startPos = dragStartPositions.getOrDefault(part, new Vector2f(part.getPosition()));
|
||||
float newX = startPos.x + deltaX;
|
||||
float newY = startPos.y + deltaY;
|
||||
part.setPosition(newX, newY);
|
||||
renderPanel.getParametersManagement().broadcast(part, "position", List.of(newX, newY));
|
||||
}
|
||||
|
||||
// 更新拖拽起始位置
|
||||
dragStartX = modelX;
|
||||
dragStartY = modelY;
|
||||
}
|
||||
@@ -336,29 +336,30 @@ public class SelectionTool extends Tool {
|
||||
*/
|
||||
private void handleRotateDrag(float modelX, float modelY) {
|
||||
if (lastSelectedMesh == null) return;
|
||||
|
||||
// 计算当前角度
|
||||
float currentAngle = (float) Math.atan2(
|
||||
modelY - renderPanel.getCameraManagement().getRotationCenter().y,
|
||||
modelX - renderPanel.getCameraManagement().getRotationCenter().x
|
||||
);
|
||||
|
||||
// 计算旋转增量
|
||||
float deltaAngle = currentAngle - rotationStartAngle;
|
||||
|
||||
// 如果按住Shift键,以15度为步长进行约束旋转
|
||||
if (renderPanel.getKeyboardManager().getIsShiftPressed() || shiftDuringDrag) {
|
||||
float constraintStep = (float) (Math.PI / 12); // 15度
|
||||
deltaAngle = Math.round(deltaAngle / constraintStep) * constraintStep;
|
||||
}
|
||||
|
||||
// 应用旋转到所有选中的部件
|
||||
List<ModelPart> selectedParts = getSelectedParts();
|
||||
for (ModelPart part : selectedParts) {
|
||||
part.rotate(deltaAngle);
|
||||
float startAngle = dragStartRotations.getOrDefault(part, part.getRotation());
|
||||
float targetAngle = startAngle + deltaAngle;
|
||||
try {
|
||||
part.getClass().getMethod("setRotation", float.class).invoke(part, targetAngle);
|
||||
} catch (NoSuchMethodException nsme) {
|
||||
float cur = part.getRotation();
|
||||
float delta = targetAngle - cur;
|
||||
part.rotate(delta);
|
||||
} catch (Exception ignored) {
|
||||
part.rotate(deltaAngle);
|
||||
}
|
||||
renderPanel.getParametersManagement().broadcast(part, "rotate", part.getRotation());
|
||||
}
|
||||
|
||||
// 更新旋转起始角度
|
||||
rotationStartAngle = currentAngle;
|
||||
}
|
||||
|
||||
@@ -378,7 +379,7 @@ public class SelectionTool extends Tool {
|
||||
Vector2f currentPivot = selectedPart.getPivot();
|
||||
float newPivotX = currentPivot.x + deltaX;
|
||||
float newPivotY = currentPivot.y + deltaY;
|
||||
|
||||
renderPanel.getParametersManagement().broadcast(selectedPart, "pivot", List.of(newPivotX, newPivotY));
|
||||
if (selectedPart.setPivot(newPivotX, newPivotY)) {
|
||||
dragStartX = modelX;
|
||||
dragStartY = modelY;
|
||||
@@ -437,19 +438,18 @@ public class SelectionTool extends Tool {
|
||||
}
|
||||
|
||||
List<ModelPart> selectedParts = getSelectedParts();
|
||||
Vector2f center = getMultiSelectionCenter(); // 整个多选的中心点
|
||||
|
||||
// 使用 dragStartScales 中记录的初始缩放来避免累积误差
|
||||
for (ModelPart part : selectedParts) {
|
||||
Vector2f currentScale = part.getScale();
|
||||
float newScaleX = currentScale.x * relScaleX;
|
||||
float newScaleY = currentScale.y * relScaleY;
|
||||
Vector2f startScale = dragStartScales.getOrDefault(part, new Vector2f(part.getScale()));
|
||||
float newScaleX = startScale.x * relScaleX;
|
||||
float newScaleY = startScale.y * relScaleY;
|
||||
|
||||
// 更新部件自身缩放
|
||||
// 更新部件自身缩放(先应用再广播绝对值)
|
||||
part.setScale(newScaleX, newScaleY);
|
||||
renderPanel.getParametersManagement().broadcast(part, "scale", List.of(newScaleX, newScaleY));
|
||||
}
|
||||
|
||||
|
||||
// 更新拖拽起始点和尺寸
|
||||
// 更新拖拽起始点和尺寸(保持与开始状态的相对关系)
|
||||
dragStartX = modelX;
|
||||
dragStartY = modelY;
|
||||
resizeStartWidth *= relScaleX;
|
||||
|
||||
@@ -14,6 +14,7 @@ import java.awt.*;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 顶点变形工具
|
||||
@@ -32,6 +33,7 @@ public class VertexDeformationTool extends Tool {
|
||||
private float savedCameraRotation = Float.NaN;
|
||||
private Vector2f savedCameraScale = new Vector2f(1,1);
|
||||
private boolean cameraStateSaved = false;
|
||||
|
||||
public VertexDeformationTool(ModelRenderPanel renderPanel) {
|
||||
super(renderPanel, "顶点变形工具", "通过二级顶点对网格进行精细变形操作");
|
||||
}
|
||||
@@ -52,26 +54,39 @@ public class VertexDeformationTool extends Tool {
|
||||
|
||||
// 记录并重置相机(如果可用)到默认状态:旋转 = 0,缩放 = 1
|
||||
try {
|
||||
if (renderPanel.getCameraManagement() != null) {
|
||||
if (renderPanel.getCameraManagement() != null && targetMesh != null && targetMesh.getModelPart() != null) {
|
||||
// 备份
|
||||
savedCameraRotation = targetMesh.getModelPart().getRotation();
|
||||
savedCameraScale = targetMesh.getModelPart().getScale();
|
||||
savedCameraScale = new Vector2f(targetMesh.getModelPart().getScale().x, targetMesh.getModelPart().getScale().y);
|
||||
cameraStateSaved = true;
|
||||
|
||||
// 设置为默认
|
||||
targetMesh.getModelPart().setRotation(0f);
|
||||
targetMesh.getModelPart().setScale(1f);
|
||||
// 设置为默认(在 GL 线程中执行变更)
|
||||
renderPanel.getGlContextManager().executeInGLContext(() -> {
|
||||
try {
|
||||
targetMesh.getModelPart().setRotation(0f);
|
||||
targetMesh.getModelPart().setScale(1f);
|
||||
targetMesh.getModelPart().updateMeshVertices();
|
||||
} catch (Throwable t) {
|
||||
logger.debug("设置相机/部件默认状态时失败: {}", t.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
// 若没有这些方法或发生异常则记录但不阻塞工具激活
|
||||
logger.debug("无法备份/设置相机状态: {}", t.getMessage());
|
||||
}
|
||||
|
||||
if (targetMesh != null) {
|
||||
// 显示二级顶点
|
||||
associatedRanderTools.setAlgorithmEnabled("showSecondaryVertices", true);
|
||||
targetMesh.setShowSecondaryVertices(true);
|
||||
targetMesh.setRenderVertices(true);
|
||||
renderPanel.getGlContextManager().executeInGLContext(() -> {
|
||||
try {
|
||||
targetMesh.setShowSecondaryVertices(true);
|
||||
targetMesh.setRenderVertices(true);
|
||||
targetMesh.updateBounds();
|
||||
} catch (Throwable t) {
|
||||
logger.debug("激活顶点显示失败: {}", t.getMessage());
|
||||
}
|
||||
});
|
||||
logger.info("激活顶点变形工具: {}", targetMesh.getName());
|
||||
} else {
|
||||
logger.warn("没有找到可用的网格用于顶点变形");
|
||||
@@ -86,9 +101,16 @@ public class VertexDeformationTool extends Tool {
|
||||
|
||||
// 恢复相机之前的旋转/缩放状态(如果已保存)
|
||||
try {
|
||||
if (cameraStateSaved && renderPanel.getCameraManagement() != null) {
|
||||
targetMesh.getModelPart().setRotation(savedCameraRotation);
|
||||
targetMesh.getModelPart().setScale(savedCameraScale);
|
||||
if (cameraStateSaved && renderPanel.getCameraManagement() != null && targetMesh != null && targetMesh.getModelPart() != null) {
|
||||
renderPanel.getGlContextManager().executeInGLContext(() -> {
|
||||
try {
|
||||
targetMesh.getModelPart().setRotation(savedCameraRotation);
|
||||
targetMesh.getModelPart().setScale(savedCameraScale);
|
||||
targetMesh.getModelPart().updateMeshVertices();
|
||||
} catch (Throwable t) {
|
||||
logger.debug("恢复相机/部件状态失败: {}", t.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
logger.debug("无法恢复相机状态: {}", t.getMessage());
|
||||
@@ -100,9 +122,19 @@ public class VertexDeformationTool extends Tool {
|
||||
|
||||
if (targetMesh != null) {
|
||||
associatedRanderTools.setAlgorithmEnabled("showSecondaryVertices", false);
|
||||
targetMesh.setShowSecondaryVertices(false);
|
||||
targetMesh.setRenderVertices(false);
|
||||
targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition());
|
||||
renderPanel.getGlContextManager().executeInGLContext(() -> {
|
||||
try {
|
||||
targetMesh.setShowSecondaryVertices(false);
|
||||
targetMesh.setRenderVertices(false);
|
||||
// 标记脏,触发必要的刷新
|
||||
if (targetMesh.getModelPart() != null) {
|
||||
targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition());
|
||||
targetMesh.getModelPart().updateMeshVertices();
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
logger.debug("停用时清理失败: {}", t.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
targetMesh = null;
|
||||
selectedVertex = null;
|
||||
@@ -116,32 +148,38 @@ public class VertexDeformationTool extends Tool {
|
||||
public void onMousePressed(MouseEvent e, float modelX, float modelY) {
|
||||
if (!isActive || targetMesh == null) return;
|
||||
|
||||
// 选择二级顶点
|
||||
SecondaryVertex clickedVertex = findSecondaryVertexAtPosition(modelX, modelY);
|
||||
if (clickedVertex != null) {
|
||||
targetMesh.setSelectedSecondaryVertex(clickedVertex);
|
||||
selectedVertex = clickedVertex;
|
||||
// 选择二级顶点(select 操作不需要 GL 线程来 read,但为一致性在GL线程处理选择标记)
|
||||
renderPanel.getGlContextManager().executeInGLContext(() -> {
|
||||
try {
|
||||
SecondaryVertex clickedVertex = findSecondaryVertexAtPosition(modelX, modelY);
|
||||
if (clickedVertex != null) {
|
||||
targetMesh.setSelectedSecondaryVertex(clickedVertex);
|
||||
selectedVertex = clickedVertex;
|
||||
|
||||
// 开始拖拽
|
||||
currentDragMode = ModelRenderPanel.DragMode.MOVE_SECONDARY_VERTEX;
|
||||
dragStartX = modelX;
|
||||
dragStartY = modelY;
|
||||
// 开始拖拽
|
||||
currentDragMode = ModelRenderPanel.DragMode.MOVE_SECONDARY_VERTEX;
|
||||
dragStartX = modelX;
|
||||
dragStartY = modelY;
|
||||
|
||||
logger.debug("开始移动二级顶点: ID={}, 位置({}, {})",
|
||||
clickedVertex.getId(), modelX, modelY);
|
||||
} else {
|
||||
// 点击空白处,取消选择
|
||||
targetMesh.setSelectedSecondaryVertex(null);
|
||||
selectedVertex = null;
|
||||
currentDragMode = ModelRenderPanel.DragMode.NONE;
|
||||
}
|
||||
logger.debug("开始移动二级顶点: ID={}, 位置({}, {})",
|
||||
clickedVertex.getId(), modelX, modelY);
|
||||
} else {
|
||||
// 点击空白处,取消选择
|
||||
targetMesh.setSelectedSecondaryVertex(null);
|
||||
selectedVertex = null;
|
||||
currentDragMode = ModelRenderPanel.DragMode.NONE;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
logger.error("onMousePressed (VertexDeformationTool) 处理失败", t);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMouseReleased(MouseEvent e, float modelX, float modelY) {
|
||||
if (!isActive) return;
|
||||
|
||||
// 记录操作历史
|
||||
// 记录操作历史(可在这里添加撤销记录)
|
||||
if (currentDragMode == ModelRenderPanel.DragMode.MOVE_SECONDARY_VERTEX && selectedVertex != null) {
|
||||
logger.debug("完成移动二级顶点: ID={}", selectedVertex.getId());
|
||||
}
|
||||
@@ -154,19 +192,43 @@ public class VertexDeformationTool extends Tool {
|
||||
if (!isActive || selectedVertex == null) return;
|
||||
|
||||
if (currentDragMode == ModelRenderPanel.DragMode.MOVE_SECONDARY_VERTEX) {
|
||||
// 在 GL 线程中修改顶点与部件状态,保持线程安全与渲染同步
|
||||
renderPanel.getGlContextManager().executeInGLContext(() -> {
|
||||
try {
|
||||
// 移动顶点到新位置
|
||||
selectedVertex.setPosition(modelX, modelY);
|
||||
|
||||
// 移动顶点到新位置
|
||||
selectedVertex.setPosition(modelX, modelY);
|
||||
// 广播:secondaryVertex -> { id, pos:[x,y] }
|
||||
try {
|
||||
if (targetMesh != null && targetMesh.getModelPart() != null) {
|
||||
Map<String, Object> payload = Map.of(
|
||||
"id", selectedVertex.getId(),
|
||||
"pos", List.of(modelX, modelY)
|
||||
);
|
||||
renderPanel.getParametersManagement().broadcast(targetMesh.getModelPart(), "secondaryVertex", payload);
|
||||
//logger.info("广播 secondaryVertex: {}", payload);
|
||||
}
|
||||
} catch (Throwable bx) {
|
||||
logger.debug("广播 secondaryVertex 失败: {}", bx.getMessage());
|
||||
}
|
||||
|
||||
// 更新拖拽起始位置
|
||||
dragStartX = modelX;
|
||||
dragStartY = modelY;
|
||||
// 更新拖拽起始位置
|
||||
dragStartX = modelX;
|
||||
dragStartY = modelY;
|
||||
|
||||
// 标记网格为脏状态,需要重新计算边界等
|
||||
targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition());
|
||||
|
||||
// 强制重绘
|
||||
renderPanel.repaint();
|
||||
// 标记网格为脏状态,需要重新计算边界等
|
||||
if (targetMesh != null && targetMesh.getModelPart() != null) {
|
||||
targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition());
|
||||
targetMesh.updateBounds();
|
||||
targetMesh.getModelPart().updateMeshVertices();
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
logger.error("onMouseDragged (VertexDeformationTool) 处理失败", t);
|
||||
} finally {
|
||||
// 请求 UI 重绘(在 UI 线程)
|
||||
renderPanel.repaint();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,13 +236,13 @@ public class VertexDeformationTool extends Tool {
|
||||
public void onMouseMoved(MouseEvent e, float modelX, float modelY) {
|
||||
if (!isActive || targetMesh == null) return;
|
||||
|
||||
// 更新悬停的二级顶点
|
||||
// 更新悬停的二级顶点(仅读取,不进行写入) —— 在主线程做轻量检测(容忍略微延迟)
|
||||
SecondaryVertex newHoveredVertex = findSecondaryVertexAtPosition(modelX, modelY);
|
||||
|
||||
if (newHoveredVertex != hoveredVertex) {
|
||||
hoveredVertex = newHoveredVertex;
|
||||
|
||||
// 更新光标
|
||||
// 更新光标(在 UI 线程)
|
||||
if (hoveredVertex != null) {
|
||||
renderPanel.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
||||
} else {
|
||||
@@ -195,8 +257,7 @@ public class VertexDeformationTool extends Tool {
|
||||
|
||||
// 如果点击了空白处且没有顶点被选中,可以创建新顶点
|
||||
if (selectedVertex == null && findSecondaryVertexAtPosition(modelX, modelY) == null) {
|
||||
// 这里可以选择是否允许通过单击创建顶点
|
||||
// createSecondaryVertexAt(modelX, modelY);
|
||||
// 这里选择不在单击时自动创建顶点,保留为可选功能
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,15 +265,23 @@ public class VertexDeformationTool extends Tool {
|
||||
public void onMouseDoubleClicked(MouseEvent e, float modelX, float modelY) {
|
||||
if (!isActive || targetMesh == null) return;
|
||||
|
||||
// 检查是否双击了二级顶点
|
||||
SecondaryVertex clickedVertex = findSecondaryVertexAtPosition(modelX, modelY);
|
||||
if (clickedVertex != null) {
|
||||
// 双击二级顶点:删除该顶点
|
||||
deleteSecondaryVertex(clickedVertex);
|
||||
} else {
|
||||
// 双击空白处:创建新的二级顶点
|
||||
createSecondaryVertexAt(modelX, modelY);
|
||||
}
|
||||
// 双击需要修改模型,放到 GL 线程中
|
||||
renderPanel.getGlContextManager().executeInGLContext(() -> {
|
||||
try {
|
||||
SecondaryVertex clickedVertex = findSecondaryVertexAtPosition(modelX, modelY);
|
||||
if (clickedVertex != null) {
|
||||
// 双击二级顶点:删除该顶点
|
||||
deleteSecondaryVertex(clickedVertex);
|
||||
} else {
|
||||
// 双击空白处:创建新的二级顶点
|
||||
createSecondaryVertexAt(modelX, modelY);
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
logger.error("onMouseDoubleClicked (VertexDeformationTool) 处理失败", t);
|
||||
} finally {
|
||||
renderPanel.repaint();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -253,31 +322,51 @@ public class VertexDeformationTool extends Tool {
|
||||
private void createSecondaryVertexAt(float x, float y) {
|
||||
if (targetMesh == null) return;
|
||||
|
||||
// 确保边界框是最新的
|
||||
targetMesh.updateBounds();
|
||||
BoundingBox bounds = targetMesh.getBounds();
|
||||
if (bounds == null || !bounds.isValid()) {
|
||||
logger.warn("无法创建二级顶点:边界框无效");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 确保边界框是最新的
|
||||
targetMesh.updateBounds();
|
||||
BoundingBox bounds = targetMesh.getBounds();
|
||||
if (bounds == null || !bounds.isValid()) {
|
||||
logger.warn("无法创建二级顶点:边界框无效");
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算UV坐标(基于边界框)
|
||||
float u = (x - bounds.getMinX()) / bounds.getWidth();
|
||||
float v = (y - bounds.getMinY()) / bounds.getHeight();
|
||||
// 计算UV坐标(基于边界框)
|
||||
float u = (x - bounds.getMinX()) / bounds.getWidth();
|
||||
float v = (y - bounds.getMinY()) / bounds.getHeight();
|
||||
|
||||
// 限制UV在0-1范围内
|
||||
u = Math.max(0.0f, Math.min(1.0f, u));
|
||||
v = Math.max(0.0f, Math.min(1.0f, v));
|
||||
// 限制UV在0-1范围内
|
||||
u = Math.max(0.0f, Math.min(1.0f, u));
|
||||
v = Math.max(0.0f, Math.min(1.0f, v));
|
||||
|
||||
SecondaryVertex newVertex = targetMesh.addSecondaryVertex(x, y, u, v);
|
||||
if (newVertex != null) {
|
||||
logger.info("创建二级顶点: ID={}, 位置({}, {}), UV({}, {})",
|
||||
newVertex.getId(), x, y, u, v);
|
||||
SecondaryVertex newVertex = targetMesh.addSecondaryVertex(x, y, u, v);
|
||||
if (newVertex != null) {
|
||||
logger.info("创建二级顶点: ID={}, 位置({}, {}), UV({}, {})",
|
||||
newVertex.getId(), x, y, u, v);
|
||||
|
||||
targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition());
|
||||
renderPanel.repaint();
|
||||
} else {
|
||||
logger.warn("创建二级顶点失败");
|
||||
// 广播创建(GL 线程内)
|
||||
try {
|
||||
if (targetMesh.getModelPart() != null) {
|
||||
Map<String, Object> payload = Map.of(
|
||||
"id", newVertex.getId(),
|
||||
"pos", List.of(x, y)
|
||||
);
|
||||
renderPanel.getParametersManagement().broadcast(targetMesh.getModelPart(), "secondaryVertex", payload);
|
||||
}
|
||||
} catch (Throwable bx) {
|
||||
logger.debug("广播 secondaryVertex(创建) 失败: {}", bx.getMessage());
|
||||
}
|
||||
|
||||
if (targetMesh.getModelPart() != null) {
|
||||
targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition());
|
||||
targetMesh.getModelPart().updateMeshVertices();
|
||||
}
|
||||
renderPanel.repaint();
|
||||
} else {
|
||||
logger.warn("创建二级顶点失败");
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
logger.error("createSecondaryVertexAt 失败", t);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,21 +376,41 @@ public class VertexDeformationTool extends Tool {
|
||||
private void deleteSecondaryVertex(SecondaryVertex vertex) {
|
||||
if (targetMesh == null || vertex == null) return;
|
||||
|
||||
boolean removed = targetMesh.removeSecondaryVertex(vertex);
|
||||
if (removed) {
|
||||
if (selectedVertex == vertex) {
|
||||
selectedVertex = null;
|
||||
}
|
||||
if (hoveredVertex == vertex) {
|
||||
hoveredVertex = null;
|
||||
}
|
||||
logger.info("删除二级顶点: ID={}", vertex.getId());
|
||||
try {
|
||||
boolean removed = targetMesh.removeSecondaryVertex(vertex);
|
||||
if (removed) {
|
||||
if (selectedVertex == vertex) {
|
||||
selectedVertex = null;
|
||||
}
|
||||
if (hoveredVertex == vertex) {
|
||||
hoveredVertex = null;
|
||||
}
|
||||
logger.info("删除二级顶点: ID={}", vertex.getId());
|
||||
|
||||
// 标记网格为脏状态
|
||||
targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition());
|
||||
renderPanel.repaint();
|
||||
} else {
|
||||
logger.warn("删除二级顶点失败: ID={}", vertex.getId());
|
||||
// 广播删除(将 pos 设为 null 表示删除,可由 FrameInterpolator 识别)
|
||||
try {
|
||||
if (targetMesh.getModelPart() != null) {
|
||||
Map<String, Object> payload = Map.of(
|
||||
"id", vertex.getId(),
|
||||
"pos", null
|
||||
);
|
||||
renderPanel.getParametersManagement().broadcast(targetMesh.getModelPart(), "secondaryVertex", payload);
|
||||
}
|
||||
} catch (Throwable bx) {
|
||||
logger.debug("广播 secondaryVertex(删除) 失败: {}", bx.getMessage());
|
||||
}
|
||||
|
||||
// 标记网格为脏状态
|
||||
if (targetMesh.getModelPart() != null) {
|
||||
targetMesh.getModelPart().setPosition(targetMesh.getModelPart().getPosition());
|
||||
targetMesh.getModelPart().updateMeshVertices();
|
||||
}
|
||||
renderPanel.repaint();
|
||||
} else {
|
||||
logger.warn("删除二级顶点失败: ID={}", vertex.getId());
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
logger.error("deleteSecondaryVertex 失败", t);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,4 +485,4 @@ public class VertexDeformationTool extends Tool {
|
||||
public boolean isDragging() {
|
||||
return currentDragMode == ModelRenderPanel.DragMode.MOVE_SECONDARY_VERTEX;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,630 @@
|
||||
package com.chuangzhou.vivid2D.render.awt.util;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.awt.manager.*;
|
||||
import com.chuangzhou.vivid2D.render.model.*;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class FrameInterpolator {
|
||||
private FrameInterpolator() {}
|
||||
|
||||
// ---- 辅助转换方法(统一处理 Number / String / List 等) ----
|
||||
private static float toFloat(Object o) {
|
||||
if (o == null) return 0f;
|
||||
if (o instanceof Number) return ((Number) o).floatValue();
|
||||
if (o instanceof String) {
|
||||
try {
|
||||
return Float.parseFloat((String) o);
|
||||
} catch (NumberFormatException ignored) { }
|
||||
}
|
||||
return 0f;
|
||||
}
|
||||
|
||||
private static float[] readVec2(Object o) {
|
||||
float[] out = new float[]{0f, 0f};
|
||||
if (o == null) return out;
|
||||
if (o instanceof List) {
|
||||
List<?> l = (List<?>) o;
|
||||
if (l.size() > 0) out[0] = toFloat(l.get(0));
|
||||
if (l.size() > 1) out[1] = toFloat(l.get(1));
|
||||
return out;
|
||||
}
|
||||
// 支持数组情况(float[] / Double[] / Number[])
|
||||
if (o.getClass().isArray()) {
|
||||
Object[] arr = (Object[]) o;
|
||||
if (arr.length > 0) out[0] = toFloat(arr[0]);
|
||||
if (arr.length > 1) out[1] = toFloat(arr[1]);
|
||||
return out;
|
||||
}
|
||||
// 单个数字 -> 两分量相同(兼容性)
|
||||
if (o instanceof Number || o instanceof String) {
|
||||
float v = toFloat(o);
|
||||
out[0] = out[1] = v;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// 兼容 AnimationParameter.getValue() 的安全读取(保留原反射回退)
|
||||
private static float getAnimValueSafely(Object animParam) {
|
||||
if (animParam == null) return 0f;
|
||||
try {
|
||||
if (animParam instanceof AnimationParameter) {
|
||||
Object v = ((AnimationParameter) animParam).getValue();
|
||||
return toFloat(v);
|
||||
} else {
|
||||
try {
|
||||
Object v = animParam.getClass().getMethod("getValue").invoke(animParam);
|
||||
return toFloat(v);
|
||||
} catch (NoSuchMethodException | IllegalAccessException | java.lang.reflect.InvocationTargetException ignored) {}
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
return 0f;
|
||||
}
|
||||
|
||||
private static float normalizeAngle(float a) {
|
||||
while (a <= -Math.PI) a += 2 * Math.PI;
|
||||
while (a > Math.PI) a -= 2 * Math.PI;
|
||||
return a;
|
||||
}
|
||||
|
||||
private static float normalizeAnimAngleUnits(float a) {
|
||||
float abs = Math.abs(a);
|
||||
if (abs > Math.PI * 2f) {
|
||||
// 很可能是度而不是弧度
|
||||
return (float) Math.toRadians(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
// ---- 找 paramId 对应索引集合 ----
|
||||
private static List<Integer> findIndicesForParam(ParametersManagement.Parameter fullParam, String paramId) {
|
||||
List<Integer> indices = new ArrayList<>();
|
||||
if (fullParam == null || fullParam.paramId() == null) return indices;
|
||||
List<String> pids = fullParam.paramId();
|
||||
for (int i = 0; i < pids.size(); i++) {
|
||||
if (paramId.equals(pids.get(i))) indices.add(i);
|
||||
}
|
||||
return indices;
|
||||
}
|
||||
|
||||
// ---- 在指定索引集合中查找围绕 current 的前后关键帧(返回全局索引) ----
|
||||
private static int[] findSurroundingKeyframesForIndices(List<?> animParams, List<Boolean> isKeyframeList, List<Integer> indices, float current) {
|
||||
int prevIndex = -1;
|
||||
int nextIndex = -1;
|
||||
float prevVal = Float.NEGATIVE_INFINITY;
|
||||
float nextVal = Float.POSITIVE_INFINITY;
|
||||
if (animParams == null || indices == null) return new int[]{-1, -1};
|
||||
for (int idx : indices) {
|
||||
if (idx < 0 || idx >= animParams.size()) continue;
|
||||
// 注意:这里不再强制要求 isKeyframe 为 true,因实时广播可能没有标记为 keyframe
|
||||
float val = getAnimValueSafely(animParams.get(idx));
|
||||
if (val <= current) {
|
||||
if (prevIndex == -1 || val >= prevVal) {
|
||||
prevIndex = idx;
|
||||
prevVal = val;
|
||||
}
|
||||
}
|
||||
if (val >= current) {
|
||||
if (nextIndex == -1 || val <= nextVal) {
|
||||
nextIndex = idx;
|
||||
nextVal = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new int[] { prevIndex, nextIndex };
|
||||
}
|
||||
|
||||
private static float computeT(float prevVal, float nextVal, float current) {
|
||||
if (Float.compare(nextVal, prevVal) == 0) return 0f;
|
||||
float t = (current - prevVal) / (nextVal - prevVal);
|
||||
if (t < 0f) t = 0f;
|
||||
if (t > 1f) t = 1f;
|
||||
return t;
|
||||
}
|
||||
|
||||
// ---- 计算 position/scale/pivot 的目标值(在 fullParam 的特定 paramId 索引集合中计算) ----
|
||||
private static boolean computeVec2Target(ParametersManagement.Parameter fullParam, String paramId, float current, float[] out) {
|
||||
if (fullParam == null || out == null) return false;
|
||||
List<Integer> idxs = findIndicesForParam(fullParam, paramId);
|
||||
if (idxs.isEmpty()) return false;
|
||||
List<AnimationParameter> animParams = fullParam.animationParameter();
|
||||
//List<Boolean> isKey = fullParam.isKeyframe(); // 不强制使用 isKeyframe
|
||||
int[] idx = findSurroundingKeyframesForIndices(animParams, fullParam.isKeyframe(), idxs, current);
|
||||
int prevIndex = idx[0], nextIndex = idx[1];
|
||||
|
||||
List<Object> values = fullParam.value();
|
||||
if (values == null) return false;
|
||||
try {
|
||||
if (prevIndex != -1 && nextIndex != -1) {
|
||||
if (prevIndex == nextIndex) {
|
||||
float[] v = readVec2(values.get(prevIndex));
|
||||
out[0] = v[0]; out[1] = v[1];
|
||||
return true;
|
||||
} else {
|
||||
float[] prev = readVec2(values.get(prevIndex));
|
||||
float[] next = readVec2(values.get(nextIndex));
|
||||
float prevVal = getAnimValueSafely(animParams.get(prevIndex));
|
||||
float nextVal = getAnimValueSafely(animParams.get(nextIndex));
|
||||
float t = computeT(prevVal, nextVal, current);
|
||||
out[0] = prev[0] + t * (next[0] - prev[0]);
|
||||
out[1] = prev[1] + t * (next[1] - prev[1]);
|
||||
return true;
|
||||
}
|
||||
} else if (prevIndex != -1) {
|
||||
float[] v = readVec2(values.get(prevIndex));
|
||||
out[0] = v[0]; out[1] = v[1];
|
||||
return true;
|
||||
} else if (nextIndex != -1) {
|
||||
float[] v = readVec2(values.get(nextIndex));
|
||||
out[0] = v[0]; out[1] = v[1];
|
||||
return true;
|
||||
} else {
|
||||
// 精确匹配(兜底)
|
||||
for (int i : idxs) {
|
||||
if (i < 0 || i >= animParams.size()) continue;
|
||||
// 允许非 keyframe 的值作为实时覆盖
|
||||
float val = getAnimValueSafely(animParams.get(i));
|
||||
if (Float.compare(val, current) == 0) {
|
||||
float[] v = readVec2(values.get(i));
|
||||
out[0] = v[0]; out[1] = v[1];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean computeRotationTargetGeneric(ParametersManagement.Parameter fullParam, String paramId, float current, float[] outSingle) {
|
||||
if (fullParam == null || outSingle == null) return false;
|
||||
List<Integer> idxs = findIndicesForParam(fullParam, paramId);
|
||||
if (idxs.isEmpty()) return false;
|
||||
List<?> animParams = fullParam.animationParameter();
|
||||
List<Object> values = fullParam.value();
|
||||
int[] idx = findSurroundingKeyframesForIndices(animParams, fullParam.isKeyframe(), idxs, current);
|
||||
int prevIndex = idx[0], nextIndex = idx[1];
|
||||
|
||||
try {
|
||||
float target;
|
||||
if (prevIndex != -1 && nextIndex != -1) {
|
||||
if (prevIndex == nextIndex) {
|
||||
target = toFloat(values.get(prevIndex));
|
||||
} else {
|
||||
float p = toFloat(values.get(prevIndex));
|
||||
float q = toFloat(values.get(nextIndex));
|
||||
float prevVal = getAnimValueSafely(animParams.get(prevIndex));
|
||||
float nextVal = getAnimValueSafely(animParams.get(nextIndex));
|
||||
float t = computeT(prevVal, nextVal, current);
|
||||
float diff = normalizeAngle(q - p);
|
||||
target = p + diff * t;
|
||||
}
|
||||
} else if (prevIndex != -1) {
|
||||
target = toFloat(values.get(prevIndex));
|
||||
} else if (nextIndex != -1) {
|
||||
target = toFloat(values.get(nextIndex));
|
||||
} else {
|
||||
float found = Float.NaN;
|
||||
for (int i : idxs) {
|
||||
if (i < 0 || i >= animParams.size()) continue;
|
||||
float val = getAnimValueSafely(animParams.get(i));
|
||||
if (Float.compare(val, current) == 0) {
|
||||
found = toFloat(values.get(i));
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (Float.isNaN(found)) return false;
|
||||
target = found;
|
||||
}
|
||||
|
||||
outSingle[0] = normalizeAnimAngleUnits(target);
|
||||
return true;
|
||||
} catch (Throwable ignored) {}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- Secondary vertex 插值(为每个 vertex id 计算目标) ----
|
||||
// 返回列表:每个 SecondaryVertexTarget 表示 id -> 插值后的位置 或 标记为 deleted
|
||||
private static List<SecondaryVertexTarget> computeAllSecondaryVertexTargets(ParametersManagement.Parameter fullParam, String paramId, float current) {
|
||||
List<SecondaryVertexTarget> results = new ArrayList<>();
|
||||
if (fullParam == null) return results;
|
||||
|
||||
List<Integer> idxs = findIndicesForParam(fullParam, paramId);
|
||||
if (idxs.isEmpty()) return results;
|
||||
|
||||
List<?> animParams = fullParam.animationParameter();
|
||||
List<Object> values = fullParam.value();
|
||||
if (animParams == null || values == null) return results;
|
||||
|
||||
// 按 vertex id 分组(包含 keyframe 与 非 keyframe,允许实时覆盖)
|
||||
Map<Integer, List<Integer>> idToIndices = new HashMap<>();
|
||||
for (int i : idxs) {
|
||||
if (i < 0 || i >= values.size() || i >= animParams.size()) continue;
|
||||
ParsedVertex pv = parseVertexValue(values.get(i));
|
||||
if (pv == null) continue;
|
||||
idToIndices.computeIfAbsent(pv.id, k -> new ArrayList<>()).add(i);
|
||||
}
|
||||
|
||||
// 对每个 id 单独计算 prev/next 并插值(包括处理删除标记)
|
||||
for (Map.Entry<Integer, List<Integer>> e : idToIndices.entrySet()) {
|
||||
int vid = e.getKey();
|
||||
List<Integer> list = e.getValue();
|
||||
if (list.isEmpty()) continue;
|
||||
|
||||
int prevIndex = -1, nextIndex = -1;
|
||||
float prevVal = Float.NEGATIVE_INFINITY, nextVal = Float.POSITIVE_INFINITY;
|
||||
for (int idx : list) {
|
||||
float val = getAnimValueSafely(animParams.get(idx));
|
||||
if (val <= current) {
|
||||
if (prevIndex == -1 || val >= prevVal) {
|
||||
prevIndex = idx;
|
||||
prevVal = val;
|
||||
}
|
||||
}
|
||||
if (val >= current) {
|
||||
if (nextIndex == -1 || val <= nextVal) {
|
||||
nextIndex = idx;
|
||||
nextVal = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// 优先 prev/next 插值或取值;若存在 pos==null 的条目(删除),则将其转为 deleted 标记
|
||||
if (prevIndex != -1 && nextIndex != -1) {
|
||||
if (prevIndex == nextIndex) {
|
||||
ParsedVertex pv = parseVertexValue(values.get(prevIndex));
|
||||
if (pv != null) {
|
||||
SecondaryVertexTarget tgt = new SecondaryVertexTarget();
|
||||
tgt.id = vid;
|
||||
tgt.deleted = pv.deleted;
|
||||
tgt.x = pv.x;
|
||||
tgt.y = pv.y;
|
||||
results.add(tgt);
|
||||
}
|
||||
} else {
|
||||
ParsedVertex pv = parseVertexValue(values.get(prevIndex));
|
||||
ParsedVertex nv = parseVertexValue(values.get(nextIndex));
|
||||
if (pv == null || nv == null) {
|
||||
// 若任意一端为 null,则退化为非插值处理(尝试取有意义的一端)
|
||||
if (pv != null) {
|
||||
SecondaryVertexTarget tgt = new SecondaryVertexTarget();
|
||||
tgt.id = vid; tgt.deleted = pv.deleted; tgt.x = pv.x; tgt.y = pv.y;
|
||||
results.add(tgt);
|
||||
} else if (nv != null) {
|
||||
SecondaryVertexTarget tgt = new SecondaryVertexTarget();
|
||||
tgt.id = vid; tgt.deleted = nv.deleted; tgt.x = nv.x; tgt.y = nv.y;
|
||||
results.add(tgt);
|
||||
}
|
||||
} else {
|
||||
// 如果任意一侧为 deleted(pos==null),则不做插值(选择 prev 的删除/存在状态)
|
||||
if (pv.deleted || nv.deleted) {
|
||||
// 选择较靠近 current 的那端(这里优先 prev)
|
||||
SecondaryVertexTarget tgt = new SecondaryVertexTarget();
|
||||
tgt.id = vid;
|
||||
tgt.deleted = pv.deleted;
|
||||
tgt.x = pv.x;
|
||||
tgt.y = pv.y;
|
||||
results.add(tgt);
|
||||
} else {
|
||||
float t = computeT(prevVal, nextVal, current);
|
||||
SecondaryVertexTarget tgt = new SecondaryVertexTarget();
|
||||
tgt.id = vid;
|
||||
tgt.deleted = false;
|
||||
tgt.x = pv.x + t * (nv.x - pv.x);
|
||||
tgt.y = pv.y + t * (nv.y - pv.y);
|
||||
results.add(tgt);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (prevIndex != -1) {
|
||||
ParsedVertex pv = parseVertexValue(values.get(prevIndex));
|
||||
if (pv != null) {
|
||||
SecondaryVertexTarget tgt = new SecondaryVertexTarget();
|
||||
tgt.id = vid; tgt.deleted = pv.deleted; tgt.x = pv.x; tgt.y = pv.y;
|
||||
results.add(tgt);
|
||||
}
|
||||
} else if (nextIndex != -1) {
|
||||
ParsedVertex nv = parseVertexValue(values.get(nextIndex));
|
||||
if (nv != null) {
|
||||
SecondaryVertexTarget tgt = new SecondaryVertexTarget();
|
||||
tgt.id = vid; tgt.deleted = nv.deleted; tgt.x = nv.x; tgt.y = nv.y;
|
||||
results.add(tgt);
|
||||
}
|
||||
} else {
|
||||
// 兜底:查找与 current 相等的条目
|
||||
for (int idx : list) {
|
||||
float val = getAnimValueSafely(animParams.get(idx));
|
||||
if (Float.compare(val, current) == 0) {
|
||||
ParsedVertex pv = parseVertexValue(values.get(idx));
|
||||
if (pv != null) {
|
||||
SecondaryVertexTarget tgt = new SecondaryVertexTarget();
|
||||
tgt.id = vid; tgt.deleted = pv.deleted; tgt.x = pv.x; tgt.y = pv.y;
|
||||
results.add(tgt);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// 辅助结构
|
||||
private static class ParsedVertex {
|
||||
int id;
|
||||
float x;
|
||||
float y;
|
||||
boolean deleted; // pos == null 表示删除
|
||||
ParsedVertex(int id, float x, float y, boolean deleted) { this.id = id; this.x = x; this.y = y; this.deleted = deleted; }
|
||||
}
|
||||
|
||||
private static ParsedVertex parseVertexValue(Object v) {
|
||||
if (v == null) return null;
|
||||
try {
|
||||
if (v instanceof Map) {
|
||||
Map<?,?> m = (Map<?,?>) v;
|
||||
Object idObj = m.get("id");
|
||||
// pos 可能为 null(表示删除)
|
||||
Object posObj = m.get("pos");
|
||||
if (idObj == null) return null;
|
||||
int id = (idObj instanceof Number) ? ((Number) idObj).intValue() : Integer.parseInt(String.valueOf(idObj));
|
||||
if (posObj == null) {
|
||||
return new ParsedVertex(id, 0f, 0f, true);
|
||||
}
|
||||
float[] p = readVec2(posObj);
|
||||
return new ParsedVertex(id, p[0], p[1], false);
|
||||
} else if (v instanceof List) {
|
||||
List<?> l = (List<?>) v;
|
||||
if (l.size() >= 3) {
|
||||
Object idObj = l.get(0);
|
||||
if (!(idObj instanceof Number)) return null;
|
||||
int id = ((Number) idObj).intValue();
|
||||
float x = toFloat(l.get(1));
|
||||
float y = toFloat(l.get(2));
|
||||
return new ParsedVertex(id, x, y, false);
|
||||
} else if (l.size() == 2) {
|
||||
// [ id, null ] 之类(不常见)
|
||||
Object idObj = l.get(0);
|
||||
if (!(idObj instanceof Number)) return null;
|
||||
int id = ((Number) idObj).intValue();
|
||||
Object posObj = l.get(1);
|
||||
if (posObj == null) return new ParsedVertex(id, 0f, 0f, true);
|
||||
}
|
||||
} else {
|
||||
// 可能是自定义对象,尝试反射获取 id/pos(容错)
|
||||
try {
|
||||
Object idObj = v.getClass().getMethod("getId").invoke(v);
|
||||
Object px = v.getClass().getMethod("getX").invoke(v);
|
||||
Object py = v.getClass().getMethod("getY").invoke(v);
|
||||
int id = idObj instanceof Number ? ((Number) idObj).intValue() : Integer.parseInt(String.valueOf(idObj));
|
||||
return new ParsedVertex(id, toFloat(px), toFloat(py), false);
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
// 用于输出 secondary vertex 结果
|
||||
private static class SecondaryVertexTarget {
|
||||
int id;
|
||||
float x;
|
||||
float y;
|
||||
boolean deleted = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 SelectionTool 的四类操作(pivot/scale/rotate/position/secondaryVertex)按当前关键帧插值并应用到 parts。
|
||||
* 该方法应在 GL 上下文线程中被调用(即 glContextManager.executeInGLContext 内)。
|
||||
*
|
||||
* 对每个 part:先计算所有目标(如果存在),再一次性按 pivot->scale->rotation->position 的顺序应用,
|
||||
* 并在最后只做一次 updateMeshVertices/updateBounds。
|
||||
*/
|
||||
public static void applyFrameInterpolations(ParametersManagement pm,
|
||||
List<ModelPart> parts,
|
||||
AnimationParameter currentAnimationParameter,
|
||||
Logger logger) {
|
||||
if (pm == null || parts == null || parts.isEmpty() || currentAnimationParameter == null) return;
|
||||
float current = 0f;
|
||||
try {
|
||||
Object v = currentAnimationParameter.getValue();
|
||||
current = toFloat(v);
|
||||
} catch (Exception ex) {
|
||||
logger.debug("读取当前动画参数值失败,使用0作为默认值", ex);
|
||||
}
|
||||
|
||||
for (ModelPart part : parts) {
|
||||
try {
|
||||
// Full parameter record for this ModelPart (contains lists for all paramIds)
|
||||
ParametersManagement.Parameter fullParam = pm.getModelPartParameters(part);
|
||||
if (fullParam == null) {
|
||||
// 没有记录则继续
|
||||
continue;
|
||||
}
|
||||
|
||||
// 目标容器(null 表示未设置)
|
||||
float[] targetPivot = null;
|
||||
float[] targetScale = null;
|
||||
Float targetRotation = null;
|
||||
float[] targetPosition = null;
|
||||
List<SecondaryVertexTarget> svTargets = null;
|
||||
|
||||
// pivot
|
||||
float[] tmp2 = new float[2];
|
||||
if (computeVec2Target(fullParam, "pivot", current, tmp2)) {
|
||||
targetPivot = new float[]{tmp2[0], tmp2[1]};
|
||||
}
|
||||
|
||||
// scale
|
||||
if (computeVec2Target(fullParam, "scale", current, tmp2)) {
|
||||
targetScale = new float[]{tmp2[0], tmp2[1]};
|
||||
}
|
||||
|
||||
// rotate
|
||||
float[] tmp1 = new float[1];
|
||||
if (computeRotationTargetGeneric(fullParam, "rotate", current, tmp1)) {
|
||||
targetRotation = tmp1[0];
|
||||
}
|
||||
|
||||
// position
|
||||
if (computeVec2Target(fullParam, "position", current, tmp2)) {
|
||||
targetPosition = new float[]{tmp2[0], tmp2[1]};
|
||||
}
|
||||
|
||||
// secondaryVertex: 为每个记录的 vertex id 计算目标位置(包含实时广播)
|
||||
List<SecondaryVertexTarget> computedSV = computeAllSecondaryVertexTargets(fullParam, "secondaryVertex", current);
|
||||
if (computedSV != null && !computedSV.isEmpty()) {
|
||||
svTargets = computedSV;
|
||||
}
|
||||
|
||||
// 如果没有任何要修改的,跳过
|
||||
if (targetPivot == null && targetScale == null && targetRotation == null && targetPosition == null && (svTargets == null || svTargets.isEmpty())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 记录当前状态(用于没有 setRotation 方法时计算 delta)
|
||||
float currentRot = part.getRotation();
|
||||
|
||||
// 一次性应用:pivot -> scale -> rotation -> position(最小化中间更新)
|
||||
try {
|
||||
if (targetPivot != null) {
|
||||
part.setPivot(targetPivot[0], targetPivot[1]);
|
||||
}
|
||||
|
||||
if (targetScale != null) {
|
||||
part.setScale(targetScale[0], targetScale[1]);
|
||||
}
|
||||
|
||||
if (targetRotation != null) {
|
||||
try {
|
||||
Method setRotation = part.getClass().getMethod("setRotation", float.class);
|
||||
setRotation.invoke(part, targetRotation);
|
||||
} catch (NoSuchMethodException nsme) {
|
||||
float delta = normalizeAngle(targetRotation - currentRot);
|
||||
part.rotate(delta);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
if (targetPosition != null) {
|
||||
part.setPosition(targetPosition[0], targetPosition[1]);
|
||||
}
|
||||
|
||||
// 处理 secondary vertex:对每个计算出的目标,找到对应 mesh 与 vertex id,设置顶点位置或删除
|
||||
if (svTargets != null && !svTargets.isEmpty()) {
|
||||
try {
|
||||
List<com.chuangzhou.vivid2D.render.model.util.Mesh2D> meshes = part.getMeshes();
|
||||
if (meshes != null) {
|
||||
for (SecondaryVertexTarget s : svTargets) {
|
||||
boolean appliedGlobal = false;
|
||||
for (com.chuangzhou.vivid2D.render.model.util.Mesh2D mesh : meshes) {
|
||||
if (mesh == null) continue;
|
||||
try {
|
||||
boolean applied = false;
|
||||
// 优先尝试 mesh.getSecondaryVertices()
|
||||
try {
|
||||
List<com.chuangzhou.vivid2D.render.model.util.SecondaryVertex> svs =
|
||||
(List<com.chuangzhou.vivid2D.render.model.util.SecondaryVertex>) mesh.getClass().getMethod("getSecondaryVertices").invoke(mesh);
|
||||
if (svs != null) {
|
||||
com.chuangzhou.vivid2D.render.model.util.SecondaryVertex found = null;
|
||||
for (com.chuangzhou.vivid2D.render.model.util.SecondaryVertex sv : svs) {
|
||||
if (sv != null && sv.getId() == s.id) {
|
||||
found = sv;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found != null) {
|
||||
if (s.deleted) {
|
||||
// 尝试通过 mesh 提供的 remove 方法
|
||||
try {
|
||||
mesh.getClass().getMethod("removeSecondaryVertex", com.chuangzhou.vivid2D.render.model.util.SecondaryVertex.class)
|
||||
.invoke(mesh, found);
|
||||
} catch (NoSuchMethodException nsme) {
|
||||
// 没有 remove 方法则尝试 mesh.removeSecondaryVertex(found) via known signature
|
||||
try {
|
||||
mesh.getClass().getMethod("removeSecondaryVertexById", int.class).invoke(mesh, s.id);
|
||||
} catch (Throwable ignore) {
|
||||
// 最后作为保险:尝试直接从 list 删除(不推荐,但做容错)
|
||||
try {
|
||||
svs.remove(found);
|
||||
} catch (Throwable ignore2) {}
|
||||
}
|
||||
} catch (Throwable ignore) {}
|
||||
} else {
|
||||
found.setPosition(s.x, s.y);
|
||||
}
|
||||
applied = true;
|
||||
}
|
||||
}
|
||||
} catch (NoSuchMethodException nsme) {
|
||||
// 忽略:没有该方法
|
||||
} catch (Throwable ignore2) {
|
||||
// 忽略运行时异常
|
||||
}
|
||||
|
||||
// 回退策略:尝试 getSecondaryVertexById(int)
|
||||
if (!applied) {
|
||||
try {
|
||||
Object svObj = mesh.getClass().getMethod("getSecondaryVertexById", int.class).invoke(mesh, s.id);
|
||||
if (svObj instanceof com.chuangzhou.vivid2D.render.model.util.SecondaryVertex) {
|
||||
com.chuangzhou.vivid2D.render.model.util.SecondaryVertex found = (com.chuangzhou.vivid2D.render.model.util.SecondaryVertex) svObj;
|
||||
if (s.deleted) {
|
||||
try {
|
||||
mesh.getClass().getMethod("removeSecondaryVertex", com.chuangzhou.vivid2D.render.model.util.SecondaryVertex.class)
|
||||
.invoke(mesh, found);
|
||||
} catch (Throwable ignore) {}
|
||||
} else {
|
||||
found.setPosition(s.x, s.y);
|
||||
}
|
||||
applied = true;
|
||||
}
|
||||
} catch (NoSuchMethodException ignoreMethod) {
|
||||
// 忽略
|
||||
} catch (Throwable ignore3) {}
|
||||
}
|
||||
|
||||
if (applied) {
|
||||
appliedGlobal = true;
|
||||
break;
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
// 未找到对应 id 的 mesh/vertex 则忽略(容错)
|
||||
if (!appliedGlobal) {
|
||||
// 可选: 记录日志方便调试
|
||||
logger.debug("未找到二级顶点 id={} 的对应 mesh/vertex", s.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
|
||||
// 最后统一刷新一次顶点与 bounds,避免频繁刷新导致与交互冲突
|
||||
try {
|
||||
part.updateMeshVertices();
|
||||
if (part.getMeshes() != null) {
|
||||
for (Object m : part.getMeshes()) {
|
||||
try {
|
||||
if (m instanceof com.chuangzhou.vivid2D.render.model.util.Mesh2D) {
|
||||
((com.chuangzhou.vivid2D.render.model.util.Mesh2D) m).updateBounds();
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
}
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
|
||||
} catch (Throwable t) {
|
||||
logger.debug("应用目标变换时失败(single part): {}", t.getMessage());
|
||||
}
|
||||
|
||||
} catch (Exception ex) {
|
||||
logger.error("FrameInterpolator 在应用插值时发生异常", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
package com.chuangzhou.vivid2D.render.model;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
|
||||
public class AnimationParameter {
|
||||
private final String id;
|
||||
private float value;
|
||||
@@ -8,6 +13,8 @@ public class AnimationParameter {
|
||||
private final float maxValue;
|
||||
private boolean changed = false;
|
||||
|
||||
private final TreeSet<Float> keyframes = new TreeSet<>();
|
||||
|
||||
public AnimationParameter(String id, float min, float max, float defaultValue) {
|
||||
this.id = id;
|
||||
this.minValue = min;
|
||||
@@ -24,6 +31,17 @@ public class AnimationParameter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 一个新的 AnimationParameter 实例,包含相同的配置、值、状态和关键帧。
|
||||
*/
|
||||
public AnimationParameter copy() {
|
||||
AnimationParameter copy = new AnimationParameter(this.id, this.minValue, this.maxValue, this.defaultValue);
|
||||
copy.value = this.value;
|
||||
copy.changed = this.changed;
|
||||
copy.keyframes.addAll(this.keyframes);
|
||||
return copy;
|
||||
}
|
||||
|
||||
public boolean hasChanged() {
|
||||
return changed;
|
||||
}
|
||||
@@ -53,21 +71,150 @@ public class AnimationParameter {
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
this.value = defaultValue;
|
||||
this.changed = false;
|
||||
setValue(defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取归一化值 [0, 1]
|
||||
*/
|
||||
public float getNormalizedValue() {
|
||||
return (value - minValue) / (maxValue - minValue);
|
||||
float range = maxValue - minValue;
|
||||
if (range == 0) return 0;
|
||||
return (value - minValue) / range;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置归一化值
|
||||
*/
|
||||
public void setNormalizedValue(float normalized) {
|
||||
this.value = minValue + normalized * (maxValue - minValue);
|
||||
float newValue = minValue + normalized * (maxValue - minValue);
|
||||
setValue(newValue); // 使用 setValue 来确保钳位和 'changed' 标记
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 添加一个关键帧。值会被自动钳位(clamp)到 min/max 范围内。
|
||||
* @param frameValue 参数值
|
||||
* @return 如果成功添加了新帧,返回 true;如果帧已存在,返回 false。
|
||||
*/
|
||||
public boolean addKeyframe(float frameValue) {
|
||||
float clampedValue = Math.max(minValue, Math.min(maxValue, frameValue));
|
||||
return keyframes.add(clampedValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除一个关键帧。
|
||||
* @param frameValue 参数值
|
||||
* @return 如果成功移除了该帧,返回 true;如果帧不存在,返回 false。
|
||||
*/
|
||||
public boolean removeKeyframe(float frameValue) {
|
||||
return keyframes.remove(frameValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查某个值是否是关键帧。
|
||||
* @param frameValue 参数值
|
||||
* @return 如果是,返回 true。
|
||||
*/
|
||||
public boolean isKeyframe(float frameValue) {
|
||||
// 使用 epsilon 进行浮点数比较可能更稳健,但 TreeSet 存储的是精确值
|
||||
// 为了简单起见,我们假设我们操作的是精确的 float
|
||||
return keyframes.contains(frameValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有关键帧的只读、排序视图。
|
||||
* @return 排序后的关键帧集合
|
||||
*/
|
||||
public SortedSet<Float> getKeyframes() {
|
||||
return Collections.unmodifiableSortedSet(keyframes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有关键帧。
|
||||
*/
|
||||
public void clearKeyframes() {
|
||||
keyframes.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找在给定阈值(threshold)内最接近指定值的关键帧。
|
||||
*
|
||||
* @param value 要查找的值
|
||||
* @param snapThreshold 绝对吸附阈值 (例如 0.05)
|
||||
* @return 如果找到,返回最近的帧值;否则返回 null。
|
||||
*/
|
||||
public Float getNearestKeyframe(float value, float snapThreshold) {
|
||||
if (snapThreshold <= 0) return null;
|
||||
|
||||
// 查找 value 附近的关键帧
|
||||
SortedSet<Float> head = keyframes.headSet(value);
|
||||
SortedSet<Float> tail = keyframes.tailSet(value);
|
||||
|
||||
Float prev = head.isEmpty() ? null : head.last();
|
||||
Float next = tail.isEmpty() ? null : tail.first();
|
||||
|
||||
float distToPrev = prev != null ? Math.abs(value - prev) : Float.MAX_VALUE;
|
||||
float distToNext = next != null ? Math.abs(value - next) : Float.MAX_VALUE;
|
||||
|
||||
if (distToPrev < snapThreshold && distToPrev <= distToNext) {
|
||||
return prev;
|
||||
}
|
||||
if (distToNext < snapThreshold && distToNext < distToPrev) {
|
||||
return next;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
AnimationParameter that = (AnimationParameter) obj;
|
||||
// 比较所有定义参数的 final 字段和关键帧集合
|
||||
return Float.compare(that.defaultValue, defaultValue) == 0 &&
|
||||
Float.compare(that.minValue, minValue) == 0 &&
|
||||
Float.compare(that.maxValue, maxValue) == 0 &&
|
||||
Objects.equals(id, that.id) &&
|
||||
Objects.equals(keyframes, that.keyframes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String idStr = Objects.requireNonNullElse(id, "[null id]");
|
||||
String valStr = String.format("%.3f", value);
|
||||
String minStr = String.format("%.3f", minValue);
|
||||
String maxStr = String.format("%.3f", maxValue);
|
||||
String defStr = String.format("%.3f", defaultValue);
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
sb.append("AnimationParameter[ID=").append(idStr);
|
||||
sb.append(", Value=").append(valStr);
|
||||
sb.append(changed ? " (Changed)" : "");
|
||||
sb.append(", Range=[").append(minStr).append(", ").append(maxStr).append("]");
|
||||
sb.append(", Default=").append(defStr);
|
||||
if (keyframes.isEmpty()) {
|
||||
sb.append(", Keyframes=[]");
|
||||
} else {
|
||||
sb.append(", Keyframes=[");
|
||||
boolean first = true;
|
||||
for (Float kf : keyframes) {
|
||||
if (!first) {
|
||||
sb.append(", ");
|
||||
}
|
||||
sb.append(String.format("%.3f", kf));
|
||||
first = false;
|
||||
}
|
||||
sb.append("]");
|
||||
}
|
||||
|
||||
sb.append("]");
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ public class ModelPart {
|
||||
// ==================== 变形系统 ====================
|
||||
private final List<Deformer> deformers;
|
||||
private final List<LiquifyStroke> liquifyStrokes = new ArrayList<>();
|
||||
private final Map<String, AnimationParameter> parameters;
|
||||
|
||||
// ==================== 状态标记 ====================
|
||||
private boolean transformDirty;
|
||||
@@ -51,7 +52,6 @@ public class ModelPart {
|
||||
|
||||
private final List<ModelEvent> events = new ArrayList<>();
|
||||
private boolean inMultiSelectionOperation = false;
|
||||
private boolean startLiquefy = false;
|
||||
|
||||
// ====== 液化模式枚举 ======
|
||||
public enum LiquifyMode {
|
||||
@@ -65,6 +65,31 @@ public class ModelPart {
|
||||
TURBULENCE // 湍流(噪声扰动)
|
||||
}
|
||||
|
||||
public AnimationParameter createParameter(String id, float min, float max, float defaultValue) {
|
||||
AnimationParameter param = new AnimationParameter(id, min, max, defaultValue);
|
||||
parameters.put(id, param);
|
||||
return param;
|
||||
}
|
||||
|
||||
public AnimationParameter getParameter(String id) {
|
||||
return parameters.get(id);
|
||||
}
|
||||
|
||||
public Map<String, AnimationParameter> getParameters() {
|
||||
return parameters;
|
||||
}
|
||||
|
||||
public void addParameter(AnimationParameter param) {
|
||||
parameters.put(param.getId(), param);
|
||||
}
|
||||
|
||||
public void setParameterValue(String paramId, float value) {
|
||||
AnimationParameter param = parameters.get(paramId);
|
||||
if (param != null) {
|
||||
param.setValue(value);
|
||||
markTransformDirty();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 构造器 ====================
|
||||
|
||||
@@ -77,25 +102,18 @@ public class ModelPart {
|
||||
this.children = new ArrayList<>();
|
||||
this.meshes = new ArrayList<>();
|
||||
this.deformers = new ArrayList<>();
|
||||
|
||||
// 初始化变换属性
|
||||
this.position = new Vector2f();
|
||||
this.rotation = 0.0f;
|
||||
this.scale = new Vector2f(1.0f, 1.0f);
|
||||
this.localTransform = new Matrix3f();
|
||||
this.worldTransform = new Matrix3f();
|
||||
|
||||
// 初始化渲染属性
|
||||
this.visible = true;
|
||||
this.blendMode = BlendMode.NORMAL;
|
||||
this.opacity = 1.0f;
|
||||
|
||||
// 标记需要更新
|
||||
parameters = new HashMap<>();
|
||||
this.transformDirty = true;
|
||||
this.boundsDirty = true;
|
||||
|
||||
updateLocalTransform();
|
||||
// 初始时 worldTransform = localTransform(无父节点时)
|
||||
recomputeWorldTransformRecursive();
|
||||
}
|
||||
|
||||
@@ -117,7 +135,6 @@ public class ModelPart {
|
||||
* 设置液化状态
|
||||
*/
|
||||
public void setStartLiquefy(boolean startLiquefy) {
|
||||
this.startLiquefy = startLiquefy;
|
||||
|
||||
// 同步到所有网格
|
||||
for (Mesh2D mesh : meshes) {
|
||||
|
||||
@@ -43,7 +43,6 @@ public class Mesh2D {
|
||||
private ModelPart modelPart;
|
||||
private float[] renderVertices;
|
||||
|
||||
|
||||
// ==================== 二级顶点支持 ====================
|
||||
private final List<SecondaryVertex> secondaryVertices = new ArrayList<>();
|
||||
private boolean showSecondaryVertices = false;
|
||||
|
||||
@@ -2,7 +2,9 @@ package com.chuangzhou.vivid2D.test;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.awt.ModelLayerPanel;
|
||||
import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel;
|
||||
import com.chuangzhou.vivid2D.render.awt.ParametersPanel;
|
||||
import com.chuangzhou.vivid2D.render.awt.TransformPanel;
|
||||
import com.chuangzhou.vivid2D.render.awt.manager.ParametersManagement;
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.formdev.flatlaf.themes.FlatMacDarkLaf;
|
||||
@@ -52,6 +54,11 @@ public class ModelLayerPanelTest {
|
||||
rightTabbedPane.addTab("变换控制", transformScroll);
|
||||
rightTabbedPane.setPreferredSize(new Dimension(300, 600));
|
||||
frame.add(rightTabbedPane, BorderLayout.EAST);
|
||||
ParametersPanel parametersPanel = new ParametersPanel(renderPanel);
|
||||
renderPanel.setParametersManagement(new ParametersManagement(parametersPanel));
|
||||
JScrollPane paramScroll = new JScrollPane(parametersPanel);
|
||||
paramScroll.setPreferredSize(new Dimension(280, 600));
|
||||
rightTabbedPane.addTab("参数管理", paramScroll);
|
||||
JPanel bottom = getBottom(renderPanel, transformPanel);
|
||||
frame.add(bottom, BorderLayout.SOUTH);
|
||||
renderPanel.addModelClickListener((mesh, modelX, modelY, screenX, screenY) -> {
|
||||
|
||||
Reference in New Issue
Block a user