Bambu Studio projects for the P1S: every v3 part on 9 plates
print/3mf/sand-table-v3-P1S.3mf: every part oriented for printing, PLA case parts and PETG moving parts, supports only on the arm motor bracket, stronger walls on the hub, carriage and brackets, solid counterweight. Slices on all 9 plates: about 23.5 h, 760 g PLA + 165 g PETG. print/3mf/sand-table-v3-P1S-options.3mf: 12.5 mm sand ring, M2 switch mount. print/tools/make_3mf.py builds both from the STLs with the Bambu Studio command line (flattened system presets, fixed-up per-filament settings). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
58129c2b59
commit
8967271e90
5 files changed
+215
No files matched your search
Binary file not shown.
Binary file not shown.
@@ -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 |
|
||||
|---|---|---|
|
||||
|
||||
@@ -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('<I', b[80:84])[0]
|
||||
tris = []
|
||||
for i in range(n):
|
||||
v = list(struct.unpack('<12f', b[84 + 50 * i:84 + 50 * i + 48]))
|
||||
if how == FLIP: # turn over about X: (x, y, z) -> (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)))
|
||||
Reference in new issue
Block a user