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

ShapePath: Fix unlinked holes when path has hole on first AND last #24114

Closed
wants to merge 1 commit into from

Conversation

neofuji
Copy link
Contributor

@neofuji neofuji commented May 23, 2022

Related issue: none

Description

When holes on first AND last in the path, ShapePath.toShapes() incorrectly ignores the holes.
For example, the letter in typeface.json has 5 holes, 1 shape, and 2 holes. The last 2 holes are incorrectly ignored.

Expected behavior

image

Actual behavior

image

typeface.json

{
  "glyphs": {
    "激": {
      "ha": 1389,
      "x_min": 5,
      "x_max": 1389,
      "o": "m 802 545 l 751 545 l 751 530 l 817 536 l 802 545 m 1104 519 l 1116 696 l 1072 696 l 1051 629 l 1088 637 l 1104 519 m 449 545 l 358 545 l 351 536 l 449 530 l 449 545 m 595 746 l 595 727 l 628 727 l 628 746 l 595 746 m 595 872 l 628 872 l 628 891 l 595 891 l 595 872 m 286 410 l 8 521 l 153 818 l 350 635 l 350 897 l 267 774 l 5 903 l 175 1185 l 350 1005 l 350 1078 l 434 1078 l 439 1184 l 773 1175 l 754 1078 l 852 1078 l 852 935 l 898 1183 l 1222 1154 l 1179 1019 l 1388 1024 l 1388 693 l 1355 696 l 1314 220 l 1389 199 l 1359 -175 l 1107 -49 l 864 -175 l 850 -62 l 844 -186 l 589 -179 l 581 -95 l 570 -182 l 328 -75 l 311 -180 l 5 -77 l 172 346 l 392 294 l 373 184 l 412 319 l 323 313 l 323 487 l 286 410 m 873 313 l 646 319 l 640 300 l 865 300 l 863 198 l 939 228 l 884 484 l 873 492 l 873 313 m 623 60 l 629 99 l 612 99 l 605 62 l 623 60 z "
    }
  },
  "familyName": "GN-KMBFont-UB-NewstyleKanaB",
  "ascender": 1194,
  "descender": -475,
  "underlinePosition": -130,
  "underlineThickness": 65,
  "boundingBox": {
    "yMin": -450,
    "xMin": -142,
    "yMax": 1298,
    "xMax": 1679
  },
  "resolution": 1000,
  "original_font_information": {
    "format": 0,
    "copyright": "Nagoriyuki/Getsuren",
    "fontFamily": "GN-KMBFont-UB-NewstyleKanaB",
    "fontSubfamily": "Ultra",
    "uniqueID": "GN-KMBFont-UB-NewstyleKanaB",
    "fullName": "GN-KMBFont-UB-NewstyleKanaB",
    "version": "Version 0.99b",
    "postScriptName": "GN-KMBFont-UB-NewstyleKanaB",
    "trademark": "GN-KMBFont-UB-NewstyleKanaB"
  },
  "cssFontWeight": "normal",
  "cssFontStyle": "normal"
}

@mrdoob mrdoob requested a review from Mugen87 May 24, 2022 04:11
@mrdoob mrdoob added this to the r141 milestone May 24, 2022

const restShapeHoles = newShapeHoles.pop();

Array.prototype.push.apply( newShapeHoles[ newShapeHoles.length - 1 ], restShapeHoles );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just newShapeHoles[ newShapeHoles.length - 1 ].push( ...restShapeHoles )?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find both ways hard to read 😅

Copy link
Collaborator

@Mugen87 Mugen87 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mind explaining in more detail how the error occurs? TBH, the implementation of ShapePath.toShapes() is borderline. Before adding more complexity it's important to understand why the code fails. And if the patch can be applied elsewhere.

@neofuji
Copy link
Contributor Author

neofuji commented May 24, 2022

ShapePath.toShapes() has 3 steps to build shapes.

  1. Divides this.subPaths into newShapes and newShapeHoles.
  2. Moves the items in newShapeHoles if newShapeHoles[i][j] is outside of newShapes[i].
  3. Apply newShapeHoles[i] to newShapes[i].

This problem occurs in the step 1.
Step 1 divides this.subPaths to a pair of array, newShapes: [shape#0, shape#1, ...] and newShapeHoles: [holes in shape#0, holes in shape#1, ...].
However, when holes on first AND last, the length of newShapeHoles is incorrectly more than newShapes.
Step 2 and 3 assumes the length of the arrays is same. If not same, the last holes are ignored incorrectly.

Shapes first, holes last
Input: [s0, h0, h1, s1, h2]
Output newShapes: [s0, s1]
Output newShapeHoles: [[h0, h1], [h2]]

Holes first, shapes last
Input: [h0, h1, s0, h2, s1]
Output newShapes: [s0, s1]
Output newShapeHoles: [[h0, h1], [h2]]

Holes first, HOLES last
Input: [h0, h1, s0, h2, s1, h3]
Output newShapes: [s0, s1]
Output newShapeHoles: [[h0, h1], [h2], [h3]] <- [h3] has no matching newShapes!

If step 1 is fixed to divide all subPaths correctly, step 2 and step 3 works correctly with no fix.
This PR fixes the step 1.

@Mugen87
Copy link
Collaborator

Mugen87 commented May 25, 2022

I think I remember now. We've discussed a similar problem in #16950.

Instead of using ShapePath.toShapes(), do you mind trying the static SVGLoader.createShapes() method? It would be interesting to know if this implementation fixes your issue.

I know it's odd importing a separate loader for this task but this is just for testing. If you need a code example, SVGLoader.createShapes() is used here:

const shapes = SVGLoader.createShapes( path );

@neofuji
Copy link
Contributor Author

neofuji commented May 25, 2022

@Mugen87 SVGLoader.createShapes() works well but ShapePath.userData.style.fillRule must be set.

I tested by patching Font.prototype.generateShapes().

Array.prototype.push.apply( shapes, paths[ p ].toShapes() );

			// Array.prototype.push.apply( shapes, paths[ p ].toShapes() );
			paths[ p ].userData ??= {};
			paths[ p ].userData.style ??= {};
			paths[ p ].userData.style.fillRule ??= "nonzero";
			Array.prototype.push.apply( shapes, SVGLoader.createShapes( paths[ p ] ) );

@mrdoob mrdoob modified the milestones: r141, r142 May 26, 2022
@mrdoob mrdoob modified the milestones: r142, r143 Jun 29, 2022
@Mugen87
Copy link
Collaborator

Mugen87 commented Jul 12, 2022

SVGLoader.createShapes() works well but ShapePath.userData.style.fillRule must be set.

Fixed via #24293.

@mrdoob mrdoob modified the milestones: r143, r144 Jul 28, 2022
@mrdoob mrdoob modified the milestones: r144, r145 Aug 31, 2022
@Mugen87
Copy link
Collaborator

Mugen87 commented Sep 22, 2022

Closing. The workaround for this issue is the usage of SVGLoader.createShapes().

In the long term, the implementation of ShapePath.toShapes() will be replaced, see #24560 (comment).

@Mugen87 Mugen87 closed this Sep 22, 2022
@Mugen87 Mugen87 removed this from the r145 milestone Sep 22, 2022
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

Successfully merging this pull request may close these issues.

4 participants