beamz9000 0.1.0__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.
@@ -0,0 +1,230 @@
1
+ Metadata-Version: 2.4
2
+ Name: beamz9000
3
+ Version: 0.1.0
4
+ Summary: Beam free-body diagrams from a simple YAML scheme, drawn through shapely geometry.
5
+ Author: Connor Ferster
6
+ Author-email: Connor Ferster <connor@structuralpython.com>
7
+ License-Expression: Apache-2.0
8
+ Requires-Dist: forallpeople>=3.4.0
9
+ Requires-Dist: matplotlib>=3.11
10
+ Requires-Dist: pydantic>=2.10
11
+ Requires-Dist: pyyaml>=6.0
12
+ Requires-Dist: shapely>=2.1
13
+ Requires-Dist: pycba-tools>=0.1.0 ; extra == 'pycba'
14
+ Requires-Dist: pynite-tools>=0.8.1 ; extra == 'pynite'
15
+ Requires-Dist: reportlab>=4.2 ; extra == 'reportlab'
16
+ Requires-Dist: ymprint>=0.12.0 ; python_full_version >= '3.14' and extra == 'ymprint'
17
+ Requires-Python: >=3.12
18
+ Project-URL: Home, https://github.com/StructuralPython/beamz9000
19
+ Project-URL: Documentation, https://beamz9000.structuralpython.com
20
+ Provides-Extra: pycba
21
+ Provides-Extra: pynite
22
+ Provides-Extra: reportlab
23
+ Provides-Extra: ymprint
24
+ Description-Content-Type: text/markdown
25
+
26
+ # beamz9000
27
+
28
+ Beam free-body diagrams from a simple YAML scheme, drawn through shapely geometry.
29
+
30
+ **Documentation:** https://beamz9000.structuralpython.com
31
+
32
+ ```python
33
+ import beamz9000 as bz
34
+
35
+ bz.draw("examples/beam_scheme.yaml", "B1.png") # or .pdf, .svg
36
+ ```
37
+
38
+ ## How it works
39
+
40
+ ```
41
+ scheme YAML ──► scheme.Beam ──► spec.BeamSpec ──► graphics elements ──► renderer
42
+ (pydantic, (engine-neutral (shapely geometry (matplotlib:
43
+ validated, SI floats, legend + style mixins, png/pdf/svg;
44
+ locations keys, labels) Text, Group) native SVG)
45
+ resolved)
46
+ ```
47
+
48
+ - **shapely is the intermediate type.** Supports, arrows, load regions and
49
+ dimension ticks are `Polygon`s and `LineString`s built by small pure
50
+ functions in `graphics/glyphs.py` (the decomposition and proportions follow
51
+ fbd_plotting). Each glyph is drawn at the origin, then oriented and placed.
52
+ - **Style lives next to the geometry**, never inside a backend. `Shape`,
53
+ `Text` and `Group` (`graphics/elements.py`) are frozen pydantic models built
54
+ from `Stroke` (colour, opacity, width, line type), `Fill` (colour, opacity,
55
+ hatch) and `Font` mixins, plus z-order, legend key and role. `translate`,
56
+ `rotate`, `scale`, `mirror_x/y` and `restyle` return new elements. A whole
57
+ diagram serializes to JSON and reads back equal.
58
+ - **Text stays text.** Labels are drawn as native, searchable text (matplotlib
59
+ Text artists with TrueType embedded in PDF, `<text>` in SVG). Layout
60
+ measures each label with libraqm shaping (bundled with matplotlib ≥ 3.11),
61
+ so the space it reserves is the space the label is drawn in.
62
+ - **Strata, top to bottom:** load bands (one per load case, occupancy, or
63
+ "Factored", with overlapping loads packed into rows), the beam, supports,
64
+ dimensions, and a legend to the right. Sizes are fractions of the beam length
65
+ (`diagram.Layout`), and the palette is colour-blind safe (Okabe-Ito).
66
+
67
+ ## The scheme format
68
+
69
+ A scheme file maps beam ids to beams. Write plain numbers in any consistent
70
+ unit system, or name a [forallpeople](https://github.com/connorferster/forallpeople)
71
+ environment and write quantities:
72
+
73
+ ```yaml
74
+ _environment: structural # optional; enables "4500 mm", "2.5 kN/m", ...
75
+
76
+ B1:
77
+ spans: [4500 mm, 2500 mm]
78
+ cantilever: right # left | right | both
79
+ e: 200 GPa # optional; or `ei:`
80
+ i: 300e6 mm**4
81
+ depth: 450 mm # optional; drawn as a rectangle to scale
82
+ node_labels: auto # optional; A, B, C ... or a list like [A, B, null]
83
+ supports: [pin, roller, free] # optional override (see below)
84
+ loads:
85
+ - point: {p: 15 kN, span_loc: [1, 1250 mm], case: L}
86
+ - point: {mag: 10 kN, loc: -500 mm} # no case: already factored
87
+ - dist: {mag: 2.5 kN/m, case: D} # no locations: whole beam
88
+ - dist: {mag1: 1 kN/m, mag2: 4 kN/m, x1: 1000 mm, span_loc2: [0, S], case: L}
89
+ - pressure: {mag: residential, trib_width1: 3 m, trib_width2: 1.5 m, span_loc1: [1, 0 mm], loc2: L}
90
+ - moment: {mag: 5 kN*m, loc: 0 mm}
91
+ occupancies:
92
+ - residential: {D: 1.5 kPa, L: 1.9 kPa}
93
+ load_combos:
94
+ - ULS1: {name: D, factor: 1.4}
95
+ - ULS2: {D: 1.25, L: 1.5}
96
+ results:
97
+ diagrams: [moment, shear, deflection]
98
+ ```
99
+
100
+ ### Locations
101
+
102
+ | Written | Means |
103
+ |---|---|
104
+ | `loc: 2000` (alias `x`) | 2000 from the left end |
105
+ | `loc: -500` | 500 back from the right end (L − 500) |
106
+ | `loc: L` | the right end |
107
+ | `span_loc: [1, 1250]` | 1250 into span 1 (spans count from 0) |
108
+ | `span_loc: [1, -250]` | 250 back from the end of span 1 |
109
+ | `span_loc: [0, S]` | the end of span 0 |
110
+ | `span_loc: 0` | the end of span 0 (a bare index) |
111
+
112
+ A `dist` or `pressure` load takes `loc1`/`x1`/`span_loc1` and
113
+ `loc2`/`x2`/`span_loc2`. Leaving out the start means the left end, and leaving
114
+ out the end means the right end.
115
+
116
+ ### Loads
117
+
118
+ | Type | Magnitude | Notes |
119
+ |---|---|---|
120
+ | `point` | `mag` (aliases `magnitude`, `p`) | Positive acts downward. `angle` (alias `alpha`), in degrees from vertical between −90 and 90, tilts the arrow: positive turns its tail counter-clockwise about its tip. |
121
+ | `moment` | `mag` (aliases `magnitude`, `m`) | Positive is counter-clockwise. |
122
+ | `dist` | `mag` (aliases `magnitude`, `w`) for a UDL, or `mag1` → `mag2` | Linear between the two. |
123
+ | `pressure` | `mag` (aliases `magnitude`, `q`, `occupancy`) × `trib_width1` → `trib_width2` | `trib_width`/`trib` gives a uniform width. `mag` may name an occupancy: the load then stands for one pressure per case in it, or only `case` if given. |
124
+
125
+ Every load takes an optional `case` (with no case, the load is already factored)
126
+ and an optional `label` that replaces the drawn magnitude. Loads may also be
127
+ written flat, as `{type: point, p: 15, loc: 2000}`.
128
+
129
+ ### Supports
130
+
131
+ Without `supports`, the first supported node is pinned and the rest are
132
+ rollers, with cantilevered ends left free. A single span with one cantilevered
133
+ end is fixed at the other end. `supports` overrides this, either with one
134
+ support per node or as `{node index: support}` for just the nodes that differ.
135
+ If `cantilever` is also given, `supports` must leave that end free.
136
+
137
+ | Type | Aliases | Drawn as |
138
+ |---|---|---|
139
+ | `pin` | `pinned`, `hinge` | A triangle on the ground |
140
+ | `roller` | | A circle on the ground |
141
+ | `fixed` | `fix` | A block. At a beam end it becomes a wall covering the beam's depth. |
142
+ | `spring` | `v_spring`, `vertical_spring` | A zigzag spring to the ground |
143
+ | `rotational_spring` | `m_spring`, `rot_spring` | A pin with a spiral |
144
+ | `free` | `none` | Nothing |
145
+
146
+ A spring can take a stiffness, which is drawn beside it. Write it as
147
+ `{spring: 12 kN/mm}` or `{type: spring, k: 12 kN/mm}`. A `spring` takes a force
148
+ per length, and a `rotational_spring` a moment per radian (written as a moment,
149
+ e.g. `8000 kN*m`).
150
+
151
+ ```yaml
152
+ supports:
153
+ - rotational_spring: 8000 kN*m
154
+ - roller
155
+ - spring: 12 kN/mm
156
+ - free
157
+ ```
158
+
159
+ ### Drawing
160
+
161
+ - Labels show quantities as they were written: `12 kN/mm` stays "12 kN/mm"
162
+ rather than forallpeople's "12 MN/m". `*` is drawn as a middle dot and `**n`
163
+ as a superscript (`5 kN*m` → "5 kN·m"). Computed values, such as the overall
164
+ length, use forallpeople's units.
165
+ - Load labels show the magnitude without a sign, because the arrow shows the
166
+ direction. The zero end of a triangular load has no label.
167
+ - `node_labels` are drawn in bold on one line under the lowest support.
168
+ - See `examples/drawing_features.yaml` for depth, springs, angled loads and
169
+ node labels together.
170
+
171
+ ### Fitting to a size
172
+
173
+ A renderer with a `height` outputs exactly `width × height`, and `draw`
174
+ arranges the diagram to fill it. The load strata stretch within
175
+ `Layout.stretch`, and the legend moves on top of the loads when that draws the
176
+ beam larger:
177
+
178
+ ```python
179
+ bz.draw("beams.yaml", "B1.png", renderer=bz.MatplotlibRenderer(width=6.5, height=2.5))
180
+ diagram = bz.beam_diagram("beams.yaml", aspect=6.5 / 2.5) # without a renderer
181
+ ```
182
+
183
+ With `Layout(font_size_pt=8)`, text is 8 pt in the output whatever the beam's
184
+ length, and symbols scale with it.
185
+
186
+ ### ymprint block and ReportLab
187
+
188
+ `uv add "beamz9000[ymprint]"` adds a `_beam` block for
189
+ [ymprint](https://ymprint.structuralpython.com) reports
190
+ (`ym convert report.yml` renders it, or call `register(app)` in your own
191
+ application). `beamz9000.render.reportlab.ReportLabRenderer` draws a
192
+ diagram into any ReportLab document as vector graphics with searchable text.
193
+
194
+ ### Occupancies, combinations, results
195
+
196
+ - `occupancies` and `load_combos` accept a list of `- name: {...}` items or a
197
+ plain mapping.
198
+ - A combination is `{name: D, factor: 1.4}`, a list of those, or
199
+ `{D: 1.25, L: 1.5}`.
200
+ - `results` analyses the beam with pycba-tools or pynite-tools (the `pycba`
201
+ and `pynite` extras): list any of `forces`, `shear`, `moment`,
202
+ `deflection(s)`, `rotation(s)`, or write `{diagrams: [...], engine: pynite}`.
203
+ The diagram then shows each support's reactions per load case, and the
204
+ result diagrams below, with positive moment on the tension side.
205
+
206
+ ### Validation
207
+
208
+ Errors name the load and the problem, for example `load 3 (dist): it must end
209
+ after it starts`. Other checks:
210
+
211
+ - every load lands on the beam
212
+ - span offsets stay inside their span
213
+ - quantities have the right dimensions
214
+ - a beam doesn't mix quantities with bare numbers
215
+ - named occupancies exist
216
+ - supports agree with `cantilever`
217
+
218
+ ## Development
219
+
220
+ ```
221
+ uv sync
222
+ uv run pytest
223
+ uv run --group docs sphinx-build -b html docs docs/_build/html # docs
224
+ ```
225
+
226
+ Tests run on every push and pull request (`.github/workflows/tests.yml`). The docs
227
+ are built on pull requests and deployed to bunny.net on every push to `main`
228
+ (`.github/workflows/publish-docs-to-bunny.yml`). Publishing a GitHub release
229
+ uploads the package to PyPI (`.github/workflows/publish.yml`); the release tag
230
+ must match the version in `pyproject.toml`.
@@ -0,0 +1,205 @@
1
+ # beamz9000
2
+
3
+ Beam free-body diagrams from a simple YAML scheme, drawn through shapely geometry.
4
+
5
+ **Documentation:** https://beamz9000.structuralpython.com
6
+
7
+ ```python
8
+ import beamz9000 as bz
9
+
10
+ bz.draw("examples/beam_scheme.yaml", "B1.png") # or .pdf, .svg
11
+ ```
12
+
13
+ ## How it works
14
+
15
+ ```
16
+ scheme YAML ──► scheme.Beam ──► spec.BeamSpec ──► graphics elements ──► renderer
17
+ (pydantic, (engine-neutral (shapely geometry (matplotlib:
18
+ validated, SI floats, legend + style mixins, png/pdf/svg;
19
+ locations keys, labels) Text, Group) native SVG)
20
+ resolved)
21
+ ```
22
+
23
+ - **shapely is the intermediate type.** Supports, arrows, load regions and
24
+ dimension ticks are `Polygon`s and `LineString`s built by small pure
25
+ functions in `graphics/glyphs.py` (the decomposition and proportions follow
26
+ fbd_plotting). Each glyph is drawn at the origin, then oriented and placed.
27
+ - **Style lives next to the geometry**, never inside a backend. `Shape`,
28
+ `Text` and `Group` (`graphics/elements.py`) are frozen pydantic models built
29
+ from `Stroke` (colour, opacity, width, line type), `Fill` (colour, opacity,
30
+ hatch) and `Font` mixins, plus z-order, legend key and role. `translate`,
31
+ `rotate`, `scale`, `mirror_x/y` and `restyle` return new elements. A whole
32
+ diagram serializes to JSON and reads back equal.
33
+ - **Text stays text.** Labels are drawn as native, searchable text (matplotlib
34
+ Text artists with TrueType embedded in PDF, `<text>` in SVG). Layout
35
+ measures each label with libraqm shaping (bundled with matplotlib ≥ 3.11),
36
+ so the space it reserves is the space the label is drawn in.
37
+ - **Strata, top to bottom:** load bands (one per load case, occupancy, or
38
+ "Factored", with overlapping loads packed into rows), the beam, supports,
39
+ dimensions, and a legend to the right. Sizes are fractions of the beam length
40
+ (`diagram.Layout`), and the palette is colour-blind safe (Okabe-Ito).
41
+
42
+ ## The scheme format
43
+
44
+ A scheme file maps beam ids to beams. Write plain numbers in any consistent
45
+ unit system, or name a [forallpeople](https://github.com/connorferster/forallpeople)
46
+ environment and write quantities:
47
+
48
+ ```yaml
49
+ _environment: structural # optional; enables "4500 mm", "2.5 kN/m", ...
50
+
51
+ B1:
52
+ spans: [4500 mm, 2500 mm]
53
+ cantilever: right # left | right | both
54
+ e: 200 GPa # optional; or `ei:`
55
+ i: 300e6 mm**4
56
+ depth: 450 mm # optional; drawn as a rectangle to scale
57
+ node_labels: auto # optional; A, B, C ... or a list like [A, B, null]
58
+ supports: [pin, roller, free] # optional override (see below)
59
+ loads:
60
+ - point: {p: 15 kN, span_loc: [1, 1250 mm], case: L}
61
+ - point: {mag: 10 kN, loc: -500 mm} # no case: already factored
62
+ - dist: {mag: 2.5 kN/m, case: D} # no locations: whole beam
63
+ - dist: {mag1: 1 kN/m, mag2: 4 kN/m, x1: 1000 mm, span_loc2: [0, S], case: L}
64
+ - pressure: {mag: residential, trib_width1: 3 m, trib_width2: 1.5 m, span_loc1: [1, 0 mm], loc2: L}
65
+ - moment: {mag: 5 kN*m, loc: 0 mm}
66
+ occupancies:
67
+ - residential: {D: 1.5 kPa, L: 1.9 kPa}
68
+ load_combos:
69
+ - ULS1: {name: D, factor: 1.4}
70
+ - ULS2: {D: 1.25, L: 1.5}
71
+ results:
72
+ diagrams: [moment, shear, deflection]
73
+ ```
74
+
75
+ ### Locations
76
+
77
+ | Written | Means |
78
+ |---|---|
79
+ | `loc: 2000` (alias `x`) | 2000 from the left end |
80
+ | `loc: -500` | 500 back from the right end (L − 500) |
81
+ | `loc: L` | the right end |
82
+ | `span_loc: [1, 1250]` | 1250 into span 1 (spans count from 0) |
83
+ | `span_loc: [1, -250]` | 250 back from the end of span 1 |
84
+ | `span_loc: [0, S]` | the end of span 0 |
85
+ | `span_loc: 0` | the end of span 0 (a bare index) |
86
+
87
+ A `dist` or `pressure` load takes `loc1`/`x1`/`span_loc1` and
88
+ `loc2`/`x2`/`span_loc2`. Leaving out the start means the left end, and leaving
89
+ out the end means the right end.
90
+
91
+ ### Loads
92
+
93
+ | Type | Magnitude | Notes |
94
+ |---|---|---|
95
+ | `point` | `mag` (aliases `magnitude`, `p`) | Positive acts downward. `angle` (alias `alpha`), in degrees from vertical between −90 and 90, tilts the arrow: positive turns its tail counter-clockwise about its tip. |
96
+ | `moment` | `mag` (aliases `magnitude`, `m`) | Positive is counter-clockwise. |
97
+ | `dist` | `mag` (aliases `magnitude`, `w`) for a UDL, or `mag1` → `mag2` | Linear between the two. |
98
+ | `pressure` | `mag` (aliases `magnitude`, `q`, `occupancy`) × `trib_width1` → `trib_width2` | `trib_width`/`trib` gives a uniform width. `mag` may name an occupancy: the load then stands for one pressure per case in it, or only `case` if given. |
99
+
100
+ Every load takes an optional `case` (with no case, the load is already factored)
101
+ and an optional `label` that replaces the drawn magnitude. Loads may also be
102
+ written flat, as `{type: point, p: 15, loc: 2000}`.
103
+
104
+ ### Supports
105
+
106
+ Without `supports`, the first supported node is pinned and the rest are
107
+ rollers, with cantilevered ends left free. A single span with one cantilevered
108
+ end is fixed at the other end. `supports` overrides this, either with one
109
+ support per node or as `{node index: support}` for just the nodes that differ.
110
+ If `cantilever` is also given, `supports` must leave that end free.
111
+
112
+ | Type | Aliases | Drawn as |
113
+ |---|---|---|
114
+ | `pin` | `pinned`, `hinge` | A triangle on the ground |
115
+ | `roller` | | A circle on the ground |
116
+ | `fixed` | `fix` | A block. At a beam end it becomes a wall covering the beam's depth. |
117
+ | `spring` | `v_spring`, `vertical_spring` | A zigzag spring to the ground |
118
+ | `rotational_spring` | `m_spring`, `rot_spring` | A pin with a spiral |
119
+ | `free` | `none` | Nothing |
120
+
121
+ A spring can take a stiffness, which is drawn beside it. Write it as
122
+ `{spring: 12 kN/mm}` or `{type: spring, k: 12 kN/mm}`. A `spring` takes a force
123
+ per length, and a `rotational_spring` a moment per radian (written as a moment,
124
+ e.g. `8000 kN*m`).
125
+
126
+ ```yaml
127
+ supports:
128
+ - rotational_spring: 8000 kN*m
129
+ - roller
130
+ - spring: 12 kN/mm
131
+ - free
132
+ ```
133
+
134
+ ### Drawing
135
+
136
+ - Labels show quantities as they were written: `12 kN/mm` stays "12 kN/mm"
137
+ rather than forallpeople's "12 MN/m". `*` is drawn as a middle dot and `**n`
138
+ as a superscript (`5 kN*m` → "5 kN·m"). Computed values, such as the overall
139
+ length, use forallpeople's units.
140
+ - Load labels show the magnitude without a sign, because the arrow shows the
141
+ direction. The zero end of a triangular load has no label.
142
+ - `node_labels` are drawn in bold on one line under the lowest support.
143
+ - See `examples/drawing_features.yaml` for depth, springs, angled loads and
144
+ node labels together.
145
+
146
+ ### Fitting to a size
147
+
148
+ A renderer with a `height` outputs exactly `width × height`, and `draw`
149
+ arranges the diagram to fill it. The load strata stretch within
150
+ `Layout.stretch`, and the legend moves on top of the loads when that draws the
151
+ beam larger:
152
+
153
+ ```python
154
+ bz.draw("beams.yaml", "B1.png", renderer=bz.MatplotlibRenderer(width=6.5, height=2.5))
155
+ diagram = bz.beam_diagram("beams.yaml", aspect=6.5 / 2.5) # without a renderer
156
+ ```
157
+
158
+ With `Layout(font_size_pt=8)`, text is 8 pt in the output whatever the beam's
159
+ length, and symbols scale with it.
160
+
161
+ ### ymprint block and ReportLab
162
+
163
+ `uv add "beamz9000[ymprint]"` adds a `_beam` block for
164
+ [ymprint](https://ymprint.structuralpython.com) reports
165
+ (`ym convert report.yml` renders it, or call `register(app)` in your own
166
+ application). `beamz9000.render.reportlab.ReportLabRenderer` draws a
167
+ diagram into any ReportLab document as vector graphics with searchable text.
168
+
169
+ ### Occupancies, combinations, results
170
+
171
+ - `occupancies` and `load_combos` accept a list of `- name: {...}` items or a
172
+ plain mapping.
173
+ - A combination is `{name: D, factor: 1.4}`, a list of those, or
174
+ `{D: 1.25, L: 1.5}`.
175
+ - `results` analyses the beam with pycba-tools or pynite-tools (the `pycba`
176
+ and `pynite` extras): list any of `forces`, `shear`, `moment`,
177
+ `deflection(s)`, `rotation(s)`, or write `{diagrams: [...], engine: pynite}`.
178
+ The diagram then shows each support's reactions per load case, and the
179
+ result diagrams below, with positive moment on the tension side.
180
+
181
+ ### Validation
182
+
183
+ Errors name the load and the problem, for example `load 3 (dist): it must end
184
+ after it starts`. Other checks:
185
+
186
+ - every load lands on the beam
187
+ - span offsets stay inside their span
188
+ - quantities have the right dimensions
189
+ - a beam doesn't mix quantities with bare numbers
190
+ - named occupancies exist
191
+ - supports agree with `cantilever`
192
+
193
+ ## Development
194
+
195
+ ```
196
+ uv sync
197
+ uv run pytest
198
+ uv run --group docs sphinx-build -b html docs docs/_build/html # docs
199
+ ```
200
+
201
+ Tests run on every push and pull request (`.github/workflows/tests.yml`). The docs
202
+ are built on pull requests and deployed to bunny.net on every push to `main`
203
+ (`.github/workflows/publish-docs-to-bunny.yml`). Publishing a GitHub release
204
+ uploads the package to PyPI (`.github/workflows/publish.yml`); the release tag
205
+ must match the version in `pyproject.toml`.
@@ -0,0 +1,58 @@
1
+ [project]
2
+ name = "beamz9000"
3
+ version = "0.1.0"
4
+ description = "Beam free-body diagrams from a simple YAML scheme, drawn through shapely geometry."
5
+ readme = "README.md"
6
+ license = "Apache-2.0"
7
+ requires-python = ">=3.12"
8
+ dependencies = [
9
+ "forallpeople>=3.4.0",
10
+ "matplotlib>=3.11",
11
+ "pydantic>=2.10",
12
+ "pyyaml>=6.0",
13
+ "shapely>=2.1",
14
+ ]
15
+
16
+ [[project.authors]]
17
+ name = "Connor Ferster"
18
+ email = "connor@structuralpython.com"
19
+
20
+ [project.urls]
21
+ Home = "https://github.com/StructuralPython/beamz9000"
22
+ Documentation = "https://beamz9000.structuralpython.com"
23
+
24
+ [project.optional-dependencies]
25
+ reportlab = ["reportlab>=4.2"]
26
+ ymprint = ["ymprint>=0.12.0 ; python_full_version >= '3.14'"]
27
+ pycba = ["pycba-tools>=0.1.0"]
28
+ pynite = ["pynite-tools>=0.8.1"]
29
+
30
+ [project.entry-points."ymprint.blocks"]
31
+ _beam = "beamz9000.ymprint:beam_block"
32
+
33
+ [build-system]
34
+ requires = ["uv_build>=0.9.14,<0.12"]
35
+ build-backend = "uv_build"
36
+
37
+ [dependency-groups]
38
+ dev = [
39
+ "ipykernel>=7.1.0",
40
+ "pycba-tools>=0.1.0",
41
+ "pynite-tools>=0.8.1",
42
+ "pytest>=8.3",
43
+ "reportlab>=4.2",
44
+ "ymprint>=0.12.0 ; python_full_version >= '3.14'",
45
+ ]
46
+ docs = [
47
+ "autodoc-pydantic>=2.2.0",
48
+ "pycba-tools>=0.1.0",
49
+ "pynite-tools>=0.8.1",
50
+ "myst-parser>=5.2.0",
51
+ "shibuya>=2026.7.12",
52
+ "sphinx>=9.1.1",
53
+ "sphinx-copybutton>=0.5.2",
54
+ "sphinx-design>=0.8.0",
55
+ ]
56
+
57
+ [tool.pytest.ini_options]
58
+ testpaths = ["tests"]
@@ -0,0 +1,65 @@
1
+ [project]
2
+ name = "beamz9000"
3
+ version = "0.1.0"
4
+ description = "Beam free-body diagrams from a simple YAML scheme, drawn through shapely geometry."
5
+ readme = "README.md"
6
+ license = "Apache-2.0"
7
+ authors = [{ name = "Connor Ferster", email = "connor@structuralpython.com" }]
8
+ requires-python = ">=3.12"
9
+ dependencies = [
10
+ "forallpeople>=3.4.0",
11
+ "matplotlib>=3.11",
12
+ "pydantic>=2.10",
13
+ "pyyaml>=6.0",
14
+ "shapely>=2.1",
15
+ ]
16
+
17
+ [project.urls]
18
+ Home = "https://github.com/StructuralPython/beamz9000"
19
+ Documentation = "https://beamz9000.structuralpython.com"
20
+
21
+ [project.optional-dependencies]
22
+ reportlab = [
23
+ "reportlab>=4.2",
24
+ ]
25
+ ymprint = [
26
+ "ymprint>=0.12.0 ; python_full_version >= '3.14'",
27
+ ]
28
+ pycba = [
29
+ "pycba-tools>=0.1.0",
30
+ ]
31
+ pynite = [
32
+ "pynite-tools>=0.8.1",
33
+ ]
34
+
35
+ # The _beam block for ymprint's `ym` command and every other ymprint application
36
+ # (ymprint 0.12 and later read the `ymprint.blocks` entry-point group).
37
+ [project.entry-points."ymprint.blocks"]
38
+ _beam = "beamz9000.ymprint:beam_block"
39
+
40
+ [build-system]
41
+ requires = ["uv_build>=0.9.14,<0.12"]
42
+ build-backend = "uv_build"
43
+
44
+ [dependency-groups]
45
+ dev = [
46
+ "ipykernel>=7.1.0",
47
+ "pycba-tools>=0.1.0",
48
+ "pynite-tools>=0.8.1",
49
+ "pytest>=8.3",
50
+ "reportlab>=4.2",
51
+ "ymprint>=0.12.0 ; python_full_version >= '3.14'",
52
+ ]
53
+ docs = [
54
+ "autodoc-pydantic>=2.2.0",
55
+ "pycba-tools>=0.1.0",
56
+ "pynite-tools>=0.8.1",
57
+ "myst-parser>=5.2.0",
58
+ "shibuya>=2026.7.12",
59
+ "sphinx>=9.1.1",
60
+ "sphinx-copybutton>=0.5.2",
61
+ "sphinx-design>=0.8.0",
62
+ ]
63
+
64
+ [tool.pytest.ini_options]
65
+ testpaths = ["tests"]
@@ -0,0 +1,52 @@
1
+ """
2
+ beamz9000: beam free-body diagrams from a simple YAML scheme.
3
+
4
+ The pipeline is: scheme YAML -> `scheme.Beam` (pydantic, validated) ->
5
+ `spec.BeamSpec` (engine-neutral floats) -> shapely-based graphics elements ->
6
+ a renderer (matplotlib, SVG).
7
+
8
+ import beamz9000 as bz
9
+ bz.draw("examples/beam_scheme.yaml", "B1.png")
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import pathlib
15
+ from typing import Optional, Union
16
+
17
+ from beamz9000.diagram import Layout, as_spec, beam_diagram
18
+ from beamz9000.graphics.elements import Group, Shape, Text
19
+ from beamz9000.render import MatplotlibRenderer, SVGRenderer
20
+ from beamz9000.scheme import Beam, Scheme, load_scheme, parse_scheme
21
+ from beamz9000.spec import BeamSpec, from_scheme
22
+
23
+ __version__ = "0.1.0"
24
+
25
+
26
+ def draw(obj, path: Union[str, pathlib.Path], layout: Optional[Layout] = None,
27
+ beam_id: Optional[str] = None, renderer=None) -> pathlib.Path:
28
+ """
29
+ Draws the free-body diagram of 'obj' (a scheme file path, Scheme, Beam,
30
+ BeamSpec, or an already-built diagram Group) to 'path'. The suffix picks
31
+ the format: .svg uses the native SVGRenderer, anything else (png, pdf,
32
+ ...) the MatplotlibRenderer, unless 'renderer' is given.
33
+
34
+ A renderer with a `height` sets the output's exact size, and the diagram
35
+ is arranged to fill it (see `beam_diagram`'s 'aspect'). With
36
+ `layout.font_size_pt`, text is that many points high in the output.
37
+ """
38
+ path = pathlib.Path(path)
39
+ if renderer is None:
40
+ renderer = SVGRenderer() if path.suffix.lower() == ".svg" else MatplotlibRenderer()
41
+ if isinstance(obj, Group):
42
+ diagram = obj
43
+ else:
44
+ diagram = beam_diagram(obj, layout, beam_id, aspect=renderer.aspect, margin=renderer.margin,
45
+ width=renderer.width_pt)
46
+ return renderer.save(diagram, path)
47
+
48
+
49
+ __all__ = [
50
+ "Beam", "BeamSpec", "Group", "Layout", "MatplotlibRenderer", "SVGRenderer", "Scheme",
51
+ "Shape", "Text", "as_spec", "beam_diagram", "draw", "from_scheme", "load_scheme", "parse_scheme",
52
+ ]