Skip to content
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

Address behavior of FreeForm drag. #218

Closed
veillette opened this issue Feb 2, 2023 · 8 comments
Closed

Address behavior of FreeForm drag. #218

veillette opened this issue Feb 2, 2023 · 8 comments

Comments

@veillette
Copy link
Contributor

After adding a drag listener to the graphNode instead of the curve themselves (see #210) we will need to address the behavior of the FreeForm curve manipulation mode as it makes an assumption that the initial drag event is on the curve.

@veillette veillette self-assigned this Feb 2, 2023
veillette added a commit that referenced this issue Feb 3, 2023
@veillette
Copy link
Contributor Author

The commit above fixes the issue above by setting the penultimatePosition to null.

Therefore, for the first drag event, there is only one point, which does not lie on the curve, such that you can create segments of curve as

image

Note the single dot that results from an exceedingly short drag event.

I'll address this next week to come up with a more acceptable behavior.

veillette added a commit that referenced this issue Feb 7, 2023
veillette added a commit that referenced this issue Feb 8, 2023
veillette added a commit that referenced this issue Feb 8, 2023
Signed-off-by: Martin Veillette <[email protected]>
@veillette
Copy link
Contributor Author

Note for self: New approach to average curve based on mollifying functions.

New approach to average curve for free form
 /**
   * Allows the user to drag Points in the Curve to any desired position to create customs but smooth shapes.
   * This method will update the curve with the new position value. It attempts to create a smooth curve
   * between position and antepenultimatePosition. 
   * The main goal of the drawToForm method is to create a curve segment that is smooth enough that it can be
   * twice differentiable without generating discontinuities.
   *
   * @param position - in model coordinates
   * @param penultimatePosition - in model coordinates
   * @param antepenultimatePosition - in model coordinates
   */
  private drawFreeformToPosition( position: Vector2,
                                  penultimatePosition: Vector2 | null,
                                  antepenultimatePosition: Vector2 | null ): void {

    // Closest point associated with the position
    const closestPoint = this.getClosestPointAt( position.x );

    // Amount to shift the CurvePoint closest to the passed-in position.
    closestPoint.y = position.y;

    // Point associated with the last drag event
    if ( penultimatePosition ) {
      const lastPoint = this.getClosestPointAt( penultimatePosition.x );

      // We want to create a straight line between this point and the last drag event point
      const closestVector = closestPoint.getVector();
      this.interpolate( closestVector.x, closestVector.y, lastPoint.x, penultimatePosition.y );
    }
    else {

      // There is no position associated with the last drag event.
      // Let's create a hill with a narrow width at the closestPoint.
      // See https://github.com/phetsims/calculus-grapher/issues/218
      this.createHillAt( WEE_WIDTH, closestPoint.x, closestPoint.y );
    }

    if ( penultimatePosition && antepenultimatePosition ) {

      const lastPoint = this.getClosestPointAt( penultimatePosition.x );

      // Point associated with the last drag event
      const nextToLastPoint = this.getClosestPointAt( antepenultimatePosition.x );

      // Checks that lastPoint is in between closestPoint and lastPoint
      if ( ( closestPoint.x - lastPoint.x ) * ( nextToLastPoint.x - lastPoint.x ) < 0 ) {

        // Finds two control points that are approximately midway between our three points
        const cp1Point = this.getClosestPointAt( ( position.x + penultimatePosition.x ) / 2 );
        const cp2Point = this.getClosestPointAt( ( penultimatePosition.x + antepenultimatePosition.x ) / 2 );

        // Check that the lastPoint is between cp1 and cp2
        if ( ( cp1Point.x - lastPoint.x ) * ( cp2Point.x - lastPoint.x ) < 0 ) {

          // x separation between two adjacent points in a curve array
          const deltaX = this.deltaX;

          const isDescending = cp1Point.x < cp2Point.x;
          const p1x = isDescending ? cp1Point.x : cp2Point.x;
          const p2x = isDescending ? cp2Point.x : cp1Point.x;

          const linearOne = this.linear( closestPoint.x, position.y, lastPoint.x, penultimatePosition.y );
          const linearTwo = this.linear( lastPoint.x, penultimatePosition.y,
            nextToLastPoint.x, antepenultimatePosition.y );
          const stepFunction: MathFunction = x => {
            if ( isDescending ) {
              return ( x < penultimatePosition.x ) ? linearOne( x ) : linearTwo( x );
            }
            else {
              return ( x < penultimatePosition.x ) ? linearTwo( x ) : linearOne( x );
            }
          };

          const displacement = p2x - p1x;

          const mollifierFunction: MathFunction = x => {
            const width = 0.5 * displacement;
            if ( Math.abs( x ) < width ) {
              return Math.exp( -1 / ( 1 - ( x / width ) ** 2 ) );
            }
            else {
              return 0;
            }
          };

          for ( let x = p1x; x < p2x; x += deltaX ) {
            let weight = 0;
            let functionWeight = 0;

            for ( let dx = -displacement; dx < displacement; dx += deltaX ) {
              weight += mollifierFunction( dx );
              functionWeight += mollifierFunction( dx ) * stepFunction( x + dx );
            }
            this.getClosestPointAt( x ).y = functionWeight / weight;
          }
        }
      }
    }
  }

  private linear( x1: number, y1: number, x2: number, y2: number ): MathFunction {
    assert && assert( x1 !== x2, 'linear requires different x values' );
    return x => ( x - x2 ) * ( y1 - y2 ) / ( x1 - x2 ) + y2;
  }

veillette added a commit that referenced this issue Feb 9, 2023
Signed-off-by: Martin Veillette <[email protected]>
@veillette
Copy link
Contributor Author

I committed the approach above (after some clean up).

It uses a very different approach on how to smooth functions based on a mollifier. We previously used short quadratic segments, but these had the unfortunate property to becomes a bunch of piecewise constants once we take the second derivative.

A typical result using the mollifying function gives
image

For reference, here is a typical case using the quadratic segment algorithm.
image

@veillette
Copy link
Contributor Author

Even with mollifying approach, you can see that the second derivative is a somewhat bizarre function. However, that is probably the best I can do.

@veillette
Copy link
Contributor Author

I'll assign this to @amanda-phet to test and review the free-form. I suggest you used the Lab Screen with the second derivative to truly test the free-form.

@veillette veillette assigned amanda-phet and unassigned veillette Feb 9, 2023
veillette added a commit that referenced this issue Feb 9, 2023
Signed-off-by: Martin Veillette <[email protected]>
@amanda-phet
Copy link
Contributor

Let's discuss this in a meeting. I'm not sure what exactly I'd like changed, but this seems difficult to interpret.

Ex 1: This situation wasn't possible in the flash version, so it's cool we can do it now. However the result is difficult to interpret.
image

Ex 2: This situation is just drawing a random-ish curve, nothing too fancy or unusual.
image
WIth many presses of the "smooth" button, I expected the 2nd derivative to smooth out, but it didn't:
image

@veillette
Copy link
Contributor Author

I should add that the presence of discontinuity points in the derivatives is symptomatic of a failure of the discontinuity detection algorithm rather than an intrinsic problem with free form drag.

@amanda-phet
Copy link
Contributor

Thanks for clarifying @veillette . I think the free form drag is working very well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants