@dailies-studio/shot-editor 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.vite/manifest.json +382 -0
- package/dist/assets/AgentPanel-CQejLbiX.js +2 -0
- package/dist/assets/AgentPanel-DJEphah0.css +1 -0
- package/dist/assets/AgentQuestionDialog-BqkD0kXF.js +1 -0
- package/dist/assets/CodeStudio-Dv4ihHpz.js +23 -0
- package/dist/assets/CommentsPanel-B4yhhT1T.js +1 -0
- package/dist/assets/ComponentLibraryRail-Ch_dsAru.js +1 -0
- package/dist/assets/ComponentStage-BRhRotyH.js +1 -0
- package/dist/assets/CurvePane-BjFVabKX.css +1 -0
- package/dist/assets/CurvePane-R22WzllJ.js +1 -0
- package/dist/assets/ExportDialog-CWLQWTmG.js +2 -0
- package/dist/assets/FontPicker-DWKaYL7F.js +1 -0
- package/dist/assets/FontPicker-v0IOIArM.css +1 -0
- package/dist/assets/GenerateDrawer-Bh3SqDKt.js +2 -0
- package/dist/assets/InsertDrawer-Ce1fod1L.js +1 -0
- package/dist/assets/LibraryBrowser-D9sEGz5w.js +2 -0
- package/dist/assets/MakeItYoursSection-CMO4319_.js +2 -0
- package/dist/assets/MaterialGraphCanvas-BFzl4wWR.js +23 -0
- package/dist/assets/MaterialGraphCanvas-BIUD59ik.css +1 -0
- package/dist/assets/MaterialWorkspace-CqTWqaRA.js +14 -0
- package/dist/assets/PreferencesDialog-CBf9_OoD.js +1 -0
- package/dist/assets/PresetsSection-CUEaoCVU.js +1 -0
- package/dist/assets/SoundHistory-BeS8EW1h.js +1 -0
- package/dist/assets/SoundMode-BmcZru3i.js +3 -0
- package/dist/assets/StockSounds-DdAUGRYK.js +1 -0
- package/dist/assets/TemplateFieldSection-CaeXpLFo.js +1 -0
- package/dist/assets/VersionHistoryPanel-CxG6XhNP.js +1 -0
- package/dist/assets/board-B88EPuLO.css +1 -0
- package/dist/assets/board-CL-he7SL.js +3 -0
- package/dist/assets/components-0oOSPN0l.js +1 -0
- package/dist/assets/googleFontRanges-gp32wJMo.js +1 -0
- package/dist/assets/googleFonts-BNon6M7X.js +1 -0
- package/dist/assets/iconsAndAliases-DL7gXP5j.js +1863 -0
- package/dist/assets/index-4F5NQmI8.css +1 -0
- package/dist/assets/index-BGHQGV5z.js +117 -0
- package/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/assets/libraryModel-CpIwfFha.js +1 -0
- package/dist/assets/lucide-6iK64tTB.js +9319 -0
- package/dist/assets/motionStamp-CH-Hew4E.js +1 -0
- package/dist/assets/takeMeasure-Tdg0TxEi.js +1 -0
- package/dist/assets/webmcp-BsB9gxWC.js +2 -0
- package/dist/index.html +34 -3
- package/package.json +12 -9
- package/src/addFamilies.ts +66 -0
- package/src/api.ts +1631 -55
- package/src/app/App.tsx +977 -198
- package/src/app/lazyPanel.tsx +137 -0
- package/src/app/lazySurfaces.tsx +203 -0
- package/src/audio/index.tsx +41 -10
- package/src/audio/meter.ts +485 -0
- package/src/audio/meterMath.ts +273 -0
- package/src/audio/meterStore.ts +155 -0
- package/src/audio/offlineMix.ts +289 -0
- package/src/audio/preview.ts +928 -118
- package/src/bridge.ts +15 -5
- package/src/bridgeTypes.ts +1611 -36
- package/src/channels.ts +17 -32
- package/src/fonts/googleFonts.ts +4 -0
- package/src/hostApi.ts +174 -0
- package/src/icons.tsx +770 -0
- package/src/layerWords.ts +61 -0
- package/src/main.tsx +56 -4
- package/src/panels/ResizeHandle.tsx +33 -43
- package/src/panels/agent/AgentDots.tsx +36 -0
- package/src/panels/agent/AgentPanel.tsx +119 -43
- package/src/panels/agent/AgentQuestionDialog.tsx +202 -0
- package/src/panels/agent/agent.css +113 -97
- package/src/panels/agent/agentGate.ts +35 -0
- package/src/panels/code/CodeDefaultsPanel.tsx +345 -0
- package/src/panels/code/CodeStudio.tsx +373 -0
- package/src/panels/code/FileTree.tsx +265 -0
- package/src/panels/code/codeBufferGuard.ts +52 -0
- package/src/panels/code/codeParts.tsx +321 -0
- package/src/panels/comments/CommentComposer.tsx +239 -0
- package/src/panels/comments/CommentThread.tsx +346 -0
- package/src/panels/comments/CommentsPanel.tsx +336 -0
- package/src/panels/comments/InboxButton.tsx +131 -0
- package/src/panels/comments/MentionMenu.tsx +98 -0
- package/src/panels/comments/parts.tsx +171 -0
- package/src/panels/export/ExportDialog.tsx +919 -96
- package/src/panels/export/ExportScrim.tsx +57 -0
- package/src/panels/files/AssetsTab.tsx +656 -0
- package/src/panels/files/FilePanel.tsx +1466 -216
- package/src/panels/files/LayerMarks.tsx +40 -0
- package/src/panels/files/LeftColumn.tsx +37 -0
- package/src/panels/files/McpSection.tsx +690 -47
- package/src/panels/files/MissingAssetsNotice.tsx +60 -0
- package/src/panels/files/assetDrag.ts +79 -0
- package/src/panels/files/resourceParts.tsx +255 -0
- package/src/panels/fonts/FontPicker.tsx +762 -0
- package/src/panels/fonts/faceCache.ts +207 -0
- package/src/panels/fonts/fontPicker.css +271 -0
- package/src/panels/fonts/pickerModel.ts +265 -0
- package/src/panels/fonts/pickerRequest.ts +46 -0
- package/src/panels/header/AccountAvatar.tsx +76 -0
- package/src/panels/header/AgentChip.tsx +77 -0
- package/src/panels/header/AppMenu.tsx +213 -0
- package/src/panels/header/Header.tsx +564 -84
- package/src/panels/header/HistoryControls.tsx +31 -5
- package/src/panels/header/PresenceStack.tsx +329 -0
- package/src/panels/header/ProjectTitle.tsx +232 -0
- package/src/panels/header/ShareButton.tsx +59 -0
- package/src/panels/header/StatusBadges.tsx +358 -27
- package/src/panels/header/SyncDeletedDialog.tsx +88 -0
- package/src/panels/header/ZoomControls.tsx +46 -15
- package/src/panels/header/agentHandoff.ts +52 -0
- package/src/panels/header/leave.ts +26 -0
- package/src/panels/help/HelpSection.tsx +189 -163
- package/src/panels/insert/InsertDrawer.tsx +723 -0
- package/src/panels/insert/iconSet.ts +109 -0
- package/src/panels/inspector/ExposeDot.tsx +117 -0
- package/src/panels/inspector/FramingRows.tsx +314 -44
- package/src/panels/inspector/InspectorFooter.tsx +91 -0
- package/src/panels/inspector/InspectorHead.tsx +425 -0
- package/src/panels/inspector/InspectorPanel.tsx +408 -124
- package/src/panels/inspector/KeyframeDot.tsx +373 -0
- package/src/panels/inspector/KeyframeSection.tsx +257 -0
- package/src/panels/inspector/MarkerSection.tsx +55 -11
- package/src/panels/inspector/ParamRow.tsx +84 -53
- package/src/panels/inspector/RightColumn.tsx +152 -0
- package/src/panels/inspector/TimeSection.tsx +866 -0
- package/src/panels/inspector/animate/EaseControl.tsx +244 -0
- package/src/panels/inspector/animate/PresetsSection.tsx +135 -0
- package/src/panels/inspector/animate/ShotSection.tsx +562 -48
- package/src/panels/inspector/design/AlignRow.tsx +95 -31
- package/src/panels/inspector/design/AppearanceSection.tsx +118 -9
- package/src/panels/inspector/design/ClonerSection.tsx +323 -62
- package/src/panels/inspector/design/ComponentSection.tsx +50 -43
- package/src/panels/inspector/design/CornerSection.tsx +45 -14
- package/src/panels/inspector/design/DefinitionProps.tsx +315 -0
- package/src/panels/inspector/design/DefinitionSection.tsx +136 -0
- package/src/panels/inspector/design/DeviceSection.tsx +210 -0
- package/src/panels/inspector/design/FillSection.tsx +83 -51
- package/src/panels/inspector/design/FontField.tsx +196 -0
- package/src/panels/inspector/design/HtmlSection.tsx +23 -12
- package/src/panels/inspector/design/InstanceSection.tsx +373 -154
- package/src/panels/inspector/design/MaskSection.tsx +84 -0
- package/src/panels/inspector/design/MaterialSection.tsx +177 -0
- package/src/panels/inspector/design/MediaSection.tsx +273 -120
- package/src/panels/inspector/design/MeshSection.tsx +910 -82
- package/src/panels/inspector/design/PaintRows.tsx +550 -0
- package/src/panels/inspector/design/PlateStyleSection.tsx +177 -0
- package/src/panels/inspector/design/ShadowSection.tsx +221 -48
- package/src/panels/inspector/design/SplineFollowSection.tsx +235 -0
- package/src/panels/inspector/design/StrokeSection.tsx +228 -43
- package/src/panels/inspector/design/TemplateFieldSection.tsx +57 -0
- package/src/panels/inspector/design/TransformSection.tsx +244 -50
- package/src/panels/inspector/design/TypographySection.tsx +679 -110
- package/src/panels/inspector/layout.ts +295 -0
- package/src/panels/inspector/propValue.ts +199 -0
- package/src/panels/inspector/scene/CameraSection.tsx +232 -22
- package/src/panels/inspector/scene/EffectsSection.tsx +722 -71
- package/src/panels/inspector/scene/MakeItYoursSection.tsx +180 -0
- package/src/panels/inspector/scene/SceneDocSection.tsx +191 -0
- package/src/panels/inspector/scene/SceneSection.tsx +219 -44
- package/src/panels/inspector/scene/TransitionSection.tsx +523 -72
- package/src/panels/inspector/storyboard/StoryboardReferenceSection.tsx +593 -0
- package/src/panels/inspector/storyboard/VideoStill.tsx +120 -0
- package/src/panels/inspector/timeModel.ts +190 -0
- package/src/panels/inspector/useDocWrite.ts +438 -146
- package/src/panels/library/LibraryBrowser.tsx +435 -0
- package/src/panels/library/LibraryGrid.tsx +190 -0
- package/src/panels/library/LibraryPreviewPanel.tsx +349 -0
- package/src/panels/library/LibraryRail.tsx +84 -0
- package/src/panels/library/LibrarySoundPanel.tsx +293 -0
- package/src/panels/library/SoundHistory.tsx +245 -0
- package/src/panels/library/StockSounds.tsx +390 -0
- package/src/panels/library/libraryModel.ts +147 -0
- package/src/panels/library/stockSearch.ts +77 -0
- package/src/panels/maskMarks.tsx +117 -0
- package/src/panels/material/MaterialCommentNode.tsx +54 -0
- package/src/panels/material/MaterialGraphCanvas.tsx +861 -0
- package/src/panels/material/MaterialGraphMenu.tsx +45 -0
- package/src/panels/material/MaterialGroupNode.tsx +56 -0
- package/src/panels/material/MaterialLibrary.tsx +253 -0
- package/src/panels/material/MaterialNode.tsx +453 -0
- package/src/panels/material/MaterialNodePalette.tsx +349 -0
- package/src/panels/material/MaterialParameterControls.tsx +525 -0
- package/src/panels/material/MaterialRerouteNode.tsx +42 -0
- package/src/panels/material/MaterialWorkspace.tsx +501 -0
- package/src/panels/material/NodePreview.tsx +70 -0
- package/src/panels/material/flowModel.ts +441 -0
- package/src/panels/material/parameterValues.ts +231 -0
- package/src/panels/material/previewScheduler.ts +235 -0
- package/src/panels/material/reactFlow.css +6 -0
- package/src/panels/material/socketGlyph.ts +85 -0
- package/src/panels/material/viewportMemory.ts +58 -0
- package/src/panels/preferences/PreferencesDialog.tsx +215 -0
- package/src/panels/share/ShareComponentDialog.tsx +247 -0
- package/src/panels/stage/StageMenu.tsx +35 -5
- package/src/panels/stage/StagePanel.tsx +338 -23
- package/src/panels/stage/cellSlots.tsx +70 -0
- package/src/panels/storyboard/BoardCanvas.tsx +739 -0
- package/src/panels/storyboard/BoardCursors.tsx +158 -0
- package/src/panels/storyboard/BoardDropDialog.tsx +161 -0
- package/src/panels/storyboard/BoardPlan.tsx +42 -0
- package/src/panels/storyboard/ComponentSheet.tsx +187 -0
- package/src/panels/storyboard/ComponentStage.tsx +235 -0
- package/src/panels/storyboard/MiniSketch.tsx +173 -0
- package/src/panels/storyboard/PartPicker.tsx +64 -0
- package/src/panels/storyboard/PlanPanel.tsx +617 -0
- package/src/panels/storyboard/ReferencePreview.tsx +217 -0
- package/src/panels/storyboard/ShotBrief.tsx +360 -0
- package/src/panels/storyboard/ShotCard.tsx +251 -0
- package/src/panels/storyboard/StagePreview.tsx +348 -0
- package/src/panels/storyboard/StillThumbs.tsx +162 -0
- package/src/panels/storyboard/StoryboardView.tsx +79 -0
- package/src/panels/storyboard/board.css +1065 -0
- package/src/panels/storyboard/board.ts +12 -0
- package/src/panels/storyboard/boardClipboard.ts +111 -0
- package/src/panels/storyboard/boardOrder.ts +68 -0
- package/src/panels/storyboard/boardRows.tsx +425 -0
- package/src/panels/storyboard/liveCap.ts +90 -0
- package/src/panels/storyboard/plan.css +295 -0
- package/src/panels/storyboard/posterCache.ts +133 -0
- package/src/panels/storyboard/shotboard/ComponentPicker.tsx +373 -0
- package/src/panels/storyboard/shotboard/ImagePicker.tsx +145 -0
- package/src/panels/storyboard/shotboard/ShotBoard.tsx +292 -0
- package/src/panels/storyboard/shotboard/SketchCanvas.tsx +443 -0
- package/src/panels/storyboard/shotboard/SketchInspector.tsx +471 -0
- package/src/panels/storyboard/shotboard/SketchItem.tsx +240 -0
- package/src/panels/storyboard/shotboard/SketchLayers.tsx +130 -0
- package/src/panels/storyboard/shotboard/SketchPlay.tsx +108 -0
- package/src/panels/storyboard/shotboard/SketchStrip.tsx +178 -0
- package/src/panels/storyboard/shotboard/SketchToolFooter.tsx +99 -0
- package/src/panels/storyboard/shotboard/sketch.css +1392 -0
- package/src/panels/storyboard/shotboard/sketchModel.ts +219 -0
- package/src/panels/storyboard/storyboardDrop.ts +164 -0
- package/src/panels/studio/ComponentLibraryRail.tsx +346 -0
- package/src/panels/studio/ComponentSidePanel.tsx +355 -0
- package/src/panels/studio/ComponentStudio.tsx +243 -0
- package/src/panels/studio/ComponentsHome.tsx +318 -0
- package/src/panels/studio/ComponentsPreview.tsx +179 -0
- package/src/panels/studio/EveryUse.tsx +244 -0
- package/src/panels/studio/ThisUsePanel.tsx +679 -0
- package/src/panels/studio/componentParts.tsx +341 -0
- package/src/panels/studio/components.ts +6 -0
- package/src/panels/studio/newCodeComponent.ts +66 -0
- package/src/panels/timeline/AudioFxButton.tsx +290 -0
- package/src/panels/timeline/AudioLane.tsx +674 -85
- package/src/panels/timeline/BlockRow.tsx +154 -85
- package/src/panels/timeline/ChannelRow.tsx +378 -129
- package/src/panels/timeline/CurvePane.tsx +508 -0
- package/src/panels/timeline/Filmstrip.tsx +361 -0
- package/src/panels/timeline/GenerateDrawer.tsx +407 -0
- package/src/panels/timeline/KeyStretch.tsx +115 -0
- package/src/panels/timeline/LifespanRow.tsx +730 -47
- package/src/panels/timeline/MarkerLane.tsx +46 -8
- package/src/panels/timeline/PlayButton.tsx +91 -0
- package/src/panels/timeline/PropertyPicker.tsx +254 -0
- package/src/panels/timeline/ShotRow.tsx +400 -0
- package/src/panels/timeline/SoundMode.tsx +1406 -0
- package/src/panels/timeline/TimelineDock.tsx +2338 -272
- package/src/panels/timeline/TransportVolume.tsx +163 -0
- package/src/panels/timeline/beatRuler.ts +74 -0
- package/src/panels/timeline/curveModel.ts +500 -0
- package/src/panels/timeline/duckUnderVoice.ts +68 -0
- package/src/panels/timeline/keyframeMenu.ts +138 -0
- package/src/panels/timeline/labelWidth.ts +49 -0
- package/src/panels/timeline/marquee.ts +155 -0
- package/src/panels/timeline/model.ts +1860 -199
- package/src/panels/timeline/snap.ts +125 -43
- package/src/panels/timeline/staggerPreview.tsx +92 -0
- package/src/panels/timeline/takeMeasure.ts +72 -0
- package/src/panels/timeline/useLive.ts +48 -0
- package/src/panels/timeline/viewport.ts +234 -0
- package/src/panels/toolbar/BackChip.tsx +116 -0
- package/src/panels/toolbar/CameraViewBar.tsx +118 -49
- package/src/panels/toolbar/CanvasBar.tsx +117 -0
- package/src/panels/toolbar/InsertButton.tsx +38 -0
- package/src/panels/toolbar/IsolationTrail.tsx +156 -0
- package/src/panels/toolbar/PanelToggles.tsx +86 -0
- package/src/panels/toolbar/ShadingBar.tsx +119 -26
- package/src/panels/toolbar/ToolDockBar.tsx +329 -80
- package/src/panels/toolbar/ToolFooter.tsx +102 -0
- package/src/panels/toolbar/ViewModeBar.tsx +247 -38
- package/src/panels/toolbar/WorkPlaneBar.tsx +94 -0
- package/src/panels/toolbar/useAddRows.tsx +326 -0
- package/src/panels/toolbar/useDismiss.ts +122 -0
- package/src/panels/versions/VersionHistoryPanel.tsx +274 -0
- package/src/panels/versions/versionModel.ts +103 -0
- package/src/panels/virtualWindow.ts +271 -0
- package/src/shotTokens.css +151 -60
- package/src/stage/FrustumGizmo.tsx +504 -84
- package/src/stage/GridPlane.tsx +225 -16
- package/src/stage/NodePreviews.tsx +628 -0
- package/src/stage/PerfHud.tsx +437 -47
- package/src/stage/SplitRenderer.tsx +816 -169
- package/src/stage/StageApp.tsx +4274 -486
- package/src/stage/StageRootBoundary.tsx +114 -0
- package/src/stage/StageTools.tsx +651 -151
- package/src/stage/TransformGizmo.tsx +696 -0
- package/src/stage/adaptivePresentation.ts +145 -0
- package/src/stage/bridgeClient.ts +2522 -41
- package/src/stage/exportWorker.ts +9 -0
- package/src/stage/feed.ts +305 -14
- package/src/stage/fontPreview.ts +176 -0
- package/src/stage/frustumGeometry.ts +319 -1
- package/src/stage/furnitureTokens.ts +33 -0
- package/src/stage/gizmoMath.ts +508 -16
- package/src/stage/gpuTimer.ts +276 -0
- package/src/stage/instanceModules.ts +194 -33
- package/src/stage/interactions/move.ts +24 -5
- package/src/stage/isolation.ts +291 -33
- package/src/stage/jsSelfProfiler.ts +325 -0
- package/src/stage/main.tsx +116 -16
- package/src/stage/modalState.ts +222 -23
- package/src/stage/moduleEpoch.ts +60 -0
- package/src/stage/motionPathStage.ts +279 -0
- package/src/stage/motionPreview.ts +74 -0
- package/src/stage/overlays/Overlays.tsx +1112 -159
- package/src/stage/overlays/Rulers.tsx +495 -0
- package/src/stage/panes.ts +159 -10
- package/src/stage/project.ts +68 -3
- package/src/stage/renderScaleController.ts +730 -0
- package/src/stage/rulers.ts +129 -0
- package/src/stage/sceneExtent.ts +75 -0
- package/src/stage/shading.ts +20 -0
- package/src/stage/splineStage.ts +632 -0
- package/src/stage/stagePerformanceTelemetry.ts +717 -0
- package/src/stage/stageTheme.ts +36 -0
- package/src/stage/stageWarmup.ts +11 -0
- package/src/stage/textAutoSize.ts +124 -0
- package/src/stage/tier1Export.ts +641 -0
- package/src/stage/tokenColor.ts +14 -9
- package/src/stage/view.ts +56 -9
- package/src/stage/workingView.ts +277 -33
- package/src/state/accountPrefs.ts +261 -0
- package/src/state/agentActions.ts +173 -15
- package/src/state/agentActivity.ts +198 -0
- package/src/state/agentQuestion.ts +239 -0
- package/src/state/bootOpen.ts +42 -0
- package/src/state/cameraVerbs.ts +139 -0
- package/src/state/clientId.ts +6 -1
- package/src/state/comments.ts +1099 -0
- package/src/state/context.tsx +19 -1
- package/src/state/editorPrefs.ts +220 -0
- package/src/state/errorReports.ts +121 -0
- package/src/state/fieldWrite.ts +161 -0
- package/src/state/fontRecents.ts +70 -0
- package/src/state/fonts.ts +656 -0
- package/src/state/graphClipboard.ts +414 -0
- package/src/state/imageThumbs.ts +216 -0
- package/src/state/insert.ts +444 -67
- package/src/state/isolationTrail.ts +92 -0
- package/src/state/keyMoves.ts +134 -0
- package/src/state/keyframeClipboard.ts +177 -0
- package/src/state/keymap.ts +1153 -117
- package/src/state/layoutPrefs.ts +272 -0
- package/src/state/layouts.ts +205 -0
- package/src/state/measureMedia.ts +167 -0
- package/src/state/menu.ts +2341 -28
- package/src/state/motionStamp.ts +7 -0
- package/src/state/panelSize.ts +375 -5
- package/src/state/pendingChanges.ts +119 -0
- package/src/state/playHold.ts +78 -0
- package/src/state/playbackPreparation.ts +562 -0
- package/src/state/posterCapture.ts +242 -0
- package/src/state/presence.ts +254 -0
- package/src/state/projectComponents.ts +160 -0
- package/src/state/renderEvents.ts +298 -18
- package/src/state/runtimeErrors.ts +46 -0
- package/src/state/selection.ts +12 -0
- package/src/state/shortcuts.ts +1606 -0
- package/src/state/sketchGuide.ts +51 -0
- package/src/state/splineRefs.ts +87 -0
- package/src/state/stageHealth.ts +259 -0
- package/src/state/stageWatchdog.ts +47 -0
- package/src/state/stillStage.ts +330 -0
- package/src/state/stillThumbs.ts +474 -0
- package/src/state/store.ts +23591 -2982
- package/src/state/storyboardModel.ts +528 -0
- package/src/state/sync.ts +1257 -0
- package/src/state/syncCache.ts +159 -0
- package/src/state/triangles.ts +13 -0
- package/src/state/types.ts +1698 -55
- package/src/state/unsavedGuard.ts +49 -0
- package/src/state/viewMode.ts +200 -0
- package/src/state/webmcp.ts +1128 -0
- package/src/state/workspace.ts +1147 -0
- package/src/styles/assets-tab.css +304 -0
- package/src/styles/canvas.css +545 -0
- package/src/styles/comments.css +1435 -0
- package/src/styles/controls.css +162 -0
- package/src/styles/dialogs.css +390 -0
- package/src/styles/insert-drawer.css +586 -0
- package/src/styles/inspector-board.css +207 -0
- package/src/styles/inspector-object.css +486 -0
- package/src/styles/inspector-shot.css +252 -0
- package/src/styles/inspector.css +360 -0
- package/src/styles/left-column.css +1628 -0
- package/src/styles/library-preview.css +575 -0
- package/src/styles/library.css +512 -0
- package/src/styles/modes/code.css +569 -0
- package/src/styles/modes/component.css +1561 -0
- package/src/styles/modes/material.css +1201 -0
- package/src/styles/modes/storyboard.css +518 -0
- package/src/styles/share-component.css +49 -0
- package/src/styles/shell.css +653 -0
- package/src/styles/shot-brief.css +360 -0
- package/src/styles/states.css +76 -0
- package/src/styles/sync.css +380 -0
- package/src/styles/timeline-graph.css +384 -0
- package/src/styles/timeline-meters.css +89 -0
- package/src/styles/timeline-rows.css +1959 -0
- package/src/styles/timeline-sound.css +1003 -0
- package/src/styles/timeline.css +754 -0
- package/src/styles/tool-footer.css +525 -0
- package/src/styles/topbar.css +886 -0
- package/src/styles/versions.css +262 -0
- package/src/styles.css +72 -2921
- package/src/tools.ts +108 -23
- package/dist/assets/index-B7notge7.css +0 -1
- package/dist/assets/index-BBE6Ta9L.js +0 -52
- package/src/panels/files/icons.tsx +0 -118
- package/src/panels/header/DocSwitcher.tsx +0 -85
- package/src/panels/header/TransportBar.tsx +0 -86
- package/src/panels/inspector/animate/BlockTiming.tsx +0 -341
- package/src/panels/inspector/animate/DuringBlocks.tsx +0 -85
- package/src/panels/inspector/animate/InOutPresets.tsx +0 -57
- package/src/panels/inspector/animate/ResolvedChannels.tsx +0 -150
- package/src/panels/inspector/animate/useBlocks.ts +0 -43
- package/src/panels/material/MaterialPicker.tsx +0 -172
- package/src/panels/timeline/SceneStrip.tsx +0 -145
- package/src/panels/toolbar/AddMenu.tsx +0 -230
- package/src/stage/MoveGizmo.tsx +0 -411
|
@@ -0,0 +1,861 @@
|
|
|
1
|
+
// The node canvas: React Flow's props and handlers, and nothing else.
|
|
2
|
+
//
|
|
3
|
+
// Everything this file used to also be now lives beside it — the graph-to-flow
|
|
4
|
+
// conversion in `flowModel.ts`, the socket vocabulary in `socketGlyph.ts`, the
|
|
5
|
+
// pan memory in `viewportMemory.ts`, the thumbnails in `previewScheduler.ts`,
|
|
6
|
+
// the four renderers in their own files. What is left is the wiring, which is
|
|
7
|
+
// the part that has to read as one page.
|
|
8
|
+
//
|
|
9
|
+
// ── THE THREE RULES THIS FILE IS HELD TO ────────────────────────────────
|
|
10
|
+
//
|
|
11
|
+
// NO EDIT HAPPENS HERE. Every handler ends at a store verb, so a gesture is
|
|
12
|
+
// one transaction, one undo entry and one refusal route. v1 built the next
|
|
13
|
+
// graph inline and handed it to `onUpdate`, which is how "connect" ended up
|
|
14
|
+
// with its own duplicate-edge rule that the validator did not share.
|
|
15
|
+
//
|
|
16
|
+
// NO VALIDATOR PER POINTER FRAME. `isValidConnection` fires on every hover
|
|
17
|
+
// while a wire is in hand; v1 ran `validateMaterialGraph` over the WHOLE
|
|
18
|
+
// graph inside it, which is O(nodes + edges) per mouse move and was
|
|
19
|
+
// measurably the slowest thing in the lane. It asks the memoised type index
|
|
20
|
+
// one question now. The problem LIST still runs the full validator, once per
|
|
21
|
+
// graph, in a `useMemo`.
|
|
22
|
+
//
|
|
23
|
+
// THE KEYMAP OWNS THE KEYS. This canvas has no `onKeyDown` of its own. It is
|
|
24
|
+
// focusable and it carries the `.material-graph-canvas` class, which is the
|
|
25
|
+
// whole of its contract with `state/keymap.ts` — that class under the event
|
|
26
|
+
// target is what puts the resolver in the graph lane. A local handler here
|
|
27
|
+
// was a second binding site for Delete, and the two disagreed about whether
|
|
28
|
+
// an edge counted as a selection.
|
|
29
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
30
|
+
import { createPortal } from "react-dom";
|
|
31
|
+
import {
|
|
32
|
+
Background,
|
|
33
|
+
MiniMap,
|
|
34
|
+
ReactFlow,
|
|
35
|
+
ReactFlowProvider,
|
|
36
|
+
SelectionMode,
|
|
37
|
+
useNodesInitialized,
|
|
38
|
+
useReactFlow,
|
|
39
|
+
useStore as useFlowStore,
|
|
40
|
+
} from "@xyflow/react";
|
|
41
|
+
import { IconMinus, IconPlus, LockedControls } from "@dailies-studio/ui";
|
|
42
|
+
import type {
|
|
43
|
+
Connection,
|
|
44
|
+
Edge,
|
|
45
|
+
EdgeChange,
|
|
46
|
+
EdgeMouseHandler,
|
|
47
|
+
FinalConnectionState,
|
|
48
|
+
NodeChange,
|
|
49
|
+
NodeMouseHandler,
|
|
50
|
+
OnNodeDrag,
|
|
51
|
+
Viewport,
|
|
52
|
+
} from "@xyflow/react";
|
|
53
|
+
import {
|
|
54
|
+
indexMaterialGraphTypes,
|
|
55
|
+
materialGraphPixelSignature,
|
|
56
|
+
materialGraphSemanticSignature,
|
|
57
|
+
validateMaterialGraph,
|
|
58
|
+
} from "@dailies-studio/shot/doc";
|
|
59
|
+
import type { MaterialGraph3D } from "@dailies-studio/shot/doc";
|
|
60
|
+
import { useShotEditor, useStore } from "../../state/context.js";
|
|
61
|
+
import { MATERIAL_GRAPH_GRID } from "../../state/store.js";
|
|
62
|
+
import {
|
|
63
|
+
buildMaterialFlow,
|
|
64
|
+
graphPositionOf,
|
|
65
|
+
groupUnderPoint,
|
|
66
|
+
nodesInScope,
|
|
67
|
+
segmentIntersectsRect,
|
|
68
|
+
} from "./flowModel.js";
|
|
69
|
+
import type { MaterialFlowNode } from "./flowModel.js";
|
|
70
|
+
import { MaterialConnectProvider, MaterialNodeCard } from "./MaterialNode.js";
|
|
71
|
+
import { MaterialGroupNode } from "./MaterialGroupNode.js";
|
|
72
|
+
import { MaterialCommentNode } from "./MaterialCommentNode.js";
|
|
73
|
+
import { MaterialRerouteNode } from "./MaterialRerouteNode.js";
|
|
74
|
+
import { MaterialNodePalette } from "./MaterialNodePalette.js";
|
|
75
|
+
import { useMaterialGraphMenu } from "./MaterialGraphMenu.js";
|
|
76
|
+
import { MaterialPreviewProvider, NODE_PREVIEW_PX } from "./NodePreview.js";
|
|
77
|
+
import { createPreviewScheduler, visiblePreviewNodes } from "./previewScheduler.js";
|
|
78
|
+
import type { PreviewEntry } from "./previewScheduler.js";
|
|
79
|
+
import { materialViewportKey, recallMaterialViewport, rememberMaterialViewport } from "./viewportMemory.js";
|
|
80
|
+
// React Flow's own sheet, in this chunk with the canvas that needs it.
|
|
81
|
+
import "./reactFlow.css";
|
|
82
|
+
|
|
83
|
+
const NODE_TYPES = {
|
|
84
|
+
material: MaterialNodeCard,
|
|
85
|
+
materialGroup: MaterialGroupNode,
|
|
86
|
+
materialComment: MaterialCommentNode,
|
|
87
|
+
materialReroute: MaterialRerouteNode,
|
|
88
|
+
} as const;
|
|
89
|
+
|
|
90
|
+
const FIT_VIEW = { padding: 0.2, maxZoom: 1 } as const;
|
|
91
|
+
const NO_PREVIEWS: ReadonlyMap<string, PreviewEntry> = new Map();
|
|
92
|
+
|
|
93
|
+
/** Fallback card size for the two drag hit tests, used only until React Flow
|
|
94
|
+
* has measured a card. Same nominal box `MATERIAL_NODE_CARD` names in the
|
|
95
|
+
* store, and for the same reason: a hit test may not depend on the zoom. */
|
|
96
|
+
const MATERIAL_CARD_W = 180;
|
|
97
|
+
const MATERIAL_CARD_H = 96;
|
|
98
|
+
|
|
99
|
+
export interface MaterialGraphCanvasProps {
|
|
100
|
+
/** The graph the workspace resolved THROUGH THE SLOT. Passed rather than
|
|
101
|
+
* looked up, so the canvas cannot disagree with the header about which
|
|
102
|
+
* surface is on screen. */
|
|
103
|
+
graph: MaterialGraph3D;
|
|
104
|
+
materialId: string;
|
|
105
|
+
layerId: string;
|
|
106
|
+
slot: number | null;
|
|
107
|
+
/** Where the graph's zoom goes: a place in the canvas bar the workspace
|
|
108
|
+
* keeps for it (MaterialWorkspace.tsx). Absent or null, no zoom is shown. */
|
|
109
|
+
zoomSlot?: HTMLElement | null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function MaterialGraphCanvas(props: MaterialGraphCanvasProps) {
|
|
113
|
+
// A viewer's cards show every field in its own locked state. The zoom is a
|
|
114
|
+
// view control, so it stays outside.
|
|
115
|
+
const readOnly = useShotEditor((state) => state.readOnly);
|
|
116
|
+
return (
|
|
117
|
+
<ReactFlowProvider>
|
|
118
|
+
<LockedControls value={readOnly}>
|
|
119
|
+
<MaterialGraphCanvasInner {...props} />
|
|
120
|
+
</LockedControls>
|
|
121
|
+
{/* INSIDE THE PROVIDER, and portalled out to the canvas bar. The zoom
|
|
122
|
+
drives this flow's own viewport, which only a component under the
|
|
123
|
+
provider can reach; the portal keeps it in this subtree while its
|
|
124
|
+
buttons sit in the bar with the rest of the mode's verbs. */}
|
|
125
|
+
{props.zoomSlot == null ? null : createPortal(<GraphZoom />, props.zoomSlot)}
|
|
126
|
+
</ReactFlowProvider>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* The graph's zoom, in the canvas bar: out, the percentage, in, Fit.
|
|
132
|
+
*
|
|
133
|
+
* It replaces React Flow's own `<Controls>` island, which floated over the
|
|
134
|
+
* graph's corner in a second look. The readout subscribes to the ZOOM alone,
|
|
135
|
+
* not the viewport, so a pan does not re-render it.
|
|
136
|
+
*/
|
|
137
|
+
function GraphZoom() {
|
|
138
|
+
const flow = useReactFlow();
|
|
139
|
+
const zoom = useFlowStore((flowState) => flowState.transform[2]);
|
|
140
|
+
return (
|
|
141
|
+
<div className="zoom-controls" role="group" aria-label="Graph zoom" data-testid="material-zoom">
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
className="btn is-icon"
|
|
145
|
+
aria-label="Zoom the graph out"
|
|
146
|
+
title="Zoom the graph out"
|
|
147
|
+
onClick={() => void flow.zoomOut()}
|
|
148
|
+
>
|
|
149
|
+
<IconMinus />
|
|
150
|
+
</button>
|
|
151
|
+
<span className="zoom-readout" data-testid="material-zoom-level">
|
|
152
|
+
{Math.round(zoom * 100)}%
|
|
153
|
+
</span>
|
|
154
|
+
<button
|
|
155
|
+
type="button"
|
|
156
|
+
className="btn is-icon"
|
|
157
|
+
aria-label="Zoom the graph in"
|
|
158
|
+
title="Zoom the graph in"
|
|
159
|
+
onClick={() => void flow.zoomIn()}
|
|
160
|
+
>
|
|
161
|
+
<IconPlus />
|
|
162
|
+
</button>
|
|
163
|
+
{/* Named for its subject: the dock head under the graph has a "Fit" of
|
|
164
|
+
its own, for the timeline. */}
|
|
165
|
+
<button
|
|
166
|
+
type="button"
|
|
167
|
+
className="btn"
|
|
168
|
+
aria-label="Fit the graph"
|
|
169
|
+
title="Fit the whole graph (Home or F over the graph)"
|
|
170
|
+
onClick={() => void flow.fitView(FIT_VIEW)}
|
|
171
|
+
>
|
|
172
|
+
Fit
|
|
173
|
+
</button>
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function MaterialGraphCanvasInner({ graph, materialId, layerId, slot }: MaterialGraphCanvasProps) {
|
|
179
|
+
const store = useStore();
|
|
180
|
+
const flow = useReactFlow<MaterialFlowNode, Edge>();
|
|
181
|
+
const initialised = useNodesInitialized();
|
|
182
|
+
const menu = useMaterialGraphMenu();
|
|
183
|
+
const openMenu = menu.open;
|
|
184
|
+
const surface = useRef<HTMLElement | null>(null);
|
|
185
|
+
|
|
186
|
+
const assets = useShotEditor((state) => state.doc?.assets);
|
|
187
|
+
const path = useShotEditor((state) => state.materialGraphPath);
|
|
188
|
+
const selection = useShotEditor((state) => state.materialGraphSelection);
|
|
189
|
+
const palette = useShotEditor((state) => state.materialPalette);
|
|
190
|
+
const snapToGrid = useShotEditor((state) => state.materialSnapToGrid);
|
|
191
|
+
// A VIEWER reads the graph: no node moves and no wire is drawn.
|
|
192
|
+
const readOnly = useShotEditor((state) => state.readOnly);
|
|
193
|
+
const frameRequest = useShotEditor((state) => state.materialFrameRequest);
|
|
194
|
+
|
|
195
|
+
/** Positions while a drag is live. React Flow needs the cards to follow the
|
|
196
|
+
* pointer, and the DOCUMENT must not: sixty writes per drag is sixty undo
|
|
197
|
+
* entries. Committed once at drag stop, through `moveMaterialNodes`. */
|
|
198
|
+
const [draft, setDraft] = useState<Readonly<Record<string, { x: number; y: number }>> | null>(null);
|
|
199
|
+
const [previews, setPreviews] = useState<ReadonlyMap<string, PreviewEntry>>(NO_PREVIEWS);
|
|
200
|
+
/** What the live drag would do on release: the wire it would be spliced into,
|
|
201
|
+
* and the group box it would join. Both are ANSWERS on screen, never edits —
|
|
202
|
+
* the document learns about either only at `onNodeDragStop`. */
|
|
203
|
+
const [spliceEdge, setSpliceEdge] = useState<string | null>(null);
|
|
204
|
+
const [dropGroup, setDropGroup] = useState<string | null>(null);
|
|
205
|
+
/** Box sizes while a resize is live, for the reason `draft` exists: the flow
|
|
206
|
+
* is controlled, so nothing follows the handle unless this does. */
|
|
207
|
+
const [draftSize, setDraftSize] = useState<Readonly<
|
|
208
|
+
Record<string, { width: number; height: number }>
|
|
209
|
+
> | null>(null);
|
|
210
|
+
|
|
211
|
+
const types = useMemo(() => indexMaterialGraphTypes(graph), [graph]);
|
|
212
|
+
const problems = useMemo(() => validateMaterialGraph(graph, assets ?? {}), [graph, assets]);
|
|
213
|
+
const built = useMemo(
|
|
214
|
+
() =>
|
|
215
|
+
buildMaterialFlow({
|
|
216
|
+
graph,
|
|
217
|
+
materialId,
|
|
218
|
+
path,
|
|
219
|
+
assets: assets ?? {},
|
|
220
|
+
selection,
|
|
221
|
+
problems,
|
|
222
|
+
types,
|
|
223
|
+
}),
|
|
224
|
+
[graph, materialId, path, assets, selection, problems, types],
|
|
225
|
+
);
|
|
226
|
+
// The draft is spliced in AFTER the memo, so a drag re-renders the cards it
|
|
227
|
+
// moves and leaves every other card's object identity alone. The drop
|
|
228
|
+
// highlight rides the same splice, for the same reason.
|
|
229
|
+
const nodes = useMemo(
|
|
230
|
+
() =>
|
|
231
|
+
draft === null && dropGroup === null && draftSize === null
|
|
232
|
+
? built.nodes
|
|
233
|
+
: built.nodes.map((node) => {
|
|
234
|
+
const at = draft?.[node.id];
|
|
235
|
+
const box = draftSize?.[node.id];
|
|
236
|
+
const marked = node.id === dropGroup;
|
|
237
|
+
if (at === undefined && box === undefined && !marked) return node;
|
|
238
|
+
return {
|
|
239
|
+
...node,
|
|
240
|
+
...(at === undefined ? {} : { position: at }),
|
|
241
|
+
...(box === undefined ? {} : { style: { ...node.style, ...box } }),
|
|
242
|
+
...(marked ? { className: [node.className, "is-drop-target"].filter(Boolean).join(" ") } : {}),
|
|
243
|
+
};
|
|
244
|
+
}),
|
|
245
|
+
[built.nodes, draft, dropGroup, draftSize],
|
|
246
|
+
);
|
|
247
|
+
// Only the one wire changes identity, so a pointer frame during a drag
|
|
248
|
+
// re-renders one edge rather than all of them.
|
|
249
|
+
const edges = useMemo(
|
|
250
|
+
() =>
|
|
251
|
+
spliceEdge === null
|
|
252
|
+
? built.edges
|
|
253
|
+
: built.edges.map((edge) =>
|
|
254
|
+
edge.id === spliceEdge
|
|
255
|
+
? { ...edge, className: `${edge.className ?? ""} is-splice-target`.trim() }
|
|
256
|
+
: edge,
|
|
257
|
+
),
|
|
258
|
+
[built.edges, spliceEdge],
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const canConnect = useCallback(
|
|
262
|
+
(source: { nodeId: string; socket: string }, target: { nodeId: string; socket: string }): boolean =>
|
|
263
|
+
types.canConnect(source, target).ok,
|
|
264
|
+
[types],
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
// ── viewport memory ──────────────────────────────────────────────────────
|
|
268
|
+
const viewportKey = materialViewportKey(materialId, path);
|
|
269
|
+
const restored = useRef<string | null>(null);
|
|
270
|
+
useEffect(() => {
|
|
271
|
+
// React Flow renders nothing until it has measured its container, and it
|
|
272
|
+
// reports a zero-size viewport until then — restoring into that lands the
|
|
273
|
+
// graph off screen with no way back but the frame key.
|
|
274
|
+
if (!initialised || restored.current === viewportKey) return;
|
|
275
|
+
restored.current = viewportKey;
|
|
276
|
+
const remembered = recallMaterialViewport(viewportKey);
|
|
277
|
+
if (remembered === null) void flow.fitView(FIT_VIEW);
|
|
278
|
+
else void flow.setViewport(remembered);
|
|
279
|
+
}, [initialised, viewportKey, flow]);
|
|
280
|
+
|
|
281
|
+
// The FRAME mailbox: a fresh identity per ask, like `appMenu`, because only
|
|
282
|
+
// this component knows its own viewport.
|
|
283
|
+
const framed = useRef(frameRequest);
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
if (frameRequest === null || framed.current === frameRequest) {
|
|
286
|
+
framed.current = frameRequest;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
framed.current = frameRequest;
|
|
290
|
+
void flow.fitView(FIT_VIEW);
|
|
291
|
+
}, [frameRequest, flow]);
|
|
292
|
+
|
|
293
|
+
const rememberNow = useCallback(
|
|
294
|
+
(viewport: Viewport) => rememberMaterialViewport(materialViewportKey(materialId, path), viewport),
|
|
295
|
+
[materialId, path],
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
// ── node previews ────────────────────────────────────────────────────────
|
|
299
|
+
const scheduler = useMemo(
|
|
300
|
+
() =>
|
|
301
|
+
createPreviewScheduler({
|
|
302
|
+
request: async (request) => (await store.stage()?.requestNodePreviews(request)) ?? null,
|
|
303
|
+
onChange: (entries) => setPreviews(entries),
|
|
304
|
+
}),
|
|
305
|
+
[store],
|
|
306
|
+
);
|
|
307
|
+
useEffect(() => () => scheduler.dispose(), [scheduler]);
|
|
308
|
+
|
|
309
|
+
const askForPreviews = useCallback(() => {
|
|
310
|
+
const box = surface.current?.getBoundingClientRect();
|
|
311
|
+
// INSIDE A FUNCTION, NOTHING IS PREVIEWED. A function body compiles with
|
|
312
|
+
// its call site's arguments bound, so a thumbnail of a node in there would
|
|
313
|
+
// be a picture of one call among several — v2 is main-scope only, and a
|
|
314
|
+
// wrong picture is worse than none.
|
|
315
|
+
if (box === undefined || path.length > 0) {
|
|
316
|
+
scheduler.ask(null);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const wanted = visiblePreviewNodes(nodesInScope(graph, path), flow.getViewport(), box);
|
|
320
|
+
if (wanted.length === 0) {
|
|
321
|
+
scheduler.ask(null);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
scheduler.ask({
|
|
325
|
+
materialId,
|
|
326
|
+
layerId,
|
|
327
|
+
slot,
|
|
328
|
+
nodeIds: wanted,
|
|
329
|
+
size: Math.round(NODE_PREVIEW_PX * (window.devicePixelRatio || 1)),
|
|
330
|
+
pixelSignature: materialGraphPixelSignature(graph),
|
|
331
|
+
semanticSignature: materialGraphSemanticSignature(graph),
|
|
332
|
+
});
|
|
333
|
+
}, [graph, path, materialId, layerId, slot, flow, scheduler]);
|
|
334
|
+
|
|
335
|
+
useEffect(() => {
|
|
336
|
+
askForPreviews();
|
|
337
|
+
}, [askForPreviews, initialised]);
|
|
338
|
+
|
|
339
|
+
// ── handlers ─────────────────────────────────────────────────────────────
|
|
340
|
+
/**
|
|
341
|
+
* The store's selection, written from one place.
|
|
342
|
+
*
|
|
343
|
+
* ── WHY THIS IS THE CHANGE STREAM AND NOT `onSelectionChange` ─────────
|
|
344
|
+
*
|
|
345
|
+
* In a CONTROLLED flow — one where `nodes` is a prop, which is the only kind
|
|
346
|
+
* this canvas can be — React Flow does not apply a selection itself. Look at
|
|
347
|
+
* `triggerNodeChanges`: it applies changes only when it owns the nodes, and
|
|
348
|
+
* otherwise emits them and waits for the owner to write them back. So
|
|
349
|
+
* `onSelectionChange` fires from React Flow's LOOKUP, which is filled from
|
|
350
|
+
* our `nodes` prop, which is filled from the store — it can only ever echo
|
|
351
|
+
* what we already wrote. Wiring the store to it makes clicking a card do
|
|
352
|
+
* precisely nothing, which is what it did.
|
|
353
|
+
*
|
|
354
|
+
* So the change stream is the writer, and it is still exactly one: both
|
|
355
|
+
* halves land in `setMaterialGraphSelection`, which is identity-stable when
|
|
356
|
+
* the answer has not moved.
|
|
357
|
+
*/
|
|
358
|
+
const writeSelection = useCallback(
|
|
359
|
+
(next: { nodeIds?: readonly string[]; edgeIds?: readonly string[] }) => {
|
|
360
|
+
// The half that did not change is re-read LIVE rather than closed over:
|
|
361
|
+
// one gesture emits node changes and edge changes separately, and a
|
|
362
|
+
// stale read would make the second undo the first.
|
|
363
|
+
const current = store.getState().materialGraphSelection;
|
|
364
|
+
store.setMaterialGraphSelection({
|
|
365
|
+
nodeIds: next.nodeIds ?? current.nodeIds,
|
|
366
|
+
edgeIds: next.edgeIds ?? current.edgeIds,
|
|
367
|
+
});
|
|
368
|
+
},
|
|
369
|
+
[store],
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
const onNodesChange = useCallback(
|
|
373
|
+
(changes: NodeChange<MaterialFlowNode>[]) => {
|
|
374
|
+
// POSITION, SELECT and a LIVE RESIZE. Removal is impossible because
|
|
375
|
+
// `deleteKeyCode` is null — the keymap owns Delete.
|
|
376
|
+
let moved: Record<string, { x: number; y: number }> | null = null;
|
|
377
|
+
let picked: Set<string> | null = null;
|
|
378
|
+
let sized: Record<string, { width: number; height: number }> | null = null;
|
|
379
|
+
let settled = false;
|
|
380
|
+
for (const change of changes) {
|
|
381
|
+
if (change.type === "position" && change.dragging === true && change.position !== undefined) {
|
|
382
|
+
moved ??= {};
|
|
383
|
+
moved[change.id] = change.position;
|
|
384
|
+
} else if (change.type === "select") {
|
|
385
|
+
picked ??= new Set(store.getState().materialGraphSelection.nodeIds);
|
|
386
|
+
if (change.selected) picked.add(change.id);
|
|
387
|
+
else picked.delete(change.id);
|
|
388
|
+
} else if (change.type === "dimensions" && change.dimensions !== undefined) {
|
|
389
|
+
// THE SAME BARGAIN THE POSITION DRAFT MAKES. The flow is controlled,
|
|
390
|
+
// so React Flow cannot resize a box on its own; without this the
|
|
391
|
+
// corner handle would drag against a card that never moved. Held
|
|
392
|
+
// locally until `onResizeEnd` commits, so a resize is one entry and
|
|
393
|
+
// not one per frame.
|
|
394
|
+
if (change.resizing === true) {
|
|
395
|
+
sized ??= {};
|
|
396
|
+
sized[change.id] = change.dimensions;
|
|
397
|
+
} else if (change.resizing === false) settled = true;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (picked !== null) writeSelection({ nodeIds: [...picked] });
|
|
401
|
+
// The commit has landed in the document, so the local override must go or
|
|
402
|
+
// it would outrank it.
|
|
403
|
+
if (settled) setDraftSize(null);
|
|
404
|
+
if (sized !== null) {
|
|
405
|
+
const next = sized;
|
|
406
|
+
setDraftSize((current) => ({ ...(current ?? {}), ...next }));
|
|
407
|
+
}
|
|
408
|
+
if (moved === null) return;
|
|
409
|
+
const next = moved;
|
|
410
|
+
setDraft((current) => ({ ...(current ?? {}), ...next }));
|
|
411
|
+
},
|
|
412
|
+
[store, writeSelection],
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
const onEdgesChange = useCallback(
|
|
416
|
+
(changes: EdgeChange<Edge>[]) => {
|
|
417
|
+
let picked: Set<string> | null = null;
|
|
418
|
+
for (const change of changes) {
|
|
419
|
+
if (change.type !== "select") continue;
|
|
420
|
+
picked ??= new Set(store.getState().materialGraphSelection.edgeIds);
|
|
421
|
+
if (change.selected) picked.add(change.id);
|
|
422
|
+
else picked.delete(change.id);
|
|
423
|
+
}
|
|
424
|
+
if (picked !== null) writeSelection({ edgeIds: [...picked] });
|
|
425
|
+
},
|
|
426
|
+
[store, writeSelection],
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* WHAT THE DRAG WOULD DO IF IT ENDED NOW — the wire it would splice into and
|
|
431
|
+
* the box it would join. Live state, so the answer is on screen before the
|
|
432
|
+
* author commits to it; both are recomputed from final positions at drop.
|
|
433
|
+
*/
|
|
434
|
+
const onNodeDrag = useCallback<OnNodeDrag<MaterialFlowNode>>(
|
|
435
|
+
(_event, dragged) => {
|
|
436
|
+
// SPLICE NEEDS ONE CARD WITH TWO ENDS. Dragging a selection says nothing
|
|
437
|
+
// about which card goes in the wire, and a node with no inputs or no
|
|
438
|
+
// outputs cannot sit in the middle of one.
|
|
439
|
+
const alone = draft === null || Object.keys(draft).length === 1;
|
|
440
|
+
const data = dragged.data;
|
|
441
|
+
const spliceable =
|
|
442
|
+
alone &&
|
|
443
|
+
(dragged.type === "material" || dragged.type === "materialReroute") &&
|
|
444
|
+
data.inputs.length > 0 &&
|
|
445
|
+
data.outputs.length > 0;
|
|
446
|
+
|
|
447
|
+
let hit: string | null = null;
|
|
448
|
+
if (spliceable) {
|
|
449
|
+
// Which of this card's inputs are already wired. Built once per pointer
|
|
450
|
+
// Which of this card's inputs are already wired, so the highlight
|
|
451
|
+
// promises only splices `dropMaterialNodes` will actually make. Built
|
|
452
|
+
// once per pointer frame rather than once per candidate wire.
|
|
453
|
+
const filled = new Set(
|
|
454
|
+
graph.edges
|
|
455
|
+
.filter((candidate) => candidate.target.nodeId === dragged.id)
|
|
456
|
+
.map((candidate) => `${candidate.target.nodeId}\u0000${candidate.target.socket}`),
|
|
457
|
+
);
|
|
458
|
+
const self = flow.getInternalNode(dragged.id);
|
|
459
|
+
const box = self?.measured;
|
|
460
|
+
if (self !== undefined && box?.width != null && box.height != null) {
|
|
461
|
+
const rect = {
|
|
462
|
+
x: self.internals.positionAbsolute.x,
|
|
463
|
+
y: self.internals.positionAbsolute.y,
|
|
464
|
+
width: box.width,
|
|
465
|
+
height: box.height,
|
|
466
|
+
};
|
|
467
|
+
for (const edge of built.edges) {
|
|
468
|
+
if (edge.source === dragged.id || edge.target === dragged.id) continue;
|
|
469
|
+
const from = flow.getInternalNode(edge.source);
|
|
470
|
+
const to = flow.getInternalNode(edge.target);
|
|
471
|
+
if (from === undefined || to === undefined) continue;
|
|
472
|
+
// ponytail: the wire is approximated as a straight segment between
|
|
473
|
+
// the two cards' facing edges. Edges are drawn smoothstep, so a
|
|
474
|
+
// sharply bent one can be reported a few pixels off. Upgrade by
|
|
475
|
+
// sampling `getSmoothStepPath` if it ever reads wrong.
|
|
476
|
+
const a = {
|
|
477
|
+
x: from.internals.positionAbsolute.x + (from.measured?.width ?? 0),
|
|
478
|
+
y: from.internals.positionAbsolute.y + (from.measured?.height ?? 0) / 2,
|
|
479
|
+
};
|
|
480
|
+
const b = {
|
|
481
|
+
x: to.internals.positionAbsolute.x,
|
|
482
|
+
y: to.internals.positionAbsolute.y + (to.measured?.height ?? 0) / 2,
|
|
483
|
+
};
|
|
484
|
+
if (!segmentIntersectsRect(a, b, rect)) continue;
|
|
485
|
+
// CROSSING IS NOT ENOUGH — it has to be a splice the graph would
|
|
486
|
+
// accept. Two type questions against the memoised index, which is
|
|
487
|
+
// the same budget `isValidConnection` spends per hover.
|
|
488
|
+
const source = { nodeId: edge.source, socket: edge.sourceHandle ?? "value" };
|
|
489
|
+
const target = { nodeId: edge.target, socket: edge.targetHandle ?? "value" };
|
|
490
|
+
// A FREE input, matching `dropMaterialNodes` exactly — the
|
|
491
|
+
// highlight must promise the edit the drop will actually make, and
|
|
492
|
+
// an already-wired input is one this gesture will not take.
|
|
493
|
+
const takes = data.inputs.some(
|
|
494
|
+
(socket) =>
|
|
495
|
+
!filled.has(`${dragged.id}\u0000${socket.id}`) &&
|
|
496
|
+
types.canConnect(source, { nodeId: dragged.id, socket: socket.id }).ok,
|
|
497
|
+
);
|
|
498
|
+
const gives = data.outputs.some(
|
|
499
|
+
(socket) => types.canConnect({ nodeId: dragged.id, socket: socket.id }, target).ok,
|
|
500
|
+
);
|
|
501
|
+
if (takes && gives) {
|
|
502
|
+
hit = edge.id;
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
setSpliceEdge(hit);
|
|
509
|
+
|
|
510
|
+
// THE BOX, for any dragged card. A group cannot go inside a group, and a
|
|
511
|
+
// card's CENTRE decides — using a corner would join a box the author only
|
|
512
|
+
// brushed the edge of.
|
|
513
|
+
const centre = graphPositionOf(
|
|
514
|
+
{ position: dragged.position, parentId: dragged.parentId },
|
|
515
|
+
graph,
|
|
516
|
+
);
|
|
517
|
+
const size = dragged.measured;
|
|
518
|
+
setDropGroup(
|
|
519
|
+
dragged.type === "materialGroup"
|
|
520
|
+
? null
|
|
521
|
+
: groupUnderPoint(graph, path, [
|
|
522
|
+
centre[0] + (size?.width ?? MATERIAL_CARD_W) / 2,
|
|
523
|
+
centre[1] + (size?.height ?? MATERIAL_CARD_H) / 2,
|
|
524
|
+
]),
|
|
525
|
+
);
|
|
526
|
+
},
|
|
527
|
+
[draft, flow, built.edges, types, graph, path],
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
const onNodeDragStop = useCallback(() => {
|
|
531
|
+
const held = draft;
|
|
532
|
+
const splicing = spliceEdge;
|
|
533
|
+
setDraft(null);
|
|
534
|
+
setSpliceEdge(null);
|
|
535
|
+
setDropGroup(null);
|
|
536
|
+
if (held === null) return;
|
|
537
|
+
const positions: Record<string, [number, number]> = {};
|
|
538
|
+
const membership: Record<string, string | null> = {};
|
|
539
|
+
let single: string | null = null;
|
|
540
|
+
for (const [id, at] of Object.entries(held)) {
|
|
541
|
+
const node = built.nodes.find((candidate) => candidate.id === id);
|
|
542
|
+
if (node === undefined) continue;
|
|
543
|
+
const landed = graphPositionOf({ position: at, parentId: node.parentId }, graph);
|
|
544
|
+
positions[id] = landed;
|
|
545
|
+
single = single === null ? id : "";
|
|
546
|
+
if (node.type === "materialGroup") continue;
|
|
547
|
+
// RECOMPUTED, not read off the hover state: the last `onNodeDrag` fired
|
|
548
|
+
// wherever the pointer last MOVED, and a drop can land somewhere else.
|
|
549
|
+
// The document should agree with where the card actually is.
|
|
550
|
+
const size = node.measured;
|
|
551
|
+
const box = groupUnderPoint(graph, path, [
|
|
552
|
+
landed[0] + (size?.width ?? MATERIAL_CARD_W) / 2,
|
|
553
|
+
landed[1] + (size?.height ?? MATERIAL_CARD_H) / 2,
|
|
554
|
+
]);
|
|
555
|
+
const before = graph.nodesById[id]?.groupId ?? null;
|
|
556
|
+
if (box !== before) membership[id] = box;
|
|
557
|
+
}
|
|
558
|
+
// ONE CALL for the whole drag: twelve cards moved, whatever they joined,
|
|
559
|
+
// and the wire they were dropped across are one gesture and one undo entry.
|
|
560
|
+
store.dropMaterialNodes(materialId, path, positions, {
|
|
561
|
+
...(Object.keys(membership).length === 0 ? {} : { membership }),
|
|
562
|
+
...(splicing !== null && single !== null && single !== ""
|
|
563
|
+
? { splice: { nodeId: single, edgeId: splicing } }
|
|
564
|
+
: {}),
|
|
565
|
+
});
|
|
566
|
+
}, [draft, spliceEdge, built.nodes, graph, store, materialId, path]);
|
|
567
|
+
|
|
568
|
+
const onConnect = useCallback(
|
|
569
|
+
(connection: Connection) => {
|
|
570
|
+
if (connection.sourceHandle === null || connection.targetHandle === null) return;
|
|
571
|
+
store.connectMaterialSockets(
|
|
572
|
+
materialId,
|
|
573
|
+
path,
|
|
574
|
+
{ nodeId: connection.source, socket: connection.sourceHandle },
|
|
575
|
+
{ nodeId: connection.target, socket: connection.targetHandle },
|
|
576
|
+
);
|
|
577
|
+
},
|
|
578
|
+
[store, materialId, path],
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
/** Set by `onReconnect`, read by `onReconnectEnd`: React Flow's own way of
|
|
582
|
+
* telling "moved somewhere" from "let go over nothing", which is the
|
|
583
|
+
* gesture that means DISCONNECT. */
|
|
584
|
+
const reconnected = useRef(false);
|
|
585
|
+
|
|
586
|
+
const onConnectEnd = useCallback(
|
|
587
|
+
(event: MouseEvent | TouchEvent, connection: FinalConnectionState) => {
|
|
588
|
+
if (connection.isValid === true || connection.fromHandle?.id == null) return;
|
|
589
|
+
const point = "changedTouches" in event ? event.changedTouches[0] : event;
|
|
590
|
+
if (point === undefined) return;
|
|
591
|
+
const at = flow.screenToFlowPosition({ x: point.clientX, y: point.clientY });
|
|
592
|
+
// A WIRE DROPPED IN EMPTY SPACE IS A QUESTION, not a mistake: "what could
|
|
593
|
+
// take this?". The palette answers it filtered, and `addMaterialNode`
|
|
594
|
+
// draws the wire for whatever is picked.
|
|
595
|
+
store.openMaterialPalette(at, {
|
|
596
|
+
nodeId: connection.fromHandle.nodeId,
|
|
597
|
+
socket: connection.fromHandle.id,
|
|
598
|
+
side: connection.fromHandle.type === "source" ? "source" : "target",
|
|
599
|
+
type: socketTypeOf(connection.fromHandle.nodeId, connection.fromHandle.id, connection.fromHandle.type, built.nodes),
|
|
600
|
+
});
|
|
601
|
+
},
|
|
602
|
+
[flow, store, built.nodes],
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
const onNodeDoubleClick: NodeMouseHandler<MaterialFlowNode> = useCallback(
|
|
606
|
+
(_event, node) => {
|
|
607
|
+
if (node.data.node.kind !== "subgraph") return;
|
|
608
|
+
store.enterMaterialSubgraph(node.id);
|
|
609
|
+
},
|
|
610
|
+
[store],
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
const targetsAt = useCallback(
|
|
614
|
+
(event: { clientX: number; clientY: number }, over: { nodeIds?: string[]; edgeIds?: string[] }) => {
|
|
615
|
+
const at = flow.screenToFlowPosition({ x: event.clientX, y: event.clientY });
|
|
616
|
+
return {
|
|
617
|
+
materialId,
|
|
618
|
+
path,
|
|
619
|
+
nodeIds: over.nodeIds ?? [],
|
|
620
|
+
edgeIds: over.edgeIds ?? [],
|
|
621
|
+
at: { x: at.x, y: at.y },
|
|
622
|
+
};
|
|
623
|
+
},
|
|
624
|
+
[flow, materialId, path],
|
|
625
|
+
);
|
|
626
|
+
|
|
627
|
+
const onNodeContextMenu: NodeMouseHandler<MaterialFlowNode> = useCallback(
|
|
628
|
+
(event, node) => {
|
|
629
|
+
event.preventDefault();
|
|
630
|
+
// The press SELECTS what it landed on unless that was already part of the
|
|
631
|
+
// selection, which is the rule every other menu in this chrome follows:
|
|
632
|
+
// right-clicking outside a selection is a new subject, inside it is the
|
|
633
|
+
// whole of it.
|
|
634
|
+
const nodeIds = selection.nodeIds.includes(node.id) ? [...selection.nodeIds] : [node.id];
|
|
635
|
+
if (!selection.nodeIds.includes(node.id)) {
|
|
636
|
+
store.setMaterialGraphSelection({ nodeIds, edgeIds: [] });
|
|
637
|
+
}
|
|
638
|
+
openMenu({ x: event.clientX, y: event.clientY }, targetsAt(event, { nodeIds }));
|
|
639
|
+
},
|
|
640
|
+
[openMenu, selection.nodeIds, store, targetsAt],
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
const onEdgeContextMenu: EdgeMouseHandler<Edge> = useCallback(
|
|
644
|
+
(event, edge) => {
|
|
645
|
+
event.preventDefault();
|
|
646
|
+
const edgeIds = selection.edgeIds.includes(edge.id) ? [...selection.edgeIds] : [edge.id];
|
|
647
|
+
if (!selection.edgeIds.includes(edge.id)) {
|
|
648
|
+
store.setMaterialGraphSelection({ nodeIds: [], edgeIds });
|
|
649
|
+
}
|
|
650
|
+
openMenu({ x: event.clientX, y: event.clientY }, targetsAt(event, { edgeIds }));
|
|
651
|
+
},
|
|
652
|
+
[openMenu, selection.edgeIds, store, targetsAt],
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
const isValidConnection = useCallback(
|
|
656
|
+
(connection: Connection | Edge): boolean => {
|
|
657
|
+
const sourceHandle = connection.sourceHandle ?? null;
|
|
658
|
+
const targetHandle = connection.targetHandle ?? null;
|
|
659
|
+
if (sourceHandle === null || targetHandle === null) return false;
|
|
660
|
+
return types.canConnect(
|
|
661
|
+
{ nodeId: connection.source, socket: sourceHandle },
|
|
662
|
+
{ nodeId: connection.target, socket: targetHandle },
|
|
663
|
+
).ok;
|
|
664
|
+
},
|
|
665
|
+
[types],
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
// Where the palette sits, and where what it adds lands. The request carries a
|
|
669
|
+
// GRAPH point (or null for the keyboard route, which has no pointer to ask),
|
|
670
|
+
// and only this component can turn one into a screen point.
|
|
671
|
+
const paletteAt = useMemo(() => {
|
|
672
|
+
if (palette === null) return null;
|
|
673
|
+
const box = surface.current?.getBoundingClientRect();
|
|
674
|
+
if (palette.at !== null) {
|
|
675
|
+
const screen = flow.flowToScreenPosition(palette.at);
|
|
676
|
+
return {
|
|
677
|
+
screenAt: { x: screen.x - (box?.left ?? 0), y: screen.y - (box?.top ?? 0) },
|
|
678
|
+
graphAt: [palette.at.x, palette.at.y] as const,
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
const centre = flow.screenToFlowPosition({
|
|
682
|
+
x: (box?.left ?? 0) + (box?.width ?? 0) / 2,
|
|
683
|
+
y: (box?.top ?? 0) + (box?.height ?? 0) / 2,
|
|
684
|
+
});
|
|
685
|
+
return {
|
|
686
|
+
screenAt: { x: (box?.width ?? 0) / 2, y: (box?.height ?? 0) / 2 },
|
|
687
|
+
graphAt: [centre.x, centre.y] as const,
|
|
688
|
+
};
|
|
689
|
+
}, [palette, flow]);
|
|
690
|
+
|
|
691
|
+
return (
|
|
692
|
+
<section
|
|
693
|
+
className="material-graph-canvas"
|
|
694
|
+
aria-label="Material node graph"
|
|
695
|
+
tabIndex={0}
|
|
696
|
+
ref={surface}
|
|
697
|
+
// FOCUS FOLLOWS THE PRESS, because `state/keymap.ts` decides it is in the
|
|
698
|
+
// graph lane by asking the event TARGET for `.material-graph-canvas`. A
|
|
699
|
+
// canvas nobody has focused sends its keys to `<body>` and the whole lane
|
|
700
|
+
// silently does not exist. Fields and buttons keep their own presses.
|
|
701
|
+
onPointerDownCapture={(event) => {
|
|
702
|
+
const target = event.target;
|
|
703
|
+
if (
|
|
704
|
+
target instanceof Element &&
|
|
705
|
+
target.closest("button, input, textarea, select, [contenteditable='true']") !== null
|
|
706
|
+
) {
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
event.currentTarget.focus({ preventScroll: true });
|
|
710
|
+
}}
|
|
711
|
+
>
|
|
712
|
+
<MaterialPreviewProvider entries={previews}>
|
|
713
|
+
<MaterialConnectProvider canConnect={canConnect}>
|
|
714
|
+
<ReactFlow<MaterialFlowNode, Edge>
|
|
715
|
+
nodes={nodes}
|
|
716
|
+
edges={edges}
|
|
717
|
+
nodeTypes={NODE_TYPES}
|
|
718
|
+
nodesDraggable={!readOnly}
|
|
719
|
+
nodesConnectable={!readOnly}
|
|
720
|
+
edgesReconnectable={!readOnly}
|
|
721
|
+
onNodesChange={onNodesChange}
|
|
722
|
+
onEdgesChange={onEdgesChange}
|
|
723
|
+
onNodeDrag={onNodeDrag}
|
|
724
|
+
onNodeDragStop={onNodeDragStop}
|
|
725
|
+
onConnect={onConnect}
|
|
726
|
+
onConnectEnd={onConnectEnd}
|
|
727
|
+
isValidConnection={isValidConnection}
|
|
728
|
+
onReconnectStart={() => {
|
|
729
|
+
reconnected.current = false;
|
|
730
|
+
}}
|
|
731
|
+
onReconnect={(oldEdge, connection) => {
|
|
732
|
+
if (connection.sourceHandle === null || connection.targetHandle === null) return;
|
|
733
|
+
reconnected.current = true;
|
|
734
|
+
store.reconnectMaterialEdge(materialId, path, oldEdge.id, {
|
|
735
|
+
source: { nodeId: connection.source, socket: connection.sourceHandle },
|
|
736
|
+
target: { nodeId: connection.target, socket: connection.targetHandle },
|
|
737
|
+
});
|
|
738
|
+
}}
|
|
739
|
+
onReconnectEnd={(_event, edge) => {
|
|
740
|
+
// LET GO OVER NOTHING IS A DISCONNECT. It is the gesture every
|
|
741
|
+
// node editor binds, and without it the only way to remove one
|
|
742
|
+
// wire is to select it and press Delete.
|
|
743
|
+
if (reconnected.current) return;
|
|
744
|
+
store.disconnectMaterialEdges(materialId, path, [edge.id]);
|
|
745
|
+
}}
|
|
746
|
+
onNodeDoubleClick={onNodeDoubleClick}
|
|
747
|
+
onNodeContextMenu={onNodeContextMenu}
|
|
748
|
+
onEdgeContextMenu={onEdgeContextMenu}
|
|
749
|
+
onPaneContextMenu={(event) => {
|
|
750
|
+
event.preventDefault();
|
|
751
|
+
openMenu({ x: event.clientX, y: event.clientY }, targetsAt(event, {}));
|
|
752
|
+
}}
|
|
753
|
+
onDoubleClick={(event) => {
|
|
754
|
+
// The PANE only. A double-click on a card is how a subgraph is
|
|
755
|
+
// entered, and on a field it is how a word is selected.
|
|
756
|
+
if (!(event.target instanceof Element)) return;
|
|
757
|
+
if (event.target.classList.contains("react-flow__pane") !== true) return;
|
|
758
|
+
const at = flow.screenToFlowPosition({ x: event.clientX, y: event.clientY });
|
|
759
|
+
store.openMaterialPalette({ x: at.x, y: at.y });
|
|
760
|
+
}}
|
|
761
|
+
onMoveEnd={(_event, viewport) => {
|
|
762
|
+
rememberNow(viewport);
|
|
763
|
+
askForPreviews();
|
|
764
|
+
}}
|
|
765
|
+
snapToGrid={snapToGrid}
|
|
766
|
+
snapGrid={[MATERIAL_GRAPH_GRID, MATERIAL_GRAPH_GRID]}
|
|
767
|
+
deleteKeyCode={null}
|
|
768
|
+
// SAME ROOT CAUSE AS THE BOARD'S, and here the skipped rung is
|
|
769
|
+
// worse. React Flow's a11y handler clears node selection on Escape
|
|
770
|
+
// synchronously, so `materialEscape` then read an empty selection
|
|
771
|
+
// and took its next rung, which closes the whole workspace. The
|
|
772
|
+
// ladder is meant to drop the selection first.
|
|
773
|
+
disableKeyboardA11y
|
|
774
|
+
// OFF, because a double-click on the pane opens the palette and
|
|
775
|
+
// d3-zoom's own dblclick handler calls `stopImmediatePropagation`
|
|
776
|
+
// — with it on, the native event never reaches React's delegated
|
|
777
|
+
// root and the palette silently never opened.
|
|
778
|
+
zoomOnDoubleClick={false}
|
|
779
|
+
// ── THE POINTER, AS EVERY OTHER CANVAS TOOL SPELLS IT ──────────
|
|
780
|
+
//
|
|
781
|
+
// Left-drag on empty space SELECTS; the middle and right buttons
|
|
782
|
+
// pan. React Flow's default is the other way round, which makes
|
|
783
|
+
// the commonest thing in a node graph — "take those four and move
|
|
784
|
+
// them" — the one gesture behind a modifier, while panning, which
|
|
785
|
+
// an author does with the wheel anyway, gets the whole left button.
|
|
786
|
+
// Figma, Blender's node editor and Unreal all read it this way, and
|
|
787
|
+
// an author arrives here with those in their fingers.
|
|
788
|
+
//
|
|
789
|
+
// Shift-drag still marquees, so the old habit is not punished.
|
|
790
|
+
selectionOnDrag
|
|
791
|
+
// TOUCHING counts, not enclosing. A long wire chain never fits in
|
|
792
|
+
// a rubber band drawn inside the viewport, so "fully contained"
|
|
793
|
+
// would mean zooming out before every multi-select.
|
|
794
|
+
selectionMode={SelectionMode.Partial}
|
|
795
|
+
// MIDDLE BUTTON ONLY, and deliberately not the right one. React
|
|
796
|
+
// Flow's `Pane.onContextMenu` opens with
|
|
797
|
+
// if (Array.isArray(panOnDrag) && panOnDrag.includes(2)) return;
|
|
798
|
+
// — an UNCONDITIONAL bail, not a "did this right press actually
|
|
799
|
+
// pan" test. Listing button 2 here would trade every right-click
|
|
800
|
+
// menu on this canvas (add node here, paste here, snap while
|
|
801
|
+
// dragging) for a second way to do what the middle button and
|
|
802
|
+
// Space already do. The menu is worth more.
|
|
803
|
+
panOnDrag={[1]}
|
|
804
|
+
// Hold-to-pan, and the contract with `state/keymap.ts`: the graph
|
|
805
|
+
// lane returns `null` for Space so nothing calls `preventDefault`
|
|
806
|
+
// and React Flow's own window listener sees the key.
|
|
807
|
+
panActivationKeyCode="Space"
|
|
808
|
+
// A HAND TREMOR IS NOT AN EDIT. The default is 1px, so brushing a
|
|
809
|
+
// card while clicking it committed a `moveMaterialNodes` — a real
|
|
810
|
+
// undo entry that restores a position nobody meant to change and
|
|
811
|
+
// buries the edit before it.
|
|
812
|
+
nodeDragThreshold={4}
|
|
813
|
+
// One socket-row pitch (rows are 24px), up from the 20px default:
|
|
814
|
+
// the closest handle inside the radius wins, so a wire dropped a
|
|
815
|
+
// little short still lands rather than opening the palette.
|
|
816
|
+
connectionRadius={24}
|
|
817
|
+
// …and the same idea at the other end: a click ON a socket is a
|
|
818
|
+
// click, not a one-pixel wire dropped back where it started.
|
|
819
|
+
connectionDragThreshold={3}
|
|
820
|
+
selectionKeyCode="Shift"
|
|
821
|
+
multiSelectionKeyCode={["Meta", "Control"]}
|
|
822
|
+
minZoom={0.15}
|
|
823
|
+
maxZoom={2.5}
|
|
824
|
+
proOptions={{ hideAttribution: true }}
|
|
825
|
+
>
|
|
826
|
+
<Background gap={MATERIAL_GRAPH_GRID} size={1} color="var(--material-grid-dot)" />
|
|
827
|
+
<MiniMap pannable zoomable position="bottom-right" ariaLabel="Material graph overview" />
|
|
828
|
+
</ReactFlow>
|
|
829
|
+
</MaterialConnectProvider>
|
|
830
|
+
</MaterialPreviewProvider>
|
|
831
|
+
{palette === null || paletteAt === null ? null : (
|
|
832
|
+
<MaterialNodePalette
|
|
833
|
+
materialId={materialId}
|
|
834
|
+
path={path}
|
|
835
|
+
request={palette}
|
|
836
|
+
screenAt={paletteAt.screenAt}
|
|
837
|
+
graphAt={paletteAt.graphAt}
|
|
838
|
+
/>
|
|
839
|
+
)}
|
|
840
|
+
{menu.menu}
|
|
841
|
+
</section>
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* The declared type of the socket a drag started from.
|
|
847
|
+
*
|
|
848
|
+
* Read off the flow nodes rather than the graph, because those already carry
|
|
849
|
+
* the PER-NODE sockets — a subgraph call's are its function's signature, and
|
|
850
|
+
* the static spec would say it has none.
|
|
851
|
+
*/
|
|
852
|
+
function socketTypeOf(
|
|
853
|
+
nodeId: string,
|
|
854
|
+
socket: string,
|
|
855
|
+
side: "source" | "target",
|
|
856
|
+
nodes: readonly MaterialFlowNode[],
|
|
857
|
+
) {
|
|
858
|
+
const node = nodes.find((candidate) => candidate.id === nodeId);
|
|
859
|
+
const list = side === "source" ? node?.data.outputs : node?.data.inputs;
|
|
860
|
+
return list?.find((candidate) => candidate.id === socket)?.type ?? "numeric";
|
|
861
|
+
}
|