Open adam-urbanczyk opened 5 years ago
I'm working on wrapping text and have had some success: Currently I've mapped to a cylinder but I believe mapping to an arbitrary surface is just an extension of the same algorithm. The hard part is handling letter interiors as the direction of the wire is critical while simple subtraction of the interior solid has problems. More work to do...
What is exactly "the algorithm" you are referring to? I don't understand the remark about handling letter interiors. What I had in mind here was to extend #108 with using projection (see #562) and offset #642 #643
Sorry, I should have included a more complete explanation.
My goal was to create text wrapped around a cylinder as shown above so I came up with the following algorithm:
As the input is Wires it can be generated to map any wires to a cylinder like this:
The complexity with text is that the insides of the letters need to be removed which should be done during Face creation. For this to work the Wires need to be in the correct direction - something I'm still working on. For the "Beingφθ⌀" picture I just cut the inner Solids from the outer Solids which is slow and problematic.
BRepProj_Projection looks good but from what I can see it only projects to a cylinder or a cone. The above algorithm can be generalized to map in the input wires on to an arbitrary surface by sampling the appropriate location (in u,v space) and using this to build the mapped Edges. There should probably be a text on cylinder/cone method (which uses BRepProj_Projection) and a text on surface method that is more general.
With respect to 2D text, it seems as though AIS_Dimension could be used to generate the text as wires which should be faster than how I extracted wires from the text Solid.
Adding link: AIS_Dimension:drawText() ... https://dev.opencascade.org/doc/occt-7.3.0/refman/html/class_a_i_s___dimension.html#a24b48debc577aa7f1f914b021f339c81
Thanks for the clarification! I don't think you are right regarding BRepProj_Projection
- you are confusing projection type with the surface being projected onto. TBH I don't think the proposed approach is robust.
I'll build a prototype with BRepProj_Projection
and see what it can do...
Looking good: You were right, there are two projections - one with a direction and the other with a point. It will be fun to see what can be done with this.
BRepProj_Projection
is great but it doesn't solve one of the central problems with Text on a surface - so I'm looking for some advice here. The problem is that Text can have multiple outer and inner wires and I can't find a way to create a non-planar face with a hole in it. Consider a "D" - with the projected inner and outer wires non-planar faces can be created from both but the inner face needs to be subtracted from the outer face. Planar faces support the cut() method but this doesn't work with non-planar faces from what I can see. The non-optimal solution that I can think of is to create Solids first then do the cutting but this is slow and may have tolerance problems with the surfaces. Any suggestions are welcome.
@gumyr When i wrote the same feature for Onshape (pictured in the original issue here), I ran into these same issues.
It turns out that doing text on a surface really well is quite hard. You need to consider not only text alignment, but also kerning. I ultimately just made a planar surface under each individual character, and projected each character to the desired surface.
At first it seems inelegant. but
In practice most surfaces people are applying text to are quite tame, and projecting the letters individually works ok most of the time.
[EDIT] : I looked back at my code, and it appears that The algorithm i ended up with is slightly more complex than just use a flat plane. What I did was this psuedocode:
for each character
* compute position ( kerning, alignment, etc)
* create text character, and transform it to the right place
* offset the underlying surface the desired letter height. This becomes the upper text surface
* create a plane tangent to the target surface, and project the letter onto that surface
* extrude the letter from the tangent plane UP to the offset surface, and downward onto the target surface
This is an approximation, but ends up being very very close to the generalized approach you're looking for.
@gumyr also if you are interested in developing text code you're willing to contribute back to CQ, PM me and I'll be happy to share the complete source of the Onshape feature I wrote. Its a weird javascript Onshape language thing, but it might help you see exactly what i ended up doing
BRepProj_Projection
is great but it doesn't solve one of the central problems with Text on a surface - so I'm looking for some advice here. The problem is that Text can have multiple outer and inner wires and I can't find a way to create a non-planar face with a hole in it. Consider a "D" - with the projected inner and outer wires non-planar faces can be created from both but the inner face needs to be subtracted from the outer face. Planar faces support the cut() method but this doesn't work with non-planar faces from what I can see. The non-optimal solution that I can think of is to create Solids first then do the cutting but this is slow and may have tolerance problems with the surfaces. Any suggestions are welcome.
You know the topology of every letter (inner/outer wires) and have the underlying surface so it should be trivial to construct a Face using BRepBuilderAPI_MakeFace
. What is the problem with such an approach?
If you want to work on this topic, I'd really strongly suggest to start with #108 and #562 and only then handle this issue.
Thanks for the help @dcowden, turns out that I'm doing almost exactly what you describe in the Onshape feature - working character by character with kerning and generating a Compound in the end - but I haven't given up on non-planar faces yet. As you describe there is a fair bit of complexity here. And yes, I intend to do one (or more) PRs with all of this code.
@adam-urbanczyk, I have no problem generating both the outer or inner wires - here is an example of just the outer wires mapped on a sphere:
but when I try to combine the outer and inner wires with BRepBuilderAPI_MakeFace
I get OCP.Standard.Standard_NoSuchObject: NCollection_BaseList::PRemove
when using the Add() method on the inner wires.
I thought I read that holes can be created with BRepBuilderAPI_MakeFace
- I haven't given up yet... although I'd welcome an example if you know of one.
P.S. I essentially already have #562 and could do #108 too which sounds relatively easy as this would be a planar faces with simple extrusions (just translating and rotating each character if I understand the intent of the feature).
I wrote the following which generates a non-planar face with holes:
def makeNonPlanarFace(
exterior: Union[cq.Wire, list[cq.Edge]],
surfacePoints: list[cq.Vector] = None,
interiorWires: list[cq.Wire] = None,
) -> cq.Face:
"""Create a potentially non-planar face bounded by exterior (wire or edges),
optionally refined by surfacePoints with optional holes defined by interiorWires"""
# First, create the non-planar surface
surface = BRepOffsetAPI_MakeFilling(
Degree=3,
NbPtsOnCur=15,
NbIter=2,
Anisotropie=False,
Tol2d=0.00001,
Tol3d=0.0001,
TolAng=0.01,
TolCurv=0.1,
MaxDeg=8,
MaxSegments=9,
)
if isinstance(exterior, cq.Wire):
outside_edges = exterior.Edges()
else:
outside_edges = [e.Edge() for e in exterior]
for edge in outside_edges:
surface.Add(edge.wrapped, GeomAbs_C0)
if surfacePoints:
for pt in surfacePoints:
surface.Add(gp_Pnt(*pt.toTuple()))
surface.Build()
# Next, add wires that define interior holes - note these wires must be entirely interior
surface_face = cq.Face(surface.Shape())
if interiorWires:
makeface_object = BRepBuilderAPI_MakeFace(surface_face.wrapped)
for w in interiorWires:
makeface_object.Add(w.wrapped)
surface_face = cq.Face(makeface_object.Face()).fix()
return surface_face
with which I was able to generate: with:
(outer_e, inner_e) = makeTextWires("e", 10)[0]
e_face = makeNonPlanarFace(outer_e[0], interiorWires=inner_e)
e_solid = e_face.thicken(1)
Still lots to do:
BRepProj_Projection
produces wires for the front and back of the solid and seems to mix the edges up in some situationsI think the rest is just normal coding problems - I'm confident I'll be able to make this work. Thanks for the guidance.
Thanks! Note that it should not be needed to use BRepOffsetAPI_MakeFilling
because you already have the underlying geometry. Here is an example:
import OCP
w1 = cq.Face.makePlane(2,2).Wires()[0]
w2 = cq.Face.makePlane(1,1).Wires()[0]
f0 = cq.Workplane("XZ", origin=(0,3,3)).ellipse(2,1).extrude(6).faces('not %PLANE').val()
#show_object(w1)
#show_object(w2)
#show_object(f0)
d = OCP.gp.gp_Dir(0,0,1)
def project(w):
return OCP.BRepProj.BRepProj_Projection(w.wrapped, f0.wrapped, d).Current()
w1_p = project(w1)
w2_p = project(w2)
show_object(w1_p)
show_object(w2_p)
bldr = OCP.BRepBuilderAPI.BRepBuilderAPI_MakeFace(f0._geomAdaptor(), w1_p)
bldr.Add(OCP.TopoDS.TopoDS.Wire_s(w2_p.Reversed()))
show_object(bldr.Face())
Regarding your remarks:
Making progress - here is what both the conical (at the back) and cylindrical projections look like:
I need more experience with using BRepProj_Projection
to see if they'll actually do what I want - which was to wrap text around a cylinder to label some of my parts - as neither projection currently is doing a good job of that with my level of experience. As currently designed, alternative text projections (e.g. the cylindrical wrap described at the top of this thread) would be easy to create.
Click here if you'd like to see the code as is. I think there are many opportunities for improvements - each projection takes 6 to 7 seconds.
The code makes no assumptions about the shape of the letters - with the font shown above a "g" has two holes and the last "letter" in the above image is actually a diameter symbol "⌀". I wrote a createWireRelationships
method that creates a list of dict where the key is an exterior wire and the value is a list of the interior wire(s) (which could be an empty list) for each letter in the text string. Another source for optimization.
I still need to work on face normal direction - the dot on the "i" in the cylindrical projection is backwards from the rest of the "i".
My projectWireOnSolid()
method now sorts the wires into front and back lists if there is more than one projected wire - although I suspect more usage could reveal weaknesses to this sorting algorithm.
I haven't looked at removing BRepOffsetAPI_MakeFilling
yet. As currently implemented, a new makeNonPlanarFace
method (like makeNSidedSurface
but will all native cadquery typed inputs and the ability to add a hole by providing an interior wire) creates all of the non-planar faces for the letters. Before I'm done I'll add at least a single point into face creation as a circle projected onto a sphere would incorrectly result in a planar face.
Text alignment (halign/valign) isn't working yet (although the code I copied from the workplane text
method is there), a minor bit of coding still needed to implement this.
Thanks for sharing the code and progress! Note that what you are building is becoming quite distinct from the intent of this issue (i.e. extending #108 to working on 3D surfaces).
I don't understand your comment "what you are building is becoming quite distinct from the intent of this issue". How is the result of my implementation: "quite distinct" from the image you included in the description of this feature?
Okay, I don't know if you want to go in this direction but I've come to the conclusion that this feature should have nothing to do with text - it's a Face feature. With this code:
text_faces = (
cq.Workplane("XZ")
.text(
"Beingφθ⌀",
fontsize=10,
distance=1,
font="Serif",
fontPath="/usr/share/fonts/truetype/freefont",
halign="center",
)
.faces(">Y")
.vals()
)
projected_text_faces = [
f.projectToSolid(sphere_solid, cq.Vector(0, 1, 0))[FRONT] for f in text_faces
]
projected_text_solids = [f.thicken(5) for f in projected_text_faces]
square = cq.Workplane("YZ").rect(10, 10).extrude(1).faces(">X").val()
square_projected = square.projectToSolid(sphere_solid, cq.Vector(1, 0, 0))
I get:
In using the previous text projection solution I found it difficult to get the result I wanted without being able to actually see the text. This Face based solution avoids this problem by allowing the user to create faces any way they want and setup the projection while looking at the Faces. The face normal problem still needs to be fixed but that shouldn't be too difficult.
Much of the code required for the previous solution is not required here and the solution is much more general. If you're not interested in going this way I'm okay with that - I'll just release this as part of cq_warehouse.
As I already mentioned above, the idea was to generalize #108 to 3D space. So you'd start with a text, face (e.g. cylinder) and a wire/edge (e.g. helix) on that face. Projection is indeed one of the needed ingredients.
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
Thanks - this is very helpful. How do you propose the user would describe the path on a 3D surface?
Using cq.Edge
/ cq.Wire
.
We are having quite some back and forth on this. I don't know what your plans are here (i.e. are you working on a PR or just brainstorming), but if you want to contribute, I strongly suggest to start small in order to avoid difficult discussions/wasted effort afterwards. Starting with separate PRs related to #562 and #643 would be very welcome.
My plans are to do some PRs with what I've been building. I'm still testing the projection #562 and thickening #643 methods as they are complex operations and can produce unexpected results. For example, here is a Canadian flag waving in the wind: It looks good at first glance but if one looks closely at the maple leaf it differs in thickness from the white section around it. I've been trying to get more consistent projected faces by controlling the direction of wires (outer opposite to inner) to control face normals but clearly this isn't sufficient.
The code for the core functionality is here: https://github.com/gumyr/cq_warehouse/blob/dev/src/cq_warehouse/map_texture.py The code for the examples is here: https://github.com/gumyr/cq_warehouse/blob/dev/src/cq_warehouse/map_texture_examples.py
With respect to the 3D text algorithm - is there an existing method for determining the normal at a point on a Solid or Compound? This is a key requirement to extend #562 and #643 into 3D text with the algorithm described.
Support text placement on a 3D surface:
Continuation of #48