React Rendering Frontend
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.
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 inpenrose-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 thepenrose/
dir yourself. Doing this automatically is planned in the future.
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(..)
.
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 websocketonmessage
events down yourself. Use a React ref and in your dependent, call the ref'scurrent.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.
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".
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
Found a problem or got a suggestion? Please open a GitHub issue and tag it with documentation
!