robotools / fontParts

The replacement for RoboFab
MIT License
132 stars 44 forks source link

Bounds Named Positions #704

Open connordavenport opened 10 months ago

connordavenport commented 10 months ago

Hey! Just dropping this here based on the Discord conversation! This only applies to glyphs' bounds but I liked @typesupply's suggestion for all bounds!

'''
fontParts add-on to get specific locations of a glyph's bounds
          10
     0    3     6  
       #######         
       #      #        
       #      #        
  9  1 ## 4 ##  7      
       #    #          
       #     #         
       #      #        
     2    5     8     
starting from top to bottom and left to right
0  leftTop
1  leftCenter
2  leftBottom
3  centerTop
4  centerCenter
5  centerBottom
6  rightTop
7  rightCenter
8  rightBottom
9  yCenter
10 xCenter
'''

def leftTop(self):
    xMin,yMin,xMax,yMax = self.bounds
    return (xMin,yMax)

def leftCenter(self):
    xMin,yMin,xMax,yMax = self.bounds
    x = xMin
    y = ((yMax - yMin) / 2) + yMin
    return (x,y)

def leftBottom(self):
    xMin,yMin,xMax,yMax = self.bounds
    return (xMin,Ymin)

def centerTop(self):
    xMin,yMin,xMax,yMax = self.bounds
    x = ((xMax - xMin) / 2) + xMin
    y = yMax
    return (x,y)

def centerCenter(self):
    xMin,yMin,xMax,yMax = self.bounds
    x = ((xMax - xMin) / 2) + xMin
    y = ((yMax - yMin) / 2) + yMin
    return (x,y)

def centerBottom(self):
    xMin,yMin,xMax,yMax = self.bounds
    x = ((xMax - xMin) / 2) + xMin
    y = yMin
    return (x,y)

def rightTop(self):
    xMin,yMin,xMax,yMax = self.bounds
    return (xMax,yMax)

def rightCenter(self):
    xMin,yMin,xMax,yMax = self.bounds
    x = xMax
    y = ((yMax - yMin) / 2) + yMin
    return (x,y)

def rightBottom(self):
    xMin,yMin,xMax,yMax = self.bounds
    return (xMax,yMin)

def yCenter(self):
    xMin,yMin,xMax,yMax = self.bounds
    y = ((yMax - yMin) / 2) + yMin
    return y

def xCenter(self):
    xMin,yMin,xMax,yMax = self.bounds
    x = ((xMax - xMin) / 2) + xMin
    return x

from mojo.roboFont import RGlyph

RGlyph.leftTop = leftTop
RGlyph.leftCenter = leftCenter
RGlyph.leftBottom = leftBottom
RGlyph.centerTop = centerTop
RGlyph.centerCenter = centerCenter
RGlyph.centerBottom = centerBottom
RGlyph.rightTop = rightTop
RGlyph.rightCenter = rightCenter
RGlyph.rightBottom = rightBottom
RGlyph.yCenter = yCenter
RGlyph.xCenter = xCenter
LettError commented 10 months ago

These are based on the bounds. Is there also a need for the same values, but based on the width, descender and ascender?

connordavenport commented 10 months ago

A g.width.center would be nice:)

LettError commented 10 months ago

as long as they're all read-only, right?

connordavenport commented 10 months ago

That was my idea, yes. Not sure if Tal had other ideas but I imagine a setter for this would be a bit weird haha

typesupply commented 10 months ago

Here's what I'm thinking of for bounds:

class Bounds(tuple):

    def _get_xMin(self):
        return self[0]

    def _get_xMax(self):
        return self[2]

    def _get_xCenter(self):
        return ((self[2] - self[0]) / 2) + self[0]

    def _get_yMin(self):
        return self[1]

    def _get_yMax(self):
        return self[3]

    def _get_yCenter(self):
        return ((self[3] - self[1]) / 2) + self[1]

    xMin = property(_get_xMin)
    xMax = property(_get_xMax)
    xCenter = property(_get_xCenter)
    yMin = property(_get_yMin)
    yMax = property(_get_yMax)
    yCenter = property(_get_yCenter)

glyph = CurrentGlyph()
bounds = Bounds(glyph.bounds)
print(bounds.xMin, bounds.xCenter)

All bounds properties would return a Bounds object. glyph.bounds, contour.bounds, component.bounds and any others.


Values based on metrics are complicated. We could add dynamic glyph.xCenter and glyph.yCenter properties that would avoid the vagaries of determining the relevant top and bottom font.info vertical metrics. Something like this:

    def _get_xCenter(self):
        return self.width / 2

    def _get_yCenter(self):
        return self.height / 2

    xCenter = property(_get_xCenter)
    yCenter = property(_get_yCenter)
typemytype commented 10 months ago

I voter for bounds being more then just a tuple! so they can be used in different places, it just has to be a subclass of Tuple

justvanrossum commented 10 months ago

Suggest to subclass typing.NamedTuple:

import typing

class Point(typing.NamedTuple):
    x: float
    y: float

class Rectangle(typing.NamedTuple):
    xMin: float
    yMin: float
    xMax: float
    yMax: float

    @property
    def center(self):
        return Point((self.xMin + self.xMax) / 2, (self.yMin + self.yMax) / 2)

b = Rectangle(10, 20, 30, 40)
print(b)
print(b.xMin)
print(b.center)

Output:

Rectangle(xMin=10, yMin=20, xMax=30, yMax=40)
10
Point(x=20.0, y=30.0)
typesupply commented 10 months ago

I thought about a named tuple, but didn't think I could subclass it. Here's a new sketch:

import typing

class Bounds(typing.NamedTuple):

    xMin: float
    yMin: float
    xMax: float
    yMax: float

    @property
    def width(self):
        return self.xMax - self.xMin

    @property
    def xCenter(self):
        return self.xMin + (self.width / 2)

    @property
    def height(self):
        return self.yMax - self.yMin

    @property
    def yCenter(self):
        return self.yMin + (self.height / 2)

glyph = CurrentGlyph()
bounds = Bounds(*glyph.bounds)
print(isinstance(bounds, tuple))
print(bounds.xMin)
print(bounds.width)
print(bounds.xCenter)

I'm still not convinced that we need the (x, y) combination values so I've left those off.


Also, I guess I'll finally learn about decorators now. 😬

LettError commented 10 months ago

Is this solving a problem, rather than just convenience?

typesupply commented 10 months ago

fontParts is mostly about making things easy for scripters, and I'm tired of doing bounds calculations so I see the need for this.

benkiel commented 10 months ago

I am all for this, yes: read only. PRs gratefully accepted!

benkiel commented 10 months ago

PRs with tests, joyfully accepted!