1 | sclass BaseBase { |
2 | S globalID = aGlobalID(); |
3 | S text; |
4 | S textForRender() { ret text; } |
5 | } |
6 | |
7 | sbool traits_multiLine = true; |
8 | |
9 | sclass Base extends BaseBase { |
10 | new L<S> traits; |
11 | bool hasTrait(S t) { ret containsIC(traits(), t); } |
12 | L<S> traits() { if (nempty(text) && neq(first(traits), text)) traits.add(0, text); ret traits; } |
13 | void addTraits(L<S> l) { setAddAll(traits(), l); } |
14 | void addTrait(S t) { if (nempty(t)) setAdd(traits(), t); } |
15 | |
16 | S textForRender() { |
17 | L<S> traits = traits(); |
18 | if (traits_multiLine) ret lines_rtrim(traits); |
19 | if (l(traits) <= 1) ret first(traits); |
20 | ret first(traits) + " [" + join(", ", dropFirst(traits)) + "]"; |
21 | } |
22 | |
23 | void setText(S text) { |
24 | this.text = text; |
25 | traits = ll(text); |
26 | } |
27 | } |
28 | |
29 | sclass CirclesAndLines { |
30 | new L<Circle> circles; |
31 | new L<Line> lines; |
32 | Class<? extends Arrow> arrowClass = Arrow; |
33 | Class<? extends Circle> circleClass = Circle; |
34 | S title; |
35 | S globalID = aGlobalID(); |
36 | long created = nowUnlessLoading(); |
37 | transient Lock lock = fairLock(); |
38 | transient S defaultImageID = #1007372; |
39 | transient double imgZoom = 1; // zoom for the circle images |
40 | transient Pt translate; |
41 | Circle hoverCircle; // which one we are hovering over |
42 | transient O onUserMadeArrow, onUserMadeCircle, onLayoutChange; |
43 | transient O onFullLayoutChange, onDeleteCircle, onDeleteLine; |
44 | transient O onRenameCircle, onRenameLine, onStructureChange; |
45 | transient BufferedImage imageForUserMadeNodes; |
46 | static int maxDistanceToLine = 20; // for clicking |
47 | transient S backgroundImageID = defaultBackgroundImageID; |
48 | static S defaultBackgroundImageID = #1007195; |
49 | static Color defaultLineColor = Color.white; |
50 | static bool debugRender; |
51 | static O staticPopupExtender; |
52 | transient double scale = 1; // zoom whole image |
53 | transient bool recordHistory = true; |
54 | L history; |
55 | |
56 | // auto-visualize |
57 | Circle circle_autoVis(S text, S visualizationText, double x, double y) { |
58 | ret addAndReturn(circles, |
59 | nu(circleClass, +x, +y, +text, |
60 | quickvis := visualizationText, |
61 | img := processImage(quickVisualizeOr(visualizationText, defaultImageID)))); |
62 | } |
63 | |
64 | S makeVisualizationText(S text) { |
65 | ret possibleGlobalID(text) ? "" : text; |
66 | } |
67 | |
68 | Circle circle_autoVis(S text, double x, double y) { |
69 | ret circle_autoVis(text, makeVisualizationText(text), x, y); |
70 | } |
71 | |
72 | Circle circle(BufferedImage img, double x, double y, S text) { |
73 | ret addAndReturn(circles, nu(circleClass, +x, +y, +text, img := processImage(img)); |
74 | } |
75 | |
76 | Circle circle(S text, BufferedImage img, double x, double y) { |
77 | ret circle(img, x, y, text); |
78 | } |
79 | |
80 | Circle circle(S text, double x, double y) { |
81 | ret addAndReturn(circles, nu(circleClass, +x, +y, +text, img := processImage(imageForUserMadeNodes()); |
82 | } |
83 | |
84 | Circle addCircle(S imageID, double x, double y) { |
85 | ret addCircle(imageID, x, y, ""); |
86 | } |
87 | |
88 | Circle addCircle(S imageID, double x, double y, S text) { |
89 | ret addAndReturn(circles, nu(circleClass, +x, +y, +text, img := processImage(loadImage2(imageID)))); |
90 | } |
91 | |
92 | Arrow findArrow(Circle a, Circle b) { |
93 | for (Line l : getWhere(lines, +a, +b)) |
94 | if (l instanceof Arrow) |
95 | ret (Arrow) l; |
96 | null; |
97 | } |
98 | |
99 | Line addLine(Circle a, Circle b) { |
100 | Line line = findWhere(lines, +a, +b); |
101 | if (line == null) |
102 | lines.add(line = nu(Line, +a, +b)); |
103 | ret line; |
104 | } |
105 | |
106 | Arrow arrow(Circle a, S text, Circle b) { |
107 | ret addArrow(a, b, text); |
108 | } |
109 | |
110 | Arrow addArrow(Circle a, Circle b) { |
111 | ret addArrow(a, b, ""); |
112 | } |
113 | |
114 | Arrow addArrow(Circle a, Circle b, S text) { |
115 | ret addAndReturn(lines, nu(arrowClass, +a, +b, +text)); |
116 | } |
117 | |
118 | BufferedImage makeImage(int w, int h) { |
119 | BufferedImage bg = renderTiledBackground(backgroundImageID, w, h, ptX(translate), ptY(translate)); |
120 | if (!lock.tryLock()) null; |
121 | try { |
122 | if (scale != 1) |
123 | createGraphics_modulate(bg, voidfunc(Graphics2D g) { |
124 | g.scale(scale, scale); |
125 | }); |
126 | |
127 | // Lines |
128 | |
129 | if (debugRender) |
130 | print("Have " + n(lines, "line")); |
131 | |
132 | // flipMap is false for bidirectional connections |
133 | |
134 | HashMap<Pair<Circle>, Line> hasLine = new HashMap; |
135 | new HashMap<Line, Bool> flipMap; |
136 | for (Line l : lines) { |
137 | hasLine.put(pair(l.a, l.b), l); |
138 | Line x = hasLine.get(pair(l.b, l.a)); |
139 | if (x != null) { |
140 | if (debugRender) |
141 | print("flipMap " + l.a.text + " / " + l.b.text); |
142 | flipMap.put(x, false); |
143 | flipMap.put(l, false); |
144 | } |
145 | } |
146 | |
147 | for (Line l : lines) { |
148 | DoublePt a = translateDoublePt(translate, l.a.doublePt(w, h, this)); |
149 | DoublePt b = translateDoublePt(translate, l.b.doublePt(w, h, this)); |
150 | if (debugRender) |
151 | print("Line " + a + " " + b); |
152 | if (l instanceof Arrow) |
153 | drawThoughtArrow(bg, l.a.img(this), iround(a.x), iround(a.y), l.b.img(this), iround(b.x), iround(b.y), l.color); |
154 | else |
155 | drawThoughtLine(bg, l.a.img(this), iround(a.x), iround(a.y), l.b.img(this), iround(b.x), iround(b.y), l.color); |
156 | S text = l.textForRender(); |
157 | if (nempty(text)) { |
158 | Bool flip = flipMap.get(l); |
159 | drawOutlineTextAlongLine_flip.set(flip); |
160 | |
161 | // mark bidirectional arrow labels with direction for clarity |
162 | if (flip != null) text += " " + unicode_blackRightArrow(); |
163 | |
164 | drawThoughtLineText_multiLine(bg, l.a.img(this), iround(a.x), iround(a.y), l.b.img(this), iround(b.x), iround(b.y), text, Color.white /*l.color*/); |
165 | } |
166 | } |
167 | |
168 | // Circles |
169 | |
170 | for (Circle c : circles) { |
171 | DoublePt p = translateDoublePt(translate, c.doublePt(w, h, this)); |
172 | drawThoughtCircle(bg, c.img(this), p.x, p.y); |
173 | } |
174 | |
175 | for (Circle c : circles) { |
176 | DoublePt p = translateDoublePt(translate, c.doublePt(w, h, this)); |
177 | S text = c.textForRender(); |
178 | if (nempty(text)) |
179 | drawThoughtCircleText(bg, c.img(this), p, text); |
180 | |
181 | if (c == hoverCircle) |
182 | drawThoughtCirclePlus(bg, c.img(this), p.x, p.y); |
183 | } |
184 | } finally { |
185 | lock.unlock(); |
186 | createGraphics_modulate(bg, null); |
187 | } |
188 | ret bg; |
189 | } |
190 | |
191 | Canvas showAsFrame(int w, int h) { |
192 | Canvas canvas = showAsFrame(); |
193 | frameInnerSize(canvas, w, h); |
194 | centerFrame(getFrame(canvas)); |
195 | ret canvas; |
196 | } |
197 | |
198 | Canvas showAsFrame() { |
199 | ret (Canvas) swing(func { |
200 | Canvas canvas = makeCanvas(); |
201 | showCenterFrame(canvas); |
202 | ret canvas; |
203 | }); |
204 | } |
205 | |
206 | Canvas makeCanvas() { |
207 | fO makeImg = func(int w, int h) { makeImage(w, h) }; |
208 | final Canvas canvas = jcanvas(makeImg); |
209 | disableImageSurfaceSelector(canvas); |
210 | new CircleDragger(this, canvas, r { updateCanvas(canvas, makeImg) }); |
211 | |
212 | componentPopupMenu(canvas, voidfunc(JPopupMenu menu) { |
213 | // POPUP MENU START |
214 | Pt p = pointFromEvent(canvas, componentPopupMenu_mouseEvent.get()); |
215 | JMenu imageMenu = jmenu("Image"); |
216 | moveAllMenuItems(menu, imageMenu); |
217 | |
218 | addMenuItem(menu, "New Circle...", r { newCircle(canvas) }); |
219 | |
220 | final Line l = findLine(canvas, p); |
221 | if (l != null) { |
222 | addMenuItem(menu, "Rename Relation...", r { |
223 | renameLine(canvas, l) |
224 | }); |
225 | |
226 | addMenuItem(menu, "Delete Relation", r { |
227 | deleteLine(l); |
228 | canvas.update(); |
229 | }); |
230 | } |
231 | |
232 | final Circle c = findCircle(canvas, p); |
233 | if (c != null) { |
234 | addMenuItem(menu, "Rename Circle...", r { renameCircle(canvas, c) }); |
235 | addMenuItem(menu, "Delete Circle", r { |
236 | deleteCircle(c); |
237 | canvas.update(); |
238 | }); |
239 | |
240 | if (c.img != null || c.quickvis != null) |
241 | addMenuItem(menu, "Delete Image", r { |
242 | c.img = null; |
243 | c.quickvis = null; |
244 | canvas.update(); |
245 | }); |
246 | |
247 | if (neqic(c.text, c.quickvis)) |
248 | addMenuItem(menu, "Visualize", r { |
249 | thread "Visualizing" { |
250 | quickVisualize(c.text); |
251 | print("Quickvis done"); |
252 | swing { |
253 | c.img = null; |
254 | c.quickvis = c.text; |
255 | canvas.update(); |
256 | pcallF(onRenameCircle, c); schange(); |
257 | } |
258 | } |
259 | }); |
260 | } |
261 | |
262 | addMenuItem(menu, "Copy structure to clipboard", r { |
263 | copyTextToClipboard(cal_simplifiedStructure(CirclesAndLines.this)) |
264 | }); |
265 | |
266 | addMenuItem(menu, "Paste structure", r { |
267 | S text = getTextFromClipboard(); |
268 | if (nempty(text)) { |
269 | copyCAL(cal_unstructure(text), CirclesAndLines.this); |
270 | canvas.update(); |
271 | schange(); |
272 | } |
273 | }); |
274 | |
275 | pcallF(staticPopupExtender, CirclesAndLines.this, canvas, menu); |
276 | |
277 | addMenuItem(menu, imageMenu); |
278 | |
279 | // POPUP MENU END |
280 | }); |
281 | |
282 | ret canvas; |
283 | } |
284 | |
285 | void newCircle(final Canvas canvas) { |
286 | final JTextField text = jtextfield(); |
287 | showFormTitled("New Circle", "Text", text, r-thread { loading { |
288 | S theText = getTextTrim(text); |
289 | makeCircle(theText); |
290 | canvas.update(); |
291 | }}); |
292 | } |
293 | |
294 | Canvas show() { ret showAsFrame(); } |
295 | Canvas show(int w, int h) { ret showAsFrame(w, h); } |
296 | |
297 | Circle findCircle(S text) { |
298 | for (Circle c : circles) if (eq(c.text, text)) ret c; |
299 | for (Circle c : circles) if (eqic(c.text, text)) ret c; |
300 | null; |
301 | } |
302 | |
303 | void renameCircle(final Canvas canvas, final Circle c) { |
304 | final JTextField tf = jtextfield(c.text); |
305 | showFormTitled("Rename circle", |
306 | "Old name", jlabel(c.text), |
307 | "New name", tf, |
308 | r { |
309 | c.setText(getTextTrim(tf)); |
310 | canvas.update(); |
311 | pcallF(onRenameCircle, c); schange(); |
312 | }); |
313 | } |
314 | |
315 | void renameLine(final Canvas canvas, final Line l) { |
316 | final JTextField tf = jtextfield(l.text); |
317 | showFormTitled("Rename relation", |
318 | "Old name", jlabel(l.text), |
319 | "New name", tf, |
320 | r { |
321 | l.setText(getTextTrim(tf)); |
322 | canvas.update(); |
323 | pcallF(onRenameLine, l); schange(); |
324 | }); |
325 | } |
326 | |
327 | void clear { |
328 | clearAll(circles, lines); |
329 | } |
330 | |
331 | // only finds actually containing circles |
332 | Circle findCircle(ImageSurface canvas, Pt p) { |
333 | p = untranslatePt(translate, p); |
334 | new Lowest<Circle> best; |
335 | for (Circle c : circles) |
336 | if (c.contains(this, canvas, p)) |
337 | best.put(c, pointDistance(p, c.pt2(this, canvas))); |
338 | ret best!; |
339 | } |
340 | |
341 | Circle findNearestCircle(ImageSurface canvas, Pt p) { |
342 | new Lowest<Circle> best; |
343 | for (Circle c : circles) |
344 | if (c.contains(this, canvas, p)) |
345 | best.put(c, pointDistance(p, c.pt2(this, canvas))); |
346 | ret best.get(); |
347 | } |
348 | |
349 | BufferedImage processImage(BufferedImage img) { |
350 | ret scaleImage(img, imgZoom); |
351 | } |
352 | |
353 | void deleteCircle(Circle c) { |
354 | for (Line l : cloneList(lines)) |
355 | if (l.a == c || l.b == c) deleteLine(l); |
356 | circles.remove(c); |
357 | pcallF(onDeleteCircle, c); schange(); |
358 | } |
359 | |
360 | void deleteLine(Line l) { |
361 | lines.remove(l); |
362 | pcallF(onDeleteLine, l); schange(); |
363 | } |
364 | |
365 | void openPlusDialog(final Circle c, final ImageSurface canvas) { |
366 | if (c == null) ret; |
367 | |
368 | final JTextField tfFrom = jtextfield(c.text); |
369 | final JTextField tfRel = jtextfield(web_defaultRelationName()); |
370 | final JComboBox tfTo = autoComboBox(collect(circles, 'text)); |
371 | |
372 | showFormTitled("Add connection", |
373 | "From node", tfFrom, |
374 | "Connection name", tfRel, |
375 | "To node", tfTo, |
376 | func { |
377 | S sA = getTextTrim(tfFrom); |
378 | Circle a = eq(sA, c.text) ? c : findOrMakeCircle(sA); |
379 | if (a == null) { messageBox("Not found: " + getTextTrim(tfFrom)); false; } |
380 | Circle b = findOrMakeCircle(getTextTrim(tfTo)); |
381 | if (b == null) { messageBox("Not found: " + getTextTrim(tfTo)); false; } |
382 | if (a == b) { infoBox("Can't connect circle to itself for now"); false; } |
383 | Arrow arrow = arrow(a, getTextTrim(tfRel), b); |
384 | ((Canvas) canvas).update(); |
385 | pcallF(onUserMadeArrow, arrow); schange(); |
386 | null; |
387 | }); |
388 | awtLater(tfRel, 100, r { requestFocus(tfRel) }); |
389 | } |
390 | |
391 | void schange() { |
392 | pcallF(onStructureChange); |
393 | logQuoted("user-web-edits", now() + " " + cal_structure(this)); |
394 | markWebsPosted(); |
395 | } |
396 | |
397 | Circle findOrMakeCircle(S text) { |
398 | Circle c = findCircle(text); |
399 | if (c != null) ret c; |
400 | ret makeCircle(text); |
401 | } |
402 | |
403 | Circle makeCircle(S text) { |
404 | Circle c = circle(imageForUserMadeNodes(), random(0.1, 0.9), random(0.1, 0.9), text); |
405 | pcallF(onUserMadeCircle, c); schange(); |
406 | historyLog(lisp("Made circle", text)); |
407 | ret c; |
408 | } |
409 | |
410 | BufferedImage imageForUserMadeNodes() { |
411 | if (imageForUserMadeNodes == null) |
412 | imageForUserMadeNodes = whiteImage(20, 20); |
413 | ret imageForUserMadeNodes; |
414 | } |
415 | |
416 | Line findLine(Canvas is, Pt p) { |
417 | p = untranslatePt(translate, p); |
418 | new Lowest<Line> best; |
419 | for (Line line : lines) { |
420 | double d = distancePointToLineSegment(line.a.pt(this, is), line.b.pt(this, is), p); |
421 | if (d <= maxDistanceToLine) |
422 | best.put(line, d); |
423 | } |
424 | ret best!; |
425 | } |
426 | |
427 | Pt pointFromEvent(ImageSurface canvas, MouseEvent e) { |
428 | ret scalePt(canvas.pointFromEvent(e), 1/scale); |
429 | } |
430 | |
431 | void historyLog(O o) { |
432 | if (!recordHistory) ret; |
433 | if (history == null) history = new L; |
434 | history.add(o); |
435 | } |
436 | } // end of class CirclesAndLines |
437 | |
438 | sclass Circle extends Base { |
439 | transient BufferedImage img; |
440 | double x, y; |
441 | //static BufferedImage defaultImage; |
442 | S quickvis; |
443 | |
444 | BufferedImage img(CirclesAndLines cal) { |
445 | if (img != null) ret img; |
446 | if (nempty(quickvis)) img = quickVisualize(quickvis); |
447 | //if (defaultImage == null) defaultImage = loadImage2(#1007372); |
448 | ret cal.imageForUserMadeNodes(); |
449 | } |
450 | |
451 | Pt pt2(CirclesAndLines cal, ImageSurface is) { |
452 | ret pt2(is.getWidth(), is.getHeight(), cal); |
453 | } |
454 | |
455 | Pt pt(CirclesAndLines cal, ImageSurface is) { |
456 | ret pt(is.getWidth(), is.getHeight(), cal); |
457 | } |
458 | |
459 | Pt pt(int w, int h, CirclesAndLines cal) { |
460 | ret new Pt(iround(x*w/cal.scale), iround(y*h/cal.scale)); |
461 | } |
462 | |
463 | Pt pt2(int w, int h, CirclesAndLines cal) { |
464 | ret new Pt(iround(x*w), iround(y*h)); |
465 | } |
466 | |
467 | DoublePt doublePt(int w, int h, CirclesAndLines cal) { |
468 | ret new DoublePt(x*w/cal.scale, y*h/cal.scale); |
469 | } |
470 | |
471 | bool contains(CirclesAndLines cal, ImageSurface is, Pt p) { |
472 | ret pointDistance(p, pt2(cal, is)) <= iround(thoughtCircleSize(img(cal))*cal.scale)/2+1; |
473 | } |
474 | } |
475 | |
476 | sclass Line extends Base { |
477 | Circle a, b; |
478 | transient Color color = CirclesAndLines.defaultLineColor; |
479 | |
480 | Line setColor(Color color) { |
481 | this.color = color; |
482 | this; |
483 | } |
484 | } |
485 | |
486 | Line > Arrow {} |
487 | |
488 | sclass CircleDragger extends MouseAdapter { |
489 | CirclesAndLines cal; |
490 | ImageSurface is; |
491 | O update; |
492 | int dx, dy; |
493 | Circle circle; |
494 | Pt startPoint; |
495 | |
496 | *(CirclesAndLines *cal, ImageSurface *is, O *update) { |
497 | if (containsInstance(is.tools, CircleDragger)) ret; |
498 | is.tools.add(this); |
499 | is.addMouseListener(this); |
500 | is.addMouseMotionListener(this); |
501 | } |
502 | |
503 | public void mouseMoved(MouseEvent e) { |
504 | Pt p = is.pointFromEvent(e); |
505 | Circle c = cal.findCircle(is, p); |
506 | if (c != cal.hoverCircle) { |
507 | cal.hoverCircle = c; |
508 | callF(update); |
509 | } |
510 | } |
511 | |
512 | public void mousePressed(MouseEvent e) { |
513 | if (e.getButton() == MouseEvent.BUTTON1) { |
514 | Pt p = is.pointFromEvent(e); |
515 | startPoint = p; |
516 | circle = cal.findCircle(is, p); |
517 | if (circle != null) { |
518 | dx = p.x-iround(circle.x*is.getWidth()); |
519 | dy = p.y-iround(circle.y*is.getHeight()); |
520 | //printVars("mousePressed", +dx, +dy, +p, cx := circle.x, cy := circle.y, w := is.getWidth(), h := is.getHeight()); |
521 | } else { |
522 | Pt t = unnull(cal.translate); |
523 | dx = p.x-t.x; |
524 | dy = p.y-t.y; |
525 | } |
526 | } |
527 | } |
528 | |
529 | public void mouseDragged(MouseEvent e) { |
530 | if (startPoint == null) ret; |
531 | Pt p = is.pointFromEvent(e); |
532 | if (circle != null) { |
533 | circle.x = (p.x-dx)/(double) is.getWidth(); |
534 | circle.y = (p.y-dy)/(double) is.getHeight(); |
535 | //printVars("mouseDragged", +dx, +dy, +p, cx := circle.x, cy := circle.y, w := is.getWidth(), h := is.getHeight()); |
536 | pcallF(cal.onLayoutChange, circle); |
537 | callF(update); |
538 | } else { |
539 | cal.translate = new Pt(p.x-dx, p.y-dy); |
540 | callF(update); |
541 | } |
542 | } |
543 | |
544 | public void mouseReleased(MouseEvent e) { |
545 | mouseDragged(e); |
546 | if (eq(is.pointFromEvent(e), startPoint)) |
547 | cal.openPlusDialog(circle, is); |
548 | circle = null; |
549 | startPoint = null; |
550 | } |
551 | } |
Began life as a copy of #1007298
download show line numbers debug dex old transpilations
Travelled to 17 computer(s): aoiabmzegqzx, bhatertpkbcr, cbybwowwnfue, cfunsshuasjs, gwrvuhgaqvyk, ishqpsrjomds, jtubtzbbkimh, lpdgvwnxivlt, mqqgnosmbjvj, onxytkatvevr, ppjhyzlbdabe, pyentgdyhuwx, pzhvpgtvlbxg, tslmcundralx, tvejysmllsmz, vouqrxazstgt, xrpafgyirdlv
No comments. add comment
Snippet ID: | #1007303 |
Snippet name: | CirclesAndLines [include] |
Eternal ID of this version: | #1007303/197 |
Text MD5: | bdaf89feb430d4dbb3ef2323baace056 |
Author: | stefan |
Category: | javax / gui |
Type: | JavaX fragment (include) |
Public (visible to everyone): | Yes |
Archived (hidden from active list): | No |
Created/modified: | 2020-02-20 13:22:40 |
Source code size: | 16898 bytes / 551 lines |
Pitched / IR pitched: | No / No |
Views / Downloads: | 1101 / 5017 |
Version history: | 196 change(s) |
Referenced in: | [show references] |