@elicdavis/node-flow 0.1.0

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.
Files changed (68) hide show
  1. package/.github/workflows/deploy.yml +57 -0
  2. package/README.md +159 -0
  3. package/dev.tsconfig.json +18 -0
  4. package/docs/promotional-image.png +0 -0
  5. package/esbuild.ts +32 -0
  6. package/index.html +311 -0
  7. package/package.json +50 -0
  8. package/src/camera.ts +30 -0
  9. package/src/connection.ts +249 -0
  10. package/src/contextMenu.ts +367 -0
  11. package/src/draggable.ts +13 -0
  12. package/src/elements/base.ts +192 -0
  13. package/src/elements/container.ts +119 -0
  14. package/src/elements/interfaces.ts +50 -0
  15. package/src/elements/text.ts +86 -0
  16. package/src/graph.ts +535 -0
  17. package/src/graphSubsystem.ts +31 -0
  18. package/src/index.ts +28 -0
  19. package/src/input.ts +153 -0
  20. package/src/markdown/entry.ts +323 -0
  21. package/src/markdown/lexicalParser.ts +148 -0
  22. package/src/markdown/markdown.ts +14 -0
  23. package/src/markdown/syntaxParser.ts +344 -0
  24. package/src/markdown/token.ts +43 -0
  25. package/src/metadata.ts +3 -0
  26. package/src/node.ts +1130 -0
  27. package/src/nodes/factory.ts +87 -0
  28. package/src/nodes/publisher.ts +98 -0
  29. package/src/nodes/subsystem.ts +760 -0
  30. package/src/notes/note.ts +295 -0
  31. package/src/notes/subsystem.ts +262 -0
  32. package/src/organize.ts +181 -0
  33. package/src/pass/subsystem.ts +64 -0
  34. package/src/performance.ts +9 -0
  35. package/src/popup.ts +81 -0
  36. package/src/popups/form.ts +63 -0
  37. package/src/popups/string.ts +33 -0
  38. package/src/port.ts +223 -0
  39. package/src/quickMenu.ts +330 -0
  40. package/src/styles/box.ts +104 -0
  41. package/src/styles/canvasTextAlign.ts +8 -0
  42. package/src/styles/canvasTextBaseline.ts +9 -0
  43. package/src/styles/cursor.ts +18 -0
  44. package/src/styles/stroke.ts +39 -0
  45. package/src/styles/text.ts +90 -0
  46. package/src/styles/textBox.ts +82 -0
  47. package/src/theme.ts +78 -0
  48. package/src/types/box.ts +126 -0
  49. package/src/types/list.ts +41 -0
  50. package/src/types/pool.ts +66 -0
  51. package/src/types/text.ts +136 -0
  52. package/src/types/vector2.ts +34 -0
  53. package/src/utils/color.ts +136 -0
  54. package/src/utils/html.ts +21 -0
  55. package/src/utils/math.ts +8 -0
  56. package/src/utils/string.ts +98 -0
  57. package/src/widgets/button.ts +107 -0
  58. package/src/widgets/color.ts +151 -0
  59. package/src/widgets/factory.ts +46 -0
  60. package/src/widgets/image.ts +163 -0
  61. package/src/widgets/number.ts +153 -0
  62. package/src/widgets/slider.ts +216 -0
  63. package/src/widgets/string.ts +128 -0
  64. package/src/widgets/text.ts +92 -0
  65. package/src/widgets/toggle.ts +205 -0
  66. package/src/widgets/widget.ts +12 -0
  67. package/tsconfig.json +15 -0
  68. package/tsup.config.ts +10 -0
@@ -0,0 +1,249 @@
1
+ import { FlowNode } from "./node";
2
+ import { Port } from "./port";
3
+ import { BoxCenter, InBox } from "./types/box";
4
+ import { Vector2, Zero } from "./types/vector2";
5
+
6
+ export interface ConnectionRendererParams {
7
+ ctx: CanvasRenderingContext2D;
8
+ start: Vector2;
9
+ end: Vector2;
10
+ graphScale: number;
11
+ mouseOver: boolean;
12
+
13
+ inNode: FlowNode | null;
14
+ inPort: Port | null;
15
+ outNode: FlowNode | null;
16
+ outPort: Port | null;
17
+ }
18
+
19
+ export type ConnectionRenderer = (params: ConnectionRendererParams) => void
20
+
21
+ export function DefaultConnectionRenderer(connectionSize: number, connectionColor: string | undefined, mouseOverSize: number, mouseOverColor: string | undefined): ConnectionRenderer {
22
+ return (params: ConnectionRendererParams) => {
23
+
24
+ let color = "#00FF00";
25
+ if (params.outPort !== null) {
26
+ color = params.outPort.filledStyleColor()
27
+ } else if (params.inPort !== null) {
28
+ color = params.inPort.filledStyleColor()
29
+ }
30
+
31
+ let lineSize = connectionSize * params.graphScale;
32
+ if (connectionColor !== undefined) {
33
+ color = connectionColor;
34
+ }
35
+
36
+ if (params.mouseOver || params.inNode?.selected() || params.outNode?.selected()) {
37
+ lineSize = mouseOverSize * params.graphScale;
38
+ params.ctx.shadowBlur = 25 * params.graphScale;
39
+
40
+ if (mouseOverColor !== undefined) {
41
+ color = mouseOverColor;
42
+ }
43
+
44
+ params.ctx.shadowColor = color;
45
+ }
46
+
47
+ params.ctx.strokeStyle = color;
48
+ params.ctx.lineWidth = lineSize;
49
+
50
+ // Draw
51
+ params.ctx.beginPath();
52
+ params.ctx.moveTo(params.start.x, params.start.y);
53
+ const midX = (params.start.x + params.end.x) / 2;
54
+ params.ctx.bezierCurveTo(midX, params.start.y, midX, params.end.y, params.end.x, params.end.y);
55
+ params.ctx.stroke();
56
+ params.ctx.shadowBlur = 0;
57
+ }
58
+ }
59
+
60
+ export class Connection {
61
+
62
+ // Variables for holding temp data, preventing object allocations each
63
+ // frame
64
+ #inPos: Vector2;
65
+ #outPos: Vector2;
66
+
67
+ #inNode: FlowNode | null;
68
+ #inNodePortIndex: number;
69
+ #outNode: FlowNode | null;
70
+ #outNodePortIndex: number;
71
+ #renderer: ConnectionRenderer;
72
+
73
+ constructor(
74
+ inNode: FlowNode | null,
75
+ inNodePortIndex: number,
76
+ outNode: FlowNode | null,
77
+ outNodePortIndex: number,
78
+ renderer: ConnectionRenderer,
79
+ ) {
80
+
81
+ this.#inNode = inNode;
82
+ this.#inNodePortIndex = inNodePortIndex;
83
+ this.#outNode = outNode;
84
+ this.#outNodePortIndex = outNodePortIndex;
85
+ this.#renderer = renderer;
86
+
87
+ this.#inPos = Zero();
88
+ this.#outPos = Zero();
89
+
90
+ if (inNode !== null) {
91
+ inNode.inputPort(this.#inNodePortIndex).addConnection(this);
92
+ }
93
+
94
+ if (outNode !== null) {
95
+ outNode.outputPort(this.#outNodePortIndex).addConnection(this);
96
+ }
97
+ }
98
+
99
+ render(ctx: CanvasRenderingContext2D, graphScale: number, mouseOver: boolean, mousePosition: Vector2 | undefined): void {
100
+
101
+ // Not sure what to do here? Maybe we should throw an error in the
102
+ // future?
103
+ if (this.#inNode === null && this.#outNode === null) {
104
+ return;
105
+ }
106
+
107
+ if (this.#inNode !== null) {
108
+ const inPortBox = this.#inNode.inputPortPosition(this.#inNodePortIndex)
109
+ if (inPortBox === undefined) {
110
+ return;
111
+ }
112
+ BoxCenter(inPortBox, this.#inPos);
113
+ } else if (mousePosition !== undefined) {
114
+ this.#inPos.x = mousePosition.x;
115
+ this.#inPos.y = mousePosition.y;
116
+ } else {
117
+ return;
118
+ }
119
+
120
+ if (this.#outNode !== null) {
121
+ const outPortBox = this.#outNode.outputPortPosition(this.#outNodePortIndex);
122
+ if (outPortBox === undefined) {
123
+ return;
124
+ }
125
+ BoxCenter(outPortBox, this.#outPos);
126
+ } else if (mousePosition !== undefined) {
127
+ this.#outPos.x = mousePosition.x;
128
+ this.#outPos.y = mousePosition.y;
129
+ } else {
130
+ return;
131
+ }
132
+
133
+ this.#renderer({
134
+ ctx: ctx,
135
+ start: this.#inPos,
136
+ end: this.#outPos,
137
+ graphScale: graphScale,
138
+ mouseOver: mouseOver,
139
+
140
+ inPort: this.inPort(),
141
+ inNode: this.#inNode,
142
+ outPort: this.outPort(),
143
+ outNode: this.#outNode,
144
+ });
145
+ }
146
+
147
+ inNode(): FlowNode | null {
148
+ return this.#inNode;
149
+ }
150
+
151
+ outNode(): FlowNode | null {
152
+ return this.#outNode;
153
+ }
154
+
155
+ clearPort(mousePosition: Vector2): void {
156
+ if (this.#inNode !== null) {
157
+ const inPortBox = this.#inNode.inputPortPosition(this.#inNodePortIndex)
158
+ if (inPortBox !== undefined && InBox(inPortBox, mousePosition)) {
159
+ this.clearInput();
160
+ }
161
+ }
162
+
163
+ if (this.#outNode !== null) {
164
+ const outPortBox = this.#outNode.outputPortPosition(this.#outNodePortIndex);
165
+ if (outPortBox !== undefined && InBox(outPortBox, mousePosition)) {
166
+ this.clearOutput();
167
+ }
168
+ }
169
+ }
170
+
171
+ clearPorts(): void {
172
+ this.clearInput();
173
+ this.clearOutput();
174
+ }
175
+
176
+ setInput(node: FlowNode, portIndex: number, replace?: boolean): void {
177
+ this.#inNode = node;
178
+ this.#inNodePortIndex = portIndex;
179
+
180
+ const port = this.#inNode.inputPort(portIndex);
181
+ if(!!replace && port.connections().length > 0) {
182
+ port.replaceConnection(this, 0);
183
+ } else {
184
+ port.addConnection(this);
185
+ }
186
+ }
187
+
188
+ setOutput(node: FlowNode, portIndex: number): void {
189
+ this.#outNode = node;
190
+ this.#outNodePortIndex = portIndex;
191
+ this.#outNode.outputPort(portIndex).addConnection(this);
192
+ }
193
+
194
+ clearInput() {
195
+ this.#inNode?.inputPort(this.#inNodePortIndex).clearConnection(this);
196
+ this.#inNode = null;
197
+ this.#inNodePortIndex = -1;
198
+ }
199
+
200
+ clearOutput() {
201
+ this.#outNode?.outputPort(this.#outNodePortIndex).clearConnection(this);
202
+ this.#outNode = null;
203
+ this.#outNodePortIndex = -1;
204
+ }
205
+
206
+ mouseOverPort(mousePosition: Vector2): Port | null {
207
+ if (this.#inNode !== null) {
208
+ const inPortBox = this.#inNode.inputPortPosition(this.#inNodePortIndex)
209
+ if (inPortBox !== undefined && InBox(inPortBox, mousePosition)) {
210
+ return this.#inNode.inputPort(this.#inNodePortIndex)
211
+ }
212
+ }
213
+
214
+ if (this.#outNode !== null) {
215
+ const outPortBox = this.#outNode.outputPortPosition(this.#outNodePortIndex);
216
+ if (outPortBox !== undefined && InBox(outPortBox, mousePosition)) {
217
+ return this.#outNode.outputPort(this.#outNodePortIndex)
218
+ }
219
+ }
220
+
221
+ return null;
222
+ }
223
+
224
+ outPort(): Port | null {
225
+ if (this.#outNode === null) {
226
+ return null;
227
+ }
228
+ return this.#outNode.outputPort(this.#outNodePortIndex);
229
+ }
230
+
231
+ inPort(): Port | null {
232
+ if (this.#inNode === null) {
233
+ return null;
234
+ }
235
+ return this.#inNode.inputPort(this.#inNodePortIndex);
236
+ }
237
+
238
+ referencesNode(node: FlowNode): boolean {
239
+ if (this.#inNode === node) {
240
+ return true;
241
+ }
242
+
243
+ if (this.#outNode === node) {
244
+ return true;
245
+ }
246
+
247
+ return false;
248
+ }
249
+ }
@@ -0,0 +1,367 @@
1
+ import { Theme } from './theme';
2
+ import { TextAlign } from './styles/canvasTextAlign';
3
+ import { TextStyle, TextStyleConfig, TextStyleFallback } from './styles/text';
4
+ import { Box, InBox } from './types/box';
5
+ import { List } from './types/list';
6
+ import { CopyVector2, Vector2, Zero } from './types/vector2';
7
+
8
+ const contextEntryHeight = 30;
9
+ const contextEntryWidth = 250;
10
+
11
+ export interface ContextMenuItemConfig {
12
+ name?: string;
13
+ textStyle?: TextStyleConfig;
14
+ group?: string;
15
+
16
+ callback?: () => void;
17
+ }
18
+
19
+ export interface ContextMenuConfig {
20
+ name?: string;
21
+ textStyle?: TextStyleConfig;
22
+ group?: string;
23
+
24
+ subMenus?: Array<ContextMenuConfig>;
25
+ items?: Array<ContextMenuItemConfig>;
26
+ }
27
+
28
+
29
+ export class ContextMenuItem {
30
+ #name: string;
31
+
32
+ #callback?: () => void;
33
+
34
+ #textStyle: TextStyle;
35
+
36
+ public group?: string;
37
+
38
+ constructor(config?: ContextMenuItemConfig) {
39
+ this.#name = config?.name === undefined ? "item" : config.name;
40
+ this.#callback = config?.callback;
41
+ this.#textStyle = new TextStyle(config?.textStyle);
42
+ this.group = config?.group;
43
+ }
44
+
45
+ getName(): string {
46
+ return this.#name;
47
+ }
48
+
49
+ execute(): void {
50
+ if (this.#callback === undefined) {
51
+ return;
52
+ }
53
+ this.#callback();
54
+ }
55
+ }
56
+
57
+ class ContextGroup {
58
+
59
+ #calculatedHeight: number;
60
+
61
+ constructor(public entries: Array<ContextEntry>) {
62
+ this.#calculatedHeight = entries.length * contextEntryHeight
63
+ }
64
+
65
+ height() {
66
+ return this.#calculatedHeight;
67
+ }
68
+
69
+ }
70
+
71
+ export class ContextEntry {
72
+ constructor(public text: string, public subMenu: ContextMenu | undefined, public item: ContextMenuItem | undefined) {
73
+ }
74
+
75
+ click(): void {
76
+ this.item?.execute();
77
+ }
78
+ }
79
+
80
+ export function CombineContextMenus(...contextMenus: Array<ContextMenuConfig | undefined>): ContextMenuConfig {
81
+ const finalConfig: ContextMenuConfig = {
82
+ items: new Array<ContextMenuItemConfig>,
83
+ subMenus: new Array<ContextMenuConfig>,
84
+ }
85
+
86
+ for (let i = 0; i < contextMenus.length; i++) {
87
+ const config = contextMenus[i];
88
+ if (config === undefined) {
89
+ continue;
90
+ }
91
+
92
+ if (config.items !== undefined) {
93
+ finalConfig.items = finalConfig.items?.concat(config.items);
94
+ }
95
+
96
+ if (config.subMenus !== undefined) {
97
+ finalConfig.subMenus = finalConfig.subMenus?.concat(config.subMenus);
98
+ }
99
+ }
100
+
101
+ return finalConfig;
102
+ }
103
+
104
+ export class ContextMenu {
105
+
106
+ #name: string;
107
+
108
+ #items: Array<ContextMenuItem>;
109
+
110
+ #subMenus: Array<ContextMenu>;
111
+
112
+ #textStyle: TextStyle;
113
+
114
+ #groups: List<ContextGroup>;
115
+
116
+ #group?: string;
117
+
118
+ #submenuPosition: Vector2;
119
+
120
+ constructor(config?: ContextMenuConfig) {
121
+ this.#submenuPosition = Zero();
122
+ this.#groups = new List<ContextGroup>();
123
+ this.#name = config?.name === undefined ? "menu" : config?.name;
124
+ this.#items = new Array<ContextMenuItem>();
125
+ this.#subMenus = new Array<ContextMenu>();
126
+ this.#textStyle = new TextStyle(TextStyleFallback(config?.textStyle, {
127
+ color: Theme.ContextMenu.FontColor
128
+ }));
129
+ this.#group = config?.group;
130
+
131
+ if (config?.subMenus !== undefined) {
132
+ for (let i = 0; i < config?.subMenus.length; i++) {
133
+ this.#subMenus.push(new ContextMenu(config?.subMenus[i]));
134
+ }
135
+ }
136
+
137
+ if (config?.items !== undefined) {
138
+ for (let i = 0; i < config?.items.length; i++) {
139
+ this.#items.push(new ContextMenuItem(config?.items[i]));
140
+ }
141
+ }
142
+
143
+ this.#calculateEntries();
144
+ }
145
+
146
+ #calculateEntries(): void {
147
+
148
+ const groupLUT = new Map<string, number>();
149
+ const workingGroups = new Array<Array<ContextEntry>>();
150
+
151
+ // Initialize first group for undefined entries
152
+ workingGroups.push(new Array<ContextEntry>());
153
+
154
+ for (let i = 0; i < this.#items.length; i++) {
155
+ let group = 0; // index to undefined entries
156
+
157
+ // Find collection that this entry belongs too
158
+ const sub = this.#items[i]
159
+ if (sub.group !== undefined) {
160
+ const groupIndex = groupLUT.get(sub.group)
161
+ if (groupIndex !== undefined) {
162
+ group = groupIndex
163
+ } else {
164
+ group = workingGroups.length
165
+ groupLUT.set(sub.group, group)
166
+ workingGroups.push(new Array<ContextEntry>());
167
+ }
168
+ }
169
+
170
+ workingGroups[group].push(new ContextEntry(
171
+ this.#items[i].getName(),
172
+ undefined,
173
+ this.#items[i]
174
+ ));
175
+ }
176
+
177
+ for (let i = 0; i < this.#subMenus.length; i++) {
178
+ let group = 0; // index to undefined entries
179
+
180
+ // Find collection that this entry belongs too
181
+ const sub = this.#subMenus[i]
182
+ if (sub.#group !== undefined) {
183
+ const groupIndex = groupLUT.get(sub.#group)
184
+ if (groupIndex !== undefined) {
185
+ group = groupIndex
186
+ } else {
187
+ group = workingGroups.length
188
+ groupLUT.set(sub.#group, group)
189
+ workingGroups.push(new Array<ContextEntry>());
190
+ }
191
+ }
192
+
193
+ workingGroups[group].push(new ContextEntry(
194
+ this.#subMenus[i].getName(),
195
+ this.#subMenus[i],
196
+ undefined
197
+ ));
198
+ }
199
+
200
+ this.#groups.Clear();
201
+ for (let i = 0; i < workingGroups.length; i++) {
202
+ const groupContent = workingGroups[i];
203
+ if (groupContent.length === 0) {
204
+ continue;
205
+ }
206
+ this.#groups.Push(new ContextGroup(groupContent));
207
+ }
208
+ }
209
+
210
+ #calculatedWidth: number = 0;
211
+
212
+ #getMaxWidthForText(ctx: CanvasRenderingContext2D, scale: number): number {
213
+ if (this.#calculatedWidth > 0) {
214
+ return this.#calculatedWidth;
215
+ }
216
+
217
+ const tempVec = Zero();
218
+ for (let groupIndex = 0; groupIndex < this.#groups.Count(); groupIndex++) {
219
+ const group = this.#groups.At(groupIndex);
220
+ for (let entryIndex = 0; entryIndex < group.entries.length; entryIndex++) {
221
+ this.#textStyle.measure(ctx, scale, group.entries[entryIndex].text, tempVec);
222
+ this.#calculatedWidth = Math.max(tempVec.x, this.#calculatedWidth);
223
+ }
224
+ }
225
+ return this.#calculatedWidth;
226
+ }
227
+
228
+ getName(): string {
229
+ return this.#name;
230
+ }
231
+
232
+ #tempBox: Box = { Position: Zero(), Size: Zero() };
233
+
234
+ #openSubMenu: ContextMenu | undefined;
235
+
236
+
237
+ public render(ctx: CanvasRenderingContext2D, pppp: Vector2, graphScale: number, mousePosition: Vector2 | undefined, openRight: boolean): ContextEntry | null {
238
+ const menuScale = 1.25;
239
+ const scaledEntryHeight = menuScale * contextEntryHeight;
240
+ const scaledEntryWidth = (menuScale * 40) + (this.#getMaxWidthForText(ctx, menuScale)); // contextEntryWidth;
241
+
242
+ let totalScaledHeight = 0
243
+ for (let i = 0; i < this.#groups.Count(); i++) {
244
+ totalScaledHeight += this.#groups.At(i).height();
245
+ }
246
+ totalScaledHeight *= menuScale;
247
+
248
+ const position = { x: 0, y: 0 };
249
+ CopyVector2(position, pppp)
250
+
251
+ if (!openRight) {
252
+ position.x -= scaledEntryWidth;
253
+ }
254
+
255
+ // Clamp the position so it's not spilling off the canvas
256
+ if (position.y + totalScaledHeight > ctx.canvas.clientHeight) {
257
+ position.y = ctx.canvas.clientHeight - totalScaledHeight;
258
+ }
259
+
260
+ let submenuOpenRight = openRight;
261
+ if (openRight && position.x + scaledEntryWidth > ctx.canvas.clientWidth) {
262
+ position.x = ctx.canvas.clientWidth - scaledEntryWidth;
263
+ submenuOpenRight = !submenuOpenRight;
264
+ }
265
+
266
+ this.#tempBox.Size.x = scaledEntryWidth;
267
+ this.#tempBox.Size.y = scaledEntryHeight;
268
+ CopyVector2(this.#tempBox.Position, position)
269
+
270
+
271
+
272
+ ctx.textAlign = TextAlign.Left;
273
+ ctx.fillStyle = Theme.ContextMenu.BackgroundColor;
274
+ ctx.shadowColor = "#000000";
275
+ ctx.shadowBlur = 5 * menuScale;
276
+ ctx.beginPath();
277
+ ctx.roundRect(
278
+ position.x,
279
+ this.#tempBox.Position.y,
280
+ scaledEntryWidth,
281
+ totalScaledHeight,
282
+ 5 * menuScale
283
+ );
284
+ ctx.fill();
285
+ ctx.shadowBlur = 0;
286
+
287
+ let mouseIsOver: ContextEntry | null = null;
288
+ let subOpenedThisFrame = false;
289
+
290
+ let optionsRendered = 0;
291
+ for (let groupIndex = 0; groupIndex < this.#groups.Count(); groupIndex++) {
292
+ const group = this.#groups.At(groupIndex);
293
+ for (let entryIndex = 0; entryIndex < group.entries.length; entryIndex++) {
294
+ const entry = group.entries[entryIndex];
295
+
296
+ this.#tempBox.Position.y = position.y + (scaledEntryHeight * optionsRendered);
297
+
298
+ let entryMousedOver = false;
299
+ if (mousePosition !== undefined && InBox(this.#tempBox, mousePosition)) {
300
+ mouseIsOver = entry;
301
+ entryMousedOver = true
302
+ if (entry.subMenu !== undefined) {
303
+ this.#openSubMenu = entry.subMenu;
304
+ this.#submenuPosition.x = position.x
305
+ this.#submenuPosition.y = this.#tempBox.Position.y;
306
+ if (submenuOpenRight) {
307
+ this.#submenuPosition.x += scaledEntryWidth
308
+ }
309
+ subOpenedThisFrame = true;
310
+ } else {
311
+ this.#openSubMenu = undefined;
312
+ }
313
+ }
314
+
315
+ if (entryMousedOver || (this.#openSubMenu !== undefined && entry.subMenu === this.#openSubMenu)) {
316
+ ctx.fillStyle = Theme.ContextMenu.HighlightColor;
317
+ ctx.beginPath();
318
+ ctx.roundRect(
319
+ position.x + (scaledEntryHeight / 10),
320
+ this.#tempBox.Position.y + (scaledEntryHeight / 10),
321
+ scaledEntryWidth - (scaledEntryHeight / 5),
322
+ scaledEntryHeight - (scaledEntryHeight / 5),
323
+ 5 * menuScale
324
+ );
325
+ ctx.fill();
326
+ }
327
+
328
+ this.#textStyle.setupStyle(ctx, menuScale);
329
+ ctx.fillText(entry.text, position.x + (scaledEntryHeight / 5), this.#tempBox.Position.y + (scaledEntryHeight / 2))
330
+
331
+ // Render arrows
332
+ if (entry.subMenu !== undefined) {
333
+ ctx.beginPath()
334
+ ctx.strokeStyle = Theme.ContextMenu.FontColor;
335
+ ctx.lineWidth = 1 * menuScale;
336
+ ctx.lineTo(position.x + scaledEntryWidth - (scaledEntryHeight / 2.5), this.#tempBox.Position.y + (scaledEntryHeight / 3))
337
+ ctx.lineTo(position.x + scaledEntryWidth - (scaledEntryHeight / 4), this.#tempBox.Position.y + (scaledEntryHeight / 2))
338
+ ctx.lineTo(position.x + scaledEntryWidth - (scaledEntryHeight / 2.5), this.#tempBox.Position.y + scaledEntryHeight - (scaledEntryHeight / 3))
339
+ ctx.stroke();
340
+ }
341
+
342
+ optionsRendered++;
343
+ }
344
+
345
+ // Draw a line seperating the groups
346
+ if (groupIndex !== this.#groups.Count() - 1) {
347
+ ctx.strokeStyle = Theme.ContextMenu.FontColor;
348
+ ctx.lineWidth = .5 * menuScale;
349
+ ctx.beginPath();
350
+ const startX = position.x + (scaledEntryHeight / 10);
351
+ const y = this.#tempBox.Position.y + scaledEntryHeight
352
+ ctx.lineTo(startX, y);
353
+ ctx.lineTo(startX + scaledEntryWidth - (scaledEntryHeight / 5), y);
354
+ ctx.stroke();
355
+ }
356
+ }
357
+
358
+ if (this.#openSubMenu !== undefined) {
359
+ const mouseOverSub = this.#openSubMenu.render(ctx, this.#submenuPosition, menuScale, mousePosition, submenuOpenRight)
360
+ if (mouseOverSub !== null) {
361
+ mouseIsOver = mouseOverSub;
362
+ }
363
+ }
364
+
365
+ return mouseIsOver;
366
+ }
367
+ }
@@ -0,0 +1,13 @@
1
+ import { Box } from "./types/box";
2
+ import { Vector2 } from "./types/vector2";
3
+
4
+ export interface Draggable {
5
+ Components(): Array<DraggableComponent>;
6
+ }
7
+
8
+ export interface DraggableComponent {
9
+ Bounds(): Box;
10
+ ClickStart(mousePosition: Vector2): void;
11
+ ClickEnd(): void;
12
+ MouseMove(mousePosition: Vector2): void;
13
+ }