sort hit shapes when draw order changes and stop propagation on hit s…

sort hit shapes when draw order changes and stop propagation on hit success

Diffs=
8bca56dca sort hit shapes when draw order changes and stop propagation on hit s… (#6624)
9d605a1fe Updating harfbuzz to 8.3.0 (#6652)
5cb42a9b0 Unity compute bounds (#6649)
b765280df Fix path for downstream runtime. (#6645)
1cf6a65f1 Fix downstream cpp tests (#6643)
a35883508 Single test script for windows and mac. (#6642)
37ce9aaea Fix tests to use harfbuzz renames. (#6641)
9338d6ec6 make a change to force a mono flush (#6638)
6059f744d update mono to keep details in commit and not pr (#6637)
a394393a0 update mono scripts to be able to create "fixing" pr (#6636)

Co-authored-by: hernan <hernan@rive.app>
This commit is contained in:
bodymovin
2024-02-21 14:40:02 +00:00
parent 5ee2d85279
commit 870234fddc
8 changed files with 280 additions and 158 deletions

View File

@ -1 +1 @@
5e834adc22de1f8b1b445d79019b9b285a964913
8bca56dcaffd0f563a91f628b0ed432eca71acb5

View File

@ -6,6 +6,7 @@ import 'package:rive/src/rive_core/animation/nested_bool.dart';
import 'package:rive/src/rive_core/animation/nested_input.dart';
import 'package:rive/src/rive_core/animation/nested_number.dart';
import 'package:rive/src/rive_core/nested_artboard.dart';
import 'package:rive/src/rive_core/state_machine_controller.dart';
import 'package:rive_common/math.dart';
export 'package:rive/src/generated/animation/nested_state_machine_base.dart';
@ -18,11 +19,13 @@ abstract class NestedStateMachineInstance {
bool hitTest(Vec2D position);
void pointerMove(Vec2D position);
HitResult pointerMove(Vec2D position);
void pointerDown(Vec2D position, PointerDownEvent event);
HitResult pointerDown(Vec2D position, PointerDownEvent event);
void pointerUp(Vec2D position);
HitResult pointerUp(Vec2D position);
HitResult pointerExit(Vec2D position);
dynamic getInputValue(int id);
void setInputValue(int id, dynamic value);
@ -79,13 +82,17 @@ class NestedStateMachine extends NestedStateMachineBase {
bool hitTest(Vec2D position) =>
_stateMachineInstance?.hitTest(position) ?? false;
void pointerMove(Vec2D position) =>
_stateMachineInstance?.pointerMove(position);
HitResult pointerMove(Vec2D position) =>
_stateMachineInstance?.pointerMove(position) ?? HitResult.none;
void pointerDown(Vec2D position, PointerDownEvent event) =>
_stateMachineInstance?.pointerDown(position, event);
HitResult pointerDown(Vec2D position, PointerDownEvent event) =>
_stateMachineInstance?.pointerDown(position, event) ?? HitResult.none;
void pointerUp(Vec2D position) => _stateMachineInstance?.pointerUp(position);
HitResult pointerUp(Vec2D position) =>
_stateMachineInstance?.pointerUp(position) ?? HitResult.none;
HitResult pointerExit(Vec2D position) =>
_stateMachineInstance?.pointerExit(position) ?? HitResult.none;
void _isActiveChanged() {
// When a nested state machine re-activates (usually when an input changes)

View File

@ -44,6 +44,7 @@ class Artboard extends ArtboardBase with ShapePaintContainer {
}
bool _frameOrigin = true;
bool hasChangedDrawOrderInLastUpdate = false;
/// Returns true when the artboard will shift the origin from the top left to
/// the relative width/height of the artboard itself. This is what the editor
@ -238,6 +239,7 @@ class Artboard extends ArtboardBase with ShapePaintContainer {
didUpdate = true;
}
}
hasChangedDrawOrderInLastUpdate = false;
// Joysticks can be applied before updating components if none of the
// joysticks have "external" control. If they are controlled/moved by some
@ -443,7 +445,7 @@ class Artboard extends ArtboardBase with ShapePaintContainer {
fill.draw(canvas, path);
}
for (var drawable = _firstDrawable;
for (var drawable = firstDrawable;
drawable != null;
drawable = drawable.prev) {
if (drawable.isHidden || drawable.renderOpacity == 0) {
@ -551,7 +553,7 @@ class Artboard extends ArtboardBase with ShapePaintContainer {
@override
Vec2D get worldTranslation => Vec2D();
Drawable? _firstDrawable;
Drawable? firstDrawable;
void computeDrawOrder() {
_drawables.clear();
@ -589,12 +591,13 @@ class Artboard extends ArtboardBase with ShapePaintContainer {
}
void sortDrawOrder() {
hasChangedDrawOrderInLastUpdate = true;
// Clear out rule first/last items.
for (final rule in _sortedDrawRules) {
rule.first = rule.last = null;
}
_firstDrawable = null;
firstDrawable = null;
Drawable? lastDrawable;
for (final drawable in _drawables) {
var rules = drawable.flattenedDrawRules;
@ -614,7 +617,7 @@ class Artboard extends ArtboardBase with ShapePaintContainer {
drawable.prev = lastDrawable;
drawable.next = null;
if (lastDrawable == null) {
lastDrawable = _firstDrawable = drawable;
lastDrawable = firstDrawable = drawable;
} else {
lastDrawable.next = drawable;
lastDrawable = drawable;
@ -632,8 +635,8 @@ class Artboard extends ArtboardBase with ShapePaintContainer {
rule.drawable!.prev?.next = rule.first;
rule.first?.prev = rule.drawable!.prev;
}
if (rule.drawable == _firstDrawable) {
_firstDrawable = rule.first;
if (rule.drawable == firstDrawable) {
firstDrawable = rule.first;
}
rule.drawable?.prev = rule.last;
rule.last?.next = rule.drawable;
@ -652,7 +655,7 @@ class Artboard extends ArtboardBase with ShapePaintContainer {
}
}
_firstDrawable = lastDrawable;
firstDrawable = lastDrawable;
}
// Make an instance of the artboard, clones internal objects and properties.

View File

@ -9,4 +9,8 @@ class ComponentFlags {
/// Whether this Component is disconnected from the hierarchy meaning it won't
/// receive any update cycles nor will any drawables draw.
static const int disconnected = 1 << 2;
/// Whether this Component lets hit events pass through to components behind
/// it (used by shapes at runtine)
static const int opaque = 1 << 3;
}

View File

@ -40,6 +40,9 @@ abstract class Drawable extends DrawableBase {
@override
void blendModeValueChanged(int from, int to) {}
@override
void isTargetOpaqueChanged(bool from, bool to) {}
List<ClippingShape> _clippingShapes = [];
bool clip(Canvas canvas) {
@ -88,4 +91,8 @@ abstract class Drawable extends DrawableBase {
bool get isHidden =>
(drawableFlags & ComponentFlags.hidden) != 0 ||
(dirt & ComponentDirt.collapsed) != 0;
bool get isTargetOpaque {
return (drawableFlags & ComponentFlags.opaque) != 0;
}
}

View File

@ -2,6 +2,7 @@ library rive_core;
import 'dart:collection';
import 'package:collection/collection.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/scheduler.dart';
import 'package:rive/src/core/core.dart';
@ -22,6 +23,8 @@ import 'package:rive/src/rive_core/animation/state_machine_listener.dart';
import 'package:rive/src/rive_core/animation/state_machine_trigger.dart';
import 'package:rive/src/rive_core/animation/state_transition.dart';
import 'package:rive/src/rive_core/artboard.dart';
import 'package:rive/src/rive_core/component.dart';
import 'package:rive/src/rive_core/drawable.dart';
import 'package:rive/src/rive_core/event.dart';
import 'package:rive/src/rive_core/nested_artboard.dart';
import 'package:rive/src/rive_core/node.dart';
@ -363,8 +366,7 @@ class StateMachineController extends RiveAnimationController<CoreContext>
onStateChange?.call(stateMachine.name, stateName);
});
late List<_HitShape> hitShapes;
late List<NestedArtboard> hitNestedArtboards;
late List<_HitComponent> hitComponents = [];
Artboard? _artboard;
@ -407,7 +409,7 @@ class StateMachineController extends RiveAnimationController<CoreContext>
if (component is Shape) {
var hitShape = hitShapeLookup[component];
if (hitShape == null) {
hitShapeLookup[component] = hitShape = _HitShape(component);
hitShapeLookup[component] = hitShape = _HitShape(component, this);
}
hitShape.events.add(event);
}
@ -416,19 +418,18 @@ class StateMachineController extends RiveAnimationController<CoreContext>
});
}
}
hitShapes = hitShapeLookup.values.toList();
hitShapeLookup.values.toList().forEach(hitComponents.add);
_artboard = core as RuntimeArtboard;
List<NestedArtboard> nestedArtboards = [];
if (_artboard != null) {
for (final nestedArtboard in _artboard!.activeNestedArtboards) {
if (nestedArtboard.hasNestedStateMachine) {
nestedArtboards.add(nestedArtboard);
hitComponents.add(_HitNestedArtboard(nestedArtboard, this));
}
}
}
hitNestedArtboards = nestedArtboards;
_sortHittableComponents();
return super.init(core);
}
@ -457,8 +458,40 @@ class StateMachineController extends RiveAnimationController<CoreContext>
}
}
void _sortHittableComponents() {
Drawable? firstDrawable = artboard?.firstDrawable;
if (firstDrawable != null) {
// walk to the end, so we can visit in reverse-order
while (firstDrawable!.prev != null) {
firstDrawable = firstDrawable.prev;
}
int hitComponentsCount = hitComponents.length;
int currentSortedIndex = 0;
while (firstDrawable != null) {
for (var i = currentSortedIndex; i < hitComponentsCount; i++) {
if (hitComponents.elementAt(i).component == firstDrawable) {
if (currentSortedIndex != i) {
hitComponents.swap(i, currentSortedIndex);
}
currentSortedIndex++;
break;
}
}
if (currentSortedIndex == hitComponentsCount) {
break;
}
firstDrawable = firstDrawable.next;
}
}
}
@override
void apply(CoreContext core, double elapsedSeconds) {
if (artboard?.hasChangedDrawOrderInLastUpdate ?? false) {
_sortHittableComponents();
}
bool keepGoing = false;
for (final layerController in layerControllers) {
if (layerController.apply(core, elapsedSeconds)) {
@ -513,14 +546,14 @@ class StateMachineController extends RiveAnimationController<CoreContext>
}
}
bool _processEvent(
HitResult _processEvent(
Vec2D position, {
PointerEvent? pointerEvent,
ListenerType? hitEvent,
}) {
var artboard = this.artboard;
if (artboard == null) {
return false;
return HitResult.none;
}
if (artboard.frameOrigin) {
// ignore: parameter_assignments
@ -530,89 +563,25 @@ class StateMachineController extends RiveAnimationController<CoreContext>
artboard.height * artboard.originY,
);
}
const hitRadius = 2;
var hitArea = IAABB(
(position.x - hitRadius).round(),
(position.y - hitRadius).round(),
(position.x + hitRadius).round(),
(position.y + hitRadius).round(),
);
bool hitSomething = false;
for (final hitShape in hitShapes) {
// for (final hitShape in event.shapes) {
var shape = hitShape.shape;
var bounds = shape.worldBounds;
// Quick reject
bool isOver = false;
if (bounds.contains(position)) {
// Make hit tester.
var hitTester = TransformingHitTester(hitArea);
shape.fillHitTester(hitTester);
// TODO: figure out where we get the fill rule. We could get it from
// the Shape's first fill or do we want to store it on the event as a
// user-selectable value in the inspector?
// Just use bounds for now
isOver = hitTester.test();
if (isOver) {
hitSomething = true;
}
}
bool hoverChange = hitShape.isHovered != isOver;
hitShape.isHovered = isOver;
// iterate all events associated with this hit shape
for (final event in hitShape.events) {
// Always update hover states regardless of which specific event type
// we're trying to trigger.
if (hoverChange) {
if (isOver && event.listenerType == ListenerType.enter) {
event.performChanges(this, position);
isActive = true;
} else if (!isOver && event.listenerType == ListenerType.exit) {
event.performChanges(this, position);
isActive = true;
}
}
if (isOver && hitEvent == event.listenerType) {
event.performChanges(this, position);
isActive = true;
bool hitOpaque = false;
HitResult hitResult = HitResult.none;
for (final hitComponent in hitComponents) {
hitResult = hitComponent.processEvent(position,
hitEvent: hitEvent, pointerEvent: pointerEvent, canHit: !hitOpaque);
if (hitResult != HitResult.none) {
hitSomething = true;
if (hitResult == HitResult.hitOpaque) {
hitOpaque = true;
}
}
}
for (final nestedArtboard in hitNestedArtboards) {
if (nestedArtboard.isCollapsed) {
continue;
}
var nestedPosition = nestedArtboard.worldToLocal(position);
if (nestedPosition == null) {
// Mounted artboard isn't ready or has a 0 scale transform.
continue;
}
for (final nestedStateMachine
in nestedArtboard.animations.whereType<NestedStateMachine>()) {
switch (hitEvent) {
case ListenerType.down:
nestedStateMachine.pointerDown(
nestedPosition,
pointerEvent as PointerDownEvent,
);
break;
case ListenerType.up:
nestedStateMachine.pointerUp(nestedPosition);
break;
default:
nestedStateMachine.pointerMove(nestedPosition);
break;
}
}
}
return hitSomething;
return hitSomething
? hitOpaque
? HitResult.hitOpaque
: HitResult.hit
: HitResult.none;
}
/// Hit testing. If any listeners were hit, returns true.
@ -633,78 +602,44 @@ class StateMachineController extends RiveAnimationController<CoreContext>
artboard.height * artboard.originY,
);
}
const hitRadius = 2;
var hitArea = IAABB(
(position.x - hitRadius).round(),
(position.y - hitRadius).round(),
(position.x + hitRadius).round(),
(position.y + hitRadius).round(),
);
for (final hitShape in hitShapes) {
var shape = hitShape.shape;
var bounds = shape.worldBounds;
// Quick reject
bool isOver = false;
if (bounds.contains(position)) {
// Make hit tester.
var hitTester = TransformingHitTester(hitArea);
shape.fillHitTester(hitTester);
isOver = hitTester.test();
if (isOver) {
return true; // exit early
}
}
}
for (final nestedArtboard in hitNestedArtboards) {
if (nestedArtboard.isCollapsed) {
continue;
}
var nestedPosition = nestedArtboard.worldToLocal(position);
if (nestedPosition == null) {
// Mounted artboard isn't ready or has a 0 scale transform.
continue;
}
for (final nestedStateMachine
in nestedArtboard.animations.whereType<NestedStateMachine>()) {
if (nestedStateMachine.hitTest(nestedPosition)) {
return true; // exit early
}
for (final hitComponent in hitComponents) {
if (hitComponent.hitTest(position)) {
return true;
}
}
return false; // no hit targets found
}
void pointerMove(Vec2D position) => _processEvent(
HitResult pointerMove(Vec2D position) => _processEvent(
position,
hitEvent: ListenerType.move,
);
void pointerDown(Vec2D position, PointerDownEvent event) {
if (_processEvent(
HitResult pointerDown(Vec2D position, PointerDownEvent event) {
final hitResult = _processEvent(
position,
hitEvent: ListenerType.down,
pointerEvent: event,
)) {
);
if (hitResult != HitResult.none) {
_recognizer.addPointer(event);
}
return hitResult;
}
void pointerUp(Vec2D position) => _processEvent(
HitResult pointerUp(Vec2D position) => _processEvent(
position,
hitEvent: ListenerType.up,
);
void pointerExit(Vec2D position) => _processEvent(
HitResult pointerExit(Vec2D position) => _processEvent(
position,
hitEvent: ListenerType.exit,
);
void pointerEnter(Vec2D position) => _processEvent(
HitResult pointerEnter(Vec2D position) => _processEvent(
position,
hitEvent: ListenerType.enter,
);
@ -725,14 +660,170 @@ class StateMachineController extends RiveAnimationController<CoreContext>
}
}
enum HitResult {
none,
hit,
hitOpaque,
}
class _HitComponent {
final Component component;
final StateMachineController controller;
HitResult processEvent(
Vec2D position, {
PointerEvent? pointerEvent,
ListenerType? hitEvent,
bool canHit = true,
}) {
return HitResult.none;
}
bool hitTest(Vec2D position) {
return false;
}
_HitComponent(this.component, this.controller);
}
/// Representation of a Shape from the Artboard Instance and all the events it
/// triggers. Allows tracking hover and performing hit detection only once on
/// shapes that trigger multiple events.
class _HitShape {
Shape shape;
class _HitShape extends _HitComponent {
final Shape shape;
double hitRadius = 2;
bool isHovered = false;
List<StateMachineListener> events = [];
_HitShape(this.shape);
_HitShape(this.shape, StateMachineController controller)
: super(shape, controller);
@override
bool hitTest(Vec2D position) {
var shape = component as Shape;
var bounds = shape.worldBounds;
// Quick reject
if (bounds.contains(position)) {
var hitArea = IAABB(
(position.x - hitRadius).round(),
(position.y - hitRadius).round(),
(position.x + hitRadius).round(),
(position.y + hitRadius).round(),
);
// Make hit tester.
var hitTester = TransformingHitTester(hitArea);
shape.fillHitTester(hitTester);
return hitTester.test(); // exit early
}
return false;
}
@override
HitResult processEvent(
Vec2D position, {
PointerEvent? pointerEvent,
ListenerType? hitEvent,
bool canHit = true,
}) {
var shape = component as Shape;
var isOver = false;
if (canHit) {
isOver = hitTest(position);
}
////
bool hoverChange = isHovered != isOver;
isHovered = isOver;
// iterate all events associated with this hit shape
for (final event in events) {
// Always update hover states regardless of which specific event type
// we're trying to trigger.
if (hoverChange) {
if (isOver && event.listenerType == ListenerType.enter) {
event.performChanges(controller, position);
controller.isActive = true;
} else if (!isOver && event.listenerType == ListenerType.exit) {
event.performChanges(controller, position);
controller.isActive = true;
}
}
if (isOver && hitEvent == event.listenerType) {
event.performChanges(controller, position);
controller.isActive = true;
}
}
////
return isOver
? shape.isTargetOpaque
? HitResult.hitOpaque
: HitResult.hit
: HitResult.none;
}
}
class _HitNestedArtboard extends _HitComponent {
final NestedArtboard nestedArtboard;
_HitNestedArtboard(this.nestedArtboard, StateMachineController controller)
: super(nestedArtboard, controller);
@override
bool hitTest(Vec2D position) {
var nestedPosition = nestedArtboard.worldToLocal(position);
if (nestedArtboard.isCollapsed) {
return false;
}
if (nestedPosition == null) {
// Mounted artboard isn't ready or has a 0 scale transform.
return false;
}
for (final nestedStateMachine
in nestedArtboard.animations.whereType<NestedStateMachine>()) {
if (nestedStateMachine.hitTest(nestedPosition)) {
return true; // exit early
}
}
return false;
}
@override
HitResult processEvent(
Vec2D position, {
PointerEvent? pointerEvent,
ListenerType? hitEvent,
bool canHit = true,
}) {
HitResult hitResult = HitResult.none;
if (nestedArtboard.isCollapsed) {
return hitResult;
}
var nestedPosition = nestedArtboard.worldToLocal(position);
if (nestedPosition == null) {
// Mounted artboard isn't ready or has a 0 scale transform.
return hitResult;
}
for (final nestedStateMachine
in nestedArtboard.animations.whereType<NestedStateMachine>()) {
if (canHit) {
switch (hitEvent) {
case ListenerType.down:
hitResult = nestedStateMachine.pointerDown(
nestedPosition,
pointerEvent as PointerDownEvent,
);
break;
case ListenerType.up:
hitResult = nestedStateMachine.pointerUp(nestedPosition);
break;
default:
hitResult = nestedStateMachine.pointerMove(nestedPosition);
break;
}
} else {
nestedStateMachine.pointerExit(nestedPosition);
}
}
return hitResult;
}
}
/// This allows a value of type T or T?

View File

@ -6,6 +6,8 @@ import 'package:rive/src/rive_core/animation/nested_linear_animation.dart';
import 'package:rive/src/rive_core/animation/nested_state_machine.dart';
import 'package:rive/src/rive_core/artboard.dart';
import 'package:rive/src/rive_core/nested_artboard.dart';
import 'package:rive/src/rive_core/state_machine_controller.dart'
as state_machine_core;
import 'package:rive/src/runtime_mounted_artboard.dart';
import 'package:rive_common/math.dart';
@ -123,15 +125,24 @@ class RuntimeNestedStateMachineInstance extends NestedStateMachineInstance {
bool hitTest(Vec2D position) => stateMachineController.hitTest(position);
@override
void pointerDown(Vec2D position, PointerDownEvent event) =>
stateMachineController.pointerDown(position, event);
state_machine_core.HitResult pointerDown(
Vec2D position, PointerDownEvent event) {
final result = stateMachineController.pointerDown(position, event);
return result;
}
@override
void pointerMove(Vec2D position) =>
state_machine_core.HitResult pointerMove(Vec2D position) =>
stateMachineController.pointerMove(position);
@override
void pointerUp(Vec2D position) => stateMachineController.pointerUp(position);
state_machine_core.HitResult pointerUp(Vec2D position) =>
stateMachineController.pointerUp(position);
@override
state_machine_core.HitResult pointerExit(Vec2D position) =>
stateMachineController.pointerExit(position);
@override
dynamic getInputValue(int id) => stateMachineController.getInputValue(id);

View File

@ -320,8 +320,7 @@ class RiveAnimationState extends State<RiveAnimation> {
bool get _shouldAddHitTesting => _artboard!.animationControllers.any(
(controller) =>
controller is StateMachineController &&
(controller.hitShapes.isNotEmpty ||
controller.hitNestedArtboards.isNotEmpty),
controller.hitComponents.isNotEmpty,
);
@override