Why will the rectangle be centered, when setting the .size property?
var rect = new Rectangle();
rect.center = view.center;
rect.size = new Size(100, 200);
var path = new Path.Rectangle(rect);
path.fillColor = 'red';
Why will the rectangle not be centered, when setting the .width and .height properties?
var rect = new Rectangle();
rect.center = view.center;
rect.width = 100;
rect.height = 200;
var path = new Path.Rectangle(rect);
path.fillColor = 'red';
I was curious too and took a look to source code.
That happens because updating width / height directly only changes the point where bottomRight point should be. You only tells to "grow" in X and Y direction.
On the other hand, using ".size", which is a setter
(method here)
does more than that. It re-calculates X & Y based on an internal state called "_sx" & "_sy" (which will have 0.5 - center - as value by default. More here).
The key is here:
...
if (sx) {
this.x += (this.width - w) * sx;
}
if (sy) {
this.y += (this.height - h) * sy;
}
...
When "sx" and/or "sy" are 0.5 the rectangle centers (by default).
Related
I have an resizable ImageView and some parts of the image should be clickable.
My idea was to create a GridPane that would act as an overlay and position several transparent buttons inside it so that when a user clicks some part of the image, he would actually trigger a hidden button over the image:
<!-- parent that resizes from time to time -->
<StackPane>
<StackPane>
<!-- background image, preserves ration -->
<ImageView/>
<!-- overlay with transparent buttons -->
<!-- should be positionion exactly the same as Image inside ImageView -->
<GridPane>
<!- buttons here, columns == rows -->
</GridPane>
</StackPane>
</StackPane>
Code looks likes this:
StackPane stackPane_wrapper = new StackPane();
stackPane_wrapper.setAlignment(Pos.CENTER);
//menu wheel
WrappedImageView imageView_wheel = new WrappedImageView();
imageView_wheel.setImage(new Image("images/menu-wheel.png"));
imageView_wheel.setPreserveRatio(true);
stackPane_wrapper.getChildren().add(imageView_wheel);
GridPane gridPane_overlay = new GridPane();
stackPane_wrapper.getChildren().add(gridPane_overlay);
int size = 20;
for (int i = 0; i < size; ++i)
{
ColumnConstraints columnConstraints = new ColumnConstraints();
columnConstraints.setPercentWidth(100.0d / size);
gridPane_overlay.getColumnConstraints().add(columnConstraints);
RowConstraints rowConstraints = new RowConstraints();
rowConstraints.setPercentHeight(100.0d / size);
gridPane_overlay.getRowConstraints().add(rowConstraints);
}
Button button = new Button();
button.setText("test");
//... set style to make this button transparent
gridPane_overlay.add(button, 3, 5, 2, 4);
My problem is that I am unable to place gridPane_overlay on the same position as the Image inside ImageView. Image inside imageView_wheel keeps resizing and changing it's position, which is totally fine, but I do not know how assign it's size and position to gridPane_overlay.
I've tries adding listeners to various x/y and width/height properties and was able to achieve some results but it stopped working one the stage became maximized and kept setting completely invalid coordinates.
Update:
Seems like getParentInParent.getWidth() and getParentInParent.getHeight() return correct size of the Image inside WrappedImageView, now I need to get it's position and assign both size and position to the grid.
Update 2:
Based on the comments, I've made the following solution;
/**
*
* #param bounds bounds of image - result of {#link WrappedImageView#localToScene(Bounds)} from {#link WrappedImageView#getBoundsInLocal()}
* #param x click X - {#link MouseEvent#getSceneX()} minus {#link Bounds#getMinX()}
* #param y click Y - {#link MouseEvent#getSceneY()} minus {#link Bounds#getMinY()}
* #return
*/
private int determineClick(Bounds bounds, double x, double y)
{
double centerX = bounds.getWidth() / 2;
double centerY = bounds.getHeight() / 2;
double angle = Math.toDegrees(Math.atan2(x - centerX, y - centerY));
Point2D center = new Point2D(centerX, centerY);
Point2D click = new Point2D(x, y);
double distance = center.distance(click);
double diameter = centerX;
boolean isInner = distance < diameter * 0.6;
//-90 -> -135
if (angle <= -90 && angle > -135)
{
if (isInner)
{
return 10;
}
return 0;
}
//-135 -> -180/180
if (angle <= -135 && angle > -180)
{
if (isInner)
{
return 11;
}
return 1;
}
//-180/180 -> 135
if (angle <= 180 && angle > 135)
{
if (isInner)
{
return 12;
}
return 2;
}
//135 -> 90
if (angle <= 135 && angle > 90)
{
if (isInner)
{
return 13;
}
return 3;
}
//90 -> 45
if (angle <= 90 && angle > 45)
{
if (isInner)
{
return 14;
}
return 4;
}
//45 -> -0/0
if (angle <= 45 && angle > 0)
{
if (isInner)
{
return 15;
}
return 5;
}
//-0/0 -> -45
if (angle <= 0 && angle > -45)
{
if (isInner)
{
return 16;
}
return 6;
}
//-45 -> -90
if (angle <= -45 && angle > -90)
{
if (isInner)
{
return 17;
}
return 7;
}
throw new RuntimeException("Unable to determine point coordinates");
}
I am also thinking as exactly what #Jai mentioned in the comment. You can include a mouse clicked event on the imageview itself to determine at what point of its bounds is clicked. And then you can determine the range of click position and perform your logic.
Something like...
imageView_wheel.setOnMouseClicked(e -> {
Bounds bounds = imageView_wheel.localToScene(imageView_wheel.getBoundsInLocal());
double x = e.getSceneX()-bounds.getMinX();
double y = e.getSceneY()-bounds.getMinY();
System.out.println("Clicked at :: "+x+","+y);
// Now you know at what point in the image bounds is clicked, compute your logic as per your needs..
});
I have a sample 3D application (built by taking reference from the Javafx sample 3DViewer) which has a table created by laying out Boxes and Panes:
The table is centered wrt (0,0,0) coordinates and camera is at -z position initially.
It has the zoom-in/out based on the camera z position from the object.
On zooming in/out the object's boundsInParent increases/decreases i.e. area of the face increases/decreases. So the idea is to put more text when we have more area (always confining within the face) and lesser text or no text when the face area is too less. I am able to to do that using this node hierarchy:
and resizing the Pane (and managing the vBox and number of texts in it) as per Box on each zoom-in/out.
Now the issue is that table boundsInParent is giving incorrect results (table image showing the boundingBox off at the top) whenever a text is added to the vBox for the first time only. On further zooming-in/out gives correct boundingBox and does not go off.
Below is the UIpane3D class:
public class UIPane3D extends Pane
{
VBox textPane;
ArrayList<String> infoTextKeys = new ArrayList<>();
ArrayList<Text> infoTextValues = new ArrayList<>();
Rectangle bgCanvasRect = null;
final double fontSize = 16.0;
public UIPane3D() {
setMouseTransparent(true);
textPane = new VBox(2.0)
}
public void updateContent() {
textPane.getChildren().clear();
getChildren().clear();
for (Text textNode : infoTextValues) {
textPane.getChildren().add(textNode);
textPane.autosize();
if (textPane.getHeight() > getHeight()) {
textPane.getChildren().remove(textNode);
textPane.autosize();
break;
}
}
textPane.setTranslateY(getHeight() / 2 - textPane.getHeight() / 2.0);
bgCanvasRect = new Rectangle(getWidth(), getHeight());
bgCanvasRect.setFill(Color.web(Color.BURLYWOOD.toString(), 0.10));
bgCanvasRect.setVisible(true);
getChildren().addAll(bgCanvasRect, textPane);
}
public void resetInfoTextMap()
{
if (infoTextKeys != null || infoTextValues != null)
{
try
{
infoTextKeys.clear();
infoTextValues.clear();
} catch (Exception e){e.printStackTrace();}
}
}
public void updateInfoTextMap(String pKey, String pValue)
{
int index = -1;
boolean objectFound = false;
for (String string : infoTextKeys)
{
index++;
if(string.equals(pKey))
{
objectFound = true;
break;
}
}
if(objectFound)
{
infoTextValues.get(index).setText(pValue.toUpperCase());
}
else
{
if (pValue != null)
{
Text textNode = new Text(pValue.toUpperCase());
textNode.setFont(Font.font("Consolas", FontWeight.BLACK, FontPosture.REGULAR, fontSize));
textNode.wrappingWidthProperty().bind(widthProperty());
textNode.setTextAlignment(TextAlignment.CENTER);
infoTextKeys.add(pKey);
infoTextValues.add(textNode);
}
}
}
}
The code which get called at the last after all the manipulations:
public void refreshBoundingBox()
{
if(boundingBox != null)
{
root3D.getChildren().remove(boundingBox);
}
PhongMaterial blueMaterial = new PhongMaterial();
blueMaterial.setDiffuseColor(Color.web(Color.CRIMSON.toString(), 0.25));
Bounds tableBounds = table.getBoundsInParent();
boundingBox = new Box(tableBounds.getWidth(), tableBounds.getHeight(), tableBounds.getDepth());
boundingBox.setMaterial(blueMaterial);
boundingBox.setTranslateX(tableBounds.getMinX() + tableBounds.getWidth()/2.0);
boundingBox.setTranslateY(tableBounds.getMinY() + tableBounds.getHeight()/2.0);
boundingBox.setTranslateZ(tableBounds.getMinZ() + tableBounds.getDepth()/2.0);
boundingBox.setMouseTransparent(true);
root3D.getChildren().add(boundingBox);
}
Two things:
The table3D's boundsInParent is not updated properly when texts are added for the first time.
What would be the right way of putting texts on 3D nodes? I am having to manipulate a whole lot to bring the texts as required.
Sharing code here.
For the first question, about the "jump" that can be noticed just when after scrolling a new text item is laid out:
After digging into the code, I noticed that the UIPane3D has a VBox textPane that contains the different Text nodes. Every time updateContent is called, it tries to add a text node, but it checks that the vbox's height is always lower than the pane's height, or else the node will be removed:
for (Text textNode : infoTextValues) {
textPane.getChildren().add(textNode);
textPane.autosize();
if (textPane.getHeight() > getHeight()) {
textPane.getChildren().remove(textNode);
textPane.autosize();
break;
}
}
While this is basically correct, when you add a node to the scene, you can't get textPane.getHeight() immediately, as it hasn't been laid out yet, and you have to wait until the next pulse. This is why the next time you scroll, the height is correct and the bounding box is well placed.
One way to force the layout and get the correct height of the textNode is by forcing css and a layout pass:
for (Text textNode : infoTextValues) {
textPane.getChildren().add(textNode);
// force css and layout
textPane.applyCss();
textPane.layout();
textPane.autosize();
if (textPane.getHeight() > getHeight()) {
textPane.getChildren().remove(textNode);
textPane.autosize();
break;
}
}
Note that:
This method [applyCss] does not normally need to be invoked directly but may be used in conjunction with Parent.layout() to size a Node before the next pulse, or if the Scene is not in a Stage.
For the second question, about a different solution to add Text to 3D Shape.
Indeed, placing a (2D) text on top of a 3D shape is quite difficult, and requires complex maths (that are done quite nicely in the project, by the way).
There is an alternative avoiding the use of 2D nodes directly.
Precisely in a previous question, I "wrote" into an image, that later on I used as the material diffuse map of a 3D shape.
The built-in 3D Box places the same image into every face, so that wouldn't work. We can implement a 3D prism, or we can make use of the CuboidMesh node from the FXyz3D library.
Replacing the Box in UIPaneBoxGroup:
final CuboidMesh contentShape;
UIPane3D displaypane = null;
PhongMaterial shader = new PhongMaterial();
final Color pColor;
public UIPaneBoxGroup(final double pWidth, final double pHeight, final double pDepth, final Color pColor) {
contentShape = new CuboidMesh(pWidth, pHeight, pDepth);
this.pColor = pColor;
contentShape.setMaterial(shader);
getChildren().add(contentShape);
addInfoUIPane();
}
and adding the generateNet method:
private Image generateNet(String string) {
GridPane grid = new GridPane();
grid.setAlignment(Pos.CENTER);
Label label5 = new Label(string);
label5.setFont(Font.font("Consolas", FontWeight.BLACK, FontPosture.REGULAR, 40));
GridPane.setHalignment(label5, HPos.CENTER);
grid.add(label5, 3, 1);
double w = contentShape.getWidth() * 10; // more resolution
double h = contentShape.getHeight() * 10;
double d = contentShape.getDepth() * 10;
final double W = 2 * d + 2 * w;
final double H = 2 * d + h;
ColumnConstraints col1 = new ColumnConstraints();
col1.setPercentWidth(d * 100 / W);
ColumnConstraints col2 = new ColumnConstraints();
col2.setPercentWidth(w * 100 / W);
ColumnConstraints col3 = new ColumnConstraints();
col3.setPercentWidth(d * 100 / W);
ColumnConstraints col4 = new ColumnConstraints();
col4.setPercentWidth(w * 100 / W);
grid.getColumnConstraints().addAll(col1, col2, col3, col4);
RowConstraints row1 = new RowConstraints();
row1.setPercentHeight(d * 100 / H);
RowConstraints row2 = new RowConstraints();
row2.setPercentHeight(h * 100 / H);
RowConstraints row3 = new RowConstraints();
row3.setPercentHeight(d * 100 / H);
grid.getRowConstraints().addAll(row1, row2, row3);
grid.setPrefSize(W, H);
grid.setBackground(new Background(new BackgroundFill(pColor, CornerRadii.EMPTY, Insets.EMPTY)));
new Scene(grid);
return grid.snapshot(null, null);
}
Now all the 2D related code can be removed (including displaypane), and after a scrolling event get the image:
public void refreshBomUIPane() {
Image net = generateNet(displaypane.getText());
shader.setDiffuseMap(net);
}
where in UIPane3D:
public String getText() {
return infoTextKeys.stream().collect(Collectors.joining("\n"));
}
I've also removed the bounding box to get this picture:
I haven't played around with the number of text nodes that can be added to the VBox, the font size nor with an strategy to avoid generating images on every scroll: only when the text changes this should be done. So with the current approach is quite slow, but it can be improved notably as there are only three possible images for each box.
I am unsure what the markdirty() function does in QML. The QT documentation of that function does not seem clear to me.
My interpretation of this is that it allows us to track changes to a small subset of the canvas, so that any changes to that part redraws everything only in that part, using paint()
Doing requestPaint() on the other hand would be far more inefficient because it would redraw the whole canvas.
Is this correct? Some simple example codes would be quite helpful in understanding the usecase of markDirty()
That's a widely used term in programming especially GUI. Using that you can mark part of the canvas as in need of updating. So if this part is visible the render engine will fire paint(rect region) as soon as possible. In the onPaint handler you should repaint only items inside this region. requestPaint() does almost the same but for all the visible region.
Check the output in the example below:
import QtQuick 2.9
import QtQuick.Window 2.2
Window {
visible: true
width: 800
height: 800
title: qsTr("QML Test")
Canvas {
property var shapes: [20,20,220,20,20,220,220,220]
id: canvas
width: 400
height: 400
anchors.centerIn: parent
onPaint: {
console.log(region);
var ctx = getContext("2d");
ctx.beginPath();
ctx.fillStyle = Qt.rgba(Math.random(),Math.random(),Math.random(),1);
// draw 4 circles
for(var i = 0;i < canvas.shapes.length;i +=2) {
var x = canvas.shapes[i];
var y = canvas.shapes[i + 1];
var width = 160;
var height = 160;
// check the circle is inside the region
if(!( (x + width) < region.x || x > (region.x + region.width) || (y + height) < region.y || y > (region.y + region.height) ) ) {
ctx.ellipse(x, y, width, height);
}
}
ctx.fill();
}
}
Timer {
id: timer
property int step: 0
interval: 2000
repeat: true
running: true
onTriggered: {
switch(step++) {
case 0: canvas.markDirty(Qt.rect(0, 0, 200, 200)); break;
case 1: canvas.requestPaint(); break;
case 2: timer.stop(); break;
}
}
}
}
I have this snippet of ActionScript code that is supposed to resize an image to fit the sprite: (as in rescale the image not just clip it)
package
{
import flash.display.Sprite;
import flash.display.Bitmap;
import flash.events.Event;
public class Main extends Sprite
{
[Embed(source = 'img.png')]
private var TheImage:Class;
public static const TheImageWidth:Number = 1300;
public static const TheImageHeight:Number = 1300;
public function Main():void
{
if (stage) init();
else addEventListener(Event.ADDED_TO_STAGE, init);
}
private function init(e:Event = null):void
{
removeEventListener(Event.ADDED_TO_STAGE, init);
// entry point
var image:Bitmap = new TheImage();
addChild(image);
image.scaleX = width / TheImageWidth;
image.scaleY = height / TheImageHeight;
}
}
}
Why isn't it working? The image isn't getting the right dimensions.
try
image.image.content.width = TheImageWidth;
image.image.content.height = TheImageHeight;
I had to do the something like this for imagery. We had issue with using MapImage and the MapImageLayer using ArcGIS for Flex 2.5. It seems to be fix in 3.5, Anyways I had to put an image into a sprite and paint the image based on the sprites width. If you want to scale the image to the sprite's dimensions you will need to use the Matrix API and use the scale method. The values passed into this should be the sprite's width divided by the image's width assuming the image is bigger than your sprite. Do the same with the height. Here is the code I used to accomplish the task.
const point:MapPoint = geometry as MapPoint;
sprite.x = toScreenX(map, point.x);
sprite.y = toScreenY(map, point.y);
// grab left x point for upper left then for lower right
// actua
var bottomLeftX:Number = toScreenX(map, geomertyWrapper.extent.xmin);
var topRightX:Number = toScreenX(map, geomertyWrapper.extent.xmax);
var bottomLeftY:Number = toScreenY(map, geomertyWrapper.extent.ymin);
var topRightY:Number = toScreenY(map, geomertyWrapper.extent.ymax);
iconWidth = Math.abs(topRightX - bottomLeftX);
iconHeight = Math.abs(topRightY - bottomLeftY);
sprite.width = iconWidth;
sprite.height = iconHeight;
var scaleX:Number = iconWidth/bitmapData.width;
var scaleY:Number = iconHeight/bitmapData.height;
m_matrix.a = 1.0;
m_matrix.d = 1.0;
m_matrix.scale(scaleX, scaleY);
m_matrix.tx = -iconWidth / 2;
m_matrix.ty = -iconHeight / 2;
//Values needed for drawing AlertNotificationInstance
sprite.graphics.beginBitmapFill(m_bitmapData, m_matrix, false, false);
sprite.graphics.drawRect(m_matrix.tx, m_matrix.ty,
iconWidth,
iconHeight);
sprite.graphics.endFill();
Hope this helps.
Given this code:
var circle:Sprite = new Sprite();
circle.graphics.beginFill(0xFFCC00);
circle.graphics.drawCircle(0, 0, 20);
var uiComp:UIComponent = new UIComponent();
uiComp.x = 100;
uiComp.y = 100;
uiComp.measuredHeight = 0;
uiComp.measuredWidth = 0;
uiComp.addChild(circle);
addChild(uiComp);
Why does changing the width and height of uiComp not affect the Sprite? Wouldn't UIComponent provide Sprite with a Graphics object, which limits that area where Sprite can draw?
As I said in the other thread, use unscaledWidth and unscaledHeight as the bounds for drawing.