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

Bug: Graphics with only strokes that don't have a closePath, but an event is fired where a closePath would have been #10303

Closed
naramdash opened this issue Mar 9, 2024 · 3 comments
Assignees

Comments

@naramdash
Copy link
Contributor

Current Behavior

Graphics is not closed, but events are fired there as if there is a path to close.

image

Expected Behavior

Events should not be fired in areas where no strokes are drawn.

Steps to Reproduce

  1. make a graphics with strokes but no closePath called
  2. add eventMode
  3. then test events where closePath might have been

you can check running example codesandbox

Environment

  • pixi.js version: 8.0.1
  • Browser & Version: e.g. Microsoft Edge 122 & Google Chrome 122
  • OS & Version: e.g. Windows 11
  • Running Example: codesandbox

Possible Solution

Might need to check if closePath was called.

Additional Information

I know that events were not supported in strokes only graphics in v7.

#7058

@naramdash
Copy link
Contributor Author

Share what I've debugged.

  1. Event fired.
  2. Called the containsPoint of that Graphic in EventSystem.
  3. GraphicsContext.__proto__.containsPoint is called.
  4. This graphic's instruction.action is stroke, so shape.strokeContains is called
  5. shape is a polygon, so the Polygon.__proto__.strokeContains is called
  6. (*) However, when passing arguments to strokeContains, shape.closePath is missing.

If closePath is false, that GraphicsContext.__proto__.containsPoint may needs to use a variation of shape(polygon) instead of using it directly.

code => src\scene\graphics\shared\GraphicsContext.ts line @ 1156

@naramdash
Copy link
Contributor Author

Below is my custom eventable line code.

Heavily depends on #7058 comments (v7) and migrated to v8.
(v7 => geometry.points, v8 => context.instructions)

Because this is event-enabled code for graphics with only a single stroke & no fill, I haven't tested its suitability for some of the basic situations that pixi supports (with linecap, using with fill).

It would be nice to see this as pseudo code for the adjustable width eventable line.

export class EventableLine extends Graphics {
  ...
  #contains(x: number, y: number) {
    this.#hitAreaPolygon ??= this.#createHitAreaPolygon();
    return this.#hitAreaPolygon.contains(x, y);
  }

  #createHitAreaPolygon(): Polygon {
    const instruction = this.context.instructions[0];
    if (instruction.action !== "stroke") return new Polygon([]);

    const shape = instruction.data.path.shapePath.shapePrimitives[0].shape;
    if (shape instanceof Polygon === false) return new Polygon([]);

    const points = chunk(shape.points, 2) as [x: number, y: number][];

    const lefts = [] as PointData[];
    const rights = [] as PointData[];

    // Until last point (skip last point to avoid writing conditional code)
    for (let index = 0; index < points.length - 1; index++) {
      const drawingVector = getDrawingVector(points[index], points[index + 1]!);
      if (drawingVector.x !== 0 || drawingVector.y !== 0) {
        const [left, right] = getLeftRight(
          points[index],
          drawingVector,
          this.hitAreaWidth,
        );
        lefts.push(left);
        rights.push(right);
      }
    }
    // Last point
    {
      const drawingVector = getDrawingVector(points.at(-1)!, points.at(-2)!);
      if (drawingVector.x !== 0 || drawingVector.y !== 0) {
        const [left, right] = getLeftRight(
          points.at(-1)!,
          drawingVector,
          this.hitAreaWidth,
        );
        lefts.push(right);
        rights.push(left);
      }
    }

    const hitAreaPoints = lefts.concat(rights.reverse());
    return new Polygon(hitAreaPoints);
  }
  ...
}

function getDrawingVector(
  curPoint: [x: number, y: number],
  nextPoint: [x: number, y: number],
) {
  return subtractVector(
    { x: nextPoint[0], y: nextPoint[1] },
    { x: curPoint[0], y: curPoint[1] },
  );
}

function getLeftRight(
  curPoint: [x: number, y: number],
  drawingVector: PointData,
  hitAreaWidth: number,
) {
  return orthogonalVectors(drawingVector).map((v) => {
    const unit = unitVector(v);
    const hitAreaVector = multiplyVector(unit, hitAreaWidth / 2);
    return addVector({ x: curPoint[0], y: curPoint[1] }, hitAreaVector);
  }) as [PointData, PointData];
}

@naramdash
Copy link
Contributor Author

I rewrote eventable-line with almost copy & paste from Polygon.__proto__.strokeContains.

To fundamentally fix the issue, might need to add skipLastLineSegment?: boolean = false to the input parameters of Polygon.__proto__.strokeContains.

export class EventableLine extends Graphics {
  #strokeContains(x: number, y: number) {
    const instruction = this.context.instructions[0];
    if (instruction.action !== "stroke") return false;

    const shape = instruction.data.path.shapePath.shapePrimitives[0].shape;
    if (shape instanceof Polygon === false) return false;

    const halfHitAreaWidth = this.hitAreaWidth / 2;
    const halfHitAreaWidthSqrd = halfHitAreaWidth * halfHitAreaWidth;

    const points = shape.points;

    // skip last line segment => i < points.length - 2
    for (let i = 0; i < points.length - 2; i += 2) {
      const x1 = points[i];
      const y1 = points[i + 1];
      const x2 = points[(i + 2) % points.length];
      const y2 = points[(i + 3) % points.length];

      const distanceSqrd = squaredDistanceToLineSegment(x, y, x1, y1, x2, y2);

      if (distanceSqrd <= halfHitAreaWidthSqrd) {
        return true;
      }
    }

    return false;
  }
}

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

No branches or pull requests

2 participants