Skip to content

fix(print-layout): improve atlas feature framing - #1783

Merged
giswqs merged 5 commits into
mainfrom
fix/discussion-1778-atlas-layout
Aug 8, 2026
Merged

fix(print-layout): improve atlas feature framing#1783
giswqs merged 5 commits into
mainfrom
fix/discussion-1778-atlas-layout

Conversation

@giswqs

@giswqs giswqs commented Aug 8, 2026

Copy link
Copy Markdown
Member

Summary

  • fit atlas bounds to the actual print map frame so margin-based pages do not crop coverage features
  • add an optional inverted-fill mask for the current polygon feature
  • add a current atlas feature filter for attribute tables and use the true print crop bounds for page-extent filtering

Testing

  • npm run test:frontend
  • npm run build
  • pre-commit run --all-files
  • verified the reporter's GeoJSON in light and dark themes

Addresses #1778

Summary by CodeRabbit

  • New Features

    • Added atlas-page controls to filter content to the current atlas feature.
    • Added an option to mask areas outside the selected atlas feature on printed maps.
    • Improved atlas previews and exports with accurate print-frame geographic bounds.
    • Added automatic cleanup of atlas overlays when closing or disabling print layout.
    • Updated English and French guidance for the new filtering and masking options.
  • Bug Fixes

    • Improved handling of invalid or unavailable atlas feature geometry.
    • Ensured legends render correctly when using background images.
    • Prevented print-layout dialogs from closing during capture or export.

Copilot AI lite review requested due to automatic review settings August 8, 2026 17:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 836ba88c-769d-495b-a072-84ec88bb4944

📥 Commits

Reviewing files that changed from the base of the PR and between dea711d and 91e72bd.

📒 Files selected for processing (4)
  • apps/geolibre-desktop/src/components/layout/PrintLayoutDialog.tsx
  • apps/geolibre-desktop/src/lib/print-atlas-mask.ts
  • packages/map/package.json
  • tests/print-atlas-mask.test.ts

📝 Walkthrough

Walkthrough

Atlas printing now supports polygon-feature masks, print-frame viewport fitting, and optional table filtering by the active atlas feature. The dialog clears mask state during lifecycle changes, guards capture and export, and adds English and French controls.

Changes

Atlas print flow

Layer / File(s) Summary
Capture geometry and layout contracts
apps/geolibre-desktop/src/lib/print-atlas.ts, apps/geolibre-desktop/src/lib/print-layout.ts, tests/print-atlas.test.ts, tests/print-layout.test.ts
Added viewport-frame and map-body aspect-ratio helpers. Added tests for crop geometry and title placement.
Atlas mask and capture integration
packages/map/package.json, apps/geolibre-desktop/src/lib/print-atlas-mask.ts, apps/geolibre-desktop/src/components/layout/PrintLayoutDialog.tsx, tests/print-atlas-mask.test.ts
Added inverted polygon masks, lifecycle cleanup, frame padding, cropped bounds, map-fit propagation, and guarded closing during capture or export.
Current-feature table filtering
apps/geolibre-desktop/src/lib/print-data-blocks.ts, apps/geolibre-desktop/src/components/layout/PrintLayoutDialog.tsx, tests/print-data-blocks.test.ts
Added source-index row selection for table previews and atlas exports. Page-bound filtering remains the fallback.
Atlas controls and localization
apps/geolibre-desktop/src/components/layout/PrintLayoutDialog.tsx, apps/geolibre-desktop/src/i18n/locales/en.json, apps/geolibre-desktop/src/i18n/locales/fr.json
Added filtering and masking controls, explanatory text, and quoted legend SVG background URLs.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PrintLayoutDialog
  participant AtlasMask
  participant MapLibreMap
  participant AtlasExport
  PrintLayoutDialog->>AtlasMask: show selected atlas feature mask
  AtlasMask->>MapLibreMap: create or update inverted fill source
  PrintLayoutDialog->>MapLibreMap: fit cropped print-frame bounds
  PrintLayoutDialog->>AtlasExport: export filtered atlas page with map-fit state
Loading

Possibly related PRs

Poem

I’m a rabbit with an atlas to frame,
Masks bloom softly around each name.
Rows follow features, pages fit true,
Maps clear their layers when closing too.
Print bounds hop neatly into view.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: improving atlas feature framing in print layouts.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/discussion-1778-atlas-layout

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: dependency version conflict. Check your lock file or package.json.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/geolibre-desktop/src/components/layout/PrintLayoutDialog.tsx`:
- Around line 906-913: Update the mask-retention condition in the useEffect
within PrintLayoutDialog so it also requires an active atlas page and mask
availability before returning. When no active atlas page remains, or the mask
cannot be used, clear the map through clearAtlasFeatureMask; preserve the
existing behavior while open with atlas and mask enabled and an active page.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: dc39b33d-60f2-4ae4-91f0-b7fe71cab986

📥 Commits

Reviewing files that changed from the base of the PR and between f2142fc and 71aa3d9.

📒 Files selected for processing (10)
  • apps/geolibre-desktop/src/components/layout/PrintLayoutDialog.tsx
  • apps/geolibre-desktop/src/i18n/locales/en.json
  • apps/geolibre-desktop/src/i18n/locales/fr.json
  • apps/geolibre-desktop/src/lib/print-atlas-mask.ts
  • apps/geolibre-desktop/src/lib/print-atlas.ts
  • apps/geolibre-desktop/src/lib/print-data-blocks.ts
  • apps/geolibre-desktop/src/lib/print-layout.ts
  • tests/print-atlas.test.ts
  • tests/print-data-blocks.test.ts
  • tests/print-layout.test.ts

Comment thread apps/geolibre-desktop/src/components/layout/PrintLayoutDialog.tsx Outdated
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

🔍 Cloudflare PR preview

Item Value
Site https://4e883bdd.geolibre-preview.pages.dev
Demo app https://4e883bdd.geolibre-preview.pages.dev/demo/
Commit 91e72bd

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

🔍 GitHub Pages PR preview

Item Value
Site https://opengeos.org/pages-preview/GeoLibre/pr-1783/
Demo app https://opengeos.org/pages-preview/GeoLibre/pr-1783/demo/
Commit 91e72bd

Note

GitHub Pages built this preview successfully, but its serving edge returned HTTP 403 when checked. The links may still be propagating.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/geolibre-desktop/src/components/layout/PrintLayoutDialog.tsx (3)

826-836: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Limit the mask option to the current polygon page.

atlasMaskAvailable is true when any feature in the layer is polygonal. Atlas pages can still contain point or line features. showAtlasFeatureMask clears the mask for those features, but the UI still shows the toggle. A previous polygon mask can also remain visible until the delayed page drive runs.

Base availability on the current page’s source feature, or restrict mask pages to Polygon and MultiPolygon features.

Also applies to: 2266-2279

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/components/layout/PrintLayoutDialog.tsx` around
lines 826 - 836, Update atlasMaskAvailable and the related showAtlasFeatureMask
flow in PrintLayoutDialog so mask availability is determined by the current
atlas page’s source feature, not any polygon feature in the whole layer.
Restrict the toggle and mask state to Polygon or MultiPolygon pages, clearing
any existing mask immediately when the current page is non-polygonal.

1703-1722: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Pass the atlas capture fit through optionsForPage.

captureAtlasPage calls setMapFit(...) instead of returning the effective atlas fit, so handleAtlasExport receives the previous mapFit via options. Atlas pages therefore export with the wrong crop; if the capture used contain, graticule labels can be trimmed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/components/layout/PrintLayoutDialog.tsx` around
lines 1703 - 1722, The page options construction around optionsForPage must use
the effective fit produced by captureAtlasPage rather than the stale
options.mapFit value. Update captureAtlasPage to return or expose the fit
applied by setMapFit, then pass that atlas fit through handleAtlasExport into
optionsForPage so atlas exports preserve the actual contain/cover crop used
during capture.

1231-1242: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Cancel or block atlas work when the dialog closes.

Close does not stop captureAtlasPage, so later await waitForAtlasSettle(map) can call showAtlasFeatureMask(map, coverageFeature) or map.fitBounds(...) after the dialog has closed. Abort the atlas export at close or block close until atlasBusy/exporting finishes, and guard MapLibre mutations with the cancellation state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/components/layout/PrintLayoutDialog.tsx` around
lines 1231 - 1242, The captureAtlasPage flow must not mutate the map after the
dialog closes. Add cancellation handling tied to dialog close, abort or await
active atlas export work, and check that cancellation state before
showAtlasFeatureMask, clearAtlasFeatureMask, map.fitBounds, and after
waitForAtlasSettle; preserve normal atlas export behavior while open.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/geolibre-desktop/src/components/layout/PrintLayoutDialog.tsx`:
- Around line 826-836: Update atlasMaskAvailable and the related
showAtlasFeatureMask flow in PrintLayoutDialog so mask availability is
determined by the current atlas page’s source feature, not any polygon feature
in the whole layer. Restrict the toggle and mask state to Polygon or
MultiPolygon pages, clearing any existing mask immediately when the current page
is non-polygonal.
- Around line 1703-1722: The page options construction around optionsForPage
must use the effective fit produced by captureAtlasPage rather than the stale
options.mapFit value. Update captureAtlasPage to return or expose the fit
applied by setMapFit, then pass that atlas fit through handleAtlasExport into
optionsForPage so atlas exports preserve the actual contain/cover crop used
during capture.
- Around line 1231-1242: The captureAtlasPage flow must not mutate the map after
the dialog closes. Add cancellation handling tied to dialog close, abort or
await active atlas export work, and check that cancellation state before
showAtlasFeatureMask, clearAtlasFeatureMask, map.fitBounds, and after
waitForAtlasSettle; preserve normal atlas export behavior while open.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0e82c33e-a7eb-4fac-aab5-90bfc56f4523

📥 Commits

Reviewing files that changed from the base of the PR and between 71aa3d9 and dea711d.

📒 Files selected for processing (1)
  • apps/geolibre-desktop/src/components/layout/PrintLayoutDialog.tsx

? rowsForBlock(tableFeatureInfos, tableAllRows, tableFilterToPage, displayFilterBounds)
? tableFilterToAtlasFeature && tableUsesAtlasLayer && currentAtlasPage
? rowForAtlasFeature(tableAllRows, currentAtlasPage.sourceIndex)
: rowsForBlock(tableFeatureInfos, tableAllRows, tableFilterToPage, displayFilterBounds)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edge case: when tableFilterToAtlasFeature is checked but currentAtlasPage is momentarily null (e.g. the atlas filter currently yields 0 pages, or a newly selected page hasn't finished driving), this silently falls back to rowsForBlock(...) (page-extent/full-layer rows) instead of showing an empty table. That contradicts the hint text ("This takes priority over the page extent filter") and could surprise a user who expects "only the current feature" to mean "none" rather than "whatever the page-extent filter would show."

Same pattern applies to the export path at line ~1709 (tableFilterToAtlasFeature && tableUsesAtlasLayer ? rowForAtlasFeature(...) : rowsForBlock(...)), though there pages[i] is always defined so it's less reachable in practice.

Confidence: medium — this is a real behavioral gap, though it only surfaces in the narrow window where the atlas is enabled with a matching table layer but has no current page.

clearAtlasFeatureMask(map);
return false;
}
const collection: FeatureCollection<Polygon | MultiPolygon> = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor efficiency nit: buildInvertedMask (in @geolibre/map) memoizes on the identity of the collection object it's given, but showAtlasFeatureMask constructs a brand-new FeatureCollection literal on every call. That means the memoization never hits and turf.mask() re-runs on every invocation — once per debounced page-drive and once per page during a full atlas export/print. For a dense polygon coverage layer this adds avoidable recompute on every export page. Not a correctness issue, just worth knowing the cache is effectively bypassed here.

Confidence: low — likely a negligible cost for typical polygon complexity, but could matter for large/complex coverage geometries exported across many pages.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • PrintLayoutDialog.tsx:1131 — When "Only the current atlas feature" is checked but currentAtlasPage is null (atlas filter yields 0 pages, or a newly selected page hasn't finished driving yet), the table silently falls back to page-extent/full-layer rows instead of showing nothing, contradicting the "takes priority over the page extent filter" hint. Medium confidence.

Security

  • None found.

Performance

  • print-atlas-mask.ts:27showAtlasFeatureMask builds a fresh FeatureCollection object on every call, bypassing buildInvertedMask's identity-keyed memoization, so turf.mask() recomputes on every page drive/export page. Low confidence, likely negligible in practice.

Quality

  • The new print-atlas-mask.ts module (source/layer add-remove logic) has no dedicated unit tests, unlike the other new pure helpers in this PR (atlasViewportFrame, mapBodyAspectRatio, rowForAtlasFeature), all of which got tests. That said, the closest existing analog (showPrintExtent/clearPrintExtent) is also untested in this codebase, so this is a weak, low-confidence nit rather than a clear convention violation.
  • atlasMaskAvailable gates the mask checkbox on any polygon feature existing in the coverage layer, but for a mixed-geometry layer a specific page's feature may not be a polygon, in which case showAtlasFeatureMask silently no-ops. Minor UX nit, low confidence.

CLAUDE.md

  • i18n additions are present in both en.json and fr.json with matching keys, as required. No other CLAUDE.md-governed constants/catalogs are touched by this change.

The core fix (atlasViewportFrame/mapBodyAspectRatio/the padding passed to fitBounds, and the resulting frameBounds computation) was checked carefully against coverScaleFor/drawLayout's actual centered crop math in print-layout.ts and is internally consistent — the symmetric padding/crop rectangles line up with how the captured image is later cover-cropped into the print body, so the described bug (margin-mode pages cropping coverage features) is genuinely fixed. The rowForAtlasFeature/sourceIndex alignment between atlasLayer and tableLayer was also verified correct since both come from the same atlasLayers list keyed by the same layer id.

});
}
return true;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test coverage gap (low-medium confidence). This new module has no accompanying test file. The codebase already has a precedent for testing this exact kind of MapLibre-mutating code with a fake Map stub — see tests/print-extent.test.ts, which was added specifically because print-extent.ts "had no coverage before" and is DOM/map-driven the same way this file is. clearAtlasFeatureMask/showAtlasFeatureMask are pure enough (a few getLayer/addLayer/removeSource calls plus the Polygon/MultiPolygon type guard) that a similar lightweight fake-map test would be cheap to add and would catch regressions in the guard logic or layer/source lifecycle.

Comment on lines +27 to +31
const collection: FeatureCollection<Polygon | MultiPolygon> = {
type: "FeatureCollection",
features: [feature as Feature<Polygon | MultiPolygon>],
};
const mask = buildInvertedMask(collection);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor performance nit (low confidence). buildInvertedMask memoizes on the FeatureCollection object identity via a WeakMap (packages/map/src/derived-geometry.ts), but a brand-new collection object is constructed here on every call, so the cache never hits and @turf/mask's polygon-clipping union is recomputed from scratch each time — including every time captureAtlasPage re-applies the mask for the same page (e.g. after the fixed-scale zoom correction triggers a second settle+capture). Since it's one polygon per call this is likely cheap in practice, but for a large "features" atlas with many pages and the mask enabled it adds needless repeated work. Not blocking, just flagging since the memoization exists specifically for this kind of repeated-call scenario.

Comment on lines +898 to +902
const atlasMaskAvailable = Boolean(
atlasCoverage === "features" &&
(currentAtlasFeature?.geometry?.type === "Polygon" ||
currentAtlasFeature?.geometry?.type === "MultiPolygon"),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor UX nit (low confidence). atlasMaskAvailable only looks at the current page's feature geometry. For a "features" coverage layer that mixes polygon and non-polygon geometries, the "Mask area outside current feature" toggle (and its state) will appear/disappear as the user steps through atlas pages — a page on a non-polygon feature hides the control even though atlasMaskEnabled stays true underneath and silently resumes on the next polygon page. That's probably fine functionally (the mask is inherently per-feature), but the checkbox popping in and out while paging may read as a glitch rather than an intentional per-page state. Consider basing availability on whether any page in the atlas has polygon geometry, if that's not already the intent.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • None found with meaningful confidence. I traced the new atlasViewportFrame/captureAtlasPage geometry math (padding vs. crop-rect vs. mapBodyAspectRatio/coverScaleFor) end-to-end and it's internally consistent; the rowForAtlasFeature/sourceIndex row-alignment invariant holds because layerRows/collectAtlasFeatures both index off the same unfiltered features array and tableUsesAtlasLayer gates on matching layer id. I also checked the mask show/clear effect against the async captureAtlasPage flow for races (dialog open/close, atlas export loop, setAtlasIndex re-renders) — each page's capture re-applies its own mask state synchronously before its first await, so no stale-mask frame can be captured. (High confidence, nothing to flag.)

Security

  • No injection, unsafe input handling, or secret-handling concerns — all changes are client-side map/canvas geometry and UI state. (High confidence.)

Performance

  • showAtlasFeatureMask builds a fresh FeatureCollection object on every call, which defeats buildInvertedMask's WeakMap memoization, so the turf mask() union is recomputed on every atlas page (and again on the second capture after a fixed-scale zoom correction) instead of being cached. Likely cheap per call, but avoidable repeated work for a large masked atlas. Low confidence, minor. (Inline comment posted.)

Quality

  • The new print-atlas-mask.ts module has no unit tests, despite the codebase having an established precedent (tests/print-extent.test.ts) for testing exactly this kind of MapLibre-mutating helper with a lightweight fake Map. Medium-low confidence this is worth doing given how small the module is, but it's a real coverage gap. (Inline comment posted.)
  • atlasMaskAvailable only checks the current atlas page's geometry type, so on a coverage layer mixing polygon and non-polygon features, the "Mask area outside current feature" toggle appears/disappears as the user pages through the atlas even though the underlying atlasMaskEnabled state persists. Likely intentional (mask is inherently per-feature) but could read as UI flicker. Low confidence. (Inline comment posted.)

CLAUDE.md

  • The new i18n strings are added to both en.json and fr.json with matching keys and consistent typographic-apostrophe style. The direct map.addSource/addLayer/removeLayer calls in print-atlas-mask.ts bypass the store-driven layer-sync pattern, but this mirrors the existing, already-established print-extent.ts pattern for ephemeral live-map print overlays, so it's consistent with codebase convention rather than a violation. No other guideline in CLAUDE.md appears implicated by this change. (High confidence.)

Comment on lines +1229 to +1231
const canvas = map.getCanvas();
const viewportWidth = canvas.clientWidth || canvas.width;
const viewportHeight = canvas.clientHeight || canvas.height;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

canvas.clientWidth/clientHeight are CSS pixels, but the || canvas.width / || canvas.height fallback (used when the client size reads 0, e.g. a not-yet-laid-out or hidden canvas) returns device pixels (canvas.width = dispW * dpr per line ~1608). atlasViewportFrame's result feeds both fitBounds's padding and the map.unproject([x, y]) calls a few lines below (line ~1307), both of which expect CSS-pixel screen coordinates. If the fallback path is ever hit on a HiDPI display, the computed padding/crop rectangle will be off by the device pixel ratio, skewing the atlas fit and the frameBounds used to filter data blocks.

Confidence: low-to-medium — this only triggers when clientWidth/clientHeight are 0 while an atlas capture runs, which should be rare since the map must be visible to drive the camera, but the fallback silently produces wrong units rather than failing loudly.

Comment on lines +54 to +55
paint: {
"fill-color": "#ffffff",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mask fill is a hardcoded #ffffff at 0.7 opacity. On a dark basemap/theme this reads as a bright white overlay rather than an adaptive "de-emphasize" treatment — worth confirming this is the desired look for the dark-theme case the PR description says was tested, since GeoLibre otherwise threads light/dark awareness through most UI-facing rendering.

Confidence: low — this may well be an intentional, theme-independent print-composition choice (print output is typically a light background regardless of app theme).

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Both inline comments posted successfully.

Code review

Bugs

  • PrintLayoutDialog.tsx:1229-1231viewportWidth/viewportHeight fall back to canvas.width/canvas.height (device pixels) when clientWidth/clientHeight read 0, but atlasViewportFrame's output feeds fitBounds's padding and map.unproject, both of which expect CSS-pixel coordinates. On a HiDPI display this fallback path (only reachable if the canvas has zero layout size while a capture runs) would compute the atlas fit/crop off by the device pixel ratio. Confidence: low-medium.
  • No other logic errors found in the new/changed atlas-framing math (atlasViewportFrame, mapBodyAspectRatio, geometryBounds-based frame bounds), the mask cache/reuse logic, or the rowForAtlasFeature/sourceIndex alignment — traced these through collectAtlasFeatures, buildAtlasPages, buildLineAtlasPages, and tableAllRows and they stay index-consistent. Mask cleanup is wired into open/close, unmount, and the mask-availability effect, so no obvious leaked MapLibre layer/source.

Security

  • None found. No new user input parsing, no injection surface; the mask/geometry code operates on already-loaded layer data.

Performance

  • None found. The inverted-mask computation is memoized per feature object via a WeakMap, so page revisits and repeated exports of the same feature don't recompute it. The viewport-frame math is O(1) per page.

Quality

  • print-atlas-mask.ts:54-57 — the mask fill color is hardcoded to #ffffff, not theme-aware; worth a quick confirmation it's intentional for both light and dark themes. Confidence: low.
  • Minor, non-functional: the diff carries a fair amount of pure Prettier reformatting unrelated to the feature (e.g. PAPER_SIZES array entries, several test object literals, one inline style={{...}}), presumably from the pre-commit npm-build/format hook mentioned in the PR's testing notes. Harmless, just adds diff noise.

CLAUDE.md

  • New user-facing strings correctly go through t() and are added to both en.json and fr.json. The packages/map/package.json subpath export (./derived-geometry) follows the existing pattern of exposing raw .ts sources. No CLAUDE.md convention appears violated.

if (source) source.setData(mask);
else map.addSource(SOURCE_ID, { type: "geojson", data: mask });
if (!map.getLayer(FILL_LAYER_ID)) {
map.addLayer({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addLayer here has no beforeId, so the mask fill always lands at the very top of the style, above the graticule label layer (GRATICULE_LABEL_LAYER_ID). captureAtlasPage explicitly switches to mapFit: "contain" specifically so the graticule's edge coordinate labels aren't cropped — but if a user also enables "Mask area outside current feature" on a page where the polygon doesn't fill the frame, the 70%-opacity white mask will sit on top of those edge labels (which are typically drawn right at/near the edges, i.e. in the masked "outside" region) and wash them out. Consider passing a beforeId (e.g. the graticule label layer, when present) so the mask renders under labels that should stay legible.

Confidence: medium — this is a plausible visual regression for the graticule + polygon-mask combination, but I haven't run the app to confirm the labels actually fall in the masked region.

Comment on lines +1319 to +1320
"filterToAtlasFeature": "Seulement l’entité courante de l’atlas",
"filterToAtlasFeatureHint": "Disponible lorsque la table utilise la couche de couverture de l’atlas. Ce filtre est prioritaire sur l’emprise de la page."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor style nit: these two new strings use a curly apostrophe () in "l’entité"/"l’atlas", while the rest of this file consistently uses a straight apostrophe ('), e.g. filterToPageHint right above uses "S'applique", "dataChart.noNumericFields" uses "n'a aucun", etc. Same for atlas.maskOutside/maskOutsideHint below. Worth normalizing for consistency with the rest of the catalog.

Confidence: low — purely cosmetic, doesn't affect functionality.

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • showAtlasFeatureMask's addLayer call (apps/geolibre-desktop/src/lib/print-atlas-mask.ts:49) adds the mask fill with no beforeId, so it always renders on top of the graticule label layer. captureAtlasPage deliberately switches to mapFit: "contain" to keep graticule edge labels un-cropped, but if "Mask area outside current feature" is also enabled, the 70%-opacity white mask can wash out those same edge labels since they typically sit in the "outside" region being masked. Confidence: medium.

Security

  • None found.

Performance

  • None found. The mask WeakMap-based caching (feature → wrapped FeatureCollection, then buildInvertedMask's own cache) correctly avoids recomputing the turf mask() union on repeated captures of the same feature.

Quality

  • fr.json's two new string pairs (dataBlocks.filterToAtlasFeature* and atlas.maskOutside*) use curly apostrophes () while the rest of the file consistently uses straight apostrophes ('). Cosmetic only. Confidence: low.

CLAUDE.md

  • No violations found: new strings correctly go through t() and are added to both en.json/fr.json; no physical RTL-unsafe classes were introduced; the packages/map/package.json subpath export (./derived-geometry) is a reasonable, lightweight addition (the module has no heavy/DOM-dependent imports) rather than pulling in the full @geolibre/map barrel, which already re-exports MapCanvas/maplibre-gl-dependent code that could be unsuitable for the plain-Node test environment used by tests/print-atlas-mask.test.ts.

Overall the core fix (fitting the atlas camera to the actual cover-cropped print frame, and deriving viewBounds/data-block filtering from that same crop) is logically sound and well covered by the new unit tests (atlasViewportFrame, mapBodyAspectRatio, rowForAtlasFeature, mask add/reuse/clear). I traced the sourceIndex alignment between atlas pages and table rows and confirmed it holds because tableLayer.geojson and atlasLayer.geojson are the same object whenever the "filter to atlas feature" path is taken.

@giswqs
giswqs merged commit 5dedd17 into main Aug 8, 2026
16 checks passed
@giswqs
giswqs deleted the fix/discussion-1778-atlas-layout branch August 8, 2026 18:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants