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