Draw substantially more of the jolly owl

This commit is contained in:
Tony Garnock-Jones 2022-01-26 17:16:32 +01:00
parent 6165c10a6c
commit f5a04ff608
4 changed files with 514 additions and 107 deletions

View File

@ -15,6 +15,30 @@
<p>Interaction Diagrams for Syndicated Actors</p>
<div id="container">
<svg id="playground" viewBox="0 0 100 100" width="100" height="100">
<defs>
<marker id="arrowhead" markerWidth="15" markerHeight="15" refX="3.75" refY="7.5"
orient="auto-start-reverse">
<polygon points="0 0, 15 7.5, 0 15, 3.75 7.5, 0 0" />
</marker>
<linearGradient id="swimlane-fadeout" x1="0" x2="0" y1="0" y2="1">
<stop stop-color="var(--lifeline-color)" stop-opacity="1" offset="0%"/>
<stop stop-color="var(--lifeline-color)" stop-opacity="0" offset="100%"/>
</linearGradient>
</defs>
<!-- <path d="M 0 30 c 17.7 17.7 232.3 -17.7 250 0" -->
<!-- stroke-width="2" -->
<!-- fill="none" -->
<!-- stroke="#000" -->
<!-- marker-end="url(#arrowhead)"/> -->
<!-- <line x1="0" y1="50" x2="250" y2="50" stroke="#000" stroke-width="2" marker-end="url(#arrowhead)" /> -->
<!-- <path d="M16.7,178 c87.6-46.9,162.9-185.4,227-136.4C307.2,90.1,195,158.5,111,108.9C71,85.2,92.2,30.7,126,7" -->
<!-- stroke-width="2" -->
<!-- fill="none" -->
<!-- stroke="#000" -->
<!-- marker-end="url(#arrowhead)"/> -->
<g id="underlay"></g>
<g id="overlay"></g>

View File

@ -1,18 +1,23 @@
/// SPDX-License-Identifier: GPL-3.0-or-later
/// SPDX-FileCopyrightText: Copyright © 2022 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
import { Dataspace, Cap, Value, IdentitySet, Ref, Bytes, Observe, Turn, Reader, genericEmbeddedTypeDecode, Schemas, GenericEmbedded, Dictionary, fromJS } from "@syndicate-lang/core";
import { Dataspace, IdentitySet, Ref, Bytes, Observe, Turn, Reader, genericEmbeddedTypeDecode, Schemas, GenericEmbedded, Dictionary, fromJS } from "@syndicate-lang/core";
import { QuasiValue as Q } from "@syndicate-lang/core";
const Trace = Schemas.trace;
import * as P from './pict';
import { boot as bootHtml, Anchor, makeTemplate, UIEvent, UIFragment } from "@syndicate-lang/html";
// const svg = makeTemplate('svg');
import { boot as bootHtml, UIFragment } from "@syndicate-lang/html";
assertion type URLContent(url: string, content: Bytes | false);
const SWIMLANE_WIDTH = 100;
const LIFELINE_WIDTH = 10;
const LIFELINE_WIDTH = 5;
const ACTIVATION_WIDTH = LIFELINE_WIDTH * 2;
const VERTICAL_GAP = 20;
const POST_TURN_GAP = VERTICAL_GAP * 3;
const PLAYGROUND_PADDING = 10;
const TAIL_LENGTH = 60;
const TERMINATOR_SIZE = new P.Point(LIFELINE_WIDTH * 3, LIFELINE_WIDTH);
export function main() {
Dataspace.boot(ds => {
@ -57,14 +62,6 @@ function bootRenderer(ds: Ref) {
spawn named 'renderer' {
const r = new Renderer(ds);
at ds {
// const ui = new Anchor();
// field y: number = 50;
// assert ui.html('#playground', svg`<text y="${y.value}">foo</text>`);
// on message UIEvent(ui.fragmentId, '.', 'click', $_e) => {
// y.value += 20;
// }
// // Turn.active.every(1000 / 30.0, () => y.value += 5);
const traceUrl = 'd2';
during URLContent(traceUrl, false) => {
alert("Couldn't fetch trace file: " + JSON.stringify(traceUrl));
@ -89,10 +86,10 @@ class Swimlane {
readonly renderer: Renderer;
readonly laneNumber: number;
readonly startY: number;
readonly entries: Array<Schemas.trace.TraceEntry<GenericEmbedded>> = [];
readonly entries: Array<P.BoundedPict<any>> = [];
readonly lifeline = P.rect([LIFELINE_WIDTH, 0], {"class": "lifeline"});
// readonly lifeline = svg.tag(SVGRectElement)`<rect class="lifeline"/>`;
tail: P.BoundedPict<SVGRectElement> | null =
P.rect([LIFELINE_WIDTH, TAIL_LENGTH], {"class": "lifeline-tail"});
constructor (actorId: Schemas.trace.ActorId<GenericEmbedded>, laneNumber: number, renderer: Renderer) {
this.actorId = actorId;
@ -100,37 +97,47 @@ class Swimlane {
this.laneNumber = laneNumber;
this.startY = renderer.ypos;
new P.Point(this.midX - LIFELINE_WIDTH / 2, this.startY).apply(this.lifeline);
this.renderer.ypos += 10;
this.lifeline.setAttribute('height', '' + (this.renderer.ypos - this.startY));
this.renderer.underlay.appendChild(this.lifeline);
this.lifeline.updateBounds(b => b.withTopMid([this.midX, this.startY]));
this.updateLifelineHeight();
this.renderer.underlay.appendChild(this.lifeline.node);
this.renderer.underlay.appendChild(this.tail!.node);
}
updateLifelineHeight() {
this.lifeline.updateBounds(b => b.withHeight(this.renderer.ypos - b.top));
this.tail?.updateBounds(b => b.withTopMid([this.midX, this.renderer.ypos]));
}
get midX(): number {
return this.laneNumber * SWIMLANE_WIDTH;
}
addTraceEntry(entry: Schemas.trace.TraceEntry<GenericEmbedded>) {
addEntry(klass: string | null, entry: P.BoundedPict<any>): P.BoundedPict<any> {
this.entries.push(entry);
// const n = this.renderer.placard(
// 'test1', this.renderer.textNode('label', fromJS(entry.item).asPreservesText()));
// const b = this.renderer.measureBBox(n);
// n.setAttribute('x', '' + (this.laneNumber * SWIMLANE_WIDTH - b.width / 2));
// n.setAttribute('y', '' + this.renderer.ypos);
const n = P.placard(P.text(fromJS(entry.item).asPreservesText(), {"class": "label"}),
{"class": "test1"});
const b = P.bbox(n);
b.withTopMid([this.midX, this.renderer.ypos]).apply(n);
this.renderer.ypos += b.height + 10;
this.renderer.overlay.appendChild(n);
this.lifeline.setAttribute('height', '' + (this.renderer.ypos - this.startY));
if (entry.item._variant === "stop") {
this.renderer.releaseSwimlane(this.laneNumber);
if (klass !== null) {
const pin = P.circle(2, {"class": "pin"});
pin.updateBounds(b => b.withTopMid([this.midX, this.renderer.ypos]));
this.renderer.advance(pin.bounds.height);
this.renderer.overlay.appendChild(pin.node);
}
const n = klass === null ? entry : P.placard(entry, {"class": klass});
n.updateBounds(b => b.withTopMid([this.midX, this.renderer.ypos]));
this.renderer.overlay.appendChild(n.node);
this.renderer.advance(n.bounds.height);
this.updateLifelineHeight();
this.renderer.adjustViewport();
return n;
}
finish() {
this.renderer.underlay.removeChild(this.tail!.node);
this.tail = null;
this.renderer.releaseSwimlane(this.laneNumber);
}
}
@ -140,8 +147,9 @@ class Renderer {
readonly playground: SVGSVGElement = document.getElementById('playground')! as any;
readonly underlay: SVGGElement = this.playground.getElementById('underlay')! as any;
readonly overlay: SVGGElement = this.playground.getElementById('overlay')! as any;
readonly swimlanes: Dictionary<GenericEmbedded, Swimlane> = new Dictionary();
readonly swimlanes = new Dictionary<GenericEmbedded, Swimlane>();
readonly busyLanes = new IdentitySet<number>();
readonly turnMap = new Dictionary<GenericEmbedded, P.Point>();
ypos = 0;
constructor (ds: Ref) {
@ -155,44 +163,14 @@ class Renderer {
}
}
// temporarilyInDom<R>(e: SVGElement, f: () => R): R {
// this.playground.appendChild(e);
// const result = f();
// this.playground.removeChild(e);
// return result;
// }
// measureClientRect(e: SVGElement) {
// return this.temporarilyInDom(e, () => e.getBoundingClientRect());
// }
// measureBBox(e: SVGGraphicsElement) {
// return this.temporarilyInDom(e, () => e.getBBox());
// }
// measureTextLength(e: SVGTextContentElement) {
// return this.temporarilyInDom(e, () => e.getComputedTextLength());
// }
// textNode(klass: string, text: string): SVGTextElement {
// return svg.tag(SVGTextElement)`<text class="${klass}">${text}</text>`;
// }
// placard(klass: string, n: SVGGraphicsElement): SVGSVGElement {
// const b = this.measureBBox(n);
// const [px, py] = [12, 6];
// const base = svg.tag()`<rect class="${klass}" width="${b.width + px * 2}" height="${b.height + py * 2}"/>`;
// return svg.tag(SVGSVGElement)`<svg class="placard">${base}<g transform="translate(${px} ${py})">${n}</g></svg>`;
// }
adjustViewport() {
const bbox = this.playground.getBBox();
this.playground.viewBox.baseVal.x = bbox.x - 10;
this.playground.viewBox.baseVal.y = bbox.y - 10;
this.playground.viewBox.baseVal.width = bbox.width + 20;
this.playground.viewBox.baseVal.height = bbox.height + 20;
this.playground.width.baseVal.value = bbox.width + 20;
this.playground.height.baseVal.value = bbox.height + 20;
this.playground.viewBox.baseVal.x = bbox.x - PLAYGROUND_PADDING;
this.playground.viewBox.baseVal.y = bbox.y - PLAYGROUND_PADDING;
this.playground.viewBox.baseVal.width = bbox.width + PLAYGROUND_PADDING * 2;
this.playground.viewBox.baseVal.height = bbox.height + PLAYGROUND_PADDING * 2;
this.playground.width.baseVal.value = bbox.width + PLAYGROUND_PADDING * 2;
this.playground.height.baseVal.value = bbox.height + PLAYGROUND_PADDING * 2;
}
allocateSwimlane(): number {
@ -215,7 +193,163 @@ class Renderer {
return s;
}
advance(delta: number) {
this.ypos += delta;
}
connect(ps: P.Point, pe: P.Point) {
if (pe.x == ps.x) {
const pullMagnitude = pe.sub(ps).scale(0.1).length;
const pullAngle = Math.PI;
const pull = P.Point.rTheta(pullMagnitude + 30, pullAngle);
// TODO: compute marker size from the actual definition
const arrowheadSpace = P.Point.rTheta(15 - 3.75, pullAngle);
this.overlay.appendChild(
P.cubicBezier(ps,
ps.add(pull),
pe.add(pull),
pe.add(arrowheadSpace),
{"class": "arrow"}).node);
} else {
const pullMagnitude = pe.sub(ps).scale(0.1).length;
const pullAngle = pe.x > ps.x ? Math.PI/4 : 3*Math.PI/4;
const pull = P.Point.rTheta(pullMagnitude, pullAngle);
// TODO: compute marker size from the actual definition
const arrowheadSpace = P.Point.rTheta(15 - 3.75, pullAngle);
const pe1 = pe.sub(arrowheadSpace);
this.overlay.appendChild(
P.cubicBezier(ps,
ps.add(pull),
pe1.sub(pull),
pe1,
{"class": "arrow"}).node);
}
}
rewindTo<R>(pos: number, f: () => R): R {
const saved = this.ypos;
this.ypos = pos;
try {
return f();
} finally {
this.ypos = saved;
}
}
addTraceEntry(entry: Schemas.trace.TraceEntry<GenericEmbedded>) {
this.swimlaneFor(entry.actor).addTraceEntry(entry);
switch (entry.item._variant) {
case "start": {
this.advance(LIFELINE_WIDTH);
const swimlane = this.swimlaneFor(entry.actor);
swimlane.addEntry(null, P.rect(TERMINATOR_SIZE, {"class": "terminator"}));
this.advance(LIFELINE_WIDTH);
swimlane.addEntry("start", P.text(fromJS(entry.item).asPreservesText()));
break;
}
case "turn": {
const swimlane = this.swimlaneFor(entry.actor);
const activation = P.rect([ACTIVATION_WIDTH, 0], {"class": "activation"});
this.advance(LIFELINE_WIDTH);
activation.updateBounds(b => b.withTopMid([swimlane.midX, this.ypos]));
let causingTurnId: Schemas.trace.TurnId<GenericEmbedded> | null = null;
switch (entry.item.value.cause._variant) {
case "turn":
causingTurnId = entry.item.value.cause.id;
break;
case "delay":
causingTurnId = entry.item.value.cause.causingTurn;
break;
default:
break;
}
if (causingTurnId !== null) {
this.connect(this.turnMap.get(fromJS(causingTurnId))!,
new P.Point(swimlane.midX, this.ypos));
}
this.advance(LIFELINE_WIDTH);
swimlane.addEntry("turn-start", P.text(
entry.item.value.id.asPreservesText() + ': ' + fromJS(entry.item.value.cause).asPreservesText()));
entry.item.value.actions.forEach(a => {
let entryClass: string;
switch (a._variant) {
case "dequeue":
case "dequeueInternal":
entryClass = "event"; break;
case "enqueue":
case "enqueueInternal":
case "link":
case "spawn":
entryClass = "action"; break;
case "facetStart":
case "facetStop":
entryClass = "internal"; break;
case "linkedTaskStart":
entryClass = "start"; break;
}
this.advance(LIFELINE_WIDTH);
const fPos = this.ypos;
const e = swimlane.addEntry(entryClass, P.text(fromJS(a).asPreservesText()));
switch (a._variant) {
case "link": {
const other = this.swimlaneFor(a.childActor);
const f = this.rewindTo(fPos, () =>
other.addEntry(entryClass, P.text(
`linked to ${fromJS(a.parentActor).asPreservesText()}`)));
// TODO: compute marker size from the actual definition
const arrowheadSpace = new P.Point(
(15 - 3.75) * (other.midX > swimlane.midX ? -1 : 1),
0);
const ps = other.midX > swimlane.midX
? e.bounds.midRight.sub(arrowheadSpace)
: e.bounds.midLeft.sub(arrowheadSpace);
const pe = other.midX > swimlane.midX
? f.bounds.midLeft.add(arrowheadSpace)
: f.bounds.midRight.sub(arrowheadSpace);
this.overlay.appendChild(P.line(ps, pe, {"class": "doublearrow"}).node);
break;
}
case "spawn": {
if (!a.link) {
const other = this.swimlaneFor(a.id);
this.connect(new P.Point(swimlane.midX, this.ypos),
new P.Point(other.midX, this.ypos));
}
break;
}
default:
break;
}
});
this.advance(LIFELINE_WIDTH);
this.turnMap.set(fromJS(entry.item.value.id), new P.Point(swimlane.midX, this.ypos));
activation.updateBounds(b => b.withHeight(this.ypos - b.top));
this.underlay.appendChild(activation.node);
this.advance(POST_TURN_GAP - VERTICAL_GAP);
swimlane.updateLifelineHeight();
break;
}
case "stop": {
this.advance(VERTICAL_GAP);
const swimlane = this.swimlaneFor(entry.actor);
switch (entry.item.status._variant) {
case "ok":
swimlane.addEntry("stop-ok", P.text("✓"));
break;
case "Error":
swimlane.addEntry("stop-error", P.text(
"ERROR: " + fromJS(entry.item.status.value).asPreservesText(),
{"class": "stop-error"}));
break;
}
this.advance(LIFELINE_WIDTH);
swimlane.addEntry(null, P.rect(TERMINATOR_SIZE, {"class": "terminator"}));
this.advance(LIFELINE_WIDTH);
swimlane.finish();
break;
}
}
}
}

View File

@ -26,10 +26,18 @@ export class Point {
}
}
static rTheta(r: number, theta /* radians */: number): Point {
return new Point(r * Math.cos(theta), r * Math.sin(theta));
}
toDOM(): DOMPoint {
return new DOMPoint(this.x, this.y);
}
get length(): number {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
add(p0: Pointlike): Point {
const p = Point.from(p0);
return new Point(this.x + p.x, this.y + p.y);
@ -51,10 +59,6 @@ export class Point {
return new Point(Math.min(this.x, p.x), Math.min(this.y, p.y));
}
apply<N extends SVGElement>(n: N): N {
return applyAttributes(n, { "x": this.x, "y": this.y });
}
withX(x: number): Point { return new Point(x, this.y); }
withY(y: number): Point { return new Point(this.x, y); }
@ -170,13 +174,158 @@ export class Rect {
return new Rect(p.x - this.width, p.y - this.height / 2, this.width, this.height);
}
apply<N extends SVGElement>(n: N): N {
return applyAttributes(n, {
"x": this.x,
"y": this.y,
"width": this.width,
"height": this.height,
union(r: Rect): Rect {
const x = Math.min(this.x, r.x);
const y = Math.min(this.y, r.y);
const w = Math.max(this.right, r.right) - x;
const h = Math.max(this.bottom, r.bottom) - y;
return new Rect(x, y, w, h);
}
}
export abstract class Pict<E extends SVGGraphicsElement> {
readonly node: E;
constructor (node: E) {
this.node = node;
}
abstract get bounds(): Rect;
}
export class BoundedPict<E extends SVGGraphicsElement> extends Pict<E> {
_bounds: Rect;
constructor (node: E, bounds?: Rectlike) {
super(node);
this._bounds = Rect.from(bounds ?? bbox(node));
}
get bounds(): Rect { return this._bounds; }
set bounds(r: Rect) {
const oldBounds = this._bounds;
this._bounds = r;
this.applyBounds(oldBounds);
}
updateBounds(f: (b: Rect) => Rect): this {
this.bounds = f(this.bounds);
return this;
}
applyBounds(_oldBounds: Rect) {
const b = this.bounds;
applyAttributes(this.node, { "x": b.x, "y": b.y, "width": b.width, "height": b.height });
}
}
export class LinelikePict<E extends SVGGeometryElement> extends Pict<E> {
_start: Point;
_end: Point;
constructor (node: E, start?: Pointlike, end?: Pointlike) {
super(node);
this._start = Point.from(start ?? Point.ZERO);
this._end = Point.from(end ?? Point.ZERO);
}
get bounds(): Rect { return bbox(this.node); }
get start(): Point { return this._start; }
set start(p: Pointlike) {
this._start = Point.from(p);
this.applyPoints();
}
get end(): Point { return this._end; }
set end(p: Pointlike) {
this._end = Point.from(p);
this.applyPoints();
}
applyPoints(): this {
applyAttributes(this.node, {
"x1": this._start.x,
"y1": this._start.y,
"x2": this._end.x,
"y2": this._end.y,
});
return this;
}
}
export class CubicBezierPict extends LinelikePict<SVGPathElement> {
_c1: Point;
_c2: Point;
constructor (
n: SVGPathElement,
start: Pointlike,
c1: Pointlike,
c2: Pointlike,
end: Pointlike,
) {
super(n, start, end);
this._c1 = Point.from(c1);
this._c2 = Point.from(c2);
}
get c1(): Point { return this._c1; }
set c1(p: Pointlike) {
this._c1 = Point.from(p);
this.applyPoints();
}
get c2(): Point { return this._c2; }
set c2(p: Pointlike) {
this._c2 = Point.from(p);
this.applyPoints();
}
applyPoints(): this {
applyAttributes(this.node, {
"d": `M ${this._start.x} ${this._start.y}
C ${this._c1.x} ${this._c1.y}
${this._c2.x} ${this._c2.y}
${this._end.x} ${this._end.y}`,
});
return this;
}
}
export class EllipsePict extends BoundedPict<SVGEllipseElement> {
applyBounds(_oldBounds: Rect) {
const c = this.bounds.center;
const r = c.sub(this.bounds.topLeft);
applyAttributes(this.node, { "cx": c.x, "cy": c.y, "rx": r.x, "ry": r.y });
}
}
export class PictGroup extends BoundedPict<SVGGElement> {
readonly children: BoundedPict<any>[] = [];
constructor() {
super(new SVGGElement());
}
add(p: BoundedPict<any>) {
this.children.push(p);
this.node.appendChild(p.node);
p.updateBounds(this.place);
if (this.children.length === 1) {
this._bounds = p.bounds;
} else {
this._bounds = this._bounds.union(p.bounds);
}
}
applyBounds(_oldBounds: Rect) {
const p = this.bounds.topLeft;
applyAttributes(this.node, { "transform": `translate(${p.x} ${p.y})` });
}
place(r: Rect): Rect {
return r;
}
}
@ -243,31 +392,55 @@ export function applyAttributes<N extends Element>(n: N, attributes?: Attributes
return n;
}
export function text(text: string, attributes?: Attributes): SVGTextElement {
return applyAttributes(svg.tag(SVGTextElement)`<text>${text}</text>`, attributes);
export function text(text: string, attributes?: Attributes): BoundedPict<SVGTextElement> {
return new BoundedPict(applyAttributes(svg.tag(SVGTextElement)`<text>${text}</text>`, attributes));
}
export function rect(extent0: Pointlike = Point.ZERO, attributes?: Attributes): SVGRectElement {
export function rect(extent0: Pointlike = Point.ZERO, attributes?: Attributes): BoundedPict<SVGRectElement> {
const extent = Point.from(extent0);
return applyAttributes(
svg.tag(SVGRectElement)`<rect width="${extent.x}" height="${extent.y}"/>`,
attributes);
return new BoundedPict(
applyAttributes(svg.tag(SVGRectElement)`<rect width="${extent.x}" height="${extent.y}"/>`,
attributes),
Rect.originExtent(Point.ZERO, extent));
}
export function circle(radius: number, attributes?: Attributes): SVGCircleElement {
return applyAttributes(svg.tag(SVGCircleElement)`<circle r="${radius}"/>`, attributes);
export function circle(radius: number, attributes?: Attributes): EllipsePict {
return new EllipsePict(applyAttributes(
svg.tag(SVGEllipseElement)`<ellipse rx="${radius}" ry="${radius}"/>`,
attributes));
}
export function alignTo(a: SVGGraphicsElement, n: Anchor, b: SVGGraphicsElement) {
n(bbox(a)).set(n(bbox(b)).get()).apply(a);
export function alignTo(a1: Anchor, p1: BoundedPict<any>, a2: Anchor, p2: BoundedPict<any>) {
p1.bounds = a1(p1.bounds).set(a2(p2.bounds).get());
}
export function placard(n: SVGGraphicsElement, attributes?: Attributes): SVGSVGElement {
export function placard(p: BoundedPict<any>, attributes?: Attributes): BoundedPict<SVGSVGElement> {
const padding = new Point(12, 6);
const base = rect(bbox(n).extent.add(padding.scale(2)), attributes);
return svg.tag(SVGSVGElement)`
const base = rect(p.bounds.extent.add(padding.scale(2)), attributes);
p.bounds = p.bounds.withTopLeft(padding);
return new BoundedPict(svg.tag(SVGSVGElement)`
<svg class="placard">
${base}
<g transform="translate(${padding.x} ${padding.y})">${n}</g>
</svg>`;
${base.node}
${p.node}
</svg>`, base.bounds);
}
export function line(
start?: Pointlike,
end?: Pointlike,
attributes?: Attributes,
): LinelikePict<SVGLineElement> {
const n = applyAttributes(svg.tag(SVGLineElement)`<line/>`, attributes)
return new LinelikePict(n, start, end).applyPoints();
}
export function cubicBezier(
s: Pointlike,
c1: Pointlike,
c2: Pointlike,
e: Pointlike,
attributes?: Attributes,
): CubicBezierPict {
const n = applyAttributes(svg.tag(SVGPathElement)`<path/>`, attributes);
return new CubicBezierPict(n, s, c1, c2, e).applyPoints();
}

View File

@ -1,3 +1,17 @@
:root {
--lifeline-color: #bebebe;
--lifeline-start-color: #98fb98;
--lifeline-stop-ok-color: #98fb98;
--lifeline-stop-error-color: #ff4400;
--lifeline-stop-error-text-color: #ffffff;
--event-color: #ffa500;
--action-color: #ffffff;
--internal-color: #e0e0e0;
--activation-color: #ffffff;
--turn-start-color: var(--event-color);
--arrow-color: #000000;
}
html {
box-sizing: border-box;
}
@ -21,8 +35,9 @@ body {
}
body > p {
padding: 0 0.33em;
margin: 0.33em 0;
padding: 0.33em 0.33em;
margin: 0;
border-bottom: solid black 1px;
}
body > div {
@ -34,25 +49,86 @@ body > div {
svg {
background: white;
font-family: monospace;
border-top: solid black 1px;
min-width: 100%;
min-height: 100%;
}
svg rect.lifeline {
fill: #ddd;
fill: var(--lifeline-color);
}
svg text.label {
svg rect.terminator {
fill: var(--lifeline-color);
}
svg rect.lifeline-tail {
fill: url(#swimlane-fadeout);
}
svg rect.activation {
fill: var(--activation-color);
stroke: #000;
}
svg text {
white-space: pre;
dominant-baseline: text-before-edge;
}
rect.test1 {
fill: #fff;
svg rect.start {
fill: var(--lifeline-start-color);
stroke: #000;
}
svg rect.stop-ok {
fill: var(--lifeline-stop-ok-color);
stroke: #000;
}
svg rect.stop-error {
fill: var(--lifeline-stop-error-color);
stroke: #000;
}
svg text.stop-error {
fill: var(--lifeline-stop-error-text-color);
stroke: none;
}
svg rect.event {
fill: var(--event-color);
stroke: #000;
}
svg rect.turn-start {
fill: var(--turn-start-color);
stroke: #000;
}
svg rect.action {
fill: var(--action-color);
stroke: #000;
}
svg rect.internal {
fill: var(--internal-color);
stroke: #000;
}
svg.placard {
overflow: visible;
}
svg .arrow {
fill: none;
stroke: var(--arrow-color);
stroke-width: 1;
marker-end: url(#arrowhead);
}
svg .doublearrow {
fill: none;
stroke: var(--arrow-color);
stroke-width: 1;
marker-start: url(#arrowhead);
marker-end: url(#arrowhead);
}