Last active
August 13, 2020 10:11
-
-
Save mangtronix/7f5b7cb4dc5f75019890 to your computer and use it in GitHub Desktop.
FreeCAD macro to wrap text on a cylinder
This file contains 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
# 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