From a74edf43a71d66533c64cf66281c4ea04418400d Mon Sep 17 00:00:00 2001 From: Eli Davis Date: Sat, 11 Jan 2025 23:00:42 -0600 Subject: [PATCH] Bunch of note utils --- README.md | 36 ++++++++------- index.html | 51 ++++++++++++--------- src/graph.ts | 21 +++++++-- src/node.ts | 32 +++++++++++-- src/nodes/subsystem.ts | 5 ++ src/notes/note.ts | 76 +++++++++++++++++++++++++++++-- src/notes/subsystem.ts | 101 +++++++++++++++++++++++++++++++++++++++-- 7 files changed, 266 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index acc0e04..355ce5b 100644 --- a/README.md +++ b/README.md @@ -81,23 +81,25 @@ var graph = new NodeFlowGraph(canvas, { }, // Notes we want rendered on the graph. - notes: [ - { - // Where to render the note - position: { x: 20, y: 20 }, - - // Whether or not the note can be - // interacted with on the graph - locked: true, - - // Markdown enabled text - text: ` - # My First note!!! - - Not sure what to write here - ` - }, - ], + board: { + notes: [ + { + // Where to render the note + position: { x: 20, y: 20 }, + + // Whether or not the note can be + // interacted with on the graph + locked: true, + + // Markdown enabled text + text: ` + # My First note!!! + + Not sure what to write here + ` + }, + ] + }, }); ``` diff --git a/index.html b/index.html index 26f1fec..65b760d 100644 --- a/index.html +++ b/index.html @@ -124,9 +124,10 @@ }, }, }, - notes: [ - { - text: ` + board: { + notes: [ + { + text: ` # Notes This is an example of a *note* written in **markdown**. @@ -146,19 +147,19 @@ * Unordered Lists * Code blocks `, - position: { - x: 2300, - y: 20 - }, - locked: true, - }, - { - position: { - x: 20, - y: 20 + position: { + x: 2300, + y: 20 + }, + locked: true, }, - locked: true, - text: ` + { + position: { + x: 20, + y: 20 + }, + locked: true, + text: ` # Node Flow Node Flow is a javascript library that enables developers to build node based tools similar to Unreal Blueprints or Blender Nodes. @@ -175,9 +176,10 @@ var graph = new NodeFlowGraph(canvas) \`\`\` ` - }, + }, - ] + ] + } }); graph.addNote(new FlowNote({ @@ -268,10 +270,7 @@ ] }) - graph.addNode(sumNode) - graph.addNode(aNode) - graph.addNode(bNode) - graph.addNode(new FlowNode({ + const arrNode = new FlowNode({ position: { x: 1050, y: 600 @@ -283,11 +282,19 @@ outputs: [ { name: "sum", type: "float32" } ], - })) + }); + + graph.addNode(sumNode) + graph.addNode(aNode) + graph.addNode(bNode) + graph.addNode(arrNode) graph.connectNodes(aNode, 0, sumNode, 0) graph.connectNodes(bNode, 0, sumNode, 1) + graph.connectNodes(aNode, 0, arrNode, 0) + graph.connectNodes(bNode, 0, arrNode, 0) + diff --git a/src/graph.ts b/src/graph.ts index 3f26ae8..9862690 100644 --- a/src/graph.ts +++ b/src/graph.ts @@ -8,8 +8,8 @@ import { CursorStyle } from "./styles/cursor"; import { CopyVector2, Vector2, Zero } from './types/vector2'; import { Clamp01 } from "./utils/math"; import { GraphSubsystem, RenderResults } from './graphSubsystem'; -import { FlowNote, FlowNoteConfig } from "./notes/note"; -import { NoteSubsystem } from "./notes/subsystem"; +import { FlowNote } from "./notes/note"; +import { NoteAddedCallback, NoteDragStartCallback, NoteDragStopCallback, NoteSubsystem, NoteSubsystemConfig } from "./notes/subsystem"; import { ConnectionRendererConfiguration, NodeAddedCallback, NodeSubsystem } from "./nodes/subsystem"; import { Connection } from './connection'; import { Publisher } from './nodes/publisher'; @@ -52,7 +52,7 @@ export interface FlowNodeGraphConfiguration { idleConnection?: ConnectionRendererConfiguration contextMenu?: ContextMenuConfig nodes?: NodeFactoryConfig - notes?: Array + board?: NoteSubsystemConfig } interface OpenContextMenu { @@ -165,7 +165,7 @@ export class NodeFlowGraph { idleConnection: config?.idleConnection }); - this.#mainNoteSubsystem = new NoteSubsystem(config?.notes); + this.#mainNoteSubsystem = new NoteSubsystem(config?.board); this.#views = [ new GraphView([ @@ -255,6 +255,19 @@ export class NodeFlowGraph { })); } + public addNoteAddedListener(callback: NoteAddedCallback): void { + this.#mainNoteSubsystem.addNoteAddedListener(callback); + } + + public addNoteDragStartListener(callback: NoteDragStartCallback): void { + this.#mainNoteSubsystem.addNoteDragStartListener(callback); + } + + public addNoteDragStopListener(callback: NoteDragStopCallback): void { + this.#mainNoteSubsystem.addNoteDragStopListener(callback); + } + + zoom(amount: number): void { let oldPos: Vector2 | undefined = undefined; diff --git a/src/node.ts b/src/node.ts index 22ec697..504bf19 100644 --- a/src/node.ts +++ b/src/node.ts @@ -71,6 +71,7 @@ export interface FlowNodeConfig { onRelease?: () => void; onSelect?: () => void; onUnselect?: () => void; + onDragStop?: (FlowNode) => void; onFileDrop?: (file: File) => void; // Widgets @@ -128,6 +129,8 @@ export class FlowNode { #onFiledrop: Array<(file: File) => void>; + #onDragStop: Array<(node: FlowNode) => void>; + // Styling ================================================================ #titleColor: string; @@ -175,9 +178,10 @@ export class FlowNode { this.#metadata = config?.metadata; this.#selected = false; - this.#onSelect = new Array<() => void>; - this.#onUnselect = new Array<() => void>; - this.#onFiledrop = new Array<(file: File) => void>; + this.#onSelect = new Array<() => void>(); + this.#onUnselect = new Array<() => void>(); + this.#onFiledrop = new Array<(file: File) => void>(); + this.#onDragStop = new Array<() => void>(); if (config?.onSelect) { this.#onSelect.push(config?.onSelect); @@ -191,6 +195,10 @@ export class FlowNode { this.#onFiledrop.push(config?.onFileDrop); } + if (config?.onDragStop) { + this.#onDragStop.push(config.onDragStop); + } + this.#position = config?.position === undefined ? { x: 0, y: 0 } : config.position; this.#title = new Text( config?.title === undefined ? "" : config.title, @@ -275,6 +283,18 @@ export class FlowNode { } } + public raiseDragStoppedEvent() { + for (let i = 0; i < this.#onDragStop.length; i++) { + this.#onDragStop[i](this); + } + } + + public addDragStoppedListener(callback: (node: FlowNode) => void): void { + if (callback === undefined || callback === null) { + } + this.#onDragStop.push(callback); + } + public addAnyPropertyChangeListener(callback: AnyPropertyChangeCallback): void { if (callback === undefined || callback === null) { } @@ -587,6 +607,10 @@ export class FlowNode { CopyVector2(this.#position, position); } + public getPosition(): Vector2 { + return this.#position; + } + // #measureTitleText(ctx: CanvasRenderingContext2D, scale: number): Vector2 { // return this.#titleTextStyle.measure(ctx, scale, this.#title); // } @@ -643,7 +667,7 @@ export class FlowNode { } addInput(config: PortConfig): Port { - const port = new Port(this, config.array? PortType.InputArray : PortType.Input, config); + const port = new Port(this, config.array ? PortType.InputArray : PortType.Input, config); this.#input.push(port); return port; } diff --git a/src/nodes/subsystem.ts b/src/nodes/subsystem.ts index ef4be08..ea0a602 100644 --- a/src/nodes/subsystem.ts +++ b/src/nodes/subsystem.ts @@ -231,6 +231,11 @@ export class NodeSubsystem { } clickEnd(): void { + + for(let i = 0; i < this.#nodesGrabbed.Count(); i ++) { + const node = this.#nodes[this.#nodesGrabbed.At(i)]; + node.raiseDragStoppedEvent(); + } this.#nodesGrabbed.Clear(); if (this.#boxSelect) { diff --git a/src/notes/note.ts b/src/notes/note.ts index 06b1a88..95ea3e6 100644 --- a/src/notes/note.ts +++ b/src/notes/note.ts @@ -10,12 +10,17 @@ import { Box } from "../types/box"; import { CopyVector2, Vector2, Zero } from "../types/vector2"; import { Camera } from "../camera"; +export type NoteContentChangeCallback = (node: FlowNote, newContents: string) => void +export type NoteWidthChangeCallback = (node: FlowNote, newWidth: number) => void + export interface FlowNoteConfig { text?: string; style?: TextStyleConfig; position?: Vector2; width?: number; locked?: boolean; + onWidthChange?: NoteWidthChangeCallback; + onContentChange?: NoteContentChangeCallback; } export enum DragHandle { @@ -49,9 +54,19 @@ export class FlowNote { #hovering: boolean; + // Callbacks ============================================================== + + #widthChangeCallbacks: Array; + + #contentChangeCallbacks: Array; + // ======================================================================== constructor(config?: FlowNoteConfig) { + this.#widthChangeCallbacks = new Array(); + this.#contentChangeCallbacks = new Array(); + + this.#hovering = false; this.#edittingLayout = config?.locked === undefined ? true : !config?.locked; this.#width = config?.width === undefined ? 500 : config.width; @@ -66,11 +81,34 @@ export class FlowNote { size: 1 }, }) + + if (config?.onWidthChange) { + this.#widthChangeCallbacks.push(config.onWidthChange); + } + + if (config?.onContentChange) { + this.#contentChangeCallbacks.push(config.onContentChange); + } } setText(text: string): void { this.#originalText = text; this.#document = BuildMarkdown(this.#originalText); + for (let i = 0; i < this.#contentChangeCallbacks.length; i++) { + this.#contentChangeCallbacks[i](this, text); + } + } + + text(): string { + return this.#originalText; + } + + width(): number { + return this.#width; + } + + position(): Vector2 { + return this.#position; } translate(delta: Vector2): void { @@ -78,6 +116,10 @@ export class FlowNote { this.#position.y += delta.y; } + setPosition(position: Vector2): void { + CopyVector2(this.#position, position); + } + handleSelected(): DragHandle { return this.#handleSelected; } @@ -88,6 +130,30 @@ export class FlowNote { #tempPosition: Vector2 = Zero(); + public setWidth(newWidth: number): void { + if (newWidth === undefined) { + console.error("Attempted to set note's width to undefined"); + } + this.#width = newWidth; + for (let i = 0; i < this.#widthChangeCallbacks.length; i++) { + this.#widthChangeCallbacks[i](this, newWidth); + } + } + + public addWidthChangeListener(callback: NoteWidthChangeCallback): void { + if (callback === null || callback === undefined) { + return; + } + this.#widthChangeCallbacks.push(callback); + } + + public addContentChangeListener(callback: NoteContentChangeCallback): void { + if (callback === null || callback === undefined) { + return; + } + this.#contentChangeCallbacks.push(callback); + } + render(ctx: CanvasRenderingContext2D, camera: Camera, mousePosition: Vector2 | undefined): void { if (this.#edittingLayout && (this.#hovering || this.#handleSelected !== DragHandle.None)) { @@ -95,15 +161,15 @@ export class FlowNote { if (this.#handleSelected === DragHandle.Right) { const leftPosition = (this.#position.x * camera.zoom) + camera.position.x; - this.#width = Math.max((mousePosition.x - leftPosition) / camera.zoom, 1) + this.setWidth(Math.max((mousePosition.x - leftPosition) / camera.zoom, 1)); } else if (this.#handleSelected === DragHandle.Left) { const scaledWidth = this.#width * camera.zoom; const rightPosition = (this.#position.x * camera.zoom) + camera.position.x + scaledWidth; - this.#width = Math.max((rightPosition - mousePosition.x) / camera.zoom, 1) + this.setWidth(Math.max((rightPosition - mousePosition.x) / camera.zoom, 1)); this.#position.x = rightPosition - (this.#width * camera.zoom) - camera.position.x; - this.#position.x /= camera.zoom; + this.#position.x /= camera.zoom; } } @@ -127,10 +193,10 @@ export class FlowNote { ctx.stroke(); // this.#edittingStyle.Outline(ctx, bigBox, camera.zoom, 2); } - + camera.graphSpaceToScreenSpace(this.#position, this.#tempPosition); CopyVector2(this.#lastRenderedBox.Position, this.#tempPosition) - + const startY = this.#tempPosition.y; const lineSpacing = Theme.Note.EntrySpacing * camera.zoom; diff --git a/src/notes/subsystem.ts b/src/notes/subsystem.ts index 9c88f25..7c9554c 100644 --- a/src/notes/subsystem.ts +++ b/src/notes/subsystem.ts @@ -6,6 +6,21 @@ import { Vector2 } from "../types/vector2"; import { DragHandle, FlowNote, FlowNoteConfig } from './note'; +export type NoteAddedCallback = (addedNote: FlowNote) => void +export type NoteRemovedCallback = (noteRemoved: FlowNote) => void + +export type NoteDragStartCallback = (nodeDragged: FlowNote) => void +export type NoteDragStopCallback = (nodeDragged: FlowNote) => void + + +export interface NoteSubsystemConfig { + onNoteAdded?: NoteAddedCallback; + onNoteRemoved?: NoteRemovedCallback; + onNoteDragStop?: NoteDragStopCallback + onNoteDragStart?: NoteDragStartCallback + notes?: Array +} + export class NoteSubsystem { #notes: Array; @@ -16,21 +31,87 @@ export class NoteSubsystem { #hoveringHandle: DragHandle; - constructor(notes?: Array) { + #onNoteAddedCallbacks: Array; + + #onNoteRemovedCallbacks: Array; + + #onNoteDragStartCallbacks: Array; + + #onNoteDragStopCallbacks: Array; + + constructor(config?: NoteSubsystemConfig) { this.#hoveringHandle = DragHandle.None; this.#notes = []; this.#noteHovering = null; this.#noteSelected = null; - if (notes !== undefined) { - for (let i = 0; i < notes.length; i++) { - this.addNote(new FlowNote(notes[i])); + // Callbacks + this.#onNoteAddedCallbacks = new Array(); + this.#onNoteRemovedCallbacks = new Array(); + this.#onNoteDragStartCallbacks = new Array(); + this.#onNoteDragStopCallbacks = new Array(); + + if (config?.notes !== undefined) { + for (let i = 0; i < config?.notes.length; i++) { + this.addNote(new FlowNote(config?.notes[i])); } } + + // Add callback *after* we added all the initial notes + if (config?.onNoteAdded !== undefined) { + this.#onNoteAddedCallbacks.push(config?.onNoteAdded); + } + + if (config?.onNoteDragStop !== undefined) { + this.#onNoteDragStopCallbacks.push(config?.onNoteDragStop); + } + + if (config?.onNoteDragStart !== undefined) { + this.#onNoteDragStartCallbacks.push(config?.onNoteDragStart); + } + + if (config?.onNoteRemoved !== undefined) { + this.#onNoteRemovedCallbacks.push(config?.onNoteRemoved); + } } addNote(note: FlowNote): void { + if (note === null || note === undefined) { + return; + } + this.#notes.push(note); + for (let i = 0; i < this.#onNoteAddedCallbacks.length; i++) { + this.#onNoteAddedCallbacks[i](note); + } + } + + public addNoteAddedListener(callback: NoteAddedCallback): void { + if (callback === null || callback === undefined) { + return; + } + this.#onNoteAddedCallbacks.push(callback); + } + + public addNoteRemovedListener(callback: NoteRemovedCallback): void { + if (callback === null || callback === undefined) { + return; + } + this.#onNoteRemovedCallbacks.push(callback); + } + + public addNoteDragStartListener(callback: NoteDragStartCallback): void { + if (callback === null || callback === undefined) { + return; + } + this.#onNoteDragStartCallbacks.push(callback); + } + + public addNoteDragStopListener(callback: NoteDragStopCallback): void { + if (callback === null || callback === undefined) { + return; + } + this.#onNoteDragStopCallbacks.push(callback); } openContextMenu(ctx: CanvasRenderingContext2D, position: Vector2): ContextMenuConfig | null { @@ -92,6 +173,10 @@ export class NoteSubsystem { if (this.#noteHovering !== null && this.#noteHovering.edittingLayout()) { this.#noteSelected = this.#noteHovering; this.#noteSelected.selectHandle(this.#hoveringHandle); + + for (let i = 0; i < this.#onNoteDragStartCallbacks.length; i++) { + this.#onNoteDragStartCallbacks[i](this.#noteSelected); + } return true; } return false; @@ -100,6 +185,9 @@ export class NoteSubsystem { clickEnd(): void { if (this.#noteSelected !== null) { this.#noteSelected.selectHandle(DragHandle.None); + for (let i = 0; i < this.#onNoteDragStopCallbacks.length; i++) { + this.#onNoteDragStopCallbacks[i](this.#noteSelected); + } } this.#noteSelected = null; } @@ -128,7 +216,12 @@ export class NoteSubsystem { #removeNote(note: FlowNote): void { const index = this.#notes.indexOf(note); if (index > -1) { + const noteRemoved = this.#notes[index]; this.#notes.splice(index, 1); + + for (let i = 0; i < this.#onNoteRemovedCallbacks.length; i++) { + this.#onNoteRemovedCallbacks[i](noteRemoved) + } } else { console.error("no note found to remove"); }