Skip to content

Commit

Permalink
Improve logic for interval tool center dragging, see #278
Browse files Browse the repository at this point in the history
  • Loading branch information
samreid committed Apr 2, 2024
1 parent aa7404f commit baec4f8
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 87 deletions.
52 changes: 4 additions & 48 deletions js/measures/model/IntervalTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,6 @@ export default class IntervalTool extends PhetioObject {
public readonly edge1Property: Property<number>;
public readonly edge2Property: Property<number>;

// This Property can be used to read and write the center of the interval tool. It is necessary for wiring up to the
// DragListener that translates the entire IntervalTool. This value may go out of the bounds of the field while dragging.
// This is preferable to updating and maintaining an allowed range that changes based on the width of the tool.
public readonly centerProperty: Property<number>;

public constructor( providedOptions: IntervalToolOptions ) {

const options = optionize<IntervalToolOptions, SelfOptions, PhetioObjectOptions>()( {
Expand All @@ -57,7 +52,8 @@ export default class IntervalTool extends PhetioObject {
units: 'm',
rangePropertyOptions: {
tandem: Tandem.OPT_OUT
}
},
// reentrant: true
} );

this.edge2Property = new NumberProperty( DEFAULT_EDGE_2, {
Expand All @@ -66,47 +62,8 @@ export default class IntervalTool extends PhetioObject {
units: 'm',
rangePropertyOptions: {
tandem: Tandem.OPT_OUT
}
} );

// This one is not PhET-iO instrumented, to avoid bounds and circularity issues. Clients should use edge1Property and edge2Property.
// This is a transient Property used to get drag events when dragging the center. It just passes through to the
// edges. The view is centered by averaging the edges.
// This Property is for input only, for use in the drag listener. Do not try to read the value, instead average the edges.
// This works because DragListener only sets value and does not read values for this property (do not use _useParentOffset
// in the node that drags based on this Property.)
this.centerProperty = new NumberProperty( ( DEFAULT_EDGE_1 + DEFAULT_EDGE_2 ) / 2 );

this.centerProperty.lazyLink( ( center, oldCenter ) => {

let delta = center - oldCenter;

let edge1 = this.edge1Property.value;
let edge2 = this.edge2Property.value;

if ( delta > 0 ) {

const max = Math.max( edge1, edge2 );
if ( max + delta > PDLConstants.MAX_FIELD_DISTANCE ) {
delta = PDLConstants.MAX_FIELD_DISTANCE - max;
}
}
else if ( delta < 0 ) {

const min = Math.min( edge1, edge2 );
if ( min + delta < 0 ) {
delta = -min;
}
}
else {
// nothing to do
}

edge1 += delta;
edge2 += delta;

this.edge1Property.value = edge1;
this.edge2Property.value = edge2;
},
// reentrant: true
} );

this.dataFractionProperty = new NumberProperty( 0, {
Expand All @@ -120,7 +77,6 @@ export default class IntervalTool extends PhetioObject {
public reset(): void {
this.edge1Property.reset();
this.edge2Property.reset();
this.centerProperty.reset();
}
}

Expand Down
131 changes: 92 additions & 39 deletions js/measures/view/IntervalToolNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import Bounds2 from '../../../../dot/js/Bounds2.js';
import DynamicProperty from '../../../../axon/js/DynamicProperty.js';
import TReadOnlyProperty from '../../../../axon/js/TReadOnlyProperty.js';
import isResettingAllProperty from '../../common/model/isResettingAllProperty.js';
import Multilink from '../../../../axon/js/Multilink.js';

const edgeFilter = new BiquadFilterNode( phetAudioContext, {
type: 'lowpass',
Expand Down Expand Up @@ -127,9 +128,9 @@ export default class IntervalToolNode extends Node {
this.addChild( edge1Line );
this.addChild( edge2Line );

// This is a downstream Property (only updated in the update function) that is used for sonification and the center position label..
// This is a downstream Property (only updated in the update function) that is used for sonification and the center position label.
// IntervalTool.centerXProperty may go out of range, so we use this Property to clamp the value.
const boundedCenterXProperty = new NumberProperty( intervalTool.centerProperty.value );
const centerPositionSonificationProperty = new NumberProperty( ( intervalTool.edge1Property.value + intervalTool.edge2Property.value ) / 2 );

class DraggableShadedSphereNode extends AccessibleSlider( Node, 0 ) {
public constructor( options: AccessibleSliderOptions ) {
Expand Down Expand Up @@ -206,7 +207,7 @@ export default class IntervalToolNode extends Node {
} );

this.centerLineLabel = new PDLText( new PatternStringProperty( ProjectileDataLabStrings.meanMPatternStringProperty, {
mean: roundedValueProperty( boundedCenterXProperty )
mean: roundedValueProperty( centerPositionSonificationProperty )
} ), {
font: PDLConstants.INTERVAL_TOOL_FONT,
maxWidth: 100,
Expand Down Expand Up @@ -273,9 +274,39 @@ export default class IntervalToolNode extends Node {
}
}

// This is where the UI wants to put the center.
const desiredCenterLocationProperty = new Property( new Vector2( 50, 0 ), {
valueComparisonStrategy: 'equalsFunction'
} );

// The drag listener requires a Vector2 instead of a number, so we need to create a DynamicProperty to convert between the two
const createDynamicAdapterProperty = ( property: Property<number> ) => {
return new DynamicProperty( new Property( property ), {
bidirectional: true,
map: function( value: number ) { return new Vector2( value, 0 );},
inverseMap: function( value: Vector2 ) { return value.x; }
} );
};

// Vector2 => number
const createAlternateDynamicProperty = ( property: Property<Vector2> ) => {
return new DynamicProperty( new Property( property ), {
bidirectional: true,
map: function( value: Vector2 ) { return value.x; },
inverseMap: function( value: number ) { return new Vector2( value, 0 ); }
} );
};

const separation = Math.abs( intervalTool.edge1Property.value - intervalTool.edge2Property.value );
const centerDragBoundsProperty = new Property( new Bounds2( separation / 2, -1000, PDLConstants.MAX_FIELD_DISTANCE - separation / 2, 1000 ) );

const centerDragBounds1DProperty = new DerivedProperty( [ centerDragBoundsProperty ], bounds => new Range( bounds.minX, bounds.maxX ) );

const readoutVBox = new ReadoutVBox( {
valueProperty: intervalTool.centerProperty,
enabledRangeProperty: new Property( new Range( 0, PDLConstants.MAX_FIELD_DISTANCE ) )
valueProperty: createAlternateDynamicProperty( desiredCenterLocationProperty ),
enabledRangeProperty: centerDragBounds1DProperty,
startDrag: () => { this.isCenterDraggingProperty.value = true; },
endDrag: () => { this.isCenterDraggingProperty.value = false; }
} );
this.addChild( readoutVBox );

Expand Down Expand Up @@ -317,7 +348,7 @@ export default class IntervalToolNode extends Node {
readoutVBox.top = ARROW_Y - intervalReadout.height / 2;

// Update the downstream Property which is only used for sonification.
boundedCenterXProperty.value = ( intervalTool.edge1Property.value + intervalTool.edge2Property.value ) / 2;
centerPositionSonificationProperty.value = ( intervalTool.edge1Property.value + intervalTool.edge2Property.value ) / 2;
};

intervalTool.dataFractionProperty.link( update );
Expand All @@ -343,64 +374,86 @@ export default class IntervalToolNode extends Node {
};

const edge1DragListenerOptions = {
start: () => {
this.isEdge1DraggingProperty.value = true;
},
end: () => {
this.isEdge1DraggingProperty.value = false;
},
start: () => { this.isEdge1DraggingProperty.value = true; },
end: () => { this.isEdge1DraggingProperty.value = false; },
transform: modelViewTransform
};

const edge2DragListenerOptions = {
start: () => {
this.isEdge2DraggingProperty.value = true;
},
end: () => {
this.isEdge2DraggingProperty.value = false;
},
start: () => { this.isEdge2DraggingProperty.value = true; },
end: () => { this.isEdge2DraggingProperty.value = false; },
transform: modelViewTransform
};

const centerDragListenerOptions = {
start: () => {
this.isCenterDraggingProperty.value = true;
},
end: () => {
this.isCenterDraggingProperty.value = false;
},
start: () => {this.isCenterDraggingProperty.value = true;},
end: () => { this.isCenterDraggingProperty.value = false;},
transform: modelViewTransform
};

// The drag listener requires a Vector2 instead of a number, so we need to create a DynamicProperty to convert between the two
const createDynamicAdapterProperty = ( property: Property<number>, isReentrant: boolean ) => {
return new DynamicProperty( new Property( property ), {
bidirectional: true,
map: function( value: number ) { return new Vector2( value, 0 );},
inverseMap: function( value: Vector2 ) { return value.x; },
reentrant: isReentrant
} );
};

edge1Sphere.addInputListener( new DragListener( combineOptions<DragListenerOptions<PressedDragListener>>( {
positionProperty: createDynamicAdapterProperty( intervalTool.edge1Property, false ),
positionProperty: createDynamicAdapterProperty( intervalTool.edge1Property ),
tandem: providedOptions.tandem.createTandem( 'edge1DragListener' ),
drag: moveToFront( edge1Sphere )
}, edge1DragListenerOptions, dragListenerOptions ) ) );

edge2Sphere.addInputListener( new DragListener( combineOptions<DragListenerOptions<PressedDragListener>>( {
positionProperty: createDynamicAdapterProperty( intervalTool.edge2Property, false ),
positionProperty: createDynamicAdapterProperty( intervalTool.edge2Property ),
tandem: providedOptions.tandem.createTandem( 'edge2DragListener' ),
drag: moveToFront( edge2Sphere )
}, edge2DragListenerOptions, dragListenerOptions ) ) );

desiredCenterLocationProperty.lazyLink( ( value: Vector2, oldValue: Vector2 ) => {
if ( this.isCenterDraggingProperty.value ) {
let delta = value.x - oldValue.x;

const max = Math.max( intervalTool.edge1Property.value, intervalTool.edge2Property.value );
const min = Math.min( intervalTool.edge1Property.value, intervalTool.edge2Property.value );

if ( max + delta > PDLConstants.MAX_FIELD_DISTANCE ) {
delta = PDLConstants.MAX_FIELD_DISTANCE - max;
}
if ( min + delta < 0 ) {
delta = -min;
}

intervalTool.edge1Property.setDeferred( true );
intervalTool.edge2Property.setDeferred( true );

intervalTool.edge1Property.value += delta;
intervalTool.edge2Property.value += delta;

const a = intervalTool.edge1Property.setDeferred( false );
const b = intervalTool.edge2Property.setDeferred( false );

a && a();
b && b();
}
} );

Multilink.multilink( [ this.isEdge1DraggingProperty, this.isEdge2DraggingProperty ], ( isEdge1Dragging, isEdge2Dragging ) => {
if ( !isEdge1Dragging || !isEdge2Dragging ) {
desiredCenterLocationProperty.value = new Vector2( ( intervalTool.edge1Property.value + intervalTool.edge2Property.value ) / 2, 0 );
}
} );

Multilink.multilink( [ intervalTool.edge1Property, intervalTool.edge2Property, this.isCenterDraggingProperty, this.isEdge1DraggingProperty, this.isEdge2DraggingProperty ], () => {
if ( ( this.isEdge1DraggingProperty.value || this.isEdge2DraggingProperty.value ) && !this.isCenterDraggingProperty.value ) {
desiredCenterLocationProperty.value = new Vector2( ( intervalTool.edge1Property.value + intervalTool.edge2Property.value ) / 2, 0 );

const separation = Math.abs( intervalTool.edge1Property.value - intervalTool.edge2Property.value );
centerDragBoundsProperty.value = new Bounds2( separation / 2, -1000, PDLConstants.MAX_FIELD_DISTANCE - separation / 2, 1000 );
}
} );

readoutVBox.addInputListener( new DragListener( combineOptions<DragListenerOptions<PressedDragListener>>( {
applyOffset: true,
useParentOffset: true,
positionProperty: createDynamicAdapterProperty( intervalTool.centerProperty, true ),
positionProperty: desiredCenterLocationProperty,
tandem: providedOptions.tandem.createTandem( 'centerDragListener' )
}, centerDragListenerOptions, {
useInputListenerCursor: true
useInputListenerCursor: true,
dragBoundsProperty: centerDragBoundsProperty
} ) ) );

// Play a ratcheting sound as either edge is dragged. The sound is played when passing thresholds on the field,
Expand Down Expand Up @@ -457,7 +510,7 @@ export default class IntervalToolNode extends Node {
maxSoundPlayer: nullSoundPlayer
} );

boundedCenterXProperty.lazyLink( ( newValue: number, oldValue: number ) => {
centerPositionSonificationProperty.lazyLink( ( newValue: number, oldValue: number ) => {
if ( this.isCenterDraggingProperty.value && !isResettingAllProperty.value ) {
centerValueChangeSoundPlayer.playSoundIfThresholdReached( newValue, oldValue );
}
Expand Down

0 comments on commit baec4f8

Please sign in to comment.