Skip to content

Commit 257ba78

Browse files
authored
Allow event-based objects to define a rotation center point (#4910)
* It will allow sliders to work vertically.
1 parent fb3d821 commit 257ba78

23 files changed

+873
-559
lines changed

Extensions/PrimitiveDrawing/Extension.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -722,7 +722,7 @@ void DeclarePrimitiveDrawingExtension(gd::PlatformExtension& extension) {
722722
_("Center of rotation"),
723723
_("Change the center of rotation of an object relatively to the "
724724
"object origin."),
725-
_("Change the center of rotation of _PARAM0_: _PARAM1_; _PARAM2_"),
725+
_("Change the center of rotation of _PARAM0_ to _PARAM1_, _PARAM2_"),
726726
_("Angle"),
727727
"res/actions/position24_black.png",
728728
"res/actions/position_black.png")

Extensions/PrimitiveDrawing/shapepainterruntimeobject.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,7 @@ namespace gdjs {
474474
/**
475475
* The center of rotation is defined relatively
476476
* to the drawing origin (the object position).
477-
* This avoid the center to move on the drawing
477+
* This avoids the center to move on the drawing
478478
* when new shapes push the bounds.
479479
*
480480
* When no custom center is defined, it will move

GDJS/GDJS/IDE/ExporterHelper.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -591,8 +591,9 @@ void ExporterHelper::AddLibsInclude(bool pixiRenderers,
591591
InsertUnique(includesFiles, "runtimescene.js");
592592
InsertUnique(includesFiles, "scenestack.js");
593593
InsertUnique(includesFiles, "force.js");
594+
InsertUnique(includesFiles, "RuntimeLayer.js");
594595
InsertUnique(includesFiles, "layer.js");
595-
InsertUnique(includesFiles, "RuntimeSceneLayer.js");
596+
InsertUnique(includesFiles, "RuntimeCustomObjectLayer.js");
596597
InsertUnique(includesFiles, "timer.js");
597598
InsertUnique(includesFiles, "runtimewatermark.js");
598599
InsertUnique(includesFiles, "runtimegame.js");

GDJS/Runtime/CustomRuntimeObject.ts

Lines changed: 137 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,15 @@ namespace gdjs {
2727
_untransformedHitBoxes: gdjs.Polygon[] = [];
2828
/** The dimension of this object is calculated from its children AABBs. */
2929
_unrotatedAABB: AABB = { min: [0, 0], max: [0, 0] };
30-
_scaleX: number = 1;
31-
_scaleY: number = 1;
30+
_scaleX: float = 1;
31+
_scaleY: float = 1;
3232
_flippedX: boolean = false;
3333
_flippedY: boolean = false;
3434
opacity: float = 255;
35+
_customCenter: FloatPoint | null = null;
36+
_localTransformation: gdjs.AffineTransformation = new gdjs.AffineTransformation();
37+
_localInverseTransformation: gdjs.AffineTransformation = new gdjs.AffineTransformation();
38+
_isLocalTransformationDirty: boolean = true;
3539

3640
/**
3741
* @param parent The container the object belongs to
@@ -149,8 +153,9 @@ namespace gdjs {
149153
this._updateUntransformedHitBoxes();
150154
}
151155

152-
//Update the current hitboxes with the frame custom hit boxes
153-
//and apply transformations.
156+
// Update the current hitboxes with the frame custom hit boxes
157+
// and apply transformations.
158+
const localTransformation = this.getLocalTransformation();
154159
for (let i = 0; i < this._untransformedHitBoxes.length; ++i) {
155160
if (i >= this.hitBoxes.length) {
156161
this.hitBoxes.push(new gdjs.Polygon());
@@ -163,9 +168,8 @@ namespace gdjs {
163168
if (j >= this.hitBoxes[i].vertices.length) {
164169
this.hitBoxes[i].vertices.push([0, 0]);
165170
}
166-
this.applyObjectTransformation(
167-
this._untransformedHitBoxes[i].vertices[j][0],
168-
this._untransformedHitBoxes[i].vertices[j][1],
171+
localTransformation.transform(
172+
this._untransformedHitBoxes[i].vertices[j],
169173
this.hitBoxes[i].vertices[j]
170174
);
171175
}
@@ -181,11 +185,6 @@ namespace gdjs {
181185
_updateUntransformedHitBoxes() {
182186
this._isUntransformedHitBoxesDirty = false;
183187

184-
const oldUnrotatedMinX = this._unrotatedAABB.min[0];
185-
const oldUnrotatedMinY = this._unrotatedAABB.min[1];
186-
const oldUnrotatedMaxX = this._unrotatedAABB.max[0];
187-
const oldUnrotatedMaxY = this._unrotatedAABB.max[1];
188-
189188
this._untransformedHitBoxes.length = 0;
190189
if (this._instanceContainer.getAdhocListOfAllInstances().length === 0) {
191190
this._unrotatedAABB.min[0] = 0;
@@ -218,20 +217,6 @@ namespace gdjs {
218217
}
219218
this.hitBoxes.length = this._untransformedHitBoxes.length;
220219
}
221-
222-
// The default camera center depends on the object dimensions so checking
223-
// the AABB center is not enough.
224-
if (
225-
this._unrotatedAABB.min[0] !== oldUnrotatedMinX ||
226-
this._unrotatedAABB.min[1] !== oldUnrotatedMinY ||
227-
this._unrotatedAABB.max[0] !== oldUnrotatedMaxX ||
228-
this._unrotatedAABB.max[1] !== oldUnrotatedMaxY
229-
) {
230-
this._instanceContainer.onObjectUnscaledCenterChanged(
231-
(oldUnrotatedMinX + oldUnrotatedMaxX) / 2,
232-
(oldUnrotatedMinY + oldUnrotatedMaxY) / 2
233-
);
234-
}
235220
}
236221

237222
// Position:
@@ -246,37 +231,52 @@ namespace gdjs {
246231
* @param result Array that will be updated with the result
247232
* (x and y position of the point in parent coordinates).
248233
*/
249-
applyObjectTransformation(x: float, y: float, result: number[]) {
250-
let cx = this.getCenterX();
251-
let cy = this.getCenterY();
234+
applyObjectTransformation(x: float, y: float, destination: FloatPoint) {
235+
const source = destination;
236+
source[0] = x;
237+
source[1] = y;
238+
this.getLocalTransformation().transform(source, destination);
239+
}
252240

253-
// Flipping
254-
if (this._flippedX) {
255-
x = x + (cx - x) * 2;
241+
/**
242+
* Return the affine transformation that represents
243+
* flipping, scale, rotation and translation of the object.
244+
* @returns the affine transformation.
245+
*/
246+
getLocalTransformation(): gdjs.AffineTransformation {
247+
if (this._isLocalTransformationDirty) {
248+
this._updateLocalTransformation();
256249
}
257-
if (this._flippedY) {
258-
y = y + (cy - y) * 2;
250+
return this._localTransformation;
251+
}
252+
253+
getLocalInverseTransformation(): gdjs.AffineTransformation {
254+
if (this._isLocalTransformationDirty) {
255+
this._updateLocalTransformation();
259256
}
257+
return this._localInverseTransformation;
258+
}
260259

261-
// Scale
260+
_updateLocalTransformation() {
262261
const absScaleX = Math.abs(this._scaleX);
263262
const absScaleY = Math.abs(this._scaleY);
264-
x *= absScaleX;
265-
y *= absScaleY;
266-
cx *= absScaleX;
267-
cy *= absScaleY;
268-
269-
// Rotation
270-
const angleInRadians = (this.angle / 180) * Math.PI;
271-
const cosValue = Math.cos(angleInRadians);
272-
const sinValue = Math.sin(angleInRadians);
273-
const xToCenterXDelta = x - cx;
274-
const yToCenterYDelta = y - cy;
275-
x = cx + cosValue * xToCenterXDelta - sinValue * yToCenterYDelta;
276-
y = cy + sinValue * xToCenterXDelta + cosValue * yToCenterYDelta;
277-
result.length = 2;
278-
result[0] = x + this.x;
279-
result[1] = y + this.y;
263+
const centerX = this.getUnscaledCenterX() * absScaleX;
264+
const centerY = this.getUnscaledCenterY() * absScaleY;
265+
const angleInRadians = (this.angle * Math.PI) / 180;
266+
267+
this._localTransformation.setToTranslation(this.x, this.y);
268+
this._localTransformation.rotateAround(angleInRadians, centerX, centerY);
269+
if (this._flippedX) {
270+
this._localTransformation.flipX(centerX);
271+
}
272+
if (this._flippedY) {
273+
this._localTransformation.flipY(centerY);
274+
}
275+
this._localTransformation.scale(absScaleX, absScaleY);
276+
277+
this._localInverseTransformation.copyFrom(this._localTransformation);
278+
this._localInverseTransformation.invert();
279+
this._isLocalTransformationDirty = false;
280280
}
281281

282282
/**
@@ -290,53 +290,51 @@ namespace gdjs {
290290
* @param result Array that will be updated with the result
291291
* (x and y position of the point in object coordinates).
292292
*/
293-
applyObjectInverseTransformation(x: float, y: float, result: number[]) {
294-
x -= this.getCenterXInScene();
295-
y -= this.getCenterYInScene();
296-
297-
const absScaleX = Math.abs(this._scaleX);
298-
const absScaleY = Math.abs(this._scaleY);
299-
300-
// Rotation
301-
const angleInRadians = (this.angle / 180) * Math.PI;
302-
const cosValue = Math.cos(-angleInRadians);
303-
const sinValue = Math.sin(-angleInRadians);
304-
const oldX = x;
305-
x = cosValue * x - sinValue * y;
306-
y = sinValue * oldX + cosValue * y;
307-
308-
// Scale
309-
x /= absScaleX;
310-
y /= absScaleY;
311-
312-
// Flipping
313-
if (this._flippedX) {
314-
x = -x;
315-
}
316-
if (this._flippedY) {
317-
y = -y;
318-
}
319-
320-
const positionToCenterX =
321-
this.getUnscaledWidth() / 2 + this._unrotatedAABB.min[0];
322-
const positionToCenterY =
323-
this.getUnscaledHeight() / 2 + this._unrotatedAABB.min[1];
324-
result[0] = x + positionToCenterX;
325-
result[1] = y + positionToCenterY;
293+
applyObjectInverseTransformation(
294+
x: float,
295+
y: float,
296+
destination: FloatPoint
297+
) {
298+
const source = destination;
299+
source[0] = x;
300+
source[1] = y;
301+
this.getLocalInverseTransformation().transform(source, destination);
326302
}
327303

328304
getDrawableX(): float {
329305
if (this._isUntransformedHitBoxesDirty) {
330306
this._updateUntransformedHitBoxes();
331307
}
332-
return this.x + this._unrotatedAABB.min[0] * this._scaleX;
308+
const absScaleX = this.getScaleX();
309+
if (!this._flippedX) {
310+
return this.x + this._unrotatedAABB.min[0] * absScaleX;
311+
} else {
312+
return (
313+
this.x +
314+
(-this._unrotatedAABB.min[0] -
315+
this.getUnscaledWidth() +
316+
2 * this.getUnscaledCenterX()) *
317+
absScaleX
318+
);
319+
}
333320
}
334321

335322
getDrawableY(): float {
336323
if (this._isUntransformedHitBoxesDirty) {
337324
this._updateUntransformedHitBoxes();
338325
}
339-
return this.y + this._unrotatedAABB.min[1] * this._scaleY;
326+
const absScaleY = this.getScaleY();
327+
if (!this._flippedY) {
328+
return this.y + this._unrotatedAABB.min[1] * absScaleY;
329+
} else {
330+
return (
331+
this.y +
332+
(-this._unrotatedAABB.min[1] -
333+
this.getUnscaledHeight() +
334+
2 * this.getUnscaledCenterY()) *
335+
absScaleY
336+
);
337+
}
340338
}
341339

342340
/**
@@ -363,6 +361,9 @@ namespace gdjs {
363361
* @returns the center X from the local origin (0;0).
364362
*/
365363
getUnscaledCenterX(): float {
364+
if (this._customCenter) {
365+
return this._customCenter[0];
366+
}
366367
if (this._isUntransformedHitBoxesDirty) {
367368
this._updateUntransformedHitBoxes();
368369
}
@@ -373,12 +374,57 @@ namespace gdjs {
373374
* @returns the center Y from the local origin (0;0).
374375
*/
375376
getUnscaledCenterY(): float {
377+
if (this._customCenter) {
378+
return this._customCenter[1];
379+
}
376380
if (this._isUntransformedHitBoxesDirty) {
377381
this._updateUntransformedHitBoxes();
378382
}
379383
return (this._unrotatedAABB.min[1] + this._unrotatedAABB.max[1]) / 2;
380384
}
381385

386+
/**
387+
* The center of rotation is defined relatively to the origin (the object
388+
* position).
389+
* This avoids the center to move when children push the bounds.
390+
*
391+
* When no custom center is defined, it will move
392+
* to stay at the center of the children bounds.
393+
*
394+
* @param x coordinate of the custom center
395+
* @param y coordinate of the custom center
396+
*/
397+
setRotationCenter(x: float, y: float) {
398+
if (!this._customCenter) {
399+
this._customCenter = [0, 0];
400+
}
401+
this._customCenter[0] = x;
402+
this._customCenter[1] = y;
403+
404+
this._isLocalTransformationDirty = true;
405+
this.invalidateHitboxes();
406+
}
407+
408+
getCenterX(): float {
409+
if (this._isUntransformedHitBoxesDirty) {
410+
this._updateUntransformedHitBoxes();
411+
}
412+
return (
413+
(this.getUnscaledCenterX() - this._unrotatedAABB.min[0]) *
414+
this.getScaleX()
415+
);
416+
}
417+
418+
getCenterY(): float {
419+
if (this._isUntransformedHitBoxesDirty) {
420+
this._updateUntransformedHitBoxes();
421+
}
422+
return (
423+
(this.getUnscaledCenterY() - this._unrotatedAABB.min[1]) *
424+
this.getScaleY()
425+
);
426+
}
427+
382428
getWidth(): float {
383429
return this.getUnscaledWidth() * this.getScaleX();
384430
}
@@ -417,6 +463,7 @@ namespace gdjs {
417463
return;
418464
}
419465
this.x = x;
466+
this._isLocalTransformationDirty = true;
420467
this.invalidateHitboxes();
421468
this.getRenderer().updateX();
422469
}
@@ -426,6 +473,7 @@ namespace gdjs {
426473
return;
427474
}
428475
this.y = y;
476+
this._isLocalTransformationDirty = true;
429477
this.invalidateHitboxes();
430478
this.getRenderer().updateY();
431479
}
@@ -435,6 +483,7 @@ namespace gdjs {
435483
return;
436484
}
437485
this.angle = angle;
486+
this._isLocalTransformationDirty = true;
438487
this.invalidateHitboxes();
439488
this.getRenderer().updateAngle();
440489
}
@@ -456,6 +505,7 @@ namespace gdjs {
456505
}
457506
this._scaleX = newScale * (this._flippedX ? -1 : 1);
458507
this._scaleY = newScale * (this._flippedY ? -1 : 1);
508+
this._isLocalTransformationDirty = true;
459509
this.invalidateHitboxes();
460510
this.getRenderer().update();
461511
}
@@ -473,6 +523,7 @@ namespace gdjs {
473523
return;
474524
}
475525
this._scaleX = newScale * (this._flippedX ? -1 : 1);
526+
this._isLocalTransformationDirty = true;
476527
this.invalidateHitboxes();
477528
this.getRenderer().update();
478529
}

0 commit comments

Comments
 (0)