Loading files
import {
useCallback,
useEffect,
useRef,
useState,
useSyncExternalStore,
} from 'react';
import {
getTopicSnapshot,
type PluginBridgeTopic,
subscribeTopic,
type ViewerPickEvent,
} from './bridge.js';
import { gsender } from './index.js';
// --- React hooks --------------------------------------------------------------
// These run in the plugin's OWN React. They mirror gSender's `useWorkspaceState`
// and `useTypedSelector` by subscribing to snapshots pushed across the bridge.
const useTopicSnapshot = <T>(topic: PluginBridgeTopic): T | undefined => {
const subscribe = useCallback(
(notify: () => void) => subscribeTopic(topic, notify),
[topic],
);
const getSnapshot = useCallback(() => getTopicSnapshot<T>(topic), [topic]);
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
};
/**
* Live gSender workspace state (units, machine profile, tools, etc.).
* Mirrors the host `useWorkspaceState` hook.
*/
export const useWorkspaceState = <T = unknown>(): T | undefined =>
useTopicSnapshot<T>('workspace');
/**
* The latest result from one of your parsers, re-rendering when it fires.
*
* Returns immediately with the cached last result if one exists, so a view that
* mounts long after the match still shows it. Use `machine.onParsed` instead if
* you need to observe EVERY match — this hook holds only the most recent one.
*/
export const useParsed = <T = unknown>(id: string): T | undefined => {
const byId = useTopicSnapshot<Record<string, T>>('parser');
return byId?.[id];
};
/**
* Select a slice of gSender's redux state, re-rendering when it changes.
* Mirrors the host `useTypedSelector` hook.
*
* @param selector Derives the value you care about from the root redux state.
* @param equalityFn Optional; skip re-renders when the selected value is "equal".
*/
export const useTypedSelector = <Selected, State = unknown>(
selector: (state: State) => Selected,
equalityFn?: (a: Selected, b: Selected) => boolean,
): Selected | undefined => {
const root = useTopicSnapshot<State>('redux');
const lastSelected = useRef<{ value: Selected } | null>(null);
if (root === undefined) {
return undefined;
}
const nextSelected = selector(root);
if (lastSelected.current) {
const prev = lastSelected.current.value;
const isSame = equalityFn
? equalityFn(prev, nextSelected)
: prev === nextSelected;
if (isSame) {
return prev;
}
}
lastSelected.current = { value: nextSelected };
return nextSelected;
};
/**
* Arm point-picking on the host visualizer for the lifetime of the component.
*
* Arms on mount (unless `opts.enabled === false`) and disarms on unmount or when
* disabled. The latest `handler` is kept in a ref, so re-renders that only change
* the handler don't re-arm the pick.
*
* @param mode `'click'` (single click) or `'hold'` (press-and-hold, which also
* emits `hold-progress` events).
* @param handler Called with each `ViewerPickEvent` while armed.
* @param opts.enabled Set `false` to keep the pick disarmed (default `true`).
* @returns `armed` (whether the host accepted the arm) and `error` (the host's
* rejection message, or `null`).
*/
export const useVisualizerPick = (
mode: 'click' | 'hold',
handler: (e: ViewerPickEvent) => void,
opts?: { enabled?: boolean },
): { armed: boolean; error: string | null } => {
const enabled = opts?.enabled !== false;
const [armed, setArmed] = useState(false);
const [error, setError] = useState<string | null>(null);
// Keep the latest handler without re-arming when it changes.
const handlerRef = useRef(handler);
handlerRef.current = handler;
useEffect(() => {
if (!enabled) {
return;
}
let cancelled = false;
let dispose: (() => void) | null = null;
gsender.viewer
.armPick(mode, (event) => handlerRef.current(event))
.then((disposer) => {
if (cancelled) {
// Unmounted/disabled before arming resolved — tear down now.
disposer();
return;
}
dispose = disposer;
setArmed(true);
setError(null);
})
.catch((err: unknown) => {
if (cancelled) {
return;
}
setArmed(false);
setError(err instanceof Error ? err.message : String(err));
});
return () => {
cancelled = true;
setArmed(false);
dispose?.();
};
}, [mode, enabled]);
return { armed, error };
};
bb6208f20This file is pinned to the selected release. Download the original to open it in your design tool.