Open adam-urbanczyk opened 5 years ago
Is this the basics of what is intended by this feature?
from math import degrees, atan2
import cadquery as cq
def _faceOnWire(self, path: cq.Wire) -> cq.Face:
"""Reposition a face from alignment to the x-axis to the provided path"""
path_length = path.Length()
bbox = self.BoundingBox()
face_bottom_center = cq.Vector((bbox.xmin + bbox.xmax) / 2, 0, 0)
relative_position_on_wire = face_bottom_center.x / path_length
wire_tangent = path.tangentAt(relative_position_on_wire)
wire_angle = degrees(atan2(wire_tangent.y, wire_tangent.x))
wire_position = path.positionAt(relative_position_on_wire)
return self.rotate(
face_bottom_center, face_bottom_center + cq.Vector(0, 0, 1), wire_angle
).translate(wire_position - face_bottom_center)
cq.Face.faceOnWire = _faceOnWire
def textOnWire(txt: str, fontsize: float, distance: float, path: cq.Wire) -> cq.Solid:
"""Create 3D text with a baseline following the given path"""
linear_faces = (
cq.Workplane("XY")
.text(
txt=txt,
fontsize=fontsize,
distance=distance,
halign="left",
valign="bottom",
)
.faces("<Z")
.vals()
)
faces_on_path = [f.faceOnWire(path) for f in linear_faces]
return cq.Compound.makeCompound(
[cq.Solid.extrudeLinear(f, cq.Vector(0, 0, 1)) for f in faces_on_path]
)
path = cq.Edge.makeThreePointArc(
cq.Vector(0, 0, 0), cq.Vector(50, 30, 0), cq.Vector(100, 0, 0)
)
text_on_path = textOnWire(
txt="The quick brown fox jumped over the lazy dog",
fontsize=5,
distance=1,
path=path,
)
if "show_object" in locals():
show_object(text_on_path, name="text_on_path")
show_object(path, name="path")
which generates this:
If so, I don't see how this is related to #109
(1) put every letter on the path so that normal is aligned with the local normal of the face (2) project every letter separately (using said normal) (3) construct new letter faces from the projected wires (4) thicken
Thank you very much @gumyr for the example code on this one, it only took me an hour to get a result I wanted.
Not sure if you are looking for feedback/issue report, but when I change the font type to something else like Freestyle Script (https://font.download/font/freestyle-script), the solid out of which I am cutting turns into an odd shell shape of it's former self. I don't have this with the initial default font:
While it's rendering, I notice this oddness with the inner circles for the A and R, they don't have the green color, perhaps that is relevant?
Let me know if you'd like a isolated piece of code to reproduce?
Problems like this with text are very common as many fonts are not well suited for CAD operations due to crossing or disconnected lines. One could try to repair the text/font with an SVG tool like Inkscape and import that (not sure if CQ supports SVG import but build123d does).
@CasperH2O could you try this suggestion https://github.com/CadQuery/cadquery/issues/1244#issuecomment-1401193688 or fixing the fonts in FontForge (if FontForge finds issues)?
If we have a second confirmation that the suggestion from #1244 sovles the issue, then we'll merge it.
Thank you for the suggestions and explanation, that makes a lot of sense. I tried both the code adjustment (and so did ChatGPT) and the FontForge route with no success. I'm afraid this is beyond my current skill level, which is OK, the default font is fine for me for now.
Spoke too soon, got it working. Had to rework the code a bit though, this is with the initial font, not the rebuilt one with Fontforge:
def _faceOnWire(self, path: cq.Wire) -> cq.Face:
"""Reposition a face from alignment to the x-axis to the provided path"""
path_length = path.Length()
bbox = self.BoundingBox()
face_bottom_center = cq.Vector((bbox.xmin + bbox.xmax) / 2, 0, 0)
relative_position_on_wire = face_bottom_center.x / path_length
wire_tangent = path.tangentAt(relative_position_on_wire)
wire_angle = math.degrees(math.atan2(wire_tangent.y, wire_tangent.x))
wire_position = path.positionAt(relative_position_on_wire)
return self.rotate(
face_bottom_center, face_bottom_center + cq.Vector(0, 0, 1), wire_angle
).translate(wire_position - face_bottom_center)
# Attach the method to cq.Face
cq.Face.faceOnWire = _faceOnWire
def textOnWire(txt: str, fontsize: float, path: cq.Wire, extrude_depth: float) -> cq.Solid:
"""Create 3D text with a baseline following the given path"""
# Create the text as faces
text_wp = cq.Workplane("XY").text(
txt=txt,
fontsize=fontsize,
distance=0, # Create text as faces (2D)
halign="center",
valign="center",
# You can specify font parameters here if needed
font='Freescript',
fontPath="FREESCPT.TTF",
)
linear_faces = text_wp.faces().vals()
# Fuse the faces together and clean the result
text_flat = linear_faces[0]
if len(linear_faces) > 1:
for face in linear_faces[1:]:
text_flat = text_flat.fuse(face)
text_flat = text_flat.clean()
else:
text_flat = text_flat.clean()
# After fusing, text_flat is a Compound. Extract the faces
fused_faces = text_flat.Faces()
# Reposition each face along the path
faces_on_path = [face.faceOnWire(path) for face in fused_faces]
# Extrude each face by the specified depth using extrudeLinear
extruded_solids = [
cq.Solid.extrudeLinear(face, cq.Vector(0, 0, extrude_depth)) for face in faces_on_path
]
# Combine all extruded solids into one compound
return cq.Compound.makeCompound(extruded_solids)
Support text on a 2D curve
Continuation of #48