--import config.lua
--import utils.lua
--import uiLib.lua
--import polyUtil.lua
--import programs/polyEditorUI.lua

--import userConfig.lua

PolyEditor = {}
PolyEditor.enabled = false
PolyEditor.currentPageId = 1
PolyEditor.pagesData = {}
PolyEditor.openFileType = "3dm"
PolyEditor.screenWidth = 340.0
PolyEditor.screenHeight = 320.0
PolyEditor.screenSize = vec2(PolyEditor.screenWidth, PolyEditor.screenHeight)
PolyEditor.leftSideGUIOffset = PolyEditor.screenWidth - (16.0 + 8.0 + 4.0)
PolyEditor.camView1Mat = mat4()
PolyEditor.camView2Mat = mat4()
PolyEditor.camView3Mat = mat4()
PolyEditor.camView4Mat = mat4()
PolyEditor.camMats = {}
PolyEditor.viewsSpriteData = {}
PolyEditor.mousePressedOnUI = false
PolyEditor.currentLayerId = 1                  -- the active/primary layer (always a member of selectedLayers)
PolyEditor.selectedLayers = {1}                -- ordered array of selected layerIds (active included, never empty)
PolyEditor.selectedLayersById = {[1] = 1}      -- set { [layerId] = 1 } for O(1) membership tests
PolyEditor.assetName = ""
PolyEditor.primitiveResolution = 1
PolyEditor.scissorEnabled = false
PolyEditor.textureScissorEnabled = false
PolyEditor.snapEnabled = false
PolyEditor.shadingType = 2
PolyEditor.modelModeShadingType = 2
PolyEditor.textureModeShadingType = 1
PolyEditor.rigModeShadingType = 2
PolyEditor.clipboard = nil
PolyEditor.textureLocked = {}
PolyEditor.snapStep = 0.05
PolyEditor.tileSize = vec3(1, 1, 1)
PolyEditor.tileBoundsEnabled = false
PolyEditor.showAllLayers = false
PolyEditor.drawPerspVertsEnabled = true
PolyEditor.bilinearInterp = false
PolyEditor.ditherEnabled = false

CAMERA_PERSPECTIVE = 1
CAMERA_ORTHO = 2
PolyEditor.cameraType = CAMERA_PERSPECTIVE

PolyEditor.camManipView1 = {pos = vec3(0.0, 10.0, 0.0), rot = eulerRot(-90.0, 0.0, 0.0), focusDist = 0.25, ortho = true}
PolyEditor.camManipView2 = {pos = vec3(0.0, 0.0, 10.0), rot = eulerRot(0.0, 0.0, 0.0), focusDist = 10.0, ortho = false}
PolyEditor.camManipView3 = {pos = vec3(0.0, 0.0, 10.0), rot = eulerRot(0.0, 0.0, 0.0), focusDist = 0.25, ortho = true}
PolyEditor.camManipView4 = {pos = vec3(10.0, 0.0, 0.0), rot = eulerRot(0.0, 90.0, 0.0), focusDist = 0.25, ortho = true}

PolyEditor.camManipView1Model = {pos = vec3(0.0, 10.0, 0.0), rot = eulerRot(-90.0, 0.0, 0.0), focusDist = 0.25, ortho = true}
PolyEditor.camManipView3Model = {pos = vec3(0.0, 0.0, 10.0), rot = eulerRot(0.0, 0.0, 0.0), focusDist = 0.25, ortho = true}
PolyEditor.camManipView4Model = {pos = vec3(10.0, 0.0, 0.0), rot = eulerRot(0.0, 90.0, 0.0), focusDist = 0.25, ortho = true}

PolyEditor.camManipView1Texture = {pos = vec3(0.0, 10.0, 0.0), rot = eulerRot(-90.0, 0.0, 0.0), focusDist = 0.25, ortho = true}
PolyEditor.camManipView3Texture = {pos = vec3(0.0, 0.0, 10.0), rot = eulerRot(0.0, 0.0, 0.0), focusDist = 0.25, ortho = true}
PolyEditor.camManipView4Texture = {pos = vec3(10.0, 0.0, 0.0), rot = eulerRot(0.0, 90.0, 0.0), focusDist = 0.25, ortho = true}

PolyEditor.camManips = {}
PolyEditor.focusSpeed = 1.0
PolyEditor.mouseClickStartedOutside = false
PolyEditor.mousePressedPos = vec2(0.0, 0.0)
PolyEditor.mouseOffset = vec2(0.0, 0.0)
PolyEditor.mousePressedInViewport = -1
PolyEditor.camFov = degToRad(35.0)
PolyEditor.clickedViewport = -1
PolyEditor.mouseInViewport = -1
PolyEditor.mousePos = vec2(0.0, 0.0)
PolyEditor.mousePressed = -1
PolyEditor.mouseReleased = -1
PolyEditor.mouseDown = -1
PolyEditor.pickedVerts = {}
PolyEditor.hoveredVerts = {}
PolyEditor.pickedVertsByHash = {}
PolyEditor.pickedQuad = -1
PolyEditor.pickedLayerId = 1   -- which selected layer the current pickedVerts/pickedQuad belong to
PolyEditor.selectedVerts = {}
PolyEditor.selectedVertsById = {}
PolyEditor.selectedVertsByHash = {}
PolyEditor.selectedVertsOverlapping = {}
PolyEditor.selectedVertsByLayer = {}   -- [layerId] = {verts, byId, byHash, overlapping}; active layer aliased by the globals above
PolyEditor.selectedUVsByLayer = {}     -- [layerId] = {array of uv indices}; active layer aliased by selectedUVs
PolyEditor.pickingSelection = false
PolyEditor.pickedUVId = -1
PolyEditor.pickedUVQuad = -1
PolyEditor.vertexSize = 4.0
PolyEditor.manipulatingCamera = false
PolyEditor.debugMousePos = false
PolyEditor.currentMesh = nil
PolyEditor.currentMeshQuads = {}
PolyEditor.currentMeshNormals = {}
PolyEditor.currentMeshUVs = {}
PolyEditor.currentMeshVertexColors = {}
PolyEditor.meshes = {}
PolyEditor.models = {}
PolyEditor.hierarchy = {}
PolyEditor.newPrimitiveMesh = nil
PolyEditor.newPrimitiveQuads = {}
PolyEditor.newPrimitiveMeshNormals = {}
PolyEditor.activeTool = ""
PolyEditor.orthoCamZoomMin = 0.05
PolyEditor.orthoCamZoomMax = 2.0
PolyEditor.perspCamZoomMin = 1.0
PolyEditor.perspCamZoomMax = 100.0
PolyEditor.skipClick = false
PolyEditor.skipClickRelease = false
PolyEditor.pickingQuadsEnabled = true
PolyEditor.pickingVertsEnabled = true
PolyEditor.undoStack = {}
PolyEditor.undoStackId = 0
PolyEditor.dirty = false
PolyEditor.lastClickTime = -1.0
PolyEditor.draggingSelection = false
PolyEditor.shiftKeyDown = false
PolyEditor.ctrlKeyDown = false
PolyEditor.time = 0.0
PolyEditor.uvAnimFrame = 0
PolyEditor.uvAnimPlaying = false
PolyEditor.uvAnimSpeed = 4.0
PolyEditor.uvAnimLastFrameTime = 0.0
PolyEditor.arrowRepeatKey = -1        -- currently held arrow scancode, or -1
PolyEditor.arrowRepeatDelta = nil     -- vec3 direction for the held arrow
PolyEditor.arrowRepeatNextTime = 0.0  -- next getTime() at which to repeat
PolyEditor.arrowMovePending = false   -- a move occurred that still needs storeUndo() on release
PolyEditor.arrowMoveLayerIds = nil    -- layers touched by the in-progress arrow nudge (for storeUndo)
PolyEditor.arrowRepeatInitialDelay = 0.3   -- delay before auto-repeat kicks in
PolyEditor.arrowRepeatInterval = 0.05      -- interval between repeated moves
PolyEditor.assetsOpen = {}
PolyEditor.draggingPivotCenter = false

-- texture data
PolyEditor.textureData = {}
PolyEditor.uvRegionPos = vec2(0.0, 0.0)
PolyEditor.uvRegionSize = vec2(0.5, 0.5)
PolyEditor.uvRegionP1Sprite = nil
PolyEditor.uvRegionP1SpriteData = {}
PolyEditor.uvRegionP2Sprite = nil
PolyEditor.uvRegionP2SpriteData = {}
PolyEditor.uvRegionP3Sprite = nil
PolyEditor.uvRegionP3SpriteData = {}
PolyEditor.uvRegionP4Sprite = nil
PolyEditor.uvRegionP4SpriteData = {}
PolyEditor.editingUVRegion = -1
PolyEditor.editingUVQuads = {}
PolyEditor.selectedUVs = {}
PolyEditor.uvVertSprite = nil
PolyEditor.uvVertSpriteData = {}
PolyEditor.highlightUVs = {}
PolyEditor.uvDrawDepth = 0.3
PolyEditor.activePaintTool = ""
PolyEditor.currentVertexColor = vec4(255, 255, 255, 255)
PolyEditor.texturePencilColor = vec4(255, 255, 255, 255)
PolyEditor.paintPixelSprite = nil
PolyEditor.paintBrushRadius = 1
PolyEditor.paintBrushRadiusMin = 1
PolyEditor.paintBrushRadiusMax = 4
PolyEditor.oldPixelPos = vec2(-1.0, -1.0)
PolyEditor.textureDrawMagnified = true

MODEL_MODE = 1
TEXTURE_MODE = 2
RIG_MODE = 3

PolyEditor.mode = MODEL_MODE

-- tools data
PolyEditor.draggingVerts = false
PolyEditor.draggingVertsJustStarted = false
PolyEditor.dragVerts = {}
PolyEditor.dragVertsPos = {}
PolyEditor.dragCenter = vec3(0.0, 0.0, 0.0)
PolyEditor.dragOffsetMat = mat4()

function start()
	UILib.init()

	initNewModel()
	
	_registerShortcuts()

	PolyUtil.init()

	local viewportWidth = ((PolyEditor.leftSideGUIOffset - 4.0) / 2)
	local viewportHeight = ((PolyEditor.screenHeight - 10.0) / 2)
	
	PolyEditor.camMats[1] = PolyEditor.camView1Mat
	PolyEditor.camMats[2] = PolyEditor.camView2Mat
	PolyEditor.camMats[3] = PolyEditor.camView3Mat
	PolyEditor.camMats[4] = PolyEditor.camView4Mat

	PolyEditor.camManips[1] = PolyEditor.camManipView1
	PolyEditor.camManips[2] = PolyEditor.camManipView2
	PolyEditor.camManips[3] = PolyEditor.camManipView3
	PolyEditor.camManips[4] = PolyEditor.camManipView4

	-- init view 1
	PolyEditor.view1SpriteData = {}
	PolyEditor.view1SpriteData.position = vec3(1.0, 8.0, 0.0)
	PolyEditor.view1SpriteData.size = vec2(viewportWidth, viewportHeight)
	PolyEditor.view1SpriteData.color = Config.uiColors.polyEditorViewport
	PolyEditor.view1SpriteData.border = 0.0

	PolyEditor.viewsSpriteData[1] = PolyEditor.view1SpriteData
	
	PolyEditor.view1Sprite = allocSprite()
	setSprite(PolyEditor.view1Sprite, PolyEditor.view1SpriteData)

	-- init view 2
	PolyEditor.view2SpriteData = {}
	PolyEditor.view2SpriteData.position = vec3(viewportWidth + 2.0, 8.0, 0.0)
	PolyEditor.view2SpriteData.size = vec2(viewportWidth, viewportHeight)
	PolyEditor.view2SpriteData.color = Config.uiColors.polyEditorViewport
	PolyEditor.view2SpriteData.border = 0.0

	PolyEditor.viewsSpriteData[2] = PolyEditor.view2SpriteData
	
	PolyEditor.view2Sprite = allocSprite()
	setSprite(PolyEditor.view2Sprite, PolyEditor.view2SpriteData)
	
	-- init view 3
	PolyEditor.view3SpriteData = {}
	PolyEditor.view3SpriteData.position = vec3(1.0, viewportHeight + 8.0 + 1.0, 0.0)
	PolyEditor.view3SpriteData.size = vec2(viewportWidth, viewportHeight)
	PolyEditor.view3SpriteData.color = Config.uiColors.polyEditorViewport
	PolyEditor.view3SpriteData.border = 0.0

	PolyEditor.viewsSpriteData[3] = PolyEditor.view3SpriteData
	
	PolyEditor.view3Sprite = allocSprite()
	setSprite(PolyEditor.view3Sprite, PolyEditor.view3SpriteData)

	-- init view 4
	PolyEditor.view4SpriteData = {}
	PolyEditor.view4SpriteData.position = vec3(viewportWidth + 2.0, viewportHeight + 8.0 + 1.0, 0.0)
	PolyEditor.view4SpriteData.size = vec2(viewportWidth, viewportHeight)
	PolyEditor.view4SpriteData.color = Config.uiColors.polyEditorViewport
	PolyEditor.view4SpriteData.border = 0.0

	PolyEditor.viewsSpriteData[4] = PolyEditor.view4SpriteData
	
	PolyEditor.view4Sprite = allocSprite()
	setSprite(PolyEditor.view4Sprite, PolyEditor.view4SpriteData)

	local viewLabelNames = {"top", "persp", "front", "side"}
	local viewLabelViews = {PolyEditor.view1SpriteData, PolyEditor.view2SpriteData, PolyEditor.view3SpriteData, PolyEditor.view4SpriteData}
	PolyEditor.viewLabelSprites = {}
	PolyEditor.viewLabelSpriteData = {}
	for i = 1, 4 do
		local text = viewLabelNames[i]
		local textWidth = 8 * #text
		local vsd = viewLabelViews[i]
		local labelData = {}
		labelData.position = vec3(vsd.position.x + vsd.size.x - textWidth, vsd.position.y, 0.1)
		labelData.size = vec2(textWidth, 8)
		labelData.color = vec4(0, 0, 0, 0)
		labelData.textColor = Config.paletteNames.grey
		labelData.text = text
		PolyEditor.viewLabelSpriteData[i] = labelData
		PolyEditor.viewLabelSprites[i] = allocSprite()
		setSprite(PolyEditor.viewLabelSprites[i], labelData)
	end

	initTextureMode()
	
	PolyEditorUI.build()

	PolyEditor.lastTimeFPS = PolyEditor.time
	PolyEditor.timeFPS = 0.0
	PolyEditor.fps = 0

	setAssetName("")

	for i = 1, MEM_PAGES do
		PolyEditor.textureLocked[i] = false
	end

	storeUndo()
	PolyEditor.dirty = false

	_restoreAppVars()
end

function _restoreAppVars()
	PolyEditor.snapEnabled = Utils.getAppVar("polyEditor_snapEnabled")	 or false
	PolyEditor.tileBoundsEnabled = Utils.getAppVar("tileBoundsEnabled") or false
	
	_setCameraType(Utils.getAppVar("cameraType") or CAMERA_PERSPECTIVE)

	PolyEditor.setShadingType(Utils.getAppVar("polyEditor_shadingType") or 2)
end

function programActive(state)
	PolyEditor.enabled = state

	if state == true then
		_updateAssetPreview()
	end

	if state == true and PolyEditor.assetName ~= "" then
		invokeProgram(MAIN_PROGRAMID, "selectAsset", PolyEditor.assetName)
	end

	PolyEditor.bilinearInterp = invokeProgram(MAIN_PROGRAMID, "getProjectVar", "video_bilinearInterpEnabled")
	PolyEditor.ditherEnabled = invokeProgram(MAIN_PROGRAMID, "getProjectVar", "video_ditherEnabled")
end

function reloadTextures()
	for i = 1, MEM_PAGES do
		if PolyEditor.textureLocked[i] == false then
			reloadTexture(i)

			local pageData = PolyEditor.pagesData[i]
			if pageData ~= nil then
				for meshIt = 1, MESH_LAYERS do
					local mesh = pageData.meshes[meshIt]
					if mesh ~= nil then
						setMeshTexture(mesh.mesh, i)
					end
				end
			end
		end
	end

	-- rebind the viewport texture panels so they show the current page's texture
	-- (their sprite.textureAddress is a snapshot and must be re-applied after a reload)
	if PolyEditor.view1TextureSprite ~= nil then
		PolyEditor.view1TextureSpriteData.textureId = PolyEditor.currentPageId
		setSprite(PolyEditor.view1TextureSprite, PolyEditor.view1TextureSpriteData)

		PolyEditor.view3TextureSpriteData.textureId = PolyEditor.currentPageId
		setSprite(PolyEditor.view3TextureSprite, PolyEditor.view3TextureSpriteData)

		PolyEditor.view4TextureSpriteData.textureId = PolyEditor.currentPageId
		setSprite(PolyEditor.view4TextureSprite, PolyEditor.view4TextureSpriteData)
	end

	PolyEditor.textureData = getTexture(PolyEditor.currentPageId)
end

function _getMode()
	return PolyEditor.mode
end

function setPage(pageId)
	_abortCurrentTool()

	local pageData = PolyEditor.pagesData[PolyEditor.currentPageId]
	if pageData == nil then
		pageData = {}
		pageData.overlayLayers = {}
		PolyEditor.pagesData[PolyEditor.currentPageId] = pageData
	end

	-- capture the live active-layer globals into its per-layer cache/store before stashing
	_syncActiveCacheFromGlobals()

	pageData.selectedVerts = {table.unpack(PolyEditor.selectedVerts)}
	pageData.selectedUVs = {table.unpack(PolyEditor.selectedUVs)}
	pageData.selectedVertsByLayer = PolyEditor.selectedVertsByLayer
	pageData.selectedUVsByLayer = PolyEditor.selectedUVsByLayer
	pageData.currentLayerId = PolyEditor.currentLayerId
	pageData.selectedLayers = {table.unpack(PolyEditor.selectedLayers)}
	pageData.undoStack = PolyEditor.undoStack
	pageData.undoStackId = PolyEditor.undoStackId
	pageData.dirty = PolyEditor.dirty
	pageData.mode = PolyEditor.mode
	pageData.meshes = PolyEditor.meshes
	pageData.hierarchy = PolyEditor.hierarchy
	pageData.hierarchyByLayerId = PolyEditor.hierarchyByLayerId
	pageData.models = PolyEditor.models
	pageData.rootModel = PolyEditor.rootModel
	
	PolyEditor.currentPageId = pageId
	
	local pageData = PolyEditor.pagesData[PolyEditor.currentPageId]
	if pageData == nil then
		pageData = {}
		pageData.selectedVerts = {}
		pageData.selectedUVs = {}
		pageData.selectedVertsByLayer = {}
		pageData.selectedUVsByLayer = {}
		pageData.currentLayerId = 1
		pageData.selectedLayers = {1}
		pageData.undoStack = {}
		pageData.undoStackId = 0
		pageData.dirty = false
		pageData.mode = MODEL_MODE
		pageData.overlayLayers = {}

		PolyEditor.pagesData[PolyEditor.currentPageId] = pageData

		initNewModel()

		PolyEditor.models = {}
		PolyEditor.rootModel = nil

		pageData.meshes = PolyEditor.meshes
		pageData.hierarchy = PolyEditor.hierarchy
		pageData.hierarchyByLayerId = PolyEditor.hierarchyByLayerId
		pageData.models = PolyEditor.models
		pageData.rootModel = PolyEditor.rootModel
	end
	
	PolyEditor.meshes = pageData.meshes
	PolyEditor.hierarchy = pageData.hierarchy
	PolyEditor.hierarchyByLayerId = pageData.hierarchyByLayerId
	PolyEditor.models = pageData.models
	PolyEditor.rootModel = pageData.rootModel

	PolyEditor.currentLayerId = pageData.currentLayerId
	setSelectedLayers(pageData.selectedLayers or {pageData.currentLayerId})
	PolyEditorUI.layerSelected(PolyEditor.currentLayerId)
	_syncHierarchySelectionFlags()

	-- restore this page's per-layer selection stores, then bind the active layer's cache
	PolyEditor.selectedVertsByLayer = pageData.selectedVertsByLayer or {}
	PolyEditor.selectedUVsByLayer = pageData.selectedUVsByLayer or {}

	_bindActiveLayerGlobals(pageData.currentLayerId)

	PolyEditor.uvRegionPos = getMeshUVRegionPos(PolyEditor.currentMesh)
	PolyEditor.uvRegionSize = getMeshUVRegionSize(PolyEditor.currentMesh)

	PolyEditor.uvAnimFrame = 0

	-- back-compat: pages saved before per-layer selection existed
	if pageData.selectedVertsByLayer == nil then
		setSelectedVerts(pageData.selectedVerts or {})
		setSelectedUVs(pageData.selectedUVs or {})
	end

	PolyEditor.view1TextureSpriteData.textureId = PolyEditor.currentPageId
	setSprite(PolyEditor.view1TextureSprite, PolyEditor.view1TextureSpriteData)

	PolyEditor.view3TextureSpriteData.textureId = PolyEditor.currentPageId
	setSprite(PolyEditor.view3TextureSprite, PolyEditor.view3TextureSpriteData)

	PolyEditor.view4TextureSpriteData.textureId = PolyEditor.currentPageId
	setSprite(PolyEditor.view4TextureSprite, PolyEditor.view4TextureSpriteData)

	PolyEditor.textureData = getTexture(PolyEditor.currentPageId)

	PolyEditor.editingUVQuads = {}
	PolyEditor.highlightUVs = {}

	PolyEditor.undoStack = pageData.undoStack
	PolyEditor.undoStackId = pageData.undoStackId
	PolyEditor.dirty = pageData.dirty

	if pageData.mode == MODEL_MODE then
		PolyEditor.switchToModel()
	else
		PolyEditor.switchToTexture()
	end

	PolyEditorUI.updatePalette()
	
	local assetName = PolyEditor.assetsOpen[PolyEditor.currentPageId]
	if assetName == nil then
		assetName = ""
	end
	setAssetName(assetName)

	_updateAssetPreview()
end

function _stripAssetName(filename)
    local name = filename:match("([^/]+)%.[^/]+$")

    return name
end

function _clearHierarchyTable()
	PolyEditor.hierarchy = {}
	PolyEditor.hierarchyByLayerId = {}
	for i = 1, MESH_LAYERS do
		PolyEditor.hierarchy[i] = {layerId = i, name = "layer" .. i, children = {}, position = vec3(0.0, 0.0, 0.0)}
		PolyEditor.hierarchyByLayerId[i] = PolyEditor.hierarchy[i]
	end
end

function _clearAll()
	for i = 1, MESH_LAYERS do
		local meshData = PolyEditor.meshes[i]

		if meshData ~= nil then
			destroy(meshData.mesh)
		end

		PolyEditor.meshes[i] = nil

		if isValidAddress(PolyEditor.models[i]) == true then
			destroy(PolyEditor.models[i])
		end
	end

	PolyEditor.models = {}

	if PolyEditor.rootModel ~= nil then
		destroy(PolyEditor.rootModel)

		PolyEditor.rootModel = nil
	end

	_clearHierarchyTable()

	_refreshModelHierarchy(PolyEditor.hierarchy)

	PolyEditor.currentMeshQuads = {}
	PolyEditor.currentMeshNormals = {}
	PolyEditor.currentMeshUVs = {}
	PolyEditor.currentMeshVertexColors = {}
	PolyEditor.uvRegionPos = vec2(0.0, 0.0)
	PolyEditor.uvRegionSize = vec2(0.5, 0.5)

	PolyEditor.currentLayerId = 1
	setSelectedLayers({1})
	PolyEditorUI.layerSelected(PolyEditor.currentLayerId)

	PolyEditor.meshes = {}

	PolyEditor.selectedVerts = {}
	PolyEditor.selectedVertsByHash = {}
	PolyEditor.selectedVertsById = {}
	PolyEditor.selectedVertsOverlapping = {}
	PolyEditor.selectedVertsByLayer = {}
	PolyEditor.selectedUVsByLayer = {}
	PolyEditor.selectedUVs = {}
	PolyEditor.highlightUVs = {}
	PolyEditor.editingUVQuads = {}

	PolyEditor.currentVertexColor = vec4(255, 255, 255, 255)

	local pageData = PolyEditor.pagesData[PolyEditor.currentPageId]
	pageData.overlayLayers = {}

	for i = 1, MESH_LAYERS do
		PolyEditorUI.setLayerOverlay(i, false)
	end

	if PolyEditor.newPrimitiveMesh ~= nil then
		destroy(PolyEditor.newPrimitiveMesh)
		PolyEditor.newPrimitiveMesh = nil
	end

	if PolyEditor.tmpToolMesh ~= nil then
		destroy(PolyEditor.tmpToolMesh)
		PolyEditor.tmpToolMesh = nil
	end
	
	PolyEditor.pickingVertsEnabled = true
	PolyEditor.pickingQuadsEnabled = true

	PolyEditor.tmpMirrorVertsId = {}
	PolyEditor.tmpMirrorVerts = {}

	PolyEditor.tmpSplitSelectedQuadPoints = {}
	PolyEditor.vertIdToRemove = {}

	PolyEditor.tileSize.x = 1.0
	PolyEditor.tileSize.y = 1.0
	PolyEditor.tileSize.z = 1.0

	PolyEditor.undoStack = {}
	PolyEditor.undoStackId = 0

	PolyEditor.uvAnimFrame = 0
	PolyEditor.uvAnimPlaying = false
	
	storeUndo()
	PolyEditor.dirty = false
end

function findNewAssetName()
	local newAssetName = ""

	local files = listFiles("page" .. PolyEditor.currentPageId)

	for i = 1, 32 do
		local fileExists = false

		for fileIt = 1, #files do
			local fileName = files[fileIt]
			
			if fileName == "m" .. i .. ".gltf" then
				fileExists = true

				break
			end
		end

		if fileExists == false then
			newAssetName = "m" .. i

			break
		end
	end

	return newAssetName
end

function PolyEditor.newModel()
	clearProgram()
end

function clearProgram()
	_clearAll()

	initNewModel()

	PolyEditor.dirty = false
	local newAssetName = findNewAssetName()
	setAssetName(newAssetName)

	_updateAssetPreview()

	switchToModel()
end

function setAssetName(assetName)
	assetName = replaceStr(assetName, ".gltf", "")

	PolyEditor.assetName = assetName

	updateViewTitle()
end

function updateViewTitle()
	local programId = getProgramId()
	local topBarTitle = "poly editor"
	if PolyEditor.assetName ~= "" then
		topBarTitle = "poly editor: " .. PolyEditor.assetName
	end
	if PolyEditor.dirty then
		topBarTitle = topBarTitle .. " *"
	end

	invokeProgram(MAIN_PROGRAMID, "setViewTitle", programId, topBarTitle)
end

function _modelToHierarchyTableRecursive(model, tableEntry)
	if model == nil or tableEntry == nil then
		return
	end

	tableEntry.children = {}

	tableEntry.layerId = getModelData(model, "layerId", -1)
	if tableEntry.layerId == -1 then
		local modelName = getModelName(model)
		tableEntry.layerId = tonumber(string.match(modelName, "%d+$"))
	end

	tableEntry.name = getModelName(model)
	if tableEntry.name == "" then
		tableEntry.name = "layer" .. tableEntry.layerId
	end

	PolyEditor.hierarchyByLayerId[tableEntry.layerId] = tableEntry

	tableEntry.position = vec3(getModelWorldMatrix(model).position)

	local mesh = getModelMesh(model)
	if mesh ~= nil then
		local meshData = _newLayerMeshData(mesh)
		PolyEditor.meshes[tableEntry.layerId] = meshData
	end

	local childrenModels = getModelChildren(model)
	for i = 1, #childrenModels do
		local childModel = childrenModels[i]
		local childTableEntry = {}
		tableEntry.children[i] = childTableEntry
		_modelToHierarchyTableRecursive(childModel, childTableEntry)
	end
end

function openAsset(assetName)
	_clearAll()

	PolyEditor.dirty = false

	local rootModel = loadModel(assetName, PolyEditor.currentPageId)[1]

	local tsx = getModelData(rootModel, "tileSizeX", 1)
	local tsy = getModelData(rootModel, "tileSizeY", 1)
	local tsz = getModelData(rootModel, "tileSizeZ", 1)
	PolyEditor.tileSize = vec3(tsx, tsy, tsz)

	local childModels = getModelChildren(rootModel)

	PolyEditor.hierarchy = {}

	for i = 1, #childModels do
		PolyEditor.hierarchy[i] = {}
		_modelToHierarchyTableRecursive(childModels[i], PolyEditor.hierarchy[i])
	end

	PolyEditor.assetsOpen[PolyEditor.currentPageId] = assetName

	setAssetName(assetName)
	
	if PolyEditor.meshes[PolyEditor.currentLayerId] ~= nil then
		PolyEditor.currentMesh = PolyEditor.meshes[PolyEditor.currentLayerId].mesh
	else
		PolyEditor.currentMesh = nil
		for i = 1, MESH_LAYERS do
			if PolyEditor.meshes[i] ~= nil then
				PolyEditor.currentLayerId = i
				PolyEditor.currentMesh = PolyEditor.meshes[i].mesh
				break
			end
		end
	end

	-- fresh asset: start with empty per-layer selections, bind the active layer's cache
	PolyEditor.selectedVertsByLayer = {}
	PolyEditor.selectedUVsByLayer = {}

	if PolyEditor.meshes[PolyEditor.currentLayerId] ~= nil then
		_bindActiveLayerGlobals(PolyEditor.currentLayerId)
	else
		PolyEditor.currentMeshQuads = {}
		PolyEditor.currentMeshNormals = {}
		PolyEditor.currentMeshUVs = {}
		PolyEditor.currentMeshVertexColors = {}
		PolyEditor.selectedVerts = {}
		PolyEditor.selectedVertsById = {}
		PolyEditor.selectedVertsByHash = {}
		PolyEditor.selectedVertsOverlapping = {}
		PolyEditor.selectedUVs = {}
	end

	PolyEditor.uvRegionPos = getMeshUVRegionPos(PolyEditor.currentMesh)
	PolyEditor.uvRegionSize = getMeshUVRegionSize(PolyEditor.currentMesh)

	PolyEditor.uvAnimFrame = 0

	setSelectedLayers({PolyEditor.currentLayerId})
	PolyEditorUI.layerSelected(PolyEditor.currentLayerId)

	_refreshModelHierarchy(PolyEditor.hierarchy)

	_updateAssetPreview()

	PolyEditor.switchToModel()

	PolyEditor.frameAllViewsAllLayers()
end

function _newLayerMeshData(mesh)
	local meshData = {}

	if mesh == nil then
		meshData.mesh = allocMesh()
		meshData.uvRegionPos = vec2(0.0, 0.0)
		meshData.uvRegionSize = vec2(0.5, 0.5)
	else
		meshData.mesh = mesh
		meshData.uvRegionPos = getMeshUVRegionPos(mesh)
		meshData.uvRegionSize = getMeshUVRegionSize(mesh)
	end
	
	setMeshTexture(meshData.mesh, PolyEditor.currentPageId)

	return meshData
end

function initNewModel()
	_clearHierarchyTable()

	PolyEditor.meshes = {}
	PolyEditor.selectedVertsByLayer = {}
	PolyEditor.selectedUVsByLayer = {}

	PolyEditor.currentMeshQuads = {}
	PolyEditor.currentMeshNormals = {}
	PolyEditor.currentMeshUVs = {}
	PolyEditor.currentMeshVertexColors = {}

	PolyEditor.currentLayerId = 1
	setSelectedLayers({1})
	PolyEditorUI.layerSelected(PolyEditor.currentLayerId)

	PolyEditor.meshes[1] = _newLayerMeshData()

	PolyEditor.currentMesh = PolyEditor.meshes[1].mesh

	setMeshMicroShader(PolyEditor.currentMesh, "modelMat", mat4(), MICROSHADER_UNIFORM)

	setMeshTexture(PolyEditor.currentMesh, PolyEditor.currentPageId)

	-- bind layer 1's (empty) cache + selection store into the working globals
	_bindActiveLayerGlobals(1)

	PolyEditor.uvRegionPos = PolyEditor.meshes[1].uvRegionPos
	PolyEditor.uvRegionSize = PolyEditor.meshes[1].uvRegionSize

	refreshCurrentMesh()
end

function renamedAsset(oldFileName, newFileName)
	local newName = _stripAssetName(newFileName)
	
	setAssetName(newName)
end

function _updateAssetPreview()
	if #PolyEditor.currentMeshQuads == 0 then
		return
	end

	if PolyEditor.enabled == false then
		return
	end

	_refreshModelHierarchy(PolyEditor.hierarchy)
	saveModel("tmp", 0, {PolyEditor.rootModel})
	
	invokeProgram(MAIN_PROGRAMID, "refreshModelVisualizer", "tmp")
end

function _hierarchyTableToModelRecursive(parentModel, parentHierarchyEntry)
	if parentHierarchyEntry == nil then
		return
	end

	local model = allocModel()

	setModelName(model, parentHierarchyEntry.name)
	setModelData(model, "layerId", parentHierarchyEntry.layerId)

	PolyEditor.models[parentHierarchyEntry.layerId] = model

	setModelParent(model, parentModel)

	local modelMat = mat4()
	modelMat.position = parentHierarchyEntry.position or vec3(0.0, 0.0, 0.0)
	setModelWorldMatrix(model, modelMat)

	local meshData = PolyEditor.meshes[parentHierarchyEntry.layerId]
	if meshData ~= nil then
		setModelMesh(model, meshData.mesh)
	end

	for i = 1, #parentHierarchyEntry.children do
		_hierarchyTableToModelRecursive(model, parentHierarchyEntry.children[i])
	end
end

function _refreshModelHierarchy(hierarchyTable)
	for i = 1, MESH_LAYERS do
		local layerModel = PolyEditor.models[i]
		if layerModel ~= nil then
			setModelMesh(layerModel, nil)
		end
	end

	if PolyEditor.rootModel ~= nil then
		destroy(PolyEditor.rootModel)
	end

	PolyEditor.rootModel = allocModel()
	setModelName(PolyEditor.rootModel, "root")
	setModelData(PolyEditor.rootModel, "tileSizeX", PolyEditor.tileSize.x)
	setModelData(PolyEditor.rootModel, "tileSizeY", PolyEditor.tileSize.y)
	setModelData(PolyEditor.rootModel, "tileSizeZ", PolyEditor.tileSize.z)

	PolyEditor.models = {}

	for i = 1, MESH_LAYERS do
		_hierarchyTableToModelRecursive(PolyEditor.rootModel, PolyEditor.hierarchy[i])
	end
end

function save()
	if PolyEditor.mode == TEXTURE_MODE then
		clearPreviewPaint()
	end

	if PolyEditor.assetName == "" then
		local newAssetName = findNewAssetName()

		setAssetName(newAssetName)
	end

	local savedAnimFrame = PolyEditor.uvAnimFrame
	if savedAnimFrame > 0 and hasMeshUVAnimFrame(PolyEditor.currentMesh, 0) then
		meshUVAnimFrame(PolyEditor.currentMesh, 0)
		PolyEditor.currentMeshUVs = getMeshVertices(PolyEditor.currentMesh, VERT_UV)
		updateUV()
	end

	_refreshModelHierarchy(PolyEditor.hierarchy)

	saveModel(PolyEditor.assetName, PolyEditor.currentPageId, {PolyEditor.rootModel})

	if savedAnimFrame > 0 and hasMeshUVAnimFrame(PolyEditor.currentMesh, 0) then
		_loadUVAnimFrame(savedAnimFrame)
	end

	PolyEditorUI.saved(PolyEditor.assetName)

	PolyEditor.textureLocked[PolyEditor.currentPageId] = false
	PolyEditor.dirty = false
	updateViewTitle()

	invokeProgram(MAIN_PROGRAMID, "refreshAssetIcons")
end

function PolyEditor.save()
	save()
end

function PolyEditor.saveNew()
	local newAssetName = findNewAssetName()
	setAssetName(newAssetName)

	PolyEditor.save()	
end

function initTextureMode()
	PolyEditor.textureData = getTexture(PolyEditor.currentPageId)

	local label = "texture region"
	PolyEditor.view1TextureLabelSprite = allocSprite()
	PolyEditor.view1TextureLabelSpriteData = {}
	PolyEditor.view1TextureLabelSpriteData.position = vec3(PolyEditor.view1SpriteData.position.x + PolyEditor.view1SpriteData.size.x - (8 * #label) - 2.0, PolyEditor.view1SpriteData.position.y + 2.0, 0.0)
	PolyEditor.view1TextureLabelSpriteData.size = vec2(8 * #label, 8)
	PolyEditor.view1TextureLabelSpriteData.color = vec4(0, 0, 0, 0)
	PolyEditor.view1TextureLabelSpriteData.text = label
	PolyEditor.view1TextureLabelSpriteData.textColor = Config.paletteNames.grey
	setSprite(PolyEditor.view1TextureLabelSprite, PolyEditor.view1TextureLabelSpriteData)

	PolyEditor.view1TextureSprite = allocSprite()
	PolyEditor.view1TextureSpriteData = {}
	PolyEditor.view1TextureSpriteData.textureId = PolyEditor.currentPageId
	PolyEditor.view1TextureSpriteData.size = vec2(128.0, 128.0)
	PolyEditor.view1TextureSpriteData.position = vec3(0.0, 0.0, 0.0)
	PolyEditor.view1TextureSpriteData.rotation = eulerRot(90.0, 0.0, 0.0)
	PolyEditor.view1TextureSpriteData.color = vec4(255, 255, 255, 255)
	setSprite(PolyEditor.view1TextureSprite, PolyEditor.view1TextureSpriteData)

	PolyEditor.camManipView1Texture.focusDist = 0.0061

	PolyEditor.uvRegionP1Sprite = allocSprite()
	PolyEditor.uvRegionP1SpriteData.billboard = 1.0
	PolyEditor.uvRegionP1SpriteData.billboardPixelPerfect = 1.0
	PolyEditor.uvRegionP1SpriteData.position = vec3(0.0, 0.0, 0.0)
	PolyEditor.uvRegionP1SpriteData.size = vec2(PolyEditor.vertexSize * 2.0, PolyEditor.vertexSize * 2.0)
	PolyEditor.uvRegionP1SpriteData.color = vec4(100, 100, 100, 255)

	PolyEditor.uvRegionP2Sprite = allocSprite()
	PolyEditor.uvRegionP2SpriteData.billboard = 1.0
	PolyEditor.uvRegionP2SpriteData.billboardPixelPerfect = 1.0
	PolyEditor.uvRegionP2SpriteData.position = vec3(0.0, 0.0, 0.0)
	PolyEditor.uvRegionP2SpriteData.size = vec2(PolyEditor.vertexSize * 2.0, PolyEditor.vertexSize * 2.0)
	PolyEditor.uvRegionP2SpriteData.color = vec4(100, 100, 100, 255)

	PolyEditor.uvRegionP3Sprite = allocSprite()
	PolyEditor.uvRegionP3SpriteData.billboard = 1.0
	PolyEditor.uvRegionP3SpriteData.billboardPixelPerfect = 1.0
	PolyEditor.uvRegionP3SpriteData.position = vec3(0.0, 0.0, 0.0)
	PolyEditor.uvRegionP3SpriteData.size = vec2(PolyEditor.vertexSize * 2.0, PolyEditor.vertexSize * 2.0)
	PolyEditor.uvRegionP3SpriteData.color = vec4(100, 100, 100, 255)

	PolyEditor.uvRegionP4Sprite = allocSprite()
	PolyEditor.uvRegionP4SpriteData.billboard = 1.0
	PolyEditor.uvRegionP4SpriteData.billboardPixelPerfect = 1.0
	PolyEditor.uvRegionP4SpriteData.position = vec3(0.0, 0.0, 0.0)
	PolyEditor.uvRegionP4SpriteData.size = vec2(PolyEditor.vertexSize * 2.0, PolyEditor.vertexSize * 2.0)
	PolyEditor.uvRegionP4SpriteData.color = vec4(100, 100, 100, 255)

	local label = "UV"
	PolyEditor.view3TextureLabelSprite = allocSprite()
	PolyEditor.view3TextureLabelSpriteData = {}
	PolyEditor.view3TextureLabelSpriteData.position = vec3(PolyEditor.view3SpriteData.position.x + PolyEditor.view3SpriteData.size.x - (8 * #label) - 2.0, PolyEditor.view3SpriteData.position.y + 2.0, 0.0)
	PolyEditor.view3TextureLabelSpriteData.size = vec2(8 * #label, 8)
	PolyEditor.view3TextureLabelSpriteData.color = vec4(0, 0, 0, 0)
	PolyEditor.view3TextureLabelSpriteData.text = label
	PolyEditor.view3TextureLabelSpriteData.textColor = Config.paletteNames.grey
	setSprite(PolyEditor.view3TextureLabelSprite, PolyEditor.view3TextureLabelSpriteData)

	PolyEditor.view3TextureSprite = allocSprite()
	PolyEditor.view3TextureSpriteData = {}
	PolyEditor.view3TextureSpriteData.textureId = PolyEditor.currentPageId
	PolyEditor.view3TextureSpriteData.size = vec2(128.0, 128.0)
	PolyEditor.view3TextureSpriteData.position = vec3(128.0 * 0.5, 128.0 * 0.5, -0.1)
	PolyEditor.view3TextureSpriteData.rotation = eulerRot(0.0, 0.0, 0.0)
	PolyEditor.view3TextureSpriteData.color = vec4(100, 100, 100, 255)
	
	PolyEditor.camManipView3Texture.focusDist = 0.0061
	PolyEditor.camManipView3Texture.pos.x = 128.0 * 0.5
	PolyEditor.camManipView3Texture.pos.y = 128.0 * 0.5

	-- Square-pixel display scale for the UV editor quad (updated per-frame from region aspect).
	PolyEditor.uvDispW = 128.0
	PolyEditor.uvDispH = 128.0
	PolyEditor.uvDispWLast = 128.0
	PolyEditor.uvDispHLast = 128.0

	local label = "paint"
	PolyEditor.view4TextureLabelSprite = allocSprite()
	PolyEditor.view4TextureLabelSpriteData = {}
	PolyEditor.view4TextureLabelSpriteData.position = vec3(PolyEditor.view4SpriteData.position.x + PolyEditor.view4SpriteData.size.x - (8 * #label) - 2.0, PolyEditor.view4SpriteData.position.y + 2.0, 0.0)
	PolyEditor.view4TextureLabelSpriteData.size = vec2(8 * #label, 8)
	PolyEditor.view4TextureLabelSpriteData.color = vec4(0, 0, 0, 0)
	PolyEditor.view4TextureLabelSpriteData.text = label
	PolyEditor.view4TextureLabelSpriteData.textColor = Config.paletteNames.grey
	setSprite(PolyEditor.view4TextureLabelSprite, PolyEditor.view4TextureLabelSpriteData)

	PolyEditor.view4TextureSprite = allocSprite()
	PolyEditor.view4TextureSpriteData = {}
	PolyEditor.view4TextureSpriteData.textureId = PolyEditor.currentPageId
	PolyEditor.view4TextureSpriteData.size = vec2(128.0, 128.0)
	PolyEditor.view4TextureSpriteData.position = vec3(0.0, 0.0, 0.0)
	PolyEditor.view4TextureSpriteData.rotation = eulerRot(0.0, -90.0, 0.0)
	PolyEditor.view4TextureSpriteData.color = vec4(255, 255, 255, 255)
	PolyEditor.view4TextureSpriteData.texturePos = vec2(0.0, 0.0)
	PolyEditor.view4TextureSpriteData.textureSize = vec2(10.0, 10.0)
	setSprite(PolyEditor.view4TextureSprite, PolyEditor.view4TextureSpriteData)

	PolyEditor.camManipView4Texture.focusDist = 0.0061

	PolyEditor.uvVertSprite = allocSprite()
	PolyEditor.uvVertSpriteData.position = vec3(0.0, 0.0, 0.0)
	PolyEditor.uvVertSpriteData.billboard = 1.0
	PolyEditor.uvVertSpriteData.billboardPixelPerfect = 1.0
	PolyEditor.uvVertSpriteData.size = vec2(4.0, 4.0)
	PolyEditor.uvVertSpriteData.color = Config.uiColors.polyEditorVertex

	PolyEditor.currentVertexColor = vec4(255, 255, 255, 255)
	PolyEditor.texturePencilColor = vec4Copy(Config.paletteNames.blackLight)
end

function cameraManipRot(cameraMat, camManip, viewSpriteData, viewSprite, viewId)
	if UILib.isHovered() == true then
		return
	end

	local mouseDelta = PolyEditor.mousePressedPos - PolyEditor.mousePos

	if vec2Length(mouseDelta) > 0.0 then
		PolyEditor.manipulatingCamera = true

		local dist = camManip.perspFocusDist or camManip.focusDist
		
		local forward = vec3(0.0, 0.0, 1.0) * camManip.rot
		local focusPoint = camManip.pos - (forward * dist)

		local manipSpeed = 1.0
		local delta = (PolyEditor.mousePressedPos - PolyEditor.mousePos) * (manipSpeed)
		camManip.rot = eulerRot(delta.y, 0.0, 0.0) * camManip.rot * eulerRot(0.0, delta.x, 0.0)

		local newForward = vec3(0.0, 0.0, 1.0) * camManip.rot
		camManip.pos = focusPoint + (newForward * dist)
		
		PolyEditor.mousePressedPos = PolyEditor.mousePos
	end
end

function cameraManipPan(cameraMat, camManip, viewSpriteData, viewSprite, viewId)
	if UILib.isHovered() == true then
		return
	end

	local mouseDelta = PolyEditor.mousePressedPos - PolyEditor.mousePos

	if vec2Length(mouseDelta) > 0.0 then
		PolyEditor.manipulatingCamera = true

		local p1 = screenToWorld(PolyEditor.mousePressedPos, PolyEditor.screenSize, camManip.focusDist, cameraMat, PolyEditor.camFov, camManip.ortho)
		local p2 = screenToWorld(PolyEditor.mousePos, PolyEditor.screenSize, camManip.focusDist, cameraMat, PolyEditor.camFov, camManip.ortho)
		
		if camManip.ortho == false then
			camManip.pos = camManip.pos + (p1 - p2)
		else
			camManip.pos = camManip.pos - (p1 - p2)
		end

		PolyEditor.mousePressedPos = PolyEditor.mousePos
	end
end

function cameraManipTrackpadPan(trackpadDelta, cameraMat, camManip, viewSpriteData, viewSprite, viewId)
	if UILib.isHovered() == true then
		return
	end

	PolyEditor.manipulatingCamera = true

	local p1 = screenToWorld(PolyEditor.mousePos, PolyEditor.screenSize, camManip.focusDist, cameraMat, PolyEditor.camFov, camManip.ortho)
	local p2 = screenToWorld(PolyEditor.mousePos - (trackpadDelta * 10.0), PolyEditor.screenSize, camManip.focusDist, cameraMat, PolyEditor.camFov, camManip.ortho)
	
	if camManip.ortho == false then
		camManip.pos = camManip.pos + (p1 - p2)
	else
		camManip.pos = camManip.pos - (p1 - p2)
	end
end

function cameraManipZoom(cameraMat, camManip, viewSpriteData, viewSprite, viewId)
	if UILib.isHovered() == true then
		return
	end

	local mouseDelta = PolyEditor.mousePressedPos - PolyEditor.mousePos

	if vec2Length(mouseDelta) > 0.0 then
		PolyEditor.manipulatingCamera = true

		local deltaLen = mouseDelta.x
		if abs(mouseDelta.y) > abs(mouseDelta.x) then
			deltaLen = mouseDelta.y
		end

		if camManip.ortho == false then
			local forward = vec3(0.0, 0.0, 1.0) * camManip.rot
			local focusPoint = camManip.pos - (forward * camManip.focusDist)

			local focusSpeed = 0.1 * PolyEditor.focusSpeed
			camManip.focusDist = clamp(camManip.focusDist + (deltaLen * focusSpeed), PolyEditor.perspCamZoomMin, PolyEditor.perspCamZoomMax)

			camManip.pos = focusPoint + (forward * camManip.focusDist)
		else
			local focusSpeed = 0.01 * PolyEditor.focusSpeed
			camManip.focusDist = clamp(camManip.focusDist - (deltaLen * focusSpeed), PolyEditor.orthoCamZoomMin, PolyEditor.orthoCamZoomMax)
		end

		PolyEditor.mousePressedPos = PolyEditor.mousePos
	end
end

function cameraManipScrollWheelZoom(scroll, cameraMat, camManip, viewSpriteData, viewSprite, viewId)
	if UILib.isHovered() == true then
		return
	end
	
	PolyEditor.manipulatingCamera = true

	if camManip.ortho == false then
		local forward = vec3(0.0, 0.0, 1.0) * camManip.rot
		local focusPoint = camManip.pos - (forward * camManip.focusDist)

		local focusSpeed = 1.5

		camManip.focusDist = clamp(camManip.focusDist + (scroll * focusSpeed), PolyEditor.perspCamZoomMin, PolyEditor.perspCamZoomMax)

		camManip.pos = focusPoint + (forward * camManip.focusDist)
	else
		if PolyEditor.mode == MODEL_MODE or PolyEditor.mode == RIG_MODE then
			local focusSpeed = 0.05
			camManip.focusDist = clamp(camManip.focusDist - (scroll * focusSpeed), PolyEditor.orthoCamZoomMin, PolyEditor.orthoCamZoomMax)
		else
			local focusSpeed = 0.002
			camManip.focusDist = clamp(camManip.focusDist - (scroll * focusSpeed), 0.0061, 0.3)
		end
	end
end

function PolyEditor.switchCameraPerspective()
	if PolyEditor.cameraType == CAMERA_PERSPECTIVE then
		_setCameraType(CAMERA_ORTHO)
	else
		_setCameraType(CAMERA_PERSPECTIVE)
	end
end

function _setCameraType(cameraType)
	local camManip = PolyEditor.camManipView2
	local tanHalfFov = math.tan(PolyEditor.camFov / 2.0)
	local forward = vec3(0.0, 0.0, 1.0) * camManip.rot

	if cameraType == CAMERA_ORTHO then
		local focusPoint = camManip.pos - (forward * camManip.focusDist)
		PolyEditor.cameraType = CAMERA_ORTHO
		camManip.ortho = true
		camManip.perspFocusDist = camManip.focusDist
		camManip.focusDist = clamp(1.0 / (camManip.perspFocusDist * tanHalfFov), PolyEditor.orthoCamZoomMin, PolyEditor.orthoCamZoomMax)
		
		PolyEditor.viewLabelSpriteData[2].text = "ortho"
	else
		if camManip.perspFocusDist == nil then
			camManip.perspFocusDist = 10.0
			camManip.focusDist = 0.25
		end

		local focusPoint = camManip.pos - (forward * camManip.perspFocusDist)
		PolyEditor.cameraType = CAMERA_PERSPECTIVE
		camManip.ortho = false
		camManip.focusDist = clamp(1.0 / (camManip.focusDist * tanHalfFov), 1.0, 50.0)
		camManip.perspFocusDist = nil
		camManip.pos = focusPoint + (forward * camManip.focusDist)

		PolyEditor.viewLabelSpriteData[2].text = "persp"
	end

	setSprite(PolyEditor.viewLabelSprites[2], PolyEditor.viewLabelSpriteData[2])

	Utils.setAppVar("cameraType", PolyEditor.cameraType)

	invokeProgram(MAIN_PROGRAMID, "refreshModelVisualizer")
end

function insideViewport(mousePos, viewSpriteData)
	local insideView = false
	if intersectRect(mousePos, vec2(viewSpriteData.position.x, viewSpriteData.position.y), viewSpriteData.size) then
		insideView = true
	end

	return insideView
end

function updateCamera(cameraMat, camManip, viewSpriteData, viewSprite, viewId)
	if PolyEditor.mouseDown == -1 then
		PolyEditor.manipulatingCamera = false
	end

	if PolyEditor.mouseInViewport == viewId then
		local mousePosInViewport = viewportToScreen(PolyEditor.mousePos, viewSpriteData, camManip.ortho)

		PolyEditor.mousePos3d = screenToWorld(mousePosInViewport, PolyEditor.screenSize, camManip.focusDist, cameraMat, PolyEditor.camFov, camManip.ortho)
		if PolyEditor.mousePressed ~= -1 then
			local mousePressedPosInViewport = viewportToScreen(PolyEditor.mousePressedPos, viewSpriteData, camManip.ortho)
			PolyEditor.mousePressedPos3d = screenToWorld(mousePressedPosInViewport, PolyEditor.screenSize, camManip.focusDist, cameraMat, PolyEditor.camFov, camManip.ortho)
			PolyEditor.mousePressedInViewport = viewId
		end

		if PolyEditor.draggingVerts == false then
			local trackpadPan = getTrackpadPanned()
			if vec2Length(trackpadPan) > 0.0 then
				cameraManipTrackpadPan(trackpadPan, cameraMat, camManip, viewSpriteData, viewSprite, viewId)
			end

			local scrollWheel = getMouseWheelScrolled()

			if scrollWheel ~= 0.0 then
				cameraManipScrollWheelZoom(scrollWheel, cameraMat, camManip, viewSpriteData, viewSprite, viewId)
			end

			local pinchZoom = getTrackpadPinchZoom()
			if pinchZoom ~= 0.0 then
				cameraManipScrollWheelZoom(-pinchZoom, cameraMat, camManip, viewSpriteData, viewSprite, viewId)
			end
		end
	end

	if PolyEditor.clickedViewport == viewId then
		local leftClickBusy = false

		if hasActiveTool() == true then
			if PolyEditor.pickedQuad ~= -1 or #PolyEditor.pickedVerts > 0 then
				leftClickBusy = true

				if PolyEditor.draggingVerts == false and PolyEditor.clickedViewport == 2 and _pickedInLayerSelection(PolyEditor.pickedVerts, PolyEditor.pickedLayerId or PolyEditor.currentLayerId, false) == false then
					leftClickBusy = false
				end
			end
		end

		if PolyEditor.clickedViewport == 2 and PolyEditor.mode == TEXTURE_MODE then
			leftClickBusy = false
		end

		if leftClickBusy == false and PolyEditor.draggingVerts == false then
			-- rotate
			if viewId == 2 then
				if PolyEditor.mouseDown == MOUSE_BUTTON_LEFT then
					cameraManipRot(cameraMat, camManip, viewSpriteData, viewSprite, viewId)
				end
			end
		end

		-- pan
		if PolyEditor.mouseDown == MOUSE_BUTTON_MIDDLE then
			cameraManipPan(cameraMat, camManip, viewSpriteData, viewSprite, viewId)
		end

		-- zoom
		if PolyEditor.mouseDown == MOUSE_BUTTON_RIGHT then
			cameraManipZoom(cameraMat, camManip, viewSpriteData, viewSprite, viewId)
		end
	end

	cameraMat.position = camManip.pos
	cameraMat.rotation = camManip.rot

	if camManip.ortho == true then
		setCameraOrtho(cameraMat, camManip.focusDist)
	else
		setCamera(cameraMat, PolyEditor.camFov)
	end
end

function viewportToScreen(screenPoint, viewSpriteData, ortho)
	local viewSpriteCenterX = (PolyEditor.screenWidth * 0.5)
	local viewSpriteLeft = viewSpriteCenterX - (viewSpriteData.size.x * 0.5)
	local viewSpriteRight = viewSpriteCenterX + (viewSpriteData.size.x * 0.5)

	local screenPointInViewport = vec2(0.0, 0.0)
	screenPointInViewport.x = remapRange(
		screenPoint.x, viewSpriteData.position.x, viewSpriteData.position.x + viewSpriteData.size.x, 
		viewSpriteLeft, viewSpriteRight)

	local viewSpriteCenterY = (PolyEditor.screenHeight * 0.5)
	local viewSpriteTopBound = viewSpriteCenterY + (viewSpriteData.size.y * 0.5)
	local viewSpriteBottomBound = viewSpriteCenterY - (viewSpriteData.size.y * 0.5)

	screenPointInViewport.y = remapRange(screenPoint.y, 
		viewSpriteData.position.y, viewSpriteData.position.y + viewSpriteData.size.y, 
		viewSpriteBottomBound, viewSpriteTopBound)

	if ortho == true then
		screenPointInViewport.x = PolyEditor.screenWidth - screenPointInViewport.x
		screenPointInViewport.y = PolyEditor.screenHeight - screenPointInViewport.y
	end

	return screenPointInViewport
end

function drawViewportGrid(viewSpriteData, camManipView)
	setDepthEnabled(false)
	
	local gridScale = remapRange(camManipView.focusDist, PolyEditor.orthoCamZoomMin, PolyEditor.orthoCamZoomMax, 0.0, 1.0)

	drawGrid(gridScale, camManipView.rot, Config.uiColors.polyEditorGrid)

	drawLine(vec3(0.0, -0.1, 0.0), vec3(0.0, 0.1, 0.0), Config.uiColors.polyEditorGrid + 50)
	drawLine(vec3(-0.1, 0.0, 0.0), vec3(0.1, 0.0, 0.0), Config.uiColors.polyEditorGrid + 50)
	drawLine(vec3(0.0, 0.0, -0.1), vec3(0.0, 0.0, 0.1), Config.uiColors.polyEditorGrid + 50)

	setDepthEnabled(true)
end

function drawTileBounds()
	if PolyEditor.tileBoundsEnabled == false then return end

	local half = PolyEditor.tileSize * 0.5
	local min = vec3(-half.x, -half.y, -half.z)
	local max = vec3(half.x, half.y, half.z)
	local color = Config.uiColors.polyEditorGrid + 50

	setDepthEnabled(false)

	drawLine(vec3(min.x, min.y, min.z), vec3(max.x, min.y, min.z), color)
	drawLine(vec3(min.x, min.y, min.z), vec3(min.x, max.y, min.z), color)
	drawLine(vec3(min.x, min.y, min.z), vec3(min.x, min.y, max.z), color)

	drawLine(vec3(max.x, max.y, max.z), vec3(min.x, max.y, max.z), color)
	drawLine(vec3(max.x, max.y, max.z), vec3(max.x, min.y, max.z), color)
	drawLine(vec3(max.x, max.y, max.z), vec3(max.x, max.y, min.z), color)

	drawLine(vec3(min.x, max.y, min.z), vec3(max.x, max.y, min.z), color)
	drawLine(vec3(min.x, max.y, min.z), vec3(min.x, max.y, max.z), color)

	drawLine(vec3(max.x, min.y, min.z), vec3(max.x, max.y, min.z), color)
	drawLine(vec3(max.x, min.y, min.z), vec3(max.x, min.y, max.z), color)

	drawLine(vec3(min.x, min.y, max.z), vec3(max.x, min.y, max.z), color)
	drawLine(vec3(min.x, min.y, max.z), vec3(min.x, max.y, max.z), color)

	setDepthEnabled(true)
end

function drawOrthoViewModel(cameraMat, camManip, viewSpriteData, viewSprite, viewId)
	local viewSpriteCenterX = (PolyEditor.screenWidth * 0.5)
	local viewSpriteLeft = viewSpriteCenterX - (viewSpriteData.size.x * 0.5)

	local viewSpriteCenterY = (PolyEditor.screenHeight * 0.5)
	local viewSpriteBottomBound = viewSpriteCenterY - (viewSpriteData.size.y * 0.5)

	setSprite(viewSprite, viewSpriteData)
	startRenderToSprite(viewSprite, 
		vec2(viewSpriteLeft, viewSpriteBottomBound),
		viewSpriteData.size)

	drawViewportGrid(viewSpriteData, camManip)

	drawTileBounds()

	drawOverlayLayers()

	local backfaceCull = false
	setDepthEnabled(false)
	_drawSelectedLayersWireframe(backfaceCull)
	drawWireframe(PolyEditor.currentMesh, Config.uiColors.polyEditorWireframe, backfaceCull)
	setDepthEnabled(true)

	if PolyEditor.newPrimitiveMesh ~= nil then
		setDepthEnabled(false)
		drawWireframe(PolyEditor.newPrimitiveMesh, Config.uiColors.polyEditorWireframe, backfaceCull)
		setDepthEnabled(true)
	end

	drawVertices(viewId)

	if PolyEditor.debugMousePos == true then
		local p = screenToWorld(viewportToScreen(PolyEditor.mousePos, viewSpriteData, true), PolyEditor.screenSize, camManip.focusDist, cameraMat, 1.0, true)

		-- local mouseInViewport = viewportToScreen(PolyEditor.mousePos, viewSpriteData, true)
		-- local ray = screenRay(mouseInViewport, -camManip.focusDist, cameraMat, true)
		-- local p = ray.origin + (ray.direction * camManip.focusDist)

		drawLine(vec3(0.0, 0.0, 0.0), p)
	end
end

function drawOrthoViewTexture(cameraMat, camManip, viewSpriteData, viewSprite, viewId)
	local viewSpriteCenterX = (PolyEditor.screenWidth * 0.5)
	local viewSpriteLeft = viewSpriteCenterX - (viewSpriteData.size.x * 0.5)

	local viewSpriteCenterY = (PolyEditor.screenHeight * 0.5)
	local viewSpriteBottomBound = viewSpriteCenterY - (viewSpriteData.size.y * 0.5)

	setSprite(viewSprite, viewSpriteData)
	startRenderToSprite(viewSprite, 
		vec2(viewSpriteLeft, viewSpriteBottomBound),
		viewSpriteData.size)

	if PolyEditor.debugMousePos == true then
		local p = screenToWorld(viewportToScreen(PolyEditor.mousePos, viewSpriteData, true), PolyEditor.screenSize, camManip.focusDist, cameraMat, 1.0, true)
		
		-- local mouseInViewport = viewportToScreen(PolyEditor.mousePos, viewSpriteData, true)
		-- local ray = screenRay(mouseInViewport, -camManip.focusDist, cameraMat, true)
		-- local p = ray.origin + (ray.direction * camManip.focusDist)

		drawLine(vec3(0.0, 0.0, 0.0), p)
	end
end

function drawOrthoViewRig(cameraMat, camManip, viewSpriteData, viewSprite, viewId)
	local viewSpriteCenterX = (PolyEditor.screenWidth * 0.5)
	local viewSpriteLeft = viewSpriteCenterX - (viewSpriteData.size.x * 0.5)

	local viewSpriteCenterY = (PolyEditor.screenHeight * 0.5)
	local viewSpriteBottomBound = viewSpriteCenterY - (viewSpriteData.size.y * 0.5)

	setSprite(viewSprite, viewSpriteData)
	startRenderToSprite(viewSprite, 
		vec2(viewSpriteLeft, viewSpriteBottomBound),
		viewSpriteData.size)

	drawViewportGrid(viewSpriteData, camManip)

	drawOverlayLayers()

	local pivotPos = currentLayerPivotPos()
	drawLine(pivotPos, pivotPos + vec3(1.0, 0.0, 0.0), vec4(255.0, 0.0, 100.0, 255.0))
	drawLine(pivotPos, pivotPos + vec3(0.0, 1.0, 0.0), vec4(0.0, 255.0, 100.0, 255.0))
	drawLine(pivotPos, pivotPos + vec3(0.0, 0.0, 1.0), vec4(100.0, 0.0, 255.0, 255.0))

	drawSprite(PolyEditor.pivotCenterSprite)

	local backfaceCull = false
	setDepthEnabled(false)
	_drawSelectedLayersWireframe(backfaceCull)
	drawWireframe(PolyEditor.currentMesh, Config.uiColors.polyEditorWireframe, backfaceCull)
	setDepthEnabled(true)
end

function _drawLayerMeshSolid(mesh)
	if PolyEditor.shadingType == 1 or PolyEditor.shadingType == 4 then
		setMeshMicroShader(mesh, "useTexture", 1.0, MICROSHADER_UNIFORM)
	else
		setMeshMicroShader(mesh, "useTexture", 0.0, MICROSHADER_UNIFORM)
	end

	updateMeshLight(mesh)

	drawMesh(mesh)
end

function _drawSelectedLayersWireframe(backfaceCull)
	for i = 1, #PolyEditor.selectedLayers do
		local layerId = PolyEditor.selectedLayers[i]
		if layerId ~= PolyEditor.currentLayerId then
			local meshData = PolyEditor.meshes[layerId]
			if meshData ~= nil then
				drawWireframe(meshData.mesh, Config.uiColors.polyEditorWireframe, backfaceCull)
			end
		end
	end
end

function drawOverlayLayers()
	local pageData = PolyEditor.pagesData[PolyEditor.currentPageId]
	
	local backfaceCull = true
	for layerId, v in pairs(pageData.overlayLayers) do
		if layerId ~= PolyEditor.currentLayerId then
			local meshData = PolyEditor.meshes[layerId]
			if meshData ~= nil then
				local overlayMesh = PolyEditor.meshes[layerId].mesh
				drawWireframe(overlayMesh, Config.uiColors.polyEditorOverlayWireframe, backfaceCull)
			end
		end
	end
end

function drawPerspViewport(cameraMat, camManip, viewSpriteData, viewSprite, viewId)
	if PolyEditor.bilinearInterp == true then
		setBilinearInterpEnabled(true)
	end

	local viewSpriteCenterX = (PolyEditor.screenWidth * 0.5)
	local viewSpriteLeft = viewSpriteCenterX - (viewSpriteData.size.x * 0.5)

	local viewSpriteCenterY = (PolyEditor.screenHeight * 0.5)
	local viewSpriteBottomBound = viewSpriteCenterY - (viewSpriteData.size.y * 0.5)

	setSprite(viewSprite, viewSpriteData)
	startRenderToSprite(viewSprite, 
		vec2(viewSpriteLeft, viewSpriteBottomBound), 
		viewSpriteData.size)

	local lit = PolyEditor.shadingType ~= 4

	if lit then
		PolyUtil.beginLighting()
	end

	if PolyEditor.showAllLayers == true then
		for i = 1, MESH_LAYERS do
			local meshData = PolyEditor.meshes[i]
			if meshData ~= nil then
				_drawLayerMeshSolid(meshData.mesh)
			end
		end
	end

	if lit then
		PolyUtil.endLighting()
	end

	drawOverlayLayers()

	drawTileBounds()

	if lit then
		PolyUtil.beginLighting()
	end

	if PolyEditor.showAllLayers == false then          -- showAllLayers already draws every layer
		for i = 1, #PolyEditor.selectedLayers do
			local layerId = PolyEditor.selectedLayers[i]
			local meshData = PolyEditor.meshes[layerId]
			if meshData ~= nil then
				_drawLayerMeshSolid(meshData.mesh)
			end
		end
	end

	if PolyEditor.newPrimitiveMesh ~= nil then
		drawMesh(PolyEditor.newPrimitiveMesh)
	end

	if lit then
		PolyUtil.endLighting()
	end

	if PolyEditor.shadingType == 3 then
		local backfaceCull = true
		setDepthEnabled(false)
		drawWireframe(PolyEditor.currentMesh, Config.paletteNames.grey, backfaceCull)
		setDepthEnabled(true)

		if PolyEditor.newPrimitiveMesh ~= nil then
			setDepthEnabled(false)
			drawWireframe(PolyEditor.newPrimitiveMesh, Config.paletteNames.grey, backfaceCull)
			setDepthEnabled(true)
		end
	end
	
	if PolyEditor.drawPerspVertsEnabled == true then
		drawVertices(viewId)
	end

	if PolyEditor.debugMousePos == true then
		local p = screenToWorld(viewportToScreen(PolyEditor.mousePos + vec2(2.0, 0.0), viewSpriteData, false), PolyEditor.screenSize, PolyEditor.camManipView2.focusDist, cameraMat, PolyEditor.camFov, false)
		drawLine(vec3(0.0, 0.0, 0.0), p)
	end

	if PolyEditor.bilinearInterp == true then
		setBilinearInterpEnabled(false)
	end
end

function pickedVertsInSelection(pickedVerts, selected, fullSearch, mesh)
	if #selected == 0 or #pickedVerts == 0 then
		return false
	end

	mesh = mesh or PolyEditor.currentMesh

	for j = 1, #pickedVerts do
		local pickVertHash = getVertexHash(mesh, pickedVerts[j])

		local foundInSel = false

		for i = 1, #selected do
			local selVertHash = getVertexHash(mesh, selected[i])

			if pickVertHash == selVertHash then
				foundInSel = true
				break
			end
		end

		if fullSearch == true then
			if foundInSel then
				return true
			end
		else
			if foundInSel == false then
				return false
			end
		end
	end

	if fullSearch == true then
		return false
	end

	return true
end

function moveToolUpdate()
	if PolyEditor.pickingSelection == true then
		UILib.setCursor(UILib.CURSOR_MOVE)	
	else
		UILib.setCursor(UILib.CURSOR)
	end

	if PolyEditor.draggingVerts == true then
		local mouseDeltaVec = PolyEditor.mousePos3d - PolyEditor.mousePressedPos3d
		local pivots = _dragLayerPivots()

		for i = 1, #PolyEditor.dragVerts do
			local d = PolyEditor.dragVerts[i]

			local p = PolyEditor.dragVertsPos[i] + mouseDeltaVec

			if PolyEditor.snapEnabled then
				p.x = PolyEditor.snapStep * round(p.x / PolyEditor.snapStep)
				p.y = PolyEditor.snapStep * round(p.y / PolyEditor.snapStep)
				p.z = PolyEditor.snapStep * round(p.z / PolyEditor.snapStep)
			end

			local meshData = PolyEditor.meshes[d.layerId]
			if meshData ~= nil and meshData.cache ~= nil then
				meshData.cache.quads[d.vertId] = p - pivots[d.layerId]
			end
		end
		_pushDragLayers()

		PolyEditor.dragToolEngaged = true
	end

	if PolyEditor.dragToolEngaged == true and PolyEditor.mouseReleased == MOUSE_BUTTON_LEFT then
		PolyEditor.dragToolEngaged = false

		_finishDrag()
	end
end

function updateUV()
	setMeshVertices(PolyEditor.currentMesh, VERT_UV, PolyEditor.currentMeshUVs)
	setMeshTexture(PolyEditor.currentMesh, PolyEditor.currentPageId, PolyEditor.uvRegionPos, PolyEditor.uvRegionSize)
end

function moveUVToolUpdate()
	if PolyEditor.pickingSelection == true then
		UILib.setCursor(UILib.CURSOR_MOVE)	
	else
		UILib.setCursor(UILib.CURSOR)
	end

	local pixelSizeX = 1.0 / (PolyEditor.uvRegionSize.x * 128.0)
	local pixelSizeY = 1.0 / (PolyEditor.uvRegionSize.y * 128.0)

	if PolyEditor.draggingVerts == true then
		local mouseDeltaVec = PolyEditor.mousePos3d - PolyEditor.mousePressedPos3d

		for i = 1, #PolyEditor.dragVerts do
			local vertId = PolyEditor.dragVerts[i]

			local startUVPos = PolyEditor.dragVertsPos[i]

			local newUVPos = startUVPos + mouseDeltaVec

			local newUV = vec2(newUVPos.x / PolyEditor.uvDispW, newUVPos.y / PolyEditor.uvDispH)

			newUV.x = pixelSizeX * round(newUV.x / pixelSizeX)
			newUV.y = pixelSizeY * round(newUV.y / pixelSizeY)

			PolyEditor.currentMeshUVs[vertId] = newUV
		end

		updateUV()

		PolyEditor.dragToolEngaged = true
	end

	if PolyEditor.dragToolEngaged == true and PolyEditor.mouseReleased == MOUSE_BUTTON_LEFT then
		PolyEditor.dragToolEngaged = false

		PolyEditor.skipClickRelease = true

		refreshCurrentMesh()

		storeUndo()
	end
end

function rotateUVToolUpdate()
	if PolyEditor.pickingSelection == true then
		UILib.setCursor(UILib.CURSOR_ROTATE)	
	else
		UILib.setCursor(UILib.CURSOR)
	end

	if PolyEditor.selectedUVs == 0 then
		return
	end

	if PolyEditor.mouseInViewport == -1 or PolyEditor.mousePressedInViewport == -1 then
		return
	end

	local ortho = true
	local fov = - PolyEditor.camManipView3.focusDist
	
	if PolyEditor.draggingVertsJustStarted == true then
		PolyEditor.dragCenter = vec3(0.0, 0.0, 0.0)

		PolyEditor.editingUVs = {}

		for i = 1, #PolyEditor.selectedUVs do
			local vId = PolyEditor.selectedUVs[i]

			local uv = PolyEditor.currentMeshUVs[vId]
			PolyEditor.dragCenter = PolyEditor.dragCenter + vec3(uv.x * PolyEditor.uvDispW, uv.y * PolyEditor.uvDispH, 0.0)

			PolyEditor.editingUVs[vId] = vec2Copy(uv)
		end

		PolyEditor.dragCenter = PolyEditor.dragCenter * (1.0 / #PolyEditor.selectedUVs)

		local camMat = PolyEditor.camView3Mat

		local viewSpriteData = PolyEditor.view3SpriteData

		local mouseInViewport = viewportToScreen(PolyEditor.mousePos, viewSpriteData, ortho)
		local ray = screenRay(mouseInViewport, fov, camMat, ortho)

		PolyEditor.dragMousePressedPos3d = ray.origin + (ray.direction * intersectPlane(ray.origin, ray.direction, PolyEditor.dragCenter, - camMat.forwardAxis))
		
		local clickMat = mat4LookAt(vec3(0.0, 0.0, 0.0), 
			camMat.forwardAxis,
			vec3Normalize(PolyEditor.dragMousePressedPos3d - PolyEditor.dragCenter))

		local cameraRestMat = mat4LookAt(vec3(0.0, 0.0, 0.0), 
			camMat.forwardAxis,
			camMat.upAxis)

		mat4Invert(clickMat)

		PolyEditor.dragOffsetMat = cameraRestMat * clickMat

		mat4Invert(cameraRestMat)

		PolyEditor.dragRestMat = cameraRestMat
	end

	if PolyEditor.draggingVerts == true then
		local camMat = PolyEditor.camView3Mat

		local viewSpriteData = PolyEditor.view3SpriteData

		local mouseInViewport = viewportToScreen(PolyEditor.mousePos, viewSpriteData, ortho)
		local ray = screenRay(mouseInViewport, fov, camMat, ortho)

		local p = ray.origin + (ray.direction * intersectPlane(ray.origin, ray.direction, PolyEditor.dragCenter, - camMat.forwardAxis))

		local dragMat = mat4LookAt(vec3(0.0, 0.0, 0.0), 
			camMat.forwardAxis,
			vec3Normalize(p - PolyEditor.dragCenter))

		local rotateMat = PolyEditor.dragOffsetMat * (dragMat * PolyEditor.dragRestMat)

		local pixelSizeX = 1.0 / (PolyEditor.uvRegionSize.x * 128.0)
		local pixelSizeY = 1.0 / (PolyEditor.uvRegionSize.y * 128.0)

		for i = 1, #PolyEditor.selectedUVs do
			local vertId = PolyEditor.selectedUVs[i]

			local uv = PolyEditor.editingUVs[vertId]
			local uvP = vec3(uv.x * PolyEditor.uvDispW, uv.y * PolyEditor.uvDispH, 0.0) - PolyEditor.dragCenter

			local newUVP = PolyEditor.dragCenter + (rotateMat * uvP)

			newUVP.x = pixelSizeX * round((newUVP.x / PolyEditor.uvDispW) / pixelSizeX)
			newUVP.y = pixelSizeY * round((newUVP.y / PolyEditor.uvDispH) / pixelSizeY)

			PolyEditor.currentMeshUVs[vertId] = vec2(newUVP.x, newUVP.y)
		end

		updateUV()

		PolyEditor.dragToolEngaged = true
	end

	if PolyEditor.dragToolEngaged == true and PolyEditor.mouseReleased == MOUSE_BUTTON_LEFT then
		PolyEditor.dragToolEngaged = false

		refreshCurrentMesh()

		storeUndo()
	end
end

function scaleUVToolUpdate()
	if PolyEditor.pickingSelection == true then
		UILib.setCursor(UILib.CURSOR_SCALE)	
	else
		UILib.setCursor(UILib.CURSOR)
	end

	if PolyEditor.selectedUVs == 0 then
		return
	end

	if PolyEditor.mouseInViewport == -1 or PolyEditor.mousePressedInViewport == -1 then
		return
	end

	local ortho = true
	local fov = - PolyEditor.camManipView3.focusDist

	if PolyEditor.draggingVertsJustStarted == true then
		PolyEditor.editingUVs = {}
		PolyEditor.dragScaleRest = vec3(0.0, 0.0, 0.0)
		PolyEditor.editingUVScale = -1

		local mouseInViewport = viewportToScreen(PolyEditor.mousePos, PolyEditor.view3SpriteData, ortho)
		local ray = screenRay(mouseInViewport, fov, PolyEditor.camView3Mat, ortho)
		local p = ray.origin + (ray.direction * intersectPlane(ray.origin, ray.direction, vec3(0.0, 0.0, 0.0), - PolyEditor.camView3Mat.forwardAxis))
		local mousePixelPos = vec3(p.x, p.y, 0.0)

		for i = 1, #PolyEditor.selectedUVs do
			local vId = PolyEditor.selectedUVs[i]

			local uv = PolyEditor.currentMeshUVs[vId]

			PolyEditor.editingUVs[i] = vec2Copy(uv)
		end

		local maxDist = 0.0
		local maxDistP = vec3(0.0, 0.0, 0.0)

		for i = 1, #PolyEditor.selectedUVs do
			local vId = PolyEditor.selectedUVs[i]

			local uv = PolyEditor.currentMeshUVs[vId]
			local uvP = vec3(uv.x * PolyEditor.uvDispW, uv.y * PolyEditor.uvDispH, 0.0)

			local dist = vec3Length(uvP - mousePixelPos)
			if dist > maxDist then
				maxDist = dist

				maxDistP = uvP
			end
		end

		PolyEditor.dragCenter = maxDistP

		local pReproj = ray.origin + (ray.direction * intersectPlane(ray.origin, ray.direction, PolyEditor.dragCenter, - PolyEditor.camView3Mat.forwardAxis))
		PolyEditor.dragScaleRest = pReproj - PolyEditor.dragCenter
		PolyEditor.dragScaleRest.x = abs(PolyEditor.dragScaleRest.x)
		PolyEditor.dragScaleRest.y = abs(PolyEditor.dragScaleRest.y)

	end

	if PolyEditor.draggingVerts == true and #PolyEditor.editingUVs > 0 then
		local camMat = PolyEditor.camView3Mat

		local viewSpriteData = PolyEditor.view3SpriteData

		local mouseInViewport = viewportToScreen(PolyEditor.mousePos, viewSpriteData, ortho)
		local ray = screenRay(mouseInViewport, fov, camMat, ortho)

		local p = ray.origin + (ray.direction * intersectPlane(ray.origin, ray.direction, PolyEditor.dragCenter, - camMat.forwardAxis))

		local scaleVec = p - PolyEditor.dragCenter

		local scaleX = 1.0
		if PolyEditor.dragScaleRest.x > 0.0 then
			scaleX = abs(scaleVec.x) / PolyEditor.dragScaleRest.x
		end
		
		local scaleY = 1.0
		if PolyEditor.dragScaleRest.y > 0.0 then
			scaleY = abs(scaleVec.y) / PolyEditor.dragScaleRest.y
		end
		
		local scale = vec3(1.0, 1.0, 1.0)

		scale.x = scaleX
		scale.y = scaleY

		local pixelSizeX = 1.0 / (PolyEditor.uvRegionSize.x * 128.0)
		local pixelSizeY = 1.0 / (PolyEditor.uvRegionSize.y * 128.0)

		for i = 1, #PolyEditor.selectedUVs do
			local vertId = PolyEditor.selectedUVs[i]

			local uv = PolyEditor.editingUVs[i]
			local uvP = vec3(uv.x * PolyEditor.uvDispW, uv.y * PolyEditor.uvDispH, 0.0) - PolyEditor.dragCenter

			local newUV = PolyEditor.dragCenter + (uvP * scale)
			newUV.x = pixelSizeX * round((newUV.x / PolyEditor.uvDispW) / pixelSizeX)
			newUV.y = pixelSizeY * round((newUV.y / PolyEditor.uvDispH) / pixelSizeY)

			PolyEditor.currentMeshUVs[vertId] = vec2(newUV.x, newUV.y)
		end

		updateUV()

		PolyEditor.dragToolEngaged = true
	end

	if PolyEditor.dragToolEngaged == true and PolyEditor.mouseReleased == MOUSE_BUTTON_LEFT then
		PolyEditor.dragToolEngaged = false

		refreshCurrentMesh()

		storeUndo()
	end
end

function updateViewport1Model()
	updateCamera(PolyEditor.camView1Mat, PolyEditor.camManipView1, PolyEditor.view1SpriteData, PolyEditor.view1Sprite, 1)

	drawOrthoViewModel(PolyEditor.camView1Mat, PolyEditor.camManipView1, PolyEditor.view1SpriteData, PolyEditor.view1Sprite, 1)

	if PolyEditor.mouseInViewport == 1 then
		updateMeshPicking(PolyEditor.camView1Mat, PolyEditor.camManipView1, PolyEditor.view1SpriteData, true)
		updateModelTools()
	end

	drawSprite2D(PolyEditor.view1Sprite)

	drawSprite2D(PolyEditor.viewLabelSprites[1])
end

function updateViewport2Model()
	updateCamera(PolyEditor.camView2Mat, PolyEditor.camManipView2, PolyEditor.view2SpriteData, PolyEditor.view2Sprite, 2)

	drawPerspViewport(PolyEditor.camView2Mat, PolyEditor.camManipView2, PolyEditor.view2SpriteData, PolyEditor.view2Sprite, 2)

	if PolyEditor.mouseInViewport == 2 then
		updateMeshPicking(PolyEditor.camView2Mat, PolyEditor.camManipView2, PolyEditor.view2SpriteData, false)
		updateModelTools()
	end

	if PolyEditor.ditherEnabled == true then
		setDitherEnabled(true)
	end

	drawSprite2D(PolyEditor.view2Sprite)

	if PolyEditor.ditherEnabled == true then
		setDitherEnabled(false)
	end

	drawSprite2D(PolyEditor.viewLabelSprites[2])
end

function updateViewport3Model()
	updateCamera(PolyEditor.camView3Mat, PolyEditor.camManipView3, PolyEditor.view3SpriteData, PolyEditor.view3Sprite, 3)

	drawOrthoViewModel(PolyEditor.camView3Mat, PolyEditor.camManipView3, PolyEditor.view3SpriteData, PolyEditor.view3Sprite, 3)

	if PolyEditor.mouseInViewport == 3 then
		updateMeshPicking(PolyEditor.camView3Mat, PolyEditor.camManipView3, PolyEditor.view3SpriteData, true)
		updateModelTools()
	end

	drawSprite2D(PolyEditor.view3Sprite)

	drawSprite2D(PolyEditor.viewLabelSprites[3])
end

function updateViewport4Model()
	updateCamera(PolyEditor.camView4Mat, PolyEditor.camManipView4, PolyEditor.view4SpriteData, PolyEditor.view4Sprite, 4)

	drawOrthoViewModel(PolyEditor.camView4Mat, PolyEditor.camManipView4, PolyEditor.view4SpriteData, PolyEditor.view4Sprite, 4)

	if PolyEditor.mouseInViewport == 4 then
		updateMeshPicking(PolyEditor.camView4Mat, PolyEditor.camManipView4, PolyEditor.view4SpriteData, true)
		updateModelTools()
	end

	drawSprite2D(PolyEditor.view4Sprite)

	drawSprite2D(PolyEditor.viewLabelSprites[4])
end

function updateViewport1Rig()
	updateCamera(PolyEditor.camView1Mat, PolyEditor.camManipView1, PolyEditor.view1SpriteData, PolyEditor.view1Sprite, 1)

	drawOrthoViewRig(PolyEditor.camView1Mat, PolyEditor.camManipView1, PolyEditor.view1SpriteData, PolyEditor.view1Sprite, 1)

	if PolyEditor.mouseInViewport == 1 then
		updateRigTools()
	end

	drawSprite2D(PolyEditor.view1Sprite)

	drawSprite2D(PolyEditor.viewLabelSprites[1])
end

function updateViewport2Rig()
	updateCamera(PolyEditor.camView2Mat, PolyEditor.camManipView2, PolyEditor.view2SpriteData, PolyEditor.view2Sprite, 2)

	drawPerspViewport(PolyEditor.camView2Mat, PolyEditor.camManipView2, PolyEditor.view2SpriteData, PolyEditor.view2Sprite, 2)

	setDepthEnabled(false)

	local pivotPos = currentLayerPivotPos()
	drawLine(pivotPos, pivotPos + vec3(1.0, 0.0, 0.0), vec4(255.0, 0.0, 100.0, 255.0))
	drawLine(pivotPos, pivotPos + vec3(0.0, 1.0, 0.0), vec4(0.0, 255.0, 100.0, 255.0))
	drawLine(pivotPos, pivotPos + vec3(0.0, 0.0, 1.0), vec4(100.0, 0.0, 255.0, 255.0))
	
	setDepthEnabled(true)

	if PolyEditor.mouseInViewport == 2 then
		updateRigTools()
	end

	if PolyEditor.ditherEnabled == true then
		setDitherEnabled(true)
	end

	drawSprite2D(PolyEditor.view2Sprite)

	if PolyEditor.ditherEnabled == true then
		setDitherEnabled(false)
	end

	drawSprite2D(PolyEditor.viewLabelSprites[2])
end

function updateViewport3Rig()
	updateCamera(PolyEditor.camView3Mat, PolyEditor.camManipView3, PolyEditor.view3SpriteData, PolyEditor.view3Sprite, 3)

	drawOrthoViewRig(PolyEditor.camView3Mat, PolyEditor.camManipView3, PolyEditor.view3SpriteData, PolyEditor.view3Sprite, 3)

	if PolyEditor.mouseInViewport == 3 then
		updateRigTools()
	end

	drawSprite2D(PolyEditor.view3Sprite)

	drawSprite2D(PolyEditor.viewLabelSprites[3])
end

function updateViewport4Rig()
	updateCamera(PolyEditor.camView4Mat, PolyEditor.camManipView4, PolyEditor.view4SpriteData, PolyEditor.view4Sprite, 4)

	drawOrthoViewRig(PolyEditor.camView4Mat, PolyEditor.camManipView4, PolyEditor.view4SpriteData, PolyEditor.view4Sprite, 4)

	if PolyEditor.mouseInViewport == 4 then
		updateRigTools()
	end

	drawSprite2D(PolyEditor.view4Sprite)

	drawSprite2D(PolyEditor.viewLabelSprites[4])
end

function currentLayerPivotPos()
	return _layerPivotPos(PolyEditor.currentLayerId)
end

function worldToViewport(vertPos, viewSpriteData)
	local viewPos = worldToScreen(vertPos, getCameraViewProjectionMatrix())

	viewPos.x = viewPos.x * (PolyEditor.screenWidth / viewSpriteData.size.x)
	viewPos.y = viewPos.y * (PolyEditor.screenHeight / viewSpriteData.size.y)

	local screenPosX = viewSpriteData.position.x + (viewSpriteData.size.x * (-viewPos.x + 0.5))
	local screenPosY = viewSpriteData.position.y + (viewSpriteData.size.y * (viewPos.y + 0.5))

	local pos2d = vec2(screenPosX, screenPosY)

	return pos2d
end

function pickVertex(vertPos, viewSpriteData)
	local pos2d = worldToViewport(vertPos, viewSpriteData)

	local distToVert2d = vec2Length(pos2d - PolyEditor.mousePos)

	return distToVert2d
end

function clearSelectionWithUndo()
	setSelectedVerts({})

	storeUndo()
end

-- Clear the vertex selection on every selected layer except keepLayerId.
function _clearVertSelectionOnOtherLayers(keepLayerId)
	for i = 1, #PolyEditor.selectedLayers do
		local layerId = PolyEditor.selectedLayers[i]
		if layerId ~= keepLayerId then
			setSelectedVertsForLayer(layerId, {})
		end
	end
end

function selectPickedElementsModel()
	-- picks apply to whichever selected layer the cursor actually hit
	local layerId = PolyEditor.pickedLayerId or PolyEditor.currentLayerId

	local sel = {}
	local store = PolyEditor.selectedVertsByLayer[layerId]
	if store ~= nil then
		for i = 1, #store.verts do
			sel[i] = store.verts[i]
		end
	end

	if PolyEditor.shiftKeyDown == true then
		if PolyEditor.pickedQuad ~= -1 then
			sel[#sel + 1] = ((PolyEditor.pickedQuad - 1) * 4) + 1
			sel[#sel + 1] = ((PolyEditor.pickedQuad - 1) * 4) + 2
			sel[#sel + 1] = ((PolyEditor.pickedQuad - 1) * 4) + 3
			sel[#sel + 1] = ((PolyEditor.pickedQuad - 1) * 4) + 4
		elseif #PolyEditor.pickedVerts > 0 then
			for i = 1, #PolyEditor.pickedVerts do
				sel[#sel + 1] = PolyEditor.pickedVerts[i]
			end
		end

		setSelectedVertsForLayer(layerId, sel)
		storeUndo({layerId})

		return
	end

	if PolyEditor.ctrlKeyDown == true then
		local deselect = {}

		if PolyEditor.pickedQuad ~= -1 then
			deselect[1] = ((PolyEditor.pickedQuad - 1) * 4) + 1
			deselect[2] = ((PolyEditor.pickedQuad - 1) * 4) + 2
			deselect[3] = ((PolyEditor.pickedQuad - 1) * 4) + 3
			deselect[4] = ((PolyEditor.pickedQuad - 1) * 4) + 4
		elseif #PolyEditor.pickedVerts > 0 then
			for i = 1, #PolyEditor.pickedVerts do
				deselect[i] = PolyEditor.pickedVerts[i]
			end
		end

		-- also deselect any vertices coincident with the picked ones: store.verts may hold a
		-- different-but-overlapping id at the same position than the one actually picked.
		local removeById = {}
		for i = 1, #deselect do
			removeById[deselect[i]] = true
		end

		local meshData = PolyEditor.meshes[layerId]
		if meshData ~= nil then
			local expanded = PolyUtil.getAllOverlappingVerts(meshData.mesh, deselect)
			for i = 1, #expanded do
				removeById[expanded[i]] = true
			end
		end

		local result = {}
		for i = 1, #sel do
			local vertId = sel[i]

			if removeById[vertId] == nil then
				result[#result + 1] = vertId
			end
		end

		setSelectedVertsForLayer(layerId, result)
		storeUndo({layerId})

		return
	end

	sel = {}

	if PolyEditor.pickedQuad ~= -1 then
		sel[1] = ((PolyEditor.pickedQuad - 1) * 4) + 1
		sel[2] = ((PolyEditor.pickedQuad - 1) * 4) + 2
		sel[3] = ((PolyEditor.pickedQuad - 1) * 4) + 3
		sel[4] = ((PolyEditor.pickedQuad - 1) * 4) + 4
	elseif #PolyEditor.pickedVerts > 0 then
		for i = 1, #PolyEditor.pickedVerts do
			sel[i] = PolyEditor.pickedVerts[i]
		end
	end

	-- plain click selects on the picked layer only, clearing the others
	_clearVertSelectionOnOtherLayers(layerId)
	setSelectedVertsForLayer(layerId, sel)
	storeUndo(PolyEditor.selectedLayers)
end

function selectPickedElementsUV()
	local sel = {}
	for i = 1, #PolyEditor.selectedUVs do
		sel[i] = PolyEditor.selectedUVs[i]
	end
	PolyEditor.selectedUVs = {}

	if PolyEditor.shiftKeyDown == true then
		if PolyEditor.pickedUVQuad ~= -1 then
			sel[#sel + 1] = ((PolyEditor.pickedUVQuad - 1) * 4) + 1
			sel[#sel + 1] = ((PolyEditor.pickedUVQuad - 1) * 4) + 2
			sel[#sel + 1] = ((PolyEditor.pickedUVQuad - 1) * 4) + 3
			sel[#sel + 1] = ((PolyEditor.pickedUVQuad - 1) * 4) + 4
		elseif PolyEditor.pickedUVId ~= -1 then
			sel[#sel + 1] = PolyEditor.pickedUVId
		end

		setSelectedUVs(sel)
		storeUndo()

		return
	end

	if PolyEditor.ctrlKeyDown == true then
		local deselect = {}

		if PolyEditor.pickedUVQuad ~= -1 then
			deselect[1] = ((PolyEditor.pickedUVQuad - 1) * 4) + 1
			deselect[2] = ((PolyEditor.pickedUVQuad - 1) * 4) + 2
			deselect[3] = ((PolyEditor.pickedUVQuad - 1) * 4) + 3
			deselect[4] = ((PolyEditor.pickedUVQuad - 1) * 4) + 4
		elseif PolyEditor.pickedUVId ~= -1 then
			deselect[1] = PolyEditor.pickedUVId
		end

		local result = {}
		for i = 1, #sel do
			local vertId = sel[i]

			local toDeselect = false
			for j = 1, #deselect do
				if deselect[j] == vertId then
					toDeselect = true

					break
				end
			end

			if toDeselect == false then
				result[#result + 1] = vertId
			end
		end

		setSelectedUVs(result)
		storeUndo()

		return
	end

	sel = {}

	if PolyEditor.pickedUVQuad ~= -1 then
		sel[1] = ((PolyEditor.pickedUVQuad - 1) * 4) + 1
		sel[2] = ((PolyEditor.pickedUVQuad - 1) * 4) + 2
		sel[3] = ((PolyEditor.pickedUVQuad - 1) * 4) + 3
		sel[4] = ((PolyEditor.pickedUVQuad - 1) * 4) + 4
	elseif PolyEditor.pickedUVId ~= -1 then
		sel[1] = PolyEditor.pickedUVId
	end

	setSelectedUVs(sel)
	storeUndo()
end

function updateMeshPicking(cameraMat, camManip, viewSpriteData, ortho)
	if UILib.isHovered() == true then
		return
	end

	if PolyEditor.mouseDown ~= -1 then
		return
	end

	if PolyEditor.pickingVertsEnabled == false and PolyEditor.pickingQuadsEnabled == false then
		return
	end

	PolyEditor.pickedVerts = {}
	PolyEditor.hoveredVerts = {}
	PolyEditor.pickedQuad = -1
	PolyEditor.pickedLayerId = PolyEditor.currentLayerId

	if PolyEditor.manipulatingCamera == true or
		PolyEditor.draggingVerts == true then

		return
	end

	local fov = PolyEditor.camFov
	if ortho == true then
		fov = - camManip.focusDist
	end

	local mouseInViewport = viewportToScreen(PolyEditor.mousePos, viewSpriteData, ortho)
	local ray = screenRay(mouseInViewport, fov, cameraMat, ortho)

	local pixelDistThreshold = 4.0

	local closestVertDist = 9999.0
	local closestVertId = -1
	local closestVertLayerId = -1

	local closestQuadDist = 9999.0
	local closestQuad = -1
	local closestQuadLayerId = -1

	-- pick the globally-closest element across every selected layer
	_syncSelectedLayerCaches()

	for layerIt = 1, #PolyEditor.selectedLayers do
		local layerId = PolyEditor.selectedLayers[layerIt]
		local meshData = PolyEditor.meshes[layerId]

		if meshData ~= nil and meshData.cache ~= nil then
			local quads = meshData.cache.quads
			local pivot = _layerPivotPos(layerId)

			for i = 1, #quads / 4 do
				local vertPos1 = quads[((i - 1) * 4) + 1] + pivot
				local vertPos2 = quads[((i - 1) * 4) + 2] + pivot
				local vertPos3 = quads[((i - 1) * 4) + 3] + pivot
				local vertPos4 = quads[((i - 1) * 4) + 4] + pivot

				local vertDistToMouse = pickVertex(vertPos1, viewSpriteData)

				local pickedVert = 1

				if vertDistToMouse > pixelDistThreshold then
					vertDistToMouse = pickVertex(vertPos2, viewSpriteData)
					pickedVert = 2
				end

				if vertDistToMouse > pixelDistThreshold then
					vertDistToMouse = pickVertex(vertPos3, viewSpriteData)
					pickedVert = 3
				end

				if vertDistToMouse > pixelDistThreshold then
					vertDistToMouse = pickVertex(vertPos4, viewSpriteData)
					pickedVert = 4
				end

				if vertDistToMouse < pixelDistThreshold then
					local pickedVertexId = ((i - 1) * 4) + pickedVert

					-- hoveredVerts is consumed against the active-layer selection only
					if layerId == PolyEditor.currentLayerId then
						PolyEditor.hoveredVerts[#PolyEditor.hoveredVerts + 1] = pickedVertexId
					end

					local vertexPos = quads[pickedVertexId] + pivot

					local dist = vec3Length(vertexPos - cameraMat.position)

					if dist < closestVertDist then
						closestVertDist = dist
						closestVertId = pickedVertexId
						closestVertLayerId = layerId
					end
				else
					local backfaceCull = true
					local intersectDist = intersectQuad(ray.origin, ray.direction, vertPos1, vertPos2, vertPos3, vertPos4, backfaceCull)

					if intersectDist > -1.0 and intersectDist < closestQuadDist then
						closestQuad = i
						closestQuadLayerId = layerId

						closestQuadDist = intersectDist
					end
				end
			end
		end
	end

	PolyEditor.pickedQuad = closestQuad

	if PolyEditor.pickingQuadsEnabled == false then
		PolyEditor.pickedQuad = -1
		closestQuadLayerId = -1
	end

	if PolyEditor.pickingVertsEnabled == false then
		closestVertId = -1
		closestVertLayerId = -1
	end

	local pickedLayerId = PolyEditor.currentLayerId

	if ortho == true then
		-- vertex sees through quad
		if closestVertId > -1 then
			PolyEditor.pickedVerts[1] = closestVertId
			pickedLayerId = closestVertLayerId
			PolyEditor.pickedQuad = -1
		elseif PolyEditor.pickedQuad ~= -1 then
			pickedLayerId = closestQuadLayerId
		end
	else
		if PolyEditor.pickedQuad == -1 and closestVertId > -1 then
			PolyEditor.pickedVerts[1] = closestVertId
			pickedLayerId = closestVertLayerId
		elseif PolyEditor.pickedQuad > -1 and closestVertId > -1 then
			if closestVertDist < closestQuadDist then
				PolyEditor.pickedVerts[1] = closestVertId
				pickedLayerId = closestVertLayerId
				PolyEditor.pickedQuad = -1
			else
				closestVertId = -1
				pickedLayerId = closestQuadLayerId
			end
		elseif PolyEditor.pickedQuad > -1 then
			pickedLayerId = closestQuadLayerId
		end
	end

	if PolyEditor.pickedQuad ~= -1 then
		PolyEditor.pickedVerts = {}
		PolyEditor.pickedVerts[1] = ((PolyEditor.pickedQuad - 1) * 4) + 1
		PolyEditor.pickedVerts[2] = ((PolyEditor.pickedQuad - 1) * 4) + 2
		PolyEditor.pickedVerts[3] = ((PolyEditor.pickedQuad - 1) * 4) + 3
		PolyEditor.pickedVerts[4] = ((PolyEditor.pickedQuad - 1) * 4) + 4
	end

	PolyEditor.pickedLayerId = pickedLayerId

	local pickedMesh = PolyEditor.meshes[pickedLayerId]

	PolyEditor.pickedVertsByHash = {}
	if pickedMesh ~= nil then
		for i = 1, #PolyEditor.pickedVerts do
			local vertId = PolyEditor.pickedVerts[i]
			local hash = getVertexHash(pickedMesh.mesh, vertId)
			PolyEditor.pickedVertsByHash[hash] = vertId
		end
	end

	if PolyEditor.mode == TEXTURE_MODE then
		highlightUVEditor()
	end
end

function drawVertices(viewId)
	if PolyEditor.manipulatingCamera == true then
		return
	end

	if PolyEditor.dragToolEngaged == true then
		return
	end

	if PolyEditorUI.colorPicker.visible == true then
		return
	end

	local vertexColor = Config.uiColors.polyEditorVertex
	local pickColor = Config.uiColors.polyEditorVertexHighlight
	local selectColor = Config.uiColors.polyEditorVertexSelected
	local selectInactiveColor = Config.uiColors.polyEditorVertexSelectedInactive

	if viewId ~= 2 then
		setDepthEnabled(false)
	end

	local baseSize = PolyEditor.vertexSize * 1.5
	local bigSize = baseSize + 4
	local baseSizeVec = vec2(baseSize, baseSize)
	local bigSizeVec = vec2(bigSize, bigSize)

	local pickedLayerId = PolyEditor.pickedLayerId or PolyEditor.currentLayerId

	-- draw the vertex handles of every selected layer at once; the active layer's
	-- selected handles use the bright color, the others a dimmer one.
	if PolyEditor.mouseInViewport == viewId then
		local normalVerts = {}
		local pickVerts = {}
		local selectBigVertsActive = {}
		local selectVertsActive = {}
		local selectBigVertsInactive = {}
		local selectVertsInactive = {}

		for layerIt = 1, #PolyEditor.selectedLayers do
			local layerId = PolyEditor.selectedLayers[layerIt]
			local meshData = PolyEditor.meshes[layerId]
			local cache = _ensureLayerCache(layerId)

			if meshData ~= nil and cache ~= nil then
				local mesh = meshData.mesh
				local quads = cache.quads
				local pivotPos = _layerPivotPos(layerId)
				local isActive = (layerId == PolyEditor.currentLayerId)
				local store = PolyEditor.selectedVertsByLayer[layerId]
				local selByHash = (store ~= nil and store.byHash) or {}

				local selBig = isActive and selectBigVertsActive or selectBigVertsInactive
				local selReg = isActive and selectVertsActive or selectVertsInactive

				local vertsDrawn = {}
				for vertId = 1, #quads do
					local vertHash = getVertexHash(mesh, vertId)
					if vertsDrawn[vertHash] == nil then
						vertsDrawn[vertHash] = 1
						local pos = quads[vertId] + pivotPos

						if selByHash[vertHash] ~= nil then
							selBig[#selBig + 1] = pos
							if PolyEditor.pickingSelection == true then
								pickVerts[#pickVerts + 1] = pos
							else
								selReg[#selReg + 1] = pos
							end
						elseif layerId == pickedLayerId and PolyEditor.pickedVertsByHash[vertHash] ~= nil then
							pickVerts[#pickVerts + 1] = pos
						else
							normalVerts[#normalVerts + 1] = pos
						end
					end
				end
			end
		end

		if #selectBigVertsInactive > 0 then
			drawPoints(selectBigVertsInactive, selectInactiveColor, bigSizeVec)
		end
		if #selectBigVertsActive > 0 then
			drawPoints(selectBigVertsActive, selectColor, bigSizeVec)
		end
		if #normalVerts > 0 then
			drawPoints(normalVerts, vertexColor, baseSizeVec)
		end
		if #selectVertsInactive > 0 then
			drawPoints(selectVertsInactive, selectInactiveColor, baseSizeVec)
		end
		if #selectVertsActive > 0 then
			drawPoints(selectVertsActive, selectColor, baseSizeVec)
		end
		if #pickVerts > 0 then
			drawPoints(pickVerts, pickColor, baseSizeVec)
		end
	end

	setDepthEnabled(false)

	-- selected-vertex overlay (drawn on top with depth off), per layer
	local overlayActive = {}
	local overlayInactive = {}

	for layerIt = 1, #PolyEditor.selectedLayers do
		local layerId = PolyEditor.selectedLayers[layerIt]
		local meshData = PolyEditor.meshes[layerId]
		local cache = _ensureLayerCache(layerId)

		if meshData ~= nil and cache ~= nil then
			local mesh = meshData.mesh
			local quads = cache.quads
			local pivotPos = _layerPivotPos(layerId)
			local isActive = (layerId == PolyEditor.currentLayerId)
			local store = PolyEditor.selectedVertsByLayer[layerId]
			local selByHash = (store ~= nil and store.byHash) or {}

			local overlay = isActive and overlayActive or overlayInactive

			local vertsDrawn = {}
			for hash, vertId in pairs(selByHash) do
				local vertHash = getVertexHash(mesh, vertId)
				if vertsDrawn[vertHash] == nil then
					vertsDrawn[vertHash] = 1
					local vertPos = quads[vertId]
					if vertPos ~= nil then
						overlay[#overlay + 1] = vertPos + pivotPos
					end
				end
			end
		end
	end

	if PolyEditor.pickingSelection == true then
		-- while actively rubber-banding, selected verts show in the pick color
		local combined = {}
		for i = 1, #overlayInactive do combined[#combined + 1] = overlayInactive[i] end
		for i = 1, #overlayActive do combined[#combined + 1] = overlayActive[i] end
		if #combined > 0 then
			drawPoints(combined, pickColor, baseSizeVec)
		end
	else
		if #overlayInactive > 0 then
			drawPoints(overlayInactive, selectInactiveColor, baseSizeVec)
		end
		if #overlayActive > 0 then
			drawPoints(overlayActive, selectColor, baseSizeVec)
		end
	end

	-- picked-vertex overlay only for the layer under the cursor
	if PolyEditor.pickingSelection == false then
		local meshData = PolyEditor.meshes[pickedLayerId]
		local cache = _ensureLayerCache(pickedLayerId)
		if meshData ~= nil and cache ~= nil then
			local mesh = meshData.mesh
			local quads = cache.quads
			local pivotPos = _layerPivotPos(pickedLayerId)
			local pickedOverlayVerts = {}
			local vertsDrawn = {}

			for i = 1, #PolyEditor.pickedVerts do
				local vertId = PolyEditor.pickedVerts[i]
				local vertHash = getVertexHash(mesh, vertId)
				if vertsDrawn[vertHash] == nil then
					vertsDrawn[vertHash] = 1
					local vertPos = quads[vertId]
					if vertPos ~= nil then
						pickedOverlayVerts[#pickedOverlayVerts + 1] = vertPos + pivotPos
					end
				end
			end

			if #pickedOverlayVerts > 0 then
				drawPoints(pickedOverlayVerts, pickColor, baseSizeVec)
			end
		end
	end

	setDepthEnabled(true)
end

function drawUVVertices()
	local vertexColor = Config.uiColors.polyEditorVertex
	local pickColor = Config.uiColors.polyEditorVertexHighlight
	local selectColor = Config.uiColors.polyEditorVertexSelected

	local selVerts = {}
	for i = 1, #PolyEditor.selectedUVs do
		selVerts[PolyEditor.selectedUVs[i]] = 1
	end

	local drawUVOnTop = PolyEditor.uvDrawDepth
	local baseSize = PolyEditor.vertexSize * 2.0
	local bigSize = baseSize + 4
	local baseSizeVec = vec2(baseSize, baseSize)
	local bigSizeVec = vec2(bigSize, bigSize)

	if PolyEditor.mouseInViewport == 3 then
		local normalVerts = {}
		local selectBigVerts = {}
		local selectVerts = {}
		local pickVerts = {}

		for quadId, v in pairs(PolyEditor.editingUVQuads) do
			for i = 1, 4 do
				local vId = ((quadId - 1) * 4) + i
				local uv = PolyEditor.currentMeshUVs[vId]
				local pos = vec3(uv.x * PolyEditor.uvDispW, uv.y * PolyEditor.uvDispH, drawUVOnTop)

				if selVerts[vId] == 1 then
					selectBigVerts[#selectBigVerts + 1] = pos
					if PolyEditor.pickingSelection == true then
						pickVerts[#pickVerts + 1] = pos
					else
						selectVerts[#selectVerts + 1] = pos
					end
				else
					normalVerts[#normalVerts + 1] = pos
				end
			end
		end

		if #selectBigVerts > 0 then
			drawPoints(selectBigVerts, selectColor, bigSizeVec)
		end
		if #normalVerts > 0 then
			drawPoints(normalVerts, vertexColor, baseSizeVec)
		end
		if #selectVerts > 0 then
			drawPoints(selectVerts, selectColor, baseSizeVec)
		end
		if #pickVerts > 0 then
			drawPoints(pickVerts, pickColor, baseSizeVec)
		end
	else
		local selectedOnlyVerts = {}
		for k, v in pairs(selVerts) do
			local uv = PolyEditor.currentMeshUVs[k]
			selectedOnlyVerts[#selectedOnlyVerts + 1] = vec3(uv.x * PolyEditor.uvDispW, uv.y * PolyEditor.uvDispH, drawUVOnTop + 0.1)
		end
		if #selectedOnlyVerts > 0 then
			drawPoints(selectedOnlyVerts, selectColor, baseSizeVec)
		end
	end

	if PolyEditor.mouseInViewport == 3 then
		if PolyEditor.pickedUVQuad ~= -1 then
			local pickedQuadVerts = {}
			for i = 1, 4 do
				local uv = PolyEditor.currentMeshUVs[((PolyEditor.pickedUVQuad - 1) * 4) + i]
				pickedQuadVerts[#pickedQuadVerts + 1] = vec3(uv.x * PolyEditor.uvDispW, uv.y * PolyEditor.uvDispH, drawUVOnTop + 0.1)
			end
			drawPoints(pickedQuadVerts, pickColor, baseSizeVec)
		elseif PolyEditor.pickedUVId ~= -1 then
			local uv = PolyEditor.currentMeshUVs[PolyEditor.pickedUVId]
			drawPoints(
				{vec3(uv.x * PolyEditor.uvDispW, uv.y * PolyEditor.uvDispH, drawUVOnTop + 0.1)},
				pickColor,
				baseSizeVec
			)
		end
	end
end

function PolyEditor.setShadingType(shadingType)
	PolyEditor.shadingType = shadingType

	if PolyEditor.mode == MODEL_MODE or PolyEditor.mode == RIG_MODE then
		PolyEditor.modelModeShadingType = shadingType
	else
		PolyEditor.textureModeShadingType = shadingType
	end

	PolyEditorUI.shadingTypeChanged()

	Utils.setAppVar("polyEditor_shadingType", shadingType)

	invokeProgram(MAIN_PROGRAMID, "refreshModelVisualizer")
end

function _abortCurrentTool()
	if #PolyEditor.textureData > 0 then
		setTexture(PolyEditor.currentPageId, PolyEditor.textureData)
	end

	if PolyEditor.newPrimitiveMesh ~= nil then
		destroy(PolyEditor.newPrimitiveMesh)
		PolyEditor.newPrimitiveMesh = nil
	end

	if PolyEditor.tmpToolMesh ~= nil then
		destroy(PolyEditor.tmpToolMesh)
		PolyEditor.tmpToolMesh = nil
	end
	
	if hasActiveTool() == true then
		setActiveTool("select")

		if PolyEditor.mode == TEXTURE_MODE then
			PolyEditor.pencilToolStart()
		elseif PolyEditor.mode == RIG_MODE then
			PolyEditor.movePivotToolStart()
		end
	else
		if #PolyEditor.selectedVerts > 0 then
			clearSelectionWithUndo()
		end
	end

	PolyEditor.pickingVertsEnabled = true
	PolyEditor.pickingQuadsEnabled = true

	PolyEditor.tmpMirrorVertsId = {}
	PolyEditor.tmpMirrorVerts = {}

	PolyEditor.tmpSplitSelectedQuadPoints = {}
	PolyEditor.vertIdToRemove = {}
end

function shortcutEsc()
	_abortCurrentTool()

	UILib.closeMenu()
end

function shortcutSelectAll()
	if PolyEditor.mode ~= TEXTURE_MODE then
		-- select every vert on every selected layer (not just the active one)
		_syncSelectedLayerCaches()

		for li = 1, #PolyEditor.selectedLayers do
			local layerId = PolyEditor.selectedLayers[li]
			local meshData = PolyEditor.meshes[layerId]
			
			if meshData ~= nil and meshData.cache ~= nil then
				local newSel = {}
				for i = 1, #meshData.cache.quads do
					newSel[i] = i
				end
				setSelectedVertsForLayer(layerId, newSel)
			end
		end
	else
		-- UV editor is active-layer-only (locked follow-up); keep single-layer behavior
		local newSel = {}
		for i = 1, #PolyEditor.currentMeshQuads do
			newSel[i] = i
		end

		if PolyEditor.mouseInViewport == 3 then
			setSelectedUVs(PolyEditor.selectedVerts)
		else
			setSelectedVerts(newSel)
			refreshUVEditor()
			setSelectedUVs(newSel)
		end
	end
end

-- Restore a single layer's mesh + per-layer selection from a serialized meshData blob.
-- Pure per-layer work: no mode switch, no texture-bytes, no UI — the caller owns those.
function _restoreLayerFromMeshData(layerId, meshData, sel, uvAnimFrames)
	if meshData == nil then
		return
	end

	local layer = PolyEditor.meshes[layerId]
	if layer == nil then
		-- the slot was destroyed (e.g. an empty layer cleaned up on a selection change).
		-- resurrect it only when this entry actually carries geometry; an empty restore
		-- should leave the slot destroyed.
		if #meshData.vertices == 0 then
			return
		end
		PolyEditor.meshes[layerId] = _newLayerMeshData()
		layer = PolyEditor.meshes[layerId]
	end

	local mesh = layer.mesh

	local quads = {}
	local normals = {}
	local uvs = {}
	local colors = {}

	for i = 1, #meshData.vertices do
		local vertData = meshData.vertices[i]
		local normalData = meshData.normals[i]
		local uvData = meshData.uvs[i]
		local vertColorData = meshData.vertexColors[i]

		quads[i] = vec3(vertData[1], vertData[2], vertData[3])
		normals[i] = vec3(normalData[1], normalData[2], normalData[3])
		uvs[i] = vec2(uvData[1], uvData[2])
		colors[i] = vec4(vertColorData[1], vertColorData[2], vertColorData[3], vertColorData[4])
	end

	local uvRegionPos = vec2(meshData.uvRegionPos[1], meshData.uvRegionPos[2])
	local uvRegionSize = vec2(meshData.uvRegionSize[1], meshData.uvRegionSize[2])

	setMeshVertices(mesh, VERT_POSITION, quads)
	setMeshVertices(mesh, VERT_NORMAL, normals)
	computeNormals(mesh)
	setMeshVertices(mesh, VERT_COLORBASE, colors)
	setMeshVertices(mesh, VERT_UV, uvs)
	setMeshTexture(mesh, PolyEditor.currentPageId, uvRegionPos, uvRegionSize)
	setMeshSmoothNormals(mesh, meshData.smoothNormals)

	for f = 0, 8 do
		clearMeshUVAnimFrame(mesh, f)
	end
	if uvAnimFrames ~= nil then
		for f, frameData in pairs(uvAnimFrames) do
			local indices = {}
			local frameUVs = {}
			for vertIdx, uv in pairs(frameData) do
				indices[#indices + 1] = vertIdx
				frameUVs[#frameUVs + 1] = uv
			end
			setMeshUVAnimFrame(mesh, f, indices, frameUVs)
		end
	end

	if meshData.pivotPos ~= nil and PolyEditor.hierarchyByLayerId[layerId] ~= nil then
		PolyEditor.hierarchyByLayerId[layerId].position =
			vec3(meshData.pivotPos[1], meshData.pivotPos[2], meshData.pivotPos[3])
	end

	-- re-read the layer's cache off the GPU so it matches the restored mesh, then
	-- restore that layer's vertex selection (hashed against the rebuilt mesh)
	_loadLayerCache(layerId)
	setSelectedVertsForLayer(layerId, sel or {})
end

-- Restore `layerId` from the most recent undo entry at or below `fromIndex` that carries it.
-- Used by undo to roll back sibling layers a cross-layer edit changed but the target entry lacks.
function _restoreLayerFromHistory(layerId, fromIndex)
	for i = fromIndex, 1, -1 do
		local entry = PolyEditor.undoStack[i]
		if entry ~= nil and entry.meshDataByLayer ~= nil
			and entry.meshDataByLayer[layerId] ~= nil then
			local sel = entry.selectedVertsByLayer and entry.selectedVertsByLayer[layerId]
			local uvAnim = entry.uvAnimFramesByLayer and entry.uvAnimFramesByLayer[layerId]
			_restoreLayerFromMeshData(layerId, entry.meshDataByLayer[layerId], sel, uvAnim)
			return true
		end
	end
	return false
end

function restoreUndoEntry(undoEntry, mode, extraLayers, walkFromIndex)
	-- back-compat: an entry captured before multi-layer undo only has .meshData
	if undoEntry.meshDataByLayer == nil then
		_restoreUndoEntryLegacy(undoEntry, mode)
		return
	end

	-- stash the outgoing active layer before switching to the undo entry's layer
	_syncActiveCacheFromGlobals()

	PolyEditor.currentLayerId = undoEntry.layerId
	if PolyEditor.meshes[PolyEditor.currentLayerId] ~= nil then
		PolyEditor.currentMesh = PolyEditor.meshes[PolyEditor.currentLayerId].mesh
	end
	setSelectedLayers(undoEntry.selectedLayers or {undoEntry.layerId})
	PolyEditorUI.layerSelected(PolyEditor.currentLayerId)
	_syncHierarchySelectionFlags()

	PolyEditor.editingUVQuads = {}
	PolyEditor.highlightUVs = {}

	-- page-level restore (texture + mode) done once, before pushing geometry
	if mode == MODEL_MODE then
		PolyEditor.switchToModel()
	elseif mode == TEXTURE_MODE then
		setTextureBytes(PolyEditor.currentPageId, undoEntry.texture)
		PolyEditor.textureData = getTexture(PolyEditor.currentPageId)
		PolyEditor.switchToTexture()
	else
		PolyEditor.switchToRig()
	end

	-- restore every affected layer's mesh + per-layer selection
	for layerId, meshData in pairs(undoEntry.meshDataByLayer) do
		local sel = undoEntry.selectedVertsByLayer and undoEntry.selectedVertsByLayer[layerId]
		local uvAnim = undoEntry.uvAnimFramesByLayer and undoEntry.uvAnimFramesByLayer[layerId]
		_restoreLayerFromMeshData(layerId, meshData, sel, uvAnim)
	end

	-- back-fill: the undone edit may have changed layers this target entry doesn't carry
	-- (e.g. the previous snapshot was a single-layer selection). Pull each such layer's
	-- pre-edit state from the newest earlier entry that has it, so all N layers roll back.
	if extraLayers ~= nil then
		for i = 1, #extraLayers do
			local layerId = extraLayers[i]
			if undoEntry.meshDataByLayer[layerId] == nil then
				_restoreLayerFromHistory(layerId, walkFromIndex or PolyEditor.undoStackId)
			end
		end
	end

	_hierarchyChanged()

	-- re-point the globals at the (freshly reloaded) active layer's cache/store
	_bindActiveLayerGlobals(PolyEditor.currentLayerId)

	PolyEditor.uvRegionPos = getMeshUVRegionPos(PolyEditor.currentMesh)
	PolyEditor.uvRegionSize = getMeshUVRegionSize(PolyEditor.currentMesh)

	PolyEditor.uvAnimFrame = undoEntry.uvAnimFrame or 0

	if PolyEditor.mode == TEXTURE_MODE then
		refreshUVEditor()
	end
end

function _restoreUndoEntryLegacy(undoEntry, mode)
	if undoEntry.meshData == nil then
		return
	end

	-- stash the outgoing active layer before switching to the undo entry's layer
	_syncActiveCacheFromGlobals()

	PolyEditor.currentLayerId = undoEntry.layerId
	if PolyEditor.meshes[PolyEditor.currentLayerId] ~= nil then
		PolyEditor.currentMesh = PolyEditor.meshes[PolyEditor.currentLayerId].mesh
	end
	setSelectedLayers(undoEntry.selectedLayers or {undoEntry.layerId})
	PolyEditorUI.layerSelected(PolyEditor.currentLayerId)
	_syncHierarchySelectionFlags()

	setSelectedVerts({})
	setSelectedUVs({})

	PolyEditor.editingUVQuads = {}
	PolyEditor.highlightUVs = {}

	PolyEditor.currentMeshQuads = {}
	PolyEditor.currentMeshNormals = {}
	PolyEditor.currentMeshUVs = {}
	PolyEditor.currentMeshVertexColors = {}

	for i = 1, #undoEntry.meshData.vertices do
		local vertData = undoEntry.meshData.vertices[i]
		local normalData = undoEntry.meshData.normals[i]
		local uvData = undoEntry.meshData.uvs[i]
		local vertColorData = undoEntry.meshData.vertexColors[i]
		
		PolyEditor.currentMeshQuads[#PolyEditor.currentMeshQuads + 1] = vec3(vertData[1], vertData[2], vertData[3])
		PolyEditor.currentMeshNormals[#PolyEditor.currentMeshNormals + 1] = vec3(normalData[1], normalData[2], normalData[3])
		PolyEditor.currentMeshUVs[#PolyEditor.currentMeshUVs + 1] = vec2(uvData[1], uvData[2])
		PolyEditor.currentMeshVertexColors[#PolyEditor.currentMeshVertexColors + 1] = vec4(vertColorData[1], vertColorData[2], vertColorData[3], vertColorData[4])
	end

	PolyEditor.uvRegionPos.x = undoEntry.meshData.uvRegionPos[1]
	PolyEditor.uvRegionPos.y = undoEntry.meshData.uvRegionPos[2]

	PolyEditor.uvRegionSize.x = undoEntry.meshData.uvRegionSize[1]
	PolyEditor.uvRegionSize.y = undoEntry.meshData.uvRegionSize[2]

	if mode == MODEL_MODE then
		PolyEditor.switchToModel()
	elseif mode == TEXTURE_MODE then
		setTextureBytes(PolyEditor.currentPageId, undoEntry.texture)
		PolyEditor.textureData = getTexture(PolyEditor.currentPageId)

		PolyEditor.switchToTexture()
	else
		PolyEditor.switchToRig()
	end

	setMeshVertices(PolyEditor.currentMesh, VERT_POSITION, PolyEditor.currentMeshQuads)
	PolyEditor.currentMeshQuads = getMeshVertices(PolyEditor.currentMesh)

	setMeshVertices(PolyEditor.currentMesh, VERT_NORMAL, PolyEditor.currentMeshNormals)
	computeNormals(PolyEditor.currentMesh)
	PolyEditor.currentMeshNormals = getMeshVertices(PolyEditor.currentMesh, VERT_NORMAL)

	setMeshVertices(PolyEditor.currentMesh, VERT_COLORBASE, PolyEditor.currentMeshVertexColors)
	PolyEditor.currentMeshVertexColors = getMeshVertices(PolyEditor.currentMesh, VERT_COLORBASE)

	setMeshVertices(PolyEditor.currentMesh, VERT_UV, PolyEditor.currentMeshUVs)
	setMeshTexture(PolyEditor.currentMesh, PolyEditor.currentPageId, PolyEditor.uvRegionPos, PolyEditor.uvRegionSize)
	PolyEditor.currentMeshUVs = getMeshVertices(PolyEditor.currentMesh, VERT_UV)

	PolyEditor.uvRegionPos = getMeshUVRegionPos(PolyEditor.currentMesh)
	PolyEditor.uvRegionSize = getMeshUVRegionSize(PolyEditor.currentMesh)
	
	setMeshSmoothNormals(PolyEditor.currentMesh, undoEntry.meshData.smoothNormals)

	PolyEditor.uvAnimFrame = undoEntry.uvAnimFrame or 0
	for f = 0, 8 do
		clearMeshUVAnimFrame(PolyEditor.currentMesh, f)
	end
	if undoEntry.uvAnimFrames then
		for f, frameData in pairs(undoEntry.uvAnimFrames) do
			local indices = {}
			local uvs = {}
			for vertIdx, uv in pairs(frameData) do
				indices[#indices + 1] = vertIdx
				uvs[#uvs + 1] = uv
			end
			setMeshUVAnimFrame(PolyEditor.currentMesh, f, indices, uvs)
		end
	end

	if undoEntry.meshData.pivotPos ~= nil then
		local pos = vec3(undoEntry.meshData.pivotPos[1], undoEntry.meshData.pivotPos[2], undoEntry.meshData.pivotPos[3])
		PolyEditor.hierarchyByLayerId[PolyEditor.currentLayerId].position = pos
		_hierarchyChanged()
	end

	local sel = {}
	for i = 1, #undoEntry.selectedVerts do
		sel[i] = undoEntry.selectedVerts[i]
	end

	setSelectedVerts(sel)

	-- capture the rebuilt globals back into the active layer's cache/store so the
	-- per-layer state stays consistent for the next layer switch
	_syncActiveCacheFromGlobals()

	if PolyEditor.mode == TEXTURE_MODE then
		refreshUVEditor()
	end
end

function shortcutUndo()
	_undo()
end

function _undo()
	if getKeyDown(KEY_LEFT_SHIFT) == true then
		return
	end

	PolyEditorUI.colorPicker:setVisible(false)

	PolyEditor.undoStackId = PolyEditor.undoStackId - 1

	if PolyEditor.undoStackId < 1 then
		PolyEditor.undoStackId = 1

		return
	end

	local undoEntry = PolyEditor.undoStack[PolyEditor.undoStackId]

	-- the entry we're undoing (was the top of the stack); snapshot-after means we roll back
	-- to `undoEntry`, but that target may not carry every layer this edit changed
	local undoneEntry = PolyEditor.undoStack[PolyEditor.undoStackId + 1]

	local mode = undoneEntry.mode

	-- layers the undone edit touched; back-fill any the target entry lacks by walking further
	-- down the stack so a cross-layer edit reverts ALL its layers, not just the current one
	local extraLayers = undoneEntry.affectedLayerIds
	if extraLayers == nil and undoneEntry.layerId ~= nil then
		extraLayers = { undoneEntry.layerId }
	end

	restoreUndoEntry(undoEntry, mode, extraLayers, PolyEditor.undoStackId)
end

function shortcutRedo()
	_redo()
end

function _redo()
	PolyEditor.undoStackId = PolyEditor.undoStackId + 1

	local undoEntry = PolyEditor.undoStack[PolyEditor.undoStackId]

	if undoEntry == nil then
		PolyEditor.undoStackId = PolyEditor.undoStackId - 1

		return
	end

	local mode = undoEntry.mode

	restoreUndoEntry(undoEntry, mode)
end

function serializeMesh(layerId, vertsSelection)
	local mesh = PolyEditor.meshes[layerId]

	local meshData = nil

	if mesh ~= nil then
		local meshPtr = mesh.mesh

		local vertices = getMeshVertices(meshPtr)
		local normals = getMeshVertices(meshPtr, VERT_NORMAL)
		local smoothNormals = getMeshSmoothNormals(meshPtr)
		local uvs = getMeshVertices(meshPtr, VERT_UV)
		local vertexColors = getMeshVertices(meshPtr, VERT_COLORBASE)
		local uvRegionPos = getMeshUVRegionPos(meshPtr)
		local uvRegionSize = getMeshUVRegionSize(meshPtr)

		meshData = {}
		meshData.vertices = {}
		meshData.normals = {}
		meshData.uvs = {}
		meshData.vertexColors = {}

		local vertIds = {}
		if vertsSelection == nil or #vertsSelection == 0 then
			for i = 1, #vertices do
				vertIds[i] = i
			end
		else
			vertIds = {table.unpack(vertsSelection)}
		end

		for vertIt = 1, #vertIds do
			local vertId = vertIds[vertIt]
			
			local vert = vertices[vertId]
			local normal = normals[vertId]
			local uv = uvs[vertId]
			local color = vertexColors[vertId]
			
			meshData.vertices[vertId] = {[1] = vert.x, [2] = vert.y, [3] = vert.z}
			meshData.normals[vertId] = {[1] = normal.x, [2] = normal.y, [3] = normal.z}
			meshData.uvs[vertId] = {[1] = uv.x, [2] = uv.y}
			meshData.vertexColors[vertId] = {[1] = color.x, [2] = color.y, [3] = color.z, [4] = color.w}
		end

		meshData.uvRegionPos = {[1] = uvRegionPos.x, [2] = uvRegionPos.y}
		meshData.uvRegionSize = {[1] = uvRegionSize.x, [2] = uvRegionSize.y}

		meshData.smoothNormals = smoothNormals

		local pivotPos = _layerPivotPos(layerId)
		meshData.pivotPos = {[1] = pivotPos.x, [2] = pivotPos.y, [3] = pivotPos.z}
	end

	return meshData
end

-- storeUndo(affectedLayerIds): snapshot the given layers (default: just the active layer,
-- which keeps selection-only undos cheap). Every edit tool passes the layers it touched.
function storeUndo(affectedLayerIds)
	local undoEntry = {}

	undoEntry.mode = PolyEditor.mode
	undoEntry.layerId = PolyEditor.currentLayerId
	undoEntry.uvAnimFrame = PolyEditor.uvAnimFrame

	-- capture the active layer's live edits into its cache/store before serializing
	_syncActiveCacheFromGlobals()

	if affectedLayerIds == nil then
		affectedLayerIds = {PolyEditor.currentLayerId}
	end

	undoEntry.affectedLayerIds = {table.unpack(affectedLayerIds)}
	undoEntry.meshDataByLayer = {}
	undoEntry.uvAnimFramesByLayer = {}
	undoEntry.selectedVertsByLayer = {}

	for i = 1, #affectedLayerIds do
		local layerId = affectedLayerIds[i]
		local meshData = PolyEditor.meshes[layerId]
		if meshData ~= nil then
			undoEntry.meshDataByLayer[layerId] = serializeMesh(layerId)

			local frames = {}
			for f = 0, 8 do
				if hasMeshUVAnimFrame(meshData.mesh, f) then
					frames[f] = getMeshUVAnimFrame(meshData.mesh, f)
				end
			end
			undoEntry.uvAnimFramesByLayer[layerId] = frames

			local store = PolyEditor.selectedVertsByLayer[layerId]
			local selCopy = {}
			if store ~= nil then
				for j = 1, #store.verts do
					selCopy[j] = store.verts[j]
				end
			end
			undoEntry.selectedVertsByLayer[layerId] = selCopy
		end
	end

	undoEntry.texture = getTextureBytes(PolyEditor.currentPageId)

	undoEntry.selectedLayers = {table.unpack(PolyEditor.selectedLayers)}

	PolyEditor.undoStackId = PolyEditor.undoStackId + 1
	PolyEditor.undoStack[PolyEditor.undoStackId] = undoEntry

	for i = PolyEditor.undoStackId + 1, #PolyEditor.undoStack do
		PolyEditor.undoStack[i] = nil
	end

	if undoEntry.mode == TEXTURE_MODE then
		PolyEditor.textureLocked[PolyEditor.currentPageId] = true
	end

	if not PolyEditor.dirty then
		PolyEditor.dirty = true
		updateViewTitle()
	end

	_updateAssetPreview()
end

-- Delete the selected faces of a single layer, operating on that layer's cache/mesh/store.
-- Layers without a selection are skipped (no "delete everything" fallback) — cross-layer
-- delete only touches layers the user actually selected verts on.
function _deleteSelectedVertsInLayer(layerId)
	local meshData = PolyEditor.meshes[layerId]
	if meshData == nil then
		return false
	end

	local cache = _ensureLayerCache(layerId)
	local store = PolyEditor.selectedVertsByLayer[layerId]

	if cache == nil or #cache.quads == 0 then
		return false
	end
	if store == nil or #store.verts == 0 then
		return false
	end

	local mesh = meshData.mesh
	local vertsToDelete = {}
	local numVerts = 0

	local quadIds = {}
	if #store.verts % 4 == 0 then
		-- selected quads, we can delete the faces
		quadIds = PolyUtil.selectedVertsToQuads(mesh, store.overlapping)
	else
		-- selected non quads, delete connected
		local connectedVerts = getConnectedVertices(mesh, store.overlapping)
		quadIds = PolyUtil.selectedVertsToQuads(mesh, connectedVerts)
	end

	for quadId, v in pairs(quadIds) do
		for vIt = 1, 4 do
			local vId = ((quadId - 1) * 4) + vIt

			vertsToDelete[vId] = true
			numVerts = numVerts + 1
		end
	end

	if numVerts == 0 then
		return false
	end

	cache.quads = Utils.removeElements(cache.quads, vertsToDelete)
	cache.normals = Utils.removeElements(cache.normals, vertsToDelete)
	cache.uvs = Utils.removeElements(cache.uvs, vertsToDelete)
	cache.colors = Utils.removeElements(cache.colors, vertsToDelete)

	setSelectedVertsForLayer(layerId, {})

	if layerId == PolyEditor.currentLayerId then
		-- re-point globals at the rebuilt cache arrays before refreshLayer syncs from them
		_bindActiveLayerGlobals(layerId)
	end

	refreshLayer(layerId)

	return true
end

-- Delete selected faces across every selected layer. Records the affected layers on
-- PolyEditor.deleteAffectedLayerIds for the caller's storeUndo.
function _deleteSelectedVerts()
	_syncSelectedLayerCaches()

	local deleted = false
	local affected = {}

	for i = 1, #PolyEditor.selectedLayers do
		local layerId = PolyEditor.selectedLayers[i]
		if _deleteSelectedVertsInLayer(layerId) then
			deleted = true
			affected[#affected + 1] = layerId
		end
	end

	PolyEditor.deleteAffectedLayerIds = affected

	if deleted then
		PolyEditor.pickedVerts = {}
		PolyEditor.pickedVertsByHash = {}
		PolyEditor.pickedQuad = -1
	end

	return deleted
end

function shortcutDeleteSelected()
	if PolyEditor.mode == MODEL_MODE then
		local deleted = _deleteSelectedVerts()

		if deleted == true then
			storeUndo(PolyEditor.deleteAffectedLayerIds)
		end
	end
end

function shortcutCut()
	_cut()
end

-- does any currently-selected layer hold a vertex selection?
function _anyLayerHasSelection()
	for li = 1, #PolyEditor.selectedLayers do
		local store = PolyEditor.selectedVertsByLayer[PolyEditor.selectedLayers[li]]
		if store ~= nil and #store.verts > 0 then
			return true
		end
	end
	return false
end

-- Build a flattened clipboard from the selected verts of EVERY selected layer, stored in WORLD
-- space so relative positions across layers (each with its own pivot) are preserved. Paste then
-- drops the whole set onto the active layer, converting world -> active-local.
-- If nothing is selected anywhere, falls back to copying the whole active layer.
-- clipboard.vertices/normals/uvs/vertexColors are DENSE arrays keyed 1..N.
function _buildClipboardFromSelection()
	_syncSelectedLayerCaches()

	local clip = { vertices = {}, normals = {}, uvs = {}, vertexColors = {}, smoothNormals = {} }
	local idx = 0

	local function gather(layerId, vertIds)
		local meshData = PolyEditor.meshes[layerId]
		if meshData == nil then
			return
		end
		local cache = _ensureLayerCache(layerId)
		if cache == nil then
			return
		end
		local pivot = _layerPivotPos(layerId)

		-- sort ascending so a quad's 4 consecutive vertIds stay contiguous in the output
		local ids = {}
		for i = 1, #vertIds do
			ids[i] = vertIds[i]
		end
		table.sort(ids)

		for i = 1, #ids do
			local vId = ids[i]
			if cache.quads[vId] ~= nil then
				idx = idx + 1
				local p = cache.quads[vId] + pivot
				clip.vertices[idx] = { p.x, p.y, p.z }
				local n = cache.normals[vId] or vec3(0, 0, 0)
				clip.normals[idx] = { n.x, n.y, n.z }
				local uv = cache.uvs[vId] or vec2(0, 0)
				clip.uvs[idx] = { uv.x, uv.y }
				local c = cache.colors[vId] or vec4(255, 255, 255, 255)
				clip.vertexColors[idx] = { c.x, c.y, c.z, c.w }
			end
		end
	end

	if _anyLayerHasSelection() then
		for li = 1, #PolyEditor.selectedLayers do
			local layerId = PolyEditor.selectedLayers[li]
			local store = PolyEditor.selectedVertsByLayer[layerId]
			if store ~= nil and #store.verts > 0 then
				gather(layerId, store.verts)
			end
		end
	else
		-- nothing selected anywhere => copy the whole active layer
		local layerId = PolyEditor.currentLayerId
		local cache = _ensureLayerCache(layerId)
		if cache ~= nil then
			local all = {}
			for i = 1, #cache.quads do
				all[i] = i
			end
			gather(layerId, all)
		end
	end

	if idx == 0 then
		return nil
	end

	-- region taken from the active layer so paste's region-apply is a no-op on the paste target
	local activeMesh = PolyEditor.meshes[PolyEditor.currentLayerId].mesh
	local rp = getMeshUVRegionPos(activeMesh)
	local rs = getMeshUVRegionSize(activeMesh)
	clip.uvRegionPos = { rp.x, rp.y }
	clip.uvRegionSize = { rs.x, rs.y }

	return clip
end

function _cut()
	PolyEditor.clipboard = nil

	if PolyEditor.mode ~= MODEL_MODE then
		return
	end

	PolyEditor.clipboard = _buildClipboardFromSelection()
	if PolyEditor.clipboard == nil then
		return
	end

	if _anyLayerHasSelection() then
		-- delete the copied verts from every selected layer
		if _deleteSelectedVerts() then
			storeUndo(PolyEditor.deleteAffectedLayerIds)
		end
	else
		-- nothing was selected => we copied the whole active layer; cut it wholesale
		local layerId = PolyEditor.currentLayerId
		local all = {}
		for i = 1, #PolyEditor.currentMeshQuads do
			all[i] = i
		end
		setSelectedVertsForLayer(layerId, all)
		if _deleteSelectedVertsInLayer(layerId) then
			storeUndo({ layerId })
		end
	end
end

function shortcutCopy()
	_copy()
end

function _copy()
	PolyEditor.clipboard = nil

	if PolyEditor.mode == MODEL_MODE then
		PolyEditor.clipboard = _buildClipboardFromSelection()
	end
end

function shortcutPaste()
	_paste()	
end

function _paste()
	if PolyEditor.mode ~= MODEL_MODE or PolyEditor.clipboard == nil then
		return
	end

	local sel = {}

	-- clipboard verts are WORLD-space; convert to the active layer's local space on paste
	local activePivot = _layerPivotPos(PolyEditor.currentLayerId)

	local orderedIds = {}
	for vId, v in pairs(PolyEditor.clipboard.vertices) do
		orderedIds[#orderedIds + 1] = vId
	end

	table.sort(orderedIds)

	for i = 1, #orderedIds do
		local vId = orderedIds[i]

		local vertData = PolyEditor.clipboard.vertices[vId]
		local normalData = PolyEditor.clipboard.normals[vId]
		local uvData = PolyEditor.clipboard.uvs[vId]
		local vertColorData = PolyEditor.clipboard.vertexColors[vId]

		PolyEditor.currentMeshQuads[#PolyEditor.currentMeshQuads + 1] = vec3(vertData[1], vertData[2], vertData[3]) - activePivot
		PolyEditor.currentMeshNormals[#PolyEditor.currentMeshNormals + 1] = vec3(normalData[1], normalData[2], normalData[3])
		PolyEditor.currentMeshUVs[#PolyEditor.currentMeshUVs + 1] = vec2(uvData[1], uvData[2])
		PolyEditor.currentMeshVertexColors[#PolyEditor.currentMeshVertexColors + 1] = vec4(vertColorData[1], vertColorData[2], vertColorData[3], vertColorData[4])
		
		sel[#sel + 1] = #PolyEditor.currentMeshQuads
	end

	if PolyEditor.clipboard.uvRegionPos ~= nil then
		PolyEditor.uvRegionPos.x = PolyEditor.clipboard.uvRegionPos[1]
		PolyEditor.uvRegionPos.y = PolyEditor.clipboard.uvRegionPos[2]

		PolyEditor.uvRegionSize.x = PolyEditor.clipboard.uvRegionSize[1]
		PolyEditor.uvRegionSize.y = PolyEditor.clipboard.uvRegionSize[2]

		PolyEditor.uvRegionPos = getMeshUVRegionPos(PolyEditor.currentMesh)
		PolyEditor.uvRegionSize = getMeshUVRegionSize(PolyEditor.currentMesh)
	end

	setMeshVertices(PolyEditor.currentMesh, VERT_POSITION, PolyEditor.currentMeshQuads)
	PolyEditor.currentMeshQuads = getMeshVertices(PolyEditor.currentMesh)

	setMeshVertices(PolyEditor.currentMesh, VERT_NORMAL, PolyEditor.currentMeshNormals)
	computeNormals(PolyEditor.currentMesh)
	PolyEditor.currentMeshNormals = getMeshVertices(PolyEditor.currentMesh, VERT_NORMAL)

	setMeshVertices(PolyEditor.currentMesh, VERT_COLORBASE, PolyEditor.currentMeshVertexColors)
	PolyEditor.currentMeshVertexColors = getMeshVertices(PolyEditor.currentMesh, VERT_COLORBASE)

	setMeshVertices(PolyEditor.currentMesh, VERT_UV, PolyEditor.currentMeshUVs)
	setMeshTexture(PolyEditor.currentMesh, PolyEditor.currentPageId, PolyEditor.uvRegionPos, PolyEditor.uvRegionSize)
	PolyEditor.currentMeshUVs = getMeshVertices(PolyEditor.currentMesh, VERT_UV)

	setMeshSmoothNormals(PolyEditor.currentMesh, PolyEditor.clipboard.smoothNormals)

	-- the getMeshVertices reassignments above replaced the currentMesh* globals with fresh
	-- tables, breaking the active-layer cache alias; re-capture them into the layer cache now
	_syncActiveCacheFromGlobals()

	setSelectedVerts(sel)

	-- make sure _moveSelectedVerts will only move the newly pasted verts; write through the
	-- active layer's store so the selectedVertsOverlapping alias stays valid (arrow-move reads the store).
	-- The setMeshVertices/getMeshVertices round-trip above can re-normalize the quad-based mesh's
	-- vertex count, so drop any pasted id that no longer exists in the rebuilt cache — otherwise a
	-- later arrow-nudge would index a nil quad and crash.
	local validSel = {}
	for i = 1, #sel do
		if PolyEditor.currentMeshQuads[sel[i]] ~= nil then
			validSel[#validSel + 1] = sel[i]
		end
	end

	local activeStore = PolyEditor.selectedVertsByLayer[PolyEditor.currentLayerId]
	if activeStore ~= nil then
		activeStore.overlapping = validSel
		activeStore.verts = validSel
	end
	PolyEditor.selectedVertsOverlapping = validSel
	PolyEditor.selectedVerts = validSel

	storeUndo()
end

function shortcutFrameSelected()
	_frameSelected(PolyEditor.mouseInViewport)
end

function PolyEditor.frameSelectedAllViews()
	_frameSelected(1)
	_frameSelected(2)
	_frameSelected(3)
	_frameSelected(4)
end

-- Frame the view on the selection across every selected layer. With no verts selected
-- anywhere, frame all verts of all selected layers; otherwise frame the selected verts of
-- each layer. Each layer contributes its own cache + pivot.
function _frameSelected(viewId)
	if viewId == nil then
		viewId = PolyEditor.mouseInViewport
	end

	if viewId == -1 then
		return
	end

	_syncSelectedLayerCaches()

	local hasSelection = _anyLayerHasSelection()

	local minPoint = vec3(9999.0, 9999.0, 9999.0)
	local maxPoint = vec3(-9999.0, -9999.0, -9999.0)
	local found = false

	for li = 1, #PolyEditor.selectedLayers do
		local layerId = PolyEditor.selectedLayers[li]
		local meshData = PolyEditor.meshes[layerId]
		local cache = _ensureLayerCache(layerId)

		if meshData ~= nil and cache ~= nil and #cache.quads > 0 then
			local pivot = _layerPivotPos(layerId)
			local quads = cache.quads

			-- selection present -> this layer's selected verts (may be empty); else all verts
			local verts = nil
			if hasSelection then
				local store = PolyEditor.selectedVertsByLayer[layerId]
				verts = (store ~= nil) and store.verts or {}
			end

			local count = verts and #verts or #quads
			for i = 1, count do
				local vertId = verts and verts[i] or i
				local vert = quads[vertId] + pivot
				found = true

				if vert.x > maxPoint.x then
					maxPoint.x = vert.x
				end

				if vert.y > maxPoint.y then
					maxPoint.y = vert.y
				end

				if vert.z > maxPoint.z then
					maxPoint.z = vert.z
				end

				if vert.x < minPoint.x then
					minPoint.x = vert.x
				end

				if vert.y < minPoint.y then
					minPoint.y = vert.y
				end

				if vert.z < minPoint.z then
					minPoint.z = vert.z
				end
			end
		end
	end

	if not found then
		return
	end

	_frameViewToBounds(viewId, minPoint, maxPoint)
end

function _frameViewToBounds(viewId, minPoint, maxPoint)
	local size = vec3Length(maxPoint - minPoint)
	if size < 0.001 then
		size = 1.0
	end

	local camManip = PolyEditor.camManips[viewId]
	local camMat = PolyEditor.camMats[viewId]

	local forwardAxis = camMat.forwardAxis

	local center = vec3Lerp(maxPoint, minPoint, 0.5)

	if camManip.ortho then
		camManip.pos = center + (forwardAxis * 10.0)
		camManip.focusDist = (1.0 / size) * 0.5
		if viewId == 2 then
			camManip.perspFocusDist = 10.0
		end
	else
		camManip.pos = center + (forwardAxis * (size * 5.0))
		camManip.focusDist = vec3Length(camManip.pos - center)
	end
end

function PolyEditor.frameAllViewsAllLayers()
	if PolyEditor.rootModel == nil then
		return
	end

	local bb = computeWorldBoundingBox(PolyEditor.rootModel)
	local minPoint = bb.min
	local maxPoint = bb.max

	-- computeWorldBoundingBox returns an inverted box when there is no geometry
	if minPoint.x > maxPoint.x then
		return
	end

	_frameViewToBounds(1, minPoint, maxPoint)
	_frameViewToBounds(2, minPoint, maxPoint)
	_frameViewToBounds(3, minPoint, maxPoint)
	_frameViewToBounds(4, minPoint, maxPoint)
end

function PolyEditor.startCaptureGif()
	PolyEditor.gifCaptureTimeStart = getTime()

	_refreshModelHierarchy(PolyEditor.hierarchy)

	local bb = computeWorldBoundingBox(PolyEditor.rootModel)
	local minPoint = bb.min
	local maxPoint = bb.max

	PolyEditor.gifModelCenter = vec3(
		lerp(minPoint.x, maxPoint.x, 0.5),
		lerp(minPoint.y, maxPoint.y, 0.5),
		lerp(minPoint.z, maxPoint.z, 0.5))

	PolyEditor.gifModelSize = vec3Length(maxPoint - minPoint)

	PolyEditor.gifModelRot = 0.0

	if PolyEditor.gifModelSpinSprite == nil then
		PolyEditor.gifModelSpinSprite = allocSprite()

		PolyEditor.gifModelSpinSpriteData = {}
		PolyEditor.gifModelSpinSpriteData.position = vec3(0.0, 0.0, 0.0)
		PolyEditor.gifModelSpinSpriteData.size = vec2(256.0, 256.0)
		PolyEditor.gifModelSpinSpriteData.color = Config.uiColors.polyEditorViewport
	end

	PolyEditor.captureGifWarmup = 1

	PolyEditor.capturingGif = true

	invokeProgram(MAIN_PROGRAMID, "setCapturingGif", true)
end

function _captureGif()
	if PolyEditor.captureGifWarmup == 10 then
		startCaptureGif()

		PolyEditor.gifCaptureTimeStart = getTime()

		PolyEditor.captureGifWarmup = 11

		PolyEditor.gifModelRot = 0.0
	end

	if PolyEditor.captureGifWarmup < 10 then
		PolyEditor.captureGifWarmup = PolyEditor.captureGifWarmup + 1

		PolyEditor.gifModelRot = 0.0
	end

	setViewport(0, 0, PolyEditor.gifModelSpinSpriteData.size.x, PolyEditor.gifModelSpinSpriteData.size.y)

	clear(vec4(0.0, 0.0, 0.0, 255.0))

	startRenderToSprite(PolyEditor.gifModelSpinSprite, vec2(0.0, 0.0), vec2(PolyEditor.gifModelSpinSpriteData.size.x, PolyEditor.gifModelSpinSpriteData.size.y))

	local gifFocusDist = 1.0 / (PolyEditor.gifModelSize * 0.5)

	local camMat = mat4()
	camMat.position = vec3(0.0, 0.0, PolyEditor.gifModelSize * 1.5)

	local focalPointMat = mat4()
	focalPointMat.position = PolyEditor.gifModelCenter

	focalPointMat.rotation = eulerRot(0.0, PolyEditor.gifModelRot, 0.0)

	local spinningCameraMat = focalPointMat * camMat
	local fov = degToRad(35.0)
	setCamera(spinningCameraMat, fov)

	PolyEditor.gifModelRot = PolyEditor.gifModelRot + 1.0

 	local lightDir = vec3Normalize(spinningCameraMat.position + vec3(1.0, 10.0, 0.0))
	local lightColor = vec4(255.0, 255.0, 255.0, 255.0)
	local ambientColor = vec4(100.0, 100.0, 100.0, 255.0)

	local useTexture = 0.0
	if PolyEditor.shadingType == 1 or PolyEditor.shadingType == 4 then
		useTexture = 1.0
	end

	local lit = PolyEditor.shadingType ~= 4

	if lit then
		PolyUtil.beginLighting()
	end

	for i = 1, MESH_LAYERS do
		local meshData = PolyEditor.meshes[i]
		if meshData ~= nil then
			PolyUtil.meshLight(meshData.mesh, lightDir, lightColor, ambientColor)
			setMeshMicroShader(meshData.mesh, "useTexture", useTexture, MICROSHADER_UNIFORM)
			drawMesh(meshData.mesh)
		end
	end

	if lit then
		PolyUtil.endLighting()
	end

	local winSize = getWindowSize()
	setViewport(0.0, 0.0, winSize.x, winSize.x)
	
	setSprite(PolyEditor.gifModelSpinSprite, PolyEditor.gifModelSpinSpriteData)
	drawSprite2D(PolyEditor.gifModelSpinSprite)

	if PolyEditor.gifModelRot > 360.0 then
 		endCaptureGif()

 		PolyEditor.capturingGif = false

 		invokeProgram(MAIN_PROGRAMID, "setCapturingGif", false)
 	end
end

function _moveSelectedVerts(deltaVec)
	if PolyEditor.mouseInViewport == -1 then
		return false
	end

	if PolyEditor.mode == MODEL_MODE then
		local camRotMat = mat4(PolyEditor.camMats[PolyEditor.mouseInViewport])
		camRotMat.position = vec3(0.0, 0.0, 0.0)

		-- Pure translation is pivot-invariant, so the same camera-space delta applies to
		-- every selected layer's LOCAL positions directly (no world round-trip needed).
		local camDeltaVec = camRotMat * (deltaVec * PolyEditor.snapStep)

		_syncSelectedLayerCaches()

		-- does ANY selected layer carry a vertex selection?
		local anySelection = false
		for li = 1, #PolyEditor.selectedLayers do
			local store = PolyEditor.selectedVertsByLayer[PolyEditor.selectedLayers[li]]
			if store ~= nil and #store.overlapping > 0 then
				anySelection = true
				break
			end
		end

		local affected = {}

		for li = 1, #PolyEditor.selectedLayers do
			local layerId = PolyEditor.selectedLayers[li]
			local meshData = PolyEditor.meshes[layerId]
			local store = PolyEditor.selectedVertsByLayer[layerId]

			if meshData ~= nil and meshData.cache ~= nil then
				local quads = meshData.cache.quads
				local verts = (store ~= nil) and store.overlapping or {}

				-- no selection anywhere => legacy whole-model nudge, applied per layer
				if #verts == 0 and anySelection == false then
					verts = {}
					for i = 1, #quads do
						verts[i] = i
					end
				end

				if #verts > 0 then
					local moved = false
					for i = 1, #verts do
						local vertId = verts[i]
						-- skip stale ids: a layer's overlapping list can outlive its cache after a
						-- topology change (e.g. paste round-trip), leaving out-of-range ids
						if quads[vertId] ~= nil then
							quads[vertId] = quads[vertId] + camDeltaVec
							moved = true
						end
					end

					if moved then
						refreshLayer(layerId)
						affected[#affected + 1] = layerId
					end
				end
			end
		end

		PolyEditor.arrowMoveLayerIds = affected

		return #affected > 0
	else
		local pixelSizeX = 1.0 / round(128.0 * PolyEditor.uvRegionSize.x)
		local pixelSizeY = 1.0 / round(128.0 * PolyEditor.uvRegionSize.y)

		for i = 1, #PolyEditor.selectedUVs do
			local vId = PolyEditor.selectedUVs[i]

			local uv = PolyEditor.currentMeshUVs[vId]
			if uv ~= nil then
				uv.x = uv.x + (pixelSizeX * deltaVec.x)
				uv.y = uv.y + (pixelSizeY * deltaVec.y)
			end
		end

		PolyEditor.arrowMoveLayerIds = { PolyEditor.currentLayerId }

		refreshCurrentMesh()

		return true
	end
end

function _startArrowMove(key, deltaVec)
	-- close out any run still in progress (e.g. switching direction without releasing)
	if PolyEditor.arrowMovePending then
		storeUndo(PolyEditor.arrowMoveLayerIds)
		PolyEditor.arrowMovePending = false
	end

	PolyEditor.arrowRepeatKey = key
	PolyEditor.arrowRepeatDelta = deltaVec
	PolyEditor.arrowRepeatNextTime = PolyEditor.time + PolyEditor.arrowRepeatInitialDelay

	if _moveSelectedVerts(deltaVec) then
		PolyEditor.arrowMovePending = true
	end
end

function shortcutLeft()
	_startArrowMove(KEY_LEFT, vec3(-1.0, 0.0, 0.0))
end

function shortcutRight()
	_startArrowMove(KEY_RIGHT, vec3(1.0, 0.0, 0.0))
end

function shortcutUp()
	_startArrowMove(KEY_UP, vec3(0.0, 1.0, 0.0))
end

function shortcutDown()
	_startArrowMove(KEY_DOWN, vec3(0.0, -1.0, 0.0))
end

function shortcutNewQuad()
	PolyEditor.quadToolStart()
end

function shortcutExtrude()
	PolyEditor.extrudeToolStart()
end

function shortcutSplitVertical()
	PolyEditor.verticalSplitToolStart()
end

function shortcutSplitHorizontal()
	PolyEditor.horizontalSplitToolStart()
end

function shortcutMirror()
	PolyEditor.mirrorToolStart()
end

function shortcutFlipNormal()
	PolyEditor.flipNormalToolStart()
end

function shortcutSharpNormal()
	PolyEditor.sharpNormalToolStart()
end

function shortcutSmoothNormal()
	PolyEditor.smoothNormalToolStart()
end

function shortcutPencil()
	PolyEditor.pencilToolStart()
end

function shortcutPickColor()
	PolyEditor.texturePickColorStart()
end

function shortcutLine()
	PolyEditor.lineToolStart()
end

function shortcutBucket()
	PolyEditor.bucketToolStart()
end

function shortcutRectFill()
	PolyEditor.rectFillToolStart()
end

function shortcutStamp()
	PolyEditor.stampToolStart()
end

function shortcutGradient()
	PolyEditor.gradientToolStart()
end

function shortcutMagnify()
	PolyEditor.magnifyTexture()
end

function _registerShortcuts()
	Utils.registerShortcut("frame selected", "shortcutFrameSelected", "F", nil, Config)

	Utils.registerShortcut("select", "shortcutSelectTool", "Q", nil, Config)
	Utils.registerShortcut("move", "shortcutMoveTool", "W", nil, Config)
	Utils.registerShortcut("rotate", "shortcutRotateTool", "E", nil, Config)
	Utils.registerShortcut("scale", "shortcutScaleTool", "R", nil, Config)

	Utils.registerShortcut("new quad", "shortcutNewQuad", "N", nil, Config)
	Utils.registerShortcut("extrude", "shortcutExtrude", "X", nil, Config)
	Utils.registerShortcut("split vertical", "shortcutSplitVertical", "P", nil, Config)
	Utils.registerShortcut("split horizontal", "shortcutSplitHorizontal", "O", nil, Config)
	Utils.registerShortcut("mirror", "shortcutMirror", "I", nil, Config)
	Utils.registerShortcut("flip normal", "shortcutFlipNormal", "U", nil, Config)
	Utils.registerShortcut("sharp normal", "shortcutSharpNormal", "Y", nil, Config)
	Utils.registerShortcut("smooth normal", "shortcutSmoothNormal", "T", nil, Config)

	Utils.registerShortcut("move UV", "shortcutMoveUVTool", "W", nil, Config)
	Utils.registerShortcut("rotate UV", "shortcutRotateUVTool", "E", nil, Config)
	Utils.registerShortcut("scale UV", "shortcutScaleUVTool", "R", nil, Config)

	Utils.registerShortcut("pencil", "shortcutPencil", "A", nil, Config)
	Utils.registerShortcut("pick color", "shortcutPickColor", "S", nil, Config)
	Utils.registerShortcut("line", "shortcutLine", "L", nil, Config)
	Utils.registerShortcut("bucket", "shortcutBucket", "K", nil, Config)
	Utils.registerShortcut("rect fill", "shortcutRectFill", "J", nil, Config)
	Utils.registerShortcut("stamp", "shortcutStamp", "H", nil, Config)
	Utils.registerShortcut("gradient", "shortcutGradient", "G", nil, Config)
	Utils.registerShortcut("magnified region ON/OFF", "shortcutMagnify", "M", nil, Config)

	local programId = getProgramId()
	invokeProgram(MAIN_PROGRAMID, "registerShortcut", {desc = "esc", key = KEY_ESCAPE, callback = "shortcutEsc", programId = programId})
	invokeProgram(MAIN_PROGRAMID, "registerShortcut", {desc = "undo", key = KEY_Z, modifier = KEY_LEFT_CONTROL, callback = "shortcutUndo", programId = programId})
	invokeProgram(MAIN_PROGRAMID, "registerShortcut", {desc = "redo", key = KEY_Y, modifier = KEY_LEFT_CONTROL, callback = "shortcutRedo", programId = programId})
	invokeProgram(MAIN_PROGRAMID, "registerShortcut", {desc = "redo", key = KEY_Z, modifier = {KEY_LEFT_CONTROL, KEY_LEFT_SHIFT}, callback = "shortcutRedo", programId = programId})

	invokeProgram(MAIN_PROGRAMID, "registerShortcut", {desc = "delete", key = KEY_DELETE, callback = "shortcutDeleteSelected", programId = programId})
	invokeProgram(MAIN_PROGRAMID, "registerShortcut", {desc = "delete2", key = KEY_BACKSPACE, callback = "shortcutDeleteSelected", programId = programId})

	invokeProgram(MAIN_PROGRAMID, "registerShortcut", {desc = "copy", key = KEY_X, modifier = KEY_LEFT_CONTROL, callback = "shortcutCut", programId = programId})
	invokeProgram(MAIN_PROGRAMID, "registerShortcut", {desc = "copy", key = KEY_C, modifier = KEY_LEFT_CONTROL, callback = "shortcutCopy", programId = programId})
	invokeProgram(MAIN_PROGRAMID, "registerShortcut", {desc = "paste", key = KEY_V, modifier = KEY_LEFT_CONTROL, callback = "shortcutPaste", programId = programId})


	invokeProgram(MAIN_PROGRAMID, "registerShortcut", {desc = "move selected verts", key = KEY_LEFT, callback = "shortcutLeft", programId = programId})
	invokeProgram(MAIN_PROGRAMID, "registerShortcut", {desc = "move selected verts", key = KEY_RIGHT, callback = "shortcutRight", programId = programId})
	invokeProgram(MAIN_PROGRAMID, "registerShortcut", {desc = "move selected verts", key = KEY_UP, callback = "shortcutUp", programId = programId})
	invokeProgram(MAIN_PROGRAMID, "registerShortcut", {desc = "move selected verts", key = KEY_DOWN, callback = "shortcutDown", programId = programId})

	invokeProgram(MAIN_PROGRAMID, "registerShortcut", {desc = "select all", key = KEY_A, modifier = KEY_LEFT_CONTROL, callback = "shortcutSelectAll", programId = programId})
end

-- Grow the selection to all connected verts. In model mode this runs on every selected
-- layer (mirrors shortcutSelectAll); the UV editor stays active-layer-only.
function PolyEditor.selectAllConnected()
	if PolyEditor.mode ~= TEXTURE_MODE then
		_syncSelectedLayerCaches()

		if not _anyLayerHasSelection() then
			return
		end

		for li = 1, #PolyEditor.selectedLayers do
			local layerId = PolyEditor.selectedLayers[li]
			local meshData = PolyEditor.meshes[layerId]
			local store = PolyEditor.selectedVertsByLayer[layerId]

			if meshData ~= nil and store ~= nil and #store.verts > 0 then
				local mesh = meshData.mesh

				-- seed from a fresh copy so we never mutate the layer's store in place
				local vertsById = {}
				local connectedVerts = {}
				for i = 1, #store.verts do
					local vertId = store.verts[i]
					vertsById[vertId] = 1
					connectedVerts[i] = vertId
				end
				local numVerts = #connectedVerts

				for i = 1, 100 do
					local oldNumVerts = numVerts

					local newConnectedVerts = getConnectedVertices(mesh, connectedVerts)

					for j = 1, #newConnectedVerts do
						local vertId = newConnectedVerts[j]

						if vertsById[vertId] == nil then
							vertsById[vertId] = 1
							connectedVerts[#connectedVerts + 1] = vertId
							numVerts = numVerts + 1
						end
					end

					if numVerts == oldNumVerts then
						break
					end
				end

				setSelectedVertsForLayer(layerId, connectedVerts)
			end
		end

		storeUndo(PolyEditor.selectedLayers)
	else
		-- UV editor is active-layer-only (keep single-layer behavior)
		if #PolyEditor.selectedVerts == 0 then
			return
		end

		local vertsById = {}
		local connectedVerts = PolyEditor.selectedVerts
		local numVerts = #PolyEditor.selectedVerts

		for i = 1, numVerts do
			vertsById[PolyEditor.selectedVerts[i]] = 1
		end

		for i = 1, 100 do
			local oldNumVerts = numVerts

			local newConnectedVerts = getConnectedVertices(PolyEditor.currentMesh, connectedVerts)

			for j = 1, #newConnectedVerts do
				local vertId = newConnectedVerts[j]

				if vertsById[vertId] == nil then
					vertsById[vertId] = 1
					connectedVerts[#connectedVerts + 1] = vertId
					numVerts = numVerts + 1
				end
			end

			if numVerts == oldNumVerts then
				break
			end
		end

		setSelectedVerts(connectedVerts)
		storeUndo()

		refreshUVEditor()
	end
end

-- Active-layer wrapper; per-layer body lives in setSelectedVertsForLayer.
function setSelectedVerts(verts)
	setSelectedVertsForLayer(PolyEditor.currentLayerId, verts)
end

-- Active-layer wrapper; per-layer body lives in setSelectedUVsForLayer.
function setSelectedUVs(uvs)
	setSelectedUVsForLayer(PolyEditor.currentLayerId, uvs)
end

function doubleClicked()
	if PolyEditor.mouseInViewport == -1 then
		return
	end

	if PolyEditor.mode == MODEL_MODE then
		PolyEditor.selectAllConnected()
	else
		if PolyEditor.mouseInViewport == 2 then
			PolyEditor.selectAllConnected()	
		end
	end
end

function updateMouseInput()
	UILib.setCursor(UILib.CURSOR)

	PolyEditor.shiftKeyDown = getKeyDown(KEY_LEFT_SHIFT)
	PolyEditor.ctrlKeyDown = getKeyDown(KEY_LEFT_CONTROL)

	if PolyEditor.mouseDown == -1 then -- react to this one frame later or nothing works
		PolyEditor.mousePressedInViewport = -1
	end

	if PolyEditor.mouseClickStartedOutside == true then
		if getMouseButtonDown(MOUSE_BUTTON_LEFT) == false and
			getMouseButtonDown(MOUSE_BUTTON_MIDDLE) == false and
			getMouseButtonDown(MOUSE_BUTTON_RIGHT) == false then

			PolyEditor.mouseClickStartedOutside = false
		end

		return
	end

	PolyEditor.mousePos = getMousePosition()

	PolyEditor.mouseInViewport = -1
	if insideViewport(PolyEditor.mousePos, PolyEditor.view1SpriteData) then
		PolyEditor.mouseInViewport = 1
	elseif insideViewport(PolyEditor.mousePos, PolyEditor.view2SpriteData) then
		PolyEditor.mouseInViewport = 2
	elseif insideViewport(PolyEditor.mousePos, PolyEditor.view3SpriteData) then
		PolyEditor.mouseInViewport = 3
	elseif insideViewport(PolyEditor.mousePos, PolyEditor.view4SpriteData) then
		PolyEditor.mouseInViewport = 4
	end

	PolyEditor.mousePressed = -1
	PolyEditor.mouseReleased = -1
	PolyEditor.mouseDown = -1

	if PolyEditor.skipClick == true then
		PolyEditor.manipulatingCamera = false
		PolyEditor.clickedViewport = -1

		PolyEditor.skipClick = false
		PolyEditor.skipClickRelease = true

		return
	end

	if getMouseButtonPressed(MOUSE_BUTTON_LEFT) == true then
		if PolyEditor.mousePos.x > PolyEditor.screenWidth or 
			PolyEditor.mousePos.y > PolyEditor.screenHeight then

			PolyEditor.mouseClickStartedOutside = true
		end

		PolyEditor.mousePressed = MOUSE_BUTTON_LEFT

		if PolyEditor.lastClickTime ~= -1.0 then
			local deltaTime = PolyEditor.time - PolyEditor.lastClickTime
			
			if deltaTime < 0.3 then
				PolyEditor.skipClick = true

				doubleClicked()

				return
			end
		end

		PolyEditor.lastClickTime = PolyEditor.time

	elseif getMouseButtonPressed(MOUSE_BUTTON_MIDDLE) == true then
		PolyEditor.mousePressed = MOUSE_BUTTON_MIDDLE
	elseif getMouseButtonPressed(MOUSE_BUTTON_RIGHT) == true then
		PolyEditor.mousePressed = MOUSE_BUTTON_RIGHT
	end

	if getMouseButtonReleased(MOUSE_BUTTON_LEFT) == true then
		PolyEditor.mouseReleased = MOUSE_BUTTON_LEFT
	elseif getMouseButtonReleased(MOUSE_BUTTON_MIDDLE) == true then
		PolyEditor.mouseReleased = MOUSE_BUTTON_MIDDLE
	elseif getMouseButtonReleased(MOUSE_BUTTON_RIGHT) == true then
		PolyEditor.mouseReleased = MOUSE_BUTTON_RIGHT
	end

	if getMouseButtonDown(MOUSE_BUTTON_LEFT) == true then
		PolyEditor.mouseDown = MOUSE_BUTTON_LEFT
	elseif getMouseButtonDown(MOUSE_BUTTON_MIDDLE) == true then
		PolyEditor.mouseDown = MOUSE_BUTTON_MIDDLE
	elseif getMouseButtonDown(MOUSE_BUTTON_RIGHT) == true then
		PolyEditor.mouseDown = MOUSE_BUTTON_RIGHT
	end

	if PolyEditor.mousePressed ~= -1 and UILib.isHovered() == true then
		PolyEditor.mousePressed = -1
		PolyEditor.mousePressedOnUI = true
	end

	if PolyEditor.mousePressedOnUI == true then
		PolyEditor.mouseDown = -1
	end

	if PolyEditor.mouseReleased ~= -1 and PolyEditor.mousePressedOnUI == true then
		PolyEditor.mousePressedOnUI = false
		PolyEditor.mouseReleased = -1
	end

	if PolyEditor.mouseDown == -1 and PolyEditor.skipClickRelease == true then
		PolyEditor.skipClickRelease = false

		PolyEditor.manipulatingCamera = false
		PolyEditor.clickedViewport = -1

		return
	end

	if PolyEditor.mousePressed ~= -1 then
		PolyEditor.clickedViewport = PolyEditor.mouseInViewport
		PolyEditor.mousePressedPos = PolyEditor.mousePos
	end

	if PolyEditor.mouseDown == -1 then
		PolyEditor.clickedViewport = -1
	end

	if PolyEditor.mouseReleased == MOUSE_BUTTON_LEFT then 
		if PolyEditor.mode == MODEL_MODE then
			refreshSelectionModelMode()
		elseif PolyEditor.mode == TEXTURE_MODE then
			refreshSelectionTextureMode()
		end
	end
end

function refreshSelectionModelMode()
	if PolyEditor.manipulatingCamera == true or
		PolyEditor.draggingVerts == true or
		PolyEditor.draggingSelection == true or
		PolyEditor.mouseInViewport == -1 then

		return
	end
		
	selectPickedElementsModel()
end

function refreshSelectionTextureMode()
	if PolyEditor.manipulatingCamera == true or
		PolyEditor.draggingVerts == true or
		PolyEditor.draggingSelection == true or
		PolyEditor.mouseInViewport == -1 then

		return
	end

	if PolyEditor.mouseInViewport == 2 then
		setSelectedUVs({})

		selectPickedElementsModel()

		refreshUVEditor()
	end

	if PolyEditor.mouseInViewport == 3 then
		selectPickedElementsUV()
	end
end

function setActiveTool(toolName, toolType)
	UILib.setCursor(UILib.CURSOR)

	if toolType == nil then
		toolType = PolyEditor.mode
	end

	if toolType == MODEL_MODE then
		if toolName == "" then
			toolName = "select"
		end

		PolyEditor.activeTool = toolName

	elseif toolType == TEXTURE_MODE then
		if toolName == "" then
			toolName = "pencil"
		end

		PolyEditor.activePaintTool = toolName

	elseif toolType == RIG_MODE then
		if toolName == "" then
			toolName = "move pivot"
		end

		PolyEditor.activeTool = toolName
	end
	
	PolyEditorUI.toolActivated()
end

function PolyEditor.setPrimitiveResolution(resolution)
	PolyEditor.primitiveResolution = resolution

	PolyEditorUI.primitiveResolutionChanged()
end

function _newPrimitive(primitiveType)
	PolyEditor.newPrimitiveMesh = allocMesh()

	if primitiveType == "box" then
		PolyEditor.newPrimitiveQuads = PolyUtil.box(1.0, PolyEditor.primitiveResolution)
	elseif primitiveType == "sphere" then
		if PolyEditor.primitiveResolution == 1 then
			PolyEditor.newPrimitiveQuads = PolyUtil.sphere(1.0, 6, 5)
		elseif PolyEditor.primitiveResolution == 2 then
			PolyEditor.newPrimitiveQuads = PolyUtil.sphere(1.0, 8, 6)
		elseif PolyEditor.primitiveResolution == 3 then
			PolyEditor.newPrimitiveQuads = PolyUtil.sphere(1.0, 10, 7)
		end
	elseif primitiveType == "cylinder" then
		if PolyEditor.primitiveResolution == 1 then
			PolyEditor.newPrimitiveQuads = PolyUtil.cylinder(0.5, 1.0, 5, 1)
		elseif PolyEditor.primitiveResolution == 2 then
			PolyEditor.newPrimitiveQuads = PolyUtil.cylinder(0.5, 1.0, 6, 1)
		elseif PolyEditor.primitiveResolution == 3 then
			PolyEditor.newPrimitiveQuads = PolyUtil.cylinder(0.5, 1.0, 8, 1)
		end
	elseif primitiveType == "cone" then
		if PolyEditor.primitiveResolution == 1 then
			PolyEditor.newPrimitiveQuads = PolyUtil.cone(0.5, 1.0, 5, 1)
		elseif PolyEditor.primitiveResolution == 2 then
			PolyEditor.newPrimitiveQuads = PolyUtil.cone(0.5, 1.0, 6, 1)
		elseif PolyEditor.primitiveResolution == 3 then
			PolyEditor.newPrimitiveQuads = PolyUtil.cone(0.5, 1.0, 8, 1)
		end

		PolyEditor.newPrimitiveQuads = PolyUtil.cone(1.0, PolyEditor.primitiveResolution)
	end

	PolyEditor.newPrimitiveQuadsRest = {}

	local vertsColor = {}
	for i = 1, #PolyEditor.newPrimitiveQuads do
		PolyEditor.newPrimitiveQuadsRest[#PolyEditor.newPrimitiveQuadsRest + 1] = vec3Copy(PolyEditor.newPrimitiveQuads[i])
		
		vertsColor[i] = PolyEditor.currentVertexColor
	end

	setMeshVertices(PolyEditor.newPrimitiveMesh, VERT_POSITION, PolyEditor.newPrimitiveQuads)

	computeNormals(PolyEditor.newPrimitiveMesh)

	PolyEditor.newPrimitiveMeshNormals = getMeshVertices(PolyEditor.newPrimitiveMesh, VERT_NORMAL)
	
	setMeshMicroShader(PolyEditor.newPrimitiveMesh, "modelMat", mat4(), MICROSHADER_UNIFORM)

	setMeshTexture(PolyEditor.newPrimitiveMesh, PolyEditor.currentPageId)

	setMeshMicroShader(PolyEditor.newPrimitiveMesh, "useTexture", 0.0, MICROSHADER_UNIFORM)

	setMeshVertices(PolyEditor.newPrimitiveMesh, VERT_COLORBASE, vertsColor)

	updateMeshLight(PolyEditor.newPrimitiveMesh)
end

function _updateNewPrimitive()
	local posInView = vec3(0.0, 0.0, 0.0)
	if PolyEditor.mouseInViewport == 1 then
		posInView = screenToWorld(viewportToScreen(PolyEditor.mousePos, PolyEditor.view1SpriteData, true), PolyEditor.screenSize, PolyEditor.camManipView1.focusDist, PolyEditor.camView1Mat, 1.0, true)
		
		local viewPos = vec2(PolyEditor.view3SpriteData.position.x, PolyEditor.view3SpriteData.position.y) + (PolyEditor.view3SpriteData.size * 0.5)
		posInView.y = screenToWorld(viewportToScreen(viewPos, PolyEditor.view3SpriteData, true), PolyEditor.screenSize, PolyEditor.camManipView3.focusDist, PolyEditor.camView3Mat, 1.0, true).y
	elseif PolyEditor.mouseInViewport == 2 then
		local camManip2 = PolyEditor.camManipView2
		if camManip2.ortho then
			posInView = screenToWorld(viewportToScreen(PolyEditor.mousePos, PolyEditor.view2SpriteData, true), PolyEditor.screenSize, camManip2.focusDist, PolyEditor.camView2Mat, 1.0, true)
			local forward = PolyEditor.camView2Mat.forwardAxis
			local orbitDist = camManip2.perspFocusDist or camManip2.focusDist
			local focusPoint = camManip2.pos - (forward * orbitDist)
			posInView = posInView + forward * vec3Dot(focusPoint - posInView, forward)
		else
			posInView = screenToWorld(viewportToScreen(PolyEditor.mousePos, PolyEditor.view2SpriteData, false), PolyEditor.screenSize, camManip2.focusDist, PolyEditor.camView2Mat, PolyEditor.camFov, false)
		end
	elseif PolyEditor.mouseInViewport == 3 then
		posInView = screenToWorld(viewportToScreen(PolyEditor.mousePos, PolyEditor.view3SpriteData, true), PolyEditor.screenSize, PolyEditor.camManipView3.focusDist, PolyEditor.camView3Mat, 1.0, true)

		local viewPos = vec2(PolyEditor.view1SpriteData.position.x, PolyEditor.view1SpriteData.position.y) + (PolyEditor.view1SpriteData.size * 0.5)
		posInView.z = screenToWorld(viewportToScreen(viewPos, PolyEditor.view1SpriteData, true), PolyEditor.screenSize, PolyEditor.camManipView1.focusDist, PolyEditor.camView1Mat, 1.0, true).z
	elseif PolyEditor.mouseInViewport == 4 then
		posInView = screenToWorld(viewportToScreen(PolyEditor.mousePos, PolyEditor.view4SpriteData, true), PolyEditor.screenSize, PolyEditor.camManipView4.focusDist, PolyEditor.camView4Mat, 1.0, true)

		local viewPos = vec2(PolyEditor.view1SpriteData.position.x, PolyEditor.view1SpriteData.position.y) + (PolyEditor.view1SpriteData.size * 0.5)
		posInView.x = screenToWorld(viewportToScreen(viewPos, PolyEditor.view1SpriteData, true), PolyEditor.screenSize, PolyEditor.camManipView1.focusDist, PolyEditor.camView1Mat, 1.0, true).x
	end

	if PolyEditor.snapEnabled == true then
		posInView.x = PolyEditor.snapStep * round(posInView.x / PolyEditor.snapStep)
		posInView.y = PolyEditor.snapStep * round(posInView.y / PolyEditor.snapStep)
		posInView.z = PolyEditor.snapStep * round(posInView.z / PolyEditor.snapStep)
	else
		if vec3Length(posInView - vec3(0.0, 0.0, 0.0)) < 0.2 then
			posInView.x = 0.0
			posInView.y = 0.0
			posInView.z = 0.0
		end
	end

	for i = 1, #PolyEditor.newPrimitiveQuadsRest do
		PolyEditor.newPrimitiveQuads[i] = PolyEditor.newPrimitiveQuadsRest[i] + posInView
	end

	setMeshVertices(PolyEditor.newPrimitiveMesh, VERT_POSITION, PolyEditor.newPrimitiveQuads)

	updateMeshLight(PolyEditor.newPrimitiveMesh)

	if PolyEditor.mousePressed == MOUSE_BUTTON_LEFT then
		setActiveTool("select", MODEL_MODE)
		
		PolyEditor.skipClick = true

		PolyEditor.newPrimitiveQuads = getMeshVertices(PolyEditor.newPrimitiveMesh) -- get quantized verts

		local pivot = currentLayerPivotPos()
		for i = 1, #PolyEditor.newPrimitiveQuads do
			PolyEditor.currentMeshQuads[#PolyEditor.currentMeshQuads + 1] = PolyEditor.newPrimitiveQuads[i] - pivot

			PolyEditor.currentMeshVertexColors[#PolyEditor.currentMeshVertexColors + 1] = vec4(PolyEditor.currentVertexColor)
		end

		refreshCurrentMesh()

		if PolyEditor.newPrimitiveMesh ~= nil then
			destroy(PolyEditor.newPrimitiveMesh)
		end

		PolyEditor.newPrimitiveMesh = nil
		PolyEditor.newPrimitiveQuads = {}
		PolyEditor.newPrimitiveMeshNormals = {}

		storeUndo()
	end
end

function PolyEditor.newCubeToolStart()
	setActiveTool("new cube", MODEL_MODE)

	_newPrimitive("box")
end

function _newCubeToolUpdate()
	_updateNewPrimitive()
end

function PolyEditor.newSphereToolStart()
	setActiveTool("new sphere", MODEL_MODE)

	_newPrimitive("sphere")
end

function _newSphereToolUpdate()
	_updateNewPrimitive()
end

function PolyEditor.newCylinderToolStart()
	setActiveTool("new cylinder", MODEL_MODE)

	_newPrimitive("cylinder")
end

function _newCylinderToolUpdate()
	_updateNewPrimitive()
end

function PolyEditor.newConeToolStart()
	setActiveTool("new cone", MODEL_MODE)

	_newPrimitive("cone")
end

function _newConeToolUpdate()
	_updateNewPrimitive()
end

function PolyEditor.switchToModel()
	if PolyEditor.mode == MODEL_MODE then
		return
	end

	PolyEditor.mode = MODEL_MODE

	PolyEditor.selectToolStart()

	PolyEditor.camManipView1 = PolyEditor.camManipView1Model
	PolyEditor.camManips[1] = PolyEditor.camManipView1

	PolyEditor.camManipView3 = PolyEditor.camManipView3Model
	PolyEditor.camManips[3] = PolyEditor.camManipView3

	PolyEditor.camManipView4 = PolyEditor.camManipView4Model
	PolyEditor.camManips[4] = PolyEditor.camManipView4

	PolyEditor.orthoCamZoomMin = 0.05
	PolyEditor.orthoCamZoomMax = 2.0
	PolyEditor.focusSpeed = 1.0

	PolyEditor.setShadingType(PolyEditor.modelModeShadingType)

	setSelectedUVs({})

	PolyEditor.drawPerspVertsEnabled = true

	PolyEditorUI.hierarchyInspector:setVisible(false)
	PolyEditorUI.colorPicker:setVisible(false)

	storeUndo()
end

function PolyEditor.switchToTexture()
	if PolyEditor.mode == TEXTURE_MODE then
		return
	end

	PolyEditor.mode = TEXTURE_MODE

	PolyEditor.selectToolStart()
	PolyEditor.pencilToolStart()

	PolyEditor.camManipView1 = PolyEditor.camManipView1Texture
	PolyEditor.camManips[1] = PolyEditor.camManipView1

	PolyEditor.camManipView3 = PolyEditor.camManipView3Texture
	PolyEditor.camManips[3] = PolyEditor.camManipView3

	PolyEditor.camManipView4 = PolyEditor.camManipView4Texture
	PolyEditor.camManips[4] = PolyEditor.camManipView4

	PolyEditor.orthoCamZoomMin = 0.0061
	PolyEditor.orthoCamZoomMax = 0.05
	PolyEditor.focusSpeed = 0.1
	
	PolyEditor.setShadingType(PolyEditor.textureModeShadingType)

	refreshUVEditor()

	PolyEditor.drawPerspVertsEnabled = true

	PolyEditorUI.hierarchyInspector:setVisible(false)

	storeUndo()
end

function PolyEditor.switchToRig()
	if PolyEditor.mode == RIG_MODE then
		return
	end

	PolyEditor.mode = RIG_MODE

	PolyEditor.selectToolStart()

	PolyEditor.camManipView1 = PolyEditor.camManipView1Model
	PolyEditor.camManips[1] = PolyEditor.camManipView1

	PolyEditor.camManipView3 = PolyEditor.camManipView3Model
	PolyEditor.camManips[3] = PolyEditor.camManipView3

	PolyEditor.camManipView4 = PolyEditor.camManipView4Model
	PolyEditor.camManips[4] = PolyEditor.camManipView4

	PolyEditor.orthoCamZoomMin = 0.05
	PolyEditor.orthoCamZoomMax = 2.0
	PolyEditor.focusSpeed = 1.0

	PolyEditor.setShadingType(PolyEditor.rigModeShadingType)

	setSelectedUVs({})

	PolyEditor.drawPerspVertsEnabled = false

	PolyEditor.movePivotToolStart()

	if PolyEditor.pivotCenterSprite == nil then
		PolyEditor.pivotCenterSprite = allocSprite()
		PolyEditor.pivotCenterSpriteData = {}
		PolyEditor.pivotCenterSpriteData.position = vec3(0.0, 0.0, 0.0)
		PolyEditor.pivotCenterSpriteData.size = vec2(20.0, 20.0)
		PolyEditor.pivotCenterSpriteData.color = vec4(255.0, 255.0, 255.0, 100.0)
		PolyEditor.pivotCenterSpriteData.billboard = 1.0
		PolyEditor.pivotCenterSpriteData.billboardPixelPerfect = 1.0
	end

	storeUndo()
end

function shortcutSelectTool()
	PolyEditor.selectToolStart()
end

function PolyEditor.selectToolStart()
	setActiveTool("select", MODEL_MODE)
end

function shortcutMoveUVTool()
	PolyEditor.moveUVToolStart()	
end

function PolyEditor.moveUVToolStart()
	if PolyEditor.mode ~= TEXTURE_MODE then
		return
	end

	setActiveTool("move uv", MODEL_MODE)
end

function shortcutRotateUVTool()
	PolyEditor.rotateUVToolStart()	
end

function PolyEditor.rotateUVToolStart()
	if PolyEditor.mode ~= TEXTURE_MODE then
		return
	end

	setActiveTool("rotate uv", MODEL_MODE)
end

function shortcutScaleUVTool()
	PolyEditor.scaleUVToolStart()	
end

function PolyEditor.scaleUVToolStart()
	if PolyEditor.mode ~= TEXTURE_MODE then
		return
	end

	setActiveTool("scale uv", MODEL_MODE)
end

function PolyEditor.startCaptureUVSnapshot()
	if PolyEditor.mode ~= TEXTURE_MODE then
		return
	end

	local verts = PolyEditor.selectedVerts

	if #verts == 0 then
		for i = 1, #PolyEditor.currentMeshQuads do
			verts[i] = i
		end
	end

	if #verts < 4 then
		return
	end

	local camMat = PolyEditor.camMats[2]
	local right = camMat.rightAxis
	local up = camMat.upAxis

	local projU = {}
	local projV = {}
	local minU, maxU = 999999, -999999
	local minV, maxV = 999999, -999999

	for i = 1, #verts do
		local vertId = verts[i]
		local pos = PolyEditor.currentMeshQuads[vertId]

		local u = vec3Dot(pos, right)
		local v = vec3Dot(pos, up)

		projU[i] = u
		projV[i] = v

		if u < minU then minU = u end
		if u > maxU then maxU = u end
		if v < minV then minV = v end
		if v > maxV then maxV = v end
	end

	local rangeU = maxU - minU
	local rangeV = maxV - minV
	if rangeU < 0.001 then rangeU = 1.0 end
	if rangeV < 0.001 then rangeV = 1.0 end

	local uvRegionPos = PolyEditor.uvRegionPos
	local uvRegionSize = PolyEditor.uvRegionSize
	local pixelSizeX = 1.0 / (uvRegionSize.x * 128.0)
	local pixelSizeY = 1.0 / (uvRegionSize.y * 128.0)

	for i = 1, #verts do
		local vertId = verts[i]

		local u = uvRegionPos.x + ((projU[i] - minU) / rangeU) * uvRegionSize.x
		local v = uvRegionPos.y + ((projV[i] - minV) / rangeV) * uvRegionSize.y

		u = pixelSizeX * round(u / pixelSizeX)
		v = pixelSizeY * round(v / pixelSizeY)

		PolyEditor.currentMeshUVs[vertId] = vec2(u, v)
	end

	updateUV()
	refreshCurrentMesh()
	refreshUVEditor()

	setSelectedUVs(verts)

	storeUndo()
end

function shortcutMoveTool()
	PolyEditor.moveToolStart()
end

function PolyEditor.moveToolStart()
	if PolyEditor.mode ~= MODEL_MODE then
		return
	end

	setActiveTool("move", MODEL_MODE)
end

function shortcutRotateTool()
	PolyEditor.rotateToolStart()
end

function PolyEditor.rotateToolStart()
	if PolyEditor.mode ~= MODEL_MODE then
		return
	end

	setActiveTool("rotate", MODEL_MODE)
end

function shortcutScaleTool()
	PolyEditor.scaleToolStart()
end

function PolyEditor.scaleToolStart()
	if PolyEditor.mode ~= MODEL_MODE then
		return
	end

	setActiveTool("scale", MODEL_MODE)
end

function PolyEditor.quadToolStart()
	setActiveTool("new quad", MODEL_MODE)

	PolyEditor.tmpQuadVerts = {}

	PolyEditor.pickingVertsEnabled = true
	PolyEditor.pickingQuadsEnabled = false
end

function PolyEditor.tileBoundsEnabledSwitch()
	PolyEditor.tileBoundsEnabled = not PolyEditor.tileBoundsEnabled

	Utils.setAppVar("tileBoundsEnabled", PolyEditor.tileBoundsEnabled)
end

function PolyEditor.snapSwitch()
	PolyEditor.snapEnabled = not PolyEditor.snapEnabled

	Utils.setAppVar("polyEditor_snapEnabled", PolyEditor.snapEnabled)
end

function PolyEditor.scissorSwitch()
	PolyEditor.scissorEnabled = not PolyEditor.scissorEnabled
end

function PolyEditor.textureScissorSwitch()
	PolyEditor.textureScissorEnabled = not PolyEditor.textureScissorEnabled
end

-- Extrude a single layer's selected faces (needs >= 4 selected verts). Returns true if it ran.
function _extrudeLayer(layerId)
	local meshData = PolyEditor.meshes[layerId]
	local store = PolyEditor.selectedVertsByLayer[layerId]
	if meshData == nil or store == nil or #store.verts < 4 then
		return false
	end

	local cache = _ensureLayerCache(layerId)
	if cache == nil then
		return false
	end

	local oldSel = {table.unpack(store.verts)}

	cache.quads = PolyUtil.extrude(oldSel, meshData.mesh)

	if layerId == PolyEditor.currentLayerId then
		-- re-point globals at the rebuilt cache before refreshLayer syncs from them
		_bindActiveLayerGlobals(layerId)
	end

	refreshLayer(layerId)

	store.overlapping = PolyUtil.getAllOverlappingVerts(meshData.mesh, store.verts)
	if layerId == PolyEditor.currentLayerId then
		PolyEditor.selectedVertsOverlapping = store.overlapping
	end

	return true
end

function PolyEditor.extrudeToolStart()
	setActiveTool("select", MODEL_MODE)

	_syncSelectedLayerCaches()

	local extruded = false
	local affected = {}

	for i = 1, #PolyEditor.selectedLayers do
		local layerId = PolyEditor.selectedLayers[i]
		if _extrudeLayer(layerId) then
			extruded = true
			affected[#affected + 1] = layerId
		end
	end

	if extruded then
		storeUndo(affected)
	end
end

function _polySplitStart(direction)
	if #PolyEditor.selectedVerts < 4 then
		setActiveTool("select", MODEL_MODE)

		return
	end
	
	setActiveTool("split " .. direction, MODEL_MODE)

	PolyEditor.pickingVertsEnabled = false
	PolyEditor.pickingQuadsEnabled = false

	PolyEditor.tmpToolMesh = allocMesh()

	local quadIds = PolyUtil.selectedVertsToQuads(PolyEditor.currentMesh, PolyEditor.selectedVerts)
	
	PolyEditor.tmpSplitSelectedQuadPoints = {}
	PolyEditor.tmpSplitSelectedQuadColors = {}
	PolyEditor.vertIdToRemove = {}

	for quadId, v in pairs(quadIds) do
		for vIt = 1, 4 do
			local vId = ((quadId - 1) * 4) + vIt

			PolyEditor.tmpSplitSelectedQuadPoints[#PolyEditor.tmpSplitSelectedQuadPoints + 1] = vec3(PolyEditor.currentMeshQuads[vId])
			PolyEditor.tmpSplitSelectedQuadColors[#PolyEditor.tmpSplitSelectedQuadColors + 1] = vec4(PolyEditor.currentMeshVertexColors[vId])

			PolyEditor.vertIdToRemove[vId] = true
		end
	end
end

function _polySplitToolUpdate(direction)
	local viewSpriteData = PolyEditor.viewsSpriteData[PolyEditor.mouseInViewport]

	local minX = 999.0
	local maxX = -999.0
	local minY = 999.0
	local maxY = -999.0
	for i = 1, #PolyEditor.tmpSplitSelectedQuadPoints do
		local vertPos = PolyEditor.tmpSplitSelectedQuadPoints[i]

		local pos2d = worldToViewport(vertPos, viewSpriteData)

		if pos2d.x < minX then
			minX = pos2d.x
		end

		if pos2d.x > maxX then
			maxX = pos2d.x
		end

		if pos2d.y < minY then
			minY = pos2d.y
		end

		if pos2d.y > maxY then
			maxY = pos2d.y
		end
	end

	local splitT = 0.0

	if direction == "vertical" then
		splitT = remapRange(
			PolyEditor.mousePos.x, minX, maxX, 
			0.0, 1.0)
	else
		splitT = remapRange(
			PolyEditor.mousePos.y, minY, maxY, 
			1.0, 0.0)
	end

	splitT = clamp(splitT, 0.0, 1.0)

	if PolyEditor.snapEnabled then
		local snapT = PolyEditor.snapStep
		splitT = snapT * round(splitT / snapT)
	end

	local camMat = PolyEditor.camView2Mat
	if PolyEditor.mouseInViewport ~= -1 then
		camMat = PolyEditor.camMats[PolyEditor.mouseInViewport]
	end

	local splitQuads = PolyUtil.splitQuads(PolyEditor.tmpSplitSelectedQuadPoints, direction, camMat, splitT)

	local splitColors = {}
	local numInputQuads = #PolyEditor.tmpSplitSelectedQuadPoints / 4

	for q = 0, numInputQuads - 1 do
		local srcBase = q * 4
		local outBase = q * 8

		local cornerIndices
		if direction == "vertical" then
			cornerIndices = {1, 4, 6, 7}
		else
			cornerIndices = {1, 2, 7, 8}
		end

		local cornerColors = {}
		for _, ci in ipairs(cornerIndices) do
			local pos = splitQuads[outBase + ci]
			for k = 1, 4 do
				local srcPos = PolyEditor.tmpSplitSelectedQuadPoints[srcBase + k]
				if pos.x == srcPos.x and pos.y == srcPos.y and pos.z == srcPos.z then
					cornerColors[ci] = vec4(PolyEditor.tmpSplitSelectedQuadColors[srcBase + k])
					break
				end
			end
		end

		if direction == "vertical" then
			local colorMB = vec4Lerp(cornerColors[1], cornerColors[6], splitT)
			local colorMT = vec4Lerp(cornerColors[4], cornerColors[7], splitT)

			splitColors[outBase + 1] = cornerColors[1]
			splitColors[outBase + 2] = vec4(colorMB)
			splitColors[outBase + 3] = vec4(colorMT)
			splitColors[outBase + 4] = cornerColors[4]
			splitColors[outBase + 5] = vec4(colorMB)
			splitColors[outBase + 6] = cornerColors[6]
			splitColors[outBase + 7] = cornerColors[7]
			splitColors[outBase + 8] = vec4(colorMT)
		else
			local colorML = vec4Lerp(cornerColors[1], cornerColors[8], splitT)
			local colorMR = vec4Lerp(cornerColors[2], cornerColors[7], splitT)

			splitColors[outBase + 1] = cornerColors[1]
			splitColors[outBase + 2] = cornerColors[2]
			splitColors[outBase + 3] = vec4(colorMR)
			splitColors[outBase + 4] = vec4(colorML)
			splitColors[outBase + 5] = vec4(colorML)
			splitColors[outBase + 6] = vec4(colorMR)
			splitColors[outBase + 7] = cornerColors[7]
			splitColors[outBase + 8] = cornerColors[8]
		end
	end

	setMeshVertices(PolyEditor.tmpToolMesh, VERT_POSITION, splitQuads)
	
	local color = Config.paletteNames.greyLight
	local backfaceCull = false
	setDepthEnabled(false)
	drawWireframe(PolyEditor.tmpToolMesh, color, backfaceCull)
	setDepthEnabled(true)

	if PolyEditor.mousePressed == MOUSE_BUTTON_LEFT then
		setActiveTool("select", MODEL_MODE)
		
		PolyEditor.skipClick = true

		destroy(PolyEditor.tmpToolMesh)
		PolyEditor.tmpToolMesh = nil

		PolyEditor.currentMeshQuads = Utils.removeElements(PolyEditor.currentMeshQuads, PolyEditor.vertIdToRemove)
		PolyEditor.currentMeshVertexColors = Utils.removeElements(PolyEditor.currentMeshVertexColors, PolyEditor.vertIdToRemove)

		local newSel = {}
		for i = 1, #splitQuads do
			PolyEditor.currentMeshQuads[#PolyEditor.currentMeshQuads + 1] = vec3(splitQuads[i])
			PolyEditor.currentMeshVertexColors[#PolyEditor.currentMeshVertexColors + 1] = splitColors[i]

			newSel[#newSel + 1] = #PolyEditor.currentMeshQuads
		end

		setMeshVertices(PolyEditor.currentMesh, VERT_POSITION, PolyEditor.currentMeshQuads)
		computeNormals(PolyEditor.currentMesh)

		PolyUtil.fixNormalsFromNeighbors(newSel, PolyEditor.currentMesh)

		PolyEditor.currentMeshQuads = getMeshVertices(PolyEditor.currentMesh)

		refreshCurrentMesh()

		setSelectedVerts(newSel)

		PolyEditor.tmpSplitSelectedQuadPoints = {}
		PolyEditor.tmpSplitSelectedQuadColors = {}
		PolyEditor.vertIdToRemove = {}

		storeUndo()

		PolyEditor.pickingVertsEnabled = true
		PolyEditor.pickingQuadsEnabled = true
	end
end

function PolyEditor.verticalSplitToolStart()
	_polySplitStart("vertical")
end

function PolyEditor.horizontalSplitToolStart()
	_polySplitStart("horizontal")
end

-- Resolve the per-layer vert lists for a normals op across all selected layers.
-- Returns { {layerId=, verts=} ... }:
--   * if any selected layer holds a selection: one entry per such layer (its overlapping verts)
--   * otherwise: one entry per selected layer, covering ALL of that layer's verts
function _resolveNormalVertsPerLayer()
	_syncSelectedLayerCaches()

	local out = {}

	if _anyLayerHasSelection() then
		for li = 1, #PolyEditor.selectedLayers do
			local layerId = PolyEditor.selectedLayers[li]
			local store = PolyEditor.selectedVertsByLayer[layerId]
			local meshData = PolyEditor.meshes[layerId]
			if store ~= nil and meshData ~= nil and #store.verts > 0 then
				out[#out + 1] = {
					layerId = layerId,
					verts = PolyUtil.getAllOverlappingVerts(meshData.mesh, store.verts),
				}
			end
		end
	else
		for li = 1, #PolyEditor.selectedLayers do
			local layerId = PolyEditor.selectedLayers[li]
			local cache = _ensureLayerCache(layerId)
			if cache ~= nil then
				local all = {}
				for i = 1, #cache.quads do
					all[i] = i
				end
				out[#out + 1] = { layerId = layerId, verts = all }
			end
		end
	end

	return out
end

-- Remove `verts` from a layer's smooth-normal set (make them sharp) and recompute.
function _sharpNormalLayer(layerId, verts)
	local meshData = PolyEditor.meshes[layerId]
	if meshData == nil then
		return
	end
	local mesh = meshData.mesh

	local smoothNormalsVertsIds = getMeshSmoothNormals(mesh)
	local newSmoothNormalsVertsIds = {}

	for i = 1, #smoothNormalsVertsIds do
		local vId = smoothNormalsVertsIds[i]

		local toBeSmoothed = true
		for vIt = 1, #verts do
			if verts[vIt] == vId then
				toBeSmoothed = false
				break
			end
		end

		if toBeSmoothed == true then
			newSmoothNormalsVertsIds[#newSmoothNormalsVertsIds + 1] = vId
		end
	end

	setMeshSmoothNormals(mesh, newSmoothNormalsVertsIds)
	refreshLayer(layerId)
end

-- Add `verts` to a layer's smooth-normal set (make them smooth) and recompute.
function _smoothNormalLayer(layerId, verts)
	local meshData = PolyEditor.meshes[layerId]
	if meshData == nil then
		return
	end
	local mesh = meshData.mesh

	local smoothNormalsVertsIds = getMeshSmoothNormals(mesh)

	for i = 1, #verts do
		smoothNormalsVertsIds[#smoothNormalsVertsIds + 1] = verts[i]
	end

	setMeshSmoothNormals(mesh, smoothNormalsVertsIds)
	refreshLayer(layerId)
end

function PolyEditor.sharpNormalToolStart()
	local work = _resolveNormalVertsPerLayer()

	local affected = {}
	for w = 1, #work do
		_sharpNormalLayer(work[w].layerId, work[w].verts)
		affected[#affected + 1] = work[w].layerId
	end

	if #affected > 0 then
		storeUndo(affected)
	end
end

function PolyEditor.smoothNormalToolStart()
	local work = _resolveNormalVertsPerLayer()

	local affected = {}
	for w = 1, #work do
		_smoothNormalLayer(work[w].layerId, work[w].verts)
		affected[#affected + 1] = work[w].layerId
	end

	if #affected > 0 then
		storeUndo(affected)
	end
end

-- Flip the winding/normals of a layer's selected quads; returns true if it acted.
function _flipNormalLayer(layerId)
	local meshData = PolyEditor.meshes[layerId]
	if meshData == nil then
		return false
	end
	local store = PolyEditor.selectedVertsByLayer[layerId]
	if store == nil or #store.verts < 4 then
		return false
	end
	local mesh = meshData.mesh

	local quadIds = PolyUtil.selectedVertsToQuads(mesh, store.verts)
	for k, v in pairs(quadIds) do
		flipNormal(mesh, k)
	end

	-- flipNormal reordered the GPU winding; capture the new positions before refreshLayer
	-- pushes them back (write through the active global so refreshLayer's sync won't clobber it)
	local cache = _ensureLayerCache(layerId)
	local newQuads = getMeshVertices(mesh)
	cache.quads = newQuads
	if layerId == PolyEditor.currentLayerId then
		PolyEditor.currentMeshQuads = newQuads
	end

	refreshLayer(layerId)
	return true
end

function PolyEditor.flipNormalToolStart()
	_syncSelectedLayerCaches()

	local affected = {}
	for li = 1, #PolyEditor.selectedLayers do
		local layerId = PolyEditor.selectedLayers[li]
		if _flipNormalLayer(layerId) then
			affected[#affected + 1] = layerId
		end
	end

	if #affected == 0 then
		setActiveTool("select", MODEL_MODE)
		return
	end

	storeUndo(affected)
end

function PolyEditor.mirrorToolStart()
	local sel = {}

	if #PolyEditor.selectedVerts == 0 then
		for i = 1, #PolyEditor.currentMeshQuads do
			sel[i] = i
		end
	else
		if #PolyEditor.selectedVerts < 4 then
			return
		end

		sel = PolyEditor.selectedVerts
	end
	
	setActiveTool("mirror", MODEL_MODE)

	PolyEditor.pickingVertsEnabled = false
	PolyEditor.pickingQuadsEnabled = false

	PolyEditor.tmpToolMesh = allocMesh()

	local quadIds = PolyUtil.selectedVertsToQuads(PolyEditor.currentMesh, sel)
	
	PolyEditor.tmpMirrorVertsId = {}
	PolyEditor.tmpMirrorVerts = {}
	
	local tmpMirrorUVs = {}
	local tmpMirrorColors = {}

	local vertIdMap = {}

	for quadId, v in pairs(quadIds) do
		for vIt = 1, 4 do
			local vId = ((quadId - 1) * 4) + vIt

			PolyEditor.tmpMirrorVertsId[#PolyEditor.tmpMirrorVertsId + 1] = vId
			PolyEditor.tmpMirrorVerts[#PolyEditor.tmpMirrorVerts + 1] = PolyEditor.currentMeshQuads[vId]
			tmpMirrorUVs[#tmpMirrorUVs + 1] = PolyEditor.currentMeshUVs[vId]
			tmpMirrorColors[#tmpMirrorColors + 1] = PolyEditor.currentMeshVertexColors[vId]
			
			vertIdMap[vId] = #PolyEditor.tmpMirrorVertsId
		end
	end
	
	local smoothNormalsVertsIds = getMeshSmoothNormals(PolyEditor.currentMesh)

	PolyEditor.tmpMirrorSmoothNormals = {}
	for i = 1, #smoothNormalsVertsIds do
		local sourceVertId = smoothNormalsVertsIds[i]
		local destVertId = vertIdMap[i]

		PolyEditor.tmpMirrorSmoothNormals[i] = destVertId
	end

	setMeshVertices(PolyEditor.tmpToolMesh, VERT_POSITION, PolyEditor.tmpMirrorVerts)

	setMeshSmoothNormals(PolyEditor.tmpToolMesh, PolyEditor.tmpMirrorSmoothNormals)
	
	setMeshVertices(PolyEditor.tmpToolMesh, VERT_UV, tmpMirrorUVs)

	setMeshTexture(PolyEditor.tmpToolMesh, PolyEditor.currentPageId, PolyEditor.uvRegionPos, PolyEditor.uvRegionSize)

	setMeshVertices(PolyEditor.tmpToolMesh, VERT_COLORBASE, tmpMirrorColors)

	setMeshMicroShader(PolyEditor.currentMesh, "modelMat", mat4(), MICROSHADER_UNIFORM)
end

function _mirrorToolUpdate()
	if PolyEditor.mouseInViewport == -1 or PolyEditor.mouseInViewport == 2 then
		return
	end
	
	local fov = - PolyEditor.camManips[PolyEditor.mouseInViewport].focusDist
	
	local camMat = PolyEditor.camMats[PolyEditor.mouseInViewport]

	local viewSpriteData = PolyEditor.viewsSpriteData[PolyEditor.mouseInViewport]

	local ortho = true

	local planePoint = vec3(0.0, 0.0, 0.0)

	local mouseInViewport = viewportToScreen(vec2(PolyEditor.mousePos.x, viewSpriteData.position.y), viewSpriteData, ortho)
	local ray = screenRay(mouseInViewport, fov, camMat, ortho)
	local axisP1 = ray.origin + (ray.direction * intersectPlane(ray.origin, ray.direction, planePoint, - camMat.forwardAxis))

	if PolyEditor.snapEnabled == true then
		axisP1.x = PolyEditor.snapStep * round(axisP1.x / PolyEditor.snapStep)
		axisP1.y = PolyEditor.snapStep * round(axisP1.y / PolyEditor.snapStep)
		axisP1.z = PolyEditor.snapStep * round(axisP1.z / PolyEditor.snapStep)
	end

	local axisP2 = axisP1 + (camMat.upAxis * 10.0)
	local axisP3 = axisP1 - (camMat.upAxis * 10.0)

	drawLine(axisP2, axisP3, Config.paletteNames.grey)

	local mirrorVerts = {}
	local mirrorNormal = camMat.rightAxis

	for i = 1, #PolyEditor.tmpMirrorVerts do
		local p = vec3(PolyEditor.tmpMirrorVerts[i])

		local dist = vec3Dot(p - axisP1, mirrorNormal)
		local mirrorP = p - (mirrorNormal * (2.0 * dist))

		mirrorVerts[i] = mirrorP
	end

	setMeshVertices(PolyEditor.tmpToolMesh, VERT_POSITION, mirrorVerts)
	computeNormals(PolyEditor.tmpToolMesh)

	local backfaceCull = false
	setDepthEnabled(false)
	drawWireframe(PolyEditor.tmpToolMesh, Config.paletteNames.grey, backfaceCull)
	setDepthEnabled(true)

	if PolyEditor.mousePressed == MOUSE_BUTTON_LEFT then
		local smoothNormalsVertsIds = getMeshSmoothNormals(PolyEditor.currentMesh)

		local lastQuadId = #PolyEditor.currentMeshQuads

		for polyId = 1, #PolyEditor.tmpMirrorVertsId / 4 do
			-- the newly mirrored ids, order is inverted to flip the normals
			local mirrorVId1 = ((polyId - 1) * 4) + 4
			local mirrorVId2 = ((polyId - 1) * 4) + 3
			local mirrorVId3 = ((polyId - 1) * 4) + 2
			local mirrorVId4 = ((polyId - 1) * 4) + 1

			-- the source mesh ids
			local vId1 = PolyEditor.tmpMirrorVertsId[mirrorVId1]
			local vId2 = PolyEditor.tmpMirrorVertsId[mirrorVId2]
			local vId3 = PolyEditor.tmpMirrorVertsId[mirrorVId3]
			local vId4 = PolyEditor.tmpMirrorVertsId[mirrorVId4]

			PolyEditor.currentMeshQuads[#PolyEditor.currentMeshQuads + 1] = mirrorVerts[mirrorVId1]
			PolyEditor.currentMeshQuads[#PolyEditor.currentMeshQuads + 1] = mirrorVerts[mirrorVId2]
			PolyEditor.currentMeshQuads[#PolyEditor.currentMeshQuads + 1] = mirrorVerts[mirrorVId3]
			PolyEditor.currentMeshQuads[#PolyEditor.currentMeshQuads + 1] = mirrorVerts[mirrorVId4]

			PolyEditor.currentMeshUVs[#PolyEditor.currentMeshUVs + 1] = vec2(PolyEditor.currentMeshUVs[vId1])
			PolyEditor.currentMeshUVs[#PolyEditor.currentMeshUVs + 1] = vec2(PolyEditor.currentMeshUVs[vId2])
			PolyEditor.currentMeshUVs[#PolyEditor.currentMeshUVs + 1] = vec2(PolyEditor.currentMeshUVs[vId3])
			PolyEditor.currentMeshUVs[#PolyEditor.currentMeshUVs + 1] = vec2(PolyEditor.currentMeshUVs[vId4])
			
			PolyEditor.currentMeshVertexColors[#PolyEditor.currentMeshVertexColors + 1] = vec4(PolyEditor.currentMeshVertexColors[vId1])
			PolyEditor.currentMeshVertexColors[#PolyEditor.currentMeshVertexColors + 1] = vec4(PolyEditor.currentMeshVertexColors[vId2])
			PolyEditor.currentMeshVertexColors[#PolyEditor.currentMeshVertexColors + 1] = vec4(PolyEditor.currentMeshVertexColors[vId3])
			PolyEditor.currentMeshVertexColors[#PolyEditor.currentMeshVertexColors + 1] = vec4(PolyEditor.currentMeshVertexColors[vId4])

			smoothNormalsVertsIds[#smoothNormalsVertsIds + 1] = PolyEditor.tmpMirrorSmoothNormals[mirrorVId1]
			smoothNormalsVertsIds[#smoothNormalsVertsIds + 1] = PolyEditor.tmpMirrorSmoothNormals[mirrorVId2]
			smoothNormalsVertsIds[#smoothNormalsVertsIds + 1] = PolyEditor.tmpMirrorSmoothNormals[mirrorVId3]
			smoothNormalsVertsIds[#smoothNormalsVertsIds + 1] = PolyEditor.tmpMirrorSmoothNormals[mirrorVId4]
		end

		setMeshSmoothNormals(PolyEditor.currentMesh, smoothNormalsVertsIds)

		refreshCurrentMesh()

		if PolyEditor.tmpToolMesh ~= nil then
			destroy(PolyEditor.tmpToolMesh)
			PolyEditor.tmpToolMesh = nil
		end

		setActiveTool("select", MODEL_MODE)
		PolyEditor.pickingVertsEnabled = true
		PolyEditor.pickingQuadsEnabled = true
		PolyEditor.skipClick = true

		PolyEditor.tmpMirrorVertsId = {}
		PolyEditor.tmpMirrorVerts = {}

		storeUndo()
	end
end

function _quadToolUpdate()
	if PolyEditor.mouseInViewport == -1 then
		return
	end

	if PolyEditor.mouseDown == MOUSE_BUTTON_LEFT and PolyEditor.mousePressedInViewport == 2 and #PolyEditor.pickedVerts == 0 and #PolyEditor.tmpQuadVerts == 0 then
		return
	end

	UILib.setCursor(UILib.CURSOR_PIXEL)

	local ortho = PolyEditor.camManips[PolyEditor.mouseInViewport].ortho

	local fov = PolyEditor.camFov
	if ortho == true then
		fov = - PolyEditor.camManips[PolyEditor.mouseInViewport].focusDist
	end
	
	local camMat = PolyEditor.camMats[PolyEditor.mouseInViewport]

	local viewSpriteData = PolyEditor.viewsSpriteData[PolyEditor.mouseInViewport]

	local mouseInViewport = viewportToScreen(PolyEditor.mousePos, viewSpriteData, ortho)
	local ray = screenRay(mouseInViewport, fov, camMat, ortho)

	local p = nil

	if #PolyEditor.pickedVerts > 0 then
		local vertId = PolyEditor.pickedVerts[1]
		local pickedLayerId = PolyEditor.pickedLayerId or PolyEditor.currentLayerId
		p = _pickedVertActiveLocalPos(vertId, pickedLayerId) or vec3Copy(PolyEditor.currentMeshQuads[vertId])

	elseif #PolyEditor.tmpQuadVerts == 1 then
		local planePoint = PolyEditor.tmpQuadVerts[1]

		p = ray.origin + (ray.direction * intersectPlane(ray.origin, ray.direction, planePoint, - camMat.forwardAxis))
		
	elseif #PolyEditor.tmpQuadVerts == 2 then
		local planePoint = PolyEditor.tmpQuadVerts[2]

		local planeNormal = - camMat.forwardAxis

		p = ray.origin + (ray.direction * intersectPlane(ray.origin, ray.direction, planePoint, planeNormal))

	elseif #PolyEditor.tmpQuadVerts == 3 then
		local planePoint = (PolyEditor.tmpQuadVerts[1] + PolyEditor.tmpQuadVerts[2] + PolyEditor.tmpQuadVerts[3]) * (1.0 / 3.0)
		
		local planeNormal = - camMat.forwardAxis

		local delta1 = PolyEditor.tmpQuadVerts[1] - PolyEditor.tmpQuadVerts[2]
		local delta2 = PolyEditor.tmpQuadVerts[1] - PolyEditor.tmpQuadVerts[3]
		if vec3Length(delta1) > 0.00001 and vec3Length(delta2) > 0.00001 then
			planeNormal = vec3Normalize(vec3Cross(delta1, delta2))
		end

		if vec3Dot(planeNormal, camMat.forwardAxis) > 0.0 then
			planeNormal = - planeNormal
		end

		p = ray.origin + (ray.direction * intersectPlane(ray.origin, ray.direction, planePoint, planeNormal))
	else
		p = ray.origin + (ray.direction * intersectPlane(ray.origin, ray.direction, vec3(0.0, 0.0, 0.0), - camMat.forwardAxis))
	
	end

	p.x = quantize(p.x)
	p.y = quantize(p.y)
	p.z = quantize(p.z)

	if PolyEditor.snapEnabled then
		p.x = PolyEditor.snapStep * round(p.x / PolyEditor.snapStep)
		p.y = PolyEditor.snapStep * round(p.y / PolyEditor.snapStep)
		p.z = PolyEditor.snapStep * round(p.z / PolyEditor.snapStep)
	end
	
	PolyUtil.drawVert(p, PolyEditor.vertexSize, Config.uiColors.polyEditorVertex)
	
	if #PolyEditor.tmpQuadVerts > 0 then
		drawLine(PolyEditor.tmpQuadVerts[#PolyEditor.tmpQuadVerts], p)
	end

	if #PolyEditor.tmpQuadVerts == 2 then
		drawLine(PolyEditor.tmpQuadVerts[1], PolyEditor.tmpQuadVerts[2])
	end

	if #PolyEditor.tmpQuadVerts == 3 then
		drawLine(PolyEditor.tmpQuadVerts[1], PolyEditor.tmpQuadVerts[2])
		drawLine(PolyEditor.tmpQuadVerts[2], PolyEditor.tmpQuadVerts[3])
		drawLine(p, PolyEditor.tmpQuadVerts[1])
	end
	
	for i = 1, #PolyEditor.tmpQuadVerts do
		PolyUtil.drawVert(PolyEditor.tmpQuadVerts[i], PolyEditor.vertexSize, Config.uiColors.polyEditorVertex)
	end

	if PolyEditor.mousePressed == MOUSE_BUTTON_LEFT then
		PolyEditor.tmpQuadVerts[#PolyEditor.tmpQuadVerts + 1] = p

		if #PolyEditor.tmpQuadVerts == 4 then
			for i = 1, #PolyEditor.tmpQuadVerts do
				PolyEditor.currentMeshQuads[#PolyEditor.currentMeshQuads + 1] = PolyEditor.tmpQuadVerts[i]
			end

			refreshCurrentMesh()

			PolyEditor.tmpQuadVerts = {}

			PolyEditor.pickingQuadsEnabled = true

			setActiveTool("select", MODEL_MODE)

			-- flip normal towards camera
			local newNormal = PolyEditor.currentMeshNormals[#PolyEditor.currentMeshNormals]
			if vec3Dot(newNormal, camMat.forwardAxis) < 0.0 then
				flipNormal(PolyEditor.currentMesh, #PolyEditor.currentMeshQuads / 4)

				PolyEditor.currentMeshQuads = getMeshVertices(PolyEditor.currentMesh)
				
				refreshCurrentMesh()
			end

			storeUndo()

			PolyEditor.pickingVertsEnabled = true
			PolyEditor.pickingQuadsEnabled = true
		end
	end
end

function scaleToolUpdate()
	if PolyEditor.pickingSelection == true then
		UILib.setCursor(UILib.CURSOR_SCALE)
	else
		UILib.setCursor(UILib.CURSOR)
	end

	if PolyEditor.selectedVerts == 0 then
		return
	end

	if PolyEditor.mouseInViewport == -1 or PolyEditor.mousePressedInViewport == -1 then
		return
	end

	local ortho = PolyEditor.camManips[PolyEditor.mousePressedInViewport].ortho

	local fov = PolyEditor.camFov
	if ortho == true then
		fov = - PolyEditor.camManips[PolyEditor.mousePressedInViewport].focusDist
	end

	if PolyEditor.draggingVertsJustStarted == true then
		local camMat = PolyEditor.camMats[PolyEditor.mousePressedInViewport]
		local viewSpriteData = PolyEditor.viewsSpriteData[PolyEditor.mousePressedInViewport]
		local mouseInViewport = viewportToScreen(PolyEditor.mousePos, viewSpriteData, ortho)
		local ray = screenRay(mouseInViewport, fov, camMat, ortho)

		local tempCenter = vec3(0.0, 0.0, 0.0)
		for i = 1, #PolyEditor.dragVerts do
			tempCenter = tempCenter + PolyEditor.dragVertsPos[i]
		end
		tempCenter = tempCenter * (1.0 / #PolyEditor.dragVerts)

		if PolyEditor.ctrlKeyDown == true then
			PolyEditor.uniformScaleMode = true
			PolyEditor.dragCenter = tempCenter

			local pReproj = ray.origin + (ray.direction * intersectPlane(ray.origin, ray.direction, PolyEditor.dragCenter, - camMat.forwardAxis))
			PolyEditor.dragScaleRestDist = vec3Length(pReproj - PolyEditor.dragCenter)
		else
			PolyEditor.uniformScaleMode = false

			local p = ray.origin + (ray.direction * intersectPlane(ray.origin, ray.direction, tempCenter, - camMat.forwardAxis))

			local maxDist = 0.0
			local maxDistP = vec3(0.0, 0.0, 0.0)

			for i = 1, #PolyEditor.dragVerts do
				local vertPos = PolyEditor.dragVertsPos[i]
				local dist = vec3Length(vertPos - p)
				if dist > maxDist then
					maxDist = dist
					maxDistP = vertPos
				end
			end

			PolyEditor.dragCenter = maxDistP

			local pReproj = ray.origin + (ray.direction * intersectPlane(ray.origin, ray.direction, PolyEditor.dragCenter, - camMat.forwardAxis))
			PolyEditor.dragScaleRest = vec3(abs(pReproj.x - PolyEditor.dragCenter.x), abs(pReproj.y - PolyEditor.dragCenter.y), abs(pReproj.z - PolyEditor.dragCenter.z))
		end
	end

	if PolyEditor.draggingVerts == true then
		local camMat = PolyEditor.camMats[PolyEditor.mousePressedInViewport]

		local viewSpriteData = PolyEditor.viewsSpriteData[PolyEditor.mousePressedInViewport]

		local mouseInViewport = viewportToScreen(PolyEditor.mousePos, viewSpriteData, ortho)
		local ray = screenRay(mouseInViewport, fov, camMat, ortho)

		local p = ray.origin + (ray.direction * intersectPlane(ray.origin, ray.direction, PolyEditor.dragCenter, - camMat.forwardAxis))

		local scaleVec = p - PolyEditor.dragCenter

		local scale = vec3(1.0, 1.0, 1.0)

		if PolyEditor.uniformScaleMode == true then
			local uniformScale = 1.0
			if PolyEditor.dragScaleRestDist > 0.0 then
				uniformScale = vec3Length(scaleVec) / PolyEditor.dragScaleRestDist
			end
			scale = vec3(uniformScale, uniformScale, uniformScale)
		else
			local scaleX = 1.0
			if PolyEditor.dragScaleRest.x > 0.0 then
				scaleX = abs(scaleVec.x) / PolyEditor.dragScaleRest.x
			end

			local scaleY = 1.0
			if PolyEditor.dragScaleRest.y > 0.0 then
				scaleY = abs(scaleVec.y) / PolyEditor.dragScaleRest.y
			end

			local scaleZ = 1.0
			if PolyEditor.dragScaleRest.z > 0.0 then
				scaleZ = abs(scaleVec.z) / PolyEditor.dragScaleRest.z
			end

			if PolyEditor.mousePressedInViewport == 1 then
				scale.x = scaleX
				scale.z = scaleZ
			elseif PolyEditor.mousePressedInViewport == 2 then
				scale.x = scaleX
				scale.y = scaleY
				scale.z = scaleZ
			elseif PolyEditor.mousePressedInViewport == 3 then
				scale.x = scaleX
				scale.y = scaleY
			elseif PolyEditor.mousePressedInViewport == 4 then
				scale.y = scaleY
				scale.z = scaleZ
			end
		end

		local pivots = _dragLayerPivots()
		for i = 1, #PolyEditor.dragVerts do
			local d = PolyEditor.dragVerts[i]

			local vertPos = PolyEditor.dragVertsPos[i] - PolyEditor.dragCenter

			local p = PolyEditor.dragCenter + (vertPos * scale)

			if PolyEditor.snapEnabled then
				p.x = PolyEditor.snapStep * round(p.x / PolyEditor.snapStep)
				p.y = PolyEditor.snapStep * round(p.y / PolyEditor.snapStep)
				p.z = PolyEditor.snapStep * round(p.z / PolyEditor.snapStep)
			end

			local meshData = PolyEditor.meshes[d.layerId]
			if meshData ~= nil and meshData.cache ~= nil then
				meshData.cache.quads[d.vertId] = p - pivots[d.layerId]
			end
		end
		_pushDragLayers()

		PolyEditor.dragToolEngaged = true
	end

	if PolyEditor.dragToolEngaged == true and PolyEditor.mouseReleased == MOUSE_BUTTON_LEFT then
		PolyEditor.dragToolEngaged = false

		_finishDrag()
	end
end

function rotateToolUpdate()
	if PolyEditor.pickingSelection == true then
		UILib.setCursor(UILib.CURSOR_ROTATE)
	else
		UILib.setCursor(UILib.CURSOR)
	end

	if PolyEditor.selectedVerts == 0 then
		return
	end

	if PolyEditor.mouseInViewport == -1 or PolyEditor.mousePressedInViewport == -1 then
		return
	end
	
	local ortho = PolyEditor.camManips[PolyEditor.mousePressedInViewport].ortho

	local fov = PolyEditor.camFov
	if ortho == true then
		fov = - PolyEditor.camManips[PolyEditor.mousePressedInViewport].focusDist
	end

	if PolyEditor.draggingVertsJustStarted == true then
		PolyEditor.dragCenter = vec3(0.0, 0.0, 0.0)

		for i = 1, #PolyEditor.dragVerts do
			PolyEditor.dragCenter = PolyEditor.dragCenter + PolyEditor.dragVertsPos[i]
		end

		PolyEditor.dragCenter = PolyEditor.dragCenter * (1.0 / #PolyEditor.dragVerts)
		
		local camMat = PolyEditor.camMats[PolyEditor.mousePressedInViewport]

		local viewSpriteData = PolyEditor.viewsSpriteData[PolyEditor.mousePressedInViewport]

		local mouseInViewport = viewportToScreen(PolyEditor.mousePos, viewSpriteData, ortho)
		local ray = screenRay(mouseInViewport, fov, camMat, ortho)

		PolyEditor.dragMousePressedPos3d = ray.origin + (ray.direction * intersectPlane(ray.origin, ray.direction, PolyEditor.dragCenter, - camMat.forwardAxis))
		
		local clickMat = mat4LookAt(vec3(0.0, 0.0, 0.0), 
			camMat.forwardAxis,
			vec3Normalize(PolyEditor.dragMousePressedPos3d - PolyEditor.dragCenter))

		local cameraRestMat = mat4LookAt(vec3(0.0, 0.0, 0.0), 
			camMat.forwardAxis,
			camMat.upAxis)

		mat4Invert(clickMat)

		PolyEditor.dragOffsetMat = cameraRestMat * clickMat

		mat4Invert(cameraRestMat)

		PolyEditor.dragRestMat = cameraRestMat
	end

	if PolyEditor.draggingVerts == true then
		local camMat = PolyEditor.camMats[PolyEditor.mousePressedInViewport]

		local viewSpriteData = PolyEditor.viewsSpriteData[PolyEditor.mousePressedInViewport]

		local mouseInViewport = viewportToScreen(PolyEditor.mousePos, viewSpriteData, ortho)
		local ray = screenRay(mouseInViewport, fov, camMat, ortho)
		
		local p = ray.origin + (ray.direction * intersectPlane(ray.origin, ray.direction, PolyEditor.dragCenter, - camMat.forwardAxis))
		
		local dragMat = mat4LookAt(vec3(0.0, 0.0, 0.0), 
			camMat.forwardAxis,
			vec3Normalize(p - PolyEditor.dragCenter))

		local rotateMat = PolyEditor.dragOffsetMat * (dragMat * PolyEditor.dragRestMat)

		local pivots = _dragLayerPivots()
		for i = 1, #PolyEditor.dragVerts do
			local d = PolyEditor.dragVerts[i]

			local vertPos = PolyEditor.dragVertsPos[i] - PolyEditor.dragCenter

			local p = PolyEditor.dragCenter + (rotateMat * vertPos)

			if PolyEditor.snapEnabled then
				p.x = PolyEditor.snapStep * round(p.x / PolyEditor.snapStep)
				p.y = PolyEditor.snapStep * round(p.y / PolyEditor.snapStep)
				p.z = PolyEditor.snapStep * round(p.z / PolyEditor.snapStep)
			end

			local meshData = PolyEditor.meshes[d.layerId]
			if meshData ~= nil and meshData.cache ~= nil then
				meshData.cache.quads[d.vertId] = p - pivots[d.layerId]
			end
		end
		_pushDragLayers()

		PolyEditor.dragToolEngaged = true
	end

	if PolyEditor.dragToolEngaged == true and PolyEditor.mouseReleased == MOUSE_BUTTON_LEFT then
		PolyEditor.dragToolEngaged = false

		_finishDrag()
	end
end

-- ===== Stage C: cross-layer transform drag helpers =====
-- dragVerts entries are {layerId, vertId}; dragVertsPos entries are WORLD positions
-- (local + layer pivot), so a single shared world center transforms every selected layer.

-- Unique list of layerIds present in the current drag set.
function _dragAffectedLayerIds()
	local seen = {}
	local ids = {}
	for i = 1, #PolyEditor.dragVerts do
		local lid = PolyEditor.dragVerts[i].layerId
		if seen[lid] == nil then
			seen[lid] = 1
			ids[#ids + 1] = lid
		end
	end
	return ids
end

-- Map of layerId -> world pivot for the current drag set (one lookup per layer per frame).
function _dragLayerPivots()
	local pivots = {}
	for i = 1, #PolyEditor.dragLayerIds do
		local lid = PolyEditor.dragLayerIds[i]
		pivots[lid] = _layerPivotPos(lid)
	end
	return pivots
end

-- Push every drag-affected layer's cached quad positions to its GPU mesh (live preview).
function _pushDragLayers()
	for i = 1, #PolyEditor.dragLayerIds do
		local layerId = PolyEditor.dragLayerIds[i]
		local meshData = PolyEditor.meshes[layerId]
		if meshData ~= nil and meshData.cache ~= nil then
			setMeshVertices(meshData.mesh, VERT_POSITION, meshData.cache.quads)
		end
	end
end

-- Finish a drag: refresh each affected layer and store one multi-layer undo entry.
function _finishDrag()
	local ids = _dragAffectedLayerIds()
	for i = 1, #ids do
		refreshLayer(ids[i])
	end
	storeUndo(ids)
end

-- Is a set of picked verts (on the given layer's mesh) part of that layer's selection?
function _pickedInLayerSelection(pickVerts, layerId, fullSearch)
	local meshData = PolyEditor.meshes[layerId]
	local store = PolyEditor.selectedVertsByLayer[layerId]
	if meshData == nil or store == nil then
		return false
	end
	return pickedVertsInSelection(pickVerts, store.verts, fullSearch, meshData.mesh)
end

function updateToolActivation()
	PolyEditor.draggingVertsJustStarted = false

	if PolyEditor.manipulatingCamera == true then
		return
	end

	if PolyEditor.mouseDown == -1 then
		PolyEditor.draggingVerts = false
	end

	if PolyEditor.draggingVerts == true then
		return
	end

	-- picked verts belong to the layer under the cursor; hovered verts to the active layer
	local pickedLayerId = PolyEditor.pickedLayerId or PolyEditor.currentLayerId
	PolyEditor.pickingSelection = _pickedInLayerSelection(PolyEditor.pickedVerts, pickedLayerId, false)
	if PolyEditor.pickingSelection == false then
		PolyEditor.pickingSelection = _pickedInLayerSelection(PolyEditor.hoveredVerts, PolyEditor.currentLayerId, true)
	end

	if PolyEditor.mouseDown == MOUSE_BUTTON_LEFT then
		if #PolyEditor.pickedVerts == 0 then
			return
		end

		local mouseDelta = PolyEditor.mousePressedPos - PolyEditor.mousePos

		if PolyEditor.draggingVerts == false and vec2Length(mouseDelta) > 0.1 then
			if PolyEditor.pickingSelection == false then
				return
			end

			PolyEditor.draggingVerts = true
			PolyEditor.draggingVertsJustStarted = true

			-- make sure every selected layer's cache is resident before reading positions
			_syncSelectedLayerCaches()

			PolyEditor.dragVerts = {}
			PolyEditor.dragVertsPos = {}
			PolyEditor.pickedVerts = {}

			if PolyEditor.clickedViewport ~= 2 and PolyEditor.pickingSelection == false then
				for li = 1, #PolyEditor.selectedLayers do
					setSelectedVertsForLayer(PolyEditor.selectedLayers[li], {})
				end
			end

			-- gather drag verts from every selected layer, in WORLD space
			for li = 1, #PolyEditor.selectedLayers do
				local layerId = PolyEditor.selectedLayers[li]
				local store = PolyEditor.selectedVertsByLayer[layerId]
				local meshData = PolyEditor.meshes[layerId]

				if store ~= nil and meshData ~= nil and meshData.cache ~= nil then
					local verts = store.overlapping
					if PolyEditor.scissorEnabled == true then
						verts = store.verts
					end

					local pivot = _layerPivotPos(layerId)
					local quads = meshData.cache.quads

					for i = 1, #verts do
						local vertId = verts[i]
						-- skip stale ids: a layer's overlapping list can outlive a topology
						-- change, leaving ids that no longer exist in the rebuilt cache
						if quads[vertId] ~= nil then
							local n = #PolyEditor.dragVerts + 1
							PolyEditor.dragVerts[n] = { layerId = layerId, vertId = vertId }
							PolyEditor.dragVertsPos[n] = quads[vertId] + pivot

							if layerId == PolyEditor.currentLayerId then
								PolyEditor.pickedVerts[#PolyEditor.pickedVerts + 1] = vertId
							end
						end
					end
				end
			end

			PolyEditor.dragLayerIds = _dragAffectedLayerIds()
		end
	end
end

function updateUVToolActivation()
	PolyEditor.draggingVertsJustStarted = false
	
	if PolyEditor.mouseInViewport ~= 3 then
		return
	end

	if PolyEditor.manipulatingCamera == true then	
		return
	end

	if PolyEditor.mouseDown == -1 then
		PolyEditor.draggingVerts = false
	end

	if PolyEditor.draggingVerts == true then
		return
	end
	
	local pickedUVVerts = {}

	if PolyEditor.pickedUVQuad ~= -1 then
		pickedUVVerts[1] = ((PolyEditor.pickedUVQuad - 1) * 4) + 1
		pickedUVVerts[2] = ((PolyEditor.pickedUVQuad - 1) * 4) + 2
		pickedUVVerts[3] = ((PolyEditor.pickedUVQuad - 1) * 4) + 3
		pickedUVVerts[4] = ((PolyEditor.pickedUVQuad - 1) * 4) + 4
	elseif PolyEditor.pickedUVId ~= -1 then
		pickedUVVerts[1] = PolyEditor.pickedUVId
	end

	PolyEditor.pickingSelection = pickedVertsInSelection(pickedUVVerts, PolyEditor.selectedUVs)
	if PolyEditor.pickingSelection == false then
		local fullSearch = true
		PolyEditor.pickingSelection = pickedVertsInSelection(pickedUVVerts, PolyEditor.selectedUVs, fullSearch)
	end

	if PolyEditor.mouseDown == MOUSE_BUTTON_LEFT then
		if #pickedUVVerts == 0 or PolyEditor.pickingSelection == false then
			return
		end

		local mouseDelta = PolyEditor.mousePressedPos - PolyEditor.mousePos

		if PolyEditor.draggingVerts == false and vec2Length(mouseDelta) > 0.1 then
			PolyEditor.draggingVerts = true
			PolyEditor.draggingVertsJustStarted = true

			PolyEditor.dragVerts = {}
			PolyEditor.dragVertsPos = {}

			for i = 1, #PolyEditor.selectedUVs do
				local vertId = PolyEditor.selectedUVs[i]

				PolyEditor.dragVerts[i] = vertId

				local uv = PolyEditor.currentMeshUVs[vertId]
				
				PolyEditor.dragVertsPos[i] = vec3(uv.x * PolyEditor.uvDispW, uv.y * PolyEditor.uvDispH, PolyEditor.uvDrawDepth)
			end
		end
	end
end


function selectToolUpdate()
	if PolyEditor.draggingVerts == true then
		return
	end

	if PolyEditor.dragToolEngaged == true then
		return
	end

	if PolyEditor.mousePressedInViewport == -1 then
		PolyEditor.draggingSelection = false

		return
	end

	if PolyEditor.mouseInViewport == -1 or PolyEditor.mousePressedInViewport == 2 then
		return
	end

	if PolyEditor.mouseDown == MOUSE_BUTTON_LEFT then
		local mouseDelta = PolyEditor.mousePressedPos - PolyEditor.mousePos

		if PolyEditor.draggingSelection == false and vec2Length(mouseDelta) > 0.1 then
			PolyEditor.draggingSelection = true
		end
	end

	if PolyEditor.draggingSelection == true then
		local screenP1 = PolyEditor.mousePressedPos
		local screenP2 = vec2(PolyEditor.mousePos.x, PolyEditor.mousePressedPos.y)
		local screenP3 = vec2(PolyEditor.mousePos.x, PolyEditor.mousePos.y)
		local screenP4 = vec2(PolyEditor.mousePressedPos.x, PolyEditor.mousePos.y)

		local ortho = true

		local fov = PolyEditor.camManips[PolyEditor.mousePressedInViewport].focusDist

		local camMat = PolyEditor.camMats[PolyEditor.mousePressedInViewport]

		local viewSpriteData = PolyEditor.viewsSpriteData[PolyEditor.mousePressedInViewport]

		local ray1 = screenRay(viewportToScreen(screenP1, viewSpriteData, ortho), -fov, camMat, ortho)
		local ray2 = screenRay(viewportToScreen(screenP2, viewSpriteData, ortho), -fov, camMat, ortho)
		local ray3 = screenRay(viewportToScreen(screenP3, viewSpriteData, ortho), -fov, camMat, ortho)
		local ray4 = screenRay(viewportToScreen(screenP4, viewSpriteData, ortho), -fov, camMat, ortho)

		setDepthEnabled(false)
		drawLine(ray1.origin + ray1.direction, ray2.origin + ray2.direction)
		drawLine(ray2.origin + ray2.direction, ray3.origin + ray3.direction)
		drawLine(ray3.origin + ray3.direction, ray4.origin + ray4.direction)
		drawLine(ray4.origin + ray4.direction, ray1.origin + ray1.direction)
		setDepthEnabled(true)

		if PolyEditor.mouseReleased == MOUSE_BUTTON_LEFT then
			PolyEditor.draggingSelection = false

			local p1 = screenToWorld(viewportToScreen(screenP1, viewSpriteData, ortho), PolyEditor.screenSize, fov, camMat, 1.0, ortho)
			local p3 = screenToWorld(viewportToScreen(screenP3, viewSpriteData, ortho), PolyEditor.screenSize, fov, camMat, 1.0, ortho)

			local delta = (p3 - p1) * 0.5
			delta.x = abs(delta.x)
			delta.y = abs(delta.y)
			delta.z = abs(delta.z)

			local rectStartPoint = vec3Lerp(p1, p3, 0.5) - delta
			local rectEndPoint = vec3Lerp(p1, p3, 0.5) + delta
			
			if PolyEditor.mode == MODEL_MODE then
				selectInRectModel(rectStartPoint, rectEndPoint, PolyEditor.mousePressedInViewport)
			elseif PolyEditor.mode == TEXTURE_MODE then
				selectInRectUV(rectStartPoint, rectEndPoint, PolyEditor.mousePressedInViewport)
			end
		end
	end
end

function selectInRectUV(rectStartPoint, rectEndPoint, viewId)
	-- UV ids enclosed by the box
	local enclosed = {}

	for quadId, v in pairs(PolyEditor.editingUVQuads) do
		for i = 1, 4 do
			local vId = ((quadId - 1) * 4) + i

			local uv = vec2(PolyEditor.currentMeshUVs[vId].x * PolyEditor.uvDispW, PolyEditor.currentMeshUVs[vId].y * PolyEditor.uvDispH)

			if uv.x > rectStartPoint.x and uv.x < rectEndPoint.x and
				uv.y > rectStartPoint.y and uv.y < rectEndPoint.y then

				enclosed[#enclosed + 1] = vId
			end
		end
	end

	local sel = {}

	if PolyEditor.ctrlKeyDown == true then
		-- remove the enclosed UVs from the existing selection
		local enclosedById = {}
		for i = 1, #enclosed do
			enclosedById[enclosed[i]] = true
		end

		for i = 1, #PolyEditor.selectedUVs do
			if enclosedById[PolyEditor.selectedUVs[i]] == nil then
				sel[#sel + 1] = PolyEditor.selectedUVs[i]
			end
		end
	elseif PolyEditor.shiftKeyDown == true then
		-- add the enclosed UVs to the existing selection
		for i = 1, #PolyEditor.selectedUVs do
			sel[#sel + 1] = PolyEditor.selectedUVs[i]
		end
		for i = 1, #enclosed do
			sel[#sel + 1] = enclosed[i]
		end
	else
		sel = enclosed
	end

	setSelectedUVs(sel)
	storeUndo()
end

function selectInRectModel(rectStartPoint, rectEndPoint, viewId)
	-- box-select gathers vertices from every selected layer at once
	_syncSelectedLayerCaches()

	for layerIt = 1, #PolyEditor.selectedLayers do
		local layerId = PolyEditor.selectedLayers[layerIt]
		local meshData = PolyEditor.meshes[layerId]

		if meshData ~= nil and meshData.cache ~= nil then
			local quads = meshData.cache.quads
			local pivot = _layerPivotPos(layerId)

			-- vert indices enclosed by the box
			local enclosed = {}

			if viewId == 1 then
				for i = 1, #quads do
					local vertPos = quads[i] + pivot

					if vertPos.x > rectStartPoint.x and vertPos.x < rectEndPoint.x and
						vertPos.z > rectStartPoint.z and vertPos.z < rectEndPoint.z then

						enclosed[#enclosed + 1] = i
					end
				end
			elseif viewId == 3 then
				for i = 1, #quads do
					local vertPos = quads[i] + pivot

					if vertPos.x > rectStartPoint.x and vertPos.x < rectEndPoint.x and
						vertPos.y > rectStartPoint.y and vertPos.y < rectEndPoint.y then

						enclosed[#enclosed + 1] = i
					end
				end
			elseif viewId == 4 then
				for i = 1, #quads do
					local vertPos = quads[i] + pivot

					if vertPos.z > rectStartPoint.z and vertPos.z < rectEndPoint.z and
						vertPos.y > rectStartPoint.y and vertPos.y < rectEndPoint.y then

						enclosed[#enclosed + 1] = i
					end
				end
			end

			local sel = {}
			local store = PolyEditor.selectedVertsByLayer[layerId]

			if PolyEditor.ctrlKeyDown == true then
				-- remove the enclosed verts from the existing selection
				local enclosedById = {}
				for i = 1, #enclosed do
					enclosedById[enclosed[i]] = true
				end

				if store ~= nil then
					for i = 1, #store.verts do
						if enclosedById[store.verts[i]] == nil then
							sel[#sel + 1] = store.verts[i]
						end
					end
				end
			elseif PolyEditor.shiftKeyDown == true then
				-- add the enclosed verts to the existing selection
				if store ~= nil then
					for i = 1, #store.verts do
						sel[#sel + 1] = store.verts[i]
					end
				end
				for i = 1, #enclosed do
					sel[#sel + 1] = enclosed[i]
				end
			else
				sel = enclosed
			end

			setSelectedVertsForLayer(layerId, sel)
		end
	end

	storeUndo(PolyEditor.selectedLayers)
end

function putBrushPixels(textureData, pixelPosX, pixelPosY, brushRadius, color)
	local numBrushPixels = 1

	if brushRadius > 1 then
		numBrushPixels = brushRadius * 2
	end

	if numBrushPixels == 1 then
		local pixelX = round(pixelPosX - brushRadius)
		local pixelY = round(pixelPosY - brushRadius)

		local minX = 0
		local maxX = 127
		local minY = 1
		local maxY = 128

		if PolyEditor.textureDrawMagnified == true then
			minX = round((PolyEditor.uvRegionPos.x) * 128)
			maxX = round((PolyEditor.uvRegionPos.x + PolyEditor.uvRegionSize.x) * 128) - 1
			minY = 128 - round((PolyEditor.uvRegionPos.y + PolyEditor.uvRegionSize.y) * 128) + 1
			maxY = 128 - round((PolyEditor.uvRegionPos.y) * 128)
		end

		pixelX = clamp(pixelX, minX, maxX)
		pixelY = clamp(pixelY, minY, maxY)

		local pixelCoord = round((((128 - pixelY) * 128) + pixelX) * 4) + 1

		textureData[pixelCoord] = round(color.x)
		textureData[pixelCoord + 1] = round(color.y)
		textureData[pixelCoord + 2] = round(color.z)
		textureData[pixelCoord + 3] = round(color.w)

		return
	else
		local minX = 0
		local maxX = 127

		local minY = 1
		local maxY = 128

		if PolyEditor.textureDrawMagnified == true then
			minX = round((PolyEditor.uvRegionPos.x) * 128)
			maxX = round((PolyEditor.uvRegionPos.x + PolyEditor.uvRegionSize.x) * 128) - 1

			minY = 128 - round((PolyEditor.uvRegionPos.y + PolyEditor.uvRegionSize.y) * 128) + 1
			maxY = 128 - round((PolyEditor.uvRegionPos.y) * 128)
		end

		for x = 1, numBrushPixels do
			for y = 1, numBrushPixels do
				local pixelX = clamp((pixelPosX - brushRadius) + x, minX, maxX)
				local pixelY = clamp((pixelPosY - brushRadius) + y, minY, maxY)

				local pixelCoord = round((((128 - pixelY) * 128) + pixelX) * 4) + 1

				local includePixel = vec2Length(vec2(pixelX, pixelY) - vec2(pixelPosX, pixelPosY)) < brushRadius

				if includePixel then
					textureData[pixelCoord] = round(color.x)
					textureData[pixelCoord + 1] = round(color.y)
					textureData[pixelCoord + 2] = round(color.z)
					textureData[pixelCoord + 3] = round(color.w)
				end
			end
		end
	end
end

function getTextureToolPixelPos()
	local screenPos = vec2Copy(PolyEditor.mousePos)

	if PolyEditor.previewBrushSize == true then
		screenPos.x = PolyEditor.view4SpriteData.position.x + (PolyEditor.view4SpriteData.size.x * 0.5)
		screenPos.y = PolyEditor.view4SpriteData.position.y + (PolyEditor.view4SpriteData.size.y * 0.5)
	end
	
	local ortho = true
	local fov = - PolyEditor.camManipView4.focusDist
	local mouseInViewport = viewportToScreen(screenPos, PolyEditor.view4SpriteData, ortho)
	local ray = screenRay(mouseInViewport, fov, PolyEditor.camView4Mat, ortho)
	local p = ray.origin + (ray.direction * intersectPlane(ray.origin, vec3(1.0, 0.0, 0.0), vec3(1.0, 0.0, 0.0), - vec3(1.0, 0.0, 0.0)))

	local uvX = 0
	local uvY = 0

	local clampXMin = 1.0
	local clampXMax = 128.0

	local clampYMin = 2.0
	local clampYMax = 129.0

	if PolyEditor.textureDrawMagnified == true then
		-- Square-pixel magnified quad: world extents scale with dispW/dispH, not a fixed 128.
		uvX = clamp((PolyEditor.uvDispW * 0.5 - p.z) / PolyEditor.uvDispW, 0.0, 1.0)
		uvY = clamp((p.y + PolyEditor.uvDispH * 0.5) / PolyEditor.uvDispH, 0.0, 1.0)

		uvX = remapRange(uvX, 0.0, 1.0, PolyEditor.uvRegionPos.x, PolyEditor.uvRegionPos.x + PolyEditor.uvRegionSize.x) + ((1.0 / 128.0) * 0.5)
		uvY = remapRange(uvY, 1.0, 0.0, 1.0 - PolyEditor.uvRegionPos.y, 1.0 - (PolyEditor.uvRegionPos.y + PolyEditor.uvRegionSize.y)) + ((1.0 / 128.0) * 0.5)
		
		clampXMin = PolyEditor.uvRegionPos.x * 128.0
		clampXMax = ((PolyEditor.uvRegionPos.x + PolyEditor.uvRegionSize.x) * 128.0)

		clampYMin = 128.0 - ((PolyEditor.uvRegionPos.y + PolyEditor.uvRegionSize.y) * 128.0) + 2
		clampYMax = 128.0 - (PolyEditor.uvRegionPos.y * 128.0) + 1
	else
		-- Full texture fills the 128-unit quad, so 1 world unit == 1 texel; snap to the texel grid.
		local pixelSize = 1.0
		p.y = round((p.y + 1.5) / pixelSize)
		p.z = round((p.z + 0.5) / pixelSize)

		uvX = ((128.0 - (p.z + 64.0)) / 128.0) + (1.0 / 128.0)
		uvY = (p.y + 64.0) / 128.0
	end

	local pixelPosX = round(uvX * 128.0)
	local pixelPosY = round(uvY * 128.0)
	
	pixelPosX = clamp(pixelPosX, clampXMin, clampXMax)
	pixelPosY = clamp(pixelPosY, clampYMin, clampYMax)

	return vec2(pixelPosX, pixelPosY)
end

function updatePaintToolPencil(erase)
	if PolyEditor.mouseInViewport == 4 then
		UILib.setCursor(UILib.CURSOR_PIXEL)
	end

	if PolyEditor.previewingPaint == true and PolyEditorUI.brushSizePanel.visible == false then
		clearPreviewPaint()

		PolyEditor.previewingPaint = false
	end

	local pass = true

	if PolyEditor.mouseInViewport ~= 4 then
		pass = false
	end

	if PolyEditor.mouseDown == MOUSE_BUTTON_LEFT and PolyEditor.mousePressedInViewport == 4 then
		pass = true
	end

	if PolyEditor.previewBrushSize == true then
		pass = true
	end

	if pass == false then
		return
	end

	local pixelPos = getTextureToolPixelPos()

	local textureDataTmp = {table.unpack(PolyEditor.textureData)}

	local color = PolyEditor.texturePencilColor
	
	putBrushPixels(textureDataTmp, pixelPos.x, pixelPos.y, PolyEditor.paintBrushRadius, color)
	PolyEditor.previewingPaint = true

	setTexture(PolyEditor.currentPageId, textureDataTmp)

	local mousePressed = false
	if PolyEditor.mouseDown == MOUSE_BUTTON_LEFT then
		mousePressed = true
	end

	if PolyEditor.mousePressed == MOUSE_BUTTON_LEFT then
		mousePressed = true
	end

	if mousePressed and UILib.isHovered() == false then
		if PolyEditor.oldPixelPos.x == -1.0 then
			PolyEditor.oldPixelPos.x = pixelPos.x
			PolyEditor.oldPixelPos.y = pixelPos.y
		end

		rasterLine(PolyEditor.textureData,
			pixelPos.x, pixelPos.y, 
			round(PolyEditor.oldPixelPos.x), round(PolyEditor.oldPixelPos.y), 
			color)

		setTexture(PolyEditor.currentPageId, PolyEditor.textureData)

		PolyEditor.oldPixelPos.x = pixelPos.x
		PolyEditor.oldPixelPos.y = pixelPos.y
	end

	if PolyEditor.mouseReleased == MOUSE_BUTTON_LEFT then
		PolyEditor.oldPixelPos.x = -1.0
		PolyEditor.oldPixelPos.y = -1.0
		
		storeUndo()
	end
end

function clearPreviewPaint()
	setTexture(PolyEditor.currentPageId, PolyEditor.textureData)
end

function bucketPropagate(pixelX, pixelY, sourceColor, destColor)
	local clampXMin = 0.0
	local clampXMax = 127.0

	local clampYMin = 1.0
	local clampYMax = 128.0

	if PolyEditor.textureDrawMagnified == true then
		clampXMin = (PolyEditor.uvRegionPos.x * 128.0) - 1
		clampXMax = ((PolyEditor.uvRegionPos.x + PolyEditor.uvRegionSize.x) * 128.0) - 1.0

		clampYMin = 128.0 - ((PolyEditor.uvRegionPos.y + PolyEditor.uvRegionSize.y) * 128.0) + 1
		clampYMax = 128.0 - (PolyEditor.uvRegionPos.y * 128.0)
	end


	if pixelX < clampXMin then
		return
	end

	if pixelX > clampXMax then
		return
	end

	if pixelY < clampYMin then
		return
	end

	if pixelY > clampYMax then
		return
	end

	if sourceColor == destColor then
		return
	end

	local pixelCoord = round((((128 - pixelY) * 128) + pixelX) * 4) + 1

	if PolyEditor.textureData[pixelCoord] == round(destColor.x) and
		PolyEditor.textureData[pixelCoord + 1] == round(destColor.y) and
		PolyEditor.textureData[pixelCoord + 2] == round(destColor.z) and
		PolyEditor.textureData[pixelCoord + 3] == round(destColor.w) then

		return
	end

	if PolyEditor.textureData[pixelCoord] ~= round(sourceColor.x) or
		PolyEditor.textureData[pixelCoord + 1] ~= round(sourceColor.y) or
		PolyEditor.textureData[pixelCoord + 2] ~= round(sourceColor.z) or
		PolyEditor.textureData[pixelCoord + 3] ~= round(sourceColor.w) then

		return
	end
	
	PolyEditor.textureData[pixelCoord] = round(destColor.x)
	PolyEditor.textureData[pixelCoord + 1] = round(destColor.y)
	PolyEditor.textureData[pixelCoord + 2] = round(destColor.z)
	PolyEditor.textureData[pixelCoord + 3] = round(destColor.w)

	bucketPropagate(pixelX - 1, pixelY, sourceColor, destColor)
	bucketPropagate(pixelX + 1, pixelY, sourceColor, destColor)
	bucketPropagate(pixelX, pixelY - 1, sourceColor, destColor)
	bucketPropagate(pixelX, pixelY + 1, sourceColor, destColor)
end

function rasterLine(textureData, x0, y0, x1, y1, color)
    local dx = abs(x1 - x0)
    local dy = abs(y1 - y0)
    
    local sx = 0
    if x0 < x1 then
    	sx = 1
    else
    	sx = -1
    end

 	local sy = 0
    if y0 < y1 then
    	sy = 1
    else
    	sy = -1
    end

    local err = dx - dy

    local maxSteps = round(vec2Length(vec2(x0, y0) - vec2(x1, y1)))

    for i = 0, maxSteps do
        putBrushPixels(textureData, round(x0), round(y0), PolyEditor.paintBrushRadius, color)

        if x0 == x1 and y0 == y1 then
            break
        end

        local e2 = 2 * err
        if e2 > -dy then
	        err = round(err - dy)
	        x0 = round(x0 + sx)
        end

        if e2 < dx then
            err = round(err + dx)
            y0 = round(y0 + sy)
       end
   end
end

function updatePaintToolLine()
	if PolyEditor.mouseInViewport == 4 then
		UILib.setCursor(UILib.CURSOR_PIXEL)
	end

	if PolyEditor.previewingPaint == true and PolyEditorUI.brushSizePanel.visible == false then
		clearPreviewPaint()

		PolyEditor.previewingPaint = false
	end

	local pass = true

	if PolyEditor.mouseInViewport ~= 4 then
		pass = false
	end

	if PolyEditor.mouseDown == MOUSE_BUTTON_LEFT and PolyEditor.mousePressedInViewport == 4 then
		pass = true
	end

	if PolyEditor.previewBrushSize == true then
		pass = true
	end

	if pass == false then
		return
	end

	local pixelPos = getTextureToolPixelPos()

	local textureDataTmp = {table.unpack(PolyEditor.textureData)}

	local color = PolyEditor.texturePencilColor
	
	local endPX = PolyEditor.oldPixelPos.x
	local endPY = PolyEditor.oldPixelPos.y
	if PolyEditor.oldPixelPos.x == -1.0 then
		endPX = pixelPos.x
		endPY = pixelPos.y
	end

	putBrushPixels(textureDataTmp, pixelPos.x, pixelPos.y, PolyEditor.paintBrushRadius, color)

	rasterLine(textureDataTmp,
			pixelPos.x, pixelPos.y, 
			endPX, endPY, 
			color)

	PolyEditor.previewingPaint = true

	setTexture(PolyEditor.currentPageId, textureDataTmp)

	if PolyEditor.mousePressed == MOUSE_BUTTON_LEFT and UILib.isHovered() == false then
		PolyEditor.oldPixelPos.x = pixelPos.x
		PolyEditor.oldPixelPos.y = pixelPos.y
	end

	if PolyEditor.mouseReleased == MOUSE_BUTTON_LEFT then
		PolyEditor.oldPixelPos.x = -1.0
		PolyEditor.oldPixelPos.y = -1.0

		for i = 1, #PolyEditor.textureData do
			PolyEditor.textureData[i] = textureDataTmp[i]
		end

		setTexture(PolyEditor.currentPageId, PolyEditor.textureData)

		storeUndo()
	end
end

function updatePaintToolRectFill()
	if PolyEditor.previewingPaint == true and PolyEditorUI.brushSizePanel.visible == false then
		clearPreviewPaint()

		PolyEditor.previewingPaint = false
	end

	if PolyEditor.mouseInViewport ~= 4 or UILib.isHovered() == true then
		return
	end

	UILib.setCursor(UILib.CURSOR_PIXEL)

	local pixelPos = getTextureToolPixelPos()
	local pixelPosX = round(pixelPos.x)
	local pixelPosY = round(pixelPos.y)

	local textureDataTmp = {table.unpack(PolyEditor.textureData)}

	putBrushPixels(textureDataTmp, pixelPosX, pixelPosY, 1, PolyEditor.texturePencilColor)

	if PolyEditor.mousePressed == MOUSE_BUTTON_LEFT then
		PolyEditor.oldPixelPos.x = pixelPosX
		PolyEditor.oldPixelPos.y = pixelPosY
	end

	if PolyEditor.mouseDown == MOUSE_BUTTON_LEFT then
		local numX = round(abs(PolyEditor.oldPixelPos.x - pixelPosX))
		local numY = round(abs(PolyEditor.oldPixelPos.y - pixelPosY))

		local startX = round(PolyEditor.oldPixelPos.x)
		local startY = round(PolyEditor.oldPixelPos.y)
		local endX = pixelPosX
		local endY = pixelPosY

		if endX < startX then
			startX = pixelPosX
		end

		if endY > startY then
			startY = pixelPosY
		end

		for x = 0, numX do
			putBrushPixels(textureDataTmp, startX + x, startY, 1, PolyEditor.texturePencilColor)
		end

		for x = 0, numX do
			putBrushPixels(textureDataTmp, startX + x, startY - numY, 1, PolyEditor.texturePencilColor)
		end

		for y = 0, numY do
			putBrushPixels(textureDataTmp, startX, startY - y, 1, PolyEditor.texturePencilColor)
		end

		for y = 0, numY do
			putBrushPixels(textureDataTmp, startX + numX, startY - y, 1, PolyEditor.texturePencilColor)
		end
	end

	PolyEditor.previewingPaint = true

	setTexture(PolyEditor.currentPageId, textureDataTmp)

	if PolyEditor.mouseReleased == MOUSE_BUTTON_LEFT then
		local numX = round(abs(PolyEditor.oldPixelPos.x - pixelPosX))
		local numY = round(abs(PolyEditor.oldPixelPos.y - pixelPosY)) + 1
		
		local startX = round(PolyEditor.oldPixelPos.x)
		local startY = round(PolyEditor.oldPixelPos.y)
		local endX = pixelPosX
		local endY = pixelPosY

		if endX < startX then
			startX = pixelPosX
		end

		if endY > startY then
			startY = pixelPosY
		end

		if numX > 0 and numY > 0 then
			for y = 1, numY do
				local t = (1.0 / numY) * y

				for x = 0, numX do
					local px = round(startX) + x - 1
					local py = round(startY) - y

					local pixelCoord = round((((128 - py) * 128) + px) * 4) + 1

					PolyEditor.textureData[pixelCoord] = round(PolyEditor.texturePencilColor.x)
					PolyEditor.textureData[pixelCoord + 1] = round(PolyEditor.texturePencilColor.y)
					PolyEditor.textureData[pixelCoord + 2] = round(PolyEditor.texturePencilColor.z)
					PolyEditor.textureData[pixelCoord + 3] = round(PolyEditor.texturePencilColor.w)
				end
			end
			
			setTexture(PolyEditor.currentPageId, PolyEditor.textureData)
		end

		PolyEditor.oldPixelPos.x = -1.0
		PolyEditor.oldPixelPos.y = -1.0	

		PolyEditor.previewingPaint = false
		
		storeUndo()
	end
end

function updatePaintToolBucket()
	if PolyEditor.previewingPaint == true and PolyEditorUI.brushSizePanel.visible == false then
		clearPreviewPaint()
		
		PolyEditor.previewingPaint = false
	end

	if PolyEditor.mouseInViewport ~= 4 or UILib.isHovered() == true then
		return
	end
	
	UILib.setCursor(UILib.CURSOR_PIXEL)

	local pixelPos = getTextureToolPixelPos()
	local pixelPosX = round(pixelPos.x)
	local pixelPosY = round(pixelPos.y)

	local textureDataTmp = {table.unpack(PolyEditor.textureData)}

	if PolyEditor.previewPencilFlashingColor == nil then
		PolyEditor.previewPencilFlashingColor = vec4(255, 255, 255, 255)
		PolyEditor.lastPreviewPencilTime = PolyEditor.time
	end

	if PolyEditor.time - PolyEditor.lastPreviewPencilTime > 0.05 then
		local rectPulse = 255 * ((sin(PolyEditor.time * 50) + 1.0) * 0.5)
		PolyEditor.previewPencilFlashingColor = vec4(rectPulse, rectPulse, rectPulse, 255)

		PolyEditor.lastPreviewPencilTime = PolyEditor.time
	end

	PolyEditor.previewingPaint = true
	putBrushPixels(textureDataTmp, pixelPosX, pixelPosY, 1, PolyEditor.previewPencilFlashingColor)

	setTexture(PolyEditor.currentPageId, textureDataTmp)

	if PolyEditor.mousePressed == MOUSE_BUTTON_LEFT then
		local pixelPos = getTextureToolPixelPos()
		local pixelPosX = round(pixelPos.x) - 1
		local pixelPosY = round(pixelPos.y) - 1

		local pixelCoord = round((((128 - pixelPosY) * 128) + pixelPosX) * 4) + 1
		
		local pixelColor = vec4(
			PolyEditor.textureData[pixelCoord],
			PolyEditor.textureData[pixelCoord + 1],
			PolyEditor.textureData[pixelCoord + 2],
			PolyEditor.textureData[pixelCoord + 3])

		bucketPropagate(pixelPosX, pixelPosY, pixelColor, PolyEditor.texturePencilColor)

		setTexture(PolyEditor.currentPageId, PolyEditor.textureData)

		storeUndo()
	end
end

function updatePaintToolStamp()
	if PolyEditor.previewingPaint == true and PolyEditorUI.brushSizePanel.visible == false then
		clearPreviewPaint()

		PolyEditor.previewingPaint = false
	end

	if PolyEditor.mouseInViewport ~= 4 or UILib.isHovered() == true then
		return
	end

	UILib.setCursor(UILib.CURSOR_PIXEL)
	
	local pixelPos = getTextureToolPixelPos()
	local pixelPosX = round(pixelPos.x)
	local pixelPosY = round(pixelPos.y)

	local textureDataTmp = {table.unpack(PolyEditor.textureData)}

	if PolyEditor.previewPencilFlashingColor == nil then
		PolyEditor.previewPencilFlashingColor = vec4(255, 255, 255, 255)
		PolyEditor.lastPreviewPencilTime = PolyEditor.time
	end

	if PolyEditor.time - PolyEditor.lastPreviewPencilTime > 0.05 then
		local rectPulse = 255 * ((sin(PolyEditor.time * 50) + 1.0) * 0.5)
		PolyEditor.previewPencilFlashingColor = vec4(rectPulse, rectPulse, rectPulse, 255)

		PolyEditor.lastPreviewPencilTime = PolyEditor.time
	end

	if PolyEditor.textureStamp ~= nil then
		for x = 0, PolyEditor.textureStampNumX do
			for y = 0, PolyEditor.textureStampNumY do
				local px = pixelPosX + (x - 1)
				local py = pixelPosY - y - 1

				if px >= 0 and px <= 127 and py >= 1 and py <= 128 then
					local pixelCoord = round((((128 - py) * 128) + px) * 4) + 1
					local k = round((((128 - y) * 128) + x) * 4) + 1

					textureDataTmp[pixelCoord] = PolyEditor.textureStamp[k]
					textureDataTmp[pixelCoord + 1] = PolyEditor.textureStamp[k + 1]
					textureDataTmp[pixelCoord + 2] = PolyEditor.textureStamp[k + 2]
					textureDataTmp[pixelCoord + 3] = PolyEditor.textureStamp[k + 3]
				end
			end
		end
	else
		putBrushPixels(textureDataTmp, pixelPosX, pixelPosY, 1, PolyEditor.previewPencilFlashingColor)

		if PolyEditor.mousePressed == MOUSE_BUTTON_LEFT then
			PolyEditor.oldPixelPos.x = pixelPosX
			PolyEditor.oldPixelPos.y = pixelPosY	
		end

		if PolyEditor.mouseDown == MOUSE_BUTTON_LEFT then
			local numX = round(abs(PolyEditor.oldPixelPos.x - pixelPosX))
			local numY = round(abs(PolyEditor.oldPixelPos.y - pixelPosY))

			local startX = round(PolyEditor.oldPixelPos.x)
			local startY = round(PolyEditor.oldPixelPos.y)
			local endX = pixelPosX
			local endY = pixelPosY

			if endX < startX then
				startX = pixelPosX
			end

			if endY > startY then
				startY = pixelPosY
			end

			for x = 0, numX do
				putBrushPixels(textureDataTmp, startX + x, startY, 1, PolyEditor.previewPencilFlashingColor)
			end

			for x = 0, numX do
				putBrushPixels(textureDataTmp, startX + x, startY - numY, 1, PolyEditor.previewPencilFlashingColor)
			end

			for y = 0, numY do
				putBrushPixels(textureDataTmp, startX, startY - y, 1, PolyEditor.previewPencilFlashingColor)
			end

			for y = 0, numY do
				putBrushPixels(textureDataTmp, startX + numX, startY - y, 1, PolyEditor.previewPencilFlashingColor)
			end
		end
	end

	PolyEditor.previewingPaint = true

	setTexture(PolyEditor.currentPageId, textureDataTmp)

	if PolyEditor.mousePressed == MOUSE_BUTTON_LEFT and PolyEditor.textureStamp ~= nil then
		for i = 1, #PolyEditor.textureData do
			PolyEditor.textureData[i] = textureDataTmp[i]
		end

		PolyEditor.oldPixelPos.x = -1.0
		PolyEditor.oldPixelPos.y = -1.0

		PolyEditor.textureStamp = nil
		PolyEditor.textureStampNumX = 0
		PolyEditor.textureStampNumY = 0

		PolyEditor.previewingPaint = false

		return
	end

	if PolyEditor.mouseReleased == MOUSE_BUTTON_LEFT and PolyEditor.oldPixelPos.x ~= -1.0 then
		local numX = round(abs(PolyEditor.oldPixelPos.x - pixelPosX))
		local numY = round(abs(PolyEditor.oldPixelPos.y - pixelPosY)) + 1

		if numX == 0 or numY == 0 then
			PolyEditor.oldPixelPos.x = -1.0
			PolyEditor.oldPixelPos.y = -1.0

			return
		end

		local startX = round(PolyEditor.oldPixelPos.x)
		local startY = round(PolyEditor.oldPixelPos.y)
		local endX = pixelPosX
		local endY = pixelPosY

		if endX < startX then
			startX = pixelPosX
		end

		if endY > startY then
			startY = pixelPosY
		end

		PolyEditor.textureStamp = {}
		PolyEditor.textureStampNumX = numX
		PolyEditor.textureStampNumY = numY - 1
		
		for x = 0, numX do
			for y = 0, numY do
				local px = round(startX + x - 1)
				local py = round(startY - y - 1)

				if px >= 0 and px <= 127 and py >= 1 and py <= 128 then
					local pixelCoord = round((((128 - py) * 128) + px) * 4) + 1
					local k = round((((128 - y) * 128) + x) * 4) + 1

					PolyEditor.textureStamp[k] = PolyEditor.textureData[pixelCoord]
					PolyEditor.textureStamp[k + 1] = PolyEditor.textureData[pixelCoord + 1]
					PolyEditor.textureStamp[k + 2] = PolyEditor.textureData[pixelCoord + 2]
					PolyEditor.textureStamp[k + 3] = PolyEditor.textureData[pixelCoord + 3]
				end
			end
		end

		PolyEditor.oldPixelPos.x = -1.0
		PolyEditor.oldPixelPos.y = -1.0

		storeUndo()
	end
end

function updatePaintToolGradient()
	if PolyEditor.previewingPaint == true and PolyEditorUI.brushSizePanel.visible == false then
		clearPreviewPaint()

		PolyEditor.previewingPaint = false
	end

	if PolyEditor.mouseInViewport ~= 4 or UILib.isHovered() == true then
		return
	end

	UILib.setCursor(UILib.CURSOR_PIXEL)

	local pixelPos = getTextureToolPixelPos()
	local pixelPosX = round(pixelPos.x)
	local pixelPosY = round(pixelPos.y)

	local textureDataTmp = {table.unpack(PolyEditor.textureData)}

	if PolyEditor.time - PolyEditor.lastPreviewPencilTime > 0.05 then
		local rectPulse = 255 * ((sin(PolyEditor.time * 50) + 1.0) * 0.5)
		PolyEditor.previewPencilFlashingColor = vec4(rectPulse, rectPulse, rectPulse, 255)

		PolyEditor.lastPreviewPencilTime = PolyEditor.time
	end

	putBrushPixels(textureDataTmp, pixelPosX, pixelPosY, 1, PolyEditor.previewPencilFlashingColor)

	if PolyEditor.mousePressed == MOUSE_BUTTON_LEFT then
		PolyEditor.oldPixelPos.x = pixelPosX
		PolyEditor.oldPixelPos.y = pixelPosY
	end

	if PolyEditor.mouseDown == MOUSE_BUTTON_LEFT then
		local numX = round(abs(PolyEditor.oldPixelPos.x - pixelPosX))
		local numY = round(abs(PolyEditor.oldPixelPos.y - pixelPosY))

		local startX = round(PolyEditor.oldPixelPos.x)
		local startY = round(PolyEditor.oldPixelPos.y)
		local endX = pixelPosX
		local endY = pixelPosY

		if endX < startX then
			startX = pixelPosX
		end

		if endY > startY then
			startY = pixelPosY
		end

		for x = 0, numX do
			putBrushPixels(textureDataTmp, startX + x, startY, 1, PolyEditor.previewPencilFlashingColor)
		end

		for x = 0, numX do
			putBrushPixels(textureDataTmp, startX + x, startY - numY, 1, PolyEditor.previewPencilFlashingColor)
		end

		for y = 0, numY do
			putBrushPixels(textureDataTmp, startX, startY - y, 1, PolyEditor.previewPencilFlashingColor)
		end

		for y = 0, numY do
			putBrushPixels(textureDataTmp, startX + numX, startY - y, 1, PolyEditor.previewPencilFlashingColor)
		end
	end

	PolyEditor.previewingPaint = true

	setTexture(PolyEditor.currentPageId, textureDataTmp)

	if PolyEditor.mouseReleased == MOUSE_BUTTON_LEFT then
		local numX = round(abs(PolyEditor.oldPixelPos.x - pixelPosX))
		local numY = round(abs(PolyEditor.oldPixelPos.y - pixelPosY)) + 1

		local startX = round(PolyEditor.oldPixelPos.x)
		local startY = round(PolyEditor.oldPixelPos.y)
		local endX = pixelPosX
		local endY = pixelPosY

		if endX < startX then
			startX = pixelPosX
		end

		if endY > startY then
			startY = pixelPosY
		end

		if numX > 0 and numY > 0 then
			for y = 1, numY do
				local t = (1.0 / (numY - 1)) * (y - 1)

				for x = 0, numX do
					local px = round(startX) + x - 1
					local py = round(startY) - y
					
					local pixelCoord = round((((128 - py) * 128) + px) * 4) + 1

					PolyEditor.textureData[pixelCoord] = round(lerp(PolyEditor.texturePencilColor.x, PolyEditor.textureData[pixelCoord], t))
					PolyEditor.textureData[pixelCoord + 1] = round(lerp(PolyEditor.texturePencilColor.y, PolyEditor.textureData[pixelCoord + 1], t))
					PolyEditor.textureData[pixelCoord + 2] = round(lerp(PolyEditor.texturePencilColor.z, PolyEditor.textureData[pixelCoord + 2], t))
					PolyEditor.textureData[pixelCoord + 3] = round(lerp(PolyEditor.texturePencilColor.w, PolyEditor.textureData[pixelCoord + 3], t))
				end
			end

			setTexture(PolyEditor.currentPageId, PolyEditor.textureData)
		end

		PolyEditor.oldPixelPos.x = -1.0
		PolyEditor.oldPixelPos.y = -1.0	

		PolyEditor.previewingPaint = false
		
		storeUndo()
	end
end

function updatePaintTools(viewId)
	if viewId == 4 then
		if PolyEditor.previewingPaint == true and PolyEditorUI.brushSizePanel.visible == false then
			clearPreviewPaint()

			PolyEditor.previewingPaint = false
		end
	end

	if PolyEditor.mouseInViewport == 2 and viewId == 2 then
		if PolyEditor.mousePressedInViewport ~= -1 and PolyEditor.mousePressedInViewport ~= 2 then
			return
		end

		if PolyEditor.activePaintTool == "vertexColorPick" then
			updateVertexColorPick()
		end

		return
	end
	
	if PolyEditor.mouseInViewport == 4 and viewId == 4 then
		if PolyEditor.mousePressedInViewport ~= -1 and PolyEditor.mousePressedInViewport ~= 4 then
			return
		end

		if PolyEditor.activePaintTool == "pencil" then
			updatePaintToolPencil()
		elseif PolyEditor.activePaintTool == "line" then
			updatePaintToolLine()
		elseif PolyEditor.activePaintTool == "rect fill" then
			updatePaintToolRectFill()
		elseif PolyEditor.activePaintTool == "bucket" then
			updatePaintToolBucket()
		elseif PolyEditor.activePaintTool == "stamp" then
			updatePaintToolStamp()
		elseif PolyEditor.activePaintTool == "gradient" then
			updatePaintToolGradient()
		elseif PolyEditor.activePaintTool == "textureColorPick" then
			updatePaintColorPick()
		elseif PolyEditor.activePaintTool == "vertexColorPick" then
			updateVertexColorPick()
		end
	end
end

function updateVertexColorPick()
	if PolyEditor.previewingPaint == true and PolyEditorUI.brushSizePanel.visible == false then
		clearPreviewPaint()

		PolyEditor.previewingPaint = false
	end
	
	if PolyEditor.mouseReleased == MOUSE_BUTTON_LEFT then
		setActiveTool("select", TEXTURE_MODE)

		PolyEditorUI.colorPicker.updateColorCallback = PolyEditor.vertexPickColorCallback

		PolyEditorUI.colorPicker:endPicking()

		if #PolyEditor.pickedVerts > 0 or PolyEditor.mouseInViewport == 4 then
			local newColor = vec4(PolyEditor.currentVertexColor)

			setSelectedVerts(PolyEditor.vertexPickColorSelection)

			PolyEditor.setVertexColor(newColor)

			storeUndo(PolyEditor.colorAffectedLayerIds)
		end

		PolyEditor.skipClick = true

		return
	end

	if UILib.isHovered() == true then
		return
	end

	if PolyEditor.mouseInViewport == 2 then
		if #PolyEditor.pickedVerts == 0 then
			return
		end

		local vertId = PolyEditor.pickedVerts[1]

		local pickedCache = _ensureLayerCache(PolyEditor.pickedLayerId or PolyEditor.currentLayerId)
		local col = (pickedCache ~= nil and pickedCache.colors[vertId]) or PolyEditor.currentMeshVertexColors[vertId]

		if col == nil then
			return
		end

		PolyEditor.currentVertexColor.x = col.x
		PolyEditor.currentVertexColor.y = col.y
		PolyEditor.currentVertexColor.z = col.z
		PolyEditor.currentVertexColor.w = col.w

		PolyEditorUI.colorPicker:setColor(PolyEditor.currentVertexColor)

	elseif PolyEditor.mouseInViewport == 4 then
		local pixelPos = getTextureToolPixelPos()
		local pixelPosX = round(pixelPos.x)
		local pixelPosY = round(pixelPos.y)

		local pixelCoord = round((((128 - pixelPosY) * 128) + pixelPosX) * 4)
		
		local r = PolyEditor.textureData[pixelCoord + 1]
		local g = PolyEditor.textureData[pixelCoord + 2]
		local b = PolyEditor.textureData[pixelCoord + 3]
		local a = PolyEditor.textureData[pixelCoord + 4]
		
		if r and g and b and a then
			PolyEditor.currentVertexColor.x = PolyEditor.textureData[pixelCoord + 1]
			PolyEditor.currentVertexColor.y = PolyEditor.textureData[pixelCoord + 2]
			PolyEditor.currentVertexColor.z = PolyEditor.textureData[pixelCoord + 3]
			PolyEditor.currentVertexColor.w = PolyEditor.textureData[pixelCoord + 4]
		end

		PolyEditorUI.colorPicker:setColor(PolyEditor.currentVertexColor)
	end
end

function updatePaintColorPick()
	if PolyEditor.mouseInViewport == 4 then
		UILib.setCursor(UILib.CURSOR_PIXEL)
	end

	if PolyEditor.previewingPaint == true and PolyEditorUI.brushSizePanel.visible == false then
		clearPreviewPaint()

		PolyEditor.previewingPaint = false
	end

	if PolyEditor.mouseInViewport ~= 4 or UILib.isHovered() == true then
		if PolyEditor.mouseReleased == MOUSE_BUTTON_LEFT then
			setActiveTool("pencil", TEXTURE_MODE)
		end

		return
	end

	local pixelPos = getTextureToolPixelPos()
	local pixelPosX = round(pixelPos.x) - 1
	local pixelPosY = round(pixelPos.y) - 1

	local pixelCoord = round((((128 - pixelPosY) * 128) + pixelPosX) * 4) + 1

	local r = PolyEditor.textureData[pixelCoord]
	local g = PolyEditor.textureData[pixelCoord + 1]
	local b = PolyEditor.textureData[pixelCoord + 2]
	local a = PolyEditor.textureData[pixelCoord + 3]

	if r and g and b and a then
		PolyEditor.texturePencilColor.x = PolyEditor.textureData[pixelCoord]
		PolyEditor.texturePencilColor.y = PolyEditor.textureData[pixelCoord + 1]
		PolyEditor.texturePencilColor.z = PolyEditor.textureData[pixelCoord + 2]
		PolyEditor.texturePencilColor.w = PolyEditor.textureData[pixelCoord + 3]
	end

	PolyEditorUI.colorPicker:setColor(PolyEditor.texturePencilColor)

	if PolyEditor.mouseReleased == MOUSE_BUTTON_LEFT then
		setActiveTool(PolyEditor.previousActiveTool, TEXTURE_MODE)

		PolyEditorUI.colorPicker:endPicking()
	end
end

function PolyEditor.texturePickColorStart()
	PolyEditor.previousActiveTool = PolyEditor.activeTool

	setActiveTool("textureColorPick", TEXTURE_MODE)
end

function PolyEditor.vertexPickColorStart()
	PolyEditor.vertexPickColorSelection = {table.unpack(PolyEditor.selectedVerts)}
	PolyEditor.vertexPickColorOld = vec4(PolyEditor.currentVertexColor)

	-- prevent callback from actually changing vertex colors
	PolyEditor.vertexPickColorCallback = PolyEditorUI.colorPicker.updateColorCallback
	PolyEditorUI.colorPicker.updateColorCallback = nil

	setActiveTool("vertexColorPick", TEXTURE_MODE)
end

function updateTextureTools()
	if PolyEditor.activeTool == "" then
		setActiveTool("select", MODEL_MODE)
	end

	if PolyEditor.mouseInViewport == -1 then
		return
	end

	if PolyEditor.activeTool ~= "select" then
		updateUVToolActivation()
	end

	selectToolUpdate()
	
	if PolyEditor.activeTool == "move uv" then
		moveUVToolUpdate()
	elseif PolyEditor.activeTool == "rotate uv" then
		rotateUVToolUpdate()
	elseif PolyEditor.activeTool == "scale uv" then
		scaleUVToolUpdate()
	end
end

function updateModelTools()
	if PolyEditor.activeTool == "" then
		setActiveTool("select", MODEL_MODE)
	end

	if PolyEditor.mouseInViewport == -1 then
		return
	end

	if PolyEditor.activeTool ~= "select" then
		updateToolActivation()
	else
		PolyEditor.pickingSelection = pickedVertsInSelection(PolyEditor.pickedVerts, PolyEditor.selectedVerts)
		if PolyEditor.pickingSelection == false then
			local fullSearch = true
			PolyEditor.pickingSelection = pickedVertsInSelection(PolyEditor.hoveredVerts, PolyEditor.selectedVerts, fullSearch)
		end
	end

	selectToolUpdate()

	if PolyEditor.activeTool == "move" then
		moveToolUpdate()
	elseif PolyEditor.activeTool == "rotate" then
		rotateToolUpdate()
	elseif PolyEditor.activeTool == "scale" then
		scaleToolUpdate()
	elseif PolyEditor.activeTool == "new quad" then
		_quadToolUpdate()
	elseif PolyEditor.activeTool == "new cube" then
		_newCubeToolUpdate()
	elseif PolyEditor.activeTool == "new sphere" then
		_newSphereToolUpdate()
	elseif PolyEditor.activeTool == "new cylinder" then
		_newCylinderToolUpdate()
	elseif PolyEditor.activeTool == "new cone" then
		_newConeToolUpdate()
	elseif PolyEditor.activeTool == "split vertical" then
		_polySplitToolUpdate("vertical")
	elseif PolyEditor.activeTool == "split horizontal" then
		_polySplitToolUpdate("horizontal")
	elseif PolyEditor.activeTool == "mirror" then
		_mirrorToolUpdate()
	end

end

function movePivotToolUpdate()
	if PolyEditor.mouseInViewport == -1 then
		return
	end

	if PolyEditor.mouseInViewport == 2 then
		PolyEditor.pivotCenterSpriteData.color.w = 100.0
		setSprite(PolyEditor.pivotCenterSprite, PolyEditor.pivotCenterSpriteData)

		return
	end

	local ortho = true
	local fov = - PolyEditor.camManips[PolyEditor.mouseInViewport].focusDist

	local viewSpriteData = PolyEditor.viewsSpriteData[PolyEditor.mouseInViewport]

	local camMat = PolyEditor.camMats[PolyEditor.mouseInViewport]

	local mouseInViewport = viewportToScreen(PolyEditor.mousePos, viewSpriteData, ortho)
	local ray = screenRay(mouseInViewport, fov, camMat, ortho)

	local currentPivotPos = PolyEditor.pivotCenterSpriteData.position

	local p = ray.origin + (ray.direction * intersectPlane(ray.origin, ray.direction, currentPivotPos, - camMat.forwardAxis))

	local pos2d = worldToViewport(currentPivotPos, viewSpriteData)
	local mousePos2d = PolyEditor.mousePos

	local hoveringPivotCenter = false

	if PolyEditor.draggingPivotCenter == false then
		if vec2Length(pos2d - mousePos2d) < 10.0 then
			PolyEditor.pivotCenterSpriteData.color.w = 255.0
			hoveringPivotCenter = true
		else
			PolyEditor.pivotCenterSpriteData.color.w = 100.0
		end
	end

	if PolyEditor.mousePressed == MOUSE_BUTTON_LEFT then
		if hoveringPivotCenter == true then
			PolyEditor.draggingPivotCenter = true
			PolyEditor.dragPivotOffset = currentPivotPos - p
			PolyEditor.pivotDragStartPos = vec3(currentPivotPos)
		end
	end

	if PolyEditor.draggingPivotCenter == true then
		local newP = p + PolyEditor.dragPivotOffset

		newP.x = QUANTIZE_STEP * round(newP.x / QUANTIZE_STEP)
		newP.y = QUANTIZE_STEP * round(newP.y / QUANTIZE_STEP)
		newP.z = QUANTIZE_STEP * round(newP.z / QUANTIZE_STEP)

		PolyEditor.pivotCenterSpriteData.position = newP

		local oldP = vec3(getModelWorldMatrix(PolyEditor.models[PolyEditor.currentLayerId]).position)
		local delta = newP - oldP

		if delta.x ~= 0.0 or delta.y ~= 0.0 or delta.z ~= 0.0 then
			for i = 1, #PolyEditor.currentMeshQuads do
				PolyEditor.currentMeshQuads[i] = PolyEditor.currentMeshQuads[i] - delta
			end

			local pivotMat = mat4()
			pivotMat.position = vec3(newP)
			setModelWorldMatrix(PolyEditor.models[PolyEditor.currentLayerId], pivotMat)

			refreshCurrentMesh()
		end
	end

	if PolyEditor.mouseReleased == MOUSE_BUTTON_LEFT then
		if PolyEditor.draggingPivotCenter == true then
			PolyEditor.draggingPivotCenter = false

			local newPivotPos = vec3(getModelWorldMatrix(PolyEditor.models[PolyEditor.currentLayerId]).position)

			if newPivotPos.x ~= PolyEditor.pivotDragStartPos.x or newPivotPos.y ~= PolyEditor.pivotDragStartPos.y or newPivotPos.z ~= PolyEditor.pivotDragStartPos.z then
				PolyEditor.hierarchyByLayerId[PolyEditor.currentLayerId].position = vec3(newPivotPos)
				_hierarchyChanged()
				storeUndo()
			end
		end
	end

	setSprite(PolyEditor.pivotCenterSprite, PolyEditor.pivotCenterSpriteData)
end

function updateRigTools()
	if PolyEditor.activeTool == "" then
		setActiveTool("move pivot", RIG_MODE)
	end

	if PolyEditor.activeTool == "move pivot" then
		movePivotToolUpdate()
	elseif PolyEditor.activeTool == "edit hierarchy" then
		
	end
end

function PolyEditor.movePivotToolStart()
	setActiveTool("move pivot", RIG_MODE)
	
	PolyEditorUI.hierarchyInspector:setVisible(false)

	PolyEditor.mouseOffset = vec2(0.0, 0.0)
end

function _hierarchyChanged()
	_refreshModelHierarchy(PolyEditor.hierarchy)
end

function PolyEditor.hierarchyEditorStart()
	if PolyEditor.activeTool == "edit hierarchy" then
		setActiveTool("move pivot")
		PolyEditorUI.hierarchyInspector:setVisible(false)

		return
	end

	setActiveTool("edit hierarchy", RIG_MODE)

	-- mark non empty layers with a highlight color
	for i = 1, MESH_LAYERS do
		local hierarchyEntry = PolyEditor.hierarchyByLayerId[i]
		if hierarchyEntry ~= nil then
			if PolyEditor.meshes[i] ~= nil then
				hierarchyEntry.color = vec4(Config.uiColors.accent)
			else
				hierarchyEntry.color = nil
			end
		end
	end

	PolyEditorUI.hierarchyInspector:inspect(PolyEditor.hierarchy)
	PolyEditorUI.hierarchyInspector:setVisible(true)
	_syncHierarchySelectionFlags()
	PolyEditorUI.hierarchyInspector.hierarchyChangeCallback = _hierarchyChanged
	PolyEditorUI.hierarchyInspector.nodeClickCallback = function(node)
		if getKeyDown(KEY_LEFT_SHIFT) or getKeyDown(KEY_LEFT_CONTROL) then
			PolyEditor.toggleLayerSelected(node.layerId)
		else
			PolyEditor.selectLayer(node.layerId)
		end
		-- _syncHierarchySelectionFlags (called from select/toggle) mirrors the
		-- selection onto node.selected and refreshes the inspector
	end
	PolyEditorUI.hierarchyInspector.nodeRenameCallback = function(node, newName)
		node.name = newName
		if node.layerId and PolyEditor.models[node.layerId] then
			setModelName(PolyEditor.models[node.layerId], newName)
		end
	end
end

function hasActiveTool()
	if PolyEditor.activeTool ~= "select" then
		return true
	end

	return false
end

-- Active-layer wrapper; per-layer body lives in refreshLayer.
function refreshCurrentMesh()
	refreshLayer(PolyEditor.currentLayerId)
end

function updateMeshLight(mesh)
	local lightDir = vec3Normalize(vec3(0.5, 1.0, 0.7))
	local lightColor = vec4(255.0, 255.0, 255.0, 255.0)
	local ambientColor = vec4(100.0, 100.0, 100.0, 255.0)

	local lightOnCamera = lightDir * PolyEditor.camManipView2.rot

	PolyUtil.meshLight(mesh, lightOnCamera, lightColor, ambientColor)
end

function setTextureRegion(posX, posY, sizeX, sizeY)
	PolyEditor.uvRegionPos.x = round(posX * 128) / 128.0
	PolyEditor.uvRegionPos.y = round(posY * 128) / 128.0
	PolyEditor.uvRegionSize.x = round(sizeX * 128) / 128.0
	PolyEditor.uvRegionSize.y = round(sizeY * 128) / 128.0
end

function updateTextureRegion()
	local clickedOutsideViewport = false
	if PolyEditor.clickedViewport ~= -1 and PolyEditor.clickedViewport ~= 1 then
		clickedOutsideViewport = true
	end

	local regionPos = PolyEditor.uvRegionPos * 128.0
	local regionSize = PolyEditor.uvRegionSize * 128.0

	local rectP1 = vec3(
		clamp(regionPos.x - (128.0 * 0.5), -64, 64),
		1.0, 
		clamp(regionPos.y - (128.0 * 0.5), -64, 64))

	local rectP2 =vec3(
		clamp(regionPos.x - (128.0 * 0.5) + regionSize.x, -64, 64), 
		1.0, 
		clamp(regionPos.y - (128.0 * 0.5), -64, 64))

	local rectP3 = vec3(
		clamp(regionPos.x - (128.0 * 0.5) + regionSize.x, -64, 64), 
		1.0, 
		clamp(regionPos.y +  regionSize.y - (128.0 * 0.5), -64, 64))

	local rectP4 = vec3(
		clamp(regionPos.x - (128.0 * 0.5), -64, 64), 
		1.0, 
		clamp(regionPos.y +  regionSize.y - (128.0 * 0.5), -64, 64))

	local uvRegionColor = Config.uiColors.polyEditorGrid

	if PolyEditor.mouseDown == -1 then
		if PolyEditor.editingUVRegion ~= -1 then
			storeUndo()
		end

		PolyEditor.editingUVRegion = -1
	end

	if PolyEditor.mouseInViewport == 1 then
		local mouseHoverHandle = -1
		local mouseHoverRegion = -1

		local fov = - PolyEditor.camManipView1.focusDist
		local ortho = true
		local mouseInViewport = viewportToScreen(PolyEditor.mousePos, PolyEditor.view1SpriteData, ortho)
		local ray = screenRay(mouseInViewport, fov, PolyEditor.camView1Mat, ortho)
		local p = ray.origin + (ray.direction * intersectPlane(ray.origin, ray.direction, vec3(0.0, 1.0, 0.0), - PolyEditor.camView1Mat.forwardAxis))
		local mouseUV = vec2(p.x + 64.0, p.z + 64.0)

		mouseUV.x = 8.0 * round(mouseUV.x / 8.0)
		mouseUV.y = 8.0 * round(mouseUV.y / 8.0)
		
		local p1 = worldToViewport(rectP1, PolyEditor.view1SpriteData)
		if vec2Length(p1 - PolyEditor.mousePos) < 4.0 and clickedOutsideViewport == false then
			PolyEditor.uvRegionP1SpriteData.color = Config.uiColors.polyEditorVertexHighlight
			mouseHoverHandle = 1
		else
			PolyEditor.uvRegionP1SpriteData.color = Config.uiColors.polyEditorGrid
		end

		local p2 = worldToViewport(rectP2, PolyEditor.view1SpriteData)
		if vec2Length(p2 - PolyEditor.mousePos) < 4.0 and clickedOutsideViewport == false then
			PolyEditor.uvRegionP2SpriteData.color = Config.uiColors.polyEditorVertexHighlight
			mouseHoverHandle = 2
		else
			PolyEditor.uvRegionP2SpriteData.color = Config.uiColors.polyEditorGrid
		end

		local p3 = worldToViewport(rectP3, PolyEditor.view1SpriteData)
		if vec2Length(p3 - PolyEditor.mousePos) < 4.0 and clickedOutsideViewport == false then
			PolyEditor.uvRegionP3SpriteData.color = Config.uiColors.polyEditorVertexHighlight
			mouseHoverHandle = 3
		else
			PolyEditor.uvRegionP3SpriteData.color = Config.uiColors.polyEditorGrid
		end

		local p4 = worldToViewport(rectP4, PolyEditor.view1SpriteData)
		if vec2Length(p4 - PolyEditor.mousePos) < 4.0 and clickedOutsideViewport == false then
			PolyEditor.uvRegionP4SpriteData.color = Config.uiColors.polyEditorVertexHighlight
			mouseHoverHandle = 4
		else
			PolyEditor.uvRegionP4SpriteData.color = Config.uiColors.polyEditorGrid
		end

		if mouseHoverHandle == -1 and clickedOutsideViewport == false then
			local backfaceCull = false
			local intersectDist = intersectQuad(ray.origin, ray.direction, rectP1, rectP2, rectP3, rectP4, backfaceCull)
			
			if intersectDist ~= -1.0 then
				mouseHoverRegion = true

				uvRegionColor = Config.uiColors.polyEditorVertexHighlight
			end 
		end

		if PolyEditor.mousePressed == MOUSE_BUTTON_LEFT and clickedOutsideViewport == false then
			if mouseHoverRegion == true then
				PolyEditor.editingUVRegion = 0

				PolyEditor.mouseOffset = mouseUV - regionPos

			elseif mouseHoverHandle ~= -1 then
				PolyEditor.editingUVRegion = mouseHoverHandle

				PolyEditor.uvRegionMouseClickPos = vec2Copy(regionPos)
				PolyEditor.uvRegionMouseClickSize = vec2Copy(regionSize)

				PolyEditor.mouseOffset = mouseUV
			end
		end

		if PolyEditor.editingUVRegion ~= -1 then
			uvRegionColor = Config.uiColors.polyEditorVertexHighlight

			if mouseUV.x < 0 then
				mouseUV.x = 0
			end

			if mouseUV.x > 128 then
				mouseUV.x = 128
			end

			if mouseUV.y < 0 then
				mouseUV.y = 0
			end

			if mouseUV.y > 128 then
				mouseUV.y = 128
			end

			if PolyEditor.editingUVRegion == 0 then
				regionPos = mouseUV - PolyEditor.mouseOffset

				uvRegionColor = Config.uiColors.polyEditorVertexHighlight

				PolyEditor.uvRegionP1SpriteData.color = Config.uiColors.polyEditorVertexHighlight
				PolyEditor.uvRegionP2SpriteData.color = Config.uiColors.polyEditorVertexHighlight
				PolyEditor.uvRegionP3SpriteData.color = Config.uiColors.polyEditorVertexHighlight
				PolyEditor.uvRegionP4SpriteData.color = Config.uiColors.polyEditorVertexHighlight

			elseif PolyEditor.editingUVRegion == 1 then
				-- P1 (min corner): move both edges adjacent to this handle; P3 (max corner) stays fixed.
				local offset = PolyEditor.mouseOffset - PolyEditor.uvRegionMouseClickPos
				regionPos = mouseUV - offset

				local oldEndPoint = PolyEditor.uvRegionMouseClickPos + PolyEditor.uvRegionMouseClickSize
				regionSize = oldEndPoint - regionPos

				PolyEditor.uvRegionP1SpriteData.color = Config.uiColors.polyEditorVertexHighlight

			elseif PolyEditor.editingUVRegion == 2 then
				-- P2 (max x, min y): move max-x and min-y edges; P4 corner stays fixed.
				local offsetX = PolyEditor.mouseOffset.x - (PolyEditor.uvRegionMouseClickPos.x + PolyEditor.uvRegionMouseClickSize.x)
				local offsetY = PolyEditor.mouseOffset.y - PolyEditor.uvRegionMouseClickPos.y

				regionSize.x = (mouseUV.x - PolyEditor.uvRegionMouseClickPos.x) - offsetX

				regionPos.y = mouseUV.y - offsetY
				regionSize.y = (PolyEditor.uvRegionMouseClickPos.y + PolyEditor.uvRegionMouseClickSize.y) - regionPos.y

				PolyEditor.uvRegionP2SpriteData.color = Config.uiColors.polyEditorVertexHighlight

			elseif PolyEditor.editingUVRegion == 3 then
				-- P3 (max corner): move both edges adjacent to this handle; P1 (min corner) stays fixed.
				local offsetX = PolyEditor.mouseOffset.x - (PolyEditor.uvRegionMouseClickPos.x + PolyEditor.uvRegionMouseClickSize.x)
				local offsetY = PolyEditor.mouseOffset.y - (PolyEditor.uvRegionMouseClickPos.y + PolyEditor.uvRegionMouseClickSize.y)

				regionSize.x = (mouseUV.x - regionPos.x) - offsetX
				regionSize.y = (mouseUV.y - regionPos.y) - offsetY

				PolyEditor.uvRegionP3SpriteData.color = Config.uiColors.polyEditorVertexHighlight

			elseif PolyEditor.editingUVRegion == 4 then
				-- P4 (min x, max y): move min-x and max-y edges; P2 corner stays fixed.
				local offsetX = PolyEditor.mouseOffset.x - PolyEditor.uvRegionMouseClickPos.x
				local offsetY = PolyEditor.mouseOffset.y - (PolyEditor.uvRegionMouseClickPos.y + PolyEditor.uvRegionMouseClickSize.y)

				regionSize.y = (mouseUV.y - regionPos.y) - offsetY

				regionPos.x = mouseUV.x - offsetX
				regionSize.x = (PolyEditor.uvRegionMouseClickPos.x + PolyEditor.uvRegionMouseClickSize.x) - regionPos.x

				PolyEditor.uvRegionP4SpriteData.color = Config.uiColors.polyEditorVertexHighlight

			end

			if regionPos.x < 0.0 then
				regionPos.x = 0.0
			end

			if regionPos.y < 0.0 then
				regionPos.y = 0.0
			end

			if regionPos.x + regionSize.x > 128.0 then
				regionPos.x = 128.0 - regionSize.x
			end

			if regionPos.y + regionSize.y > 128.0 then
				regionPos.y = 128.0 - regionSize.y
			end

			setTextureRegion(
				regionPos.x / 128.0, regionPos.y / 128.0,
				regionSize.x / 128.0, regionSize.y / 128.0)
			
			updateUV()
		end

		-- draw handles
		PolyEditor.uvRegionP1SpriteData.position = rectP1
		PolyEditor.uvRegionP1SpriteData.position.y = 2.0
		setSprite(PolyEditor.uvRegionP1Sprite, PolyEditor.uvRegionP1SpriteData)
		drawSprite(PolyEditor.uvRegionP1Sprite)

		PolyEditor.uvRegionP2SpriteData.position = rectP2
		PolyEditor.uvRegionP2SpriteData.position.y = 2.0
		setSprite(PolyEditor.uvRegionP2Sprite, PolyEditor.uvRegionP2SpriteData)
		drawSprite(PolyEditor.uvRegionP2Sprite)

		PolyEditor.uvRegionP3SpriteData.position = rectP3
		PolyEditor.uvRegionP3SpriteData.position.y = 2.0
		setSprite(PolyEditor.uvRegionP3Sprite, PolyEditor.uvRegionP3SpriteData)
		drawSprite(PolyEditor.uvRegionP3Sprite)

		PolyEditor.uvRegionP4SpriteData.position = rectP4
		PolyEditor.uvRegionP4SpriteData.position.y = 2.0
		setSprite(PolyEditor.uvRegionP4Sprite, PolyEditor.uvRegionP4SpriteData)
		drawSprite(PolyEditor.uvRegionP4Sprite)
	end

	drawLine(rectP1, rectP2, uvRegionColor)
	drawLine(rectP2, rectP3, uvRegionColor)
	drawLine(rectP3, rectP4, uvRegionColor)
	drawLine(rectP4, rectP1, uvRegionColor)
end

function updateViewport1Texture()
	updateCamera(PolyEditor.camView1Mat, PolyEditor.camManipView1, PolyEditor.view1SpriteData, PolyEditor.view1Sprite, 1)

	drawOrthoViewTexture(PolyEditor.camView1Mat, PolyEditor.camManipView1, PolyEditor.view1SpriteData, PolyEditor.view1Sprite, 1)

	setSprite(PolyEditor.view1TextureSprite, PolyEditor.view1TextureSpriteData)
	drawSprite(PolyEditor.view1TextureSprite)

	updateTextureRegion()

	drawSprite2D(PolyEditor.view1Sprite)

	drawSprite2D(PolyEditor.view1TextureLabelSprite)
end

function updateViewport2Texture()
	updateCamera(PolyEditor.camView2Mat, PolyEditor.camManipView2, PolyEditor.view2SpriteData, PolyEditor.view2Sprite, 2)

	drawPerspViewport(PolyEditor.camView2Mat, PolyEditor.camManipView2, PolyEditor.view2SpriteData, PolyEditor.view2Sprite, 2)

	updatePaintTools(2)

	if PolyEditor.mouseInViewport == 2 then
		updateMeshPicking(PolyEditor.camView2Mat, PolyEditor.camManipView2, PolyEditor.view2SpriteData, false)
	end
	
	if PolyEditor.ditherEnabled == true then
		setDitherEnabled(true)
	end

	drawSprite2D(PolyEditor.view2Sprite)

	if PolyEditor.ditherEnabled == true then
		setDitherEnabled(false)
	end

	drawSprite2D(PolyEditor.viewLabelSprites[2])
end


function highlightUVEditor()
	local pixelX = 1.0 / (PolyEditor.uvRegionSize.x * 128.0)
	local pixelY = 1.0 / (PolyEditor.uvRegionSize.y * 128.0)

	local centerX = floor((PolyEditor.uvRegionSize.x * 128.0) * 0.5)
	local centerY = floor((PolyEditor.uvRegionSize.y * 128.0) * 0.5)

	local offsetX = floor((PolyEditor.uvRegionSize.x * 128.0) * 0.5)
	local offsetY = floor((PolyEditor.uvRegionSize.y * 128.0) * 0.5)

	PolyEditor.highlightUVs = {}

	if PolyEditor.pickedQuad ~= -1 then
		PolyEditor.highlightUVs[PolyEditor.pickedQuad] = true

		local vId1 = ((PolyEditor.pickedQuad - 1) * 4) + 1
		local vId2 = ((PolyEditor.pickedQuad - 1) * 4) + 2
		local vId3 = ((PolyEditor.pickedQuad - 1) * 4) + 3
		local vId4 = ((PolyEditor.pickedQuad - 1) * 4) + 4

		local uv1 = PolyEditor.currentMeshUVs[vId1]
		local uv2 = PolyEditor.currentMeshUVs[vId2]
		local uv3 = PolyEditor.currentMeshUVs[vId3]
		local uv4 = PolyEditor.currentMeshUVs[vId4]

		if uv1.x == -1.0 or uv2.x == -1 or uv3.x == -1 or uv4.x == -1 then
			uv1.x = pixelX * (centerX - offsetX)
			uv1.y = pixelY * (centerY + offsetY)

			uv2.x = pixelX * (centerX + offsetX)
			uv2.y = pixelY * (centerY + offsetY)

			uv3.x = pixelX * (centerX + offsetX)
			uv3.y = pixelY * (centerY - offsetY)

			uv4.x = pixelX * (centerX - offsetX)
			uv4.y = pixelY * (centerY - offsetY)
		end
	end
end

function refreshUVEditor()
	PolyEditor.editingUVQuads = {}

	if #PolyEditor.selectedVerts < 4 then
		return
	end

	for i = 1, #PolyEditor.selectedVerts do
		local vertId = PolyEditor.selectedVerts[i]

		local quadId = ceil(vertId / 4)
		PolyEditor.editingUVQuads[quadId] = true
	end
end

function drawUVs()
	setDepthEnabled(false)

	local p1 = vec3(0.0, 0.0, 0.0)
	local p2 = vec3(0.0, 0.0, 0.0)

	for quadId, v in pairs(PolyEditor.highlightUVs) do
		local vId1 = ((quadId - 1) * 4) + 1
		local vId2 = ((quadId - 1) * 4) + 2
		local vId3 = ((quadId - 1) * 4) + 3
		local vId4 = ((quadId - 1) * 4) + 4

		local uv1 = PolyEditor.currentMeshUVs[vId1]
		local uv2 = PolyEditor.currentMeshUVs[vId2]
		local uv3 = PolyEditor.currentMeshUVs[vId3]
		local uv4 = PolyEditor.currentMeshUVs[vId4]
		
		p1.x = uv1.x * PolyEditor.uvDispW
		p1.y = uv1.y * PolyEditor.uvDispH

		p2.x = uv2.x * PolyEditor.uvDispW
		p2.y = uv2.y * PolyEditor.uvDispH

		drawLine(p1, p2, Config.uiColors.polyEditorVertexHighlight)

		p1.x = uv3.x * PolyEditor.uvDispW
		p1.y = uv3.y * PolyEditor.uvDispH

		drawLine(p2, p1, Config.uiColors.polyEditorVertexHighlight)

		p2.x = uv4.x * PolyEditor.uvDispW
		p2.y = uv4.y * PolyEditor.uvDispH

		drawLine(p1, p2, Config.uiColors.polyEditorVertexHighlight)

		p1.x = uv1.x * PolyEditor.uvDispW
		p1.y = uv1.y * PolyEditor.uvDispH

		drawLine(p2, p1, Config.uiColors.polyEditorVertexHighlight)
	end

	for quadId, v in pairs(PolyEditor.editingUVQuads) do
		local vId1 = ((quadId - 1) * 4) + 1
		local vId2 = ((quadId - 1) * 4) + 2
		local vId3 = ((quadId - 1) * 4) + 3
		local vId4 = ((quadId - 1) * 4) + 4

		local uv1 = PolyEditor.currentMeshUVs[vId1]
		local uv2 = PolyEditor.currentMeshUVs[vId2]
		local uv3 = PolyEditor.currentMeshUVs[vId3]
		local uv4 = PolyEditor.currentMeshUVs[vId4]
		
		p1.x = uv1.x * PolyEditor.uvDispW
		p1.y = uv1.y * PolyEditor.uvDispH

		p2.x = uv2.x * PolyEditor.uvDispW
		p2.y = uv2.y * PolyEditor.uvDispH

		drawLine(p1, p2, Config.uiColors.polyEditorWireframe)

		p1.x = uv3.x * PolyEditor.uvDispW
		p1.y = uv3.y * PolyEditor.uvDispH

		drawLine(p2, p1, Config.uiColors.polyEditorWireframe)

		p2.x = uv4.x * PolyEditor.uvDispW
		p2.y = uv4.y * PolyEditor.uvDispH

		drawLine(p1, p2, Config.uiColors.polyEditorWireframe)

		p1.x = uv1.x * PolyEditor.uvDispW
		p1.y = uv1.y * PolyEditor.uvDispH

		drawLine(p2, p1, Config.uiColors.polyEditorWireframe)
	end

	setDepthEnabled(true)
end

function updateUVPicking()
	if PolyEditor.mouseDown ~= -1 then
		return
	end

	PolyEditor.pickedUVId = -1
	PolyEditor.pickedUVQuad = -1

	local fov = - PolyEditor.camManipView3.focusDist
	local ortho = true
	local mouseInViewport = viewportToScreen(PolyEditor.mousePos, PolyEditor.view3SpriteData, ortho)
	local ray = screenRay(mouseInViewport, fov, PolyEditor.camView3Mat, ortho)

	local pixelDistThreshold = 4.0

	local closestQuadDist = 9999.0

	local uvPoint1 = vec3(0.0, 0.0, 0.0)
	local uvPoint2 = vec3(0.0, 0.0, 0.0)
	local uvPoint3 = vec3(0.0, 0.0, 0.0)
	local uvPoint4 = vec3(0.0, 0.0, 0.0)

	for quadId, v in pairs(PolyEditor.editingUVQuads) do
		local vId1 = ((quadId - 1) * 4) + 1
		local vId2 = ((quadId - 1) * 4) + 2
		local vId3 = ((quadId - 1) * 4) + 3
		local vId4 = ((quadId - 1) * 4) + 4

		local uv1 = PolyEditor.currentMeshUVs[vId1]
		local uv2 = PolyEditor.currentMeshUVs[vId2]
		local uv3 = PolyEditor.currentMeshUVs[vId3]
		local uv4 = PolyEditor.currentMeshUVs[vId4]

		uvPoint1.x = uv1.x * PolyEditor.uvDispW
		uvPoint1.y = uv1.y * PolyEditor.uvDispH

		uvPoint2.x = uv2.x * PolyEditor.uvDispW
		uvPoint2.y = uv2.y * PolyEditor.uvDispH

		uvPoint3.x = uv3.x * PolyEditor.uvDispW
		uvPoint3.y = uv3.y * PolyEditor.uvDispH

		uvPoint4.x = uv4.x * PolyEditor.uvDispW
		uvPoint4.y = uv4.y * PolyEditor.uvDispH

		local vertDistToMouse = pickVertex(uvPoint1, PolyEditor.view3SpriteData)

		local pickedVert = 1
		
		if vertDistToMouse > pixelDistThreshold then
			vertDistToMouse = pickVertex(uvPoint2, PolyEditor.view3SpriteData)
			pickedVert = 2
		end

		if vertDistToMouse > pixelDistThreshold then
			vertDistToMouse = pickVertex(uvPoint3, PolyEditor.view3SpriteData)
			pickedVert = 3
		end

		if vertDistToMouse > pixelDistThreshold then
			vertDistToMouse = pickVertex(uvPoint4, PolyEditor.view3SpriteData)
			pickedVert = 4
		end

		if vertDistToMouse < pixelDistThreshold then
			PolyEditor.pickedUVId = ((quadId - 1) * 4) + pickedVert
		else
			local backfaceCull = false
			local intersectDist = intersectQuad(ray.origin, ray.direction, uvPoint1, uvPoint2, uvPoint3, uvPoint4, backfaceCull)

			if intersectDist > -1.0 and intersectDist < closestQuadDist then
				PolyEditor.pickedUVQuad = quadId

				closestQuadDist = intersectDist
			end
		end
	end
end

function updateViewport3Texture()
	-- Square-pixel display: scale the world quad by the region aspect so texels stay square.
	local maxDim = math.max(PolyEditor.uvRegionSize.x, PolyEditor.uvRegionSize.y)
	if maxDim <= 0.0 then maxDim = 1.0 end
	PolyEditor.uvDispW = 128.0 * (PolyEditor.uvRegionSize.x / maxDim)
	PolyEditor.uvDispH = 128.0 * (PolyEditor.uvRegionSize.y / maxDim)

	-- Recenter the camera on the quad only when the region size changes, so pan/zoom is preserved.
	if PolyEditor.uvDispW ~= PolyEditor.uvDispWLast or PolyEditor.uvDispH ~= PolyEditor.uvDispHLast then
		PolyEditor.camManipView3Texture.pos.x = PolyEditor.uvDispW * 0.5
		PolyEditor.camManipView3Texture.pos.y = PolyEditor.uvDispH * 0.5
		PolyEditor.uvDispWLast = PolyEditor.uvDispW
		PolyEditor.uvDispHLast = PolyEditor.uvDispH
	end

	updateCamera(PolyEditor.camView3Mat, PolyEditor.camManipView3, PolyEditor.view3SpriteData, PolyEditor.view3Sprite, 3)

	drawOrthoViewTexture(PolyEditor.camView3Mat, PolyEditor.camManipView3, PolyEditor.view3SpriteData, PolyEditor.view3Sprite, 3)

	drawUVVertices()

	PolyEditor.view3TextureSpriteData.texturePos = PolyEditor.uvRegionPos * 128.0
	PolyEditor.view3TextureSpriteData.texturePos.x = round(PolyEditor.view3TextureSpriteData.texturePos.x)
	PolyEditor.view3TextureSpriteData.texturePos.y = round(PolyEditor.view3TextureSpriteData.texturePos.y)

	PolyEditor.view3TextureSpriteData.textureSize = PolyEditor.uvRegionSize * 128.0
	PolyEditor.view3TextureSpriteData.textureSize.x = round(PolyEditor.view3TextureSpriteData.textureSize.x)
	PolyEditor.view3TextureSpriteData.textureSize.y = round(PolyEditor.view3TextureSpriteData.textureSize.y)

	PolyEditor.view3TextureSpriteData.size = vec2(PolyEditor.uvDispW, PolyEditor.uvDispH)
	PolyEditor.view3TextureSpriteData.position = vec3(PolyEditor.uvDispW * 0.5, PolyEditor.uvDispH * 0.5, -0.1)

	setSprite(PolyEditor.view3TextureSprite, PolyEditor.view3TextureSpriteData)

	drawSprite(PolyEditor.view3TextureSprite)

	drawUVs()

	if PolyEditor.mouseInViewport == 3 then
		updateUVPicking()

		updateTextureTools()
	end

	drawSprite2D(PolyEditor.view3Sprite)

	drawSprite2D(PolyEditor.view3TextureLabelSprite)
end

function PolyEditor.magnifyTexture()
	PolyEditor.textureDrawMagnified = not PolyEditor.textureDrawMagnified
end

function PolyEditor.pencilToolStart()
	setActiveTool("pencil", TEXTURE_MODE)
end

function PolyEditor.eraserToolStart()
	setActiveTool("eraser", TEXTURE_MODE)
end

function PolyEditor.lineToolStart()
	setActiveTool("line", TEXTURE_MODE)

	PolyEditor.oldPixelPos.x = -1.0
	PolyEditor.oldPixelPos.y = -1.0
end

function PolyEditor.bucketToolStart()
	setActiveTool("bucket", TEXTURE_MODE)
end

function PolyEditor.setUVAnimFrame()
	local meshData = PolyEditor.meshes[PolyEditor.currentLayerId]
	if not meshData then return end

	if not hasMeshUVAnimFrame(meshData.mesh, 0) then
		local allIndices, allUVs = {}, {}
		for i = 1, #PolyEditor.currentMeshUVs do
			allIndices[i] = i
			allUVs[i] = PolyEditor.currentMeshUVs[i]
		end
		setMeshUVAnimFrame(meshData.mesh, 0, allIndices, allUVs)
	end

	if PolyEditor.uvAnimFrame == 0 then
		local allIndices, allUVs = {}, {}
		for i = 1, #PolyEditor.currentMeshUVs do
			allIndices[i] = i
			allUVs[i] = PolyEditor.currentMeshUVs[i]
		end
		setMeshUVAnimFrame(meshData.mesh, 0, allIndices, allUVs)
		return
	end

	local frame0Data = getMeshUVAnimFrame(meshData.mesh, 0)
	local changedIndices, changedUVs = {}, {}
	for i = 1, #PolyEditor.currentMeshUVs do
		local cur = PolyEditor.currentMeshUVs[i]
		local base = frame0Data[i]
		if base == nil or cur.x ~= base.x or cur.y ~= base.y then
			changedIndices[#changedIndices + 1] = i
			changedUVs[#changedUVs + 1] = cur
		end
	end
	setMeshUVAnimFrame(meshData.mesh, PolyEditor.uvAnimFrame, changedIndices, changedUVs)
end

function _loadUVAnimFrame(frame)
	local meshData = PolyEditor.meshes[PolyEditor.currentLayerId]
	if not meshData then return end
	if not hasMeshUVAnimFrame(meshData.mesh, frame) then return end

	meshUVAnimFrame(meshData.mesh, frame)
	PolyEditor.currentMeshUVs = getMeshVertices(PolyEditor.currentMesh, VERT_UV)
end

function PolyEditor.prevUVAnimFrame()
	PolyEditor.uvAnimPlaying = false
	
	PolyEditor.uvAnimFrame = PolyEditor.uvAnimFrame - 1
	if PolyEditor.uvAnimFrame < 0 then
		PolyEditor.uvAnimFrame = 0
	end
	_loadUVAnimFrame(PolyEditor.uvAnimFrame)
end

function PolyEditor.nextUVAnimFrame()
	PolyEditor.uvAnimPlaying = false

	PolyEditor.uvAnimFrame = PolyEditor.uvAnimFrame + 1
	if PolyEditor.uvAnimFrame > 8 then
		PolyEditor.uvAnimFrame = 8
	end
	_loadUVAnimFrame(PolyEditor.uvAnimFrame)
end

function PolyEditor.deleteUVAnimFrame()
	local meshData = PolyEditor.meshes[PolyEditor.currentLayerId]
	if not meshData then return end
	if PolyEditor.uvAnimFrame == 0 then return end
	clearMeshUVAnimFrame(meshData.mesh, PolyEditor.uvAnimFrame)
	_loadUVAnimFrame(PolyEditor.uvAnimFrame)

	local hasOtherFrames = false
	for f = 1, 8 do
		if hasMeshUVAnimFrame(meshData.mesh, f) then
			hasOtherFrames = true
			break
		end
	end
	if not hasOtherFrames then
		clearMeshUVAnimFrame(meshData.mesh, 0)
	end
end

function PolyEditor.playUVAnim()
	if PolyEditor.uvAnimPlaying then
		PolyEditor.uvAnimPlaying = false
		return
	end

	PolyEditor.uvAnimFrame = 0
	_loadUVAnimFrame(0)
	PolyEditor.uvAnimLastFrameTime = PolyEditor.time
	PolyEditor.uvAnimPlaying = true
end

function _updateArrowRepeat()
	-- key released: commit a single undo entry for the whole hold
	if not getKeyDown(PolyEditor.arrowRepeatKey) then
		if PolyEditor.arrowMovePending then
			storeUndo(PolyEditor.arrowMoveLayerIds)
			PolyEditor.arrowMovePending = false
		end
		PolyEditor.arrowRepeatKey = -1
		PolyEditor.arrowRepeatDelta = nil
		return
	end

	-- still held: repeat the move on the interval, without storing undo
	if PolyEditor.time >= PolyEditor.arrowRepeatNextTime then
		if _moveSelectedVerts(PolyEditor.arrowRepeatDelta) then
			PolyEditor.arrowMovePending = true
		end
		PolyEditor.arrowRepeatNextTime = PolyEditor.time + PolyEditor.arrowRepeatInterval
	end
end

function _updateUVAnim()
	if not PolyEditor.uvAnimPlaying then
		return
	end

	if PolyEditor.time - PolyEditor.uvAnimLastFrameTime < 1.0 / PolyEditor.uvAnimSpeed then
		return
	end

	PolyEditor.uvAnimLastFrameTime = PolyEditor.time

	local nextFrame = PolyEditor.uvAnimFrame + 1
	local meshData = PolyEditor.meshes[PolyEditor.currentLayerId]

	if nextFrame > 8 or not meshData or not hasMeshUVAnimFrame(meshData.mesh, nextFrame) then
		nextFrame = 0
	end

	PolyEditor.uvAnimFrame = nextFrame
	_loadUVAnimFrame(nextFrame)
end

function PolyEditor.stampToolStart()
	setActiveTool("stamp", TEXTURE_MODE)

	PolyEditor.textureStamp = nil
end

function PolyEditor.rectFillToolStart()
	setActiveTool("rect fill", TEXTURE_MODE)
end

function PolyEditor.gradientToolStart()
	setActiveTool("gradient", TEXTURE_MODE)
end

function PolyEditor.layerIsEmpty(layerId)
	layerId = round(clamp(layerId, 1, MESH_LAYERS))

	if PolyEditor.meshes[layerId] == nil then
		return true
	end

	local verts = getMeshVertices(PolyEditor.meshes[layerId].mesh)

	local empty = true
	if #verts > 0 then
		empty = false
	end

	return empty
end

function PolyEditor.toggleShowAllLayers()
	PolyEditor.showAllLayers = not PolyEditor.showAllLayers
end

function PolyEditor.toggleAllLayersOverlay()
	local overlay = true

	local pageData = PolyEditor.pagesData[PolyEditor.currentPageId]

	if pageData.overlayLayers[1] == nil then
		overlay = true
	else
		overlay = nil
	end

	for layerId = 1, MESH_LAYERS do
		pageData.overlayLayers[layerId] = overlay
		PolyEditorUI.setLayerOverlay(layerId, overlay)
	end
end

function PolyEditor.switchLayerOverlay(layerId)
	local pageData = PolyEditor.pagesData[PolyEditor.currentPageId]

	if pageData.overlayLayers[layerId] == nil then
		pageData.overlayLayers[layerId] = true

		PolyEditorUI.setLayerOverlay(layerId, true)
	else
		pageData.overlayLayers[layerId] = nil

		PolyEditorUI.setLayerOverlay(layerId, false)
	end
end

-- Rebuild the layer-selection set from an array of ids (deduped, clamped, never empty).
-- Does NOT change the active layer or the working-mesh cache; use _activateLayer for that.
function setSelectedLayers(ids)
	PolyEditor.selectedLayers = {}
	PolyEditor.selectedLayersById = {}

	for i = 1, #ids do
		local layerId = round(clamp(ids[i], 1, MESH_LAYERS))
		if PolyEditor.selectedLayersById[layerId] == nil then
			PolyEditor.selectedLayers[#PolyEditor.selectedLayers + 1] = layerId
			PolyEditor.selectedLayersById[layerId] = 1
		end
	end

	-- the selection is never empty; fall back to the active layer
	if #PolyEditor.selectedLayers == 0 then
		PolyEditor.selectedLayers = {PolyEditor.currentLayerId}
		PolyEditor.selectedLayersById = {[PolyEditor.currentLayerId] = 1}
	end
end

-- World-space pivot position of any layer (each layer has its own scene node/pivot).
function _layerPivotPos(layerId)
	local model = PolyEditor.models[layerId]
	if model ~= nil then
		return vec3(getModelWorldMatrix(model).position)
	end
	return vec3(0.0, 0.0, 0.0)
end

-- Fresh empty per-layer vertex-selection store.
function _newSelectionStore()
	return { verts = {}, byId = {}, byHash = {}, overlapping = {} }
end

-- Read a layer's four CPU vertex arrays off the GPU into meshes[layerId].cache.
function _loadLayerCache(layerId)
	local meshData = PolyEditor.meshes[layerId]
	if meshData == nil then
		return nil
	end
	local mesh = meshData.mesh
	meshData.cache = {
		quads = getMeshVertices(mesh),
		normals = getMeshVertices(mesh, VERT_NORMAL),
		uvs = getMeshVertices(mesh, VERT_UV),
		colors = getMeshVertices(mesh, VERT_COLORBASE),
	}
	return meshData.cache
end

-- Return a layer's CPU cache, lazily loading it off the GPU the first time.
function _ensureLayerCache(layerId)
	local meshData = PolyEditor.meshes[layerId]
	if meshData == nil then
		return nil
	end
	if meshData.cache == nil then
		return _loadLayerCache(layerId)
	end
	return meshData.cache
end

-- World position of a picked vertex, expressed in the ACTIVE layer's local frame.
-- Picking is cross-layer: pickedVerts index into pickedLayerId's mesh, which may not be the
-- active layer. Converting picked-local -> world -> active-local lets active-frame quad math
-- (e.g. the quad tool's corner list) coincide with the picked vertex in world space.
-- When the picked layer IS the active layer this reduces to cache.quads[vertId].
function _pickedVertActiveLocalPos(vertId, pickedLayerId)
	local cache = _ensureLayerCache(pickedLayerId)
	if cache == nil or cache.quads[vertId] == nil then
		return nil
	end
	return vec3Copy(cache.quads[vertId])
		+ _layerPivotPos(pickedLayerId)
		- _layerPivotPos(PolyEditor.currentLayerId)
end

-- Return a layer's vertex-selection store, creating an empty one on first use.
function _ensureLayerSelection(layerId)
	if PolyEditor.selectedVertsByLayer[layerId] == nil then
		PolyEditor.selectedVertsByLayer[layerId] = _newSelectionStore()
	end
	if PolyEditor.selectedUVsByLayer[layerId] == nil then
		PolyEditor.selectedUVsByLayer[layerId] = {}
	end
	return PolyEditor.selectedVertsByLayer[layerId]
end

-- Point the currentMesh*/selectedVerts* globals at the given layer's cache + store,
-- so the large body of single-layer code transparently operates on that layer.
-- No GPU reads (beyond the lazy cache load) and no selection wipe.
function _bindActiveLayerGlobals(layerId)
	local meshData = PolyEditor.meshes[layerId]
	if meshData == nil then
		return
	end

	local cache = _ensureLayerCache(layerId)
	local sel = _ensureLayerSelection(layerId)

	PolyEditor.currentMesh = meshData.mesh
	PolyEditor.currentMeshQuads = cache.quads
	PolyEditor.currentMeshNormals = cache.normals
	PolyEditor.currentMeshUVs = cache.uvs
	PolyEditor.currentMeshVertexColors = cache.colors

	PolyEditor.selectedVerts = sel.verts
	PolyEditor.selectedVertsById = sel.byId
	PolyEditor.selectedVertsByHash = sel.byHash
	PolyEditor.selectedVertsOverlapping = sel.overlapping

	PolyEditor.selectedUVs = PolyEditor.selectedUVsByLayer[layerId]
end

-- Snapshot the live globals back into the active layer's cache + store. Tools reassign
-- the global arrays freely; this re-captures whatever they now point at so the per-layer
-- state stays correct when we leave the layer (switch/page/undo). Correctness of the
-- per-layer model rests on calling this before ever leaving the active layer.
function _syncActiveCacheFromGlobals()
	local layerId = PolyEditor.currentLayerId
	local meshData = PolyEditor.meshes[layerId]
	if meshData == nil then
		return
	end
	-- never bound as active yet → nothing established to capture; let bind load from GPU
	if meshData.cache == nil then
		return
	end
	meshData.cache.quads = PolyEditor.currentMeshQuads
	meshData.cache.normals = PolyEditor.currentMeshNormals
	meshData.cache.uvs = PolyEditor.currentMeshUVs
	meshData.cache.colors = PolyEditor.currentMeshVertexColors

	local sel = _ensureLayerSelection(layerId)
	sel.verts = PolyEditor.selectedVerts
	sel.byId = PolyEditor.selectedVertsById
	sel.byHash = PolyEditor.selectedVertsByHash
	sel.overlapping = PolyEditor.selectedVertsOverlapping
	PolyEditor.selectedUVsByLayer[layerId] = PolyEditor.selectedUVs
end

-- Make sure every currently-selected layer has a resident CPU cache, syncing the active
-- layer's live globals first. Call before any cross-layer read of meshes[id].cache.
function _syncSelectedLayerCaches()
	_syncActiveCacheFromGlobals()
	for i = 1, #PolyEditor.selectedLayers do
		_ensureLayerCache(PolyEditor.selectedLayers[i])
	end
end

-- Set the vertex selection for a specific layer (hashing/overlap against that layer's mesh).
-- Re-points the globals if the layer is active. Body of the old setSelectedVerts.
function setSelectedVertsForLayer(layerId, verts)
	local meshData = PolyEditor.meshes[layerId]
	if meshData == nil then
		return
	end

	local mesh = meshData.mesh
	local sel = _ensureLayerSelection(layerId)

	sel.verts = {}
	sel.byId = {}
	sel.byHash = {}

	for i = 1, #verts do
		local vertId = verts[i]
		sel.verts[i] = vertId
		sel.byId[vertId] = 1
		local hash = getVertexHash(mesh, vertId)
		sel.byHash[hash] = vertId
	end

	sel.overlapping = PolyUtil.getAllOverlappingVerts(mesh, sel.verts)

	if layerId == PolyEditor.currentLayerId then
		local cache = _ensureLayerCache(layerId)
		if #verts > 0 then
			if cache.colors[verts[1]] ~= nil then
				PolyEditor.currentVertexColor = vec4(cache.colors[verts[1]])
			end
		else
			PolyEditor.currentVertexColor = vec4(255, 255, 255, 255)
		end

		-- re-point the globals at the freshly rebuilt tables
		PolyEditor.selectedVerts = sel.verts
		PolyEditor.selectedVertsById = sel.byId
		PolyEditor.selectedVertsByHash = sel.byHash
		PolyEditor.selectedVertsOverlapping = sel.overlapping
	end
end

-- Set the UV selection for a specific layer. Re-points the global if the layer is active.
function setSelectedUVsForLayer(layerId, uvs)
	local copy = {}
	for i = 1, #uvs do
		copy[i] = uvs[i]
	end
	PolyEditor.selectedUVsByLayer[layerId] = copy
	if layerId == PolyEditor.currentLayerId then
		PolyEditor.selectedUVs = copy
	end
end

-- Push a layer's cache arrays back to the GPU and rebuild derived state (normals, UV/color
-- length reconciliation, selection hash). Generalizes the old refreshCurrentMesh. For the
-- active layer it syncs from + re-binds the globals so the alias survives.
function refreshLayer(layerId)
	local meshData = PolyEditor.meshes[layerId]
	if meshData == nil then
		return
	end

	if layerId == PolyEditor.currentLayerId then
		_syncActiveCacheFromGlobals()
	end

	local cache = _ensureLayerCache(layerId)
	local mesh = meshData.mesh

	setMeshVertices(mesh, VERT_POSITION, cache.quads)
	cache.quads = getMeshVertices(mesh)

	computeNormals(mesh)
	cache.normals = getMeshVertices(mesh, VERT_NORMAL)

	if #cache.uvs ~= #cache.quads then
		cache.uvs = getMeshVertices(mesh, VERT_UV)
	end

	if #cache.colors ~= #cache.quads then
		if #cache.colors > #cache.quads then
			for i = #cache.colors, #cache.quads + 1, -1 do
				cache.colors[i] = nil
			end
		else
			local lastColor = vec4(255, 255, 255, 255)
			if #cache.colors > 0 then
				lastColor = cache.colors[#cache.colors]
			end
			for i = #cache.colors + 1, #cache.quads do
				cache.colors[i] = vec4Copy(lastColor)
			end
		end
	end

	setMeshVertices(mesh, VERT_UV, cache.uvs)
	setMeshTexture(mesh, PolyEditor.currentPageId)
	setMeshVertices(mesh, VERT_COLORBASE, cache.colors)
	setMeshMicroShader(mesh, "modelMat", mat4(), MICROSHADER_UNIFORM)

	-- rebuild this layer's selection hash against its (possibly re-indexed) mesh
	local sel = _ensureLayerSelection(layerId)
	sel.byHash = {}
	for i = 1, #sel.verts do
		local vertId = sel.verts[i]
		sel.byHash[getVertexHash(mesh, vertId)] = vertId
	end

	if layerId == PolyEditor.currentLayerId then
		_bindActiveLayerGlobals(layerId)
	end
end

-- Make layerId the active layer, binding its per-layer cache + selection into the
-- working-mesh globals every tool reads. Selection now persists per layer (no wipe):
-- the outgoing layer's live state is snapshotted first, then the incoming layer's restored.
function _activateLayer(layerId)
	layerId = round(clamp(layerId, 1, MESH_LAYERS))

	-- stash the outgoing layer's working state so its cache/selection survive the switch
	_syncActiveCacheFromGlobals()

	PolyEditor.currentLayerId = layerId

	if PolyEditor.meshes[PolyEditor.currentLayerId] == nil then
		PolyEditor.meshes[PolyEditor.currentLayerId] = _newLayerMeshData()
	end

	_bindActiveLayerGlobals(layerId)

	PolyEditor.uvRegionPos = getMeshUVRegionPos(PolyEditor.currentMesh)
	PolyEditor.uvRegionSize = getMeshUVRegionSize(PolyEditor.currentMesh)

	PolyEditor.editingUVQuads = {}
	PolyEditor.highlightUVs = {}
end

function _destroyEmptyUnselectedLayers()
	local destroyedAny = false

	for layerId = 1, MESH_LAYERS do
		if PolyEditor.selectedLayersById[layerId] == nil
			and PolyEditor.meshes[layerId] ~= nil
			and PolyEditor.layerIsEmpty(layerId) then

			-- detach the mesh from its model before freeing so nothing references it
			if isValidAddress(PolyEditor.models[layerId]) == true then
				setModelMesh(PolyEditor.models[layerId], nil)
			end
			
			destroy(PolyEditor.meshes[layerId].mesh)
			PolyEditor.meshes[layerId] = nil

			PolyEditor.selectedVertsByLayer[layerId] = nil
			PolyEditor.selectedUVsByLayer[layerId] = nil

			destroyedAny = true
		end
	end

	-- rebuild the model hierarchy so it no longer references the freed meshes
	if destroyedAny == true then
		_hierarchyChanged()
	end
end

-- Mirror the layer-selection set onto the hierarchy tree's per-node `selected` flags
-- so the tree highlights the same layers as the swatch grid.
function _syncHierarchySelectionFlags()
	local function walk(nodeList)
		for i = 1, #nodeList do
			local node = nodeList[i]
			node.selected = PolyEditor.selectedLayersById[node.layerId] ~= nil
			if node.children then
				walk(node.children)
			end
		end
	end

	if PolyEditor.hierarchy ~= nil then
		walk(PolyEditor.hierarchy)
	end

	if PolyEditorUI.hierarchyInspector ~= nil and PolyEditorUI.hierarchyInspector.visible == true then
		PolyEditorUI.hierarchyInspector:refresh()
	end
end

-- Plain-click: select exactly one layer (collapsing any multi-selection) and make it active.
function PolyEditor.selectLayer(layerId)
	layerId = round(clamp(layerId, 1, MESH_LAYERS))

	local sameActive = (layerId == PolyEditor.currentLayerId)

	-- already the sole selection → nothing to do (preserves the vertex selection)
	if sameActive and #PolyEditor.selectedLayers == 1 then
		return
	end

	setSelectedLayers({layerId})

	-- only re-swap the working cache when the active layer actually changes
	if not sameActive then
		_activateLayer(layerId)
	end

	_destroyEmptyUnselectedLayers()

	PolyEditorUI.layerSelected(PolyEditor.currentLayerId)
	_syncHierarchySelectionFlags()

	storeUndo()
end

-- Modifier-click: toggle a layer in/out of the selection. Adding makes it active;
-- removing the active layer promotes another remaining member. Never empties the set.
function PolyEditor.toggleLayerSelected(layerId)
	layerId = round(clamp(layerId, 1, MESH_LAYERS))

	if PolyEditor.selectedLayersById[layerId] == nil then
		-- add layer to selection
		PolyEditor.selectedLayers[#PolyEditor.selectedLayers + 1] = layerId
		PolyEditor.selectedLayersById[layerId] = 1
	else
		-- refuse to remove the last remaining member
		if #PolyEditor.selectedLayers <= 1 then
			return
		end

		PolyEditor.selectedLayersById[layerId] = nil
		for i = 1, #PolyEditor.selectedLayers do
			if PolyEditor.selectedLayers[i] == layerId then
				table.remove(PolyEditor.selectedLayers, i)
				break
			end
		end

		-- if the active layer was removed, promote another member
		if PolyEditor.currentLayerId == layerId then
			_activateLayer(PolyEditor.selectedLayers[#PolyEditor.selectedLayers])
		end
	end

	_destroyEmptyUnselectedLayers()

	PolyEditorUI.layerSelected(PolyEditor.currentLayerId)
	_syncHierarchySelectionFlags()

	storeUndo()
end

function PolyEditor.setTexturePencilColor(color)
	PolyEditor.texturePencilColor.x = color.x
	PolyEditor.texturePencilColor.y = color.y
	PolyEditor.texturePencilColor.z = color.z
	PolyEditor.texturePencilColor.w = color.w
end

function PolyEditor.setVertexColor(color)
	PolyEditor.currentVertexColor.x = color.x
	PolyEditor.currentVertexColor.y = color.y
	PolyEditor.currentVertexColor.z = color.z
	PolyEditor.currentVertexColor.w = color.w

	_syncSelectedLayerCaches()

	local affected = {}

	for li = 1, #PolyEditor.selectedLayers do
		local layerId = PolyEditor.selectedLayers[li]
		local store = PolyEditor.selectedVertsByLayer[layerId]
		local meshData = PolyEditor.meshes[layerId]

		if store ~= nil and meshData ~= nil and #store.verts > 0 then
			local mesh = meshData.mesh
			local cache = _ensureLayerCache(layerId)

			local verts = store.verts
			if PolyEditor.textureScissorEnabled == false then
				verts = PolyUtil.getAllOverlappingVerts(mesh, store.verts)
			end

			for i = 1, #verts do
				cache.colors[verts[i]] = vec4(PolyEditor.currentVertexColor)
			end

			setMeshVertices(mesh, VERT_COLORBASE, cache.colors)

			-- keep the active layer's global alias pointing at the mutated cache
			if layerId == PolyEditor.currentLayerId then
				_bindActiveLayerGlobals(layerId)
			end

			affected[#affected + 1] = layerId
		end
	end

	PolyEditor.colorAffectedLayerIds = affected
end

function updateViewport4Texture()
	updateCamera(PolyEditor.camView4Mat, PolyEditor.camManipView4, PolyEditor.view4SpriteData, PolyEditor.view4Sprite, 4)

	drawOrthoViewTexture(PolyEditor.camView4Mat, PolyEditor.camManipView4, PolyEditor.view4SpriteData, PolyEditor.view4Sprite, 4)

	updatePaintTools(4)
	
	if PolyEditor.textureDrawMagnified == true then
		PolyEditor.view4TextureSpriteData.texturePos = PolyEditor.uvRegionPos * 128.0
		PolyEditor.view4TextureSpriteData.textureSize = PolyEditor.uvRegionSize * 128.0
		-- Square-pixel display: shape the quad to the region aspect (view3 set uvDispW/H this frame).
		PolyEditor.view4TextureSpriteData.size = vec2(PolyEditor.uvDispW, PolyEditor.uvDispH)
	else
		PolyEditor.view4TextureSpriteData.texturePos.x = 0.0
		PolyEditor.view4TextureSpriteData.texturePos.y = 0.0
		PolyEditor.view4TextureSpriteData.textureSize.x = 128.0
		PolyEditor.view4TextureSpriteData.textureSize.y = 128.0
		PolyEditor.view4TextureSpriteData.size = vec2(128.0, 128.0)
	end

	setSprite(PolyEditor.view4TextureSprite, PolyEditor.view4TextureSpriteData)

	drawSprite(PolyEditor.view4TextureSprite)

	drawSprite2D(PolyEditor.view4Sprite)

	drawSprite2D(PolyEditor.view4TextureLabelSprite)
end

function updateViewports()
	if PolyEditor.mode == MODEL_MODE then
		updateViewport1Model()
		updateViewport2Model()
		updateViewport3Model()
		updateViewport4Model()
	elseif PolyEditor.mode == TEXTURE_MODE then
		updateViewport1Texture()
		updateViewport2Texture()
		updateViewport3Texture()
		updateViewport4Texture()
	elseif PolyEditor.mode == RIG_MODE then
		if PolyEditor.mouseInViewport == -1 then
			PolyEditor.pivotCenterSpriteData.color.w = 100.0
			setSprite(PolyEditor.pivotCenterSprite, PolyEditor.pivotCenterSpriteData)
		end

		PolyEditor.pivotCenterSpriteData.position = currentLayerPivotPos()

		updateViewport1Rig()
		updateViewport2Rig()
		updateViewport3Rig()
		updateViewport4Rig()
	end
end

function update()
	if PolyEditor.capturingGif == true then
		_captureGif()

		return
	end

	drawSprite2D(PolyEditorUI.backgroundSprite)

	PolyEditor.time = getTime()

	updateMeshLight(PolyEditor.currentMesh)

	if PolyEditor.arrowRepeatKey ~= -1 then
		_updateArrowRepeat()
	end

	updateMouseInput()
	_updateUVAnim()
	updateViewports()

	PolyEditorUI.update()
end
