-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
When dragging any soccer ball in the stack, it should really drag the top ball #125
Comments
I see a comment in the code that says:
So it seems likely that the last ball landed at the bottom of the stack, and the other balls moved above it. @Nancy-Salpepi would this explain the behavior you reported? @amanda-phet I see how this could be confusing for users. How should we proceed for the milestone and for 1.1? |
Yes @samreid, it explains the behavior I reported. I do think it can be confusing though. |
@chrisklus and I noticed another implication of having balls land at the "bottom" of the stack when I was testing out a PhET-iO example. (If I make the first 14 balls disabled and the 15th enabled, the 15th ball gets kicked and shows up at the bottom of the stack and is very obvious because of its appearance.) We will either need to accept this weirdness, or think of another way to animate the balls landing. |
Should we change the rule so the ball that just landed is the only one that animates to the top, since that's what it looks like? |
I agree it might be confusing to see a different card move when it's seemingly tracking itself to a specific ball. Here's a video where I put a ball on a stack and you can see which card is moving. As long as I didn't let go of my mouse, the same card changed. But as soon as I let go of the mouse button and went back to the same stack, a different card moved. I expected the same card to move, but it didn't. Screen.Recording.2023-04-24.at.11.10.14.AM.movIs there a way to attach a ball to its card to they're connected (so the same card moves when the same ball is moved)? |
In a stack of 3 soccer balls, A,B,C (with soccer ball A at the top), if you click and drag soccer ball B, what is the expected behavior? The user drags ball B and A falls? (it looks like that is happening in the video above) Or ball A drags? Will we need to consider keyboard accessibility #162 to decide this? |
In today's design meeting, we agreed that the top ball is always the one that is dragged from the stack. It makes no sense to drag a ball out of the center or bottom of the stack. This also makes sense for a11y--when we implement tab navigation in #162 we only want to visit/drag the topmost soccer balls. Fixing this may also solve #82 |
Thoughts on how to fix this: Option 1: Only the top ball on each stack has an active drag listener, and the size of the hit box stretches to overlap the other balls under it in a stack. Option 2: CAVSceneModel has a single drag listener, which is triggered by a press event on each soccer ball. The scene model handles the drag event and connects it to the top ball in the clicked stack. Option 3: ? |
@samreid and I discussed this, and agree to try a solution close to Option 2 above, where the 'down' event from any soccer balls lower in the stack is forwarded to the top ball in the stack. |
I wrote in slack:
|
@catherinecarter replied:
I said:
@catherinecarter replied:
I said:
@catherinecarter said:
|
@amanda-phet said:
|
@catherinecarter said:
|
Patch from work @samreid and I did today on trying to get the top ball to be the only one in each stack that is pickable Subject: [PATCH] https://github.com/phetsims/center-and-variability/issues/125
---
Index: js/common/view/CAVObjectNode.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/common/view/CAVObjectNode.ts b/js/common/view/CAVObjectNode.ts
--- a/js/common/view/CAVObjectNode.ts (revision f5f4e9299e516c82db7d044f9f2a8c72db3d017e)
+++ b/js/common/view/CAVObjectNode.ts (date 1683313273176)
@@ -33,7 +33,7 @@
protected readonly medianHighlight: Circle;
- public constructor( soccerBall: SoccerBall, isShowingPlayAreaMedianProperty: TReadOnlyProperty<boolean>,
+ public constructor( public readonly soccerBall: SoccerBall, isShowingPlayAreaMedianProperty: TReadOnlyProperty<boolean>,
modelViewTransform: ModelViewTransform2,
modelRadius: number,
providedOptions?: CAVObjectNodeOptions ) {
Index: js/common/view/SceneView.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/common/view/SceneView.ts b/js/common/view/SceneView.ts
--- a/js/common/view/SceneView.ts (revision f5f4e9299e516c82db7d044f9f2a8c72db3d017e)
+++ b/js/common/view/SceneView.ts (date 1683315823894)
@@ -18,6 +18,7 @@
import VariabilitySceneModel from '../../variability/model/VariabilitySceneModel.js';
import VariabilityModel from '../../variability/model/VariabilityModel.js';
import Multilink from '../../../../axon/js/Multilink.js';
+import SoccerBall from '../model/SoccerBall.js';
export default class SceneView {
@@ -77,7 +78,9 @@
model.soccerBallHasBeenDraggedProperty.link( updateDragIndictatorVisible );
- sceneModel.soccerBalls.forEach( ( soccerBall, index ) => {
+ const soccerBallMap = new Map<SoccerBall, SoccerBallNode>();
+
+ const soccerBallNodes = sceneModel.soccerBalls.map( ( soccerBall, index ) => {
const soccerBallNode = new SoccerBallNode(
soccerBall,
sceneModel.isVisibleProperty,
@@ -112,6 +115,29 @@
model.soccerBallHasBeenDraggedProperty.value = true;
}
} );
+
+ soccerBallMap.set( soccerBall, soccerBallNode );
+
+ return soccerBallNode;
+ } );
+
+ soccerBallNodes.forEach( soccerBallNode => {
+ soccerBallNode.soccerBall.valueProperty.lazyLink( ( value, oldValue ) => {
+
+ soccerBallNodes.forEach( soccerBallNode => {
+ soccerBallNode.pickable = false;
+ } );
+
+ if ( oldValue !== null ) {
+ const oldStack = sceneModel.getStackAtLocation( oldValue );
+ soccerBallMap.get( oldStack[ oldStack.length - 1 ] )!.pickable = true;
+ }
+
+ if ( value !== null ) {
+ const newStack = sceneModel.getStackAtLocation( value );
+ soccerBallMap.get( newStack[ newStack.length - 1] )!.pickable = true;
+ }
+ } );
} );
const playAreaMedianIndicatorNode = new PlayAreaMedianIndicatorNode();
Index: js/common/model/CAVSceneModel.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/common/model/CAVSceneModel.ts b/js/common/model/CAVSceneModel.ts
--- a/js/common/model/CAVSceneModel.ts (revision f5f4e9299e516c82db7d044f9f2a8c72db3d017e)
+++ b/js/common/model/CAVSceneModel.ts (date 1683315787092)
@@ -242,6 +242,16 @@
} );
}
+ /**
+ * Returns all objects at the target location
+ */
+ public getStackAtLocation( location: number ): SoccerBall[] {
+ return this.getActiveSoccerBalls().filter( ( o: SoccerBall ) => {
+ return o.valueProperty.value === location;
+ } ).sort( ( objectA, objectB ) =>
+ objectA.positionProperty.value.y - objectB.positionProperty.value.y );
+ }
+
/**
* Set the position of the parameter object to be on top of the other objects at that target position.
*/
@@ -292,7 +302,7 @@
public getSortedLandedObjects(): SoccerBall[] {
return _.sortBy( this.getActiveSoccerBalls().filter( soccerBall => soccerBall.valueProperty.value !== null ),
- // The numerical value takes predence for sorting
+ // The numerical value takes precedence for sorting
soccerBall => soccerBall.valueProperty.value,
// Then consider the height within the stack |
@amanda-phet and @catherinecarter don't worry about the preceding comment. Just a stepping stone to get toward the functionality you described. |
Here is some more progress in that direction, still not working well though: Subject: [PATCH] Add TODO reference, see https://github.com/phetsims/center-and-variability/issues/45
---
Index: js/common/model/SoccerBall.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/common/model/SoccerBall.ts b/js/common/model/SoccerBall.ts
--- a/js/common/model/SoccerBall.ts (revision 96e0542851d4706e1bb33dd31f6e56fb2f6d9d54)
+++ b/js/common/model/SoccerBall.ts (date 1683317599748)
@@ -37,6 +37,8 @@
& PhetioObjectOptions
& PickRequired<PhetioObjectOptions, 'tandem'>;
+let count = 0;
+
export default class SoccerBall {
// Continuous value for the drag listener. When dragging, the object snaps to each tickmark
@@ -63,6 +65,9 @@
public readonly isActiveProperty: BooleanProperty;
public soccerPlayer: SoccerPlayer | null = null;
+ // Global index for debugging
+ public readonly index = count++;
+
public constructor( providedOptions: CAVObjectOptions ) {
const options = optionize<CAVObjectOptions, SelfOptions, PhetioObjectOptions>()( {
Index: js/common/view/SceneView.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/common/view/SceneView.ts b/js/common/view/SceneView.ts
--- a/js/common/view/SceneView.ts (revision 96e0542851d4706e1bb33dd31f6e56fb2f6d9d54)
+++ b/js/common/view/SceneView.ts (date 1683318219080)
@@ -18,6 +18,9 @@
import VariabilitySceneModel from '../../variability/model/VariabilitySceneModel.js';
import VariabilityModel from '../../variability/model/VariabilityModel.js';
import Multilink from '../../../../axon/js/Multilink.js';
+import SoccerBall from '../model/SoccerBall.js';
+import { Shape } from '../../../../kite/js/imports.js';
+import Bounds2 from '../../../../dot/js/Bounds2.js';
export default class SceneView {
@@ -77,7 +80,9 @@
model.soccerBallHasBeenDraggedProperty.link( updateDragIndictatorVisible );
- sceneModel.soccerBalls.forEach( ( soccerBall, index ) => {
+ const soccerBallMap = new Map<SoccerBall, SoccerBallNode>();
+
+ const soccerBallNodes = sceneModel.soccerBalls.map( ( soccerBall, index ) => {
const soccerBallNode = new SoccerBallNode(
soccerBall,
sceneModel.isVisibleProperty,
@@ -112,6 +117,51 @@
model.soccerBallHasBeenDraggedProperty.value = true;
}
} );
+
+ soccerBallMap.set( soccerBall, soccerBallNode );
+
+ return soccerBallNode;
+ } );
+
+ const updateStackPickability = ( stack: SoccerBall[] ) => {
+ // TODO: https://github.com/phetsims/center-and-variability/issues/125 remove debug code
+ // console.log( 'visiting stack at: ' + stack.map( soccerBall => soccerBall.valueProperty.value ) );
+ // console.log( 'stack = ' + stack.map( soccerBall => soccerBall.index ) );
+
+ let bounds: Bounds2 | null = null;
+
+ for ( let i = 0; i < stack.length; i++ ) {
+
+ const soccerBallNode = soccerBallMap.get( stack[ i ] )!;
+ soccerBallNode.pickable = i === stack.length - 1;
+
+ if ( i === 0 ) {
+ bounds = soccerBallNode.globalBounds;
+ }
+ else {
+ bounds!.includeBounds( soccerBallNode.globalBounds );
+ }
+
+ if ( i === stack.length - 1 ) {
+ soccerBallNode.mouseArea = Shape.bounds( soccerBallNode.globalToLocalBounds( bounds! ) );
+ soccerBallNode.touchArea = Shape.bounds( soccerBallNode.globalToLocalBounds( bounds! ) );
+ }
+ else {
+ soccerBallNode.mouseArea = Shape.rectangle( 0, 0, 0, 0 );
+ soccerBallNode.touchArea = Shape.rectangle( 0, 0, 0, 0 );
+ }
+ }
+ };
+
+ soccerBallNodes.forEach( soccerBallNode => {
+ soccerBallNode.soccerBall.valueProperty.lazyLink( () => {
+
+ setTimeout( () => {
+ for ( let i = 0; i <= 15; i++ ) {
+ updateStackPickability( sceneModel.getStackAtLocation( i ) );
+ }
+ }, 0 );
+ } );
} );
const playAreaMedianIndicatorNode = new PlayAreaMedianIndicatorNode();
Index: js/common/model/CAVSceneModel.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/common/model/CAVSceneModel.ts b/js/common/model/CAVSceneModel.ts
--- a/js/common/model/CAVSceneModel.ts (revision 96e0542851d4706e1bb33dd31f6e56fb2f6d9d54)
+++ b/js/common/model/CAVSceneModel.ts (date 1683316070732)
@@ -242,6 +242,16 @@
} );
}
+ /**
+ * Returns all objects at the target location
+ */
+ public getStackAtLocation( location: number ): SoccerBall[] {
+ return this.getActiveSoccerBalls().filter( ( o: SoccerBall ) => {
+ return o.valueProperty.value === location;
+ } ).sort( ( objectA, objectB ) =>
+ objectA.positionProperty.value.y - objectB.positionProperty.value.y );
+ }
+
/**
* Set the position of the parameter object to be on top of the other objects at that target position.
*/
@@ -292,7 +302,7 @@
public getSortedLandedObjects(): SoccerBall[] {
return _.sortBy( this.getActiveSoccerBalls().filter( soccerBall => soccerBall.valueProperty.value !== null ),
- // The numerical value takes predence for sorting
+ // The numerical value takes precedence for sorting
soccerBall => soccerBall.valueProperty.value,
// Then consider the height within the stack
Index: js/common/view/CAVObjectNode.ts
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/js/common/view/CAVObjectNode.ts b/js/common/view/CAVObjectNode.ts
--- a/js/common/view/CAVObjectNode.ts (revision 96e0542851d4706e1bb33dd31f6e56fb2f6d9d54)
+++ b/js/common/view/CAVObjectNode.ts (date 1683317543983)
@@ -26,14 +26,11 @@
& StrictOmit<NodeOptions, 'inputEnabledProperty'>
& PickRequired<NodeOptions, 'tandem'>;
-// for debugging with ?dev
-let index = 0;
-
export default abstract class CAVObjectNode extends Node {
protected readonly medianHighlight: Circle;
- public constructor( soccerBall: SoccerBall, isShowingPlayAreaMedianProperty: TReadOnlyProperty<boolean>,
+ public constructor( public readonly soccerBall: SoccerBall, isShowingPlayAreaMedianProperty: TReadOnlyProperty<boolean>,
modelViewTransform: ModelViewTransform2,
modelRadius: number,
providedOptions?: CAVObjectNodeOptions ) {
@@ -65,7 +62,7 @@
// Show index when debugging with ?dev
if ( phet.chipper.queryParameters.dev ) {
- this.addChild( new Text( index++ + '', {
+ this.addChild( new Text( soccerBall.index + '', {
font: new PhetFont( 14 ),
fill: 'red',
x: this.width / 2 + 1
|
Fixed and ready for review. @matthew-blackman can you please review since you are more familiar with the issue? And keep in mind #82 is still buggy independently of this issue. |
I tested the solution @samreid committed above and the behavior looks correct. Nice job! The median highlight and ball animations also appear to be working correctly. Looking through the above commits, the implementation seems to make sense. I have the following questions/feedback:
Since stackChangedEmitter is already causing updateStackPointerAreas to check each stack and update the pointer areas based on the ball y-position, I think this additional call to restackWithTopBall may be redundant. Seeing isSettingPhetioStateProperty is making me wonder if this is a PhET-iO consideration. Thoughts @samreid? |
Question for PhET-iO design meeting: If the position of a ball is changed via PhET-iO, do we want to move that ball to the top of the stack (the same as if it was dragged there)? |
@samreid I noticed some redundancy in how often functions like
It looks like the The above commit removes one extra call of |
@matthew-blackman and I made commits in #186 to reduce redundancy and improve performance. |
I also have a patch in-progress that makes the stackChangedEmitter take a |
@matthew-blackman and I worked out the stack variable in stackChangedEmitter as part of #175. @matthew-blackman can you please re-evaluate this issue and see what remains to do? |
Grabbing and moving the top soccer ball in each stack appears to be working well, and the code changes make sense to me. This seems ready to close. We can continue dealing with corner-cases in #188 |
Closing as completed. |
Test device
MacBook Air (m1 chip)
Operating System
12.3.1
Browser
Safari
Problem description
For phetsims/qa#795, when the last ball kicked lands on a number that is already occupied: If I move the "last ball" kicked, it doesn't change the value of the last card. Instead, it changes the value of first card that had that number. Shouldn't the last ball to get that value be the "top" ball?
Steps to reproduce
Visuals
CardBallmatch.mp4
The text was updated successfully, but these errors were encountered: