Skip to content

Allow event-based objects to define a rotation center point #4910

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Extensions/PrimitiveDrawing/Extension.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -722,7 +722,7 @@ void DeclarePrimitiveDrawingExtension(gd::PlatformExtension& extension) {
_("Center of rotation"),
_("Change the center of rotation of an object relatively to the "
"object origin."),
_("Change the center of rotation of _PARAM0_: _PARAM1_; _PARAM2_"),
_("Change the center of rotation of _PARAM0_ to _PARAM1_, _PARAM2_"),
_("Angle"),
"res/actions/position24_black.png",
"res/actions/position_black.png")
Expand Down
2 changes: 1 addition & 1 deletion Extensions/PrimitiveDrawing/shapepainterruntimeobject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ namespace gdjs {
/**
* The center of rotation is defined relatively
* to the drawing origin (the object position).
* This avoid the center to move on the drawing
* This avoids the center to move on the drawing
* when new shapes push the bounds.
*
* When no custom center is defined, it will move
Expand Down
3 changes: 2 additions & 1 deletion GDJS/GDJS/IDE/ExporterHelper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -589,8 +589,9 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
InsertUnique(includesFiles, "runtimescene.js");
InsertUnique(includesFiles, "scenestack.js");
InsertUnique(includesFiles, "force.js");
InsertUnique(includesFiles, "RuntimeLayer.js");
InsertUnique(includesFiles, "layer.js");
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be sure I understand: ideally, from the beginning, gdjs.Layer would have been gdjs.RuntimeSceneLayer if it was perfectly named, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly

InsertUnique(includesFiles, "RuntimeSceneLayer.js");
InsertUnique(includesFiles, "RuntimeCustomObjectLayer.js");
InsertUnique(includesFiles, "timer.js");
InsertUnique(includesFiles, "runtimewatermark.js");
InsertUnique(includesFiles, "runtimegame.js");
Expand Down
223 changes: 137 additions & 86 deletions GDJS/Runtime/CustomRuntimeObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,15 @@ namespace gdjs {
_untransformedHitBoxes: gdjs.Polygon[] = [];
/** The dimension of this object is calculated from its children AABBs. */
_unrotatedAABB: AABB = { min: [0, 0], max: [0, 0] };
_scaleX: number = 1;
_scaleY: number = 1;
_scaleX: float = 1;
_scaleY: float = 1;
_flippedX: boolean = false;
_flippedY: boolean = false;
opacity: float = 255;
_customCenter: FloatPoint | null = null;
_localTransformation: gdjs.AffineTransformation = new gdjs.AffineTransformation();
_localInverseTransformation: gdjs.AffineTransformation = new gdjs.AffineTransformation();
_isLocalTransformationDirty: boolean = true;
Comment on lines +36 to +38
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will take more memory but the code is easier to maintain and maybe faster because an affine transformation is 8 arithmetical operations versus something like 25 for the hand crafted transformation.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's fine and fair to have these. Custom objects are not designed to be super lightweight objects working as particles for example.


/**
* @param parent The container the object belongs to
Expand Down Expand Up @@ -149,8 +153,9 @@ namespace gdjs {
this._updateUntransformedHitBoxes();
}

//Update the current hitboxes with the frame custom hit boxes
//and apply transformations.
// Update the current hitboxes with the frame custom hit boxes
// and apply transformations.
const localTransformation = this.getLocalTransformation();
for (let i = 0; i < this._untransformedHitBoxes.length; ++i) {
if (i >= this.hitBoxes.length) {
this.hitBoxes.push(new gdjs.Polygon());
Expand All @@ -163,9 +168,8 @@ namespace gdjs {
if (j >= this.hitBoxes[i].vertices.length) {
this.hitBoxes[i].vertices.push([0, 0]);
}
this.applyObjectTransformation(
this._untransformedHitBoxes[i].vertices[j][0],
this._untransformedHitBoxes[i].vertices[j][1],
localTransformation.transform(
this._untransformedHitBoxes[i].vertices[j],
this.hitBoxes[i].vertices[j]
);
}
Expand All @@ -181,11 +185,6 @@ namespace gdjs {
_updateUntransformedHitBoxes() {
this._isUntransformedHitBoxesDirty = false;

const oldUnrotatedMinX = this._unrotatedAABB.min[0];
const oldUnrotatedMinY = this._unrotatedAABB.min[1];
const oldUnrotatedMaxX = this._unrotatedAABB.max[0];
const oldUnrotatedMaxY = this._unrotatedAABB.max[1];

this._untransformedHitBoxes.length = 0;
if (this._instanceContainer.getAdhocListOfAllInstances().length === 0) {
this._unrotatedAABB.min[0] = 0;
Expand Down Expand Up @@ -218,20 +217,6 @@ namespace gdjs {
}
this.hitBoxes.length = this._untransformedHitBoxes.length;
}

// The default camera center depends on the object dimensions so checking
// the AABB center is not enough.
if (
this._unrotatedAABB.min[0] !== oldUnrotatedMinX ||
this._unrotatedAABB.min[1] !== oldUnrotatedMinY ||
this._unrotatedAABB.max[0] !== oldUnrotatedMaxX ||
this._unrotatedAABB.max[1] !== oldUnrotatedMaxY
) {
this._instanceContainer.onObjectUnscaledCenterChanged(
(oldUnrotatedMinX + oldUnrotatedMaxX) / 2,
(oldUnrotatedMinY + oldUnrotatedMaxY) / 2
);
}
}

// Position:
Expand All @@ -246,37 +231,52 @@ namespace gdjs {
* @param result Array that will be updated with the result
* (x and y position of the point in parent coordinates).
*/
applyObjectTransformation(x: float, y: float, result: number[]) {
let cx = this.getCenterX();
let cy = this.getCenterY();
applyObjectTransformation(x: float, y: float, destination: FloatPoint) {
const source = destination;
source[0] = x;
source[1] = y;
this.getLocalTransformation().transform(source, destination);
}

// Flipping
if (this._flippedX) {
x = x + (cx - x) * 2;
/**
* Return the affine transformation that represents
* flipping, scale, rotation and translation of the object.
* @returns the affine transformation.
*/
getLocalTransformation(): gdjs.AffineTransformation {
if (this._isLocalTransformationDirty) {
this._updateLocalTransformation();
}
if (this._flippedY) {
y = y + (cy - y) * 2;
return this._localTransformation;
}

getLocalInverseTransformation(): gdjs.AffineTransformation {
if (this._isLocalTransformationDirty) {
this._updateLocalTransformation();
}
return this._localInverseTransformation;
}

// Scale
_updateLocalTransformation() {
const absScaleX = Math.abs(this._scaleX);
const absScaleY = Math.abs(this._scaleY);
x *= absScaleX;
y *= absScaleY;
cx *= absScaleX;
cy *= absScaleY;

// Rotation
const angleInRadians = (this.angle / 180) * Math.PI;
const cosValue = Math.cos(angleInRadians);
const sinValue = Math.sin(angleInRadians);
const xToCenterXDelta = x - cx;
const yToCenterYDelta = y - cy;
x = cx + cosValue * xToCenterXDelta - sinValue * yToCenterYDelta;
y = cy + sinValue * xToCenterXDelta + cosValue * yToCenterYDelta;
result.length = 2;
result[0] = x + this.x;
result[1] = y + this.y;
const centerX = this.getUnscaledCenterX() * absScaleX;
const centerY = this.getUnscaledCenterY() * absScaleY;
const angleInRadians = (this.angle * Math.PI) / 180;

this._localTransformation.setToTranslation(this.x, this.y);
this._localTransformation.rotateAround(angleInRadians, centerX, centerY);
if (this._flippedX) {
this._localTransformation.flipX(centerX);
}
if (this._flippedY) {
this._localTransformation.flipY(centerY);
}
this._localTransformation.scale(absScaleX, absScaleY);

this._localInverseTransformation.copyFrom(this._localTransformation);
this._localInverseTransformation.invert();
this._isLocalTransformationDirty = false;
}

/**
Expand All @@ -290,53 +290,51 @@ namespace gdjs {
* @param result Array that will be updated with the result
* (x and y position of the point in object coordinates).
*/
applyObjectInverseTransformation(x: float, y: float, result: number[]) {
x -= this.getCenterXInScene();
y -= this.getCenterYInScene();

const absScaleX = Math.abs(this._scaleX);
const absScaleY = Math.abs(this._scaleY);

// Rotation
const angleInRadians = (this.angle / 180) * Math.PI;
const cosValue = Math.cos(-angleInRadians);
const sinValue = Math.sin(-angleInRadians);
const oldX = x;
x = cosValue * x - sinValue * y;
y = sinValue * oldX + cosValue * y;

// Scale
x /= absScaleX;
y /= absScaleY;

// Flipping
if (this._flippedX) {
x = -x;
}
if (this._flippedY) {
y = -y;
}

const positionToCenterX =
this.getUnscaledWidth() / 2 + this._unrotatedAABB.min[0];
const positionToCenterY =
this.getUnscaledHeight() / 2 + this._unrotatedAABB.min[1];
result[0] = x + positionToCenterX;
result[1] = y + positionToCenterY;
applyObjectInverseTransformation(
x: float,
y: float,
destination: FloatPoint
) {
const source = destination;
source[0] = x;
source[1] = y;
this.getLocalInverseTransformation().transform(source, destination);
}

getDrawableX(): float {
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
return this.x + this._unrotatedAABB.min[0] * this._scaleX;
const absScaleX = this.getScaleX();
if (!this._flippedX) {
return this.x + this._unrotatedAABB.min[0] * absScaleX;
} else {
return (
this.x +
(-this._unrotatedAABB.min[0] -
this.getUnscaledWidth() +
2 * this.getUnscaledCenterX()) *
absScaleX
);
}
}

getDrawableY(): float {
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
return this.y + this._unrotatedAABB.min[1] * this._scaleY;
const absScaleY = this.getScaleY();
if (!this._flippedY) {
return this.y + this._unrotatedAABB.min[1] * absScaleY;
} else {
return (
this.y +
(-this._unrotatedAABB.min[1] -
this.getUnscaledHeight() +
2 * this.getUnscaledCenterY()) *
absScaleY
);
}
}

/**
Expand All @@ -363,6 +361,9 @@ namespace gdjs {
* @returns the center X from the local origin (0;0).
*/
getUnscaledCenterX(): float {
if (this._customCenter) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this a bug?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a feature I forgot to add. Though, we had already done it for the shape painter version of the slider.

return this._customCenter[0];
}
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
Expand All @@ -373,12 +374,57 @@ namespace gdjs {
* @returns the center Y from the local origin (0;0).
*/
getUnscaledCenterY(): float {
if (this._customCenter) {
return this._customCenter[1];
}
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
return (this._unrotatedAABB.min[1] + this._unrotatedAABB.max[1]) / 2;
}

/**
* The center of rotation is defined relatively to the origin (the object
* position).
* This avoids the center to move when children push the bounds.
*
* When no custom center is defined, it will move
* to stay at the center of the children bounds.
*
* @param x coordinate of the custom center
* @param y coordinate of the custom center
*/
setRotationCenter(x: float, y: float) {
if (!this._customCenter) {
this._customCenter = [0, 0];
}
this._customCenter[0] = x;
this._customCenter[1] = y;

this._isLocalTransformationDirty = true;
this.invalidateHitboxes();
}

getCenterX(): float {
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
return (
(this.getUnscaledCenterX() - this._unrotatedAABB.min[0]) *
this.getScaleX()
);
}

getCenterY(): float {
if (this._isUntransformedHitBoxesDirty) {
this._updateUntransformedHitBoxes();
}
return (
(this.getUnscaledCenterY() - this._unrotatedAABB.min[1]) *
this.getScaleY()
);
}

getWidth(): float {
return this.getUnscaledWidth() * this.getScaleX();
}
Expand Down Expand Up @@ -417,6 +463,7 @@ namespace gdjs {
return;
}
this.x = x;
this._isLocalTransformationDirty = true;
this.invalidateHitboxes();
this.getRenderer().updateX();
}
Expand All @@ -426,6 +473,7 @@ namespace gdjs {
return;
}
this.y = y;
this._isLocalTransformationDirty = true;
this.invalidateHitboxes();
this.getRenderer().updateY();
}
Expand All @@ -435,6 +483,7 @@ namespace gdjs {
return;
}
this.angle = angle;
this._isLocalTransformationDirty = true;
this.invalidateHitboxes();
this.getRenderer().updateAngle();
}
Expand All @@ -456,6 +505,7 @@ namespace gdjs {
}
this._scaleX = newScale * (this._flippedX ? -1 : 1);
this._scaleY = newScale * (this._flippedY ? -1 : 1);
this._isLocalTransformationDirty = true;
this.invalidateHitboxes();
this.getRenderer().update();
}
Expand All @@ -473,6 +523,7 @@ namespace gdjs {
return;
}
this._scaleX = newScale * (this._flippedX ? -1 : 1);
this._isLocalTransformationDirty = true;
this.invalidateHitboxes();
this.getRenderer().update();
}
Expand Down
Loading