Last active
October 14, 2025 22:19
-
-
Save WizBangCrash/0fac61f7b74431c651993ddaaf1f2e82 to your computer and use it in GitHub Desktop.
Multibin label creator python script for FreeCAD
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
| 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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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
oneCUListandtwoCUListto 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:

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.