/* Copyright 2010 Aaron J. Radke Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cc.drx trait Shape{ //def draw(implicit c:DrawContext):Unit /**Attach a style to a shape*/ def ~(style:Style):Shape.Styled = Shape.Styled(this,style) /**initialize shape with a style */ def ~(property:Style.Property):Shape.Styled = Shape.Styled(this,Style()~property) /** provide a quick automatic way to set a fill or stroke * all shapes have a stroke even if they can be filled (but the ClosedShape overrides this to provide a fill color) */ def ~(color:Color):Shape.Styled = this ~ Style.Stroke(color) //default is stroke color... /** generic draw method*/ def draw(implicit g:DrawContext):Unit } object Shape{ /**these are shapes that have an area and default to coloring by area fill rather than stroke*/ trait Open extends Shape { override def ~(color:Color):Styled = this ~ Style.Stroke(color) } trait Closed extends Shape { override def ~(color:Color):Styled = this ~ Style.Fill(color) } case class Styled(shape:Shape,style:Style) extends Shape{ override def ~(mergeStyle:Style):Styled = Styled(shape, style ~ mergeStyle) override def ~(addProperty:Style.Property):Styled = Styled(shape, style ~ addProperty) def draw(implicit g:DrawContext):Unit = g ! this } case class Grouped(shapes:Iterable[Shape]) extends Shape{ def draw(implicit g:DrawContext):Unit = g ! this } lazy val empty = new Empty class Empty extends Shape{ def draw(implicit g:DrawContext):Unit = () } } //TODO possibly remove this in favor of a speciallized unit draw function abstract class DrawContext{ import Style._ //---mutable state variables private var horzAlign:AlignHorizontal = Center private var vertAlign:AlignVertical = Midline private def align() = this ! Align(horzAlign,vertAlign) //---Required //--Query def size:Vec def screen:Rect = Rect(Vec(0,0), size) //== events to values //-- mutable event values //-- raw events private var _lastClick:Option[Vec] = None private var _lastPress:Option[Vec] = None // TODO private var _lastPressButton:Option[Style.HorizontalAlign] = None //Left or Right private var _lastRelease:Option[Vec] = None private var _lastDrag:Option[Line] = None private var _lastTouchPressed:List[Vec] = Nil private var _lastTouchMoved:List[Vec] = Nil private var _lastTouchReleased:List[Vec] = Nil private var _lastScrollDelta:Option[Vec] = None //--drx derived private var _mouse:Option[Vec] = None private var _press:Option[Vec] = None private var _drag:Option[Line] = None private var _touch:List[Line] = Nil private var _onRelease:Option[Vec => Unit] = None private var _scrollSum:Vec = Vec.zero private var _focus:Boolean = false //-- functional access to event values def lastClick:Option[Vec] = _lastClick def lastPress:Option[Vec] = _lastPress def lastRelease:Option[Vec] = _lastRelease def lastDrag:Option[Line] = _lastDrag def mouseOption:Option[Vec] = _mouse def mouse:Vec = _mouse getOrElse Vec(-1,-1) //a default offscreen location if not specified def press:Option[Vec] = _press def drag:Option[Line] = _drag def touch:List[Line] = _touch def scrollSum:Vec = _scrollSum // def scroll:Option[Vec] = _lastScrollDelta def focus:Boolean = _focus //def wheel = TODO //-- call the following to implement event value states private[drx] def mouseMoved(v:Vec):Unit = {_mouse = Some(v)} private[drx] def mouseClicked(v:Vec):Unit = _lastClick = Some(v) private[drx] def mousePressed(v:Vec):Unit = _press = Some(v) def onRelease(f: Vec => Unit):Unit = _onRelease = Some(f) //TODO make this a onRelease stack so more than one click is allowed def onInit(f: => Unit):Unit = if(frame == 0) f private[drx] def mouseReleased(v:Vec):Unit = { _lastRelease = Some(v) _drag = None _press = None for(f <- _onRelease) f(v) } private[drx] def mouseDragged(v:Vec):Unit = { _drag match { case Some(Line(a,_)) => _drag = Some(Line(a,v)) //update new drag point v from starting point a case _ => _drag = Some(Line(_lastPress getOrElse v,v)) } _lastDrag = _drag } private[drx] def touchPressed(vs:List[Vec]):Unit = { _lastTouchPressed = vs _touch = vs zip vs map {case (a,b) => a lineTo b} } private[drx] def touchMoved(vs:List[Vec]):Unit = { _lastTouchMoved = vs _touch = _lastTouchPressed zip vs map {case (a,b) => a lineTo b} } private[drx] def touchReleased(vs:List[Vec]):Unit = { _lastTouchReleased = vs _touch = Nil } private[drx] def scrollMoved(delta:Vec):Unit = { _lastScrollDelta = Some(delta) _scrollSum = _scrollSum + delta } private[drx] def focusChanged(v:Boolean):Unit = { _focus = v if(!focus) keyboard.clearHeld() //since alt-tab loses focus this is needed to make sure alt is not held (similar for meta-windows and other switch mechanisms) } //TODO FIXME test these drag features private var _drop:Option[String] = None def drop:Option[String] = _drop private[drx] def dragOver(str:String):Unit = _drop = Some(str) private[drx] def dragDropped(str:String):Unit = _drop = Some(str) private var _frame:Long = 0L private var _frameNanos:Long = 1L private var _lastFrameNanos = System.nanoTime() final private[drx] def frameFinished():Unit = { val nowNanos = System.nanoTime() _frameNanos = nowNanos - _lastFrameNanos //dt _frame += 1 // Log(nowNanos, _lastFrameNanos, _frame, _frameNanos, fps, 1E9 / _frameNanos) _lastFrameNanos = nowNanos } final def frame:Long = _frame final def fps:Int = (1E9 / _frameNanos).round.toInt // (ns/frame).inv => frame/ns * n/1E-9 * 1count/frame => 1E9/ns final def frameRate:Frequency = Frequency(1E9 / _frameNanos) implicit val keyboard:Keyboard = new Keyboard(Keyboard.lookupJavaFX) //--add default key bindings keyboard.on("print"){ //print screen should export to a screen save match { case Success(f) => println(s"Saved screenshot to $f") case Failure(e) => Console.err.println(s"Failed to save screenshot ($e)") } } keyboard.on("ctrl-print"){ //cntrl-print screen should launch the export directory OS.open(exportDir) } private[drx] def keyPressed(k:Keyboard.KeyCode):Unit = keyboard += k private[drx] def keyReleased(k:Keyboard.KeyCode):Unit = keyboard -= k //--required def save(img:Img, file:File):Try[File] = ??? //TODO remove default implementation def save(file:File):Try[File] = ??? def snapshot(size:Vec):Img = ??? //default export methods private lazy val exportDir = File("export").canon def save:Try[File] = save( exportDir / s"snapshot-${Date.now.krypton.take(6)}.png" ) def snapshot:Img = snapshot(size) //--Style def !(p:Default.type):Unit = { horzAlign = Center vertAlign = Midline align() } // color def !(p:Background):Unit def !(p:Fill):Unit def !(p:Stroke):Unit def !(p:FillNone.type):Unit def !(p:StrokeNone.type):Unit def !(p:Weight):Unit //--get active color styles (support infered styles like arrowheads) def fill:Option[Color] def stroke:Option[Color] def weight:Double // font def !(p:Font):Unit def !(p:Align):Unit // def !(p:AlignVertical):Unit // transform def !(p:Translate):Unit def !(p:ScaleProperty):Unit def !(p:Rotate):Unit //--Shapes //--required def !(p:Path):Unit // deried from path def !(s:Circ):Unit def !(s:Ellipse):Unit def !(s:Rect):Unit def !(s:Line):Unit def !(s:Poly):Unit def !(s:Tri):Unit def !(s:Arc):Unit def !(s:Text):Unit def textSize(text:Text, font:Font):Vec // def textBox(text:Text, font:Font):Rect //to really get the box we need to know justfication styles aswell def !(s:Bezier):Unit = ??? def !(s:Polys):Unit = ??? def !(s:SvgPath):Unit = ??? //--derived def !(arrow:Arrow):Unit = { val color:Color = stroke orElse fill getOrElse Black val headSize:Double=weight*8 val a = arrow.a val b = arrow.b val vel = b - a if(vel.range != 0){ val head = b - vel.mag(headSize) this ! Stroke(color) this ! Line(a, head) //--triangle vec boid this ! StrokeNone this ! Fill(color) val ortho = (vel x Vec.z) mag (headSize/2) this ! Poly(List(head-ortho, b, head+ortho)) ////--reset color TODO let the style wrapper do this if(fill.isDefined) this ! Fill(fill.get) else this ! FillNone if(stroke.isDefined) this ! Stroke(stroke.get) else this ! StrokeNone } } def !(s:Star):Unit //--video context methods def remote(video:Video):MediaRemote def !(s:Video):Unit def !(s:Img):Unit def !(s:Svg):Unit def !(s:Html):Unit //TODO add more required interfaces... //--Derived= final def !(p:AlignHorizontal):Unit = {horzAlign = p; align()} final def !(p:AlignVertical):Unit = {vertAlign = p; align()} final def !(s:Shape.Styled):Unit = style{ //--clear the current styles // this ! Default //--push the new style this ! s.style //--hack to make the runtime figure out the shape type drawShape(s.shape) //FIXME pop back to last style } def style(f: =>Unit):Unit //require a function that will push a style on the stack and then and pop it back (save; restore) final def drawShape(shape:Shape) = shape match { //this is code smell that polymoric calles are not autmatic here a runtime instance off case list is required... //--basic shapes case p:Circ => this ! p case p:Rect => this ! p case p:Ellipse => this ! p case p:Text => this ! p case p:Arrow => this ! p case p:Tri => this ! p case p:Line => this ! p case p:Path => this ! p case p:Poly => this ! p case p:Html => this ! p case p:Video => this ! p case p:Img => this ! p // case p:Html => this ! p //TODO add basic shapes //--compound shapes case p:Shape.Grouped => this ! p case p:Shape.Styled => this ! p case p:Axes[_,_] => this ! p case _ => {println(Red ansi "Error: drawShape match not implemented for $p"); ??? } //NOte should never be reached } final def !(p:Shape.Grouped):Unit = p.shapes foreach drawShape final def !(ps:Iterable[Shape]):Unit = ps foreach drawShape final def !(ps:Shape.Empty):Unit = () final def !(p:Sketch):Unit = for(f <- p.drawFunctions) this ! Shape.empty //TODO //use the actual f(g) // derived final def !(p:Transform):Unit = { this ! Translate(p.translate) this ! Rotate(p.rotate) this ! ScaleProperty(p.scale) } //------derived *final* //-- a style is a collection of properties, and this does a smart selection by (what hasn't been said) the negative space i.e. not specifying a stroke value implies setting noStroke final def !(style:Style):Unit = { var hasFill = false var hasStroke = false var hasAlign = true for(p <- style){ //have to use matching since the Property parent doesn't know which specific property method to use //TODO use a typeclass p match { //--wait later case x:Fill => this ! x; hasFill = true case x:Stroke => this ! x; hasStroke = true case FillNone => hasFill = false case StrokeNone => hasStroke = false case h:AlignHorizontal => hasAlign = true; horzAlign = h case v:AlignVertical => hasAlign = true; vertAlign = v case a:Align => hasAlign = true; horzAlign = a.horz; vertAlign = a.vert //--do now case Default => this ! Default; hasStroke = false; hasFill = false case x:Background => this ! x case x:Weight => this ! x case x:Font => this ! x case x:Translate => this ! x case x:Rotate => this ! x case x:ScaleProperty => this ! x case x:Transform => this ! x //--do nothing?? // case _ => //do nothing } } if(hasStroke && !hasFill) this ! FillNone if(hasFill && !hasStroke) this ! StrokeNone if(hasAlign) align() } def ![A,B](a:Axes[A,B]):Unit = { //TODO add draw ticks and detect font size and spacings (and get tick stroke color from the set fill color a.xTicks foreach {t => this ! (t + 3.x) ~ Style.Top} //auto center a.yTicks foreach {t => this ! (t - 3.x) ~ Style.Right} //auto midline } /**this is called before exiting in case resources are still being used*/ def close():Unit }