12
$\begingroup$

This may be related to How to discretize a BezierCurve?, but this question deals with BSplineCurves with specific SplineWeights, so I don't think the answers there will help here.


Background

I am using version 11.3.0.0 (on Mac OS 10.11.5).

I recently wrote an Arc function that takes the same arguments as Circle. This spits out a list of degree 2 BSplineCurves with the appropriate SplineWeights to generate pieces of a circle. I did this so that the last piece can be wrapped in an Arrow to make circular-arc arrows.

I also wanted to use JoinedCurve and FilledCurve with the arcs, which cannot be done with Circle.

The Problem

The code

segments = {
   BSplineCurve[{{1,0},{1,1},{0,1}},SplineWeights->{1,1/Sqrt[2],1}],  
   BSplineCurve[{{0,1},{-1,1},{-1,0}},SplineWeights->{1,1/Sqrt[2],1}]  
};
Graphics[{{Red,segments},JoinedCurve[segments]}]

produces the output

joined curve bug

The red curve is drawn first and seems to be correct. The black curve is drawn second and overlays the first segment properly; however, the second segment doesn't quite follow the red circle. Ideally, the black curve should completely obscure the red curve.

I was thinking that the problem might be that that JoinedCurve was trying to use a cubic spline to try to match the quadratic rational spline; however, then the first segment would not overlay so precisely (a cubic spline cannot exactly trace an arc of a circle unless it employs the proper weights).

The same problem is displayed by FilledCurve:

segments = {
   BSplineCurve[{{1/4,-3/4},{1,-3/4},{1,0}},SplineWeights->{1,1/Sqrt[2],1}],
   BSplineCurve[{{1,0},{1,3/4},{1/4,3/4}},SplineWeights->{1,1/Sqrt[2],1}],
   Line[{{1/4,3/4},{-1/4,3/4}}],
   BSplineCurve[{{-1/4,3/4},{-1,3/4},{-1,0}},SplineWeights->{1,1/Sqrt[2],1}],
   BSplineCurve[{{-1,0},{-1,-3/4},{-1/4,-3/4}},SplineWeights->{1,1/Sqrt[2],1}],
   Line[{{-1/4,-3/4},{1/4,-3/4}}]
};
Graphics[{{Red,segments},Lighter[Purple,3/4],FilledCurve[segments]}]

filled curve bug

Does anyone know what is going on here, and whether this persists in newer versions as well.


The Masked Pumpkin

This problem arose when I was creating my Gravatar for Halloween 2020:

enter image description here

Hopefully, I can remove the mask by next Halloween.

$\endgroup$

3 Answers 3

10
$\begingroup$

1. To get "the black curve (to) completely obscure the red curve"

You can replace BSplineCurves with Lines using BSplineFunction:

sw = {1, 1/Sqrt[2], 1};
segments = {BSplineCurve[{{1, 0}, {1, 1}, {0, 1}}, SplineWeights -> sw], 
  BSplineCurve[{{0, 1}, {-1, 1}, {-1, 0}}, SplineWeights -> sw]}; 

Graphics[{Red , segments, Black, Dashed, 
  JoinedCurve[segments /. 
    BSplineCurve[a__] :> Line[BSplineFunction[a] /@ Subdivide[100]]]}, 
 ImageSize -> Large]

enter image description here

Similarly, for FilledCurve:

segments = {BSplineCurve[{{1/4, -(3/4)}, {1, -(3/4)}, {1, 0}}, 
    SplineWeights -> {1, 1/Sqrt[2], 1}], 
   BSplineCurve[{{1, 0}, {1, 3/4}, {1/4, 3/4}}, 
    SplineWeights -> {1, 1/Sqrt[2], 1}], 
   Line[{{1/4, 3/4}, {-(1/4), 3/4}}], 
   BSplineCurve[{{-(1/4), 3/4}, {-1, 3/4}, {-1, 0}}, 
    SplineWeights -> {1, 1/Sqrt[2], 1}], 
   BSplineCurve[{{-1, 0}, {-1, -(3/4)}, {-(1/4), -(3/4)}}, 
    SplineWeights -> {1, 1/Sqrt[2], 1}], 
   Line[{{-(1/4), -(3/4)}, {1/4, -(3/4)}}]};

Graphics[{Red, segments, EdgeForm[{Dashed, Black}], FaceForm[Opacity[.25, Blue]], 
  FilledCurve[segments /. 
    BSplineCurve[a__] :> Line[BSplineFunction[a] /@ Subdivide[100]]]}, 
 ImageSize -> Large]

enter image description here

2. "Why is there a discrepancy?"

JoinedCurve >> Details and Options:

enter image description here

and FilledCurve >> Details and options

enter image description here

That is, JoinedCurve and FilledCurve both prepend the coordinate list of segment i+1 with the last coordinate of segment i. This does not create an issue when segment primitives are Lines; but with BezierCurve and BSplineCurve repeated coordinates give a different picture. (It is unfortunate that these functions seem not to check if the first coordinate of segment i+1 is already the same as th last coordinate of segment i before inserting a new point).

What spline weight is assigned to the new point is anyone's guess. With a small weight, say 10^-5, for the newly inserted point we can replicate the output from JoinedCurve[segments]:

segmentsb = {BSplineCurve[{{1, 0}, {1, 1}, {0, 1}}, SplineWeights -> sw], 
   BSplineCurve[{{0, 1}, {0, 1}, {-1, 1}, {-1, 0}}, 
    SplineWeights -> Prepend[10^-5] @ sw]};

Graphics[{Green, segmentsb, Black, Dashed, 
  JoinedCurve[segments]}, ImageSize -> Large]

enter image description here

$\endgroup$
1
  • $\begingroup$ (+1) I saw the description in the documentation, which says "... is equivalent to ..." Obviously, they are not equivalent. I have written a function which takes the points that will be duplicated out of the data. This makes things work as I had expected. $\endgroup$
    – robjohn
    Commented Nov 1, 2020 at 11:35
8
$\begingroup$

Additional Problem

In addition to the two problems I mentioned above, there was a third problem that

segments = {
   BSplineCurve[{{1,0},{1,1},{0,1}},SplineWeights->{1,1/Sqrt[2],1}],
   BSplineCurve[{{0,1},{-1,1},{-1,0}},SplineWeights->{1,1/Sqrt[2],1}]
};
Graphics[{Arrow[JoinedCurve[segments]}]

produced no output.

The other answers do get to the root cause of the problem; that is, the addition of points to the segments by JoinedCurve and FilledCurve. However, approximating the curve with small line segments produces choppy output. Encapsulating the segments in Lists produces odd results when filled.


Solution to All Three Problems

To solve all three problems and produce a nice, smooth curve, I ended up writing a function which removes the points that are going to be added by JoinedCurve and FilledCurve. This function is

alef = {First[#],Sequence @@ MapAt[Rest,Rest[#],{All,1}]}&

In a comment, kglr notes that the following is equivalent and shorter

alef = MapAt[Rest,#,{2;;,1}]&

The name is an acronym for "assume last equals first".

1. JoinedCurve Problem

segments = {
   BSplineCurve[{{1,0},{1,1},{0,1}},SplineWeights->{1,1/Sqrt[2],1}],
   BSplineCurve[{{0,1},{-1,1},{-1,0}},SplineWeights->{1,1/Sqrt[2],1}]
};
Graphics[{{Red,segments},JoinedCurve[alef[segments]]}]

produces

joined curve bug

as desired.

2. Arrow and JoinedCurve Problem

segments = {
   BSplineCurve[{{1,0},{1,1},{0,1}},SplineWeights->{1,1/Sqrt[2],1}],
   BSplineCurve[{{0,1},{-1,1},{-1,0}},SplineWeights->{1,1/Sqrt[2],1}]
};
Graphics[{Arrow[JoinedCurve[alef[segments]]]}]

produces

arrow joined curve bug

as desired.

3. FilledCurve Problem

segments = {
   BSplineCurve[{{1/4,-3/4},{1,-3/4},{1,0}},SplineWeights->{1,1/Sqrt[2],1}],
   BSplineCurve[{{1,0},{1,3/4},{1/4,3/4}},SplineWeights->{1,1/Sqrt[2],1}],
   Line[{{1/4,3/4},{-1/4,3/4}}],
   BSplineCurve[{{-1/4,3/4},{-1,3/4},{-1,0}},SplineWeights->{1,1/Sqrt[2],1}],
   BSplineCurve[{{-1,0},{-1,-3/4},{-(1/4),-3/4}},SplineWeights->{1,1/Sqrt[2],1}],
   Line[{{-1/4,-3/4},{1/4,-3/4}}]
};
Graphics[{{Red,segments},Lighter[Purple,3/4],FilledCurve[alef[segments]]}]

produces

filled curve bug

as desired.

$\endgroup$
3
  • 1
    $\begingroup$ slightly shorter: aleph = MapAt[Rest, #, {2 ;;, 1}]& $\endgroup$
    – kglr
    Commented Nov 1, 2020 at 14:57
  • $\begingroup$ Indeed, that works, too. Thanks. $\endgroup$
    – robjohn
    Commented Nov 1, 2020 at 15:03
  • $\begingroup$ @kglr: I have incorporated your comment into my answer (with attribution). $\endgroup$
    – robjohn
    Commented Nov 1, 2020 at 18:13
8
$\begingroup$

To avoid the issue mentioned by kglr where points are repeated, you can just add another layer of list:

segments = {
    BSplineCurve[{{1,0},{1,1},{0,1}},SplineWeights->{1,1/Sqrt[2],1}],
    BSplineCurve[{{0,1},{-1,1},{-1,0}},SplineWeights->{1,1/Sqrt[2],1}]
};
Graphics[{{Red,segments}, JoinedCurve[List/@segments]}]

enter image description here

Update

Now that the "SplineCircle" ResourceFunction works again, you could use it to create the BSplineCurve objects:

segments = {
    ResourceFunction["SplineCircle"][{1/4, 0}, 3/4, {1, 0}, {-Pi/2, Pi/2}],
    Line[{{1/4, 3/4}, {-1/4, 3/4}}],
    ResourceFunction["SplineCircle"][{-1/4,0}, 3/4, {1, 0}, {Pi/2, 3Pi/2}],
    Line[{{-1/4, -3/4}, {1/4, -3/4}}]
};

Graphics[{{Red, segments}, Lighter[Purple, 3/4], FilledCurve[segments]}]

enter image description here

$\endgroup$
7
  • 1
    $\begingroup$ (+1) This works for JoinedCurve, but gives odd output when applied to the FilledCurve example. $\endgroup$
    – robjohn
    Commented Nov 1, 2020 at 11:31
  • $\begingroup$ @robjohn You're right, my approach doesn't work for FilledCurve. Why don't you create a single BSplineCurve instead of trying to use JoinedCurve/FilledCurve to connect multiple BSplineCurves? $\endgroup$
    – Carl Woll
    Commented Nov 1, 2020 at 15:23
  • $\begingroup$ I thought about it, but it seemed easier to put together the pieces than to work out the placement all of the knots to get the same output. Furthermore, I still need to use FilledCurve to fill the inside, anyway. $\endgroup$
    – robjohn
    Commented Nov 1, 2020 at 17:38
  • $\begingroup$ @robjohn It's too bad the "SplineCircle" resource function is broken, otherwise you could just use that. $\endgroup$
    – Carl Woll
    Commented Nov 1, 2020 at 17:49
  • $\begingroup$ Does any one have a reference to understanding SplineWeights? The documentation illustrates how to use them, but not their definition. Is it obvious that the o.p.'s choice of SplineWeights -> {1, 1/Sqrt[2], 1} is the correct one? Why? $\endgroup$ Commented Nov 3, 2020 at 17:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.