CadQuery / cadquery

A python parametric CAD scripting framework based on OCCT
https://cadquery.readthedocs.io
Other
3.24k stars 294 forks source link

Text placement on a 2D curve #108

Open adam-urbanczyk opened 5 years ago

adam-urbanczyk commented 5 years ago

Support text on a 2D curve

Continuation of #48

gumyr commented 2 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: image

If so, I don't see how this is related to #109

adam-urbanczyk commented 2 years ago

109 is to me generalization of putting text on a 2d curve (2d curve --> 3d curve on a surface). It also includes the projection part, which indeed not related to this issue. The proposed algo would be:

(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

CasperH2O commented 1 month ago

Thank you very much @gumyr for the example code on this one, it only took me an hour to get a result I wanted.

image image

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:

image

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?

image

Let me know if you'd like a isolated piece of code to reproduce?

gumyr commented 1 month ago

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).

adam-urbanczyk commented 1 month ago

@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.

CasperH2O commented 1 month ago

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.

CasperH2O commented 1 month ago

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:

image

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)