]> gerrit.simantics Code Review - simantics/platform.git/commitdiff
HiDPI scaling for diagram ruler and model activity tracker 72/2672/1
authorTuukka Lehtonen <tuukka.lehtonen@semantum.fi>
Mon, 1 Oct 2018 17:20:30 +0000 (20:20 +0300)
committerMarko Luukkainen <marko.luukkainen@semantum.fi>
Fri, 22 Feb 2019 11:29:09 +0000 (11:29 +0000)
gitlab #137

Change-Id: Icc3cf5cd828aa4580699dde01635b1ea669b0a0c
(cherry picked from commit bc195f999a0a3f4571ff45342327223b4b480750)

bundles/org.simantics.diagram/src/org/simantics/diagram/participant/DiagramModelActivityTracker.java
bundles/org.simantics.g2d/src/org/simantics/g2d/participant/RulerPainter.java
bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/g2d/nodes/RulerNode.java
bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/utils/DPIUtil.java [new file with mode: 0644]

index 630032baeda2def1bd4b1e0b4c39a4c53907fb04..e2787743886058aaf4a5ab0b5e4fde0984ca2c99 100644 (file)
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2007, 2010 Association for Decentralized Information Management
+ * Copyright (c) 2007, 2018 Association for Decentralized Information Management
  * in Industry THTH ry.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
@@ -35,10 +35,12 @@ import org.simantics.g2d.canvas.impl.AbstractCanvasParticipant;
 import org.simantics.g2d.canvas.impl.HintReflection.HintListener;
 import org.simantics.g2d.canvas.impl.SGNodeReflection.SGCleanup;
 import org.simantics.g2d.canvas.impl.SGNodeReflection.SGInit;
+import org.simantics.g2d.participant.RulerPainter;
 import org.simantics.g2d.utils.Alignment;
 import org.simantics.layer0.Layer0;
 import org.simantics.modeling.ModelingResources;
 import org.simantics.scenegraph.g2d.G2DParentNode;
+import org.simantics.scenegraph.utils.DPIUtil;
 import org.simantics.structural.stubs.StructuralResource2;
 import org.simantics.utils.datastructures.hints.IHintContext;
 import org.simantics.utils.datastructures.hints.IHintContext.Key;
@@ -67,6 +69,9 @@ public class DiagramModelActivityTracker extends AbstractCanvasParticipant {
     Resource                   input;
     IsInActiveModelListener    listener;
     TextNode                   bannerNode;
+    Rectangle2D                bounds;
+    boolean                    rulerVisible = false;
+    double                     rulerSize = 0;
 
     public DiagramModelActivityTracker(Resource input) {
         if (input == null)
@@ -76,18 +81,34 @@ public class DiagramModelActivityTracker extends AbstractCanvasParticipant {
 
     @HintListener(Class=Hints.class, Field="KEY_CONTROL_BOUNDS")
     public void controlBoundsChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) {
+        bounds = (Rectangle2D) newValue;
+        resetBannerNode(bounds, rulerVisible, rulerSize);
+    }
+
+    @HintListener(Class=RulerPainter.class, Field="KEY_RULER_ENABLED")
+    public void rulerToggled(IHintObservable sender, Key key, Object oldValue, Object newValue) {
+        rulerVisible = Boolean.TRUE.equals(newValue);
+        resetBannerNode(bounds, rulerVisible, rulerSize);
+    }
+
+    @HintListener(Class=RulerPainter.class, Field="KEY_RULER_SIZE")
+    public void rulerSizeChanged(IHintObservable sender, Key key, Object oldValue, Object newValue) {
+        rulerSize = newValue != null ? (Double) newValue : 0.0;
+        resetBannerNode(bounds, rulerVisible, rulerSize);
+    }
+
+    private void resetBannerNode(Rectangle2D bounds, boolean rulerVisible, double rulerSize) {
         if (bannerNode == null)
             return;
-        if (newValue != null) {
-            Rectangle2D bounds = (Rectangle2D) newValue;
+        if (bounds != null) {
             AffineTransform at = new AffineTransform();
             at.translate(5,5);
-//            at.translate(bounds.getWidth() / 2, bounds.getHeight() / 2);
-//            at.scale(2, 2);
-//            at.rotate(-Math.PI/6);
-//            at.translate(bounds.getWidth() - 150, bounds.getHeight() - 30);
-//            at.scale(1, 1);
+            if (rulerVisible) {
+                double s = DPIUtil.upscale(rulerSize);
+                at.translate(s, s);
+            }
             bannerNode.setTransform(at);
+            bannerNode.setColor(Color.BLACK);
         } else {
             // Disable rendering
             bannerNode.setColor(null);
@@ -112,6 +133,10 @@ public class DiagramModelActivityTracker extends AbstractCanvasParticipant {
     public void addedToContext(ICanvasContext ctx) {
         super.addedToContext(ctx);
 
+        rulerVisible = Boolean.TRUE.equals(getHint(RulerPainter.KEY_RULER_ENABLED));
+        Double rs = getHint(RulerPainter.KEY_RULER_SIZE);
+        rulerSize = rs != null ? rs : 0;
+
         listener = new IsInActiveModelListener(ctx);
         Simantics.getSession().async(new IsActiveDiagram(input), listener);
 
@@ -139,12 +164,14 @@ public class DiagramModelActivityTracker extends AbstractCanvasParticipant {
         bannerNode.setVerticalAlignment((byte) Alignment.LEADING.ordinal());
         //bannerNode.setText("Model is not active.");
         bannerNode.setText("");
-        bannerNode.setFont(Font.decode("Arial 16"));
+        int fontSize = DPIUtil.upscale(16);
+        bannerNode.setFont(new Font("Tahoma", Font.PLAIN, fontSize));
         //bannerNode.setBackgroundColor(new Color(192, 192, 192, 128));
         bannerNode.setPadding(10, 10);
         bannerNode.setBorderColor(Color.GRAY);
         bannerNode.setBorderWidth(0);
         bannerNode.setEditable(false);
+        bannerNode.setColor(Color.BLACK);
     }
 
     @SGCleanup
index bffe0c9df4862a160f95ac0e61610dffb1d2a1e1..dc9f5f5a73ec0a0f97383c5499ce6f2622e5a688 100644 (file)
@@ -56,6 +56,11 @@ public class RulerPainter extends AbstractCanvasParticipant {
 
     public static final Key KEY_RULER_ENABLED = new KeyOf(Boolean.class, "RULER_ENABLED");
 
+    /**
+     * Size of ruler in normalized (non-zoomed or 100% zoom) coordinates, not screen pixel coordinates.
+     */
+    public static final Key KEY_RULER_SIZE = new KeyOf(Double.class, "RULER_SIZE");
+
     /** Background color */
     public static final Key KEY_RULER_BACKGROUND_COLOR = new KeyOf(Color.class, "RULER_BACKGROUND_COLOR");
 
@@ -78,7 +83,12 @@ public class RulerPainter extends AbstractCanvasParticipant {
     @Override
     public void addedToContext(ICanvasContext ctx) {
         super.addedToContext(ctx);
+
+        if (!hasHint(KEY_RULER_SIZE))
+            setHint(KEY_RULER_SIZE, RULER_WIDTH);
+
         getHintStack().addKeyHintListener(getThread(), KEY_RULER_ENABLED, hintListener);
+        getHintStack().addKeyHintListener(getThread(), KEY_RULER_SIZE, hintListener);
         getHintStack().addKeyHintListener(getThread(), KEY_RULER_BACKGROUND_COLOR, hintListener);
         getHintStack().addKeyHintListener(getThread(), KEY_RULER_TEXT_COLOR, hintListener);
         getHintStack().addKeyHintListener(getThread(), GridPainter.KEY_GRID_SIZE, hintListener);
@@ -88,6 +98,7 @@ public class RulerPainter extends AbstractCanvasParticipant {
     @Override
     public void removedFromContext(ICanvasContext ctx) {
         getHintStack().removeKeyHintListener(getThread(), KEY_RULER_ENABLED, hintListener);
+        getHintStack().removeKeyHintListener(getThread(), KEY_RULER_SIZE, hintListener);
         getHintStack().removeKeyHintListener(getThread(), KEY_RULER_BACKGROUND_COLOR, hintListener);
         getHintStack().removeKeyHintListener(getThread(), KEY_RULER_TEXT_COLOR, hintListener);
         getHintStack().removeKeyHintListener(getThread(), GridPainter.KEY_GRID_SIZE, hintListener);
@@ -119,6 +130,7 @@ public class RulerPainter extends AbstractCanvasParticipant {
     protected void updateNode() {
         node.setEnabled(isPaintingEnabled());
         node.setGridSize(getGridSize());
+        node.setRulerSize(getRulerSize());
     }
 
     private double getGridSize() {
@@ -139,6 +151,11 @@ public class RulerPainter extends AbstractCanvasParticipant {
         return !Boolean.FALSE.equals(b);
     }
 
+    private double getRulerSize() {
+        Double d = getHint(KEY_RULER_SIZE);
+        return d != null ? d : RULER_WIDTH;
+    }
+
     public Color getRulerTextColor()
     {
         Color c = getHint(KEY_RULER_TEXT_COLOR);
index 7e962976e52f1b67ec5f489af713c25357e90a2b..785c246b4e20679396f5cf9b5282e100d512455f 100644 (file)
@@ -22,6 +22,7 @@ import java.awt.geom.Rectangle2D;
 import java.util.Locale;
 
 import org.simantics.scenegraph.g2d.G2DNode;
+import org.simantics.scenegraph.utils.DPIUtil;
 import org.simantics.scenegraph.utils.GridUtils;
 
 public class RulerNode extends G2DNode {
@@ -30,17 +31,14 @@ public class RulerNode extends G2DNode {
      */
     private static final long  serialVersionUID  = 2490944880914577411L;
 
-    /**
-     * FIXME: this is a hack for the map UI that has to be solved some other way.
-     */
-    private static final boolean MAP_Y_SCALING = false;
-
     private static final Color GRAY              = new Color(100, 100, 100);
 
     protected Boolean          enabled           = true;
 
     protected double           gridSize          = 1.0;
 
+    protected double           rulerSize         = 20;
+
     @SyncField("enabled")
     public void setEnabled(Boolean enabled) {
         this.enabled = enabled;
@@ -53,11 +51,16 @@ public class RulerNode extends G2DNode {
         this.gridSize = gridSize;
     }
 
+    @SyncField("rulerSize")
+    public void setRulerSize(double rulerSize) {
+        this.rulerSize = rulerSize;
+    }
+
     @Override
     public void render(Graphics2D g) {
         if (!enabled)
             return;
-        
+
         AffineTransform tr = g.getTransform();
         double scaleX = Math.abs(tr.getScaleX());
         double scaleY = Math.abs(tr.getScaleY());
@@ -69,7 +72,7 @@ public class RulerNode extends G2DNode {
         double offsetY = tr.getTranslateY();
         g.setTransform(new AffineTransform());
 
-        Font rulerFont = new Font("Tahoma", Font.PLAIN, 9);
+        Font rulerFont = new Font("Tahoma", Font.PLAIN, DPIUtil.upscale(9));
 
         //g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
         g.setStroke(new BasicStroke(1));
@@ -80,10 +83,15 @@ public class RulerNode extends G2DNode {
 
         g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.8f));
 
-        Rectangle2D vertical = new Rectangle2D.Double(bounds.getMinX(), bounds.getMinY(), bounds.getMaxX(), bounds.getMinY()+20);
+        int rulerPixelSize = (int) DPIUtil.upscale(rulerSize);
+        int u7 = DPIUtil.upscale(7);
+        int u4 = DPIUtil.upscale(4);
+        int u2 = DPIUtil.upscale(2);
+
+        Rectangle2D vertical = new Rectangle2D.Double(bounds.getMinX(), bounds.getMinY(), bounds.getMaxX(), bounds.getMinY()+rulerPixelSize);
         g.fill(vertical);
 
-        Rectangle2D horizontal = new Rectangle2D.Double(bounds.getMinX(), bounds.getMinY()+20, bounds.getMinX()+20, bounds.getMaxY());
+        Rectangle2D horizontal = new Rectangle2D.Double(bounds.getMinX(), bounds.getMinY()+rulerPixelSize, bounds.getMinX()+rulerPixelSize, bounds.getMaxY());
         g.fill(horizontal);
 
         // stepX and stepY should be something between 50 and 100
@@ -103,16 +111,16 @@ public class RulerNode extends G2DNode {
 
         g.setColor(GRAY);
         g.setFont(rulerFont);
+        FontMetrics fm = g.getFontMetrics();
 
         double previousText = -100;
 
         // Vertical ruler
         for(double x = offsetX%stepX-stepX; x < bounds.getMaxX(); x+=stepX) {
-            if(x > 20) {
+            if(x > rulerPixelSize) {
                 double val = (x-offsetX)/scaleX / getTransform().getScaleX();
                 double modifiedValue = modifyHorizontalValue(val);
                 String str = formatValue(modifiedValue, getMaxDigits());
-                FontMetrics fm = g.getFontMetrics();
                 Rectangle2D r = fm.getStringBounds(str, g);
                 if((x-r.getWidth()/2) > previousText) {
                     g.setColor(Color.BLACK);
@@ -121,31 +129,29 @@ public class RulerNode extends G2DNode {
                 }
 
                 g.setColor(GRAY);
-                g.drawLine((int)x, (int)bounds.getMinY()+12, (int)x, (int)bounds.getMinY()+19);
+                g.drawLine((int)x, (int)bounds.getMinY()+rulerPixelSize-1-u7, (int)x, (int)bounds.getMinY()+rulerPixelSize-1);
             }
             if(stepX/5 > 2) {
                 for(double x2 = x+stepX/5; x2 < x+stepX; x2+=stepX/5) {
-                    if(x2 > 20) {
-                        g.drawLine((int)x2, (int)bounds.getMinY()+15, (int)x2, (int)bounds.getMinY()+19);
+                    if(x2 > rulerPixelSize) {
+                        g.drawLine((int)x2, (int)bounds.getMinY()+rulerPixelSize-1-u4, (int)x2, (int)bounds.getMinY()+rulerPixelSize-1);
                     }
                 }
                 for(double x2 = x+stepX/10; x2 < x+stepX; x2+=stepX/5) {
-                    if(x2 > 20) {
-                        g.drawLine((int)x2, (int)bounds.getMinY()+17, (int)x2, (int)bounds.getMinY()+19);
+                    if(x2 > rulerPixelSize) {
+                        g.drawLine((int)x2, (int)bounds.getMinY()+rulerPixelSize-1-u2, (int)x2, (int)bounds.getMinY()+rulerPixelSize-1);
                     }
                 }
-
             }
         }
 
         // Horizontal ruler
         previousText = -100;
         for(double y = offsetY%stepY-stepY; y < bounds.getMaxY(); y+=stepY) {
-            if(y > 20) {
+            if(y > rulerPixelSize) {
                 double val = (y-offsetY)/scaleY / getTransform().getScaleY();
                 double modifiedValue = modifyVerticalValue(val);
                 String str = formatValue(modifiedValue, getMaxDigits());
-                FontMetrics fm = g.getFontMetrics();
                 Rectangle2D r = fm.getStringBounds(str, g);
                 if(y-1+r.getHeight()/2 > previousText) {
                     g.setColor(Color.BLACK);
@@ -157,17 +163,17 @@ public class RulerNode extends G2DNode {
                     previousText = y-1+r.getHeight();
                 }
                 g.setColor(GRAY);
-                g.drawLine((int)bounds.getMinX()+12, (int)y, (int)bounds.getMinX()+19, (int)y);
+                g.drawLine((int)bounds.getMinX()+rulerPixelSize-1-u7, (int)y, (int)bounds.getMinX()+rulerPixelSize-1, (int)y);
             }
             if(stepY/5 > 2) {
                 for(double y2 = y+stepY/5; y2 < y+stepY; y2+=stepY/5) {
-                    if(y2 > 20) {
-                        g.drawLine((int)bounds.getMinX()+15, (int)y2, (int)bounds.getMinX()+19, (int)y2);
+                    if(y2 > rulerPixelSize) {
+                        g.drawLine((int)bounds.getMinX()+rulerPixelSize-1-u4, (int)y2, (int)bounds.getMinX()+rulerPixelSize-1, (int)y2);
                     }
                 }
                 for(double y2 = y+stepY/10; y2 < y+stepY; y2+=stepY/5) {
-                    if(y2 > 20) {
-                        g.drawLine((int)bounds.getMinX()+17, (int)y2, (int)bounds.getMinX()+19, (int)y2);
+                    if(y2 > rulerPixelSize) {
+                        g.drawLine((int)bounds.getMinX()+rulerPixelSize-1-u2, (int)y2, (int)bounds.getMinX()+rulerPixelSize-1, (int)y2);
                     }
                 }
             }
diff --git a/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/utils/DPIUtil.java b/bundles/org.simantics.scenegraph/src/org/simantics/scenegraph/utils/DPIUtil.java
new file mode 100644 (file)
index 0000000..0a138b3
--- /dev/null
@@ -0,0 +1,254 @@
+/*******************************************************************************
+ * Copyright (c) 2018 Association for Decentralized Information Management
+ * in Industry THTH ry.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ *
+ * Contributors:
+ *     Semantum Oy - initial API and implementation
+ *******************************************************************************/
+package org.simantics.scenegraph.utils;
+
+import java.awt.Point;
+import java.awt.Rectangle;
+import java.awt.Toolkit;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+
+/**
+ * @author Tuukka Lehtonen
+ * @since 1.36.0
+ */
+public class DPIUtil {
+
+       private static boolean initialized = false;
+       private static boolean hasZoom;
+
+       private static float upscaleFactorF;
+       private static double upscaleFactorD;
+       private static float downscaleFactorF;
+       private static double downscaleFactorD;
+
+       private static void initialize() {
+               if (initialized)
+                       return;
+
+               double dpi = Toolkit.getDefaultToolkit().getScreenResolution();
+               double baseDpi = 96;
+               int zoom = (int) Math.round(100.0 * dpi / baseDpi);
+               hasZoom = zoom != 100;
+
+               upscaleFactorD   = dpi / baseDpi;
+               upscaleFactorF   = (float) upscaleFactorD;
+               downscaleFactorD = baseDpi / dpi;
+               downscaleFactorF   = (float) downscaleFactorD;
+
+//             System.out.format("DPIUtil:%n\tswt zoom = %d%n\tdownscale factor = %f%n\tupscale factor = %f%n",
+//                             zoom,
+//                             downscaleFactorD,
+//                             upscaleFactorD
+//                             );
+
+               initialized = true;
+       }
+
+       // Internals
+
+       private static Rectangle scale(float s, Rectangle r, Rectangle target) {
+               if (s == 1.0f) {
+                       if (r == target)
+                               return r;
+                       if (target == null) {
+                               return new Rectangle(r.x, r.y, r.width, r.height);
+                       } else {
+                               target.x = r.x;
+                               target.y = r.y;
+                               target.width = r.width;
+                               target.height = r.height;
+                               return target;
+                       }
+               }
+               if (target == null) {
+                       return new Rectangle(
+                                       Math.round(r.x*s),
+                                       Math.round(r.y*s),
+                                       Math.round(r.width*s),
+                                       Math.round(r.height*s));
+               } else {
+                       target.x = Math.round(r.x*s);
+                       target.y = Math.round(r.y*s);
+                       target.width = Math.round(r.width*s);
+                       target.height = Math.round(r.height*s);
+                       return target;
+               }
+       }
+
+       private static Rectangle2D scale(double s, Rectangle2D r, Rectangle2D target) {
+               if (s == 1.0) {
+                       if (r == target)
+                               return r;
+                       if (target == null)
+                               return (Rectangle2D) r.clone();
+                       target.setFrame(r);
+                       return target;
+               }
+               if (target == null)
+                       target = (Rectangle2D) r.clone();
+               target.setFrame(r.getX()*s, r.getY()*s, r.getWidth()*s, r.getHeight()*s);
+               return target;
+       }
+
+       private static double downscale0(double x) {
+               return hasZoom ? x * downscaleFactorD : x;
+       }
+
+       private static int downscaleToInteger0(double x) {
+               return (int)(hasZoom ? Math.round((double) x * downscaleFactorD) : x);
+       }
+
+       private static int downscale0(int x) {
+               return hasZoom ? (int) Math.round((double) x * downscaleFactorD) : x;
+       }
+
+       private static double upscale0(double x) {
+               return hasZoom ? x * upscaleFactorD : x;
+       }
+
+       private static int upscaleToInteger0(double x) {
+               return (int)(hasZoom ? Math.round((double) x * upscaleFactorD) : x);
+       }
+
+       private static int upscale0(int x) {
+               return hasZoom ? (int) Math.round((double) x * upscaleFactorD) : x;
+       }
+
+       // Downscaling
+
+       public static double downscale(double x) {
+               initialize();
+               return downscale0(x);
+       }
+
+       public static int downscale(int x) {
+               initialize();
+               return downscale0(x);
+       }
+
+       public static Point2D downscale(double x, double y) {
+               initialize();
+               if (!hasZoom)
+                       return new Point2D.Double(x, y);
+               double s = downscaleFactorD;
+               return new Point2D.Double(x * s, y * s);
+       }
+
+       public static Point downscale(int x, int y) {
+               initialize();
+               return new Point(downscale0(x), downscale0(y));
+       }
+
+       public static Point2D downscale(Point2D p) {
+               return downscale(p.getX(), p.getY());
+       }
+
+       public static Point downscaleToInteger(Point2D p) {
+               initialize();
+               return new Point(downscaleToInteger0(p.getX()), downscaleToInteger0(p.getY()));
+       }
+
+       public static Rectangle2D downscale(Rectangle2D r, Rectangle2D target) {
+               initialize();
+               return scale(downscaleFactorD, r, target);
+       }
+
+       public static Rectangle2D downscale(Rectangle2D r) {
+               return downscale(r, null);
+       }
+
+       public static Rectangle downscale(Rectangle r, Rectangle target) {
+               initialize();
+               return scale(downscaleFactorF, r, target);
+       }
+
+       public static Rectangle downscale(Rectangle r) {
+               return downscale(r, null);
+       }
+
+       public static Rectangle downscaleToInteger(Rectangle2D r) {
+               initialize();
+               return new Rectangle( 
+                               downscaleToInteger0(r.getMinX()),
+                               downscaleToInteger0(r.getMinY()),
+                               downscaleToInteger0(r.getWidth()),
+                               downscaleToInteger0(r.getHeight()));
+       }
+
+       // Upscaling
+
+       public static double upscale(double x) {
+               initialize();
+               return upscale0(x);
+       }
+
+       public static int upscale(int x) {
+               initialize();
+               return upscale0(x);
+       }
+
+       public static Point2D upscale(double x, double y) {
+               initialize();
+               if (!hasZoom)
+                       return new Point2D.Double(x, y);
+               double s = upscaleFactorD;
+               return new Point2D.Double(x * s, y * s);
+       }
+
+       public static Point upscale(int x, int y) {
+               initialize();
+               return new Point(upscale0(x), upscale0(y));
+       }
+
+       public static Point2D upscale(Point2D p) {
+               initialize();
+               return (hasZoom && p != null) ? upscale(p.getX(), p.getY()) : p;
+       }
+
+       public static Point upscaleToInteger(Point2D p) {
+               initialize();
+               return new Point(upscaleToInteger0(p.getX()), upscaleToInteger0(p.getY()));
+       }
+
+       public static Point upscale(Point p) {
+               initialize();
+               return (hasZoom && p != null) ? upscale(p.x, p.y) : p;
+       }
+
+       public static Rectangle2D upscale(Rectangle2D r, Rectangle2D target) {
+               initialize();
+               return scale(upscaleFactorD, r, target);
+       }
+
+       public static Rectangle upscale(Rectangle r, Rectangle target) {
+               initialize();
+               return scale(upscaleFactorF, r, target);
+       }
+
+       public static Rectangle2D upscale(Rectangle2D r) {
+               return upscale(r, null);
+       }
+
+       public static Rectangle upscale(Rectangle r) {
+               return upscale(r, null);
+       }
+
+       public static Rectangle upscaleToInteger(Rectangle2D r) {
+               return new Rectangle( 
+                               upscaleToInteger0(r.getMinX()),
+                               upscaleToInteger0(r.getMinY()),
+                               upscaleToInteger0(r.getWidth()),
+                               upscaleToInteger0(r.getHeight()));
+       }
+
+}