@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
package/src/nodesvg.ts
ADDED
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
import Connection, { Connectable } from "./connection";
|
|
2
|
+
import { NodePrototype } from "./node-types";
|
|
3
|
+
import { ColorStyle, Color } from './visual-types';
|
|
4
|
+
import hasProp from '../util/has-prop';
|
|
5
|
+
import Field, { AnyField, AnyFieldCls, DummyField, FieldMap, FieldOptions } from "./field";
|
|
6
|
+
import CategoryColors from "./colors";
|
|
7
|
+
import Coordinates from "./coordinates";
|
|
8
|
+
import { generateUID } from "../util/uid";
|
|
9
|
+
import EventEmitter from '../util/emitter';
|
|
10
|
+
import { G } from "@svgdotjs/svg.js";
|
|
11
|
+
import WorkspaceSvg from "./workspace-svg";
|
|
12
|
+
import RendererConstants from "../renderers/constants";
|
|
13
|
+
import CommentModel, { CommentSerialized } from "./comment";
|
|
14
|
+
import { RepresenterNode } from '../renderers/representer-node';
|
|
15
|
+
import NodePrototypes from "./prototypes";
|
|
16
|
+
/**
|
|
17
|
+
* Represents the JSON structure used to initialize a field on a node.
|
|
18
|
+
* Each field has a type, label, and name. Additional properties can be included for field-specific configuration.
|
|
19
|
+
*/
|
|
20
|
+
export interface InputFieldJson {
|
|
21
|
+
/** Human-readable label for the field, shown on the node UI */
|
|
22
|
+
label: string;
|
|
23
|
+
|
|
24
|
+
/** Field type identifier, corresponding to a field constructor in FieldMap */
|
|
25
|
+
type: string;
|
|
26
|
+
|
|
27
|
+
/** Unique field name within the node */
|
|
28
|
+
name: string;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Optional additional properties for field initialization.
|
|
32
|
+
* Can include type-specific options like min/max for number fields,
|
|
33
|
+
* default values, dropdown options, etc.
|
|
34
|
+
*/
|
|
35
|
+
[key: string]: any;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Represents a JSON structure for initializing a NodeSvg instance.
|
|
40
|
+
* Includes colors, connections, label, fields, category, and type information.
|
|
41
|
+
*/
|
|
42
|
+
export interface NodeJson {
|
|
43
|
+
/** Primary color of the node (e.g., top bar, main connections) */
|
|
44
|
+
primaryColor?: Color | undefined;
|
|
45
|
+
|
|
46
|
+
/** Secondary color of the node (e.g., field backgrounds) */
|
|
47
|
+
secondaryColor?: Color | undefined;
|
|
48
|
+
|
|
49
|
+
/** Tertiary color of the node (e.g., outlines) */
|
|
50
|
+
tertiaryColor?: Color | undefined;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Optional previous connection data.
|
|
54
|
+
* Presence triggers creation of a previous connection when initializing NodeSvg.
|
|
55
|
+
*/
|
|
56
|
+
previousConnection?: any | undefined;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Optional next connection data.
|
|
60
|
+
* Presence triggers creation of a next connection when initializing NodeSvg.
|
|
61
|
+
*/
|
|
62
|
+
nextConnection?: any | undefined;
|
|
63
|
+
|
|
64
|
+
/** Optional node label text to display */
|
|
65
|
+
labelText?: string | undefined;
|
|
66
|
+
|
|
67
|
+
/** Array of field definitions (InputFieldJson) to attach to this node */
|
|
68
|
+
arguments?: InputFieldJson[] | undefined;
|
|
69
|
+
|
|
70
|
+
/** Optional category name for color theming */
|
|
71
|
+
category?: string | undefined;
|
|
72
|
+
|
|
73
|
+
/** Node type identifier, used to look up the NodePrototype */
|
|
74
|
+
type: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Represents a fully serialized node including its fields, colors, coordinates, connections, and optional comment.
|
|
79
|
+
* Used for saving or transferring node data.
|
|
80
|
+
*/
|
|
81
|
+
export interface SerializedNode {
|
|
82
|
+
/** Node type string */
|
|
83
|
+
type: string;
|
|
84
|
+
|
|
85
|
+
/** Unique node ID */
|
|
86
|
+
id: string;
|
|
87
|
+
|
|
88
|
+
/** Display label of the node */
|
|
89
|
+
label: string;
|
|
90
|
+
|
|
91
|
+
/** Node colors including primary, secondary, tertiary, and category */
|
|
92
|
+
colors: ColorStyle;
|
|
93
|
+
|
|
94
|
+
/** Coordinates of the node relative to its workspace */
|
|
95
|
+
relativeCoords: { x: number; y: number };
|
|
96
|
+
|
|
97
|
+
/** Optional comment text attached to the node */
|
|
98
|
+
comment?: CommentSerialized | undefined;
|
|
99
|
+
|
|
100
|
+
/** Array of serialized fields, may contain any field-specific structure */
|
|
101
|
+
fields?: any[] | undefined;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Serialized representation of the previous connection.
|
|
105
|
+
* If `field` is true, the connection originates from a field rather than a node.
|
|
106
|
+
*/
|
|
107
|
+
previousConnection?: { field?: boolean | undefined; node?: SerializedNode | undefined } | undefined;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Serialized representation of the next connection.
|
|
111
|
+
* If `field` is true, the connection originates from a field rather than a node.
|
|
112
|
+
*/
|
|
113
|
+
nextConnection?: { field?: boolean | undefined; node?: SerializedNode | undefined } | undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* NodeStyle represents the styling configuration of a node.
|
|
118
|
+
* Includes ColorStyle properties plus optional renderer-specific constants and arbitrary additional fields.
|
|
119
|
+
*/
|
|
120
|
+
export type NodeStyle = ColorStyle &
|
|
121
|
+
{
|
|
122
|
+
/** Optional renderer-specific constants from RendererConstants */
|
|
123
|
+
[key in keyof RendererConstants]?: RendererConstants[key];
|
|
124
|
+
} &
|
|
125
|
+
{
|
|
126
|
+
/** Any additional style properties supported by extensions or custom renderers */
|
|
127
|
+
[key: string]: any;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Events emitted by NodeSvg instances.
|
|
132
|
+
* Consumers can listen to these events to react to node lifecycle changes.
|
|
133
|
+
*/
|
|
134
|
+
export interface NodeEvents {
|
|
135
|
+
/** Triggered before the node is removed from the workspace */
|
|
136
|
+
"REMOVING": null;
|
|
137
|
+
|
|
138
|
+
/** Triggered immediately after node initialization */
|
|
139
|
+
"INITING": null;
|
|
140
|
+
|
|
141
|
+
/** Triggered while the node is being dragged */
|
|
142
|
+
"NODE_DRAG": null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Represents a node in the workspace.
|
|
147
|
+
* Handles connections, fields, colors, serialization, and events.
|
|
148
|
+
*/
|
|
149
|
+
class NodeSvg extends EventEmitter<NodeEvents> {
|
|
150
|
+
/** The previous connection for this node (null if none) */
|
|
151
|
+
previousConnection: Connection | null;
|
|
152
|
+
/** The next connection for this node (null if none) */
|
|
153
|
+
nextConnection: Connection | null;
|
|
154
|
+
/** Node type string, usually derived from prototype */
|
|
155
|
+
type: string | null;
|
|
156
|
+
/** Prototype object providing behavior for this node */
|
|
157
|
+
prototype: NodePrototype | null;
|
|
158
|
+
/** Node color style object */
|
|
159
|
+
colors: NodeStyle; // Node's color scheme
|
|
160
|
+
/** Displayed label text for this node */
|
|
161
|
+
labelText: string; // Displayed node label
|
|
162
|
+
/** Set of fields attached to this node */
|
|
163
|
+
_fieldColumn: Set<AnyField>; // Stores attached fields
|
|
164
|
+
/** Node coordinates relative to workspace */
|
|
165
|
+
relativeCoords: Coordinates;
|
|
166
|
+
/** Unique node ID */
|
|
167
|
+
id: string;
|
|
168
|
+
/** SVG representation of this node */
|
|
169
|
+
svg?: RepresenterNode | object | null = null;
|
|
170
|
+
/** Workspace this node belongs to */
|
|
171
|
+
workspace: WorkspaceSvg | null = null;
|
|
172
|
+
/** Optional comment attached to this node */
|
|
173
|
+
comment: CommentModel | null
|
|
174
|
+
/** Event key: "REMOVING" */
|
|
175
|
+
static REMOVING: keyof NodeEvents = "REMOVING";
|
|
176
|
+
|
|
177
|
+
/** Event key: "INITING" */
|
|
178
|
+
static INITING: keyof NodeEvents = "INITING";
|
|
179
|
+
/**
|
|
180
|
+
* Creates a NodeSvg instance.
|
|
181
|
+
* @param prototype Optional NodePrototype to associate with this node.
|
|
182
|
+
* @param workspace Optional WorkspaceSvg this node belongs to.
|
|
183
|
+
* @param svgGroup Optional SVG group to attach node visuals.
|
|
184
|
+
*/
|
|
185
|
+
constructor(prototype: NodePrototype | null, workspace?: WorkspaceSvg, svgGroup?: G) {
|
|
186
|
+
super();
|
|
187
|
+
this.type = null;
|
|
188
|
+
this.comment = null;
|
|
189
|
+
this.prototype = prototype;
|
|
190
|
+
this.colors = {
|
|
191
|
+
primary: '#000000', // Topbar & connection color
|
|
192
|
+
secondary: '#000000', // Field & dropdown backgrounds
|
|
193
|
+
tertiary: '#000000', // Outline color
|
|
194
|
+
category: '' // Node category name (optional)
|
|
195
|
+
};
|
|
196
|
+
this.previousConnection = new Connection(null, this, true); //1st arg is from, 2nd is to, third is if this conn is prev
|
|
197
|
+
this.nextConnection = new Connection(this, null, false); //1st arg is from, 2nd is to, third is if this conn is prev
|
|
198
|
+
this.labelText = '';
|
|
199
|
+
this._fieldColumn = new Set();
|
|
200
|
+
this.relativeCoords = new Coordinates(0, 0);
|
|
201
|
+
this.id = generateUID('nanoid', { alphabet: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0129384756!)@(#*$&%^' });
|
|
202
|
+
if (workspace) {
|
|
203
|
+
this.workspace = workspace;
|
|
204
|
+
}
|
|
205
|
+
if (svgGroup) {
|
|
206
|
+
this.svg = new RepresenterNode(this, {
|
|
207
|
+
id: this.id,
|
|
208
|
+
pendingConnections: [],
|
|
209
|
+
group: svgGroup
|
|
210
|
+
}, workspace!.renderer)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/** Returns true if this node has no previous connection (i.e., top-level node) */
|
|
214
|
+
get topLevel() {
|
|
215
|
+
return !(this.previousConnection?.getFrom());
|
|
216
|
+
}
|
|
217
|
+
/** Returns the raw SVG group element if present */
|
|
218
|
+
get svgGroup() {
|
|
219
|
+
return (this.svg as RepresenterNode)?.getRaw?.();
|
|
220
|
+
}
|
|
221
|
+
/** Returns the text of the node's comment, if any */
|
|
222
|
+
getCommentText() {
|
|
223
|
+
return this.comment?.getText?.();
|
|
224
|
+
}
|
|
225
|
+
/** Returns the CommentModel instance for this node, if any */
|
|
226
|
+
getComment() {
|
|
227
|
+
return this.comment;
|
|
228
|
+
}
|
|
229
|
+
/** Adds a new comment to this node if none exists */
|
|
230
|
+
addComment() {
|
|
231
|
+
if (!this.comment) {
|
|
232
|
+
this.comment = new CommentModel(this);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
console.warn('Comment already exists.')
|
|
236
|
+
}
|
|
237
|
+
/** Sets the text for the node's comment, creating one if needed */
|
|
238
|
+
setCommentText(text: string) {
|
|
239
|
+
if (!this.comment) {
|
|
240
|
+
this.comment = new CommentModel(this);
|
|
241
|
+
}
|
|
242
|
+
this.comment.setText(text);
|
|
243
|
+
}
|
|
244
|
+
/** Removes the comment from the node and triggers workspace redraw */
|
|
245
|
+
removeComment() {
|
|
246
|
+
this.comment = null;
|
|
247
|
+
this.workspace?.redrawComments?.();
|
|
248
|
+
}
|
|
249
|
+
/** Returns an array of all fields attached to this node */
|
|
250
|
+
allFields() {
|
|
251
|
+
return Array.from(this._fieldColumn);
|
|
252
|
+
}
|
|
253
|
+
/** Retrieves a field by name from this node */
|
|
254
|
+
getFieldByName(name: string): AnyField | null | undefined {
|
|
255
|
+
let field: AnyField | null | undefined = this.allFields().find(fld => fld.getName() === name);
|
|
256
|
+
|
|
257
|
+
return field;
|
|
258
|
+
}
|
|
259
|
+
/** Alias for getFieldByName */
|
|
260
|
+
getField(name: string): AnyField | null | undefined {
|
|
261
|
+
return this.getFieldByName(name);
|
|
262
|
+
}
|
|
263
|
+
/** Retrieves the current value of a field by name */
|
|
264
|
+
getFieldValue(name: string): any | undefined {
|
|
265
|
+
const fld: AnyField | null | undefined = this.getFieldByName(name);
|
|
266
|
+
if (fld) {
|
|
267
|
+
return fld.getValue();
|
|
268
|
+
}
|
|
269
|
+
return undefined;
|
|
270
|
+
}
|
|
271
|
+
/** Retrieves the display value of a field by name */
|
|
272
|
+
getFieldDisplayValue(name: string): any | undefined {
|
|
273
|
+
const fld: AnyField | null | undefined = this.getFieldByName(name);
|
|
274
|
+
if (fld) {
|
|
275
|
+
return fld.getDisplayValue();
|
|
276
|
+
}
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
/** Sets the value of a field by name */
|
|
280
|
+
setFieldValue(name: string, value: any) {
|
|
281
|
+
const fld: AnyField | null | undefined = this.getFieldByName(name);
|
|
282
|
+
if (fld) {
|
|
283
|
+
fld.setValue(value as never); // I don't like using the "as" statement here, but it's necessary to satisfy TypeScript.
|
|
284
|
+
}
|
|
285
|
+
return fld;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Initiates the node, calling prototype methods.
|
|
290
|
+
*/
|
|
291
|
+
init() {
|
|
292
|
+
this.emit(NodeSvg.INITING, null);
|
|
293
|
+
if (this.prototype) {
|
|
294
|
+
if (this.prototype.init) this.prototype.init.call(this, this.prototype, this);
|
|
295
|
+
if (this.workspace) {
|
|
296
|
+
this.workspace.addNode(this)
|
|
297
|
+
}
|
|
298
|
+
if (this.prototype.removed) {
|
|
299
|
+
this.on(NodeSvg.REMOVING, () => {
|
|
300
|
+
this.prototype?.removed.call(this, this.prototype, this);
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
console.warn(`Node with id ${this.id} is missing a prototype.`)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/** Returns whether this node has a category style applied */
|
|
308
|
+
hasCategoryStyle() {
|
|
309
|
+
return !!this.colors?.category && this.colors?.category?.length > 0;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Returns the category name or null if none */
|
|
313
|
+
getCategoryName() {
|
|
314
|
+
return this.colors?.category || null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Returns the node's current ColorStyle */
|
|
318
|
+
getStyle() {
|
|
319
|
+
return this.colors;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Internal helper: attach a field to this node */
|
|
323
|
+
_appendFieldItem(field: AnyField) {
|
|
324
|
+
if (!field) console.warn("Falsey field passed to _appendFieldItem.");
|
|
325
|
+
this._fieldColumn.add(field);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Initialize node from a NodeJson object */
|
|
329
|
+
jsonInit(json: NodeJson) {
|
|
330
|
+
if (json.primaryColor) this.colors.primary = json.primaryColor;
|
|
331
|
+
if (json.secondaryColor) this.colors.secondary = json.secondaryColor;
|
|
332
|
+
if (json.tertiaryColor) this.colors.tertiary = json.tertiaryColor;
|
|
333
|
+
|
|
334
|
+
// Apply category colors if defined
|
|
335
|
+
if (json.category && CategoryColors[json.category]) {
|
|
336
|
+
const style: ColorStyle = CategoryColors[json.category] as ColorStyle;
|
|
337
|
+
Object.assign(this.colors, { category: json.category }, style);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
this.previousConnection = hasProp(json, 'previousConnection') ? new Connection(null, this, true) : null;
|
|
341
|
+
this.nextConnection = hasProp(json, 'nextConnection') ? new Connection(this, null, false) : null;
|
|
342
|
+
|
|
343
|
+
if (json.labelText) this.labelText = json.labelText;
|
|
344
|
+
if (json.arguments) this.applyJsonArguments(json.arguments);
|
|
345
|
+
if (json.type) {
|
|
346
|
+
this.type = json.type;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/* JAVASCRIPT API */
|
|
351
|
+
|
|
352
|
+
/** Apply field definitions from a JSON-like array without full NodeJson */
|
|
353
|
+
applyJsonArguments(args: InputFieldJson[]) {
|
|
354
|
+
for (let field of args) {
|
|
355
|
+
if (!field.type || !field.name) {
|
|
356
|
+
console.warn(`Invalid argument definition at: ${args.indexOf(field)}.`);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const FieldConstructor: AnyFieldCls | undefined = FieldMap[field.type] as AnyFieldCls | undefined;
|
|
361
|
+
if (!FieldConstructor) {
|
|
362
|
+
console.warn(`Missing field constructor for ${field.type}!`);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const fld: AnyField = new FieldConstructor();
|
|
367
|
+
fld.fromJson(field); // initialize field
|
|
368
|
+
fld.node = this;
|
|
369
|
+
this._appendFieldItem(fld);
|
|
370
|
+
}
|
|
371
|
+
return this;
|
|
372
|
+
}
|
|
373
|
+
/** Appends a connection field to this node */
|
|
374
|
+
appendConnection(name: string): Field {
|
|
375
|
+
const fld = new (FieldMap['connection'])();
|
|
376
|
+
this._appendFieldItem(fld);
|
|
377
|
+
fld.setName(name);
|
|
378
|
+
fld.node = this;
|
|
379
|
+
return fld;
|
|
380
|
+
}
|
|
381
|
+
/** Appends a numeric input field to this node */
|
|
382
|
+
appendNumber(name: string): Field {
|
|
383
|
+
const fld = new (FieldMap['field_num'])();
|
|
384
|
+
this._appendFieldItem(fld);
|
|
385
|
+
fld.setName(name);
|
|
386
|
+
fld.node = this;
|
|
387
|
+
return fld;
|
|
388
|
+
}
|
|
389
|
+
/** Appends a text input field to this node */
|
|
390
|
+
appendText(name: string): Field {
|
|
391
|
+
const fld = new (FieldMap['field_str'])();
|
|
392
|
+
this._appendFieldItem(fld);
|
|
393
|
+
fld.setName(name);
|
|
394
|
+
fld.node = this;
|
|
395
|
+
return fld;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** Appends a field that can hold a connection or raw value */
|
|
399
|
+
appendOptLink(name: string): Field {
|
|
400
|
+
const fld = new (FieldMap['field_both'])();
|
|
401
|
+
this._appendFieldItem(fld);
|
|
402
|
+
fld.setName(name);
|
|
403
|
+
fld.node = this;
|
|
404
|
+
return fld;
|
|
405
|
+
}
|
|
406
|
+
/** Sets the category name for the node */
|
|
407
|
+
setCategoryName(name: string) {
|
|
408
|
+
this.colors.category = name;
|
|
409
|
+
return this;
|
|
410
|
+
}
|
|
411
|
+
/** Applies a ColorStyle to the node */
|
|
412
|
+
setStyle(style: ColorStyle) {
|
|
413
|
+
// apply properties from style into this.colors
|
|
414
|
+
Object.assign(this.colors, style);
|
|
415
|
+
return this;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** Sets primary, secondary, and tertiary colors for the node */
|
|
419
|
+
setColor(primary: Color, secondary: Color, tertiary: Color) {
|
|
420
|
+
this.setStyle({ primary, secondary, tertiary });
|
|
421
|
+
return this;
|
|
422
|
+
}
|
|
423
|
+
/** Sets the label text for the node */
|
|
424
|
+
setLabelText(text: string) {
|
|
425
|
+
this.labelText = text;
|
|
426
|
+
return this;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Add or replace a previous/next connection based on argument */
|
|
430
|
+
setConnection(prevOrNext: string | number | boolean) {
|
|
431
|
+
const stringed = String(prevOrNext).toLowerCase();
|
|
432
|
+
const cast = stringed == '0' ? 0 : (stringed == '1' ? 1 : (stringed == 'true' ? 1 : (stringed == 'false' ? 0 : 3)));
|
|
433
|
+
|
|
434
|
+
if (cast === 0) {
|
|
435
|
+
this.previousConnection = new Connection(null, this, true);
|
|
436
|
+
} else if (cast === 1) {
|
|
437
|
+
this.nextConnection = new Connection(this, null, false);
|
|
438
|
+
} else {
|
|
439
|
+
console.warn('Invalid prevOrNext argument for NodeSvg.setConnection');
|
|
440
|
+
}
|
|
441
|
+
return this;
|
|
442
|
+
}
|
|
443
|
+
/** Copies another NodeSvg into this node */
|
|
444
|
+
fromNode(other: NodeSvg) {
|
|
445
|
+
if (!other) return;
|
|
446
|
+
|
|
447
|
+
// Copy primitive props
|
|
448
|
+
this.type = other.type;
|
|
449
|
+
this.labelText = other.labelText;
|
|
450
|
+
this.relativeCoords = new Coordinates(other.relativeCoords.x, other.relativeCoords.y);
|
|
451
|
+
|
|
452
|
+
// Copy colors
|
|
453
|
+
this.colors = { ...other.colors };
|
|
454
|
+
|
|
455
|
+
// Copy connections
|
|
456
|
+
this.previousConnection = other.previousConnection
|
|
457
|
+
? new Connection(null, this, true)
|
|
458
|
+
: null;
|
|
459
|
+
this.nextConnection = other.nextConnection
|
|
460
|
+
? new Connection(this, null, false)
|
|
461
|
+
: null;
|
|
462
|
+
|
|
463
|
+
// Copy fields
|
|
464
|
+
this._fieldColumn.clear();
|
|
465
|
+
for (let field of other._fieldColumn) {
|
|
466
|
+
const FieldCls = field.constructor as AnyFieldCls;
|
|
467
|
+
const newField = (new FieldCls()) as any;
|
|
468
|
+
|
|
469
|
+
// Copy basic properties
|
|
470
|
+
newField.setName(field.getName());
|
|
471
|
+
if ('getValue' in field && 'setValue' in newField) {
|
|
472
|
+
newField.setValue(field.getValue());
|
|
473
|
+
}
|
|
474
|
+
if ('getLabel' in field && 'setLabel' in newField) {
|
|
475
|
+
newField.setLabel(field.getLabel())
|
|
476
|
+
}
|
|
477
|
+
this._appendFieldItem(newField);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Copy workspace reference
|
|
481
|
+
this.workspace = other.workspace;
|
|
482
|
+
|
|
483
|
+
// Copy prototype reference
|
|
484
|
+
this.prototype = other.prototype;
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
return this;
|
|
488
|
+
}
|
|
489
|
+
/** Serializes a Connection object, handling fields and nested nodes */
|
|
490
|
+
_serializeConnection(
|
|
491
|
+
c: Connection,
|
|
492
|
+
alreadyProcessed: { [key: string]: SerializedNode }
|
|
493
|
+
): { field?: boolean | undefined; node?: SerializedNode | undefined } {
|
|
494
|
+
const returned: { field?: boolean; node?: SerializedNode } = {};
|
|
495
|
+
let connected: NodeSvg | AnyField | null = c.isPrevious ? c.getFrom() : c.getTo();
|
|
496
|
+
|
|
497
|
+
if (!connected) return returned;
|
|
498
|
+
|
|
499
|
+
if (connected instanceof NodeSvg) {
|
|
500
|
+
// Avoid serializing the same node twice
|
|
501
|
+
if (alreadyProcessed[connected.id]) {
|
|
502
|
+
return { node: alreadyProcessed[connected.id] };
|
|
503
|
+
}
|
|
504
|
+
returned.node = connected.serialize(alreadyProcessed);
|
|
505
|
+
} else {
|
|
506
|
+
// Field serialization
|
|
507
|
+
const fld = connected as AnyField;
|
|
508
|
+
// If the field has a node, we serialize the node first
|
|
509
|
+
let fieldNode: SerializedNode | undefined;
|
|
510
|
+
if (fld.node) {
|
|
511
|
+
if (alreadyProcessed[fld.node.id]) {
|
|
512
|
+
fieldNode = alreadyProcessed[fld.node.id];
|
|
513
|
+
} else {
|
|
514
|
+
fieldNode = fld.node.serialize(alreadyProcessed);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
returned.field = true;
|
|
518
|
+
if (fieldNode) returned.node = fieldNode;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return returned;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Serialize a node, this includes circular references. use toJson to avoid those.
|
|
526
|
+
* @param alreadyProcessed - Internal.
|
|
527
|
+
* @returns
|
|
528
|
+
*/
|
|
529
|
+
serialize(alreadyProcessed: { [key: string]: SerializedNode } = {}): SerializedNode {
|
|
530
|
+
if (alreadyProcessed[this.id]) {
|
|
531
|
+
return alreadyProcessed[this.id] as SerializedNode;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Put a placeholder in map *before* serializing connections
|
|
535
|
+
const serialized: SerializedNode = {
|
|
536
|
+
id: this.id,
|
|
537
|
+
type: this.type || '',
|
|
538
|
+
colors: { primary: this.colors.primary, secondary: this.colors.secondary, tertiary: this.colors.tertiary, category: this.colors.category } as ColorStyle,
|
|
539
|
+
label: this.labelText,
|
|
540
|
+
previousConnection: undefined,
|
|
541
|
+
nextConnection: undefined,
|
|
542
|
+
relativeCoords: { x: this.relativeCoords.x, y: this.relativeCoords.y },
|
|
543
|
+
comment: this.comment?.toJson?.(),
|
|
544
|
+
fields: [], // fill after placeholder
|
|
545
|
+
};
|
|
546
|
+
alreadyProcessed[this.id] = serialized;
|
|
547
|
+
|
|
548
|
+
// Now safely fill in the heavy parts
|
|
549
|
+
serialized.fields = this.allFields().map(fld =>
|
|
550
|
+
fld.toJson
|
|
551
|
+
? fld.toJson(true, alreadyProcessed)
|
|
552
|
+
: {
|
|
553
|
+
name: fld.getName(),
|
|
554
|
+
type: fld.constructor.name,
|
|
555
|
+
value: fld.getValue ? fld.getValue() : undefined,
|
|
556
|
+
}
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
serialized.previousConnection = this.previousConnection
|
|
560
|
+
? this._serializeConnection(this.previousConnection, alreadyProcessed)
|
|
561
|
+
: undefined;
|
|
562
|
+
|
|
563
|
+
serialized.nextConnection = this.nextConnection
|
|
564
|
+
? this._serializeConnection(this.nextConnection, alreadyProcessed)
|
|
565
|
+
: undefined;
|
|
566
|
+
|
|
567
|
+
return serialized;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Return a flattened version of the serialized node structure, which is non-circular.
|
|
572
|
+
* Any node reference inside connections or fields is replaced by its ID.
|
|
573
|
+
*/
|
|
574
|
+
toJson(): {
|
|
575
|
+
[id: string]: Omit<SerializedNode, 'previousConnection' | 'nextConnection'> & {
|
|
576
|
+
previousConnection?: { field?: FieldOptions; node?: string };
|
|
577
|
+
nextConnection?: { field?: FieldOptions; node?: string };
|
|
578
|
+
}
|
|
579
|
+
} {
|
|
580
|
+
const serialized = this.serialize();
|
|
581
|
+
const flat: { [id: string]: any } = {};
|
|
582
|
+
|
|
583
|
+
const processNode = (node: SerializedNode) => {
|
|
584
|
+
if (flat[node.id]) return;
|
|
585
|
+
|
|
586
|
+
const copy: any = {
|
|
587
|
+
...node,
|
|
588
|
+
previousConnection: node.previousConnection ? { ...node.previousConnection } : undefined,
|
|
589
|
+
nextConnection: node.nextConnection ? { ...node.nextConnection } : undefined,
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
flat[node.id] = copy;
|
|
593
|
+
|
|
594
|
+
// Handle connections
|
|
595
|
+
if (copy.previousConnection?.node) {
|
|
596
|
+
const prevNode = copy.previousConnection.node;
|
|
597
|
+
copy.previousConnection.node = prevNode.id;
|
|
598
|
+
processNode(prevNode);
|
|
599
|
+
}
|
|
600
|
+
if (copy.nextConnection?.node) {
|
|
601
|
+
const nextNode = copy.nextConnection.node;
|
|
602
|
+
copy.nextConnection.node = nextNode.id;
|
|
603
|
+
processNode(nextNode);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Handle fields recursively
|
|
607
|
+
if (Array.isArray(copy.fields)) {
|
|
608
|
+
for (let fld of copy.fields) {
|
|
609
|
+
if (fld.node) {
|
|
610
|
+
processNode(fld.node);
|
|
611
|
+
fld.node = fld.node.id;
|
|
612
|
+
}
|
|
613
|
+
for (let key in fld) {
|
|
614
|
+
if (fld[key]?.node) {
|
|
615
|
+
processNode(fld[key]?.node);
|
|
616
|
+
fld[key].node = fld[key].node.id;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
processNode(serialized);
|
|
624
|
+
return flat;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Reconstruct a NodeSvg from a SerializedNode structure (handles circular references)
|
|
629
|
+
*/
|
|
630
|
+
static _deserialize(
|
|
631
|
+
data: SerializedNode,
|
|
632
|
+
allNodes: { [id: string]: NodeSvg } = {},
|
|
633
|
+
workspace?: WorkspaceSvg
|
|
634
|
+
): NodeSvg {
|
|
635
|
+
// If already created, return the existing instance
|
|
636
|
+
if (allNodes[data.id]) return allNodes[data.id] as NodeSvg;
|
|
637
|
+
|
|
638
|
+
if (workspace && workspace.getNode(data.id)) {
|
|
639
|
+
workspace.removeNodeById(data.id); // remove old node which had the same id.
|
|
640
|
+
}
|
|
641
|
+
// Create a new node with minimal prototype info (can be patched later)
|
|
642
|
+
const node = new NodeSvg(NodePrototypes[data.type] as NodePrototype, workspace);
|
|
643
|
+
node.id = data.id;
|
|
644
|
+
node.init();
|
|
645
|
+
node.type = data.type;
|
|
646
|
+
node.relativeCoords.set(data.relativeCoords.x, data.relativeCoords.y);
|
|
647
|
+
node.labelText = data.label || '';
|
|
648
|
+
if (data.comment && workspace) {
|
|
649
|
+
node.comment = CommentModel.fromJson(data.comment as CommentSerialized);
|
|
650
|
+
node.comment._parent = node;
|
|
651
|
+
node.comment._isWorkspaceComment = false;
|
|
652
|
+
}
|
|
653
|
+
// IMPORTANT: restore colors from serialized data (if present)
|
|
654
|
+
if (data.colors) {
|
|
655
|
+
// Start with category colors if a category is present and known
|
|
656
|
+
if (data.colors.category && CategoryColors[data.colors.category]) {
|
|
657
|
+
const style: ColorStyle = CategoryColors[data.colors.category] as ColorStyle;
|
|
658
|
+
Object.assign(node.colors, style, { category: data.colors.category });
|
|
659
|
+
}
|
|
660
|
+
// Then override with explicit serialized colors (primary/secondary/tertiary)
|
|
661
|
+
// This preserves explicit color values even when a category was saved
|
|
662
|
+
const explicit: Partial<ColorStyle> = {};
|
|
663
|
+
if (data.colors.primary) explicit.primary = data.colors.primary;
|
|
664
|
+
if (data.colors.secondary) explicit.secondary = data.colors.secondary;
|
|
665
|
+
if (data.colors.tertiary) explicit.tertiary = data.colors.tertiary;
|
|
666
|
+
if (data.colors.category) explicit.category = data.colors.category;
|
|
667
|
+
node.setStyle(explicit as ColorStyle);
|
|
668
|
+
}
|
|
669
|
+
// Register placeholder before deserializing connections to handle circular refs
|
|
670
|
+
allNodes[node.id] = node;
|
|
671
|
+
|
|
672
|
+
// Deserialize fields
|
|
673
|
+
if (Array.isArray(data.fields)) {
|
|
674
|
+
node._fieldColumn = new Set();
|
|
675
|
+
for (let fldData of data.fields) {
|
|
676
|
+
const FieldConstructor = FieldMap[fldData.type];
|
|
677
|
+
if (!FieldConstructor) continue;
|
|
678
|
+
|
|
679
|
+
const fld: AnyField = new FieldConstructor();
|
|
680
|
+
fld.fromJson(fldData, workspace); // ONLY fldData and workspace
|
|
681
|
+
fld.node = node;
|
|
682
|
+
node._appendFieldItem(fld);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Deserialize previous/next connections
|
|
687
|
+
if (data.previousConnection?.node) {
|
|
688
|
+
node.previousConnection = new Connection(null, node, true);
|
|
689
|
+
node.previousConnection.setFrom(NodeSvg._deserialize(data.previousConnection.node, allNodes, workspace));
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (data.nextConnection?.node) {
|
|
693
|
+
node.nextConnection = new Connection(node, null, false);
|
|
694
|
+
node.nextConnection.setTo(NodeSvg._deserialize(data.nextConnection.node, allNodes, workspace));
|
|
695
|
+
} else {
|
|
696
|
+
workspace?.redraw(); // redraw if we reached the end.
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return node;
|
|
700
|
+
}
|
|
701
|
+
/** Public: Deserialize a SerializedNode or plain object into a NodeSvg attached to a workspace */
|
|
702
|
+
static deserialize(json: SerializedNode | any, workspace: WorkspaceSvg) {
|
|
703
|
+
return this._deserialize(json as SerializedNode, {}, workspace);
|
|
704
|
+
}
|
|
705
|
+
/** Reconstructs nodes from a flattened JSON structure into a NodeSvg tree */
|
|
706
|
+
static fromJson(flat: Record<string, any>, workspace: WorkspaceSvg): any {
|
|
707
|
+
const nodes: Record<string, any> = {};
|
|
708
|
+
// shallow clone so we can safely mutate
|
|
709
|
+
for (const id in flat) {
|
|
710
|
+
nodes[id] = { ...flat[id] };
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// rebuild references
|
|
714
|
+
for (const id in nodes) {
|
|
715
|
+
const node = nodes[id];
|
|
716
|
+
|
|
717
|
+
// fix connection refs
|
|
718
|
+
if (node.previousConnection?.node) {
|
|
719
|
+
const refId = node.previousConnection.node as string;
|
|
720
|
+
node.previousConnection = {
|
|
721
|
+
...node.previousConnection,
|
|
722
|
+
node: nodes[refId],
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
if (node.nextConnection?.node) {
|
|
726
|
+
const refId = node.nextConnection.node as string;
|
|
727
|
+
node.nextConnection = {
|
|
728
|
+
...node.nextConnection,
|
|
729
|
+
node: nodes[refId],
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// fix fields
|
|
734
|
+
if (Array.isArray(node.fields)) {
|
|
735
|
+
for (const fld of node.fields) {
|
|
736
|
+
if (typeof fld.node === 'string')
|
|
737
|
+
fld.node = nodes[fld.node];
|
|
738
|
+
|
|
739
|
+
for (const key in fld) {
|
|
740
|
+
const maybe = fld[key];
|
|
741
|
+
if (maybe?.node && typeof maybe.node === 'string')
|
|
742
|
+
maybe.node = nodes[maybe.node];
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// the root is just the one w/ no previousConnection
|
|
749
|
+
const root = Object.values(nodes).find(n => !n.previousConnection?.node) ?? null;
|
|
750
|
+
return this._deserialize(root, {}, workspace);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
export default NodeSvg;
|