logic-mesh 0.9.0

Control logic engine using event based and reactive blocks written in Rust.
Documentation
<script lang="ts">
	import { Handle, Position } from '@xyflow/svelte';
	import { Plus, Minus } from 'lucide-svelte';
	import { Button } from '$lib/components/ui/button';
	import BlockCommons from '../BlockCommons.svelte';
	import { useEngine } from '$lib/Engine';
	import type { Block } from '$lib/Block';
	import { numericValue } from '$lib/utils';

	interface Props {
		data: { value: Block };
	}

	let { data }: Props = $props();
	const { command } = useEngine();

	const block = $derived(data.value);
	const inputKey = $derived(Object.keys(block.inputs)[0] ?? 'in');
	const outputKey = $derived(Object.keys(block.outputs)[0] ?? 'out');

	const numValue = $derived(numericValue(block.inputs.in.value) ?? 0);

	// SVG knob parameters
	const radius = 36;
	const cx = 44;
	const cy = 44;
	const minAngle = -225;
	const maxAngle = 45;
	const minVal = 0;
	const maxVal = 100;

	const angle = $derived(
		minAngle + ((numValue - minVal) / (maxVal - minVal)) * (maxAngle - minAngle)
	);

	function toXY(angleDeg: number, r: number) {
		const rad = ((angleDeg - 90) * Math.PI) / 180;
		return { x: cx + r * Math.cos(rad), y: cy + r * Math.sin(rad) };
	}

	const thumbPos = $derived(toXY(angle, radius - 4));
	const arcStart = $derived(toXY(minAngle, radius));
	const arcEnd = $derived(toXY(angle, radius));
	const largeArc = $derived(angle - minAngle > 180 ? 1 : 0);

	function updateOutput(newVal: number) {
		block.inputs.in.value = newVal;
		block.outputs.out.value = newVal;
		command.writeBlockOutput(block.id, outputKey, newVal);
	}

	function increment() {
		updateOutput(numValue + 1);
	}

	function decrement() {
		updateOutput(numValue - 1);
	}
</script>

<BlockCommons data={block}>
	<div class="ui-block-body">
		<Handle id={inputKey} type="target" position={Position.Left} class="handle-dot handle-input" />

		<div class="flex flex-col items-center gap-1">
			<svg width="88" height="88" viewBox="0 0 88 88">
				<circle cx={cx} cy={cy} r={radius} fill="none" stroke="var(--muted)" stroke-width="6" />
				<path
					d="M {arcStart.x} {arcStart.y} A {radius} {radius} 0 {largeArc} 1 {arcEnd.x} {arcEnd.y}"
					fill="none"
					stroke="var(--primary)"
					stroke-width="6"
					stroke-linecap="round"
				/>
				<circle cx={thumbPos.x} cy={thumbPos.y} r="4" fill="var(--primary)" />
				<text x={cx} y={cy + 5} text-anchor="middle" font-size="12" fill="var(--foreground)">
					{numValue}
				</text>
			</svg>

			<div class="flex gap-1">
				<Button variant="ghost" size="icon" class="h-6 w-6" onclick={decrement} aria-label="Decrement">
					<Minus class="h-3 w-3" />
				</Button>
				<Button variant="ghost" size="icon" class="h-6 w-6" onclick={increment} aria-label="Increment">
					<Plus class="h-3 w-3" />
				</Button>
			</div>
		</div>

		<Handle id={outputKey} type="source" position={Position.Right} class="handle-dot handle-output" />
	</div>
</BlockCommons>

<style>
	.ui-block-body {
		display: flex;
		align-items: center;
		padding: 6px 10px;
		position: relative;
	}

	:global(.handle-dot) {
		width: 8px !important;
		height: 8px !important;
		border-radius: 50% !important;
		min-width: 0 !important;
		border: 1.5px solid white !important;
	}
	:global(.handle-input) { background: #6b9eff !important; }
	:global(.handle-output) { background: #6bcf7f !important; }
</style>