Skip to content

Instantly share code, notes, and snippets.

@mangtronix
Last active August 13, 2020 10:11
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mangtronix/7f5b7cb4dc5f75019890 to your computer and use it in GitHub Desktop.
Save mangtronix/7f5b7cb4dc5f75019890 to your computer and use it in GitHub Desktop.
FreeCAD macro to wrap text on a cylinder
# Macro to put text on a cylinder. Select a cylinder and one or more
# ShapeStrings to place on the cylinder. You can edit the vertical
# and angular offset of the text in the TextOnCylinder properties.
#
# Michael Ang <http://github.com/mangtronix>
# http://michaelang.com
#
# TODO:
# - Split up the string and actually wrap it around the cylinder
# - Make into a proper module. Currently if you save and then reopen
# the TextOnCylinder objects are no longer editable.
# - More placement options
# - Work on any surface
import FreeCAD
import Draft, Units
from FreeCAD import Base
from Units import Quantity
v = FreeCAD.Vector
_debug = False
# General idea
# - find text width
# - extrude text
# - center (optional)
# - place extruded text
# - rotate so text face normal is pointing out
# - place at radius
# - rotate around circle (optional)
# - translate to cylinder surface
class TextOnCylinder:
def __init__(self, obj,
Thickness = Quantity("1 mm"),
BaseLineOffset = Quantity("2 mm"),
AngleOffset = Quantity("0 deg"),
):
if _debug:
FreeCAD.Console.PrintMessage("__init__\n")
obj.addProperty("App::PropertyDistance","Thickness","Text","Distance into/out of surface").Thickness = Thickness
obj.addProperty("App::PropertyDistance","BaseLineOffset","Text","Baseline offset from bottom of cylinder").BaseLineOffset = BaseLineOffset
obj.addProperty("App::PropertyAngle","AngleOffset","Text","Rotation around cylinder").AngleOffset = AngleOffset
obj.addProperty("App::PropertyLink","Cylinder","Links","Link to Cylinder")
obj.addProperty("App::PropertyLink","ShapeString","Links","Link to ShapeString")
# $$$ rotation degrees, vertical offset
obj.Proxy = self
def execute(self, feature):
if _debug:
FreeCAD.Console.PrintMessage("execute\n")
if feature.ShapeString:
# We reset the placement of the text before extruding
shape_copy = feature.ShapeString.Shape.copy()
shape_copy.Placement = Base.Placement()
extruded = shape_copy.extrude(v(0,0,feature.Thickness*2))
feature.Shape = extruded
else:
return # No text linked
# Figure out placement
if feature.Cylinder:
# Start by placing text as if on a default upright cylinder
# Rotate around z-axis
text_placement = Base.Placement(v(0,0,0), Base.Rotation(v(0,0,1), feature.AngleOffset))
# Place text upright and at front outside of cylinder
# Rotate from default text rotation (x-y plane) to cylinder default upright
# then rotate by the cylinder rotation
text_placement = text_placement.multiply(Base.Placement(v(0,-feature.Cylinder.Radius,0), Base.Rotation(v(0,1,0), v(0,0,1))))
# Center text
center_offset = v(-extruded.BoundBox.XLength/2,0,-extruded.BoundBox.ZLength/2)
text_placement = text_placement.multiply(Base.Placement(center_offset, Base.Rotation()))
# Apply baseline offset
text_placement = text_placement.multiply(Base.Placement(v(0,feature.BaseLineOffset,0), Base.Rotation()))
# Now multiply by the cylinder's actual placement, to get into the cylinder's
# coordinate space
feature.Placement = feature.Cylinder.Placement.multiply(text_placement)
def onChanged(self, feature, property):
if _debug:
FreeCAD.Console.PrintMessage("onChanged: %s\n" % property)
pass
def TextOnCylinderCmd():
max_label_length = 30
selection = FreeCADGui.Selection.getSelection()
strings = findShapeStrings(selection)
cylinders = findByDerivedTypeId(selection, 'Part::Cylinder')
if cylinders and strings:
cylinder = cylinders[0]
for string in strings:
label = "TextOnCyl_%s" % string.Label
label = label[:max_label_length]
tocf = FreeCAD.ActiveDocument.addObject("Part::FeaturePython",label)
TextOnCylinder(tocf)
tocf.ShapeString = string
tocf.Cylinder = cylinder
tocf.ViewObject.Proxy=0
FreeCAD.ActiveDocument.recompute()
else:
if cylinders or strings:
# Might be trying to select a valid combo
FreeCAD.Console.PrintWarning("Please select a cylinder and at least one ShapeString\n")
else:
FreeCAD.Console.PrintWarning("Running demo since no cylinder or shape strings selected\n")
demo()
# Utility
def findShapeStrings(selection):
shape_strings = []
for thing in selection:
if 'String' in thing.PropertiesList: # $$$ more reliable way to detect?
shape_strings.append(thing)
return shape_strings
def findByDerivedTypeId(selection, typeId):
things = []
for thing in selection:
if thing.isDerivedFrom(typeId):
things.append(thing)
return things
# Demo
def demo():
import FreeCADGui
# Make a new document if none active
if not FreeCAD.ActiveDocument:
doc = FreeCAD.newDocument("Text on cylinder")
# Full path to a font
font = u'/Users/mangtronix/Dropbox/Work/Changemakrs-mang/changemakrs-ios-module-insanity/Changemakrs/Changemakrs/Assets/Argumentum-Medium.otf'
# Create Cylinder
cf = FreeCAD.ActiveDocument.addObject("Part::Cylinder","Cylinder")
cf.Radius = Quantity("5 mm")
cf.Height = Quantity("30 mm")
cf.Placement = Base.Placement(v(0,20,10), Base.Rotation(v(0,1,0),15))
# Create ShapeString feature
text = u"Hi"
ssf = Draft.makeShapeString(String=text,FontFile=font,Size=1.0,Tracking=0)
ssf.Label = text
# The ShapeString.Placement doesn't affect the placement on the cylinder
# Just rotating to see it easier in the UI.
upright_rotation = Base.Rotation(v(1,0,0), 90)
ssf.Placement.Base = v(0,2,0)
ssf.Placement.Rotation = upright_rotation
# Put it on the cylinder
tocf=FreeCAD.ActiveDocument.addObject("Part::FeaturePython","HiOnCylinder")
TextOnCylinder(tocf)
tocf.ShapeString = ssf
tocf.Cylinder = cf
tocf.ViewObject.Proxy=0
# More text
text = u"23"
ssf2 = Draft.makeShapeString(String=text,FontFile=font,Size=1.0,Tracking=0)
# Store text height so we can put the new text above the existing text
text_height = ssf2.Shape.BoundBox.YLength # Approximately
# Make sure the text height is a Quantity with correct Unit
text_height = Quantity(text_height)
if text_height.Unit != Units.Length:
text_height.Unit = Units.Length
ssf2.Label = text
ssf2.Placement.Base = v(10,2,0)
ssf2.Placement.Rotation = upright_rotation
tocf2 = FreeCAD.ActiveDocument.addObject("Part::FeaturePython","NumberOnCylinder")
TextOnCylinder(tocf2)
tocf2.ShapeString = ssf2
tocf2.Cylinder = cf
tocf2.ViewObject.Proxy=0
# Offset the text above the previous text
if _debug:
FreeCAD.Console.PrintMessage("text height %s" % text_height)
tocf2.BaseLineOffset += text_height * 1.2 # 20% line spacing
# Text 90 degrees around
text = u"90"
ssf3 = Draft.makeShapeString(String=text,FontFile=font,Size=1.0,Tracking=0)
ssf3.Label = text
ssf3.Placement.Base = v(20,2,0)
ssf3.Placement.Rotation = upright_rotation
tocf3 = FreeCAD.ActiveDocument.addObject("Part::FeaturePython","RotatedOnCylinder")
TextOnCylinder(tocf3)
tocf3.ShapeString = ssf3
tocf3.Cylinder = cf
tocf3.AngleOffset = Quantity('90 deg')
tocf3.ViewObject.Proxy=0
# So everything shows up
FreeCAD.ActiveDocument.recompute()
FreeCADGui.ActiveDocument.ActiveView.viewFront()
FreeCADGui.SendMsgToActiveView("ViewFit")
if __name__ == "__main__":
TextOnCylinderCmd()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment