Skip to content

columnRenderLayoutsEqual ignores per-column gaps, so a section is laid out with the previous section's gutters #3966

Description

@Nathaniel-260

columnRenderLayoutsEqual decides whether two column configs split a region and whether the
normalized-columns cache is invalidated. It skips per-column gaps on purpose, and says why:

 * Per-column `gaps` are
 * intentionally ignored until geometry/separators consume them (step 4), so a gaps-only authored
 * delta does not split regions or invalidate the normalized-columns cache before it becomes
 * paint-significant.

Step 4 has landed. buildColumnGeometry reads gaps[i] ?? gap for both the column x and the
separator x, so a gaps-only delta moves every column after the first and moves the separators with
them. The stated precondition no longer holds, and the skip has become a defect.

Reporting separately from #3964: that one is normalizeColumnLayout and resolveColumnLayout
disagreeing about which gutter survives a dropped column. This one is the render-equality predicate
not looking at gutters at all. Different function, different trigger.

Impact

Two sections differing only in their per-column gutters compare render-equal. Nothing splits the
region, nothing invalidates the normalized-columns cache, so the later section is laid out with the
earlier section's gutters — and the painter draws the whole page's separators from them.

Direction-independent. A plain LTR document with two sections that differ only in
w:cols/w:col/@w:space reaches it.

Reproduction

import { columnRenderLayoutsEqual, buildColumnGeometry } from '@superdoc/contracts';

const base = { count: 3, gap: 48, widths: [100, 100, 300], equalWidth: false };
const a = { ...base, gaps: [20] };      // second gutter omitted -> falls back to the scalar 48
const b = { ...base, gaps: [20, 0] };   // second gutter spelled out as 0

columnRenderLayoutsEqual(a, b);         // true

They do not render the same. A short gaps array falls back to the scalar gap for the gutter it
omits; it does not mean 0. So buildColumnGeometry places column 2 forty-eight pixels apart
between the two, and puts the separator in two different places. The predicate calls them equal.

Why the obvious fix is wrong

Comparing the authored arrays directly trades this for the opposite defect, because the authored
arrays do not describe what renders. resolveColumnLayout emits gaps only when the author supplied
them and pads a short array with 0, while geometry falls back to the scalar gap and floors at 0.
So the authored arrays differ where the output is identical — an omitted array against one spelling
out the scalar gap, or a negative gutter against 0 — and they match where the output differs.

A spurious split is not merely a cache miss: a continuous section break with changed columns resets
to column 0 mid-page, so following content restarts in the first column instead of continuing where
it was.

The predicate has to derive the effective gutters the way normalizeColumnLayout does, which keeps
it exactly as discriminating as the geometry it stands in for.

Where

columnRenderLayoutsEqual in packages/layout-engine/contracts/src/column-layout.ts.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions