Draw substantially more of the jolly owl
This commit is contained in:
parent
6165c10a6c
commit
f5a04ff608
24
index.html
24
index.html
|
@ -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>
|
||||
|
||||
|
|
282
src/index.ts
282
src/index.ts
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
225
src/pict.ts
225
src/pict.ts
|
@ -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();
|
||||
}
|
||||
|
|
90
style.css
90
style.css
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue