@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,272 @@
|
|
|
1
|
+
// The workspace layout — three panel sizes and three collapse flags —
|
|
2
|
+
// remembered across reloads.
|
|
3
|
+
//
|
|
4
|
+
// `workspacePrefs` in the store already carries these six across a DOCUMENT
|
|
5
|
+
// switch, which is the right instinct applied to the wrong boundary: the
|
|
6
|
+
// layout is a fact about the person and their screen, not about the film they
|
|
7
|
+
// happen to have open. An author who dragged the dock tall to read a curve had
|
|
8
|
+
// it back at 300 the moment they refreshed the tab, which is why the reach for
|
|
9
|
+
// the handle became a habit rather than a decision.
|
|
10
|
+
//
|
|
11
|
+
// ONE KEY HOLDING ONE OBJECT, not six keys. The six move together — every one
|
|
12
|
+
// of them is answering "how much of this window is chrome" — so there is no
|
|
13
|
+
// case where half of them should be restored and the other half not, and a
|
|
14
|
+
// single `setItem` is also the whole of the write cost this module adds.
|
|
15
|
+
//
|
|
16
|
+
// Guarded on both sides, like `editorPrefs`: private mode and a locked-down
|
|
17
|
+
// storage policy each throw on access, and a preference is never worth an
|
|
18
|
+
// exception. A read that fails answers `{}`, which is "no opinion" — `bootLayout`
|
|
19
|
+
// then fits the window, which is the state the chrome had before this existed.
|
|
20
|
+
//
|
|
21
|
+
// ── A SIZE THIS WINDOW CANNOT HONOUR IS SUSPENDED, NOT OVERWRITTEN ────────
|
|
22
|
+
//
|
|
23
|
+
// `bootLayout` puts a saved size through the SETTERS so the bounds this session
|
|
24
|
+
// enforces win over whatever storage remembers: a 600px dock dragged on a
|
|
25
|
+
// desktop arrives on a 700px laptop as 490, and that is right — a dock with no
|
|
26
|
+
// stage behind it is worse than a short dock. But the store was then the only
|
|
27
|
+
// copy of that number, so the next thing the author moved wrote 490 back as if
|
|
28
|
+
// they had chosen it, and reconnecting the monitor did not bring the 600 back.
|
|
29
|
+
// A size suspended for one session had become a size deleted.
|
|
30
|
+
//
|
|
31
|
+
// So the RECORD and the LIVE VALUE are allowed to disagree. A size this boot
|
|
32
|
+
// could not apply as written is held aside and re-written verbatim on every
|
|
33
|
+
// save, until the author drags that panel — at which point the number on screen
|
|
34
|
+
// is theirs, about this window, and the held one is dropped for good. Nothing
|
|
35
|
+
// else moves: the session still runs at the clamped size, `authorLayout` still
|
|
36
|
+
// keeps the window's own fitted numbers out of the record, and focus mode's
|
|
37
|
+
// collapse is still not a preference.
|
|
38
|
+
//
|
|
39
|
+
// That buys the module its one ORDERING rule. `persistLayoutPrefs` reads the
|
|
40
|
+
// record itself and compares it against the store, so it has to be called AFTER
|
|
41
|
+
// `bootLayout` (main.tsx does, and says why). Subscribe first and the boot's own
|
|
42
|
+
// application of the record is indistinguishable from a drag — see the test.
|
|
43
|
+
import { layoutFor } from "./panelSize.js";
|
|
44
|
+
import type { BootLayoutPrefs } from "./panelSize.js";
|
|
45
|
+
import type { ShotEditorStore } from "./store.js";
|
|
46
|
+
|
|
47
|
+
/** The one key, exported so the suite asserts against THIS string rather than
|
|
48
|
+
* a second copy of it. A test that retypes the name still passes when the
|
|
49
|
+
* name here changes, and a renamed key silently forgets every saved layout. */
|
|
50
|
+
export const WORKSPACE_PREFS_KEY = "dailies.workspace";
|
|
51
|
+
|
|
52
|
+
const SIZE_FIELDS = ["railWidth", "inspectorWidth", "dockHeight"] as const;
|
|
53
|
+
const COLLAPSE_FIELDS = [
|
|
54
|
+
"railCollapsed",
|
|
55
|
+
"inspectorCollapsed",
|
|
56
|
+
"dockCollapsed",
|
|
57
|
+
] as const;
|
|
58
|
+
|
|
59
|
+
type SizeField = (typeof SIZE_FIELDS)[number];
|
|
60
|
+
|
|
61
|
+
/** A size the author chose (`chosen`) and the one this boot left in its place
|
|
62
|
+
* (`boot`); see `suspendedSizes`. */
|
|
63
|
+
interface SuspendedSize {
|
|
64
|
+
chosen: number;
|
|
65
|
+
boot: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The saved layout, validated FIELD BY FIELD rather than object by object.
|
|
70
|
+
*
|
|
71
|
+
* A whole-object check would make one bad number throw away a good one, and
|
|
72
|
+
* the object is exactly the shape most likely to go half-bad: it is written by
|
|
73
|
+
* an older build, hand-edited in devtools, or truncated by a quota error
|
|
74
|
+
* mid-write. `bootLayout` already treats every field as independently absent,
|
|
75
|
+
* so an unusable `dockHeight` is best expressed as a missing one — the author
|
|
76
|
+
* keeps the rail they dragged and gets a fitted dock.
|
|
77
|
+
*
|
|
78
|
+
* Numbers must be FINITE: `JSON.parse` cannot produce a NaN, but it happily
|
|
79
|
+
* produces `1e400` as `Infinity`, and an infinite width reaches `clampSize`,
|
|
80
|
+
* which answers `min` — silently the wrong panel rather than no opinion.
|
|
81
|
+
*/
|
|
82
|
+
export function readLayoutPrefs(): BootLayoutPrefs {
|
|
83
|
+
let parsed: unknown;
|
|
84
|
+
try {
|
|
85
|
+
const text = localStorage.getItem(WORKSPACE_PREFS_KEY);
|
|
86
|
+
if (text === null) return {};
|
|
87
|
+
parsed = JSON.parse(text);
|
|
88
|
+
} catch {
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
if (typeof parsed !== "object" || parsed === null) return {};
|
|
92
|
+
const saved = parsed as Record<string, unknown>;
|
|
93
|
+
const prefs: BootLayoutPrefs = {};
|
|
94
|
+
for (const field of SIZE_FIELDS) {
|
|
95
|
+
const value = saved[field];
|
|
96
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
97
|
+
prefs[field] = value;
|
|
98
|
+
}
|
|
99
|
+
for (const field of COLLAPSE_FIELDS) {
|
|
100
|
+
const value = saved[field];
|
|
101
|
+
if (typeof value === "boolean") prefs[field] = value;
|
|
102
|
+
}
|
|
103
|
+
return prefs;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function writeLayoutPrefs(prefs: BootLayoutPrefs): void {
|
|
107
|
+
try {
|
|
108
|
+
localStorage.setItem(WORKSPACE_PREFS_KEY, JSON.stringify(prefs));
|
|
109
|
+
} catch {
|
|
110
|
+
/* storage disabled or full — the session still honours the drag, it just forgets */
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The layout as the AUTHOR has it, which is not the layout the store has.
|
|
116
|
+
*
|
|
117
|
+
* Two things in the state look like a preference and are not, and saving
|
|
118
|
+
* either hands the next session a decision nobody made.
|
|
119
|
+
*
|
|
120
|
+
* A FITTED SIZE IS THE WINDOW'S OPINION. Seeding the signature after
|
|
121
|
+
* `bootLayout` only defers that: `App`'s resize refit puts the NEW fitted
|
|
122
|
+
* numbers through the very setters a drag uses, so docking a 1366 laptop to a
|
|
123
|
+
* 2560 monitor would save 232/288/300, and the next boot on the laptop would
|
|
124
|
+
* prefer that record over the fit — permanently, because 232 is no longer the
|
|
125
|
+
* fitted number and refit reads it as a panel the author dragged. The test
|
|
126
|
+
* here is the one refit already uses: a panel holding exactly `layoutFor`'s
|
|
127
|
+
* number is a panel nobody has dragged. A drag that lands on the fitted value
|
|
128
|
+
* to the pixel is indistinguishable from no drag, and being re-fitted next
|
|
129
|
+
* time is the right answer for it anyway.
|
|
130
|
+
*
|
|
131
|
+
* A COLLAPSED PANEL MAY BE FOCUS MODE'S. Video at the scene level collapses
|
|
132
|
+
* both side rails and records what it found in `focusRestore*`; the collapse
|
|
133
|
+
* verbs null that slot the moment the author takes ownership. So a non-null
|
|
134
|
+
* slot reads exactly "this bit is the mode's, and here is the author's", the
|
|
135
|
+
* same rule `workspacePrefs` carries across a document switch. Without it a
|
|
136
|
+
* reload taken in Video comes back with those rails collapsed and no pending
|
|
137
|
+
* restore to bring them back: the mode's opinion promoted to a preference.
|
|
138
|
+
*
|
|
139
|
+
* THE DOCK HAS NO SLOT, because no view collapses the timeline. A collapsed
|
|
140
|
+
* dock is always the author's, so it is recorded as it stands.
|
|
141
|
+
*
|
|
142
|
+
* A SUSPENDED SIZE IS STILL THE AUTHOR'S. `held` carries the sizes this boot
|
|
143
|
+
* clamped (see `suspendedSizes`); while a panel is untouched they answer for it
|
|
144
|
+
* instead of the store, so the record keeps the number the author picked rather
|
|
145
|
+
* than the one this window allowed. This function is where the suspension is
|
|
146
|
+
* RELEASED as well as read — it is the only place that sees every notification
|
|
147
|
+
* with the store beside it, and a `held` entry outliving its panel's first drag
|
|
148
|
+
* would pin the record to a number the author has since replaced.
|
|
149
|
+
*/
|
|
150
|
+
function authorLayout(
|
|
151
|
+
store: ShotEditorStore,
|
|
152
|
+
held: Map<SizeField, SuspendedSize>,
|
|
153
|
+
): BootLayoutPrefs {
|
|
154
|
+
const s = store.getState();
|
|
155
|
+
const prefs: BootLayoutPrefs = {
|
|
156
|
+
railCollapsed: s.focusRestoreRail ?? s.railCollapsed,
|
|
157
|
+
inspectorCollapsed: s.focusRestoreInspector ?? s.inspectorCollapsed,
|
|
158
|
+
dockCollapsed: s.dockCollapsed,
|
|
159
|
+
};
|
|
160
|
+
// No window is no fit to compare against, so nothing can be ruled out as
|
|
161
|
+
// the window's doing — same fallback posture as `setDockHeight`.
|
|
162
|
+
const fitted =
|
|
163
|
+
typeof window === "undefined"
|
|
164
|
+
? null
|
|
165
|
+
: layoutFor(window.innerWidth, window.innerHeight);
|
|
166
|
+
for (const field of SIZE_FIELDS) {
|
|
167
|
+
const suspended = held.get(field);
|
|
168
|
+
if (suspended !== undefined) {
|
|
169
|
+
// Still exactly what the boot left it at, so the author has said nothing
|
|
170
|
+
// about this panel since: the record keeps the size they last chose.
|
|
171
|
+
if (s[field] === suspended.boot) {
|
|
172
|
+
prefs[field] = suspended.chosen;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
// They have dragged it. Permanently — a later drag back onto the boot
|
|
176
|
+
// value is a statement about THIS window and belongs in the record.
|
|
177
|
+
held.delete(field);
|
|
178
|
+
}
|
|
179
|
+
if (fitted === null || s[field] !== fitted[field]) prefs[field] = s[field];
|
|
180
|
+
}
|
|
181
|
+
return prefs;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* The sizes the boot did not apply as written, kept out of the record's way.
|
|
186
|
+
*
|
|
187
|
+
* Read at subscribe time, which is the one moment the record on disk and the
|
|
188
|
+
* store agree about what the author asked for: `bootLayout` has just put each
|
|
189
|
+
* saved size through the setters, so a field the store does not hold verbatim
|
|
190
|
+
* is a field this session refused — the dock past `dockCeiling` on a shorter
|
|
191
|
+
* window, or any size a build with different bounds once allowed.
|
|
192
|
+
*
|
|
193
|
+
* The refit gets no exception. It only touches a panel still holding the number
|
|
194
|
+
* `layoutFor` gave it, and a clamp lands on a BOUND: 168/460 for the rail,
|
|
195
|
+
* 264/480 for the inspector, neither of which `layoutFor` returns. The dock is
|
|
196
|
+
* the one that can coincide: under ~536px of window its fit is also its 120
|
|
197
|
+
* floor (`layoutFor` stays under the footer's ceiling), so a sub-minimum record
|
|
198
|
+
* clamps onto the fitted number, and under ~472px the boot's own ceiling is that
|
|
199
|
+
* number too, so any record does. On a window that short the
|
|
200
|
+
* refit is what releases the suspension and the record falls back to fitted.
|
|
201
|
+
* That is a viewport with no editor left in it, and the price is one forgotten
|
|
202
|
+
* size, so it is named here rather than coded around.
|
|
203
|
+
*
|
|
204
|
+
* ONE READ, no argument. `main.tsx` already has the record in hand, but adding
|
|
205
|
+
* a parameter would let a caller pass a record that is not the one the boot
|
|
206
|
+
* saw, which is the only input that could make this lie. A second `getItem` at
|
|
207
|
+
* startup is cheaper than that.
|
|
208
|
+
*/
|
|
209
|
+
function suspendedSizes(store: ShotEditorStore): Map<SizeField, SuspendedSize> {
|
|
210
|
+
const s = store.getState();
|
|
211
|
+
const saved = readLayoutPrefs();
|
|
212
|
+
const held = new Map<SizeField, SuspendedSize>();
|
|
213
|
+
for (const field of SIZE_FIELDS) {
|
|
214
|
+
const chosen = saved[field];
|
|
215
|
+
if (chosen !== undefined && chosen !== s[field])
|
|
216
|
+
held.set(field, { chosen, boot: s[field] });
|
|
217
|
+
}
|
|
218
|
+
return held;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Keep storage in step with the store for the life of the page.
|
|
223
|
+
*
|
|
224
|
+
* ── WHY THE SIGNATURE AND NOT JUST THE SUBSCRIPTION ───────────────────────
|
|
225
|
+
*
|
|
226
|
+
* `subscribe` is the raw channel: it fires on EVERY observable change, and
|
|
227
|
+
* during playback that is once per frame for the transport tick alone. A
|
|
228
|
+
* `setItem` per frame is a synchronous serialise-and-store sixty times a
|
|
229
|
+
* second for a value that has not moved since the author last let go of a
|
|
230
|
+
* handle. So the write is gated on `authorLayout` actually changing, which is
|
|
231
|
+
* the only event this module cares about.
|
|
232
|
+
*
|
|
233
|
+
* The signature is SEEDED at subscribe time, after `bootLayout` has run, so a
|
|
234
|
+
* boot that only restored what was already on disk writes nothing. What keeps
|
|
235
|
+
* the window's own opinions OUT of the record is `authorLayout`, not the seed.
|
|
236
|
+
*
|
|
237
|
+
* CALL IT AFTER `bootLayout`. The seed is also where `suspendedSizes` compares
|
|
238
|
+
* the record against the store, and before the boot there is nothing to
|
|
239
|
+
* compare: the boot's own application of a saved size then arrives as a change
|
|
240
|
+
* like any other, and a size it clamps is written back clamped — the loss the
|
|
241
|
+
* suspension exists to prevent. It is an ordering rule, not a guess, so it is
|
|
242
|
+
* stated here and in `main.tsx` rather than defended against.
|
|
243
|
+
*/
|
|
244
|
+
export function persistLayoutPrefs(store: ShotEditorStore): () => void {
|
|
245
|
+
const held = suspendedSizes(store);
|
|
246
|
+
let last = authorLayout(store, held);
|
|
247
|
+
// A RAW subscription: this runs on every store notification, sixty times a
|
|
248
|
+
// second while the transport plays and once per coalesced pointermove of
|
|
249
|
+
// every drag. It used to `JSON.stringify` the record each time to detect a
|
|
250
|
+
// change that only ever happens on a panel resize; six `Object.is` reads is
|
|
251
|
+
// the same answer.
|
|
252
|
+
return store.subscribe(() => {
|
|
253
|
+
const prefs = authorLayout(store, held);
|
|
254
|
+
if (sameLayoutPrefs(prefs, last)) return;
|
|
255
|
+
last = prefs;
|
|
256
|
+
writeLayoutPrefs(prefs);
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const LAYOUT_PREF_KEYS: readonly (keyof BootLayoutPrefs)[] = [
|
|
261
|
+
"railWidth",
|
|
262
|
+
"inspectorWidth",
|
|
263
|
+
"dockHeight",
|
|
264
|
+
"railCollapsed",
|
|
265
|
+
"inspectorCollapsed",
|
|
266
|
+
"dockCollapsed",
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
function sameLayoutPrefs(a: BootLayoutPrefs, b: BootLayoutPrefs): boolean {
|
|
270
|
+
for (const key of LAYOUT_PREF_KEYS) if (!Object.is(a[key], b[key])) return false;
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// THE LAYOUT TABLE: the three editors Edit can show, and every word, key, menu
|
|
2
|
+
// id and refusal sentence that names one of them.
|
|
3
|
+
//
|
|
4
|
+
// ── WHY THIS FILE EXISTS ────────────────────────────────────────────────
|
|
5
|
+
//
|
|
6
|
+
// Renaming the third layout from "Frame" to "Camera" (founder order,
|
|
7
|
+
// 2026-09-05) cost edits in the toolbar, the View menu, the shortcut table,
|
|
8
|
+
// two refusal sentences in `keymap.ts`, the store's error strip, four tests
|
|
9
|
+
// and two docs — and STILL left the in-app manual teaching the withdrawn
|
|
10
|
+
// word, because nothing tied the manual's prose to the control's chip. Every
|
|
11
|
+
// one of those files had written the same three rows out by hand.
|
|
12
|
+
//
|
|
13
|
+
// So the rows live here once, exactly the way `tools.ts` holds the tool rows:
|
|
14
|
+
// one literal, `as const satisfies Record<...>`, and every list, lookup and
|
|
15
|
+
// sentence below DERIVED from it. A layout cannot be renamed on one surface
|
|
16
|
+
// and not another, because no surface owns a word any more.
|
|
17
|
+
//
|
|
18
|
+
// ── THREE SPELLINGS PER LAYOUT IS THE FACT, NOT THE BUG ─────────────────
|
|
19
|
+
//
|
|
20
|
+
// The chip says "3D", the menu row and the manual say "3D view", and a clause
|
|
21
|
+
// in the middle of a sentence says "the 3D view". Those are three different
|
|
22
|
+
// jobs: a word in a 36px segmented track, a row that has to read as a noun
|
|
23
|
+
// beside "Split", and a fragment inside "…this layout shows ___ only". The
|
|
24
|
+
// table carries all three so that the SURFACES stop inventing a fourth.
|
|
25
|
+
//
|
|
26
|
+
// ── WHY NOT IN `stage/panes.ts` ─────────────────────────────────────────
|
|
27
|
+
//
|
|
28
|
+
// The stage owns the MODES (`StageLayoutMode` there is the vocabulary the
|
|
29
|
+
// bridge versions and the scissor reads); this file owns what the CHROME
|
|
30
|
+
// calls them. The stage has no business carrying a menu id, and the chrome
|
|
31
|
+
// has no business inventing a fourth mode. Types flow one way: this file
|
|
32
|
+
// imports the union, never the reverse.
|
|
33
|
+
import type { StageLayoutMode, StagePane } from "../stage/panes.js";
|
|
34
|
+
|
|
35
|
+
export interface LayoutSpec {
|
|
36
|
+
/** The segmented control's chip. Short: the three sit in one track beside
|
|
37
|
+
* the Edit and Video glyphs, and the track does not grow. */
|
|
38
|
+
label: string;
|
|
39
|
+
/** The App menu row and the manual's own name for it. Reads as a noun on
|
|
40
|
+
* its own, which "3D" does not. */
|
|
41
|
+
title: string;
|
|
42
|
+
/** The same layout inside a sentence, article included: "this layout shows
|
|
43
|
+
* ___ only". Written out rather than composed from `title` because "the
|
|
44
|
+
* split" is not "the Split" and English will not derive that. */
|
|
45
|
+
noun: string;
|
|
46
|
+
/** The combo that goes there, in the house glyphs ("⇧1"). The manual, the
|
|
47
|
+
* menu accelerator, the tooltips and the refusal sentences all print THIS,
|
|
48
|
+
* so a rebind is one edit here. */
|
|
49
|
+
key: string;
|
|
50
|
+
/** The physical key the combo is pressed on, which is what `keymap.ts`
|
|
51
|
+
* reads (`layoutForEvent`). Shift turns "1" into "!" on a US board and
|
|
52
|
+
* leaves it "1" on AZERTY, so the character is no witness to the key. */
|
|
53
|
+
code: string;
|
|
54
|
+
/** The App menu row's id, which is also how `shortcuts.ts` points a row at
|
|
55
|
+
* a menu entry and how the menu asks for its accelerator. */
|
|
56
|
+
menuId: string;
|
|
57
|
+
/** The manual's line for the key. One sentence, no trailing stop: the
|
|
58
|
+
* shortcut table's other rows have none either. */
|
|
59
|
+
does: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The three layouts, IN KEY ORDER (⇧1, ⇧2, ⇧3), which is also the order the
|
|
64
|
+
* toolbar draws them and the order the View menu lists them.
|
|
65
|
+
*
|
|
66
|
+
* `Object.keys` on an object literal is insertion order for string keys, so
|
|
67
|
+
* `LAYOUT_MODES` below inherits that order rather than restating it. The
|
|
68
|
+
* `satisfies` is what makes a fourth mode in `stage/panes.ts` a compile error
|
|
69
|
+
* here instead of a layout with no word for it.
|
|
70
|
+
*/
|
|
71
|
+
export const LAYOUT_SPECS = {
|
|
72
|
+
working: {
|
|
73
|
+
label: "3D",
|
|
74
|
+
title: "3D view",
|
|
75
|
+
noun: "the 3D view",
|
|
76
|
+
key: "⇧1",
|
|
77
|
+
code: "Digit1",
|
|
78
|
+
menuId: "view.layout.working",
|
|
79
|
+
does: "3D view",
|
|
80
|
+
},
|
|
81
|
+
split: {
|
|
82
|
+
label: "Split",
|
|
83
|
+
title: "Split",
|
|
84
|
+
noun: "the split",
|
|
85
|
+
key: "⇧2",
|
|
86
|
+
code: "Digit2",
|
|
87
|
+
menuId: "view.layout.split",
|
|
88
|
+
// Names the other two through the table rather than spelling them, so the
|
|
89
|
+
// manual's description of the split cannot outlive their names.
|
|
90
|
+
does: "Split (the 3D view with the camera beside it)",
|
|
91
|
+
},
|
|
92
|
+
camera: {
|
|
93
|
+
label: "Camera",
|
|
94
|
+
title: "Camera",
|
|
95
|
+
noun: "the camera view",
|
|
96
|
+
key: "⇧3",
|
|
97
|
+
code: "Digit3",
|
|
98
|
+
menuId: "view.layout.camera",
|
|
99
|
+
does: "Camera: the shot camera's picture, with handles",
|
|
100
|
+
},
|
|
101
|
+
} as const satisfies Record<StageLayoutMode, LayoutSpec>;
|
|
102
|
+
|
|
103
|
+
/** Every layout, in key order. The cast is the one place this file asserts
|
|
104
|
+
* what `satisfies` has already proved: the literal above is the only writer
|
|
105
|
+
* of the object and its key set IS `StageLayoutMode`, which `Object.keys`
|
|
106
|
+
* cannot say because its return type is `string[]` for every object. */
|
|
107
|
+
export const LAYOUT_MODES = Object.keys(LAYOUT_SPECS) as readonly StageLayoutMode[];
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* One row, seen as a `LayoutSpec` rather than as its own literal type.
|
|
111
|
+
*
|
|
112
|
+
* `as const satisfies` narrows every row to exactly the strings it wrote,
|
|
113
|
+
* which is excellent for the table and useless for anything iterating it.
|
|
114
|
+
* This is the one widening, and it is in one place — the same bargain
|
|
115
|
+
* `specOf` makes in `tools.ts`.
|
|
116
|
+
*/
|
|
117
|
+
export function layoutSpec(mode: StageLayoutMode): LayoutSpec {
|
|
118
|
+
return LAYOUT_SPECS[mode];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Keyed on the physical key, built from the table so a rebind cannot leave
|
|
122
|
+
* the key lane pointing at the old one. */
|
|
123
|
+
const BY_CODE: ReadonlyMap<string, StageLayoutMode> = new Map(
|
|
124
|
+
LAYOUT_MODES.map((mode) => [LAYOUT_SPECS[mode].code, mode]),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* THE KEYMAP'S WHOLE LAYOUT LANE: the layout this event picks, or null when
|
|
129
|
+
* no layout claims it.
|
|
130
|
+
*
|
|
131
|
+
* SHIFTED, AND BY `code`. The bare digits are left free for the editor's
|
|
132
|
+
* numeric verbs, and a shifted digit reports a different character on every
|
|
133
|
+
* keyboard layout ("!" on US, "1" on AZERTY), so only the physical key names
|
|
134
|
+
* it. The keymap asks this after the modifier lanes, so ⌘ and ⌥ never reach
|
|
135
|
+
* it; the shift is checked here because the bare digit must not match.
|
|
136
|
+
*/
|
|
137
|
+
export function layoutForEvent(event: { shiftKey: boolean; code?: string }): StageLayoutMode | null {
|
|
138
|
+
if (!event.shiftKey || event.code === undefined) return null;
|
|
139
|
+
return BY_CODE.get(event.code) ?? null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The TAIL every "this layout cannot do that" refusal shares, given the pane
|
|
144
|
+
* that IS on screen.
|
|
145
|
+
*
|
|
146
|
+
* ── WHY A FRAGMENT AND NOT A SENTENCE ───────────────────────────────────
|
|
147
|
+
*
|
|
148
|
+
* Three callers refuse for three different reasons and each owns its own lead
|
|
149
|
+
* clause: a tool says "Rect draws in the camera view, and …", the menu says
|
|
150
|
+
* "That aims the 3D view, and …", the key lane says "that key aims the 3D
|
|
151
|
+
* view, and …" (lower case, because the store prefixes the strip's own word).
|
|
152
|
+
* Only the second half is one rule, so only the second half is shared. A
|
|
153
|
+
* function that returned the whole sentence would have to take the lead as an
|
|
154
|
+
* argument, which is the same duplication with extra steps.
|
|
155
|
+
*
|
|
156
|
+
* What it says: which view you are looking at, the key for the other one,
|
|
157
|
+
* and the key for the split that has both. The keys are in the house glyphs
|
|
158
|
+
* ("⇧1"); a caller that shows the sentence runs it through `formatCombo`, which
|
|
159
|
+
* this file cannot import (`shortcuts.ts` imports this one). Every one of those comes out of
|
|
160
|
+
* the table, so renaming a layout or rebinding a digit rewrites all three
|
|
161
|
+
* refusals with no edit here.
|
|
162
|
+
*/
|
|
163
|
+
export function paneOnlyRefusal(shown: StagePane): string {
|
|
164
|
+
const here = LAYOUT_SPECS[shown];
|
|
165
|
+
const other = LAYOUT_SPECS[shown === "camera" ? "working" : "camera"];
|
|
166
|
+
const split = LAYOUT_SPECS.split;
|
|
167
|
+
return (
|
|
168
|
+
`this layout shows ${here.noun} only. ` +
|
|
169
|
+
`Press ${other.key} for ${other.noun}, or ${split.key} for ${split.noun}.`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Why a view the author asked for cannot be entered here, or null when it can.
|
|
175
|
+
*
|
|
176
|
+
* Its own sentence rather than an arm of `toolRefusal`: these are not tools,
|
|
177
|
+
* and what is refused is a VIEW rather than a place to draw.
|
|
178
|
+
*
|
|
179
|
+
* ONE REFUSAL LEFT. This used to turn the 3D view and the split away above a
|
|
180
|
+
* shot too ("the scene view has one pane"), and that was withdrawn by founder
|
|
181
|
+
* order on 2026-09-05: all three layouts are live at the scene level, where
|
|
182
|
+
* the working pane draws the shot under the playhead, exactly as the stage
|
|
183
|
+
* already resolved it (`StageApp` reads `masterToSceneCached`, never the
|
|
184
|
+
* entered shot). What remains is the definition, which has no shot camera at
|
|
185
|
+
* all.
|
|
186
|
+
*
|
|
187
|
+
* It lives beside the table rather than in `keymap.ts` because the three
|
|
188
|
+
* things that ask it — the digits, the segmented control and the View menu —
|
|
189
|
+
* are the three things that read the table, and the answer is about layouts
|
|
190
|
+
* rather than about keys.
|
|
191
|
+
*/
|
|
192
|
+
export function layoutRefusal(inDefinition: boolean): string | null {
|
|
193
|
+
// A DEFINITION HAS NO SHOT CAMERA AT ALL, so neither Video nor a layout over
|
|
194
|
+
// it names anything it can show. The stage forces the working-only layout
|
|
195
|
+
// while one is open (StageApp's `inComponent`), so without this the keys
|
|
196
|
+
// (and the control that shares this rule) changed a mode nothing read and
|
|
197
|
+
// then reported it as active.
|
|
198
|
+
if (inDefinition) {
|
|
199
|
+
return (
|
|
200
|
+
`A component definition has no shot camera, so there is nothing to ` +
|
|
201
|
+
`switch to. Leave the definition (Esc) to get Edit and Video back.`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// The one impure half of "a dropped recording should land at its own aspect":
|
|
2
|
+
// asking the browser to decode enough of an image or a video to report how wide
|
|
3
|
+
// and how tall it is.
|
|
4
|
+
//
|
|
5
|
+
// ── WHY IT IS NOT IN insert.ts ──────────────────────────────────────────────
|
|
6
|
+
//
|
|
7
|
+
// That file's contract is stated in its own header: no document, no history, no
|
|
8
|
+
// asynchrony, no DOM. That is exactly what makes `boxOf` provable by calling it,
|
|
9
|
+
// and a throwaway `<video>` on an object URL is all four of the things it
|
|
10
|
+
// forbids. So the measurement happens here, the number crosses as
|
|
11
|
+
// `InsertContext.sourceAspect`, and the geometry stays pure.
|
|
12
|
+
//
|
|
13
|
+
// ── WHY IT NEVER REJECTS, AND WHY IT HAS A DEADLINE ─────────────────────────
|
|
14
|
+
//
|
|
15
|
+
// Every failure resolves to `null`, and `null` reproduces the pre-aspect
|
|
16
|
+
// geometry byte for byte. That is not defensiveness, it is the requirement:
|
|
17
|
+
//
|
|
18
|
+
// - jsdom, which the editor's own test lane runs in, decodes no media at all.
|
|
19
|
+
// `loadedmetadata` never fires and `videoWidth` stays 0. Without a deadline
|
|
20
|
+
// the existing `completeImageDraw` tests would each become a 30s timeout
|
|
21
|
+
// (the fast lane's own `testTimeout`), which is a suite that "still passes"
|
|
22
|
+
// while taking four minutes to say so.
|
|
23
|
+
// - A real browser fails too: a codec it cannot decode, a file that is not
|
|
24
|
+
// the media its extension claims, a revoked blob.
|
|
25
|
+
//
|
|
26
|
+
// Nothing here is on the render path and nothing here reaches a document, so a
|
|
27
|
+
// wrong answer costs a badly proportioned box the author can drag. A HANG would
|
|
28
|
+
// cost the insert itself, which is why the deadline is not optional.
|
|
29
|
+
//
|
|
30
|
+
// ── NOT PERSISTED ON THE ASSET ──────────────────────────────────────────────
|
|
31
|
+
//
|
|
32
|
+
// `ShotAsset` is `{id, path, hash, kind}` and stays that way. Storing width and
|
|
33
|
+
// height would mean a probe at upload (ffmpeg, which the CLI only shells for
|
|
34
|
+
// audio today), a schema field, and two places that can disagree about one
|
|
35
|
+
// file's dimensions. The runtime already learns the real size from the decoded
|
|
36
|
+
// texture; this is only the INSERT's guess at a good starting box.
|
|
37
|
+
|
|
38
|
+
/** How long a decode may take before the insert gives up and uses the default
|
|
39
|
+
* geometry. Long enough for a large local file to report metadata, short
|
|
40
|
+
* enough that a host which will never answer (jsdom) does not stall a click. */
|
|
41
|
+
const MEASURE_TIMEOUT_MS = 2000;
|
|
42
|
+
|
|
43
|
+
/** The slice of the media elements this needs. Named so a test can hand over a
|
|
44
|
+
* fake without a DOM, the same seam `textureCache` uses for its videos. */
|
|
45
|
+
export interface MeasurableElement {
|
|
46
|
+
addEventListener(type: string, listener: () => void): void;
|
|
47
|
+
removeEventListener(type: string, listener: () => void): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface MeasureDeps {
|
|
51
|
+
/** Absent = `URL.createObjectURL` / `URL.revokeObjectURL`. */
|
|
52
|
+
objectUrl?: { create: (file: Blob) => string; revoke: (url: string) => void };
|
|
53
|
+
/** Absent = a real `<video>` or `<img>`. */
|
|
54
|
+
createElement?: (kind: "image" | "video") => MeasurableElement;
|
|
55
|
+
setTimeout?: (fn: () => void, ms: number) => unknown;
|
|
56
|
+
clearTimeout?: (handle: unknown) => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* width / height of the media at `url`, or null if it cannot be determined.
|
|
61
|
+
*
|
|
62
|
+
* NEVER REJECTS. See the header: every failure path is a `null` that puts the
|
|
63
|
+
* caller back on the geometry it had before this function existed.
|
|
64
|
+
*/
|
|
65
|
+
export async function measureMediaUrl(
|
|
66
|
+
url: string,
|
|
67
|
+
kind: "image" | "video",
|
|
68
|
+
deps: MeasureDeps = {},
|
|
69
|
+
): Promise<number | null> {
|
|
70
|
+
const create =
|
|
71
|
+
deps.createElement ??
|
|
72
|
+
((k: "image" | "video") =>
|
|
73
|
+
(k === "video"
|
|
74
|
+
? Object.assign(document.createElement("video"), { preload: "metadata", muted: true })
|
|
75
|
+
: document.createElement("img")) as unknown as MeasurableElement);
|
|
76
|
+
const later = deps.setTimeout ?? ((fn: () => void, ms: number) => globalThis.setTimeout(fn, ms));
|
|
77
|
+
const cancel = deps.clearTimeout ?? ((h: unknown) => globalThis.clearTimeout(h as number));
|
|
78
|
+
|
|
79
|
+
return new Promise<number | null>((resolve) => {
|
|
80
|
+
let settled = false;
|
|
81
|
+
let timer: unknown = null;
|
|
82
|
+
const el = create(kind);
|
|
83
|
+
// The event that says the dimensions exist. A video reports them at
|
|
84
|
+
// `loadedmetadata`, which is well before it could play; an image at `load`.
|
|
85
|
+
const ready = kind === "video" ? "loadedmetadata" : "load";
|
|
86
|
+
|
|
87
|
+
const finish = (aspect: number | null): void => {
|
|
88
|
+
if (settled) return;
|
|
89
|
+
settled = true;
|
|
90
|
+
if (timer !== null) cancel(timer);
|
|
91
|
+
el.removeEventListener(ready, onReady);
|
|
92
|
+
el.removeEventListener("error", onError);
|
|
93
|
+
// The src is cleared so a video that has not finished buffering stops,
|
|
94
|
+
// and so the element drops its reference to a URL the caller is about to
|
|
95
|
+
// revoke. Guarded, because a fake element has no `src`.
|
|
96
|
+
try {
|
|
97
|
+
(el as unknown as { src?: string }).src = "";
|
|
98
|
+
} catch {
|
|
99
|
+
// A fake with a getter-only `src`. Nothing to release.
|
|
100
|
+
}
|
|
101
|
+
resolve(aspect);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
function onReady(): void {
|
|
105
|
+
const e = el as unknown as { videoWidth?: number; videoHeight?: number; naturalWidth?: number; naturalHeight?: number };
|
|
106
|
+
// Chosen BY KIND rather than by `videoWidth ?? naturalWidth`. The
|
|
107
|
+
// coalesce works only because a real `<img>` has no `videoWidth` at all,
|
|
108
|
+
// which makes it correct by accident: anything exposing both (a fake, a
|
|
109
|
+
// polyfill, a future element) reads the video pair's 0 and reports
|
|
110
|
+
// nothing. The kind is already known here, so ask for the right pair.
|
|
111
|
+
const w = (kind === "video" ? e.videoWidth : e.naturalWidth) ?? 0;
|
|
112
|
+
const h = (kind === "video" ? e.videoHeight : e.naturalHeight) ?? 0;
|
|
113
|
+
// `!(...)` so a NaN from a host that reports one lands here rather than
|
|
114
|
+
// becoming a NaN aspect the geometry would then have to defend against.
|
|
115
|
+
finish(!(w > 0) || !(h > 0) ? null : w / h);
|
|
116
|
+
}
|
|
117
|
+
function onError(): void {
|
|
118
|
+
finish(null);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
el.addEventListener(ready, onReady);
|
|
122
|
+
el.addEventListener("error", onError);
|
|
123
|
+
timer = later(() => finish(null), MEASURE_TIMEOUT_MS);
|
|
124
|
+
try {
|
|
125
|
+
(el as unknown as { src: string }).src = url;
|
|
126
|
+
} catch {
|
|
127
|
+
finish(null);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The same, for a `File` the author just picked: wraps it in an object URL and
|
|
134
|
+
* always revokes it, including on the timeout path.
|
|
135
|
+
*
|
|
136
|
+
* A blob left unrevoked pins the whole file in memory for the tab's lifetime,
|
|
137
|
+
* and this runs on a path whose files are screen recordings.
|
|
138
|
+
*/
|
|
139
|
+
export async function measureMediaFile(
|
|
140
|
+
file: Blob,
|
|
141
|
+
kind: "image" | "video",
|
|
142
|
+
deps: MeasureDeps = {},
|
|
143
|
+
): Promise<number | null> {
|
|
144
|
+
const urls = deps.objectUrl ?? {
|
|
145
|
+
create: (f: Blob) => URL.createObjectURL(f),
|
|
146
|
+
revoke: (u: string) => {
|
|
147
|
+
URL.revokeObjectURL(u);
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
let url: string;
|
|
151
|
+
try {
|
|
152
|
+
url = urls.create(file);
|
|
153
|
+
} catch {
|
|
154
|
+
return null; // a host with no object-URL support measures nothing
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
return await measureMediaUrl(url, kind, deps);
|
|
158
|
+
} finally {
|
|
159
|
+
try {
|
|
160
|
+
urls.revoke(url);
|
|
161
|
+
} catch {
|
|
162
|
+
// Already gone. Revoking twice is not an error worth surfacing.
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export { MEASURE_TIMEOUT_MS };
|