diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/KeyframeDetailsDialog.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/KeyframeDetailsDialog.java new file mode 100644 index 0000000..a68ada4 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/KeyframeDetailsDialog.java @@ -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 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 allRelatedParameters = new ArrayList<>(); + + + public KeyframeDetailsDialog(Window owner, AnimationParameter parameter, Float value, SortedSet 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 data = new ArrayList<>(); + private final ModelPart modelPart; + private final ParametersManagement management; + private final Runnable refreshCallback; + + // [新增] 参数ID 中文映射 + private final Map 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 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 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 anims = fullRecord.animationParameter(); + List keyframes = fullRecord.keyframe(); + List paramIds = fullRecord.paramId(); + List values = fullRecord.value(); + + int size = Math.min(keyframes.size(), Math.min(anims.size(), Math.min(paramIds.size(), values.size()))); + + List 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 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 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; + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/KeyframeEditorDialog.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/KeyframeEditorDialog.java index 6e7f4ad..ccf52ef 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/KeyframeEditorDialog.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/KeyframeEditorDialog.java @@ -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 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(); } diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/ParametersPanel.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/ParametersPanel.java index 93cc247..542a520 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/ParametersPanel.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/ParametersPanel.java @@ -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(); } diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ParametersManagement.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ParametersManagement.java index 9074be0..b3c6a49 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ParametersManagement.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/manager/ParametersManagement.java @@ -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 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 newAnimationParameters = new ArrayList<>(existingParameter.animationParameter()); + List newParamIds = new ArrayList<>(existingParameter.paramId()); + List newValues = new ArrayList<>(existingParameter.value()); + List newKeyframes = new ArrayList<>(existingParameter.keyframe()); + List 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 newAnimationParameters = new ArrayList<>(existingParameter.animationParameter()); List newParamIds = new ArrayList<>(existingParameter.paramId()); List newValues = new ArrayList<>(existingParameter.value()); List newKeyframes = new ArrayList<>(existingParameter.keyframe()); List 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); } diff --git a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/MeshTextureUtil.java b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/MeshTextureUtil.java index b33bc5a..619a4b9 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/awt/util/MeshTextureUtil.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/awt/util/MeshTextureUtil.java @@ -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(); diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java b/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java index b1efd5a..9cd44eb 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/Model2D.java @@ -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"; + } + } /** * 从压缩文件加载模型 diff --git a/src/main/java/com/chuangzhou/vivid2D/util/ManagementDataToJsonConverter.java b/src/main/java/com/chuangzhou/vivid2D/util/ManagementDataToJsonConverter.java new file mode 100644 index 0000000..d73a58a --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/util/ManagementDataToJsonConverter.java @@ -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 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); + } +} diff --git a/src/main/java/com/chuangzhou/vivid2D/util/ModelDataJsonConverter.java b/src/main/java/com/chuangzhou/vivid2D/util/ModelDataJsonConverter.java new file mode 100644 index 0000000..15928f6 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/util/ModelDataJsonConverter.java @@ -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); + } +} \ No newline at end of file diff --git a/src/main/java/com/chuangzhou/vivid2D/window/MainWindow.java b/src/main/java/com/chuangzhou/vivid2D/window/MainWindow.java index c4b94c0..5843583 100644 --- a/src/main/java/com/chuangzhou/vivid2D/window/MainWindow.java +++ b/src/main/java/com/chuangzhou/vivid2D/window/MainWindow.java @@ -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; }