gridquill 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. gridquill-0.1.1/CHANGELOG.md +5 -0
  2. gridquill-0.1.1/LICENSE +21 -0
  3. gridquill-0.1.1/MANIFEST.in +3 -0
  4. gridquill-0.1.1/PKG-INFO +164 -0
  5. gridquill-0.1.1/README.md +138 -0
  6. gridquill-0.1.1/examples/__init__.py +1 -0
  7. gridquill-0.1.1/examples/canvas_background_example.py +32 -0
  8. gridquill-0.1.1/examples/canvas_scope_example.py +42 -0
  9. gridquill-0.1.1/examples/draw_chevrons_example.py +15 -0
  10. gridquill-0.1.1/examples/draw_connectors_example.py +17 -0
  11. gridquill-0.1.1/examples/draw_diagram_example.py +21 -0
  12. gridquill-0.1.1/examples/draw_objects_example.py +24 -0
  13. gridquill-0.1.1/examples/recreate_udt_overview.py +139 -0
  14. gridquill-0.1.1/examples/software_data_path.py +323 -0
  15. gridquill-0.1.1/pyproject.toml +40 -0
  16. gridquill-0.1.1/setup.cfg +4 -0
  17. gridquill-0.1.1/src/gridquill/__init__.py +18 -0
  18. gridquill-0.1.1/src/gridquill/model.py +846 -0
  19. gridquill-0.1.1/src/gridquill/normalize.py +145 -0
  20. gridquill-0.1.1/src/gridquill/reactive.py +254 -0
  21. gridquill-0.1.1/src/gridquill/render.py +128 -0
  22. gridquill-0.1.1/src/gridquill.egg-info/PKG-INFO +164 -0
  23. gridquill-0.1.1/src/gridquill.egg-info/SOURCES.txt +29 -0
  24. gridquill-0.1.1/src/gridquill.egg-info/dependency_links.txt +1 -0
  25. gridquill-0.1.1/src/gridquill.egg-info/entry_points.txt +2 -0
  26. gridquill-0.1.1/src/gridquill.egg-info/requires.txt +5 -0
  27. gridquill-0.1.1/src/gridquill.egg-info/top_level.txt +1 -0
  28. gridquill-0.1.1/tests/__init__.py +0 -0
  29. gridquill-0.1.1/tests/test_auto_connectors.py +100 -0
  30. gridquill-0.1.1/tests/test_gridquill.py +846 -0
  31. gridquill-0.1.1/tests/test_layers.py +109 -0
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 — 2026-10-26
4
+
5
+ Initial extraction of the Gridquill diagram library, examples and regression tests.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 rpeschke
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ include README.md CHANGELOG.md LICENSE
2
+ recursive-include examples *.py
3
+ recursive-include tests *.py
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: gridquill
3
+ Version: 0.1.1
4
+ Summary: Reactive grid-based diagrams with nested canvases and editable Excalidraw output
5
+ License-Expression: MIT
6
+ Keywords: diagrams,excalidraw,reactive,grid,visualization
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Multimedia :: Graphics
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Provides-Extra: dev
22
+ Requires-Dist: build>=1.2; extra == "dev"
23
+ Requires-Dist: setuptools>=77.0.3; extra == "dev"
24
+ Requires-Dist: twine>=6; extra == "dev"
25
+ Dynamic: license-file
26
+
27
+ # Gridquill
28
+
29
+ Build editable diagrams in Python using reactive coordinates, nested canvases,
30
+ elements, signals, captions and connectors. Export to `.excalidraw` for Excalidraw
31
+ or `.excalidraw.md` for Obsidian. Python 3.10+; no runtime dependencies.
32
+
33
+ ## Install from this checkout
34
+
35
+ ```console
36
+ python -m pip install -e .
37
+ ```
38
+
39
+ The package is prepared for PyPI but has not been published.
40
+
41
+ ## Draw a diagram
42
+
43
+ ```python
44
+ import gridquill as gq
45
+
46
+ gq.set_color_scheme(
47
+ element_color=0x1971C2, element_fill=0xA5D8FF,
48
+ signal_color=0xF08C00, signal_fill=0xFFEC99,
49
+ )
50
+
51
+ with gq.canvas("Example") as diagram:
52
+ with gq.canvas("Acquisition", column=2, row=1,
53
+ background=0xEDF2FF, margin_left=20, margin_right=20,
54
+ margin_top=20, margin_bottom=20):
55
+ adc = gq.element("ADC", column=0, top=0, length=4)
56
+ fpga = gq.element("FPGA", column=4, top=0, length=4)
57
+ samples = gq.signal("Samples", row=adc.port(2), components=[adc])
58
+ samples.components.append(fpga)
59
+ gq.connector(fpga, samples, inout="in")
60
+ gq.caption("Acquisition", row=-1, column=0)
61
+
62
+ diagram.save("example.excalidraw.md")
63
+ ```
64
+
65
+ ## Coordinates and ownership
66
+
67
+ Literal coordinates are local to their canvas. On save, coordinate references
68
+ resolve to positions in the drawing, including all parent offsets. A captured
69
+ reference such as `fpga.column` follows later moves and assignments. Sizes and
70
+ lengths are distances; coordinate formulas return drawing coordinates.
71
+
72
+ ```python
73
+ fpga.column = lambda: adc.column + 5
74
+ diagram.resolve() # resolve without writing a file
75
+ ```
76
+
77
+ Geometry fields and span entries accept numbers, reactive arithmetic or
78
+ zero-argument functions. Each save evaluates them in passes. Pending dependencies
79
+ read as NaN; stalled resolution raises `ReactiveResolutionError` naming the
80
+ unresolved fields. Functions must be pure. `float(value)` reads a snapshot of the
81
+ last resolved value. Failed resolution leaves an existing output file untouched.
82
+
83
+ Creating a canvas opens it. Subsequent objects attach automatically; `pop()`
84
+ returns to the previous canvas. `with canvas(...)` restores the previous scope
85
+ on exit. Use `canvas(activate=False)` for explicit ownership and `append()`.
86
+
87
+ ## Objects
88
+
89
+ - `element(label, column=..., top=..., bottom=... / length=...)`: vertical box.
90
+ Alternatively use `span=[top, bottom]`. `element.port(index)` returns a reactive
91
+ row, with 0.5 grid-unit spacing; negative indices count from the bottom.
92
+ - `signal(label, row=..., left=..., right=... / length=...)`: horizontal box.
93
+ Alternatively use `span=[left, right]` or a mutable `components` list whose
94
+ outermost columns are connected. Endpoint formulas are prepared at save time.
95
+ - `caption(label, row=..., column=..., fill=..., length=...)`: automatically sized
96
+ label box; omit `length` to fit the text on each save.
97
+ - `connector(element_or_column, signal_or_row, position="auto", inout="out")`:
98
+ chevron marker. With a signal, the default chooses `left` or `right` when its
99
+ span lies entirely on that side of the element's column, including a signal
100
+ ending at the element. The side and arrow direction follow layout changes on
101
+ every save. A signal crossing both sides, or a numeric row, defaults to `left`.
102
+ Set `position="left"` or `"right"` to override. Flow is `in`, `out` or `inout`.
103
+ - `chevron(...)`: individual direction stencil.
104
+ - `canvas(...)`: group with an optional background, border style and per-side
105
+ margins in pixels. Bounds follow its members automatically.
106
+
107
+ Default grid size is 80 pixels. `set_color_scheme()` changes global defaults;
108
+ explicit object colors take precedence. Assign `None` to inherit again.
109
+ `get_color_scheme()` returns a copy; `reset_color_scheme()` restores defaults.
110
+
111
+ `draw_diagram()` provides the lower-level renderer. To normalize existing
112
+ drawings, run `gridquill-normalize drawing.excalidraw.md` (edits the input file).
113
+
114
+ ## Drawing order
115
+
116
+ Set `z_index` to control overlaps. Higher values appear in front, and each box
117
+ and its label stay together:
118
+
119
+ ```python
120
+ with gq.canvas() as diagram:
121
+ block = gq.element("FPGA", column=2, span=[0, 4])
122
+ bus = gq.signal("Bus", row=2, span=[0, 4], z_index=1)
123
+
124
+ bus.z_index = 2 # editable after creation; applied on the next save
125
+ diagram.save("layers.excalidraw")
126
+ ```
127
+
128
+ Signals, elements, chevrons, captions, connectors and canvases accept `z_index`
129
+ (default `0`). Negative and fractional values are allowed, as are reactive
130
+ formulas. A canvas adds its `z_index` to its descendants' values, so a connector
131
+ or a nested canvas can shift all its members together. Layers compare across
132
+ canvas boundaries. Canvas backgrounds remain behind all drawing objects.
133
+
134
+ Equal values preserve the existing order: signals (including captions), then
135
+ elements, then chevrons, in traversal order within each kind. Existing diagrams
136
+ therefore keep their drawing order. For `draw_diagram()`, set `z_index` in each
137
+ horizontal or vertical item dictionary; connection arrows remain behind boxes.
138
+
139
+ ## Examples and development
140
+
141
+ ```console
142
+ python -m pip install -e ".[dev]"
143
+ python -m unittest discover -s tests
144
+ python examples/canvas_scope_example.py
145
+ python examples/recreate_udt_overview.py --output overview.excalidraw.md
146
+ python -m build
147
+ python -m twine check dist/*
148
+ ```
149
+
150
+ Source lives in `src/gridquill`; tests and runnable examples live in `tests` and
151
+ `examples`. The firmware-specific CSV layout exporter remains in the firmware
152
+ repository because it depends on the separate `fmake` toolchain.
153
+
154
+ ## Publishing
155
+
156
+ Add author details and actual project URLs to `pyproject.toml`.
157
+ Set `__version__` in `src/gridquill/__init__.py` for a release.
158
+ Build and validate the distributions using the commands above. When ready,
159
+ upload explicitly with `python -m twine upload dist/*` using your PyPI account.
160
+ No publishing credentials or upload automation are included.
161
+
162
+ ## License
163
+
164
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,138 @@
1
+ # Gridquill
2
+
3
+ Build editable diagrams in Python using reactive coordinates, nested canvases,
4
+ elements, signals, captions and connectors. Export to `.excalidraw` for Excalidraw
5
+ or `.excalidraw.md` for Obsidian. Python 3.10+; no runtime dependencies.
6
+
7
+ ## Install from this checkout
8
+
9
+ ```console
10
+ python -m pip install -e .
11
+ ```
12
+
13
+ The package is prepared for PyPI but has not been published.
14
+
15
+ ## Draw a diagram
16
+
17
+ ```python
18
+ import gridquill as gq
19
+
20
+ gq.set_color_scheme(
21
+ element_color=0x1971C2, element_fill=0xA5D8FF,
22
+ signal_color=0xF08C00, signal_fill=0xFFEC99,
23
+ )
24
+
25
+ with gq.canvas("Example") as diagram:
26
+ with gq.canvas("Acquisition", column=2, row=1,
27
+ background=0xEDF2FF, margin_left=20, margin_right=20,
28
+ margin_top=20, margin_bottom=20):
29
+ adc = gq.element("ADC", column=0, top=0, length=4)
30
+ fpga = gq.element("FPGA", column=4, top=0, length=4)
31
+ samples = gq.signal("Samples", row=adc.port(2), components=[adc])
32
+ samples.components.append(fpga)
33
+ gq.connector(fpga, samples, inout="in")
34
+ gq.caption("Acquisition", row=-1, column=0)
35
+
36
+ diagram.save("example.excalidraw.md")
37
+ ```
38
+
39
+ ## Coordinates and ownership
40
+
41
+ Literal coordinates are local to their canvas. On save, coordinate references
42
+ resolve to positions in the drawing, including all parent offsets. A captured
43
+ reference such as `fpga.column` follows later moves and assignments. Sizes and
44
+ lengths are distances; coordinate formulas return drawing coordinates.
45
+
46
+ ```python
47
+ fpga.column = lambda: adc.column + 5
48
+ diagram.resolve() # resolve without writing a file
49
+ ```
50
+
51
+ Geometry fields and span entries accept numbers, reactive arithmetic or
52
+ zero-argument functions. Each save evaluates them in passes. Pending dependencies
53
+ read as NaN; stalled resolution raises `ReactiveResolutionError` naming the
54
+ unresolved fields. Functions must be pure. `float(value)` reads a snapshot of the
55
+ last resolved value. Failed resolution leaves an existing output file untouched.
56
+
57
+ Creating a canvas opens it. Subsequent objects attach automatically; `pop()`
58
+ returns to the previous canvas. `with canvas(...)` restores the previous scope
59
+ on exit. Use `canvas(activate=False)` for explicit ownership and `append()`.
60
+
61
+ ## Objects
62
+
63
+ - `element(label, column=..., top=..., bottom=... / length=...)`: vertical box.
64
+ Alternatively use `span=[top, bottom]`. `element.port(index)` returns a reactive
65
+ row, with 0.5 grid-unit spacing; negative indices count from the bottom.
66
+ - `signal(label, row=..., left=..., right=... / length=...)`: horizontal box.
67
+ Alternatively use `span=[left, right]` or a mutable `components` list whose
68
+ outermost columns are connected. Endpoint formulas are prepared at save time.
69
+ - `caption(label, row=..., column=..., fill=..., length=...)`: automatically sized
70
+ label box; omit `length` to fit the text on each save.
71
+ - `connector(element_or_column, signal_or_row, position="auto", inout="out")`:
72
+ chevron marker. With a signal, the default chooses `left` or `right` when its
73
+ span lies entirely on that side of the element's column, including a signal
74
+ ending at the element. The side and arrow direction follow layout changes on
75
+ every save. A signal crossing both sides, or a numeric row, defaults to `left`.
76
+ Set `position="left"` or `"right"` to override. Flow is `in`, `out` or `inout`.
77
+ - `chevron(...)`: individual direction stencil.
78
+ - `canvas(...)`: group with an optional background, border style and per-side
79
+ margins in pixels. Bounds follow its members automatically.
80
+
81
+ Default grid size is 80 pixels. `set_color_scheme()` changes global defaults;
82
+ explicit object colors take precedence. Assign `None` to inherit again.
83
+ `get_color_scheme()` returns a copy; `reset_color_scheme()` restores defaults.
84
+
85
+ `draw_diagram()` provides the lower-level renderer. To normalize existing
86
+ drawings, run `gridquill-normalize drawing.excalidraw.md` (edits the input file).
87
+
88
+ ## Drawing order
89
+
90
+ Set `z_index` to control overlaps. Higher values appear in front, and each box
91
+ and its label stay together:
92
+
93
+ ```python
94
+ with gq.canvas() as diagram:
95
+ block = gq.element("FPGA", column=2, span=[0, 4])
96
+ bus = gq.signal("Bus", row=2, span=[0, 4], z_index=1)
97
+
98
+ bus.z_index = 2 # editable after creation; applied on the next save
99
+ diagram.save("layers.excalidraw")
100
+ ```
101
+
102
+ Signals, elements, chevrons, captions, connectors and canvases accept `z_index`
103
+ (default `0`). Negative and fractional values are allowed, as are reactive
104
+ formulas. A canvas adds its `z_index` to its descendants' values, so a connector
105
+ or a nested canvas can shift all its members together. Layers compare across
106
+ canvas boundaries. Canvas backgrounds remain behind all drawing objects.
107
+
108
+ Equal values preserve the existing order: signals (including captions), then
109
+ elements, then chevrons, in traversal order within each kind. Existing diagrams
110
+ therefore keep their drawing order. For `draw_diagram()`, set `z_index` in each
111
+ horizontal or vertical item dictionary; connection arrows remain behind boxes.
112
+
113
+ ## Examples and development
114
+
115
+ ```console
116
+ python -m pip install -e ".[dev]"
117
+ python -m unittest discover -s tests
118
+ python examples/canvas_scope_example.py
119
+ python examples/recreate_udt_overview.py --output overview.excalidraw.md
120
+ python -m build
121
+ python -m twine check dist/*
122
+ ```
123
+
124
+ Source lives in `src/gridquill`; tests and runnable examples live in `tests` and
125
+ `examples`. The firmware-specific CSV layout exporter remains in the firmware
126
+ repository because it depends on the separate `fmake` toolchain.
127
+
128
+ ## Publishing
129
+
130
+ Add author details and actual project URLs to `pyproject.toml`.
131
+ Set `__version__` in `src/gridquill/__init__.py` for a release.
132
+ Build and validate the distributions using the commands above. When ready,
133
+ upload explicitly with `python -m twine upload dist/*` using your PyPI account.
134
+ No publishing credentials or upload automation are included.
135
+
136
+ ## License
137
+
138
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1 @@
1
+ """Runnable Gridquill examples; install the package before running them."""
@@ -0,0 +1,32 @@
1
+ """Run python canvas_background_example.py to generate an editable example."""
2
+
3
+ from pathlib import Path
4
+
5
+ import gridquill
6
+
7
+
8
+ if __name__ == "__main__":
9
+ # The root stays invisible. Each nested canvas opts into its own style.
10
+ diagram = gridquill.canvas(activate=False)
11
+ acquisition = diagram.append(gridquill.canvas(
12
+ "acquisition", column=1, row=1,
13
+ background=0xEDF2FF, border_color=0x1971C2, border_style="dashed",
14
+ margin_left=30, margin_right=40, margin_top=20, margin_bottom=25,
15
+ activate=False,
16
+ ))
17
+ acquisition.append(gridquill.element("ADC", column=0, span=[0, 4]))
18
+ acquisition.append(gridquill.signal("Samples", row=2, span=[0, 4]))
19
+ acquisition.append(gridquill.element("Processing", column=4, span=[0, 4]))
20
+
21
+ group = acquisition.append(gridquill.canvas(
22
+ "control", column=0, row=6,
23
+ background=0xFFF9DB, border_color=0xE67700, border_style="dotted",
24
+ margin_left=15, margin_right=15, margin_top=15, margin_bottom=15,
25
+ activate=False,
26
+ ))
27
+ group.append(gridquill.signal("Configuration", row=0, span=[0, 4]))
28
+
29
+ # Resizing, moving or removing members changes the bounds on the next save.
30
+ output = Path(__file__).parent / "build" / "canvas_background_example.excalidraw"
31
+ output.parent.mkdir(exist_ok=True)
32
+ print(diagram.save(output))
@@ -0,0 +1,42 @@
1
+ """Run py canvas_scope_example.py for automatic canvas ownership examples."""
2
+
3
+ from pathlib import Path
4
+
5
+ import gridquill
6
+
7
+
8
+ def build_diagram():
9
+ with gridquill.canvas("diagram") as diagram:
10
+ # A constructor opens a child canvas; pop returns to diagram.
11
+ acquisition = gridquill.canvas(
12
+ "acquisition", column=1, row=1, background=0xEDF2FF,
13
+ border_color=0x1971C2, margin_left=20, margin_right=20,
14
+ margin_top=20, margin_bottom=20,
15
+ )
16
+ gridquill.element("ADC", column=0, span=[0, 4])
17
+ gridquill.signal("Samples", row=2, span=[0, 4])
18
+ gridquill.connector(column=4, row=2, position="left", inout="in")
19
+ gridquill.element("Processing", column=4, span=[0, 4])
20
+ gridquill.pop()
21
+
22
+ # A with block automatically pops, including on an exception.
23
+ with gridquill.canvas(
24
+ "control", column=1, row=7, background=0xFFF9DB,
25
+ border_color=0xE67700, border_style="dashed",
26
+ margin_left=20, margin_right=20, margin_top=20, margin_bottom=20,
27
+ ) as control:
28
+ gridquill.signal("Configuration", row=0, span=[0, 4])
29
+
30
+ assert gridquill.current_canvas() is diagram
31
+ assert acquisition.parent is diagram and control.parent is diagram
32
+
33
+ # No active canvas now: this object remains unassigned and is not exported.
34
+ unassigned = gridquill.element("not in the diagram")
35
+ assert unassigned.parent is None
36
+ return diagram
37
+
38
+
39
+ if __name__ == "__main__":
40
+ output = Path(__file__).parent / "build" / "canvas_scope_example.excalidraw"
41
+ output.parent.mkdir(exist_ok=True)
42
+ print(build_diagram().save(output))
@@ -0,0 +1,15 @@
1
+ """Run py draw_chevrons_example.py to preview the UDT chevron stencil."""
2
+
3
+ from pathlib import Path
4
+
5
+ import gridquill
6
+
7
+
8
+ if __name__ == "__main__":
9
+ diagram = gridquill.canvas(activate=False)
10
+ section = diagram.append(gridquill.canvas("chevron examples", column=1, row=1, activate=False))
11
+ for row, direction in enumerate(("right", "left", "up", "down")):
12
+ section.append(gridquill.signal(direction, row=row, span=[0, 3]))
13
+ section.append(gridquill.chevron(
14
+ direction, column=3.25, row=row, direction=direction))
15
+ print(diagram.save(Path(__file__).with_name("chevrons_example.excalidraw.md")))
@@ -0,0 +1,17 @@
1
+ """Run py draw_connectors_example.py to preview left/right in/out/inout ports."""
2
+
3
+ from pathlib import Path
4
+
5
+ import gridquill
6
+
7
+
8
+ if __name__ == "__main__":
9
+ diagram = gridquill.canvas(activate=False)
10
+ diagram.append(gridquill.element("element at column 4", column=4, span=[0, 4]))
11
+ for row, flow in enumerate(("in", "out", "inout"), start=1):
12
+ diagram.append(gridquill.signal("left: " + flow, row=row, span=[0, 4]))
13
+ diagram.append(gridquill.signal("right: " + flow, row=row, span=[4, 8]))
14
+ for side in ("left", "right"):
15
+ diagram.append(gridquill.connector(column=4, row=row,
16
+ position=side, inout=flow))
17
+ print(diagram.save(Path(__file__).with_name("connectors_example.excalidraw.md")))
@@ -0,0 +1,21 @@
1
+ """Run `python draw_diagram_example.py`, then open the generated drawing."""
2
+
3
+ from gridquill.render import draw_diagram
4
+
5
+
6
+ if __name__ == "__main__":
7
+ output = draw_diagram(
8
+ "udt_example.excalidraw.md",
9
+ horizontal=[
10
+ {"text": "Lemo", "x": 20, "y": 60, "length": 240},
11
+ {"text": "RF waveform", "x": 300, "y": 60, "length": 240},
12
+ {"text": "HV Pulse", "x": 300, "y": 200, "length": 240},
13
+ ],
14
+ vertical=[
15
+ {"text": "Ultrasound Probe", "x": 0, "y": 0, "length": 300},
16
+ {"text": "IO decoupler", "x": 280, "y": 0, "length": 300},
17
+ {"text": "ADC", "x": 560, "y": 0, "length": 140},
18
+ {"text": "HV pulse generator", "x": 560, "y": 160, "length": 280},
19
+ ],
20
+ )
21
+ print(output)
@@ -0,0 +1,24 @@
1
+ """Run python draw_objects_example.py to generate an editable nested diagram."""
2
+
3
+ import gridquill
4
+
5
+
6
+ if __name__ == "__main__":
7
+ canvas = gridquill.canvas(activate=False)
8
+ canvas.signal_gap = 4
9
+ signal = gridquill.signal()
10
+ signal.row = 5
11
+ signal.span = [2, 4]
12
+ signal.label = "signal 1"
13
+ canvas.append(signal)
14
+
15
+
16
+ channel = gridquill.canvas("channel", activate=False)
17
+ channel.append(gridquill.element("ADC", column=0, span=[0, 4]))
18
+ channel.append(gridquill.signal("RF waveform", row=1, span=[0, 3]))
19
+ channel.row = 5
20
+ channel.column = 2
21
+ canvas.append(channel)
22
+
23
+ # Children stay relative to channel.column and channel.row.
24
+ print(canvas.save("objects_example.excalidraw.md"))
@@ -0,0 +1,139 @@
1
+ """Recreate UDT_overview using only the public Gridquill library objects.
2
+
3
+ Run: py recreate_udt_overview.py
4
+ The original drawing is not read or modified by this generator. The layout
5
+ preserves its labels (including original spelling), two staggered planes,
6
+ probe banks, shared PC/backplane and signal directions. Standalone captions
7
+ become signal boxes because the library currently has no text-only object.
8
+
9
+ Canvas ownership uses with blocks and automatic attachment; helpers do not
10
+ need a parent parameter or append calls. Each scope restores its parent.
11
+ All positions/spans are grid units. Background margins are pixels.
12
+ """
13
+
14
+ import argparse
15
+ from pathlib import Path
16
+ import gridquill
17
+
18
+
19
+ COLOR_SCHEME = dict(
20
+ element_color=0x1971C2,
21
+ element_fill=0xA5D8FF,
22
+ signal_color=0xF08C00,
23
+ signal_fill=0xFFEC99,
24
+ chevron_color=0x1971C2,
25
+ chevron_fill=0xA5D8FF,
26
+ text_color=0x1E1E1E,
27
+ )
28
+ PANEL_BORDER = 0x228BE6
29
+ PINK = 0xFFC9C9
30
+ GREEN = 0xB2F2BB
31
+
32
+
33
+ def probe_bank(label, column, row, decoupler_column):
34
+ """Four staggered symbols represent the original bank labelled 8 Probes."""
35
+ with gridquill.canvas(label, column=column, row=row) as bank:
36
+ gridquill.caption("8 Probes", row=-1.55, column=-10.7, fill=0xFFFFFF)
37
+ for i in range(4):
38
+ slope = 0.35
39
+ x = gridquill.element("Ultra sound Probe", column=-10 + i*slope,
40
+ top=-1.10 + i*slope, length=4)
41
+ gridquill.signal("Lemo", left= x.column, right= decoupler_column, row=0.27 + i*slope)
42
+ return bank
43
+
44
+
45
+
46
+ def plane(label, column, row, fill, fpga = None) -> gridquill.canvas:
47
+ margins=(50, 50, 20, 20)
48
+ fpga = [0, 0] if fpga is None else fpga
49
+ with gridquill.canvas(
50
+ label, column=column, row=row, background=fill,
51
+ border_color=PANEL_BORDER, border_style="solid",
52
+ margin_left=margins[0], margin_right=margins[1],
53
+ margin_top=margins[2], margin_bottom=margins[3],
54
+ ) as result:
55
+ gridquill.caption(label, row=-1.5, column=-5., fill=fill)
56
+ IO_decopler = gridquill.element("IO decopler", column=-4.5, top=-1.0, bottom=2)
57
+ gain = gridquill.element("Gain", column=-1.5, top=-1.5, bottom=0)
58
+ ADC = gridquill.element("ADC", column=2, top=-.5, bottom=1.5)
59
+ HV = gridquill.element("HV pusle generator", column=1, top=0.5, length=3)
60
+ temp = gridquill.element("Temp. sensor", column=0., top=2.5, bottom=4.5)
61
+ fpga = gridquill.element("FPGA", column=5, top=gain.span[0]+fpga[0],
62
+ bottom=temp.span[1]+fpga[1])
63
+
64
+ gridquill.signal( "Gain Control", row=gain.port(0), components=[gain , fpga] )
65
+ gridquill.signal( "RF waveform", row=IO_decopler.port(0), components=[ IO_decopler , gain])
66
+ gridquill.signal( "RF waveform", row=gain.port(2), components=[ gain , ADC])
67
+ gridquill.signal( "RF waveform", row=ADC.port(-1), components=[ ADC , fpga ])
68
+
69
+ gridquill.signal( "HV Pulse", row=HV.port(-4), components=[ IO_decopler , HV ])
70
+ gridquill.signal( "Pulse", row=HV.port(-1), components=[ HV , fpga ])
71
+ gridquill.signal( "Temperatur", row=temp.port(-1), components=[ temp , fpga ])
72
+
73
+
74
+ return result
75
+
76
+ def build_overview():
77
+ # A standalone root keeps the caller's active canvas and drawing separate.
78
+ # The with block opens it; all nested helpers attach to the active scope.
79
+ with gridquill.canvas(
80
+ "UDT overview", grid_size=80, height=40, font_size=20,
81
+ signal_gap=2, activate=False,
82
+ ) as diagram:
83
+ pl0 = plane(
84
+ "Top Plane",
85
+ column=0,
86
+ row=0,
87
+ fill=PINK,
88
+ fpga= [ 0, 4]
89
+ )
90
+
91
+ pl1 = plane(
92
+ "Bottom Plane",
93
+ column=-2,
94
+ row=7.8,
95
+ fill=GREEN,
96
+ fpga= [-1, 0]
97
+ )
98
+
99
+
100
+ probe_bank("Top probe bank", column=0, row=-0.5, decoupler_column=pl0["IO decopler"].column)
101
+ probe_bank("Bottom probe bank", column=-2, row=7.5, decoupler_column=pl1["IO decopler"].column)
102
+
103
+ # Shared components remain outside the plane canvases, as in the reference.
104
+ pc = gridquill.element("Embedded PC", column=10, top=2, bottom=5.8)
105
+ Backplane = gridquill.element("Backplane", column=8, top=6.5, bottom=12.30)
106
+ gridquill.signal("DMA", row=pl1["FPGA"].port(0), components=[pc,pl1["FPGA"] , pl0["FPGA"]])
107
+
108
+ eth = gridquill.signal("Ethernet", left= lambda:pc.column, right= 15.0, row= 2.25)
109
+ gridquill.connector(15.65, eth, "left", "inout")
110
+
111
+ # PIN has one direction marker at each FPGA, preserving both directions.
112
+ PIN = gridquill.signal("PIN", row=pl0["FPGA"].port(-1), components= [pl1["FPGA"] , pl0["FPGA"] ] )
113
+ gridquill.connector(pl1["FPGA"], PIN, "right", "in")
114
+ gridquill.connector(pl0["FPGA"] , PIN, "left", "in")
115
+
116
+ gridquill.signal("Trigger IO", row=Backplane.port(0), components= [Backplane ,pl0["FPGA"] ] )
117
+ gridquill.signal("Position Encoder", row=Backplane.port(1.5), components= [Backplane ,pl0["FPGA"] ] )
118
+
119
+
120
+ gridquill.signal("Trigger IO", row=Backplane.port(5.7), components= [Backplane ,pl1["FPGA"] ] )
121
+
122
+ gridquill.signal("Position Encoder", row=Backplane.port(7.2), components= [Backplane ,pl1["FPGA"] ] )
123
+
124
+
125
+ gridquill.signal("UDT_OptEingang", row=Backplane.port(-3), left= Backplane.column, right= 15.15 )
126
+
127
+ gridquill.signal("UDT_Output", row=Backplane.port(-1), left= Backplane.column, right= 15.15 )
128
+
129
+ return diagram
130
+
131
+
132
+ if __name__ == "__main__":
133
+ parser = argparse.ArgumentParser(description=__doc__)
134
+ parser.add_argument("--output", type=Path,
135
+ default=Path("UDT_overview_recreated.excalidraw.md"))
136
+ args = parser.parse_args()
137
+ gridquill.set_color_scheme(**COLOR_SCHEME)
138
+ print(build_overview().save(args.output))
139
+