Skip to main content


Eclipse Community Forums
Forum Search:

Search      Help    Register    Login    Home
Home » Eclipse Projects » GEF » [GEF4] Calculation of stroke outline in model
[GEF4] Calculation of stroke outline in model [message #1734940] Mon, 13 June 2016 23:44 Go to next message
Colin Sharples is currently offline Colin SharplesFriend
Messages: 90
Registered: July 2009
Location: Wellington, New Zealand
Member

I have parts that render a PolyBezier using a stroke width that is specified by the model object. The model object is just storing the PolyBezier (which is infinitely thin) and an int for the stroke width. However, I'd like to be able to calculate the actual outline of the rendered geometry in the model code as well.

It's clear that something in either GEF4 or JavaFX is able to do this, as I can see that hit detection is using the outline of the stroke rather than just the actual model PolyBezier - I just can't see where this happens in the code.

The reason I need this is that I might have two shapes with different stroke widths that connect up, and rather than having an abrupt change from one stroke width to the other, I want to create a new connection object that has an outline that smoothly increases the stroke. To do that, I'd need to be able to identify points on the outer edges of the strokes, so that I can then create a polygon to join them together.

I'll try some ASCII art to illustrate. Imagine that each character is a pixel. The 'x' marks a waypoint on the model object. There are two lines, which share a common point at the third x. The first line has a stroke width of 3, so the 'a's represent the pixels filled by that line; the second line has a stroke width of 9, represented by the 'c's.

                           4ccccccccccccccccccccccccccccccc
                           cccccccccccccccccccccccccccccccc
                           cccccccccccccccccccccccccccccccc
aaaaaaaaaaaa1aaaaaaaaaaaaaacccccccccccccccccccccccccccccccc
xaaaaaaaaaaaxaaaaaaaaaaaaaxcccccccccccccccccxcccccccccccccx
aaaaaaaaaaaa2aaaaaaaaaaaaaacccccccccccccccccccccccccccccccc
                           cccccccccccccccccccccccccccccccc
                           cccccccccccccccccccccccccccccccc
                           3ccccccccccccccccccccccccccccccc


The points I want to identify are represented by numbers 1, 2, 3 and 4 - I would use these to create a polygon which would be rendered underneath the other two, represented by the 'o's here:

                           4ccccccccccccccccccccccccccccccc
                     oooooocccccccccccccccccccccccccccccccc
               oooooooooooocccccccccccccccccccccccccccccccc
aaaaaaaaaaaa1aaaaaaaaaaaaaacccccccccccccccccccccccccccccccc
xaaaaaaaaaaaxaaaaaaaaaaaaaxcccccccccccccccccxcccccccccccccx
aaaaaaaaaaaa2aaaaaaaaaaaaaacccccccccccccccccccccccccccccccc
                ooooooooooocccccccccccccccccccccccccccccccc
                    ooooooocccccccccccccccccccccccccccccccc
                           3ccccccccccccccccccccccccccccccc


I have come up with a couple of algorithms to generate these points, based on making a polyline approximation of the PolyBezier, then creating a series of polygons of the appropriate width using the straight lines as the centre. (Note, the version using Ring to join the polygons together produces a more accurate outline but has terrible performance).

However, it strikes me that something must already be doing this for hit detection to work. If it's happening in the JavaFX layer, then I probably can't use it in the model code, but if it's in the GEF4 layer then I should be able to use it. Where should I be looking?


Colin Sharples
CTG Games Ltd
Wellington, New Zealand
Re: [GEF4] Calculation of stroke outline in model [message #1734947 is a reply to message #1734940] Tue, 14 June 2016 04:48 Go to previous messageGo to next message
Alexander Nyssen is currently offline Alexander NyssenFriend
Messages: 235
Registered: July 2009
Location: L√ľnen
Senior Member
Hi Colin,

within GEF4 there is currently no functionality that allows to calculate such a "fat-line" for a PolyBezier. We have raised https://bugs.eclipse.org/bugs/show_bug.cgi?id=495290 in order to add such a functionality to NodeUtils#getShapeOutline(Node).

The related computations within JavaFX are internal and AFAIK there is no public API to access it. That's the reason why the shape outline we compute for shape geometries within NodeUtils#getShapeOutline(Node) is only an approximation as well (it simply resizes the geometry to the shape/layout bounds that contain the stroke; this is not precise, as it does not consider the 'effect' of line cap and join properly). As such, computing an approximation as you do is probably the best that can be done.

Regards,
Alexander

PS: Can you please raise an issue concerning the poor performance of the Ring geometry (providing a snippet or some data that allows us to investigate this in more detail)?

[Updated on: Tue, 14 June 2016 04:50]

Report message to a moderator

Re: [GEF4] Calculation of stroke outline in model [message #1735148 is a reply to message #1734947] Wed, 15 June 2016 21:10 Go to previous messageGo to next message
Colin Sharples is currently offline Colin SharplesFriend
Messages: 90
Registered: July 2009
Location: Wellington, New Zealand
Member

Bug #496220 raised for the Ring performance issue

Colin Sharples
CTG Games Ltd
Wellington, New Zealand
Re: [GEF4] Calculation of stroke outline in model [message #1735200 is a reply to message #1735148] Thu, 16 June 2016 10:00 Go to previous messageGo to next message
Matthias Wienand is currently offline Matthias WienandFriend
Messages: 163
Registered: March 2015
Senior Member
Hi Colin,

while it is true that curve offsetting is not supported by the Geometry component, yet, and the JavaFX functionality is indeed internal, the points that are needed can be computed using the Geometry component as follows:

1) Find the BezierCurve within the PolyBezier that contains the two "x" points before/at the joint of the two curves. This should either be the last or the second to last BezierCurve within the array returned by PolyBezier#toBezier().
2) Compute the first derivative of that BezierCurve in order to offset the point by the stroke width. The first derivative of a BezierCurve is called its hodograph and be accessed using BezierCurve#getDerivative().
3) Offset the "x" points by the stroke width. Suppose the first "x" point is called startPoint, and the last "x" point (i.e. the one at the joint) is called endPoint. The startPoint needs to be offset by the start curve's stroke width, the endPoint needs to be offset twice: once for the start curve's stroke width, and once for the end curve's stroke width.

The following snippet demonstrates the computation:

import org.eclipse.gef4.fx.utils.Geometry2Shape;
import org.eclipse.gef4.geometry.euclidean.Vector;
import org.eclipse.gef4.geometry.planar.BezierCurve;
import org.eclipse.gef4.geometry.planar.Line;
import org.eclipse.gef4.geometry.planar.Point;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Path;
import javafx.scene.shape.Polygon;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.shape.StrokeLineJoin;
import javafx.scene.shape.StrokeType;
import javafx.stage.Stage;

public class CurveOffsetSnippet extends Application {

	public static void main(String[] args) {
		launch();
	}

	protected void addCurves(Pane root, double y, boolean transition) {
		// create curves
		BezierCurve firstCurve = new Line(100, y, 200, y);
		BezierCurve secondCurve = new Line(200, y, 300, y);

		// create paths
		Path firstPath = Geometry2Shape.toPath(firstCurve.toPath());
		Path secondPath = Geometry2Shape.toPath(secondCurve.toPath());

		// add to scene
		root.getChildren().addAll(firstPath, secondPath);

		// strokes
		double firstStrokeWidth = 5;
		double secondStrokeWidth = 15;

		// configure strokes
		firstPath.setStrokeType(StrokeType.CENTERED);
		firstPath.setStrokeWidth(firstStrokeWidth);
		firstPath.setStrokeLineCap(StrokeLineCap.BUTT);
		firstPath.setStrokeLineJoin(StrokeLineJoin.BEVEL);
		firstPath.setStroke(Color.DARKRED);

		secondPath.setStrokeType(StrokeType.CENTERED);
		secondPath.setStrokeWidth(secondStrokeWidth);
		secondPath.setStrokeLineCap(StrokeLineCap.BUTT);
		secondPath.setStrokeLineJoin(StrokeLineJoin.BEVEL);
		secondPath.setStroke(Color.DARKRED);

		// compute polygons to transition from the thinner/first curve to
		// the thicker/second curve
		if (transition) {
			// compute start point and end point
			Point startPoint = firstCurve.get(0);
			Point endPoint = firstCurve.get(1);

			// compute curve normals for start point and end point
			Vector startNormal = new Vector(firstCurve.getDerivative().get(0))
					.getOrthogonalComplement().getNormalized();
			Vector endNormal = new Vector(firstCurve.getDerivative().get(1))
					.getOrthogonalComplement().getNormalized();

			// compute left and right offset points for the start point (first
			// curve stroke width)
			Point startOffsetLeft = startPoint.getTranslated(
					startNormal.getMultiplied(firstStrokeWidth / 2).toPoint());
			Point startOffsetRight = startPoint.getTranslated(
					startNormal.getMultiplied(firstStrokeWidth / 2).toPoint()
							.getNegated());

			// compute left and right offset points for the end point (first
			// curve stroke width)
			Point endThinOffsetLeft = endPoint.getTranslated(
					endNormal.getMultiplied(firstStrokeWidth / 2).toPoint());
			Point endThinOffsetRight = endPoint
					.getTranslated(endNormal.getMultiplied(firstStrokeWidth / 2)
							.toPoint().getNegated());

			// compute left and right offset points for the end point (second
			// curve stroke width)
			Point endOffsetLeft = endPoint.getTranslated(
					endNormal.getMultiplied(secondStrokeWidth / 2).toPoint());
			Point endOffsetRight = endPoint.getTranslated(
					endNormal.getMultiplied(secondStrokeWidth / 2).toPoint()
							.getNegated());

			// compute left and right polygons for a smooth transition between
			// the stroke widths
			Polygon leftPolygon = new Polygon(startOffsetLeft.x,
					startOffsetLeft.y, endThinOffsetLeft.x, endThinOffsetLeft.y,
					endOffsetLeft.x, endOffsetLeft.y);
			Polygon rightPolygon = new Polygon(startOffsetRight.x,
					startOffsetRight.y, endThinOffsetRight.x,
					endThinOffsetRight.y, endOffsetRight.x, endOffsetRight.y);

			// fill with stroke color
			leftPolygon.setFill(Color.DARKRED);
			rightPolygon.setFill(Color.DARKRED);

			// add to scene
			root.getChildren().addAll(leftPolygon, rightPolygon);
		}
	}

	@Override
	public void start(Stage primaryStage) throws Exception {
		Pane root = new Pane();
		Scene scene = new Scene(root, 400, 400);

		addCurves(root, 100.5, false);
		addCurves(root, 200.5, true);

		primaryStage.setScene(scene);
		primaryStage.show();
	}

}


Re: [GEF4] Calculation of stroke outline in model [message #1735279 is a reply to message #1735200] Thu, 16 June 2016 22:02 Go to previous messageGo to next message
Colin Sharples is currently offline Colin SharplesFriend
Messages: 90
Registered: July 2009
Location: Wellington, New Zealand
Member

Ah, so that's what a hodograph is used for, I wondered about that Smile

Thanks Matthias, that's really cool. Actually, the shapes I'm joining together are constructed from QuadraticCurves rather than lines, so I have modified your example to create a PolyBezier from two new curves using the calculated offsets - I also offset the curve's control point in a similar manner to the start and end offsets. That produces a really beautiful effect, just what I was looking for. GEF4 rocks!

package nz.co.ctg.mote.client.test;

import org.eclipse.gef4.fx.nodes.GeometryNode;
import org.eclipse.gef4.fx.utils.Geometry2Shape;
import org.eclipse.gef4.geometry.euclidean.Vector;
import org.eclipse.gef4.geometry.planar.BezierCurve;
import org.eclipse.gef4.geometry.planar.Line;
import org.eclipse.gef4.geometry.planar.Point;
import org.eclipse.gef4.geometry.planar.PolyBezier;
import org.eclipse.gef4.geometry.planar.QuadraticCurve;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Path;
import javafx.scene.shape.StrokeLineCap;
import javafx.scene.shape.StrokeLineJoin;
import javafx.scene.shape.StrokeType;
import javafx.stage.Stage;

public class QuadCurveOffsetSnippet extends Application {

  public static void main(String[] args) {
    launch();
  }

  protected void addCurves(Pane root, double y, boolean transition) {
    // create curves
    QuadraticCurve firstCurve = new QuadraticCurve(100, y, 150, y - 50, 200, y);
    BezierCurve secondCurve = new QuadraticCurve(200, y, 250, y + 50, 300, y);

    // create paths
    Path firstPath = Geometry2Shape.toPath(firstCurve.toPath());
    Path secondPath = Geometry2Shape.toPath(secondCurve.toPath());


    // strokes
    double firstStrokeWidth = 16;
    double secondStrokeWidth = 48;

    // configure strokes
    firstPath.setStrokeType(StrokeType.CENTERED);
    firstPath.setStrokeWidth(firstStrokeWidth);
    firstPath.setStrokeLineCap(StrokeLineCap.BUTT);
    firstPath.setStrokeLineJoin(StrokeLineJoin.BEVEL);
    firstPath.setStroke(Color.BLUE);

    secondPath.setStrokeType(StrokeType.CENTERED);
    secondPath.setStrokeWidth(secondStrokeWidth);
    secondPath.setStrokeLineCap(StrokeLineCap.BUTT);
    secondPath.setStrokeLineJoin(StrokeLineJoin.BEVEL);
    secondPath.setStroke(Color.BLUE);

    // compute polygons to transition from the thinner/first curve to
    // the thicker/second curve
    if (transition) {
      // compute start point and end point
      Point startPoint = firstCurve.get(0);
      Point endPoint = firstCurve.get(1);
      Point controlPoint = firstCurve.getCtrl();

      // compute curve normals for start point and end point
      Vector startNormal = new Vector(firstCurve.getDerivative().get(0)).getOrthogonalComplement().getNormalized();
      Vector endNormal = new Vector(firstCurve.getDerivative().get(1)).getOrthogonalComplement().getNormalized();

      // compute left and right offset points for the start point (first
      // curve stroke width)
      Point startOffsetLeft = startPoint.getTranslated(startNormal.getMultiplied(firstStrokeWidth / 2).toPoint());
      Point startOffsetRight = startPoint.getTranslated(startNormal.getMultiplied(firstStrokeWidth / 2).toPoint().getNegated());

      // compute left and right offset points for the quadratic curve control point
      Point ctrlOffsetLeft = controlPoint.getTranslated(startNormal.getMultiplied(firstStrokeWidth / 2).toPoint());
      Point ctrlOffsetRight = controlPoint.getTranslated(startNormal.getMultiplied(firstStrokeWidth / 2).toPoint().getNegated());

      // compute left and right offset points for the end point (second
      // curve stroke width)
      Point endOffsetLeft = endPoint.getTranslated(endNormal.getMultiplied(secondStrokeWidth / 2).toPoint());
      Point endOffsetRight = endPoint.getTranslated(endNormal.getMultiplied(secondStrokeWidth / 2).toPoint().getNegated());

      // compute left and right curves for a smooth transition between
      // the stroke widths
      BezierCurve left = new QuadraticCurve(startOffsetLeft, ctrlOffsetLeft, endOffsetLeft);
      BezierCurve cap = new Line(endOffsetLeft, endOffsetRight);
      BezierCurve right = new QuadraticCurve(endOffsetRight, ctrlOffsetRight, startOffsetRight);
      PolyBezier cone = new PolyBezier(left, cap, right);

      // add to scene
      GeometryNode<PolyBezier> coneNode = new GeometryNode<PolyBezier>(cone);
      coneNode.setFill(Color.BLUE);
      coneNode.setStroke(Color.BLUE);
      coneNode.setStrokeLineCap(StrokeLineCap.BUTT);
      coneNode.setStrokeLineJoin(StrokeLineJoin.BEVEL);

      root.getChildren().add(coneNode);
    }
    // add to scene
    root.getChildren().addAll(firstPath, secondPath);
  }

  @Override
  public void start(Stage primaryStage) throws Exception {
    Pane root = new Pane();
    Scene scene = new Scene(root, 400, 400);

    addCurves(root, 100.5, false);
    addCurves(root, 200.5, true);

    primaryStage.setScene(scene);
    primaryStage.show();
  }

}


Colin Sharples
CTG Games Ltd
Wellington, New Zealand
Re: [GEF4] Calculation of stroke outline in model [message #1735282 is a reply to message #1735279] Thu, 16 June 2016 22:55 Go to previous messageGo to next message
Colin Sharples is currently offline Colin SharplesFriend
Messages: 90
Registered: July 2009
Location: Wellington, New Zealand
Member

Oops, just realized that the control point offset would be more accurate if it was using the derivative at the point of the curve closest to where the control point is, so when calculating the control offsets I changed it to:

      Point ctrlOffset = firstCurve.getProjection(firstCurve.getCtrl());
      Vector ctrlNormal = new Vector(firstCurve.getDerivative().get(firstCurve.getParameterAt(ctrlOffset))).getOrthogonalComplement().getNormalized();
      Point ctrlOffsetLeft = controlPoint.getTranslated(ctrlNormal.getMultiplied(firstStrokeWidth / 2).toPoint());
      Point ctrlOffsetRight = controlPoint.getTranslated(ctrlNormal.getMultiplied(firstStrokeWidth / 2).toPoint().getNegated());


Even that is not absolutely accurate - the amount by which the control point should be offset is proportional to how bendy that curve is, but my maths isn't good enough to figure out what that relationship is - but it's definitely something to do with the distance between the control point and the projection of the control point on the curve.


Colin Sharples
CTG Games Ltd
Wellington, New Zealand
Re: [GEF4] Calculation of stroke outline in model [message #1735302 is a reply to message #1735282] Fri, 17 June 2016 08:32 Go to previous messageGo to next message
Matthias Wienand is currently offline Matthias WienandFriend
Messages: 163
Registered: March 2015
Senior Member
Hey that looks cool indeed!

I manipulated the code further to accurately align the shapes:

- Compute control points for cones so that C1 continuity is achieved.
- Set stroke to transparent for the cones to properly align the shapes.

		if (transition) {
			// compute start point and end point of the transition curve
			Point startPoint = firstCurve.get(0);
			Point endPoint = firstCurve.get(1);

			// compute derivatives for start/end
			BezierCurve derivative = firstCurve.getDerivative();
			Vector startDirection = new Vector(derivative.get(0));
			Vector endDirection = new Vector(derivative.get(1));

			// compute curve normals for start point and end point
			Vector startNormal = startDirection.getOrthogonalComplement()
					.getNormalized();
			Vector endNormal = endDirection.getOrthogonalComplement()
					.getNormalized();

			// compute left and right offset points for the start point (first
			// curve stroke width)
			Point startOffsetLeft = startPoint.getTranslated(
					startNormal.getMultiplied(firstStrokeWidth / 2).toPoint());
			Point startOffsetRight = startPoint.getTranslated(
					startNormal.getMultiplied(firstStrokeWidth / 2).toPoint()
							.getNegated());

			// compute left and right offset points for the end point (second
			// curve stroke width)
			Point endOffsetLeft = endPoint.getTranslated(
					endNormal.getMultiplied(secondStrokeWidth / 2).toPoint());
			Point endOffsetRight = endPoint.getTranslated(
					endNormal.getMultiplied(secondStrokeWidth / 2).toPoint()
							.getNegated());

			// compute control points by intersecting the derivatives through
			// the offsetted start/end points to achieve C1 continuity
			Straight s1Left = new Straight(new Vector(startOffsetLeft),
					startDirection);
			Straight s2Left = new Straight(new Vector(endOffsetLeft),
					endDirection);
			Vector ctrlOffsetLeft = s1Left.getIntersection(s2Left);
			if (ctrlOffsetLeft == null) {
				ctrlOffsetLeft = new Vector(endOffsetLeft);
			}

			Straight s1Right = new Straight(new Vector(startOffsetRight),
					startDirection);
			Straight s2Right = new Straight(new Vector(endOffsetRight),
					endDirection);
			Vector ctrlOffsetRight = s1Right.getIntersection(s2Right);
			if (ctrlOffsetRight == null) {
				ctrlOffsetRight = new Vector(endOffsetRight);
			}

			// compute left and right curves for a smooth transition between
			// the stroke widths
			org.eclipse.gef4.geometry.planar.Path leftCone = new org.eclipse.gef4.geometry.planar.Path(
					new Segment(Segment.MOVE_TO, startOffsetLeft),
					new Segment(Segment.QUAD_TO, ctrlOffsetLeft.toPoint(),
							endOffsetLeft),
					new Segment(Segment.LINE_TO, endPoint),
					new Segment(Segment.QUAD_TO, firstCurve.getCtrl(),
							startPoint),
					new Segment(Segment.CLOSE));
			org.eclipse.gef4.geometry.planar.Path rightCone = new org.eclipse.gef4.geometry.planar.Path(
					new Segment(Segment.MOVE_TO, endOffsetRight),
					new Segment(Segment.QUAD_TO, ctrlOffsetRight.toPoint(),
							startOffsetRight),
					new Segment(Segment.LINE_TO, startPoint),
					new Segment(Segment.QUAD_TO, firstCurve.getCtrl(),
							endPoint),
					new Segment(Segment.CLOSE));
			Line capLine = new Line(endOffsetLeft, endOffsetRight);

			// create corresponding JavaFX shapes
			Path leftConeShape = Geometry2Shape.toPath(leftCone);
			Path capShape = Geometry2Shape.toPath(capLine.toPath());
			Path rightConeShape = Geometry2Shape.toPath(rightCone);

			// style the shapes for accurate alignment
			leftConeShape.setStroke(Color.TRANSPARENT);
			leftConeShape.setFill(Color.BLUE);
			rightConeShape.setStroke(Color.TRANSPARENT);
			rightConeShape.setFill(Color.BLUE);
			capShape.setStroke(Color.BLUE);
			capShape.setStrokeLineCap(StrokeLineCap.BUTT);

			// add to scene
			root.getChildren().addAll(leftConeShape, capShape, rightConeShape);
		}


Screenshot:
index.php/fa/26197/0/

Best regards,
Matthias

[Updated on: Fri, 17 June 2016 08:34]

Report message to a moderator

Re: [GEF4] Calculation of stroke outline in model [message #1735380 is a reply to message #1735302] Sat, 18 June 2016 00:00 Go to previous message
Colin Sharples is currently offline Colin SharplesFriend
Messages: 90
Registered: July 2009
Location: Wellington, New Zealand
Member

Awesome, that looks great. I also tried the same approach on calculating the fat outline - it's pretty good except for the degenerate case where the control point is actually on the curve (i.e. it's really a straight line). In that case I just calculated the offset as the midpoint between the two curve offset points. Apart from that, it gives a really good fat outline - on very wide lines with sharp curves, it overestimates the control points just a bit, but otherwise a very good fit, and with excellent performance.

Colin Sharples
CTG Games Ltd
Wellington, New Zealand
Previous Topic:[GEF4] Possible memory leak in SelectionBehavior.updateHandles()
Next Topic:[GEF4] Adapter binding warning messages
Goto Forum:
  


Current Time: Mon Sep 24 20:54:42 GMT 2018

Powered by FUDForum. Page generated in 0.02382 seconds
.:: Contact :: Home ::.

Powered by: FUDforum 3.0.2.
Copyright ©2001-2010 FUDforum Bulletin Board Software

Back to the top