diff --git a/core/resources/l10n/messages.properties b/core/resources/l10n/messages.properties index 67d9018fb..101c4e961 100644 --- a/core/resources/l10n/messages.properties +++ b/core/resources/l10n/messages.properties @@ -1757,7 +1757,7 @@ Warning.ZERO_RADIUS_BODY = Zero length bodies may not result in accurate simulat ! Scale dialog ScaleDialog.lbl.scaleRocket = Entire rocket ScaleDialog.lbl.scaleSubselection = Selection and all subcomponents -ScaleDialog.lbl.scaleSelection = Only selected component +ScaleDialog.lbl.scaleSelection = Only selected component(s) ScaleDialog.title = Scale design ScaleDialog.lbl.scale = Scale: ScaleDialog.lbl.scale.ttip = Select whether to scale the entire design or only the selected component @@ -1769,6 +1769,8 @@ ScaleDialog.lbl.scaleTo = to ScaleDialog.lbl.scaleFromTo.ttip = Define the scaling based on an original and resulting length. ScaleDialog.checkbox.scaleMass = Update explicit mass values ScaleDialog.checkbox.scaleMass.ttip = Scale mass component and override mass values by the cube of the scaling factor +ScaleDialog.checkbox.scaleOffsets = Scale component offsets +ScaleDialog.checkbox.scaleOffsets.ttip = Also scale offsets between components ScaleDialog.button.scale = Scale ScaleDialog.undo.scaleRocket = Scale rocket ScaleDialog.undo.scaleComponent = Scale component diff --git a/swing/src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java b/swing/src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java index 3b7da656d..0a73dd8dd 100644 --- a/swing/src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java +++ b/swing/src/net/sf/openrocket/gui/configdialog/FreeformFinSetConfig.java @@ -17,6 +17,7 @@ import java.io.OutputStreamWriter; import java.io.Writer; import java.util.ArrayList; import java.util.EventObject; +import java.util.List; import javax.swing.JButton; import javax.swing.JComboBox; @@ -241,7 +242,7 @@ public class FreeformFinSetConfig extends FinSetConfig { @Override public void actionPerformed(ActionEvent e) { log.info(Markers.USER_MARKER, "Scaling free-form fin"); - ScaleDialog dialog = new ScaleDialog(document, finset, SwingUtilities.getWindowAncestor(FreeformFinSetConfig.this), true); + ScaleDialog dialog = new ScaleDialog(document, new ArrayList<>(List.of(finset)), SwingUtilities.getWindowAncestor(FreeformFinSetConfig.this), true); dialog.setVisible(true); dialog.dispose(); } diff --git a/swing/src/net/sf/openrocket/gui/dialogs/ScaleDialog.java b/swing/src/net/sf/openrocket/gui/dialogs/ScaleDialog.java index 6a94441b0..baea99547 100644 --- a/swing/src/net/sf/openrocket/gui/dialogs/ScaleDialog.java +++ b/swing/src/net/sf/openrocket/gui/dialogs/ScaleDialog.java @@ -3,7 +3,10 @@ package net.sf.openrocket.gui.dialogs; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.ItemEvent; +import java.awt.event.ItemListener; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -194,22 +197,23 @@ public class ScaleDialog extends JDialog { private final DoubleModel toField = new DoubleModel(0, UnitGroup.UNITS_LENGTH, 0); private final OpenRocketDocument document; - private final RocketComponent selection; + private final List selection; private final boolean onlySelection; private JComboBox selectionOption; private JCheckBox scaleMassValues; + private JCheckBox scaleOffsets; private boolean changing = false; - + /** * Sole constructor. * * @param document the document to modify. - * @param selection the currently selected component (or null if none selected). + * @param selection the currently selected componentents (or null if none selected). * @param parent the parent window. */ - public ScaleDialog(OpenRocketDocument document, RocketComponent selection, Window parent) { + public ScaleDialog(OpenRocketDocument document, List selection, Window parent) { this(document, selection, parent, false); } @@ -221,7 +225,7 @@ public class ScaleDialog extends JDialog { * @param parent the parent window. * @param onlySelection true to only allow scaling on the selected component (not the whole rocket) */ - public ScaleDialog(OpenRocketDocument document, RocketComponent selection, Window parent, Boolean onlySelection) { + public ScaleDialog(OpenRocketDocument document, List selection, Window parent, Boolean onlySelection) { super(parent, trans.get("title"), ModalityType.APPLICATION_MODAL); this.document = document; @@ -236,10 +240,21 @@ public class ScaleDialog extends JDialog { List options = new ArrayList(); if (!onlySelection) options.add(SCALE_ROCKET); - if (selection != null && selection.getChildCount() > 0) { + + boolean subPartsPresent = false; + if (selection != null) { + for (RocketComponent component : selection) { + if (component.getChildCount() > 0) { + subPartsPresent = true; + break; + } + } + } + if (selection != null && subPartsPresent) { options.add(SCALE_SUBSELECTION); } - if (selection != null) { + + if (selection != null && selection.size() > 0) { options.add(SCALE_SELECTION); } @@ -251,15 +266,16 @@ public class ScaleDialog extends JDialog { * Otherwise the maximum body diameter is selected. As a fallback DEFAULT_INITIAL_SIZE is used. */ double initialSize = 0; - if (selection != null) { - if (selection instanceof SymmetricComponent) { - SymmetricComponent s = (SymmetricComponent) selection; + if (selection != null && selection.size() == 1) { + RocketComponent component = selection.get(0); + if (component instanceof SymmetricComponent) { + SymmetricComponent s = (SymmetricComponent) component; initialSize = s.getForeRadius() * 2; initialSize = MathUtil.max(initialSize, s.getAftRadius() * 2); - }else if ((selection instanceof ParallelStage) || (selection instanceof PodSet )) { - initialSize = selection.getRadiusOffset(); + }else if ((component instanceof ParallelStage) || (component instanceof PodSet )) { + initialSize = component.getRadiusOffset(); } else { - initialSize = selection.getLength(); + initialSize = component.getLength(); } } else { for (RocketComponent c : document.getRocket()) { @@ -267,8 +283,6 @@ public class ScaleDialog extends JDialog { SymmetricComponent s = (SymmetricComponent) c; initialSize = s.getForeRadius() * 2; initialSize = MathUtil.max(initialSize, s.getAftRadius() * 2); - } else if ((selection instanceof ParallelStage) || (selection instanceof PodSet )) { - initialSize = selection.getRadiusOffset(); } } } @@ -330,6 +344,18 @@ public class ScaleDialog extends JDialog { selectionOption.setEditable(false); selectionOption.setToolTipText(tip); panel.add(selectionOption, "growx, wrap para*2"); + + // Change the offset checkbox to false when 'Scale selection' is selection and only one component is selected, + // since this is a common action. + selectionOption.addItemListener(new ItemListener() { + @Override + public void itemStateChanged(ItemEvent e) { + if (SCALE_SELECTION.equals(selectionOption.getSelectedItem()) && (selection != null) && + (selection.size() == 1) && (scaleOffsets != null)) { + scaleOffsets.setSelected(false); + } + } + }); // Scale multiplier @@ -393,7 +419,13 @@ public class ScaleDialog extends JDialog { } } scaleMassValues.setEnabled(overridden); - panel.add(scaleMassValues, "span, wrap para*3"); + panel.add(scaleMassValues, "span, wrap"); + + // Scale offsets + scaleOffsets = new JCheckBox(trans.get("checkbox.scaleOffsets")); + scaleOffsets.setToolTipText(trans.get("checkbox.scaleOffsets.ttip")); + scaleOffsets.setSelected(true); + panel.add(scaleOffsets, "span, wrap para*3"); // Scale / Accept Buttons @@ -455,7 +487,7 @@ public class ScaleDialog extends JDialog { try { document.startUndo(trans.get("undo.scaleRocket")); for (RocketComponent c : document.getRocket()) { - scale(c, mul, scaleMass, true); + scale(c, mul, scaleMass, scaleOffsets.isSelected()); } } finally { document.stopUndo(); @@ -466,9 +498,17 @@ public class ScaleDialog extends JDialog { // Scale component and subcomponents try { document.startUndo(trans.get("undo.scaleComponents")); - scale(selection, mul, scaleMass, false); - for (RocketComponent c : selection.getChildren()) { - scale(c, mul, scaleMass, true); + + // Keep track of which components are already scaled so that we don't scale children multiple times (if + // they were also part of selection) + List scaledComponents = new ArrayList<>(); + for (RocketComponent component : selection) { + scale(component, mul, scaleMass, scaleOffsets.isSelected()); + scaledComponents.add(component); + + if (component.getChildCount() > 0) { + scaleChildren(component, scaledComponents, mul, scaleMass); + } } } finally { document.stopUndo(); @@ -476,10 +516,13 @@ public class ScaleDialog extends JDialog { } else if (SCALE_SELECTION.equals(item)) { - // Scale only the selected component + // Scale only the selected components try { document.startUndo(trans.get("undo.scaleComponent")); - scale(selection, mul, scaleMass, false); + + for (RocketComponent component : selection) { + scale(component, mul, scaleMass, scaleOffsets.isSelected()); + } } finally { document.stopUndo(); } @@ -519,6 +562,22 @@ public class ScaleDialog extends JDialog { clazz = clazz.getSuperclass(); } } + + /** + * Iteratively scale the children of component. If one of the children was already present in scaledComponents, + * don't scale it. + * @param component component whose children need to be scaled + * @param scaledComponents list of components that were already scaled + */ + private void scaleChildren(RocketComponent component, List scaledComponents, double mul, boolean scaleMass) { + for (RocketComponent child : component.getChildren()) { + if (!scaledComponents.contains(component)) { + scale(child, mul, scaleMass, scaleOffsets.isSelected()); + scaledComponents.add(child); + scaleChildren(child, scaledComponents, mul, scaleMass); + } + } + } private void updateToField() { diff --git a/swing/src/net/sf/openrocket/gui/main/BasicFrame.java b/swing/src/net/sf/openrocket/gui/main/BasicFrame.java index a084939f7..885fc1d85 100644 --- a/swing/src/net/sf/openrocket/gui/main/BasicFrame.java +++ b/swing/src/net/sf/openrocket/gui/main/BasicFrame.java @@ -407,6 +407,24 @@ public class BasicFrame extends JFrame { return (RocketComponent) path.getLastPathComponent(); } + /** + * Return the currently selected rocket component, or null if none selected. + */ + private List getSelectedComponents() { + TreePath[] paths = componentSelectionModel.getSelectionPaths(); + if (paths == null || paths.length == 0) + return null; + + List result = new LinkedList<>(); + for (TreePath path : paths) { + tree.scrollPathToVisible(path); + RocketComponent component = (RocketComponent) path.getLastPathComponent(); + result.add(component); + } + + return result; + } + /** * Creates the menu for the window. @@ -651,7 +669,7 @@ public class BasicFrame extends JFrame { @Override public void actionPerformed(ActionEvent e) { log.info(Markers.USER_MARKER, "Scale... selected"); - ScaleDialog dialog = new ScaleDialog(document, getSelectedComponent(), BasicFrame.this); + ScaleDialog dialog = new ScaleDialog(document, getSelectedComponents(), BasicFrame.this); dialog.setVisible(true); dialog.dispose(); }