Skip to content

Coordinate spaces#32

Draft
thehuglet wants to merge 7 commits intomainfrom
hug/coord-spaces
Draft

Coordinate spaces#32
thehuglet wants to merge 7 commits intomainfrom
hug/coord-spaces

Conversation

@thehuglet
Copy link
Owner

@thehuglet thehuglet commented Feb 19, 2026

Draft since the docs still require tweaking.

Coordinate spaces for each drawing format

Each drawing format has a set of corresponding coordinate space structs. FooPosition has x and y fields, FooSize has width and height. All builtin types:

  • (NativePosition, NativeSize)
  • (TwoxelPosition, TwoxelSize)
  • (OctadPosition, OctadSize)
  • (BlocktadPosition, BlocktadSize)

Relative conversions

Position types can convert to other Position types freely in a relative fashion. Same goes for Size. This is done through dedicated to_*() methods, as some conversions are lossy (e.g OctadPosition -> NativePosition)

The Position and Size traits include default convenience helpers such as offset_x(), offset_xy(), to_tuple().

Drawing functions API changes

Drawing functions now each take a dedicated impl Into<FooPosition> (or FooSize) argument. The only From<> implementation included is From<(i16, i16)> for all built-in types for convenience. This means that twoxels and octads no longer use f32s for sub-cell positioning.

Convenience arithmetic impls

I'm not entirely sure if this is a good thing or not, but I added the following macros which implement the simplest form of arithmetic for Position and Size types:

  • impl_coord_space_position_arithmetic!()
  • impl_coord_space_size_arithmetic!()

The main reason for their inclusion is reducing boilerplate when adding custom coordinate spaces.


I thought of the following, but decided to hold off for now:

  • Adding a trait that implements to_*() methods for tuples
  • Possibly getting rid of Size types as they are quite repetitive and most operations seem to be done on Position types
  • Related to the one above, making Size and Position of the same type (e.g. twoxel) convertable. I'm not sure whether an x -> width & y -> height conversion makes sense though.

Is there anything in here that doesn't belong or should be changed?

@airblast-dev
Copy link
Collaborator

I am not a fan of having a trait for Size and Position. I consider a Size to be a value, not a types characteristic.

I would prefer using a Size<X: Copy = u16, Y: Copy = X> and Position<X: Copy = u16, Y: Copy = X>. We can implement conversion methods or traits on these and have less traits and distinct types floating around. As an unlikely to be useful upside this gives us the flexibility to use different type for X and Y with minimal boilerplate.
If we have a kind of size or position that can accept multiple types (so Size<MyEnum> or Size<i16>) we can allow those via traits in the future if needed.


Related to the one above, making Size and Position of the same type (e.g. twoxel) convertable. I'm not sure whether an x -> width & y -> height conversion makes sense though.

I don't think its really needed. Might cause more confusion than practicality. Its just two fields/values so I would rather just construct it manually.

@thehuglet
Copy link
Owner Author

Gotcha, I'll admit I'm still new to traits so I really appreciate your feedback here!

I would prefer using a Size<X: Copy = u16, Y: Copy = X> and Position<X: Copy = u16, Y: Copy = X>.

Hmm I'm not sure how Position<X: Copy = u16, Y: Copy = X> would convey the information regarding which coordinate space we're in, unless I misunderstood something.

How would you feel about having a Position<T> with struct markers for Native, Twoxel, etc. and locking the x and y fields to i16? I don't mind custom coordinate spaces using other types, but I don't want someone using f32 with Twoxel for example.

With this approach the conversions could be implemented like this:

impl Position<Twoxel> {
    fn to_native(self) -> Position<Native> {
        Position::new(self.x, self.y / 2)
    }
    fn to_octad(self) -> Position<Octad> {
        Position::new(self.x * 2, self.y * 2)
    }
}

Related to the one above, making Size and Position of the same type (e.g. twoxel) convertable. I'm not sure whether an x -> width & y -> height conversion makes sense though.

I don't think its really needed. Might cause more confusion than practicality. Its just two fields/values so I would rather just construct it manually.

I understand, in hindsight it's probably clearer to construct it manually.

One idea: what if we used a single type for both positions and size args, e.g. Coords instead of Position & Size? This would reduce duplication, though it would mean size: Coords args, which I’m not entirely sure about yet.

@airblast-dev
Copy link
Collaborator

Hmm I'm not sure how Position<X: Copy = u16, Y: Copy = X> would convey the information regarding which coordinate space we're in, unless I misunderstood something.

It doesn't really convey the information but we can create a type alias if we want to. This way we don't have to define new traits or duplicate a bunch of logic by other structs. Not saying we should do that as using separate types also has advantages. Just mentioning alternatives solutions.

How would you feel about having a Position with struct markers for Native, Twoxel, etc. and locking the x and y fields to i16? I don't mind custom coordinate spaces using other types, but I don't want someone using f32 with Twoxel for example.

We can just make twoxel functions accept a Position<i16> or introduce other bounds.

One idea: what if we used a single type for both positions and size args, e.g. Coords instead of Position & Size? This would reduce duplication, though it would mean size: Coords args, which I’m not entirely sure about yet.

Aside from the naming being confusing its blocks us from separating Position and Size behavior in the future. Some methods could only make sense on one of them. The field names would also be confusing.

@thehuglet
Copy link
Owner Author

thehuglet commented Feb 20, 2026

We could do type aliases for coord-spaces and Position<i16> for draw func params, though I'm worried about this solution being less explicit than dedicated generic markers. I'm worried about footguns here, as draw_text() and draw_octad() positions would take the same Position<i16> type for coordinates, but draw at different positions on screen. This is something Position<Native> and Position<Twoxel> generics with markers would fix.

After rewriting the examples I personally find that dedicated types helped a lot with avoiding logic errors.

I also don't really think we need traits for making this PR work with the approach I suggested. It would also reduce a lot of the repetition the current PR implementation suffers from.

Aside from the naming being confusing its blocks us from separating Position and Size behavior in the future. Some methods could only make sense on one of them. The field names would also be confusing.

Gotcha, no problem. We can keep them separate.


I'd like to try implementing the Position<T> and Size<T> with markers solution to see how it feels in practice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants