package nodebox.function; import com.google.common.base.Function; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import nodebox.graphics.*; import nodebox.handle.*; import nodebox.util.MathUtils; import java.awt.geom.Arc2D; import java.util.List; import static com.google.common.base.Preconditions.checkNotNull; import static nodebox.function.MathFunctions.coordinates; /** * Core vector function library. */ public class CoreVectorFunctions { public static final FunctionLibrary LIBRARY; static { LIBRARY = JavaLibrary.ofClass("corevector", CoreVectorFunctions.class, "generator", "filter", "align", "arc", "centroid", "colorize", "connect", "copy", "doNothing", "ellipse", "fit", "fitTo", "freehand", "grid", "group", "line", "lineAngle", "link", "makePoint", "point", "pointOnPath", "rect", "snap", "skew", "toPoints", "ungroup", "textpath", "fourPointHandle", "freehandHandle", "lineAngleHandle", "lineHandle", "pointHandle", "snapHandle", "translateHandle"); } /** * Example function that generates a path. * This is here so people can view and change the existing code. * * @return An example Path. */ public static Path generator() { Path p = new Path(); p.rect(0, 0, 100, 100); return p; } /** * Example function that rotates the given shape. * * @param geometry The input geometry. * @return The new, rotated geometry. */ public static Geometry filter(Geometry geometry) { if (geometry == null) return null; Transform t = new Transform(); t.rotate(45); return t.map(geometry); } /** * Align a shape in relation to the origin. * * @param geometry The input geometry. * @param position The point to align to. * @param hAlign The horizontal align mode. Either "left", "right" or "center". * @param vAlign The vertical align mode. Either "top", "bottom" or "middle". * @return The aligned Geometry. The original geometry is left intact. */ public static AbstractGeometry align(AbstractGeometry geometry, Point position, String hAlign, String vAlign) { if (geometry == null) return null; double x = position.x; double y = position.y; Rect bounds = geometry.getBounds(); double dx, dy; if (hAlign.equals("left")) { dx = x - bounds.x; } else if (hAlign.equals("right")) { dx = x - bounds.x - bounds.width; } else if (hAlign.equals("center")) { dx = x - bounds.x - bounds.width / 2; } else { dx = 0; } if (vAlign.equals("top")) { dy = y - bounds.y; } else if (vAlign.equals("bottom")) { dy = y - bounds.y - bounds.height; } else if (vAlign.equals("middle")) { dy = y - bounds.y - bounds.height / 2; } else { dy = 0; } Transform t = Transform.translated(dx, dy); if (geometry instanceof Path) { return t.map((Path) geometry); } else if (geometry instanceof Geometry) { return t.map((Geometry) geometry); } else { throw new IllegalArgumentException("Unknown geometry type " + geometry.getClass().getName()); } } /** * Create an arc at the given position. * <p/> * Arcs rotate in the opposite direction from Java's Arc2D to be compatible with our transform functions. * * @param position The position of the arc. * @param width The arc width. * @param height The arc height. * @param startAngle The start angle. * @param degrees The amount of degrees. * @param arcType The type of arc. Either "chord", "pie", or "open" * @return The new arc. */ public static Path arc(Point position, double width, double height, double startAngle, double degrees, String arcType) { int awtType; if (arcType.equals("chord")) { awtType = Arc2D.CHORD; } else if (arcType.equals("pie")) { awtType = Arc2D.PIE; } else { awtType = Arc2D.OPEN; } return new Path(new Arc2D.Double(position.x - width / 2, position.y - height / 2, width, height, -startAngle, -degrees, awtType)); } /** * Calculate the geometric center of a shape. * * @param shape The input shape. * @return a Point at the center of the input shape. */ public static Point centroid(IGeometry shape) { if (shape == null) return Point.ZERO; Rect bounds = shape.getBounds(); return new Point(bounds.x + bounds.width / 2, bounds.y + bounds.height / 2); } /** * Change the color of a shape. * * @param shape The input shape. * @param fill The new fill color. * @param stroke The new stroke color. * @param strokeWidth The new stroke width. * @return The new colored shape. */ public static Colorizable colorize(Colorizable shape, Color fill, Color stroke, double strokeWidth) { if (shape == null) return null; Colorizable newShape = shape.clone(); newShape.setFill(fill); if (strokeWidth > 0) { newShape.setStrokeColor(stroke); newShape.setStrokeWidth(strokeWidth); } else { newShape.setStrokeColor(null); newShape.setStrokeWidth(0); } return newShape; } /** * Connects all given points, in order, as a new path. * * @param points A list of points. * @param closed If true, close the path contour. * @return A new path with all points connected. */ public static Path connect(List<Point> points, boolean closed) { if (points == null) return null; Path p = new Path(); for (Point pt : points) { p.addPoint(pt); } if (closed) p.close(); p.setFill(null); p.setStroke(Color.BLACK); p.setStrokeWidth(1); return p; } public static List<IGeometry> copy(IGeometry shape, long copies, String order, Point translate, double rotate, Point scale) { ImmutableList.Builder<IGeometry> builder = ImmutableList.builder(); Geometry geo = new Geometry(); double tx = 0; double ty = 0; double r = 0; double sx = 1.0; double sy = 1.0; char[] cOrder = order.toCharArray(); for (long i = 0; i < copies; i++) { Transform t = new Transform(); // Each letter of the order describes an operation. for (char op : cOrder) { if (op == 't') { t.translate(tx, ty); } else if (op == 'r') { t.rotate(r); } else if (op == 's') { t.scale(sx, sy); } } builder.add(t.map(shape)); tx += translate.x; ty += translate.y; r += rotate; sx += scale.x / 100 - 1; sy += scale.y / 100 - 1; } return builder.build(); } /** * Return the given object back, as-is. * <p/> * This function is used in nodes for organizational purposes. * * @param object The input object. * @return The unchanged input object. */ public static Object doNothing(Object object) { return object; } /** * Create an ellipse at the given position. * * @param position The center position of the ellipse. * @param width The ellipse width. * @param height The ellipse height. * @return The new ellipse, as a Path. */ public static Path ellipse(Point position, double width, double height) { Path p = new Path(); p.ellipse(position.x, position.y, width, height); return p; } /** * Fit a shape within the given bounds. * * @param shape The shape to fit. * @param position The center of the target shape. * @param width The width of the target bounds. * @param height The height of the target shape. * @param keepProportions If true, the shape will not be stretched or squashed. * @return A new shape that fits within the given bounds. */ public static IGeometry fit(IGeometry shape, Point position, double width, double height, boolean keepProportions) { if (shape == null) return null; Rect bounds = shape.getBounds(); // Make sure bw and bh aren't infinitely small numbers. // This will lead to incorrect transformations with for examples lines. double bw = bounds.width > 0.000000000001 ? bounds.width : 0; double bh = bounds.height > 0.000000000001 ? bounds.height : 0; Transform t = new Transform(); t.translate(position.x, position.y); double sx, sy; if (keepProportions) { // don't scale widths or heights that are equal to zero. sx = bw > 0 ? width / bw : Float.MAX_VALUE; sy = bh > 0 ? height / bh : Float.MAX_VALUE; sx = sy = Math.min(sx, sy); } else { sx = bw > 0 ? width / bw : 1; sy = bh > 0 ? height / bh : 1; } t.scale(sx, sy); t.translate(-bw / 2 - bounds.x, -bh / 2 - bounds.y); return t.map(shape); } /** * Fit a shape to another given shape. * * @param shape The shape to fit. * @param bounding The bounding, or target shape. * @param keepProportions If true, the shape will not be stretched or squashed. * @return A new shape that fits within the given bounding shape. */ public static IGeometry fitTo(IGeometry shape, IGeometry bounding, boolean keepProportions) { if (shape == null) return null; if (bounding == null) return shape; Rect bounds = bounding.getBounds(); return fit(shape, bounds.getCentroid(), bounds.width, bounds.height, keepProportions); } private final static Splitter PATH_SPLITTER = Splitter.on("M").omitEmptyStrings(); private final static Splitter CONTOUR_SPLITTER = Splitter.on(" ").omitEmptyStrings(); private final static Splitter POINT_SPLITTER = Splitter.on(","); /** * Create a new, open path with the given path string. * <p/> * The path string is composed of contour strings, starting with "M". Points are separated by a a space, e.g.: * <p/> * "M0,0 100,0 100,100 0,100M10,20 30,40 50,60" * * @param pathString The string to parse * @return a new Path. */ public static Path freehand(String pathString) { if (pathString == null) return new Path(); Path p = parsePath(pathString); p.setFill(null); p.setStroke(Color.BLACK); p.setStrokeWidth(1); return p; } /** * Create a grid of (rows * columns) points. * <p/> * The total width and height of the grid are given, and the * spacing between rows and columns is calculated. * * @param columns The number of columns. * @param rows The number of rows. * @param width The total width of the grid. * @param height The total height of the grid. * @param position The center position of the grid. * @return A list of Points. */ public static List<Point> grid(long columns, long rows, double width, double height, Point position) { double columnSize, left, rowSize, top; if (columns > 1) { columnSize = width / (columns - 1); left = position.x - width / 2; } else { columnSize = left = position.x; } if (rows > 1) { rowSize = height / (rows - 1); top = position.y - height / 2; } else { rowSize = top = position.y; } ImmutableList.Builder<Point> builder = new ImmutableList.Builder<Point>(); for (long rowIndex = 0; rowIndex < rows; rowIndex++) { for (long colIndex = 0; colIndex < columns; colIndex++) { double x = left + colIndex * columnSize; double y = top + rowIndex * rowSize; builder.add(new Point(x, y)); } } return builder.build(); } /** * Combine multiple shapes together into one Geometry. * * @param shapes The list of shapes (Path or Geometry objects) to combine. * @return The combined Geometry. */ public static Geometry group(List<IGeometry> shapes) { if (shapes == null) return null; Geometry geo = new Geometry(); for (IGeometry shape : shapes) { if (shape instanceof Path) { geo.add((Path) shape); } else if (shape instanceof Geometry) { geo.extend((Geometry) shape); } else { throw new RuntimeException("Unable to group " + shape + ": I can only group paths or geometry objects."); } } return geo; } /** * Create a line from point 1 to point 2. * * @param p1 The first point. * @param p2 The second point. * @param points The amount of points to generate along the line. * @return A line between two points. */ public static Path line(Point p1, Point p2, long points) { Path p = new Path(); p.line(p1.x, p1.y, p2.x, p2.y); p.setFill(null); p.setStroke(Color.BLACK); p.setStrokeWidth(1); p = p.resampleByAmount((int) points, true); return p; } /** * Create a line at the given starting point with the end point calculated by the angle and distance. * * @param point The starting point of the line. * @param angle The angle of the line. * @param distance The distance of the line. * @param points The amount of points to generate along the line. * @return A new line. */ public static Path lineAngle(Point point, double angle, double distance, long points) { Point p2 = coordinates(point, angle, distance); Path p = new Path(); p.line(point.x, point.y, p2.x, p2.y); p.setFill(null); p.setStroke(Color.BLACK); p.setStrokeWidth(1); p = p.resampleByAmount((int) points, true); return p; } /** * Create a path that visually links the two shapes together. * The shapes are only used for their bounding rectangles. * * @param shape1 The first shape. * @param shape2 The second shape. * @param orientation The link orientation, either "horizontal" or "vertical". * @return A new path. */ public static Path link(Grob shape1, Grob shape2, String orientation) { if (shape1 == null || shape2 == null) return null; Path p = new Path(); Rect a = shape1.getBounds(); Rect b = shape2.getBounds(); if (orientation.equals("horizontal")) { double hw = (b.x - (a.x + a.width)) / 2; p.moveto(a.x + a.width, a.y); p.curveto(a.x + a.width + hw, a.y, b.x - hw, b.y, b.x, b.y); p.lineto(b.x, b.y + b.height); p.curveto(b.x - hw, b.y + b.height, a.x + a.width + hw, a.y + a.height, a.x + a.width, a.y + a.height); } else { double hh = (b.y - (a.y + a.height)) / 2; p.moveto(a.x, a.y + a.height); p.curveto(a.x, a.y + a.height + hh, b.x, b.y - hh, b.x, b.y); p.lineto(b.x + b.width, b.y); p.curveto(b.x + b.width, b.y - hh, a.x + a.width, a.y + a.height + hh, a.x + a.width, a.y + a.height); } return p; } /** * Calculate a point on the given shape. * * @param shape The shape. * @param t The position of the point, going from 0.0-100.0 * @return The point on the given location of the path. */ public static Point pointOnPath(AbstractGeometry shape, double t) { if (shape == null) return null; t = Math.abs(t % 100); return shape.pointAt(t / 100); } @SuppressWarnings("unchecked") public static Object skew(Object shape, Point skew, Point origin) { if (shape == null) return null; Transform t = new Transform(); t.translate(origin); t.skew(skew.x, skew.y); t.translate(-origin.x, -origin.y); if (shape instanceof IGeometry) { return t.map((IGeometry) shape); } else if (shape instanceof List) { return t.map((List<Point>) shape); } else { throw new UnsupportedOperationException("I cannot work with " + shape.getClass().getSimpleName() + " objects."); } } /** * Snap the shape to a grid. * * @param shape The shape to snap. * @param distance The grid size, or distance between grid lines. * @param strength The snap strength, between 0.0-100.0. If 0.0, no snapping occurs. If 100.0, all points are on the grid. * @param position The grid position. * @return The snapped geometry. */ public static AbstractGeometry snap(AbstractGeometry shape, final double distance, final double strength, final Point position) { if (shape == null) return null; final double dStrength = strength / 100.0; return shape.mapPoints(new Function<Point, Point>() { public Point apply(Point point) { if (point == null) return Point.ZERO; double x = MathUtils.snap(point.x + position.x, distance, dStrength) - position.x; double y = MathUtils.snap(point.y + position.y, distance, dStrength) - position.y; return new Point(x, y, point.type); } }); } /** * Create a rectangle. * * @param position The center position of the rectangle. * @param width The width of the rectangle. * @param height The height of the rectangle. * @param roundness The roundness of the rectangle, given as a x,y Point. If the roundness is (0,0), we draw a normal rectangle. * @return The new rectangle. */ public static Path rect(Point position, double width, double height, Point roundness) { Path p = new Path(); if (roundness.equals(Point.ZERO)) { p.rect(position.x, position.y, width, height); } else { p.roundedRect(position.x, position.y, width, height, roundness.x, roundness.y); } return p; } /** * Get the points of a given shape. * * @param shape The input shape. * @return A list of all points of the shape. */ public static List<Point> toPoints(IGeometry shape) { if (shape == null) return null; return shape.getPoints(); } /** * Decompose the given geometry into paths. * * @param shape The input geometry * @return The list of contained paths. */ public static List<Path> ungroup(IGeometry shape) { if (shape == null) return null; if (shape instanceof Geometry) { return ((Geometry) shape).getPaths(); } else if (shape instanceof Path) { return ImmutableList.of((Path) shape); } else { throw new RuntimeException("Don't know how to decompose " + shape + " into paths."); } } /** * Create a text path. * * @return A new Path. */ public static Path textpath(String text, String fontName, double fontSize, String alignment, Point position, double width) { Text.Align align; try { align = Text.Align.valueOf(alignment); } catch (IllegalArgumentException ignore) { align = Text.Align.CENTER; } if (align == Text.Align.LEFT) { position = position.moved(0, 0); } else if (align == Text.Align.CENTER) { position = position.moved(-width / 2, 0); } else if (align == Text.Align.RIGHT) { position = position.moved(-width, 0); } Text t = new Text(text, position.x, position.y, width, 0); t.setFontName(fontName); t.setFontSize(fontSize); t.setAlign(align); return t.getPath(); } /** * Create a new point with the given x,y coordinates. * * @param x The x coordinate. * @param y The y coordinate. * @return A new Point. */ public static Point makePoint(double x, double y) { return new Point(x, y); } /** * Return the given Point as-is. */ public static Point point(Point value) { return value; } //// Utility functions //// public static Path parsePath(String s) { checkNotNull(s); Path p = new Path(); s = s.trim(); for (String pointString : PATH_SPLITTER.split(s)) { pointString = pointString.trim(); if (!pointString.isEmpty()) { p.add(parseContour(pointString)); } } return p; } public static Contour parseContour(String s) { s = s.replace(",", " "); Contour contour = new Contour(); boolean parseX = true; Double x = null; String lastString = null; for (String pointString : CONTOUR_SPLITTER.split(s)) { lastString = pointString; Double d = Double.parseDouble(pointString); if (parseX) { x = d; parseX = false; } else { contour.addPoint(new Point(x, d)); parseX = true; } } if (! parseX) throw new IllegalArgumentException("Could not parse point " + lastString); return contour; } public static Point parsePoint(String s) { Double x = null, y = null; for (String numberString : POINT_SPLITTER.split(s)) { if (x == null) { x = Double.parseDouble(numberString); } else if (y == null) { y = Double.parseDouble(numberString); } else { throw new IllegalArgumentException("Too many coordinates in point " + s); } } if (x != null && y != null) { return new Point(x, y); } else { throw new IllegalArgumentException("Could not parse point " + s); } } //// Handles //// public static Handle fourPointHandle() { return new FourPointHandle(); } public static Handle freehandHandle() { return new FreehandHandle(); } public static Handle lineAngleHandle() { CombinedHandle handle = new CombinedHandle(); handle.addHandle(new PointHandle()); handle.addHandle(new RotateHandle("angle", "position")); return handle; } public static Handle lineHandle() { return new LineHandle(); } public static Handle pointHandle() { return new PointHandle(); } public static Handle snapHandle() { return new SnapHandle(); } public static Handle translateHandle() { return new TranslateHandle(); } }