@elicdavis/node-flow 0.1.15 → 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/.mocharc.json +5 -0
- package/dist/index.d.mts +139 -4
- package/dist/index.d.ts +139 -4
- package/dist/index.js +3754 -3352
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3741 -3352
- package/dist/index.mjs.map +1 -1
- package/index.html +0 -30
- package/package.json +4 -2
- package/src/graph.ts +234 -41
- package/src/index.ts +82 -40
- package/src/input.ts +22 -14
- package/src/markdown/entry.ts +26 -3
- package/src/markdown/lexicalParser.ts +22 -32
- package/src/markdown/syntaxParser.ts +93 -68
- package/src/minimap.ts +178 -0
- package/src/node.ts +114 -0
- package/src/nodes/factory.ts +9 -5
- package/src/nodes/publisher.ts +39 -4
- package/src/nodes/subsystem.ts +218 -8
- package/src/notes/subsystem.ts +3 -0
- package/src/organize.ts +276 -181
- package/src/port.ts +143 -7
- package/src/render_scheduler.ts +49 -0
- package/src/styles/text.ts +8 -0
- package/src/theme.ts +8 -0
- package/src/types/text.ts +8 -4
- package/src/utils/string.ts +7 -1
- package/src/widgets/color.ts +5 -6
- package/src/widgets/factory.ts +1 -1
- package/src/widgets/image.ts +2 -0
- package/src/widgets/number.ts +2 -2
- package/src/widgets/slider.ts +2 -2
- package/src/widgets/string.ts +2 -2
- package/src/widgets/toggle.ts +2 -2
- package/src/widgets/widget.ts +22 -0
- package/test/markdown.test.ts +165 -0
- package/test/minimap.test.ts +103 -0
- package/test/node_menu_filter.test.ts +119 -0
- package/test/organize.test.ts +156 -0
- package/test/port.test.ts +125 -0
- package/test/render_scheduler.test.ts +119 -0
- package/tsconfig.json +6 -2
package/src/markdown/entry.ts
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
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
|
|
8
9
|
}
|
|
9
10
|
|
|
11
|
+
function onFontsLoaded(callback: () => void): void {
|
|
12
|
+
if (typeof document === "undefined" || document.fonts === undefined) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
document.fonts.addEventListener("loadingdone", () => {
|
|
16
|
+
callback();
|
|
17
|
+
requestRender();
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
10
21
|
export class CodeBlockEntry {
|
|
11
22
|
|
|
12
23
|
#text: Text
|
|
@@ -17,6 +28,10 @@ export class CodeBlockEntry {
|
|
|
17
28
|
|
|
18
29
|
#calculatedForWidth: number;
|
|
19
30
|
|
|
31
|
+
text(): Text {
|
|
32
|
+
return this.#text;
|
|
33
|
+
}
|
|
34
|
+
|
|
20
35
|
constructor(text: Text) {
|
|
21
36
|
this.#text = text;
|
|
22
37
|
this.#calculatedForWidth = -1
|
|
@@ -24,7 +39,7 @@ export class CodeBlockEntry {
|
|
|
24
39
|
this.#calculatedEntries = new List<Text>();
|
|
25
40
|
this.#calculatedPositions = new List<Vector2>();
|
|
26
41
|
|
|
27
|
-
|
|
42
|
+
onFontsLoaded(() => {
|
|
28
43
|
this.#calculatedForWidth = -1
|
|
29
44
|
});
|
|
30
45
|
}
|
|
@@ -113,6 +128,10 @@ export class UnorderedListMarkdownEntry {
|
|
|
113
128
|
this.#entries = entries;
|
|
114
129
|
}
|
|
115
130
|
|
|
131
|
+
entries(): ReadonlyArray<BasicMarkdownEntry> {
|
|
132
|
+
return this.#entries;
|
|
133
|
+
}
|
|
134
|
+
|
|
116
135
|
render(ctx: CanvasRenderingContext2D, position: Vector2, scale: number, maxWidth: number): number {
|
|
117
136
|
let offset = 0;
|
|
118
137
|
let shift = 20;
|
|
@@ -152,6 +171,10 @@ export class BasicMarkdownEntry {
|
|
|
152
171
|
|
|
153
172
|
#calculatedForWidth: number;
|
|
154
173
|
|
|
174
|
+
lines(): ReadonlyArray<Text> {
|
|
175
|
+
return this.#entries;
|
|
176
|
+
}
|
|
177
|
+
|
|
155
178
|
constructor(lines: Array<Text>, underline: boolean, background: boolean) {
|
|
156
179
|
this.#entries = lines;
|
|
157
180
|
this.#underline = underline;
|
|
@@ -161,7 +184,7 @@ export class BasicMarkdownEntry {
|
|
|
161
184
|
this.#calculatedEntries = new List<Text>();
|
|
162
185
|
this.#calculatedPositions = new List<Vector2>();
|
|
163
186
|
|
|
164
|
-
|
|
187
|
+
onFontsLoaded(() => {
|
|
165
188
|
this.#calculatedForWidth = -1
|
|
166
189
|
});
|
|
167
190
|
}
|
|
@@ -37,32 +37,27 @@ export class MarkdownLexicalParser {
|
|
|
37
37
|
return this.#body.charAt(this.#index);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
#
|
|
41
|
-
|
|
42
|
-
#addToken(token: MarkdownTokenType, lexeme: string): void {
|
|
43
|
-
this.#tokens.push(new MarkdownToken(token, lexeme, this.#lastToken, this.#index));
|
|
44
|
-
this.#lastToken = this.#index;
|
|
45
|
-
}
|
|
40
|
+
#tokenStart = 0;
|
|
46
41
|
|
|
47
|
-
#
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
this.#addToken(MarkdownTokenType.H3, "###");
|
|
42
|
+
#addToken(token: MarkdownTokenType, lexeme: string, start?: number): void {
|
|
43
|
+
this.#tokens.push(new MarkdownToken(token, lexeme, start ?? this.#tokenStart, this.#index));
|
|
44
|
+
}
|
|
51
45
|
|
|
52
|
-
|
|
46
|
+
#header(): void {
|
|
47
|
+
let count = 0;
|
|
48
|
+
while (this.#current() === "#") {
|
|
49
|
+
count++;
|
|
53
50
|
this.#inc();
|
|
54
|
-
} else {
|
|
55
|
-
this.#addToken(MarkdownTokenType.H2, "##");
|
|
56
51
|
}
|
|
57
|
-
}
|
|
58
52
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
this.#addToken(MarkdownTokenType.H1, "#");
|
|
53
|
+
let type = MarkdownTokenType.H3;
|
|
54
|
+
if (count === 1) {
|
|
55
|
+
type = MarkdownTokenType.H1;
|
|
56
|
+
} else if (count === 2) {
|
|
57
|
+
type = MarkdownTokenType.H2;
|
|
65
58
|
}
|
|
59
|
+
|
|
60
|
+
this.#addToken(type, "#".repeat(count));
|
|
66
61
|
}
|
|
67
62
|
|
|
68
63
|
#whiteSpace(): void {
|
|
@@ -110,34 +105,29 @@ export class MarkdownLexicalParser {
|
|
|
110
105
|
}
|
|
111
106
|
|
|
112
107
|
if (started != -1) {
|
|
113
|
-
this.#addToken(MarkdownTokenType.Text, this.#body.substring(started, this.#index))
|
|
108
|
+
this.#addToken(MarkdownTokenType.Text, this.#body.substring(started, this.#index), started)
|
|
114
109
|
}
|
|
115
110
|
}
|
|
116
111
|
|
|
117
|
-
// S( ) => S
|
|
118
|
-
// S(\t) => S
|
|
119
|
-
// S(#) => H1
|
|
120
|
-
// H1(#) => H2
|
|
121
|
-
// H1(!#) => S
|
|
122
|
-
// H2(#) => H3
|
|
123
|
-
// H2(#) => H3
|
|
124
112
|
parse(): void {
|
|
125
113
|
let char = this.#current();
|
|
126
114
|
|
|
127
115
|
while (char !== "") {
|
|
116
|
+
this.#tokenStart = this.#index;
|
|
117
|
+
|
|
128
118
|
if (char === " " || char === "\t") {
|
|
129
119
|
this.#whiteSpace();
|
|
130
120
|
} else if (char === "#") {
|
|
131
|
-
this.#
|
|
121
|
+
this.#header();
|
|
132
122
|
} else if (char === "\n") {
|
|
133
|
-
this.#addToken(MarkdownTokenType.NewLine, "\n");
|
|
134
123
|
this.#inc();
|
|
124
|
+
this.#addToken(MarkdownTokenType.NewLine, "\n");
|
|
135
125
|
} else if (char === "*") {
|
|
136
|
-
this.#addToken(MarkdownTokenType.Star, "*");
|
|
137
126
|
this.#inc();
|
|
127
|
+
this.#addToken(MarkdownTokenType.Star, "*");
|
|
138
128
|
} else if (char === "`") {
|
|
139
|
-
this.#addToken(MarkdownTokenType.BackTick, "`");
|
|
140
129
|
this.#inc();
|
|
130
|
+
this.#addToken(MarkdownTokenType.BackTick, "`");
|
|
141
131
|
} else {
|
|
142
132
|
this.#text();
|
|
143
133
|
}
|
|
@@ -31,21 +31,18 @@ export class MarkdownSyntaxParser {
|
|
|
31
31
|
|
|
32
32
|
#next(): MarkdownToken | null {
|
|
33
33
|
this.#index++
|
|
34
|
-
if (this.#index >= this.#tokens.length
|
|
34
|
+
if (this.#index >= this.#tokens.length) {
|
|
35
35
|
return null
|
|
36
36
|
}
|
|
37
37
|
return this.#tokens[this.#index];
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
#peak(): MarkdownToken | null {
|
|
41
|
-
|
|
42
|
-
return null
|
|
43
|
-
}
|
|
44
|
-
return this.#tokens[this.#index + 1];
|
|
41
|
+
return this.#peakInto(1);
|
|
45
42
|
}
|
|
46
43
|
|
|
47
44
|
#peakInto(amount: number): MarkdownToken | null {
|
|
48
|
-
if (this.#index + amount >= this.#tokens.length
|
|
45
|
+
if (this.#index + amount >= this.#tokens.length) {
|
|
49
46
|
return null
|
|
50
47
|
}
|
|
51
48
|
return this.#tokens[this.#index + amount];
|
|
@@ -53,82 +50,107 @@ export class MarkdownSyntaxParser {
|
|
|
53
50
|
|
|
54
51
|
#emphasis(): Array<Text> {
|
|
55
52
|
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
// 11. * Test: <ul>Test</ul>
|
|
69
|
-
|
|
70
|
-
let token = this.#next();
|
|
71
|
-
if (token?.type() === MarkdownTokenType.Space) {
|
|
53
|
+
// Count the stars either side of the content and match as many as
|
|
54
|
+
// both ends can supply; the rest stay on the line as characters.
|
|
55
|
+
//
|
|
56
|
+
// *a* -> <i>a</i> **a* -> *<i>a</i>
|
|
57
|
+
// **a** -> <b>a</b> *a** -> <i>a</i>*
|
|
58
|
+
// **a f** -> <b>a f</b> *a -> *a
|
|
59
|
+
// ** a ** -> ** a ** **a ** -> **a **
|
|
60
|
+
|
|
61
|
+
// A star with a space after it opens nothing, so leave the space
|
|
62
|
+
// where it is rather than consuming it - the caller still has to
|
|
63
|
+
// read it as part of the line.
|
|
64
|
+
if (this.#peak()?.type() === MarkdownTokenType.Space) {
|
|
72
65
|
return [new Text("*")];
|
|
73
66
|
}
|
|
74
67
|
|
|
75
|
-
|
|
76
|
-
// Read off all leading stars
|
|
77
|
-
while (token !== null && token.type() === MarkdownTokenType.Star) {
|
|
78
|
-
bold = true;
|
|
79
|
-
token = this.#next();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
let textContent = "";
|
|
68
|
+
const openers = this.#runOfStars();
|
|
83
69
|
|
|
84
|
-
let
|
|
85
|
-
let
|
|
86
|
-
|
|
87
|
-
|
|
70
|
+
let content = "";
|
|
71
|
+
let closers = 0;
|
|
72
|
+
while (true) {
|
|
73
|
+
const token = this.#current();
|
|
74
|
+
if (token === null || token.type() === MarkdownTokenType.NewLine) {
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
88
77
|
if (token.type() === MarkdownTokenType.Star) {
|
|
78
|
+
closers = this.#runOfStars();
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
content += token.lexeme();
|
|
82
|
+
this.#inc();
|
|
83
|
+
}
|
|
89
84
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
break;
|
|
94
|
-
}
|
|
85
|
+
// Leave the reader on the last token taken, since the caller steps
|
|
86
|
+
// forward once more before looking at anything.
|
|
87
|
+
this.#index--;
|
|
95
88
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
89
|
+
// A run of stars only emphasises what it hugs. With a space against
|
|
90
|
+
// the inside of either end, or nothing between them at all, the
|
|
91
|
+
// stars are just stars.
|
|
92
|
+
const hugsContent = content !== ""
|
|
93
|
+
&& content.trimStart() === content
|
|
94
|
+
&& content.trimEnd() === content;
|
|
102
95
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
96
|
+
const matched = hugsContent ? Math.min(openers, closers) : 0;
|
|
97
|
+
if (matched === 0) {
|
|
98
|
+
return [new Text("*".repeat(openers) + content + "*".repeat(closers))];
|
|
99
|
+
}
|
|
106
100
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
101
|
+
const style: TextStyleConfig = {};
|
|
102
|
+
if (matched >= 2) {
|
|
103
|
+
style.weight = FontWeight.Bold;
|
|
104
|
+
} else {
|
|
105
|
+
style.style = FontStyle.Italic;
|
|
106
|
+
}
|
|
112
107
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
108
|
+
// Stars past the pair that matched were never part of the emphasis,
|
|
109
|
+
// so they stay on the line as the characters they are.
|
|
110
|
+
const runs = new Array<Text>();
|
|
111
|
+
if (openers > matched) {
|
|
112
|
+
runs.push(new Text("*".repeat(openers - matched)));
|
|
113
|
+
}
|
|
114
|
+
runs.push(new Text(content, style));
|
|
115
|
+
if (closers > matched) {
|
|
116
|
+
runs.push(new Text("*".repeat(closers - matched)));
|
|
117
|
+
}
|
|
118
|
+
return runs;
|
|
119
|
+
}
|
|
117
120
|
|
|
118
|
-
|
|
121
|
+
/** Consumes a run of stars, leaving the reader on the token after it. */
|
|
122
|
+
#runOfStars(): number {
|
|
123
|
+
let count = 0;
|
|
124
|
+
while (this.#current()?.type() === MarkdownTokenType.Star) {
|
|
125
|
+
count++;
|
|
126
|
+
this.#inc();
|
|
119
127
|
}
|
|
128
|
+
return count;
|
|
129
|
+
}
|
|
120
130
|
|
|
121
|
-
|
|
131
|
+
/**
|
|
132
|
+
* Reads `code` up to the closing backtick on the same line. An opening
|
|
133
|
+
* backtick with no partner is only a backtick.
|
|
134
|
+
*/
|
|
135
|
+
#inlineCode(): Array<Text> {
|
|
136
|
+
const opened = this.#index;
|
|
122
137
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
138
|
+
this.#inc();
|
|
139
|
+
let content = "";
|
|
140
|
+
while (true) {
|
|
141
|
+
const token = this.#current();
|
|
142
|
+
if (token === null || token.type() === MarkdownTokenType.NewLine) {
|
|
143
|
+
this.#index = opened;
|
|
144
|
+
return [new Text("`")];
|
|
128
145
|
}
|
|
146
|
+
if (token.type() === MarkdownTokenType.BackTick) {
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
content += token.lexeme();
|
|
150
|
+
this.#inc();
|
|
129
151
|
}
|
|
130
152
|
|
|
131
|
-
return [new Text(
|
|
153
|
+
return [new Text(content, { font: "monospace" })];
|
|
132
154
|
}
|
|
133
155
|
|
|
134
156
|
#text(): Array<Text> {
|
|
@@ -153,13 +175,16 @@ export class MarkdownSyntaxParser {
|
|
|
153
175
|
break;
|
|
154
176
|
|
|
155
177
|
case MarkdownTokenType.Star:
|
|
178
|
+
case MarkdownTokenType.BackTick:
|
|
156
179
|
if (textContent !== "") {
|
|
157
180
|
contents.push(new Text(textContent))
|
|
158
181
|
textContent = "";
|
|
159
182
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
183
|
+
const runs = token.type() === MarkdownTokenType.Star
|
|
184
|
+
? this.#emphasis()
|
|
185
|
+
: this.#inlineCode();
|
|
186
|
+
for (let i = 0; i < runs.length; i++) {
|
|
187
|
+
contents.push(runs[i]);
|
|
163
188
|
}
|
|
164
189
|
break;
|
|
165
190
|
}
|
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
|
+
}
|