mirror of
https://github.com/openmlsys/openmlsys-zh.git
synced 2026-10-29 03:24:10 +08:00
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:
1
.agents/skills/excalidraw-diagram
Symbolic link
1
.agents/skills/excalidraw-diagram
Symbolic link
@@ -0,0 +1 @@
|
||||
../../.claude/skills/excalidraw-diagram
|
||||
35
.claude/skills/excalidraw-diagram/LICENSE.txt
Normal file
35
.claude/skills/excalidraw-diagram/LICENSE.txt
Normal 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.
|
||||
69
.claude/skills/excalidraw-diagram/SKILL.md
Normal file
69
.claude/skills/excalidraw-diagram/SKILL.md
Normal 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
|
||||

|
||||
: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)).
|
||||
499
.claude/skills/excalidraw-diagram/references/element-types.md
Normal file
499
.claude/skills/excalidraw-diagram/references/element-types.md
Normal 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) |
|
||||
@@ -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": {}
|
||||
}
|
||||
```
|
||||
@@ -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
|
||||
`` 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.
|
||||
317
.claude/skills/excalidraw-diagram/scripts/add-arrow.py
Executable file
317
.claude/skills/excalidraw-diagram/scripts/add-arrow.py
Executable 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()
|
||||
146
.claude/skills/excalidraw-diagram/scripts/check_figures.py
Executable file
146
.claude/skills/excalidraw-diagram/scripts/check_figures.py
Executable 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:]))
|
||||
@@ -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": {}
|
||||
}
|
||||
@@ -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": {}
|
||||
}
|
||||
@@ -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": {}
|
||||
}
|
||||
@@ -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": {}
|
||||
}
|
||||
@@ -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": {}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 提交流程
|
||||
|
||||
Reference in New Issue
Block a user