@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
package/src/state/keymap.ts
CHANGED
|
@@ -27,23 +27,38 @@
|
|
|
27
27
|
//
|
|
28
28
|
// ARROWS are listed as both "nudge the playhead" and "nudge the layer".
|
|
29
29
|
// SELECTION WINS; an empty selection falls through to the playhead. That is
|
|
30
|
-
// the only reading where both are reachable without a mode.
|
|
30
|
+
// the only reading where both are reachable without a mode. The playhead
|
|
31
|
+
// half then forks three ways — ⇧ is a second, ↑/↓ walk the edit points, and
|
|
32
|
+
// ←/→ step a frame — because the vertical pair was otherwise a second
|
|
33
|
+
// spelling of the horizontal one.
|
|
31
34
|
//
|
|
32
35
|
// `C` was listed as both the cut tool and split-at-playhead. THE CUT TOOL IS
|
|
33
36
|
// GONE — it was a fourteenth row in the tool table that split a scene under
|
|
34
|
-
// the cursor, which
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
37
|
+
// the cursor, which `⌘⇧B` now does from the playhead with no mode to arm
|
|
38
|
+
// and no mode to leave. The splitters are `⌘B` (the razor, over the layers
|
|
39
|
+
// under the playhead) and `⌘⇧B` (the shot itself), and no letter arms
|
|
40
|
+
// either. `⇧C` is Blender's "frame all" rather than the camera-move it used
|
|
41
|
+
// to be (the `+ shot` button owns that, via `store.addShotAtPlayhead`).
|
|
38
42
|
//
|
|
39
43
|
// SPACE is both play/pause and hold-to-pan. It fires on KEYUP, and only when
|
|
40
44
|
// no pan drag happened during the hold — which is why the stage owns the
|
|
41
45
|
// suppression (it is the half that knows) and the chrome, which cannot pan,
|
|
42
46
|
// always fires.
|
|
43
|
-
import { toolForKey } from "../tools.js";
|
|
47
|
+
import { isInsertTool, specOf, toolForKey } from "../tools.js";
|
|
44
48
|
import type { ShotTool } from "../tools.js";
|
|
45
49
|
import type { StageLayout } from "../stage/panes.js";
|
|
50
|
+
// The layout table: the digit each of the three answers to, the words that name
|
|
51
|
+
// one inside a sentence, and the tail both pane refusals below share with the
|
|
52
|
+
// View menu and the segmented control.
|
|
53
|
+
import { layoutForEvent, layoutSpec, paneOnlyRefusal } from "./layouts.js";
|
|
54
|
+
import { formatCombo } from "./shortcuts.js";
|
|
55
|
+
// The digit for Video, from the same table the bar and the menu row read.
|
|
56
|
+
import { VIEW_MODE_SPECS } from "./viewMode.js";
|
|
57
|
+
import { VIEW_ONLY } from "./workspace.js";
|
|
58
|
+
import type { ViewMode } from "./viewMode.js";
|
|
59
|
+
import type { SketchTool } from "./types.js";
|
|
46
60
|
import { ORBIT_STEP_DEG } from "../stage/workingView.js";
|
|
61
|
+
import type { AlignMode, FlipAxis, RestackTarget } from "@dailies-studio/shot/doc";
|
|
47
62
|
|
|
48
63
|
export type KeyAction =
|
|
49
64
|
| { kind: "tool"; tool: ShotTool }
|
|
@@ -51,10 +66,42 @@ export type KeyAction =
|
|
|
51
66
|
| { kind: "redo" }
|
|
52
67
|
| { kind: "save" }
|
|
53
68
|
| { kind: "playToggle" }
|
|
69
|
+
/**
|
|
70
|
+
* SHUTTLE: one rung along `SHUTTLE_RATES`, and run.
|
|
71
|
+
*
|
|
72
|
+
* `dir` is which way along the LADDER, never which way the picture goes: from
|
|
73
|
+
* −2 a `+1` is −1, which is still reverse and slower. The store owns the
|
|
74
|
+
* ladder because the rung depends on the rate the transport is already at,
|
|
75
|
+
* which this table deliberately does not take.
|
|
76
|
+
*/
|
|
77
|
+
| { kind: "shuttle"; dir: -1 | 1 }
|
|
78
|
+
/** Stop, and put the rate back to 1. NOT `playToggle`: an author leaning on K
|
|
79
|
+
* means "stop", and a toggle would start the transport again on the second
|
|
80
|
+
* press of a key whose whole job is to end a shuttle. */
|
|
81
|
+
| { kind: "shuttleStop" }
|
|
54
82
|
/** Move the playhead by whole frames. */
|
|
55
83
|
| { kind: "step"; delta: number }
|
|
84
|
+
/** Move the playhead by whole SECONDS. Its own action and not a `step` with a
|
|
85
|
+
* bigger delta, because this table cannot know the fps: a second is
|
|
86
|
+
* `doc.fps` frames, and inventing a number here would be a second opinion on
|
|
87
|
+
* the document's own rate. `delta` is ±1 second. */
|
|
88
|
+
| { kind: "stepSeconds"; delta: -1 | 1 }
|
|
89
|
+
/** The playhead to the previous / next EDIT POINT: a shot boundary, a layer's
|
|
90
|
+
* lifespan edge, or a marker. The three places something in the film
|
|
91
|
+
* actually changes, which is what an author scanning a cut is looking for. */
|
|
92
|
+
| { kind: "jumpEdit"; dir: -1 | 1 }
|
|
93
|
+
/** The playhead to the selection's own in or out point. */
|
|
94
|
+
| { kind: "seekSelectionEdge"; edge: "in" | "out" }
|
|
95
|
+
/** Put one edge of the work area at the playhead. */
|
|
96
|
+
| { kind: "setWorkArea"; edge: "in" | "out" }
|
|
97
|
+
/** Drop the work area: playback goes back to looping the whole document. */
|
|
98
|
+
| { kind: "clearWorkArea" }
|
|
56
99
|
/** Move the selection, in design px. */
|
|
57
100
|
| { kind: "nudge"; dx: number; dy: number }
|
|
101
|
+
| { kind: "nudgeKeys"; delta: number }
|
|
102
|
+
| { kind: "jumpKeyframe"; dir: -1 | 1 }
|
|
103
|
+
| { kind: "copyKeyframes" }
|
|
104
|
+
| { kind: "pasteKeyframes" }
|
|
58
105
|
| { kind: "fit" }
|
|
59
106
|
/** Fit the whole document across the timeline dock. A SECOND fit and not an
|
|
60
107
|
* arm of the first: `⇧Z` addresses the active stage pane, and the dock is
|
|
@@ -62,28 +109,84 @@ export type KeyAction =
|
|
|
62
109
|
| { kind: "fitTimeline" }
|
|
63
110
|
| { kind: "zoom"; direction: 1 | -1 }
|
|
64
111
|
| { kind: "zoomReset" }
|
|
65
|
-
/**
|
|
66
|
-
* is where we already are", which is what numpad-0
|
|
67
|
-
* 1 / 2 / 3 row deliberately does NOT: pressing 2
|
|
68
|
-
* split.
|
|
112
|
+
/** WHICH EDITOR the stage shows, and over how much of it. `toggle` means
|
|
113
|
+
* "…and back again if that is where we already are", which is what numpad-0
|
|
114
|
+
* needs and what the ⇧1 / ⇧2 / ⇧3 row deliberately does NOT: pressing ⇧2
|
|
115
|
+
* twice must not leave the split. Asking for any of the three also ends a
|
|
116
|
+
* screening. See `setViewLayout`. */
|
|
69
117
|
| { kind: "stageLayout"; mode: StageLayout["mode"]; toggle?: boolean }
|
|
118
|
+
/** WATCHING OR WORKING: ⇧4 goes to the finished picture, Escape's top rung
|
|
119
|
+
* comes back. Not a layout. It is drawn OVER whichever of the three the
|
|
120
|
+
* author left, and hands it back untouched. */
|
|
121
|
+
| { kind: "viewMode"; mode: ViewMode }
|
|
70
122
|
/** Point the shot camera where the working pane is looking. */
|
|
71
123
|
| { kind: "alignCameraToView" }
|
|
124
|
+
/** PILOT the shot camera from the Camera pane, or stop: Num0. Blender's
|
|
125
|
+
* numpad-0 looks through the camera; ours also puts the author's hand on
|
|
126
|
+
* it, and the badge over the pane with its ESC is what keeps that honest. */
|
|
127
|
+
| { kind: "pilotToggle" }
|
|
128
|
+
/** Escape's first rung while piloting: hands are off the camera, which stays
|
|
129
|
+
* where it is. */
|
|
130
|
+
| { kind: "pilotEject" }
|
|
72
131
|
/** Drop a marker at the playhead. */
|
|
73
132
|
| { kind: "addMarker" }
|
|
74
133
|
/** Hide a side rail, or every panel at once. */
|
|
75
134
|
| { kind: "togglePanel"; panel: "rail" | "inspector" | "all" }
|
|
135
|
+
/**
|
|
136
|
+
* Open the app menu, with its search field focused.
|
|
137
|
+
*
|
|
138
|
+
* The only action in this table whose effect is CHROME rather than document or
|
|
139
|
+
* workspace state, which is why the store answers it by parking a mailbox
|
|
140
|
+
* (`appMenu`) rather than by doing anything: a keydown listener on the window
|
|
141
|
+
* cannot reach a component's popover, and a second listener inside that
|
|
142
|
+
* component is the thing this file's header forbids.
|
|
143
|
+
*/
|
|
144
|
+
| { kind: "appMenu" }
|
|
145
|
+
/** Put the caret in the left column's search box. A mailbox for `appMenu`'s
|
|
146
|
+
* reason: only the panel holds the input. */
|
|
147
|
+
| { kind: "focusSearch" }
|
|
76
148
|
| { kind: "animateToggle" }
|
|
77
149
|
| { kind: "openExport" }
|
|
78
150
|
| { kind: "deleteSelection" }
|
|
151
|
+
/** Delete the selected layers AND pull everything after them back, closing
|
|
152
|
+
* the gap they leave. Its own action and not a flag on `deleteSelection`,
|
|
153
|
+
* because plain delete leaving a gap is the deliberate answer: a gap shows
|
|
154
|
+
* the scene background, which is a picture in this product. */
|
|
155
|
+
| { kind: "rippleDelete" }
|
|
79
156
|
| { kind: "duplicate" }
|
|
157
|
+
/** The selected layers onto the layer clip (`store.copyLayers`), and the
|
|
158
|
+
* same plus a delete; the paste lands copies in their Shot. */
|
|
159
|
+
| { kind: "copyLayers" }
|
|
160
|
+
| { kind: "cutLayers" }
|
|
161
|
+
| { kind: "pasteLayers" }
|
|
162
|
+
/** The motion clip (`store.copyAnimation` / `copyEffects`) and its pastes
|
|
163
|
+
* onto the selection. Animation pastes at the playhead from the keyboard;
|
|
164
|
+
* "At original time" is a menu row only. */
|
|
165
|
+
| { kind: "copyAnimation" }
|
|
166
|
+
| { kind: "pasteAnimation" }
|
|
167
|
+
| { kind: "copyEffects" }
|
|
168
|
+
| { kind: "pasteEffects" }
|
|
80
169
|
| { kind: "selectAll" }
|
|
81
170
|
| { kind: "deselect" }
|
|
171
|
+
/** THE RAZOR: cut the layers under the playhead. See `store.splitAtPlayhead`
|
|
172
|
+
* for the subject ladder it walks. */
|
|
82
173
|
| { kind: "splitAtPlayhead" }
|
|
174
|
+
/** Cut the SHOT in two at the playhead. A separate verb and a separate key
|
|
175
|
+
* because it is a different subject, not a different mode of the razor: one
|
|
176
|
+
* cuts content inside a shot, the other cuts the shot itself and moves every
|
|
177
|
+
* master frame after it. */
|
|
178
|
+
| { kind: "splitSceneAtPlayhead" }
|
|
83
179
|
| { kind: "group" }
|
|
84
180
|
| { kind: "ungroup" }
|
|
181
|
+
/** ARRANGE the selection (`store.restack`): each layer among its own
|
|
182
|
+
* siblings, which is paint order. The bracket chords, see `arrangeChord`. */
|
|
183
|
+
| { kind: "arrange"; to: RestackTarget }
|
|
184
|
+
/** An Arrange chord over the storyboard or the material graph, CLAIMED and
|
|
185
|
+
* dropped: nothing there stacks, and the layer selection under the canvas
|
|
186
|
+
* is not the subject (the lanes' own rule), but on a Mac ⌘[ and ⌘] would
|
|
187
|
+
* otherwise be the browser's Back and Forward, out of the editor. */
|
|
188
|
+
| { kind: "arrangeNowhere" }
|
|
85
189
|
| { kind: "toggleLock" }
|
|
86
|
-
| { kind: "toggleHidden" }
|
|
87
190
|
/** Finish the path the pen is drawing, keeping it. */
|
|
88
191
|
| { kind: "penCommit" }
|
|
89
192
|
/** Throw away the path the pen is drawing. */
|
|
@@ -92,13 +195,25 @@ export type KeyAction =
|
|
|
92
195
|
| { kind: "enterIsolation" }
|
|
93
196
|
/** Come back up ONE level. */
|
|
94
197
|
| { kind: "exitIsolation" }
|
|
198
|
+
/** Back to the Shot a door into Components came from: its Shot, frame and
|
|
199
|
+
* selection. The bar's "Back to Shot" makes the same move by click. */
|
|
200
|
+
| { kind: "backToShot" }
|
|
201
|
+
| { kind: "closeComponentContext" }
|
|
202
|
+
/** Leave the code studio. The panel owns the confirm when the buffer is
|
|
203
|
+
* dirty, because only the panel can ask. */
|
|
204
|
+
| { kind: "closeCodeComponent" }
|
|
205
|
+
/** Leave Components while it shows no component (its empty state). */
|
|
206
|
+
| { kind: "leaveComponents" }
|
|
207
|
+
/** Back out of the entered shot, to the scene context. */
|
|
208
|
+
| { kind: "leaveScene" }
|
|
95
209
|
/** Begin a Blender-style modal transform in the working pane. The stage
|
|
96
210
|
* decides the TARGET (selection = layers, empty = the camera); the action
|
|
97
211
|
* only carries which transform the letter named. */
|
|
98
212
|
| { kind: "modalStart"; mode: "move" | "rotate" | "scale" }
|
|
99
|
-
/** Constrain the live modal transform to one axis
|
|
100
|
-
*
|
|
101
|
-
|
|
213
|
+
/** Constrain the live modal transform to one axis — or, with `plane`, to the
|
|
214
|
+
* PLANE normal to it (Blender's `⇧X`). The same constraint again clears it
|
|
215
|
+
* (the reducer owns that toggle, not this table). */
|
|
216
|
+
| { kind: "modalAxis"; axis: "x" | "y" | "z"; plane: boolean }
|
|
102
217
|
/** One character typed into the live transform's numeric buffer: a digit, a
|
|
103
218
|
* decimal point, or `-` to flip the sign (Blender's own rule). `G X 200
|
|
104
219
|
* Enter` is what this exists for. */
|
|
@@ -133,15 +248,110 @@ export type KeyAction =
|
|
|
133
248
|
/**
|
|
134
249
|
* Hide explicitly, rather than toggling `selection[0]`'s own flag.
|
|
135
250
|
*
|
|
136
|
-
* `"selection"` hides what is selected, `"others"` hides everything else
|
|
137
|
-
* (
|
|
251
|
+
* `"selection"` hides what is selected (H), `"others"` hides everything else
|
|
252
|
+
* (⌥⇧H), `"reveal"` unhides the whole scene (⌘⇧H). The toggle had no way back
|
|
138
253
|
* once the hidden layers were deselected, which is most of the time.
|
|
139
254
|
*/
|
|
140
255
|
| { kind: "setHidden"; scope: "selection" | "others" | "reveal" }
|
|
141
|
-
/** ⌥G / ⌥R /
|
|
256
|
+
/** ⌥G / ⌥R / ⌥⇧S: back to the layer's own zero on one channel. */
|
|
142
257
|
| { kind: "clearTransform"; channel: "offset" | "rotation" | "scale" }
|
|
258
|
+
/** ⌥A / ⌥D / ⌥W / ⌥S / ⌥H / ⌥V: the six alignments (`store.alignSelection`). */
|
|
259
|
+
| { kind: "align"; mode: AlignMode }
|
|
260
|
+
/** A bare digit with a selection: opacity in tenths, and 0 is full. */
|
|
261
|
+
| { kind: "opacity"; value: number }
|
|
262
|
+
/** Q / W: trim the selection's in or out point to the playhead. W keeps the
|
|
263
|
+
* playhead's own frame, so the out lands one past it. */
|
|
264
|
+
| { kind: "trimToPlayhead"; edge: "in" | "out" }
|
|
265
|
+
/** ⇧H / ⇧V: mirror the selection on the screen's x or y (Figma's keys). */
|
|
266
|
+
| { kind: "flip"; axis: FlipAxis }
|
|
143
267
|
/** ⇧Z: wireframe on, and back to whatever the shading was. Blender's. */
|
|
144
|
-
| { kind: "wireframeToggle" }
|
|
268
|
+
| { kind: "wireframeToggle" }
|
|
269
|
+
/** ⌥Z: the working pane's grid floor on / off. The alt lane is where this
|
|
270
|
+
* chrome keeps the verbs whose letter is already spoken for, and the grid
|
|
271
|
+
* is the Z family's overlay (Blender keeps its grid behind ⇧⌥Z's overlay
|
|
272
|
+
* toggle; this stage has exactly one overlay floor, so one key does). */
|
|
273
|
+
| { kind: "gridToggle" }
|
|
274
|
+
/** ⌥M: the dock's Sound face on / off (gap 8). M for mixer; bare M drops a
|
|
275
|
+
* marker, so the mixer takes the alt lane. */
|
|
276
|
+
| { kind: "soundMode" }
|
|
277
|
+
/** ⇧R: the camera pane's rulers on / off (Paper page 24). Figma's and
|
|
278
|
+
* Photoshop's key family; the plain letter stays rotate / rect. */
|
|
279
|
+
| { kind: "rulersToggle" }
|
|
280
|
+
/** ⌘, : the Preferences dialog, every Mac app's key for it. */
|
|
281
|
+
| { kind: "openPrefs" }
|
|
282
|
+
// ── THE STORYBOARD'S OWN LANE ────────────────────────────────────────────
|
|
283
|
+
//
|
|
284
|
+
// Prefixed `storyboard`, produced only with `storyboard: true` (or, for the
|
|
285
|
+
// toggle, from the ordinary shell), for the graph lane's reason: Delete over
|
|
286
|
+
// a card must remove the card, never the layer the shell still has selected
|
|
287
|
+
// underneath the board.
|
|
288
|
+
| { kind: "storyboardToggle" }
|
|
289
|
+
| { kind: "storyboardDelete" }
|
|
290
|
+
| { kind: "storyboardSelectAll" }
|
|
291
|
+
| { kind: "storyboardEscape" }
|
|
292
|
+
| { kind: "storyboardDuplicate" }
|
|
293
|
+
| { kind: "storyboardRename" }
|
|
294
|
+
// COMMENTS (plan C5, S5): C comments on the board card selected or, off
|
|
295
|
+
// the board, arms the comment tool on the Studio's camera pane. N and
|
|
296
|
+
// Shift+N walk the open threads in board order, on the board only.
|
|
297
|
+
| { kind: "storyboardComment" }
|
|
298
|
+
| { kind: "storyboardNextThread" }
|
|
299
|
+
| { kind: "storyboardPrevThread" }
|
|
300
|
+
/**
|
|
301
|
+
* ⌘C / ⌘X / ⌘V ON THE BOARD, AND ITS ARM RETURNS `false`. That is the whole
|
|
302
|
+
* design and it is not a stub.
|
|
303
|
+
*
|
|
304
|
+
* These three currently fall through to `null`, nothing is
|
|
305
|
+
* `preventDefault`ed, and THAT IS PRECISELY why the browser's own `paste`
|
|
306
|
+
* event still reaches the board and lands image and video FILES from the
|
|
307
|
+
* system clipboard. Binding them to a handled action would make `handleKey`
|
|
308
|
+
* answer true, which preventDefaults the keydown, which cancels the paste
|
|
309
|
+
* event, which is the only thing carrying `clipboardData.files`. The
|
|
310
|
+
* capability would disappear and nothing would say so.
|
|
311
|
+
*
|
|
312
|
+
* So the document handlers in StoryboardView.tsx do the work, and these rows exist
|
|
313
|
+
* only so the three combos are documented and `shortcutFor` can print them in
|
|
314
|
+
* a menu. `ShortcutRow.action` is required, so a documented combo must
|
|
315
|
+
* resolve to something; resolving to an arm that returns `false` keeps
|
|
316
|
+
* behaviour byte-identical.
|
|
317
|
+
*
|
|
318
|
+
* A test asserts the arm returns false. It fails the moment someone tidies
|
|
319
|
+
* this to `true`, which is the point.
|
|
320
|
+
*/
|
|
321
|
+
| { kind: "storyboardClipboard" }
|
|
322
|
+
// ── THE MATERIAL GRAPH'S OWN LANE ────────────────────────────────────────
|
|
323
|
+
//
|
|
324
|
+
// Every one of these is prefixed `graph` and every one of them is produced
|
|
325
|
+
// ONLY with `materialGraph: true`. That is not decoration: `graphDelete` and
|
|
326
|
+
// `deleteSelection` are the same key over two different subjects, and a lane
|
|
327
|
+
// that answered the layer verb inside the canvas would delete the MESH the
|
|
328
|
+
// author is painting rather than the node under their cursor.
|
|
329
|
+
/** Open the node palette. */
|
|
330
|
+
| { kind: "graphPalette" }
|
|
331
|
+
/** Remove the selected nodes and wires. */
|
|
332
|
+
| { kind: "graphDelete" }
|
|
333
|
+
| { kind: "graphDuplicate" }
|
|
334
|
+
| { kind: "graphCopy" }
|
|
335
|
+
| { kind: "graphCut" }
|
|
336
|
+
| { kind: "graphPaste" }
|
|
337
|
+
| { kind: "graphSelectAll" }
|
|
338
|
+
/** Box the selection, and take it back out again. */
|
|
339
|
+
| { kind: "graphGroup" }
|
|
340
|
+
| { kind: "graphUngroup" }
|
|
341
|
+
/** Go INTO the selected Subgraph node's function body. */
|
|
342
|
+
| { kind: "graphEnter" }
|
|
343
|
+
/** Show or hide the live thumbnail on the selected cards. */
|
|
344
|
+
| { kind: "graphPreviewToggle" }
|
|
345
|
+
/** Fit the whole graph in the canvas. */
|
|
346
|
+
| { kind: "graphFrame" }
|
|
347
|
+
| { kind: "graphTidy" }
|
|
348
|
+
/** Move the selected nodes, in GRAPH px. */
|
|
349
|
+
| { kind: "graphNudge"; dx: number; dy: number }
|
|
350
|
+
/** Escape, whose meaning inside the workspace is a four-rung ladder the
|
|
351
|
+
* STORE owns — the palette, then the selection, then one function level,
|
|
352
|
+
* then the workspace itself. The table cannot answer it, because three of
|
|
353
|
+
* the four rungs are state this file deliberately does not take. */
|
|
354
|
+
| { kind: "graphEscape" };
|
|
145
355
|
|
|
146
356
|
/** What the key event lands in. Only what actually changes the ANSWER — a
|
|
147
357
|
* keymap that took the whole store would be a keymap nobody could test. */
|
|
@@ -149,6 +359,31 @@ export interface KeyContext {
|
|
|
149
359
|
/** The event target is a field, a textarea or a contenteditable. */
|
|
150
360
|
editing: boolean;
|
|
151
361
|
hasSelection: boolean;
|
|
362
|
+
/**
|
|
363
|
+
* At least one KEYFRAME is selected.
|
|
364
|
+
*
|
|
365
|
+
* A subject of its own, above the layer selection on the rungs that both
|
|
366
|
+
* could claim: the arrows nudge keys when there are keys, and layers
|
|
367
|
+
* otherwise. That is `deleteSelection`'s own most-specific-subject-wins
|
|
368
|
+
* logic, applied to the other gesture that has two possible targets.
|
|
369
|
+
*
|
|
370
|
+
* Absent counts as false, so every caller that predates keyframes keeps
|
|
371
|
+
* today's answers. */
|
|
372
|
+
hasKeyframeSelection?: boolean;
|
|
373
|
+
/** Sound mode has clips picked (`selectedAudio`): Q / W, Escape and Delete
|
|
374
|
+
* answer to them. */
|
|
375
|
+
hasAudioSelection?: boolean;
|
|
376
|
+
/** Something is on the keyframe clipboard. Gates ⌘V so that with an empty
|
|
377
|
+
* clipboard the key falls through to the browser rather than being
|
|
378
|
+
* swallowed by a verb with nothing to do. Absent = false. */
|
|
379
|
+
hasKeyframeClip?: boolean;
|
|
380
|
+
/** Something is on the layer clip, which gates ⌘V the same way. Absent =
|
|
381
|
+
* false. */
|
|
382
|
+
hasLayerClip?: boolean;
|
|
383
|
+
/** An animation, or a layer's effects, is on the motion clip. Gates ⌥⌘V and
|
|
384
|
+
* ⇧⌥⌘V the way `hasLayerClip` gates ⌘V. Absent = false. */
|
|
385
|
+
hasAnimationClip?: boolean;
|
|
386
|
+
hasEffectsClip?: boolean;
|
|
152
387
|
/**
|
|
153
388
|
* The pen HOLDS AT LEAST ONE POINT — not "the path tool is armed".
|
|
154
389
|
*
|
|
@@ -166,19 +401,82 @@ export interface KeyContext {
|
|
|
166
401
|
/** The author is inside an isolated subtree. Only Escape's precedence reads
|
|
167
402
|
* it, and only once nothing is selected — see the lane. Absent = false. */
|
|
168
403
|
isolation?: boolean;
|
|
404
|
+
/** A component DEFINITION is open (the outline is scoped to its interior).
|
|
405
|
+
* A rung on Escape's ladder, so the gesture that opened a component context
|
|
406
|
+
* can close all of it. Absent = false. */
|
|
407
|
+
editingDef?: boolean;
|
|
408
|
+
/** Components was opened from a Shot (`componentsOrigin`), so Escape goes
|
|
409
|
+
* back to that Shot, frame and selection. Absent = false. */
|
|
410
|
+
componentsOrigin?: boolean;
|
|
411
|
+
/** THE STORYBOARD IS OPEN. Mode-level, unlike `materialGraph`: there is no
|
|
412
|
+
* working stage pane beside this canvas (the preview is a monitor that
|
|
413
|
+
* takes its own pointer), and a key pressed over the inspector must still
|
|
414
|
+
* delete a card. Absent = false. */
|
|
415
|
+
storyboard?: boolean;
|
|
416
|
+
/** A CODE component is open (its source beside its own render). The other
|
|
417
|
+
* family's rung, at the same height as the definition's because both put
|
|
418
|
+
* the stage in the component context and neither can nest in the other.
|
|
419
|
+
* Absent = false. */
|
|
420
|
+
codeComponent?: boolean;
|
|
421
|
+
/** Components is up with no component open (its empty state). Escape
|
|
422
|
+
* leaves it, at the subjects' height. Absent = false. */
|
|
423
|
+
componentsOpen?: boolean;
|
|
424
|
+
/** A SHOT is open (the author is authoring its elements rather than
|
|
425
|
+
* arranging shots). Escape's LAST rung, below the definition, because the
|
|
426
|
+
* component context nests inside the shot context and Escape climbs out one
|
|
427
|
+
* level per press. Absent = false. */
|
|
428
|
+
enteredScene?: boolean;
|
|
429
|
+
/**
|
|
430
|
+
* THE AUTHOR IS PILOTING THE SHOT CAMERA from the Camera pane.
|
|
431
|
+
*
|
|
432
|
+
* Escape's first rung of all: a mode the author armed most recently, and
|
|
433
|
+
* one whose badge promises "ESC TO EJECT". Video and pilot never coexist
|
|
434
|
+
* (entering Video ejects), so the order between the two rungs is moot.
|
|
435
|
+
* Absent = false.
|
|
436
|
+
*/
|
|
437
|
+
pilot?: boolean;
|
|
438
|
+
/**
|
|
439
|
+
* THE AUTHOR IS WATCHING: Video is on screen (`viewMode.ts`'s `videoView`).
|
|
440
|
+
*
|
|
441
|
+
* Escape's TOP rung, above the deselect, because the selection SURVIVES a
|
|
442
|
+
* screening: it is invisible in Video and comes back with the handles, so
|
|
443
|
+
* an author who watched their shot and pressed Escape means "put the editor
|
|
444
|
+
* back", not "drop what I had selected". Absent = false.
|
|
445
|
+
*/
|
|
446
|
+
video?: boolean;
|
|
169
447
|
/**
|
|
170
448
|
* The CURSOR IS OVER THE WORKING PANE and a modal transform could start.
|
|
171
449
|
*
|
|
172
450
|
* Stage-side callers only: the chrome has no cursor-over-pane answer, and
|
|
173
|
-
* passing nothing
|
|
174
|
-
* always has. Cursor position and not `activePane`, because a modal needs an
|
|
451
|
+
* passing nothing leaves G unbound from a panel. Cursor position and not `activePane`, because a modal needs an
|
|
175
452
|
* anchor point and activePane is only written on press/wheel — the mouse can
|
|
176
453
|
* hover one pane while the other is active. Absent = false.
|
|
177
454
|
*/
|
|
178
455
|
workingPane?: boolean;
|
|
456
|
+
/**
|
|
457
|
+
* The working pane under the cursor is also the FRAME being edited: an open
|
|
458
|
+
* definition's one pane (StageApp's `framePane`). A letter a tool claims
|
|
459
|
+
* keeps its tool there, as it does over the camera pane, so R arms Rect
|
|
460
|
+
* while G and S keep their modals. Stage-side only, like `workingPane`.
|
|
461
|
+
* Absent = false.
|
|
462
|
+
*/
|
|
463
|
+
framePane?: boolean;
|
|
179
464
|
/** A modal transform is LIVE. Owns its terminator and axis keys, the way
|
|
180
465
|
* `penActive` owns Enter/Escape. Absent = false. */
|
|
181
466
|
transformModal?: boolean;
|
|
467
|
+
/**
|
|
468
|
+
* THE KEY LANDED INSIDE THE MATERIAL GRAPH CANVAS.
|
|
469
|
+
*
|
|
470
|
+
* Measured from the event's target (`closest(".material-graph-canvas")`) and
|
|
471
|
+
* not from "the workspace is open", which is the distinction the whole lane
|
|
472
|
+
* rests on: the workspace shows a stage pane BESIDE the graph, and a key
|
|
473
|
+
* pressed over that pane still aims the working view. Only the canvas itself
|
|
474
|
+
* owns the graph verbs.
|
|
475
|
+
*
|
|
476
|
+
* Absent = false, so every caller and probe that predates the lane keeps
|
|
477
|
+
* today's answers.
|
|
478
|
+
*/
|
|
479
|
+
materialGraph?: boolean;
|
|
182
480
|
}
|
|
183
481
|
|
|
184
482
|
export interface KeyEventLike {
|
|
@@ -187,9 +485,16 @@ export interface KeyEventLike {
|
|
|
187
485
|
altKey: boolean;
|
|
188
486
|
ctrlKey: boolean;
|
|
189
487
|
metaKey: boolean;
|
|
190
|
-
/** `KeyboardEvent.code
|
|
191
|
-
*
|
|
488
|
+
/** `KeyboardEvent.code`: the numpad lane, the shifted layout digits, and
|
|
489
|
+
* the chord letters when `key` is composed (see `chordChar`). Optional so
|
|
490
|
+
* every caller and probe that predates it keeps today's answers; absent =
|
|
491
|
+
* no numpad, and no shifted-digit layout key. */
|
|
192
492
|
code?: string;
|
|
493
|
+
/** `KeyboardEvent.repeat`: the key is held and this keydown is the OS
|
|
494
|
+
* repeating it. The table answers a repeat as it answers the press; the
|
|
495
|
+
* play toggle ignores it, so a held Space toggles once (the store's
|
|
496
|
+
* `handleKey`). Absent = a press. */
|
|
497
|
+
repeat?: boolean;
|
|
193
498
|
}
|
|
194
499
|
|
|
195
500
|
/** How far a plain arrow moves a layer, and how far a shifted one does. Design
|
|
@@ -197,21 +502,89 @@ export interface KeyEventLike {
|
|
|
197
502
|
* the view would be a different edit depending on how far you were zoomed in. */
|
|
198
503
|
export const NUDGE_PX = 1;
|
|
199
504
|
export const NUDGE_COARSE_PX = 10;
|
|
505
|
+
/** Shift's multiplier on a keyframe nudge, in FRAMES. Ten, the same relationship
|
|
506
|
+
* the layer nudge has — but frames rather than px, because a keyframe moves in
|
|
507
|
+
* time and a design pixel means nothing on that axis. */
|
|
508
|
+
export const NUDGE_COARSE_FRAMES = 10;
|
|
200
509
|
|
|
201
510
|
/**
|
|
202
|
-
*
|
|
511
|
+
* …and the same pair for the GRAPH canvas, which is a different space.
|
|
203
512
|
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
513
|
+
* A node lives in canvas px, where one px is invisible: the cards are ~180 px
|
|
514
|
+
* wide and the grid they land on is 16. So the fine step is half a grid cell,
|
|
515
|
+
* which is the smallest nudge that reads as a move, and the coarse one is ten
|
|
516
|
+
* of those — the same 10x relationship the layer nudge has, so the modifier
|
|
517
|
+
* means one thing in both places.
|
|
209
518
|
*/
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
519
|
+
export const GRAPH_NUDGE_PX = 8;
|
|
520
|
+
export const GRAPH_NUDGE_COARSE_PX = 80;
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* The Project context's refusal for anything that acts on elements, in one
|
|
524
|
+
* sentence: a tool "draws" them, a rail button that inserts without a drag
|
|
525
|
+
* "adds" them. The keys and the board's tool rail both say it this way.
|
|
526
|
+
*/
|
|
527
|
+
export function sceneContextRefusal(label: string, verb: string): string {
|
|
528
|
+
return `${label} ${verb} elements, and elements live inside a shot. Open a shot from the timeline to add one to it.`;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Why this tool's key does nothing in this layout, or null when it works.
|
|
533
|
+
*
|
|
534
|
+
* The seven creating tools draw on the frame, which over a Shot is the CAMERA
|
|
535
|
+
* pane and nowhere else (see `isInsertTool`), so in the working-only layout
|
|
536
|
+
* their letters armed a tool that no press could reach. That is worse than a
|
|
537
|
+
* dead key: with the tool armed, a press in the working pane falls through the
|
|
538
|
+
* stage's generic lanes and ORBITS the view, deselects, or drags whatever was
|
|
539
|
+
* selected, so the author sees the tool do the wrong thing rather than
|
|
540
|
+
* nothing, and has no reason to suspect the layout.
|
|
541
|
+
*
|
|
542
|
+
* `mode` is the layout the tools are judged against, which every caller reads
|
|
543
|
+
* through `toolLayoutMode`: inside a definition the stage's one pane is both
|
|
544
|
+
* the frame and the 3D view, so every tool's pane is on screen.
|
|
545
|
+
*
|
|
546
|
+
* A MESSAGE rather than a boolean, and pure: the store owns `state.error` (this
|
|
547
|
+
* is `noWorkingPane`'s job the other way round, and it names the digit to press
|
|
548
|
+
* for the same reason), and the tool dock puts the identical sentence in the
|
|
549
|
+
* disabled button's title — so what the key says and what the button says about
|
|
550
|
+
* where a tool works cannot drift apart.
|
|
551
|
+
*/
|
|
552
|
+
export function toolRefusal(
|
|
553
|
+
tool: ShotTool,
|
|
554
|
+
mode: StageLayout["mode"],
|
|
555
|
+
/** The author is in the SCENE context — arranging shots, not authoring the
|
|
556
|
+
* elements of one. Absent = false, so every caller that predates the two
|
|
557
|
+
* contexts keeps today's answers. */
|
|
558
|
+
sceneContext = false,
|
|
559
|
+
/** The Project is view only for this person: true, or the sentence that
|
|
560
|
+
* says why (`readOnlyReason`). Absent = false. */
|
|
561
|
+
readOnly: boolean | string = false,
|
|
562
|
+
/** Why the isolated layer cannot take an insert (`isolationInsertRefusal`),
|
|
563
|
+
* or null. True in every layout, so it outranks the pane answer. */
|
|
564
|
+
isolation: string | null = null,
|
|
565
|
+
): string | null {
|
|
566
|
+
if (!isInsertTool(tool)) return null;
|
|
567
|
+
// Every creating tool writes the document, so a viewer has none of them.
|
|
568
|
+
if (readOnly !== false) return readOnly === true ? VIEW_ONLY : readOnly;
|
|
569
|
+
// THE CONTEXT ANSWER COMES FIRST, because it is the one that is true whatever
|
|
570
|
+
// the layout is: elements live inside a shot, so no layout up here makes a
|
|
571
|
+
// creating tool legal, and a layout sentence would send the author to a
|
|
572
|
+
// digit that would not help.
|
|
573
|
+
if (sceneContext) return sceneContextRefusal(specOf(tool).label, "draws");
|
|
574
|
+
if (isolation !== null) return isolation;
|
|
575
|
+
const pane = specOf(tool).pane;
|
|
576
|
+
// The lead clause is this tool's own; the tail is the one every "this layout
|
|
577
|
+
// cannot do that" refusal shares (`paneOnlyRefusal`), so the digits it names
|
|
578
|
+
// follow a rebind and the words follow a rename with no edit here. Both arms
|
|
579
|
+
// have narrowed `mode` to a single pane by the time they ask.
|
|
580
|
+
if (pane === "camera" && mode === "working") {
|
|
581
|
+
return formatCombo(`${specOf(tool).label} draws in ${layoutSpec(pane).noun}, and ${paneOnlyRefusal(mode)}`);
|
|
582
|
+
}
|
|
583
|
+
if (pane === "working" && mode === "camera") {
|
|
584
|
+
return formatCombo(`${specOf(tool).label} draws in ${layoutSpec(pane).noun}, and ${paneOnlyRefusal(mode)}`);
|
|
585
|
+
}
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
215
588
|
|
|
216
589
|
const ARROWS: Readonly<Record<string, readonly [number, number]>> = {
|
|
217
590
|
ArrowLeft: [-1, 0],
|
|
@@ -220,7 +593,72 @@ const ARROWS: Readonly<Record<string, readonly [number, number]>> = {
|
|
|
220
593
|
ArrowDown: [0, 1],
|
|
221
594
|
};
|
|
222
595
|
|
|
223
|
-
|
|
596
|
+
/**
|
|
597
|
+
* ARRANGE ON THE BRACKET KEYS, Figma's chords (founder decision 3, critic 5).
|
|
598
|
+
*
|
|
599
|
+
* ⌘] / Ctrl+] brings the selection forward one sibling and ⌘[ / Ctrl+[ sends
|
|
600
|
+
* it backward, on every platform. To the FRONT and BACK the chord splits by
|
|
601
|
+
* platform, as Figma's does: Ctrl+⇧ on Windows and Linux, ⌥⌘ on a Mac. ⌘⇧]
|
|
602
|
+
* and ⌘⇧[ are the Mac's next and previous TAB, so they are never claimed: they
|
|
603
|
+
* answer null and the browser keeps them. The split reads the modifier itself
|
|
604
|
+
* (meta is a Mac's command key, and Windows never delivers its own) rather
|
|
605
|
+
* than the platform, so this table stays a pure function of the event.
|
|
606
|
+
*
|
|
607
|
+
* Keyed on `event.code`, never `key`: Shift makes `]` a `}` and Option makes it
|
|
608
|
+
* a quote on a Mac, so the character is no witness to the key. ⌘[ and ⌘] ARE
|
|
609
|
+
* taken from Chrome's Back and Forward on a Mac, as Figma takes them, and
|
|
610
|
+
* `handleKey` claims them like ⌘K. The bare brackets stay the panel toggles,
|
|
611
|
+
* in lane 3.
|
|
612
|
+
*/
|
|
613
|
+
function arrangeChord(event: KeyEventLike): KeyAction | null {
|
|
614
|
+
const side =
|
|
615
|
+
event.code === "BracketRight" ? 1 : event.code === "BracketLeft" ? -1 : 0;
|
|
616
|
+
if (side === 0) return null;
|
|
617
|
+
const { shiftKey: shift, altKey: alt, ctrlKey: ctrl, metaKey: meta } = event;
|
|
618
|
+
if (!shift && !alt)
|
|
619
|
+
return { kind: "arrange", to: side > 0 ? "forward" : "backward" };
|
|
620
|
+
const macFront = meta && alt && !shift && !ctrl;
|
|
621
|
+
const pcFront = ctrl && shift && !alt && !meta;
|
|
622
|
+
if (macFront || pcFront)
|
|
623
|
+
return { kind: "arrange", to: side > 0 ? "front" : "back" };
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* An Arrange chord over a canvas with nothing to arrange (the storyboard, the
|
|
629
|
+
* material graph, lanes 1e and 1f): CLAIMED and dropped, so ⌘[ and ⌘] are not
|
|
630
|
+
* the browser's Back and Forward there either. A character lane 2a keeps for
|
|
631
|
+
* itself before it asks `arrangeChord` (a letter, a digit, the zoom keys: a
|
|
632
|
+
* layout can put `+` or `=` on a bracket key) is left to the browser, as every
|
|
633
|
+
* combo those lanes do not bind.
|
|
634
|
+
*/
|
|
635
|
+
function arrangeNowhere(event: KeyEventLike, lower: string): KeyAction | null {
|
|
636
|
+
if (/^[a-z0-9=+-]$/.test(lower)) return null;
|
|
637
|
+
return arrangeChord(event) === null ? null : { kind: "arrangeNowhere" };
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* The letter or digit a ctrl or alt chord names.
|
|
642
|
+
*
|
|
643
|
+
* A plain one in `key` wins, so a non-QWERTY layout keeps its own letters
|
|
644
|
+
* (AZERTY's ⌘A is the key printed A). When `key` is NOT a plain letter or digit
|
|
645
|
+
* the physical key answers instead: a Mac's Option composes it ("ç" for ⌥⌘C,
|
|
646
|
+
* "¬" for ⌥L, a dead key for ⌥I), Shift turns a digit into punctuation, and a
|
|
647
|
+
* Cyrillic layout reports "я" on the key every shortcut chart calls Z.
|
|
648
|
+
* Anything else (`=`, `+`, `-`, a bracket) comes back as it was typed.
|
|
649
|
+
*/
|
|
650
|
+
function chordChar(event: KeyEventLike, lower: string): string {
|
|
651
|
+
if (/^[a-z0-9]$/.test(lower)) return lower;
|
|
652
|
+
const code = event.code ?? "";
|
|
653
|
+
if (/^Key[A-Z]$/.test(code)) return code.slice(3).toLowerCase();
|
|
654
|
+
if (/^Digit[0-9]$/.test(code)) return code.slice(5);
|
|
655
|
+
return lower;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
export function resolveKey(
|
|
659
|
+
event: KeyEventLike,
|
|
660
|
+
context: KeyContext,
|
|
661
|
+
): KeyAction | null {
|
|
224
662
|
// 1. A field owns its own keystrokes. Every editor that has ever swallowed
|
|
225
663
|
// Space while the author was naming something shipped that bug by checking
|
|
226
664
|
// the KEY before the TARGET.
|
|
@@ -230,6 +668,21 @@ export function resolveKey(event: KeyEventLike, context: KeyContext): KeyAction
|
|
|
230
668
|
const lower = key.length === 1 ? key.toLowerCase() : key;
|
|
231
669
|
const mod = event.metaKey || event.ctrlKey;
|
|
232
670
|
|
|
671
|
+
// 1a. WINDOWS ALTGR IS NOT A CHORD. AltGr arrives as ctrlKey + altKey, so a
|
|
672
|
+
// Polish author typing "ć" or a German one typing "@" or "€" would
|
|
673
|
+
// otherwise reach the ctrl lane and fire whatever the physical key's
|
|
674
|
+
// letter is bound to. A composed character (one that is not a plain
|
|
675
|
+
// letter or digit) is typing, never a shortcut. A Mac's ⌥⌘ carries
|
|
676
|
+
// metaKey and is a real chord, so it passes.
|
|
677
|
+
if (
|
|
678
|
+
event.ctrlKey &&
|
|
679
|
+
event.altKey &&
|
|
680
|
+
!event.metaKey &&
|
|
681
|
+
key.length === 1 &&
|
|
682
|
+
!/^[a-z0-9]$/i.test(key)
|
|
683
|
+
)
|
|
684
|
+
return null;
|
|
685
|
+
|
|
233
686
|
// 1b. A PATH IN PROGRESS owns Enter and Escape, and only those two.
|
|
234
687
|
//
|
|
235
688
|
// Above every other lane because both keys already mean something here
|
|
@@ -244,9 +697,14 @@ export function resolveKey(event: KeyEventLike, context: KeyContext): KeyAction
|
|
|
244
697
|
|
|
245
698
|
// 1c. A LIVE MODAL TRANSFORM owns everything bare.
|
|
246
699
|
//
|
|
247
|
-
// Enter/Escape terminate it and x/y/z constrain it
|
|
248
|
-
//
|
|
249
|
-
//
|
|
700
|
+
// Enter/Escape terminate it and x/y/z constrain it. SHIFT IS PART OF THE
|
|
701
|
+
// CONSTRAINT: `⇧X` locks to the PLANE normal to x — Blender's own
|
|
702
|
+
// binding, and the one thing a keyboard-only author could not say here
|
|
703
|
+
// (the drawn gizmo grew plane pads for it; this is the same constraint on
|
|
704
|
+
// the keys). It used to fall through as "still the z axis, not fit",
|
|
705
|
+
// which was right while there was no plane to mean instead. Shift keeps
|
|
706
|
+
// its precision job on MOTION, which is a different event entirely.
|
|
707
|
+
// Every OTHER bare key answers null so no tool can arm
|
|
250
708
|
// and no panel can toggle under an open scrub. Modifier combos fall
|
|
251
709
|
// through on purpose: the table stays honest about what ⌘Z means — the
|
|
252
710
|
// stage's capture listener is the one that swallows it while the modal
|
|
@@ -255,25 +713,28 @@ export function resolveKey(event: KeyEventLike, context: KeyContext): KeyAction
|
|
|
255
713
|
//
|
|
256
714
|
// ALT FALLS THROUGH, and `mod` alone does not cover it: `mod` is
|
|
257
715
|
// ctrl/meta, so ⌥G reached this lane and RESTARTED the modal instead of
|
|
258
|
-
// reaching lane 2b's `clearTransform` (Blender's ⌥G / ⌥R
|
|
716
|
+
// reaching lane 2b's `clearTransform` (Blender's ⌥G / ⌥R, and ⌥⇧S, "clear
|
|
259
717
|
// this channel"), and ⌥X set an axis. The alt bindings are a different
|
|
260
718
|
// verb that happens to share a letter, so the lane has to let them past.
|
|
261
719
|
if (context.transformModal === true && !mod && !event.altKey) {
|
|
262
720
|
if (key === "Enter") return { kind: "modalConfirm" };
|
|
263
721
|
if (key === "Escape") return { kind: "modalCancel" };
|
|
264
|
-
if (lower === "x")
|
|
265
|
-
|
|
266
|
-
if (lower === "
|
|
722
|
+
if (lower === "x")
|
|
723
|
+
return { kind: "modalAxis", axis: "x", plane: event.shiftKey === true };
|
|
724
|
+
if (lower === "y")
|
|
725
|
+
return { kind: "modalAxis", axis: "y", plane: event.shiftKey === true };
|
|
726
|
+
if (lower === "z")
|
|
727
|
+
return { kind: "modalAxis", axis: "z", plane: event.shiftKey === true };
|
|
267
728
|
// SWITCHING TRANSFORM MID-GESTURE, which in Blender feels like a mode
|
|
268
729
|
// change rather than a restart: `G` then `R` keeps the anchor the author
|
|
269
730
|
// started from, so the rotation is measured from where they pressed and not
|
|
270
731
|
// from wherever the pointer had wandered to.
|
|
271
732
|
if (lower === "g") return { kind: "modalStart", mode: "move" };
|
|
272
|
-
// `R` NEEDS
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
if (lower === "r"
|
|
733
|
+
// `R` NEEDS NO SELECTION ANY MORE: the camera has a modal roll, so
|
|
734
|
+
// switching to rotate mid-gesture is symmetric with `G` and `S` whichever
|
|
735
|
+
// subject the modal is on. `framingDeltaFor`'s rotate arm is what it emits
|
|
736
|
+
// into, and `axisRefusal` names the two axes a framing cannot turn about.
|
|
737
|
+
if (lower === "r") return { kind: "modalStart", mode: "rotate" };
|
|
277
738
|
if (lower === "s") return { kind: "modalStart", mode: "scale" };
|
|
278
739
|
// NUMERIC ENTRY: `G X 200 Enter`, which is the single biggest thing a
|
|
279
740
|
// Blender user's fingers reach for and finds missing. It goes through the
|
|
@@ -330,20 +791,30 @@ export function resolveKey(event: KeyEventLike, context: KeyContext): KeyAction
|
|
|
330
791
|
Numpad2: { dx: 0, dy: 1 },
|
|
331
792
|
};
|
|
332
793
|
const one = step[event.code]!;
|
|
333
|
-
return {
|
|
794
|
+
return {
|
|
795
|
+
kind: "orbitStep",
|
|
796
|
+
dx: one.dx * ORBIT_STEP_DEG,
|
|
797
|
+
dy: one.dy * ORBIT_STEP_DEG,
|
|
798
|
+
};
|
|
334
799
|
}
|
|
335
800
|
case "Numpad0":
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
// twice
|
|
339
|
-
|
|
801
|
+
// PILOT, and back: Blender's numpad-0 looks through the camera, and
|
|
802
|
+
// ours puts the author's hands on it in the Camera pane. A toggle, so
|
|
803
|
+
// pressing it twice leaves the camera where it is and the hands off.
|
|
804
|
+
// The layouts keep ⇧1 / ⇧2 / ⇧3; the store brings the Camera pane on
|
|
805
|
+
// screen if the 3D layout has none.
|
|
806
|
+
if (!mod) return { kind: "pilotToggle" };
|
|
340
807
|
break;
|
|
341
808
|
case "NumpadAdd":
|
|
342
809
|
case "NumpadSubtract":
|
|
343
810
|
// The existing zoom verb, which already routes to the active pane —
|
|
344
811
|
// so it dollies the working camera and zooms the flat one, correctly,
|
|
345
812
|
// with no new action.
|
|
346
|
-
if (!mod)
|
|
813
|
+
if (!mod)
|
|
814
|
+
return {
|
|
815
|
+
kind: "zoom",
|
|
816
|
+
direction: event.code === "NumpadAdd" ? 1 : -1,
|
|
817
|
+
};
|
|
347
818
|
break;
|
|
348
819
|
case "NumpadDivide":
|
|
349
820
|
// LITERALLY Blender's local view. Ours is isolation, and the two mean
|
|
@@ -371,96 +842,353 @@ export function resolveKey(event: KeyEventLike, context: KeyContext): KeyAction
|
|
|
371
842
|
if (!mod && key.length === 1) return null;
|
|
372
843
|
}
|
|
373
844
|
|
|
845
|
+
// 1e. THE MATERIAL GRAPH CANVAS owns its own vocabulary.
|
|
846
|
+
//
|
|
847
|
+
// Above the modifier lanes because ⌘D, ⌘G and ⌘A all mean something else
|
|
848
|
+
// in here, and BELOW the field lane because a node's inline number field
|
|
849
|
+
// is still a field. The lane answers `"pass"` for exactly the keys that
|
|
850
|
+
// have to keep meaning what they mean everywhere — undo, redo, save, the
|
|
851
|
+
// app menu, and the space bar — and `null` for everything it does not
|
|
852
|
+
// claim, which is what keeps a bare tool letter from arming the rect tool
|
|
853
|
+
// under a graph nobody could draw a rectangle in.
|
|
854
|
+
if (context.materialGraph === true) {
|
|
855
|
+
const graph = materialGraphKey(event, key, lower, mod);
|
|
856
|
+
if (graph !== "pass") return graph;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// 1f. THE STORYBOARD holds the canvas while it is open, on the graph lane's
|
|
860
|
+
// terms: pass what must keep meaning what it means everywhere, blank the
|
|
861
|
+
// rest, because nothing under the board can take a tool letter.
|
|
862
|
+
if (context.storyboard === true) {
|
|
863
|
+
const board = storyboardKey(event, key, lower, mod);
|
|
864
|
+
if (board !== "pass") return board;
|
|
865
|
+
}
|
|
866
|
+
|
|
374
867
|
// 2a. ctrl / meta.
|
|
868
|
+
//
|
|
869
|
+
// EVERY CASE NAMES THE MODIFIERS IT OWNS, and a combo it does not own
|
|
870
|
+
// answers null. The lane used to switch on the letter alone, so ⌘⇧C
|
|
871
|
+
// copied, ⌥⌘S saved and Ctrl+Alt+D duplicated: every extra modifier was
|
|
872
|
+
// a second spelling of the bare chord, and no chord with one could ever
|
|
873
|
+
// be bound to anything else. A refused combo is null and not
|
|
874
|
+
// `arrangeChord`: a letter is never a bracket.
|
|
375
875
|
if (mod) {
|
|
376
|
-
|
|
876
|
+
const shift = event.shiftKey;
|
|
877
|
+
const alt = event.altKey;
|
|
878
|
+
const plain = !shift && !alt;
|
|
879
|
+
switch (chordChar(event, lower)) {
|
|
377
880
|
case "z":
|
|
378
|
-
|
|
881
|
+
if (alt) return null;
|
|
882
|
+
return shift ? { kind: "redo" } : { kind: "undo" };
|
|
379
883
|
case "y":
|
|
380
|
-
return { kind: "redo" };
|
|
884
|
+
return plain ? { kind: "redo" } : null;
|
|
885
|
+
// ── KEYFRAME COPY / PASTE, AND ONLY WHEN THERE IS ONE ─────────────
|
|
886
|
+
//
|
|
887
|
+
// Both fall through to the BROWSER when their subject is absent, which
|
|
888
|
+
// is why they answer `null` rather than a no-op verb: ⌘C with nothing
|
|
889
|
+
// keyframed must still copy whatever the page has selected, and a
|
|
890
|
+
// chrome that swallowed it would break the one shortcut every user
|
|
891
|
+
// trusts. The field lane returns above this, so text copy is untouched
|
|
892
|
+
// either way. Keys first, then layers, the most specific subject the way
|
|
893
|
+
// `deleteSelection` reads it. The store keeps one clip: a copy of either
|
|
894
|
+
// clears the other, so at most one of the two paste bits is set.
|
|
895
|
+
// ── THE MOTION CLIP ON ⌥ ──────────────────────────────────────────
|
|
896
|
+
//
|
|
897
|
+
// ⌥⌘C / ⌥⌘V carry a layer's animation and ⇧⌥⌘C / ⇧⌥⌘V its effects. Each
|
|
898
|
+
// needs a selected layer (and a paste a clip of its kind), else it is the
|
|
899
|
+
// browser's, the same rule as the plain pair below.
|
|
900
|
+
case "c":
|
|
901
|
+
if (alt) {
|
|
902
|
+
if (!context.hasSelection) return null;
|
|
903
|
+
return shift ? { kind: "copyEffects" } : { kind: "copyAnimation" };
|
|
904
|
+
}
|
|
905
|
+
if (!plain) return null;
|
|
906
|
+
if (context.hasKeyframeSelection === true) return { kind: "copyKeyframes" };
|
|
907
|
+
return context.hasSelection ? { kind: "copyLayers" } : null;
|
|
908
|
+
case "x":
|
|
909
|
+
if (!plain) return null;
|
|
910
|
+
if (context.hasKeyframeSelection === true) return null;
|
|
911
|
+
return context.hasSelection ? { kind: "cutLayers" } : null;
|
|
912
|
+
case "v":
|
|
913
|
+
if (alt) {
|
|
914
|
+
if (!context.hasSelection) return null;
|
|
915
|
+
if (shift) return context.hasEffectsClip === true ? { kind: "pasteEffects" } : null;
|
|
916
|
+
return context.hasAnimationClip === true ? { kind: "pasteAnimation" } : null;
|
|
917
|
+
}
|
|
918
|
+
if (!plain) return null;
|
|
919
|
+
if (context.hasKeyframeClip === true) return { kind: "pasteKeyframes" };
|
|
920
|
+
return context.hasLayerClip === true ? { kind: "pasteLayers" } : null;
|
|
381
921
|
case "s":
|
|
382
|
-
return { kind: "save" };
|
|
922
|
+
return plain ? { kind: "save" } : null;
|
|
383
923
|
case "d":
|
|
384
|
-
return { kind: "duplicate" };
|
|
924
|
+
return plain ? { kind: "duplicate" } : null;
|
|
385
925
|
case "e":
|
|
386
926
|
// Export. The one destination-producing verb in the whole chrome, so
|
|
387
927
|
// it gets the letter rather than a chord.
|
|
388
|
-
return { kind: "openExport" };
|
|
928
|
+
return plain ? { kind: "openExport" } : null;
|
|
389
929
|
case "a":
|
|
390
|
-
|
|
930
|
+
// ⌘A selects all and ⌘⇧A drops it, the pair every design tool ships.
|
|
931
|
+
// ⌥A was the deselect until the alt lane was cleared for new verbs.
|
|
932
|
+
if (alt) return null;
|
|
933
|
+
return shift ? { kind: "deselect" } : { kind: "selectAll" };
|
|
934
|
+
case "h":
|
|
935
|
+
// ⌘⇧H shows everything again. The reveal moved here off ⌥H, which is
|
|
936
|
+
// left free; ⌘H alone is the Mac's "hide the app" and is never claimed.
|
|
937
|
+
return shift && !alt ? { kind: "setHidden", scope: "reveal" } : null;
|
|
938
|
+
case ",":
|
|
939
|
+
return plain ? { kind: "openPrefs" } : null;
|
|
940
|
+
case "k":
|
|
941
|
+
// The app menu, with its search focused — the ⌘K every tool now ships.
|
|
942
|
+
// It CLAIMS Chrome's own ⌘K (the address bar's search shortcut), which is
|
|
943
|
+
// deliberate and is why `handleKey` answers true: the alternative is a
|
|
944
|
+
// menu whose advertised key opens the browser instead.
|
|
945
|
+
return plain ? { kind: "appMenu" } : null;
|
|
391
946
|
case "b":
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
|
|
947
|
+
// THE TWO SPLITTERS, and the only ones now that the cut tool is gone —
|
|
948
|
+
// see the header. ⇧ picks the bigger subject, the same way it does on
|
|
949
|
+
// ⌘G / ⌘⇧G: the bare key cuts inside the shot and the shifted one cuts
|
|
950
|
+
// the shot. Nothing else claims ⌘⇧B, so the shift has to be read
|
|
951
|
+
// HERE or a shot split would silently resolve as a razor.
|
|
952
|
+
if (alt) return null;
|
|
953
|
+
return shift ? { kind: "splitSceneAtPlayhead" } : { kind: "splitAtPlayhead" };
|
|
395
954
|
case "g":
|
|
396
|
-
|
|
955
|
+
if (alt) return null;
|
|
956
|
+
return shift ? { kind: "ungroup" } : { kind: "group" };
|
|
397
957
|
case "0":
|
|
398
|
-
return { kind: "zoomReset" };
|
|
958
|
+
return plain ? { kind: "zoomReset" } : null;
|
|
959
|
+
// The zoom keys own Shift: "+" IS a shifted "=" on most layouts.
|
|
399
960
|
case "=":
|
|
400
961
|
case "+":
|
|
401
|
-
return { kind: "zoom", direction: 1 };
|
|
962
|
+
return alt ? null : { kind: "zoom", direction: 1 };
|
|
402
963
|
case "-":
|
|
403
|
-
return { kind: "zoom", direction: -1 };
|
|
964
|
+
return alt ? null : { kind: "zoom", direction: -1 };
|
|
404
965
|
default:
|
|
405
966
|
// An unclaimed modifier combo belongs to the BROWSER (⌘T, ⌘W, ⌘R).
|
|
406
967
|
// Falling through to the letter lane here would arm a tool on ⌘R and
|
|
407
|
-
// reload the page at the same time.
|
|
408
|
-
|
|
968
|
+
// reload the page at the same time. The one exception is Arrange's
|
|
969
|
+
// bracket chords, asked HERE, after every character above: a layout
|
|
970
|
+
// whose bracket key types a character this lane binds keeps that
|
|
971
|
+
// meaning (German `+` sits on BracketRight, and ⌘+ is zoom in).
|
|
972
|
+
return arrangeChord(event);
|
|
409
973
|
}
|
|
410
974
|
}
|
|
411
975
|
|
|
412
|
-
// 2b. alt.
|
|
976
|
+
// 2b. alt. A Mac's Option composes the key ("¬" for ⌥L, a dead key for ⌥I,
|
|
977
|
+
// "⁄" for ⇧⌥1), so the letter comes from `code` when `key` is not one
|
|
978
|
+
// (`chordChar`). Like lane 2a, every case names whether it owns Shift.
|
|
413
979
|
if (event.altKey) {
|
|
414
|
-
|
|
980
|
+
const shift = event.shiftKey;
|
|
981
|
+
const altKey = chordChar(event, lower);
|
|
982
|
+
switch (altKey) {
|
|
415
983
|
case "l":
|
|
416
|
-
return { kind: "toggleLock" };
|
|
984
|
+
return shift ? null : { kind: "toggleLock" };
|
|
417
985
|
case "h":
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
|
|
421
|
-
|
|
986
|
+
// ⌥⇧H HIDES EVERYTHING ELSE. It was ⇧H, which is cleared for a new
|
|
987
|
+
// verb. Bare ⌥H centres horizontally (the align block below).
|
|
988
|
+
return shift
|
|
989
|
+
? { kind: "setHidden", scope: "others" }
|
|
990
|
+
: { kind: "align", mode: "hcenter" };
|
|
991
|
+
// ── ALIGN, Figma's ⌥ letters ───────────────────────────────────────
|
|
992
|
+
//
|
|
993
|
+
// A and D are the left and right edges, W and S the top and bottom (the
|
|
994
|
+
// WASD block), H and V the two centres. Bare only: ⌥⇧S is the size
|
|
995
|
+
// clear and ⌥⇧H hides everything else.
|
|
422
996
|
case "a":
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
return { kind: "
|
|
997
|
+
return shift ? null : { kind: "align", mode: "left" };
|
|
998
|
+
case "d":
|
|
999
|
+
return shift ? null : { kind: "align", mode: "right" };
|
|
1000
|
+
case "w":
|
|
1001
|
+
return shift ? null : { kind: "align", mode: "top" };
|
|
1002
|
+
case "v":
|
|
1003
|
+
return shift ? null : { kind: "align", mode: "vcenter" };
|
|
426
1004
|
case "c":
|
|
427
1005
|
// ⌥C and not bare C, because the alt lane is where this chrome keeps
|
|
428
1006
|
// the verbs whose letter is already spoken for.
|
|
429
|
-
return { kind: "alignCameraToView" };
|
|
430
|
-
|
|
1007
|
+
return shift ? null : { kind: "alignCameraToView" };
|
|
1008
|
+
case "m":
|
|
1009
|
+
return shift ? null : { kind: "soundMode" };
|
|
1010
|
+
case "z":
|
|
1011
|
+
// ⌥Z: the grid floor. Z is the viewport-look letter (bare Z shading,
|
|
1012
|
+
// ⇧Z wireframe), and the grid is that family's overlay.
|
|
1013
|
+
return shift ? null : { kind: "gridToggle" };
|
|
1014
|
+
// ── THE KEYFRAME JUMPS, MOVED HERE OFF BARE J / K ──────────────────
|
|
1015
|
+
//
|
|
1016
|
+
// They held the bare letters until the shuttle arrived, and the shuttle
|
|
1017
|
+
// has the better claim: J and K are the transport's own keys in every
|
|
1018
|
+
// NLE an author comes here from, and a keyframe jump is the more local
|
|
1019
|
+
// verb of the two. So they take the alt lane, which is where this file
|
|
1020
|
+
// keeps the verbs whose letter is spoken for — the same move ⌥C and ⌥Z
|
|
1021
|
+
// already made.
|
|
1022
|
+
case "j":
|
|
1023
|
+
return shift ? null : { kind: "jumpKeyframe", dir: -1 };
|
|
1024
|
+
case "k":
|
|
1025
|
+
return shift ? null : { kind: "jumpKeyframe", dir: 1 };
|
|
1026
|
+
// ── THE WORK AREA: ⌥I, ⌥O, ⌥X ──────────────────────────────────────
|
|
1027
|
+
//
|
|
1028
|
+
// I and O are in and out in every editor that has the idea, and both
|
|
1029
|
+
// BARE letters are tools here (`i` is Media, `o` is Ellipse), so the alt
|
|
1030
|
+
// lane is where they land. ⌥X clears, because X is the axis letter and
|
|
1031
|
+
// has no meaning of its own outside a live modal.
|
|
1032
|
+
case "i":
|
|
1033
|
+
return shift ? null : { kind: "setWorkArea", edge: "in" };
|
|
1034
|
+
case "o":
|
|
1035
|
+
return shift ? null : { kind: "setWorkArea", edge: "out" };
|
|
1036
|
+
case "x":
|
|
1037
|
+
return shift ? null : { kind: "clearWorkArea" };
|
|
1038
|
+
// ── CLEAR TRANSFORM, Blender's ⌥G / ⌥R, and ⌥⇧S ───────────────────
|
|
431
1039
|
//
|
|
432
1040
|
// Back to the layer's own zero: position, rotation, or size. It is the
|
|
433
1041
|
// fastest way out of a transform that went wrong several edits ago, and
|
|
434
|
-
// undo cannot reach it once anything else has happened since.
|
|
1042
|
+
// undo cannot reach it once anything else has happened since. Size is
|
|
1043
|
+
// ⌥⇧S: bare ⌥S is align bottom.
|
|
435
1044
|
case "g":
|
|
436
|
-
return { kind: "clearTransform", channel: "offset" };
|
|
1045
|
+
return shift ? null : { kind: "clearTransform", channel: "offset" };
|
|
437
1046
|
case "r":
|
|
438
|
-
return { kind: "clearTransform", channel: "rotation" };
|
|
1047
|
+
return shift ? null : { kind: "clearTransform", channel: "rotation" };
|
|
439
1048
|
case "s":
|
|
440
|
-
return
|
|
1049
|
+
return shift
|
|
1050
|
+
? { kind: "clearTransform", channel: "scale" }
|
|
1051
|
+
: { kind: "align", mode: "bottom" };
|
|
1052
|
+
// ── AXIS VIEWS WITHOUT A NUMPAD, Blender's "Emulate Numpad" ────────
|
|
1053
|
+
//
|
|
1054
|
+
// The numpad lane above keys on `event.code.startsWith("Numpad")`, so a
|
|
1055
|
+
// laptop without one had NO KEYBOARD PATH to front, right or top — the
|
|
1056
|
+
// three views a 3D pane is most often asked for. Blender ships a
|
|
1057
|
+
// preference that maps the top-row digits; this chrome keeps the bare
|
|
1058
|
+
// digits free and puts the stage layouts on ⇧1 / ⇧2 / ⇧3. The alt lane
|
|
1059
|
+
// is where this file keeps the verbs whose letter is spoken for, so they
|
|
1060
|
+
// land here with the same digits, and ⇧ for the opposite side.
|
|
1061
|
+
case "1":
|
|
1062
|
+
return {
|
|
1063
|
+
kind: "workingViewPreset",
|
|
1064
|
+
preset: shift ? "back" : "front",
|
|
1065
|
+
};
|
|
1066
|
+
case "3":
|
|
1067
|
+
return {
|
|
1068
|
+
kind: "workingViewPreset",
|
|
1069
|
+
preset: shift ? "left" : "right",
|
|
1070
|
+
};
|
|
1071
|
+
case "7":
|
|
1072
|
+
return {
|
|
1073
|
+
kind: "workingViewPreset",
|
|
1074
|
+
preset: shift ? "bottom" : "top",
|
|
1075
|
+
};
|
|
1076
|
+
case "5":
|
|
1077
|
+
return shift ? null : { kind: "toggleWorkingOrtho" };
|
|
441
1078
|
default:
|
|
442
1079
|
return null;
|
|
443
1080
|
}
|
|
444
1081
|
}
|
|
445
1082
|
|
|
446
1083
|
// 3. Bare and shifted.
|
|
1084
|
+
// ── KEYFRAMES BEFORE LAYERS, ON THE HORIZONTAL PAIR ONLY ────────────────
|
|
1085
|
+
//
|
|
1086
|
+
// A selected keyframe is a MORE SPECIFIC subject than the layer under it —
|
|
1087
|
+
// the same most-specific-wins ladder `deleteSelection` walks — so ←/→ moves
|
|
1088
|
+
// the keys in time when there are keys. ⇧ is the same 10x it is everywhere
|
|
1089
|
+
// else in this table.
|
|
1090
|
+
//
|
|
1091
|
+
// ↑/↓ ARE DELIBERATELY NOT CLAIMED. A keyframe's other axis is its VALUE, and
|
|
1092
|
+
// there is no step size that means anything across kinds: 1 is imperceptible
|
|
1093
|
+
// on a position and the whole range on an opacity. It stays a drag in the
|
|
1094
|
+
// graph, where the axis has a scale on it.
|
|
1095
|
+
if (
|
|
1096
|
+
(key === "ArrowLeft" || key === "ArrowRight") &&
|
|
1097
|
+
context.hasKeyframeSelection === true
|
|
1098
|
+
) {
|
|
1099
|
+
const dir = key === "ArrowLeft" ? -1 : 1;
|
|
1100
|
+
return {
|
|
1101
|
+
kind: "nudgeKeys",
|
|
1102
|
+
delta: dir * (event.shiftKey ? NUDGE_COARSE_FRAMES : 1),
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
447
1105
|
const arrow = ARROWS[key];
|
|
448
1106
|
if (arrow !== undefined) {
|
|
449
|
-
const step = event.shiftKey ? NUDGE_COARSE_PX : NUDGE_PX;
|
|
450
1107
|
// SELECTION WINS. With nothing selected the same keys move the playhead,
|
|
451
1108
|
// which is the only reading where both meanings are reachable without a
|
|
452
1109
|
// mode to switch between them.
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
1110
|
+
if (context.hasSelection) {
|
|
1111
|
+
const step = event.shiftKey ? NUDGE_COARSE_PX : NUDGE_PX;
|
|
1112
|
+
return { kind: "nudge", dx: arrow[0] * step, dy: arrow[1] * step };
|
|
1113
|
+
}
|
|
1114
|
+
const dir = arrow[0] !== 0 ? arrow[0] : arrow[1];
|
|
1115
|
+
// ── THE PLAYHEAD'S OWN THREE READINGS ────────────────────────────────
|
|
1116
|
+
//
|
|
1117
|
+
// ⇧ IS A SECOND, on all four arrows. Shift is the coarse step everywhere
|
|
1118
|
+
// else in this table (10 px on a nudge, 10 frames on a keyframe), and a
|
|
1119
|
+
// frame's own coarse unit is the second — which is also the only jump size
|
|
1120
|
+
// that means the same thing at 24, 30 and 60 fps. It cannot be spelled as
|
|
1121
|
+
// a frame count here: see `stepSeconds`.
|
|
1122
|
+
if (event.shiftKey) return { kind: "stepSeconds", delta: dir < 0 ? -1 : 1 };
|
|
1123
|
+
// ↑ / ↓ WALK THE EDIT POINTS. Bare, and it costs nothing: with nothing
|
|
1124
|
+
// selected the vertical pair used to be a second, identical spelling of
|
|
1125
|
+
// ←/→'s one-frame step, so the only key this claims is a duplicate. What
|
|
1126
|
+
// it buys is the gesture an author scanning a cut actually wants —
|
|
1127
|
+
// straight to the next place something happens.
|
|
1128
|
+
if (arrow[0] === 0) return { kind: "jumpEdit", dir: dir < 0 ? -1 : 1 };
|
|
1129
|
+
return { kind: "step", delta: dir };
|
|
1130
|
+
}
|
|
1131
|
+
// ── BARE DIGITS SET OPACITY, the After Effects and Figma keys ───────────
|
|
1132
|
+
//
|
|
1133
|
+
// 1 is 10% through 9 at 90%, and 0 is 100%. Selection only, by `key`: a
|
|
1134
|
+
// shifted digit is a layout (`layoutForEvent`), a numpad digit returned in
|
|
1135
|
+
// lane 1d, and a live modal kept the digits as typed numbers in lane 1c.
|
|
1136
|
+
// With nothing selected the digits answer null and stay free.
|
|
1137
|
+
if (/^[0-9]$/.test(key) && !event.shiftKey && context.hasSelection) {
|
|
1138
|
+
const n = Number(key);
|
|
1139
|
+
return { kind: "opacity", value: n === 0 ? 1 : n / 10 };
|
|
456
1140
|
}
|
|
457
|
-
|
|
458
|
-
|
|
1141
|
+
// ── THE SHUTTLE: J, K, ⇧K ────────────────────────────────────────────────
|
|
1142
|
+
//
|
|
1143
|
+
// J runs backwards and steps down the ladder, K stops, ⇧K runs forwards and
|
|
1144
|
+
// steps up. Both bare letters were free once the keyframe jumps moved to the
|
|
1145
|
+
// alt lane above, and ⌘K stays the app menu (a lane resolved long before
|
|
1146
|
+
// this one).
|
|
1147
|
+
//
|
|
1148
|
+
// ── L IS NOT AVAILABLE, AND IS NOT SHADOWED ──────────────────────────────
|
|
1149
|
+
//
|
|
1150
|
+
// Every NLE spells this J / K / L, and this chrome cannot: bare `L` is the
|
|
1151
|
+
// LINE tool (`TOOL_SPECS.line.key`), reached through `toolForKey` at the
|
|
1152
|
+
// bottom of this lane, and ⇧L is the Arrow tool beside it. A shuttle bound
|
|
1153
|
+
// above them would shadow both, which is the one thing this file's header
|
|
1154
|
+
// forbids — the tool would still be armable from the dock and from nowhere
|
|
1155
|
+
// on the keyboard, with nothing saying so. The alt lane cannot take it
|
|
1156
|
+
// either: ⌥L is `toggleLock`.
|
|
1157
|
+
//
|
|
1158
|
+
// So the third rung is ⇧K, on the shuttle's own remaining letter. Shift
|
|
1159
|
+
// already means "the same verb, the other subject" in this table (⌘B / ⌘⇧B,
|
|
1160
|
+
// ⌘G / ⌘⇧G, ⌘A / ⌘⇧A), and K is the letter an author's hand is already on.
|
|
1161
|
+
if (lower === "j" && !event.shiftKey) return { kind: "shuttle", dir: -1 };
|
|
1162
|
+
if (lower === "k")
|
|
1163
|
+
return event.shiftKey ? { kind: "shuttle", dir: 1 } : { kind: "shuttleStop" };
|
|
1164
|
+
// `;` and `'` seek to the selection's own in and out — the two keys to the
|
|
1165
|
+
// right of the shuttle's run, and free: no tool claims a punctuation mark,
|
|
1166
|
+
// and the bracket pair one row up already proved this lane safe to be bare.
|
|
1167
|
+
if (key === ";") return { kind: "seekSelectionEdge", edge: "in" };
|
|
1168
|
+
if (key === "'") return { kind: "seekSelectionEdge", edge: "out" };
|
|
1169
|
+
// The three layouts on ⇧1 / ⇧2 / ⇧3, LEFT TO RIGHT as the toolbar draws
|
|
1170
|
+
// them, by the physical key (`layoutForEvent`). The modifier lanes above
|
|
1171
|
+
// have already returned by here, so ⌘1 stays the browser's tab switch. The
|
|
1172
|
+
// BARE digits answer nothing: they are kept free for numeric verbs.
|
|
1173
|
+
const mode = layoutForEvent(event);
|
|
1174
|
+
if (mode !== null) return { kind: "stageLayout", mode };
|
|
1175
|
+
// ── AND ⇧4 IS THE PICTURE ─────────────────────────────────────────────
|
|
1176
|
+
//
|
|
1177
|
+
// Next in the row the three layouts occupy. It is deliberately NOT a toggle
|
|
1178
|
+
// (Escape is the way back, one rung above every other Escape verb) because
|
|
1179
|
+
// ⇧4 names a place rather than a switch, exactly as ⇧1 / ⇧2 / ⇧3 do.
|
|
1180
|
+
if (event.shiftKey && event.code === VIEW_MODE_SPECS.video.code)
|
|
1181
|
+
return { kind: "viewMode", mode: "video" };
|
|
459
1182
|
if (key === " ") return { kind: "playToggle" };
|
|
460
1183
|
// `\` fits the timeline. It is the NLE convention (Premiere and Resolve both
|
|
461
1184
|
// bind "zoom to sequence" to it) and it is a key no letter lane can reach, so
|
|
462
1185
|
// the dock's fit costs the tool table nothing.
|
|
463
1186
|
if (key === "\\") return { kind: "fitTimeline" };
|
|
1187
|
+
// `/` puts the caret in the column's search, the key the field's own hint
|
|
1188
|
+
// shows. By `key`, so a layout that types `/` with Shift still reaches it;
|
|
1189
|
+
// the numpad's `/` returned in lane 1d as isolation. A field already owns
|
|
1190
|
+
// its keystrokes (lane 1), so typing a path into an input is untouched.
|
|
1191
|
+
if (key === "/") return { kind: "focusSearch" };
|
|
464
1192
|
// The bracket keys hide their own side, and `.` hides everything. Bare, and
|
|
465
1193
|
// safe to be bare: none of the three is a tool letter, and this lane's
|
|
466
1194
|
// complaint was that the panels leave too little stage — a chord to see your
|
|
@@ -470,30 +1198,75 @@ export function resolveKey(event: KeyEventLike, context: KeyContext): KeyAction
|
|
|
470
1198
|
if (key === "]") return { kind: "togglePanel", panel: "inspector" };
|
|
471
1199
|
if (key === ".") return { kind: "togglePanel", panel: "all" };
|
|
472
1200
|
// ESCAPE, IN PRECEDENCE ORDER: abandon the pen (lane 1b, above), then
|
|
473
|
-
// deselect, then leave one level of isolation
|
|
474
|
-
//
|
|
475
|
-
//
|
|
476
|
-
//
|
|
477
|
-
//
|
|
1201
|
+
// deselect, then leave one level of isolation, then the definition's own two
|
|
1202
|
+
// rungs (its layers back to its Studio, then the Studio out of the definition
|
|
1203
|
+
// entirely), then leave the shot. Innermost subject first, and they are
|
|
1204
|
+
// ordered rather than exclusive because an author inside a card with a child
|
|
1205
|
+
// selected presses Escape twice — once to drop the selection, once to come
|
|
1206
|
+
// out. Collapsing them would make the second press do nothing and the first
|
|
1207
|
+
// do too much.
|
|
1208
|
+
//
|
|
1209
|
+
// The definition sits above the shot because it NESTS INSIDE one: the
|
|
1210
|
+
// component context is entered from the shot context and every ladder in this
|
|
1211
|
+
// chrome climbs one level per press. The component context and isolation are
|
|
1212
|
+
// mutually exclusive since v14 (entering a definition clears isolation), so at
|
|
1213
|
+
// most one of the two middle rungs fires — but a selection inside either still
|
|
1214
|
+
// takes the first press.
|
|
1215
|
+
//
|
|
1216
|
+
// A SHOT DOOR GOES BACK TO ITS SHOT. When Components was opened from a
|
|
1217
|
+
// Shot (`componentsOrigin`), one press restores that Shot, its frame and its
|
|
1218
|
+
// selection, which is what "Back to Shot" in the bar does.
|
|
478
1219
|
if (key === "Escape") {
|
|
479
|
-
|
|
1220
|
+
// ── RUNG -1: HANDS OFF THE CAMERA ───────────────────────────────────
|
|
1221
|
+
//
|
|
1222
|
+
// The pilot badge promises "ESC TO EJECT", and a mode that writes the
|
|
1223
|
+
// document on every drag has to be the first thing Escape leaves. The
|
|
1224
|
+
// selection, the container and the screening all survive it.
|
|
1225
|
+
if (context.pilot === true) return { kind: "pilotEject" };
|
|
1226
|
+
// ── RUNG 0: PUT THE EDITOR BACK ─────────────────────────────────────
|
|
1227
|
+
//
|
|
1228
|
+
// Above the deselect, and that order is the whole rule: the selection
|
|
1229
|
+
// survives a screening (Video simply stops drawing its handles), so an
|
|
1230
|
+
// author who pressed ⇧4, watched, and pressed Escape is asking for the
|
|
1231
|
+
// editor back and would be surprised to find their selection gone and the
|
|
1232
|
+
// picture unchanged. Every rung below is about the CONTAINER the author is
|
|
1233
|
+
// inside; this one is about whether they are looking or working, which sits
|
|
1234
|
+
// over all of them.
|
|
1235
|
+
if (context.video === true) return { kind: "viewMode", mode: "edit" };
|
|
1236
|
+
if (context.hasSelection || context.hasAudioSelection === true) return { kind: "deselect" };
|
|
480
1237
|
if (context.isolation === true) return { kind: "exitIsolation" };
|
|
1238
|
+
if (context.componentsOrigin === true) return { kind: "backToShot" };
|
|
1239
|
+
if (context.editingDef === true) return { kind: "closeComponentContext" };
|
|
1240
|
+
// The other family's subject, at the same height and never both: the two
|
|
1241
|
+
// are mutually exclusive by construction (each entry clears the other), so
|
|
1242
|
+
// this is a sibling rung rather than one below.
|
|
1243
|
+
if (context.codeComponent === true) return { kind: "closeCodeComponent" };
|
|
1244
|
+
if (context.componentsOpen === true) return { kind: "leaveComponents" };
|
|
1245
|
+
if (context.enteredScene === true) return { kind: "leaveScene" };
|
|
481
1246
|
return { kind: "deselect" };
|
|
482
1247
|
}
|
|
483
1248
|
// ENTER goes the other way: into the selected layer. Unclaimed until now, and
|
|
484
1249
|
// the pen already owns it while a path is open (lane 1b).
|
|
485
|
-
if (key === "Enter" && context.hasSelection)
|
|
486
|
-
|
|
1250
|
+
if (key === "Enter" && context.hasSelection)
|
|
1251
|
+
return { kind: "enterIsolation" };
|
|
1252
|
+
// ⇧BACKSPACE IS THE RIPPLE, above the plain delete because that line reads
|
|
1253
|
+
// neither key's modifiers. Backspace and not Delete, and only Backspace: the
|
|
1254
|
+
// two keys are one binding everywhere else in this table, and splitting them
|
|
1255
|
+
// by name is what keeps a Del pressed near the shift key from closing a gap
|
|
1256
|
+
// the author wanted. Every NLE spells the ripple with a shift for the same
|
|
1257
|
+
// reason.
|
|
1258
|
+
if (key === "Backspace" && event.shiftKey) return { kind: "rippleDelete" };
|
|
1259
|
+
if (key === "Delete" || key === "Backspace")
|
|
1260
|
+
return { kind: "deleteSelection" };
|
|
487
1261
|
// HOME frames the active pane, the Blender "frame all". A key no field
|
|
488
1262
|
// shares (lane 1 already returned for a field) and no tool can reach.
|
|
489
1263
|
if (key === "Home") return { kind: "fit" };
|
|
490
1264
|
if (lower === "a" && event.shiftKey) return { kind: "animateToggle" };
|
|
491
|
-
// ── BLENDER'S SELECTION
|
|
1265
|
+
// ── BLENDER'S SELECTION KEY ───────────────────────────────────────────
|
|
492
1266
|
//
|
|
493
|
-
// Bare `A` selects all
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
// Escape also LEAVES A SUBTREE and deselecting must not.
|
|
1267
|
+
// Bare `A` selects all, which is muscle memory a Blender user brings with
|
|
1268
|
+
// them. Its partner is ⌘⇧A in lane 2a, its own action rather than Escape's,
|
|
1269
|
+
// because Escape also LEAVES A SUBTREE and deselecting must not.
|
|
497
1270
|
if (lower === "a" && !event.shiftKey) return { kind: "selectAll" };
|
|
498
1271
|
// `B` arms a one-shot box select. Stage-local: the letter still has one owner
|
|
499
1272
|
// here, and the stage reads the armed bit beside `e.shiftKey` in its own
|
|
@@ -507,10 +1280,17 @@ export function resolveKey(event: KeyEventLike, context: KeyContext): KeyAction
|
|
|
507
1280
|
//
|
|
508
1281
|
// `toggleFlag` reads `selection[0]`'s own flag, so after hiding five layers
|
|
509
1282
|
// one at a time there was NO key that brought them back — the selection was
|
|
510
|
-
// gone with them. Explicit verbs fix that: H hides,
|
|
511
|
-
//
|
|
512
|
-
|
|
513
|
-
if (lower === "h" && event.shiftKey)
|
|
1283
|
+
// gone with them. Explicit verbs fix that: H hides, ⌘⇧H reveals everything
|
|
1284
|
+
// (lane 2a), ⌥⇧H hides everything else (lane 2b). The outliner's eye button
|
|
1285
|
+
// keeps the toggle.
|
|
1286
|
+
if (lower === "h" && !event.shiftKey)
|
|
1287
|
+
return { kind: "setHidden", scope: "selection" };
|
|
1288
|
+
// ⇧H / ⇧V FLIP, Figma's keys. Only with a selection, so with none both stay
|
|
1289
|
+
// free for the browser.
|
|
1290
|
+
if (event.shiftKey && context.hasSelection) {
|
|
1291
|
+
if (lower === "h") return { kind: "flip", axis: "x" };
|
|
1292
|
+
if (lower === "v") return { kind: "flip", axis: "y" };
|
|
1293
|
+
}
|
|
514
1294
|
// ⇧Z IS THE WIREFRAME TOGGLE, matching Blender. It used to fit the active
|
|
515
1295
|
// pane, which `Home` already does and which is the Blender-correct key for
|
|
516
1296
|
// it — so the letter was spent on a duplicate.
|
|
@@ -525,36 +1305,292 @@ export function resolveKey(event: KeyEventLike, context: KeyContext): KeyAction
|
|
|
525
1305
|
// key. The `+ shot` button in the dock is where an author looks for a move,
|
|
526
1306
|
// and it is still there.
|
|
527
1307
|
if (lower === "c" && event.shiftKey) return { kind: "fit" };
|
|
1308
|
+
// BARE C ARMS THE COMMENT TOOL (S5): the next click on the camera pane
|
|
1309
|
+
// leaves a Studio comment at the playhead. On the board the same key
|
|
1310
|
+
// comments on the selected card.
|
|
1311
|
+
if (lower === "c" && !event.shiftKey) return { kind: "storyboardComment" };
|
|
528
1312
|
// BARE M drops a marker, which is the binding Premiere, Resolve and Final Cut
|
|
529
1313
|
// all ship. Guarded to bare rather than left open, because ⇧M is "go to the
|
|
530
1314
|
// next marker" in the same three tools and this chrome does not have that
|
|
531
1315
|
// verb yet — claiming the shifted letter now would make it unavailable later
|
|
532
1316
|
// without breaking somebody's muscle memory twice.
|
|
533
1317
|
if (lower === "m" && !event.shiftKey) return { kind: "addMarker" };
|
|
1318
|
+
// Q AND W TRIM TO THE PLAYHEAD: Premiere's letters, without the ripple. Bare
|
|
1319
|
+
// and selection only: neither letter is a tool (`toolForKey`), and with
|
|
1320
|
+
// nothing selected both stay free.
|
|
1321
|
+
if (!event.shiftKey && (context.hasSelection || context.hasAudioSelection === true)) {
|
|
1322
|
+
if (lower === "q") return { kind: "trimToPlayhead", edge: "in" };
|
|
1323
|
+
if (lower === "w") return { kind: "trimToPlayhead", edge: "out" };
|
|
1324
|
+
}
|
|
534
1325
|
|
|
535
|
-
// THE MODAL STARTERS, checked immediately before the tool table so bare G
|
|
536
|
-
// and
|
|
1326
|
+
// THE MODAL STARTERS, checked immediately before the tool table so bare G, R
|
|
1327
|
+
// and S shadow the move/rect tools ONLY with the cursor over a 3D working
|
|
537
1328
|
// pane. Blender's letters, Blender's scope: a transform needs an anchor
|
|
538
1329
|
// under the cursor, so from a panel or the camera pane the letters keep
|
|
539
|
-
// arming tools exactly as before.
|
|
540
|
-
//
|
|
541
|
-
//
|
|
542
|
-
//
|
|
543
|
-
|
|
1330
|
+
// arming tools exactly as before.
|
|
1331
|
+
//
|
|
1332
|
+
// ── R NO LONGER NEEDS A SELECTION ──────────────────────────────────────
|
|
1333
|
+
//
|
|
1334
|
+
// With nothing selected all three address the CAMERA (the stage decides that
|
|
1335
|
+
// from the same selection), and the camera now HAS a modal roll — so R is
|
|
1336
|
+
// symmetric with G and S rather than falling through to the rect tool.
|
|
1337
|
+
//
|
|
1338
|
+
// THIS TAKES A KEY FROM THE RECT TOOL, deliberately and only here: R still
|
|
1339
|
+
// arms rect over the camera pane, over every panel and from the toolbar. The
|
|
1340
|
+
// trade is that roll was otherwise unreachable by any gesture at all (the
|
|
1341
|
+
// frustum gizmo has no grip for it — see `framingDeltaFor`), and an
|
|
1342
|
+
// unreachable channel costs more than one shadowed letter in one pane.
|
|
1343
|
+
//
|
|
1344
|
+
// ── BUT NOT OVER A DEFINITION'S FRAME ─────────────────────────────────
|
|
1345
|
+
//
|
|
1346
|
+
// Inside a definition the stage draws one working pane, and it is the frame
|
|
1347
|
+
// the tools draw on (`framePane`), so the trade above does not hold there:
|
|
1348
|
+
// the starters would shadow R over the whole stage while the footer says
|
|
1349
|
+
// "Rect (R)", and the camera roll they bought does not exist. A letter a
|
|
1350
|
+
// tool claims keeps its tool over the frame. Today that is R alone, so G
|
|
1351
|
+
// and S keep their modals.
|
|
1352
|
+
if (
|
|
1353
|
+
context.workingPane === true &&
|
|
1354
|
+
!event.shiftKey &&
|
|
1355
|
+
!(context.framePane === true && toolForKey(lower) !== null)
|
|
1356
|
+
) {
|
|
544
1357
|
if (lower === "g") return { kind: "modalStart", mode: "move" };
|
|
545
1358
|
if (lower === "s") return { kind: "modalStart", mode: "scale" };
|
|
546
|
-
if (lower === "r"
|
|
1359
|
+
if (lower === "r") return { kind: "modalStart", mode: "rotate" };
|
|
547
1360
|
}
|
|
548
1361
|
|
|
1362
|
+
// ⇧S OPENS THE STORYBOARD from the ordinary shell. Unclaimed by every tool
|
|
1363
|
+
// (`toolForKey` binds ⇧L and ⇧P, never ⇧S) and by the modal starters above,
|
|
1364
|
+
// which read the bare letter only.
|
|
1365
|
+
if (lower === "s" && event.shiftKey) return { kind: "storyboardToggle" };
|
|
1366
|
+
// ⇧R IS THE RULERS, for the same reason: the modal starters and the tool
|
|
1367
|
+
// table read bare R only.
|
|
1368
|
+
if (lower === "r" && event.shiftKey) return { kind: "rulersToggle" };
|
|
1369
|
+
|
|
549
1370
|
const tool = toolForKey(lower, event.shiftKey);
|
|
550
1371
|
return tool === null ? null : { kind: "tool", tool };
|
|
551
1372
|
}
|
|
552
1373
|
|
|
1374
|
+
/**
|
|
1375
|
+
* The storyboard's lane, as its own function so lane 1f reads as one line.
|
|
1376
|
+
* `"pass"` keeps undo, redo, save, the app menu and SPACE, which plays.
|
|
1377
|
+
* `null` blanks every tool letter and modal starter.
|
|
1378
|
+
*
|
|
1379
|
+
* THE ARROWS DO NOTHING HERE: a focused strip or Story card walks with them
|
|
1380
|
+
* and stops them there, and a card has no position to nudge.
|
|
1381
|
+
*/
|
|
1382
|
+
function storyboardKey(
|
|
1383
|
+
event: KeyEventLike,
|
|
1384
|
+
key: string,
|
|
1385
|
+
lower: string,
|
|
1386
|
+
mod: boolean,
|
|
1387
|
+
): KeyAction | null | "pass" {
|
|
1388
|
+
if (mod) {
|
|
1389
|
+
switch (lower) {
|
|
1390
|
+
case "z":
|
|
1391
|
+
case "y":
|
|
1392
|
+
case "s":
|
|
1393
|
+
case "k":
|
|
1394
|
+
return "pass";
|
|
1395
|
+
case "a":
|
|
1396
|
+
return { kind: "storyboardSelectAll" };
|
|
1397
|
+
case "d":
|
|
1398
|
+
return { kind: "storyboardDuplicate" };
|
|
1399
|
+
// THE CLIPBOARD, AND ITS ARM RETURNS FALSE ON PURPOSE. See the union.
|
|
1400
|
+
// Native DOM copy / cut / paste handlers do the work, and they are the
|
|
1401
|
+
// only route by which a pasted PNG can reach the board.
|
|
1402
|
+
case "c":
|
|
1403
|
+
case "x":
|
|
1404
|
+
case "v":
|
|
1405
|
+
return { kind: "storyboardClipboard" };
|
|
1406
|
+
default:
|
|
1407
|
+
return arrangeNowhere(event, lower);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
if (event.altKey) return null;
|
|
1411
|
+
const comment = commentKey(event, lower);
|
|
1412
|
+
if (comment !== null) return comment;
|
|
1413
|
+
if (key === " ") return "pass";
|
|
1414
|
+
if (key === "Delete" || key === "Backspace") return { kind: "storyboardDelete" };
|
|
1415
|
+
if (key === "Escape") return { kind: "storyboardEscape" };
|
|
1416
|
+
if (key === "F2") return { kind: "storyboardRename" };
|
|
1417
|
+
// `/` passes through: the board has no search field of its own.
|
|
1418
|
+
if (key === "/") return "pass";
|
|
1419
|
+
if (lower === "s" && event.shiftKey) return { kind: "storyboardToggle" };
|
|
1420
|
+
return null;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
/**
|
|
1424
|
+
* THE SHOT BOARD'S SKETCH KEYS: the arrows move the selected items (Shift:
|
|
1425
|
+
* 10), `[` and `]` restack one step, Enter edits the one text item selected,
|
|
1426
|
+
* and V T R A I C arm its tools. Bare C is Component there, not Comment: a
|
|
1427
|
+
* sketch has no card to comment on.
|
|
1428
|
+
*
|
|
1429
|
+
* THEIR OWN UNION AND THEIR OWN RESOLVER, outside `KeyAction`: only the shot
|
|
1430
|
+
* board's element answers them (ShotBoard.tsx) and stops them there, so an
|
|
1431
|
+
* arrow pressed in the inspector moves nothing. The store's `handleKey` never
|
|
1432
|
+
* meets one, and its dispatch stays exhaustive over `KeyAction`.
|
|
1433
|
+
*/
|
|
1434
|
+
export type SketchKeyAction =
|
|
1435
|
+
| { kind: "sketchMove"; dx: number; dy: number }
|
|
1436
|
+
| { kind: "sketchRestack"; by: 1 | -1 }
|
|
1437
|
+
| { kind: "sketchEdit" }
|
|
1438
|
+
| { kind: "sketchTool"; tool: SketchTool };
|
|
1439
|
+
|
|
1440
|
+
/** A key on a shot board: its sketch lane first (bare or shifted, never with
|
|
1441
|
+
* ⌘ or ⌥, never into a field), then the board's own lane. */
|
|
1442
|
+
export function resolveShotBoardKey(event: KeyEventLike, context: KeyContext): KeyAction | SketchKeyAction | null {
|
|
1443
|
+
if (!context.editing && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
1444
|
+
const sketch = sketchKey(event, event.key, event.key.toLowerCase());
|
|
1445
|
+
if (sketch !== null) return sketch;
|
|
1446
|
+
}
|
|
1447
|
+
return resolveKey(event, { ...context, storyboard: true });
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
const SKETCH_TOOL_KEYS: Readonly<Record<string, SketchTool>> = {
|
|
1451
|
+
v: "select",
|
|
1452
|
+
t: "text",
|
|
1453
|
+
r: "box",
|
|
1454
|
+
a: "arrow",
|
|
1455
|
+
i: "image",
|
|
1456
|
+
c: "component",
|
|
1457
|
+
};
|
|
1458
|
+
const ARROW_STEPS: Readonly<Record<string, readonly [number, number]>> = {
|
|
1459
|
+
ArrowLeft: [-1, 0],
|
|
1460
|
+
ArrowRight: [1, 0],
|
|
1461
|
+
ArrowUp: [0, -1],
|
|
1462
|
+
ArrowDown: [0, 1],
|
|
1463
|
+
};
|
|
1464
|
+
|
|
1465
|
+
function sketchKey(event: KeyEventLike, key: string, lower: string): SketchKeyAction | null {
|
|
1466
|
+
const step = ARROW_STEPS[key];
|
|
1467
|
+
if (step !== undefined) {
|
|
1468
|
+
const by = event.shiftKey ? 10 : 1;
|
|
1469
|
+
return { kind: "sketchMove", dx: step[0] * by, dy: step[1] * by };
|
|
1470
|
+
}
|
|
1471
|
+
if (key === "[") return { kind: "sketchRestack", by: -1 };
|
|
1472
|
+
if (key === "]") return { kind: "sketchRestack", by: 1 };
|
|
1473
|
+
if (key === "Enter") return { kind: "sketchEdit" };
|
|
1474
|
+
const tool = event.shiftKey ? undefined : SKETCH_TOOL_KEYS[lower];
|
|
1475
|
+
return tool === undefined ? null : { kind: "sketchTool", tool };
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
/** The comment keys: bare C (Shift+C stays "fit"), N and Shift+N. */
|
|
1479
|
+
function commentKey(event: KeyEventLike, lower: string): KeyAction | null {
|
|
1480
|
+
if (lower === "c" && !event.shiftKey) return { kind: "storyboardComment" };
|
|
1481
|
+
if (lower === "n") return { kind: event.shiftKey ? "storyboardPrevThread" : "storyboardNextThread" };
|
|
1482
|
+
return null;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
/**
|
|
1486
|
+
* The graph canvas's lane, as its own function so lane 1e reads as one line.
|
|
1487
|
+
*
|
|
1488
|
+
* `"pass"` means "this key is not mine, keep walking the ordinary lanes" and is
|
|
1489
|
+
* reserved for the four that must survive a context change: ⌘Z, ⌘⇧Z / ⌘Y, ⌘S
|
|
1490
|
+
* and ⌘K. Everything else in this lane is either a graph verb or `null`.
|
|
1491
|
+
*
|
|
1492
|
+
* SPACE USED TO BE A FIFTH, and is not any more. Over this canvas it is
|
|
1493
|
+
* hold-to-pan, which React Flow implements on its OWN window listener — so the
|
|
1494
|
+
* lane has to return `null` rather than `"pass"`: `null` means `handleKey`
|
|
1495
|
+
* reports the key unhandled and never calls `preventDefault`, which is exactly
|
|
1496
|
+
* what lets that listener see it. Play/pause is untouched everywhere else in
|
|
1497
|
+
* the workspace, including the dock right below this canvas.
|
|
1498
|
+
*
|
|
1499
|
+
* THE NULL RETURN IS THE POINT elsewhere too. Bare letters reach the tool table
|
|
1500
|
+
* at the bottom of `resolveKey`, and a graph canvas has no rectangle to draw and
|
|
1501
|
+
* no pen to arm — an `R` that silently armed the rect tool would leave the
|
|
1502
|
+
* author back on the stage with a tool they never chose, several clicks later.
|
|
1503
|
+
*/
|
|
1504
|
+
function materialGraphKey(
|
|
1505
|
+
event: KeyEventLike,
|
|
1506
|
+
key: string,
|
|
1507
|
+
lower: string,
|
|
1508
|
+
mod: boolean,
|
|
1509
|
+
): KeyAction | null | "pass" {
|
|
1510
|
+
if (mod) {
|
|
1511
|
+
switch (lower) {
|
|
1512
|
+
// The four that mean the same thing everywhere in this chrome.
|
|
1513
|
+
case "z":
|
|
1514
|
+
case "y":
|
|
1515
|
+
case "s":
|
|
1516
|
+
case "k":
|
|
1517
|
+
return "pass";
|
|
1518
|
+
case "d":
|
|
1519
|
+
return { kind: "graphDuplicate" };
|
|
1520
|
+
case "c":
|
|
1521
|
+
return { kind: "graphCopy" };
|
|
1522
|
+
case "x":
|
|
1523
|
+
return { kind: "graphCut" };
|
|
1524
|
+
case "v":
|
|
1525
|
+
return { kind: "graphPaste" };
|
|
1526
|
+
case "a":
|
|
1527
|
+
return { kind: "graphSelectAll" };
|
|
1528
|
+
case "g":
|
|
1529
|
+
return event.shiftKey
|
|
1530
|
+
? { kind: "graphUngroup" }
|
|
1531
|
+
: { kind: "graphGroup" };
|
|
1532
|
+
default:
|
|
1533
|
+
// An unclaimed combo belongs to the BROWSER, exactly as in lane 2a,
|
|
1534
|
+
// which claims Arrange's brackets, and so does this lane.
|
|
1535
|
+
return arrangeNowhere(event, lower);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
// The alt lane's verbs are all about layers and the working view, and none of
|
|
1539
|
+
// them has a meaning over a node. Blanked rather than passed, so ⌥G cannot
|
|
1540
|
+
// clear a mesh's position from inside the graph.
|
|
1541
|
+
if (event.altKey) return null;
|
|
1542
|
+
|
|
1543
|
+
// SPACE BELONGS TO REACT FLOW HERE — hold-to-pan, on its own window
|
|
1544
|
+
// listener. `null` and not `"pass"`: see the header. Play/pause keeps the
|
|
1545
|
+
// key everywhere in this workspace that is not this canvas.
|
|
1546
|
+
if (key === " ") return null;
|
|
1547
|
+
// TAB and ⇧A both open the palette: Tab is what Blender's and Unreal's node
|
|
1548
|
+
// editors bind, ⇧A is Blender's Add menu, and an author arrives with one or
|
|
1549
|
+
// the other in their fingers. Tab is not a focus trap here because the canvas
|
|
1550
|
+
// has no tabbable content of its own; the panels around it still do.
|
|
1551
|
+
if (key === "Tab") return { kind: "graphPalette" };
|
|
1552
|
+
if (lower === "a" && event.shiftKey) return { kind: "graphPalette" };
|
|
1553
|
+
if (key === "Delete" || key === "Backspace") return { kind: "graphDelete" };
|
|
1554
|
+
if (lower === "d" && event.shiftKey) return { kind: "graphDuplicate" };
|
|
1555
|
+
if (key === "Enter") return { kind: "graphEnter" };
|
|
1556
|
+
if (lower === "p" && !event.shiftKey) return { kind: "graphPreviewToggle" };
|
|
1557
|
+
// HOME is this chrome's "frame all" and `F` is what every node editor binds.
|
|
1558
|
+
// Both, because the graph is the one surface an author reaches from muscle
|
|
1559
|
+
// memory built somewhere else.
|
|
1560
|
+
if (key === "Home" || (lower === "f" && !event.shiftKey))
|
|
1561
|
+
return { kind: "graphFrame" };
|
|
1562
|
+
// T for TIDY. A bare letter this lane used to blank; it is the one arrangement
|
|
1563
|
+
// key a node editor is expected to have, and there is no tool for it to
|
|
1564
|
+
// collide with because this canvas arms none.
|
|
1565
|
+
if (lower === "t" && !event.shiftKey) return { kind: "graphTidy" };
|
|
1566
|
+
const arrow = ARROWS[key];
|
|
1567
|
+
if (arrow !== undefined) {
|
|
1568
|
+
const step = event.shiftKey ? GRAPH_NUDGE_COARSE_PX : GRAPH_NUDGE_PX;
|
|
1569
|
+
return { kind: "graphNudge", dx: arrow[0] * step, dy: arrow[1] * step };
|
|
1570
|
+
}
|
|
1571
|
+
if (key === "Escape") return { kind: "graphEscape" };
|
|
1572
|
+
return null;
|
|
1573
|
+
}
|
|
1574
|
+
|
|
553
1575
|
/** Is the event target something the author is typing into? Exported because
|
|
554
1576
|
* both documents ask it and neither should re-derive it. */
|
|
555
1577
|
export function isEditable(target: EventTarget | null): boolean {
|
|
556
|
-
if (target === null || typeof (target as HTMLElement).tagName !== "string")
|
|
1578
|
+
if (target === null || typeof (target as HTMLElement).tagName !== "string")
|
|
1579
|
+
return false;
|
|
557
1580
|
const el = target as HTMLElement;
|
|
558
1581
|
if (el.isContentEditable) return true;
|
|
559
|
-
return
|
|
1582
|
+
return (
|
|
1583
|
+
el.tagName === "INPUT" ||
|
|
1584
|
+
el.tagName === "TEXTAREA" ||
|
|
1585
|
+
el.tagName === "SELECT"
|
|
1586
|
+
);
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
/** Is the target a control the keyboard focused, which Enter and Space press?
|
|
1590
|
+
* Not part of `isEditable`: a focused button does not own letters or Delete. */
|
|
1591
|
+
export function ownsActivation(target: EventTarget | null): boolean {
|
|
1592
|
+
if (target === null || typeof (target as HTMLElement).matches !== "function") return false;
|
|
1593
|
+
return (target as HTMLElement).matches(
|
|
1594
|
+
'button:focus-visible, [role="menuitem"]:focus-visible, [role="option"]:focus-visible, [role="tab"]:focus-visible, [role="radio"]:focus-visible',
|
|
1595
|
+
);
|
|
560
1596
|
}
|