Issue: dotnet/runtime#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:
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();
}
}
/cc @joshfree @dsplaisted @terrajobst @KrzysztofCwalina
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.
@justinvp : This is just a speclet, i'll fix these names in the actual PR, that will be committed.
Integral Point/Size/Rect are quite useful as well. Should we make these generic or just create float/int variants as system.drawing does?
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.
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.
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.
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?
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?
@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?
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.
@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]
@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.
@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.
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?
@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.
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.
:+1: for @MrJul's idea of having IsOrigin instead.
Should these types really be mutable?
No, I'm hoping that was a typo. Mutable structs are a footgun.
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.
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.
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.
Related: with floating point, how close do X and Y need to be to zero for IsOrigin to be true?
@MrJul @jasonwilliams200OK : Origin is (0,0) only in cartesian coordinate system, otherwise origin may be chosen as any point of reference.
@Priya91 Forgive my ignorance, but doesn't the Point type represent a point in Cartesian space? What other coordinate system could it represent?
@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.
Thanks for the explanation @Priya91.
I am fine with renaming Empty to Origin
Other options are:
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.@jasonwilliams200OK Why does Empty sound confusing, this has been in the System.Drawing space and I would think this was already being used and well known of. And the concept of origin makes sense for a data type like Plane and not a Point. Any point can be origin (point of reference) on a plane, and we name point (0,0) as special and call it empty. If there is to be a property Origin, then it should exist on a type like Plane.
Empty means an absence of data. For a collection or a nullable type, Empty is appropriate. For a number, Empty doesn't make sense. Zero is a value like any other.
Why not call 0,0... zero? IsEmpty -> IsZero. You might add Point.Zero as a static instance of 0,0, like IntPtr.Zero.
@nathanaeljones : Zero sounds good
It would help a lot if the intended use and some scenarios for these primitives were discussed. Is this just open-sourcing parts of System.Drawing, or a new library to be grown and developed?
It is not clear that adding such primitives to the framework now would assist in any type of consolidation in the future. New primitives can't replace System.Drawing (GDI+/WinForms), or WPF or SharpDX / XNA / MonoGame / Unity or the Win2D library for WinApps, all of which have some form of Point and Vector etc. and are used in .NET code already. They're all flawed (or blessed) from being tied to some external technology, so can't be 'the one' set of graphics primitives, but adding a new API just adds one more to this list, with no clear motivation.
If there must be some set of primitives in corefx blessed as the standard for cross-platform libraries, my suggestion would be to pick one of the existing APIs, and provide an implementation that exactly matches the existing API. System.Drawing (GDI+/WinForms) has the longest track record, has a partial implementation in Mono, and uses floats. Or pick from the XNA / SharpDX / MonoGame / Unity primitive types (and work towards the corefx types to be exactly those used in these libraries).
Whatever choices are made, corefx doesn't seem like the place to design the next generation drawing and geometry API.
I agree with @govert and wonder why we wouldn't just use the already existing System.Drawing APIs even if we do pull them into another library. We could make that library compatible the full .NET Framework and also be a standalone library for .NET Core. If we don't reuse those APIs the biggest question I have is how will people rationalize with all the other existing APIs. I really don't believe creating yet another set of these is the right direction.
I agree with @govert too. Some libraries do make use of System.Drawing instead of defining their own data types (i.e. OpenTK) and would help those libraries support .NET Core out of the box.
Maybe some of the math related functions in XNA's Rectangle/Vector/Point can be applied to these types too as extension/additional API to encourage users to use these ones rather than redefining their own set.
@govert @weshaggard @xanather I think you're missing the point. There will be no System.Drawing, XNA, WinForms, GDI+, WPF, SharpDX, or Win2D on CoreCLR, as all those depend on windows-only APIs.
We're not creating _yet another_ set of primitives for an existing platform - we're trying to head off proliferation on a newly created one. If there is eventual consolidation on .NET full, that's just icing.
Think of these in the context of server-side applications; web apps, imaging services, etc. That's the focus of CoreCLR, and should drive design goals. So yes, semantic parity with CSS is important for Color, and no, real-time rendering may not be the primary use case for Point. Correctness should be king.
As the community eventually replaces bits of System.Drawing, we want to give them interoperable primitives with which to do so.
Looking around a bit, I did find the following:
System.Numerics. These include Float-based Vector2/3/4 / Plane / Quaternion and Matrix3x2/4x4 types, with relevant Transform methods etc.System.Numerics types are also the WinRT projection types in .NET for the Windows.Foundation.Numerics types, so interop with Windows will already use these types as the graphics primitives.System.Numerics.Vector2 will already be used in the role under discussion here for a point type, as primitive interop types to represent a pair of floats in a drawing context.Point (not really used by Win2D), Rect, and Size. These project to the Windows.Foundation namespace under .NET 4.6. They're presumably not used from corefx at the moment, though they appear in the MetaData mapping table here: https://github.com/dotnet/corefx/blob/7116584186f8f3a886616aaf8cb5d4a982c60e27/src/System.Reflection.Metadata/src/System/Reflection/Metadata/MetadataReader.WinMD.csI think it would then make more sense to add implementations in corefx that are compatible with the .NET types projected from Windows.Foundation for Point, Rect and Size. This might be in a different namespace, but should be signature-compatible to reduce #ifdefs. The aims would be that a bit more code targeting Win2D could in future become platform independent.
The motivating example of allowing OpenTK to target corefx more easily seems quite weak - it uses int-based System.Drawing classes like Point, Size and Rect in additional to PointF, SizeF etc. but already reimplements those in the code too. Since OpenTK is a wrapper aroujnd OpenGL, there are conversions to OpenGL types, to Mac NSPoint etc. The System.Drawing use seems mostly in the context of dealing with a WinForms host. It's kind of at the level of Bitmap and Font classes, which corefx does not intend to provide at this stage.
So my suggestion is to not create new drawing primitives in corefx, and to bless the existing System.Numerics.Vector2/3 etc. as the interop types for these use cases going forward. If Rect, Size and Color are to be added to corefx, they should be signature-compatible with the WinRT projections from Windows.Foundation as would already be used from .NET 4.6, or anything using Win2D. This would target the API design at the most likely forward-looking use case.
@govert I would have nothing against making types compatible with Windows.Foundation -- if nothing else was sacrificed. I do challenge a few of your assumptions.
The aims would be that a bit more code targeting Win2D could in future become platform independent.
This is not happening. Platform-specific graphics APIs of that scope are not portable, ever. Been there, done that with mono System.Drawing. Tens of thousands of man-hours and we've barely started.
The System.Drawing use seems mostly in the context of dealing with a WinForms host. It's kind of at the level of Bitmap and Font classes, which corefx does not intend to provide at this stage.
Every ASP.NET CMS I have worked with (and I have worked with dozens) depends on System.Drawing. We're talking at least 80% of public ASP.NET websites here.
We want future OSS libraries to be as xplat and framework-agnostic as possible. This means lowering the barrier for them to drop System.Drawing & friends as dependencies. Point, PointF, Size, SizeF, Rect, RectF, and Color are very pragmatic places to start. It's quite reasonable to specifically target replacement of System.Drawing types, given its ubiquitous use in web apps.
Size and Rect offer semantic clarity that Vector* types can't represent well. Vector* is also a mutable struct, optimized for performance.
Immutable structs for both integer and floating-point variants are still a compelling proposal to me.
Think of these in the context of server-side applications; web apps, imaging services, etc. That's the focus of CoreCLR, and should drive design goals.
I want to try and clear up this statement as CoreCLR/CoreFx is not only focused on server-side applications. Sure it is one of it usages with ASP.Net vNext but it will also be used by client applications so the goal of corefx is to try and provide a general framework for numerous scenarios not just server side.
My earlier point is less about picking System.Drawing and more about not trying to provide yet another set of these types. I think we should pick one or more of the existing ones (i.e System.Drawing, System.Numerics, etc) as opposed to creating another set of APIs. I actually hope we can provide these without ties to a particular OS graphic stack such that they can be x-plat and be general for everyone even if the APIs do match one of the existing OS GUI stacks today.
As other commenters have said - these structs _should not_ be mutable. When it is so widely agreed that mutable structs are bad, they should not be added to corefx!
I'd like to see Integer Versions of Everything. They allow for pixel-Level manual math to be easily reasoned about.
And yes, Everything immutable, please. I know this might irritate for some usages of Vector3f etc that people are used to (esp. those coming from gpu shader programing), but it's still the right thing to do.
I agree with @weshaggard's top-level goal here: we should avoid creating yet another set of primitives when the core ones we have seem to be "good enough."
@nathanaeljones, you mentioned "Every ASP.NET CMS I have worked with (and I have worked with dozens) depends on System.Drawing" and "Point, PointF, Size, SizeF, Rect, RectF, and Color are very pragmatic places to start"... I agree with that.
So, could we just take such types, and expose them (and any others like them) from a System.Drawing.Primitives.dll contract? These would not be tied in any way to an OS or its implementation, as we wouldn't be exposing the rest of System.Drawing (e.g. System.Drawing.Graphics) that does have strong ties to Win32 and GDI+. Are there fundamental problems with these types such that this would spell disaster? I'm not suggesting that they'd be the end-all-be-all of drawing primitives, but that they'd represent a reasonable foundation and place to start.
My main problems with the existing primitives:
In particular the second issue would probably stop me using these primitives in my project (https://github.com/grokys/Perspex/) although I imagine that's not something you'll lose sleep over ;)
Mutability is the deal-killer for me.
I'd also like to see fewer synonyms for the same thing. I don't think Rectangle.Top/Left/Bottom/Right are helpful. At most, I would expect X,X2, Y,Y2, Width, and Height.
Type conversion should also be as explicit as possible.
Having mutable structs will cause problems.
I just want to point out that if we did want to switch to having these be immutable, that should be possible while still using the existing types. These are currently mutable as they expose setters on the relevant properties (e.g. Point.X, RectangleF.Width, etc.)... if we wanted these to be immutable, we would not need to expose those set accessors in the contract; they could still be present in the implementation, but would be unavailable for anyone compiling against the contract's surface area.
I'd also like to see fewer synonyms for the same thing
Same deal; it's easy to subset in a contract.
@stephentoub But a start has already been made - I'm not sure the discussion can still be about a 'place to start'. Both corefx and .NET 4.6 will have a new two-float data type, System.Numerics.Vector2, with matching transformations. This type is already used as the graphics primitive in at least one drawing library that will run on top of corefx. (And is in fact blessed as the both a WinRT projection type, and the JIT optimized type.)
So any graphics stuff done that is compatible with System.Drawing will be the _second_ set introduced into corefx. Anyway, both Vector2 and the System.Drawing types are mutable. So then it's about designing a new, incompatible set of primitives. Before that's added to corefx I'd imagine it would be nice to have some example of how it would interop with existing code, and what a concrete rendering implementation that uses these types would be.
I think there are multiple conversations happening here:
I'm suggesting that if we answer "yes" for 1, then the answer to 2 should be "bring forward the existing primitives from System.Drawing", and the answer to 3 should be "yes, we can subset the surface area as desired (we've done that in lots of places)".
@govert, I believe you're focused purely on 1, saying "no, we have Vector2/3/4 and they can be used to represent any such primitives"; that's a fine place to argue, and I agree that @Priya91 or others should elaborate on the desired scenarios and use cases for these types, as that'll help inform all of these. But I think it's perfectly reasonable to have a conversation about 2 and 3 in parallel, and to not confuse those conversations with the conversation for 1, even though whether 2/3 happen is dependent on 1.
At least that's what I'm seeing from this thread. Apologies if I've misrepresented anyone.
I'm happy with the breakdown of questions @stephentoub offers.
I guess to me it looks like question 1 has already been answered (long before this proposal), and with a 'yes', by the fact the some graphics primitives have been added to corefx in System.Numerics. Those types are not just Vector2/3/4 for the SIMD stuff, but also Plane and Quaternion and have methods like AxisAngle rotations. These are already in both corefx and .Net 4.6, and are used as the graphics primitives in the Win2D projection. I'm not sure whether changes to System.Numerics would still be under discussion.
For question 2, one might now argue for another set of primitives to be added, either new and better, or compatible with System.Drawing (GDI+) or compatible with System.Windows (WPF - which uses doubles), or with some cross-platform library. I don't know what would inform the choice or backward-compatible library, or what would inform the need for this to be inside corefx and not a library outside. But I think it should be considered that this might be 'yet another set of primitives' for this use case in corefx, whether we like the existing ones or not.
Are there perhaps some documented guidelines on the scope of corefx vs. external libraries somewhere?
This is just a speclet, i'll fix these names in the actual PR, that will be committed.
@Priya91, FWIW, I think such speclets should represent the complete API we plan to add, naming and all, and so such feedback as @justinvp provided is valid and, if/when agreed upon, should be factored into the speclet prior to moving to do a PR, which would be primarily focused then on implementing the agreed upon API. My $.02.
@stephentoub : I've updated the parameter names, although pt, sz are the parameter names that are already given in System.Drawing types. If we are to bring forward existing implementations, then these names are not going to be as desired.
Quoting @govert:
If Rect, Size and Color are to be added to corefx, they should be signature-compatible with the WinRT projections from Windows.Foundation as would already be used from .NET 4.6, or anything using Win2D. This would target the API design at the most likely forward-looking use case.
I would love to be able to have Point/Size/Rect types for .NET Core that could just be projections of the Windows.Foundation types. However, the Windows.Foundation Point, Size, and Rect types use doubles instead of floats. My understanding is that floats are preferable because they can be used with SIMD, and because the extra precision from doubles isn't needed.
How much of an issue would it be to use doubles instead of floats? Would it be worth using doubles for the canonical .NET Point, Size, and Rectangle types going forward in order to be compatible with the WinRT types that already exist?
Quoting @stephentoub:
the answer to 2 should be "bring forward the existing primitives from System.Drawing"
This is what we were initially considering. The reasons we went with new types for the current proposal were:
PointF to .NET Core without a corresponding Point type would be weird.Nothing is set in stone at this point though. We appreciate the feedback so far- please keep it coming!
RE Mutability.
I was under the impression that this was done for performance reasons?
dotnet/runtime#13999 appears to relate to this issue as well.
Some constructive feedback:
This type has some serious deficiencies - my highest priority feedback is to remove it until a proper color library can be written by people who have a strong background in color theory and the issues involved.
Answers to @stephentoub's questions
Should drawing primitives be added to CoreFX? No - not yet. My recommendation is to place this in a "futures" library near the CoreFX umbrella, but not an official part of it yet. It's likely this library will have a high degree of churn as it stabilizes and future dependencies (e.g. new imaging libraries) are worked on. Toss it in NuGet, but don't make it "System.*" yet.
@Priya91 @dsplaisted The floating point types are awkward when working with raster (pixel) images. Consider that a theoretical "rasterImage.Size" would be an integral/discrete count of pixels, not floating point. In my web-applications I typically use System.Drawing.Size in my settings & configuration to specify thumbnail sizes, for example. You can't have "150.4" pixels. I agree completely with @nathanaeljones that floating-point precision issues can cause a ton of bugs in certain scenarios. No one wants to have the width of their image returned as 39.9999999998 pixels.
@govert I agree that this isn't the right place to be discussing a next generation drawing and geometric API. I'd rather this be a "futures" or side project. In effect, Microsoft-sanctioned projects or libraries that aren't part of Corefx, but are closely related to it.
@nathanaeljones While I partially agree that cross platform graphics APIs of a large scope are not portable, smaller ones may be - consider an imaging API. Loading, resizing, and saving a JPEG, GIF, or PNG is a bread and butter webserver activity, and is going to become a huge blocker for ASP.NET 5 adoption. It's already blocking me. The approach I'm envisioning is the same as OWIN - provide a set of base interfaces for graphics and imaging, then "bridge" them over to the existing APIs on each platform. You could have a bridge/implementation for GDI+, Direct2D, whatever.
You also mentioned that "Vector* is optimized for performance. That's exactly why I'm recommending that these structures use Vector for their internal storage.
@stephentoub I agree that the existing types in System.Drawing are "good enough". Remember, .NET Core is modular. Create System.Drawing.Primitives.dll and make it contain the exact same Rectangle, Point, Size, and Color structures with both integer and floating-point versions. If you look at other decisions in the CoreFX, older/bad design patterns were left in place for the same of compatibility. The same holds true here.
Then, spin up a separate project for a fresh API for geometry, color, and perhaps an OWIN-like interface for imaging and drawing. As new libraries come online within the corefx ecosystem, they can use the legacy-free variants.
``` C#
// System.Drawing.Primitives.dll
namespace System.Drawing
{
// All of these structures will be the same as the .NET Framework versions, with minor changes to remove members that no longer make sense.
public struct Color : IEquatable
{
// Changed named colors to be readonly fields.
// Removed FromKnownColor, FromName, ToKnownColor
public static readonly Color Empty;
public static readonly Color AliceBlue;
public static readonly Color AntiqueWhite;
public static readonly Color Aqua;
public static readonly Color Aquamarine;
public static readonly Color Azure;
public static readonly Color Beige;
public static readonly Color Bisque;
public static readonly Color Black;
public static readonly Color BlanchedAlmond;
public static readonly Color Blue;
public static readonly Color BlueViolet;
public static readonly Color Brown;
public static readonly Color BurlyWood;
public static readonly Color CadetBlue;
public static readonly Color Chartreuse;
public static readonly Color Chocolate;
public static readonly Color Coral;
public static readonly Color CornflowerBlue;
public static readonly Color Cornsilk;
public static readonly Color Crimson;
public static readonly Color Cyan;
public static readonly Color DarkBlue;
public static readonly Color DarkCyan;
public static readonly Color DarkGoldenrod;
public static readonly Color DarkGray;
public static readonly Color DarkGreen;
public static readonly Color DarkKhaki;
public static readonly Color DarkMagenta;
public static readonly Color DarkOliveGreen;
public static readonly Color DarkOrange;
public static readonly Color DarkOrchid;
public static readonly Color DarkRed;
public static readonly Color DarkSalmon;
public static readonly Color DarkSeaGreen;
public static readonly Color DarkSlateBlue;
public static readonly Color DarkSlateGray;
public static readonly Color DarkTurquoise;
public static readonly Color DarkViolet;
public static readonly Color DeepPink;
public static readonly Color DeepSkyBlue;
public static readonly Color DimGray;
public static readonly Color DodgerBlue;
public static readonly Color Firebrick;
public static readonly Color FloralWhite;
public static readonly Color ForestGreen;
public static readonly Color Fuchsia;
public static readonly Color Gainsboro;
public static readonly Color GhostWhite;
public static readonly Color Gold;
public static readonly Color Goldenrod;
public static readonly Color Gray;
public static readonly Color Green;
public static readonly Color GreenYellow;
public static readonly Color Honeydew;
public static readonly Color HotPink;
public static readonly Color IndianRed;
public static readonly Color Indigo;
public static readonly Color Ivory;
public static readonly Color Khaki;
public static readonly Color Lavender;
public static readonly Color LavenderBlush;
public static readonly Color LawnGreen;
public static readonly Color LemonChiffon;
public static readonly Color LightBlue;
public static readonly Color LightCoral;
public static readonly Color LightCyan;
public static readonly Color LightGoldenrodYellow;
public static readonly Color LightGray;
public static readonly Color LightGreen;
public static readonly Color LightPink;
public static readonly Color LightSalmon;
public static readonly Color LightSeaGreen;
public static readonly Color LightSkyBlue;
public static readonly Color LightSlateGray;
public static readonly Color LightSteelBlue;
public static readonly Color LightYellow;
public static readonly Color Lime;
public static readonly Color LimeGreen;
public static readonly Color Linen;
public static readonly Color Magenta;
public static readonly Color Maroon;
public static readonly Color MediumAquamarine;
public static readonly Color MediumBlue;
public static readonly Color MediumOrchid;
public static readonly Color MediumPurple;
public static readonly Color MediumSeaGreen;
public static readonly Color MediumSlateBlue;
public static readonly Color MediumSpringGreen;
public static readonly Color MediumTurquoise;
public static readonly Color MediumVioletRed;
public static readonly Color MidnightBlue;
public static readonly Color MintCream;
public static readonly Color MistyRose;
public static readonly Color Moccasin;
public static readonly Color NavajoWhite;
public static readonly Color Navy;
public static readonly Color OldLace;
public static readonly Color Olive;
public static readonly Color OliveDrab;
public static readonly Color Orange;
public static readonly Color OrangeRed;
public static readonly Color Orchid;
public static readonly Color PaleGoldenrod;
public static readonly Color PaleGreen;
public static readonly Color PaleTurquoise;
public static readonly Color PaleVioletRed;
public static readonly Color PapayaWhip;
public static readonly Color PeachPuff;
public static readonly Color Peru;
public static readonly Color Pink;
public static readonly Color Plum;
public static readonly Color PowderBlue;
public static readonly Color Purple;
public static readonly Color Red;
public static readonly Color RosyBrown;
public static readonly Color RoyalBlue;
public static readonly Color SaddleBrown;
public static readonly Color Salmon;
public static readonly Color SandyBrown;
public static readonly Color SeaGreen;
public static readonly Color SeaShell;
public static readonly Color Sienna;
public static readonly Color Silver;
public static readonly Color SkyBlue;
public static readonly Color SlateBlue;
public static readonly Color SlateGray;
public static readonly Color Snow;
public static readonly Color SpringGreen;
public static readonly Color SteelBlue;
public static readonly Color Tan;
public static readonly Color Teal;
public static readonly Color Thistle;
public static readonly Color Tomato;
public static readonly Color Transparent;
public static readonly Color Turquoise;
public static readonly Color Violet;
public static readonly Color Wheat;
public static readonly Color White;
public static readonly Color WhiteSmoke;
public static readonly Color Yellow;
public static readonly Color YellowGreen;
public bool IsEmpty { get; }
public byte A { get; }
public byte B { get; }
public byte G { get; }
public byte R { get; }
public static Color FromArgb(int argb);
public static Color FromArgb(int alpha, Color baseColor);
public static Color FromArgb(int red, int green, int blue);
public static Color FromArgb(int alpha, int red, int green, int blue);
public override int GetHashCode();
public bool Equals(Color other);
public override bool Equals(object obj);
public float GetBrightness();
public float GetHue();
public float GetSaturation();
public int ToArgb();
public override string ToString();
public static bool operator ==(Color left, Color right);
public static bool operator !=(Color left, Color right);
}
public struct Point : IEquatable<Point>
{
public static readonly Point Empty;
public Point(int dw);
public Point(Size sz);
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 static Point Round(PointF value);
public static Point Subtract(Point pt, Size sz);
public static Point Truncate(PointF value);
public bool Equals(Point other);
public override bool Equals(object obj);
public override int GetHashCode();
public void Offset(Point p);
public void Offset(int dx, int dy);
public override string ToString();
public static Point operator +(Point pt, Size sz);
public static Point operator -(Point pt, Size sz);
public static bool operator ==(Point left, Point right);
public static bool operator !=(Point left, Point right);
public static implicit operator PointF(Point p);
public static explicit operator Size(Point p);
}
public struct PointF : IEquatable<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, SizeF sz);
public static PointF Add(PointF pt, Size sz);
public static PointF Subtract(PointF pt, SizeF sz);
public static PointF Subtract(PointF pt, Size sz);
public bool Equals(PointF other);
public override bool Equals(object obj);
public override int GetHashCode();
public override string ToString();
public static PointF operator +(PointF pt, SizeF sz);
public static PointF operator +(PointF pt, Size sz);
public static PointF operator -(PointF pt, SizeF sz);
public static PointF operator -(PointF pt, Size sz);
public static bool operator ==(PointF left, PointF right);
public static bool operator !=(PointF left, PointF right);
}
public struct Rectangle : IEquatable<Rectangle>
{
public static readonly Rectangle Empty;
public Rectangle(Point location, Size size);
public Rectangle(int x, int y, int width, int height);
public bool IsEmpty { get; }
public int X { get; set; }
public int Y { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public int Top { get; }
public int Left { get; }
public int Bottom { get; }
public int Right { get; }
public Point Location { get; set; }
public Size Size { get; set; }
public static Rectangle Ceiling(RectangleF value);
public static Rectangle FromLTRB(int left, int top, int right, int bottom);
public static Rectangle Inflate(Rectangle rect, int x, int y);
public static Rectangle Intersect(Rectangle a, Rectangle b);
public static Rectangle Round(RectangleF value);
public static Rectangle Truncate(RectangleF value);
public static Rectangle Union(Rectangle a, Rectangle b);
public bool Contains(Rectangle rect);
public bool Contains(Point pt);
public bool Contains(int x, int y);
public bool Equals(Rectangle other);
public override bool Equals(object obj);
public override int GetHashCode();
public void Inflate(Size size);
public void Inflate(int width, int height);
public void Intersect(Rectangle rect);
public bool IntersectsWith(Rectangle rect);
public void Offset(Point pos);
public void Offset(int x, int y);
public override string ToString();
public static bool operator ==(Rectangle left, Rectangle right);
public static bool operator !=(Rectangle left, Rectangle right);
}
public struct RectangleF : IEquatable<RectangleF>
{
public static readonly RectangleF Empty;
public RectangleF(PointF location, SizeF size);
public RectangleF(float x, float y, float width, float height);
public bool IsEmpty { get; }
public float X { get; set; }
public float Y { get; set; }
public float Width { get; set; }
public float Height { get; set; }
public float Top { get; }
public float Left { get; }
public float Bottom { get; }
public float Right { get; }
public PointF Location { get; set; }
public SizeF Size { get; set; }
public static RectangleF FromLTRB(float left, float top, float right, float bottom);
public static RectangleF Inflate(RectangleF rect, float x, float y);
public static RectangleF Intersect(RectangleF a, RectangleF b);
public static RectangleF Union(RectangleF a, RectangleF b);
public bool Contains(RectangleF rect);
public bool Contains(PointF pt);
public bool Contains(float x, float y);
public bool Equals(RectangleF other);
public override bool Equals(object obj);
public override int GetHashCode();
public void Inflate(SizeF size);
public void Inflate(float x, float y);
public void Intersect(RectangleF rect);
public bool IntersectsWith(RectangleF rect);
public void Offset(PointF pos);
public void Offset(float x, float y);
public override string ToString();
public static bool operator ==(RectangleF left, RectangleF right);
public static bool operator !=(RectangleF left, RectangleF right);
public static implicit operator RectangleF(Rectangle r);
}
public struct Size : IEquatable<Size>
{
public static readonly Size Empty;
public Size(Point pt);
public Size(int width, int height);
public bool IsEmpty { get; }
public int Height { get; set; }
public int Width { get; set; }
public static Size Add(Size sz1, Size sz2);
public static Size Ceiling(SizeF value);
public static Size Round(SizeF value);
public static Size Subtract(Size sz1, Size sz2);
public static Size Truncate(SizeF value);
public override bool Equals(object obj);
public bool Equals(Size other);
public override int GetHashCode();
public override string ToString();
public static Size operator +(Size sz1, Size sz2);
public static Size operator -(Size sz1, Size sz2);
public static bool operator ==(Size sz1, Size sz2);
public static bool operator !=(Size sz1, Size sz2);
public static implicit operator SizeF(Size p);
public static explicit operator Point(Size size);
}
public struct SizeF : IEquatable<SizeF>
{
public static readonly SizeF Empty;
public SizeF(PointF pt);
public SizeF(SizeF size);
public SizeF(float width, float height);
public bool IsEmpty { get; }
public float Height { get; set; }
public float Width { get; set; }
public static SizeF Add(SizeF sz1, SizeF sz2);
public bool Equals(SizeF other);
public override bool Equals(object obj);
public override int GetHashCode();
public PointF ToPointF();
public Size ToSize();
public override string ToString();
public static SizeF operator +(SizeF sz1, SizeF sz2);
public static SizeF operator -(SizeF sz1, SizeF sz2);
public static bool operator ==(SizeF sz1, SizeF sz2);
public static bool operator !=(SizeF sz1, SizeF sz2);
public static explicit operator PointF(SizeF size);
}
}
```
This proposal is my preference. Rather than try to worry about compatibility with older data structures and potentially impact newer use-cases, create a proper geometry library that depends on the new SIMD vectors.
The structures below aren't complete; for example, quite a few common intersections are missing. Most of the types would use center+extents as their fields for efficient storage and intersections.
``` C#
namespace System.Numerics.Geometry
{
public struct Rectangle : IEquatable
{
public Rectangle(Vector2 center, Vector2 size);
public Rectangle(float x, float y, float width, float height);
public static Rectangle FromMinMax(Vector2 min, Vector2 max);
public static readonly Rectangle Empty;
public float X { get; }
public float Y { get; }
public float Width { get; }
public float Height { get; }
public Vector2 Center { get; }
public Vector2 Extents { get; }
public Vector2 Min { get; }
public Vector2 Max { get; }
public Vector2 Size { get; }
public bool Contains(Vector2 point);
public bool Contains(Rectangle rectangle);
public Rectangle Expand(Vector3 value);
public Rectangle Intersect(Rectangle rectangle);
public bool Intersects(Rectangle rectangle);
public Rectangle Offset(Vector2 value);
public static Rectangle Union(Rectangle a, Rectangle b);
public override string ToString();
public override bool Equals(object obj);
public bool Equals(Rectangle other);
public override int GetHashCode();
public static bool operator==(Rectangle left, Rectangle right);
public static bool operator !=(Rectangle left, Rectangle right);
}
public struct Box : IEquatable<Box>
{
public Box(Vector3 center, Vector3 size);
public Box(float x, float y, float z, float width, float height, float depth);
public static Box FromMinMax(Vector3 min, Vector3 max);
public static readonly Box Empty;
public Vector3 Center { get; }
public Vector3 Extents { get; }
public Vector3 Min { get; }
public Vector3 Max { get; }
public Vector3 Size { get; }
public bool Contains(Vector3 point);
public bool Contains(Box box);
public Box Scale(float value);
public Box Scale(Vector3 value);
public Box Intersect(Box box);
public bool Intersects(Box box);
public Box Offset(Vector3 value);
public static Box Union(Box a, Box b);
public override string ToString();
public override bool Equals(object obj);
public bool Equals(Box other);
public override int GetHashCode();
public static bool operator ==(Box left, Box right);
public static bool operator !=(Box left, Box right);
}
public struct OrientedBox : IEquatable<OrientedBox>
{
public OrientedBox(Vector3 center, Vector3 extents, Vector3[] axis);
public OrientedBox(Vector3 center, Vector3 extents, Vector3 xAxis, Vector3 yAxis, Vector3 zAxis);
public Vector3 Center { get; }
public Vector3 Extents { get; }
public Vector3 XAxis { get; }
public Vector3 YAxis { get; }
public Vector3 ZAxis { get; }
public bool Contains(Vector3 point);
public OrientedBox Scale(float value);
public OrientedBox Scale(Vector3 value);
public OrientedBox Offset(Vector3 value);
public override string ToString();
public override bool Equals(object obj);
public bool Equals(OrientedBox other);
public override int GetHashCode();
public static bool operator ==(Box left, Box right);
public static bool operator !=(Box left, Box right);
}
public struct Sphere : IEquatable<Sphere>
{
public Sphere(Vector3 center, float radius);
public Vector3 Center { get; }
public float Radius { get; }
public bool Contains(Vector3 point);
public Box Scale(float value);
public Box Offset(Vector3 value);
public Box Offset(Vector3 value);
public override string ToString();
public override bool Equals(object obj);
public bool Equals(Sphere other);
public override int GetHashCode();
public static bool operator ==(Box left, Box right);
public static bool operator !=(Box left, Box right);
}
public struct Ray2 : IEquatable<Ray2>
{
public Ray2(Vector2 origin, Vector2 direction);
public Vector2 Origin { get; }
public Vector2 Direction { get; }
public Ray2 Offset(Vector2 value);
public override string ToString();
public override bool Equals(object obj);
public bool Equals(Ray2 other);
public override int GetHashCode();
public static bool operator ==(Box left, Box right);
public static bool operator !=(Box left, Box right);
}
public struct Ray3 : IEquatable<Ray3>
{
public Ray3(Vector3 origin, Vector3 direction);
public Vector3 Origin { get; }
public Vector3 Direction { get; }
public Ray3 Offset(Vector3 value);
public override string ToString();
public override bool Equals(object obj);
public bool Equals(Ray3 other);
public override int GetHashCode();
public static bool operator ==(Box left, Box right);
public static bool operator !=(Box left, Box right);
}
}
```
:+1: I think @ebickle has nailed it. Personally I don't see much value in adding a set of primitives copied directly over from System.Drawing; even if every library uses a different Point, Rect, Color it's not hard to convert between them. However, I don't see a problem in providing a NuGet package for anyone who would find them useful. I do think that a well-designed set of primitives however would be really useful but it's not something to be rushed.
@ebickle, Thanks for the refined proposal. :+1:
Tip: use https://gist.github.com/ for large code chunks for brevity.
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
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:
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?
:+1: now that the core scenario for the creation if these types has been explained, it's a bit clearer to me.
@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.
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.
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.
I've added an issue to corefxlab for further discussion of the legacy-free direction: https://github.com/dotnet/corefxlab/issues/86
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:
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?
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.
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 using integers are often used for interop scenarios:
Point (now RawPoint for interop) in SharpDX is used by various methods/datas in Direct2D1, a bit in DXGIRectangle (now RawRectangle for interop) is used by almost all DirectX APIs(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.) (X,Y,Width,Height) that is used currently by WIC (Windows Imaging Component)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)
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.)
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:
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.
I agree that Color needs special crafting. There is a need to correctly handle:
Color4F, Color3F, but also Color4 for a int 8 bits per component.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.
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:
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 !
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.
LGTM @Priya91
This is close to what @xoofx is using with SharpDX. Right?
@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.
We've reviewed this week and it looks good as proposed. Notes are available here.
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?
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?
Has this been canceled? https://github.com/dotnet/corefxlab/issues/86
PR: dotnet/corefx#5223
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
Most helpful comment
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.