- 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>
75 lines
3.3 KiB
Python
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)
|