diff --git a/Fushigi/ui/CourseAreaEditContext.cs b/Fushigi/ui/CourseAreaEditContext.cs index 18669421..6ce74850 100644 --- a/Fushigi/ui/CourseAreaEditContext.cs +++ b/Fushigi/ui/CourseAreaEditContext.cs @@ -215,6 +215,20 @@ public void DeleteWall(CourseUnit unit, Wall wall) $"{IconUtil.ICON_TRASH} Delete Wall")); } + public void AddBeltRail(CourseUnit unit, BGUnitRail rail) + { + LogAdding(); + CommitAction(unit.mBeltRails.RevertableAdd(rail, + $"{IconUtil.ICON_PLUS_CIRCLE} Add Belt")); + } + + public void DeleteBeltRail(CourseUnit unit, BGUnitRail rail) + { + LogDeleting(); + CommitAction(unit.mBeltRails.RevertableRemove(rail, + $"{IconUtil.ICON_TRASH} Delete Belt")); + } + private void LogAdding(ulong hash) => LogAdding($"[{hash}]"); diff --git a/Fushigi/ui/SceneObjects/bgunit/BGUnitRailSceneObj.cs b/Fushigi/ui/SceneObjects/bgunit/BGUnitRailSceneObj.cs index f3a8bf5e..19edfa29 100644 --- a/Fushigi/ui/SceneObjects/bgunit/BGUnitRailSceneObj.cs +++ b/Fushigi/ui/SceneObjects/bgunit/BGUnitRailSceneObj.cs @@ -7,7 +7,7 @@ namespace Fushigi.ui.SceneObjects.bgunit { - internal class BGUnitRailSceneObj(CourseUnit unit, BGUnitRail rail) : ISceneObject, IViewportDrawable, IViewportSelectable + internal class BGUnitRailSceneObj(CourseUnit unit, BGUnitRail rail, bool isBelt) : ISceneObject, IViewportDrawable, IViewportSelectable { public IReadOnlyDictionary ChildPoints; public List GetSelected(CourseAreaEditContext ctx) => rail.Points.Where(ctx.IsSelected).ToList(); @@ -114,7 +114,7 @@ public void OnKeyDown(CourseAreaEditContext ctx, LevelViewport viewport) private bool HitTest(LevelViewport viewport) { - return LevelViewport.HitTestLineLoopPoint(GetPoints(viewport), 10f, + return MathUtil.HitTestLineLoopPoint(GetPoints(viewport), 10f, ImGui.GetMousePos()); } @@ -300,7 +300,7 @@ void IViewportDrawable.Draw2D(CourseAreaEditContext ctx, LevelViewport viewport, addPointPos = EvaluateAddPointPos(ctx, viewport); - if ((addPointPos.HasValue && ctx.IsSelected(rail)) || HitTest(viewport)) + if ((addPointPos.HasValue && ctx.IsSelected(rail)) || (!isBelt && HitTest(viewport))) isNewHoveredObj = true; bool isSelected = IsSelected(ctx); @@ -318,31 +318,49 @@ void IViewportDrawable.Draw2D(CourseAreaEditContext ctx, LevelViewport viewport, var lineThickness = viewport.IsHovered(this) ? 3.5f : 2.5f; + + for (int i = 0; i < rail.Points.Count; i++) { - Vector3 point = rail.Points[i].Position; - var pos2D = viewport.WorldToScreen(new(point.X, point.Y, point.Z)); + Vector3 pos = rail.Points[i].Position; - //Next pos 2D - Vector2 nextPos2D = Vector2.Zero; + Vector3 nextPos; if (i < rail.Points.Count - 1) //is not last point { - nextPos2D = viewport.WorldToScreen( - rail.Points[i + 1].Position); + nextPos = rail.Points[i + 1].Position; } else if (rail.IsClosed) //last point to first if closed { - nextPos2D = viewport.WorldToScreen( - rail.Points[0].Position); + nextPos = rail.Points[0].Position; } else //last point but not closed, draw no line continue; + var pos2D = viewport.WorldToScreen(pos); + var nextPos2D = viewport.WorldToScreen(nextPos); + uint line_color = IsValidAngle(pos2D, nextPos2D) ? Color_Default : Color_SlopeError; if (isSelected && line_color != Color_SlopeError) line_color = Color_SelectionEdit; - dl.AddLine(pos2D, nextPos2D, line_color, lineThickness); + if (isBelt) + { + var bottomPos2D = viewport.WorldToScreen(pos - Vector3.UnitY * 0.5f); + var bottomNextPos2D = viewport.WorldToScreen(nextPos - Vector3.UnitY * 0.5f); + + dl.AddQuadFilled(pos2D, nextPos2D, bottomNextPos2D, bottomPos2D, line_color & 0x00FFFFFF | 0x55000000); + dl.AddQuad(pos2D, nextPos2D, bottomNextPos2D, bottomPos2D, line_color, lineThickness - 1); + + if (MathUtil.HitTestConvexQuad(pos2D, nextPos2D, bottomNextPos2D, bottomPos2D, + ImGui.GetMousePos())) + { + isNewHoveredObj = true; + } + } + else + { + dl.AddLine(pos2D, nextPos2D, line_color, lineThickness); + } if (isSelected) { @@ -379,7 +397,9 @@ void IViewportDrawable.Draw2D(CourseAreaEditContext ctx, LevelViewport viewport, var pointB = viewport.WorldToScreen(rail.Points.GetWrapped(index).Position); var pointC = pos2D; - dl.AddTriangleFilled(pointA, pointB, pointC, 0x99FFFFFF); + if(!isBelt) + dl.AddTriangleFilled(pointA, pointB, pointC, 0x99FFFFFF); + if(rail.IsClosed || index > 0) dl.AddLine(pointA, pointC, 0xFFFFFFFF, 2.5f); if(rail.IsClosed || index < rail.Points.Count) diff --git a/Fushigi/ui/SceneObjects/bgunit/BGUnitSceneObj.cs b/Fushigi/ui/SceneObjects/bgunit/BGUnitSceneObj.cs index f3323b73..f22e6b5c 100644 --- a/Fushigi/ui/SceneObjects/bgunit/BGUnitSceneObj.cs +++ b/Fushigi/ui/SceneObjects/bgunit/BGUnitSceneObj.cs @@ -8,9 +8,15 @@ public void Update(ISceneUpdateContext ctx, bool isSelected) { unit.GenerateTileSubUnits(); - void CreateOrUpdateRail(BGUnitRail rail) + void CreateOrUpdateRail(BGUnitRail rail, bool isBelt = false) { - ctx.UpdateOrCreateObjFor(rail, () => new BGUnitRailSceneObj(unit, rail)); + ctx.UpdateOrCreateObjFor(rail, () => new BGUnitRailSceneObj(unit, rail, isBelt)); + } + + if (unit.mModelType is CourseUnit.ModelType.SemiSolid or CourseUnit.ModelType.Bridge) + { + foreach (var rail in unit.mBeltRails) + CreateOrUpdateRail(rail, true); } foreach (var wall in unit.Walls) @@ -21,10 +27,6 @@ void CreateOrUpdateRail(BGUnitRail rail) CreateOrUpdateRail(rail); } } - - //Don't include belt for now. TODO how should this be handled? - //foreach (var rail in unit.mBeltRails) - // CreateOrUpdateRail(rail); } } } diff --git a/Fushigi/ui/widgets/CourseScene.cs b/Fushigi/ui/widgets/CourseScene.cs index aff30fb8..27da7171 100644 --- a/Fushigi/ui/widgets/CourseScene.cs +++ b/Fushigi/ui/widgets/CourseScene.cs @@ -877,6 +877,90 @@ private void SelectionParameterPanel() ImGui.Columns(1); } + + if(mSelectedUnit.mModelType is CourseUnit.ModelType.SemiSolid) + { + ImGui.Spacing(); + ImGui.Separator(); + ImGui.Spacing(); + + if(ImGui.Button("Remove all Belts")) + { + var batchAction = editContext.BeginBatchAction(); + + for (int i = mSelectedUnit.mBeltRails.Count - 1; i >= 0; i--) + editContext.DeleteBeltRail(mSelectedUnit, mSelectedUnit.mBeltRails[i]); + + batchAction.Commit("Remove all Belts from TileUnit"); + } + + ImGui.SameLine(); + + if (ImGui.Button("Generate Belts")) + { + var batchAction = editContext.BeginBatchAction(); + + void ProcessRail(BGUnitRail rail) + { + if (rail.Points.Count <= 1) + return; + + BGUnitRail? firstBeltRail = null; + BGUnitRail? currentBeltRail = null; + + var lastPoint = new Vector3(float.NaN); + + for (int i = 0; i < rail.Points.Count; i++) + { + var point0 = rail.Points[i].Position; + var point1 = rail.Points.GetWrapped(i + 1).Position; + + if (point0.X >= point1.X) + continue; + + if (point0 != lastPoint) + { + if (currentBeltRail is not null) + editContext.AddBeltRail(mSelectedUnit, currentBeltRail); + + currentBeltRail = new BGUnitRail(mSelectedUnit); + currentBeltRail.Points.Add(new BGUnitRail.RailPoint(currentBeltRail, point0)); + firstBeltRail ??= currentBeltRail; + } + + currentBeltRail!.Points.Add(new BGUnitRail.RailPoint(currentBeltRail, point1)); + lastPoint = point1; + } + + var lastBeltRail = currentBeltRail; + + if(firstBeltRail is not null && lastBeltRail is not null && + firstBeltRail != lastBeltRail && + lastBeltRail.Points[^1].Position == firstBeltRail.Points[0].Position) + { + //connect first and last rail + + for (int i = 0; i < lastBeltRail.Points.Count-1; i++) + { + var position = lastBeltRail.Points[i].Position; + firstBeltRail.Points.Insert(i, new BGUnitRail.RailPoint(firstBeltRail, position)); + } + } + else if (lastBeltRail is not null) + editContext.AddBeltRail(mSelectedUnit, lastBeltRail); + } + + foreach (var wall in mSelectedUnit.Walls) + { + ProcessRail(wall.ExternalRail); + + foreach (var internalRail in wall.InternalRails) + ProcessRail(internalRail); + } + + batchAction.Commit("Add Belts"); + } + } } else if (editContext.IsSingleObjectSelected(out BGUnitRail? mSelectedUnitRail)) { @@ -1317,45 +1401,76 @@ void SelectRail() } } - if (ImGui.Button("Add Wall")) - editContext.AddWall(unit, new Wall(unit)); - ImGui.SameLine(); - if (ImGui.Button("Remove Wall")) + if (unit.mModelType is not CourseUnit.ModelType.Bridge) { - editContext.WithSuspendUpdateDo(() => + if (ImGui.Button("Add Wall")) + editContext.AddWall(unit, new Wall(unit)); + ImGui.SameLine(); + if (ImGui.Button("Remove Wall")) { - for (int i = unit.Walls.Count - 1; i >= 0; i--) + editContext.WithSuspendUpdateDo(() => { - //TODO is that REALLY how we want to do this? - if (editContext.IsSelected(unit.Walls[i].ExternalRail)) - editContext.DeleteWall(unit, unit.Walls[i]); - } - }); - } + for (int i = unit.Walls.Count - 1; i >= 0; i--) + { + //TODO is that REALLY how we want to do this? + if (editContext.IsSelected(unit.Walls[i].ExternalRail)) + editContext.DeleteWall(unit, unit.Walls[i]); + } + }); + } - foreach (var wall in unit.Walls) - { - if (wall.InternalRails.Count > 0) + for (int iWall = 0; iWall < unit.Walls.Count; iWall++) { - bool ex = ImGui.TreeNodeEx($"##{name}Wall{unit.Walls.IndexOf(wall)}", ImGuiTreeNodeFlags.DefaultOpen); - ImGui.SameLine(); + Wall wall = unit.Walls[iWall]; + if (wall.InternalRails.Count > 0) + { + ImGui.Unindent(); + bool ex = ImGui.TreeNodeEx($"##{name}Wall{iWall}", ImGuiTreeNodeFlags.DefaultOpen); + ImGui.SameLine(); - RailListItem("Wall", wall.ExternalRail, unit.Walls.IndexOf(wall)); + RailListItem("Wall", wall.ExternalRail, unit.Walls.IndexOf(wall)); - ImGui.Indent(); + ImGui.Indent(); - if (ex) + if (ex) + { + for (int iInternal = 0; iInternal < wall.InternalRails.Count; iInternal++) + { + BGUnitRail? rail = wall.InternalRails[iInternal]; + RailListItem("Internal Rail", rail, iInternal); + } + } + + ImGui.TreePop(); + } + else { - foreach (var rail in wall.InternalRails) - RailListItem("Internal Rail", rail, wall.InternalRails.IndexOf(rail)); + RailListItem("Wall", wall.ExternalRail, iWall); } - ImGui.Unindent(); + } + } - ImGui.TreePop(); + if (unit.mModelType is CourseUnit.ModelType.SemiSolid or CourseUnit.ModelType.Bridge) + { + if (ImGui.Button("Add Belt")) + editContext.AddBeltRail(unit, new BGUnitRail(unit)); + ImGui.SameLine(); + if (ImGui.Button("Remove Belt")) + { + editContext.WithSuspendUpdateDo(() => + { + for (int i = unit.mBeltRails.Count - 1; i >= 0; i--) + { + if (editContext.IsSelected(unit.mBeltRails[i])) + editContext.DeleteBeltRail(unit, unit.mBeltRails[i]); + } + }); } - else + + for (int iBeltRail = 0; iBeltRail < unit.mBeltRails.Count; iBeltRail++) { - RailListItem("Wall", wall.ExternalRail, unit.Walls.IndexOf(wall)); + BGUnitRail beltRail = unit.mBeltRails[iBeltRail]; + RailListItem("Belt", beltRail, iBeltRail); } } ImGui.TreePop(); @@ -1878,7 +1993,7 @@ private void CourseMiniView() { foreach(var wall in foregroundTileUnits .SelectMany(x => x.Walls) - .Where(x => x.ExternalRail.Points[0].Position.Z == subUnit.mOrigin.Z)) + .Where(x => x.ExternalRail.Points.FirstOrDefault()?.Position.Z == subUnit.mOrigin.Z)) { var rail = wall.ExternalRail; diff --git a/Fushigi/ui/widgets/LevelViewport.cs b/Fushigi/ui/widgets/LevelViewport.cs index 4e23a37c..0c6fcc3d 100644 --- a/Fushigi/ui/widgets/LevelViewport.cs +++ b/Fushigi/ui/widgets/LevelViewport.cs @@ -929,7 +929,7 @@ Vector2[] GetPoints() } bool isSelected = mEditContext.IsSelected(rail); - bool hovered = LevelViewport.HitTestLineLoopPoint(GetPoints(), 10f, ImGui.GetMousePos()); + bool hovered = MathUtil.HitTestLineLoopPoint(GetPoints(), 10f, ImGui.GetMousePos()); CourseRail.CourseRailPoint selectedPoint = null; @@ -1172,11 +1172,11 @@ Vector2[] GetPoints() string name = actor.mPackName; - isHovered = HitTestConvexPolygonPoint(s_actorRectPolygon, ImGui.GetMousePos()); + isHovered = MathUtil.HitTestConvexPolygonPoint(s_actorRectPolygon, ImGui.GetMousePos()); if (name.Contains("Area")) { - isHovered = HitTestLineLoopPoint(s_actorRectPolygon, 4f, + isHovered = MathUtil.HitTestLineLoopPoint(s_actorRectPolygon, 4f, ImGui.GetMousePos()); } @@ -1189,61 +1189,6 @@ Vector2[] GetPoints() mHoveredObject = newHoveredObject; } - - /// - /// Does a collision check between a convex polygon and a point - /// - /// Points of Polygon a in Clockwise orientation (in screen coordinates) - /// Point - /// - public static bool HitTestConvexPolygonPoint(ReadOnlySpan polygon, Vector2 point) - { - // separating axis theorem (lite) - // we can view the point as a polygon with 0 sides and 1 point - for (int i = 0; i < polygon.Length; i++) - { - var p1 = polygon[i]; - var p2 = polygon[(i + 1) % polygon.Length]; - var vec = (p2 - p1); - var normal = new Vector2(vec.Y, -vec.X); - - (Vector2 origin, Vector2 normal) edge = (p1, normal); - - if (Vector2.Dot(point - edge.origin, edge.normal) >= 0) - return false; - } - - //no separating axis found -> collision - return true; - } - - /// - /// Does a collision check between a LineLoop and a point - /// - /// Points of a LineLoop - /// Point - /// - public static bool HitTestLineLoopPoint(ReadOnlySpan points, float thickness, Vector2 point) - { - for (int i = 0; i < points.Length; i++) - { - var p1 = points[i]; - var p2 = points[(i + 1) % points.Length]; - if (HitTestPointLine(point, - p1, p2, thickness)) - return true; - } - - return false; - } - - static bool HitTestPointLine(Vector2 p, Vector2 a, Vector2 b, float thickness) - { - Vector2 pa = p - a, ba = b - a; - float h = Math.Clamp(Vector2.Dot(pa, ba) / - Vector2.Dot(ba, ba), 0, 1); - return (pa - ba * h).Length() < thickness / 2; - } } static class ColorExtensions diff --git a/Fushigi/util/MathUtil.cs b/Fushigi/util/MathUtil.cs index 8f5f4561..93bb9151 100644 --- a/Fushigi/util/MathUtil.cs +++ b/Fushigi/util/MathUtil.cs @@ -60,6 +60,66 @@ static float isLeft(Vector2 p0, Vector2 p1, Vector2 point) => } return wn; } + + /// + /// Does a collision check between a convex polygon and a point + /// + /// Points of Polygon a in Clockwise orientation (in screen coordinates) + /// Point + /// + public static bool HitTestConvexPolygonPoint(ReadOnlySpan polygon, Vector2 point) + { + // separating axis theorem (lite) + // we can view the point as a polygon with 0 sides and 1 point + for (int i = 0; i < polygon.Length; i++) + { + var p1 = polygon[i]; + var p2 = polygon[(i + 1) % polygon.Length]; + var vec = (p2 - p1); + var normal = new Vector2(vec.Y, -vec.X); + + (Vector2 origin, Vector2 normal) edge = (p1, normal); + + if (Vector2.Dot(point - edge.origin, edge.normal) >= 0) + return false; + } + + //no separating axis found -> collision + return true; + } + + public static bool HitTestConvexQuad(Vector2 p1, Vector2 p2, Vector2 p3, Vector2 p4, Vector2 point) + { + return HitTestConvexPolygonPoint([p1, p2, p3, p4], point); + } + + /// + /// Does a collision check between a LineLoop and a point + /// + /// Points of a LineLoop + /// Point + /// + public static bool HitTestLineLoopPoint(ReadOnlySpan points, float thickness, Vector2 point) + { + for (int i = 0; i < points.Length; i++) + { + var p1 = points[i]; + var p2 = points[(i + 1) % points.Length]; + if (HitTestPointLine(point, + p1, p2, thickness)) + return true; + } + + return false; + } + + static bool HitTestPointLine(Vector2 p, Vector2 a, Vector2 b, float thickness) + { + Vector2 pa = p - a, ba = b - a; + float h = Math.Clamp(Vector2.Dot(pa, ba) / + Vector2.Dot(ba, ba), 0, 1); + return (pa - ba * h).Length() < thickness / 2; + } } struct BoundingBox2D(Vector2 min, Vector2 max)