Skip to content

Instantly share code, notes, and snippets.

@WizBangCrash
Last active October 14, 2025 22:19
Show Gist options
  • Save WizBangCrash/0fac61f7b74431c651993ddaaf1f2e82 to your computer and use it in GitHub Desktop.
Save WizBangCrash/0fac61f7b74431c651993ddaaf1f2e82 to your computer and use it in GitHub Desktop.
Multibin label creator python script for FreeCAD
import os # Provides os functions
from typing import NamedTuple
from typing import Final
import FreeCAD as App
import FreeCADGui
import Draft
import ImportGui
# Constants
MAX_ONECU_PER_ROW: Final[int] = 6 # Number of 1CU labels per row
MAX_TWOCU_PER_ROW: Final[int] = 2 # Number of 2CU labels per row
GAP_BETWEEN_LABELS: Final[float] = 2.0 # mm gap between labels
TEXT_MARGIN: Final[float] = 1.5 # mm margin between text and label edge
EMBOSSED_TEXT_HEIGHT: Final[float] = 0.8 # mm height of embossed text
# Define out Box dimensions
class BoxDimensions(NamedTuple):
"""Dimensions of a box in mm."""
length: float # X dimension
width: float # Y dimension
height: float # Z dimension
font_path: str = "/System/Library/Fonts/Supplemental/DIN Condensed Bold.ttf"
font_size: int = 10
ONE_CU: Final[BoxDimensions] = BoxDimensions(length=28 , width=12, height=0.4) # mm
TWO_CU: Final[BoxDimensions] = BoxDimensions(length=78 , width=12, height=0.4) # mm
oneCUList: list[str] = [
"USB-C Skt",
"5v DC-DC",
"5v DC-DC",
"SN74HCT125N"
]
twoCUList: list[str] = [
"Level Shifters",
"1W 1% Resistors",
"Relays",
"M4 Nuts, Bolts & Washers",
"Crimp Tool Heads",
"PG9 Cable Glands (B)",
"PG7 Cable Glands (B)",
"PG7 Cable Glands (W)",
"Joint & Terminal Crimps",
"Lightstrip Connector 3-wire",
"Lightstrip Connector 4-wire",
"6mm Male/Female Spade Crimps",
"PCB Screw Terminal Blocks",
"Nail Cutters",
"Pen Knives",
"ESP8266 Boards",
"ESP32 Boards",
"Veroboard/Breadboard",
"USB/Power/Battery Testers",
]
def position_label(label_type: BoxDimensions, position: int) -> App.Vector:
"""Calculate the position of a label based on its size and index position."""
column: int = position % (MAX_ONECU_PER_ROW if label_type == ONE_CU else MAX_TWOCU_PER_ROW)
row: int = position // (MAX_ONECU_PER_ROW if label_type == ONE_CU else MAX_TWOCU_PER_ROW)
x = (label_type.length + GAP_BETWEEN_LABELS) * column
y = (label_type.width + GAP_BETWEEN_LABELS) * row
z = 0
if label_type == TWO_CU:
# Position these labels below the X axis
y = -(y + label_type.width + GAP_BETWEEN_LABELS)
# y = y if label_type == oneCU else -y - label_type.width - GAPBETWEENLABELS # larger labels are below X axis
# print(f'Placing label at row: {row}, column: {column}, x: {x}, y: {y}, z: {z}')
return App.Vector(x, y, z)
def position_text(label_type: BoxDimensions, vector: App.Vector) -> App.Vector:
""" Calculate the position of the text based on the label vector """
x = vector.x + label_type.length / 2
y = vector.y + label_type.width / 2
z = vector.z + label_type.height
return App.Vector(x, y, z)
def create_label(label_type: BoxDimensions, text: str, position: int) -> None:
"""Create a label of given type with the given text at the given position."""
print(f'Creating label for "{text}"')
# Create a text shape so I can see if the string fits inside the label
label_text = Draft.make_shapestring(String=text, FontFile=font_path, Size=font_size, Tracking=0.0)
label_text.Label = "Text_" + text[:10]
label_text.ScaleToSize = True
label_text.Size = (label_type.width - TEXT_MARGIN * 2) * 0.75 # mm height
label_text.Justification = 'Middle-Center'
doc.recompute()
# print(f'Label text bounding box: {labelText.Shape.BoundBox}, XLength: {labelText.Shape.BoundBox.XLength}')
# TODO: Iterate attempting many different text sizes to see if I cam make the text fit.
if label_text.Shape.BoundBox.XLength > label_type.length - (TEXT_MARGIN * 2):
# Try the smaller font size to see if text does not fit on label """
label_text.Size = (label_type.width - TEXT_MARGIN * 2) * 0.50 # mm height
doc.recompute()
if label_text.Shape.BoundBox.XLength > label_type.length - (TEXT_MARGIN * 2):
# If the text won;t fit then warn user and move on to next label
print(f'Warning: Label text "{text}" is too long "{label_text.Shape.BoundBox.XLength:.1f}" to fit inside a 1CU label box of length {label_type.length}mm')
doc.removeObject(label_text.Name)
return None
label_vector = position_label(label_type, position)
text_vector = position_text(label_type, label_vector)
label_text.Placement = App.Placement(text_vector, App.Rotation(0, 0, 0, 1))
# Extrude the text to create a 3D shape
extrude = doc.addObject("Part::Extrusion", "Extrude_" + text[:10])
extrude.Base = label_text
extrude.Dir = (0, 0, EMBOSSED_TEXT_HEIGHT) # mm height
extrude.Solid = True
extrude.Reversed = False
labelMaterial.AmbientColor = (1.0, 0.0, 0.0) # red
extrude.ViewObject.ShapeAppearance = labelMaterial
# extrude.ViewObject.LineMaterial = labelMaterial
# doc.recompute()
# Create a box to represent the label size
label = doc.addObject("Part::Box", "Box_" + text[:10])
label.Length = label_type.length
label.Width = label_type.width
label.Height = label_type.height
label.Placement = App.Placement(label_vector, App.Rotation(0, 0, 0, 1))
labelMaterial.AmbientColor = (1.0, 1.0, 1.0) # white
label.ViewObject.ShapeAppearance = labelMaterial
doc.recompute()
# Make a compound of the text and label
compound = doc.addObject("Part::Compound", "Label_" + text[:10])
compound.Links = [label, extrude]
label.Visibility = True
extrude.Visibility = True
compound.Visibility = False
doc.recompute()
return None
#
#
# Create a new document to place labels inside
doc = App.newDocument('LabelPrintLayout')
labelMaterial = App.Material()
for index, labelString in enumerate(oneCUList):
create_label(ONE_CU, labelString, index)
for index, labelString in enumerate(twoCUList):
create_label(TWO_CU, labelString, index)
# Combine all the compound objects into one large compound object
links = [obj for obj in doc.Objects if obj.TypeId == "Part::Compound"]
compound_all = doc.addObject("Part::Compound", "AllLabels")
compound_all.Links = links
doc.recompute()
# Show all the labels
FreeCADGui.SendMsgToActiveView("ViewFit")
# Export the compound object as a STEP file
output_dir: str = "/Users/wizbang/Downloads/Dave"
output_path: str = os.path.join(output_dir, "Multibin_Labels.step")
App.Console.PrintMessage(f'Exporting label to {output_path}\n')
ImportGui.export([compound_all], output_path)
# # Close the document
# App.closeDocument(doc.Name)
@WizBangCrash
Copy link
Author

Python script to create a set of Multibin labels in FreeCAD to simplify how to produce multiple labels in one 3D print.
Just edit the two lists at the beginning of the script oneCUList and twoCUList to contain the text you want on each label.

You can also change the font_path variable to point to your preferred font. I use macOS so this contains the absolute path to the required font.
The script will try to fill the height of the label with the text, but if it is too long it will reduce the text size to have another go at seeing if it fits. A label will not be created for any text that cannot fit within the label.

The resultant FreeCAD objects are compounded and output as a STEP file (you will need to edit where to save the STEP).

You will end up with something along the lines of the following:
Screenshot 2025-10-14 at 23 00 55

Printing
I use a Bambu Lab P1S with a single external spool and Bambu Studio. To print, I load the STEP file, slice it and then pause the printing at layer 3. Once the printer pauses I then change the filament to a different colour before resuming. If you have an AMS then you can change filament colour at layer 3.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment