X & Y bounds are incorrect on objects nested within a Group


#1

Issue

The X and Y values of an object don’t match up with the inspector when using boundsInParent for an object or Group within a Group (on an Artboard).

Steps to reproduce

  1. Create a rectangle on an artboard and use boundsInParent to get the x and y coordinates.
  2. Group the rectangle and use boundsInParent on the rectangle to get the x and y coordinates.
  3. Group the group and use boundsInParent on the nested group and the rectangle to get the x and y coordinates.

Expected Behavior

The x and y values should be 0 on every object and group that is within a group.

Actual Behavior

The x and y values are incorrect when nested one level in a Group. I can’t identify where how the values are being calculated. On the third level down, the values are correct. It looks like localCenterPoint is also affected.

Additional information

Artboard is 120x120 and 1000px off the x and y.
Rectangles are 80x80 and 20px off the x and y.

Rectangle 1
boundsInParent:  { x: 20, y: 20, width: 80, height: 80 }
globalBounds:  { x: 1020, y: 1020, width: 80, height: 80 }
localBounds:  { x: 0, y: 0, width: 80, height: 80 }
topLeftInParent:  { x: 20, y: 20 }
localCenterPoint:  { x: 40, y: 40 }
----------------
Group 1
boundsInParent:  { x: 20, y: 20, width: 80, height: 80 }
globalBounds:  { x: 1020, y: 1020, width: 80, height: 80 }
localBounds:  { x: 192, y: 418, width: 80, height: 80 }
topLeftInParent:  { x: 20, y: 20 }
localCenterPoint:  { x: 232, y: 458 }
----------------
Rectangle 2
boundsInParent:  { x: 192, y: 418, width: 80, height: 80 }
globalBounds:  { x: 1020, y: 1020, width: 80, height: 80 }
localBounds:  { x: 0, y: 0, width: 80, height: 80 }
topLeftInParent:  { x: 192, y: 418 }
localCenterPoint:  { x: 40, y: 40 }
----------------
Group 2
boundsInParent:  { x: 192, y: 418, width: 80, height: 80 }
globalBounds:  { x: 1020, y: 1020, width: 80, height: 80 }
localBounds:  { x: 0, y: 0, width: 80, height: 80 }
topLeftInParent:  { x: 192, y: 418 }
localCenterPoint:  { x: 40, y: 40 }
----------------
Rectangle 3
boundsInParent:  { x: 0, y: 0, width: 80, height: 80 }
globalBounds:  { x: 1020, y: 1020, width: 80, height: 80 }
localBounds:  { x: 0, y: 0, width: 80, height: 80 }
topLeftInParent:  { x: 0, y: 0 }
localCenterPoint:  { x: 40, y: 40 }
----------------

For completeness, here is the function I’m calling:

function test(selection) {

    // Iterate through the selection
    selection.items.forEach(function (item) {

        console.log(item.name);
        console.log('boundsInParent: ', item.boundsInParent);
        console.log('globalBounds: ', item.globalBounds);
        console.log('localBounds: ', item.localBounds);
        console.log('topLeftInParent: ', item.topLeftInParent);
        console.log('localCenterPoint: ', item.localCenterPoint);
        console.log('----------------');

    })
}

#2

Using the same artboard’s size and position, I got these logs:

Rectangle 1

boundsInParent:  { x: 20, y: 20, width: 80, height: 80 }
globalBounds:  { x: 1020, y: 1020, width: 80, height: 80 }
localBounds:  { x: 0, y: 0, width: 80, height: 80 }
topLeftInParent:  { x: 20, y: 20 }
localCenterPoint:  { x: 40, y: 40 }

Group 1 (Rectangle 1 grouped)

boundsInParent:  { x: 20, y: 20, width: 80, height: 80 }
globalBounds:  { x: 1020, y: 1020, width: 80, height: 80 }
localBounds:  { x: 20, y: 20, width: 80, height: 80 }
topLeftInParent:  { x: 20, y: 20 }
localCenterPoint:  { x: 60, y: 60 }

it’s correct, except the group’s localCenterPoint.


#3

@PaoloBiagini After further investigation it seems that it’s objects inside duplicated groups that are inheriting their original’s offsets.

To replicate: Do as I described above and then duplicate a group (ALT + drag with mouse) and run the function again on one of the rectangles.


#4

Ok, I’ve duplicated the first group.

This is the result on the second group and the inner rectangle:

Group 2

boundsInParent:  { x: 33, y: 34, width: 80, height: 80 }
globalBounds:  { x: 1033, y: 1034, width: 80, height: 80 }
localBounds:  { x: 20, y: 20, width: 80, height: 80 }
topLeftInParent:  { x: 33, y: 34 }
localCenterPoint:  { x: 60, y: 60 }

Rectangle 2 (inside Group 2)

boundsInParent:  { x: 20, y: 20, width: 80, height: 80 }
globalBounds:  { x: 1033, y: 1034, width: 80, height: 80 }
localBounds:  { x: 0, y: 0, width: 80, height: 80 }
topLeftInParent:  { x: 20, y: 20 }
localCenterPoint:  { x: 40, y: 40 }

#5

I am able to replicate consistently


#6

Ok, reduced test case:

  1. Create a rectangle and check the boundsInParent
  2. Group it and check the rectangle bounds
  3. Move the group
  4. check the rectangle bounds

#7

I am able to replicate these inconsistencies reported here. Once an object is grouped, the object’s boundsInParent metrics become unreliable. @peterflynn would be the best person to confirm this potential bug. Could you please look at this?


#8

One note: my group contained just one rectangle but the container group and the inner rectangle have a different localCenterPoint.


#9

I’m having a bit of trouble following the various console logs posted above. If someone could share a snippet of JS code along with steps you take in XD’s UI to log each step of output, that would be helpful.

I think it’s unlikely there is a bug here though – more likely just that scenegraph coordinate systems can be tricky to wrap one’s head around.

Make sure you’re read all the caveats in https://adobexdplatform.com/plugin-docs/reference/core/coordinate-spaces-and-units.html as starting point. Particularly important to note:

The top-left corner of a node is not always located at (0,0) in its own local coordinate space.

For example, when you select a Group in the UI, the top-left corner of the blue selection rectangle is not necessarily going to be at (0,0) in the group’s coordinate space (i.e. the coordinate space its childrens’ boundsInParent are expressed in). The top-left corner of the selection rectangle is placed at (localBounds.x, localBounds.y) – so in general, you usually work relative to that point in the parent’s coordinates, or relative to the parent’s localCenterPoint.

Also: the properties panel shows X/Y coordinates either relative to the Artboard’s origin (for items in an artboard) or global coordinates (for items on the pasteboard). If your plugin is working with any other coordinate system (e.g. a Group’s local coordinate system), then the numbers are not necessarily going to match.


#10

I’ll expand on my reduced test case:


In the above gif; the last two logs should be the same, right?

Steps:

  1. Add the function (below) to a plugin
  2. Create a rectangle, call the plugin and note the bounds
  3. Group the rectangle
  4. Select the Group, call the plugin and note the bounds
  5. Move the Group
  6. Select the Group, call the plugin and note the bounds
  7. Select the rectangle within the Group, call the plugin and note the bounds

Expected: The bounds of the rectangle should change to reflect the new position
Actual: The bounds are identical to the original position, before grouped and before the group had moved.

function test(selection) {

    // Iterate through the selection
    selection.items.forEach(function (item) {

        console.log('boundsInParent: ', item.boundsInParent);

    })
}

module.exports = {
    commands: {
        "test": test
    }
};

Additional info:

  • Moving the rectangle within the group triggers the boundsInParent to be correct.
  • Using globalDrawBounds is accurate.

#11

boundsInParent expresses the bounds in the immediate parent of the node, i.e. its node.parent value. So when you move a group, the boundsInParent of the things inside the group don’t change because they haven’t moved around within the group – instead, the group that contains them has moved relative to its parent (which is why your 3rd line differs from your 2nd line).

Depending on your use case, it might be easier to work in terms of globalBounds – global coordinates are absolute, so you can compare the globalBounds of any two nodes regardless of their parents, and a node’s globalBounds will change if and only if an object actually visibly has moved to another location.


#12

I’d love to hear feedback on what parts of this are confusing by the way, so we can improve documentation like the Coordinate systems & units page!


Add or rename properties to describe parent container and parent artboard
#13

My test was instead (using the same sizes and positions mentioned by @craigmdennis at the beginning):

  1. create an artboard: 120x120px / x:1000 / y:1000
  2. create a rectangle inside the artboard: 80x80px / x:20 / y:20
  3. select the rectangle and run the plugin:

Rectangle 1
boundsInParent: { x: 20, y: 20, width: 80, height: 80 }
globalBounds: { x: 1020, y: 1020, width: 80, height: 80 }
localBounds: { x: 0, y: 0, width: 80, height: 80 }
topLeftInParent: { x: 20, y: 20 }
localCenterPoint: { x: 40, y: 40 }

  1. group the rectangle and run the plugin again:

Group 1
boundsInParent: { x: 20, y: 20, width: 80, height: 80 }
globalBounds: { x: 1020, y: 1020, width: 80, height: 80 }
localBounds: { x: 20, y: 20, width: 80, height: 80 }
topLeftInParent: { x: 20, y: 20 }
localCenterPoint: { x: 60, y: 60 }

All values are correct, except the group’s localBounds.x, localBounds.y and localCenterPoint, though I’ve not moved anything.


#14

@peterflynn my first assumption was that boundsInParent was a relative value to the parent (including Groups). So if I have a single rectangle in a group, the x and y would be 0,0.

My confusion is regarding why the bounds of the grouped rectangle aren’t updated to match the group when the group is moved, and yet aren’t relative to the parent. It seems very counterintuitive.


#15

Further tests…

TEST 1
Starting from scratch with a rectangle 100x100px / x:0 / y:0

Rectangle 1
boundsInParent: { x: 0, y: 0, width: 100, height: 100 }
globalBounds: { x: 3434, y: 1895, width: 100, height: 100 }
localBounds: { x: 0, y: 0, width: 100, height: 100 }
topLeftInParent: { x: 0, y: 0 }
localCenterPoint: { x: 50, y: 50 }

Group the rectangle and run the plugin

Group 3446
boundsInParent: { x: 0, y: 0, width: 100, height: 100 }
globalBounds: { x: 3434, y: 1895, width: 100, height: 100 }
localBounds: { x: 0, y: 0, width: 100, height: 100 }
topLeftInParent: { x: 0, y: 0 }
localCenterPoint: { x: 50, y: 50 }

TEST 2
Starting from scratch with a rectangle 100x100px / x:1 / y:1

Rectangle 442
boundsInParent: { x: 1, y: 1, width: 100, height: 100 }
globalBounds: { x: 3435, y: 1896, width: 100, height: 100 }
localBounds: { x: 0, y: 0, width: 100, height: 100 }
topLeftInParent: { x: 1, y: 1 }
localCenterPoint: { x: 50, y: 50 }

Group the rectangle and run the plugin

Group 3446
boundsInParent: { x: 1, y: 1, width: 100, height: 100 }
globalBounds: { x: 3435, y: 1896, width: 100, height: 100 }
localBounds: { x: 1, y: 1, width: 100, height: 100 }
topLeftInParent: { x: 1, y: 1 }
localCenterPoint: { x: 51, y: 51 }

When the rectangle is at x:0 / y:0 all values are correct.
If otherwise it’s at x:1 / y:1 the group and the rectangle inside of it start getting different values.


#16

The local bounds did not give me the values I was needing so I wrote a getBoundsInParent method posted here.

The x and y value it returns are relative to the element’s container along with some other related properties.