diff --git a/src/globalState.ts b/src/globalState.ts index d164e32..c393d96 100644 --- a/src/globalState.ts +++ b/src/globalState.ts @@ -1,9 +1,11 @@ import { ref } from "vue"; import { defineStore } from "pinia"; +import { IGraphTemplateState } from "baklavajs"; import { BaklavaEvent } from "@baklavajs/events"; import { ITimelineState, useTimeline } from "@/timeline"; import { ILibraryState, useLibrary } from "./library"; +import { useGraphTemplateSync } from "./graph"; import { TICKS_PER_BEAT } from "./constants"; import { StageState, useStage } from "./stage"; @@ -26,6 +28,7 @@ export interface SavedState { resolution: number; snapUnits: number; bridgeUrl: string; + graphTemplates: IGraphTemplateState[]; } export const useGlobalState = defineStore("globalState", () => { @@ -43,6 +46,7 @@ export const useGlobalState = defineStore("globalState", () => { const stage = useStage(); const library = useLibrary(); const timeline = useTimeline(); + const graphTemplates = useGraphTemplateSync(); const events = { positionSetByUser: new BaklavaEvent(undefined), @@ -69,6 +73,7 @@ export const useGlobalState = defineStore("globalState", () => { stage: stage.save(), timeline: timeline.save(), library: library.save(), + graphTemplates: graphTemplates.save(), bpm: bpm.value, fps: fps.value, volume: volume.value, @@ -83,6 +88,7 @@ export const useGlobalState = defineStore("globalState", () => { async function load(raw: string) { const data: SavedState = JSON.parse(raw) as SavedState; stage.load(data.stage); + graphTemplates.load(data.graphTemplates ?? []); await library.load(data.library); timeline.load(data.timeline); bpm.value = data.bpm ?? defaults.bpm; @@ -110,6 +116,7 @@ export const useGlobalState = defineStore("globalState", () => { snapUnits, metronome, bridgeUrl, + graphTemplates, timeline, events, reset, diff --git a/src/graph/editor.ts b/src/graph/editor.ts index 9c6582d..5f6a35f 100644 --- a/src/graph/editor.ts +++ b/src/graph/editor.ts @@ -24,9 +24,6 @@ export class BaklavaEditor { this.editor.graphEvents.addConnection.subscribe(this, () => this.updateNodeInterfaceTypes()); this.editor.graphEvents.removeConnection.subscribe(this, () => this.updateNodeInterfaceTypes()); - // TODO: Remove after next Baklava upgrade - this.enginePlugin.calculateOrder(); - this.enginePlugin.events.afterRun.subscribe(this, (r) => { applyResult(r, this.editor); }); diff --git a/src/graph/graph.libraryItem.ts b/src/graph/graph.libraryItem.ts index 98f4df4..f577072 100644 --- a/src/graph/graph.libraryItem.ts +++ b/src/graph/graph.libraryItem.ts @@ -2,6 +2,7 @@ import { IEditorState } from "baklavajs"; import { LibraryItem, LibraryItemType } from "@/library"; import { BaklavaEditor } from "./editor"; import { KeyframeManager, InterfaceKeyframes } from "./keyframes/KeyframeManager"; +import { useGlobalState } from "@/globalState"; export interface GraphLibraryItemState { graph: IEditorState; @@ -15,6 +16,11 @@ export class GraphLibraryItem extends LibraryItem { public editor = new BaklavaEditor(); public keyframeManager = new KeyframeManager(this); + public constructor() { + super(); + useGlobalState().graphTemplates.registerTarget(this.editor.editor); + } + public override save() { return { graph: this.editor.editor.save(), @@ -23,7 +29,15 @@ export class GraphLibraryItem extends LibraryItem { } public override load(state: GraphLibraryItemState) { - this.editor.editor.load(state.graph); + const warnings = this.editor.editor.load(state.graph); + if (warnings.length > 0) { + console.warn("Warnings while loading graph:", warnings); + } this.keyframeManager.keyframes = new Map(Object.entries(state.keyframes)); } + + public override destroy(): Promise { + useGlobalState().graphTemplates.unregisterTarget(this.editor.editor); + return Promise.resolve(); + } } diff --git a/src/graph/graphTemplateSync.ts b/src/graph/graphTemplateSync.ts new file mode 100644 index 0000000..4032cd7 --- /dev/null +++ b/src/graph/graphTemplateSync.ts @@ -0,0 +1,95 @@ +import { Editor, GraphTemplate, IGraphTemplateState } from "baklavajs"; + +export function useGraphTemplateSync() { + const token = Symbol("useGraphTemplateSync"); + const targets: Editor[] = []; + const templates = new Map(); + let updating = false; + + function registerTarget(target: Editor) { + targets.push(target); + target.events.addGraphTemplate.subscribe(token, (template) => { + updateTemplate(template.save()); + }); + target.events.removeGraphTemplate.subscribe(token, (template) => { + removeTemplate(template.id); + }); + target.graphTemplateEvents.updated.subscribe(token, (_, template) => { + updateTemplate(template.save()); + }); + target.graphTemplateEvents.nameChanged.subscribe(token, (_, template) => { + updateTemplate(template.save()); + }); + for (const template of templates.values()) { + if (target.graphTemplates.find((t) => t.id === template.id)) { + continue; + } + + const newTemplate = new GraphTemplate(template, target); + newTemplate.id = template.id; + target.addGraphTemplate(newTemplate); + } + } + + function unregisterTarget(target: Editor) { + const index = targets.indexOf(target); + if (index !== -1) { + targets.splice(index, 1); + + const target = targets[index]; + target.events.addGraphTemplate.unsubscribe(token); + target.events.removeGraphTemplate.unsubscribe(token); + target.graphTemplateEvents.updated.unsubscribe(token); + target.graphTemplateEvents.nameChanged.unsubscribe(token); + } + } + + function save() { + return Array.from(templates.values()); + } + + function load(state: IGraphTemplateState[]) { + for (const template of state) { + updateTemplate(template); + } + } + + function updateTemplate(template: IGraphTemplateState) { + if (updating) { + return; + } + + updating = true; + templates.set(template.id, template); + for (const target of targets) { + const targetTemplate = target.graphTemplates.find((t) => t.id === template.id); + if (targetTemplate) { + targetTemplate.update(template); + targetTemplate.name = template.name; + } else { + const newTemplate = new GraphTemplate(template, target); + newTemplate.id = template.id; + target.addGraphTemplate(newTemplate); + } + } + updating = false; + } + + function removeTemplate(id: string) { + if (updating) { + return; + } + + updating = true; + templates.delete(id); + for (const target of targets) { + const targetTemplate = target.graphTemplates.find((t) => t.id === id); + if (targetTemplate) { + target.removeGraphTemplate(targetTemplate); + } + } + updating = false; + } + + return { registerTarget, unregisterTarget, save, load }; +} diff --git a/src/graph/index.ts b/src/graph/index.ts index d586499..186e7e4 100644 --- a/src/graph/index.ts +++ b/src/graph/index.ts @@ -1,4 +1,5 @@ export * from "./editor"; export * from "./graph.libraryItem"; export * from "./types"; +export * from "./graphTemplateSync"; export { default as GraphEditor } from "./GraphEditor.vue"; diff --git a/src/timeline/timelineProcessor.ts b/src/timeline/timelineProcessor.ts index 3d92915..2902d8c 100644 --- a/src/timeline/timelineProcessor.ts +++ b/src/timeline/timelineProcessor.ts @@ -12,6 +12,7 @@ import { INote, PatternLibraryItem } from "@/pattern"; import { ICalculationData } from "@/graph"; import { useGlobalState } from "@/globalState"; import { useStage } from "@/stage/stage"; +import { BaseFixture } from "@/stage"; export class TimelineProcessor { public trackValues = new Map(); // maps trackId -> value @@ -91,7 +92,7 @@ export class TimelineProcessor { const audioData = this.audioProcessor!.getAudioData(); - const uncontrolledFixtures = new Set(this.stage.fixtures.values()); + const uncontrolledFixtures = new Set(this.stage.fixtures.values()) as Set; const calculationData: ICalculationData = { resolution: this.globalState.resolution, fps: this.globalState.fps, @@ -105,21 +106,10 @@ export class TimelineProcessor { for (const g of graphs) { try { const results = await this.processGraph(g, unit, calculationData); - if (g.libraryItem.error) { g.libraryItem.error = ""; } - - results.forEach((intfValues) => { - if (!intfValues.has("fixtureId")) { - return; - } - const fixture = this.stage.fixtures.get(intfValues.get("fixtureId")); - if (fixture) { - uncontrolledFixtures.delete(fixture); - fixture.setValue(intfValues.get("data")); - } - }); + this.applyGraphResults(results, uncontrolledFixtures); } catch (err) { console.error(err); g.libraryItem.error = String(err); @@ -192,4 +182,18 @@ export class TimelineProcessor { const notes = np.getNotesAt(unit - item.start); this.trackValues.set(item.trackId, notes); } + + private applyGraphResults(results: CalculationResult, uncontrolledFixtures: Set): void { + for (const nodeResults of results.values()) { + if (nodeResults.has("_calculationResults")) { + this.applyGraphResults(nodeResults.get("_calculationResults") as CalculationResult, uncontrolledFixtures); + } else if (nodeResults.has("fixtureId")) { + const fixture = this.stage.fixtures.get(nodeResults.get("fixtureId")); + if (fixture) { + uncontrolledFixtures.delete(fixture as BaseFixture); + fixture.setValue(nodeResults.get("data")); + } + } + } + } }