/* * @(#)ShapeBounds.java * * $Date: 2015-03-16 17:04:37 -0700 (Mon, 16 Mar 2015) $ * * Copyright (c) 2011 by Jeremy Wood. * All rights reserved. * * The copyright of this software is owned by Jeremy Wood. * You may not use, copy or modify this software, except in * accordance with the license agreement you entered into with * Jeremy Wood. For details see accompanying license terms. * * This software is probably, but not necessarily, discussed here: * https://javagraphics.java.net/ * * That site should also contain the most recent official version * of this software. (See the SVN repository for more details.) */ package com.bric.geom; import java.awt.Shape; import java.awt.geom.AffineTransform; import java.awt.geom.PathIterator; import java.awt.geom.Rectangle2D; /** This class features an efficient and accurate * <code>getBounds()</code> method. The <code>java.awt.Shape</code> * API clearly states that the <code>Shape.getBounds2D()</code> method * may return a rectangle larger than the bounds of the actual shape, * so here I present a method to get the bounds without resorting to * the very-accurate-but-very-slow <code>java.awt.geom.Area</code> class. * * @see com.bric.geom.ShapeBoundsDemo */ public class ShapeBounds { /** This calculates the precise bounds of a shape. * @param shape the shape you want the bounds of. * This method throws a NullPointerException if this is null. * @return the bounds of <code>shape</code>. * * @throws EmptyPathException if the shape argument is empty. */ public static Rectangle2D getBounds(Shape shape) throws EmptyPathException{ return getBounds(shape,null,null); } public static Rectangle2D getBounds(Shape[] shapes) { Rectangle2D r = null; for(int a = 0; a<shapes.length; a++) { try { Rectangle2D t = getBounds(shapes[a]); if(r==null) { r = t; } else { r.add(t); } } catch(EmptyPathException e) {} } return r; } /** This calculates the precise bounds of a shape. * @param shape the shape you want the bounds of. * This method throws a NullPointerException if this is null. * @param transform if this is non-null, then this method returns the bounds of * <code>shape</code> as seen through <code>t</code>. * @return the bounds of <code>shape</code>, as seen through <code>transform</code>. * * @throws EmptyPathException if the shape argument is empty. */ public static Rectangle2D getBounds(Shape shape,AffineTransform transform) throws EmptyPathException { return getBounds(shape,transform,null); } /** This calculates the precise bounds of a shape. * @param shape the shape you want the bounds of. * This method throws a NullPointerException if this is null. * @param transform if this is non-null, then this method returns the bounds of * <code>shape</code> as seen through <code>t</code>. * @param r if this is non-null, then the result is stored in * this rectangle. This is useful when you need to call this method * repeatedly without allocating a lot of memory. * @return the bounds of <code>shape</code>, as seen through <code>transform</code>. * * @throws EmptyPathException if the shape argument is empty. */ public static Rectangle2D getBounds(Shape shape,AffineTransform transform,Rectangle2D r) throws EmptyPathException { PathIterator i = shape.getPathIterator(transform); return getBounds(i,r); } /** This calculates the precise bounds of a shape. * @param shape the shape you want the bounds of. * This method throws a NullPointerException if this is null. * @param r if this is non-null, then the result is stored in * this rectangle. This is useful when you need to call this method * repeatedly without allocating a lot of memory. * @return the bounds of <code>shape</code>. * * @throws EmptyPathException if the shape argument is empty. */ public static Rectangle2D getBounds(Shape shape,Rectangle2D r) throws EmptyPathException { return getBounds(shape,null,r); } /** This calculates the precise bounds of a shape. * @param i the shape you want the bounds of. * This method throws a NullPointerException if this is null. * @return the bounds of <code>i</code>. */ public static Rectangle2D getBounds(PathIterator i) { return getBounds(i,null); } /** This calculates the precise bounds of a shape. * @param i the shape you want the bounds of. * This method throws a NullPointerException if this is null. * @param r if this is non-null, then the result is stored in * this rectangle. This is useful when you need to call this method * repeatedly without allocating a lot of memory. * @return the bounds of <code>i</code>. */ public static Rectangle2D getBounds(PathIterator i,Rectangle2D r) { float[] f = new float[6]; int k; /** left, top, right, and bottom bounds */ float[] bounds = null; float lastX = 0; float lastY = 0; //A, B, C, and D in the equation x = a*t^3+b*t^2+c*t+d //or A, B, and C in the equation x = a*t^2+b*t+c float[] x_coeff = new float[4]; float[] y_coeff = new float[4]; float t, x, y, det; while(i.isDone()==false) { k = i.currentSegment(f); if(k==PathIterator.SEG_MOVETO) { lastX = f[0]; lastY = f[1]; } else if(k==PathIterator.SEG_CLOSE) { //do nothing //note if we had a simple MOVETO and SEG_CLOSE then //we haven't changed "bounds". This is intentional, //so if the shape is badly defined the bounds //should still make sense. } else { if(bounds==null) { bounds = new float[] {lastX,lastY,lastX,lastY}; } else { if(lastX<bounds[0]) bounds[0] = lastX; if(lastY<bounds[1]) bounds[1] = lastY; if(lastX>bounds[2]) bounds[2] = lastX; if(lastY>bounds[3]) bounds[3] = lastY; } if(k==PathIterator.SEG_LINETO) { if(f[0]<bounds[0]) bounds[0] = f[0]; if(f[1]<bounds[1]) bounds[1] = f[1]; if(f[0]>bounds[2]) bounds[2] = f[0]; if(f[1]>bounds[3]) bounds[3] = f[1]; lastX = f[0]; lastY = f[1]; } else if(k==PathIterator.SEG_QUADTO) { //check the end point if(f[2]<bounds[0]) bounds[0] = f[2]; if(f[3]<bounds[1]) bounds[1] = f[3]; if(f[2]>bounds[2]) bounds[2] = f[2]; if(f[3]>bounds[3]) bounds[3] = f[3]; //find the extrema x_coeff[0] = lastX-2*f[0]+f[2]; x_coeff[1] = -2*lastX+2*f[0]; x_coeff[2] = lastX; y_coeff[0] = lastY-2*f[1]+f[3]; y_coeff[1] = -2*lastY+2*f[1]; y_coeff[2] = lastY; //x = a*t^2+b*t+c //dx/dt = 0 = 2*a*t+b //t = -b/(2a) t = -x_coeff[1]/(2*x_coeff[0]); if(t>0 && t<1) { x = x_coeff[0]*t*t+x_coeff[1]*t+x_coeff[2]; if(x<bounds[0]) bounds[0] = x; if(x>bounds[2]) bounds[2] = x; } t = -y_coeff[1]/(2*y_coeff[0]); if(t>0 && t<1) { y = y_coeff[0]*t*t+y_coeff[1]*t+y_coeff[2]; if(y<bounds[1]) bounds[1] = y; if(y>bounds[3]) bounds[3] = y; } lastX = f[2]; lastY = f[3]; } else if(k==PathIterator.SEG_CUBICTO) { if(f[4]<bounds[0]) bounds[0] = f[4]; if(f[5]<bounds[1]) bounds[1] = f[5]; if(f[4]>bounds[2]) bounds[2] = f[4]; if(f[5]>bounds[3]) bounds[3] = f[5]; x_coeff[0] = -lastX+3*f[0]-3*f[2]+f[4]; x_coeff[1] = 3*lastX-6*f[0]+3*f[2]; x_coeff[2] = -3*lastX+3*f[0]; x_coeff[3] = lastX; y_coeff[0] = -lastY+3*f[1]-3*f[3]+f[5]; y_coeff[1] = 3*lastY-6*f[1]+3*f[3]; y_coeff[2] = -3*lastY+3*f[1]; y_coeff[3] = lastY; //x = a*t*t*t+b*t*t+c*t+d //dx/dt = 3*a*t*t+2*b*t+c //t = [-B+-sqrt(B^2-4*A*C)]/(2A) //A = 3*a //B = 2*b //C = c //t = (-2*b+-sqrt(4*b*b-12*a*c)]/(6*a) det = (4*x_coeff[1]*x_coeff[1]-12*x_coeff[0]*x_coeff[2]); if(det<0) { //there are no solutions! nothing to do here } else if(det==0) { //there is 1 solution t = -2*x_coeff[1]/(6*x_coeff[0]); if(t>0 && t<1) { x = x_coeff[0]*t*t*t+x_coeff[1]*t*t+x_coeff[2]*t+x_coeff[3]; if(x<bounds[0]) bounds[0] = x; if(x>bounds[2]) bounds[2] = x; } } else { //there are 2 solutions: det = (float)Math.sqrt(det); t = (-2*x_coeff[1]+det)/(6*x_coeff[0]); if(t>0 && t<1) { x = x_coeff[0]*t*t*t+x_coeff[1]*t*t+x_coeff[2]*t+x_coeff[3]; if(x<bounds[0]) bounds[0] = x; if(x>bounds[2]) bounds[2] = x; } t = (-2*x_coeff[1]-det)/(6*x_coeff[0]); if(t>0 && t<1) { x = x_coeff[0]*t*t*t+x_coeff[1]*t*t+x_coeff[2]*t+x_coeff[3]; if(x<bounds[0]) bounds[0] = x; if(x>bounds[2]) bounds[2] = x; } } det = (4*y_coeff[1]*y_coeff[1]-12*y_coeff[0]*y_coeff[2]); if(det<0) { //there are no solutions! nothing to do here } else if(det==0) { //there is 1 solution t = -2*y_coeff[1]/(6*y_coeff[0]); if(t>0 && t<1) { y = y_coeff[0]*t*t*t+y_coeff[1]*t*t+y_coeff[2]*t+y_coeff[3]; if(y<bounds[1]) bounds[1] = y; if(y>bounds[3]) bounds[3] = y; } } else { //there are 2 solutions: det = (float)Math.sqrt(det); t = (-2*y_coeff[1]+det)/(6*y_coeff[0]); if(t>0 && t<1) { y = y_coeff[0]*t*t*t+y_coeff[1]*t*t+y_coeff[2]*t+y_coeff[3]; if(y<bounds[1]) bounds[1] = y; if(y>bounds[3]) bounds[3] = y; } t = (-2*y_coeff[1]-det)/(6*y_coeff[0]); if(t>0 && t<1) { y = y_coeff[0]*t*t*t+y_coeff[1]*t*t+y_coeff[2]*t+y_coeff[3]; if(y<bounds[1]) bounds[1] = y; if(y>bounds[3]) bounds[3] = y; } } lastX = f[4]; lastY = f[5]; } } i.next(); } if(bounds==null) { throw new EmptyPathException(); } if(r!=null) { r.setFrame(bounds[0],bounds[1],bounds[2]-bounds[0],bounds[3]-bounds[1]); return r; } return new Rectangle2D.Float(bounds[0],bounds[1],bounds[2]-bounds[0],bounds[3]-bounds[1]); } }