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

feat: basic group shape #1294

Merged
merged 49 commits into from Feb 27, 2023
Merged

feat: basic group shape #1294

merged 49 commits into from Feb 27, 2023

Conversation

liangyiliang
Copy link
Contributor

@liangyiliang liangyiliang commented Feb 2, 2023

Description

Resolves #783 .

We added a basic Group shape that contains multiple already-declared shapes. Each group is now rendered as a <g> tag which contains the internal shapes.

As an example,

forall T t {
    t.s1 = Circle {}
    t.s2 = Rectangle {}
    t.s3 = Text {
        string: "Hello"
    }
    t.g = Group {
        shapes : [t.s1, t.s2, t.s3]
    }
    t.s4 = Circle {}
    t.g2 = Group {
        shapes : [t.s4, t.g]
    }
}

can now generate

<svg ...>
  <g>
    <circle ... />
    <g>
      <circle ... />
      <rect ... />
      <text .../>
    </g>
  </g>
</svg>

New Shape

We add a new shape, Group, to the shape library. As of now, Group shape only has one property, shapes, and an automatically generated name. Property shapes is a list of paths to previously-defined shapes.

Changes to the Compiler

The Style language now allows users to have a list of shapes, denoted [path1, path2, ...] where path1, path2, ... are paths to shapes.

GPI or ShapeAD Objects?

We go through and process each Style statement in the translation process, at which point only the GPIs of each shape can be accessed. The actual ShapeAD objects are available near the end of the compilation process. Therefore, a Group shape, initially, stores a list of GPIs. Near the end of the compilation process, after we already have a list of all ShapeADs, we switch each GPI into the corresponding ShapeAD objects.

Group Graph

The compiler uses a directed graph, named GroupGraph, to represent group-shape relationships between shapes. Formally, a directed edge exists between two shapes ($g \rightarrow s$) if and only if shape $s$ is a child of group $g$.

For example,

  • Suppose we have shapes s1, s2, s3, s4, s5, s6
  • Shapes s1, s2 belong to group g1
  • Shapes s3, s4 belong to group g2
  • Groups g1 and g2, and shape s5, all belong to group g3
  • Shape s6 does not belong to any groups.

Then, the group graph should look like
image

We can use this group graph to check whether or not the grouping relations are well-defined, which means

  • the grouping relations should not be cyclic
  • each shape can have at most one parent.

The compiler fires a warning if any of these checks fail. A well-defined grouping relation should look like a tree (as in the example).

Layering

Suppose all layering directives have the form layer s above s' (layer s below s' is equivalent to layer s' above s). We want the following nice properties: (these are not the actual rules we follow for the algorithm)

  • Layering on a group implies the same layering on all members of the group.
  • Layering on a member of a group implies the same layering on the entire group.

Concretely, in the group graph above, writing layer s1 above g2 implies:

  • layer s1 above g3 and layer s3 above s4 (since s3, s4 are members of g2)
    • layer g1 above s3 and layer g1 above s4 (since s1 is a member of g1)
      • layer s2 above s3 and layer s2 above s4 (since s2 is a member of g1)
  • layer g1 above g2 (since s1 is a member of g1)

These are the "saturation" rules which saturates the entire layer graph and hence is not efficient. The algorithm leverages upon the observation that layering directives are only useful when two shapes are siblings within the same group. Suppose we encounter the layering directive layer s above s'.

When encountering layer s1 above s2, the algorithm attempts to "walk up" the group graph from both s1 and s2 towards the root, until when the algorithm finds s1' and s2' which are siblings. Then, the algorithm applies the same layering directive to s1' and s2'.

In the concrete example of layer s1 above g2, the algorithm would "walk up" s1 to get g1. Since g1 and g2 are siblings, we add layer g1 above g2 to the layering graph. As another example, layer s1 above s6 involves walking up s1 twice to get g3 which is a sibling of s6, so we add layer g3 above s6 to the layer graph.

A few more (more trivial) examples:

  • layer s3 above s4 gets added to the layer graph directly, since s3 and s4 are already siblings.
  • layer g3 above g3 also gets added to the layer graph directly, since we consider g3 to be a "sibling" of itself. This will cause a cycle, which is perfectly expected.
  • layer s4 above g3 would involve walking up s4 twice to get g3. Similarly, we add layer g3 above g3.

The final layer graph would only contain edges between siblings.

Keeping Groups Together

s1 = // some shape
s2 = // some shape
g = Group {
  shapes: [s1, s2]
}
s3 = // some shape

layer s1 above s2
layer s3 above s2
layer s3 below s1

The layering directives would force the layering to be bottom s2 s3 s1 top. This itself does not cause a loop, but does break up the group g. The compiler should generate a warning.

Running the algorithm, we have

  • layer s1 above s2 just gets added to the layer graph because s1, s2 are siblings.
  • layer s3 above s2 requires one "walk up" from s2 to g, which is a sibling of s3, so layer s3 above g gets added to the layer graph.
  • layer s3 below s1 requires one "walk up" from s1 to g, which is a sibling of s3, so layer s3 below g gets added to the layer graph.

We hence have both layer s3 above s2 and layer s3 below s1, which causes a loop and a warning should fire.

Changes to Compilation

The style compilation results in a State object whose shapes field contains a flat, ordered list of all shapes. With groups, the list of shapes should no longer be a flat list. It still stores ShapeAD objects, but each Group shape's members should be actual ShapeAD objects. That is, the list of shapes should contain only ShapeAD objects for the top-level shapes, and all lower-level shapes should be within their respective parent groups. Furthermore, the list of top-level shapes, and for each group, the list of its child shapes, should all be ordered according to the layering. This is easy simply by sorting the elements of the lists based on the layer ordering. The list of top-level shapes should then be passed into the renderer.

Previously, after translation, the compiler does the following in order:

  • Compute the layer ordering based on the layering graph (computeShapeOrdering)
  • Using the layer ordering, compute an ordered list of ShapeAD objects (getShapes)

The change goes,

  • Generate a flat list of all shapes (where Group shapes store GPIs, not actual shape objects, since shape objects don't exist yet).
  • Generate a group graph based on the list of shapes. Each node is a string. This group graph does not respect the layering.
  • Generate layer ordering using the layering directives and group graph.
  • Annotate the group graph so that each node are labeled with the layer ordering index. This group graph now respects layering within each group.
  • Use the annotated group graph to generate a list of top-level ShapeADs, this time each group storing its actual sub-shapes. This is what we pass into the renderer.

Changes to the Renderer

The renderer now takes a list of top-level shapes, and renders them. Whenever it encounters a group, it recursively renders all the member shapes before packaging everything into one <g> ... </g> tag.

Examples with steps to reproduce them

The Wet Floor example now uses groups.

Can also do bbox-related constraints on groups: here

Checklist

  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new ESLint warnings
  • I have reviewed any generated registry diagram changes

Open questions

Turns out, warnings don't show up in the IDE.

@github-actions
Copy link

github-actions bot commented Feb 2, 2023

± Registry diff

M	one-water-molecule-atoms-and-bonds.svg
M	wet-floor-atoms-and-bonds.svg

📊 Performance

Key

Note that each bar component rounds up to the nearest 100ms, so each full bar is an overestimate by up to 400ms.

     0s   1s   2s   3s   4s   5s   6s   7s   8s   9s
     |    |    |    |    |    |    |    |    |    |
name ▝▀▀▀▀▀▀▀▀▀▀▀▚▄▄▄▄▄▄▄▄▄▞▀▀▀▀▀▀▀▀▀▀▀▀▚▄▄▄▄▄▄▄▄▄▖
      compilation labelling optimization rendering

Data

                                                    0s   1s   2s   3s   4s   5s   6s   7s
                                                    |    |    |    |    |    |    |    |
3d-projection-fake-3d-linear-algebra                ▝▞▖
allShapes-allShapes                                 ▝▞▄▄▖
arrowheads-arrowheads                               ▝▞▖
caffeine-structural-formula                         ▝▀▀▞▚
center-shrink-circle-animation                      ▝▞▖
circle-example-euclidean                            ▝▀▚▀▀▚
closest-point-test-closest-point                    ▝▀▀▀▀▀▀▚▚
collinear-euclidean                                 ▝▀▞▖
congruent-triangles-euclidean                       ▝▀▀▚▚
continuousmap-continuousmap                         ▝▞▖
cubic-bezier-cubic-bezier                           ▝▚▀▀▖
glutamine-molecules-basic                           ▝▀▚▚
half-adder-distinctive-shape                        ▝▚▚
hypergraph-hypergraph                               ▝▀▀▀▞▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▚
incenter-triangle-euclidean                         ▝▀▞▖
lagrange-bases-lagrange-bases                       ▝▚▚
matrix-matrix-addition-matrix-ops                   ▝▚▚
matrix-matrix-division-elementwise-matrix-ops       ▝▚▚
matrix-matrix-multiplication-elementwise-matrix-ops ▝▚▚
matrix-matrix-multiplication-matrix-ops             ▝▚▚
matrix-matrix-subtraction-matrix-ops                ▝▚▚
matrix-transpose-matrix-ops                         ▝▀▞▖
matrix-vector-left-multiplication-matrix-ops        ▝▚▚
matrix-vector-right-multiplication-matrix-ops       ▝▚▚
midsegment-triangles-euclidean                      ▝▀▞▖
mobius-mobius                                       ▝▞▖
non-convex-non-convex                               ▝▀▞▖
one-water-molecule-atoms-and-bonds                  ▝▞▖
parallel-lines-euclidean                            ▝▀▞▚
persistent-homology-persistent-homology             ▝▀▀▞▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▄
points-around-line-shape-distance                   ▝▀▚▚
points-around-polyline-shape-distance               ▝▀▀▞▖
points-around-star-shape-distance                   ▝▀▀▀▞▖
quaternion-group-group-theory-cayley-graph          ▝▀▚▚
quaternion-group-group-theory-multiplication-table  ▝▀▀▄▞▖
scalar-vector-division-matrix-ops                   ▝▚▚
scalar-vector-left-multiplication-matrix-ops        ▝▚▚
scalar-vector-right-multiplication-matrix-ops       ▝▚▚
siggraph-teaser-euclidean-teaser                    ▝▀▚▚
small-graph-disjoint-rect-line-horiz                ▝▀▚▚
small-graph-disjoint-rects                          ▝▞▖
small-graph-disjoint-rects-large-canvas             ▝▚▚
small-graph-disjoint-rects-small-canvas             ▝▞▖
tree-tree                                           ▝▚▚
tree-venn                                           ▝▀▚▚
tree-venn-3d                                        ▝▀▞▄▄▖
two-triangles-triangle-mesh-3d                      ▝▚▚
two-vectors-perp-vectors-dashed                     ▝▞▖
vector-vector-addition-matrix-ops                   ▝▚▚
vector-vector-division-elementwise-matrix-ops       ▝▚▚
vector-vector-multiplication-elementwise-matrix-ops ▝▚▚
vector-vector-outerproduct-matrix-ops               ▝▚▚
vector-vector-subtraction-matrix-ops                ▝▚▚
vector-wedge-exterior-algebra                       ▝▞▖
wet-floor-atoms-and-bonds                           ▝▀▞▀▖
word-cloud-example-word-cloud                       ▝▀▞▖
wos-laplace-estimator-walk-on-spheres               ▝▀▚▚
wos-nested-estimator-walk-on-spheres                ▝▀▀▀▞▀▀▖
wos-offcenter-estimator-walk-on-spheres             ▝▀▚▀▀▀▚
wos-poisson-estimator-walk-on-spheres               ▝▀▚▀▚

@codecov
Copy link

codecov bot commented Feb 2, 2023

Codecov Report

Merging #1294 (5a8ff8a) into main (c479eec) will increase coverage by 0.15%.
The diff coverage is 58.58%.

❗ Current head 5a8ff8a differs from pull request most recent head 3c3f9f8. Consider uploading reports for the commit 3c3f9f8 to get more accurate results

@@            Coverage Diff             @@
##             main    #1294      +/-   ##
==========================================
+ Coverage   61.28%   61.43%   +0.15%     
==========================================
  Files          63       65       +2     
  Lines        7828     8053     +225     
  Branches     1845     1887      +42     
==========================================
+ Hits         4797     4947     +150     
- Misses       2924     2998      +74     
- Partials      107      108       +1     
Impacted Files Coverage Δ
packages/core/src/utils/Error.ts 27.42% <0.00%> (-0.60%) ⬇️
packages/core/src/renderer/Renderer.ts 28.57% <24.61%> (+3.57%) ⬆️
packages/core/src/engine/EngineUtils.ts 52.04% <29.16%> (-3.74%) ⬇️
packages/core/src/utils/Graph.ts 97.91% <50.00%> (-2.09%) ⬇️
packages/core/src/shapes/Shapes.ts 76.13% <58.82%> (-23.87%) ⬇️
packages/core/src/utils/CollectLabels.ts 65.83% <65.21%> (-3.04%) ⬇️
packages/core/src/utils/Util.ts 49.73% <66.66%> (+0.41%) ⬆️
packages/core/src/compiler/Style.ts 64.06% <68.13%> (+0.41%) ⬆️
packages/core/src/utils/GroupGraph.ts 98.07% <98.07%> (ø)
packages/core/src/shapes/Group.ts 100.00% <100.00%> (ø)
... and 1 more

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

@cloudflare-pages
Copy link

cloudflare-pages bot commented Feb 2, 2023

Deploying with  Cloudflare Pages  Cloudflare Pages

Latest commit: 3c3f9f8
Status: ✅  Deploy successful!
Preview URL: https://9ff79c08.penrose-72l.pages.dev
Branch Preview URL: https://group-shape.penrose-72l.pages.dev

View logs

@liangyiliang liangyiliang marked this pull request as ready for review February 8, 2023 07:12
Copy link
Member

@wodeni wodeni left a comment

Choose a reason for hiding this comment

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

Thanks for doing this @liangyiliang! Very important first step towards better structure and reusability in Style.

My main request is to take a closer look at group graph construction in the renderer and figure out if we can avoid re-computation of the graph as much as possible.

I haven't thought deeply about the usability and performance implications of the saturation rules. At a glance it makes sense to me and the implementation seems straightforward. I'll leave it to @joshsunshine and @samestep to evaluate.

Generally, this PR has some duplicated code and some functions tend to have very long names. I'd suggest refactoring them to avoid duplication. If a function doesn't have a great name and it's called once in your code, I think you can consider inlining it to improve readability. See my code-level comments below.

packages/core/src/renderer/Renderer.ts Outdated Show resolved Hide resolved
packages/core/src/renderer/Renderer.ts Outdated Show resolved Hide resolved
const rawShapeProps = gpi.contents[1];
if (!isShapeType(shapeType)) {
throw new Error("Unknown shape in Group bbox: " + shapeType);
} else {
Copy link
Member

Choose a reason for hiding this comment

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

Unrelated to this PR: we should really remove the need for boilerplate like this.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yep, agreed; #715 touches on this, but doesn't entirely cover the problem.

packages/core/src/types/state.ts Outdated Show resolved Hide resolved
packages/core/src/utils/GroupGraph.ts Outdated Show resolved Hide resolved
packages/core/src/utils/GroupGraph.ts Outdated Show resolved Hide resolved
packages/core/src/utils/GroupGraph.ts Outdated Show resolved Hide resolved
packages/core/src/compiler/Style.test.ts Show resolved Hide resolved
@liangyiliang
Copy link
Contributor Author

My main request is to take a closer look at group graph construction in the renderer and figure out if we can avoid re-computation of the graph as much as possible.

I agree! I will store the Group Graph in the State.

@liangyiliang liangyiliang marked this pull request as ready for review February 23, 2023 18:47
@liangyiliang liangyiliang changed the title feat: group shape feat: basic group shape Feb 23, 2023
@samestep
Copy link
Collaborator

@liangyiliang you modified some newline at the end of yarn.lock; could you revert that?

Copy link
Collaborator

@samestep samestep left a comment

Choose a reason for hiding this comment

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

I still need to review the rest of the code, but while I'm doing that, could you add information about Group to the docs on the website in this PR? You'll at least need to add a packages/docs-site/docs/ref/style/shapes/group.md file, but you should probably also put more info in packages/docs-site/docs/ref/style.md too.

@liangyiliang
Copy link
Contributor Author

@liangyiliang you modified some newline at the end of yarn.lock; could you revert that?

Done!

could you add information about Group to the docs on the website in this PR? You'll at least need to add a packages/docs-site/docs/ref/style/shapes/group.md file, but you should probably also put more info in packages/docs-site/docs/ref/style.md too.

Yup I will do it!

@liangyiliang
Copy link
Contributor Author

I still need to review the rest of the code, but while I'm doing that, could you add information about Group to the docs on the website in this PR? You'll at least need to add a packages/docs-site/docs/ref/style/shapes/group.md file, but you should probably also put more info in packages/docs-site/docs/ref/style.md too.

Done!

Copy link
Collaborator

@samestep samestep left a comment

Choose a reason for hiding this comment

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

Looks great modulo the remaining comments I left above! Thanks for pulling this through :)

@liangyiliang liangyiliang merged commit cf77bff into main Feb 27, 2023
@liangyiliang liangyiliang mentioned this pull request Mar 1, 2023
@samestep samestep deleted the group-shape branch March 15, 2023 17:25
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.

Add Group shape to Style
3 participants