cozyclay 3.0.3 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +93 -0
- package/INSTALL_FOR_AGENTS.md +146 -0
- package/README.md +74 -192
- package/THIRD_PARTY_NOTICES.md +0 -21
- package/bin/agent/agent-routes.mjs +29 -62
- package/bin/agent/agent-runner.mjs +5 -3
- package/bin/agent/main-agent.mjs +70 -0
- package/bin/agent/main-prompt.mjs +17 -0
- package/bin/agent/plan-tool.mjs +114 -0
- package/bin/agent/script-tool.mjs +2 -0
- package/bin/agent/sculpt-sketch.mjs +190 -0
- package/bin/agent/session-store.mjs +4 -3
- package/bin/agent/sketch-code.mjs +151 -0
- package/bin/agent/sketch-sandbox.mjs +102 -0
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-task-routes.mjs +185 -0
- package/bin/agent/studio-tasks.mjs +156 -0
- package/bin/agent/studio-tools.mjs +3 -2
- package/bin/agent/studio-turn-transaction.mjs +76 -0
- package/bin/agent/turn-frames.mjs +4 -0
- package/bin/cozyclay.mjs +5 -7
- package/dist/app/index.html +7 -6
- package/dist/assets/TaskBoard-ngdqiu7u.js +1 -0
- package/dist/assets/app-BlwOpest.css +1 -0
- package/dist/assets/app-DmR7jBkz.js +5144 -0
- package/dist/assets/compile-worker-D0pKioa0.js +5 -0
- package/dist/assets/landing-y3EqDgSZ.js +1 -0
- package/dist/assets/modulepreload-polyfill-P2Xu9kJm.js +1 -0
- package/dist/assets/motion3-compile-BB3_1-EB.js +1 -0
- package/dist/assets/scene-asset-sync-CBx7XRti.css +1 -0
- package/dist/assets/scene-asset-sync-Cgdtr1Yw.js +13 -0
- package/dist/assets/task-board-ErQvfoaz.js +14 -0
- package/dist/assets/workflow-zQM-EKW1.js +179 -0
- package/dist/assets/x-bot-tpose-body-BusyR3eg.json +1 -0
- package/dist/assets/y-bot-tpose-body-Dy1VKChZ.json +1 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/greybox-to-video/index.html +2 -3
- package/dist/index.html +7 -57
- package/dist/llms.txt +2 -1
- package/dist/previs-software/index.html +2 -2
- package/dist/privacy/index.html +3 -7
- package/dist/seedance-camera-control/index.html +2 -2
- package/dist/sitemap.xml +5 -5
- package/dist/sw.js +5 -1
- package/dist/thumbs/blank-stage-dark.webp +0 -0
- package/dist/thumbs/blank-stage-light.webp +0 -0
- package/dist/workflow/index.html +5 -4
- package/mcp/mesh-file.mjs +3 -1
- package/mcp/server.mjs +3 -0
- package/mcp/tool-handlers.mjs +2 -2
- package/package.json +5 -3
- package/src/App.jsx +51 -282
- package/src/analytics.js +3 -148
- package/src/app-stage.jsx +1 -1
- package/src/asset-pane.jsx +5 -10
- package/src/auto-flat.js +10 -0
- package/src/bus-tasks.js +11 -5
- package/src/command-bus.js +1 -1
- package/src/commands/elements.js +4 -3
- package/src/commands/motion3-compile.js +51 -27
- package/src/commands/motion3.js +3 -3
- package/src/commands/objects.js +1 -1
- package/src/commands/project.js +1 -1
- package/src/commands/shot.js +16 -4
- package/src/controls.jsx +0 -11
- package/src/cuts.js +11 -0
- package/src/domains/cast.js +3 -2
- package/src/domains/motion.js +3 -10
- package/src/domains/playback.js +32 -0
- package/src/domains/scenes.js +3 -22
- package/src/domains/shots.js +5 -5
- package/src/dualview.jsx +11 -15
- package/src/hierarchy-panel.css +18 -4
- package/src/hierarchy-panel.jsx +12 -11
- package/src/kits/held.js +3 -2
- package/src/light-blockers.js +2 -2
- package/src/main.jsx +5 -0
- package/src/material-look.js +189 -0
- package/src/material-presets.js +78 -0
- package/src/material-textures.js +157 -0
- package/src/motion/ground.js +5 -14
- package/src/motion/playback-frame-store.js +79 -0
- package/src/motion/playback-probe.js +16 -0
- package/src/motion/playback-subject.js +7 -0
- package/src/motion/timeline.css +43 -5
- package/src/motion/timeline.jsx +4 -3
- package/src/motion3/body.js +37 -5
- package/src/motion3/carried.js +11 -3
- package/src/motion3/compile-job.js +1 -1
- package/src/motion3/compiler/compile.js +7 -1
- package/src/motion3/compiler/finger-close.js +137 -0
- package/src/motion3/compiler/limbs.js +18 -3
- package/src/motion3/compiler/pose.js +6 -0
- package/src/motion3/compiler/validate-objects.js +17 -2
- package/src/motion3/compiler/validate-parts.js +2 -1
- package/src/motion3/compiler/vocab.js +4 -4
- package/src/motion3/describe.js +2 -2
- package/src/motion3/fix/index.js +5 -3
- package/src/motion3/flight.js +38 -0
- package/src/motion3/free-motion.js +10 -7
- package/src/motion3/load.js +125 -0
- package/src/motion3/mass.js +23 -0
- package/src/motion3/object-keys.js +11 -6
- package/src/motion3/object-pose.js +11 -3
- package/src/motion3/pipeline.js +3 -0
- package/src/motion3/qa/explain.js +3 -1
- package/src/motion3/qa/grasp-checks.js +123 -0
- package/src/motion3/qa/hints-grasp.js +71 -0
- package/src/motion3/qa/index.js +3 -1
- package/src/motion3/qa/object-checks.js +7 -2
- package/src/motion3/qa/patch.js +2 -1
- package/src/motion3/qa/spec-checks.js +10 -1
- package/src/motion3/schema/fields.js +3 -3
- package/src/motion3/schema/topics.js +9 -8
- package/src/motion3/schema/types.js +4 -1
- package/src/motion3/seat.js +84 -0
- package/src/motion3/settle.js +11 -6
- package/src/motion3/shapes.js +70 -0
- package/src/motion3/skills/catch.js +71 -0
- package/src/motion3/skills/index.js +10 -3
- package/src/motion3/skills/info.js +13 -0
- package/src/motion3/skills/throw.js +72 -0
- package/src/object-catalog.jsx +0 -4
- package/src/object-gizmo.jsx +2 -1
- package/src/object-look.js +9 -5
- package/src/object-path.js +1 -1
- package/src/panels/ProjectPanel.jsx +42 -21
- package/src/panels/details.css +15 -4
- package/src/popover.js +37 -0
- package/src/posestudio.jsx +15 -10
- package/src/primitive-look.js +38 -0
- package/src/project-browser-art.jsx +19 -0
- package/src/project-browser.css +74 -121
- package/src/project-browser.jsx +36 -60
- package/src/props.jsx +24 -35
- package/src/render-passes.js +7 -1
- package/src/retired-kinds.js +37 -0
- package/src/scene-objects.js +5 -9
- package/src/scenes.js +4 -5
- package/src/sculpt-colours.js +65 -0
- package/src/sculpt-compare.js +13 -50
- package/src/sculpt-csg.js +112 -0
- package/src/sculpt-curve.js +123 -0
- package/src/sculpt-cutters.js +43 -0
- package/src/sculpt-detail.js +207 -0
- package/src/sculpt-geometry-loft-convex.js +100 -0
- package/src/sculpt-geometry-shapes.js +250 -0
- package/src/sculpt-geometry.js +33 -15
- package/src/sculpt-loft-rings.js +225 -0
- package/src/sculpt-noise.js +65 -0
- package/src/sculpt-part-fields.js +27 -0
- package/src/sculpt-recipe-read.js +65 -0
- package/src/sculpt-recipe.js +33 -75
- package/src/sculpt-run.js +1 -1
- package/src/sculpt-scatter-items.js +183 -0
- package/src/sculpt-scatter-shape.js +57 -0
- package/src/sculpt-scatter.js +124 -0
- package/src/sculpt-shapes-curved.js +211 -0
- package/src/sculpt-shapes-loft-convex.js +218 -0
- package/src/sculpt-shapes.js +138 -0
- package/src/sculpt-sketch-state.js +76 -0
- package/src/sculpt-sketch.jsx +124 -0
- package/src/sculpt-surface-fields.js +65 -0
- package/src/settings-menu.jsx +1 -20
- package/src/shell/BottomDock.jsx +9 -9
- package/src/shell/MenuBar.jsx +1 -9
- package/src/shell/PreferencesDialog.jsx +3 -4
- package/src/shell/ViewportToolbar.jsx +3 -3
- package/src/shell/add-camera-shot.js +11 -0
- package/src/shell/glass-regions.css +3 -2
- package/src/shell/glass.css +0 -5
- package/src/shell/lens-scrub-math.js +51 -0
- package/src/shell/lens-scrub.css +73 -0
- package/src/shell/lens-scrub.jsx +98 -0
- package/src/shell/mode.css +6 -5
- package/src/shell/preferences.css +1 -32
- package/src/shell/shell.css +18 -1
- package/src/shell/studio-shell-context.js +5 -0
- package/src/shot-frame.js +26 -0
- package/src/stage-canvas-rigs.jsx +7 -8
- package/src/stage-characters.jsx +26 -0
- package/src/stage-gizmo.js +43 -0
- package/src/stage-labels-ko.js +0 -6
- package/src/stage-startup-defaults.js +3 -9
- package/src/starter-scenes.js +5 -11
- package/src/studio-agent-commands.js +2 -2
- package/src/studio-agent-protocol.js +2 -7
- package/src/studio-app-binding.js +1 -1
- package/src/studio-elements.js +6 -2
- package/src/styles/themes.css +13 -0
- package/src/styles.css +6 -674
- package/src/text/text-object.jsx +1 -1
- package/src/ui.jsx +8 -16
- package/src/use-embed-bridge.js +3 -6
- package/src/use-render-activity.js +3 -1
- package/src/view-ghosts.jsx +5 -3
- package/src/viewport-visible-rect.js +37 -0
- package/src/workflow/AgentPanel.jsx +79 -46
- package/src/workflow/AgentPlanCard.jsx +41 -0
- package/src/workflow/AgentProgressCard.jsx +1 -1
- package/src/workflow/TaskBoard.jsx +102 -0
- package/src/workflow/TaskInline.jsx +114 -0
- package/src/workflow/agent-client.js +25 -36
- package/src/workflow/agent-panel.css +14 -1
- package/src/workflow/agent-plan.css +93 -0
- package/src/workflow/agent-plan.js +59 -0
- package/src/workflow/attachment-image.js +10 -3
- package/src/workflow/attachment-video.js +91 -0
- package/src/workflow/task-board.css +98 -0
- package/src/workflow/task-board.js +169 -0
- package/tools/check-eslint.mjs +2 -1
- package/tools/check-knip.mjs +2 -1
- package/tools/dev/pages/greybox-to-video.html +2 -3
- package/tools/dev/pages/previs-software.html +2 -2
- package/tools/dev/pages/privacy.html +3 -7
- package/tools/dev/pages/seedance-camera-control.html +2 -2
- package/tools/dev-full.mjs +11 -2
- package/tools/eslint-baseline.json +6 -6
- package/tools/knip-baseline.json +0 -11
- package/tools/loc-baseline.json +30 -31
- package/tools/metrics-snapshot.mjs +43 -5
- package/tools/render-start-thumbs.mjs +175 -0
- package/tools/run-tests.mjs +10 -17
- package/tools/sitemap.mjs +1 -1
- package/dist/assets/analytics-BibyE5ar.js +0 -6
- package/dist/assets/app-DhRJ-PBz.js +0 -5088
- package/dist/assets/app-f-fkV5uP.css +0 -1
- package/dist/assets/compile-worker-DqiA4AaN.js +0 -5
- package/dist/assets/first-shot-handoff-Qx7SKl38.js +0 -1
- package/dist/assets/landing-DYg87MDY.js +0 -1
- package/dist/assets/scene-asset-sync-DBYQBR0T.js +0 -20
- package/dist/assets/scene-asset-sync-_S8NQl5B.css +0 -1
- package/dist/assets/workflow-BSA22Ofr.js +0 -179
- package/dist/scenes/city-block.cclayproject +0 -1
- package/src/burn-in.js +0 -91
- package/src/camera-tutorial.jsx +0 -520
- package/src/facing-marks.js +0 -88
- package/src/first-shot-handoff.js +0 -44
- package/src/panels/motion.css +0 -197
- package/src/set-pieces.jsx +0 -183
- package/src/tutorial-analytics.js +0 -86
- package/src/tutorial-walk.js +0 -17
- package/src/use-camera-tutorial.js +0 -50
- package/src/use-semantic-state.js +0 -12
- package/tools/metrics/archive-traffic.mjs +0 -138
- package/tools/perf/ik-camera-drag.mjs +0 -138
package/src/shell/BottomDock.jsx
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
2
|
-
import { useStudioShell } from "./studio-shell-context.js";
|
|
2
|
+
import { useStudioPlaybackFrame, useStudioShell } from "./studio-shell-context.js";
|
|
3
3
|
import { logStore } from "./log-store.js";
|
|
4
4
|
import "./dock.css";
|
|
5
5
|
import LibrarySlot from "./LibrarySlot.jsx";
|
|
6
6
|
import BoardSlot from "./BoardSlot.jsx";
|
|
7
|
+
import LensScrub from "./lens-scrub.jsx";
|
|
7
8
|
import { ko } from "../locale.js";
|
|
8
9
|
import Timeline, { timelineTake } from "../motion/timeline.jsx";
|
|
9
|
-
import { DEFAULT_PLAYBACK_SPEED,
|
|
10
|
+
import { DEFAULT_PLAYBACK_SPEED, sceneObjectNameDisplayKo } from "../app-stage.jsx";
|
|
10
11
|
|
|
11
12
|
import { trackFeature } from "../analytics.js";
|
|
12
13
|
import { defaultRailRange } from "../camera-rail-schedule.js";
|
|
@@ -67,11 +68,11 @@ function dockTabsFor(previsMode) {
|
|
|
67
68
|
* switching keeps the Sequencer's scroll and zoom and the Library's folder
|
|
68
69
|
* and search. Any stored tab the mode does not offer opens its first tab. */
|
|
69
70
|
export default function BottomDock({ tab: requestedTab = "animation", onTabChange, embedded = false }) {
|
|
70
|
-
const { previsMode } = useStudioShell();
|
|
71
|
+
const { previsMode } = useStudioShell(), liveFrame = useStudioPlaybackFrame();
|
|
71
72
|
const tabs = embedded ? dockTabsFor(null) : dockTabsFor(previsMode);
|
|
72
73
|
const tab = tabs.some((entry) => entry.key === requestedTab) ? requestedTab : tabs[0].key;
|
|
73
74
|
const {
|
|
74
|
-
|
|
75
|
+
motion, waypointMode, craneSelectedIndex,
|
|
75
76
|
isCameraSelection, addActiveCranePoint, deleteSelectedCranePoint, setCraneSelectedIndex, tlFrameCount,
|
|
76
77
|
tlFps, characters, activeCharIndex, ghostLayers, pathSpeed,
|
|
77
78
|
tlPlaying, workflowMode, waypoints, pendingWaypointFrame,
|
|
@@ -205,7 +206,7 @@ export default function BottomDock({ tab: requestedTab = "animation", onTabChang
|
|
|
205
206
|
{!embedded && previsMode === "storyboard" && <BoardSlot active={tab === "board"} />}
|
|
206
207
|
<div className="bottom-timeline" hidden={tab !== "animation"}>
|
|
207
208
|
<Timeline
|
|
208
|
-
frame={
|
|
209
|
+
frame={liveFrame}
|
|
209
210
|
craneSelectedIndex={craneSelectedIndex}
|
|
210
211
|
cameraSelected={isCameraSelection}
|
|
211
212
|
onCranePointAdd={addActiveCranePoint}
|
|
@@ -390,11 +391,10 @@ const CARD_RATIOS = [
|
|
|
390
391
|
|
|
391
392
|
/** The shot camera's card at the dock's right end. The camera itself is drawn
|
|
392
393
|
* into the stage canvas under `.vp-shot-preview`, which sits over the card's
|
|
393
|
-
* frame slot (see glass.css); the card holds its title, ratio and look-through. */
|
|
394
|
+
* frame slot (see glass.css); the card holds its title, lens, ratio and look-through. */
|
|
394
395
|
function ShotCard() {
|
|
395
|
-
const { activeShot, shot, shotAspectKey, runStudioAction, enterShotLook, exitShotLook, lookThroughShot, tlFps, embedMode } = useStudioShell();
|
|
396
|
+
const { activeShot, shot, fovDeg, shotsDomain, shotAspectKey, runStudioAction, enterShotLook, exitShotLook, lookThroughShot, tlFps, embedMode } = useStudioShell();
|
|
396
397
|
if (embedMode) return null;
|
|
397
|
-
const ratio = SHOT_ASPECT_PRESETS[shotAspectKey]?.label?.replace(/:1$/, "") ?? shotAspectKey;
|
|
398
398
|
const frames = activeShot && Number.isFinite(activeShot.startFrame) && Number.isFinite(activeShot.endFrame)
|
|
399
399
|
? `${activeShot.startFrame}–${activeShot.endFrame} · ${((activeShot.endFrame - activeShot.startFrame + 1) / (tlFps || 24)).toFixed(1)}s`
|
|
400
400
|
: null;
|
|
@@ -403,7 +403,7 @@ function ShotCard() {
|
|
|
403
403
|
<header className="dock-shot-head">
|
|
404
404
|
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M2 5.75A1.5 1.5 0 0 1 3.5 4.25h6A1.5 1.5 0 0 1 11 5.75v4.5a1.5 1.5 0 0 1-1.5 1.5h-6A1.5 1.5 0 0 1 2 10.25z M11 7l3-1.5v5L11 9" /></svg>
|
|
405
405
|
<span className="dock-shot-title">{ko("Shot Camera", "샷 카메라")}</span>
|
|
406
|
-
<
|
|
406
|
+
<LensScrub fovDeg={fovDeg} sensorHeightMm={shot?.usedSensorHeightMm} shotsDomain={shotsDomain} disabled={!shot} />
|
|
407
407
|
</header>
|
|
408
408
|
{/* While the main view looks through this camera the card has nothing
|
|
409
409
|
of its own to show (the preview pass is off): it says where the
|
package/src/shell/MenuBar.jsx
CHANGED
|
@@ -196,7 +196,7 @@ export default function MenuBar({ preferences }) {
|
|
|
196
196
|
? panelCount ? "" : noPanels
|
|
197
197
|
: shots.length ? "" : ko("Add a shot first — a pack describes one cut", "샷을 먼저 추가하세요 — 팩은 컷 하나를 설명합니다");
|
|
198
198
|
|
|
199
|
-
//
|
|
199
|
+
// Export's flyout lives in File, so an open Export menu opens File.
|
|
200
200
|
useEffect(() => {
|
|
201
201
|
if (exportMenuOpen) setOpen("file");
|
|
202
202
|
}, [exportMenuOpen]);
|
|
@@ -534,14 +534,6 @@ export default function MenuBar({ preferences }) {
|
|
|
534
534
|
),
|
|
535
535
|
help: (
|
|
536
536
|
<>
|
|
537
|
-
<button
|
|
538
|
-
type="button"
|
|
539
|
-
role="menuitem"
|
|
540
|
-
className="menubar-item"
|
|
541
|
-
onClick={item(() => window.dispatchEvent(new CustomEvent("cozyclay:camera-tutorial", { detail: { open: true, source: "help" } })))}
|
|
542
|
-
>
|
|
543
|
-
<span className="menubar-item-label">{ko("Tutorial", "튜토리얼")}</span>
|
|
544
|
-
</button>
|
|
545
537
|
<button type="button" role="menuitem" className="menubar-item" onClick={item(() => setDialog("shortcuts"))}>
|
|
546
538
|
<span className="menubar-item-label">{ko("Keyboard shortcuts", "단축키")}</span>
|
|
547
539
|
</button>
|
|
@@ -114,7 +114,7 @@ function ThemeControls() {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
function preferenceSection(section, props) {
|
|
117
|
-
const { autoColor, setAutoColor, analyticsOptOut, setAnalyticsOptOutState, gridView, setGridView
|
|
117
|
+
const { autoColor, setAutoColor, analyticsOptOut, setAnalyticsOptOutState, gridView, setGridView } = props;
|
|
118
118
|
const languageControls = (
|
|
119
119
|
<div className="v2-preferences__language-controls">
|
|
120
120
|
{LANGUAGES.map((language) => (
|
|
@@ -158,7 +158,6 @@ function preferenceSection(section, props) {
|
|
|
158
158
|
/>
|
|
159
159
|
</PreferenceRow>
|
|
160
160
|
</PreferenceSection>
|
|
161
|
-
{cameraTutorial && <div className="v2-preferences__help">{cameraTutorial}</div>}
|
|
162
161
|
</>
|
|
163
162
|
);
|
|
164
163
|
case "Viewport":
|
|
@@ -181,7 +180,7 @@ function preferenceSection(section, props) {
|
|
|
181
180
|
}
|
|
182
181
|
}
|
|
183
182
|
|
|
184
|
-
export default function PreferencesDialog({ open, onClose, initialSection = "Appearance"
|
|
183
|
+
export default function PreferencesDialog({ open, onClose, initialSection = "Appearance" }) {
|
|
185
184
|
const dialogRef = useRef(null);
|
|
186
185
|
const searchRef = useRef(null);
|
|
187
186
|
const closeRef = useRef(onClose);
|
|
@@ -233,7 +232,7 @@ export default function PreferencesDialog({ open, onClose, initialSection = "App
|
|
|
233
232
|
}, [open, initialSection]);
|
|
234
233
|
|
|
235
234
|
if (!open || typeof document === "undefined") return null;
|
|
236
|
-
const currentProps = { autoColor, setAutoColor, analyticsOptOut, setAnalyticsOptOutState, gridView, setGridView
|
|
235
|
+
const currentProps = { autoColor, setAutoColor, analyticsOptOut, setAnalyticsOptOutState, gridView, setGridView };
|
|
237
236
|
|
|
238
237
|
return createPortal(
|
|
239
238
|
<div className="v2-preferences" data-testid="preferences-dialog-backdrop" onPointerDown={(event) => { if (event.target === event.currentTarget) closeRef.current?.(); }}>
|
|
@@ -11,7 +11,7 @@ import { elementByPath } from "../studio-elements.js";
|
|
|
11
11
|
import { saveAutoColor } from "../auto-color.js";
|
|
12
12
|
import { trackFeature } from "../analytics.js";
|
|
13
13
|
import { GUIDE_LABELS, nextGuideMode } from "../shot-guides.js";
|
|
14
|
-
import GhostsMenuGroup from "./GhostsMenuGroup.jsx";
|
|
14
|
+
import GhostsMenuGroup from "./GhostsMenuGroup.jsx"; import { addCameraShot } from "./add-camera-shot.js";
|
|
15
15
|
import "./viewport.css";
|
|
16
16
|
|
|
17
17
|
// G1: the single mode+tool pill. Keys 1-5 and W/E/R are bound in App's
|
|
@@ -122,7 +122,7 @@ export default function ViewportToolbar() {
|
|
|
122
122
|
autoColor, setAutoColor, isCharacterSelection, partColoursChoice, embedMode,
|
|
123
123
|
agentCollapsed, addSceneObject, spawnCharacter, characters, flySpeed,
|
|
124
124
|
ikEditTool, setIkEditTool, motion, setRangePinPartPick,
|
|
125
|
-
previsMode, sculptView, requestSculptView,
|
|
125
|
+
previsMode, sculptView, requestSculptView, appContext, setToast,
|
|
126
126
|
} = useStudioShell();
|
|
127
127
|
const menu = useOverlayMenu();
|
|
128
128
|
|
|
@@ -292,7 +292,7 @@ export default function ViewportToolbar() {
|
|
|
292
292
|
role="menuitem"
|
|
293
293
|
className="add-object-item"
|
|
294
294
|
data-add="camera"
|
|
295
|
-
onClick={() => { runStudioAction
|
|
295
|
+
onClick={() => { addCameraShot({ appContext, runStudioAction, setToast }); menu.close(); }}
|
|
296
296
|
>
|
|
297
297
|
<span className="add-object-swatch vp-add-swatch-camera" aria-hidden="true" />
|
|
298
298
|
<span>{ko("Camera", "카메라")}</span>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Add > Camera names the cut it made (ViewportToolbar). A refusal is already toasted by runStudioAction.
|
|
2
|
+
import { ko } from "../locale.js";
|
|
3
|
+
|
|
4
|
+
export function addCameraShot({ appContext, runStudioAction, setToast }) {
|
|
5
|
+
const shots = appContext.storeDomain("shot"), before = shots.read(), receipt = runStudioAction("shot.create");
|
|
6
|
+
const made = shots.read().find((row) => row.id === receipt?.affectedIds.find((id) => !before.some((old) => old.id === id)));
|
|
7
|
+
if (!made) return;
|
|
8
|
+
setToast(made.kind === "still"
|
|
9
|
+
? ko(`${made.name} added — a storyboard panel at frame ${made.startFrame}`, `${made.name} 추가됨 — ${made.startFrame}프레임의 스토리보드 패널`)
|
|
10
|
+
: ko(`${made.name} added — a camera cut at frame ${made.startFrame}`, `${made.name} 추가됨 — ${made.startFrame}프레임에서 카메라 컷`));
|
|
11
|
+
}
|
|
@@ -800,11 +800,12 @@
|
|
|
800
800
|
|
|
801
801
|
/* ----------------------------------------------- Dock: Animation | Assets */
|
|
802
802
|
/* The switch sits at the head's left end, where the Sequencer's transport
|
|
803
|
-
and the Library's Content | Log row start after it.
|
|
803
|
+
and the Library's Content | Log row start after it. It paints above the
|
|
804
|
+
timeline (z-index 6, timeline.css), which would otherwise take its clicks. */
|
|
804
805
|
.app[data-shell="glass"]:not([data-embed-mode]) .v2-dock { --dock-tabs-w: 176px; }
|
|
805
806
|
.app[data-shell="glass"]:not([data-embed-mode]) .dock-tabs {
|
|
806
807
|
position: absolute;
|
|
807
|
-
z-index:
|
|
808
|
+
z-index: 7;
|
|
808
809
|
top: 9px;
|
|
809
810
|
left: 16px;
|
|
810
811
|
box-sizing: border-box;
|
package/src/shell/glass.css
CHANGED
|
@@ -326,11 +326,6 @@
|
|
|
326
326
|
font-size: 11.5px;
|
|
327
327
|
font-weight: 400;
|
|
328
328
|
}
|
|
329
|
-
.app[data-shell="glass"]:not([data-embed-mode]) .studio-project-head .project-menu {
|
|
330
|
-
top: calc(100% + 6px);
|
|
331
|
-
left: -6px;
|
|
332
|
-
z-index: 40;
|
|
333
|
-
}
|
|
334
329
|
|
|
335
330
|
/* --------------------------------------------- viewport overlay rails */
|
|
336
331
|
/* Overlays fill the open stage between the columns: the mode switch rides
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// The Shot Camera card's focal-length scrub (BottomDock ShotCard). The lens is
|
|
2
|
+
// stored as a vertical FOV (shotsDomain.changeLens takes degrees); the card
|
|
3
|
+
// speaks millimetres on the current filmback, the shot.js lens relation.
|
|
4
|
+
import { PRIME_SET } from "../shot.js";
|
|
5
|
+
|
|
6
|
+
// Editor lens range: shotsDomain.changeLens refuses FOVs outside 14-90°.
|
|
7
|
+
const FOV_MIN = 14, FOV_MAX = 90;
|
|
8
|
+
// px of horizontal drag per millimetre, and the Shift / Alt multipliers.
|
|
9
|
+
const PX_PER_MM = 4, FINE = 0.1, COARSE = 4;
|
|
10
|
+
|
|
11
|
+
const round1 = (value) => Math.round(value * 10) / 10;
|
|
12
|
+
|
|
13
|
+
export function fovDegToMm(fovDeg, sensorHeightMm) {
|
|
14
|
+
return sensorHeightMm / (2 * Math.tan((fovDeg * Math.PI) / 360));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function mmToFovDeg(mm, sensorHeightMm) {
|
|
18
|
+
return (2 * Math.atan(sensorHeightMm / (2 * mm)) * 180) / Math.PI;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The millimetre range the editor FOV range allows on this filmback. */
|
|
22
|
+
export function lensRange(sensorHeightMm) {
|
|
23
|
+
return { min: round1(fovDegToMm(FOV_MAX, sensorHeightMm)) + 0.1, max: round1(fovDegToMm(FOV_MIN, sensorHeightMm)) - 0.1 };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function clampMm(mm, range) {
|
|
27
|
+
return round1(Math.min(range.max, Math.max(range.min, mm)));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The lens a horizontal drag of dx px from startMm lands on. */
|
|
31
|
+
export function dragMm(startMm, dx, { fine = false, coarse = false } = {}, range) {
|
|
32
|
+
const scale = fine ? FINE : coarse ? COARSE : 1;
|
|
33
|
+
return clampMm(startMm + (dx / PX_PER_MM) * scale, range);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A click steps to the next real prime in range, wrapping to the widest. */
|
|
37
|
+
export function nextPrime(mm, range) {
|
|
38
|
+
const primes = PRIME_SET.filter((prime) => prime >= range.min && prime <= range.max);
|
|
39
|
+
return primes.find((prime) => prime > mm + 0.05) ?? primes[0];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The left arrow steps back to the previous real prime, wrapping to the longest. */
|
|
43
|
+
export function previousPrime(mm, range) {
|
|
44
|
+
const primes = PRIME_SET.filter((prime) => prime >= range.min && prime <= range.max);
|
|
45
|
+
return [...primes].reverse().find((prime) => prime < mm - 0.05) ?? primes.at(-1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function formatMm(mm) {
|
|
49
|
+
const value = round1(mm);
|
|
50
|
+
return `${Number.isInteger(value) ? value : value.toFixed(1)} mm`;
|
|
51
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/* The Shot Camera card's focal-length scrub (lens-scrub.jsx): a small field, the mm value over a track with a
|
|
2
|
+
thumb, so it reads as a control at rest; the step arrows appear just outside it on hover, taking no width. */
|
|
3
|
+
.dock-lens-scrub {
|
|
4
|
+
position: relative;
|
|
5
|
+
flex: none;
|
|
6
|
+
display: inline-flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
gap: 0;
|
|
9
|
+
padding: 1px;
|
|
10
|
+
border-radius: 6px;
|
|
11
|
+
background: var(--chip-fill, rgba(127, 127, 127, .1));
|
|
12
|
+
color: var(--text-1, currentColor);
|
|
13
|
+
font: 500 11px var(--sans);
|
|
14
|
+
font-variant-numeric: tabular-nums;
|
|
15
|
+
cursor: ew-resize;
|
|
16
|
+
user-select: none;
|
|
17
|
+
touch-action: none;
|
|
18
|
+
}
|
|
19
|
+
.dock-lens-body { display: flex; flex-direction: column; align-items: stretch; gap: 2px; padding: 2px 4px; }
|
|
20
|
+
.dock-lens-value { line-height: 1; white-space: nowrap; text-align: center; }
|
|
21
|
+
.dock-lens-track {
|
|
22
|
+
position: relative;
|
|
23
|
+
width: 38px;
|
|
24
|
+
height: 2px;
|
|
25
|
+
border-radius: 1px;
|
|
26
|
+
background: var(--hairline, rgba(127, 127, 127, .3));
|
|
27
|
+
}
|
|
28
|
+
.dock-lens-fill {
|
|
29
|
+
position: absolute;
|
|
30
|
+
inset: 0 auto 0 0;
|
|
31
|
+
width: var(--lens-at, 0%);
|
|
32
|
+
border-radius: inherit;
|
|
33
|
+
background: var(--text-3, currentColor);
|
|
34
|
+
}
|
|
35
|
+
.dock-lens-thumb {
|
|
36
|
+
position: absolute;
|
|
37
|
+
top: 50%;
|
|
38
|
+
left: var(--lens-at, 0%);
|
|
39
|
+
width: 5px;
|
|
40
|
+
height: 5px;
|
|
41
|
+
border-radius: 50%;
|
|
42
|
+
background: var(--text-2, currentColor);
|
|
43
|
+
box-shadow: 0 0 0 1px var(--surface-raised, #fff);
|
|
44
|
+
transform: translate(-50%, -50%);
|
|
45
|
+
}
|
|
46
|
+
.dock-lens-step {
|
|
47
|
+
position: absolute;
|
|
48
|
+
top: 50%;
|
|
49
|
+
display: grid;
|
|
50
|
+
place-items: center;
|
|
51
|
+
width: 12px;
|
|
52
|
+
height: 18px;
|
|
53
|
+
border-radius: 3px;
|
|
54
|
+
color: var(--text-3, currentColor);
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
opacity: 0;
|
|
57
|
+
transform: translateY(-50%);
|
|
58
|
+
transition: opacity .12s;
|
|
59
|
+
}
|
|
60
|
+
.dock-lens-step[data-step="back"] { right: 100%; }
|
|
61
|
+
.dock-lens-step[data-step="next"] { left: 100%; }
|
|
62
|
+
.dock-lens-scrub:is(:hover, :focus-visible, .dragging) .dock-lens-step { opacity: 1; }
|
|
63
|
+
.dock-lens-step:hover { color: var(--select); background: var(--hairline, rgba(127, 127, 127, .2)); }
|
|
64
|
+
.dock-lens-scrub:hover,
|
|
65
|
+
.dock-lens-scrub.dragging { color: var(--select); }
|
|
66
|
+
.dock-lens-scrub:hover .dock-lens-fill,
|
|
67
|
+
.dock-lens-scrub.dragging .dock-lens-fill,
|
|
68
|
+
.dock-lens-scrub:hover .dock-lens-thumb,
|
|
69
|
+
.dock-lens-scrub.dragging .dock-lens-thumb { background: var(--select); }
|
|
70
|
+
.dock-lens-scrub.dragging .dock-lens-thumb { transform: translate(-50%, -50%) scale(1.3); }
|
|
71
|
+
.dock-lens-scrub:focus-visible { outline: 2px solid var(--select); outline-offset: 1px; }
|
|
72
|
+
.dock-lens-scrub[aria-disabled="true"] { cursor: default; opacity: .45; background: none; box-shadow: none; color: var(--label, var(--text-2)); }
|
|
73
|
+
.dock-lens-scrub[aria-disabled="true"] :is(.dock-lens-fill, .dock-lens-thumb, .dock-lens-step) { visibility: hidden; }
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { FiChevronLeft, FiChevronRight } from "react-icons/fi";
|
|
3
|
+
import { ko } from "../locale.js";
|
|
4
|
+
import { PRESETS } from "../app-stage.jsx";
|
|
5
|
+
import { clampMm, dragMm, fovDegToMm, formatMm, lensRange, mmToFovDeg, nextPrime, previousPrime } from "./lens-scrub-math.js";
|
|
6
|
+
import "./lens-scrub.css";
|
|
7
|
+
|
|
8
|
+
// A drag starts after this many px; less is a click (the next prime).
|
|
9
|
+
const DRAG_SLOP = 3;
|
|
10
|
+
// A click waits this long so a double-click (reset) does not also step.
|
|
11
|
+
const CLICK_DELAY_MS = 220;
|
|
12
|
+
|
|
13
|
+
/** The Shot Camera card's focal length: drag left/right to change the lens.
|
|
14
|
+
* A drag is one undo step (shotsDomain's gesture commits on pointer-up, Esc
|
|
15
|
+
* cancels); a click steps to the next prime (the left arrow: the previous
|
|
16
|
+
* one); a double-click resets. It is drawn as a field with arrows and a thumb
|
|
17
|
+
* so it reads as something to drag, not as a label. */
|
|
18
|
+
export default function LensScrub({ fovDeg, sensorHeightMm, shotsDomain, disabled = false }) {
|
|
19
|
+
const off = disabled || !shotsDomain || !Number.isFinite(fovDeg) || !(sensorHeightMm > 0);
|
|
20
|
+
const range = off ? { min: 0, max: 0 } : lensRange(sensorHeightMm);
|
|
21
|
+
const mm = off ? 0 : clampMm(fovDegToMm(fovDeg, sensorHeightMm), range);
|
|
22
|
+
const drag = useRef(null);
|
|
23
|
+
const clickTimer = useRef(0);
|
|
24
|
+
const [dragging, setDragging] = useState(false);
|
|
25
|
+
useEffect(() => () => clearTimeout(clickTimer.current), []);
|
|
26
|
+
|
|
27
|
+
const setMm = (value) => shotsDomain.changeLens(mmToFovDeg(clampMm(value, range), sensorHeightMm));
|
|
28
|
+
const onPointerDown = (event) => {
|
|
29
|
+
if (off || event.button !== 0) return;
|
|
30
|
+
event.currentTarget.setPointerCapture?.(event.pointerId);
|
|
31
|
+
drag.current = { x: event.clientX, mm, moved: false, back: event.target.closest?.("[data-step]")?.dataset.step === "back" };
|
|
32
|
+
};
|
|
33
|
+
const onPointerMove = (event) => {
|
|
34
|
+
const state = drag.current;
|
|
35
|
+
if (!state) return;
|
|
36
|
+
const dx = event.clientX - state.x;
|
|
37
|
+
if (!state.moved) {
|
|
38
|
+
if (Math.abs(dx) < DRAG_SLOP) return;
|
|
39
|
+
state.moved = true;
|
|
40
|
+
setDragging(true);
|
|
41
|
+
shotsDomain.beginGesture();
|
|
42
|
+
}
|
|
43
|
+
setMm(dragMm(state.mm, dx, { fine: event.shiftKey, coarse: event.altKey }, range));
|
|
44
|
+
};
|
|
45
|
+
const onPointerUp = () => {
|
|
46
|
+
const state = drag.current;
|
|
47
|
+
drag.current = null;
|
|
48
|
+
setDragging(false);
|
|
49
|
+
if (!state || state.moved) return;
|
|
50
|
+
clearTimeout(clickTimer.current);
|
|
51
|
+
clickTimer.current = setTimeout(() => setMm((state.back ? previousPrime : nextPrime)(state.mm, range)), CLICK_DELAY_MS);
|
|
52
|
+
};
|
|
53
|
+
const onDoubleClick = () => {
|
|
54
|
+
if (off) return;
|
|
55
|
+
clearTimeout(clickTimer.current);
|
|
56
|
+
shotsDomain.changeLens(PRESETS.medium.fov);
|
|
57
|
+
};
|
|
58
|
+
const onKeyDown = (event) => {
|
|
59
|
+
if (off || (event.key !== "ArrowLeft" && event.key !== "ArrowRight")) return;
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
event.stopPropagation();
|
|
62
|
+
const step = (event.shiftKey ? 10 : 1) * (event.key === "ArrowRight" ? 1 : -1);
|
|
63
|
+
setMm(Math.round(mm + step));
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<span
|
|
68
|
+
className={"dock-lens-scrub" + (dragging ? " dragging" : "")}
|
|
69
|
+
role="slider"
|
|
70
|
+
tabIndex={off ? -1 : 0}
|
|
71
|
+
aria-label={ko("Focal length", "초점 거리")}
|
|
72
|
+
aria-disabled={off || undefined}
|
|
73
|
+
aria-valuemin={range.min}
|
|
74
|
+
aria-valuemax={range.max}
|
|
75
|
+
aria-valuenow={mm}
|
|
76
|
+
aria-valuetext={off ? ko("No shot camera", "샷 카메라 없음") : formatMm(mm)}
|
|
77
|
+
aria-orientation="horizontal"
|
|
78
|
+
title={ko("Focal length — drag to change, click an arrow for the next lens", "초점 거리 — 드래그해 변경, 화살표를 눌러 다음 렌즈")}
|
|
79
|
+
data-testid="dock-lens-scrub"
|
|
80
|
+
onPointerDown={onPointerDown}
|
|
81
|
+
onPointerMove={onPointerMove}
|
|
82
|
+
onPointerUp={onPointerUp}
|
|
83
|
+
onPointerCancel={() => { drag.current = null; setDragging(false); }}
|
|
84
|
+
onDoubleClick={onDoubleClick}
|
|
85
|
+
onKeyDown={onKeyDown}
|
|
86
|
+
>
|
|
87
|
+
<span className="dock-lens-step" data-step="back" aria-hidden="true"><FiChevronLeft size={9} /></span>
|
|
88
|
+
<span className="dock-lens-body">
|
|
89
|
+
<span className="dock-lens-value">{off ? "— mm" : formatMm(mm)}</span>
|
|
90
|
+
<span className="dock-lens-track" aria-hidden="true" style={{ "--lens-at": `${off ? 0 : ((mm - range.min) / (range.max - range.min)) * 100}%` }}>
|
|
91
|
+
<span className="dock-lens-fill" />
|
|
92
|
+
<span className="dock-lens-thumb" />
|
|
93
|
+
</span>
|
|
94
|
+
</span>
|
|
95
|
+
<span className="dock-lens-step" data-step="next" aria-hidden="true"><FiChevronRight size={9} /></span>
|
|
96
|
+
</span>
|
|
97
|
+
);
|
|
98
|
+
}
|
package/src/shell/mode.css
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/* Pose mode (#521) is IK editing on the motion workspace: it hides what
|
|
2
|
-
Motion hides (scene transform and camera framing chrome, the inset and
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
Motion hides (scene transform and camera framing chrome, the inset) and
|
|
3
|
+
shows the mode hint. The shot preview stays: it is drawn into the dock's
|
|
4
|
+
Shot Camera frame, and hiding it left that frame a hole onto the editor
|
|
5
|
+
view. The timeline's pose-correction tools are hidden only in Scene and
|
|
6
|
+
Camera, so they stay visible here. */
|
|
5
7
|
.app[data-workflow-mode="pose"] .workflow-scene-context,
|
|
6
8
|
.app[data-workflow-mode="pose"] .workflow-camera-context { display: none; }
|
|
7
9
|
.app[data-workflow-mode="pose"] .workflow-toolbar-hint { display: inline-flex; }
|
|
8
|
-
.app[data-workflow-mode="pose"] .vp-inset
|
|
9
|
-
.app[data-workflow-mode="pose"] .vp-shot-preview { display: none !important; }
|
|
10
|
+
.app[data-workflow-mode="pose"] .vp-inset { display: none !important; }
|
|
10
11
|
|
|
11
12
|
/* A storyboard project (#650) is stills: the take, motion and pose-correction
|
|
12
13
|
tools, the root-path (waypoint) switch and a prop's Top-View travel path
|
|
@@ -203,8 +203,7 @@
|
|
|
203
203
|
.v2-preferences .v2-preferences__select:focus-visible,
|
|
204
204
|
.v2-preferences .v2-preferences__toggle:focus-visible,
|
|
205
205
|
.v2-preferences .v2-preferences__primary-button:focus-visible,
|
|
206
|
-
.v2-preferences .v2-preferences__reset:focus-visible
|
|
207
|
-
.v2-preferences .v2-preferences__help button:focus-visible {
|
|
206
|
+
.v2-preferences .v2-preferences__reset:focus-visible {
|
|
208
207
|
outline: none;
|
|
209
208
|
box-shadow: var(--prefs-focus-ring, 0 0 0 1px var(--select, #e8a33d));
|
|
210
209
|
}
|
|
@@ -506,36 +505,6 @@
|
|
|
506
505
|
font: 500 var(--prefs-secondary-size, 11px) var(--mono, 'JetBrains Mono');
|
|
507
506
|
}
|
|
508
507
|
|
|
509
|
-
.v2-preferences .v2-preferences__help {
|
|
510
|
-
padding-top: var(--sp-3, 16px);
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
.v2-preferences .v2-preferences__help h4 {
|
|
514
|
-
margin: 0 0 var(--sp-1, 8px);
|
|
515
|
-
color: var(--text-3, #636369);
|
|
516
|
-
font-size: var(--prefs-secondary-size, 11px);
|
|
517
|
-
font-weight: 500;
|
|
518
|
-
letter-spacing: 0;
|
|
519
|
-
text-transform: none;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
.v2-preferences .v2-preferences__help button {
|
|
523
|
-
min-height: var(--h-control, 28px);
|
|
524
|
-
padding: 0 var(--sp-2, 12px);
|
|
525
|
-
border: 0;
|
|
526
|
-
border-radius: var(--r-1, 4px);
|
|
527
|
-
background: var(--field-fill, var(--surface-raised, #1e1e21));
|
|
528
|
-
color: var(--text-2, #9b9ba1);
|
|
529
|
-
font: inherit;
|
|
530
|
-
font-size: var(--prefs-secondary-size, 11px);
|
|
531
|
-
cursor: pointer;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
.v2-preferences .v2-preferences__help button:hover {
|
|
535
|
-
background: var(--surface-raised, #1e1e21);
|
|
536
|
-
color: var(--text-1, #e4e4e7);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
508
|
.v2-preferences .v2-preferences__motion-setup {
|
|
540
509
|
max-width: var(--prefs-control-width, 240px);
|
|
541
510
|
display: grid;
|
package/src/shell/shell.css
CHANGED
|
@@ -24,6 +24,9 @@
|
|
|
24
24
|
--shell-center-left: calc(var(--shell-left) + var(--shell-gap) * 2);
|
|
25
25
|
--shell-center-right: calc(var(--shell-right) + var(--shell-gap) * 2);
|
|
26
26
|
--shell-center-bottom: calc(var(--shell-dock) + var(--shell-status-h) + var(--shell-dock-bottom) + var(--shell-gap));
|
|
27
|
+
/* Top edge of the open stage: the cut-out the frame mask leaves under the
|
|
28
|
+
top bar (.workspace::after). */
|
|
29
|
+
--shell-stage-top: calc(var(--shell-gap) * 2 + 40px);
|
|
27
30
|
/* The shot card's picture. The slot is the room the card leaves between
|
|
28
31
|
its 48px head and the 84px of ratios + footer under it; the picture is
|
|
29
32
|
the shot aspect fitted into that slot and centred in it, so the stage
|
|
@@ -242,7 +245,7 @@
|
|
|
242
245
|
this frame through their glass, and on the darker --surface-0 they
|
|
243
246
|
went a step darker than designed. */
|
|
244
247
|
background: var(--shell-frame, var(--surface-panel));
|
|
245
|
-
--shell-view-top:
|
|
248
|
+
--shell-view-top: var(--shell-stage-top);
|
|
246
249
|
--shell-cut-w: calc(100% - var(--shell-center-left) - var(--shell-center-right));
|
|
247
250
|
--shell-view-h: max(0px, calc(100% - var(--shell-view-top) - var(--shell-center-bottom)));
|
|
248
251
|
--shell-shot-w: var(--shot-pic-w);
|
|
@@ -268,6 +271,20 @@
|
|
|
268
271
|
}
|
|
269
272
|
.app[data-dock-tab="assets"]:not([data-embed-mode]) .workspace::after,
|
|
270
273
|
.app[data-shot-look]:not([data-embed-mode]) .workspace::after { --shell-shot-w: 0px; --shell-shot-h: 0px; }
|
|
274
|
+
/* Look through: the panels and the frame mask cover the canvas outside the
|
|
275
|
+
open stage, and inside it the tool rails (44px, a gutter off each side,
|
|
276
|
+
glass.css) and the top-centre exit pill (styles.css) float over it. The
|
|
277
|
+
shot pane is the open stage less those bands, the same on both sides so
|
|
278
|
+
its centre is the stage centre. DualRender fits the shot aspect into
|
|
279
|
+
this pane, centred, with bars (fitAspect); the gizmo and the guides map
|
|
280
|
+
onto the same rect. Drawn under the panels or the rails, the frame lost
|
|
281
|
+
picture the export keeps. */
|
|
282
|
+
.app[data-shot-look]:not([data-embed-mode]) .vp-main,
|
|
283
|
+
.app[data-shot-look]:not([data-embed-mode]) .shot-guides.lookthrough {
|
|
284
|
+
--shot-look-x: calc(var(--shell-gap) * 2 + 44px);
|
|
285
|
+
--shot-look-y: 36px;
|
|
286
|
+
inset: calc(var(--shell-stage-top) + var(--shot-look-y)) calc(var(--shell-center-right) + var(--shot-look-x)) calc(var(--shell-center-bottom) + var(--shot-look-y)) calc(var(--shell-center-left) + var(--shot-look-x));
|
|
287
|
+
}
|
|
271
288
|
.app[data-dock-tab="assets"]:not([data-embed-mode]) .vp-shot-preview,
|
|
272
289
|
.app[data-dock-tab="assets"]:not([data-embed-mode]) .shell-camera-splitter { display: none; }
|
|
273
290
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createContext, useContext } from "react";
|
|
2
|
+
import { playbackFrame, usePlaybackSnapshot } from "../motion/playback-frame-store.js";
|
|
2
3
|
|
|
3
4
|
// App owns the document/domain wiring. Region components consume this one
|
|
4
5
|
// render snapshot and call its named actions; they never acquire domain state
|
|
@@ -20,3 +21,7 @@ export const StudioShellContext = createContext(null);
|
|
|
20
21
|
export function useStudioShell() {
|
|
21
22
|
return useContext(StudioShellContext);
|
|
22
23
|
}
|
|
24
|
+
|
|
25
|
+
export function useStudioPlaybackFrame() {
|
|
26
|
+
return usePlaybackSnapshot(playbackFrame);
|
|
27
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Shot framing: one rule for every surface that shows the shot camera.
|
|
2
|
+
//
|
|
3
|
+
// The frustum is the shot camera's pose, its vertical FOV (camera.fov, the
|
|
4
|
+
// value a shot stores as fovDeg) and the shot aspect. The aspect only changes
|
|
5
|
+
// the horizontal extent, about the optical axis: a wider ratio extends the
|
|
6
|
+
// sides, a narrower one crops them, and the picture centre never moves. The
|
|
7
|
+
// exports render exactly that (CaptureRig: aspect = output width / height,
|
|
8
|
+
// fov untouched). The focal length shown for a ratio is that FOV read on the
|
|
9
|
+
// ratio's gate (shot.js usedSensorHeightMm), so it is a readout, not the
|
|
10
|
+
// invariant.
|
|
11
|
+
//
|
|
12
|
+
// On screen the frame is the largest centred rect of the shot aspect inside
|
|
13
|
+
// the pane, with bars around it. The dock preview pane is the card's picture
|
|
14
|
+
// slot; the Look through pane is the open stage between the panels (shell.css
|
|
15
|
+
// .vp-main under [data-shot-look]), never the whole canvas under them.
|
|
16
|
+
|
|
17
|
+
/** largest centred sub-rect of `rect` with the given aspect */
|
|
18
|
+
export function fitAspect(rect, aspect) {
|
|
19
|
+
let w = rect.w;
|
|
20
|
+
let h = w / aspect;
|
|
21
|
+
if (h > rect.h) {
|
|
22
|
+
h = rect.h;
|
|
23
|
+
w = h * aspect;
|
|
24
|
+
}
|
|
25
|
+
return { x: rect.x + (rect.w - w) / 2, y: rect.y + (rect.h - h) / 2, w, h };
|
|
26
|
+
}
|
|
@@ -5,13 +5,14 @@ import { aimAt } from "./controls.jsx";
|
|
|
5
5
|
import { SUBJECT_HEIGHT_M } from "./shot.js";
|
|
6
6
|
import { shotAtFrame } from "./cuts.js";
|
|
7
7
|
import { sampleAt } from "./sample-at.js";
|
|
8
|
+
import { playbackProbe, probeVisibility } from "./motion/playback-probe.js";
|
|
8
9
|
import { RENDER_ACTIVITY_EVENT } from "./use-render-activity.js";
|
|
9
10
|
import { PRESETS, editorFrameForSubject } from "./stage-startup-defaults.js";
|
|
10
11
|
|
|
11
12
|
/** Selection marker for the picked cast member: a Unity-style XYZ tripod
|
|
12
13
|
* plus a ground ring at the feet. X/Z arrows also drag the character on the
|
|
13
14
|
* floor (Y is display-only — characters stand on the deck). */
|
|
14
|
-
export function ShotRig({ preset, nonce, fovDeg, charA, charB, showB,
|
|
15
|
+
export function ShotRig({ preset, nonce, fovDeg, charA, charB, showB, probe: playbackProbeInput = {}, camRef, look, onMetrics, appliedPresetRef, lastPosRef }) {
|
|
15
16
|
useEffect(() => {
|
|
16
17
|
const cam = camRef.current;
|
|
17
18
|
if (!cam) return;
|
|
@@ -65,13 +66,11 @@ export function ShotRig({ preset, nonce, fovDeg, charA, charB, showB, probeX, pr
|
|
|
65
66
|
useFrame(() => {
|
|
66
67
|
const cam = camRef.current;
|
|
67
68
|
if (!cam) return;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (probe.current.z < 1 && Math.abs(probe.current.x) <= 1 && Math.abs(probe.current.y) <= 1) visible = true;
|
|
74
|
-
}
|
|
69
|
+
// During motion playback the probe follows the played root, so the
|
|
70
|
+
// "subject out of frame" caption stays honest.
|
|
71
|
+
const frame = playbackProbeInput.frameRef?.current ?? 0;
|
|
72
|
+
const current = playbackProbe(playbackProbeInput, frame, charA);
|
|
73
|
+
const visible = probeVisibility(probe.current, cam, current, SUBJECT_HEIGHT_M);
|
|
75
74
|
const previous = lastMetrics.current;
|
|
76
75
|
if (
|
|
77
76
|
Math.abs(previous.x - cam.position.x) +
|
package/src/stage-characters.jsx
CHANGED
|
@@ -8,12 +8,38 @@ import { isProxyFigure } from "./scenes.js";
|
|
|
8
8
|
import { applyPartColours } from "./part-colours.js";
|
|
9
9
|
import { applyHipsOffset, applyPose, primeBindPose } from "./poses.js";
|
|
10
10
|
import { REST_BONES } from "./stage-startup-defaults.js";
|
|
11
|
+
import { playbackFrame, usePlaybackSnapshot } from "./motion/playback-frame-store.js";
|
|
12
|
+
import { placementAt } from "./root-path.js";
|
|
13
|
+
import { shotAtFrame } from "./cuts.js";
|
|
14
|
+
import { sampleAt } from "./sample-at.js";
|
|
11
15
|
|
|
12
16
|
// Memoized: unchanged cast members skip re-rendering on every playhead tick.
|
|
13
17
|
export const Character = memo(function Character(props) {
|
|
14
18
|
return isProxyFigure(props) ? <ProxyFigure {...props} /> : <RiggedCharacter {...props} />;
|
|
15
19
|
});
|
|
16
20
|
|
|
21
|
+
export const PlaybackCharacter = memo(function PlaybackCharacter({
|
|
22
|
+
entry,
|
|
23
|
+
clip,
|
|
24
|
+
shots,
|
|
25
|
+
...props
|
|
26
|
+
}) {
|
|
27
|
+
const frame = usePlaybackSnapshot(playbackFrame);
|
|
28
|
+
const placement = placementAt(entry, frame, {
|
|
29
|
+
shotAt: (at) => shotAtFrame(shots, at),
|
|
30
|
+
takeRoot: clip ? sampleAt({ frameCount: clip.frames, motion: clip }, null, frame).subject : null,
|
|
31
|
+
});
|
|
32
|
+
return (
|
|
33
|
+
<Character
|
|
34
|
+
{...props}
|
|
35
|
+
position={[placement.x, entry.y ?? 0, placement.z]}
|
|
36
|
+
rot={placement.rot}
|
|
37
|
+
pose={clip ? null : (placement.pose ?? props.pose)}
|
|
38
|
+
posture={placement.posture}
|
|
39
|
+
/>
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
17
43
|
const RiggedCharacter = memo(function RiggedCharacter({ url, position, rot, tint, pose, scale = 1, onRig, pickId, partColoursEnabled = false, partColoursMode = "shaded" }) {
|
|
18
44
|
const fbx = useFBX(url);
|
|
19
45
|
const model = useMemo(() => {
|