@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.
- package/.github/workflows/deploy.yml +57 -0
- package/README.md +159 -0
- package/dev.tsconfig.json +18 -0
- package/docs/promotional-image.png +0 -0
- package/esbuild.ts +32 -0
- package/index.html +311 -0
- package/package.json +50 -0
- package/src/camera.ts +30 -0
- package/src/connection.ts +249 -0
- package/src/contextMenu.ts +367 -0
- package/src/draggable.ts +13 -0
- package/src/elements/base.ts +192 -0
- package/src/elements/container.ts +119 -0
- package/src/elements/interfaces.ts +50 -0
- package/src/elements/text.ts +86 -0
- package/src/graph.ts +535 -0
- package/src/graphSubsystem.ts +31 -0
- package/src/index.ts +28 -0
- package/src/input.ts +153 -0
- package/src/markdown/entry.ts +323 -0
- package/src/markdown/lexicalParser.ts +148 -0
- package/src/markdown/markdown.ts +14 -0
- package/src/markdown/syntaxParser.ts +344 -0
- package/src/markdown/token.ts +43 -0
- package/src/metadata.ts +3 -0
- package/src/node.ts +1130 -0
- package/src/nodes/factory.ts +87 -0
- package/src/nodes/publisher.ts +98 -0
- package/src/nodes/subsystem.ts +760 -0
- package/src/notes/note.ts +295 -0
- package/src/notes/subsystem.ts +262 -0
- package/src/organize.ts +181 -0
- package/src/pass/subsystem.ts +64 -0
- package/src/performance.ts +9 -0
- package/src/popup.ts +81 -0
- package/src/popups/form.ts +63 -0
- package/src/popups/string.ts +33 -0
- package/src/port.ts +223 -0
- package/src/quickMenu.ts +330 -0
- package/src/styles/box.ts +104 -0
- package/src/styles/canvasTextAlign.ts +8 -0
- package/src/styles/canvasTextBaseline.ts +9 -0
- package/src/styles/cursor.ts +18 -0
- package/src/styles/stroke.ts +39 -0
- package/src/styles/text.ts +90 -0
- package/src/styles/textBox.ts +82 -0
- package/src/theme.ts +78 -0
- package/src/types/box.ts +126 -0
- package/src/types/list.ts +41 -0
- package/src/types/pool.ts +66 -0
- package/src/types/text.ts +136 -0
- package/src/types/vector2.ts +34 -0
- package/src/utils/color.ts +136 -0
- package/src/utils/html.ts +21 -0
- package/src/utils/math.ts +8 -0
- package/src/utils/string.ts +98 -0
- package/src/widgets/button.ts +107 -0
- package/src/widgets/color.ts +151 -0
- package/src/widgets/factory.ts +46 -0
- package/src/widgets/image.ts +163 -0
- package/src/widgets/number.ts +153 -0
- package/src/widgets/slider.ts +216 -0
- package/src/widgets/string.ts +128 -0
- package/src/widgets/text.ts +92 -0
- package/src/widgets/toggle.ts +205 -0
- package/src/widgets/widget.ts +12 -0
- package/tsconfig.json +15 -0
- 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
|
+
}
|
package/src/draggable.ts
ADDED
|
@@ -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
|
+
}
|