package org.geogebra.common.euclidian;
import org.geogebra.common.awt.GAlphaComposite;
import org.geogebra.common.awt.GBasicStroke;
import org.geogebra.common.awt.GBufferedImage;
import org.geogebra.common.awt.GColor;
import org.geogebra.common.awt.GFont;
import org.geogebra.common.awt.GGeneralPath;
import org.geogebra.common.awt.GGraphics2D;
import org.geogebra.common.awt.GPaint;
import org.geogebra.common.awt.GRectangle;
import org.geogebra.common.awt.MyImage;
import org.geogebra.common.awt.font.GTextLayout;
import org.geogebra.common.factories.AwtFactory;
import org.geogebra.common.kernel.Kernel;
import org.geogebra.common.kernel.geos.GeoElement.FillType;
import org.geogebra.common.kernel.kernelND.GeoElementND;
import org.geogebra.common.main.App;
/**
* Handles hatching of fillable geos
*/
public class HatchingHandler {
private GBufferedImage bufferedImage = null;
private GBufferedImage subImage = null;
private GGeneralPath path;
private GRectangle rect;
/**
*
*/
public HatchingHandler() {
path = AwtFactory.getPrototype().newGeneralPath();
rect = AwtFactory.getPrototype().newRectangle(0, 0, 1, 1);
}
/**
* Prototype decides what implementation will be used for static methods
*/
/**
* @param g3
* graphics
* @param defObjStroke
* hatching stroke
* @param color
* stroke color
* @param bgColor
* background color
* @param backgroundTransparency
* alpha value of background
* @param hatchDist
* distance between hatches
* @param angleDegrees
* hatching angle in degrees
* @param fillType
* type of pattern
* @param symbol
* for symbol filling
* @param app
* needed to determine right font
* @return texture paint
*/
public final GPaint setHatching(GGraphics2D g3, GBasicStroke defObjStroke,
GColor color, GColor bgColor, double backgroundTransparency,
double hatchDist, double angleDegrees, FillType fillType,
String symbol, App app) {
// round to nearest 5 degrees
double angle = Math.round(angleDegrees / 5) * Math.PI / 36;
GBasicStroke objStroke = defObjStroke;
// constrain angle between 0 and 175 degrees
if (angle < 0 || angle >= Math.PI) {
angle = 0;
}
// constrain distance between 5 and 50 pixels
double dist = hatchDist;
if (dist < 5) {
dist = 5;
} else if (dist > 50) {
dist = 50;
}
double x = dist / Math.sin(angle);
double y = dist / Math.cos(angle);
int xInt = (int) Math.abs(Math.round((x)));
int yInt = (int) Math.abs(Math.round((y)));
if (angle == 0 // horizontal
|| Kernel.isEqual(Math.PI / 2, angle, 10E-8)) { // vertical
xInt = (int) dist;
yInt = xInt;
}
int exportScale = 1;
// use higher resolution when exporting
// to avoid blockiness
if (app.isExporting()) {
// arbitrary (can run out of memory if too high though)
exportScale = (int) Math.ceil(app.getExportScale());
xInt *= exportScale;
yInt *= exportScale;
objStroke = AwtFactory.getPrototype()
.newBasicStroke(objStroke.getLineWidth() * exportScale);
}
GGraphics2D g2d = createImage(objStroke, color, bgColor,
backgroundTransparency, xInt * exportScale, yInt * exportScale);
int startX = xInt;
int startY = yInt;
int height = yInt;
int width = xInt;
switch (fillType) {
case HATCH:
drawHatching(angle, y, xInt, yInt, g2d);
break;
case CROSSHATCHED:
drawHatching(angle, y, xInt, yInt, g2d);
// draw with complementary degrees
drawHatching(Math.PI / 2 - angle, -y, xInt, yInt, g2d);
break;
case CHESSBOARD:
drawChessboard(angle, dist, g2d);
// multiply for sin for to have the same size in 0 and 45
if (Kernel.isEqual(Math.PI / 4, angle, 10E-8)) {
dist = dist * Math.sin(angle);
}
// use a frame around middle square of our 3 x 3 grid
height = width = (int) (dist * 2);
startX = startY = (int) (dist / 2);
break;
case HONEYCOMB:
drawHoneycomb(dist, g2d);
double side = dist * Math.sqrt(3) / 2;
startY = 0;
startX = 0;
height = (int) (dist * 3);
width = (int) (2 * side);
break;
case WEAVING:
startY = startX = xInt / 2;
height = width = startX * 4;
drawWeaving(angle, xInt / 2, g2d);
break;
case BRICK:
if (angle == 0 || Kernel.isEqual(Math.PI, angle, 10E-8)
|| Kernel.isEqual(Math.PI / 2, angle, 10E-8)) {
startY = startX = xInt / 2;
height = width *= 2;
}
drawBricks(angle, xInt, yInt, g2d);
break;
case DOTTED:
drawDotted(dist, g2d);
break;
case SYMBOLS:
g2d.setFont(app.getFontCanDisplay(symbol).deriveFont(GFont.PLAIN,
(int) (dist * 2.5)));
GTextLayout t = AwtFactory.getPrototype().newTextLayout(symbol,
g2d.getFont(), g2d.getFontRenderContext());
g2d = createImage(objStroke, color, bgColor, backgroundTransparency,
(int) (Math.round(t.getAscent() + t.getDescent()) / 3),
(int) (Math.round(t.getAscent() + t.getDescent()) / 3));
g2d.setFont(
app.getFontCanDisplay(symbol).deriveFont(GFont.PLAIN, 24));
g2d.drawString(symbol, 0, Math.round(t.getAscent()));
startY = 0;
startX = 0;
width = (int) t.getAscent() + (int) t.getDescent() - 1;
height = (int) t.getAscent() + (int) t.getDescent() - 1;
break;
case IMAGE:
break;
case STANDARD:
break;
}
// use the middle square of our 3 x 3 grid to fill with
GPaint ret = AwtFactory.getPrototype().newTexturePaint(
subImage = bufferedImage.getSubimage(startX, startY, width,
height),
AwtFactory.getPrototype().newRectangle(0, 0,
width / exportScale, height / exportScale));
g3.setPaint(ret);
return ret;
}
private GGraphics2D createImage(GBasicStroke objStroke, GColor color,
GColor bgColor, double backgroundTransparency, int xInt, int yInt) {
bufferedImage = AwtFactory.getPrototype().newBufferedImage(xInt * 3,
yInt * 3, 1);
GGraphics2D g2d = bufferedImage.createGraphics();
// enable anti-aliasing
g2d.setAntialiasing();
// enable transparency
g2d.setTransparent();
// paint background transparent
if (bgColor == null) {
g2d.setColor(GColor.newColor(255, 255, 255,
(int) (backgroundTransparency * 255f)));
} else {
g2d.setColor(GColor.newColor(bgColor.getRed(), bgColor.getGreen(),
bgColor.getBlue(), (int) (backgroundTransparency * 255f)));
}
g2d.fillRect(0, 0, xInt * 3, yInt * 3);
g2d.setColor(color);
g2d.setStroke(objStroke);
return g2d;
}
/**
* @param g3
* graphics
* @param geo
* geo
* @param alpha
* alpha value
*/
protected void setTexture(GGraphics2D g3, GeoElementND geo, double alpha) {
// Graphics2D g2 = geogebra.awt.GGraphics2DD.getAwtGraphics(g3);
if (geo.getFillImage() == null || geo.getFillImage().isSVG()) {
g3.setPaint(geo.getFillColor());
return;
}
MyImage image = geo.getFillImage();
GRectangle tr = AwtFactory.getPrototype().newRectangle(0, 0,
image.getWidth(), image.getHeight());
GPaint tp;
if (alpha < 1.0f) {
GBufferedImage copy = AwtFactory.getPrototype()
.newBufferedImage(image.getWidth(), image.getHeight(), 1);
GGraphics2D g2d = copy.createGraphics();
// enable anti-aliasing
g2d.setAntialiasing();
// set total transparency
g2d.setTransparent();
GColor bgColor = geo.getBackgroundColor();
// paint background transparent
if (bgColor == null) {
g2d.setColor(GColor.newColor(0, 0, 0, 0));
} else {
g2d.setColor(bgColor);
}
g2d.fillRect(0, 0, image.getWidth(), image.getHeight());
if (alpha > 0.0f) {
// set partial transparency
// AlphaComposite alphaComp = AlphaComposite.getInstance(
// AlphaComposite.SRC_OVER, alpha);
GAlphaComposite ac = AwtFactory.getPrototype()
.newAlphaComposite(alpha);
g2d.setComposite(ac);
// paint image with specified transparency
g2d.drawImage(image, 0, 0);
}
tp = AwtFactory.getPrototype().newTexturePaint(copy, tr);
} else {
tp = AwtFactory.getPrototype().newTexturePaint(image, tr);
}
// tr = new Rectangle2D.Double(0, 0, 200, 200);
// tp = new TexturePaint(getSeamlessTexture(4, 50), tr);
g3.setPaint(tp);
}
private void drawWeaving(double angle, int dist, GGraphics2D g2d) {
if (Kernel.isEqual(Math.PI / 4, angle, 10E-8)) { // 45 degrees
g2d.drawLine(2 * dist, dist, 5 * dist, 4 * dist);
g2d.drawLine(3 * dist, 0, 5 * dist, 2 * dist);
g2d.drawLine(3 * dist, 2 * dist, 0, 5 * dist);
g2d.drawLine(4 * dist, 3 * dist, 2 * dist, 5 * dist);
g2d.drawLine(2 * dist, dist, dist, 2 * dist);
g2d.drawLine(2 * dist, 3 * dist, dist, 2 * dist);
g2d.drawLine(4 * dist, 5 * dist, 6 * dist, 3 * dist);
g2d.drawLine(3 * dist, 4 * dist, 5 * dist, 6 * dist);
path.reset();
path.moveTo(dist, 2 * dist);
path.lineTo(2 * dist, dist);
path.lineTo(3 * dist + 1, 2 * dist + 1);
path.lineTo(2 * dist + 1, 3 * dist + 1);
g2d.fill(path);
path.reset();
path.moveTo(3 * dist, 4 * dist);
path.lineTo(4 * dist, 3 * dist);
path.lineTo(5 * dist + 1, 4 * dist);
path.lineTo(4 * dist, 5 * dist + 1);
g2d.fill(path);
} else { // 0 degrees
rect.setRect(dist, dist, 3 * dist, dist);
g2d.draw(rect);
rect.setRect(2 * dist, 2 * dist, dist, 3 * dist);
g2d.draw(rect);
rect.setRect(3 * dist, 3 * dist, 3 * dist, dist);
g2d.draw(rect);
rect.setRect(4 * dist, 0, dist, 3 * dist);
g2d.draw(rect);
rect.setRect(-1 * dist, 3 * dist, 3 * dist, dist);
g2d.draw(rect);
rect.setRect(4 * dist, 4 * dist, dist, 3 * dist);
g2d.draw(rect);
g2d.drawLine(4 * dist, 3 * dist, 5 * dist, 3 * dist);
g2d.drawLine(4 * dist, 4 * dist, 5 * dist, 4 * dist);
g2d.fillRect(dist, 2 * dist, dist, dist);
g2d.fillRect(3 * dist, 2 * dist, dist, dist);
g2d.fillRect(dist, 4 * dist, dist, dist);
g2d.fillRect(3 * dist, 4 * dist, dist, dist);
}
}
private void drawBricks(double angle, int xInt, int yInt, GGraphics2D g2d) {
if (angle == 0 || Kernel.isEqual(Math.PI, angle, 10E-8)) {
rect.setRect(xInt / 2.0, yInt, 2 * xInt, yInt);
g2d.draw(rect);
g2d.drawLine(xInt + xInt / 2, yInt / 2, xInt + xInt / 2, yInt);
g2d.drawLine(xInt + xInt / 2, yInt * 2, xInt + xInt / 2,
yInt * 2 + yInt / 2);
} else if (Kernel.isEqual(Math.PI / 2, angle, 10E-8)) {
rect.setRect(xInt, yInt / 2.0, xInt, 2 * yInt);
g2d.draw(rect);
g2d.drawLine(xInt / 2, yInt + yInt / 2, xInt, yInt + yInt / 2);
g2d.drawLine(xInt * 2, yInt + yInt / 2, 2 * xInt + xInt / 2,
yInt + yInt / 2);
} else if (Kernel.isEqual(Math.PI / 4, angle, 10E-8)) {
g2d.drawLine(xInt * 3, 0, 0, yInt * 3);
g2d.drawLine(xInt * 3, yInt, xInt, yInt * 3);
g2d.drawLine(xInt * 2, 0, 0, yInt * 2);
g2d.drawLine(xInt + xInt / 2, yInt + yInt / 2, 2 * xInt, yInt * 2);
} else {
g2d.drawLine(0, 0, xInt * 3, yInt * 3);
g2d.drawLine(0, yInt, xInt * 2, yInt * 3);
g2d.drawLine(xInt, 0, xInt * 3, yInt * 2);
g2d.drawLine(xInt + xInt / 2, yInt + yInt / 2, xInt, yInt * 2);
}
}
private static void drawDotted(double dist, GGraphics2D g2d) {
final double size = 2;
g2d.fill(AwtFactory.getPrototype().newEllipse2DDouble(dist, dist,
size, size));
g2d.fill(AwtFactory.getPrototype().newEllipse2DDouble(2 * dist, dist,
size, size));
g2d.fill(AwtFactory.getPrototype().newEllipse2DDouble(dist, 2 * dist,
size, size));
g2d.fill(AwtFactory.getPrototype().newEllipse2DDouble(2 * dist,
2 * dist, size, size));
}
private boolean drawChessboard(double angle, double hatchDist,
GGraphics2D g2d) {
if (Kernel.isEqual(Math.PI / 4, angle, 10E-8)) { // 45 degrees
double dist = (hatchDist * Math.sin(angle));
path.moveTo(dist / 2, dist / 2 - 1);
path.lineTo(2 * dist + dist / 2, dist / 2 - 1);
path.lineTo(dist + dist / 2, dist + dist / 2);
g2d.fill(path);
path.reset();
path.moveTo(dist + dist / 2, dist + dist / 2);
path.lineTo(2 * dist + dist / 2, 2 * dist + dist / 2);
path.lineTo(dist / 2, dist * 2 + dist / 2);
g2d.fill(path);
} else { // 0 degrees
int distInt = (int) hatchDist;
g2d.fillRect(distInt / 2, distInt / 2, distInt, distInt);
g2d.fillRect(distInt + distInt / 2, distInt + distInt / 2, distInt,
distInt);
}
return true;
}
private void drawHoneycomb(double dist, GGraphics2D g2d) {
double centerX = (dist * Math.sqrt(3) / 2);
double width = centerX + centerX;
path.moveTo(centerX, dist);
path.lineTo(centerX, 2 * dist);
path.lineTo(0, 2 * dist + dist / 2);
path.lineTo(0, 3 * dist);
g2d.draw(path);
path.reset();
path.moveTo(centerX, 2 * dist);
path.lineTo(width, 2 * dist + dist / 2);
path.lineTo(width, 3 * dist);
g2d.draw(path);
path.reset();
path.moveTo(0, 0);
path.lineTo(0, dist / 2);
path.lineTo(centerX, dist);
path.lineTo(width, dist / 2);
path.lineTo(width, 0);
g2d.draw(path);
}
private static void drawHatching(double angle, double y, int xInt, int yInt,
GGraphics2D g2d) {
if (angle == 0) { // horizontal
g2d.drawLine(0, yInt, xInt * 3, yInt);
g2d.drawLine(0, yInt * 2, xInt * 3, yInt * 2);
} else if (Kernel.isEqual(Math.PI / 2, angle, 10E-8)) { // vertical
g2d.drawLine(xInt, 0, xInt, yInt * 3);
g2d.drawLine(xInt * 2, 0, xInt * 2, yInt * 3);
} else if (y > 0) {
g2d.drawLine(xInt * 3, 0, 0, yInt * 3);
g2d.drawLine(xInt * 3, yInt, xInt, yInt * 3);
g2d.drawLine(xInt * 2, 0, 0, yInt * 2);
} else {
g2d.drawLine(0, 0, xInt * 3, yInt * 3);
g2d.drawLine(0, yInt, xInt * 2, yInt * 3);
g2d.drawLine(xInt, 0, xInt * 3, yInt * 2);
}
}
/**
* Used to check whether the hatching image is already loaded
*
* @return GBufferedImage subImage
*/
public GBufferedImage getSubImage() {
return subImage;
}
}