
org.jhotdraw8.draw.constrain.GridConstrainer Maven / Gradle / Ivy
/*
* @(#)GridConstrainer.java
* Copyright © 2023 The authors and contributors of JHotDraw. MIT License.
*/
package org.jhotdraw8.draw.constrain;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.ObservableList;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.geometry.Rectangle2D;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.PathElement;
import javafx.scene.transform.Transform;
import org.jhotdraw8.css.value.CssSize;
import org.jhotdraw8.css.value.DefaultUnitConverter;
import org.jhotdraw8.draw.DrawingView;
import org.jhotdraw8.draw.css.value.CssColor;
import org.jhotdraw8.draw.css.value.CssPoint2D;
import org.jhotdraw8.draw.css.value.CssRectangle2D;
import org.jhotdraw8.draw.figure.Drawing;
import org.jhotdraw8.draw.figure.Figure;
import org.jhotdraw8.draw.figure.ViewBoxableDrawing;
import static java.lang.Math.ceil;
import static java.lang.Math.floor;
import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.lang.Math.round;
/**
* GridConstrainer.
*
* @author Werner Randelshofer
*/
public class GridConstrainer extends AbstractConstrainer {
/**
* Up-Vector.
*/
private final Point2D UP = new Point2D(0, 1);
/**
* The angle for constrained rotations on the grid (in degrees). The value 0
* turns the constrainer off for rotations.
*/
private final DoubleProperty angle = new SimpleDoubleProperty(this, "angle") {
@Override
public void invalidated() {
fireInvalidated(this);
}
};
/**
* Whether to draw the grid.
*/
private final BooleanProperty drawGrid = new SimpleBooleanProperty(this, "drawGrid") {
@Override
public void invalidated() {
fireInvalidated();
}
};
/**
* Height of a grid cell. The value 0 turns the constrainer off for the
* vertical axis.
*/
private final ObjectProperty height = new SimpleObjectProperty<>(this, "height") {
@Override
public void invalidated() {
fireInvalidated();
}
};
private final ObjectProperty gridColorProperty = new SimpleObjectProperty<>(this, "majorGridColor", new CssColor("hsba(226,100%,75%,40%)", Color.hsb(226, 1.0, 0.75, 0.4))) {
@Override
public void invalidated() {
fireInvalidated();
}
};
private final Path majorNode = new Path();
/**
* The x-factor for the major grid of the grid.
*/
private final IntegerProperty majorX = new SimpleIntegerProperty(this, "major-x", 5) {
@Override
public void invalidated() {
fireInvalidated();
}
};
/**
* The x-factor for the major grid of the grid.
*/
private final IntegerProperty majorY = new SimpleIntegerProperty(this, "major-y", 5) {
@Override
public void invalidated() {
fireInvalidated();
}
};
private final Path minorNode = new Path();
private final Group node = new Group();
/**
* Whether to snap to the grid.
*/
private final BooleanProperty snapToGrid = new SimpleBooleanProperty(this, "snapToGrid", true) {
@Override
public void invalidated() {
fireInvalidated();
}
};
/**
* Width of a grid cell. The value 0 turns the constrainer off for the
* horizontal axis.
*/
private final ObjectProperty width = new SimpleObjectProperty<>(this, "width") {
@Override
public void invalidated() {
fireInvalidated();
}
};
/**
* The x-origin of the grid.
*/
private final ObjectProperty x = new SimpleObjectProperty<>(this, "x") {
@Override
public void invalidated() {
fireInvalidated();
}
};
/**
* The y-origin of the grid.
*/
private final ObjectProperty y = new SimpleObjectProperty<>(this, "y") {
@Override
public void invalidated() {
fireInvalidated();
}
};
/**
* Creates a grid of 10x10 pixels at origin 0,0 and 22.5 degree rotations.
*/
public GridConstrainer() {
this(0, 0, 10, 10, 22.5, 5, 5);
}
/**
* Creates a grid of width x height pixels at origin 0,0 and 22.5 degree
* rotations.
*
* @param width The width of the grid. 0 turns the grid of for the x-axis.
* @param height The width of the grid. 0 turns the grid of for the y-axis.
*/
public GridConstrainer(double width, double height) {
this(0, 0, width, height, 22.5, 5, 5);
}
/**
* Creates a grid with the specified constraints.
*
* @param x The x-origin of the grid
* @param y The y-origin of the grid
* @param width The width of the grid. 0 turns the grid of for the x-axis.
* @param height The width of the grid. 0 turns the grid of for the y-axis.
* @param angle The angular grid (in degrees). 0 turns the grid off for
* rotations.
* @param majorx the interval for major grid lines on the x-axis
* @param majory the interval for major grid lines on the y-axis
*/
public GridConstrainer(double x, double y, double width, double height, double angle, int majorx, int majory) {
this(CssSize.of(x), CssSize.of(y), CssSize.of(width), CssSize.of(height), angle, majorx, majory);
}
public GridConstrainer(CssSize x, CssSize y, CssSize width, CssSize height, double angle, int majorx, int majory) {
this.x.set(x);
this.y.set(y);
this.width.set(width);
this.height.set(height);
this.angle.set(angle);
this.minorNode.getStyleClass().setAll(STYLECLASS_CONSTRAINER_MINOR_GRID);
this.majorNode.getStyleClass().setAll(STYLECLASS_CONSTRAINER_MAJOR_GRID);
this.majorX.set(majorx);
this.majorY.set(majory);
node.getChildren().addAll(minorNode, majorNode);
}
public DoubleProperty angleProperty() {
return angle;
}
private boolean canSnapToGrid() {
return snapToGrid.get() && getWidth().getValue() > 0 && getHeight().getValue() > 0;
}
public BooleanProperty drawGridProperty() {
return drawGrid;
}
public CssColor getGridColor() {
return gridColorProperty.getValue();
}
public void setGridColor(CssColor newValue) {
gridColorProperty.setValue(newValue);
}
public CssSize getHeight() {
return height.get();
}
public int getMajorX() {
return majorX.get();
}
public int getMajorY() {
return majorY.get();
}
@Override
public Node getNode() {
return node;
}
public CssSize getWidth() {
return width.get();
}
public CssSize getX() {
return x.get();
}
public CssSize getY() {
return y.get();
}
public Property gridColorProperty() {
return gridColorProperty;
}
public ObjectProperty heightProperty() {
return height;
}
public IntegerProperty majorXProperty() {
return majorX;
}
public IntegerProperty majorYProperty() {
return majorY;
}
public BooleanProperty snapToGridProperty() {
return snapToGrid;
}
@Override
public double translateAngle(Figure f, double angle, double dir) {
if (!snapToGrid.get()) {
return angle;
}
double cAngle = this.angle.get();
if (cAngle == 0) {
return angle;
}
double ta = angle / cAngle;
if (Double.isNaN(dir) || dir == 0) {
ta = round(ta);
} else if (dir < 0) {
ta = floor(ta + 1);
} else {
ta = ceil(ta - 1);
}
double result = (ta * cAngle) % 360;
return result < 0 ? 360 + result : result;
}
@Override
public CssPoint2D translatePoint(Figure f, CssPoint2D cssp, CssPoint2D dir) {
if (!canSnapToGrid()) {
Point2D p = cssp.getConvertedValue();
Point2D covertedDir = dir.getConvertedValue();
return new CssPoint2D(p.add(covertedDir));
}
DefaultUnitConverter c = DefaultUnitConverter.getInstance();
String wunits = this.width.get().getUnits();
String hunits = this.height.get().getUnits();
double px = c.convert(cssp.getX(), wunits);
double py = c.convert(cssp.getY(), hunits);
double cx = c.convert(this.x.get(), wunits);
double cy = c.convert(this.y.get(), hunits);
double cwidth = this.width.get().getValue();
double cheight = this.height.get().getValue();
double tx = (cwidth == 0) ? px : (px - cx) / cwidth;
double ty = (cheight == 0) ? py : (py - cy) / cheight;
if (dir.getX().getValue() > 0) {
tx = floor(tx + 1);
} else if (dir.getX().getValue() < 0) {
tx = ceil(tx - 1);
} else {
tx = round(tx);
}
if (dir.getY().getValue() > 0) {
ty = ceil(ty);
} else if (dir.getY().getValue() < 0) {
ty = floor(ty);
} else {
ty = round(ty);
}
double x = Math.fma(tx, cwidth, cx);
double y = Math.fma(ty, cheight, cy);
return new CssPoint2D(CssSize.of(x, wunits), CssSize.of(y, hunits));
}
@Override
public CssRectangle2D translateRectangle(Figure f, CssRectangle2D cssr, CssPoint2D cssdir) {
if (!canSnapToGrid()) {
Rectangle2D r = cssr.getConvertedValue();
Point2D dir = cssdir.getConvertedValue();
return new CssRectangle2D(r.getMinX() + dir.getX(), r.getMinY() + dir.getY(), r.getWidth(), r.getHeight());
}
Rectangle2D r = cssr.getConvertedValue();
Point2D dir = cssdir.getConvertedValue();
double cx = this.x.get().getConvertedValue();
double cy = this.y.get().getConvertedValue();
double cwidth = this.width.get().getConvertedValue();
double cheight = this.height.get().getConvertedValue();
double tx = (cwidth == 0) ? r.getMinX() : (r.getMinX() - cx) / cwidth;
double ty = (cheight == 0) ? r.getMinY() : (r.getMinY() - cy) / cheight;
double tmaxx = (cwidth == 0) ? r.getMaxX() : (r.getMaxX() - cx) / cwidth;
double tmaxy = (cheight == 0) ? r.getMaxY() : (r.getMaxY() - cy) / cheight;
if (dir.getX() > 0) {
tx += floor(tmaxx + 1) - tmaxx;
} else if (dir.getX() < 0) {
tx = ceil(tx - 1);
} else {
tx = round(tx);
}
if (dir.getY() > 0) {
ty += floor(tmaxy + 1) - tmaxy;
} else if (dir.getY() < 0) {
ty = ceil(ty - 1);
} else {
ty = round(ty);
}
return new CssRectangle2D(
Math.fma(tx, cwidth, cx),
Math.fma(ty, cheight, cy), r.getWidth(), r.getHeight());
}
@Override
public void updateNode(DrawingView drawingView) {
ObservableList minor = minorNode.getElements();
ObservableList major = majorNode.getElements();
minor.clear();
major.clear();
CssColor gridColor = getGridColor();
minorNode.setStroke(gridColor == null ? null : gridColor.getColor());
majorNode.setStroke(gridColor == null ? null : gridColor.getColor());
minorNode.setStrokeWidth(0.5);
majorNode.setStrokeWidth(1.0);
Drawing drawing = drawingView.getDrawing();
final double dx, dy, dw, dh;
if (drawing instanceof ViewBoxableDrawing) {
dx = drawing.getNonNull(ViewBoxableDrawing.VIEW_BOX_X).getConvertedValue();
dy = drawing.getNonNull(ViewBoxableDrawing.VIEW_BOX_Y).getConvertedValue();
} else {
dx = 0;
dy = 0;
}
dw = drawing.getNonNull(Drawing.WIDTH).getConvertedValue();
dh = drawing.getNonNull(Drawing.HEIGHT).getConvertedValue();
Bounds visibleRect = drawingView.viewToWorld(drawingView.getVisibleRect());
if (drawGrid.get()) {
Transform t = drawingView.getWorldToView();
t = t.createConcatenation(drawing.getLocalToParent());
double gx0 = x.get().getConvertedValue();
double gy0 = y.get().getConvertedValue();
double gxdelta = Math.abs(width.get().getConvertedValue());
double gydelta = Math.abs(height.get().getConvertedValue());
if (gx0 < 0) {
gx0 = gx0 % gxdelta + gxdelta;
}
if (gy0 < 0) {
gy0 = gy0 % gydelta + gydelta;
}
int gmx = Math.max(0, Math.abs(majorX.get()));
int gmy = Math.max(0, Math.abs(majorY.get()));
// render minor
Point2D scaled = t.deltaTransform(gxdelta, gydelta);
if (scaled.getX() > 2 && gmx != 1) {
final int start = (int) ceil((max(dx, visibleRect.getMinX()) - gx0) / gxdelta);
final int end = (int) ceil((min(dw + dx, visibleRect.getMaxX()) - gx0) / gxdelta);
for (int i = start; i < end; i++) {
if (gmx > 0 && i % gmx == 0) {
continue;
}
double x = gx0 + i * gxdelta;
double x1 = x;
double y1 = dy;
double x2 = x;
double y2 = dh + dy;
Point2D p1 = t.transform(x1, y1);
Point2D p2 = t.transform(x2, y2);
minor.add(new MoveTo(Math.round(p1.getX()) + 0.5, p1.getY()));
minor.add(new LineTo(Math.round(p2.getX()) + 0.5, p2.getY()));
}
}
if (scaled.getY() > 2 && gmy != 1) {
final int start = (int) ceil((max(dy, visibleRect.getMinY()) - gy0) / gydelta);
final int end = (int) Math.ceil((min(dh + dy, visibleRect.getMaxY()) - gy0) / gydelta);
for (int i = start; i < end; i++) {
if (gmy > 0 && i % gmy == 0) {
continue;
}
double y = gy0 + i * gydelta;
double x1 = dx;
double y1 = y;
double x2 = dw + dx;
double y2 = y;
Point2D p1 = t.transform(x1, y1);
Point2D p2 = t.transform(x2, y2);
minor.add(new MoveTo(p1.getX(), Math.round(p1.getY()) + 0.5));
minor.add(new LineTo(p2.getX(), Math.round(p2.getY()) + 0.5));
}
}
// render major
double gmydelta = gydelta * gmy;
double gmxdelta = gxdelta * gmx;
scaled = t.deltaTransform(gmxdelta, gmydelta);
if (scaled.getX() > 2) {
final int start = (int) ceil((max(dx, visibleRect.getMinX()) - gx0) / gmxdelta);
final int end = (int) ceil((min(dw + dx, visibleRect.getMaxX()) - gx0) / gmxdelta);
for (int i = start; i < end; i++) {
double x = gx0 + i * gmxdelta;
double x1 = x;
double y1 = dy;
double x2 = x;
double y2 = dh + dy;
Point2D p1 = t.transform(x1, y1);
Point2D p2 = t.transform(x2, y2);
major.add(new MoveTo(Math.round(p1.getX()) + 0.5, p1.getY()));
major.add(new LineTo(Math.round(p2.getX()) + 0.5, p2.getY()));
}
}
if (scaled.getY() > 2) {
final int start = (int) ceil((max(dy, visibleRect.getMinY()) - gy0) / gmydelta);
final int end = (int) Math.ceil((min(dh + dy, visibleRect.getMaxY()) - gy0) / gmydelta);
for (int i = start; i < end; i++) {
double y = gy0 + i * gmydelta;
double x1 = dx;
double y1 = y;
double x2 = dw + dx;
double y2 = y;
Point2D p1 = t.transform(x1, y1);
Point2D p2 = t.transform(x2, y2);
major.add(new MoveTo(p1.getX(), Math.round(p1.getY()) + 0.5));
major.add(new LineTo(p2.getX(), Math.round(p2.getY()) + 0.5));
}
}
}
}
public ObjectProperty widthProperty() {
return width;
}
public ObjectProperty xProperty() {
return x;
}
public ObjectProperty yProperty() {
return y;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy