-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathArrowNodeStyleAngleHandle.java
424 lines (375 loc) · 17 KB
/
ArrowNodeStyleAngleHandle.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
/****************************************************************************
**
** This demo file is part of yFiles for JavaFX 3.6.
**
** Copyright (c) 2000-2023 by yWorks GmbH, Vor dem Kreuzberg 28,
** 72070 Tuebingen, Germany. All rights reserved.
**
** yFiles demo files exhibit yFiles for JavaFX functionalities. Any redistribution
** of demo files in source code or binary form, with or without
** modification, is not permitted.
**
** Owners of a valid software license for a yFiles for JavaFX version that this
** demo is shipped with are allowed to use the demo source code as basis
** for their own yFiles for JavaFX powered applications. Use of such programs is
** governed by the rights and conditions as set out in the yFiles for JavaFX
** license agreement.
**
** THIS SOFTWARE IS PROVIDED ''AS IS'' AND ANY EXPRESS OR IMPLIED
** WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
** MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
** NO EVENT SHALL yWorks BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
** TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
** PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
** LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
** NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**
***************************************************************************/
package style.arrownodestyle;
import com.yworks.yfiles.geometry.IPoint;
import com.yworks.yfiles.geometry.IRectangle;
import com.yworks.yfiles.geometry.PointD;
import com.yworks.yfiles.graph.INode;
import com.yworks.yfiles.graph.styles.ArrowNodeDirection;
import com.yworks.yfiles.graph.styles.ArrowNodeStyle;
import com.yworks.yfiles.graph.styles.ArrowStyleShape;
import com.yworks.yfiles.view.ICanvasObject;
import com.yworks.yfiles.view.ICanvasObjectDescriptor;
import com.yworks.yfiles.view.IRenderContext;
import com.yworks.yfiles.view.IVisualCreator;
import com.yworks.yfiles.view.Pen;
import com.yworks.yfiles.view.input.ClickEventArgs;
import com.yworks.yfiles.view.input.HandleTypes;
import com.yworks.yfiles.view.input.IHandle;
import com.yworks.yfiles.view.input.IInputModeContext;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.paint.Color;
import javafx.scene.shape.Line;
/**
* An {@link IHandle} for nodes with a {@link ArrowNodeStyle} to change the
* {@link ArrowNodeStyle#getAngle()} interactively.
*/
public class ArrowNodeStyleAngleHandle implements IHandle, IPoint, IVisualCreator {
private final double HandleOffset = 15.0;
private final INode node;
private final Runnable angleChanged;
private final ArrowNodeStyle style;
// x and y factors that are used to translate the mouse delta to the relative handle movement
private double xFactor;
private double yFactor;
private double arrowSideWidth;
private double initialAngle;
private double initialHandleOffset;
private double handleOffsetToHeadLengthForPositiveAngles;
private double handleOffsetToHeadLengthForNegativeAngles;
// minimum and maximum handle offsets that result in the minimum and maximum allowed angles
private double handleOffsetForMinAngle;
private double handleOffsetForMaxAngle;
private ICanvasObject angleLineCanvasObject;
/**
* Creates a new instance for the given node.
* @param node The node whose style shall be changed.
* @param angleChanged An action that is called when the handle has been moved.
*/
public ArrowNodeStyleAngleHandle(INode node, Runnable angleChanged) {
this.node = node;
this.angleChanged = angleChanged;
this.style = (ArrowNodeStyle) node.getStyle();
}
/**
* Gets a live view of the handle location.
* The handle is placed with an offset to the node bounds on the line from the arrow head tip
* along the arrow blade.
*/
@Override
public IPoint getLocation() {
return this;
}
/**
* Initializes the drag gesture and adds a line from the arrow head tip along the arrow blade to
* the handle to the view.
*
* @param context The current input mode context.
*/
@Override
public void initializeDrag(IInputModeContext context) {
IRectangle nodeLayout = node.getLayout();
boolean isParallelogram = style.getShape() == ArrowStyleShape.PARALLELOGRAM;
boolean isTrapezoid = style.getShape() == ArrowStyleShape.TRAPEZOID;
// negative angles are only allowed for trapezoids, parallelograms or arrows with shaft ratio = 1
boolean negativeAngleAllowed = style.getShaftRatio() >= 1 || isTrapezoid || isParallelogram;
arrowSideWidth = getArrowSideWidth(node.getLayout(), style);
// calculate the factors to convert the handle offset to the new length of the arrowhead
// note that for positive angles the angle rotates around the arrow tip while for negative ones it rotates around
// a node corner
handleOffsetToHeadLengthForPositiveAngles = arrowSideWidth / (HandleOffset + arrowSideWidth);
handleOffsetToHeadLengthForNegativeAngles = arrowSideWidth / HandleOffset;
initialAngle = getClampedAngle(style);
initialHandleOffset = getArrowHeadLength(node.getLayout(), style) / (initialAngle < 0
? -handleOffsetToHeadLengthForNegativeAngles
: handleOffsetToHeadLengthForPositiveAngles);
// the maximum length of the arrow head depends on the direction and shape
double maxHeadLength = getMaxArrowHeadLength(nodeLayout, style);
// calculate handle offsets for the current node size that correspond to the minimum and maximum allowed angle
handleOffsetForMaxAngle = maxHeadLength / handleOffsetToHeadLengthForPositiveAngles;
handleOffsetForMinAngle = negativeAngleAllowed ? -maxHeadLength / handleOffsetToHeadLengthForNegativeAngles : 0;
// xFactor and yFactor are used later to translate the mouse delta to the relative handle movement
ArrowNodeDirection direction = style.getDirection();
xFactor = direction == ArrowNodeDirection.LEFT ? 1 : direction == ArrowNodeDirection.RIGHT ? -1 : 0;
yFactor = direction == ArrowNodeDirection.UP ? 1 : direction == ArrowNodeDirection.DOWN ? -1 : 0;
if (isParallelogram) {
// for parallelograms the slope of the arrow blade is in the opposite direction
xFactor *= -1;
yFactor *= -1;
}
// add a line from the arrow tip along the arrow blade to the handle location to the view
// this line is created and updated in the CreateVisual and UpdateVisual methods
angleLineCanvasObject =
context.getCanvasControl().getInputModeGroup().addChild(this, ICanvasObjectDescriptor.ALWAYS_DIRTY_INSTANCE);
}
/**
* Calculates the new angle depending on the new mouse location and updates the node style and
* angle visualization.
* @param context The current input mode context.
* @param originalLocation The original handle location.
* @param newLocation The new mouse location.
*/
@Override
public void handleMove(IInputModeContext context, PointD originalLocation, PointD newLocation) {
// determine delta of the handle
double handleDelta =
xFactor * (newLocation.getX() - originalLocation.getX()) +
yFactor * (newLocation.getY() - originalLocation.getY());
// determine handle offset from the location that corresponds to angle = 0
double handleOffset = initialHandleOffset + handleDelta;
// ... and clamp to valid values
handleOffset = Math.max(handleOffsetForMinAngle, Math.min(handleOffset, handleOffsetForMaxAngle));
// calculate the new arrow head length based on the offset of the handle
double newHeadLength = handleOffset < 0
? handleOffset * handleOffsetToHeadLengthForNegativeAngles
: handleOffset * handleOffsetToHeadLengthForPositiveAngles;
double newAngle = Math.atan(newHeadLength / arrowSideWidth);
style.setAngle(newAngle);
if (angleChanged != null) {
angleChanged.run();
}
}
/**
* Resets the initial angle and removes the angle visualization.
*/
@Override
public void cancelDrag(IInputModeContext context, PointD originalLocation) {
style.setAngle(initialAngle);
angleLineCanvasObject.remove();
}
/**
* Sets the angle for the new location, removes the angle visualization and triggers the angleChanged callback.
*/
@Override
public void dragFinished(IInputModeContext context, PointD originalLocation, PointD newLocation) {
handleMove(context, originalLocation, newLocation);
angleLineCanvasObject.remove();
}
@Override
public HandleTypes getType() {
return HandleTypes.ROTATE;
}
@Override
public Cursor getCursor() {
return Cursor.CROSSHAIR;
}
/**
* Ignore clicks.
*/
@Override
public void handleClick(ClickEventArgs eventArgs) {
// ignore clicks
}
@Override
public double getX() {
switch (style.getDirection()) {
case RIGHT: {
double offset = calculateHandleInDirectionOffset();
return style.getShape() == ArrowStyleShape.PARALLELOGRAM
? node.getLayout().getX() + offset
: node.getLayout().getMaxX() - offset;
}
case UP:
return node.getLayout().getX() - HandleOffset;
case LEFT: {
double offset = calculateHandleInDirectionOffset();
return style.getShape() == ArrowStyleShape.PARALLELOGRAM
? node.getLayout().getMaxX() - offset
: node.getLayout().getX() + offset;
}
case DOWN:
return style.getShape() == ArrowStyleShape.TRAPEZOID
? node.getLayout().getMaxX() + HandleOffset
: node.getLayout().getX() - HandleOffset;
}
return 0;
}
@Override
public double getY() {
switch (style.getDirection()) {
case RIGHT:
return node.getLayout().getY() - HandleOffset;
case UP: {
double offset = calculateHandleInDirectionOffset();
return style.getShape() == ArrowStyleShape.PARALLELOGRAM
? node.getLayout().getMaxY() - offset
: node.getLayout().getY() + offset;
}
case LEFT:
return style.getShape() == ArrowStyleShape.TRAPEZOID
? node.getLayout().getMaxY() + HandleOffset
: node.getLayout().getY() - HandleOffset;
case DOWN: {
double offset = calculateHandleInDirectionOffset();
return style.getShape() == ArrowStyleShape.PARALLELOGRAM
? node.getLayout().getY() + offset
: node.getLayout().getMaxY() - offset;
}
}
return 0;
}
/**
* Returns the width of one arrow side for the given node layout and style.
* @param nodeLayout The node layout whose size shall be used.
* @param style The style whose shape and direction shall be used.
* @return The width of one arrow side for the given node layout and style.
*/
private static double getArrowSideWidth(IRectangle nodeLayout, ArrowNodeStyle style) {
ArrowStyleShape shape = style.getShape();
boolean isParallelogram = shape == ArrowStyleShape.PARALLELOGRAM;
boolean isTrapezoid = shape == ArrowStyleShape.TRAPEZOID;
double againstDirectionSize =
style.getDirection() == ArrowNodeDirection.UP ||
style.getDirection() == ArrowNodeDirection.DOWN
? nodeLayout.getWidth()
: nodeLayout.getHeight();
// for parallelogram and trapezoid, one side of the arrow fills the full againstDirectionSize
return againstDirectionSize * (isParallelogram || isTrapezoid ? 1 : 0.5);
}
/**
* Clamps the {@link ArrowNodeStyle#getAngle()} of the given style to a valid value.
* A valid angle is less then <c>π / 2</c>.
* For styles using {@link ArrowStyleShape#PARALLELOGRAM} or {@link ArrowStyleShape#TRAPEZOID}
* shape or having {@link ArrowNodeStyle#getShaftRatio()} <c>1</c>, the angle also has to be
* bigger then <c>-π / 2</c>, otherwise it has to be <c>>= 0</c>
* @param style The style to return the clamped angle for.
* @return The angle of the style clamped to a valid value.
*/
private static double getClampedAngle(ArrowNodeStyle style) {
// clamp angle to be <= Math.PI/2
double angle = Math.min(Math.PI * 0.5, style.getAngle());
if (angle < 0) {
// if a negative angle is set, check if the effective shaft ratio is 1
if (style.getShaftRatio() >= 1 ||
style.getShape() == ArrowStyleShape.PARALLELOGRAM ||
style.getShape() == ArrowStyleShape.TRAPEZOID) {
// negative angle allowed but has to be > -Math.PI/2
angle = Math.max(-Math.PI * 0.5, angle);
} else {
angle = 0;
}
}
return angle;
}
/**
* Calculates the length of the arrow head for the given node layout and style.
* @param nodeLayout The layout of the node.
* @param style The style whose shape and angle shall be considered.
* @return The length of the arrow head for the given style and node layout.
*/
static double getArrowHeadLength(IRectangle nodeLayout, ArrowNodeStyle style) {
double maxArrowLength = getMaxArrowHeadLength(nodeLayout, style);
double arrowSideWidth = getArrowSideWidth(nodeLayout, style);
double angle = getClampedAngle(style);
double maxHeadLength = arrowSideWidth * Math.tan(Math.abs(angle));
return Math.min(maxHeadLength, maxArrowLength);
}
/**
* Returns the maximum possible arrow head length for the given node layout and style.
* @param nodeLayout The node layout whose size shall be used.
* @param style The style whose shape and direction shall be used.
* @return The maximum possible arrow head length for the given node layout and style.
*/
private static double getMaxArrowHeadLength(IRectangle nodeLayout, ArrowNodeStyle style) {
ArrowStyleShape shape = style.getShape();
boolean isTrapezoid = shape == ArrowStyleShape.TRAPEZOID;
boolean isDoubleArrow = shape == ArrowStyleShape.DOUBLE_ARROW;
double inDirectionSize =
style.getDirection() == ArrowNodeDirection.UP ||
style.getDirection() == ArrowNodeDirection.DOWN
? nodeLayout.getHeight()
: nodeLayout.getWidth();
// for double arrow and trapezoid the arrow may only fill half the inDirectionSize
double maxArrowLength = inDirectionSize * (isDoubleArrow || isTrapezoid ? 0.5 : 1);
return maxArrowLength;
}
/**
* Calculates the offset of the current handle location to the location corresponding to an angle of 0.
* @return The offset of the current handle location to the location corresponding to an angle of 0.
*/
private double calculateHandleInDirectionOffset() {
double headLength = getArrowHeadLength(node.getLayout(), style);
double arrowSideWidth = getArrowSideWidth(node.getLayout(), style);
double scaledHeadLength = headLength * (HandleOffset + arrowSideWidth) / (arrowSideWidth);
double angle = getClampedAngle(style);
double offset = angle >= 0 ? scaledHeadLength : (headLength - scaledHeadLength);
return offset;
}
@Override
public Node createVisual(IRenderContext context) {
return updateVisual(context, new Line());
}
@Override
public Node updateVisual(IRenderContext context, Node oldVisual) {
Line line = (Line) oldVisual;
// line shall point from handle to arrow tip
// synchronize first line point with handle location
double startX = getLocation().getX();
double startY = getLocation().getY();
// synchronize second line point with arrow tip
IRectangle nodeLayout = node.getLayout();
boolean isParallelogram = style.getShape() == ArrowStyleShape.PARALLELOGRAM;
boolean isTrapezoid = style.getShape() == ArrowStyleShape.TRAPEZOID;
double againstDirectionRatio = isParallelogram || isTrapezoid ? 1 : 0.5;
double endX = 0;
double endY = 0;
// for negative angles, the arrow tip is moved
double arrowTipOffset = style.getAngle() < 0 ? getArrowHeadLength(node.getLayout(), style) : 0;
switch (style.getDirection()) {
case RIGHT: {
endX = isParallelogram ? nodeLayout.getX() + arrowTipOffset : nodeLayout.getMaxX() - arrowTipOffset;
endY = nodeLayout.getY() + nodeLayout.getHeight() * againstDirectionRatio;
break;
}
case LEFT: {
endX = isParallelogram ? nodeLayout.getMaxX() - arrowTipOffset : nodeLayout.getX() + arrowTipOffset;
endY = isTrapezoid ? nodeLayout.getY() : nodeLayout.getY() + nodeLayout.getHeight() * againstDirectionRatio;
break;
}
case UP: {
endX = nodeLayout.getX() + nodeLayout.getWidth() * againstDirectionRatio;
endY = isParallelogram ? nodeLayout.getMaxY() - arrowTipOffset : nodeLayout.getY() + arrowTipOffset;
break;
}
case DOWN: {
endX = isTrapezoid ? nodeLayout.getX() : nodeLayout.getX() + nodeLayout.getWidth() * againstDirectionRatio;
endY = isParallelogram ? nodeLayout.getY() + arrowTipOffset : nodeLayout.getMaxY() - arrowTipOffset;
break;
}
}
line.setStartX(startX);
line.setStartY(startY);
line.setEndX(endX);
line.setEndY(endY);
new Pen(Color.GOLDENROD, (2 / context.getZoom())).styleShape(line);
return line;
}
}