feat: expand editor scripting asset and viewport flow

This commit is contained in:
2026-04-03 13:22:30 +08:00
parent ed8c27fde2
commit a05d0b80a2
124 changed files with 10397 additions and 1737 deletions

View File

@@ -19,6 +19,21 @@ constexpr float kRotateGizmoViewRadiusPixels = 106.0f;
constexpr float kRotateGizmoHoverThresholdPixels = 9.0f;
constexpr float kRotateGizmoAngleFillMinRadians = 0.01f;
Math::Vector3 GetBaseRotateAxisVector(SceneViewportRotateGizmoAxis axis) {
switch (axis) {
case SceneViewportRotateGizmoAxis::X:
return Math::Vector3::Right();
case SceneViewportRotateGizmoAxis::Y:
return Math::Vector3::Up();
case SceneViewportRotateGizmoAxis::Z:
return Math::Vector3::Forward();
case SceneViewportRotateGizmoAxis::View:
case SceneViewportRotateGizmoAxis::None:
default:
return Math::Vector3::Zero();
}
}
Math::Vector3 NormalizeVector3(const Math::Vector3& value, const Math::Vector3& fallback) {
return value.SqrMagnitude() <= Math::EPSILON ? fallback : value.Normalized();
}
@@ -30,6 +45,22 @@ bool IsMouseInsideViewport(const SceneViewportRotateGizmoContext& context) {
context.mousePosition.y <= context.viewportSize.y;
}
Math::Quaternion ComputeStableWorldRotation(const Components::GameObject* gameObject) {
if (gameObject == nullptr || gameObject->GetTransform() == nullptr) {
return Math::Quaternion::Identity();
}
const Components::TransformComponent* transform = gameObject->GetTransform();
Math::Quaternion worldRotation = transform->GetLocalRotation();
for (const Components::TransformComponent* parent = transform->GetParent();
parent != nullptr;
parent = parent->GetParent()) {
worldRotation = parent->GetLocalRotation() * worldRotation;
}
return worldRotation.Normalized();
}
Math::Color GetRotateAxisBaseColor(SceneViewportRotateGizmoAxis axis) {
switch (axis) {
case SceneViewportRotateGizmoAxis::X:
@@ -48,14 +79,13 @@ Math::Color GetRotateAxisBaseColor(SceneViewportRotateGizmoAxis axis) {
Math::Vector3 GetRotateAxisVector(
SceneViewportRotateGizmoAxis axis,
const SceneViewportOverlayData& overlay) {
const SceneViewportOverlayData& overlay,
const Math::Quaternion& axisOrientation) {
switch (axis) {
case SceneViewportRotateGizmoAxis::X:
return Math::Vector3::Right();
case SceneViewportRotateGizmoAxis::Y:
return Math::Vector3::Up();
case SceneViewportRotateGizmoAxis::Z:
return Math::Vector3::Forward();
return NormalizeVector3(axisOrientation * GetBaseRotateAxisVector(axis), GetBaseRotateAxisVector(axis));
case SceneViewportRotateGizmoAxis::View:
return NormalizeVector3(overlay.cameraForward, Math::Vector3::Forward());
case SceneViewportRotateGizmoAxis::None:
@@ -67,20 +97,21 @@ Math::Vector3 GetRotateAxisVector(
bool GetRotateRingBasis(
SceneViewportRotateGizmoAxis axis,
const SceneViewportOverlayData& overlay,
const Math::Quaternion& axisOrientation,
Math::Vector3& outBasisA,
Math::Vector3& outBasisB) {
switch (axis) {
case SceneViewportRotateGizmoAxis::X:
outBasisA = Math::Vector3::Up();
outBasisB = Math::Vector3::Forward();
outBasisA = NormalizeVector3(axisOrientation * Math::Vector3::Up(), Math::Vector3::Up());
outBasisB = NormalizeVector3(axisOrientation * Math::Vector3::Forward(), Math::Vector3::Forward());
return true;
case SceneViewportRotateGizmoAxis::Y:
outBasisA = Math::Vector3::Forward();
outBasisB = Math::Vector3::Right();
outBasisA = NormalizeVector3(axisOrientation * Math::Vector3::Forward(), Math::Vector3::Forward());
outBasisB = NormalizeVector3(axisOrientation * Math::Vector3::Right(), Math::Vector3::Right());
return true;
case SceneViewportRotateGizmoAxis::Z:
outBasisA = Math::Vector3::Right();
outBasisB = Math::Vector3::Up();
outBasisA = NormalizeVector3(axisOrientation * Math::Vector3::Right(), Math::Vector3::Right());
outBasisB = NormalizeVector3(axisOrientation * Math::Vector3::Up(), Math::Vector3::Up());
return true;
case SceneViewportRotateGizmoAxis::View:
outBasisA = NormalizeVector3(overlay.cameraRight, Math::Vector3::Right());
@@ -147,11 +178,12 @@ SceneViewportRotateGizmoAxis GetRotateAxisForIndex(size_t index) {
bool TryComputeRingAngleFromWorldDirection(
SceneViewportRotateGizmoAxis axis,
const SceneViewportOverlayData& overlay,
const Math::Quaternion& axisOrientation,
const Math::Vector3& directionWorld,
float& outAngle) {
Math::Vector3 basisA = Math::Vector3::Zero();
Math::Vector3 basisB = Math::Vector3::Zero();
if (!GetRotateRingBasis(axis, overlay, basisA, basisB)) {
if (!GetRotateRingBasis(axis, overlay, axisOrientation, basisA, basisB)) {
return false;
}
@@ -171,7 +203,7 @@ bool TryComputeRingAngleFromWorldDirection(
void SceneViewportRotateGizmo::Update(const SceneViewportRotateGizmoContext& context) {
BuildDrawData(context);
if (m_activeAxis == SceneViewportRotateGizmoAxis::None && IsMouseInsideViewport(context)) {
m_hoveredAxis = HitTestAxis(context.mousePosition);
m_hoveredAxis = EvaluateHit(context.mousePosition).axis;
} else if (m_activeAxis == SceneViewportRotateGizmoAxis::None) {
m_hoveredAxis = SceneViewportRotateGizmoAxis::None;
} else {
@@ -190,8 +222,8 @@ bool SceneViewportRotateGizmo::TryBeginDrag(const SceneViewportRotateGizmoContex
return false;
}
const Math::Vector3 pivotWorldPosition = context.selectedObject->GetTransform()->GetPosition();
const Math::Vector3 worldAxis = GetRotateAxisVector(m_hoveredAxis, context.overlay);
const Math::Vector3 pivotWorldPosition = context.pivotWorldPosition;
const Math::Vector3 worldAxis = GetRotateAxisVector(m_hoveredAxis, context.overlay, context.axisOrientation);
if (worldAxis.SqrMagnitude() <= Math::EPSILON) {
return false;
}
@@ -225,6 +257,7 @@ bool SceneViewportRotateGizmo::TryBeginDrag(const SceneViewportRotateGizmoContex
if (!TryComputeRingAngleFromWorldDirection(
m_hoveredAxis,
context.overlay,
context.axisOrientation,
startDirection,
startRingAngle)) {
return false;
@@ -238,12 +271,31 @@ bool SceneViewportRotateGizmo::TryBeginDrag(const SceneViewportRotateGizmoContex
m_activeAxis = m_hoveredAxis;
m_activeEntityId = context.selectedObject->GetID();
m_localSpace = context.localSpace && m_hoveredAxis != SceneViewportRotateGizmoAxis::View;
m_rotateAroundSharedPivot = context.rotateAroundSharedPivot;
m_activeWorldAxis = worldAxis.Normalized();
m_screenSpaceDrag = useScreenSpaceDrag;
m_dragPlane = dragPlane;
m_dragStartWorldRotation = context.selectedObject->GetTransform()->GetRotation();
m_dragStartRingAngle = startRingAngle;
m_dragCurrentDeltaRadians = 0.0f;
m_dragStartPivotWorldPosition = pivotWorldPosition;
m_dragObjects = context.selectedObjects;
if (m_dragObjects.empty()) {
m_dragObjects.push_back(context.selectedObject);
}
m_dragStartWorldPositions.clear();
m_dragStartWorldRotations.clear();
m_dragStartWorldPositions.reserve(m_dragObjects.size());
m_dragStartWorldRotations.reserve(m_dragObjects.size());
for (Components::GameObject* gameObject : m_dragObjects) {
if (gameObject != nullptr && gameObject->GetTransform() != nullptr) {
m_dragStartWorldPositions.push_back(gameObject->GetTransform()->GetPosition());
m_dragStartWorldRotations.push_back(gameObject->GetTransform()->GetRotation());
} else {
m_dragStartWorldPositions.push_back(Math::Vector3::Zero());
m_dragStartWorldRotations.push_back(Math::Quaternion::Identity());
}
}
RefreshHandleState();
return true;
}
@@ -251,7 +303,10 @@ bool SceneViewportRotateGizmo::TryBeginDrag(const SceneViewportRotateGizmoContex
void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext& context) {
if (m_activeAxis == SceneViewportRotateGizmoAxis::None ||
context.selectedObject == nullptr ||
context.selectedObject->GetID() != m_activeEntityId) {
context.selectedObject->GetID() != m_activeEntityId ||
m_dragObjects.empty() ||
m_dragObjects.size() != m_dragStartWorldPositions.size() ||
m_dragObjects.size() != m_dragStartWorldRotations.size()) {
return;
}
@@ -275,7 +330,7 @@ void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext&
return;
}
const Math::Vector3 pivotWorldPosition = context.selectedObject->GetTransform()->GetPosition();
const Math::Vector3 pivotWorldPosition = m_dragStartPivotWorldPosition;
const Math::Vector3 hitPoint = worldRay.GetPoint(hitDistance);
const Math::Vector3 currentDirection = Math::Vector3::ProjectOnPlane(hitPoint - pivotWorldPosition, m_activeWorldAxis);
if (currentDirection.SqrMagnitude() <= Math::EPSILON) {
@@ -285,6 +340,7 @@ void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext&
if (!TryComputeRingAngleFromWorldDirection(
m_activeAxis,
context.overlay,
context.axisOrientation,
currentDirection,
currentRingAngle)) {
return;
@@ -293,9 +349,37 @@ void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext&
const float deltaRadians = NormalizeSignedAngleRadians(currentRingAngle - m_dragStartRingAngle);
m_dragCurrentDeltaRadians = deltaRadians;
const Math::Quaternion deltaRotation = Math::Quaternion::FromAxisAngle(m_activeWorldAxis, deltaRadians);
context.selectedObject->GetTransform()->SetRotation(deltaRotation * m_dragStartWorldRotation);
BuildDrawData(context);
const Math::Quaternion worldDeltaRotation = Math::Quaternion::FromAxisAngle(m_activeWorldAxis, deltaRadians);
const Math::Vector3 localAxis = GetBaseRotateAxisVector(m_activeAxis);
const Math::Quaternion localDeltaRotation =
localAxis.SqrMagnitude() > Math::EPSILON
? Math::Quaternion::FromAxisAngle(localAxis, deltaRadians)
: Math::Quaternion::Identity();
for (size_t index = 0; index < m_dragObjects.size(); ++index) {
Components::GameObject* gameObject = m_dragObjects[index];
if (gameObject == nullptr || gameObject->GetTransform() == nullptr) {
continue;
}
if (m_rotateAroundSharedPivot) {
gameObject->GetTransform()->SetPosition(
m_dragStartPivotWorldPosition +
worldDeltaRotation * (m_dragStartWorldPositions[index] - m_dragStartPivotWorldPosition));
} else {
gameObject->GetTransform()->SetPosition(m_dragStartWorldPositions[index]);
}
if (m_localSpace && m_activeAxis != SceneViewportRotateGizmoAxis::View) {
gameObject->GetTransform()->SetRotation(m_dragStartWorldRotations[index] * localDeltaRotation);
} else {
gameObject->GetTransform()->SetRotation(worldDeltaRotation * m_dragStartWorldRotations[index]);
}
}
SceneViewportRotateGizmoContext drawContext = context;
drawContext.pivotWorldPosition = m_dragStartPivotWorldPosition;
if (drawContext.localSpace && drawContext.selectedObject != nullptr) {
drawContext.axisOrientation = ComputeStableWorldRotation(drawContext.selectedObject);
}
BuildDrawData(drawContext);
m_hoveredAxis = m_activeAxis;
RefreshHandleState();
}
@@ -312,10 +396,15 @@ void SceneViewportRotateGizmo::EndDrag(IUndoManager& undoManager) {
m_activeAxis = SceneViewportRotateGizmoAxis::None;
m_activeEntityId = 0;
m_screenSpaceDrag = false;
m_localSpace = false;
m_rotateAroundSharedPivot = false;
m_activeWorldAxis = Math::Vector3::Zero();
m_dragStartWorldRotation = Math::Quaternion::Identity();
m_dragStartRingAngle = 0.0f;
m_dragCurrentDeltaRadians = 0.0f;
m_dragStartPivotWorldPosition = Math::Vector3::Zero();
m_dragObjects.clear();
m_dragStartWorldPositions.clear();
m_dragStartWorldRotations.clear();
RefreshHandleState();
}
@@ -327,10 +416,15 @@ void SceneViewportRotateGizmo::CancelDrag(IUndoManager* undoManager) {
m_activeAxis = SceneViewportRotateGizmoAxis::None;
m_activeEntityId = 0;
m_screenSpaceDrag = false;
m_localSpace = false;
m_rotateAroundSharedPivot = false;
m_activeWorldAxis = Math::Vector3::Zero();
m_dragStartWorldRotation = Math::Quaternion::Identity();
m_dragStartRingAngle = 0.0f;
m_dragCurrentDeltaRadians = 0.0f;
m_dragStartPivotWorldPosition = Math::Vector3::Zero();
m_dragObjects.clear();
m_dragStartWorldPositions.clear();
m_dragStartWorldRotations.clear();
m_hoveredAxis = SceneViewportRotateGizmoAxis::None;
RefreshHandleState();
}
@@ -351,18 +445,57 @@ const SceneViewportRotateGizmoDrawData& SceneViewportRotateGizmo::GetDrawData()
return m_drawData;
}
SceneViewportRotateGizmoHitResult SceneViewportRotateGizmo::EvaluateHit(const Math::Vector2& mousePosition) const {
SceneViewportRotateGizmoHitResult result = {};
if (!m_drawData.visible) {
return result;
}
const float hoverThresholdSq = kRotateGizmoHoverThresholdPixels * kRotateGizmoHoverThresholdPixels;
for (const SceneViewportRotateGizmoHandleDrawData& handle : m_drawData.handles) {
if (!handle.visible) {
continue;
}
for (const SceneViewportRotateGizmoSegmentDrawData& segment : handle.segments) {
if (!segment.visible ||
(handle.axis != SceneViewportRotateGizmoAxis::View && !segment.frontFacing)) {
continue;
}
const float distanceSq = DistanceToSegmentSquared(mousePosition, segment.start, segment.end);
if (distanceSq > result.distanceSq || distanceSq > hoverThresholdSq) {
continue;
}
result.axis = handle.axis;
result.distanceSq = distanceSq;
}
}
return result;
}
void SceneViewportRotateGizmo::SetHoveredHandle(SceneViewportRotateGizmoAxis axis) {
if (m_activeAxis != SceneViewportRotateGizmoAxis::None) {
return;
}
m_hoveredAxis = axis;
RefreshHandleState();
}
void SceneViewportRotateGizmo::BuildDrawData(const SceneViewportRotateGizmoContext& context) {
m_drawData = {};
const Components::GameObject* selectedObject = context.selectedObject;
if (selectedObject == nullptr ||
if ((context.selectedObject == nullptr && context.selectedObjects.empty()) ||
!context.overlay.valid ||
context.viewportSize.x <= 1.0f ||
context.viewportSize.y <= 1.0f) {
return;
}
const Math::Vector3 pivotWorldPosition = selectedObject->GetTransform()->GetPosition();
const Math::Vector3 pivotWorldPosition = context.pivotWorldPosition;
const SceneViewportProjectedPoint projectedPivot = ProjectSceneViewportWorldPoint(
context.overlay,
context.viewportSize.x,
@@ -383,6 +516,7 @@ void SceneViewportRotateGizmo::BuildDrawData(const SceneViewportRotateGizmoConte
m_drawData.visible = true;
m_drawData.pivot = projectedPivot.screenPosition;
const bool hasActiveDragFeedback =
!context.localSpace &&
m_activeAxis != SceneViewportRotateGizmoAxis::None &&
m_activeAxis != SceneViewportRotateGizmoAxis::View &&
std::abs(m_dragCurrentDeltaRadians) > Math::EPSILON;
@@ -398,7 +532,7 @@ void SceneViewportRotateGizmo::BuildDrawData(const SceneViewportRotateGizmoConte
Math::Vector3 basisA = Math::Vector3::Zero();
Math::Vector3 basisB = Math::Vector3::Zero();
if (!GetRotateRingBasis(handle.axis, context.overlay, basisA, basisB)) {
if (!GetRotateRingBasis(handle.axis, context.overlay, context.axisOrientation, basisA, basisB)) {
continue;
}
if (hasActiveDragFeedback && handle.axis != SceneViewportRotateGizmoAxis::View) {
@@ -468,7 +602,7 @@ void SceneViewportRotateGizmo::BuildDrawData(const SceneViewportRotateGizmoConte
Math::Vector3 basisA = Math::Vector3::Zero();
Math::Vector3 basisB = Math::Vector3::Zero();
if (GetRotateRingBasis(m_activeAxis, context.overlay, basisA, basisB)) {
if (GetRotateRingBasis(m_activeAxis, context.overlay, context.axisOrientation, basisA, basisB)) {
const float ringRadiusWorld = worldUnitsPerPixel * GetRotateRingRadiusPixels(m_activeAxis);
const float sweepRadians = NormalizeSignedAngleRadians(m_dragCurrentDeltaRadians);
const float sweepAbs = std::abs(sweepRadians);
@@ -519,39 +653,6 @@ void SceneViewportRotateGizmo::RefreshHandleState() {
}
}
SceneViewportRotateGizmoAxis SceneViewportRotateGizmo::HitTestAxis(const Math::Vector2& mousePosition) const {
if (!m_drawData.visible) {
return SceneViewportRotateGizmoAxis::None;
}
const float hoverThresholdSq = kRotateGizmoHoverThresholdPixels * kRotateGizmoHoverThresholdPixels;
SceneViewportRotateGizmoAxis bestAxis = SceneViewportRotateGizmoAxis::None;
float bestDistanceSq = hoverThresholdSq;
for (const SceneViewportRotateGizmoHandleDrawData& handle : m_drawData.handles) {
if (!handle.visible) {
continue;
}
for (const SceneViewportRotateGizmoSegmentDrawData& segment : handle.segments) {
if (!segment.visible ||
(handle.axis != SceneViewportRotateGizmoAxis::View && !segment.frontFacing)) {
continue;
}
const float distanceSq = DistanceToSegmentSquared(mousePosition, segment.start, segment.end);
if (distanceSq > bestDistanceSq) {
continue;
}
bestDistanceSq = distanceSq;
bestAxis = handle.axis;
}
}
return bestAxis;
}
bool SceneViewportRotateGizmo::TryGetClosestRingAngle(
SceneViewportRotateGizmoAxis axis,
const Math::Vector2& mousePosition,