feat(control): grid snap, NPC context actions, and overlay dim fix

Re-enable users-branch UI, snap session tokens to square/hex grid from the control preview, refine inactive/open-info NPC menus, block marker actions while an effect brush is active, and dim materials/NPC overlays only when they are open.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-08-06 13:48:59 +08:00
parent e53d1ea934
commit 4456eb0277
24 changed files with 563 additions and 41 deletions
+29 -1
View File
@@ -19,6 +19,7 @@ import {
stripProjectZipExtension,
} from '../shared/project/projectZipExtension';
import type { Project } from '../shared/types';
import { asNpcId } from '../shared/types/ids';
import { EffectsStore, effectsDefaultTool } from './effects/effectsStore';
import { SceneDarknessStore } from './effects/sceneDarknessStore';
@@ -34,6 +35,7 @@ import { PlayersStore } from './players/playersStore';
import { SceneNpcTokensSessionStore } from './players/sceneNpcTokensSessionStore';
import { ScenePlayerTokensSessionStore } from './players/scenePlayerTokensSessionStore';
import { SceneTokensSessionStore } from './tokens/sceneTokensSessionStore';
import { TokenGridSnapSessionStore } from './tokens/tokenGridSnapSessionStore';
import { TokensStore } from './tokens/tokensStore';
import { installAutoUpdater } from './update/installAutoUpdater';
import { getAppSemanticVersion, getOptionalBuildNumber } from './versionInfo';
@@ -156,6 +158,7 @@ const npcsOverlayStore = new NpcsOverlayStore();
const sceneTokensSessionStore = new SceneTokensSessionStore();
const sceneNpcTokensSessionStore = new SceneNpcTokensSessionStore();
const scenePlayerTokensSessionStore = new ScenePlayerTokensSessionStore();
const tokenGridSnapSessionStore = new TokenGridSnapSessionStore();
let tokensStore: TokensStore | null = null;
let playersStore: PlayersStore | null = null;
@@ -226,6 +229,13 @@ function emitSceneTokensSessionState(): void {
}
}
function emitTokenGridSnapState(): void {
const { enabled } = tokenGridSnapSessionStore.getState();
for (const win of BrowserWindow.getAllWindows()) {
win.webContents.send(ipcChannels.tokenGridSnap.stateChanged, { enabled });
}
}
function emitPlayersState(): void {
const players = playersStore?.listPlayers() ?? [];
const teams = playersStore?.listTeams() ?? [];
@@ -432,6 +442,7 @@ async function main() {
sceneTokensSessionStore.reset();
sceneNpcTokensSessionStore.reset();
scenePlayerTokensSessionStore.reset();
tokenGridSnapSessionStore.reset();
if (USERS_BRANCH_FEATURES_ENABLED) {
const playerIds = Array.isArray(req?.playerIds) ? req.playerIds.map(String).filter(Boolean) : [];
if (playerIds.length > 0) {
@@ -450,11 +461,14 @@ async function main() {
emitSceneTokensSessionState();
emitSceneNpcTokensSessionState();
emitScenePlayerTokensSessionState();
emitTokenGridSnapState();
emitEffectsState();
return { ok: true };
});
registerHandler(ipcChannels.windows.closeMultiWindow, () => {
closeMultiWindow();
tokenGridSnapSessionStore.reset();
emitTokenGridSnapState();
return { ok: true };
});
registerHandler(ipcChannels.windows.syncChromeTitles, ({ localeTag }) => {
@@ -507,8 +521,13 @@ async function main() {
closeSceneEditorWindow();
return { ok: true };
});
registerHandler(ipcChannels.windows.openNpcs, () => {
registerHandler(ipcChannels.windows.openNpcs, (req) => {
openNpcsWindow();
const npcId = req?.npcId ? asNpcId(String(req.npcId)) : null;
if (npcId) {
npcsOverlayStore.dispatch({ kind: 'show', npcId });
emitNpcsOverlayState();
}
return { ok: true };
});
registerHandler(ipcChannels.windows.closeNpcs, () => {
@@ -576,6 +595,7 @@ async function main() {
sceneTokensSessionStore.reset();
sceneNpcTokensSessionStore.reset();
scenePlayerTokensSessionStore.reset();
tokenGridSnapSessionStore.reset();
emitEffectsState();
emitMaterialsOverlayState();
emitNpcsOverlayState();
@@ -585,6 +605,7 @@ async function main() {
emitSceneTokensSessionState();
emitSceneNpcTokensSessionState();
emitScenePlayerTokensSessionState();
emitTokenGridSnapState();
emitSessionState();
return { ok: true };
});
@@ -1361,6 +1382,13 @@ async function main() {
return { ok: true };
});
registerHandler(ipcChannels.tokenGridSnap.getState, () => tokenGridSnapSessionStore.getState());
registerHandler(ipcChannels.tokenGridSnap.setEnabled, ({ enabled }) => {
const next = tokenGridSnapSessionStore.setEnabled(enabled);
emitTokenGridSnapState();
return next;
});
registerHandler(ipcChannels.players.list, async () => {
if (!USERS_BRANCH_FEATURES_ENABLED || !playersStore) {
return { players: [], teams: [] };
@@ -0,0 +1,12 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { TokenGridSnapSessionStore } from './tokenGridSnapSessionStore';
void test('TokenGridSnapSessionStore: set and reset', () => {
const store = new TokenGridSnapSessionStore();
assert.equal(store.getState().enabled, false);
assert.equal(store.setEnabled(true).enabled, true);
assert.equal(store.getState().enabled, true);
assert.equal(store.reset().enabled, false);
});
@@ -0,0 +1,21 @@
export type TokenGridSnapSessionState = {
enabled: boolean;
};
export class TokenGridSnapSessionStore {
private enabled = false;
getState(): TokenGridSnapSessionState {
return { enabled: this.enabled };
}
setEnabled(enabled: boolean): TokenGridSnapSessionState {
this.enabled = Boolean(enabled);
return this.getState();
}
reset(): TokenGridSnapSessionState {
this.enabled = false;
return this.getState();
}
}