cycloidgen 7.1.0__py3-none-any.whl
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.
- cycloidgen/__init__.py +11 -0
- cycloidgen/__main__.py +344 -0
- cycloidgen/analysis/__init__.py +490 -0
- cycloidgen/analysis/bearings.py +719 -0
- cycloidgen/analysis/compliance.py +256 -0
- cycloidgen/analysis/efficiency.py +177 -0
- cycloidgen/analysis/fatigue.py +311 -0
- cycloidgen/analysis/lubrication.py +415 -0
- cycloidgen/analysis/mass.py +226 -0
- cycloidgen/analysis/mechanics.py +159 -0
- cycloidgen/analysis/stiffness.py +779 -0
- cycloidgen/analysis/thermal.py +245 -0
- cycloidgen/analysis/tolerance.py +108 -0
- cycloidgen/core/__init__.py +1 -0
- cycloidgen/core/designfile.py +88 -0
- cycloidgen/core/explain.py +616 -0
- cycloidgen/core/guide.py +471 -0
- cycloidgen/core/kinematics.py +237 -0
- cycloidgen/core/profile.py +290 -0
- cycloidgen/core/spec.py +910 -0
- cycloidgen/core/validate.py +297 -0
- cycloidgen/design/__init__.py +26 -0
- cycloidgen/design/batch.py +386 -0
- cycloidgen/design/optimize.py +621 -0
- cycloidgen/design/sweep.py +187 -0
- cycloidgen/export/__init__.py +92 -0
- cycloidgen/export/animation.py +358 -0
- cycloidgen/export/bom.py +183 -0
- cycloidgen/export/dxf.py +261 -0
- cycloidgen/export/manifest.py +199 -0
- cycloidgen/export/solid.py +397 -0
- cycloidgen/export/svg.py +75 -0
- cycloidgen/report/__init__.py +1 -0
- cycloidgen/report/build.py +737 -0
- cycloidgen/report/plots.py +732 -0
- cycloidgen/ui/__init__.py +1 -0
- cycloidgen/ui/app.py +54 -0
- cycloidgen/ui/assets/chevron-down-dark.png +0 -0
- cycloidgen/ui/assets/chevron-down-light.png +0 -0
- cycloidgen/ui/assets/chevron-up-dark.png +0 -0
- cycloidgen/ui/assets/chevron-up-light.png +0 -0
- cycloidgen/ui/assets/cycloidgen.ico +0 -0
- cycloidgen/ui/assets/mark-black.png +0 -0
- cycloidgen/ui/assets/mark-blue.png +0 -0
- cycloidgen/ui/assets/mark-white.png +0 -0
- cycloidgen/ui/assets/tick.png +0 -0
- cycloidgen/ui/assets/wordmark-black.png +0 -0
- cycloidgen/ui/assets/wordmark-blue.png +0 -0
- cycloidgen/ui/assets/wordmark-white.png +0 -0
- cycloidgen/ui/branding.py +637 -0
- cycloidgen/ui/fields.py +282 -0
- cycloidgen/ui/history.py +65 -0
- cycloidgen/ui/logpanel.py +315 -0
- cycloidgen/ui/main_window.py +2356 -0
- cycloidgen/ui/optimise_dialog.py +430 -0
- cycloidgen/ui/outputs.py +279 -0
- cycloidgen/ui/plotbar.py +100 -0
- cycloidgen/ui/settings.py +33 -0
- cycloidgen/ui/tables.py +56 -0
- cycloidgen/ui/tradestudy.py +278 -0
- cycloidgen/ui/view3d.py +600 -0
- cycloidgen/ui/view3d_qtgl.py +412 -0
- cycloidgen/ui/view3d_vtk.py +625 -0
- cycloidgen/units.py +70 -0
- cycloidgen/viz/__init__.py +14 -0
- cycloidgen/viz/mesh.py +697 -0
- cycloidgen/viz/scene.py +233 -0
- cycloidgen/viz/vtkbridge.py +235 -0
- cycloidgen-7.1.0.dist-info/METADATA +943 -0
- cycloidgen-7.1.0.dist-info/RECORD +74 -0
- cycloidgen-7.1.0.dist-info/WHEEL +5 -0
- cycloidgen-7.1.0.dist-info/entry_points.txt +5 -0
- cycloidgen-7.1.0.dist-info/licenses/LICENSE +202 -0
- cycloidgen-7.1.0.dist-info/top_level.txt +1 -0
cycloidgen/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Parametric cycloidal drive generator."""
|
|
2
|
+
|
|
3
|
+
#: The one place the version is written.
|
|
4
|
+
#:
|
|
5
|
+
#: `pyproject.toml` reads it from here (`dynamic = ["version"]`), the
|
|
6
|
+
#: PyInstaller spec stamps it into the executable, and `packaging/cycloidgen.nsi`
|
|
7
|
+
#: parses this exact line with `!searchparse`. Keep it a plain string literal
|
|
8
|
+
#: assignment on one line: setuptools reads it statically, without importing the
|
|
9
|
+
#: package, and NSIS reads it with a text match. Anything cleverer - a computed
|
|
10
|
+
#: string, a tuple, an import - breaks both.
|
|
11
|
+
__version__ = "7.1.0"
|
cycloidgen/__main__.py
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""``python -m cycloidgen`` - launch the desktop app, or work from the CLI.
|
|
2
|
+
|
|
3
|
+
python -m cycloidgen # GUI
|
|
4
|
+
python -m cycloidgen --ratio 29 --out ./x # headless export
|
|
5
|
+
python -m cycloidgen --ratio 29 --list-outputs # what an export would write
|
|
6
|
+
python -m cycloidgen --optimise --ratio 29 --torque 20 --max-od 120 --out ./x
|
|
7
|
+
python -m cycloidgen --ratio 21 --vary disc_count=1 --vary disc_count=2 \
|
|
8
|
+
--vary output_pin_count=8:16:5 --csv study.csv # parameter study
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _spec_from_args(args):
|
|
18
|
+
"""The design a headless run works on: a saved file if given, else a preset."""
|
|
19
|
+
import json
|
|
20
|
+
|
|
21
|
+
from .core.designfile import numbers_may_have_moved, provenance, spec_from_dict, written_by
|
|
22
|
+
from .core.spec import preset
|
|
23
|
+
if args.design:
|
|
24
|
+
data = json.loads(args.design.read_text(encoding="utf-8"))
|
|
25
|
+
spec = spec_from_dict(data)
|
|
26
|
+
# No dialog to put it in out here, and a headless run is the one most
|
|
27
|
+
# likely to end up in a script whose output nobody reads twice.
|
|
28
|
+
written = written_by(data)
|
|
29
|
+
if numbers_may_have_moved(written):
|
|
30
|
+
print(provenance(written), file=sys.stderr)
|
|
31
|
+
else:
|
|
32
|
+
spec = preset(args.ratio or 15)
|
|
33
|
+
return _apply_omissions(spec, args)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _apply_omissions(spec, args):
|
|
37
|
+
"""Take out the bearings the caller says this drive does not have.
|
|
38
|
+
|
|
39
|
+
Only ever subtractive - the flags are ``--no-...`` - so applying them over a
|
|
40
|
+
saved design can remove a bearing it had and never put one back that it did
|
|
41
|
+
not, which is the only reading of a command line that is not a surprise.
|
|
42
|
+
"""
|
|
43
|
+
if args.no_cam_bearing:
|
|
44
|
+
spec.cam_bearing_fitted = False
|
|
45
|
+
if args.no_shaft_bearings:
|
|
46
|
+
spec.shaft_bearings_fitted = False
|
|
47
|
+
if args.no_output_bearing:
|
|
48
|
+
spec.output_bearing_fitted = False
|
|
49
|
+
return spec
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _list_outputs(spec, groups: set[str]) -> int:
|
|
53
|
+
"""Print the bundle without producing it.
|
|
54
|
+
|
|
55
|
+
Straight off the manifest, which is also what ``write_bundle`` walks, so
|
|
56
|
+
this is a promise the exporter keeps rather than a table someone wrote once.
|
|
57
|
+
"""
|
|
58
|
+
from .export.manifest import GROUPS, outputs_for
|
|
59
|
+
|
|
60
|
+
print(f"An export of this {spec.ratio}:1 design writes:\n")
|
|
61
|
+
total = 0
|
|
62
|
+
for group in GROUPS:
|
|
63
|
+
mark = "x" if group.key in groups else " "
|
|
64
|
+
print(f"[{mark}] {group.title} - {group.note}")
|
|
65
|
+
for out in outputs_for({group.key}):
|
|
66
|
+
names = out.files(spec)
|
|
67
|
+
print(f" {out.fmt:<5} {out.where:<16} {out.title}")
|
|
68
|
+
if out.is_folder:
|
|
69
|
+
for name in names:
|
|
70
|
+
print(f" {name}")
|
|
71
|
+
total += len(names) if group.key in groups else 0
|
|
72
|
+
print()
|
|
73
|
+
print(f"{total} file(s) with the current selection.")
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _batch(args, spec) -> int:
|
|
78
|
+
"""Run the parameter study and print it, or write it, and stop there.
|
|
79
|
+
|
|
80
|
+
A study is numbers rather than parts: four hundred designs are four hundred
|
|
81
|
+
answers and one folder of STEP files nobody asked for, so this deliberately
|
|
82
|
+
does not export. ``--out`` with ``--vary`` is refused rather than ignored,
|
|
83
|
+
because ignoring it would look like it had worked.
|
|
84
|
+
"""
|
|
85
|
+
import sys as _sys
|
|
86
|
+
|
|
87
|
+
from .design.batch import METRICS, as_text, merge_axes, parse_axis, run_batch, write_csv
|
|
88
|
+
|
|
89
|
+
if args.out:
|
|
90
|
+
print("--out writes one design; --vary evaluates many. Use --csv for a "
|
|
91
|
+
"study, or drop --vary to export this design.", file=_sys.stderr)
|
|
92
|
+
return 2
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
axes = merge_axes([parse_axis(*_split_vary(text)) for text in args.vary])
|
|
96
|
+
except ValueError as exc:
|
|
97
|
+
print(exc, file=_sys.stderr)
|
|
98
|
+
return 2
|
|
99
|
+
|
|
100
|
+
total = 1
|
|
101
|
+
for axis in axes:
|
|
102
|
+
total *= len(axis)
|
|
103
|
+
plan = " x ".join(f"{len(a)} {a.field}" for a in axes)
|
|
104
|
+
print(f"{total} design(s): {plan}", file=_sys.stderr)
|
|
105
|
+
|
|
106
|
+
def tick(done: int, of: int) -> None:
|
|
107
|
+
if of > 20 and (done % max(1, of // 20) == 0 or done == of):
|
|
108
|
+
print(f"\r {done}/{of}", end="", file=_sys.stderr, flush=True)
|
|
109
|
+
|
|
110
|
+
points = run_batch(spec, axes, progress=tick)
|
|
111
|
+
if total > 20:
|
|
112
|
+
print(file=_sys.stderr)
|
|
113
|
+
|
|
114
|
+
if args.csv:
|
|
115
|
+
path = write_csv(points, axes, args.csv)
|
|
116
|
+
built = sum(1 for p in points if p.ok)
|
|
117
|
+
print(f"wrote {len(points)} row(s) to {path} ({built} built, "
|
|
118
|
+
f"{len(points) - built} blocked)")
|
|
119
|
+
return 0
|
|
120
|
+
|
|
121
|
+
# No file asked for, so this goes on the terminal - and the whole table is
|
|
122
|
+
# too wide for one. The five shown are the five the calibration plan
|
|
123
|
+
# measures on real hardware, which is why they are the first five in
|
|
124
|
+
# METRICS rather than a second list kept here.
|
|
125
|
+
shown = METRICS[:5]
|
|
126
|
+
header = [a.field for a in axes] + ["ok"] + [m.name for m in shown]
|
|
127
|
+
rows = [[as_text(p.values[a.field]) for a in axes]
|
|
128
|
+
+ ["yes" if p.ok else "no"]
|
|
129
|
+
+ [f"{p.metrics.get(m.name, float('nan')):.4g}" for m in shown]
|
|
130
|
+
for p in points]
|
|
131
|
+
|
|
132
|
+
# Sized to what is in them. A lubricant name is 24 characters and a fixed
|
|
133
|
+
# width either truncates it into two rows that read as the same design or
|
|
134
|
+
# pushes every column out to fit the longest thing in the app.
|
|
135
|
+
widths = [max(len(header[i]), *(len(r[i]) for r in rows)) if rows
|
|
136
|
+
else len(header[i]) for i in range(len(header))]
|
|
137
|
+
line = " ".join(h.rjust(w) for h, w in zip(header, widths, strict=True))
|
|
138
|
+
print(line)
|
|
139
|
+
print("-" * len(line))
|
|
140
|
+
for row in rows:
|
|
141
|
+
print(" ".join(cell.rjust(w) for cell, w in zip(row, widths, strict=True)))
|
|
142
|
+
print(f"\n{len(METRICS)} metrics per design; --csv writes all of them.")
|
|
143
|
+
return 0
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _split_vary(text: str) -> tuple[str, str]:
|
|
147
|
+
"""``field=value`` into its two halves, splitting on the *first* ``=``.
|
|
148
|
+
|
|
149
|
+
Values can contain one - a bearing designation or a lubricant name is
|
|
150
|
+
somebody else's naming scheme, not ours to constrain.
|
|
151
|
+
"""
|
|
152
|
+
name, sep, value = text.partition("=")
|
|
153
|
+
if not sep:
|
|
154
|
+
raise ValueError(f"--vary wants field=value, not {text!r}")
|
|
155
|
+
return name.strip(), value
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _search(args) -> tuple[int, object | None]:
|
|
159
|
+
"""Run the requirements search and print the shortlist.
|
|
160
|
+
|
|
161
|
+
Returns ``(exit_code, spec)``; the spec is the winner, ready to export.
|
|
162
|
+
"""
|
|
163
|
+
from .core.spec import MATERIALS, Process
|
|
164
|
+
from .design import Objective, Requirements, optimise
|
|
165
|
+
|
|
166
|
+
if args.disc_material not in MATERIALS:
|
|
167
|
+
print(f"unknown material {args.disc_material!r}; choose from "
|
|
168
|
+
+ ", ".join(MATERIALS), file=sys.stderr)
|
|
169
|
+
return 2, None
|
|
170
|
+
|
|
171
|
+
req = Requirements(
|
|
172
|
+
ratio=args.ratio or 29,
|
|
173
|
+
output_torque_Nm=args.torque,
|
|
174
|
+
input_rpm=args.rpm,
|
|
175
|
+
max_outer_diameter_mm=args.max_od,
|
|
176
|
+
max_length_mm=args.max_length,
|
|
177
|
+
process=Process(args.process),
|
|
178
|
+
disc_material=args.disc_material,
|
|
179
|
+
pin_material=args.pin_material,
|
|
180
|
+
housing_material=args.housing_material,
|
|
181
|
+
ring_pins_are_rollers=args.rollers,
|
|
182
|
+
output_pins_are_rollers=args.rollers,
|
|
183
|
+
cam_bearing_fitted=not args.no_cam_bearing,
|
|
184
|
+
shaft_bearings_fitted=not args.no_shaft_bearings,
|
|
185
|
+
output_bearing_fitted=not args.no_output_bearing,
|
|
186
|
+
min_safety_factor=args.min_safety,
|
|
187
|
+
objective=Objective(args.objective),
|
|
188
|
+
disc_count=args.discs,
|
|
189
|
+
)
|
|
190
|
+
print(f"searching for a {req.ratio}:1 drive, {req.output_torque_Nm:g} Nm out, "
|
|
191
|
+
f"under {req.max_outer_diameter_mm:g} mm across, "
|
|
192
|
+
f"optimising for {req.objective.value}...\n")
|
|
193
|
+
result = optimise(req, effort=args.effort)
|
|
194
|
+
|
|
195
|
+
if not result.ok:
|
|
196
|
+
print(f"nothing met those requirements after {result.evaluations} "
|
|
197
|
+
f"candidates.\nwhat stopped them: {result.tally.explain()}",
|
|
198
|
+
file=sys.stderr)
|
|
199
|
+
return 3, None
|
|
200
|
+
|
|
201
|
+
header = (f"{'#':>2} {'OD':>6} {'len':>6} {'capacity':>9} {'margin':>7} "
|
|
202
|
+
f"{'eff':>6} {'mass':>7} {'backlash':>9} {'temp':>6}")
|
|
203
|
+
print(header)
|
|
204
|
+
print("-" * len(header))
|
|
205
|
+
for i, c in enumerate(result.best, 1):
|
|
206
|
+
print(f"{i:>2} {c.outer_diameter_mm:6.1f} {c.length_mm:6.1f} "
|
|
207
|
+
f"{c.capacity_Nm:8.2f}N {c.margin:6.2f}x {100 * c.efficiency:5.1f}% "
|
|
208
|
+
f"{c.mass_g:6.0f}g {c.lost_motion_arcmin:8.1f}' "
|
|
209
|
+
f"{c.temperature_C:5.0f}C")
|
|
210
|
+
best = result.best[0].spec
|
|
211
|
+
print(f"\ntaking #1: R={best.pin_circle_radius:.2f} Rr={best.pin_radius:.2f} "
|
|
212
|
+
f"E={best.eccentricity:.3f} K1={best.K1:.3f}, "
|
|
213
|
+
f"{best.disc_count} x {best.disc_thickness:.1f} mm disc(s), "
|
|
214
|
+
f"{best.output_pin_count} x {best.output_pin_diameter:.1f} mm output pins")
|
|
215
|
+
return 0, best
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def main(argv: list[str] | None = None) -> int:
|
|
219
|
+
from . import __version__
|
|
220
|
+
|
|
221
|
+
parser = argparse.ArgumentParser(prog="cycloidgen",
|
|
222
|
+
description="Cycloidal drive generator")
|
|
223
|
+
parser.add_argument("--version", action="version",
|
|
224
|
+
version=f"cycloidgen {__version__}")
|
|
225
|
+
parser.add_argument("--ratio", type=int, help="generate a preset and exit")
|
|
226
|
+
parser.add_argument("--design", type=Path, help="load a saved design JSON")
|
|
227
|
+
parser.add_argument("--out", type=Path, help="output folder for a headless run")
|
|
228
|
+
parser.add_argument("--no-solids", action="store_true",
|
|
229
|
+
help="skip STEP/STL, write drawings and report only")
|
|
230
|
+
parser.add_argument("--only", metavar="GROUPS",
|
|
231
|
+
help="write only these output groups, comma separated: "
|
|
232
|
+
"drawings, solids, data")
|
|
233
|
+
parser.add_argument("--list-outputs", action="store_true",
|
|
234
|
+
help="print every file an export would write, and exit")
|
|
235
|
+
|
|
236
|
+
study = parser.add_argument_group(
|
|
237
|
+
"parameter study", "evaluate a grid of designs and get a table out "
|
|
238
|
+
"instead of a folder of parts")
|
|
239
|
+
study.add_argument("--vary", action="append", default=[], metavar="FIELD=VALUE",
|
|
240
|
+
help="a parameter and a value to put the design through. "
|
|
241
|
+
"Repeat it for more values of the same field, or for "
|
|
242
|
+
"another field - every combination is evaluated. "
|
|
243
|
+
"Numeric fields also take a lo:hi:steps range")
|
|
244
|
+
study.add_argument("--csv", type=Path, metavar="PATH",
|
|
245
|
+
help="write the full table here instead of a summary to "
|
|
246
|
+
"the terminal")
|
|
247
|
+
|
|
248
|
+
built = parser.add_argument_group(
|
|
249
|
+
"bearings fitted", "three of the five load paths can be built without a "
|
|
250
|
+
"bearing of their own; these leave them out of the "
|
|
251
|
+
"design, not just out of the picture")
|
|
252
|
+
built.add_argument("--no-cam-bearing", action="store_true",
|
|
253
|
+
help="the disc bore runs straight on the cam")
|
|
254
|
+
built.add_argument("--no-shaft-bearings", action="store_true",
|
|
255
|
+
help="the drive hangs on the driving motor's bearings")
|
|
256
|
+
built.add_argument("--no-output-bearing", action="store_true",
|
|
257
|
+
help="the driven machine locates the output flange")
|
|
258
|
+
|
|
259
|
+
search = parser.add_argument_group(
|
|
260
|
+
"design search", "state what the drive has to do and let the app find "
|
|
261
|
+
"the geometry, instead of giving it one")
|
|
262
|
+
search.add_argument("--optimise", "--optimize", action="store_true",
|
|
263
|
+
dest="optimise", help="search for a design")
|
|
264
|
+
search.add_argument("--torque", type=float, default=5.0,
|
|
265
|
+
help="required output torque, Nm (default 5)")
|
|
266
|
+
search.add_argument("--rpm", type=float, default=1000.0,
|
|
267
|
+
help="input speed (default 1000)")
|
|
268
|
+
search.add_argument("--max-od", type=float, default=120.0,
|
|
269
|
+
help="outer diameter limit, mm (default 120)")
|
|
270
|
+
search.add_argument("--max-length", type=float, default=60.0,
|
|
271
|
+
help="axial length limit, mm (default 60)")
|
|
272
|
+
search.add_argument("--process", default="FDM 3D print",
|
|
273
|
+
help="manufacturing process (default 'FDM 3D print')")
|
|
274
|
+
search.add_argument("--disc-material", default="PLA")
|
|
275
|
+
search.add_argument("--pin-material", default="Steel 1045")
|
|
276
|
+
search.add_argument("--housing-material", default="PLA")
|
|
277
|
+
search.add_argument("--rollers", action="store_true",
|
|
278
|
+
help="ring and output pins carry rolling elements")
|
|
279
|
+
search.add_argument("--discs", type=int, default=0, choices=(0, 1, 2, 3),
|
|
280
|
+
help="0 lets the search choose (default)")
|
|
281
|
+
search.add_argument("--min-safety", type=float, default=1.5,
|
|
282
|
+
help="required margin on contact stress (default 1.5)")
|
|
283
|
+
search.add_argument("--objective", default="balanced",
|
|
284
|
+
choices=["balanced", "torque capacity", "efficiency",
|
|
285
|
+
"small and light", "stiffness and low backlash"])
|
|
286
|
+
search.add_argument("--effort", default="normal",
|
|
287
|
+
choices=["quick", "normal", "thorough"])
|
|
288
|
+
args = parser.parse_args(argv)
|
|
289
|
+
|
|
290
|
+
headless = (args.out or args.ratio or args.design or args.optimise
|
|
291
|
+
or args.list_outputs or args.vary)
|
|
292
|
+
if headless:
|
|
293
|
+
import matplotlib
|
|
294
|
+
matplotlib.use("Agg")
|
|
295
|
+
from .analysis import analyse
|
|
296
|
+
from .export import write_bundle
|
|
297
|
+
from .export.manifest import resolve_groups
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
groups = resolve_groups(
|
|
301
|
+
not args.no_solids,
|
|
302
|
+
[g.strip() for g in args.only.split(",")] if args.only else None)
|
|
303
|
+
except ValueError as exc:
|
|
304
|
+
print(exc, file=sys.stderr)
|
|
305
|
+
return 2
|
|
306
|
+
|
|
307
|
+
if args.optimise:
|
|
308
|
+
code, spec = _search(args)
|
|
309
|
+
if code:
|
|
310
|
+
return code
|
|
311
|
+
print()
|
|
312
|
+
else:
|
|
313
|
+
spec = _spec_from_args(args)
|
|
314
|
+
|
|
315
|
+
if args.list_outputs:
|
|
316
|
+
return _list_outputs(spec, groups)
|
|
317
|
+
|
|
318
|
+
if args.vary:
|
|
319
|
+
# After the search rather than instead of it: varying around a
|
|
320
|
+
# design the app found is a more useful study than varying around
|
|
321
|
+
# a preset, and it costs nothing to allow.
|
|
322
|
+
return _batch(args, spec)
|
|
323
|
+
|
|
324
|
+
report = analyse(spec).report
|
|
325
|
+
print(report)
|
|
326
|
+
if not args.out and args.optimise:
|
|
327
|
+
return 0 # searched only, nothing to write
|
|
328
|
+
if not report.ok:
|
|
329
|
+
print("\nblocked: fix the errors above", file=sys.stderr)
|
|
330
|
+
return 2
|
|
331
|
+
|
|
332
|
+
out = args.out or Path.cwd() / f"cycloidal_{spec.ratio}to1"
|
|
333
|
+
files = write_bundle(spec, out, groups=groups)
|
|
334
|
+
print(f"\nwrote {len(files)} files to {out}")
|
|
335
|
+
for f in files:
|
|
336
|
+
print(f" {f.relative_to(out)}")
|
|
337
|
+
return 0
|
|
338
|
+
|
|
339
|
+
from .ui.app import main as gui_main
|
|
340
|
+
return gui_main()
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
if __name__ == "__main__":
|
|
344
|
+
raise SystemExit(main())
|