From fda4e0bc241c94fc57c9068011b0336b5bdb0b54 Mon Sep 17 00:00:00 2001 From: David Bond Date: Tue, 13 Oct 2026 12:01:13 +0100 Subject: [PATCH 1/2] Knuth-Plass: keep only the best active node per line and fitness class FindBreaks added every candidate node at every legal breakpoint, so while lines were too loose the active set doubled per breakpoint. One 25-breakpoint paragraph reached about 26 million nodes, and a 13-page DOCX took about 190 s to render to SVG (MS-23775, MS-27476 on Magic Suite test2). Keep the lowest-demerit node per (line, fitness class); on a tie the later candidate wins, which keeps the rendered output byte-identical for that document. Adds a regression test that times out without the fix. Co-Authored-By: Claude Opus 5.5 --- PanoramicData.Render.Test/KnuthPlassTests.cs | 27 ++++++++++++++++++++ PanoramicData.Render/KnuthPlassAlgorithm.cs | 12 +++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/PanoramicData.Render.Test/KnuthPlassTests.cs b/PanoramicData.Render.Test/KnuthPlassTests.cs index 7721e04..5d4bb60 100644 --- a/PanoramicData.Render.Test/KnuthPlassTests.cs +++ b/PanoramicData.Render.Test/KnuthPlassTests.cs @@ -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(); + 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); } diff --git a/PanoramicData.Render/KnuthPlassAlgorithm.cs b/PanoramicData.Render/KnuthPlassAlgorithm.cs index 9b749a1..27d125e 100644 --- a/PanoramicData.Render/KnuthPlassAlgorithm.cs +++ b/PanoramicData.Render/KnuthPlassAlgorithm.cs @@ -195,12 +195,20 @@ public static IReadOnlyList 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) { From fee5c83b1df903a3275b03f4e0611a84d532d624 Mon Sep 17 00:00:00 2001 From: David Bond Date: Tue, 13 Oct 2026 13:02:59 +0100 Subject: [PATCH 2/2] ReferenceGenerator: mark the assembly Windows-only so CI builds again Since its target moved from net10.0-windows to net10.0 (e4358eb, 2026-10-04), the Word COM interop calls fail CA1416 on the Linux CI runner, and the build job has failed on main ever since, which also blocks publishing. Declaring SupportedOSPlatform("windows") keeps the net10.0 target. Co-Authored-By: Claude Opus 5.5 --- .../PanoramicData.Render.ReferenceGenerator.csproj | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/PanoramicData.Render.ReferenceGenerator/PanoramicData.Render.ReferenceGenerator.csproj b/PanoramicData.Render.ReferenceGenerator/PanoramicData.Render.ReferenceGenerator.csproj index 4eb3dc0..5514699 100644 --- a/PanoramicData.Render.ReferenceGenerator/PanoramicData.Render.ReferenceGenerator.csproj +++ b/PanoramicData.Render.ReferenceGenerator/PanoramicData.Render.ReferenceGenerator.csproj @@ -8,6 +8,12 @@ false en + + + + <_Parameter1>windows + +