Files
sandTable/software/tools/make_library.py
T
ckochandClaude Opus 5.5 95ad4d25de Software: pattern player, web page with a design library, GRBL tools
- python3 -m sandtable web: local web page to play designs from a library of
  27 built-in patterns, upload .thr or .svg, pause/skip/stop, jog and calibrate.
  Reconnects by itself when the USB cable comes back.
- Plays .thr files and built-in generators with an erase spiral between them,
  constant ball speed, host-side homing on stock GRBL 1.1 (Uno + CNC shield),
  FluidNC for the ESP32 board.
- Home switch guard: patterns never press the switch, so a press means lost
  steps and stops the machine at once with an alert.
- lineart.py turns SVG line drawings into one continuous path.
- Tests against a GRBL simulator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 13:40:23 -05:00

75 lines
3.3 KiB
Python

"""Write the built-in design library into software/patterns/<category>/<name>.thr.
Run from the software folder: python3 tools/make_library.py
Your own designs go in patterns/uploads/ (the web page's Upload button puts them there); this
script never touches that folder.
"""
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(HERE))
from sandtable import config, patterns, thr # noqa: E402
from sandtable.motion import Planner # noqa: E402
LIBRARY = {
'spirals': {
'spiral': ('spiral', {'loops': 20}),
'spiral-wide': ('spiral', {'loops': 12}),
'ripples': ('ripples', {}),
'ripples-fine': ('ripples', {'waves': 15, 'amplitude': 0.025, 'loops': 20}),
'galaxy-4': ('spiral_arms', {'arms': 4, 'turns': 1.0}),
'galaxy-8': ('spiral_arms', {'arms': 8, 'turns': 0.6}),
},
'flowers': {
'rose-5': ('rose', {'petals': 5, 'copies': 3, 'twist_deg': 12}),
'rose-8': ('rose', {'petals': 4}),
'rose-5-3': ('rose', {'petals': 5, 'denominator': 3, 'copies': 2, 'twist_deg': 36}),
'rose-7-4': ('rose', {'petals': 7, 'denominator': 4}),
'rose-twisted': ('rose', {'petals': 7, 'copies': 4, 'twist_deg': 12}),
'bouquet': ('rose', {'petals': 3, 'copies': 6, 'twist_deg': 20}),
},
'spirograph': {
'spirograph': ('spirograph', {}),
'spirograph-7': ('spirograph', {'big': 90, 'small': 35, 'pen': 0.7}),
'spirograph-5': ('spirograph', {'big': 120, 'small': 50, 'pen': 0.6}),
'spirograph-lace': ('spirograph', {'big': 100, 'small': 43, 'pen': 0.9}),
},
'geometric': {
'hexagons': ('polygons', {}),
'squares': ('polygons', {'sides': 4, 'layers': 50, 'twist_deg': 4}),
'triangles': ('polygons', {'sides': 3, 'layers': 45, 'twist_deg': 5}),
'octagons': ('polygons', {'sides': 8, 'layers': 40, 'twist_deg': 2}),
'string-art': ('string_art', {'points': 101, 'step': 40}),
'string-art-star': ('string_art', {'points': 61, 'step': 25}),
'sunburst': ('sunburst', {'rays': 24}),
'sunburst-fine': ('sunburst', {'rays': 40}),
'lissajous-3-2': ('lissajous', {'a': 3, 'b': 2, 'copies': 6, 'twist_deg': 30}),
'lissajous-5-4': ('lissajous', {'a': 5, 'b': 4, 'copies': 3, 'twist_deg': 20}),
},
'tools': {
'test_f': ('test_f', {}),
},
}
def main(dry_run=False):
root = os.path.join(os.path.dirname(HERE), 'patterns')
cfg = config.load()
total = 0
for cat, items in LIBRARY.items():
for name, (gen, params) in items.items():
pts = patterns.generate(gen, **params)
_, _, est = Planner(cfg, 0.0, cfg['machine']['rho_min_mm']).program(pts, erase=False)
total += est
print(f' {cat}/{name}: {len(pts)} points, about {est / 60:.0f} min')
if not dry_run:
os.makedirs(os.path.join(root, cat), exist_ok=True)
args = ' '.join(f'{k}={v}' for k, v in params.items())
thr.save(os.path.join(root, cat, name + '.thr'), pts, header=f'sandtable generator {gen} {args}'.strip())
print(f' {sum(len(v) for v in LIBRARY.values())} designs, about {total / 3600:.1f} h in all (plus erasing)')
if __name__ == '__main__':
main(dry_run='--dry-run' in sys.argv)