diff --git a/BUILD.md b/BUILD.md index 3cbf07d..42a353e 100644 --- a/BUILD.md +++ b/BUILD.md @@ -97,6 +97,8 @@ Head notes: ## 3. Print the parts All files are in `print/v3/`. [`print/README.md`](print/README.md) has the details for each part. +**Bambu Studio users:** `print/3mf/sand-table-v3-P1S.3mf` has every part on 9 plates, already oriented, with supports and settings. It takes about 23.5 hours and uses 760 g PLA plus 165 g PETG. Print plate 1, the fit tests, first. + **Print the two fit tests first.** The bearing fits are tuned for a P1S: - `00-fit-test-608`: the pins should hold a 608 firmly, and a 608 should press into the pocket with a snug push. The parts use the 8.1 mm pin and the 22.2 mm pocket. - `00b-fit-test-6804`: a 6804 should press into the 32.2 mm pocket and onto the 20.1 mm stub with a firm push. diff --git a/print/3mf/sand-table-v3-P1S-options.3mf b/print/3mf/sand-table-v3-P1S-options.3mf new file mode 100644 index 0000000..a3976e7 Binary files /dev/null and b/print/3mf/sand-table-v3-P1S-options.3mf differ diff --git a/print/3mf/sand-table-v3-P1S.3mf b/print/3mf/sand-table-v3-P1S.3mf new file mode 100644 index 0000000..1fbdc8f Binary files /dev/null and b/print/3mf/sand-table-v3-P1S.3mf differ diff --git a/print/README.md b/print/README.md index c8328c8..a9cde8a 100644 --- a/print/README.md +++ b/print/README.md @@ -4,6 +4,7 @@ | Folder | What | |---|---| +| `3mf/` | **Bambu Studio projects** of the v3 set for a P1S, on plates with settings (see below). | | `v3/` | **The complete v3 set: every printed part.** New shell, deck, center plate, hub and slip-ring parts, seam plates and plugs, and a counterweight for the arm, plus copies of the unchanged parts (14–25). The rail bolts straight to the hub again (no `29c`). Print `00b-fit-test-6804` first. See "What changed in v3". | | `v2-current/` | The full v2 set, numbered in print order. Parts not replaced in a later folder still come from here. | | `v2.7-removable-arm/` | **Replaced by `v3`.** In v3 the rail bolts straight to the hub, so neither `29b` nor `29c` is used. `30` and `13` are copied into `v3/` unchanged. | @@ -15,6 +16,16 @@ | `v2.1-switch-update/` | **Also print these 2.** A new switch mount (it replaces `v2-current/14`) and a striker you glue onto the carriage. | | `v1-original/` | The first export, kept for reference. Don't print its `05-deck-Q1-arduino.stl`. | +## Bambu Studio projects (P1S) +`3mf/` has ready-to-print Bambu Studio projects for a P1S (0.4 mm nozzle, textured PEI plate): +- **`sand-table-v3-P1S.3mf`:** every v3 part on 9 plates, turned the way it prints, with settings: supports only where needed, stronger walls on the hub, carriage and brackets, and a solid counterweight. + - Filament 1 is PLA (case parts), filament 2 is PETG (moving parts). + - About 23.5 hours and 760 g PLA + 165 g PETG in all. + - Print plate 1 (the fit tests) first. +- **`sand-table-v3-P1S-options.3mf`:** the alternatives: the 12.5 mm sand ring (only for a raised floor, BUILD.md step 13) and the switch mount for M2 screws. + +They're built from the STLs by `tools/make_3mf.py` (it needs the Bambu Studio flatpak). Run it again after changing a part. + ## Full parts list (v3) | Part | Folder | Notes | |---|---|---| diff --git a/print/tools/make_3mf.py b/print/tools/make_3mf.py new file mode 100644 index 0000000..1890d8c --- /dev/null +++ b/print/tools/make_3mf.py @@ -0,0 +1,202 @@ +"""Build Bambu Studio project files (.3mf) for the sand table from the STLs in print/v3. + +Run from the repository root: python3 print/tools/make_3mf.py +Needs Bambu Studio (the flatpak, com.bambulab.BambuStudio) on this computer; it is used +from the command line to lay the parts out on plates. Writes: + print/3mf/sand-table-v3-P1S.3mf every part, on plates, oriented and with settings + print/3mf/sand-table-v3-P1S-options.3mf the alternatives (12.5 mm sand ring, M2 switch mount) + +Open them in Bambu Studio, check the plates, slice, and save before uploading to MakerWorld. +""" +import json +import math +import os +import shutil +import struct +import subprocess +import sys +import zipfile + +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +V3 = os.path.join(ROOT, 'print', 'v3') +OUT = os.path.join(ROOT, 'print', '3mf') +WORK = os.path.expanduser('~/.cache/sandtable-3mf') # the flatpak can only see the home folder +SYS = os.path.expanduser('~/.var/app/com.bambulab.BambuStudio/config/BambuStudio/system/BBL') +MACHINE, PROCESS = 'Bambu Lab P1S 0.4 nozzle', '0.20mm Standard @BBL X1C' +PLA, PETG = 'Bambu PLA Basic @BBL P1S 0.4 nozzle', 'Bambu PETG Basic @BBL X1C' +FLIP = 'flip' # print upside down (top face on the bed) + +SUPPORTS = {'enable_support': '1', 'support_type': 'tree(auto)'} +SOLID = {'sparse_infill_density': '100%'} +STRONG = {'wall_loops': '4', 'sparse_infill_density': '40%'} + +# (file, filament 1=PLA 2=PETG, orientation, count, print settings) +PLATES = [ + ('Fit tests - print these first', [ + ('00-fit-test-608.stl', 2, None, 1, {}), + ('00b-fit-test-6804.stl', 2, None, 1, {}), + ]), + ('Shell 1 + 2 (PLA)', [('01-shell-Q1-cable-notch.stl', 1, None, 1, {}), ('02-shell-Q2.stl', 1, None, 1, {})]), + ('Shell 3 + 4 (PLA)', [('03-shell-Q3.stl', 1, None, 1, {}), ('04-shell-Q4.stl', 1, None, 1, {})]), + ('Deck 1 + 2 (PLA)', [('05-deck-Q1-arduino.stl', 1, FLIP, 1, {}), ('06-deck-Q2.stl', 1, FLIP, 1, {})]), + ('Deck 3 + 4 (PLA)', [('07-deck-Q3.stl', 1, FLIP, 1, {}), ('08-deck-Q4.stl', 1, FLIP, 1, {})]), + ('Sand ring (PLA)', [('21-sand-ring-Q1.stl', 1, None, 1, {}), ('22-sand-ring-Q2.stl', 1, None, 1, {}), + ('23-sand-ring-Q3.stl', 1, None, 1, {}), ('24-sand-ring-Q4.stl', 1, None, 1, {})]), + ('Centre (PETG)', [ + ('09-center-plate.stl', 2, FLIP, 1, {}), + ('10-hub-80T-pulley-bolted.stl', 2, FLIP, 1, STRONG), + ('11-hub-clamp-collar.stl', 2, None, 1, STRONG), + ('26-slip-ring-coupler.stl', 2, None, 1, {}), + ('27-slip-ring-holder.stl', 2, None, 1, {}), + ]), + ('Arm (PETG)', [ + ('17-carriage.stl', 2, FLIP, 1, STRONG), + ('13-arm-motor-bracket-SUPPORTS-tall-window.stl', 2, FLIP, 1, dict(STRONG, **SUPPORTS)), + ('15-idler-bracket.stl', 2, None, 1, STRONG), + ('16-idler-sleeve-wide.stl', 2, None, 1, {}), + ('18-magnet-cup-short.stl', 2, None, 1, {}), + ('14-switch-mount-pegs.stl', 2, None, 1, {}), + ('19-roller-washers-x4.stl', 2, None, 1, {}), + ('19b-idler-washer.stl', 2, None, 1, {}), + ('20-magnet-spacers-1-2-3mm.stl', 2, None, 1, {}), + ('25-carriage-striker.stl', 2, None, 1, {}), + ('30-motor-bracket-shim.stl', 2, None, 1, {}), + ]), + ('Counterweight, Uno plate, seam parts (PLA)', [ + ('34-counterweight.stl', 1, None, 1, SOLID), + ('35-counterweight-lid.stl', 1, None, 1, {}), + ('36-uno-turnaround-plate.stl', 1, None, 1, {}), + ('31-seam-plate-low-x4.stl', 1, None, 4, {}), + ('32-seam-plate-high-x4.stl', 1, None, 4, {}), + ('33-seam-plug-x16.stl', 1, None, 16, {}), + ]), +] + +OPTIONS = [ + ('Sand ring 12.5 mm 1 + 2 - only for a raised floor', [('21-sand-ring-Q1-12.5mm.stl', 1, None, 1, {}), + ('22-sand-ring-Q2-12.5mm.stl', 1, None, 1, {})]), + ('Sand ring 12.5 mm 3 + 4 - only for a raised floor', [('23-sand-ring-Q3-12.5mm.stl', 1, None, 1, {}), + ('24-sand-ring-Q4-12.5mm.stl', 1, None, 1, {})]), + ('Switch mount for 2x M2 screws (instead of the peg one)', [('14-switch-mount-M2.stl', 2, None, 1, {})]), +] + + +# ---------------------------------------------------------------- presets +def _find(kind, name): + for k in (kind, 'machine', 'process', 'filament'): + p = os.path.join(SYS, k, name + '.json') + if os.path.exists(p): + return p + raise FileNotFoundError(f'Bambu Studio preset {name!r} not found under {SYS}') + + +def flatten(kind, name): + """A system preset with its `inherits` chain and `include` templates merged in, so the + command line gets the whole thing (it does not follow them itself).""" + with open(_find(kind, name)) as f: + own = json.load(f) + out = {} + if own.get('inherits'): + out.update(flatten(kind, own['inherits'])) + for inc in own.get('include', []) or []: + out.update({k: v for k, v in flatten(kind, inc).items() if k not in ('name', 'inherits', 'include')}) + out.update({k: v for k, v in own.items() if k not in ('inherits', 'include')}) + return out + + +# ---------------------------------------------------------------- meshes +def write_oriented(src, dst, how): + """Copy an STL turned the way it prints, centred, and sitting on z = 0 (the files keep their + place in the table's model, so most of them float far above the bed).""" + b = open(src, 'rb').read() + n = struct.unpack(' (x, -y, -z) + for k in range(0, 12, 3): + v[k + 1], v[k + 2] = -v[k + 1], -v[k + 2] + tris.append(v) + pts = [(v[k], v[k + 1], v[k + 2]) for v in tris for k in (3, 6, 9)] + cx = (min(p[0] for p in pts) + max(p[0] for p in pts)) / 2 + cy = (min(p[1] for p in pts) + max(p[1] for p in pts)) / 2 + z0 = min(p[2] for p in pts) + out = bytearray(b[:84]) + for v in tris: + for k in (3, 6, 9): + v[k], v[k + 1], v[k + 2] = v[k] - cx, v[k + 1] - cy, v[k + 2] - z0 + out += struct.pack('<12fH', *v, 0) + open(dst, 'wb').write(out) + + +def build(plates, out_3mf): + os.makedirs(WORK, exist_ok=True) + os.makedirs(OUT, exist_ok=True) + settings = [] + for kind, name in (('machine', MACHINE), ('process', PROCESS)): + p = os.path.join(WORK, f'{kind}.json') + json.dump(flatten(kind, name), open(p, 'w'), indent=1) + settings.append(p) + fils = [] + for i, name in enumerate((PLA, PETG), 1): + p = os.path.join(WORK, f'filament{i}.json') + json.dump(flatten('filament', name), open(p, 'w'), indent=1) + fils.append(p) + asm = {'plates': []} + for title, objs in plates: + plate = {'plate_name': title, 'need_arrange': True, 'objects': []} + for fn, fil, how, count, params in objs: + dst = os.path.join(WORK, fn) + write_oriented(os.path.join(V3, fn), dst, how) + plate['objects'].append({'path': dst, 'count': count, 'filaments': [fil], + 'pos_x': [0.0] * count, 'pos_y': [0.0] * count, 'pos_z': [0.0] * count, + 'print_params': params}) + asm['plates'].append(plate) + asm_path = os.path.join(WORK, 'assemble.json') + json.dump(asm, open(asm_path, 'w'), indent=1) + tmp = os.path.join(WORK, 'out.3mf') + if os.path.exists(tmp): + os.remove(tmp) + cmd = ['flatpak', 'run', '--command=bambu-studio', 'com.bambulab.BambuStudio', + '--load-settings', ';'.join(settings), '--load-filaments', ';'.join(fils), + '--load-assemble-list', asm_path, '--allow-rotations', '--export-3mf', 'out.3mf', '--outputdir', WORK] + r = subprocess.run(cmd, capture_output=True, text=True, timeout=900, cwd=WORK) # it leaves a result.json behind + if not os.path.exists(tmp): + sys.exit('Bambu Studio did not write the 3mf:\n' + r.stdout[-3000:] + r.stderr[-3000:]) + set_bed_type(tmp, out_3mf) + return out_3mf + + +COLOURS = ['#161616', '#A8A8A8'] # PLA black (the case), PETG grey (the moving parts) + + +def set_bed_type(src, dst, bed='Textured PEI Plate'): + """Fix up the project settings the command line leaves unfinished: + - the bed type (there is no option for it); + - per-filament lists that it leaves with one value although there are two filaments + (colour, filament-to-nozzle map and others). Slicing a plate printed in filament 2 then + crashes, so give every such list one value per filament.""" + zin = zipfile.ZipFile(src) + with zipfile.ZipFile(dst + '.tmp', 'w', zipfile.ZIP_DEFLATED) as zout: + for item in zin.infolist(): + data = zin.read(item.filename) + if item.filename == 'Metadata/project_settings.config': + cfg = json.loads(data) + cfg['curr_bed_type'] = bed + n = len(cfg['filament_type']) + for k, v in cfg.items(): + if k.startswith(('filament_', 'first_x_layer')) and isinstance(v, list) and len(v) == 1 and n > 1: + cfg[k] = v * n + cfg['filament_colour'] = COLOURS[:n] + cfg['filament_map'] = ['1'] * n + # purge volumes when changing filament: an n x n table (no plate mixes the two) + cfg['flush_volumes_matrix'] = ['0' if i == j else '280' for i in range(n) for j in range(n)] + cfg['flush_volumes_vector'] = ['140'] * (2 * n) + data = json.dumps(cfg, indent=4).encode() + zout.writestr(item, data) + os.replace(dst + '.tmp', dst) + + +if __name__ == '__main__': + for plates, name in ((PLATES, 'sand-table-v3-P1S.3mf'), (OPTIONS, 'sand-table-v3-P1S-options.3mf')): + print('wrote', build(plates, os.path.join(OUT, name)))