@dhq_boiler

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

ベジエ曲線の始点の位置がおかしい

お世話になっております。

解決したいこと

C# + WPFでベクターグラフィックスドローイングツールを開発しています。

2021-07-09.png

※下にソースコードへの案内を記載しております。よろしければそちらを参照ください。

発生している問題・エラー

現在、ベジエ曲線の描画ツールを実装中なのですが、ほぼ実装できたと思ったのですが、始点の方の制御点をぐるぐる回してみたところ、ベジエ曲線の始点の位置がずれて動いてしまうという問題が発覚しました。
詳細は以下のGIF画像を参照ください。

bezier_curve.gif

GIF画像では終点の方の制御点もぐるぐる回していますが、終点は正しく固定されているようです。

このバグについて原因がわかる方はいらっしゃいますでしょうか。よろしければ教えて下さい。

BezierCurveViewModel.cs
public class BezierCurveViewModel : ConnectorBaseViewModel
    {
        public ReactiveProperty<Point> ControlPoint1 { get; set; } = new ReactiveProperty<Point>();
        public ReactiveProperty<Point> ControlPoint2 { get; set; } = new ReactiveProperty<Point>();

        public ReactiveProperty<Point> ControlLine1LeftTop { get; set; } = new ReactiveProperty<Point>();
        public ReactiveProperty<Point> ControlLine2LeftTop { get; set; } = new ReactiveProperty<Point>();
        public ReactiveProperty<Point> LeftTop { get; set; } = new ReactiveProperty<Point>();

        public BezierCurveViewModel(int id, IDiagramViewModel parent)
            : base(id, parent)
        {
            Init();
        }

        public BezierCurveViewModel()
            : base()
        {
            Init();
        }

        public BezierCurveViewModel(Point p1, Point p2, Point c1, Point c2)
            : base()
        {
            Init();
            Points.Add(p1);
            Points.Add(p2);
            ControlPoint1.Value = c1;
            ControlPoint2.Value = c2;
        }

        private void Init()
        {
            Points.CollectionChanged += Points_CollectionChanged;
            ControlPoint1.Subscribe(x =>
            {
                if (Points.Count > 0)
                {
                    var point = new Point();
                    point.X = Math.Min(Points[0].X, ControlPoint1.Value.X);
                    point.Y = Math.Min(Points[0].Y, ControlPoint1.Value.Y);
                    ControlLine1LeftTop.Value = point;
                }
            })
            .AddTo(_CompositeDisposable);
            ControlPoint2.Subscribe(x =>
            {
                if (Points.Count > 1)
                {
                    var point = new Point();
                    point.X = Math.Min(Points[1].X, ControlPoint2.Value.X);
                    point.Y = Math.Min(Points[1].Y, ControlPoint2.Value.Y);
                    ControlLine2LeftTop.Value = point;
                }
            })
            .AddTo(_CompositeDisposable);
        }

        private void Points_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            if (Points.Count >= 2)
            {
                var point = new Point();
                point.X = Math.Min(Points[0].X, Points[1].X);
                point.Y = Math.Min(Points[0].Y, Points[1].Y);
                LeftTop.Value = point;
            }
        }

        public override object Clone()
        {
            var clone = new BezierCurveViewModel(Points[0], Points[1], ControlPoint1.Value, ControlPoint2.Value);
            clone.Owner = Owner;
            clone.EdgeColor = EdgeColor;
            clone.EdgeThickness = EdgeThickness;

            return clone;
        }
    }
BezierCurveDataTemplate.xaml
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:convereter="clr-namespace:boilersGraphics.Converters"
    xmlns:local="clr-namespace:boilersGraphics.Resources.DesignerItems"
    xmlns:control="clr-namespace:boilersGraphics.Controls"
    xmlns:viewModel="clr-namespace:boilersGraphics.ViewModels">
    <convereter:ToSolidColorBrushConverter x:Key="solidColorBrushConverter" />
    <DataTemplate DataType="{x:Type viewModel:BezierCurveViewModel}">
        <Canvas>
            <Path Canvas.Left="{Binding LeftTop.Value.X}"
                  Canvas.Top="{Binding LeftTop.Value.Y}"
                  IsHitTestVisible="False"
                  Stretch="Fill"
                  Stroke="{Binding EdgeColor, Converter={StaticResource solidColorBrushConverter}}"
                  StrokeThickness="{Binding EdgeThickness}">
                <Path.Data>
                    <PathGeometry>
                        <PathFigure StartPoint="{Binding Points[0]}">
                            <BezierSegment Point1="{Binding ControlPoint1.Value}"
                                            Point2="{Binding ControlPoint2.Value}"
                                            Point3="{Binding Points[1]}" />
                        </PathFigure>
                    </PathGeometry>
                </Path.Data>
            </Path>
            <Path Canvas.Left="{Binding ControlLine1LeftTop.Value.X}"
                  Canvas.Top="{Binding ControlLine1LeftTop.Value.Y}"
                  IsHitTestVisible="False"
                  Stretch="Fill"
                  Stroke="SkyBlue"
                  StrokeThickness="2"
                  StrokeDashArray="2 2">
                <Path.Data>
                    <PathGeometry>
                        <PathFigure StartPoint="{Binding Points[0]}">
                            <LineSegment Point="{Binding ControlPoint1.Value}" />
                        </PathFigure>
                    </PathGeometry>
                </Path.Data>
            </Path>
            <Path Canvas.Left="{Binding ControlLine2LeftTop.Value.X}"
                  Canvas.Top="{Binding ControlLine2LeftTop.Value.Y}"
                  IsHitTestVisible="False"
                  Stretch="Fill"
                  Stroke="SkyBlue"
                  StrokeThickness="2"
                  StrokeDashArray="2 2">
                <Path.Data>
                    <PathGeometry>
                        <PathFigure StartPoint="{Binding ControlPoint2.Value}">
                            <LineSegment Point="{Binding Points[1]}" />
                        </PathFigure>
                    </PathGeometry>
                </Path.Data>
            </Path>
            <control:ControlPoint x:Name="ControlPoint1"
                                  Background="Red"
                                  Canvas.Left="{Binding ControlPoint1.Value.X}"
                                  Canvas.Top="{Binding ControlPoint1.Value.Y}"
                                  Point="{Binding ControlPoint1.Value, Mode=TwoWay}" />
            <control:ControlPoint x:Name="ControlPoint2"
                                  Background="Red"
                                  Canvas.Left="{Binding ControlPoint2.Value.X}"
                                  Canvas.Top="{Binding ControlPoint2.Value.Y}"
                                  Point="{Binding ControlPoint2.Value, Mode=TwoWay}" />
        </Canvas>
    </DataTemplate>
</ResourceDictionary>

ソースコード

boiler's Graphics
https://github.com/dhq-boiler/boiler-s-Graphics

gitリポジトリ
https://github.com/dhq-boiler/boiler-s-Graphics.git

ブランチ:feature/BezierCurve

コミット:d04bd4b

自分で試したこと

LeftTopプロパティを実装して、Points[0]とPoint[1]のX, Yについて最小値をとり、それを設定するようなメソッドを作りました。
そして、それをPathのCanvas.Left, Canvas.Topに設定したのですが、上記のGIFアニメのようになりました。

BezierCurveViewModel.cs
        private void Points_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            if (Points.Count >= 2)
            {
                var point = new Point();
                point.X = Math.Min(Points[0].X, Points[1].X);
                point.Y = Math.Min(Points[0].Y, Points[1].Y);
                LeftTop.Value = point;
            }
        }

何か私の見落とし、致命的な勘違いなど気づいたところがあれば、回答していただけると助かります。よろしくお願いいたします。

0 likes

3Answer

アンカーの線は直線なので、Leftは「2つの点のX座標の小さい方」でいけますが、
始点アンカーを左に引っ張るとベジェの線は始点より左を通ります。
そのためベジェ曲線のLeftは「曲線を構成するすべての点の中で最もX座標が小さいもの」とする必要があります。

2Like

Comments

  1. @dhq_boiler

    Questioner

    ベジエ曲線のLeftTopを「曲線を構成するすべての点の中で最もX座標、Y座標が小さいもの」に修正したところ、正しい挙動をするようになりました。
    回答ありがとうございました。
    非常に参考になりました。

    別途修正後のソースコードを載せます。

動作には直接影響しないことばかりなんですが、SetLeftTop()はもうちょっと効率をよくして高速化できます。

今のコードだと

  1. BezierCurve.Evaluateを11回実行して結果をpointsに格納
  2. pointsからXの最小値を取り出す
  3. pointsからYの最小値を取り出す

の3回ループをしています。XとYの最小値が欲しいだけなのにちょっと無駄。
以下のように書けばループの回数を減らせます。

private void SetLeftTop()
{
    var tarray = new List<double> { 0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0 };
    var minX = int.MaxValue;
    var minY = int.MaxValue;
    foreach (var t in tarray)
    {
        var result = BezierCurve.Evaluate(t, new List<Point>() { this.Points[0], this.ControlPoint1.Value, this.ControlPoint2.Value, this.Points[1] });
        minX = Math.Min(minX, result.X);
        minY = Math.Min(minY, result.Y);
    }
    LeftTop.Value = new Point(minX, minY);
}

次にBezierCurve.Evaluateの第二引数はループ内で変化しないので、毎回newする必要はありません。

private void SetLeftTop()
{
    var tarray = new List<double> { 0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0 };
    var minX = int.MaxValue;
    var minY = int.MaxValue;
    var points = new List<Point>() { this.Points[0], this.ControlPoint1.Value, this.ControlPoint2.Value, this.Points[1] }
    foreach (var t in tarray)
    {
        var result = BezierCurve.Evaluate(t, points);
        minX = Math.Min(minX, result.X);
        minY = Math.Min(minY, result.Y);
    }
    LeftTop.Value = new Point(minX, minY);
}

またtarrayは固定長で単調増加なので、いちいちListを作らずfor文でループした方が高速でコードもわかりやすくなります。

private void SetLeftTop()
{
    var minX = int.MaxValue;
    var minY = int.MaxValue;
    var points = new List<Point>() { this.Points[0], this.ControlPoint1.Value, this.ControlPoint2.Value, this.Points[1] }
    for(var t = 0.0; t <= 1.0; t += 0.1)
    {
        var result = BezierCurve.Evaluate(t, points);
        minX = Math.Min(minX, result.X);
        minY = Math.Min(minY, result.Y);
    }
    LeftTop.Value = new Point(minX, minY);
}

さらに要素数が固定なら、ListよりArrayを使った方がちょっぴり速いです。

private void SetLeftTop()
{
    var minX = int.MaxValue;
    var minY = int.MaxValue;
    var points = new Point[] { this.Points[0], this.ControlPoint1.Value, this.ControlPoint2.Value, this.Points[1] }
    for(var t = 0.0; t <= 1.0; t += 0.1)
    {
        var result = BezierCurve.Evaluate(t, points);
        minX = Math.Min(minX, result.X);
        minY = Math.Min(minY, result.Y);
    }
    LeftTop.Value = new Point(minX, minY);
}

最後にBezierCurve.Evaluate()はtが0ならthis.Points[0]が、tが1ならthis.Points[1]と同じ値が返ってくるはずなので、最初と最後はBezierCurve.Evaluate()の呼び出し自体をスキップできます。

private void SetLeftTop()
{
    var minX = Math.Min(this.Points[0].X, this.Points[1].X);
    var minY = Math.Min(this.Points[0].Y, this.Points[1].Y);
    var points = new Point[] { this.Points[0], this.ControlPoint1.Value, this.ControlPoint2.Value, this.Points[1] }
    var diffT = 0.1;
    for(var t = diffT; t < 1.0; t += diffT)//0.1から0.9までだけ計算
    {
        var result = BezierCurve.Evaluate(t, points);
        minX = Math.Min(minX, result.X);
        minY = Math.Min(minY, result.Y);
    }
    LeftTop.Value = new Point(minX, minY);
}

正直、10分割では体感できるほどの速度差はないと思いますが、たとえばvar diffT = 1.0 / 64.0;とするだけで10分割から64分割に切り替えて精度を高めることも可能です。

1Like

Comments

  1. @dhq_boiler

    Questioner

    回答してくださりありがとうごさいます。
    正直動けば良いやと思っていて、高速化は頭の中にありませんでした。でもパフォーマンスを意識するなら重要なことですね。
    認識を改めようと思います。
    コードまで書いてくださってありがとうございます。参考にさせていただきますね。

以下、修正後のソースコードです。
ベジエ曲線を構成する点群をBezierCurve.Evaluate()で生成し、points.Select(x => x.X).Min(), points.Select(x => x.Y).Min()でX座標Y座標それぞれの最小値を取得し、LeftTopに設定します。
これは、点1、点2と制御点1と制御点2の値変更時に実行されます。

BezierCurveDataTemplate.xaml : 修正なし

BezierCurveViewModel.cs
    public class BezierCurveViewModel : ConnectorBaseViewModel
    {
        public ReactiveProperty<Point> ControlPoint1 { get; set; } = new ReactiveProperty<Point>();
        public ReactiveProperty<Point> ControlPoint2 { get; set; } = new ReactiveProperty<Point>();

        public ReactiveProperty<Point> ControlLine1LeftTop { get; set; } = new ReactiveProperty<Point>();
        public ReactiveProperty<Point> ControlLine2LeftTop { get; set; } = new ReactiveProperty<Point>();
        public ReactiveProperty<Point> LeftTop { get; set; } = new ReactiveProperty<Point>();

        public BezierCurveViewModel(int id, IDiagramViewModel parent)
            : base(id, parent)
        {
            Init();
        }

        public BezierCurveViewModel()
            : base()
        {
            Init();
        }

        public BezierCurveViewModel(Point p1, Point p2, Point c1, Point c2)
            : base()
        {
            Init();
            Points.Add(p1);
            Points.Add(p2);
            ControlPoint1.Value = c1;
            ControlPoint2.Value = c2;
        }

        private void Init()
        {
            Points.CollectionChanged += Points_CollectionChanged;
            ControlPoint1.Subscribe(x =>
            {
                if (Points.Count > 0)
                {
                    var point = new Point();
                    point.X = Math.Min(Points[0].X, ControlPoint1.Value.X);
                    point.Y = Math.Min(Points[0].Y, ControlPoint1.Value.Y);
                    ControlLine1LeftTop.Value = point;
                    SetLeftTop();
                }
            })
            .AddTo(_CompositeDisposable);
            ControlPoint2.Subscribe(x =>
            {
                if (Points.Count > 1)
                {
                    var point = new Point();
                    point.X = Math.Min(Points[1].X, ControlPoint2.Value.X);
                    point.Y = Math.Min(Points[1].Y, ControlPoint2.Value.Y);
                    ControlLine2LeftTop.Value = point;
                    SetLeftTop();
                }
            })
            .AddTo(_CompositeDisposable);
        }

        private void Points_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            if (Points.Count >= 2)
            {
                SetLeftTop();
            }
        }

        private void SetLeftTop()
        {
            var tarray = new List<double> { 0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0 };
            var points = new List<Point>();
            foreach (var t in tarray)
            {
                points.Add(BezierCurve.Evaluate(t, new List<Point>() { this.Points[0], this.ControlPoint1.Value, this.ControlPoint2.Value, this.Points[1] }));
            }
            LeftTop.Value = new Point(points.Select(x => x.X).Min(), points.Select(x => x.Y).Min());
        }

        public override object Clone()
        {
            var clone = new BezierCurveViewModel(Points[0], Points[1], ControlPoint1.Value, ControlPoint2.Value);
            clone.Owner = Owner;
            clone.EdgeColor = EdgeColor;
            clone.EdgeThickness = EdgeThickness;

            return clone;
        }
    }
BezierCurve.cs
    /// <summary>
    /// [数学] 3D空間にベジェ曲線を描く
    /// Author: @edo_m18
    /// https://qiita.com/edo_m18/items/643512f27c2b083b47ac
    /// で掲載されているソースコードを改変して実装
    /// </summary>
    public static class BezierCurve
    {

        /// <summary>
        /// ベジェ曲線関数
        /// </summary>
        public static Point Evaluate(double t, IEnumerable<Point> points)
        {
            Point result = new Point();
            int n = points.Count();
            for (int i = 0; i < n; i++)
            {
                result.X += points.ElementAt(i).X * Bernstein(n - 1, i, t);
                result.Y += points.ElementAt(i).Y * Bernstein(n - 1, i, t);
            }

            return result;
        }

        /// <summary>
        /// バーンスタイン基底関数
        /// </summary>
        static double Bernstein(int n, int i, double t)
        {
            return Binomial(n, i) * Math.Pow(t, i) * Math.Pow(1 - t, n - i);
        }

        /// <summary>
        /// 二項係数を計算する
        /// </summary>
        static double Binomial(int n, int k)
        {
            return Factorial(n) / (Factorial(k) * Factorial(n - k));
        }

        /// <summary>
        /// 階乗を計算する
        /// </summary>
        static double Factorial(int a)
        {
            double result = 1d;
            for (int i = 2; i <= a; i++)
            {
                result *= i;
            }

            return result;
        }
    }

bezier_curve_fixed.gif

0Like

Your answer might help someone💌