diff --git a/src/engine/shared/config_variables.h b/src/engine/shared/config_variables.h index 436daae7333..790416ddea5 100644 --- a/src/engine/shared/config_variables.h +++ b/src/engine/shared/config_variables.h @@ -101,6 +101,8 @@ MACRO_CONFIG_INT(EdSmoothZoomTime, ed_smooth_zoom_time, 250, 0, 5000, CFGFLAG_CL MACRO_CONFIG_INT(EdLimitMaxZoomLevel, ed_limit_max_zoom_level, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Specifies, if zooming in the editor should be limited or not (0 = no limit)") MACRO_CONFIG_INT(EdZoomTarget, ed_zoom_target, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Zoom to the current mouse target") MACRO_CONFIG_INT(EdShowkeys, ed_showkeys, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show pressed keys") +MACRO_CONFIG_INT(EdAlignQuads, ed_align_quads, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Enable/disable quad alignment. When enabled, red lines appear to show how quad/points are aligned and snapped to other quads/points when moving them") +MACRO_CONFIG_INT(EdShowQuadsRect, ed_show_quads_rect, 0, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show the bounds of the selected quad. In case of multiple quads, it shows the bounds of the englobing rect. Can be helpful when aligning a group of quads") MACRO_CONFIG_INT(ClShowWelcome, cl_show_welcome, 1, 0, 1, CFGFLAG_CLIENT | CFGFLAG_SAVE, "Show welcome message indicating the first launch of the client") MACRO_CONFIG_INT(ClMotdTime, cl_motd_time, 10, 0, 100, CFGFLAG_CLIENT | CFGFLAG_SAVE, "How long to show the server message of the day") diff --git a/src/game/editor/editor.cpp b/src/game/editor/editor.cpp index 4fc1cd80da8..7d826ea4d12 100644 --- a/src/game/editor/editor.cpp +++ b/src/game/editor/editor.cpp @@ -1529,6 +1529,461 @@ void CEditor::DoSoundSource(CSoundSource *pSource, int Index) Graphics()->QuadsDraw(&QuadItem, 1); } +void CEditor::PreparePointDrag(const std::shared_ptr &pLayer, CQuad *pQuad, int QuadIndex, int PointIndex) +{ + m_QuadDragOriginalPoints[QuadIndex][PointIndex] = pQuad->m_aPoints[PointIndex]; +} + +void CEditor::DoPointDrag(const std::shared_ptr &pLayer, CQuad *pQuad, int QuadIndex, int PointIndex, int OffsetX, int OffsetY) +{ + pQuad->m_aPoints[PointIndex].x = m_QuadDragOriginalPoints[QuadIndex][PointIndex].x + OffsetX; + pQuad->m_aPoints[PointIndex].y = m_QuadDragOriginalPoints[QuadIndex][PointIndex].y + OffsetY; +} + +CEditor::EAxis CEditor::GetDragAxis(int OffsetX, int OffsetY) +{ + if(Input()->ShiftIsPressed()) + if(absolute(OffsetX) < absolute(OffsetY)) + return EAxis::AXIS_Y; + else + return EAxis::AXIS_X; + else + return EAxis::AXIS_NONE; +} + +void CEditor::DrawAxis(EAxis Axis, CPoint &OriginalPoint, CPoint &Point) +{ + if(Axis == EAxis::AXIS_NONE) + return; + + Graphics()->SetColor(1, 0, 0.1f, 1); + if(Axis == EAxis::AXIS_X) + { + IGraphics::CQuadItem Line(fx2f(OriginalPoint.x + Point.x) / 2.0f, fx2f(OriginalPoint.y), fx2f(Point.x - OriginalPoint.x), 1.0f * m_MouseWScale); + Graphics()->QuadsDraw(&Line, 1); + } + else if(Axis == EAxis::AXIS_Y) + { + IGraphics::CQuadItem Line(fx2f(OriginalPoint.x), fx2f(OriginalPoint.y + Point.y) / 2.0f, 1.0f * m_MouseWScale, fx2f(Point.y - OriginalPoint.y)); + Graphics()->QuadsDraw(&Line, 1); + } + + // Draw ghost of original point + IGraphics::CQuadItem QuadItem(fx2f(OriginalPoint.x), fx2f(OriginalPoint.y), 5.0f * m_MouseWScale, 5.0f * m_MouseWScale); + Graphics()->QuadsDraw(&QuadItem, 1); +} + +void CEditor::ComputePointAlignments(const std::shared_ptr &pLayer, CQuad *pQuad, int QuadIndex, int PointIndex, int OffsetX, int OffsetY, std::vector &vAlignments, bool Append) const +{ + if(!Append) + vAlignments.clear(); + if(!g_Config.m_EdAlignQuads) + return; + + // Perform computation from the original position of this point + int Threshold = f2fx(maximum(10.0f, 10.0f * m_MouseWScale)); + CPoint OrigPoint = m_QuadDragOriginalPoints.at(QuadIndex)[PointIndex]; + // Get the "current" point by applying the offset + CPoint Point = OrigPoint + ivec2(OffsetX, OffsetY); + + // Save smallest diff on both axis to only keep closest alignments + int SmallestDiffX = Threshold + 1, SmallestDiffY = Threshold + 1; + // Store both axis alignments in separate vectors + std::vector vAlignmentsX, vAlignmentsY; + + // Check if we can align/snap to a specific point + auto &&CheckAlignment = [&](CPoint *pQuadPoint) { + int DX = pQuadPoint->x - Point.x; + int DY = pQuadPoint->y - Point.y; + int DiffX = absolute(DX); + int DiffY = absolute(DY); + + // Check the X axis + if(DiffX <= Threshold) + { + // Only store alignments that have the smallest difference + if(DiffX < SmallestDiffX) + { + vAlignmentsX.clear(); + SmallestDiffX = DiffX; + } + + // We can have multiple alignments having the same difference/distance + if(DiffX == SmallestDiffX) + { + vAlignmentsX.push_back(SAlignmentInfo{ + *pQuadPoint, // Aligned point + {OrigPoint.y}, // Value that can change (which is not snapped), original position + EAxis::AXIS_Y, // The alignment axis + PointIndex, // The index of the point + DX, + }); + } + } + if(DiffY <= Threshold) + { + // Only store alignments that have the smallest difference + if(DiffY < SmallestDiffY) + { + vAlignmentsY.clear(); + SmallestDiffY = DiffY; + } + + if(DiffY == SmallestDiffY) + { + vAlignmentsY.push_back(SAlignmentInfo{ + *pQuadPoint, + {OrigPoint.x}, + EAxis::AXIS_X, + PointIndex, + DY, + }); + } + } + }; + + // Iterate through all the quads of the current layer + // Check alignment with each point of the quad (corners & pivot) + // Compute an AABB (Axis Aligned Bounding Box) to get the center of the quad + // Check alignment with the center of the quad + for(size_t i = 0; i < pLayer->m_vQuads.size(); i++) + { + auto *pCurrentQuad = &pLayer->m_vQuads[i]; + CPoint Min = pCurrentQuad->m_aPoints[0]; + CPoint Max = pCurrentQuad->m_aPoints[0]; + + for(int v = 0; v < 5; v++) + { + CPoint *pQuadPoint = &pCurrentQuad->m_aPoints[v]; + + if(v != 4) + { // Don't use pivot to compute AABB + if(pQuadPoint->x < Min.x) + Min.x = pQuadPoint->x; + if(pQuadPoint->y < Min.y) + Min.y = pQuadPoint->y; + if(pQuadPoint->x > Max.x) + Max.x = pQuadPoint->x; + if(pQuadPoint->y > Max.y) + Max.y = pQuadPoint->y; + } + + // Don't check alignment with current point + if(pQuadPoint == &pQuad->m_aPoints[PointIndex]) + continue; + + // Don't check alignment with other selected points + bool IsCurrentPointSelected = IsQuadSelected(i) && (IsQuadCornerSelected(v) || (v == PointIndex && PointIndex == 4)); + if(IsCurrentPointSelected) + continue; + + CheckAlignment(pQuadPoint); + } + + CPoint Center = (Min + Max) / 2.0f; + CheckAlignment(&Center); + } + + // Finally concatenate both alignment vectors into the output + vAlignments.reserve(vAlignmentsX.size() + vAlignmentsY.size()); + vAlignments.insert(vAlignments.end(), vAlignmentsX.begin(), vAlignmentsX.end()); + vAlignments.insert(vAlignments.end(), vAlignmentsY.begin(), vAlignmentsY.end()); +} + +void CEditor::ComputePointsAlignments(const std::shared_ptr &pLayer, bool Pivot, int OffsetX, int OffsetY, std::vector &vAlignments) const +{ + // This method is used to compute alignments from selected points + // and only apply the closest alignment on X and Y to the offset. + + vAlignments.clear(); + std::vector vAllAlignments; + + for(int Selected : m_vSelectedQuads) + { + CQuad *pQuad = &pLayer->m_vQuads[Selected]; + + if(!Pivot) + { + for(int m = 0; m < 4; m++) + { + if(IsQuadPointSelected(Selected, m)) + { + ComputePointAlignments(pLayer, pQuad, Selected, m, OffsetX, OffsetY, vAllAlignments, true); + } + } + } + else + { + ComputePointAlignments(pLayer, pQuad, Selected, 4, OffsetX, OffsetY, vAllAlignments, true); + } + } + + int SmallestDiffX, SmallestDiffY; + SmallestDiffX = SmallestDiffY = std::numeric_limits::max(); + + std::vector vAlignmentsX, vAlignmentsY; + + for(const auto &Alignment : vAllAlignments) + { + int AbsDiff = absolute(Alignment.m_Diff); + if(Alignment.m_Axis == EAxis::AXIS_X) + { + if(AbsDiff < SmallestDiffY) + { + SmallestDiffY = AbsDiff; + vAlignmentsY.clear(); + } + if(AbsDiff == SmallestDiffY) + vAlignmentsY.emplace_back(Alignment); + } + else if(Alignment.m_Axis == EAxis::AXIS_Y) + { + if(AbsDiff < SmallestDiffX) + { + SmallestDiffX = AbsDiff; + vAlignmentsX.clear(); + } + if(AbsDiff == SmallestDiffX) + vAlignmentsX.emplace_back(Alignment); + } + } + + vAlignments.reserve(vAlignmentsX.size() + vAlignmentsY.size()); + vAlignments.insert(vAlignments.end(), vAlignmentsX.begin(), vAlignmentsX.end()); + vAlignments.insert(vAlignments.end(), vAlignmentsY.begin(), vAlignmentsY.end()); +} + +void CEditor::ComputeAABBAlignments(const std::shared_ptr &pLayer, const SAxisAlignedBoundingBox &AABB, int OffsetX, int OffsetY, std::vector &vAlignments) const +{ + vAlignments.clear(); + if(!g_Config.m_EdAlignQuads) + return; + + // This method is a bit different than the point alignment in the way where instead of trying to aling 1 point to all quads, + // we try to align 5 points to all quads, these 5 points being 5 points of an AABB. + // Otherwise, the concept is the same, we use the original position of the AABB to make the computations. + int Threshold = f2fx(maximum(10.0f, 10.0f * m_MouseWScale)); + int SmallestDiffX = Threshold + 1, SmallestDiffY = Threshold + 1; + std::vector vAlignmentsX, vAlignmentsY; + + auto &&CheckAlignment = [&](CPoint &Aligned, int Point) { + CPoint ToCheck = AABB.m_aPoints[Point] + ivec2(OffsetX, OffsetY); + int DX = Aligned.x - ToCheck.x; + int DY = Aligned.y - ToCheck.y; + int DiffX = absolute(DX); + int DiffY = absolute(DY); + + if(DiffX <= Threshold) + { + if(DiffX < SmallestDiffX) + { + SmallestDiffX = DiffX; + vAlignmentsX.clear(); + } + + if(DiffX == SmallestDiffX) + { + vAlignmentsX.push_back(SAlignmentInfo{ + Aligned, + {AABB.m_aPoints[Point].y}, + EAxis::AXIS_Y, + Point, + DX, + }); + } + } + if(DiffY <= Threshold) + { + if(DiffY < SmallestDiffY) + { + SmallestDiffY = DiffY; + vAlignmentsY.clear(); + } + + if(DiffY == SmallestDiffY) + { + vAlignmentsY.push_back(SAlignmentInfo{ + Aligned, + {AABB.m_aPoints[Point].x}, + EAxis::AXIS_X, + Point, + DY, + }); + } + } + }; + + auto &&CheckAABBAlignment = [&](CPoint &QuadMin, CPoint &QuadMax) { + CPoint QuadCenter = (QuadMin + QuadMax) / 2.0f; + CPoint aQuadPoints[5] = { + QuadMin, // Top left + {QuadMax.x, QuadMin.y}, // Top right + {QuadMin.x, QuadMax.y}, // Bottom left + QuadMax, // Bottom right + QuadCenter, + }; + + // Check all points with all the other points + for(auto &QuadPoint : aQuadPoints) + { + // i is the quad point which is "aligned" and that we want to compare with + for(int j = 0; j < 5; j++) + { + // j is the point we try to align + CheckAlignment(QuadPoint, j); + } + } + }; + + // Iterate through all quads of the current layer + // Compute AABB of all quads and check if the dragged AABB can be aligned to this AABB. + for(size_t i = 0; i < pLayer->m_vQuads.size(); i++) + { + auto *pCurrentQuad = &pLayer->m_vQuads[i]; + if(IsQuadSelected(i)) // Don't check with other selected quads + continue; + + // Get AABB of this quad + CPoint QuadMin = pCurrentQuad->m_aPoints[0], QuadMax = pCurrentQuad->m_aPoints[0]; + for(int v = 1; v < 4; v++) + { + QuadMin.x = minimum(QuadMin.x, pCurrentQuad->m_aPoints[v].x); + QuadMin.y = minimum(QuadMin.y, pCurrentQuad->m_aPoints[v].y); + QuadMax.x = maximum(QuadMax.x, pCurrentQuad->m_aPoints[v].x); + QuadMax.y = maximum(QuadMax.y, pCurrentQuad->m_aPoints[v].y); + } + + CheckAABBAlignment(QuadMin, QuadMax); + } + + // Finally, concatenate both alignment vectors into the output + vAlignments.reserve(vAlignmentsX.size() + vAlignmentsY.size()); + vAlignments.insert(vAlignments.end(), vAlignmentsX.begin(), vAlignmentsX.end()); + vAlignments.insert(vAlignments.end(), vAlignmentsY.begin(), vAlignmentsY.end()); +} + +void CEditor::DrawPointAlignments(const std::vector &vAlignments, int OffsetX, int OffsetY) +{ + if(!g_Config.m_EdAlignQuads) + return; + + // Drawing an alignment is easy, we convert fixed to float for the aligned point coords + // and we also convert the "changing" value after applying the offset (which might be edited to actually align the value with the alignment). + Graphics()->SetColor(1, 0, 0.1f, 1); + for(const SAlignmentInfo &Alignment : vAlignments) + { + // We don't use IGraphics::CLineItem to draw because we don't want to stop QuadsBegin(), quads work just fine. + if(Alignment.m_Axis == EAxis::AXIS_X) + { // Alignment on X axis is same Y values but different X values + IGraphics::CQuadItem Line(fx2f(Alignment.m_AlignedPoint.x), fx2f(Alignment.m_AlignedPoint.y), fx2f(Alignment.m_X + OffsetX - Alignment.m_AlignedPoint.x), 1.0f * m_MouseWScale); + Graphics()->QuadsDrawTL(&Line, 1); + } + else if(Alignment.m_Axis == EAxis::AXIS_Y) + { // Alignment on Y axis is same X values but different Y values + IGraphics::CQuadItem Line(fx2f(Alignment.m_AlignedPoint.x), fx2f(Alignment.m_AlignedPoint.y), 1.0f * m_MouseWScale, fx2f(Alignment.m_Y + OffsetY - Alignment.m_AlignedPoint.y)); + Graphics()->QuadsDrawTL(&Line, 1); + } + } +} + +void CEditor::DrawAABB(const SAxisAlignedBoundingBox &AABB, int OffsetX, int OffsetY) +{ + // Drawing an AABB is simply converting the points from fixed to float + // Then making lines out of quads and drawing them + vec2 TL = {fx2f(AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_TL].x + OffsetX), fx2f(AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_TL].y + OffsetY)}; + vec2 TR = {fx2f(AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_TR].x + OffsetX), fx2f(AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_TR].y + OffsetY)}; + vec2 BL = {fx2f(AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_BL].x + OffsetX), fx2f(AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_BL].y + OffsetY)}; + vec2 BR = {fx2f(AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_BR].x + OffsetX), fx2f(AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_BR].y + OffsetY)}; + vec2 Center = {fx2f(AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_CENTER].x + OffsetX), fx2f(AABB.m_aPoints[SAxisAlignedBoundingBox::POINT_CENTER].y + OffsetY)}; + + // We don't use IGraphics::CLineItem to draw because we don't want to stop QuadsBegin(), quads work just fine. + IGraphics::CQuadItem Lines[4] = { + {TL.x, TL.y, TR.x - TL.x, 1.0f * m_MouseWScale}, + {TL.x, TL.y, 1.0f * m_MouseWScale, BL.y - TL.y}, + {TR.x, TR.y, 1.0f * m_MouseWScale, BR.y - TR.y}, + {BL.x, BL.y, BR.x - BL.x, 1.0f * m_MouseWScale}, + }; + Graphics()->SetColor(1, 0, 1, 1); + Graphics()->QuadsDrawTL(Lines, 4); + + IGraphics::CQuadItem CenterQuad(Center.x, Center.y, 5.0f * m_MouseWScale, 5.0f * m_MouseWScale); + Graphics()->QuadsDraw(&CenterQuad, 1); +} + +void CEditor::QuadSelectionAABB(const std::shared_ptr &pLayer, SAxisAlignedBoundingBox &OutAABB) +{ + // Compute an englobing AABB of the current selection of quads + CPoint Min{ + std::numeric_limits::max(), + std::numeric_limits::max(), + }; + CPoint Max{ + std::numeric_limits::min(), + std::numeric_limits::min(), + }; + for(int Selected : m_vSelectedQuads) + { + CQuad *pQuad = &pLayer->m_vQuads[Selected]; + for(auto &Point : pQuad->m_aPoints) + { + Min.x = minimum(Min.x, Point.x); + Min.y = minimum(Min.y, Point.y); + Max.x = maximum(Max.x, Point.x); + Max.y = maximum(Max.y, Point.y); + } + } + CPoint Center = (Min + Max) / 2.0f; + CPoint aPoints[SAxisAlignedBoundingBox::NUM_POINTS] = { + Min, // Top left + {Max.x, Min.y}, // Top right + {Min.x, Max.y}, // Bottom left + Max, // Bottom right + Center, + }; + mem_copy(OutAABB.m_aPoints, aPoints, sizeof(CPoint) * SAxisAlignedBoundingBox::NUM_POINTS); +} + +void CEditor::ApplyAlignments(const std::vector &vAlignments, int &OffsetX, int &OffsetY) +{ + if(vAlignments.empty()) + return; + + // Find X and Y aligment + const int *pAlignedX = nullptr; + const int *pAlignedY = nullptr; + + // To Find the alignments we simply iterate through the vector of alignments and find the first + // X and Y alignments. + // Then, we use the saved m_Diff to adjust the offset + int AdjustX = 0, AdjustY = 0; + for(const SAlignmentInfo &Alignment : vAlignments) + { + if(Alignment.m_Axis == EAxis::AXIS_X && !pAlignedY) + { + pAlignedY = &Alignment.m_AlignedPoint.y; + AdjustY = Alignment.m_Diff; + } + else if(Alignment.m_Axis == EAxis::AXIS_Y && !pAlignedX) + { + pAlignedX = &Alignment.m_AlignedPoint.x; + AdjustX = Alignment.m_Diff; + } + } + + // Adjust offset + OffsetX += AdjustX; + OffsetY += AdjustY; +} + +void CEditor::ApplyAxisAlignment(int &OffsetX, int &OffsetY) +{ + // This is used to preserve axis alignment when pressing `Shift` + // Should be called before any other computation + EAxis Axis = GetDragAxis(OffsetX, OffsetY); + OffsetX = ((Axis == EAxis::AXIS_NONE || Axis == EAxis::AXIS_X) ? OffsetX : 0); + OffsetY = ((Axis == EAxis::AXIS_NONE || Axis == EAxis::AXIS_Y) ? OffsetY : 0); +} + void CEditor::DoQuad(const std::shared_ptr &pLayer, CQuad *pQuad, int Index) { enum @@ -1551,6 +2006,11 @@ void CEditor::DoQuad(const std::shared_ptr &pLayer, CQuad *pQuad, i static float s_RotateAngle = 0; float wx = UI()->MouseWorldX(); float wy = UI()->MouseWorldY(); + static CPoint s_OriginalPosition; + static std::vector s_PivotAlignments; // Alignments per pivot per quad + static std::vector s_vAABBAlignments; // Alignments for one AABB (single quad or selection of multiple quads) + static SAxisAlignedBoundingBox s_SelectionAABB; // Selection AABB + static ivec2 s_LastOffset; // Last offset, stored as static so we can use it to draw every frame // get pivot float CenterX = fx2f(pQuad->m_aPoints[4].x); @@ -1558,6 +2018,18 @@ void CEditor::DoQuad(const std::shared_ptr &pLayer, CQuad *pQuad, i const bool IgnoreGrid = Input()->AltIsPressed(); + auto &&GetDragOffset = [&]() -> ivec2 { + float x = wx; + float y = wy; + if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid) + MapView()->MapGrid()->SnapToGrid(x, y); + + int OffsetX = f2fx(x) - s_OriginalPosition.x; + int OffsetY = f2fx(y) - s_OriginalPosition.y; + + return {OffsetX, OffsetY}; + }; + // draw selection background if(IsQuadSelected(Index)) { @@ -1580,10 +2052,32 @@ void CEditor::DoQuad(const std::shared_ptr &pLayer, CQuad *pQuad, i if(!IsQuadSelected(Index)) SelectQuad(Index); + s_OriginalPosition = pQuad->m_aPoints[4]; + if(Input()->ShiftIsPressed()) + { s_Operation = OP_MOVE_PIVOT; + // When moving, we need to save the original position of all selected pivots + for(int Selected : m_vSelectedQuads) + { + CQuad *pCurrentQuad = &pLayer->m_vQuads[Selected]; + PreparePointDrag(pLayer, pCurrentQuad, Selected, 4); + } + } else + { s_Operation = OP_MOVE_ALL; + // When moving, we need to save the original position of all selected quads points + for(int Selected : m_vSelectedQuads) + { + CQuad *pCurrentQuad = &pLayer->m_vQuads[Selected]; + for(size_t v = 0; v < 5; v++) + PreparePointDrag(pLayer, pCurrentQuad, Selected, v); + } + // And precompute AABB of selection since it will not change during drag + if(g_Config.m_EdAlignQuads) + QuadSelectionAABB(pLayer, s_SelectionAABB); + } } } @@ -1591,41 +2085,37 @@ void CEditor::DoQuad(const std::shared_ptr &pLayer, CQuad *pQuad, i if(s_Operation == OP_MOVE_PIVOT) { m_QuadTracker.BeginQuadTrack(pLayer, m_vSelectedQuads); - float x = wx; - float y = wy; - if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid) - MapView()->MapGrid()->SnapToGrid(x, y); - int OffsetX = f2fx(x) - pQuad->m_aPoints[4].x; - int OffsetY = f2fx(y) - pQuad->m_aPoints[4].y; + s_LastOffset = GetDragOffset(); // Update offset + ApplyAxisAlignment(s_LastOffset.x, s_LastOffset.y); // Apply axis alignment to the offset + + ComputePointsAlignments(pLayer, true, s_LastOffset.x, s_LastOffset.y, s_PivotAlignments); + ApplyAlignments(s_PivotAlignments, s_LastOffset.x, s_LastOffset.y); for(auto &Selected : m_vSelectedQuads) { CQuad *pCurrentQuad = &pLayer->m_vQuads[Selected]; - pCurrentQuad->m_aPoints[4].x += OffsetX; - pCurrentQuad->m_aPoints[4].y += OffsetY; + DoPointDrag(pLayer, pCurrentQuad, Selected, 4, s_LastOffset.x, s_LastOffset.y); } } else if(s_Operation == OP_MOVE_ALL) { m_QuadTracker.BeginQuadTrack(pLayer, m_vSelectedQuads); - // move all points including pivot - float x = wx; - float y = wy; - if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid) - MapView()->MapGrid()->SnapToGrid(x, y); - int OffsetX = f2fx(x) - pQuad->m_aPoints[4].x; - int OffsetY = f2fx(y) - pQuad->m_aPoints[4].y; + // Compute drag offset + s_LastOffset = GetDragOffset(); + ApplyAxisAlignment(s_LastOffset.x, s_LastOffset.y); - for(auto &Selected : m_vSelectedQuads) + // Then compute possible alignments with the selection AABB + ComputeAABBAlignments(pLayer, s_SelectionAABB, s_LastOffset.x, s_LastOffset.y, s_vAABBAlignments); + // Apply alignments before drag + ApplyAlignments(s_vAABBAlignments, s_LastOffset.x, s_LastOffset.y); + // Then do the drag + for(int Selected : m_vSelectedQuads) { CQuad *pCurrentQuad = &pLayer->m_vQuads[Selected]; - for(auto &Point : pCurrentQuad->m_aPoints) - { - Point.x += OffsetX; - Point.y += OffsetY; - } + for(int v = 0; v < 5; v++) + DoPointDrag(pLayer, pCurrentQuad, Selected, v, s_LastOffset.x, s_LastOffset.y); } } else if(s_Operation == OP_ROTATE) @@ -1649,6 +2139,24 @@ void CEditor::DoQuad(const std::shared_ptr &pLayer, CQuad *pQuad, i } } + // Draw axis and aligments when moving + if(s_Operation == OP_MOVE_PIVOT || s_Operation == OP_MOVE_ALL) + { + EAxis Axis = GetDragAxis(s_LastOffset.x, s_LastOffset.y); + DrawAxis(Axis, s_OriginalPosition, pQuad->m_aPoints[4]); + } + + if(s_Operation == OP_MOVE_PIVOT) + DrawPointAlignments(s_PivotAlignments, s_LastOffset.x, s_LastOffset.y); + + if(s_Operation == OP_MOVE_ALL) + { + DrawPointAlignments(s_vAABBAlignments, s_LastOffset.x, s_LastOffset.y); + + if(g_Config.m_EdShowQuadsRect) + DrawAABB(s_SelectionAABB, s_LastOffset.x, s_LastOffset.y); + } + if(s_Operation == OP_CONTEXT_MENU) { if(!UI()->MouseButton(1)) @@ -1722,6 +2230,11 @@ void CEditor::DoQuad(const std::shared_ptr &pLayer, CQuad *pQuad, i UI()->DisableMouseLock(); s_Operation = OP_NONE; UI()->SetActiveItem(nullptr); + + s_LastOffset = ivec2(); + s_OriginalPosition = ivec2(); + s_vAABBAlignments.clear(); + s_PivotAlignments.clear(); } } @@ -1822,9 +2335,24 @@ void CEditor::DoQuadPoint(const std::shared_ptr &pLayer, CQuad *pQu static int s_Operation = OP_NONE; static float s_MouseXStart = 0.0f; static float s_MouseYStart = 0.0f; + static CPoint s_OriginalPoint; + static std::vector s_Alignments; // Alignments + static ivec2 s_LastOffset; const bool IgnoreGrid = Input()->AltIsPressed(); + auto &&GetDragOffset = [&]() -> ivec2 { + float x = wx; + float y = wy; + if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid) + MapView()->MapGrid()->SnapToGrid(x, y); + + int OffsetX = f2fx(x) - s_OriginalPoint.x; + int OffsetY = f2fx(y) - s_OriginalPoint.y; + + return {OffsetX, OffsetY}; + }; + if(UI()->CheckActiveItem(pID)) { if(m_MouseDeltaWx * m_MouseDeltaWx + m_MouseDeltaWy * m_MouseDeltaWy > 0.0f) @@ -1845,7 +2373,14 @@ void CEditor::DoQuadPoint(const std::shared_ptr &pLayer, CQuad *pQu UI()->EnableMouseLock(pID); } else + { s_Operation = OP_MOVEPOINT; + // Save original positions before moving + s_OriginalPoint = pQuad->m_aPoints[V]; + for(int m = 0; m < 4; m++) + if(IsQuadPointSelected(QuadIndex, m)) + PreparePointDrag(pLayer, pQuad, QuadIndex, m); + } } } @@ -1853,20 +2388,17 @@ void CEditor::DoQuadPoint(const std::shared_ptr &pLayer, CQuad *pQu { m_QuadTracker.BeginQuadTrack(pLayer, m_vSelectedQuads); - float x = wx; - float y = wy; - if(MapView()->MapGrid()->IsEnabled() && !IgnoreGrid) - MapView()->MapGrid()->SnapToGrid(x, y); + s_LastOffset = GetDragOffset(); // Update offset + ApplyAxisAlignment(s_LastOffset.x, s_LastOffset.y); // Apply axis alignment to offset - int OffsetX = f2fx(x) - pQuad->m_aPoints[V].x; - int OffsetY = f2fx(y) - pQuad->m_aPoints[V].y; + ComputePointsAlignments(pLayer, false, s_LastOffset.x, s_LastOffset.y, s_Alignments); + ApplyAlignments(s_Alignments, s_LastOffset.x, s_LastOffset.y); for(int m = 0; m < 4; m++) { if(IsQuadPointSelected(QuadIndex, m)) { - pQuad->m_aPoints[m].x += OffsetX; - pQuad->m_aPoints[m].y += OffsetY; + DoPointDrag(pLayer, pQuad, QuadIndex, m, s_LastOffset.x, s_LastOffset.y); } } } @@ -1895,6 +2427,19 @@ void CEditor::DoQuadPoint(const std::shared_ptr &pLayer, CQuad *pQu } } + // Draw axis and alignments when dragging + if(s_Operation == OP_MOVEPOINT) + { + Graphics()->SetColor(1, 0, 0.1f, 1); + + // Axis + EAxis Axis = GetDragAxis(s_LastOffset.x, s_LastOffset.y); + DrawAxis(Axis, s_OriginalPoint, pQuad->m_aPoints[V]); + + // Alignments + DrawPointAlignments(s_Alignments, s_LastOffset.x, s_LastOffset.y); + } + if(s_Operation == OP_CONTEXT_MENU) { if(!UI()->MouseButton(1)) @@ -7317,7 +7862,7 @@ void CEditor::RenderMenubar(CUIRect MenuBar) if(DoButton_Menu(&s_SettingsButton, "Settings", 0, &SettingsButton, 0, nullptr)) { static SPopupMenuId s_PopupMenuEntitiesId; - UI()->DoPopupMenu(&s_PopupMenuEntitiesId, SettingsButton.x, SettingsButton.y + SettingsButton.h - 1.0f, 200.0f, 64.0f, this, PopupMenuSettings, PopupProperties); + UI()->DoPopupMenu(&s_PopupMenuEntitiesId, SettingsButton.x, SettingsButton.y + SettingsButton.h - 1.0f, 200.0f, 92.0f, this, PopupMenuSettings, PopupProperties); } CUIRect ChangedIndicator, Info, Close; diff --git a/src/game/editor/editor.h b/src/game/editor/editor.h index f9bbcc52ebf..7977cb0904c 100644 --- a/src/game/editor/editor.h +++ b/src/game/editor/editor.h @@ -853,12 +853,62 @@ class CEditor : public IEditor void DoSoundSource(CSoundSource *pSource, int Index); + enum class EAxis + { + AXIS_NONE = 0, + AXIS_X, + AXIS_Y + }; + struct SAxisAlignedBoundingBox + { + enum + { + POINT_TL = 0, + POINT_TR, + POINT_BL, + POINT_BR, + POINT_CENTER, + NUM_POINTS + }; + CPoint m_aPoints[NUM_POINTS]; + }; void DoMapEditor(CUIRect View); void DoToolbarLayers(CUIRect Toolbar); void DoToolbarSounds(CUIRect Toolbar); void DoQuad(const std::shared_ptr &pLayer, CQuad *pQuad, int Index); + void PreparePointDrag(const std::shared_ptr &pLayer, CQuad *pQuad, int QuadIndex, int PointIndex); + void DoPointDrag(const std::shared_ptr &pLayer, CQuad *pQuad, int QuadIndex, int PointIndex, int OffsetX, int OffsetY); + EAxis GetDragAxis(int OffsetX, int OffsetY); + void DrawAxis(EAxis Axis, CPoint &OriginalPoint, CPoint &Point); + void DrawAABB(const SAxisAlignedBoundingBox &AABB, int OffsetX = 0, int OffsetY = 0); ColorRGBA GetButtonColor(const void *pID, int Checked); + // Alignment methods + // These methods take `OffsetX` and `OffsetY` because the calculations are made with the original positions + // of the quad(s), before we started dragging. This allows us to edit `OffsetX` and `OffsetY` based on the previously + // calculated alignments. + struct SAlignmentInfo + { + CPoint m_AlignedPoint; // The "aligned" point, which we want to align/snap to + union + { + // The current changing value when aligned to this point. When aligning to a point on the X axis, then the X value is changing because + // we aligned the Y values (X axis aligned => Y values are the same, Y axis aligned => X values are the same). + int m_X; + int m_Y; + }; + EAxis m_Axis; // The axis we are aligning on + int m_PointIndex; // The point index we are aligning + int m_Diff; // Store the difference + }; + void ComputePointAlignments(const std::shared_ptr &pLayer, CQuad *pQuad, int QuadIndex, int PointIndex, int OffsetX, int OffsetY, std::vector &vAlignments, bool Append = false) const; + void ComputePointsAlignments(const std::shared_ptr &pLayer, bool Pivot, int OffsetX, int OffsetY, std::vector &vAlignments) const; + void ComputeAABBAlignments(const std::shared_ptr &pLayer, const SAxisAlignedBoundingBox &AABB, int OffsetX, int OffsetY, std::vector &vAlignments) const; + void DrawPointAlignments(const std::vector &vAlignments, int OffsetX, int OffsetY); + void QuadSelectionAABB(const std::shared_ptr &pLayer, SAxisAlignedBoundingBox &OutAABB); + void ApplyAlignments(const std::vector &vAlignments, int &OffsetX, int &OffsetY); + void ApplyAxisAlignment(int &OffsetX, int &OffsetY); + bool ReplaceImage(const char *pFilename, int StorageType, bool CheckDuplicate); static bool ReplaceImageCallback(const char *pFilename, int StorageType, void *pUser); bool ReplaceSound(const char *pFileName, int StorageType, bool CheckDuplicate); @@ -1011,6 +1061,7 @@ class CEditor : public IEditor unsigned char m_SwitchNum; unsigned char m_SwitchDelay; +public: // Undo/Redo CEditorHistory m_EditorHistory; CEditorHistory m_ServerSettingsHistory; @@ -1021,6 +1072,9 @@ class CEditor : public IEditor private: void UndoLastAction(); void RedoLastAction(); + +private: + std::map m_QuadDragOriginalPoints; }; // make sure to inline this function diff --git a/src/game/editor/popups.cpp b/src/game/editor/popups.cpp index 2e5210db4ec..e9bcb721c47 100644 --- a/src/game/editor/popups.cpp +++ b/src/game/editor/popups.cpp @@ -328,6 +328,58 @@ CUI::EPopupMenuFunctionResult CEditor::PopupMenuSettings(void *pContext, CUIRect } } + View.HSplitTop(2.0f, nullptr, &View); + View.HSplitTop(12.0f, &Slot, &View); + { + Slot.VMargin(5.0f, &Slot); + + CUIRect Label, Selector; + Slot.VSplitMid(&Label, &Selector); + CUIRect No, Yes; + Selector.VSplitMid(&No, &Yes); + + pEditor->UI()->DoLabel(&Label, "Align quads", 10.0f, TEXTALIGN_ML); + if(pEditor->m_AllowPlaceUnusedTiles != -1) + { + static int s_ButtonNo = 0; + static int s_ButtonYes = 0; + if(pEditor->DoButton_ButtonDec(&s_ButtonNo, "No", !g_Config.m_EdAlignQuads, &No, 0, "Do not perform quad alignment to other quads/points when moving quads")) + { + g_Config.m_EdAlignQuads = false; + } + if(pEditor->DoButton_ButtonInc(&s_ButtonYes, "Yes", g_Config.m_EdAlignQuads, &Yes, 0, "Allow quad alignment to other quads/points when moving quads")) + { + g_Config.m_EdAlignQuads = true; + } + } + } + + View.HSplitTop(2.0f, nullptr, &View); + View.HSplitTop(12.0f, &Slot, &View); + { + Slot.VMargin(5.0f, &Slot); + + CUIRect Label, Selector; + Slot.VSplitMid(&Label, &Selector); + CUIRect No, Yes; + Selector.VSplitMid(&No, &Yes); + + pEditor->UI()->DoLabel(&Label, "Show quads bounds", 10.0f, TEXTALIGN_ML); + if(pEditor->m_AllowPlaceUnusedTiles != -1) + { + static int s_ButtonNo = 0; + static int s_ButtonYes = 0; + if(pEditor->DoButton_ButtonDec(&s_ButtonNo, "No", !g_Config.m_EdShowQuadsRect, &No, 0, "Do not show quad bounds when moving quads")) + { + g_Config.m_EdShowQuadsRect = false; + } + if(pEditor->DoButton_ButtonInc(&s_ButtonYes, "Yes", g_Config.m_EdShowQuadsRect, &Yes, 0, "Show quad bounds when moving quads")) + { + g_Config.m_EdShowQuadsRect = true; + } + } + } + return CUI::POPUP_KEEP_OPEN; }