blob: 4087ce2735c0f4fe94d1059d5e8c1bc0a782da7d [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
package flashx.textLayout.utils
{
import flash.geom.Rectangle;
import flashx.textLayout.debug.Debugging;
import flashx.textLayout.debug.assert;
import flashx.textLayout.elements.FlowElement;
import flashx.textLayout.elements.FlowLeafElement;
import flashx.textLayout.tlf_internal;
use namespace tlf_internal;
[ExcludeClass]
/**
* The HitTestArea class is a lightweight implementation of the Warnock
* algorithm for rectangles. It is used for hit-testing in FlowElements, which
* may contain multiple child elements. The algorithm attempts to set the logical
* midpoint to be outside of a bounding rectangle to avoid having to split too many rectangles.
* Note that the code is optimized for non-overlapping rectangles;
* the determination of the logical midpoint does not work well for overlapping
* rectangles because the algorithm breaks once it found a rectangle that contains
* the geometric midpoint.
*
* The pseudocode for this algorithm is:
*
* if !(point in this rectangle) return false;
* if !(hasKids) return true;
* determine the quadrant where the point is in (top left "tl", top right "tr",
* bottom left "bl", or bottom right "br");
* if this[quadrant] == null, return false; // no rectangle covers this area
* else return this[quadrant].hitTest(x,y);
*
* To avoid having to create a 4-element array for the four quadrants, the code
* constructs the property name and accesses the property with dynamic lookup.
*/
public class HitTestArea
{
private var tl:HitTestArea = null; // top left quadrant // NO PMD
private var tr:HitTestArea = null; // top right quadrant // NO PMD
private var bl:HitTestArea = null; // bottom left quadrant // NO PMD
private var br:HitTestArea = null; // bottom right quadrant // NO PMD
private var _rect:Rectangle; // the bounding rectangle
private var _xm:Number; // logical midpoint
private var _ym:Number;
/**
* The owning FlowElement is the element whose area this HitTestArea
* covers. This variable is set for a rectangle if the structure passed
* in to the constructor contains an "owner" property. If it is null,
* the instance has child quadrants.
*/
private var _owner:FlowElement = null;
CONFIG::debug
{
static private var _depth:int = 0;
/**
* This array takes the counts for recursion depths on hit testing.
*/
static public const depths:Array = [];
}
/**
* Create a HitTestArea with an object containing enumerable property objects as a rectangle
* in a "rect" property, and a a FlowElement in its "owner" property. First,
* determine the bounding rectangle; then, determine the midpoint, and fill
* in each quadrant with the intersecting rectangles.
* @param obj An object containing {rect:Rectangle, owner:FlowElement} objects as property values.
*/
public function HitTestArea(objects:Object)
{
initialize(objects);
}
/** @private */
tlf_internal function initialize(objects:Object):void
{
var obj:Object;
// determine the number of objects; all we need to know is 0, 1, or more
var count:int = 0;
if (objects)
{
for (obj in objects)
{
if (++count > 1)
break;
}
}
if (count == 0)
{
_rect = new Rectangle();
_xm = _ym = 0;
return;
}
// Determine the bounding rectangle
// if the object only has one element, it is easy
if (count == 1)
{
for each (obj in objects)
{
_rect = obj.rect;
_xm = _rect.left;
_ym = _rect.top;
_owner = obj.owner;
CONFIG::debug { assert(_owner != null, "No owner given"); }
return;
}
}
var r:Rectangle;
// if more than one element, we need to calulate and fill
for each (obj in objects)
{
r = obj.rect;
if (!_rect)
_rect = r;
else
_rect = _rect.union(r);
}
// Set the geometric midpoint
_xm = Math.ceil(_rect.left + _rect.width / 2);
_ym = Math.ceil(_rect.top + _rect.height / 2);
// Hard stop here: if the bounding rectangle is < 3 pixels in any direction,
// we consider it to be a single rectangle.
if (_rect.width <= 3 || _rect.height <= 3)
{
// Here, we set the owner to the first element's owner
for each (obj in objects)
{
_owner = obj.owner;
CONFIG::debug { assert(_owner != null, "No owner given"); }
return;
}
}
// Determine the logical midpoint. This point is as close to
// the geometric midpoint as possible. If the midpoint is inside
// a rectangle, the midpoint is adjusted to the nearest point
// outside of the rectangle if possible to avoid excessive
// splitting of rectangles. The first point is used as the new
// midpoint to avoid having to deal with overlapping rectangles,
// which should usually not appear at all.
for each (obj in objects)
{
r = obj.rect;
if (r.equals(_rect))
continue;
if (r.contains(_xm, _ym))
{
// The midpoint is over a rectangle; find the closest
// rectangle boundary and move the midpoint there
var dxLower:Number = _xm - r.left;
var dxUpper:Number = r.right - _xm;
var dyLower:Number = _ym - r.top;
var dyUpper:Number = r.bottom - _ym;
// this may lead to _xm and/or _ym being equal to left/top
_xm = (dxLower > dxUpper) ? _xm + dxUpper : _xm - dxLower;
_ym = (dyLower > dyUpper) ? _ym + dyUpper : _ym - dyLower;
// this should be changed if overlapping rectangles need to be supported
// in that case, all rectangles that have a hit shouldd be checked to
// determine the closest boundary
break;
}
}
// Insert all rectangles into the respective quadrants
var quadrant:Rectangle = new Rectangle(_rect.left, _rect.top, _xm - _rect.left, _ym - _rect.top);
addQuadrant(objects, "tl", quadrant);
quadrant.left = _xm;
quadrant.right = _rect.right;
addQuadrant(objects, "tr", quadrant);
quadrant.left = _rect.left;
quadrant.top = _ym;
quadrant.right = _xm;
quadrant.bottom = _rect.bottom;
addQuadrant(objects, "bl", quadrant);
quadrant.left = _xm;
quadrant.right = _rect.right;
addQuadrant(objects, "br", quadrant);
}
/**
* Do a hit test. If the point is within this rectangle, determine
* the quadrant of the point. If the quadrant is empty, the hit test
* is true. If not, recurse into that quadrant.
* @param x the X coordinate
* @param y the Y coordinate
* @return the owner if the hit test succeeds, null otherwise
*/
public function hitTest(x:Number, y:Number):FlowElement
{
if (!_rect.contains(x, y))
return null;
// if there are no kids (the owner is set), the rectangle has been hit
// no need to attempt to retrieve the quadrant
if (_owner)
return _owner;
// determine quadrant
var quadrantName:String = (y < _ym) ? "t" : "b";
quadrantName += (x < _xm) ? "l" : "r";
var quadrant:HitTestArea = this[quadrantName];
// there is no child rectangle here: no hit
if (quadrant == null)
return null;
// ask the child
return quadrant.hitTest(x, y);
}
/* Use this version to track hit testing in debug code
public function hitTest(x:Number, y:Number, depth:int=1):FlowElement
{
if (!_rect.contains(x, y))
{
trace("Depth " + depth + ": " + x + "," + y);
return null;
}
// if there are no kids (the owner is set), the rectangle has been hit
// no need to attempt to retrieve the quadrant
if (_owner)
{
trace("Depth " + depth + ": " + x + "," + y + ": " + _owner);
if (depths[depth] == undefined)
depths[depth] = 0;
depths[depth]++;
return _owner;
}
// determine quadrant
var quadrantName:String = (y < _ym) ? "t" : "b";
quadrantName += (x < _xm) ? "l" : "r";
var quadrant:HitTestArea = this[quadrantName];
// there is no child rectangle here: no hit
if (quadrant == null)
{
trace("Depth " + depth + ": " + x + "," + y);
return null;
}
// ask the child
return quadrant.hitTest(x, y, depth+1);
}
*/
/** @private
* Add the given objects of rectangles to the given quadrant. Create an array of
* intersecting rectangles, and, if that array if not empty, create a new
* HitTestArea covering these rectangles, and store it into the quadrant
* property. Note that this method is recursive.
* @param arr An object of rectangle objects.
* @param propName The name of the quadrant property (tl, tr, bl, br).
* @param quadrant The bounding rectangle for this quadrant.
*/
private function addQuadrant(objects:Object, propName:String, quadrant:Rectangle):void
{
if (quadrant.isEmpty())
return;
// Collect the objects of intersecting rectangles
var qrects:Object = {};
var i:int = 0;
for each (var obj:Object in objects)
{
var intersect:Rectangle = obj.rect.intersection(quadrant);
if (!intersect.isEmpty())
qrects[i++] = {owner:obj.owner, rect:intersect};
}
if (i > 0)
this[propName] = new HitTestArea(qrects);
}
}
}