feat(animation): 添加关键帧编辑与插值功能

- 为 AnimationParameter 类添加关键帧管理功能,包括添加、删除、查找和清空关键帧
- 实现关键帧的复制方法,支持完整状态复制
- 添加关键帧吸附与最近关键帧查找逻辑
- 实现 FrameInterpolator 类,支持 position、scale、pivot、rotation 和 secondaryVertex 的插值计算
- 添加 KeyframeEditorDialog 图形界面,用于可视化编辑关键帧- 支持鼠标交互添加/删除关键帧,并提供悬浮提示功能
- 实现标尺组件,用于显示关键帧分布与当前值位置
- 添加对角度单位的自动识别与归一化处理- 支持 secondary vertex 的插值与删除标记处理-优化插值性能,减少不必要的中间更新,提升渲染效率
This commit is contained in:
tzdwindows 7
2025-11-07 00:52:19 +08:00
parent 1c75006d51
commit 7a04cc2a2d
12 changed files with 2789 additions and 135 deletions

View File

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

View File

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

View File

@@ -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, // 移动部件

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 {
// 如果任意一侧为 deletedpos==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);
}
}
}
}

View File

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

View File

@@ -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) {

View File

@@ -43,7 +43,6 @@ public class Mesh2D {
private ModelPart modelPart;
private float[] renderVertices;
// ==================== 二级顶点支持 ====================
private final List<SecondaryVertex> secondaryVertices = new ArrayList<>();
private boolean showSecondaryVertices = false;

View File

@@ -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) -> {