Compare commits

...
Author SHA1 Message Date
DumiFlex 73f32cbe32 fix(bookmark): compact dock grid tiles that fit long shortcuts 2026-11-14 01:38:51 +00:00
DumiFlex b52e6e5fb3 fix(bookmark): Bookmark this view places the bookmark from its live bounds
A new node's cached boundingRect is [0, 0, …] until it is drawn, so the placement thought it sat at the canvas origin and moved it far from the view. Placement now reads LiteGraph's getBounding (worked out from pos and size) at the zoom the jump will use.
2026-11-14 01:36:39 +00:00
9 changed files with 70 additions and 34 deletions
+1 -1
View File
@@ -29,7 +29,7 @@ A bookmark has no node frame and can't be resized: it is a small pill with its i
## The Bookmarks dock
- **Row**: Click to jump. Number (a Step note's in its circle), icon, name and shortcut; a mark for one hidden on the canvas or inside a subgraph, OFF for a muted one.
- **Grid / List**: Shows the bookmarks as icon tiles (number, icon in its colour, name, shortcut) that wrap three to a line, or as rows. The dock remembers your choice; edit mode always shows rows.
- **Grid / List**: Shows the bookmarks as small tiles (number, icon in its colour, name, shortcut with Shift as ⇧; the tooltip has the full name and keys) that wrap four to a line, or as rows. The dock remembers your choice; edit mode always shows rows.
- **Back**: Return to where you were before the last jump.
- **↑ / ↓**: Step through the bookmarks that are on, in dock order.
- **Pencil**: Edit mode: drag the dock by its header and it snaps to the nearest spot of a 3×3 grid on the free canvas (a dashed outline shows where); drag the grips to reorder, the eye shows or hides a bookmark on the canvas, and Bookmark this view adds one at the zoom you're at: the small 3×3 grid beside it picks where on screen the new bookmark sits, and it lands there, so jumping to it brings back that exact view. Land clear of the dock (on by default) moves a jump out from under the dock; off, bookmarks land where they would without it, behind the see-through dock. Done goes back. The dock follows the layout (side panels, the bottom panel, the top bar, the minimap and zoom controls, a floating run bar) and stays put when the job progress panel, the error card, a toast or a menu pops up; those float over it.
+1 -1
View File
@@ -22,7 +22,7 @@
- **Hide the variables an Assembler doesn't use.** The Prompt Assembler's variables toolbar has a new eye button beside Load, Save and Clear: it marks every upstream variable that neither the prompt nor the negative template reads as internal, so the chips and the `$` list only offer what you print. Combines and rules still read internal variables, so `$hero` used inside `$scene` keeps working. Every other Assembler on the same context is checked first, and a variable one of them prints is kept; so is a module that also writes a variable in use, since one switch covers all its variables. The message says what was marked and what was kept and why, and **Undo** puts it all back.
- **Show a WP panel on its subgraph node.** Click the eye beside a WP panel in the subgraph's **Parameters** panel, the eye in the toolbar over a selected WP node inside the subgraph, or right-click that node (Context, Toggles, Label Grid, Debug, Context Loop and the rest) and pick **Show on subgraph node**: a copy of its panel appears on the subgraph node outside, under the node's title, so you can change toggles or settings without opening the subgraph. Both copies stay in step, the node inside is what runs, and the subgraph node remembers what it shows when you save. The copy's header folds it away, its sliders button (**Choose what shows**) lets you tick which parts of the panel show there (Label Grid's Font, Spacing or Title, single toggles or groups, Context modules, Cleaner rules …; new parts show until you untick them), **Move up** / **Move down** in the same menu order the WP panels on the subgraph node, and its × (or the eye again, or **Hide from subgraph node**) takes it off. The node inside always keeps the whole panel. Works in Nodes 2.0 and the classic canvas.
- **A grid for the Bookmarks dock.** The dock's header has a grid button: it shows your bookmarks as tiles, three to a line, each with its icon large in the bookmark's colour, the name and the shortcut, so you can find one by its icon. Click a tile to jump; the list button brings the rows back, and the dock remembers which you picked. Edit mode keeps the rows.
- **A grid for the Bookmarks dock.** The dock's header has a grid button: it shows your bookmarks as small tiles, four to a line, each with its icon in the bookmark's colour, the name and the shortcut (Shift drawn as ⇧ so a long one fits; point at a tile for the full name and keys), so you can find one by its icon. Click a tile to jump; the list button brings the rows back, and the dock remembers which you picked. Edit mode keeps the rows.
- **Left and right labels in WP Label & Grid can be vertical, or over the image.** With Place set to left or right, a new switch turns the text to run along the edge (upwards on the left, downwards on the right), which keeps the strip thin, and **Over image** now works on the left and right too, drawing the band over that edge and keeping the image's size.
### Wildcards
+1 -1
View File
@@ -1903,7 +1903,7 @@
},
{
"title": "The Bookmarks dock",
"text": "The bookmark button in the top bar shows or hides the dock. It also opens by itself in a workflow with bookmarks, unless you closed it. It sits in the free part of the canvas, clear of the side panel, the bottom panel, the minimap and the zoom controls, and moves when they open or close. What pops up on its own (the job progress panel while a workflow runs, the error card, toasts, menus, tooltips) never moves it or where bookmarks land; it floats over them. It is see-through until you point at it (more so when folded). The grid button in its header shows the bookmarks as icon tiles instead of rows, and the list button goes back; the dock remembers which you picked.\nFigure: The dock in view mode: click a row to jump. Its pencil opens edit mode, where the eye brings back a hidden bookmark. Screenshot of the Bookmarks dock, in view mode, at the top left of the canvas, slightly see-through. Its header reads Bookmarks, 4 · 1 hidden, with Back, Previous, Next, a pencil to edit and Close. The rows show a number, a coloured icon, a name and a shortcut: 1 Prompts (Alt + 1), 2 Sampler (Alt + 2, highlighted as the bookmark just jumped to), 3 Upscale in grey italics with a small crossed-out eye because it is hidden on the canvas (Alt + 3), and 4 Save and share marked OFF because the node is muted. On the canvas, the Sampler bookmark's pill (Alt + 2) sits by the KSampler and VAE Decode, the muted Save and share pill is greyed and marked OFF, and the prompt and latent nodes are below; the hidden Upscale bookmark is not drawn.\nRow\nClick to jump. Number (a Step note's in its circle), icon, name and shortcut; a mark for one hidden on the canvas or inside a subgraph, OFF for a muted one.\nGrid / List\nShows the bookmarks as icon tiles (number, icon in its colour, name, shortcut) that wrap three to a line, or as rows. The dock remembers your choice; edit mode always shows rows.\nBack\nReturn to where you were before the last jump.\n↑ / ↓\nStep through the bookmarks that are on, in dock order.\nPencil\nEdit mode: drag the dock by its header and it snaps to the nearest spot of a 3×3 grid on the free canvas (a dashed outline shows where); drag the grips to reorder, the eye shows or hides a bookmark on the canvas, and Bookmark this view adds one at the zoom you're at: the small 3×3 grid beside it picks where on screen the new bookmark sits, and it lands there, so jumping to it brings back that exact view. Land clear of the dock (on by default) moves a jump out from under the dock; off, bookmarks land where they would without it, behind the see-through dock. Done goes back. The dock follows the layout (side panels, the bottom panel, the top bar, the minimap and zoom controls, a floating run bar) and stays put when the job progress panel, the error card, a toast or a menu pops up; those float over it.\nNext, Previous, Back, Bookmark this view and the dock toggle are commands without keys: bind them in Settings › Keybinding."
"text": "The bookmark button in the top bar shows or hides the dock. It also opens by itself in a workflow with bookmarks, unless you closed it. It sits in the free part of the canvas, clear of the side panel, the bottom panel, the minimap and the zoom controls, and moves when they open or close. What pops up on its own (the job progress panel while a workflow runs, the error card, toasts, menus, tooltips) never moves it or where bookmarks land; it floats over them. It is see-through until you point at it (more so when folded). The grid button in its header shows the bookmarks as icon tiles instead of rows, and the list button goes back; the dock remembers which you picked.\nFigure: The dock in view mode: click a row to jump. Its pencil opens edit mode, where the eye brings back a hidden bookmark. Screenshot of the Bookmarks dock, in view mode, at the top left of the canvas, slightly see-through. Its header reads Bookmarks, 4 · 1 hidden, with Back, Previous, Next, a pencil to edit and Close. The rows show a number, a coloured icon, a name and a shortcut: 1 Prompts (Alt + 1), 2 Sampler (Alt + 2, highlighted as the bookmark just jumped to), 3 Upscale in grey italics with a small crossed-out eye because it is hidden on the canvas (Alt + 3), and 4 Save and share marked OFF because the node is muted. On the canvas, the Sampler bookmark's pill (Alt + 2) sits by the KSampler and VAE Decode, the muted Save and share pill is greyed and marked OFF, and the prompt and latent nodes are below; the hidden Upscale bookmark is not drawn.\nRow\nClick to jump. Number (a Step note's in its circle), icon, name and shortcut; a mark for one hidden on the canvas or inside a subgraph, OFF for a muted one.\nGrid / List\nShows the bookmarks as small tiles (number, icon in its colour, name, shortcut with Shift as ⇧; the tooltip has the full name and keys) that wrap four to a line, or as rows. The dock remembers your choice; edit mode always shows rows.\nBack\nReturn to where you were before the last jump.\n↑ / ↓\nStep through the bookmarks that are on, in dock order.\nPencil\nEdit mode: drag the dock by its header and it snaps to the nearest spot of a 3×3 grid on the free canvas (a dashed outline shows where); drag the grips to reorder, the eye shows or hides a bookmark on the canvas, and Bookmark this view adds one at the zoom you're at: the small 3×3 grid beside it picks where on screen the new bookmark sits, and it lands there, so jumping to it brings back that exact view. Land clear of the dock (on by default) moves a jump out from under the dock; off, bookmarks land where they would without it, behind the see-through dock. Done goes back. The dock follows the layout (side panels, the bottom panel, the top bar, the minimap and zoom controls, a floating run bar) and stays put when the job progress panel, the error card, a toast or a menu pops up; those float over it.\nNext, Previous, Back, Bookmark this view and the dock toggle are commands without keys: bind them in Settings › Keybinding."
},
{
"title": "Works with",
@@ -54,6 +54,9 @@ describe("BookmarkDock", () => {
expect(w.findAll('[data-test^="bm-dock-row-"]')).toHaveLength(0);
expect(tiles[0]?.text()).toContain("Prompts");
expect(tiles[1]?.attributes("title")).toContain("hidden on the canvas");
// Shift is drawn as ⇧ so a long chord fits; the tooltip spells it out.
expect(tiles[0]?.text()).toContain("Alt");
expect(tiles[0]?.attributes("title")).toContain("Alt + 1");
expect(JSON.parse(localStorage.getItem("wp.bookmarkDock") ?? "{}").layout).toBe("grid");
await w.get('[data-test="bm-dock-edit"]').trigger("click");
+3 -3
View File
@@ -396,9 +396,9 @@ function close(): void {
margin: 0;
padding: 0;
display: grid;
/* Three a line in the 300 px dock; tiles wrap, never widen it. */
grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
gap: 5px;
/* Four a line in the 300 px dock; tiles wrap, never widen it. */
grid-template-columns: repeat(auto-fill, minmax(64px, 1fr));
gap: 4px;
}
.wp-bmdock__grid > li { display: flex; min-width: 0; }
.wp-bmdock__grid > li > * { flex: 1; }
+27 -20
View File
@@ -2,6 +2,7 @@
/** One bookmark of the dock's grid: its icon large in its colour, the name
* under it and the shortcut. A jump target like a list row (DockRow); the
* grid is view mode only, so it has no eye. */
import { computed } from "vue";
import Kbd from "@/kit/Kbd.vue";
import type { BookmarkEntry } from "../../widgets/bookmark";
@@ -20,11 +21,15 @@ function onKey(e: KeyboardEvent): void {
}
}
/** Shift as ⇧ (everyone knows the arrow), so Ctrl + Shift + 1 fits a tile. */
const keys = computed(() => props.entry.config.shortcut.split(/\+(?!$)/).map((k) => (k === "Shift" ? "⇧" : k)));
/** The full name and its marks, since the tile cuts a long name short. */
function tip(): string {
const e = props.entry;
return [
e.title,
e.config.shortcut ? e.config.shortcut.replace(/\+/g, " + ") : "",
e.nested ? "in a subgraph" : "",
e.config.hidden ? "hidden on the canvas" : "",
e.disabled ? "muted or bypassed" : "",
@@ -54,7 +59,7 @@ function tip(): string {
<i v-else class="pi wp-bmtile__icon" :class="`pi-${entry.config.icon}`" aria-hidden="true" />
<span class="wp-bmtile__name">{{ entry.title }}</span>
<span v-if="entry.disabled" class="wp-bmtile__off">off</span>
<Kbd v-else-if="entry.config.shortcut" :keys="entry.config.shortcut" size="sm" />
<Kbd v-else-if="entry.config.shortcut" class="wp-bmtile__keys" :keys="keys" size="sm" />
</div>
</template>
@@ -66,10 +71,10 @@ function tip(): string {
flex-direction: column;
align-items: center;
justify-content: flex-start;
gap: 4px;
gap: 3px;
min-width: 0;
box-sizing: border-box;
padding: 14px 4px 6px;
padding: 11px 3px 4px;
border-radius: min(6px, var(--wp-radius-cap, 999px));
background: color-mix(in srgb, var(--wp-bg-1) 60%, transparent);
box-shadow: inset 0 2px 0 var(--wp-bm-color);
@@ -83,47 +88,49 @@ function tip(): string {
.wp-bmtile.is-hidden .wp-bmtile__name { font-style: italic; color: var(--wp-text-dim); }
.wp-bmtile__num {
position: absolute;
top: 4px;
left: 6px;
top: 2px;
left: 4px;
color: var(--wp-text-dim);
font-size: 10px;
font-size: 9px;
font-variant-numeric: tabular-nums;
}
.wp-bmtile__marks {
position: absolute;
top: 4px;
right: 5px;
top: 2px;
right: 4px;
display: flex;
gap: 3px;
color: var(--wp-text-dim);
font-size: 10px;
}
.wp-bmtile__marks .pi { font-size: 10px; }
.wp-bmtile__icon { color: var(--wp-bm-color); font-size: 20px; line-height: 24px; }
.wp-bmtile__marks .pi { font-size: 9px; }
.wp-bmtile__icon { color: var(--wp-bm-color); font-size: 15px; line-height: 18px; }
/* A Step note's number, in a circle like on the note. */
.wp-bmtile__step {
display: inline-grid;
place-items: center;
width: 24px;
height: 24px;
width: 18px;
height: 18px;
border-radius: var(--wp-radius-cap, 999px);
background: var(--wp-bm-color);
color: white;
font-size: 12px;
font-size: 10px;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
/* Two lines, then an ellipsis; the tooltip has the rest. */
.wp-bmtile__name {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
max-width: 100%;
overflow: hidden;
overflow-wrap: anywhere;
white-space: nowrap;
text-overflow: ellipsis;
font-size: 11.5px;
font-weight: 600;
line-height: 1.25;
line-height: 1.2;
}
/* Tighter caps with no plus signs, so Ctrl ⇧ 1 fits the tile; the tooltip
spells the shortcut out. */
.wp-bmtile__keys { max-width: 100%; gap: 1px; }
.wp-bmtile__keys :deep(.wp-kbd-keys__plus) { display: none; }
.wp-bmtile__keys :deep(.wp-kbd-keys__cap) { min-width: 14px; height: 14px; padding: 0 2px; font-size: 9px; }
.wp-bmtile__off { color: var(--wp-text-dim); font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.04em; }
</style>
+1 -1
View File
@@ -60,7 +60,7 @@ export const NODE_FIELDS = {
],
dock: [
{ term: "Row", desc: "Click to jump. Number (a Step note's in its circle), icon, name and shortcut; a mark for one hidden on the canvas or inside a subgraph, OFF for a muted one." },
{ term: "Grid / List", desc: "Shows the bookmarks as icon tiles (number, icon in its colour, name, shortcut) that wrap three to a line, or as rows. The dock remembers your choice; edit mode always shows rows." },
{ term: "Grid / List", desc: "Shows the bookmarks as small tiles (number, icon in its colour, name, shortcut with Shift as ⇧; the tooltip has the full name and keys) that wrap four to a line, or as rows. The dock remembers your choice; edit mode always shows rows." },
{ term: "Back", desc: "Return to where you were before the last jump." },
{ term: "↑ / ↓", desc: "Step through the bookmarks that are on, in dock order." },
{ term: "Pencil", desc: "Edit mode: drag the dock by its header and it snaps to the nearest spot of a 3×3 grid on the free canvas (a dashed outline shows where); drag the grips to reorder, the eye shows or hides a bookmark on the canvas, and Bookmark this view adds one at the zoom you're at: the small 3×3 grid beside it picks where on screen the new bookmark sits, and it lands there, so jumping to it brings back that exact view. Land clear of the dock (on by default) moves a jump out from under the dock; off, bookmarks land where they would without it, behind the see-through dock. Done goes back. The dock follows the layout (side panels, the bottom panel, the top bar, the minimap and zoom controls, a floating run bar) and stays put when the job progress panel, the error card, a toast or a menu pops up; those float over it." },
+13 -2
View File
@@ -326,7 +326,13 @@ describe("Bookmark this view", () => {
let n = 0;
(globalThis as { LiteGraph?: unknown }).LiteGraph = {
createNode: () => {
const node = { id: 100 + n++, title: "WP Bookmark", type: BOOKMARK_NODE_TYPE, graph: root, pos: [0, 0], size: [160, 30] };
// Like LiteGraph: `boundingRect` is cached at the last draw (a new
// node's is all zeros); `getBounding` works it out from pos and size.
const node = {
id: 100 + n++, title: "WP Bookmark", type: BOOKMARK_NODE_TYPE, graph: root, pos: [0, 0], size: [160, 30],
boundingRect: [0, 0, 0, 0],
getBounding(): number[] { return [this.pos[0] ?? 0, (this.pos[1] ?? 0) - 30, 160, 60]; },
};
let config: BookmarkConfig = defaultBookmarkConfig();
bookmarkCells.set(node, { get: () => config, set: (next) => { config = next as BookmarkConfig; } });
return node;
@@ -352,7 +358,12 @@ describe("Bookmark this view", () => {
const sy = ((b?.node.pos[1] ?? 0) + oy()) * 1.5;
// On screen and not under the dock.
expect(sx >= 300 || sy >= 400).toBe(true);
if (b) jumpTo(b.node);
// ComfyUI draws it before you can jump, which refreshes its cache.
const node = b?.node as (BookmarkNode & { getBounding(): number[] }) | undefined;
if (node) {
node.boundingRect = node.getBounding();
jumpTo(node);
}
expect(ox()).toBeCloseTo(-500, 0);
expect(oy()).toBeCloseTo(-200, 0);
expect(canvas.ds.scale).toBe(1.5);
+20 -5
View File
@@ -51,6 +51,9 @@ export interface BookmarkNode extends MountTargetNode {
graph?: LiteGraphLike | null;
widgets?: { name?: string; value?: unknown }[];
boundingRect?: ArrayLike<number>;
/** LiteGraph's own bounds, worked out from `pos` and `size` now (title
* included). `boundingRect` is only refreshed when the node is drawn. */
getBounding?: (out?: Float32Array, includeExternal?: boolean) => ArrayLike<number>;
setDirtyCanvas?: (fg: boolean, bg?: boolean) => void;
}
@@ -562,25 +565,36 @@ function showGraph(c: CanvasLike, graph: LiteGraphLike | null | undefined, then:
setTimeout(then, 50);
}
function nodeRect(node: BookmarkNode): [number, number, number, number] {
const b = node.boundingRect;
/** The node's box on the canvas. `fresh`: from where it is now, never the
* `boundingRect` cached at its last draw (a node just added or just moved
* has not been drawn there yet; a new one's cache is still [0, 0, …]). */
function nodeRect(node: BookmarkNode, fresh = false): [number, number, number, number] {
const b = fresh ? safeBounding(node) : node.boundingRect;
if (b && b.length >= 4) return [b[0] ?? 0, b[1] ?? 0, b[2] ?? 0, b[3] ?? 0];
const [x = 0, y = 0] = node.pos;
const [w = 0, h = 0] = node.size;
return [x, y - TITLE_H, w, h + TITLE_H];
}
function safeBounding(node: BookmarkNode): ArrayLike<number> | undefined {
try {
return node.getBounding?.(new Float32Array(4));
} catch {
return undefined;
}
}
/** Where a jump to `node` puts the view (its offset) at `scale`: its spot of
* the free screen, clear of the minimap, zoom controls and (switch on) the
* dock. */
function landingOffset(c: CanvasLike, node: BookmarkNode, config: BookmarkConfig, scale: number): [number, number] {
function landingOffset(c: CanvasLike, node: BookmarkNode, config: BookmarkConfig, scale: number, fresh = false): [number, number] {
const cr = c.canvas?.getBoundingClientRect();
const area = freeArea(c.canvas);
const rel = (b: ScreenBox): ScreenBox => [b[0] - (cr?.left ?? 0), b[1] - (cr?.top ?? 0), b[2] - (cr?.left ?? 0), b[3] - (cr?.top ?? 0)];
const [vw, vh] = viewportSize(c);
const free = rel(area.free);
return jumpOffset({
rect: nodeRect(node),
rect: nodeRect(node, fresh),
viewport: [vw, vh],
anchor: config.anchor,
scale,
@@ -857,7 +871,8 @@ export function addBookmarkHere(at?: readonly number[], spot?: DockSpot): void {
function settleAtLanding(c: CanvasLike, node: BookmarkNode, config: BookmarkConfig): void {
if (!c.ds) return;
for (let i = 0; i < 2; i++) {
const [ox, oy] = landingOffset(c, node, config, c.ds.scale);
// At the zoom the jump will use (the stored one, rounded to 0.01).
const [ox, oy] = landingOffset(c, node, config, jumpScale(config, c.ds.scale), true);
const dx = ox - (c.ds.offset[0] ?? 0);
const dy = oy - (c.ds.offset[1] ?? 0);
if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5) break;