Files
DndGamePlayer/app/renderer/materials/MaterialsApp.tsx
T
Ivan Fontosh c1c332364c feat(materials): free rotate session overlays and show save progress
Match NPC drag-to-rotate on the material frame during playback, and change the add/edit Save button to Saving… while import runs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 14:56:42 +08:00

159 lines
5.4 KiB
TypeScript

import React, { useCallback, useEffect, useState } from 'react';
import { ipcChannels, type SessionState } from '../../shared/ipc/contracts';
import type { MaterialId } from '../../shared/types';
import { MaterialsBrowser } from '../editor/MaterialsBrowser';
import matStyles from '../editor/MaterialsModals.module.css';
import { useEditorI18n } from '../editor/i18n/EditorI18nContext';
import { getDndApi } from '../shared/dndApi';
import { useMaterialsOverlayState } from '../shared/materials/useMaterialsOverlayState';
import { Button } from '../shared/ui/controls';
import styles from './MaterialsApp.module.css';
function ZoomInIcon() {
return (
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden>
<circle cx="10.5" cy="10.5" r="6.25" fill="none" stroke="currentColor" strokeWidth="1.8" />
<path d="M15.2 15.2 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
<path
d="M10.5 7.8v5.4M7.8 10.5h5.4"
fill="none"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
/>
</svg>
);
}
function ZoomOutIcon() {
return (
<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden>
<circle cx="10.5" cy="10.5" r="6.25" fill="none" stroke="currentColor" strokeWidth="1.8" />
<path d="M15.2 15.2 20 20" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
<path d="M7.8 10.5h5.4" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
</svg>
);
}
export function MaterialsApp() {
const { t } = useEditorI18n();
const api = getDndApi();
const [session, setSession] = useState<SessionState | null>(null);
const [overlay, overlayApi] = useMaterialsOverlayState();
const [selectedId, setSelectedId] = useState<MaterialId | null>(null);
const onSelect = useCallback((id: MaterialId | null) => setSelectedId(id), []);
useEffect(() => {
void api.invoke(ipcChannels.project.get, {}).then(({ project }) => {
setSession({ project, currentSceneId: project?.currentSceneId ?? null });
const mats = project?.materials ?? [];
setSelectedId(mats[0]?.id ?? null);
});
return api.on(ipcChannels.session.stateChanged, ({ state }) => {
setSession(state);
});
}, [api]);
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
if (overlay?.activeMaterialId) {
void overlayApi.dispatch({ kind: 'hide' });
return;
}
if (overlay?.zoomTool) {
void overlayApi.dispatch({ kind: 'zoomTool.set', tool: null });
return;
}
window.close();
}
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [overlay?.activeMaterialId, overlay?.zoomTool, overlayApi]);
const materials = session?.project?.materials ?? [];
const activeId = overlay?.activeMaterialId ?? null;
const zoomTool = overlay?.zoomTool ?? null;
return (
<div className={styles.page}>
<MaterialsBrowser
mode="runtime"
fillHeight
listOnly
className={styles.browser}
materials={materials}
selectedId={selectedId}
onSelect={onSelect}
activeMaterialId={activeId}
onTileActivate={(id) => {
const mat = materials.find((m) => m.id === id);
void overlayApi.dispatch({
kind: 'toggle',
materialId: id,
rotationDeg: mat?.rotationDeg ?? 0,
});
}}
toolbar={
<>
<div className={matStyles.browserToolbarRow}>
<Button
variant={zoomTool === 'zoomIn' ? 'primary' : 'ghost'}
iconOnly
title={t('materials.zoomIn')}
ariaLabel={t('materials.zoomIn')}
tooltipPlacement="bottom"
onClick={() => {
void overlayApi.dispatch({
kind: 'zoomTool.set',
tool: zoomTool === 'zoomIn' ? null : 'zoomIn',
});
}}
>
<ZoomInIcon />
</Button>
<Button
variant={zoomTool === 'zoomOut' ? 'primary' : 'ghost'}
iconOnly
title={t('materials.zoomOut')}
ariaLabel={t('materials.zoomOut')}
tooltipPlacement="bottom"
onClick={() => {
void overlayApi.dispatch({
kind: 'zoomTool.set',
tool: zoomTool === 'zoomOut' ? null : 'zoomOut',
});
}}
>
<ZoomOutIcon />
</Button>
</div>
{activeId ? (
<Button
title={t('materials.closeOverlay')}
ariaLabel={t('materials.closeOverlay')}
tooltipPlacement="bottom"
onClick={() => {
void overlayApi.dispatch({ kind: 'hide' });
}}
>
{t('materials.closeOverlay')}
</Button>
) : null}
<div className={matStyles.browserToolbarHint}>
{zoomTool === 'zoomIn'
? t('materials.zoomInHint')
: zoomTool === 'zoomOut'
? t('materials.zoomOutHint')
: t('materials.zoomIdleHint')}
</div>
</>
}
/>
</div>
);
}