feat: add Excalidraw diagram agent skill for book figures (#507)

Add a project-level Agent Skill at .claude/skills/excalidraw-diagram
(symlinked from .agents/skills for other agents) that guides AI agents
to draw book figures with Excalidraw: editable .excalidraw source next
to exported SVG/PNG in v1/img/chNN, no PDF references, a palette that
stays legible on the dark-mode #e8e8e8 image backdrop, and separate
-zh variants for figures with text.

Adapted from github/awesome-copilot excalidraw-diagram-generator (MIT):
templates normalized to Excalifont and proper bound-text labels, plus a
stdlib-only check_figures.py linter for scenes and chapter image refs.
Document the skill in CONTRIBUTING_en.md and CONTRIBUTING_zh.md.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yeqi Huang
2026-10-26 02:11:55 +00:00
committed by GitHub
parent 40348fcb14
commit 9be2fb30f7
15 changed files with 3582 additions and 0 deletions

View File

@@ -0,0 +1 @@
../../.claude/skills/excalidraw-diagram

View File

@@ -0,0 +1,35 @@
This skill is adapted from the "excalidraw-diagram-generator" skill in
https://github.com/github/awesome-copilot (skills/excalidraw-diagram-generator).
Files derived from upstream:
references/excalidraw-schema.md (unchanged, openmlsys note added at top)
references/element-types.md (unchanged, openmlsys note added at top)
scripts/add-arrow.py (label font size 14 -> 16, note in docstring)
templates/*.excalidraw (fontFamily -> 5, bound text labels fixed,
font size >= 16)
New for openmlsys: SKILL.md (rewritten), references/openmlsys-conventions.md,
scripts/check_figures.py.
The upstream license follows.
MIT License
Copyright GitHub, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,69 @@
---
name: excalidraw-diagram
description: Draw figures for the openmlsys "Machine Learning Systems" book with Excalidraw. Use when a chapter needs a new or redrawn diagram (architecture, pipeline, flowchart, data flow, sequence, mind map), or when asked to "draw", "diagram", "make a figure" or "create an Excalidraw file" for this repo. Produces an editable .excalidraw source plus an exported SVG/PNG under v1/img/chXX/, with EN/ZH variants when the figure contains text.
---
# Excalidraw figures for openmlsys
Every figure in the book has two files side by side: the editable `.excalidraw`
source and the exported `.svg` (preferred) or `.png` that the Markdown
references. Read [references/openmlsys-conventions.md](references/openmlsys-conventions.md)
before drawing. It covers paths, house style, dark mode and bilingual variants.
## Workflow
1. **Plan the figure.** Pick the diagram type and list the nodes, edges and
labels. Keep it to about 20 elements. If it needs more, split it into
several figures.
2. **Choose the paths.** Images live in `v1/img/ch<NN>/`, which every chapter
tree reaches through its `img` symlink. Name files in lowercase with
hyphens, e.g. `v1/img/ch03/tensor-core-dataflow.excalidraw`. v1 and v2
share this folder, so check that the name is not taken.
3. **Write the scene.** Start from a file in [templates/](templates/) or write
the JSON yourself. The schema is in
[references/excalidraw-schema.md](references/excalidraw-schema.md) and the
element specs are in [references/element-types.md](references/element-types.md).
House rules:
- Text uses `fontFamily: 5` (Excalifont), or `3` for code. Use `fontSize`
16 or more and the dark stroke `#1e1e1e`.
- A label inside a shape is a **separate `text` element** with
`containerId` set to the shape. The shape must list it in
`boundElements`. A `text` property on a rectangle is silently ignored.
- Use pastel fills, dark strokes and no white or light-colored text (see
the palette in the conventions file).
- To add a connector to an existing file, run
`python3 .claude/skills/excalidraw-diagram/scripts/add-arrow.py <file> x1 y1 x2 y2 [--label T] [--style dashed]`.
4. **Make the ZH variant** if the figure contains words. Copy the scene to
`<name>-zh.excalidraw` and translate only the text. Then widen any boxes
that the Chinese labels overflow. Figures without text use one shared file.
5. **Lint.** Run `python3 .claude/skills/excalidraw-diagram/scripts/check_figures.py <files or dirs>`.
It needs only the standard library. Fix every ERROR.
6. **Export** each scene to `<name>.svg` (or `<name>.png` at 2x) next to its
source: background off, dark mode off, light theme. You can export with the
Excalidraw MCP, excalidraw.com or the VS Code Excalidraw extension (steps in
the conventions file). If you have no tool that can render, stop here. Tell
the contributor which files still need exporting. Do not hand-write an SVG.
**Never export or reference a PDF.** mdBook pages cannot display PDF images.
7. **Reference** the image from the chapter with a relative path:
```markdown
![Data flow of a training step](../img/ch06/training-step-dataflow.svg)
:width:`700px`
:label:`fig-training-step-dataflow`
```
Use the `-zh.svg` variant in `v2/zh_chapters/...`. Re-run `check_figures.py`
on the edited `.md` files to confirm that the paths resolve.
## Deliverables checklist
- [ ] `<name>.excalidraw` + exported `<name>.svg|png` (and `-zh` pair if the figure has text)
- [ ] `check_figures.py` reports 0 errors for the scenes and the edited chapter files
- [ ] Markdown references the SVG/PNG, never `.pdf` or `.excalidraw`
- [ ] Commit the source and the export together
## Attribution
Adapted from the `excalidraw-diagram-generator` skill in
[github/awesome-copilot](https://github.com/github/awesome-copilot/tree/main/skills/excalidraw-diagram-generator)
(MIT, see [LICENSE.txt](LICENSE.txt)).

View File

@@ -0,0 +1,499 @@
# Excalidraw Element Types Guide
> **openmlsys note:** This file comes from github/awesome-copilot (MIT, see `../LICENSE.txt`) and is kept unchanged. Where it shows `fontFamily` 1 to 3, use `5` (Excalifont, or `3` for code) instead. House style and labels inside shapes are covered in [openmlsys-conventions.md](openmlsys-conventions.md).
Detailed specifications for each Excalidraw element type with visual examples and use cases.
## Element Type Overview
| Type | Visual | Primary Use | Text Support |
|------|--------|-------------|--------------|
| `rectangle` | □ | Boxes, containers, process steps | ✅ Yes |
| `ellipse` | ○ | Emphasis, terminals, states | ✅ Yes |
| `diamond` | ◇ | Decision points, choices | ✅ Yes |
| `arrow` | → | Directional flow, relationships | ❌ No (use separate text) |
| `line` | — | Connections, dividers | ❌ No |
| `text` | A | Labels, annotations, titles | ✅ (Its purpose) |
---
## Rectangle
**Best for:** Process steps, entities, data stores, components
### Properties
```typescript
{
type: "rectangle",
roundness: { type: 3 }, // Rounded corners
text: "Step Name", // Optional embedded text
fontSize: 20,
textAlign: "center",
verticalAlign: "middle"
}
```
### Use Cases
| Scenario | Configuration |
|----------|---------------|
| **Process step** | Green background (`#b2f2bb`), centered text |
| **Entity/Object** | Blue background (`#a5d8ff`), medium size |
| **System component** | Light color, descriptive text |
| **Data store** | Gray/white, database-like label |
### Size Guidelines
| Content | Width | Height |
|---------|-------|--------|
| Single word | 120-150px | 60-80px |
| Short phrase (2-4 words) | 180-220px | 80-100px |
| Sentence | 250-300px | 100-120px |
### Example
```json
{
"type": "rectangle",
"x": 100,
"y": 100,
"width": 200,
"height": 80,
"backgroundColor": "#b2f2bb",
"text": "Validate Input",
"fontSize": 20,
"textAlign": "center",
"verticalAlign": "middle",
"roundness": { "type": 3 }
}
```
---
## Ellipse
**Best for:** Start/end points, states, emphasis circles
### Properties
```typescript
{
type: "ellipse",
text: "Start",
fontSize: 18,
textAlign: "center",
verticalAlign: "middle"
}
```
### Use Cases
| Scenario | Configuration |
|----------|---------------|
| **Flow start** | Light green, "Start" text |
| **Flow end** | Light red, "End" text |
| **State** | Soft color, state name |
| **Highlight** | Bright color, emphasis text |
### Size Guidelines
For circular shapes, use `width === height`:
| Content | Diameter |
|---------|----------|
| Icon/Symbol | 60-80px |
| Short text | 100-120px |
| Longer text | 150-180px |
### Example
```json
{
"type": "ellipse",
"x": 100,
"y": 100,
"width": 120,
"height": 120,
"backgroundColor": "#d0f0c0",
"text": "Start",
"fontSize": 18,
"textAlign": "center",
"verticalAlign": "middle"
}
```
---
## Diamond
**Best for:** Decision points, conditional branches
### Properties
```typescript
{
type: "diamond",
text: "Valid?",
fontSize: 18,
textAlign: "center",
verticalAlign": "middle"
}
```
### Use Cases
| Scenario | Text Example |
|----------|--------------|
| **Yes/No decision** | "Is Valid?", "Exists?" |
| **Multiple choice** | "Type?", "Status?" |
| **Conditional** | "Score > 50?" |
### Size Guidelines
Diamonds need more space than rectangles for the same text:
| Content | Width | Height |
|---------|-------|--------|
| Yes/No | 120-140px | 120-140px |
| Short question | 160-180px | 160-180px |
| Longer question | 200-220px | 200-220px |
### Example
```json
{
"type": "diamond",
"x": 100,
"y": 100,
"width": 150,
"height": 150,
"backgroundColor": "#ffe4a3",
"text": "Valid?",
"fontSize": 18,
"textAlign": "center",
"verticalAlign": "middle"
}
```
---
## Arrow
**Best for:** Flow direction, relationships, dependencies
### Properties
```typescript
{
type: "arrow",
points: [[0, 0], [endX, endY]], // Relative coordinates
roundness: { type: 2 }, // Curved
startBinding: null, // Or { elementId, focus, gap }
endBinding: null
}
```
### Arrow Directions
#### Horizontal (Left to Right)
```json
{
"x": 100,
"y": 150,
"width": 200,
"height": 0,
"points": [[0, 0], [200, 0]]
}
```
#### Vertical (Top to Bottom)
```json
{
"x": 200,
"y": 100,
"width": 0,
"height": 150,
"points": [[0, 0], [0, 150]]
}
```
#### Diagonal
```json
{
"x": 100,
"y": 100,
"width": 200,
"height": 150,
"points": [[0, 0], [200, 150]]
}
```
### Arrow Styles
| Style | `strokeStyle` | `strokeWidth` | Use Case |
|-------|---------------|---------------|----------|
| **Normal flow** | `"solid"` | 2 | Standard connections |
| **Optional/Weak** | `"dashed"` | 2 | Optional paths |
| **Important** | `"solid"` | 3-4 | Emphasized flow |
| **Dotted** | `"dotted"` | 2 | Indirect relationships |
### Adding Arrow Labels
Use separate text elements positioned near arrow midpoint:
```json
[
{
"type": "arrow",
"id": "arrow1",
"x": 100,
"y": 150,
"points": [[0, 0], [200, 0]]
},
{
"type": "text",
"x": 180, // Near midpoint
"y": 130, // Above arrow
"text": "sends",
"fontSize": 14
}
]
```
---
## Line
**Best for:** Non-directional connections, dividers, borders
### Properties
```typescript
{
type: "line",
points: [[0, 0], [x2, y2], [x3, y3], ...],
roundness: null // Or { type: 2 } for curved
}
```
### Use Cases
| Scenario | Configuration |
|----------|---------------|
| **Divider** | Horizontal, thin stroke |
| **Border** | Closed path (polygon) |
| **Connection** | Multi-point path |
| **Underline** | Short horizontal line |
### Multi-Point Line Example
```json
{
"type": "line",
"x": 100,
"y": 100,
"points": [
[0, 0],
[100, 50],
[200, 0]
]
}
```
---
## Text
**Best for:** Labels, titles, annotations, standalone text
### Properties
```typescript
{
type: "text",
text: "Label text",
fontSize: 20,
fontFamily: 1, // 1=Virgil, 2=Helvetica, 3=Cascadia
textAlign: "left",
verticalAlign: "top"
}
```
### Font Sizes by Purpose
| Purpose | Font Size |
|---------|-----------|
| **Main title** | 28-36 |
| **Section header** | 24-28 |
| **Element label** | 18-22 |
| **Annotation** | 14-16 |
| **Small note** | 12-14 |
### Width/Height Calculation
```javascript
// Approximate width
const width = text.length * fontSize * 0.6;
// Approximate height (single line)
const height = fontSize * 1.2;
// Multi-line
const lines = text.split('\n').length;
const height = fontSize * 1.2 * lines;
```
### Text Positioning
| Position | textAlign | verticalAlign | Use Case |
|----------|-----------|---------------|----------|
| **Top-left** | `"left"` | `"top"` | Default labels |
| **Centered** | `"center"` | `"middle"` | Titles |
| **Bottom-right** | `"right"` | `"bottom"` | Footnotes |
### Example: Title
```json
{
"type": "text",
"x": 100,
"y": 50,
"width": 400,
"height": 40,
"text": "System Architecture",
"fontSize": 32,
"fontFamily": 2,
"textAlign": "center",
"verticalAlign": "top"
}
```
### Example: Annotation
```json
{
"type": "text",
"x": 150,
"y": 200,
"width": 100,
"height": 20,
"text": "User input",
"fontSize": 14,
"fontFamily": 1,
"textAlign": "left",
"verticalAlign": "top"
}
```
---
## Combining Elements
### Pattern: Labeled Box
```json
[
{
"type": "rectangle",
"id": "box1",
"x": 100,
"y": 100,
"width": 200,
"height": 100,
"text": "Component",
"textAlign": "center",
"verticalAlign": "middle"
}
]
```
### Pattern: Connected Boxes
```json
[
{
"type": "rectangle",
"id": "box1",
"x": 100,
"y": 100,
"width": 150,
"height": 80,
"text": "Step 1"
},
{
"type": "arrow",
"id": "arrow1",
"x": 250,
"y": 140,
"points": [[0, 0], [100, 0]]
},
{
"type": "rectangle",
"id": "box2",
"x": 350,
"y": 100,
"width": 150,
"height": 80,
"text": "Step 2"
}
]
```
### Pattern: Decision Tree
```json
[
{
"type": "diamond",
"id": "decision",
"x": 100,
"y": 100,
"width": 140,
"height": 140,
"text": "Valid?"
},
{
"type": "arrow",
"id": "yes-arrow",
"x": 240,
"y": 170,
"points": [[0, 0], [60, 0]]
},
{
"type": "text",
"id": "yes-label",
"x": 250,
"y": 150,
"text": "Yes",
"fontSize": 14
},
{
"type": "rectangle",
"id": "yes-box",
"x": 300,
"y": 140,
"width": 120,
"height": 60,
"text": "Process"
}
]
```
---
## Summary
| When you need... | Use this element |
|------------------|------------------|
| Process box | `rectangle` with text |
| Decision point | `diamond` with question |
| Flow direction | `arrow` |
| Start/End | `ellipse` |
| Title/Header | `text` (large font) |
| Annotation | `text` (small font) |
| Non-directional link | `line` |
| Divider | `line` (horizontal) |

View File

@@ -0,0 +1,352 @@
# Excalidraw JSON Schema Reference
> **openmlsys note:** This file comes from github/awesome-copilot (MIT, see `../LICENSE.txt`) and is kept unchanged. Where it shows `fontFamily` 1 to 3, use `5` (Excalifont, or `3` for code) instead. House style and labels inside shapes are covered in [openmlsys-conventions.md](openmlsys-conventions.md).
This document describes the structure of Excalidraw `.excalidraw` files for diagram generation.
## Top-Level Structure
```typescript
interface ExcalidrawFile {
type: "excalidraw";
version: number; // Always 2
source: string; // "https://excalidraw.com"
elements: ExcalidrawElement[];
appState: AppState;
files: Record<string, any>; // Usually empty {}
}
```
## AppState
```typescript
interface AppState {
viewBackgroundColor: string; // Hex color, e.g., "#ffffff"
gridSize: number; // Typically 20
}
```
## ExcalidrawElement Base Properties
All elements share these common properties:
```typescript
interface BaseElement {
id: string; // Unique identifier
type: ElementType; // See Element Types below
x: number; // X coordinate (pixels from top-left)
y: number; // Y coordinate (pixels from top-left)
width: number; // Width in pixels
height: number; // Height in pixels
angle: number; // Rotation angle in radians (usually 0)
strokeColor: string; // Hex color, e.g., "#1e1e1e"
backgroundColor: string; // Hex color or "transparent"
fillStyle: "solid" | "hachure" | "cross-hatch";
strokeWidth: number; // 1-4 typically
strokeStyle: "solid" | "dashed" | "dotted";
roughness: number; // 0-2, controls hand-drawn effect (1 = default)
opacity: number; // 0-100
groupIds: string[]; // IDs of groups this element belongs to
frameId: null; // Usually null
index: string; // Stacking order identifier
roundness: Roundness | null;
seed: number; // Random seed for deterministic rendering
version: number; // Element version (increment on edit)
versionNonce: number; // Random number changed on edit
isDeleted: boolean; // Should be false
boundElements: any; // Usually null
updated: number; // Timestamp in milliseconds
link: null; // External link (usually null)
locked: boolean; // Whether element is locked
}
```
## Element Types
### Rectangle
```typescript
interface RectangleElement extends BaseElement {
type: "rectangle";
roundness: { type: 3 }; // 3 = rounded corners
text?: string; // Optional text inside
fontSize?: number; // Font size (16-32 typical)
fontFamily?: number; // 1 = Virgil, 2 = Helvetica, 3 = Cascadia
textAlign?: "left" | "center" | "right";
verticalAlign?: "top" | "middle" | "bottom";
}
```
**Example:**
```json
{
"id": "rect1",
"type": "rectangle",
"x": 100,
"y": 100,
"width": 200,
"height": 100,
"strokeColor": "#1e1e1e",
"backgroundColor": "#a5d8ff",
"text": "My Box",
"fontSize": 20,
"textAlign": "center",
"verticalAlign": "middle",
"roundness": { "type": 3 }
}
```
### Ellipse
```typescript
interface EllipseElement extends BaseElement {
type: "ellipse";
text?: string;
fontSize?: number;
fontFamily?: number;
textAlign?: "left" | "center" | "right";
verticalAlign?: "top" | "middle" | "bottom";
}
```
### Diamond
```typescript
interface DiamondElement extends BaseElement {
type: "diamond";
text?: string;
fontSize?: number;
fontFamily?: number;
textAlign?: "left" | "center" | "right";
verticalAlign?: "top" | "middle" | "bottom";
}
```
### Arrow
```typescript
interface ArrowElement extends BaseElement {
type: "arrow";
points: [number, number][]; // Array of [x, y] coordinates relative to element
startBinding: Binding | null;
endBinding: Binding | null;
roundness: { type: 2 }; // 2 = curved arrow
}
```
**Example:**
```json
{
"id": "arrow1",
"type": "arrow",
"x": 100,
"y": 100,
"width": 200,
"height": 0,
"points": [
[0, 0],
[200, 0]
],
"roundness": { "type": 2 },
"startBinding": null,
"endBinding": null
}
```
**Points explanation:**
- First point `[0, 0]` is relative to `(x, y)`
- Subsequent points are relative to the first point
- For straight horizontal arrow: `[[0, 0], [width, 0]]`
- For straight vertical arrow: `[[0, 0], [0, height]]`
### Line
```typescript
interface LineElement extends BaseElement {
type: "line";
points: [number, number][];
startBinding: Binding | null;
endBinding: Binding | null;
roundness: { type: 2 } | null;
}
```
### Text
```typescript
interface TextElement extends BaseElement {
type: "text";
text: string;
fontSize: number;
fontFamily: number; // 1-3
textAlign: "left" | "center" | "right";
verticalAlign: "top" | "middle" | "bottom";
roundness: null; // Text has no roundness
}
```
**Example:**
```json
{
"id": "text1",
"type": "text",
"x": 100,
"y": 100,
"width": 150,
"height": 25,
"text": "Hello World",
"fontSize": 20,
"fontFamily": 1,
"textAlign": "left",
"verticalAlign": "top",
"roundness": null
}
```
**Width/Height calculation:**
- Width ≈ `text.length * fontSize * 0.6`
- Height ≈ `fontSize * 1.2 * numberOfLines`
## Bindings
Bindings connect arrows to shapes:
```typescript
interface Binding {
elementId: string; // ID of bound element
focus: number; // -1 to 1, position along edge
gap: number; // Distance from element edge
}
```
## Common Colors
| Color Name | Hex Code | Use Case |
|------------|----------|----------|
| Black | `#1e1e1e` | Default stroke |
| Light Blue | `#a5d8ff` | Primary entities |
| Light Green | `#b2f2bb` | Process steps |
| Yellow | `#ffd43b` | Important/Central |
| Light Red | `#ffc9c9` | Warnings/Errors |
| Cyan | `#96f2d7` | Secondary items |
| Transparent | `transparent` | No fill |
| White | `#ffffff` | Background |
## ID Generation
IDs should be unique strings. Common patterns:
```javascript
// Timestamp-based
const id = Date.now().toString(36) + Math.random().toString(36).substr(2);
// Sequential
const id = "element-" + counter++;
// Descriptive
const id = "step-1", "entity-user", "arrow-1-to-2";
```
## Seed Generation
Seeds are used for deterministic randomness in hand-drawn effect:
```javascript
const seed = Math.floor(Math.random() * 2147483647);
```
## Version and VersionNonce
```javascript
const version = 1; // Increment when element is edited
const versionNonce = Math.floor(Math.random() * 2147483647);
```
## Coordinate System
- Origin `(0, 0)` is top-left corner
- X increases to the right
- Y increases downward
- All units are in pixels
## Recommended Spacing
| Context | Spacing |
|---------|---------|
| Horizontal gap between elements | 200-300px |
| Vertical gap between rows | 100-150px |
| Minimum margin from edge | 50px |
| Arrow-to-box clearance | 20-30px |
## Font Families
| ID | Name | Description |
|----|------|-------------|
| 1 | Virgil | Hand-drawn style (default) |
| 2 | Helvetica | Clean sans-serif |
| 3 | Cascadia | Monospace |
## Validation Rules
✅ **Required:**
- All IDs must be unique
- `type` must match actual element type
- `version` must be an integer ≥ 1
- `opacity` must be 0-100
⚠️ **Recommended:**
- Keep `roughness` at 1 for consistency
- Use `strokeWidth` of 2 for clarity
- Set `isDeleted` to `false`
- Set `locked` to `false`
- Keep `frameId`, `boundElements`, `link` as `null`
## Complete Minimal Example
```json
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [
{
"id": "box1",
"type": "rectangle",
"x": 100,
"y": 100,
"width": 200,
"height": 100,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#a5d8ff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a0",
"roundness": { "type": 3 },
"seed": 1234567890,
"version": 1,
"versionNonce": 987654321,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"text": "Hello",
"fontSize": 20,
"fontFamily": 1,
"textAlign": "center",
"verticalAlign": "middle"
}
],
"appState": {
"viewBackgroundColor": "#ffffff",
"gridSize": 20
},
"files": {}
}
```

View File

@@ -0,0 +1,143 @@
# openmlsys figure conventions
These rules apply to every Excalidraw figure in the book. They extend
`CONTRIBUTING/style_en.md`, section "Figure Guidelines".
## Where files go
| What | Path |
|------|------|
| Image folder (physical) | `v1/img/ch<NN>/` |
| Same folder seen from a chapter | `v1/{en,zh}_chapters/img` → `../img`, `v2/{en,zh}_chapters/img` → `../../v1/img` (symlinks) |
| Chapter file (v2) | `v2/en_chapters/<NN>_chapter_<topic>/<section>.md` |
| Reference from a chapter | `../img/ch<NN>/<name>.svg` |
- `NN` is the chapter number of the chapter that uses the figure, for example
`v2/en_chapters/03_chapter_accelerator/` → `img/ch03/`. v1 and v2 share this
folder, so run `ls v1/img/ch<NN>/` first to avoid name clashes.
- Use lowercase names with hyphens: `pipeline-parallel-schedule`.
- The source and the export share a stem. Commit both:
```
v1/img/ch06/pipeline-parallel-schedule.excalidraw # editable source (EN)
v1/img/ch06/pipeline-parallel-schedule.svg # exported, used by en_chapters
v1/img/ch06/pipeline-parallel-schedule-zh.excalidraw # editable source (ZH)
v1/img/ch06/pipeline-parallel-schedule-zh.svg # exported, used by zh_chapters
```
- Do not save a source as `.json`. The repo `.gitignore` ignores `*.json`.
## Export formats
- **SVG** is the default for diagrams. Use **PNG** at 2x scale, at most
1200 px wide, only if the SVG renders badly (very large scenes, embedded
bitmaps).
- **Never use PDF.** mdBook output is HTML, and browsers do not render
`![](x.pdf)` as an image. Some old v1 pages still point at `.pdf` files.
Replace those references when you redraw the figures. Do not copy the pattern.
- Do not reference the `.excalidraw` file from Markdown.
### How to export
Use one of the following:
1. **Excalidraw MCP.** The style guide recommends the official server at
`https://mcp.excalidraw.com` or the self-hosted `interactive-drawer`. Load
the scene and export it to SVG or PNG with the settings below.
2. **excalidraw.com** (no install). Open the `.excalidraw` file, then
*Menu → Export image*. Set *Background* off, *Dark mode* off and scale 2x
for PNG, then save as SVG or PNG. Use *Menu → Save to…* to write the
`.excalidraw` file back after you edit it.
3. **VS Code "Excalidraw" extension** (`pomdtr.excalidraw-editor`). It edits
`.excalidraw` files in the editor and exports through the same dialog.
Export settings are the same for every method: light theme, no background
(the style guide asks for figures without a white background), and
`appState.exportWithDarkMode` false.
## Dark mode
The book's themes include `theme/dark-mode-images.css` (see
`tests/test_dark_mode_images_css.py`). In the dark themes `navy`, `coal` and
`ayu`, every body image gets a light gray `#e8e8e8` backdrop with 8 px
padding. Images are never inverted. A transparent figure therefore sits on
white in light mode and on `#e8e8e8` in dark mode. Consequences:
- Text and strokes must be dark. Use `#1e1e1e` by default. White or pastel
text disappears, and `check_figures.py` reports it as an error.
- Every shape needs a visible dark stroke. A very light fill (`#e9ecef`,
`#f8f9fa`) blends into the dark-mode backdrop, so do not rely on fill alone.
- Do not export with Excalidraw's "dark mode" option.
- Do not encode meaning by color alone. Add a label or a dashed/solid style
difference so the figure also works in grayscale print.
## Palette
These are Excalidraw's built-in swatches, so the figures stay editable from
the stock UI.
| Role | Fill (`backgroundColor`) | Stroke / text |
|------|--------------------------|---------------|
| Default component | `#a5d8ff` (blue) | `#1e1e1e` |
| Data / tensors | `#b2f2bb` (green) | `#1e1e1e` |
| Highlight / focus | `#ffec99` (yellow) | `#1e1e1e` |
| Hardware / device | `#d0bfff` (violet) | `#1e1e1e` |
| Error / bottleneck | `#ffc9c9` (red) | `#1e1e1e` |
| Group / region frame | `transparent`, dashed stroke | `#495057` |
| Accent arrows (optional) | – | `#1971c2`, `#2f9e44`, `#e03131`, `#f08c00` |
Keep `fillStyle: "solid"`, `strokeWidth: 2`, `roughness: 1` and
`roundness: {"type": 3}` for boxes. Use the same values across one chapter.
## Text and fonts
- Use `fontFamily: 5` (Excalifont) for all labels and `3` (Cascadia) for code
or identifiers. `1` (Virgil) is the legacy hand-drawn font and `2`
(Helvetica) depends on the reader's system fonts. Do not use either.
- Excalidraw falls back to its bundled CJK hand-drawn font (Xiaolai) for
Chinese automatically, and SVG export embeds the glyphs it uses. You do not
need a different `fontFamily` for ZH variants.
- Use `fontSize` 20 for box labels, 16 for edge labels and annotations, and
28 for an optional in-figure title. Most figures should have no title,
because the Markdown caption serves that purpose.
- For math, write plain Unicode such as `W·x + b` or `∇L`. LaTeX is not
rendered inside Excalidraw.
### Labels inside shapes (bound text)
```json
{ "id": "box1", "type": "rectangle", "...": "...",
"boundElements": [{ "type": "text", "id": "box1-label" }] },
{ "id": "box1-label", "type": "text", "containerId": "box1",
"text": "Parameter Server", "originalText": "Parameter Server",
"fontSize": 20, "fontFamily": 5, "textAlign": "center",
"verticalAlign": "middle", "lineHeight": 1.25, "autoResize": true,
"strokeColor": "#1e1e1e", "...": "..." }
```
Center the text element in its container. Estimate the width as about
0.6 × fontSize per Latin character and 1.0 × fontSize per CJK character.
Excalidraw re-measures the text when the file is opened. Arrows that connect
shapes should set `startBinding`/`endBinding` (`{"elementId": ..., "focus": 0, "gap": 4}`).
The bound shapes must list the arrow in their `boundElements`.
## Bilingual figures
- If a figure contains words, make two variants: `<name>` (English, used by
`en_chapters`) and `<name>-zh` (Chinese, used by `zh_chapters`).
- Build the ZH scene from the EN scene. Keep the ids and the geometry, and
translate only `text`/`originalText`. Chinese labels are often wider, so
widen the boxes and move neighbors symmetrically if needed.
- Keep established English acronyms (GPU, NCCL, AllReduce, KV cache) in the ZH
variant. Follow `CONTRIBUTING/terminology.md` for the translated terms.
- A figure with no words, or only symbols and math, uses one file shared by
both languages.
- Captions belong in the Markdown in each language, not in the image.
## Layout tips
- Draw main flows left to right, or top to bottom for stacks and layers.
- Leave a 40–80 px gap between boxes and use one box size per role.
- Use about 20 elements per figure at most. Split a dense figure and refer
to both parts in the text.
- Leave a margin of about 20 px around the content so the exported image
is not cropped tight.

View File

@@ -0,0 +1,317 @@
#!/usr/bin/env python3
"""
Add arrows (connections) between elements in Excalidraw diagrams.
From github/awesome-copilot (MIT, see ../LICENSE.txt); label font size raised to 16 for openmlsys.
Usage:
python add-arrow.py <diagram_path> <from_x> <from_y> <to_x> <to_y> [OPTIONS]
Options:
--style {solid|dashed|dotted} Arrow line style (default: solid)
--color HEX Arrow color (default: #1e1e1e)
--label TEXT Add text label on the arrow
--use-edit-suffix Edit via .excalidraw.edit to avoid editor overwrite issues (enabled by default; use --no-use-edit-suffix to disable)
Examples:
python add-arrow.py diagram.excalidraw 300 200 500 300
python add-arrow.py diagram.excalidraw 300 200 500 300 --label "HTTP"
python add-arrow.py diagram.excalidraw 300 200 500 300 --style dashed --color "#7950f2"
python add-arrow.py diagram.excalidraw 300 200 500 300 --use-edit-suffix
"""
import json
import sys
import uuid
from pathlib import Path
from typing import Dict, Any
def generate_unique_id() -> str:
"""Generate a unique ID for Excalidraw elements."""
return str(uuid.uuid4()).replace('-', '')[:16]
def prepare_edit_path(diagram_path: Path, use_edit_suffix: bool) -> tuple[Path, Path | None]:
"""
Prepare a safe edit path to avoid editor overwrite issues.
Returns:
(work_path, final_path)
- work_path: file path to read/write during edit
- final_path: file path to rename back to (or None if not used)
"""
if not use_edit_suffix:
return diagram_path, None
if diagram_path.suffix != ".excalidraw":
return diagram_path, None
edit_path = diagram_path.with_suffix(diagram_path.suffix + ".edit")
if diagram_path.exists():
if edit_path.exists():
raise FileExistsError(f"Edit file already exists: {edit_path}")
diagram_path.rename(edit_path)
return edit_path, diagram_path
def finalize_edit_path(work_path: Path, final_path: Path | None) -> None:
"""Finalize edit by renaming .edit back to .excalidraw if needed."""
if final_path is None:
return
if final_path.exists():
final_path.unlink()
work_path.rename(final_path)
def create_arrow(
from_x: float,
from_y: float,
to_x: float,
to_y: float,
style: str = "solid",
color: str = "#1e1e1e",
label: str = None
) -> list:
"""
Create an arrow element.
Args:
from_x: Starting X coordinate
from_y: Starting Y coordinate
to_x: Ending X coordinate
to_y: Ending Y coordinate
style: Line style (solid, dashed, dotted)
color: Arrow color
label: Optional text label on the arrow
Returns:
List of elements (arrow and optional label)
"""
elements = []
# Arrow element
arrow = {
"id": generate_unique_id(),
"type": "arrow",
"x": from_x,
"y": from_y,
"width": to_x - from_x,
"height": to_y - from_y,
"angle": 0,
"strokeColor": color,
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": style,
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": None,
"index": "a0",
"roundness": {
"type": 2
},
"seed": 1000000000 + hash(f"{from_x}{from_y}{to_x}{to_y}") % 1000000000,
"version": 1,
"versionNonce": 2000000000 + hash(f"{from_x}{from_y}{to_x}{to_y}") % 1000000000,
"isDeleted": False,
"boundElements": [],
"updated": 1738195200000,
"link": None,
"locked": False,
"points": [
[0, 0],
[to_x - from_x, to_y - from_y]
],
"startBinding": None,
"endBinding": None,
"startArrowhead": None,
"endArrowhead": "arrow",
"lastCommittedPoint": None
}
elements.append(arrow)
# Optional label
if label:
mid_x = (from_x + to_x) / 2 - (len(label) * 5)
mid_y = (from_y + to_y) / 2 - 10
label_element = {
"id": generate_unique_id(),
"type": "text",
"x": mid_x,
"y": mid_y,
"width": len(label) * 10,
"height": 20,
"angle": 0,
"strokeColor": color,
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": None,
"index": "a0",
"roundness": None,
"seed": 1000000000 + hash(label) % 1000000000,
"version": 1,
"versionNonce": 2000000000 + hash(label) % 1000000000,
"isDeleted": False,
"boundElements": [],
"updated": 1738195200000,
"link": None,
"locked": False,
"text": label,
"fontSize": 16,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "top",
"containerId": None,
"originalText": label,
"autoResize": True,
"lineHeight": 1.25
}
elements.append(label_element)
return elements
def add_arrow_to_diagram(
diagram_path: Path,
from_x: float,
from_y: float,
to_x: float,
to_y: float,
style: str = "solid",
color: str = "#1e1e1e",
label: str = None
) -> None:
"""
Add an arrow to an Excalidraw diagram.
Args:
diagram_path: Path to the Excalidraw diagram file
from_x: Starting X coordinate
from_y: Starting Y coordinate
to_x: Ending X coordinate
to_y: Ending Y coordinate
style: Line style (solid, dashed, dotted)
color: Arrow color
label: Optional text label
"""
print(f"Creating arrow from ({from_x}, {from_y}) to ({to_x}, {to_y})")
arrow_elements = create_arrow(from_x, from_y, to_x, to_y, style, color, label)
if label:
print(f" With label: '{label}'")
# Load diagram
print(f"Loading diagram: {diagram_path}")
with open(diagram_path, 'r', encoding='utf-8') as f:
diagram = json.load(f)
# Add arrow elements
if 'elements' not in diagram:
diagram['elements'] = []
original_count = len(diagram['elements'])
diagram['elements'].extend(arrow_elements)
print(f" Added {len(arrow_elements)} elements (total: {original_count} -> {len(diagram['elements'])})")
# Save diagram
print(f"Saving diagram")
with open(diagram_path, 'w', encoding='utf-8') as f:
json.dump(diagram, f, indent=2, ensure_ascii=False)
print(f"✓ Successfully added arrow to diagram")
def main():
"""Main entry point."""
if hasattr(sys.stdout, "reconfigure"):
# Ensure consistent UTF-8 output on Windows consoles.
sys.stdout.reconfigure(encoding="utf-8")
if len(sys.argv) < 6:
print("Usage: python add-arrow.py <diagram_path> <from_x> <from_y> <to_x> <to_y> [OPTIONS]")
print("\nOptions:")
print(" --style {solid|dashed|dotted} Line style (default: solid)")
print(" --color HEX Color (default: #1e1e1e)")
print(" --label TEXT Text label on arrow")
print(" --use-edit-suffix Edit via .excalidraw.edit to avoid editor overwrite issues (enabled by default; use --no-use-edit-suffix to disable)")
print("\nExamples:")
print(" python add-arrow.py diagram.excalidraw 300 200 500 300")
print(" python add-arrow.py diagram.excalidraw 300 200 500 300 --label 'HTTP'")
sys.exit(1)
diagram_path = Path(sys.argv[1])
from_x = float(sys.argv[2])
from_y = float(sys.argv[3])
to_x = float(sys.argv[4])
to_y = float(sys.argv[5])
# Parse optional arguments
style = "solid"
color = "#1e1e1e"
label = None
# Default: use edit suffix to avoid editor overwrite issues
use_edit_suffix = True
i = 6
while i < len(sys.argv):
if sys.argv[i] == '--style':
if i + 1 < len(sys.argv):
style = sys.argv[i + 1]
if style not in ['solid', 'dashed', 'dotted']:
print(f"Error: Invalid style '{style}'. Must be: solid, dashed, or dotted")
sys.exit(1)
i += 2
else:
print("Error: --style requires an argument")
sys.exit(1)
elif sys.argv[i] == '--color':
if i + 1 < len(sys.argv):
color = sys.argv[i + 1]
i += 2
else:
print("Error: --color requires an argument")
sys.exit(1)
elif sys.argv[i] == '--label':
if i + 1 < len(sys.argv):
label = sys.argv[i + 1]
i += 2
else:
print("Error: --label requires a text argument")
sys.exit(1)
elif sys.argv[i] == '--use-edit-suffix':
use_edit_suffix = True
i += 1
elif sys.argv[i] == '--no-use-edit-suffix':
use_edit_suffix = False
i += 1
else:
print(f"Error: Unknown option: {sys.argv[i]}")
sys.exit(1)
# Validate inputs
if not diagram_path.exists():
print(f"Error: Diagram file not found: {diagram_path}")
sys.exit(1)
try:
work_path, final_path = prepare_edit_path(diagram_path, use_edit_suffix)
add_arrow_to_diagram(work_path, from_x, from_y, to_x, to_y, style, color, label)
finalize_edit_path(work_path, final_path)
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,146 @@
#!/usr/bin/env python3
"""Lint Excalidraw sources and figure references for the openmlsys book.
Standard library only (Python 3.8+). No network access.
Usage:
python3 check_figures.py PATH [PATH ...]
PATH may be:
* a ``.excalidraw`` file or a directory (searched recursively) -- each scene
is checked for structural problems and house-style violations, and for an
exported ``.svg``/``.png`` sibling with the same stem;
* a ``.md`` chapter file -- every image reference is checked: it must not
point at a ``.pdf`` and the referenced file must exist.
Exit status is 1 if any ERROR was reported, otherwise 0 (WARNINGs only).
"""
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
ALLOWED_FONTS = {5, 3} # 5 = Excalifont (body text), 3 = Cascadia (code)
MIN_FONT_SIZE = 16
IMAGE_REF = re.compile(r"!\[[^\]]*\]\(([^)\s]+)(?:\s+\"[^\"]*\")?\)")
errors = 0
warnings = 0
def report(level: str, where: Path, msg: str) -> None:
global errors, warnings
if level == "ERROR":
errors += 1
else:
warnings += 1
print(f"{level}: {where}: {msg}")
def luminance(color: str) -> float | None:
m = re.fullmatch(r"#([0-9a-fA-F]{6})", color or "")
if not m:
return None
r, g, b = (int(m.group(1)[i : i + 2], 16) / 255 for i in (0, 2, 4))
return 0.2126 * r + 0.7152 * g + 0.0722 * b
def check_scene(path: Path) -> None:
try:
doc = json.loads(path.read_text(encoding="utf-8"))
except (OSError, ValueError) as exc:
report("ERROR", path, f"not valid JSON: {exc}")
return
if doc.get("type") != "excalidraw" or not isinstance(doc.get("elements"), list):
report("ERROR", path, 'expected {"type": "excalidraw", "elements": [...]}')
return
elements = [e for e in doc["elements"] if not e.get("isDeleted")]
by_id = {}
for el in elements:
eid = el.get("id")
if not eid:
report("ERROR", path, f"element without id: {el.get('type')}")
elif eid in by_id:
report("ERROR", path, f"duplicate id {eid!r}")
by_id[eid] = el
for el in elements:
eid, etype = el.get("id"), el.get("type")
tag = f"{etype} {eid!r}"
if etype != "text" and "text" in el:
report("ERROR", path, f"{tag} has a 'text' property; Excalidraw ignores it. "
"Use a separate text element with containerId (see references/openmlsys-conventions.md)")
if etype == "text":
if el.get("fontFamily") not in ALLOWED_FONTS:
report("WARNING", path, f"{tag} uses fontFamily {el.get('fontFamily')}; house style is 5 (or 3 for code)")
if (el.get("fontSize") or 0) < MIN_FONT_SIZE:
report("WARNING", path, f"{tag} fontSize {el.get('fontSize')} < {MIN_FONT_SIZE}; may be illegible when scaled")
lum = luminance(el.get("strokeColor", ""))
if lum is not None and lum > 0.6:
report("ERROR", path, f"{tag} text color {el['strokeColor']} is too light for the light page / #e8e8e8 dark-mode backdrop")
cid = el.get("containerId")
if cid:
container = by_id.get(cid)
if container is None:
report("ERROR", path, f"{tag} containerId {cid!r} does not exist")
elif not any(b.get("id") == eid for b in container.get("boundElements") or []):
report("ERROR", path, f"container {cid!r} does not list text {eid!r} in boundElements")
for b in el.get("boundElements") or []:
if b.get("id") not in by_id:
report("ERROR", path, f"{tag} boundElements references missing {b.get('id')!r}")
for key in ("startBinding", "endBinding"):
binding = el.get(key)
if binding and binding.get("elementId") not in by_id:
report("ERROR", path, f"{tag} {key} references missing {binding.get('elementId')!r}")
if etype in ("arrow", "line") and len(el.get("points") or []) < 2:
report("ERROR", path, f"{tag} needs at least two points")
app = doc.get("appState") or {}
if app.get("exportWithDarkMode"):
report("WARNING", path, "exportWithDarkMode is on; export in light mode (the site handles dark mode)")
stem = path.name[: -len(".excalidraw")]
if not any((path.parent / f"{stem}{ext}").exists() for ext in (".svg", ".png")):
report("WARNING", path, f"no exported {stem}.svg or {stem}.png next to the source")
def check_markdown(path: Path) -> None:
text = path.read_text(encoding="utf-8")
for lineno, line in enumerate(text.splitlines(), 1):
for target in IMAGE_REF.findall(line):
if re.match(r"[a-z]+://", target):
continue
where = f"{path}:{lineno}"
if target.lower().endswith(".pdf"):
report("ERROR", Path(where), f"{target} is a PDF; browsers cannot show it in mdBook, export SVG/PNG instead")
if target.endswith(".excalidraw"):
report("ERROR", Path(where), f"{target} is an Excalidraw source; reference the exported SVG/PNG")
if not (path.parent / target).exists():
report("ERROR", Path(where), f"{target} does not resolve relative to the chapter file")
def main(argv: list[str]) -> int:
if not argv or argv[0] in ("-h", "--help"):
print(__doc__)
return 0 if argv else 2
for arg in argv:
p = Path(arg)
if p.is_dir():
for scene in sorted(p.rglob("*.excalidraw")):
check_scene(scene)
elif p.suffix == ".excalidraw":
check_scene(p)
elif p.suffix == ".md":
check_markdown(p)
else:
report("ERROR", p, "not a directory, .excalidraw or .md file")
print(f"{errors} error(s), {warnings} warning(s)")
return 1 if errors else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))

View File

@@ -0,0 +1,414 @@
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [
{
"id": "external-entity-1",
"type": "rectangle",
"x": 100,
"y": 200,
"width": 120,
"height": 80,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffc9c9",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a0",
"roundness": {
"type": 3
},
"seed": 1001001001,
"version": 1,
"versionNonce": 2002002002,
"isDeleted": false,
"boundElements": [
{
"type": "text",
"id": "external-entity-1-label"
}
],
"updated": 1706659200000,
"link": null,
"locked": false
},
{
"id": "external-entity-1-label",
"type": "text",
"x": 138.4,
"y": 228.75,
"width": 43.199999999999996,
"height": 22.5,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a0V",
"roundness": null,
"seed": 1001001008,
"version": 1,
"versionNonce": 2002002009,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"text": "User",
"fontSize": 18,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "external-entity-1",
"originalText": "User",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "data-flow-1",
"type": "arrow",
"x": 220,
"y": 240,
"width": 80,
"height": 0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a1",
"roundness": {
"type": 2
},
"seed": 1001001002,
"version": 1,
"versionNonce": 2002002003,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"points": [
[
0,
0
],
[
80,
0
]
],
"startBinding": null,
"endBinding": null
},
{
"id": "flow-label-1",
"type": "text",
"x": 230,
"y": 220,
"width": 80,
"height": 20,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a2",
"roundness": null,
"seed": 1001001003,
"version": 1,
"versionNonce": 2002002004,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"text": "input data",
"fontSize": 16,
"fontFamily": 5,
"textAlign": "left",
"verticalAlign": "top"
},
{
"id": "process-1",
"type": "ellipse",
"x": 300,
"y": 200,
"width": 120,
"height": 80,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#a5d8ff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a3",
"roundness": null,
"seed": 1001001004,
"version": 1,
"versionNonce": 2002002005,
"isDeleted": false,
"boundElements": [
{
"type": "text",
"id": "process-1-label"
}
],
"updated": 1706659200000,
"link": null,
"locked": false
},
{
"id": "process-1-label",
"type": "text",
"x": 326.4,
"y": 220.0,
"width": 67.2,
"height": 40.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a3V",
"roundness": null,
"seed": 1001001011,
"version": 1,
"versionNonce": 2002002012,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"text": "Process\nData",
"fontSize": 16,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "process-1",
"originalText": "Process\nData",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "data-flow-2",
"type": "arrow",
"x": 420,
"y": 240,
"width": 80,
"height": 0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a4",
"roundness": {
"type": 2
},
"seed": 1001001005,
"version": 1,
"versionNonce": 2002002006,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"points": [
[
0,
0
],
[
80,
0
]
],
"startBinding": null,
"endBinding": null
},
{
"id": "flow-label-2",
"type": "text",
"x": 425,
"y": 220,
"width": 100,
"height": 20,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a5",
"roundness": null,
"seed": 1001001006,
"version": 1,
"versionNonce": 2002002007,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"text": "processed data",
"fontSize": 16,
"fontFamily": 5,
"textAlign": "left",
"verticalAlign": "top"
},
{
"id": "data-store-1",
"type": "rectangle",
"x": 500,
"y": 200,
"width": 150,
"height": 80,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#96f2d7",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a6",
"roundness": null,
"seed": 1001001007,
"version": 1,
"versionNonce": 2002002008,
"isDeleted": false,
"boundElements": [
{
"type": "text",
"id": "data-store-1-label"
}
],
"updated": 1706659200000,
"link": null,
"locked": false
},
{
"id": "data-store-1-label",
"type": "text",
"x": 527.0,
"y": 220.0,
"width": 96.0,
"height": 40.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a6V",
"roundness": null,
"seed": 1001001014,
"version": 1,
"versionNonce": 2002002015,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"text": "Data Store\n(Database)",
"fontSize": 16,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "data-store-1",
"originalText": "Data Store\n(Database)",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "data-store-line",
"type": "line",
"x": 500,
"y": 225,
"width": 150,
"height": 0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a7",
"roundness": null,
"seed": 1001001008,
"version": 1,
"versionNonce": 2002002009,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"points": [
[
0,
0
],
[
150,
0
]
],
"startBinding": null,
"endBinding": null
}
],
"appState": {
"viewBackgroundColor": "#ffffff",
"gridSize": 20
},
"files": {}
}

View File

@@ -0,0 +1,312 @@
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [
{
"id": "step1",
"type": "rectangle",
"x": 400,
"y": 200,
"width": 200,
"height": 80,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#b2f2bb",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a0",
"roundness": {
"type": 3
},
"seed": 1234567890,
"version": 1,
"versionNonce": 987654321,
"isDeleted": false,
"boundElements": [
{
"type": "text",
"id": "step1-label"
}
],
"updated": 1706659200000,
"link": null,
"locked": false
},
{
"id": "step1-label",
"type": "text",
"x": 464.0,
"y": 227.5,
"width": 72.0,
"height": 25.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a0V",
"roundness": null,
"seed": 1234567897,
"version": 1,
"versionNonce": 987654328,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"text": "Step 1",
"fontSize": 20,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "step1",
"originalText": "Step 1",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "arrow1",
"type": "arrow",
"x": 500,
"y": 280,
"width": 0,
"height": 100,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a1",
"roundness": {
"type": 2
},
"seed": 1234567891,
"version": 1,
"versionNonce": 987654322,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"points": [
[
0,
0
],
[
0,
100
]
],
"startBinding": null,
"endBinding": null
},
{
"id": "step2",
"type": "rectangle",
"x": 400,
"y": 380,
"width": 200,
"height": 80,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#b2f2bb",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a2",
"roundness": {
"type": 3
},
"seed": 1234567892,
"version": 1,
"versionNonce": 987654323,
"isDeleted": false,
"boundElements": [
{
"type": "text",
"id": "step2-label"
}
],
"updated": 1706659200000,
"link": null,
"locked": false
},
{
"id": "step2-label",
"type": "text",
"x": 464.0,
"y": 407.5,
"width": 72.0,
"height": 25.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a2V",
"roundness": null,
"seed": 1234567899,
"version": 1,
"versionNonce": 987654330,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"text": "Step 2",
"fontSize": 20,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "step2",
"originalText": "Step 2",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "arrow2",
"type": "arrow",
"x": 500,
"y": 460,
"width": 0,
"height": 100,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a3",
"roundness": {
"type": 2
},
"seed": 1234567893,
"version": 1,
"versionNonce": 987654324,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"points": [
[
0,
0
],
[
0,
100
]
],
"startBinding": null,
"endBinding": null
},
{
"id": "step3",
"type": "rectangle",
"x": 400,
"y": 560,
"width": 200,
"height": 80,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#b2f2bb",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a4",
"roundness": {
"type": 3
},
"seed": 1234567894,
"version": 1,
"versionNonce": 987654325,
"isDeleted": false,
"boundElements": [
{
"type": "text",
"id": "step3-label"
}
],
"updated": 1706659200000,
"link": null,
"locked": false
},
{
"id": "step3-label",
"type": "text",
"x": 464.0,
"y": 587.5,
"width": 72.0,
"height": 25.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a4V",
"roundness": null,
"seed": 1234567901,
"version": 1,
"versionNonce": 987654332,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"text": "Step 3",
"fontSize": 20,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "step3",
"originalText": "Step 3",
"autoResize": true,
"lineHeight": 1.25
}
],
"appState": {
"viewBackgroundColor": "#ffffff",
"gridSize": 20
},
"files": {}
}

View File

@@ -0,0 +1,352 @@
{
"type": "excalidraw",
"version": 2,
"source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor",
"elements": [
{
"id": "center",
"type": "rectangle",
"x": 500,
"y": 350,
"width": 200,
"height": 100,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffd43b",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a0",
"roundness": {
"type": 3
},
"seed": 3333333333,
"version": 3,
"versionNonce": 641024845,
"isDeleted": false,
"boundElements": [
{
"id": "arrow1",
"type": "arrow"
},
{
"id": "arrow2",
"type": "arrow"
},
{
"type": "text",
"id": "center-label"
}
],
"updated": 1769755916717,
"link": null,
"locked": false
},
{
"id": "center-label",
"type": "text",
"x": 522.0,
"y": 387.5,
"width": 156.0,
"height": 25.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a0V",
"roundness": null,
"seed": 3333333340,
"version": 1,
"versionNonce": 641024852,
"isDeleted": false,
"boundElements": null,
"updated": 1769755916717,
"link": null,
"locked": false,
"text": "Central Topic",
"fontSize": 20,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "center",
"originalText": "Central Topic",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "branch1",
"type": "rectangle",
"x": 250,
"y": 150,
"width": 150,
"height": 80,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#96f2d7",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a1",
"roundness": {
"type": 3
},
"seed": 3333333334,
"version": 2,
"versionNonce": 2040232045,
"isDeleted": false,
"boundElements": [
{
"id": "arrow1",
"type": "arrow"
},
{
"type": "text",
"id": "branch1-label"
}
],
"updated": 1769755912840,
"link": null,
"locked": false
},
{
"id": "branch1-label",
"type": "text",
"x": 281.8,
"y": 178.75,
"width": 86.39999999999999,
"height": 22.5,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a1V",
"roundness": null,
"seed": 3333333341,
"version": 1,
"versionNonce": 2040232052,
"isDeleted": false,
"boundElements": null,
"updated": 1769755912840,
"link": null,
"locked": false,
"text": "Branch 1",
"fontSize": 18,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "branch1",
"originalText": "Branch 1",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "arrow1",
"type": "arrow",
"x": 600,
"y": 350,
"width": 246.39999389648438,
"height": 111.20001220703125,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a2",
"roundness": {
"type": 2
},
"seed": 3333333335,
"version": 23,
"versionNonce": 308894189,
"isDeleted": false,
"boundElements": [],
"updated": 1769755914127,
"link": null,
"locked": false,
"points": [
[
0,
0
],
[
-246.39999389648438,
-111.20001220703125
]
],
"startBinding": {
"elementId": "center",
"focus": 0.5255972360761778,
"gap": 1
},
"endBinding": {
"elementId": "branch1",
"focus": 0.48604063201707415,
"gap": 8.79998779296875
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow"
},
{
"id": "branch2",
"type": "rectangle",
"x": 750,
"y": 150,
"width": 150,
"height": 80,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#96f2d7",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a3",
"roundness": {
"type": 3
},
"seed": 3333333336,
"version": 2,
"versionNonce": 1459929741,
"isDeleted": false,
"boundElements": [
{
"id": "arrow2",
"type": "arrow"
},
{
"type": "text",
"id": "branch2-label"
}
],
"updated": 1769755916716,
"link": null,
"locked": false
},
{
"id": "branch2-label",
"type": "text",
"x": 781.8,
"y": 178.75,
"width": 86.39999999999999,
"height": 22.5,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a3V",
"roundness": null,
"seed": 3333333343,
"version": 1,
"versionNonce": 1459929748,
"isDeleted": false,
"boundElements": null,
"updated": 1769755916716,
"link": null,
"locked": false,
"text": "Branch 2",
"fontSize": 18,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "branch2",
"originalText": "Branch 2",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "arrow2",
"type": "arrow",
"x": 600,
"y": 350,
"width": 216,
"height": 112.80001831054688,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a4",
"roundness": {
"type": 2
},
"seed": 3333333337,
"version": 41,
"versionNonce": 1447859213,
"isDeleted": false,
"boundElements": [],
"updated": 1769756030188,
"link": null,
"locked": false,
"points": [
[
0,
0
],
[
216,
-112.80001831054688
]
],
"startBinding": {
"elementId": "center",
"focus": -0.48913039421990545,
"gap": 1
},
"endBinding": {
"elementId": "branch2",
"focus": -0.5368418212214556,
"gap": 7.199981689453125
},
"lastCommittedPoint": null,
"startArrowhead": null,
"endArrowhead": "arrow"
}
],
"appState": {
"gridSize": 20,
"gridStep": 5,
"gridModeEnabled": false,
"viewBackgroundColor": "#ffffff"
},
"files": {}
}

View File

@@ -0,0 +1,231 @@
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [
{
"id": "entity1",
"type": "rectangle",
"x": 300,
"y": 300,
"width": 180,
"height": 100,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#a5d8ff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a0",
"roundness": {
"type": 3
},
"seed": 1111111111,
"version": 1,
"versionNonce": 2222222222,
"isDeleted": false,
"boundElements": [
{
"type": "text",
"id": "entity1-label"
}
],
"updated": 1706659200000,
"link": null,
"locked": false
},
{
"id": "entity1-label",
"type": "text",
"x": 342.0,
"y": 337.5,
"width": 96.0,
"height": 25.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a0V",
"roundness": null,
"seed": 1111111118,
"version": 1,
"versionNonce": 2222222229,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"text": "Entity A",
"fontSize": 20,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "entity1",
"originalText": "Entity A",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "entity2",
"type": "rectangle",
"x": 600,
"y": 300,
"width": 180,
"height": 100,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#a5d8ff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a1",
"roundness": {
"type": 3
},
"seed": 1111111112,
"version": 1,
"versionNonce": 2222222223,
"isDeleted": false,
"boundElements": [
{
"type": "text",
"id": "entity2-label"
}
],
"updated": 1706659200000,
"link": null,
"locked": false
},
{
"id": "entity2-label",
"type": "text",
"x": 642.0,
"y": 337.5,
"width": 96.0,
"height": 25.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a1V",
"roundness": null,
"seed": 1111111119,
"version": 1,
"versionNonce": 2222222230,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"text": "Entity B",
"fontSize": 20,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "entity2",
"originalText": "Entity B",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "relationship",
"type": "arrow",
"x": 480,
"y": 350,
"width": 120,
"height": 0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a2",
"roundness": {
"type": 2
},
"seed": 1111111113,
"version": 1,
"versionNonce": 2222222224,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"points": [
[
0,
0
],
[
120,
0
]
],
"startBinding": null,
"endBinding": null
},
{
"id": "label",
"type": "text",
"x": 510,
"y": 325,
"width": 60,
"height": 24,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a3",
"roundness": null,
"seed": 1111111114,
"version": 1,
"versionNonce": 2222222225,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"text": "relates to",
"fontSize": 16,
"fontFamily": 5,
"textAlign": "left",
"verticalAlign": "top"
}
],
"appState": {
"viewBackgroundColor": "#ffffff",
"gridSize": 20
},
"files": {}
}

View File

@@ -0,0 +1,670 @@
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [
{
"id": "object-1",
"type": "rectangle",
"x": 150,
"y": 100,
"width": 120,
"height": 50,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#e7f5ff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a0",
"roundness": null,
"seed": 4001001001,
"version": 1,
"versionNonce": 5002002001,
"isDeleted": false,
"boundElements": [
{
"type": "text",
"id": "object-1-label"
}
],
"updated": 1706659200000,
"link": null,
"locked": false
},
{
"id": "object-1-label",
"type": "text",
"x": 177.6,
"y": 113.75,
"width": 64.8,
"height": 22.5,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a0V",
"roundness": null,
"seed": 4001001008,
"version": 1,
"versionNonce": 5002002008,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"text": "Client",
"fontSize": 18,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "object-1",
"originalText": "Client",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "lifeline-1",
"type": "line",
"x": 210,
"y": 150,
"width": 0,
"height": 300,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "dashed",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a1",
"roundness": null,
"seed": 4001001002,
"version": 1,
"versionNonce": 5002002002,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"points": [
[
0,
0
],
[
0,
300
]
],
"startBinding": null,
"endBinding": null
},
{
"id": "object-2",
"type": "rectangle",
"x": 350,
"y": 100,
"width": 120,
"height": 50,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#fff3bf",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a2",
"roundness": null,
"seed": 4001001003,
"version": 1,
"versionNonce": 5002002003,
"isDeleted": false,
"boundElements": [
{
"type": "text",
"id": "object-2-label"
}
],
"updated": 1706659200000,
"link": null,
"locked": false
},
{
"id": "object-2-label",
"type": "text",
"x": 377.6,
"y": 113.75,
"width": 64.8,
"height": 22.5,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a2V",
"roundness": null,
"seed": 4001001010,
"version": 1,
"versionNonce": 5002002010,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"text": "Server",
"fontSize": 18,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "object-2",
"originalText": "Server",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "lifeline-2",
"type": "line",
"x": 410,
"y": 150,
"width": 0,
"height": 300,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "dashed",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a3",
"roundness": null,
"seed": 4001001004,
"version": 1,
"versionNonce": 5002002004,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"points": [
[
0,
0
],
[
0,
300
]
],
"startBinding": null,
"endBinding": null
},
{
"id": "object-3",
"type": "rectangle",
"x": 550,
"y": 100,
"width": 120,
"height": 50,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#d0f0c0",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a4",
"roundness": null,
"seed": 4001001005,
"version": 1,
"versionNonce": 5002002005,
"isDeleted": false,
"boundElements": [
{
"type": "text",
"id": "object-3-label"
}
],
"updated": 1706659200000,
"link": null,
"locked": false
},
{
"id": "object-3-label",
"type": "text",
"x": 566.8,
"y": 113.75,
"width": 86.39999999999999,
"height": 22.5,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a4V",
"roundness": null,
"seed": 4001001012,
"version": 1,
"versionNonce": 5002002012,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"text": "Database",
"fontSize": 18,
"fontFamily": 5,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "object-3",
"originalText": "Database",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "lifeline-3",
"type": "line",
"x": 610,
"y": 150,
"width": 0,
"height": 300,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "dashed",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a5",
"roundness": null,
"seed": 4001001006,
"version": 1,
"versionNonce": 5002002006,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"points": [
[
0,
0
],
[
0,
300
]
],
"startBinding": null,
"endBinding": null
},
{
"id": "message-1",
"type": "arrow",
"x": 210,
"y": 200,
"width": 200,
"height": 0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a6",
"roundness": {
"type": 2
},
"seed": 4001001007,
"version": 1,
"versionNonce": 5002002007,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"points": [
[
0,
0
],
[
200,
0
]
],
"startBinding": null,
"endBinding": null
},
{
"id": "message-label-1",
"type": "text",
"x": 250,
"y": 180,
"width": 120,
"height": 20,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a7",
"roundness": null,
"seed": 4001001008,
"version": 1,
"versionNonce": 5002002008,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"text": "1: request()",
"fontSize": 16,
"fontFamily": 5,
"textAlign": "left",
"verticalAlign": "top"
},
{
"id": "activation-1",
"type": "rectangle",
"x": 405,
"y": 200,
"width": 10,
"height": 80,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffd43b",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a8",
"roundness": null,
"seed": 4001001009,
"version": 1,
"versionNonce": 5002002009,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false
},
{
"id": "message-2",
"type": "arrow",
"x": 415,
"y": 230,
"width": 195,
"height": 0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a9",
"roundness": {
"type": 2
},
"seed": 4001001010,
"version": 1,
"versionNonce": 5002002010,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"points": [
[
0,
0
],
[
195,
0
]
],
"startBinding": null,
"endBinding": null
},
{
"id": "message-label-2",
"type": "text",
"x": 450,
"y": 210,
"width": 120,
"height": 20,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a10",
"roundness": null,
"seed": 4001001011,
"version": 1,
"versionNonce": 5002002011,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"text": "2: query()",
"fontSize": 16,
"fontFamily": 5,
"textAlign": "left",
"verticalAlign": "top"
},
{
"id": "return-message-1",
"type": "arrow",
"x": 610,
"y": 250,
"width": 195,
"height": 0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "dashed",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a11",
"roundness": {
"type": 2
},
"seed": 4001001012,
"version": 1,
"versionNonce": 5002002012,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"points": [
[
0,
0
],
[
-195,
0
]
],
"startBinding": null,
"endBinding": null
},
{
"id": "return-label-1",
"type": "text",
"x": 450,
"y": 255,
"width": 120,
"height": 20,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a12",
"roundness": null,
"seed": 4001001013,
"version": 1,
"versionNonce": 5002002013,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"text": "3: result",
"fontSize": 16,
"fontFamily": 5,
"textAlign": "left",
"verticalAlign": "top"
},
{
"id": "return-message-2",
"type": "arrow",
"x": 410,
"y": 280,
"width": 200,
"height": 0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "dashed",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a13",
"roundness": {
"type": 2
},
"seed": 4001001014,
"version": 1,
"versionNonce": 5002002014,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"points": [
[
0,
0
],
[
-200,
0
]
],
"startBinding": null,
"endBinding": null
},
{
"id": "return-label-2",
"type": "text",
"x": 250,
"y": 285,
"width": 120,
"height": 20,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": "a14",
"roundness": null,
"seed": 4001001015,
"version": 1,
"versionNonce": 5002002015,
"isDeleted": false,
"boundElements": null,
"updated": 1706659200000,
"link": null,
"locked": false,
"text": "4: response",
"fontSize": 16,
"fontFamily": 5,
"textAlign": "left",
"verticalAlign": "top"
}
],
"appState": {
"viewBackgroundColor": "#ffffff",
"gridSize": 20
},
"files": {}
}

View File

@@ -78,6 +78,27 @@ After modifying source files under `zh_chapters/` or `en_chapters/`, the browser
For detailed custom syntax (math, figure labels, citations, code blocks) and figure guidelines, refer to the **[Writing Style Guide](style_en.md)**.
### Drawing Figures with AI Agents (Excalidraw Skill)
The repository includes a project-level agent skill,
[`.claude/skills/excalidraw-diagram/`](../.claude/skills/excalidraw-diagram/SKILL.md).
`.agents/skills/excalidraw-diagram` is a symlink to the same directory.
Claude Code, and other agents that support the Agent Skills format, load it
automatically when you ask them to draw a figure for the book.
The skill tells the agent to:
- keep the editable `.excalidraw` source next to the exported SVG/PNG in `v1/img/ch<NN>/`
- make separate `-zh` variants for figures that contain text
- follow a palette that stays legible in dark mode
- never reference PDF images
Lint figures and chapter references before you commit:
```bash
python3 .claude/skills/excalidraw-diagram/scripts/check_figures.py v1/img/ch06 v2/en_chapters/06_chapter_training_systems/index.md
```
---
## Submission Workflow

View File

@@ -78,6 +78,26 @@ cd v2/config/en_config && mdbook serve --open
详细的自定义语法(公式、图片标签、文献引用、代码块等)及图片规范,请参阅 **[写作样式规范](style_zh.md)**。
### 使用 AI Agent 绘图(Excalidraw Skill)
仓库内置了项目级 Agent Skill
[`.claude/skills/excalidraw-diagram/`](../.claude/skills/excalidraw-diagram/SKILL.md)。
`.agents/skills/excalidraw-diagram` 是指向同一目录的符号链接。
让 Claude Code 或其他支持 Agent Skills 格式的 Agent 为本书绘图时,它们会自动加载该 Skill。
该 Skill 要求 Agent:
- 将可编辑的 `.excalidraw` 源文件与导出的 SVG/PNG 一起放在 `v1/img/ch<NN>/`
- 为含文字的图另做 `-zh` 中文版本
- 使用在暗色模式下同样清晰的配色
- 不引用 PDF 图片
提交前可运行以下命令检查图片源文件与章节中的引用:
```bash
python3 .claude/skills/excalidraw-diagram/scripts/check_figures.py v1/img/ch06 v2/zh_chapters/06_chapter_training_systems/index.md
```
---
## 提交流程