feat(render): 实现关键帧详细编辑对话框及相关数据转换工具- 新增 KeyframeDetailsDialog 类,用于编辑单个关键帧值并管理同时间点的其他参数

- 添加参数ID中文映射显示功能,提升用户界面友好性
- 实现同一时间点多个参数的批量删除和快捷键支持
- 集成搜索过滤功能,便于查找特定参数- 新增 ManagementDataToJsonConverter 工具类,支持将序列化管理数据转为JSON格式
- 添加 ModelDataJsonConverter 工具类,支持模型数据序列化文件转JSON
- 修改 MainWindow 保存逻辑,自动生成对应的JSON数据文件-优化界面布局和组件结构,改善用户体验
This commit is contained in:
tzdwindows 7
2025-11-08 18:04:19 +08:00
parent 3b4b1b1b26
commit c72fc19602
9 changed files with 1010 additions and 47 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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";
}
}
/**
* 从压缩文件加载模型

View File

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

View File

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

View File

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