From 76ac7b68886aaa683788227a884d7a55e701d93c Mon Sep 17 00:00:00 2001 From: Bhavul Gauri Date: Sun, 9 Nov 2025 20:37:28 +0000 Subject: [PATCH 1/3] Add Smart Lasso Selection feature --- .../notable/data/datastore/AppSettings.kt | 1 + .../java/com/ethran/notable/data/db/Kv.kt | 7 ++- .../com/ethran/notable/editor/DrawCanvas.kt | 30 ++++++--- .../notable/editor/EditorControlTower.kt | 61 +++++++++++++++++++ .../notable/editor/state/SelectionState.kt | 6 ++ .../notable/editor/ui/SelectorBitmap.kt | 5 +- .../com/ethran/notable/ui/views/Settings.kt | 7 +++ app/src/main/res/values-pl/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 9 files changed, 107 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/data/datastore/AppSettings.kt b/app/src/main/java/com/ethran/notable/data/datastore/AppSettings.kt index b00eb98a..a5b85316 100644 --- a/app/src/main/java/com/ethran/notable/data/datastore/AppSettings.kt +++ b/app/src/main/java/com/ethran/notable/data/datastore/AppSettings.kt @@ -37,6 +37,7 @@ data class AppSettings( val visualizePdfPagination: Boolean = false, val paginatePdf: Boolean = true, val scribbleToEraseEnabled: Boolean = false, + val smartLassoEnabled: Boolean = false, val simpleRendering: Boolean = false, val openGLRendering: Boolean = true, val muPdfRendering: Boolean = true, diff --git a/app/src/main/java/com/ethran/notable/data/db/Kv.kt b/app/src/main/java/com/ethran/notable/data/db/Kv.kt index b3c0cedd..b2c2a0ca 100644 --- a/app/src/main/java/com/ethran/notable/data/db/Kv.kt +++ b/app/src/main/java/com/ethran/notable/data/db/Kv.kt @@ -73,18 +73,21 @@ class KvRepository(context: Context) { class KvProxy(context: Context) { private val kvRepository = KvRepository(context) + // Configure JSON to ignore unknown keys for backward compatibility + private val json = Json { ignoreUnknownKeys = true } + fun observeKv(key: String, serializer: KSerializer, default: T): LiveData { return kvRepository.getLive(key).map { if (it == null) return@map default val jsonValue = it.value - Json.decodeFromString(serializer, jsonValue) + json.decodeFromString(serializer, jsonValue) } } fun get(key: String, serializer: KSerializer): T? { val kv = kvRepository.get(key) ?: return null //returns null when there is no database val jsonValue = kv.value - return Json.decodeFromString(serializer, jsonValue) + return json.decodeFromString(serializer, jsonValue) } diff --git a/app/src/main/java/com/ethran/notable/editor/DrawCanvas.kt b/app/src/main/java/com/ethran/notable/editor/DrawCanvas.kt index 3656014d..f014d4ae 100644 --- a/app/src/main/java/com/ethran/notable/editor/DrawCanvas.kt +++ b/app/src/main/java/com/ethran/notable/editor/DrawCanvas.kt @@ -41,6 +41,7 @@ import com.ethran.notable.editor.utils.handleDraw import com.ethran.notable.editor.utils.handleErase import com.ethran.notable.editor.utils.handleScribbleToErase import com.ethran.notable.editor.utils.handleSelect +import com.ethran.notable.editor.utils.handleSmartLasso import com.ethran.notable.editor.utils.onSurfaceChanged import com.ethran.notable.editor.utils.onSurfaceDestroy import com.ethran.notable.editor.utils.onSurfaceInit @@ -305,6 +306,8 @@ class DrawCanvas( val scaledPoints = copyInput(plist.points, page.scroll, page.zoomLevel.value) val firstPointTime = plist.points.first().timestamp + + // First check for scribble-to-erase val erasedByScribbleDirtyRect = handleScribbleToErase( page, scaledPoints, @@ -313,17 +316,30 @@ class DrawCanvas( currentLastStrokeEndTime, firstPointTime ) + if (erasedByScribbleDirtyRect.isNullOrEmpty()) { - log.d("Drawing...") - // draw the stroke - handleDraw( + // Not a scribble-to-erase, check for smart lasso + val handledAsSmartLasso = handleSmartLasso( + coroutineScope, this@DrawCanvas.page, - strokeHistoryBatch, - getActualState().penSettings[getActualState().pen.penName]!!.strokeSize, - getActualState().penSettings[getActualState().pen.penName]!!.color, - getActualState().pen, + getActualState(), scaledPoints ) + + if (!handledAsSmartLasso) { + // Neither scribble nor smart lasso, draw as regular stroke + log.d("Drawing...") + handleDraw( + this@DrawCanvas.page, + strokeHistoryBatch, + getActualState().penSettings[getActualState().pen.penName]!!.strokeSize, + getActualState().penSettings[getActualState().pen.penName]!!.color, + getActualState().pen, + scaledPoints + ) + } else { + log.d("Handled as smart lasso selection") + } } else { log.d("Erased by scribble, $erasedByScribbleDirtyRect") drawCanvasToView(erasedByScribbleDirtyRect) diff --git a/app/src/main/java/com/ethran/notable/editor/EditorControlTower.kt b/app/src/main/java/com/ethran/notable/editor/EditorControlTower.kt index 42d8d0db..65c1eea1 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorControlTower.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorControlTower.kt @@ -146,6 +146,9 @@ class EditorControlTower( } fun deleteSelection() { + // Clear pending smart lasso stroke since user has committed to the selection action + state.selectionState.pendingSmartLassoStroke = null + val operationList = state.selectionState.deleteSelection(page) history.addOperationsToHistory(operationList) state.isDrawing = true @@ -166,6 +169,9 @@ class EditorControlTower( } fun duplicateSelection() { + // Clear pending smart lasso stroke since user has committed to the selection action + state.selectionState.pendingSmartLassoStroke = null + // finish ongoing movement applySelectionDisplace() state.selectionState.duplicateSelection() @@ -173,12 +179,18 @@ class EditorControlTower( } fun cutSelectionToClipboard(context: Context) { + // Clear pending smart lasso stroke since user has committed to the selection action + state.selectionState.pendingSmartLassoStroke = null + state.clipboard = state.selectionState.selectionToClipboard(page.scroll, context) deleteSelection() showHint("Content cut to clipboard", scope) } fun copySelectionToClipboard(context: Context) { + // Clear pending smart lasso stroke since user has committed to the selection action + state.selectionState.pendingSmartLassoStroke = null + state.clipboard = state.selectionState.selectionToClipboard(page.scroll, context) } @@ -230,5 +242,54 @@ class EditorControlTower( showHint("Pasted content from clipboard", scope) } + + /** + * Dismisses the current selection. If the selection was from smart lasso, + * draws the original stroke instead. + */ + fun dismissSelection() { + val pendingStroke = state.selectionState.pendingSmartLassoStroke + if (pendingStroke != null) { + Log.i("SmartLasso", "User dismissed smart lasso selection, drawing the original stroke") + // User dismissed without using the panel, so draw the original stroke + // Save the pending stroke before reset clears it + val strokeToDrawCopy = pendingStroke.toList() + + // Reset the selection + state.selectionState.reset() + + // Now draw the pending stroke + val strokeHistoryBatch = mutableListOf() + com.ethran.notable.editor.utils.handleDraw( + page, + strokeHistoryBatch, + state.penSettings[state.pen.penName]!!.strokeSize, + state.penSettings[state.pen.penName]!!.color, + state.pen, + strokeToDrawCopy + ) + + // Add to history + if (strokeHistoryBatch.isNotEmpty()) { + history.addOperationsToHistory( + operations = listOf( + Operation.DeleteStroke(strokeHistoryBatch.map { it }) + ) + ) + } + + state.isDrawing = true + + // Refresh UI + scope.launch { + DrawCanvas.refreshUi.emit(Unit) + } + } else { + // Normal selection dismissal + applySelectionDisplace() + state.selectionState.reset() + state.isDrawing = true + } + } } diff --git a/app/src/main/java/com/ethran/notable/editor/state/SelectionState.kt b/app/src/main/java/com/ethran/notable/editor/state/SelectionState.kt index 8143f78a..9983aa3e 100644 --- a/app/src/main/java/com/ethran/notable/editor/state/SelectionState.kt +++ b/app/src/main/java/com/ethran/notable/editor/state/SelectionState.kt @@ -14,6 +14,7 @@ import androidx.core.graphics.createBitmap import com.ethran.notable.TAG import com.ethran.notable.data.db.Image import com.ethran.notable.data.db.Stroke +import com.ethran.notable.data.db.StrokePoint import com.ethran.notable.data.model.SimplePointF import com.ethran.notable.editor.PageView import com.ethran.notable.editor.drawing.drawImage @@ -35,6 +36,10 @@ class SelectionState { var selectedStrokes by mutableStateOf?>(null) var selectedImages by mutableStateOf?>(null) + // Smart lasso: stores the original stroke points if selection was made via smart lasso + // This allows fallback to drawing the stroke if user dismisses without using the panel + var pendingSmartLassoStroke by mutableStateOf?>(null) + // TODO: Bitmap should be change, if scale changes. var selectedBitmap by mutableStateOf(null) @@ -53,6 +58,7 @@ class SelectionState { selectionStartOffset = null selectionDisplaceOffset = null placementMode = null + pendingSmartLassoStroke = null setAnimationMode(false) } diff --git a/app/src/main/java/com/ethran/notable/editor/ui/SelectorBitmap.kt b/app/src/main/java/com/ethran/notable/editor/ui/SelectorBitmap.kt index 12c61a1f..87ef1d60 100644 --- a/app/src/main/java/com/ethran/notable/editor/ui/SelectorBitmap.kt +++ b/app/src/main/java/com/ethran/notable/editor/ui/SelectorBitmap.kt @@ -68,9 +68,8 @@ fun SelectedBitmap( Modifier .fillMaxSize() .noRippleClickable { - controlTower.applySelectionDisplace() - selectionState.reset() - editorState.isDrawing = true + // Delegate dismissal logic to control tower + controlTower.dismissSelection() }) { Image( bitmap = selectionState.selectedBitmap!!.asImageBitmap(), diff --git a/app/src/main/java/com/ethran/notable/ui/views/Settings.kt b/app/src/main/java/com/ethran/notable/ui/views/Settings.kt index 58de824f..fd7360f3 100644 --- a/app/src/main/java/com/ethran/notable/ui/views/Settings.kt +++ b/app/src/main/java/com/ethran/notable/ui/views/Settings.kt @@ -202,6 +202,13 @@ fun GeneralSettings(kv: KvProxy, settings: AppSettings) { kv.setAppSettings(settings.copy(scribbleToEraseEnabled = isChecked)) }) + SettingToggleRow( + label = stringResource(R.string.enable_smart_lasso), + value = settings.smartLassoEnabled, + onToggle = { isChecked -> + kv.setAppSettings(settings.copy(smartLassoEnabled = isChecked)) + }) + SettingToggleRow( label = stringResource(R.string.enable_smooth_scrolling), value = settings.smoothScroll, diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 982831b0..710481cb 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -17,6 +17,7 @@ Używaj Onyx NeoTools (może powodować awarie) Zamaż aby usunąć + Włącz inteligentne zaznaczanie lasso (narysuj zamkniętą pętlę, aby zaznaczyć zawartość) Włącz płynne przewijanie Włącz płynne przybliżanie Płynny regulator grubości kreski diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 12d84e01..4e5e4712 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,6 +17,7 @@ Hexagon grid Use Onyx NeoTools (may cause crashes) Enable scribble-to-erase (scribble out your mistakes to erase them) + Enable smart lasso selection (draw a closed loop to select content) Enable smooth scrolling Continuous Zoom Continuous Stroke Slider From 3bca44a770ebde31dee0bd6d5c58b701912e0ae6 Mon Sep 17 00:00:00 2001 From: Bhavul Gauri Date: Sun, 9 Nov 2025 21:29:55 +0000 Subject: [PATCH 2/3] Had forgotten to commit the main file implementing Smart Lasso! --- .../ethran/notable/editor/utils/SmartLasso.kt | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 app/src/main/java/com/ethran/notable/editor/utils/SmartLasso.kt diff --git a/app/src/main/java/com/ethran/notable/editor/utils/SmartLasso.kt b/app/src/main/java/com/ethran/notable/editor/utils/SmartLasso.kt new file mode 100644 index 00000000..9b380908 --- /dev/null +++ b/app/src/main/java/com/ethran/notable/editor/utils/SmartLasso.kt @@ -0,0 +1,177 @@ +package com.ethran.notable.editor.utils + +import android.graphics.Path +import com.ethran.notable.data.datastore.GlobalAppSettings +import com.ethran.notable.data.db.StrokePoint +import com.ethran.notable.data.model.SimplePointF +import com.ethran.notable.editor.PageView +import com.ethran.notable.editor.state.EditorState +import io.shipbook.shipbooksdk.Log +import kotlinx.coroutines.CoroutineScope +import kotlin.math.sqrt + +// Minimum number of points required for a smart lasso stroke +const val MINIMUM_SMART_LASSO_POINTS = 20 + +// Maximum distance between first and last point to consider it a closed loop +// This is a percentage of the stroke's bounding box diagonal +const val CLOSED_LOOP_THRESHOLD_PERCENTAGE = 0.15f + +// Minimum perimeter required to avoid accidental tiny loops +const val MINIMUM_PERIMETER_THRESHOLD = 100f + +/** + * Calculates the Euclidean distance between two points + */ +private fun distance(p1: StrokePoint, p2: StrokePoint): Float { + val dx = p1.x - p2.x + val dy = p1.y - p2.y + return sqrt(dx * dx + dy * dy) +} + +/** + * Calculates the total perimeter length of a stroke path + */ +private fun calculatePerimeter(points: List): Float { + var totalDistance = 0.0f + for (i in 1 until points.size) { + totalDistance += distance(points[i - 1], points[i]) + } + return totalDistance +} + +/** + * Checks if a stroke forms a closed loop suitable for smart lasso selection + */ +private fun isClosedLoop(points: List): Boolean { + if (points.size < MINIMUM_SMART_LASSO_POINTS) { + Log.d("SmartLasso", "Too few points: ${points.size} < $MINIMUM_SMART_LASSO_POINTS") + return false + } + + val firstPoint = points.first() + val lastPoint = points.last() + + // Calculate bounding box to determine if closure distance is reasonable + val boundingBox = calculateBoundingBox(points) { Pair(it.x, it.y) } + val boxWidth = boundingBox.width() + val boxHeight = boundingBox.height() + + // Calculate diagonal of bounding box + val diagonal = sqrt(boxWidth * boxWidth + boxHeight * boxHeight) + + // Check if first and last points are close enough (relative to stroke size) + val closureDistance = distance(firstPoint, lastPoint) + val closureThreshold = diagonal * CLOSED_LOOP_THRESHOLD_PERCENTAGE + + if (closureDistance > closureThreshold) { + Log.d("SmartLasso", "Not closed: distance=$closureDistance, threshold=$closureThreshold") + return false + } + + // Check minimum perimeter to avoid tiny accidental loops + val perimeter = calculatePerimeter(points) + if (perimeter < MINIMUM_PERIMETER_THRESHOLD) { + Log.d("SmartLasso", "Perimeter too small: $perimeter < $MINIMUM_PERIMETER_THRESHOLD") + return false + } + + // Additional check: ensure the stroke doesn't have too many sharp reversals + // (which would indicate scribbling rather than deliberate loop drawing) + // We allow some reversals (unlike scribble which requires many), but not too many + val reversals = calculateNumReversalsForLasso(points) + if (reversals > 8) { // Allow some natural hand movements but reject scribbles + Log.d("SmartLasso", "Too many reversals: $reversals > 8 (likely scribble, not lasso)") + return false + } + + Log.d("SmartLasso", "Detected closed loop: points=${points.size}, perimeter=$perimeter, closure=$closureDistance") + return true +} + +/** + * Calculates direction reversals but with different thresholds for lasso detection + * Unlike scribble detection, we're looking for smooth loops, not back-and-forth motion + */ +private fun calculateNumReversalsForLasso( + points: List, + stepSize: Int = 8 +): Int { + var numReversals = 0 + for (i in 0 until points.size - 2 * stepSize step stepSize) { + val p1 = points[i] + val p2 = points[i + stepSize] + val p3 = points[i + 2 * stepSize] + val segment1 = SimplePointF(p2.x - p1.x, p2.y - p1.y) + val segment2 = SimplePointF(p3.x - p2.x, p3.y - p2.y) + val dotProduct = segment1.x * segment2.x + segment1.y * segment2.y + // Count sharp reversals (angle > 120 degrees) instead of just > 90 + if (dotProduct < -0.5f * sqrt( + (segment1.x * segment1.x + segment1.y * segment1.y) * + (segment2.x * segment2.x + segment2.y * segment2.y) + )) { + numReversals++ + } + } + return numReversals +} + +/** + * Attempts to handle a stroke as a smart lasso selection. + * Returns true if the stroke was handled as a smart lasso (and selection was triggered), + * false if the stroke should be drawn normally. + * + * @param scope Coroutine scope for async operations + * @param page Current page view + * @param editorState Current editor state + * @param touchPoints The stroke points to analyze (in page coordinates) + * @return true if handled as smart lasso, false if should be drawn as regular stroke + */ +fun handleSmartLasso( + scope: CoroutineScope, + page: PageView, + editorState: EditorState, + touchPoints: List +): Boolean { + // Check if feature is enabled + if (!GlobalAppSettings.current.smartLassoEnabled) { + return false + } + + // Don't interfere with marker (highlighter) strokes + if (editorState.pen == Pen.MARKER) { + return false + } + + // Check if this stroke forms a closed loop + if (!isClosedLoop(touchPoints)) { + return false + } + + // Convert points to SimplePointF for handleSelect + val selectionPoints = touchPoints.map { SimplePointF(it.x, it.y) } + + // Create selection path and find selected content + val selectionPath = pointsToPath(selectionPoints) + selectionPath.close() + + val selectedStrokes = selectStrokesFromPath(page.strokes, selectionPath) + val selectedImages = selectImagesFromPath(page.images, selectionPath) + + // If nothing was selected, don't treat this as a smart lasso + // (let it be drawn as a normal stroke instead) + if (selectedStrokes.isEmpty() && selectedImages.isEmpty()) { + Log.d("SmartLasso", "No content selected, drawing as regular stroke") + return false + } + + Log.i("SmartLasso", "Smart lasso triggered! Selected ${selectedStrokes.size} strokes and ${selectedImages.size} images") + + // Store the original stroke points so they can be drawn if user dismisses without using panel + editorState.selectionState.pendingSmartLassoStroke = touchPoints + + // Trigger selection + selectImagesAndStrokes(scope, page, editorState, selectedImages, selectedStrokes) + + return true +} From ea7ceb045a83ad6ba14504f0ca278426fc1fa03b Mon Sep 17 00:00:00 2001 From: Bhavul Gauri Date: Sun, 9 Nov 2025 21:58:07 +0000 Subject: [PATCH 3/3] Copilot review fixes --- .../notable/editor/EditorControlTower.kt | 55 ++++++++++++------- .../notable/editor/state/SelectionState.kt | 9 ++- .../ethran/notable/editor/utils/SmartLasso.kt | 5 +- 3 files changed, 48 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/ethran/notable/editor/EditorControlTower.kt b/app/src/main/java/com/ethran/notable/editor/EditorControlTower.kt index 65c1eea1..7635aca1 100644 --- a/app/src/main/java/com/ethran/notable/editor/EditorControlTower.kt +++ b/app/src/main/java/com/ethran/notable/editor/EditorControlTower.kt @@ -1,9 +1,9 @@ package com.ethran.notable.editor import android.content.Context -import android.util.Log import androidx.compose.ui.geometry.Offset import com.ethran.notable.TAG +import io.shipbook.shipbooksdk.Log import com.ethran.notable.data.datastore.GlobalAppSettings import com.ethran.notable.editor.state.EditorState import com.ethran.notable.editor.state.History @@ -146,8 +146,8 @@ class EditorControlTower( } fun deleteSelection() { - // Clear pending smart lasso stroke since user has committed to the selection action - state.selectionState.pendingSmartLassoStroke = null + // Clear pending smart lasso data since user has committed to the selection action + clearPendingSmartLassoData() val operationList = state.selectionState.deleteSelection(page) history.addOperationsToHistory(operationList) @@ -169,8 +169,8 @@ class EditorControlTower( } fun duplicateSelection() { - // Clear pending smart lasso stroke since user has committed to the selection action - state.selectionState.pendingSmartLassoStroke = null + // Clear pending smart lasso data since user has committed to the selection action + clearPendingSmartLassoData() // finish ongoing movement applySelectionDisplace() @@ -179,8 +179,8 @@ class EditorControlTower( } fun cutSelectionToClipboard(context: Context) { - // Clear pending smart lasso stroke since user has committed to the selection action - state.selectionState.pendingSmartLassoStroke = null + // Clear pending smart lasso data since user has committed to the selection action + clearPendingSmartLassoData() state.clipboard = state.selectionState.selectionToClipboard(page.scroll, context) deleteSelection() @@ -188,8 +188,8 @@ class EditorControlTower( } fun copySelectionToClipboard(context: Context) { - // Clear pending smart lasso stroke since user has committed to the selection action - state.selectionState.pendingSmartLassoStroke = null + // Clear pending smart lasso data since user has committed to the selection action + clearPendingSmartLassoData() state.clipboard = state.selectionState.selectionToClipboard(page.scroll, context) } @@ -243,29 +243,46 @@ class EditorControlTower( showHint("Pasted content from clipboard", scope) } + /** + * Clears all pending smart lasso data when user commits to a selection action + */ + private fun clearPendingSmartLassoData() { + state.selectionState.pendingSmartLassoStroke = null + state.selectionState.pendingSmartLassoPen = null + state.selectionState.pendingSmartLassoStrokeSize = null + state.selectionState.pendingSmartLassoColor = null + } + /** * Dismisses the current selection. If the selection was from smart lasso, - * draws the original stroke instead. + * draws the original stroke with its original pen settings instead. */ fun dismissSelection() { val pendingStroke = state.selectionState.pendingSmartLassoStroke - if (pendingStroke != null) { - Log.i("SmartLasso", "User dismissed smart lasso selection, drawing the original stroke") - // User dismissed without using the panel, so draw the original stroke - // Save the pending stroke before reset clears it + val pendingPen = state.selectionState.pendingSmartLassoPen + val pendingStrokeSize = state.selectionState.pendingSmartLassoStrokeSize + val pendingColor = state.selectionState.pendingSmartLassoColor + + if (pendingStroke != null && pendingPen != null && pendingStrokeSize != null && pendingColor != null) { + Log.i("SmartLasso", "User dismissed smart lasso selection, drawing the original stroke with original pen settings") + // User dismissed without using the panel, so draw the original stroke with original settings + // Save the pending data before reset clears it val strokeToDrawCopy = pendingStroke.toList() + val penCopy = pendingPen + val strokeSizeCopy = pendingStrokeSize + val colorCopy = pendingColor // Reset the selection state.selectionState.reset() - // Now draw the pending stroke + // Now draw the pending stroke with its original pen settings val strokeHistoryBatch = mutableListOf() com.ethran.notable.editor.utils.handleDraw( page, strokeHistoryBatch, - state.penSettings[state.pen.penName]!!.strokeSize, - state.penSettings[state.pen.penName]!!.color, - state.pen, + strokeSizeCopy, + colorCopy, + penCopy, strokeToDrawCopy ) @@ -273,7 +290,7 @@ class EditorControlTower( if (strokeHistoryBatch.isNotEmpty()) { history.addOperationsToHistory( operations = listOf( - Operation.DeleteStroke(strokeHistoryBatch.map { it }) + Operation.DeleteStroke(strokeHistoryBatch) ) ) } diff --git a/app/src/main/java/com/ethran/notable/editor/state/SelectionState.kt b/app/src/main/java/com/ethran/notable/editor/state/SelectionState.kt index 9983aa3e..42a48d1e 100644 --- a/app/src/main/java/com/ethran/notable/editor/state/SelectionState.kt +++ b/app/src/main/java/com/ethran/notable/editor/state/SelectionState.kt @@ -18,6 +18,7 @@ import com.ethran.notable.data.db.StrokePoint import com.ethran.notable.data.model.SimplePointF import com.ethran.notable.editor.PageView import com.ethran.notable.editor.drawing.drawImage +import com.ethran.notable.editor.utils.Pen import com.ethran.notable.editor.utils.imageBoundsInt import com.ethran.notable.editor.utils.offsetImage import com.ethran.notable.editor.utils.offsetStroke @@ -36,9 +37,12 @@ class SelectionState { var selectedStrokes by mutableStateOf?>(null) var selectedImages by mutableStateOf?>(null) - // Smart lasso: stores the original stroke points if selection was made via smart lasso + // Smart lasso: stores the original stroke data if selection was made via smart lasso // This allows fallback to drawing the stroke if user dismisses without using the panel var pendingSmartLassoStroke by mutableStateOf?>(null) + var pendingSmartLassoPen by mutableStateOf(null) + var pendingSmartLassoStrokeSize by mutableStateOf(null) + var pendingSmartLassoColor by mutableStateOf(null) // TODO: Bitmap should be change, if scale changes. var selectedBitmap by mutableStateOf(null) @@ -59,6 +63,9 @@ class SelectionState { selectionDisplaceOffset = null placementMode = null pendingSmartLassoStroke = null + pendingSmartLassoPen = null + pendingSmartLassoStrokeSize = null + pendingSmartLassoColor = null setAnimationMode(false) } diff --git a/app/src/main/java/com/ethran/notable/editor/utils/SmartLasso.kt b/app/src/main/java/com/ethran/notable/editor/utils/SmartLasso.kt index 9b380908..d7f10886 100644 --- a/app/src/main/java/com/ethran/notable/editor/utils/SmartLasso.kt +++ b/app/src/main/java/com/ethran/notable/editor/utils/SmartLasso.kt @@ -167,8 +167,11 @@ fun handleSmartLasso( Log.i("SmartLasso", "Smart lasso triggered! Selected ${selectedStrokes.size} strokes and ${selectedImages.size} images") - // Store the original stroke points so they can be drawn if user dismisses without using panel + // Store the original stroke data (points + pen settings) so they can be drawn if user dismisses without using panel editorState.selectionState.pendingSmartLassoStroke = touchPoints + editorState.selectionState.pendingSmartLassoPen = editorState.pen + editorState.selectionState.pendingSmartLassoStrokeSize = editorState.penSettings[editorState.pen.penName]?.strokeSize + editorState.selectionState.pendingSmartLassoColor = editorState.penSettings[editorState.pen.penName]?.color // Trigger selection selectImagesAndStrokes(scope, page, editorState, selectedImages, selectedStrokes)