Skip to content

Instantly share code, notes, and snippets.

@vjeranc
Last active January 13, 2026 19:56
Show Gist options
  • Select an option

  • Save vjeranc/c65f8d02833d5c8bb8b9b1e7a855cef1 to your computer and use it in GitHub Desktop.

Select an option

Save vjeranc/c65f8d02833d5c8bb8b9b1e7a855cef1 to your computer and use it in GitHub Desktop.
FreeCAD suitcase push button with filled full shapes for durability, edge cylinders deep enough to catch the metal rods
#!/usr/bin/env python3
"""
FreeCAD script to create a mirrored solid box with elliptical depth profile and curved protrusions:
- Main box: 45mm x 13-14mm (elliptical) x 18.5mm (solid, created by mirroring 22.5mm half)
- Elliptical depth profile: 13mm at edges → 14mm at center (smooth curve)
- Protrusions: 40mm wide x 7mm tall on each side (125mm total width)
- First 28mm: 13mm → 7mm deep (smooth curved transition from main box)
- Last 12mm: 7mm → 5mm deep, wraps around tip cylinder (circular contour on right end)
- Main cylinder holes: 8mm diameter holes through protrusions (for handle mechanism)
- Tip cylinders: 5mm outer diameter, 3mm inner, 12mm tall (extends 5mm below), with holes through protrusion
- Junction dents: 4mm wide x 14mm deep x 2mm tall dent at protrusion-box interface
- Text imprint: "Sara" in Iosevka SS06 Heavy Extended font, 8mm height, 0.5mm deep on top center
- Main box: Completely solid (no hollow interior)
- Blue color, perfect symmetry through mirroring
"""
import FreeCAD as App
import Part
from FreeCAD import Vector
def create_solid_box():
"""Create a solid rectangular box with protrusions and cylinders."""
# Create new document
doc = App.newDocument("SolidBox")
# Dimensions (half width for mirroring)
width = 22.5 # mm (will be mirrored to 45mm total)
depth_edge = 13.0 # mm at edges (where it meets protrusions)
depth_center = 14.0 # mm at center (peak of elliptical profile)
height = 18.5 # mm
# Create main box with elliptical depth profile
# Depth varies from 13mm at edges to 14mm at center using elliptical curve
import math
# Create cross-sections at multiple x positions with varying depths
num_sections = 20 # More sections = smoother curve
wires = []
for i in range(num_sections + 1):
# Calculate x position and corresponding depth using elliptical formula
x_pos = (i / num_sections) * width
# Elliptical profile: depth increases from edge to center
# Using: depth = depth_edge + (depth_center - depth_edge) * sqrt(1 - ((x - width) / width)²)
normalized_x = (x_pos - width) / width # -1 at x=0, 0 at x=width
ellipse_factor = math.sqrt(max(0, 1 - normalized_x * normalized_x))
current_depth = depth_edge + (depth_center - depth_edge) * ellipse_factor
# Create rectangular wire for this cross-section (bottom face)
# Each section has varying depth based on elliptical curve
y_offset = (depth_center - current_depth) / 2 # Center the varying depth
# Create a rectangular wire at this x position
v1 = Vector(x_pos, y_offset, 0)
v2 = Vector(x_pos, y_offset + current_depth, 0)
v3 = Vector(x_pos, y_offset + current_depth, height)
v4 = Vector(x_pos, y_offset, height)
# Create wire from edges
edge1 = Part.makeLine(v1, v2)
edge2 = Part.makeLine(v2, v3)
edge3 = Part.makeLine(v3, v4)
edge4 = Part.makeLine(v4, v1)
wire = Part.Wire([edge1, edge2, edge3, edge4])
wires.append(wire)
# Create solid by lofting through all cross-sections
main_box = Part.makeLoft(wires, True) # True = create solid
# Create side protrusion with two sections:
# - First 28mm: 13mm depth tapering to 7mm (curved transition)
# - Last 12mm: 7mm depth (centered)
protrusion_width = 40.0 # extends outward from left side (x-axis)
protrusion_height = 7.0 # along z-axis (height)
# Section 1: Tapered depth section (28mm wide, curves from 13mm to 7mm depth)
section1_width = 28.0 # 28mm from main box edge
section1_depth_start = depth_edge # 13mm at main box junction
section1_depth_end = 7.0 # 7mm where it meets section 2
# Create lofted protrusion section 1 with curved depth transition
num_protrusion_sections = 15 # Sections for smooth curve
protrusion_wires = []
for i in range(num_protrusion_sections + 1):
# Calculate x position (from 0 to -28mm)
x_pos = -(i / num_protrusion_sections) * section1_width
# Curved depth transition using smooth interpolation
# Using cosine curve for smooth transition: depth = start + (end - start) * (1 - cos(π*t)) / 2
t = i / num_protrusion_sections
curve_factor = (1 - math.cos(math.pi * t)) / 2 # Smooth S-curve
current_depth = (
section1_depth_start
+ (section1_depth_end - section1_depth_start) * curve_factor
)
# Center the varying depth in y-direction
y_offset = (depth_center - current_depth) / 2
# Create rectangular wire for this cross-section
v1 = Vector(x_pos, y_offset, 0)
v2 = Vector(x_pos, y_offset + current_depth, 0)
v3 = Vector(x_pos, y_offset + current_depth, protrusion_height)
v4 = Vector(x_pos, y_offset, protrusion_height)
edge1 = Part.makeLine(v1, v2)
edge2 = Part.makeLine(v2, v3)
edge3 = Part.makeLine(v3, v4)
edge4 = Part.makeLine(v4, v1)
wire = Part.Wire([edge1, edge2, edge3, edge4])
protrusion_wires.append(wire)
# Create solid protrusion section 1 by lofting
protrusion_section1 = Part.makeLoft(protrusion_wires, True)
# Section 2: Simple 2D profile in XY plane extruded along Z-axis
# Profile: rectangle narrowing from 7mm to 5mm depth over 9.5mm, then semicircular cap
# X direction: 12mm total width (9.5mm taper + 2.5mm semicircle)
# Y direction: depth (narrowing from 7mm to 5mm with semicircle)
# Z direction: extruded upward for 7mm (protrusion height)
section2_width = protrusion_width - section1_width # 12mm total extrusion length
section2_taper_width = 9.5 # 9.5mm for the tapered rectangle
section2_semicircle_width = (
section2_width - section2_taper_width
) # 2.5mm for semicircle
# Tip cylinder parameters
tip_outer_radius = 2.5 # 5mm diameter / 2
tip_outer_diameter = tip_outer_radius * 2 # 5mm
# Create 2D profile shape (in XY plane)
# X direction: 0 to -9.5mm (taper), then -9.5mm to -12mm (semicircle)
# Y direction: "depth" (7mm narrowing to 5mm, then semicircular cap)
section2_start_depth = 7.0
section2_end_depth = tip_outer_diameter # 5mm
# Center everything in Y direction
y_center = depth_center / 2
# Create profile points (in XY plane, z=0)
points = []
# Start at x=0 (connection point with section 1) with 7mm depth
# Left edge, bottom
points.append(Vector(0, y_center - section2_start_depth / 2, 0))
# Left edge, top
points.append(Vector(0, y_center + section2_start_depth / 2, 0))
# Taper along the top edge to narrower width over 9.5mm
# Top edge transitions from 7mm to 5mm depth along the 9.5mm length
points.append(Vector(-section2_taper_width, y_center + section2_end_depth / 2, 0))
# Semicircle on the right end (from x=-9.5mm, bulging to x=-12mm)
# The semicircle has radius 2.5mm, centered at x=-9.5mm, y=y_center
# Arc extends in the -X direction (bulging left)
num_arc_points = 15
for j in range(1, num_arc_points):
# Arc from top (90°) to bottom (270°), bulging in -X direction (180°)
# Parametric circle: x = center_x + r*cos(angle), y = center_y + r*sin(angle)
# angle sweeps from π/2 (top) to 3π/2 (bottom) passing through π (left)
angle = math.pi / 2 + math.pi * (j / num_arc_points)
px = -section2_taper_width + tip_outer_radius * math.cos(angle)
py = y_center + tip_outer_radius * math.sin(angle)
points.append(Vector(px, py, 0))
# Bottom of the semicircle (at x=-9.5mm, bottom edge)
points.append(Vector(-section2_taper_width, y_center - section2_end_depth / 2, 0))
# Bottom edge back to start (tapering back)
points.append(Vector(0, y_center - section2_start_depth / 2, 0))
# Create wire and face
profile_wire = Part.makePolygon(points)
profile_face = Part.Face(profile_wire)
# Extrude upward along Z-axis for protrusion height (7mm)
extrusion_vector = Vector(0, 0, protrusion_height)
protrusion_section2 = profile_face.extrude(extrusion_vector)
# Move to correct position (starts at x=-28)
protrusion_section2.translate(Vector(-section1_width, 0, 0))
# Combine both sections
protrusion = protrusion_section1.fuse(protrusion_section2)
# Create main cylinder feature with hole through protrusion
# Position: 13mm from main block edge (9mm + 4mm hole radius)
cylinder_hole_radius = 4.0 # 8mm diameter hole / 2
cylinder_outer_radius = 6.0 # 12mm diameter outer cylinder / 2
cylinder_height = protrusion_height # 7mm tall (same as protrusion)
# Position cylinder 13mm from main block edge (at x=0)
# Cylinder center at x=-13mm
cylinder_x_position = -(9.0 + cylinder_hole_radius) # x=-13mm
# Create outer solid cylinder (6mm radius)
outer_cylinder = Part.makeCylinder(
cylinder_outer_radius,
cylinder_height,
Vector(cylinder_x_position, depth_center / 2, 0), # Centered on depth
Vector(0, 0, 1), # Cylinder axis along z-direction
)
# Add the outer cylinder to the protrusion
protrusion = protrusion.fuse(outer_cylinder)
# Create inner hole cutter (4mm radius)
hole_cutter = Part.makeCylinder(
cylinder_hole_radius,
cylinder_height,
Vector(cylinder_x_position, depth_center / 2, 0), # Centered on depth
Vector(0, 0, 1), # Cylinder axis along z-direction
)
# Cut hole through the protrusion and outer cylinder
protrusion = protrusion.cut(hole_cutter)
# Create tip cylinder at the very end of the protrusion
# 5mm diameter, 12mm tall (7mm above + 5mm below protrusion)
# Note: tip_outer_radius already defined above for section 2 blending
tip_inner_radius = 1.5 # 3mm diameter / 2
tip_total_height = 7 + 5 # 12mm total (7mm + 5mm extension below)
tip_hole_depth = 7 + 4 # 11mm deep hole from bottom
# Position tip cylinder 2.5mm inward from the tip (so it doesn't extend beyond protrusion)
tip_x_position = (
-protrusion_width + tip_outer_radius
) # x=-37.5 (2.5mm inward from tip)
tip_y_position = depth_center / 2 # center (matches section 2 circular contour)
tip_z_start = -5.0 # starts 5mm below protrusion (protrusion is at z=0)
# Create outer tip cylinder
tip_cylinder_outer = Part.makeCylinder(
tip_outer_radius,
tip_total_height,
Vector(tip_x_position, tip_y_position, tip_z_start),
Vector(0, 0, 1), # Vertical cylinder
)
# Create inner hole cylinder (3mm diameter, 11mm deep from bottom)
tip_cylinder_inner = Part.makeCylinder(
tip_inner_radius,
tip_hole_depth, # 11mm deep from bottom
Vector(tip_x_position, tip_y_position, tip_z_start),
Vector(0, 0, 1), # Vertical cylinder
)
# Cut hole in tip cylinder
tip_cylinder = tip_cylinder_outer.cut(tip_cylinder_inner)
# Create hole cutter for the protrusion (3mm diameter hole through protrusion)
tip_hole_cutter = Part.makeCylinder(
tip_inner_radius, # 3mm diameter hole
protrusion_height, # Cut through the full 7mm height of protrusion
Vector(tip_x_position, tip_y_position, 0), # Same x,y position, but at z=0
Vector(0, 0, 1), # Vertical cylinder
)
# Cut the hole through the protrusion BEFORE adding the tip cylinder
protrusion = protrusion.cut(tip_hole_cutter)
# Add tip cylinder to protrusion
protrusion = protrusion.fuse(tip_cylinder)
# Combine solid main box with protrusion
half_shape = main_box.fuse(protrusion)
# Create rectangular dent at the top of protrusion where it meets main box
# 2mm deep dent, 4mm wide, full depth
hole_width = 4.0 # 4mm wide extending into protrusion (along x-axis)
hole_depth = depth_center # full depth at center (14mm along y-axis)
hole_height = 2.0 # 2mm deep dent from top (along z-axis)
# Position dent at the junction between main box and protrusion
# Starting from main box edge (x=0) extending 4mm into protrusion
# From the top of protrusion down 2mm (z = 7mm down to z = 5mm)
# Centered in y-direction to align with the depth
hole_cutter = Part.makeBox(
hole_width,
hole_depth,
hole_height,
Vector(
-hole_width, 0, protrusion_height - hole_height
), # x=-4 to 0, y=0 to 14mm (centered), z=5 to 7
)
# Cut the dent from the combined shape
half_shape = half_shape.cut(hole_cutter)
# Create mirrored copy to get full 45mm width
# Mirror across the right edge (x=22.5mm plane) to create the other half
mirrored_half = half_shape.mirror(Vector(width, 0, 0), Vector(1, 0, 0))
# Combine both halves to create continuous 45mm wide solid box
final_shape = half_shape.fuse(mirrored_half)
# Refine the shape to create a single unified solid (fix broken surfaces)
try:
# For compound objects, we need to fuse all the parts first
if hasattr(final_shape, "ShapeType") and final_shape.ShapeType == "Compound":
# Extract all solids from the compound and fuse them
solids = []
for shape in final_shape.SubShapes:
if shape.ShapeType == "Solid":
solids.append(shape)
if len(solids) > 1:
# Fuse all solids into one
unified_solid = solids[0]
for solid in solids[1:]:
unified_solid = unified_solid.fuse(solid)
final_shape = unified_solid
elif len(solids) == 1:
final_shape = solids[0]
# Now try to refine the unified shape
if hasattr(final_shape, "removeSplitter"):
final_shape = final_shape.removeSplitter()
if hasattr(final_shape, "refine"):
final_shape = final_shape.refine()
print("Shape successfully unified into a single solid")
except Exception as e:
print(f"Shape refinement failed: {e}")
print("Continuing with current shape")
# Create border trace on the FULL 45mm box top surface with rounded corners
try:
print("Adding border trace 1mm from edge, 1mm deep with rounded corners...")
border_inset = 1.0 # 1mm from edge
border_depth = 1.0 # 1mm deep cut
border_width = 0.5 # 0.5mm wide line
corner_radius = 2.0 # 2mm radius for rounded corners
# Full box dimensions: 45mm x 14mm (use depth_center for proper centering) x 18.5mm
box_full_width = width * 2 # 45mm
box_depth = depth_center # 14mm (use center depth for proper centering)
# Create outer rounded rectangle (with rounded corners)
# Start at x=border_inset, y=border_inset
outer_points = []
# Create rounded rectangle using line segments and arcs
# Bottom-left corner (rounded)
x_min = border_inset + corner_radius
x_max = box_full_width - border_inset - corner_radius
y_min = border_inset + corner_radius
y_max = box_depth - border_inset - corner_radius
# Bottom line
outer_points.append(Vector(x_min, border_inset, height - border_depth))
outer_points.append(Vector(x_max, border_inset, height - border_depth))
# Bottom-right arc
for i in range(1, 10):
angle = -math.pi / 2 + (math.pi / 2) * (i / 10)
px = x_max + corner_radius * math.cos(angle)
py = y_min + corner_radius * math.sin(angle)
outer_points.append(Vector(px, py, height - border_depth))
# Right line
outer_points.append(
Vector(box_full_width - border_inset, y_max, height - border_depth)
)
# Top-right arc
for i in range(1, 10):
angle = 0 + (math.pi / 2) * (i / 10)
px = x_max + corner_radius * math.cos(angle)
py = y_max + corner_radius * math.sin(angle)
outer_points.append(Vector(px, py, height - border_depth))
# Top line
outer_points.append(
Vector(x_min, box_depth - border_inset, height - border_depth)
)
# Top-left arc
for i in range(1, 10):
angle = math.pi / 2 + (math.pi / 2) * (i / 10)
px = x_min + corner_radius * math.cos(angle)
py = y_max + corner_radius * math.sin(angle)
outer_points.append(Vector(px, py, height - border_depth))
# Left line
outer_points.append(Vector(border_inset, y_min, height - border_depth))
# Bottom-left arc
for i in range(1, 10):
angle = math.pi + (math.pi / 2) * (i / 10)
px = x_min + corner_radius * math.cos(angle)
py = y_min + corner_radius * math.sin(angle)
outer_points.append(Vector(px, py, height - border_depth))
# Close the loop
outer_points.append(outer_points[0])
# Create outer wire and extrude
outer_wire = Part.makePolygon(outer_points)
outer_face = Part.Face(outer_wire)
outer_border = outer_face.extrude(Vector(0, 0, border_depth + 0.5))
# Create inner rounded rectangle (hollow center)
inner_inset = border_inset + border_width
inner_corner_radius = max(
0.5, corner_radius - border_width
) # Smaller radius for inner
inner_points = []
x_min_inner = inner_inset + inner_corner_radius
x_max_inner = box_full_width - inner_inset - inner_corner_radius
y_min_inner = inner_inset + inner_corner_radius
y_max_inner = box_depth - inner_inset - inner_corner_radius
# Bottom line
inner_points.append(
Vector(x_min_inner, inner_inset, height - border_depth - 0.1)
)
inner_points.append(
Vector(x_max_inner, inner_inset, height - border_depth - 0.1)
)
# Bottom-right arc
for i in range(1, 10):
angle = -math.pi / 2 + (math.pi / 2) * (i / 10)
px = x_max_inner + inner_corner_radius * math.cos(angle)
py = y_min_inner + inner_corner_radius * math.sin(angle)
inner_points.append(Vector(px, py, height - border_depth - 0.1))
# Right line
inner_points.append(
Vector(
box_full_width - inner_inset, y_max_inner, height - border_depth - 0.1
)
)
# Top-right arc
for i in range(1, 10):
angle = 0 + (math.pi / 2) * (i / 10)
px = x_max_inner + inner_corner_radius * math.cos(angle)
py = y_max_inner + inner_corner_radius * math.sin(angle)
inner_points.append(Vector(px, py, height - border_depth - 0.1))
# Top line
inner_points.append(
Vector(x_min_inner, box_depth - inner_inset, height - border_depth - 0.1)
)
# Top-left arc
for i in range(1, 10):
angle = math.pi / 2 + (math.pi / 2) * (i / 10)
px = x_min_inner + inner_corner_radius * math.cos(angle)
py = y_max_inner + inner_corner_radius * math.sin(angle)
inner_points.append(Vector(px, py, height - border_depth - 0.1))
# Left line
inner_points.append(
Vector(inner_inset, y_min_inner, height - border_depth - 0.1)
)
# Bottom-left arc
for i in range(1, 10):
angle = math.pi + (math.pi / 2) * (i / 10)
px = x_min_inner + inner_corner_radius * math.cos(angle)
py = y_min_inner + inner_corner_radius * math.sin(angle)
inner_points.append(Vector(px, py, height - border_depth - 0.1))
# Close the loop
inner_points.append(inner_points[0])
# Create inner wire and extrude
inner_wire = Part.makePolygon(inner_points)
inner_face = Part.Face(inner_wire)
inner_border = inner_face.extrude(Vector(0, 0, border_depth + 0.7))
# Create border frame by subtracting inner from outer
border_frame = outer_border.cut(inner_border)
# Cut the border into the main shape
original_volume = final_shape.Volume if hasattr(final_shape, "Volume") else 0
final_shape = final_shape.cut(border_frame)
new_volume = final_shape.Volume if hasattr(final_shape, "Volume") else 0
volume_diff = original_volume - new_volume
if volume_diff > 0:
print(
f"✅ Border trace successfully cut: {border_depth}mm deep, {border_width}mm wide, {border_inset}mm from edge"
)
print(f" Rounded corners with {corner_radius}mm radius")
print(f" Volume removed: {volume_diff:.2f} mm³")
else:
print("⚠️ WARNING: No volume removed by border - may not be intersecting")
except Exception as e:
print(f"Border trace cutting failed: {e}")
print("Continuing without border trace")
# Create text "Sara" to imprint on the main box
try:
import os
import Draft
# Text specifications
text_string = "Sara"
text_size = 8.0 # 8mm text height
text_depth = (
1.0 # 1.5mm deep cut (1mm into surface + 0.5mm above for clean cut)
)
# Create 3D text shape using the newer make_text function
# Try to find the Iosevka SS06 font file path on macOS
font_paths = [
# Your specific Heavy Extended font file
"/Users/"
+ os.environ.get("USER", "user")
+ "/Downloads/Iosevka-SS06-Heavy-Extended-148.ttf",
# Look for other DfontSplitter extracted Heavy Extended files
"/Users/"
+ os.environ.get("USER", "user")
+ "/Downloads/IosevkaSS06-HeavyExtended.ttf",
"/Users/"
+ os.environ.get("USER", "user")
+ "/Downloads/Iosevka-SS06-Heavy-Extended.ttf",
"/Users/"
+ os.environ.get("USER", "user")
+ "/Desktop/IosevkaSS06-HeavyExtended.ttf",
"/Users/"
+ os.environ.get("USER", "user")
+ "/Desktop/Iosevka-SS06-Heavy-Extended.ttf",
"/Users/"
+ os.environ.get("USER", "user")
+ "/Library/Fonts/IosevkaSS06-HeavyExtended.ttf",
"/Users/"
+ os.environ.get("USER", "user")
+ "/Library/Fonts/Iosevka-SS06-Heavy-Extended.ttf",
"/Users/"
+ os.environ.get("USER", "user")
+ "/Library/Fonts/Iosevka SS06 Heavy Extended.ttf",
# Then try the .ttc collection (will use default style - probably Regular)
"/Users/"
+ os.environ.get("USER", "user")
+ "/Library/Fonts IosevkaSS06.ttc",
"/Users/"
+ os.environ.get("USER", "user")
+ "/Library/Fonts/IosevkaSS06.ttc",
# System font fallbacks that are bold
"/System/Library/Fonts/Arial Black.ttf",
"/System/Library/Fonts/Helvetica.ttc",
"/System/Library/Fonts/Arial Bold.ttf",
]
# Find first available font
font_file = "/System/Library/Fonts/Helvetica.ttc" # Default fallback
for path in font_paths:
if os.path.exists(path):
font_file = path
break
# Determine if we found a bold/heavy variant
font_name = os.path.basename(font_file)
is_heavy = "Heavy" in font_name or "Black" in font_name or "Bold" in font_name
print(f"Using font file: {font_file}")
print(f"Font appears to be heavy/bold: {is_heavy}")
# Use the correct FreeCAD API for ShapeString
text_obj = Draft.make_shapestring(
String=text_string,
FontFile=font_file,
Size=text_size,
Tracking=0.0,
)
# Position the text (will be adjusted later during cutting)
text_obj.Placement.Base = Vector(0, 0, 0)
doc.recompute() # Generate the shape
# Check if we got a valid shape
if hasattr(text_obj, "Shape") and text_obj.Shape and text_obj.Shape.Wires:
# Create faces from text wires, preserving holes in letters like "a", "e", "o", "p"
print(f"Found {len(text_obj.Shape.Wires)} wires in text shape")
# Group wires by letter - outer boundaries and inner holes
text_faces = []
# For complex text with holes, we need to create faces properly
# First try to get faces directly if they exist (ShapeString should provide proper faces)
if hasattr(text_obj.Shape, "Faces") and text_obj.Shape.Faces:
print("Using existing faces from text shape (preserves holes)")
text_faces = text_obj.Shape.Faces
print(f"Found {len(text_faces)} faces with holes preserved")
else:
# Fallback: Create faces from wires, but this may not preserve holes correctly
print(
"Creating faces from wires (may lose holes in letters like 'a', 'e', 'o', 'p')..."
)
closed_wires = [w for w in text_obj.Shape.Wires if w.isClosed()]
print(f"Found {len(closed_wires)} closed wires")
# Create faces from wires (this approach may fill holes)
for i, wire in enumerate(closed_wires):
try:
face = Part.Face(wire)
text_faces.append(face)
print(f"Created face {i + 1} with area {abs(face.Area):.2f}")
except Exception as e:
print(f"Failed to create face from wire {i + 1}: {e}")
if len(text_faces) > 1:
print("⚠️ Multiple faces detected - holes in letters may be filled")
print(
" ShapeString should provide proper faces with holes preserved"
)
if text_faces:
# Combine all letter faces
text_compound = Part.makeCompound(text_faces)
# Create 3D text cutter that extends ABOVE the top surface to cut DOWN
text_3d = text_compound.extrude(Vector(0, 0, text_depth))
print(f"Text 3D bounding box before positioning: {text_3d.BoundBox}")
# Position text at the center of the FULL 45mm box
text_center_x = width # Dead center of the 45mm wide box
text_center_y = depth_center / 2 # Dead center of the box depth
# Position the cutter to START BELOW the surface and extend ABOVE it
# This ensures the cutter intersects with the top surface for cutting
cut_depth = 1.0 # Actual depth to cut into surface (1mm)
text_z_start = height - cut_depth # Start 1mm below surface (z=17.5)
text_z_end = text_z_start + text_depth # End at z=19.0
print(f"Main box height: {height}mm (top surface at z={height})")
print(
f"Text cutter: z={text_z_start} to z={text_z_end} (should overlap with box)"
)
text_3d.translate(
Vector(
text_center_x - text_3d.BoundBox.XLength / 2,
text_center_y - text_3d.BoundBox.YLength / 2,
text_z_start, # Start 2mm below surface and extend upward
),
)
print(f"Text 3D bounding box after positioning: {text_3d.BoundBox}")
print(
f"Text positioned at: x={text_center_x - text_3d.BoundBox.XLength / 2:.1f}, y={text_center_y - text_3d.BoundBox.YLength / 2:.1f}, z={text_z_start}",
)
# Cut text into the main box (imprint from the top)
print("Attempting to cut text into main box...")
original_volume = (
final_shape.Volume if hasattr(final_shape, "Volume") else 0
)
final_shape = final_shape.cut(text_3d)
new_volume = final_shape.Volume if hasattr(final_shape, "Volume") else 0
volume_diff = original_volume - new_volume
print(f"Volume removed by text cutting: {volume_diff:.2f} mm³")
if volume_diff > 0:
print(
f"✅ Text 'Sara' successfully cut into surface: {cut_depth}mm deep using {font_name}"
)
print(f"Bold/Heavy font detected: {is_heavy}")
else:
print(
"⚠️ WARNING: No volume was removed - text may not be intersecting with the box!"
)
print(
" Check positioning - text cutter should overlap with main box"
)
if not is_heavy:
print("⚠️ WARNING: Using regular weight font - may appear thin!")
print(
" For Heavy Extended, try: FontBook > Iosevka SS06 > Export Heavy Extended style"
)
# Clean up the temporary text object
doc.removeObject(text_obj.Name)
# Unify the shape again after text cutting to ensure single solid
print("Unifying shape after text cutting...")
try:
if hasattr(final_shape, "removeSplitter"):
final_shape = final_shape.removeSplitter()
if hasattr(final_shape, "refine"):
final_shape = final_shape.refine()
print("Shape successfully unified after text cutting")
except Exception as e:
print(f"Shape unification after text cutting failed: {e}")
print("Continuing with current shape")
except Exception as e:
print(f"Text imprinting failed: {e}")
print("\n💡 To get Heavy Extended style from your .ttc file:")
print(" 1. Install 'DfontSplitter' from Mac App Store (free)")
print(" 2. Drag IosevkaSS06.ttc into DfontSplitter")
print(" 3. Find IosevkaSS06-HeavyExtended.ttf in output")
print(" 4. Update font_paths in script with that .ttf path")
print("\n📝 Font Book can't export from .ttc - need DfontSplitter or FontForge")
print("For now, continuing without text - you can add manually in FreeCAD GUI")
# Create the box object in FreeCAD
box_obj = doc.addObject("Part::Feature", "SolidBoxWithProtrusions")
box_obj.Shape = final_shape
# Set the color to blue
if hasattr(box_obj, "ViewObject"):
box_obj.ViewObject.ShapeColor = (0.0, 0.4, 1.0) # Blue color (RGB)
box_obj.ViewObject.Transparency = 0
# Recompute the document
doc.recompute()
print(
"Unified solid box with stepped protrusions, tip cylinders, junction dents, and text imprint created successfully!",
)
print(
f"Total dimensions: {width * 2}mm x {depth_edge}-{depth_center}mm (elliptical) x {height}mm"
)
print(
f"Main box depth: {depth_edge}mm at edges → {depth_center}mm at center (elliptical profile)"
)
print(
f"Total width with protrusions: {protrusion_width + width * 2 + protrusion_width}mm"
)
print(f"Each protrusion: {protrusion_width}mm wide x {protrusion_height}mm tall")
print(
f" - First 28mm: {section1_depth_start}mm → {section1_depth_end}mm deep (curved transition)"
)
print(
f" - Last 12mm: {section2_depth_start}mm → {tip_outer_diameter}mm, wraps around {tip_outer_diameter}mm circle (contours tip cylinder)"
)
print("Protrusion structure: 8mm solid + 12mm hole + 20mm solid")
print(
f"Main cylinder hole: {cylinder_hole_radius * 2}mm diameter through protrusion"
)
print(
f"Tip cylinder: {tip_outer_radius * 2}mm outer diameter, {tip_inner_radius * 2}mm inner diameter"
)
print(
f" - Height: {tip_total_height}mm ({protrusion_height}mm + {tip_total_height - protrusion_height}mm below)"
)
print(f" - Hole depth: {tip_hole_depth}mm from bottom")
print(
f"Junction dent: {hole_width}mm wide x {hole_depth}mm deep x {hole_height}mm tall (2mm dent from top)"
)
print(
"Text imprint: 'Sara' in Iosevka SS06 Heavy Extended font, 8mm height, 0.5mm deep on main box top"
)
print("Main box: Completely solid (no hollow interior)")
print("Perfect symmetry achieved through mirroring!")
return box_obj
if __name__ == "__main__":
# Run this script in FreeCAD
if "FreeCAD" in globals() or "App" in dir():
# Create mirrored solid box with protrusions (125mm total width)
box = create_solid_box()
else:
print("This script should be run within FreeCAD!")
print("Open FreeCAD, then run: exec(open('/path/to/tent.py').read())")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment