Skip to content

Add a Blueprint Info editor: name, icons, description, and grid alignment - #222

Closed
koenigstag wants to merge 18 commits into
FactoryGameFan:wormeyman-space-age-supportfrom
koenigstag:blueprint-info-editor
Closed

Add a Blueprint Info editor: name, icons, description, and grid alignment#222
koenigstag wants to merge 18 commits into
FactoryGameFan:wormeyman-space-age-supportfrom
koenigstag:blueprint-info-editor

Conversation

@koenigstag

Copy link
Copy Markdown
Contributor

Summary

  • New "Blueprint Info" dialog for the blueprint itself (not an entity), opened via a button in the top-left corner next to the FBE logo — the editor had no way to view or change a blueprint's own name/description/icons/grid-alignment before this, only round-tripping them silently on import/export.
  • Name and Description — plain/multiline text fields, routed through history.updateValue so edits are undoable like everything else.
  • Icons — up to 4 slots, left-click opens the icon picker (items/fluids/recipes/virtual signals), right-click clears a slot.
  • Grid alignment — a new section covering snap-to-grid, absolute-snapping and position-relative-to-grid, the three blueprint-string fields the editor already decoded/re-encoded but never exposed:
    • "Snap to grid" checkbox, Grid size (Width/Height), Grid position (X/Y), and an Absolute/Relative choice.
    • The exact serialization rules (when each field is written vs. omitted, and what "enabling from off" defaults to) were verified by decoding several real exported blueprint strings by hand rather than guessed — see commit messages for the specifics. This caught and fixed a real bug where turning snapping back off could leave a stale absolute-snapping: true behind in the exported string.
    • New RadioButton control (circular) for the Absolute/Relative choice, since Checkbox draws a rounded square that reads as an independent toggle rather than one of two options.
entrypoint vertfixfull

Test plan

  • vp check / vp test pass
  • Manually verified end-to-end: dialog opens from the corner button; name/description/icons edit and persist; grid-size/position fields enable/disable correctly across Snap-to-grid/Absolute/Relative combinations; round-tripped several real blueprint strings (Absolute, Relative, grid off) through the app and confirmed the re-exported JSON matches the original byte-for-byte in the relevant keys; undo/redo works throughout.

claude added 7 commits August 12, 2026 08:57
The blueprint's own name, description, and icons had no editing UI at
all: name was a bare mutable field nothing wrote to interactively,
description was explicitly marked "unused" (round-tripped but never
read or written by the app), and icons only had a private auto-generation
path (generateIcons, used by serialize() when none were imported).

Adds:
- Blueprint.name/description as proper get/set pairs routed through
  history (undo/redo support), each boxed the same way scheduleStore
  already is - `history.updateValue` needs `keyof T` on whatever it's
  handed, and `keyof this` never includes a private member.
- Blueprint.getIcon/setIcon, wrapping the existing icons Map with
  history.updateMap. Clearing all four slots by hand is a real choice
  rather than a no-op: it puts the blueprint back into the same "auto"
  state generateIcons uses when nothing was ever set.
- BlueprintInfoEditor, a Dialog (not Editor - there's no Entity here)
  with a name field, a multi-line description field, and four icon
  slots (BlueprintIconSlot, a Slot like every entity editor's icon
  pickers) backed by a new FD.acceptedSignalIcons() - items, fluids,
  and virtual signals.
- BlueprintInfoButton, a persistent corner button that opens it -
  positioned left of EntityInfoPanel's own 270px width so the two,
  both anchored top-right, never overlap.
- TextInput gained optional multiline/height parameters (backing a
  <textarea> instead of an <input>) for the description field - every
  other existing call site is unaffected, since both default off.

InventoryDialog changes, needed by the icon picker rather than by
blueprint info itself - the same computeWidth/scrollbar-anchor/
showRecipePanel changes as the display-panel editor PR, ported here
independently since this branch does not depend on that one:
- computeWidth widens the dialog when a filter populates more group
  tabs than the fixed 404px layout was designed for - the icon
  picker's items+fluids+signals filter is the first caller on this
  branch to hit that, up to 7 tabs against Space Age data.
- The scrollbar thumb was pinned to a fixed VP_X + VP_W offset,
  correct only at the old fixed width; anchored it to `this.width`
  instead.
- A showRecipePanel flag (default on) lets the icon picker skip the
  recipe panel, which would otherwise render as a permanently empty
  bar - none of items/fluids/signals have a recipe.
Right-docked (left of EntityInfoPanel) worked but sat far from
everything else in that corner. Left of the website's own DOM overlay
(FBE logo, Discord/Github buttons) puts it next to the rest of the
persistent chrome instead, and the position no longer depends on
screen width, so the resize listener is gone with it.
New "Snap to grid" section in BlueprintInfoEditor - grid size
(Width/Height), grid position (X/Y), and an Absolute/Relative choice -
covering `snap-to-grid`, `absolute-snapping` and
`position-relative-to-grid`, which the editor already round-tripped on
import/export but never exposed or let anyone edit.

Verified the exact serialization rules by decoding three real
exported blueprint strings by hand (Absolute, Relative with a default
position, and grid off) rather than guessing:
- Absolute writes `absolute-snapping: true`.
- Relative at the default `{0, 0}` position omits both
  `absolute-snapping` and `position-relative-to-grid` entirely -
  `false` and `{0, 0}` are Factorio's own omitted defaults, not
  something to write out.
- Grid off omits all three keys together.
`Blueprint.absoluteSnapping`'s default flips to `false` accordingly,
and `serialize()` now gates `absolute-snapping`/`position-relative-to-grid`
on `snap-to-grid` being set (and the position on being non-zero)
instead of forwarding the boxed stores directly - otherwise turning
snapping back off could leave a stale `absolute-snapping: true` behind
from before it was, which the old direct-forward version would have
done since nothing cleared it.

Also: enabling "Snap to grid" from off always selects Absolute, which
is what the game's own default does (confirmed the same way - a
freshly-enabled-and-exported string always carries `absolute-snapping:
true` rather than omitting it).

New `RadioButton` control (circular, mutually-exclusive-by-convention)
for the Absolute/Relative choice - `Checkbox` draws a rounded square,
which reads as an independent toggle rather than one of two options.

Icons moved above Description in the dialog, per request.
.disabled already stopped typing (the real DOM disabled attribute
underneath), but nothing dimmed them: TextInput's own box config
comments out a 'disabled' style, and .alpha only reaches the box
graphic - _updateDOMInput's opacity line is commented out too, citing
a pixi.js worldAlpha/DOM sync issue - so a disabled field looked
identical to an editable one. setInputStyle('opacity', ...) reaches
the DOM element's own CSS directly instead, which the other two routes
each miss half of.
Grid position's X/Y now stay editable whenever the grid itself is on,
regardless of which of Absolute/Relative is picked - only
Blueprint.serialize cares which one is chosen, so switching between
them and back doesn't lose whatever was typed there. The decorative
Absolute row's X/Y (no Blueprint field backs them - see the comment
where they're built) go the other way: enabled only while grid is on
*and* Absolute is the active choice, disabled otherwise, since there's
no data behind them to preserve across a switch.
actual content width instead of guessed coordinates

Two bugs from the same root cause: Grid size's Width/Height weren't
flush with the dialog's right edge, and Grid position/Absolute's X/Y
ran past it entirely. Both rows used fixed x-coordinates guessed from
a mock-up rather than derived from the dialog's real content width -
336px, the same width BlueprintInfoEditor's Name/Description
TextInputs already use.

COL1_X/COL2_X now anchor the two value columns' input boxes to that
width, and every field label (Width:/Height:/X:/Y:, four of them
across three rows) right-aligns to its own input at a fixed 3px gap
via makeFieldLabel, rather than each sitting at its own guessed x -
which had put "X:"/"Y:" far from their input while "Width:"/"Height:"
sat close to theirs.
…labels

RadioButton draws its own label at local y=0, but every field label
(Width:/Height:/X:/Y:) sits at its row's y+8 - the offset that centres
it against the neighbouring 24px-tall input. Absolute's radio and its
own X:/Y: label shared a row but not that offset, so "Absolute" read
8px higher than the text right next to it. Both radios now position at
+8 too, which also centres their circle against the row's input boxes
better than the unshifted position had.
wormeyman added a commit that referenced this pull request Aug 12, 2026
Two loose ends the previous commit named and left, both now closed.

`deploy` waits on `e2e` as well as `checks`. The class of bug the browser
suite exists to catch is precisely the one that reaches production
otherwise: PR #222 type-checks, lints and passes every unit test while
dropping `position-relative-to-grid` from 325 of the corpus's 367
blueprints, and `checks` was green on it. Gating on `checks` alone leaves
that free to deploy. The price is about five minutes on the deploy's
critical path.

`serve` becomes a declared devDependency instead of something localpreview
fetches with `npx --yes`. Unpinned was tolerable while that command only ran
on a laptop; as a CI dependency it was a package resolved at run time in a
job whose previous step is `vp install --frozen-lockfile`, which is the one
thing that pipeline is meant not to do. It costs ~990 lines of lockfile for
a dev-only static file server and buys a reproducible sprite server plus
Renovate tracking it.

Verified: `npx --no-install serve --version` answers 14.2.6, so nothing is
fetched, and localpreview still brings both ports up - data.json 200 off
8081.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Dbza77QEZnkqt1mGssY71
wormeyman added a commit that referenced this pull request Aug 12, 2026
The 192 browser specs ran nowhere but a maintainer's laptop, and the gap was
not theoretical: PR #222 arrived with every check green and a
blueprint-round-trip.spec.ts failure in it, dropping `position-relative-to-grid`
from every absolutely-snapped blueprint - 325 of the corpus's 367. Neither
`vp check` nor `vp test` can see that, because the decode -> model -> serialize
path needs `FD` loaded, which needs a browser and the sprite server.

Needs no Factorio: the corpus is committed under test-blueprints/ and the
sprite data under packages/exporter/data/output.

Three choices worth stating, since each has a wrong version that looks right:

- Reuses `npm run localpreview` rather than carrying its own copy of how to
  start the two servers. A second copy is a bound written down twice, and the
  one that loses is the one nothing tests.
- The browser cache is keyed on the *resolved* @playwright/test version read
  out of node_modules, not the caret range in package.json. The resolved
  version decides the browser build, so keying on `^1.62.0` serves yesterday's
  browser to today's Playwright - which fails as a missing
  chrome-headless-shell and reads as a suite-wide regression, not a cache miss.
- The server step redirects stdout to a file instead of inheriting it. A
  backgrounded process holding the step's stdout pipe open can stop that step
  from ever completing.

`deploy` waits on `e2e` as well as `checks`, so the #222 failure class cannot
reach fbe.factorygamefan.com. `serve` becomes a declared devDependency rather
than an `npx --yes` fetch - unpinned was fine on a laptop, and is not fine as
the one run-time-resolved package in a job whose previous step is
`vp install --frozen-lockfile`.

Also corrects the eight comments across six files that asserted Playwright does
not run in CI. The reasoning under each survives - a pure unit test still
answers in seconds inside `checks` where this job takes five minutes and two
dev servers - but the flat claim does not, and the split is now about cost
rather than coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Dbza77QEZnkqt1mGssY71
@wormeyman

Copy link
Copy Markdown
Collaborator

I've updated the branch onto current base, which pulls in a CI job that didn't exist when you opened this: the Playwright suite runs on PRs now. It's running above. Locally it comes back 191 of 192, and I think the one failure is real.

blueprint-round-trip.spec.ts:

-   "serializedHash": 825830683,
+   "serializedHash": 120098912,

Everything else in that fixture holds. All the counts, both position checksums. That narrows it to a field vanishing from the serialized JSON with no geometry moving.

The field is position-relative-to-grid. In Blueprint.serialize:

'position-relative-to-grid':
    snapToGrid && !absoluteSnapping && !positionIsDefault
        ? positionRelativeToGrid
        : undefined,

It only writes when snapping is Relative. I decoded the corpus to see how often that branch is taken: of 367 blueprints, 325 carry snapping, and every one of them is absolute-snapping: true. The single blueprint that also carries a grid position, "Biolabs 750 SPM" in test-blueprints/EARN/pocket-base-space-age-v22.1.2.txt at {80, 106}, is absolute as well. So on real data the condition never fires and the value is dropped on export.

Running the same probe against base and against this branch, the count of blueprints that keep a grid position through a round trip goes from 1 to 0.

My guess is that position-relative-to-grid belongs to Absolute rather than Relative. The other half of the PR points the same way: the X/Y boxes on the Absolute row have nothing behind them, as your own comment there says, so anything typed into them is discarded, while the live X/Y sits under Relative. Swapping the two would fix both halves at once. Worth checking against the game before changing anything, though. I could be wrong about which way round it goes, and tools/oracle/ is set up for exactly this kind of question.

One unrelated thing while you're in there: BlueprintInfoButton is added outside the if (!isMobile.any) block in UIContainer, so the edit button appears on mobile, where the editor is meant to be view-only.

The alignment work itself is a real gap filled, and the serialization rules you worked out by decoding real strings are the right way to have got them.

@wormeyman

Copy link
Copy Markdown
Collaborator

I said above that my guess was that position-relative-to-grid belongs to Absolute rather than Relative, and that it was worth checking against the game before changing anything. I've now done that, on 2.0.77: #226.

The guess was right, and here is the table rather than the reasoning:

case set snap set abs set pos wrote snap wrote abs wrote pos
relative-origin 2,2 false 0,0 2,2 - -
relative-offset 2,2 false 3,5 2,2 - -
absolute-origin 2,2 true 0,0 2,2 true -
absolute-offset 2,2 true 3,5 2,2 true 3,5

Set a grid position in relative mode and the game writes no position key at all. Set the same one in absolute mode and it writes it. So the condition wants to be snapToGrid && absoluteSnapping && !positionIsDefault, and the {0, 0} rows say the non-default check is right to be there: the game omits the key at the origin in both modes.

I put both candidate rules into the probe and scored them against every measured row before writing any of this down. The inversion agrees on all seven rows that have a grid; the current condition disagrees on three.

Two things from the probe worth having if you go near this.

Setting blueprint_position_relative_to_grid turns blueprint_absolute_snapping on. My first run set snap, then absolute, then position, so the position write flipped absolute back on afterwards and every row exported as absolute. Relative mode was never reached and the relative rows measured nothing while looking entirely reasonable. If you probe this yourself, set position before absolute.

And a grid position with no snap-to-grid is a state the GUI cannot reach, which the game handles inconsistently: it writes keys with no grid beside them and does not reconstruct them on import. I recorded those rows but did not score them. Worth knowing because your refreshEnabled already disables the position fields when the grid is off, which is the right call for a reason the game agrees with.

The rest of the review stands: the Absolute row's X/Y still needs backing by the real field rather than the placeholder, and BlueprintInfoButton is still outside the isMobile gate.

wormeyman added a commit that referenced this pull request Aug 13, 2026
Nothing pinned this. `blueprint-round-trip.spec.ts` caught #222 dropping
`position-relative-to-grid`, but only by accident of the corpus: exactly one of
its 367 blueprints carries a grid position, so re-capturing that fixture or
swapping the corpus the way #186 did would take the coverage with it. And what
it reports is a moved hash rather than a named field.

The rule asserted here is the measured one from
tools/oracle/fixtures/blueprint-snapping.json: the game writes
`position-relative-to-grid` under absolute snapping and omits it under
relative.

Synthetic because it has to be. All 325 corpus blueprints that carry snapping
are absolute, so a test built from real exports cannot tell the two modes
apart - the same reason the corpus could not answer the question and the
binary had to.

Mutation-checked with PR #222's own condition pasted into
`Blueprint.serialize`: two of the five fail and three pass, so the headline
test is not decoration and the origin test is doing work of its own. The
no-snapping test is a control that survives that mutation and would catch the
opposite mistake, a default grid written onto every blueprint.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Dbza77QEZnkqt1mGssY71
wormeyman added a commit that referenced this pull request Aug 13, 2026
#226)

PR #222 rewrites Blueprint.serialize to write `position-relative-to-grid` only
under Relative snapping, which drops it from every real blueprint. The corpus
can say the two co-occur - 325 of 367 carry snapping and all 325 are absolute
- but it cannot say one requires the other, because no corpus blueprint uses
relative snapping at all. That is the shape of argument #133 item 5 and #142
both got wrong, so this asks the binary.

Measured on 2.0.77, the version the editor targets: the game writes
`position-relative-to-grid` under ABSOLUTE and omits it under RELATIVE. Set a
position of {3,5} in relative mode and the game writes no position key; set the
same in absolute mode and it writes it. Both candidate rules were transcribed
into the probe and scored against every measured row before any editor code was
touched - the inversion agrees on all seven rows with a grid, PR #222's rule
disagrees on three.

The trap, and why the probe carries a per-setter trace: setting
`blueprint_position_relative_to_grid` turns `blueprint_absolute_snapping` ON.
The first run set snap, then absolute, then position, so the position write
flipped absolute back on and relative mode was never reached. The readback
control caught that the instrument was broken; only the trace said which setter
did it.

Three no-grid rows are recorded and deliberately not scored: a grid position
with no `snap-to-grid` is a state the GUI cannot reach, and the game answers it
inconsistently. #133 item 4's lesson.

Also adds tests/blueprint-snapping.spec.ts, because nothing pinned this.
blueprint-round-trip.spec.ts caught #222 only by accident of the corpus -
exactly one of its 367 blueprints carries a grid position - and reports a moved
hash rather than a named field. Synthetic, since all 325 snapping blueprints are
absolute and real exports cannot tell the modes apart. Mutation-checked with
#222's own condition pasted into serialize: two of the five fail, three pass.

No editor behaviour changes. The existing pass-through is correct.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Dbza77QEZnkqt1mGssY71
@wormeyman wormeyman reopened this Aug 13, 2026
@wormeyman

Copy link
Copy Markdown
Collaborator

Reopened. I closed this yesterday while merging #226, and there was no decision behind it. Sorry for the noise.

Nothing has changed from the two comments above. The serialize condition still wants inverting to snapToGrid && absoluteSnapping && !positionIsDefault, and BlueprintInfoButton still wants moving inside the if (!isMobile.any) block in UIContainer.

What has changed is that #226 is merged, so tools/oracle/fixtures/blueprint-snapping.json and tests/blueprint-snapping.spec.ts are on the base branch now. Merge base in and two of that spec's five tests fail against the current condition and pass against the inverted one, so you can check the change locally without going near the game.

The alignment section is a real gap and I want it in.

@wormeyman

Copy link
Copy Markdown
Collaborator

For whatever agent picks this up, here is the same thing in a form you can act on directly. Merge wormeyman-space-age-support in first: the fixture and spec named below landed after this PR was opened.

1. Invert the serialize condition

packages/editor/src/core/Blueprint.ts, in serialize():

// current
'position-relative-to-grid':
    snapToGrid && !absoluteSnapping && !positionIsDefault
        ? positionRelativeToGrid
        : undefined,

// wanted
'position-relative-to-grid':
    snapToGrid && absoluteSnapping && !positionIsDefault
        ? positionRelativeToGrid
        : undefined,

This was measured against Factorio 2.0.77, not reasoned about. The game writes position-relative-to-grid under absolute snapping and omits it under relative. The table is tools/oracle/fixtures/blueprint-snapping.json and the probe that produced it is tools/oracle/probe-blueprint-snapping.mjs. Do not edit either one to make something pass. If the fixture and the code disagree, the code is wrong.

The !positionIsDefault half stays as it is. The game omits the key at {0, 0} in both modes.

2. The UI half follows from the same rule

packages/editor/src/UI/BlueprintAlignment.ts. refreshEnabled currently enables the live X/Y whenever the grid is on, with a comment saying only serialization cares which mode is chosen. Under the measured rule that is now a way to lose a value: a position typed while Relative is selected never reaches the export. The two boxes on the Absolute row, m_AbsoluteXInput and m_AbsoluteYInput, have no Blueprint field behind them, and Absolute is the mode that carries the real position, so those and the live pair want reconciling into one set of inputs.

How that is presented is open. What is not open is that a number the user can type has to reach the exported string or be visibly unavailable.

3. Mobile guard

packages/editor/src/UI/UIContainer.ts. blueprintInfoButton is passed to the unconditional addChild call at the top of the constructor, so the button appears on mobile, where the editor is view-only. Move it into the if (!isMobile.any) block just below, which already guards quickbarPanel and wiresPanel.

Verify before pushing

vp check
vp test
npm run localpreview   # repo root, separate terminal, leave it running
npx playwright test tests/blueprint-snapping.spec.ts tests/blueprint-round-trip.spec.ts

Both dev servers have to be up for the Playwright half, which is what localpreview starts. blueprint-snapping.spec.ts has five tests and two of them fail against the current condition. blueprint-round-trip.spec.ts checks a committed fixture and should pass untouched. If it does not, do not refresh that fixture. A moved hash there means real blueprint output changed and the change is the thing to look at.

claude added 3 commits August 13, 2026 06:39
position-relative-to-grid belongs to Absolute snapping, not Relative -
measured against Factorio 2.0.77 (issue teoxoy#226,
tools/oracle/fixtures/blueprint-snapping.json), reversing the guess
the original comment made from three hand-decoded strings. Inverts
the condition in Blueprint.serialize() and reconciles
BlueprintAlignment's two X/Y pairs into the one backed by the real
field, enabled only under Absolute.

Also fixes positionIsDefault, which conflated an explicit {0, 0} with
an absent position and stripped both - tests/blueprint-snapping.spec.ts
pins that an explicit {0, 0} passes through unchanged, so this now
checks only for undefined.

And moves BlueprintInfoButton inside the `if (!isMobile.any)` guard in
UIContainer, where it belongs alongside quickbarPanel/wiresPanel - the
editor is view-only on mobile.

Merges wormeyman-space-age-support in first, which is where
blueprint-snapping.spec.ts and its fixture/probe landed.
@koenigstag

Copy link
Copy Markdown
Contributor Author

Fixed the places you've mentioned.

Inverted the serialize condition to Absolute (measured against 2.0.77, issue #226), reconciled the grid-position X/Y inputs into the one field it actually backs, moved BlueprintInfoButton inside the mobile guard, and fixed positionIsDefault stripping an explicit {0, 0} that blueprint-snapping.spec.ts expects to pass through — all three review items plus what the test caught, vp check/vp test and both named specs green.

claude added 2 commits August 13, 2026 08:17
…Button

The only way to switch entries in a loaded blueprint book was a bare
"BP Book Index" number field in the settings pane, with no names,
icons, or indication of nesting. BookDialog walks the book's raw
entries directly (not the flattened index space Book.selectBlueprint
reads) and renders them as an always-expanded tree - nested books get
a header row and indented children, planners get a dimmed placeholder
row, and a depth guard of 10 stops runaway recursion on a malformed
book. Rows scroll with the same mask+wheel+thumb pattern
InventoryDialog already uses.

BookButton opens it, sitting one slot pitch right of
BlueprintInfoButton and visible only while a book is loaded, rather
than living in ToolsPanel (teoxoy#221) - this feature has no real dependency
on that PR's work, so it gets its own minimal QuickActions
(getCurrentBook/selectBookEntry) instead of pulling in the larger
interface teoxoy#221 introduces. Editor.init takes a single options object
for the same reason teoxoy#221 landed on that shape: a required
quickActions and an optional logger don't have to fight over
positional order.

Also collapses three copies of the same index-switching logic (the
settings pane callback, testApi.selectBookIndex, and now
QuickActions.selectBookEntry) into one function.
@wormeyman

Copy link
Copy Markdown
Collaborator

Re-reviewed against the base branch as it stands now. All four items are fixed, and I checked each one against the code rather than taking the summary.

The one worth calling out is the positionIsDefault to positionIsUnset change, because you went against what I wrote and you were right to. My comment said the !positionIsDefault half stays as it is. But tests/blueprint-snapping.spec.ts landed after I wrote that, and its fifth test pins that an explicit {0, 0} passes straight through. Base behaviour was verbatim pass-through, so keeping !positionIsDefault would have failed a test I had just merged. The comment you left at that site says so clearly. Good catch.

Two things left.

absolute-snapping stopped being pass-through and nothing pins it

'absolute-snapping': snapToGrid && absoluteSnapping ? true : undefined,

Base forwarded that field as it arrived. Now a blueprint carrying absolute-snapping: true with no snap-to-grid loses the key on export.

I think the behaviour is right. tools/oracle/fixtures/blueprint-snapping.json has that exact state in its no-grid-absolute-set row, and its roundTrip.absolute comes back false, so the game does not reconstruct it on import either. Your reasoning in the comment above it is sound too: with the UI able to turn snapping off, the boxed store keeps a stale true that has to be gated somewhere.

What bothers me is that it sits two lines from a comment arguing the opposite principle for the position field, that this layer round-trips what it was given, and nothing tests it. No corpus blueprint is in that state, which is why the round-trip hash did not move. A sixth test in blueprint-snapping.spec.ts would close it, and the fixture already has the row to write it from.

acceptedSignalIcons() already exists

Entity.acceptedDisplayPanelIcons at Entity.ts:1721-1726 is the same three lines. factorioData.ts is the better home, since the function reads nothing but FD and has no entity in it. The miss is that the original site was left alone, so the list is now written down twice. Point acceptedDisplayPanelIcons at the new function.

What I checked and found clean

The setters now route through History, so I looked at the load path. this.name = data.label runs in the constructor and this.description = data.description runs just above this.history.reset(), so neither leaves a stray undo entry for a user's first Ctrl+Z after a load. The three snapping fields write their stores directly and skip history entirely, which is right.

RadioButton copies Checkbox's redraw pattern, including replacing both graphics without destroying the old ones. That matches what is already there, so I am not asking you to change it.

Tests

There are none, and this PR adds six Blueprint setters, a new control, two new dialogs and a TextInput mode. The part I would most want covered is undo. snapToGrid = undefined for turning snapping off goes through History.updateValue with undefined as a real value meaning "delete this key", and nothing exercises that path.

The alignment section is the gap I wanted filled and I still want this in.

claude and others added 2 commits August 15, 2026 12:45
…o-grid edge case

Entity.acceptedDisplayPanelIcons duplicated acceptedSignalIcons() in
factorioData.ts verbatim; it now calls that function instead of
recomputing the same list.

Also adds a sixth test to blueprint-snapping.spec.ts pinning that
absolute-snapping/position-relative-to-grid are stripped on serialize
when there is no snap-to-grid to be relative to - a state the in-game
GUI cannot reach, and one the game's own measured fixture shows it
cannot reliably round-trip either.
TextInput is the one control here not drawn with pixi: it appends an <input>
to document.body and keeps it positioned over the canvas, so nothing pixi
draws on top can occlude it. Opening the icon picker over Blueprint Info left
six fields showing through it - the name box printing "Blueprint" across the
picker's slot row, and five more invisible only because their background is
none, all still answering elementFromPoint and so eating the clicks aimed at
the slots underneath.

Dialog now hides the DOM fields of every dialog that is not the topmost, and
gives them back when the one above closes. Topmost rather than "not
overlapped" because occlusion is not a question the DOM can answer for canvas
content - there is no partial hiding to be had, so the dialog stack decides
it. The walk is recursive since BlueprintAlignment's inputs sit in a nested
container.

TextInput._dom_visible already existed with no way to set it; domVisible
applies the change immediately rather than waiting for a render, because
_needsUpdate watches only the transform and the canvas rect and a field that
never moves again would keep its old visibility forever.

Covered in tests/text-input.spec.ts, which is the only spec that asserts on
real DOM. Mutation-checked: dropping the call from the constructor fails only
the hidden assertion, dropping it from close() only the restored one. Its
reads sync on a frame rather than a timeout - _onAdded hides the element and
only onRender puts it back, which made a first draft fail 1 run in 6, and an
unrendered frame would satisfy the hidden assertion for the wrong reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
claude added 2 commits August 15, 2026 14:38
A real game screenshot showed two separate X/Y pairs in the grid
alignment section - "Grid position" above the Absolute/Relative choice,
and a second pair beside "Absolute" itself - where the editor only had
one. Decoding blueprint strings exported at each step of a manual game
test (not reasoning about it) settled what each one actually does:
typing into "Grid position" writes no blueprint field at all, neither
snap-to-grid, absolute-snapping nor position-relative-to-grid. It moves
every entity's own position by the negation of what was typed, baked
directly into the exported entity coordinates, then resets to 0.
"Absolute"'s own X/Y is the one that round-trips as
position-relative-to-grid.

Entity.forceMoveBy bypasses position's collision/wire-reach checks,
which compare a moving entity against every other entity's current
position - unsafe mid-shift, where that's a mix of already-moved and
not-yet-moved entities, even though a uniform translation can't
actually introduce a collision or break a wire's reach.
Blueprint.translateEntities wraps every entity's forceMoveBy in one
undo transaction. BlueprintAlignment's layout now mirrors the game:
Grid position gated only by Snap to grid, Absolute's X/Y on its own row
gated by Snap to grid and Absolute being selected.
TextInput's 'changed' event fires on every keystroke (the DOM 'input'
event), not on blur. commitGridPositionNudge() resets both fields' text
back to '0' on every commit, so wiring it to 'changed' meant the field
wiped itself back to '0' after the very first character typed - it
looked like the box refused to hold more than one digit.

TextInput._onBlurred already called _setState('DEFAULT') but had its
emit('blur') commented out; uncommented it and moved Grid position's
commit there instead; blur fires once per edit rather than once per
keystroke. The other fields in BlueprintAlignment stay on 'changed' -
their commits re-write the blueprint with the current full text rather
than clearing anything, so repeating them per keystroke is harmless.

Updates text-input.spec.ts's DOM field count/visibility assertions,
which pinned six TextInputs in Blueprint Info before Grid position
added two more.
@wormeyman

Copy link
Copy Markdown
Collaborator

Re-reviewed at 2497d246. The four items from last time are all fixed, and I checked each against the code rather than the summary. acceptedSignalIcons() is shared now and acceptedDisplayPanelIcons points at it. The sixth blueprint-snapping.spec.ts test is in, and it pins the absolute-snapping gating I asked about.

I drove this one in a browser instead of only reading it, because the new "Grid position as a real entity shift" work is the kind of change a green suite cannot speak for. Both dev servers up, throwaway probe, numbers printed. The first five items below are all measured that way rather than reasoned about, and the first two are why I am not merging yet.

1. "Grid position" does not change the exported blueprint at all

Two chests, one nudge of (-3, -4):

model before : #1(-3.5,-3.5)  #2(4.5,4.5)
model after  : #1(-6.5,-7.5)  #2(1.5,0.5)

export before: #1(-4.5,-4.5)  #2(3.5,3.5)
export after : #1(-4.5,-4.5)  #2(3.5,3.5)

The model moves. The exported coordinates do not move at all.

serialize() re-centres every position through getCenter(), which is Math.round((minX + maxX) / 2). Shift every entity by a whole number k and minX, maxX and the centre all shift by k as well, since Math.round(v + k) === Math.round(v) + k for whole k. Then e.position.x -= center.x takes the same k straight back out. The firstRailPos parity bump survives it too, because both halves of firstRailPos.x - center.x move together. And k really is always whole here: parseGridValue is parseInt, and the restriction is /^-?\d*$/. So the cancelling is exact, not approximate.

tests/blueprint-grid-position.spec.ts cannot see this. Test 1 reads model positions, test 2 reads the three snapping keys, test 3 reads undo. None of them reads an exported entity coordinate.

2. On a blueprint with tiles, the nudge slides entities off the floor

translateEntities walks this.entities and never this.tiles. getCenter() averages both. So the two disagree, and this is the one case where the nudge does reach the export.

Two chests plus two concrete tiles, nudge of x = 3:

export before: entities #1(-4.5,-4.5) #2(3.5,3.5)   tiles (-5,-5) (3,3)
export after : entities #1(-5.5,-4.5) #2(2.5,3.5)   tiles (-3,-5) (5,3)

Entities moved one tile left. Tiles moved two tiles right. Re-import that and the machines stand three tiles away from the concrete they were built on.

So the feature does nothing on a blueprint with no tiles, and damages one that has them. I think that points at something structural: a grid nudge cannot be an entity translation while serialize() owns the centring, because the centring will always take the translation back out. Worth settling before more code goes on top.

3. Opening Blueprint Info crashes on five of the twelve corpus files

BlueprintIconSlot.updateContent calls F.CreateIcon(name) with nothing catching it. CreateIcon throws for any name that is not an item, fluid, recipe, signal or inventory group. Planet names are none of those.

Measured against packages/exporter/data/output/data.json and the committed corpus: 17 blueprint-level icon references use vulcanus, fulgora, gleba or nauvis, spread over five files. JEPAKAZOL/vulcanus-starter-mk2.txt is a book whose five blueprints each carry one.

Loading that file and clicking the corner button:

Error: No item, fluid, recipe, signal or inventory group named vulcanus
    at CreateIcon (functions.ts:147)
    at BlueprintIconSlot.updateContent (BlueprintInfoEditor.ts:35)
    at new BlueprintIconSlot (BlueprintInfoEditor.ts:22)
    at new BlueprintInfoEditor (BlueprintInfoEditor.ts:79)

The dialog never opens, and it throws again on every later click.

There is a second effect worth knowing about, because it outlives the click. Dialog's constructor runs Dialog.s_openDialogs.push(this) at line 37, before the subclass body throws, so each failed open leaves an entry behind that nothing removes. openDialogCount reads dialogsContainer.children.length, so the test hook cannot see it, but Dialog.anyOpen() can. Measured with a control:

press E openDialogCount
clean load inventory opens 1
after one failed open nothing happens 0
same page, press E again inventory opens 1

So the keypress after the crash is swallowed. The control opens the inventory on the first press, which is what makes the treatment mean something.

BookDialog in #227 already wrote the guard this wants, as tryCreateIcon. Worth lifting it somewhere both can reach rather than writing it twice.

4. A negative Absolute X or Y cannot be typed, and the wrong sign is exported

Snapping on, no position-relative-to-grid on the blueprint. Typing character by character into the Absolute X box:

type "-"  ->  box reads "0"
type "5"  ->  box reads "5"     (wanted "-5")
type "-"  ->  box reads "0"
type "7"  ->  box reads "7"     (wanted "-7")

exported position-relative-to-grid: {"x":7,"y":0}

It is not a first-try problem that clears up on a retry. It never works, and the exported value carries the opposite sign to the one the user asked for.

The loop is: m_XInput.on('changed') fires on each keystroke, commitPosition() reads parseGridValue('-') as 0, the setter sees a real change and emits, the listener at line 228 calls refreshFromBlueprint(), and that writes .text = '0' back into the box being typed in.

The comment at BlueprintAlignment.ts:197-204 explains why the other fields are safe on changed: "their commits re-write the blueprint with the current full text rather than clearing anything, so an intermediate keystroke is harmless to repeat." That reasoning is the part that does not hold. The commit does not clear the field, but the emit it causes comes back round and overwrites it.

5. Clearing the Grid Width box exports a grid the game will not take

select all in Width, press Backspace  ->  box reads "0"
exported snap-to-grid: {"x":0,"y":4}

parseGridValue('') is 0 and serialize() forwards snap-to-grid as it stands. Same loop as item 4, and the same reason the box cannot be left empty for even one keystroke. There is no upper bound either, so 9999 goes straight through.

Smaller things

Control+A in the new spec. blueprint-grid-position.spec.ts:97 uses page.keyboard.press('Control+A'). #207 already fixed exactly this in display-panel-editor.spec.ts:165, with a comment on the line above: on macOS Control+A is the emacs "beginning of line" binding, not select all. Nothing gets selected, the typed value lands next to whatever was there, and the assertion fails. It is green on the Linux CI shards and red on my machine, which is the same asymmetry #207 records.

Every field is on changed, so every keystroke is an undo step. Name, Description, Width, Height and both Absolute boxes. Typing a 20-character name puts 20 entries on the history stack, and one Ctrl+Z takes back one character. You found this shape for Grid position in the last commit and moved it to blur. The other five want the same treatment.

commitGridPositionNudge on blur has its own trap. Opening the icon picker builds a Dialog, whose constructor calls updateDOMInputVisibility(), which sets display: none on Blueprint Info's fields. The browser blurs a focused field when it is hidden, so the pending nudge commits. I did not measure this one, so treat it as a thing to check rather than a finding.

The class doc has the ordering backwards. BlueprintInfoEditor.ts:72-73 says Editor.loadBlueprint calls Dialog.closeAll() before swapping G.bp. Editor.ts does G.bp = bp at 328 and Dialog.closeAll() at 332, with new BlueprintContainer and initBP() in between. It is harmless today because nothing in that gap touches the outgoing blueprint, but that is a weaker promise than the one written down, and the comment is what the next person will read.

RadioButton duplicates Checkbox. Same field triple, same constructor shape, same fill expression, same cacheAsTexture tail, same remove-and-redraw setter. Its checked setter also drops the old graphics without destroying them, and refreshFromBlueprint() runs on every keystroke, so the orphans accumulate. There is no e.button === 0 guard either, so a right click selects the option, which is the opposite of what right click means everywhere else in this dialog.

text-input.spec.ts:279 hardcodes page.mouse.click(170, 24) for the corner button, while openBlueprintInfoEditor exists in the very same PR so that the coordinate lives in one place, and blueprint-grid-position.spec.ts uses it.

Where this leaves it

The name, icons and description half is good work and I want it. The snapping half is right too, and the fix you made against my advice on positionIsDefault is still the best catch in this PR.

Items 1 and 2 are the blockers. Item 3 is a crash on real files people will load. Items 4 and 5 are a single mechanism, the commit-and-refresh loop, and closing that closes both.

One thing I would raise before the code changes: item 1 suggests the game's own "Grid position" may not be an entity translation at all. What the probe in #226 measured is which snapping mode carries position-relative-to-grid, not what the second X/Y pair does. tools/oracle/ can answer that directly, and the answer decides whether item 1 wants a fix or a different feature.

@wormeyman

Copy link
Copy Markdown
Collaborator

I said in my last comment that the premise under "Grid position" was worth asking the game about before changing any code. I have now done that, on 2.0.77, the same binary #226 was measured on.

The premise is refuted. Setting a grid position does not move entities.

Here is the table rather than the reasoning. Two chests at (0.5, 0.5) and (8.5, 8.5), snapping on, absolute:

grid position set model after exported round-tripped position-relative-to-grid written
none #1(0.5,0.5) #2(8.5,8.5) same same absent
{0, 0} #1(0.5,0.5) #2(8.5,8.5) same same absent
{3, 5} #1(0.5,0.5) #2(8.5,8.5) same same {3, 5}
{10, -7} #1(0.5,0.5) #2(8.5,8.5) same same {10, -7}

Nothing moves. Not in get_blueprint_entities(), not in the exported string, not after importing that string back into a second stack. The game writes the key and leaves every coordinate exactly where it was.

So there is no second thing to model. position-relative-to-grid is the whole feature, #226 already measured everything there is to know about it, and the Absolute X/Y pair you already wired up is the correct and complete implementation. translateEntities and Entity.forceMoveBy are solving a problem the game does not have.

The probe is tools/oracle/probe-blueprint-grid-position.mjs and the table is tools/oracle/fixtures/blueprint-grid-position.json. Do not edit either to make something pass. Two captures in a row are byte-identical, so if a third disagrees, that is a finding.

The control is the part worth reading

A result like this is mostly zeros, and zeros are exactly what a broken probe produces. So there is a positive control: a shifted-entities case that places the same two chests three tiles left and four up through set_blueprint_entities, touching no snapping property at all. Its exported coordinates have to differ from the baseline, and they do:

baseline         #1(0.5,0.5)   #2(8.5,8.5)
shifted-entities #1(-2.5,-3.5) #2(5.5,4.5)

Without that, "the coordinates never changed" would read the same whether the finding is real or the probe is comparing a value with itself. That is the #133 item 4 lesson in its null-result form, and it is the only reason the zeros above are evidence.

One new thing about the API, which confirms #226 from the other end

#226 found that setting blueprint_position_relative_to_grid turns blueprint_absolute_snapping on, and warned to set position before absolute.

This probe found the reverse: setting blueprint_absolute_snapping = false clears the position. The readback control caught it, on the one relative-mode row in the sweep, which came back holding no position at all.

That is worth knowing for two reasons. It means a relative row cannot be scored here, because it holds no grid position by the time anything is read, so asking whether its grid position moved the entities is a question about a value that is not there. Those rows are recorded and left unscored rather than counted as "did not move". And it is independent confirmation of #226's result from the opposite direction: relative snapping does not carry a position because the game takes the position away, not merely because the exporter drops it.

What this means for the PR

Items 1 and 2 in my last comment are not bugs to fix. They are a feature to remove.

Deleting the "Grid position" row, Blueprint.translateEntities, Entity.forceMoveBy and tests/blueprint-grid-position.spec.ts takes both blockers with them, and it takes the tiles problem with them too, since nothing translates entities any more. What is left is the name, icons, description and the alignment section built on Absolute's own X/Y, which is the part I wanted and which is already right.

That leaves the crash on planet icons, and the commit-and-refresh loop behind the negative-value and zero-width bugs. Those are real and still want fixing. They are also much smaller than what just went away.

Sorry for the round trip on this one. The screenshot reading was a reasonable inference and I did not question it either until the export numbers came back identical.

wormeyman added a commit that referenced this pull request Aug 17, 2026
…d pin it (#230)

PR #222 adds a second X/Y pair to the alignment dialog, "Grid position", on
the premise that the game's own control writes no blueprint field and instead
moves every entity by the negation of what is typed. That premise was inferred
from a screenshot of the game's dialog, not measured, and it is why
Blueprint.translateEntities and Entity.forceMoveBy exist.

Measured on 2.0.77, the binary #226 used. Setting a grid position moves no
entity: not in get_blueprint_entities(), not in the exported string, not after
importing that string back. The game writes position-relative-to-grid and
leaves every coordinate where it was, over positions of {3, 5} and {10, -7}.
So there is no second thing to model, and #226 already measured all of it.

The positive control is what makes a null result mean anything. Four of the
seven cases report "nothing changed" whether the probe is right or comparing a
value with itself, so shifted-entities places the same two chests three tiles
left and four up through set_blueprint_entities, touching no snapping
property. Its exported coordinates differ from the baseline, which is the only
reason the zeros count as evidence. The #133 item 4 lesson in null-result form.

One new API fact, which confirms #226 from the other end. That probe found
that setting blueprint_position_relative_to_grid turns
blueprint_absolute_snapping on. This one found the reverse: setting
blueprint_absolute_snapping to false clears the position. The readback control
caught it on the one relative row, which holds no position by the time
anything is read, so that row is recorded and left unscored rather than
counted as "did not move". Relative snapping cannot carry a position because
the game takes it away, not only because the exporter drops it.

Two consecutive captures are byte-identical, so a third that disagrees is a
finding rather than noise. Also adds the probe-blueprint-snapping.mjs row that
#226 left out of the script table.


Claude-Session: https://claude.ai/code/session_01DxZFeyzpXmxjwVHXxDo8zb

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
wormeyman added a commit that referenced this pull request Aug 17, 2026
A vitest text scan over tests/*.spec.ts for `Control+A` and its four
siblings. On macOS `Control+A` is the emacs "beginning of line" binding,
not select-all, so nothing is selected, the typed text lands beside the
old text, and the assertion reads "New textOld text". That is #197, fixed
in 402fbe3.

It reads source rather than driving a browser because the bug **passes on
Linux**, and every runner this project has is ubuntu-latest - `vp test`
and all four `e2e` shards. #223 and #225 put Playwright in CI, which does
not help here: a sharded browser suite gating every PR will go green on
this forever.

The rule is not "never write Control". A chord reaching the app through
actions.ts stays Control, where ModifierKey is Control | Shift | Alt and
no Meta binding exists; a chord reaching a focused DOM <input> wants
ControlOrMeta. A regex cannot separate them, so it flags both and the
ALLOWLIST carries the distinction in writing - one entry today.

Mutation-checked against the live instance: dropping PR #222's
blueprint-grid-position.spec.ts into tests/ fails the guard naming
line 97, and removing it goes green again.

The knowledge already existed in prose. After 402fbe3 there is a
five-line comment at display-panel-editor.spec.ts:159 explaining the
whole trap, one line above its own corrected call, and a contributor then
wrote `Control+A` in a different file. A comment at the scene of the last
occurrence does not reach the next one.

Every figure in the new file's header is measured rather than recalled,
which caught two: the display panel comment is five lines and not three,
and four specs hold `keyboard.down('Control')` for the ctrl-drag gesture
and not five. Both came from #208's own write-up.

Part 1 of #208. Deliberately not `Closes`: that issue's part 2, the
display panel editor's uncovered surface (the alt-mode checkbox, the icon
picker and the connected branch), is untouched here and is real work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mg2oe7JgL3xEzSoN5emTsd
wormeyman added a commit that referenced this pull request Aug 17, 2026
A vitest text scan over tests/*.spec.ts for `Control+A` and its four
siblings. On macOS `Control+A` is the emacs "beginning of line" binding,
not select-all, so nothing is selected, the typed text lands beside the
old text, and the assertion reads "New textOld text". That is #197, fixed
in 402fbe3.

It reads source rather than driving a browser because the bug **passes on
Linux**, and every runner this project has is ubuntu-latest - `vp test`
and all four `e2e` shards. #223 and #225 put Playwright in CI, which does
not help here: a sharded browser suite gating every PR will go green on
this forever.

The rule is not "never write Control". A chord reaching the app through
actions.ts stays Control, where ModifierKey is Control | Shift | Alt and
no Meta binding exists; a chord reaching a focused DOM <input> wants
ControlOrMeta. A regex cannot separate them, so it flags both and the
ALLOWLIST carries the distinction in writing - one entry today.

Mutation-checked against the live instance: dropping PR #222's
blueprint-grid-position.spec.ts into tests/ fails the guard naming
line 97, and removing it goes green again.

The knowledge already existed in prose. After 402fbe3 there is a
five-line comment at display-panel-editor.spec.ts:159 explaining the
whole trap, one line above its own corrected call, and a contributor then
wrote `Control+A` in a different file. A comment at the scene of the last
occurrence does not reach the next one.

Every figure in the new file's header is measured rather than recalled,
which caught two: the display panel comment is five lines and not three,
and four specs hold `keyboard.down('Control')` for the ctrl-drag gesture
and not five. Both came from #208's own write-up.

Part 1 of #208. Deliberately not `Closes`: that issue's part 2, the
display panel editor's uncovered surface (the alt-mode checkbox, the icon
picker and the connected branch), is untouched here and is real work.


Claude-Session: https://claude.ai/code/session_01Mg2oe7JgL3xEzSoN5emTsd

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@wormeyman

Copy link
Copy Markdown
Collaborator

One line in this PR is now an enforced check rather than a review note, so here is the heads-up before you hit it.

#233 merged as 7d2f3d33. It adds tests/spec-modifier-keys.test.ts, a text scan over the specs that runs under vp test inside checks, in milliseconds. When you next merge base into this branch, blueprint-grid-position.spec.ts:97 fails it. I ran the merged guard against your whole tests/ directory to be sure of the scope, and that line is the only match across all three spec files this PR touches:

1 spec chord(s) hardcode a platform modifier:

  blueprint-grid-position.spec.ts:97  Control+A
      await page.keyboard.press('Control+A')

The fix is the same one #207 made at display-panel-editor.spec.ts:165:

await page.keyboard.press('ControlOrMeta+A')

Why this needed a guard and not another comment

I flagged this line in my review a few hours ago, and I want to be clear that the guard is not aimed at you. It exists because the same thing happened to me. #207 fixed exactly this bug, wrote a five line comment above the corrected call explaining the trap, and the comment still did not reach the next person writing a spec in a different file. That is the failure the guard is for.

The mechanics, since your CI is green today and will stay green on this: on macOS Control+A is the emacs "beginning of line" binding, not select all. Nothing gets selected, the typed value lands next to whatever was in the field, and the assertion sees both. It passes on Linux. Every runner this repo has is ubuntu-latest, including checks and all four Playwright shards, so the browser suite cannot catch it no matter how much of it runs. That is why the new test reads spec source instead of driving a browser.

The rule it encodes

Not "never write Control". There are two cases and they want opposite things:

  • A chord that reaches the app through actions.ts stays Control. ModifierKey there is 'Control' | 'Shift' | 'Alt', so the app has no Meta binding at all.
  • A chord that reaches a focused DOM <input> wants ControlOrMeta, because the OS binding differs by platform. Only TextInput puts a real input on the page, so this case is small: select all, copy, cut, paste and undo inside a text field. Yours is the first kind of case in the second category since Add a settings editor for display-panel entities #197.

A regex cannot separate those, so the test flags both and an ALLOWLIST entry carries the distinction in writing. One entry on base today, chest-filters.spec.ts's undo. If you ever need a literal Control for a chord that reaches the app, add an entry with a reason rather than working around the check.

@wormeyman

Copy link
Copy Markdown
Collaborator

My last two comments point at the same file in opposite directions, so here is which one wins.

The deletion is the one to act on. If tests/blueprint-grid-position.spec.ts goes, line 97 goes with it, the guard has nothing to flag, and there is no ControlOrMeta edit to make. That note only applies if some form of that spec survives, or if the chord turns up in another spec later.

Sorry for the crossed wires. I posted the guard heads-up without re-reading what I had written above it.

…#222 review items

Grid position's premise was refuted by review: Blueprint.serialize()
re-centres every exported position on getCenter()'s bounding box,
recomputed on every call. Translating every entity by (dx, dy) moves
that box's centre by exactly (dx, dy) too, so subtracting the shifted
centre from the shifted positions always reproduces the
pre-translation numbers - a uniform translation is invisible to a
bounding-box recentre by construction, for any implementation built on
moving entities. It also explains a second reported bug for free:
translating only entities and not tiles let the two drift apart
relative to each other once re-centred, since getCenter() averages
both together.

Replaces it with Blueprint.gridPositionOffset, an accumulated IPoint
applied once inside serialize() against the already-computed centre -
after re-centring, not before it, which a recentre cannot undo. Live
entity/tile positions never move, so PositionGrid, rendering and every
other model-level read are unaffected; only the exported string
differs. Entity.forceMoveBy and Blueprint.translateEntities, built for
the old approach, are removed as dead code.

Also:
- BlueprintInfoEditor: BlueprintIconSlot.updateContent() called
  F.CreateIcon() uncaught, which throws for a `space-location` icon
  (planet names - vulcanus, fulgora, gleba) since nothing in FD covers
  that category; took the whole dialog down on five corpus files.
  Wrapped in try/catch with a warning and a blank slot.
- BlueprintAlignment: every text field now commits on 'blur' rather
  than 'changed' (which fires per keystroke). Width/Height/Grid
  position already had reasons to; Absolute X/Y needed it too - a
  leading '-' parses to 0 and was immediately echoed back by the
  post-commit refresh, so a negative value could never be typed at
  all. Also fixes a second bug the blur switch exposed: blurring
  Absolute X/Y after only tabbing through them (never typing) still
  wrote positionRelativeToGrid, turning "never set" into an explicit
  {0, 0} - a real difference to serialize(), not a cosmetic one. A
  dirty flag, set on 'changed' and checked before the blur-commit,
  makes an untouched blur a no-op again.
- parseGridSize() floors Grid size at 1: clearing the Width/Height box
  parsed to 0 through parseGridValue and exported an invalid
  snap-to-grid the game will not accept back.
- Checkbox and RadioButton were near-identical (constructor scaffolding,
  the checked-swap-graphics setter, hover wiring) apart from their drawn
  shape and what a click does to the state. Extracted the shared
  machinery into ToggleControl, taking the shape-drawing function and
  click behaviour as constructor arguments rather than overridden
  methods, so neither needs `this` before `super()` has run.
- Exported BlueprintAlignment's layout constants and
  BlueprintInfoEditor's ALIGNMENT_X/Y instead of tests/blueprint-grid-
  position.spec.ts hand-copying them, so a layout change cannot
  silently desync what the spec clicks.

tests/blueprint-grid-position.spec.ts is rewritten throughout: every
assertion now reads the *exported* positions (encodeLoaded +
decodeBlueprintString) rather than the live model, which is exactly
the gap that let the original bug through - the model position did
change under the old implementation, and nothing checked the string
that mattered.
@koenigstag koenigstag closed this Aug 18, 2026
@koenigstag

Copy link
Copy Markdown
Contributor Author

Closed PR by mistake. Reopening

@wormeyman

Copy link
Copy Markdown
Collaborator

I have detached this repo and it is standalone now you may have to re-fork this repo and resubmit the PR's. @koenigstag

@wormeyman

Copy link
Copy Markdown
Collaborator

trying to reopen

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.

3 participants