Files
watrbx-game-engine/App/tool/MoveResizeJoinTool.cpp
T
2025-09-18 17:55:52 -04:00

609 lines
22 KiB
C++

/* Copyright 2003-2005 ROBLOX Corporation, All Rights Reserved */
#include "stdafx.h"
#include "Tool/MoveResizeJoinTool.h"
#include "V8DataModel/Workspace.h"
#include "V8DataModel/PartInstance.h"
#include "V8DataModel/ChangeHistory.h"
#include "Tool/Dragger.h"
#include "V8World/World.h"
#include "V8World/ContactManager.h"
#include "Util/Math.h"
#include "Util/HitTest.h"
#include "Util/SoundService.h"
#include "AppDraw/DrawAdorn.h"
#include "V8DataModel/Camera.h"
#include "V8World/Tolerance.h"
DYNAMIC_FASTFLAG(RestoreTransparencyOnToolChange)
DYNAMIC_FASTFLAG(DraggerUsesNewPartOnDuplicate)
FASTFLAGVARIABLE(StudioUseDraggerGrid, true)
//Deprecate FormFactor
DYNAMIC_FASTFLAG(FormFactorDeprecated)
namespace RBX {
const char* const sMoveResizeJoinTool = "MoveResizeJoin";
/////////////////////////////////////////////////////////////////////
//
// MoveResizeJoin TOOL
//
weak_ptr<PVInstance> MoveResizeJoinTool::scalingPart;
void MoveResizeJoinTool::findTargetPV(const shared_ptr<InputObject>& inputObject)
{
ServiceClient< Selection > selection(this->workspace);
Instances::const_iterator it;
for (it = selection->begin(); it != selection->end(); ++it) {
if (PartInstance* part = Instance::fastDynamicCast<PartInstance>(it->get())) {
RbxRay gridRay(getUnitMouseRay(inputObject));
if (HandleHitTest::hitTestHandleLocal( part->getPartPrimitive()->getExtentsLocal(),
part->getLocation(),
HANDLE_RESIZE,
gridRay,
hitPointGrid,
localNormalId,
part->getResizeHandleMask().normalIdMask))
{
targetPV = shared_from(part);
if (DFFlag::DraggerUsesNewPartOnDuplicate)
scalingPart = shared_from(part);
return;
}
}
}
targetPV.reset();
};
void MoveResizeJoinTool::render3dAdorn(Adorn* adorn)
{
renderHoverOver(adorn);
ServiceClient< Selection > selection(this->workspace);
shared_ptr<PartInstance> hoveredPart = targetPV.expired() ? shared_ptr<PartInstance>() : shared_polymorphic_downcast<PartInstance>(targetPV.lock());
Instances::const_iterator it;
for (it = selection->begin(); it != selection->end(); ++it) {
if (PartInstance* part = Instance::fastDynamicCast<PartInstance>(it->get())) {
RBX::DrawAdorn::handles3d(
part->getPartSizeXml(),
part->getCoordinateFrame(),
adorn,
HANDLE_RESIZE,
workspace->getConstCamera()->coordinateFrame().translation,
DrawAdorn::resizeColor(),
true,
part->getResizeHandleMask().normalIdMask,
hoveredPart.get() == part ? localNormalId : NORM_UNDEFINED,
Color3(0.3f, 0.7f, 1.0f));
}
}
}
void MoveResizeJoinTool::render2d(Adorn* adorn)
{
//Super::render2d(adorn);
ServiceClient< Selection > selection(this->workspace);
shared_ptr<PartInstance> hoveredPart = targetPV.expired() ? shared_ptr<PartInstance>() : shared_polymorphic_downcast<PartInstance>(targetPV.lock());
Instances::const_iterator it;
for (it = selection->begin(); it != selection->end(); ++it) {
if (PartInstance* part = Instance::fastDynamicCast<PartInstance>(it->get())) {
DrawAdorn::handles2d(
part->getPartSizeXml(),
part->getCoordinateFrame(),
*(workspace->getConstCamera()),
adorn,
HANDLE_RESIZE,
DrawAdorn::resizeColor(),
true,
part->getResizeHandleMask().normalIdMask);
if ( FFlag::StudioUseDraggerGrid && showDraggerGrid && AdvArrowToolBase::advGridMode != DRAG::OFF &&
part == hoveredPart.get() &&
dragging &&
part->getPartType() != BALL_PART &&
part->getPartType() != CYLINDER_PART)
{
CoordinateFrame cf = part->getCoordinateFrame();
Vector3 cameraDirection = workspace->getConstCamera()->getCameraCoordinateFrame().vectorToWorldSpace(Vector3::unitZ());
Vector3 bodyXWorld = part->getCoordinateFrame().vectorToWorldSpace(Vector3::unitX());
Vector3 bodyYWorld = part->getCoordinateFrame().vectorToWorldSpace(Vector3::unitY());
Vector3 bodyZWorld = part->getCoordinateFrame().vectorToWorldSpace(Vector3::unitZ());
cf.translation = origPartPosition.translation;
Vector4 bounds = Vector4::zero();
float integerPart = 0.0f;
Color3 gridColor = DrawAdorn::resizeColor();
int boxesPerStud = smallGridBoxesPerStud();
switch( localNormalId )
{
case NORM_X:
modff(part->getPartSizeUi().x * 0.2f, &integerPart);
cf.translation += cf.vectorToWorldSpace(-origPartSize.x * 0.5f * Vector3::unitX());
// Show grid on best plane based on camera direction
if( fabs(bodyZWorld.dot(cameraDirection)) > fabs(bodyYWorld.dot(cameraDirection)) ) // z is normal to grid
{
bounds.x = (integerPart + 1.0f) * 5.0f; // max x-dir
bounds.z = 0.0f; // min x-dir
bounds.y = part->getPartSizeUi().y * 0.5f; // max y-dir
bounds.w = -bounds.y; // min y-dir
DrawAdorn::surfaceGridAtCoord(adorn, cf, bounds, Vector3::unitX(), Vector3::unitY(), gridColor, boxesPerStud);
}
else // y is normal to grid
{
bounds.x = part->getPartSizeUi().z * 0.5f; // max x-dir
bounds.z = -bounds.x; // min x-dir
bounds.y = (integerPart + 1.0f) * 5.0f; // max y-dir
bounds.w = 0.0f; // min y-dir
DrawAdorn::surfaceGridAtCoord(adorn, cf, bounds, Vector3::unitZ(), Vector3::unitX(), gridColor, boxesPerStud);
}
break;
case NORM_X_NEG:
modff(part->getPartSizeUi().x * 0.2f, &integerPart);
cf.translation += cf.vectorToWorldSpace(origPartSize.x * 0.5f * Vector3::unitX());
// Show grid on best plane based on camera direction
if( fabs(bodyZWorld.dot(cameraDirection)) > fabs(bodyYWorld.dot(cameraDirection)) )
{
bounds.x = 0.0f; // max x-dir
bounds.z = -(integerPart + 1.0f) * 5.0f; // min x-dir
bounds.y = part->getPartSizeUi().y * 0.5f; // max y-dir
bounds.w = -bounds.y; // min y-dir
DrawAdorn::surfaceGridAtCoord(adorn, cf, bounds, Vector3::unitX(), Vector3::unitY(), gridColor, boxesPerStud);
}
else
{
bounds.x = part->getPartSizeUi().z * 0.5f; // max x-dir
bounds.z = -bounds.x; // min x-dir
bounds.y = 0.0f; // max y-dir
bounds.w = -(integerPart + 1.0f) * 5.0f; // min y-dir
DrawAdorn::surfaceGridAtCoord(adorn, cf, bounds, Vector3::unitZ(), Vector3::unitX(), gridColor, boxesPerStud);
}
break;
case NORM_Y:
// only allow one stud y-dir resize for brick form factor
modff(part->getPartSizeUi().y * 0.2f, &integerPart);
cf.translation += cf.vectorToWorldSpace(-origPartSize.y * 0.5f * Vector3::unitY());
// Show grid on best plane based on camera direction
if( fabs(bodyZWorld.dot(cameraDirection)) > fabs(bodyXWorld.dot(cameraDirection)) )
{
bounds.x = part->getPartSizeUi().x * 0.5f; // max x-dir
bounds.z = -bounds.x; // min x-dir
bounds.y = (integerPart + 1.0f) * 5.0f; // max y-dir
bounds.w = 0.0f; // min y-dir
DrawAdorn::surfaceGridAtCoord(adorn, cf, bounds, Vector3::unitX(), Vector3::unitY(), gridColor, boxesPerStud);
}
else
{
bounds.x = (integerPart + 1.0f) * 5.0f; // max x-dir
bounds.z = 0.0f; // min x-dir
bounds.y = part->getPartSizeUi().z * 0.5f; // max y-dir
bounds.w = -bounds.y; // min y-dir
DrawAdorn::surfaceGridAtCoord(adorn, cf, bounds, Vector3::unitY(), Vector3::unitZ(), gridColor, boxesPerStud);
}
break;
case NORM_Y_NEG:
// only allow one stud y-dir resize for brick form factor
modff(part->getPartSizeUi().y * 0.2f, &integerPart);
cf.translation += cf.vectorToWorldSpace(origPartSize.y * 0.5f * Vector3::unitY());
// Show grid on best plane based on camera direction
if( fabs(bodyZWorld.dot(cameraDirection)) > fabs(bodyXWorld.dot(cameraDirection)) )
{
bounds.x = part->getPartSizeUi().x * 0.5f; // max x-dir
bounds.z = -bounds.x; // min x-dir
bounds.y = 0.0f; // max y-dir
bounds.w = -(integerPart + 1.0f) * 5.0f; // min y-dir
DrawAdorn::surfaceGridAtCoord(adorn, cf, bounds, Vector3::unitX(), Vector3::unitY(), gridColor, boxesPerStud);
}
else
{
bounds.x = 0.0f; // max x-dir
bounds.z = -(integerPart + 1.0f) * 5.0f; // min x-dir
bounds.y = part->getPartSizeUi().z * 0.5f; // max y-dir
bounds.w = -bounds.y; // min y-dir
DrawAdorn::surfaceGridAtCoord(adorn, cf, bounds, Vector3::unitY(), Vector3::unitZ(), gridColor, boxesPerStud);
}
break;
case NORM_Z:
modff(part->getPartSizeUi().z * 0.2f, &integerPart);
cf.translation += cf.vectorToWorldSpace(-origPartSize.z * 0.5f * Vector3::unitZ());
// Show grid on best plane based on camera direction
if( fabs(bodyYWorld.dot(cameraDirection)) > fabs(bodyXWorld.dot(cameraDirection)) )
{
bounds.x = (integerPart + 1.0f) * 5.0f; // max x-dir
bounds.z = 0.0f; // min x-dir
bounds.y = part->getPartSizeUi().x * 0.5f; // max y-dir
bounds.w = -bounds.y; // min y-dir
DrawAdorn::surfaceGridAtCoord(adorn, cf, bounds, Vector3::unitZ(), Vector3::unitX(), gridColor, boxesPerStud);
}
else
{
bounds.x = part->getPartSizeUi().y * 0.5f; // max x-dir
bounds.z = -bounds.x; // min x-dir
bounds.y = (integerPart + 1.0f) * 5.0f; // max y-dir
bounds.w = 0.0f; // min y-dir
DrawAdorn::surfaceGridAtCoord(adorn, cf, bounds, Vector3::unitY(), Vector3::unitZ(), gridColor, boxesPerStud);
}
break;
case NORM_Z_NEG:
modff(part->getPartSizeUi().z * 0.2f, &integerPart);
cf.translation += cf.vectorToWorldSpace(origPartSize.z * 0.5f * Vector3::unitZ());
// Show grid on best plane based on camera direction
if( fabs(bodyYWorld.dot(cameraDirection)) > fabs(bodyXWorld.dot(cameraDirection)) )
{
bounds.x = 0.0f; // max x-dir
bounds.z = -(integerPart + 1.0f) * 5.0f; // min x-dir
bounds.y = part->getPartSizeUi().x * 0.5f; // max y-dir
bounds.w = -bounds.y; // min y-dir
DrawAdorn::surfaceGridAtCoord(adorn, cf, bounds, Vector3::unitZ(), Vector3::unitX(), gridColor, boxesPerStud);
}
else
{
bounds.x = part->getPartSizeUi().y * 0.5f; // max x-dir
bounds.z = -bounds.x; // min x-dir
bounds.y = 0.0f; // max y-dir
bounds.w = -(integerPart + 1.0f) * 5.0f; // min y-dir
DrawAdorn::surfaceGridAtCoord(adorn, cf, bounds, Vector3::unitY(), Vector3::unitZ(), gridColor, boxesPerStud);
}
break;
default:
break;
}
}
}
}
}
void MoveResizeJoinTool::onMouseHover(const shared_ptr<InputObject>& inputObject)
{
RBXASSERT(!captured());
findTargetPV(inputObject);
overHandle = (!targetPV.expired());
Super::onMouseHover(inputObject);
}
void MoveResizeJoinTool::onMouseIdle(const shared_ptr<InputObject>& inputObject)
{
Super::onMouseIdle(inputObject);
if (dragging) {
;
}
else if (captured()) {
cursor = "advClosed-hand";
}
else if (overHandle) {
cursor = "advCursor-openedHand";
}
else {
cursor = Super::getCursorName();
}
}
shared_ptr<MouseCommand> MoveResizeJoinTool::onMouseDown(const shared_ptr<InputObject>& inputObject)
{
findTargetPV(inputObject);
if ( targetPV.expired())
{
return Super::onMouseDown(inputObject);
}
else
{
down = inputObject->get2DPosition();
movePerp = 0;
overHandle = true;
shared_ptr<PartInstance> part = shared_polymorphic_downcast<PartInstance>(targetPV.lock());
// try to prevent the part from moving due to physics effects during the resize operation
part->setDragging(true);
// set this based on Grid setting
origPartPosition = part->getCoordinateFrame();
origPartSize = part->getPartSizeUi();
// if collision checks are turned off then, make part transparent
if (!RBX::AdvArrowTool::advCollisionCheckMode)
{
if (DFFlag::RestoreTransparencyOnToolChange)
AdvArrowToolBase::savePartTransparency(part);
else
{
origPartTransparency = part->getTransparencyUi();
part->setTransparency(0.5);
}
}
capture();
return shared_from(this); // captured is true....
}
}
float MoveResizeJoinTool::moveIncrement( void )
{
switch( AdvArrowTool::advGridMode )
{
case DRAG::OFF:
return 0.01f;
case DRAG::QUARTER_STUD:
return 0.2f;
case DRAG::ONE_STUD:
default:
{
if (DFFlag::FormFactorDeprecated)
{
return 1.0f;
}
shared_ptr<PartInstance> part = shared_polymorphic_downcast<PartInstance>(targetPV.lock());
switch(part->getFormFactor())
{
case PartInstance::BRICK:
return localNormalId % 3 == 1 ? 1.2f : 1.0f;
case PartInstance::PLATE:
return localNormalId % 3 == 1 ? 0.4f : 1.0f;
default:
return 1.0f;
}
}
}
}
int MoveResizeJoinTool::smallGridBoxesPerStud( void )
{
switch( AdvArrowTool::advGridMode )
{
case DRAG::OFF:
return 0;
case DRAG::QUARTER_STUD:
return 5;
case DRAG::ONE_STUD:
default:
return 1;
}
}
shared_ptr<MouseCommand> MoveResizeJoinTool::onKeyDown(const shared_ptr<InputObject>& inputObject)
{
Super::onKeyDown(inputObject);
return shared_from(this);
}
void MoveResizeJoinTool::onMouseMove(const shared_ptr<InputObject>& inputObject)
{
Super::onMouseMove(inputObject);
if (DFFlag::DraggerUsesNewPartOnDuplicate && scalingPart.lock() != targetPV.lock())
{
shared_ptr<PartInstance> origPart = shared_dynamic_cast<PartInstance>(targetPV.lock());
origPart->setDragging(false);
targetPV = scalingPart;
shared_ptr<PartInstance> part = shared_dynamic_cast<PartInstance>(targetPV.lock());
part->setDragging(true);
origPartPosition = origPartPosition + Vector3(0.0f, part->getCoordinateFrame().translation.y - origPart->getCoordinateFrame().translation.y, 0.0f);
}
if (!targetPV.expired()) {
Vector3 worldNormal = Math::getWorldNormal(localNormalId, targetPV.lock()->getLocation());
RbxRay axisRay = RbxRay::fromOriginAndDirection(hitPointGrid, worldNormal);
RbxRay gridRay = getUnitMouseRay(inputObject);
Vector3 closePoint = RBX::Math::closestPointOnRay(axisRay, gridRay);
Vector3 handleToCurrent = closePoint - hitPointGrid;
float distanceAlongAxis = axisRay.direction().dot(handleToCurrent);
capturedDrag(distanceAlongAxis);
}
// During drag set the grid adornment based on current grid setting
dragging = true;
}
shared_ptr<MouseCommand> MoveResizeJoinTool::onMouseUp(const shared_ptr<InputObject>& inputObject)
{
dragging = false;
if (DFFlag::RestoreTransparencyOnToolChange)
AdvArrowToolBase::restoreSavedPartsTransparency();
shared_ptr<PartInstance> part = shared_polymorphic_downcast<PartInstance>(targetPV.lock());
if( part )
{
if (!RBX::AdvArrowTool::advCollisionCheckMode && !DFFlag::RestoreTransparencyOnToolChange)
part->setTransparency(origPartTransparency);
// resize is done. Let physics take effect again.
part->setDragging(false);
origPartPosition = part->getCoordinateFrame();
origPartSize = part->getPartSizeUi();
}
if (captured()) {
releaseCapture();
// Save the undo/redo state - if deleted in the process, then don't
if (!targetPV.expired()) {
workspace->getDataState().setDirty(true);
}
}
//baseclass will take care of setting waypoint for undo/redo
Super::onMouseUp(inputObject);
return shared_ptr<MouseCommand>();
}
void MoveResizeJoinTool::capturedDrag(float axisDelta)
{
if (targetPV.expired()) {
RBXASSERT(0); // should be caught higher up
}
else {
// Debug version confirms the dynamic_cast of targetInstance
shared_ptr<PartInstance> part = shared_polymorphic_downcast<PartInstance>(targetPV.lock());
resizeFloat(part, localNormalId, axisDelta, RBX::AdvArrowTool::advCollisionCheckMode);
}
}
bool MoveResizeJoinTool::resizeFloat(shared_ptr<PartInstance> part, NormalId localNormalId, float amount, bool checkIntersection)
{
part->destroyJoints();
bool result = advResizeImpl(part, localNormalId, amount, checkIntersection);
part->join();
return result;
}
bool MoveResizeJoinTool::advResizeImpl(shared_ptr<PartInstance> part, NormalId localNormalId, float amount, bool checkIntersection)
{
amount = G3D::iRound(amount / moveIncrement()) * moveIncrement();
Vector3 originalSizeXml = origPartSize;
CoordinateFrame originalPosition = origPartPosition;
Vector3 deltaSizeUi;
float posNeg = (localNormalId < NORM_X_NEG) ? 1.0f : -1.0f;
if (part->hasThreeDimensionalSize())
{
deltaSizeUi[localNormalId % 3] = amount;
}
else
{
RBXASSERT(part->getFormFactor() == PartInstance::SYMETRIC);
deltaSizeUi = amount * Vector3(originalSizeXml.x / originalSizeXml.y, 1, originalSizeXml.z / originalSizeXml.y);
}
Vector3 newSizeUi = originalSizeXml + deltaSizeUi;
if (!part->hasThreeDimensionalSize())
{
if (fabs(newSizeUi.x) < fabs(newSizeUi.z) && fabs(newSizeUi.x) < fabs(newSizeUi.y))
{
float minDimension = part->getMinimumXOrZDimension();
if (newSizeUi.x < minDimension)
newSizeUi = Vector3(minDimension, minDimension, minDimension) * Vector3(1.0f, originalSizeXml.y / originalSizeXml.x, originalSizeXml.z / originalSizeXml.x);
}
else if (fabs(newSizeUi.z) < fabs(newSizeUi.y))
{
float minDimension = part->getMinimumXOrZDimension();
if (newSizeUi.z < minDimension)
newSizeUi = Vector3(minDimension, minDimension, minDimension) * Vector3(originalSizeXml.x / originalSizeXml.z, originalSizeXml.y / originalSizeXml.z, 1.0f);
}
else
{
float minDimension = part->getMinimumYDimension();
if (newSizeUi.y < minDimension)
newSizeUi = Vector3(minDimension, minDimension, minDimension) * Vector3(originalSizeXml.x / originalSizeXml.y, 1.0f, originalSizeXml.z / originalSizeXml.y);
}
}
else
{
if (DFFlag::FormFactorDeprecated) {
if (AdvArrowTool::advGridMode != DRAG::OFF) {
double gridSize = 0.2f;
if (AdvArrowTool::advGridMode == DRAG::ONE_STUD) gridSize = 1.0f;
/*
To explain the logic below:
If the new size is greater than the grid size, then don't do anything. Just set the new size. Otherwise, use a separate logic to get the size:
newSizeUi = newSizeUi < gridSize? (x) : newSizeUi;
If the fmod(originalSizeXml,gridSize) is greater than the minimum dimensions (should be 0.2) then set the part size to that fmod - otherwise, set to the gridSize.
This logic is there for the cases where fmod(originalSizeXml,gridSize) is very close to 0 - as in, the originalSizeXml is an integer multiple of the gridSize.
In this case we want the minimum size not to be 0, but the gridSize.
*/
newSizeUi.x = newSizeUi.x < gridSize ? (fmod(originalSizeXml.x,gridSize) >= part->getMinimumXOrZDimension() ? fmod(originalSizeXml.x,gridSize) : gridSize) : newSizeUi.x;
newSizeUi.y = newSizeUi.y < gridSize ? (fmod(originalSizeXml.y,gridSize) >= part->getMinimumYDimension() ? fmod(originalSizeXml.y,gridSize) : gridSize) : newSizeUi.y;
newSizeUi.z = newSizeUi.z < gridSize ? (fmod(originalSizeXml.z,gridSize) >= part->getMinimumXOrZDimension() ? fmod(originalSizeXml.z,gridSize) : gridSize) : newSizeUi.z;
}
newSizeUi.x = newSizeUi.x < part->getMinimumXOrZDimension() ? part->getMinimumXOrZDimension() : newSizeUi.x;
newSizeUi.y = newSizeUi.y < part->getMinimumYDimension() ? part->getMinimumYDimension() : newSizeUi.y;
newSizeUi.z = newSizeUi.z < part->getMinimumXOrZDimension() ? part->getMinimumXOrZDimension() : newSizeUi.z;
} else {
newSizeUi.x = newSizeUi.x < part->getMinimumXOrZDimension() ? part->getMinimumXOrZDimension() : newSizeUi.x;
newSizeUi.y = newSizeUi.y < part->getMinimumYDimension() ? part->getMinimumYDimension() : newSizeUi.y;
newSizeUi.z = newSizeUi.z < part->getMinimumXOrZDimension() ? part->getMinimumXOrZDimension() : newSizeUi.z;
}
}
Vector3 deltaSizeXml = newSizeUi - originalSizeXml;
Vector3 deltaSizeWorld = originalPosition.rotation * deltaSizeXml;
Vector3 newTranslation;
if (!part->hasThreeDimensionalSize())
{
Vector3 direction;
direction[localNormalId % 3] = 1.0f;
Vector3 deltaSizeWorldY = originalPosition.rotation * (deltaSizeXml * direction);
newTranslation = originalPosition.translation;
newTranslation = newTranslation + 0.5 * deltaSizeWorldY * posNeg;
}
else
{
newTranslation = originalPosition.translation + 0.5f * deltaSizeWorld * posNeg;
}
CoordinateFrame newCoord(originalPosition.rotation, newTranslation);
Vector3 preCollisionSizeXml = part->getPartSizeXml();
CoordinateFrame preCollisionPosition = part->getCoordinateFrame();
part->setCoordinateFrame(newCoord);
part->setPartSizeXml(newSizeUi);
if (checkIntersection)
{
if (ContactManager* contactManager = Workspace::getContactManagerIfInWorkspace(part.get())) {
if (Dragger::intersectingWorldOrOthers( *part,
*contactManager,
Tolerance::maxOverlapAllowedForResize(),
Dragger::maxDragDepth())) {
part->setCoordinateFrame(preCollisionPosition);
part->setPartSizeXml(preCollisionSizeXml);
return false;
}
}
}
return true;
}
void MoveResizeJoinTool::setSelection(shared_ptr<RBX::Instance> oldSelection, shared_ptr<RBX::Instance> newSelection)
{
if (shared_ptr<RBX::PVInstance> partInstance = RBX::Instance::fastSharedDynamicCast<RBX::PVInstance>(oldSelection))
{
if (partInstance == scalingPart.lock())
{
if (shared_ptr<RBX::PVInstance> pvInstance = RBX::Instance::fastSharedDynamicCast<RBX::PVInstance>((newSelection)))
{
scalingPart = pvInstance;
if (DFFlag::RestoreTransparencyOnToolChange && !RBX::AdvArrowTool::advCollisionCheckMode)
{
shared_ptr<PartInstance> selectedPart = RBX::Instance::fastSharedDynamicCast<RBX::PartInstance>(newSelection);
AdvArrowToolBase::savePartTransparency(selectedPart);
}
}
}
}
}
} // namespace