diff --git a/app/src/main/kotlin/net/metacircular/engpad/ui/editor/PadCanvasView.kt b/app/src/main/kotlin/net/metacircular/engpad/ui/editor/PadCanvasView.kt index 2f62483..2dd232a 100644 --- a/app/src/main/kotlin/net/metacircular/engpad/ui/editor/PadCanvasView.kt +++ b/app/src/main/kotlin/net/metacircular/engpad/ui/editor/PadCanvasView.kt @@ -38,6 +38,11 @@ class PadCanvasView(context: Context) : View(context) { // --- Strokes --- private val completedStrokes = mutableListOf() + // --- Backing bitmap for completed strokes (screen resolution) --- + private var backingBitmap: android.graphics.Bitmap? = null + private var backingCanvas: Canvas? = null + private var bitmapDirty = true + // --- In-progress stroke --- private var currentPath: Path? = null private var currentPaint: Paint? = null @@ -203,6 +208,7 @@ class PadCanvasView(context: Context) : View(context) { val paint = buildPaint(stroke.penSize, stroke.color, stroke.style) completedStrokes.add(StrokeRender(path, paint, stroke.id, stroke.style, points)) } + bitmapDirty = true invalidate() } @@ -210,11 +216,13 @@ class PadCanvasView(context: Context) : View(context) { val path = buildPathFromPoints(points) val paint = buildPaint(penSize, color, style) completedStrokes.add(StrokeRender(path, paint, id, style, points)) + bitmapDirty = true invalidate() } fun removeStroke(id: Long) { completedStrokes.removeAll { it.id == id } + bitmapDirty = true invalidate() } @@ -223,6 +231,7 @@ class PadCanvasView(context: Context) : View(context) { override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) rebuildViewMatrix() + bitmapDirty = true } override fun onDraw(canvas: Canvas) { @@ -241,26 +250,12 @@ class PadCanvasView(context: Context) : View(context) { // Draw grid in screen space with pixel-snapped positions drawGridScreenSpace(canvas) - // Draw everything else in canonical space - canvas.withMatrix(viewMatrix) { - // Completed strokes - for (sr in completedStrokes) { - 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) - } - } - } + // Draw completed strokes from backing bitmap (screen resolution) + ensureBacking() + backingBitmap?.let { canvas.drawBitmap(it, 0f, 0f, null) } + // Draw dynamic elements in canonical space (in-progress stroke, previews, selection) + canvas.withMatrix(viewMatrix) { // In-progress stroke currentPath?.let { path -> currentPaint?.let { paint -> @@ -319,6 +314,42 @@ class PadCanvasView(context: Context) : View(context) { * artifacts that cause uneven grid spacing when drawn in canonical space * and scaled by the view matrix. */ + /** + * Ensure the backing bitmap is up-to-date. The bitmap is at view pixel + * resolution with the viewMatrix applied, so completed strokes are + * pixel-perfect. Only redrawn when strokes change or zoom/pan changes. + */ + private fun ensureBacking() { + val w = width + val h = height + if (w <= 0 || h <= 0) return + + if (backingBitmap == null || backingBitmap!!.width != w || backingBitmap!!.height != h) { + backingBitmap?.recycle() + backingBitmap = androidx.core.graphics.createBitmap(w, h) + backingCanvas = Canvas(backingBitmap!!) + bitmapDirty = true + } + + if (bitmapDirty) { + backingBitmap!!.eraseColor(android.graphics.Color.TRANSPARENT) + val c = backingCanvas!! + c.setMatrix(viewMatrix) + for (sr in completedStrokes) { + c.drawPath(sr.path, sr.paint) + val pts = sr.points + if (pts != null && pts.size >= 4) { + if (sr.style == Stroke.STYLE_ARROW || sr.style == Stroke.STYLE_DOUBLE_ARROW) { + drawArrowHeads(c, pts[0], pts[1], pts[2], pts[3], + if (sr.style == Stroke.STYLE_ARROW) LineStyle.ARROW else LineStyle.DOUBLE_ARROW, + sr.paint.strokeWidth) + } + } + } + bitmapDirty = false + } + } + private fun drawGridScreenSpace(canvas: Canvas) { val pageW = canvasState.pageSize.widthPt.toFloat() val pageH = canvasState.pageSize.heightPt.toFloat() @@ -600,6 +631,7 @@ class PadCanvasView(context: Context) : View(context) { } moveLastX = pt[0] moveLastY = pt[1] + bitmapDirty = true invalidate() return true } @@ -870,6 +902,7 @@ class PadCanvasView(context: Context) : View(context) { sr.path.offset(dx, dy) } } + bitmapDirty = true } fun clearSelection() { @@ -970,6 +1003,7 @@ class PadCanvasView(context: Context) : View(context) { viewMatrix.postTranslate(panX, panY) viewMatrix.invert(inverseMatrix) + bitmapDirty = true } private fun screenToCanonical(screenX: Float, screenY: Float): FloatArray {