我创建了一个静态实用程序类,用于为给定的 String 和 Graphics2D 渲染表面生成准确的 Shape 实例,这将有效地计算交叉点检测,而不会出现与仅使用边界框相关的错误。
/**
* Provides methods for generating accurate shapes describing the area a particular {@link String} will occupy when
* drawn alongside methods which can calculate the intersection of those shapes efficiently and accurately.
*
* @author Emily Mabrey (emabrey@users.noreply.github.com)
*/
public class TextShapeIntersectionCalculator {
/**
* An {@link AffineTransform} which returns the given {@link Area} unchanged.
*/
private static final AffineTransform NEW_AREA_COPY = new AffineTransform();
/**
* Calculates the delta between two single coordinate values.
*
* @param coordinateA
* The origination coordinate which we are calculating from
* @param coordinateB
* The destination coordinate which the delta takes us to
* @return A coordinate value delta which expresses the change from A to B
*/
private static int getCoordinateDelta(final int coordinateA, final int coordinateB) {
return coordinateB - coordinateA;
}
/**
* Calls {@link #getTextShape(TextLayout)} using a {@link TextLayout} generated from the provided objects and
* returns the generated {@link Shape}.
*
* @param graphicsContext
* A non-null {@link Graphics2D} object with the configuration of the desired drawing surface
* @param string
* An {@link AttributedString} containing the data describing which characters to draw alongside the
* {@link Attribute Attributes} describing how those characters should be drawn.
* @return A {@link Shape} generated via {@link #getTextShape(TextLayout)}
*/
public static Shape getTextShape(final Graphics2D graphicsContext, final AttributedString string) {
final FontRenderContext fontContext = graphicsContext.getFontRenderContext();
final TextLayout textLayout = new TextLayout(string.getIterator(), fontContext);
return getTextShape(textLayout);
}
/**
* Calls {@link #getTextShape(TextLayout)} using a {@link TextLayout} generated from the provided objects and
* returns the generated {@link Shape}.
*
* @param graphicsContext
* A non-null {@link Graphics2D} object with the configuration of the desired drawing surface
* @param attributes
* A non-null {@link Map} object populated with {@link Attribute} objects which will be used to determine the
* glyphs and styles for rendering the character data
* @param string
* A {@link String} containing the character data which is to be drawn
* @return A {@link Shape} generated via {@link #getTextShape(TextLayout)}
*/
public static Shape getTextShape(final Graphics2D graphicsContext, final Map<? extends Attribute, ?> attributes,
final String string) {
final FontRenderContext fontContext = graphicsContext.getFontRenderContext();
final TextLayout textLayout = new TextLayout(string, attributes, fontContext);
return getTextShape(textLayout);
}
/**
* Calls {@link #getTextShape(TextLayout)} using a {@link TextLayout} generated from the provided objects and
* returns the generated {@link Shape}.
*
* @param graphicsContext
* A non-null {@link Graphics2D} object with the configuration of the desired drawing surface
* @param outputFont
* A non-null {@link Font} object used to determine the glyphs and styles for rendering the character data
* @param string
* A {@link String} containing the character data which is to be drawn
* @return A {@link Shape} generated via {@link #getTextShape(TextLayout)}
*/
public static Shape getTextShape(final Graphics2D graphicsContext, final Font outputFont, final String string) {
final FontRenderContext fontContext = graphicsContext.getFontRenderContext();
final TextLayout textLayout = new TextLayout(string, outputFont, fontContext);
return getTextShape(textLayout);
}
/**
* Determines the {@link Shape} which should be generated by rendering the given {@link TextLayout} object using the
* internal {@link Graphics2D} rendering state alongside the internal {@link String} and {@link Font}. The returned
* {@link Shape} is a potentially disjoint union of all the glyph shapes generated from the character data. Note that
* the states of the mutable contents of the {@link TextLayout}, such as {@link Graphics2D}, will not be modified.
*
* @param textLayout
* A {@link TextLayout} with an available {@link Graphics2D} object
* @return A {@link Shape} which is likely a series of disjoint polygons
*/
public static Shape getTextShape(final TextLayout textLayout) {
final int firstSequenceEndpoint = 0, secondSequenceEndpoint = textLayout.getCharacterCount();
final Shape generatedCollisionShape = textLayout.getBlackBoxBounds(firstSequenceEndpoint, secondSequenceEndpoint);
return generatedCollisionShape;
}
/**
* Converts the absolute coordinates of {@link Shape Shapes} a and b into relative coordinates and uses the converted
* coordinates to call and return the result of {@link #checkForIntersection(Shape, Shape, int, int)}.
*
* @param a
* A shape located with a user space location
* @param aX
* The x coordinate of {@link Shape} a
* @param aY
* The y coordinate of {@link Shape} a
* @param b
* A shape located with a user space location
* @param bX
* The x coordinate of {@link Shape} b
* @param bY
* The x coordinate of {@link Shape} b
* @return True if the two shapes at the given locations intersect, false if they do not intersect.
*/
public static boolean checkForIntersection(final Shape a, final int aX, final int aY, final Shape b, final int bX,
final int bY) {
return checkForIntersection(a, b, getCoordinateDelta(aX, bX), getCoordinateDelta(aY, bY));
}
/**
* Detects if two shapes with relative user space locations intersect. The intersection is checked in a way which
* fails quickly if there is no intersection and which succeeds using the least amount of calculation required to
* determine there is an intersection. The location of {@link Shape} a is considered to be the origin and the position
* of {@link Shape} b is defined relative to the position of {@link Shape} a using the provided coordinate deltas.
*
* @param a
* The shape placed at what is considered the origin
* @param b
* The shape placed in the position relative to a
* @param relativeDeltaX
* The delta x coordinate of {@link Shape} b compared to the x coordination of {@link Shape} a (which is always
* 0).
* @param relativeDeltaY
* The delta y coordinate of {@link Shape} b compared to the y coordination of {@link Shape} a (which is always
* 0).
* @return True to indicate the provided {@link Shape Shapes} intersect when placed in the indicated locations.
*/
public static boolean checkForIntersection(final Shape a, final Shape b, int relativeDeltaX, int relativeDeltaY) {
return isIntersectionUsingSimpleBounds(a, b, relativeDeltaX, relativeDeltaY)
&& isIntersectionUsingAdvancedBounds(a, b, relativeDeltaX, relativeDeltaY)
&& isIntersectionUsingExactAreas(a, b, relativeDeltaX, relativeDeltaY);
}
/**
* Detects if two shapes with relative user space locations intersect. The intersection is checked using a fast but
* extremely simplified bounding box calculation. The location of {@link Shape} a is considered to be the origin and
* the position of {@link Shape} b is defined relative to the position of {@link Shape} a using the provided
* coordinate deltas.
*
* @param a
* The shape placed at what is considered the origin
* @param b
* The shape placed in the position relative to a
* @param relativeDeltaX
* The delta x coordinate of {@link Shape} b compared to the x coordination of {@link Shape} a (which is always
* 0).
* @param relativeDeltaY
* The delta y coordinate of {@link Shape} b compared to the y coordination of {@link Shape} a (which is always
* 0).
* @return True to indicate the provided {@link Shape Shapes} intersect when placed in the indicated locations.
*/
private static boolean isIntersectionUsingSimpleBounds(final Shape a, final Shape b, int relativeDeltaX,
int relativeDeltaY) {
final Rectangle rectA = a.getBounds();
final Rectangle rectB = b.getBounds();
rectB.setLocation(rectA.getLocation());
rectB.translate(relativeDeltaX, relativeDeltaY);
return rectA.contains(rectB);
}
/**
* Detects if two shapes with relative user space locations intersect. The intersection is checked using a slightly
* simplified bounding box calculation. The location of {@link Shape} a is considered to be the origin and the
* position of {@link Shape} b is defined relative to the position of {@link Shape} a using the provided coordinate
* deltas.
*
* @param a
* The shape placed at what is considered the origin
* @param b
* The shape placed in the position relative to a
* @param relativeDeltaX
* The delta x coordinate of {@link Shape} b compared to the x coordination of {@link Shape} a (which is always
* 0).
* @param relativeDeltaY
* The delta y coordinate of {@link Shape} b compared to the y coordination of {@link Shape} a (which is always
* 0).
* @return True to indicate the provided {@link Shape Shapes} intersect when placed in the indicated locations.
*/
private static boolean isIntersectionUsingAdvancedBounds(final Shape a, final Shape b, int relativeDeltaX,
int relativeDeltaY) {
final Rectangle2D rectA = a.getBounds();
final Rectangle2D rectB = b.getBounds();
rectB.setRect(rectA.getX() + relativeDeltaX, rectA.getY() + relativeDeltaY, rectB.getWidth(), rectB.getHeight());
return rectA.contains(rectB);
}
/**
* Detects if two shapes with relative user space locations intersect. The intersection is checked using a slow but
* perfectly accurate calculation. The location of {@link Shape} a is considered to be the origin and the position of
* {@link Shape} b is defined relative to the position of {@link Shape} a using the provided coordinate deltas.
*
* @param a
* The shape placed at what is considered the origin
* @param b
* The shape placed in the position relative to a
* @param relativeDeltaX
* The delta x coordinate of {@link Shape} b compared to the x coordination of {@link Shape} a (which is always
* 0).
* @param relativeDeltaY
* The delta y coordinate of {@link Shape} b compared to the y coordination of {@link Shape} a (which is always
* 0).
* @return True to indicate the provided {@link Shape Shapes} intersect when placed in the indicated locations.
*/
private static boolean isIntersectionUsingExactAreas(final Shape a, final Shape b, int relativeDeltaX,
int relativeDeltaY) {
final Area aClone = new Area(a).createTransformedArea(NEW_AREA_COPY);
final Area bClone = new Area(b).createTransformedArea(NEW_AREA_COPY);
bClone.transform(AffineTransform.getTranslateInstance(relativeDeltaX, relativeDeltaY));
aClone.intersect(bClone);
return !aClone.isEmpty();
}
}
使用这个类,您应该能够在实际字符字形不存在的任何地方绘制String,即使您要绘制的位置在另一个String 的边界框内。
我重写了你给我的代码以使用我的新交叉点检测,但在重写它时我清理了它并添加了一些新的类来改进它。这两个类只是数据结构,在我重写您的代码时需要它们:
class StringDrawInformation {
public StringDrawInformation(final String s, final Font f, final Color c, final int x, final int y) {
this.text = s;
this.font = f;
this.color = c;
this.x = x;
this.y = y;
}
public final String text;
public final Font font;
public final Color color;
public int x, y;
}
class DrawShape {
public DrawShape(final Shape s, final StringDrawInformation drawInfo) {
this.shape = s;
this.drawInfo = drawInfo;
}
public final Shape shape;
public StringDrawInformation drawInfo;
}
使用我的三个新类,我将您的代码重写为如下所示:
private static final Random random = new Random();
public static final List<StringDrawInformation> generateRandomDrawInformation(int newCount) {
ArrayList<StringDrawInformation> newInfos = new ArrayList<>();
for (int i = 0; newCount > i; i++) {
String s = "Popup!";
Font f = new Font("STENCIL", Font.BOLD, random.nextInt(100) + 10);
Color c = Color.WHITE;
int x = random.nextInt(800);
int y = random.nextInt(800);
newInfos.add(new StringDrawInformation(s, f, c, x, y));
}
return newInfos;
}
public static List<DrawShape> generateRenderablePopups(final List<StringDrawInformation> in, Graphics2D g2d) {
List<DrawShape> outShapes = new ArrayList<>();
for (StringDrawInformation currentInfo : in) {
Shape currentShape = TextShapeIntersectionCalculator.getTextShape(g2d, currentInfo.font, currentInfo.text);
boolean placeIntoOut = true;
for (DrawShape nextOutShape : outShapes) {
if (TextShapeIntersectionCalculator.checkForIntersection(nextOutShape.shape, nextOutShape.drawInfo.x,
nextOutShape.drawInfo.y, currentShape, currentInfo.x, currentInfo.y)) {
// we found an intersection so we dont place into out and we stop verifying
placeIntoOut = false;
break;
}
}
if (placeIntoOut) {
outShapes.add(new DrawShape(currentShape, currentInfo));
}
}
return outShapes;
}
private List<StringDrawInformation> popups = generateRandomDrawInformation(20);
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setBackground(Color.BLACK);
for (DrawShape renderablePopup : generateRenderablePopups(popups, g2d)) {
g2d.setColor(renderablePopup.drawInfo.color);
g2d.setFont(renderablePopup.drawInfo.font);
g2d.drawString(renderablePopup.drawInfo.text, renderablePopup.drawInfo.x, renderablePopup.drawInfo.y);
}
}
重写后的代码很容易修改以使用更多的形状、不同的字体、不同的颜色等,而不是非常难以修改。我将不同的数据包装成超类型,这些超类型封装了较小的数据类型以使它们更易于使用。我的重写并不完美,但希望这会有所帮助。
我还没有实际测试过这段代码,只是手工编写的。所以希望它能按预期工作。我最终会开始测试它,很难找到时间来写我已经完成的东西。如果您有任何问题,请随时问他们。抱歉,我花了这么长时间才做出回答!
编辑:一个小小的事后思考 - StringDrawInformation List 传递给 generateRenderabePopups(...) 的顺序是优先顺序。每个列表元素都与所有当前验证的元素进行比较。第一个未检查的元素总是成功验证,因为没有比较。第 2 个未检查元素与第 1 个元素进行检查,因为第 1 个元素已经过验证。第 3 个未检查的元素最多可与 2 个其他元素进行检查,第 4 个元素最多可检查 3 个。基本上,位置 i 的元素可能会与 i-1 个其他元素进行检查。因此,如果重要,请将更重要的文本放在列表的前面,将最不重要的文本放在列表的后面。