Skip to content

Draft: Add a BookDialog tree view, opened from a button beside BlueprintInfoButton - #227

Draft
koenigstag wants to merge 1 commit into
FactoryGameFan:wormeyman-space-age-supportfrom
koenigstag:book-dialog-v2
Draft

Draft: Add a BookDialog tree view, opened from a button beside BlueprintInfoButton#227
koenigstag wants to merge 1 commit into
FactoryGameFan:wormeyman-space-age-supportfrom
koenigstag:book-dialog-v2

Conversation

@koenigstag

@koenigstag koenigstag commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

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 (#221).

Stacked on #222 — future base is wormeyman-space-age-support, not the default branch.

image

--

image

Adds a standalone BookButton (top-left corner) that opens a scrollable
tree dialog listing every entry of a loaded Blueprint Book, recursively
indented for nested books, with planner entries shown disabled. Clicking
a blueprint row selects it via the same flattened-index path the
settings pane's "BP Book Index" field already used.

QuickActions gains getCurrentBook/selectBookEntry so the editor package
can reach the book state packages/website owns, and Editor.init now
takes a single options object (quickActions + optional logger) rather
than positional params, so a required option doesn't have to precede
an optional one.

This is independent of any other in-flight change - the button's
position only echoes a similar corner-button layout by convention, not
by shared code.
@koenigstag koenigstag changed the title Add a BookDialog tree view, opened from a button beside BlueprintInfoButton Draft: Add a BookDialog tree view, opened from a button beside BlueprintInfoButton Aug 13, 2026
@wormeyman

Copy link
Copy Markdown
Collaborator

Still a draft, so this is orientation rather than a review. Two things will bite at merge and are much cheaper to know now than after more work goes on top.

The button coordinate collides with #222

BookButton sits at position.set(152, 6). That is the same coordinate as BlueprintInfoButton in #222, and this branch does not contain #222, so if both land the two buttons sit exactly on top of each other. The class comment says the button goes beside BlueprintInfoButton, which is what it should do but not what the code does. Whichever of the two rebases second has to pick a real second slot.

QuickActions is being defined twice

#221 also adds a QuickActions interface to common/globals.ts, and also reshapes Editor.init into an EditorInitOptions object. The members do not overlap at all. #221 has importReplace, importAppend, exportString, exportImage, encodeCurrent and readClipboardText; this branch has getCurrentBook and selectBookEntry.

Git will flag the conflict, but resolving it is a design call rather than a merge. It has to end up as one interface carrying both sets, not whichever version lands second. Rebasing on #221 before going further would mean you are adding two members to an existing interface instead of writing a second one and then reconciling.

One small thing

Book.entries hands back the live blueprints array. The readonly return type stops honest callers and nothing else, so a cast reaches straight into the book's own state. BookDialog only reads it, so this is about the next caller rather than this one.

The tree view itself reads well. Threading flatIndex through the recursion so a row's index means what selectBlueprint means by it is the right way round, and tests/book-serialize.spec.ts exists because that index space is easy to get wrong, so it is worth a look while you are in there.

@wormeyman

Copy link
Copy Markdown
Collaborator

Still a draft, and unchanged since my orientation comment, so this is not a merge review. I read it properly this time while reviewing #221 and #222 together, and four things are worth having before more work goes on top. Two of them are structural and cheap to fix now.

Everything in my earlier comment still stands: the (152, 6) collision with #222's button, the two competing QuickActions definitions, and Book.entries handing out the live array.

The scroll viewport's hit area scrolls with the content

This is the one to fix first, because it makes the list stop working rather than look wrong.

InventoryDialog keeps m_InventoryItems parked at (VP_X, VP_Y), puts the hit area on it, and scrolls an inner child. You can see it at line 312, where the scroll amount is read as -active.y, off the group container rather than off the one holding the hit area.

BookDialog collapses those two into one. m_Rows carries the hit area at line 208 and is also what the wheel handler moves at line 239. The rectangle is in m_Rows' own space, so it travels with it. Scroll down by S pixels and the live region is only the top VP_H - S of the viewport: the bottom S pixels take neither a click nor a wheel event. A book with 27 or more blueprints has maxScroll >= VP_H, so at full scroll the hit area sits entirely above the viewport and the list goes completely dead, with no way to scroll back.

There is a smaller version of the same thing even at rest. The rectangle starts at local x = 0 while every row spans x 12 to 308, so 12 pixels of padding are live and the right-hand 12 pixels of every row are not.

Splitting it back into a fixed outer container and a scrolling inner one is the whole fix, and it is what the file it was copied from already does.

The active-row highlight never shows

Button's constructor ends with this.addChild(this.m_Background, this.m_Active, this.m_Hover), so m_Background is child 0. button.addChildAt(highlight, 0) at line 89 puts the highlight in front of that in the child list, which means behind it on screen. colors.controls.button.background is {color: 0x646464, alpha: 1}, fully opaque, so nothing of the highlight survives. Open a book on any entry and no row is marked.

The part that makes this worth a second look is that Button.m_Active already draws this. colors.controls.button.active is {color: 0xb16925, alpha: 1}, and ACTIVE_ROW_COLOR at line 30 is 0xb16925. So the hand-rolled graphic is both invisible and a second copy of a mechanism that is already there. button.active = true is the whole thing.

The depth guard makes later rows point at the wrong blueprint

When depth >= MAX_DEPTH the code pushes a placeholder row and returns without recursing, so the blueprints inside that subtree never advance flatIndex.current. Book.getBlueprintAtFlattenedActiveIndex still counts them.

Given entries [a book nested too deep holding 3 blueprints, bpA], bpA is drawn carrying index 0, and clicking it calls selectBookEntry(0), which loads the first blueprint inside the skipped book. The highlight lands on the wrong row for the same reason.

Walking the subtree for its count even when not drawing it is the fix. Worth noting that the mutable { current: number } box is what let this happen quietly: a returned index would have made the missing increment a visible hole in the control flow.

firstIconName and BookButton both need the guard this file already has

Two spots read an icon without the protection tryCreateIcon provides three lines away.

icons?.[0]?.signal.name at line 49 guards icons and icons[0] but not signal. The schema marks signal required, but validation is lenient on purpose, so a blueprint carrying icons: [{index: 1}] loads and then throws inside buildRows, outside tryCreateIcon, with nothing catching above new BookDialog(book). One ?. closes it.

BookButton calls F.CreateIcon('blueprint-book') in UIContainer's constructor, which Editor.init calls with nothing catching above it. That is not hypothetical: #222 has a live crash of exactly this shape. F.CreateIcon throws on any name outside items, fluids, recipes, signals and inventory groups, and I measured 17 icon references in the committed corpus that land there. tryCreateIcon in this file is exactly the guard #222 is missing, so it would be worth putting one copy somewhere both branches can use.

Smaller

BookButton polls G.quickActions.getCurrentBook() from the render ticker every frame, forever, with no ticker.remove, for state that only changes when loadBp runs. It is also constructed on mobile, where UIContainer never adds it, so the poll runs for the life of the page writing .visible on a container with no parent.

BookButton at (152, 6) also overlaps DebugContainer, which sets x = 145; y = 5 and draws its FPS line at its own origin. The button is added after it, so with debug on the readout goes behind the button. The comment justifying 152 reasons about the DOM #corner-panel and misses the canvas-side neighbour.

Every row is built eagerly, with three sprites and a Text whose height is measured on the spot, though only about 13 fit in the viewport and the dialog is rebuilt from scratch on every open.

An entry that is none of blueprint, book, upgrade planner or deconstruction planner produces no row at all. The MAX_DEPTH comment says the point of a placeholder is to avoid "silently dropping the rest of the tree", and a trailing else would hold to that for one line.

tryCreateIcon catches with a bare catch {} and no log, so a missing icon leaves nothing to identify which name failed. getSpriteData and OverlayContainer.createEntityInfo both turn the same failure into a named line.

On tests

There are none, and the flat-index walk in buildRows is pure and needs no FD, which puts it in the same class as railSignalSnapping.test.ts and zoomLevels.test.ts: unit testable under vp test, seconds in CI, no browser. Given item 3 above is arithmetic that goes wrong silently, that is where I would start rather than with a Playwright spec. tests/book-serialize.spec.ts exists because this index space is easy to get backwards, and it is worth reading before the fix rather than after.

The tree view itself still reads well, and threading flatIndex so a row's index means what selectBlueprint means by it is the right call. Rebasing on #221 before going further would also turn the QuickActions collision into two added members rather than a merge to resolve.

wormeyman added a commit that referenced this pull request Aug 25, 2026
… notes (#263)

* Skip the Claude review workflow on pull requests from forks

GitHub withholds repository secrets from a `pull_request` event raised by a
fork, so `secrets.CLAUDE_CODE_OAUTH_TOKEN` resolves to an empty string and the
action fails every time. Measured across the open backlog: `claude-review`
failed on all five fork PRs (#227, #242, #243, #258, and #249 before it merged)
and passed on both in-repo ones (#257, #260). That is the whole pattern - it is
not a misconfiguration the workflow can fix, it is what the event is for.

The failure blocks nothing, which is the problem. Every fork PR opens with a red
X, and a check that is always red is a check nobody reads - so a real failure in
it would be missed. A job-level `if` turns it grey instead.

The alternative is `pull_request_target`, which does get fork PRs reviewed but
hands base-repo secrets to a fork's code. Every outside contribution here
arrives from a fork, so that trade is not available. The comment at the guard
says so, since the next person to notice the skipped runs will reach for it.

In-repo branches, Renovate's included, still run.

Also corrects a stale note in CLAUDE.md: the `ajv` entry still described
`ModdedBlueprintError` and `TrainBlueprintError` as declared-but-never-thrown,
and #262 deleted both. The point it was making survives - ajv is ~100 kB and
nothing branches on its result - so the entry keeps that and records what went.

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

* Correct CLAUDE.md's vite-plus entries against what the repo actually pins

Three corrections, each measured rather than read off the file.

The documented local-install command did not set the version at all. It read
`VP_VERSION=0.2.8 VP_NODE_MANAGER=yes curl -fsSL https://vite.plus | bash`, and
an assignment ahead of a command applies to that command alone - `curl` got the
variables and the `bash` on the far side of the pipe read an empty string.
Measured against a stub script, which printed `VP_VERSION=[]`. The installer
then falls back to `VP_VERSION="${VP_VERSION:-latest}"`, read off the script
itself, so anyone following that line installed `latest` rather than the pin.
That is the "green, and wrong" split the same file warns about one section
down, with a local toolchain silently different from the lockfile's and CI's.
The command now downloads the script and runs it with the variables ahead of
`bash`, matching setup-vp/action.yml, and sets VP_HOME for the layout reason
#260 established. Syntax-checked with `fish -n`, since it is a fish block.

The pin is 0.2.9 everywhere in the repo - root, editor and website
package.json, the root overrides alias, and VP_VERSION in setup-vp/action.yml -
while the file still said 0.2.8 in three places. It also claimed 0.2.8 was
`latest` as of 2026-08-11; `npm view vite-plus dist-tags` gives 0.3.0 today.
That entry has now gone stale twice, which is its own best argument, so it says
so and points at the command to re-measure with.

And the installer-checksum note said the hash did not move across 0.2.6 ->
0.2.8, so a bump usually leaves it alone. True when written, and it is the
reassurance that made 2026-08-24 expensive: the script rotated with VP_VERSION
untouched and every job on every branch failed at `Set up Vite+`. The note now
records that a hash can move with no bump at all. Re-fetched today and the
current sha256 still matches the pin, so nothing in CI needs changing.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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