Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

.NET core should have primitive drawing types #14503

Closed
Priya91 opened this issue Apr 28, 2015 · 79 comments
Closed

.NET core should have primitive drawing types #14503

Priya91 opened this issue Apr 28, 2015 · 79 comments
Assignees
Labels
api-needs-work API needs work before it is approved, it is NOT ready for implementation area-System.Drawing
Milestone

Comments

@Priya91
Copy link
Contributor

Priya91 commented Apr 28, 2015

Issue: #14431
Based on the analysis done by Experience and Insights, primitive drawing types are commonly used even outside Image processing scenarios. The following types are to be considered:

  • Point
  • Rectangle
  • Size

From the discussion, it is clear we need int and float based versions of the primitives, we decided to expose the already existing System.Drawing primitives as is, that is, Point, PointF, Size, SizeF, Rectangle and RectangleF to .NET Core. We are not exposing Color at the moment because we feel the existing Color type in System.Drawing is insufficient as specified in the below conversations. Once the existing types are exposed, we will start a separate discussion on crafting Color primitive for .NET Core.

Contract

System.Drawing.Primitives

API Proposal

namespace System.Drawing
{
    public struct Point 
    {
        public static readonly Point Empty;
        public Point(Size sz);
        public Point(int dw);
        public Point(int x, int y);
        public bool IsEmpty { get; }
        public int X { get; set; }
        public int Y { get; set; }
        public static Point Add(Point pt, Size sz);
        public static Point Ceiling(PointF value);
        public override bool Equals(object obj);
        public override int GetHashCode();
        public void Offset(Point p);
        public void Offset(int dx, int dy);
        public static Point operator +(Point pt, Size sz);
        public static bool operator ==(Point left, Point right);
        public static explicit operator Size (Point p);
        public static implicit operator PointF (Point p);
        public static bool operator !=(Point left, Point right);
        public static Point operator -(Point pt, Size sz);
        public static Point Round(PointF value);
        public static Point Subtract(Point pt, Size sz);
        public override string ToString();
        public static Point Truncate(PointF value);
    }

    public struct PointF 
    {
        public static readonly PointF Empty;
        public PointF(float x, float y);
        public bool IsEmpty { get; }
        public float X { get; set; }
        public float Y { get; set; }
        public static PointF Add(PointF pt, Size sz);
        public static PointF Add(PointF pt, SizeF sz);
        public override bool Equals(object obj);
        public override int GetHashCode();
        public static PointF operator +(PointF pt, Size sz);
        public static PointF operator +(PointF pt, SizeF sz);
        public static bool operator ==(PointF left, PointF right);
        public static bool operator !=(PointF left, PointF right);
        public static PointF operator -(PointF pt, Size sz);
        public static PointF operator -(PointF pt, SizeF sz);
        public static PointF Subtract(PointF pt, Size sz);
        public static PointF Subtract(PointF pt, SizeF sz);
        public override string ToString();
    }

    public struct Rectangle 
    {
        public static readonly Rectangle Empty;
        public Rectangle(Point location, Size size);
        public Rectangle(int x, int y, int width, int height);
        public int Bottom { get; }
        public int Height { get; set; }
        public bool IsEmpty { get; }
        public int Left { get; }
        public Point Location { get; set; }
        public int Right { get; }
        public Size Size { get; set; }
        public int Top { get; }
        public int Width { get; set; }
        public int X { get; set; }
        public int Y { get; set; }
        public static Rectangle Ceiling(RectangleF value);
        public bool Contains(Point pt);
        public bool Contains(Rectangle rect);
        public bool Contains(int x, int y);
        public override bool Equals(object obj);
        public static Rectangle FromLTRB(int left, int top, int right, int bottom);
        public override int GetHashCode();
        public static Rectangle Inflate(Rectangle rect, int x, int y);
        public void Inflate(Size size);
        public void Inflate(int width, int height);
        public void Intersect(Rectangle rect);
        public static Rectangle Intersect(Rectangle a, Rectangle b);
        public bool IntersectsWith(Rectangle rect);
        public void Offset(Point pos);
        public void Offset(int x, int y);
        public static bool operator ==(Rectangle left, Rectangle right);
        public static bool operator !=(Rectangle left, Rectangle right);
        public static Rectangle Round(RectangleF value);
        public override string ToString();
        public static Rectangle Truncate(RectangleF value);
        public static Rectangle Union(Rectangle a, Rectangle b);
    }

    public struct RectangleF 
    {
        public static readonly RectangleF Empty;
        public RectangleF(PointF location, SizeF size);
        public RectangleF(float x, float y, float width, float height);
        public float Bottom { get; }
        public float Height { get; set; }
        public bool IsEmpty { get; }
        public float Left { get; }
        public PointF Location { get; set; }
        public float Right { get; }
        public SizeF Size { get; set; }
        public float Top { get; }
        public float Width { get; set; }
        public float X { get; set; }
        public float Y { get; set; }
        public bool Contains(PointF pt);
        public bool Contains(RectangleF rect);
        public bool Contains(float x, float y);
        public override bool Equals(object obj);
        public static RectangleF FromLTRB(float left, float top, float right, float bottom);
        public override int GetHashCode();
        public static RectangleF Inflate(RectangleF rect, float x, float y);
        public void Inflate(SizeF size);
        public void Inflate(float x, float y);
        public void Intersect(RectangleF rect);
        public static RectangleF Intersect(RectangleF a, RectangleF b);
        public bool IntersectsWith(RectangleF rect);
        public void Offset(PointF pos);
        public void Offset(float x, float y);
        public static bool operator ==(RectangleF left, RectangleF right);
        public static implicit operator RectangleF (Rectangle r);
        public static bool operator !=(RectangleF left, RectangleF right);
        public override string ToString();
        public static RectangleF Union(RectangleF a, RectangleF b);
    }

    public struct Size 
    {
        public static readonly Size Empty;
        public Size(Point pt);
        public Size(int width, int height);
        public int Height { get; set; }
        public bool IsEmpty { get; }
        public int Width { get; set; }
        public static Size Add(Size sz1, Size sz2);
        public static Size Ceiling(SizeF value);
        public override bool Equals(object obj);
        public override int GetHashCode();
        public static Size operator +(Size sz1, Size sz2);
        public static bool operator ==(Size sz1, Size sz2);
        public static explicit operator Point (Size size);
        public static implicit operator SizeF (Size p);
        public static bool operator !=(Size sz1, Size sz2);
        public static Size operator -(Size sz1, Size sz2);
        public static Size Round(SizeF value);
        public static Size Subtract(Size sz1, Size sz2);
        public override string ToString();
        public static Size Truncate(SizeF value);
    }

    public struct SizeF 
    {
        public static readonly SizeF Empty;
        public SizeF(PointF pt);
        public SizeF(SizeF size);
        public SizeF(float width, float height);
        public float Height { get; set; }
        public bool IsEmpty { get; }
        public float Width { get; set; }
        public static SizeF Add(SizeF sz1, SizeF sz2);
        public override bool Equals(object obj);
        public override int GetHashCode();
        public static SizeF operator +(SizeF sz1, SizeF sz2);
        public static bool operator ==(SizeF sz1, SizeF sz2);
        public static explicit operator PointF (SizeF size);
        public static bool operator !=(SizeF sz1, SizeF sz2);
        public static SizeF operator -(SizeF sz1, SizeF sz2);
        public static SizeF Subtract(SizeF sz1, SizeF sz2);
        public PointF ToPointF();
        public Size ToSize();
        public override string ToString();
    }
}
@Priya91 Priya91 self-assigned this Apr 28, 2015
@Priya91
Copy link
Contributor Author

Priya91 commented Apr 28, 2015

@terrajobst terrajobst changed the title [Api-addition] .NET core should have primitive drawing types .NET core should have primitive drawing types Apr 28, 2015
@justinvp
Copy link
Contributor

Method argument names need some tweaking...

Some examples:

- public Point(Size sz) : this() { }
+ public Point(Size size) : this() { }
- public void Offset(Point p) { }
+ public void Offset(Point point) { }
- public bool Equals(Point pt) { return default(bool); }
+ public bool Equals(Point other) { return default(bool); }
- public static bool operator ==(Size sz1, Size sz2) { return default(bool); }
+ public static bool operator ==(Size left, Size right) { return default(bool); }

There are a bunch of other similar cases; same feedback applies throughout.

@Priya91
Copy link
Contributor Author

Priya91 commented Apr 28, 2015

@justinvp : This is just a speclet, i'll fix these names in the actual PR, that will be committed.

@lilith
Copy link

lilith commented Apr 28, 2015

Integral Point/Size/Rect are quite useful as well. Should we make these generic or just create float/int variants as system.drawing does?

@lilith
Copy link

lilith commented Apr 28, 2015

We might want to qualify Color a bit more, say, by calling it ColorRgba. If we're trying to represent an RGB value, in byte form, then we need to specify the color profile (or list it as an assumption). sRGB is similar to gamma=2.2, and both avoid the visible banding that occurs if you try to store linear RGB values in 24 bits. If we accept floating-point parameters, we need to be clear about whether they are in linear form or 'compressed' sRGB form. Typically floating point RGB values are assumed to be in linear light.

Since composition and averaging of color values must (for correctness) occur in linear light, we should probably expose a float variant if we want to offer related math functions.

Also, if we want to reuse this for (fast) interop, we would want the structure to have byte offsets such that it can be stored as a 32-bit integer in BGRA form (little-endian, windows), or ARGB (big-endian, ARM, etc, linux). Things get much slower if there is a memory layout discrepancy when compared to the host platform.

Regardless of interop, memory layout matters. Anything deal with large arrays of colors will be very sensitive to cache misses, which will happen much more frequently if we're taking 128 bytes (4 fields) instead of 32.

Punting and mirroring the CSS color specification, listing Color variants as storage-only representations might work. Although there are sure to be lots of extension methods that deal with them incorrectly.

I'd suggest making Color a separate specification, as it has more aspects to consider than Point/Size/Rect.

@leppie
Copy link
Contributor

leppie commented Apr 29, 2015

I agree with @nathanaeljones.

Color needs to be separate and flexible enough to deal with various color spaces.

I wrote some code a while back to deal with conversions. See https://github.com/leppie/Colorspace, if interested.

@xanather
Copy link

I think the Color data type should stay as it is proposed. If chosen to take the other path there could be countless other Color types that all have different internal structuring. I don't think .NET Core needs this and is very application specific. Int based Point/Size/Rectangle could be beneficial however but don't see it being that important.

I mean if the choice is made to go slightly out of scope of .NET Core and define every single drawing primitive type possible then why not, but I think its best to keep it simple.

@patricksadowski
Copy link

The Color type is still based on RGB so it's easy to add more methods.

public static Color Invert(Color color) { return default(Color); } // Inversion of alpha is not necessary
// Maybe:
public static Color Light(Color color, float amount) { return default(Color); } // lerp towards white
public static Color Dark(Color color, float amount) { return default(Color); } // lerp towards black

The API Proposal decribes the methods GetBrightness, GetHue and GetSaturation - a one-way conversion from RGB to HSB. A two-way conversion would be great and consistent. This would cover simple use cases for RGB and HSB without the need for special Color types.

Are there thoughts to add a method to create a color from r, g, b?

@lilith
Copy link

lilith commented Apr 29, 2015

is very application specific. Int based Point/Size/Rectangle could be beneficial however but don't see it being that important.

@xanather Could you be a bit more specific about the use-cases and the context you're making these statements from?

@lilith
Copy link

lilith commented Apr 29, 2015

@patricksadowski The point is that Light and Dark have completely different meanings depending on the color space you're talking about. And do you want 'amount' to be perceptually linear, as would be intuitive? Or mimic some gamma curve, as the naive implementation would?

@krwq
Copy link
Member

krwq commented Apr 29, 2015

I have a few questions:

Point.IsEmpty // what does it do?

Why isn't point a generic type and this class one of its implementations (Point<float>)?
Intuitively I'd expect point to rather have some integer type if it is not a generic.

        public static Point Ceiling(Point value) { return default(Point); }
        public static Point Truncate(Point value) { return default(Point); }
        public static Point Round(Point value) { return default(Point); }

I don't think we should have that operations on point like that (and other similar), I'd move them to separate class with static extensions. Point is not something you do mathematical operations on, that's what vector is for. Point is just a data structure and should not represent a two element vector.

Same comments for Rectangle and Size.

        // I don't understand what do these method names mean but looking at args:
        // Why not FromRGBA?
        public static Color FromNonPremultiplied(int r, int g, int b, int a) { return default(Color); }
        // Why not: FromVector4, also feels like FromVector3 is missing
        public static Color FromNonPremultiplied(Vector4 vector) { return default(Color); }

        // Why not Interpolate?
        public static Color Lerp(Color value1, Color value2, float amount) { return default(Color); }
        // No clue what does this do but whatever graphical represantation you can see you should be using Vector4 for computation and Color for representation
        public static Color Multiply(Color value, float scale) { return default(Color); 

Also I believe we should completely remove any dependencies on Vector and rather add some static extensions class to make conversions. Why might factor them out to separate assembly in the future.

@Priya91
Copy link
Contributor Author

Priya91 commented Apr 29, 2015

@krwq

Point.IsEmpty // what does it do?

This checks for if point is empty (0,0)

        public static Point Ceiling(Point value) { return default(Point); }
        public static Point Truncate(Point value) { return default(Point); }
        public static Point Round(Point value) { return default(Point); }

These APIs were taken from existing System.Drawing.Point, and these are defined as methods over Point. Following already existing design, and I don't agree with Point is not something you do mathematical operations on, it's a mathematical data structure. Vector has a magnitude and direction, Point is just a location in space. And these methods are not vector methods.

        // I don't understand what do these method names mean but looking at args:
        // Why not FromRGBA?
        public static Color FromNonPremultiplied(int r, int g, int b, int a) { return default(Color); }
        // Why not: FromVector4, also feels like FromVector3 is missing
        public static Color FromNonPremultiplied(Vector4 vector) { return default(Color); }

        // Why not Interpolate?
        public static Color Lerp(Color value1, Color value2, float amount) { return default(Color); }
        // No clue what does this do but whatever graphical represantation you can see you should be using Vector4 for computation and Color for representation
        public static Color Multiply(Color value, float scale) { return default(Color); 

Why not FromRGBA?

This method is not to construct a rgba color, it converts a non-premultipled alpha color to a color that contains premultiplied alpha. Refer docs on what's premultiplied alpha

Why not FromVector4, also feels like FromVector3 is missing

Same, these methods are to support alpha composting, it requires an A value, so no Vector3.

Why not Interpolate?

Lerp = linear interpolation, this is the quasi acronym for linear interpolation and widely used in other frameworks like unity, etc.

No clue what does this do but whatever graphical represantation you can see you should be using Vector4 for computation and Color for representation

This method can be used to lighten or darken a color by a given scale. Refer stackoverflow [http://stackoverflow.com/questions/12894605/how-to-make-specific-color-darken-or-lighten-based-on-value-in-wpf]

@Priya91
Copy link
Contributor Author

Priya91 commented Apr 29, 2015

@nathanaeljones @xanather : On adding int based types, based on analysis done by Experience and Insights, they said integer versions were not widely used, and can be added later if there are limitations without the int type. We want to keep the Core implementation concise.

@Priya91
Copy link
Contributor Author

Priya91 commented Apr 29, 2015

@patricksadowski :

Are there thoughts to add a method to create a color from r, g, b?

The color constructors can be used to create a color from r,g,b or vector3, vector4.

@dsplaisted
Copy link
Member

Integral Point/Size/Rect are quite useful as well. Should we make these generic or just create float/int variants as system.drawing does?

@nathanaeljones @xanather : On adding int based types, based on analysis done by Experience and Insights, they said integer versions were not widely used, and can be added later if there are limitations without the int type. We want to keep the Core implementation concise.

It wasn't exactly that the integer versions of Point, Size, and Rectangle aren't widely used. The System.Drawing drawing APIs support them so people naturally do use them.

Rather, we aren't sure whether integer versions of these types are necessary. Looking at more recent APIs than System.Drawing, we saw that out of WPF, WinRT, OxyPlot, and NGraphics, none of them had integer versions of these types. A single type to represent a Point is much less cumbersome than trying to find good names to disambiguate (ie Point/PointF), or using a generic type like Point<T> where T can only be int or float.

We're definitely interested in feedback on this. Are there scenarios where the float versions of the types wouldn't work well?

@patricksadowski
Copy link

@nathanaeljones

The point is that Light and Dark have completely different meanings depending on the color space you're talking about. And do you want 'amount' to be perceptually linear, as would be intuitive? Or mimic some gamma curve, as the naive implementation would?

The proposed Color type is still based on (s)RGB so the color space is fixed. As proposed Light and Dark use Lerp so the meaning of 'amount' should be defined there.

@Priya91
Sorry, I missed the constructors.

@MrJul
Copy link

MrJul commented Apr 30, 2015

Point.IsEmpty // what does it do?

This checks for if point is empty (0,0)

IMO, it makes sense for a Rect to be an empty, not for a point.
IsOrigin maybe? Or simply dropping that notion.

@ghost
Copy link

ghost commented Apr 30, 2015

👍 for @MrJul's idea of having IsOrigin instead.

@cdrnet
Copy link

cdrnet commented Apr 30, 2015

Should these types really be mutable?

@lilith
Copy link

lilith commented Apr 30, 2015

No, I'm hoping that was a typo. Mutable structs are a footgun.

@MrJul
Copy link

MrJul commented Apr 30, 2015

For information, System.Numerics.Vectors had immutable structures at first. Then setters were added with the following explanation:

Vector2f, Vector3f and Vector4f are now mutable. This is primarily done to match the shape of existing vector types, especially from the graphics domain.

Since we're still in the graphics domain, it's probably done on purpose.
I would prefer them to be immutable too, but there should be some consistency between core libraries.

@lilith
Copy link

lilith commented Apr 30, 2015

It wasn't exactly that the integer versions of Point, Size, and Rectangle aren't widely used. The System.Drawing drawing APIs support them so people naturally do use them.

Rather, we aren't sure whether integer versions of these types are necessary. Looking at more recent APIs than System.Drawing, we saw that out of WPF, WinRT, OxyPlot, and NGraphics, none of them had integer versions of these types. A single type to represent a Point is much less cumbersome than trying to find good names to disambiguate (ie Point/PointF), or using a generic type like Point where T can only be int or float.

We're definitely interested in feedback on this. Are there scenarios where the float versions of the types wouldn't work well?

Well, early versions of ImageResizer used floating point exclusively, only rounding at the System.Drawing API boundary. It turns out that this is an easy way to introduce floating-point bugs. Storing semantically integer data as floating point is much more problematic than it would seem, and we had more bugs than lines of code. Floating point is a bit more difficult to reason about than integers. You have non-comparable values like NaN and +/- infinity. Good luck trying to implement rounding or comparison that is consistent with an external language or library.

Debuggers are also the devil with floating point, as they will round values for display, hiding the .99999999999 that is actually in memory.

I'm concerned that providing only floating-point variants in Core will reduce the correctness of software and APIs that want to use Point/Size/Rect.

@lilith
Copy link

lilith commented Apr 30, 2015

I would prefer them to be immutable too, but there should be some consistency between core libraries.

Tuple is immutable. I'd wager it is more widely used than Vector2f.

@cdrnet
Copy link

cdrnet commented Apr 30, 2015

Related: with floating point, how close do X and Y need to be to zero for IsOrigin to be true?

@Priya91
Copy link
Contributor Author

Priya91 commented Apr 30, 2015

@MrJul @jasonwilliams200OK : Origin is (0,0) only in cartesian coordinate system, otherwise origin may be chosen as any point of reference.

@dsplaisted
Copy link
Member

@Priya91 Forgive my ignorance, but doesn't the Point type represent a point in Cartesian space? What other coordinate system could it represent?

@Priya91
Copy link
Contributor Author

Priya91 commented Apr 30, 2015

@dsplaisted I was thinking along the lines of origin being relative, as in my point of reference (x',y') can be different from (0,0), the point where x axis and y axis meet on the cartesian plane. I am not sure if the concept of origin in graphics always remains (0,0), or changes with the images modeled. I am fine with renaming Empty to Origin, and leave the context to the consumers of the library.

@ghost
Copy link

ghost commented Apr 30, 2015

Thanks for the explanation @Priya91.

I am fine with renaming Empty to Origin

Other options are:

  • remove this confusing property from the mix.
  • supplement it with a function public ConfigureOrigin<T>(params T coordinates) and make IsOrigin prop test against configured defaults; where the auto/factory configuration of instance could be: all coordinates are set to 0.

@govert
Copy link
Contributor

govert commented May 9, 2015

I also support the suggestion of @ebickle to work towards a legacy free geometry library, using the existing Vector2/3 types where appropriate.

Would https://github.com/dotnet/corefxlab be a location where such work can be hosted while under development? It would be nice to have an area where high churn, discussion and experimentation is acceptable, but which also indicates this is work intended towards incorporation into corefx in future

@stephentoub
Copy link
Member

Would https://github.com/dotnet/corefxlab be a location where such work can be hosted while under development?

Yes, I think so.

It wasn't stated initially, but one of the core scenarios that drove the creation of this work item is discussions with a fair number of (non-graphics) customers (e.g. financial) that have existing .NET applications that use these drawing types, just because they were convenient and approximately what they needed for their app. The lack of these basic types in the .NET Core surface area makes it that much more challenging for them to migrate to .NET Core, which they want to do for a variety of reasons, including cross-platform support. Making these basic primitives available largely as-is is then in my mind really about enabling these customers to move over, such that more existing code continues to "just work." It's not focused on real graphics workloads, for which such primitives are in-and-of-themselves insufficient.

My suggestion is two-fold:

  1. We bring the existing primitives (e.g. Point, PointF, Rectangle, RectangelF, etc.) as-is (mutability and all) into their own library. We can call it System.Drawing.Primitives, or we can call it something else if the name is concerning. This is very much like System.Collections.NonGeneric; we don't encourage new code to use the library, but it's available for code that wants to use it, and it eases porting.
  2. We (the community) start work in corefxlab on a new System.Graphics or System.Numerics.Geometry or some such project, using the proposals outlined in this issue as a starting point. I expect that @KrzysztofCwalina would be happy to help curate, as would I, but I'm expecting that right now this would be largely community driven.

I think we should re-focus this issue on (1), to make the existing primitives available; what exact existing types do we want to bring in, what do we want to call the library, etc.. Then we'll have a separate project/set of issues on corefxlabs for making progress on (2).

Reasonable?

@grokys
Copy link

grokys commented May 11, 2015

👍 now that the core scenario for the creation if these types has been explained, it's a bit clearer to me.

@KrzysztofCwalina
Copy link
Member

@stephentoub, corefxlab sounds like a fine place for dotnet/corefx#2. I would be more than happy to accept a PR that adds such library to corefxlab.

@akoeplinger
Copy link
Member

we don't encourage new code to use the library, but it's available for code that wants to use it, and it eases porting.

Sounds ok to me, as long as this is made undoubtedly clear in the description of the package and everywhere else.

@govert
Copy link
Contributor

govert commented May 11, 2015

Basically the only issue to discuss is then the assembly name (the code can presumably be taken from the .NET references source). I suggest System.Drawing.Compatibility.

But the motivation for adding these compatibility types sounds like a slippery slope to me.

Would porting to corefx not likely go well beyond six tiny struct types that are missing? And do the missing types have to be part of corefx, or would any compatible assembly on NuGet also satisfy the porting use-case?

I use the WPF types Point3D and Vector3D in namespace System.Windows.Media.Media3D as primitives in code that I might like to port to corefx. If I wanted them available too, would that be considered? Where would the line be drawn?

While the proposed set of types are small and the types simple, this issue and the interest in it highlights a lot of confusion around the corefx positioning.

@govert
Copy link
Contributor

govert commented May 11, 2015

I've added an issue to corefxlab for further discussion of the legacy-free direction: dotnet/corefxlab#86

@dsplaisted
Copy link
Member

I have some concerns about porting the existing System.Drawing primitive APIs to .NET Core as "legacy" with the intent of creating new legacy-free APIs.

The existing types are "good enough". So even if we mark them as legacy or obsolete, people will still use them for non-legacy code. This is especially true since porting the existing APIs is relatively easy so we can do that fairly quickly, while designing legacy-free APIs will take longer. So there will be a time when the legacy APIs are the only ones available, and the ecosystem will start to adopt them. I also think that even though there are pitfalls with mutable value types, there may be people who find mutable APIs easier to use and will thus prefer the legacy APIs.

Basically I think we risk a split where the libraries in the ecosystem don't agree on a set of exchange types for these concepts, hurting interoperability. I'd like more clarity on what the "legacy-free" APIs would look like before we move forward with porting the existing APIs as is. That would help us make a more informed decision.

A few other points:

  • As has been mentioned, these drawing / geometry primitives are applicable to a lot of scenarios. However, we're going to start looking at creating an image manipulation library for .NET Core. So I think that's the primary scenario that we'll be interested in with these geometry primitives, at least at first.
  • It seems like there's the impression that the corefx repo isn't appropriate for APIs that are experimental and undergoing a lot of churn. I agree with this for APIs where the entire concept is somewhat experimental. However, for APIs that we expect to eventually be part of CoreFX, but where we're not sure in exactly what form, I would keep them in CoreFX. We can iterate on them there for as long as we need to and mark the NuGet package as prerelease until they are done.

@lilith
Copy link

lilith commented May 21, 2015

However, we're going to start looking at creating an image manipulation library for .NET Core.

I have some tips; is there a place for related design discussions?

@dsplaisted
Copy link
Member

However, we're going to start looking at creating an image manipulation library for .NET Core.

I have some tips; is there a place for related design discussions?

There will be. It will probably be another GitHub issue. We will definitely be interested in feedback and expertise from you and the rest of the community.

@xoofx
Copy link
Member

xoofx commented May 24, 2015

Wow, it is a long thread with lots of in-depth discussion! I will try to give some feedback from a SharpDX/DirectX and gamedev perspective.

Point/Rectangle for integers

Point/Rectangle using integers are often used for interop scenarios:

  • For example, the struct Point (now RawPoint for interop) in SharpDX is used by various methods/datas in Direct2D1, a bit in DXGI
  • The struct Rectangle (now RawRectangle for interop) is used by almost all DirectX APIs
    • Note that the Rectangle used for interop is stored as (Left, Top, Right, Bottom) instead of (X,Y,Width,Height). Using Left,Top, Right, Bottom is also relevant for intersection checks, or point inclusions for example, as they avoid to perform any additions at comparison time (instead of the X+Width...etc.)
    • For interop, there is a version of (X,Y,Width,Height) that is used currently by WIC (Windows Imaging Component)
  • As someone pointed out, having integers versions is important when you don't want floats to mess the precision.

At the very beginning of SharpDX, these structs were used directly by using System.Drawing, but when Windows 8 came out and System.Drawing was no longer accessible, I had to remove this dependency (and not really happy to define this kind of basic interop types)

PointF vs Vector2

Mathematically the semantic of a point is quite different from a vector. A Length of a Point doesn't really make sense. The old System.Drawing.Point class was quite simple to provide a simple representation of a pixel on the screen. Though, in 3d graphics rendering, we often reuse the storage of a Vector3 for a position. In HLSL, they choose maybe a better semantic with float3, where this type doesn't have any methods attached (appart the slicing operator .xyz, xxy...etc.) but functions are provided on these types (length(myvector)... etc.)

Size (Size2. Size2F, Size3, Size3F)

This one has also a specific semantic (Width, Height) and obviously, you don't want to alienate it to the semantic of a Vector2 (you don't rotate a Size, you don't translate it... etc.)

Instead of plain Size, I would prefer to have Size2(int Width, int Height) and Size3(int Width, int Height, int Depth), as they are better matching usages with GPU functionality:

  • store the size of an image texture
  • A method to calculate the next mip-level (/2) or upscale (*2) of a size

Immutability

For the types discussed here (Size, Point, Plane...etc.), it is not much an issue having immutable types. They are barely modified in intensive loops. It is more a problem when you force immutability on types that are bigger and can be modified in batch on a particular component and you want to avoid the whole recreation of a struct (causing a bunch of copy on the stack, copy back on the heap...etc.) It it less an issue for small structs - 8 bytes-, it becomes a bit problematic for 16 bytes (Vector4...), is is absolutely a non-sense for large structs >= 64 bytes (Matrix...). Suppose you just want to apply a translation on a single axis to a matrix, you don't want to copy-back-and-forth 64 bytes just to modify 4 bytes.

For a common .NET developer, this discussion could sound a bit finicky, but when you start having a budget constraint of 16ms (60 FPS in games), you really care about these details.

Colors vs Geometry or Graphics

I agree that Color needs special crafting. There is a need to correctly handle:

  • different color formats (int8 bits per component, float 16 bits per component, float 32 bits per component, alpha or not...etc.): Having Color4F, Color3F, but also Color4 for a int 8 bits per component.
  • color spaces
  • gamma vs linear: you can manipulate colors as a vector as long as it is expressed in linear space. All calculations must be done in linear space, but for the sake of simplicity and storage performance, a color must not contain its space. It is up to the developer to know in which space he is working (load color, convert from gamma to linear, perform all calculations, convert back to gamma and store). Usually it is just a matter of calling a ToLinear (from sRGB space) or ToGamma (to sRGB space) method.

I don't have any particular incentive at keeping backward compatibility with System.Drawing. From a graphics developer point of view, I would prefer to have all these things into a single assembly because it is very common to work with both colors and geometry (e.g we can pass inside the geometry of a 3d model some colors for each vertices, a texture is a 2d or 3d rectangular region of memory that contains colors...etc.).

Though I would not mix Texture/Image manipulation with Color/Vector basic types and put all this code to a dedicated assembly as it can become pretty large. While Color/Vector are not platform dependent, you may want to use platform specific optimized API to perform image manipulation (like using WIC on Windows), though, in this case, you cannot guarantee the correctness of operations across platforms and it can become difficult (lots of libraries don't handle correctly gamma correct space...etc.), so It may still be relevant to have a platform independent library for texture/image manipulation.

@Priya91
Copy link
Contributor Author

Priya91 commented May 29, 2015

I agree with @stephentoub and @weshaggard to bring forth the existing drawing types from System.Drawing. These types are intended to be independent of any graphic stack (we have a separate issue tracking developing a geometry library for image processing scenarios dotnet\corefxlab#86). The only concern is with Color, existing color has a lot of knowncolor values, which gets rendered differently based on the graphics adapter, hence skipping these on .NET Core. For the other vector operations on color, like lerp, scaling, etc, we can define extension methods. Note that the proposed API does not have any new additions on Point, Size and Rectangle that is not there in the existing drawing types. If everyone agrees with bringing the existing types, Issues to discuss:

  1. Making the structures immutable
  2. Name of the assembly contract, for now, System.Drawing.Primitives
  3. Expose Int versions in the contract, for now, expose both int and float.

@xanather
Copy link

  1. I think they should be mutable though I know many will disprove this.
  2. System.Drawing.Primitives sounds good.
  3. Both Int and Float versions should exist (Point & PointF) in my opinion. Hopefully having both will allow more people to use these types rather than defining their own.

@Vargol
Copy link

Vargol commented Jun 2, 2015

I write lots of code for generative art and I modify colours, polygon vertices (points) and vectors all the time. If the structures were immutable I'd have to create tens or even hundreds of thousands of new structures per frame, potentially millions per second.

e.g.

 foreach (int element in bigarraywith10000000elements)
{
            var c = element.colour;
            // just how stupid does the next line look
            var newc = Color.FromArgb(c.A+1, c.R, c.G, c.B);
            element.colour = newc;
}

Of course I could have made it look in worse by doing

element.colour = Color.FromArgb(element.colour.A+1, element.colour.R, element.colour.G, element.colour.B);

just to reinforce that I'm having to create a new struct to increment the alpha of the existing one
cf

 foreach (int element in bigarraywith10000000elements)
{
            element.colour.A += 1;
}

You can imagine which would perform better too.
I'm voting for mutable !

@Priya91
Copy link
Contributor Author

Priya91 commented Jun 25, 2015

From the discussion, it is clear we need int and float based versions of the primitives, and comparing the proposed and already existing system.drawing types, there is not any difference in the api surface area except for the name changes. Hence it makes sense to expose the already existing System.Drawing primitives as is, that is, Point, PointF, Size, SizeF, Rectangle and RectangleF to .NET Core. We are not exposing Color at the moment because we feel the existing Color type in System.Drawing is insufficient as specified in the above conversations. Once the existing types are exposed, we will start a separate discussion on crafting Color primitive for .NET Core.

@richlander
Copy link
Member

LGTM @Priya91

This is close to what @xoofx is using with SharpDX. Right?

@Priya91
Copy link
Contributor Author

Priya91 commented Jun 25, 2015

@richlander : Yup, SharpDX has Point, Rectangle and RectangleF. There is no float version of Point. Point also has implicit and explicit conversions to Vector2, which we can provide via extension methods over Vector2 and Point in a separate Extensions contract as ToVector2(this point p) and ToPoint(this Vector2). The APIs are also similar between System.Drawing and SharpDX.

@terrajobst
Copy link
Member

We've reviewed this week and it looks good as proposed. Notes are available here.

@manu-st
Copy link

manu-st commented Sep 2, 2015

Is there somewhere where we can get the code while waiting for it to be included?

What about System.Drawing.Font, is this planned too?

@xanather
Copy link

Will these types still use the hardware acceleration (SIMD) underneath (where applicible), or do we need to use System.Numerics.Vectors directly if we want that?

@lilith
Copy link

lilith commented Nov 26, 2015

Has this been canceled? dotnet/corefxlab#86

@Priya91
Copy link
Contributor Author

Priya91 commented Jan 7, 2016

PR: dotnet/corefx#5223

@Priya91 Priya91 closed this as completed Jan 12, 2016
@ststeiger
Copy link

ststeiger commented May 2, 2017

Just wanted to add that a class like "Rectangle" should have properties TopLeft, TopRight & BottomLeft, BottomRight.
PdfSharp XRectrangle has them, and they are very useful when drawing a non-trivial graph.

Also, instead of having a standalone-class like Rectangle, Triangle, Trapezoid, Square, etc. it would be nicer to have a single more general/abstract class like GeneralizedNonOverlappingPolygonWithNCorners instead, with properties Midpoint, Barycenter , area and circumference. These aren't hard to code.
And then maybe you can have a class Rectangle & Triangle inherit from this.
That would be proper Geometry.
And then GeneralizedPolygon would inherit from an even-more abstract base-class like "Shape".

Something more like this:

CalculateArea(polygon)
CalculateCentroid(polygon)
CalculateMidPoint(polygon)
FindNearestLineEndIndex(point)

Implementation (ActionScript):

package BaseTypes
{
	import flash.display.Stage;
	import flash.display.Sprite;
	import flash.display.MovieClip;
	
	import flash.geom.Point;
	import flash.geom.Rectangle;
	
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.events.ContextMenuEvent;
	
	import flash.ui.ContextMenu;
	import flash.ui.ContextMenuItem;
	
	import Vectors.cVector_2d;
	import BaseTypes.*;
	import UserInterface.cColorPicker;
	
	// import BaseTypes.cPolygon;
	public class cPolygon extends Sprite
	{
		public static const COLOR_TRANSPARENT:Number = 0;
		public static const COLOR_SEMITRANSPARENT:Number = 0.5;
		public static const COLOR_SOLID:Number = 1.0;
		
		public static const POINTS_SHOW:Boolean = true;
		public static const POINTS_HIDE:Boolean = false;
		
		
		protected var pAddPolygonCallback:Object = null;
		protected var pRemovePolygonCallback:Object = null;
		//public var objRelated:Object = null;
		public var aptDefinitionPoints:Array = new Array();
		public var aspEdgePoints:Array = new Array();
		public var shPolygonShape:cSprite = new cSprite(this);
		public var objParentObject:Object = null;
		public var iColor:uint = 0xFF0000;
		public var bIsValid:Boolean = true;
		public var CurrentColorPicker:cColorPicker = null;
		public var CurrentMovingCircleSprite:cSprite;
		//public var strGUID:String = cGUID.create();
		
		protected var bMovePoint:Boolean = false;
		
		//Constructor
		public function cPolygon(pAddPolygonCallbackParameter:Object = null, pRemovePolygonCallbackParameter:Object = null) 
		{
			trace("cPolygon.Constructor");
			super();
			this.pAddPolygonCallback = pAddPolygonCallbackParameter;
			this.pRemovePolygonCallback = pRemovePolygonCallbackParameter;
		}  // End Constructor
		
		
		/*
		var apts:Array=new Array();
		var xx:Point=new Point();
		xx.x=10;
		xx.y=10;
		apts.push(xx);
		xx=new Point();
		xx.x=10;
		xx.y=100;
		apts.push(xx);
		xx=new Point();
		xx.x=50;
		xx.y=100;
		apts.push(xx);
		xx=new Point();
		xx.x=100;
		xx.y=10;
		apts.push(xx);
		//xx=new Point();
		//xx.x=10;
		//xx.y=10;
		//apts.push(xx);
		*/
		// instance.CreatePolygon(this,apts);
		public function CreatePolygon(objParentObjectParameter:Object, aptPoints:Array, iColorParameter:uint, bEnableEdit:Boolean=true, nAlpha:Number = COLOR_SEMITRANSPARENT, bShowPoints:Boolean=POINTS_SHOW):Sprite
		{
			this.objParentObject = objParentObjectParameter;
			this.aptDefinitionPoints = aptPoints;
			if(aptPoints == null)
			{
				trace("NULL array...");
				aptDefinitionPoints = new Array();
			}
			this.iColor = iColorParameter;
			
			shPolygonShape.alpha = nAlpha;
			shPolygonShape.graphics.lineStyle(1,0x000000)
			shPolygonShape.graphics.beginFill(iColor);
			var i:uint;
			var bIsFirst:Boolean = true;
			for(i=0; i < aptDefinitionPoints.length;++i)
			{
				
				if(!aptDefinitionPoints[i].bCurrentlyValid)
					continue;
					
				if(bIsFirst)
				{
					bIsFirst=false;
					shPolygonShape.graphics.moveTo(aptDefinitionPoints[i].x, aptDefinitionPoints[i].y);
				}
				else
				{
					shPolygonShape.graphics.lineTo(aptDefinitionPoints[i].x, aptDefinitionPoints[i].y);
				}
			}
			shPolygonShape.graphics.endFill();
			
			//------------------------------------------------------
			var cmAreaContextMenu:ContextMenu = new ContextMenu();
			cmAreaContextMenu.hideBuiltInItems(); // hide items like Zoom, Play, Loop etc
			
			var cmiItem0:ContextMenuItem = new ContextMenuItem("Farbe ändern");
			cmiItem0.addEventListener(ContextMenuEvent.MENU_ITEM_SELECT, ChangeColorEvent);
			cmAreaContextMenu.customItems.push(cmiItem0);
			var cmiItem0a:ContextMenuItem = new ContextMenuItem("Punkt hinzufügen");
			cmiItem0a.addEventListener(ContextMenuEvent.MENU_ITEM_SELECT, AddPointEvent);
			cmAreaContextMenu.customItems.push(cmiItem0a);
			
			var cmiItem0b:ContextMenuItem = new ContextMenuItem("Polygon zum Plan hinzufügen");
			cmiItem0b.addEventListener(ContextMenuEvent.MENU_ITEM_SELECT, AddPolygonToPlan);
			cmAreaContextMenu.customItems.push(cmiItem0b);
			
			var cmiItem0c:ContextMenuItem = new ContextMenuItem("Polygon entfernen");
			cmiItem0c.addEventListener(ContextMenuEvent.MENU_ITEM_SELECT, RemovePolygonEvent);
			cmAreaContextMenu.customItems.push(cmiItem0c);
			
			
			if(bEnableEdit)
				shPolygonShape.contextMenu = cmAreaContextMenu;
			
			//------------------------------------------------------
			if(objParentObject != null)
				objParentObject.addChild(shPolygonShape);
			
			var cmPointContextMenu:ContextMenu = new ContextMenu();
			cmPointContextMenu.hideBuiltInItems();
			
			var cmiItem1:ContextMenuItem = new ContextMenuItem("Punkt löschen");
			cmiItem1.addEventListener(ContextMenuEvent.MENU_ITEM_SELECT, RemovePoint);
			cmPointContextMenu.customItems.push(cmiItem1);
			
			var cmiItem2:ContextMenuItem = new ContextMenuItem("Punkt verschieben");
			cmiItem2.addEventListener(ContextMenuEvent.MENU_ITEM_SELECT, MovePointInitEvent);
			cmPointContextMenu.customItems.push(cmiItem2);
			
			if(bEnableEdit && bShowPoints)
			{
				for(i=0; i < aptDefinitionPoints.length;++i)
				{
					if(!aptDefinitionPoints[i].bCurrentlyValid)
					{
						continue;
					}
					var CircleSprite:cSprite = cDrawTools.CreateCircleSprite(objParentObject, aptDefinitionPoints[i].x, aptDefinitionPoints[i].y, cGlobals.nPointSize, 0x000000);
					CircleSprite.iIndex = i;
					
					CircleSprite.objSpritePolygonParent = this;
					CircleSprite.contextMenu = cmPointContextMenu;
					
					
					CircleSprite.addEventListener(MouseEvent.CLICK,   onMouseClickEvent);
					
					CircleSprite.addEventListener(MouseEvent.MOUSE_UP,   onMouseUpEvent);
					CircleSprite.addEventListener(MouseEvent.MOUSE_DOWN, onMouseDownEvent);
					aspEdgePoints.push(CircleSprite);
				}
			}
			return shPolygonShape;
		} // End function CreatePolygon	
		
		
		public override function set contextMenu(cmThisContextMenu:ContextMenu):void
		{
			this.shPolygonShape.contextMenu = cmThisContextMenu;
		}
		
		
		public override function get contextMenu():ContextMenu
		{
			return this.shPolygonShape.contextMenu ;
		}
		
		
		public function onMouseDownEvent(event:MouseEvent):void
		{
			this.CurrentMovingCircleSprite=cSprite(event.target);
			this.objParentObject.addEventListener(MouseEvent.MOUSE_MOVE, MovePointEvent);
			//cGlobals.mcRootObject.addEventListener(MouseEvent.MOUSE_MOVE, MovePointEvent);
		}
		
		
		// Finish polygon when click on existing point.
		public function onMouseClickEvent(event:MouseEvent):void
		{
			//this.CurrentMovingCircleSprite = null;
			trace("click");
			
			 var xxx:cSprite = cSprite(event.target);
			 trace(xxx.objSpritePolygonParent);
			
			 var yyy:cPolygon = cPolygon(xxx.objSpritePolygonParent);
			 
			 if(yyy.pAddPolygonCallback != null)
				yyy.pAddPolygonCallback(this);
			 
			 
			event.stopImmediatePropagation();
			event.updateAfterEvent();
			//cGlobals.mcRootObject.removeEventListener(MouseEvent.MOUSE_MOVE, MovePointEvent);
		}
			
		
		public function onMouseUpEvent(event:MouseEvent):void
		{
			this.CurrentMovingCircleSprite = null;
			this.objParentObject.removeEventListener(MouseEvent.MOUSE_MOVE, MovePointEvent);
			//cGlobals.mcRootObject.removeEventListener(MouseEvent.MOUSE_MOVE, MovePointEvent);
		}
		
		
		// RemovePolygon
		public function RemovePolygonEvent(event:ContextMenuEvent):void
		{
			if(this.pRemovePolygonCallback != null)
				this.pRemovePolygonCallback(this);
		}
		
		
		public function AddPolygonToPlan(event:ContextMenuEvent):void
		{
			if(this.pAddPolygonCallback != null)
				this.pAddPolygonCallback(this);
			/*
			var cpThisPolygon:cPolygon=new cPolygon();
			trace("cGlobals.ccPlanZoomView.x: " + cGlobals.ccPlanZoomView.x);
			trace("cGlobals.ccPlanZoomView.y: " + cGlobals.ccPlanZoomView.y);
			
			var i:uint;
			var aNewCoordinates:Array=new Array();
			var cptMyPoint:cPoint;
			for(i=0; i < this.aptDefinitionPoints.length;++i)
			{
				cptMyPoint = new cPoint();
				
				var ptTempPoint:Point= new Point(this.aptDefinitionPoints[i].x, this.aptDefinitionPoints[i].y);
				ptTempPoint = cGlobals.ccPlanZoomView.globalToLocal(ptTempPoint);
				cptMyPoint.x = ptTempPoint.x;
				cptMyPoint.y = ptTempPoint.y;
				ptTempPoint = null;
				
				//cptMyPoint.x=this.aptDefinitionPoints[i].x-cGlobals.ccPlanZoomView.x; // *cGlobals.ccPlanZoomView.scaleX;
				//cptMyPoint.y=this.aptDefinitionPoints[i].y-cGlobals.ccPlanZoomView.y; // *cGlobals.ccPlanZoomView.scaleY;
				cptMyPoint.bCurrentlyValid = this.aptDefinitionPoints[i].bCurrentlyValid;
				aNewCoordinates.push(cptMyPoint);
			}
			
			cpThisPolygon.CreatePolygon(cGlobals.ccPlanZoomView, aNewCoordinates, this.iColor, false);
			//cGlobals.aPolygonsOnPlan.push(cpThisPolygon);
			this.RemovePolygon();
			*/
		}
		
		
		public function ChangeColorEvent(event:ContextMenuEvent):void
		{
			var ptGlobalMousePosition:Point = new Point(event.mouseTarget.mouseX,  event.mouseTarget.mouseY);
			ptGlobalMousePosition = event.mouseTarget.localToGlobal(ptGlobalMousePosition);

			if(CurrentColorPicker == null)
				CurrentColorPicker = new cColorPicker(objParentObject,ptGlobalMousePosition.x,ptGlobalMousePosition.y, 0xFF0000, this);
			else
			{
				CurrentColorPicker.RemoveColorPicker();
				CurrentColorPicker = null;
				CurrentColorPicker = new cColorPicker(objParentObject,ptGlobalMousePosition.x,ptGlobalMousePosition.y, 0xFF0000, this);
			}
			CurrentColorPicker.cpColorPicker.open();
		}
		
		
		public function MovePointInitEvent(e:ContextMenuEvent):void
		{
			bMovePoint = true;
			this.parent.addEventListener(MouseEvent.MOUSE_MOVE, MovePointEvent);
			//cGlobals.ccPlanZoomView.addEventListener(MouseEvent.MOUSE_MOVE, MovePointEvent);
			//mcMenuItem.addEventListener(MouseEvent.CLICK, onIconClick);
		}
		
		
		public function MovePointEvent(event:MouseEvent):void
		{
			trace("Moving point " + this.CurrentMovingCircleSprite.iIndex);
			var ptGlobalMousePosition:Point = new Point(event.target.mouseX,  event.target.mouseY);
			ptGlobalMousePosition = event.target.localToGlobal(ptGlobalMousePosition)
			//CurrentMovingCircleSprite.x = ptGlobalMousePosition.x;
			//CurrentMovingCircleSprite.y = ptGlobalMousePosition.y;
			this.aptDefinitionPoints[this.CurrentMovingCircleSprite.iIndex].x=ptGlobalMousePosition.x;
			this.aptDefinitionPoints[this.CurrentMovingCircleSprite.iIndex].y=ptGlobalMousePosition.y;
			Redraw(this.aptDefinitionPoints);
		}
		
		
		public function RemovePoint(e:ContextMenuEvent):void
		{
			trace("cPolygon.RemovePoint");
			if(objParentObject!=null)
			{
				objParentObject.removeChild(shPolygonShape);
			}
			shPolygonShape = null;
			shPolygonShape = new cSprite(this);
			
						
			var i:uint;
			for(i=0; i <aspEdgePoints.length;++i)
			{
				if(aspEdgePoints[i] != null)
					objParentObject.removeChild(aspEdgePoints[i]);
			}
			
			
			for(i=0; i < aptDefinitionPoints.length;++i)
			{
				if(aptDefinitionPoints[i].x == Object(e.contextMenuOwner).ptOrigin.x && aptDefinitionPoints[i].y == Object(e.contextMenuOwner).ptOrigin.y)
				{
					trace("Invalidating index: " + i);
					aptDefinitionPoints[i].bCurrentlyValid = false;
					//trace("Splicing at index: "+i);
					//aptDefinitionPoints.splice(i, 1); // splice = RemoveArrayItem(at index, for length)
					break;
				}
			}
			
			aspEdgePoints = null;
			aspEdgePoints = new Array();
			
			
			CreatePolygon(objParentObject, aptDefinitionPoints, this.iColor);
			//objParentObject.removeChild(e.contextMenuOwner);
			//e.mouseTarget.objRelated.x=100;
			//e.mouseTarget.x=100;
			//e.currentTarget.x=100;
		}
		
		
		public function FindNearestLineEndIndex(cptPointToAdd:cPoint):uint
		{
			if(aptDefinitionPoints.length==0)
			{
				return 0;
			}
			
			if(aptDefinitionPoints.length>1)
			{
				var nOldDistance:Number = 1000000;
				var iOldIndex:uint      = 0;
				var nDistance:Number    = 0;
				var vec2_LineStart:cVector_2d   = null;
				var vec2_LineEnd:cVector_2d     = null;
				var vec2_Point:cVector_2d       = null;
				var vec2_VecLine:cVector_2d     = null;
				var cptIntersectionPoint:cPoint = null;
				
				
				var bHasFirst:Boolean = false;
				var bHasLast:Boolean  = true;
				var iFirst:uint = 0;
				var iLast:uint  = 0;
				
				for(var i:uint = 0; i < aptDefinitionPoints.length; ++i)
				{
					if(aptDefinitionPoints[i].bCurrentlyValid)
					{
						if(bHasFirst == false)
						{
							bHasFirst = true;
							iFirst = i;
							iLast  = i;
							continue;
						}
						bHasLast = true;
						
						vec2_LineStart = cVector_2d.MakeVector(aptDefinitionPoints[iLast].x,aptDefinitionPoints[iLast].y);
						//trace("vec2_LineStart: " + vec2_LineStart.toString() );
						vec2_LineEnd = cVector_2d.MakeVector(aptDefinitionPoints[i].x,aptDefinitionPoints[i].y);
						//trace("vec2_LineEnd: " + vec2_LineEnd.toString() );
						vec2_Point = cVector_2d.MakeVector(cptPointToAdd.x,cptPointToAdd.y);
						//trace("vec2_Point: " + vec2_Point.toString() );
						nDistance = cVector_2d.DistanceOfPointToLine(vec2_Point, vec2_LineStart, vec2_LineEnd);
						
						vec2_VecLine = cVector_2d.VectorSubtract(vec2_LineStart,vec2_LineEnd);
						
						if(nDistance < nOldDistance)
						{
							cptIntersectionPoint = cVector_2d.GetPointVerticalIntersection(aptDefinitionPoints[i], vec2_VecLine, cptPointToAdd);
							if(cptIntersectionPoint.bHasInterSection)
							{
								//trace("Has intersection");
								if (cVector_2d.isPointOnLine(aptDefinitionPoints[i], aptDefinitionPoints[iLast], cptIntersectionPoint)  )
								{
									//trace("is on line");
									nOldDistance = nDistance;
									iOldIndex = i;
								}
								 // else
								//      trace("is not on line.");
							}
							 // else
							//      trace("has no intersection");
						}
					
						// trace("Length: " + aptDefinitionPoints.length);
						// trace("Pair["+i+"]: " + aptDefinitionPoints[iLast].toString() + " ; "+aptDefinitionPoints[i].toString() + "Distance: " + nDistance);
						// trace("Dist: " + nDistance );
						
						iLast = i;
					}// End isvalid
				}// End for
				
				
				if(bHasLast)
				{
					trace("Has Last...");
					vec2_LineStart = cVector_2d.MakeVector(aptDefinitionPoints[iLast ].x, aptDefinitionPoints[iLast].y);
					vec2_LineEnd   = cVector_2d.MakeVector(aptDefinitionPoints[iFirst].x, aptDefinitionPoints[iFirst].y);
					vec2_Point     = cVector_2d.MakeVector(cptPointToAdd.x, cptPointToAdd.y);
					nDistance      = cVector_2d.DistanceOfPointToLine(vec2_Point, vec2_LineStart, vec2_LineEnd);
					vec2_VecLine   = cVector_2d.VectorSubtract(vec2_LineStart,vec2_LineEnd);
					//trace("Final Pair: " + aptDefinitionPoints[iFirst].toString()+" ; " + aptDefinitionPoints[iLast].toString() + "Distance: " + nDistance);
				
					if(nDistance < nOldDistance)
					{
						// nOldDistance = nDistance;
						// iOldIndex = aptDefinitionPoints.length;
						cptIntersectionPoint = cVector_2d.GetPointVerticalIntersection(aptDefinitionPoints[iLast], vec2_VecLine, cptPointToAdd);
						if(cptIntersectionPoint.bHasInterSection)
						{
							//trace("Is point on this line? "+ aptDefinitionPoints[iLast].toString()+", "+  aptDefinitionPoints[iFirst].toString() );
							if (cVector_2d.isPointOnLine(aptDefinitionPoints[iLast], aptDefinitionPoints[iFirst], cptIntersectionPoint)  )
							{
								//trace("isonline");
								nOldDistance = nDistance;
								iOldIndex = iLast + 1; //aptDefinitionPoints.length;
							}
							//else
							//	trace("is not on line.");
							
						}
						//else
						//	trace("has not");
						
					} // End if(nDistance<nOldDistance)
					
					//trace("Selected pair: "+iOldIndex);
					return iOldIndex;
				}// End if(bHasLast)
				else
				{
					//trace("Selected pair: 1");
					return 1;
				}
			}
			else
				return 1;
			
			return aptDefinitionPoints.length;
		}
		
		
		public function AddPointEvent(event:ContextMenuEvent):void
		{
			//trace("Xmouse: "+ Object(event.mouseTarget).mouseX);
			
			var ptGlobalMousePosition:Point = new Point(event.mouseTarget.mouseX, event.mouseTarget.mouseY);
			ptGlobalMousePosition = event.mouseTarget.localToGlobal(Point(ptGlobalMousePosition));
			
			var cptPointToAdd:cPoint = new cPoint();
			cptPointToAdd.x = ptGlobalMousePosition.x;
			cptPointToAdd.y = ptGlobalMousePosition.y;
			//trace("isvalid? " + cptPointToAdd.bCurrentlyValid);
			//trace("New point: " + cptPointToAdd.toString());
			AddPoint(cptPointToAdd, FindNearestLineEndIndex(cptPointToAdd) );
		}
		
		
		public function AddPoint(ptPoint:cPoint , iIndex:uint):void
		{  
			trace("cPolygon.AddPoint");
			if(objParentObject != null)
				objParentObject.removeChild(shPolygonShape);
			
			shPolygonShape = null;
			shPolygonShape = new cSprite(this);
			
			
			var i:uint;
			for(i=0; i < aspEdgePoints.length;++i)
			{
				if(aspEdgePoints[i] != null)
					objParentObject.removeChild(aspEdgePoints[i]);
			}
			
			aptDefinitionPoints.splice(iIndex,0, ptPoint); // insert
			//aptDefinitionPoints.push(ptPoint);
			aspEdgePoints = null;
			aspEdgePoints = new Array();
			
			
			CreatePolygon(this.objParentObject, this.aptDefinitionPoints, this.iColor);
			//objParentObject.removeChild(e.contextMenuOwner);
			//e.mouseTarget.objRelated.x=100;
			//e.mouseTarget.x=100;
			//e.currentTarget.x=100;
		}
		
		
		public function Redraw(aptPoints:Array):void
		{  
			trace("cPolygon.Redraw");
			if(this.objParentObject != null)
				this.objParentObject.removeChild(shPolygonShape);
			
			this.shPolygonShape = null;
			this.shPolygonShape = new cSprite(this);
			
			
			var i:uint;
			for(i=0; i < this.aspEdgePoints.length;++i)
			{
				if(this.aspEdgePoints[i] != null)
					this.objParentObject.removeChild(this.aspEdgePoints[i]);
			}
			
			this.aptDefinitionPoints = null;
			this.aspEdgePoints = null;
			this.aspEdgePoints = new Array();
			
			
			CreatePolygon(this.objParentObject, aptPoints, this.iColor);
			//objParentObject.removeChild(e.contextMenuOwner);
			//e.mouseTarget.objRelated.x=100;
			//e.mouseTarget.x=100;
			//e.currentTarget.x=100;
		}
		
		
		public function RemovePolygon():void
		{  
			trace("cPolygon.RemovePolygon");
			this.bIsValid = false;
			
			if(this.objParentObject != null)
				this.objParentObject.removeChild(shPolygonShape);
			
			this.shPolygonShape = null;
			
						
			var i:uint;
			for(i=0; i < this.aspEdgePoints.length;++i)
			{
				if(this.aspEdgePoints[i] != null)
					this.objParentObject.removeChild(this.aspEdgePoints[i]);
			}
			
			this.aptDefinitionPoints = null;
			this.aspEdgePoints = null;
		}
		
		
		// instance.CalculateMathematicalArea();
		protected function CalculateMathematicalArea():Number
		{  
			trace("cPolygon.CalculateMathematicalArea");
			
			var nArea:Number =0;
			
			var i:uint;
			for(i=0; i < this.aptDefinitionPoints.length; ++i)
			{
				if(this.aptDefinitionPoints[i] != null)
				{
					if(!aptDefinitionPoints[i].bCurrentlyValid)
						continue;
					
					
					
					if(i!=this.aptDefinitionPoints.length-1)
					{
						nArea += aptDefinitionPoints[i].x*aptDefinitionPoints[i+1].y-aptDefinitionPoints[i+1].x*aptDefinitionPoints[i].y;
					}
					else
					{
						nArea += aptDefinitionPoints[i].x*aptDefinitionPoints[0].y-aptDefinitionPoints[0].x*aptDefinitionPoints[i].y;
					}
					
				} // End if(this.aptDefinitionPoints[i] != null)
			} // End for aptDefinitionPoints
			
			nArea *= 0.5;
			return nArea;
		} // End function cPolygon.CalculateMathematicalArea
		
		
		// instance.CalculateArea();
		public function CalculateArea():Number
		{
			trace("cPolygon.CalculateArea");
			var nArea:Number = Math.abs(this.CalculateMathematicalArea()) ;
			trace("Physical area: " + nArea);
			return nArea;
		} // End function cPolygon.CalculateArea
		
		
		//var ptCentroid:Point = cpPolygon.CalculateCentroid();
		public function CalculateCentroid():Point
		{  
			trace("cPolygon.CalculateCentroid");
			
			var nCentroidX:Number = 0;
			var nCentroidY:Number = 0;
			
			var iFirst:uint = 0;
			var bFirst:Boolean = true;
			var i:uint;
			for(i=0; i < this.aptDefinitionPoints.length; ++i)
			{
				if(this.aptDefinitionPoints[i] != null)
				{
					if(!aptDefinitionPoints[i].bCurrentlyValid)
						continue;
					
					if(bFirst)
					{
						bFirst = false;
						iFirst = i;
					}
					
					
					if( i != this.aptDefinitionPoints.length - 1)
					{
						nCentroidX += (aptDefinitionPoints[i].x + aptDefinitionPoints[i+1].x) * (aptDefinitionPoints[i].x * aptDefinitionPoints[i+1].y - aptDefinitionPoints[i+1].x * aptDefinitionPoints[i].y);
						nCentroidY += (aptDefinitionPoints[i].y + aptDefinitionPoints[i+1].y) * (aptDefinitionPoints[i].x * aptDefinitionPoints[i+1].y - aptDefinitionPoints[i+1].x * aptDefinitionPoints[i].y);
					}
					else
					{
						nCentroidX += (aptDefinitionPoints[i].x + aptDefinitionPoints[iFirst].x) * (aptDefinitionPoints[i].x * aptDefinitionPoints[iFirst].y - aptDefinitionPoints[iFirst].x * aptDefinitionPoints[i].y);
						nCentroidY += (aptDefinitionPoints[i].y + aptDefinitionPoints[iFirst].y) * (aptDefinitionPoints[i].x * aptDefinitionPoints[iFirst].y - aptDefinitionPoints[iFirst].x * aptDefinitionPoints[i].y);
					}
					
				} // End if(this.aptDefinitionPoints[i] != null)
				
				
			} // End for aptDefinitionPoints
			
			var nArea:Number = this.CalculateMathematicalArea();
			nCentroidX *= 1/(6 * nArea);
			nCentroidY *= 1/(6 * nArea);
			var ptCentroid:Point = new Point(nCentroidX, nCentroidY);
			//trace("Centroid (x,y): " + ptCentroid.toString() );
			return ptCentroid;
		} // End function cPolygon.CalculateCentroidX
		
		
		//var ptMidPoint:Point = cpPolygon.CalculateMidPoint();
		public function CalculateMidPoint():Point
		{  
			trace("cPolygon.CalculateMidPoint");
			
			var nX:Number =0;
			var nY:Number =0;
			
			var j:uint = 0;
			var i:uint;
			for(i=0; i < this.aptDefinitionPoints.length; ++i)
			{
				if(this.aptDefinitionPoints[i] != null)
				{
					if(!aptDefinitionPoints[i].bCurrentlyValid)
						continue;
					
					nX += aptDefinitionPoints[i].x;
					nY += aptDefinitionPoints[i].y;
					j += 1;
				} // End if(this.aptDefinitionPoints[i] != null)
			} // End for aptDefinitionPoints
			
			nX/=j;
			nY/=j;
			var ptReturnValue:Point = new Point(nX, nY);
			return ptReturnValue;
		} // End function cPolygon.CalculateMidPoint()
		
		
		// instance.ContainsPoint(x,y);
		public function ContainsPoint(ptPointInQuestion:Point):Boolean
		{
			if( this.shPolygonShape.hitTestPoint(ptPointInQuestion.x, ptPointInQuestion.y, true) )
				return true;
			return false;
		}
		
		
		// instance.ContainsXY(x,y);
		public function ContainsXY(xc:Number, yc:Number):Boolean
		{
			if( this.shPolygonShape.hitTestPoint(xc,yc,true) )
				return true;
			return false;
		}
		
		
	} // End cDrawTools 
	
} // End Package

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-needs-work API needs work before it is approved, it is NOT ready for implementation area-System.Drawing
Projects
None yet
Development

No branches or pull requests