sclass BaseBase { S text; S textForRender() { ret text; } } sclass Base extends BaseBase { } sclass CirclesAndLines { new L circles; new L lines; Class arrowClass = Arrow; Class circleClass = Circle; transient Lock lock = fairLock(); transient S defaultImageID = #1007372; // auto-visualize Circle circle_autoVis(S text, S visualizationText, double x, double y) { ret addAndReturn(circles, nu(circleClass, +x, +y, +text, img := quickVisualizeOr(visualizationText, defaultImageID))); } S makeVisualizationText(S text) { ret possibleGlobalID(text) ? "" : text; } Circle circle_autoVis(S text, double x, double y) { ret circle_autoVis(text, makeVisualizationText(text), x, y); } Circle circle(BufferedImage img, double x, double y, S text) { ret addAndReturn(circles, nu(circleClass, +x, +y, +text, +img)); } Circle circle(S text, double x, double y) { ret addAndReturn(circles, nu(circleClass, +x, +y, +text, img := loadImage2(#1007452)); // white } Circle addCircle(S imageID, double x, double y) { ret addCircle(imageID, x, y, ""); } Circle addCircle(S imageID, double x, double y, S text) { ret addAndReturn(circles, nu(circleClass, +x, +y, +text, img := loadImage2(imageID))); } Arrow findArrow(Circle a, Circle b) { for (Line l : getWhere(lines, +a, +b)) if (l instanceof Arrow) ret (Arrow) l; null; } Line addLine(Circle a, Circle b) { Line line = findWhere(lines, +a, +b); if (line == null) lines.add(line = nu(Line, +a, +b)); ret line; } Arrow arrow(Circle a, S text, Circle b) { ret addArrow(a, b, text); } Arrow addArrow(Circle a, Circle b) { ret addArrow(a, b, ""); } Arrow addArrow(Circle a, Circle b, S text) { ret addAndReturn(lines, nu(arrowClass, +a, +b, +text)); } BufferedImage makeImage(int w, int h) { BufferedImage bg = renderTiledBackground(#1007195, w, h); if (!lock.tryLock()) null; try { for (Line l : lines) { Pt a = l.a.pt(w, h), b = l.b.pt(w, h); if (l << Arrow) drawThoughtArrow(bg, l.a.img, a.x, a.y, l.b.img, b.x, b.y, l.color); else drawThoughtLine(bg, l.a.img, a.x, a.y, l.b.img, b.x, b.y, l.color); S text = l.textForRender(); if (nempty(text)) drawThoughtLineText(bg, l.a.img, a.x, a.y, l.b.img, b.x, b.y, text, l.color); } for (Circle c : circles) { Pt p = c.pt(w, h); drawThoughtCircle(bg, c.img, p.x, p.y); S text = c.textForRender(); if (nempty(text)) drawThoughtCircleText(bg, c.img, p, text); } } finally { lock.unlock(); } ret bg; } Canvas showAsFrame(int w, int h) { Canvas canvas = showAsFrame(); frameInnerSize(canvas, w, h); centerFrame(getFrame(canvas)); ret canvas; } Canvas showAsFrame() { ret (Canvas) swing(func { Canvas canvas = makeCanvas(); showCenterFrame(canvas); ret canvas; }); } Canvas makeCanvas() { fO makeImg = func(int w, int h) { makeImage(w, h) }; final Canvas canvas = jcanvas(makeImg); disableImageSurfaceSelector(canvas); new CircleDragger(canvas, r { updateCanvas(canvas, makeImg) }, circles); ret canvas; } Canvas show() { ret showAsFrame(); } Canvas show(int w, int h) { ret showAsFrame(w, h); } Circle findCircle(S text) { for (Circle c : circles) if (eqic(c.text, text)) ret c; null; } void clear { lines.clear(); circles.clear(); } Circle findCircle(Pt p) { new Lowest best; for (Circle c : circles) if (c.contains(is, p)) best.put(c, pointDistance(p, c.pt(is))); ret best.get(); } } sclass Circle extends Base { transient BufferedImage img; double x, y; Pt pt(ImageSurface is) { ret pt(is.getWidth(), is.getHeight()); } Pt pt(int w, int h) { ret new Pt(iround(x*w), iround(y*h)); } bool contains(ImageSurface is, Pt p) { ret pointDistance(p, pt(is)) <= thoughtCircleSize(img)/2+1; } } sclass Line extends Base { Circle a, b; Color color = Color.white; Line setColor(Color color) { this.color = color; this; } } Line > Arrow {} sclass CircleDragger extends MouseAdapter { ImageSurface is; L circles; O update; int dx, dy; Circle circle; *(ImageSurface *is, O *update, L *circles) { if (containsInstance(is.tools, CircleDragger)) ret; is.tools.add(this); is.addMouseListener(this); is.addMouseMotionListener(this); } public void mousePressed(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON1) { Pt p = is.pointFromEvent(e); circle = findCircle(p); if (circle != null) { dx = p.x-iround(circle.x*is.getWidth()); dy = p.y-iround(circle.y*is.getHeight()); } } } public void mouseDragged(MouseEvent e) { if (circle != null) { Pt p = is.pointFromEvent(e); circle.x = (p.x-dx)/(double) is.getWidth(); circle.y = (p.y-dy)/(double) is.getHeight(); callF(update); } } public void mouseReleased(MouseEvent e) { mouseDragged(e); circle = null; } Circle findCircle(Pt p) { new Lowest best; for (Circle c : circles) if (c.contains(is, p)) best.put(c, pointDistance(p, c.pt(is))); ret best.get(); } }