@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,186 @@
|
|
|
1
|
+
import WorkspaceSvg from '../src/workspace-svg';
|
|
2
|
+
import userState from '../util/user-state';
|
|
3
|
+
|
|
4
|
+
interface Vec2 { x: number; y: number; }
|
|
5
|
+
|
|
6
|
+
export default class WorkspaceController {
|
|
7
|
+
workspace: WorkspaceSvg;
|
|
8
|
+
|
|
9
|
+
keysDown: Set<string>;
|
|
10
|
+
mouseBtns: Set<number>;
|
|
11
|
+
mousePos: Vec2;
|
|
12
|
+
lastMousePos: Vec2;
|
|
13
|
+
isDragging: boolean;
|
|
14
|
+
|
|
15
|
+
wheelDelta: number;
|
|
16
|
+
movedListeners: (() => void)[];
|
|
17
|
+
_lastMoveFire = 0;
|
|
18
|
+
_moveThrottleMs = 100; // bump this to whatever doesn't lag you
|
|
19
|
+
_queuedMove = false;
|
|
20
|
+
_moveTimeout: any = null;
|
|
21
|
+
_updateInt: any;
|
|
22
|
+
|
|
23
|
+
constructor(workspace: WorkspaceSvg) {
|
|
24
|
+
this.workspace = workspace;
|
|
25
|
+
|
|
26
|
+
this.keysDown = new Set();
|
|
27
|
+
this.mouseBtns = new Set();
|
|
28
|
+
this.mousePos = { x: 0, y: 0 };
|
|
29
|
+
this.lastMousePos = { x: 0, y: 0 };
|
|
30
|
+
this.isDragging = false;
|
|
31
|
+
this.wheelDelta = 0;
|
|
32
|
+
this.movedListeners = [];
|
|
33
|
+
|
|
34
|
+
this._setupListeners();
|
|
35
|
+
|
|
36
|
+
this._updateInt = setInterval(() => this.update(), 16);
|
|
37
|
+
}
|
|
38
|
+
addMoveListener(cb: () => void) {
|
|
39
|
+
this.movedListeners.push(cb);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
removeMoveListener(cb: () => void) {
|
|
43
|
+
const i = this.movedListeners.indexOf(cb);
|
|
44
|
+
if (i !== -1) this.movedListeners.splice(i, 1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fireDidMove() {
|
|
48
|
+
if (typeof queueMicrotask !== 'undefined') {
|
|
49
|
+
queueMicrotask(() => {
|
|
50
|
+
for (let cb of this.movedListeners) {
|
|
51
|
+
try { cb(); } catch (e) { console.error(e); }
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
} else {
|
|
55
|
+
Promise.resolve().then(() => {
|
|
56
|
+
for (let cb of this.movedListeners) {
|
|
57
|
+
try { cb(); } catch (e) { console.error(e); }
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_throttledFireDidMove() {
|
|
64
|
+
const now = performance.now();
|
|
65
|
+
|
|
66
|
+
// enough time passed → fire instantly
|
|
67
|
+
if (now - this._lastMoveFire >= this._moveThrottleMs) {
|
|
68
|
+
this._lastMoveFire = now;
|
|
69
|
+
this.fireDidMove();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// otherwise queue ONE fire
|
|
74
|
+
if (!this._queuedMove) {
|
|
75
|
+
this._queuedMove = true;
|
|
76
|
+
const wait = this._moveThrottleMs - (now - this._lastMoveFire);
|
|
77
|
+
|
|
78
|
+
this._moveTimeout = setTimeout(() => {
|
|
79
|
+
this._queuedMove = false;
|
|
80
|
+
this._lastMoveFire = performance.now();
|
|
81
|
+
this.fireDidMove();
|
|
82
|
+
}, wait);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
getZoom() {
|
|
86
|
+
return 1;
|
|
87
|
+
}
|
|
88
|
+
canMove() {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
private _setupListeners() {
|
|
92
|
+
window.addEventListener('keydown', e => this.keysDown.add(e.key));
|
|
93
|
+
window.addEventListener('keyup', e => this.keysDown.delete(e.key));
|
|
94
|
+
|
|
95
|
+
window.addEventListener('mousedown', e => this.mouseBtns.add(e.button));
|
|
96
|
+
window.addEventListener('mouseup', e => this.mouseBtns.delete(e.button));
|
|
97
|
+
|
|
98
|
+
window.addEventListener('mousemove', e => {
|
|
99
|
+
this.lastMousePos = { ...this.mousePos };
|
|
100
|
+
this.mousePos = { x: e.clientX, y: e.clientY };
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
window.addEventListener('wheel', e => {
|
|
104
|
+
this.wheelDelta = e.deltaY;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
window.addEventListener('mousedown', e => {
|
|
108
|
+
if (e.button === 0) this.isDragging = true;
|
|
109
|
+
});
|
|
110
|
+
window.addEventListener('mouseup', e => {
|
|
111
|
+
if (e.button === 0) this.isDragging = false;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
update() {
|
|
116
|
+
// Can handle keyboard shortcuts or auto-pan here
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// --- Camera methods ---
|
|
120
|
+
pan(dx: number, dy: number) {
|
|
121
|
+
const x = this.workspace._camera.x, y = this.workspace._camera.y;
|
|
122
|
+
this.workspace._camera.x += dx;
|
|
123
|
+
this.workspace._camera.y += dy;
|
|
124
|
+
if (x == this.workspace._camera.x && y == this.workspace._camera.y) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this._throttledFireDidMove();
|
|
128
|
+
this.workspace.didMove = true;
|
|
129
|
+
this.workspace.fireMoveListeners();
|
|
130
|
+
this.refreshPos();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
setCamera(pos: Vec2) {
|
|
134
|
+
const x = this.workspace._camera.x, y = this.workspace._camera.y;
|
|
135
|
+
this.workspace._camera.x = pos.x;
|
|
136
|
+
this.workspace._camera.y = pos.y;
|
|
137
|
+
if (x == this.workspace._camera.x && y == this.workspace._camera.y) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
this._throttledFireDidMove();
|
|
141
|
+
this.workspace.didMove = true;
|
|
142
|
+
this.workspace.fireMoveListeners();
|
|
143
|
+
this.refreshPos();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
centerOn(pos: Vec2) {
|
|
147
|
+
const wsSize = this.workspace.getSize?.() ?? { width: 0, height: 0 };
|
|
148
|
+
this.setCamera({
|
|
149
|
+
x: pos.x - wsSize.width / 2,
|
|
150
|
+
y: pos.y - wsSize.height / 2
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
// --- Coordinate conversion ---
|
|
157
|
+
screenToWorkspace(x: number, y: number): Vec2 {
|
|
158
|
+
const cam = this.workspace._camera;
|
|
159
|
+
return {
|
|
160
|
+
x: x + cam.x,
|
|
161
|
+
y: y + cam.y
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
workspaceToScreen(x: number, y: number): Vec2 {
|
|
166
|
+
const cam = this.workspace._camera;
|
|
167
|
+
return {
|
|
168
|
+
x: (x - cam.x),
|
|
169
|
+
y: (y - cam.y)
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// --- Refresh ---
|
|
174
|
+
refreshPos() {
|
|
175
|
+
this.workspace.refresh?.();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
redraw() {
|
|
179
|
+
this.workspace.redraw?.();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// --- Cleanup ---
|
|
183
|
+
stop() {
|
|
184
|
+
clearInterval(this._updateInt);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import WorkspaceController from './base';
|
|
2
|
+
import WorkspaceSvg from '../src/workspace-svg';
|
|
3
|
+
import userState from '../util/user-state';
|
|
4
|
+
|
|
5
|
+
interface Vec2 { x: number; y: number; }
|
|
6
|
+
|
|
7
|
+
export default class WASDController extends WorkspaceController {
|
|
8
|
+
moveSpeed: number;
|
|
9
|
+
doAccelerate?: boolean;
|
|
10
|
+
accelSpeed: number;
|
|
11
|
+
friction: number;
|
|
12
|
+
velocity: Vec2;
|
|
13
|
+
zoom: number = 1;
|
|
14
|
+
zoomSpeed: number;
|
|
15
|
+
minZoom: number;
|
|
16
|
+
maxZoom: number;
|
|
17
|
+
isFalloff: boolean;
|
|
18
|
+
constructor(workspace: WorkspaceSvg, moveSpeed?: number) {
|
|
19
|
+
super(workspace);
|
|
20
|
+
this.moveSpeed = workspace.options.moveSpeed || moveSpeed || 5;
|
|
21
|
+
|
|
22
|
+
this.doAccelerate = workspace.options?.controls?.wasdSmooth ?? false;
|
|
23
|
+
this.accelSpeed = workspace.options?.controls?.wasdAccelerate ?? 0.2;
|
|
24
|
+
this.friction = workspace.options?.controls?.wasdFriction ?? 0.85;
|
|
25
|
+
|
|
26
|
+
this.velocity = { x: 0, y: 0 };
|
|
27
|
+
this.isFalloff = false;
|
|
28
|
+
|
|
29
|
+
// Zoom settings
|
|
30
|
+
this.zoomSpeed = workspace.options?.controls?.zoomSpeed ?? 0.1;
|
|
31
|
+
this.minZoom = workspace.options?.controls?.minZoom ?? 0.1;
|
|
32
|
+
this.maxZoom = workspace.options?.controls?.maxZoom ?? 4;
|
|
33
|
+
this.zoom = 1;
|
|
34
|
+
// Bind wheel for zoom
|
|
35
|
+
workspace._wsTop.addEventListener('wheel', this.onWheel.bind(this), { passive: false });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
canMove() {
|
|
39
|
+
return !userState.hasState('typing');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
update() {
|
|
43
|
+
super.update();
|
|
44
|
+
if (!this.canMove()) return;
|
|
45
|
+
|
|
46
|
+
let inputX = 0;
|
|
47
|
+
let inputY = 0;
|
|
48
|
+
|
|
49
|
+
if (this.keysDown.has('w') || this.keysDown.has('ArrowUp')) inputY -= 1;
|
|
50
|
+
if (this.keysDown.has('s') || this.keysDown.has('ArrowDown')) inputY += 1;
|
|
51
|
+
if (this.keysDown.has('a') || this.keysDown.has('ArrowLeft')) inputX -= 1;
|
|
52
|
+
if (this.keysDown.has('d') || this.keysDown.has('ArrowRight')) inputX += 1;
|
|
53
|
+
|
|
54
|
+
if (this.doAccelerate) {
|
|
55
|
+
// Accelerate velocity towards input direction
|
|
56
|
+
this.velocity.x += inputX * this.accelSpeed;
|
|
57
|
+
this.velocity.y += inputY * this.accelSpeed;
|
|
58
|
+
|
|
59
|
+
// Apply friction
|
|
60
|
+
this.velocity.x *= this.friction;
|
|
61
|
+
this.velocity.y *= this.friction;
|
|
62
|
+
if (inputX == 0 && inputY == 0) {
|
|
63
|
+
this.isFalloff = true;
|
|
64
|
+
} else {
|
|
65
|
+
this.isFalloff = false;
|
|
66
|
+
}
|
|
67
|
+
// Only pan if velocity is noticeable
|
|
68
|
+
if (Math.abs(this.velocity.x) > 0.01 || Math.abs(this.velocity.y) > 0.01) {
|
|
69
|
+
this.pan(this.velocity.x, this.velocity.y);
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
// Instant movement
|
|
73
|
+
const dx = inputX * this.moveSpeed;
|
|
74
|
+
const dy = inputY * this.moveSpeed;
|
|
75
|
+
if (dx !== 0 || dy !== 0) this.pan(dx, dy);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
pan(dx: number, dy: number) {
|
|
79
|
+
const x = this.workspace._camera.x, y = this.workspace._camera.y;
|
|
80
|
+
this.workspace._camera.x += dx;
|
|
81
|
+
this.workspace._camera.y += dy;
|
|
82
|
+
if (x == this.workspace._camera.x && y == this.workspace._camera.y) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (!this.isFalloff) {
|
|
86
|
+
this._throttledFireDidMove();
|
|
87
|
+
this.workspace.didMove = true;
|
|
88
|
+
this.workspace.fireMoveListeners();
|
|
89
|
+
this.refreshPos();
|
|
90
|
+
}
|
|
91
|
+
this.refreshPos();
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Handles wheel events for zooming.
|
|
95
|
+
* Zooms around the mouse position for intuitive zooming.
|
|
96
|
+
*/
|
|
97
|
+
onWheel(e: WheelEvent) {
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
|
|
100
|
+
const oldZoom = this.getZoom();
|
|
101
|
+
let delta = -e.deltaY * this.zoomSpeed * 0.01; // normalize wheel
|
|
102
|
+
let newZoom = oldZoom * (1 + delta);
|
|
103
|
+
|
|
104
|
+
// Clamp zoom
|
|
105
|
+
newZoom = Math.max(this.minZoom, Math.min(this.maxZoom, newZoom));
|
|
106
|
+
|
|
107
|
+
// Zoom around cursor
|
|
108
|
+
const mouseX = e.clientX;
|
|
109
|
+
const mouseY = e.clientY;
|
|
110
|
+
|
|
111
|
+
const wsMouse = this.workspace.screenToWorkspace(mouseX, mouseY);
|
|
112
|
+
|
|
113
|
+
// Apply new zoom
|
|
114
|
+
this.setZoom(newZoom);
|
|
115
|
+
|
|
116
|
+
// Adjust camera so the point under cursor stays stable
|
|
117
|
+
this.workspace._camera.x = wsMouse.x - mouseX / newZoom;
|
|
118
|
+
this.workspace._camera.y = wsMouse.y - mouseY / newZoom;
|
|
119
|
+
|
|
120
|
+
this.workspace.refresh();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Returns current zoom level */
|
|
124
|
+
getZoom() {
|
|
125
|
+
return this.zoom;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Sets zoom directly */
|
|
129
|
+
setZoom(zoom: number) {
|
|
130
|
+
this.zoom = zoom;
|
|
131
|
+
}
|
|
132
|
+
}
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
**Kabel Project Docs v1.0.6**
|
|
2
|
+
|
|
3
|
+
***
|
|
4
|
+
|
|
5
|
+
# Kabel
|
|
6
|
+
[](https://www.npmjs.com/package/@kabel-project/kabel)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
Node-based visual editor framework with an API inspired by Google’s Blockly project.
|
|
10
|
+
Fully extensible with custom nodes, fields, and renderers.
|
|
11
|
+
Written in TypeScript and ready to use out of the box.
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
You can install Kabel in two main ways:
|
|
20
|
+
|
|
21
|
+
**Using npm (recommended):**
|
|
22
|
+
```bash
|
|
23
|
+
cd path/to/your/project
|
|
24
|
+
npm install @kabel-project/kabel
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
**Cloning from GitHub (Experimental builds):**
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
git clone https://github.com/FentFentFent/Kabel.git --depth 1
|
|
31
|
+
cd Kabel
|
|
32
|
+
npm install
|
|
33
|
+
npm run build
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Then import Kabel from the build:
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
import Kabel from './Kabel/dist/kabel.js';
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
##### Quick Starter HTML
|
|
43
|
+
|
|
44
|
+
```html
|
|
45
|
+
<div id="workspace-container" style="width:800px;height:600px;"></div>
|
|
46
|
+
<script type="module">
|
|
47
|
+
import Kabel from 'kabel';
|
|
48
|
+
Kabel.inject('workspace-container', {
|
|
49
|
+
/* Your options here... */
|
|
50
|
+
});
|
|
51
|
+
</script>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Features
|
|
57
|
+
|
|
58
|
+
* ⚡ Extensible: create custom nodes, fields, and renderers.
|
|
59
|
+
* 🛠️ TypeScript support out of the box.
|
|
60
|
+
* 🎨 Renderer override system for custom visuals.
|
|
61
|
+
* ⌨️ Built-in workspace controls (WASD movement, drag, etc.).
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Example
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
import Kabel from 'kabel';
|
|
69
|
+
|
|
70
|
+
const ws = Kabel.inject('workspace-container', {
|
|
71
|
+
controls: { wasd: true, wasdSmooth: true }
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Register a simple node
|
|
75
|
+
Kabel.Nodes['example'] = {
|
|
76
|
+
init() {
|
|
77
|
+
this.jsonInit({
|
|
78
|
+
labelText: 'Hello Kabel',
|
|
79
|
+
type: 'example_node',
|
|
80
|
+
primaryColor: '#cc0c00'
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
For another example refer to [Kabel Test](_media/index.html)
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Documentation
|
|
91
|
+
|
|
92
|
+
Please refer to [Kabel Guide](_media/docs.md)
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
|
|
2
|
+
# Kabel Documentation
|
|
3
|
+
|
|
4
|
+
**Kabel** is a node-based visual programming editor for building interactive, logic-driven UIs in the browser. It’s built around JavaScript and uses SVG for rendering. Kabel supports node creation, custom fields, widgets, toolboxes, and smooth workspace controls.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Table of Contents
|
|
9
|
+
|
|
10
|
+
1. Getting Started
|
|
11
|
+
2. Setting Up Your First Workspace
|
|
12
|
+
3. Creating Your First Node Prototype
|
|
13
|
+
4. Creating a Toolbox
|
|
14
|
+
5. Manipulating Nodes
|
|
15
|
+
6. Serializing & Deserializing Nodes
|
|
16
|
+
7. Finishing Up
|
|
17
|
+
|
|
18
|
+
## In-depth Docs
|
|
19
|
+
[Kabel API Documentation](../../docs/globals.md)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 1. Getting Started
|
|
24
|
+
|
|
25
|
+
You can include Kabel either via a script tag or import it as a module:
|
|
26
|
+
|
|
27
|
+
```html
|
|
28
|
+
<script src="./kabel.js"></script>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Once loaded:
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
if (typeof Kabel !== 'undefined') {
|
|
35
|
+
console.log('Kabel is ready!');
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
That’s all you need to confirm it’s available before initializing a workspace.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## 2. Setting Up Your First Workspace
|
|
44
|
+
|
|
45
|
+
The **workspace** is where nodes live and interact. Start by creating an HTML container:
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
const el = document.createElement('div');
|
|
49
|
+
Object.assign(el.style, {
|
|
50
|
+
width: '100vw',
|
|
51
|
+
height: '100vh',
|
|
52
|
+
background: '#f0f0f0'
|
|
53
|
+
});
|
|
54
|
+
document.body.appendChild(el);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Then inject Kabel into that container:
|
|
58
|
+
|
|
59
|
+
```js
|
|
60
|
+
const ws = Kabel.inject(el, {
|
|
61
|
+
moveSpeed: 6,
|
|
62
|
+
controls: {
|
|
63
|
+
wasd: true,
|
|
64
|
+
wasdSmooth: true,
|
|
65
|
+
wasdAccelerate: 1,
|
|
66
|
+
wasdFriction: 0.9
|
|
67
|
+
},
|
|
68
|
+
toolbox: { /* defined later */ }
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This gives you a fullscreen workspace with smooth WASD movement controls.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 3. Creating Your First Node Prototype
|
|
77
|
+
|
|
78
|
+
Nodes are defined once as **prototypes** under `Kabel.Nodes`. Here’s an example:
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
Kabel.Nodes['my_node'] = {
|
|
82
|
+
init() {
|
|
83
|
+
this.jsonInit({
|
|
84
|
+
previousConnection: true,
|
|
85
|
+
nextConnection: true,
|
|
86
|
+
labelText: 'TopbarText',
|
|
87
|
+
type: 'my_node',
|
|
88
|
+
primaryColor: '#cc0c00',
|
|
89
|
+
tertiaryColor: '#660500',
|
|
90
|
+
arguments: [
|
|
91
|
+
{ name: 'FLD', type: 'field_str', label: 'LabelText: ', value: 'ValueText' },
|
|
92
|
+
{ name: 'Dummy', type: 'field_dummy', label: 'Label with no input!' },
|
|
93
|
+
{ name: 'Conn', type: 'connection', label: 'Label!' },
|
|
94
|
+
{ name: 'Conn2', type: 'field_both', label: 'lowk: ', value: 'lowk' },
|
|
95
|
+
{
|
|
96
|
+
name: 'Dropdown',
|
|
97
|
+
type: 'dropdown',
|
|
98
|
+
label: 'lowk dropdown?: ',
|
|
99
|
+
options: ['Oooo option', { text: 'Opt', value: 'opt' }]
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Key options:**
|
|
108
|
+
|
|
109
|
+
* `previousConnection` / `nextConnection` → Whether the node can chain with others.
|
|
110
|
+
* `arguments` → Defines fields, inputs, and connection points.
|
|
111
|
+
* `jsonInit()` → Initializes node configuration.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 4. Creating a Toolbox
|
|
116
|
+
|
|
117
|
+
Toolboxes organize your available nodes:
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
toolbox: {
|
|
121
|
+
type: 'category',
|
|
122
|
+
contents: [{
|
|
123
|
+
name: 'Cat',
|
|
124
|
+
color: '#cc0c00',
|
|
125
|
+
contents: [{
|
|
126
|
+
type: 'my_node',
|
|
127
|
+
arguments: {}
|
|
128
|
+
}]
|
|
129
|
+
}]
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Toolbox types:**
|
|
134
|
+
|
|
135
|
+
* `'category'` → Groups nodes visually into collapsible sections.
|
|
136
|
+
* `'flyout'` → Displays all nodes in one scrollable list.
|
|
137
|
+
|
|
138
|
+
**Category JSON fields:**
|
|
139
|
+
|
|
140
|
+
* `name`: The category name.
|
|
141
|
+
* `color`: The display color.
|
|
142
|
+
* `contents`: Array of node definitions.
|
|
143
|
+
|
|
144
|
+
> Currently, categories and flyouts can only contain nodes. Support for buttons or UI elements may be added later.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 5. Manipulating Node Properties
|
|
149
|
+
|
|
150
|
+
Once a node is created, you can move, modify, or connect it programmatically:
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
const node = new Kabel.NodeSvg(Kabel.Nodes['my_node'], ws);
|
|
154
|
+
node.init();
|
|
155
|
+
|
|
156
|
+
const node2 = new Kabel.NodeSvg(Kabel.Nodes['my_node'], ws);
|
|
157
|
+
node2.init();
|
|
158
|
+
node2.relativeCoords.set(20, 0);
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Connect them:
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
node.nextConnection.to = node2;
|
|
165
|
+
node2.previousConnection.from = node;
|
|
166
|
+
ws.redraw();
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
You can also manipulate the SVG directly (temporary, not saved across rerenders):
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
node.svg.highlight('#ff0');
|
|
173
|
+
node.svg.setScale(1);
|
|
174
|
+
node.svg.moveTo(0, 1);
|
|
175
|
+
node.svg.applyTransform('translate(0, 1)');
|
|
176
|
+
node.svg.getRaw(); // Returns the svg.js G element
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
### Optional: Widgets
|
|
182
|
+
|
|
183
|
+
**Widgets** are custom UI overlays within the workspace:
|
|
184
|
+
|
|
185
|
+
```js
|
|
186
|
+
Kabel.Widgets['testWidget'] = {
|
|
187
|
+
name: 'Test Widget',
|
|
188
|
+
width: 150,
|
|
189
|
+
height: 100,
|
|
190
|
+
coords: new Kabel.Coordinates(50, 50),
|
|
191
|
+
html: '<button id="counterBtn">Count: 0</button>',
|
|
192
|
+
init(widget, container) {
|
|
193
|
+
let count = 0;
|
|
194
|
+
const btn = container.querySelector('#counterBtn');
|
|
195
|
+
if (btn) {
|
|
196
|
+
btn.addEventListener('click', () => {
|
|
197
|
+
count++;
|
|
198
|
+
btn.textContent = 'Count: ' + count;
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const myWidget = ws.newWidget('testWidget');
|
|
205
|
+
if (myWidget) myWidget.show();
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Widgets let you add custom interactivity or information panels.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## 6. Serializing Nodes
|
|
213
|
+
|
|
214
|
+
Kabel supports both **circular** and **non-circular** JSON serialization.
|
|
215
|
+
|
|
216
|
+
### Circular Serialization
|
|
217
|
+
|
|
218
|
+
Contains live object references (not exportable as plain JSON, but usable within JS).
|
|
219
|
+
|
|
220
|
+
```js
|
|
221
|
+
const circular = node.serialize();
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Example (simplified):
|
|
225
|
+
|
|
226
|
+
```json
|
|
227
|
+
{
|
|
228
|
+
"id": "sisDG24Gp6lc7*S#m#I9c",
|
|
229
|
+
"type": "my_node",
|
|
230
|
+
"nextConnection": {
|
|
231
|
+
"node": "[CircularNodeRef]"
|
|
232
|
+
},
|
|
233
|
+
"fields": [...]
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
### Non-Circular Serialization
|
|
240
|
+
|
|
241
|
+
Safe for exporting or saving to files:
|
|
242
|
+
|
|
243
|
+
```js
|
|
244
|
+
const nonCircular = node.toJson();
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Example:
|
|
248
|
+
|
|
249
|
+
```json
|
|
250
|
+
{
|
|
251
|
+
"7f7#2Rrt16uFuH^2f2^ad": {
|
|
252
|
+
"id": "7f7#2Rrt16uFuH^2f2^ad",
|
|
253
|
+
"type": "my_node",
|
|
254
|
+
"colors": { "primary": "#cc0c00", "tertiary": "#660500" },
|
|
255
|
+
"label": "TopbarText",
|
|
256
|
+
"nextConnection": { "node": "jX^$kh)yHW5p^bx98vD#s" },
|
|
257
|
+
"fields": [...]
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
### Workspace Serialization
|
|
265
|
+
|
|
266
|
+
```js
|
|
267
|
+
// Serialize
|
|
268
|
+
const data = workspace.toJson(false); // false = non-circular
|
|
269
|
+
|
|
270
|
+
// Deserialize
|
|
271
|
+
workspace.fromJson(data);
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The serialization format automatically includes the circular/non-circular flag internally.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## 7. Finishing Up
|
|
279
|
+
|
|
280
|
+
You can expose internals for quick debugging:
|
|
281
|
+
|
|
282
|
+
```js
|
|
283
|
+
window.ws = ws;
|
|
284
|
+
window.nodes = [node, node2];
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
That’s it — you now have a working Kabel workspace with nodes, a toolbox, and serialization support.
|
|
288
|
+
|
|
289
|
+
---
|