@kabel-project/kabel 1.0.7
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/(1.0.7)kabel.md +18 -0
- package/README.md +96 -0
- package/_READ_ME_MEDIA_/documentation/docs.md +293 -0
- package/_READ_ME_MEDIA_/workspace.png +0 -0
- package/comment-renderer/renderer.ts +228 -0
- package/controllers/base.ts +186 -0
- package/controllers/wasd.ts +132 -0
- package/docs/README.md +98 -0
- package/docs/_media/docs.md +289 -0
- package/docs/_media/index.html +168 -0
- package/docs/_media/workspace.png +0 -0
- package/docs/classes/CommentModel.md +271 -0
- package/docs/classes/CommentRenderer.md +457 -0
- package/docs/classes/ConnectableField.md +597 -0
- package/docs/classes/Connection.md +191 -0
- package/docs/classes/ContextMenuHTML.md +163 -0
- package/docs/classes/Coordinates.md +187 -0
- package/docs/classes/DropdownContainer.md +300 -0
- package/docs/classes/DummyField.md +393 -0
- package/docs/classes/Eventer.md +185 -0
- package/docs/classes/Field.md +461 -0
- package/docs/classes/InjectMsg.md +85 -0
- package/docs/classes/NodeSvg.md +1011 -0
- package/docs/classes/NumberField.md +559 -0
- package/docs/classes/OptConnectField.md +624 -0
- package/docs/classes/Renderer.md +1636 -0
- package/docs/classes/RendererConstants.md +343 -0
- package/docs/classes/Representer.md +95 -0
- package/docs/classes/RepresenterNode.md +175 -0
- package/docs/classes/TextField.md +559 -0
- package/docs/classes/Toolbox.md +172 -0
- package/docs/classes/WASDController.md +616 -0
- package/docs/classes/Widget.md +195 -0
- package/docs/classes/WorkspaceController.md +385 -0
- package/docs/classes/WorkspaceCoords.md +218 -0
- package/docs/classes/WorkspaceSvg.md +1380 -0
- package/docs/functions/clearMainWorkspace.md +20 -0
- package/docs/functions/getMainWorkspace.md +19 -0
- package/docs/functions/inject.md +35 -0
- package/docs/functions/setMainWorkspace.md +28 -0
- package/docs/globals.md +95 -0
- package/docs/interfaces/ColorStyle.md +43 -0
- package/docs/interfaces/ConnectorToFrom.md +57 -0
- package/docs/interfaces/DrawState.md +81 -0
- package/docs/interfaces/FieldConnectionData.md +25 -0
- package/docs/interfaces/FieldOptions.md +63 -0
- package/docs/interfaces/FieldRawBoxData.md +25 -0
- package/docs/interfaces/FieldVisualInfo.md +65 -0
- package/docs/interfaces/GridOptions.md +61 -0
- package/docs/interfaces/InjectOptions.md +133 -0
- package/docs/interfaces/InputFieldJson.md +50 -0
- package/docs/interfaces/KabelCommentRendering.md +31 -0
- package/docs/interfaces/KabelInterface.md +469 -0
- package/docs/interfaces/KabelNodeRendering.md +77 -0
- package/docs/interfaces/KabelUIX.md +105 -0
- package/docs/interfaces/KabelUtils.md +215 -0
- package/docs/interfaces/NodeEvents.md +42 -0
- package/docs/interfaces/NodeJson.md +104 -0
- package/docs/interfaces/NodePrototype.md +82 -0
- package/docs/interfaces/RegisteredEl.md +53 -0
- package/docs/interfaces/SerializedNode.md +128 -0
- package/docs/interfaces/TblxCategoryStruct.md +41 -0
- package/docs/interfaces/TblxFieldStruct.md +28 -0
- package/docs/interfaces/TblxNodeStruct.md +35 -0
- package/docs/interfaces/WidgetOptions.md +115 -0
- package/docs/interfaces/WidgetPrototypeList.md +15 -0
- package/docs/type-aliases/AnyField.md +13 -0
- package/docs/type-aliases/AnyFieldCls.md +13 -0
- package/docs/type-aliases/Color.md +13 -0
- package/docs/type-aliases/Connectable.md +13 -0
- package/docs/type-aliases/EventArgs.md +11 -0
- package/docs/type-aliases/EventSetupFn.md +25 -0
- package/docs/type-aliases/Hex.md +13 -0
- package/docs/type-aliases/RGBObject.md +37 -0
- package/docs/type-aliases/RGBString.md +13 -0
- package/docs/type-aliases/RGBTuple.md +13 -0
- package/docs/type-aliases/TblxObjStruct.md +52 -0
- package/docs/variables/CategoryColors.md +29 -0
- package/docs/variables/FieldMap.md +41 -0
- package/docs/variables/NodePrototypes.md +18 -0
- package/docs/variables/default.md +11 -0
- package/events/comment-drag-handle.ts +61 -0
- package/events/comment-input.ts +291 -0
- package/events/connection-line.ts +68 -0
- package/events/connector.ts +116 -0
- package/events/draggable.ts +119 -0
- package/events/events.ts +7 -0
- package/events/input-box.ts +213 -0
- package/events/node-x-btn.ts +25 -0
- package/index.d.ts +4 -0
- package/package.json +49 -0
- package/renderers/apollo/apollo.ts +21 -0
- package/renderers/apollo/constants.ts +40 -0
- package/renderers/apollo/renderer.ts +331 -0
- package/renderers/atlas/atlas.ts +15 -0
- package/renderers/constants.ts +87 -0
- package/renderers/renderer.ts +1288 -0
- package/renderers/representer-node.ts +52 -0
- package/renderers/representer.ts +25 -0
- package/src/category.ts +107 -0
- package/src/colors.ts +20 -0
- package/src/comment.ts +142 -0
- package/src/connection.ts +114 -0
- package/src/context-menu.ts +194 -0
- package/src/coordinates.ts +74 -0
- package/src/core.ts +202 -0
- package/src/ctx-menu-registry.ts +143 -0
- package/src/dropdown-menu.ts +215 -0
- package/src/field.ts +595 -0
- package/src/flyout.ts +165 -0
- package/src/fonts-manager.ts +38 -0
- package/src/grid.ts +162 -0
- package/src/headless-node.ts +27 -0
- package/src/index.ts +115 -0
- package/src/inject-headless.ts +18 -0
- package/src/inject.ts +213 -0
- package/src/main-workspace.ts +51 -0
- package/src/mutator.ts +40 -0
- package/src/node-types.ts +27 -0
- package/src/nodesvg.ts +756 -0
- package/src/prototypes.ts +9 -0
- package/src/renderer-map.ts +86 -0
- package/src/styles.css +224 -0
- package/src/toolbox.ts +125 -0
- package/src/types.ts +205 -0
- package/src/undo-redo.ts +87 -0
- package/src/visual-types.ts +29 -0
- package/src/widget-prototypes.ts +11 -0
- package/src/widget.ts +139 -0
- package/src/workspace-coords.ts +14 -0
- package/src/workspace-svg.ts +736 -0
- package/src/workspace.ts +155 -0
- package/test-server.js +61 -0
- package/themes/dark.ts +32 -0
- package/themes/default.ts +28 -0
- package/themes/themes.ts +9 -0
- package/tsconfig.json +25 -0
- package/typedoc.json +10 -0
- package/util/emitter.ts +33 -0
- package/util/env.ts +11 -0
- package/util/escape-html.ts +22 -0
- package/util/eventer.ts +108 -0
- package/util/has-prop.ts +4 -0
- package/util/parse-color.ts +42 -0
- package/util/path.ts +99 -0
- package/util/styler.ts +41 -0
- package/util/uid.ts +184 -0
- package/util/unescape-html.ts +22 -0
- package/util/user-state.ts +68 -0
- package/util/wait-anim-frames.ts +24 -0
- package/util/window-listeners.ts +62 -0
- package/webpack.config.js +80 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { Element, Rect, Text } from "@svgdotjs/svg.js";
|
|
2
|
+
import eventer, { EventSetupFn } from "../util/eventer";
|
|
3
|
+
import userState from '../util/user-state';
|
|
4
|
+
import { Renderer } from "../src";
|
|
5
|
+
|
|
6
|
+
function initInputBox(element: Element, args: Record<string, any>) {
|
|
7
|
+
let editing = false;
|
|
8
|
+
let skipNextClick = false;
|
|
9
|
+
let buffer = args.field.getDisplayValue?.() ?? "";
|
|
10
|
+
let cursorPos = buffer.length;
|
|
11
|
+
let anchorPos = buffer.length;
|
|
12
|
+
const txt: Text = args.text;
|
|
13
|
+
const rect: Rect = element as Rect;
|
|
14
|
+
const renderer = args.renderer as Renderer;
|
|
15
|
+
const PADDING_X = 4;
|
|
16
|
+
const PADDING_Y = 4;
|
|
17
|
+
let caretLine: Rect | null = null;
|
|
18
|
+
let selectionRect: Rect | null = null;
|
|
19
|
+
// @ts-ignore
|
|
20
|
+
txt!.style('user-select', 'none');
|
|
21
|
+
// @ts-ignore
|
|
22
|
+
rect!.style('user-select', 'none');
|
|
23
|
+
|
|
24
|
+
// Helper: measure width of text using renderer or fallback
|
|
25
|
+
function measureTextWidth(text: string) {
|
|
26
|
+
|
|
27
|
+
if (renderer.measureTextWidth) return renderer.measureTextWidth(text);
|
|
28
|
+
return text.length * 8; // rough fallback
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getSelectionRange() {
|
|
32
|
+
const start = Math.min(cursorPos, anchorPos);
|
|
33
|
+
const end = Math.max(cursorPos, anchorPos);
|
|
34
|
+
return { start, end };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function hasSelection() {
|
|
38
|
+
return cursorPos !== anchorPos;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function deleteSelection() {
|
|
42
|
+
if (!hasSelection()) return false;
|
|
43
|
+
const { start, end } = getSelectionRange();
|
|
44
|
+
buffer = buffer.slice(0, start) + buffer.slice(end);
|
|
45
|
+
cursorPos = start;
|
|
46
|
+
anchorPos = start;
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function updateText() {
|
|
51
|
+
// redraw main text
|
|
52
|
+
txt.text(buffer);
|
|
53
|
+
|
|
54
|
+
const { start, end } = getSelectionRange();
|
|
55
|
+
const { width: totalWidth } = renderer.measureRawField(buffer);
|
|
56
|
+
const { height: rectHeight } = rect.bbox(); // actual rect height
|
|
57
|
+
|
|
58
|
+
const textBBox = txt.bbox();
|
|
59
|
+
const offsetY = (rectHeight - textBBox.height) / 2; // center vertically
|
|
60
|
+
|
|
61
|
+
// --- selection highlight ---
|
|
62
|
+
if (hasSelection()) {
|
|
63
|
+
const textBeforeStart = buffer.slice(0, start);
|
|
64
|
+
const textBeforeEnd = buffer.slice(0, end);
|
|
65
|
+
const highlightX = args.startX + PADDING_X + measureTextWidth(textBeforeStart);
|
|
66
|
+
const highlightWidth = Math.max(measureTextWidth(textBeforeEnd) - measureTextWidth(textBeforeStart), 1);
|
|
67
|
+
const highlightY = offsetY; // use offsetY for vertical alignment
|
|
68
|
+
|
|
69
|
+
if (!selectionRect) {
|
|
70
|
+
// @ts-ignore
|
|
71
|
+
selectionRect = rect.parent()!.rect(highlightWidth, textBBox.height)
|
|
72
|
+
.fill('#3390ff')
|
|
73
|
+
.attr({ 'fill-opacity': 0.35 });
|
|
74
|
+
selectionRect!.node.parentNode!.insertBefore(selectionRect!.node, txt.node);
|
|
75
|
+
} else {
|
|
76
|
+
selectionRect.size(highlightWidth, textBBox.height);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
selectionRect!.move(highlightX, highlightY);
|
|
80
|
+
} else {
|
|
81
|
+
if (selectionRect) { selectionRect.remove(); selectionRect = null; }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// --- caret ---
|
|
85
|
+
if (editing) {
|
|
86
|
+
const zoom = renderer.getWs().getZoom(); // workspace zoom
|
|
87
|
+
const caretX = (args.startX + PADDING_X + measureTextWidth(buffer.slice(0, cursorPos))); // scale pos
|
|
88
|
+
const caretH = textBBox.height;
|
|
89
|
+
// @ts-ignore
|
|
90
|
+
if (!caretLine) caretLine = rect.parent()!.rect(1, caretH).fill(renderer.constants.FIELD_RAW_TEXT_COLOR);
|
|
91
|
+
caretLine!.size(1, caretH).move(caretX, offsetY);
|
|
92
|
+
} else {
|
|
93
|
+
if (caretLine) { caretLine.remove(); caretLine = null; }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// --- background rect ---
|
|
97
|
+
rect.size(Math.max(16, totalWidth + PADDING_X * 2), Math.max(rectHeight, textBBox.height + PADDING_Y * 2));
|
|
98
|
+
txt.move(args.startX + PADDING_X, offsetY);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
function onKeyDown(e: KeyboardEvent) {
|
|
103
|
+
if (!args.field.canEdit()) { // If editing isnt allowed, close the event early.
|
|
104
|
+
if (editing) stopEditing();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (!editing) return;
|
|
108
|
+
|
|
109
|
+
if (e.key === "Escape") { e.preventDefault(); stopEditing(); return; }
|
|
110
|
+
if (e.key === "Enter") { e.preventDefault(); stopEditing(); return; }
|
|
111
|
+
|
|
112
|
+
if (e.key === "Backspace") {
|
|
113
|
+
if (!deleteSelection() && cursorPos > 0) {
|
|
114
|
+
buffer = buffer.slice(0, cursorPos - 1) + buffer.slice(cursorPos);
|
|
115
|
+
cursorPos--;
|
|
116
|
+
anchorPos = cursorPos;
|
|
117
|
+
}
|
|
118
|
+
} else if (e.key === "Delete") {
|
|
119
|
+
if (!deleteSelection() && cursorPos < buffer.length) {
|
|
120
|
+
buffer = buffer.slice(0, cursorPos) + buffer.slice(cursorPos + 1);
|
|
121
|
+
}
|
|
122
|
+
} else if (e.key === "ArrowLeft") {
|
|
123
|
+
if (e.shiftKey) cursorPos = Math.max(0, cursorPos - 1);
|
|
124
|
+
else { cursorPos = Math.max(0, cursorPos - 1); anchorPos = cursorPos; }
|
|
125
|
+
} else if (e.key === "ArrowRight") {
|
|
126
|
+
if (e.shiftKey) cursorPos = Math.min(buffer.length, cursorPos + 1);
|
|
127
|
+
else { cursorPos = Math.min(buffer.length, cursorPos + 1); anchorPos = cursorPos; }
|
|
128
|
+
} else if (e.key === "Home") {
|
|
129
|
+
if (!e.shiftKey) anchorPos = 0;
|
|
130
|
+
cursorPos = 0;
|
|
131
|
+
} else if (e.key === "End") {
|
|
132
|
+
if (!e.shiftKey) anchorPos = buffer.length;
|
|
133
|
+
cursorPos = buffer.length;
|
|
134
|
+
} else if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
|
|
135
|
+
if (hasSelection()) deleteSelection();
|
|
136
|
+
buffer = buffer.slice(0, cursorPos) + e.key + buffer.slice(cursorPos);
|
|
137
|
+
cursorPos++;
|
|
138
|
+
anchorPos = cursorPos;
|
|
139
|
+
} else return;
|
|
140
|
+
|
|
141
|
+
e.preventDefault();
|
|
142
|
+
updateText();
|
|
143
|
+
args.field.setValue(buffer);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function onClickOutside(ev: MouseEvent) {
|
|
147
|
+
if (!editing) return;
|
|
148
|
+
if (skipNextClick) { skipNextClick = false; return; }
|
|
149
|
+
const target = ev.target as Node;
|
|
150
|
+
if (target !== rect.node && target !== txt.node) stopEditing();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function startEditing(ev?: MouseEvent) {
|
|
154
|
+
if (!args.field.canEdit()) { // If editing isnt allowed, close the event early.
|
|
155
|
+
if (editing) stopEditing();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (editing) return;
|
|
159
|
+
editing = true;
|
|
160
|
+
buffer = args.field.getValue?.() ?? "";
|
|
161
|
+
cursorPos = buffer.length;
|
|
162
|
+
anchorPos = buffer.length;
|
|
163
|
+
|
|
164
|
+
userState.setState('typing');
|
|
165
|
+
|
|
166
|
+
if (ev) {
|
|
167
|
+
const rectBox = rect.node.getBoundingClientRect();
|
|
168
|
+
const zoom = renderer.getWs().getZoom(); // workspace zoom
|
|
169
|
+
const clickX = (ev.clientX - rectBox.left - PADDING_X) / zoom;
|
|
170
|
+
|
|
171
|
+
let cumulativeWidth = 0;
|
|
172
|
+
cursorPos = 0;
|
|
173
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
174
|
+
const charWidth = measureTextWidth(buffer[i]); // scale widths
|
|
175
|
+
if (cumulativeWidth + charWidth / 2 >= clickX) break;
|
|
176
|
+
cumulativeWidth += charWidth;
|
|
177
|
+
cursorPos = i + 1;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
anchorPos = cursorPos;
|
|
182
|
+
updateText();
|
|
183
|
+
skipNextClick = true;
|
|
184
|
+
|
|
185
|
+
document.addEventListener("keydown", onKeyDown);
|
|
186
|
+
document.addEventListener("mousedown", onClickOutside);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function stopEditing() {
|
|
190
|
+
editing = false;
|
|
191
|
+
userState.removeState('typing');
|
|
192
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
193
|
+
document.removeEventListener("mousedown", onClickOutside);
|
|
194
|
+
args.field.setValue(buffer);
|
|
195
|
+
updateText();
|
|
196
|
+
renderer.getWs().redraw();
|
|
197
|
+
renderer.getWs().emitChange();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
rect.on("mousedown", (ev: Event) => startEditing(ev as MouseEvent));
|
|
201
|
+
txt.on("mousedown", (ev: Event) => startEditing(ev as MouseEvent));
|
|
202
|
+
|
|
203
|
+
updateText();
|
|
204
|
+
|
|
205
|
+
return () => {
|
|
206
|
+
rect.off("mousedown", startEditing as EventListener);
|
|
207
|
+
txt.off("mousedown", startEditing as EventListener);
|
|
208
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
209
|
+
document.removeEventListener("mousedown", onClickOutside);
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
eventer.registerEvent("k_inputbox", initInputBox as EventSetupFn);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Element, G } from '@svgdotjs/svg.js';
|
|
2
|
+
import NodeSvg from '../src/nodesvg';
|
|
3
|
+
import eventer, { EventSetupFn } from '../util/eventer';
|
|
4
|
+
import WorkspaceSvg from '../src/workspace-svg';
|
|
5
|
+
|
|
6
|
+
function initXButton(element: Element, args: Record<string, any>): () => void {
|
|
7
|
+
const xBtnGroup = element as G;
|
|
8
|
+
const ws: WorkspaceSvg = args.workspace;
|
|
9
|
+
|
|
10
|
+
// click handler
|
|
11
|
+
const onClick = () => {
|
|
12
|
+
ws.removeNode(args.node);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// attach
|
|
16
|
+
xBtnGroup.on('click', onClick);
|
|
17
|
+
|
|
18
|
+
// return cleanup function
|
|
19
|
+
return () => {
|
|
20
|
+
xBtnGroup.off('click', onClick);
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// register as Kabel event
|
|
25
|
+
eventer.registerEvent('k_closenode', initXButton as EventSetupFn);
|
package/index.d.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kabel-project/kabel",
|
|
3
|
+
"version": "1.0.7",
|
|
4
|
+
"main": "index.js",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "node test-server.js",
|
|
7
|
+
"build": "npx webpack",
|
|
8
|
+
"docs": "typedoc --out docs src"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"editor",
|
|
12
|
+
"code",
|
|
13
|
+
"script",
|
|
14
|
+
"visual",
|
|
15
|
+
"programming",
|
|
16
|
+
"node",
|
|
17
|
+
"graph"
|
|
18
|
+
],
|
|
19
|
+
"repository": "https://github.com/FentFentFent/Kabel",
|
|
20
|
+
"author": "fentiefentfent",
|
|
21
|
+
"license": "ISC",
|
|
22
|
+
"description": "A blockly-inspired visual programming interface.",
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^24.3.1",
|
|
25
|
+
"chokidar": "^5.0.0",
|
|
26
|
+
"connect-livereload": "^0.6.1",
|
|
27
|
+
"css-minimizer-webpack-plugin": "^7.0.2",
|
|
28
|
+
"express": "^5.2.1",
|
|
29
|
+
"jsdoc": "^4.0.5",
|
|
30
|
+
"livereload": "^0.10.3",
|
|
31
|
+
"raw-loader": "^4.0.2",
|
|
32
|
+
"source-map-loader": "^5.0.0",
|
|
33
|
+
"terser-webpack-plugin": "^5.3.14",
|
|
34
|
+
"ts-loader": "^9.5.2",
|
|
35
|
+
"typedoc": "^0.28.14",
|
|
36
|
+
"typescript": "^5.9.2",
|
|
37
|
+
"webpack": "^5.101.2",
|
|
38
|
+
"webpack-cli": "^6.0.1"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@fontsource/fredoka": "^5.2.10",
|
|
42
|
+
"@svgdotjs/svg.js": "^3.2.4",
|
|
43
|
+
"svgpath": "^2.6.0",
|
|
44
|
+
"typedoc-plugin-markdown": "^4.9.0"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
|
|
2
|
+
import ApolloRenderer from "./renderer"
|
|
3
|
+
import ApolloConstants from "./constants"
|
|
4
|
+
import Representer from "../representer"
|
|
5
|
+
import { RepresenterNode } from "../representer-node"
|
|
6
|
+
|
|
7
|
+
export interface apolloType {
|
|
8
|
+
Renderer: typeof ApolloRenderer;
|
|
9
|
+
Constants: typeof ApolloConstants;
|
|
10
|
+
Representer: typeof Representer;
|
|
11
|
+
RepresenterNode: typeof RepresenterNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
ApolloRenderer as Renderer,
|
|
17
|
+
ApolloConstants as Constants,
|
|
18
|
+
// Despite apollo not extending these, we still need to re-export them for the API.
|
|
19
|
+
Representer,
|
|
20
|
+
RepresenterNode
|
|
21
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Color } from "../../src/visual-types";
|
|
2
|
+
import RendererConstants from "../constants";
|
|
3
|
+
|
|
4
|
+
// Define the new Dogear shape
|
|
5
|
+
type DogearShape = {
|
|
6
|
+
PathMain: string;
|
|
7
|
+
Width: number;
|
|
8
|
+
Height: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Extend the original SHAPES type
|
|
12
|
+
type ApolloShapes = RendererConstants['SHAPES'] & {
|
|
13
|
+
Dogear?: DogearShape;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
class ApolloConstants extends RendererConstants {
|
|
17
|
+
SHAPES: ApolloShapes = {};
|
|
18
|
+
CONNECTOR_COLOR: Color = '#9912e8ff'
|
|
19
|
+
FIELD_RAW_COLOR: Color = '#343745ff';
|
|
20
|
+
FIELD_RAW_TEXT_COLOR: Color = '#cfd4e4ff';
|
|
21
|
+
FIELD_RAW_OUTLINE_COLOR: Color = '#202128ff';
|
|
22
|
+
constructor(overrides: Partial<ApolloConstants>) {
|
|
23
|
+
super(overrides);
|
|
24
|
+
this.CONNECTOR_LINE_WIDTH = 2.5;
|
|
25
|
+
this.CORNER_RADIUS = 10;
|
|
26
|
+
this.FOOTER_HEIGHT = 25;
|
|
27
|
+
this.CONNECTOR_LINE_WIDTH = 7;
|
|
28
|
+
this.init();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
init() {
|
|
32
|
+
this.SHAPES.Dogear = {
|
|
33
|
+
PathMain: `M -1 1 Q -1 21 0 30 L 26 0 Q 2 -1 0 0 Z`,
|
|
34
|
+
Width: 26,
|
|
35
|
+
Height: 30
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default ApolloConstants;
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
|
|
2
|
+
import { Path as SvgPath, StrokeData, G } from "@svgdotjs/svg.js";
|
|
3
|
+
import * as Path from '../../util/path'
|
|
4
|
+
import { ColorStyle, Hex } from "../../src/visual-types";
|
|
5
|
+
import WorkspaceSvg from "../../src/workspace-svg";
|
|
6
|
+
import Renderer, { DrawState, NodeMeasurements } from "../renderer"; // Saying the value of "Renderer" is undefined when it's clearly defined and theres no circular refs.
|
|
7
|
+
import ApolloConstants from "./constants";
|
|
8
|
+
import { parseColor } from "../../util/parse-color";
|
|
9
|
+
import eventer from "../../util/eventer";
|
|
10
|
+
import NodeSvg from "../../src/nodesvg";
|
|
11
|
+
import Connection from "../../src/connection";
|
|
12
|
+
import { AnyField, FieldRawBoxData } from "../../src/field";
|
|
13
|
+
console.log(Renderer);
|
|
14
|
+
|
|
15
|
+
function darkenColor(hex: string, amount: number = 0.2): string {
|
|
16
|
+
// Remove # if present
|
|
17
|
+
hex = hex.replace(/^#/, "");
|
|
18
|
+
|
|
19
|
+
// Parse r, g, b
|
|
20
|
+
const r = parseInt(hex.substring(0, 2), 16);
|
|
21
|
+
const g = parseInt(hex.substring(2, 4), 16);
|
|
22
|
+
const b = parseInt(hex.substring(4, 6), 16);
|
|
23
|
+
|
|
24
|
+
// Darken each channel
|
|
25
|
+
const newR = Math.max(0, Math.floor(r * (1 - amount)));
|
|
26
|
+
const newG = Math.max(0, Math.floor(g * (1 - amount)));
|
|
27
|
+
const newB = Math.max(0, Math.floor(b * (1 - amount)));
|
|
28
|
+
|
|
29
|
+
// Convert back to hex and pad with 0s
|
|
30
|
+
const toHex = (c: number) => c.toString(16).padStart(2, "0");
|
|
31
|
+
|
|
32
|
+
return `#${toHex(newR)}${toHex(newG)}${toHex(newB)}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* ApolloRenderer
|
|
40
|
+
*
|
|
41
|
+
* Custom renderer extending the base `Renderer` for a visual programming workspace.
|
|
42
|
+
* This renderer provides a distinct node style:
|
|
43
|
+
* - Node background uses the primary color entirely
|
|
44
|
+
* - Topbar is hidden (but still present for dragging)
|
|
45
|
+
* - Bottom-right dog-ear decoration
|
|
46
|
+
* - Previous connection at the top, next connection at the bottom
|
|
47
|
+
*/
|
|
48
|
+
class ApolloRenderer extends Renderer {
|
|
49
|
+
declare _constants: ApolloConstants;
|
|
50
|
+
|
|
51
|
+
static get NAME() {
|
|
52
|
+
return 'apollo';
|
|
53
|
+
}
|
|
54
|
+
static get ElEMENT_TAG() {
|
|
55
|
+
return 'ApolloElement';
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Constructor
|
|
59
|
+
* @param workspace - The WorkspaceSvg instance
|
|
60
|
+
* @param overrides - Partial constant overrides for ApolloConstants
|
|
61
|
+
*/
|
|
62
|
+
constructor(workspace: WorkspaceSvg, overrides: Partial<ApolloConstants>) {
|
|
63
|
+
super(workspace, overrides);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Typed getter for renderer constants
|
|
68
|
+
*/
|
|
69
|
+
get constants(): ApolloConstants {
|
|
70
|
+
return super.constants as ApolloConstants;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Initialize constants using Apollo-specific overrides */
|
|
74
|
+
initConstants() {
|
|
75
|
+
this._constants = new ApolloConstants(this.constantOverrides);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Draws the node's topbar.
|
|
80
|
+
* Overridden to hide the topbar visually while retaining its group for dragging.
|
|
81
|
+
* @param state - Current draw state
|
|
82
|
+
* @param colors - Node colors
|
|
83
|
+
* @param measurements - Node dimensions
|
|
84
|
+
*/
|
|
85
|
+
drawNodeTopbar(state: DrawState, colors: ColorStyle, measurements: NodeMeasurements | null) {
|
|
86
|
+
const c = this.constants;
|
|
87
|
+
const width = measurements?.width ?? c.NODE_BASE_WIDTH;
|
|
88
|
+
const radius = c.CORNER_RADIUS;
|
|
89
|
+
|
|
90
|
+
state.topbar = state.group!.path(Path.roundedRect(width, c.TOPBAR_HEIGHT, radius))
|
|
91
|
+
.fill('transparent');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Draws the node background with rounded corners and bottom-right dog-ear cut.
|
|
96
|
+
* Also creates a shadow.
|
|
97
|
+
* @param state - Current draw state
|
|
98
|
+
* @param measurements - Node dimensions
|
|
99
|
+
*/
|
|
100
|
+
drawNodeBase(state: DrawState, measurements: NodeMeasurements | null) {
|
|
101
|
+
const c = this.constants;
|
|
102
|
+
const width = measurements?.width ?? c.NODE_BASE_WIDTH;
|
|
103
|
+
const height = measurements?.height ?? c.NODE_BASE_HEIGHT;
|
|
104
|
+
const r = c.CORNER_RADIUS;
|
|
105
|
+
|
|
106
|
+
const dogear = this.constants.SHAPES.Dogear;
|
|
107
|
+
const cutW = dogear?.Width ?? 0;
|
|
108
|
+
const cutH = dogear?.Height ?? 0;
|
|
109
|
+
|
|
110
|
+
// Rounded rect with bottom-right cut
|
|
111
|
+
const roundedRect = `M${r},0 H${width - r} Q${width},0 ${width},${r} V${height - cutH} L${width - cutW},${height} H${r} Q0,${height} 0,${height - r} V${r} Q0,0 ${r},0 Z`;
|
|
112
|
+
|
|
113
|
+
const colors = this.getNodeColors();
|
|
114
|
+
const primary = parseColor(colors.primary) as string;
|
|
115
|
+
const stroke = parseColor(colors.secondary) as string;
|
|
116
|
+
|
|
117
|
+
// Node background
|
|
118
|
+
state.bg = state.group!.path(roundedRect)
|
|
119
|
+
.fill(primary)
|
|
120
|
+
.stroke({ color: stroke, width: 2 });
|
|
121
|
+
|
|
122
|
+
// Dog-ear decoration
|
|
123
|
+
this.drawDogEar(state, state.group as G, width, height, colors);
|
|
124
|
+
|
|
125
|
+
// Node shadow
|
|
126
|
+
state.shadow = state.group!.path(roundedRect)
|
|
127
|
+
.fill('rgba(0,0,0,0.2)')
|
|
128
|
+
.stroke('none')
|
|
129
|
+
.attr({ 'pointer-events': 'none' })
|
|
130
|
+
.move(Number(state.bg.x()) + 5, Number(state.bg.y()) + 5)
|
|
131
|
+
.back();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Draws a dog-ear decoration at the node's bottom-right corner
|
|
136
|
+
* @param state - Current draw state
|
|
137
|
+
* @param nodeG - Node's SVG group
|
|
138
|
+
* @param w - Node width
|
|
139
|
+
* @param h - Node height
|
|
140
|
+
* @param colors - Node colors
|
|
141
|
+
*/
|
|
142
|
+
drawDogEar(state: DrawState, nodeG: G, w: number, h: number, colors: ColorStyle) {
|
|
143
|
+
const pathDef = this.constants.SHAPES.Dogear?.PathMain;
|
|
144
|
+
if (!pathDef) return;
|
|
145
|
+
|
|
146
|
+
const dogEar = nodeG.path(pathDef)
|
|
147
|
+
.fill(parseColor(colors.secondary) as string)
|
|
148
|
+
.stroke({ color: parseColor(colors.tertiary) as string, width: 1 } as StrokeData);
|
|
149
|
+
|
|
150
|
+
const offsetX = w - this.constants.SHAPES.Dogear!.Width;
|
|
151
|
+
const offsetY = h - this.constants.SHAPES.Dogear!.Height;
|
|
152
|
+
dogEar.move(offsetX, offsetY);
|
|
153
|
+
dogEar.front();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Draws previous and next connections for the node.
|
|
158
|
+
* Previous connection is on top, next is on bottom.
|
|
159
|
+
* @param state - Draw state
|
|
160
|
+
* @param node - NodeSvg instance
|
|
161
|
+
* @param nodeGroup - Node's SVG group
|
|
162
|
+
* @param measurements - Node dimensions
|
|
163
|
+
*/
|
|
164
|
+
drawPreviousNextConnections(state: DrawState, node: NodeSvg, nodeGroup: G, measurements: { width: number, height: number } | null = null) {
|
|
165
|
+
if (!state || !node || !state.bg) return;
|
|
166
|
+
|
|
167
|
+
const c = this.constants;
|
|
168
|
+
const colors: ColorStyle = this.getNodeColors();
|
|
169
|
+
const bbox = state.bg.bbox();
|
|
170
|
+
|
|
171
|
+
// Top connector (previous)
|
|
172
|
+
if (node.previousConnection) {
|
|
173
|
+
const c1 = this.drawPrimaryConnector(nodeGroup, state.bg, 'top', parseColor(this._constants.CONNECTOR_COLOR) as string);
|
|
174
|
+
if (c1) {
|
|
175
|
+
const conn = {
|
|
176
|
+
from: node.previousConnection,
|
|
177
|
+
to: this.resolveConnectable(node.previousConnection.getFrom(), node.previousConnection) as Connection,
|
|
178
|
+
fromCircle: c1 as SvgPath,
|
|
179
|
+
originConn: node.previousConnection,
|
|
180
|
+
originCircle: c1
|
|
181
|
+
};
|
|
182
|
+
this.setConnect(conn);
|
|
183
|
+
eventer.addElement(c1, 'k_connectbubble', { connection: node.previousConnection, node })
|
|
184
|
+
.tagElement(c1, [(this.constructor as typeof Renderer).ELEMENT_TAG, `node_${node.id}`]);
|
|
185
|
+
this._fillOtherNodeConnectorCircle(node.previousConnection, c1 as SvgPath, true);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Bottom connector (next)
|
|
190
|
+
if (node.nextConnection) {
|
|
191
|
+
const c2 = this.drawPrimaryConnector(nodeGroup, state.bg, 'bottom', parseColor(this._constants.CONNECTOR_COLOR) as string);
|
|
192
|
+
if (c2) {
|
|
193
|
+
const conn = {
|
|
194
|
+
from: node.nextConnection,
|
|
195
|
+
to: this.resolveConnectable(node.nextConnection.getTo(), node.nextConnection) as Connection,
|
|
196
|
+
fromCircle: c2 as SvgPath,
|
|
197
|
+
originConn: node.nextConnection,
|
|
198
|
+
originCircle: c2
|
|
199
|
+
};
|
|
200
|
+
this.setConnect(conn);
|
|
201
|
+
eventer.addElement(c2, 'k_connectbubble', { connection: node.nextConnection, node })
|
|
202
|
+
.tagElement(c2, [(this.constructor as typeof Renderer).ELEMENT_TAG, `node_${node.id}`]);
|
|
203
|
+
this._fillOtherNodeConnectorCircle(node.nextConnection, c2 as SvgPath, false);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Draws a primary connector (triangle or circle) on either the top or bottom edge.
|
|
210
|
+
* @param nodeGroup - Node's SVG group
|
|
211
|
+
* @param nodeBg - Node background path
|
|
212
|
+
* @param side - 'top' or 'bottom'
|
|
213
|
+
* @param color - Connector color
|
|
214
|
+
* @returns SvgPath of the connector
|
|
215
|
+
*/
|
|
216
|
+
drawPrimaryConnector(nodeGroup: G, nodeBg: SvgPath, side: 'top' | 'bottom', color: string): SvgPath | void {
|
|
217
|
+
const c = this.constants;
|
|
218
|
+
if (!nodeGroup || !nodeBg) return;
|
|
219
|
+
|
|
220
|
+
const bbox = nodeBg.bbox();
|
|
221
|
+
const triSize = c.CONNECTOR_TRI_SIZE;
|
|
222
|
+
const radius = c.CONNECTOR_RADIUS;
|
|
223
|
+
const x = bbox.width / 2;
|
|
224
|
+
let y: number;
|
|
225
|
+
let element: SvgPath;
|
|
226
|
+
|
|
227
|
+
if (c.CONNECTOR_TRIANGLE) {
|
|
228
|
+
let path = Path.roundedTri(triSize, triSize, 1);
|
|
229
|
+
if (side === 'top') y = -triSize;
|
|
230
|
+
else { y = bbox.height; path = Path.rotatePath(path, 180, triSize / 2, triSize / 2); }
|
|
231
|
+
|
|
232
|
+
element = nodeGroup.path(path)
|
|
233
|
+
.fill(parseColor(color as Hex))
|
|
234
|
+
.stroke({ color: parseColor('#00000000'), width: 0 })
|
|
235
|
+
.transform({ translateX: x - triSize / 2, translateY: y });
|
|
236
|
+
} else {
|
|
237
|
+
const circlePath = Path.circle(radius);
|
|
238
|
+
y = side === 'top' ? 0 : bbox.height;
|
|
239
|
+
|
|
240
|
+
element = nodeGroup.path(circlePath)
|
|
241
|
+
.fill(parseColor(color as Hex))
|
|
242
|
+
.stroke({ color: parseColor('#00000000'), width: 0 })
|
|
243
|
+
.move(x - radius, y - radius);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
element.attr({ class: (this.constructor as typeof Renderer).CONNECTOR_TAG });
|
|
247
|
+
return element;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Draws a raw input field.
|
|
251
|
+
* @param fieldGroup - The SVG group element for the field.
|
|
252
|
+
* @param field - The field to draw.
|
|
253
|
+
* @param startX - The starting X position for the field.
|
|
254
|
+
* @returns The rectangle and text elements of the raw field.
|
|
255
|
+
*/
|
|
256
|
+
drawFieldRaw(fieldGroup: G, field: AnyField, startX: number = 0) {
|
|
257
|
+
const c = this.constants;
|
|
258
|
+
const value = field.getDisplayValue?.() ?? "";
|
|
259
|
+
const { width, height } = this.measureRawField(value);
|
|
260
|
+
|
|
261
|
+
// Draw the rectangle
|
|
262
|
+
const rect = fieldGroup.rect(width, height)
|
|
263
|
+
.fill(parseColor(c.FIELD_RAW_COLOR))
|
|
264
|
+
.stroke({ color: parseColor(c.FIELD_RAW_OUTLINE_COLOR), width: c.FIELD_RAW_OUTLINE_STROKE })
|
|
265
|
+
.radius(20);
|
|
266
|
+
|
|
267
|
+
// Draw the text
|
|
268
|
+
const txt = fieldGroup.text(value)
|
|
269
|
+
.font({
|
|
270
|
+
family: c.FONT_FAMILY,
|
|
271
|
+
size: c.FONT_SIZE,
|
|
272
|
+
anchor: 'start'
|
|
273
|
+
})
|
|
274
|
+
.fill(parseColor(c.FIELD_RAW_TEXT_COLOR));
|
|
275
|
+
txt.node.style.userSelect = 'none';
|
|
276
|
+
|
|
277
|
+
const rawBox: FieldRawBoxData = { box: rect, txt };
|
|
278
|
+
|
|
279
|
+
// Measure text dimensions
|
|
280
|
+
const textBBox = txt.bbox();
|
|
281
|
+
|
|
282
|
+
// Vertical centering
|
|
283
|
+
const offsetY = (height - textBBox.height) / 2;
|
|
284
|
+
|
|
285
|
+
// Horizontal centering with padding
|
|
286
|
+
const paddedWidth = width - 2 * c.INPUT_BOX_PADDING;
|
|
287
|
+
const offsetX = startX + c.INPUT_BOX_PADDING + Math.max(0, (paddedWidth - textBBox.width) / 2);
|
|
288
|
+
|
|
289
|
+
// Move elements
|
|
290
|
+
rect.move(startX, 0);
|
|
291
|
+
txt.move(offsetX, offsetY);
|
|
292
|
+
|
|
293
|
+
// Add event listener
|
|
294
|
+
eventer.addElement(rect, "k_inputbox", {
|
|
295
|
+
field,
|
|
296
|
+
text: txt,
|
|
297
|
+
renderer: this,
|
|
298
|
+
startX
|
|
299
|
+
}).tagElement(rect, [(this.constructor as typeof Renderer).ELEMENT_TAG, `node_${this.node!.id}`]);
|
|
300
|
+
|
|
301
|
+
return { rect, txt, rawBox };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Draws the full node (base, topbar, fields, connectors, label, etc.)
|
|
306
|
+
*/
|
|
307
|
+
drawNode() {
|
|
308
|
+
if (!this.node) return;
|
|
309
|
+
|
|
310
|
+
const colors = this.getNodeColors();
|
|
311
|
+
const node = this.node;
|
|
312
|
+
const state = this.drawState(this.createNodeGroup(node), node.id);
|
|
313
|
+
this._nodeDraw = state;
|
|
314
|
+
|
|
315
|
+
const measurements = this.measureNodeDimensions();
|
|
316
|
+
if (!measurements) return;
|
|
317
|
+
|
|
318
|
+
this.drawNodeBase(state, measurements as NodeMeasurements);
|
|
319
|
+
this.drawNodeTopbar(state, colors, measurements as NodeMeasurements);
|
|
320
|
+
this.drawNodeXButton();
|
|
321
|
+
this.drawNodeLabel(state.group!);
|
|
322
|
+
this.makeNodeDraggable(state.group!, state.topbar!, node);
|
|
323
|
+
|
|
324
|
+
this.createFieldGroup(state);
|
|
325
|
+
this.drawAllFieldsForNode(measurements as NodeMeasurements);
|
|
326
|
+
this.drawPreviousNextConnections(state, node, state.group!, measurements as NodeMeasurements);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
export default ApolloRenderer;
|