// // Copyright (c) 2008, Brian Frank and Andy Frank // Licensed under the Academic Free License version 3.0 // // History: // 12 Jun 2008 Brian Frank Creation (gfx version) // 29 Mar 2017 Brian Frank SVG/CSS changes // ************************************************************************** ** Point ************************************************************************** ** ** Point models a x,y coordinate. ** @Js @Serializable { simple = true } const class Point { ** Default instance is '0,0'. const static Point defVal := Point(0f, 0f) ** Construct with x, y. new make(Float x, Float y) { this.x = x; this.y = y } ** Construct with x, y. new makeInt(Int x, Int y) { this.x = x.toFloat; this.y = y.toFloat } ** Parse from comma or space separated string. ** If invalid then throw ParseErr or return null based on checked flag. static Point? fromStr(Str s, Bool checked := true) { try { f := GeomUtil.parseFloatList(s) return make(f[0], f[1]) } catch {} if (checked) throw ParseErr("Invalid Point: $s") return null } ** Return 'x+tx, y+ty' Point translate(Point t) { make(x+t.x, y+t.y) } ** Return hash of x and y. override Int hash() { x.hash.xor(y.hash.shiftl(16)) } ** Return if obj is same Point value. override Bool equals(Obj? obj) { that := obj as Point if (that == null) return false return this.x == that.x && this.y == that.y } ** Return '"x y"' override Str toStr() { GeomUtil.formatFloats2(x, y) } ** X coordinate const Float x ** Y coordinate const Float y } ************************************************************************** ** Size ************************************************************************** ** ** Size models the width and height of a shape. ** @Js @Serializable { simple = true } const class Size { ** Default instance is '0,0'. const static Size defVal := Size(0f, 0f) ** Construct with w, h. new make(Float w, Float h) { this.w = w; this.h = h } ** Construct with w, h as integers. new makeInt(Int w, Int h) { this.w = w.toFloat; this.h = h.toFloat } ** Parse from comma or space separated string. ** If invalid then throw ParseErr or return null based on checked flag. static Size? fromStr(Str s, Bool checked := true) { try { f := GeomUtil.parseFloatList(s) return make(f[0], f[1]) } catch {} if (checked) throw ParseErr("Invalid Size: $s") return null } ** Return '"w h"' override Str toStr() { GeomUtil.formatFloats2(w, h) } ** Return hash of w and h. override Int hash() { w.hash.xor(h.hash.shiftl(16)) } ** Return if obj is same Size value. override Bool equals(Obj? obj) { that := obj as Size if (that == null) return false return this.w == that.w && this.h == that.h } ** Width const Float w ** Height const Float h } ************************************************************************** ** Rect ************************************************************************** ** ** Represents the x,y coordinate and w,h size of a rectangle. ** @Js @Serializable { simple = true } const class Rect { ** Default instance is 0, 0, 0, 0. const static Rect defVal := Rect(0f, 0f, 0f, 0f) ** Construct with x, y, w, h. new make(Float x, Float y, Float w, Float h) { this.x = x; this.y = y this.w = w; this.h = h } ** Construct with x, y, w, h as integers. new makeInt(Int x, Int y, Int w, Int h) { this.x = x.toFloat; this.y = y.toFloat this.w = w.toFloat; this.h = h.toFloat } ** Construct from a Point and Size instance new makePosSize(Point p, Size s) { this.x = p.x; this.y = p.y this.w = s.w; this.h = s.h } ** Parse from comma or space separated string. ** If invalid then throw ParseErr or return null based on checked flag. static Rect? fromStr(Str s, Bool checked := true) { try { f := GeomUtil.parseFloatList(s) return make(f[0], f[1], f[2], f[3]) } catch {} if (checked) throw ParseErr("Invalid Rect: $s") return null } ** Get the x, y coordinate of this rectangle. Point pos() { Point(x, y) } ** Get the w, h size of this rectangle. Size size() { Size(w, h) } ** Return true if x,y is inside the bounds of this rectangle. Bool contains(Point pt) { return pt.x >= this.x && pt.x <= this.x+w && pt.y >= this.y && pt.y <= this.y+h } ** Return true if this rectangle intersects any portion of that rectangle Bool intersects(Rect that) { ax1 := this.x; ay1 := this.y; ax2 := ax1 + this.w; ay2 := ay1 + this.h bx1 := that.x; by1 := that.y; bx2 := bx1 + that.w; by2 := by1 + that.h return !(ax2 <= bx1 || bx2 <= ax1 || ay2 <= by1 || by2 <= ay1) } ** Compute the intersection between this rectangle and that rectangle. ** If there is no intersection, then return `defVal`. Rect intersection(Rect that) { ax1 := this.x; ay1 := this.y; ax2 := ax1 + this.w; ay2 := ay1 + this.h bx1 := that.x; by1 := that.y; bx2 := bx1 + that.w; by2 := by1 + that.h rx1 := ax1.max(bx1); rx2 := ax2.min(bx2) ry1 := ay1.max(by1); ry2 := ay2.min(by2) rw := rx2 - rx1 rh := ry2 - ry1 if (rw <= 0f || rh <= 0f) return defVal return make(rx1, ry1, rw, rh) } ** Compute the union between this rectangle and that rectangle, ** which is the bounding box that exactly contains both rectangles. Rect union(Rect that) { ax1 := this.x; ay1 := this.y; ax2 := ax1 + this.w; ay2 := ay1 + this.h bx1 := that.x; by1 := that.y; bx2 := bx1 + that.w; by2 := by1 + that.h rx1 := ax1.min(bx1); rx2 := ax2.max(bx2) ry1 := ay1.min(by1); ry2 := ay2.max(by2) rw := rx2 - rx1 rh := ry2 - ry1 if (rw <= 0f || rh <= 0f) return defVal return make(rx1, ry1, rw, rh) } ** Return '"x y w h"' override Str toStr() { return GeomUtil.formatFloats4(x, y, w, h) } ** Return hash of x, y, w, and h. override Int hash() { x.hash.xor(y.hash.shiftl(8)).xor(w.hash.shiftl(16)).xor(w.hash.shiftl(24)) } ** Return if obj is same Rect value. override Bool equals(Obj? obj) { that := obj as Rect if (that == null) return false return this.x == that.x && this.y == that.y && this.w == that.w && this.h == that.h } ** X coordinate const Float x ** Y coordinate const Float y ** Width const Float w ** Height const Float h } ************************************************************************** ** Insets ************************************************************************** ** ** Insets represents spacing around the edge of a rectangle. ** @Js @Serializable { simple = true } const class Insets { ** Default instance 0, 0, 0, 0. const static Insets defVal := Insets(0f, 0f, 0f, 0f) ** ** Construct with top, and optional right, bottom, left. If one side ** is not specified, it is reflected from the opposite side: ** ** Insets(5) => Insets(5,5,5,5) ** Insets(5,6) => Insets(5,6,5,6) ** Insets(5,6,7) => Insets(5,6,7,6) ** new make(Num top, Num? right := null, Num? bottom := null, Num? left := null) { if (right == null) right = top if (bottom == null) bottom = top if (left == null) left = right this.top = top.toFloat this.right = right.toFloat this.bottom = bottom.toFloat this.left = left.toFloat } ** Parse from comma or space separated string using CSS format: ** - "top" ** - "top, right" (implies bottom = top, left = right) ** - "top, right, bottom" (implies left = right) ** - "top, right, bottom, left" static Insets? fromStr(Str s, Bool checked := true) { try { f := GeomUtil.parseFloatList(s) return make(f[0], f.getSafe(1), f.getSafe(2), f.getSafe(3)) } catch (Err e) {} if (checked) throw ParseErr("Invalid Insets: $s") return null } ** If all four sides are equal return '"len"' ** otherwise return '"top right bottom left"'. override Str toStr() { if (top == right && top == bottom && top == left) return GeomUtil.formatFloat(top) else return GeomUtil.formatFloats4(top, right, bottom, left) } ** Return hash of top, right, bottom, left. override Int hash() { top.hash.xor(right.hash.shiftl(8)).xor(bottom.hash.shiftl(16)).xor(left.hash.shiftl(24)) } ** Return if obj is same Insets value. override Bool equals(Obj? obj) { that := obj as Insets if (that == null) return false return this.top == that.top && this.right == that.right && this.bottom == that.bottom && this.left == that.left } ** Return right+left, top+bottom Size toSize() { Size(right+left, top+bottom) } ** Top side spacing const Float top ** Right side spacing const Float right ** Bottom side spacing const Float bottom ** Left side spacing const Float left ** Left plus right Float w() { left + right } ** Top plus bottom Float h() { top + bottom } } ************************************************************************** ** GeomUtil ************************************************************************** @NoDoc @Js const class GeomUtil { ** Split with comma or whitespace CSS/SVG styled syntax static Str[] split(Str s) { acc := Str[,] start := 0 for (i := 0; i<s.size; ++i) { c := s[i] if (c == ' ' || c == ',') { if (start < i) acc.add(s[start..<i]) start = i+1 } } if (start < s.size) acc.add(s[start..-1]) return acc } ** Parse list comma or whitespace separated floats static Float[] parseFloatList(Str s) { split(s).map |tok| { tok.trim.toFloat } } ** Format two floats to space separated string static Str formatFloats2(Float a, Float b) { StrBuf() .add(formatFloat(a)).addChar(' ') .add(formatFloat(b)).toStr } ** Format four floats to space separated string static Str formatFloats4(Float a, Float b, Float c, Float d) { StrBuf() .add(formatFloat(a)).addChar(' ') .add(formatFloat(b)).addChar(' ') .add(formatFloat(c)).addChar(' ') .add(formatFloat(d)).toStr } ** Format float to string static Str formatFloat(Float f) { f.toLocale("0.###") } }