@elicdavis/node-flow 0.1.16 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +97 -4
- package/dist/index.d.ts +97 -4
- package/dist/index.js +643 -69
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +637 -68
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/graph.ts +234 -41
- package/src/index.ts +16 -0
- package/src/input.ts +22 -14
- package/src/markdown/entry.ts +6 -2
- package/src/minimap.ts +178 -0
- package/src/node.ts +80 -1
- package/src/nodes/factory.ts +9 -5
- package/src/nodes/publisher.ts +39 -4
- package/src/nodes/subsystem.ts +211 -1
- package/src/notes/subsystem.ts +3 -0
- package/src/port.ts +17 -4
- package/src/render_scheduler.ts +49 -0
- package/src/theme.ts +8 -0
- package/src/types/text.ts +2 -0
- package/src/widgets/image.ts +2 -0
- package/test/minimap.test.ts +103 -0
- package/test/node_menu_filter.test.ts +119 -0
- package/test/render_scheduler.test.ts +119 -0
package/package.json
CHANGED
package/src/graph.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { Clamp01 } from "./utils/math";
|
|
|
10
10
|
import { GraphSubsystem, RenderResults } from './graphSubsystem';
|
|
11
11
|
import { FlowNote } from "./notes/note";
|
|
12
12
|
import { NoteAddedCallback, NoteDragStartCallback, NoteDragStopCallback, NoteRemovedCallback, NoteSubsystem, NoteSubsystemConfig } from "./notes/subsystem";
|
|
13
|
-
import { ConnectionRendererConfiguration, NodeAddedCallback, NodeRemovedCallback, NodeSubsystem } from "./nodes/subsystem";
|
|
13
|
+
import { ConnectionRendererConfiguration, DanglingConnection, InternalConnection, NodeAddedCallback, NodeRemovedCallback, NodeSubsystem } from "./nodes/subsystem";
|
|
14
14
|
import { Connection } from './connection';
|
|
15
15
|
import { Publisher } from './nodes/publisher';
|
|
16
16
|
import { VectorPool } from './types/pool';
|
|
@@ -18,6 +18,8 @@ import { Camera, CameraOrientation } from './camera';
|
|
|
18
18
|
export { CameraOrientation };
|
|
19
19
|
import { PassSubsystem } from './pass/subsystem';
|
|
20
20
|
import { QuickMenu } from './quickMenu';
|
|
21
|
+
import { onRenderRequested, renderContinuously, requestRender } from './render_scheduler';
|
|
22
|
+
import { Minimap, MinimapConfig } from './minimap';
|
|
21
23
|
|
|
22
24
|
export type GraphRenderer = (canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, position: Vector2, scale: number) => void;
|
|
23
25
|
|
|
@@ -49,6 +51,10 @@ function BuildBackgroundRenderer(backgroundColor: string): GraphRenderer {
|
|
|
49
51
|
|
|
50
52
|
const contextMenuGroup = "graph-context-menu";
|
|
51
53
|
|
|
54
|
+
export type CopyCallback = (nodes: Array<FlowNode>, connections: Array<InternalConnection>) => void;
|
|
55
|
+
|
|
56
|
+
export type PasteCallback = (position: Vector2) => void;
|
|
57
|
+
|
|
52
58
|
export interface FlowNodeGraphConfiguration {
|
|
53
59
|
backgroundRenderer?: GraphRenderer;
|
|
54
60
|
backgroundColor?: string;
|
|
@@ -56,6 +62,7 @@ export interface FlowNodeGraphConfiguration {
|
|
|
56
62
|
contextMenu?: ContextMenuConfig
|
|
57
63
|
nodes?: NodeFactoryConfig
|
|
58
64
|
board?: NoteSubsystemConfig
|
|
65
|
+
minimap?: MinimapConfig
|
|
59
66
|
}
|
|
60
67
|
|
|
61
68
|
interface OpenContextMenu {
|
|
@@ -221,7 +228,15 @@ export class NodeFlowGraph {
|
|
|
221
228
|
this.#backgroundRenderer = BuildBackgroundRenderer(backgroundColor);
|
|
222
229
|
}
|
|
223
230
|
|
|
224
|
-
|
|
231
|
+
this.#minimap = new Minimap(config?.minimap);
|
|
232
|
+
|
|
233
|
+
this.#mainNodeSubsystem.setConnectionReleasedHandler((dangling) => {
|
|
234
|
+
this.#openScopedMenu(dangling);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
this.#watchCanvasSize();
|
|
238
|
+
this.#stopRendering = onRenderRequested(this.#render.bind(this));
|
|
239
|
+
requestRender();
|
|
225
240
|
|
|
226
241
|
new MouseObserver(this.#canvas,
|
|
227
242
|
this.#mouseDragEvent.bind(this),
|
|
@@ -240,45 +255,115 @@ export class NodeFlowGraph {
|
|
|
240
255
|
document.addEventListener(
|
|
241
256
|
"keydown",
|
|
242
257
|
(e) => {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
258
|
+
this.#keyDown(e);
|
|
259
|
+
requestRender();
|
|
260
|
+
}
|
|
261
|
+
);
|
|
262
|
+
}
|
|
246
263
|
|
|
247
|
-
|
|
248
|
-
if (e.code === "Escape") {
|
|
249
|
-
this.#openQuickMenu = null;
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
264
|
+
#copyCallbacks: Array<CopyCallback> = [];
|
|
252
265
|
|
|
253
|
-
|
|
254
|
-
this.#openQuickMenu.Menu.execute();
|
|
255
|
-
this.#openQuickMenu = null;
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
266
|
+
#pasteCallbacks: Array<PasteCallback> = [];
|
|
258
267
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
268
|
+
public addCopyListener(callback: CopyCallback): void {
|
|
269
|
+
this.#copyCallbacks.push(callback);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
public addPasteListener(callback: PasteCallback): void {
|
|
273
|
+
this.#pasteCallbacks.push(callback);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
#raiseCopy(): void {
|
|
277
|
+
const nodes = this.#mainNodeSubsystem.getSelectedNodes();
|
|
278
|
+
if (nodes.length === 0) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const connections = this.#mainNodeSubsystem.connectionsWithin(nodes);
|
|
283
|
+
this.#copyCallbacks.forEach((callback) => callback(nodes, connections));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#raisePaste(): void {
|
|
287
|
+
const position = this.#mousePosition === undefined
|
|
288
|
+
? { x: 0, y: 0 }
|
|
289
|
+
: this.#sceenPositionToGraphPosition(this.#mousePosition);
|
|
290
|
+
|
|
291
|
+
this.#pasteCallbacks.forEach((callback) => callback(position));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
#deleteKey(e: KeyboardEvent): boolean {
|
|
295
|
+
return e.code === "Delete" || e.code === "Backspace" || e.key === "Delete" || e.key === "Backspace";
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
#keyDown(e: KeyboardEvent): void {
|
|
299
|
+
if (document.activeElement) {
|
|
300
|
+
if (["INPUT", "TEXTAREA"].includes(document.activeElement.tagName)) return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (this.#deleteKey(e)) {
|
|
304
|
+
if (this.#mainNodeSubsystem.getSelectedNodes().length > 0) {
|
|
305
|
+
e.preventDefault();
|
|
306
|
+
this.#mainNodeSubsystem.removeSelectedNodes();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (e.ctrlKey || e.metaKey) {
|
|
312
|
+
const letter = e.code.startsWith("Key")
|
|
313
|
+
? e.code.substring(3).toLowerCase()
|
|
314
|
+
: (e.key ?? "").toLowerCase();
|
|
262
315
|
|
|
263
|
-
|
|
264
|
-
|
|
316
|
+
switch (letter) {
|
|
317
|
+
case "c":
|
|
318
|
+
this.#raiseCopy();
|
|
265
319
|
return;
|
|
266
|
-
}
|
|
267
320
|
|
|
268
|
-
|
|
321
|
+
case "v":
|
|
322
|
+
this.#raisePaste();
|
|
323
|
+
return;
|
|
324
|
+
|
|
325
|
+
// duplicate
|
|
326
|
+
case "d":
|
|
327
|
+
e.preventDefault();
|
|
328
|
+
this.#raiseCopy();
|
|
329
|
+
this.#raisePaste();
|
|
269
330
|
return;
|
|
270
|
-
}
|
|
271
|
-
const contextMenuPosition = this.#sceenPositionToGraphPosition(this.#mousePosition);
|
|
272
|
-
|
|
273
|
-
let items = this.#mainNodeSubsystem.nodeFactory().newNodeSubmenus(this.#mainNodeSubsystem, contextMenuPosition);
|
|
274
|
-
this.#openQuickMenu = {
|
|
275
|
-
Menu: new QuickMenu({
|
|
276
|
-
subMenus: items,
|
|
277
|
-
}),
|
|
278
|
-
Position: contextMenuPosition
|
|
279
|
-
}
|
|
280
331
|
}
|
|
281
|
-
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (this.#openQuickMenu) {
|
|
335
|
+
if (e.code === "Escape") {
|
|
336
|
+
this.#openQuickMenu = null;
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (e.code === "Enter") {
|
|
341
|
+
this.#openQuickMenu.Menu.execute();
|
|
342
|
+
this.#openQuickMenu = null;
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
this.#openQuickMenu.Menu.keyboardEvent(e);
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const spacePressed = e.key == " " || e.code == "Space";
|
|
351
|
+
if (!spacePressed) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (!this.#mousePosition) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const contextMenuPosition = this.#sceenPositionToGraphPosition(this.#mousePosition);
|
|
359
|
+
|
|
360
|
+
let items = this.#mainNodeSubsystem.nodeFactory().newNodeSubmenus(this.#mainNodeSubsystem, contextMenuPosition);
|
|
361
|
+
this.#openQuickMenu = {
|
|
362
|
+
Menu: new QuickMenu({
|
|
363
|
+
subMenus: items,
|
|
364
|
+
}),
|
|
365
|
+
Position: contextMenuPosition
|
|
366
|
+
}
|
|
282
367
|
}
|
|
283
368
|
|
|
284
369
|
#fileDrop(file: File): void {
|
|
@@ -362,9 +447,33 @@ export class NodeFlowGraph {
|
|
|
362
447
|
this.#contextMenuEntryHovering = null;
|
|
363
448
|
|
|
364
449
|
this.#mousePosition = mousePosition;
|
|
450
|
+
|
|
451
|
+
if (this.#minimap.contains(mousePosition)) {
|
|
452
|
+
this.#lookAtFromMinimap(mousePosition);
|
|
453
|
+
this.#draggingMinimap = true;
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
365
457
|
this.currentView().clickStart(mousePosition, this.#camera, ctrlKey);
|
|
366
458
|
}
|
|
367
459
|
|
|
460
|
+
#draggingMinimap: boolean = false;
|
|
461
|
+
|
|
462
|
+
#lookAtFromMinimap(mousePosition: Vector2): void {
|
|
463
|
+
const target: Vector2 = { x: 0, y: 0 };
|
|
464
|
+
this.#minimap.graphPositionOf(mousePosition, target);
|
|
465
|
+
|
|
466
|
+
// Centre the viewport on it rather than putting it in the corner.
|
|
467
|
+
this.#camera.position.x = -(target.x * this.#camera.zoom) + (this.#canvas.width / 2);
|
|
468
|
+
this.#camera.position.y = -(target.y * this.#camera.zoom) + (this.#canvas.height / 2);
|
|
469
|
+
requestRender();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** The minimap, so it can be turned on and sized by the host. */
|
|
473
|
+
public minimap(): Minimap {
|
|
474
|
+
return this.#minimap;
|
|
475
|
+
}
|
|
476
|
+
|
|
368
477
|
currentView(): GraphView {
|
|
369
478
|
return this.#views[this.#currentView];
|
|
370
479
|
}
|
|
@@ -389,6 +498,14 @@ export class NodeFlowGraph {
|
|
|
389
498
|
return this.#mainNodeSubsystem.getSelectedNodes();
|
|
390
499
|
}
|
|
391
500
|
|
|
501
|
+
public unselectAllNodes(): void {
|
|
502
|
+
this.#mainNodeSubsystem.unselectAllNodes();
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
public removeSelectedNodes(): Array<FlowNode> {
|
|
506
|
+
return this.#mainNodeSubsystem.removeSelectedNodes();
|
|
507
|
+
}
|
|
508
|
+
|
|
392
509
|
/**
|
|
393
510
|
* Returns a copy of the current camera orientation (screen-space pan + zoom).
|
|
394
511
|
*/
|
|
@@ -533,11 +650,42 @@ export class NodeFlowGraph {
|
|
|
533
650
|
};
|
|
534
651
|
}
|
|
535
652
|
|
|
653
|
+
#openScopedMenu(dangling: DanglingConnection): void {
|
|
654
|
+
if (this.#mousePosition === undefined) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const position = this.#sceenPositionToGraphPosition(this.#mousePosition);
|
|
659
|
+
const menu = this.#mainNodeSubsystem.nodeFactory().openMenu(
|
|
660
|
+
this.#mainNodeSubsystem,
|
|
661
|
+
position,
|
|
662
|
+
{
|
|
663
|
+
needs: dangling.needs,
|
|
664
|
+
types: dangling.types,
|
|
665
|
+
onCreated: dangling.connectTo,
|
|
666
|
+
},
|
|
667
|
+
);
|
|
668
|
+
|
|
669
|
+
this.#openedContextMenu = {
|
|
670
|
+
Menu: new ContextMenu(menu),
|
|
671
|
+
Position: position,
|
|
672
|
+
};
|
|
673
|
+
requestRender();
|
|
674
|
+
}
|
|
675
|
+
|
|
536
676
|
#clickEnd(): void {
|
|
677
|
+
this.#draggingMinimap = false;
|
|
537
678
|
this.currentView().clickEnd();
|
|
538
679
|
}
|
|
539
680
|
|
|
540
681
|
#mouseDragEvent(delta: Vector2): void {
|
|
682
|
+
if (this.#draggingMinimap) {
|
|
683
|
+
if (this.#mousePosition !== undefined) {
|
|
684
|
+
this.#lookAtFromMinimap(this.#mousePosition);
|
|
685
|
+
}
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
|
|
541
689
|
let draggingSomething = this.currentView().mouseDragEvent(delta, this.#camera.zoom);
|
|
542
690
|
if (!draggingSomething) {
|
|
543
691
|
this.#camera.position.x += delta.x;
|
|
@@ -549,14 +697,57 @@ export class NodeFlowGraph {
|
|
|
549
697
|
|
|
550
698
|
#cursor: CursorStyle;
|
|
551
699
|
|
|
552
|
-
#
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
700
|
+
#minimap: Minimap;
|
|
701
|
+
|
|
702
|
+
#stopRendering: () => void;
|
|
703
|
+
|
|
704
|
+
#sizeObserver: ResizeObserver | undefined;
|
|
705
|
+
|
|
706
|
+
#matchCanvasToParent(): void {
|
|
707
|
+
const parent = this.#canvas.parentNode as HTMLElement | null;
|
|
708
|
+
if (parent === null) {
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const rect = parent.getBoundingClientRect();
|
|
713
|
+
if (this.#canvas.width === rect.width && this.#canvas.height === rect.height) {
|
|
714
|
+
return;
|
|
558
715
|
}
|
|
559
716
|
|
|
717
|
+
this.#canvas.width = rect.width;
|
|
718
|
+
this.#canvas.height = rect.height;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
#watchCanvasSize(): void {
|
|
722
|
+
this.#matchCanvasToParent();
|
|
723
|
+
|
|
724
|
+
const parent = this.#canvas.parentNode as HTMLElement | null;
|
|
725
|
+
if (parent === null || typeof ResizeObserver === "undefined") {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
this.#sizeObserver = new ResizeObserver(() => {
|
|
730
|
+
this.#matchCanvasToParent();
|
|
731
|
+
requestRender();
|
|
732
|
+
});
|
|
733
|
+
this.#sizeObserver.observe(parent);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
public requestRender(): void {
|
|
737
|
+
requestRender();
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
public renderContinuously(): () => void {
|
|
741
|
+
return renderContinuously();
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
public dispose(): void {
|
|
745
|
+
this.#stopRendering();
|
|
746
|
+
this.#sizeObserver?.disconnect();
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
#render(): void {
|
|
750
|
+
this.#matchCanvasToParent();
|
|
560
751
|
|
|
561
752
|
this.#cursor = CursorStyle.Default;
|
|
562
753
|
|
|
@@ -569,6 +760,10 @@ export class NodeFlowGraph {
|
|
|
569
760
|
}
|
|
570
761
|
});
|
|
571
762
|
|
|
763
|
+
TimeExecution("Render_Minimap", () => {
|
|
764
|
+
this.#minimap.render(this.#ctx, this.#canvas, this.#camera, this.#mainNodeSubsystem.getNodes());
|
|
765
|
+
});
|
|
766
|
+
|
|
572
767
|
TimeExecution("Render_Context", this.#renderContextMenu.bind(this));
|
|
573
768
|
|
|
574
769
|
TimeExecution("Render_QuickMenu", this.#renderQuickMenu.bind(this))
|
|
@@ -579,8 +774,6 @@ export class NodeFlowGraph {
|
|
|
579
774
|
this.#canvas.style.cursor = this.#cursor;
|
|
580
775
|
}
|
|
581
776
|
this.#lastFrameCursor = this.#cursor;
|
|
582
|
-
|
|
583
|
-
window.requestAnimationFrame(this.#render.bind(this));
|
|
584
777
|
}
|
|
585
778
|
|
|
586
779
|
#renderBackground(): void {
|
package/src/index.ts
CHANGED
|
@@ -26,6 +26,22 @@ export {
|
|
|
26
26
|
import { Port, PortConfig, PortStyle, PortType, portsCompatible } from './port';
|
|
27
27
|
export { Port, PortConfig, PortStyle, PortType, portsCompatible };
|
|
28
28
|
|
|
29
|
+
// Rendering
|
|
30
|
+
import { renderContinuously, requestRender } from './render_scheduler';
|
|
31
|
+
export { renderContinuously, requestRender };
|
|
32
|
+
|
|
33
|
+
import { Minimap, MinimapConfig } from './minimap';
|
|
34
|
+
export { Minimap, MinimapConfig };
|
|
35
|
+
|
|
36
|
+
import { DanglingConnection, InternalConnection } from './nodes/subsystem';
|
|
37
|
+
export { DanglingConnection, InternalConnection };
|
|
38
|
+
|
|
39
|
+
import { NodeMenuFilter } from './nodes/publisher';
|
|
40
|
+
export { NodeMenuFilter };
|
|
41
|
+
|
|
42
|
+
import { portConfigTypes, typeSetsCompatible } from './port';
|
|
43
|
+
export { portConfigTypes, typeSetsCompatible };
|
|
44
|
+
|
|
29
45
|
// Connections
|
|
30
46
|
import {
|
|
31
47
|
Connection, ConnectionRenderer, ConnectionRendererParams, DefaultConnectionRenderer
|
package/src/input.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { requestRender } from "./render_scheduler";
|
|
1
2
|
import { CopyVector2, Distance, SubVector2, Vector2, Zero } from "./types/vector2";
|
|
2
3
|
|
|
3
4
|
const LONG_PRESS_MS = 500;
|
|
@@ -49,19 +50,25 @@ export class MouseObserver {
|
|
|
49
50
|
|
|
50
51
|
ele.style.touchAction = "none";
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
const redraws = <T extends Event>(handler: (event: T) => void) =>
|
|
54
|
+
(event: T): void => {
|
|
55
|
+
handler(event);
|
|
56
|
+
requestRender();
|
|
57
|
+
};
|
|
54
58
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
document.addEventListener("touchcancel", this.#touchEnd.bind(this), false);
|
|
59
|
+
ele.addEventListener("mousedown", redraws(this.#down.bind(this)), false);
|
|
60
|
+
ele.addEventListener("touchstart", redraws(this.#touchStart.bind(this)), { passive: false });
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
document.addEventListener("mouseup", redraws(this.#up.bind(this)), false);
|
|
63
|
+
document.addEventListener("touchend", redraws(this.#touchEnd.bind(this)), false);
|
|
64
|
+
document.addEventListener("touchcancel", redraws(this.#touchEnd.bind(this)), false);
|
|
61
65
|
|
|
62
|
-
ele.addEventListener("
|
|
66
|
+
ele.addEventListener("mousemove", redraws(this.#move.bind(this)), false);
|
|
67
|
+
ele.addEventListener("touchmove", redraws(this.#touchMove.bind(this)), { passive: false });
|
|
63
68
|
|
|
64
|
-
ele.addEventListener("
|
|
69
|
+
ele.addEventListener("wheel", redraws(this.#wheel.bind(this)), { passive: false });
|
|
70
|
+
|
|
71
|
+
ele.addEventListener("drop", redraws((ev: DragEvent) => {
|
|
65
72
|
ev.preventDefault();
|
|
66
73
|
if (ev.dataTransfer?.items) {
|
|
67
74
|
[...ev.dataTransfer.items].forEach((item, i) => {
|
|
@@ -73,21 +80,21 @@ export class MouseObserver {
|
|
|
73
80
|
}
|
|
74
81
|
});
|
|
75
82
|
}
|
|
76
|
-
});
|
|
83
|
+
}));
|
|
77
84
|
|
|
78
|
-
ele.addEventListener("dragover", (ev) => {
|
|
85
|
+
ele.addEventListener("dragover", redraws((ev: DragEvent) => {
|
|
79
86
|
ev.preventDefault();
|
|
80
87
|
this.#moveCallback(this.#mousePos(ev));
|
|
81
|
-
});
|
|
88
|
+
}));
|
|
82
89
|
|
|
83
|
-
ele.addEventListener("contextmenu", (evt) => {
|
|
90
|
+
ele.addEventListener("contextmenu", redraws((evt: MouseEvent) => {
|
|
84
91
|
evt.preventDefault();
|
|
85
92
|
// Touch uses long-press; ignore synthetic browser contextmenu after touch.
|
|
86
93
|
if (Date.now() - this.#lastTouchAt < SUPPRESS_MOUSE_MS) {
|
|
87
94
|
return;
|
|
88
95
|
}
|
|
89
96
|
this.#contextMenu(this.#mousePos(evt));
|
|
90
|
-
}, false);
|
|
97
|
+
}), false);
|
|
91
98
|
}
|
|
92
99
|
|
|
93
100
|
#mousePos(event: MouseEvent | DragEvent | WheelEvent): Vector2 {
|
|
@@ -198,6 +205,7 @@ export class MouseObserver {
|
|
|
198
205
|
this.#clicked = false;
|
|
199
206
|
this.#clickStop();
|
|
200
207
|
this.#contextMenu({ x, y });
|
|
208
|
+
requestRender();
|
|
201
209
|
}, LONG_PRESS_MS);
|
|
202
210
|
|
|
203
211
|
this.#clicked = true;
|
package/src/markdown/entry.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { requestRender } from "../render_scheduler";
|
|
1
2
|
import { Theme } from "../theme";
|
|
2
3
|
import { List } from "../types/list";
|
|
3
4
|
import { Text } from "../types/text";
|
|
4
|
-
import {
|
|
5
|
+
import { Vector2, Zero } from "../types/vector2";
|
|
5
6
|
|
|
6
7
|
export interface MarkdownEntry {
|
|
7
8
|
render(ctx: CanvasRenderingContext2D, position: Vector2, scale: number, maxWidth: number): number
|
|
@@ -11,7 +12,10 @@ function onFontsLoaded(callback: () => void): void {
|
|
|
11
12
|
if (typeof document === "undefined" || document.fonts === undefined) {
|
|
12
13
|
return;
|
|
13
14
|
}
|
|
14
|
-
document.fonts.addEventListener("loadingdone", () =>
|
|
15
|
+
document.fonts.addEventListener("loadingdone", () => {
|
|
16
|
+
callback();
|
|
17
|
+
requestRender();
|
|
18
|
+
});
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
export class CodeBlockEntry {
|
package/src/minimap.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { Camera } from "./camera";
|
|
2
|
+
import { FlowNode } from "./node";
|
|
3
|
+
import { Theme } from "./theme";
|
|
4
|
+
import { Box, InBox } from "./types/box";
|
|
5
|
+
import { Vector2 } from "./types/vector2";
|
|
6
|
+
|
|
7
|
+
export interface MinimapConfig {
|
|
8
|
+
enabled?: boolean;
|
|
9
|
+
width?: number;
|
|
10
|
+
height?: number;
|
|
11
|
+
margin?: number;
|
|
12
|
+
|
|
13
|
+
/** Overrides Theme.Minimap.BackgroundColor for this minimap alone. */
|
|
14
|
+
backgroundColor?: string;
|
|
15
|
+
|
|
16
|
+
/** Overrides Theme.Minimap.BorderColor for this minimap alone. */
|
|
17
|
+
borderColor?: string;
|
|
18
|
+
|
|
19
|
+
/** Overrides Theme.Minimap.NodeColor for this minimap alone. */
|
|
20
|
+
nodeColor?: string;
|
|
21
|
+
|
|
22
|
+
/** Overrides Theme.Minimap.ViewportColor for this minimap alone. */
|
|
23
|
+
viewportColor?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class Minimap {
|
|
27
|
+
|
|
28
|
+
#enabled: boolean;
|
|
29
|
+
|
|
30
|
+
#width: number;
|
|
31
|
+
|
|
32
|
+
#height: number;
|
|
33
|
+
|
|
34
|
+
#margin: number;
|
|
35
|
+
|
|
36
|
+
#backgroundColor?: string;
|
|
37
|
+
|
|
38
|
+
#borderColor?: string;
|
|
39
|
+
|
|
40
|
+
#nodeColor?: string;
|
|
41
|
+
|
|
42
|
+
#viewportColor?: string;
|
|
43
|
+
|
|
44
|
+
#box: Box = { Position: { x: 0, y: 0 }, Size: { x: 0, y: 0 } };
|
|
45
|
+
|
|
46
|
+
#scale: number = 1;
|
|
47
|
+
|
|
48
|
+
#originX: number = 0;
|
|
49
|
+
|
|
50
|
+
#originY: number = 0;
|
|
51
|
+
|
|
52
|
+
// The padding the last frame was laid out with, so a click maps back
|
|
53
|
+
// through the same geometry it was drawn with.
|
|
54
|
+
#padding: number = Theme.Minimap.Padding;
|
|
55
|
+
|
|
56
|
+
#drawn: boolean = false;
|
|
57
|
+
|
|
58
|
+
constructor(config?: MinimapConfig) {
|
|
59
|
+
this.#enabled = config?.enabled === true;
|
|
60
|
+
this.#width = config?.width === undefined ? 200 : config.width;
|
|
61
|
+
this.#height = config?.height === undefined ? 150 : config.height;
|
|
62
|
+
this.#margin = config?.margin === undefined ? 16 : config.margin;
|
|
63
|
+
this.#backgroundColor = config?.backgroundColor;
|
|
64
|
+
this.#borderColor = config?.borderColor;
|
|
65
|
+
this.#nodeColor = config?.nodeColor;
|
|
66
|
+
this.#viewportColor = config?.viewportColor;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
enabled(): boolean {
|
|
70
|
+
return this.#enabled;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
setEnabled(enabled: boolean): void {
|
|
74
|
+
this.#enabled = enabled;
|
|
75
|
+
this.#drawn = false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
contains(position: Vector2): boolean {
|
|
79
|
+
return this.#enabled && this.#drawn && InBox(this.#box, position);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
graphPositionOf(position: Vector2, out: Vector2): void {
|
|
83
|
+
out.x = (position.x - this.#box.Position.x - this.#padding) / this.#scale + this.#originX;
|
|
84
|
+
out.y = (position.y - this.#box.Position.y - this.#padding) / this.#scale + this.#originY;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
render(ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, camera: Camera, nodes: Array<FlowNode>): void {
|
|
88
|
+
if (!this.#enabled) {
|
|
89
|
+
this.#drawn = false;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Bounds are taken with a fresh camera so they come back in graph
|
|
94
|
+
// space rather than wherever the real camera happens to be looking.
|
|
95
|
+
const flat = new Camera();
|
|
96
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
97
|
+
|
|
98
|
+
const boxes: Array<Box> = [];
|
|
99
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
100
|
+
const bounds = nodes[i].calculateBounds(ctx, flat);
|
|
101
|
+
boxes.push(bounds);
|
|
102
|
+
minX = Math.min(minX, bounds.Position.x);
|
|
103
|
+
minY = Math.min(minY, bounds.Position.y);
|
|
104
|
+
maxX = Math.max(maxX, bounds.Position.x + bounds.Size.x);
|
|
105
|
+
maxY = Math.max(maxY, bounds.Position.y + bounds.Size.y);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// What the camera can currently see, so the graph never scales out
|
|
109
|
+
// from under the viewport box.
|
|
110
|
+
const topLeft: Vector2 = { x: 0, y: 0 };
|
|
111
|
+
const bottomRight: Vector2 = { x: 0, y: 0 };
|
|
112
|
+
camera.screenSpaceToGraphSpace({ x: 0, y: 0 }, topLeft);
|
|
113
|
+
camera.screenSpaceToGraphSpace({ x: canvas.width, y: canvas.height }, bottomRight);
|
|
114
|
+
|
|
115
|
+
minX = Math.min(minX, topLeft.x);
|
|
116
|
+
minY = Math.min(minY, topLeft.y);
|
|
117
|
+
maxX = Math.max(maxX, bottomRight.x);
|
|
118
|
+
maxY = Math.max(maxY, bottomRight.y);
|
|
119
|
+
|
|
120
|
+
if (!isFinite(minX) || maxX <= minX || maxY <= minY) {
|
|
121
|
+
this.#drawn = false;
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.#box.Size.x = this.#width;
|
|
126
|
+
this.#box.Size.y = this.#height;
|
|
127
|
+
this.#box.Position.x = canvas.width - this.#width - this.#margin;
|
|
128
|
+
this.#box.Position.y = canvas.height - this.#height - this.#margin;
|
|
129
|
+
|
|
130
|
+
this.#padding = Theme.Minimap.Padding;
|
|
131
|
+
|
|
132
|
+
const usableWidth = this.#width - (this.#padding * 2);
|
|
133
|
+
const usableHeight = this.#height - (this.#padding * 2);
|
|
134
|
+
this.#scale = Math.min(usableWidth / (maxX - minX), usableHeight / (maxY - minY));
|
|
135
|
+
this.#originX = minX;
|
|
136
|
+
this.#originY = minY;
|
|
137
|
+
|
|
138
|
+
const left = this.#box.Position.x + this.#padding;
|
|
139
|
+
const top = this.#box.Position.y + this.#padding;
|
|
140
|
+
const toMapX = (x: number) => left + ((x - minX) * this.#scale);
|
|
141
|
+
const toMapY = (y: number) => top + ((y - minY) * this.#scale);
|
|
142
|
+
|
|
143
|
+
ctx.fillStyle = this.#backgroundColor ?? Theme.Minimap.BackgroundColor;
|
|
144
|
+
ctx.strokeStyle = this.#borderColor ?? Theme.Minimap.BorderColor;
|
|
145
|
+
ctx.lineWidth = 1;
|
|
146
|
+
ctx.beginPath();
|
|
147
|
+
ctx.roundRect(
|
|
148
|
+
this.#box.Position.x,
|
|
149
|
+
this.#box.Position.y,
|
|
150
|
+
this.#width,
|
|
151
|
+
this.#height,
|
|
152
|
+
Theme.Minimap.BorderRadius,
|
|
153
|
+
);
|
|
154
|
+
ctx.fill();
|
|
155
|
+
ctx.stroke();
|
|
156
|
+
|
|
157
|
+
ctx.fillStyle = this.#nodeColor ?? Theme.Minimap.NodeColor;
|
|
158
|
+
for (let i = 0; i < boxes.length; i++) {
|
|
159
|
+
const b = boxes[i];
|
|
160
|
+
ctx.fillRect(
|
|
161
|
+
toMapX(b.Position.x),
|
|
162
|
+
toMapY(b.Position.y),
|
|
163
|
+
Math.max(1, b.Size.x * this.#scale),
|
|
164
|
+
Math.max(1, b.Size.y * this.#scale),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
ctx.fillStyle = this.#viewportColor ?? Theme.Minimap.ViewportColor;
|
|
169
|
+
ctx.fillRect(
|
|
170
|
+
toMapX(topLeft.x),
|
|
171
|
+
toMapY(topLeft.y),
|
|
172
|
+
Math.max(1, (bottomRight.x - topLeft.x) * this.#scale),
|
|
173
|
+
Math.max(1, (bottomRight.y - topLeft.y) * this.#scale),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
this.#drawn = true;
|
|
177
|
+
}
|
|
178
|
+
}
|