4763: Quad knife tool r=def- a=HiRavie

A new way to slice quads that doesn't make mappers pull their hair out.
To use it, open the quad context menu and click the _Slice_ button, check out the tooltip for more instructions.
Snaps automatically to corners and edges if close enough or to grid.

![gif](https://user-images.githubusercontent.com/65019210/155854241-9e07a5b2-1915-41d4-86c9-b2084ff6b929.gif)

## Checklist

- [x] Tested the change ingame
- [x] Provided screenshots if it is a visual change
- [ ] Tested in combination with possibly related configuration options
- [ ] Written a unit test if it works standalone, system.c especially
- [x] Considered possible null pointers and out of bounds array indexing
- [x] Changed no physics that affect existing maps
- [ ] Tested the change with [ASan+UBSan or valgrind's memcheck](https://github.com/ddnet/ddnet/#using-addresssanitizer--undefinedbehavioursanitizer-or-valgrinds-memcheck) (optional)


Co-authored-by: Ravie <65019210+HiRavie@users.noreply.github.com>
Co-authored-by: Dennis Felsing <dennis@felsin9.de>
This commit is contained in:
bors[bot] 2022-03-01 10:34:34 +00:00 committed by GitHub
commit 4533a3541a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 295 additions and 13 deletions

View file

@ -698,7 +698,14 @@ void CEditor::SelectLayer(int LayerIndex, int GroupIndex)
m_SelectedGroup = GroupIndex;
m_lSelectedLayers.clear();
AddSelectedLayer(LayerIndex);
}
void CEditor::AddSelectedLayer(int LayerIndex)
{
m_lSelectedLayers.add(LayerIndex);
m_QuadKnifeActive = false;
}
void CEditor::SelectQuad(int Index)
@ -1471,7 +1478,7 @@ void CEditor::DoQuad(CQuad *pQuad, int Index)
m_SelectedQuadIndex = FindSelectedQuadIndex(Index);
static int s_QuadPopupID = 0;
UiInvokePopupMenu(&s_QuadPopupID, 0, UI()->MouseX(), UI()->MouseY(), 120, 180, PopupQuad);
UiInvokePopupMenu(&s_QuadPopupID, 0, UI()->MouseX(), UI()->MouseY(), 120, 198, PopupQuad);
m_LockMouse = false;
}
s_Operation = OP_NONE;
@ -1790,6 +1797,253 @@ void CEditor::DoQuadPoint(CQuad *pQuad, int QuadIndex, int V)
Graphics()->QuadsDraw(&QuadItem, 1);
}
float CEditor::TriangleArea(vec2 A, vec2 B, vec2 C)
{
return abs(((B.x - A.x) * (C.y - A.y) - (C.x - A.x) * (B.y - A.y)) * 0.5);
}
bool CEditor::IsInTriangle(vec2 Point, vec2 A, vec2 B, vec2 C)
{
// Normalize to increase precision
vec2 Min(minimum(A.x, B.x, C.x), minimum(A.y, B.y, C.y));
vec2 Max(maximum(A.x, B.x, C.x), maximum(A.y, B.y, C.y));
vec2 Size(Max.x - Min.x, Max.y - Min.y);
if(Size.x < 0.0000001f || Size.y < 0.0000001f)
return false;
vec2 Normal(1.f / Size.x, 1.f / Size.y);
A = (A - Min) * Normal;
B = (B - Min) * Normal;
C = (C - Min) * Normal;
Point = (Point - Min) * Normal;
float Area = TriangleArea(A, B, C);
return Area > 0.f && abs(TriangleArea(Point, A, B) + TriangleArea(Point, B, C) + TriangleArea(Point, C, A) - Area) < 0.000001f;
}
void CEditor::DoQuadKnife(int QuadIndex)
{
CLayerQuads *pLayer = (CLayerQuads *)GetSelectedLayerType(0, LAYERTYPE_QUADS);
CQuad *pQuad = &pLayer->m_lQuads[QuadIndex];
bool IgnoreGrid = Input()->KeyIsPressed(KEY_LALT) || Input()->KeyIsPressed(KEY_RALT);
float SnapRadius = 4.f * m_WorldZoom;
vec2 Mouse = vec2(UI()->MouseWorldX(), UI()->MouseWorldY());
vec2 Point = Mouse;
vec2 v[4] = {
vec2(fx2f(pQuad->m_aPoints[0].x), fx2f(pQuad->m_aPoints[0].y)),
vec2(fx2f(pQuad->m_aPoints[1].x), fx2f(pQuad->m_aPoints[1].y)),
vec2(fx2f(pQuad->m_aPoints[3].x), fx2f(pQuad->m_aPoints[3].y)),
vec2(fx2f(pQuad->m_aPoints[2].x), fx2f(pQuad->m_aPoints[2].y))};
m_pTooltip = "Left click inside the quad to select an area to slice. Hold alt to ignore grid. Right click to leave knife mode";
if(UI()->MouseButtonClicked(1))
{
m_QuadKnifeActive = false;
return;
}
// Handle snapping
if(m_GridActive && !IgnoreGrid)
{
float CellSize = (float)GetLineDistance();
vec2 OnGrid = vec2(roundf(Mouse.x / CellSize) * CellSize, roundf(Mouse.y / CellSize) * CellSize);
if(IsInTriangle(OnGrid, v[0], v[1], v[2]) || IsInTriangle(OnGrid, v[0], v[3], v[2]))
Point = OnGrid;
else
{
float MinDistance = -1.f;
for(int i = 0; i < 4; i++)
{
int j = (i + 1) % 4;
vec2 Min(minimum(v[i].x, v[j].x), minimum(v[i].y, v[j].y));
vec2 Max(maximum(v[i].x, v[j].x), maximum(v[i].y, v[j].y));
if(in_range(OnGrid.y, Min.y, Max.y) && Max.y - Min.y > 0.0000001f)
{
vec2 OnEdge(v[i].x + (OnGrid.y - v[i].y) / (v[j].y - v[i].y) * (v[j].x - v[i].x), OnGrid.y);
float Distance = abs(OnGrid.x - OnEdge.x);
if(Distance < CellSize && (Distance < MinDistance || MinDistance < 0.f))
{
MinDistance = Distance;
Point = OnEdge;
}
}
if(in_range(OnGrid.x, Min.x, Max.x) && Max.x - Min.x > 0.0000001f)
{
vec2 OnEdge(OnGrid.x, v[i].y + (OnGrid.x - v[i].x) / (v[j].x - v[i].x) * (v[j].y - v[i].y));
float Distance = abs(OnGrid.y - OnEdge.y);
if(Distance < CellSize && (Distance < MinDistance || MinDistance < 0.f))
{
MinDistance = Distance;
Point = OnEdge;
}
}
}
}
}
else
{
float MinDistance = -1.f;
// Try snapping to corners
for(const auto &x : v)
{
float Distance = distance(Mouse, x);
if(Distance <= SnapRadius && (Distance < MinDistance || MinDistance < 0.f))
{
MinDistance = Distance;
Point = x;
}
}
if(MinDistance < 0.f)
{
// Try snapping to edges
for(int i = 0; i < 4; i++)
{
int j = (i + 1) % 4;
vec2 s(v[j] - v[i]);
float t = ((Mouse.x - v[i].x) * s.x + (Mouse.y - v[i].y) * s.y) / (s.x * s.x + s.y * s.y);
if(in_range(t, 0.f, 1.f))
{
vec2 OnEdge = vec2((v[i].x + t * s.x), (v[i].y + t * s.y));
float Distance = distance(Mouse, OnEdge);
if(Distance <= SnapRadius && (Distance < MinDistance || MinDistance < 0.f))
{
MinDistance = Distance;
Point = OnEdge;
}
}
}
}
}
bool ValidPosition = IsInTriangle(Point, v[0], v[1], v[2]) || IsInTriangle(Point, v[0], v[3], v[2]);
if(UI()->MouseButtonClicked(0) && ValidPosition)
{
m_aQuadKnifePoints[m_QuadKnifeCount] = Point;
m_QuadKnifeCount++;
}
if(m_QuadKnifeCount == 4)
{
if(IsInTriangle(m_aQuadKnifePoints[3], m_aQuadKnifePoints[0], m_aQuadKnifePoints[1], m_aQuadKnifePoints[2]) ||
IsInTriangle(m_aQuadKnifePoints[1], m_aQuadKnifePoints[0], m_aQuadKnifePoints[2], m_aQuadKnifePoints[3]))
{
// Fix concave order
swap(m_aQuadKnifePoints[0], m_aQuadKnifePoints[3]);
swap(m_aQuadKnifePoints[1], m_aQuadKnifePoints[2]);
}
swap(m_aQuadKnifePoints[2], m_aQuadKnifePoints[3]);
CQuad *pResult = pLayer->NewQuad(64, 64, 64, 64);
pQuad = &pLayer->m_lQuads[QuadIndex];
for(int i = 0; i < 4; i++)
{
int t = IsInTriangle(m_aQuadKnifePoints[i], v[0], v[3], v[2]) ? 2 : 1;
vec2 A = vec2(fx2f(pQuad->m_aPoints[0].x), fx2f(pQuad->m_aPoints[0].y));
vec2 B = vec2(fx2f(pQuad->m_aPoints[3].x), fx2f(pQuad->m_aPoints[3].y));
vec2 C = vec2(fx2f(pQuad->m_aPoints[t].x), fx2f(pQuad->m_aPoints[t].y));
float TriArea = TriangleArea(A, B, C);
float WeightA = TriangleArea(m_aQuadKnifePoints[i], B, C) / TriArea;
float WeightB = TriangleArea(m_aQuadKnifePoints[i], C, A) / TriArea;
float WeightC = TriangleArea(m_aQuadKnifePoints[i], A, B) / TriArea;
pResult->m_aColors[i].r = (int)round(pQuad->m_aColors[0].r * WeightA + pQuad->m_aColors[3].r * WeightB + pQuad->m_aColors[t].r * WeightC);
pResult->m_aColors[i].g = (int)round(pQuad->m_aColors[0].g * WeightA + pQuad->m_aColors[3].g * WeightB + pQuad->m_aColors[t].g * WeightC);
pResult->m_aColors[i].b = (int)round(pQuad->m_aColors[0].b * WeightA + pQuad->m_aColors[3].b * WeightB + pQuad->m_aColors[t].b * WeightC);
pResult->m_aColors[i].a = (int)round(pQuad->m_aColors[0].a * WeightA + pQuad->m_aColors[3].a * WeightB + pQuad->m_aColors[t].a * WeightC);
pResult->m_aTexcoords[i].x = (int)round(pQuad->m_aTexcoords[0].x * WeightA + pQuad->m_aTexcoords[3].x * WeightB + pQuad->m_aTexcoords[t].x * WeightC);
pResult->m_aTexcoords[i].y = (int)round(pQuad->m_aTexcoords[0].y * WeightA + pQuad->m_aTexcoords[3].y * WeightB + pQuad->m_aTexcoords[t].y * WeightC);
pResult->m_aPoints[i].x = f2fx(m_aQuadKnifePoints[i].x);
pResult->m_aPoints[i].y = f2fx(m_aQuadKnifePoints[i].y);
}
pResult->m_aPoints[4].x = ((pResult->m_aPoints[0].x + pResult->m_aPoints[3].x) / 2 + (pResult->m_aPoints[1].x + pResult->m_aPoints[2].x) / 2) / 2;
pResult->m_aPoints[4].y = ((pResult->m_aPoints[0].y + pResult->m_aPoints[3].y) / 2 + (pResult->m_aPoints[1].y + pResult->m_aPoints[2].y) / 2) / 2;
m_QuadKnifeCount = 0;
}
// Render
Graphics()->TextureClear();
Graphics()->LinesBegin();
IGraphics::CLineItem aEdges[4] = {
IGraphics::CLineItem(v[0].x, v[0].y, v[1].x, v[1].y),
IGraphics::CLineItem(v[1].x, v[1].y, v[2].x, v[2].y),
IGraphics::CLineItem(v[2].x, v[2].y, v[3].x, v[3].y),
IGraphics::CLineItem(v[3].x, v[3].y, v[0].x, v[0].y)};
Graphics()->SetColor(1.f, 0.5f, 0.f, 1.f);
Graphics()->LinesDraw(aEdges, 4);
IGraphics::CLineItem aLines[4];
int LineCount = maximum(m_QuadKnifeCount - 1, 0);
for(int i = 0; i < LineCount; i++)
aLines[i] = IGraphics::CLineItem(m_aQuadKnifePoints[i].x, m_aQuadKnifePoints[i].y, m_aQuadKnifePoints[i + 1].x, m_aQuadKnifePoints[i + 1].y);
Graphics()->SetColor(1.f, 1.f, 1.f, 1.f);
Graphics()->LinesDraw(aLines, LineCount);
if(ValidPosition)
{
if(m_QuadKnifeCount > 0)
{
IGraphics::CLineItem LineCurrent(Point.x, Point.y, m_aQuadKnifePoints[m_QuadKnifeCount - 1].x, m_aQuadKnifePoints[m_QuadKnifeCount - 1].y);
Graphics()->LinesDraw(&LineCurrent, 1);
}
if(m_QuadKnifeCount == 3)
{
IGraphics::CLineItem LineClose(Point.x, Point.y, m_aQuadKnifePoints[0].x, m_aQuadKnifePoints[0].y);
Graphics()->LinesDraw(&LineClose, 1);
}
}
Graphics()->LinesEnd();
Graphics()->QuadsBegin();
IGraphics::CQuadItem aMarkers[4];
for(int i = 0; i < m_QuadKnifeCount; i++)
aMarkers[i] = IGraphics::CQuadItem(m_aQuadKnifePoints[i].x, m_aQuadKnifePoints[i].y, 5.f * m_WorldZoom, 5.f * m_WorldZoom);
Graphics()->SetColor(0.f, 0.f, 1.f, 1.f);
Graphics()->QuadsDraw(aMarkers, m_QuadKnifeCount);
if(ValidPosition)
{
IGraphics::CQuadItem MarkerCurrent(Point.x, Point.y, 5.f * m_WorldZoom, 5.f * m_WorldZoom);
Graphics()->QuadsDraw(&MarkerCurrent, 1);
}
Graphics()->QuadsEnd();
}
void CEditor::DoQuadEnvelopes(const array<CQuad> &lQuads, IGraphics::CTextureHandle Texture)
{
int Num = lQuads.size();
@ -2405,7 +2659,7 @@ void CEditor::DoMapEditor(CUIRect View)
m_Brush.Clear();
}
if(UI()->MouseButton(0) && s_Operation == OP_NONE)
if(UI()->MouseButton(0) && s_Operation == OP_NONE && !m_QuadKnifeActive)
{
UI()->SetActiveItem(s_pEditorID);
@ -2486,16 +2740,21 @@ void CEditor::DoMapEditor(CUIRect View)
if(!m_ShowEnvelopePreview)
m_ShowEnvelopePreview = 2;
Graphics()->TextureClear();
Graphics()->QuadsBegin();
for(int i = 0; i < pLayer->m_lQuads.size(); i++)
if(m_QuadKnifeActive)
DoQuadKnife(m_lSelectedQuads[m_SelectedQuadIndex]);
else
{
for(int v = 0; v < 4; v++)
DoQuadPoint(&pLayer->m_lQuads[i], i, v);
Graphics()->TextureClear();
Graphics()->QuadsBegin();
for(int i = 0; i < pLayer->m_lQuads.size(); i++)
{
for(int v = 0; v < 4; v++)
DoQuadPoint(&pLayer->m_lQuads[i], i, v);
DoQuad(&pLayer->m_lQuads[i], i);
DoQuad(&pLayer->m_lQuads[i], i);
}
Graphics()->QuadsEnd();
}
Graphics()->QuadsEnd();
}
if(pEditLayers[k]->m_Type == LAYERTYPE_SOUNDS)
@ -3076,7 +3335,7 @@ void CEditor::RenderLayers(CUIRect ToolBox, CUIRect View)
m_lSelectedLayers.clear();
for(int i = 0; i < m_Map.m_lGroups[g]->m_lLayers.size(); i++)
{
m_lSelectedLayers.add(i);
AddSelectedLayer(i);
}
}
@ -3169,7 +3428,7 @@ void CEditor::RenderLayers(CUIRect ToolBox, CUIRect View)
if((Input()->KeyIsPressed(KEY_LSHIFT) || Input()->KeyIsPressed(KEY_RSHIFT)) && m_SelectedGroup == g)
{
if(!m_lSelectedLayers.remove(i))
m_lSelectedLayers.add(i);
AddSelectedLayer(i);
}
else if(!(Input()->KeyIsPressed(KEY_LSHIFT) || Input()->KeyIsPressed(KEY_RSHIFT)))
{
@ -3233,7 +3492,7 @@ void CEditor::RenderLayers(CUIRect ToolBox, CUIRect View)
if(Input()->KeyIsPressed(KEY_LSHIFT) || Input()->KeyIsPressed(KEY_RSHIFT))
{
if(m_lSelectedLayers[m_lSelectedLayers.size() - 1] < m_Map.m_lGroups[m_SelectedGroup]->m_lLayers.size() - 1)
m_lSelectedLayers.add(m_lSelectedLayers[m_lSelectedLayers.size() - 1] + 1);
AddSelectedLayer(m_lSelectedLayers[m_lSelectedLayers.size() - 1] + 1);
}
else
{
@ -3264,7 +3523,7 @@ void CEditor::RenderLayers(CUIRect ToolBox, CUIRect View)
if(Input()->KeyIsPressed(KEY_LSHIFT) || Input()->KeyIsPressed(KEY_RSHIFT))
{
if(m_lSelectedLayers[m_lSelectedLayers.size() - 1] > 0)
m_lSelectedLayers.add(m_lSelectedLayers[m_lSelectedLayers.size() - 1] - 1);
AddSelectedLayer(m_lSelectedLayers[m_lSelectedLayers.size() - 1] - 1);
}
else
{

View file

@ -722,6 +722,9 @@ public:
m_SelectedQuadEnvelope = -1;
m_SelectedEnvelopePoint = -1;
m_QuadKnifeActive = false;
m_QuadKnifeCount = 0;
m_CommandBox = 0.0f;
m_aSettingsCommand[0] = 0;
@ -770,6 +773,7 @@ public:
CLayerGroup *GetSelectedGroup() const;
CSoundSource *GetSelectedSource();
void SelectLayer(int LayerIndex, int GroupIndex = -1);
void AddSelectedLayer(int LayerIndex);
void SelectQuad(int Index);
void DeleteSelectedQuads();
bool IsQuadSelected(int Index) const;
@ -905,6 +909,10 @@ public:
int m_SelectedSound;
int m_SelectedSource;
bool m_QuadKnifeActive;
int m_QuadKnifeCount;
vec2 m_aQuadKnifePoints[4];
IGraphics::CTextureHandle m_CheckerTexture;
IGraphics::CTextureHandle m_BackgroundTexture;
IGraphics::CTextureHandle m_CursorTexture;
@ -1000,6 +1008,10 @@ public:
void DoQuadEnvPoint(const CQuad *pQuad, int QIndex, int pIndex);
void DoQuadPoint(CQuad *pQuad, int QuadIndex, int v);
float TriangleArea(vec2 A, vec2 B, vec2 C);
bool IsInTriangle(vec2 Point, vec2 A, vec2 B, vec2 C);
void DoQuadKnife(int QuadIndex);
void DoSoundSource(CSoundSource *pSource, int Index);
void DoMapEditor(CUIRect View);

View file

@ -603,6 +603,17 @@ int CEditor::PopupQuad(CEditor *pEditor, CUIRect View, void *pContext)
return 1;
}
// slice button
View.HSplitBottom(6.0f, &View, &Button);
View.HSplitBottom(12.0f, &View, &Button);
static int s_SliceButton = 0;
if(pEditor->DoButton_Editor(&s_SliceButton, "Slice", 0, &Button, 0, "Enables quad knife mode"))
{
pEditor->m_QuadKnifeCount = 0;
pEditor->m_QuadKnifeActive = true;
return 1;
}
enum
{
PROP_POS_X = 0,