import { NameTransitionCommand } from '../../core/commands/graphCommands'; import { currentNodePalette } from '../graph/NodePalette'; import { currentEqEditor } from '../soundfx/EqEditor'; import { rectOf } from '../textProbe'; import { centreOf, snapButton, type HookContext } from './hookHelpers'; /** The node graphs: behaviors, state machines, particles and shaders. */ export function graphHooks({ state, editor }: HookContext) { const { graphEditor } = editor; return { graph: { current: () => graphEditor.currentAssetId, portAt: (nodeId: string, port: string, isOutput: boolean) => graphEditor.canvas.portGlobal(nodeId, port, isOutput), nodeAt: (nodeId: string) => graphEditor.canvas.nodeGlobal(nodeId), chevronAt: (nodeId: string) => graphEditor.canvas.chevronGlobal(nodeId), nodeLabel: (nodeId: string) => graphEditor.canvas.nodeLabel(nodeId), nodeProblem: (nodeId: string) => graphEditor.canvas.nodeProblem(nodeId), nodeSettings: (nodeId: string) => graphEditor.canvas.nodeSettings(nodeId), /** Each input row as drawn: its name, and the inline value beside it while unconnected. */ nodeInputs: (nodeId: string) => graphEditor.canvas.nodeInputs(nodeId), /** Each output row as drawn: while playing, the value it last carried in front of its name. */ nodeOutputs: (nodeId: string) => graphEditor.canvas.nodeOutputs(nodeId), /** Each row's input and output text as cut and placed, in node units: two that overlap collide (#306). */ rowSpans: (nodeId: string) => graphEditor.canvas.nodeRowSpans(nodeId), /** Beads in flight on exec edges as drawn, in graph coordinates; `progress` 0 = source, 1 = target. */ beads: () => graphEditor.canvas.beadsDrawn(), /** Drawn rectangles on screen: what "fit" has to keep inside `canvasRect`. */ nodeRect: (nodeId: string) => graphEditor.canvas.nodeRect(nodeId), canvasRect: () => graphEditor.canvas.canvasRect(), /** Bare canvas below every node – where a step right-clicks to open the palette. */ emptySpot: () => graphEditor.canvas.emptySpotGlobal(), /** * What the open palette lists, in order. No colour check can see a ranking: the rows all * look alike, only their order carries the answer (#43). */ paletteRows: () => currentNodePalette()?.rowsDrawn() ?? null, selected: () => state.selectedNodeIds, /** Groups and comments as drawn – text and both colours included, since no pixel says them. */ annotations: () => graphEditor.canvas.annotationsDrawn(), /** Where one is grabbed: its title bar, or with `corner` the handle that resizes it. */ annotationAt: (id: string, corner = false) => graphEditor.canvas.annotationGlobal(id, corner), /** The box as drawn on screen – how a check sees whether a fit brought it into view. */ annotationRect: (id: string) => graphEditor.canvas.annotationRect(id), dropTarget: () => graphEditor.canvas.pendingDropTarget(), danglingEdge: () => graphEditor.canvas.danglingEdgeVisible(), edgeDrawn: (edgeId: string) => graphEditor.canvas.edgeDrawn(edgeId), /** The colour it was stroked in: `danger` is an edge whose two ends stopped fitting (#207). */ edgeColor: (edgeId: string) => graphEditor.canvas.edgeColor(edgeId), /** Halfway along a link on screen, and the links drawn as picked (#289). */ edgeAt: (edgeId: string) => graphEditor.canvas.edgeGlobal(edgeId), pickedEdges: () => graphEditor.canvas.pickedEdgesDrawn(), /** The types the node's ports are drawn with, after a shared group had its say (#207). */ portTypes: (nodeId: string) => graphEditor.canvas.portTypes(nodeId), blockFrames: () => graphEditor.canvas.blockFrames(), foldMark: (edgeId: string, isOutput: boolean) => graphEditor.canvas.foldMarkGlobal(edgeId, isOutput), foldMarks: () => graphEditor.canvas.foldMarksDrawn(), // The canvas, not the whole tab: a centre measured through the header sits above the // drawing area. center: () => centreOf(graphEditor.canvas), snapping: () => graphEditor.canvas.snapEnabled, snapButton: () => snapButton(graphEditor.snap), zoom: () => graphEditor.canvas.zoomLevel, /** Nodes left in the picture, and how often texts, boxes and links were really made (#292). */ nodesDrawn: () => graphEditor.canvas.nodesDrawn(), made: () => graphEditor.canvas.partsMade(), crumbs: () => graphEditor.breadcrumb.crumbsDrawn(), }, stateMachine: { current: () => editor.stateMachineEditor.currentAssetId, nodeAt: (nodeId: string) => editor.stateMachineEditor.canvas.nodeGlobal(nodeId), nodeLabel: (nodeId: string) => editor.stateMachineEditor.canvas.nodeLabel(nodeId), nodeRect: (nodeId: string) => editor.stateMachineEditor.canvas.nodeRect(nodeId), canvasRect: () => editor.stateMachineEditor.canvas.canvasRect(), center: () => centreOf(editor.stateMachineEditor.canvas), snapping: () => editor.stateMachineEditor.canvas.snapEnabled, snapButton: () => snapButton(editor.stateMachineEditor.snap), /** Where a state's hidden pin sits; it cannot be grabbed – `portGrabbable` is the check. */ portAt: (nodeId: string, port: string, isOutput: boolean) => editor.stateMachineEditor.canvas.portGlobal(nodeId, port, isOutput), portGrabbable: (nodeId: string, port: string, isOutput: boolean) => editor.stateMachineEditor.canvas.portGrabbable(nodeId, port, isOutput), edgeDrawn: (edgeId: string) => editor.stateMachineEditor.canvas.edgeDrawn(edgeId), edgeAt: (edgeId: string) => editor.stateMachineEditor.canvas.edgeGlobal(edgeId), pickedEdges: () => editor.stateMachineEditor.canvas.pickedEdgesDrawn(), transitionLabel: (edgeId: string) => editor.stateMachineEditor.canvas.transitionLabelDrawn(edgeId), transitionLabelAt: (edgeId: string) => editor.stateMachineEditor.canvas.transitionLabelAt(edgeId), hint: () => editor.stateMachineEditor.canvas.shownHint(), /** Names a transition the way the inspector row does, for steps that need one already named. */ nameTransition: (edgeId: string, name: string) => state.execute(new NameTransitionCommand(editor.stateMachineEditor.currentAssetId, edgeId, name)), dropTarget: () => editor.stateMachineEditor.canvas.pendingDropTarget(), danglingEdge: () => editor.stateMachineEditor.canvas.danglingEdgeVisible(), emptySpot: () => editor.stateMachineEditor.canvas.emptySpotGlobal(), selected: () => state.selectedNodeIds, /** The start ring and the live marks as drawn – what no colour check could tell apart. */ startRing: (nodeId: string) => editor.stateMachineEditor.canvas.startRingDrawn(nodeId), stateMarks: () => editor.stateMachineEditor.canvas.stateMarksDrawn(), }, soundfx: { current: () => editor.soundFxEditor.currentAssetId, info: () => editor.soundFxEditor.shownInfo, canvasRect: () => editor.soundFxEditor.canvas.canvasRect(), nodeProblem: (nodeId: string) => editor.soundFxEditor.canvas.nodeProblem(nodeId), nodeSettings: (nodeId: string) => editor.soundFxEditor.canvas.nodeSettings(nodeId), nodeAlpha: (nodeId: string) => editor.soundFxEditor.canvas.nodeAlpha(nodeId), /** The header's listening controls: which sound, and whether Play is offered. */ preview: () => ({ sound: editor.soundFxEditor.previewSoundId, picker: editor.soundFxEditor.soundPicker.text, play: editor.soundFxEditor.playButton.enabled, }), playAt: () => centreOf(editor.soundFxEditor.playButton), stopAt: () => centreOf(editor.soundFxEditor.stopButton), /** The header's parameter values, and setting one as its field would. */ params: () => editor.soundFxEditor.previewValues, setParam: (key: string, value: number) => editor.soundFxEditor.setPreviewValue(key, value), nodeAt: (nodeId: string) => editor.soundFxEditor.canvas.nodeGlobal(nodeId), }, /** The Parametric EQ's window (#367): for which node it is open, its bands and where to aim. */ eq: { shown: () => currentEqEditor()?.shownFor ?? null, bands: () => currentEqEditor()?.bands() ?? null, picked: () => currentEqEditor()?.picked ?? null, handleAt: (index: number) => currentEqEditor()?.curve.handleGlobal(index) ?? null, pointAt: (hz: number, db: number) => currentEqEditor()?.curve.pointGlobal(hz, db) ?? null, fields: () => { const eq = currentEqEditor(); return ( eq && { type: eq.typeButton.text, slope: eq.slopeButton.enabled ? eq.slopeButton.text : null, gain: eq.gainField.enabled ? eq.gainField.value : null, q: eq.qField.enabled ? eq.qField.value : null, frequency: eq.frequencyField.value, } ); }, typeAt: () => { const eq = currentEqEditor(); return eq ? centreOf(eq.typeButton) : null; }, closeAt: () => { const eq = currentEqEditor(); return eq ? centreOf(eq.closeButton) : null; }, }, instrument: { current: () => editor.instrumentEditor.currentAssetId, info: () => editor.instrumentEditor.shownInfo, nodeAt: (nodeId: string) => editor.instrumentEditor.canvas.nodeGlobal(nodeId), nodeLabel: (nodeId: string) => editor.instrumentEditor.canvas.nodeLabel(nodeId), nodeOutputs: (nodeId: string) => editor.instrumentEditor.canvas.nodeOutputs(nodeId), nodeProblem: (nodeId: string) => editor.instrumentEditor.canvas.nodeProblem(nodeId), portAt: (nodeId: string, port: string, isOutput: boolean) => editor.instrumentEditor.canvas.portGlobal(nodeId, port, isOutput), canvasRect: () => editor.instrumentEditor.canvas.canvasRect(), /** * The keys: which make a sound, which are held, which are let go but still heard, where one is, * and the computer keyboard's octave. */ keys: () => ({ sounding: editor.instrumentEditor.soundingNotes(), held: editor.instrumentEditor.keyboard.heldNotes, ringing: editor.instrumentEditor.keyboard.ringingNotes, octave: editor.instrumentEditor.keyboard.bottomOctave, }), keyAt: (note: number) => editor.instrumentEditor.keyboard.keyCentre(note), stopAt: () => centreOf(editor.instrumentEditor.stopButton), presetAt: () => centreOf(editor.instrumentEditor.presetButton), randomizeAt: () => centreOf(editor.instrumentEditor.randomizeButton), mutateAt: () => centreOf(editor.instrumentEditor.mutateButton), auditionAt: () => ({ ...centreOf(editor.instrumentEditor.auditionButton), toggled: editor.instrumentEditor.auditionButton.toggled, }), params: () => editor.instrumentEditor.previewValues, setParam: (key: string, value: number) => editor.instrumentEditor.setPreviewValue(key, value), /** Starts a note as a key does, held until stopped; gives its voice. */ play: (note: number) => editor.instrumentEditor.playNote(note), }, particles: { current: () => editor.particlesEditor.currentAssetId, info: () => editor.particlesEditor.shownInfo, snapButton: () => snapButton(editor.particlesEditor.snap), nodeAt: (nodeId: string) => editor.particlesEditor.canvas.nodeGlobal(nodeId), nodeLabel: (nodeId: string) => editor.particlesEditor.canvas.nodeLabel(nodeId), nodeRect: (nodeId: string) => editor.particlesEditor.canvas.nodeRect(nodeId), nodeProblem: (nodeId: string) => editor.particlesEditor.canvas.nodeProblem(nodeId), canvasRect: () => editor.particlesEditor.canvas.canvasRect(), emptySpot: () => editor.particlesEditor.canvas.emptySpotGlobal(), center: () => centreOf(editor.particlesEditor.canvas), /** The panel beside the graph: how many particles it draws and which second it stands at. */ preview: () => ({ count: editor.particlesEditor.preview.count, time: Math.round(editor.particlesEditor.preview.time * 100) / 100, holding: editor.particlesEditor.preview.holding, }), previewRect: () => rectOf(editor.particlesEditor.preview), /** Hold button, time readout and slider as they are shown, not as the preview holds them. */ controls: () => editor.particlesEditor.controls, splitterAt: () => centreOf(editor.particlesEditor.splitter), sliderAt: () => centreOf(editor.particlesEditor.slider), /** Jumps the preview to a second – exact, because a particle is a function of its age. */ hold: (at: number | null) => editor.particlesEditor.preview.hold(at), }, shader: { current: () => editor.shaderEditor.currentAssetId, info: () => editor.shaderEditor.shownInfo, nodeAt: (nodeId: string) => editor.shaderEditor.canvas.nodeGlobal(nodeId), nodeLabel: (nodeId: string) => editor.shaderEditor.canvas.nodeLabel(nodeId), nodeRect: (nodeId: string) => editor.shaderEditor.canvas.nodeRect(nodeId), nodeProblem: (nodeId: string) => editor.shaderEditor.canvas.nodeProblem(nodeId), nodeSettings: (nodeId: string) => editor.shaderEditor.canvas.nodeSettings(nodeId), portAt: (nodeId: string, port: string, isOutput: boolean) => editor.shaderEditor.canvas.portGlobal(nodeId, port, isOutput), /** What an arithmetic node's result is drawn as: the type that really comes out (#219). */ portTypes: (nodeId: string) => editor.shaderEditor.canvas.portTypes(nodeId), edgeColor: (edgeId: string) => editor.shaderEditor.canvas.edgeColor(edgeId), canvasRect: () => editor.shaderEditor.canvas.canvasRect(), emptySpot: () => editor.shaderEditor.canvas.emptySpotGlobal(), center: () => centreOf(editor.shaderEditor.canvas), snapping: () => editor.shaderEditor.canvas.snapEnabled, snapButton: () => snapButton(editor.shaderEditor.snap), /** The panel beside the graph (#168): what it shows, its clock and the sliders' values. */ preview: () => ({ ...editor.shaderEditor.controls, holding: editor.shaderEditor.preview.holding, seconds: Math.round(editor.shaderEditor.preview.time * 100) / 100, }), previewRect: () => rectOf(editor.shaderEditor.preview), /** Where the picture sits inside the panel, in screen pixels. */ pictureRect: () => editor.shaderEditor.preview.pictureRect, sliderAt: (key: string) => { const slider = editor.shaderEditor.sliderOf(key); return slider ? rectOf(slider) : null; }, splitterAt: () => centreOf(editor.shaderEditor.splitter), hold: (at: number | null) => editor.shaderEditor.hold(at), }, }; }