@dailies-studio/shot-editor 0.1.0 → 0.2.0
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/dist/.vite/manifest.json +382 -0
- package/dist/assets/AgentPanel-CQejLbiX.js +2 -0
- package/dist/assets/AgentPanel-DJEphah0.css +1 -0
- package/dist/assets/AgentQuestionDialog-BqkD0kXF.js +1 -0
- package/dist/assets/CodeStudio-Dv4ihHpz.js +23 -0
- package/dist/assets/CommentsPanel-B4yhhT1T.js +1 -0
- package/dist/assets/ComponentLibraryRail-Ch_dsAru.js +1 -0
- package/dist/assets/ComponentStage-BRhRotyH.js +1 -0
- package/dist/assets/CurvePane-BjFVabKX.css +1 -0
- package/dist/assets/CurvePane-R22WzllJ.js +1 -0
- package/dist/assets/ExportDialog-CWLQWTmG.js +2 -0
- package/dist/assets/FontPicker-DWKaYL7F.js +1 -0
- package/dist/assets/FontPicker-v0IOIArM.css +1 -0
- package/dist/assets/GenerateDrawer-Bh3SqDKt.js +2 -0
- package/dist/assets/InsertDrawer-Ce1fod1L.js +1 -0
- package/dist/assets/LibraryBrowser-D9sEGz5w.js +2 -0
- package/dist/assets/MakeItYoursSection-CMO4319_.js +2 -0
- package/dist/assets/MaterialGraphCanvas-BFzl4wWR.js +23 -0
- package/dist/assets/MaterialGraphCanvas-BIUD59ik.css +1 -0
- package/dist/assets/MaterialWorkspace-CqTWqaRA.js +14 -0
- package/dist/assets/PreferencesDialog-CBf9_OoD.js +1 -0
- package/dist/assets/PresetsSection-CUEaoCVU.js +1 -0
- package/dist/assets/SoundHistory-BeS8EW1h.js +1 -0
- package/dist/assets/SoundMode-BmcZru3i.js +3 -0
- package/dist/assets/StockSounds-DdAUGRYK.js +1 -0
- package/dist/assets/TemplateFieldSection-CaeXpLFo.js +1 -0
- package/dist/assets/VersionHistoryPanel-CxG6XhNP.js +1 -0
- package/dist/assets/board-B88EPuLO.css +1 -0
- package/dist/assets/board-CL-he7SL.js +3 -0
- package/dist/assets/components-0oOSPN0l.js +1 -0
- package/dist/assets/googleFontRanges-gp32wJMo.js +1 -0
- package/dist/assets/googleFonts-BNon6M7X.js +1 -0
- package/dist/assets/iconsAndAliases-DL7gXP5j.js +1863 -0
- package/dist/assets/index-4F5NQmI8.css +1 -0
- package/dist/assets/index-BGHQGV5z.js +117 -0
- package/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/assets/libraryModel-CpIwfFha.js +1 -0
- package/dist/assets/lucide-6iK64tTB.js +9319 -0
- package/dist/assets/motionStamp-CH-Hew4E.js +1 -0
- package/dist/assets/takeMeasure-Tdg0TxEi.js +1 -0
- package/dist/assets/webmcp-BsB9gxWC.js +2 -0
- package/dist/index.html +34 -3
- package/package.json +12 -9
- package/src/addFamilies.ts +66 -0
- package/src/api.ts +1631 -55
- package/src/app/App.tsx +977 -198
- package/src/app/lazyPanel.tsx +137 -0
- package/src/app/lazySurfaces.tsx +203 -0
- package/src/audio/index.tsx +41 -10
- package/src/audio/meter.ts +485 -0
- package/src/audio/meterMath.ts +273 -0
- package/src/audio/meterStore.ts +155 -0
- package/src/audio/offlineMix.ts +289 -0
- package/src/audio/preview.ts +928 -118
- package/src/bridge.ts +15 -5
- package/src/bridgeTypes.ts +1611 -36
- package/src/channels.ts +17 -32
- package/src/fonts/googleFonts.ts +4 -0
- package/src/hostApi.ts +174 -0
- package/src/icons.tsx +770 -0
- package/src/layerWords.ts +61 -0
- package/src/main.tsx +56 -4
- package/src/panels/ResizeHandle.tsx +33 -43
- package/src/panels/agent/AgentDots.tsx +36 -0
- package/src/panels/agent/AgentPanel.tsx +119 -43
- package/src/panels/agent/AgentQuestionDialog.tsx +202 -0
- package/src/panels/agent/agent.css +113 -97
- package/src/panels/agent/agentGate.ts +35 -0
- package/src/panels/code/CodeDefaultsPanel.tsx +345 -0
- package/src/panels/code/CodeStudio.tsx +373 -0
- package/src/panels/code/FileTree.tsx +265 -0
- package/src/panels/code/codeBufferGuard.ts +52 -0
- package/src/panels/code/codeParts.tsx +321 -0
- package/src/panels/comments/CommentComposer.tsx +239 -0
- package/src/panels/comments/CommentThread.tsx +346 -0
- package/src/panels/comments/CommentsPanel.tsx +336 -0
- package/src/panels/comments/InboxButton.tsx +131 -0
- package/src/panels/comments/MentionMenu.tsx +98 -0
- package/src/panels/comments/parts.tsx +171 -0
- package/src/panels/export/ExportDialog.tsx +919 -96
- package/src/panels/export/ExportScrim.tsx +57 -0
- package/src/panels/files/AssetsTab.tsx +656 -0
- package/src/panels/files/FilePanel.tsx +1466 -216
- package/src/panels/files/LayerMarks.tsx +40 -0
- package/src/panels/files/LeftColumn.tsx +37 -0
- package/src/panels/files/McpSection.tsx +690 -47
- package/src/panels/files/MissingAssetsNotice.tsx +60 -0
- package/src/panels/files/assetDrag.ts +79 -0
- package/src/panels/files/resourceParts.tsx +255 -0
- package/src/panels/fonts/FontPicker.tsx +762 -0
- package/src/panels/fonts/faceCache.ts +207 -0
- package/src/panels/fonts/fontPicker.css +271 -0
- package/src/panels/fonts/pickerModel.ts +265 -0
- package/src/panels/fonts/pickerRequest.ts +46 -0
- package/src/panels/header/AccountAvatar.tsx +76 -0
- package/src/panels/header/AgentChip.tsx +77 -0
- package/src/panels/header/AppMenu.tsx +213 -0
- package/src/panels/header/Header.tsx +564 -84
- package/src/panels/header/HistoryControls.tsx +31 -5
- package/src/panels/header/PresenceStack.tsx +329 -0
- package/src/panels/header/ProjectTitle.tsx +232 -0
- package/src/panels/header/ShareButton.tsx +59 -0
- package/src/panels/header/StatusBadges.tsx +358 -27
- package/src/panels/header/SyncDeletedDialog.tsx +88 -0
- package/src/panels/header/ZoomControls.tsx +46 -15
- package/src/panels/header/agentHandoff.ts +52 -0
- package/src/panels/header/leave.ts +26 -0
- package/src/panels/help/HelpSection.tsx +189 -163
- package/src/panels/insert/InsertDrawer.tsx +723 -0
- package/src/panels/insert/iconSet.ts +109 -0
- package/src/panels/inspector/ExposeDot.tsx +117 -0
- package/src/panels/inspector/FramingRows.tsx +314 -44
- package/src/panels/inspector/InspectorFooter.tsx +91 -0
- package/src/panels/inspector/InspectorHead.tsx +425 -0
- package/src/panels/inspector/InspectorPanel.tsx +408 -124
- package/src/panels/inspector/KeyframeDot.tsx +373 -0
- package/src/panels/inspector/KeyframeSection.tsx +257 -0
- package/src/panels/inspector/MarkerSection.tsx +55 -11
- package/src/panels/inspector/ParamRow.tsx +84 -53
- package/src/panels/inspector/RightColumn.tsx +152 -0
- package/src/panels/inspector/TimeSection.tsx +866 -0
- package/src/panels/inspector/animate/EaseControl.tsx +244 -0
- package/src/panels/inspector/animate/PresetsSection.tsx +135 -0
- package/src/panels/inspector/animate/ShotSection.tsx +562 -48
- package/src/panels/inspector/design/AlignRow.tsx +95 -31
- package/src/panels/inspector/design/AppearanceSection.tsx +118 -9
- package/src/panels/inspector/design/ClonerSection.tsx +323 -62
- package/src/panels/inspector/design/ComponentSection.tsx +50 -43
- package/src/panels/inspector/design/CornerSection.tsx +45 -14
- package/src/panels/inspector/design/DefinitionProps.tsx +315 -0
- package/src/panels/inspector/design/DefinitionSection.tsx +136 -0
- package/src/panels/inspector/design/DeviceSection.tsx +210 -0
- package/src/panels/inspector/design/FillSection.tsx +83 -51
- package/src/panels/inspector/design/FontField.tsx +196 -0
- package/src/panels/inspector/design/HtmlSection.tsx +23 -12
- package/src/panels/inspector/design/InstanceSection.tsx +373 -154
- package/src/panels/inspector/design/MaskSection.tsx +84 -0
- package/src/panels/inspector/design/MaterialSection.tsx +177 -0
- package/src/panels/inspector/design/MediaSection.tsx +273 -120
- package/src/panels/inspector/design/MeshSection.tsx +910 -82
- package/src/panels/inspector/design/PaintRows.tsx +550 -0
- package/src/panels/inspector/design/PlateStyleSection.tsx +177 -0
- package/src/panels/inspector/design/ShadowSection.tsx +221 -48
- package/src/panels/inspector/design/SplineFollowSection.tsx +235 -0
- package/src/panels/inspector/design/StrokeSection.tsx +228 -43
- package/src/panels/inspector/design/TemplateFieldSection.tsx +57 -0
- package/src/panels/inspector/design/TransformSection.tsx +244 -50
- package/src/panels/inspector/design/TypographySection.tsx +679 -110
- package/src/panels/inspector/layout.ts +295 -0
- package/src/panels/inspector/propValue.ts +199 -0
- package/src/panels/inspector/scene/CameraSection.tsx +232 -22
- package/src/panels/inspector/scene/EffectsSection.tsx +722 -71
- package/src/panels/inspector/scene/MakeItYoursSection.tsx +180 -0
- package/src/panels/inspector/scene/SceneDocSection.tsx +191 -0
- package/src/panels/inspector/scene/SceneSection.tsx +219 -44
- package/src/panels/inspector/scene/TransitionSection.tsx +523 -72
- package/src/panels/inspector/storyboard/StoryboardReferenceSection.tsx +593 -0
- package/src/panels/inspector/storyboard/VideoStill.tsx +120 -0
- package/src/panels/inspector/timeModel.ts +190 -0
- package/src/panels/inspector/useDocWrite.ts +438 -146
- package/src/panels/library/LibraryBrowser.tsx +435 -0
- package/src/panels/library/LibraryGrid.tsx +190 -0
- package/src/panels/library/LibraryPreviewPanel.tsx +349 -0
- package/src/panels/library/LibraryRail.tsx +84 -0
- package/src/panels/library/LibrarySoundPanel.tsx +293 -0
- package/src/panels/library/SoundHistory.tsx +245 -0
- package/src/panels/library/StockSounds.tsx +390 -0
- package/src/panels/library/libraryModel.ts +147 -0
- package/src/panels/library/stockSearch.ts +77 -0
- package/src/panels/maskMarks.tsx +117 -0
- package/src/panels/material/MaterialCommentNode.tsx +54 -0
- package/src/panels/material/MaterialGraphCanvas.tsx +861 -0
- package/src/panels/material/MaterialGraphMenu.tsx +45 -0
- package/src/panels/material/MaterialGroupNode.tsx +56 -0
- package/src/panels/material/MaterialLibrary.tsx +253 -0
- package/src/panels/material/MaterialNode.tsx +453 -0
- package/src/panels/material/MaterialNodePalette.tsx +349 -0
- package/src/panels/material/MaterialParameterControls.tsx +525 -0
- package/src/panels/material/MaterialRerouteNode.tsx +42 -0
- package/src/panels/material/MaterialWorkspace.tsx +501 -0
- package/src/panels/material/NodePreview.tsx +70 -0
- package/src/panels/material/flowModel.ts +441 -0
- package/src/panels/material/parameterValues.ts +231 -0
- package/src/panels/material/previewScheduler.ts +235 -0
- package/src/panels/material/reactFlow.css +6 -0
- package/src/panels/material/socketGlyph.ts +85 -0
- package/src/panels/material/viewportMemory.ts +58 -0
- package/src/panels/preferences/PreferencesDialog.tsx +215 -0
- package/src/panels/share/ShareComponentDialog.tsx +247 -0
- package/src/panels/stage/StageMenu.tsx +35 -5
- package/src/panels/stage/StagePanel.tsx +338 -23
- package/src/panels/stage/cellSlots.tsx +70 -0
- package/src/panels/storyboard/BoardCanvas.tsx +739 -0
- package/src/panels/storyboard/BoardCursors.tsx +158 -0
- package/src/panels/storyboard/BoardDropDialog.tsx +161 -0
- package/src/panels/storyboard/BoardPlan.tsx +42 -0
- package/src/panels/storyboard/ComponentSheet.tsx +187 -0
- package/src/panels/storyboard/ComponentStage.tsx +235 -0
- package/src/panels/storyboard/MiniSketch.tsx +173 -0
- package/src/panels/storyboard/PartPicker.tsx +64 -0
- package/src/panels/storyboard/PlanPanel.tsx +617 -0
- package/src/panels/storyboard/ReferencePreview.tsx +217 -0
- package/src/panels/storyboard/ShotBrief.tsx +360 -0
- package/src/panels/storyboard/ShotCard.tsx +251 -0
- package/src/panels/storyboard/StagePreview.tsx +348 -0
- package/src/panels/storyboard/StillThumbs.tsx +162 -0
- package/src/panels/storyboard/StoryboardView.tsx +79 -0
- package/src/panels/storyboard/board.css +1065 -0
- package/src/panels/storyboard/board.ts +12 -0
- package/src/panels/storyboard/boardClipboard.ts +111 -0
- package/src/panels/storyboard/boardOrder.ts +68 -0
- package/src/panels/storyboard/boardRows.tsx +425 -0
- package/src/panels/storyboard/liveCap.ts +90 -0
- package/src/panels/storyboard/plan.css +295 -0
- package/src/panels/storyboard/posterCache.ts +133 -0
- package/src/panels/storyboard/shotboard/ComponentPicker.tsx +373 -0
- package/src/panels/storyboard/shotboard/ImagePicker.tsx +145 -0
- package/src/panels/storyboard/shotboard/ShotBoard.tsx +292 -0
- package/src/panels/storyboard/shotboard/SketchCanvas.tsx +443 -0
- package/src/panels/storyboard/shotboard/SketchInspector.tsx +471 -0
- package/src/panels/storyboard/shotboard/SketchItem.tsx +240 -0
- package/src/panels/storyboard/shotboard/SketchLayers.tsx +130 -0
- package/src/panels/storyboard/shotboard/SketchPlay.tsx +108 -0
- package/src/panels/storyboard/shotboard/SketchStrip.tsx +178 -0
- package/src/panels/storyboard/shotboard/SketchToolFooter.tsx +99 -0
- package/src/panels/storyboard/shotboard/sketch.css +1392 -0
- package/src/panels/storyboard/shotboard/sketchModel.ts +219 -0
- package/src/panels/storyboard/storyboardDrop.ts +164 -0
- package/src/panels/studio/ComponentLibraryRail.tsx +346 -0
- package/src/panels/studio/ComponentSidePanel.tsx +355 -0
- package/src/panels/studio/ComponentStudio.tsx +243 -0
- package/src/panels/studio/ComponentsHome.tsx +318 -0
- package/src/panels/studio/ComponentsPreview.tsx +179 -0
- package/src/panels/studio/EveryUse.tsx +244 -0
- package/src/panels/studio/ThisUsePanel.tsx +679 -0
- package/src/panels/studio/componentParts.tsx +341 -0
- package/src/panels/studio/components.ts +6 -0
- package/src/panels/studio/newCodeComponent.ts +66 -0
- package/src/panels/timeline/AudioFxButton.tsx +290 -0
- package/src/panels/timeline/AudioLane.tsx +674 -85
- package/src/panels/timeline/BlockRow.tsx +154 -85
- package/src/panels/timeline/ChannelRow.tsx +378 -129
- package/src/panels/timeline/CurvePane.tsx +508 -0
- package/src/panels/timeline/Filmstrip.tsx +361 -0
- package/src/panels/timeline/GenerateDrawer.tsx +407 -0
- package/src/panels/timeline/KeyStretch.tsx +115 -0
- package/src/panels/timeline/LifespanRow.tsx +730 -47
- package/src/panels/timeline/MarkerLane.tsx +46 -8
- package/src/panels/timeline/PlayButton.tsx +91 -0
- package/src/panels/timeline/PropertyPicker.tsx +254 -0
- package/src/panels/timeline/ShotRow.tsx +400 -0
- package/src/panels/timeline/SoundMode.tsx +1406 -0
- package/src/panels/timeline/TimelineDock.tsx +2338 -272
- package/src/panels/timeline/TransportVolume.tsx +163 -0
- package/src/panels/timeline/beatRuler.ts +74 -0
- package/src/panels/timeline/curveModel.ts +500 -0
- package/src/panels/timeline/duckUnderVoice.ts +68 -0
- package/src/panels/timeline/keyframeMenu.ts +138 -0
- package/src/panels/timeline/labelWidth.ts +49 -0
- package/src/panels/timeline/marquee.ts +155 -0
- package/src/panels/timeline/model.ts +1860 -199
- package/src/panels/timeline/snap.ts +125 -43
- package/src/panels/timeline/staggerPreview.tsx +92 -0
- package/src/panels/timeline/takeMeasure.ts +72 -0
- package/src/panels/timeline/useLive.ts +48 -0
- package/src/panels/timeline/viewport.ts +234 -0
- package/src/panels/toolbar/BackChip.tsx +116 -0
- package/src/panels/toolbar/CameraViewBar.tsx +118 -49
- package/src/panels/toolbar/CanvasBar.tsx +117 -0
- package/src/panels/toolbar/InsertButton.tsx +38 -0
- package/src/panels/toolbar/IsolationTrail.tsx +156 -0
- package/src/panels/toolbar/PanelToggles.tsx +86 -0
- package/src/panels/toolbar/ShadingBar.tsx +119 -26
- package/src/panels/toolbar/ToolDockBar.tsx +329 -80
- package/src/panels/toolbar/ToolFooter.tsx +102 -0
- package/src/panels/toolbar/ViewModeBar.tsx +247 -38
- package/src/panels/toolbar/WorkPlaneBar.tsx +94 -0
- package/src/panels/toolbar/useAddRows.tsx +326 -0
- package/src/panels/toolbar/useDismiss.ts +122 -0
- package/src/panels/versions/VersionHistoryPanel.tsx +274 -0
- package/src/panels/versions/versionModel.ts +103 -0
- package/src/panels/virtualWindow.ts +271 -0
- package/src/shotTokens.css +151 -60
- package/src/stage/FrustumGizmo.tsx +504 -84
- package/src/stage/GridPlane.tsx +225 -16
- package/src/stage/NodePreviews.tsx +628 -0
- package/src/stage/PerfHud.tsx +437 -47
- package/src/stage/SplitRenderer.tsx +816 -169
- package/src/stage/StageApp.tsx +4274 -486
- package/src/stage/StageRootBoundary.tsx +114 -0
- package/src/stage/StageTools.tsx +651 -151
- package/src/stage/TransformGizmo.tsx +696 -0
- package/src/stage/adaptivePresentation.ts +145 -0
- package/src/stage/bridgeClient.ts +2522 -41
- package/src/stage/exportWorker.ts +9 -0
- package/src/stage/feed.ts +305 -14
- package/src/stage/fontPreview.ts +176 -0
- package/src/stage/frustumGeometry.ts +319 -1
- package/src/stage/furnitureTokens.ts +33 -0
- package/src/stage/gizmoMath.ts +508 -16
- package/src/stage/gpuTimer.ts +276 -0
- package/src/stage/instanceModules.ts +194 -33
- package/src/stage/interactions/move.ts +24 -5
- package/src/stage/isolation.ts +291 -33
- package/src/stage/jsSelfProfiler.ts +325 -0
- package/src/stage/main.tsx +116 -16
- package/src/stage/modalState.ts +222 -23
- package/src/stage/moduleEpoch.ts +60 -0
- package/src/stage/motionPathStage.ts +279 -0
- package/src/stage/motionPreview.ts +74 -0
- package/src/stage/overlays/Overlays.tsx +1112 -159
- package/src/stage/overlays/Rulers.tsx +495 -0
- package/src/stage/panes.ts +159 -10
- package/src/stage/project.ts +68 -3
- package/src/stage/renderScaleController.ts +730 -0
- package/src/stage/rulers.ts +129 -0
- package/src/stage/sceneExtent.ts +75 -0
- package/src/stage/shading.ts +20 -0
- package/src/stage/splineStage.ts +632 -0
- package/src/stage/stagePerformanceTelemetry.ts +717 -0
- package/src/stage/stageTheme.ts +36 -0
- package/src/stage/stageWarmup.ts +11 -0
- package/src/stage/textAutoSize.ts +124 -0
- package/src/stage/tier1Export.ts +641 -0
- package/src/stage/tokenColor.ts +14 -9
- package/src/stage/view.ts +56 -9
- package/src/stage/workingView.ts +277 -33
- package/src/state/accountPrefs.ts +261 -0
- package/src/state/agentActions.ts +173 -15
- package/src/state/agentActivity.ts +198 -0
- package/src/state/agentQuestion.ts +239 -0
- package/src/state/bootOpen.ts +42 -0
- package/src/state/cameraVerbs.ts +139 -0
- package/src/state/clientId.ts +6 -1
- package/src/state/comments.ts +1099 -0
- package/src/state/context.tsx +19 -1
- package/src/state/editorPrefs.ts +220 -0
- package/src/state/errorReports.ts +121 -0
- package/src/state/fieldWrite.ts +161 -0
- package/src/state/fontRecents.ts +70 -0
- package/src/state/fonts.ts +656 -0
- package/src/state/graphClipboard.ts +414 -0
- package/src/state/imageThumbs.ts +216 -0
- package/src/state/insert.ts +444 -67
- package/src/state/isolationTrail.ts +92 -0
- package/src/state/keyMoves.ts +134 -0
- package/src/state/keyframeClipboard.ts +177 -0
- package/src/state/keymap.ts +1153 -117
- package/src/state/layoutPrefs.ts +272 -0
- package/src/state/layouts.ts +205 -0
- package/src/state/measureMedia.ts +167 -0
- package/src/state/menu.ts +2341 -28
- package/src/state/motionStamp.ts +7 -0
- package/src/state/panelSize.ts +375 -5
- package/src/state/pendingChanges.ts +119 -0
- package/src/state/playHold.ts +78 -0
- package/src/state/playbackPreparation.ts +562 -0
- package/src/state/posterCapture.ts +242 -0
- package/src/state/presence.ts +254 -0
- package/src/state/projectComponents.ts +160 -0
- package/src/state/renderEvents.ts +298 -18
- package/src/state/runtimeErrors.ts +46 -0
- package/src/state/selection.ts +12 -0
- package/src/state/shortcuts.ts +1606 -0
- package/src/state/sketchGuide.ts +51 -0
- package/src/state/splineRefs.ts +87 -0
- package/src/state/stageHealth.ts +259 -0
- package/src/state/stageWatchdog.ts +47 -0
- package/src/state/stillStage.ts +330 -0
- package/src/state/stillThumbs.ts +474 -0
- package/src/state/store.ts +23591 -2982
- package/src/state/storyboardModel.ts +528 -0
- package/src/state/sync.ts +1257 -0
- package/src/state/syncCache.ts +159 -0
- package/src/state/triangles.ts +13 -0
- package/src/state/types.ts +1698 -55
- package/src/state/unsavedGuard.ts +49 -0
- package/src/state/viewMode.ts +200 -0
- package/src/state/webmcp.ts +1128 -0
- package/src/state/workspace.ts +1147 -0
- package/src/styles/assets-tab.css +304 -0
- package/src/styles/canvas.css +545 -0
- package/src/styles/comments.css +1435 -0
- package/src/styles/controls.css +162 -0
- package/src/styles/dialogs.css +390 -0
- package/src/styles/insert-drawer.css +586 -0
- package/src/styles/inspector-board.css +207 -0
- package/src/styles/inspector-object.css +486 -0
- package/src/styles/inspector-shot.css +252 -0
- package/src/styles/inspector.css +360 -0
- package/src/styles/left-column.css +1628 -0
- package/src/styles/library-preview.css +575 -0
- package/src/styles/library.css +512 -0
- package/src/styles/modes/code.css +569 -0
- package/src/styles/modes/component.css +1561 -0
- package/src/styles/modes/material.css +1201 -0
- package/src/styles/modes/storyboard.css +518 -0
- package/src/styles/share-component.css +49 -0
- package/src/styles/shell.css +653 -0
- package/src/styles/shot-brief.css +360 -0
- package/src/styles/states.css +76 -0
- package/src/styles/sync.css +380 -0
- package/src/styles/timeline-graph.css +384 -0
- package/src/styles/timeline-meters.css +89 -0
- package/src/styles/timeline-rows.css +1959 -0
- package/src/styles/timeline-sound.css +1003 -0
- package/src/styles/timeline.css +754 -0
- package/src/styles/tool-footer.css +525 -0
- package/src/styles/topbar.css +886 -0
- package/src/styles/versions.css +262 -0
- package/src/styles.css +72 -2921
- package/src/tools.ts +108 -23
- package/dist/assets/index-B7notge7.css +0 -1
- package/dist/assets/index-BBE6Ta9L.js +0 -52
- package/src/panels/files/icons.tsx +0 -118
- package/src/panels/header/DocSwitcher.tsx +0 -85
- package/src/panels/header/TransportBar.tsx +0 -86
- package/src/panels/inspector/animate/BlockTiming.tsx +0 -341
- package/src/panels/inspector/animate/DuringBlocks.tsx +0 -85
- package/src/panels/inspector/animate/InOutPresets.tsx +0 -57
- package/src/panels/inspector/animate/ResolvedChannels.tsx +0 -150
- package/src/panels/inspector/animate/useBlocks.ts +0 -43
- package/src/panels/material/MaterialPicker.tsx +0 -172
- package/src/panels/timeline/SceneStrip.tsx +0 -145
- package/src/panels/toolbar/AddMenu.tsx +0 -230
- package/src/stage/MoveGizmo.tsx +0 -411
|
@@ -0,0 +1,866 @@
|
|
|
1
|
+
// WHEN A LAYER IS ON SCREEN, AND — FOR A RECORDING — WHICH PART OF IT PLAYS.
|
|
2
|
+
//
|
|
3
|
+
// ── WHY THIS SECTION HAD TO EXIST ───────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// A lifetime was reachable only by dragging a bar in the dock. That is the
|
|
6
|
+
// right gesture and it is the only one, so "start this at frame 90" was a drag
|
|
7
|
+
// against a ruler, unrepeatable across two layers and impossible at a zoom
|
|
8
|
+
// where a frame is a third of a pixel. The clip fields were worse: `sourceIn`,
|
|
9
|
+
// `sourceOut` and the natural length of a recording had no field ANYWHERE in
|
|
10
|
+
// the chrome, and the three things that can end a clip were indistinguishable
|
|
11
|
+
// by looking at it.
|
|
12
|
+
//
|
|
13
|
+
// ── ITS ASYMMETRY IS `useDocWrite`'s, AND BOTH HALVES ARE LIVE HERE ─────
|
|
14
|
+
//
|
|
15
|
+
// Read that file's header first. Readouts come from the layer AS PAINTED at the
|
|
16
|
+
// playhead (`useSelectedLayers`, which runs `resolveTracksAt`), and writes are
|
|
17
|
+
// compensated so the rendered value lands on what was typed. This section is
|
|
18
|
+
// where the two halves are visibly different work rather than the same read
|
|
19
|
+
// twice, because it has one field of each kind:
|
|
20
|
+
//
|
|
21
|
+
// THE GAIN IS TRACKED. `video.sound.gain` is a keyframable path (tracks.ts),
|
|
22
|
+
// so its base is DEAD the moment a track exists — `routeTrackedEdit` writes
|
|
23
|
+
// the old base back. A row reading the base would show a number the mixer
|
|
24
|
+
// stopped using, and typing the number the field displayed would diff
|
|
25
|
+
// against itself and write nothing at all. So it reads through the resolved
|
|
26
|
+
// layer and writes through `useSelectionWrite`, which turns the diff into a
|
|
27
|
+
// keyframe at the playhead.
|
|
28
|
+
// EVERY TIME FIELD IS NOT. A lifetime has no track, so there is
|
|
29
|
+
// nothing to compensate — but the value still must not be read at RENDER
|
|
30
|
+
// and written back later. Each write below re-reads its subject from the
|
|
31
|
+
// document `useDocWrite` hands it and computes the delta THERE, so a commit
|
|
32
|
+
// that lands after an undo, a scene crossing or another panel's edit is
|
|
33
|
+
// measured against the document it is actually writing into.
|
|
34
|
+
//
|
|
35
|
+
// ── AND EVERY TIME EDIT GOES THROUGH AN OP THAT ALREADY EXISTS ──────────
|
|
36
|
+
//
|
|
37
|
+
// `writeLifespan` (panels/timeline/model.ts) is what the dock's bars drag
|
|
38
|
+
// through. It forks a VIDEO layer in a scene onto `trimLayer`/`moveLayers`,
|
|
39
|
+
// which bound an edge by the FOOTAGE as well as by the scene, and leaves a
|
|
40
|
+
// plain layer on the clamped lifetime write. Typing a number and dragging a bar
|
|
41
|
+
// are one operation with different input devices — the rule `useDocWrite`'s
|
|
42
|
+
// header states for the canvas — so this section spends a subtraction to reach
|
|
43
|
+
// the delta API rather than owning a second set of clamps that would disagree
|
|
44
|
+
// with the bars on the first edge case.
|
|
45
|
+
//
|
|
46
|
+
// ── ONE LAYER, DECLARED ─────────────────────────────────────────────────
|
|
47
|
+
//
|
|
48
|
+
// A lifetime is a property of one layer and every clip op takes one `layerId`.
|
|
49
|
+
// A fan-out would need `moveLayers`' all-or-nothing posture for the edges and
|
|
50
|
+
// has no meaning at all for "source in", so this section renders for a single
|
|
51
|
+
// selection and withholds itself otherwise — the rule `component`, `instance`,
|
|
52
|
+
// `triggers` and `material` already follow (layout.ts).
|
|
53
|
+
//
|
|
54
|
+
// CEILING, stated: a NEGATIVE source in is readable here and not typeable. It
|
|
55
|
+
// means the window opens before the recording does, which is what `splitScene`
|
|
56
|
+
// leaves behind for a clip already playing at a cut; `setVideoSource` clamps a
|
|
57
|
+
// requested source frame to the recording, so the row shows the lead and
|
|
58
|
+
// refuses to invent one. Slipping the clip in the dock is what moves it.
|
|
59
|
+
//
|
|
60
|
+
// ── EVERY FIELD DRAGS LIVE ──────────────────────────────────────────────
|
|
61
|
+
//
|
|
62
|
+
// Each number's drag runs the SAME op its typed value does, as a scrub: one
|
|
63
|
+
// undo entry, every capture re-derived from the gesture's base (so an edge's
|
|
64
|
+
// delta is measured against the document the drag started from), and the
|
|
65
|
+
// store's lock rule applied to each capture. A capture an op refuses (a retime
|
|
66
|
+
// the sound forbids, a fade longer than the sound) holds the last value the
|
|
67
|
+
// same gesture landed, and the refusal is said once, at the release.
|
|
68
|
+
import { useRef, useState } from "react";
|
|
69
|
+
import {
|
|
70
|
+
InspectorRow,
|
|
71
|
+
InspectorSection,
|
|
72
|
+
NumberField,
|
|
73
|
+
PropertyGrid,
|
|
74
|
+
SegmentedControl,
|
|
75
|
+
Toggle,
|
|
76
|
+
useLocked,
|
|
77
|
+
wordOptions,
|
|
78
|
+
} from "@dailies-studio/ui";
|
|
79
|
+
import type { ScrubHandlers } from "@dailies-studio/ui";
|
|
80
|
+
import {
|
|
81
|
+
MAX_AUDIO_GAIN,
|
|
82
|
+
MEDIA_LOOPS,
|
|
83
|
+
containerDurationOf,
|
|
84
|
+
defDurationFrames,
|
|
85
|
+
homeOf,
|
|
86
|
+
retimeVideo,
|
|
87
|
+
retimeVideoRefusal,
|
|
88
|
+
setVideoSound,
|
|
89
|
+
setVideoSoundRefusal,
|
|
90
|
+
setVideoSource,
|
|
91
|
+
videoClipOf,
|
|
92
|
+
} from "@dailies-studio/shot/doc";
|
|
93
|
+
import type {
|
|
94
|
+
ComponentLayer3D,
|
|
95
|
+
Doc3D,
|
|
96
|
+
InstanceLayer3D,
|
|
97
|
+
Layer3D,
|
|
98
|
+
MediaLoop,
|
|
99
|
+
Scene3D,
|
|
100
|
+
VideoLayer3D,
|
|
101
|
+
VideoSound,
|
|
102
|
+
} from "@dailies-studio/shot/doc";
|
|
103
|
+
import { useShotEditor, useStore } from "../../state/context.js";
|
|
104
|
+
import { framesToSeconds } from "@dailies-studio/engine";
|
|
105
|
+
import { formatDuration, lifespanOf, timecode, writeLifespan } from "../timeline/model.js";
|
|
106
|
+
import { KeyframeDot } from "./KeyframeDot.js";
|
|
107
|
+
import {
|
|
108
|
+
MAX_GAIN_DB,
|
|
109
|
+
MIN_GAIN_DB,
|
|
110
|
+
closingCaption,
|
|
111
|
+
closingEdge,
|
|
112
|
+
dbToGain,
|
|
113
|
+
gainAsDb,
|
|
114
|
+
naturalOutFrame,
|
|
115
|
+
} from "./timeModel.js";
|
|
116
|
+
import { useDocWrite, useScrub, useSelectedLayers, useSelectionScrub, useSelectionWrite } from "./useDocWrite.js";
|
|
117
|
+
|
|
118
|
+
/** The loops, as data, so a word renamed in the schema is a compile error here
|
|
119
|
+
* rather than a segment that writes a value the runtime ignores. */
|
|
120
|
+
const LOOP_OPTIONS = wordOptions(MEDIA_LOOPS);
|
|
121
|
+
|
|
122
|
+
/** A placement's loop (schema v47): the same two words, plus Off, which is the
|
|
123
|
+
* field's absence and what every older placement means. */
|
|
124
|
+
const PLACEMENT_LOOP_OFF = "off";
|
|
125
|
+
const PLACEMENT_LOOP_OPTIONS = [{ value: PLACEMENT_LOOP_OFF, label: "Off" }, ...LOOP_OPTIONS];
|
|
126
|
+
|
|
127
|
+
export function TimeSection() {
|
|
128
|
+
const store = useStore();
|
|
129
|
+
const write = useDocWrite();
|
|
130
|
+
const layers = useSelectedLayers();
|
|
131
|
+
const doc = useShotEditor((s) => s.doc);
|
|
132
|
+
const sceneId = useShotEditor((s) => s.sceneId);
|
|
133
|
+
const editingDef = useShotEditor((s) => s.editingDef);
|
|
134
|
+
|
|
135
|
+
// ONE layer, by the rule in this file's header. `layers` is already the
|
|
136
|
+
// selection AS PAINTED and in document order, so the single case is the
|
|
137
|
+
// whole of it.
|
|
138
|
+
const layer = layers.length === 1 ? layers[0] : undefined;
|
|
139
|
+
if (doc === null || layer === undefined) return null;
|
|
140
|
+
// The CONTAINER, the store's own `layerContainer` rule: a definition while
|
|
141
|
+
// one is open, and the scene otherwise. `writeLifespan` takes either.
|
|
142
|
+
const container = editingDef ?? sceneId;
|
|
143
|
+
if (container === null) return null;
|
|
144
|
+
// The SCENE, and null inside a definition. Every clip op resolves its layer
|
|
145
|
+
// through `doc.scenesById`, so a `cmp_` container would have them refuse
|
|
146
|
+
// silently — the same fork `writeLifespan` takes for exactly this reason.
|
|
147
|
+
const scene: Scene3D | undefined = editingDef === null && sceneId !== null ? doc.scenesById[sceneId] : undefined;
|
|
148
|
+
const life = lifespanOf(layer, containerDurationOf(doc, container));
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<InspectorSection className="insp-object" title="Time" persistKey="shot:time">
|
|
152
|
+
{/* A plain pair, two boxes across the row. A lifetime has no track, so
|
|
153
|
+
neither box carries a diamond, and neither is narrower for it. */}
|
|
154
|
+
<PropertyGrid>
|
|
155
|
+
<LifetimeField
|
|
156
|
+
side="in"
|
|
157
|
+
container={container}
|
|
158
|
+
layerId={layer.id}
|
|
159
|
+
value={life.in}
|
|
160
|
+
title={`Scene frame this layer appears on: ${timecode(life.in, doc.fps)}`}
|
|
161
|
+
onWrite={write}
|
|
162
|
+
/>
|
|
163
|
+
<LifetimeField
|
|
164
|
+
side="out"
|
|
165
|
+
container={container}
|
|
166
|
+
layerId={layer.id}
|
|
167
|
+
value={life.out}
|
|
168
|
+
title={`Scene frame this layer leaves on: ${timecode(life.out, doc.fps)}`}
|
|
169
|
+
onWrite={write}
|
|
170
|
+
/>
|
|
171
|
+
</PropertyGrid>
|
|
172
|
+
|
|
173
|
+
{/* THE TIMECODE, as a readout and not a second pair of fields. Frames are
|
|
174
|
+
what a lifetime is stored in and what a keyframe is written in, so the
|
|
175
|
+
fields stay in frames; the clock is what an author reads a cut
|
|
176
|
+
against, and having both means never converting by hand. Drawn as a
|
|
177
|
+
read-only BOX in the numeral face, the field's shape with nothing to
|
|
178
|
+
type into, so it reads as a value of this layer rather than a note:
|
|
179
|
+
the house readout (`insp-readout`, widgets.css). */}
|
|
180
|
+
<InspectorRow label="Timecode" className="insp-readout">
|
|
181
|
+
<span className="insp-dim" data-testid="time-timecode">
|
|
182
|
+
{timecode(life.in, doc.fps)} → {timecode(life.out, doc.fps)}
|
|
183
|
+
</span>
|
|
184
|
+
</InspectorRow>
|
|
185
|
+
|
|
186
|
+
{/* READ-ONLY, because it is not a third fact. Duration is `out - in`, and
|
|
187
|
+
a field for it would be a second absolute writer for one of the two
|
|
188
|
+
edges: typing into it would have to pick an edge to move, silently. */}
|
|
189
|
+
<InspectorRow label="Duration" className="insp-readout">
|
|
190
|
+
<span className="insp-dim" data-testid="time-duration">
|
|
191
|
+
{formatDuration(framesToSeconds(life.out - life.in, doc.fps))}
|
|
192
|
+
</span>
|
|
193
|
+
</InspectorRow>
|
|
194
|
+
|
|
195
|
+
{layer.kind === "video" && scene !== undefined && sceneId !== null ? (
|
|
196
|
+
<ClipRows doc={doc} scene={scene} sceneId={sceneId} layer={layer} />
|
|
197
|
+
) : null}
|
|
198
|
+
|
|
199
|
+
{layer.kind === "instance" || layer.kind === "component" ? <PlacementLoopRows doc={doc} layer={layer} /> : null}
|
|
200
|
+
|
|
201
|
+
{layer.kind === "video" && scene !== undefined && sceneId !== null && layer.video.sound !== undefined ? (
|
|
202
|
+
<SoundRows
|
|
203
|
+
doc={doc}
|
|
204
|
+
sceneId={sceneId}
|
|
205
|
+
layer={layer}
|
|
206
|
+
sound={layer.video.sound}
|
|
207
|
+
onRefusal={(message) => store.reportError(message)}
|
|
208
|
+
/>
|
|
209
|
+
) : null}
|
|
210
|
+
</InspectorSection>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* One lifetime edge, typed or dragged.
|
|
216
|
+
*
|
|
217
|
+
* Both halves re-read their subject from the document they are handed and
|
|
218
|
+
* measure the delta there. See this file's header: the typed value is
|
|
219
|
+
* absolute, `writeLifespan` is a gesture API, and the subtraction must not be
|
|
220
|
+
* taken against a render-time reading. A drag's every capture starts from the
|
|
221
|
+
* gesture's base, so its delta never compounds.
|
|
222
|
+
*/
|
|
223
|
+
function LifetimeField(props: {
|
|
224
|
+
side: "in" | "out";
|
|
225
|
+
container: string;
|
|
226
|
+
layerId: string;
|
|
227
|
+
value: number;
|
|
228
|
+
title: string;
|
|
229
|
+
onWrite: (write: (doc: Doc3D) => Doc3D) => void;
|
|
230
|
+
}) {
|
|
231
|
+
const { side, container, layerId } = props;
|
|
232
|
+
const edge = (base: Doc3D, to: number): Doc3D => {
|
|
233
|
+
const subject = homeOf(base, container)?.layersById[layerId];
|
|
234
|
+
if (subject === undefined) return base;
|
|
235
|
+
const now = lifespanOf(subject, containerDurationOf(base, container));
|
|
236
|
+
const delta = Math.round(to) - now[side];
|
|
237
|
+
return delta === 0 ? base : writeLifespan(base, container, layerId, side, delta);
|
|
238
|
+
};
|
|
239
|
+
const scrub = useScrub((doc, raw) => edge(doc, Number(raw)));
|
|
240
|
+
return (
|
|
241
|
+
<InspectorRow>
|
|
242
|
+
<NumberField
|
|
243
|
+
value={props.value}
|
|
244
|
+
label={side === "in" ? "In" : "Out"}
|
|
245
|
+
ariaLabel={side === "in" ? "Lifetime in" : "Lifetime out"}
|
|
246
|
+
unit="f"
|
|
247
|
+
min={0}
|
|
248
|
+
step={1}
|
|
249
|
+
title={props.title}
|
|
250
|
+
onCommit={(next) => props.onWrite((base) => edge(base, next))}
|
|
251
|
+
onScrubStart={scrub.start}
|
|
252
|
+
onScrub={scrub.move}
|
|
253
|
+
onScrubEnd={scrub.end}
|
|
254
|
+
/>
|
|
255
|
+
</InspectorRow>
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* A drag over one of this section's REFUSING ops: `useScrub`'s bracket, with
|
|
261
|
+
* the op's refusal kept from the last capture and said ONCE, at the release,
|
|
262
|
+
* the way `useSelectionScrub` says a lock.
|
|
263
|
+
*
|
|
264
|
+
* A REFUSED CAPTURE HOLDS THE LAST VALUE THIS GESTURE LANDED. Every capture
|
|
265
|
+
* re-derives from the gesture's base, so a refused one that wrote the base
|
|
266
|
+
* threw the whole drag away: a fade dragged to 10 and on past what the sound
|
|
267
|
+
* allows went back to 0 at the release. It re-runs the op with the last value
|
|
268
|
+
* it accepted instead (the press value when it accepted none), so the field
|
|
269
|
+
* stops where the op stopped and the release lands that.
|
|
270
|
+
*
|
|
271
|
+
* `refused` hears the refusal and the value it refused, for a row that holds
|
|
272
|
+
* it on screen; `landed` hears a release whose last capture was accepted, so
|
|
273
|
+
* that row can let go of a refusal it was still holding.
|
|
274
|
+
*/
|
|
275
|
+
function useOpScrub(
|
|
276
|
+
write: (doc: Doc3D, value: number) => { doc: Doc3D; refusal: string | null },
|
|
277
|
+
on: { refused?: (why: string, value: number) => void; landed?: () => void } = {},
|
|
278
|
+
): ScrubHandlers {
|
|
279
|
+
const store = useStore();
|
|
280
|
+
const gesture = useRef<{ refusal: string | null; value: number; landed: number | null } | null>(null);
|
|
281
|
+
const scrub = useScrub((doc, raw) => {
|
|
282
|
+
const value = Number(raw);
|
|
283
|
+
const out = write(doc, value);
|
|
284
|
+
const landed = out.refusal === null ? value : (gesture.current?.landed ?? null);
|
|
285
|
+
gesture.current = { refusal: out.refusal, value, landed };
|
|
286
|
+
return out.refusal === null || landed === null ? out.doc : write(doc, landed).doc;
|
|
287
|
+
});
|
|
288
|
+
return {
|
|
289
|
+
start: () => {
|
|
290
|
+
gesture.current = null;
|
|
291
|
+
scrub.start();
|
|
292
|
+
},
|
|
293
|
+
move: scrub.move,
|
|
294
|
+
end: (commit) => {
|
|
295
|
+
scrub.end(commit);
|
|
296
|
+
const last = gesture.current;
|
|
297
|
+
gesture.current = null;
|
|
298
|
+
if (!commit || last === null) return;
|
|
299
|
+
if (last.refusal === null) {
|
|
300
|
+
on.landed?.();
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
store.reportError(last.refusal);
|
|
304
|
+
on.refused?.(last.refusal, last.value);
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* The clip half: which part of the recording this window selects, how fast it
|
|
311
|
+
* runs, what happens past its end, and which edge actually closes it.
|
|
312
|
+
*
|
|
313
|
+
* A child component rather than inline JSX because the video case needs its own
|
|
314
|
+
* hooks (the speed field keeps a draft while a refusal is on screen) and a hook
|
|
315
|
+
* cannot live behind the `kind === "video"` test above.
|
|
316
|
+
*/
|
|
317
|
+
function ClipRows(props: { doc: Doc3D; scene: Scene3D; sceneId: string; layer: VideoLayer3D }) {
|
|
318
|
+
const { doc, scene, sceneId, layer } = props;
|
|
319
|
+
const store = useStore();
|
|
320
|
+
const write = useDocWrite();
|
|
321
|
+
// Natural length is a plain button, so it reads a viewer's lock here.
|
|
322
|
+
const locked = useLocked();
|
|
323
|
+
const rate = layer.video.timeScale ?? 1;
|
|
324
|
+
const clip = videoClipOf(layer, scene.durationFrames, doc.fps, doc.assets[layer.video.asset]);
|
|
325
|
+
const close = closingEdge(clip, scene.durationFrames, rate);
|
|
326
|
+
const natural = naturalOutFrame(clip, layer.video.offsetFrames ?? 0, rate);
|
|
327
|
+
|
|
328
|
+
/** Both source edges through ONE op. `setVideoSource` is the only thing in
|
|
329
|
+
* the repo that converts between the two rulers, and its header says why:
|
|
330
|
+
* expressing a source statement through the gesture ops means the caller
|
|
331
|
+
* doing the `(f - offsetFrames) * timeScale` arithmetic, which is exactly
|
|
332
|
+
* the drift `videoClipOf` refuses. */
|
|
333
|
+
const source = (base: Doc3D, which: "inFrames" | "outFrames", to: number): Doc3D =>
|
|
334
|
+
setVideoSource(base, sceneId, layer.id, { [which]: Math.round(to) });
|
|
335
|
+
// The drag halves: `setVideoSource` clamps to the recording itself, so a
|
|
336
|
+
// capture past either end lands on it rather than refusing.
|
|
337
|
+
const inScrub = useScrub((doc, raw) => source(doc, "inFrames", Number(raw)));
|
|
338
|
+
const outScrub = useScrub((doc, raw) => source(doc, "outFrames", Number(raw)));
|
|
339
|
+
|
|
340
|
+
return (
|
|
341
|
+
<>
|
|
342
|
+
{/* A plain pair: the source edges in the recording's own frames. */}
|
|
343
|
+
<PropertyGrid>
|
|
344
|
+
<InspectorRow>
|
|
345
|
+
<NumberField
|
|
346
|
+
value={clip.sourceInFrames}
|
|
347
|
+
label="Src in"
|
|
348
|
+
ariaLabel="Source in frame"
|
|
349
|
+
unit="f"
|
|
350
|
+
step={1}
|
|
351
|
+
// NO `min`. A negative value is a real state — the window opens
|
|
352
|
+
// before the recording does — and `NumberField` clamps on every
|
|
353
|
+
// commit path, so a floor here would erase it on the first touch.
|
|
354
|
+
// The WRITE clamps instead, in `setVideoSource`, which is the
|
|
355
|
+
// ceiling this file's header states.
|
|
356
|
+
title="Which source frame plays under the window's in. Moves the footage, never the window"
|
|
357
|
+
onCommit={(next) => write((base) => source(base, "inFrames", next))}
|
|
358
|
+
onScrubStart={inScrub.start}
|
|
359
|
+
onScrub={inScrub.move}
|
|
360
|
+
onScrubEnd={inScrub.end}
|
|
361
|
+
/>
|
|
362
|
+
</InspectorRow>
|
|
363
|
+
<InspectorRow>
|
|
364
|
+
<NumberField
|
|
365
|
+
value={clip.sourceOutFrames}
|
|
366
|
+
label="Src out"
|
|
367
|
+
ariaLabel="Source out frame"
|
|
368
|
+
unit="f"
|
|
369
|
+
step={1}
|
|
370
|
+
title="Which source frame plays under the window's out. Moves the out, never the in"
|
|
371
|
+
onCommit={(next) => write((base) => source(base, "outFrames", next))}
|
|
372
|
+
onScrubStart={outScrub.start}
|
|
373
|
+
onScrub={outScrub.move}
|
|
374
|
+
onScrubEnd={outScrub.end}
|
|
375
|
+
/>
|
|
376
|
+
</InspectorRow>
|
|
377
|
+
</PropertyGrid>
|
|
378
|
+
|
|
379
|
+
<SpeedRow
|
|
380
|
+
doc={doc}
|
|
381
|
+
sceneId={sceneId}
|
|
382
|
+
layer={layer}
|
|
383
|
+
rate={rate}
|
|
384
|
+
onWrite={(next) => write(next)}
|
|
385
|
+
onRefusal={(message) => store.reportError(message)}
|
|
386
|
+
/>
|
|
387
|
+
|
|
388
|
+
{/* Past the recording's END. `hold` is the default — a screen capture
|
|
389
|
+
under a longer shot ends on its last frame rather than restarting
|
|
390
|
+
mid-shot — and every document written before schema v22 carries an
|
|
391
|
+
explicit `loop` put there by the migration, so an old film keeps the
|
|
392
|
+
wrap it was authored with. */}
|
|
393
|
+
<InspectorRow label="Past end">
|
|
394
|
+
<SegmentedControl
|
|
395
|
+
ariaLabel="Video loop"
|
|
396
|
+
value={layer.video.loop ?? "hold"}
|
|
397
|
+
options={LOOP_OPTIONS}
|
|
398
|
+
onChange={(next) =>
|
|
399
|
+
write((base) => patchVideo(base, sceneId, layer.id, (v) => ({ ...v, loop: next as MediaLoop })))
|
|
400
|
+
}
|
|
401
|
+
/>
|
|
402
|
+
</InspectorRow>
|
|
403
|
+
|
|
404
|
+
{/* THE NATURAL LENGTH. Through `writeLifespan` like every other edge, so
|
|
405
|
+
the footage clamp and the scene clamp are `trimLayer`'s and not a
|
|
406
|
+
second pair here. DISABLED, never hidden, when nobody has probed the
|
|
407
|
+
asset: the affordance for a capability stays visible at rest (the
|
|
408
|
+
design system's rule), and the tooltip says what is missing. */}
|
|
409
|
+
<InspectorRow label="Length">
|
|
410
|
+
<button
|
|
411
|
+
type="button"
|
|
412
|
+
className="btn"
|
|
413
|
+
data-testid="time-natural-length"
|
|
414
|
+
disabled={locked || natural === null}
|
|
415
|
+
title={
|
|
416
|
+
natural === null
|
|
417
|
+
? "This recording has not been probed, so its length is unknown"
|
|
418
|
+
: `Set the out so the whole recording plays once, at frame ${natural}`
|
|
419
|
+
}
|
|
420
|
+
onClick={() =>
|
|
421
|
+
write((base) => {
|
|
422
|
+
if (natural === null) return base;
|
|
423
|
+
const now = lifespanOf(base.scenesById[sceneId]?.layersById[layer.id] ?? layer, scene.durationFrames);
|
|
424
|
+
return natural === now.out ? base : writeLifespan(base, sceneId, layer.id, "out", natural - now.out);
|
|
425
|
+
})
|
|
426
|
+
}
|
|
427
|
+
>
|
|
428
|
+
Natural length
|
|
429
|
+
</button>
|
|
430
|
+
</InspectorRow>
|
|
431
|
+
|
|
432
|
+
<InspectorRow>
|
|
433
|
+
<span className="insp-notice" data-testid="time-closing-edge">
|
|
434
|
+
{closingCaption(close, doc.fps, rate)}
|
|
435
|
+
</span>
|
|
436
|
+
</InspectorRow>
|
|
437
|
+
</>
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* The rate, with KEEP-WINDOW semantics and its refusal on screen.
|
|
443
|
+
*
|
|
444
|
+
* `retimeVideo` pins the source frame under the window's `in` across the
|
|
445
|
+
* change, so the bar does not move and the footage does not scrub sideways
|
|
446
|
+
* under a cut that is already timed. It REFUSES a rate away from 1 on a layer
|
|
447
|
+
* whose inline sound is in the mix, because neither mixer can resample — and a
|
|
448
|
+
* refusal that only returned the document unchanged would read as a field that
|
|
449
|
+
* snaps back for no reason.
|
|
450
|
+
*
|
|
451
|
+
* So the typed value is held in a DRAFT while the refusal is up. Without it the
|
|
452
|
+
* field re-renders from the document, shows the old rate, and the author's only
|
|
453
|
+
* evidence that anything happened is one line in the error strip.
|
|
454
|
+
*
|
|
455
|
+
* ── AND THE DRAFT IS RE-ASKED EVERY RENDER, NOT LATCHED ─────────────────
|
|
456
|
+
*
|
|
457
|
+
* The refusal names its own cure ("mute or remove the sound first"), so the
|
|
458
|
+
* document that cures it arrives while the draft is still on screen. A latched
|
|
459
|
+
* draft would then keep showing 2 over a layer that is now free to BE 2 — and
|
|
460
|
+
* because the field's value already reads 2, retyping it commits nothing at
|
|
461
|
+
* all: the author performs the cure the message asked for and the control is
|
|
462
|
+
* dead. So the stored draft is only honoured while the refusal still applies,
|
|
463
|
+
* and the field falls back to the document's own rate the moment it lifts.
|
|
464
|
+
*/
|
|
465
|
+
function SpeedRow(props: {
|
|
466
|
+
doc: Doc3D;
|
|
467
|
+
sceneId: string;
|
|
468
|
+
layer: VideoLayer3D;
|
|
469
|
+
rate: number;
|
|
470
|
+
onWrite: (write: (doc: Doc3D) => Doc3D) => void;
|
|
471
|
+
onRefusal: (message: string | null) => void;
|
|
472
|
+
}) {
|
|
473
|
+
const { doc, sceneId, layer, rate } = props;
|
|
474
|
+
const [refused, setRefused] = useState<{ value: number; why: string } | null>(null);
|
|
475
|
+
// THE DRAG HALF, through the same op and the same refusal as a typed rate:
|
|
476
|
+
// a capture the sound forbids lands nothing new, and the release holds the
|
|
477
|
+
// refused rate on screen exactly as a typed one is held. A drag that lands
|
|
478
|
+
// lets go of a held one, as a typed rate that lands does: otherwise the field
|
|
479
|
+
// kept showing the refused 2 and its notice over a document now at 1.
|
|
480
|
+
const scrub = useOpScrub(
|
|
481
|
+
(base, raw) => {
|
|
482
|
+
const to = speedOf(raw);
|
|
483
|
+
const why = retimeVideoRefusal(base, sceneId, layer.id, to);
|
|
484
|
+
return why !== null ? { doc: base, refusal: why } : { doc: retimeVideo(base, sceneId, layer.id, to), refusal: null };
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
refused: (why, raw) => setRefused({ value: speedOf(raw), why }),
|
|
488
|
+
landed: () => setRefused(null),
|
|
489
|
+
},
|
|
490
|
+
);
|
|
491
|
+
// Re-asked, never latched — see the header. `retimeVideoRefusal` is a pure
|
|
492
|
+
// read over the live document, so the draft survives exactly as long as the
|
|
493
|
+
// reason for it does.
|
|
494
|
+
const standing =
|
|
495
|
+
refused !== null && retimeVideoRefusal(doc, sceneId, layer.id, refused.value) !== null ? refused : null;
|
|
496
|
+
return (
|
|
497
|
+
<>
|
|
498
|
+
<InspectorRow>
|
|
499
|
+
<NumberField
|
|
500
|
+
value={standing?.value ?? rate}
|
|
501
|
+
label="Speed"
|
|
502
|
+
ariaLabel="Video speed"
|
|
503
|
+
unit="x"
|
|
504
|
+
step={0.1}
|
|
505
|
+
// NO `min`, for the reason the source-in row has none: a negative
|
|
506
|
+
// rate is a legal authored value (backwards playback from a late
|
|
507
|
+
// start) and `NumberField` clamps on every commit path.
|
|
508
|
+
title="Playback rate, keeping the window and the frame it opens on. 2 runs the recording twice as fast"
|
|
509
|
+
onCommit={(next) => {
|
|
510
|
+
const to = speedOf(next);
|
|
511
|
+
const why = retimeVideoRefusal(doc, sceneId, layer.id, to);
|
|
512
|
+
if (why !== null) {
|
|
513
|
+
setRefused({ value: to, why });
|
|
514
|
+
props.onRefusal(why);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
setRefused(null);
|
|
518
|
+
props.onWrite((base) => retimeVideo(base, sceneId, layer.id, to));
|
|
519
|
+
}}
|
|
520
|
+
onScrubStart={scrub.start}
|
|
521
|
+
onScrub={scrub.move}
|
|
522
|
+
onScrubEnd={scrub.end}
|
|
523
|
+
/>
|
|
524
|
+
</InspectorRow>
|
|
525
|
+
{standing === null ? null : (
|
|
526
|
+
<InspectorRow>
|
|
527
|
+
<span className="insp-notice is-warn" data-testid="time-speed-refusal">
|
|
528
|
+
{standing.why}
|
|
529
|
+
</span>
|
|
530
|
+
</InspectorRow>
|
|
531
|
+
)}
|
|
532
|
+
</>
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** A rate as the document may hold it: three decimals, and 0 (or anything
|
|
537
|
+
* not a number) snapped to 1. Zero is REFUSED at the save gate rather than
|
|
538
|
+
* written, so it is snapped here instead of being handed to the validator as
|
|
539
|
+
* an error the author cannot see the cause of. */
|
|
540
|
+
function speedOf(next: number): number {
|
|
541
|
+
const rounded = Number(next.toFixed(3));
|
|
542
|
+
return rounded === 0 || !Number.isFinite(rounded) ? 1 : rounded;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* THE RECORDING'S OWN SOUND, on the layer that draws its picture.
|
|
547
|
+
*
|
|
548
|
+
* Only for a video that HAS one: `video.sound` absent means silent, which is
|
|
549
|
+
* every video layer written before the field existed, and a level field over
|
|
550
|
+
* nothing is three controls that write into a record that is not there.
|
|
551
|
+
* Attaching one is `add_footage`'s and the dock's job, not a level row's.
|
|
552
|
+
*
|
|
553
|
+
* ── THE GAIN AND THE REST TAKE DIFFERENT DOORS, ON PURPOSE ──────────────
|
|
554
|
+
*
|
|
555
|
+
* GAIN goes through `useSelectionWrite`, because `video.sound.gain` is a
|
|
556
|
+
* keyframable path: the write has to become a KEYFRAME at the playhead when the
|
|
557
|
+
* layer animates it, and that is precisely what `routeLayerEdit` does inside
|
|
558
|
+
* that hook. `setVideoSound` would write the base instead, which a track then
|
|
559
|
+
* ignores.
|
|
560
|
+
*
|
|
561
|
+
* MUTE AND THE FADES go through `setVideoSound`, because they are not tracked
|
|
562
|
+
* and they ARE gated: a fade pair longer than the frames the recording is
|
|
563
|
+
* audible over, a gain past the mixer's ceiling, a duck naming a clip that is
|
|
564
|
+
* gone — six rules that live in `validateShotDoc` and that the predicate asks
|
|
565
|
+
* rather than restates. Writing those fields raw would produce a document the
|
|
566
|
+
* save gate refuses, with the author holding the only copy.
|
|
567
|
+
*
|
|
568
|
+
* ── LOCK GOVERNS THIS SECTION LIKE EVERY OTHER ─────────────────────────
|
|
569
|
+
*
|
|
570
|
+
* `useDocWrite` and `useSelectionWrite` both run `lockRefuses`, and so does the
|
|
571
|
+
* dock's bar drag, so a locked clip refuses In/Out, the source rows, the sound
|
|
572
|
+
* rows and Gain alike, and names the layer.
|
|
573
|
+
*
|
|
574
|
+
* ── AND THE PATCH IS BUILT FROM THE BASE RECORD, NEVER THE RESOLVED ONE ──
|
|
575
|
+
*
|
|
576
|
+
* `setVideoSound` takes a WHOLE `VideoSound` and writes it verbatim, so the
|
|
577
|
+
* record handed to it is the record the document ends up with — every field,
|
|
578
|
+
* not only the one the author touched. `props.sound` is the layer AS PAINTED
|
|
579
|
+
* (`useSelectedLayers` runs `resolveTracksAt`), so when `video.sound.gain` is
|
|
580
|
+
* KEYFRAMED that record's `gain` is a SAMPLE OF AN ANIMATION at the playhead.
|
|
581
|
+
* Spreading it meant ticking Mute or nudging a fade froze the whole automation
|
|
582
|
+
* to whatever the needle happened to read on that frame, silently and with the
|
|
583
|
+
* author's only copy of the curve gone the moment the save landed.
|
|
584
|
+
*
|
|
585
|
+
* This is exactly the asymmetry `useDocWrite`'s header exists to state: READ
|
|
586
|
+
* resolved, WRITE the base. So `baseSoundOf` re-reads the authored record out
|
|
587
|
+
* of the document being written into, `next` patches the one field the author
|
|
588
|
+
* touched, and the gain the track owns passes through untouched — which is
|
|
589
|
+
* also what keeps `routeTrackedEdit`'s "the base is dead once a track exists"
|
|
590
|
+
* true rather than something this section quietly overwrites.
|
|
591
|
+
*/
|
|
592
|
+
function SoundRows(props: {
|
|
593
|
+
doc: Doc3D;
|
|
594
|
+
sceneId: string;
|
|
595
|
+
layer: VideoLayer3D;
|
|
596
|
+
sound: VideoSound;
|
|
597
|
+
onRefusal: (message: string | null) => void;
|
|
598
|
+
}) {
|
|
599
|
+
const { doc, sceneId, layer, sound } = props;
|
|
600
|
+
const write = useDocWrite();
|
|
601
|
+
const selectionWrite = useSelectionWrite();
|
|
602
|
+
// THE DRAG HALVES. The gain through the selection seam its typed value takes
|
|
603
|
+
// (so a drag over an animated gain keys it), the fades through the same gate
|
|
604
|
+
// their typed values ask, refusing a capture the gate refuses.
|
|
605
|
+
const gainScrub = useSelectionScrub((subject, raw) => withGain(subject, Number(raw)));
|
|
606
|
+
const fadeScrub = (key: "fadeInFrames" | "fadeOutFrames") => (base: Doc3D, value: number) => {
|
|
607
|
+
const from = baseSoundOf(base, sceneId, layer.id);
|
|
608
|
+
if (from === undefined) return { doc: base, refusal: null };
|
|
609
|
+
const next = withFade(from, key, value);
|
|
610
|
+
const why = setVideoSoundRefusal(base, sceneId, layer.id, next);
|
|
611
|
+
return why !== null ? { doc: base, refusal: why } : { doc: setVideoSound(base, sceneId, layer.id, next), refusal: null };
|
|
612
|
+
};
|
|
613
|
+
const fadeInScrub = useOpScrub(fadeScrub("fadeInFrames"));
|
|
614
|
+
const fadeOutScrub = useOpScrub(fadeScrub("fadeOutFrames"));
|
|
615
|
+
|
|
616
|
+
/** One door for the three gated fields, with the refusal reported rather
|
|
617
|
+
* than swallowed: a control that silently did nothing is the failure this
|
|
618
|
+
* whole lane keeps removing.
|
|
619
|
+
*
|
|
620
|
+
* `next` is applied TWICE and to two different records on purpose. The
|
|
621
|
+
* predicate has to judge the document the author is looking at, so it gets
|
|
622
|
+
* the base read out of `doc`; the write re-reads and re-patches inside
|
|
623
|
+
* `useDocWrite`'s own document, which is this file's rule for every
|
|
624
|
+
* untracked field (see the header) and is what makes a commit that lands
|
|
625
|
+
* after an undo or another panel's edit measure against the right subject.
|
|
626
|
+
* `next` is pure — `withFade` and `without` build a new object and read
|
|
627
|
+
* nothing else — so applying it twice cannot compound. */
|
|
628
|
+
const patchSound = (next: (from: VideoSound) => VideoSound): void => {
|
|
629
|
+
const base = baseSoundOf(doc, sceneId, layer.id);
|
|
630
|
+
if (base === undefined) return;
|
|
631
|
+
const why = setVideoSoundRefusal(doc, sceneId, layer.id, next(base));
|
|
632
|
+
if (why !== null) {
|
|
633
|
+
props.onRefusal(why);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
write((into) => {
|
|
637
|
+
const from = baseSoundOf(into, sceneId, layer.id);
|
|
638
|
+
return from === undefined ? into : setVideoSound(into, sceneId, layer.id, next(from));
|
|
639
|
+
});
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
return (
|
|
643
|
+
<>
|
|
644
|
+
{/* A HEADING and not a nested `InspectorSection`: the sound is part of
|
|
645
|
+
this clip's timing, and a second collapsible inside one would give
|
|
646
|
+
the author two disclosure states for one subject. */}
|
|
647
|
+
<div className="insp-heading">Recording sound</div>
|
|
648
|
+
|
|
649
|
+
{/* dB, because that is the unit a level decision is made in, over a
|
|
650
|
+
document that stores the LINEAR multiplier the engine applies. The
|
|
651
|
+
conversion is `timeModel.ts`'s and its round trip is pinned there.
|
|
652
|
+
Both ends are the mixer's: silence (`MIN_GAIN_DB`, where the gain
|
|
653
|
+
reads back as a true 0) and its ceiling, so a drag stops where the
|
|
654
|
+
commit would. */}
|
|
655
|
+
<InspectorRow>
|
|
656
|
+
<NumberField
|
|
657
|
+
value={gainAsDb(sound.gain ?? 1)}
|
|
658
|
+
label="Gain"
|
|
659
|
+
ariaLabel="Recording gain"
|
|
660
|
+
unit="dB"
|
|
661
|
+
step={0.1}
|
|
662
|
+
min={MIN_GAIN_DB}
|
|
663
|
+
max={MAX_GAIN_DB}
|
|
664
|
+
title="Level of the recording's own sound. 0 dB is the file as recorded"
|
|
665
|
+
trailing={<KeyframeDot path="video.sound.gain" label="Gain" />}
|
|
666
|
+
onCommit={(next) => selectionWrite((subject) => withGain(subject, next))}
|
|
667
|
+
onScrubStart={gainScrub.start}
|
|
668
|
+
onScrub={gainScrub.move}
|
|
669
|
+
onScrubEnd={gainScrub.end}
|
|
670
|
+
/>
|
|
671
|
+
</InspectorRow>
|
|
672
|
+
|
|
673
|
+
<InspectorRow label="Mute">
|
|
674
|
+
<Toggle
|
|
675
|
+
checked={sound.muted === true}
|
|
676
|
+
ariaLabel="Mute the recording's sound"
|
|
677
|
+
onChange={(checked) =>
|
|
678
|
+
patchSound((from) => (checked ? { ...from, muted: true } : without(from, "muted")))
|
|
679
|
+
}
|
|
680
|
+
/>
|
|
681
|
+
</InspectorRow>
|
|
682
|
+
|
|
683
|
+
{/* A plain pair, the two fades. */}
|
|
684
|
+
<PropertyGrid>
|
|
685
|
+
<InspectorRow>
|
|
686
|
+
<NumberField
|
|
687
|
+
value={sound.fadeInFrames ?? 0}
|
|
688
|
+
label="Fade in"
|
|
689
|
+
ariaLabel="Recording fade in"
|
|
690
|
+
unit="f"
|
|
691
|
+
min={0}
|
|
692
|
+
step={1}
|
|
693
|
+
onCommit={(next) => patchSound((from) => withFade(from, "fadeInFrames", next))}
|
|
694
|
+
onScrubStart={fadeInScrub.start}
|
|
695
|
+
onScrub={fadeInScrub.move}
|
|
696
|
+
onScrubEnd={fadeInScrub.end}
|
|
697
|
+
/>
|
|
698
|
+
</InspectorRow>
|
|
699
|
+
<InspectorRow>
|
|
700
|
+
<NumberField
|
|
701
|
+
value={sound.fadeOutFrames ?? 0}
|
|
702
|
+
label="Fade out"
|
|
703
|
+
ariaLabel="Recording fade out"
|
|
704
|
+
unit="f"
|
|
705
|
+
min={0}
|
|
706
|
+
step={1}
|
|
707
|
+
onCommit={(next) => patchSound((from) => withFade(from, "fadeOutFrames", next))}
|
|
708
|
+
onScrubStart={fadeOutScrub.start}
|
|
709
|
+
onScrub={fadeOutScrub.move}
|
|
710
|
+
onScrubEnd={fadeOutScrub.end}
|
|
711
|
+
/>
|
|
712
|
+
</InspectorRow>
|
|
713
|
+
</PropertyGrid>
|
|
714
|
+
</>
|
|
715
|
+
);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* A placement's loop (schema v47): what a component's clock does past its
|
|
720
|
+
* period. A code instance names its period (`loopFrames`), because a code
|
|
721
|
+
* component has no length of its own; a Studio-made component's period is its
|
|
722
|
+
* definition's length, shown here and edited on the definition. Both go
|
|
723
|
+
* through `useSelectionWrite`, which writes into whichever container holds the
|
|
724
|
+
* layer. Off removes the fields, the absence every older placement has.
|
|
725
|
+
*/
|
|
726
|
+
function PlacementLoopRows(props: { doc: Doc3D; layer: InstanceLayer3D | ComponentLayer3D }) {
|
|
727
|
+
const { doc, layer } = props;
|
|
728
|
+
const write = useSelectionWrite();
|
|
729
|
+
const loop = layer.kind === "instance" ? layer.instance.loop : layer.component.loop;
|
|
730
|
+
// A loop needs a period the moment it is switched on: one second of frames.
|
|
731
|
+
const second = Math.max(1, Math.round(doc.fps.num / doc.fps.den));
|
|
732
|
+
const def = layer.kind === "component" ? doc.components?.[layer.component.def] : undefined;
|
|
733
|
+
return (
|
|
734
|
+
<>
|
|
735
|
+
<InspectorRow label="Loop">
|
|
736
|
+
<SegmentedControl
|
|
737
|
+
ariaLabel="Placement loop"
|
|
738
|
+
value={loop ?? PLACEMENT_LOOP_OFF}
|
|
739
|
+
options={PLACEMENT_LOOP_OPTIONS}
|
|
740
|
+
onChange={(next) => write((base) => withPlacementLoop(base, next, second))}
|
|
741
|
+
/>
|
|
742
|
+
</InspectorRow>
|
|
743
|
+
{layer.kind === "instance" && loop !== undefined ? (
|
|
744
|
+
<InspectorRow>
|
|
745
|
+
<NumberField
|
|
746
|
+
value={layer.instance.loopFrames ?? second}
|
|
747
|
+
label="Every"
|
|
748
|
+
ariaLabel="Loop period"
|
|
749
|
+
title="The loop's period, in frames"
|
|
750
|
+
unit="f"
|
|
751
|
+
min={1}
|
|
752
|
+
step={1}
|
|
753
|
+
onCommit={(next) =>
|
|
754
|
+
write((base) =>
|
|
755
|
+
base.kind === "instance" && base.instance.loop !== undefined
|
|
756
|
+
? { ...base, instance: { ...base.instance, loopFrames: Math.max(1, Math.round(next)) } }
|
|
757
|
+
: base,
|
|
758
|
+
)
|
|
759
|
+
}
|
|
760
|
+
/>
|
|
761
|
+
</InspectorRow>
|
|
762
|
+
) : null}
|
|
763
|
+
{def !== undefined && loop !== undefined ? (
|
|
764
|
+
<InspectorRow label="Every" className="insp-readout">
|
|
765
|
+
<span className="insp-dim" data-testid="time-loop-period" title="The definition's length">
|
|
766
|
+
{`${defDurationFrames(def)}f · ${formatDuration(framesToSeconds(defDurationFrames(def), doc.fps))}`}
|
|
767
|
+
</span>
|
|
768
|
+
</InspectorRow>
|
|
769
|
+
) : null}
|
|
770
|
+
</>
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/** One placement's loop set to `next` ("off", "hold" or "loop"). Off deletes
|
|
775
|
+
* the keys rather than writing undefined, the `exactOptionalPropertyTypes`
|
|
776
|
+
* rule; an instance switched on keeps its period or takes `period`. */
|
|
777
|
+
function withPlacementLoop(layer: Layer3D, next: string, period: number): Layer3D {
|
|
778
|
+
const on = next === PLACEMENT_LOOP_OFF ? null : (next as MediaLoop);
|
|
779
|
+
if (layer.kind === "instance") {
|
|
780
|
+
const { loop: _loop, loopFrames, ...rest } = layer.instance;
|
|
781
|
+
return { ...layer, instance: on === null ? rest : { ...rest, loop: on, loopFrames: loopFrames ?? period } };
|
|
782
|
+
}
|
|
783
|
+
if (layer.kind === "component") {
|
|
784
|
+
const { loop: _loop, ...rest } = layer.component;
|
|
785
|
+
return { ...layer, component: on === null ? rest : { ...rest, loop: on } };
|
|
786
|
+
}
|
|
787
|
+
return layer;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* The recording's sound AS AUTHORED, out of the document it is read from.
|
|
792
|
+
*
|
|
793
|
+
* The one record `setVideoSound` may legally be handed. See `SoundRows`'s
|
|
794
|
+
* header: a resolved layer's `video.sound` carries a SAMPLED `gain` whenever
|
|
795
|
+
* the path is keyframed, and writing that back is how an animation gets
|
|
796
|
+
* flattened by a mute toggle.
|
|
797
|
+
*
|
|
798
|
+
* SCENE-ADDRESSED and not `homeOf`, matching the clip ops above: every
|
|
799
|
+
* `setVideoSound` call resolves its layer through `doc.scenesById`, so a
|
|
800
|
+
* container id that is a definition has to answer undefined here rather than
|
|
801
|
+
* hand back a record the op would then refuse to write.
|
|
802
|
+
*/
|
|
803
|
+
function baseSoundOf(doc: Doc3D, sceneId: string, layerId: string): VideoSound | undefined {
|
|
804
|
+
const layer: Layer3D | undefined = doc.scenesById[sceneId]?.layersById[layerId];
|
|
805
|
+
return layer !== undefined && layer.kind === "video" ? layer.video.sound : undefined;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/** The recording's gain set from dB on one layer, the one builder its typed
|
|
809
|
+
* value and its drag both run. Another layer, or a video with no sound, is
|
|
810
|
+
* left alone. */
|
|
811
|
+
function withGain(subject: Layer3D, db: number): Layer3D {
|
|
812
|
+
return subject.kind === "video" && subject.video.sound !== undefined
|
|
813
|
+
? {
|
|
814
|
+
...subject,
|
|
815
|
+
video: { ...subject.video, sound: { ...subject.video.sound, gain: roundGain(dbToGain(db)) } },
|
|
816
|
+
}
|
|
817
|
+
: subject;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/** Four decimals, `audioLoudness.ts`'s own rule for a stored gain: enough to be
|
|
821
|
+
* exact at any audible level, few enough that the number written into the
|
|
822
|
+
* document is one a human can read back. Clamped to the mixer's range, so the
|
|
823
|
+
* field's dB ceiling and the document's linear one are one limit. */
|
|
824
|
+
function roundGain(gain: number): number {
|
|
825
|
+
const clamped = Math.max(0, Math.min(MAX_AUDIO_GAIN, Number.isFinite(gain) ? gain : 1));
|
|
826
|
+
return Math.round(clamped * 1e4) / 1e4;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/** A fade of 0 DELETES the key: absent already means no fade, and an explicit
|
|
830
|
+
* 0 is a second way to say the same thing that codegen then prints. */
|
|
831
|
+
function withFade(sound: VideoSound, key: "fadeInFrames" | "fadeOutFrames", value: number): VideoSound {
|
|
832
|
+
const frames = Math.max(0, Math.round(Number.isFinite(value) ? value : 0));
|
|
833
|
+
return frames === 0 ? without(sound, key) : { ...sound, [key]: frames };
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/** Drop one optional key rather than setting it to `undefined`. Under
|
|
837
|
+
* `exactOptionalPropertyTypes` those are different values, and codegen prints
|
|
838
|
+
* the record by key order — a present-but-undefined key is a key. */
|
|
839
|
+
function without(sound: VideoSound, key: "muted" | "fadeInFrames" | "fadeOutFrames"): VideoSound {
|
|
840
|
+
const { [key]: _drop, ...rest } = sound;
|
|
841
|
+
void _drop;
|
|
842
|
+
return rest;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/** A plain video-block patch, for the ONE field here that is neither a clip op
|
|
846
|
+
* nor a gated sound field. `loop` has no arithmetic and no gate: it picks
|
|
847
|
+
* which of two branches `sourceTimeAt` takes past the end. */
|
|
848
|
+
function patchVideo(
|
|
849
|
+
doc: Doc3D,
|
|
850
|
+
sceneId: string,
|
|
851
|
+
layerId: string,
|
|
852
|
+
next: (video: VideoLayer3D["video"]) => VideoLayer3D["video"],
|
|
853
|
+
): Doc3D {
|
|
854
|
+
const scene = doc.scenesById[sceneId];
|
|
855
|
+
const layer: Layer3D | undefined = scene?.layersById[layerId];
|
|
856
|
+
if (scene === undefined || layer === undefined || layer.kind !== "video") return doc;
|
|
857
|
+
const video = next(layer.video);
|
|
858
|
+
if (video === layer.video) return doc;
|
|
859
|
+
return {
|
|
860
|
+
...doc,
|
|
861
|
+
scenesById: {
|
|
862
|
+
...doc.scenesById,
|
|
863
|
+
[sceneId]: { ...scene, layersById: { ...scene.layersById, [layerId]: { ...layer, video } } },
|
|
864
|
+
},
|
|
865
|
+
};
|
|
866
|
+
}
|