# How can I create a 3D FilledCurve object?

FilledCurve can create a 2D graphics object; for example:

a = {{-1, 0}, {0, 1}, {1, 0}}; b = {{0, -(2/3)}};
Graphics[FilledCurve[{{BezierCurve[2 a], Line[2 b]}, {BezierCurve[a], Line[b]}}]]


How can I put a Graphics3D object like this on a plane, say $z=0$, and create something like the following 3D graphic (without mapping it on a polygon as a texture)?

-
Althrough I cann't see your images, but this post may be useful to you, mathematica.stackexchange.com/questions/14954/… . – yulinlinyu May 10 '13 at 2:03
Having 3D FilledCurves would be very useful indeed... – Yves Klett May 10 '13 at 8:03

Here is some code to convert filled curves to polygons (2D or 3D).

Updated

(I had the same idea as J.M., to combine the best of both answers...)

• The code now handles filled curves containing BSplineCurve primitives as well as BezierCurve and Line.
• The curve primitives are converted to lines using J.M.'s ParametricPlot trick, ensuring good sampling.
• Disconnected polygons such as separate letters are kept as separate polygons. Polygons representing holes are merged with their parent polygon.

The conversion is done using the functions filledCurveToPolygons and filledCurveToPolygons3D. The rest of the code is helper functions.

The basic process is to convert the FilledCurve to a nested list of line and curve primitives, convert the curves to lines, and then convert the lines to polygons. The devil is in the detail of course, in particular handling the coordinate lists to make sure each segment starts and finishes at the same point - this is crucial to get the holes to work properly.

Examples

a = {{-1, 0}, {0, 1}, {1, 0}}; b = {{0, -(2/3)}};
fc = FilledCurve[{{BezierCurve[2 a], Line[2 b]}, {BezierCurve[a], Line[b]}}];
Graphics3D[filledCurveToPolygons3D[fc]]


fc = ImportString[ExportString[
Style["ABC", FontFamily -> "Times"], "PDF"]][[1, 1, 2, 1, 1]];
Graphics3D[filledCurveToPolygons3D[fc]]


Note that polygons have edges joining the holes with the outsides - these seem to be hidden in Graphics3D but are visible in the 2D version:

Graphics[{EdgeForm[Red], Yellow, filledCurveToPolygons[fc]}]


To show outlines in 2D it is better to display the polygons without edges, and add the outlines separately using filledCurveToLines:

Graphics[{Yellow, filledCurveToPolygons[fc], Red, filledCurveToLines[fc]}]


Here's the code:

toSegments[fc : FilledCurve[_List, _List]] :=
First@GeometricFunctionsDecodeFilledCurve[fc]
toSegments[FilledCurve[data : {_List ..}]] := data
toSegments[FilledCurve[data : _List]] := {data}
toSegments[FilledCurve[data_]] := {{data}}

processSegment[seg_List] := Module[{s, pts, st, fi},
s = seg; pts = s[[All, 1]];
If[Length[pts] > 1, s[[2 ;;, 1]] = Join[pts[[;; -2, {-1}]], pts[[2 ;;]], 2]];
st = pts[[1, 1]]; fi = pts[[-1, -1]];
If[st != fi, AppendTo[s, Line[{fi, st}]]];
s]

segmentsToLines[segs_] := segs /. {
BezierCurve[data_, opts___] :> First@Cases[
ParametricPlot[BezierFunction[data, opts][t], {t, 0, 1}], _Line, -1],
BSplineCurve[data_, opts___] :> First@Cases[
ParametricPlot[BSplineFunction[data, opts][t], {t, 0, 1}], _Line, -1]}

coordList[seg_] := Module[{temp},
temp = seg /. Line -> Sequence;
temp[[2 ;;]] = temp[[2 ;;, 2 ;;]];
Join @@ temp]

processHoles[polys_] := With[{ipq = GraphicsMeshInPolygonQ},
polys //. {a___, p : Polygon[x_], q : Polygon[y_], b___} /;
ipq[p, y[[2]]] || ipq[q, x[[2]]] :> {a, Polygon[Join[x, y, {First@x}]], b}]

filledCurveToLines[fc_FilledCurve] :=
segmentsToLines[processSegment /@ toSegments[fc]]

filledCurveToPolygons[fc_FilledCurve] :=
processHoles[Polygon /@ coordList /@ filledCurveToLines[fc]]

filledCurveToPolygons3D[fc_FilledCurve] :=
filledCurveToPolygons[fc] /.
Polygon[data_] :> Polygon[ArrayPad[data, {{0, 0}, {0, 1}}]]

-
I keep forgetting GeometricFunctionsDecodeFilledCurve[]... rather useful. – J. M. May 10 '13 at 11:06
Thanks, that's wonderful! But there seems to be a problem on my computer when I try the example of "ABC". The hole in the letter "A" doesn't show up. And it seems to because the processHoles function can only handel the case that polygon x is larger than polygon y, but on my computer y is larger than x somehow. I'm using V9 on mac. – xslittlegrass May 10 '13 at 15:45
@xslittlegrass, try changing the RHS of processHoles to this: polys//.{a___,p:Polygon[x_],q:Polygon[y_],b___}/;GraphicsMeshInPolygonQ[p,y[[2]]]‌​||GraphicsMeshInPolygonQ[q, x[[2]]]:>{a,Polygon[Join[x,y,{First@x}]],b} – Simon Woods May 10 '13 at 15:55
Thanks. That works! – xslittlegrass May 10 '13 at 20:55
@xslittlegrass, thanks for letting me know (and for the accept). I've updated the code in the answer. – Simon Woods May 11 '13 at 12:25

Cheating a bit:

a = {{-1, 0}, {0, 1}, {1, 0}}; b = {{0, -2/3}};

big = First @ Cases[ParametricPlot[BezierFunction[2 a][t], {t, 0, 1}], Line[l_] :> l, ∞];
small = First @ Cases[ParametricPlot[BezierFunction[a][t], {t, 0, 1}], Line[l_] :> l, ∞];

Graphics3D[{Directive[Black, EdgeForm[]], Polygon[Map[Append[#, 0] &,
(2 b) ~Join~ big ~Join~ (2 b) ~Join~ b ~Join~ Reverse[small] ~Join~ b]]},
Lighting -> "Neutral"]


I decided to extend Simon's fine answer to be able to handle both Béziers and B-splines, as well as enabling adaptive sampling. Here is the result:

toSegments[fc_FilledCurve] :=
First @ If[Length[fc] == 1, Identity, GeometricFunctionsDecodeFilledCurve][fc]

sampleSegment[prims_List, opts___] :=
If[First[#] != Last[#], Append[#, First[#]], #] &[
Apply[Join, prims /. {(b : (BSplineCurve | BezierCurve))[data_, rest___] :>
Block[{bf, h, t},
h = Switch[b, BSplineCurve, BSplineFunction, BezierCurve, BezierFunction];
bf = Apply[h, {data, rest} /.
(SplineDegree -> 3) :> (SplineDegree -> Automatic)];
First @ Cases[ParametricPlot[bf[t], {t, 0, 1}, opts], Line[l_] :> l, ∞]],
Line -> Sequence}]]

filledCurvetoPolygon[fc_FilledCurve] := With[{s = sampleSegment /@ toSegments[fc]},
Polygon[ArrayPad[Join @@ (Append[#, s[[1, -1]]] & /@ s), {{0, 0}, {0, 1}}]]]


Examples:

Graphics3D[filledCurvetoPolygon[First @ Cases[
ImportString[ExportString["AB", "PDF"]], _FilledCurve, ∞]]]


pts = {{0., -0.5}, {0.5, -0.5}, {0.5, 0.5}, {0., 0.5}, {-0.5, 0.5},
{-0.5, -0.5}, {0., -0.5}};
w = {1, .5, .5, 1, .5, .5, 1};
k = {0, 0, 0, 1/4, 1/2, 1/2, 3/4, 1, 1, 1};

FilledCurve[{{BSplineCurve[2 pts, SplineDegree -> 2, SplineKnots -> k, SplineWeights -> w]},
{BSplineCurve[pts, SplineDegree -> 2, SplineKnots -> k, SplineWeights -> w]}}]
// filledCurvetoPolygon // Graphics3D


-
Thanks for the answer, that's very helpful. But if we have multiple holes, do you have any ideas to automatic find the points to break the loop, instead of manually break them? For example ImportString[ExportString[AB, "PDF"], "PDF"][[1, 1, 2, 1, 1]] gives a FilledCurve object with multiple holes. Thanks. – xslittlegrass May 10 '13 at 6:09
That'd be tougher to do. Choosing where to break seems nontrivial. – J. M. May 10 '13 at 6:11
Looks like we both had the same idea :-) – Simon Woods May 10 '13 at 13:55
@Simon, thanks to your refresher on that decoding function. :) – J. M. May 10 '13 at 13:58
@J.M. you probably noticed that the BezierCurves returned by the decoding function seem to be missing a point (which is why the SplineDegree is too high). I think FilledCurve assumes that each line or curve starts from the end of the previous one, so to get "stand alone" curves you need to prepend each one's coordinate list with the last coordinate of the preceding curve. – Simon Woods May 10 '13 at 14:29

Maybe the right way is to use RegionFunction for this, then it's very straight forward if we have a test function that checks whether a point lies inside given filled curve or not. I use rasterization to perform such test, but i'm sure it can be done better.

a = {{-1, 0}, {0, 1}, {1, 0}}; b = {{0, -2/3}};
g = Graphics[
FilledCurve[{{BezierCurve[2 a], Line[2 b]}, {BezierCurve[a],
Line[b]}}], PlotRange -> {{-2, 2}, {-2, 2}}];
i = Rasterize[g, ImageSize -> {256, 256}];

Plot3D[0, {x, -2, 2}, {y, -2, 2},
RegionFunction -> (PixelValue[i, {64 (#1 + 2), 64 (#2 + 2)}] == {0,
0, 0} &)]

-
I upvoted for RegionFunction, but then I found your code not performing correctly in my MMA 9.0 (multiple Throw::nocatch errors). – Silvia May 10 '13 at 9:49
@Silvia I don't have any errors. – swish May 10 '13 at 13:04
Maybe you two are using different versions, @Silvia. – J. M. May 10 '13 at 13:08
this is my approach, since it works for a lot of different kinds of stuff you could make. some functions to keep in mind for this sort of thing: Rescale, Position`. – amr May 10 '13 at 16:30