local imat = mat4()
local sin = math.sin
local cos = math.cos
local matvec4 = { 1, 1, 1, 1 }
local matvec = { 1, 1, 1, 1 }
local iray = { position = vec3(), direction = vec3() }

local function matvecmul(m, v, z)
  matvec[1], matvec[2], matvec[3], matvec[4] = v.x, v.y, v.z, z and 0 or 1
  local s, r = pcall(mat4.mul_vec4, matvec4, m, matvec)
  return matvec4[1], matvec4[2], matvec4[3]
end

local function wobbleNumber(w, speed, dist)
  game.wobbleCount = game.wobbleCount + 1
  local x = (time + game.wobbleCount) * (speed or 0)
  return w + (sin(x) + 0.5 * sin(2 * x) + sin(x) * cos(x * pi)) * (dist or 0) * 0.1
end

local function defaultCardStyle()
  return {
    time = 0,
    size = 32,
    center = false,
    over = false,
    box = false,
    wobble = false,
  }
end

local function recurse(f, x, n)
  local r = x
  for i = 1, n, 1 do
    r = f(r)
  end
  return r
end

local function getSymbolsUsed(entity, symbols)
  if load_getSymbol(entity.key) then
    if not lume.find(symbols, entity.key) then
      table.insert(symbols, entity.key)
      for a, anim in pairs(entity.symbol.animations) do
        for l, layer in ipairs(anim.layers) do
          for f, frame in ipairs(layer.frames) do
            for s, symbol in ipairs(frame.symbols) do
              getSymbolsUsed(symbol.entity, symbols)
            end
          end
        end
      end
      print("", entity.key)
    end
  end
end

local function updateActiveRooms(depth)
  local key = ctx.viewport.key
  local active = {}

  for r, room in pairs(ctx.views) do
    key = room.key
    if not lume.find(active, room.key) then table.insert(active, room.key) end
    if depth > 0 then
      for c, connect in pairs(ctx.level.connections) do
        local side1 = connect[1].room == key and 1 or (connect[2].room == key and 2 or 0)
        local side2 = side1 == 1 and 2 or side1 == 2 and 1 or 0
        if side1 > 0 then
          if not lume.find(active, connect[side2].room) then table.insert(active, connect[side2].room) end
        end
      end
    end
  end

  return active
end

local function activeRoomsChanged(t1, t2)
  if #t1 ~= #t2 then return true end

  for i = 1, #t1, 1 do
    if not lume.find(t2, t1[i]) then return true end
  end
  return false
end

local function sortEntities(room)
  --table.sort    (room.entities, f(a, b) if a.z == b.z then return a.uid > b.uid else return a.z < b.z end end)
end

local function updateEntities(room)
  bench.push("entities")
  for i, entity in ipairs(room.entities) do
    entity.index = i

    if load_getSymbol(entity.key) == nil then
      scene_setDelete(entity)
    elseif not (animatePanel.open and animatePanel.backing) then
      --scene_updateEntityRenderInfo(entity)

      if editor.intersect then
        -- editor.modelIntersect = editor.intersect:clone()
      end

      scene_updateEntityVars(entity)
      scene_updateScripts(entity)
      scene_updateEntityAnimation(entity)
      scene_updateEntityAnimData(entity)
      scene_updateEntityBounds(entity)
      editor_updateEntityTransform(entity)
      scene_updateEntityRenderInfo(entity)
      scene_updateEntityLasso(entity)

      entity.colID = (entity.i or #room.entities + 1) / #room.entities
      for m, model in ipairs(entity.models) do
        model.i = entity.colID
        model.m = m / #entity.models
      end
    end

    if not entity.simple then
      if ctx.viewport.key == room.key then table.add(ctx.tagged.Room, entity.key) end
      table.add(ctx.tagged.Level, entity.key)
    end
  end
  bench.pop("entities")
end

local function loadEntities(room)
  for e, entity in ipairs(room.instances) do
    entity.i = e
    if load_getSymbol(entity.key) then scene_newEntity(room, entity) end
  end
end

local function addEnts(room)
  for i, part in ipairs(room.entsToAdd or {}) do
    part()
  end
  room.entsToAdd = {}
end

local function newModel(mesh, transform, parent)
  return {
    mesh = mesh or cubeMesh,
    transform = transform or mat4(),
    parent = parent or ctx.viewport.room,
  }
end

local function reset()
  game.time = game.time + 1
  editor.hoveredEntity = nil
  anySelectedSimple = false
  ctx.tagged.Level = {}
  ctx.tagged.Room = {}

  if not contextMenu.open then
    editor.modelHovered = nil
    editor.entityHovered = nil
    editor.intersect = nil

    if editor.preModelHovered then
      editor.modelHovered = editor.preModelHovered
      if editor.modelHovered.parent and editor.modelHovered.parent.isEntity then
        editor.entityHovered = editor.modelHovered.parent
      else
        if not pathsPanel.open then
          pathsPanel.preview = true
          pathsPanel.id = editor.viewport.room.key
          pathsPanel.hoveredPath = editor.modelHovered.id
          pathsPanel.mode = editor.modelHovered.mode
        end
      end
    end

    editor.preModelHovered = nil
  end
end

local function tweens()
  game.wobbleCount = 0
  for t, tween in lume.ripairs(game.tweens) do
    local progress = mc(0, (time - tween.startTime) / tween.duration, 1)
    for p, prop in pairs(tween.props) do
      if prop.startValue then
        if tween.kind == "linear" then
          tween.thing[prop.key] = lerp(prop.startValue, prop.endValue, progress)
        elseif tween.kind == "easeOut" then
          tween.thing[prop.key] = lerp(prop.startValue, prop.endValue, lerp(progress, 1 + (progress - 1) ^ 5, 1))
        elseif tween.kind == "easeIn" then
          tween.thing[prop.key] = lerp(prop.startValue, prop.endValue, lerp(progress, progress ^ 5, 1))
        elseif tween.kind == "easeInOut" then
          tween.thing[prop.key] = lerp(prop.startValue, prop.endValue, recurse(easeInOut, progress, 5))
        end
        -- print("tween",prop.key,tween.thing[prop.key])
      end
    end
    if progress >= 1 then
      tween.func()
      table.remove(game.tweens, t)
    end
  end
end

local function transfer()
  for i, t in ipairs(game.transfers) do
    if t.dest then
      scene_destroyEntity(t.entity)
      lume.remove(ctx.rooms[t.source].entities, t.entity)
      table.insert(ctx.rooms[t.dest].entities, t.entity)
      t.entity.x, t.entity.y, t.entity.z = t.pos.x, t.pos.y, t.pos.z
    end
    t.entity:setPosition(t.pos.x, t.pos.y, t.pos.z)
    t.entity.floor = t.pos.y
    t.entity:setVelocity(nil, 0, nil)
    t.entity.transferring = false

    if t.entity.switchOnTransfer and not game_setContains(game.page, t.dest) then
      --game_loadSet(game_findSet(t.dest))
    end
  end
  game.transfers = {}
end

local function updatePlanes(room)
  if #room.nextPlanes > 0 then
    room.planes = lume.sort(room.nextPlanes, "y")
    room.nextPlanes = {}
  end
end

local function updateViews()
  if not (_entered and _entered == 2) then
    _entered = _entered and 2 or 1
    local proj = config.projects[engine.project]
    if proj.page then
      print("OPEN PAGE", proj.pageView)
      scene_loadSet(proj.page)
      ctx.pageView = proj.pageView
      if not ctx.pageView then
        print("ENTER VIEW:")
        local view = lume.match(ctx.views, function(v)
          return v.id == proj.view
        end)
        if view then
          ctx.roomZoom = 1
          editor.viewport = view
          scene_setNextView(view)
          print("         ", proj.view, editor.viewport.key)
          -- game_enterRoom()
        else
          print("invalid vieew")
        end
      end
    else
      print("OPEN ROOM")
      game_openRoom(proj.room)
    end
  end

  local s = 0.05
  ctx.roomZoom = ctx.isAnim and 0 or 
  -- (ctx.pageView and 1 or 0)
  easeTo(ctx.roomZoom,ctx.pageView and 1 or 0,2,3)
  -- math.clamp(0,1,ctx.pageView and ctx.roomZoom +s or ctx.roomZoom-s)


  local main = ctx.editing and editor.viewport or game.viewport
  local views = ctx.roomZoom > 0.01 and ctx.views or { main }

  if lume.count(views) > 1 then
    table.sort(views, function(a, b)
      if a.depth == b.depth then
        if a.room.name == b.room.name then
          return a.viewportID < b.viewportID
        else
          return a.room.name < b.room.name
        end
      else
        return a.depth < b.depth
      end
    end)
    if ctx.roomZoom > 0.01 then
      local k = lume.find(views, main)
      table.remove(views, k)
      table.insert(views, main)
    end
  end

  setmetatable(ctx, {
    __newindex = function(table, key, value)
      if key == "views" then
        error("FUCK")
      else
        rawset(table, key, value)
      end
    end,
  })

  for v, view in ipairs(views) do
    view.room.updated = false
    view.room.draws = {}
  end

  bench.push("updateRooms")

  lume.clear(game.views)

  for v, view in ipairs(views) do
    bench.push("updateRoom")
    local room = scene_setView(view)
    local draw = (ctx.roomZoom > 0 or view == editor.viewport)

    if room then
      if not room.updated then
        addEnts(room)
        sortEntities(room)
      end

      local update
      if ctx.editing and ctx.viewport.key == view.key then
        update = view == ctx.viewport
      else
        update = not room.updated
      end

      if update then updateEntities(room) end

      if not room.updated then
        scene_updateScripts(room)
        floors_addPlanes(room)
        updatePlanes(room)
        room.updated = true
      end

      if draw then
        if view.viewport and not planPanel.skipBehind then view_pushView(view) end
      end
      scene_deleteEntities(room)
    end
    bench.pop("updateRoom")
  end

  bench.pop("updateRooms")

  if ctx.isAnim then return end

  -- bench.push("drawStacks")
  -- pushDrawUI
  --  plan_drawLinkPreview()
  --  -- 
  -- bench.pop("drawStacks")plan_drawRoomStack()

  prevRoomZoom = ctx.roomZoom
  prevRoom = ctx.viewport.room
end

local function drawCard()
  if game.cards[1] then
    local card = game.cards[1]
    if card.over then drawSetBackground(ctx.page) end
    local height = card.size * game.resolution
    local cb = scene_wobble(card.box, 0.4, 0.05)
    gfx.setFont(fonts.interfont, height)
    gfx.setColor(card.color.r, card.color.g, card.color.b, 1)
    gfx.rectangle("fill", cb.x * renderW, cb.y * renderH, cb.w * renderW, cb.h * renderH)
    gfx.setColor(card.textColor.r, card.textColor.g, card.textColor.b, 1)
    shaders.harsh:send("tint", { card.textColor.r, card.textColor.g, card.textColor.b, 1 })
    gfx.setShader(shaders.harsh)
    gfx.setBlendMode("alpha")

    if card.wobble then
      local font = gfx.getFont()
      local wwidth = font:getWidth(card.text) * 1.1
      local xoff = card.center.x * renderW - wwidth / 2
      for i = 1, #card.text do
        local letter = card.text:sub(i, i)
        local lwidth = font:getWidth(letter)
        gfx.printf(
          letter,
          scene_wobble(xoff, card.wobble[1], card.wobble[2]),
          scene_wobble(card.center.y * renderH - height / 2, card.wobble[1], card.wobble[2]),
          lwidth,
          "left"
        )
        xoff = xoff + lwidth * 1.1
      end
    else
      gfx.printf(card.text, (card.center.x - card.wrap / 2) * renderW, card.center.y * renderH - height / 2, card.wrap * renderW, "center")
    end

    if not ctx.editing then card.time = card.time - 1 end
    if card.time <= 0 then table.remove(game.cards, 1) end
  end
end

local function loadAssets(depth)
  local new = updateActiveRooms(1)

  if editor.debug.trace_active_rooms then
    trace("")
    trace("Active Rooms:")
    for i, v in ipairs(new) do
      trace("  " .. ctx.rooms[v].name)
    end
  end

  game.toUnload = {}
  game.toLoad = {}

  if editor.refresh or activeRoomsChanged(new, game.activeRooms) then
    print(ansicolors("\n%{bluebg}CHECK FOR SYMBOLS TO LOAD%{reset}"))
    game.symbolsLoaded = game.symbolsLoaded or {}
    game.activeRooms = new

    local symbolsLoaded = {}
    print("new rooms to load from:")
    for i, v in ipairs(new) do
      print("", ansicolors("%{red}" .. v .. "%{reset}"))
    end

    print("\npreload symbols:")
    for r, room in pairs(game.activeRooms) do
      if multicheckOR(room, game.activeRooms) then
        for e, entity in ipairs(ctx.rooms[room].instances) do
          -- getSymbolsUsed(entity,symbolsLoaded,room)
          if load_getSymbol(entity.key) then
            if not lume.find(symbolsLoaded, entity.key) then table.insert(symbolsLoaded, entity.key) end
          end
        end
      end
    end

    local unloaded = false
    for i, v in ipairs(game.symbolsLoaded) do
      if not lume.find(symbolsLoaded, v) then
        if not unloaded then
          print("\nsymbols to unload:")
          unloaded = true
        end
        print("", ansicolors("%{red}" .. v .. "%{reset}"))
        table.insert(game.toUnload, v)
      end
    end

    game.symbolsLoaded = symbolsLoaded

    for i, v in pairs(game.symbolsLoaded) do
      if not load_getSymbol(v).image then table.insert(game.toLoad, v) end
    end

    local garbage = collectgarbage("count")
    unloaded = false
    for i, v in ipairs(game.toUnload) do
      if not unloaded then
        print("\nunload:")
        unloaded = true
      end
      print("  ", i, v)
      -- print("imgs"..gfx.getStats().images)
      load_releaseSymbol(v)
      -- print("imgs"..gfx.getStats().images)
    end

    collectgarbage("collect")

    print("\n Size of assets loaded: ", collectgarbage("count") - garbage)

    bench.push("load")

    load_loadGameImages(game.toLoad)
    bench.pop("load")
    -- collectgarbage("collect")
  end

  load_loadLoop()

  --config.debug and config.debug.trace_loaded_images then
end


------------------------------------------------------------------------------
------------------------------------------------------------------------------
-- GLOBALS            
------------------------------------------------------------------------------
------------------------------------------------------------------------------


function scene_load()
  game.stiffness = 1.8
  game.time = 0
  game.meter = 32
  game.transfers = {}
  game.cards = {}
  game.tweens = {}
  game.wobbleCount = 0

  ctx.roomStack = {}

  -- cubeMesh = gfx.newMesh(zformat, obj_loader(LINK_DIR.."models/cube.obj"), "triangles")

  input_resetControls()

  scene_loadSet(game.state == "Edit" and editor.editingPage or ctx.level.defaultPage)

  print("CTX VIEWPORT", ctx.viewport, ctx.viewport.room)
  ctx.roomZoom = ctx.editing and ctx.roomZoom or (ctx.pageView and 1 or 0)

  game.setNames = {}
  for s, set in pairs(ctx.level.pages) do
    set.ox = set.x or set.ox or 0
    set.oy = set.y or set.oy or 0
    if not set.preview and lfs.exists("cache/" .. set.key .. ".png") then
      set.preview = gfx.newImage("cache/" .. set.key .. ".png")
      editor.setPreviews[set.key] = set.preview
    end
    table.insert(game.setNames, set.name)
    set.dateCreated = set.dateCreated or os.time()
  end

  game.spawned = {}
  game.activeRooms = {}
  game.toUnload = {}
  game.toLoad = {}
  game.loadDepth = 1
  game.views = {}
  --scene_loadAssets(game.loadDepth)

  for r, room in pairs(ctx.rooms) do
    scene_loadRoom(r)
  end

  editor.keepCamera = nil

  gfx.setLineJoin("none")
end

function scene_loadRoom(r)
  local room = ctx.rooms[r]

  room.isRoom = true
  room.onStage = 0
  room.resort = false
  room.zbounds.range = room.zbounds.front - room.zbounds.back

  --room.x,room.y,room.z  = 0,0,0
  room.bx, room.by, room.bz = 0, 0, 0
  room.links = {}
  room.instances = room.instances or room.objects
  room.objects = nil
  room.entities = { nocopy = true }
  room.entitiesByID = { nocopy = true }
  room.wind = {}
  room.planes = {}
  room.nextPlanes = {}
  room.toDelete = {}
  room.entsToAdd = {}
  room.parentFinder = {}
  room.draws = {}
  room.cards = {}
  room.lights = { nocopy = true }
  room.delayed = {}
  room.dialogue = room.dialogue or {}
  room.transform = mat4()
  room.lighting = room.lighting
    or {
      enabled = false,
      ambient = { amount = 1, color = { r = 1, g = 1, b = 1 }, enabled = false },
      dynamic = { amount = 1, color = { r = 1, g = 1, b = 1 }, enabled = false },
      shadows = { amount = 1, color = { r = 1, g = 1, b = 1 }, enabled = false },
    }
  room.lighting.toon = room.lighting.toon or {}
  room.lighting.map = room.lighting.map or {}
  room.lighting.static = room.lighting.static or {}
  room.lighting.render = room.lighting.render or {}
  room.lighting.probes = room.lighting.probes or {}
  room.lighting.vct = room.lighting.vct or {}
  
  room.sky.skybox = room.sky.skybox or {}

  room.x = 0
  room.y = 0

  room.lightPosition = { nocopy = true, setAll({ 0, 0, 0 }) }
  room.lightColor = { nocopy = true, setAll({ 1, 1, 1 }) }
  room.lightPower = { nocopy = true, setAll(1) }
  room.lightRange = { nocopy = true, setAll(50) }
  room.lightAperture = { nocopy = true, setAll(45) }
  room.lightFalloff = { nocopy = true, setAll(0) }
  room.lightShadowed = { nocopy = true, setAll(false) }
  room.lightDirection = { nocopy = true, setAll({ 0, 0, 0 }) }
  room.lightType = { nocopy = true, setAll(1) }
  room.lightQuad = { nocopy = true, setAll({ 0, 0, 1, 1 }) }

  room.selectedEntities = room.selectedEntities or {}

  if not room.preview and lfs.exists("cache/" .. room.key .. ".png") then
    room.preview = gfx.newImage("cache/" .. room.key .. ".png")
    editor.roomPreviews[room.key] = room.preview
    room.dateCreated = room.dateCreated or os.time()
  end

  for c, cam in ipairs(room.cameras) do
    cam.index = c
    room.cameras[c] = camera_new(cam)
  end

  if not ctx.editing then
  end

  loadEntities(room)
  scene_computePath(room)
  floors_addPlanes(room)
  updatePlanes(room)
  scene_setScripts(room)
end

function scene_loadSet(key)
  local from = ctx.viewport and ctx.viewport.key or nil
  print("load set ", key, "/", lume.count(ctx.level.pages))
  -- os.exit()
  ctx.page = ctx.level.pages[key]
    or {
      views = {
        {
          viewportID = 1,
          cameraID = 1,
          depth = 1,
          key = room.key,
        },
      },
      key = "temp",
      name = "temp",
      dateCreated = 0,
    }
  ctx.views = ctx.page.views

  ctx.page.sky = ctx.page.sky
    or {
      image = {
        key = "",
        frame = 1,
        play = false,
        color = { r = 1, g = 1, b = 1, a = 1 },
      },
      color = { r = 0.8, g = 0.8, b = 0.8, a = 1 },
    }

  game.page = ctx.page.name
  for v, view in ipairs(ctx.views) do
    scene_setView(view)
  end
  local v, i = lume.match(ctx.views, function(x)
    return x.key == from
  end)
  ctx.nextView = ctx.nextView or (from and v or (ctx.views[1] or nil))
  -- input_resetControls()
  -- if ctx.editing then delayedCall(function()
  --   plan_cacheSetPreview()
  -- end, 0.1) end
end

function scene_setNextView(view)
  ctx.nextView = view
end

function scene_setView(view)
  if not ctx.rooms[view.key] then 
    for r,room in pairs(ctx.rooms) do 
      print("room",r,room.key)
    end
    print("view.key",view.key)
    error("ivalid room") 
  end

  local room = ctx.rooms[view.key]
  view.id = view.id or uuid()
  view.depth = view.depth or 1
  view.viewportID = view.viewportID or 1
  view.cameraID = view.cameraID or 1
  view.room = ctx.rooms[view.key]
  view.camera = view.camera or camera_new(room.cameras[view.cameraID])
  view.viewport = view.viewport or deepcopy(room.viewports[view.viewportID])
  editor.selectedEntities = ctx.rooms[view.key].selectedEntities or {}
  -- print("view.viewport",view.viewport)

  if not (view.room and view.viewport and view.camera) then
    print(lume.count(ctx.rooms[view.key].viewports), "view.viewportID", view.viewportID, ctx.rooms[view.key].viewports[view.viewportID])
    print(view.room, view.viewport, view.camera)
    error("bad room")
    os.exit()
  end
  -- print("ctx.viewport",ctx.viewport,view)
  ctx.viewport = view
  return view.room
end

function scene_destroy()
  for r, room in pairs(ctx.rooms) do
    room.draws = {}
    room.entities = {}
    for l, light in lume.ripairs(room.lights) do
      if light.canvasCache then light.canvasCache:release() end
      if light.canvas then light.canvas:release() end
      if light.canvas2 then light.canvas2:release() end
      room.lights[l] = nil
    end
  end
end

function scene_updateScene()
  bench.push("scene_update")

  reset()
  tweens()
  updateViews()
  drawCard()
  loadAssets(game.loadDepth)

  bench.pop("scene_update")
end

function scene_setScripts(target)
  target.public = { test = 5 }
  if not editor.debug.no_scripts then
    if target.scripts ~= nil and #target.scripts > 0 and not ctx.editing then
      for i, b in ipairs(target.scripts) do
        if scripts[b.key] then
          local success, result = pcall(lfs.load, LINK_DIR .. scripts[b.key].filename .. ".lua")
          if success then
            script, publics = result()
            scripts[b.key].broken = false
            script.target = target
            b.file = script
            if publics then
              for p, public in pairs(publics) do
                if target.public[p] then log("Public '" .. p .. "' already defined") end
                target.public[p] = public
              end
            end
          else
            scripts[b.key].broken = true
          end
        else
          target.scripts[i] = nil
          log("RRRREMoved")
        end
      end
    end
  end
end

function scene_setPlayer(entity)
  if entity.uid == ctx.viewport.room.player then
    entity.category = 4
    player = entity
  else
    entity.category = 2
  end
  entity.id = entity.uid
end

function scene_tween(thing, properties, duration, kind, ease, func)
  local props = {}
  for p, prop in pairs(properties) do
    table.insert(props, { key = p, startValue = thing[p], endValue = prop })
  end
  for t, tween in lume.ripairs(game.tweens) do
    if tween.thing == thing then
      for p, prop in pairs(properties) do
        if multicheckOR(p, tween.props, "key") then tween.props[multicheckOR(p, tween.props, "key")] = nil end
      end
    end
  end
  table.insert(game.tweens, {
    thing = thing,
    props = props,
    startTime = time,
    duration = duration,
    kind = kind or "linear",
    ease = ease or 1,
    func = func or function() end,
  })
end

function scene_flashCard(color, textColor, text, time, size, center, over, box, wobble)
  local time = (time == 0 and text:len() / 15 or time) * 60
  local box = box and {
    x = box.x or (1 - box.w) / 2,
    y = box.y or (1 - box.h) / 2,
    h = box.h or 1,
    w = box.w or 1,
  } or {}
  local card = { box = lume.merge({ x = 0, y = 0, w = 1, h = 1 }, box), wobble = wobble }
  card.wrap = card.wrap or card.box.w / 1.2
  card.color = (color ~= nil) and (#color > 0 and colorTable(color) or color) or room.sky.color
  local backDark = mean(card.color) < 100 and 80 or -80
  card.textColor = (textColor ~= nil) and (#textColor > 0 and colorTable(textColor) or textColor)
    or contrast({
      r = 1 - card.color.r + backDark,
      g = 1 - card.color.g + backDark,
      b = 1 - card.color.b + backDark,
    }, 0.2)
  card.size = size
  card.center = center or { x = card.box.x + card.box.w / 2, y = card.box.y + card.box.h / 2 }
  card.over = over

  if type(text) == "table" then
    local length
    if #text == 1 then
      length = text[1]:len()
      text = lume.split(text[1])
    else
      length = table.concat(text):len()
    end
    for i, v in pairs(text) do
      local card2 = shallowcopy(card)
      card2.text = v
      card2.time = mf(time / #text)
      --temp time proportional to length of word
      --card2.time = mf(time*(card2.text:len()/length))
      table.insert(game.cards, card2)
    end
  else
    card.text = text
    card.time = time
    table.insert(game.cards, card)
  end
end

function scene_toggleResolution()
  if altDown then
    if input_keyPressed("-") and game.resolution > 0.3 then game.newRes = game.resolution / 2 end
    if input_keyPressed("=") and game.resolution < 1 then game.newRes = game.resolution * 2 end
    if input_keyPressed("0") then game.newRes = 1 end
  end
end

function scene_wobble(wobbles, speed, dist, auto)
  local rval = wobbles
  if type(wobbles) == "table" then
    local t = {}
    if speed and dist then
      for k, w in pairs(wobbles) do
        if type(w) == "number" then
          t[k] = wobbleNumber(w, speed, dist) -- print("wobnum",t[k])
        end
      end
      rval = t
    elseif wobbles.wobbles and lume.count(wobbles.wobbles) > 0 then
      for k, w in pairs(wobbles.wobbles) do
        t[k] = wobbles[k] + wobbleNumber(0, w[1], w[2])
      end
      rval = t
    end
  else
    wobbles = wobbleNumber(wobbles, speed, dist)
    rval = wobbles
  end
  return rval
end

function scene_testCursorModel(model)
  return scene_testRayModel(ctx.viewport.camera.ray, model)
end

function scene_testRayModel(ray, model)
  bench.disable()
  if contextMenu.open or not ctx.viewport.camera.ray then
    bench.enable()
    return false
  end

  local success = false
  if model.models then
    for s, submodel in ipairs(model.models) do
      if scene_testRayModel(ray, submodel) then
        editor.preEntityHovered = model
        success = true
      end
    end
  else
    bench.push("tcm1")
    local tris

    if animatePanel.instance then
      if animatePanel.open and animatePanel.instance == parent then tris = animatePanel.l.data and animatePanel.l.data.tris end
    else
      -- model.bbox = scene_calcBBOX(model)
      tris = model.tris
    end
    bench.pop("tcm1")

    -- if  not tris then bench.enable() return end

    bench.push("tcm2")
    local pos = vec3(0)
    mat4.invert(imat, model.matrix or blankMat)
    iray.position:set(matvecmul(imat, ray.position))
    iray.direction:set(matvecmul(imat, ray.direction, true))
    bench.pop("tcm2")

    local dist, hit = 999999

    bench.push("tcm3")

    if (model.newbb or not model.bbox) and model.tris and not paths_isCurrentPath(model) then
      model.newbb = false
      -- model.bbox = scene_calcBBOX(model)
    end

    if model.verts and not model.bbox and not editor.transforming then
      -- trace("recalec")
      -- model.bbox = scene_calcBBOX(model)
    end

    bench.pop("tcm3")

    bench.push("tcm4")
    if model.bbox then
      hit = testTempBBOX(iray, model.bbox)
      model.rayBB = hit
    else
      hit = true
    end
    bench.pop("tcm4")

    bench.push("tcm5")
    if hit and model.mesh then
      -- trace("TRIANGLES",model.mesh.mesh.triangleCount, model.mesh.mesh.components)
      local hit, distance, position, normal = testTempModel(iray, model.mesh)

      if hit and (distance < editor.nearestPlane) then
        trace("HIT", position, model)
        if model.parent and model.parent.isEntity then
          trace("model.isEntity", model.m)
        end
        editor.nearestPlane = distance
        editor.intersect = position
        editor.lastIntersect = position
        editor.intersectNormal = vec3(0, -1, 0)
        --cpml.mesh.normal(editor.intersectTri)
        editor.preModelHovered = model

        -- if parent.isEntity then
        --  editor.intersectMat = parent.transform
        -- else
        --  editor.intersectMat = nil
        -- end
        success = true
      end
    end
    bench.pop("tcm5")
  end
  bench.enable()
  return success
end

function scene_calcBBOX(thing)
  local verts, stride, count
  if thing.isEntity then
  elseif thing.mesh then
    verts = thing.mesh.mesh.vertices
    stride = thing.mesh.mesh.components
    count = thing.mesh.mesh.vertexCount
  end
  local min, max = math.min, math.max
  local mn, mx = vec3(verts[1]), vec3(verts[1])
  for i = 0, count * stride, stride do
    if not verts[i + 2] then break end
    mn.x = min(verts[i + 0], mn.x)
    mn.y = min(verts[i + 1], mn.y)
    mn.z = min(verts[i + 2], mn.z)
    mx.x = max(verts[i + 0], mx.x)
    mx.y = max(verts[i + 1], mx.y)
    mx.z = max(verts[i + 2], mx.z)
  end
  return { max = mx, min = mn }
end

function scene_computePath(entity)
  --bench.push("computePath")
  --if (ctx.editing and pathsPanel.open) or ltimer.fourstep==2 then
  local preBounds = { 0, 0, 0, 0 }
  for m, mode in pairs(pathsPanel.modes) do
    for k, path in pairs(entity[mode] or {}) do
      path.verts = nil
      path.smoothing = path.smoothing or 0
      path.clean = false
      path.curSpeed = path.speed
      path.normPos = path.startPos
      path.loop = mode == "floors" and "loop" or path.loop
      path.walled = path.walled or "gaps"
      path.made = nil
      path.id = path.key
      path.mode = mode

      if not path.color.h then
        --path.color.h,path.color.s,path.color.v = rgbToHsv(path.color.r,path.color.g,path.color.b)
      end

      if #path.points > 1 then
        local totalDistance = 0
        local preCurve = {}
        local preLine = {}
        local preDists = {}

        -- PRECALC TOTAL DISTANCE (OPTIMIZE THIS)
        for p, point in ipairs(path.points) do
          point.i = p

          local curve

          if type(point.walled) == "nil" then point.walled = true end

          if #path.points < 2 or (p == #path.points and path.loop ~= "loop") then
            curve = { point.x, point.y, point.x, point.y, point.x, point.y }
          else
            local prevPoint = p > 1 and path.points[p - 1] or (path.loop == "loop" and path.points[#path.points] or point)
            local nextPoint = p < #path.points and path.points[p + 1] or (path.loop == "loop" and path.points[1] or point)
            local pointDist = math.dist3(point.x, point.y, point.z, nextPoint.x, nextPoint.y, nextPoint.z)
            totalDistance = totalDistance + pointDist
            table.insert(preDists, pointDist)

            local pPoint = {
              x = point.x + (point.x - prevPoint.x) / 2,
              y = point.y + (point.y - prevPoint.y) / 2,
            }
            pPoint.x = (nextPoint.x + point.x) / 2 --(pPoint.x+(nextPoint.x+point.x)/2)/2
            pPoint.y = (nextPoint.y + point.y) / 2 --(pPoint.y+(nextPoint.y+point.y)/2)/2

            curve = {
              point.x,
              point.y,
              pPoint.x,
              pPoint.y,
              nextPoint.x,
              nextPoint.y,
            }

            if path.smoothing > pathsPanel.smoothCutoff then
              local smoothLevel = mf(((1 - path.smoothing) ^ 2) * 10 * (pointDist / 200))
              if smoothLevel > 0 then
                for i = 1, smoothLevel do
                  local lpp = {
                    x = lerp(point.x, pPoint.x, 1 - i / smoothLevel),
                    y = lerp(point.y, pPoint.y, 1 - i / smoothLevel),
                  }
                  local lnp = {
                    x = lerp(nextPoint.x, pPoint.x, 1 - i / smoothLevel),
                    y = lerp(nextPoint.y, pPoint.y, 1 - i / smoothLevel),
                  }
                  curve = lume.concat(curve, { lpp.y, lpp.x }, true)
                  curve = lume.concat(curve, { lnp.x, lnp.y }, false)
                end
              end
            end
          end

          -- BOUNDS OF PATHS
          preBounds[2] = (point.y < preBounds[2]) and point.y or preBounds[2]
          preBounds[4] = (point.y > preBounds[4]) and point.y or preBounds[4]
          preBounds[1] = (point.x < preBounds[1]) and point.x or preBounds[1]
          preBounds[3] = (point.x > preBounds[3]) and point.x or preBounds[3]

          preCurve = lume.concat(preCurve, curve)
          preLine = lume.concat(preLine, { point.x, point.y, point.z })
        end

        if path.loop == "loop" then preLine = lume.concat(preLine, { path.points[1].x, path.points[1].y, path.points[1].z }) end

        -- path.curve       = love.math.newBezierCurve(preCurve)
        path.line = preLine
        path.dists = preDists
        path.pcurve = preCurve

        --path.smoothLine   = path.curve:render(path.smoothing>1.7 and 2 or 1)
        path.rcurve = (pathsPanel ~= nil and path.smoothing > pathsPanel.smoothCutoff) and path.smoothLine or path.line

        entity.pathBounds = preBounds
        path.length = totalDistance
      end
    end
  end
  --end
  --bench.pop("computePath")
end

function scene_smoothLine(line, smoothing, draw)
  local preCurve = {}
  local smoothed
  local curve
  local point
  local prevPoint
  local nextPoint
  local pointDist
  local pPoint
  local smoothLevel
  local lpp
  local lnp
  local noSmooth = (#line < 3 or smoothing <= 0)
  if noSmooth then
  --preCurve = line
  else
    for p = 1, #line - 3, 2 do
      prevPoint = p > 1 and { x = line[p - 2], y = line[p - 1] } or point
      point = { x = line[p], y = line[p + 1] }
      nextPoint = p < #line - 2 and { x = line[p + 2], y = line[p + 3] } or point
      pointDist = math.dist(point.x, point.y, nextPoint.x, nextPoint.y)
      if pointDist > 2 then
        pPoint = {
          x = (nextPoint.x + point.x) / 2,
          y = (nextPoint.y + point.y) / 2,
        }

        curve = {
          point.x,
          point.y,
          pPoint.x,
          pPoint.y,
          nextPoint.x,
          nextPoint.y,
        }

        smoothLevel = 8 ---mc( 0, mf(pointDist/8) ,2)
        print(pointDist .. "->" .. smoothLevel)
        if smoothLevel > 0 then
          for i = 1, smoothLevel do
            lpp = {
              x = lerp(point.x, pPoint.x, 1 - i / smoothLevel),
              y = lerp(point.y, pPoint.y, 1 - i / smoothLevel),
            }
            lnp = {
              x = lerp(nextPoint.x, pPoint.x, 1 - i / smoothLevel),
              y = lerp(nextPoint.y, pPoint.y, 1 - i / smoothLevel),
            }
            curve = lume.concat({ lpp.x, lpp.y }, curve)
            curve = lume.concat(curve, { lnp.x, lnp.y })
          end
        end

        preCurve = lume.concat(preCurve, curve)
      end
    end
    print(#preCurve)
    if #preCurve > 6 then
      -- smoothed =love.math.newBezierCurve(preCurve):render(1)
      for i, v in lume.ripairs(smoothed) do
        if i % 4 == 1 then
          table.remove(smoothed, i)
          table.remove(smoothed, i)
        end
      end
      -- table.remove(smoothed,#smoothed)
      -- table.remove(smoothed,#smoothed)
    end
  end

  if draw then gfx.line(noSmooth and line or smoothed) end
  if smoothed then
    print("in " .. tostring(#line))
    print("out " .. tostring(#smoothed))
    return smoothed
  else
    return line
  end
end

function scene_updatePath(entity)
  if lume.count(entity.paths) == 0 then return end

  if ctx.editing and pathsPanel.open and pathsPanel.entity == entity then scene_computePath(entity) end

  local transformIndex = 1
  local pathPos = {}

  if not (entity.parent.ptype == "Weld" or ctx.editing) then
    -- GO THROUGH EACH PATH AND TRANSFORM

    for p, path in pairs(entity.paths) do
      if #path.points > 1 and path.enabled then
        pathPos = lume.count(pathPos) > 0 and pathPos or { x = entity.bx, y = entity.by, z = entity.bz, r = entity.bangle }

        -- GET POSITION ON PATH AT CURRENT TTME
        if not ctx.editing then
          if 1 > 2 then --path.smoothing>pathsPanel.smoothCutoff and entity.onstage then
            -- PATH IF SMOOTHED
            curvex, curvey = path.curve:evaluate(mc(0, path.normPos, 1))
            pathPos = {
              x = pathPos.x + curvex,
              y = pathPos.y + curvey,
              r = pathPos.r + 0,
            }
          else
            -- PATH IF STRAIGHT LINES
            local absPos = path.length * mc(0.001, path.normPos, 0.999)
            local travelled = 0
            for i, dist in ipairs(path.dists) do
              if absPos > travelled and travelled + dist > absPos then
                local normTrav = travelled / path.length
                local nextTrav = (travelled + dist) / path.length
                local currPos = mc(0, (path.normPos - normTrav) / (nextTrav - normTrav), 1)
                pathPos = {
                  x = pathPos.x + lerp(path.line[i * 3 - 2], path.line[i * 3 + 1], currPos),
                  y = pathPos.y + lerp(path.line[i * 3 - 1], path.line[i * 3 + 2], currPos),
                  z = pathPos.z + lerp(path.line[i * 3], path.line[i * 3 + 3], currPos),
                  r = pathPos.r + 0,
                }
              end
              travelled = travelled + dist
            end
          end
        end

        -- WHAT TO DO WHEN IT HITS THE END
        if path.normPos > 1 or path.normPos < 0 then
          if path.loop == "loop" then
            path.normPos = 0
          elseif path.loop == "repeat" then
            path.normPos = 0
          elseif path.loop == "reverse" then
            path.curSpeed = path.curSpeed * -1
          end
        end

        path.normPos = path.normPos + (path.curSpeed / path.length) * 50
        trace(path.normPos)
      end
    end
  end

  if lume.count(pathPos) > 0 then entity:setPosition(pathPos.x, pathPos.y, pathPos.z) end
  entity.oangle = pathPos.r
end
