No description
Find a file
ValleytheKnight 9dff779660 Bump version to 1.0.0
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-21 19:07:38 -05:00
.github/workflows Scaffold Canvas Positioning Toolkit plugin 2026-07-21 19:03:58 -05:00
docs Add README and demo GIFs 2026-07-21 19:05:03 -05:00
src Add auto-layout, pick-up-and-place, and find-card-by-text features 2026-07-21 19:03:58 -05:00
test Add auto-layout, pick-up-and-place, and find-card-by-text features 2026-07-21 19:03:58 -05:00
.gitignore Scaffold Canvas Positioning Toolkit plugin 2026-07-21 19:03:58 -05:00
esbuild.config.mjs Scaffold Canvas Positioning Toolkit plugin 2026-07-21 19:03:58 -05:00
eslint.config.mts Scaffold Canvas Positioning Toolkit plugin 2026-07-21 19:03:58 -05:00
LICENSE Initial commit 2026-07-21 17:22:47 -05:00
manifest.json Bump version to 1.0.0 2026-07-21 19:07:38 -05:00
package-lock.json Scaffold Canvas Positioning Toolkit plugin 2026-07-21 19:03:58 -05:00
package.json Bump version to 1.0.0 2026-07-21 19:07:38 -05:00
README.md Add README and demo GIFs 2026-07-21 19:05:03 -05:00
styles.css Add auto-layout, pick-up-and-place, and find-card-by-text features 2026-07-21 19:03:58 -05:00
tsconfig.json Scaffold Canvas Positioning Toolkit plugin 2026-07-21 19:03:58 -05:00
version-bump.mjs Scaffold Canvas Positioning Toolkit plugin 2026-07-21 19:03:58 -05:00
versions.json Bump version to 1.0.0 2026-07-21 19:07:38 -05:00

Canvas Positioning Toolkit

I built this because moving cards around on an Obsidian Canvas board never had a good answer for two specific situations: a messy board that needs tidying, and pulling one card out of a dense cluster when your mouse precision runs out at low zoom. Neither problem had an existing plugin solving it when I checked, so here's what I built instead.

Auto-layout demo

Scope

This plugin only touches Canvas card x/y/width/height on the active board. It never touches file content, other views, or anything outside a Canvas. Every feature below goes through the same three calls: read a card's position off canvas.nodes, write a new one with node.setData(), then tell the canvas it moved with canvas.markMoved() so undo history and the on-disk save stay in sync.

I confirmed that API by reading how the Advanced Canvas plugin (github.com/Developer-Mike/obsidian-advanced-canvas) uses the same objects, and by checking that its own patcher overrides markMoved and requestSave as already-existing methods rather than adding them. That told me these are core Obsidian Canvas methods, not something Advanced Canvas invented, so this plugin has no dependency on it.

Auto-layout

What it does: arranges cards into a tidy grid. Run it with nothing selected and it lays out the whole board; select two or more cards first and only those move, everything else stays put.

Why: I kept ending up with boards where cards landed wherever I'd dropped them, and there was no "just tidy this up" button. Advanced Canvas doesn't have one, and the two mindmap-focused plugins I found only auto-layout tree/connection structures, not an arbitrary set of cards.

How I built it: each row uses the tallest card in that row to offset the next row, so a mix of short and tall cards never overlaps:

export function computeGridLayout(
	nodes: LayoutNode[],
	columns: number,
	gap: number,
): Map<string, LayoutPosition> {
	const positions = new Map<string, LayoutPosition>();
	if (nodes.length === 0 || columns < 1) return positions;

	let cursorY = 0;
	for (let rowStart = 0; rowStart < nodes.length; rowStart += columns) {
		const row = nodes.slice(rowStart, rowStart + columns);
		const rowHeight = Math.max(...row.map((n) => n.height));

		let cursorX = 0;
		for (const node of row) {
			positions.set(node.id, { x: cursorX, y: cursorY });
			cursorX += node.width + gap;
		}

		cursorY += rowHeight + gap;
	}

	return positions;
}

Try it: Command palette → Auto-layout cards (selection, or whole board if nothing selected). Columns and spacing are configurable in the plugin's settings.

Pick up and place

Pick up and place demo

What it does: select a card, run the command, move your mouse with no button held down, click an empty spot to place it. Press Escape instead and it snaps back to where it started.

Why: this one came from a forum post someone sent me (forum.obsidian.md/t/moving-dragging-cards-more-efficiently-across-the-canvas/116135). At low zoom, a card's drag hit-target gets small enough that pulling one card out of a cluster means repeatedly hovering to find the hand cursor. Their proposed fix, a hotkey that picks the card up so it follows your pointer without holding a button, was exactly right, and nobody had built it.

Under the hood: canvas.posFromEvt(event) converts a mouse event straight to canvas coordinates regardless of zoom, so following the pointer comes down to this:

private followPointer(e: PointerEvent): void {
	if (!this.canvas || !this.node) return;
	const pos = this.canvas.posFromEvt(e);
	const node = this.node;
	node.setData({
		...node.getData(),
		x: pos.x - node.width / 2,
		y: pos.y - node.height / 2,
	});
	this.canvas.markMoved(node);
}

Placing (or canceling) removes the listeners and either saves the new position or restores the original one I recorded when the pick-up started.

On mobile: this listens for Pointer Events (pointermove/pointerup), not mouse events specifically, so a touch drag works the same way: the card follows your finger while it's down, and lifting it places the card. The gesture is naturally a hold-and-drag on touch instead of a move-with-no-button-held, since touch input has no hover state, but the same command and the same code path handle both.

Try it: select a card, then run Pick up selected card (click empty space to place) from the command palette.

Find card by text

Find card by text demo

What it does: opens a fuzzy search over every card's text on the current board. Type part of the card's content, pick it from the list, and it's selected and picked up in one step, ready to place.

Why: pick-up-and-place solves moving a card once you've selected it, but selecting the right card in a tight cluster in the first place has the exact same aiming problem. Searching by text sidesteps aiming entirely instead of trying to make it easier.

How it works: Obsidian's own FuzzySuggestModal does the fuzzy matching. I feed it every card's label:

export class FindCardModal extends FuzzySuggestModal<CanvasNode> {
	getItems(): CanvasNode[] {
		return [...this.canvas.nodes.values()];
	}

	getItemText(node: CanvasNode): string {
		return cardLabel(node.getData());
	}

	onChooseItem(node: CanvasNode): void {
		this.onChoose(node);
	}
}

cardLabel reads a text card's first line, or a file card's filename, so the list shows something meaningful instead of raw IDs.

Try it: run Find card by text and pick it up from the command palette, type part of the card's text, and press Enter.

Settings

  • Grid columns: how many cards per row when running auto-layout.
  • Grid gap: pixel spacing between cards when running auto-layout.

Installation

Not yet in Obsidian's community plugin directory. Manual install for now:

  1. Download main.js, manifest.json, and styles.css from the latest release.
  2. Put them in <your vault>/.obsidian/plugins/canvas-positioning-toolkit/.
  3. Reload Obsidian and enable the plugin in Community plugins.

Contributing

Issues and pull requests are welcome.

Support

If this saved you some dragging, buy me a coffee.

License

MIT