Я создал статический класс полезности для генерации точных Shape
экземпляров для заданных String
и Graphics2D
поверхностей рендеринга, которые будут эффективно вычислять обнаружение пересечения без ошибки, связанной только с ограничительными рамками.
/**
* 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 ([email protected])
*/
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(...)
в порядке очереди. Каждый элемент списка сравнивается со всеми проверенными в настоящее время элементами. Первый неконтролируемый элемент всегда успешно проверяется, потому что нет сопоставлений. Второй непроверенный элемент проверяется на 1-й, поскольку 1-й был проверен. Третий непроверенный может быть проверен до двух других элементов: от 4 до 3. В принципе, элемент в позиции i может быть проверен на i-1 других элементах. Итак, если это имеет значение, поместите более важный текст ранее в список и наименее важный текст позже в списке.
Помогает ли это? https://docs.oracle.com/javase/tutorial/2d/text/measuringtext.html – ajb
@ajb на самом деле да, он действительно благодарит. Но он все равно не отвечает на него полностью. Я подумал о том, чтобы указывать прямоугольники hitboxes, когда я создаю случайные шрифты и координаты, чтобы это позволяло мне это делать, но тогда даже с помощью метода .intersects у меня нет возможности проверить все остальные шрифты, если есть перекрытия. (извините, если это звучало очень запутанно. Короткий ответ: да, это помогает, но все равно нужна дополнительная помощь!) –
1) Получите «форму» для текста, как показано в [этом ответе] (http://stackoverflow.com/ а/6296381/418556). 2) Проверьте, пересекаются ли фигуры, как показано в [этом ответе] (http://stackoverflow.com/a/14575043/418556). –