Skip to content

Commit c0885e7

Browse files
committed
node messages, multiline text
1 parent 55a07b7 commit c0885e7

File tree

6 files changed

+148
-10
lines changed

6 files changed

+148
-10
lines changed

index.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,15 @@
296296
outputs: [
297297
{ name: "sum", type: "float32", description: "The sum of all connected nodes to our 'numbers' port" }
298298
],
299+
messages: [
300+
{
301+
message: "I am a message attatched to the node"
302+
},
303+
{
304+
message: "I am another message attatched to the node with type \"error\"",
305+
type: "error"
306+
}
307+
],
299308
});
300309

301310
graph.addNode(sumNode)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@elicdavis/node-flow",
33
"description": "build node graphs",
44
"author": "Eli C Davis",
5-
"version": "0.1.8",
5+
"version": "0.1.9",
66
"license": "MIT",
77
"keywords": [
88
"nodes",

src/graph.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { QuickMenu } from './quickMenu';
2121
export type GraphRenderer = (canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, position: Vector2, scale: number) => void;
2222

2323
function BuildBackgroundRenderer(backgroundColor: string): GraphRenderer {
24-
return (canvas: HTMLCanvasElement, context: CanvasRenderingContext2D, position: Vector2, scale: number) => {
24+
return (canvas: HTMLCanvasElement, context: CanvasRenderingContext2D, position: Vector2, scale: number): void => {
2525
context.fillStyle = backgroundColor;
2626
context.fillRect(0, 0, canvas.width, canvas.height);
2727

src/node.ts

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,39 @@ export interface FlowNodeStyle {
5656
portText?: TextStyleConfig;
5757
}
5858

59+
export enum MessageType {
60+
Info = "info",
61+
Warning = "warning",
62+
Error = "error"
63+
};
64+
65+
function MessageTypeColor(messageType: MessageType): string {
66+
switch (messageType) {
67+
case MessageType.Info:
68+
return Theme.Node.Message.InfoColor;
69+
70+
case MessageType.Warning:
71+
return Theme.Node.Message.WarnColor;
72+
73+
case MessageType.Error:
74+
return Theme.Node.Message.ErrorColor;
75+
76+
default:
77+
throw new Error(`unrecognized message type ${messageType}`)
78+
}
79+
}
80+
81+
export interface NodeMessageConfig {
82+
message: string;
83+
type?: MessageType;
84+
}
85+
5986
export interface FlowNodeConfig {
6087
position?: Vector2;
6188
title?: string;
6289
subTitle?: string;
6390
info?: string;
91+
messages?: Array<NodeMessageConfig>
6492
locked?: boolean;
6593
data?: Metadata;
6694
contextMenu?: ContextMenuConfig;
@@ -111,6 +139,29 @@ export interface NodeIntersection {
111139

112140
}
113141

142+
class MessageRenderer {
143+
144+
#text: Text;
145+
146+
constructor(config: NodeMessageConfig) {
147+
this.#text = new Text(config.message,
148+
{
149+
color: MessageTypeColor(config.type ? config.type : MessageType.Info),
150+
},
151+
{
152+
LineSpacing: 2.5,
153+
MaxWidth: 200
154+
}
155+
);
156+
}
157+
158+
render(ctx: CanvasRenderingContext2D, scale: number, position: Vector2): number {
159+
ctx.textAlign = TextAlign.Center;
160+
this.#text.render(ctx, scale, position);
161+
return this.#text.height(ctx) * scale;
162+
}
163+
}
164+
114165
export class FlowNode {
115166

116167
#position: Vector2;
@@ -123,6 +174,8 @@ export class FlowNode {
123174

124175
#infoText: string;
125176

177+
#messages: Array<MessageRenderer>
178+
126179
#input: Array<Port>;
127180

128181
#output: Array<Port>;
@@ -203,6 +256,14 @@ export class FlowNode {
203256
this.#contextMenu = config?.contextMenu === undefined ? null : config.contextMenu;
204257
this.#metadata = config?.metadata;
205258

259+
this.#messages = [];
260+
if (config?.messages) {
261+
for (let i = 0; i < config.messages.length; i++) {
262+
const message = config.messages[i];
263+
this.#messages.push(new MessageRenderer(message));
264+
}
265+
}
266+
206267
this.#selected = false;
207268
this.#onSelect = new Array<() => void>();
208269
this.#onUnselect = new Array<() => void>();
@@ -879,7 +940,6 @@ export class FlowNode {
879940
console.warn("setInfo instruction ignored, as node has been marked un-editable");
880941
}
881942

882-
883943
let cleaned = newInfo;
884944
if (cleaned === null || cleaned === undefined) {
885945
cleaned = "";
@@ -935,6 +995,14 @@ export class FlowNode {
935995
return boxStyle;
936996
}
937997

998+
addMessage(message: NodeMessageConfig): void {
999+
this.#messages.push(new MessageRenderer(message));
1000+
}
1001+
1002+
clearMessages(): void {
1003+
this.#messages = [];
1004+
}
1005+
9381006
render(ctx: CanvasRenderingContext2D, camera: Camera, state: NodeState, mousePosition: Vector2 | undefined, postProcess: PassSubsystem): void {
9391007
VectorPool.run(() => {
9401008
const tempMeasurement = VectorPool.get();
@@ -1113,6 +1181,7 @@ export class FlowNode {
11131181
startY += tempMeasurement.y + scaledElementSpacing;
11141182
}
11151183

1184+
// Widgets
11161185
for (let i = 0; i < this.#widgets.length; i++) {
11171186
const widget = this.#widgets[i];
11181187
const widgetSize = widget.Size();
@@ -1125,6 +1194,15 @@ export class FlowNode {
11251194
this.#widgetPositions.Push(widget.Draw(ctx, position, camera.zoom, mousePosition));
11261195
startY += (widgetSize.y * camera.zoom) + scaledElementSpacing;
11271196
}
1197+
1198+
// Messages
1199+
const messageStart = VectorPool.get();
1200+
messageStart.x = nodeBounds.Position.x + (nodeBounds.Size.x / 2);
1201+
messageStart.y = nodeBounds.Position.y + nodeBounds.Size.y + (15 * camera.zoom);
1202+
for (let i = 0; i < this.#messages.length; i++) {
1203+
const message = this.#messages[i];
1204+
messageStart.y += message.render(ctx, camera.zoom, messageStart) + (10 * camera.zoom);
1205+
}
11281206
})
11291207
}
11301208
}

src/theme.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ export const Theme = {
2020
},
2121
Port: {
2222
FontColor: "#afb9bb"
23+
},
24+
Message: {
25+
InfoColor: "#afb9bb",
26+
WarnColor: "#f1f663ff",
27+
ErrorColor: "#FF0000"
2328
}
2429
},
2530
BoxSelect: {

src/types/text.ts

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import { FontWeight, TextStyle, TextStyleConfig } from "../styles/text";
22
import { splitString, splitStringIntoLines } from "../utils/string";
33
import { CopyVector2, ScaleVector, Vector2, Zero } from "./vector2";
44

5+
export interface MultilineTextConfig {
6+
MaxWidth?: number;
7+
LineSpacing?: number;
8+
}
9+
510
export class Text {
611

712
#measured: boolean
@@ -12,11 +17,20 @@ export class Text {
1217

1318
#value: string
1419

15-
constructor(value: string, style?: TextStyleConfig) {
20+
#maxWidth: number;
21+
22+
#lineSpacing: number;
23+
24+
#textToRender: Array<string>;
25+
26+
constructor(value: string, style?: TextStyleConfig, config?: MultilineTextConfig) {
1627
this.#value = value;
1728
this.#measured = false;
1829
this.#size = Zero();
1930
this.#style = new TextStyle(style);
31+
this.#maxWidth = config?.MaxWidth ? config?.MaxWidth : -1;
32+
this.#lineSpacing = config?.LineSpacing ? config?.LineSpacing : 5;
33+
this.#textToRender = [value];
2034

2135
if (!document.fonts.check(`16px "${this.#style.getFont()}"`)) {
2236
document.fonts.addEventListener("loadingdone", (event) => {
@@ -96,10 +110,28 @@ export class Text {
96110
return;
97111
}
98112

113+
99114
this.#style.setupStyle(ctx, 1);
100-
const measurements = ctx.measureText(this.#value);
101-
this.#size.x = measurements.width;
102-
this.#size.y = measurements.actualBoundingBoxAscent + measurements.actualBoundingBoxDescent;
115+
116+
if (this.#maxWidth == -1) {
117+
const measurements = ctx.measureText(this.#value);
118+
this.#size.x = measurements.width;
119+
this.#size.y = measurements.actualBoundingBoxAscent + measurements.actualBoundingBoxDescent;
120+
this.#textToRender = [this.#value];
121+
this.#measured = true;
122+
return;
123+
}
124+
125+
this.#textToRender = splitStringIntoLines(ctx, this.#value, this.#maxWidth);
126+
127+
this.#size.x = 0;
128+
this.#size.y = 0;
129+
for (let i = 0; i < this.#textToRender.length; i++) {
130+
const measurements = ctx.measureText(this.#textToRender[i]);
131+
this.#size.x = Math.max(this.#size.x, measurements.width);
132+
}
133+
134+
this.#size.y += ((this.#textToRender.length - 1) * this.#lineSpacing) + (this.#style.getSize() * this.#textToRender.length);
103135
this.#measured = true;
104136
}
105137

@@ -109,10 +141,15 @@ export class Text {
109141
ScaleVector(out, scale);
110142
}
111143

144+
height(ctx: CanvasRenderingContext2D): number {
145+
this.#measure(ctx);
146+
return this.#size.y;
147+
}
148+
112149
setColor(color: string): void {
113150
this.#style.setColor(color);
114151
}
115-
152+
116153
getColor(): string {
117154
return this.#style.getColor();
118155
}
@@ -126,8 +163,17 @@ export class Text {
126163
}
127164

128165
render(ctx: CanvasRenderingContext2D, scale: number, position: Vector2): void {
129-
this.#style.setupStyle(ctx, scale)
130-
ctx.fillText(this.#value, position.x, position.y);
166+
this.#measure(ctx);
167+
this.#style.setupStyle(ctx, scale);
168+
let yOffset = 0;
169+
for (let i = 0; i < this.#textToRender.length; i++) {
170+
ctx.fillText(
171+
this.#textToRender[i],
172+
position.x,
173+
position.y + yOffset
174+
);
175+
yOffset += (this.#style.getSize() + this.#lineSpacing) * scale
176+
}
131177
}
132178

133179
style(): TextStyle {

0 commit comments

Comments
 (0)