892 lines
33 KiB
C++
892 lines
33 KiB
C++
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
|
|
/*
|
|
* This file is part of the LibreOffice project.
|
|
*
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
*/
|
|
|
|
#include <box2dtools.hxx>
|
|
#include <config_box2d.h>
|
|
#include BOX2D_HEADER
|
|
|
|
#include <shapemanager.hxx>
|
|
#include <attributableshape.hxx>
|
|
#include <basegfx/polygon/b2dpolypolygontools.hxx>
|
|
#include <basegfx/polygon/b2dpolygontools.hxx>
|
|
#include <basegfx/polygon/b2dpolygontriangulator.hxx>
|
|
|
|
#include <svx/svdobj.hxx>
|
|
#include <svx/svdoashp.hxx>
|
|
#include <svx/svdpage.hxx>
|
|
|
|
#include <svx/unoapi.hxx>
|
|
#include <utility>
|
|
|
|
#define BOX2D_SLIDE_SIZE_IN_METERS 100.00f
|
|
constexpr double fDefaultStaticBodyBounciness(0.1);
|
|
|
|
namespace box2d::utils
|
|
{
|
|
namespace
|
|
{
|
|
double calculateScaleFactor(const ::basegfx::B2DVector& rSlideSize)
|
|
{
|
|
double fWidth = rSlideSize.getX();
|
|
double fHeight = rSlideSize.getY();
|
|
|
|
// Scale factor is based on whatever is the larger
|
|
// value between slide width and height
|
|
if (fWidth > fHeight)
|
|
return BOX2D_SLIDE_SIZE_IN_METERS / fWidth;
|
|
else
|
|
return BOX2D_SLIDE_SIZE_IN_METERS / fHeight;
|
|
}
|
|
|
|
b2BodyType getBox2DInternalBodyType(const box2DBodyType eType)
|
|
{
|
|
switch (eType)
|
|
{
|
|
default:
|
|
case BOX2D_STATIC_BODY:
|
|
return b2_staticBody;
|
|
case BOX2D_KINEMATIC_BODY:
|
|
return b2_kinematicBody;
|
|
case BOX2D_DYNAMIC_BODY:
|
|
return b2_dynamicBody;
|
|
}
|
|
}
|
|
|
|
box2DBodyType getBox2DLOBodyType(const b2BodyType eType)
|
|
{
|
|
switch (eType)
|
|
{
|
|
default:
|
|
case b2_staticBody:
|
|
return BOX2D_STATIC_BODY;
|
|
case b2_kinematicBody:
|
|
return BOX2D_KINEMATIC_BODY;
|
|
case b2_dynamicBody:
|
|
return BOX2D_DYNAMIC_BODY;
|
|
}
|
|
}
|
|
|
|
b2Vec2 convertB2DPointToBox2DVec2(const basegfx::B2DPoint& aPoint, const double fScaleFactor)
|
|
{
|
|
return { static_cast<float>(aPoint.getX() * fScaleFactor),
|
|
static_cast<float>(aPoint.getY() * -fScaleFactor) };
|
|
}
|
|
|
|
// expects rTriangleVector to have coordinates relative to the shape's bounding box center
|
|
void addTriangleVectorToBody(const basegfx::triangulator::B2DTriangleVector& rTriangleVector,
|
|
b2Body* aBody, const float fDensity, const float fFriction,
|
|
const float fRestitution, const double fScaleFactor)
|
|
{
|
|
for (const basegfx::triangulator::B2DTriangle& aTriangle : rTriangleVector)
|
|
{
|
|
b2FixtureDef aFixture;
|
|
b2PolygonShape aPolygonShape;
|
|
b2Vec2 aTriangleVertices[3]
|
|
= { convertB2DPointToBox2DVec2(aTriangle.getA(), fScaleFactor),
|
|
convertB2DPointToBox2DVec2(aTriangle.getB(), fScaleFactor),
|
|
convertB2DPointToBox2DVec2(aTriangle.getC(), fScaleFactor) };
|
|
|
|
bool bValidPointDistance
|
|
= b2DistanceSquared(aTriangleVertices[0], aTriangleVertices[1]) > 0.003f
|
|
&& b2DistanceSquared(aTriangleVertices[0], aTriangleVertices[2]) > 0.003f
|
|
&& b2DistanceSquared(aTriangleVertices[1], aTriangleVertices[2]) > 0.003f;
|
|
|
|
if (bValidPointDistance)
|
|
{
|
|
// create a fixture that represents the triangle
|
|
aPolygonShape.Set(aTriangleVertices, 3);
|
|
aFixture.shape = &aPolygonShape;
|
|
aFixture.density = fDensity;
|
|
aFixture.friction = fFriction;
|
|
aFixture.restitution = fRestitution;
|
|
aBody->CreateFixture(&aFixture);
|
|
}
|
|
}
|
|
}
|
|
|
|
// expects rPolygon to have coordinates relative to it's center
|
|
void addEdgeShapeToBody(const basegfx::B2DPolygon& rPolygon, b2Body* aBody, const float fDensity,
|
|
const float fFriction, const float fRestitution, const double fScaleFactor)
|
|
{
|
|
// make sure there's no bezier curves on the polygon
|
|
assert(!rPolygon.areControlPointsUsed());
|
|
basegfx::B2DPolygon aPolygon = basegfx::utils::removeNeutralPoints(rPolygon);
|
|
|
|
// value that somewhat defines half width of the quadrilateral
|
|
// that will be representing edge segment in the box2d world
|
|
const float fHalfWidth = 0.1f;
|
|
bool bHasPreviousQuadrilateralEdge = false;
|
|
b2Vec2 aQuadrilateralVertices[4];
|
|
|
|
for (sal_uInt32 nIndex = 0; nIndex < aPolygon.count(); nIndex++)
|
|
{
|
|
b2FixtureDef aFixture;
|
|
b2PolygonShape aPolygonShape;
|
|
|
|
basegfx::B2DPoint aPointA;
|
|
basegfx::B2DPoint aPointB;
|
|
if (nIndex != 0)
|
|
{
|
|
// get two adjacent points to create an edge out of
|
|
aPointA = aPolygon.getB2DPoint(nIndex - 1);
|
|
aPointB = aPolygon.getB2DPoint(nIndex);
|
|
}
|
|
else if (aPolygon.isClosed())
|
|
{
|
|
// start by connecting the last point to the first one
|
|
aPointA = aPolygon.getB2DPoint(aPolygon.count() - 1);
|
|
aPointB = aPolygon.getB2DPoint(nIndex);
|
|
}
|
|
else // the polygon isn't closed, won't connect last and first points
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// create a vector that represents the direction of the edge
|
|
// and make it a unit vector
|
|
b2Vec2 aEdgeUnitVec(convertB2DPointToBox2DVec2(aPointB, fScaleFactor)
|
|
- convertB2DPointToBox2DVec2(aPointA, fScaleFactor));
|
|
aEdgeUnitVec.Normalize();
|
|
|
|
// create a unit vector that represents Normal of the edge
|
|
b2Vec2 aEdgeNormal(-aEdgeUnitVec.y, aEdgeUnitVec.x);
|
|
|
|
// if there was an edge previously created it should just connect
|
|
// using it's ending points so that there are no empty spots
|
|
// between edge segments, if not use wherever aPointA is at
|
|
if (!bHasPreviousQuadrilateralEdge)
|
|
{
|
|
// the point is translated along the edge normal both directions by
|
|
// fHalfWidth to create a quadrilateral edge
|
|
aQuadrilateralVertices[0]
|
|
= convertB2DPointToBox2DVec2(aPointA, fScaleFactor) + fHalfWidth * aEdgeNormal;
|
|
aQuadrilateralVertices[1]
|
|
= convertB2DPointToBox2DVec2(aPointA, fScaleFactor) + -fHalfWidth * aEdgeNormal;
|
|
bHasPreviousQuadrilateralEdge = true;
|
|
}
|
|
aQuadrilateralVertices[2]
|
|
= convertB2DPointToBox2DVec2(aPointB, fScaleFactor) + fHalfWidth * aEdgeNormal;
|
|
aQuadrilateralVertices[3]
|
|
= convertB2DPointToBox2DVec2(aPointB, fScaleFactor) + -fHalfWidth * aEdgeNormal;
|
|
|
|
// check whether the edge would have degenerately close points
|
|
bool bValidPointDistance
|
|
= b2DistanceSquared(aQuadrilateralVertices[0], aQuadrilateralVertices[2]) > 0.003f;
|
|
|
|
if (bValidPointDistance)
|
|
{
|
|
// create a quadrilateral shaped fixture to represent the edge
|
|
aPolygonShape.Set(aQuadrilateralVertices, 4);
|
|
aFixture.shape = &aPolygonShape;
|
|
aFixture.density = fDensity;
|
|
aFixture.friction = fFriction;
|
|
aFixture.restitution = fRestitution;
|
|
aBody->CreateFixture(&aFixture);
|
|
|
|
// prepare the quadrilateral edge for next connection
|
|
aQuadrilateralVertices[0] = aQuadrilateralVertices[2];
|
|
aQuadrilateralVertices[1] = aQuadrilateralVertices[3];
|
|
}
|
|
}
|
|
}
|
|
|
|
void addEdgeShapeToBody(const basegfx::B2DPolyPolygon& rPolyPolygon, b2Body* aBody,
|
|
const float fDensity, const float fFriction, const float fRestitution,
|
|
const double fScaleFactor)
|
|
{
|
|
for (const basegfx::B2DPolygon& rPolygon : rPolyPolygon)
|
|
{
|
|
addEdgeShapeToBody(rPolygon, aBody, fDensity, fFriction, fRestitution, fScaleFactor);
|
|
}
|
|
}
|
|
}
|
|
|
|
box2DWorld::box2DWorld(const ::basegfx::B2DVector& rSlideSize)
|
|
: mpBox2DWorld()
|
|
, mfScaleFactor(calculateScaleFactor(rSlideSize))
|
|
, mbShapesInitialized(false)
|
|
, mbHasWorldStepper(false)
|
|
, mbAlreadyStepped(false)
|
|
, mnPhysicsAnimationCounter(0)
|
|
, mpXShapeToBodyMap()
|
|
, maShapeParallelUpdateQueue()
|
|
{
|
|
}
|
|
|
|
box2DWorld::~box2DWorld() = default;
|
|
|
|
bool box2DWorld::initiateWorld(const ::basegfx::B2DVector& rSlideSize)
|
|
{
|
|
if (!mpBox2DWorld)
|
|
{
|
|
mpBox2DWorld = std::make_unique<b2World>(b2Vec2(0.0f, -30.0f));
|
|
createStaticFrameAroundSlide(rSlideSize);
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
void box2DWorld::createStaticFrameAroundSlide(const ::basegfx::B2DVector& rSlideSize)
|
|
{
|
|
assert(mpBox2DWorld);
|
|
|
|
float fWidth = static_cast<float>(rSlideSize.getX() * mfScaleFactor);
|
|
float fHeight = static_cast<float>(rSlideSize.getY() * mfScaleFactor);
|
|
|
|
// static body for creating the frame around the slide
|
|
b2BodyDef aBodyDef;
|
|
aBodyDef.type = b2_staticBody;
|
|
aBodyDef.position.Set(0, 0);
|
|
|
|
// not going to be stored anywhere, will live
|
|
// as long as the Box2DWorld does
|
|
b2Body* pStaticBody = mpBox2DWorld->CreateBody(&aBodyDef);
|
|
|
|
// create an edge loop that represents slide frame
|
|
b2Vec2 aEdgePoints[4];
|
|
aEdgePoints[0].Set(0, 0);
|
|
aEdgePoints[1].Set(0, -fHeight);
|
|
aEdgePoints[2].Set(fWidth, -fHeight);
|
|
aEdgePoints[3].Set(fWidth, 0);
|
|
|
|
b2ChainShape aEdgesChainShape;
|
|
aEdgesChainShape.CreateLoop(aEdgePoints, 4);
|
|
|
|
// create the fixture for the shape
|
|
b2FixtureDef aFixtureDef;
|
|
aFixtureDef.shape = &aEdgesChainShape;
|
|
pStaticBody->CreateFixture(&aFixtureDef);
|
|
}
|
|
|
|
void box2DWorld::setShapePosition(const css::uno::Reference<css::drawing::XShape>& xShape,
|
|
const basegfx::B2DPoint& rOutPos)
|
|
{
|
|
const auto iter = mpXShapeToBodyMap.find(xShape);
|
|
assert(iter != mpXShapeToBodyMap.end());
|
|
Box2DBodySharedPtr pBox2DBody = iter->second;
|
|
pBox2DBody->setPosition(rOutPos);
|
|
}
|
|
|
|
void box2DWorld::setShapePositionByLinearVelocity(
|
|
const css::uno::Reference<css::drawing::XShape>& xShape, const basegfx::B2DPoint& rOutPos,
|
|
const double fPassedTime)
|
|
{
|
|
assert(mpBox2DWorld);
|
|
if (fPassedTime > 0) // this only makes sense if there was an advance in time
|
|
{
|
|
const auto iter = mpXShapeToBodyMap.find(xShape);
|
|
assert(iter != mpXShapeToBodyMap.end());
|
|
Box2DBodySharedPtr pBox2DBody = iter->second;
|
|
pBox2DBody->setPositionByLinearVelocity(rOutPos, fPassedTime);
|
|
}
|
|
}
|
|
|
|
void box2DWorld::setShapeLinearVelocity(const css::uno::Reference<css::drawing::XShape>& xShape,
|
|
const basegfx::B2DVector& rVelocity)
|
|
{
|
|
assert(mpBox2DWorld);
|
|
const auto iter = mpXShapeToBodyMap.find(xShape);
|
|
assert(iter != mpXShapeToBodyMap.end());
|
|
Box2DBodySharedPtr pBox2DBody = iter->second;
|
|
pBox2DBody->setLinearVelocity(rVelocity);
|
|
}
|
|
|
|
void box2DWorld::setShapeAngle(const css::uno::Reference<css::drawing::XShape>& xShape,
|
|
const double fAngle)
|
|
{
|
|
const auto iter = mpXShapeToBodyMap.find(xShape);
|
|
assert(iter != mpXShapeToBodyMap.end());
|
|
Box2DBodySharedPtr pBox2DBody = iter->second;
|
|
pBox2DBody->setAngle(fAngle);
|
|
}
|
|
|
|
void box2DWorld::setShapeAngleByAngularVelocity(
|
|
const css::uno::Reference<css::drawing::XShape>& xShape, const double fAngle,
|
|
const double fPassedTime)
|
|
{
|
|
assert(mpBox2DWorld);
|
|
if (fPassedTime > 0) // this only makes sense if there was an advance in time
|
|
{
|
|
const auto iter = mpXShapeToBodyMap.find(xShape);
|
|
assert(iter != mpXShapeToBodyMap.end());
|
|
Box2DBodySharedPtr pBox2DBody = iter->second;
|
|
pBox2DBody->setAngleByAngularVelocity(fAngle, fPassedTime);
|
|
}
|
|
}
|
|
|
|
void box2DWorld::setShapeAngularVelocity(const css::uno::Reference<css::drawing::XShape>& xShape,
|
|
const double fAngularVelocity)
|
|
{
|
|
assert(mpBox2DWorld);
|
|
const auto iter = mpXShapeToBodyMap.find(xShape);
|
|
assert(iter != mpXShapeToBodyMap.end());
|
|
Box2DBodySharedPtr pBox2DBody = iter->second;
|
|
pBox2DBody->setAngularVelocity(fAngularVelocity);
|
|
}
|
|
|
|
void box2DWorld::setShapeCollision(const css::uno::Reference<css::drawing::XShape>& xShape,
|
|
bool bCanCollide)
|
|
{
|
|
assert(mpBox2DWorld);
|
|
const auto iter = mpXShapeToBodyMap.find(xShape);
|
|
assert(iter != mpXShapeToBodyMap.end());
|
|
Box2DBodySharedPtr pBox2DBody = iter->second;
|
|
pBox2DBody->setCollision(bCanCollide);
|
|
}
|
|
|
|
void box2DWorld::processUpdateQueue(const double fPassedTime)
|
|
{
|
|
while (!maShapeParallelUpdateQueue.empty())
|
|
{
|
|
Box2DDynamicUpdateInformation& aQueueElement = maShapeParallelUpdateQueue.front();
|
|
|
|
if (aQueueElement.mnDelayForSteps > 0)
|
|
{
|
|
// it was queued as a delayed action, skip it, don't pop
|
|
aQueueElement.mnDelayForSteps--;
|
|
}
|
|
else
|
|
{
|
|
switch (aQueueElement.meUpdateType)
|
|
{
|
|
default:
|
|
case BOX2D_UPDATE_POSITION_CHANGE:
|
|
setShapePositionByLinearVelocity(aQueueElement.mxShape,
|
|
aQueueElement.maPosition, fPassedTime);
|
|
break;
|
|
case BOX2D_UPDATE_POSITION:
|
|
setShapePosition(aQueueElement.mxShape, aQueueElement.maPosition);
|
|
break;
|
|
case BOX2D_UPDATE_ANGLE:
|
|
setShapeAngleByAngularVelocity(aQueueElement.mxShape, aQueueElement.mfAngle,
|
|
fPassedTime);
|
|
break;
|
|
case BOX2D_UPDATE_SIZE:
|
|
break;
|
|
case BOX2D_UPDATE_VISIBILITY:
|
|
setShapeCollision(aQueueElement.mxShape, aQueueElement.mbVisibility);
|
|
break;
|
|
case BOX2D_UPDATE_LINEAR_VELOCITY:
|
|
setShapeLinearVelocity(aQueueElement.mxShape, aQueueElement.maVelocity);
|
|
break;
|
|
case BOX2D_UPDATE_ANGULAR_VELOCITY:
|
|
setShapeAngularVelocity(aQueueElement.mxShape, aQueueElement.mfAngularVelocity);
|
|
}
|
|
maShapeParallelUpdateQueue.pop();
|
|
}
|
|
}
|
|
}
|
|
|
|
void box2DWorld::initiateAllShapesAsStaticBodies(
|
|
const slideshow::internal::ShapeManagerSharedPtr& pShapeManager)
|
|
{
|
|
assert(mpBox2DWorld);
|
|
|
|
mbShapesInitialized = true;
|
|
auto aXShapeToShapeMap = pShapeManager->getXShapeToShapeMap();
|
|
|
|
std::unordered_map<css::uno::Reference<css::drawing::XShape>, bool> aXShapeBelongsToAGroup;
|
|
|
|
// iterate over the shapes in the current slide and flag them if they belong to a group
|
|
// will flag the only ones that are belong to a group since std::unordered_map operator[]
|
|
// defaults the value to false if the key doesn't have a corresponding value
|
|
for (auto aIt = aXShapeToShapeMap.begin(); aIt != aXShapeToShapeMap.end(); aIt++)
|
|
{
|
|
slideshow::internal::ShapeSharedPtr pShape = aIt->second;
|
|
if (pShape->isForeground())
|
|
{
|
|
SdrObject* pTemp = SdrObject::getSdrObjectFromXShape(pShape->getXShape());
|
|
if (pTemp && pTemp->IsGroupObject())
|
|
{
|
|
// if it is a group object iterate over its children and flag them
|
|
SdrObjList* aObjList = pTemp->GetSubList();
|
|
|
|
for (const rtl::Reference<SdrObject>& pGroupMember : *aObjList)
|
|
{
|
|
aXShapeBelongsToAGroup.insert(
|
|
std::make_pair(GetXShapeForSdrObject(pGroupMember.get()), true));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// iterate over shapes in the current slide
|
|
for (auto aIt = aXShapeToShapeMap.begin(); aIt != aXShapeToShapeMap.end(); aIt++)
|
|
{
|
|
slideshow::internal::ShapeSharedPtr pShape = aIt->second;
|
|
// only create static bodies for the shapes that do not belong to a group
|
|
// groups themselves will have one body that represents the whole shape
|
|
// collection
|
|
if (pShape->isForeground() && !aXShapeBelongsToAGroup[pShape->getXShape()])
|
|
{
|
|
Box2DBodySharedPtr pBox2DBody = createStaticBody(pShape);
|
|
|
|
mpXShapeToBodyMap.insert(std::make_pair(pShape->getXShape(), pBox2DBody));
|
|
if (!pShape->isVisible())
|
|
{
|
|
// if the shape isn't visible, queue an update for it
|
|
queueShapeVisibilityUpdate(pShape->getXShape(), false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool box2DWorld::hasWorldStepper() const { return mbHasWorldStepper; }
|
|
|
|
void box2DWorld::setHasWorldStepper(const bool bHasWorldStepper)
|
|
{
|
|
mbHasWorldStepper = bHasWorldStepper;
|
|
}
|
|
|
|
void box2DWorld::queueDynamicPositionUpdate(const css::uno::Reference<css::drawing::XShape>& xShape,
|
|
const basegfx::B2DPoint& rOutPos)
|
|
{
|
|
Box2DDynamicUpdateInformation aQueueElement = { xShape, {}, BOX2D_UPDATE_POSITION_CHANGE };
|
|
aQueueElement.maPosition = rOutPos;
|
|
maShapeParallelUpdateQueue.push(aQueueElement);
|
|
}
|
|
|
|
void box2DWorld::queueLinearVelocityUpdate(const css::uno::Reference<css::drawing::XShape>& xShape,
|
|
const basegfx::B2DVector& rVelocity,
|
|
const int nDelayForSteps)
|
|
{
|
|
Box2DDynamicUpdateInformation aQueueElement
|
|
= { xShape, {}, BOX2D_UPDATE_LINEAR_VELOCITY, nDelayForSteps };
|
|
aQueueElement.maVelocity = rVelocity;
|
|
maShapeParallelUpdateQueue.push(aQueueElement);
|
|
}
|
|
|
|
void box2DWorld::queueDynamicRotationUpdate(const css::uno::Reference<css::drawing::XShape>& xShape,
|
|
const double fAngle)
|
|
{
|
|
Box2DDynamicUpdateInformation aQueueElement = { xShape, {}, BOX2D_UPDATE_ANGLE };
|
|
aQueueElement.mfAngle = fAngle;
|
|
maShapeParallelUpdateQueue.push(aQueueElement);
|
|
}
|
|
|
|
void box2DWorld::queueAngularVelocityUpdate(const css::uno::Reference<css::drawing::XShape>& xShape,
|
|
const double fAngularVelocity, const int nDelayForSteps)
|
|
{
|
|
Box2DDynamicUpdateInformation aQueueElement
|
|
= { xShape, {}, BOX2D_UPDATE_ANGULAR_VELOCITY, nDelayForSteps };
|
|
aQueueElement.mfAngularVelocity = fAngularVelocity;
|
|
maShapeParallelUpdateQueue.push(aQueueElement);
|
|
}
|
|
|
|
void box2DWorld::queueShapeVisibilityUpdate(const css::uno::Reference<css::drawing::XShape>& xShape,
|
|
const bool bVisibility)
|
|
{
|
|
Box2DDynamicUpdateInformation aQueueElement = { xShape, {}, BOX2D_UPDATE_VISIBILITY };
|
|
aQueueElement.mbVisibility = bVisibility;
|
|
maShapeParallelUpdateQueue.push(aQueueElement);
|
|
}
|
|
|
|
void box2DWorld::queueShapePositionUpdate(const css::uno::Reference<css::drawing::XShape>& xShape,
|
|
const basegfx::B2DPoint& rOutPos)
|
|
{
|
|
Box2DDynamicUpdateInformation aQueueElement = { xShape, {}, BOX2D_UPDATE_POSITION };
|
|
aQueueElement.maPosition = rOutPos;
|
|
maShapeParallelUpdateQueue.push(aQueueElement);
|
|
}
|
|
|
|
void box2DWorld::queueShapePathAnimationUpdate(
|
|
const css::uno::Reference<css::drawing::XShape>& xShape,
|
|
const slideshow::internal::ShapeAttributeLayerSharedPtr& pAttrLayer, const bool bIsFirstUpdate)
|
|
{
|
|
// Workaround for PathAnimations since they do not have their own AttributeType
|
|
// - using PosX makes it register a DynamicPositionUpdate -
|
|
queueShapeAnimationUpdate(xShape, pAttrLayer, slideshow::internal::AttributeType::PosX,
|
|
bIsFirstUpdate);
|
|
}
|
|
|
|
void box2DWorld::queueShapeAnimationUpdate(
|
|
const css::uno::Reference<css::drawing::XShape>& xShape,
|
|
const slideshow::internal::ShapeAttributeLayerSharedPtr& pAttrLayer,
|
|
const slideshow::internal::AttributeType eAttrType, const bool bIsFirstUpdate)
|
|
{
|
|
switch (eAttrType)
|
|
{
|
|
case slideshow::internal::AttributeType::Visibility:
|
|
queueShapeVisibilityUpdate(xShape, pAttrLayer->getVisibility());
|
|
return;
|
|
case slideshow::internal::AttributeType::Rotate:
|
|
queueDynamicRotationUpdate(xShape, pAttrLayer->getRotationAngle());
|
|
return;
|
|
case slideshow::internal::AttributeType::PosX:
|
|
case slideshow::internal::AttributeType::PosY:
|
|
if (bIsFirstUpdate) // if it is the first update shape should _teleport_ to the position
|
|
queueShapePositionUpdate(xShape, { pAttrLayer->getPosX(), pAttrLayer->getPosY() });
|
|
else
|
|
queueDynamicPositionUpdate(xShape,
|
|
{ pAttrLayer->getPosX(), pAttrLayer->getPosY() });
|
|
return;
|
|
default:
|
|
return;
|
|
}
|
|
}
|
|
|
|
void box2DWorld::queueShapeAnimationEndUpdate(
|
|
const css::uno::Reference<css::drawing::XShape>& xShape,
|
|
const slideshow::internal::AttributeType eAttrType)
|
|
{
|
|
switch (eAttrType)
|
|
{
|
|
// end updates that change the velocity are delayed for a step
|
|
// since we do not want them to override the last position/angle
|
|
case slideshow::internal::AttributeType::Rotate:
|
|
queueAngularVelocityUpdate(xShape, 0.0, 1);
|
|
return;
|
|
case slideshow::internal::AttributeType::PosX:
|
|
case slideshow::internal::AttributeType::PosY:
|
|
queueLinearVelocityUpdate(xShape, { 0, 0 }, 1);
|
|
return;
|
|
default:
|
|
return;
|
|
}
|
|
}
|
|
|
|
void box2DWorld::alertPhysicsAnimationEnd(const slideshow::internal::ShapeSharedPtr& pShape)
|
|
{
|
|
const auto iter = mpXShapeToBodyMap.find(pShape->getXShape());
|
|
assert(iter != mpXShapeToBodyMap.end());
|
|
Box2DBodySharedPtr pBox2DBody = iter->second;
|
|
// since the animation ended make the body static
|
|
makeBodyStatic(pBox2DBody);
|
|
pBox2DBody->setRestitution(fDefaultStaticBodyBounciness);
|
|
if (--mnPhysicsAnimationCounter == 0)
|
|
{
|
|
// if there are no more physics animation effects going on clean up
|
|
maShapeParallelUpdateQueue = {};
|
|
mbShapesInitialized = false;
|
|
// clearing the map will make the box2d bodies get
|
|
// destroyed if there's nothing else that owns them
|
|
mpXShapeToBodyMap.clear();
|
|
}
|
|
else
|
|
{
|
|
// the physics animation that will take over the lock after this one
|
|
// shouldn't step the world for an update cycle - since it was already
|
|
// stepped.
|
|
mbAlreadyStepped = true;
|
|
}
|
|
}
|
|
|
|
void box2DWorld::alertPhysicsAnimationStart(
|
|
const ::basegfx::B2DVector& rSlideSize,
|
|
const slideshow::internal::ShapeManagerSharedPtr& pShapeManager)
|
|
{
|
|
if (!mpBox2DWorld)
|
|
initiateWorld(rSlideSize);
|
|
|
|
if (!mbShapesInitialized)
|
|
initiateAllShapesAsStaticBodies(pShapeManager);
|
|
|
|
mnPhysicsAnimationCounter++;
|
|
}
|
|
|
|
void box2DWorld::step(const float fTimeStep, const int nVelocityIterations,
|
|
const int nPositionIterations)
|
|
{
|
|
assert(mpBox2DWorld);
|
|
mpBox2DWorld->Step(fTimeStep, nVelocityIterations, nPositionIterations);
|
|
}
|
|
|
|
double box2DWorld::stepAmount(const double fPassedTime, const float fTimeStep,
|
|
const int nVelocityIterations, const int nPositionIterations)
|
|
{
|
|
assert(mpBox2DWorld);
|
|
|
|
unsigned int nStepAmount = static_cast<unsigned int>(std::round(fPassedTime / fTimeStep));
|
|
// find the actual time that will be stepped through so
|
|
// that the updates can be processed using that value
|
|
double fTimeSteppedThrough = fTimeStep * nStepAmount;
|
|
|
|
// do the updates required to simulate other animation effects going in parallel
|
|
processUpdateQueue(fTimeSteppedThrough);
|
|
|
|
if (!mbAlreadyStepped)
|
|
{
|
|
for (unsigned int nStepCounter = 0; nStepCounter < nStepAmount; nStepCounter++)
|
|
{
|
|
step(fTimeStep, nVelocityIterations, nPositionIterations);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// just got the step lock from another physics animation
|
|
// so skipping stepping the world for an update cycle
|
|
mbAlreadyStepped = false;
|
|
}
|
|
|
|
return fTimeSteppedThrough;
|
|
}
|
|
|
|
bool box2DWorld::shapesInitialized() { return mbShapesInitialized; }
|
|
|
|
bool box2DWorld::isInitialized() const
|
|
{
|
|
if (mpBox2DWorld)
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
Box2DBodySharedPtr
|
|
box2DWorld::makeShapeDynamic(const css::uno::Reference<css::drawing::XShape>& xShape,
|
|
const basegfx::B2DVector& rStartVelocity, const double fDensity,
|
|
const double fBounciness)
|
|
{
|
|
assert(mpBox2DWorld);
|
|
Box2DBodySharedPtr pBox2DBody = mpXShapeToBodyMap.find(xShape)->second;
|
|
pBox2DBody->setDensityAndRestitution(fDensity, fBounciness);
|
|
queueLinearVelocityUpdate(xShape, rStartVelocity, 1);
|
|
return makeBodyDynamic(pBox2DBody);
|
|
}
|
|
|
|
Box2DBodySharedPtr makeBodyDynamic(const Box2DBodySharedPtr& pBox2DBody)
|
|
{
|
|
if (pBox2DBody->getType() != BOX2D_DYNAMIC_BODY)
|
|
{
|
|
pBox2DBody->setType(BOX2D_DYNAMIC_BODY);
|
|
}
|
|
return pBox2DBody;
|
|
}
|
|
|
|
Box2DBodySharedPtr box2DWorld::makeShapeStatic(const slideshow::internal::ShapeSharedPtr& pShape)
|
|
{
|
|
assert(mpBox2DWorld);
|
|
assert(pShape && pShape->getXShape());
|
|
const auto iter = mpXShapeToBodyMap.find(pShape->getXShape());
|
|
assert(iter != mpXShapeToBodyMap.end());
|
|
Box2DBodySharedPtr pBox2DBody = iter->second;
|
|
return makeBodyStatic(pBox2DBody);
|
|
}
|
|
|
|
Box2DBodySharedPtr makeBodyStatic(const Box2DBodySharedPtr& pBox2DBody)
|
|
{
|
|
if (pBox2DBody->getType() != BOX2D_STATIC_BODY)
|
|
{
|
|
pBox2DBody->setType(BOX2D_STATIC_BODY);
|
|
}
|
|
return pBox2DBody;
|
|
}
|
|
|
|
Box2DBodySharedPtr box2DWorld::createStaticBody(const slideshow::internal::ShapeSharedPtr& rShape,
|
|
const float fDensity, const float fFriction)
|
|
{
|
|
assert(mpBox2DWorld);
|
|
|
|
::basegfx::B2DRectangle aShapeBounds = rShape->getBounds();
|
|
|
|
b2BodyDef aBodyDef;
|
|
aBodyDef.type = b2_staticBody;
|
|
aBodyDef.position = convertB2DPointToBox2DVec2(aShapeBounds.getCenter(), mfScaleFactor);
|
|
|
|
slideshow::internal::ShapeAttributeLayerSharedPtr pShapeAttributeLayer
|
|
= static_cast<slideshow::internal::AttributableShape*>(rShape.get())
|
|
->getTopmostAttributeLayer();
|
|
if (pShapeAttributeLayer && pShapeAttributeLayer->isRotationAngleValid())
|
|
{
|
|
// if the shape's rotation value was altered by another animation effect set it.
|
|
aBodyDef.angle = ::basegfx::deg2rad(-pShapeAttributeLayer->getRotationAngle());
|
|
}
|
|
|
|
// create a shared pointer with a destructor so that the body will be properly destroyed
|
|
std::shared_ptr<b2Body> pBody(mpBox2DWorld->CreateBody(&aBodyDef), [](b2Body* pB2Body) {
|
|
pB2Body->GetWorld()->DestroyBody(pB2Body);
|
|
});
|
|
|
|
SdrObject* pSdrObject = SdrObject::getSdrObjectFromXShape(rShape->getXShape());
|
|
|
|
rtl::OUString aShapeType = rShape->getXShape()->getShapeType();
|
|
|
|
basegfx::B2DPolyPolygon aPolyPolygon;
|
|
// workaround:
|
|
// TakeXorPoly() doesn't return beziers for CustomShapes and we want the beziers
|
|
// so that we can decide the complexity of the polygons generated from them
|
|
if (aShapeType == "com.sun.star.drawing.CustomShape")
|
|
{
|
|
aPolyPolygon = static_cast<SdrObjCustomShape*>(pSdrObject)->GetLineGeometry(true);
|
|
}
|
|
else
|
|
{
|
|
aPolyPolygon = pSdrObject->TakeXorPoly();
|
|
}
|
|
|
|
// make beziers into polygons, using a high degree angle as fAngleBound in
|
|
// adaptiveSubdivideByAngle reduces complexity of the resulting polygon shapes
|
|
aPolyPolygon = aPolyPolygon.areControlPointsUsed()
|
|
? basegfx::utils::adaptiveSubdivideByAngle(aPolyPolygon, 20)
|
|
: aPolyPolygon;
|
|
aPolyPolygon.removeDoublePoints();
|
|
|
|
// make polygon coordinates relative to the center of the shape instead of top left of the slide
|
|
// since box2d shapes are expressed this way
|
|
aPolyPolygon
|
|
= basegfx::utils::distort(aPolyPolygon, aPolyPolygon.getB2DRange(),
|
|
{ -aShapeBounds.getWidth() / 2, -aShapeBounds.getHeight() / 2 },
|
|
{ aShapeBounds.getWidth() / 2, -aShapeBounds.getHeight() / 2 },
|
|
{ -aShapeBounds.getWidth() / 2, aShapeBounds.getHeight() / 2 },
|
|
{ aShapeBounds.getWidth() / 2, aShapeBounds.getHeight() / 2 });
|
|
|
|
if (pSdrObject->IsClosedObj() && !pSdrObject->IsEdgeObj() && pSdrObject->HasFillStyle())
|
|
{
|
|
basegfx::triangulator::B2DTriangleVector aTriangleVector;
|
|
// iterate over the polygons of the shape and create representations for them
|
|
for (const auto& rPolygon : std::as_const(aPolyPolygon))
|
|
{
|
|
// if the polygon is closed it will be represented by triangles
|
|
if (rPolygon.isClosed())
|
|
{
|
|
basegfx::triangulator::B2DTriangleVector aTempTriangleVector(
|
|
basegfx::triangulator::triangulate(rPolygon));
|
|
aTriangleVector.insert(aTriangleVector.end(), aTempTriangleVector.begin(),
|
|
aTempTriangleVector.end());
|
|
}
|
|
else // otherwise it will be an edge representation (example: smile line of the smiley shape)
|
|
{
|
|
addEdgeShapeToBody(rPolygon, pBody.get(), fDensity, fFriction,
|
|
static_cast<float>(fDefaultStaticBodyBounciness), mfScaleFactor);
|
|
}
|
|
}
|
|
addTriangleVectorToBody(aTriangleVector, pBody.get(), fDensity, fFriction,
|
|
static_cast<float>(fDefaultStaticBodyBounciness), mfScaleFactor);
|
|
}
|
|
else
|
|
{
|
|
addEdgeShapeToBody(aPolyPolygon, pBody.get(), fDensity, fFriction,
|
|
static_cast<float>(fDefaultStaticBodyBounciness), mfScaleFactor);
|
|
}
|
|
|
|
return std::make_shared<box2DBody>(pBody, mfScaleFactor);
|
|
}
|
|
|
|
box2DBody::box2DBody(std::shared_ptr<b2Body> pBox2DBody, double fScaleFactor)
|
|
: mpBox2DBody(std::move(pBox2DBody))
|
|
, mfScaleFactor(fScaleFactor)
|
|
{
|
|
}
|
|
|
|
::basegfx::B2DPoint box2DBody::getPosition() const
|
|
{
|
|
b2Vec2 aPosition = mpBox2DBody->GetPosition();
|
|
double fX = static_cast<double>(aPosition.x) / mfScaleFactor;
|
|
double fY = static_cast<double>(aPosition.y) / -mfScaleFactor;
|
|
return ::basegfx::B2DPoint(fX, fY);
|
|
}
|
|
|
|
void box2DBody::setPosition(const basegfx::B2DPoint& rPos)
|
|
{
|
|
mpBox2DBody->SetTransform(convertB2DPointToBox2DVec2(rPos, mfScaleFactor),
|
|
mpBox2DBody->GetAngle());
|
|
}
|
|
|
|
void box2DBody::setPositionByLinearVelocity(const basegfx::B2DPoint& rDesiredPos,
|
|
const double fPassedTime)
|
|
{
|
|
// kinematic bodies are not affected by other bodies, but unlike static ones can still have velocity
|
|
if (mpBox2DBody->GetType() != b2_kinematicBody)
|
|
mpBox2DBody->SetType(b2_kinematicBody);
|
|
|
|
::basegfx::B2DPoint aCurrentPos = getPosition();
|
|
// calculate the velocity needed to reach the rDesiredPos in the given time frame
|
|
::basegfx::B2DVector aVelocity = (rDesiredPos - aCurrentPos) / fPassedTime;
|
|
|
|
setLinearVelocity(aVelocity);
|
|
}
|
|
|
|
void box2DBody::setAngleByAngularVelocity(const double fDesiredAngle, const double fPassedTime)
|
|
{
|
|
// kinematic bodies are not affected by other bodies, but unlike static ones can still have velocity
|
|
if (mpBox2DBody->GetType() != b2_kinematicBody)
|
|
mpBox2DBody->SetType(b2_kinematicBody);
|
|
|
|
double fDeltaAngle = fDesiredAngle - getAngle();
|
|
|
|
// temporary hack for repeating animation effects
|
|
while (fDeltaAngle > 180
|
|
|| fDeltaAngle < -180) // if it is bigger than 180 opposite rotation is actually closer
|
|
fDeltaAngle += fDeltaAngle > 0 ? -360 : +360;
|
|
|
|
double fAngularVelocity = fDeltaAngle / fPassedTime;
|
|
setAngularVelocity(fAngularVelocity);
|
|
}
|
|
|
|
void box2DBody::setLinearVelocity(const ::basegfx::B2DVector& rVelocity)
|
|
{
|
|
b2Vec2 aVelocity = { static_cast<float>(rVelocity.getX() * mfScaleFactor),
|
|
static_cast<float>(rVelocity.getY() * -mfScaleFactor) };
|
|
mpBox2DBody->SetLinearVelocity(aVelocity);
|
|
}
|
|
|
|
void box2DBody::setAngularVelocity(const double fAngularVelocity)
|
|
{
|
|
float fBox2DAngularVelocity = static_cast<float>(basegfx::deg2rad(-fAngularVelocity));
|
|
mpBox2DBody->SetAngularVelocity(fBox2DAngularVelocity);
|
|
}
|
|
|
|
void box2DBody::setCollision(const bool bCanCollide)
|
|
{
|
|
// collision have to be set for each fixture of the body individually
|
|
for (b2Fixture* pFixture = mpBox2DBody->GetFixtureList(); pFixture;
|
|
pFixture = pFixture->GetNext())
|
|
{
|
|
b2Filter aFilter = pFixture->GetFilterData();
|
|
// 0xFFFF means collides with everything
|
|
// 0x0000 means collides with nothing
|
|
aFilter.maskBits = bCanCollide ? 0xFFFF : 0x0000;
|
|
pFixture->SetFilterData(aFilter);
|
|
}
|
|
}
|
|
|
|
double box2DBody::getAngle() const
|
|
{
|
|
double fAngle = static_cast<double>(mpBox2DBody->GetAngle());
|
|
return ::basegfx::rad2deg(-fAngle);
|
|
}
|
|
|
|
void box2DBody::setAngle(const double fAngle)
|
|
{
|
|
mpBox2DBody->SetTransform(mpBox2DBody->GetPosition(), ::basegfx::deg2rad(-fAngle));
|
|
}
|
|
|
|
void box2DBody::setDensityAndRestitution(const double fDensity, const double fRestitution)
|
|
{
|
|
// density and restitution have to be set for each fixture of the body individually
|
|
for (b2Fixture* pFixture = mpBox2DBody->GetFixtureList(); pFixture;
|
|
pFixture = pFixture->GetNext())
|
|
{
|
|
pFixture->SetDensity(static_cast<float>(fDensity));
|
|
pFixture->SetRestitution(static_cast<float>(fRestitution));
|
|
}
|
|
// without resetting the massdata of the body, density change won't take effect
|
|
mpBox2DBody->ResetMassData();
|
|
}
|
|
|
|
void box2DBody::setRestitution(const double fRestitution)
|
|
{
|
|
for (b2Fixture* pFixture = mpBox2DBody->GetFixtureList(); pFixture;
|
|
pFixture = pFixture->GetNext())
|
|
{
|
|
pFixture->SetRestitution(static_cast<float>(fRestitution));
|
|
}
|
|
}
|
|
|
|
void box2DBody::setType(box2DBodyType eType)
|
|
{
|
|
mpBox2DBody->SetType(getBox2DInternalBodyType(eType));
|
|
}
|
|
|
|
box2DBodyType box2DBody::getType() const { return getBox2DLOBodyType(mpBox2DBody->GetType()); }
|
|
}
|
|
|
|
/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */
|