/******************************************************************************* * Copyright (c) 2007, 2010 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: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.g2d.utils; import java.awt.geom.AffineTransform; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; /** * @author Tuukka Lehtonen */ public final class ViewBoxUtils { /** * A default viewbox generated with Stetson-Harrison method for a last * fallback value if nothing else is available. */ private static final Rectangle2D defaultViewBox = new Rectangle2D.Double(-10, -10, 20, 20); public static Rectangle2D getDefaultViewBox() { Rectangle2D r = new Rectangle2D.Double(); r.setFrame(defaultViewBox); return r; } /** * @param x * @param y * @param distanceFromPlane (0, +inf) * @param fieldOfView (0, 180) deg * @param aspectRatio (0, +inf), view-width / view-height * @return */ // public static Rectangle2D toViewBox(double x, double y, double distanceFromPlane, double fieldOfView, double aspectRatio) { // // w/2 = d * tan(fov) // // h = w / aspectRatio // double w = distanceFromPlane * Math.tan(fieldOfView * 0.5); // double h = w / aspectRatio; // // Rectangle2D r = new Rectangle2D.Double(x - w, y - h, w * 2, h * 2); // return r; // } /** * Get the view transformation of the specified canvas as an * AffineTransform. * */ public static AffineTransform getViewTransform(Rectangle2D viewBox, Point2D canvasSize) { double sx = canvasSize.getX() / viewBox.getWidth(); double sy = canvasSize.getY() / viewBox.getHeight(); AffineTransform tr = new AffineTransform(); tr.scale(sx, sy); // System.out.println("getViewTransform:"); // System.out.println(" scale: (" + sx + ", " + sy + ")"); // System.out.println(" after scale: " + tr); tr.translate(-viewBox.getMinX(), -viewBox.getMinY()); // System.out.println(" after translation: " + tr); return tr; } /** * Get the view transformation of the specified canvas as an * AffineTransform. * */ public static AffineTransform getViewTransform(Rectangle2D viewBox, Point2D controlSize, Point2D usableControlSize) { double sx = usableControlSize.getX() / viewBox.getWidth(); double sy = usableControlSize.getY() / viewBox.getHeight(); AffineTransform tr = new AffineTransform(); tr.translate( (-usableControlSize.getX()+controlSize.getX())/2, (-usableControlSize.getX()+controlSize.getX())/2); tr.scale(sx, sy); tr.translate(-viewBox.getMinX(), -viewBox.getMinY()); return tr; } /** * Interpolates a 2D position with respect to a rectangle and two normalized * interpolation factors. * * @param r the rectangle inside which to interpolate a coordinate * @param nx x interpolation value in [0,1] * @param ny y interpolation value in [0,1] * @return */ public static Point2D interpolate(Rectangle2D r, double nx, double ny) { return new Point2D.Double( r.getMinX() + r.getWidth() * nx, r.getMinY() + r.getHeight() * ny); } /** * Converts the specified AffineTransform into a view box rectangle with * respect to the size of the specified canvas instance. The client can * specify whether the scaling should be uniform or non-uniform. In the * uniform case the X scale factor is used for both dimensions. * * @param canvasSize * @param tr * @param uniform * @return */ public static Rectangle2D transformToViewBox(Point2D canvasSize, AffineTransform tr, boolean uniform) { double sx; double sy; if (uniform) { // if (tr.getScaleX() != tr.getScaleY()) // System.out.println("WARNING: scale not uniform: " + tr.getScaleX() + " vs. " + tr.getScaleY()); sx = sy = 1.0 / tr.getScaleX(); } else { sx = 1.0 / tr.getScaleX(); sy = 1.0 / tr.getScaleY(); } double startX = -tr.getTranslateX() * sx; double startY = -tr.getTranslateY() * sy; double endX = startX + sx * canvasSize.getX(); double endY = startY + sy * canvasSize.getY(); return new Rectangle2D.Double(startX, startY, endX - startX, endY - startY); } /** * Uniformly inflates the specified rectangle by a scaling factor around its * the center of the box. Modifies the specified rectangle instance itself. * * @param r the rectangle to inflate * @param scale the inflation scale factor */ public static void inflate(Rectangle2D r, double scale) { double cx = r.getCenterX(); double cy = r.getCenterY(); double nw = r.getWidth() * scale; double nh = r.getHeight() * scale; double nw2 = nw * 0.5; double nh2 = nh * 0.5; r.setFrame(cx - nw2, cy - nh2, nw, nh); } /** * Uniformly inflates the specified rectangle by a scaling factor around its * the center of the box. Returns the inflated rectangle as a new instance. * The original rectangle is not modified. * * @param r the rectangle to inflate * @param scale the inflation scale factor * @return an inflated rectangle */ public static Rectangle2D inflated(Rectangle2D r, double scale) { Rectangle2D r2 = new Rectangle2D.Double(); r2.setFrame(r); inflate(r2, scale); return r2; } /** * Uniformly fits the specified view box to size of the specified canvas. * * This means that the viewbox if the viewbox coordinates are not a perfect * multiple of the canvas size (i.e. viewBox-size * s = canvas-size), the * view box is always scaled in either the horizontal or the vertical * dimension. Here the scaled dimension is chosen to always inflate the view * box, never deflate it. * * @param canvasSize the size of the canvas to fit the view box to * @param viewBox the view box to fit */ public static void uniformFitToCanvas(Point2D canvasSize, Rectangle2D viewBox) { double cx = viewBox.getCenterX(); double cy = viewBox.getCenterY(); double cw = (double) canvasSize.getX(); double ch = (double) canvasSize.getY(); double w = viewBox.getWidth(); double h = viewBox.getHeight(); double sw = cw / w; double sh = ch / h; if (sw < sh) { // The specified viewbox fits the canvas in width but in height there // is extra space which the viewbox needs to fill! // height of requested viewbox in device coordinates = h' = h * sw // height of viewbox that fits the canvas height = h * ch / h' double dh = h * sw; double fh = h * ch / dh; viewBox.setFrameFromCenter(cx, cy, cx + w * 0.5, cy + fh * 0.5); // System.out.println("sw < sh: " + dh + ", " + fh + ": " + viewBox); } else if (sw > sh) { // The specified viewbox fits the canvas in height but in width there // is extra space which the viewbox needs to fill! // width of requested viewbox in device coordinates = dw = w * sh // width of viewbox that fits the canvas height = fw = w * cw / w' double dw = w * sh; double fw = w * cw / dw; viewBox.setFrameFromCenter(cx, cy, cx + fw * 0.5, cy + h * 0.5); // System.out.println("sw > sh: " + dw + ", " + fw + ": " + viewBox); } } public static void move(Rectangle2D r, double tx, double ty) { r.setFrame(r.getMinX() + tx, r.getMinY() + ty, r.getWidth(), r.getHeight()); } public static Rectangle2D moved(Rectangle2D r, double tx, double ty) { Rectangle2D r2 = new Rectangle2D.Double(); r2.setFrame(r.getMinX() + tx, r.getMinY() + ty, r.getWidth(), r.getHeight()); return r2; } /** * @param canvasSize * @param px * @param py * @param sx * @param sy * @return */ public static Rectangle2D uniformZoomedViewBox(Rectangle2D viewBox, Point2D canvasSize, int px, int py, double sx, double sy) { return zoomedViewBox(viewBox, canvasSize, px, py, sx, sy, true); } /** * @param c * @param px * @param py * @param sx * @param sy * @param uniform * @return */ public static Rectangle2D zoomedViewBox(Rectangle2D viewBox, Point2D canvasSize, int px, int py, double sx, double sy, boolean uniform) { if (sx <= 0 || sy <= 0) { throw new IllegalArgumentException("invalid scaling: " + sx + ", " + sy); } AffineTransform view = getViewTransform(viewBox, canvasSize); AffineTransform at = AffineTransform.getTranslateInstance(px, py); at.scale(sx, sy); at.translate(-px, -py); at.concatenate(view); Rectangle2D box = transformToViewBox(canvasSize, at, uniform); return box; } public enum Align {Begin, Center, End, Fill}; /** * Creates tranform matrix, that converts coordinates from * uniform viewport to a 100x100 square. Aspect ratio remains the same. * * @param viewport * @param horiz Horizontal alignment of 100x100 square * @param vert Vertical alignment of 100x100 square * @return transform (that can be concatenated) */ public static AffineTransform viewportToSquare( Rectangle2D viewport, Align horiz, Align vert) { boolean wide = viewport.getWidth() > viewport.getHeight(); double shorterEdge = wide ? viewport.getHeight() : viewport.getWidth(); double longerEdge = !wide ? viewport.getHeight() : viewport.getWidth(); double tx = -viewport.getX(); double ty = -viewport.getY(); double s = 100 / shorterEdge; if (wide) { if (horiz == Align.Center) tx += (longerEdge - shorterEdge) / 2; else if (horiz == Align.End) tx += longerEdge - shorterEdge; else if (horiz == Align.Fill) s = 100 / viewport.getWidth(); } else { if (vert == Align.Center) ty += (longerEdge - shorterEdge) / 2; else if (vert == Align.End) ty += longerEdge - shorterEdge; else if (vert == Align.Fill) s = 100 / viewport.getHeight(); } AffineTransform at = new AffineTransform(); at.scale(s, s); at.translate(tx, ty); return at; } /** * Creates tranform matrix, that converts coordinates from * 100x100 square to a uniform viewport. Aspect ratio remains the same. * * @param viewport * @param horiz Horizontal alignment of 100x100 square * @param vert Vertical alignment of 100x100 square * @return transform (that can be concatenated) */ public static AffineTransform squareToViewport( Rectangle2D viewport, Align horiz, Align vert) { AffineTransform at = viewportToSquare(viewport, horiz, vert); try { return at.createInverse(); } catch (NoninvertibleTransformException e) { throw new RuntimeException(e); } } }