-
Notifications
You must be signed in to change notification settings - Fork 26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Potential breaking changes for 2.0 #18
Comments
In my use case, I've needed also the polygons so I'd have needed the whole lot anyway. How do you choose/define those primitive types? Would this imply that rendering packages should also be split? I see why you would want that however, especially with all other 2d/3d types in waiting (torus, sphere, cylinder, ...). Maybe an organization by categories of objects? But I've no idea how to categorize them.
Disk and circle as different entities fits well in the library in my opinion. Though I'm not sure it makes sense for a disk to have a circumference. Probably better to have an "inexpensive" conversion to and from a disk to it's corresponding circle. Not a breaking change, but I've felt the rectangle type as a missing component. Sure polygon can do the trick but it's so much easier to manipulate rectangles than polygons when needing only rectangles. I thought of this now because in case it's added to opensolid, I feel there is also a lack of terminology to distinguish between rectangle the contour (bounding box?) and the surface. About the types of objects in the library. Have you reconsidered moving to opaque? Similar to what style-elements does with its |
Primitives: points, vectors, directions, axes, planes, frames, sketch planes, bounding boxes This is partially for technical reasons (the primitive types circularly depend on each other and so can't be split into separate packages, while the shape types build on top of the primitives), but I think also makes sense at a higher level - the shapes are things you might actually draw (in SVG or WebGL), while the primitives you typically wouldn't - they're the building blocks that make up everything else. I expect that a lot of people will be like you and would need both packages, but there might be some people who just want some lightweight point and vector types and don't want to pull in a much larger package. The primary downside is that in the common case instead of import OpenSolid.Geometry.Types exposing (..) you'd need something like import OpenSolid.Primitive.Types exposing (..)
import OpenSolid.Shape.Types exposing (..) in all of your source files. |
Logged as a separate issue in #19. |
I'm still a bit reluctant to do this because I still don't really see they 'why'. I realize that opaque types should be the default choice in most cases, and I love them and use them almost exclusively in some other packages I'm working on (e.g. opensolid/scene, opensolid/step) where they are extremely useful. But I don't want to use them blindly/dogmatically, and especially for low-level stuff like points and triangles, I'm a lot more confident about committing to a specific exposed representation. Plus, writing stuff like myAxis =
Axis3d
{ originPoint = Point3d ( 1, 2, 3 )
, direction = Direction3d.y
} is just so convenient; otherwise presumably we'd have something like myAxis =
Axis3d.with
{ originPoint = Point3d.withCoordinates ( 1, 2, 3 )
, direction = Direction3d.y
} which is not bad but is not a naming convention I've seen in other libraries. How would you see replacing the exposed constructors? |
Fine, that makes sense. Just one remark about the exposed constructors: as an elm beginner, you tend to get quite early that capitalized words are types and the rest are constants and functions, ... except until you realize not exactly. Type aliases as constructors, or taggers (in unions) are also functions. And that may bring the first confusions for newcomers. An alternative here might be to spotlight the default constructor as being the one with the same name that the type: import OpenSolid.Geometry.Types exposing (..)
import OpenSolid.Point3d as Point3d exposing (point3d)
import OpenSolid.Axis3d as Axis3d exposing (axis3d)
import OpenSolid.Direction3d as Direction3d exposing (direction3d)
myAxis =
axis3d
{ originPoint = point3d ( 1, 2, 3 )
, direction = Direction3d.y
} Now, with just a little more experience in elm, I guess the version with the tagger feel better since you know it's a function and you see directly the return type :). Whereas with the function version, there might be a little voice in your head saying, "are you sure this returns what you think it returns" and you might check, just to be sure. |
I certainly want to make this package friendly for newcomers! One potentially interesting compromise could be to make some of the more primitive types opaque, while keeping the higher-level record-based ones exposed; I think for the more primitive types there are more obvious names for constructor functions, and it means that they could in theory be switched to having some different (native?) implementation in the future (although I think I'd prefer to just wait for the Elm compiler to get better at optimizing code rather than write custom JavaScript to work around it). Something like point =
Point3d.fromCoordinates ( 1, 2, 3 )
direction =
Direction3d.fromComponents ( 0, 0, -1 )
lineSegment =
LineSegment3d.fromEndpoints ( p1, p2 )
triangle =
Triangle3d.fromVertices ( pt1, pt2, pt3 )
polygon =
Polygon2d.fromVertices [ p1, p2, p3, p4, p5 ]
boundingBox =
BoundingBox2d.fromExtrema
{ minX = -1
, maxX = 3
, minY = 2
, maxY = 4
}
spline =
QuadraticSpline2d.fromControlPoints ( p1, p2, p3 )
axis =
Axis3d { originPoint = point, direction = direction }
arc =
Arc2d
{ centerPoint = Point2d.origin
, startPoint = Point2d.fromCoordinates ( 2, 3 )
, sweptAngle = degrees 30
} One advantage of constructor functions is that they look nicer in pipelines: fromVec3 : Vec3 -> Point3d
fromVec3 =
Math.Vector3.toTuple >> Point3d.fromCoordinates vs fromVec3 : Vec3 -> Point3d
fromVec3 =
Math.Vector3.toTuple >> Point3d which looks a bit funky. Of course, having some types be constructed directly and other types be opaque with constructor functions could just make things more confusing... |
Yes, and this should be the preferred way to write pipeline, whether direct type constructors exist or not. However, I agree with the following:
Actually, if you didn't run into needing another internal representation, it might be wiser to just wait if the need arises. You might come to need other potential breaking changes in the way. As Donald said, premature optimization is the root of all evil. And I tend to have compulsive need for premature optimizations too often. By the way, do you know other people using this library for their projects? |
Yeah, I'm happy to sit on these sorts of potential breaking changes for a while until some sort of consensus or clear answer emerges - no real hurry. I know of a small number of others using OpenSolid, but not a lot yet...I suspect it might become more popular once I publish |
My personal takes, as someone who has yet to start using the library: Module RenamingHaving the I wonder about a library that wraps Opensolid (say, a videogame engine) and wants to export a higher level module that's called What about a CAD that needs to have several object types, augmented with UI state? Regardless, no big deal either way. RenamesThe renames proposed seem like a good idea. Opaque typesIn general, having the user instantiate types directly is not a good idea:
If instantiation performance is an issue, provide a
I would advise against making only some types opaque, simply because of consistency, it might make things more confusing for newbies. The problem I see with opaque types is that they limit the efficiency of external libraries. |
Great comments @xarvh! Keep them coming =)
Yes, that would probably make the most sense. If the module was just in an app, though (not published as a package), then you could call your local high-level module
I think I like the idea of renaming
I do like the consistency of having all values and functions prefixed with the module (an
I don't think this should be an issue as long as there are some zero-overhead 'directly construct' functions like |
I think 2.0 is almost ready to be released - I've placed my draft release notes and a copy of the |
Fantastic job @ianmackenzie ! I went through the README.md, I think 2.0 is a great improvement. For the sake of nitpicking, I have a few comments. The withCoordinates constructors seem to be unnecessarily verbose. If we follow linear-algebra/core convention, the obvious constructor has the name of the type (eg, Another straightforward convention would be a conversion: instead of naming a function ${contructedType}From${Argument} a common, and easy to think about naming pattern is ${argument}To${contructedType}. Of course, which one you use depends on where you want the focus to be. Regardless of the above, the API seems good! |
Hmm @xarvh you have a point - there's a bit of inconsistency in that 'from' sometimes means "convert from" (as in points |> List.map (Vector2d.from Point2d.origin) to get a list of radial vectors from the origin point to a bunch of other points, but I guess it does really come into conflict with the general Elm pattern of As for 'with', that was largely a result of just trying to find something that read well, for example Axis2d.with
{ originPoint = Point2d.withCoordinates ( 2, 3 )
, direction = Direction2d.withPolarAngle (degrees 30)
} can be read pretty literally as "an axis2d
{ originPoint = point2d ( 2, 3 )
, direction = Direction2d.fromAngle (degrees 30)
} is more concise but I don't think reads quite as well (and is a bit less consistent, where one constructor is prefixed but the other two aren't). Perhaps a compromise could be to just use Axis2d.with
{ originPoint = Point2d.fromCoordinates ( 2, 3 )
, direction = Direction2d.fromAngle (degrees 30)
} This would keep what I think is the nice property of always using the module as a prefix, and be pretty explicit, but cut down on the unconventional use of 'with'. Not sure what to do with the bare On a separate note, I have no particular desire to follow any given pattern just because |
From what I'm reading, it seems great. But take my impression lightly since I've not used a lot of this package functionalities, mostly simplest stuff. |
Again, bear in mind that I'm nitpicking and I think the API looks perfectly OK.
When it means "from one point to another" TBH I find it a bit confusing, because it leaves the "to another" part completely out.
|
Regarding the
|
OK how about this: change most
...simplify the
...change
...and keep all the existing plain @xarvh, I agree with your points about the bare Thanks all for the comments - this is really useful feedback! |
Glad we're in agreement - hope I didn't come off as snippy! (The general comment about following |
It's still something I'm considering (placeholder issue here), although I'm currently tempted to publish 2.0 and maybe split the package into two for a 3.0. Fortunately, the new module layout in 2.0 should make that even easier for end users since code won't change at all, just import OpenSolid.Point2d as Point2d exposing (Point2d)
import OpenSolid.Triangle2d as Triangle2d exposing (Triangle2d) even if in the future those modules actually come from two different packages like |
LGTM =) |
This is a meta-issue for potential breaking changes to make for a 2.0 version. Feel free to post suggestions in the comments and I'll edit this issue text.
Module naming
I used prefixed module names like
OpenSolid.Point2d
to avoid potential naming conflicts with modules from other packages, but based on elm/compiler#1625 it seems that this might be considered "silly module renaming". Should the prefixes be removed? I think they should remain in a few cases likeOpenSolid.Geometry.Types
andOpenSolid.Geometry.Decode
, but otherwise module names could be switched to just plainPoint2d
,Triangle2d
etc.This would be more consistent with other Elm packages -
elm-community/elm-test
uses top-levelExpect
,Fuzz
andTest
modules,mdgriffith/style-elements
uses top-levelStyle
andElement
modules,terezka/elm-plot
uses a top-levelPlot
module, etc.Additionally, if the non-prefixed names don't work out and run into conflicts with other packages, then these conflicts will provide additional data points for elm/compiler#1625.
Renames
I used things like
radialDistanceFrom
to disambiguate from plaindistanceFrom
(which takes another point as argument) while keeping the nice 'read as a phrase' quality. However, I've come to think these names are a little too 'cute'/obscure and it would probably be better to use more 'boring' names, even thoughPoint3d.distanceFromAxis axis
doesn't read quite as nicely asPoint3d.radialDistanceFrom axis
.Point3d.radialDistanceFrom
->Point3d.distanceFromAxis
Point3d.squaredRadialDistanceFrom
->Point3d.squaredDistanceFromAxis
Point3d.projectRadiallyOnto
->Point3d.projectOntoAxis
Removals
Direction#d.scaleBy
: useVector#d.in_
instead (sounds a bit weird to 'scale a direction')opensolid/primitives
package? Could make them more appealing for people who don't want to depend on a large package or (shudder) 'framework'...Circle2d.area
andCircle3d.area
and addDisk2d
andDisk3d
types/modules with those functions? Then a 'circle' would be a curve (that has no area) and a 'disk' would be a closed circle (that has area). Circles and disks would probably both be considered to have 'circumference'. This clears up some ambiguity - for example right now should a WebGLRender3d.circle
function render a curve or a surface?Type changes
normalDirection
field toSketchPlane3d
? This would allow sketch planes to be flipped, and would help in defining surfaces and bodies. It would also be a bit more consistent withPlane3d
in that the normal direction would get mirrored whenever mirroring the plane. However, there could be some weird edge cases where semantics of left-handed sketch planes (where the normal direction is opposite to the cross product of the X and Y directions) are not clear...Signature changes
[Point,Vector]#d.interpolate
: instead of a synonym forinterpolateFrom : a -> a -> Float -> a
, switch to( a, a ) -> Float -> a
and addinterpolateBy : Float -> ( a, a ) -> a
(both designed for different applications of chaining).Arc2d.fromEndpoints
to take a record withstartPoint
,endPoint
,radius
,windingDirection
andwhichHalf
(?) fields instead of separate arguments? More clear, more similar toArc2d.with
...Opaque types?
Should some or all of the types in this package be switched to opaque with constructor functions?
Pros
Math.Vector3.toTuple >> Point3d.fromComponents
is pretty readable whileMath.Vector3d.toTuple >> Point3d
seems a bit awkwardTypes
module and switch to more conventionalimport OpenSolid.Point2d as Point2d exposing (Point2d)
etc.Cons
Point3d.fromCoordinates ( 1, 2, 3 )
instead ofPoint3d ( 1, 2, 3 )
)Axis3d.with { originPoint = ..., direction = ... }
?Axis3d.axis3d { originPoint = ..., direction = ...}
, expecting that people would expose theaxis3d
function and use it unqualified?)Direction3d ( 2, 0, 0 )
would just construct an invalid direction (since there's no way to customize the behavior of a constructor), while you might guess thatDirection3d.fromComponents ( 2, 0, 0 )
would perform normalization. It might be nice for constructors to perform normalization, but then they should really also returnMaybe
orResult
values. I kind of like the fact that having directDirection3d
,Frame3d
constructors etc. allows you to directly construct objects with no normalization etc. overhead, but makes it pretty clear that you are then responsible for correctness yourself.The text was updated successfully, but these errors were encountered: