Code Sketch


yoiiiii11
By: Mhalsakant School
Category: Programming
import java.awt.BasicStroke
import java.awt.BorderLayout
import java.awt.Color
import java.awt.Cursor
import java.awt.Dimension
import java.awt.FlowLayout
import java.awt.Font
import java.awt.GradientPaint
import java.awt.Graphics
import java.awt.Graphics2D
import java.awt.GridLayout
import java.awt.Point
import java.awt.Polygon
import java.awt.RenderingHints
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
import java.awt.event.KeyAdapter
import java.awt.event.KeyEvent
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import java.awt.event.MouseMotionAdapter
import java.awt.geom.AffineTransform
import java.awt.image.BufferedImage

import javax.swing.BorderFactory
import javax.swing.DefaultListModel
import javax.swing.JButton
import javax.swing.JFrame
import javax.swing.JLabel
import javax.swing.JList
import javax.swing.JOptionPane
import javax.swing.JPanel
import javax.swing.JScrollPane
import javax.swing.JSlider
import javax.swing.JTextArea
import javax.swing.JTextField
import javax.swing.SwingUtilities
import javax.swing.Timer
import javax.swing.WindowConstants

import scala.collection.mutable.ArrayBuffer


// ============================================================
// ULTRA LEGEND DRAW -> EXACT KOJO -> 3D ANIMATION STUDIO X
// ============================================================
//
// 1. DRAW ANYTHING
// 2. EVERY STROKE IS RECORDED
// 3. EXACT POINTS ARE KEPT
// 4. GENERATE KOJO CODE
// 5. OBJECT LIBRARY
// 6. SHAPE LIBRARY
// 7. 3D EXTRUDED PREVIEW
// 8. 3D ROTATION ANIMATION
// 9. DRAWING ANIMATION
// 10. UNDO / REDO
// 11. GRID / SNAP
// 12. COPY CODE
// 13. SAVE CODE
// 14. MANY FUTURE OBJECTS
//
// ============================================================


// ============================================================
// CANVAS SIZE
// ============================================================

val CW = 1000
val CH = 650

val CX = CW / 2
val CY = CH / 2


// ============================================================
// DATA
// ============================================================

case class DrawStroke(
  points: Vector[Point]
)

case class LibraryShape(
  name: String,
  kind: String
)


// ============================================================
// STATE
// ============================================================

var strokes =
  ArrayBuffer[DrawStroke]()

var undoHistory =
  ArrayBuffer[
    ArrayBuffer[DrawStroke]
  ]()

var redoHistory =
  ArrayBuffer[
    ArrayBuffer[DrawStroke]
  ]()

var activeStroke =
  ArrayBuffer[Point]()

var drawing =
  false

var eraser =
  false

var gridOn =
  true

var snapOn =
  false

var brushSize =
  4

var currentColor =
  new Color(
    20,
    50,
    120
  )

var generatedCode =
  ""

var animationStroke =
  0

var animationPoint =
  1

var animationPlaying =
  false

var animationTimer: Timer =
  null

var animationData =
  ArrayBuffer[DrawStroke]()


// ============================================================
// UI
// ============================================================

var frame: JFrame =
  null

var canvas: JPanel =
  null

var codeArea: JTextArea =
  null

var status: JLabel =
  null

var searchField: JTextField =
  null

var objectModel =
  new DefaultListModel[String]()

var objectList: JList[String] =
  null

var speedSlider: JSlider =
  null


// ============================================================
// BUTTON
// ============================================================

def makeButton(
  text: String
): JButton = {

  val b =
    new JButton(text)

  b.setFocusable(false)

  b.setFont(
    new Font(
      "Arial",
      Font.BOLD,
      11
    )
  )

  b
}


// ============================================================
// STATUS
// ============================================================

def statusText(
  text: String
): Unit = {

  if (
    status != null
  ) {

    status.setText(
      text
    )
  }
}


// ============================================================
// REPAINT
// ============================================================

def repaintCanvas(): Unit = {

  if (
    canvas != null
  ) {

    canvas.repaint()
  }
}


// ============================================================
// COPY STROKES
// ============================================================

def copyStrokes(
  source: ArrayBuffer[DrawStroke]
): ArrayBuffer[DrawStroke] = {

  val result =
    ArrayBuffer[DrawStroke]()

  var i = 0

  while (
    i < source.length
  ) {

    val pts =
      source(i).points.map(
        p =>
          new Point(
            p.x,
            p.y
          )
      )

    result +=
      DrawStroke(
        pts
      )

    i += 1
  }

  result
}


// ============================================================
// HISTORY
// ============================================================

def remember(): Unit = {

  undoHistory +=
    copyStrokes(
      strokes
    )

  redoHistory.clear()

  while (
    undoHistory.length > 35
  ) {

    undoHistory.remove(
      0
    )
  }
}


def doUndo(): Unit = {

  if (
    undoHistory.nonEmpty
  ) {

    redoHistory +=
      copyStrokes(
        strokes
      )

    val previous =
      undoHistory(
        undoHistory.length - 1
      )

    undoHistory.remove(
      undoHistory.length - 1
    )

    strokes.clear()

    strokes ++=
      copyStrokes(
        previous
      )

    repaintCanvas()

    statusText(
      "UNDO"
    )

  } else {

    statusText(
      "NOTHING TO UNDO"
    )
  }
}


def doRedo(): Unit = {

  if (
    redoHistory.nonEmpty
  ) {

    undoHistory +=
      copyStrokes(
        strokes
      )

    val next =
      redoHistory(
        redoHistory.length - 1
      )

    redoHistory.remove(
      redoHistory.length - 1
    )

    strokes.clear()

    strokes ++=
      copyStrokes(
        next
      )

    repaintCanvas()

    statusText(
      "REDO"
    )

  } else {

    statusText(
      "NOTHING TO REDO"
    )
  }
}


// ============================================================
// SNAP
// ============================================================

def snap(
  value: Int
): Int = {

  if (
    snapOn
  ) {

    (value / 10) * 10

  } else {

    value
  }
}


// ============================================================
// MOUSE POINT
// ============================================================

def mousePoint(
  e: MouseEvent
): Point = {

  val x =
    math.max(
      0,
      math.min(
        CW - 1,
        e.getX
      )
    )

  val y =
    math.max(
      0,
      math.min(
        CH - 1,
        e.getY
      )
    )

  new Point(
    snap(x),
    snap(y)
  )
}


// ============================================================
// GRID
// ============================================================

def drawGrid(
  g2: Graphics2D
): Unit = {

  if (
    !gridOn
  ) {

    return
  }

  g2.setColor(
    new Color(
      0,
      0,
      0,
      23
    )
  )

  var x = 0

  while (
    x <= CW
  ) {

    g2.drawLine(
      x,
      0,
      x,
      CH
    )

    x += 50
  }

  var y = 0

  while (
    y <= CH
  ) {

    g2.drawLine(
      0,
      y,
      CW,
      y
    )

    y += 50
  }

  g2.setColor(
    new Color(
      0,
      0,
      0,
      55
    )
  )

  g2.drawLine(
    CX,
    0,
    CX,
    CH
  )

  g2.drawLine(
    0,
    CY,
    CW,
    CY
  )
}


// ============================================================
// DRAW STROKE
// ============================================================

def paintStroke(
  g2: Graphics2D,
  points: Seq[Point]
): Unit = {

  if (
    points.length == 1
  ) {

    val p =
      points.head

    g2.fillOval(
      p.x - brushSize / 2,
      p.y - brushSize / 2,
      brushSize,
      brushSize
    )

  } else {

    var i = 1

    while (
      i < points.length
    ) {

      val a =
        points(i - 1)

      val b =
        points(i)

      g2.drawLine(
        a.x,
        a.y,
        b.x,
        b.y
      )

      i += 1
    }
  }
}


// ============================================================
// CANVAS
// ============================================================

canvas =
  new JPanel {

    override def paintComponent(
      graphics: Graphics
    ): Unit = {

      super.paintComponent(
        graphics
      )

      val g2 =
        graphics
          .asInstanceOf[Graphics2D]

      g2.setRenderingHint(
        RenderingHints.KEY_ANTIALIASING,
        RenderingHints.VALUE_ANTIALIAS_ON
      )

      g2.setColor(
        new Color(
          250,
          252,
          255
        )
      )

      g2.fillRect(
        0,
        0,
        getWidth,
        getHeight
      )

      drawGrid(
        g2
      )

      g2.setStroke(
        new BasicStroke(
          brushSize.toFloat,
          BasicStroke.CAP_ROUND,
          BasicStroke.JOIN_ROUND
        )
      )

      g2.setColor(
        currentColor
      )

      var i = 0

      while (
        i < strokes.length
      ) {

        paintStroke(
          g2,
          strokes(i).points
        )

        i += 1
      }

      if (
        activeStroke.nonEmpty
      ) {

        if (
          eraser
        ) {

          g2.setColor(
            Color.WHITE
          )

        } else {

          g2.setColor(
            currentColor
          )
        }

        paintStroke(
          g2,
          activeStroke.toVector
        )
      }
    }
  }


// ============================================================
// GEOMETRY
// ============================================================

def distance(
  a: Point,
  b: Point
): Double = {

  math.hypot(
    b.x - a.x,
    b.y - a.y
  )
}


def heading(
  a: Point,
  b: Point
): Double = {

  math.toDegrees(
    math.atan2(
      b.y - a.y,
      b.x - a.x
    )
  )
}


def normalize(
  angle: Double
): Double = {

  var a =
    angle

  while (
    a > 180
  ) {

    a -= 360
  }

  while (
    a < -180
  ) {

    a += 360
  }

  a
}


// ============================================================
// RDP SIMPLIFIER
// ============================================================

def lineDistance(
  p: Point,
  a: Point,
  b: Point
): Double = {

  val dx =
    b.x - a.x

  val dy =
    b.y - a.y

  if (
    dx == 0 &&
    dy == 0
  ) {

    distance(
      p,
      a
    )

  } else {

    val t =
      (
        (p.x - a.x) * dx +
        (p.y - a.y) * dy
      ).toDouble /
        (
          dx * dx +
          dy * dy
        )

    val tt =
      math.max(
        0.0,
        math.min(
          1.0,
          t
        )
      )

    val px =
      a.x +
      tt * dx

    val py =
      a.y +
      tt * dy

    math.hypot(
      p.x - px,
      p.y - py
    )
  }
}


def simplify(
  pts: Seq[Point],
  tolerance: Double
): Vector[Point] = {

  if (
    pts.length <= 2
  ) {

    pts.toVector

  } else {

    var maxDist = 0.0

    var index = 0

    var i = 1

    while (
      i < pts.length - 1
    ) {

      val d =
        lineDistance(
          pts(i),
          pts.head,
          pts.last
        )

      if (
        d > maxDist
      ) {

        maxDist =
          d

        index =
          i
      }

      i += 1
    }

    if (
      maxDist > tolerance
    ) {

      val left =
        simplify(
          pts.take(
            index + 1
          ),
          tolerance
        )

      val right =
        simplify(
          pts.drop(
            index
          ),
          tolerance
        )

      (
        left.dropRight(1) ++
        right
      ).toVector

    } else {

      Vector(
        pts.head,
        pts.last
      )
    }
  }
}


// ============================================================
// DETECT CLOSED
// ============================================================

def closed(
  pts: Seq[Point]
): Boolean = {

  pts.length >= 4 &&
  distance(
    pts.head,
    pts.last
  ) < 45
}


// ============================================================
// DETECT CIRCLE
// ============================================================

def isCircle(
  pts: Seq[Point]
): Boolean = {

  if (
    pts.length < 18
  ) {

    false

  } else if (
    !closed(pts)
  ) {

    false

  } else {

    val center =
      averagePoint(
        pts
      )

    var total = 0.0

    var i = 0

    while (
      i < pts.length
    ) {

      total +=
        distance(
          center,
          pts(i)
        )

      i += 1
    }

    val radius =
      total /
        pts.length

    var error = 0.0

    i = 0

    while (
      i < pts.length
    ) {

      error +=
        math.abs(
          distance(
            center,
            pts(i)
          ) -
          radius
        )

      i += 1
    }

    val avgError =
      error /
        pts.length

    radius > 25 &&
    avgError / radius < 0.23
  }
}


// ============================================================
// AVERAGE POINT
// ============================================================

def averagePoint(
  pts: Seq[Point]
): Point = {

  var sx = 0.0

  var sy = 0.0

  var i = 0

  while (
    i < pts.length
  ) {

    sx += pts(i).x
    sy += pts(i).y

    i += 1
  }

  new Point(
    (sx / pts.length).toInt,
    (sy / pts.length).toInt
  )
}


// ============================================================
// DETECT POLYGON
// ============================================================

def polygon(
  pts: Seq[Point]
): Vector[Point] = {

  simplify(
    pts,
    18
  )
}


// ============================================================
// KOJO COORDINATES
// ============================================================

def kx(
  x: Int
): Int = {

  x - CX
}


def ky(
  y: Int
): Int = {

  CY - y
}


// ============================================================
// NUMBER
// ============================================================

def num(
  d: Double
): String = {

  if (
    math.abs(
      d - d.round
    ) < 0.05
  ) {

    d.round.toString

  } else {

    "%.2f".format(d)
  }
}


// ============================================================
// EXACT PATH CODE
// ============================================================
// setPosition + lineTo is deliberately used for arbitrary
// freehand strokes so that the recorded geometry is preserved.
// ============================================================

def exactPathCode(
  points: Seq[Point],
  number: Int
): String = {

  val p =
    simplify(
      points,
      2
    )

  if (
    p.isEmpty
  ) {

    ""

  } else {

    val sb =
      new StringBuilder()

    sb.append(
      "// EXACT STROKE " +
      number +
      "\n"
    )

    sb.append(
      "penUp()\n"
    )

    sb.append(
      "setPosition(" +
      kx(p.head.x) +
      ", " +
      ky(p.head.y) +
      ")\n"
    )

    sb.append(
      "penDown()\n"
    )

    var i = 1

    while (
      i < p.length
    ) {

      sb.append(
        "lineTo(" +
        kx(p(i).x) +
        ", " +
        ky(p(i).y) +
        ")\n"
      )

      i += 1
    }

    sb.toString
  }
}


// ============================================================
// SMART SQUARE CODE
// ============================================================

def squareCode(
  p: Vector[Point]
): String = {

  if (
    p.length == 5 &&
    closed(p)
  ) {

    val a =
      distance(
        p(0),
        p(1)
      )

    val b =
      distance(
        p(1),
        p(2)
      )

    if (
      b > 0 &&
      math.max(a,b) /
      math.min(a,b) < 1.25
    ) {

      val angle =
        heading(
          p(0),
          p(1)
        )

      "penUp()\n" +
      "setPosition(" +
      kx(p(0).x) +
      ", " +
      ky(p(0).y) +
      ")\n" +
      "setHeading(" +
      num(angle) +
      ")\n" +
      "penDown()\n" +
      "repeat(4) {\n" +
      "    forward(" +
      num(a) +
      ")\n" +
      "    right(90)\n" +
      "}\n"

    } else {

      exactPathCode(
        p,
        0
      )
    }

  } else {

    exactPathCode(
      p,
      0
    )
  }
}


// ============================================================
// SMART RECTANGLE CODE
// ============================================================

def rectangleCode(
  p: Vector[Point]
): String = {

  if (
    p.length == 5 &&
    closed(p)
  ) {

    val a =
      distance(
        p(0),
        p(1)
      )

    val b =
      distance(
        p(1),
        p(2)
      )

    if (
      a > 20 &&
      b > 20
    ) {

      val ratio =
        math.max(a,b) /
        math.min(a,b)

      if (
        ratio >= 1.25
      ) {

        val angle =
          heading(
            p(0),
            p(1)
          )

        "penUp()\n" +
        "setPosition(" +
        kx(p(0).x) +
        ", " +
        ky(p(0).y) +
        ")\n" +
        "setHeading(" +
        num(angle) +
        ")\n" +
        "penDown()\n" +
        "forward(" +
        num(a) +
        ")\n" +
        "right(90)\n" +
        "forward(" +
        num(b) +
        ")\n" +
        "right(90)\n" +
        "forward(" +
        num(a) +
        ")\n" +
        "right(90)\n" +
        "forward(" +
        num(b) +
        ")\n" +
        "right(90)\n"

      } else {

        squareCode(
          p
        )
      }

    } else {

      exactPathCode(
        p,
        0
      )
    }

  } else {

    exactPathCode(
      p,
      0
    )
  }
}


// ============================================================
// CIRCLE CODE
// ============================================================

def circleCode(
  pts: Seq[Point]
): String = {

  val c =
    averagePoint(
      pts
    )

  val radius =
    distance(
      c,
      pts.head
    )

  val circumference =
    2.0 *
    math.Pi *
    radius

  val step =
    circumference /
    360.0

  "penUp()\n" +
  "setPosition(" +
  kx(pts.head.x) +
  ", " +
  ky(pts.head.y) +
  ")\n" +
  "setHeading(0)\n" +
  "penDown()\n" +
  "repeat(360) {\n" +
  "    forward(" +
  num(step) +
  ")\n" +
  "    right(1)\n" +
  "}\n"
}


// ============================================================
// SMART STROKE
// ============================================================

def generateStroke(
  points: Seq[Point],
  number: Int
): String = {

  val simple =
    polygon(
      points
    )

  if (
    simple.isEmpty
  ) {

    ""

  } else if (
    isCircle(points)
  ) {

    "// CIRCLE DETECTED - STROKE " +
    number +
    "\n" +
    circleCode(
      points
    )

  } else if (
    simple.length == 5 &&
    closed(simple)
  ) {

    val a =
      distance(
        simple(0),
        simple(1)
      )

    val b =
      distance(
        simple(1),
        simple(2)
      )

    val ratio =
      if (
        math.min(a,b) > 0
      )
        math.max(a,b) /
        math.min(a,b)
      else
        99.0

    if (
      ratio < 1.25
    ) {

      "// SQUARE DETECTED - STROKE " +
      number +
      "\n" +
      squareCode(
        simple
      )

    } else {

      "// RECTANGLE DETECTED - STROKE " +
      number +
      "\n" +
      rectangleCode(
        simple
      )
    }

  } else {

    exactPathCode(
      points,
      number
    )
  }
}


// ============================================================
// GENERATE WHOLE DRAWING
// ============================================================

def generateKojoCode(): Unit = {

  if (
    strokes.isEmpty
  ) {

    codeArea.setText(
      "// NOTHING DRAWN YET\n" +
      "// Draw on the left canvas."
    )

    statusText(
      "DRAW SOMETHING FIRST"
    )

  } else {

    val sb =
      new StringBuilder()

    sb.append(
      "// ==================================================\n"
    )

    sb.append(
      "// ULTRA LEGEND EXACT CANVAS -> KOJO\n"
    )

    sb.append(
      "// Generated from recorded geometry\n"
    )

    sb.append(
      "// ==================================================\n\n"
    )

    sb.append(
      "cleari()\n"
    )

    sb.append(
      "setAnimationDelay(5)\n"
    )

    sb.append(
      "setPenColor(blue)\n"
    )

    sb.append(
      "setPenThickness(" +
      brushSize +
      ")\n\n"
    )

    var i = 0

    while (
      i < strokes.length
    ) {

      sb.append(
        generateStroke(
          strokes(i).points,
          i + 1
        )
      )

      sb.append(
        "\n"
      )

      i += 1
    }

    sb.append(
      "\n// END OF GENERATED DRAWING\n"
    )

    generatedCode =
      sb.toString

    codeArea.setText(
      generatedCode
    )

    codeArea.setCaretPosition(
      0
    )

    statusText(
      "EXACT KOJO CODE GENERATED"
    )
  }
}


// ============================================================
// SAVE CODE
// ============================================================

def saveCode(): Unit = {

  val chooser =
    new javax.swing.JFileChooser()

  chooser.setDialogTitle(
    "SAVE KOJO CODE"
  )

  if (
    chooser.showSaveDialog(frame) ==
    javax.swing.JFileChooser.APPROVE_OPTION
  ) {

    try {

      var file =
        chooser.getSelectedFile

      if (
        !file.getName
          .toLowerCase
          .endsWith(".scala")
      ) {

        file =
          new java.io.File(
            file.getAbsolutePath +
            ".scala"
          )
      }

      val writer =
        new java.io.PrintWriter(
          file
        )

      writer.write(
        codeArea.getText
      )

      writer.close()

      statusText(
        "CODE SAVED"
      )

    } catch {

      case _: Exception =>
        statusText(
          "SAVE ERROR"
        )
    }
  }
}


// ============================================================
// COPY CODE
// ============================================================

def copyCode(): Unit = {

  if (
    generatedCode.trim.isEmpty
  ) {

    generateKojoCode()
  }

  try {

    val clipboard =
      java.awt.Toolkit
        .getDefaultToolkit
        .getSystemClipboard

    clipboard.setContents(
      new java.awt.datatransfer.StringSelection(
        codeArea.getText
      ),
      null
    )

    statusText(
      "KOJO CODE COPIED"
    )

  } catch {

    case _: Exception =>
      statusText(
        "COPY FAILED"
      )
  }
}


// ============================================================
// CLEAR
// ============================================================

def clearDrawing(): Unit = {

  remember()

  strokes.clear()

  activeStroke.clear()

  generatedCode =
    ""

  codeArea.setText(
    "// Canvas cleared."
  )

  repaintCanvas()

  statusText(
    "CANVAS CLEARED"
  )
}


// ============================================================
// OBJECT LIBRARY
// ============================================================

val objects =
  Array(
    "CAR",
    "SPORTS CAR",
    "RACING CAR",
    "TRUCK",
    "BUS",
    "TRAIN",
    "AIRPLANE",
    "ROCKET",
    "HELICOPTER",
    "BICYCLE",
    "MOTORBIKE",
    "SCOOTER",
    "BOAT",
    "SHIP",
    "SUBMARINE",

    "HOUSE",
    "VILLA",
    "CASTLE",
    "PALACE",
    "TOWER",
    "SCHOOL",
    "SHOP",
    "HOSPITAL",
    "LIGHTHOUSE",
    "BRIDGE",
    "CITY",
    "SKYSCRAPER",

    "TREE",
    "PALM TREE",
    "FLOWER",
    "CACTUS",
    "MUSHROOM",
    "MOUNTAIN",
    "VOLCANO",
    "ISLAND",
    "CLOUD",
    "RAINBOW",
    "SUN",
    "MOON",
    "PLANET",
    "EARTH",
    "GALAXY",

    "BOY",
    "GIRL",
    "MAN",
    "WOMAN",
    "PERSON",
    "ROBOT",
    "ASTRONAUT",
    "SCIENTIST",
    "KING",
    "QUEEN",
    "SUPERHERO",

    "CAT",
    "DOG",
    "BIRD",
    "FISH",
    "SHARK",
    "WHALE",
    "DOLPHIN",
    "LION",
    "TIGER",
    "ELEPHANT",
    "HORSE",
    "RABBIT",
    "BEAR",
    "PANDA",
    "MONKEY",
    "FOX",
    "DEER",
    "COW",
    "GOAT",
    "SHEEP",
    "CHICKEN",
    "DUCK",
    "BUTTERFLY",
    "BEE",
    "SNAKE",
    "CROCODILE",
    "TURTLE",

    "APPLE",
    "BANANA",
    "ORANGE",
    "MANGO",
    "WATERMELON",
    "PIZZA",
    "BURGER",
    "CAKE",
    "ICE CREAM",
    "DONUT",

    "GUITAR",
    "PIANO",
    "DRUM",
    "MICROPHONE",
    "SPEAKER",
    "CAMERA",
    "COMPUTER",
    "PHONE",
    "BOOK",
    "PENCIL",
    "CLOCK",
    "KEY",
    "LAMP",
    "CHAIR",
    "TABLE",
    "BACKPACK",

    "STAR",
    "HEART",
    "DIAMOND",
    "CROWN",
    "TROPHY",
    "MEDAL",
    "GIFT",
    "BALLOON",
    "UMBRELLA",
    "MAGIC WAND",

    "FOOTBALL",
    "BASKETBALL",
    "TENNIS BALL",
    "CRICKET BAT",
    "CRICKET BALL",

    "CYBER CITY",
    "AI ROBOT",
    "FUTURE CAR",
    "DRONE",
    "SPACE STATION",
    "TIME MACHINE",
    "HOLOGRAM",
    "NEON TOWER",
    "FUTURE HOUSE"
  )


// ============================================================
// SHAPE LIBRARY
// ============================================================

val shapes =
  Array(
    "LINE",
    "SQUARE",
    "RECTANGLE",
    "ROUNDED RECTANGLE",
    "CIRCLE",
    "ELLIPSE",
    "TRIANGLE",
    "DIAMOND",
    "PENTAGON",
    "HEXAGON",
    "OCTAGON",
    "STAR 5",
    "STAR 6",
    "STAR 8",
    "HEART",
    "ARROW RIGHT",
    "ARROW LEFT",
    "ARROW UP",
    "ARROW DOWN",
    "CHEVRON",
    "PLUS",
    "CROSS",
    "X",
    "LIGHTNING",
    "CLOUD",
    "SUN",
    "MOON",
    "CRESCENT",
    "RING",
    "ARC",
    "SEMICIRCLE",
    "WAVE",
    "SPIRAL",
    "BURST",
    "HOUSE",
    "TREE",
    "MOUNTAIN",
    "FLOWER",
    "SNOWFLAKE",
    "SMILEY"
  )


def populateObjects(): Unit = {

  objectModel.clear()

  var i = 0

  while (
    i < objects.length
  ) {

    objectModel.addElement(
      objects(i)
    )

    i += 1
  }
}


// ============================================================
// OBJECT HASH
// ============================================================

def objectSeed(
  name: String
): Int = {

  math.abs(
    name.hashCode
  )
}


// ============================================================
// OBJECT MODEL GEOMETRY
// ============================================================
// The same deterministic geometry is used both for the canvas
// object and generated Kojo code.
// ============================================================

def objectPoints(
  name: String
): Vector[Point] = {

  val seed =
    objectSeed(
      name
    )

  val kind =
    seed % 7

  if (
    name == "STAR"
  ) {

    starPoints(
      5,
      250
    )

  } else if (
    name == "HEART"
  ) {

    heartPoints()

  } else if (
    name == "HOUSE" ||
    name == "VILLA" ||
    name == "CASTLE" ||
    name == "PALACE"
  ) {

    Vector(
      new Point(260,500),
      new Point(260,290),
      new Point(500,90),
      new Point(740,290),
      new Point(740,500),
      new Point(260,500)
    )

  } else if (
    name == "TREE" ||
    name == "PALM TREE"
  ) {

    Vector(
      new Point(455,570),
      new Point(545,570),
      new Point(545,330),
      new Point(670,250),
      new Point(600,160),
      new Point(500,210),
      new Point(400,160),
      new Point(330,250),
      new Point(455,330),
      new Point(455,570)
    )

  } else if (
    name == "CAR" ||
    name == "SPORTS CAR" ||
    name == "RACING CAR"
  ) {

    Vector(
      new Point(200,470),
      new Point(280,370),
      new Point(390,370),
      new Point(470,260),
      new Point(650,260),
      new Point(760,370),
      new Point(850,370),
      new Point(910,470),
      new Point(200,470)
    )

  } else if (
    name == "ROCKET"
  ) {

    Vector(
      new Point(430,500),
      new Point(450,210),
      new Point(500,100),
      new Point(550,210),
      new Point(570,500),
      new Point(500,570),
      new Point(430,500)
    )

  } else if (
    name == "FISH" ||
    name == "SHARK" ||
    name == "WHALE" ||
    name == "DOLPHIN"
  ) {

    Vector(
      new Point(180,350),
      new Point(350,230),
      new Point(650,230),
      new Point(820,350),
      new Point(650,470),
      new Point(350,470),
      new Point(180,350)
    )

  } else {

    val r =
      180 + seed % 90

    if (
      kind == 0
    ) {

      Vector(
        new Point(
          CX - r,
          CY - r
        ),
        new Point(
          CX + r,
          CY - r
        ),
        new Point(
          CX + r,
          CY + r
        ),
        new Point(
          CX - r,
          CY + r
        ),
        new Point(
          CX - r,
          CY - r
        )
      )

    } else if (
      kind == 1
    ) {

      starPoints(
        6,
        r
      )

    } else if (
      kind == 2
    ) {

      polygonPoints(
        5,
        r
      )

    } else if (
      kind == 3
    ) {

      polygonPoints(
        6,
        r
      )

    } else if (
      kind == 4
    ) {

      polygonPoints(
        8,
        r
      )

    } else if (
      kind == 5
    ) {

      Vector(
        new Point(
          CX,
          CY - r
        ),
        new Point(
          CX + r,
          CY
        ),
        new Point(
          CX,
          CY + r
        ),
        new Point(
          CX - r,
          CY
        ),
        new Point(
          CX,
          CY - r
        )
      )

    } else {

      starPoints(
        5,
        r
      )
    }
  }
}


// ============================================================
// POLYGON POINTS
// ============================================================

def polygonPoints(
  sides: Int,
  radius: Int
): Vector[Point] = {

  val out =
    ArrayBuffer[Point]()

  var i = 0

  while (
    i <= sides
  ) {

    val angle =
      -math.Pi / 2 +
      i *
      2.0 *
      math.Pi /
      sides

    out +=
      new Point(
        CX +
        (math.cos(angle) * radius).toInt,
        CY +
        (math.sin(angle) * radius).toInt
      )

    i += 1
  }

  out.toVector
}


// ============================================================
// STAR
// ============================================================

def starPoints(
  points: Int,
  radius: Int
): Vector[Point] = {

  val out =
    ArrayBuffer[Point]()

  var i = 0

  while (
    i <= points * 2
  ) {

    val angle =
      -math.Pi / 2 +
      i *
      math.Pi /
      points

    val r =
      if (
        i % 2 == 0
      )
        radius
      else
        (radius * 0.45).toInt

    out +=
      new Point(
        CX +
        (math.cos(angle) * r).toInt,
        CY +
        (math.sin(angle) * r).toInt
      )

    i += 1
  }

  out.toVector
}


// ============================================================
// HEART
// ============================================================

def heartPoints(): Vector[Point] = {

  val out =
    ArrayBuffer[Point]()

  var i = 0

  while (
    i <= 180
  ) {

    val t =
      i *
      math.Pi /
      180.0

    val x =
      16 *
      math.pow(
        math.sin(t),
        3
      )

    val y =
      13 *
      math.cos(t) -
      5 *
      math.cos(
        2 * t
      ) -
      2 *
      math.cos(
        3 * t
      ) -
      math.cos(
        4 * t
      )

    out +=
      new Point(
        CX +
        (x * 13).toInt,
        CY -
        (y * 13).toInt
      )

    i += 1
  }

  out.toVector
}


// ============================================================
// DRAW OBJECT ON CANVAS
// ============================================================

def drawObjectPreview(
  g2: Graphics2D,
  name: String
): Unit = {

  val pts =
    objectPoints(
      name
    )

  g2.setColor(
    new Color(
      25,
      105,
      210
    )
  )

  g2.setStroke(
    new BasicStroke(
      5,
      BasicStroke.CAP_ROUND,
      BasicStroke.JOIN_ROUND
    )
  )

  var i = 1

  while (
    i < pts.length
  ) {

    g2.drawLine(
      pts(i - 1).x,
      pts(i - 1).y,
      pts(i).x,
      pts(i).y
    )

    i += 1
  }

  g2.setFont(
    new Font(
      "Arial",
      Font.BOLD,
      24
    )
  )

  val labelWidth =
    g2.getFontMetrics
      .stringWidth(
        name
      )

  g2.drawString(
    name,
    CX -
    labelWidth / 2,
    620
  )
}


// ============================================================
// ADD OBJECT TO EXACT STROKES
// ============================================================

def addObject(
  name: String
): Unit = {

  remember()

  val pts =
    objectPoints(
      name
    )

  strokes +=
    DrawStroke(
      pts
    )

  repaintCanvas()

  statusText(
    "OBJECT ADDED: " +
    name
  )
}


// ============================================================
// OBJECT KOJO CODE
// ============================================================

def objectCode(
  name: String,
  number: Int
): String = {

  val pts =
    objectPoints(
      name
    )

  exactPathCode(
    pts,
    number
  )
}


// ============================================================
// SEARCH
// ============================================================

def searchObjects(): Unit = {

  val query =
    searchField
      .getText
      .trim
      .toLowerCase

  objectModel.clear()

  var i = 0

  while (
    i < objects.length
  ) {

    if (
      query.isEmpty ||
      objects(i)
        .toLowerCase
        .contains(
          query
        )
    ) {

      objectModel.addElement(
        objects(i)
      )
    }

    i += 1
  }

  statusText(
    "SEARCH COMPLETE"
  )
}


// ============================================================
// 3D PROJECTOR
// ============================================================

def project3D(
  x: Double,
  y: Double,
  z: Double,
  angle: Double,
  scale: Double,
  ox: Int,
  oy: Int
): Point = {

  val rad =
    angle *
    math.Pi /
    180.0

  val rx =
    x *
    math.cos(rad) -
    z *
    math.sin(rad)

  val rz =
    x *
    math.sin(rad) +
    z *
    math.cos(rad)

  val px =
    ox +
    (
      rx * scale
    ).toInt

  val py =
    oy +
    (
      y * scale -
      rz * scale * 0.55
    ).toInt

  new Point(
    px,
    py
  )
}


// ============================================================
// 3D MODEL RENDER
// ============================================================

def draw3DModel(
  g2: Graphics2D,
  pts: Seq[Point],
  angle: Double,
  depth: Double,
  scale: Double,
  ox: Int,
  oy: Int,
  reveal: Double
): Unit = {

  if (
    pts.length < 2
  ) {

    return
  }

  val front =
    ArrayBuffer[Point]()

  var i = 0

  while (
    i < pts.length
  ) {

    val px =
      pts(i).x -
      CX

    val py =
      pts(i).y -
      CY

    front +=
      project3D(
        px,
        py,
        0,
        angle,
        scale,
        ox,
        oy
      )

    i += 1
  }

  val back =
    ArrayBuffer[Point]()

  i = 0

  while (
    i < pts.length
  ) {

    val px =
      pts(i).x -
      CX

    val py =
      pts(i).y -
      CY

    back +=
      project3D(
        px,
        py,
        depth,
        angle,
        scale,
        ox,
        oy
      )

    i += 1
  }

  val maxFront =
    math.max(
      1,
      (
        front.length *
        reveal
      ).toInt
    )

  val maxBack =
    math.max(
      1,
      (
        back.length *
        reveal
      ).toInt
    )

  g2.setStroke(
    new BasicStroke(
      3,
      BasicStroke.CAP_ROUND,
      BasicStroke.JOIN_ROUND
    )
  )

  g2.setColor(
    new Color(
      80,
      130,
      220
    )
  )

  i = 1

  while (
    i < maxFront &&
    i < front.length
  ) {

    g2.drawLine(
      front(i - 1).x,
      front(i - 1).y,
      front(i).x,
      front(i).y
    )

    i += 1
  }

  g2.setColor(
    new Color(
      120,
      160,
      230
    )
  )

  i = 1

  while (
    i < maxBack &&
    i < back.length
  ) {

    g2.drawLine(
      back(i - 1).x,
      back(i - 1).y,
      back(i).x,
      back(i).y
    )

    i += 1
  }

  val maxConnect =
    math.min(
      maxFront,
      maxBack
    )

  g2.setColor(
    new Color(
      40,
      80,
      160
    )
  )

  i = 0

  while (
    i < maxConnect
  ) {

    g2.drawLine(
      front(i).x,
      front(i).y,
      back(i).x,
      back(i).y
    )

    i += 1
  }
}


// ============================================================
// 3D PREVIEW
// ============================================================

def start3DPreview(): Unit = {

  if (
    strokes.isEmpty
  ) {

    statusText(
      "DRAW OR ADD AN OBJECT FIRST"
    )

  } else {

    val preview =
      new JFrame(
        "TRUE-TIME 3D STYLE MODEL PREVIEW"
      )

    preview.setDefaultCloseOperation(
      WindowConstants.DISPOSE_ON_CLOSE
    )

    preview.setSize(
      1050,
      720
    )

    preview.setLocationRelativeTo(
      frame
    )

    var angle =
      0.0

    var reveal =
      0.0

    var playing =
      true

    val previewPanel =
      new JPanel {

        override def paintComponent(
          graphics: Graphics
        ): Unit = {

          super.paintComponent(
            graphics
          )

          val g2 =
            graphics
              .asInstanceOf[Graphics2D]

          g2.setRenderingHint(
            RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON
          )

          val w =
            getWidth

          val h =
            getHeight

          g2.setPaint(
            new GradientPaint(
              0,
              0,
              new Color(
                10,
                20,
                45
              ),
              0,
              h,
              new Color(
                55,
                20,
                95
              )
            )
          )

          g2.fillRect(
            0,
            0,
            w,
            h
          )

          val all =
            ArrayBuffer[Point]()

          var s = 0

          while (
            s < animationData.length
          ) {

            var p = 0

            while (
              p < animationData(s).points.length
            ) {

              all +=
                animationData(s).points(p)

              p += 1
            }

            s += 1
          }

          var minX = 0
          var maxX = CW
          var minY = 0
          var maxY = CH

          if (
            all.nonEmpty
          ) {

            minX =
              all.map(
                _.x
              ).min

            maxX =
              all.map(
                _.x
              ).max

            minY =
              all.map(
                _.y
              ).min

            maxY =
              all.map(
                _.y
              ).max
          }

          val width =
            math.max(
              100,
              maxX - minX
            )

          val height =
            math.max(
              100,
              maxY - minY
            )

          val scale =
            math.min(
              (
                w * 0.65
              ) / width,
              (
                h * 0.65
              ) / height
            )

          val ox =
            w / 2

          val oy =
            h / 2

          s = 0

          while (
            s < animationData.length
          ) {

            draw3DModel(
              g2,
              animationData(s).points,
              angle +
              s * 12,
              80 + s * 8,
              scale,
              ox,
              oy,
              reveal
            )

            s += 1
          }

          g2.setColor(
            new Color(
              255,
              255,
              255,
              210
            )
          )

          g2.setFont(
            new Font(
              "Arial",
              Font.BOLD,
              18
            )
          )

          g2.drawString(
            "3D MODEL PREVIEW",
            25,
            35
          )
        }
      }

    val timer =
      new Timer(
        30,
        new ActionListener {

          override def actionPerformed(
            e: ActionEvent
          ): Unit = {

            if (
              playing
            ) {

              angle += 2.0

              reveal += 0.025

              if (
                reveal > 1.0
              ) {

                reveal =
                  1.0
              }

              previewPanel.repaint()
            }
          }
        }
      )

    val playButton =
      makeButton(
        "PLAY / PAUSE"
      )

    val restartButton =
      makeButton(
        "RESTART 3D"
      )

    val closeButton =
      makeButton(
        "CLOSE"
      )

    playButton.addActionListener(
      new ActionListener {

        override def actionPerformed(
          e: ActionEvent
        ): Unit = {

          playing =
            !playing
        }
      }
    )

    restartButton.addActionListener(
      new ActionListener {

        override def actionPerformed(
          e: ActionEvent
        ): Unit = {

          angle =
            0.0

          reveal =
            0.0

          previewPanel.repaint()
        }
      }
    )

    closeButton.addActionListener(
      new ActionListener {

        override def actionPerformed(
          e: ActionEvent
        ): Unit = {

          timer.stop()

          preview.dispose()
        }
      }
    )

    val controls =
      new JPanel(
        new FlowLayout(
          FlowLayout.CENTER
        )
      )

    controls.add(
      playButton
    )

    controls.add(
      restartButton
    )

    controls.add(
      closeButton
    )

    preview.setLayout(
      new BorderLayout()
    )

    preview.add(
      previewPanel,
      BorderLayout.CENTER
    )

    preview.add(
      controls,
      BorderLayout.SOUTH
    )

    preview.setVisible(
      true
    )

    animationData =
      copyStrokes(
        strokes
      )

    timer.start()
  }
}


// ============================================================
// DRAWING ANIMATION
// ============================================================

def startDrawingAnimation(): Unit = {

  if (
    strokes.isEmpty
  ) {

    statusText(
      "DRAW SOMETHING FIRST"
    )

  } else {

    val preview =
      new JFrame(
        "EXACT DRAWING ANIMATION"
      )

    preview.setDefaultCloseOperation(
      WindowConstants.DISPOSE_ON_CLOSE
    )

    preview.setSize(
      1050,
      720
    )

    preview.setLocationRelativeTo(
      frame
    )

    val data =
      copyStrokes(
        strokes
      )

    var currentStrokeIndex =
      0

    var currentPointIndex =
      1

    var running =
      true

    val panel =
      new JPanel {

        override def paintComponent(
          graphics: Graphics
        ): Unit = {

          super.paintComponent(
            graphics
          )

          val g2 =
            graphics
              .asInstanceOf[Graphics2D]

          g2.setRenderingHint(
            RenderingHints.KEY_ANTIALIASING,
            RenderingHints.VALUE_ANTIALIAS_ON
          )

          val scale =
            math.min(
              (
                getWidth -
                40
              ).toDouble /
                CW,
              (
                getHeight -
                40
              ).toDouble /
                CH
            )

          val ox =
            (
              getWidth -
              CW * scale
            ) / 2.0

          val oy =
            (
              getHeight -
              CH * scale
            ) / 2.0

          val old =
            g2.getTransform

          g2.translate(
            ox,
            oy
          )

          g2.scale(
            scale,
            scale
          )

          g2.setColor(
            Color.WHITE
          )

          g2.fillRect(
            0,
            0,
            CW,
            CH
          )

          g2.setColor(
            currentColor
          )

          g2.setStroke(
            new BasicStroke(
              brushSize.toFloat,
              BasicStroke.CAP_ROUND,
              BasicStroke.JOIN_ROUND
            )
          )

          var s = 0

          while (
            s < currentStrokeIndex
          ) {

            paintStroke(
              g2,
              data(s).points
            )

            s += 1
          }

          if (
            currentStrokeIndex <
            data.length
          ) {

            paintStroke(
              g2,
              data(
                currentStrokeIndex
              ).points.take(
                math.min(
                  currentPointIndex,
                  data(
                    currentStrokeIndex
                  ).points.length
                )
              )
            )
          }

          g2.setTransform(
            old
          )
        }
      }

    val timer =
      new Timer(
        math.max(
          5,
          speedSlider.getValue
        ),
        new ActionListener {

          override def actionPerformed(
            e: ActionEvent
          ): Unit = {

            if (
              running
            ) {

              if (
                currentStrokeIndex <
                data.length
              ) {

                currentPointIndex += 3

                if (
                  currentPointIndex >=
                  data(
                    currentStrokeIndex
                  ).points.length
                ) {

                  currentStrokeIndex += 1

                  currentPointIndex = 1
                }

                panel.repaint()

              } else {

                running =
                  false
              }
            }
          }
        }
      )

    val play =
      makeButton(
        "PLAY / PAUSE"
      )

    val reset =
      makeButton(
        "RESET"
      )

    val close =
      makeButton(
        "CLOSE"
      )

    play.addActionListener(
      new ActionListener {

        override def actionPerformed(
          e: ActionEvent
        ): Unit = {

          running =
            !running
        }
      }
    )

    reset.addActionListener(
      new ActionListener {

        override def actionPerformed(
          e: ActionEvent
        ): Unit = {

          currentStrokeIndex =
            0

          currentPointIndex =
            1

          running =
            true

          panel.repaint()
        }
      }
    )

    close.addActionListener(
      new ActionListener {

        override def actionPerformed(
          e: ActionEvent
        ): Unit = {

          timer.stop()

          preview.dispose()
        }
      }
    )

    val controls =
      new JPanel(
        new FlowLayout()
      )

    controls.add(
      play
    )

    controls.add(
      reset
    )

    controls.add(
      close
    )

    preview.setLayout(
      new BorderLayout()
    )

    preview.add(
      panel,
      BorderLayout.CENTER
    )

    preview.add(
      controls,
      BorderLayout.SOUTH
    )

    preview.setVisible(
      true
    )

    timer.start()
  }
}


// ============================================================
// BUTTONS
// ============================================================

val pencilButton =
  makeButton(
    "PENCIL"
  )

val eraserButton =
  makeButton(
    "ERASER"
  )

val undoButton =
  makeButton(
    "UNDO"
  )

val redoButton =
  makeButton(
    "REDO"
  )

val clearButton =
  makeButton(
    "CLEAR"
  )

val generateButton =
  makeButton(
    "GENERATE CODE"
  )

val copyButton =
  makeButton(
    "COPY CODE"
  )

val saveCodeButton =
  makeButton(
    "SAVE CODE"
  )

val animationButton =
  makeButton(
    "2D ANIMATION"
  )

val model3DButton =
  makeButton(
    "3D MODEL"
  )

val gridButton =
  makeButton(
    "GRID"
  )

val snapButton =
  makeButton(
    "SNAP"
  )

val colorButton =
  makeButton(
    "COLOR"
  )


// ============================================================
// SEARCH
// ============================================================

searchField =
  new JTextField(
    12
  )

val searchButton =
  makeButton(
    "SEARCH"
  )


// ============================================================
// OBJECT LIST
// ============================================================

objectList =
  new JList[String](
    objectModel
  )

objectList.setFont(
  new Font(
    "Arial",
    Font.BOLD,
    13
  )
)

populateObjects()


// ============================================================
// CODE AREA
// ============================================================

codeArea =
  new JTextArea()

codeArea.setEditable(
  false
)

codeArea.setFont(
  new Font(
    "Monospaced",
    Font.PLAIN,
    13
  )
)

codeArea.setText(
  "// DRAW SOMETHING ON THE CANVAS\n" +
  "// THEN PRESS GENERATE CODE"
)


// ============================================================
// STATUS
// ============================================================

status =
  new JLabel(
    "READY"
  )

status.setFont(
  new Font(
    "Arial",
    Font.BOLD,
    13
  )
)


// ============================================================
// SPEED
// ============================================================

speedSlider =
  new JSlider(
    5,
    100,
    25
  )

speedSlider.setPreferredSize(
  new Dimension(
    140,
    35
  )
)


// ============================================================
// TOP TOOLBAR
// ============================================================

val toolbar =
  new JPanel(
    new FlowLayout(
      FlowLayout.LEFT,
      4,
      4
    )
  )

toolbar.add(
  pencilButton
)

toolbar.add(
  eraserButton
)

toolbar.add(
  undoButton
)

toolbar.add(
  redoButton
)

toolbar.add(
  clearButton
)

toolbar.add(
  generateButton
)

toolbar.add(
  copyButton
)

toolbar.add(
  saveCodeButton
)

toolbar.add(
  animationButton
)

toolbar.add(
  model3DButton
)

toolbar.add(
  gridButton
)

toolbar.add(
  snapButton
)

toolbar.add(
  colorButton
)

toolbar.add(
  new JLabel(
    "SPEED"
  )
)

toolbar.add(
  speedSlider
)


// ============================================================
// LEFT PANEL
// ============================================================

val leftPanel =
  new JPanel(
    new BorderLayout(
      5,
      5
    )
  )

leftPanel.setBorder(
  BorderFactory.createEmptyBorder(
    5,
    5,
    5,
    5
  )
)

val leftTitle =
  new JLabel(
    "OBJECT LIBRARY"
  )

leftTitle.setFont(
  new Font(
    "Arial",
    Font.BOLD,
    18
  )
)

val searchPanel =
  new JPanel(
    new FlowLayout(
      FlowLayout.LEFT,
      2,
      2
    )
  )

searchPanel.add(
  searchField
)

searchPanel.add(
  searchButton
)

val libraryTop =
  new JPanel(
    new BorderLayout()
  )

libraryTop.add(
  leftTitle,
  BorderLayout.NORTH
)

libraryTop.add(
  searchPanel,
  BorderLayout.SOUTH
)

leftPanel.add(
  libraryTop,
  BorderLayout.NORTH
)

leftPanel.add(
  new JScrollPane(
    objectList
  ),
  BorderLayout.CENTER
)


// ============================================================
// RIGHT PANEL
// ============================================================

val rightPanel =
  new JPanel(
    new BorderLayout(
      5,
      5
    )
  )

rightPanel.setBorder(
  BorderFactory.createEmptyBorder(
    5,
    5,
    5,
    5
  )
)

val rightTitle =
  new JLabel(
    "EXACT KOJO / SCALA CODE"
  )

rightTitle.setFont(
  new Font(
    "Arial",
    Font.BOLD,
    18
  )
)

rightPanel.add(
  rightTitle,
  BorderLayout.NORTH
)

rightPanel.add(
  new JScrollPane(
    codeArea
  ),
  BorderLayout.CENTER
)


// ============================================================
// MAIN FRAME
// ============================================================

frame =
  new JFrame(
    "ULTRA LEGEND DRAW -> EXACT KOJO -> 3D STUDIO X"
  )

frame.setDefaultCloseOperation(
  WindowConstants.EXIT_ON_CLOSE
)

frame.setSize(
  1650,
  900
)

frame.setLocationRelativeTo(
  null
)

frame.setLayout(
  new BorderLayout(
    4,
    4
  )
)

frame.add(
  toolbar,
  BorderLayout.NORTH
)

val centerPanel =
  new JPanel(
    new GridLayout(
      1,
      3
    )
  )

val canvasContainer =
  new JPanel(
    new BorderLayout()
  )

canvasContainer.add(
  new JLabel(
    "CANVAS"
  ),
  BorderLayout.NORTH
)

canvasContainer.add(
  canvas,
  BorderLayout.CENTER
)

centerPanel.add(
  leftPanel
)

centerPanel.add(
  canvasContainer
)

centerPanel.add(
  rightPanel
)

frame.add(
  centerPanel,
  BorderLayout.CENTER
)

frame.add(
  status,
  BorderLayout.SOUTH
)


// ============================================================
// PENCIL
// ============================================================

pencilButton.addActionListener(
  new ActionListener {

    override def actionPerformed(
      e: ActionEvent
    ): Unit = {

      eraser =
        false

      canvas.requestFocusInWindow()

      statusText(
        "PENCIL MODE"
      )
    }
  }
)


// ============================================================
// ERASER
// ============================================================

eraserButton.addActionListener(
  new ActionListener {

    override def actionPerformed(
      e: ActionEvent
    ): Unit = {

      eraser =
        true

      canvas.requestFocusInWindow()

      statusText(
        "ERASER MODE"
      )
    }
  }
)


// ============================================================
// UNDO
// ============================================================

undoButton.addActionListener(
  new ActionListener {

    override def actionPerformed(
      e: ActionEvent
    ): Unit = {

      doUndo()

      canvas.requestFocusInWindow()
    }
  }
)


// ============================================================
// REDO
// ============================================================

redoButton.addActionListener(
  new ActionListener {

    override def actionPerformed(
      e: ActionEvent
    ): Unit = {

      doRedo()

      canvas.requestFocusInWindow()
    }
  }
)


// ============================================================
// CLEAR
// ============================================================

clearButton.addActionListener(
  new ActionListener {

    override def actionPerformed(
      e: ActionEvent
    ): Unit = {

      clearDrawing()

      canvas.requestFocusInWindow()
    }
  }
)


// ============================================================
// GRID
// ============================================================

gridButton.addActionListener(
  new ActionListener {

    override def actionPerformed(
      e: ActionEvent
    ): Unit = {

      gridOn =
        !gridOn

      repaintCanvas()

      statusText(
        if (
          gridOn
        )
          "GRID ON"
        else
          "GRID OFF"
      )
    }
  }
)


// ============================================================
// SNAP
// ============================================================

snapButton.addActionListener(
  new ActionListener {

    override def actionPerformed(
      e: ActionEvent
    ): Unit = {

      snapOn =
        !snapOn

      statusText(
        if (
          snapOn
        )
          "SNAP ON"
        else
          "SNAP OFF"
      )
    }
  }
)


// ============================================================
// COLOR
// ============================================================

colorButton.addActionListener(
  new ActionListener {

    override def actionPerformed(
      e: ActionEvent
    ): Unit = {

      val selected =
        javax.swing.JColorChooser.showDialog(
          frame,
          "CHOOSE DRAW COLOR",
          currentColor
        )

      if (
        selected != null
      ) {

        currentColor =
          selected

        repaintCanvas()

        statusText(
          "COLOR CHANGED"
        )
      }
    }
  }
)


// ============================================================
// GENERATE
// ============================================================

generateButton.addActionListener(
  new ActionListener {

    override def actionPerformed(
      e: ActionEvent
    ): Unit = {

      generateKojoCode()

      canvas.requestFocusInWindow()
    }
  }
)


// ============================================================
// COPY
// ============================================================

copyButton.addActionListener(
  new ActionListener {

    override def actionPerformed(
      e: ActionEvent
    ): Unit = {

      copyCode()
    }
  }
)


// ============================================================
// SAVE CODE
// ============================================================

saveCodeButton.addActionListener(
  new ActionListener {

    override def actionPerformed(
      e: ActionEvent
    ): Unit = {

      saveCode()
    }
  }
)


// ============================================================
// 2D ANIMATION
// ============================================================

animationButton.addActionListener(
  new ActionListener {

    override def actionPerformed(
      e: ActionEvent
    ): Unit = {

      startDrawingAnimation()
    }
  }
)


// ============================================================
// 3D MODEL
// ============================================================

model3DButton.addActionListener(
  new ActionListener {

    override def actionPerformed(
      e: ActionEvent
    ): Unit = {

      start3DPreview()
    }
  }
)


// ============================================================
// SEARCH
// ============================================================

searchButton.addActionListener(
  new ActionListener {

    override def actionPerformed(
      e: ActionEvent
    ): Unit = {

      searchObjects()
    }
  }
)

searchField.addActionListener(
  new ActionListener {

    override def actionPerformed(
      e: ActionEvent
    ): Unit = {

      searchObjects()
    }
  }
)


// ============================================================
// OBJECT DOUBLE CLICK
// ============================================================

objectList.addMouseListener(
  new MouseAdapter {

    override def mouseClicked(
      e: MouseEvent
    ): Unit = {

      if (
        e.getClickCount == 2
      ) {

        val value =
          objectList.getSelectedValue

        if (
          value != null
        ) {

          addObject(
            value
          )
        }
      }
    }
  }
)


// ============================================================
// MOUSE PRESS
// ============================================================

canvas.addMouseListener(
  new MouseAdapter {

    override def mousePressed(
      e: MouseEvent
    ): Unit = {

      if (
        e.getButton ==
        MouseEvent.BUTTON1
      ) {

        remember()

        activeStroke.clear()

        activeStroke +=
          mousePoint(e)

        drawing =
          true

        statusText(
          "DRAWING..."
        )
      }
    }


    override def mouseReleased(
      e: MouseEvent
    ): Unit = {

      if (
        drawing
      ) {

        activeStroke +=
          mousePoint(e)

        if (
          eraser
        ) {

          val hit =
            activeStroke.last

          val remaining =
            ArrayBuffer[DrawStroke]()

          var i = 0

          while (
            i < strokes.length
          ) {

            val s =
              strokes(i)

            var remove =
              false

            var j = 0

            while (
              j < s.points.length &&
              !remove
            ) {

              if (
                distance(
                  hit,
                  s.points(j)
                ) < 30
              ) {

                remove =
                  true
              }

              j += 1
            }

            if (
              !remove
            ) {

              remaining +=
                s
            }

            i += 1
          }

          strokes.clear()

          strokes ++=
            remaining

        } else {

          if (
            activeStroke.length >= 2
          ) {

            strokes +=
              DrawStroke(
                activeStroke.toVector
              )
          }
        }

        activeStroke.clear()

        drawing =
          false

        repaintCanvas()

        statusText(
          "STROKE SAVED"
        )
      }
    }
  }
)


// ============================================================
// MOUSE DRAG
// ============================================================

canvas.addMouseMotionListener(
  new MouseMotionAdapter {

    override def mouseDragged(
      e: MouseEvent
    ): Unit = {

      if (
        drawing
      ) {

        val p =
          mousePoint(e)

        if (
          activeStroke.isEmpty ||
          distance(
            activeStroke.last,
            p
          ) >= 2
        ) {

          activeStroke +=
            p
        }

        repaintCanvas()
      }
    }
  }
)


// ============================================================
// KEYBOARD SHORTCUTS
// ============================================================

canvas.addKeyListener(
  new KeyAdapter {

    override def keyPressed(
      e: KeyEvent
    ): Unit = {

      val key =
        e.getKeyCode

      if (
        e.isControlDown &&
        key == KeyEvent.VK_Z
      ) {

        doUndo()

      } else if (
        e.isControlDown &&
        key == KeyEvent.VK_Y
      ) {

        doRedo()

      } else if (
        e.isControlDown &&
        key == KeyEvent.VK_G
      ) {

        generateKojoCode()

      } else if (
        e.isControlDown &&
        key == KeyEvent.VK_C
      ) {

        copyCode()

      } else if (
        e.isControlDown &&
        key == KeyEvent.VK_S
      ) {

        saveCode()

      } else if (
        e.isControlDown &&
        key == KeyEvent.VK_A
      ) {

        startDrawingAnimation()

      } else if (
        key == KeyEvent.VK_F1
      ) {

        JOptionPane.showMessageDialog(
          frame,
          "ULTRA LEGEND X SHORTCUTS\n\n" +
          "CTRL + Z  = UNDO\n" +
          "CTRL + Y  = REDO\n" +
          "CTRL + G  = GENERATE KOJO CODE\n" +
          "CTRL + C  = COPY CODE\n" +
          "CTRL + S  = SAVE CODE\n" +
          "CTRL + A  = 2D ANIMATION\n" +
          "DELETE    = CLEAR\n" +
          "E         = ERASER / PENCIL\n" +
          "F1        = THIS HELP\n\n" +
          "DOUBLE CLICK OBJECT = ADD OBJECT\n" +
          "3D MODEL BUTTON = 3D PREVIEW\n\n" +
          "IMPORTANT:\n" +
          "Canvas strokes are stored as geometry.\n" +
          "The generator reproduces the stored path.",
          "SECRET CONTROL CENTER",
          JOptionPane.INFORMATION_MESSAGE
        )

      } else if (
        key == KeyEvent.VK_DELETE
      ) {

        clearDrawing()

      } else if (
        key == KeyEvent.VK_E
      ) {

        eraser =
          !eraser

        statusText(
          if (
            eraser
          )
            "ERASER ON"
          else
            "PENCIL ON"
        )
      }
    }
  }
)


// ============================================================
// FOCUS
// ============================================================

canvas.setFocusable(
  true
)

canvas.setCursor(
  Cursor.getPredefinedCursor(
    Cursor.CROSSHAIR_CURSOR
  )
)


// ============================================================
// START APPLICATION
// ============================================================

SwingUtilities.invokeLater(
  new Runnable {

    override def run(): Unit = {

      frame.setVisible(
        true
      )

      canvas.requestFocusInWindow()

      statusText(
        "ULTRA LEGEND X READY"
      )
    }
  }
)