.Net 与 GraphicsPath.Widen() 相反

发布于 2024-10-09 09:44:13 字数 7841 浏览 3 评论 0原文

我需要与 .Net 中的 GraphicsPath.Widen() 方法相反的方法:

public GraphicsPath Widen()

Widen() 方法不接受负参数,因此我需要相当于 < code>Inset 方法:

public GraphicsPath Inset()

您可以在开源 Inkscape 应用程序 (www.Inkscape.org) 中执行此操作,方法是转到菜单并选择“路径/插图”(插图数量存储在 Inkscape 属性对话框中)。由于 Inkscape 是开源的,所以应该可以在 C#.Net 中执行此操作,但我无法终生遵循 Inkscape C++ 源代码(而且我只需要这个函数,所以我不能证明学习 C++ 是合理的)来完成这个)。

基本上,我需要一个带有此签名的 GraphicsPath 扩展方法:

public static GraphicsPath Inset(this GraphicsPath original, float amount)
{
   //implementation
}

正如签名所述,它将采用一个 GraphicsPath 对象和一个传递量的 .Inset() 路径。就像今天的 Inkscape 一样。如果它简化了问题的话,所讨论的 GraphicsPaths 都是从 .PolyBezier 方法创建的(而不是其他),因此不需要考虑矩形、椭圆或任何其他形状,除非您想要为了完整性而这样做。

不幸的是,我没有 C++ 代码经验,所以我几乎不可能遵循 Inkscape 中包含的 C++ 逻辑。

[编辑:] 根据要求,这里是“MakeOffset”Inkscape 代码。对于 Inset,第二个参数 (double dec) 将为负,该参数的绝对值是引入形状的量。

我知道这里有很多依赖关系。如果您需要查看更多 Inkscape 源文件,请访问:http:// /sourceforge.net/projects/inkscape/files/inkscape/0.48/

int
Shape::MakeOffset (Shape * a, double dec, JoinType join, double miter, bool do_profile, double cx, double cy, double radius, Geom::Matrix *i2doc)
{
  Reset (0, 0);
  MakeBackData(a->_has_back_data);

    bool done_something = false;

  if (dec == 0)
  {
    _pts = a->_pts;
    if (numberOfPoints() > maxPt)
    {
      maxPt = numberOfPoints();
      if (_has_points_data) {
        pData.resize(maxPt);
        _point_data_initialised = false;
        _bbox_up_to_date = false;
        }
    }

    _aretes = a->_aretes;
    if (numberOfEdges() > maxAr)
    {
      maxAr = numberOfEdges();
      if (_has_edges_data)
    eData.resize(maxAr);
      if (_has_sweep_src_data)
        swsData.resize(maxAr);
      if (_has_sweep_dest_data)
        swdData.resize(maxAr);
      if (_has_raster_data)
        swrData.resize(maxAr);
      if (_has_back_data)
        ebData.resize(maxAr);
    }
    return 0;
  }
  if (a->numberOfPoints() <= 1 || a->numberOfEdges() <= 1 || a->type != shape_polygon)
    return shape_input_err;

  a->SortEdges ();

  a->MakeSweepDestData (true);
  a->MakeSweepSrcData (true);

  for (int i = 0; i < a->numberOfEdges(); i++)
  {
    //              int    stP=a->swsData[i].stPt/*,enP=a->swsData[i].enPt*/;
    int stB = -1, enB = -1;
    if (dec > 0)
    {
      stB = a->CycleNextAt (a->getEdge(i).st, i);
      enB = a->CyclePrevAt (a->getEdge(i).en, i);
    }
    else
    {
      stB = a->CyclePrevAt (a->getEdge(i).st, i);
      enB = a->CycleNextAt (a->getEdge(i).en, i);
    }

    Geom::Point stD, seD, enD;
    double stL, seL, enL;
    stD = a->getEdge(stB).dx;
    seD = a->getEdge(i).dx;
    enD = a->getEdge(enB).dx;

    stL = sqrt (dot(stD,stD));
    seL = sqrt (dot(seD,seD));
    enL = sqrt (dot(enD,enD));
    MiscNormalize (stD);
    MiscNormalize (enD);
    MiscNormalize (seD);

    Geom::Point ptP;
    int stNo, enNo;
    ptP = a->getPoint(a->getEdge(i).st).x;

        double this_dec;
        if (do_profile && i2doc) {
            double alpha = 1;
            double x = (Geom::L2(ptP * (*i2doc) - Geom::Point(cx,cy))/radius);
            if (x > 1) {
                this_dec = 0;
            } else if (x <= 0) {
                this_dec = dec;
            } else {
                this_dec = dec * (0.5 * cos (M_PI * (pow(x, alpha))) + 0.5);
            }
        } else {
            this_dec = dec;
        }

        if (this_dec != 0)
            done_something = true;

    int   usePathID=-1;
    int   usePieceID=0;
    double useT=0.0;
    if ( a->_has_back_data ) {
      if ( a->ebData[i].pathID >= 0 && a->ebData[stB].pathID == a->ebData[i].pathID && a->ebData[stB].pieceID == a->ebData[i].pieceID
           && a->ebData[stB].tEn == a->ebData[i].tSt ) {
        usePathID=a->ebData[i].pathID;
        usePieceID=a->ebData[i].pieceID;
        useT=a->ebData[i].tSt;
      } else {
        usePathID=a->ebData[i].pathID;
        usePieceID=0;
        useT=0;
      }
    }
    if (dec > 0)
    {
      Path::DoRightJoin (this, this_dec, join, ptP, stD, seD, miter, stL, seL,
                         stNo, enNo,usePathID,usePieceID,useT);
      a->swsData[i].stPt = enNo;
      a->swsData[stB].enPt = stNo;
    }
    else
    {
      Path::DoLeftJoin (this, -this_dec, join, ptP, stD, seD, miter, stL, seL,
                        stNo, enNo,usePathID,usePieceID,useT);
      a->swsData[i].stPt = enNo;
      a->swsData[stB].enPt = stNo;
    }
  }

  if (dec < 0)
  {
    for (int i = 0; i < numberOfEdges(); i++)
      Inverse (i);
  }

  if ( _has_back_data ) {
    for (int i = 0; i < a->numberOfEdges(); i++)
    {
      int nEd=AddEdge (a->swsData[i].stPt, a->swsData[i].enPt);
      ebData[nEd]=a->ebData[i];
    }
  } else {
    for (int i = 0; i < a->numberOfEdges(); i++)
    {
      AddEdge (a->swsData[i].stPt, a->swsData[i].enPt);
    }
  }

  a->MakeSweepSrcData (false);
  a->MakeSweepDestData (false);

  return (done_something? 0 : shape_nothing_to_do);
}

[编辑]

@Simon Mourier - 了不起的工作。代码甚至干净且可读!干得好,先生。不过,我确实有几个问题要问你。

首先,金额为正数代表什么?我认为对于 Offset 方法,正数是“开始”,负数是“插入”,但你的例子似乎做了相反的事情。

其次,我做了一些基本测试(只是扩展你的样本),并发现了一些奇怪的地方。

这是当偏移量增加时,cool 中的“l”会发生什么(对于这样一个简单的字母,它肯定会引起问题!)。

Simon Test 2

...以及重现该代码的代码:

    private void Form1_Paint(object sender, PaintEventArgs e)
    {
            GraphicsPath path = new GraphicsPath();

            path.AddString("cool", new FontFamily("Arial"), 0, 200, new PointF(), StringFormat.GenericDefault);

            GraphicsPath offset1 = path.Offset(32);

            e.Graphics.DrawPath(new Pen(Color.Black, 1), path);
            e.Graphics.DrawPath(new Pen(Color.Red, 1), offset1);
    }

最后,有些不同。这是 Wingdings 中的“S”字符(看起来像泪滴):

Tear Drop

这是代码:

    private void Form1_Paint(object sender, PaintEventArgs e)
    {
        GraphicsPath path = new GraphicsPath();
        path.AddString("S", new FontFamily("Wingdings"), 0, 200, new PointF(), StringFormat.GenericDefault);
        GraphicsPath offset1 = path.Offset(20);

        e.Graphics.DrawPath(new Pen(Color.Black, 1), path);
        e.Graphics.DrawPath(new Pen(Color.Red, 1), offset1);
    }

伙计,距离如此之近,让我想哭。但它仍然不起作用。

我认为解决这个问题的方法是查看插入向量何时相交,并停止插入超过该点。如果插入量太大(或路径太小)以至于没有任何剩余,则路径应该消失(变为空),而不是自行反转并重新扩展。

再说一次,我并不是以任何方式批评你所做的事情,但我想知道你是否知道这些例子可能会发生什么。

(PS - 我添加了“this”关键字以使其成为扩展方法,因此您可能需要使用方法(参数)表示法调用代码才能运行这些示例)

@RAN Ran 通过重新使用 GraphicsPath 本机方法得出了类似的输出。伙计,这很难。两人的距离如此之近。

以下是两个示例的屏幕截图,使用 Wingdings 中的字符“S”:

Tear drop Comparison

@Simon 位于左边,@Ran 右边。

这是在 Inkscape 中进行“插入”后的相同泪滴“S”字符。插图是干净的:

Tear Inkscape

顺便说一句,这是 @Ran 测试的代码:

    private void Form1_Paint(object sender, PaintEventArgs e)
    {
        GraphicsPath path = new GraphicsPath();
        path.AddString("S", new FontFamily("Wingdings"), 0, 200, new PointF(), StringFormat.GenericDefault);
        e.Graphics.DrawPath(new Pen(Color.Black, 1), path);

        GraphicsPath offset1 = path.Shrink(20);
        e.Graphics.DrawPath(new Pen(Color.Red, 1), offset1);
    }

I need the opposite of the GraphicsPath.Widen() method in .Net:

public GraphicsPath Widen()

The Widen() method does not accept a negative parameter, so I need the equivalent of an Inset method:

public GraphicsPath Inset()

You can do this in the open source Inkscape application (www.Inkscape.org) by going to menu and selecting "Path / Inset" (the Inset amount is stored in the Inkscape Properties dialog). Since Inkscape is open source, it should be possible to do this in C#.Net, but I can't follow the the Inkscape C++ source for the life of me (and I just need this one function so I can't justify learning C++ to complete this).

Basically, I need a GraphicsPath extension method with this signature:

public static GraphicsPath Inset(this GraphicsPath original, float amount)
{
   //implementation
}

As the signature states, it will take a GraphicsPath object and .Inset() the path by a passed amount... just like Inkscape does today. If it simplifies matters any, the GraphicsPaths in question are all created from the .PolyBezier method (and nothing else), so there is no need to account for rects, ellipses, or any other shapes unless you want to do it for completeness.

Unfortunately, I have no experience with C++ code, so its just about impossible for me to follow the C++ logic contained in Inkscape.

.

[EDIT:]
As requested, here is the "MakeOffset" Inkscape code. The second parameter (double dec) will be negative for an Inset, and the absolute value of that parameter is the amount to bring in the shape.

I know that there are a lot of dependencies here. If you need to see more of the Inkscape source files, they are here: http://sourceforge.net/projects/inkscape/files/inkscape/0.48/

int
Shape::MakeOffset (Shape * a, double dec, JoinType join, double miter, bool do_profile, double cx, double cy, double radius, Geom::Matrix *i2doc)
{
  Reset (0, 0);
  MakeBackData(a->_has_back_data);

    bool done_something = false;

  if (dec == 0)
  {
    _pts = a->_pts;
    if (numberOfPoints() > maxPt)
    {
      maxPt = numberOfPoints();
      if (_has_points_data) {
        pData.resize(maxPt);
        _point_data_initialised = false;
        _bbox_up_to_date = false;
        }
    }

    _aretes = a->_aretes;
    if (numberOfEdges() > maxAr)
    {
      maxAr = numberOfEdges();
      if (_has_edges_data)
    eData.resize(maxAr);
      if (_has_sweep_src_data)
        swsData.resize(maxAr);
      if (_has_sweep_dest_data)
        swdData.resize(maxAr);
      if (_has_raster_data)
        swrData.resize(maxAr);
      if (_has_back_data)
        ebData.resize(maxAr);
    }
    return 0;
  }
  if (a->numberOfPoints() <= 1 || a->numberOfEdges() <= 1 || a->type != shape_polygon)
    return shape_input_err;

  a->SortEdges ();

  a->MakeSweepDestData (true);
  a->MakeSweepSrcData (true);

  for (int i = 0; i < a->numberOfEdges(); i++)
  {
    //              int    stP=a->swsData[i].stPt/*,enP=a->swsData[i].enPt*/;
    int stB = -1, enB = -1;
    if (dec > 0)
    {
      stB = a->CycleNextAt (a->getEdge(i).st, i);
      enB = a->CyclePrevAt (a->getEdge(i).en, i);
    }
    else
    {
      stB = a->CyclePrevAt (a->getEdge(i).st, i);
      enB = a->CycleNextAt (a->getEdge(i).en, i);
    }

    Geom::Point stD, seD, enD;
    double stL, seL, enL;
    stD = a->getEdge(stB).dx;
    seD = a->getEdge(i).dx;
    enD = a->getEdge(enB).dx;

    stL = sqrt (dot(stD,stD));
    seL = sqrt (dot(seD,seD));
    enL = sqrt (dot(enD,enD));
    MiscNormalize (stD);
    MiscNormalize (enD);
    MiscNormalize (seD);

    Geom::Point ptP;
    int stNo, enNo;
    ptP = a->getPoint(a->getEdge(i).st).x;

        double this_dec;
        if (do_profile && i2doc) {
            double alpha = 1;
            double x = (Geom::L2(ptP * (*i2doc) - Geom::Point(cx,cy))/radius);
            if (x > 1) {
                this_dec = 0;
            } else if (x <= 0) {
                this_dec = dec;
            } else {
                this_dec = dec * (0.5 * cos (M_PI * (pow(x, alpha))) + 0.5);
            }
        } else {
            this_dec = dec;
        }

        if (this_dec != 0)
            done_something = true;

    int   usePathID=-1;
    int   usePieceID=0;
    double useT=0.0;
    if ( a->_has_back_data ) {
      if ( a->ebData[i].pathID >= 0 && a->ebData[stB].pathID == a->ebData[i].pathID && a->ebData[stB].pieceID == a->ebData[i].pieceID
           && a->ebData[stB].tEn == a->ebData[i].tSt ) {
        usePathID=a->ebData[i].pathID;
        usePieceID=a->ebData[i].pieceID;
        useT=a->ebData[i].tSt;
      } else {
        usePathID=a->ebData[i].pathID;
        usePieceID=0;
        useT=0;
      }
    }
    if (dec > 0)
    {
      Path::DoRightJoin (this, this_dec, join, ptP, stD, seD, miter, stL, seL,
                         stNo, enNo,usePathID,usePieceID,useT);
      a->swsData[i].stPt = enNo;
      a->swsData[stB].enPt = stNo;
    }
    else
    {
      Path::DoLeftJoin (this, -this_dec, join, ptP, stD, seD, miter, stL, seL,
                        stNo, enNo,usePathID,usePieceID,useT);
      a->swsData[i].stPt = enNo;
      a->swsData[stB].enPt = stNo;
    }
  }

  if (dec < 0)
  {
    for (int i = 0; i < numberOfEdges(); i++)
      Inverse (i);
  }

  if ( _has_back_data ) {
    for (int i = 0; i < a->numberOfEdges(); i++)
    {
      int nEd=AddEdge (a->swsData[i].stPt, a->swsData[i].enPt);
      ebData[nEd]=a->ebData[i];
    }
  } else {
    for (int i = 0; i < a->numberOfEdges(); i++)
    {
      AddEdge (a->swsData[i].stPt, a->swsData[i].enPt);
    }
  }

  a->MakeSweepSrcData (false);
  a->MakeSweepDestData (false);

  return (done_something? 0 : shape_nothing_to_do);
}

.

[EDITS]

@Simon Mourier - Amazing work. The code was even clean and readable! Nice work, sir. I did have a couple of questions for you, though.

First, what does a positive number for the Amount represent? I was thinking that for the Offset method, positive would be "outset" and negative would be "inset", but your example seems to do the opposite.

Second, I did some basic testing (just extending your sample), and found some oddities.

Here is what happens to the "l" in cool when the offset grows (for such a simple letter, it sure likes to cause problems!).

Simon Test 2

...and the code to reproduce that one:

    private void Form1_Paint(object sender, PaintEventArgs e)
    {
            GraphicsPath path = new GraphicsPath();

            path.AddString("cool", new FontFamily("Arial"), 0, 200, new PointF(), StringFormat.GenericDefault);

            GraphicsPath offset1 = path.Offset(32);

            e.Graphics.DrawPath(new Pen(Color.Black, 1), path);
            e.Graphics.DrawPath(new Pen(Color.Red, 1), offset1);
    }

Finally, something a little different. Here is the "S" character from Wingdings (appears like a tear drop):

Tear Drop

Here is the code:

    private void Form1_Paint(object sender, PaintEventArgs e)
    {
        GraphicsPath path = new GraphicsPath();
        path.AddString("S", new FontFamily("Wingdings"), 0, 200, new PointF(), StringFormat.GenericDefault);
        GraphicsPath offset1 = path.Offset(20);

        e.Graphics.DrawPath(new Pen(Color.Black, 1), path);
        e.Graphics.DrawPath(new Pen(Color.Red, 1), offset1);
    }

Man, this is so close, it makes me want to cry. It still doesn't work, though.

I think what would fix it is to see when the inset vectors intersect, and stop insetting past that point. If the Inset amount is so large (or the path so small) that there is nothing left, the path should disappear (become null), instead of reversing on itself and re-expanding.

Again, I'm not knocking what you've done in any way, but I was wondering if you know what might be going on with these examples.

(PS - I added the 'this' keyword to make it an extension method, so you might need to call the code using method(parameters) notation to get these samples to run)

.

@RAN
Ran come up with a similar output, by re-using the GraphicsPath native methods. Man, this is tough. Both of them are so close.

Here is a screen shot of both examples, using the character "S" from Wingdings:

Tear drop comparison

@Simon is on the left, @Ran on the right.

Here is the same tear drop "S" character after doing an "Inset" in Inkscape. The Inset is clean:

Tear Inkscape

By the way, here is the code for @Ran's test:

    private void Form1_Paint(object sender, PaintEventArgs e)
    {
        GraphicsPath path = new GraphicsPath();
        path.AddString("S", new FontFamily("Wingdings"), 0, 200, new PointF(), StringFormat.GenericDefault);
        e.Graphics.DrawPath(new Pen(Color.Black, 1), path);

        GraphicsPath offset1 = path.Shrink(20);
        e.Graphics.DrawPath(new Pen(Color.Red, 1), offset1);
    }

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

马蹄踏│碎落叶 2024-10-16 09:44:13

我仍然会发布我的新解决方案,尽管它并不完美,并列出一些需要解决的问题。也许您会想参与其中并改进它们,或者其中可能有一些学习价值。

首先,图片 - 我最好的嵌入泪滴符号:

alt text

我所做的

  1. 我使用 GraphicsPath.Widen 生成给定图形的“内”和“外”边缘。

  2. 我扫描了生成的 GraphicsPath 的点,以删除外边缘并仅保留内边缘。

  3. 我使用GraphicsPath.Flatten展平内边缘,以便图形仅由线段组成(无曲线)。

  4. 然后我扫描了内部路径上的所有点,以及每个当前段:

    4.1。如果当前点 p 位于原始路径之外,或者距离原始路径上的线段太近,我会在当前边缘上计算一个新点,该点与原始路径,我用这个点代替p,并将其连接到我已经扫描的部分。

    4.2。解决方案当前的限制:我从计算出的点继续向前扫描。这意味着对带有孔的形状(例如Arial“o”)没有良好的支持。为了解决这个问题,人们必须维护一个“断开连接”的图形列表,并重新连接末端位于同一点(或末端彼此“足够接近”)的图形。

问题

首先,我将指定最主要的问题和限制,然后我将发布代码本身。

  1. 似乎 GraphicsPath.Widen 没有产生干净的形状。我得到的内部图形有很小的(但大部分是看不见的)“锯齿状”。这样做的意义在于,A) 我的剔除算法产生更多噪声,B) 图形有更多点,因此性能下降。

  2. 目前,性能勉强可以接受(如果有的话)。我的解决方案目前以一种非常简单的方式进行扫描(O(n^n))来查找“太靠近”内边缘上的候选点的线段。这导致算法非常慢。可以通过维护一些数据结构来改进,其中段按x排序,从而大大减少距离计算的数量。

  3. 我没有费心去优化代码以使用结构,而且还有很多其他地方可以优化代码以使其更快。

  4. 不支持带孔的形状,其中内部图形必须“拆分”为多个图形(如 Arial“o”)。我知道如何实现它,但需要更多时间:)

  5. 我会考虑采用西蒙移动现有点以获得内部图形的方法,并用我的方法来清理该路径。 (但我此时无法执行此操作,因为 Simon 的解决方案中存在一个错误,例如,该错误会导致撕裂符号的尖端移动到形状内的有效位置。我的算法认为该位置是有效的,并且不会清理它)。

代码

我无法避免想出一些我自己的数学/几何实用程序。
所以这是代码...就

我个人而言,我认为这值得赏金,即使它不是一个完美的解决方案...:)

public class LineSegment
{
    private readonly LineEquation line;
    private RectangleF bindingRectangle;

    public PointF A { get; private set; }
    public PointF B { get; private set; }

    public LineSegment(PointF a, PointF b)
    {
        A = a;
        B = b;

        line = new LineEquation(a, b);
        bindingRectangle = new RectangleF(
            Math.Min(a.X, b.X), Math.Min(a.Y, b.Y), 
            Math.Abs(a.X - b.X), Math.Abs(a.Y - b.Y));
    }

    public PointF? Intersect(LineSegment other)
    {
        var p = line.Intersect(other.line);
        if (p == null) return null;

        if (bindingRectangle.Contains(p.Value) &&
            other.bindingRectangle.Contains(p.Value))
        {
            return p;
        }
        return null;
    }

    public float Distance(PointF p)
    {
        if (LineEquation.IsBetween(line.GetNormalAt(A), p, line.GetNormalAt(B)))
        {
            return line.Distance(p);
        }
        return Math.Min(Distance(A, p), Distance(B, p));

    }

    static float Distance(PointF p1, PointF p2)
    {
        var x = p1.X - p2.X;
        var y = p1.Y - p2.Y;
        return (float) Math.Sqrt(x*x + y*y);
    }

    public PointF? IntersectAtDistance(LineSegment segmentToCut, float width)
    {
        // always assuming other.A is the farthest end
        var distance = width* (line.IsAboveOrRightOf(segmentToCut.A) ? 1 : -1);
        var parallelLine = line.GetParallelLine(distance);

        var p = parallelLine.Intersect(segmentToCut.line);
        if (p.HasValue)
        {
            if (LineEquation.IsBetween(line.GetNormalAt(A), p.Value, line.GetNormalAt(B)) &&
                segmentToCut.bindingRectangle.Contains(p.Value))
            {
                return p;
            }
        }

        List<PointF> points = new List<PointF>();
        points.AddRange(segmentToCut.line.Intersect(new CircleEquation(width, A)));
        points.AddRange(segmentToCut.line.Intersect(new CircleEquation(width, B)));

        return GetNearestPoint(segmentToCut.A, points);
    }

    public static PointF GetNearestPoint(PointF p, IEnumerable<PointF> points)
    {
        float minDistance = float.MaxValue;
        PointF nearestPoint = p;
        foreach (var point in points)
        {
            var d = Distance(p, point);
            if (d < minDistance)
            {
                minDistance = d;
                nearestPoint = point;
            }
        }
        return nearestPoint;
    }
}

public class LineEquation
{
    private readonly float a;
    private readonly float b;

    private readonly bool isVertical;
    private readonly float xConstForVertical;

    public LineEquation(float a, float b)
    {
        this.a = a;
        this.b = b;
        isVertical = false;
    }

    public LineEquation(float xConstant)
    {
        isVertical = true;
        xConstForVertical = xConstant;
    }

    public LineEquation(float a, PointF p)
    {
        this.a = a;
        b = p.Y - a*p.X;
        isVertical = false;
    }

    public LineEquation(PointF p1, PointF p2)
    {
        if (p1.X == p2.X)
        {
            isVertical = true;
            xConstForVertical = p1.X;
            return;
        }

        a = (p1.Y - p2.Y)/(p1.X - p2.X);
        b = p1.Y - a * p1.X;
        isVertical = false;
    }

    public PointF? Intersect(float x)
    {
        if (isVertical)
        {
            return null;
        }
        return new PointF(x, a*x + b);
    }

    public PointF? Intersect(LineEquation other)
    {
        if (isVertical && other.isVertical) return null;
        if (a == other.a) return null;

        if (isVertical) return other.Intersect(xConstForVertical);
        if (other.isVertical) return Intersect(other.xConstForVertical);

        // both have slopes and are not parallel
        var x = (b - other.b) / (other.a - a);
        return Intersect(x);
    }

    public float Distance(PointF p)
    {
        if (isVertical)
        {
            return Math.Abs(p.X - xConstForVertical);
        }
        var p1 = Intersect(0).Value;
        var p2 = Intersect(100).Value;

        var x1 = p.X - p1.X;
        var y1 = p.Y - p1.Y;
        var x2 = p2.X - p1.X;
        var y2 = p2.Y - p1.Y;

        return (float) (Math.Abs(x1*y2 - x2*y1) / Math.Sqrt(x2*x2 + y2*y2));
    }

    public bool IsAboveOrRightOf(PointF p)
    {
        return isVertical ? 
            xConstForVertical > p.X : 
            a*p.X + b > p.Y;
    }

    public static bool IsBetween(LineEquation l1, PointF p, LineEquation l2)
    {
        return l1.IsAboveOrRightOf(p) ^ l2.IsAboveOrRightOf(p);
    }

    public LineEquation GetParallelLine(float distance)
    {
        if (isVertical) return new LineEquation(xConstForVertical + distance);

        var angle = Math.Atan(a);
        float dy = (float) (distance/Math.Sin(angle));
        return new LineEquation(a, b - dy);
    }

    public LineEquation GetNormalAt(PointF p)
    {
        if (isVertical) return new LineEquation(p.X);

        var newA = -1/a;
        var newB = (a + 1/a)*p.X + b;
        return new LineEquation(newA, newB);
    }

    public PointF[] Intersect(CircleEquation circle)
    {
        var cx = circle.Center.X;
        var cy = circle.Center.Y;
        var r = circle.Radius;

        if (isVertical)
        {
            var distance = Math.Abs(cx - xConstForVertical);
            if (distance > r) return new PointF[0];
            if (distance == r) return new[] {new PointF(xConstForVertical, cy) };

            // two intersections
            var dx = cx - xConstForVertical;

            var qe = new QuadraticEquation(
                1,
                -2 * cy,
                r * r - dx * dx);

            return qe.Solve();
        }

        var t = b - cy;
        var q = new QuadraticEquation(
            1 + a*a,
            2*a*t - 2*cx,
            cx*cx + t*t - r*r);

        var solutions = q.Solve();
        for (var i = 0; i < solutions.Length; i++) 
           solutions[i] = Intersect(solutions[i].X).Value;
        return solutions;
    }
}

public class CircleEquation
{
    public float Radius { get; private set; }
    public PointF Center { get; private set; }

    public CircleEquation(float radius, PointF center)
    {
        Radius = radius;
        Center = center;
    }
}

public class QuadraticEquation
{
    public float A { get; private set; }
    public float B { get; private set; }
    public float C { get; private set; }

    public QuadraticEquation(float a, float b, float c)
    {
        A = a;
        B = b;
        C = c;
    }

    public PointF Intersect(float x)
    {
        return new PointF(x, A*x*x + B*x + C);
    }
    public PointF[] Solve()
    {
        var d = B*B - 4*A*C;
        if (d < 0) return new PointF[0];
        if (d == 0)
        {
            var x = -B / (2*A);
            return new[] { Intersect(x) };
        }

        var sd = Math.Sqrt(d);
        var x1 = (float) ((-B - sd) / (2f*A));
        var x2 = (float) ((-B + sd) / (2*A));
        return new[] { Intersect(x1), Intersect(x2) };
    }
}

public static class GraphicsPathExtension
{
    public static GraphicsPath Shrink(this GraphicsPath originalPath, float width)
    {
        originalPath.CloseAllFigures();
        originalPath.Flatten();
        var parts = originalPath.SplitFigures();
        var shrunkPaths = new List<GraphicsPath>();

        foreach (var part in parts)
        {
            using (var widePath = new GraphicsPath(part.PathPoints, part.PathTypes))
            {
                // widen the figure
                widePath.Widen(new Pen(Color.Black, width * 2));

                // pick the inner edge
                var innerEdge = widePath.SplitFigures()[1];
                var fixedPath = CleanPath(innerEdge, part, width);
                if (fixedPath.PointCount > 0)
                    shrunkPaths.Add(fixedPath);
            }
        }

        // build the result
        originalPath.Reset();
        foreach (var p in shrunkPaths)
        {
            originalPath.AddPath(p, false);
        }
        return originalPath;
    }

    public static IList<GraphicsPath> SplitFigures(this GraphicsPath path)
    {
        var paths = new List<GraphicsPath>();
        var position = 0;
        while (position < path.PointCount)
        {
            var figureCount = CountNextFigure(path.PathData, position);

            var points = new PointF[figureCount];
            var types = new byte[figureCount];

            Array.Copy(path.PathPoints, position, points, 0, figureCount);
            Array.Copy(path.PathTypes, position, types, 0, figureCount);
            position += figureCount;

            paths.Add(new GraphicsPath(points, types));
        }
        return paths;
    }

    static int CountNextFigure(PathData data, int position)
    {
        var count = 0;
        for (var i = position; i < data.Types.Length; i++)
        {
            count++;
            if (0 != (data.Types[i] & (int)PathPointType.CloseSubpath))
            {
                return count;
            }
        }
        return count;
    }

    static GraphicsPath CleanPath(GraphicsPath innerPath, GraphicsPath originalPath, float width)
    {
        var points = new List<PointF>();
        Region originalRegion = new Region(originalPath);

        // find first valid point
        int firstValidPoint = 0;
        IEnumerable<LineSegment> segs;

        while (IsPointTooClose(
                   innerPath.PathPoints[firstValidPoint], 
                   originalPath, originalRegion, width, out segs))
        {
            firstValidPoint++;
            if (firstValidPoint == innerPath.PointCount) return new GraphicsPath();
        }

        var prevP = innerPath.PathPoints[firstValidPoint];
        points.Add(prevP);

        for (int i = 1; i < innerPath.PointCount; i++)
        {
            var p = innerPath.PathPoints[(firstValidPoint + i) % innerPath.PointCount];

            if (!IsPointTooClose(p, originalPath, originalRegion, width, out segs))
            {
                prevP = p;
                points.Add(p);
                continue;
            }

            var invalidSegment = new LineSegment(prevP, p);

            // found invalid point (too close or external to original figure)
            IEnumerable<PointF> cutPoints = 
                segs.Select(seg => seg.IntersectAtDistance(invalidSegment, width).Value);
            var cutPoint = LineSegment.GetNearestPoint(prevP, cutPoints);

            // now add the cutPoint instead of 'p'.
            points.Add(cutPoint);
            prevP = cutPoint;
        }

        var types = new List<byte>();
        for (int i = 0; i < points.Count - 1; i++)
        {
            types.Add(1);
        }
        types.Add(129);

        return points.Count == 0 ?
            new GraphicsPath() :
            new GraphicsPath(points.ToArray(), types.ToArray());
    }

    static bool IsPointTooClose(
        PointF p, GraphicsPath path, Region region, 
        float distance, out IEnumerable<LineSegment> breakingSegments)
    {
        if (!region.IsVisible(p))
        {
            breakingSegments = new LineSegment[0];
            return true;
        }

        var segs = new List<LineSegment>();
        foreach (var seg in GetSegments(path))
        {
            if (seg.Distance(p) < distance)
            {
                segs.Add(seg);
            }
        }
        breakingSegments = segs;
        return segs.Count > 0;
    }

    static public IEnumerable<LineSegment> GetSegments(GraphicsPath path)
    {
        for (var i = 0; i < path.PointCount; i++)
        {
            yield return 
                new LineSegment(path.PathPoints[i], path.PathPoints[(i + 1) % path.PointCount]);
        }
    }
}

I'll still post my new solution, even though it's not perfect, with some list of problems that need to be fixed. Maybe you will want to take parts of it and improve them, or maybe there's some learning value in it.

First of all, the picture - my best inset teardrop symbol:

alt text

What I've done

  1. I used GraphicsPath.Widen to generate the "inner" and "outer" edges of the given figure.

  2. I scanned the points of the resulting GraphicsPath, to remove the outer edge and keep only the inner one.

  3. I flattened the inner edge using GraphicsPath.Flatten so that the figures consist only of line segments (no curves).

  4. I then scanned all the points on the inner path, and for each current segment:

    4.1. If the current point p is outside of the original path, or is too close to a segment on the original path, I calculate a new point, on the current edge, which is in the desired distance from the original path, and I take this point instead of p, and connect it to the part I've already scanned.

    4.2. A current limitation in the solution: I continue from the calculated point, to scan onward. This means that there is not good support for shapes with holes (e.g. the Arial "o"). To fix this, one would have to maintain a list of "disconnected" figures, and reconnect figures that have ends at the same point (or ends that are "close enough" to each other).

The problems

First I'll specifiy the most major problems and limitations, and then I'll post the code itself.

  1. It seems that GraphicsPath.Widen does not produce a clean shape. The inner figure that I get has small (but mostly invisible) "jaggedness". The significance of this is that A) my culling algorithm generates more noise, and B) the figure has more points, so performance degrades.

  2. Performance is barely acceptable, if at all, at this point. My solution currently scans in a very naive way (in O(n^n)) to find line segments that are "too near" the candidate points on the inner edge. This causes the algorithm to be very slow. It can be improved by maintaining some data structure in which segments are sorted by x, so that the number of distance calculations is dramatically reduced.

  3. I didn't bother to optimize the code to use structs and there are a lot of other places the code can be optimized to be much much faster.

  4. There is no support for shapes with holes, where the inner figure has to "split" into several figures (like the Arial "o"). I know how to implement it, but it needs more time :)

  5. I would consider adapting Simon's approach of moving existing points to get the inner figure, with my approach to clean that path up. (But I couldn't do it at this point because of a bug in Simon's solution, which, for example, causes the pointed end of the Tear symbol to move to a valid location inside the shape. My algorithm thinks this location is valid and doesn't clean it up).

The code

I couldn't avoid coming up with some math/geometry utilities of my own.
So here's the code...

Personally, I think this can be worthy of the bounty, even though it's not a perfect solution... :)

public class LineSegment
{
    private readonly LineEquation line;
    private RectangleF bindingRectangle;

    public PointF A { get; private set; }
    public PointF B { get; private set; }

    public LineSegment(PointF a, PointF b)
    {
        A = a;
        B = b;

        line = new LineEquation(a, b);
        bindingRectangle = new RectangleF(
            Math.Min(a.X, b.X), Math.Min(a.Y, b.Y), 
            Math.Abs(a.X - b.X), Math.Abs(a.Y - b.Y));
    }

    public PointF? Intersect(LineSegment other)
    {
        var p = line.Intersect(other.line);
        if (p == null) return null;

        if (bindingRectangle.Contains(p.Value) &&
            other.bindingRectangle.Contains(p.Value))
        {
            return p;
        }
        return null;
    }

    public float Distance(PointF p)
    {
        if (LineEquation.IsBetween(line.GetNormalAt(A), p, line.GetNormalAt(B)))
        {
            return line.Distance(p);
        }
        return Math.Min(Distance(A, p), Distance(B, p));

    }

    static float Distance(PointF p1, PointF p2)
    {
        var x = p1.X - p2.X;
        var y = p1.Y - p2.Y;
        return (float) Math.Sqrt(x*x + y*y);
    }

    public PointF? IntersectAtDistance(LineSegment segmentToCut, float width)
    {
        // always assuming other.A is the farthest end
        var distance = width* (line.IsAboveOrRightOf(segmentToCut.A) ? 1 : -1);
        var parallelLine = line.GetParallelLine(distance);

        var p = parallelLine.Intersect(segmentToCut.line);
        if (p.HasValue)
        {
            if (LineEquation.IsBetween(line.GetNormalAt(A), p.Value, line.GetNormalAt(B)) &&
                segmentToCut.bindingRectangle.Contains(p.Value))
            {
                return p;
            }
        }

        List<PointF> points = new List<PointF>();
        points.AddRange(segmentToCut.line.Intersect(new CircleEquation(width, A)));
        points.AddRange(segmentToCut.line.Intersect(new CircleEquation(width, B)));

        return GetNearestPoint(segmentToCut.A, points);
    }

    public static PointF GetNearestPoint(PointF p, IEnumerable<PointF> points)
    {
        float minDistance = float.MaxValue;
        PointF nearestPoint = p;
        foreach (var point in points)
        {
            var d = Distance(p, point);
            if (d < minDistance)
            {
                minDistance = d;
                nearestPoint = point;
            }
        }
        return nearestPoint;
    }
}

public class LineEquation
{
    private readonly float a;
    private readonly float b;

    private readonly bool isVertical;
    private readonly float xConstForVertical;

    public LineEquation(float a, float b)
    {
        this.a = a;
        this.b = b;
        isVertical = false;
    }

    public LineEquation(float xConstant)
    {
        isVertical = true;
        xConstForVertical = xConstant;
    }

    public LineEquation(float a, PointF p)
    {
        this.a = a;
        b = p.Y - a*p.X;
        isVertical = false;
    }

    public LineEquation(PointF p1, PointF p2)
    {
        if (p1.X == p2.X)
        {
            isVertical = true;
            xConstForVertical = p1.X;
            return;
        }

        a = (p1.Y - p2.Y)/(p1.X - p2.X);
        b = p1.Y - a * p1.X;
        isVertical = false;
    }

    public PointF? Intersect(float x)
    {
        if (isVertical)
        {
            return null;
        }
        return new PointF(x, a*x + b);
    }

    public PointF? Intersect(LineEquation other)
    {
        if (isVertical && other.isVertical) return null;
        if (a == other.a) return null;

        if (isVertical) return other.Intersect(xConstForVertical);
        if (other.isVertical) return Intersect(other.xConstForVertical);

        // both have slopes and are not parallel
        var x = (b - other.b) / (other.a - a);
        return Intersect(x);
    }

    public float Distance(PointF p)
    {
        if (isVertical)
        {
            return Math.Abs(p.X - xConstForVertical);
        }
        var p1 = Intersect(0).Value;
        var p2 = Intersect(100).Value;

        var x1 = p.X - p1.X;
        var y1 = p.Y - p1.Y;
        var x2 = p2.X - p1.X;
        var y2 = p2.Y - p1.Y;

        return (float) (Math.Abs(x1*y2 - x2*y1) / Math.Sqrt(x2*x2 + y2*y2));
    }

    public bool IsAboveOrRightOf(PointF p)
    {
        return isVertical ? 
            xConstForVertical > p.X : 
            a*p.X + b > p.Y;
    }

    public static bool IsBetween(LineEquation l1, PointF p, LineEquation l2)
    {
        return l1.IsAboveOrRightOf(p) ^ l2.IsAboveOrRightOf(p);
    }

    public LineEquation GetParallelLine(float distance)
    {
        if (isVertical) return new LineEquation(xConstForVertical + distance);

        var angle = Math.Atan(a);
        float dy = (float) (distance/Math.Sin(angle));
        return new LineEquation(a, b - dy);
    }

    public LineEquation GetNormalAt(PointF p)
    {
        if (isVertical) return new LineEquation(p.X);

        var newA = -1/a;
        var newB = (a + 1/a)*p.X + b;
        return new LineEquation(newA, newB);
    }

    public PointF[] Intersect(CircleEquation circle)
    {
        var cx = circle.Center.X;
        var cy = circle.Center.Y;
        var r = circle.Radius;

        if (isVertical)
        {
            var distance = Math.Abs(cx - xConstForVertical);
            if (distance > r) return new PointF[0];
            if (distance == r) return new[] {new PointF(xConstForVertical, cy) };

            // two intersections
            var dx = cx - xConstForVertical;

            var qe = new QuadraticEquation(
                1,
                -2 * cy,
                r * r - dx * dx);

            return qe.Solve();
        }

        var t = b - cy;
        var q = new QuadraticEquation(
            1 + a*a,
            2*a*t - 2*cx,
            cx*cx + t*t - r*r);

        var solutions = q.Solve();
        for (var i = 0; i < solutions.Length; i++) 
           solutions[i] = Intersect(solutions[i].X).Value;
        return solutions;
    }
}

public class CircleEquation
{
    public float Radius { get; private set; }
    public PointF Center { get; private set; }

    public CircleEquation(float radius, PointF center)
    {
        Radius = radius;
        Center = center;
    }
}

public class QuadraticEquation
{
    public float A { get; private set; }
    public float B { get; private set; }
    public float C { get; private set; }

    public QuadraticEquation(float a, float b, float c)
    {
        A = a;
        B = b;
        C = c;
    }

    public PointF Intersect(float x)
    {
        return new PointF(x, A*x*x + B*x + C);
    }
    public PointF[] Solve()
    {
        var d = B*B - 4*A*C;
        if (d < 0) return new PointF[0];
        if (d == 0)
        {
            var x = -B / (2*A);
            return new[] { Intersect(x) };
        }

        var sd = Math.Sqrt(d);
        var x1 = (float) ((-B - sd) / (2f*A));
        var x2 = (float) ((-B + sd) / (2*A));
        return new[] { Intersect(x1), Intersect(x2) };
    }
}

public static class GraphicsPathExtension
{
    public static GraphicsPath Shrink(this GraphicsPath originalPath, float width)
    {
        originalPath.CloseAllFigures();
        originalPath.Flatten();
        var parts = originalPath.SplitFigures();
        var shrunkPaths = new List<GraphicsPath>();

        foreach (var part in parts)
        {
            using (var widePath = new GraphicsPath(part.PathPoints, part.PathTypes))
            {
                // widen the figure
                widePath.Widen(new Pen(Color.Black, width * 2));

                // pick the inner edge
                var innerEdge = widePath.SplitFigures()[1];
                var fixedPath = CleanPath(innerEdge, part, width);
                if (fixedPath.PointCount > 0)
                    shrunkPaths.Add(fixedPath);
            }
        }

        // build the result
        originalPath.Reset();
        foreach (var p in shrunkPaths)
        {
            originalPath.AddPath(p, false);
        }
        return originalPath;
    }

    public static IList<GraphicsPath> SplitFigures(this GraphicsPath path)
    {
        var paths = new List<GraphicsPath>();
        var position = 0;
        while (position < path.PointCount)
        {
            var figureCount = CountNextFigure(path.PathData, position);

            var points = new PointF[figureCount];
            var types = new byte[figureCount];

            Array.Copy(path.PathPoints, position, points, 0, figureCount);
            Array.Copy(path.PathTypes, position, types, 0, figureCount);
            position += figureCount;

            paths.Add(new GraphicsPath(points, types));
        }
        return paths;
    }

    static int CountNextFigure(PathData data, int position)
    {
        var count = 0;
        for (var i = position; i < data.Types.Length; i++)
        {
            count++;
            if (0 != (data.Types[i] & (int)PathPointType.CloseSubpath))
            {
                return count;
            }
        }
        return count;
    }

    static GraphicsPath CleanPath(GraphicsPath innerPath, GraphicsPath originalPath, float width)
    {
        var points = new List<PointF>();
        Region originalRegion = new Region(originalPath);

        // find first valid point
        int firstValidPoint = 0;
        IEnumerable<LineSegment> segs;

        while (IsPointTooClose(
                   innerPath.PathPoints[firstValidPoint], 
                   originalPath, originalRegion, width, out segs))
        {
            firstValidPoint++;
            if (firstValidPoint == innerPath.PointCount) return new GraphicsPath();
        }

        var prevP = innerPath.PathPoints[firstValidPoint];
        points.Add(prevP);

        for (int i = 1; i < innerPath.PointCount; i++)
        {
            var p = innerPath.PathPoints[(firstValidPoint + i) % innerPath.PointCount];

            if (!IsPointTooClose(p, originalPath, originalRegion, width, out segs))
            {
                prevP = p;
                points.Add(p);
                continue;
            }

            var invalidSegment = new LineSegment(prevP, p);

            // found invalid point (too close or external to original figure)
            IEnumerable<PointF> cutPoints = 
                segs.Select(seg => seg.IntersectAtDistance(invalidSegment, width).Value);
            var cutPoint = LineSegment.GetNearestPoint(prevP, cutPoints);

            // now add the cutPoint instead of 'p'.
            points.Add(cutPoint);
            prevP = cutPoint;
        }

        var types = new List<byte>();
        for (int i = 0; i < points.Count - 1; i++)
        {
            types.Add(1);
        }
        types.Add(129);

        return points.Count == 0 ?
            new GraphicsPath() :
            new GraphicsPath(points.ToArray(), types.ToArray());
    }

    static bool IsPointTooClose(
        PointF p, GraphicsPath path, Region region, 
        float distance, out IEnumerable<LineSegment> breakingSegments)
    {
        if (!region.IsVisible(p))
        {
            breakingSegments = new LineSegment[0];
            return true;
        }

        var segs = new List<LineSegment>();
        foreach (var seg in GetSegments(path))
        {
            if (seg.Distance(p) < distance)
            {
                segs.Add(seg);
            }
        }
        breakingSegments = segs;
        return segs.Count > 0;
    }

    static public IEnumerable<LineSegment> GetSegments(GraphicsPath path)
    {
        for (var i = 0; i < path.PointCount; i++)
        {
            yield return 
                new LineSegment(path.PathPoints[i], path.PathPoints[(i + 1) % path.PointCount]);
        }
    }
}
夕嗳→ 2024-10-16 09:44:13

这是一个不错的选择。它不像@Simon 那样复杂,但它给出了很好的结果(可以进一步改进),并且代码更简单。

这个想法是重用 GraphicsPath.Widen 的现有功能来获取点。

当我们在由 n 个封闭图形组成的 GraphicsPath 上调用 Widen 时,生成的路径有 2n 条边。每个原始图形的外边缘和内边缘。

因此,我创建了一条临时路径,将其加宽,然后仅复制内边缘。

代码如下:

public static GraphicsPath Shrink(this GraphicsPath path, float width)
{
    using (var p = new GraphicsPath())
    {
        p.AddPath(path, false);
        p.CloseAllFigures();
        p.Widen(new Pen(Color.Black, width*2));

        var position = 0;
        var result = new GraphicsPath();
        while (position < p.PointCount)
        {
            // skip outer edge
            position += CountNextFigure(p.PathData, position);
            // count inner edge
            var figureCount = CountNextFigure(p.PathData, position);
            var points = new PointF[figureCount];
            var types = new byte[figureCount];

            Array.Copy(p.PathPoints, position, points, 0, figureCount);
            Array.Copy(p.PathTypes, position, types, 0, figureCount);
            position += figureCount;
            result.AddPath(new GraphicsPath(points, types), false);
        }
        path.Reset();
        path.AddPath(result, false);
        return path;
    }
}

static int CountNextFigure(PathData data, int position)
{
    int count = 0;
    for (var i = position; i < data.Types.Length; i++)
    {
        count++;
        if (0 != (data.Types[i] & (int) PathPointType.CloseSubpath))
        {
            return count;
        }
    }
    return count;
}

这是一个示例:

GraphicsPath path = new GraphicsPath();
path.AddString("cool", new FontFamily("Times New Roman"), 0, 300, 
    new PointF(), StringFormat.GenericDefault);
e.Graphics.DrawPath(new Pen(Color.Black, 1), path); 
path.Shrink(3);
e.Graphics.DrawPath(new Pen(Color.Red), path);

诚然,当偏移量大到足以导致形状与其自身相交时,我的解决方案也会出现不需要的伪影。

替代文字
编辑:

我可以轻松检测 O(n^2) 中的所有交点,或者付出一些努力 - 在 O(n logn) 中检测它们>),使用扫描线算法(n 是点数)。

但是,一旦找到交点,我不确定如何决定要删除路径的哪些部分。有人有主意吗? :)

编辑2:

实际上,我们并不需要找到图形的交集。

我们能做的就是扫描图形上的所有点。一旦我们发现一个点位于原始图形之外,或者太靠近原始图形的边缘,那么我们就必须修复它。

为了修复一个点,我们查看该点和前一个点之间的边缘,并且必须剪切该边缘,以便它现在将在距原始图形正确距离的新点处结束。

我用这个算法的近似值做了一些实验(用一个粗略但简单的算法,我完全删除了“关闭”点,而不是移动它们以缩短它们的边缘,并且我检查了到原始图形上的点的距离,而不是其边缘)。这得到了一些很好的结果,消除了大部分不需要的伪影。

要实现完整的解决方案可能需要几个小时...

编辑3:

虽然还远未达到完美,但我在单独的答案中发布了改进的解决方案。

Here is a nice alternative. It's not as sophisticated as @Simon's, but it gives nice results (that can be further improved), with much simpler code.

The idea is to reuse the existing functionality of GraphicsPath.Widen in order to get the points.

When we call Widen on a GraphicsPath that consists of n closed figures, the resulting path has 2n edges. An outer and an inner edge for each original figure.

So, I create a temporary path, widen it, and copy only the inner edges.

Here's the code:

public static GraphicsPath Shrink(this GraphicsPath path, float width)
{
    using (var p = new GraphicsPath())
    {
        p.AddPath(path, false);
        p.CloseAllFigures();
        p.Widen(new Pen(Color.Black, width*2));

        var position = 0;
        var result = new GraphicsPath();
        while (position < p.PointCount)
        {
            // skip outer edge
            position += CountNextFigure(p.PathData, position);
            // count inner edge
            var figureCount = CountNextFigure(p.PathData, position);
            var points = new PointF[figureCount];
            var types = new byte[figureCount];

            Array.Copy(p.PathPoints, position, points, 0, figureCount);
            Array.Copy(p.PathTypes, position, types, 0, figureCount);
            position += figureCount;
            result.AddPath(new GraphicsPath(points, types), false);
        }
        path.Reset();
        path.AddPath(result, false);
        return path;
    }
}

static int CountNextFigure(PathData data, int position)
{
    int count = 0;
    for (var i = position; i < data.Types.Length; i++)
    {
        count++;
        if (0 != (data.Types[i] & (int) PathPointType.CloseSubpath))
        {
            return count;
        }
    }
    return count;
}

And here's an example:

GraphicsPath path = new GraphicsPath();
path.AddString("cool", new FontFamily("Times New Roman"), 0, 300, 
    new PointF(), StringFormat.GenericDefault);
e.Graphics.DrawPath(new Pen(Color.Black, 1), path); 
path.Shrink(3);
e.Graphics.DrawPath(new Pen(Color.Red), path);

Admittedly, my solution also has undesired artifacts when the offset is large enough to cause the shape to intersect with itself.

alt text
EDIT:

I can easily detect all the intersection points in O(n^2), or with some effort - detect them in O(n logn), using a sweep line algorithm (n being the number of points).

But once I've found the intersection points, I'm not sure how to decide which parts of the path to remove. Anyone has an idea? :)

EDIT 2:

Actually, we don't really need to find intersections of the figures.

What we can do is scan all the points on the figure. Once we found a point that is either outside of the original figure, or is too close to an edge on the original figure, then we have to fix it.

In order to fix a point, we look at the edge between this point and the previous one, and we have to cut this edge so that it will now end in a new point, on the right distance from the original figure.

I've done some experiments with an approximate of this algorithm (with a crude but easy algorithm where I removed the "off" points entirely instead of moving them to shorten their edge, and I checked distances to points on the original figure instead of to edges on it). This got some nice results of removing most of the unwanted artifacts.

To implement the full solution would probably take a few hours...

EDIT 3:

Though still far from perfect, I posted my improved solution in a separate answer.

孤蝉 2024-10-16 09:44:13

这是一个似乎有效的代码。它支持封闭式和封闭式。开放的数字(这是困难的部分......),积极的&amp;负偏移量。

基本上,在路径中的每个点,它都会计算一个偏移点。偏移点是使用法线向量确定的,但实际上,它是使用两条偏移线的交点(等效)计算的。在某些情况下,它不会很好地显示(如果路径块太近,例如比偏移量更近)。

请注意,它不会对相交的图形进行组合/合并偏移,但这是另一个故事了。理论文章可以在这里找到:折线曲线的偏移算法。

您可以通过以下示例尝试一下:

protected override void OnPaint(PaintEventArgs e)
{
    GraphicsPath path = new GraphicsPath();

    path.AddString("cool", new FontFamily("Arial"), 0, 200, new PointF(), StringFormat.GenericDefault);
    path.AddEllipse(150, 50, 80, 80);
    path.AddEllipse(150 + 100, 50 + 100, 80 + 100, 80 + 100);

    GraphicsPath offset1 = Offset(path, -5);
    GraphicsPath offset2 = Offset(path, 5);

    e.Graphics.DrawPath(new Pen(Color.Black, 1), path);
    e.Graphics.DrawPath(new Pen(Color.Red, 1), offset1);
    e.Graphics.DrawPath(new Pen(Color.Blue, 1), offset2);
}

完整代码:

public static GraphicsPath Offset(GraphicsPath path, float offset)
{
    if (path == null)
        throw new ArgumentNullException("path");

    // death from natural causes
    if (path.PointCount < 2)
        throw new ArgumentException(null, "path");

    PointF[] points = new PointF[path.PointCount];

    for (int i = 0; i < path.PointCount; i++)
    {
        PointF current = path.PathPoints[i];
        PointF prev = GetPreviousPoint(path, i);
        PointF next = GetNextPoint(path, i);

        PointF offsetPoint = Offset(prev, current, next, offset);
        points[i] = offsetPoint;
    }

    GraphicsPath newPath = new GraphicsPath(points, path.PathTypes);
    return newPath;
}

// get the closing point for a figure or null if none was found
private static PointF? GetClosingPoint(GraphicsPath path, ref int index)
{
    for (int i = index + 1; i < path.PointCount; i++)
    {
        if (IsClosingPoint(path, i))
        {
            index = i;
            return path.PathPoints[i];
        }
    }
    return null;
}

// get the starting point for a figure or null if none was found
private static PointF? GetStartingPoint(GraphicsPath path, ref int index)
{
    for (int i = index - 1; i >= 0; i--)
    {
        if (IsStartingPoint(path, i))
        {
            index = i;
            return path.PathPoints[i];
        }
    }
    return null;
}

// get a previous point to compute normal vector at specified index
private static PointF GetPreviousPoint(GraphicsPath path, int index)
{
    if (IsStartingPoint(path, index))
    {
        int closingIndex = index;
        PointF? closing = GetClosingPoint(path, index, ref closingIndex);
        if (closing.HasValue)
        {
            if (closing.Value != path.PathPoints[index])
                return closing.Value;

            return GetPreviousPoint(path, closingIndex);
        }
    }
    else
    {
        return path.PathPoints[index - 1];
    }

    // we are on an unclosed end point, emulate a prev point on the same line using next point
    PointF point = path.PathPoints[index];
    PointF next = path.PathPoints[index + 1];
    return VectorF.Add(point, VectorF.Substract(point, next));
}

// get a next point to compute normal vector at specified index
private static PointF GetNextPoint(GraphicsPath path, int index)
{
    if (IsClosingPoint(path, index))
    {
        int startingIndex = index;
        PointF? starting = GetStartingPoint(path, ref startingIndex);
        if (starting.HasValue)
        {
            // some figures (Ellipse) are closed with the same point as the starting point
            // in this case, we need the starting point's next point
            if (starting.Value != path.PathPoints[index])
                return starting.Value;

            return GetNextPoint(path, startingIndex);
        }
    }
    else if ((index != (path.PointCount - 1)) && (!IsStartingPoint(path, index + 1)))
    {
        return path.PathPoints[index + 1];
    }

    // we are on an unclosed end point, emulate a next point on the same line using previous point
    PointF point = path.PathPoints[index];
    PointF prev = path.PathPoints[index - 1];
    return VectorF.Add(point, VectorF.Substract(point, prev));
}

// determine if a point is a closing point
private static bool IsClosingPoint(GraphicsPath path, int index)
{
    return (path.PathTypes[index] & (byte)PathPointType.CloseSubpath) == (byte)PathPointType.CloseSubpath;
}

// determine if a point is a starting point
private static bool IsStartingPoint(GraphicsPath path, int index)
{
    return (path.PathTypes[index] == (byte)PathPointType.Start);
}

// offsets a Point using the normal vector (actually computed using intersection or 90° rotated vectors)
private static PointF Offset(PointF prev, PointF current, PointF next, float offset)
{
    VectorF vnext = VectorF.Substract(next, current);
    vnext = vnext.DegreeRotate(Math.Sign(offset) * 90);
    vnext = vnext.Normalize() * Math.Abs(offset);
    PointF pnext1 = current + vnext;
    PointF pnext2 = next + vnext;

    VectorF vprev = VectorF.Substract(prev, current);
    vprev = vprev.DegreeRotate(-Math.Sign(offset) * 90);
    vprev = vprev.Normalize() * Math.Abs(offset);
    PointF pprev1 = current + vprev;
    PointF pprev2 = prev + vprev;

    PointF ix = VectorF.GetIntersection(pnext1, pnext2, pprev1, pprev2);
    if (ix.IsEmpty)
    {
        // 3 points on the same line, just translate (both vectors are identical)
        ix = current + vnext;
    }
    return ix;
}

// a useful Vector class (does not exists in GDI+, why?)
[Serializable, StructLayout(LayoutKind.Sequential)]
public struct VectorF : IFormattable, IEquatable<VectorF>
{
    private float _x;
    private float _y;

    public VectorF(float x, float y)
    {
        _x = x;
        _y = y;
    }

    public float X
    {
        get
        {
            return _x;
        }
        set
        {
            _x = value;
        }
    }

    public float Y
    {
        get
        {
            return _y;
        }
        set
        {
            _y = value;
        }
    }

    public double Length
    {
        get
        {
            return Math.Sqrt(_x * _x + _y * _y);
        }
    }

    public VectorF Rotate(double angle)
    {
        float cos = (float)Math.Cos(angle);
        float sin = (float)Math.Sin(angle);
        return new VectorF(_x * cos - _y * sin, _x * sin + _y * cos);
    }

    public VectorF DegreeRotate(double angle)
    {
        return Rotate(DegreeToGradiant(angle));
    }

    public static PointF GetIntersection(PointF start1, PointF end1, PointF start2, PointF end2)
    {
        float denominator = ((end1.X - start1.X) * (end2.Y - start2.Y)) - ((end1.Y - start1.Y) * (end2.X - start2.X));
        if (denominator == 0) // parallel
            return PointF.Empty;

        float numerator = ((start1.Y - start2.Y) * (end2.X - start2.X)) - ((start1.X - start2.X) * (end2.Y - start2.Y));
        float r = numerator / denominator;

        PointF result = new PointF();
        result.X = start1.X + (r * (end1.X - start1.X));
        result.Y = start1.Y + (r * (end1.Y - start1.Y));
        return result;
    }

    public static PointF Add(PointF point, VectorF vector)
    {
        return new PointF(point.X + vector._x, point.Y + vector._y);
    }

    public static VectorF Add(VectorF vector1, VectorF vector2)
    {
        return new VectorF(vector1._x + vector2._x, vector1._y + vector2._y);
    }

    public static VectorF Divide(VectorF vector, float scalar)
    {
        return vector * (1.0f / scalar);
    }

    public static VectorF Multiply(float scalar, VectorF vector)
    {
        return new VectorF(vector._x * scalar, vector._y * scalar);
    }

    public static VectorF Multiply(VectorF vector, float scalar)
    {
        return Multiply(scalar, vector);
    }

    public static VectorF operator *(float scalar, VectorF vector)
    {
        return Multiply(scalar, vector);
    }

    public static VectorF operator *(VectorF vector, float scalar)
    {
        return Multiply(scalar, vector);
    }

    public static PointF operator -(PointF point, VectorF vector)
    {
        return Substract(point, vector);
    }

    public static PointF operator +(VectorF vector, PointF point)
    {
        return Add(point, vector);
    }

    public static PointF operator +(PointF point, VectorF vector)
    {
        return Add(point, vector);
    }

    public static VectorF operator +(VectorF vector1, VectorF vector2)
    {
        return Add(vector1, vector2);
    }

    public static VectorF operator /(VectorF vector, float scalar)
    {
        return Divide(vector, scalar);
    }

    public static VectorF Substract(PointF point1, PointF point2)
    {
        return new VectorF(point1.X - point2.X, point1.Y - point2.Y);
    }

    public static PointF Substract(PointF point, VectorF vector)
    {
        return new PointF(point.X - vector._x, point.Y - vector._y);
    }

    public static double AngleBetween(VectorF vector1, VectorF vector2)
    {
        double y = (vector1._x * vector2._y) - (vector2._x * vector1._y);
        double x = (vector1._x * vector2._x) + (vector1._y * vector2._y);
        return Math.Atan2(y, x);
    }

    private static double GradiantToDegree(double angle)
    {
        return (angle * 180) / Math.PI;
    }

    private static double DegreeToGradiant(double angle)
    {
        return (angle * Math.PI) / 180;
    }

    public static double DegreeAngleBetween(VectorF vector1, VectorF vector2)
    {
        return GradiantToDegree(AngleBetween(vector1, vector2));
    }

    public VectorF Normalize()
    {
        if (Length == 0)
            return this;

        VectorF vector = this / (float)Length;
        return vector;
    }

    public override string ToString()
    {
        return ToString(null, null);
    }

    public string ToString(string format, IFormatProvider provider)
    {
        return string.Format(provider, "{0:" + format + "};{1:" + format + "}", _x, _y);
    }

    public override int GetHashCode()
    {
        return _x.GetHashCode() ^ _y.GetHashCode();
    }

    public override bool Equals(object obj)
    {
        if ((obj == null) || !(obj is VectorF))
            return false;

        return Equals(this, (VectorF)obj);
    }

    public bool Equals(VectorF value)
    {
        return Equals(this, value);
    }

    public static bool Equals(VectorF vector1, VectorF vector2)
    {
        return (vector1._x.Equals(vector2._x) && vector1._y.Equals(vector2._y));
    }
}

Here is a code that seems to work. It supports closed & open figures (that's the difficult part...), positive & negative offsets.

Basically, at each point in the path, it computes an Offset point. The offset point is determined using the normal vector, but in fact, it's computed using the intersection of the two offset lines (which is equivalent). There are some cases where it will not be displayed nicely (if path chunks are too close, closer than the offset for example).

Note it does no combine/merge offsets for intersecting figures, but this is another story. A theoretical article can be found here: An offset algorithm for polyline curves.

You can try it with this example:

protected override void OnPaint(PaintEventArgs e)
{
    GraphicsPath path = new GraphicsPath();

    path.AddString("cool", new FontFamily("Arial"), 0, 200, new PointF(), StringFormat.GenericDefault);
    path.AddEllipse(150, 50, 80, 80);
    path.AddEllipse(150 + 100, 50 + 100, 80 + 100, 80 + 100);

    GraphicsPath offset1 = Offset(path, -5);
    GraphicsPath offset2 = Offset(path, 5);

    e.Graphics.DrawPath(new Pen(Color.Black, 1), path);
    e.Graphics.DrawPath(new Pen(Color.Red, 1), offset1);
    e.Graphics.DrawPath(new Pen(Color.Blue, 1), offset2);
}

The complete code:

public static GraphicsPath Offset(GraphicsPath path, float offset)
{
    if (path == null)
        throw new ArgumentNullException("path");

    // death from natural causes
    if (path.PointCount < 2)
        throw new ArgumentException(null, "path");

    PointF[] points = new PointF[path.PointCount];

    for (int i = 0; i < path.PointCount; i++)
    {
        PointF current = path.PathPoints[i];
        PointF prev = GetPreviousPoint(path, i);
        PointF next = GetNextPoint(path, i);

        PointF offsetPoint = Offset(prev, current, next, offset);
        points[i] = offsetPoint;
    }

    GraphicsPath newPath = new GraphicsPath(points, path.PathTypes);
    return newPath;
}

// get the closing point for a figure or null if none was found
private static PointF? GetClosingPoint(GraphicsPath path, ref int index)
{
    for (int i = index + 1; i < path.PointCount; i++)
    {
        if (IsClosingPoint(path, i))
        {
            index = i;
            return path.PathPoints[i];
        }
    }
    return null;
}

// get the starting point for a figure or null if none was found
private static PointF? GetStartingPoint(GraphicsPath path, ref int index)
{
    for (int i = index - 1; i >= 0; i--)
    {
        if (IsStartingPoint(path, i))
        {
            index = i;
            return path.PathPoints[i];
        }
    }
    return null;
}

// get a previous point to compute normal vector at specified index
private static PointF GetPreviousPoint(GraphicsPath path, int index)
{
    if (IsStartingPoint(path, index))
    {
        int closingIndex = index;
        PointF? closing = GetClosingPoint(path, index, ref closingIndex);
        if (closing.HasValue)
        {
            if (closing.Value != path.PathPoints[index])
                return closing.Value;

            return GetPreviousPoint(path, closingIndex);
        }
    }
    else
    {
        return path.PathPoints[index - 1];
    }

    // we are on an unclosed end point, emulate a prev point on the same line using next point
    PointF point = path.PathPoints[index];
    PointF next = path.PathPoints[index + 1];
    return VectorF.Add(point, VectorF.Substract(point, next));
}

// get a next point to compute normal vector at specified index
private static PointF GetNextPoint(GraphicsPath path, int index)
{
    if (IsClosingPoint(path, index))
    {
        int startingIndex = index;
        PointF? starting = GetStartingPoint(path, ref startingIndex);
        if (starting.HasValue)
        {
            // some figures (Ellipse) are closed with the same point as the starting point
            // in this case, we need the starting point's next point
            if (starting.Value != path.PathPoints[index])
                return starting.Value;

            return GetNextPoint(path, startingIndex);
        }
    }
    else if ((index != (path.PointCount - 1)) && (!IsStartingPoint(path, index + 1)))
    {
        return path.PathPoints[index + 1];
    }

    // we are on an unclosed end point, emulate a next point on the same line using previous point
    PointF point = path.PathPoints[index];
    PointF prev = path.PathPoints[index - 1];
    return VectorF.Add(point, VectorF.Substract(point, prev));
}

// determine if a point is a closing point
private static bool IsClosingPoint(GraphicsPath path, int index)
{
    return (path.PathTypes[index] & (byte)PathPointType.CloseSubpath) == (byte)PathPointType.CloseSubpath;
}

// determine if a point is a starting point
private static bool IsStartingPoint(GraphicsPath path, int index)
{
    return (path.PathTypes[index] == (byte)PathPointType.Start);
}

// offsets a Point using the normal vector (actually computed using intersection or 90° rotated vectors)
private static PointF Offset(PointF prev, PointF current, PointF next, float offset)
{
    VectorF vnext = VectorF.Substract(next, current);
    vnext = vnext.DegreeRotate(Math.Sign(offset) * 90);
    vnext = vnext.Normalize() * Math.Abs(offset);
    PointF pnext1 = current + vnext;
    PointF pnext2 = next + vnext;

    VectorF vprev = VectorF.Substract(prev, current);
    vprev = vprev.DegreeRotate(-Math.Sign(offset) * 90);
    vprev = vprev.Normalize() * Math.Abs(offset);
    PointF pprev1 = current + vprev;
    PointF pprev2 = prev + vprev;

    PointF ix = VectorF.GetIntersection(pnext1, pnext2, pprev1, pprev2);
    if (ix.IsEmpty)
    {
        // 3 points on the same line, just translate (both vectors are identical)
        ix = current + vnext;
    }
    return ix;
}

// a useful Vector class (does not exists in GDI+, why?)
[Serializable, StructLayout(LayoutKind.Sequential)]
public struct VectorF : IFormattable, IEquatable<VectorF>
{
    private float _x;
    private float _y;

    public VectorF(float x, float y)
    {
        _x = x;
        _y = y;
    }

    public float X
    {
        get
        {
            return _x;
        }
        set
        {
            _x = value;
        }
    }

    public float Y
    {
        get
        {
            return _y;
        }
        set
        {
            _y = value;
        }
    }

    public double Length
    {
        get
        {
            return Math.Sqrt(_x * _x + _y * _y);
        }
    }

    public VectorF Rotate(double angle)
    {
        float cos = (float)Math.Cos(angle);
        float sin = (float)Math.Sin(angle);
        return new VectorF(_x * cos - _y * sin, _x * sin + _y * cos);
    }

    public VectorF DegreeRotate(double angle)
    {
        return Rotate(DegreeToGradiant(angle));
    }

    public static PointF GetIntersection(PointF start1, PointF end1, PointF start2, PointF end2)
    {
        float denominator = ((end1.X - start1.X) * (end2.Y - start2.Y)) - ((end1.Y - start1.Y) * (end2.X - start2.X));
        if (denominator == 0) // parallel
            return PointF.Empty;

        float numerator = ((start1.Y - start2.Y) * (end2.X - start2.X)) - ((start1.X - start2.X) * (end2.Y - start2.Y));
        float r = numerator / denominator;

        PointF result = new PointF();
        result.X = start1.X + (r * (end1.X - start1.X));
        result.Y = start1.Y + (r * (end1.Y - start1.Y));
        return result;
    }

    public static PointF Add(PointF point, VectorF vector)
    {
        return new PointF(point.X + vector._x, point.Y + vector._y);
    }

    public static VectorF Add(VectorF vector1, VectorF vector2)
    {
        return new VectorF(vector1._x + vector2._x, vector1._y + vector2._y);
    }

    public static VectorF Divide(VectorF vector, float scalar)
    {
        return vector * (1.0f / scalar);
    }

    public static VectorF Multiply(float scalar, VectorF vector)
    {
        return new VectorF(vector._x * scalar, vector._y * scalar);
    }

    public static VectorF Multiply(VectorF vector, float scalar)
    {
        return Multiply(scalar, vector);
    }

    public static VectorF operator *(float scalar, VectorF vector)
    {
        return Multiply(scalar, vector);
    }

    public static VectorF operator *(VectorF vector, float scalar)
    {
        return Multiply(scalar, vector);
    }

    public static PointF operator -(PointF point, VectorF vector)
    {
        return Substract(point, vector);
    }

    public static PointF operator +(VectorF vector, PointF point)
    {
        return Add(point, vector);
    }

    public static PointF operator +(PointF point, VectorF vector)
    {
        return Add(point, vector);
    }

    public static VectorF operator +(VectorF vector1, VectorF vector2)
    {
        return Add(vector1, vector2);
    }

    public static VectorF operator /(VectorF vector, float scalar)
    {
        return Divide(vector, scalar);
    }

    public static VectorF Substract(PointF point1, PointF point2)
    {
        return new VectorF(point1.X - point2.X, point1.Y - point2.Y);
    }

    public static PointF Substract(PointF point, VectorF vector)
    {
        return new PointF(point.X - vector._x, point.Y - vector._y);
    }

    public static double AngleBetween(VectorF vector1, VectorF vector2)
    {
        double y = (vector1._x * vector2._y) - (vector2._x * vector1._y);
        double x = (vector1._x * vector2._x) + (vector1._y * vector2._y);
        return Math.Atan2(y, x);
    }

    private static double GradiantToDegree(double angle)
    {
        return (angle * 180) / Math.PI;
    }

    private static double DegreeToGradiant(double angle)
    {
        return (angle * Math.PI) / 180;
    }

    public static double DegreeAngleBetween(VectorF vector1, VectorF vector2)
    {
        return GradiantToDegree(AngleBetween(vector1, vector2));
    }

    public VectorF Normalize()
    {
        if (Length == 0)
            return this;

        VectorF vector = this / (float)Length;
        return vector;
    }

    public override string ToString()
    {
        return ToString(null, null);
    }

    public string ToString(string format, IFormatProvider provider)
    {
        return string.Format(provider, "{0:" + format + "};{1:" + format + "}", _x, _y);
    }

    public override int GetHashCode()
    {
        return _x.GetHashCode() ^ _y.GetHashCode();
    }

    public override bool Equals(object obj)
    {
        if ((obj == null) || !(obj is VectorF))
            return false;

        return Equals(this, (VectorF)obj);
    }

    public bool Equals(VectorF value)
    {
        return Equals(this, value);
    }

    public static bool Equals(VectorF vector1, VectorF vector2)
    {
        return (vector1._x.Equals(vector2._x) && vector1._y.Equals(vector2._y));
    }
}
灼痛 2024-10-16 09:44:13

好吧,我想我已经为你们提供了线索……但方向完全不同。

无论如何,我意识到较大路径的“子路径”实际上在 .Widen 操作期间收缩(插入),因此我决定看看该路径是否有任何成果(没有双关语意图) )。

实际上,这里的想法是从外部.拓宽路径!

如果我们采用原始 GraphicsPath 并将其“包裹”在一个更大的 矩形 中(在 .GetBounds 上执行 10 的 Inflate)会怎样? GraphicsPath 应该会给我们一个简单的包装器)。

然后,首先添加包装器,然后将真正的 GraphicsPath 作为子路径添加到其中。然后整个事物得到一个 .Widen,最后,使用 .PathPoints从头开始​​创建一个新的 GraphicsPath。加宽路径的 PathTypes,它删除了无用的包装器(幸运的是,GraphicsPath 在构造函数之一中接受 PathPointsPathTypes过载)。

今天剩下的时间我将不在办公室,所以我无法完成这件事,但这是线索。

只需将此代码放入常规的 ol' 形式即可:

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            GraphicsPath g = new GraphicsPath();
            g.AddRectangle(new Rectangle(0, 0, 200, 200));
            g.AddEllipse(50, 50, 100, 100);

            //Original path
            e.Graphics.DrawPath(new Pen(Color.Black,2), g);

            //"Inset" path
            g.Widen(new Pen(Color.Black, 10));
            e.Graphics.DrawPath(new Pen(Color.Red, 2), g);
        }

从这个简单的实验中,您将看到目标路径(圆圈)现在具有难以捉摸的插图(红色)!

Inset Experiment

还有一些我不太明白的其他废话(也出现在矩形包装上) ),但是从 PathPointsPathTypes 来看,应该可以在创建原始 GraphicsPath 时迭代数组并删除垃圾(或者找出垃圾来自哪里)并防止其发生)。然后返回新的、干净的GraphicsPath

这种技术避免了所有复杂的数学运算,但可能性有点大。

OK, I think I have a lead for you guys... but its in a completely different direction.

Anyway, I realized that a "sub-path" of a larger path actually shrinks (insets) during a .Widen operation, so I decided to see if there was anything fruitful down that path (no pun intended).

Really, the idea here is to .Widen the path... from the outside!

What if we took the original GraphicsPath and 'wrapped' it in a larger Rectangle (doing an Inflate of 10 on the .GetBounds of the GraphicsPath should get us an easy wrapper).

Then, the wrapper is added first, and the real GraphicsPath is the added as a sub-path to that. The entire thing then gets a .Widen, and finally, a new GraphicsPath is created from scratch, using the .PathPoints and .PathTypes of the widened path, which removes the useless wrapper (luckily, the GraphicsPath accepts PathPoints and PathTypes in one of the constructor overloads).

I will be out of the office for the rest of the day, so I can't see this through to completion, but here is the lead.

Just drop this code into a regular ol' form:

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            GraphicsPath g = new GraphicsPath();
            g.AddRectangle(new Rectangle(0, 0, 200, 200));
            g.AddEllipse(50, 50, 100, 100);

            //Original path
            e.Graphics.DrawPath(new Pen(Color.Black,2), g);

            //"Inset" path
            g.Widen(new Pen(Color.Black, 10));
            e.Graphics.DrawPath(new Pen(Color.Red, 2), g);
        }

From that simple experiment, you will see that the target path (the circle) now has the elusive Inset (in red)!

Inset Experiment

There is also has some other crap that I don't really understand in there (which also appears on the rectangle wrapper), but from the PathPoints and PathTypes, it should be possible to iterate the arrays and remove the junk when the virgin GraphicsPath is created (or find out where that junk comes from and prevent it from happening). Then return the new, clean GraphicsPath.

This technique avoids all of the complex math, but its a bit of a long shot.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文