Fix line styles, move tool selection, stroke style storage
Line styles: - Store stroke style in DB (style column, migration v2->v3) - Dashed lines rendered with DashPathEffect on the paint - Arrow heads drawn dynamically from stored two-point line data - Line tool stores just two endpoints; style determines rendering - Removed buildLinePoints — arrow heads no longer baked into point data Move tool: - If there's an active selection, Move drags the entire selection - Otherwise, hit-tests a single stroke and drags it - Selection stays highlighted after move completes Line dropdown: - Removed conflicting onClick from Surface, combinedClickable now correctly handles both tap (select tool) and long-press (style menu) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ import net.metacircular.engpad.data.model.Stroke
|
|||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [Notebook::class, Page::class, Stroke::class],
|
entities = [Notebook::class, Page::class, Stroke::class],
|
||||||
version = 2,
|
version = 3,
|
||||||
exportSchema = false,
|
exportSchema = false,
|
||||||
)
|
)
|
||||||
abstract class EngPadDatabase : RoomDatabase() {
|
abstract class EngPadDatabase : RoomDatabase() {
|
||||||
@@ -32,13 +32,21 @@ abstract class EngPadDatabase : RoomDatabase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val MIGRATION_2_3 = object : Migration(2, 3) {
|
||||||
|
override fun migrate(db: SupportSQLiteDatabase) {
|
||||||
|
db.execSQL(
|
||||||
|
"ALTER TABLE strokes ADD COLUMN style TEXT NOT NULL DEFAULT 'plain'"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getInstance(context: Context): EngPadDatabase =
|
fun getInstance(context: Context): EngPadDatabase =
|
||||||
instance ?: synchronized(this) {
|
instance ?: synchronized(this) {
|
||||||
instance ?: Room.databaseBuilder(
|
instance ?: Room.databaseBuilder(
|
||||||
context.applicationContext,
|
context.applicationContext,
|
||||||
EngPadDatabase::class.java,
|
EngPadDatabase::class.java,
|
||||||
"engpad.db",
|
"engpad.db",
|
||||||
).addMigrations(MIGRATION_1_2).build().also { instance = it }
|
).addMigrations(MIGRATION_1_2, MIGRATION_2_3).build().also { instance = it }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,13 @@ import androidx.room.PrimaryKey
|
|||||||
],
|
],
|
||||||
indices = [Index(value = ["page_id"])],
|
indices = [Index(value = ["page_id"])],
|
||||||
)
|
)
|
||||||
|
/**
|
||||||
|
* Style constants stored in the `style` column.
|
||||||
|
* - "plain" — solid line (default, freehand strokes, boxes)
|
||||||
|
* - "dashed" — dashed line
|
||||||
|
* - "arrow" — solid line with arrow at end
|
||||||
|
* - "double_arrow" — solid line with arrows at both ends
|
||||||
|
*/
|
||||||
data class Stroke(
|
data class Stroke(
|
||||||
@PrimaryKey(autoGenerate = true) val id: Long = 0,
|
@PrimaryKey(autoGenerate = true) val id: Long = 0,
|
||||||
@ColumnInfo(name = "page_id") val pageId: Long,
|
@ColumnInfo(name = "page_id") val pageId: Long,
|
||||||
@@ -26,6 +33,7 @@ data class Stroke(
|
|||||||
@ColumnInfo(name = "point_data") val pointData: ByteArray,
|
@ColumnInfo(name = "point_data") val pointData: ByteArray,
|
||||||
@ColumnInfo(name = "stroke_order") val strokeOrder: Int,
|
@ColumnInfo(name = "stroke_order") val strokeOrder: Int,
|
||||||
@ColumnInfo(name = "created_at") val createdAt: Long,
|
@ColumnInfo(name = "created_at") val createdAt: Long,
|
||||||
|
@ColumnInfo(name = "style", defaultValue = "plain") val style: String = STYLE_PLAIN,
|
||||||
) {
|
) {
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
@@ -36,7 +44,8 @@ data class Stroke(
|
|||||||
color == other.color &&
|
color == other.color &&
|
||||||
pointData.contentEquals(other.pointData) &&
|
pointData.contentEquals(other.pointData) &&
|
||||||
strokeOrder == other.strokeOrder &&
|
strokeOrder == other.strokeOrder &&
|
||||||
createdAt == other.createdAt
|
createdAt == other.createdAt &&
|
||||||
|
style == other.style
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode(): Int {
|
||||||
@@ -47,6 +56,14 @@ data class Stroke(
|
|||||||
result = 31 * result + pointData.contentHashCode()
|
result = 31 * result + pointData.contentHashCode()
|
||||||
result = 31 * result + strokeOrder
|
result = 31 * result + strokeOrder
|
||||||
result = 31 * result + createdAt.hashCode()
|
result = 31 * result + createdAt.hashCode()
|
||||||
|
result = 31 * result + style.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val STYLE_PLAIN = "plain"
|
||||||
|
const val STYLE_DASHED = "dashed"
|
||||||
|
const val STYLE_ARROW = "arrow"
|
||||||
|
const val STYLE_DOUBLE_ARROW = "double_arrow"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ fun EditorScreen(
|
|||||||
|
|
||||||
// Wire up callbacks
|
// Wire up callbacks
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
canvasView.onStrokeCompleted = { penSize, color, points ->
|
canvasView.onStrokeCompleted = { penSize, color, points, style ->
|
||||||
viewModel.onStrokeCompleted(penSize, color, points)
|
viewModel.onStrokeCompleted(penSize, color, points, style)
|
||||||
}
|
}
|
||||||
canvasView.onZoomPanChanged = { zoom, panX, panY ->
|
canvasView.onZoomPanChanged = { zoom, panX, panY ->
|
||||||
viewModel.onZoomPanChanged(zoom, panX, panY)
|
viewModel.onZoomPanChanged(zoom, panX, panY)
|
||||||
@@ -92,7 +92,7 @@ fun EditorScreen(
|
|||||||
viewModel.onStrokeAdded = { stroke ->
|
viewModel.onStrokeAdded = { stroke ->
|
||||||
canvasView.addCompletedStroke(
|
canvasView.addCompletedStroke(
|
||||||
stroke.id, stroke.penSize, stroke.color,
|
stroke.id, stroke.penSize, stroke.color,
|
||||||
stroke.pointData.toFloatArray(),
|
stroke.pointData.toFloatArray(), stroke.style,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
viewModel.onStrokeRemoved = { id ->
|
viewModel.onStrokeRemoved = { id ->
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ class EditorViewModel(
|
|||||||
_canvasState.value = _canvasState.value.copy(zoom = zoom, panX = panX, panY = panY)
|
_canvasState.value = _canvasState.value.copy(zoom = zoom, panX = panX, panY = panY)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onStrokeCompleted(penSize: Float, color: Int, points: FloatArray) {
|
fun onStrokeCompleted(penSize: Float, color: Int, points: FloatArray, style: String = Stroke.STYLE_PLAIN) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val pageId = _currentPageId.value
|
val pageId = _currentPageId.value
|
||||||
val order = pageRepository.getNextStrokeOrder(pageId)
|
val order = pageRepository.getNextStrokeOrder(pageId)
|
||||||
@@ -163,6 +163,7 @@ class EditorViewModel(
|
|||||||
pointData = points.toBlob(),
|
pointData = points.toBlob(),
|
||||||
strokeOrder = order,
|
strokeOrder = order,
|
||||||
createdAt = System.currentTimeMillis(),
|
createdAt = System.currentTimeMillis(),
|
||||||
|
style = style,
|
||||||
)
|
)
|
||||||
undoManager.perform(
|
undoManager.perform(
|
||||||
AddStrokeAction(
|
AddStrokeAction(
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class PadCanvasView(context: Context) : View(context) {
|
|||||||
private val inverseMatrix = Matrix()
|
private val inverseMatrix = Matrix()
|
||||||
|
|
||||||
// --- Callbacks ---
|
// --- Callbacks ---
|
||||||
var onStrokeCompleted: ((penSize: Float, color: Int, points: FloatArray) -> Unit)? = null
|
var onStrokeCompleted: ((penSize: Float, color: Int, points: FloatArray, style: String) -> Unit)? = null
|
||||||
var onZoomPanChanged: ((zoom: Float, panX: Float, panY: Float) -> Unit)? = null
|
var onZoomPanChanged: ((zoom: Float, panX: Float, panY: Float) -> Unit)? = null
|
||||||
var onStrokeErased: ((strokeId: Long) -> Unit)? = null
|
var onStrokeErased: ((strokeId: Long) -> Unit)? = null
|
||||||
var onSelectionComplete: ((selectedIds: Set<Long>) -> Unit)? = null
|
var onSelectionComplete: ((selectedIds: Set<Long>) -> Unit)? = null
|
||||||
@@ -93,7 +93,8 @@ class PadCanvasView(context: Context) : View(context) {
|
|||||||
var onStrokeMoved: ((strokeId: Long, deltaX: Float, deltaY: Float) -> Unit)? = null
|
var onStrokeMoved: ((strokeId: Long, deltaX: Float, deltaY: Float) -> Unit)? = null
|
||||||
|
|
||||||
// --- Move tool state ---
|
// --- Move tool state ---
|
||||||
private var movingStrokeId: Long? = null
|
private var isMoving = false
|
||||||
|
private var movingIds = mutableSetOf<Long>()
|
||||||
private var moveLastX = 0f
|
private var moveLastX = 0f
|
||||||
private var moveLastY = 0f
|
private var moveLastY = 0f
|
||||||
private var moveOriginX = 0f
|
private var moveOriginX = 0f
|
||||||
@@ -199,16 +200,16 @@ class PadCanvasView(context: Context) : View(context) {
|
|||||||
for (stroke in strokes) {
|
for (stroke in strokes) {
|
||||||
val points = stroke.pointData.toFloatArray()
|
val points = stroke.pointData.toFloatArray()
|
||||||
val path = buildPathFromPoints(points)
|
val path = buildPathFromPoints(points)
|
||||||
val paint = buildPaint(stroke.penSize, stroke.color)
|
val paint = buildPaint(stroke.penSize, stroke.color, stroke.style)
|
||||||
completedStrokes.add(StrokeRender(path, paint, stroke.id))
|
completedStrokes.add(StrokeRender(path, paint, stroke.id, stroke.style, points))
|
||||||
}
|
}
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addCompletedStroke(id: Long, penSize: Float, color: Int, points: FloatArray) {
|
fun addCompletedStroke(id: Long, penSize: Float, color: Int, points: FloatArray, style: String = Stroke.STYLE_PLAIN) {
|
||||||
val path = buildPathFromPoints(points)
|
val path = buildPathFromPoints(points)
|
||||||
val paint = buildPaint(penSize, color)
|
val paint = buildPaint(penSize, color, style)
|
||||||
completedStrokes.add(StrokeRender(path, paint, id))
|
completedStrokes.add(StrokeRender(path, paint, id, style, points))
|
||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,6 +246,19 @@ class PadCanvasView(context: Context) : View(context) {
|
|||||||
// Completed strokes
|
// Completed strokes
|
||||||
for (sr in completedStrokes) {
|
for (sr in completedStrokes) {
|
||||||
drawPath(sr.path, sr.paint)
|
drawPath(sr.path, sr.paint)
|
||||||
|
// Draw arrow heads for arrow-style strokes
|
||||||
|
val pts = sr.points
|
||||||
|
if (pts != null && pts.size >= 4) {
|
||||||
|
val x1 = pts[0]
|
||||||
|
val y1 = pts[1]
|
||||||
|
val x2 = pts[2]
|
||||||
|
val y2 = pts[3]
|
||||||
|
if (sr.style == Stroke.STYLE_ARROW || sr.style == Stroke.STYLE_DOUBLE_ARROW) {
|
||||||
|
drawArrowHeads(this, x1, y1, x2, y2,
|
||||||
|
if (sr.style == Stroke.STYLE_ARROW) LineStyle.ARROW else LineStyle.DOUBLE_ARROW,
|
||||||
|
sr.paint.strokeWidth)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// In-progress stroke
|
// In-progress stroke
|
||||||
@@ -465,7 +479,7 @@ class PadCanvasView(context: Context) : View(context) {
|
|||||||
isSnappedToLine = false
|
isSnappedToLine = false
|
||||||
if (currentPoints.size >= 4) {
|
if (currentPoints.size >= 4) {
|
||||||
val points = currentPoints.toFloatArray()
|
val points = currentPoints.toFloatArray()
|
||||||
onStrokeCompleted?.invoke(canvasState.penWidthPt, Color.BLACK, points)
|
onStrokeCompleted?.invoke(canvasState.penWidthPt, Color.BLACK, points, Stroke.STYLE_PLAIN)
|
||||||
}
|
}
|
||||||
currentPath = null
|
currentPath = null
|
||||||
currentPaint = null
|
currentPaint = null
|
||||||
@@ -537,7 +551,7 @@ class PadCanvasView(context: Context) : View(context) {
|
|||||||
left, bottom,
|
left, bottom,
|
||||||
left, top, // close the rectangle
|
left, top, // close the rectangle
|
||||||
)
|
)
|
||||||
onStrokeCompleted?.invoke(canvasState.penWidthPt, Color.BLACK, points)
|
onStrokeCompleted?.invoke(canvasState.penWidthPt, Color.BLACK, points, Stroke.STYLE_PLAIN)
|
||||||
invalidate()
|
invalidate()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -551,42 +565,59 @@ class PadCanvasView(context: Context) : View(context) {
|
|||||||
when (event.actionMasked) {
|
when (event.actionMasked) {
|
||||||
MotionEvent.ACTION_DOWN -> {
|
MotionEvent.ACTION_DOWN -> {
|
||||||
val pt = screenToCanonical(event.x, event.y)
|
val pt = screenToCanonical(event.x, event.y)
|
||||||
val hitId = hitTestStroke(pt[0], pt[1])
|
moveLastX = pt[0]
|
||||||
if (hitId != null) {
|
moveLastY = pt[1]
|
||||||
movingStrokeId = hitId
|
moveOriginX = pt[0]
|
||||||
moveLastX = pt[0]
|
moveOriginY = pt[1]
|
||||||
moveLastY = pt[1]
|
|
||||||
moveOriginX = pt[0]
|
if (selectedStrokeIds.isNotEmpty()) {
|
||||||
moveOriginY = pt[1]
|
// Move the existing selection
|
||||||
// Highlight the stroke being moved
|
movingIds.clear()
|
||||||
selectedStrokeIds.clear()
|
movingIds.addAll(selectedStrokeIds)
|
||||||
selectedStrokeIds.add(hitId)
|
isMoving = true
|
||||||
invalidate()
|
} else {
|
||||||
|
// Hit-test a single stroke to grab
|
||||||
|
val hitId = hitTestStroke(pt[0], pt[1])
|
||||||
|
if (hitId != null) {
|
||||||
|
movingIds.clear()
|
||||||
|
movingIds.add(hitId)
|
||||||
|
selectedStrokeIds.add(hitId)
|
||||||
|
isMoving = true
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
MotionEvent.ACTION_MOVE -> {
|
MotionEvent.ACTION_MOVE -> {
|
||||||
val id = movingStrokeId ?: return true
|
if (!isMoving) return true
|
||||||
val pt = screenToCanonical(event.x, event.y)
|
val pt = screenToCanonical(event.x, event.y)
|
||||||
val dx = pt[0] - moveLastX
|
val dx = pt[0] - moveLastX
|
||||||
val dy = pt[1] - moveLastY
|
val dy = pt[1] - moveLastY
|
||||||
// Offset the path visually
|
for (sr in completedStrokes) {
|
||||||
completedStrokes.find { it.id == id }?.path?.offset(dx, dy)
|
if (sr.id in movingIds) {
|
||||||
|
sr.path.offset(dx, dy)
|
||||||
|
}
|
||||||
|
}
|
||||||
moveLastX = pt[0]
|
moveLastX = pt[0]
|
||||||
moveLastY = pt[1]
|
moveLastY = pt[1]
|
||||||
invalidate()
|
invalidate()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
MotionEvent.ACTION_UP -> {
|
MotionEvent.ACTION_UP -> {
|
||||||
val id = movingStrokeId ?: return true
|
if (!isMoving) return true
|
||||||
val pt = screenToCanonical(event.x, event.y)
|
val pt = screenToCanonical(event.x, event.y)
|
||||||
val totalDx = pt[0] - moveOriginX
|
val totalDx = pt[0] - moveOriginX
|
||||||
val totalDy = pt[1] - moveOriginY
|
val totalDy = pt[1] - moveOriginY
|
||||||
movingStrokeId = null
|
isMoving = false
|
||||||
selectedStrokeIds.clear()
|
val ids = movingIds.toSet()
|
||||||
if (Math.abs(totalDx) > 1f || Math.abs(totalDy) > 1f) {
|
if (Math.abs(totalDx) > 1f || Math.abs(totalDy) > 1f) {
|
||||||
onStrokeMoved?.invoke(id, totalDx, totalDy)
|
if (ids.size == 1) {
|
||||||
|
onStrokeMoved?.invoke(ids.first(), totalDx, totalDy)
|
||||||
|
} else {
|
||||||
|
onSelectionMoved?.invoke(ids, totalDx, totalDy)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Keep selection highlighted after move
|
||||||
invalidate()
|
invalidate()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -617,12 +648,15 @@ class PadCanvasView(context: Context) : View(context) {
|
|||||||
}
|
}
|
||||||
MotionEvent.ACTION_UP -> {
|
MotionEvent.ACTION_UP -> {
|
||||||
isDrawingLine = false
|
isDrawingLine = false
|
||||||
// Build the line stroke as points
|
val lineStyle = canvasState.lineStyle
|
||||||
val points = buildLinePoints(
|
val points = floatArrayOf(lineStartX, lineStartY, lineEndX, lineEndY)
|
||||||
lineStartX, lineStartY, lineEndX, lineEndY,
|
val strokeStyle = when (lineStyle) {
|
||||||
canvasState.lineStyle,
|
LineStyle.PLAIN -> Stroke.STYLE_PLAIN
|
||||||
)
|
LineStyle.ARROW -> Stroke.STYLE_ARROW
|
||||||
onStrokeCompleted?.invoke(canvasState.penWidthPt, Color.BLACK, points)
|
LineStyle.DOUBLE_ARROW -> Stroke.STYLE_DOUBLE_ARROW
|
||||||
|
LineStyle.DASHED -> Stroke.STYLE_DASHED
|
||||||
|
}
|
||||||
|
onStrokeCompleted?.invoke(canvasState.penWidthPt, Color.BLACK, points, strokeStyle)
|
||||||
invalidate()
|
invalidate()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -630,50 +664,6 @@ class PadCanvasView(context: Context) : View(context) {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Build points for a line stroke. For plain/dashed, just two endpoints.
|
|
||||||
* Arrow heads are encoded as extra path segments.
|
|
||||||
*/
|
|
||||||
private fun buildLinePoints(
|
|
||||||
x1: Float, y1: Float, x2: Float, y2: Float, style: LineStyle,
|
|
||||||
): FloatArray {
|
|
||||||
val points = mutableListOf<Float>()
|
|
||||||
points.add(x1); points.add(y1)
|
|
||||||
points.add(x2); points.add(y2)
|
|
||||||
|
|
||||||
val arrowLen = 40f // Arrow head length in canonical points
|
|
||||||
val arrowAngle = Math.toRadians(25.0)
|
|
||||||
val dx = (x2 - x1).toDouble()
|
|
||||||
val dy = (y2 - y1).toDouble()
|
|
||||||
val angle = Math.atan2(dy, dx)
|
|
||||||
|
|
||||||
if (style == LineStyle.ARROW || style == LineStyle.DOUBLE_ARROW) {
|
|
||||||
// Arrow at end (x2, y2)
|
|
||||||
val ax1 = x2 - (arrowLen * Math.cos(angle - arrowAngle)).toFloat()
|
|
||||||
val ay1 = y2 - (arrowLen * Math.sin(angle - arrowAngle)).toFloat()
|
|
||||||
val ax2 = x2 - (arrowLen * Math.cos(angle + arrowAngle)).toFloat()
|
|
||||||
val ay2 = y2 - (arrowLen * Math.sin(angle + arrowAngle)).toFloat()
|
|
||||||
// Draw as: tip -> left wing, then move back to tip -> right wing
|
|
||||||
points.add(ax1); points.add(ay1)
|
|
||||||
points.add(x2); points.add(y2)
|
|
||||||
points.add(ax2); points.add(ay2)
|
|
||||||
}
|
|
||||||
if (style == LineStyle.DOUBLE_ARROW) {
|
|
||||||
// Arrow at start (x1, y1)
|
|
||||||
val revAngle = angle + Math.PI
|
|
||||||
val bx1 = x1 - (arrowLen * Math.cos(revAngle - arrowAngle)).toFloat()
|
|
||||||
val by1 = y1 - (arrowLen * Math.sin(revAngle - arrowAngle)).toFloat()
|
|
||||||
val bx2 = x1 - (arrowLen * Math.cos(revAngle + arrowAngle)).toFloat()
|
|
||||||
val by2 = y1 - (arrowLen * Math.sin(revAngle + arrowAngle)).toFloat()
|
|
||||||
points.add(x1); points.add(y1)
|
|
||||||
points.add(bx1); points.add(by1)
|
|
||||||
points.add(x1); points.add(y1)
|
|
||||||
points.add(bx2); points.add(by2)
|
|
||||||
}
|
|
||||||
|
|
||||||
return points.toFloatArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun drawArrowHeads(
|
private fun drawArrowHeads(
|
||||||
canvas: Canvas, x1: Float, y1: Float, x2: Float, y2: Float,
|
canvas: Canvas, x1: Float, y1: Float, x2: Float, y2: Float,
|
||||||
style: LineStyle, strokeWidth: Float,
|
style: LineStyle, strokeWidth: Float,
|
||||||
@@ -996,18 +986,27 @@ class PadCanvasView(context: Context) : View(context) {
|
|||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildPaint(penSize: Float, color: Int): Paint {
|
private fun buildPaint(penSize: Float, color: Int, style: String = Stroke.STYLE_PLAIN): Paint {
|
||||||
return Paint().apply {
|
return Paint().apply {
|
||||||
this.color = color
|
this.color = color
|
||||||
strokeWidth = penSize
|
strokeWidth = penSize
|
||||||
style = Paint.Style.STROKE
|
this.style = Paint.Style.STROKE
|
||||||
strokeCap = Paint.Cap.ROUND
|
strokeCap = Paint.Cap.ROUND
|
||||||
strokeJoin = Paint.Join.ROUND
|
strokeJoin = Paint.Join.ROUND
|
||||||
isAntiAlias = false // Crisp lines on e-ink — no edge fading
|
isAntiAlias = false
|
||||||
|
if (style == Stroke.STYLE_DASHED) {
|
||||||
|
pathEffect = android.graphics.DashPathEffect(floatArrayOf(30f, 20f), 0f)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class StrokeRender(val path: Path, val paint: Paint, val id: Long)
|
private data class StrokeRender(
|
||||||
|
val path: Path,
|
||||||
|
val paint: Paint,
|
||||||
|
val id: Long,
|
||||||
|
val style: String = Stroke.STYLE_PLAIN,
|
||||||
|
val points: FloatArray? = null, // Needed for arrow rendering
|
||||||
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** Eraser hit radius in canonical points (~3.5mm at 300 DPI). */
|
/** Eraser hit radius in canonical points (~3.5mm at 300 DPI). */
|
||||||
|
|||||||
Reference in New Issue
Block a user