Skip to content

Commit fa79c56

Browse files
3d printed flap cleanup (#260)
Base of this pull request is the flap3dcleanup branch - those changes had this description from Scott: - follow typical generate_ patterns for the script - make script executable - support font specification and custom text - don't require specifying input/output directories - export multiple flaps in parallel w/ threadpool rendering - revert changes to flap.scad example rendering; extract the 3dp-specific setup to flap_3dp.scad file Additional changes are: - Update the generate_3d_print_flaps.py file to just call openscad directly for each of the needed files (flap and letters) instead of doing a single call and then trying to split the output by color. - Update flap_3dp.scad: - Change the flap STL to subtract out the letters - Add support for generating just the flap or just the letters or both at the same time --------- Co-authored-by: Scott Bezek <scottbez1@gmail.com>
1 parent 05a0898 commit fa79c56

7 files changed

Lines changed: 208 additions & 42 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,6 @@ sensor.xml
2222
*.hex
2323

2424
.pio
25+
.vscode
26+
.nvmrc
27+
.python_version

3d/flap.scad

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,6 @@ letter_facet_number = 100;
3939
vertical_keepout_mode = 1; // 0=ignore; 1=highlight; 2=cut
4040
vertical_keepout_size_factor = 1.1; // Expand calculated keepout region by this factor. 1=no expansion, 1.5=50% expansion, etc
4141

42-
// generate lower half of the flap
43-
generate_second_half = true;
44-
45-
// index number of flap, see flap_characters.scad
46-
flap_number = 1;
47-
48-
// generate 3d printable flap for 0.15mm layer height
49-
print_3d = false;
50-
5142
vertical_keepout_size = get_flap_arc_separation() * vertical_keepout_size_factor;
5243
font_extrusion_3d = 0.3;
5344
font_extrusion = 0.1;
@@ -218,11 +209,12 @@ module flap_with_letters(flap_color, letter_color, flap_index, flap_gap, flap=tr
218209
}
219210
}
220211

221-
flap_gap = get_flap_gap();
222-
flap_with_letters([1,0,0], [1,1,0], flap_index=flap_number, flap_gap=flap_gap, bleed=0, print_3d=true);
223-
translate([0, -flap_pin_width-flap_gap, 0])
224-
if (generate_second_half) {
225-
rotate([180, 0, 0]) {
226-
flap_with_letters([1,0,0], [1,1,0], flap_index=flap_number - 1, flap_gap=flap_gap, bleed=0, print_3d=true);
227-
}
212+
// Example:
213+
i = 1;
214+
gap = 5;
215+
216+
flap_with_letters([1,0,0], [1,1,0], flap_index=i, flap_gap=gap, bleed=2);
217+
translate([0, -flap_pin_width-gap, 0])
218+
rotate([180, 0, 0]) {
219+
flap_with_letters([1,0,0], [1,1,0], flap_index=i - 1, flap_gap=gap, bleed=2);
228220
}

3d/flap_3dp.scad

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
Copyright 2025 Scott Bezek and the splitflap contributors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
use<flap.scad>;
17+
18+
// TODO: extract core flap spool dimensions used for flap gap instead of using the full splitflap file
19+
use<splitflap.scad>;
20+
21+
generateFullFlap = false; // If true, generates the full flap STL
22+
generateText = false; // If generateFullFlap is false, this controls what part of the STL to generate
23+
generateFrontText = true; // If generateText is true, this controls generation of the front text
24+
generateBackText = true; // If generateText is true, this controls generation of the back text
25+
26+
flap_number = 0;
27+
28+
flap_gap = get_flap_gap();
29+
if (generateFullFlap) {
30+
// Generate the full flap using the standard function
31+
flap_with_letters([0,0,0], [1,1,1], flap_index=flap_number, flap_gap=flap_gap, bleed=0, print_3d=true);
32+
} else {
33+
if (generateText == false) {
34+
// Generate just the flap STL with the letters removed
35+
difference() {
36+
flap_with_letters([0,0,0], [1,1,1], front_letter=false, back_letter=false, flap_index=flap_number, flap_gap=flap_gap, bleed=0, print_3d=true);
37+
flap_with_letters([0,0,0], [1,1,1], flap=false, flap_index=flap_number, flap_gap=flap_gap, bleed=0, print_3d=true);
38+
}
39+
} else {
40+
// Generate just the letters STL
41+
flap_with_letters([0,0,0], [1,1,1], front_letter=generateFrontText, back_letter=generateBackText, flap=false, flap_index=flap_number, flap_gap=flap_gap, bleed=0, print_3d=true);
42+
}
43+
}

3d/font_generator.scad

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,10 @@ bleed = 0; // Amount of bleed (in mm) for text to expand be
6666

6767
flap_color = [1,1,1];
6868
letter_color = [0,0,0];
69+
print_3d = false;
6970

7071
render_alignment_marks = false; // Whether to render markings to help with alignment/registration (e.g. for screen printing)
7172

72-
print_3d = false; // embeds and substracts a 0.3mm think font into the flap. Used together with colored_stl_exporter.py
73-
7473
// ---------------------------
7574
// End configurable parameters
7675
// ---------------------------

3d/scripts/colored_flap_exporter.py

Lines changed: 0 additions & 23 deletions
This file was deleted.
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
#!/usr/bin/env python3
2+
3+
# Copyright 2025 Scott Bezek and the splitflap contributors
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
18+
from __future__ import division
19+
from __future__ import print_function
20+
21+
import argparse
22+
import logging
23+
import os
24+
import sys
25+
import time
26+
import openscad
27+
28+
from multiprocessing.dummy import Pool
29+
from shutil import rmtree
30+
31+
script_dir = os.path.dirname(os.path.abspath(__file__))
32+
source_parts_dir = os.path.dirname(script_dir)
33+
repo_root = os.path.dirname(source_parts_dir)
34+
sys.path.append(repo_root)
35+
36+
from util import file_util
37+
38+
if __name__ == "__main__":
39+
logging.basicConfig(level=logging.DEBUG)
40+
41+
parser = argparse.ArgumentParser()
42+
parser.add_argument(
43+
"-f",
44+
"--font",
45+
type=str,
46+
help="Name of font preset to use - see flap_fonts.scad",
47+
)
48+
parser.add_argument("-t", "--text", type=str, help="String of text to generate")
49+
parser.add_argument(
50+
"--vertical-keepout-clip",
51+
action="store_true",
52+
help="Clip letters that violate vertical keepout zones",
53+
)
54+
parser.add_argument(
55+
"--single-stl",
56+
action="store_true",
57+
default=False,
58+
help="Generate a single STL file for each flap",
59+
)
60+
parser.add_argument(
61+
"--single-letter-stl",
62+
action="store_true",
63+
default=False,
64+
help="Generate a single STL file for the front and back letters (only used if --single-stl is not set)",
65+
)
66+
parser.add_argument(
67+
"--parallelism",
68+
type=int,
69+
help="How many OpenSCAD exports to run in parallel. Defaults to CPU count if unspecified.",
70+
default=None,
71+
)
72+
args = parser.parse_args()
73+
74+
extra_variables = {}
75+
76+
if args.font is not None:
77+
extra_variables["font_preset"] = args.font
78+
79+
if args.text is not None:
80+
extra_variables["character_list"] = args.text
81+
82+
if args.vertical_keepout_clip:
83+
extra_variables["vertical_keepout_mode"] = 2
84+
85+
output_directory = os.path.join(source_parts_dir, "build", "flap_3dp")
86+
rmtree(output_directory, ignore_errors=True)
87+
88+
def render_flap(flap_number):
89+
flap_path = os.path.join(output_directory, f"flap_{flap_number:02}")
90+
file_util.mkdir_p(flap_path)
91+
openscad_variables = {
92+
"flap_number": flap_number,
93+
"generateFullFlap": args.single_stl,
94+
"generateText": False,
95+
}
96+
openscad_variables.update(extra_variables)
97+
98+
openscad.run(
99+
os.path.join(source_parts_dir, "flap_3dp.scad"),
100+
os.path.join(flap_path, f"{flap_number:02}_flap.stl"),
101+
variables=openscad_variables,
102+
capture_output=True,
103+
)
104+
105+
if not args.single_stl:
106+
if args.single_letter_stl:
107+
openscad_variables["generateText"] = True
108+
openscad.run(
109+
os.path.join(source_parts_dir, "flap_3dp.scad"),
110+
os.path.join(flap_path, f"{flap_number:02}_letters.stl"),
111+
variables=openscad_variables,
112+
capture_output=True,
113+
ignore_empty_top_level_object=True,
114+
)
115+
else:
116+
openscad_variables["generateText"] = True
117+
openscad_variables["generateFrontText"] = True
118+
openscad_variables["generateBackText"] = False
119+
openscad.run(
120+
os.path.join(source_parts_dir, "flap_3dp.scad"),
121+
os.path.join(flap_path, f"{flap_number:02}_letter_front.stl"),
122+
variables=openscad_variables,
123+
capture_output=True,
124+
ignore_empty_top_level_object=True,
125+
)
126+
127+
openscad_variables["generateText"] = True
128+
openscad_variables["generateFrontText"] = False
129+
openscad_variables["generateBackText"] = True
130+
openscad.run(
131+
os.path.join(source_parts_dir, "flap_3dp.scad"),
132+
os.path.join(flap_path, f"{flap_number:02}_letter_back.stl"),
133+
variables=openscad_variables,
134+
capture_output=True,
135+
ignore_empty_top_level_object=True,
136+
)
137+
138+
pool = Pool(args.parallelism)
139+
num_flaps = len(args.text) if args.text is not None else 52
140+
for _ in pool.imap_unordered(render_flap, range(num_flaps)):
141+
# Consume results as they occur so any exception is rethrown
142+
pass
143+
pool.close()
144+
pool.join()

3d/scripts/openscad.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ def run(
5151
variables=None,
5252
capture_output=False,
5353
colorscheme=None,
54+
ignore_empty_top_level_object=False,
5455
):
5556

5657
command = [
@@ -96,6 +97,10 @@ def run(
9697

9798
logger.info(command)
9899

100+
if ignore_empty_top_level_object:
101+
# In order to ignore empty top level object errors, we need to be able to look at stderr
102+
capture_output = True
103+
99104
stdout_type = subprocess.PIPE if capture_output else None
100105
stderr_type = subprocess.PIPE if capture_output else None
101106
try:
@@ -111,7 +116,10 @@ def run(
111116

112117
logger.debug('returncode:%d', returncode)
113118
if returncode != 0:
114-
raise OpenSCADException('openscad returned non-zero!', returncode, stdout=stdout, stderr=stderr)
119+
if ignore_empty_top_level_object and b'Current top level object is empty.' in stderr:
120+
logger.info('Ignoring empty top level object!')
121+
else:
122+
raise OpenSCADException('openscad returned non-zero!', returncode, stdout=stdout, stderr=stderr)
115123
return stdout, stderr
116124

117125

0 commit comments

Comments
 (0)