diff --git a/src/main/kotlin/solve/constants/ResourcesPaths.kt b/src/main/kotlin/solve/constants/ResourcesPaths.kt index 33ef2e4d3..adc61e2a3 100644 --- a/src/main/kotlin/solve/constants/ResourcesPaths.kt +++ b/src/main/kotlin/solve/constants/ResourcesPaths.kt @@ -41,3 +41,5 @@ const val ShadersLineLandmarkVertexPath = "engine/shaders/landmark/line/line.ver const val ShadersLineLandmarkFragmentPath = "engine/shaders/landmark/line/line.frag" const val ShadersPlaneLandmarkVertexPath = "engine/shaders/landmark/plane/plane.vert" const val ShadersPlaneLandmarkFragmentPath = "engine/shaders/landmark/plane/plane.frag" +const val ShadersPointAssociationVertexPath = "engine/shaders/association/point/point.vert" +const val ShadersPointAssociationFragmentPath = "engine/shaders/association/point/point.frag" diff --git a/src/main/kotlin/solve/rendering/canvas/SceneCanvas.kt b/src/main/kotlin/solve/rendering/canvas/SceneCanvas.kt index 8aacb76d9..ad9a6ea14 100644 --- a/src/main/kotlin/solve/rendering/canvas/SceneCanvas.kt +++ b/src/main/kotlin/solve/rendering/canvas/SceneCanvas.kt @@ -12,6 +12,7 @@ import solve.rendering.engine.core.input.MouseInputHandler import solve.rendering.engine.core.renderers.FramesRenderer import solve.rendering.engine.core.renderers.LinesLayerRenderer import solve.rendering.engine.core.renderers.PlanesLayerRenderer +import solve.rendering.engine.core.renderers.PointAssociationsRenderer import solve.rendering.engine.core.renderers.PointsLayerRenderer import solve.rendering.engine.core.renderers.Renderer import solve.rendering.engine.utils.minus @@ -24,6 +25,7 @@ import solve.scene.model.Landmark import solve.scene.model.Layer import solve.scene.model.Scene import solve.scene.model.VisualizationFrame +import solve.scene.view.association.AssociationManager import solve.utils.ServiceLocator import solve.utils.action import solve.utils.ceilToInt @@ -43,7 +45,6 @@ class SceneCanvas : OpenGLCanvas() { private var leftUpperCornerCameraPosition = Vector2f() private var rightLowerCornerCameraPosition = Vector2f() - private var lastLayersWithCommonSettings = listOf() private var lastFramesSelection = listOf() private var framesSelectionSize = 0 private var framesSize = Vector2i() @@ -60,6 +61,8 @@ class SceneCanvas : OpenGLCanvas() { private var contextMenuInvokeFrame: VisualizationFrame? = null + private val associationManager = AssociationManager() + private val contextMenu = buildContextMenu(canvas) init { @@ -69,8 +72,7 @@ class SceneCanvas : OpenGLCanvas() { fun setNewScene(scene: Scene) { this.scene = scene this.framesSize = Vector2i(scene.frameSize.width.toInt(), scene.frameSize.height.toInt()) - engineScene?.framesRenderer?.setNewSceneFrames(scene.frames, framesSize.toFloatVector()) - engineScene?.landmarkRenderers?.forEach { it.setNewSceneFrames(scene.frames, framesSize.toFloatVector()) } + engineScene?.setNewScene(scene) isFirstFramesSelection = true needToReinitializeRenderers = true @@ -85,23 +87,17 @@ class SceneCanvas : OpenGLCanvas() { recalculateCameraCornersPositions() window.camera.position = leftUpperCornerCameraPosition - engineScene?.framesRenderer?.setFramesSelection(framesSelection) - engineScene?.landmarkRenderers?.forEach { it.setFramesSelection(framesSelection) } - engineScene?.landmarkRenderers?.forEach { renderer -> - val rendererLayerSettings = - engineScene?.landmarkLayerRendererLayers?.get(renderer)?.settings ?: return@forEach - lastLayersWithCommonSettings = - scene?.getLayersWithCommonSettings(rendererLayerSettings, framesSelection) ?: return@forEach - renderer.setFramesSelectionLayers(lastLayersWithCommonSettings) - } + engineScene?.setFramesSelection(framesSelection) + associationManager.setFramesSelection(framesSelection) framesSelectionSize = framesSelection.count() lastFramesSelection = framesSelection + + associationManager.associate(0, 12) } fun setColumnsNumber(columnsNumber: Int) { - engineScene?.framesRenderer?.setNewGridWidth(columnsNumber) - engineScene?.landmarkRenderers?.forEach { it.setNewGridWidth(columnsNumber) } this.columnsNumber = columnsNumber + engineScene?.setColumnsNumber(columnsNumber) } fun dragTo(toScreenPoint: Vector2i) { @@ -160,8 +156,13 @@ class SceneCanvas : OpenGLCanvas() { super.onInit() val controller = ServiceLocator.getService() ?: return sceneController = controller + val framesRenderer = FramesRenderer(window) + val pointAssociationsRenderer = PointAssociationsRenderer(window, associationManager) - engineScene = EngineScene(FramesRenderer(window)) + engineScene = EngineScene( + framesRenderer, + pointAssociationsRenderer + ) { scene } } override fun onDraw(deltaTime: Float) { diff --git a/src/main/kotlin/solve/rendering/engine/core/renderers/LandmarkLayerRenderer.kt b/src/main/kotlin/solve/rendering/engine/core/renderers/LandmarkLayerRenderer.kt index 58bea72ce..3e1cc5cfc 100644 --- a/src/main/kotlin/solve/rendering/engine/core/renderers/LandmarkLayerRenderer.kt +++ b/src/main/kotlin/solve/rendering/engine/core/renderers/LandmarkLayerRenderer.kt @@ -97,21 +97,4 @@ abstract class LandmarkLayerRenderer( this._visibleLayers = newVisibleLayers _visibleLayersSelectionIndices = newVisibleLayersSelectionIndices } - - protected fun getFrameTopLeftShaderPosition(frameIndex: Int): Vector2f { - val frameXIndex = frameIndex % gridWidth - val frameYIndex = frameIndex / gridWidth - - return Vector2f( - frameXIndex.toFloat() * framesRatio + frameXIndex * FramesSpacing, - frameYIndex.toFloat() + frameYIndex * FramesSpacing - ) - } - - protected fun getFramePixelShaderPosition(frameIndex: Int, framePixelPosition: Vector2f): Vector2f { - val frameRelativePosition = Vector2f(framePixelPosition) / framesSize.y - val frameTopLeftPosition = getFrameTopLeftShaderPosition(frameIndex) - - return frameTopLeftPosition + frameRelativePosition - } } diff --git a/src/main/kotlin/solve/rendering/engine/core/renderers/LinesLayerRenderer.kt b/src/main/kotlin/solve/rendering/engine/core/renderers/LinesLayerRenderer.kt index 2bd9a6560..2244f1ba6 100644 --- a/src/main/kotlin/solve/rendering/engine/core/renderers/LinesLayerRenderer.kt +++ b/src/main/kotlin/solve/rendering/engine/core/renderers/LinesLayerRenderer.kt @@ -38,7 +38,6 @@ class LinesLayerRenderer( override fun createNewBatch(zIndex: Int): RenderBatch { val shaderAttributesTypes = listOf( ShaderAttributeType.FLOAT2, - ShaderAttributeType.FLOAT, ShaderAttributeType.FLOAT3 ) @@ -72,7 +71,7 @@ class LinesLayerRenderer( val linesWidth = getLinesWidth() visibleLineLayersLandmarks.forEachIndexed { visibleLayerIndex, linesLayerLandmarks -> - linesLayerLandmarks.forEachIndexed { lineLandmarkIndex, lineLandmark -> + linesLayerLandmarks.forEach { lineLandmark -> val selectionLayerIndex = visibleLayersSelectionIndices[visibleLayerIndex] val batch = getAvailableBatch(null, 0) @@ -119,10 +118,8 @@ class LinesLayerRenderer( val firstVertexPosition = if (sideIndex == 0) upperVertexPosition else bottomVertexPosition val secondVertexPosition = if (sideIndex == 0) bottomVertexPosition else upperVertexPosition batch.pushVector2f(firstVertexPosition) - batch.pushFloat(lineLandmarkIndex.toFloat()) batch.pushVector3f(lineColorVector) batch.pushVector2f(secondVertexPosition) - batch.pushFloat(lineLandmarkIndex.toFloat()) batch.pushVector3f(lineColorVector) } } diff --git a/src/main/kotlin/solve/rendering/engine/core/renderers/PointAssociationsRenderer.kt b/src/main/kotlin/solve/rendering/engine/core/renderers/PointAssociationsRenderer.kt new file mode 100644 index 000000000..9d717377c --- /dev/null +++ b/src/main/kotlin/solve/rendering/engine/core/renderers/PointAssociationsRenderer.kt @@ -0,0 +1,114 @@ +package solve.rendering.engine.core.renderers + +import org.joml.Vector2f +import org.joml.Vector3f +import solve.constants.ShadersPointAssociationFragmentPath +import solve.constants.ShadersPointAssociationVertexPath +import solve.rendering.engine.Window +import solve.rendering.engine.core.batch.PrimitiveType +import solve.rendering.engine.core.batch.RenderBatch +import solve.rendering.engine.shader.ShaderAttributeType +import solve.rendering.engine.shader.ShaderProgram +import solve.rendering.engine.shader.ShaderType +import solve.rendering.engine.utils.minus +import solve.rendering.engine.utils.plus +import solve.rendering.engine.utils.times +import solve.rendering.engine.utils.toFloatVector +import solve.rendering.engine.utils.toVector2i +import solve.scene.view.association.AssociationManager + +class PointAssociationsRenderer( + window: Window, + private val associationManager: AssociationManager +) : Renderer(window) { + override val maxBatchSize = 1000 + + init { + renderPriority = 1 + } + + override fun createShaderProgram(): ShaderProgram { + val shaderProgram = ShaderProgram() + shaderProgram.addShader(ShadersPointAssociationVertexPath, ShaderType.VERTEX) + shaderProgram.addShader(ShadersPointAssociationFragmentPath, ShaderType.FRAGMENT) + shaderProgram.link() + + return shaderProgram + } + + override fun createNewBatch(zIndex: Int): RenderBatch { + val shaderAttributesTypes = listOf( + ShaderAttributeType.FLOAT2, + ShaderAttributeType.FLOAT3 + ) + + return RenderBatch( + maxBatchSize, + zIndex, + PrimitiveType.Quad, + shaderAttributesTypes + ) + } + + override fun uploadUniforms(shaderProgram: ShaderProgram) { + shaderProgram.uploadMatrix4f(ProjectionUniformName, window.calculateProjectionMatrix()) + } + + override fun updateBatchesData() { + val associationConnections = associationManager.associationConnections + + associationConnections.forEach { associationConnection -> + associationConnection.associationLines.forEach { associationLine -> + val batch = getAvailableBatch(null, 0) + + val lineStartShaderPosition = getFramePixelShaderPosition( + associationLine.firstKeypointFrameIndex, + associationLine.firstKeypoint.coordinate.toVector2i().toFloatVector() + ) + val lineFinishShaderPosition = getFramePixelShaderPosition( + associationLine.secondKeypointFrameIndex, + associationLine.secondKeypoint.coordinate.toVector2i().toFloatVector() + ) + + println("line start shader pos: ${lineStartShaderPosition}") + println("line finish shader pos: ${lineFinishShaderPosition}") + + val lineVector = lineFinishShaderPosition - lineStartShaderPosition + val normalVector = Vector2f(-lineVector.y, lineVector.x).normalize() + val linePoints = listOf(lineStartShaderPosition, lineFinishShaderPosition) + + val firstKeypointColor = + associationLine.firstKeypoint.layerSettings.getColor(associationLine.firstKeypoint) + val secondKeypointColor = + associationLine.secondKeypoint.layerSettings.getColor(associationLine.secondKeypoint) + val lineColor = firstKeypointColor.interpolate(secondKeypointColor, 0.5) + + val lineColorVector = Vector3f( + lineColor.red.toFloat(), + lineColor.green.toFloat(), + lineColor.blue.toFloat() + ) + + linePoints.forEachIndexed { sideIndex, linePoint -> + val pointToVertexVector = Vector2f(normalVector) * AssociationLineWidth / window.camera.zoom / + DefaultLocalVerticesPositionsDivider + + val upperVertexPosition = linePoint + pointToVertexVector + val bottomVertexPosition = linePoint - pointToVertexVector + val firstVertexPosition = if (sideIndex == 0) upperVertexPosition else bottomVertexPosition + val secondVertexPosition = if (sideIndex == 0) bottomVertexPosition else upperVertexPosition + batch.pushVector2f(firstVertexPosition) + batch.pushVector3f(lineColorVector) + batch.pushVector2f(secondVertexPosition) + batch.pushVector3f(lineColorVector) + } + } + } + } + + companion object { + private const val DefaultLocalVerticesPositionsDivider = 800f + + private const val AssociationLineWidth = 3.0f + } +} diff --git a/src/main/kotlin/solve/rendering/engine/core/renderers/Renderer.kt b/src/main/kotlin/solve/rendering/engine/core/renderers/Renderer.kt index c584aa185..11e3f177d 100644 --- a/src/main/kotlin/solve/rendering/engine/core/renderers/Renderer.kt +++ b/src/main/kotlin/solve/rendering/engine/core/renderers/Renderer.kt @@ -8,6 +8,7 @@ import solve.rendering.engine.Window import solve.rendering.engine.core.batch.RenderBatch import solve.rendering.engine.core.texture.Texture import solve.rendering.engine.shader.ShaderProgram +import solve.rendering.engine.utils.plus import solve.rendering.engine.utils.toIntVector import solve.scene.model.VisualizationFrame import solve.utils.ceilToInt @@ -168,6 +169,24 @@ abstract class Renderer(protected val window: Window) : Comparable { batches.forEach { it.rebuffer() } } + protected fun getFrameTopLeftShaderPosition(frameIndex: Int): Vector2f { + val frameXIndex = frameIndex % gridWidth + val frameYIndex = frameIndex / gridWidth + val framesRatio = framesSize.x / framesSize.y + + return Vector2f( + frameXIndex.toFloat() * framesRatio + frameXIndex * FramesSpacing, + frameYIndex.toFloat() + frameYIndex * FramesSpacing + ) + } + + protected fun getFramePixelShaderPosition(frameIndex: Int, framePixelPosition: Vector2f): Vector2f { + val frameRelativePosition = Vector2f(framePixelPosition) / framesSize.y + val frameTopLeftPosition = getFrameTopLeftShaderPosition(frameIndex) + + return frameTopLeftPosition + frameRelativePosition + } + companion object { const val ProjectionUniformName = "uProjection" const val GridWidthUniformName = "uGridWidth" diff --git a/src/main/kotlin/solve/rendering/engine/scene/Scene.kt b/src/main/kotlin/solve/rendering/engine/scene/Scene.kt index fa31b65c0..883c08992 100644 --- a/src/main/kotlin/solve/rendering/engine/scene/Scene.kt +++ b/src/main/kotlin/solve/rendering/engine/scene/Scene.kt @@ -4,14 +4,24 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import org.joml.Vector2i import solve.rendering.engine.core.renderers.FramesRenderer import solve.rendering.engine.core.renderers.LandmarkLayerRenderer +import solve.rendering.engine.core.renderers.PointAssociationsRenderer +import solve.rendering.engine.utils.toFloatVector import solve.scene.model.Layer +import solve.scene.model.Scene +import solve.scene.model.VisualizationFrame import java.util.concurrent.CopyOnWriteArrayList -class Scene(val framesRenderer: FramesRenderer) { +class Scene( + val framesRenderer: FramesRenderer, + val pointAssociationsRenderer: PointAssociationsRenderer, + val getProjectScene: () -> Scene? +) { private val _landmarkRenderers = CopyOnWriteArrayList() private val _landmarkRenderersLayers = mutableMapOf() + val landmarkRenderers: List get() = _landmarkRenderers val landmarkLayerRendererLayers: Map @@ -19,6 +29,35 @@ class Scene(val framesRenderer: FramesRenderer) { var isFramesRendererInitializing = false + fun setNewScene(scene: Scene) { + val framesSize = Vector2i(scene.frameSize.width.toInt(), scene.frameSize.height.toInt()) + + framesRenderer.setNewSceneFrames(scene.frames, framesSize.toFloatVector()) + pointAssociationsRenderer.setNewSceneFrames(scene.frames, framesSize.toFloatVector()) + landmarkRenderers.forEach { it.setNewSceneFrames(scene.frames, framesSize.toFloatVector()) } + } + + fun setFramesSelection(selection: List) { + framesRenderer.setFramesSelection(selection) + pointAssociationsRenderer.setFramesSelection(selection) + _landmarkRenderers.forEach { + it.setFramesSelection(selection) + } + landmarkRenderers.forEach { renderer -> + val rendererLayerSettings = + landmarkLayerRendererLayers[renderer]?.settings ?: return@forEach + val lastLayersWithCommonSettings = + getProjectScene()?.getLayersWithCommonSettings(rendererLayerSettings, selection) ?: return@forEach + renderer.setFramesSelectionLayers(lastLayersWithCommonSettings) + } + } + + fun setColumnsNumber(columnsNumber: Int) { + framesRenderer.setNewGridWidth(columnsNumber) + pointAssociationsRenderer.setNewGridWidth(columnsNumber) + landmarkRenderers.forEach { it.setNewGridWidth(columnsNumber) } + } + fun update() { render() } @@ -48,6 +87,7 @@ class Scene(val framesRenderer: FramesRenderer) { return } + pointAssociationsRenderer.render() _landmarkRenderers.sort() _landmarkRenderers.forEach { it.render() } } diff --git a/src/main/kotlin/solve/scene/view/SceneView.kt b/src/main/kotlin/solve/scene/view/SceneView.kt index 82b1bf632..387405cf5 100644 --- a/src/main/kotlin/solve/scene/view/SceneView.kt +++ b/src/main/kotlin/solve/scene/view/SceneView.kt @@ -10,7 +10,6 @@ import solve.scene.SceneFacade import solve.scene.controller.SceneController import solve.scene.model.Landmark import solve.scene.model.VisualizationFrame -import solve.scene.view.association.AssociationsManager import tornadofx.View import tornadofx.onChange import javafx.scene.input.MouseButton as JavaFXMouseButton @@ -27,9 +26,6 @@ class SceneView : View() { private var wasMouseDragging = false - var currentAssociationsManager: AssociationsManager? = null - private set - override val root = canvas.canvas private val sceneChangedEventHandler = InvalidationListener { diff --git a/src/main/kotlin/solve/scene/view/association/AssociationConnection.kt b/src/main/kotlin/solve/scene/view/association/AssociationConnection.kt new file mode 100644 index 000000000..48b9ef88e --- /dev/null +++ b/src/main/kotlin/solve/scene/view/association/AssociationConnection.kt @@ -0,0 +1,8 @@ +package solve.scene.view.association + +data class AssociationConnection( + val firstFrameIndex: Int, + val secondFrameIndex: Int, + val keypointLayerIndex: Int, + val associationLines: List +) diff --git a/src/main/kotlin/solve/scene/view/association/AssociationLine.kt b/src/main/kotlin/solve/scene/view/association/AssociationLine.kt index ec15f0f50..561775296 100644 --- a/src/main/kotlin/solve/scene/view/association/AssociationLine.kt +++ b/src/main/kotlin/solve/scene/view/association/AssociationLine.kt @@ -1,64 +1,12 @@ package solve.scene.view.association -import javafx.beans.InvalidationListener -import javafx.beans.property.BooleanProperty -import javafx.beans.property.DoubleProperty -import javafx.beans.property.ObjectProperty -import javafx.scene.Node -import javafx.scene.paint.Color -import javafx.scene.shape.Line -import solve.scene.model.Point -import solve.utils.structures.DoublePoint - -/** - * Creates and manages line between two associated keypoints. - * Manages color and visibility of line to suit the corresponding layer. - * Manages position of the line to keep end of the line at keypoints. - * - * @param firstFramePosition position of the frame of the first keypoint within the grid. - * @param secondFramePosition position of the frame of the second keypoint within the grid. - * @param firstKeypointCoordinate coordinate of the first keypoint within its frame. - * @param secondKeypointCoordinate coordinate of the second keypoint within its frame. - */ -class AssociationLine( - private var firstFramePosition: DoublePoint, - private var secondFramePosition: DoublePoint, - private val firstKeypointCoordinate: Point, - private val secondKeypointCoordinate: Point, - private val scale: DoubleProperty, - colorProperty: ObjectProperty, - enabledProperty: BooleanProperty -) { - private val line = Line() - val node: Node = line - - private val scaleChangedListener = InvalidationListener { - updateLinePosition() - } - - init { - updateLinePosition() - line.strokeProperty().bind(colorProperty) - line.visibleProperty().bind(enabledProperty) - scale.addListener(scaleChangedListener) - } - - fun updateFramesPosition(firstFramePosition: DoublePoint, secondFramePosition: DoublePoint) { - this.firstFramePosition = firstFramePosition - this.secondFramePosition = secondFramePosition - updateLinePosition() - } - - fun dispose() { - line.strokeProperty().unbind() - line.visibleProperty().unbind() - scale.removeListener(scaleChangedListener) - } - - private fun updateLinePosition() { - line.startX = (firstFramePosition.x + firstKeypointCoordinate.x) * scale.value - line.startY = (firstFramePosition.y + firstKeypointCoordinate.y) * scale.value - line.endX = (secondFramePosition.x + secondKeypointCoordinate.x) * scale.value - line.endY = (secondFramePosition.y + secondKeypointCoordinate.y) * scale.value - } -} +import org.joml.Vector2i +import solve.scene.model.Landmark + +data class AssociationLine( + val keypointsUID: Long, + val firstKeypointFrameIndex: Int, + val secondKeypointFrameIndex: Int, + val firstKeypoint: Landmark.Keypoint, + val secondKeypoint: Landmark.Keypoint +) diff --git a/src/main/kotlin/solve/scene/view/association/AssociationManager.kt b/src/main/kotlin/solve/scene/view/association/AssociationManager.kt new file mode 100644 index 000000000..9acfa7a4f --- /dev/null +++ b/src/main/kotlin/solve/scene/view/association/AssociationManager.kt @@ -0,0 +1,67 @@ +package solve.scene.view.association + +import solve.scene.model.Layer +import solve.scene.model.VisualizationFrame + +class AssociationManager { + private var framesSelection = listOf() + private val _associationConnections = mutableListOf() + val associationConnections: List + get() = _associationConnections + + fun setFramesSelection(framesSelection: List) { + this.framesSelection = framesSelection + _associationConnections.clear() + } + + fun associate(firstFrameIndex: Int, secondFrameIndex: Int, associatingKeypointLayerIndex: Int = 0) { + if (firstFrameIndex !in framesSelection.indices || secondFrameIndex !in framesSelection.indices) { + println("Associating frames indices is out of range!") + return + } + + val firstFrame = framesSelection[firstFrameIndex] + val secondFrame = framesSelection[secondFrameIndex] + val firstFrameKeypointLayers = firstFrame.layers.filterIsInstance() + val secondFramePointLayers = secondFrame.layers.filterIsInstance() + + if (associatingKeypointLayerIndex !in firstFrameKeypointLayers.indices || + associatingKeypointLayerIndex !in secondFramePointLayers.indices) { + println("There are no point layer with a given index to associate!") + return + } + + val firstFramePointLayer = firstFrameKeypointLayers[associatingKeypointLayerIndex] + val secondFramePointLayer = secondFramePointLayers[associatingKeypointLayerIndex] + val firstFrameKeypoints = firstFramePointLayer.getLandmarks().associateBy { it.uid } + val secondFrameKeypoints = secondFramePointLayer.getLandmarks().associateBy { it.uid } + + val associationLines = mutableListOf() + firstFrameKeypoints.keys.forEach { firstFrameKeypointUID -> + if (secondFrameKeypoints.contains(firstFrameKeypointUID)) { + val firstKeypoint = firstFrameKeypoints[firstFrameKeypointUID] ?: return@forEach + val secondKeypoint = secondFrameKeypoints[firstFrameKeypointUID] ?: return@forEach + + associationLines.add(AssociationLine( + firstFrameKeypointUID, + firstFrameIndex, + secondFrameIndex, + firstKeypoint, + secondKeypoint + ) + ) + } + } + _associationConnections.add( + AssociationConnection( + firstFrameIndex, secondFrameIndex, associatingKeypointLayerIndex, associationLines) + ) + } + + fun clearAssociation(frameIndex: Int, associatedKeypointLayerIndex: Int = 0) { + _associationConnections.removeAll { + it.keypointLayerIndex == associatedKeypointLayerIndex && + (it.firstFrameIndex == frameIndex || it.secondFrameIndex == frameIndex) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/solve/scene/view/association/AssociationsManager.kt b/src/main/kotlin/solve/scene/view/association/AssociationsManager.kt deleted file mode 100644 index c50d4022e..000000000 --- a/src/main/kotlin/solve/scene/view/association/AssociationsManager.kt +++ /dev/null @@ -1,214 +0,0 @@ -package solve.scene.view.association - -import javafx.beans.InvalidationListener -import javafx.beans.property.BooleanProperty -import javafx.beans.property.DoubleProperty -import javafx.beans.property.IntegerProperty -import javafx.beans.property.ObjectProperty -import javafx.scene.paint.Color -import solve.utils.structures.DoublePoint -import solve.utils.structures.Size -import tornadofx.add -import tornadofx.toObservable - -/** - * Responsible for creating associations line between keypoints from selected frames and layers. - * - * @param T type of data objects backing to frames. - * @param S type of objects, which will be associated, keypoints in our case. - * - * @param frameSize size of a frame on the scene without indent, used to calculate association lines positions. - * @param framesIndent margin between frames on the scene, used to calculate association lines positions. - * @param scale global scale property, used to recalculate associations lines positions when scale changed. - * @param frames list of frames data, used to calculate frame column and row. - * @param outOfFramesLayer pane, where association lines and frames adorners placed. - */ -class AssociationsManager( - private val frameSize: Size, - private val framesIndent: Double, - private val scale: DoubleProperty, - private val frames: List, - private val columnsNumber: IntegerProperty, - private val outOfFramesLayer: OutOfFramesLayer -) { - /** - * name of the current selected layer. - */ - val chosenLayerName - get() = firstFrameAssociationParameters?.key?.layerName - private var firstFrameAssociationParameters: AssociationParameters? = null - - private val columnsNumberChangedListener = InvalidationListener { - updateLinesPosition() - } - - init { - columnsNumber.addListener(columnsNumberChangedListener) - } - - /** - * Matches first association frame and chosen layer to a list of all associated second frames and drawn lines. - * - * Example: - * drawnAssociations[AssociationKey(25, "keypoints12345")][27] returns list of drawn lines between 25 and 27 frames, - * for landmarks in "keypoints12345" layer. - */ - val drawnAssociations = - mutableMapOf, MutableMap>>().toObservable() - private val drawnAdorners = mutableMapOf() - - /** - * Chooses the frame to associate keypoints of the specified layer with another. - * Draws an adorner on top of the selected frame. - */ - fun initAssociation(associationParameters: AssociationParameters) { - val firstFrame = firstFrameAssociationParameters?.key?.frame - firstFrame?.apply { clearAdorner(firstFrame) } - firstFrameAssociationParameters = associationParameters - drawAdorner(associationParameters.key.frame) - } - - /** - * Draws lines between keypoints in previously selected layer on the first and second frames. - * - * The drawn adorner is cleared. - * - * @param colorProperty chosen layer color property from the settings, used to keep lines color the same to keypoints. - * @param enabledProperty chosen layer enabled property from the settings, used to keep visibility of lines the same to keypoints. - */ - fun associate( - secondFrameParameters: AssociationParameters, - colorProperty: ObjectProperty, - enabledProperty: BooleanProperty - ) { - val firstFrameParameters = firstFrameAssociationParameters ?: return - val firstFrame = firstFrameParameters.key.frame - val secondFrame = secondFrameParameters.key.frame - - clearAdorner(firstFrame) - - val isAlreadyAssociated = isAlreadyAssociated(firstFrameParameters.key, secondFrame) - if (isAlreadyAssociated || firstFrame == secondFrame) { - firstFrameAssociationParameters = null - return - } - - associate( - firstFrameParameters.key, - secondFrameParameters.key, - firstFrameParameters.landmarks, - secondFrameParameters.landmarks, - colorProperty, - enabledProperty - ) - firstFrameAssociationParameters = null - } - - private fun isAlreadyAssociated(associationKey: AssociationKey, secondFrame: T) = - drawnAssociations[associationKey]?.containsKey(secondFrame) == true - - private fun drawAdorner(frame: T) { - val framePosition = getFramePosition(frame) - val adorner = AssociationAdorner(frameSize.width, frameSize.height, framePosition, scale) - drawnAdorners[frame] = adorner - outOfFramesLayer.add(adorner.node) - } - - private fun clearAdorner(frame: T) { - val adorner = drawnAdorners[frame] ?: return - outOfFramesLayer.children.remove(adorner.node) - adorner.dispose() - drawnAdorners.remove(frame) - } - - private fun associate( - firstKey: AssociationKey, - secondKey: AssociationKey, - firstLandmarks: List, - secondLandmarks: List, - colorProperty: ObjectProperty, - enabledProperty: BooleanProperty - ) { - val firstFramePosition = getFramePosition(firstKey.frame) - val secondFramePosition = getFramePosition(secondKey.frame) - - val lines = firstLandmarks.map { firstLandmark -> - val secondLandmark = secondLandmarks.firstOrNull { landmark -> - firstLandmark.uid == landmark.uid - } ?: return@map null // not all keypoints are matching - - AssociationLine( - firstFramePosition, - secondFramePosition, - firstLandmark.coordinate, - secondLandmark.coordinate, - scale, - colorProperty, - enabledProperty - ) - }.filterNotNull() - - lines.forEach { line -> - outOfFramesLayer.add(line.node) - } - - drawnAssociations.putIfAbsent(firstKey, mutableMapOf()) - drawnAssociations[firstKey]?.set(secondKey.frame, lines) - - // should save association twice to make it possible to clear association from the target frame. - drawnAssociations.putIfAbsent(secondKey, mutableMapOf()) - drawnAssociations[secondKey]?.set(firstKey.frame, lines) - } - - private fun getFramePosition(frame: T): DoublePoint { - val indexOfFrame = frames.indexOf(frame) - val firstFrameRow = indexOfFrame / columnsNumber.value - val firstFrameColumn = indexOfFrame % columnsNumber.value - return DoublePoint( - firstFrameColumn * (frameSize.width + framesIndent), - firstFrameRow * (frameSize.height + framesIndent) - ) - } - - /** - * Clears associations of chosen layer and frame to all another frames. - */ - fun clearAssociation(key: AssociationKey) { - drawnAssociations[key]?.forEach { (frame, lines) -> - val secondKey = AssociationKey(frame, key.layerName) - drawnAssociations[secondKey]?.remove(key.frame) - lines.forEach { line -> - outOfFramesLayer.children.remove(line.node) - line.dispose() - } - } - drawnAssociations.remove(key) - } - - fun dispose() { - columnsNumber.removeListener(columnsNumberChangedListener) - } - - private fun updateLinesPosition() { - drawnAssociations.forEach { (firstFrameKey, associations) -> - associations.forEach { (secondFrame, lines) -> - lines.forEach { line -> - line.updateFramesPosition(getFramePosition(firstFrameKey.frame), getFramePosition(secondFrame)) - } - } - } - } - - /** - * Data structure aimed to address associations. - */ - data class AssociationKey(val frame: T, val layerName: String) - - /** - * Data structure, which aggregates association data. - */ - data class AssociationParameters( - val key: AssociationKey, - val landmarks: List - ) -} diff --git a/src/main/kotlin/solve/scene/view/association/OutOfFramesLayer.kt b/src/main/kotlin/solve/scene/view/association/OutOfFramesLayer.kt deleted file mode 100644 index 5b3388416..000000000 --- a/src/main/kotlin/solve/scene/view/association/OutOfFramesLayer.kt +++ /dev/null @@ -1,13 +0,0 @@ -package solve.scene.view.association - -import javafx.scene.layout.Pane - -/** - * A region where not in frames visual elements such as association adorners and lines is located. - */ -class OutOfFramesLayer : Pane() { - init { - // The element should pass all mouse event to the scene - isMouseTransparent = true - } -} diff --git a/src/main/resources/engine/shaders/association/point/point.frag b/src/main/resources/engine/shaders/association/point/point.frag new file mode 100644 index 000000000..5d28dbbbc --- /dev/null +++ b/src/main/resources/engine/shaders/association/point/point.frag @@ -0,0 +1,10 @@ +#version 330 core + +in vec3 fColor; + +out vec4 color; + +void main() +{ + color = vec4(fColor, 1.0); +} \ No newline at end of file diff --git a/src/main/resources/engine/shaders/association/point/point.vert b/src/main/resources/engine/shaders/association/point/point.vert new file mode 100644 index 000000000..f3046ec49 --- /dev/null +++ b/src/main/resources/engine/shaders/association/point/point.vert @@ -0,0 +1,14 @@ +#version 330 core + +layout (location=0) in vec2 aPos; +layout (location=1) in vec3 aColor; + +uniform mat4 uProjection; + +out vec3 fColor; + +void main() +{ + fColor = aColor; + gl_Position = uProjection * vec4(aPos, 0.0, 1.0); +} diff --git a/src/main/resources/engine/shaders/landmark/line/line.frag b/src/main/resources/engine/shaders/landmark/line/line.frag index ccc5a5533..8e32b6e84 100644 --- a/src/main/resources/engine/shaders/landmark/line/line.frag +++ b/src/main/resources/engine/shaders/landmark/line/line.frag @@ -1,6 +1,5 @@ #version 330 core -in float fIndex; in vec3 fColor; uniform int uUseCommonColor; diff --git a/src/main/resources/engine/shaders/landmark/line/line.vert b/src/main/resources/engine/shaders/landmark/line/line.vert index 7a07ee696..f3046ec49 100644 --- a/src/main/resources/engine/shaders/landmark/line/line.vert +++ b/src/main/resources/engine/shaders/landmark/line/line.vert @@ -1,17 +1,14 @@ #version 330 core layout (location=0) in vec2 aPos; -layout (location=1) in float aIndex; -layout (location=2) in vec3 aColor; +layout (location=1) in vec3 aColor; uniform mat4 uProjection; -out float fIndex; out vec3 fColor; void main() { - fIndex = aIndex; fColor = aColor; gl_Position = uProjection * vec4(aPos, 0.0, 1.0); } diff --git a/src/test/kotlin/solve/interactive/scene/view/association/AssociationAdornerTests.kt b/src/test/kotlin/solve/interactive/scene/view/association/AssociationConnectionAdornerTests.kt similarity index 96% rename from src/test/kotlin/solve/interactive/scene/view/association/AssociationAdornerTests.kt rename to src/test/kotlin/solve/interactive/scene/view/association/AssociationConnectionAdornerTests.kt index 727ace4ca..dd924f776 100644 --- a/src/test/kotlin/solve/interactive/scene/view/association/AssociationAdornerTests.kt +++ b/src/test/kotlin/solve/interactive/scene/view/association/AssociationConnectionAdornerTests.kt @@ -13,7 +13,7 @@ import solve.testMemoryLeak import solve.utils.structures.DoublePoint @ExtendWith(ApplicationExtension::class) -internal class AssociationAdornerTests : InteractiveTestClass() { +internal class AssociationConnectionAdornerTests : InteractiveTestClass() { @Test fun `Adorner rectangle fills the corresponding frame`() { val width = 10.0 diff --git a/src/test/kotlin/solve/interactive/scene/view/association/AssociationLineTests.kt b/src/test/kotlin/solve/interactive/scene/view/association/AssociationLineTests.kt deleted file mode 100644 index a1a97c55b..000000000 --- a/src/test/kotlin/solve/interactive/scene/view/association/AssociationLineTests.kt +++ /dev/null @@ -1,107 +0,0 @@ -package solve.interactive.scene.view.association - -import javafx.beans.property.SimpleBooleanProperty -import javafx.beans.property.SimpleDoubleProperty -import javafx.beans.property.SimpleObjectProperty -import javafx.scene.paint.Color -import javafx.scene.shape.Line -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.testfx.framework.junit5.ApplicationExtension -import solve.interactive.InteractiveTestClass -import solve.scene.model.Point -import solve.scene.view.association.AssociationLine -import solve.testMemoryLeak -import solve.utils.structures.DoublePoint -import tornadofx.* - -@ExtendWith(ApplicationExtension::class) -internal class AssociationLineTests : InteractiveTestClass() { - private lateinit var firstFramePosition: DoublePoint - private lateinit var secondFramePosition: DoublePoint - private lateinit var firstKeypointCoordinate: Point - private lateinit var secondKeypointCoordinate: Point - private lateinit var scale: SimpleDoubleProperty - private lateinit var initialColor: Color - private lateinit var colorProperty: SimpleObjectProperty - private lateinit var enabledProperty: SimpleBooleanProperty - private lateinit var associationLine: AssociationLine - - @BeforeEach - fun setUp() { - firstFramePosition = DoublePoint(0.0, 0.0) - secondFramePosition = DoublePoint(100.0, 150.0) - firstKeypointCoordinate = Point(0, 0) - secondKeypointCoordinate = Point(15, 25) - scale = SimpleDoubleProperty(1.0) - initialColor = c("F0F0F0") - colorProperty = SimpleObjectProperty(initialColor) - enabledProperty = SimpleBooleanProperty(true) - associationLine = createAssociationLine() - } - - @Test - fun `Updates stroke when color changed`() { - val line = associationLine.node as Line - assertEquals(initialColor, line.stroke) - - val newColor = c("0FFF00") - colorProperty.value = newColor - assertEquals(newColor, line.stroke) - } - - @Test - fun `Updates visibility when enabled changed`() { - val line = associationLine.node as Line - assertEquals(enabledProperty.value, line.isVisible) - - enabledProperty.value = false - assertEquals(false, line.isVisible) - } - - @Test - fun `Correct coordinates within the association layer pane`() { - val line = associationLine.node as Line - assertEquals(firstFramePosition.x + firstKeypointCoordinate.x, line.layoutX) - assertEquals(firstFramePosition.y + firstKeypointCoordinate.y, line.layoutY) - } - - @Test - fun `Updates coordinates within the association layer pane when scale changed`() { - val line = associationLine.node as Line - scale.value *= 2 - assertEquals(firstFramePosition.x * scale.value + firstKeypointCoordinate.x * scale.value, line.layoutX) - assertEquals(firstFramePosition.y * scale.value + firstKeypointCoordinate.y * scale.value, line.layoutY) - } - - @Test - fun `Line can be garbage collected after dispose`() { - val factory = { createAssociationLine() } - val action: (AssociationLine) -> Unit = { line -> line.dispose() } - testMemoryLeak(factory, action) - } - - @Test - fun `Update frame position`() { - val line = associationLine.node as Line - firstFramePosition = DoublePoint(122.0, 22.0) - secondFramePosition = DoublePoint(283.0, 111.0) - associationLine.updateFramesPosition(firstFramePosition, secondFramePosition) - assertEquals(firstFramePosition.x + firstKeypointCoordinate.x, line.startX) - assertEquals(firstFramePosition.y + firstKeypointCoordinate.y, line.startY) - assertEquals(secondFramePosition.x + secondKeypointCoordinate.x, line.endX) - assertEquals(secondFramePosition.y + secondKeypointCoordinate.y, line.endY) - } - - private fun createAssociationLine() = AssociationLine( - firstFramePosition, - secondFramePosition, - firstKeypointCoordinate, - secondKeypointCoordinate, - scale, - colorProperty, - enabledProperty - ) -} diff --git a/src/test/kotlin/solve/interactive/scene/view/association/AssociationsManagerTests.kt b/src/test/kotlin/solve/interactive/scene/view/association/AssociationsManagerTests.kt deleted file mode 100644 index c59449a60..000000000 --- a/src/test/kotlin/solve/interactive/scene/view/association/AssociationsManagerTests.kt +++ /dev/null @@ -1,749 +0,0 @@ -package solve.interactive.scene.view.association - -import javafx.beans.property.BooleanProperty -import javafx.beans.property.DoubleProperty -import javafx.beans.property.IntegerProperty -import javafx.beans.property.ObjectProperty -import javafx.beans.property.SimpleBooleanProperty -import javafx.beans.property.SimpleDoubleProperty -import javafx.beans.property.SimpleIntegerProperty -import javafx.beans.property.SimpleObjectProperty -import javafx.scene.paint.Color -import javafx.scene.shape.Line -import org.junit.jupiter.api.Assertions.assertDoesNotThrow -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertNotNull -import org.junit.jupiter.api.Assertions.assertNull -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Assertions.fail -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.testfx.framework.junit5.ApplicationExtension -import solve.interactive.InteractiveTestClass -import solve.scene.model.Point -import solve.scene.view.association.Associatable -import solve.scene.view.association.AssociationsManager -import solve.scene.view.association.OutOfFramesLayer -import solve.testMemoryLeak -import solve.utils.structures.DoublePoint -import solve.utils.structures.Size -import tornadofx.* - -@ExtendWith(ApplicationExtension::class) -internal class AssociationsManagerTests : InteractiveTestClass() { - private class TestAssociatable(override val coordinate: Point, override val uid: Long) : Associatable - - private lateinit var frameSize: Size - private var frameIndent = 0.0 - private lateinit var scaleProperty: DoubleProperty - private lateinit var frames: List - private lateinit var columnsNumber: IntegerProperty - private lateinit var outOfFramesLayer: OutOfFramesLayer - private lateinit var associationsManager: AssociationsManager - - private val frameWidth = 200.0 - private val frameHeight = 100.0 - - @BeforeEach - fun setUp() { - frameSize = Size(frameWidth, frameHeight) - frameIndent = 10.0 - scaleProperty = SimpleDoubleProperty(1.0) - frames = (0 until 100).toMutableList() - columnsNumber = SimpleIntegerProperty(10) - outOfFramesLayer = OutOfFramesLayer() - associationsManager = - AssociationsManager(frameSize, frameIndent, scaleProperty, frames, columnsNumber, outOfFramesLayer) - } - - @Test - fun `Choose first association frame`() { - val layerName = "kp1" - val frameNumber = 25 - val associationKey = AssociationsManager.AssociationKey(frameNumber, layerName) - val landmarks = listOf(TestAssociatable(Point(1, 1), 1), TestAssociatable(Point(2, 2), 2)) - val associationParameters = AssociationsManager.AssociationParameters(associationKey, landmarks) - - associationsManager.initAssociation(associationParameters) - - val adorner = outOfFramesLayer.children.single() - val expectedRowIndex = 2 - val expectedColumnIndex = 5 - assertEquals((frameHeight + frameIndent) * expectedRowIndex, adorner.layoutY) - assertEquals((frameWidth + frameIndent) * expectedColumnIndex, adorner.layoutX) - assertEquals(0, associationsManager.drawnAssociations.size) - assertEquals(layerName, associationsManager.chosenLayerName) - } - - @Test - fun `Associate two frames from one row`() { - val framesRow = 2 - val firstFrameColumn = 5 - val secondFrameColumn = 7 - - val firstFrameLandmarks = listOf( - TestAssociatable(Point(1, 1), 1), - TestAssociatable(Point(130, 55), 2) - ) - - val secondFrameLandmarks = listOf( - TestAssociatable(Point(10, 4), 1), - TestAssociatable(Point(13, 77), 2) - ) - - testAssociation( - framesRow, - framesRow, - firstFrameColumn, - secondFrameColumn, - firstFrameLandmarks, - secondFrameLandmarks - ) - - assertNull(associationsManager.chosenLayerName) - assertTrue(outOfFramesLayer.children.all { it is Line }, "Adorner is drawn after association is done") - } - - @Test - fun `Associate two frames different rows`() { - val firstFrameRow = 2 - val firstFrameColumn = 5 - val secondFrameRow = 4 - val secondFrameColumn = 7 - - val firstFrameLandmarks = listOf( - TestAssociatable(Point(1, 1), 1), - TestAssociatable(Point(130, 55), 2) - ) - - val secondFrameLandmarks = listOf( - TestAssociatable(Point(10, 4), 1), - TestAssociatable(Point(13, 77), 2) - ) - - testAssociation( - firstFrameRow, - secondFrameRow, - firstFrameColumn, - secondFrameColumn, - firstFrameLandmarks, - secondFrameLandmarks - ) - } - - @Test - fun `Associate two frames if not all points are matching`() { - val firstFrameRow = 2 - val firstFrameColumn = 5 - val secondFrameRow = 4 - val secondFrameColumn = 7 - - val firstFrameLandmarks = listOf( - TestAssociatable(Point(1, 1), 1), - TestAssociatable(Point(130, 55), 2), - TestAssociatable(Point(2, 2), 3) - ) - - val secondFrameLandmarks = listOf( - TestAssociatable(Point(10, 4), 1), - TestAssociatable(Point(13, 77), 2), - TestAssociatable(Point(2, 2), 4) - ) - - testAssociation( - firstFrameRow, - secondFrameRow, - firstFrameColumn, - secondFrameColumn, - firstFrameLandmarks, - secondFrameLandmarks - ) - } - - @Test - fun `Associate frame with itself`() { - val frameNumber = 25 - val landmarks = listOf(TestAssociatable(Point(1, 1), 1), TestAssociatable(Point(2, 2), 2)) - - associateTwoFrames(frameNumber, frameNumber, landmarks, landmarks) - - assertNull(associationsManager.chosenLayerName) - assertEquals(0, outOfFramesLayer.children.size) - } - - @Test - fun `Associate frame with a few frames`() { - val firstFrameRow = 2 - val firstFrameColumn = 5 - val secondFrameRow = 4 - val secondFrameColumn = 7 - val thirdFrameRow = 3 - val thirdFrameColumn = 6 - - val firstFrameLandmarks = listOf( - TestAssociatable(Point(1, 1), 1), - TestAssociatable(Point(130, 55), 2) - ) - val secondFrameLandmarks = listOf( - TestAssociatable(Point(10, 4), 1), - TestAssociatable(Point(13, 77), 2) - ) - val thirdFrameLandmarks = listOf( - TestAssociatable(Point(1, 2), 1), - TestAssociatable(Point(10, 10), 2) - ) - - testAssociation( - firstFrameRow, - secondFrameRow, - firstFrameColumn, - secondFrameColumn, - firstFrameLandmarks, - secondFrameLandmarks - ) - - testAssociation( - firstFrameRow, - thirdFrameRow, - firstFrameColumn, - thirdFrameColumn, - firstFrameLandmarks, - thirdFrameLandmarks, - listOf(47) - ) - } - - @Test - fun `Associate multiple layers`() { - val firstLayerName = "kp1" - val secondLayerName = "kp2" - val row = 1 - val firstFrameColumn = 5 - val secondFrameColumn = 7 - - val firstLayerFirstFrameLandmarks = listOf( - TestAssociatable(Point(1, 1), 1), - TestAssociatable(Point(130, 55), 2) - ) - val secondLayerFirstFrameLandmarks = listOf( - TestAssociatable(Point(4, 2), 1), - TestAssociatable(Point(111, 5), 2) - ) - - val firstLayerSecondFrameLandmarks = listOf( - TestAssociatable(Point(10, 4), 1), - TestAssociatable(Point(13, 77), 2) - ) - val secondLayerSecondFrameLandmarks = listOf( - TestAssociatable(Point(46, 21), 1), - TestAssociatable(Point(11, 35), 2) - ) - - testAssociation( - row, - row, - firstFrameColumn, - secondFrameColumn, - firstLayerFirstFrameLandmarks, - firstLayerSecondFrameLandmarks, - layerName = firstLayerName - ) - testAssociation( - row, - row, - firstFrameColumn, - secondFrameColumn, - secondLayerFirstFrameLandmarks, - secondLayerSecondFrameLandmarks, - layerName = secondLayerName - ) - } - - @Test - fun `Clear association from the source frame`() { - val row = 2 - val firstFrameColumn = 5 - val secondFrameColumn = 6 - val firstFrameNumber = row * columnsNumber.value + firstFrameColumn - val secondFrameNumber = row * columnsNumber.value + secondFrameColumn - - val firstFrameLandmarks = listOf( - TestAssociatable(Point(1, 1), 1), - TestAssociatable(Point(130, 55), 2) - ) - val secondFrameLandmarks = listOf( - TestAssociatable(Point(10, 4), 1), - TestAssociatable(Point(13, 77), 2) - ) - - val layerName = "kp1" - val firstFrameAssociationKey = AssociationsManager.AssociationKey(firstFrameNumber, layerName) - - associateTwoFrames( - firstFrameNumber, - secondFrameNumber, - firstFrameLandmarks, - secondFrameLandmarks, - layerName = layerName - ) - - associationsManager.clearAssociation(firstFrameAssociationKey) - - assertTrue( - associationsManager.drawnAssociations.values.all { it.values.isEmpty() }, - "There are not cleared associations" - ) - } - - @Test - fun `Clear association from the target frame`() { - val row = 2 - val firstFrameColumn = 5 - val secondFrameColumn = 6 - val firstFrameNumber = row * columnsNumber.value + firstFrameColumn - val secondFrameNumber = row * columnsNumber.value + secondFrameColumn - - val firstFrameLandmarks = listOf( - TestAssociatable(Point(1, 1), 1), - TestAssociatable(Point(130, 55), 2) - ) - val secondFrameLandmarks = listOf( - TestAssociatable(Point(10, 4), 1), - TestAssociatable(Point(13, 77), 2) - ) - - val layerName = "kp1" - val secondFrameAssociationKey = AssociationsManager.AssociationKey(secondFrameNumber, layerName) - - associateTwoFrames( - firstFrameNumber, - secondFrameNumber, - firstFrameLandmarks, - secondFrameLandmarks, - layerName = layerName - ) - - associationsManager.clearAssociation(secondFrameAssociationKey) - - assertTrue( - associationsManager.drawnAssociations.values.all { it.values.isEmpty() }, - "There are not cleared associations" - ) - } - - @Test - fun `Clear associations from the frame associated with many frames`() { - val firstFrameNumber = 25 - val secondFrameNumber = 27 - val thirdFrameNumber = 26 - - val layerName = "kp1" - - val firstFrameLandmarks = listOf( - TestAssociatable(Point(1, 1), 1), - TestAssociatable(Point(130, 55), 2) - ) - val secondFrameLandmarks = listOf( - TestAssociatable(Point(10, 4), 1), - TestAssociatable(Point(13, 77), 2) - ) - val thirdFrameLandmarks = listOf( - TestAssociatable(Point(1, 2), 1), - TestAssociatable(Point(10, 10), 2) - ) - - val firstFrameAssociationKey = AssociationsManager.AssociationKey(firstFrameNumber, layerName) - associateTwoFrames( - firstFrameNumber, - secondFrameNumber, - firstFrameLandmarks, - secondFrameLandmarks, - layerName = layerName - ) - associateTwoFrames( - firstFrameNumber, - thirdFrameNumber, - firstFrameLandmarks, - thirdFrameLandmarks, - layerName = layerName - ) - - associationsManager.clearAssociation(firstFrameAssociationKey) - assertTrue( - associationsManager.drawnAssociations.values.all { it.values.isEmpty() }, - "There are not cleared associations" - ) - } - - @Test - fun `Clear association in one layer from the frame with many associated layers`() { - val firstLayerName = "kp1" - val secondLayerName = "kp2" - val firstFrameNumber = 25 - val secondFrameNumber = 27 - - val firstLayerFirstFrameLandmarks = listOf( - TestAssociatable(Point(1, 1), 1), - TestAssociatable(Point(130, 55), 2) - ) - val secondLayerFirstFrameLandmarks = listOf( - TestAssociatable(Point(4, 2), 1), - TestAssociatable(Point(111, 5), 2) - ) - - val firstLayerSecondFrameLandmarks = listOf( - TestAssociatable(Point(10, 4), 1), - TestAssociatable(Point(13, 77), 2) - ) - val secondLayerSecondFrameLandmarks = listOf( - TestAssociatable(Point(46, 21), 1), - TestAssociatable(Point(11, 35), 2) - ) - - associateTwoFrames( - firstFrameNumber, - secondFrameNumber, - firstLayerFirstFrameLandmarks, - firstLayerSecondFrameLandmarks, - layerName = firstLayerName - ) - associateTwoFrames( - firstFrameNumber, - secondFrameNumber, - secondLayerFirstFrameLandmarks, - secondLayerSecondFrameLandmarks, - layerName = secondLayerName - ) - - val firstFrameFirstLayerAssociationKey = AssociationsManager.AssociationKey(firstFrameNumber, firstLayerName) - val firstFrameSecondLayerAssociationKey = AssociationsManager.AssociationKey(firstFrameNumber, secondLayerName) - - associationsManager.clearAssociation(firstFrameFirstLayerAssociationKey) - assertEquals(1, associationsManager.drawnAssociations.keys.count { it.frame == firstFrameNumber }) - assertNotNull(associationsManager.drawnAssociations[firstFrameSecondLayerAssociationKey]) - assertNotNull(associationsManager.drawnAssociations[firstFrameSecondLayerAssociationKey]!![secondFrameNumber]) - - associationsManager.clearAssociation(firstFrameSecondLayerAssociationKey) - assertTrue( - associationsManager.drawnAssociations.values.all { it.values.isEmpty() }, - "There are not cleared associations" - ) - } - - @Test - fun `Remove one of multiple associations from the frame`() { - val firstFrameNumber = 25 - val secondFrameNumber = 27 - val thirdFrameNumber = 26 - - val layerName = "kp1" - - val firstFrameLandmarks = listOf( - TestAssociatable(Point(1, 1), 1), - TestAssociatable(Point(130, 55), 2) - ) - val secondFrameLandmarks = listOf( - TestAssociatable(Point(10, 4), 1), - TestAssociatable(Point(13, 77), 2) - ) - val thirdFrameLandmarks = listOf( - TestAssociatable(Point(1, 2), 1), - TestAssociatable(Point(10, 10), 2) - ) - - val firstFrameAssociationsKey = AssociationsManager.AssociationKey(firstFrameNumber, layerName) - val thirdFrameAssociationsKey = AssociationsManager.AssociationKey(thirdFrameNumber, layerName) - associateTwoFrames( - firstFrameNumber, - secondFrameNumber, - firstFrameLandmarks, - secondFrameLandmarks, - layerName = layerName - ) - associateTwoFrames( - firstFrameNumber, - thirdFrameNumber, - firstFrameLandmarks, - thirdFrameLandmarks, - layerName = layerName - ) - - associationsManager.clearAssociation(thirdFrameAssociationsKey) - assertNull(associationsManager.drawnAssociations[thirdFrameAssociationsKey]) - - assertEquals(1, associationsManager.drawnAssociations[firstFrameAssociationsKey]!!.size) - val firstToSecondFrameAssociation = - associationsManager.drawnAssociations[firstFrameAssociationsKey]!![secondFrameNumber]!! - assertEquals(2, firstToSecondFrameAssociation.size) - - associationsManager.clearAssociation(firstFrameAssociationsKey) - assertTrue( - associationsManager.drawnAssociations.values.all { it.values.isEmpty() }, - "There are not cleared associations" - ) - } - - @Test - fun `Association lines update stroke if layer color changed`() { - val initialColor = c("F0F0F0") - val colorProperty = SimpleObjectProperty(initialColor) - val newColor = c("0F0F0F") - - val line = createAssociationLine(2, 2, 5, 7, Point(1, 1), Point(1, 2), colorProperty = colorProperty) - assertEquals(initialColor, line.stroke) - colorProperty.value = newColor - assertEquals(newColor, line.stroke) - } - - @Test - fun `Association lines update visibility if layer enabled changed`() { - val enabledProperty = SimpleBooleanProperty(false) - val line = createAssociationLine(2, 2, 5, 7, Point(1, 1), Point(1, 2), enabledProperty = enabledProperty) - assertFalse(line.isVisible) - enabledProperty.value = true - assertTrue(line.isVisible) - } - - @Test - fun `Association lines keep right position on scaling`() { - scaleProperty.value = 2.0 - val firstFrameRow = 2 - val secondFrameRow = 4 - val firstFrameColumn = 3 - val secondFrameColumn = 7 - val firstFramePosition = - DoublePoint((frameWidth + frameIndent) * firstFrameColumn, (frameHeight + frameIndent) * firstFrameRow) - val secondFramePosition = - DoublePoint((frameWidth + frameIndent) * secondFrameColumn, (frameHeight + frameIndent) * secondFrameRow) - val startPosition = Point(1, 10) - val finishPosition = Point(15, 25) - - val line = createAssociationLine( - firstFrameRow, - secondFrameRow, - firstFrameColumn, - secondFrameColumn, - startPosition, - finishPosition - ) - assertEquals((firstFramePosition.x + startPosition.x) * scaleProperty.value, line.startX) - assertEquals((firstFramePosition.y + startPosition.y) * scaleProperty.value, line.startY) - assertEquals((secondFramePosition.x + finishPosition.x) * scaleProperty.value, line.endX) - assertEquals((secondFramePosition.y + finishPosition.y) * scaleProperty.value, line.endY) - - scaleProperty.value = 0.4 - assertEquals((firstFramePosition.x + startPosition.x) * scaleProperty.value, line.startX) - assertEquals((firstFramePosition.y + startPosition.y) * scaleProperty.value, line.startY) - assertEquals((secondFramePosition.x + finishPosition.x) * scaleProperty.value, line.endX) - assertEquals((secondFramePosition.y + finishPosition.y) * scaleProperty.value, line.endY) - } - - @Test - fun `Choose second frame if the first is not selected`() { - val secondFrameAssociationKey = AssociationsManager.AssociationKey(27, "kp1") - val secondFrameAssociationParameters = AssociationsManager.AssociationParameters( - secondFrameAssociationKey, - listOf(TestAssociatable(Point(1, 1), 1)) - ) - val colorProperty = SimpleObjectProperty(c("FF00FF")) - val enabledProperty = SimpleBooleanProperty() - - assertDoesNotThrow { - associationsManager.associate( - secondFrameAssociationParameters, - colorProperty, - enabledProperty - ) - } - } - - @Test - fun `Associate same frames and layers twice`() { - val framesRow = 2 - val firstFrameColumn = 5 - val secondFrameColumn = 7 - - val firstFrameLandmarks = listOf( - TestAssociatable(Point(1, 1), 1), - TestAssociatable(Point(130, 55), 2) - ) - - val secondFrameLandmarks = listOf( - TestAssociatable(Point(10, 4), 1), - TestAssociatable(Point(13, 77), 2) - ) - - testAssociation( - framesRow, - framesRow, - firstFrameColumn, - secondFrameColumn, - firstFrameLandmarks, - secondFrameLandmarks - ) - testAssociation( - framesRow, - framesRow, - firstFrameColumn, - secondFrameColumn, - firstFrameLandmarks, - secondFrameLandmarks - ) - } - - @Test - fun `Lines can be garbage collected when association is cleared`() { - var line: Line? = createAssociationLine(2, 2, 5, 7, Point(1, 1), Point(1, 2)) - val factory = { line!! } - testMemoryLeak(factory) { - associationsManager.clearAssociation(AssociationsManager.AssociationKey(25, "kp1")) - line = null - } - } - - @Test - fun `Lines update position when columns number changed`() { - val firstFrameNumber = 17 - val secondFrameNumber = 39 - val initialColumnsNumber = columnsNumber.value - val line = createAssociationLine( - firstFrameNumber / initialColumnsNumber, - secondFrameNumber / initialColumnsNumber, - firstFrameNumber % initialColumnsNumber, - secondFrameNumber % initialColumnsNumber, - Point(0, 0), - Point(0, 0) - ) - - val newColumnsNumber = 5 - columnsNumber.value = newColumnsNumber - - assertEquals((firstFrameNumber / newColumnsNumber) * (frameHeight + frameIndent), line.endY) - assertEquals((firstFrameNumber % newColumnsNumber) * (frameWidth + frameIndent), line.endX) - assertEquals((secondFrameNumber / newColumnsNumber) * (frameHeight + frameIndent), line.startY) - assertEquals((secondFrameNumber % newColumnsNumber) * (frameWidth + frameIndent), line.startX) - } - - @Test - fun `Can be garbage collected after dispose`() { - val factory = { associationsManager } - testMemoryLeak(factory) { - associationsManager.dispose() - associationsManager = - AssociationsManager(frameSize, frameIndent, scaleProperty, frames, columnsNumber, outOfFramesLayer) - } - } - - private fun createAssociationLine( - firstFrameRow: Int, - secondFrameRow: Int, - firstFrameColumn: Int, - secondFrameColumn: Int, - startPosition: Point, - finishPosition: Point, - colorProperty: ObjectProperty = SimpleObjectProperty(c("F0F0F0")), - enabledProperty: BooleanProperty = SimpleBooleanProperty(true) - ): Line { - val firstFrameLandmarks = listOf( - TestAssociatable(startPosition, 1) - ) - - val secondFrameLandmarks = listOf( - TestAssociatable(finishPosition, 1) - ) - - associateTwoFrames( - firstFrameRow * columnsNumber.value + firstFrameColumn, - secondFrameRow * columnsNumber.value + secondFrameColumn, - firstFrameLandmarks, - secondFrameLandmarks, - colorProperty = colorProperty, - enabledProperty = enabledProperty - ) - - return associationsManager.drawnAssociations.values.first().values.single().single().node as Line - } - - private fun testAssociation( - firstFrameRow: Int, - secondFrameRow: Int, - firstFrameColumn: Int, - secondFrameColumn: Int, - firstFrameLandmarks: List, - secondFrameLandmarks: List, - alreadyAssociatedFrames: List = listOf(), - layerName: String = "kp1" - ) { - val firstFrameNumber = firstFrameRow * columnsNumber.value + firstFrameColumn - val secondFrameNumber = secondFrameRow * columnsNumber.value + secondFrameColumn - - val firstFrameAssociationKey = AssociationsManager.AssociationKey(firstFrameNumber, layerName) - associateTwoFrames( - firstFrameNumber, - secondFrameNumber, - firstFrameLandmarks, - secondFrameLandmarks, - layerName = layerName - ) - - assertEquals(null, associationsManager.chosenLayerName) - val associatedFramesNumbers = associationsManager.drawnAssociations.keys.map { it.frame } - assertEquals( - setOf(firstFrameNumber, secondFrameNumber) + alreadyAssociatedFrames.toSet(), - associatedFramesNumbers.toSet() - ) - val lines = - (associationsManager.drawnAssociations[firstFrameAssociationKey] ?: fail())[secondFrameNumber] ?: fail() - - val secondFrameUids = secondFrameLandmarks.map { it.uid } - val matchingUids = firstFrameLandmarks.map { it.uid }.filter { secondFrameUids.contains(it) } - - assertEquals(matchingUids.size, lines.count()) - - val firstFrameX = (frameWidth + frameIndent) * firstFrameColumn - val firstFrameY = (frameHeight + frameIndent) * firstFrameRow - val secondFrameX = (frameWidth + frameIndent) * secondFrameColumn - val secondFrameY = (frameHeight + frameIndent) * secondFrameRow - - val linesNodes = lines.map { it.node as Line } - - matchingUids.forEach { uid -> - val firstFrameLandmarkPosition = firstFrameLandmarks.single { it.uid == uid }.coordinate - val secondFrameFirstLandmarkPosition = secondFrameLandmarks.single { it.uid == uid }.coordinate - - assertEquals( - 1, - linesNodes.count { - it.startX == firstFrameX + firstFrameLandmarkPosition.x && - it.startY == firstFrameY + firstFrameLandmarkPosition.y && - it.endX == secondFrameX + secondFrameFirstLandmarkPosition.x && - it.endY == secondFrameY + secondFrameFirstLandmarkPosition.y - }, - "No line was drawn for landmark with uid: $uid" - ) - } - } - - private fun associateTwoFrames( - firstFrameNumber: Int, - secondFrameNumber: Int, - firstFrameLandmarks: List, - secondFrameLandmarks: List, - colorProperty: ObjectProperty = SimpleObjectProperty(c("F0F0F0")), - enabledProperty: BooleanProperty = SimpleBooleanProperty(true), - layerName: String = "kp1" - ) { - val firstFrameAssociationKey = AssociationsManager.AssociationKey(firstFrameNumber, layerName) - val firstFrameAssociationParameters = - AssociationsManager.AssociationParameters(firstFrameAssociationKey, firstFrameLandmarks) - - val secondFrameAssociationKey = AssociationsManager.AssociationKey(secondFrameNumber, layerName) - val secondFrameAssociationParameters = - AssociationsManager.AssociationParameters(secondFrameAssociationKey, secondFrameLandmarks) - - associationsManager.initAssociation(firstFrameAssociationParameters) - associationsManager.associate(secondFrameAssociationParameters, colorProperty, enabledProperty) - } -}