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: Add generic SVG Element shape to Style #1824

Open
keenancrane opened this issue Jul 5, 2024 · 12 comments
Open

feat: Add generic SVG Element shape to Style #1824

keenancrane opened this issue Jul 5, 2024 · 12 comments

Comments

@keenancrane
Copy link
Collaborator

keenancrane commented Jul 5, 2024

This issue is a stub to discuss the design of a potential Style language feature.

Currently we have a fixed set of shape definitions that will get rendered as SVG code (Circle, Line, Rectangle, etc.). The proposal is to allow a generic definition of the form

Element e = elementname {
   attribute1: value1
   attribute2: value2
   ...
}

These tag definitions would be rendered by a direct translation into the corresponding SVG code

<elementname attribute1=value1 attribute2=value2>

In some sense this is just an extension of the existing SVG passthrough functionality (with a similar set of warts).

Motivation

One of the original design goals for Penrose is that "There should be no inherent limit to visual sophistication." At present, there is quite a lot that can be expressed in the output format (SVG) but is not exposed through Penrose. Adding an Element construct to Style provides an "escape hatch" that exposes essentially all of SVG to Penrose, much in the same way that the passthrough feature already exposes significant additional functionality. It would for instance enable us to specify filters, animations, etc., which are currently not available through the fixed set of shape definitions.

In principle this design also enables Penrose to generate arbitrary XML code, which need not conform to the SVG schema. For instance, one could immediately use Penrose to construct 3D diagrams in COLLADA format.

Finally, the design may also lighten the developer burden, in the sense that we do not have to expose such features in a one-by-one fashion. Moreover, it lightens the burden on the Penrose team to fully design and spec out system capabilities (gradients, animation, interactivity, etc.), enabling our user community to drive the development of new features in a more organic way. For instance, rather than trying to imagine what kinds of animation features and use cases might be relevant to users, we provide a generic mechanism that lets them specify whatever they want—albeit in a way that is low-level and perhaps not completely ergonomic. Common usage patterns can then be developed into more thoughtful language/system features as they emerge.

Of course, this design is not without some potential pitfalls—discussed below.

Nested Elements

SVG (and XML more generally) relies on the ability to specify nested tags. We already have support for nesting through the Group shape:

shape G = Group {
   shapes: [ shape1, shape2,  ]
}

Likewise, it would make sense to allow the proposed Element definitions to be nested. For instance, the definitions

Element s1 = stop {
   stopColor: #f00
   offset: "0"
}
Element offset2 = animate {
   attributeName: "offset"
   values: ".1;.9;.1"
   dur: "2s"
   repeatCount: "indefinite"
}
Element s2 = stop {
   stopColor: #fff
   children: [ offset2 ]
}
Element s3 = stop {
   stopColor: #00c
   offset: "1"
}
Element C = linearGradient {
   children: [ stop 1, stop2, stop3 ]
   gradientTransform: "rotate(45)"
   id: "myGradient"
}

would get rendered as the SVG code

<linearGradient id="myGradient">
   <stop offset="0" stop-color="#f00"/>
   <stop offset=".5" stop-color="#fff">
      <animate attributeName="offset" values=".1;.9;.1" dur="2s" repeatCount="indefinite"/>
   </stop>
   <stop offset="1" stop-color="#00c"/>
</linearGradient>

producing an animated gradient like this:

stop-animation

Interaction with Optimization

A big challenge when designing anything for Penrose is that we are not simply building a renderer—we are also building an optimizer/layout engine. Here, a very legitimate concern is that attributes of the elements may be ignored by the optimizer. There are at least a couple paths we could take here:

  1. Forbid certain element types. For example, since we already have a native Circle shape that plays well with optimization, we could forbid the definition Element C = circle (which would otherwise generate a raw <circle> element in the rendered SVG). This way, our handling of shape optimization is unchanged. A downside here, perhaps, is that we may lose some functionality in Circle (e.g., using an animated gradient) unless this shape definition is also augmented to allowed child Elements.
  2. Abandon shape-based library functions. We could also jettison all library functions that currently take a shape as an argument, and instead express constraints/objectives purely in terms of scalar attributes. E.g., tangentTo( circle1, circle2 ) would need to be written as ensure norm( circle1.center - circle2.center ) == circle1.r + circle2.r. We already moved somewhat in this direction with @liangyiliang's revision of the Style library to allow most functions to be invoked directly (rather than through shapes). However, in addition to making some common functions more verbose, this makes it a bit harder to do generic programming (e.g., tangentTo( x.icons, y.icons ), where the icons for x and y may range over a wide variety of shapes).
  3. Don't optimize Elements. This is perhaps a nice middle ground between the two above: we simply provide no Style functions that define constraints/objectives for Elements. So for instance, someone can write Element C = circle, but, similar to proposal (1) can't pass this element in to a function like disjoint. Similar to proposal (2), they can still write their own objectives/constraints in terms of the scalar attributes of the elements (e.g., the center and radius). This adds a bit of cruft to the language, since there are now two ways to specify a circle: one that can be optimized, and one that can't. But if this functionality is for "experts only," perhaps it's an ok middle ground.
  4. Give some Elements special treatment. For instance, if a Style program states disjoint( x.element, y.element ) we could do inference on the elementname (e.g., is it a circle?), sending it down the appropriate path if so. If attributes are specified that we do not support (e.g., the rotation of a rectangle), we can issue a warning that these attributes will be ignored for the purposes of layout. This path is perhaps the most "complete," but also requires the most development effort.

As much as this issue is important to resolve, it is a bit tangential to the main use case for Element: incorporating "pass through" SVG features that we don't yet support (such as gradients and animations). The goal is not really to replace our basic shape definitions, which already work fine. (Though perhaps if there's some elegant solution, it's worth rolling shape definitions into this design someday down the line, for simplicity's sake.)

Implementation

Implementation of a basic prototype shouldn't actually be so hard. A first step is to copy/paste/modify the existing Group shape, which already handles nesting. The main change that would need to be made to the parser is recognizing lines of the form

Element e = elementname

where elementname is an arbitrary string (perhaps within some pattern), rather than one of a list of predefined shapes. Fortunately, we already do this kind of parsing for SVG passthrough (i.e., a field can have an arbitrary, unquoted name, without needing to be listed a priori).

Interactions and Other Issues

There does not seem to be much interaction between this feature and other parts of the system, nor would it seem to cause any ambiguity for the parser. At its core, we're essentially just defining one more shape. (This shape just happens to be particularly flexible.)

Similar to SVG passthrough, SVG code generated in this way may not validate. Rather than place the validation burden on the Penrose compiler, Style programmers can simply rely on existing tools that do XML validation. A "convenience feature," perhaps, would be to integrate such validators with the IDE—but this is a somewhat orthogonal issue.

@keenancrane keenancrane changed the title feat: Support generic tag shape in Style feat: Add generic SVG element shape to Style Jul 5, 2024
@keenancrane
Copy link
Collaborator Author

For an experimental implementation, I would probably try for proposal (3) since this requires the least implementation effort. We simply don't provide constaints/objectives for Elements.

@keenancrane keenancrane changed the title feat: Add generic SVG element shape to Style feat: Add generic SVG Element shape to Style Jul 5, 2024
@keenancrane
Copy link
Collaborator Author

keenancrane commented Jul 5, 2024

@liangyiliang I wonder if the Group shape can safely be subsumed into Element? I.e., replace

shape myGroup = Group {
   shapes: [ shape1, shape2,  ]
}

with

Element myGroup = g {
   elements: [ shape1, shape2,  ]
}

(You would best know from the implementation side, given that you implemented groups.)

I guess the main counterargument is that it may hurt readability (g versus Group).

@keenancrane keenancrane changed the title feat: Add generic SVG Element shape to Style feat: Add generic SVG element shape to Style Jul 5, 2024
@keenancrane keenancrane changed the title feat: Add generic SVG element shape to Style feat: Add generic SVG Element shape to Style Jul 5, 2024
@keenancrane
Copy link
Collaborator Author

I also realize now that the proposed syntax sort of breaks with our existing syntax. For instance, we currently write things like

shape C = Circle { ... }

Would it make more sense to have something like

shape S = Element<elementname> { ... }

? For instance,

shape gradient = Element<linearGradient> {
   ...
}

This way we can still catch errors like

shape S = Square { ... }

where the user mistakenly tries to define a Penrose shape of type Square. If we don't catch this error, then we will end up emitting SVG code with a tag <Square> that also doesn't validate.

@liangyiliang
Copy link
Member

@liangyiliang I wonder if the Group shape can safely be subsumed into Element?

Arbitrary elements also won't have any pre-defined geometry (like bounding boxes), whereas Group does. At least that's under the assumption that we aren't adding any geometry into arbitrary elements.

@liangyiliang
Copy link
Member

I also realize now that the proposed syntax sort of breaks with our existing syntax. For instance, we currently write things like

shape C = Circle { ... }

Would it make more sense to have something like

shape S = Element<elementname> { ... }

? For instance,

shape gradient = Element<linearGradient> {
   ...
}

This way we can still catch errors like

shape S = Square { ... }

where the user mistakenly tries to define a Penrose shape of type Square. If we don't catch this error, then we will end up emitting SVG code with a tag <Square> that also doesn't validate.

I don't think that's problematic.

We can parse linearGradient { ... } as usual into GPIDecls.

But during compilation, we check whether or not linearGradient is a built-in shape. If not, then we know that it is a non-built-in element.

@liangyiliang
Copy link
Member

liangyiliang commented Jul 5, 2024

This way we can still catch errors like\n\nshape S = Square { ... }\nwhere the user mistakenly tries to define a Penrose shape of type Square. If we don't catch this error, then we will end up emitting SVG code with a tag \u003CSquare> that also doesn't validate.

We can still do that. The logic is,

if shapeType in built-in shape types:
  return built-in shape
else:
  if shapeType is actually a SVG tag:
    return non-built-in element
  else:
    return error

@keenancrane
Copy link
Collaborator Author

@liangyiliang I wonder if the Group shape can safely be subsumed into Element?

Arbitrary elements also won't have any pre-defined geometry (like bounding boxes), whereas Group does. At least that's under the assumption that we aren't adding any geometry into arbitrary elements.

Whoops of course—you still want to optimize Groups.

@keenancrane
Copy link
Collaborator Author

This way we can still catch errors like\n\nshape S = Square { ... }\nwhere the user mistakenly tries to define a Penrose shape of type Square. If we don't catch this error, then we will end up emitting SVG code with a tag \u003CSquare> that also doesn't validate.

We can still do that. The logic is,

if shapeType in built-in shape types:
  return built-in shape
else:
  if shapeType is actually a SVG tag:
    return non-built-in element
  else:
    return error

So, an important decision when we sis SVG passthrough was to not fo any checking on the field names. The issue that there are many versions and variants of SVG (and even moreso with HTML), and this puts a burden on us to manage that somehow. Likewise with element names.

So, I think overall it's gonna be more straightforward to not so explicit checking—but also make the declaration of an Element more explicit in Style.

@samestep
Copy link
Collaborator

samestep commented Jul 5, 2024

Just to make sure I understand correctly: is this not a subset of what Style would gain from having functions? #894

@liangyiliang
Copy link
Member

Just to make sure I understand correctly: is this not a subset of what Style would gain from having functions? #894

I think it's more expressive than that. As far as I know it is not currently possible to generate arbitrary SVG nodes in Style.

@liangyiliang
Copy link
Member

So, I think overall it's gonna be more straightforward to not so explicit checking—but also make the declaration of an Element more explicit in Style.

Makes sense to me!

@liangyiliang
Copy link
Member

@keenancrane Just want to make sure we are on the same page in terms of design - here are some design decisions.

First, should Element shapes be able to be parts of Group? In other words should I be able to write,

x = Element<linearGradient> {
  ...
}
y = Group {
  shapes: [x]
}

?

I think the answer should be "no" to maintain the conceptual consistency (which I think makes sense) that all built-in shapes (and hence all their children, if any) have geometry. Recall that at least for now, Element has no geometry.

Then, if I want to make a group that contains a non-geometric element, then the entire group must be declared as a non-built-in element, like

y = Element<g> {
  children: [x]
}

Second, should built-in shapes be able to be a children of a non-built-in element, like

x = Circle {}
y = Element<myTag> {
  children: [x]
}

?

I think the answer to this question is yes.

Finally, I think there is no harm for non-built-in elements to participate in layering, so we should allow that.

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