|
|
||
|---|---|---|
| .github/workflows | ||
| docs | ||
| src | ||
| test | ||
| .gitignore | ||
| esbuild.config.mjs | ||
| eslint.config.mts | ||
| LICENSE | ||
| manifest.json | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| styles.css | ||
| tsconfig.json | ||
| version-bump.mjs | ||
| versions.json | ||
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.
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
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
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:
- Download
main.js,manifest.json, andstyles.cssfrom the latest release. - Put them in
<your vault>/.obsidian/plugins/canvas-positioning-toolkit/. - 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


