feat(render): 实现关键帧详细编辑对话框及相关数据转换工具- 新增 KeyframeDetailsDialog 类,用于编辑单个关键帧值并管理同时间点的其他参数
- 添加参数ID中文映射显示功能,提升用户界面友好性 - 实现同一时间点多个参数的批量删除和快捷键支持 - 集成搜索过滤功能,便于查找特定参数- 新增 ManagementDataToJsonConverter 工具类,支持将序列化管理数据转为JSON格式 - 添加 ModelDataJsonConverter 工具类,支持模型数据序列化文件转JSON - 修改 MainWindow 保存逻辑,自动生成对应的JSON数据文件-优化界面布局和组件结构,改善用户体验
This commit is contained in:
@@ -0,0 +1,684 @@
|
||||
package com.chuangzhou.vivid2D.render.awt;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.awt.manager.ParametersManagement;
|
||||
import com.chuangzhou.vivid2D.render.awt.manager.ParametersManagement.Parameter;
|
||||
import com.chuangzhou.vivid2D.render.model.AnimationParameter;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.Border;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import javax.swing.table.AbstractTableModel;
|
||||
import javax.swing.table.DefaultTableCellRenderer;
|
||||
import javax.swing.table.TableCellEditor;
|
||||
import javax.swing.table.TableCellRenderer;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.SortedSet;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 用于编辑单个关键帧值并显示同一时间点其他参数信息的对话框。
|
||||
*/
|
||||
public class KeyframeDetailsDialog extends JDialog {
|
||||
|
||||
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 static final float FLOAT_TOLERANCE = 1e-6f; // 浮点数比较容差
|
||||
|
||||
private final AnimationParameter parameter;
|
||||
private final ParametersManagement parametersManagement;
|
||||
private final ModelPart modelPart;
|
||||
private final SortedSet<Float> keyframesSet;
|
||||
private final Float originalValue;
|
||||
private Float confirmedValue = null;
|
||||
|
||||
private final JTextField valueField = new JTextField(15);
|
||||
private final JSlider valueSlider = new JSlider(0, 1000); // 使用 0-1000 归一化
|
||||
|
||||
// [新增] 搜索字段
|
||||
private final JTextField searchField = new JTextField(15);
|
||||
|
||||
// 内部类,用于存储和显示同一时间点的其他参数信息
|
||||
private final RelatedParametersTableModel relatedTableModel;
|
||||
private final JTable relatedTable;
|
||||
|
||||
// [新增] 用于存储所有相关参数的完整列表(过滤前)
|
||||
private List<RelatedParameterInfo> allRelatedParameters = new ArrayList<>();
|
||||
|
||||
|
||||
public KeyframeDetailsDialog(Window owner, AnimationParameter parameter, Float value, SortedSet<Float> keyframesSet,
|
||||
ParametersManagement parametersManagement, ModelPart modelPart) {
|
||||
super(owner, "编辑关键帧: " + parameter.getId(), ModalityType.APPLICATION_MODAL);
|
||||
this.parameter = parameter;
|
||||
this.originalValue = value;
|
||||
this.keyframesSet = keyframesSet;
|
||||
this.parametersManagement = parametersManagement;
|
||||
this.modelPart = modelPart;
|
||||
|
||||
// 字段初始化
|
||||
// [修改] 传递中文 ID 映射
|
||||
this.relatedTableModel = new RelatedParametersTableModel(modelPart, parametersManagement, this::refreshTableData);
|
||||
this.relatedTable = new JTable(relatedTableModel);
|
||||
|
||||
initUI();
|
||||
loadData(value);
|
||||
fetchRelatedParameters(); // 查询并加载所有相关参数,并初始化表格
|
||||
}
|
||||
|
||||
// 已修改的 record:新增 recordIndex,用于精确指向 ModelPart 完整记录中的条目
|
||||
private record RelatedParameterInfo(String paramId, Object value, int recordIndex) {}
|
||||
|
||||
// ----------------------------------------------------------------------------------
|
||||
// 内部类:RelatedParametersTableModel (处理表格数据和删除逻辑)
|
||||
// ----------------------------------------------------------------------------------
|
||||
private class RelatedParametersTableModel extends AbstractTableModel {
|
||||
private final String[] columnNames = {"参数 ID", "值", "操作"};
|
||||
private final List<RelatedParameterInfo> data = new ArrayList<>();
|
||||
private final ModelPart modelPart;
|
||||
private final ParametersManagement management;
|
||||
private final Runnable refreshCallback;
|
||||
|
||||
// [新增] 参数ID 中文映射
|
||||
private final Map<String, String> paramIdMap;
|
||||
|
||||
|
||||
public RelatedParametersTableModel(ModelPart modelPart, ParametersManagement management, Runnable refreshCallback) {
|
||||
this.modelPart = modelPart;
|
||||
this.management = management;
|
||||
this.refreshCallback = refreshCallback;
|
||||
// 初始化中文映射
|
||||
this.paramIdMap = new HashMap<>();
|
||||
this.paramIdMap.put("position", "位置");
|
||||
this.paramIdMap.put("rotate", "旋转");
|
||||
this.paramIdMap.put("secondaryVertex", "二级顶点变形器(顶点位置)");
|
||||
this.paramIdMap.put("scale", "缩放");
|
||||
}
|
||||
|
||||
public void setData(List<RelatedParameterInfo> list) {
|
||||
data.clear();
|
||||
data.addAll(list);
|
||||
fireTableDataChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRowCount() { return data.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) {
|
||||
RelatedParameterInfo info = data.get(rowIndex);
|
||||
return switch (columnIndex) {
|
||||
case 0 -> getDisplayParamId(info.paramId()); // [修改] 使用显示 ID
|
||||
case 1 -> info.value();
|
||||
case 2 -> "删除"; // Button text
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* [新增] 获取用于显示的参数 ID (中文映射)
|
||||
*/
|
||||
private String getDisplayParamId(String paramId) {
|
||||
return paramIdMap.getOrDefault(paramId, paramId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCellEditable(int rowIndex, int columnIndex) {
|
||||
// "值"列 (1) 不可编辑,"操作"列 (2) 可点击 (仅允许删除)
|
||||
return columnIndex == 2;
|
||||
}
|
||||
|
||||
// 移除 setValueAt 方法,禁用修改功能。
|
||||
|
||||
/**
|
||||
* 处理单行删除操作 (用于按钮点击)。
|
||||
*/
|
||||
public void deleteRow(int rowIndex) {
|
||||
if (rowIndex >= 0 && rowIndex < data.size()) {
|
||||
RelatedParameterInfo info = data.get(rowIndex);
|
||||
String paramId = info.paramId();
|
||||
int recordIndex = info.recordIndex();
|
||||
|
||||
int confirm = JOptionPane.showConfirmDialog(KeyframeDetailsDialog.this,
|
||||
String.format("确定要删除参数 '%s' 的此关键帧记录吗?", getDisplayParamId(paramId)),
|
||||
"确认删除", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
|
||||
|
||||
if (confirm == JOptionPane.YES_OPTION) {
|
||||
// 调用 ParametersManagement 的新 API 进行精确删除
|
||||
management.removeParameterAt(modelPart, recordIndex);
|
||||
|
||||
// [修改] 不直接调用 fetchRelatedParameters,而是调用 refreshCallback,它会重新查询并刷新 UI
|
||||
refreshCallback.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理多行删除操作 (用于快捷键)。
|
||||
* @param modelRows 模型索引数组。
|
||||
*/
|
||||
public void deleteRows(int[] modelRows) {
|
||||
if (modelRows.length == 0) return;
|
||||
|
||||
// 提取要删除的 ModelPart 记录的原始索引,并按降序排序。
|
||||
List<Integer> recordIndices = Arrays.stream(modelRows)
|
||||
.mapToObj(data::get)
|
||||
.map(RelatedParameterInfo::recordIndex)
|
||||
.sorted(Collections.reverseOrder())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 确认对话框
|
||||
int confirm = JOptionPane.showConfirmDialog(KeyframeDetailsDialog.this,
|
||||
String.format("确定要删除选中的 %d 个关键帧参数记录吗?", recordIndices.size()),
|
||||
"确认批量删除", JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
|
||||
|
||||
if (confirm == JOptionPane.YES_OPTION) {
|
||||
for (int index : recordIndices) {
|
||||
// 调用 ParametersManagement 的新 API 进行精确删除
|
||||
management.removeParameterAt(modelPart, index);
|
||||
}
|
||||
|
||||
// [修改] 重新获取数据以刷新 UI (只需要一次)
|
||||
refreshCallback.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------
|
||||
// 内部类:ButtonRenderer & ButtonEditor (处理删除按钮)
|
||||
// ----------------------------------------------------------------------------------
|
||||
private class ButtonRenderer extends JButton implements TableCellRenderer {
|
||||
public ButtonRenderer() {
|
||||
setOpaque(true);
|
||||
setBackground(COLOR_ACCENT_1);
|
||||
setForeground(Color.WHITE);
|
||||
setFocusPainted(false);
|
||||
setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createLineBorder(COLOR_GRID),
|
||||
new EmptyBorder(0, 0, 0, 0)
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getTableCellRendererComponent(JTable table, Object value,
|
||||
boolean isSelected, boolean hasFocus, int row, int column) {
|
||||
setText((value == null) ? "" : value.toString());
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
private class ButtonEditor extends AbstractCellEditor implements TableCellEditor {
|
||||
private final JButton button;
|
||||
private int currentRow;
|
||||
|
||||
public ButtonEditor() {
|
||||
button = new JButton();
|
||||
button.setOpaque(true);
|
||||
button.setBackground(COLOR_ACCENT_1.darker());
|
||||
button.setForeground(Color.WHITE);
|
||||
button.setFocusPainted(false);
|
||||
button.setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createLineBorder(COLOR_GRID),
|
||||
new EmptyBorder(0, 0, 0, 0)
|
||||
));
|
||||
|
||||
button.addActionListener((ActionEvent e) -> {
|
||||
// 在 Event Dispatch Thread 中执行删除逻辑
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
fireEditingStopped();
|
||||
relatedTableModel.deleteRow(currentRow); // 调用单行删除
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component getTableCellEditorComponent(JTable table, Object value,
|
||||
boolean isSelected, int row, int column) {
|
||||
currentRow = row;
|
||||
button.setText((value == null) ? "" : value.toString());
|
||||
return button;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCellEditorValue() {
|
||||
return button.getText();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* [修改] 查询在当前关键帧值处,ModelPart 中所有已记录的参数变化,并存储在 allRelatedParameters 中。
|
||||
*/
|
||||
private void fetchRelatedParameters() {
|
||||
// 获取该 ModelPart 的所有历史记录
|
||||
Parameter fullRecord = parametersManagement.getModelPartParameters(modelPart);
|
||||
if (fullRecord == null) {
|
||||
allRelatedParameters = new ArrayList<>();
|
||||
relatedTableModel.setData(allRelatedParameters); // 刷新表格
|
||||
return;
|
||||
}
|
||||
|
||||
List<AnimationParameter> anims = fullRecord.animationParameter();
|
||||
List<Float> keyframes = fullRecord.keyframe();
|
||||
List<String> paramIds = fullRecord.paramId();
|
||||
List<Object> values = fullRecord.value();
|
||||
|
||||
int size = Math.min(keyframes.size(), Math.min(anims.size(), Math.min(paramIds.size(), values.size())));
|
||||
|
||||
List<RelatedParameterInfo> related = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
Float currentKeyframe = keyframes.get(i);
|
||||
AnimationParameter recordAnimParam = anims.get(i);
|
||||
|
||||
// 检查 1 (参数): 使用 equals 判断 AnimationParameter 是否与当前编辑的参数相等
|
||||
boolean isSameAnimationParameter = recordAnimParam != null && recordAnimParam.equals(parameter);
|
||||
|
||||
// 检查 2 (时间点): 使用 Objects.equals 判断 keyframe 是否与 originalValue 相等 (处理 null)
|
||||
boolean isSameKeyframe = Objects.equals(currentKeyframe, originalValue);
|
||||
|
||||
if (isSameAnimationParameter && isSameKeyframe) {
|
||||
// [修改] 不再排除当前正在编辑的参数本身
|
||||
related.add(new RelatedParameterInfo(
|
||||
paramIds.get(i),
|
||||
values.get(i),
|
||||
i // <-- 记录此条目在 ModelPart 完整记录中的原始索引 i
|
||||
));
|
||||
}
|
||||
}
|
||||
allRelatedParameters = related; // 存储完整列表
|
||||
applyFilter(); // 应用过滤器刷新表格
|
||||
}
|
||||
|
||||
/**
|
||||
* [新增] 根据搜索框内容过滤 allRelatedParameters 并更新表格。
|
||||
*/
|
||||
private void applyFilter() {
|
||||
String searchText = searchField.getText().trim().toLowerCase();
|
||||
|
||||
if (searchText.isEmpty()) {
|
||||
relatedTableModel.setData(allRelatedParameters);
|
||||
return;
|
||||
}
|
||||
|
||||
List<RelatedParameterInfo> filteredList = allRelatedParameters.stream()
|
||||
.filter(info -> {
|
||||
// 过滤逻辑:匹配原始 ID 或中文显示 ID
|
||||
String paramId = info.paramId().toLowerCase();
|
||||
String displayId = relatedTableModel.getDisplayParamId(info.paramId()).toLowerCase();
|
||||
|
||||
return paramId.contains(searchText) || displayId.contains(searchText);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
relatedTableModel.setData(filteredList);
|
||||
}
|
||||
|
||||
/**
|
||||
* [新增] 重新查询所有数据并应用当前过滤器。用于删除操作后的刷新。
|
||||
*/
|
||||
private void refreshTableData() {
|
||||
fetchRelatedParameters();
|
||||
}
|
||||
|
||||
|
||||
// 处理表格选中的多行删除
|
||||
private void deleteSelectedRelatedRows() {
|
||||
int[] selectedViewRows = relatedTable.getSelectedRows();
|
||||
if (selectedViewRows.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 转换视图索引到模型索引
|
||||
int[] modelRowsToDelete = new int[selectedViewRows.length];
|
||||
for(int i = 0; i < selectedViewRows.length; i++) {
|
||||
modelRowsToDelete[i] = relatedTable.convertRowIndexToModel(selectedViewRows[i]);
|
||||
}
|
||||
|
||||
// 调用批量删除方法。确认框已移至 model 内部。
|
||||
relatedTableModel.deleteRows(modelRowsToDelete);
|
||||
}
|
||||
|
||||
|
||||
private void initUI() {
|
||||
setSize(550, 450);
|
||||
setMinimumSize(new Dimension(500, 350));
|
||||
setLocationRelativeTo(getOwner());
|
||||
getContentPane().setBackground(COLOR_BACKGROUND);
|
||||
setLayout(new BorderLayout(10, 10));
|
||||
((JPanel) getContentPane()).setBorder(DIALOG_PADDING);
|
||||
|
||||
// --- 顶部信息面板 (ID, Range, Default) ---
|
||||
JPanel topPanel = new JPanel(new GridLayout(4, 2, 5, 5));
|
||||
topPanel.setBackground(COLOR_BACKGROUND);
|
||||
topPanel.add(createLabel("参数 ID:"));
|
||||
// [修改] 显示参数 ID 的中文映射
|
||||
topPanel.add(createValueLabel(relatedTableModel.getDisplayParamId(parameter.getId())));
|
||||
topPanel.add(createLabel("Model Part:"));
|
||||
topPanel.add(createValueLabel(modelPart != null ? modelPart.getName() : "N/A"));
|
||||
topPanel.add(createLabel("值域:"));
|
||||
String range = String.format("[%.3f, %.3f]", parameter.getMinValue(), parameter.getMaxValue());
|
||||
topPanel.add(createValueLabel(range));
|
||||
topPanel.add(createLabel("默认值:"));
|
||||
topPanel.add(createValueLabel(String.format("%.3f", parameter.getDefaultValue())));
|
||||
|
||||
add(topPanel, BorderLayout.NORTH);
|
||||
|
||||
// --- 中部主编辑/信息区 ---
|
||||
JSplitPane centerSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
|
||||
centerSplit.setOpaque(false);
|
||||
centerSplit.setDividerLocation(150);
|
||||
centerSplit.setDividerSize(5);
|
||||
centerSplit.setBorder(null);
|
||||
|
||||
|
||||
// 1. 关键帧值编辑面板
|
||||
JPanel editPanel = createEditPanel();
|
||||
centerSplit.setTopComponent(editPanel);
|
||||
|
||||
// 2. 相关参数列表面板
|
||||
JPanel listPanel = createRelatedParametersListPanel();
|
||||
centerSplit.setBottomComponent(listPanel);
|
||||
|
||||
add(centerSplit, BorderLayout.CENTER);
|
||||
|
||||
|
||||
// --- 底部操作栏 ---
|
||||
add(createBottomPanel(), BorderLayout.SOUTH);
|
||||
|
||||
// Esc 键关闭 = Cancel
|
||||
getRootPane().registerKeyboardAction(e -> onCancel(),
|
||||
KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
|
||||
JComponent.WHEN_IN_FOCUSED_WINDOW);
|
||||
}
|
||||
|
||||
private JPanel createEditPanel() {
|
||||
JPanel panel = new JPanel(new GridBagLayout());
|
||||
panel.setBackground(COLOR_BACKGROUND);
|
||||
panel.setBorder(BorderFactory.createTitledBorder(
|
||||
BorderFactory.createLineBorder(COLOR_GRID), "关键帧值编辑",
|
||||
0, 0, getFont(), COLOR_ACCENT_2
|
||||
));
|
||||
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.insets = new Insets(5, 5, 5, 5);
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
// 标签
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 0;
|
||||
gbc.weightx = 0.0;
|
||||
panel.add(createLabel("关键帧值:"), gbc);
|
||||
|
||||
// 文本字段
|
||||
styleTextField(valueField);
|
||||
valueField.addActionListener(e -> updateSliderFromField());
|
||||
valueField.addFocusListener(new java.awt.event.FocusAdapter() {
|
||||
@Override
|
||||
public void focusLost(java.awt.event.FocusEvent e) {
|
||||
updateSliderFromField();
|
||||
}
|
||||
});
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = 0;
|
||||
gbc.weightx = 1.0;
|
||||
panel.add(valueField, gbc);
|
||||
|
||||
// 滑块
|
||||
valueSlider.setBackground(COLOR_BACKGROUND);
|
||||
valueSlider.setForeground(COLOR_FOREGROUND);
|
||||
valueSlider.setPaintTicks(false);
|
||||
valueSlider.setPaintLabels(false);
|
||||
valueSlider.addChangeListener(e -> updateFieldFromSlider());
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 1;
|
||||
gbc.gridwidth = 2;
|
||||
gbc.weightx = 1.0;
|
||||
panel.add(valueSlider, gbc);
|
||||
|
||||
// 底部留白
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 2;
|
||||
gbc.gridwidth = 2;
|
||||
gbc.weighty = 1.0;
|
||||
panel.add(new JPanel() {{ setOpaque(false); }}, gbc);
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
private JPanel createRelatedParametersListPanel() {
|
||||
JPanel panel = new JPanel(new BorderLayout(5, 5)); // [修改] 增加边距
|
||||
panel.setBackground(COLOR_BACKGROUND);
|
||||
panel.setBorder(BorderFactory.createTitledBorder(
|
||||
BorderFactory.createLineBorder(COLOR_GRID), "在该关键帧值上所有 ModelPart 参数值",
|
||||
0, 0, getFont(), COLOR_FOREGROUND
|
||||
));
|
||||
|
||||
// --- 搜索面板 ---
|
||||
JPanel searchPanel = new JPanel(new BorderLayout(5, 0));
|
||||
searchPanel.setBackground(COLOR_BACKGROUND);
|
||||
searchPanel.add(createLabel("搜索参数 ID:"), BorderLayout.WEST);
|
||||
styleTextField(searchField);
|
||||
searchPanel.add(searchField, BorderLayout.CENTER);
|
||||
|
||||
// [新增] 搜索框事件监听
|
||||
searchField.getDocument().addDocumentListener(new DocumentListener() {
|
||||
@Override
|
||||
public void insertUpdate(DocumentEvent e) { applyFilter(); }
|
||||
@Override
|
||||
public void removeUpdate(DocumentEvent e) { applyFilter(); }
|
||||
@Override
|
||||
public void changedUpdate(DocumentEvent e) { applyFilter(); }
|
||||
});
|
||||
|
||||
panel.add(searchPanel, BorderLayout.NORTH);
|
||||
|
||||
|
||||
relatedTable.setBackground(COLOR_BACKGROUND);
|
||||
relatedTable.setForeground(COLOR_FOREGROUND);
|
||||
relatedTable.setGridColor(COLOR_GRID);
|
||||
relatedTable.getTableHeader().setBackground(COLOR_HEADER);
|
||||
relatedTable.getTableHeader().setForeground(COLOR_FOREGROUND);
|
||||
relatedTable.setFont(getFont().deriveFont(12f));
|
||||
relatedTable.setRowHeight(20);
|
||||
|
||||
// 允许批量选择
|
||||
relatedTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
|
||||
|
||||
// 值列右对齐 (columnIndex == 1)
|
||||
DefaultTableCellRenderer rightRenderer = new DefaultTableCellRenderer();
|
||||
rightRenderer.setHorizontalAlignment(JLabel.RIGHT);
|
||||
rightRenderer.setBackground(COLOR_BACKGROUND);
|
||||
rightRenderer.setForeground(COLOR_FOREGROUND);
|
||||
relatedTable.getColumnModel().getColumn(1).setCellRenderer(rightRenderer);
|
||||
|
||||
// 设置 "操作" 列的 Renderer 和 Editor (columnIndex == 2)
|
||||
relatedTable.getColumnModel().getColumn(2).setCellRenderer(new ButtonRenderer());
|
||||
relatedTable.getColumnModel().getColumn(2).setCellEditor(new ButtonEditor());
|
||||
relatedTable.getColumnModel().getColumn(2).setMaxWidth(60); // 限制按钮列宽度
|
||||
relatedTable.getColumnModel().getColumn(2).setMinWidth(60);
|
||||
|
||||
// 添加 DELETE 和 BACK_SPACE 快捷键绑定
|
||||
InputMap inputMap = relatedTable.getInputMap(JComponent.WHEN_FOCUSED);
|
||||
ActionMap actionMap = relatedTable.getActionMap();
|
||||
|
||||
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "deleteSelected");
|
||||
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0), "deleteSelected");
|
||||
|
||||
actionMap.put("deleteSelected", new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
deleteSelectedRelatedRows();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
JScrollPane scroll = new JScrollPane(relatedTable);
|
||||
scroll.setBackground(COLOR_BACKGROUND);
|
||||
scroll.getViewport().setBackground(COLOR_BACKGROUND);
|
||||
scroll.setBorder(BorderFactory.createLineBorder(COLOR_GRID));
|
||||
|
||||
panel.add(scroll, BorderLayout.CENTER);
|
||||
return panel;
|
||||
}
|
||||
|
||||
private JPanel createBottomPanel() {
|
||||
JPanel panel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0));
|
||||
panel.setBackground(COLOR_BACKGROUND);
|
||||
|
||||
JButton okButton = new JButton("确定");
|
||||
JButton cancelButton = new JButton("取消");
|
||||
styleButton(okButton);
|
||||
styleButton(cancelButton);
|
||||
|
||||
panel.add(okButton);
|
||||
panel.add(cancelButton);
|
||||
|
||||
okButton.addActionListener(e -> onOK());
|
||||
cancelButton.addActionListener(e -> onCancel());
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
private JLabel createLabel(String text) {
|
||||
JLabel label = new JLabel(text);
|
||||
label.setForeground(COLOR_FOREGROUND.darker());
|
||||
return label;
|
||||
}
|
||||
|
||||
private JLabel createValueLabel(String text) {
|
||||
JLabel label = new JLabel(text);
|
||||
label.setForeground(COLOR_FOREGROUND);
|
||||
return label;
|
||||
}
|
||||
|
||||
private void loadData(Float value) {
|
||||
// 设置滑块和文本字段的初始值
|
||||
if (value != null) {
|
||||
valueField.setText(String.format("%.6f", value));
|
||||
|
||||
float range = parameter.getMaxValue() - parameter.getMinValue();
|
||||
if (range > 0) {
|
||||
int sliderValue = (int) (((value - parameter.getMinValue()) / range) * 1000f);
|
||||
valueSlider.setValue(sliderValue);
|
||||
} else {
|
||||
valueSlider.setValue(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateFieldFromSlider() {
|
||||
float normalized = valueSlider.getValue() / 1000f;
|
||||
float value = parameter.getMinValue() + normalized * (parameter.getMaxValue() - parameter.getMinValue());
|
||||
valueField.setText(String.format("%.6f", value));
|
||||
}
|
||||
|
||||
private void updateSliderFromField() {
|
||||
try {
|
||||
float val = Float.parseFloat(valueField.getText().trim());
|
||||
// 钳位
|
||||
val = Math.max(parameter.getMinValue(), Math.min(parameter.getMaxValue(), val));
|
||||
valueField.setText(String.format("%.6f", val)); // 格式化并显示钳位后的值
|
||||
|
||||
float range = parameter.getMaxValue() - parameter.getMinValue();
|
||||
if (range > 0) {
|
||||
int sliderValue = (int) (((val - parameter.getMinValue()) / range) * 1000f);
|
||||
valueSlider.setValue(sliderValue);
|
||||
} else {
|
||||
valueSlider.setValue(0);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
JOptionPane.showMessageDialog(this, "无效的数值", "格式错误", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
private void onOK() {
|
||||
try {
|
||||
float newValue = Float.parseFloat(valueField.getText().trim());
|
||||
// 钳位
|
||||
newValue = Math.max(parameter.getMinValue(), Math.min(parameter.getMaxValue(), newValue));
|
||||
|
||||
if (originalValue != null && Math.abs(originalValue - newValue) > FLOAT_TOLERANCE) {
|
||||
// 只有值发生变化时才移除旧值
|
||||
keyframesSet.remove(originalValue);
|
||||
}
|
||||
|
||||
// 检查新值是否已存在
|
||||
if (keyframesSet.contains(newValue)) {
|
||||
// 如果是原来的值或已存在的值,直接确认
|
||||
this.confirmedValue = newValue;
|
||||
} else {
|
||||
// 添加新值
|
||||
keyframesSet.add(newValue);
|
||||
this.confirmedValue = newValue;
|
||||
}
|
||||
|
||||
dispose();
|
||||
} catch (NumberFormatException e) {
|
||||
JOptionPane.showMessageDialog(this, "请输入有效的浮点数", "格式错误", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
private void onCancel() {
|
||||
this.confirmedValue = null;
|
||||
dispose();
|
||||
}
|
||||
|
||||
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)
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 显示对话框。
|
||||
* @param owner 父窗口
|
||||
* @param parameter 动画参数
|
||||
* @param currentValue 要编辑的关键帧值
|
||||
* @param keyframesSet 关键帧集合 (用于添加/移除操作)
|
||||
* @param parametersManagement 参数管理实例
|
||||
* @param modelPart 模型部件
|
||||
* @return 如果用户点击确定,返回新的关键帧值;否则返回 null。
|
||||
*/
|
||||
public static Float showEditor(Window owner, AnimationParameter parameter, Float currentValue, SortedSet<Float> keyframesSet,
|
||||
ParametersManagement parametersManagement, ModelPart modelPart) {
|
||||
if (parameter == null || currentValue == null || parametersManagement == null || modelPart == null) return null;
|
||||
KeyframeDetailsDialog dialog = new KeyframeDetailsDialog(owner, parameter, currentValue, keyframesSet, parametersManagement, modelPart);
|
||||
dialog.setVisible(true);
|
||||
return dialog.confirmedValue;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.chuangzhou.vivid2D.render.awt;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.awt.manager.ParametersManagement;
|
||||
import com.chuangzhou.vivid2D.render.model.AnimationParameter;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.Border;
|
||||
@@ -36,6 +38,8 @@ public class KeyframeEditorDialog extends JDialog {
|
||||
* 临时存储编辑,直到用户点击 "OK"
|
||||
*/
|
||||
private final TreeSet<Float> tempKeyframes;
|
||||
private final ParametersManagement parametersManagement;
|
||||
private final ModelPart modelPart;
|
||||
|
||||
// 用于跟踪 OK/Cancel 状态
|
||||
private boolean confirmed = false;
|
||||
@@ -345,15 +349,17 @@ public class KeyframeEditorDialog extends JDialog {
|
||||
}
|
||||
|
||||
|
||||
public KeyframeEditorDialog(Window owner, AnimationParameter parameter) {
|
||||
public KeyframeEditorDialog(Window owner, AnimationParameter parameter, ParametersManagement parametersManagement, ModelPart modelPart) {
|
||||
super(owner, "关键帧编辑器: " + parameter.getId(), ModalityType.APPLICATION_MODAL);
|
||||
this.parameter = parameter;
|
||||
this.modelPart = modelPart;
|
||||
|
||||
this.tempKeyframes = new TreeSet<>(parameter.getKeyframes());
|
||||
|
||||
this.ruler = new EditorRuler();
|
||||
this.tableModel = new KeyframeTableModel();
|
||||
this.keyframeTable = new JTable(tableModel);
|
||||
this.parametersManagement = parametersManagement;
|
||||
|
||||
initUI();
|
||||
updateAllUI();
|
||||
@@ -396,16 +402,12 @@ public class KeyframeEditorDialog extends JDialog {
|
||||
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();
|
||||
}
|
||||
//int col = keyframeTable.columnAtPoint(e.getPoint()); // 不需要列判断
|
||||
|
||||
if (row >= 0) {
|
||||
Float selectedValue = tableModel.getValueAtRow(row);
|
||||
if (selectedValue != null) {
|
||||
showKeyframeDetailsDialog(selectedValue, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -414,6 +416,37 @@ public class KeyframeEditorDialog extends JDialog {
|
||||
// ------------------------------------
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示关键帧详细信息对话框
|
||||
*/
|
||||
private void showKeyframeDetailsDialog(Float currentValue, int currentRow) {
|
||||
// KeyframeDetailsDialog 将负责处理值的更新和在 tempKeyframes 中的替换
|
||||
Float newValue = KeyframeDetailsDialog.showEditor(
|
||||
this,
|
||||
parameter,
|
||||
currentValue,
|
||||
tempKeyframes, // 将 Set 传递给子对话框进行修改
|
||||
parametersManagement, // 传递 ParametersManagement
|
||||
modelPart // 传递 ModelPart
|
||||
);
|
||||
|
||||
if (newValue != null) {
|
||||
// 对话框已更新 tempKeyframes
|
||||
|
||||
// 彻底刷新UI (因为 Set 排序可能已改变)
|
||||
updateAllUI();
|
||||
|
||||
// 刷新后,重新定位并选中新值的行
|
||||
int newRow = tableModel.getRowForValue(newValue);
|
||||
if (newRow != -1) {
|
||||
keyframeTable.setRowSelectionInterval(newRow, newRow);
|
||||
keyframeTable.scrollRectToVisible(keyframeTable.getCellRect(newRow, 0, true));
|
||||
} else {
|
||||
keyframeTable.clearSelection();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private JPanel createListActionsPanel() {
|
||||
JPanel actionsPanel = new JPanel();
|
||||
actionsPanel.setBackground(COLOR_BACKGROUND);
|
||||
@@ -668,9 +701,9 @@ public class KeyframeEditorDialog extends JDialog {
|
||||
/**
|
||||
* 显示对话框。
|
||||
*/
|
||||
public static boolean showEditor(Window owner, AnimationParameter parameter) {
|
||||
public static boolean showEditor(Window owner, AnimationParameter parameter,ParametersManagement parametersManagement, ModelPart modelPart) {
|
||||
if (parameter == null) return false;
|
||||
KeyframeEditorDialog dialog = new KeyframeEditorDialog(owner, parameter);
|
||||
KeyframeEditorDialog dialog = new KeyframeEditorDialog(owner, parameter,parametersManagement,modelPart);
|
||||
dialog.setVisible(true);
|
||||
return dialog.isConfirmed();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.chuangzhou.vivid2D.render.awt;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.awt.manager.ParametersManagement;
|
||||
import com.chuangzhou.vivid2D.render.awt.tools.VertexDeformationTool;
|
||||
import com.chuangzhou.vivid2D.render.model.AnimationParameter;
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
@@ -33,7 +34,7 @@ public class ParametersPanel extends JPanel {
|
||||
private final ModelRenderPanel renderPanel;
|
||||
private final Model2D model;
|
||||
private AnimationParameter selectParameter;
|
||||
|
||||
public ParametersManagement parametersManagement;
|
||||
// UI
|
||||
private final CardLayout cardLayout = new CardLayout();
|
||||
private final JPanel cardRoot = new JPanel(cardLayout);
|
||||
@@ -152,7 +153,7 @@ public class ParametersPanel extends JPanel {
|
||||
AnimationParameter p = parameterList.getSelectedValue();
|
||||
if (p != null) {
|
||||
// 弹出编辑器
|
||||
KeyframeEditorDialog.showEditor(SwingUtilities.getWindowAncestor(ParametersPanel.this), p);
|
||||
KeyframeEditorDialog.showEditor(SwingUtilities.getWindowAncestor(ParametersPanel.this), p, parametersManagement, currentPart);
|
||||
// 编辑器关闭后,刷新滑块的显示
|
||||
valueSlider.repaint();
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class ParametersManagement {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ParametersManagement.class);
|
||||
@@ -26,6 +28,7 @@ public class ParametersManagement {
|
||||
public static ParametersManagement getInstance(ParametersPanel parametersPanel) {
|
||||
String managementFilePath = parametersPanel.getRenderPanel().getGlContextManager().getModelPath() + ".data";
|
||||
File managementFile = new File(managementFilePath);
|
||||
ParametersManagement instance = new ParametersManagement(parametersPanel);
|
||||
if (managementFile.exists()) {
|
||||
logger.info("已找到参数管理数据文件: {}", managementFilePath);
|
||||
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(managementFile))) {
|
||||
@@ -35,9 +38,10 @@ public class ParametersManagement {
|
||||
List<ModelPart> parts = parametersPanel.getRenderPanel().getModel().getParts();
|
||||
ParametersManagement management = managementData.toParametersManagement(parametersPanel, parts);
|
||||
//logger.info("参数管理数据转换成功: {}", management);
|
||||
ParametersManagement instance = new ParametersManagement(parametersPanel);
|
||||
instance = new ParametersManagement(parametersPanel);
|
||||
instance.oldValues = management.oldValues;
|
||||
//logger.info("参数管理数据加载成功: {}", management.oldValues);
|
||||
parametersPanel.parametersManagement = instance;
|
||||
return instance;
|
||||
} else {
|
||||
logger.warn("加载参数管理数据失败: 预期第二个对象为ParametersManagementData,但实际为 {}", o != null ? o.getClass().getName() : "null");
|
||||
@@ -48,7 +52,8 @@ public class ParametersManagement {
|
||||
} else {
|
||||
logger.info("未找到参数管理数据文件 {},创建新的参数管理实例", managementFilePath);
|
||||
}
|
||||
return new ParametersManagement(parametersPanel);
|
||||
parametersPanel.parametersManagement = instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,6 +93,46 @@ public class ParametersManagement {
|
||||
return parametersPanel.getSelectParameter().copy();
|
||||
}
|
||||
|
||||
/**
|
||||
* 精确地从 ModelPart 的记录中删除指定索引的参数条目。
|
||||
* * @param targetModelPart 目标 ModelPart
|
||||
* @param indexToRemove 在该 ModelPart 记录内部的索引
|
||||
*/
|
||||
public void removeParameterAt(ModelPart targetModelPart, int indexToRemove) {
|
||||
for (int i = 0; i < oldValues.size(); i++) {
|
||||
Parameter existingParameter = oldValues.get(i);
|
||||
if (existingParameter.modelPart().equals(targetModelPart)) {
|
||||
int size = existingParameter.keyframe().size();
|
||||
if (indexToRemove >= 0 && indexToRemove < size) {
|
||||
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());
|
||||
newAnimationParameters.remove(indexToRemove);
|
||||
newParamIds.remove(indexToRemove);
|
||||
newValues.remove(indexToRemove);
|
||||
newKeyframes.remove(indexToRemove);
|
||||
newIsKeyframes.remove(indexToRemove);
|
||||
if (newKeyframes.isEmpty()) {
|
||||
oldValues.remove(i);
|
||||
} else {
|
||||
Parameter updatedParameter = new Parameter(
|
||||
targetModelPart,
|
||||
newAnimationParameters,
|
||||
newParamIds,
|
||||
newValues,
|
||||
newKeyframes,
|
||||
newIsKeyframes
|
||||
);
|
||||
oldValues.set(i, updatedParameter);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听参数变化 (强制添加新记录,即使 paramId 已存在)
|
||||
* 如果列表中已存在相同 modelPart 的记录,则添加新参数到该记录的列表尾部;否则添加新记录。
|
||||
@@ -96,43 +141,80 @@ public class ParametersManagement {
|
||||
* @param value 最终值
|
||||
*/
|
||||
public void broadcast(ModelPart modelPart, String paramId, Object value) {
|
||||
if (getSelectParameter() == null){
|
||||
if (getSelectParameter() == null) {
|
||||
return;
|
||||
}
|
||||
boolean isKeyframe = getSelectedKeyframe(false) != null;
|
||||
// 获取当前正在编辑的 AnimationParameter 实例
|
||||
AnimationParameter currentAnimParam = getSelectParameter();
|
||||
|
||||
// 获取当前的关键帧时间点 (Float) 和是否为关键帧 (Boolean)
|
||||
Float currentKeyframe = getSelectedKeyframe(false);
|
||||
|
||||
// 如果当前没有选中的关键帧,通常我们不应该记录,但为了安全,先检查 null
|
||||
if (currentKeyframe == null) {
|
||||
return;
|
||||
}
|
||||
// 重新判断是否为关键帧,确保 isKeyframe 准确
|
||||
boolean isKeyframe = currentAnimParam.getKeyframes().contains(currentKeyframe);
|
||||
|
||||
// 查找是否已存在该ModelPart的记录
|
||||
for (int i = 0; i < oldValues.size(); i++) {
|
||||
Parameter existingParameter = oldValues.get(i);
|
||||
|
||||
// 步骤 1: 找到对应的 ModelPart
|
||||
if (existingParameter.modelPart().equals(modelPart)) {
|
||||
// 更新现有记录(复制所有列表以确保记录的不可变性)
|
||||
// 步骤 2: 复制所有列表(保持不可变性)
|
||||
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());
|
||||
|
||||
newAnimationParameters.add(getSelectParameter());
|
||||
newParamIds.add(paramId);
|
||||
newValues.add(value);
|
||||
newKeyframes.add(currentKeyframe);
|
||||
newIsKeyframes.add(isKeyframe);
|
||||
// 步骤 3: 查找相同 keyframe + paramId + AnimationParameter 的记录
|
||||
int existingIndex = -1;
|
||||
for (int j = 0; j < newKeyframes.size(); j++) {
|
||||
// 检查 keyframe 是否相同 (使用 Objects.equals 比较 Float)
|
||||
boolean keyframeMatches = Objects.equals(newKeyframes.get(j), currentKeyframe);
|
||||
|
||||
Parameter updatedParameter = new Parameter(modelPart, newAnimationParameters, newParamIds, newValues, newKeyframes, newIsKeyframes); // NEW
|
||||
// 检查 paramId 是否相同
|
||||
boolean paramIdMatches = paramId.equals(newParamIds.get(j));
|
||||
|
||||
// 检查 AnimationParameter 是否相同 (使用 equals)
|
||||
AnimationParameter recordAnimParam = newAnimationParameters.get(j);
|
||||
boolean animParamMatches = recordAnimParam != null && recordAnimParam.equals(currentAnimParam);
|
||||
|
||||
// 如果时间点、参数ID和参数实例都匹配,则找到了现有记录
|
||||
if (keyframeMatches && paramIdMatches && animParamMatches) {
|
||||
existingIndex = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingIndex != -1) {
|
||||
// 找到了相同的记录位置: 执行 UPDATE (设置) 操作
|
||||
newValues.set(existingIndex, value);
|
||||
} else {
|
||||
// 没有找到相同的记录: 执行 ADD (新增) 操作
|
||||
newAnimationParameters.add(currentAnimParam);
|
||||
newParamIds.add(paramId);
|
||||
newValues.add(value);
|
||||
newKeyframes.add(currentKeyframe);
|
||||
newIsKeyframes.add(isKeyframe);
|
||||
}
|
||||
Parameter updatedParameter = new Parameter(modelPart, newAnimationParameters, newParamIds, newValues, newKeyframes, newIsKeyframes);
|
||||
oldValues.set(i, updatedParameter);
|
||||
return;
|
||||
return; // ModelPart 记录已处理
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到现有记录,创建新记录
|
||||
// 如果没有找到 ModelPart 的现有记录,创建新记录
|
||||
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)
|
||||
Collections.singletonList(currentAnimParam),
|
||||
Collections.singletonList(paramId),
|
||||
Collections.singletonList(value),
|
||||
Collections.singletonList(currentKeyframe),
|
||||
Collections.singletonList(isKeyframe)
|
||||
);
|
||||
oldValues.add(parameter);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class MeshTextureUtil {
|
||||
|
||||
public static Mesh2D createQuadForImage(BufferedImage img, String meshName) {
|
||||
float w = img.getWidth();
|
||||
float h = img.getHeight();
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.chuangzhou.vivid2D.render.awt.manager.data.ParametersManagementData;
|
||||
import com.chuangzhou.vivid2D.render.model.data.ModelData;
|
||||
import com.chuangzhou.vivid2D.render.model.data.ModelMetadata;
|
||||
import com.chuangzhou.vivid2D.render.model.util.*;
|
||||
import com.chuangzhou.vivid2D.util.ModelDataJsonConverter;
|
||||
import org.joml.Matrix3f;
|
||||
|
||||
import javax.swing.tree.DefaultMutableTreeNode;
|
||||
@@ -516,8 +517,10 @@ public class Model2D {
|
||||
try {
|
||||
ModelData data = serialize();
|
||||
data.saveToFile(filePath);
|
||||
String jsonFilePath = getJsonFilePath(filePath);
|
||||
ModelDataJsonConverter.convert(filePath, jsonFilePath, false);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to save model to: " + filePath, e);
|
||||
throw new RuntimeException("Failed to save model and convert to JSON: " + filePath, e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,11 +551,22 @@ public class Model2D {
|
||||
try {
|
||||
ModelData data = serialize();
|
||||
data.saveToCompressedFile(filePath);
|
||||
String jsonFilePath = getJsonFilePath(filePath);
|
||||
ModelDataJsonConverter.convert(filePath, jsonFilePath, true);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to save compressed model to: " + filePath, e);
|
||||
throw new RuntimeException("Failed to save compressed model and convert to JSON: " + filePath, e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getJsonFilePath(String originalFilePath) {
|
||||
int lastDotIndex = originalFilePath.lastIndexOf('.');
|
||||
if (lastDotIndex > 0) {
|
||||
String baseName = originalFilePath.substring(0, lastDotIndex);
|
||||
return baseName + ".json";
|
||||
} else {
|
||||
return originalFilePath + ".json";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从压缩文件加载模型
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.chuangzhou.vivid2D.util;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.awt.manager.data.LayerOperationManagerData;
|
||||
import com.chuangzhou.vivid2D.render.awt.manager.data.ParametersManagementData;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.ObjectInputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class ManagementDataToJsonConverter {
|
||||
/**
|
||||
* 从 Java 序列化文件加载两个对象,并将它们组合成一个 JSON 对象保存。
|
||||
*
|
||||
* @param inputFilePath Java 序列化文件(.data)的路径
|
||||
* @param outputFilePath 目标 JSON 文件(.json)的路径
|
||||
*/
|
||||
public static void convert(String inputFilePath, String outputFilePath) {
|
||||
LayerOperationManagerData layerData = null;
|
||||
ParametersManagementData managementData = null;
|
||||
|
||||
File inputFile = new File(inputFilePath);
|
||||
if (!inputFile.exists()) {
|
||||
System.err.println("错误:输入文件未找到:" + inputFilePath);
|
||||
return;
|
||||
}
|
||||
|
||||
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(inputFile))) {
|
||||
layerData = (LayerOperationManagerData) ois.readObject();
|
||||
System.out.println("成功读取第一个对象: LayerOperationManagerData");
|
||||
managementData = (ParametersManagementData) ois.readObject();
|
||||
System.out.println("成功读取第二个对象: ParametersManagementData");
|
||||
|
||||
} catch (IOException e) {
|
||||
System.err.println("读取序列化文件失败(可能是文件已损坏或写入方式不匹配): " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return;
|
||||
} catch (ClassNotFoundException e) {
|
||||
System.err.println("无法找到类定义,请检查类路径是否包含 LayerOperationManagerData 和 ParametersManagementData: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Map<String, Object> dataMap = new HashMap<>();
|
||||
dataMap.put("layerData", layerData);
|
||||
dataMap.put("parametersManagementData", managementData);
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
// ==============================================================
|
||||
// 关键修改:禁用 FAIL_ON_EMPTY_BEANS 以避免 InvalidDefinitionException
|
||||
// 但会导致 LayerInfo 内部数据丢失
|
||||
mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
|
||||
// ==============================================================
|
||||
|
||||
mapper.enable(SerializationFeature.INDENT_OUTPUT);
|
||||
mapper.writeValue(new File(outputFilePath), dataMap);
|
||||
System.out.println("成功将序列化数据转换为 JSON 并保存至: " + outputFilePath);
|
||||
|
||||
} catch (IOException e) {
|
||||
System.err.println("写入 JSON 文件失败: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
String input = "C:\\Users\\Administrator\\Desktop\\testing.model.data";
|
||||
String output = "C:\\Users\\Administrator\\Desktop\\management_data.json";
|
||||
convert(input, output);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.chuangzhou.vivid2D.util;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.data.ModelData; // 确保导入了你的 ModelData 类
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 将 Java 序列化的 ModelData 文件转换为 JSON 格式的工具。
|
||||
*/
|
||||
public class ModelDataJsonConverter {
|
||||
|
||||
/**
|
||||
* 从 Java 序列化文件加载 ModelData,并将其保存为 JSON 文件。
|
||||
* * @param inputFilePath Java 序列化文件(.model)的路径
|
||||
* @param outputFilePath 目标 JSON 文件(.json)的路径
|
||||
* @param isCompressed 输入文件是否使用了 GZIP 压缩(ModelData.saveToCompressedFile 生成)
|
||||
*/
|
||||
public static void convert(String inputFilePath, String outputFilePath, boolean isCompressed) {
|
||||
ModelData modelData;
|
||||
|
||||
try {
|
||||
File inputFile = new File(inputFilePath);
|
||||
|
||||
// 1. 从 Java 序列化文件加载 ModelData 对象
|
||||
if (isCompressed) {
|
||||
// 使用 ModelData 中已有的 loadFromCompressedFile 方法
|
||||
modelData = ModelData.loadFromCompressedFile(inputFile);
|
||||
System.out.println("成功从压缩文件加载模型数据: " + modelData.getName());
|
||||
} else {
|
||||
// 使用 ModelData 中已有的 loadFromFile 方法
|
||||
modelData = ModelData.loadFromFile(inputFile);
|
||||
System.out.println("成功从标准序列化文件加载模型数据: " + modelData.getName());
|
||||
}
|
||||
|
||||
// 2. 将对象转换为 JSON 格式
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
|
||||
// 启用 Pretty Print 使 JSON 文件格式化,方便阅读和调试
|
||||
mapper.enable(SerializationFeature.INDENT_OUTPUT);
|
||||
|
||||
// 3. 将 JSON 写入文件
|
||||
mapper.writeValue(new File(outputFilePath), modelData);
|
||||
|
||||
System.out.println("成功将数据转换为 JSON 并保存至: " + outputFilePath);
|
||||
|
||||
} catch (IOException e) {
|
||||
System.err.println("文件操作失败: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
} catch (ClassNotFoundException e) {
|
||||
System.err.println("类定义未找到(ModelData 可能已更改): " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
} catch (Exception e) {
|
||||
System.err.println("转换过程中发生未知错误: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// 示例运行入口
|
||||
public static void main(String[] args) {
|
||||
String input = "C:\\Users\\Administrator\\Desktop\\testing.model";
|
||||
String output = "C:\\Users\\Administrator\\Desktop\\model.json";
|
||||
convert(input, output, false);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.util.SecondaryVertex;
|
||||
import com.chuangzhou.vivid2D.util.ManagementDataToJsonConverter;
|
||||
import com.chuangzhou.vivid2D.util.ModelDataJsonConverter;
|
||||
import jnafilechooser.api.JnaFileChooser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@@ -59,14 +61,12 @@ public class MainWindow extends JFrame {
|
||||
this.transformPanel = new TransformPanel(renderPanel);
|
||||
this.parametersPanel = new ParametersPanel(renderPanel);
|
||||
this.partInfoPanel = new ModelPartInfoPanel(renderPanel);
|
||||
|
||||
// 【新增】初始化 SecondaryVertexPanel
|
||||
this.secondaryVertexPanel = new SecondaryVertexPanel();
|
||||
|
||||
createMenuBar();
|
||||
createToolBar();
|
||||
createMainLayout();
|
||||
createStatusBar(); // 确保在构造函数中调用,以便进度条被初始化
|
||||
createStatusBar();
|
||||
setEditComponentsEnabled(false);
|
||||
setupInitialListeners();
|
||||
setSize(1600, 900);
|
||||
@@ -174,6 +174,7 @@ public class MainWindow extends JFrame {
|
||||
JScrollPane layerScroll = new JScrollPane(layerPanel);
|
||||
layerScroll.setMinimumSize(new Dimension(240, 100));
|
||||
layerScroll.setPreferredSize(new Dimension(260, 600));
|
||||
layerScroll.setBorder(BorderFactory.createTitledBorder("图层"));
|
||||
|
||||
JPanel centerPanelWrapper = new JPanel(new BorderLayout());
|
||||
centerPanelWrapper.add(renderPanel, BorderLayout.CENTER);
|
||||
@@ -194,7 +195,7 @@ public class MainWindow extends JFrame {
|
||||
JSplitPane infoSplit = new JSplitPane(
|
||||
JSplitPane.VERTICAL_SPLIT,
|
||||
partInfoPanel,
|
||||
secondaryVertexPanel // 【新增】加入二级顶点面板
|
||||
secondaryVertexPanel // 加入二级顶点面板
|
||||
);
|
||||
infoSplit.setResizeWeight(0.5);
|
||||
infoSplit.setOneTouchExpandable(true);
|
||||
@@ -203,6 +204,7 @@ public class MainWindow extends JFrame {
|
||||
// 右侧面板从上到下:参数、变换控制、顶点信息
|
||||
JSplitPane rightPanelSplit = getjSplitPane(paramScroll, transformScroll, infoSplit);
|
||||
|
||||
// 【修改主分割】使用新的 leftPanelSplit 替换原来的 layerScroll
|
||||
JSplitPane mainSplit = getjSplitPane(new JSplitPane(
|
||||
JSplitPane.HORIZONTAL_SPLIT,
|
||||
centerPanelWrapper,
|
||||
@@ -212,12 +214,16 @@ public class MainWindow extends JFrame {
|
||||
add(mainSplit, BorderLayout.CENTER);
|
||||
}
|
||||
|
||||
private static @NotNull JSplitPane getjSplitPane(JSplitPane HORIZONTAL_SPLIT, double value, int horizontalSplit, JScrollPane layerScroll, double value1) {
|
||||
/**
|
||||
* 辅助方法:创建主分割面板。
|
||||
* 修复:将第四个参数从 JScrollPane 更改为 Component。
|
||||
*/
|
||||
private static @NotNull JSplitPane getjSplitPane(JSplitPane HORIZONTAL_SPLIT, double value, int horizontalSplit, Component componentForLeft, double value1) {
|
||||
HORIZONTAL_SPLIT.setResizeWeight(value);
|
||||
HORIZONTAL_SPLIT.setOneTouchExpandable(true);
|
||||
JSplitPane mainSplit = new JSplitPane(
|
||||
horizontalSplit,
|
||||
layerScroll,
|
||||
componentForLeft, // 接受 JSplitPane 或 JScrollPane
|
||||
HORIZONTAL_SPLIT
|
||||
);
|
||||
mainSplit.setResizeWeight(value1);
|
||||
@@ -225,7 +231,7 @@ public class MainWindow extends JFrame {
|
||||
return mainSplit;
|
||||
}
|
||||
|
||||
// 【修改】调整右侧面板的布局逻辑
|
||||
// 辅助方法:调整右侧面板的布局逻辑
|
||||
private @NotNull JSplitPane getjSplitPane(JScrollPane paramScroll, JScrollPane transformScroll, JSplitPane infoSplit) {
|
||||
// 上层分割:参数面板 (上) + 下层分割 (下)
|
||||
JSplitPane upperSplit = new JSplitPane(
|
||||
@@ -324,7 +330,8 @@ public class MainWindow extends JFrame {
|
||||
transformPanel.setSelectedParts(selectedPart);
|
||||
if (!selectedPart.isEmpty()) {
|
||||
setModelModified(true);
|
||||
partInfoPanel.updatePanel(selectedPart.get(0));
|
||||
ModelPart selected = selectedPart.get(0);
|
||||
partInfoPanel.updatePanel(selected);
|
||||
} else {
|
||||
partInfoPanel.updatePanel(null);
|
||||
}
|
||||
@@ -377,7 +384,7 @@ public class MainWindow extends JFrame {
|
||||
transformPanel.setEnabled(enabled);
|
||||
parametersPanel.setEnabled(enabled);
|
||||
partInfoPanel.setEnabled(enabled);
|
||||
secondaryVertexPanel.setEnabled(enabled); // 【新增】
|
||||
secondaryVertexPanel.setEnabled(enabled);
|
||||
renderPanel.setEnabled(enabled);
|
||||
for (Component comp : menuBar.getComponents()) {
|
||||
if (comp instanceof JMenu menu) {
|
||||
@@ -490,20 +497,20 @@ public class MainWindow extends JFrame {
|
||||
@Override
|
||||
protected Void doInBackground() throws Exception {
|
||||
if (renderPanel.getModel() != null) {
|
||||
System.out.println("正在保存模型: " + currentModelPath);
|
||||
renderPanel.getModel().saveToFile(currentModelPath);
|
||||
}
|
||||
LayerOperationManager layerManager = layerPanel.getLayerOperationManager();
|
||||
LayerOperationManagerData layerData = new LayerOperationManagerData(layerManager.layerMetadata);
|
||||
ParametersManagementData managementData = new ParametersManagementData(renderPanel.getParametersManagement());
|
||||
String managementFilePath = currentModelPath + ".data";
|
||||
String managementJsonFilePath = managementFilePath + ".json";
|
||||
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(managementFilePath))) {
|
||||
oos.writeObject(layerData);
|
||||
oos.writeObject(managementData);
|
||||
} catch (IOException ex) {
|
||||
// 必须在 doInBackground 中处理或抛出
|
||||
throw ex;
|
||||
}
|
||||
ManagementDataToJsonConverter.convert(managementFilePath, managementJsonFilePath);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user