Last active
January 13, 2026 19:56
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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