feat: /canvas command — visual layer of the wiki
New files:
commands/canvas.md — slash command with full trigger table
skills/canvas/SKILL.md — complete skill: open, new, add image/text/pdf/note,
zone, list, from-banana operations
skills/canvas/references/canvas-spec.md — Obsidian canvas JSON spec:
coordinate system, all node types, color table,
image sizing by aspect ratio, auto-positioning
pseudocode, common mistakes, full example
Auto-positioning algorithm (bbox math):
- Finds rightmost node in target zone
- Places next node at rightmost_x + 40, same row y
- If overflow: wraps to new row (max_y_in_zone + 20)
- Falls back to below all content if no zone found
Banana integration:
- /canvas from banana: checks .recent-images.txt first, then
filesystem find -newer 10min, presents list for confirmation
- After any /banana run: suggests /canvas from banana
Updated:
skills/wiki/SKILL.md — added /canvas to routing table
skills/wiki/references/plugins.md — added Calendar + Thino with
pre-installed note + manual install fallback
README.md — /canvas in commands table + file structure
This commit is contained in:
2
.obsidian/graph.json
vendored
2
.obsidian/graph.json
vendored
@@ -67,6 +67,6 @@
|
|||||||
"repelStrength": 20,
|
"repelStrength": 20,
|
||||||
"linkStrength": 1,
|
"linkStrength": 1,
|
||||||
"linkDistance": 80,
|
"linkDistance": 80,
|
||||||
"scale": 0.4786753448718912,
|
"scale": 0.6620854838064228,
|
||||||
"close": false
|
"close": false
|
||||||
}
|
}
|
||||||
30
.obsidian/workspace.json
vendored
30
.obsidian/workspace.json
vendored
@@ -190,18 +190,22 @@
|
|||||||
},
|
},
|
||||||
"active": "a31de1b302cdc5cf",
|
"active": "a31de1b302cdc5cf",
|
||||||
"lastOpenFiles": [
|
"lastOpenFiles": [
|
||||||
|
"README.md.tmp.224546.1775557396282",
|
||||||
|
"README.md.tmp.224546.1775557389171",
|
||||||
|
"README.md.tmp.224546.1775557379648",
|
||||||
|
"skills/wiki/references/plugins.md.tmp.224546.1775557354797",
|
||||||
|
"skills/wiki/SKILL.md.tmp.224546.1775557343437",
|
||||||
|
"skills/canvas/references/canvas-spec.md",
|
||||||
|
"skills/canvas/references/canvas-spec.md.tmp.224546.1775557327401",
|
||||||
|
"skills/canvas/SKILL.md",
|
||||||
|
"skills/canvas/SKILL.md.tmp.224546.1775557280886",
|
||||||
|
"commands/canvas.md",
|
||||||
|
"commands/canvas.md.tmp.224546.1775557227716",
|
||||||
|
"wiki/canvases",
|
||||||
|
"skills/canvas/references",
|
||||||
|
"skills/canvas",
|
||||||
"wiki/comparisons/Wiki vs RAG.md",
|
"wiki/comparisons/Wiki vs RAG.md",
|
||||||
"wiki/comparisons/Wiki vs RAG.md.tmp.224546.1775556215080",
|
|
||||||
"wiki/questions/How does the LLM Wiki pattern work.md",
|
"wiki/questions/How does the LLM Wiki pattern work.md",
|
||||||
"wiki/questions/How does the LLM Wiki pattern work.md.tmp.224546.1775556203684",
|
|
||||||
"wiki/comparisons",
|
|
||||||
"wiki/questions",
|
|
||||||
"wiki/concepts/_index.md.tmp.224546.1775556164132",
|
|
||||||
"wiki/index.md.tmp.224546.1775556150463",
|
|
||||||
"wiki/log.md.tmp.224546.1775556137750",
|
|
||||||
"wiki/hot.md.tmp.224546.1775556127364",
|
|
||||||
"wiki/sources/_index.md.tmp.224546.1775556114770",
|
|
||||||
"wiki/entities/_index.md.tmp.224546.1775556102633",
|
|
||||||
"wiki/meta/workflow-loop.gif",
|
"wiki/meta/workflow-loop.gif",
|
||||||
"wiki/meta/wiki-graph-grow.gif",
|
"wiki/meta/wiki-graph-grow.gif",
|
||||||
"cover.gif",
|
"cover.gif",
|
||||||
@@ -233,10 +237,6 @@
|
|||||||
"wiki/Wiki Map.canvas",
|
"wiki/Wiki Map.canvas",
|
||||||
"skills/wiki/references/rest-api.md",
|
"skills/wiki/references/rest-api.md",
|
||||||
"skills/wiki/references/plugins.md",
|
"skills/wiki/references/plugins.md",
|
||||||
"skills/wiki/references/modes.md",
|
"skills/wiki/references/modes.md"
|
||||||
"skills/wiki/references/mcp-setup.md",
|
|
||||||
"skills/wiki/references/git-setup.md",
|
|
||||||
"skills/wiki/references/frontmatter.md",
|
|
||||||
"skills/wiki/references/css-snippets.md"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
15
README.md
15
README.md
@@ -81,6 +81,11 @@ Then scaffold the full wiki structure.
|
|||||||
| `/save` | File the current conversation as a wiki note |
|
| `/save` | File the current conversation as a wiki note |
|
||||||
| `/save [name]` | Save with a specific title (skips the naming question) |
|
| `/save [name]` | Save with a specific title (skips the naming question) |
|
||||||
| `/autoresearch [topic]` | Run the autonomous research loop: search, fetch, synthesize, file |
|
| `/autoresearch [topic]` | Run the autonomous research loop: search, fetch, synthesize, file |
|
||||||
|
| `/canvas` | Open or create the visual canvas, list zones and nodes |
|
||||||
|
| `/canvas add image [path]` | Add an image (URL or local path) to the canvas with auto-layout |
|
||||||
|
| `/canvas add note [page]` | Pin a wiki page as a card on the canvas |
|
||||||
|
| `/canvas zone [name]` | Add a new labeled zone to organize visual content |
|
||||||
|
| `/canvas from banana` | Capture recently generated images onto the canvas |
|
||||||
| `lint the wiki` | Health check: orphans, dead links, gaps, suggestions |
|
| `lint the wiki` | Health check: orphans, dead links, gaps, suggestions |
|
||||||
| `update hot cache` | Refresh hot.md with latest context summary |
|
| `update hot cache` | Refresh hot.md with latest context summary |
|
||||||
|
|
||||||
@@ -198,16 +203,20 @@ cosmic-brain/
|
|||||||
│ ├── wiki-query/ # QUERY operation
|
│ ├── wiki-query/ # QUERY operation
|
||||||
│ ├── wiki-lint/ # LINT operation
|
│ ├── wiki-lint/ # LINT operation
|
||||||
│ ├── save/ # /save — file conversations to wiki
|
│ ├── save/ # /save — file conversations to wiki
|
||||||
│ └── autoresearch/ # /autoresearch — autonomous research loop
|
│ ├── autoresearch/ # /autoresearch — autonomous research loop
|
||||||
|
│ │ └── references/
|
||||||
|
│ │ └── program.md # configurable research objectives
|
||||||
|
│ └── canvas/ # /canvas — visual layer (images, PDFs, notes)
|
||||||
│ └── references/
|
│ └── references/
|
||||||
│ └── program.md # configurable research objectives
|
│ └── canvas-spec.md # Obsidian canvas JSON format reference
|
||||||
├── agents/
|
├── agents/
|
||||||
│ ├── wiki-ingest.md # parallel ingestion agent
|
│ ├── wiki-ingest.md # parallel ingestion agent
|
||||||
│ └── wiki-lint.md # health check agent
|
│ └── wiki-lint.md # health check agent
|
||||||
├── commands/
|
├── commands/
|
||||||
│ ├── wiki.md # /wiki bootstrap command
|
│ ├── wiki.md # /wiki bootstrap command
|
||||||
│ ├── save.md # /save command
|
│ ├── save.md # /save command
|
||||||
│ └── autoresearch.md # /autoresearch command
|
│ ├── autoresearch.md # /autoresearch command
|
||||||
|
│ └── canvas.md # /canvas visual layer command
|
||||||
├── hooks/
|
├── hooks/
|
||||||
│ └── hooks.json # SessionStart + Stop hot cache hooks
|
│ └── hooks.json # SessionStart + Stop hot cache hooks
|
||||||
├── _templates/ # Obsidian Templater templates
|
├── _templates/ # Obsidian Templater templates
|
||||||
|
|||||||
21
commands/canvas.md
Normal file
21
commands/canvas.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
description: Open, create, or update a visual canvas — add images, text, PDFs, wiki pages, and banana-generated assets to Obsidian canvas files.
|
||||||
|
---
|
||||||
|
|
||||||
|
Read the `canvas` skill. Then run the operation matching the user's command.
|
||||||
|
|
||||||
|
| Command | What it does |
|
||||||
|
|---------|-------------|
|
||||||
|
| `/canvas` | Status check — report node counts, list zones, open instructions |
|
||||||
|
| `/canvas new [name]` | Create a new named canvas in wiki/canvases/ |
|
||||||
|
| `/canvas add image [path]` | Add image to canvas (download if URL, copy if outside vault) |
|
||||||
|
| `/canvas add text [content]` | Add a text card to the canvas |
|
||||||
|
| `/canvas add pdf [path]` | Add a PDF document node |
|
||||||
|
| `/canvas add note [page]` | Add a wiki page as a linked card |
|
||||||
|
| `/canvas zone [name] [color]` | Add a new labeled zone group |
|
||||||
|
| `/canvas list` | List all canvases with node counts |
|
||||||
|
| `/canvas from banana` | Find recent generated images and add them |
|
||||||
|
|
||||||
|
Default canvas: `wiki/canvases/main.canvas`
|
||||||
|
|
||||||
|
If the canvas file does not exist, create it before adding anything.
|
||||||
273
skills/canvas/SKILL.md
Normal file
273
skills/canvas/SKILL.md
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
---
|
||||||
|
name: canvas
|
||||||
|
description: >
|
||||||
|
Visual layer of the wiki. Add images, text cards, PDFs, and wiki pages to Obsidian
|
||||||
|
canvas files. Auto-positions nodes inside zones using bbox math. Integrates with
|
||||||
|
/banana skill for generated image capture. Triggers on: "/canvas", "canvas new",
|
||||||
|
"canvas add image", "canvas add text", "canvas add pdf", "canvas add note",
|
||||||
|
"canvas zone", "canvas list", "canvas from banana", "add to canvas",
|
||||||
|
"put this on the canvas", "open canvas", "create canvas".
|
||||||
|
allowed-tools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep"]
|
||||||
|
---
|
||||||
|
|
||||||
|
# canvas — Visual Reference Layer
|
||||||
|
|
||||||
|
The three knowledge capture layers:
|
||||||
|
- `/save` → text synthesis (wiki/questions/, wiki/concepts/)
|
||||||
|
- `/autoresearch` → structured knowledge (wiki/sources/, wiki/concepts/)
|
||||||
|
- `/canvas` → visual references (wiki/canvases/)
|
||||||
|
|
||||||
|
A canvas is a JSON file Obsidian renders as an infinite visual board. This skill reads and writes canvas JSON directly. Read `references/canvas-spec.md` for the full format reference before making any edits.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Default Canvas
|
||||||
|
|
||||||
|
`wiki/canvases/main.canvas`
|
||||||
|
|
||||||
|
If it does not exist, create it:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "title",
|
||||||
|
"type": "text",
|
||||||
|
"text": "# Visual Reference\n\nDrop images, PDFs, and notes here.",
|
||||||
|
"x": -400, "y": -300, "width": 400, "height": 120, "color": "6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "zone-default",
|
||||||
|
"type": "group",
|
||||||
|
"label": "General",
|
||||||
|
"x": -400, "y": -140, "width": 800, "height": 400, "color": "4"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"edges": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Operations
|
||||||
|
|
||||||
|
### open / status (`/canvas` with no args)
|
||||||
|
|
||||||
|
1. Check if `wiki/canvases/main.canvas` exists.
|
||||||
|
2. If yes: read it, count nodes by type, list all group node labels (zone names).
|
||||||
|
Report: "Canvas has N nodes: X images, Y text cards, Z wiki pages. Zones: [list]"
|
||||||
|
3. If no: create it with the starter structure above.
|
||||||
|
Report: "Created main.canvas with a General zone."
|
||||||
|
4. Tell user: "Open `wiki/canvases/main.canvas` in Obsidian to view."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### new (`/canvas new [name]`)
|
||||||
|
|
||||||
|
1. Slugify the name: lowercase, spaces → hyphens, strip special chars.
|
||||||
|
2. Create `wiki/canvases/[slug].canvas` with the starter structure, title updated to `# [Name]`.
|
||||||
|
3. Add entry to `wiki/index.md` under a "## Canvases" section (create the section if it doesn't exist).
|
||||||
|
4. Report: "Created wiki/canvases/[slug].canvas"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### add image (`/canvas add image [path or url]`)
|
||||||
|
|
||||||
|
**Resolve the image:**
|
||||||
|
- If URL (starts with `http`): download with `curl -sL [url] -o _attachments/images/canvas/[filename]`
|
||||||
|
Derive filename from URL path, or use `img-[timestamp].jpg` if unclear.
|
||||||
|
- If local path outside vault: `cp [path] _attachments/images/canvas/`
|
||||||
|
- If already vault-relative: use as-is.
|
||||||
|
|
||||||
|
Create `_attachments/images/canvas/` if it doesn't exist.
|
||||||
|
|
||||||
|
**Detect aspect ratio:**
|
||||||
|
Use `python3 -c "from PIL import Image; img=Image.open('[path]'); print(img.width, img.height)"` or `identify -format '%w %h' [path]`.
|
||||||
|
- 16:9 (width/height ≈ 1.7–2.0): width=420, height=236
|
||||||
|
- 1:1 (ratio ≈ 1.0): width=280, height=280
|
||||||
|
- 9:16 (ratio < 0.65): width=200, height=356
|
||||||
|
- Unknown: width=320, height=240
|
||||||
|
|
||||||
|
**Position using auto-layout** (see Auto-Positioning section below).
|
||||||
|
|
||||||
|
**Append node to canvas JSON and write.**
|
||||||
|
|
||||||
|
Report: "Added [filename] to [zone] zone at position ([x], [y])."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### add text (`/canvas add text [content]`)
|
||||||
|
|
||||||
|
Create a text node:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "text-[timestamp]",
|
||||||
|
"type": "text",
|
||||||
|
"text": "[content]",
|
||||||
|
"x": [auto], "y": [auto],
|
||||||
|
"width": 300, "height": 120,
|
||||||
|
"color": "4"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Position using auto-layout. Write and report.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### add pdf (`/canvas add pdf [path]`)
|
||||||
|
|
||||||
|
Same as add image. Obsidian renders PDFs natively as file nodes.
|
||||||
|
- Copy to `_attachments/pdfs/canvas/` if outside vault.
|
||||||
|
- Fixed size: width=400, height=520.
|
||||||
|
- Report page count if you can determine it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### add note (`/canvas add note [wiki-page]`)
|
||||||
|
|
||||||
|
1. Search `wiki/` for a file matching the page name (case-insensitive, partial match ok).
|
||||||
|
2. Use the vault-relative path as the `file` field.
|
||||||
|
3. Create a file node: width=300, height=100.
|
||||||
|
4. Position using auto-layout.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "note-[timestamp]",
|
||||||
|
"type": "file",
|
||||||
|
"file": "wiki/concepts/LLM Wiki Pattern.md",
|
||||||
|
"x": [auto], "y": [auto],
|
||||||
|
"width": 300, "height": 100
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### zone (`/canvas zone [name] [color]`)
|
||||||
|
|
||||||
|
1. Read canvas JSON.
|
||||||
|
2. Find max_y: `max(node.y + node.height for all nodes) + 60`. Use -140 if no nodes.
|
||||||
|
3. Create a group node:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "zone-[slug]",
|
||||||
|
"type": "group",
|
||||||
|
"label": "[name]",
|
||||||
|
"x": -400,
|
||||||
|
"y": [max_y],
|
||||||
|
"width": 1000,
|
||||||
|
"height": 400,
|
||||||
|
"color": "[color or '3']"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Valid colors: `"1"`=red `"2"`=orange `"3"`=yellow `"4"`=green `"5"`=cyan `"6"`=purple
|
||||||
|
|
||||||
|
Write and report.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### list (`/canvas list`)
|
||||||
|
|
||||||
|
1. `glob wiki/canvases/*.canvas`
|
||||||
|
2. For each canvas: read JSON, count nodes by type.
|
||||||
|
3. Report:
|
||||||
|
|
||||||
|
```
|
||||||
|
wiki/canvases/main.canvas — 14 nodes (8 images, 3 text, 2 file, 1 group)
|
||||||
|
wiki/canvases/design-ideas.canvas — 42 nodes (30 images, 4 text, 8 groups)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### from banana (`/canvas from banana`)
|
||||||
|
|
||||||
|
1. Check `wiki/canvases/.recent-images.txt` first (session log of newly written images).
|
||||||
|
2. If not found or empty: `find _attachments/images -name "*.png" -o -name "*.jpg" -newer [10min-ago-file]`
|
||||||
|
Create the reference file: `python3 -c "import time; open('/tmp/ten-min-ago','w').close(); import os; os.utime('/tmp/ten-min-ago', (time.time()-600, time.time()-600))"`
|
||||||
|
Then: `find _attachments/images -newer /tmp/ten-min-ago -name "*.png" -o -name "*.jpg"`
|
||||||
|
3. If still none: show the 5 most recently modified images.
|
||||||
|
4. Present list: "Found N recent images: [list]. Add to canvas? Which zone? (zone name / 'new [name]' / 'skip')"
|
||||||
|
5. On confirmation: add each using the add image logic.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Auto-Positioning Algorithm
|
||||||
|
|
||||||
|
Read `references/canvas-spec.md` for the full coordinate system.
|
||||||
|
|
||||||
|
```python
|
||||||
|
def next_position(canvas_nodes, target_zone_label, new_w, new_h):
|
||||||
|
# Find zone group node
|
||||||
|
zone = next((n for n in canvas_nodes
|
||||||
|
if n.get('type') == 'group'
|
||||||
|
and n.get('label') == target_zone_label), None)
|
||||||
|
|
||||||
|
if zone is None:
|
||||||
|
# No zone — place below all content
|
||||||
|
max_y = max((n['y'] + n.get('height', 0) for n in canvas_nodes), default=-140)
|
||||||
|
return -400, max_y + 60
|
||||||
|
|
||||||
|
zx, zy = zone['x'], zone['y']
|
||||||
|
zw, zh = zone['width'], zone['height']
|
||||||
|
|
||||||
|
# Nodes inside this zone
|
||||||
|
inside = [n for n in canvas_nodes
|
||||||
|
if n.get('type') != 'group'
|
||||||
|
and zx <= n['x'] < zx + zw
|
||||||
|
and zy <= n['y'] < zy + zh]
|
||||||
|
|
||||||
|
if not inside:
|
||||||
|
return zx + 20, zy + 20
|
||||||
|
|
||||||
|
rightmost_x = max(n['x'] + n.get('width', 0) for n in inside)
|
||||||
|
next_x = rightmost_x + 40
|
||||||
|
|
||||||
|
if next_x + new_w > zx + zw:
|
||||||
|
# New row
|
||||||
|
max_row_y = max(n['y'] + n.get('height', 0) for n in inside)
|
||||||
|
return zx + 20, max_row_y + 20
|
||||||
|
|
||||||
|
# Same row — match the top y of existing row
|
||||||
|
current_row_y = min(n['y'] for n in inside if n['x'] > zx + zw / 2) \
|
||||||
|
if any(n['x'] > zx + zw / 2 for n in inside) \
|
||||||
|
else zy + 20
|
||||||
|
return next_x, current_row_y
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ID Generation
|
||||||
|
|
||||||
|
Read the canvas, collect all existing IDs. Never reuse one.
|
||||||
|
|
||||||
|
Safe ID pattern: `[type]-[content-slug]-[unix-timestamp-last-4-digits]`
|
||||||
|
|
||||||
|
Examples: `img-cover-7823`, `text-note-1045`, `zone-branding-3391`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Session Log (optional hook)
|
||||||
|
|
||||||
|
If `wiki/canvases/.recent-images.txt` exists, append any new image path written to `_attachments/images/` during this session (one path per line, keep last 20).
|
||||||
|
|
||||||
|
`/canvas from banana` reads this file first, making it instant without filesystem search.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Banana Integration
|
||||||
|
|
||||||
|
After any `/banana` run in the same session, if the user says "add to canvas" or "put on canvas", treat it as `/canvas from banana`.
|
||||||
|
|
||||||
|
When `/banana` finishes generating images, suggest:
|
||||||
|
> "Add generated images to canvas? Run `/canvas from banana`"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
1. Read canvas-spec.md before editing any canvas JSON.
|
||||||
|
2. Always read the canvas file before writing — parse existing nodes to avoid ID collisions and calculate auto-positions.
|
||||||
|
3. Create `_attachments/images/canvas/` for downloaded/copied images.
|
||||||
|
4. Update `wiki/index.md` when creating new canvases.
|
||||||
|
5. Report position and zone after every add operation.
|
||||||
273
skills/canvas/references/canvas-spec.md
Normal file
273
skills/canvas/references/canvas-spec.md
Normal file
@@ -0,0 +1,273 @@
|
|||||||
|
# Obsidian Canvas JSON Specification
|
||||||
|
|
||||||
|
Canvas files are JSON with two top-level keys: `nodes` (array) and `edges` (array).
|
||||||
|
Obsidian reads and writes them as UTF-8 JSON files with `.canvas` extension.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Coordinate System
|
||||||
|
|
||||||
|
```
|
||||||
|
x increases →
|
||||||
|
┌─────────────────────────────────
|
||||||
|
│ (-920, -2400) (0, -2400)
|
||||||
|
│
|
||||||
|
y │ (-920, 0) (0, 0) ← origin
|
||||||
|
↓ │
|
||||||
|
│ (-920, 540) (500, 540)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Origin** (0, 0) is the center of the canvas viewport.
|
||||||
|
- **x increases rightward.** Negative x = left of center.
|
||||||
|
- **y increases downward.** Negative y = above center.
|
||||||
|
- Node `x` and `y` are the **top-left corner** of the node, not the center.
|
||||||
|
- Obsidian pans to fit all nodes on first open.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Node Types
|
||||||
|
|
||||||
|
### Text node
|
||||||
|
|
||||||
|
Renders markdown content as a styled card.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "text-title-4821",
|
||||||
|
"type": "text",
|
||||||
|
"text": "# Heading\n\nParagraph with **bold** and `code`.",
|
||||||
|
"x": -400,
|
||||||
|
"y": -300,
|
||||||
|
"width": 400,
|
||||||
|
"height": 120,
|
||||||
|
"color": "6"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `text`: markdown string. Use `\n` for newlines.
|
||||||
|
- Minimum readable size: width ≥ 200, height ≥ 60.
|
||||||
|
- `color` is optional. Omit for default (no color).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### File node
|
||||||
|
|
||||||
|
Renders an image, PDF, markdown note, or other vault file inline.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "img-cover-7823",
|
||||||
|
"type": "file",
|
||||||
|
"file": "_attachments/images/skool-hub/logo-pro.png",
|
||||||
|
"x": -900,
|
||||||
|
"y": -100,
|
||||||
|
"width": 420,
|
||||||
|
"height": 236
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `file`: **vault-relative path** (not absolute, not `~/`).
|
||||||
|
- Supported: `.png` `.jpg` `.webp` `.gif` `.pdf` `.md` `.canvas`
|
||||||
|
- For `.md` files: renders as a preview card.
|
||||||
|
- For `.pdf` files: renders the first page as preview.
|
||||||
|
- No `color` field for file nodes — color is ignored.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Group node (Zone)
|
||||||
|
|
||||||
|
A labeled rectangular region. Does not clip or contain nodes — it's a visual guide.
|
||||||
|
Nodes placed "inside" a group are just positioned within its bounding box.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "zone-branding-3391",
|
||||||
|
"type": "group",
|
||||||
|
"label": "Brand Identity",
|
||||||
|
"x": -920,
|
||||||
|
"y": -880,
|
||||||
|
"width": 1060,
|
||||||
|
"height": 290,
|
||||||
|
"color": "6"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `label`: shown at the top of the group box.
|
||||||
|
- `color`: colors the group border and label.
|
||||||
|
- Groups do not affect auto-layout — they are purely visual containers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Link node
|
||||||
|
|
||||||
|
Renders a web URL as an embedded preview card.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "link-karpathy-2233",
|
||||||
|
"type": "link",
|
||||||
|
"url": "https://github.com/karpathy",
|
||||||
|
"x": 200,
|
||||||
|
"y": -300,
|
||||||
|
"width": 400,
|
||||||
|
"height": 120
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `url`: must be a valid `https://` URL.
|
||||||
|
- Obsidian fetches the Open Graph preview (title, description, thumbnail).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Edges
|
||||||
|
|
||||||
|
Connections between nodes. Usually empty for mood boards.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "e-hub-cidx",
|
||||||
|
"fromNode": "hub",
|
||||||
|
"fromSide": "right",
|
||||||
|
"toNode": "c-idx",
|
||||||
|
"toSide": "left",
|
||||||
|
"toEnd": "arrow",
|
||||||
|
"label": "concepts",
|
||||||
|
"color": "5"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `fromSide` / `toSide`: `"top"` `"bottom"` `"left"` `"right"`
|
||||||
|
- `toEnd`: `"arrow"` (directed) or `"none"` (undirected line)
|
||||||
|
- `label` and `color` are optional.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Color Reference
|
||||||
|
|
||||||
|
| Code | Color | Hex (approx) | Use case |
|
||||||
|
|------|-------|-------------|----------|
|
||||||
|
| `"1"` | Red / Tomato | #e03e3e | Warnings, archive |
|
||||||
|
| `"2"` | Orange | #d09035 | Active work |
|
||||||
|
| `"3"` | Yellow / Gold | #d0a023 | WIP, notes |
|
||||||
|
| `"4"` | Green / Teal | #448361 | Content, sources |
|
||||||
|
| `"5"` | Blue / Cyan | #3ea7d3 | Navigation, info |
|
||||||
|
| `"6"` | Purple / Violet | #9063d2 | Title, identity |
|
||||||
|
|
||||||
|
Omit `color` entirely for the default (no border color, transparent label).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Image Sizing Guidelines
|
||||||
|
|
||||||
|
Calculate from actual image dimensions using PIL or `identify`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -c "from PIL import Image; img=Image.open('path.png'); print(img.width, img.height)"
|
||||||
|
# or
|
||||||
|
identify -format '%w %h' path.png
|
||||||
|
```
|
||||||
|
|
||||||
|
| Aspect ratio | Condition | Canvas width | Canvas height |
|
||||||
|
|-------------|-----------|-------------|--------------|
|
||||||
|
| 16:9 (wide) | ratio 1.6–2.0 | 420 | 236 |
|
||||||
|
| 2:1 (ultra wide) | ratio > 2.0 | 440 | 220 |
|
||||||
|
| 4:3 | ratio 1.2–1.6 | 380 | 285 |
|
||||||
|
| 1:1 (square) | ratio 0.9–1.1 | 280 | 280 |
|
||||||
|
| 3:4 | ratio 0.6–0.9 | 240 | 320 |
|
||||||
|
| 9:16 (portrait) | ratio < 0.6 | 200 | 356 |
|
||||||
|
| PDF | any | 400 | 520 |
|
||||||
|
| Unknown | fallback | 320 | 240 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Auto-Positioning Pseudocode
|
||||||
|
|
||||||
|
```
|
||||||
|
function place_node(canvas, zone_label, new_w, new_h):
|
||||||
|
zone = find group node where label == zone_label
|
||||||
|
padding = 20
|
||||||
|
|
||||||
|
if zone not found:
|
||||||
|
max_y = max(n.y + n.height for n in canvas.nodes) + 60
|
||||||
|
return (-400, max_y)
|
||||||
|
|
||||||
|
# Nodes visually inside zone
|
||||||
|
inside = [n for n in canvas.nodes
|
||||||
|
if n.type != 'group'
|
||||||
|
and zone.x <= n.x < zone.x + zone.width
|
||||||
|
and zone.y <= n.y < zone.y + zone.height]
|
||||||
|
|
||||||
|
if inside is empty:
|
||||||
|
return (zone.x + padding, zone.y + padding)
|
||||||
|
|
||||||
|
# Rightmost point in zone
|
||||||
|
rightmost = max(n.x + n.width for n in inside)
|
||||||
|
next_x = rightmost + 40
|
||||||
|
|
||||||
|
if next_x + new_w > zone.x + zone.width - padding:
|
||||||
|
# Overflow → new row
|
||||||
|
bottom_of_row = max(n.y + n.height for n in inside)
|
||||||
|
return (zone.x + padding, bottom_of_row + padding)
|
||||||
|
|
||||||
|
# Same row
|
||||||
|
row_y = min(n.y for n in inside) # align to top of existing row
|
||||||
|
return (next_x, row_y)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Full Example: Two-Zone Canvas
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": "title-0001",
|
||||||
|
"type": "text",
|
||||||
|
"text": "# Brand Reference\n\n**AI Marketing Hub** visual assets",
|
||||||
|
"x": -920, "y": -2440, "width": 560, "height": 180, "color": "6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "zone-logos",
|
||||||
|
"type": "group",
|
||||||
|
"label": "Logos & Icons",
|
||||||
|
"x": -920, "y": -2200, "width": 1800, "height": 320, "color": "6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "img-logo-pro",
|
||||||
|
"type": "file",
|
||||||
|
"file": "_attachments/images/skool-hub/logo-pro.png",
|
||||||
|
"x": -900, "y": -2180, "width": 420, "height": 236
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "img-icon-free",
|
||||||
|
"type": "file",
|
||||||
|
"file": "_attachments/images/skool-hub/icon-free-v1.png",
|
||||||
|
"x": -440, "y": -2180, "width": 280, "height": 280
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "zone-covers",
|
||||||
|
"type": "group",
|
||||||
|
"label": "Skill Covers",
|
||||||
|
"x": -920, "y": -1820, "width": 1800, "height": 340, "color": "3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "img-seo",
|
||||||
|
"type": "file",
|
||||||
|
"file": "_attachments/images/skool-hub/claude-seo-cover.png",
|
||||||
|
"x": -900, "y": -1800, "width": 420, "height": 236
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"edges": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Mistakes
|
||||||
|
|
||||||
|
- **Wrong path format**: use `_attachments/images/file.png` not `/home/user/...` or `~/...`
|
||||||
|
- **ID collision**: always read existing IDs before generating a new one
|
||||||
|
- **Negative y confusion**: `y: -2400` is ABOVE `y: -1000` (more negative = higher up)
|
||||||
|
- **Group does not clip**: placing a node "inside" a group is just positioning it within the group's bounding box — there is no parent-child relationship in the JSON
|
||||||
|
- **Missing height on text nodes**: Obsidian will render the text but may clip it if height is too small. Use height ≥ content-lines × 24.
|
||||||
@@ -107,6 +107,7 @@ Route to the correct operation based on what the user says:
|
|||||||
| "lint", "health check", "clean up" | LINT | `wiki-lint` |
|
| "lint", "health check", "clean up" | LINT | `wiki-lint` |
|
||||||
| "save this", "file this", "/save" | SAVE | `save` |
|
| "save this", "file this", "/save" | SAVE | `save` |
|
||||||
| "/autoresearch [topic]", "research [topic]" | AUTORESEARCH | `autoresearch` |
|
| "/autoresearch [topic]", "research [topic]" | AUTORESEARCH | `autoresearch` |
|
||||||
|
| "/canvas", "add to canvas", "open canvas" | CANVAS | `canvas` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -50,9 +50,15 @@ Install via Settings > Community Plugins > Turn off Restricted Mode > Browse.
|
|||||||
| **Dataview** | Query vault as a database. Powers dashboards in `wiki/meta/`. |
|
| **Dataview** | Query vault as a database. Powers dashboards in `wiki/meta/`. |
|
||||||
| **Templater** | Auto-populate frontmatter on note creation from `_templates/`. |
|
| **Templater** | Auto-populate frontmatter on note creation from `_templates/`. |
|
||||||
| **Obsidian Git** | Auto-commit every 15 minutes. Protects against bad writes. |
|
| **Obsidian Git** | Auto-commit every 15 minutes. Protects against bad writes. |
|
||||||
|
| **Calendar** | Right-sidebar calendar with word count, task, and link indicators. Pre-installed in this vault via `.obsidian/plugins/calendar/`. |
|
||||||
|
| **Thino** | Quick memo capture panel in right sidebar. Pre-installed via `.obsidian/plugins/thino/`. |
|
||||||
| **Iconize** | Visual folder icons for navigation. |
|
| **Iconize** | Visual folder icons for navigation. |
|
||||||
| **Minimal Theme** | Best dark theme for dense information display. |
|
| **Minimal Theme** | Best dark theme for dense information display. |
|
||||||
|
|
||||||
|
**Calendar and Thino are pre-installed** — they ship with this vault. Enable them in Settings → Community Plugins → toggle on. No download needed.
|
||||||
|
|
||||||
|
If installing in a different vault: download `main.js` + `manifest.json` from their GitHub releases into `.obsidian/plugins/calendar/` and `.obsidian/plugins/thino/` respectively.
|
||||||
|
|
||||||
Optional additions:
|
Optional additions:
|
||||||
- **Smart Connections** — semantic search across all notes
|
- **Smart Connections** — semantic search across all notes
|
||||||
- **QuickAdd** — macros for fast note creation
|
- **QuickAdd** — macros for fast note creation
|
||||||
|
|||||||
Reference in New Issue
Block a user