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:
@@ -9,6 +9,7 @@ export * from './npcDisposition';
|
||||
export * from './npcs';
|
||||
export * from './sceneDarkness';
|
||||
export * from './sceneGrid';
|
||||
export * from './sceneGridSnap';
|
||||
export * from './sceneTraps';
|
||||
export * from './sceneView';
|
||||
export * from './videoPlayback';
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import { snapNormToGridCell } from './sceneGridSnap';
|
||||
|
||||
void test('snapNormToGridCell: disabled / invalid size = no-op', () => {
|
||||
assert.deepEqual(
|
||||
snapNormToGridCell(0.33, 0.77, { enabled: false, type: 'square', sizeN: 0.1 }, 1000, 800),
|
||||
{ nx: 0.33, ny: 0.77 },
|
||||
);
|
||||
assert.deepEqual(
|
||||
snapNormToGridCell(0.33, 0.77, { enabled: true, type: 'square', sizeN: 0.1 }, 0, 800),
|
||||
{ nx: 0.33, ny: 0.77 },
|
||||
);
|
||||
});
|
||||
|
||||
void test('snapNormToGridCell: square snaps to cell center', () => {
|
||||
// minDim=1000, sizeN=0.1 → cell=100. Center of (0,0) at (50,50) → (0.05, 0.05) in 1000×1000.
|
||||
const at = snapNormToGridCell(0.04, 0.06, { enabled: true, type: 'square', sizeN: 0.1 }, 1000, 1000);
|
||||
assert.ok(Math.abs(at.nx - 0.05) < 1e-9);
|
||||
assert.ok(Math.abs(at.ny - 0.05) < 1e-9);
|
||||
|
||||
const next = snapNormToGridCell(0.14, 0.16, { enabled: true, type: 'square', sizeN: 0.1 }, 1000, 1000);
|
||||
assert.ok(Math.abs(next.nx - 0.15) < 1e-9);
|
||||
assert.ok(Math.abs(next.ny - 0.15) < 1e-9);
|
||||
});
|
||||
|
||||
void test('snapNormToGridCell: hex snaps to flat-top center', () => {
|
||||
const cell = 100;
|
||||
const horiz = cell * 0.75;
|
||||
const vert = (Math.sqrt(3) / 2) * cell;
|
||||
// Center of col=1,row=0: (75, vert/2) because odd col offset.
|
||||
const cx = 1 * horiz;
|
||||
const cy = 0 * vert + vert / 2;
|
||||
const near = snapNormToGridCell(
|
||||
(cx + 3) / 1000,
|
||||
(cy - 4) / 1000,
|
||||
{ enabled: true, type: 'hex', sizeN: 0.1 },
|
||||
1000,
|
||||
1000,
|
||||
);
|
||||
assert.ok(Math.abs(near.nx - cx / 1000) < 1e-9);
|
||||
assert.ok(Math.abs(near.ny - cy / 1000) < 1e-9);
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
/** Привязка нормализованных координат токена к центру ячейки сетки сцены. */
|
||||
|
||||
import type { SceneGrid } from './sceneGrid';
|
||||
|
||||
function clamp01(n: number): number {
|
||||
return Math.max(0, Math.min(1, n));
|
||||
}
|
||||
|
||||
function cellPx(sizeN: number, contentW: number, contentH: number): number {
|
||||
const minDim = Math.min(contentW, contentH);
|
||||
return Math.max(4, sizeN * minDim);
|
||||
}
|
||||
|
||||
function snapSquare(
|
||||
nx: number,
|
||||
ny: number,
|
||||
contentW: number,
|
||||
contentH: number,
|
||||
sizeN: number,
|
||||
): { nx: number; ny: number } {
|
||||
const cell = cellPx(sizeN, contentW, contentH);
|
||||
const px = nx * contentW;
|
||||
const py = ny * contentH;
|
||||
const col = Math.round(px / cell - 0.5);
|
||||
const row = Math.round(py / cell - 0.5);
|
||||
return {
|
||||
nx: clamp01(((col + 0.5) * cell) / contentW),
|
||||
ny: clamp01(((row + 0.5) * cell) / contentH),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Flat-top hex, как в `SceneGridOverlay.drawHexGrid`:
|
||||
* центры `(col * horiz, row * vert + (col % 2 === 0 ? 0 : vert/2))`.
|
||||
*/
|
||||
function snapHex(
|
||||
nx: number,
|
||||
ny: number,
|
||||
contentW: number,
|
||||
contentH: number,
|
||||
sizeN: number,
|
||||
): { nx: number; ny: number } {
|
||||
const cell = cellPx(sizeN, contentW, contentH);
|
||||
const hexW = cell;
|
||||
const vert = (Math.sqrt(3) / 2) * hexW;
|
||||
const horiz = hexW * 0.75;
|
||||
const px = nx * contentW;
|
||||
const py = ny * contentH;
|
||||
const col0 = Math.round(px / horiz);
|
||||
const row0 = Math.round(py / vert);
|
||||
|
||||
let bestNx = nx;
|
||||
let bestNy = ny;
|
||||
let bestD = Number.POSITIVE_INFINITY;
|
||||
for (let col = col0 - 2; col <= col0 + 2; col++) {
|
||||
for (let row = row0 - 2; row <= row0 + 2; row++) {
|
||||
const cx = col * horiz;
|
||||
const cy = row * vert + (col % 2 === 0 ? 0 : vert / 2);
|
||||
const d = (cx - px) * (cx - px) + (cy - py) * (cy - py);
|
||||
if (d < bestD) {
|
||||
bestD = d;
|
||||
bestNx = cx / contentW;
|
||||
bestNy = cy / contentH;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { nx: clamp01(bestNx), ny: clamp01(bestNy) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Snap центра токена к ближайшему центру ячейки.
|
||||
* Если сетка выключена / нет размеров — координаты без изменений.
|
||||
*/
|
||||
export function snapNormToGridCell(
|
||||
nx: number,
|
||||
ny: number,
|
||||
grid: Pick<SceneGrid, 'enabled' | 'type' | 'sizeN'> | null | undefined,
|
||||
contentW: number,
|
||||
contentH: number,
|
||||
): { nx: number; ny: number } {
|
||||
if (!grid?.enabled) return { nx, ny };
|
||||
if (!(contentW > 0) || !(contentH > 0)) return { nx, ny };
|
||||
if (!Number.isFinite(nx) || !Number.isFinite(ny)) return { nx, ny };
|
||||
if (grid.type === 'hex') {
|
||||
return snapHex(nx, ny, contentW, contentH, grid.sizeN);
|
||||
}
|
||||
return snapSquare(nx, ny, contentW, contentH, grid.sizeN);
|
||||
}
|
||||
Reference in New Issue
Block a user