mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-31 23:47:51 +00:00
Scene Builder fixes: record-mode camera lock, Blender default framing, auto-keyframe
- Record mode is now fully immutable: gate MouseControls mouse-look + focus() on the viewport lock (new MouseControlsDeps.isViewportLocked → cameraController .locked), matching the existing FreeCam/tick gates. - Default main (viewport) camera pulled back + elevated to a Blender-style 3/4 view so a blank scene shows the full render-camera frustum. - Auto-keyframe: mutating an already-keyframed object (gizmo drag-end or panel commit) replaces/creates a keyframe at the playhead via TimelineController.autoKeyIfTracked. - loadTimeline backfills fps/duration/tracks defensively (legacy save safety).
This commit is contained in:
1 parent
26aa3d3839
commit
11c7aa979d
6 files changed
+56
-3
No files matched your search
@@ -12,6 +12,20 @@ components that host these canvases** (`SceneContainer`, `EditorCanvas`, `Previe
|
||||
for mode/chrome gating — hide them with CSS (`hidden`) instead. (Regression fixed: record mode
|
||||
had gated `PreviewEngineCamera` behind `!isRecord`, wiping the scene on record→build toggle.)
|
||||
|
||||
## Behavior notes (fixes)
|
||||
|
||||
- **Record mode is fully immutable.** The viewport lock (`CameraController.locked`, set by the
|
||||
record effect) gates every camera-move path: FreeCam drag/wheel/keyboard (`useFreeCam`),
|
||||
FreeCam integration (`tickPerFrame`), AND `MouseControls` mouse-look + `focus()` (gated via
|
||||
`MouseControlsDeps.isViewportLocked`). If you add a new camera-move path, gate it on `locked`.
|
||||
- **Default camera framing** (`DEFAULT_CAMERAS.main`): a Blender-style pulled-back/elevated 3/4
|
||||
view (`pos (-4.5,4,6)`, `lookAt (0,0.5,0.6)`) so the whole render-camera (`cam2`) frustum is
|
||||
visible on a blank scene.
|
||||
- **Auto-keyframe**: mutating an **already-keyframed** object (gizmo drag-end or inspector commit)
|
||||
calls `TimelineController.autoKeyIfTracked(uuid)` → `addKeyframe` at the playhead, which
|
||||
replace-or-creates a keyframe at the current scrub point. No-op for never-keyframed objects
|
||||
(first keyframe is always explicit).
|
||||
|
||||
## Architecture (existing, do not rebuild)
|
||||
|
||||
Strict one-way data flow. Do not violate it.
|
||||
|
||||
@@ -131,11 +131,13 @@ export interface EditorLoader {
|
||||
const DEFAULT_CAMERAS: Camera[] = [
|
||||
{
|
||||
id: "main",
|
||||
// Blender-style default: viewport pulled back + elevated at a 3/4 angle so
|
||||
// the whole render-camera frustum (cam2, below) is visible in the scene.
|
||||
label: "Main View",
|
||||
focalLength: 17,
|
||||
position: { x: -2.5, y: 2.5, z: 2.5 },
|
||||
position: { x: -4.5, y: 4, z: 6 },
|
||||
rotation: { x: 0, y: 0, z: 0 },
|
||||
lookAt: { x: 0, y: 0, z: 0 },
|
||||
lookAt: { x: 0, y: 0.5, z: 0.6 },
|
||||
},
|
||||
{
|
||||
id: "cam2",
|
||||
|
||||
@@ -29,6 +29,9 @@ export function beginTransformSession(
|
||||
commit() {
|
||||
if (action.commit()) {
|
||||
editor.history.record(action);
|
||||
// Auto-key: an already-keyframed object edited via the panel updates
|
||||
// its keyframe at the playhead (or creates one at the scrub point).
|
||||
editor.timelineController.autoKeyIfTracked(uuid);
|
||||
}
|
||||
// Push the engine's post-edit transform back into the Zustand
|
||||
// objectPanel slice so the panel's currentSceneObject vectors
|
||||
|
||||
@@ -526,6 +526,11 @@ class Editor {
|
||||
} else if (this.activeTransform) {
|
||||
if (this.activeTransform.commit()) {
|
||||
this.history.record(this.activeTransform);
|
||||
// Auto-key: if the dragged object is already keyframed, record
|
||||
// its new transform at the playhead (replaces the keyframe there
|
||||
// or creates one at the current scrub point).
|
||||
const uuid = this.sceneManager?.selected_objects?.[0]?.uuid;
|
||||
if (uuid) this.timelineController.autoKeyIfTracked(uuid);
|
||||
}
|
||||
this.activeTransform = null;
|
||||
}
|
||||
@@ -575,6 +580,7 @@ class Editor {
|
||||
isHotkeyDisabled: () =>
|
||||
usePageSceneStore.getState().hotkeyStatus.disabled,
|
||||
getTransformSpace: () => usePageSceneStore.getState().transformSpace,
|
||||
isViewportLocked: () => this.cameraController.locked,
|
||||
});
|
||||
|
||||
this.sceneManager = new SceneManager(
|
||||
|
||||
@@ -72,7 +72,16 @@ export class TimelineController {
|
||||
// Replace the live timeline wholesale (used by undo/redo of a save).
|
||||
loadTimeline(data: TimelineData): void {
|
||||
this.timeline = cloneTimeline(data);
|
||||
this.saved = cloneTimeline(data);
|
||||
// Backfill defensively so legacy/partial saved timelines don't break the
|
||||
// frame-quantized UI (which assumes a valid fps) or produce a 0 duration.
|
||||
if (!this.timeline.fps || this.timeline.fps <= 0) {
|
||||
this.timeline.fps = DEFAULT_TIMELINE_FPS;
|
||||
}
|
||||
if (!this.timeline.duration || this.timeline.duration <= 0) {
|
||||
this.timeline.duration = DEFAULT_TIMELINE_DURATION;
|
||||
}
|
||||
if (!Array.isArray(this.timeline.tracks)) this.timeline.tracks = [];
|
||||
this.saved = cloneTimeline(this.timeline);
|
||||
this.playhead = Math.min(this.playhead, this.timeline.duration);
|
||||
this.evaluate();
|
||||
this.emitChanged();
|
||||
@@ -126,6 +135,19 @@ export class TimelineController {
|
||||
|
||||
// ─── keyframe editing ──────────────────────────────────────────────────
|
||||
|
||||
// Auto-key: when an ALREADY-keyframed object is mutated, capture its current
|
||||
// transform at the playhead — replacing the keyframe there or creating one at
|
||||
// the current scrub point. No-op for objects that have never been keyframed
|
||||
// (the first keyframe is always added explicitly).
|
||||
autoKeyIfTracked(objectUuid: string): void {
|
||||
if (!this.timeline) return;
|
||||
const track = this.timeline.tracks.find(
|
||||
(t) => t.objectUuid === objectUuid,
|
||||
);
|
||||
if (!track || track.keyframes.length === 0) return;
|
||||
this.addKeyframe(objectUuid, this.playhead);
|
||||
}
|
||||
|
||||
addKeyframe(objectUuid: string, atTime?: number): void {
|
||||
if (!this.timeline) return;
|
||||
const obj = this.editor.activeScene.scene.getObjectByProperty(
|
||||
|
||||
@@ -95,6 +95,9 @@ export type MouseControlsDeps = {
|
||||
getPoseMode: () => PoseMode;
|
||||
isHotkeyDisabled: () => boolean;
|
||||
getTransformSpace: () => TransformSpace;
|
||||
// True when the viewport is locked (record mode) — camera look / focus
|
||||
// must be fully inert so record playback is immutable.
|
||||
isViewportLocked: () => boolean;
|
||||
};
|
||||
|
||||
export class MouseControls {
|
||||
@@ -189,6 +192,7 @@ export class MouseControls {
|
||||
}
|
||||
|
||||
focus() {
|
||||
if (this.deps.isViewportLocked()) return;
|
||||
if (this.lockControls && this.selected) {
|
||||
this.lockControls.camera.lookAt(this.selected[0].position);
|
||||
this.lockControls.camera.position.copy(this.selected[0].position);
|
||||
@@ -499,6 +503,8 @@ export class MouseControls {
|
||||
}
|
||||
|
||||
handleMouseManualLock(event: MouseEvent) {
|
||||
// Record mode locks the viewport — no mouse-look camera rotation.
|
||||
if (this.deps.isViewportLocked()) return;
|
||||
if (this.isMouseClicked && this.lockControls) {
|
||||
// If the mouse is clicked and the lockControls is not locked, lock it
|
||||
if (this.lockControls.isLocked == false) {
|
||||
|
||||
Reference in new issue
Block a user