@@ -3,74 +3,126 @@ package com.alexdremov.notate.controller
33import android.graphics.Matrix
44import android.graphics.RectF
55import com.alexdremov.notate.model.CanvasItem
6- import java.util.concurrent.ConcurrentHashMap
76
87/* *
98 * Manages the state of the active selection.
109 * Holds the selected items and the transient transformation matrix.
1110 * Thread-safe.
1211 */
1312class SelectionManager {
14- private val _selectedItems = ConcurrentHashMap .newKeySet<CanvasItem >()
15- val selectedItems: Set <CanvasItem > get() = _selectedItems
13+ private val lock = Any ()
14+ private val _selectedItems = HashSet <CanvasItem >()
15+ val selectedItems: Set <CanvasItem >
16+ get() = synchronized(lock) { _selectedItems .toSet() }
1617
1718 // Backwards compatibility for callers expecting Stroke
1819 val selectedStrokes: Set < com.alexdremov.notate.model.Stroke >
19- get() = _selectedItems .filterIsInstance< com.alexdremov.notate.model.Stroke > ().toSet()
20+ get() = synchronized(lock) { _selectedItems .filterIsInstance< com.alexdremov.notate.model.Stroke > ().toSet() }
2021
2122 // Current transformation applied to the selection (transient)
22- val transformMatrix = Matrix ()
23+ private val transformMatrix = Matrix ()
2324
2425 // Bounding box of the original selection (before transform)
2526 private val selectionBounds = RectF ()
2627
28+ /* *
29+ * Returns a defensive copy of the current transformation matrix.
30+ *
31+ * Note: this allocates a new [Matrix] on every call. For performance-critical,
32+ * read-only access that avoids allocations, use [withTransformReadLocked].
33+ */
34+ fun getTransform (): Matrix {
35+ synchronized(lock) {
36+ return Matrix (transformMatrix)
37+ }
38+ }
39+
40+ /* *
41+ * Executes [block] while holding the internal lock, providing direct read-only
42+ * access to the current transformation matrix without creating a copy.
43+ *
44+ * Callers must not mutate [Matrix] inside [block]. Violating this contract can
45+ * break invariants and thread-safety guarantees of [SelectionManager].
46+ */
47+ fun <T > withTransformReadLocked (block : (Matrix ) -> T ): T {
48+ synchronized(lock) {
49+ return block(transformMatrix)
50+ }
51+ }
52+ fun resetTransform () {
53+ synchronized(lock) {
54+ transformMatrix.reset()
55+ }
56+ }
57+
2758 fun select (item : CanvasItem ) {
28- _selectedItems .add(item)
29- recomputeBounds()
59+ synchronized(lock) {
60+ _selectedItems .add(item)
61+ recomputeBoundsInternal()
62+ }
3063 }
3164
3265 fun selectAll (items : List <CanvasItem >) {
33- _selectedItems .addAll(items)
34- recomputeBounds()
66+ synchronized(lock) {
67+ _selectedItems .addAll(items)
68+ recomputeBoundsInternal()
69+ }
3570 }
3671
3772 fun deselect (item : CanvasItem ) {
38- _selectedItems .remove(item)
39- recomputeBounds()
73+ synchronized(lock) {
74+ _selectedItems .remove(item)
75+ recomputeBoundsInternal()
76+ }
4077 }
4178
4279 fun clearSelection () {
43- _selectedItems .clear()
44- transformMatrix.reset()
45- selectionBounds.setEmpty()
80+ synchronized(lock) {
81+ _selectedItems .clear()
82+ transformMatrix.reset()
83+ selectionBounds.setEmpty()
84+ }
4685 }
4786
48- fun hasSelection () = _selectedItems .isNotEmpty()
87+ fun hasSelection (): Boolean = synchronized(lock) { _selectedItems .isNotEmpty() }
4988
50- fun isSelected (item : CanvasItem ) = _selectedItems .contains(item)
89+ fun isSelected (item : CanvasItem ): Boolean = synchronized(lock) { _selectedItems .contains(item) }
5190
5291 private fun recomputeBounds () {
92+ synchronized(lock) {
93+ recomputeBoundsInternal()
94+ }
95+ }
96+
97+ private fun recomputeBoundsInternal () {
5398 if (_selectedItems .isEmpty()) {
5499 selectionBounds.setEmpty()
55100 return
56101 }
57- val iter = _selectedItems .iterator()
58- if (iter.hasNext()) {
59- selectionBounds.set(iter.next().bounds)
60- }
61- while (iter.hasNext()) {
62- selectionBounds.union(iter.next().bounds)
102+
103+ val tempBounds = RectF ()
104+ var first = true
105+ for (item in _selectedItems ) {
106+ if (first) {
107+ tempBounds.set(item.bounds)
108+ first = false
109+ } else {
110+ tempBounds.union(item.bounds)
111+ }
63112 }
113+ selectionBounds.set(tempBounds)
64114 }
65115
66116 /* *
67117 * Returns the bounding box of the selection with the current transform applied.
68118 * Note: This is an AABB (Axis Aligned Bounding Box) of the transformed shape.
69119 */
70120 fun getTransformedBounds (): RectF {
71- val r = RectF (selectionBounds)
72- transformMatrix.mapRect(r)
73- return r
121+ synchronized(lock) {
122+ val r = RectF (selectionBounds)
123+ transformMatrix.mapRect(r)
124+ return r
125+ }
74126 }
75127
76128 /* *
@@ -79,39 +131,47 @@ class SelectionManager {
79131 * Order: Top-Left, Top-Right, Bottom-Right, Bottom-Left.
80132 */
81133 fun getTransformedCorners (): FloatArray {
82- val pts =
83- floatArrayOf(
84- selectionBounds.left,
85- selectionBounds.top, // TL
86- selectionBounds.right,
87- selectionBounds.top, // TR
88- selectionBounds.right,
89- selectionBounds.bottom, // BR
90- selectionBounds.left,
91- selectionBounds.bottom, // BL
92- )
93- transformMatrix.mapPoints(pts)
94- return pts
134+ synchronized(lock) {
135+ val pts =
136+ floatArrayOf(
137+ selectionBounds.left,
138+ selectionBounds.top, // TL
139+ selectionBounds.right,
140+ selectionBounds.top, // TR
141+ selectionBounds.right,
142+ selectionBounds.bottom, // BR
143+ selectionBounds.left,
144+ selectionBounds.bottom, // BL
145+ )
146+ transformMatrix.mapPoints(pts)
147+ return pts
148+ }
95149 }
96150
97151 /* *
98152 * Returns the center of the selection in World coordinates,
99153 * with the current transform applied.
100154 */
101155 fun getSelectionCenter (): FloatArray {
102- val pts = floatArrayOf(selectionBounds.centerX(), selectionBounds.centerY())
103- transformMatrix.mapPoints(pts)
104- return pts
156+ synchronized(lock) {
157+ val pts = floatArrayOf(selectionBounds.centerX(), selectionBounds.centerY())
158+ transformMatrix.mapPoints(pts)
159+ return pts
160+ }
105161 }
106162
107163 fun translate (
108164 dx : Float ,
109165 dy : Float ,
110166 ) {
111- transformMatrix.postTranslate(dx, dy)
167+ synchronized(lock) {
168+ transformMatrix.postTranslate(dx, dy)
169+ }
112170 }
113171
114172 fun applyTransform (matrix : Matrix ) {
115- transformMatrix.postConcat(matrix)
173+ synchronized(lock) {
174+ transformMatrix.postConcat(matrix)
175+ }
116176 }
117177}
0 commit comments