Compare commits

...

2 Commits

Author SHA1 Message Date
Jeff Emmett f1f9e3b34d Merge branch 'dev'
CI/CD / deploy (push) Successful in 2m5s Details
2026-04-15 15:03:32 -04:00
Jeff Emmett deff7369e5 feat(rflows): add loop toggle to liquidity flow simulation
Simulation now has a 🔁 button that auto-replays the flow visualization.
Detects steady state, pauses briefly, resets to initial snapshot, repeats.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 15:03:25 -04:00
1 changed files with 54 additions and 0 deletions

View File

@ -382,7 +382,10 @@ class FolkFlowRiver extends HTMLElement {
private shadow: ShadowRoot;
private nodes: FlowNode[] = [];
private simulating = false;
private looping = false;
private simTimer: ReturnType<typeof setInterval> | null = null;
private initialNodes: FlowNode[] | null = null;
private steadyCount = 0;
private dragging = false;
private dragStartX = 0;
private dragStartY = 0;
@ -430,14 +433,51 @@ class FolkFlowRiver extends HTMLElement {
private startSimulation() {
if (this.simTimer) return;
// Snapshot initial state for loop reset
if (!this.initialNodes) {
this.initialNodes = this.nodes.map(n => ({ ...n, data: { ...n.data } }));
}
this.steadyCount = 0;
this.simTimer = setInterval(() => {
const prev = this.nodes;
this.nodes = simulateTick(this.nodes, DEFAULT_CONFIG);
// Detect steady state: check if values stopped changing
let changed = false;
for (let i = 0; i < this.nodes.length; i++) {
const a = prev[i].data as Record<string, unknown>;
const b = this.nodes[i].data as Record<string, unknown>;
if (a.currentValue !== b.currentValue || a.fundingReceived !== b.fundingReceived) {
changed = true;
break;
}
}
if (!changed) this.steadyCount++;
else this.steadyCount = 0;
this.render();
// If looping and steady for 3 ticks, reset after a brief pause
if (this.looping && this.steadyCount >= 3 && this.initialNodes) {
this.stopSimulation();
setTimeout(() => {
if (!this.looping) return;
this.nodes = this.initialNodes!.map(n => ({ ...n, data: { ...n.data } }));
this.render();
setTimeout(() => {
if (!this.looping) return;
this.simulating = true;
this.startSimulation();
this.render();
}, 500);
}, 1500);
}
}, 500);
}
private stopSimulation() {
if (this.simTimer) { clearInterval(this.simTimer); this.simTimer = null; }
this.simulating = false;
}
private showAmountPopover(sourceId: string, anchorX: number, anchorY: number) {
@ -535,6 +575,7 @@ class FolkFlowRiver extends HTMLElement {
</svg>
<div class="controls">
<button class="${this.simulating ? "active" : ""}" data-action="toggle-sim">${this.simulating ? "Pause" : "Simulate"}</button>
<button class="${this.looping ? "active" : ""}" data-action="toggle-loop" title="Loop simulation">&#x1f501;</button>
</div>
<div class="legend">
<div class="legend-item"><div class="legend-dot" style="background:${COLORS.inflow}"></div> Inflow</div>
@ -551,6 +592,19 @@ class FolkFlowRiver extends HTMLElement {
this.render();
});
// Event: toggle loop
this.shadow.querySelector("[data-action=toggle-loop]")?.addEventListener("click", () => {
this.looping = !this.looping;
if (this.looping && !this.simulating) {
// Auto-start simulation when enabling loop
this.initialNodes = null; // fresh snapshot
this.simulating = true;
this.startSimulation();
}
if (!this.looping) this.initialNodes = null;
this.render();
});
// Event delegation for interactive elements + drag-to-pan
const container = this.shadow.querySelector(".container") as HTMLElement;
if (!container) return;