Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
29 changes: 29 additions & 0 deletions Redot-Documentation-Tests/DocRendererServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.Extensions.FileProviders;
using Redot_Documentation.Services;
using Redot_Documentation.Versioning;
using System.Text.RegularExpressions;

namespace Redot_Documentation_Tests;

Expand Down Expand Up @@ -69,6 +70,34 @@ await File.WriteAllTextAsync(
Assert.Contains($"href=\"{expectedHref}\"", html);
}

[Fact]
public async Task RenderToHtmlAsync_RendersNestedTabsFromInnermostBlockOutward()
{
Directory.CreateDirectory(Path.Combine(contentRootPath, "docs"));
await File.WriteAllTextAsync(
Path.Combine(contentRootPath, "docs", "source.md"),
"""
<Tabs>
<TabItem value="outer" label="Outer">
<Tabs>
<TabItem value="inner" label="Inner">
Nested content.
</TabItem>
</Tabs>
</TabItem>
</Tabs>
""");

var renderer = new DocRendererService(new TestWebHostEnvironment(contentRootPath));

string html = await renderer.RenderToHtmlAsync("source.md", CreateVersionProvider());

Assert.Equal(2, Regex.Matches(html, "class=\"doc-tabs\"").Count);
Assert.Contains("Nested content.", html);
Assert.DoesNotContain("<Tabs>", html);
Assert.DoesNotContain("<TabItem", html);
}

[Theory]
[InlineData("Some Section", "some_section")]
[InlineData("Some_Section", "some%5Fsection")]
Expand Down
44 changes: 44 additions & 0 deletions Redot-Documentation-Tests/VersionManagerServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,50 @@ public void LoadContent_LoadsMetadataRolesAndCaseInsensitiveLookups()
provider.GetReferentialPath(Path.Combine(provider.VersionRoot, "example.md")));
}

[Fact]
public void LoadContent_RepositoryDocumentationHasUniqueSlugs()
{
DirectoryInfo? repository = new(AppContext.BaseDirectory);
while (repository != null && !File.Exists(Path.Combine(repository.FullName, "Redot-Documentation.sln")))
repository = repository.Parent;

Assert.NotNull(repository);
var manager = new VersionManagerService(
new TestWebHostEnvironment(Path.Combine(repository.FullName, "Redot-Documentation")));

manager.LoadContent();

VersionProvider provider = manager.GetVersionProvider("latest");
Assert.Equal("/en/Community/tutorials.md", provider.GetPathFromSlug("doc_tutorials"));
Assert.Equal("/en/latest/Tutorials/index.md", provider.GetPathFromSlug("doc_tutorials_overview"));
Assert.Equal("/en/latest/Tutorials/math/interpolation.md", provider.GetPathFromSlug("doc_interpolation"));
Assert.Equal("/en/latest/Tutorials/physics/interpolation/index.md", provider.GetPathFromSlug("doc_physics_interpolation"));
Assert.Equal("/en/latest/Tutorials/editor/index.md", provider.GetPathFromSlug("doc_editor"));
Assert.Equal("/en/latest/Tutorials/plugins/editor/index.md", provider.GetPathFromSlug("doc_editor_plugins"));
Assert.Equal("/en/latest/Tutorials/plugins/editor/making_plugins.md", provider.GetPathFromSlug("doc_making_plugins"));
}

[Fact]
public void LoadContent_DuplicateArticleAndSectionSlugsIdentifyBothPaths()
{
DocumentationVersion[] versions =
[
CreateVersion("latest", "Latest development", "master", isNextPrerelease: true),
CreateVersion("26.1", "Redot 26.1", "26.1", isLatestStable: true)
];
var (manager, contentRootPath) = CreateManager(versions);
string versionRoot = Path.Combine(contentRootPath, "docs", "latest");
Directory.CreateDirectory(Path.Combine(versionRoot, "example"));
File.WriteAllText(Path.Combine(versionRoot, "example.md"), "# Example");
File.WriteAllText(Path.Combine(versionRoot, "example", "index.md"), "# Example section");

InvalidOperationException exception = Assert.Throws<InvalidOperationException>(manager.LoadContent);

Assert.Contains("Duplicate documentation slug 'doc_example' in version 'latest'", exception.Message);
Assert.Contains("/en/latest/example.md", exception.Message);
Assert.Contains("/en/latest/example/index.md", exception.Message);
}

[Fact]
public void LoadContent_RejectsLegacyStringArray()
{
Expand Down
53 changes: 45 additions & 8 deletions Redot-Documentation/Services/DocRendererService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -245,24 +245,61 @@ private static (string name, string url) SplitMarkdownLink(string matchValue)
private static string TransformTabsBlocks(string markdown, Dictionary<string, string> htmlPlaceholders)
{
var tabsIndex = 0;
return Regex.Replace(
markdown,
@"<Tabs>(.*?)</Tabs>",
match => TransformSingleTabsBlock(match, tabsIndex++, htmlPlaceholders),
RegexOptions.IgnoreCase | RegexOptions.Singleline);
while (TryFindInnermostTabsBlock(markdown, out var startIndex, out var endIndex, out var contentStartIndex))
{
var tabsContent = markdown[contentStartIndex..endIndex];
var transformedTabs = TransformSingleTabsBlock(tabsContent, tabsIndex++, htmlPlaceholders);
markdown = string.Concat(
markdown.AsSpan(0, startIndex),
transformedTabs,
markdown.AsSpan(endIndex + "</Tabs>".Length));
}

return markdown;
}

private static bool TryFindInnermostTabsBlock(
string markdown,
out int startIndex,
out int endIndex,
out int contentStartIndex)
{
var openings = new Stack<Match>();
foreach (Match tag in Regex.Matches(markdown, @"</?Tabs\b[^>]*>", RegexOptions.IgnoreCase))
{
if (tag.Value.StartsWith("</", StringComparison.Ordinal))
{
if (openings.Count == 0)
{
continue;
}

var opening = openings.Pop();
startIndex = opening.Index;
endIndex = tag.Index;
contentStartIndex = opening.Index + opening.Length;
return true;
}

openings.Push(tag);
}

startIndex = -1;
endIndex = -1;
contentStartIndex = -1;
return false;
}

private static string TransformSingleTabsBlock(Match tabsBlockMatch, int tabsIndex, Dictionary<string, string> htmlPlaceholders)
private static string TransformSingleTabsBlock(string tabsContent, int tabsIndex, Dictionary<string, string> htmlPlaceholders)
{
var tabsContent = tabsBlockMatch.Groups[1].Value;
var tabItemMatches = Regex.Matches(
tabsContent,
@"<TabItem\b([^>]*)>(.*?)</TabItem>",
RegexOptions.IgnoreCase | RegexOptions.Singleline);

if (tabItemMatches.Count == 0)
{
return tabsBlockMatch.Value;
return $"<Tabs>{tabsContent}</Tabs>";
}

var tabButtonsMarkup = new List<string>(tabItemMatches.Count);
Expand Down
3 changes: 2 additions & 1 deletion Redot-Documentation/Versioning/RankingConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ namespace Redot_Documentation.Versioning;

public class RankingConfig
{
public string? Slug { get; set; }
public bool IntermingleArticles { get; set; } = false;
public string SlugPrefix { get; set; } = "doc_";
public Dictionary<string, int> RankingPriorities { get; set; } = new();
Expand All @@ -12,4 +13,4 @@ public RankingConfig()
ExcludedItems.Add("img");
ExcludedItems.Add("video");
}
}
}
2 changes: 2 additions & 0 deletions Redot-Documentation/Versioning/Section.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public void LoadAndParse()
rankingPriorities = configData.RankingPriorities;
excludedItems = configData.ExcludedItems;
SlugPrefix = configData.SlugPrefix;
if (!string.IsNullOrWhiteSpace(configData.Slug))
Slug = configData.Slug;
IntermingleArticles = configData.IntermingleArticles;
}
else
Expand Down
16 changes: 13 additions & 3 deletions Redot-Documentation/Versioning/VersionProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public void ParseSlugs()
ParseSlugs(subSection, slugLookupTable);
else
{
slugLookupTable.Add(ranking.Slug, GetReferentialPath(ranking.Path));
AddSlug(ranking, slugLookupTable);
}
}

Expand All @@ -101,16 +101,26 @@ public string GetReferentialPath(string path)
private void ParseSlugs(Section section, IDictionary<string, string> slugLookupTable)
{
if (section.IndexArticle != null)
slugLookupTable.Add(section.IndexArticle.Slug, GetReferentialPath(section.IndexArticle.Path));
AddSlug(section.IndexArticle, slugLookupTable);
foreach (IRanking ranking in section.GetSortedRankings())
{
if (ranking is Section subSection)
ParseSlugs(subSection, slugLookupTable);
else
slugLookupTable.Add(ranking.Slug, GetReferentialPath(ranking.Path));
AddSlug(ranking, slugLookupTable);
}
}

private void AddSlug(IRanking ranking, IDictionary<string, string> slugLookupTable)
{
string path = GetReferentialPath(ranking.Path);
if (slugLookupTable.TryGetValue(ranking.Slug, out string? existingPath))
throw new InvalidOperationException(
$"Duplicate documentation slug '{ranking.Slug}' in version '{Version.Slug}': '{existingPath}' and '{path}'.");

slugLookupTable.Add(ranking.Slug, path);
}

public string GetPathFromSlug(string slug)
=> Volatile.Read(ref _slugLookupTable)[slug];

Expand Down
88 changes: 88 additions & 0 deletions Redot-Documentation/docs/26.1/Tutorials/2d/2d_antialiasing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@

# 2D antialiasing

:::info

Redot also supports antialiasing in 3D rendering. This is covered on the
[doc_3d_antialiasing](../3d/3d_antialiasing.md) page.

:::

## Introduction

Due to their limited resolution, scenes rendered in 2D can exhibit aliasing
artifacts. These artifacts usually manifest in the form of a "staircase" effect on
geometry edges, and are most noticeable when using nodes such as [class_Line2D](class_Line2D),
[class_Polygon2D](class_Polygon2D) or [class_TextureProgressBar](class_TextureProgressBar). [doc_custom_drawing_in_2d](custom_drawing_in_2d.md)
can also have aliasing artifacts for methods that don't support antialiasing.

In the example below, you can notice how
edges have a blocky appearance:

![Image](/img/Tutorials/2d/img/antialiasing_none_scaled.webp)

Image is scaled by 2× with nearest-neighbor filtering to make aliasing more noticeable.

To combat this, Redot supports several methods of enabling antialiasing on 2D rendering.

## Antialiasing property in Line2D and custom drawing

This is the recommended method, as it has a lower performance impact in most cases.

Line2D has an **Antialiased** property which you can enable in the inspector.
Also, several methods for [doc_custom_drawing_in_2d](custom_drawing_in_2d.md) support an optional
``antialiased`` parameter, which can be set to ``true`` when calling the
function.

These methods do not require MSAA to be enabled, which makes their *baseline*
performance cost low. In other words, there is no permanent added cost if you're
not drawing any antialiased geometry at some point.

The downside of these antialiasing methods is that they work by generating
additional geometry. If you're generating complex 2D geometry that's updated
every frame, this may be a bottleneck. Also, Polygon2D, TextureProgressBar, and
several custom drawing methods don't feature an antialiased property. For these
nodes, you can use 2D multisample antialiasing instead.

## Multisample antialiasing (MSAA)

*This is only available in the Forward+ and Mobile renderers, not the
Compatibility renderer.*

Before enabling MSAA in 2D, it's important to understand what MSAA will operate
on. MSAA in 2D follows similar restrictions as in 3D. While it does not
introduce any blurriness, its scope of application is limited. The main
applications of 2D MSAA are:

- Geometry edges, such as line and polygon drawing.
- Sprite edges *only for pixels touching one of the texture's edges*. This works
for both linear and nearest-neighbor filtering. Sprite edges created using
transparency on the image are not affected by MSAA.

The downside of MSAA is that it only operates on edges. This is because MSAA
increases the number of *coverage* samples, but not the number of *color*
samples. However, since the number of color samples did not increase, fragment
shaders are still run for each pixel only once. As a result, MSAA will **not
affect** the following kinds of aliasing in any way:

- Aliasing *within* nearest-neighbor filtered textures (pixel art).
- Aliasing caused by custom 2D shaders.
- Specular aliasing when using Light2D.
- Aliasing in font rendering.

MSAA can be enabled in the Project Settings by changing the value of the
[Rendering &gt; Anti Aliasing &gt; Quality &gt; MSAA 2D](class_ProjectSettings_property_rendering/anti_aliasing/quality/msaa_2d)
setting. It's important to change the value of the **MSAA 2D** setting and not **MSAA 3D**, as these are entirely
separate settings.

Comparison between no antialiasing (left) and various MSAA levels (right). The
top-left corner contains a Line2D node, the top-right corner contains 2
TextureProgressBar nodes. The bottom contains 8 pixel art sprites, with 4 of
them touching the edges (green background) and 4 of them not touching the edges
(Redot logo):

![Image](/img/Tutorials/2d/img/antialiasing_msaa_2x.webp)

![Image](/img/Tutorials/2d/img/antialiasing_msaa_4x.webp)

![Image](/img/Tutorials/2d/img/antialiasing_msaa_8x.webp)
Loading
Loading