diff --git a/.github/workflows/box2d-docs-github.yml b/.github/workflows/box2d-docs-github.yml
new file mode 100644
index 00000000..65a925e0
--- /dev/null
+++ b/.github/workflows/box2d-docs-github.yml
@@ -0,0 +1,44 @@
+# More GitHub Actions for Azure: https://github.com/Azure/actions
+
+name: Build Box2D Docs for GitHub Pages
+
+env:
+ COMMON_SETTINGS_PATH: docs/docfx.json
+
+on:
+ workflow_dispatch:
+
+jobs:
+ publish-docs:
+ runs-on: windows-latest
+
+ steps:
+ - name: .NET SDK Setup
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 9.x
+
+ - name: Checkout Box2D.NET
+ uses: actions/checkout@v4
+
+ - name: Run Box2D.NET.CommentConverter.csproj
+ run: dotnet run --project tools/Box2D.NET.CommentConverter/Box2D.NET.CommentConverter.csproj --configuration Release
+
+ - name: Install DocFX
+ # This installs the latest version of DocFX and may introduce breaking changes
+ # run: dotnet tool update -g docfx
+ # This installs a specific, tested version of DocFX.
+ run: dotnet tool update -g docfx --version 2.78.2
+
+ - name: Build Box2D.NET API Docs
+ run: docfx metadata ${{ env.COMMON_SETTINGS_PATH }}
+
+ - name: Build Box2D.NET Docs
+ run: docfx build ${{ env.COMMON_SETTINGS_PATH }}
+
+ - name: Deploy
+ uses: peaceiris/actions-gh-pages@v4.0.0
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: docs/_site
+ publish_branch: gh-pages
diff --git a/.gitignore b/.gitignore
index e900518a..d50a7dd6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
-## Ignore Visual Studio temporary files, build results, and
+## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
@@ -399,3 +399,7 @@ FodyWeavers.xsd
.idea/
imgui.ini
settings.ini
+
+# DocFX API Generated Pages
+docs/api/*
+docs/_site/*
\ No newline at end of file
diff --git a/docs/docfx.json b/docs/docfx.json
new file mode 100644
index 00000000..1036bbd8
--- /dev/null
+++ b/docs/docfx.json
@@ -0,0 +1,63 @@
+{
+ "$schema": "https://raw.githubusercontent.com/dotnet/docfx/main/schemas/docfx.schema.json",
+ "metadata": [
+ {
+ "src": [
+ {
+ "src": "..",
+ "files": [
+ "src/Box2D.NET/Box2D.NET.csproj"
+ ]
+ }
+ ],
+ "dest": "api"
+ }
+ ],
+ "build": {
+ "content": [
+ {
+ "files": [
+ "**/*.{md,yml}"
+ ],
+ "exclude": [
+ "_site/**"
+ ]
+ }
+ ],
+ "resource": [
+ {
+ "files": [
+ "favicon-32x32.png",
+ "favicon-16x16.png",
+ "images/**"
+ ]
+ }
+ ],
+ "output": "_site",
+ "template": [
+ "default",
+ "modern",
+ "template"
+ ],
+ "fileMetadata": {
+ "_appTitle": {
+ "api/**/*.md": "Box2D API",
+ "api/**/*.yml": "Box2D API"
+ }
+ },
+ "globalMetadata": {
+ "_appName": "",
+ "_appTitle": "Box2D Docs",
+ "_appLogoPath": "images/box2d_logo.svg",
+ "_appFaviconPath": "favicon-32x32.png",
+ "_enableSearch": true,
+ "pdf": false,
+ "_gitContribute": {
+ "repo": "https://github.com/ikpil/Box2D.NET",
+ "branch": "master"
+ },
+ "_gitUrlPattern": "github",
+ "_gitRepo": "https://github.com/ikpil/Box2D.NET"
+ }
+ }
+}
\ No newline at end of file
diff --git a/docs/docs/index.md b/docs/docs/index.md
new file mode 100644
index 00000000..4e7dc4ff
--- /dev/null
+++ b/docs/docs/index.md
@@ -0,0 +1,4 @@
+---
+_disableToc: false
+---
+# Box2D.NET Docs
\ No newline at end of file
diff --git a/docs/docs/toc.yml b/docs/docs/toc.yml
new file mode 100644
index 00000000..093b7ebb
--- /dev/null
+++ b/docs/docs/toc.yml
@@ -0,0 +1,8 @@
+- name: 📚 Docs
+ href: index.md
+- name: Original Box2D
+ href: https://box2d.org/
+- name: Overview
+ href: https://box2d.org/documentation/index.html
+- name: FAQ
+ href: https://box2d.org/documentation/md_faq.html
\ No newline at end of file
diff --git a/docs/favicon-16x16.png b/docs/favicon-16x16.png
new file mode 100644
index 00000000..2ab65d23
Binary files /dev/null and b/docs/favicon-16x16.png differ
diff --git a/docs/favicon-32x32.png b/docs/favicon-32x32.png
new file mode 100644
index 00000000..31d602e7
Binary files /dev/null and b/docs/favicon-32x32.png differ
diff --git a/docs/images/box2d_logo.svg b/docs/images/box2d_logo.svg
new file mode 100644
index 00000000..f002689d
--- /dev/null
+++ b/docs/images/box2d_logo.svg
@@ -0,0 +1,126 @@
+
+
+
+
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 00000000..3ebcccbc
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,4 @@
+---
+_disableToc: false
+---
+[!INCLUDE [readme](../readme.md)]
\ No newline at end of file
diff --git a/docs/template/public/main.css b/docs/template/public/main.css
new file mode 100644
index 00000000..eb3143e5
--- /dev/null
+++ b/docs/template/public/main.css
@@ -0,0 +1,5 @@
+/* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License.txt in the project root for license information. */
+
+.navbar-brand #logo {
+ height: 56px;
+}
\ No newline at end of file
diff --git a/docs/template/public/main.js b/docs/template/public/main.js
new file mode 100644
index 00000000..9bbb8f99
--- /dev/null
+++ b/docs/template/public/main.js
@@ -0,0 +1,17 @@
+const app = {
+ languageDropdownCreated: false,
+ iconLinks: [
+ {
+ icon: 'github',
+ href: 'https://github.com/ikpil/Box2D.NET',
+ title: 'GitHub'
+ },
+ {
+ icon: 'discord',
+ href: 'https://github.com/ikpil/Box2D.NET',
+ title: 'Discord'
+ }
+ ]
+};
+
+export default app;
\ No newline at end of file
diff --git a/docs/toc.yml b/docs/toc.yml
new file mode 100644
index 00000000..b2a2f936
--- /dev/null
+++ b/docs/toc.yml
@@ -0,0 +1,4 @@
+- name: 📚 Docs
+ href: docs/index.md
+- name: 🔧 API
+ href: api/
\ No newline at end of file
diff --git a/tools/Box2D.NET.CommentConverter/Box2D.NET.CommentConverter.csproj b/tools/Box2D.NET.CommentConverter/Box2D.NET.CommentConverter.csproj
new file mode 100644
index 00000000..fd4bd08d
--- /dev/null
+++ b/tools/Box2D.NET.CommentConverter/Box2D.NET.CommentConverter.csproj
@@ -0,0 +1,10 @@
+
+
+
+ Exe
+ net9.0
+ enable
+ enable
+
+
+
diff --git a/tools/Box2D.NET.CommentConverter/Program.cs b/tools/Box2D.NET.CommentConverter/Program.cs
new file mode 100644
index 00000000..50504f6d
--- /dev/null
+++ b/tools/Box2D.NET.CommentConverter/Program.cs
@@ -0,0 +1,159 @@
+const string DoubleSlashCommentPrefix = "// ";
+const string TripleSlashCommentPrefix = "///";
+const string SummaryStart = "/// ";
+const string SummaryEnd = "/// ";
+const string FileFilter = "*.cs";
+
+var commentStarts = new HashSet { DoubleSlashCommentPrefix, TripleSlashCommentPrefix };
+var repoRoot = Directory.GetCurrentDirectory();
+
+#if DEBUG
+repoRoot = Path.GetFullPath(Path.Combine(repoRoot, "..", "..", "..", "..", ".."));
+#endif
+
+var folderPath = Path.Combine(repoRoot, "src", "Box2D.NET");
+
+// Tests: B2BodySim, B2World, B2WorldId
+var files = Directory.GetFiles(folderPath, FileFilter, SearchOption.AllDirectories)
+ //.Where(w => w.Contains("B2PrismaticJointDef"))
+ //.Take(50)
+ .ToList();
+
+Console.WriteLine($"Found {files.Count} C# files in the folder: {folderPath}");
+
+foreach (var filePath in files)
+{
+ await ProcessFileAsync(filePath);
+}
+
+Console.WriteLine("*** Processing completed ***");
+
+async Task ProcessFileAsync(string filePath)
+{
+ Console.WriteLine($"\nProcessing file: {filePath}");
+
+ var content = await File.ReadAllTextAsync(filePath);
+ var lines = content.Split(["\r\n", "\n"], StringSplitOptions.None).ToList();
+ var commentLineIndexes = ExtractCommentLineIndexes(lines);
+
+ RemovePreNamespaceComments(lines, commentLineIndexes);
+
+ if (commentLineIndexes.Count == 0)
+ {
+ Console.WriteLine("No comment lines found in the file.");
+ return;
+ }
+
+ var commentBlocks = ExtractCommentBlocks(lines, commentLineIndexes);
+
+ if (commentBlocks.Count == 0)
+ {
+ Console.WriteLine("No comment blocks found.");
+ return;
+ }
+
+ ConvertCommentsToTripleSlash(lines, commentBlocks);
+
+ WrapCommentsWithSummaryTags(lines, commentBlocks);
+
+ //File.WriteAllText(filePath.Replace(".cs", ".xmlcomments.cs"), string.Join(Environment.NewLine, lines));
+ File.WriteAllText(filePath, string.Join(Environment.NewLine, lines));
+
+ Console.WriteLine($"Output written to: {filePath}");
+}
+
+static void RemovePreNamespaceComments(List lines, List commentLineIndexes)
+{
+ var namespaceLineIndex = lines.FindIndex(line => line.TrimStart().StartsWith("namespace "));
+
+ commentLineIndexes.RemoveAll(index => index < namespaceLineIndex);
+}
+
+static List ExtractCommentBlocks(List lines, List commentLineIndexes)
+{
+ var commentBlocks = new List();
+ var startIndex = commentLineIndexes[0];
+ var endIndex = commentLineIndexes[0];
+
+ if (commentLineIndexes.Count == 1)
+ {
+ AddBlockIfFollowedByPublic(startIndex, endIndex);
+
+ return commentBlocks;
+ }
+
+ for (int i = 1; i < commentLineIndexes.Count; i++)
+ {
+ var nextIndex = commentLineIndexes[i];
+
+ if (nextIndex - endIndex != 1)
+ {
+ AddBlockIfFollowedByPublic(startIndex, endIndex);
+ startIndex = nextIndex;
+ }
+
+ endIndex = nextIndex;
+
+ if (i == commentLineIndexes.Count - 1)
+ {
+ AddBlockIfFollowedByPublic(startIndex, endIndex);
+ }
+ }
+
+ return commentBlocks;
+
+ void AddBlockIfFollowedByPublic(int startIndex, int endIndex)
+ {
+ if (endIndex + 1 < lines.Count && lines[endIndex + 1].Contains("public"))
+ {
+ commentBlocks.Add(new CommentBlock(startIndex, endIndex));
+ }
+ }
+}
+
+List ExtractCommentLineIndexes(List lines)
+{
+ return lines
+ .Select((line, index) => (line, index))
+ .Where(item => commentStarts.Any(commentStart =>
+ item.line.TrimStart().StartsWith(commentStart)))
+ .Select(item => item.index)
+ .ToList();
+}
+
+static void ConvertCommentsToTripleSlash(List lines, List commentBlocks)
+{
+ foreach (var block in commentBlocks)
+ {
+ Console.WriteLine($"Comment block from {block.StartIndex + 1} to {block.EndIndex + 1} (length: {block.Length})");
+
+ for (int i = 0; i < block.Length; i++)
+ {
+ int lineIndex = block.StartIndex + i;
+
+ if (!lines[lineIndex].TrimStart().StartsWith(TripleSlashCommentPrefix))
+ {
+ lines[lineIndex] = lines[lineIndex].Replace(DoubleSlashCommentPrefix, "/// ");
+ }
+ }
+ }
+}
+
+void WrapCommentsWithSummaryTags(List lines, List commentBlocks)
+{
+ for (int i = commentBlocks.Count - 1; i >= 0; i--)
+ {
+ var block = commentBlocks[i];
+ var indentation = GetIndentation(lines[block.StartIndex]);
+
+ lines.Insert(block.EndIndex + 1, $"{indentation}{SummaryEnd}");
+ lines.Insert(block.StartIndex, $"{indentation}{SummaryStart}");
+ }
+
+ string GetIndentation(string line) => new(' ', line.Length - line.TrimStart().Length);
+}
+
+record class CommentBlock(int StartIndex, int EndIndex)
+{
+ public int Length => EndIndex - StartIndex + 1;
+}