Skip to content

Commit c17da6e

Browse files
committed
core: Fix some issues with hitTest
Fixes some issues with our winding # calculation which would cause incorrect results for hitTest. * The convention for handling an intersection at endpoints was not the same between lines and bezier curves. * The bezier curve winding # function was not properly handling some cases where the curve was strictly y-monotonic. * Simplify the code a bit so that ray-curve intersections are returned in a consistent order based on upward/downward crossing.
1 parent 08917b4 commit c17da6e

File tree

5 files changed

+110
-81
lines changed

5 files changed

+110
-81
lines changed

render/src/shape_utils.rs

Lines changed: 78 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1073,14 +1073,14 @@ fn winding_number_line(
10731073
// An upward segment (-y) increments the winding number (including the initial endpoint).
10741074
// A downward segment (+y) decrements the winding number (including the final endpoint)
10751075
// Perp-dot indicates which side of the segment the point is on.
1076-
if y0 <= point_y {
1077-
if y1 > point_y {
1076+
if y0 < point_y {
1077+
if y1 >= point_y {
10781078
let val = (x1 - x0) * (point_y - y0) - (y1 - y0) * (point_x - x0);
10791079
if val > 0.0 {
10801080
return 1;
10811081
}
10821082
}
1083-
} else if y1 <= point_y {
1083+
} else if y1 < point_y {
10841084
let val = (x1 - x0) * (point_y - y0) - (y1 - y0) * (point_x - x0);
10851085

10861086
if val < 0.0 {
@@ -1107,13 +1107,13 @@ fn winding_number_curve(
11071107
// However, there are two issues:
11081108
// 1) Solving the quadratic needs to be numerically robust, particularly near the endpoints 0.0 and 1.0, and as the curve is tangent to the ray.
11091109
// We use the "Citardauq" method for improved numerical stability.
1110-
// 2) The convention for including/excluding endpoints needs to act similarly to lines, with the initial point included if the curve is "downward",
1111-
// and the final point included if the curve is pointing "upward". This is complicated by the fact that the curve could be tangent to the ray
1110+
// 2) The convention for including/excluding endpoints needs to act similarly to lines, with the initial point included if the curve is "upward",
1111+
// and the final point included if the curve is pointing "downward". This is complicated by the fact that the curve could be tangent to the ray
11121112
// at the endpoint (this is still considered "upward" or "downward" depending on the slope at earlier t).
11131113
// We solve this by splitting the curve into y-monotonic subcurves. This is helpful because
11141114
// a) each subcurve will have 1 intersection with the ray
11151115
// b) if the subcurve surrounds the ray, we know it has an intersection without having to check if t is in [0, 1]
1116-
// c) we can determine the winding of the segment upward/downward by comparing the subcurve endpoints, also properly handling the endpoint convention.
1116+
// c) we know the winding of the segment upward/downward based on which root it contains
11171117

11181118
let x0 = ax0.get() - point_x.get();
11191119
let y0 = ay0.get() - point_y.get();
@@ -1141,63 +1141,68 @@ fn winding_number_curve(
11411141
let b = 2.0 * (y1 - y0);
11421142
let c = y0;
11431143

1144-
let roots = solve_quadratic(a, b, c);
1145-
1146-
if roots.is_empty() {
1144+
let (t0, t1) = solve_quadratic(a, b, c);
1145+
let is_t0_valid = t0.is_finite();
1146+
let is_t1_valid = t1.is_finite();
1147+
if !is_t0_valid && !is_t1_valid {
11471148
return 0;
11481149
}
11491150

11501151
// Split the curve into two y-monotonic segments.
1151-
let mut y_start = y0;
1152-
let mut y_end = if roots.len() == 1 {
1153-
// Linear, so already monotonic.
1154-
y2
1155-
} else {
1156-
// Find the extrema point where the curve is horizontal.
1157-
// This is the point that splits the curve into 2 y-monotonic segments.
1158-
let t_extrema = -b / (2.0 * a);
1159-
a * t_extrema * t_extrema + b * t_extrema + c
1160-
};
1161-
1152+
let mut winding = 0;
11621153
let ax = x0 - 2.0 * x1 + x2;
11631154
let bx = 2.0 * (x1 - x0);
1155+
let t_extrema = -0.5 * b / a;
1156+
let is_monotonic = t_extrema <= 0.0 || t_extrema >= 1.0;
1157+
if a >= 0.0 {
1158+
// Downward opening parabola.
1159+
let y_min = if is_monotonic {
1160+
y0.min(y2)
1161+
} else {
1162+
a * t_extrema * t_extrema + b * t_extrema + c
1163+
};
11641164

1165-
let mut winding = 0;
1166-
for t in roots {
1167-
// Verify that this monotonic segment straddles the ray, and choose winding direction.
1168-
// This also handles the endpoint conventions.
1169-
let direction = if y_end > y_start {
1170-
// Downward edge: initial point included, increments winding.
1171-
if y_start > 0.0 || y_end <= 0.0 {
1172-
0
1173-
} else {
1174-
1
1165+
// First subcurve is moving upward, include initial point.
1166+
if is_t0_valid && y0 >= 0.0 && y_min < 0.0 {
1167+
// If curve point is to the right of the ray origin (x > 0), the ray will hit it.
1168+
// We don't have to check 0 <= t <= 1 check because we've already guaranteed that the subcurve
1169+
// straddles the ray.
1170+
let x = x0 + bx * t0 + ax * t0 * t0;
1171+
if x > 0.0 {
1172+
winding += 1;
11751173
}
1176-
} else if y_start > y_end {
1177-
// Upward edge: final point included, increments winding.
1178-
if y_start <= 0.0 || y_end > 0.0 {
1179-
0
1180-
} else {
1181-
-1
1174+
}
1175+
1176+
// Second subcurve is moving downard, include final point.
1177+
if is_t1_valid && y_min < 0.0 && y2 >= 0.0 {
1178+
let x = x0 + bx * t1 + ax * t1 * t1;
1179+
if x > 0.0 {
1180+
winding -= 1;
11821181
}
1182+
}
1183+
} else {
1184+
// Upward opening parabola.
1185+
let y_max = if is_monotonic {
1186+
y0.max(y2)
11831187
} else {
1184-
0
1188+
a * t_extrema * t_extrema + b * t_extrema + c
11851189
};
11861190

1187-
// If curve point is to the right of the ray origin, the ray will hit it.
1188-
// We don't have to do the problematic 0 <= t <= 1 check because this vertical slice is guaranteed
1189-
// to contain the monotonic segment, and our roots are returned in order by `solve_quadratic`.
1190-
// Adjust the winding as appropriate.
1191-
if direction != 0 {
1192-
let t_x = x0 + bx * t + ax * t * t;
1193-
if t_x > 0.0 {
1194-
winding += direction;
1191+
// First subcurve is moving downward, include extrema point.
1192+
if is_t1_valid && y0 < 0.0 && y_max >= 0.0 {
1193+
let x = x0 + bx * t1 + ax * t1 * t1;
1194+
if x > 0.0 {
1195+
winding -= 1;
11951196
}
11961197
}
11971198

1198-
// Advance to next monotonic segment
1199-
y_start = y_end;
1200-
y_end = y2;
1199+
// Second subcurve is moving upward, include extrema point.
1200+
if is_t0_valid && y_max >= 0.0 && y2 < 0.0 {
1201+
let x = x0 + bx * t0 + ax * t0 * t0;
1202+
if x > 0.0 {
1203+
winding += 1;
1204+
}
1205+
}
12011206
}
12021207

12031208
winding
@@ -1206,43 +1211,35 @@ fn winding_number_curve(
12061211
const COEFFICIENT_EPSILON: f64 = 0.0000001;
12071212

12081213
/// Returns the roots of the quadratic ax^2 + bx + c = 0.
1209-
/// The roots may not be unique.
1214+
/// The roots may not be unique. NAN is returned for invalid roots. The first root will be where
1215+
/// the curve is sloping upward, the second root will be where the curve is slopping downward.
12101216
/// Uses the "Citardauq" formula for numerical stability.
1211-
/// Originally from https://github.com/linebender/kurbo/blob/master/src/common.rs
12121217
/// See https://math.stackexchange.com/questions/866331
1213-
fn solve_quadratic(a: f64, b: f64, c: f64) -> SmallVec<[f64; 2]> {
1214-
let mut result = SmallVec::new();
1215-
let sc0 = c * a.recip();
1216-
let sc1 = b * a.recip();
1217-
if !sc0.is_finite() || !sc1.is_finite() {
1218-
// c2 is zero or very small, treat as linear eqn
1219-
let root = -c / b;
1220-
if root.is_finite() {
1221-
result.push(root);
1218+
fn solve_quadratic(a: f64, b: f64, c: f64) -> (f64, f64) {
1219+
if a.abs() <= COEFFICIENT_EPSILON {
1220+
// Nearly linear, solve as linear equation.
1221+
if b >= 0.0 {
1222+
return (f64::NAN, -c / b);
1223+
} else {
1224+
return (-c / b, f64::NAN);
12221225
}
1223-
return result;
12241226
}
1225-
let arg = sc1 * sc1 - 4.0 * sc0;
1226-
let root1 = if !arg.is_finite() {
1227-
// Likely, calculation of sc1 * sc1 overflowed. Find one root
1228-
// using sc1 x + x² = 0, other root as sc0 / root1.
1229-
-sc1
1230-
} else {
1231-
if arg < 0.0 {
1232-
return result;
1233-
}
1234-
-0.5 * (sc1 + arg.sqrt().copysign(sc1))
1235-
};
1236-
let root2 = sc0 / root1;
1237-
// Sort just to be friendly and make results deterministic.
1238-
if root2 > root1 {
1239-
result.push(root1);
1240-
result.push(root2);
1227+
let mut disc = b * b - 4.0 * a * c;
1228+
if disc < 0.0 {
1229+
return (f64::NAN, f64::NAN);
1230+
}
1231+
disc = disc.sqrt();
1232+
// Order the roots so that the first root is where the curve slopes upward,
1233+
// and the second root is where the root slopes downward.
1234+
if b >= 0.0 {
1235+
let root0 = (-b - disc) / (2.0 * a);
1236+
let root1 = c / (a * root0);
1237+
(root0, root1)
12411238
} else {
1242-
result.push(root2);
1243-
result.push(root1);
1239+
let root0 = (-b + disc) / (2.0 * a);
1240+
let root1 = c / (a * root0);
1241+
(root1, root0)
12441242
}
1245-
result
12461243
}
12471244

12481245
/// Returns the roots of a cubic polynomial, ax^3 + bx^2 + cx + d = 0
@@ -1255,7 +1252,8 @@ fn solve_cubic(a: f64, b: f64, c: f64, d: f64) -> SmallVec<[f64; 3]> {
12551252

12561253
if a.abs() <= COEFFICIENT_EPSILON {
12571254
// Fall back to quadratic formula.
1258-
roots.extend_from_slice(&solve_quadratic(b, c, d));
1255+
let (t0, t1) = solve_quadratic(b, c, d);
1256+
roots.extend_from_slice(&[t0, t1]);
12591257
return roots;
12601258
}
12611259

tests/tests/regression_tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -721,7 +721,7 @@ swf_tests! {
721721
(mouse_events, "avm1/mouse_events", 8),
722722
(movieclip_depth_methods, "avm1/movieclip_depth_methods", 3),
723723
(movieclip_get_instance_at_depth, "avm1/movieclip_get_instance_at_depth", 1),
724-
(movieclip_hittest_shapeflag, "avm1/movieclip_hittest_shapeflag", 10),
724+
(movieclip_hittest_shapeflag, "avm1/movieclip_hittest_shapeflag", 11),
725725
(movieclip_hittest, "avm1/movieclip_hittest", 1),
726726
(movieclip_init_object, "avm1/movieclip_init_object", 1),
727727
(movieclip_lockroot, "avm1/movieclip_lockroot", 10),

tests/tests/swfs/avm1/movieclip_hittest_shapeflag/output.txt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,3 +244,34 @@ true
244244
/// inside the clipping layer itself (false)
245245
// _level0.clip.mc_inner_clipped.hitTest(224, 200, true)
246246
false
247+
248+
// wonderputt (#7684)
249+
// _level0.clip.hitTest(151.85, 635.1, true)
250+
false
251+
// _level0.clip.hitTest(163.25, 648.9, true)
252+
false
253+
// _level0.clip.hitTest(162.55, 648.9, true)
254+
false
255+
// _level0.clip.hitTest(150.5, 648.9, true)
256+
false
257+
// _level0.clip.hitTest(165.4, 648.9, true)
258+
false
259+
// _level0.clip.hitTest(164.7, 648.9, true)
260+
false
261+
// _level0.clip.hitTest(164, 648.9, true)
262+
false
263+
// _level0.clip.hitTest(163.3, 648.9, true)
264+
false
265+
// _level0.clip.hitTest(162.6, 648.9, true)
266+
false
267+
// _level0.clip.hitTest(161.9, 648.9, true)
268+
false
269+
// _level0.clip.hitTest(161.2, 648.9, true)
270+
false
271+
// _level0.clip.hitTest(160.5, 648.9, true)
272+
false
273+
// _level0.clip.hitTest(159.75, 648.9, true)
274+
false
275+
// _level0.clip.hitTest(159.25, 648.9, true)
276+
false
277+
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)