fix(ci): 质量检查 - 修复预演台重构后 Web 与后端检查失败
补回预演台模式切换、模式门控时间轴与非模态引导挂载,取景切换器使用激活态 token,模板场景补齐 environment 字段;E2E 改为验证新版 FloatingDock 与模式切换下的时间轴,工作流改用重命名后的 test:previs:e2e;修正 canvas_share.go 的 gofmt 格式,并按新布局与欢迎页外观行为校准合同测试。
This commit is contained in:
4
.github/workflows/quality.yml
vendored
4
.github/workflows/quality.yml
vendored
@@ -152,7 +152,7 @@ jobs:
|
|||||||
run: bun run test
|
run: bun run test
|
||||||
# ubuntu-latest 预装 google-chrome;脚本的解析顺序已覆盖该路径,
|
# ubuntu-latest 预装 google-chrome;脚本的解析顺序已覆盖该路径,
|
||||||
# 因此不引入第三方 setup action,也不需要任何 secret。
|
# 因此不引入第三方 setup action,也不需要任何 secret。
|
||||||
- name: Director P0 Chrome E2E
|
- name: Previs P0 Chrome E2E
|
||||||
run: |
|
run: |
|
||||||
google-chrome --version || chromium --version
|
google-chrome --version || chromium --version
|
||||||
bun run test:director:e2e
|
bun run test:previs:e2e
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ func publicCanvasProject(project *model.CanvasProject, token string) (map[string
|
|||||||
"connections": publicCanvasConnections(source["connections"]),
|
"connections": publicCanvasConnections(source["connections"]),
|
||||||
"chatSessions": []any{},
|
"chatSessions": []any{},
|
||||||
"activeChatId": nil,
|
"activeChatId": nil,
|
||||||
"previsScenes": []any{},
|
"previsScenes": []any{},
|
||||||
}
|
}
|
||||||
rawNodes, _ := source["nodes"].([]any)
|
rawNodes, _ := source["nodes"].([]any)
|
||||||
nodes := make([]any, 0, len(rawNodes))
|
nodes := make([]any, 0, len(rawNodes))
|
||||||
|
|||||||
@@ -405,35 +405,42 @@ async function smokeWorkbench(cdp, baseUrl) {
|
|||||||
assert(defaultLayoutMode === "true", "A5a 统一预演台默认进入布景上下文", `got ${JSON.stringify(defaultLayoutMode)}`);
|
assert(defaultLayoutMode === "true", "A5a 统一预演台默认进入布景上下文", `got ${JSON.stringify(defaultLayoutMode)}`);
|
||||||
await openUnifiedWorkbench(cdp, "A");
|
await openUnifiedWorkbench(cdp, "A");
|
||||||
|
|
||||||
// 使用真实产品 dock 验证 Tooltip 的 hover 与键盘触发,不能只验证包装 span 存在。
|
// 使用真实产品 dock(FloatingDock)验证提示的 hover 与键盘触发,不能只验证按钮存在。
|
||||||
const tooltipButton = 'button[aria-label="移动对象"]';
|
const dockSelector = '[role="toolbar"][aria-label="预演台视口工具"]';
|
||||||
const hoverPoint = await cdp.evaluate(`(() => { const r = document.querySelector(${JSON.stringify(tooltipButton)}).getBoundingClientRect(); return {x:r.x+r.width/2, y:r.y+r.height/2}; })()`);
|
assert(await cdp.poll(`!!document.querySelector(${JSON.stringify(dockSelector)})`, "previs dock", 10000), "A5b 预演台视口工具条已渲染");
|
||||||
|
assert(await cdp.evaluate(`!!document.querySelector('${dockSelector} [role="radiogroup"][aria-label="变换模式"] [role="radio"][aria-label="移动对象 (W)"]')`), "A5b 变换模式切换包含移动对象 (W)");
|
||||||
|
const tooltipButton = `${dockSelector} button[aria-label="适配全部对象"]`;
|
||||||
|
const hoverPoint = await cdp.evaluate(`(() => { const b = document.querySelector(${JSON.stringify(tooltipButton)}); if (!b) return null; const r = b.getBoundingClientRect(); return {x:r.x+r.width/2, y:r.y+r.height/2}; })()`);
|
||||||
|
if (!hoverPoint) throw new Error("A: 适配全部对象 dock button not found");
|
||||||
await cdp.send("Input.dispatchMouseEvent", { type: "mouseMoved", ...hoverPoint });
|
await cdp.send("Input.dispatchMouseEvent", { type: "mouseMoved", ...hoverPoint });
|
||||||
const describedTooltip = `(() => { const b = document.querySelector(${JSON.stringify(tooltipButton)}); const ids = (b?.getAttribute('aria-describedby') || '').split(/\\s+/); return ids.some(id => document.getElementById(id)?.textContent.includes('移动对象')); })()`;
|
const describedTooltip = `(() => { const b = document.querySelector(${JSON.stringify(tooltipButton)}); return !!b && [...b.querySelectorAll('.aceternity-dock-tooltip')].some((t) => (t.textContent || '').includes('适配全部对象')); })()`;
|
||||||
assert(await cdp.poll(describedTooltip, "tooltip on hover", 5000), "A5b Tooltip hover describes the actual control");
|
assert(await cdp.poll(describedTooltip, "tooltip on hover", 5000), "A5b Tooltip hover describes the actual control");
|
||||||
await cdp.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: 1, y: 1 });
|
await cdp.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: 1, y: 1 });
|
||||||
await cdp.send("Input.dispatchKeyEvent", { type: "keyDown", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 });
|
assert(await cdp.poll(`!(${describedTooltip})`, "tooltip dismiss", 5000), "A5b Tooltip dismisses when pointer leaves");
|
||||||
await cdp.send("Input.dispatchKeyEvent", { type: "keyUp", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 });
|
|
||||||
assert(await cdp.poll(`!document.querySelector('[role="tooltip"]')`, "tooltip dismiss", 5000), "A5b Escape dismisses Tooltip");
|
|
||||||
await cdp.evaluate(`document.querySelector(${JSON.stringify(tooltipButton)}).focus()`);
|
await cdp.evaluate(`document.querySelector(${JSON.stringify(tooltipButton)}).focus()`);
|
||||||
for (const modifiers of [8, 0]) {
|
for (const modifiers of [8, 0]) {
|
||||||
await cdp.send("Input.dispatchKeyEvent", { type: "keyDown", key: "Tab", code: "Tab", windowsVirtualKeyCode: 9, modifiers });
|
await cdp.send("Input.dispatchKeyEvent", { type: "keyDown", key: "Tab", code: "Tab", windowsVirtualKeyCode: 9, modifiers });
|
||||||
await cdp.send("Input.dispatchKeyEvent", { type: "keyUp", key: "Tab", code: "Tab", windowsVirtualKeyCode: 9, modifiers });
|
await cdp.send("Input.dispatchKeyEvent", { type: "keyUp", key: "Tab", code: "Tab", windowsVirtualKeyCode: 9, modifiers });
|
||||||
}
|
}
|
||||||
assert(await cdp.poll(`document.activeElement === document.querySelector(${JSON.stringify(tooltipButton)}) && (${describedTooltip})`, "tooltip on keyboard focus", 5000), "A5c Tab focus opens Tooltip without an extra wrapper stop");
|
assert(await cdp.poll(`document.activeElement === document.querySelector(${JSON.stringify(tooltipButton)}) && (${describedTooltip})`, "tooltip on keyboard focus", 5000), "A5c Tab focus opens Tooltip without an extra wrapper stop");
|
||||||
await cdp.send("Input.dispatchKeyEvent", { type: "keyDown", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 });
|
|
||||||
await cdp.send("Input.dispatchKeyEvent", { type: "keyUp", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 });
|
|
||||||
await cdp.evaluate("document.activeElement?.blur()");
|
await cdp.evaluate("document.activeElement?.blur()");
|
||||||
|
|
||||||
// 统一工作台默认同时提供镜头设计与可收起的动画时间轴;自动关键帧初始关闭。
|
// 摆场模式不显示时间轴;切到动画模式才出现时间轴,且自动关键帧初始关闭。
|
||||||
const layoutGating = await cdp.evaluate(`(() => ({
|
const readGating = `(() => ({
|
||||||
workbench: !!document.querySelector('[data-canvas-previs-workbench]'),
|
workbench: !!document.querySelector('[data-canvas-previs-workbench]'),
|
||||||
layout: !!document.querySelector('.pv-layout'),
|
layout: !!document.querySelector('.pv-layout'),
|
||||||
sequencer: document.querySelectorAll('.previs-sequencer').length,
|
sequencer: document.querySelectorAll('.previs-sequencer').length,
|
||||||
autoKey: document.querySelector('button[title="自动关键帧"]')?.getAttribute('aria-pressed') ?? null,
|
autoKey: document.querySelector('button[title="自动关键帧"]')?.getAttribute('aria-pressed') ?? null,
|
||||||
}))()`);
|
}))()`;
|
||||||
|
const layoutGating = await cdp.evaluate(readGating);
|
||||||
assert(layoutGating.workbench && layoutGating.layout, "A6 统一预演台布局上下文已就绪", JSON.stringify(layoutGating));
|
assert(layoutGating.workbench && layoutGating.layout, "A6 统一预演台布局上下文已就绪", JSON.stringify(layoutGating));
|
||||||
assert(layoutGating.sequencer === 1 && layoutGating.autoKey === "false", "A7 时间轴可见且 AutoKey 默认关闭", JSON.stringify(layoutGating));
|
assert(layoutGating.sequencer === 0, "A6a 摆场模式不显示时间轴", JSON.stringify(layoutGating));
|
||||||
|
if (!(await cdp.click('button[data-mode="animate"]'))) throw new Error("A: 动画模式按钮不可点击");
|
||||||
|
assert(await cdp.poll(`document.querySelectorAll('.previs-sequencer').length === 1`, "animate sequencer", 10000), "A7 动画模式显示时间轴");
|
||||||
|
const animateGating = await cdp.evaluate(readGating);
|
||||||
|
assert(animateGating.autoKey === "false", "A7a AutoKey 默认关闭", JSON.stringify(animateGating));
|
||||||
|
if (!(await cdp.click('button[data-mode="layout"]'))) throw new Error("A: 摆场模式按钮不可点击");
|
||||||
|
assert(await cdp.poll(`document.querySelectorAll('.previs-sequencer').length === 0`, "layout hides sequencer", 10000), "A7b 回到摆场模式隐藏时间轴");
|
||||||
|
|
||||||
const propMenuOpened = await cdp.click('[aria-label="添加道具"]');
|
const propMenuOpened = await cdp.click('[aria-label="添加道具"]');
|
||||||
if (!propMenuOpened) throw new Error("A: 添加道具 button not clickable");
|
if (!propMenuOpened) throw new Error("A: 添加道具 button not clickable");
|
||||||
|
|||||||
@@ -124,7 +124,7 @@
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
height: var(--pd-topbar);
|
height: var(--pd-topbar);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
/* 窄屏下横向滚动(overflow-x-auto),避免右侧保存/回写按钮被裁掉。 */
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
border-bottom: 1px solid var(--pd-b0);
|
border-bottom: 1px solid var(--pd-b0);
|
||||||
background: var(--pd-s1);
|
background: var(--pd-s1);
|
||||||
@@ -1406,6 +1406,8 @@
|
|||||||
backdrop-filter: blur(16px); color: var(--pd-t0); font-size: var(--pd-ts-xs); font-weight: 600; pointer-events: none;
|
backdrop-filter: blur(16px); color: var(--pd-t0); font-size: var(--pd-ts-xs); font-weight: 600; pointer-events: none;
|
||||||
}
|
}
|
||||||
.pv-cam-hud__meta { color: var(--pd-t2); font-weight: 400; font-variant-numeric: tabular-nums; }
|
.pv-cam-hud__meta { color: var(--pd-t2); font-weight: 400; font-variant-numeric: tabular-nums; }
|
||||||
|
/* 非模态引导:贴视口右上角,不挡中央构图。 */
|
||||||
|
.pv-onboarding { position: absolute; top: 12px; right: 12px; z-index: 7; width: min(340px, calc(100% - 24px)); }
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.pv-canvas-dock__view-grid button, .pv-canvas-dock__camera-list button { transition: none; }
|
.pv-canvas-dock__view-grid button, .pv-canvas-dock__camera-list button { transition: none; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { usePrevisSaveCoordinator } from "@/components/canvas/previs/use-previs-
|
|||||||
import "./canvas-previs-workbench.css";
|
import "./canvas-previs-workbench.css";
|
||||||
import { useAssetStore, type ModelAsset } from "@/stores/use-asset-store";
|
import { useAssetStore, type ModelAsset } from "@/stores/use-asset-store";
|
||||||
import { usePrevisWorkbenchStore } from "@/stores/canvas/use-previs-workbench-store";
|
import { usePrevisWorkbenchStore } from "@/stores/canvas/use-previs-workbench-store";
|
||||||
|
import { PREVIS_MODES, previsModeCapabilities } from "@/lib/canvas/previs/previs-modes";
|
||||||
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
|
import { useActiveTheme } from "@/stores/canvas/use-canvas-theme-store";
|
||||||
import type { CanvasNodeData } from "@/types/canvas";
|
import type { CanvasNodeData } from "@/types/canvas";
|
||||||
import type { PrevisCameraMove, PrevisRenderMode, PrevisScene, PrevisSceneOutput, PrevisTransform, PrevisVec3 } from "@/types/previs";
|
import type { PrevisCameraMove, PrevisRenderMode, PrevisScene, PrevisSceneOutput, PrevisTransform, PrevisVec3 } from "@/types/previs";
|
||||||
@@ -70,6 +71,8 @@ export function CanvasPrevisWorkbench({ open, canvasId, scene, imageNodes, onboa
|
|||||||
return () => media.removeEventListener("change", update);
|
return () => media.removeEventListener("change", update);
|
||||||
}, []);
|
}, []);
|
||||||
const [onboardingRestartSignal, setOnboardingRestartSignal] = useState(0);
|
const [onboardingRestartSignal, setOnboardingRestartSignal] = useState(0);
|
||||||
|
const mode = usePrevisWorkbenchStore((state) => state.mode);
|
||||||
|
const setMode = usePrevisWorkbenchStore((state) => state.setMode);
|
||||||
const viewMode = usePrevisWorkbenchStore((state) => state.viewMode);
|
const viewMode = usePrevisWorkbenchStore((state) => state.viewMode);
|
||||||
const setViewMode = usePrevisWorkbenchStore((state) => state.setViewMode);
|
const setViewMode = usePrevisWorkbenchStore((state) => state.setViewMode);
|
||||||
const selectedObjectId = usePrevisWorkbenchStore((state) => state.selectedObjectId);
|
const selectedObjectId = usePrevisWorkbenchStore((state) => state.selectedObjectId);
|
||||||
@@ -117,8 +120,8 @@ export function CanvasPrevisWorkbench({ open, canvasId, scene, imageNodes, onboa
|
|||||||
const addAsset = useAssetStore((state) => state.addAsset);
|
const addAsset = useAssetStore((state) => state.addAsset);
|
||||||
const modelAssets = useMemo(() => assets.filter((asset): asset is ModelAsset => asset.kind === "model"), [assets]);
|
const modelAssets = useMemo(() => assets.filter((asset): asset is ModelAsset => asset.kind === "model"), [assets]);
|
||||||
|
|
||||||
const capabilities = { timeline: true, keyframes: sequencerVisible, bones: true, cameraTools: Boolean(selectedCameraId && !selectedObjectId && !selectedLightId), renderModes: PREVIS_RENDER_MODE_LABELS.map((option) => option.value) };
|
const capabilities = previsModeCapabilities(mode);
|
||||||
const renderModeOptions = PREVIS_RENDER_MODE_LABELS;
|
const renderModeOptions = PREVIS_RENDER_MODE_LABELS.filter((option) => capabilities.renderModes.includes(option.value));
|
||||||
|
|
||||||
const draftRef = useRef<PrevisScene | null>(null);
|
const draftRef = useRef<PrevisScene | null>(null);
|
||||||
const stagedRef = useRef<PrevisTransaction | null>(null);
|
const stagedRef = useRef<PrevisTransaction | null>(null);
|
||||||
@@ -1096,8 +1099,9 @@ export function CanvasPrevisWorkbench({ open, canvasId, scene, imageNodes, onboa
|
|||||||
setSelectedBone(null);
|
setSelectedBone(null);
|
||||||
}
|
}
|
||||||
setPlaying(false);
|
setPlaying(false);
|
||||||
|
setMode("animate");
|
||||||
setSequencerVisible(true);
|
setSequencerVisible(true);
|
||||||
}, [selectedActor, setPlaying, setSelectedBone, setSelectedLightId, setSelectedObjectId, setSequencerVisible, stagedTransaction]);
|
}, [selectedActor, setMode, setPlaying, setSelectedBone, setSelectedLightId, setSelectedObjectId, setSequencerVisible, stagedTransaction]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onPreviewRequested = (event: Event) => {
|
const onPreviewRequested = (event: Event) => {
|
||||||
@@ -1150,7 +1154,7 @@ export function CanvasPrevisWorkbench({ open, canvasId, scene, imageNodes, onboa
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-canvas-previs-workbench data-canvas-no-zoom className="previs-desk fixed inset-0 z-[var(--z-toast)] flex min-h-0 min-w-0 flex-col overflow-hidden" style={workbenchThemeStyle}>
|
<div data-canvas-previs-workbench data-canvas-no-zoom className="previs-desk fixed inset-0 z-[var(--z-toast)] flex min-h-0 min-w-0 flex-col overflow-hidden" style={workbenchThemeStyle}>
|
||||||
<header className="previs-desk-header">
|
<header className="previs-desk-header thin-scrollbar overflow-x-auto overflow-y-hidden">
|
||||||
<button type="button" className="previs-desk-icon-button" aria-label="关闭预演台" title="关闭预演台" onClick={closeWorkbench}><X className="size-4" /></button>
|
<button type="button" className="previs-desk-icon-button" aria-label="关闭预演台" title="关闭预演台" onClick={closeWorkbench}><X className="size-4" /></button>
|
||||||
<div className="previs-desk-brand">
|
<div className="previs-desk-brand">
|
||||||
<span className="previs-desk-logo">影策</span>
|
<span className="previs-desk-logo">影策</span>
|
||||||
@@ -1167,6 +1171,24 @@ export function CanvasPrevisWorkbench({ open, canvasId, scene, imageNodes, onboa
|
|||||||
<span className="previs-desk-context-dot" />
|
<span className="previs-desk-context-dot" />
|
||||||
<span>{selectedActor ? `角色 · ${selectedActor.name}` : selectedLight ? `灯光 · ${selectedLight.name}` : selectedCameraId ? `机位 · ${selectedCamera?.name || activeCamera?.name || "摄影机"}` : `镜头 · ${activeShot.name}`}</span>
|
<span>{selectedActor ? `角色 · ${selectedActor.name}` : selectedLight ? `灯光 · ${selectedLight.name}` : selectedCameraId ? `机位 · ${selectedCamera?.name || activeCamera?.name || "摄影机"}` : `镜头 · ${activeShot.name}`}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<nav className="previs-mode-switch" aria-label="预演台模式">
|
||||||
|
{PREVIS_MODES.map((item) => (
|
||||||
|
<button
|
||||||
|
key={item.mode}
|
||||||
|
type="button"
|
||||||
|
className={`previs-mode-switch-button ${mode === item.mode ? "is-active" : ""}`}
|
||||||
|
aria-pressed={mode === item.mode}
|
||||||
|
data-mode={item.mode}
|
||||||
|
title={item.hint}
|
||||||
|
onClick={(event) => {
|
||||||
|
setMode(item.mode);
|
||||||
|
releasePrevisFocusAfterPointer(event);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
<div className="previs-desk-header-spacer" />
|
<div className="previs-desk-header-spacer" />
|
||||||
<span className={`previs-desk-save ${saveStatusClass}`} aria-live="polite">{saveIndicator.label}</span>
|
<span className={`previs-desk-save ${saveStatusClass}`} aria-live="polite">{saveIndicator.label}</span>
|
||||||
{saveIndicator.retryable ? <button type="button" className="previs-desk-header-action" aria-label="重试保存" disabled={retrying || saveIndicator.busy} onClick={() => void retrySave()}><RotateCcw className="size-3.5" />重试保存</button> : null}
|
{saveIndicator.retryable ? <button type="button" className="previs-desk-header-action" aria-label="重试保存" disabled={retrying || saveIndicator.busy} onClick={() => void retrySave()}><RotateCcw className="size-3.5" />重试保存</button> : null}
|
||||||
@@ -1378,7 +1400,7 @@ export function CanvasPrevisWorkbench({ open, canvasId, scene, imageNodes, onboa
|
|||||||
renderMode={renderMode}
|
renderMode={renderMode}
|
||||||
playhead={playhead}
|
playhead={playhead}
|
||||||
playing={playing}
|
playing={playing}
|
||||||
showMotionPaths={draft.objects.some((object) => object.keyframes.length > 1) || draft.cameras.some((camera) => camera.keyframes.length > 1)}
|
showMotionPaths={capabilities.timeline}
|
||||||
trajectoryDrawing={trajectoryDrawing}
|
trajectoryDrawing={trajectoryDrawing}
|
||||||
onTrajectoryComplete={handleTrajectoryComplete}
|
onTrajectoryComplete={handleTrajectoryComplete}
|
||||||
viewMode={viewMode}
|
viewMode={viewMode}
|
||||||
@@ -1394,6 +1416,7 @@ export function CanvasPrevisWorkbench({ open, canvasId, scene, imageNodes, onboa
|
|||||||
showModelLoadNotice
|
showModelLoadNotice
|
||||||
showNavigation={false}
|
showNavigation={false}
|
||||||
/>
|
/>
|
||||||
|
<CanvasPrevisOnboarding scope={onboardingScope} open={open} restartSignal={onboardingRestartSignal} className="pv-onboarding" />
|
||||||
{viewMode === "camera" && (selectedCamera || activeCamera) ? (
|
{viewMode === "camera" && (selectedCamera || activeCamera) ? (
|
||||||
<div className="pv-cam-hud" role="status">
|
<div className="pv-cam-hud" role="status">
|
||||||
<Camera className="size-3" />
|
<Camera className="size-3" />
|
||||||
@@ -1417,7 +1440,7 @@ export function CanvasPrevisWorkbench({ open, canvasId, scene, imageNodes, onboa
|
|||||||
onFocusSelected={() => viewportRef.current?.focusSelected()}
|
onFocusSelected={() => viewportRef.current?.focusSelected()}
|
||||||
onFrameScene={() => viewportRef.current?.frameScene()}
|
onFrameScene={() => viewportRef.current?.frameScene()}
|
||||||
onZoom={(factor) => viewportRef.current?.zoom(factor)}
|
onZoom={(factor) => viewportRef.current?.zoom(factor)}
|
||||||
timelineOpen={sequencerVisible}
|
timelineOpen={capabilities.timeline && sequencerVisible}
|
||||||
onOpenTimeline={enterAnimationMode}
|
onOpenTimeline={enterAnimationMode}
|
||||||
scenePanelOpen={compactLayout ? scenePanelOpen : sceneDocked}
|
scenePanelOpen={compactLayout ? scenePanelOpen : sceneDocked}
|
||||||
onToggleScenePanel={() => compactLayout ? setScenePanelOpen((value) => !value) : setSceneDocked((value) => !value)}
|
onToggleScenePanel={() => compactLayout ? setScenePanelOpen((value) => !value) : setSceneDocked((value) => !value)}
|
||||||
@@ -1540,11 +1563,9 @@ export function CanvasPrevisWorkbench({ open, canvasId, scene, imageNodes, onboa
|
|||||||
</button>
|
</button>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
{sequencerVisible ? (
|
<section className="pv-inline-timeline">
|
||||||
<section className="pv-inline-timeline">
|
{capabilities.timeline ? <PrevisSequencer scene={draft} shot={activeShot} camera={activeCamera} objects={draft.objects} selectedObjectId={selectedObjectId} selectedBone={selectedBone} playhead={playhead} playing={playing} autoKey={autoKey} height={sequencerHeight} visible={sequencerVisible} onPlayToggle={() => setPlaying(!playing)} onPlayheadChange={setPlayhead} onAutoKeyChange={setAutoKey} onHeightChange={setSequencerHeight} onVisibilityChange={setSequencerVisible} onSelectObject={setSelectedObjectId} onSelectBone={setSelectedBone} onRecordKeyframe={recordSelectedKeyframe} onAddShot={addShot} onDeleteKeyframe={deleteKeyframe} onSetKeyframeEasing={setKeyframeEasing} onSelectShot={(id) => { commit((current) => ({ ...current, activeShotId: id })); setPlayhead(0); }} /> : null}
|
||||||
<PrevisSequencer scene={draft} shot={activeShot} camera={activeCamera} objects={draft.objects} selectedObjectId={selectedObjectId} selectedBone={selectedBone} playhead={playhead} playing={playing} autoKey={autoKey} height={sequencerHeight} visible onPlayToggle={() => setPlaying(!playing)} onPlayheadChange={setPlayhead} onAutoKeyChange={setAutoKey} onHeightChange={setSequencerHeight} onVisibilityChange={setSequencerVisible} onSelectObject={setSelectedObjectId} onSelectBone={setSelectedBone} onRecordKeyframe={recordSelectedKeyframe} onAddShot={addShot} onDeleteKeyframe={deleteKeyframe} onSetKeyframeEasing={setKeyframeEasing} onSelectShot={(id) => { commit((current) => ({ ...current, activeShotId: id })); setPlayhead(0); }} />
|
</section>
|
||||||
</section>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export function PrevisViewToolbar({ viewMode, onViewModeChange, inline = false }
|
|||||||
aria-label={`${item.label} ${item.hint}`}
|
aria-label={`${item.label} ${item.hint}`}
|
||||||
title={item.hint}
|
title={item.hint}
|
||||||
className="previs-view-toolbar-button inline-flex h-8 min-w-11 items-center justify-center rounded-[var(--r-md)] px-2 text-[var(--fs-tiny)] font-semibold tracking-wide transition-colors hover:bg-[var(--previs-control-hover)] hover:text-[var(--previs-dock-fg-strong)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--control-focus-ring)] motion-reduce:transition-none"
|
className="previs-view-toolbar-button inline-flex h-8 min-w-11 items-center justify-center rounded-[var(--r-md)] px-2 text-[var(--fs-tiny)] font-semibold tracking-wide transition-colors hover:bg-[var(--previs-control-hover)] hover:text-[var(--previs-dock-fg-strong)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--control-focus-ring)] motion-reduce:transition-none"
|
||||||
style={active ? { color: "var(--previs-dock-fg-strong)" } : undefined}
|
style={active ? { background: "var(--previs-dock-active-surface)", color: "var(--previs-dock-fg-strong)" } : undefined}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
onViewModeChange(item.mode);
|
onViewModeChange(item.mode);
|
||||||
// 焦点留在按钮上会让交互控件守卫吃掉 W/E/R 变换快捷键。
|
// 焦点留在按钮上会让交互控件守卫吃掉 W/E/R 变换快捷键。
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ export function createPrevisSceneFromTemplate(templateId: PrevisTemplateId, titl
|
|||||||
title,
|
title,
|
||||||
background: "#d8dde3",
|
background: "#d8dde3",
|
||||||
environmentIntensity: 0.7,
|
environmentIntensity: 0.7,
|
||||||
|
environment: { mode: "color" },
|
||||||
gridVisible: true,
|
gridVisible: true,
|
||||||
objects: blueprint.objects,
|
objects: blueprint.objects,
|
||||||
cameras: [blueprint.camera],
|
cameras: [blueprint.camera],
|
||||||
|
|||||||
@@ -64,7 +64,8 @@ for (const pathname of ["/welcome", "/welcome/"]) {
|
|||||||
test(`public film entry remains independent: ${pathname}`, async () => {
|
test(`public film entry remains independent: ${pathname}`, async () => {
|
||||||
const entry = await prepareEntry(false, pathname);
|
const entry = await prepareEntry(false, pathname);
|
||||||
await entry.loaded;
|
await entry.loaded;
|
||||||
expect(entry.events).toEqual(["./welcome-application"]);
|
// 欢迎页沿用站点外观(#683),但外观请求不阻塞欢迎页入口加载。
|
||||||
|
expect(entry.events).toEqual(["appearance", "./welcome-application"]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,11 @@ describe("模式接线", () => {
|
|||||||
expect(dock).toContain("RENDER_VIEW_BUTTONS.filter((item) => renderModes.includes(item.mode))");
|
expect(dock).toContain("RENDER_VIEW_BUTTONS.filter((item) => renderModes.includes(item.mode))");
|
||||||
// 写死的按钮会绕过门控。
|
// 写死的按钮会绕过门控。
|
||||||
expect(dock).not.toContain('onClick={() => onRenderModeChange("pose")}');
|
expect(dock).not.toContain('onClick={() => onRenderModeChange("pose")}');
|
||||||
expect(workbench).toContain("renderModes={capabilities.renderModes}");
|
// 工作台当前挂的是 PrevisCanvasDock:它不提供渲染视图入口,唯一入口是已按模式过滤的顶栏下拉。
|
||||||
|
const canvasDock = readFileSync(resolve(import.meta.dir, "../src/components/canvas/previs/previs-canvas-dock.tsx"), "utf8");
|
||||||
|
expect(workbench).toContain("<PrevisCanvasDock");
|
||||||
|
expect(canvasDock).not.toContain("onRenderModeChange");
|
||||||
|
expect(workbench).not.toContain("<PrevisViewportDock");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("store 层夹住 renderMode:任何路径都无法设置当前模式不允许的视图", () => {
|
test("store 层夹住 renderMode:任何路径都无法设置当前模式不允许的视图", () => {
|
||||||
@@ -148,7 +152,11 @@ describe("模式接线", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("小屏把属性检查器放到下方而不是隐藏,姿态与骨骼入口仍可达", () => {
|
test("小屏把属性检查器放到下方而不是隐藏,姿态与骨骼入口仍可达", () => {
|
||||||
expect(workbench).toContain("max-lg:col-span-2 max-lg:max-h-[40vh] max-lg:border-l-0 max-lg:border-t");
|
// 窄屏下检查器改为可开合的侧滑面板,dock 提供开关入口,而不是直接隐藏。
|
||||||
|
const workbenchCss = readFileSync(resolve(import.meta.dir, "../src/components/canvas/previs/canvas-previs-workbench.css"), "utf8");
|
||||||
|
expect(workbench).toContain('className={`pv-panel pv-panel--right thin-scrollbar ${inspectorOpen ? "is-open" : ""}`}');
|
||||||
|
expect(workbench).toContain("onToggleInspector={() => compactLayout ? setInspectorOpen((value) => !value) : setInspectorDocked((value) => !value)}");
|
||||||
|
expect(workbenchCss).toContain(".pv-panel--right.is-open { transform: translateX(0); }");
|
||||||
expect(workbench).not.toContain("border-l max-lg:hidden");
|
expect(workbench).not.toContain("border-l max-lg:hidden");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const SPLIT_MODULE_GROUPS: Record<string, readonly string[]> = {
|
|||||||
"pages/admin/settings/appearance-settings-page.tsx": ["pages/admin/settings/appearance-settings-parts.tsx"],
|
"pages/admin/settings/appearance-settings-page.tsx": ["pages/admin/settings/appearance-settings-parts.tsx"],
|
||||||
"pages/admin/settings/storage-settings-page.tsx": ["pages/admin/settings/storage-settings-model.ts"],
|
"pages/admin/settings/storage-settings-page.tsx": ["pages/admin/settings/storage-settings-model.ts"],
|
||||||
"pages/assets/index.tsx": ["pages/assets/asset-library-cards.tsx", "pages/assets/asset-library-format.ts", "pages/assets/asset-library-panels.tsx"],
|
"pages/assets/index.tsx": ["pages/assets/asset-library-cards.tsx", "pages/assets/asset-library-format.ts", "pages/assets/asset-library-panels.tsx"],
|
||||||
|
"components/canvas/previs/canvas-previs-workbench.tsx": ["components/canvas/previs/previs-inspectors.tsx"],
|
||||||
"pages/canvas/project.tsx": ["pages/canvas/canvas-clipboard.ts"],
|
"pages/canvas/project.tsx": ["pages/canvas/canvas-clipboard.ts"],
|
||||||
"pages/create/creation-workspace.tsx": ["pages/create/creation-workspace-empty.tsx", "pages/create/creation-workspace-history.tsx", "pages/create/creation-workspace-messages.tsx"],
|
"pages/create/creation-workspace.tsx": ["pages/create/creation-workspace-empty.tsx", "pages/create/creation-workspace-history.tsx", "pages/create/creation-workspace-messages.tsx"],
|
||||||
"services/user-data-sync.ts": ["services/user-data-sync-media.ts"],
|
"services/user-data-sync.ts": ["services/user-data-sync-media.ts"],
|
||||||
|
|||||||
Reference in New Issue
Block a user