feat(viewport): 支持左右侧栏独立折叠与专注模式
- 新增 leftPanelVisible / rightPanelVisible 独立状态,可与专注模式叠加 - 添加侧栏折叠手柄及 [ / ] / \ 快捷键 - 画布安全区与悬浮控件按单侧可见性动态贴合边缘 - 顶部工具条「全屏」按钮更名为「专注模式」并更新提示 - 调整悬浮卡片顶部留白,统一视口四周间距
This commit is contained in:
@@ -72,6 +72,27 @@ it("collapses both side panels from the fullscreen toolbar action", async () =>
|
|||||||
expect(screen.getByLabelText("属性")).toHaveAttribute("aria-hidden", "true");
|
expect(screen.getByLabelText("属性")).toHaveAttribute("aria-hidden", "true");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("collapses only one side panel and leaves a handle to bring it back", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const { container } = render(<App />);
|
||||||
|
|
||||||
|
expect(screen.getByRole("button", { name: "收起场景面板" })).toBeInTheDocument();
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
useDirectorStore.getState().toggleLeftPanelVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(container.querySelector(".director-shell-fullbleed.is-left-collapsed")).toBeInTheDocument();
|
||||||
|
expect(screen.getByLabelText("场景")).toHaveAttribute("aria-hidden", "true");
|
||||||
|
// 右侧不受影响
|
||||||
|
expect(screen.getByLabelText("属性")).not.toHaveAttribute("aria-hidden");
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "展开场景面板" }));
|
||||||
|
|
||||||
|
expect(screen.getByLabelText("场景")).not.toHaveAttribute("aria-hidden");
|
||||||
|
expect(container.querySelector(".director-shell-fullbleed.is-left-collapsed")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("shows the floating timeline by default and hides it from the record action", () => {
|
it("shows the floating timeline by default and hides it from the record action", () => {
|
||||||
const { container } = render(<App />);
|
const { container } = render(<App />);
|
||||||
|
|
||||||
|
|||||||
21
src/App.tsx
21
src/App.tsx
@@ -41,6 +41,27 @@ export default function App() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 面板显隐: [ 左侧场景栏、] 右侧属性栏、\ 底部时间轴。都不带修饰键,方便单手切换。
|
||||||
|
if (!event.metaKey && !event.ctrlKey && !event.altKey) {
|
||||||
|
if (event.key === "[") {
|
||||||
|
event.preventDefault();
|
||||||
|
useDirectorStore.getState().toggleLeftPanelVisible();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === "]") {
|
||||||
|
event.preventDefault();
|
||||||
|
useDirectorStore.getState().toggleRightPanelVisible();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === "\\") {
|
||||||
|
event.preventDefault();
|
||||||
|
useDirectorStore.getState().toggleTimelineVisible();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!event.metaKey && !event.ctrlKey) return;
|
if (!event.metaKey && !event.ctrlKey) return;
|
||||||
|
|
||||||
const key = event.key.toLowerCase();
|
const key = event.key.toLowerCase();
|
||||||
|
|||||||
@@ -1,17 +1,36 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
import { PanelLeftClose, PanelLeftOpen, PanelRightClose, PanelRightOpen } from "lucide-react";
|
||||||
import { ObjectTreePanel } from "../../editor/panels/ObjectTreePanel";
|
import { ObjectTreePanel } from "../../editor/panels/ObjectTreePanel";
|
||||||
import { RightPanel } from "../../editor/panels/RightPanel";
|
import { RightPanel } from "../../editor/panels/RightPanel";
|
||||||
import { TimelinePanel } from "../../editor/panels/TimelinePanel";
|
import { TimelinePanel } from "../../editor/panels/TimelinePanel";
|
||||||
import { useDirectorStore } from "../../editor/store/directorStore";
|
import { useDirectorStore } from "../../editor/store/directorStore";
|
||||||
|
|
||||||
|
const HANDLE_ICON_PROPS = { "aria-hidden": true, size: 15, strokeWidth: 2 } as const;
|
||||||
|
|
||||||
export function DirectorDeskShell({ children }: { children: ReactNode }) {
|
export function DirectorDeskShell({ children }: { children: ReactNode }) {
|
||||||
const viewportPanelsCollapsed = useDirectorStore((state) => state.viewportPanelsCollapsed);
|
const viewportPanelsCollapsed = useDirectorStore((state) => state.viewportPanelsCollapsed);
|
||||||
|
const leftPanelVisible = useDirectorStore((state) => state.leftPanelVisible);
|
||||||
|
const rightPanelVisible = useDirectorStore((state) => state.rightPanelVisible);
|
||||||
const timelineVisible = useDirectorStore((state) => state.timelineVisible);
|
const timelineVisible = useDirectorStore((state) => state.timelineVisible);
|
||||||
|
const toggleLeftPanelVisible = useDirectorStore((state) => state.toggleLeftPanelVisible);
|
||||||
|
const toggleRightPanelVisible = useDirectorStore((state) => state.toggleRightPanelVisible);
|
||||||
|
|
||||||
|
// 「专注模式」是临时一次收起两侧,最终可见性由两侧各自的开关与之相与决定
|
||||||
|
const leftVisible = !viewportPanelsCollapsed && leftPanelVisible;
|
||||||
|
const rightVisible = !viewportPanelsCollapsed && rightPanelVisible;
|
||||||
|
|
||||||
|
const shellClassName = [
|
||||||
|
"director-shell",
|
||||||
|
"director-shell-fullbleed",
|
||||||
|
leftVisible ? "" : "is-left-collapsed",
|
||||||
|
rightVisible ? "" : "is-right-collapsed",
|
||||||
|
!leftVisible && !rightVisible ? "is-sidebars-collapsed" : "",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={shellClassName}>
|
||||||
className={`director-shell director-shell-fullbleed${viewportPanelsCollapsed ? " is-sidebars-collapsed" : ""}`}
|
|
||||||
>
|
|
||||||
<section className="viewport-column" aria-label="3D视口">
|
<section className="viewport-column" aria-label="3D视口">
|
||||||
<div className="viewport-stack">
|
<div className="viewport-stack">
|
||||||
{children}
|
{children}
|
||||||
@@ -20,18 +39,38 @@ export function DirectorDeskShell({ children }: { children: ReactNode }) {
|
|||||||
</section>
|
</section>
|
||||||
<aside
|
<aside
|
||||||
className="left-sidebar director-sidebar"
|
className="left-sidebar director-sidebar"
|
||||||
aria-hidden={viewportPanelsCollapsed ? "true" : undefined}
|
aria-hidden={leftVisible ? undefined : "true"}
|
||||||
aria-label="场景"
|
aria-label="场景"
|
||||||
>
|
>
|
||||||
<ObjectTreePanel />
|
<ObjectTreePanel />
|
||||||
</aside>
|
</aside>
|
||||||
<aside
|
<aside
|
||||||
className="right-sidebar director-sidebar"
|
className="right-sidebar director-sidebar"
|
||||||
aria-hidden={viewportPanelsCollapsed ? "true" : undefined}
|
aria-hidden={rightVisible ? undefined : "true"}
|
||||||
aria-label="属性"
|
aria-label="属性"
|
||||||
>
|
>
|
||||||
<RightPanel />
|
<RightPanel />
|
||||||
</aside>
|
</aside>
|
||||||
|
<button
|
||||||
|
aria-expanded={leftVisible}
|
||||||
|
aria-label={leftVisible ? "收起场景面板" : "展开场景面板"}
|
||||||
|
className="panel-edge-handle panel-edge-handle-left"
|
||||||
|
title={leftVisible ? "收起场景面板([)" : "展开场景面板([)"}
|
||||||
|
type="button"
|
||||||
|
onClick={toggleLeftPanelVisible}
|
||||||
|
>
|
||||||
|
{leftVisible ? <PanelLeftClose {...HANDLE_ICON_PROPS} /> : <PanelLeftOpen {...HANDLE_ICON_PROPS} />}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-expanded={rightVisible}
|
||||||
|
aria-label={rightVisible ? "收起属性面板" : "展开属性面板"}
|
||||||
|
className="panel-edge-handle panel-edge-handle-right"
|
||||||
|
title={rightVisible ? "收起属性面板(])" : "展开属性面板(])"}
|
||||||
|
type="button"
|
||||||
|
onClick={toggleRightPanelVisible}
|
||||||
|
>
|
||||||
|
{rightVisible ? <PanelRightClose {...HANDLE_ICON_PROPS} /> : <PanelRightOpen {...HANDLE_ICON_PROPS} />}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { forwardRef } from "react";
|
import { forwardRef } from "react";
|
||||||
import { fireEvent, render, screen, within } from "@testing-library/react";
|
import { act, fireEvent, render, screen, within } from "@testing-library/react";
|
||||||
import { afterEach, beforeEach, vi } from "vitest";
|
import { afterEach, beforeEach, vi } from "vitest";
|
||||||
import { DoubleSide, PerspectiveCamera, Vector3 } from "three";
|
import { DoubleSide, PerspectiveCamera, Vector3 } from "three";
|
||||||
import { clearViewportCaptureHandler, requestViewportCapture } from "../io/captureBridge";
|
import { clearViewportCaptureHandler, requestViewportCapture } from "../io/captureBridge";
|
||||||
@@ -484,6 +484,32 @@ it("offsets the native viewport gizmo inward when overlay side panels are open",
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("pulls the viewport overlays to the edge when only the right panel is collapsed", () => {
|
||||||
|
act(() => {
|
||||||
|
useDirectorStore.setState({
|
||||||
|
...useDirectorStore.getState(),
|
||||||
|
viewMode: "director",
|
||||||
|
viewportPanelsCollapsed: false,
|
||||||
|
leftPanelVisible: true,
|
||||||
|
rightPanelVisible: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<App />);
|
||||||
|
|
||||||
|
expect(screen.getByLabelText("3D视口原生坐标控件")).toHaveStyle({ right: "332px" });
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
useDirectorStore.getState().toggleRightPanelVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 右侧面板收起后这一侧整块让给画布:坐标控件 / 重置视角 / 镜头监视器都要贴到边缘,
|
||||||
|
// 不能只看「专注模式」总开关
|
||||||
|
expect(screen.getByLabelText("3D视口原生坐标控件")).toHaveStyle({ right: "20px" });
|
||||||
|
expect(document.querySelector(".viewport-view-reset")).toHaveStyle({ right: "20px" });
|
||||||
|
expect(screen.getByLabelText("镜头监视器")).toHaveStyle({ right: "20px" });
|
||||||
|
});
|
||||||
|
|
||||||
it("resets the director view from the button below the native gizmo", () => {
|
it("resets the director view from the button below the native gizmo", () => {
|
||||||
useDirectorStore.getState().setViewMode("camera");
|
useDirectorStore.getState().setViewMode("camera");
|
||||||
render(<App />);
|
render(<App />);
|
||||||
|
|||||||
@@ -935,6 +935,8 @@ export function DirectorCanvas() {
|
|||||||
const viewportAspectRatio = useDirectorStore((state) => state.viewportAspectRatio);
|
const viewportAspectRatio = useDirectorStore((state) => state.viewportAspectRatio);
|
||||||
const viewportRuleOfThirdsEnabled = useDirectorStore((state) => state.viewportRuleOfThirdsEnabled);
|
const viewportRuleOfThirdsEnabled = useDirectorStore((state) => state.viewportRuleOfThirdsEnabled);
|
||||||
const viewportPanelsCollapsed = useDirectorStore((state) => state.viewportPanelsCollapsed);
|
const viewportPanelsCollapsed = useDirectorStore((state) => state.viewportPanelsCollapsed);
|
||||||
|
const leftPanelVisible = useDirectorStore((state) => state.leftPanelVisible);
|
||||||
|
const rightPanelVisible = useDirectorStore((state) => state.rightPanelVisible);
|
||||||
const trajectoryPlacementObjectId = useDirectorStore((state) => state.trajectoryPlacementObjectId);
|
const trajectoryPlacementObjectId = useDirectorStore((state) => state.trajectoryPlacementObjectId);
|
||||||
const setViewMode = useDirectorStore((state) => state.setViewMode);
|
const setViewMode = useDirectorStore((state) => state.setViewMode);
|
||||||
const setViewportRuleOfThirdsEnabled = useDirectorStore((state) => state.setViewportRuleOfThirdsEnabled);
|
const setViewportRuleOfThirdsEnabled = useDirectorStore((state) => state.setViewportRuleOfThirdsEnabled);
|
||||||
@@ -947,17 +949,18 @@ export function DirectorCanvas() {
|
|||||||
target: getCameraLookPoint(activeCameraWorldView.position, activeCameraWorldView.lookRotation),
|
target: getCameraLookPoint(activeCameraWorldView.position, activeCameraWorldView.lookRotation),
|
||||||
}
|
}
|
||||||
: directorViewSnapshot;
|
: directorViewSnapshot;
|
||||||
const viewportSafeAreaInsets: ViewportSafeAreaInsets = viewportPanelsCollapsed
|
// 某一侧面板收起后,画布这一侧就整块让出来了:坐标控件 / 重置视角 / 镜头监视器、
|
||||||
? { left: 0, right: 0, top: 0, bottom: 0 }
|
// 以及画幅安全区都得跟着贴到新的画布边缘。判据是「这一侧是否真的显示」,
|
||||||
: {
|
// 不能只看「专注模式」总开关 —— 单独按 [ / ] 收起一侧时总开关并没有变。
|
||||||
left: LEFT_PANEL_WIDTH + FLOATING_PANEL_GAP,
|
const leftPanelShown = !viewportPanelsCollapsed && leftPanelVisible;
|
||||||
right: RIGHT_PANEL_WIDTH + FLOATING_PANEL_GAP,
|
const rightPanelShown = !viewportPanelsCollapsed && rightPanelVisible;
|
||||||
top: 0,
|
const viewportSafeAreaInsets: ViewportSafeAreaInsets = {
|
||||||
bottom: 0,
|
left: leftPanelShown ? LEFT_PANEL_WIDTH + FLOATING_PANEL_GAP : 0,
|
||||||
};
|
right: rightPanelShown ? RIGHT_PANEL_WIDTH + FLOATING_PANEL_GAP : 0,
|
||||||
const gizmoRightOffset = viewportPanelsCollapsed
|
top: 0,
|
||||||
? GIZMO_EDGE_PADDING
|
bottom: 0,
|
||||||
: RIGHT_PANEL_WIDTH + FLOATING_PANEL_GAP;
|
};
|
||||||
|
const gizmoRightOffset = rightPanelShown ? RIGHT_PANEL_WIDTH + FLOATING_PANEL_GAP : GIZMO_EDGE_PADDING;
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const element = toolbarRef.current;
|
const element = toolbarRef.current;
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ it("renders the viewport capsule as project icon-system buttons", () => {
|
|||||||
"当前视角截图",
|
"当前视角截图",
|
||||||
"四方位截图",
|
"四方位截图",
|
||||||
"十二方位截图",
|
"十二方位截图",
|
||||||
"全屏",
|
"专注模式",
|
||||||
"添加轨迹",
|
"添加轨迹",
|
||||||
"录制",
|
"录制",
|
||||||
];
|
];
|
||||||
@@ -113,7 +113,7 @@ it("uses the requested viewport toolbar SVG icons for camera and capture actions
|
|||||||
expect(screen.getByRole("button", { name: "十二方位截图" }).querySelector("svg")).toHaveClass("lucide-grid3x3");
|
expect(screen.getByRole("button", { name: "十二方位截图" }).querySelector("svg")).toHaveClass("lucide-grid3x3");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses the fullscreen button to collapse the side panels instead of entering browser fullscreen", async () => {
|
it("uses the focus-mode button to collapse the side panels instead of entering browser fullscreen", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<ViewportToolbar />);
|
render(<ViewportToolbar />);
|
||||||
|
|
||||||
@@ -121,7 +121,7 @@ it("uses the fullscreen button to collapse the side panels instead of entering b
|
|||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
await user.click(screen.getByRole("button", { name: "全屏" }));
|
await user.click(screen.getByRole("button", { name: "专注模式" }));
|
||||||
|
|
||||||
expect((useDirectorStore.getState() as { viewportPanelsCollapsed?: boolean }).viewportPanelsCollapsed ?? false).toBe(
|
expect((useDirectorStore.getState() as { viewportPanelsCollapsed?: boolean }).viewportPanelsCollapsed ?? false).toBe(
|
||||||
true
|
true
|
||||||
|
|||||||
@@ -143,7 +143,11 @@ export function ViewportToolbar({
|
|||||||
const setViewMode = useDirectorStore((state) => state.setViewMode);
|
const setViewMode = useDirectorStore((state) => state.setViewMode);
|
||||||
const setTransformMode = useDirectorStore((state) => state.setTransformMode);
|
const setTransformMode = useDirectorStore((state) => state.setTransformMode);
|
||||||
const setViewportAspectRatio = useDirectorStore((state) => state.setViewportAspectRatio);
|
const setViewportAspectRatio = useDirectorStore((state) => state.setViewportAspectRatio);
|
||||||
const toggleViewportPanelsCollapsed = useDirectorStore((state) => state.toggleViewportPanelsCollapsed);
|
const setViewportPanelsCollapsed = useDirectorStore((state) => state.setViewportPanelsCollapsed);
|
||||||
|
// 左右侧栏是否都不见了:既可能是「专注模式」,也可能是被 [ / ] 各自收起
|
||||||
|
const sidePanelsHidden = useDirectorStore(
|
||||||
|
(state) => state.viewportPanelsCollapsed || (!state.leftPanelVisible && !state.rightPanelVisible)
|
||||||
|
);
|
||||||
const project = useDirectorStore((state) => state.project);
|
const project = useDirectorStore((state) => state.project);
|
||||||
const selectedObjectId = useDirectorStore((state) => state.selectedObjectId);
|
const selectedObjectId = useDirectorStore((state) => state.selectedObjectId);
|
||||||
const timelineVisible = useDirectorStore((state) => state.timelineVisible);
|
const timelineVisible = useDirectorStore((state) => state.timelineVisible);
|
||||||
@@ -511,7 +515,15 @@ export function ViewportToolbar({
|
|||||||
{ label: "当前视角截图", icon: ScanLine, onClick: () => void handleCapture("current") },
|
{ label: "当前视角截图", icon: ScanLine, onClick: () => void handleCapture("current") },
|
||||||
{ label: "四方位截图", icon: Grid2X2, onClick: () => void handleCapture("four") },
|
{ label: "四方位截图", icon: Grid2X2, onClick: () => void handleCapture("four") },
|
||||||
{ label: "十二方位截图", icon: Grid3X3, onClick: () => void handleCapture("twelve") },
|
{ label: "十二方位截图", icon: Grid3X3, onClick: () => void handleCapture("twelve") },
|
||||||
{ label: "全屏", icon: Expand, onClick: toggleViewportPanelsCollapsed },
|
{
|
||||||
|
label: "专注模式",
|
||||||
|
icon: Expand,
|
||||||
|
pressed: sidePanelsHidden,
|
||||||
|
title: sidePanelsHidden
|
||||||
|
? "恢复左右面板(可用 [ 左侧 / ] 右侧 单独切换一侧)"
|
||||||
|
: "专注模式:一次收起左右面板,只看画布(可用 [ 左侧 / ] 右侧 单独切换一侧)",
|
||||||
|
onClick: () => setViewportPanelsCollapsed(!sidePanelsHidden),
|
||||||
|
},
|
||||||
// 打点 / 录制放在最右侧
|
// 打点 / 录制放在最右侧
|
||||||
{
|
{
|
||||||
label: "添加轨迹",
|
label: "添加轨迹",
|
||||||
|
|||||||
@@ -91,6 +91,30 @@ it("toggles the viewport side panel collapse flag in ui state", () => {
|
|||||||
expect((useDirectorStore.getState() as CollapseUiState).viewportPanelsCollapsed ?? false).toBe(true);
|
expect((useDirectorStore.getState() as CollapseUiState).viewportPanelsCollapsed ?? false).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("toggles the left and right side panels independently", () => {
|
||||||
|
useDirectorStore.setState(createInitialDirectorState());
|
||||||
|
|
||||||
|
useDirectorStore.getState().toggleLeftPanelVisible();
|
||||||
|
|
||||||
|
const state = useDirectorStore.getState();
|
||||||
|
expect(state.leftPanelVisible).toBe(false);
|
||||||
|
// 只收起左侧,右侧不受影响
|
||||||
|
expect(state.rightPanelVisible).toBe(true);
|
||||||
|
// 单侧收起不改变「专注模式」标记
|
||||||
|
expect(state.viewportPanelsCollapsed).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("leaves focus mode and keeps only the toggled side when a panel switch is used", () => {
|
||||||
|
useDirectorStore.setState({ ...createInitialDirectorState(), viewportPanelsCollapsed: true });
|
||||||
|
|
||||||
|
useDirectorStore.getState().toggleRightPanelVisible();
|
||||||
|
|
||||||
|
const state = useDirectorStore.getState();
|
||||||
|
expect(state.viewportPanelsCollapsed).toBe(false);
|
||||||
|
expect(state.leftPanelVisible).toBe(false);
|
||||||
|
expect(state.rightPanelVisible).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("routes the right panel by object type and view mode", () => {
|
it("routes the right panel by object type and view mode", () => {
|
||||||
const state = createInitialDirectorState();
|
const state = createInitialDirectorState();
|
||||||
const characterId = state.project.objects.find((item) => item.kind === "character")!.id;
|
const characterId = state.project.objects.find((item) => item.kind === "character")!.id;
|
||||||
|
|||||||
@@ -140,7 +140,12 @@ export interface DirectorUiState {
|
|||||||
transformMode: TransformMode;
|
transformMode: TransformMode;
|
||||||
viewportAspectRatio: ViewportAspectRatio;
|
viewportAspectRatio: ViewportAspectRatio;
|
||||||
viewportRuleOfThirdsEnabled: boolean;
|
viewportRuleOfThirdsEnabled: boolean;
|
||||||
|
/** 「专注模式」:一次性收起左右两侧栏。与左右各自的显隐开关相与后决定是否渲染。 */
|
||||||
viewportPanelsCollapsed: boolean;
|
viewportPanelsCollapsed: boolean;
|
||||||
|
/** 左侧「场景」面板是否显示(专注模式下会被一并收起) */
|
||||||
|
leftPanelVisible: boolean;
|
||||||
|
/** 右侧「属性」面板是否显示(专注模式下会被一并收起) */
|
||||||
|
rightPanelVisible: boolean;
|
||||||
/** 悬浮时间轴(录制面板)是否显示,由视口快捷工具条的「录制」按钮切换 */
|
/** 悬浮时间轴(录制面板)是否显示,由视口快捷工具条的「录制」按钮切换 */
|
||||||
timelineVisible: boolean;
|
timelineVisible: boolean;
|
||||||
/** 用户自定义的时间轴轨道顺序(轨道 key 列表)。
|
/** 用户自定义的时间轴轨道顺序(轨道 key 列表)。
|
||||||
@@ -183,6 +188,10 @@ export interface DirectorActions {
|
|||||||
setViewportRuleOfThirdsEnabled: (enabled: boolean) => void;
|
setViewportRuleOfThirdsEnabled: (enabled: boolean) => void;
|
||||||
toggleViewportPanelsCollapsed: () => void;
|
toggleViewportPanelsCollapsed: () => void;
|
||||||
setViewportPanelsCollapsed: (collapsed: boolean) => void;
|
setViewportPanelsCollapsed: (collapsed: boolean) => void;
|
||||||
|
/** 单独收起 / 展开左侧「场景」面板 */
|
||||||
|
toggleLeftPanelVisible: () => void;
|
||||||
|
/** 单独收起 / 展开右侧「属性」面板 */
|
||||||
|
toggleRightPanelVisible: () => void;
|
||||||
toggleTimelineVisible: () => void;
|
toggleTimelineVisible: () => void;
|
||||||
setTimelineVisible: (visible: boolean) => void;
|
setTimelineVisible: (visible: boolean) => void;
|
||||||
/** 覆盖时间轴轨道顺序(传入解析后的完整顺序) */
|
/** 覆盖时间轴轨道顺序(传入解析后的完整顺序) */
|
||||||
@@ -435,6 +444,8 @@ const DEFAULT_UI_STATE: DirectorUiState = {
|
|||||||
viewportAspectRatio: "auto",
|
viewportAspectRatio: "auto",
|
||||||
viewportRuleOfThirdsEnabled: false,
|
viewportRuleOfThirdsEnabled: false,
|
||||||
viewportPanelsCollapsed: false,
|
viewportPanelsCollapsed: false,
|
||||||
|
leftPanelVisible: true,
|
||||||
|
rightPanelVisible: true,
|
||||||
timelineVisible: true,
|
timelineVisible: true,
|
||||||
timelineTrackOrder: [],
|
timelineTrackOrder: [],
|
||||||
timelineCollapsedTracks: [],
|
timelineCollapsedTracks: [],
|
||||||
@@ -917,6 +928,8 @@ function extractPersistedDirectorState(state: DirectorRuntimeState): DirectorSta
|
|||||||
viewportAspectRatio: state.viewportAspectRatio,
|
viewportAspectRatio: state.viewportAspectRatio,
|
||||||
viewportRuleOfThirdsEnabled: state.viewportRuleOfThirdsEnabled,
|
viewportRuleOfThirdsEnabled: state.viewportRuleOfThirdsEnabled,
|
||||||
viewportPanelsCollapsed: state.viewportPanelsCollapsed,
|
viewportPanelsCollapsed: state.viewportPanelsCollapsed,
|
||||||
|
leftPanelVisible: state.leftPanelVisible,
|
||||||
|
rightPanelVisible: state.rightPanelVisible,
|
||||||
timelineVisible: state.timelineVisible,
|
timelineVisible: state.timelineVisible,
|
||||||
timelineTrackOrder: state.timelineTrackOrder,
|
timelineTrackOrder: state.timelineTrackOrder,
|
||||||
timelineCollapsedTracks: state.timelineCollapsedTracks,
|
timelineCollapsedTracks: state.timelineCollapsedTracks,
|
||||||
@@ -981,6 +994,8 @@ function readPersistedDirectorState(options: DirectorStateOptions = {}): Directo
|
|||||||
viewportAspectRatio: state.viewportAspectRatio ?? "auto",
|
viewportAspectRatio: state.viewportAspectRatio ?? "auto",
|
||||||
viewportRuleOfThirdsEnabled: Boolean(state.viewportRuleOfThirdsEnabled),
|
viewportRuleOfThirdsEnabled: Boolean(state.viewportRuleOfThirdsEnabled),
|
||||||
viewportPanelsCollapsed: Boolean(state.viewportPanelsCollapsed),
|
viewportPanelsCollapsed: Boolean(state.viewportPanelsCollapsed),
|
||||||
|
leftPanelVisible: state.leftPanelVisible === false ? false : true,
|
||||||
|
rightPanelVisible: state.rightPanelVisible === false ? false : true,
|
||||||
timelineVisible: state.timelineVisible === false ? false : true,
|
timelineVisible: state.timelineVisible === false ? false : true,
|
||||||
timelineTrackOrder: readPersistedStringArray(state.timelineTrackOrder),
|
timelineTrackOrder: readPersistedStringArray(state.timelineTrackOrder),
|
||||||
timelineCollapsedTracks: readPersistedStringArray(state.timelineCollapsedTracks),
|
timelineCollapsedTracks: readPersistedStringArray(state.timelineCollapsedTracks),
|
||||||
@@ -2160,7 +2175,27 @@ export const useDirectorStore = create<DirectorStore>((set, get) => {
|
|||||||
commitUiMutation((state) => ({
|
commitUiMutation((state) => ({
|
||||||
...state,
|
...state,
|
||||||
viewportPanelsCollapsed: collapsed,
|
viewportPanelsCollapsed: collapsed,
|
||||||
|
// 退出专注模式时把两侧还原成可见,避免「点了展开却没反应」
|
||||||
|
leftPanelVisible: collapsed ? state.leftPanelVisible : true,
|
||||||
|
rightPanelVisible: collapsed ? state.rightPanelVisible : true,
|
||||||
})),
|
})),
|
||||||
|
toggleLeftPanelVisible: () =>
|
||||||
|
commitUiMutation((state) => {
|
||||||
|
// 专注模式下切换某一侧:退出专注模式,只保留这一侧,避免「按了没反应」
|
||||||
|
if (state.viewportPanelsCollapsed) {
|
||||||
|
return { ...state, viewportPanelsCollapsed: false, leftPanelVisible: true, rightPanelVisible: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...state, leftPanelVisible: !state.leftPanelVisible };
|
||||||
|
}),
|
||||||
|
toggleRightPanelVisible: () =>
|
||||||
|
commitUiMutation((state) => {
|
||||||
|
if (state.viewportPanelsCollapsed) {
|
||||||
|
return { ...state, viewportPanelsCollapsed: false, leftPanelVisible: false, rightPanelVisible: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...state, rightPanelVisible: !state.rightPanelVisible };
|
||||||
|
}),
|
||||||
toggleTimelineVisible: () =>
|
toggleTimelineVisible: () =>
|
||||||
commitUiMutation((state) => ({
|
commitUiMutation((state) => ({
|
||||||
...state,
|
...state,
|
||||||
|
|||||||
@@ -18,10 +18,11 @@
|
|||||||
--right-sidebar-width: 300px;
|
--right-sidebar-width: 300px;
|
||||||
--left-sidebar-content-width: 260px;
|
--left-sidebar-content-width: 260px;
|
||||||
--right-sidebar-content-width: 260px;
|
--right-sidebar-content-width: 260px;
|
||||||
/* 悬浮卡片:与视口边缘的间距、以及让开顶部工具条的起始高度 */
|
/* 悬浮卡片与视口边缘的间距(左右、以及相对时间轴的上下留白都用它),
|
||||||
|
卡片顶部同样取这个值,四周留白保持一致 */
|
||||||
--floating-inset: 16px;
|
--floating-inset: 16px;
|
||||||
--floating-top: 76px;
|
--floating-top: var(--floating-inset);
|
||||||
/* 悬浮卡片可用高度上限:扣掉顶部工具条、上下内缩、以及悬浮时间轴占用的高度 */
|
/* 悬浮卡片可用高度上限:扣掉卡片起始高度、上下内缩、以及悬浮时间轴占用的高度 */
|
||||||
--floating-card-max-height: calc(100vh - var(--floating-top) - var(--floating-inset) * 2 - var(--timeline-panel-height, 0px));
|
--floating-card-max-height: calc(100vh - var(--floating-top) - var(--floating-inset) * 2 - var(--timeline-panel-height, 0px));
|
||||||
/* 视口快捷工具条高度(ViewportToolbar 会按实测值覆盖),供悬浮时间轴让位使用 */
|
/* 视口快捷工具条高度(ViewportToolbar 会按实测值覆盖),供悬浮时间轴让位使用 */
|
||||||
--viewport-toolbar-height: 46px;
|
--viewport-toolbar-height: 46px;
|
||||||
@@ -379,7 +380,9 @@ textarea:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.director-shell-fullbleed.is-sidebars-collapsed > .left-sidebar,
|
.director-shell-fullbleed.is-sidebars-collapsed > .left-sidebar,
|
||||||
.director-shell-fullbleed.is-sidebars-collapsed > .right-sidebar {
|
.director-shell-fullbleed.is-sidebars-collapsed > .right-sidebar,
|
||||||
|
.director-shell-fullbleed.is-left-collapsed > .left-sidebar,
|
||||||
|
.director-shell-fullbleed.is-right-collapsed > .right-sidebar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3576,6 +3579,15 @@ button.inspector-dropdown-option.is-selected {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 窄屏下右侧栏本就强制隐藏,连它的手柄一起去掉,免得出现「点了没反应」的控件 */
|
||||||
|
.panel-edge-handle-right {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-edge-handle-left {
|
||||||
|
left: calc(var(--floating-inset) + min(220px, 28vw) - 11px);
|
||||||
|
}
|
||||||
|
|
||||||
.viewport-toolbar {
|
.viewport-toolbar {
|
||||||
max-width: calc(100% - 32px);
|
max-width: calc(100% - 32px);
|
||||||
}
|
}
|
||||||
@@ -5035,9 +5047,9 @@ body.is-timeline-dragging * {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ===== 全屏画布 + 悬浮卡片 =====
|
/* ===== 全屏画布 + 悬浮卡片 =====
|
||||||
画布铺满整屏;顶部工具条、左右侧栏、底部时间轴一律脱离文档流悬浮在画布之上。
|
画布铺满整屏;顶部工具条、左右侧栏、底部时间轴一律脱离文档流悬浮在画布之上。
|
||||||
1) 顶部工具条改为绝对定位的透明浮层,只有标题/视角切换/关闭按钮可点击,其余区域让给画布。
|
1) 顶部工具条改为绝对定位的透明浮层,只有视角切换按钮可点击,其余区域让给画布。
|
||||||
2) 右侧栏脱离第 1 行,直接落在 1fr 行上,从而占满整屏高度。 */
|
2) 右侧栏脱离第 1 行,直接落在 1fr 行上,从而占满整屏高度。 */
|
||||||
.app-shell > .director-shell {
|
.app-shell > .director-shell {
|
||||||
grid-row: 2;
|
grid-row: 2;
|
||||||
}
|
}
|
||||||
@@ -5053,8 +5065,13 @@ body.is-timeline-dragging * {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-bar-left,
|
/* 只有视角切换按钮需要接管点击:整格可点会连带吃掉画布顶部中段、
|
||||||
|
以及延伸到顶部的侧栏边缘,所以这里收到按钮本身 */
|
||||||
.top-bar-center {
|
.top-bar-center {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-bar-center .mode-toggle {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5066,7 +5083,7 @@ body.is-timeline-dragging * {
|
|||||||
-webkit-backdrop-filter: blur(32px);
|
-webkit-backdrop-filter: blur(32px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 左右侧栏:从贴边全高面板改成内缩、按内容包裹的圆角悬浮卡片
|
/* 左右侧栏:从贴边全高面板改成内缩、按内容包裹的悬浮卡片,顶部延伸到视口顶端
|
||||||
bottom 置为 auto + max-height 封顶 → 内容少时卡片只有内容那么高,内容多时卡片内部滚动 */
|
bottom 置为 auto + max-height 封顶 → 内容少时卡片只有内容那么高,内容多时卡片内部滚动 */
|
||||||
.director-shell-fullbleed .left-sidebar,
|
.director-shell-fullbleed .left-sidebar,
|
||||||
.director-shell-fullbleed .right-sidebar {
|
.director-shell-fullbleed .right-sidebar {
|
||||||
@@ -5096,6 +5113,53 @@ body.is-timeline-dragging * {
|
|||||||
right: var(--floating-inset);
|
right: var(--floating-inset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 侧栏折叠手柄:展开时半嵌在卡片外缘(落在卡片 20px 内边距里,不压内容),
|
||||||
|
收起后缩到视口边上,只占一条 22px 的窄边,把画布整块让出来。
|
||||||
|
z-index 高于侧栏(25) / 时间轴(30),低于顶部条(45)。 */
|
||||||
|
.panel-edge-handle {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(var(--floating-top) + 18px);
|
||||||
|
z-index: 40;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 22px;
|
||||||
|
height: 48px;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid rgb(var(--border-rgb) / 0.42);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: rgb(var(--text-muted-rgb));
|
||||||
|
background: rgb(var(--panel-rgb) / 0.9);
|
||||||
|
box-shadow: var(--ui-shadow-panel);
|
||||||
|
backdrop-filter: blur(32px);
|
||||||
|
-webkit-backdrop-filter: blur(32px);
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
color 0.15s ease,
|
||||||
|
background 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-edge-handle:hover,
|
||||||
|
.panel-edge-handle:focus-visible {
|
||||||
|
color: rgb(var(--text-rgb));
|
||||||
|
background: rgb(var(--panel-rgb) / 0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-edge-handle-left {
|
||||||
|
left: calc(var(--floating-inset) + var(--left-sidebar-width) - 11px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-edge-handle-right {
|
||||||
|
right: calc(var(--floating-inset) + var(--right-sidebar-width) - 11px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.director-shell-fullbleed.is-left-collapsed > .panel-edge-handle-left {
|
||||||
|
left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.director-shell-fullbleed.is-right-collapsed > .panel-edge-handle-right {
|
||||||
|
right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
/* 卡片自身半透明,内部面板背景需让位,避免把悬浮磨砂底压成不透明;
|
/* 卡片自身半透明,内部面板背景需让位,避免把悬浮磨砂底压成不透明;
|
||||||
同时把内层依赖父级高度的 100% 显式改成跟随内容,配合卡片「按内容包裹」 */
|
同时把内层依赖父级高度的 100% 显式改成跟随内容,配合卡片「按内容包裹」 */
|
||||||
.director-shell-fullbleed .left-sidebar .object-tree-panel {
|
.director-shell-fullbleed .left-sidebar .object-tree-panel {
|
||||||
|
|||||||
Reference in New Issue
Block a user