.
+ */
+package net.rptools.maptool.client.swing;
+
+import java.beans.*;
+import java.text.ParseException;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import javax.swing.*;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.SwingPropertyChangeSupport;
+import net.rptools.lib.MathUtil;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * Utility class for linking a numeric JSpinner, JSlider, and a variable. The slider and spinner are
+ * tied together with the class NumericModel. It holds delegates for the two component models which
+ * update each other.The default relationship between spinner value and slider value is 1:1 and
+ * utilises a Functional Interface to translate values between the two. This can be customised by
+ * setting your own Functions, e.g. an example for 1:100
+ *
+ *
+ * Function<Number, Integer> spinnerToSlider =
+ * number -> ((Number) number.doubleValue * 100).intValue
+ * Function<Integer, Number> sliderToSpinner =
+ * number -> ((Number) i).doubleValue()/100d
+ *
+ *
+ * Similarly:
+ * Providing a Consumer<Number>
for a linked property will result in the property
+ * being updated with the spinner value.
+ * Providing a Supplier<Number>
for a linked property can be used to update the
+ * spinner value by calling update()
. PropertyChangeSupport and VetoableChangeSupport
+ * have been implemented on the spinner and slider models for the methods;
+ * setMinimum,
+ * setMaximum, and
+ * setValue.
+ */
+public class SpinnerSliderPaired {
+ // constructors
+ public SpinnerSliderPaired(JSpinner spinner, JSlider slider) {
+ this(spinner, slider, null);
+ }
+
+ public SpinnerSliderPaired(JSpinner spinner, JSlider slider, Consumer propertySetter) {
+ this(spinner, slider, propertySetter, (n) -> n.intValue(), (i) -> ((Number) i).doubleValue());
+ log.debug("spinner-slider pair using default relationship.");
+ }
+
+ public SpinnerSliderPaired(
+ JSpinner spinner,
+ JSlider slider,
+ Consumer propertySetter,
+ Function spinnerToSlider,
+ Function sliderToSpinner) {
+ // set funcitional relationship
+ setSpinnerToSlider(spinnerToSlider);
+ setSliderToSpinner(sliderToSpinner);
+ // set the controls
+ setLinkedSpinner(spinner);
+ setLinkedSlider(slider);
+ // set the property setter
+ setPropertySetter(propertySetter);
+
+ commonModel.setDelegateValues();
+ getLinkedSlider().setModel(commonModel.sliderModelDelegate);
+ getLinkedSpinner().setModel(commonModel.spinnerModelDelegate);
+
+ log.debug("new spinner-slider pair: " + this.toString());
+ }
+
+ // @formatter:off
+ // spotless:off
+ private static final Logger log = LogManager.getLogger(SpinnerSliderPaired.class);
+ // Property Change Support
+ protected SwingPropertyChangeSupport pcs = new SwingPropertyChangeSupport(this);
+ protected VetoableChangeSupport vcs = new VetoableChangeSupport(this);
+ public void addVetoableChangeListener(VetoableChangeListener listener) { vcs.addVetoableChangeListener(listener); }
+ protected void removeVetoableChangeListener(VetoableChangeListener listener){ vcs.addVetoableChangeListener(listener); }
+ public void addPropertyChangeListener(PropertyChangeListener listener) { pcs.addPropertyChangeListener(listener); }
+ protected void removePropertyChangeListener(PropertyChangeListener listener) { pcs.removePropertyChangeListener(listener); }
+
+ // controls
+ private transient JSpinner linkedSpinner;
+ private transient JSlider linkedSlider;
+ public JSpinner getLinkedSpinner(){ return linkedSpinner; }
+ public JSlider getLinkedSlider(){ return linkedSlider; }
+ private void setLinkedSlider(@NotNull JSlider slider){
+ this.linkedSlider = slider;
+ getLinkedSlider().addMouseWheelListener(
+ e -> { incrementValue((int) Math.round(e.getPreciseWheelRotation()));
+ });
+ }
+ private void setLinkedSpinner(JSpinner spinner){
+ this.linkedSpinner = spinner;
+ getLinkedSpinner().addChangeListener(spinnerEditListener);
+ getLinkedSpinner().addMouseWheelListener(
+ e -> { incrementValue( e.getPreciseWheelRotation());
+ });
+ }
+ // option to set the spinner to loop/wrap at end values
+ private boolean spinnerWraps = false;
+ public void setSpinnerWraps(boolean b){ this.spinnerWraps = b; }
+
+ // property setter
+ private Consumer propertySetter;
+ private Supplier propertyGetter;
+ private String propertyName = "Property";
+ public Consumer getPropertySetter() { return propertySetter; }
+ public Supplier getPropertyGetter() { return propertyGetter; }
+ public void setPropertySetter(Consumer propertySetter) {this.propertySetter = propertySetter; }
+ public void setPropertyGetter(Supplier propertyGetter) {this.propertyGetter = propertyGetter; }
+ public void setPropertyName(String s){ this.propertyName = s; }
+ private void setProperty(Number n){
+ if(propertyGetter != null){
+ if(propertyGetter.get().doubleValue() != n.doubleValue() && propertySetter != null){
+ propertySetter.accept(n);
+ log.debug(propertyName + " set to " + n);
+ }
+ }
+ }
+ public void update(){ if(propertyGetter != null){ setValue(propertyGetter.get()); } }
+ // Instance of NumericModel that ties the two controls together
+ private NumericModel commonModel = new NumericModel();
+ // functions that define the relationship between spinner and slider values
+ private Function sliderToSpinner;
+ private Function spinnerToSlider;
+ public Function getSpinnerToSlider() { return spinnerToSlider; }
+ public void setSpinnerToSlider(Function function) { spinnerToSlider = function; }
+ public Function getSliderToSpinner() { return sliderToSpinner; }
+ public void setSliderToSpinner(Function function) { sliderToSpinner = function; }
+
+ // public methods pointing to model methods
+ public Number getNextValue(){ return commonModel.getNextValue(); }
+ public Number getPreviousValue(){ return commonModel.getPreviousValue(); }
+ public int getSliderValue(){ return commonModel.getNumber(true).intValue(); }
+ public double getSpinnerValue(){ return commonModel.getNumber(false).doubleValue(); }
+ public void incrementValue(Number n){ commonModel.incrementValue(n);}
+ public void setMaximum(Number n){ commonModel.setMaximum(n);}
+ public void setMinimum(Number n){ commonModel.setMinimum(n);}
+ public void setValue(Number n){ commonModel.setValue(n);}
+ //@formatter:on
+ // spotless:on
+ // The shared model - two delegates controlled from above
+ public class NumericModel {
+ public NumericModel() {}
+
+ // initialise delegates with linked spinner values
+ private void setDelegateValues() {
+ if (getLinkedSpinner() == null) {
+ return;
+ }
+ if (spinnerToSlider == null) { // default 1:1 relationship
+ sliderToSpinner = i -> ((Number) i).doubleValue();
+ spinnerToSlider = n -> n.intValue();
+ }
+ SpinnerNumberModel spinModel = (SpinnerNumberModel) getLinkedSpinner().getModel();
+ spinnerModelDelegate =
+ new NumericSpinnerModel(
+ spinModel.getNumber().doubleValue(),
+ ((Number) spinModel.getMinimum()).doubleValue(),
+ ((Number) spinModel.getMaximum()).doubleValue(),
+ spinModel.getStepSize().doubleValue());
+ sliderModelDelegate =
+ new NumericSliderModel(
+ spinnerToSlider.apply(spinnerModelDelegate.getNumber()),
+ 0,
+ spinnerToSlider.apply(((Number) spinnerModelDelegate.getMinimum())),
+ spinnerToSlider.apply(((Number) spinnerModelDelegate.getMaximum())));
+ }
+
+ // a method for every flavour
+ private void incrementValue(Number delta) {
+ PropertyChangeEvent pce = new PropertyChangeEvent(this, "increment", getNumber(false), delta);
+ try {
+ vcs.fireVetoableChange(pce);
+ spinnerModelDelegate.increment(delta);
+ pcs.firePropertyChange(pce);
+ } catch (PropertyVetoException e) {
+ log.info(e.getMessage(), e);
+ }
+ }
+
+ private void setStepSize(Number n) {
+ PropertyChangeEvent pce = new PropertyChangeEvent(this, "stepsize", getNumber(false), n);
+ try {
+ vcs.fireVetoableChange(pce);
+ spinnerModelDelegate.setStepSize(n);
+ pcs.firePropertyChange(pce);
+ } catch (PropertyVetoException e) {
+ log.info(e.getMessage(), e);
+ }
+ }
+
+ private void setValue(Number n) {
+ PropertyChangeEvent pce =
+ new PropertyChangeEvent(this, "value", getNumber(MathUtil.isInt(n)), n);
+ try {
+ vcs.fireVetoableChange(pce);
+ if (MathUtil.isInt(n)) {
+ sliderModelDelegate.setValue(n.intValue());
+ } else {
+ spinnerModelDelegate.setValue(n);
+ }
+ pcs.firePropertyChange(pce);
+ } catch (PropertyVetoException e) {
+ log.info(e.getMessage(), e);
+ }
+ }
+
+ private void setMaximum(Number n) {
+ PropertyChangeEvent pce =
+ new PropertyChangeEvent(this, "value", getNumber(MathUtil.isInt(n)), n);
+ try {
+ vcs.fireVetoableChange(pce);
+ if (MathUtil.isInt(n)) {
+ sliderModelDelegate.setMaximum(n.intValue());
+ } else {
+ spinnerModelDelegate.setMaximum((Comparable>) n);
+ }
+ pcs.firePropertyChange(pce);
+ } catch (PropertyVetoException e) {
+ log.info(e.getMessage(), e);
+ }
+ }
+
+ private void setMinimum(Number n) {
+ PropertyChangeEvent pce =
+ new PropertyChangeEvent(this, "value", getNumber(MathUtil.isInt(n)), n);
+ try {
+ vcs.fireVetoableChange(pce);
+ if (MathUtil.isInt(n)) {
+ sliderModelDelegate.setMinimum(n.intValue());
+ } else {
+ spinnerModelDelegate.setMinimum((Comparable>) n);
+ }
+ pcs.firePropertyChange(pce);
+ } catch (PropertyVetoException e) {
+ log.info(e.getMessage(), e);
+ }
+ }
+
+ private Number getNextValue() {
+ return (Number) spinnerModelDelegate.getNextValue();
+ }
+
+ private Number getPreviousValue() {
+ return (Number) spinnerModelDelegate.getPreviousValue();
+ }
+
+ private T getNumber(boolean asInt) {
+ if (asInt) {
+ return (T) spinnerToSlider.apply(spinnerModelDelegate.getNumber());
+ } else {
+ return (T) spinnerModelDelegate.getNumber();
+ }
+ }
+
+ // Delegate classes with aditional setters and normal setters redirected
+ public NumericSpinnerModel spinnerModelDelegate = new NumericSpinnerModel(0d, 0d, 100d, 1d);
+
+ private class NumericSpinnerModel extends SpinnerNumberModel {
+ public NumericSpinnerModel(double value, double minimum, double maximum, double stepSize) {
+ super(value, minimum, maximum, stepSize);
+ }
+
+ public void setMax(@NotNull Number n) {
+ super.setMaximum(n.doubleValue());
+ sliderModelDelegate.setMaximum(spinnerToSlider.apply((Number) super.getMaximum()));
+ }
+
+ public void setMin(@NotNull Number n) {
+ super.setMinimum(n.doubleValue());
+ sliderModelDelegate.setMinimum(spinnerToSlider.apply((Number) super.getMinimum()));
+ }
+
+ public void setVal(@NotNull Number n) {
+ super.setValue(n.doubleValue());
+ setProperty(n);
+ sliderModelDelegate.setValue(spinnerToSlider.apply(super.getNumber()));
+ }
+
+ public void setStep(@NotNull Number n) {
+ super.setStepSize(n);
+ }
+
+ public void incr(@NotNull Number n) {
+ double newVal = getNumber().doubleValue() + n.doubleValue();
+ double max = ((Number) getMaximum()).doubleValue();
+ double min = ((Number) getMinimum()).doubleValue();
+ if (spinnerWraps && (newVal < min || newVal > max)) {
+ if (newVal < min) {
+ newVal = newVal - min + max;
+ } else {
+ newVal = newVal - max + min;
+ }
+ } else {
+ newVal = Math.min(max, Math.max(newVal, min));
+ }
+ setValue(newVal);
+ }
+
+ public void increment(@NotNull Number n) {
+ n = MathUtil.doublePrecision(n.doubleValue(), 4);
+ try {
+ PropertyChangeEvent pce =
+ new PropertyChangeEvent(this, "increment", super.getNumber(), n);
+ vcs.fireVetoableChange(pce);
+ incr(n);
+ pcs.firePropertyChange(pce);
+ } catch (PropertyVetoException e) {
+ log.info(e.getMessage());
+ }
+ }
+
+ @Override
+ public Object getNextValue() {
+ try {
+ PropertyChangeEvent pce =
+ new PropertyChangeEvent(this, "increment", super.getNumber(), super.getStepSize());
+ vcs.fireVetoableChange(pce);
+ incr(getStepSize());
+ pcs.firePropertyChange(pce);
+ } catch (PropertyVetoException e) {
+ log.info(e.getMessage());
+ }
+ return getNumber();
+ }
+
+ @Override
+ public Object getPreviousValue() {
+ try {
+ PropertyChangeEvent pce =
+ new PropertyChangeEvent(this, "increment", super.getNumber(), super.getStepSize());
+ vcs.fireVetoableChange(pce);
+ incr(-getStepSize().doubleValue());
+ pcs.firePropertyChange(pce);
+ } catch (PropertyVetoException e) {
+ log.info(e.getMessage());
+ }
+ return super.getNumber();
+ }
+
+ @Override
+ public void setMinimum(Comparable> minimum) {
+ minimum = MathUtil.doublePrecision(((Number) minimum).doubleValue(), 4);
+ if (((Number) this.getMinimum()).doubleValue() != ((Number) minimum).doubleValue()) {
+ try {
+ PropertyChangeEvent pce =
+ new PropertyChangeEvent(this, "minimum", super.getNumber(), minimum);
+ vcs.fireVetoableChange(pce);
+ setMin((Number) minimum);
+ pcs.firePropertyChange(pce);
+ } catch (PropertyVetoException e) {
+ log.info(e.getMessage());
+ }
+ }
+ }
+
+ @Override
+ public void setMaximum(Comparable> maximum) {
+ maximum = MathUtil.doublePrecision(((Number) maximum).doubleValue(), 4);
+ if (((Number) this.getMaximum()).doubleValue() != ((Number) maximum).doubleValue()) {
+ try {
+ PropertyChangeEvent pce =
+ new PropertyChangeEvent(this, "maximum", super.getNumber(), maximum);
+ vcs.fireVetoableChange(pce);
+ setMax((Number) maximum);
+ pcs.firePropertyChange(pce);
+ } catch (PropertyVetoException e) {
+ log.info(e.getMessage());
+ }
+ }
+ }
+
+ @Override
+ public void setValue(Object value) {
+ value = MathUtil.doublePrecision(((Number) value).doubleValue(), 4);
+ if (this.getNumber().doubleValue() != ((Number) value).doubleValue()) {
+ try {
+ PropertyChangeEvent pce =
+ new PropertyChangeEvent(this, "value", super.getNumber(), value);
+ vcs.fireVetoableChange(pce);
+ setVal((Number) value);
+ pcs.firePropertyChange(pce);
+ } catch (PropertyVetoException e) {
+ log.info(e.getMessage());
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "spinnerModelDelegate{"
+ + "min="
+ + getMinimum()
+ + ", max="
+ + getMaximum()
+ + ", val="
+ + getValue()
+ + ", stepSize="
+ + getStepSize()
+ + '}';
+ }
+ }
+ ;
+
+ public NumericSliderModel sliderModelDelegate = new NumericSliderModel(0, 0, 0, 100);
+
+ private class NumericSliderModel extends DefaultBoundedRangeModel {
+ public NumericSliderModel(int value, int extent, int min, int max) {
+ super(value, extent, min, max);
+ }
+
+ public void setMax(@NotNull Number n) {
+ super.setMaximum(n.intValue());
+ spinnerModelDelegate.setMaximum(sliderToSpinner.apply(n.intValue()).doubleValue());
+ }
+
+ public void setMin(@NotNull Number n) {
+ super.setMinimum(n.intValue());
+ spinnerModelDelegate.setMinimum(sliderToSpinner.apply(n.intValue()).doubleValue());
+ }
+
+ public void setVal(@NotNull Number n) {
+ super.setValue(n.intValue());
+ spinnerModelDelegate.setValue(sliderToSpinner.apply(n.intValue()).doubleValue());
+ }
+
+ @Override
+ public void setValue(int n) {
+ if (this.getValue() != n) {
+ try {
+ PropertyChangeEvent pce = new PropertyChangeEvent(this, "value", super.getValue(), n);
+ vcs.fireVetoableChange(pce);
+ setVal((Number) n);
+ pcs.firePropertyChange(pce);
+ } catch (PropertyVetoException e) {
+ log.info(e.getMessage());
+ }
+ }
+ }
+
+ @Override
+ public void setMinimum(int n) {
+ if (this.getValue() != n) {
+ try {
+ PropertyChangeEvent pce = new PropertyChangeEvent(this, "value", super.getMinimum(), n);
+ vcs.fireVetoableChange(pce);
+ setMin((Number) n);
+ pcs.firePropertyChange(pce);
+ } catch (PropertyVetoException e) {
+ log.info(e.getMessage());
+ }
+ }
+ }
+
+ @Override
+ public void setMaximum(int n) {
+ if (this.getValue() != n) {
+ try {
+ PropertyChangeEvent pce = new PropertyChangeEvent(this, "value", super.getMaximum(), n);
+ vcs.fireVetoableChange(pce);
+ setMax((Number) n);
+ pcs.firePropertyChange(pce);
+ } catch (PropertyVetoException e) {
+ log.info(e.getMessage());
+ }
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "sliderModelDelegate{"
+ + "min="
+ + super.getMinimum()
+ + ", max="
+ + super.getMaximum()
+ + ", val="
+ + super.getValue()
+ + ", extent="
+ + super.getExtent()
+ + ", isAdjusting="
+ + super.getValueIsAdjusting()
+ + '}';
+ }
+ }
+ ;
+
+ // spotless:on
+ // @formatter:on
+ @Override
+ public String toString() {
+ return "NumericModel{"
+ + spinnerModelDelegate.toString()
+ + ", "
+ + sliderModelDelegate.toString()
+ + '}';
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "SpinnerSliderPaired{"
+ + "spinnerName="
+ + getLinkedSpinner().getName()
+ + ", sliderName="
+ + getLinkedSlider().getName()
+ + ", propertySetterSet="
+ + (propertySetter != null)
+ + ", sliderToSpinner(0)="
+ + sliderToSpinner.apply(0)
+ + ", sliderToSpinner(1)="
+ + sliderToSpinner.apply(1)
+ + ", spinnerToSlider(0)="
+ + spinnerToSlider.apply(0)
+ + ", spinnerToSlider(1)="
+ + spinnerToSlider.apply(1)
+ + ", spinnerToSlider(0)="
+ + spinnerToSlider.apply(0)
+ + ", sliderToSpinner("
+ + spinnerToSlider.apply(0)
+ + ")="
+ + sliderToSpinner.apply(spinnerToSlider.apply(0))
+ + ", spinnerToSlider(1)="
+ + spinnerToSlider.apply(1)
+ + ", sliderToSpinner("
+ + spinnerToSlider.apply(1)
+ + ")="
+ + sliderToSpinner.apply(spinnerToSlider.apply(1))
+ + '}';
+ }
+
+ private ChangeListener spinnerEditListener =
+ new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ JSpinner spinner = (JSpinner) e.getSource();
+ try {
+ spinner.commitEdit();
+ } catch (ParseException pe) {
+ // Edited value is invalid, revert the spinner to the last valid value,
+ JComponent editor = spinner.getEditor();
+ if (editor instanceof JSpinner.NumberEditor) {
+ ((JSpinner.NumberEditor) editor).getTextField().setValue(spinner.getValue());
+ }
+ return;
+ }
+ }
+ };
+}
diff --git a/src/main/java/net/rptools/maptool/client/tool/StampTool.java b/src/main/java/net/rptools/maptool/client/tool/StampTool.java
index 4995f79de4..a7af2a0b4e 100644
--- a/src/main/java/net/rptools/maptool/client/tool/StampTool.java
+++ b/src/main/java/net/rptools/maptool/client/tool/StampTool.java
@@ -1346,8 +1346,7 @@ public void dragTo(int mouseX, int mouseY, boolean lockAspectRatio, boolean snap
// For snap-to-grid tokens (except background stamps) we anchor at the center of the token.
final var isSnapToGridAndAnchoredAtCenter =
- tokenBeingResized.isSnapToGrid()
- && tokenBeingResized.getLayer().anchorSnapToGridAtCenter();
+ tokenBeingResized.isSnapToGrid() && tokenBeingResized.getLayer().isSnapToGridAtCenter();
final var snapToGridMultiplier = isSnapToGridAndAnchoredAtCenter ? 2 : 1;
var widthIncrease = adjustment.x * snapToGridMultiplier;
var heightIncrease = adjustment.y * snapToGridMultiplier;
diff --git a/src/main/java/net/rptools/maptool/client/ui/token/BooleanTokenOverlay.java b/src/main/java/net/rptools/maptool/client/ui/token/BooleanTokenOverlay.java
index dbe39f0b2d..5c8d1ed8a5 100644
--- a/src/main/java/net/rptools/maptool/client/ui/token/BooleanTokenOverlay.java
+++ b/src/main/java/net/rptools/maptool/client/ui/token/BooleanTokenOverlay.java
@@ -55,7 +55,7 @@ protected BooleanTokenOverlay(String aName) {
public void paintOverlay(Graphics2D g, Token token, Rectangle bounds, Object value) {
if (FunctionUtil.getBooleanValue(value)) {
// Apply Alpha Transparency
- float opacity = token.getTokenOpacity();
+ float opacity = token.getOpacity();
if (opacity < 1.0f)
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity));
diff --git a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/EditTokenDialog.java b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/EditTokenDialog.java
index e5ffed9a83..ad7b0f8d88 100644
--- a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/EditTokenDialog.java
+++ b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/EditTokenDialog.java
@@ -109,7 +109,7 @@ public class EditTokenDialog extends AbeillePanel {
RessourceManager.getBigIcon(Icons.EDIT_TOKEN_REFRESH_ON);
private static final ImageIcon REFRESH_ICON_OFF =
RessourceManager.getBigIcon(Icons.EDIT_TOKEN_REFRESH_OFF);
- // private CharSheetController controller;
+
private final RSyntaxTextArea xmlStatblockRSyntaxTextArea = new RSyntaxTextArea(2, 2);
private final RSyntaxTextArea textStatblockRSyntaxTextArea = new RSyntaxTextArea(2, 2);
private final WordWrapCellRenderer propertyCellRenderer = new WordWrapCellRenderer();
@@ -119,7 +119,6 @@ public class EditTokenDialog extends AbeillePanel {
private ImageAssetPanel imagePanel;
private final LibraryManager libraryManager = new LibraryManager();
- // private final Toolbox toolbox = new Toolbox();
private HeroLabData heroLabData;
private AutoGenerateTopologySwingWorker autoGenerateTopologySwingWorker =
new AutoGenerateTopologySwingWorker(false, Color.BLACK);
@@ -199,6 +198,26 @@ public void closeDialog() {
super.closeDialog();
}
};
+ getTokenLayoutPanel().reset(token);
+ getFlippedX()
+ .addChangeListener(
+ e ->
+ getTokenLayoutPanel()
+ .getHelper()
+ .setTokenFlipX(((JCheckBox) e.getSource()).isSelected()));
+ getFlippedY()
+ .addChangeListener(
+ e ->
+ getTokenLayoutPanel()
+ .getHelper()
+ .setTokenFlipY(((JCheckBox) e.getSource()).isSelected()));
+ getFlippedIso()
+ .addChangeListener(
+ e ->
+ getTokenLayoutPanel()
+ .getHelper()
+ .setTokenFlipIso(((JCheckBox) e.getSource()).isSelected()));
+
getTokenTopologyPanel().reset(token);
bind(token);
@@ -214,7 +233,7 @@ public void closeDialog() {
validateLibTokenURIAccess(getNameField().getName());
var combo = getStatSheetCombo();
combo.removeAllItems();
- // Default Entry
+ /* Default Entry */
var defaultSS =
new StatSheet(null, I18N.getText("token.statSheet.useDefault"), null, Set.of(), null);
combo.addItem(defaultSS);
@@ -260,27 +279,26 @@ private void validateLibTokenURIAccess(String name) {
@Override
public void bind(final Token token) {
- // ICON
+ /* ICON */
getTokenIconPanel().setImageId(token.getImageAssetId());
-
- // NOTES, GM NOTES. Due to the way things happen on different gui threads, the type must be set
- // before the text
- // otherwise the wrong values can get populated when the tab change listener fires.
+ /* NOTES, GM NOTES. Due to the way things happen on different gui threads, the type must be set
+ before the text
+ otherwise the wrong values can get populated when the tab change listener fires. */
getGMNotesEditor().setTextType(token.getGmNotesType());
getGMNotesEditor().setText(token.getGMNotes());
getPlayerNotesEditor().setTextType(token.getNotesType());
getPlayerNotesEditor().setText(token.getNotes());
- // TYPE
+ /* TYPE */
getTypeCombo().setSelectedItem(token.getType());
- // SIGHT
+ /* SIGHT */
updateSightTypeCombo();
- // Image Tables
+ /* Image Tables */
updateImageTableCombo();
- // STATES
+ /* STATES */
Component barPanel = null;
updateStatesPanel();
Component[] statePanels = getStatesPanel().getComponents();
@@ -296,7 +314,7 @@ public void bind(final Token token) {
}
}
- // BARS
+ /* BARS */
if (barPanel != null) {
Component[] barComponents = ((Container) barPanel).getComponents();
JCheckBox cb = null;
@@ -324,29 +342,27 @@ public void bind(final Token token) {
}
}
- // OWNER LIST
+ /* OWNER LIST */
EventQueue.invokeLater(() -> getOwnerList().setModel(new OwnerListModel()));
- // SPEECH TABLE
+ /* SPEECH TABLE */
EventQueue.invokeLater(() -> getSpeechTable().setModel(new SpeechTableModel(token)));
- // Player player = MapTool.getPlayer();
- // boolean editable = player.isGM() ||
- // !MapTool.getServerPolicy().useStrictTokenManagement() ||
- // token.isOwner(player.getName());
- // getAllPlayersCheckBox().setSelected(token.isOwnedByAll());
-
- // OTHER
+ /* OTHER */
getShapeCombo().setSelectedItem(token.getShape());
setSizeCombo(token);
+ getSnapToGrid().setSelected(token.isSnapToGrid());
+ getFlippedIso().setSelected(token.isFlippedIso());
+ getFlippedX().setSelected(token.isFlippedX());
+ getFlippedY().setSelected(token.isFlippedY());
- // Updates the Property Type list.
+ /* Updates the Property Type list. */
updatePropertyTypeCombo();
- // Set the selected item in Property Type list. Triggers a itemStateChanged event if index != 0
+ /* Set the selected item in Property Type list. Triggers a itemStateChanged event if index != 0 */
getPropertyTypeCombo().setSelectedItem(token.getPropertyType());
- // If index == 0, the itemStateChanged event wasn't triggered, so we update. Fix #1504
+ /* If index == 0, the itemStateChanged event wasn't triggered, so we update. Fix #1504 */
if (getPropertyTypeCombo().getSelectedIndex() == 0) {
updatePropertiesTable((String) getPropertyTypeCombo().getSelectedItem());
}
@@ -361,11 +377,11 @@ public void bind(final Token token) {
getTokenLayoutPanel().setToken(token);
getImageTableCombo().setSelectedItem(token.getImageTableName());
getTokenOpacitySlider()
- .setValue(new BigDecimal(token.getTokenOpacity()).multiply(new BigDecimal(100)).intValue());
+ .setValue(new BigDecimal(token.getOpacity()).multiply(new BigDecimal(100)).intValue());
getTerrainModifier().setText(Double.toString(token.getTerrainModifier()));
getTerrainModifierOperationComboBox().setSelectedItem(token.getTerrainModifierOperation());
- // Get tokens ignored list, match to the index in the JList then select them.
+ /* Get tokens ignored list, match to the index in the JList then select them. */
getTerrainModifiersIgnoredList()
.setSelectedIndices(
token.getTerrainModifiersIgnored().stream()
@@ -381,7 +397,7 @@ public void bind(final Token token) {
getUniqueLightSourcesTextPane()
.setText(new LightSyntax().stringifyLights(token.getUniqueLightSources()));
- // Jamz: Init the Topology tab...
+ /* Jamz: Init the Topology tab... */
JTabbedPane tabbedPane = getTabbedPane();
String topologyTitle = I18N.getText("EditTokenDialog.tab.vbl");
@@ -395,7 +411,7 @@ public void bind(final Token token) {
getVisibilityToleranceSpinner().setValue(token.getAlwaysVisibleTolerance());
getJtsMethodComboBox().setSelectedItem(getTokenTopologyPanel().getJtsMethod());
- // Reset scale
+ /* Reset scale */
getTokenTopologyPanel().setScale(1d);
} else {
tabbedPane.setEnabledAt(tabbedPane.indexOfTab(topologyTitle), false);
@@ -438,7 +454,7 @@ public void bind(final Token token) {
getAllowURLAccess().setSelected(token.getAllowURIAccess());
- // Jamz: Init the Hero Lab tab...
+ /* Jamz: Init the Hero Lab tab... */
heroLabData = token.getHeroLabData();
String heroLabTitle = I18N.getString("EditTokenDialog.tab.hero");
@@ -485,8 +501,6 @@ public void bind(final Token token) {
((JLabel) getComponent("lastModified")).setText(heroLabData.getLastModifiedDateString());
EventQueue.invokeLater(this::loadHeroLabImageList);
-
- // loadHeroLabImageList();
} else {
tabbedPane.setEnabledAt(tabbedPane.indexOfTab(heroLabTitle), false);
if (tabbedPane.getSelectedIndex() == tabbedPane.indexOfTab(heroLabTitle)) {
@@ -494,8 +508,8 @@ public void bind(final Token token) {
}
}
- // we will disable the Owner only visible check box if the token is not
- // visible to players to signify the relationship
+ /* we will disable the Owner only visible check box if the token is not
+ visible to players to signify the relationship */
ActionListener tokenVisibleActionListener =
actionEvent -> {
AbstractButton abstractButton = (AbstractButton) actionEvent.getSource();
@@ -505,23 +519,6 @@ public void bind(final Token token) {
};
getVisibleCheckBox().addActionListener(tokenVisibleActionListener);
- // Character Sheets
- // controller = null;
- // String form =
- // MapTool.getCampaign().getCharacterSheets().get(token.getPropertyType());
- // if (form == null)
- // return;
- // URL formUrl = getClass().getClassLoader().getResource(form);
- // if (formUrl == null)
- // return;
- // controller = new CharSheetController(formUrl, null);
- // HashMap properties = new HashMap();
- // for (String prop : token.getPropertyNames())
- // properties.put(prop, token.getProperty(prop));
- // controller.setData(properties);
- // controller.getPanel().setName("characterSheet");
- // replaceComponent("sheetPanel", "characterSheet", controller.getPanel());
-
super.bind(token);
}
@@ -599,6 +596,7 @@ public void initTokenIconPanel() {
public ImageAssetPanel getTokenIconPanel() {
if (imagePanel == null) {
imagePanel = new ImageAssetPanel();
+
imagePanel.setAllowEmptyImage(false);
replaceComponent("mainPanel", "tokenImage", imagePanel);
}
@@ -696,7 +694,7 @@ public JComboBox getImageTableCombo() {
}
public void initTokenOpacitySlider() {
- getTokenOpacitySlider().addChangeListener(new SliderListener());
+ getTokenOpacitySlider().addChangeListener(new OpacitySliderListener());
}
public JSlider getTokenOpacitySlider() {
@@ -721,7 +719,11 @@ public JList getTerrainModifiersIgnoredList() {
public void setUniqueLightSourcesEnabled(boolean enabled) {
getUniqueLightSourcesTextPane().setEnabled(enabled);
- getLabel("uniqueLightSourcesLabel").setEnabled(enabled);
+ getUniqueLightSourcesPanel().setEnabled(enabled);
+ }
+
+ public JPanel getUniqueLightSourcesPanel() {
+ return (JPanel) getComponent("uniqueLightSourcesPanel");
}
public JTextPane getUniqueLightSourcesTextPane() {
@@ -747,7 +749,7 @@ public void initOKButton() {
public boolean commit() {
Token token = getModel();
- if (getNameField().getText().equals("")) {
+ if (getNameField().getText().isEmpty()) {
MapTool.showError("msg.error.emptyTokenName");
return false;
}
@@ -757,34 +759,34 @@ public boolean commit() {
if (getPropertyTable().isEditing()) {
getPropertyTable().getCellEditor().stopCellEditing();
}
- // Commit the changes to the token properties
- // If no map available, cancel the commit. Fixes #1646.
+ /* Commit the changes to the token properties */
+ /* If no map available, cancel the commit. Fixes #1646. */
if (!super.commit() || MapTool.getFrame().getCurrentZoneRenderer() == null) {
return false;
}
- // TYPE
- // Only update this if it actually changed
+ /* TYPE */
+ /* Only update this if it actually changed */
if (getTypeCombo().getSelectedItem() != token.getType()) {
token.setType((Token.Type) getTypeCombo().getSelectedItem());
}
- // NOTES
+ /* NOTES */
token.setGMNotes(getGMNotesEditor().getText());
token.setGmNotesType(getGMNotesEditor().getTextType());
token.setNotes(getPlayerNotesEditor().getText());
token.setNotesType(getPlayerNotesEditor().getTextType());
- // SIZE
+ /* SIZE */
token.setSnapToScale(getSizeCombo().getSelectedIndex() != 0);
if (getSizeCombo().getSelectedIndex() > 0) {
Grid grid = MapTool.getFrame().getCurrentZoneRenderer().getZone().getGrid();
token.setFootprint(grid, (TokenFootprint) getSizeCombo().getSelectedItem());
}
- // Other
+ /* Other */
token.setPropertyType((String) getPropertyTypeCombo().getSelectedItem());
token.setSightType((String) getSightTypeCombo().getSelectedItem());
token.setImageTableName((String) getImageTableCombo().getSelectedItem());
- token.setTokenOpacity(
+ token.setOpacity(
new BigDecimal(getTokenOpacitySlider().getValue())
.divide(new BigDecimal(100))
.floatValue());
@@ -792,7 +794,7 @@ public boolean commit() {
try {
token.setTerrainModifier(Double.parseDouble(getTerrainModifier().getText()));
} catch (NumberFormatException e) {
- // User didn't enter a valid float...
+ /* User didn't enter a valid float... */
token.setTerrainModifier(1);
}
@@ -810,7 +812,7 @@ public boolean commit() {
token.addUniqueLightSource(lightSource);
}
- // Get the states
+ /* Get the states */
Component[] stateComponents = getStatesPanel().getComponents();
Container barPanel = null;
for (Component stateComponent : stateComponents) {
@@ -824,9 +826,9 @@ public boolean commit() {
String state = cb.getText();
token.setState(state, cb.isSelected() ? Boolean.TRUE : Boolean.FALSE);
}
- } // endfor
+ }
- // BARS
+ /* BARS */
if (barPanel != null) {
for (var barContainer : barPanel.getComponents()) {
var barComponents = ((Container) barContainer).getComponents();
@@ -843,9 +845,8 @@ public boolean commit() {
* 100));
}
}
- // Ownership
- // If the token is owned by all and we are a player don't alter the ownership
- // list.
+ /* OWNERSHIP */
+ /* If the token is owned by all and we are a player don't alter the ownership list. */
if (MapTool.getPlayer().isGM() || !token.isOwnedByAll()) {
token.clearAllOwners();
@@ -856,8 +857,7 @@ public boolean commit() {
token.addOwner((String) selectable.getObject());
}
}
- // If we are not a GM and the only non GM owner make sure we can't
- // take our selves off of the owners list
+ /* If we are not a GM and we are the only non-GM owner, make sure we cannot remove ourself from the owners list */
if (!MapTool.getPlayer().isGM()) {
boolean hasPlayer = token.isOwnedByAny(MapTool.getNonGMs());
if (!hasPlayer) {
@@ -865,10 +865,10 @@ public boolean commit() {
}
}
}
- // SHAPE
+ /* SHAPE */
token.setShape((Token.TokenShape) getShapeCombo().getSelectedItem());
- // Stat Sheet
+ /* Stat Sheet */
var ss = (StatSheet) getStatSheetCombo().getSelectedItem();
if (ss == null || (ss.name() == null && ss.namespace() == null)) {
token.useDefaultStatSheet();
@@ -881,37 +881,38 @@ public boolean commit() {
token.setStatSheet(new StatSheetProperties(ssManager.getId(ss), location));
}
- // Macros
+ /* Macros */
token.setSpeechMap(((KeyValueTableModel) getSpeechTable().getModel()).getMap());
- // Properties
+ /* Properties */
((TokenPropertyTableModel) getPropertyTable().getModel()).applyTo(token);
- // Charsheet
+ /* Charsheet */
if (getCharSheetPanel().getImageId() != null) {
MapToolUtil.uploadAsset(AssetManager.getAsset(getCharSheetPanel().getImageId()));
}
token.setCharsheetImage(getCharSheetPanel().getImageId());
- // IMAGE
+ /* IMAGE */
if (!token.getImageAssetId().equals(getTokenIconPanel().getImageId())) {
MapToolUtil.uploadAsset(AssetManager.getAsset(getTokenIconPanel().getImageId()));
token.setImageAsset(null, getTokenIconPanel().getImageId()); // Default image for now
}
- // PORTRAIT
+ /* PORTRAIT */
if (getPortraitPanel().getImageId() != null) {
- // Make sure the server has the image
+ /* Make sure the server has the image */
if (!MapTool.getCampaign().containsAsset(getPortraitPanel().getImageId())) {
MapTool.serverCommand().putAsset(AssetManager.getAsset(getPortraitPanel().getImageId()));
}
}
token.setPortraitImage(getPortraitPanel().getImageId());
- // LAYOUT
- token.setSizeScale(getTokenLayoutPanel().getSizeScale());
- token.setAnchor(getTokenLayoutPanel().getAnchorX(), getTokenLayoutPanel().getAnchorY());
+ /* LAYOUT */
+ getTokenLayoutPanel().getHelper().commitChanges(token);
- // TOPOLOGY
+ token.setSnapToGrid(getSnapToGrid().isSelected());
+
+ /* TOPOLOGY */
for (final var type : Zone.TopologyType.values()) {
token.setMaskTopology(type, getTokenTopologyPanel().getTopology(type));
}
@@ -925,17 +926,12 @@ public boolean commit() {
token.setHeroLabData(heroLabData);
- // URI Access
+ /* URI Access */
token.setAllowURIAccess(getAllowURLAccess().isEnabled() && getAllowURLAccess().isSelected());
- // OTHER
+ /* OTHER */
tokenSaved = true;
- // Character Sheet
- // Map properties = controller.getData();
- // for (String prop : token.getPropertyNames())
- // token.setProperty(prop, properties.get(prop));
-
- // Update UI
+ /* Update UI */
MapTool.getFrame().updateTokenTree();
MapTool.getFrame().resetTokenPanels();
@@ -969,7 +965,7 @@ public PropertyTable getPropertyTable() {
}
private void updateStatesPanel() {
- // Group the states first into individual panels
+ /* Group the states first into individual panels */
List overlays =
new ArrayList(MapTool.getCampaign().getTokenStatesMap().values());
Map groups = new TreeMap();
@@ -979,7 +975,7 @@ private void updateStatesPanel() {
groups.put("", noGroupPanel);
for (BooleanTokenOverlay overlay : overlays) {
String group = overlay.getGroup();
- if (group != null && (group = group.trim()).length() != 0) {
+ if (group != null && !(group = group.trim()).isEmpty()) {
JPanel panel = groups.get(group);
if (panel == null) {
panel =
@@ -991,17 +987,17 @@ private void updateStatesPanel() {
}
}
- // Add the group panels and bar panel to the states panel
+ /* Add the group panels and bar panel to the states panel */
JPanel statesPanel = getStatesPanel();
MigLayout layout = new MigLayout("wrap", "[fill,grow]");
statesPanel.setLayout(layout);
statesPanel.removeAll();
- // Add the individual check boxes.
+ /* Add the individual check boxes. */
for (BooleanTokenOverlay state : overlays) {
String group = state.getGroup();
var panel = groups.get("");
- if (group != null && (group = group.trim()).length() != 0) {
+ if (group != null && !(group = group.trim()).isEmpty()) {
panel = groups.get(group);
}
panel.add(new JCheckBox(state.getName()));
@@ -1015,8 +1011,8 @@ private void updateStatesPanel() {
JPanel barPanel = new JPanel(new MigLayout("wrap 2", "[fill,grow][fill,grow]"));
barPanel.setName("bar");
- // Add sliders to the bar panel
- if (MapTool.getCampaign().getTokenBarsMap().size() > 0) {
+ /* Add sliders to the bar panel */
+ if (!MapTool.getCampaign().getTokenBarsMap().isEmpty()) {
barPanel.setBorder(
BorderFactory.createTitledBorder(I18N.getText("CampaignPropertiesDialog.tab.bars")));
@@ -1181,6 +1177,22 @@ public JCheckBox getAllowURLAccess() {
return (JCheckBox) getComponent("@allowURIAccess");
}
+ public JCheckBox getSnapToGrid() {
+ return getCheckBox("@isSnapToGrid");
+ }
+
+ public JCheckBox getFlippedIso() {
+ return getCheckBox("@isFlippedIso");
+ }
+
+ public JCheckBox getFlippedX() {
+ return getCheckBox("@isFlippedX");
+ }
+
+ public JCheckBox getFlippedY() {
+ return getCheckBox("@isFlippedY");
+ }
+
public void initSpeechPanel() {
getSpeechClearAllButton()
.addActionListener(
@@ -1214,10 +1226,10 @@ public String getToolTipText(MouseEvent event) {
}
};
propertyTable.setFillsViewportHeight(true); // XXX This is Java6-only -- need
- // Java5 solution
+ /* Java5 solution */
propertyTable.setName("propertiesTable");
- // wrap button and functionality
+ /* wrap button and functionality */
JPanel buttonsAndPropertyTable = new JPanel();
buttonsAndPropertyTable.setLayout(new BorderLayout());
JCheckBox wrapToggle = new JCheckBox(I18N.getString("EditTokenDialog.msg.wrap"));
@@ -1229,17 +1241,13 @@ public String getToolTipText(MouseEvent event) {
buttonsAndPropertyTable.add(wrapToggle, BorderLayout.PAGE_END);
PropertyPane pane = new PropertyPane(propertyTable);
- // pane.setPreferredSize(new Dimension(100, 300));
buttonsAndPropertyTable.add(pane, BorderLayout.CENTER);
replaceComponent("propertiesPanel", "propertiesTable", buttonsAndPropertyTable);
}
- public void initTokenDetails() {
- // tokenGMNameLabel = panel.getLabel("tokenGMNameLabel");
- }
-
public void initTokenLayoutPanel() {
- TokenLayoutPanel layoutPanel = new TokenLayoutPanel();
+ TokenLayoutRenderPanel layoutPanel = new TokenLayoutRenderPanel();
+ new TokenLayoutPanelHelper(this, layoutPanel, getOKButton());
layoutPanel.setMinimumSize(new Dimension(150, 125));
layoutPanel.setPreferredSize(new Dimension(150, 125));
layoutPanel.setName("tokenLayout");
@@ -1275,8 +1283,8 @@ public ImageAssetPanel getCharSheetPanel() {
return (ImageAssetPanel) getComponent("charsheet");
}
- public TokenLayoutPanel getTokenLayoutPanel() {
- return (TokenLayoutPanel) getComponent("tokenLayout");
+ public TokenLayoutRenderPanel getTokenLayoutPanel() {
+ return (TokenLayoutRenderPanel) getComponent("tokenLayout");
}
public TokenTopologyPanel getTokenTopologyPanel() {
@@ -1526,7 +1534,7 @@ public void initHeroLabImageList() {
if (heroLabData != null) {
getTokenIconPanel().setImageId(heroLabData.getImageAssetID(index));
- getTokenLayoutPanel().setTokenImage(heroLabData.getImageAssetID(index));
+ getTokenLayoutPanel().getHelper().setTokenImageId(heroLabData.getImageAssetID(index));
}
});
@@ -1569,25 +1577,25 @@ public void loadHeroLabImageList() {
* Initialize the Hero Lab statblock tabs
*/
public void initStatBlocks() {
- // Setup the HTML panel
+ /* Setup the HTML panel */
JEditorPane statblockPane = getHtmlStatblockEditor();
HTMLEditorKit kit = new HTMLEditorKit();
HTMLDocument statblockDoc = (HTMLDocument) kit.createDefaultDocument();
statblockPane.setEditorKit(kit);
statblockPane.setDocument(statblockDoc);
- // We need this property as the kit can't handle {
@@ -1655,11 +1663,11 @@ public void initStatBlocks() {
textStatblockRSyntaxTextArea.setText(heroLabData.getStatBlock_text());
textStatblockRSyntaxTextArea.setCaretPosition(0);
- // Update the images
+ /* Update the images */
MD5Key tokenImageKey = heroLabData.getTokenImage();
if (tokenImageKey != null) {
getTokenIconPanel().setImageId(tokenImageKey);
- getTokenLayoutPanel().setTokenImage(tokenImageKey);
+ getTokenLayoutPanel().getHelper().setTokenImageId(tokenImageKey);
}
MD5Key portraitAssetKeY = heroLabData.getPortraitImage();
@@ -1672,8 +1680,8 @@ public void initStatBlocks() {
getCharSheetPanel().setImageId(handoutAssetKey);
}
- // If NPC, lets not overwrite the Name, it may be "Creature 229" or such, GM
- // name is enough
+ /* If NPC, lets not overwrite the Name, it may be "Creature 229" or such, GM
+ name is enough */
((JTextField) getComponent("@GMName")).setText(heroLabData.getName());
if (heroLabData.isAlly()) {
getTypeCombo().setSelectedItem(Type.PC);
@@ -1682,13 +1690,13 @@ public void initStatBlocks() {
getTypeCombo().setSelectedItem(Type.NPC);
}
- // Update image list
+ /* Update image list */
loadHeroLabImageList();
}
}
});
- // Setup xPath searching for XML StatBlock
+ /* Setup xPath searching for XML StatBlock */
JTextField xmlStatblockSearchTextField =
(JTextField) getComponent("xmlStatblockSearchTextField");
JButton xmlStatblockSearchButton = (JButton) getComponent("xmlStatblockSearchButton");
@@ -1722,7 +1730,7 @@ public void keyPressed(KeyEvent e) {
xmlStatblockRSyntaxTextArea.setCaretPosition(0);
});
- // Setup regular expression searching for TEXT StatBlock
+ /* Setup regular expression searching for TEXT StatBlock */
JTextField textStatblockSearchTextField =
(JTextField) getComponent("textStatblockSearchTextField");
JButton textStatblockSearchButton = (JButton) getComponent("textStatblockSearchButton");
@@ -1749,9 +1757,6 @@ public void keyPressed(KeyEvent e) {
SearchContext context = new SearchContext();
context.setSearchFor(searchText);
context.setRegularExpression(true);
- // context.setMatchCase(matchCaseCB.isSelected());
- // context.setSearchForward(forward);
- // context.setWholeWord(false);
SearchEngine.find(textStatblockRSyntaxTextArea, context).wasFound();
});
@@ -1869,7 +1874,7 @@ public Map getMap() {
Map map = new HashMap();
for (Association row : rowList) {
- if (row.getLeft() == null || row.getLeft().trim().length() == 0) {
+ if (row.getLeft() == null || row.getLeft().trim().isEmpty()) {
continue;
}
map.put(row.getLeft(), row.getRight());
@@ -1878,7 +1883,7 @@ public Map getMap() {
}
}
- // needed to change the popup for properties
+ /* needed to change the popup for properties */
private static class MTMultilineStringExComboBox extends MultilineStringExComboBox {
final ResourceBundle a = ResourceBundle.getBundle("com.jidesoft.combobox.combobox");
@@ -1895,7 +1900,7 @@ public PopupPanel createPopupComponent() {
}
}
- // the cell editor for property popups
+ /* the cell editor for property popups */
private static class MTMultilineStringCellEditor extends MultilineStringCellEditor {
protected MTMultilineStringExComboBox createMultilineStringComboBox() {
@@ -1907,7 +1912,7 @@ protected MTMultilineStringExComboBox createMultilineStringComboBox() {
}
}
- // the property popup table
+ /* the property popup table */
private static class MTMultilineStringPopupPanel extends PopupPanel {
private RSyntaxTextArea j = createTextArea();
@@ -1918,7 +1923,7 @@ public MTMultilineStringPopupPanel() {
public MTMultilineStringPopupPanel(String paramString) {
this.setResizable(true);
- // Set the color style via Theme
+ /* Set the color style via Theme */
try {
File themeFile =
new File(
@@ -1928,7 +1933,7 @@ public MTMultilineStringPopupPanel(String paramString) {
j.revalidate();
} catch (IOException e) {
- e.printStackTrace();
+ log.debug(e.getLocalizedMessage(), e);
}
JScrollPane localJScrollPane = new RTextScrollPane(j);
localJScrollPane.setVerticalScrollBarPolicy(22);
@@ -1953,7 +1958,6 @@ public MTMultilineStringPopupPanel(String paramString) {
syntaxComboBox.addActionListener(
e -> j.setSyntaxEditingStyle(syntaxComboBox.getSelectedItem().toString()));
- // content.add(wrapToggle);
add(syntaxComboBox, BorderLayout.BEFORE_FIRST_LINE);
add(wrapToggle, BorderLayout.AFTER_LAST_LINE);
}
@@ -1980,14 +1984,14 @@ protected RSyntaxTextArea createTextArea() {
}
}
- // cell renderer for properties table
+ /* cell renderer for properties table */
private static class WordWrapCellRenderer extends RSyntaxTextArea implements TableCellRenderer {
WordWrapCellRenderer() {
setLineWrap(false);
setWrapStyleWord(true);
- // Set the color style via Theme
+ /* Set the color style via Theme */
try {
File themeFile =
new File(
@@ -1997,7 +2001,7 @@ private static class WordWrapCellRenderer extends RSyntaxTextArea implements Tab
revalidate();
} catch (IOException e) {
- e.printStackTrace();
+ log.debug(e.getLocalizedMessage(), e);
}
}
@@ -2036,14 +2040,14 @@ protected Void doInBackground() {
publish(generatedTopology);
}
- // Nothing to do, so nothing to publish.
+ /* Nothing to do, so nothing to publish. */
return null;
}
@Override
protected void process(List areaChunk) {
if (!isCancelled()) {
- final var newArea = areaChunk.get(areaChunk.size() - 1);
+ final var newArea = areaChunk.getLast();
final var optimizedArea =
TokenVBL.simplifyArea(
newArea,
@@ -2062,7 +2066,7 @@ protected void done() {
}
}
- class SliderListener implements ChangeListener {
+ class OpacitySliderListener implements ChangeListener {
public void stateChanged(ChangeEvent e) {
JSlider source = (JSlider) e.getSource();
@@ -2088,13 +2092,13 @@ public Component getListCellRendererComponent(
(JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
try {
ImageIcon finalImage =
- ImageUtil.scaleImage(
+ ImageUtil.scaleImageIcon(
new ImageIcon(ImageManager.getImageAndWait(heroLabData.getImageAssetID(index))),
250,
175);
label.setIcon(finalImage);
} catch (Exception e) {
- e.printStackTrace();
+ log.debug(e.getLocalizedMessage(), e);
}
label.setIconTextGap(10);
label.setHorizontalTextPosition(JLabel.LEFT);
@@ -2103,8 +2107,7 @@ public Component getListCellRendererComponent(
}
}
- // //
- // HANDLER
+ /* HANDLER */
public static class MouseHandler extends MouseAdapter {
HtmlEditorSplit source;
@@ -2152,8 +2155,7 @@ public void mouseClicked(MouseEvent e) {
}
}
- // //
- // MODELS
+ /* MODELS */
private class TokenPropertyTableModel
extends AbstractPropertyTableModel
implements NavigableModel {
@@ -2203,8 +2205,7 @@ public void applyTo(Token token) {
@Override
public boolean isNavigableAt(int rowIndex, int columnIndex) {
- // make the property name column non-navigable so that tab takes you
- // directly to the next property value cell.
+ /* make the property name column non-navigable so that tab takes you directly to the next property value cell. */
return (columnIndex != 0);
}
diff --git a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanel.java b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanel.java
deleted file mode 100644
index c721955573..0000000000
--- a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanel.java
+++ /dev/null
@@ -1,230 +0,0 @@
-/*
- * This software Copyright by the RPTools.net development team, and
- * licensed under the Affero GPL Version 3 or, at your option, any later
- * version.
- *
- * MapTool Source Code is distributed in the hope that it will be
- * useful, but WITHOUT ANY WARRANTY; without even the implied warranty
- * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
- *
- * You should have received a copy of the GNU Affero General Public
- * License * along with this source Code. If not, please visit
- * and specifically the Affero license
- * text at .
- */
-package net.rptools.maptool.client.ui.token.dialog.edit;
-
-import java.awt.Color;
-import java.awt.Dimension;
-import java.awt.Graphics;
-import java.awt.Graphics2D;
-import java.awt.Point;
-import java.awt.Rectangle;
-import java.awt.TexturePaint;
-import java.awt.event.MouseAdapter;
-import java.awt.event.MouseEvent;
-import java.awt.event.MouseMotionAdapter;
-import java.awt.geom.Area;
-import java.awt.image.BufferedImage;
-import javax.swing.JPanel;
-import javax.swing.SwingUtilities;
-import net.rptools.lib.MD5Key;
-import net.rptools.maptool.client.AppStyle;
-import net.rptools.maptool.client.MapTool;
-import net.rptools.maptool.client.swing.SwingUtil;
-import net.rptools.maptool.client.ui.theme.Images;
-import net.rptools.maptool.client.ui.theme.RessourceManager;
-import net.rptools.maptool.language.I18N;
-import net.rptools.maptool.model.Token;
-import net.rptools.maptool.model.Token.TokenShape;
-import net.rptools.maptool.model.Zone;
-import net.rptools.maptool.util.ImageManager;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
-/**
- * Support class used by the token editor dialog on the "Properties" tab to allow a token's image to
- * be moved around within a one-cell grid area. Scaling is supported using the mousewheel and
- * position is supported using left-drag. We should add rotation ability using Shift-mousewheel as
- * well.
- *
- * @author trevor
- */
-public class TokenLayoutPanel extends JPanel {
- private static final Logger log = LogManager.getLogger(TokenLayoutPanel.class);
- private Token token;
- private int dragOffsetX;
- private int dragOffsetY;
- private MD5Key tokenImage;
-
- public TokenLayoutPanel() {
- addMouseWheelListener(
- e -> {
- int wheelMovement = e.getWheelRotation();
- // Not for snap-to-scale
- if (!token.isSnapToScale() || wheelMovement == 0) {
- return;
- }
- double delta = wheelMovement > 0 ? -.1 : .1;
- if (SwingUtil.isShiftDown(e)) {
- // Nothing yet, as changing the facing isn't the right way to handle it --
- // the image itself really should be rotated. And it's probably better to
- // not simply store a Transform but to create a new image. We could
- // store an AffineTransform until the dialog is closed and then create
- // the new image. But the amount of rotation needs to be saved so
- // that future adjustments can return back to the original image (as
- // a way of reducing round off error from multiple rotations).
- }
- double scale = token.getSizeScale() + delta;
- log.debug(() -> "wheel=" + wheelMovement + ", delta=" + delta);
-
- // Range
- scale = Math.max(.1, scale);
- scale = Math.min(3, scale);
- token.setSizeScale(scale);
- repaint();
- });
- addMouseListener(
- new MouseAdapter() {
- String old;
-
- @Override
- public void mousePressed(MouseEvent e) {
- dragOffsetX = e.getX();
- dragOffsetY = e.getY();
- }
-
- @Override
- public void mouseEntered(MouseEvent e) {
- old = MapTool.getFrame().getStatusMessage();
- MapTool.getFrame()
- .setStatusMessage(I18N.getString("EditTokenDialog.status.layout.instructions"));
- }
-
- @Override
- public void mouseExited(MouseEvent e) {
- if (old != null) MapTool.getFrame().setStatusMessage(old);
- }
-
- @Override
- public void mouseClicked(MouseEvent e) {
- if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() == 2) {
- dragOffsetX = 0;
- dragOffsetY = 0;
- token.setAnchor(0, 0);
- token.setSizeScale(1.0);
- repaint();
- }
- }
- });
- addMouseMotionListener(
- new MouseMotionAdapter() {
- @Override
- public void mouseDragged(MouseEvent e) {
- int dx = e.getX() - dragOffsetX;
- int dy = e.getY() - dragOffsetY;
-
- Zone zone = MapTool.getFrame().getCurrentZoneRenderer().getZone();
-
- int gridSize = zone.getGrid().getSize();
- int halfGridSize = gridSize / 2;
- int maxXoff = Math.max(halfGridSize, token.getBounds(zone).width - gridSize);
- int maxYoff = Math.max(halfGridSize, token.getBounds(zone).height - gridSize);
-
- int offX = Math.min(maxXoff, Math.max(token.getAnchor().x + dx, -maxXoff));
- int offY = Math.min(maxYoff, Math.max(token.getAnchor().y + dy, -maxYoff));
-
- token.setAnchor(offX, offY);
- dragOffsetX = e.getX();
- dragOffsetY = e.getY();
- repaint();
- }
- });
- }
-
- public double getSizeScale() {
- return token.getSizeScale();
- }
-
- public int getAnchorX() {
- return token.getAnchor().x;
- }
-
- public int getAnchorY() {
- return token.getAnchor().y;
- }
-
- public void setToken(Token token) {
- this.token = new Token(token);
- setTokenImage(token.getImageAssetId());
- }
-
- public MD5Key getTokenImage() {
- return tokenImage;
- }
-
- public void setTokenImage(MD5Key tokenImage) {
- this.tokenImage = tokenImage;
- }
-
- @Override
- protected void paintComponent(Graphics g) {
- Dimension size = getSize();
- Zone zone = MapTool.getFrame().getCurrentZoneRenderer().getZone();
-
- // Gather info
- BufferedImage image = ImageManager.getImage(getTokenImage());
-
- Rectangle tokenSize = token.getBounds(zone);
- Dimension imgSize = new Dimension(image.getWidth(), image.getHeight());
-
- // If figure we need to calculate an additional offset for the token height
- double iso_ho = 0;
- if (token.getShape() == TokenShape.FIGURE) {
- double th = token.getHeight() * (double) tokenSize.width / token.getWidth();
- iso_ho = tokenSize.height - th;
- tokenSize = new Rectangle(tokenSize.x, tokenSize.y - (int) iso_ho, tokenSize.width, (int) th);
- }
-
- SwingUtil.constrainTo(imgSize, tokenSize.width, tokenSize.height);
-
- Point centerPoint = new Point(size.width / 2, size.height / 2);
- Graphics2D g2d = (Graphics2D) g;
-
- var panelTexture = RessourceManager.getImage(Images.TEXTURE_PANEL);
- // Background
- ((Graphics2D) g)
- .setPaint(
- new TexturePaint(
- panelTexture,
- new Rectangle(0, 0, panelTexture.getWidth(), panelTexture.getHeight())));
- g2d.fillRect(0, 0, size.width, size.height);
- AppStyle.shadowBorder.paintWithin((Graphics2D) g, 0, 0, size.width, size.height);
-
- // Grid
- if (zone.getGrid().getCapabilities().isSnapToGridSupported()) {
- Area gridShape = zone.getGrid().getCellShape();
- int offsetX = (size.width - gridShape.getBounds().width) / 2;
- int offsetY = (size.height - gridShape.getBounds().height) / 2;
- g2d.setColor(Color.black);
-
- // Add horizontal and vertical lines to help with centering
- g2d.drawLine(
- 0, (size.height - (int) iso_ho) / 2, size.width, (size.height - (int) iso_ho) / 2);
- g2d.drawLine(size.width / 2, 0, size.width / 2, (size.height - (int) iso_ho));
-
- offsetY = offsetY - (int) (iso_ho / 2);
- g2d.translate(offsetX, offsetY);
- g2d.draw(gridShape);
- g2d.translate(-offsetX, -offsetY);
- }
- // Token
- g2d.drawImage(
- image,
- centerPoint.x - imgSize.width / 2 + token.getAnchor().x,
- centerPoint.y - imgSize.height / 2 + token.getAnchor().y,
- imgSize.width,
- imgSize.height,
- this);
- }
-}
diff --git a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanelHelper.java b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanelHelper.java
new file mode 100644
index 0000000000..7ca5faa91e
--- /dev/null
+++ b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutPanelHelper.java
@@ -0,0 +1,1310 @@
+/*
+ * This software Copyright by the RPTools.net development team, and
+ * licensed under the Affero GPL Version 3 or, at your option, any later
+ * version.
+ *
+ * MapTool Source Code is distributed in the hope that it will be
+ * useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License * along with this source Code. If not, please visit
+ * and specifically the Affero license
+ * text at .
+ */
+package net.rptools.maptool.client.ui.token.dialog.edit;
+
+import com.formdev.flatlaf.FlatLaf;
+import com.formdev.flatlaf.extras.FlatSVGIcon;
+import com.formdev.flatlaf.extras.components.FlatButton;
+import java.awt.*;
+import java.awt.event.*;
+import java.awt.geom.*;
+import java.awt.image.BufferedImage;
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyVetoException;
+import java.text.DecimalFormat;
+import java.util.*;
+import java.util.function.Function;
+import javax.swing.*;
+import net.rptools.lib.MD5Key;
+import net.rptools.lib.MathUtil;
+import net.rptools.lib.image.ImageUtil;
+import net.rptools.maptool.client.MapTool;
+import net.rptools.maptool.client.swing.AbeillePanel;
+import net.rptools.maptool.client.swing.GenericDialog;
+import net.rptools.maptool.client.swing.SpinnerSliderPaired;
+import net.rptools.maptool.client.swing.VerticalLabel;
+import net.rptools.maptool.client.ui.theme.Images;
+import net.rptools.maptool.client.ui.theme.RessourceManager;
+import net.rptools.maptool.language.I18N;
+import net.rptools.maptool.model.*;
+import net.rptools.maptool.util.GraphicsUtil;
+import net.rptools.maptool.util.ImageManager;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+/**
+ * Support class that does much of the heavy lifting for TokenLayoutPanel. Links control components
+ * and the mouse events to property changes in the reference token. Stores a bunch of useful values.
+ * Disables the default OK button click on "Enter" when editing spinner fields.
+ *
+ * @author 2024 - Reverend/Bubblobill
+ */
+public class TokenLayoutPanelHelper {
+ private static final Logger log = LogManager.getLogger(TokenLayoutPanelHelper.class);
+
+ public TokenLayoutPanelHelper(
+ AbeillePanel parentAbeillePanel, TokenLayoutRenderPanel renderPane, AbstractButton okBtn) {
+ parent = parentAbeillePanel;
+ renderPanel = renderPane;
+ renderPanel.setHelper(this);
+ setOKButton((JButton) okBtn);
+ init();
+
+ parent.addComponentListener(
+ new ComponentAdapter() {
+ private void doStuff() {
+ if (parentRoot == null) {
+ setParentRoot(parent.getRootPane());
+ }
+ iFeelDirty();
+ }
+
+ @Override
+ public void componentMoved(ComponentEvent e) {
+ super.componentMoved(e);
+ doStuff();
+ }
+
+ @Override
+ public void componentShown(ComponentEvent e) {
+ super.componentShown(e);
+ doStuff();
+ }
+
+ @Override
+ public void componentResized(ComponentEvent e) {
+ super.componentResized(e);
+ doStuff();
+ }
+ });
+ }
+
+ public void init() {
+ getSizeCombo().addItemListener(sizeListener);
+ initButtons();
+ initSpinners();
+ initSliders();
+ pairControls();
+ }
+
+ enum FlipState {
+ HORIZONTAL,
+ ISOMETRIC,
+ VERTICAL
+ }
+
+ EnumSet flipStates = EnumSet.noneOf(FlipState.class);
+ private static final UIDefaults UI_DEFAULTS = UIManager.getDefaults();
+ private static final double DEFAULT_FONT_SIZE = UI_DEFAULTS.getFont("defaultFont").getSize2D();
+ private static final int ICON_SIZE = (int) (DEFAULT_FONT_SIZE * 2 + 4);
+
+ Grid grid = MapTool.getFrame().getCurrentZoneRenderer().getZone().getGrid();
+ RenderBits renderBits = new RenderBits();
+ boolean isoGrid = grid.isIsometric();
+ boolean noGrid = GridFactory.getGridType(grid).equals(GridFactory.NONE);
+ boolean squareGrid = GridFactory.getGridType(grid).equals(GridFactory.SQUARE);
+ boolean hexGrid = false;
+ boolean isIsoFigure = false;
+ int gridSize = grid.getSize();
+ double cellHeight = grid.getCellHeight();
+ double cellWidth = grid.getCellWidth();
+ private static final CellPoint ORIGIN = new CellPoint(0, 0);
+ private Token originalToken, mirrorToken;
+ private BufferedImage tokenImage;
+ TokenFootprint footprint;
+ Rectangle2D footprintBounds;
+ Set occupiedCells;
+ ArrayList cellCentres;
+ /* controls/components */
+ AbeillePanel parent;
+ private JRootPane parentRoot;
+ private final TokenLayoutRenderPanel renderPanel;
+ private JComboBox sizeCombo;
+ private JLabel scaleLabel;
+ private JSpinner anchorXSpinner, anchorYSpinner, rotationSpinner, scaleSpinner, zoomSpinner;
+ private JSlider anchorXSlider, anchorYSlider, rotationSlider, scaleSlider, zoomSlider;
+ private AbstractButton scaleButton, okButton;
+
+ public void setOKButton(JButton b) {
+ okButton = b;
+ }
+
+ public void setParentRoot(JRootPane rp) {
+ parentRoot = rp;
+ }
+
+ private final String helpText = assembleHelpText();
+
+ /* Component Getters */
+ public JComboBox getSizeCombo() {
+ if (sizeCombo == null) sizeCombo = (JComboBox) parent.getComponent("size");
+ return sizeCombo;
+ }
+
+ /* Labels */
+ public JLabel getScaleLabel() {
+ if (scaleLabel == null) scaleLabel = parent.getLabel("scaleLabel");
+ return scaleLabel;
+ }
+
+ /* Spinners */
+ public JSpinner getAnchorXSpinner() {
+ if (anchorXSpinner == null) anchorXSpinner = parent.getSpinner("anchorXSpinner");
+ return anchorXSpinner;
+ }
+
+ public JSpinner getAnchorYSpinner() {
+ if (anchorYSpinner == null) anchorYSpinner = parent.getSpinner("anchorYSpinner");
+ return anchorYSpinner;
+ }
+
+ public JSpinner getRotationSpinner() {
+ if (rotationSpinner == null) rotationSpinner = parent.getSpinner("rotationSpinner");
+ return rotationSpinner;
+ }
+
+ public JSpinner getScaleSpinner() {
+ if (scaleSpinner == null) scaleSpinner = parent.getSpinner("scaleSpinner");
+ return scaleSpinner;
+ }
+
+ public JSpinner getZoomSpinner() {
+ if (zoomSpinner == null) zoomSpinner = parent.getSpinner("zoomSpinner");
+ return zoomSpinner;
+ }
+
+ /* Sliders */
+ public JSlider getAnchorXSlider() {
+ if (anchorXSlider == null) anchorXSlider = (JSlider) parent.getComponent("anchorXSlider");
+ return anchorXSlider;
+ }
+
+ public JSlider getAnchorYSlider() {
+ if (anchorYSlider == null) anchorYSlider = (JSlider) parent.getComponent("anchorYSlider");
+ return anchorYSlider;
+ }
+
+ public JSlider getRotationSlider() {
+ if (rotationSlider == null) rotationSlider = (JSlider) parent.getComponent("rotationSlider");
+ return rotationSlider;
+ }
+
+ public JSlider getScaleSlider() {
+ if (scaleSlider == null) scaleSlider = (JSlider) parent.getComponent("scaleSlider");
+ return scaleSlider;
+ }
+
+ public JSlider getZoomSlider() {
+ if (zoomSlider == null) zoomSlider = (JSlider) parent.getComponent("zoomSlider");
+ return zoomSlider;
+ }
+
+ /* Buttons */
+ public AbstractButton getScaleButton() {
+ if (scaleButton == null) scaleButton = parent.getButton("scaleButton");
+ return scaleButton;
+ }
+
+ /* Panel */
+ public TokenLayoutRenderPanel getRenderPanel() {
+ return renderPanel;
+ }
+
+ /* Linked controls */
+ SpinnerSliderPaired anchorXPair, anchorYPair, rotationPair, scalePair, zoomPair;
+
+ /* Token value getters pointing to the mirror token */
+ public double getTokenSizeScale() {
+ return mirrorToken.getSizeScale();
+ }
+
+ public double getTokenScaleX() {
+ return mirrorToken.getScaleX();
+ }
+
+ public double getTokenScaleY() {
+ return mirrorToken.getScaleY();
+ }
+
+ public double getTokenImageRotation() {
+ return mirrorToken.getImageRotation();
+ }
+
+ public int getTokenAnchorX() {
+ return mirrorToken.getAnchorX();
+ }
+
+ public int getTokenAnchorY() {
+ return mirrorToken.getAnchorY();
+ }
+
+ public boolean getTokenFlippedX() {
+ return mirrorToken.isFlippedX();
+ }
+
+ public boolean getTokenFlippedY() {
+ return mirrorToken.isFlippedY();
+ }
+
+ public boolean getTokenFlippedIso() {
+ return mirrorToken.isFlippedIso();
+ }
+
+ /* Token value setters pointing to the mirror token */
+ protected void setTokenScaleX(double scale) {
+ mirrorToken.setScaleX(scale);
+ }
+
+ protected void setTokenScaleY(double scale) {
+ mirrorToken.setScaleY(scale);
+ }
+
+ protected void setTokenSizeScale(double scale) {
+ mirrorToken.setSizeScale(scale);
+ }
+
+ protected void setTokenAnchorX(Number x) {
+ mirrorToken.setAnchorX(x.intValue());
+ }
+
+ protected void setTokenAnchorY(Number y) {
+ mirrorToken.setAnchorY(y.intValue());
+ }
+
+ protected void setTokenImageRotation(Number value) {
+ mirrorToken.setImageRotation(MathUtil.doublePrecision(value.doubleValue(), 4));
+ }
+
+ protected void setTokenFlipIso(Boolean b) {
+ mirrorToken.setFlippedIso(b);
+ if (flipStates.contains(FlipState.ISOMETRIC) && !b) {
+ flipStates.remove(FlipState.ISOMETRIC);
+ } else if (!flipStates.contains(FlipState.ISOMETRIC) && b) {
+ flipStates.add(FlipState.ISOMETRIC);
+ }
+ iFeelDirty();
+ }
+
+ protected void setTokenFlipX(Boolean b) {
+ mirrorToken.setFlippedX(b);
+ if (flipStates.contains(FlipState.HORIZONTAL) && !b) {
+ flipStates.remove(FlipState.HORIZONTAL);
+ } else if (!flipStates.contains(FlipState.HORIZONTAL) && b) {
+ flipStates.add(FlipState.HORIZONTAL);
+ }
+ iFeelDirty();
+ }
+
+ protected void setTokenFlipY(Boolean b) {
+ mirrorToken.setFlippedY(b);
+ if (flipStates.contains(FlipState.VERTICAL) && !b) {
+ flipStates.remove(FlipState.VERTICAL);
+ } else if (!flipStates.contains(FlipState.VERTICAL) && b) {
+ flipStates.add(FlipState.VERTICAL);
+ }
+ iFeelDirty();
+ }
+
+ /**
+ * There is one spinner/slider for three different scale settings. This sets the value for the
+ * active axis/axes
+ *
+ * @param n (Number) the scale value being set
+ */
+ protected void setScaleByAxis(Number n) {
+ double value = MathUtil.doublePrecision(n.doubleValue(), 4);
+ log.debug("scaleAxis: " + scaleAxis + " -> " + value);
+ switch (scaleAxis) {
+ case 0 -> setTokenSizeScale(value);
+ case 1 -> setTokenScaleX(value);
+ case 2 -> setTokenScaleY(value);
+ }
+ }
+
+ /**
+ * There is one spinner/slider for three different scale settings. This returns the value for the
+ * active axis/axes
+ *
+ * @return double value for appropriate scale
+ */
+ protected Number getScaleByAxis() {
+ if (scaleAxis == 0) {
+ return getTokenSizeScale();
+ } else if (scaleAxis == 1) {
+ return getTokenScaleX();
+ } else {
+ return getTokenScaleY();
+ }
+ }
+
+ /* used to determine which scale is being edited: 0 is XY, 1 is X, 2 is Y */
+ private int scaleAxis = 0;
+
+ /**
+ * These functions serve to link the scale and zoom sliders and spinners and allow a useful
+ * representation of values < 100%. Slider ranges from -200 to 0 are for values below 100% and 0
+ * to 200 for 100% to 300%
+ */
+ Function percentSliderToSpinner =
+ i ->
+ i <= 0
+ ? MathUtil.mapToRange(((Number) i).doubleValue(), -200.0, 0.0, 0.0, 1.0)
+ : MathUtil.mapToRange(((Number) i).doubleValue(), 0.0, 200.0, 1.0, 3.0).doubleValue();
+
+ Function percentSpinnerToSlider =
+ d ->
+ d.doubleValue() <= 1
+ ? MathUtil.mapToRange(d.doubleValue(), 0.0, 1.0, -200, 0).intValue()
+ : MathUtil.mapToRange(d.doubleValue(), 1.0, 3.0, 0, 200).intValue();
+
+ private void storeFlipDirections() {
+ if (getTokenFlippedIso()) {
+ flipStates.add(FlipState.ISOMETRIC);
+ }
+ if (getTokenFlippedX()) {
+ flipStates.add(FlipState.HORIZONTAL);
+ }
+ if (getTokenFlippedY()) {
+ flipStates.add(FlipState.VERTICAL);
+ }
+ }
+
+ void resetPanel() {
+ setToken(originalToken, false);
+ renderPanel.setInitialScale(1d);
+ renderPanel.calcZoomFactor();
+ }
+
+ void resetPanelToDefault() {
+ setToken(originalToken, true);
+ }
+
+ /* Listeners */
+ PropertyChangeListener controlListener =
+ evt -> {
+ log.debug("controlListener " + evt);
+ if (evt.getPropertyName().toLowerCase().contains("spinnervalue")) {
+ iFeelDirty();
+ } else if (evt.getPropertyName().toLowerCase().contains("flip")) {
+ storeFlipDirections();
+ }
+ };
+
+ FocusListener focusListener =
+ new FocusListener() {
+ /* Toggle "Enter" closing the window. */
+ @Override
+ public void focusGained(FocusEvent e) {
+ ((JComponent) e.getComponent()).getRootPane().setDefaultButton(null);
+ }
+
+ @Override
+ public void focusLost(FocusEvent e) {
+ ((JComponent) e.getComponent()).getRootPane().setDefaultButton((JButton) okButton);
+ }
+ };
+ ItemListener sizeListener =
+ new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ if (mirrorToken != null) {
+ setFootprint((TokenFootprint) getSizeCombo().getSelectedItem());
+ }
+ }
+ };
+
+ /** Mark the rendering panel in need of repainting */
+ void iFeelDirty() {
+ Rectangle panelBounds = getRenderPanel().getBounds();
+ RepaintManager.currentManager(getRenderPanel())
+ .addDirtyRegion(
+ getRenderPanel(), panelBounds.x, panelBounds.y, panelBounds.width, panelBounds.height);
+ }
+
+ public void setTokenImageId(MD5Key tokenImageKey) {
+ tokenImage = ImageManager.getImage(tokenImageKey);
+ }
+
+ protected BufferedImage getTokenImage() {
+ return tokenImage;
+ }
+
+ public void commitChanges(Token tok) {
+ tok.setAnchor(getTokenAnchorX(), getTokenAnchorY());
+ tok.setSizeScale(getTokenSizeScale());
+ tok.setScaleX(getTokenScaleX());
+ tok.setScaleY(getTokenScaleY());
+ tok.setImageRotation(getTokenImageRotation());
+ tok.setFlippedX(flipStates.contains(FlipState.HORIZONTAL));
+ tok.setFlippedY(flipStates.contains(FlipState.VERTICAL));
+ tok.setFlippedIso(flipStates.contains(FlipState.ISOMETRIC));
+ }
+
+ public void setToken(Token token, boolean useDefaults) {
+ grid = MapTool.getFrame().getCurrentZoneRenderer().getZone().getGrid();
+ isoGrid = grid.isIsometric();
+ hexGrid = grid.isHex();
+ noGrid = GridFactory.getGridType(grid).equals(GridFactory.NONE);
+ squareGrid = GridFactory.getGridType(grid).equals(GridFactory.SQUARE);
+ renderBits = new RenderBits();
+ gridSize = grid.getSize();
+ cellHeight = grid.getCellHeight();
+ cellWidth = grid.getCellWidth();
+
+ this.originalToken = new Token(token, true); // duplicate for resetting purposes
+ /* The mirror token exists so we can write token changes without committing them prior to clicking OK */
+ this.mirrorToken = new Token(token, false);
+
+ if (useDefaults) {
+ mirrorToken.setImageRotation(0);
+ mirrorToken.setSizeScale(1d);
+ mirrorToken.setScaleX(1d);
+ mirrorToken.setScaleY(1d);
+ mirrorToken.setAnchor(0, 0);
+ }
+
+ if (mirrorToken.getFootprint(grid) != getSizeCombo().getSelectedItem()) {
+ TokenFootprint tmpFP = (TokenFootprint) getSizeCombo().getSelectedItem();
+ if (tmpFP != null) {
+ mirrorToken.setFootprint(grid, grid.getFootprint(tmpFP.getId()));
+ }
+ }
+
+ isIsoFigure =
+ isoGrid && mirrorToken.getShape() == Token.TokenShape.FIGURE && !mirrorToken.isFlippedIso();
+
+ tokenImage = ImageManager.getImage(mirrorToken.getImageAssetId());
+ setFootprint(mirrorToken.getFootprint(grid));
+
+ getAnchorXSlider().setMinimum((int) -Math.ceil(footprintBounds.getWidth()));
+ getAnchorXSlider().setMaximum((int) Math.ceil(footprintBounds.getWidth()));
+ if (isIsoFigure) {
+ /* Allow more vertical travel for iso figures */
+ getAnchorYSlider().setMinimum((int) -Math.ceil(1.4 * footprintBounds.getHeight()));
+ getAnchorYSlider().setMaximum((int) Math.ceil(1.4 * footprintBounds.getHeight()));
+ } else {
+ getAnchorYSlider().setMinimum((int) -Math.ceil(footprintBounds.getHeight()));
+ getAnchorYSlider().setMaximum((int) Math.ceil(footprintBounds.getHeight()));
+ }
+ /* align mouse drag bounds with sliders */
+ getRenderPanel().setMaxXoff(getAnchorXSlider().getMaximum());
+ getRenderPanel().setMaxYoff(getAnchorYSlider().getMaximum());
+
+ storeFlipDirections();
+
+ /* Assign Suppliers and Consumers to the linked controls and add a PropertyChangeListener */
+ anchorXPair.setPropertySetter(this::setTokenAnchorX);
+ anchorXPair.setPropertyGetter(this::getTokenAnchorX);
+ anchorXPair.setPropertyName("AnchorX");
+ anchorXPair.addPropertyChangeListener(controlListener);
+
+ anchorYPair.setPropertySetter(this::setTokenAnchorY);
+ anchorYPair.setPropertyGetter(this::getTokenAnchorY);
+ anchorYPair.setPropertyName("AnchorY");
+ anchorYPair.addPropertyChangeListener(controlListener);
+
+ rotationPair.setPropertySetter(this::setTokenImageRotation);
+ rotationPair.setPropertyGetter(this::getTokenImageRotation);
+ rotationPair.setPropertyName("Rotation");
+ rotationPair.addPropertyChangeListener(controlListener);
+
+ scalePair.setPropertySetter(this::setScaleByAxis);
+ scalePair.setPropertyGetter(this::getScaleByAxis);
+ scalePair.setPropertyName("Scale");
+ scalePair.addPropertyChangeListener(controlListener);
+
+ zoomPair.setPropertySetter(getRenderPanel().getZoomConsumer());
+ zoomPair.setPropertyGetter(getRenderPanel().getZoomSupplier());
+ zoomPair.setPropertyName("Zoom");
+ zoomPair.addPropertyChangeListener(controlListener);
+
+ setControlValues();
+ getRenderPanel()
+ .addComponentListener(
+ new ComponentAdapter() {
+ @Override
+ public void componentShown(ComponentEvent e) {
+ super.componentShown(e);
+ renderBits.init();
+ }
+ });
+ }
+
+ private void setFootprint(TokenFootprint fp) {
+ this.footprint = fp;
+ setCentredFootprintBounds();
+ occupiedCells = footprint.getOccupiedCells(ORIGIN);
+ Rectangle2D aggregateBounds = new Rectangle2D.Double();
+ cellCentres = new ArrayList<>(occupiedCells.size());
+ for (CellPoint cp : occupiedCells) {
+ cellCentres.add(grid.getCellCenter(cp));
+ aggregateBounds.add(grid.getBounds(cp));
+ }
+ double xFix = -aggregateBounds.getCenterX();
+ double yFix = -aggregateBounds.getCenterY();
+ cellCentres.replaceAll(pt -> new Point2D.Double(pt.getX() + xFix, pt.getY() + yFix));
+ }
+
+ private void setCentredFootprintBounds() {
+ if (grid == null) {
+ return;
+ }
+ if (!noGrid) {
+ footprintBounds = footprint.getBounds(grid, ORIGIN);
+ footprintBounds =
+ new Rectangle2D.Double(
+ -footprintBounds.getWidth() / 2d,
+ -footprintBounds.getHeight() / 2d,
+ footprintBounds.getWidth(),
+ footprintBounds.getHeight());
+ } else {
+ double factor = footprint.getScale();
+ footprintBounds =
+ new Rectangle2D.Double(
+ -gridSize / 2d * factor,
+ -gridSize / 2d * factor,
+ gridSize * factor,
+ gridSize * factor);
+ }
+ }
+
+ /** Link the spinners to the sliders and join them in matrimonial bliss */
+ private void pairControls() {
+ anchorXPair = new SpinnerSliderPaired(getAnchorXSpinner(), getAnchorXSlider());
+ anchorYPair = new SpinnerSliderPaired(getAnchorYSpinner(), getAnchorYSlider());
+
+ rotationPair = new SpinnerSliderPaired(getRotationSpinner(), getRotationSlider());
+ rotationPair.setSpinnerWraps(true);
+
+ scalePair =
+ new SpinnerSliderPaired(
+ getScaleSpinner(),
+ getScaleSlider(),
+ null,
+ percentSpinnerToSlider,
+ percentSliderToSpinner);
+ scalePair.addVetoableChangeListener(
+ evt -> {
+ if (evt.getPropertyName().toLowerCase().contains("value")
+ && evt.getNewValue().getClass().isAssignableFrom(Double.class)
+ && ((Number) evt.getNewValue()).doubleValue() < 0.1) {
+ throw new PropertyVetoException("Minimum scale value reached", evt);
+ }
+ });
+ zoomPair =
+ new SpinnerSliderPaired(
+ getZoomSpinner(),
+ getZoomSlider(),
+ null,
+ percentSpinnerToSlider,
+ percentSliderToSpinner);
+ zoomPair.addVetoableChangeListener(
+ evt -> {
+ if (evt.getPropertyName().toLowerCase().contains("value")
+ && evt.getNewValue().getClass().isAssignableFrom(Double.class)
+ && ((Number) evt.getNewValue()).doubleValue() < 0.34) {
+ throw new PropertyVetoException("Minimum zoom value reached", evt);
+ }
+ });
+ anchorXPair.addPropertyChangeListener(evt -> iFeelDirty());
+ anchorYPair.addPropertyChangeListener(evt -> iFeelDirty());
+ rotationPair.addPropertyChangeListener(evt -> iFeelDirty());
+ scalePair.addPropertyChangeListener(evt -> iFeelDirty());
+ zoomPair.addPropertyChangeListener(evt -> iFeelDirty());
+ }
+
+ public void initSpinners() {
+ /* models */
+ getAnchorXSpinner().setModel(new SpinnerNumberModel(0d, -200d, 200d, 1d));
+ getAnchorYSpinner().setModel(new SpinnerNumberModel(0d, -200d, 200d, 1d));
+ getRotationSpinner().setModel(new SpinnerNumberModel(0d, 0d, 360d, 1d));
+ getScaleSpinner().setModel(new SpinnerNumberModel(1d, 0d, 3d, 0.1));
+ getZoomSpinner().setModel(new SpinnerNumberModel(1d, 0d, 3d, 0.1));
+ getZoomSpinner().setVisible(false);
+ /* editors */
+ getAnchorXSpinner().setEditor(new JSpinner.NumberEditor(anchorXSpinner, "0"));
+ getAnchorYSpinner().setEditor(new JSpinner.NumberEditor(anchorYSpinner, "0"));
+ getRotationSpinner().setEditor(new JSpinner.NumberEditor(rotationSpinner, "0.0"));
+ getScaleSpinner().setEditor(new JSpinner.NumberEditor(scaleSpinner, "0%"));
+
+ ((JSpinner.NumberEditor) getAnchorXSpinner().getEditor()).getTextField().setColumns(3);
+ ((JSpinner.NumberEditor) getAnchorYSpinner().getEditor()).getTextField().setColumns(3);
+ ((JSpinner.NumberEditor) getRotationSpinner().getEditor()).getTextField().setColumns(3);
+ ((JSpinner.NumberEditor) getScaleSpinner().getEditor()).getTextField().setColumns(4);
+
+ /* listeners */
+ ((JSpinner.NumberEditor) getAnchorXSpinner().getEditor())
+ .getTextField()
+ .addFocusListener(focusListener);
+ ((JSpinner.NumberEditor) getAnchorYSpinner().getEditor())
+ .getTextField()
+ .addFocusListener(focusListener);
+ ((JSpinner.NumberEditor) getRotationSpinner().getEditor())
+ .getTextField()
+ .addFocusListener(focusListener);
+ ((JSpinner.NumberEditor) getScaleSpinner().getEditor())
+ .getTextField()
+ .addFocusListener(focusListener);
+ }
+
+ public void initButtons() {
+ /* icons */
+ createButtonIcons();
+
+ scaleButton = new FlatButton();
+ parent.replaceComponent("scalePanel", "scaleButton", scaleButton);
+ getScaleButton().addActionListener(new ScaleButtonListener());
+ getScaleButton().setIcon(scaleIcons[scaleAxis]);
+ getScaleLabel().setText(I18N.getString("sightLight.optionLabel.scale") + " ");
+
+ FlatButton layoutHelpButton = new FlatButton();
+ layoutHelpButton.setButtonType(FlatButton.ButtonType.help);
+ parent.replaceComponent("layoutTabPanel", "layoutHelpButton", layoutHelpButton);
+ layoutHelpButton.setToolTipText(helpText);
+ layoutHelpButton.addActionListener(e -> showHelp());
+ layoutHelpButton.addMouseListener(
+ new MouseAdapter() {
+ @Override
+ public void mouseExited(MouseEvent e) {
+ super.mouseExited(e);
+ iFeelDirty();
+ }
+ });
+ }
+
+ private String assembleHelpText() {
+ String rowText = "%s | %s";
+ String caption = "%s";
+ return caption.formatted(I18N.getString("EditTokenDialog.layout.help.caption"))
+ + rowText.formatted(
+ I18N.getString("Mouse.leftDrag"),
+ I18N.getString("EditTokenDialog.layout.help.moveImage"))
+ + rowText.formatted(
+ I18N.getString("Mouse.rightDrag"),
+ I18N.getString("EditTokenDialog.layout.help.moveView"))
+ + rowText.formatted(
+ I18N.getString("Mouse.leftDoubleClick"),
+ I18N.getString("EditTokenDialog.layout.help.reset"))
+ + rowText.formatted(
+ I18N.getString("Mouse.rightDoubleClick"),
+ I18N.getString("EditTokenDialog.layout.help.resetDefaults"))
+ + rowText.formatted(
+ I18N.getString("Mouse.wheel"), I18N.getString("EditTokenDialog.layout.help.scaleImage"))
+ + rowText.formatted(
+ I18N.getString("Mouse.shiftWheel"),
+ I18N.getString("EditTokenDialog.layout.help.rotateImage"))
+ + rowText.formatted(
+ I18N.getString("Mouse.ctrlWheel"),
+ I18N.getString("EditTokenDialog.layout.help.zoomView"));
+ }
+
+ private void showHelp() {
+ JPanel helpPanel = new JPanel(new BorderLayout());
+ GenericDialog gd = new GenericDialog("Layout Controls", MapTool.getFrame(), helpPanel, true);
+ gd.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
+ JButton okayButton = new JButton();
+ okayButton.setText(I18N.getString("Button.ok"));
+
+ JLabel helpTextContainer = new JLabel();
+ helpTextContainer.setText(helpText);
+ helpPanel.add(new JScrollPane(helpTextContainer), BorderLayout.NORTH);
+ helpPanel.add(okayButton, BorderLayout.SOUTH);
+ okayButton.addActionListener(
+ e -> {
+ gd.closeDialog();
+ iFeelDirty();
+ });
+ gd.showDialog();
+ }
+
+ public void initSliders() {
+ getAnchorXSlider().setModel(new DefaultBoundedRangeModel(0, gridSize, -gridSize, gridSize));
+ getAnchorYSlider().setModel(new DefaultBoundedRangeModel(0, gridSize, -gridSize, gridSize));
+ getScaleSlider().setModel(new DefaultBoundedRangeModel(0, 0, -200, 200));
+ getZoomSlider().setModel(new DefaultBoundedRangeModel(0, 0, -200, 200));
+ getRotationSlider().setModel(new DefaultBoundedRangeModel(0, 360, 0, 360));
+
+ class VertLabel extends VerticalLabel {
+ public double divisor = 1;
+
+ public VertLabel(String text) {
+ super(text);
+ }
+
+ public void setDivisor(double divisor) {
+ this.divisor = divisor;
+ }
+
+ protected void paintComponent(Graphics g) {
+ Graphics2D g2d = (Graphics2D) g;
+ if (isRotated())
+ g2d.rotate(
+ Math.toRadians(-90 * getRotation() / divisor),
+ getPreferredSize().getWidth(),
+ getPreferredSize().getHeight());
+
+ super.paintComponent(g2d);
+ g2d.dispose();
+ }
+ }
+ Dictionary offSetYLabels = new Hashtable<>();
+ Dictionary offSetXLabels = new Hashtable<>();
+ for (int i = -1200; i <= 1200; i += 50) {
+ JLabel label1 = new JLabel(String.valueOf(i));
+ offSetYLabels.put(i, label1);
+ VertLabel label2 = new VertLabel(String.valueOf(i));
+ label2.setRotation(VerticalLabel.ROTATE_RIGHT);
+ label2.setDivisor(3d);
+ offSetXLabels.put(i, label2);
+ }
+ getAnchorYSlider().setLabelTable(offSetYLabels);
+ getAnchorXSlider().setLabelTable(offSetXLabels);
+
+ DecimalFormat df = new DecimalFormat("##0%");
+ Dictionary pctLabels = new Hashtable<>();
+ pctLabels.put(-200, new JLabel(df.format(0)));
+ pctLabels.put(-100, new JLabel(df.format(0.5)));
+ pctLabels.put(0, new JLabel(df.format(1)));
+ pctLabels.put(100, new JLabel(df.format(2)));
+ pctLabels.put(200, new JLabel(df.format(3)));
+
+ getScaleSlider().setLabelTable(pctLabels);
+ getZoomSlider().setLabelTable(pctLabels);
+
+ setupSlider(getAnchorXSlider(), 50, 25);
+ setupSlider(getAnchorYSlider(), 50, 25);
+ setupSlider(getRotationSlider(), 60, 12);
+ setupSlider(getScaleSlider(), 100, 20);
+ setupSlider(getZoomSlider(), 100, 20);
+ }
+
+ private void setupSlider(JSlider slider, int majorTick, int minorTick) {
+ slider.setMajorTickSpacing(majorTick);
+ slider.setMinorTickSpacing(minorTick);
+ slider.setPaintTicks(true);
+ slider.setPaintLabels(true);
+ }
+
+ /** Set controls to match token values */
+ private void setControlValues() {
+ getAnchorXSpinner().setValue(getTokenAnchorX());
+ getAnchorYSpinner().setValue(getTokenAnchorY());
+ scaleAxis = 0;
+ getScaleButton().setIcon(scaleIcons[scaleAxis]);
+ getScaleSpinner().setValue(getTokenSizeScale());
+ getRotationSpinner().setValue(getTokenImageRotation());
+ getZoomSpinner().setValue(1d);
+ }
+
+ ImageIcon[] scaleIcons = new ImageIcon[3];
+
+ private void createButtonIcons() {
+ String iconBase = "net/rptools/maptool/client/image/";
+ scaleIcons[0] = new FlatSVGIcon(iconBase + "scale.svg", ICON_SIZE, ICON_SIZE);
+ scaleIcons[1] = new FlatSVGIcon(iconBase + "scaleHor.svg", ICON_SIZE, ICON_SIZE);
+ scaleIcons[2] = new FlatSVGIcon(iconBase + "scaleVert.svg", ICON_SIZE, ICON_SIZE);
+ }
+
+ private class ScaleButtonListener implements ActionListener {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ scaleAxis = scaleAxis == 2 ? 0 : scaleAxis + 1;
+ getScaleButton().setIcon(scaleIcons[scaleAxis]);
+ switch (scaleAxis) {
+ case 0 -> {
+ getScaleSlider().setValue(percentSpinnerToSlider.apply(getTokenSizeScale()));
+ getScaleLabel().setText(I18N.getString("sightLight.optionLabel.scale") + " ");
+ }
+ case 1 -> {
+ getScaleSlider().setValue(percentSpinnerToSlider.apply(getTokenScaleX()));
+ getScaleLabel().setText(I18N.getString("sightLight.optionLabel.scale") + "-X");
+ }
+ case 2 -> {
+ getScaleSlider().setValue(percentSpinnerToSlider.apply(getTokenScaleY()));
+ getScaleLabel().setText(I18N.getString("sightLight.optionLabel.scale") + "-Y");
+ }
+ }
+ }
+ }
+
+ /**
+ * Just a convenient place to hold a bunch of stuff that is not token related but purely for the
+ * rendering panel
+ */
+ class RenderBits {
+ RenderBits() {
+ if (FlatLaf.isLafDark()) {
+ panelTexture = ImageUtil.negativeImage(panelTexture);
+ for (int i = 0; i < colours.length; i++) {
+ colours[i] = new Color(ImageUtil.negativeColourInt(colours[i].getRGB()));
+ }
+ }
+
+ backgroundTexture =
+ new TexturePaint(
+ panelTexture, new Rectangle(0, 0, panelTexture.getWidth(), panelTexture.getHeight()));
+ setStrokeArrays();
+ gridShapeFill = createGridShape(false);
+ gridShapeOutline = createGridShape(true);
+ }
+
+ void init() {
+ Dimension size = getRenderPanel().getSize();
+ viewBounds = getRenderPanel().getVisibleRect();
+ viewOffset = getRenderPanel().getViewOffset();
+ centrePoint =
+ new Point2D.Double(
+ size.width / 2d + viewOffset.getX(), size.height / 2d + viewOffset.getY());
+
+ if (zoomFactor != getRenderPanel().getZoomFactor()) {
+ zoomFactor = getRenderPanel().getZoomFactor();
+ constrainedZoom = MathUtil.constrainNumber(zoomFactor, 1, 1.6d).floatValue();
+ setStrokeArrays();
+ }
+
+ if (tokenImage == null) {
+ /* just to avoid Div/0 if called before image loaded */
+ tokenImage =
+ new BufferedImage(
+ (int) footprintBounds.getWidth(),
+ (int) footprintBounds.getHeight(),
+ BufferedImage.TYPE_4BYTE_ABGR_PRE);
+ }
+ iso_figure_ho = ImageUtil.getIsoFigureHeightOffset(mirrorToken, footprintBounds);
+
+ workImage = ImageUtil.getScaledTokenImage(tokenImage, mirrorToken, grid, zoomFactor);
+ workImage = getFlippedImage(workImage);
+ workImage = ImageUtil.rotateImage(workImage, mirrorToken.getImageRotation());
+ }
+
+ private void setStrokeArrays() {
+ if (solidStrokes == null) {
+ solidStrokes = new BasicStroke[strokeModels.length];
+ dashedStrokes = new BasicStroke[strokeModels.length];
+ }
+ float thickest = strokeModels[0].getLineWidth();
+ float thinnest = strokeModels[3].getLineWidth();
+ boolean mapValues = constrainedZoom != 1f;
+ for (int i = 0; i < strokeModels.length; i++) {
+ BasicStroke model = strokeModels[i];
+ float useWidth;
+ if (mapValues) {
+ useWidth =
+ MathUtil.mapToRange(
+ model.getLineWidth(),
+ thinnest,
+ thickest,
+ thinnest * constrainedZoom,
+ thickest * constrainedZoom);
+ solidStrokes[i] = new BasicStroke(useWidth, model.getEndCap(), model.getLineJoin());
+ dashedStrokes[i] =
+ new BasicStroke(
+ useWidth,
+ model.getEndCap(),
+ model.getLineJoin(),
+ model.getMiterLimit(),
+ model.getDashArray(),
+ model.getDashPhase());
+ }
+ }
+ }
+
+ static final RenderingHints RENDERING_HINTS = ImageUtil.getRenderingHintsQuality();
+ static final float LINE_SIZE = (float) (DEFAULT_FONT_SIZE / 12f);
+ Rectangle2D viewBounds;
+ Point2D viewOffset, centrePoint;
+ double zoomFactor = 1, iso_figure_ho = 0;
+ float constrainedZoom = 1;
+ private static BufferedImage panelTexture = RessourceManager.getImage(Images.TEXTURE_PANEL);
+ BufferedImage workImage;
+ static TexturePaint backgroundTexture;
+ Shape centreMark, gridShapeFill, gridShapeOutline;
+ BasicStroke[] solidStrokes, dashedStrokes;
+ BasicStroke[] strokeModels =
+ new BasicStroke[] {
+ new BasicStroke(
+ LINE_SIZE * 2.35f,
+ BasicStroke.CAP_ROUND,
+ BasicStroke.JOIN_ROUND,
+ 1f,
+ new float[] {4f, 6f},
+ 2f),
+ new BasicStroke(
+ LINE_SIZE * 1.63f,
+ BasicStroke.CAP_ROUND,
+ BasicStroke.JOIN_ROUND,
+ 1f,
+ new float[] {3.5f, 6.5f},
+ 1.5f),
+ new BasicStroke(
+ LINE_SIZE * 1.45f,
+ BasicStroke.CAP_ROUND,
+ BasicStroke.JOIN_ROUND,
+ 1f,
+ new float[] {2f, 8f},
+ 1.5f),
+ new BasicStroke(
+ LINE_SIZE,
+ BasicStroke.CAP_ROUND,
+ BasicStroke.JOIN_ROUND,
+ 1f,
+ new float[] {2.5f, 7.5f},
+ 1.5f)
+ };
+ public Color[] colours =
+ new Color[] {Color.YELLOW, Color.RED, Color.BLUE, Color.BLACK, Color.DARK_GRAY};
+
+ /**
+ * Returns an image flipped according to the token's flip properties
+ *
+ * @param bi Image to flip
+ * @return Flipped bufferedImage
+ */
+ protected BufferedImage getFlippedImage(BufferedImage bi) {
+ log.debug("getFlippedImage - flipStates: " + flipStates);
+ int direction =
+ (flipStates.contains(FlipState.HORIZONTAL) ? 1 : 0)
+ + (flipStates.contains(FlipState.VERTICAL) ? 2 : 0);
+ if (direction != 0) {
+ bi = ImageUtil.flipCartesian(bi, direction);
+ }
+ if (flipStates.contains(FlipState.ISOMETRIC)) {
+ bi = ImageUtil.flipIsometric(bi, true);
+ }
+ return bi;
+ }
+
+ private Shape createGridShape(boolean trueSize) {
+ return GraphicsUtil.createGridShape(
+ GridFactory.getGridType(grid), (trueSize ? grid.getSize() : grid.getSize() - 8));
+ }
+
+ /** Itty bitty cross to show the dead-centre of the footprint */
+ void createCentreMark() {
+ double aperture = Math.max(cellHeight, cellWidth) / 7.0;
+ double r = aperture / 4.0;
+ Path2D path = new Path2D.Double();
+ path.moveTo(-r, -r);
+ path.lineTo(r, r);
+ path.moveTo(-r, r);
+ path.lineTo(r, -r);
+ centreMark = path;
+ }
+
+ protected void paintCentreMark(Graphics g) {
+ if (centreMark == null) {
+ createCentreMark();
+ }
+ Graphics2D g2d = (Graphics2D) g;
+ g2d.translate(centrePoint.getX(), centrePoint.getY());
+ g2d.scale(constrainedZoom, constrainedZoom);
+ g2d.setStroke(
+ new BasicStroke(
+ constrainedZoom * 2.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL, 10f));
+ g2d.setColor(colours[4]);
+ Composite oldAc = g2d.getComposite();
+ g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacities[0]));
+ g2d.draw(centreMark);
+ g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacities[2]));
+ g2d.setStroke(
+ new BasicStroke(
+ constrainedZoom * 1.5f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_BEVEL, 10f));
+ g2d.setColor(colours[3]);
+ g2d.draw(centreMark);
+ g2d.setComposite(oldAc);
+ renderBits.paintShapeOutLine(g2d, centreMark, true, true);
+ g2d.dispose();
+ }
+
+ /**
+ * Used for gridless maps, paints concentric rings with an interval of the grid size. Paints a
+ * ring of a different colour at the radius associated with the footprint scale. Also paints
+ * some radial lines to assist with alignment
+ *
+ * @param g graphics object
+ * @param zoomFactor zoom level applied to view
+ */
+ void paintRings(Graphics g, double zoomFactor) {
+ Graphics2D g2d = (Graphics2D) g.create();
+ /* used with no grid. A set of rings and radial lines. */
+ TokenFootprint fp = mirrorToken.getFootprint(grid);
+ Rectangle2D fpCellBounds = fp.getBounds(grid, ORIGIN);
+ fpCellBounds =
+ new Rectangle2D.Double(
+ 0,
+ 0,
+ fpCellBounds.getWidth() * footprint.getScale(),
+ fpCellBounds.getHeight() * footprint.getScale());
+
+ double cx = viewBounds.getCenterX() + viewOffset.getX();
+ double cy = viewBounds.getCenterY() + viewOffset.getY();
+
+ double gap = grid.getSize() * zoomFactor;
+ double maxRadius = Math.hypot(this.viewBounds.getWidth(), this.viewBounds.getHeight());
+ double currentRadius = gap / 2d;
+ double tokenRadius = fpCellBounds.getCenterX() * zoomFactor;
+ Line2D lineLong = new Line2D.Double(cx + currentRadius / 2d, cy, maxRadius, cy);
+ Line2D lineShort = new Line2D.Double(cx + gap * 2d, cy, maxRadius, cy);
+
+ /* draw radial lines */
+ for (double i = 0; i < 24; i++) {
+ if (i % 6 == 0) {
+ continue; /* skip cardinal lines */
+ }
+ paintShapeOutLine(
+ g2d,
+ AffineTransform.getRotateInstance(Math.TAU / 24 * i, cx, cy)
+ .createTransformedShape(i % 2 == 1 ? lineShort : lineLong),
+ false,
+ true);
+ }
+
+ /* draw rings */
+ while (currentRadius < maxRadius) {
+ Ellipse2D e =
+ new Ellipse2D.Double(
+ cx - currentRadius, cy - currentRadius, 2 * currentRadius, 2 * currentRadius);
+ paintShapeOutLine(g2d, e, true, true);
+ currentRadius += gap;
+ }
+ paintShapeOutLine(
+ g2d,
+ new Ellipse2D.Double(
+ cx - tokenRadius, cy - tokenRadius, tokenRadius * 2, tokenRadius * 2),
+ true,
+ false);
+ g2d.dispose();
+ }
+
+ /**
+ * Horizontal and vertical lines oriented on cx/cy
+ *
+ * @param g graphics object
+ * @param rotation used to draw lines on rotated images
+ * @param solid Draw as solid else dashed
+ * @param colourSet1 Use colour set 1 else 2
+ */
+ void paintCentreLines(Graphics g, double rotation, boolean solid, boolean colourSet1) {
+ /* create cross-hair with a central gap */
+ double cx = centrePoint.getX();
+ double cy = centrePoint.getY();
+ Rectangle2D r = viewBounds;
+ double x = r.getX() - 1,
+ y = r.getY() - 1,
+ w = x + r.getWidth() + 2,
+ h = y + r.getHeight() + 2;
+
+ double aperture = Math.max(cellHeight, cellWidth) / 5.0;
+ Path2D lines = new Path2D.Double();
+ lines.moveTo(cx, y - h);
+ lines.lineTo(cx, cy - aperture);
+ lines.moveTo(cx, cy + aperture);
+ lines.lineTo(cx, h * 2);
+ lines.moveTo(x - w, cy);
+ lines.lineTo(cx - aperture, cy);
+ lines.moveTo(cx + aperture, cy);
+ lines.lineTo(2 * w, cy);
+ Shape s;
+ if (!MathUtil.inTolerance(rotation, 0, 0.009)) {
+ s =
+ AffineTransform.getRotateInstance(Math.toRadians(rotation), cx, cy)
+ .createTransformedShape(lines);
+ s =
+ AffineTransform.getTranslateInstance(
+ getTokenAnchorX() * zoomFactor, getTokenAnchorY() * zoomFactor)
+ .createTransformedShape(s);
+ } else {
+ s = lines;
+ }
+ paintShapeOutLine(g, s, solid, colourSet1);
+ }
+
+ /*
+ Each line is drawn as a sequence of overlapping lines.
+ The following arrays are used to define each stroke
+ */
+ float[] opacities = new float[] {0.15f, 0.85f, 0.6f, 1f};
+ float[] strokeWidths =
+ new float[] {2f * LINE_SIZE, 1.5f * LINE_SIZE, LINE_SIZE, 0.5f * LINE_SIZE};
+ float[] dashPhases =
+ new float[] {
+ 2f * LINE_SIZE + 2f, 2f * LINE_SIZE + 1.75f, 2f * LINE_SIZE + 1f, 2f * LINE_SIZE + 1.25f
+ };
+ float[][] dashes =
+ new float[][] {
+ {4f * LINE_SIZE + 4f, 6f},
+ {4f * LINE_SIZE + 3.5f, 6.5f},
+ {4f * LINE_SIZE + 2f, 8f},
+ {4f * LINE_SIZE + 2.5f, 7.5f}
+ };
+
+ void paintShapeOutLine(Graphics g, Shape shp, boolean solid, boolean colourSet1) {
+ Graphics2D g2d = (Graphics2D) g.create();
+ Composite oldAc = g2d.getComposite();
+ AlphaComposite ac;
+ for (int i = 0; i < 4; i++) {
+ switch (i) {
+ case 0, 1 -> g2d.setColor(colourSet1 ? colours[2] : colours[3]);
+ case 2, 3 -> g2d.setColor(colourSet1 ? colours[0] : colours[1]);
+ }
+ g2d.setStroke(
+ solid
+ ? new BasicStroke(strokeWidths[i])
+ : new BasicStroke(
+ strokeWidths[i],
+ BasicStroke.CAP_ROUND,
+ BasicStroke.JOIN_ROUND,
+ 1f,
+ dashes[i],
+ dashPhases[i]));
+
+ ac = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacities[i]);
+ g2d.setComposite(ac);
+ g2d.draw(shp);
+ }
+ g2d.setComposite(oldAc);
+ g2d.dispose();
+ }
+
+ void paintFootprint(Graphics g, double zoomFactor) {
+ if (noGrid) {
+ paintRings(g, zoomFactor);
+ return;
+ }
+
+ Graphics2D g2d = (Graphics2D) g.create();
+ Shape oldClip = g2d.getClip();
+ AffineTransform oldXform = g2d.getTransform();
+ g2d.setRenderingHints(RENDERING_HINTS);
+
+ g2d.translate(centrePoint.getX(), centrePoint.getY());
+ g2d.scale(zoomFactor, zoomFactor);
+
+ Shape tmpShape;
+
+ Area clipArea = new Area(g2d.getClipBounds());
+ Area tmpClip = new Area(), fpArea = new Area();
+ g2d.setStroke(new BasicStroke(1f));
+ g2d.setColor(colours[4]);
+
+ double footprintScale = footprint.getScale();
+ double yCorrection =
+ isIsoFigure ? footprintScale * footprintBounds.getHeight() / 2d * zoomFactor : 0;
+
+ Shape scaledOutline, scaledFill;
+ /* for drawing sub-cell-sizes */
+ if (footprintScale < 1d) {
+ scaledOutline =
+ AffineTransform.getScaleInstance(footprintScale, footprintScale)
+ .createTransformedShape(gridShapeOutline);
+ scaledFill =
+ AffineTransform.getScaleInstance(footprintScale, footprintScale)
+ .createTransformedShape(gridShapeFill);
+ } else {
+ scaledOutline = gridShapeOutline;
+ scaledFill = gridShapeFill;
+ }
+ for (Point2D pt : cellCentres) {
+ AffineTransform ptXform =
+ AffineTransform.getTranslateInstance(pt.getX(), pt.getY() + yCorrection);
+ if (footprintScale < 1) {
+ g2d.draw(ptXform.createTransformedShape(gridShapeOutline));
+ }
+ g2d.draw(ptXform.createTransformedShape(scaledOutline));
+ tmpShape = ptXform.createTransformedShape(scaledFill);
+ fpArea.add(new Area(tmpShape));
+ }
+ tmpClip.subtract(fpArea);
+ g2d.setClip(tmpClip);
+ g2d.setPaint(FlatLaf.isLafDark() ? new Color(1f, 1f, 1f, 0.35f) : new Color(0, 0, 0, 0.35f));
+ g2d.setClip(fpArea);
+ g2d.fill(fpArea);
+
+ g2d.setClip(clipArea);
+ paintShapeOutLine(g2d, fpArea, true, true);
+ g2d.setTransform(oldXform);
+ g2d.setClip(oldClip);
+
+ g2d.dispose();
+ paintExtraGuides(g);
+ }
+
+ void paintExtraGuides(Graphics g) {
+ if (noGrid) {
+ return;
+ }
+ Graphics2D g2d = (Graphics2D) g.create();
+ g2d.setRenderingHints(RENDERING_HINTS);
+
+ g2d.translate(
+ centrePoint.getX(),
+ centrePoint.getY() + (isIsoFigure ? footprintBounds.getHeight() * zoomFactor / 2d : 0));
+
+ g2d.setPaint(colours[4]);
+ g2d.setStroke(
+ new BasicStroke(
+ 0.5f,
+ BasicStroke.CAP_ROUND,
+ BasicStroke.JOIN_ROUND,
+ 10f,
+ new float[] {0.5f, 1.75f},
+ 0));
+
+ double limit = Math.hypot(g2d.getClipBounds().getWidth(), g2d.getClipBounds().getHeight());
+ double radius = footprintBounds.getHeight() * zoomFactor;
+ Rectangle2D bounds = gridShapeOutline.getBounds2D();
+ while (radius < limit) {
+ radius += 2 * cellHeight * zoomFactor;
+ Shape s =
+ AffineTransform.getScaleInstance(
+ radius / bounds.getHeight(), radius / bounds.getHeight())
+ .createTransformedShape(gridShapeOutline);
+ g2d.draw(s);
+ }
+ g2d.dispose();
+ }
+
+ void paintToken(Graphics g, boolean translucent) {
+ Graphics2D g2d = (Graphics2D) g.create();
+ g2d.setRenderingHints(RENDERING_HINTS);
+ if (centreMark == null) {
+ createCentreMark();
+ }
+
+ g2d.translate(centrePoint.getX(), centrePoint.getY());
+ g2d.translate(getTokenAnchorX() * zoomFactor, getTokenAnchorY() * zoomFactor);
+
+ Composite oldAc = g2d.getComposite();
+
+ AffineTransform imageXform =
+ AffineTransform.getTranslateInstance(
+ -workImage.getWidth() / 2d, -workImage.getHeight() / 2d);
+
+ if (translucent) {
+ AlphaComposite ac = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.4f);
+ g2d.setComposite(ac);
+ g2d.drawImage(workImage, imageXform, null);
+ g2d.setComposite(oldAc);
+ } else {
+ g2d.drawImage(workImage, imageXform, null);
+ }
+ g2d.dispose();
+ double rotAngle = getTokenImageRotation();
+ if (rotAngle > 0.01 && rotAngle < 359.99) {
+ paintCentreLines(g, rotAngle, false, true);
+ }
+ }
+ }
+}
diff --git a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutRenderPanel.java b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutRenderPanel.java
new file mode 100644
index 0000000000..5715776b3d
--- /dev/null
+++ b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenLayoutRenderPanel.java
@@ -0,0 +1,356 @@
+/*
+ * This software Copyright by the RPTools.net development team, and
+ * licensed under the Affero GPL Version 3 or, at your option, any later
+ * version.
+ *
+ * MapTool Source Code is distributed in the hope that it will be
+ * useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License * along with this source Code. If not, please visit
+ * and specifically the Affero license
+ * text at .
+ */
+package net.rptools.maptool.client.ui.token.dialog.edit;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import javax.swing.*;
+import net.rptools.lib.MathUtil;
+import net.rptools.maptool.client.AppStyle;
+import net.rptools.maptool.client.MapTool;
+import net.rptools.maptool.client.swing.SwingUtil;
+import net.rptools.maptool.language.I18N;
+import net.rptools.maptool.model.Token;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+/**
+ * Support class used by the token editor dialog on the "Properties" tab to allow a token's image to
+ * be moved around within a one-cell grid area. Scaling is supported using the mousewheel and
+ * position is supported using left-drag. We should add rotation ability using Shift-mousewheel as
+ * well.
+ *
+ * @author trevor
+ */
+public class TokenLayoutRenderPanel extends JPanel {
+ private static final Logger log = LogManager.getLogger(TokenLayoutRenderPanel.class);
+
+ public TokenLayoutRenderPanel() {
+ evtTarget = MouseTarget.NONE;
+ addRenderPaneListeners();
+ setFocusable(true);
+ addComponentListener(
+ new ComponentAdapter() {
+ @Override
+ public void componentShown(ComponentEvent e) {
+ super.componentShown(e);
+ size = getSize();
+ requestFocus();
+ }
+
+ @Override
+ public void componentResized(ComponentEvent e) {
+ super.componentResized(e);
+ size = getSize();
+ zoomFactor = -1;
+ calcZoomFactor();
+ }
+ });
+ log.debug("New TokenLayoutPanel");
+ }
+
+ private TokenLayoutPanelHelper helper;
+
+ protected void setHelper(TokenLayoutPanelHelper h) {
+ helper = h;
+ }
+
+ public TokenLayoutPanelHelper getHelper() {
+ return helper;
+ }
+
+ private enum MouseTarget {
+ NONE,
+ IMAGE_OFFSET,
+ ROTATION,
+ SCALE,
+ VIEW_OFFSET,
+ ZOOM
+ }
+
+ private MouseTarget evtTarget;
+ private Token token;
+ private int viewOffsetX = 0, viewOffsetY = 0, dragStartX, dragStartY;
+ private int maxXoff = 100, maxYoff = 100;
+
+ public void setMaxXoff(int maxX) {
+ maxXoff = maxX;
+ }
+
+ public void setMaxYoff(int maxY) {
+ maxYoff = maxY;
+ }
+
+ private double zoomFactor = -1.0;
+
+ public void setInitialScale(double initialScale) {
+ this.initialScale = initialScale;
+ }
+
+ private double initialScale = 1.0;
+ public Supplier zoom = () -> zoomFactor;
+ public Consumer zoomSet = d -> zoomFactor = MathUtil.doublePrecision(d.doubleValue(), 4);
+
+ Supplier getZoomSupplier() {
+ return zoom;
+ }
+
+ Consumer getZoomConsumer() {
+ return zoomSet;
+ }
+
+ public double getZoomFactor() {
+ return zoom.get().doubleValue();
+ }
+
+ private void setZoomFactor(double d) {
+ d = MathUtil.doublePrecision(d, 4);
+ if (d == zoomFactor) {
+ return;
+ }
+ if (helper.zoomPair.getSpinnerValue() != zoomFactor) {
+ helper.zoomPair.setValue(d);
+ }
+ }
+
+ Dimension size = getSize();
+
+ protected Point2D.Double getViewOffset() {
+ return new Point2D.Double(viewOffsetX, viewOffsetY);
+ }
+
+ public void setToken(Token token) {
+ log.debug("Setting token");
+ zoomFactor = -1d;
+ this.token = token;
+ helper.setToken(token, false);
+ setInitialScale(helper.getTokenSizeScale());
+ calcZoomFactor();
+ }
+
+ public void reset(Token token) {
+ setToken(token);
+ }
+
+ /** Work out a zoom factor to fit the token on screen with a half cell border */
+ protected void calcZoomFactor() {
+ Rectangle2D fpBounds =
+ new Rectangle2D.Double(
+ helper.footprintBounds.getX(),
+ helper.footprintBounds.getY(),
+ helper.footprintBounds.getWidth(),
+ helper.footprintBounds.getHeight());
+ if (token == null || getSize().height == 0 || size == null) {
+ return;
+ }
+ fpBounds.setRect(
+ fpBounds.getWidth() / 2d,
+ fpBounds.getHeight() / 2d,
+ fpBounds.getWidth(),
+ fpBounds.getHeight());
+ if (helper.getTokenAnchorX() != 0 || helper.getTokenAnchorY() != 0) {
+ fpBounds.add(
+ new Rectangle2D.Double(
+ fpBounds.getX() + helper.getTokenAnchorX(),
+ fpBounds.getY() + helper.getTokenAnchorY(),
+ fpBounds.getWidth() + helper.getTokenAnchorX(),
+ fpBounds.getHeight() + helper.getTokenAnchorX()));
+ }
+ if (getHelper().isIsoFigure) {
+ double th = token.getHeight() * fpBounds.getWidth() / token.getWidth();
+ double iso_ho = fpBounds.getHeight() - th;
+ fpBounds.add(
+ new Rectangle2D.Double(
+ fpBounds.getX(), fpBounds.getY() - iso_ho, fpBounds.getWidth(), th));
+ }
+ double fitWidth = fpBounds.getWidth() + helper.grid.getCellWidth() / 2;
+ double fitHeight = fpBounds.getHeight() + helper.grid.getCellHeight() / 2;
+ // which axis has the least space to grow
+ boolean scaleToWidth = size.getWidth() - fitWidth < size.getHeight() - fitHeight;
+ // set the zoom-factor
+ double newZoom = scaleToWidth ? size.getWidth() / fitWidth : size.getHeight() / fitHeight;
+
+ setZoomFactor(newZoom);
+ log.debug(
+ "calculated ZoomFactor: "
+ + zoomFactor
+ + "\nSize: "
+ + size
+ + "\nFootprint bounds: "
+ + fpBounds);
+ }
+
+ private void addRenderPaneListeners() {
+ log.debug("addRenderPaneListeners");
+ addMouseListener(
+ new MouseAdapter() {
+ String old;
+
+ @Override
+ public void mouseReleased(MouseEvent e) {
+ super.mouseReleased(e);
+ dragStartX = -1;
+ dragStartY = -1;
+ evtTarget = MouseTarget.NONE;
+ helper.iFeelDirty();
+ }
+
+ @Override
+ public void mousePressed(MouseEvent e) {
+ dragStartX = e.getX();
+ dragStartY = e.getY();
+ if (SwingUtilities.isLeftMouseButton(e)) {
+ // start token drag
+ evtTarget = MouseTarget.IMAGE_OFFSET;
+ } else if (SwingUtilities.isRightMouseButton(e)) {
+ // start view drag
+ evtTarget = MouseTarget.VIEW_OFFSET;
+ }
+ }
+
+ @Override
+ public void mouseEntered(MouseEvent e) {
+ old = MapTool.getFrame().getStatusMessage();
+ MapTool.getFrame()
+ .setStatusMessage(I18N.getString("EditTokenDialog.status.layout.instructions"));
+ }
+
+ @Override
+ public void mouseExited(MouseEvent e) {
+ if (old != null) MapTool.getFrame().setStatusMessage(old);
+ evtTarget = MouseTarget.NONE;
+ helper.iFeelDirty();
+ }
+
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() == 2) {
+ viewOffsetX = 0;
+ viewOffsetY = 0;
+ maxXoff = 100;
+ maxYoff = 100;
+ helper.resetPanel();
+ evtTarget = MouseTarget.NONE;
+ }
+ if (SwingUtilities.isRightMouseButton(e) && e.getClickCount() == 2) {
+ viewOffsetX = 0;
+ viewOffsetY = 0;
+ maxXoff = 100;
+ maxYoff = 100;
+ helper.resetPanelToDefault();
+ evtTarget = MouseTarget.NONE;
+ }
+ }
+ });
+ addMouseWheelListener(
+ new MouseWheelListener() {
+ @Override
+ public void mouseWheelMoved(MouseWheelEvent e) {
+ double delta = e.getPreciseWheelRotation();
+ if (delta == 0) {
+ return;
+ }
+ evtTarget =
+ SwingUtil.isShiftDown(e)
+ ? MouseTarget.ROTATION
+ : SwingUtil.isControlDown(e) ? MouseTarget.ZOOM : MouseTarget.SCALE;
+
+ switch (evtTarget) {
+ case MouseTarget.ROTATION -> {
+ helper.rotationPair.incrementValue(delta);
+ }
+ case MouseTarget.ZOOM -> {
+ helper.zoomPair.incrementValue(delta);
+ setZoomFactor(helper.zoomPair.getSpinnerValue());
+ }
+ case MouseTarget.SCALE -> {
+ // Only for NOT snap-to-scale
+ if (!token.isSnapToScale()) {
+ return;
+ }
+ helper.scalePair.incrementValue(delta / 16d);
+ }
+ default -> log.debug("Defaulting - invalid mouse event target.");
+ }
+ evtTarget = MouseTarget.NONE;
+ helper.iFeelDirty();
+ }
+ });
+ addMouseMotionListener(
+ new MouseMotionAdapter() {
+ @Override
+ public void mouseDragged(MouseEvent e) {
+ int dx = e.getX() - dragStartX;
+ int dy = e.getY() - dragStartY;
+ if (evtTarget == MouseTarget.IMAGE_OFFSET) {
+ int offX = MathUtil.constrainInt(helper.getTokenAnchorX() + dx, -maxXoff, maxXoff);
+ int offY = MathUtil.constrainInt(helper.getTokenAnchorY() + dy, -maxYoff, maxYoff);
+ helper.anchorXPair.setValue(offX);
+ helper.anchorYPair.setValue(offY);
+
+ } else if (evtTarget == MouseTarget.VIEW_OFFSET) {
+ // drag view
+ viewOffsetX += dx;
+ viewOffsetY += dy;
+ repaint();
+ }
+ dragStartX = e.getX();
+ dragStartY = e.getY();
+ }
+ });
+ }
+
+ @Override
+ protected void paintBorder(Graphics g) {
+ Graphics2D g2d = (Graphics2D) g;
+ AppStyle.shadowBorder.paintWithin(g2d, 0, 0, getSize().width, getSize().height);
+ super.paintBorder(g);
+ }
+
+ @Override
+ protected void paintComponent(Graphics g) {
+ helper.renderBits.init();
+ if (helper.renderBits.viewBounds == null) {
+ return;
+ }
+ Graphics2D g2d = (Graphics2D) g;
+ g2d.setRenderingHints(TokenLayoutPanelHelper.RenderBits.RENDERING_HINTS);
+ // Cleanup
+ g2d.clearRect(0, 0, size.width, size.height);
+ // Background
+ g2d.setPaint(TokenLayoutPanelHelper.RenderBits.backgroundTexture);
+ g2d.fillRect(0, 0, size.width, size.height);
+
+ if (helper.getTokenImage() == null || size.width == 0) {
+ return;
+ }
+ if (zoomFactor == -1) {
+ calcZoomFactor();
+ return;
+ }
+ // Footprint
+ helper.renderBits.paintFootprint(g, zoomFactor);
+ // Add horizontal and vertical lines to help with centering
+ helper.renderBits.paintCentreLines(g, 0, true, false);
+ // Token
+ helper.renderBits.paintToken(g, evtTarget.equals(MouseTarget.IMAGE_OFFSET));
+ helper.renderBits.paintCentreMark(g);
+ g2d.dispose();
+ }
+}
diff --git a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.form b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.form
index 57752636be..a28d3ff1a7 100644
--- a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.form
+++ b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.form
@@ -1,16 +1,16 @@
diff --git a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.java b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.java
index 30bee1f16a..d2a0dc7bb7 100644
--- a/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.java
+++ b/src/main/java/net/rptools/maptool/client/ui/token/dialog/edit/TokenPropertiesDialog.java
@@ -23,11 +23,11 @@ public class TokenPropertiesDialog {
private JPanel headPanel;
private JPanel buttonPanel;
private JLabel tokenImage;
- private JTabbedPane tabPanel;
private HtmlEditorSplit gmNotesEditor;
private HtmlEditorSplit playerNotesEditor;
private JComboBox comboBox1;
private JComboBox comboBox2;
+ private JTabbedPane tabPanel;
private JLabel ownershipList;
public JComponent getRootComponent() {
diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/TokenLocation.java b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/TokenLocation.java
index 1f95f76940..dd4b65b8cd 100644
--- a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/TokenLocation.java
+++ b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/TokenLocation.java
@@ -20,7 +20,7 @@
import net.rptools.lib.CodeTimer;
import net.rptools.maptool.model.Token;
-class TokenLocation {
+public class TokenLocation {
private final ZoneRenderer renderer;
public Area bounds;
diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java
index 380057c1e7..b73557b822 100644
--- a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java
+++ b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/ZoneRenderer.java
@@ -38,6 +38,7 @@
import javax.swing.*;
import net.rptools.lib.CodeTimer;
import net.rptools.lib.MD5Key;
+import net.rptools.lib.image.ImageUtil;
import net.rptools.maptool.client.*;
import net.rptools.maptool.client.events.ZoneLoaded;
import net.rptools.maptool.client.functions.TokenMoveFunctions;
@@ -57,6 +58,7 @@
import net.rptools.maptool.client.ui.token.BarTokenOverlay;
import net.rptools.maptool.client.ui.token.dialog.create.NewTokenDialog;
import net.rptools.maptool.client.ui.zone.*;
+import net.rptools.maptool.client.ui.zone.renderer.tokenRender.TokenRenderer;
import net.rptools.maptool.client.walker.ZoneWalker;
import net.rptools.maptool.events.MapToolEventBus;
import net.rptools.maptool.language.I18N;
@@ -148,6 +150,7 @@ public class ZoneRenderer extends JComponent implements DropTargetListener {
private final ZoneCompositor compositor;
private final GridRenderer gridRenderer;
private final HaloRenderer haloRenderer;
+ private final TokenRenderer tokenRenderer;
private final LightsRenderer lightsRenderer;
private final DarknessRenderer darknessRenderer;
private final LumensRenderer lumensRenderer;
@@ -176,6 +179,7 @@ public ZoneRenderer(Zone zone) {
this.compositor = new ZoneCompositor();
this.gridRenderer = new GridRenderer();
this.haloRenderer = new HaloRenderer();
+ this.tokenRenderer = new TokenRenderer();
this.lightsRenderer = new LightsRenderer(renderHelper, zone, zoneView);
this.darknessRenderer = new DarknessRenderer(renderHelper, zoneView);
this.lumensRenderer = new LumensRenderer(renderHelper, zone, zoneView);
@@ -285,7 +289,7 @@ public void setZoneScale(Scale scale) {
scale.addPropertyChangeListener(
evt -> {
if (Scale.PROPERTY_SCALE.equals(evt.getPropertyName())) {
- tokenLocationCache.clear();
+ clearZoomDependantCaches();
}
if (Scale.PROPERTY_OFFSET.equals(evt.getPropertyName())) {
// flushFog = true;
@@ -892,6 +896,7 @@ public void renderZone(Graphics2D g2d, PlayerView view) {
if (!compositor.isInitialised()) compositor.setRenderer(this);
if (!gridRenderer.isInitialised()) gridRenderer.setRenderer(this);
if (!haloRenderer.isInitialised()) haloRenderer.setRenderer(this);
+ if (!tokenRenderer.isInitialised()) tokenRenderer.setRenderer(this);
Rectangle viewRect = new Rectangle(getSize().width, getSize().height);
@@ -1455,19 +1460,8 @@ protected void showBlockedMoves(Graphics2D g, PlayerView view, Set
footprintBounds = token.getBounds(zone);
}
// Draw token
- double iso_ho = 0;
+ double iso_ho = ImageUtil.getIsoFigureHeightOffset(token, footprintBounds) * getScale();
Dimension imgSize = new Dimension(workImage.getWidth(), workImage.getHeight());
- if (token.getShape() == TokenShape.FIGURE) {
- double th = token.getHeight() * (double) footprintBounds.width / token.getWidth();
- iso_ho = footprintBounds.height - th;
- footprintBounds =
- new Rectangle(
- footprintBounds.x,
- footprintBounds.y - (int) iso_ho,
- footprintBounds.width,
- (int) th);
- iso_ho = iso_ho * getScale();
- }
SwingUtil.constrainTo(imgSize, footprintBounds.width, footprintBounds.height);
int offsetx = 0;
@@ -2015,60 +2009,6 @@ private List getTokenLocations(Zone.Layer layer) {
return tokenLocationMap.computeIfAbsent(layer, k -> new LinkedList<>());
}
- // TODO: I don't like this hardwiring
- protected Shape getFigureFacingArrow(int angle, int size) {
- int base = (int) (size * .75);
- int width = (int) (size * .35);
-
- facingArrow = new GeneralPath();
- facingArrow.moveTo(base, -width);
- facingArrow.lineTo(size, 0);
- facingArrow.lineTo(base, width);
- facingArrow.lineTo(base, -width);
-
- GeneralPath gp =
- (GeneralPath)
- facingArrow.createTransformedShape(
- AffineTransform.getRotateInstance(-Math.toRadians(angle)));
- return gp.createTransformedShape(AffineTransform.getScaleInstance(getScale(), getScale() / 2));
- }
-
- // TODO: I don't like this hardwiring
- protected Shape getCircleFacingArrow(int angle, int size) {
- int base = (int) (size * .75);
- int width = (int) (size * .35);
-
- facingArrow = new GeneralPath();
- facingArrow.moveTo(base, -width);
- facingArrow.lineTo(size, 0);
- facingArrow.lineTo(base, width);
- facingArrow.lineTo(base, -width);
-
- GeneralPath gp =
- (GeneralPath)
- facingArrow.createTransformedShape(
- AffineTransform.getRotateInstance(-Math.toRadians(angle)));
- return gp.createTransformedShape(AffineTransform.getScaleInstance(getScale(), getScale()));
- }
-
- // TODO: I don't like this hardwiring
- protected Shape getSquareFacingArrow(int angle, int size) {
- int base = (int) (size * .75);
- int width = (int) (size * .35);
-
- facingArrow = new GeneralPath();
- facingArrow.moveTo(0, 0);
- facingArrow.lineTo(-(size - base), -width);
- facingArrow.lineTo(-(size - base), width);
- facingArrow.lineTo(0, 0);
-
- GeneralPath gp =
- (GeneralPath)
- facingArrow.createTransformedShape(
- AffineTransform.getRotateInstance(-Math.toRadians(angle)));
- return gp.createTransformedShape(AffineTransform.getScaleInstance(getScale(), getScale()));
- }
-
protected void renderTokens(Graphics2D g, List tokenList, PlayerView view) {
renderTokens(g, tokenList, view, false);
}
@@ -2118,7 +2058,6 @@ protected void renderTokens(
// Is that because the rendering pipeline thinks they've already been drawn so isn't forced to
// re-render them? So how does this cache get filled then? It's not part of the campaign
// state...
- // tokenLocationCache.clear();
List tokenPostProcessing = new ArrayList(tokenList.size());
for (Token token : tokenList) {
@@ -2165,12 +2104,6 @@ protected void renderTokens(
double scaledWidth = (footprintBounds.width * scale);
double scaledHeight = (footprintBounds.height * scale);
- // if (!token.isStamp()) {
- // // Fit inside the grid
- // scaledWidth --;
- // scaledHeight --;
- // }
-
ScreenPoint tokenScreenLocation =
ScreenPoint.fromZonePoint(this, footprintBounds.x, footprintBounds.y);
timer.stop("tokenlist-1c");
@@ -2186,14 +2119,8 @@ protected void renderTokens(
double sx = scaledWidth / 2 + x - (token.getAnchor().x * scale);
double sy = scaledHeight / 2 + y - (token.getAnchor().y * scale);
tokenBounds.transform(
- AffineTransform.getRotateInstance(
- Math.toRadians(token.getFacingInDegrees()), sx, sy)); // facing
- // defaults
- // to
- // down,
- // or
- // -90
- // degrees
+ AffineTransform.getRotateInstance(Math.toRadians(token.getFacingInDegrees()), sx, sy));
+ // facing defaults to down or -90 degrees
}
timer.stop("tokenlist-1d");
@@ -2228,7 +2155,6 @@ protected void renderTokens(
}
// Markers
timer.start("renderTokens:Markers");
- // System.out.println("Token " + token.getName() + " is a marker? " + token.isMarker());
if (token.isMarker() && canSeeMarker(token)) {
markerLocationList.add(location);
}
@@ -2237,13 +2163,10 @@ protected void renderTokens(
// Stacking check
if (calculateStacks) {
timer.start("tokenStack");
- // System.out.println(token.getName() + " - " + location.boundsCache);
Set tokenStackSet = null;
for (TokenLocation currLocation : getTokenLocations(Zone.Layer.TOKEN)) {
// Are we covering anyone ?
- // System.out.println("\t" + currLocation.token.getName() + " - " +
- // location.boundsCache.contains(currLocation.boundsCache));
if (location.boundsCache.contains(currLocation.boundsCache)) {
if (tokenStackSet == null) {
tokenStackSet = new HashSet();
@@ -2384,11 +2307,6 @@ protected void renderTokens(
// Rotated
if (token.hasFacing() && token.getShape() == Token.TokenShape.TOP_DOWN) {
- // Jamz: Test, rotate on NW corner
- // at.rotate(Math.toRadians(token.getFacingInDegrees()), (token.getAnchor().x * scale) -
- // offsetx,
- // (token.getAnchor().y * scale) - offsety);
-
at.rotate(
Math.toRadians(token.getFacingInDegrees()),
location.scaledWidth / 2 - (token.getAnchor().x * scale) - offsetx,
@@ -2414,9 +2332,12 @@ protected void renderTokens(
haloRenderer.renderHalo(tokenG, token, location);
// Calculate alpha Transparency from token and use opacity for indicating that token is moving
- float opacity = token.getTokenOpacity();
+ float opacity = token.getOpacity();
if (isTokenMoving(token)) opacity = opacity / 2.0f;
-
+ Map renderInfo = new HashMap<>();
+ renderInfo.put(TokenRenderer.OPACITY, opacity);
+ renderInfo.put(TokenRenderer.GRAPHICS, tokenG);
+ renderInfo.put(TokenRenderer.LOCATION, location);
// Finally render the token image
timer.start("tokenlist-7");
if (!isGMView && zoneView.isUsingVision() && (token.getShape() == Token.TokenShape.FIGURE)) {
@@ -2425,24 +2346,14 @@ protected void renderTokens(
// the cell intersects visible area so
if (zone.getGrid().checkCenterRegion(cb.getBounds(), visibleScreenArea)) {
// if we can see the centre, draw the whole token
- Composite oldComposite = tokenG.getComposite();
- if (opacity < 1.0f) {
- tokenG.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity));
- }
- tokenG.drawImage(workImage, at, this);
- tokenG.setComposite(oldComposite);
- // g.draw(cb); // debugging
+ tokenRenderer.renderToken(token, renderInfo);
+
} else {
// else draw the clipped token
Area cellArea = new Area(visibleScreenArea);
cellArea.intersect(cb);
- tokenG.setClip(cellArea);
- Composite oldComposite = tokenG.getComposite();
- if (opacity < 1.0f) {
- tokenG.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity));
- }
- tokenG.drawImage(workImage, at, this);
- tokenG.setComposite(oldComposite);
+ renderInfo.put(TokenRenderer.CLIP, cellArea);
+ tokenRenderer.renderToken(token, renderInfo);
}
}
} else if (!isGMView && zoneView.isUsingVision() && token.isAlwaysVisible()) {
@@ -2452,142 +2363,27 @@ protected void renderTokens(
// if we can see a portion of the stamp/token, draw the whole thing, defaults to 2/9ths
if (zone.getGrid()
.checkRegion(cb.getBounds(), visibleScreenArea, token.getAlwaysVisibleTolerance())) {
- Composite oldComposite = tokenG.getComposite();
- if (opacity < 1.0f) {
- tokenG.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity));
- }
- tokenG.drawImage(workImage, at, this);
- tokenG.setComposite(oldComposite);
+ tokenRenderer.renderToken(token, renderInfo);
} else {
// else draw the clipped stamp/token
// This will only show the part of the token that does not have VBL on it
// as any VBL on the token will block LOS, affecting the clipping.
Area cellArea = new Area(visibleScreenArea);
cellArea.intersect(cb);
- tokenG.setClip(cellArea);
- Composite oldComposite = tokenG.getComposite();
- if (opacity < 1.0f) {
- tokenG.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity));
- }
- tokenG.drawImage(workImage, at, this);
- tokenG.setComposite(oldComposite);
+ renderInfo.put(TokenRenderer.CLIP, cellArea);
+ tokenRenderer.renderToken(token, renderInfo);
}
}
} else {
// fallthrough normal token rendered against visible area
- Composite oldComposite = tokenG.getComposite();
- if (opacity < 1.0f) {
- tokenG.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity));
- }
- tokenG.drawImage(workImage, at, this);
- tokenG.setComposite(oldComposite);
+ tokenRenderer.renderToken(token, renderInfo);
}
timer.stop("tokenlist-7");
timer.start("tokenlist-8");
- // Halo (SQUARE)
- // XXX Why are square halos drawn separately?!
- /*
- * if (token.hasHalo() && token.getShape() == Token.TokenShape.SQUARE) { Stroke oldStroke = g.getStroke(); clippedG.setStroke(new BasicStroke(AppPreferences.getHaloLineWidth()));
- * clippedG.setColor(token.getHaloColor()); clippedG.draw(new Rectangle2D.Double(location.x, location.y, location.scaledWidth, location.scaledHeight)); clippedG.setStroke(oldStroke); }
- */
-
- // Facing ?
- // TODO: Optimize this by doing it once per token per facing
+ // Facing
if (token.hasFacing()) {
- Token.TokenShape tokenType = token.getShape();
- switch (tokenType) {
- case FIGURE:
- if (token.getHasImageTable() && !AppPreferences.forceFacingArrow.get()) {
- break;
- }
- Shape arrow = getFigureFacingArrow(token.getFacing(), footprintBounds.width / 2);
-
- if (!zone.getGrid().isIsometric()) {
- arrow = getCircleFacingArrow(token.getFacing(), footprintBounds.width / 2);
- }
-
- double fx = location.x + location.scaledWidth / 2;
- double fy = location.y + location.scaledHeight / 2;
-
- tokenG.translate(fx, fy);
- if (token.getFacing() < 0) {
- tokenG.setColor(Color.yellow);
- } else {
- tokenG.setColor(ZoneRendererConstants.TRANSLUCENT_YELLOW);
- }
- tokenG.fill(arrow);
- tokenG.setColor(Color.darkGray);
- tokenG.draw(arrow);
- tokenG.translate(-fx, -fy);
- break;
- case TOP_DOWN:
- if (!AppPreferences.forceFacingArrow.get()) {
- break;
- }
- case CIRCLE:
- arrow = getCircleFacingArrow(token.getFacing(), footprintBounds.width / 2);
- if (zone.getGrid().isIsometric()) {
- arrow = getFigureFacingArrow(token.getFacing(), footprintBounds.width / 2);
- }
-
- double cx = location.x + location.scaledWidth / 2;
- double cy = location.y + location.scaledHeight / 2;
-
- tokenG.translate(cx, cy);
- tokenG.setColor(Color.yellow);
- tokenG.fill(arrow);
- tokenG.setColor(Color.darkGray);
- tokenG.draw(arrow);
- tokenG.translate(-cx, -cy);
- break;
- case SQUARE:
- if (zone.getGrid().isIsometric()) {
- arrow = getFigureFacingArrow(token.getFacing(), footprintBounds.width / 2);
- cx = location.x + location.scaledWidth / 2;
- cy = location.y + location.scaledHeight / 2;
- } else {
- int facing = token.getFacing();
- while (facing < 0) {
- facing += 360;
- } // TODO: this should really be done in Token.setFacing() but I didn't want to take
- // the chance
- // of breaking something, so change this when it's safe to break stuff
- facing %= 360;
- arrow = getSquareFacingArrow(facing, footprintBounds.width / 2);
-
- cx = location.x + location.scaledWidth / 2;
- cy = location.y + location.scaledHeight / 2;
-
- // Find the edge of the image
- // TODO: Man, this is horrible, there's gotta be a better way to do this
- double xp = location.scaledWidth / 2;
- double yp = location.scaledHeight / 2;
- if (facing >= 45 && facing <= 135 || facing >= 225 && facing <= 315) {
- xp = (int) (yp / Math.tan(Math.toRadians(facing)));
- if (facing > 180) {
- xp = -xp;
- yp = -yp;
- }
- } else {
- yp = (int) (xp * Math.tan(Math.toRadians(facing)));
- if (facing > 90 && facing < 270) {
- xp = -xp;
- yp = -yp;
- }
- }
- cx += xp;
- cy -= yp;
- }
-
- tokenG.translate(cx, cy);
- tokenG.setColor(Color.yellow);
- tokenG.fill(arrow);
- tokenG.setColor(Color.darkGray);
- tokenG.draw(arrow);
- tokenG.translate(-cx, -cy);
- break;
- }
+ tokenRenderer.paintFacingArrow(tokenG, token, footprintBounds, location);
}
timer.stop("tokenlist-8");
@@ -2653,7 +2449,7 @@ protected void renderTokens(
}
overlay.paintOverlay(locg, token, bounds, barValue);
- } // endfor
+ }
locg.dispose();
timer.stop("tokenlist-10");
@@ -2664,13 +2460,6 @@ protected void renderTokens(
tokenPostProcessing.add(token);
}
timer.stop("tokenlist-11");
-
- // DEBUGGING
- // ScreenPoint tmpsp = ScreenPoint.fromZonePoint(this, new ZonePoint(token.getX(),
- // token.getY()));
- // g.setColor(Color.red);
- // g.drawLine(tmpsp.x, 0, tmpsp.x, getSize().height);
- // g.drawLine(0, tmpsp.y, getSize().width, tmpsp.y);
}
timer.start("tokenlist-12");
boolean useIF = MapTool.getServerPolicy().isUseIndividualFOW();
@@ -2844,10 +2633,6 @@ protected void renderTokens(
}
// Markers
- // for (TokenLocation location : getMarkerLocations()) {
- // BufferedImage stackImage = AppStyle.markerImage;
- // g.drawImage(stackImage, location.bounds.getBounds().x, location.bounds.getBounds().y, null);
- // }
if (clippedG != g) {
clippedG.dispose();
@@ -3142,13 +2927,18 @@ public void setScale(double scale) {
/*
* MCL: I think it is correct to clear these caches (if not more).
*/
- tokenLocationCache.clear();
- invalidateCurrentViewCache();
+ clearZoomDependantCaches();
zoneScale.zoomScale(getWidth() / 2, getHeight() / 2, scale);
MapTool.getFrame().getZoomStatusBar().update();
}
}
+ private void clearZoomDependantCaches() {
+ tokenLocationCache.clear();
+ invalidateCurrentViewCache();
+ tokenRenderer.zoomChanged();
+ }
+
public double getScale() {
return zoneScale.getScale();
}
diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/tokenRender/FacingArrowRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/tokenRender/FacingArrowRenderer.java
new file mode 100644
index 0000000000..e4ab988ac7
--- /dev/null
+++ b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/tokenRender/FacingArrowRenderer.java
@@ -0,0 +1,234 @@
+/*
+ * This software Copyright by the RPTools.net development team, and
+ * licensed under the Affero GPL Version 3 or, at your option, any later
+ * version.
+ *
+ * MapTool Source Code is distributed in the hope that it will be
+ * useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License * along with this source Code. If not, please visit
+ * and specifically the Affero license
+ * text at .
+ */
+package net.rptools.maptool.client.ui.zone.renderer.tokenRender;
+
+import java.awt.*;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Path2D;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import net.rptools.lib.CodeTimer;
+import net.rptools.maptool.client.AppPreferences;
+import net.rptools.maptool.client.ui.zone.renderer.TokenLocation;
+import net.rptools.maptool.client.ui.zone.renderer.ZoneRenderer;
+import net.rptools.maptool.events.MapToolEventBus;
+import net.rptools.maptool.model.Grid;
+import net.rptools.maptool.model.Token;
+import net.rptools.maptool.model.Zone;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+
+public class FacingArrowRenderer {
+ private static final Logger log = LogManager.getLogger(FacingArrowRenderer.class);
+ private final CodeTimer timer;
+ private final Map> quivers = new HashMap<>();
+ Rectangle footprintBounds;
+ Token.TokenShape tokenType;
+ ArrowType arrowType;
+ double scale;
+ private boolean initialised = false;
+ private ZoneRenderer renderer;
+ private boolean isIsometric;
+ private ArrayList fillColours = new ArrayList<>();
+ private Color fillColour = Color.YELLOW;
+ private Color borderColour = Color.DARK_GRAY;
+
+ FacingArrowRenderer() {
+ new MapToolEventBus().getMainEventBus().register(this);
+ for (int i = 0; i <= 90; i++) {
+ fillColours.add(new Color(1 - 0.5f / 90f * i, 1 - 0.5f / 90f * i, 0));
+ }
+ for (int i = 89; i >= 0; i--) {
+ fillColours.add(fillColours.get(i));
+ }
+ timer = CodeTimer.get();
+ }
+
+ public boolean isInitialised() {
+ return initialised;
+ }
+
+ double getScale() {
+ return scale;
+ }
+
+ public void setScale(double scale) {
+ this.scale = scale;
+ }
+
+ public void paintArrow(
+ Graphics2D tokenG,
+ Token token,
+ Rectangle footprintBounds_,
+ TokenLocation location,
+ ZoneRenderer zoneRenderer) {
+ if (!renderer.equals(zoneRenderer)) {
+ setRenderer(zoneRenderer);
+ }
+ tokenType = token.getShape();
+ if (tokenType.equals(Token.TokenShape.TOP_DOWN) && !AppPreferences.forceFacingArrow.get()) {
+ return;
+ }
+ if (tokenType.equals(Token.TokenShape.FIGURE)
+ && token.getHasImageTable()
+ && !AppPreferences.forceFacingArrow.get()) {
+ return;
+ }
+
+ timer.start("ArrowRenderer-paintArrow");
+ AffineTransform oldAT = tokenG.getTransform();
+ Grid grid = renderer.getZone().getGrid();
+ footprintBounds = footprintBounds_;
+ double facing = token.getFacing();
+ facing = isIsometric ? facing + 45 : facing;
+ while (facing < 0) {
+ facing += 360;
+ }
+ if (facing > 360) {
+ facing %= 360;
+ }
+ double radFacing = Math.toRadians(facing);
+ double cx = location.x + location.scaledWidth / 2d;
+ double cy = location.y + location.scaledHeight / 2d;
+
+ Shape facingArrow = getArrow(token);
+
+ facingArrow = AffineTransform.getRotateInstance(-radFacing).createTransformedShape(facingArrow);
+ facingArrow =
+ AffineTransform.getScaleInstance(getScale(), isIsometric ? getScale() / 2d : getScale())
+ .createTransformedShape(facingArrow);
+
+ if (tokenType.equals(Token.TokenShape.SQUARE) && !isIsometric) {
+ double xp = location.scaledWidth / 2;
+ double yp = location.scaledHeight / 2;
+ if (facing >= 45 && facing <= 135 || facing >= 225 && facing <= 315) {
+ xp = yp / Math.tan(Math.toRadians(facing));
+ if (facing > 180) {
+ xp = -xp;
+ yp = -yp;
+ }
+ } else {
+ yp = xp * Math.tan(Math.toRadians(facing));
+ if (facing > 90 && facing < 270) {
+ xp = -xp;
+ yp = -yp;
+ }
+ }
+ cx += xp;
+ cy -= yp;
+ }
+ tokenG.translate(cx, cy);
+
+ if (tokenType.equals(Token.TokenShape.FIGURE) && facing <= 180) {
+ tokenG.setColor(fillColours.get((int) facing));
+ } else {
+ tokenG.setColor(fillColour);
+ }
+
+ tokenG.fill(facingArrow);
+ tokenG.setColor(borderColour);
+ tokenG.draw(facingArrow);
+
+ tokenG.setTransform(oldAT);
+ timer.stop("ArrowRenderer-paintArrow");
+ }
+
+ public void setRenderer(ZoneRenderer zoneRenderer) {
+ timer.start("ArrowRenderer-init");
+ renderer = zoneRenderer;
+ Zone zone = renderer.getZone();
+ isIsometric = zone.getGrid().isIsometric();
+ scale = renderer.getScale();
+ initialised = true;
+ timer.stop("ArrowRenderer-init");
+ }
+
+ private Shape getArrow(Token token) {
+ if ((!AppPreferences.forceFacingArrow.get() && tokenType.equals(Token.TokenShape.TOP_DOWN))
+ || (!AppPreferences.forceFacingArrow.get() && token.getHasImageTable())) {
+ return null;
+ }
+ Shape arrow = new Path2D.Double();
+ if (isIsometric) {
+ arrowType = ArrowType.ISOMETRIC;
+ } else {
+ switch (tokenType) {
+ case FIGURE, CIRCLE -> arrowType = ArrowType.CIRCLE;
+ case SQUARE -> arrowType = ArrowType.SQUARE;
+ default -> arrowType = ArrowType.NONE;
+ }
+ }
+ double size = footprintBounds.getWidth() / 2d;
+ Map quiver;
+ if (quivers.containsKey(arrowType)) {
+ quiver = quivers.get(arrowType);
+ } else {
+ quiver = new HashMap<>();
+ }
+ if (quiver.containsKey(size)) {
+ return quiver.get(size);
+ } else {
+ switch (arrowType) {
+ case CIRCLE -> arrow = getCircleFacingArrow(size);
+ case ISOMETRIC -> arrow = getFigureFacingArrow(size);
+ case SQUARE -> arrow = getSquareFacingArrow(size);
+ }
+ quiver.put(size, arrow);
+ quivers.put(arrowType, quiver);
+ }
+ return arrow;
+ }
+
+ protected Shape getCircleFacingArrow(double size) {
+ double base = size * .75;
+ double y = size * .35;
+ Path2D facingArrow = new Path2D.Double();
+ facingArrow.moveTo(base, -y);
+ facingArrow.lineTo(size, 0);
+ facingArrow.lineTo(base, y);
+ facingArrow.lineTo(base, -y);
+ return facingArrow;
+ }
+
+ protected Shape getFigureFacingArrow(double size) {
+ double base = size * .75;
+ double y = size * .35;
+ Path2D facingArrow = new Path2D.Double();
+ facingArrow.moveTo(base, -y);
+ facingArrow.lineTo(size, 0);
+ facingArrow.lineTo(base, y);
+ facingArrow.lineTo(base, -y);
+ return facingArrow;
+ }
+
+ protected Shape getSquareFacingArrow(double size) {
+ double base = size * .75;
+ double y = size * .35;
+ Path2D facingArrow = new Path2D.Double();
+ facingArrow.moveTo(0, 0);
+ facingArrow.lineTo(-(size - base), -y);
+ facingArrow.lineTo(-(size - base), y);
+ facingArrow.lineTo(0, 0);
+ return facingArrow;
+ }
+
+ private enum ArrowType {
+ CIRCLE,
+ ISOMETRIC,
+ SQUARE,
+ NONE
+ }
+}
diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/renderer/tokenRender/TokenRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/tokenRender/TokenRenderer.java
new file mode 100644
index 0000000000..b112f8eccd
--- /dev/null
+++ b/src/main/java/net/rptools/maptool/client/ui/zone/renderer/tokenRender/TokenRenderer.java
@@ -0,0 +1,220 @@
+/*
+ * This software Copyright by the RPTools.net development team, and
+ * licensed under the Affero GPL Version 3 or, at your option, any later
+ * version.
+ *
+ * MapTool Source Code is distributed in the hope that it will be
+ * useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License * along with this source Code. If not, please visit
+ * and specifically the Affero license
+ * text at .
+ */
+package net.rptools.maptool.client.ui.zone.renderer.tokenRender;
+
+import com.google.common.eventbus.Subscribe;
+import java.awt.*;
+import java.awt.geom.*;
+import java.awt.image.BufferedImage;
+import java.util.HashMap;
+import java.util.Map;
+import net.rptools.lib.CodeTimer;
+import net.rptools.lib.image.ImageUtil;
+import net.rptools.maptool.client.ui.zone.renderer.TokenLocation;
+import net.rptools.maptool.client.ui.zone.renderer.ZoneRenderer;
+import net.rptools.maptool.events.MapToolEventBus;
+import net.rptools.maptool.model.*;
+import net.rptools.maptool.model.zones.GridChanged;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+
+public class TokenRenderer {
+ public static final String GRAPHICS = "graphics";
+ public static final String LOCATION = "location";
+ public static final String OPACITY = "opacity";
+ public static final String CLIP = "clip";
+ private static final Logger log = LogManager.getLogger(TokenRenderer.class);
+ private final CodeTimer timer;
+ private final Map renderImageMap = new HashMap<>();
+ private final Map tokenStateMap = new HashMap<>();
+ private Graphics2D g2d;
+ private TokenLocation location;
+ private Token token;
+ private float opacity = 1f;
+ private Area clip = null;
+ private ZoneRenderer renderer;
+ private Grid grid;
+ private double scale;
+ private boolean isoFigure = false;
+ private boolean canSpin = false;
+ private BufferedImage renderImage;
+ private boolean initialised = false;
+ public FacingArrowRenderer facingArrowRenderer;
+
+ public TokenRenderer() {
+ facingArrowRenderer = new FacingArrowRenderer();
+ new MapToolEventBus().getMainEventBus().register(this);
+ timer = CodeTimer.get();
+ }
+
+ public boolean isInitialised() {
+ return initialised;
+ }
+
+ public void setRenderer(ZoneRenderer zoneRenderer) {
+ timer.start("TokenRenderer-init");
+ renderer = zoneRenderer;
+ Zone zone = renderer.getZone();
+ grid = zone.getGrid();
+
+ scale = renderer.getScale();
+ initialised = true;
+ timer.stop("TokenRenderer-init");
+ }
+
+ public void renderToken(Token token, Map renderInfo) {
+ timer.start("TokenRenderer-renderToken");
+ this.token = token;
+ if (haveSufficientRenderInfo(renderInfo)) {
+ compareStates();
+ paintTokenImage();
+ } else {
+ log.debug("Not enough render info in " + renderInfo);
+ }
+ timer.stop("TokenRenderer-renderToken");
+ }
+
+ private boolean haveSufficientRenderInfo(Map renderInfo) {
+ try {
+ // has minimum keys required
+ if (!(renderInfo.containsKey(GRAPHICS) && renderInfo.containsKey(LOCATION))) {
+ return false;
+ }
+ g2d = (Graphics2D) renderInfo.get(GRAPHICS);
+ location = (TokenLocation) renderInfo.get(LOCATION);
+ if (renderInfo.containsKey(OPACITY)) {
+ opacity = (float) renderInfo.get(OPACITY);
+ } else {
+ opacity = 1f;
+ }
+ if (renderInfo.containsKey(CLIP)) {
+ clip = (Area) renderInfo.get(CLIP);
+ } else {
+ clip = null;
+ }
+ } catch (ClassCastException cce) {
+ log.debug(cce.getLocalizedMessage(), cce);
+ return false;
+ }
+ return true;
+ }
+
+ private void compareStates() {
+ TokenState currentState = createRecord(token);
+ isoFigure =
+ grid.isIsometric()
+ && !currentState.flippedIso
+ && currentState.shape.equals(Token.TokenShape.FIGURE);
+ canSpin = currentState.shape.equals(Token.TokenShape.TOP_DOWN);
+ boolean updateStoredImage;
+ if (!tokenStateMap.containsKey(token)) {
+ updateStoredImage = true;
+ } else {
+ updateStoredImage = !tokenStateMap.get(token).equals(currentState);
+ }
+ tokenStateMap.put(token, currentState);
+ if (updateStoredImage || !renderImageMap.containsKey(token)) {
+ renderImage = ImageUtil.getTokenRenderImage(token, renderer);
+ renderImageMap.put(token, renderImage);
+ } else {
+ renderImage = renderImageMap.get(token);
+ }
+ }
+
+ private void paintTokenImage() {
+ timer.start("TokenRenderer-paintTokenImage");
+ Shape oldClip = g2d.getClip();
+ AffineTransform oldAT = g2d.getTransform();
+ Composite oldComposite = g2d.getComposite();
+ // centre image
+ double imageCx = -renderImage.getWidth() / 2d;
+ double imageCy = -renderImage.getHeight() / (isoFigure ? 4d / 3d : 2d);
+ AffineTransform imageTransform =
+ AffineTransform.getTranslateInstance(
+ imageCx + token.getAnchorX() * scale, imageCy + token.getAnchorY() * scale);
+
+ if (clip != null) {
+ g2d.setClip(clip);
+ }
+ g2d.translate(location.x + location.scaledWidth / 2d, location.y + location.scaledHeight / 2d);
+
+ if (token.hasFacing() && canSpin) {
+ g2d.rotate(Math.toRadians(token.getFacingInDegrees()));
+ }
+ if (opacity < 1.0f) {
+ g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, opacity));
+ }
+
+ g2d.drawImage(renderImage, imageTransform, renderer);
+ g2d.setStroke(new BasicStroke(1f));
+
+ g2d.setComposite(oldComposite);
+ g2d.setTransform(oldAT);
+ g2d.setClip(oldClip);
+ timer.stop("TokenRenderer-paintTokenImage");
+ }
+
+ private TokenState createRecord(Token token) {
+ return new TokenState(
+ token.getFacing(),
+ token.getImageRotation(),
+ token.getScaleX(),
+ token.getScaleY(),
+ token.getSizeScale(),
+ token.isFlippedIso(),
+ token.isFlippedX(),
+ token.isFlippedY(),
+ token.getShape(),
+ token.isSnapToScale(),
+ token.getFootprint(grid));
+ }
+
+ public void zoomChanged() {
+ if (initialised) {
+ renderImageMap.clear();
+ scale = renderer.getScale();
+ facingArrowRenderer.setScale(scale);
+ }
+ }
+
+ @Subscribe
+ private void onGridChanged(GridChanged event) {
+ if (this.initialised) {
+ setRenderer(this.renderer);
+ facingArrowRenderer.setRenderer(this.renderer);
+ }
+ }
+
+ public void paintFacingArrow(
+ Graphics2D tokenG, Token token, Rectangle footprintBounds, TokenLocation location) {
+ if (!facingArrowRenderer.isInitialised()) {
+ facingArrowRenderer.setRenderer(renderer);
+ }
+ facingArrowRenderer.paintArrow(tokenG, token, footprintBounds, location, renderer);
+ }
+
+ private record TokenState(
+ int facing,
+ double imageRotation,
+ double scaleX,
+ double scaleY,
+ double sizeScale,
+ boolean flippedIso,
+ boolean flippedX,
+ boolean flippedY,
+ Token.TokenShape shape,
+ boolean snapToScale,
+ TokenFootprint footprint) {}
+}
diff --git a/src/main/java/net/rptools/maptool/model/Token.java b/src/main/java/net/rptools/maptool/model/Token.java
index 530610473a..c730c6da46 100644
--- a/src/main/java/net/rptools/maptool/model/Token.java
+++ b/src/main/java/net/rptools/maptool/model/Token.java
@@ -46,6 +46,7 @@
import net.rptools.lib.MD5Key;
import net.rptools.lib.image.ImageUtil;
import net.rptools.lib.transferable.TokenTransferData;
+import net.rptools.maptool.client.AppPreferences;
import net.rptools.maptool.client.AppUtil;
import net.rptools.maptool.client.MapTool;
import net.rptools.maptool.client.MapToolVariableResolver;
@@ -94,6 +95,8 @@ public class Token implements Cloneable {
public static final String NUM_ON_BOTH = "Both";
public static final String LIB_TOKEN_PREFIX = "lib:";
+ private static final int OWNER_TYPE_ALL = 1;
+ private static final int OWNER_TYPE_LIST = 0;
private boolean beingImpersonated = false;
private GUID exposedAreaGUID = new GUID();
@@ -187,6 +190,7 @@ public enum Update {
setScaleX,
setScaleY,
setScaleXY,
+ setImageRotation,
setNotes,
setGMNotes,
saveMacro,
@@ -202,7 +206,7 @@ public enum Update {
setVisible,
setVisibleOnlyToOwner,
setIsAlwaysVisible,
- setTokenOpacity,
+ setOpacity,
setTerrainModifier,
setTerrainModifierOperation,
setTerrainModifiersIgnored,
@@ -232,32 +236,30 @@ public enum Update {
private int x;
private int y;
private int z;
+ private Integer facing = null;
+ private int lastX;
+ private int lastY;
+ private Path extends AbstractPoint> lastPath;
+ // Token Layout
private int anchorX;
private int anchorY;
-
private double sizeScale = 1;
+ private double scaleX = 1;
+ private double scaleY = 1;
+ private double imageRotation = 0;
+ private boolean snapToScale = true; // Is size based on footprint
- private int lastX;
- private int lastY;
- private Path extends AbstractPoint> lastPath;
-
- private boolean snapToScale = true; // Whether the scaleX and scaleY represent snap-to-grid
// measurements
-
// These are the original image width and height
private int width;
private int height;
private int isoWidth;
private int isoHeight;
- private double scaleX = 1;
- private double scaleY = 1;
-
private Map sizeMap = new HashMap<>();
- private boolean snapToGrid = true; // Whether the token snaps to the current grid or is free
- // floating
+ private boolean snapToGrid = true; // Is token position constrained to grid increments
private boolean isVisible = true;
private boolean visibleOnlyToOwner = false;
@@ -266,24 +268,17 @@ public enum Update {
// before token is shown over FoW
private boolean isAlwaysVisible = false; // Controls whether a Token is shown over VBL
- // region Topology masks
-
+ // Topology masks
private Area vbl;
private Area hillVbl;
private Area pitVbl;
private Area coverVbl;
private Area mbl;
- // endregion
-
private String name = "";
private Set ownerList = new HashSet<>();
-
private int ownerType;
- private static final int OWNER_TYPE_ALL = 1;
- private static final int OWNER_TYPE_LIST = 0;
-
private String tokenShape = TokenShape.SQUARE.toString();
private String tokenType = Type.NPC.toString();
private String layer = Zone.Layer.getDefaultPlayerLayer().toString();
@@ -292,16 +287,15 @@ public enum Update {
private String propertyType =
MapTool.getCampaign().getCampaignProperties().getDefaultTokenPropertyType();
- private Integer facing = null;
-
private Integer haloColorValue;
- private transient Color haloColor;
+ private transient Color haloColor =
+ new Color(ImageUtil.negativeColourInt(AppPreferences.defaultGridColor.getDefault().getRGB()));
private Integer visionOverlayColorValue;
private transient Color visionOverlayColor;
// Jamz: allow token alpha channel modification
- private @Nonnull Float tokenOpacity = 1.0f;
+ private @Nonnull Float opacity = 1.0f;
private String speechName = "";
@@ -331,9 +325,9 @@ public String toString() {
private Set terrainModifiersIgnored =
new HashSet<>(Collections.singletonList(TerrainModifierOperation.NONE));
- private boolean isFlippedX;
- private boolean isFlippedY;
- private Boolean isFlippedIso = false;
+ private boolean flippedX;
+ private boolean flippedY;
+ private Boolean flippedIso = false;
private MD5Key charsheetImage;
private MD5Key portraitImage;
@@ -427,9 +421,6 @@ public Token(Token token) {
height = token.height;
isoWidth = token.isoWidth;
isoHeight = token.isoHeight;
- scaleX = token.scaleX;
- scaleY = token.scaleY;
- facing = token.facing;
tokenShape = token.tokenShape;
tokenType = token.tokenType;
haloColorValue = token.haloColorValue;
@@ -455,9 +446,16 @@ public Token(Token token) {
gmNotesType = token.gmNotesType;
label = token.label;
- isFlippedX = token.isFlippedX;
- isFlippedY = token.isFlippedY;
- isFlippedIso = token.isFlippedIso;
+ anchorX = token.anchorX;
+ anchorY = token.anchorY;
+ facing = token.facing;
+ flippedX = token.flippedX;
+ flippedY = token.flippedY;
+ flippedIso = token.flippedIso;
+ imageRotation = token.imageRotation;
+ scaleX = token.scaleX;
+ scaleY = token.scaleY;
+ sizeScale = token.sizeScale;
layer = token.layer;
@@ -465,9 +463,6 @@ public Token(Token token) {
charsheetImage = token.charsheetImage;
portraitImage = token.portraitImage;
- anchorX = token.anchorX;
- anchorY = token.anchorY;
- sizeScale = token.sizeScale;
sightType = token.sightType;
hasSight = token.hasSight;
propertyType = token.propertyType;
@@ -508,7 +503,7 @@ public Token(Token token) {
exposedAreaGUID = token.exposedAreaGUID;
heroLabData = token.heroLabData;
- tokenOpacity = token.tokenOpacity;
+ opacity = token.opacity;
terrainModifier = token.terrainModifier;
terrainModifierOperation = token.terrainModifierOperation;
terrainModifiersIgnored.addAll(token.terrainModifiersIgnored);
@@ -701,8 +696,8 @@ public Color getHaloColor() {
/**
* @return The token opacity, in the range [0.0f, 1.0f].
*/
- public float getTokenOpacity() {
- return tokenOpacity;
+ public float getOpacity() {
+ return opacity;
}
/**
@@ -710,7 +705,7 @@ public float getTokenOpacity() {
*
* @param alpha the float of the opacity.
*/
- public void setTokenOpacity(float alpha) {
+ public void setOpacity(float alpha) {
if (alpha > 1.0f) {
alpha = 1.0f;
}
@@ -718,7 +713,7 @@ public void setTokenOpacity(float alpha) {
alpha = 0.0f;
}
- tokenOpacity = alpha;
+ opacity = alpha;
}
/**
@@ -852,47 +847,6 @@ public void setLayer(Zone.Layer layer) {
actualLayer = layer;
}
- public boolean hasFacing() {
- return facing != null;
- }
-
- public void setFacing(int facing) {
- while (facing > 180 || facing < -179) {
- facing += facing > 180 ? -360 : 0;
- facing += facing < -179 ? 360 : 0;
- }
- this.facing = facing;
- }
-
- public void removeFacing() {
- this.facing = null;
- }
-
- /**
- * Facing is in the map space where 0 degrees is along the X axis to the right and proceeding CCW
- * for positive angles.
- *
- * Round/Square tokens that have no facing set, return null. Top Down tokens default to -90.
- *
- * @return null or angle in degrees
- */
- public int getFacing() {
- // -90° is natural alignment. TODO This should really be a per grid setting
- return facing == null ? -90 : facing;
- }
-
- /**
- * This returns the rotation of the facing of the token from the default facing of down or -90.
- *
- * Positive for CW and negative for CCW. The range is currently from -270° (inclusive) to +90°
- * (exclusive), but callers should not rely on this.
- *
- * @return angle in degrees
- */
- public int getFacingInDegrees() {
- return -getFacing() - 90;
- }
-
public boolean getHasSight() {
return hasSight;
}
@@ -1299,22 +1253,6 @@ public Path extends AbstractPoint> getLastPath() {
return lastPath;
}
- public double getScaleX() {
- return scaleX;
- }
-
- public double getScaleY() {
- return scaleY;
- }
-
- public void setScaleX(double scaleX) {
- this.scaleX = scaleX;
- }
-
- public void setScaleY(double scaleY) {
- this.scaleY = scaleY;
- }
-
/**
* Returns whether the token is constrained to a pre-defined grid size.
*
@@ -1525,12 +1463,12 @@ public Area getTransformedMaskTopology(Area areaToTransform) {
atArea.concatenate(AffineTransform.getScaleInstance(scalerX, scalerY));
// Lets account for flipped images...
- if (isFlippedX) {
+ if (flippedX) {
atArea.concatenate(AffineTransform.getScaleInstance(-1.0, 1.0));
atArea.concatenate(AffineTransform.getTranslateInstance(-getWidth(), 0));
}
- if (isFlippedY) {
+ if (flippedY) {
atArea.concatenate(AffineTransform.getScaleInstance(1.0, -1.0));
atArea.concatenate(AffineTransform.getTranslateInstance(0, -getHeight()));
}
@@ -1580,13 +1518,10 @@ public Rectangle getBounds(Zone zone) {
footprintBounds.x = getX();
footprintBounds.y = getY();
} else {
- if (getLayer().anchorSnapToGridAtCenter()) {
+ if (getLayer().isSnapToGridAtCenter()) {
// Center it on the footprint
- footprintBounds.x -= (w - footprintBounds.width) / 2;
- footprintBounds.y -= (h - footprintBounds.height) / 2;
- } else {
- // footprintBounds.x -= zone.getGrid().getSize()/2;
- // footprintBounds.y -= zone.getGrid().getSize()/2;
+ footprintBounds.x -= (int) ((w - footprintBounds.width) / 2d);
+ footprintBounds.y -= (int) ((h - footprintBounds.height) / 2d);
}
}
footprintBounds.width = (int) w; // perhaps make this a double
@@ -1612,7 +1547,7 @@ public ZonePoint getDragAnchor(Zone zone) {
Grid grid = zone.getGrid();
int dragAnchorX, dragAnchorY;
if (isSnapToGrid() && grid.getCapabilities().isSnapToGridSupported()) {
- if (!getLayer().isStampLayer() || !getLayer().anchorSnapToGridAtCenter() || isSnapToScale()) {
+ if (!getLayer().isStampLayer() || !getLayer().isSnapToGridAtCenter() || isSnapToScale()) {
Point2D.Double centerOffset = grid.getCenterOffset();
dragAnchorX = getX() + (int) centerOffset.x;
dragAnchorY = getY() + (int) centerOffset.y;
@@ -1676,7 +1611,7 @@ public ZonePoint getSnappedPoint(Zone zone) {
Point2D.Double offset = getSnapToUnsnapOffset(zone);
double newX = getX() + offset.x;
double newY = getY() + offset.y;
- if (grid.getCapabilities().isSnapToGridSupported() || !getLayer().anchorSnapToGridAtCenter()) {
+ if (grid.getCapabilities().isSnapToGridSupported() || !getLayer().isSnapToGridAtCenter()) {
return grid.convert(
grid.convert(new ZonePoint((int) Math.ceil(newX), (int) Math.ceil(newY))));
} else {
@@ -1709,13 +1644,13 @@ private Point2D.Double getSnapToUnsnapOffset(Zone zone) {
double offsetX, offsetY;
Rectangle tokenBounds = getBounds(zone);
Grid grid = zone.getGrid();
- if (grid.getCapabilities().isSnapToGridSupported() || !getLayer().anchorSnapToGridAtCenter()) {
- if (!getLayer().anchorSnapToGridAtCenter() || isSnapToScale()) {
+ if (grid.getCapabilities().isSnapToGridSupported() || !getLayer().isSnapToGridAtCenter()) {
+ if (!getLayer().isSnapToGridAtCenter() || isSnapToScale()) {
TokenFootprint footprint = getFootprint(grid);
Rectangle footprintBounds = footprint.getBounds(grid);
double footprintOffsetX = 0;
double footprintOffsetY = 0;
- if (getLayer().anchorSnapToGridAtCenter()) {
+ if (getLayer().isSnapToGridAtCenter()) {
// Non-background tokens can have an offset from top left corner
footprintOffsetX = tokenBounds.width - footprintBounds.width;
footprintOffsetY = tokenBounds.height - footprintBounds.height;
@@ -2160,33 +2095,6 @@ public void setNotesType(String type) {
notesType = type;
}
- public boolean isFlippedY() {
- return isFlippedY;
- }
-
- public void setFlippedY(boolean isFlippedY) {
- this.isFlippedY = isFlippedY;
- }
-
- public boolean isFlippedX() {
- return isFlippedX;
- }
-
- public void setFlippedX(boolean isFlippedX) {
- this.isFlippedX = isFlippedX;
- }
-
- public boolean isFlippedIso() {
- if (isFlippedIso != null) {
- return isFlippedIso;
- }
- return false;
- }
-
- public void setFlippedIso(boolean isFlippedIso) {
- this.isFlippedIso = isFlippedIso;
- }
-
public Color getVisionOverlayColor() {
if (visionOverlayColor == null && visionOverlayColorValue != null) {
visionOverlayColor = new Color(visionOverlayColorValue);
@@ -2208,6 +2116,7 @@ public String toString() {
return "Token: " + id;
}
+ // Token Layout - Getters/Setters
public void setAnchor(int x, int y) {
anchorX = x;
anchorY = y;
@@ -2218,27 +2127,127 @@ public Point getAnchor() {
}
public int getAnchorX() {
- return anchorX;
+ return this.anchorX;
+ }
+
+ public void setAnchorX(int xAnchor) {
+ this.anchorX = xAnchor;
}
public int getAnchorY() {
- return anchorY;
+ return this.anchorY;
+ }
+
+ public void setAnchorY(int yAnchor) {
+ this.anchorY = yAnchor;
+ }
+
+ public boolean isFlippedIso() {
+ if (flippedIso != null) {
+ return flippedIso;
+ }
+ return false;
+ }
+
+ public void setFlippedIso(boolean flippedIso) {
+ this.flippedIso = flippedIso;
+ }
+
+ public boolean isFlippedX() {
+ return flippedX;
+ }
+
+ public void setFlippedX(boolean flippedX) {
+ this.flippedX = flippedX;
+ }
+
+ public boolean isFlippedY() {
+ return flippedY;
+ }
+
+ public void setFlippedY(boolean flippedY) {
+ this.flippedY = flippedY;
+ }
+
+ public double getImageRotation() {
+ return this.imageRotation;
+ }
+
+ public void setImageRotation(double angle) {
+ this.imageRotation = angle;
+ }
+
+ public double getScaleX() {
+ return this.scaleX;
+ }
+
+ public void setScaleX(double scaleX) {
+ this.scaleX = scaleX;
+ }
+
+ public double getScaleY() {
+ return this.scaleY;
+ }
+
+ public void setScaleY(double scaleY) {
+ this.scaleY = scaleY;
}
/**
- * @return the scale of the token layout
+ * @return the scale of the image in token layout
*/
public double getSizeScale() {
- return sizeScale;
+ return this.sizeScale;
}
/**
- * Set the scale of the token layout
+ * Set the scale of the image in token layout
*
* @param scale the scale of the token
*/
public void setSizeScale(double scale) {
- sizeScale = scale;
+ this.sizeScale = scale;
+ }
+
+ public boolean hasFacing() {
+ return facing != null;
+ }
+
+ public void setFacing(int direction) {
+ while (direction > 180 || direction < -179) {
+ direction += direction > 180 ? -360 : 0;
+ direction += direction < -179 ? 360 : 0;
+ }
+ this.facing = direction;
+ }
+
+ public void removeFacing() {
+ this.facing = null;
+ }
+
+ /**
+ * Facing is in the map space where 0 degrees is along the X axis to the right and proceeding CCW
+ * for positive angles.
+ *
+ * Round/Square tokens that have no facing set, return null. Top Down tokens default to -90.
+ *
+ * @return null or angle in degrees
+ */
+ public int getFacing() {
+ // -90° is natural alignment. TODO This should really be a per grid setting
+ return facing == null ? -90 : facing;
+ }
+
+ /**
+ * This returns the rotation of the facing of the token from the default facing of down or -90.
+ *
+ * Positive for CW and negative for CCW. The range is currently from -270° (inclusive) to +90°
+ * (exclusive), but callers should not rely on this.
+ *
+ * @return angle in degrees
+ */
+ public int getFacingInDegrees() {
+ return -getFacing() - 90;
}
/**
@@ -2260,8 +2269,8 @@ public TokenTransferData toTransferData() {
td.put(TokenTransferData.ASSET_ID, imageAssetMap.get(null));
td.put(TokenTransferData.Z, z);
td.put(TokenTransferData.SNAP_TO_SCALE, snapToScale);
- td.put(TokenTransferData.WIDTH, scaleX);
- td.put(TokenTransferData.HEIGHT, scaleY);
+ td.put(TokenTransferData.WIDTH, scaleX); // this makes no sense
+ td.put(TokenTransferData.HEIGHT, scaleY); // ditto
td.put(TokenTransferData.SNAP_TO_GRID, snapToGrid);
td.put(TokenTransferData.OWNER_TYPE, ownerType);
td.put(TokenTransferData.VISIBLE_OWNER_ONLY, visibleOnlyToOwner);
@@ -2583,8 +2592,8 @@ protected Object readResolve() {
if (speechMap == null) {
speechMap = new HashMap<>();
}
- if (isFlippedIso == null) {
- isFlippedIso = false;
+ if (flippedIso == null) {
+ flippedIso = false;
}
if (hasImageTable == null) {
hasImageTable = false;
@@ -2623,10 +2632,10 @@ protected Object readResolve() {
}
// Pre 1.13
- if (tokenOpacity == null) {
- tokenOpacity = 1.f;
+ if (opacity == null) {
+ opacity = 1.f;
}
- tokenOpacity = Math.max(0.f, Math.min(tokenOpacity, 1.f));
+ opacity = Math.max(0.f, Math.min(opacity, 1.f));
return this;
}
@@ -2834,8 +2843,8 @@ public void updateProperty(Zone zone, Update update, List
case setIsAlwaysVisible:
setIsAlwaysVisible(parameters.get(0).getBoolValue());
break;
- case setTokenOpacity:
- setTokenOpacity(Float.parseFloat(parameters.get(0).getStringValue()));
+ case setOpacity:
+ setOpacity(Float.parseFloat(parameters.get(0).getStringValue()));
break;
case setTerrainModifier:
setTerrainModifier(parameters.get(0).getDoubleValue());
@@ -2967,17 +2976,23 @@ public static Token fromDto(TokenDto dto) {
token.lastY = dto.getLastY();
token.y = dto.getY();
token.z = dto.getZ();
- token.anchorX = dto.getAnchorX();
- token.anchorY = dto.getAnchorY();
- token.sizeScale = dto.getSizeScale();
token.lastPath = dto.hasLastPath() ? Path.fromDto(dto.getLastPath()) : null;
token.snapToScale = dto.getSnapToScale();
token.width = dto.getWidth();
token.height = dto.getHeight();
token.isoWidth = dto.getIsoWidth();
token.isoHeight = dto.getIsoHeight();
+ // Layout
+ token.anchorX = dto.getAnchorX();
+ token.anchorY = dto.getAnchorY();
token.scaleX = dto.getScaleX();
token.scaleY = dto.getScaleY();
+ token.sizeScale = dto.getSizeScale();
+ token.flippedX = dto.getFlippedX();
+ token.flippedY = dto.getFlippedY();
+ token.flippedIso = dto.getFlippedIso();
+ token.facing = dto.hasFacing() ? dto.getFacing().getValue() : null;
+
dto.getSizeMapMap().forEach((k, v) -> token.sizeMap.put(k, GUID.valueOf(v)));
token.snapToGrid = dto.getSnapToGrid();
token.isVisible = dto.getIsVisible();
@@ -2997,22 +3012,19 @@ public static Token fromDto(TokenDto dto) {
token.tokenType = dto.getTokenType();
token.layer = dto.getLayer();
token.propertyType = dto.getPropertyType();
- token.facing = dto.hasFacing() ? dto.getFacing().getValue() : null;
token.haloColorValue = dto.hasHaloColor() ? dto.getHaloColor().getValue() : null;
token.visionOverlayColorValue =
dto.hasVisionOverlayColor() ? dto.getVisionOverlayColor().getValue() : null;
- token.tokenOpacity = dto.getTokenOpacity();
+ token.opacity = dto.getTokenOpacity();
token.speechName = dto.getSpeechName();
token.terrainModifier = dto.getTerrainModifier();
token.terrainModifierOperation =
- Token.TerrainModifierOperation.valueOf(dto.getTerrainModifierOperation().name());
+ TerrainModifierOperation.valueOf(dto.getTerrainModifierOperation().name());
token.terrainModifiersIgnored.addAll(
dto.getTerrainModifiersIgnoredList().stream()
- .map(m -> Token.TerrainModifierOperation.valueOf(m.name()))
+ .map(m -> TerrainModifierOperation.valueOf(m.name()))
.collect(Collectors.toList()));
- token.isFlippedX = dto.getIsFlippedX();
- token.isFlippedY = dto.getIsFlippedY();
- token.isFlippedIso = dto.getIsFlippedIso();
+
token.charsheetImage =
dto.hasCharsheetImage() ? new MD5Key(dto.getCharsheetImage().getValue()) : null;
token.portraitImage =
@@ -3079,9 +3091,14 @@ public TokenDto toDto() {
dto.setLastY(lastY);
dto.setY(y);
dto.setZ(z);
+ if (facing != null) {
+ dto.setFacing(Int32Value.of(facing));
+ }
dto.setAnchorX(anchorX);
dto.setAnchorY(anchorY);
dto.setSizeScale(sizeScale);
+ dto.setScaleX(scaleX);
+ dto.setScaleY(scaleY);
if (lastPath != null) {
dto.setLastPath(lastPath.toDto());
}
@@ -3090,8 +3107,6 @@ public TokenDto toDto() {
dto.setHeight(height);
dto.setIsoWidth(isoWidth);
dto.setIsoHeight(isoHeight);
- dto.setScaleX(scaleX);
- dto.setScaleY(scaleY);
sizeMap.forEach((k, v) -> dto.putSizeMap(k, v.toString()));
dto.setSnapToGrid(snapToGrid);
dto.setIsVisible(isVisible);
@@ -3121,16 +3136,14 @@ public TokenDto toDto() {
dto.setTokenType(tokenType);
dto.setLayer(layer);
dto.setPropertyType(propertyType);
- if (facing != null) {
- dto.setFacing(Int32Value.of(facing));
- }
+
if (haloColorValue != null) {
dto.setHaloColor(Int32Value.of(haloColorValue));
}
if (visionOverlayColorValue != null) {
dto.setVisionOverlayColor(Int32Value.of(visionOverlayColorValue));
}
- dto.setTokenOpacity(tokenOpacity);
+ dto.setTokenOpacity(opacity);
dto.setSpeechName(speechName);
dto.setTerrainModifier(terrainModifier);
dto.setTerrainModifierOperation(
@@ -3139,9 +3152,9 @@ public TokenDto toDto() {
terrainModifiersIgnored.stream()
.map(m -> TerrainModifierOperationDto.valueOf(m.name()))
.collect(Collectors.toList()));
- dto.setIsFlippedX(isFlippedX);
- dto.setIsFlippedY(isFlippedY);
- dto.setIsFlippedIso(isFlippedIso);
+ dto.setFlippedX(flippedX);
+ dto.setFlippedY(flippedY);
+ dto.setFlippedIso(flippedIso);
if (charsheetImage != null) {
dto.setCharsheetImage(StringValue.of(charsheetImage.toString()));
}
diff --git a/src/main/java/net/rptools/maptool/model/Zone.java b/src/main/java/net/rptools/maptool/model/Zone.java
index 9a19fe2e2b..60dd611381 100644
--- a/src/main/java/net/rptools/maptool/model/Zone.java
+++ b/src/main/java/net/rptools/maptool/model/Zone.java
@@ -252,7 +252,7 @@ public boolean supportsVision() {
* @return {@code true} if the {@code Token} instances on the layer should be resized relative
* to their center rather than their corner.
*/
- public boolean anchorSnapToGridAtCenter() {
+ public boolean isSnapToGridAtCenter() {
return this != BACKGROUND;
}
diff --git a/src/main/java/net/rptools/maptool/util/GraphicsUtil.java b/src/main/java/net/rptools/maptool/util/GraphicsUtil.java
index a2fa0ee24c..a2c7f9aef0 100644
--- a/src/main/java/net/rptools/maptool/util/GraphicsUtil.java
+++ b/src/main/java/net/rptools/maptool/util/GraphicsUtil.java
@@ -27,10 +27,7 @@
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
-import java.awt.geom.Area;
-import java.awt.geom.GeneralPath;
-import java.awt.geom.Path2D;
-import java.awt.geom.Point2D;
+import java.awt.geom.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -44,6 +41,7 @@
import net.rptools.maptool.client.ui.theme.Images;
import net.rptools.maptool.client.ui.theme.RessourceManager;
import net.rptools.maptool.client.ui.zone.renderer.ZoneRenderer;
+import net.rptools.maptool.model.GridFactory;
/** */
public class GraphicsUtil {
@@ -359,14 +357,9 @@ public static void renderSoftClipping(Graphics2D g, Shape shape, int width, doub
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_OFF); // Faster without antialiasing, and looks just as good
- // float alpha = (float)initialAlpha / width / 6;
float alpha = .04f;
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
for (int i = 1; i < width; i += 2) {
- // if (alpha * i < .2) {
- // // Too faded to see anyway, don't waste cycles on it
- // continue;
- // }
g2.setStroke(new BasicStroke(i));
g2.draw(shape);
}
@@ -377,8 +370,8 @@ public static Area createLine(int width, Point2D... points) {
if (points.length < 2) {
throw new IllegalArgumentException("Must supply at least two points");
}
- List bottomList = new ArrayList(points.length);
- List topList = new ArrayList(points.length);
+ List bottomList = new ArrayList<>(points.length);
+ List topList = new ArrayList<>(points.length);
for (int i = 0; i < points.length; i++) {
double angle =
@@ -397,17 +390,13 @@ public static Area createLine(int width, Point2D... points) {
double bottomAngle = (angle + delta / 2) % 360;
double topAngle = bottomAngle + 180;
- // System.out.println(angle + " - " + delta + " - " + bottomAngle + " - " + topAngle);
-
bottomList.add(getPointAtVector(points[i], bottomAngle, width));
topList.add(getPointAtVector(points[i], topAngle, width));
}
- // System.out.println(bottomList);
- // System.out.println(topList);
Collections.reverse(topList);
GeneralPath path = new GeneralPath();
- Point2D initialPoint = bottomList.remove(0);
+ Point2D initialPoint = bottomList.removeFirst();
path.moveTo((float) initialPoint.getX(), (float) initialPoint.getY());
for (Point2D point : bottomList) {
@@ -423,10 +412,6 @@ public static Area createLine(int width, Point2D... points) {
private static Point2D getPointAtVector(Point2D point, double angle, double length) {
double x = point.getX() + length * Math.cos(Math.toRadians(angle));
double y = point.getY() - length * Math.sin(Math.toRadians(angle));
-
- // System.out.println(point + " - " + angle + " - " + x + "x" + y + " - " +
- // Math.cos(Math.toRadians(angle)) + " - " + Math.sin(Math.toRadians(angle)) + " - " +
- // Math.toRadians(angle));
return new Point2D.Double(x, y);
}
@@ -435,8 +420,6 @@ public static void main(String[] args) {
new Point2D[] {
new Point(20, 20), new Point(50, 50), new Point(80, 20), new Point(100, 100)
};
- // final Point2D[] points = new Point2D[]{new Point(50, 50), new Point(20, 20), new Point(20,
- // 100), new Point(50,75)};
final Area line = createLine(10, points);
JFrame f = new JFrame();
@@ -464,4 +447,60 @@ protected void paintComponent(Graphics g) {
f.setVisible(true);
// System.out.println(area.equals(area2));
}
+
+ public static Shape createGridShape(String gridType, double size) {
+ final Shape gridShape;
+ int sides = 0;
+ double startAngle = 0;
+ double increment;
+ double skew = 0;
+ double hScale = 1;
+ double vScale = 1;
+ final double root2 = Math.sqrt(2d);
+ final double root3 = Math.sqrt(3d);
+ switch (gridType) {
+ case GridFactory.HEX_HORI -> {
+ sides = 6;
+ startAngle = Math.TAU / 12;
+ hScale = vScale = root3 / 3d;
+ }
+ case GridFactory.HEX_VERT -> {
+ sides = 6;
+ hScale = vScale = root3 / 3d;
+ }
+ case GridFactory.ISOMETRIC -> {
+ sides = 4;
+ vScale = 0.5;
+ }
+ case GridFactory.ISOMETRIC_HEX -> {
+ sides = 6;
+ startAngle = Math.TAU / 24;
+ hScale = vScale = root3 / 3d;
+ skew = Math.toRadians(30d);
+ }
+ case GridFactory.NONE -> {
+ return new Ellipse2D.Double(-size / 2d, -size / 2d, size, size);
+ }
+ case GridFactory.SQUARE -> {
+ sides = 4;
+ hScale = vScale = root2 / 2d;
+ startAngle = Math.TAU / 8d;
+ }
+ }
+ increment = Math.TAU / sides;
+ Path2D path = new Path2D.Double();
+ path.moveTo(Math.cos(startAngle) * size * hScale, Math.sin(startAngle) * size * vScale);
+ for (int i = 1; i < sides; i++) {
+ path.lineTo(
+ Math.cos(startAngle + i * increment) * size * hScale,
+ Math.sin(startAngle + i * increment) * size * vScale);
+ }
+ path.closePath();
+ if (skew != 0) {
+ gridShape = AffineTransform.getShearInstance(skew, 0).createTransformedShape(path);
+ } else {
+ gridShape = path;
+ }
+ return gridShape;
+ }
}
diff --git a/src/main/proto/data_transfer_objects.proto b/src/main/proto/data_transfer_objects.proto
index ab9281e245..35b602458f 100644
--- a/src/main/proto/data_transfer_objects.proto
+++ b/src/main/proto/data_transfer_objects.proto
@@ -305,6 +305,12 @@ message TokenDto {
int32 anchor_x = 9;
int32 anchor_y = 10;
double size_scale = 11;
+ double scale_x = 20;
+ double scale_y = 21;
+ double image_rotation = 73;
+ bool flipped_x = 45;
+ bool flipped_y = 46;
+ bool flipped_iso = 47;
int32 last_x = 12;
int32 last_y = 13;
PathDto last_path = 14;
@@ -313,8 +319,6 @@ message TokenDto {
int32 height = 17;
int32 iso_width = 18;
int32 iso_height = 19;
- double scale_x = 20;
- double scale_y = 21;
map size_map = 22;
bool snap_to_grid = 23;
bool is_visible = 24;
@@ -342,9 +346,6 @@ message TokenDto {
double terrain_modifier = 42;
TerrainModifierOperationDto terrain_modifier_operation = 43;
repeated TerrainModifierOperationDto terrain_modifiers_ignored = 44;
- bool is_flipped_x = 45;
- bool is_flipped_y = 46;
- bool is_flipped_iso = 47;
google.protobuf.StringValue charsheet_image = 48;
google.protobuf.StringValue portrait_image = 49;
repeated LightSourceDto unique_light_sources = 72;
@@ -639,7 +640,7 @@ enum TokenUpdateDto {
setVisible = 34;
setVisibleOnlyToOwner = 35;
setIsAlwaysVisible = 36;
- setTokenOpacity = 37;
+ setOpacity = 37;
setTerrainModifier = 38;
setTerrainModifierOperation = 39;
setTerrainModifiersIgnored = 40;
diff --git a/src/main/resources/net/rptools/maptool/client/image/scale.svg b/src/main/resources/net/rptools/maptool/client/image/scale.svg
new file mode 100644
index 0000000000..32c8b5fa42
--- /dev/null
+++ b/src/main/resources/net/rptools/maptool/client/image/scale.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/main/resources/net/rptools/maptool/client/image/scaleHor.svg b/src/main/resources/net/rptools/maptool/client/image/scaleHor.svg
new file mode 100644
index 0000000000..c722aeb276
--- /dev/null
+++ b/src/main/resources/net/rptools/maptool/client/image/scaleHor.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/main/resources/net/rptools/maptool/client/image/scaleVert.svg b/src/main/resources/net/rptools/maptool/client/image/scaleVert.svg
new file mode 100644
index 0000000000..ff31922842
--- /dev/null
+++ b/src/main/resources/net/rptools/maptool/client/image/scaleVert.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/main/resources/net/rptools/maptool/language/i18n.properties b/src/main/resources/net/rptools/maptool/language/i18n.properties
index 9d25a11103..0c82b297d2 100644
--- a/src/main/resources/net/rptools/maptool/language/i18n.properties
+++ b/src/main/resources/net/rptools/maptool/language/i18n.properties
@@ -128,6 +128,7 @@ Button.install = Install
Button.runmacro = RUN MACRO
Button.hide = Hide
+Label.footprint = Footprint
Label.lights = Vision:
Label.name = Name:
Label.gmname = GM Name:
@@ -202,10 +203,29 @@ Label.board = Board
Label.visibility = Visibility
Label.pathfilename = Path/Filename:
Label.allowURIAccess = Allow URI Access
+Label.rotation = Rotation
+Label.snapToGrid = Snap to Grid
+Label.zoom = Zoom
+
+Mouse.leftClick = Left-click
+Mouse.leftDoubleClick = Double-left-click
+Mouse.rightClick = Right-click
+Mouse.rightDoubleClick = Double-right-click
+Mouse.shiftLeftClick = Shift-left-click
+Mouse.shiftRightClick = Shift-right-click
+Mouse.ctrlLeftClick = Ctrl-left-click
+Mouse.ctrlRightClick = Ctrl-right-click
+Mouse.altLeftClick = Alt-left-click
+Mouse.altRightClick = Alt-right-click
+Mouse.leftDrag = Left-drag
+Mouse.rightDrag = Right-drag
+Mouse.wheel = Mouse-wheel
+Mouse.shiftWheel = Shift-mouse-wheel
+Mouse.ctrlWheel = Ctrl-mouse-wheel
+Mouse.altWheel = Alt-mouse-wheel
CampaignPropertyDialog.error.parenthesis = Missing right parenthesis ")" in the following property: {0}
-ColorPicker.tooltip.gridSnap = Snap to Grid
ColorPicker.tooltip.lineType = Line Type
ColorPicker.tooltip.eraser = Eraser
ColorPicker.tooltip.penWidth = Pen Width
@@ -273,7 +293,7 @@ EditTokenDialog.label.gmnotes = GM Notes
EditTokenDialog.menu.notes.sendChat = Send to Chat
EditTokenDialog.menu.notes.sendEmit = Send as Emit
EditTokenDialog.label.shape = Shape:
-EditTokenDialog.label.snaptogrid = Snap To Grid:
+
EditTokenDialog.label.size = Size:
EditTokenDialog.label.visible = Visible to players:
EditTokenDialog.label.properties = Properties:
@@ -285,7 +305,7 @@ EditTokenDialog.label.image = Image Table:
EditTokenDialog.label.uniqueLightSources = Unique Light Sources:
EditTokenDialog.label.opacity = Token Opacity:
EditTokenDialog.label.opacity.tooltip = Change the opacity of the token.
-EditTokenDialog.label.opacity.100 = 100%
+EditTokenDialog.label.opacity.100 = 100%
EditTokenDialog.label.terrain.ignore = Ignore Terrain:
EditTokenDialog.label.terrain.ignore.tooltip= Select any terrain modifier types this token should ignore.
EditTokenDialog.label.statSheet = Stat Sheet
@@ -295,7 +315,7 @@ EditTokenDialog.border.title.layout = Layout
EditTokenDialog.border.title.portrait = Portrait
EditTokenDialog.border.title.handout = Handout
EditTokenDialog.border.title.charsheet = Charsheet
-EditTokenDialog.status.layout.instructions = Mouse Wheel to zoom; double-LClick to reset position and zoom
+EditTokenDialog.status.layout.instructions = Mouse Wheel to scale; double-LClick to reset position and zoom
EditTokenDialog.label.allplayers = All Players
EditTokenDialog.tab.properties = Properties
EditTokenDialog.tab.vbl = Topology
@@ -371,6 +391,17 @@ EditTokenDialog.button.hero.refresh.tooltip.off = Refresh data from Hero L
EditTokenDialog.libTokenURI.error.notLibToken = {0} is not a valid name for a lib:Token.
EditTokenDialog.libTokenURI.error.reserved = lib:Tokens can not be named {0} if you want to enable URI access.
+EditTokenDialog.layout.help.caption = Mouse Controls
+EditTokenDialog.layout.help.moveView = Move the viewport.
+EditTokenDialog.layout.help.zoomView = Magnify the viewport.
+EditTokenDialog.layout.help.rotateImage = Adjust the rotation of the token image.
+EditTokenDialog.layout.help.scaleImage = Change the size of the token image.
+EditTokenDialog.layout.help.moveImage = Change the position of the token image.
+EditTokenDialog.layout.help.reset = Revert to starting values.
+EditTokenDialog.layout.help.resetDefaults = Revert to default values.
+EditTokenDialog.layout.scaleButton.tooltip = Switch between horizontal, vertical, or both.
+
+
MapPropertiesDialog.label.playerMapAlias = Display Name:
MapPropertiesDialog.label.playerMapAlias.tooltip = This is how players will see the map identified. Must be unique within campaign.
MapPropertiesDialog.label.Name.tooltip = This is how the map is referred to in macros and is only visible to the GM.
@@ -814,7 +845,6 @@ CampaignPropertiesDialog.button.importPredefined = Import Predefined
CampaignPropertiesDialog.button.importPredefined.tooltip = Import predefined properties and settings for the selected system
CampaignPropertiesDialog.button.import.tooltip = Import predefined properties from a file
CampaignPropertiesDialog.button.export.tooltip = Export campaign properties to file
-CampaignPropertiesDialog.export.message = Campaign properties will be exported in JSON format and cannot be imported.
CampaignPropertiesDialog.combo.importPredefined.tooltip = System to import properties for
CampaignPropertiesDialog.button.import = Import Predefined
campaignPropertiesDialog.newTokenTypeName = Enter the name for the new token type
@@ -2017,8 +2047,6 @@ macro.function.general.unknownProperty = Error executing "{0}": the
macro.function.general.unknownTokenOnMap = Error executing "{0}": the token name or id "{1}" is unknown on map "{2}".
macro.function.general.wrongNumParam = Function "{0}" requires exactly {1} parameters; {2} were provided.
macro.function.general.listCannotBeEmpty = {0}: string list at argument {1} cannot be empty
-macro.function.general.wrongParamType = A parameter is the wrong type.
-macro.function.general.paramCannotBeEmpty = A parameter is empty.
# Token Distance functions
# I.e. ONE_TWO_ONE or ONE_ONE_ONE
macro.function.getDistance.invalidMetric = Invalid metric type "{0}".
@@ -2663,7 +2691,6 @@ token.popup.menu.lights.clearAll = Clear All
token.popup.menu.auras.clear = Clear Auras Only
token.popup.menu.auras.clearGM = Clear GM Auras Only
token.popup.menu.auras.clearOwner = Clear Owner Auras Only
-token.popup.menu.snap = Snap to grid
token.popup.menu.visible = Visible to players
token.popup.menu.impersonate = Impersonate
token.popup.menu.move = Move
|
---|