From 3cae0994b684ee93838cf3657416ac3dec77e3c6 Mon Sep 17 00:00:00 2001 From: ikpil Date: Thu, 13 Aug 2026 00:32:32 +0900 Subject: [PATCH] [upstream] Public API json (erincatto/box2d#1052) Port upstream commit 3704352041ae2cdf6c9f3cda23d1b2e36c407b1b while preserving the original source structure where C# permits. Replace the contact draw enum with drawContacts and drawAnchorA, update world debug drawing, and add regression coverage for the public API and anchor selection. Align the samples with upstream sample ownership and selection flow, configurable contact recycle distance, cursor-centered zoom, and rolling profile averages. Keep the explicit profile field mapping and upstream comments so the C and C# implementations remain easy to compare. C-only CMake, compiler, Doxygen, API JSON generation, and workflow changes are intentionally omitted because they do not apply to the managed project. --- src/Box2D.NET.Samples/SampleApp.cs | 270 +++-------------- src/Box2D.NET.Samples/SampleContext.cs | 27 +- src/Box2D.NET.Samples/Samples/Sample.cs | 376 +++++++++++++++++------- src/Box2D.NET.Samples/Settings.cs | 2 - src/Box2D.NET/B2BodyDef.cs | 2 +- src/Box2D.NET/B2ContactDrawType.cs | 12 - src/Box2D.NET/B2DebugDraw.cs | 5 +- src/Box2D.NET/B2Worlds.cs | 14 +- test/Box2D.NET.Test/B2DebugDrawTests.cs | 107 +++++++ 9 files changed, 442 insertions(+), 373 deletions(-) delete mode 100644 src/Box2D.NET/B2ContactDrawType.cs create mode 100644 test/Box2D.NET.Test/B2DebugDrawTests.cs diff --git a/src/Box2D.NET.Samples/SampleApp.cs b/src/Box2D.NET.Samples/SampleApp.cs index a1a6248f..b6b7d9cc 100644 --- a/src/Box2D.NET.Samples/SampleApp.cs +++ b/src/Box2D.NET.Samples/SampleApp.cs @@ -27,6 +27,7 @@ using static Box2D.NET.B2Timers; using static Box2D.NET.Samples.Graphics.Draws; using static Box2D.NET.Samples.Graphics.Cameras; +using static Box2D.NET.Samples.Samples.Sample; using ErrorCode = Silk.NET.GLFW.ErrorCode; using Monitor = Silk.NET.GLFW.Monitor; using MouseButton = Silk.NET.GLFW.MouseButton; @@ -41,8 +42,6 @@ public class SampleApp private IWindow _window; private IInputContext _input; private ImGuiController _imgui; - private int s_selection = 0; - private Sample s_sample = null; private SampleContext _context; private bool s_rightMouseDown = false; private B2Vec2 s_clickPointWS = b2Vec2_zero; @@ -167,8 +166,7 @@ private void OnWindowClosingSafe() private void OnWindowClosing() { - s_sample?.Dispose(); - s_sample = null; + _context.sample?.Dispose(); DestroyDraw(_context.draw); DestroyUI(); } @@ -246,7 +244,6 @@ private void OnWindowLoad() _context.draw = CreateDraw(_context); _context.sampleIndex = b2ClampInt(_context.sampleIndex, 0, SampleFactory.Shared.SampleCount - 1); - s_selection = _context.sampleIndex; // todo put this in _context.settings CreateUI(glslVersion); @@ -303,27 +300,13 @@ private void OnWindowUpdate(double dt) // For the Tracy profiler //FrameMark; - if (s_selection != _context.sampleIndex) - { - ResetView(_context.camera); - _context.sampleIndex = s_selection; - - // #todo restore all drawing settings that may have been overridden by a sample - _context.subStepCount = 4; - _context.debugDraw.drawJoints = true; - - s_sample?.Dispose(); - s_sample = null; - s_sample = SampleFactory.Shared.Create(_context.sampleIndex, _context); - } - - if (s_sample == null) + if (_context.sample == null) { // delayed creation because imgui doesn't create fonts until NewFrame() is called - s_sample = SampleFactory.Shared.Create(_context.sampleIndex, _context); + _context.sample = SampleFactory.Shared.Create(_context.sampleIndex, _context); } - s_sample.Step(); + _context.sample.Step(); _context.glfw.PollEvents(); @@ -374,19 +357,19 @@ private void OnWindowRender(double dt) ImGui.Begin("Overlay", ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoInputs | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoScrollbar); ImGui.End(); - s_sample.ResetText(); + _context.sample.ResetText(); var title = SampleFactory.Shared.GetTitle(_context.sampleIndex); - s_sample.DrawColoredTextLine(B2HexColor.b2_colorYellow, title); + _context.sample.DrawColoredTextLine(B2HexColor.b2_colorYellow, title); - string buffer = $"{1000.0f * _frameTime:0.0} ms - step {s_sample.m_stepCount} - " + + string buffer = $"{1000.0f * _frameTime:0.0} ms - step {_context.sample.m_stepCount} - " + $"camera ({_context.camera.center.X:G}, {_context.camera.center.Y:G}, {_context.camera.zoom:G})"; DrawScreenString(_context.draw, 5.0f, _context.camera.height - 18.0f, B2HexColor.b2_colorSeaGreen, buffer); - s_sample.Draw(); + _context.sample.Draw(); FlushDraw(_context.draw, _context.camera); - UpdateUI(); + UpdateSampleUI(_context); //ImGui.ShowDemoWindow(); @@ -443,16 +426,6 @@ private void glfwErrorCallback(ErrorCode error, string description) Logger.Information($"GLFW error occurred. Code: {error}. Description: {description}"); } - private void RestartSample() - { - s_sample?.Dispose(); - s_sample = null; - _context.restart = true; - - s_sample = SampleFactory.Shared.Create(_context.sampleIndex, _context); - _context.restart = false; - } - private void CreateUI(string glslVersion) { //IMGUI_CHECKVERSION(); @@ -541,7 +514,7 @@ private unsafe void KeyCallback(WindowHandle* window, Keys key, int scancode, In if (0 != ((uint)mods & (uint)KeyModifiers.Control)) { B2Vec2 newOrigin = new B2Vec2(2.0f, 0.0f); - s_sample.ShiftOrigin(newOrigin); + _context.sample.ShiftOrigin(newOrigin); } else { @@ -555,7 +528,7 @@ private unsafe void KeyCallback(WindowHandle* window, Keys key, int scancode, In if (0 != ((uint)mods & (uint)KeyModifiers.Control)) { B2Vec2 newOrigin = new B2Vec2(-2.0f, 0.0f); - s_sample.ShiftOrigin(newOrigin); + _context.sample.ShiftOrigin(newOrigin); } else { @@ -569,7 +542,7 @@ private unsafe void KeyCallback(WindowHandle* window, Keys key, int scancode, In if (0 != ((uint)mods & (uint)KeyModifiers.Control)) { B2Vec2 newOrigin = new B2Vec2(0.0f, 2.0f); - s_sample.ShiftOrigin(newOrigin); + _context.sample.ShiftOrigin(newOrigin); } else { @@ -583,7 +556,7 @@ private unsafe void KeyCallback(WindowHandle* window, Keys key, int scancode, In if (0 != ((uint)mods & (uint)KeyModifiers.Control)) { B2Vec2 newOrigin = new B2Vec2(0.0f, -2.0f); - s_sample.ShiftOrigin(newOrigin); + _context.sample.ShiftOrigin(newOrigin); } else { @@ -597,7 +570,7 @@ private unsafe void KeyCallback(WindowHandle* window, Keys key, int scancode, In break; case Keys.R: - RestartSample(); + SelectSample(_context, _context.sampleIndex, true); break; case Keys.O: @@ -610,20 +583,28 @@ private unsafe void KeyCallback(WindowHandle* window, Keys key, int scancode, In case Keys.LeftBracket: // Switch to previous test - --s_selection; - if (s_selection < 0) { - s_selection = SampleFactory.Shared.SampleCount - 1; + int selection = _context.sampleIndex - 1; + if (selection < 0) + { + selection = SampleFactory.Shared.SampleCount - 1; + } + + SelectSample(_context, selection, false); } break; case Keys.RightBracket: // Switch to next test - ++s_selection; - if (s_selection == SampleFactory.Shared.SampleCount) { - s_selection = 0; + int selection = _context.sampleIndex + 1; + if (selection == SampleFactory.Shared.SampleCount) + { + selection = 0; + } + + SelectSample(_context, selection, false); } break; @@ -633,9 +614,9 @@ private unsafe void KeyCallback(WindowHandle* window, Keys key, int scancode, In break; default: - if (null != s_sample) + if (_context.sample != null) { - s_sample.Keyboard(key); + _context.sample.Keyboard(key); } break; @@ -681,12 +662,12 @@ private unsafe void MouseButtonCallback(WindowHandle* window, MouseButton button B2Vec2 pw = ConvertScreenToWorld(_context.camera, ps); if (action == InputAction.Press) { - s_sample.MouseDown(pw, button, modifiers); + _context.sample.MouseDown(pw, button, modifiers); } if (action == InputAction.Release) { - s_sample.MouseUp(pw, button); + _context.sample.MouseUp(pw, button); } } else if (button == MouseButton.Right) @@ -722,7 +703,7 @@ private unsafe void MouseMotionCallback(WindowHandle* window, double xd, double //ImGui_ImplGlfw_CursorPosCallback(window, ps.x, ps.y); B2Vec2 pw = ConvertScreenToWorld(_context.camera, ps); - s_sample?.MouseMove(pw); + _context.sample.MouseMove(pw); if (s_rightMouseDown) { @@ -747,6 +728,10 @@ private unsafe void ScrollCallback(WindowHandle* window, double dx, double dy) return; } + _context.glfw.GetCursorPos(_context.window, out double xd, out double yd); + B2Vec2 ps = new B2Vec2((float)xd, (float)yd); + B2Vec2 pw1 = ConvertScreenToWorld(_context.camera, ps); + if (dy > 0) { _context.camera.zoom /= 1.1f; @@ -755,182 +740,9 @@ private unsafe void ScrollCallback(WindowHandle* window, double dx, double dy) { _context.camera.zoom *= 1.1f; } - } - - private void UpdateUI() - { - int maxWorkers = B2_MAX_WORKERS; - - float fontSize = ImGui.GetFontSize(); - float menuWidth = 13.0f * fontSize; - if (_context.showUI) - { - ImGui.SetNextWindowPos(new Vector2(_context.camera.width - menuWidth - 0.5f * fontSize, 0.5f * fontSize)); - ImGui.SetNextWindowSize(new Vector2(menuWidth, _context.camera.height - fontSize)); - - ImGui.Begin("Tools", ref _context.showUI, ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse); - - if (ImGui.BeginTabBar("ControlTabs", ImGuiTabBarFlags.None)) - { - if (ImGui.BeginTabItem("Controls")) - { - ImGui.PushItemWidth(100.0f); - ImGui.SliderInt("Sub-steps", ref _context.subStepCount, 1, 32); - ImGui.SliderFloat("Hertz", ref _context.hertz, 5.0f, 240.0f, "%.0f hz"); - - if (ImGui.SliderInt("Workers", ref _context.workerCount, 1, maxWorkers)) - { - _context.workerCount = b2ClampInt(_context.workerCount, 1, maxWorkers); - RestartSample(); - } - - ImGui.PopItemWidth(); - - ImGui.Separator(); - - ImGui.Checkbox("Sleep", ref _context.enableSleep); - ImGui.Checkbox("Warm Starting", ref _context.enableWarmStarting); - ImGui.Checkbox("Continuous", ref _context.enableContinuous); - ImGui.Checkbox("Contact Recycling", ref _context.enableRecycling); - - ImGui.Separator(); - - ImGui.Checkbox("Shapes", ref _context.debugDraw.drawShapes); - ImGui.Checkbox("Joints", ref _context.debugDraw.drawJoints); - ImGui.Checkbox("Joint Extras", ref _context.debugDraw.drawJointExtras); - ImGui.Checkbox("Bounds", ref _context.debugDraw.drawBounds); - ImGui.Checkbox("Mass", ref _context.debugDraw.drawMass); - ImGui.Checkbox("Body Names", ref _context.debugDraw.drawBodyNames); - ImGui.Checkbox("Graph Colors", ref _context.debugDraw.drawGraphColors); - ImGui.Checkbox("Islands", ref _context.debugDraw.drawIslands); - ImGui.Checkbox("Counters", ref _context.drawCounters); - ImGui.Checkbox("Profile", ref _context.drawProfile); - ImGui.Checkbox("Frame Time", ref _context.frameTime); - - ImGui.Separator(); - - { - bool changed = false; - string[] drawTypes = - [ - "None", "Clip", "AnchorA", "AnchorB", "Average" - ]; - int drawType = (int)_context.debugDraw.contactDrawType; - changed = changed || ImGui.Combo("Contact", ref drawType, drawTypes, drawTypes.Length); - _context.debugDraw.contactDrawType = (B2ContactDrawType)drawType; - } - - ImGui.Checkbox("Contact Normals", ref _context.debugDraw.drawContactNormals); - ImGui.Checkbox("Contact Features", ref _context.debugDraw.drawContactFeatures); - ImGui.Checkbox("Contact Forces", ref _context.debugDraw.drawContactForces); - ImGui.Checkbox("Friction Forces", ref _context.debugDraw.drawFrictionForces); - - ImGui.Separator(); - - - ImGui.PushItemWidth(80.0f); - ImGui.InputFloat("Joint Scale", ref _context.debugDraw.jointScale); - ImGui.InputFloat("Force Scale", ref _context.debugDraw.forceScale); - ImGui.PopItemWidth(); - - Vector2 button_sz = new Vector2(-1, 0); - if (ImGui.Button("Pause (P)", button_sz)) - { - _context.pause = !_context.pause; - } - - if (ImGui.Button("Single Step (O)", button_sz)) - { - _context.singleStep = !_context.singleStep; - } - - if (ImGui.Button("Dump Mem Stats", button_sz)) - { - b2World_DumpMemoryStats(s_sample.m_worldId); - } - - if (ImGui.Button("Reset Profile", button_sz)) - { - s_sample.ResetProfile(); - } - - if (ImGui.Button("Restart (R)", button_sz)) - { - RestartSample(); - } - - if (ImGui.Button("Quit", button_sz)) - { - unsafe - { - _context.glfw.SetWindowShouldClose(_context.window, true); - } - } - - ImGui.EndTabItem(); - } - ImGuiTreeNodeFlags leafNodeFlags = ImGuiTreeNodeFlags.OpenOnArrow | ImGuiTreeNodeFlags.OpenOnDoubleClick; - leafNodeFlags |= ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen; - - ImGuiTreeNodeFlags nodeFlags = ImGuiTreeNodeFlags.OpenOnArrow | ImGuiTreeNodeFlags.OpenOnDoubleClick; - - if (ImGui.BeginTabItem("Samples")) - { - int categoryIndex = 0; - string category = SampleFactory.Shared.GetCategory(categoryIndex); - int i = 0; - while (i < SampleFactory.Shared.SampleCount) - { - bool categorySelected = category == SampleFactory.Shared.GetCategory(_context.sampleIndex); - ImGuiTreeNodeFlags nodeSelectionFlags = categorySelected ? ImGuiTreeNodeFlags.Selected : 0; - bool nodeOpen = ImGui.TreeNodeEx(category, nodeFlags | nodeSelectionFlags); - - if (nodeOpen) - { - while (i < SampleFactory.Shared.SampleCount && category == SampleFactory.Shared.GetCategory(i)) - { - ImGuiTreeNodeFlags selectionFlags = 0; - if (_context.sampleIndex == i) - { - selectionFlags = ImGuiTreeNodeFlags.Selected; - } - - ImGui.TreeNodeEx(SampleFactory.Shared.GetName(i), leafNodeFlags | selectionFlags); - if (ImGui.IsItemClicked()) - { - s_selection = i; - } - - ++i; - } - - ImGui.TreePop(); - } - else - { - while (i < SampleFactory.Shared.SampleCount && category == SampleFactory.Shared.GetCategory(i)) - { - ++i; - } - } - - if (i < SampleFactory.Shared.SampleCount) - { - category = SampleFactory.Shared.GetCategory(i); - categoryIndex = i; - } - } - - ImGui.EndTabItem(); - } - - ImGui.EndTabBar(); - } - - ImGui.End(); - - s_sample.UpdateGui(); - } + B2Vec2 pw2 = ConvertScreenToWorld(_context.camera, ps); + _context.camera.center -= pw2 - pw1; } + } diff --git a/src/Box2D.NET.Samples/SampleContext.cs b/src/Box2D.NET.Samples/SampleContext.cs index a10ad239..a55a0e31 100644 --- a/src/Box2D.NET.Samples/SampleContext.cs +++ b/src/Box2D.NET.Samples/SampleContext.cs @@ -6,10 +6,12 @@ using System.IO; using System.Runtime.CompilerServices; using Box2D.NET.Samples.Graphics; +using Box2D.NET.Samples.Samples; using ImGuiNET; using Silk.NET.GLFW; using Silk.NET.OpenGL; using static Box2D.NET.B2Types; +using static Box2D.NET.B2Constants; using static Box2D.NET.Samples.Graphics.Backgrounds; using static Box2D.NET.Samples.Graphics.Draws; @@ -20,11 +22,20 @@ public class SampleContext // public readonly string Signature; public readonly Glfw glfw; + public GL gl; + public unsafe WindowHandle* window; public readonly Camera camera; public Draw draw; + public Sample sample = null; + public B2Capacity capacity; + public B2DebugDraw debugDraw; + public ImFontPtr regularFont; + public ImFontPtr mediumFont; + public ImFontPtr largeFont; public float uiScale = 1.0f; public float hertz = 60.0f; + public float recycleDistance = 0.05f; public int subStepCount = 4; public int workerCount = 1; public bool restart = false; @@ -34,25 +45,12 @@ public class SampleContext public bool drawProfile = false; public bool enableWarmStarting = true; public bool enableContinuous = true; - public bool enableRecycling = true; public bool enableSleep = true; public bool showUI = true; public bool frameTime = false; // These are persisted public int sampleIndex = 0; - public B2Capacity capacity; - - - public B2DebugDraw debugDraw; - - public ImFontPtr regularFont; - public ImFontPtr mediumFont; - public ImFontPtr largeFont; - - // - public GL gl; - public unsafe WindowHandle* window; private static string CreateSignature(string member, string file, int line) { @@ -92,6 +90,8 @@ private SampleContext(string signature, Glfw glfw) public void Load() { + recycleDistance = B2_CONTACT_RECYCLE_DISTANCE; + var settings = Settings.Load(); // @@ -110,7 +110,6 @@ public void Load() debugDraw.drawJoints = settings.drawJoints; // - debugDraw.contactDrawType = settings.contactDrawType; debugDraw.drawShapes = settings.drawShapes; debugDraw.drawJoints = settings.drawJoints; debugDraw.drawJointExtras = settings.drawJointExtras; diff --git a/src/Box2D.NET.Samples/Samples/Sample.cs b/src/Box2D.NET.Samples/Samples/Sample.cs index 7db11327..f375e59c 100644 --- a/src/Box2D.NET.Samples/Samples/Sample.cs +++ b/src/Box2D.NET.Samples/Samples/Sample.cs @@ -59,13 +59,13 @@ public class Sample : IDisposable private ulong m_profileWriteIndex; // - private B2Profile m_totalProfile; private static bool s_showProfilePlots; private static readonly bool[] s_profileRowOpen = new bool[22]; // private bool m_didStep; + // private readonly float[] m_frameTimes; public Sample(SampleContext context) @@ -94,8 +94,6 @@ public Sample(SampleContext context) m_profileReadIndex = 0; m_profileWriteIndex = 0; - m_totalProfile = new B2Profile(); - g_randomSeed = RAND_SEED; CreateWorld(); @@ -124,6 +122,7 @@ public void CreateWorld() worldDef.capacity = m_context.capacity; m_worldId = b2CreateWorld(worldDef); + b2World_SetContactRecycleDistance(m_worldId, m_context.recycleDistance); } public void TestMathCpp() @@ -166,34 +165,86 @@ public virtual void UpdateGui() int count = (int)(m_profileWriteIndex - m_profileReadIndex); // Unroll ring buffer into per-field histories. - const int rowCount = 22; - float[][] histories = new float[rowCount][]; - for (int row = 0; row < rowCount; ++row) + const int kRowCount = 22; + float[][] histories = new float[kRowCount][]; + float[] totals = new float[kRowCount]; + for (int i = 0; i < kRowCount; ++i) { - histories[row] = new float[m_profileCapacity]; + histories[i] = new float[m_profileCapacity]; } for (int i = 0; i < count; ++i) { - int index = (int)((m_profileReadIndex + (ulong)i) & (m_profileCapacity - 1)); - B2Profile profile = m_profiles[index]; - for (int row = 0; row < rowCount; ++row) - { - histories[row][i] = GetProfileValue(profile, row); - } + int idx = (int)((m_profileReadIndex + (ulong)i) & (m_profileCapacity - 1)); + ref readonly B2Profile p = ref m_profiles[idx]; + histories[0][i] = p.step; + histories[1][i] = p.pairs; + histories[2][i] = p.collide; + histories[3][i] = p.solve; + histories[4][i] = p.solverSetup; + histories[5][i] = p.constraints; + histories[6][i] = p.prepareConstraints; + histories[7][i] = p.integrateVelocities; + histories[8][i] = p.warmStart; + histories[9][i] = p.solveImpulses; + histories[10][i] = p.integratePositions; + histories[11][i] = p.relaxImpulses; + histories[12][i] = p.applyRestitution; + histories[13][i] = p.storeImpulses; + histories[14][i] = p.splitIslands; + histories[15][i] = p.transforms; + histories[16][i] = p.jointEvents; + histories[17][i] = p.hitEvents; + histories[18][i] = p.refit; + histories[19][i] = p.sleepIslands; + histories[20][i] = p.bullets; + histories[21][i] = p.sensors; + + totals[0] += p.step; + totals[1] += p.pairs; + totals[2] += p.collide; + totals[3] += p.solve; + totals[4] += p.solverSetup; + totals[5] += p.constraints; + totals[6] += p.prepareConstraints; + totals[7] += p.integrateVelocities; + totals[8] += p.warmStart; + totals[9] += p.solveImpulses; + totals[10] += p.integratePositions; + totals[11] += p.relaxImpulses; + totals[12] += p.applyRestitution; + totals[13] += p.storeImpulses; + totals[14] += p.splitIslands; + totals[15] += p.transforms; + totals[16] += p.jointEvents; + totals[17] += p.hitEvents; + totals[18] += p.refit; + totals[19] += p.sleepIslands; + totals[20] += p.bullets; + totals[21] += p.sensors; } - float[] avg = new float[rowCount]; - if (m_stepCount > 0) + ref readonly B2Profile cur = ref m_profiles[m_currentProfileIndex]; + float[] now = + [ + cur.step, cur.pairs, cur.collide, cur.solve, cur.solverSetup, + cur.constraints, cur.prepareConstraints, cur.integrateVelocities, cur.warmStart, + cur.solveImpulses, cur.integratePositions, cur.relaxImpulses, cur.applyRestitution, + cur.storeImpulses, cur.splitIslands, cur.transforms, cur.jointEvents, + cur.hitEvents, cur.refit, cur.sleepIslands, cur.bullets, cur.sensors + ]; + + // Rolling average + float[] avg = new float[kRowCount]; + if (count > 0) { - float scale = 1.0f / m_stepCount; - for (int row = 0; row < rowCount; ++row) + float scale = 1.0f / count; + for (int i = 0; i < kRowCount; ++i) { - avg[row] = scale * GetProfileValue(m_totalProfile, row); + avg[i] = scale * totals[i]; } } - ref readonly B2Profile current = ref m_profiles[m_currentProfileIndex]; string[] names = [ "step", "pairs", "collide", "solve", "setup", "constraints", "prepare", @@ -203,11 +254,11 @@ public virtual void UpdateGui() ]; int[] indents = [0, 0, 0, 0, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 0]; // Derive parent/child links from the indent levels so we can collapse subtrees. - int[] parents = new int[rowCount]; - bool[] hasChildren = new bool[rowCount]; + int[] parents = new int[kRowCount]; + bool[] hasChildren = new bool[kRowCount]; int[] stack = new int[8]; int stackSize = 0; - for (int i = 0; i < rowCount; ++i) + for (int i = 0; i < kRowCount; ++i) { while (stackSize > 0 && indents[stack[stackSize - 1]] >= indents[i]) { @@ -237,15 +288,6 @@ public virtual void UpdateGui() colorDefault, colorDefault, colorDefault, colorDefault, colorDefault, colorDefault, colorDefault, colorDefault, colorDefault, colorDefault ]; - float[] now = - [ - current.step, current.pairs, current.collide, current.solve, current.solverSetup, - current.constraints, current.prepareConstraints, current.integrateVelocities, current.warmStart, - current.solveImpulses, current.integratePositions, current.relaxImpulses, current.applyRestitution, - current.storeImpulses, current.splitIslands, current.transforms, current.jointEvents, - current.hitEvents, current.refit, current.sleepIslands, current.bullets, current.sensors - ]; - if (ImGui.Button("Reset")) { ResetProfile(); @@ -272,9 +314,9 @@ public virtual void UpdateGui() float rowHeight = 1.5f * fontSize; // Bars are drawn relative to the step row so the proportions are visually consistent. - float stepNow = b2MaxFloat(current.step, 0.001f); + float stepNow = b2MaxFloat(cur.step, 0.001f); - for (int row = 0; row < rowCount; ++row) + for (int row = 0; row < kRowCount; ++row) { bool visible = true; for (int parent = parents[row]; parent >= 0; parent = parents[parent]) @@ -455,36 +497,6 @@ public virtual void UpdateGui() } } - private static float GetProfileValue(in B2Profile profile, int row) - { - return row switch - { - 0 => profile.step, - 1 => profile.pairs, - 2 => profile.collide, - 3 => profile.solve, - 4 => profile.solverSetup, - 5 => profile.constraints, - 6 => profile.prepareConstraints, - 7 => profile.integrateVelocities, - 8 => profile.warmStart, - 9 => profile.solveImpulses, - 10 => profile.integratePositions, - 11 => profile.relaxImpulses, - 12 => profile.applyRestitution, - 13 => profile.storeImpulses, - 14 => profile.splitIslands, - 15 => profile.transforms, - 16 => profile.jointEvents, - 17 => profile.hitEvents, - 18 => profile.refit, - 19 => profile.sleepIslands, - 20 => profile.bullets, - 21 => profile.sensors, - _ => 0.0f, - }; - } - private static Vector4 HexToColor(B2HexColor color) { uint hex = (uint)color; @@ -722,11 +734,7 @@ public void DrawTextLine(string text) public void ResetProfile() { - m_totalProfile = new B2Profile(); m_stepCount = 0; - m_currentProfileIndex = 0; - m_profileReadIndex = 0; - m_profileWriteIndex = 0; } public virtual void Step() @@ -769,19 +777,9 @@ public virtual void Step() b2World_EnableWarmStarting(m_worldId, m_context.enableWarmStarting); b2World_EnableContinuous(m_worldId, m_context.enableContinuous); - if (m_context.enableRecycling) - { - b2World_SetContactRecycleDistance(m_worldId, B2_CONTACT_RECYCLE_DISTANCE); - } - else - { - b2World_SetContactRecycleDistance(m_worldId, 0.0f); - } - for (int i = 0; i < 1; ++i) { b2World_Step(m_worldId, timeStep, m_context.subStepCount); - // m_taskCount = 0; } if (timeStep > 0.0f) @@ -799,33 +797,6 @@ public virtual void Step() m_profileWriteIndex += 1; } - // Accumulate profile averages - if (m_didStep) - { - B2Profile p = m_profiles[m_currentProfileIndex]; - m_totalProfile.step += p.step; - m_totalProfile.pairs += p.pairs; - m_totalProfile.collide += p.collide; - m_totalProfile.solve += p.solve; - m_totalProfile.solverSetup += p.solverSetup; - m_totalProfile.constraints += p.constraints; - m_totalProfile.prepareConstraints += p.prepareConstraints; - m_totalProfile.integrateVelocities += p.integrateVelocities; - m_totalProfile.warmStart += p.warmStart; - m_totalProfile.solveImpulses += p.solveImpulses; - m_totalProfile.integratePositions += p.integratePositions; - m_totalProfile.relaxImpulses += p.relaxImpulses; - m_totalProfile.applyRestitution += p.applyRestitution; - m_totalProfile.storeImpulses += p.storeImpulses; - m_totalProfile.transforms += p.transforms; - m_totalProfile.splitIslands += p.splitIslands; - m_totalProfile.jointEvents += p.jointEvents; - m_totalProfile.hitEvents += p.hitEvents; - m_totalProfile.refit += p.refit; - m_totalProfile.bullets += p.bullets; - m_totalProfile.sleepIslands += p.sleepIslands; - m_totalProfile.sensors += p.sensors; - } } public virtual void Draw() @@ -854,4 +825,201 @@ protected InputAction GetKey(Keys key) { return GlfwHelpers.GetKey(m_context, key); } + + public static void SelectSample(SampleContext context, int selection, bool restart) + { + if (restart == false) + { + ResetView(context.camera); + context.sampleIndex = selection; + context.subStepCount = 4; + context.debugDraw.drawJoints = true; + } + + context.sample?.Dispose(); + context.sample = null; + context.restart = restart; + context.sample = SampleFactory.Shared.Create(context.sampleIndex, context); + context.restart = false; + } + + public static void UpdateSampleUI(SampleContext context) + { + int maxWorkers = B2_MAX_WORKERS; + B2WorldId worldId = context.sample.m_worldId; + + float fontSize = ImGui.GetFontSize(); + float menuWidth = 13.0f * fontSize; + if (context.showUI) + { + ImGui.SetNextWindowPos(new Vector2(context.camera.width - menuWidth - 0.5f * fontSize, 0.5f * fontSize)); + ImGui.SetNextWindowSize(new Vector2(menuWidth, context.camera.height - fontSize)); + + ImGui.Begin("Tools", ref context.showUI, ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoCollapse); + + if (ImGui.BeginTabBar("ControlTabs", ImGuiTabBarFlags.None)) + { + if (ImGui.BeginTabItem("Controls")) + { + ImGui.PushItemWidth(100.0f); + ImGui.SliderInt("Sub-steps", ref context.subStepCount, 1, 32); + ImGui.SliderFloat("Hertz", ref context.hertz, 5.0f, 240.0f, "%.0f hz"); + + if (ImGui.SliderInt("Workers", ref context.workerCount, 1, maxWorkers)) + { + context.workerCount = b2ClampInt(context.workerCount, 1, maxWorkers); + SelectSample(context, context.sampleIndex, true); + } + ImGui.PopItemWidth(); + + ImGui.Separator(); + + ImGui.Checkbox("Sleep", ref context.enableSleep); + ImGui.Checkbox("Warm Starting", ref context.enableWarmStarting); + ImGui.Checkbox("Continuous", ref context.enableContinuous); + + ImGui.PushItemWidth(100.0f); + float recyclingCentimeters = 100.0f * context.recycleDistance; + if (ImGui.SliderFloat("Recycle", ref recyclingCentimeters, 0.0f, 10.0f, "%.1f cm")) + { + context.recycleDistance = 0.01f * recyclingCentimeters; + b2World_SetContactRecycleDistance(worldId, context.recycleDistance); + } + ImGui.PopItemWidth(); + + ImGui.Separator(); + + ImGui.Checkbox("Shapes", ref context.debugDraw.drawShapes); + ImGui.Checkbox("Joints", ref context.debugDraw.drawJoints); + ImGui.Checkbox("Joint Extras", ref context.debugDraw.drawJointExtras); + ImGui.Checkbox("Bounds", ref context.debugDraw.drawBounds); + ImGui.Checkbox("Mass", ref context.debugDraw.drawMass); + ImGui.Checkbox("Body Names", ref context.debugDraw.drawBodyNames); + ImGui.Checkbox("Graph Colors", ref context.debugDraw.drawGraphColors); + ImGui.Checkbox("Islands", ref context.debugDraw.drawIslands); + ImGui.Checkbox("Counters", ref context.drawCounters); + ImGui.Checkbox("Profile", ref context.drawProfile); + ImGui.Checkbox("Frame Time", ref context.frameTime); + + ImGui.Separator(); + + ImGui.Checkbox("Contact Points", ref context.debugDraw.drawContacts); + + if (ImGui.RadioButton("Anchor A", context.debugDraw.drawAnchorA == true)) + { + context.debugDraw.drawAnchorA = true; + } + ImGui.SameLine(); + if (ImGui.RadioButton("Anchor B", context.debugDraw.drawAnchorA == false)) + { + context.debugDraw.drawAnchorA = false; + } + ImGui.Checkbox("Contact Normals", ref context.debugDraw.drawContactNormals); + ImGui.Checkbox("Contact Features", ref context.debugDraw.drawContactFeatures); + ImGui.Checkbox("Contact Forces", ref context.debugDraw.drawContactForces); + ImGui.Checkbox("Friction Forces", ref context.debugDraw.drawFrictionForces); + + ImGui.Separator(); + + ImGui.PushItemWidth(80.0f); + ImGui.InputFloat("Joint Scale", ref context.debugDraw.jointScale); + ImGui.InputFloat("Force Scale", ref context.debugDraw.forceScale); + ImGui.PopItemWidth(); + + Vector2 button_sz = new Vector2(-1, 0); + if (ImGui.Button("Pause (P)", button_sz)) + { + context.pause = !context.pause; + } + + if (ImGui.Button("Single Step (O)", button_sz)) + { + context.singleStep = !context.singleStep; + } + + if (ImGui.Button("Dump Mem Stats", button_sz)) + { + b2World_DumpMemoryStats(context.sample.m_worldId); + } + + if (ImGui.Button("Reset Profile", button_sz)) + { + context.sample.ResetProfile(); + } + + if (ImGui.Button("Restart (R)", button_sz)) + { + SelectSample(context, context.sampleIndex, true); + } + + if (ImGui.Button("Quit", button_sz)) + { + unsafe + { + context.glfw.SetWindowShouldClose(context.window, true); + } + } + + ImGui.EndTabItem(); + } + + ImGuiTreeNodeFlags leafNodeFlags = ImGuiTreeNodeFlags.OpenOnArrow | ImGuiTreeNodeFlags.OpenOnDoubleClick; + leafNodeFlags |= ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen; + + ImGuiTreeNodeFlags nodeFlags = ImGuiTreeNodeFlags.OpenOnArrow | ImGuiTreeNodeFlags.OpenOnDoubleClick; + + if (ImGui.BeginTabItem("Samples")) + { + int categoryIndex = 0; + string category = SampleFactory.Shared.GetCategory(categoryIndex); + int i = 0; + while (i < SampleFactory.Shared.SampleCount) + { + bool categorySelected = category == SampleFactory.Shared.GetCategory(context.sampleIndex); + ImGuiTreeNodeFlags nodeSelectionFlags = categorySelected ? ImGuiTreeNodeFlags.Selected : 0; + bool nodeOpen = ImGui.TreeNodeEx(category, nodeFlags | nodeSelectionFlags); + + if (nodeOpen) + { + while (i < SampleFactory.Shared.SampleCount && category == SampleFactory.Shared.GetCategory(i)) + { + ImGuiTreeNodeFlags selectionFlags = 0; + if (context.sampleIndex == i) + { + selectionFlags = ImGuiTreeNodeFlags.Selected; + } + + ImGui.TreeNodeEx(SampleFactory.Shared.GetName(i), leafNodeFlags | selectionFlags); + if (ImGui.IsItemClicked()) + { + SelectSample(context, i, false); + } + ++i; + } + ImGui.TreePop(); + } + else + { + while (i < SampleFactory.Shared.SampleCount && category == SampleFactory.Shared.GetCategory(i)) + { + ++i; + } + } + + if (i < SampleFactory.Shared.SampleCount) + { + category = SampleFactory.Shared.GetCategory(i); + categoryIndex = i; + } + } + ImGui.EndTabItem(); + } + ImGui.EndTabBar(); + } + + ImGui.End(); + + context.sample.UpdateGui(); + } + } } diff --git a/src/Box2D.NET.Samples/Settings.cs b/src/Box2D.NET.Samples/Settings.cs index 39f87907..86360e2f 100644 --- a/src/Box2D.NET.Samples/Settings.cs +++ b/src/Box2D.NET.Samples/Settings.cs @@ -31,7 +31,6 @@ public class Settings public bool drawBounds = false; public bool drawMass = false; public bool drawBodyNames = false; - public B2ContactDrawType contactDrawType; public bool drawContactNormals = false; public bool drawContactFeatures = false; public bool drawContactForces = false; @@ -93,7 +92,6 @@ public static Settings CopyFrom(SampleContext context) setting.singleStep = context.singleStep; // - setting.contactDrawType = context.debugDraw.contactDrawType; setting.drawShapes = context.debugDraw.drawShapes; setting.drawJoints = context.debugDraw.drawJoints; setting.drawJointExtras = context.debugDraw.drawJointExtras; diff --git a/src/Box2D.NET/B2BodyDef.cs b/src/Box2D.NET/B2BodyDef.cs index b8d5626f..ce542c8b 100644 --- a/src/Box2D.NET/B2BodyDef.cs +++ b/src/Box2D.NET/B2BodyDef.cs @@ -75,7 +75,7 @@ public struct B2BodyDef /// If you want a fast moving projectile to collide with a fast moving target, you need to consider the relative /// movement in your ray or shape cast. This is out of the scope of Box2D. /// So what are good use cases for bullets? Pinball games or games with dynamic containers that hold other objects. - /// It should be a use case where it doesn't break the game if there is a collision missed, but the having them + /// It should be a use case where it doesn't break the game if there is a collision missed, but having them /// captured improves the quality of the game. public bool isBullet; diff --git a/src/Box2D.NET/B2ContactDrawType.cs b/src/Box2D.NET/B2ContactDrawType.cs deleted file mode 100644 index 2c85d870..00000000 --- a/src/Box2D.NET/B2ContactDrawType.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Box2D.NET -{ - /// The type of contact point drawing - public enum B2ContactDrawType - { - b2_drawContacts_None = 0, - b2_drawContacts_Clip = 1, - b2_drawContacts_AnchorA = 2, - b2_drawContacts_AnchorB = 3, - b2_drawContacts_Average = 4, - } -} \ No newline at end of file diff --git a/src/Box2D.NET/B2DebugDraw.cs b/src/Box2D.NET/B2DebugDraw.cs index 956b92a9..94dcdf37 100644 --- a/src/Box2D.NET/B2DebugDraw.cs +++ b/src/Box2D.NET/B2DebugDraw.cs @@ -29,7 +29,10 @@ public struct B2DebugDraw public float jointScale; /// Option to draw contact points - public B2ContactDrawType contactDrawType; + public bool drawContacts; + + /// Draw anchor A for contact points (instead of anchorB) + public bool drawAnchorA; /// Option to draw shapes public bool drawShapes; diff --git a/src/Box2D.NET/B2Worlds.cs b/src/Box2D.NET/B2Worlds.cs index b392e44a..fe3d64cd 100644 --- a/src/Box2D.NET/B2Worlds.cs +++ b/src/Box2D.NET/B2Worlds.cs @@ -1252,7 +1252,7 @@ public static void b2World_Draw(B2WorldId worldId, in B2DebugDraw draw) } float linearSlop = B2_LINEAR_SLOP; - if (draw.contactDrawType != B2ContactDrawType.b2_drawContacts_None && body.type == B2BodyType.b2_dynamicBody) + if (draw.drawContacts && body.type == B2BodyType.b2_dynamicBody) { int contactKey = body.headContactKey; while (contactKey != B2_NULL_INDEX) @@ -1279,21 +1279,15 @@ public static void b2World_Draw(B2WorldId worldId, in B2DebugDraw draw) { ref B2ManifoldPoint mp = ref contactSim.manifold.points[j]; - B2Vec2 p = mp.clipPoint; - if (draw.contactDrawType == B2ContactDrawType.b2_drawContacts_AnchorA) + B2Vec2 p; + if (draw.drawAnchorA) { p = b2Add(bodySimA.center, mp.anchorA); } - else if (draw.contactDrawType == B2ContactDrawType.b2_drawContacts_AnchorB) + else { p = b2Add(bodySimB.center, mp.anchorB); } - else if (draw.contactDrawType == B2ContactDrawType.b2_drawContacts_Average) - { - B2Vec2 pA = b2Add(bodySimA.center, mp.anchorA); - B2Vec2 pB = b2Add(bodySimB.center, mp.anchorB); - p = b2Lerp(pA, pB, 0.5f); - } if (draw.drawGraphColors && contact.colorIndex != B2_NULL_INDEX) { diff --git a/test/Box2D.NET.Test/B2DebugDrawTests.cs b/test/Box2D.NET.Test/B2DebugDrawTests.cs new file mode 100644 index 00000000..4f075857 --- /dev/null +++ b/test/Box2D.NET.Test/B2DebugDrawTests.cs @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: 2026 Erin Catto +// SPDX-FileCopyrightText: 2026 Ikpil Choi(ikpil@naver.com) +// SPDX-License-Identifier: MIT + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using static Box2D.NET.B2Bodies; +using static Box2D.NET.B2Geometries; +using static Box2D.NET.B2Shapes; +using static Box2D.NET.B2Types; +using static Box2D.NET.B2Worlds; + +namespace Box2D.NET.Test; + +public class B2DebugDrawTests +{ + private sealed class PointCapture + { + public readonly List Points = new List(); + } + + [Test] + public void ContactDrawingUsesSelectedBodyAnchor() + { + B2WorldDef worldDef = b2DefaultWorldDef(); + worldDef.gravity = new B2Vec2(0.0f, -10.0f); + B2WorldId worldId = b2CreateWorld(worldDef); + + try + { + B2BodyDef groundDef = b2DefaultBodyDef(); + B2BodyId groundId = b2CreateBody(worldId, groundDef); + B2ShapeDef groundShapeDef = b2DefaultShapeDef(); + b2CreatePolygonShape(groundId, groundShapeDef, b2MakeBox(2.0f, 0.5f)); + + B2BodyDef bodyDef = b2DefaultBodyDef(); + bodyDef.type = B2BodyType.b2_dynamicBody; + bodyDef.position = new B2Vec2(0.0f, 0.9f); + B2BodyId bodyId = b2CreateBody(worldId, bodyDef); + B2ShapeDef shapeDef = b2DefaultShapeDef(); + shapeDef.density = 1.0f; + b2CreatePolygonShape(bodyId, shapeDef, b2MakeBox(0.5f, 0.5f)); + + b2World_Step(worldId, 1.0f / 60.0f, 4); + + B2DebugDraw draw = b2DefaultDebugDraw(); + draw.drawShapes = false; + draw.DrawPointFcn = CapturePoint; + var capture = new PointCapture(); + draw.context = capture; + + b2World_Draw(worldId, draw); + Assert.That(capture.Points, Is.Empty, "drawContacts=false must suppress contact points."); + + draw.drawContacts = true; + draw.drawAnchorA = false; + b2World_Draw(worldId, draw); + B2Vec2[] anchorBPoints = capture.Points.ToArray(); + Assert.That(anchorBPoints, Is.Not.Empty); + + capture.Points.Clear(); + draw.drawAnchorA = true; + b2World_Draw(worldId, draw); + B2Vec2[] anchorAPoints = capture.Points.ToArray(); + + Assert.That(anchorAPoints, Has.Length.EqualTo(anchorBPoints.Length)); + Assert.That(AnyPointDiffers(anchorAPoints, anchorBPoints), Is.True, + "Anchor A and anchor B should be transformed from their respective body centers."); + } + finally + { + b2DestroyWorld(worldId); + } + } + + [Test] + public void ContactDrawEnumIsRemovedFromThePublicApi() + { + B2DebugDraw draw = b2DefaultDebugDraw(); + + Assert.Multiple((Action)(() => + { + Assert.That(draw.drawContacts, Is.False); + Assert.That(draw.drawAnchorA, Is.False); + Assert.That(typeof(B2DebugDraw).Assembly.GetType("Box2D.NET.B2ContactDrawType"), Is.Null); + })); + } + + private static void CapturePoint(in B2Vec2 point, float size, B2HexColor color, object context) + { + ((PointCapture)context).Points.Add(point); + } + + private static bool AnyPointDiffers(B2Vec2[] pointsA, B2Vec2[] pointsB) + { + for (int i = 0; i < pointsA.Length; ++i) + { + if (pointsA[i] != pointsB[i]) + { + return true; + } + } + + return false; + } +}