Commit graph

192 commits

Author SHA1 Message Date
isaprettycoolguy@protonmail.com
a9e6a528f4 chore: bump to 1.4.6
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 07:05:32 -04:00
isaprettycoolguy@protonmail.com
6882a3b504 perf+fix: calibration handle sizing + snapshot-based drag/resize/nudge
Resize handles live inside the scaled bg-image (~4.8×) / grid (~0.35×) layers,
but --duckmage-cal-scale only cancelled the viewport zoom, so image handles
rendered ~5× too large and grid handles too small. updateCalHandleScale() now
sets the var per layer to 1/(zoom × layerScale) so handles stay a constant
on-screen size at any zoom/scale.

Calibration drag/resize/arrow-nudge was paint-bound at ~2fps: moving or scaling
the grid re-rasterises all 3843 clip-path hex cells every frame (a probe showed
~0.05ms JS work vs ~540ms frame gaps). During an active adjustment we now swap
the cells for a single pre-drawn <canvas> colour snapshot (buildCalibrationSnapshot)
— transforming one canvas is a single composite op — and drop icons/labels/the
SVG outline. Mouse drags coalesce moves to one rAF tick and tear down on mouseup;
arrow-key nudges use a bake-on-quiet settle timer (no mouseup to end on).

Adds a dev-gated drag probe (window.DUCKMAGE_PERF) and dev/cal-handle-size-check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 07:04:12 -04:00
isaprettycoolguy@protonmail.com
b69131f16d perf: cut redundant per-item work on map + table load
Map grid: the per-hex render loop called getTerrainFromFile +
getIconOverrideFromFile + getGmIconsFromFile, each re-running
getAbstractFileByPath + getFileCache — 3 identical cache lookups per hex
(~11.5k redundant ops on a 3843-hex map). Fetch frontmatter once and feed
three new pure extractors (terrainFromFm/iconOverrideFromFm/gmIconsFromFm).
Reuse the bg-image layer across renders (bgLayerEl/bgLayerSrc) so
renderGrid()'s viewportEl.empty() no longer re-decodes the image each time.
Tag painted hexes with .duckmage-hex-painted (setHexColor) and switch the
bg-image empty-hex rule to a class selector instead of a slower
:not([style*="background-color"]) attribute-substring match.

Table view: memoise the name→entry palette Map per region instead of
rebuilding it per row, and re-filter only the rows just filled in each batch
(applyFilters(rows?)) instead of re-scanning all rows after every batch.

Adds dev/bg-hex-recalc-bench + unit tests for the new pure extractors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 07:03:23 -04:00
isaprettycoolguy@protonmail.com
e74bc0d6f6 fix: eliminate O(n²) layout thrash in renderCoordLabelsLayer (open-time stall)
renderCoordLabelsLayer read each hex's offsetWidth/offsetLeft/offsetTop and
created the label DOM inside the SAME loop. Every offset read after a DOM write
forces a synchronous layout flush, so the loop was O(n²): on the chult map
(3843 hexes) opening stalled the main thread ~21s (regression from commit
10475fd, the coord-label layer).

Split into a read phase (collect all hex geometry into an array) then a write
phase (create all labels) — matches the read-then-write pattern already used by
renderPathOverlay/renderFactionOverlay. ~21s → ~0.1s (dev/coord-label-thrash-
bench measures ~198× batched vs interleaved).

Bakes in a regression guard: CLAUDE.md convention forbidding interleaved
layout-read / DOM-write in per-hex loops, plus the committed benchmark.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 06:57:16 -04:00
isaprettycoolguy@protonmail.com
4cd0550a9a fix: render road/river chains in parallel + skip zoom-bake on bg-image maps
Path chains that share a route are stroked in path-type order, so a later
chain on the same hexes used to paint over the earlier one (hexmaker#30).
Add computeLaneOffsets() + offsetPolyline() (hexGeometry.ts): chains sharing
a segment are grouped via union-find and given symmetric perpendicular lane
offsets PER DISTINCT TYPE — same-type chains merge into one stroke, different
types fan out side by side. Chains with no shared segment keep offset 0, so
the common single-path case is pixel-identical. Wired into both the on-screen
overlay (renderPathOverlay) and the PNG exporter (mapPngRenderer) so they match.

Also fixes a macOS-only bg-image zoom drift: bakeZoom grows the em-based hex
grid via font-size while the px-based bg image is grown through a separate
multiply; device-pixel snapping on fractional/Retina DPR rounds the two paths
differently, so the grid slipped off the image on zoom settle. scheduleZoomBake
now skips baking whenever a bg image is present, keeping zoom as a single
composited viewport scale() that can't drift on any DPR.

Adds ISSUES.md (local mirror of open upstream issues for planning).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 06:56:46 -04:00
isaprettycoolguy@protonmail.com
ee6ba76274 chore: release 1.4.5 2026-06-23 19:05:34 -04:00
isaprettycoolguy@protonmail.com
a2dcfd2cfb fix: combo dropdown no longer jumps modal to top on first click
The hex-editor / map-link combo dropdowns lived inside the scrolling
`.modal-content` and were positioned `absolute`, so to avoid being
clipped the code toggled the scroll container to `overflow-y: visible`
while open. Switching a *scrolled* element from `overflow: auto` to
`visible` forces its scrollTop to 0 — so the first click on a section
you'd scrolled down to (Towns/Quests/etc.) snapped the whole modal to
the top. Browser-universal, not Linux-only (issue #31).

Replace the overflow hack with a shared `HexmakerModal.anchorDropdown`
helper that positions the dropdown `position: fixed`, anchored to its
trigger from JS. A fixed child resolves its containing block to the
viewport (no modal ancestor has a transform), so it escapes every
overflow clip without touching the scroll container — scroll position
is never disturbed. The helper repositions on scroll/resize/filter and
flips above the input when there's no room below. Both HexEditorModal
and MapLinkModal use it.

Also remove the dead `renderLinkSection` method and its now-orphaned
imports from HexEditorModal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 19:02:46 -04:00
isaprettycoolguy@protonmail.com
852e178113 chore: bump to 1.4.4
Highlights since 1.4.3:
- feat: delta-aware + rAF-batched wheel zoom (no inertia tail)
- feat: coord labels render above paths/icons in their own z:10 layer
- feat: coord label size / font / color settings (defaults to white)
- fix: hex cell spacing slider shows current value + refreshes live

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-21 13:00:57 -04:00
isaprettycoolguy@protonmail.com
10475fd21c feat: smoother wheel zoom + coord label layer/settings
Wheel zoom: delta-aware (exp(-deltaY * k)) + rAF-batched so multiple
events per frame collapse to one paint. No inertia tail — the visual
lands exactly where the cumulative wheel deltas placed it and stops
the frame after the last event. Bake-on-quiet at 80ms so coords
un-blur fast without firing mid-stream.

Coord labels now render in .duckmage-coord-labels-layer at z:10 (above
path SVG, faction, region, GM-icon overlays) instead of inside each
hex. Positioned by percentage of gridContainer dimensions so they
auto-track through bake-zoom. Inline .duckmage-hex-label stays in DOM
hidden as insertBefore anchor for hex-icon ordering.

Settings: new "Coordinate label" row with size slider + font dropdown
(Interface/Serif/Monospace) + color picker (defaults to white). Hex
cell spacing slider now shows its current value and refreshes open
maps live.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-21 13:00:32 -04:00
isaprettycoolguy@protonmail.com
a247b43851 chore: bump to 1.4.3
Highlights since 1.4.2:
- feat: multi-GM-icon support per hex with hex-subgrid layout (closes hexmaker#28)
- feat: right-click in GM paint mode removes one icon; count badge anchors to icon
- fix: GM-only paint mode is additive across icon switches
- fix: HTML region labels with PCA rotation — no slip-on-zoom
- fix: HTML coord labels honor coordPlacement (top/middle/bottom)
- fix: removed SVG coord-label copies; rely on in-hex HTML labels
- fix: token layer centered in gridContainer, not viewportEl

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-21 11:45:07 -04:00
isaprettycoolguy@protonmail.com
4ef91e3005 fix: HTML region labels with PCA rotation (no slip-on-zoom)
Same approach used for coord labels: render region names as HTML
divs inside gridContainer, positioned as percentages of the grid's
pixel dimensions. The PCA-derived rotation angle is preserved as
a CSS transform composed after the centering translate so labels
still align with each region's longest axis. Because the labels
ride the em-based hex layout instead of pixel-pinned SVG text,
they no longer slip during a bake-zoom transition.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-21 11:40:39 -04:00
isaprettycoolguy@protonmail.com
d062bef610 fix: HTML coord labels honor coordPlacement setting (top/middle/bottom)
After the SVG coord-label rendering was removed in 3cc956e, the
top/middle/bottom placement setting silently stopped working — the
HTML `.duckmage-hex-label` spans were just flex-centered inside the
hex, ignoring the setting.

Restored honour via CSS:
- `.duckmage-hex-label` is now `position: absolute; left: 50%;
  transform: translate(-50%, -50%)` so the parent's flex centering
  doesn't pin it. `top` is set per-placement class.
- `renderGrid` writes a `duckmage-coord-{top,middle,bottom}` class on
  the viewport, derived from `settings.coordPlacement`. Three CSS
  rules anchor the label at top: 22% / 50% / 78% (matching the
  previous SVG ±0.28 hex-height offsets so visual position is
  unchanged for users who never touched the setting).
- Default (when no class set) renders at top: 78% so an
  uninitialized state still looks like "bottom" — the same fallback
  the dropdown UI defaults to.

Coord-slip regression test in
frontend-testing/examples/coord-slip-on-zoom still PASSes: 0 slips
in the NEW bake mid-frame.
2026-06-21 11:29:11 -04:00
isaprettycoolguy@protonmail.com
3cc956ea79 fix: stop rendering SVG coord-label copies; rely on HTML labels (no slip)
The previous fix (tear-down SVG before font-size mutation in bakeZoom)
addressed one path but the user kept seeing labels "slip a few hexes
away on zoom." More aggressive fix here: stop rendering the SVG copies
entirely, rely solely on the HTML `.duckmage-hex-label` spans that
live inside each hex's DOM.

The SVG coord-label copies were rendered into the path overlay
specifically so they'd sit above road/river strokes regardless of CSS
stacking. The trade-off was that their pixel positions were computed
once at SVG-creation time against the THEN-current hex sizes. Once
the user zoomed and bake mutated the viewport font-size, every
em-based hex dimension grew — but the SVG label coordinates didn't.
Any paint frame between the font-size mutation and the SVG rebuild
showed the labels stranded at the OLD smaller hex positions →
"sliding several hexes away" → SVG rebuild → "snap back." The
"snap back" was the noticeable artifact users described.

Removing the SVG copies eliminates the entire race. The HTML labels
ARE part of the hex DOM (`<span>` child of the hex `<div>`), so they
track the hex layout perfectly across any font-size change — no
slip possible. The CSS rule that hid HTML labels while SVG-mode was
on is also dropped; HTML labels are always visible now.

Cost: a road or river stroke drawn through a hex can visually obscure
that hex's coord label. Acceptable vs. the slip — and most maps have
sparse paths.

Regression test moved into the shared frontend-testing suite at
examples/coord-slip-on-zoom/. Two-half assertion (same pattern as
hex-token-alignment):
  1. "OLD" bake order MUST reproduce the slip at the mid-transition
     frame (else the test isn't exercising the bug). Currently shows
     39 slips with one hex drifted by 99 px — exactly what the user
     described.
  2. "NEW" bake order MUST have zero slips at the mid-transition
     frame.

Both halves pass.
2026-06-21 11:21:08 -04:00
isaprettycoolguy@protonmail.com
0e65565e25 fix: coord labels no longer slip up-left during zoom bake
The bug the user has been chasing for several sessions. Sandbox
repro at dev/coord-slip-sandbox.html, snapshot-coord-slip.mjs
captures the half-baked frame so we can SEE the slip.

Root cause: bakeZoom mutates the viewport font-size mid-function,
which triggers a layout reflow growing every em-based hex
dimension. The path-overlay SVG holds coord labels at pixel
positions computed against the PRE-bake hex sizes, so any paint
frame between the font-size change and the SVG rebuild shows the
labels at where the hexes USED to be — visibly shifted up-left of
the now-larger hexes. updatePathOverlay() further down in bakeZoom
eventually fixes it ("snap back into place"), but the intermediate
slip-then-snap is what the user reported across multiple zoom
sessions.

Fix: tear down the path / faction / region SVGs at the START of
bakeZoom, before the font-size mutation. The CSS rule
`.duckmage-svg-labels-active .duckmage-hex-label { visibility:
hidden }` is keyed off a class on the viewport — that class gets
removed alongside the SVG, so the intermediate paint falls back to
the HTML `.duckmage-hex-label` spans. Those live inside each hex's
own DOM tree, so they always track the hex's layout perfectly
across font-size changes. No slip.

The overlays are then re-rendered later in the function via the
existing updatePathOverlay() / updateFactionOverlay() /
updateRegionOverlay() / updateTokenLayer() calls. Net result: zero
extra rebuild work, just earlier tear-down.

Sandbox confirms:
  buggy-mid: SVG labels visibly shifted up-left of small hexes
  fixed-mid: HTML labels inside hexes, tracking correctly

351 unit tests / 18 e2e pass.
2026-06-21 11:09:12 -04:00
isaprettycoolguy@protonmail.com
c82bbb3fdb feat: right-click removes one GM icon + count badge sticks to icon
Two GM icon UX refinements requested after the multi-add painter shipped.

1. Right-click on a hex while painting a GM icon now REMOVES one
   instance of the currently-selected icon from that hex's stack
   (no-op if the icon isn't on the hex). The symmetric inverse of
   the additive left-click — same UX as the editor picker's tile
   right-click. New `removeOneGmIconFromHex` does the work and
   records an undo entry so the removal is reversible. Off-hex
   right-click still opens the painter context menu (mode change,
   etc.).

2. Count badge now anchors to the icon's bottom-right CORNER (was
   floating below it at a multiplied offset, which drifted away from
   the icon at small sizes — especially noticeable at 4+ stacked
   icons where the per-slot icon size drops to 22% of the hex
   width). Uses SVG text-anchor=end + dominant-baseline=alphabetic so
   the badge sits inside the icon's bounding box regardless of the
   icon's pixel size.
2026-06-21 10:56:05 -04:00
isaprettycoolguy@protonmail.com
47a77b1a4b fix: GM-only paint mode is additive + GM-only flag persists
Two bugs from the multi-icon work:

1. GM-only stickiness: `exitIconMode` was resetting `paintIconGmOnly =
   false` on every mode toggle. Switching which icon you were painting
   (close picker → reopen with a different icon) silently dropped you
   back into regular-icon mode, so the second paint clobbered the
   override layer instead of the GM layer — without anything visible
   to tell you why. Fix: leave `paintIconGmOnly` set across mode
   toggles; the user's GM-vs-regular preference now persists for the
   whole session.

2. Painter was overwriting instead of adding. Each GM paint click
   replaced the entire `gm-icons` list with `[selected]`, so you'd
   only ever see one icon no matter how many times you clicked. The
   multi-icon model (hexmaker#28) needs the painter to APPEND a copy
   of the selected icon on each click, with the count incrementing
   on stacked clicks of the same icon. Eraser still clears the entire
   list.

Plumbing changes:
- `pendingGmIconWrites` value shape: `{ icon: string|null }` →
  `{ list: string[] }`. Empty list clears the hex's GM icons.
- New `scheduleGmIconsListWrite` replaces `scheduleGmIconWrite`. Same
  coalescing pattern, persists full list.
- `IconUndoEntry` gained `oldGmList` / `newGmList` for proper
  multi-icon undo. Legacy `oldIcon`/`newIcon` kept for stroke entries
  recorded by older paint paths (back-compat).
- `applyIconStroke` prefers the new list snapshot, falls back to
  the legacy single-icon fields.
- `setGmIconInFile` import dropped from HexMapView — painter uses
  `setGmIconsInFile` directly now.

What's intentionally not changed yet: right-click on a hex in GM
paint mode still exits the tool (the existing default). Adding
right-click=remove-one would conflict with that and warrants its own
UX call. The editor's picker already supports per-icon removal via
right-click on tiles — `setGmIconsInFile([])` to clear all.

351 unit tests / 18 e2e pass.
2026-06-21 10:50:40 -04:00
isaprettycoolguy@protonmail.com
e98a6ab084 fix: actually render multiple GM icons + auto-enable GM layer + hex-subgrid layout
Three fixes on top of the multi-GM-icon scaffold from 8926f60:

1. Auto-enable the GM layer when the user picks a GM-only paint icon.
   Without this, the paint silently writes `gm-icons` to the file but
   the layer is off so nothing visually shows — gave the impression
   that multi-icon wasn't working. Same self-preservation move the
   region/faction painters already do for their own overlays.

2. The "only one icon shows up" bug. The editor was writing the full
   icon list to frontmatter correctly, but the on-map render only saw
   one icon because:
     - editor calls onChanged()
     - HexMapView schedules renderGrid via 300ms setTimeout
     - renderGrid re-reads frontmatter via getGmIconsFromFile
   …and Obsidian's metadata cache lags processFrontMatter writes by a
   tick, so the cache read sometimes returned the pre-write state.
   Fix: extend onChanged with a `gmIconsOverrides: Map<path, list>`
   third arg (parallel to the existing terrain/icon overrides). The
   editor passes the new list directly. renderGrid applies it to the
   hex's `data-gm-icons` attribute without re-reading the cache. No
   race possible.

3. Replaced the tokenGroupOffsets layout (which centered the first
   icon) with a new `GM_ICON_HEX_SUBGRID` — 7 slots in a honeycomb
   pattern (NW, NE, W, C, E, SW, SE), filled in reading order so the
   first icon lands top-left and additions flow right then down.
   Matches the user's "hex-subgrid that starts at top-left" ask.
   Icons beyond the 7th unique icon stay in frontmatter but don't
   render (storage isn't capped, layout degrades gracefully).

351 unit tests / 18 e2e pass. Manual reload required for UX check.
2026-06-21 10:39:58 -04:00
isaprettycoolguy@protonmail.com
8926f60eb7 feat: multi-GM-icon support per hex (closes hexmaker#28)
A hex can now carry multiple GM icons. The HexEditorModal picker uses
left-click to add and right-click to remove individual icons, with a
"×N" badge on each tile when an icon is stacked. On the map, the icons
lay out in a hex-subgrid arrangement (reusing the same
tokenGroupOffsets helper the token layer uses, so multi-icon layout
stays visually consistent across the two systems).

Storage:
- New frontmatter key `gm-icons: [name, name, ...]` (array, duplicates
  preserved for count).
- Legacy `gm-icon: name` (singular) still READ for back-compat —
  existing notes Just Work. Writers always emit the new array key AND
  clean up the legacy singular at the same time, so a note touched by
  the new model standardises on the new shape.

Helpers (src/frontmatter.ts):
- New `getGmIconsFromFile(app, path): string[]` — authoritative reader.
- New `setGmIconsInFile(app, path, icons: string[])` — list writer.
- `getGmIconFromFile` / `setGmIconInFile` kept as thin compat wrappers
  on top of the list helpers so the on-map painter and any future
  caller that wants single-set semantics still has them.

HexEditorModal:
- `renderGmIconCountGrid` replaces the prior single-pick grid for GM
  icons. Left-click increments, right-click decrements (clamped at 0),
  the "— clear all —" tile wipes the list. Each tile shows ✓ for 1 and
  ×N for 2+. Writes the full list to frontmatter via `setGmIconsInFile`.
- Authoritative read via `getGmIconsFromFile`, keeping `directGmIcon`
  populated as the first entry for the rest of the modal code that
  still uses the singular field.

HexMapView:
- `data-gm-icon` (single) becomes `data-gm-icons` (JSON-encoded array)
  on each hex element. New `parseGmIconsDataset` helper decodes it.
- `renderPathOverlay` GM-icon section: iterates unique icons in
  first-seen order; positions each via `tokenGroupOffsets(n)` so they
  cluster nicely in a sub-hex pattern. Icon size shrinks with slot
  count (38%→22%) to keep them inside the hex. "×N" SVG text badge in
  the lower-right when count > 1.
- Painter (paintIconGmOnly mode) still SETs to a single-icon list —
  same overwriting behavior as before; multi-add is editor-only.
  Painter undo records the FIRST prior icon (acceptable fidelity loss
  for the painter's single-set semantics).

Styles:
- New `.duckmage-svg-gm-icon-count` for the on-map ×N badge (white
  fill, stroked black for legibility over any terrain).
- New `.duckmage-icon-count-badge` for the picker tile badge.

Validation: 351 unit tests + 18 e2e pass. Manual UX test pending.
2026-06-21 10:27:12 -04:00
isaprettycoolguy@protonmail.com
4d9550f8b1 fix: center tokens on hexes + zero-risk renderGrid micro-opts
Carries the absolute-minimum subset from the perf-pass-tier-1 branch
that the full cherry-pick didn't introduce regressions in.

Bug fix:
- Tokens are now centered on their hexes. `buildTokenCenterMap`
  returns coordinates relative to gridContainer (it walks offsetParent
  up to that element), but the token layer was being appended to
  viewportEl — a parent further out with `padding: 1em`. Every token
  was visually shifted by ~16 px (one em of viewport padding). Fixed
  by making the token layer a gridContainer child, matching the
  pattern the path / faction / region overlays already use (1.4.0
  fix). Covered by a regression test in the shared frontend-testing
  suite (examples/hex-token-alignment/) — that test fails if the
  layer is ever re-parented to viewport, OR if the viewport padding
  is removed in a way that hides the bug.

Perf micro-opts (initial-render path only, zero behavioural change):
- `paletteByName` Map built once per renderGrid. addHex no longer
  does a linear `palette.find` per hex. Output byte-identical.
- `existingHexPaths` Set built from this map's TFolder.children.
  Per-hex existence check is now O(1) instead of a
  `vault.getAbstractFileByPath` per hex. Output byte-identical.

Intentionally NOT included from the perf-pass-tier-1 branch:
- Debounced overlay cascade (changes overlay-update timing)
- Cached color/style maps (cache-invalidation surface)
- Shared hex-center cache + bakeZoom cache invalidation
- Event-delegation mouseover handler
These items either failed the user-side scroll-feel test or carry
non-trivial invalidation risk; staying out until we can profile in
a live Obsidian session.

Validation:
- 351 unit tests / 18 e2e pass.
- New regression test `frontend-testing/examples/hex-token-alignment/`
  reproduces the bug + verifies the fix:
    ✓ fixed variant: tokens centered (maxAbsDelta=0.59 ≤ 0.6 px)
    ✓ broken variant DOES drift (maxAbsDelta=16.59 ≥ 5 px)
2026-06-21 10:10:37 -04:00
isaprettycoolguy@protonmail.com
18738729ee fix: address obsidianmd reviewer rules (1.4.2)
Five separate categories the upstream obsidianmd reviewer flagged on
the 1.4.x diff, plus enforcement to catch them earlier.

- **no-static-styles-assignment** (5 sites in HexMapView.ts):
  Replace every direct `transform`/`transform-origin`/`opacity` write
  with CSS custom properties on the relevant container. Static rules
  in styles.css now reference them via
  `transform: translate(var(--duckmage-bg-tx, 0px), …) scale(...)`
  with identity-default fallbacks. JS writes only the values via
  `setCssProps({ "--duckmage-bg-tx": ... })`. Applies to the bg image
  layer, the hex grid container, AND the viewport itself. Two small
  helpers `applyBgLayerVars` / `applyGridLayerVars` centralise the
  variable plumbing and replace ad-hoc transform-string assembly at
  the drag / wheel / arrow-key / bake / shiftForGridGrowth call sites.
- **document → activeDocument** (4 sites in overlayPatternControls.ts):
  swap for popout-window compatibility.
- **setDynamicTooltip removal** (5 sites): the API is deprecated; the
  value is shown inline now.
- **Lint enforcement**: `obsidianmd/no-static-styles-assignment`
  promoted from default to error, so a regression breaks the local
  lint instead of waiting for the upstream reviewer. The other two
  patterns aren't shipped as eslint rules upstream — documented in
  CLAUDE.md under "Obsidian reviewer-bot conventions" so the next
  pass-through audits them by hand.

Sandbox regression check (dev/snapshot-bg-hex-alignment.mjs) still
PASSes — the CSS-var refactor doesn't disturb the bg-image / grid
alignment math from 1.4.1.
2026-06-17 18:48:13 -04:00
isaprettycoolguy@protonmail.com
5ce48af60d fix: grid expand/shrink preserves bg image alignment (1.4.1)
Adding or removing a row/column from the top or left of the grid used
to either visually bump the existing hexes (no compensation) or keep
the hexes in place but drift the background image away from them
(pan-only compensation). The fix pairs the pan shift with a matching
shift of the bg image's offsetX/Y in viewport coordinates, so the bg
and the hex grid both stay anchored in screen space after the click.

Also replaces the hex-height-based stride formula with a direct
measurement of the offsetTop/Left delta between two consecutive
rows/cols. The old formula `hex.offsetHeight * 0.75` ignored the
0.15em hex margin and drifted ~5 px per click; the new approach
captures everything that contributes to layout spacing.

Includes a sandbox + headless-Chromium test
(dev/bg-hex-alignment-sandbox.html + snapshot-bg-hex-alignment.mjs)
that verifies the originally-top hex AND a fixed bg-image feature
both stay within 0.6 px of their baseline screen position after one,
three, or repeated top-expand clicks.
2026-06-17 18:18:53 -04:00
isaprettycoolguy@protonmail.com
532df55342 feat: background image + grid calibration mode (1.4.0)
Per-map background image with full interactive calibration — pick or
drag-drop an image from the OS, drop straight into calibration mode,
align the hex grid to features in the image.

Calibration UX:
- Drag image body or grid body to move; corner/edge handles to resize
  (image is aspect-locked; grid has top/bottom edges for hex-aspect
  stretch). Anchor math: the side you grab moves, the opposite stays
  fixed — see topics/design/notes/resize-handle-anchor-rule in the KB
  for the derivation.
- Click an element to "focus" it; arrow keys nudge by 1 px (Shift = 10).
  Sequential nudges coalesce into one undo entry.
- Floating ✓ button bottom-right commits, Esc cancels. Wheel falls
  through to viewport zoom (precision alignment).
- Hex outlines drawn as an SVG overlay during calibration (CSS `border`
  and `drop-shadow` filter approaches both fail on clip-path-shaped
  hexes — the SVG overlay was chosen after sandbox iteration; see
  dev/hex-calibration-sandbox.html).
- Resize handles use `transform: scale(1/zoom)` so they stay constant
  screen size regardless of viewport zoom.
- Full undo/redo for every calibration mutation (drag, resize, nudge);
  Ctrl/Cmd+Z and Ctrl/Cmd+Shift+Z hotkeys wired at view scope.

Persistence + alignment:
- New MapData.savedViewport persists per-map zoom/pan/font-size across
  view close, so calibration values (sized to a specific font-size)
  don't drift when the view reopens.
- bakeZoom now scales calibration values by the bake factor so
  calibrated images stay aligned with the hex grid after wheel zoom.
  Suppressed entirely during calibration (and pending bake timers
  cancelled on entry) so the SVG outline doesn't drift mid-session.
- Path / faction / region overlay SVGs moved inside the gridContainer
  so they inherit the gridDisplay transform — previously they sat as
  siblings of the grid and drew at un-scaled coordinates, producing
  the "two grids" effect on calibrated maps.

Other UX:
- Top/left grid expand buttons now visually anchor — the existing grid
  stays put and the new row/column appears above/left, instead of the
  whole grid shifting down/right.
- New "Background image" section in both the MapModal Properties tab
  and the New map tab (with drop zone).
- Drag-and-drop OS images import to {hexFolder}/{mapName}/_bg/, with
  filename collision auto-rename.
- "No terrain" hexes become transparent when a bg image is set, so the
  image shows through.
- Calibration banner anchored to the view's contentEl (was body) so it
  can't survive across view-close; defensive sweep on open clears any
  banner left by a prior buggy build.

Dev infrastructure:
- dev/hex-calibration-sandbox.html — minimal hex grid that mirrors the
  plugin's CSS, used to iterate on calibration outline / overlay
  alignment approaches outside the live editor.
- dev/screenshot-approaches.mjs + snapshot-calibration-outline.mjs —
  runners against the shared frontend-testing tool that produce one
  PNG per CSS variant for LLM-direct visual review (see the
  frontend-testing-tools KB project).
2026-06-17 17:01:24 -04:00
isaprettycoolguy@protonmail.com
84bcd76bd5 fix: legend swatch shows pattern at readable density (1.3.1)
Bumps the faction-legend swatch and shrinks the pattern tile so the
pattern is legible at swatch-size rather than showing just 1-2 tile
repeats.

- On-screen legend: hex 28 → 32 px, pattern tile scaled by swatch/96
  with a 4 px floor — yields ~6 repeats across the swatch.
- PNG legend: swatch fontSize*1.1 → fontSize*1.7, lineHeight floored
  at swatch+4 so rows don't overlap; pattern shrunk by the same ratio.
- renderHexPreview gains an optional patternScaleMultiplier param so
  callers (legend, palette, future small-swatch contexts) can shrink
  the pattern without touching the user's scale slider value.

Also bundles the prior 1.3.0 follow-up fixes (hex-shaped on-map-scale
preview, palette tile refresh-on-save, thin outline default + slider)
since those were committed locally on top of 1.3.0.
2026-06-17 12:33:00 -04:00
isaprettycoolguy@protonmail.com
06bb93c91d fix: overlay pattern editor — hex preview, palette refresh, thin outline
Three refinements to the 1.3.0 overlay pattern editor based on user
feedback:

- Preview: was a 120×36 rect at the pattern's own scale. Now a hex
  polygon at the live on-map hex size (read from .duckmage-hex,
  falling back to 96 px), in the configured orientation — so the
  preview shows exactly what the overlay will look like on the map.

- Palette tiles: were never updating on Save because the picker
  re-read style from the metadata cache, which lags Obsidian's
  frontmatter write by a tick. The editor's onSaved callback now
  passes the new OverlayStyle directly, so the picker renders from
  the editor's in-memory state and updates immediately.

- Outline width: was hardcoded to gapPx*2+2 (often 14-22 px), which
  made overlay borders read as solid blobs. Now per-overlay,
  defaulting to 1.5 px, with a slider (0-20, step 0.5) in the editor.
  Persisted as faction-outline-width / region-outline-width
  frontmatter keys. Crank it up to recover the old thick-blob look.
2026-06-17 12:22:28 -04:00
isaprettycoolguy@protonmail.com
f9e292c5f0 feat: faction & region overlay patterns (1.3.0)
Adds per-overlay pattern fills so factions and regions can be visually
distinguished when they overlap (issue #25). Each faction note and each
region note now carries a pattern, scale, and opacity in its
frontmatter, edited via the existing FactionEditor / GeoRegionEditor
modals (dropdown + sliders + live preview swatch).

Pattern set (10 total): solid (the existing flat fill, default),
diagonal stripes ↗ / ↖, crosshatch, polka dots, grid, zigzag,
triangles, scales, checker.

On screen: SVG <pattern> defs are deduped per (key,color,scale) and
referenced from each overlay path's fill. Stroke stays solid color so
blob boundaries remain crisp. Per-overlay opacity replaces the
hardcoded 0.45 group opacity.

PNG export: mirrored via Canvas patterns built from an offscreen tile
per (key,color,scale), cached for the render pass.

Backwards compatible — overlays without the new frontmatter keys fall
back to solid + 0.45 opacity, matching pre-1.3.0 behavior.
2026-06-17 11:53:04 -04:00
isaprettycoolguy@protonmail.com
d12cf38ceb fix: pin HexEditorModal to viewport on open (1.2.3)
makeDraggable previously used position: absolute with top/left 50%, which
relies on the nearest positioned ancestor being .modal-container. Any
plugin or theme that establishes a CSS containing block on an ancestor
(via transform/will-change/filter/contain) silently re-roots the modal
against that inner element, so it opens partway off-screen — as
reported in hexmaker#26.

Switch to position: fixed and compute the centered top/left in JS
against window.innerWidth/innerHeight, clamped 8px inside the viewport.
position: fixed bypasses most ancestor-containing-block issues; the JS
clamp catches the remaining edge cases (transformed ancestors that also
hijack fixed positioning).

Drag listeners now bind to modalEl.ownerDocument so dragging works
correctly inside popout windows.
2026-06-17 10:26:30 -04:00
isaprettycoolguy@protonmail.com
ce869324fc feat: coord placement setting + terrain tables in onboarding (1.2.2)
Setup wizard now generates the terrain description/encounter tables and
roller-link preamble as part of Step 3, so freshly onboarded vaults have
the full table scaffolding ready to use.

New "Coordinate placement" setting (top/middle/bottom, default bottom)
controls where the x_y label sits inside each hex, addressing
hexmaker#22 — keeps the label from clashing with a centered terrain
icon.
2026-06-16 10:34:57 -04:00
isaprettycoolguy@protonmail.com
1fa2153bf1 chore: address obsidianmd lint and popout-window compat (1.2.1)
- Sentence-case: rephrase "GM" → "Game master"; remove disable comments;
  autofixer lowercases "🎲 Open tables view" / "← Back" to comply
- no-static-styles-assignment: dynamic colors now use CSS custom properties
  (--duckmage-bg / --duckmage-border-color / --duckmage-fg) consumed by
  existing classes; static positions moved to small modifier classes
  (duckmage-rt-roll-btn-spaced, duckmage-wf-steps-list, etc.); terrain
  toolbar uses is-terrain-preview / is-eyedropper-active toggles;
  createIconEl uses new duckmage-masked-icon class
- Popout-window compat: setTimeout / clearTimeout / requestAnimationFrame
  → window.X; document.X → activeDocument.X across all view/modal files
- Unnecessary `as ArrayBuffer`: replace `data.buffer.slice(...) as ArrayBuffer`
  with `new ArrayBuffer + Uint8Array.set` — gets a real ArrayBuffer without
  the assertion (also avoids ArrayBufferLike narrowing)
- Deprecated SettingTab.display(): extract body into private renderSettings();
  display() and post-action refresh both call it
- eslint.config: add activeDocument + activeWindow to globals so the renamed
  identifiers don't trip no-undef

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-05 23:04:11 -04:00
isaprettycoolguy@protonmail.com
6aa885b5b2 feat: export module — PDF/Markdown for notes, hexes, maps, tables, workflows (1.2.0)
Adds a unified export pipeline under src/export/ with exporters for single notes,
structured hexes, maps (with reference table), random tables, and workflows
(with rolled samples). PDF rendering reuses Obsidian's MarkdownRenderer + theme
CSS; map PNG/PDF goes through a dedicated canvas renderer.

UI surface:
- "Export" tab in MapModal (PNG/PDF with overlay + size options)
- "Export" link in HexEditorModal + HexExportModal
- "Export PDF/Markdown" links in RandomTableView header
- "Export…" button in WorkflowEditorModal + WorkflowExportModal
- Commands: export current note (PDF/MD), current hex, current workflow
- File-menu items on any .md file: Export to PDF / Markdown

Other:
- New `exportFolder` setting (defaults to {worldFolder}/exports)
- hexGeometry: add hexSize / hexCenter / hexPolygonPoints / gridBoundingBox
- HexTableView + HexEditorModal: first-wins on duplicate palette names
  (matches HexMapView's .find() behaviour)
- Tests: unit + integration suites for all exporters; sim-vault fixture

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-05 20:46:41 -04:00
isaprettycoolguy@protonmail.com
0f910233ee 1.1.15
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 09:16:19 -04:00
isaprettycoolguy@protonmail.com
62f65c741f 1.1.14
feat: Ctrl/Cmd+click hex to open submap in new tab

When no drawing tool is active, Ctrl+click (Cmd+click on Mac) on a hex
with a linked submap opens that submap in a new Obsidian tab. Regular
left-click navigation (same tab) is unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 19:59:48 -04:00
isaprettycoolguy@protonmail.com
910d425040 1.1.13
feat: tabbed Maps modal; faction hover tooltip; map starting coordinates; default map/submap sizes in settings

- Maps modal refactored into three tabs: Maps (switch/delete), Properties (rename, terrain theme), New map (labeled fields, size presets)
- Faction overlay hover shows each faction on its own line with color swatch via custom floating tooltip div
- New map and submap creation accept starting X/Y coordinates; hex filenames and labels both reflect the chosen origin (gridOffset initialized accordingly)
- Map switching via modal now goes through switchToMap so viewport save/restore applies correctly
- Added defaultNewMapCols/Rows and defaultSubmapCols/Rows settings; new-map forms pre-fill from these

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 19:43:50 -04:00
isaprettycoolguy@protonmail.com
f825e8a37d 1.1.12 2026-05-26 18:33:27 -04:00
isaprettycoolguy@protonmail.com
64810344cb feat: open submap from context menu; auto-fit grid on first visit; fix back-nav viewport
- Right-click context menu shows "Open submap: <name>" when the hex has
  a duckmage-submap link; clicking calls navigateToMap so Back works.
- switchToMap now saves/restores per-map viewport (zoom, panX, panY,
  fontSize). First visit to a map fits the full grid into view via
  fitGridToView(); subsequent visits restore the last-known position.
- fitGridToView resets the baked font-size before measuring so the
  scale calculation is always relative to the base font size.
- Fix: navigating Back was landing off-map because bakeZoom() encodes
  the visual zoom into viewportEl.style.fontSize and resets this.zoom=1.
  Saving/restoring fontSize alongside zoom/pan prevents the submap's
  baked scale from bleeding into the parent map's viewport.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 18:33:26 -04:00
isaprettycoolguy@protonmail.com
6f9c759850 1.1.11 2026-05-25 16:09:43 -04:00
isaprettycoolguy@protonmail.com
0683363d60 fix: clear duckmage-submap references when a map is deleted
When deleteMap runs, scan all markdown files for duckmage-submap
frontmatter pointing at the deleted map and null them out via
setSubmapInFile. Uses the metadata cache for the filter so no
extra file reads are needed for non-matching files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 16:07:11 -04:00
isaprettycoolguy@protonmail.com
bad258acc9 1.1.10 2026-05-25 15:58:36 -04:00
isaprettycoolguy@protonmail.com
14345a4a42 fix: clickable internal links in token note preview
Wire up delegated click handler on the rendered markdown container so
.internal-link anchors (produced by MarkdownRenderer) open the target
note in a new tab via openLinkText. Previously the links rendered
visually but did nothing on click.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 15:58:22 -04:00
isaprettycoolguy@protonmail.com
41872b28e4 feat: undo/redo for icon, faction, region tools; token stacking offsets
- Undo/redo now covers all paint tool types: terrain (existing), icon
  override, faction link paint/erase, and region paint/erase. Each
  stroke is committed on mouseup using the same Map-per-stroke pattern
  as terrain.
- Fix: undoing an icon override now restores the underlying terrain icon
  (previously left the hex blank).
- Fix: "Remove icon override" context menu item now calls renderGrid so
  the icon disappears immediately without a reload.
- Multiple tokens on the same hex now spread out using preset offset
  patterns (2=left/right, 3=triangle, 4=2×2, 5=pentagon, 6+=ring) so
  all tokens are visible with slight overlap. Spread radius scales with
  hex size.
- Hovered token rises above stacked siblings via z-index on :hover.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 15:55:04 -04:00
isaprettycoolguy@protonmail.com
d2909abdb3 docs: update README for 1.1.9
- "Regions" → "Maps" throughout (map management, data model path)
- add submap linking, map terrain theme, Back button
- add Link submap to drawing tools table with erase-mode note
- update Random Tables panel description (folder tree, filter behaviour)
- update overlay panel: list all toggles; note pencil/layers panel icons
- update source layout (FolderTree, MapModal, SubmapPickerModal, HexSidePanel, etc.)
- tighten Getting Started and Settings table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 15:15:01 -04:00
isaprettycoolguy@protonmail.com
8214294c5e 1.1.9
- folder tree view in link-table modal (collapsed by default, filter expands all)
- shared renderFolderTree utility; RandomTableView refactored to use it
- submap picker: filterable list replaces combo-dropdown; terrain theme swatch per map
- map terrain theme: per-map terrainType field drives submap center-dot color and swatch in switch-map list
- erase mode for paths/factions/regions: "Remove" tile + enters erase immediately
- submap link tool: blip animation on hex after linking
- hex map view header updates to active map name on switch
- fix: link-table modal scopes folder tree to tablesFolder only (was leaking vault-wide folders)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 12:49:41 -04:00
isaprettycoolguy@protonmail.com
244739d951 1.1.8
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:14:48 -04:00
isaprettycoolguy@protonmail.com
8078b37215 feat: right-click context menu for all painter tools; remove map mode button
Single right-click while any drawing tool is active now opens a context
menu with "Switch <toolname>" (where applicable) and "Exit tool", replacing
the old double-right-click-to-exit gesture and the "↩ map mode" toolbar
button that caused layout shift (fixes #17).

New PainterContextMenu class is designed for subclass extension via
extraOptions() — individual tools can inject additional items later.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 20:14:32 -04:00
isaprettycoolguy@protonmail.com
fd91fba26f fix: sync manifest.json and versions.json to 1.1.7 2026-05-23 14:38:09 -04:00
isaprettycoolguy@protonmail.com
a6b18dec9f 1.1.7 2026-05-23 14:30:52 -04:00
isaprettycoolguy@protonmail.com
2f4e368add pin modal button row to bottom via margin-top:auto
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 14:28:27 -04:00
isaprettycoolguy@protonmail.com
937751319d increase min-height of draggable modals to 420px
Prevents MapLinkModal and SubmapPickerModal from being too short to show
the combo dropdown without triggering a scroll.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 14:26:22 -04:00
isaprettycoolguy@protonmail.com
83cd8e2da0 fix combo dropdown scroll: suppress modal-content overflow while open
position:fixed failed because Obsidian's modal CSS animation sets transform on
.modal, making fixed children position relative to it (not the viewport).
Instead, toggle .duckmage-combo-open on .modal-content (overflow-y:visible)
while the dropdown is open so the absolute-positioned dropdown doesn't trigger
the scroll container.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 12:42:04 -04:00
isaprettycoolguy@protonmail.com
ff8479c59c fix combo dropdown: position:fixed escapes modal scroll, blank inputs in map modals
- Combo dropdown now uses position:fixed with JS-calculated viewport coords so it
  never causes the modal's overflow-y:auto container to scroll
- MapLinkModal: map filter and link text both start blank; link text auto-fills
  on map selection only if still empty
- SubmapPickerModal: focus opens all maps (blank query)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 12:35:34 -04:00
isaprettycoolguy@protonmail.com
31d3256192 add submap links: link hex to another map, centre-dot nav, back button, map link insert
- Right-click hex → "Link submap" writes duckmage-submap frontmatter
- Hex flower shows centre dot (hidden when no link) that navigates into the submap
- Centre dot centering is orientation-aware (flat-top top:24px, pointy-top top:20px)
- ← Back button appears next to map dropdown (via flex nav group) after submap drill-in
- "Insert map link" editor context menu inserts obsidian://duckmage-openmap URI
- Map rename updates all duckmage-submap references via updateSubmapReferences()
- HexEditorOptions refactored: onNavigate/onModalClose/onSwitchMap in options object
- 10 new tests for submap frontmatter helpers and rename propagation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 12:29:22 -04:00