Code Sketch


yooooo1
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.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 javax.swing.BorderFactory
import javax.swing.DefaultListModel
import javax.swing.JButton
import javax.swing.JColorChooser
import javax.swing.JFileChooser
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.JSplitPane
import javax.swing.JTabbedPane
import javax.swing.JTextArea
import javax.swing.JTextField
import javax.swing.ListSelectionModel
import javax.swing.SwingUtilities
import javax.swing.Timer
import javax.swing.WindowConstants

import scala.collection.mutable.ArrayBuffer


// ============================================================
// ULTRA LEGEND DRAW LAB X
// CANVAS -> EXACT KOJO CODE -> 2D ANIMATION -> 3D PREVIEW
// ============================================================


// ============================================================
// CONSTANTS
// ============================================================

val CANVAS_W = 1000
val CANVAS_H = 650

val CENTER_X = CANVAS_W / 2
val CENTER_Y = CANVAS_H / 2


// ============================================================
// DRAWING DATA
// ============================================================

case class DrawStroke(
  points: Vector[Point]
)


// ============================================================
// APPLICATION STATE
// ============================================================

var strokes =
  ArrayBuffer[DrawStroke]()

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

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

var activeStroke =
  ArrayBuffer[Point]()

var drawingNow =
  false

var eraserOn =
  false

var gridOn =
  true

var snapOn =
  false

var brushSize =
  5.0f

var currentColor =
  new Color(
    20,
    70,
    180
  )

var generatedCode =
  ""

var animationSpeed =
  25


// ============================================================
// UI REFERENCES
// ============================================================

var frame: JFrame =
  null

var canvas: JPanel =
  null

var codeArea: JTextArea =
  null

var status: JLabel =
  null

var objectList: JList[String] =
  null

var shapeList: JList[String] =
  null

var searchField: JTextField =
  null

var speedSlider: JSlider =
  null


// ============================================================
// MODELS
// ============================================================

val objectModel =
  new DefaultListModel[String]()

val shapeModel =
  new DefaultListModel[String]()


// ============================================================
// BUTTON REFERENCES
// ============================================================

var pencilButton: JButton =
  null

var eraserButton: JButton =
  null

var undoButton: JButton =
  null

var redoButton: JButton =
  null

var clearButton: JButton =
  null

var generateButton: JButton =
  null

var copyButton: JButton =
  null

var saveButton: JButton =
  null

var animationButton: JButton =
  null

var model3DButton: JButton =
  null

var gridButton: JButton =
  null

var snapButton: JButton =
  null

var colorButton: JButton =
  null


// ============================================================
// BUTTON HELPER
// ============================================================

def makeButton(
  text: String
): JButton = {

  val b =
    new JButton(
      text
    )

  b.setFocusable(
    false
  )

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

  b
}


// ============================================================
// STATUS HELPER
// ============================================================

def setStatus(
  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 > 40
  ) {

    undoHistory.remove(
      0
    )
  }
}


def doUndo(): Unit = {

  if (
    undoHistory.nonEmpty
  ) {

    redoHistory +=
      copyStrokes(
        strokes
      )

    val old =
      undoHistory(
        undoHistory.length - 1
      )

    undoHistory.remove(
      undoHistory.length - 1
    )

    strokes.clear()

    strokes ++=
      copyStrokes(
        old
      )

    repaintCanvas()

    setStatus(
      "UNDO COMPLETE"
    )

  } else {

    setStatus(
      "NOTHING TO UNDO"
    )
  }
}


def doRedo(): Unit = {

  if (
    redoHistory.nonEmpty
  ) {

    undoHistory +=
      copyStrokes(
        strokes
      )

    val newer =
      redoHistory(
        redoHistory.length - 1
      )

    redoHistory.remove(
      redoHistory.length - 1
    )

    strokes.clear()

    strokes ++=
      copyStrokes(
        newer
      )

    repaintCanvas()

    setStatus(
      "REDO COMPLETE"
    )

  } else {

    setStatus(
      "NOTHING TO REDO"
    )
  }
}


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

def snapCoordinate(
  value: Int
): Int = {

  if (
    snapOn
  ) {

    (value / 10) * 10

  } else {

    value
  }
}


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

def mousePoint(
  e: MouseEvent
): Point = {

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

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

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


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

def drawGrid(
  g2: Graphics2D
): Unit = {

  if (
    gridOn
  ) {

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

    var x = 0

    while (
      x <= CANVAS_W
    ) {

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

      x += 50
    }

    var y = 0

    while (
      y <= CANVAS_H
    ) {

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

      y += 50
    }

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

    g2.drawLine(
      CENTER_X,
      0,
      CENTER_X,
      CANVAS_H
    )

    g2.drawLine(
      0,
      CENTER_Y,
      CANVAS_W,
      CENTER_Y
    )
  }
}


// ============================================================
// PAINT STROKE
// ============================================================

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

  if (
    pts.length == 1
  ) {

    val p =
      pts.head

    val size =
      math.max(
        1,
        brushSize.toInt
      )

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

  } else {

    var i = 1

    while (
      i < pts.length
    ) {

      val a =
        pts(i - 1)

      val b =
        pts(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.setRenderingHint(
        RenderingHints.KEY_RENDERING,
        RenderingHints.VALUE_RENDER_QUALITY
      )

      g2.setColor(
        Color.WHITE
      )

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

      drawGrid(
        g2
      )

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

      if (
        eraserOn
      ) {

        g2.setColor(
          Color.WHITE
        )

      } else {

        g2.setColor(
          currentColor
        )
      }

      var i = 0

      while (
        i < strokes.length
      ) {

        paintStroke(
          g2,
          strokes(i).points
        )

        i += 1
      }

      if (
        activeStroke.nonEmpty
      ) {

        paintStroke(
          g2,
          activeStroke.toVector
        )
      }
    }
  }

canvas.setFocusable(
  true
)

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


// ============================================================
// GEOMETRY HELPERS
// ============================================================

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 normalizeAngle(
  angle: Double
): Double = {

  var a =
    angle

  while (
    a > 180
  ) {

    a -= 360
  }

  while (
    a < -180
  ) {

    a += 360
  }

  a
}


def centerOf(
  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
  )
}


// ============================================================
// LINE DISTANCE
// ============================================================

def pointLineDistance(
  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 tc =
      math.max(
        0.0,
        math.min(
          1.0,
          t
        )
      )

    val px =
      a.x +
      tc * dx

    val py =
      a.y +
      tc * dy

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


// ============================================================
// SIMPLIFY
// ============================================================

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

  if (
    pts.length <= 2
  ) {

    pts.toVector

  } else {

    var maxDistance =
      0.0

    var splitIndex =
      0

    var i = 1

    while (
      i < pts.length - 1
    ) {

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

      if (
        d > maxDistance
      ) {

        maxDistance =
          d

        splitIndex =
          i
      }

      i += 1
    }

    if (
      maxDistance > tolerance
    ) {

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

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

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

    } else {

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


// ============================================================
// CLOSED
// ============================================================

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

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


// ============================================================
// CIRCLE DETECTION
// ============================================================

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

  if (
    pts.length < 20
  ) {

    false

  } else if (
    !isClosed(pts)
  ) {

    false

  } else {

    val c =
      centerOf(
        pts
      )

    var total =
      0.0

    var i = 0

    while (
      i < pts.length
    ) {

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

      i += 1
    }

    val radius =
      total /
      pts.length

    if (
      radius < 25
    ) {

      false

    } else {

      var error =
        0.0

      i = 0

      while (
        i < pts.length
      ) {

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

        i += 1
      }

      val meanError =
        error /
        pts.length

      meanError /
      radius < 0.24
    }
  }
}


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

def fmt(
  n: Double
): String = {

  if (
    math.abs(
      n -
      n.round
    ) < 0.04
  ) {

    n.round.toString

  } else {

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


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

def kx(
  x: Int
): Int = {

  x - CENTER_X
}


def ky(
  y: Int
): Int = {

  CENTER_Y - y
}


// ============================================================
// EXACT PATH CODE
// ============================================================

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

  val simple =
    simplify(
      pts,
      2
    )

  if (
    simple.isEmpty
  ) {

    ""

  } else {

    val sb =
      new StringBuilder()

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

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

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

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

    var i = 1

    while (
      i < simple.length
    ) {

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

      i += 1
    }

    sb.toString
  }
}


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

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

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

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

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


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

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

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

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

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

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


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

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

  val c =
    centerOf(
      pts
    )

  val radius =
    distance(
      c,
      pts.head
    )

  val step =
    2.0 *
    math.Pi *
    radius /
    360.0

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


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

def smartStrokeCode(
  pts: Seq[Point],
  number: Int
): String = {

  val p =
    simplify(
      pts,
      18
    )

  if (
    p.isEmpty
  ) {

    ""

  } else if (
    isCircle(pts)
  ) {

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

  } else if (
    p.length == 5 &&
    isClosed(p)
  ) {

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

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

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

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

      if (
        ratio < 1.25
      ) {

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

      } else {

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

    } else {

      exactPathCode(
        pts,
        number
      )
    }

  } else {

    exactPathCode(
      pts,
      number
    )
  }
}


// ============================================================
// COMPLETE KOJO CODE
// ============================================================

def generateKojoCode(): Unit = {

  if (
    strokes.isEmpty
  ) {

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

    setStatus(
      "DRAW SOMETHING FIRST"
    )

  } else {

    val sb =
      new StringBuilder()

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

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

    sb.append(
      "// ORIGINAL 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.toInt +
      ")\n\n"
    )

    var i = 0

    while (
      i < strokes.length
    ) {

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

      sb.append(
        "\n"
      )

      i += 1
    }

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

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

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

    generatedCode =
      sb.toString

    codeArea.setText(
      generatedCode
    )

    codeArea.setCaretPosition(
      0
    )

    setStatus(
      "EXACT KOJO CODE GENERATED"
    )
  }
}


// ============================================================
// 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
    )

    setStatus(
      "CODE COPIED"
    )

  } catch {

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


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

def saveCode(): Unit = {

  if (
    generatedCode.trim.isEmpty
  ) {

    generateKojoCode()
  }

  val chooser =
    new JFileChooser()

  chooser.setDialogTitle(
    "SAVE KOJO CODE"
  )

  val result =
    chooser.showSaveDialog(
      frame
    )

  if (
    result ==
    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()

      setStatus(
        "CODE SAVED"
      )

    } catch {

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


// ============================================================
// CLEAR DRAWING
// ============================================================

def clearDrawing(): Unit = {

  remember()

  strokes.clear()

  activeStroke.clear()

  generatedCode =
    ""

  codeArea.setText(
    "// CANVAS CLEARED\n" +
    "// Draw again and press GENERATE CODE."
  )

  repaintCanvas()

  setStatus(
    "CANVAS CLEARED"
  )
}


// ============================================================
// SHAPE GEOMETRY
// ============================================================

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

  val out =
    ArrayBuffer[Point]()

  var i = 0

  while (
    i <= sides
  ) {

    val a =
      startAngle +
      i *
      2.0 *
      math.Pi /
      sides

    out +=
      new Point(
        CENTER_X +
        (
          math.cos(a) *
          radius
        ).toInt,
        CENTER_Y +
        (
          math.sin(a) *
          radius
        ).toInt
      )

    i += 1
  }

  out.toVector
}


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

  val out =
    ArrayBuffer[Point]()

  var i = 0

  while (
    i <= points * 2
  ) {

    val a =
      -math.Pi / 2.0 +
      i *
      math.Pi /
      points

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

    out +=
      new Point(
        CENTER_X +
        (
          math.cos(a) *
          r
        ).toInt,
        CENTER_Y +
        (
          math.sin(a) *
          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(
        CENTER_X +
        (
          x * 13
        ).toInt,
        CENTER_Y -
        (
          y * 13
        ).toInt
      )

    i += 1
  }

  out.toVector
}


// ============================================================
// ALL SHAPE GEOMETRY
// ============================================================

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

  val n =
    name.toUpperCase

  if (
    n == "LINE"
  ) {

    Vector(
      new Point(150,180),
      new Point(850,500)
    )

  } else if (
    n == "SQUARE"
  ) {

    Vector(
      new Point(300,200),
      new Point(700,200),
      new Point(700,600),
      new Point(300,600),
      new Point(300,200)
    )

  } else if (
    n == "RECTANGLE"
  ) {

    Vector(
      new Point(170,230),
      new Point(830,230),
      new Point(830,480),
      new Point(170,480),
      new Point(170,230)
    )

  } else if (
    n == "ROUNDED RECTANGLE"
  ) {

    Vector(
      new Point(230,200),
      new Point(770,200),
      new Point(770,500),
      new Point(230,500),
      new Point(230,200)
    )

  } else if (
    n == "CIRCLE"
  ) {

    val out =
      ArrayBuffer[Point]()

    var i = 0

    while (
      i <= 360
    ) {

      val a =
        i *
        math.Pi /
        180.0

      out +=
        new Point(
          CENTER_X +
          (
            math.cos(a) *
            220
          ).toInt,
          CENTER_Y +
          (
            math.sin(a) *
            220
          ).toInt
        )

      i += 2
    }

    out.toVector

  } else if (
    n == "ELLIPSE"
  ) {

    val out =
      ArrayBuffer[Point]()

    var i = 0

    while (
      i <= 360
    ) {

      val a =
        i *
        math.Pi /
        180.0

      out +=
        new Point(
          CENTER_X +
          (
            math.cos(a) *
            300
          ).toInt,
          CENTER_Y +
          (
            math.sin(a) *
            160
          ).toInt
        )

      i += 2
    }

    out.toVector

  } else if (
    n == "TRIANGLE"
  ) {

    polygonPoints(
      3,
      250,
      -math.Pi / 2
    )

  } else if (
    n == "DIAMOND"
  ) {

    Vector(
      new Point(
        CENTER_X,
        110
      ),
      new Point(
        820,
        CENTER_Y
      ),
      new Point(
        CENTER_X,
        540
      ),
      new Point(
        180,
        CENTER_Y
      ),
      new Point(
        CENTER_X,
        110
      )
    )

  } else if (
    n == "PENTAGON"
  ) {

    polygonPoints(
      5,
      250,
      -math.Pi / 2
    )

  } else if (
    n == "HEXAGON"
  ) {

    polygonPoints(
      6,
      240,
      0
    )

  } else if (
    n == "HEPTAGON"
  ) {

    polygonPoints(
      7,
      240,
      -math.Pi / 2
    )

  } else if (
    n == "OCTAGON"
  ) {

    polygonPoints(
      8,
      240,
      math.Pi / 8
    )

  } else if (
    n == "STAR 5"
  ) {

    starPoints(
      5,
      260
    )

  } else if (
    n == "STAR 6"
  ) {

    starPoints(
      6,
      255
    )

  } else if (
    n == "STAR 8"
  ) {

    starPoints(
      8,
      250
    )

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

    heartPoints()

  } else if (
    n == "ARROW RIGHT"
  ) {

    Vector(
      new Point(150,260),
      new Point(600,260),
      new Point(600,160),
      new Point(850,325),
      new Point(600,490),
      new Point(600,390),
      new Point(150,390),
      new Point(150,260)
    )

  } else if (
    n == "ARROW LEFT"
  ) {

    Vector(
      new Point(850,260),
      new Point(400,260),
      new Point(400,160),
      new Point(150,325),
      new Point(400,490),
      new Point(400,390),
      new Point(850,390),
      new Point(850,260)
    )

  } else if (
    n == "ARROW UP"
  ) {

    Vector(
      new Point(430,530),
      new Point(430,300),
      new Point(280,300),
      new Point(500,90),
      new Point(720,300),
      new Point(570,300),
      new Point(570,530),
      new Point(430,530)
    )

  } else if (
    n == "ARROW DOWN"
  ) {

    Vector(
      new Point(430,120),
      new Point(430,350),
      new Point(280,350),
      new Point(500,560),
      new Point(720,350),
      new Point(570,350),
      new Point(570,120),
      new Point(430,120)
    )

  } else if (
    n == "CHEVRON"
  ) {

    Vector(
      new Point(250,200),
      new Point(500,420),
      new Point(750,200),
      new Point(750,320),
      new Point(500,540),
      new Point(250,320),
      new Point(250,200)
    )

  } else if (
    n == "PLUS"
  ) {

    Vector(
      new Point(430,120),
      new Point(570,120),
      new Point(570,250),
      new Point(700,250),
      new Point(700,400),
      new Point(570,400),
      new Point(570,530),
      new Point(430,530),
      new Point(430,400),
      new Point(300,400),
      new Point(300,250),
      new Point(430,250),
      new Point(430,120)
    )

  } else if (
    n == "CROSS"
  ) {

    Vector(
      new Point(270,170),
      new Point(730,480)
    ) ++
    Vector(
      new Point(730,170),
      new Point(270,480)
    )

  } else if (
    n == "X"
  ) {

    Vector(
      new Point(240,140),
      new Point(760,500)
    ) ++
    Vector(
      new Point(760,140),
      new Point(240,500)
    )

  } else if (
    n == "LIGHTNING"
  ) {

    Vector(
      new Point(570,70),
      new Point(320,320),
      new Point(470,320),
      new Point(410,570),
      new Point(700,250),
      new Point(540,250),
      new Point(570,70)
    )

  } else if (
    n == "CLOUD"
  ) {

    Vector(
      new Point(230,440),
      new Point(270,320),
      new Point(380,250),
      new Point(480,270),
      new Point(560,180),
      new Point(690,220),
      new Point(770,320),
      new Point(830,360),
      new Point(800,470),
      new Point(230,470),
      new Point(230,440)
    )

  } else if (
    n == "SUN"
  ) {

    starPoints(
      16,
      245
    )

  } else if (
    n == "MOON"
  ) {

    Vector(
      new Point(420,140),
      new Point(620,150),
      new Point(760,300),
      new Point(620,500),
      new Point(420,500),
      new Point(520,410),
      new Point(570,300),
      new Point(520,210),
      new Point(420,140)
    )

  } else if (
    n == "CRESCENT"
  ) {

    Vector(
      new Point(470,130),
      new Point(620,170),
      new Point(720,300),
      new Point(620,470),
      new Point(470,510),
      new Point(550,420),
      new Point(590,300),
      new Point(550,200),
      new Point(470,130)
    )

  } else if (
    n == "RING"
  ) {

    val out =
      ArrayBuffer[Point]()

    var i = 0

    while (
      i <= 360
    ) {

      val a =
        i *
        math.Pi /
        180.0

      out +=
        new Point(
          CENTER_X +
          (
            math.cos(a) *
            220
          ).toInt,
          CENTER_Y +
          (
            math.sin(a) *
            220
          ).toInt
        )

      i += 3
    }

    out.toVector

  } else if (
    n == "ARC"
  ) {

    val out =
      ArrayBuffer[Point]()

    var i = 0

    while (
      i <= 240
    ) {

      val a =
        (
          -120 +
          i
        ) *
        math.Pi /
        180.0

      out +=
        new Point(
          CENTER_X +
          (
            math.cos(a) *
            260
          ).toInt,
          CENTER_Y +
          (
            math.sin(a) *
            180
          ).toInt
        )

      i += 3
    }

    out.toVector

  } else if (
    n == "SEMICIRCLE"
  ) {

    val out =
      ArrayBuffer[Point]()

    var i = 0

    while (
      i <= 180
    ) {

      val a =
        i *
        math.Pi /
        180.0

      out +=
        new Point(
          CENTER_X +
          (
            math.cos(a) *
            250
          ).toInt,
          CENTER_Y +
          (
            math.sin(a) *
            180
          ).toInt
        )

      i += 3
    }

    out.toVector

  } else if (
    n == "WAVE"
  ) {

    val out =
      ArrayBuffer[Point]()

    var x = 80

    while (
      x <= 920
    ) {

      val y =
        CENTER_Y +
        (
          math.sin(
            (
              x - 80
            ) *
            0.04
          ) *
          100
        ).toInt

      out +=
        new Point(
          x,
          y
        )

      x += 5
    }

    out.toVector

  } else if (
    n == "SPIRAL"
  ) {

    val out =
      ArrayBuffer[Point]()

    var a =
      0.0

    var r =
      5.0

    while (
      a <
      math.Pi * 8
    ) {

      out +=
        new Point(
          CENTER_X +
          (
            math.cos(a) *
            r
          ).toInt,
          CENTER_Y +
          (
            math.sin(a) *
            r
          ).toInt
        )

      a += 0.10
      r += 1.7
    }

    out.toVector

  } else if (
    n == "BURST"
  ) {

    starPoints(
      20,
      260
    )

  } else if (
    n == "SNOWFLAKE"
  ) {

    Vector(
      new Point(
        CENTER_X,
        90
      ),
      new Point(
        CENTER_X,
        560
      ),
      new Point(
        260,
        170
      ),
      new Point(
        740,
        480
      ),
      new Point(
        260,
        480
      ),
      new Point(
        740,
        170
      )
    )

  } else if (
    n == "SMILEY"
  ) {

    val out =
      ArrayBuffer[Point]()

    var i = 0

    while (
      i <= 360
    ) {

      val a =
        i *
        math.Pi /
        180.0

      out +=
        new Point(
          CENTER_X +
          (
            math.cos(a) *
            230
          ).toInt,
          CENTER_Y +
          (
            math.sin(a) *
            230
          ).toInt
        )

      i += 3
    }

    out.toVector

  } else {

    polygonPoints(
      6,
      230,
      0
    )
  }
}


// ============================================================
// OBJECT GEOMETRY
// ============================================================

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

  val n =
    name.toUpperCase

  if (
    n.indexOf("CAR") >= 0
  ) {

    Vector(
      new Point(150,470),
      new Point(220,370),
      new Point(380,360),
      new Point(470,250),
      new Point(670,250),
      new Point(780,360),
      new Point(880,370),
      new Point(940,470),
      new Point(150,470)
    )

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

    Vector(
      new Point(250,540),
      new Point(250,300),
      new Point(500,90),
      new Point(750,300),
      new Point(750,540),
      new Point(250,540)
    )

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

    Vector(
      new Point(455,570),
      new Point(545,570),
      new Point(545,340),
      new Point(680,270),
      new Point(600,160),
      new Point(500,220),
      new Point(400,160),
      new Point(320,270),
      new Point(455,340),
      new Point(455,570)
    )

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

    Vector(
      new Point(430,520),
      new Point(445,220),
      new Point(500,80),
      new Point(555,220),
      new Point(570,520),
      new Point(500,590),
      new Point(430,520)
    )

  } else if (
    n == "ROBOT" ||
    n == "AI ROBOT"
  ) {

    Vector(
      new Point(380,540),
      new Point(380,300),
      new Point(350,300),
      new Point(350,150),
      new Point(650,150),
      new Point(650,300),
      new Point(620,300),
      new Point(620,540),
      new Point(380,540)
    )

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

    Vector(
      new Point(170,330),
      new Point(330,220),
      new Point(640,220),
      new Point(820,330),
      new Point(640,440),
      new Point(330,440),
      new Point(170,330)
    )

  } else if (
    n == "BIRD"
  ) {

    Vector(
      new Point(250,340),
      new Point(400,230),
      new Point(500,300),
      new Point(620,190),
      new Point(760,320),
      new Point(620,390),
      new Point(470,390),
      new Point(350,450),
      new Point(250,340)
    )

  } else if (
    n == "FLOWER"
  ) {

    Vector(
      new Point(500,570),
      new Point(500,320),
      new Point(400,230),
      new Point(500,140),
      new Point(600,230),
      new Point(500,320),
      new Point(500,570)
    )

  } else if (
    n == "MOUNTAIN"
  ) {

    Vector(
      new Point(70,540),
      new Point(300,220),
      new Point(430,400),
      new Point(620,100),
      new Point(930,540),
      new Point(70,540)
    )

  } else if (
    n == "STAR"
  ) {

    starPoints(
      5,
      260
    )

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

    heartPoints()

  } else if (
    n == "PLANET" ||
    n == "EARTH" ||
    n == "SATURN"
  ) {

    val out =
      ArrayBuffer[Point]()

    var i = 0

    while (
      i <= 360
    ) {

      val a =
        i *
        math.Pi /
        180.0

      out +=
        new Point(
          CENTER_X +
          (
            math.cos(a) *
            220
          ).toInt,
          CENTER_Y +
          (
            math.sin(a) *
            220
          ).toInt
        )

      i += 3
    }

    out.toVector

  } else if (
    n == "CROWN"
  ) {

    Vector(
      new Point(260,480),
      new Point(220,170),
      new Point(390,300),
      new Point(500,120),
      new Point(610,300),
      new Point(780,170),
      new Point(740,480),
      new Point(260,480)
    )

  } else {

    val hash =
      math.abs(
        n.hashCode
      )

    val kind =
      hash % 5

    if (
      kind == 0
    ) {

      shapeGeometry(
        "HEXAGON"
      )

    } else if (
      kind == 1
    ) {

      shapeGeometry(
        "STAR 6"
      )

    } else if (
      kind == 2
    ) {

      shapeGeometry(
        "DIAMOND"
      )

    } else if (
      kind == 3
    ) {

      shapeGeometry(
        "PENTAGON"
      )

    } else {

      shapeGeometry(
        "OCTAGON"
      )
    }
  }
}


// ============================================================
// ADD GEOMETRY
// ============================================================

def addGeometry(
  pts: Vector[Point],
  name: String
): Unit = {

  remember()

  strokes +=
    DrawStroke(
      pts
    )

  repaintCanvas()

  setStatus(
    "ADDED: " +
    name
  )
}


def addShape(
  name: String
): Unit = {

  addGeometry(
    shapeGeometry(
      name
    ),
    name
  )
}


def addObject(
  name: String
): Unit = {

  addGeometry(
    objectGeometry(
      name
    ),
    name
  )
}


// ============================================================
// OBJECT SEARCH
// ============================================================

def searchObjects(): Unit = {

  val q =
    searchField
      .getText
      .trim
      .toLowerCase

  objectModel.clear()

  var i = 0

  while (
    i < objectNames.length
  ) {

    if (
      q.isEmpty ||
      objectNames(i)
        .toLowerCase
        .indexOf(q) >= 0
    ) {

      objectModel.addElement(
        objectNames(i)
      )
    }

    i += 1
  }

  setStatus(
    "OBJECT SEARCH COMPLETE"
  )
}


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

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

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

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

    "BOY",
    "GIRL",
    "MAN",
    "WOMAN",
    "PERSON",
    "ROBOT",
    "AI 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",
    "DONUT",
    "ICE CREAM",

    "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",
    "GOAL POST",

    "CYBER CITY",
    "FUTURE CAR",
    "FUTURE HOUSE",
    "NEON TOWER",
    "HOLOGRAM",
    "TIME MACHINE"
  )


var oi = 0

while (
  oi < objectNames.length
) {

  objectModel.addElement(
    objectNames(oi)
  )

  oi += 1
}


// ============================================================
// SHAPES
// ============================================================

val shapeNames =
  Array(
    "LINE",
    "SQUARE",
    "RECTANGLE",
    "ROUNDED RECTANGLE",
    "CIRCLE",
    "ELLIPSE",
    "TRIANGLE",
    "DIAMOND",
    "PENTAGON",
    "HEXAGON",
    "HEPTAGON",
    "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",
    "SNOWFLAKE",
    "SMILEY"
  )


var shi = 0

while (
  shi < shapeNames.length
) {

  shapeModel.addElement(
    shapeNames(shi)
  )

  shi += 1
}


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

def start2DAnimation(): Unit = {

  if (
    strokes.isEmpty
  ) {

    setStatus(
      "DRAW SOMETHING FIRST"
    )

  } else {

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

    preview.setDefaultCloseOperation(
      WindowConstants.DISPOSE_ON_CLOSE
    )

    preview.setSize(
      1100,
      750
    )

    preview.setLocationRelativeTo(
      frame
    )

    val data =
      copyStrokes(
        strokes
      )

    var strokeIndex =
      0

    var pointIndex =
      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 /
                CANVAS_W,
              (
                getHeight -
                40
              ).toDouble /
                CANVAS_H
            )

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

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

          val old =
            g2.getTransform

          g2.translate(
            ox,
            oy
          )

          g2.scale(
            scale,
            scale
          )

          g2.setColor(
            Color.WHITE
          )

          g2.fillRect(
            0,
            0,
            CANVAS_W,
            CANVAS_H
          )

          g2.setColor(
            currentColor
          )

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

          var i = 0

          while (
            i < strokeIndex
          ) {

            paintStroke(
              g2,
              data(i).points
            )

            i += 1
          }

          if (
            strokeIndex <
            data.length
          ) {

            val pts =
              data(
                strokeIndex
              ).points

            val count =
              math.min(
                pointIndex,
                pts.length
              )

            if (
              count > 0
            ) {

              paintStroke(
                g2,
                pts.take(
                  count
                )
              )
            }
          }

          g2.setTransform(
            old
          )
        }
      }


    val play =
      makeButton(
        "PLAY / PAUSE"
      )

    val reset =
      makeButton(
        "RESET"
      )

    val close =
      makeButton(
        "CLOSE"
      )


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

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

            if (
              running
            ) {

              if (
                strokeIndex <
                data.length
              ) {

                pointIndex += 4

                if (
                  pointIndex >=
                  data(
                    strokeIndex
                  ).points.length
                ) {

                  strokeIndex += 1
                  pointIndex = 1
                }

                panel.repaint()

              } else {

                running =
                  false
              }
            }
          }
        }
      )


    play.addActionListener(
      new ActionListener {

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

          running =
            !running
        }
      }
    )


    reset.addActionListener(
      new ActionListener {

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

          strokeIndex =
            0

          pointIndex =
            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(
          FlowLayout.CENTER
        )
      )

    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()
  }
}


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

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

  val radians =
    angle *
    math.Pi /
    180.0

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

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

  new Point(
    ox +
    (
      rx *
      scale
    ).toInt,

    oy +
    (
      y *
      scale -
      rz *
      scale *
      0.50
    ).toInt
  )
}


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

  if (
    pts.length < 2
  ) {

    return
  }

  val front =
    ArrayBuffer[Point]()

  val back =
    ArrayBuffer[Point]()

  var i = 0

  while (
    i < pts.length
  ) {

    front +=
      project3D(
        pts(i).x -
        CENTER_X,

        pts(i).y -
        CENTER_Y,

        0,

        angle,
        scale,
        ox,
        oy
      )

    back +=
      project3D(
        pts(i).x -
        CENTER_X,

        pts(i).y -
        CENTER_Y,

        depth,

        angle,
        scale,
        ox,
        oy
      )

    i += 1
  }


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

  g2.setColor(
    new Color(
      80,
      150,
      245
    )
  )

  i = 1

  while (
    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(
      150,
      190,
      255
    )
  )

  i = 1

  while (
    i < back.length
  ) {

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

    i += 1
  }


  g2.setColor(
    new Color(
      40,
      90,
      170
    )
  )

  i = 0

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

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

    i += 1
  }
}


def start3DPreview(): Unit = {

  if (
    strokes.isEmpty
  ) {

    setStatus(
      "DRAW OR ADD OBJECT FIRST"
    )

  } else {

    val preview =
      new JFrame(
        "ULTRA 3D DRAWING MODEL"
      )

    preview.setDefaultCloseOperation(
      WindowConstants.DISPOSE_ON_CLOSE
    )

    preview.setSize(
      1100,
      760
    )

    preview.setLocationRelativeTo(
      frame
    )

    val data =
      copyStrokes(
        strokes
      )

    var angle =
      0.0

    var depth =
      80.0

    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 w =
            getWidth

          val h =
            getHeight

          g2.setPaint(
            new GradientPaint(
              0,
              0,
              new Color(
                5,
                15,
                40
              ),
              0,
              h,
              new Color(
                70,
                20,
                105
              )
            )
          )

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

          var i = 0

          while (
            i < data.length
          ) {

            render3D(
              g2,
              data(i).points,
              angle +
              i * 7,
              depth +
              i * 6,
              0.70,
              w / 2,
              h / 2
            )

            i += 1
          }

          g2.setColor(
            Color.WHITE
          )

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

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

          g2.setFont(
            new Font(
              "Arial",
              Font.PLAIN,
              14
            )
          )

          g2.drawString(
            "DEPTH: " +
            depth.toInt,
            25,
            60
          )
        }
      }


    val play =
      makeButton(
        "PLAY / PAUSE"
      )

    val reset =
      makeButton(
        "RESET"
      )

    val depthPlus =
      makeButton(
        "DEPTH +"
      )

    val depthMinus =
      makeButton(
        "DEPTH -"
      )

    val close =
      makeButton(
        "CLOSE"
      )


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

    controls.add(
      play
    )

    controls.add(
      reset
    )

    controls.add(
      depthPlus
    )

    controls.add(
      depthMinus
    )

    controls.add(
      close
    )


    val timer =
      new Timer(
        30,
        new ActionListener {

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

            if (
              running
            ) {

              angle +=
                2.0

              panel.repaint()
            }
          }
        }
      )


    play.addActionListener(
      new ActionListener {

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

          running =
            !running
        }
      }
    )


    reset.addActionListener(
      new ActionListener {

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

          angle =
            0.0

          depth =
            80.0

          panel.repaint()
        }
      }
    )


    depthPlus.addActionListener(
      new ActionListener {

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

          depth =
            math.min(
              300.0,
              depth + 10.0
            )

          panel.repaint()
        }
      }
    )


    depthMinus.addActionListener(
      new ActionListener {

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

          depth =
            math.max(
              10.0,
              depth - 10.0
            )

          panel.repaint()
        }
      }
    )


    close.addActionListener(
      new ActionListener {

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

          timer.stop()

          preview.dispose()
        }
      }
    )


    preview.setLayout(
      new BorderLayout()
    )

    preview.add(
      panel,
      BorderLayout.CENTER
    )

    preview.add(
      controls,
      BorderLayout.SOUTH
    )

    preview.setVisible(
      true
    )

    timer.start()
  }
}


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

pencilButton =
  makeButton(
    "PENCIL"
  )

eraserButton =
  makeButton(
    "ERASER"
  )

undoButton =
  makeButton(
    "UNDO"
  )

redoButton =
  makeButton(
    "REDO"
  )

clearButton =
  makeButton(
    "CLEAR"
  )

generateButton =
  makeButton(
    "GENERATE CODE"
  )

copyButton =
  makeButton(
    "COPY CODE"
  )

saveButton =
  makeButton(
    "SAVE CODE"
  )

animationButton =
  makeButton(
    "2D ANIMATION"
  )

model3DButton =
  makeButton(
    "3D MODEL"
  )

gridButton =
  makeButton(
    "GRID"
  )

snapButton =
  makeButton(
    "SNAP"
  )

colorButton =
  makeButton(
    "COLOR"
  )


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

searchField =
  new JTextField(
    12
  )

val searchButton =
  makeButton(
    "SEARCH"
  )


// ============================================================
// LISTS
// ============================================================

objectList =
  new JList[String](
    objectModel
  )

objectList.setSelectionMode(
  ListSelectionModel.SINGLE_SELECTION
)

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


shapeList =
  new JList[String](
    shapeModel
  )

shapeList.setSelectionMode(
  ListSelectionModel.SINGLE_SELECTION
)

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


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

codeArea =
  new JTextArea()

codeArea.setEditable(
  false
)

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

codeArea.setLineWrap(
  false
)

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


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

speedSlider =
  new JSlider(
    5,
    100,
    25
  )

speedSlider.setPreferredSize(
  new Dimension(
    130,
    32
  )
)

speedSlider.addChangeListener(
  new javax.swing.event.ChangeListener {

    override def stateChanged(
      e: javax.swing.event.ChangeEvent
    ): Unit = {

      animationSpeed =
        speedSlider.getValue
    }
  }
)


// ============================================================
// 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(
  saveButton
)

toolbar.add(
  animationButton
)

toolbar.add(
  model3DButton
)

toolbar.add(
  gridButton
)

toolbar.add(
  snapButton
)

toolbar.add(
  colorButton
)

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

toolbar.add(
  speedSlider
)


// ============================================================
// LEFT LIBRARY
// ============================================================

val libraryTabs =
  new JTabbedPane()


val objectHeader =
  new JPanel(
    new BorderLayout()
  )

val objectTitle =
  new JLabel(
    "ALL OBJECTS"
  )

objectTitle.setFont(
  new Font(
    "Arial",
    Font.BOLD,
    17
  )
)

objectHeader.add(
  objectTitle,
  BorderLayout.NORTH
)

objectHeader.add(
  searchField,
  BorderLayout.CENTER
)

objectHeader.add(
  searchButton,
  BorderLayout.EAST
)


val objectPanel =
  new JPanel(
    new BorderLayout(
      4,
      4
    )
  )

objectPanel.add(
  objectHeader,
  BorderLayout.NORTH
)

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


val shapePanel =
  new JPanel(
    new BorderLayout(
      4,
      4
    )
  )

val shapeTitle =
  new JLabel(
    "ALL SHAPES"
  )

shapeTitle.setFont(
  new Font(
    "Arial",
    Font.BOLD,
    17
  )
)

shapePanel.add(
  shapeTitle,
  BorderLayout.NORTH
)

shapePanel.add(
  new JScrollPane(
    shapeList
  ),
  BorderLayout.CENTER
)


libraryTabs.addTab(
  "OBJECTS",
  objectPanel
)

libraryTabs.addTab(
  "SHAPES",
  shapePanel
)


// ============================================================
// CANVAS WRAPPER
// ============================================================

val canvasWrapper =
  new JPanel(
    new BorderLayout(
      4,
      4
    )
  )

val canvasTitle =
  new JLabel(
    "DRAWING CANVAS ? DRAG DIVIDERS TO RESIZE"
  )

canvasTitle.setFont(
  new Font(
    "Arial",
    Font.BOLD,
    16
  )
)

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

canvasWrapper.add(
  canvasTitle,
  BorderLayout.NORTH
)

canvasWrapper.add(
  canvas,
  BorderLayout.CENTER
)


// ============================================================
// CODE WRAPPER
// ============================================================

val codeWrapper =
  new JPanel(
    new BorderLayout(
      4,
      4
    )
  )

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

codeTitle.setFont(
  new Font(
    "Arial",
    Font.BOLD,
    16
  )
)

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

codeWrapper.add(
  codeTitle,
  BorderLayout.NORTH
)

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


// ============================================================
// CANVAS <-> CODE MOVABLE DIVIDER
// ============================================================

val canvasCodeSplit =
  new JSplitPane(
    JSplitPane.HORIZONTAL_SPLIT,
    canvasWrapper,
    codeWrapper
  )

canvasCodeSplit.setDividerLocation(
  620
)

canvasCodeSplit.setResizeWeight(
  0.60
)


// ============================================================
// LIBRARY <-> MAIN MOVABLE DIVIDER
// ============================================================

val mainSplit =
  new JSplitPane(
    JSplitPane.HORIZONTAL_SPLIT,
    libraryTabs,
    canvasCodeSplit
  )

mainSplit.setDividerLocation(
  260
)

mainSplit.setResizeWeight(
  0.0
)


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

status =
  new JLabel(
    "READY - DRAW ANYTHING"
  )

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


// ============================================================
// FRAME
// ============================================================

frame =
  new JFrame(
    "ULTRA LEGEND DRAW LAB X"
  )

frame.setDefaultCloseOperation(
  WindowConstants.EXIT_ON_CLOSE
)

frame.setSize(
  1650,
  900
)

frame.setMinimumSize(
  new Dimension(
    1050,
    650
  )
)

frame.setLocationRelativeTo(
  null
)

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

frame.add(
  toolbar,
  BorderLayout.NORTH
)

frame.add(
  mainSplit,
  BorderLayout.CENTER
)

frame.add(
  status,
  BorderLayout.SOUTH
)


// ============================================================
// BUTTON EVENTS
// ============================================================

pencilButton.addActionListener(
  new ActionListener {

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

      eraserOn =
        false

      setStatus(
        "PENCIL MODE"
      )

      canvas.requestFocusInWindow()
    }
  }
)


eraserButton.addActionListener(
  new ActionListener {

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

      eraserOn =
        true

      setStatus(
        "ERASER MODE"
      )

      canvas.requestFocusInWindow()
    }
  }
)


undoButton.addActionListener(
  new ActionListener {

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

      doUndo()

      canvas.requestFocusInWindow()
    }
  }
)


redoButton.addActionListener(
  new ActionListener {

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

      doRedo()

      canvas.requestFocusInWindow()
    }
  }
)


clearButton.addActionListener(
  new ActionListener {

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

      clearDrawing()

      canvas.requestFocusInWindow()
    }
  }
)


generateButton.addActionListener(
  new ActionListener {

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

      generateKojoCode()

      canvas.requestFocusInWindow()
    }
  }
)


copyButton.addActionListener(
  new ActionListener {

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

      copyCode()

      canvas.requestFocusInWindow()
    }
  }
)


saveButton.addActionListener(
  new ActionListener {

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

      saveCode()
    }
  }
)


animationButton.addActionListener(
  new ActionListener {

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

      start2DAnimation()
    }
  }
)


model3DButton.addActionListener(
  new ActionListener {

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

      start3DPreview()
    }
  }
)


gridButton.addActionListener(
  new ActionListener {

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

      gridOn =
        !gridOn

      repaintCanvas()

      setStatus(
        if (
          gridOn
        )
          "GRID ON"
        else
          "GRID OFF"
      )

      canvas.requestFocusInWindow()
    }
  }
)


snapButton.addActionListener(
  new ActionListener {

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

      snapOn =
        !snapOn

      setStatus(
        if (
          snapOn
        )
          "SNAP ON"
        else
          "SNAP OFF"
      )

      canvas.requestFocusInWindow()
    }
  }
)


colorButton.addActionListener(
  new ActionListener {

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

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

      if (
        selected != null
      ) {

        currentColor =
          selected

        repaintCanvas()

        setStatus(
          "COLOR CHANGED"
        )
      }

      canvas.requestFocusInWindow()
    }
  }
)


// ============================================================
// SEARCH EVENTS
// ============================================================

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
          )

          canvas.requestFocusInWindow()
        }
      }
    }
  }
)


// ============================================================
// SHAPE DOUBLE CLICK
// ============================================================

shapeList.addMouseListener(
  new MouseAdapter {

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

      if (
        e.getClickCount == 2
      ) {

        val value =
          shapeList.getSelectedValue

        if (
          value != null
        ) {

          addShape(
            value
          )

          canvas.requestFocusInWindow()
        }
      }
    }
  }
)


// ============================================================
// MOUSE DRAWING
// ============================================================

canvas.addMouseListener(
  new MouseAdapter {

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

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

        remember()

        activeStroke.clear()

        activeStroke +=
          mousePoint(
            e
          )

        drawingNow =
          true

        setStatus(
          "DRAWING..."
        )

        repaintCanvas()
      }
    }


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

      if (
        drawingNow
      ) {

        activeStroke +=
          mousePoint(
            e
          )

        if (
          eraserOn
        ) {

          val hit =
            activeStroke.last

          val remaining =
            ArrayBuffer[DrawStroke]()

          var i = 0

          while (
            i < strokes.length
          ) {

            val stroke =
              strokes(i)

            var remove =
              false

            var j = 0

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

              if (
                distance(
                  hit,
                  stroke.points(j)
                ) < 35
              ) {

                remove =
                  true
              }

              j += 1
            }

            if (
              !remove
            ) {

              remaining +=
                stroke
            }

            i += 1
          }

          strokes.clear()

          strokes ++=
            remaining

        } else {

          if (
            activeStroke.length >= 2
          ) {

            strokes +=
              DrawStroke(
                activeStroke.toVector
              )
          }
        }

        activeStroke.clear()

        drawingNow =
          false

        repaintCanvas()

        setStatus(
          "STROKE RECORDED"
        )

        canvas.requestFocusInWindow()
      }
    }
  }
)


// ============================================================
// MOUSE DRAGGING
// ============================================================

canvas.addMouseMotionListener(
  new MouseMotionAdapter {

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

      if (
        drawingNow
      ) {

        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 k =
        e.getKeyCode

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

        doUndo()

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

        doRedo()

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

        generateKojoCode()

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

        copyCode()

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

        saveCode()

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

        start2DAnimation()

      } else if (
        e.isControlDown &&
        k == KeyEvent.VK_3
      ) {

        start3DPreview()

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

        clearDrawing()

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

        eraserOn =
          !eraserOn

        setStatus(
          if (
            eraserOn
          )
            "ERASER ON"
          else
            "PENCIL ON"
        )

      } else if (
        k == 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" +
          "CTRL + 3  = 3D MODEL\n" +
          "DELETE    = CLEAR\n" +
          "E         = ERASER / PENCIL\n" +
          "F1        = THIS HELP\n\n" +
          "OBJECTS: DOUBLE CLICK\n" +
          "SHAPES: DOUBLE CLICK\n" +
          "DRAG DIVIDERS TO RESIZE PANELS",
          "SECRET CONTROL CENTER",
          JOptionPane.INFORMATION_MESSAGE
        )
      }
    }
  }
)


// ============================================================
// START
// ============================================================

SwingUtilities.invokeLater(
  new Runnable {

    override def run(): Unit = {

      frame.setVisible(
        true
      )

      canvas.requestFocusInWindow()

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