initial commit

This commit is contained in:
Dennis Gunia
2025-09-11 21:57:16 +02:00
commit ebd90083e7
10 changed files with 541 additions and 0 deletions

View File

@@ -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"

35
fillament_presets/bl.json Normal file
View File

@@ -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"
]
}

35
fillament_presets/wt.json Normal file
View File

@@ -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"
]
}

177
generator.py Executable file
View File

@@ -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()

BIN
media/dimensions.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

81
readme.md Normal file
View File

@@ -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 <path-to-config>
```
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.

7
result.json Normal file
View File

@@ -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
}

View File

@@ -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();
}

View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8"?><svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 110.01 122.88" style="enable-background:new 0 0 110.01 122.88" xml:space="preserve"><style type="text/css">.st0{fill-rule:evenodd;clip-rule:evenodd;}</style><g><path class="st0" d="M1.87,14.69h22.66L24.5,14.3V4.13C24.5,1.86,26.86,0,29.76,0c2.89,0,5.26,1.87,5.26,4.13V14.3l-0.03,0.39 h38.59l-0.03-0.39V4.13C73.55,1.86,75.91,0,78.8,0c2.89,0,5.26,1.87,5.26,4.13V14.3l-0.03,0.39h24.11c1.03,0,1.87,0.84,1.87,1.87 v19.46c0,1.03-0.84,1.87-1.87,1.87H1.87C0.84,37.88,0,37.04,0,36.01V16.55C0,15.52,0.84,14.69,1.87,14.69L1.87,14.69z M0.47,42.19 h109.08c0.26,0,0.46,0.21,0.46,0.46l0,0v79.76c0,0.25-0.21,0.46-0.46,0.46l-109.08,0c-0.25,0-0.47-0.21-0.47-0.46V42.66 C0,42.4,0.21,42.19,0.47,42.19L0.47,42.19L0.47,42.19z M8.23,49.97h95.07c0.52,0,0.94,0.45,0.94,0.94v64.08 c0,0.49-0.45,0.94-0.94,0.94H7.77c-0.49,0-0.94-0.42-0.94-0.94l0-63.63c0-1.03,0.84-1.86,1.86-1.86L8.23,49.97L8.23,49.97z M78.34,29.87c2.89,0,5.26-1.87,5.26-4.13V15.11l-0.03-0.41H73.11l-0.03,0.41v10.16c0,2.27,2.36,4.13,5.25,4.13L78.34,29.87 L78.34,29.87z M29.29,29.87c2.89,0,5.26-1.87,5.26-4.13V15.11l-0.03-0.41H24.07l-0.03,0.41v10.16c0,2.27,2.36,4.13,5.25,4.13 L29.29,29.87L29.29,29.87z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

49
templates/svgs/plane-icon.svg Executable file
View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 24.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 122.88 122.88"
style="enable-background:new 0 0 122.88 122.88;"
xml:space="preserve"
sodipodi:docname="plane-icon.svg"
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="3.3813477"
inkscape:cx="29.574007"
inkscape:cy="93.601731"
inkscape:window-width="1920"
inkscape:window-height="1009"
inkscape:window-x="1042"
inkscape:window-y="342"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" />
<style
type="text/css"
id="style1">
.st0{fill-rule:evenodd;clip-rule:evenodd;}
</style>
<g
id="g1">
<path
class="st0"
d="m 2.5211474,60.747063 c 2.289718,-2.278382 5.820644,-3.21354 10.4340836,-3.598938 L 8.4608114,37.192442 c -0.436407,-1.105185 0.05101,-1.558595 1.054177,-1.666279 l 4.8004716,0.37973 c 0.759461,0.181364 1.382899,0.555426 1.773965,1.241208 L 28.626196,54.784728 54.81059,53.741887 45.317331,4.5412947 c -0.306052,-1.178864 0.124687,-1.734291 1.462245,-1.541592 l 7.611611,0.85581 27.504947,49.0078933 23.384586,-0.782131 c 7.07319,0.510085 12.22505,2.726124 14.35608,6.251382 1.1732,1.943993 1.25821,2.958497 0.27772,4.90249 -1.93267,3.802971 -7.23756,6.206042 -14.6338,6.744465 l -23.384586,-0.782131 -27.504947,49.007889 -7.611611,0.85581 c -1.331891,0.18704 -1.762629,-0.36838 -1.462245,-1.54159 L 54.799255,68.319 28.620528,67.270491 16.083757,84.908118 c -0.385398,0.680119 -1.003168,1.059849 -1.773964,1.241209 l -4.8004726,0.37973 c -0.997501,-0.10202 -1.490583,-0.56109 -1.054177,-1.66628 L 12.949564,64.907095 C 8.3191204,64.516029 4.7881944,63.580872 2.4928094,61.285486 2.2887754,61.070117 2.3171134,60.951097 2.5211474,60.747063 Z"
id="path1"
style="stroke-width:0.801522" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB