{t('control.screenPreview')}
{!isVideoPreviewScene ? (
<>
{previewContentRect ? (
) : null}
{
const p = toNPoint(e);
if (!p) return;
cursorPosRef.current = p;
layoutBrushCursor();
}}
onPointerLeave={() => {
cursorPosRef.current = null;
layoutBrushCursor();
}}
onPointerDown={(e) => {
const p = toNPoint(e);
if (!p) return;
cursorPosRef.current = p;
layoutBrushCursor();
(e.currentTarget as HTMLDivElement).setPointerCapture(e.pointerId);
if (tool.tool === 'eraser') {
const pt = { x: p.x, y: p.y, tMs: Date.now() };
brushRef.current = { tool: 'eraser', startN: p, points: [pt] };
dispatchFieldEraserPoints([pt]);
tryEraseActionEffect(p);
return;
}
brushRef.current = {
tool: tool.tool,
startN: p,
points: [{ x: p.x, y: p.y, tMs: Date.now() }],
};
if (tool.tool === 'exploreBrush') {
void sd.dispatch({
kind: 'draft.set',
draft: {
points: [{ x: p.x, y: p.y, tMs: Date.now() }],
radiusN: tool.radiusN,
},
});
}
setDraftFxTick((x) => x + 1);
}}
onPointerMove={(e) => {
const p = toNPoint(e);
if (!p) return;
cursorPosRef.current = p;
layoutBrushCursor();
if (tool.tool === 'eraser' && (e.buttons & 1) !== 0) {
const b = brushRef.current;
const pt = { x: p.x, y: p.y, tMs: Date.now() };
if (b?.tool !== 'eraser' || !b.points) {
brushRef.current = { tool: 'eraser', startN: p, points: [pt] };
dispatchFieldEraserPoints([pt]);
} else {
const last = b.points[b.points.length - 1];
if (last) {
const dx = p.x - last.x;
const dy = p.y - last.y;
const minStep = Math.max(0.004, tool.radiusN * 0.25);
if (dx * dx + dy * dy >= minStep * minStep) {
b.points.push(pt);
dispatchFieldEraserPoints([last, pt]);
}
}
}
tryEraseActionEffect(p);
return;
}
const b = brushRef.current;
if (!b?.points) return;
const last = b.points[b.points.length - 1];
if (!last) return;
const dx = p.x - last.x;
const dy = p.y - last.y;
const minStep = Math.max(0.004, tool.radiusN * 0.25);
if (dx * dx + dy * dy < minStep * minStep) return;
b.points.push({ x: p.x, y: p.y, tMs: Date.now() });
scheduleDraftRepaint();
}}
onPointerUp={() => {
void commitStroke();
}}
onPointerCancel={() => {
if (brushRef.current?.tool === 'exploreBrush') {
void sd.dispatch({ kind: 'draft.set', draft: null });
}
brushRef.current = null;
setDraftFxTick((x) => x + 1);
}}
/>
>
) : null}
{(() => {
const activeMaterial =
session?.project && materialsOverlay?.activeMaterialId
? (session.project.materials ?? []).find((m) => m.id === materialsOverlay.activeMaterialId)
: undefined;
if (!activeMaterial) return null;
return (
{
void materialsApi.dispatch({ kind: 'hide' });
}}
onLayoutChange={(layout) => {
void materialsApi.dispatch({ kind: 'layout.set', layout });
}}
onZoomAt={(nx, ny) => {
void materialsApi.dispatch({ kind: 'zoomAt', nx, ny });
}}
/>
);
})()}
{t('control.branches')}
{showReturnToMain ? (
{t('control.option', { n: '1' })}
{returnSceneTitle}
) : null}
{nextScenes.map((o, i) => (
{t('control.option', { n: String(i + 1 + branchOptionOffset) })}
{o.scene.title || t('control.unnamed')}
))}
{nextScenes.length === 0 && !showReturnToMain ? (
{t('control.noBranches')}
) : null}
{t('control.sceneMusic')}
{sceneAudios.length === 0 ? (
{t('control.noSceneAudio')}
) : null}
{sceneAudios.length > 0 ? (
{sceneAudios.map(({ ref, asset }) => {
const el = sceneAudioElsRef.current.get(ref.assetId) ?? null;
const st = audioStatus('scene', ref.assetId);
const dur = el?.duration && Number.isFinite(el.duration) ? el.duration : 0;
const cur = el?.currentTime && Number.isFinite(el.currentTime) ? el.currentTime : 0;
const pct = dur > 0 ? Math.max(0, Math.min(1, cur / dur)) : 0;
return (
{asset.originalName}
{ref.autoplay ? t('control.modeAuto') : t('control.modeManual')}
{ref.loop ? t('control.loop') : t('control.once')}
{st.label}
0 ? Math.round(dur) : 0}
aria-valuenow={Math.round(cur)}
tabIndex={0}
onKeyDown={(e) => {
if (!el) return;
if (!dur) return;
if (e.key === 'ArrowLeft') el.currentTime = Math.max(0, el.currentTime - 5);
if (e.key === 'ArrowRight') el.currentTime = Math.min(dur, el.currentTime + 5);
setSceneAudioStateTick((x) => x + 1);
}}
onClick={(e) => {
if (!el) return;
if (!dur) return;
const rect = (e.currentTarget as HTMLDivElement).getBoundingClientRect();
const next = (e.clientX - rect.left) / rect.width;
el.currentTime = Math.max(0, Math.min(dur, next * dur));
setSceneAudioStateTick((x) => x + 1);
}}
className={[
styles.audioScrub,
dur > 0 ? styles.audioScrubPointer : styles.audioScrubDefault,
].join(' ')}
title={dur > 0 ? t('control.scrubSeek') : t('control.durationUnknown')}
>
{formatTime(cur)}
{dur ? formatTime(dur) : '—:—'}
);
})}
) : null}
{t('control.gameMusic')}
{campaignAudios.length === 0 ? (
{t('control.noGameAudio')}
) : (
{campaignAudios.map(({ ref, asset }) => {
const el = campaignAudioElsRef.current.get(ref.assetId) ?? null;
const st = audioStatus('campaign', ref.assetId);
const dur = el?.duration && Number.isFinite(el.duration) ? el.duration : 0;
const cur = el?.currentTime && Number.isFinite(el.currentTime) ? el.currentTime : 0;
const pct = dur > 0 ? Math.max(0, Math.min(1, cur / dur)) : 0;
return (
{asset.originalName}
{ref.autoplay ? t('control.modeAuto') : t('control.modeManual')}
{ref.loop ? t('control.loop') : t('control.once')}
{st.label}
{!allowCampaignAudio ? (
{t('control.pauseSceneMusic')}
) : null}
0 ? Math.round(dur) : 0}
aria-valuenow={Math.round(cur)}
tabIndex={0}
onKeyDown={(e) => {
if (!el) return;
if (!dur) return;
if (e.key === 'ArrowLeft') el.currentTime = Math.max(0, el.currentTime - 5);
if (e.key === 'ArrowRight') el.currentTime = Math.min(dur, el.currentTime + 5);
setCampaignAudioStateTick((x) => x + 1);
}}
onClick={(e) => {
if (!el) return;
if (!dur) return;
const rect = (e.currentTarget as HTMLDivElement).getBoundingClientRect();
const next = (e.clientX - rect.left) / rect.width;
el.currentTime = Math.max(0, Math.min(dur, next * dur));
setCampaignAudioStateTick((x) => x + 1);
}}
className={[
styles.audioScrub,
dur > 0 ? styles.audioScrubPointer : styles.audioScrubDefault,
].join(' ')}
title={dur > 0 ? t('control.scrubSeek') : t('control.durationUnknown')}
>
{formatTime(cur)}
{dur ? formatTime(dur) : '—:—'}
);
})}
)}
{sideStoryLines.length > 0 ? (
{t('control.sideStoryLines')}
{sideStoryLines.map((line) =>
line.scene ? (
enterSideStoryline(line.graphNodeId)}
/>
) : null,
)}
) : null}