commit ebd90083e760c4fbb2cc3557c37db248bcf22859 Author: Dennis Gunia Date: Thu Sep 11 21:57:16 2025 +0200 initial commit diff --git a/configs/flap_config_1.yaml b/configs/flap_config_1.yaml new file mode 100644 index 0000000..6e49527 --- /dev/null +++ b/configs/flap_config_1.yaml @@ -0,0 +1,36 @@ +defaults: # default values for all flaps. can be overwritten in 'flaps' + font: "TW Cen MT Condensed:style=Bold" + font_size: 40 + font_y_scale: 1.3 + svg_scale: 0.8 + string: ' ' + svg: '' + +globals: + flap_width: 42.8 # flap width + flap_height: 35 # flap height + flap_thickness: 1 # flap thickness + flap_recess: 18 # recessed area in mm + flap_recess_width: 2 # recessed area width + flap_tap_width: 1 # flap tap width (shoud be smaller than d) + flap_tap_offset: 1 # margin to flap top + +output_path: "./out" +scad_path: "/usr/bin/openscad" +scad_file: "./templates/flap_generator_w_svg.scad" +flaps: + - string: "A" + - string: "B" + - string: "C" + - svg: "svgs/calendar-blank-icon.svg" + svg_scale: 0.8 + - svg: "svgs/plane-icon.svg" + svg_scale: 0.8 + - string: " " + +combine: # not implemented + enable: true + orca_slicer_exec: "/home/dennisgunia/Downloads/OrcaSlicer_Linux_AppImage_V2.3.0.AppImage" + fillament: + background: "./fillament_presets/bl.json" + foreground: "./fillament_presets/wt.json" diff --git a/fillament_presets/bl.json b/fillament_presets/bl.json new file mode 100644 index 0000000..5e584c7 --- /dev/null +++ b/fillament_presets/bl.json @@ -0,0 +1,35 @@ +{ + "type": "filament", + "name": "Bambu PLA Basic @base", + "inherits": "fdm_filament_pla", + "from": "system", + "filament_id": "GFA00", + "instantiation": "false", + "filament_cost": [ + "24.99" + ], + "filament_density": [ + "1.26" + ], + "filament_flow_ratio": [ + "0.98" + ], + "filament_max_volumetric_speed": [ + "12" + ], + "filament_vendor": [ + "Bambu Lab" + ], + "filament_scarf_seam_type": [ + "none" + ], + "filament_scarf_height":[ + "10%" + ], + "filament_scarf_gap":[ + "0%" + ], + "filament_scarf_length":[ + "10" + ] +} \ No newline at end of file diff --git a/fillament_presets/wt.json b/fillament_presets/wt.json new file mode 100644 index 0000000..5e584c7 --- /dev/null +++ b/fillament_presets/wt.json @@ -0,0 +1,35 @@ +{ + "type": "filament", + "name": "Bambu PLA Basic @base", + "inherits": "fdm_filament_pla", + "from": "system", + "filament_id": "GFA00", + "instantiation": "false", + "filament_cost": [ + "24.99" + ], + "filament_density": [ + "1.26" + ], + "filament_flow_ratio": [ + "0.98" + ], + "filament_max_volumetric_speed": [ + "12" + ], + "filament_vendor": [ + "Bambu Lab" + ], + "filament_scarf_seam_type": [ + "none" + ], + "filament_scarf_height":[ + "10%" + ], + "filament_scarf_gap":[ + "0%" + ], + "filament_scarf_length":[ + "10" + ] +} \ No newline at end of file diff --git a/generator.py b/generator.py new file mode 100755 index 0000000..f128ac5 --- /dev/null +++ b/generator.py @@ -0,0 +1,177 @@ +#!/bin/python3 +import yaml +from pathlib import Path +import os +import subprocess +import sys + +config_file = sys.argv[1] +scad_exec_path = "" +scad_out_path = "" +scad_input_path = "" + +# prepare config +config = {} +with open(config_file) as conf_stream: + try: + config = yaml.safe_load(conf_stream) + except yaml.YAMLError as exc: + print(exc) + +# count flaps +flap_count = len(config['flaps']) +print(f"Found {flap_count} flaps:") + +# fill in default values +for flap in config['flaps']: + for default_key in config['defaults'].keys(): + if (default_key not in flap.keys()): + flap[default_key] = config['defaults'][default_key] + print(flap) + +def generate_mesh(): + # check if openscad is installed + scad_exec = Path(config['scad_path']) + try: + scad_exec_path = scad_exec.resolve(strict=True) + except FileNotFoundError as exc: + print(exc) + else: + print(f"Using openscad from {scad_exec_path}") + + # check if output directory exists + scad_out = Path(config['output_path']) + try: + scad_out_path = scad_out.resolve(strict=True) + except FileNotFoundError as exc: + print(exc) + else: + print(f"Save flaps to {scad_out_path}") + + # clear out path + print("Clear output directory") + [f.unlink() for f in scad_out.glob("*") if f.is_file()] + + + # check if input file exists + scad_input = Path(config['scad_file']) + try: + scad_input_path = scad_input.resolve(strict=True) + except FileNotFoundError as exc: + print(exc) + else: + print(f"Use scad file {scad_input_path}") + + ## now start generating flaps + for ix, flap in enumerate(config['flaps']): + this_flap = flap + prev_flap = config['flaps'][(ix -1)%flap_count] + print (f"Generate flap {ix}") + print(this_flap) + print(prev_flap) + + cmd_1 = [f"{scad_exec}"] + cmd_2 = [f"{scad_exec}"] + + cmd_vals = [] + cmd_vals.append(f"char_top=\"{prev_flap['string']}\"") + cmd_vals.append(f"char_bottom=\"{this_flap['string']}\"") + cmd_vals.append(f"svg_top=\"{prev_flap['svg']}\"") + cmd_vals.append(f"svg_bottom=\"{this_flap['svg']}\"") + + cmd_vals.append(f"font_top=\"{prev_flap['font']}\"") + cmd_vals.append(f"font_top_size={prev_flap['font_size']}") + cmd_vals.append(f"font_top_y_scale={prev_flap['font_y_scale']}") + cmd_vals.append(f"svg_top_scale={prev_flap['svg_scale']}") + cmd_vals.append(f"font_bottom=\"{this_flap['font']}\"") + cmd_vals.append(f"font_bottom_size={this_flap['font_size']}") + cmd_vals.append(f"font_bottom_y_scale={this_flap['font_y_scale']}") + cmd_vals.append(f"svg_bottom_scale={this_flap['svg_scale']}") + + # pass globals + for global_var in config['globals'].keys(): + cmd_vals.append(f"{global_var}={config['globals'][global_var]}") + + outfile_a = os.path.join(scad_out, f"flap_{ix}_a.stl") + outfile_b = os.path.join(scad_out, f"flap_{ix}_b.stl") + + cmd_1.append("-o") + cmd_1.append(outfile_a) + cmd_2.append("-o") + cmd_2.append(outfile_b) + + # add to cmd + for cmd_val in cmd_vals: + cmd_1.append("-D") + cmd_1.append(f"{cmd_val}") + cmd_2.append("-D") + cmd_2.append(f"{cmd_val}") + + + cmd_1.append("-D") + cmd_1.append("exp_step=0") + cmd_1.append(str(scad_input_path)) + cmd_2.append("-D") + cmd_2.append("exp_step=1") + cmd_2.append(str(scad_input_path)) + # print out cli command + print (cmd_1) + # generate both parts + subprocess.run(cmd_1) + subprocess.run(cmd_2) + +def run_combine(): # not yet implemented + # combine stls into 3mf + + if config['combine']['enable']: + orca_exec = Path(config['combine']['orca_slicer_exec']) + try: + orca_exec_path = orca_exec.resolve(strict=True) + except FileNotFoundError as exc: + print(exc) + else: + print(f"Using orca slicer from {orca_exec_path}") + # check if output directory exists + scad_out = Path(config['output_path']) + try: + scad_out_path = scad_out.resolve(strict=True) + except FileNotFoundError as exc: + print(exc) + else: + print(f"Use meshes from {scad_out_path}") + + outfile_final = os.path.join(scad_out, f"flap_combine.3mf") + cmd_final = [ + str(orca_exec_path), + "--export-3mf", f"{outfile_final}", + "--debug", "1", + "--allow-multicolor-oneplate" + ] + + for ix, flap in enumerate(config['flaps']): + outfile_a = os.path.join(scad_out, f"flap_{ix}_a.stl") + outfile_b = os.path.join(scad_out, f"flap_{ix}_b.stl") + outfile = os.path.join(scad_out, f"flap_{ix}_multi.3mf") + cmd_final.append(outfile) + cmd = [ + str(orca_exec_path), + "--arrange", "1", + "--export-3mf", f"{outfile}", + "--debug", "1", + "--assemble", + "--ensure-on-bed", + "--allow-multicolor-oneplate", + "--load-filaments", f"{config['combine']['fillament']['background']};{config['combine']['fillament']['foreground']}", + "--load-filament-ids", "1,2", + outfile_a, outfile_b + ] + print(' '.join(cmd)) + subprocess.run(cmd) + # combine into final file + + print(' '.join(cmd_final)) + subprocess.run(cmd_final) + + +#generate_mesh() +run_combine() \ No newline at end of file diff --git a/media/dimensions.png b/media/dimensions.png new file mode 100644 index 0000000..06b5f2e Binary files /dev/null and b/media/dimensions.png differ diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..a25ab31 --- /dev/null +++ b/readme.md @@ -0,0 +1,81 @@ +# Parametric Split-Flap Flap Generator +This is a parametric flap generator for my 3d printed split-flap display. You can use it to easily generate your own flaps and print them with the multi-material printer of your choice. + +The generator uses a .scad file as a template. The configuration is done in a yaml file. You can find more information in the "Config" section. + +## Prequisites +The script is requires: +- OpenSCAD +- Python3 + +## Installation +1. Clone this repository +``` +git clone +``` +2. Install openSCAD on your system +``` +sudo apt install openscad +``` +3. run +``` +./generator.py +``` + +You can find an example config in `./configs/` + +## Config +The config is stored in a yaml format and contains the following sections: +### `globals` +Contains all global settings defining the dimensions of a single flap. You can change these values to fit your specific project: + +![dimensions drawing](media\dimensions.png) + +### `defaults` +This object contains all default values for a flap. These can be overwritten for each flap. If a specific property is not defined on a flap, this default setting is used. + +- `font`: Font used for text +- `font_size`: Default font size +- `font_y_scale`: Stretches font on y axis +- `svg_scale`: Scale for svg +- `string`: default text. Rcommended to use ' ', even for svg. +- `svg`: absolute path to svg file. If empty, the text is generated. This can also be r path relative to the location of `scad_file` + +### `flaps` +Array of each flap. you can specify each of the above mentioned parameters per flap. Every parameter set here overwerites the default parameters. + +For example: +``` +flaps: + - string: "A" + - string: "B" + - string: "C" + - svg: "C:\\\\Users\\\\DG\\\\Downloads\\\\calendar-blank-icon.svg" + svg_scale: 0.8 +``` + +### `output_path` +This is the string to the output directory. The `.stl` files are stored here. +### `scad_path` +Absolute path to the OpenSCAD executable on your system. For linux you can get it by running `which openscad` +### `scad_file` +Absolute path to the OpenSCAD file used to generate the mesh. Examples located in `./templates/` + +## Printing results +All files are stored in the directory specified by `output_path`. +- The `*_a.stl` file contains the flap itself. This should be printed in black. +- The `*_b.stl` files contains the text or graphics. It should be printed in the foreground color. + +If you use Orca Slicer or Bambu Studio, you can drag both files simultaneously into the slice. It will group them, which makes handling easier. You still have to manually select the fillaments for both parts (foregournd and background). + +## Generate 3mf from stl files +I am working on a way to combine the stl files into one 3mf file. +This requires you to install the Orca slicer from https://orca-slicer.com/#download + +You need to specify it in the config under `combine.orca_slicer_exec`. + +The settings are made under the `combine`section in the config file. + +- `enable`: enables combining the files +- `orca_slicer_exec`: path to the orca slider executable (NOTE: this only works on linux and mac.) +- `fillament`: map of fillament settings to use. You can keep those settings. They only force orca to treat them as two different fillaments. You can change them later manually. \ No newline at end of file diff --git a/result.json b/result.json new file mode 100644 index 0000000..ef8c95f --- /dev/null +++ b/result.json @@ -0,0 +1,7 @@ +{ + "error_string": "Unsupported 3MF version. Please make sure the 3MF file was created with the official version of Bambu Studio, not a beta version.", + "export_time": 105869091762224, + "plate_index": 0, + "prepare_time": 41, + "return_code": -24 +} diff --git a/templates/flap_generator_w_svg.scad b/templates/flap_generator_w_svg.scad new file mode 100644 index 0000000..a40df8c --- /dev/null +++ b/templates/flap_generator_w_svg.scad @@ -0,0 +1,120 @@ +// +// Parametric Split-Flap Generator V1.0 +// (c) Dennis Gunia 2025 - www.dennisgunia.de +// +// Generates printable 3d multi-material model for each flap! +// +flap_width = 42.8*1; // flap width +flap_height = 35; // flap height +flap_thickness = 1; // flap thickness +flap_recess = 18; // recessed area in mm +flap_recess_width = 2; // recessed area width +flap_tap_width = 1; // flap tap width (shoud be smaller than d) +flap_tap_offset = 1; // margin to flap top + +char_bottom = "A"; // char for front (top) +char_top = "W"; // char for back (previous) + +// alternate SVG +svg_top = ""; +svg_top_scale = 0.8; +svg_bottom = ""; +svg_bottom_scale = 0.8; + + +// Font settings +font_bottom = "TW Cen MT Condensed:style=Bold"; +font_bottom_size = 40; +font_bottom_y_scale = 1.3; + +font_top = "TW Cen MT Condensed:style=Bold"; +font_top_size = 40; +font_top_y_scale = 1.3; + +// export step +exp_step = 0; + +// Generate single sided text +module text2d(t, size, font, rotate180=false, y_scale){ + rotate(rotate180 ? 180 : 0) + scale([1,y_scale]) + text(t, size=size, font=font, halign="center", valign="center"); +} + +// Generate base +module base_plate(){ + color([0.1,0.1,0.1]) + translate([0,flap_height/-2,0]){ + union(){ + linear_extrude(flap_thickness) + square([flap_width - flap_recess_width*2, flap_height], center=true); + //generate tab + translate([(flap_width/2)*-1,flap_height/2-(flap_tap_offset+flap_tap_width),0]){ + linear_extrude(flap_thickness) + square([flap_width, flap_tap_width]); + }; + // generate lower part + translate([(flap_width/2)*-1,(flap_height/2)*-1,0]){ + linear_extrude(flap_thickness) + square([flap_width, flap_height-flap_recess]); + }; + } + } +} + +// Generate and translate Text +module text_component(str_bottom, str_top,oversize){ + if (len(svg_bottom) == 0){ + // generate bottom text + translate([0,0,-oversize]){ + mirror([1,0,0]) + linear_extrude(height=0.4+oversize) + text2d(str_bottom,font_top_size,font_bottom,false,font_bottom_y_scale); + } + } else { + // generate bottm shape from svg + translate([0,0,-oversize]){ + mirror([1,0,0]) + linear_extrude(height=0.4+oversize) + scale([svg_bottom_scale,svg_bottom_scale]) + import(svg_bottom, center=true); + } + } + if (len(svg_top) == 0){ + // generate top text + translate([0,0,flap_thickness-0.4+oversize]){ + linear_extrude(height=0.4+oversize) + text2d(str_top,font_top_size,font_top,true,font_top_y_scale); + } + } else { + // generate bottm shape from svg + translate([0,0,flap_thickness-0.4+oversize]){ + rotate(180) + linear_extrude(height=0.4+oversize) + scale([svg_top_scale,svg_top_scale]) + import(svg_top, center=true); + } + } +} + +// assemble final flap +module base(){ + difference(){ + base_plate(); + text_component(char_bottom,char_top,0.1); + } +} +module txt1(){ + difference(){ + color("white") + text_component(char_bottom,char_top,0.0); + translate([flap_width/-2,0,-0.1]) cube([flap_width,flap_height,flap_thickness+0.2]); + } +} +if (exp_step == 0){ + group("base") + base(); +} else { + group("text") + txt1(); +} diff --git a/templates/svgs/calendar-blank-icon.svg b/templates/svgs/calendar-blank-icon.svg new file mode 100755 index 0000000..7ec2075 --- /dev/null +++ b/templates/svgs/calendar-blank-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/templates/svgs/plane-icon.svg b/templates/svgs/plane-icon.svg new file mode 100755 index 0000000..f885576 --- /dev/null +++ b/templates/svgs/plane-icon.svg @@ -0,0 +1,49 @@ + + + + + + + + +