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

Guidance on a hyperbolic drawing extension #416

Open
Vectornaut opened this issue Feb 7, 2021 · 0 comments
Open

Guidance on a hyperbolic drawing extension #416

Vectornaut opened this issue Feb 7, 2021 · 0 comments

Comments

@Vectornaut
Copy link

Vectornaut commented Feb 7, 2021

I'd like to package and publish the submodule of this project that draws things on a view of the hyperbolic plane. It currently uses Compose as a backend, because:

  • I like Compose's declarative style.
  • This makes it easy to include hyperbolic drawings in a larger Compose layout.

One of my goals for the package is to integrate my hyperbolic drawing features more smoothly into the existing Compose interface. I'd love to have guidance from the Compose community on how to do this (and whether it's a sensible thing to do). With that goal in mind, I have a more specific question about Compose's internals below.

If this issue tracker is the wrong place for this discussion, please redirect me to the right one!

Subtyping Context to manage views

Conceptually, each view of the hyperbolic plane gives its own way to turn hyperbolic forms into Euclidean ones that can be drawn on a page. Correspondingly, I'm inclined to represent each view as a subtype of Context which takes hyperbolic forms as its children, and specifies how to turn them into ordinary Compose forms. A usage mock-up is shown below.

Can I?

Can a subtype of Context do this? Maybe all it would take is a new dispatch rule for compose!, like this:

# PoincaréDiskContext <: HyperbolicView <: Container
# HyperbolicContext <: Container
# HyperbolicFormPrimitive <: FormPrimitive

function compose!(a::PoincaréDiskView, b::Form{P}) where P <: HyperbolicFormPrimitive
  # use the poincaré disk model to turn `b` into an ordinary form
  a.form_children = cons(applyview(a, b), a.form_children)
  return a
end

function compose!(a::PoincaréDiskView, b::HyperbolicContext)
  # apply the view recursively to the children of `b`
  a.container_children = cons(applyview(a, b), a.container_children)
  return a
end

compose!(a::Context, b::Form{P}) where P <: HyperbolicFormPrimitive
  error(@sprintf("Invalid composition: hyperbolic primitives only nest inside HyperbolicView and HyperbolicContext containers."))
end

compose!(a::Context, b::HyperbolicContext)
  error(@sprintf("Invalid composition: HyperbolicContext only nests inside HyperbolicView and HyperbolicContext."))
end

Does that seem like a sensible approach? Would any functions other than compose! need new dispatch rules? Should the type structure be revised? (For example, maybe it's better to not make HyperbolicForm a subtype of Form).

Should I?

Some features of Compose, like the transformation system, carry over nicely to the hyperbolic plane. However, I'm worried that other parts of this hyperbolic drawing extension would go against the spirit of Compose. For example, the composition operators hstack and vstack, don't make much sense in the hyperbolic context, and the HyperbolicView contexts described above affect the composition tree in a weird way. Have I chosen a good way to integrate hyperbolic drawing into Compose? Is that even a sensible thing to do?

Mock-up

# UpperHalfPlanePoint <: HyperbolicPoint
a = UpperHalfPlanePoint([im, 1])
b = UpperHalfPlanePoint([cis/3), 1])
c = UpperHalfPlanePoint([1, 0])

# HyperbolicView <: Container
# typeof(uhp_view) == UpperHalfPlaneContext <: HyperbolicView
# typeof(disk_view) == PoincaréDiskContext <: HyperbolicView
uhp_view = hyperbolicview(model=UpperHalfPlane(-1, 1, 2))
disk_view = hyperbolicview(model=PoincaréDisk())

# the contexts `uhp_view` and `disk_view` specify different ways to turn
# the hyperbolic `line` and `polygon` forms into ordinary `curve` and
# `bezigon` forms. `hypcontext` creates a HyperbolicContext
uhp_img = compose(uhp_view,
  (hypcontext(), line([a, b]), stroke("black")),
  (hypcontext(), polygon([a, b, c]), fill("deepskyblue"))
)
disk_img = compose(disk_view,
  (hypcontext(), line([a, b]), stroke("black")),
  (hypcontext(), polygon([a, b, c]), fill("deepskyblue"))
)

# since hyperbolic views are a kind of context, we can compose them as usual
img = compose(context(),
  hstack(
    compose(context(0.1, 0.1, 0.8, 0.8), rectangle(), stroke("red"), uhp_img),
    compose(context(0.1, 0.1, 0.8, 0.8), circle(), stroke("red"), uhp_img)
  )
)
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

1 participant