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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
<UseWindowsForms>false</UseWindowsForms>
<NeutralResourcesLanguage>en</NeutralResourcesLanguage>
</PropertyGroup>
<ItemGroup>
<!-- Drives Word through COM, so it only runs on Windows; this keeps CA1416 satisfied on the net10.0 target. -->
<AssemblyAttribute Include="System.Runtime.Versioning.SupportedOSPlatformAttribute">
<_Parameter1>windows</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup>
<PackageReference Include="DocumentFormat.OpenXml" />
<PackageReference Include="Microsoft.Office.Interop.Word" />
Expand Down
27 changes: 27 additions & 0 deletions PanoramicData.Render.Test/KnuthPlassTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,33 @@ public void FindBreaks_LooseFitnessOnMidLine_ProducesMultipleLines()
lines.Should().HaveCountGreaterThan(1);
}

[Fact]
public async Task FindBreaks_LongWordWrappedParagraph_CompletesQuickly()
{
// Shaped like RenderCommandEmitter.BuildWrapItems output: space glue stretches 0.5x and shrinks 1/3x.
// Before active nodes were deduplicated, a paragraph like this took minutes and millions of nodes.
var items = new List<KnuthPlassItem>();
for (var word = 0; word < 40; word++)
{
if (word > 0)
{
items.Add(new KnuthPlassGlue(100f, 50f, 100f / 3f));
}

items.Add(new KnuthPlassBox(300f + (word % 7 * 60f)));
}

items.Add(new KnuthPlassGlue(0f, float.PositiveInfinity, 0f));
items.Add(ForcedBreak());

var lines = await Task
.Run(() => KnuthPlassAlgorithm.FindBreaks(items, 9000f), TestContext.Current.CancellationToken)
.WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken);

lines.Should().NotBeEmpty();
lines[^1].EndIndex.Should().Be(items.Count - 1);
}

private static KnuthPlassPenalty ForcedBreak() =>
new(0f, KnuthPlassPenalty.NegativeInfinity);
}
12 changes: 10 additions & 2 deletions PanoramicData.Render/KnuthPlassAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,12 +195,20 @@ public static IReadOnlyList<KnuthPlassLine> FindBreaks(
activeNodes.RemoveAt(toDeactivate[d]);
}

// Add new active nodes (keep only the best per fitness class for this position)
// Add new active nodes, keeping only the best per (line, fitness class): without this the active set doubles at
// every breakpoint while lines are too loose. On a tie the later candidate wins, matching the previous output.
var bestByLineAndFitness = new Dictionary<(int Line, int FitnessClass), BreakNode>();
foreach (var node in toActivate)
{
activeNodes.Add(node);
var key = (node.Line, node.FitnessClass);
if (!bestByLineAndFitness.TryGetValue(key, out var existing) || node.TotalDemerits <= existing.TotalDemerits)
{
bestByLineAndFitness[key] = node;
}
}

activeNodes.AddRange(bestByLineAndFitness.Values);

// If a forced break, deactivate all preceding active nodes
if (isForcedBreakHere)
{
Expand Down
Loading