feat(control): spawn session NPCs and resize tokens in preview

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-08-18 14:42:06 +08:00
parent 1ab6ffd593
commit 1a9f0f2557
28 changed files with 1446 additions and 89 deletions
+81
View File
@@ -314,6 +314,28 @@ function emitSceneNpcTokensSessionState(): void {
}
}
let npcMapSpawnDragNpcId: string | null = null;
let npcMapSpawnHover: { nx: number; ny: number } | null = null;
function emitNpcMapSpawnDragState(): void {
const state = {
dragging: Boolean(npcMapSpawnDragNpcId),
npcId: npcMapSpawnDragNpcId,
hoverNx: npcMapSpawnHover?.nx ?? null,
hoverNy: npcMapSpawnHover?.ny ?? null,
};
for (const win of BrowserWindow.getAllWindows()) {
win.webContents.send(ipcChannels.npcMapSpawn.stateChanged, { state });
}
}
function clearNpcMapSpawnDrag(): void {
if (!npcMapSpawnDragNpcId && !npcMapSpawnHover) return;
npcMapSpawnDragNpcId = null;
npcMapSpawnHover = null;
emitNpcMapSpawnDragState();
}
function emitScenePlayerTokensSessionState(): void {
const state = scenePlayerTokensSessionStore.getState();
for (const win of BrowserWindow.getAllWindows()) {
@@ -501,6 +523,7 @@ async function main() {
sceneTokensSessionStore.reset();
tokenPathSessionStore.reset();
sceneNpcTokensSessionStore.reset();
clearNpcMapSpawnDrag();
scenePlayerTokensSessionStore.reset();
tokenGridSnapSessionStore.reset();
if (USERS_BRANCH_FEATURES_ENABLED) {
@@ -646,6 +669,7 @@ async function main() {
sceneViewStore.reset();
sceneTokensSessionStore.reset();
sceneNpcTokensSessionStore.reset();
clearNpcMapSpawnDrag();
scenePlayerTokensSessionStore.reset();
emitSceneViewState();
emitSceneTokensSessionState();
@@ -666,6 +690,7 @@ async function main() {
sceneViewStore.reset();
sceneTokensSessionStore.reset();
sceneNpcTokensSessionStore.reset();
clearNpcMapSpawnDrag();
scenePlayerTokensSessionStore.reset();
tokenGridSnapSessionStore.reset();
emitEffectsState();
@@ -1611,6 +1636,62 @@ async function main() {
emitSceneNpcTokensSessionState();
return { ok: true };
});
registerHandler(ipcChannels.npcMapSpawn.getState, () => {
return {
state: {
dragging: Boolean(npcMapSpawnDragNpcId),
npcId: npcMapSpawnDragNpcId,
hoverNx: npcMapSpawnHover?.nx ?? null,
hoverNy: npcMapSpawnHover?.ny ?? null,
},
};
});
registerHandler(ipcChannels.npcMapSpawn.beginDrag, ({ npcId }) => {
npcMapSpawnDragNpcId = String(npcId ?? '') || null;
npcMapSpawnHover = null;
emitNpcMapSpawnDragState();
return { ok: true };
});
registerHandler(ipcChannels.npcMapSpawn.setHover, ({ nx, ny }) => {
if (!npcMapSpawnDragNpcId) return { ok: true };
if (nx == null || ny == null || !Number.isFinite(nx) || !Number.isFinite(ny)) {
if (npcMapSpawnHover === null) return { ok: true };
npcMapSpawnHover = null;
emitNpcMapSpawnDragState();
return { ok: true };
}
const next = { nx: Math.max(0, Math.min(1, nx)), ny: Math.max(0, Math.min(1, ny)) };
if (npcMapSpawnHover && npcMapSpawnHover.nx === next.nx && npcMapSpawnHover.ny === next.ny) {
return { ok: true };
}
npcMapSpawnHover = next;
emitNpcMapSpawnDragState();
return { ok: true };
});
registerHandler(ipcChannels.npcMapSpawn.cancel, () => {
clearNpcMapSpawnDrag();
return { ok: true };
});
registerHandler(ipcChannels.npcMapSpawn.commit, () => {
const npcId = npcMapSpawnDragNpcId;
const hover = npcMapSpawnHover;
clearNpcMapSpawnDrag();
if (!npcId || !hover) return { spawned: false };
const project = projectStore.getOpenProject();
const sceneId = project?.currentSceneId;
const npc = project?.npcs.find((item) => item.id === npcId);
if (!sceneId || !npc) return { spawned: false };
sceneNpcTokensSessionStore.dispatch({
kind: 'spawn',
npcId: npc.id,
sceneId,
nx: hover.nx,
ny: hover.ny,
disposition: npc.disposition,
});
emitSceneNpcTokensSessionState();
return { spawned: true };
});
registerHandler(ipcChannels.scenePlayerTokensSession.getState, () => {
return { state: scenePlayerTokensSessionStore.getState() };
});