feat(excalidraw): improve diagram quality with canvas measurement, validation, and conventions

Replace the charCount * fontSize * 0.55 text sizing heuristic with canvas-based
measurement (graceful fallback when native deps unavailable). Add validate.mjs for
automated spatial checks (text overflow, arrow-text collisions, element overlap).
Update element format reference with sizing rules, label guidelines, and arrow routing
conventions. Add verification step to SKILL.md workflow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
John Lamb
2026-02-26 17:19:14 -06:00
parent 36ae861046
commit f524c1b9d8
8 changed files with 824 additions and 0 deletions

View File

@@ -0,0 +1,149 @@
# Excalidraw Element Format Reference
This reference documents the element JSON format accepted by the Excalidraw MCP `create_view` tool and the `export_png.mjs` script.
## Color Palette
### Primary Colors
| Name | Hex | Use |
|------|-----|-----|
| Blue | `#4a9eed` | Primary actions, links |
| Amber | `#f59e0b` | Warnings, highlights |
| Green | `#22c55e` | Success, positive |
| Red | `#ef4444` | Errors, negative |
| Purple | `#8b5cf6` | Accents, special |
| Pink | `#ec4899` | Decorative |
| Cyan | `#06b6d4` | Info, secondary |
### Fill Colors (pastel, for shape backgrounds)
| Color | Hex | Good For |
|-------|-----|----------|
| Light Blue | `#a5d8ff` | Input, sources, primary |
| Light Green | `#b2f2bb` | Success, output |
| Light Orange | `#ffd8a8` | Warning, pending |
| Light Purple | `#d0bfff` | Processing, middleware |
| Light Red | `#ffc9c9` | Error, critical |
| Light Yellow | `#fff3bf` | Notes, decisions |
| Light Teal | `#c3fae8` | Storage, data |
## Element Types
### Required Fields (all elements)
`type`, `id` (unique string), `x`, `y`, `width`, `height`
### Defaults (skip these)
strokeColor="#1e1e1e", backgroundColor="transparent", fillStyle="solid", strokeWidth=2, roughness=1, opacity=100
### Shapes
**Rectangle**: `{ "type": "rectangle", "id": "r1", "x": 100, "y": 100, "width": 200, "height": 100 }`
- `roundness: { type: 3 }` for rounded corners
- `backgroundColor: "#a5d8ff"`, `fillStyle: "solid"` for filled
**Ellipse**: `{ "type": "ellipse", "id": "e1", "x": 100, "y": 100, "width": 150, "height": 150 }`
**Diamond**: `{ "type": "diamond", "id": "d1", "x": 100, "y": 100, "width": 150, "height": 150 }`
### Labels
**Labeled shape (preferred)**: Add `label` to any shape for auto-centered text.
```json
{ "type": "rectangle", "id": "r1", "x": 100, "y": 100, "width": 200, "height": 80, "label": { "text": "Hello", "fontSize": 20 } }
```
**Standalone text** (titles, annotations only):
```json
{ "type": "text", "id": "t1", "x": 150, "y": 138, "text": "Hello", "fontSize": 20 }
```
### Arrows
```json
{ "type": "arrow", "id": "a1", "x": 300, "y": 150, "width": 200, "height": 0, "points": [[0,0],[200,0]], "endArrowhead": "arrow" }
```
**Bindings** connect arrows to shapes:
```json
"startBinding": { "elementId": "r1", "fixedPoint": [1, 0.5] }
```
fixedPoint: top=[0.5,0], bottom=[0.5,1], left=[0,0.5], right=[1,0.5]
**Labeled arrow**: `"label": { "text": "connects" }`
### Camera (MCP only, not exported to PNG)
```json
{ "type": "cameraUpdate", "width": 800, "height": 600, "x": 0, "y": 0 }
```
Camera sizes must be 4:3 ratio. The export script filters these out automatically.
## Sizing Rules
### Container-to-text ratios
- Box width >= estimated_text_width * 1.4 (40% horizontal margin)
- Box height >= estimated_text_height * 1.5 (50% vertical margin)
- Minimum box size: 150x60 for single-line labels, 200x80 for multi-line
### Font size constraints
- Labels inside containers: max fontSize 14
- Service/zone titles: fontSize 18-22
- Standalone annotations: fontSize 12-14
- Never exceed fontSize 16 inside a box smaller than 300px wide
### Padding
- Minimum 15px padding on each side between text and container edge
- For multi-line text, add 8px vertical padding per line beyond the first
### General
- Leave 20-30px gaps between elements
## Label Content Guidelines
### Keep labels short
- Maximum 2 lines per label inside shapes
- Maximum 25 characters per line
- If label needs 3+ lines, split: short name in box, details as annotation below
### Label patterns
- Service box: "Service Name" (1 line) or "Service Name\nBrief role" (2 lines)
- Component box: "Component Name" (1 line)
- Detail text: Use standalone text elements positioned below/beside the box
### Bad vs Good
BAD: label "Auth-MS\nOAuth tokens, credentials\n800-1K req/s, <100ms" (3 lines, 30+ chars)
GOOD: label "Auth-MS\nOAuth token management" (2 lines, 22 chars max)
+ standalone text below: "800-1K req/s, <100ms p99"
## Arrow Routing Rules
### Gutter-based routing
- Define horizontal and vertical gutters (20-30px gaps between service zones)
- Route arrows through gutters, never over content areas
- Use right-angle waypoints along zone edges
### Waypoint placement
- Start/end points: attach to box edges using fixedPoint bindings
- Mid-waypoints: offset 20px from nearest box edge
- For crossing traffic: stagger parallel arrows by 10px
### Vertical vs horizontal preference
- Prefer horizontal arrows for same-tier connections
- Prefer vertical arrows for cross-tier flows (consumer -> service -> external)
- Diagonal arrows only when routing around would add 3+ waypoints
### Label placement on arrows
- Arrow labels should sit in empty space, not over boxes
- For vertical arrows: place label to the left or right, offset 15px
- For horizontal arrows: place label above, offset 10px
## Example: Two Connected Boxes
```json
[
{ "type": "cameraUpdate", "width": 800, "height": 600, "x": 50, "y": 50 },
{ "type": "rectangle", "id": "b1", "x": 100, "y": 100, "width": 200, "height": 100, "roundness": { "type": 3 }, "backgroundColor": "#a5d8ff", "fillStyle": "solid", "label": { "text": "Start", "fontSize": 20 } },
{ "type": "rectangle", "id": "b2", "x": 450, "y": 100, "width": 200, "height": 100, "roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid", "label": { "text": "End", "fontSize": 20 } },
{ "type": "arrow", "id": "a1", "x": 300, "y": 150, "width": 150, "height": 0, "points": [[0,0],[150,0]], "endArrowhead": "arrow", "startBinding": { "elementId": "b1", "fixedPoint": [1, 0.5] }, "endBinding": { "elementId": "b2", "fixedPoint": [0, 0.5] } }
]
```