@@ -25,6 +25,43 @@ BlBezierCurveGeometry >> = anObject [
2525 ^ self controlPoints = anObject controlPoints
2626]
2727
28+ { #category : #geometry }
29+ BlBezierCurveGeometry >> asPolylineVertices [
30+ " Answer an Array of Points approximating the curve, for hit-testing.
31+
32+ For the cubic (4-point) case this reuses the same De Casteljau-flattened
33+ cache that the renderer uses, so both share a single computation and a
34+ single allocation.
35+
36+ For the quadratic (3-point) case a coarse uniform sample is sufficient
37+ for hit-testing purposes."
38+
39+ | c b a via1 via2 start end |
40+
41+ controlPoints size = 2 ifTrue: [
42+ ^ Array
43+ with: controlPoints first
44+ with: controlPoints second ].
45+
46+ controlPoints size = 4 ifTrue: [
47+ | result |
48+ result := OrderedCollection new .
49+ self flattenedCubicVerticesDo: [ :x :y | result add: x @ y ].
50+ ^ result asArray ].
51+
52+ " size = 3: treat as degenerate cubic (CP2 = CP3) and sample uniformly."
53+ start := controlPoints first.
54+ via1 := controlPoints second.
55+ via2 := controlPoints third.
56+ end := via2.
57+ c := 3 * (via1 - start).
58+ b := 3 * (via2 - via1) - c.
59+ a := end - start - c - b.
60+
61+ ^ #(0.0 0.2 0.4 0.6 0.8 1.0) collect: [ :t |
62+ a * (t raisedTo: 3 ) + (b * (t * t)) + (c * t) + start ]
63+ ]
64+
2865{ #category : #' geometry testing' }
2966BlBezierCurveGeometry >> containsPoint: aPoint alreadyInGeometryBoundsOf: aBlElement [
3067 " Answer whether aPoint is contained by this geometry.
@@ -33,10 +70,10 @@ BlBezierCurveGeometry >> containsPoint: aPoint alreadyInGeometryBoundsOf: aBlEle
3370
3471 | lines ir |
3572 ir := aBlElement border width / 2 .
36- lines := self asPolylineVertices. " TODO: cache "
73+ lines := self asPolylineVertices.
3774
3875 ^ (1 to: lines size - 1 ) anySatisfy: [ :i |
39- aPoint
76+ aPoint
4077 onLineFrom: (lines at: i)
4178 to: (lines at: i + 1 )
4279 within: ir ]
@@ -57,6 +94,96 @@ BlBezierCurveGeometry >> controlPoints: aCollection [
5794 self releaseCache
5895]
5996
97+ { #category : #private }
98+ BlBezierCurveGeometry >> flattenCubicFrom: p0 cp1: p1 cp2: p2 to: p3 into: aCollection depth: depth [
99+ " Private helper for flattenedCubicVerticesDo:.
100+ Recursively subdivides the cubic Bezier p0-p1-p2-p3 using De Casteljau and
101+ appends each new vertex to aCollection (p0 is assumed to be already present).
102+
103+ Flatness criterion: the perpendicular distance from each interior control
104+ point to the chord p0→p3 must be ≤ 1.0 user-space unit (≈ 1 pixel).
105+ Working with squared distances avoids a sqrt per call.
106+
107+ A depth limit of 16 prevents unbounded recursion on degenerate inputs
108+ (e.g. all four points coincident)."
109+
110+ | chordX chordY chordLenSq d1Sq d2Sq q0 q1 q2 r0 r1 mid |
111+
112+ depth > 16 ifTrue: [
113+ aCollection add: p3.
114+ ^ self ].
115+
116+ " Chord vector p0→p3."
117+ chordX := p3 x - p0 x.
118+ chordY := p3 y - p0 y.
119+ chordLenSq := (chordX * chordX) + (chordY * chordY).
120+
121+ " Negligibly short chord → the whole sub-curve is sub-pixel; close it."
122+ chordLenSq < 1.0e-8 ifTrue: [
123+ aCollection add: p3.
124+ ^ self ].
125+
126+ " Perpendicular-distance² from each interior control point to the chord.
127+ d = |cross(chord, p - p0)| / |chord| → d² = cross² / chordLenSq"
128+ d1Sq := ((p1 x - p0 x) * chordY - ((p1 y - p0 y) * chordX)) squared
129+ / chordLenSq.
130+ d2Sq := ((p2 x - p0 x) * chordY - ((p2 y - p0 y) * chordX)) squared
131+ / chordLenSq.
132+
133+ " Flat enough? tolerance = 1.0 px → tolerance² = 1.0"
134+ (d1Sq max: d2Sq) <= 1.0 ifTrue: [
135+ aCollection add: p3.
136+ ^ self ].
137+
138+ " Subdivide at t = 0.5 using De Casteljau."
139+ q0 := (p0 + p1) * 0.5 .
140+ q1 := (p1 + p2) * 0.5 .
141+ q2 := (p2 + p3) * 0.5 .
142+ r0 := (q0 + q1) * 0.5 .
143+ r1 := (q1 + q2) * 0.5 .
144+ mid := (r0 + r1) * 0.5 .
145+
146+ self
147+ flattenCubicFrom: p0 cp1: q0 cp2: r0 to: mid
148+ into: aCollection
149+ depth: depth + 1 .
150+ self
151+ flattenCubicFrom: mid cp1: r1 cp2: q2 to: p3
152+ into: aCollection
153+ depth: depth + 1
154+ ]
155+
156+ { #category : #geometry }
157+ BlBezierCurveGeometry >> flattenedCubicVerticesDo: aBlock [
158+ " Evaluate aBlock with (x, y) for each vertex of the De Casteljau-flattened
159+ cubic Bezier path, including the start point p0.
160+
161+ The flattened vertex array is computed lazily on first call and stored in
162+ pathCache geometryPath. The cache is cleared automatically whenever
163+ releaseCache is called (e.g. from controlPoints:), which also clears the
164+ inherited strokedBounds cache.
165+
166+ Usage:
167+ self flattenedCubicVerticesDo: [:x :y | canvas lineToX: x y: y]"
168+
169+ | vertices |
170+ vertices := pathCache geometryPath ifNil: [
171+ | result |
172+ result := OrderedCollection with: controlPoints first.
173+ self
174+ flattenCubicFrom: controlPoints first
175+ cp1: controlPoints second
176+ cp2: controlPoints third
177+ to: controlPoints fourth
178+ into: result
179+ depth: 0 .
180+ result := result asArray.
181+ pathCache geometryPath: result.
182+ result ].
183+
184+ vertices do: [ :each | aBlock value: each x value: each y ]
185+ ]
186+
60187{ #category : #' geometry bounds' }
61188BlBezierCurveGeometry >> geometryBounds: aBlBounds [
62189
0 commit comments