Skip to content

React Rendering Frontend

wodeni edited this page Oct 23, 2020 · 44 revisions

To develop for the React Rendering Frontend (at the time of this writing, located in penrose-web/), get set up with Typescript (usually npm install -g typescript) and Node.

Run npm install on first clone.

Run npm start to open a hot-reloading environment in the browser.

Working with the renderers

If you haven't done so yet, clone penrose-ide at the same level as penrose.

To rebuild after a pull or checkout

  • Run npm install in penrose-web
  • Run npm install in penrose-ide
  • Run npm install in substance-languageservice
  • Run npm run build-lib in penrose-web
  • Run npm link in penrose-web
  • Run npm link penrose-web in penrose-ide

After making changes to renderer while it's running

  • Save file, then it'll hot reload
  • Run npm run build-lib to load changes in penrose-web

Run either frontend

  • Run npm start in penrose-web or penrose-ide
  • For the IDE, you should concurrently run bash scripts/domain-server.sh in the penrose/ dir yourself. Doing this automatically is planned in the future.

Debug Logging

A scoped logger is available throughout the entire project. It's auto-enabled in the development context, and in production, can be enabled by setting the localstorage key debug to renderer:* (in addition to any other comma-separated debug scopes you want).

Use it by importing src/Log.ts e.g. import Log from "./Log";. Then you can use Log.info(message: string, source?: string), Log.warn(..), Log.error(..).

Using the Rendering Frontend as a Dependency (npm package)

First, you must build a static copy of the frontend by running npm run build-lib in the directory. This is different from npm run build because the latter builds everything as a standalone website bundle, while we only want the Javascript modules.

Next, you must link the built package to the global npm scope on your machine. Run npm link.

Next, navigate to the dependent's directory and run npm link penrose-web to allow for importing from the renderer.

Now, you can include the rendering frontend using import Renderer from "penrose-web";. Its API is as follows:

  • Prop: lock: boolean: disables user interactions (e,g drags). Useful for moments when the optimizer is busy.
  • Prop: sendPacket(packet: string): void: the canvas wants to send a WebSocket packet to the server. Passing it through is critical.
  • Method: onMessage(e: MessageEvent): void: You need to pass websocket onmessage events down yourself. Use a React ref and in your dependent, call the ref's current.onMessage(e) method.

If you make changes to the rendering frontend, you usually only have to repeat the step, npm run build-lib in the renderer directory to trigger a hot reload for your dependents.

Misc. Troubleshooting

If you Cannot find module 'penrose-web after installing a node module, or if you just restart your dependent's build process after a while and it suddenly can't resolve penrose-web, just run npm link penrose-web again and restart the hot reloading process. This also can work if your "websocket is stuck in connecting state".

Adding Shapes

Each shape (GPI) is represented as a typed React component. There is typically a 1:1 relationship between GPI and component. These relationships are defined in componentMap.tsx.

If you anticipate your shape extending simple dragging behavior a la Circle and Rectangle, your component should implement the IGPIPropsDraggable props found in types.tsx. Otherwise, you can implement the less complex parent, IGPIProps.

Both interfaces supply the [width, height] of the canvas, for convenience of coordinate system calculations (note: the width/height are not necessarily the true dimensions rendered on a page, they are just the common coordinate system of the SVG target). Optionally, the interfaces supply an onShapeUpdate event which, when called, sends a shape update packet directly back to the server. They also supply an optional dragEvent event, which sends a dy and dx back to the server if needed. The Draggable higher-order component uses this event.


For shapes implementing IGPIPropsDraggable, the implementer must accept an onClick event to signal the mouseDown start of a drag event (if your shape has a finnicky/hard to grab bounding box, look into SVG's pointerEvents API, or wrap your shape in a <g> with a bigger width/height, use pointerEvents="bounding-box", and apply the mouseDown event to the <g>).

To endow the shape with draggable behavior, in your export, compose the shape with draggable(...), the function exported in Draggable.tsx. For instance, export default draggable(Rectangle);.


Finally, for any shape, you must map the tag name to the component class in componentMap.tsx.

A sample shape, Circle, valid at this time of this writing:

import * as React from "react";
import { toScreen } from "./Util";
import draggable from "./Draggable";
import { IGPIPropsDraggable } from "./types";

class Circle extends React.Component<IGPIPropsDraggable> {
  public render() {
    const props = this.props.shape;
    const { onClick } = this.props;
    const { canvasSize } = this.props;
    const [x, y] = toScreen([props.x, props.y], canvasSize);
    const color = props.color[0];
    const alpha = props.color[1];
    return (
      <circle
        cx={x}
        cy={y}
        r={props.r}
        fill={color}
        fillOpacity={alpha}
        onMouseDown={onClick}
      />
    );
  }
}
export default draggable(Circle);

And its appropriate mapping in componentMap.tsx:

// Map between "tag" and corresponding component
import Circle from "./Circle";
import Label from "./Label";
import Rectangle from "./Rectangle";

// prettier-ignore
const componentMap = {
+  "Circle": Circle,
  "Rectangle": Rectangle,
  "Text": Label
};

export default componentMap;

See also: Common TypeScript Gotchas, Debugging Graphically

Clone this wiki locally