Compare commits

...

4 Commits

Author SHA1 Message Date
89c23a8947 Add test example 2024-09-10 15:52:56 -04:00
0cb6e28542 Add drawing functions 2024-09-10 15:52:46 -04:00
5dee53b8a9 Add incomplete TypeSetter struct 2024-09-10 15:52:33 -04:00
9c7732c95b Add incomplete reflow stage 2024-09-10 15:52:18 -04:00
5 changed files with 684 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
output.png

103
draw.go Normal file
View File

@ -0,0 +1,103 @@
package typeset
import "image"
import "unicode"
import "image/draw"
import "image/color"
import "golang.org/x/image/font"
import "golang.org/x/image/math/fixed"
// Draw draws the contents of a TypeSetter to an image at the given offset. It
// returns a rectangle containing all pixels in the image that were updated.
func Draw (destination draw.Image, setter *TypeSetter, offset fixed.Point26_6, col color.Color) image.Rectangle {
source := image.NewUniform(col)
face := setter.Face()
var updatedRegion image.Rectangle
bounds := destination.Bounds()
setter.Runes()(func (position fixed.Point26_6, run rune) bool {
// leave empty space for space characters
if unicode.IsSpace(run) {
return true
}
dot := offset.Add(position)
destinationRectangle, mask, maskPoint, _, ok := face.Glyph(dot, run)
if ok {
// don't bother drawing runes that are out of bounds
if destinationRectangle.Min.Y > bounds.Max.Y { return false }
if destinationRectangle.Intersect(bounds).Empty() { return true }
// draw rune
draw.DrawMask (
destination,
destinationRectangle,
source, image.Point { },
mask, maskPoint,
draw.Over)
} else {
// draw tofu
drawTofu(run, destination, dot, face, col)
}
updatedRegion = updatedRegion.Union(destinationRectangle)
return true
})
return updatedRegion
}
// DrawBounds draws the LayoutBounds, MinimumSize, and LayoutBoundsSpace of a
// TypeSetter to the given image.
func DrawBounds (destination draw.Image, setter *TypeSetter, offset fixed.Point26_6) {
blue := color.RGBA { B: 255, A: 255 }
red := color.RGBA { R: 255, A: 255 }
green := color.RGBA { G: 255, A: 255 }
layoutBoundsSpace := setter.LayoutBoundsSpace()
layoutBounds := setter.LayoutBounds()
minimum := setter.MinimumSize()
minimumRect := roundRect(fixed.Rectangle26_6 { Max: minimum }.Add(offset).Add(layoutBounds.Min))
drawRectangleOutline(destination, minimumRect, green)
drawRectangleOutline(destination, roundRect(layoutBoundsSpace.Add(offset)), blue)
drawRectangleOutline(destination, roundRect(layoutBounds.Add(offset)), red)
}
func drawTofu (
char rune,
destination draw.Image,
position fixed.Point26_6,
face font.Face,
col color.Color,
) {
bounds, _ := tofuBounds(face)
rectBounds := roundRect(bounds).Add(image.Pt (
position.X.Round(),
position.Y.Round()))
drawRectangleOutline(destination, rectBounds, col)
}
func drawRectangleOutline (destination draw.Image, bounds image.Rectangle, col color.Color) {
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
destination.Set(x, bounds.Min.Y, col)
}
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
destination.Set(bounds.Min.X, y, col)
destination.Set(bounds.Max.X - 1, y, col)
}
for x := bounds.Min.X; x < bounds.Max.X; x ++ {
destination.Set(x, bounds.Max.Y - 1, col)
}
}
func roundRect (rectangle fixed.Rectangle26_6) image.Rectangle {
return image.Rect (
rectangle.Min.X.Round(),
rectangle.Min.Y.Round(),
rectangle.Max.X.Round(),
rectangle.Max.Y.Round())
}

162
examples/test/main.go Normal file
View File

@ -0,0 +1,162 @@
// Example test demonstrates a variety of ways that TypeSetter can arrange text.
package main
import "os"
import "image"
import "image/png"
import "image/draw"
import "image/color"
import "golang.org/x/image/math/fixed"
import "git.tebibyte.media/tomo/typeset"
import "golang.org/x/image/font/basicfont"
func main () {
img := image.NewRGBA(image.Rect(0, 0, 2048, 1024))
setter := typeset.TypeSetter { }
setter.SetWrap(true)
setter.SetFace(basicfont.Face7x13)
setter.SetText(lipsum)
setter.SetAlign(typeset.AlignStart, typeset.AlignStart)
drawText(img, &setter, image.Rect( 0, 0, 256, 256).Inset(4))
setter.SetAlign(typeset.AlignMiddle, typeset.AlignStart)
drawText(img, &setter, image.Rect(256, 0, 512, 256).Inset(4))
setter.SetAlign(typeset.AlignEnd, typeset.AlignStart)
drawText(img, &setter, image.Rect( 0, 256, 256, 512).Inset(4))
setter.SetAlign(typeset.AlignEven, typeset.AlignStart)
drawText(img, &setter, image.Rect(256, 256, 512, 512).Inset(4))
setter.SetText(dimple)
setter.SetAlign(typeset.AlignStart, typeset.AlignStart)
drawText(img, &setter, image.Rect( 0, 0, 256, 256).Add(image.Pt(512, 0)).Inset(4))
setter.SetAlign(typeset.AlignMiddle, typeset.AlignStart)
drawText(img, &setter, image.Rect(256, 0, 512, 256).Add(image.Pt(512, 0)).Inset(4))
setter.SetAlign(typeset.AlignEnd, typeset.AlignStart)
drawText(img, &setter, image.Rect( 0, 256, 256, 512).Add(image.Pt(512, 0)).Inset(4))
setter.SetAlign(typeset.AlignEven, typeset.AlignStart)
drawText(img, &setter, image.Rect(256, 256, 512, 512).Add(image.Pt(512, 0)).Inset(4))
setter.SetAlign(typeset.AlignStart, typeset.AlignMiddle)
drawText(img, &setter, image.Rect( 0, 0, 256, 256).Add(image.Pt(0, 512)).Inset(4))
setter.SetAlign(typeset.AlignMiddle, typeset.AlignMiddle)
drawText(img, &setter, image.Rect(256, 0, 512, 256).Add(image.Pt(0, 512)).Inset(4))
setter.SetAlign(typeset.AlignEnd, typeset.AlignMiddle)
drawText(img, &setter, image.Rect( 0, 256, 256, 512).Add(image.Pt(0, 512)).Inset(4))
setter.SetAlign(typeset.AlignEven, typeset.AlignMiddle)
drawText(img, &setter, image.Rect(256, 256, 512, 512).Add(image.Pt(0, 512)).Inset(4))
setter.SetAlign(typeset.AlignStart, typeset.AlignEnd)
drawText(img, &setter, image.Rect( 0, 0, 256, 256).Add(image.Pt(512, 512)).Inset(4))
setter.SetAlign(typeset.AlignMiddle, typeset.AlignEnd)
drawText(img, &setter, image.Rect(256, 0, 512, 256).Add(image.Pt(512, 512)).Inset(4))
setter.SetAlign(typeset.AlignEnd, typeset.AlignEnd)
drawText(img, &setter, image.Rect( 0, 256, 256, 512).Add(image.Pt(512, 512)).Inset(4))
setter.SetAlign(typeset.AlignEven, typeset.AlignEnd)
drawText(img, &setter, image.Rect(256, 256, 512, 512).Add(image.Pt(512, 512)).Inset(4))
setter.SetText(haiku)
setter.SetWrap(false)
setter.SetAlign(typeset.AlignStart, typeset.AlignStart)
drawText(img, &setter, image.Rect( 0, 0, 256, 256).Add(image.Pt(1024, 0)).Inset(4))
setter.SetAlign(typeset.AlignMiddle, typeset.AlignStart)
drawText(img, &setter, image.Rect(256, 0, 512, 256).Add(image.Pt(1024, 0)).Inset(4))
setter.SetAlign(typeset.AlignEnd, typeset.AlignStart)
drawText(img, &setter, image.Rect( 0, 256, 256, 512).Add(image.Pt(1024, 0)).Inset(4))
setter.SetAlign(typeset.AlignEven, typeset.AlignStart)
drawText(img, &setter, image.Rect(256, 256, 512, 512).Add(image.Pt(1024, 0)).Inset(4))
setter.SetAlign(typeset.AlignStart, typeset.AlignMiddle)
drawText(img, &setter, image.Rect( 0, 0, 256, 256).Add(image.Pt(1536, 0)).Inset(4))
setter.SetAlign(typeset.AlignMiddle, typeset.AlignMiddle)
drawText(img, &setter, image.Rect(256, 0, 512, 256).Add(image.Pt(1536, 0)).Inset(4))
setter.SetAlign(typeset.AlignEnd, typeset.AlignMiddle)
drawText(img, &setter, image.Rect( 0, 256, 256, 512).Add(image.Pt(1536, 0)).Inset(4))
setter.SetAlign(typeset.AlignEven, typeset.AlignMiddle)
drawText(img, &setter, image.Rect(256, 256, 512, 512).Add(image.Pt(1536, 0)).Inset(4))
setter.SetAlign(typeset.AlignStart, typeset.AlignEnd)
drawText(img, &setter, image.Rect( 0, 0, 256, 256).Add(image.Pt(1024, 512)).Inset(4))
setter.SetAlign(typeset.AlignMiddle, typeset.AlignEnd)
drawText(img, &setter, image.Rect(256, 0, 512, 256).Add(image.Pt(1024, 512)).Inset(4))
setter.SetAlign(typeset.AlignEnd, typeset.AlignEnd)
drawText(img, &setter, image.Rect( 0, 256, 256, 512).Add(image.Pt(1024, 512)).Inset(4))
setter.SetAlign(typeset.AlignEven, typeset.AlignEnd)
drawText(img, &setter, image.Rect(256, 256, 512, 512).Add(image.Pt(1024, 512)).Inset(4))
setter.SetText(haikuAlt)
setter.SetAlign(typeset.AlignEven, typeset.AlignStart)
drawText(img, &setter, image.Rect( 0, 0, 256, 256).Add(image.Pt(1536, 512)).Inset(4))
setter.SetAlign(typeset.AlignEven, typeset.AlignMiddle)
drawText(img, &setter, image.Rect(256, 0, 512, 256).Add(image.Pt(1536, 512)).Inset(4))
setter.SetAlign(typeset.AlignEven, typeset.AlignEnd)
drawText(img, &setter, image.Rect( 0, 256, 256, 512).Add(image.Pt(1536, 512)).Inset(4))
file, err := os.Create("output.png")
if err != nil { panic(err) }
defer file.Close()
err = png.Encode(file, img)
if err != nil { panic(err) }
}
type subDrawImage interface {
draw.Image
SubImage (image.Rectangle) image.Image
}
func drawText (destination subDrawImage, setter *typeset.TypeSetter, bounds image.Rectangle) {
whiteRectangle(destination, bounds)
subImage := destination.SubImage(bounds).(draw.Image)
metrics := setter.Face().Metrics()
bounds = bounds.Inset(16)
grayRectangle(destination, bounds)
size := fixed.P(bounds.Dx(), bounds.Dy())
setter.SetSize(size)
offset := fixed.Point26_6 {
X: fixed.I(bounds.Min.X),
Y: metrics.Ascent + fixed.I(bounds.Min.Y),
}
// typeset.Draw(destination, setter, offset, color.Black)
typeset.DrawBounds(subImage, setter, offset)
typeset.Draw(subImage, setter, offset, color.Black)
}
func whiteRectangle (destination draw.Image, rect image.Rectangle) {
draw.Over.Draw(destination, rect, image.NewUniform(color.RGBA { R: 150, G: 150, B: 150, A: 255 }), image.Pt(0, 0))
}
func grayRectangle (destination draw.Image, rect image.Rectangle) {
draw.Over.Draw(destination, rect, image.NewUniform(color.RGBA { R: 200, G: 200, B: 200, A: 255 }), image.Pt(0, 0))
}
const lipsum = `Eum officia beatae harum. Rem aut praesentium possimus dignissimos ea sed. Recusandae sint rerum ut. Qui delectus rerum ut ut. Nobis non veritatis consequatur quia explicabo id. Et aut qui reiciendis esse voluptatem.
Eaque rem incidunt porro unde quia expedita quia. Deleniti repellat modi placeat. Et beatae aut voluptatem. Veritatis perspiciatis et aperiam sit modi sequi.
Accusantium et fugit expedita consequatur incidunt explicabo ea voluptatibus. Debitis consectetur veniam ut et esse aspernatur. Quas occaecati explicabo consequuntur. Quae dolorem ea harum ut tempora. Corporis ducimus et voluptatem. Corporis distinctio quia velit accusantium sunt omnis.
Libero blanditiis aut aut exercitationem modi. Eum corporis quam facere. Perferendis sit nulla et qui repellat eaque neque in. Expedita quidem similique sunt delectus similique non assumenda.
Hic rerum earum sapiente et itaque harum. Itaque amet voluptatem aliquid. Et qui excepturi animi voluptatem debitis necessitatibus atque animi. Nemo voluptates delectus quisquam non. Ipsam error voluptas similique dolores odit quos.`
const dimple = `I have been trying to remember this for a while
I could never place it
My brain kept saying Dimple when I tried to remember. Ever closer yet unobtainable. Censored, even. It did not want me to remember the dumple...`
const haiku = `An ocean voyage.
As waves break over the bow,
the sea welcomes me.
This is a very long line that will probably get cut off.`
const haikuAlt = `An ocean voyage.
As waves break over the bow,
the sea welcomes me.
This is a short ending`

191
flow.go Normal file
View File

@ -0,0 +1,191 @@
package typeset
import "golang.org/x/image/font"
import "golang.org/x/image/math/fixed"
// TODO perhaps follow https://unicode.org/reports/tr14/
func reflow (
tokens []token,
face font.Face, size fixed.Point26_6,
wrap bool, xAlign, yAlign Align,
) (
extents, extentsSpace fixed.Rectangle26_6,
minimumSize fixed.Point26_6,
) {
if len(tokens) == 0 { return }
metrics := face.Metrics()
var dot fixed.Point26_6
lineStart := 0
lineEnd := 0
lastWord := 0
lastToken := 0
nLines := 0
newline := func () {
// if the line isn't empty
if lineStart != lineEnd {
// align line
alignLine (
tokens[lineStart:lineEnd],
size.X, xAlign, lineEnd == len(tokens))
// calculate extents
lastWordTok := tokens[lastWord]
lastTokenTok := tokens[lastToken]
lineMax := lastWordTok.position.X + lastWordTok.width
lineMaxSpace := lastTokenTok.position.X + lastTokenTok.width
if lineMax > minimumSize.X { minimumSize.X = lineMax }
if lineMaxSpace > extentsSpace.Max.X { extentsSpace.Max.X = lineMaxSpace }
}
// update dot
dot.Y += metrics.Height
dot.X = 0
// update indices, counts
lineStart = lineEnd
lastWord = lineEnd
nLines ++
}
// for each line, arrange and align while calculating effective
// bounds/extents
sawLineBreak := false
for index, token := range tokens {
lineEnd = index
updateIndices := func () {
lastToken = index
if token.kind == tokenKindWord {
lastWord = index
}
}
// demarcate lines
if sawLineBreak {
newline()
sawLineBreak = false
}
if token.kind == tokenKindLineBreak {
updateIndices()
tokens[index].position = dot
sawLineBreak = true
} else {
needWrap :=
wrap &&
token.kind == tokenKindWord &&
dot.X + token.width > size.X
if needWrap {
newline()
}
updateIndices()
tokens[index].position = dot
dot.X += token.width
}
}
lineEnd ++ // make lineEnd equal to len(tokens)
newline()
minimumSize.Y = metrics.Height * fixed.Int26_6(nLines) + metrics.Descent
// second, vertical alignment pass
alignLinesVertically(tokens, size.Y, minimumSize.Y, yAlign)
// calculate extents
extentsOffset := fixed.Point26_6 { Y: metrics.Ascent - tokens[0].position.Y }
extents.Max.X = minimumSize.X
extents.Max.Y = dot.Y + metrics.Descent
extentsSpace.Max.Y = dot.Y + metrics.Descent
extents = extents.Sub(extentsOffset)
extentsSpace = extentsSpace.Sub(extentsOffset)
return
}
func alignLinesVertically (tokens []token, height, contentHeight fixed.Int26_6, align Align) {
if len(tokens) == 0 { return }
if align == AlignStart { return }
var topOffset fixed.Int26_6
switch align {
case AlignMiddle: topOffset = (height - contentHeight) / 2
case AlignEnd, AlignEven: topOffset = height - contentHeight
}
for index := range tokens {
tokens[index].position.Y += topOffset
}
}
func alignLine (tokens []token, width fixed.Int26_6, align Align, atEnd bool) {
if len(tokens) == 0 { return }
if align == AlignStart { return }
if align == AlignEven {
alignLineJustify(tokens, width, atEnd)
return
}
var leftOffset fixed.Int26_6
contentWidth := lineContentWidth(tokens)
switch align {
case AlignMiddle: leftOffset = (width - contentWidth) / 2
case AlignEnd: leftOffset = width - contentWidth
}
for index := range tokens {
tokens[index].position.X += leftOffset
}
}
func alignLineJustify (tokens []token, width fixed.Int26_6, atEnd bool) {
cantJustify :=
len(tokens) < 2 ||
atEnd ||
tokens[len(tokens) - 1].kind == tokenKindLineBreak
if cantJustify {
alignLine(tokens, width, AlignStart, atEnd)
return
}
contentWidth, wordCount := lineContentWordWidth(tokens)
spaceCount := wordCount - 1
if spaceCount == 0 { return }
spacePerWord := (width - contentWidth) / fixed.Int26_6(spaceCount)
var x fixed.Int26_6
for index, token := range tokens {
if token.kind == tokenKindWord {
tokens[index].position.X = x
x += spacePerWord + token.width
} else {
tokens[index].position.X = x
}
}
}
func lineContentWordWidth (tokens []token) (fixed.Int26_6, int) {
var width fixed.Int26_6
var count int
for _, token := range tokens {
if token.kind == tokenKindWord {
width += token.width
count ++
}
}
return width, count
}
func lineContentWidth (tokens []token) fixed.Int26_6 {
var width, spaceWidth fixed.Int26_6
for _, token := range tokens {
if token.kind == tokenKindWord {
width += spaceWidth + token.width
spaceWidth = 0
} else {
spaceWidth = token.width
}
}
return width
}

227
typesetter.go Normal file
View File

@ -0,0 +1,227 @@
package typeset
import "fmt"
import "strconv"
import "golang.org/x/image/font"
import "golang.org/x/image/math/fixed"
type validationLevel int; const (
validationLevelNone validationLevel = iota
validationLevelTokens
validationLevelMeasurement
validationLevelFlow
)
type tokenKind int; const (
tokenKindWord tokenKind = iota // contains everything that isn't:
tokenKindSpace // only unicode space runes, except \r or \n
tokenKindTab // only \t runes
tokenKindLineBreak // either "\n", or "\r\n"
)
func (kind tokenKind) String () string {
switch kind {
case tokenKindWord: return "Word"
case tokenKindSpace: return "Space"
case tokenKindTab: return "Tab"
case tokenKindLineBreak: return "LineBreak"
}
return fmt.Sprintf("typeset.tokenKind(%d)", kind)
}
type token struct {
kind tokenKind
width fixed.Int26_6
position fixed.Point26_6
runes []runeLayout
}
func (tok token) String () string {
str := ""
for _, runl := range tok.runes {
str += string(runl.run)
}
return fmt.Sprintf (
"%v:%v{%v,%v-%v}",
tok.kind, strconv.Quote(str),
tok.position.X, tok.position.Y, tok.width)
}
type runeLayout struct {
x fixed.Int26_6
run rune
}
func (run runeLayout) String () string {
return fmt.Sprintf("%s-{%v}", strconv.Quote(string([]rune { run.run })), run.x)
}
// RuneIter is an iterator that iterates over positioned runes.
type RuneIter func (yield func(fixed.Point26_6, rune) bool)
// Align specifies a text alignment method.
type Align int; const (
// X | Y
AlignStart Align = iota // left | top
AlignMiddle // center | center
AlignEnd // right | bottom
AlignEven // justified | (unsupported)
)
// TypeSetter manages text, and can perform layout operations on it. It
// automatically avoids performing redundant work. It has no constructor and its
// zero value can be used safely, but it must not be copied after first use.
type TypeSetter struct {
text string
runes []runeLayout
tokens []token
validationLevel validationLevel
xAlign, yAlign Align
face font.Face
size fixed.Point26_6 // width, height
wrap bool
minimumSize fixed.Point26_6
layoutBounds fixed.Rectangle26_6
layoutBoundsSpace fixed.Rectangle26_6
}
// Runes returns an iterator for all runes in the TypeSetter, and thier positions.
func (this *TypeSetter) Runes () RuneIter {
this.needFlow()
return func (yield func (fixed.Point26_6, rune) bool) {
for _, token := range this.tokens {
for _, runl := range token.runes {
pos := token.position
pos.X += runl.x
if !yield(pos, runl.run) { return }
}
}
}
}
// Em returns the width of one emspace according to the typesetter's typeface,
// which is the width of the capital letter 'M'.
func (this *TypeSetter) Em () fixed.Int26_6 {
if this.face == nil { return 0 }
width, _ := this.face.GlyphAdvance('M')
return width
}
// MinimumSize returns the minimum width and height needed to display text. If
// wrapping is enabled, this method will return { X: Em(), Y: 0 }.
func (this *TypeSetter) MinimumSize () fixed.Point26_6 {
if this.wrap { return fixed.Point26_6{ X: this.Em(), Y: 0 } }
this.needFlow()
return this.minimumSize
}
// LayoutBounds returns the semantic bounding box of the text. The origin point
// (0, 0) of the rectangle corresponds to the origin of the first line's
// baseline.
func (this *TypeSetter) LayoutBounds () fixed.Rectangle26_6 {
this.needFlow()
return this.layoutBounds
}
// LayoutBoundsSpace is like LayoutBounds, but it also takes into account the
// trailing whitespace at the end of each line (if it exists).
func (this *TypeSetter) LayoutBoundsSpace () fixed.Rectangle26_6 {
this.needFlow()
return this.layoutBoundsSpace
}
// PositionAt returns the position of the rune at the specified index.
func (this *TypeSetter) PositionAt (index int) fixed.Point26_6 {
idx := 0
var position fixed.Point26_6
this.Runes()(func (pos fixed.Point26_6, run rune) bool {
if index == idx {
position = pos
return false
}
idx ++
return true
})
return position
}
// SetText sets the text of the TypeSetter.
func (this *TypeSetter) SetText (text string) {
if this.text == text { return }
this.text = text
this.invalidate(validationLevelTokens)
}
// SetSize sets the width and height of the TypeSetter.
func (this *TypeSetter) SetSize (size fixed.Point26_6) {
if this.size == size { return }
this.size = size
this.invalidate(validationLevelFlow)
}
// SetWrap sets whether the text will wrap to the width specified by SetSize.
func (this *TypeSetter) SetWrap (wrap bool) {
if this.wrap == wrap { return }
this.wrap = wrap
this.invalidate(validationLevelFlow)
}
// SetAlign sets the horizontal and vertical alignment of the text.
func (this *TypeSetter) SetAlign (x, y Align) {
if this.xAlign == x && this.yAlign == y { return }
this.xAlign = x
this.yAlign = y
this.invalidate(validationLevelFlow)
}
// Face returns the font face as set by SetFace.
func (this *TypeSetter) Face () font.Face {
return this.face
}
// SetFace sets the font face the text will be laid out according to.
func (this *TypeSetter) SetFace (face font.Face) {
if this.face == face { return }
this.face = face
this.invalidate(validationLevelMeasurement)
}
func (this *TypeSetter) needTokens () {
if this.valid(validationLevelTokens) { return }
this.runes, this.tokens = parseString(this.text)
this.validate(validationLevelTokens)
}
func (this *TypeSetter) needMeasurement () {
if this.valid(validationLevelMeasurement) { return }
this.needTokens()
measure(this.tokens, this.face)
this.validate(validationLevelMeasurement)
}
func (this *TypeSetter) needFlow () {
if this.valid(validationLevelFlow) { return }
this.needMeasurement()
this.layoutBounds, this.layoutBoundsSpace, this.minimumSize = reflow (
this.tokens,
this.face, this.size,
this.wrap, this.xAlign, this.yAlign)
this.validate(validationLevelFlow)
}
func (this *TypeSetter) validate (level validationLevel) {
this.validationLevel = level
}
func (this *TypeSetter) invalidate (level validationLevel) {
if this.valid(level) {
this.validationLevel = level - 1
}
}
func (this *TypeSetter) valid (level validationLevel) bool {
return this.validationLevel >= level
}