@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,1147 @@
|
|
|
1
|
+
// WHICH WORKSPACE THE WINDOW IS IN, and what that allows.
|
|
2
|
+
//
|
|
3
|
+
// The docked shell has one grid and four workspaces. The stage workspace is
|
|
4
|
+
// the editor; the other three are modes that show their own content in the
|
|
5
|
+
// same cells: the Storyboard, the material nodes and Components (a component
|
|
6
|
+
// alone on the stage, with the Component Studio or Code Studio as its Edit
|
|
7
|
+
// tab). The top bar, the columns and the bottom row stay on screen in every
|
|
8
|
+
// one of them, so the chrome that used to be unreachable behind a full-window
|
|
9
|
+
// mode (the app menu, the shot switcher, Export) has to know which workspace
|
|
10
|
+
// it is in and refuse what does not apply there, with the reason.
|
|
11
|
+
//
|
|
12
|
+
// Pure functions of state, beside `storyboardBlocker` in spirit: every reader
|
|
13
|
+
// (the shell's classes, the menu, the header, the cells) asks the same
|
|
14
|
+
// question and gets the same answer.
|
|
15
|
+
import {
|
|
16
|
+
COMPONENT_LISTING_ID_RE,
|
|
17
|
+
GENERATED_SHOTS_DIR,
|
|
18
|
+
INSTANCE_PROP_PREFIX,
|
|
19
|
+
WORKSPACE_COMPONENT_ID_RE,
|
|
20
|
+
clearReferenceProp,
|
|
21
|
+
componentFilesOf,
|
|
22
|
+
componentUsesOf,
|
|
23
|
+
exposedMasterValue,
|
|
24
|
+
exposedSpecOf,
|
|
25
|
+
homeOf,
|
|
26
|
+
instanceComponentIdsOf,
|
|
27
|
+
instanceExportOf,
|
|
28
|
+
instanceModuleOf,
|
|
29
|
+
layerSnapshotContract,
|
|
30
|
+
lockRefusal,
|
|
31
|
+
lockedLayerEdited,
|
|
32
|
+
patchHome,
|
|
33
|
+
setReferenceProp,
|
|
34
|
+
toTrackHex,
|
|
35
|
+
} from "@dailies-studio/shot/doc";
|
|
36
|
+
import type { ComponentDef3D, ComponentUse, Doc3D, Layer3D } from "@dailies-studio/shot/doc";
|
|
37
|
+
import { coerceValue, controlLabel, humanizeKey } from "@dailies-studio/contract";
|
|
38
|
+
import type { Contract } from "@dailies-studio/contract";
|
|
39
|
+
import { formatHex, parseHex, valueEquals } from "@dailies-studio/engine";
|
|
40
|
+
import type { Value } from "@dailies-studio/engine";
|
|
41
|
+
import { isProjectComponent } from "@dailies-studio/ui";
|
|
42
|
+
import type {
|
|
43
|
+
ComponentFileHome,
|
|
44
|
+
ComponentListingUpdate,
|
|
45
|
+
ComponentRow,
|
|
46
|
+
WorkspaceComponent,
|
|
47
|
+
WorkspaceComponentVersionInfo,
|
|
48
|
+
} from "../api.js";
|
|
49
|
+
import { isCutId, isGapId, isPartId, isReferenceId, shotNumberOf } from "./storyboardModel.js";
|
|
50
|
+
import type { ComponentsSelection, ShotEditorState } from "./types.js";
|
|
51
|
+
|
|
52
|
+
export type Workspace = "stage" | "storyboard" | "material" | "components";
|
|
53
|
+
|
|
54
|
+
type WorkspaceFields = Pick<
|
|
55
|
+
ShotEditorState,
|
|
56
|
+
"materialWorkspace" | "codeComponent" | "editingDef" | "storyboardOpen"
|
|
57
|
+
> &
|
|
58
|
+
Partial<Pick<ShotEditorState, "componentsOpen" | "componentsTab">>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Every field `workspaceOf` reads. A write that names none of them cannot
|
|
62
|
+
* move the workspace: the store's mode guards skip their work for it (`set`).
|
|
63
|
+
*/
|
|
64
|
+
export const WORKSPACE_KEYS = [
|
|
65
|
+
"materialWorkspace",
|
|
66
|
+
"codeComponent",
|
|
67
|
+
"editingDef",
|
|
68
|
+
"componentsOpen",
|
|
69
|
+
"storyboardOpen",
|
|
70
|
+
] as const satisfies readonly (keyof WorkspaceFields)[];
|
|
71
|
+
|
|
72
|
+
// The other direction, as `SHOT_KINDS` has it: every field a workspace is read
|
|
73
|
+
// from is in the list, so a mode `workspaceOf` learns to read (a field added
|
|
74
|
+
// to `WorkspaceFields`) fails to compile until it is listed, and a field taken
|
|
75
|
+
// off the list does too. `componentsTab` is the one it never reads: the tab
|
|
76
|
+
// inside Components (`componentEditOpen`'s).
|
|
77
|
+
type _EveryWorkspaceFieldListed =
|
|
78
|
+
Exclude<keyof WorkspaceFields, "componentsTab"> extends (typeof WORKSPACE_KEYS)[number] ? true : never;
|
|
79
|
+
const _workspaceFieldsListed: _EveryWorkspaceFieldListed = true;
|
|
80
|
+
void _workspaceFieldsListed;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The workspace on screen. One precedence, spelled once: each mode's own
|
|
84
|
+
* entry clears the others, so two are never open at once in practice, and
|
|
85
|
+
* this order says which wins if they ever are. An open definition is always
|
|
86
|
+
* Components: every door into a component lands there, standalone, never in
|
|
87
|
+
* the Shot that holds it.
|
|
88
|
+
*/
|
|
89
|
+
export function workspaceOf(s: WorkspaceFields): Workspace {
|
|
90
|
+
if (s.materialWorkspace !== null) return "material";
|
|
91
|
+
if (s.codeComponent !== null) return "components";
|
|
92
|
+
if (s.editingDef !== null) return "components";
|
|
93
|
+
if (s.componentsOpen === true) return "components";
|
|
94
|
+
if (s.storyboardOpen) return "storyboard";
|
|
95
|
+
return "stage";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Components' Edit tab on a Studio-made definition (mock C6): the layers
|
|
100
|
+
* outline, the tool footer and the def timeline are the editor here, so the
|
|
101
|
+
* stage's verbs apply to the definition's interior.
|
|
102
|
+
*/
|
|
103
|
+
export function componentEditOpen(s: WorkspaceFields): boolean {
|
|
104
|
+
return workspaceOf(s) === "components" && s.codeComponent === null && s.editingDef !== null && s.componentsTab === "edit";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** How to get out of each mode, said once for every refusal below. */
|
|
108
|
+
const LEAVE: Record<Exclude<Workspace, "stage" | "storyboard">, string> = {
|
|
109
|
+
material: "Close the material nodes",
|
|
110
|
+
components: "Close Components",
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Why Export cannot run now, or null. The stage in these three modes is
|
|
115
|
+
* drawing one object or one component, not the film, and an export captures
|
|
116
|
+
* the stage. The board is fine: it draws over the film without replacing it.
|
|
117
|
+
*/
|
|
118
|
+
export function exportBlocker(
|
|
119
|
+
s: WorkspaceFields & Partial<Pick<ShotEditorState, "doc" | "componentBuilds" | "library">>,
|
|
120
|
+
): string | null {
|
|
121
|
+
const ws = workspaceOf(s);
|
|
122
|
+
if (ws !== "stage" && ws !== "storyboard") {
|
|
123
|
+
return `${LEAVE[ws]} before exporting: the stage is showing ${ws === "material" ? "one object" : "one component"}, not the film.`;
|
|
124
|
+
}
|
|
125
|
+
// A FAILING BUILD keeps its last good module playing (graph.ts `lastGood`),
|
|
126
|
+
// so the film on screen is not what the source says. Exporting it would
|
|
127
|
+
// ship a stale component silently.
|
|
128
|
+
if (s.doc == null || s.componentBuilds === undefined) return null;
|
|
129
|
+
for (const id of instanceComponentIdsOf(s.doc)) {
|
|
130
|
+
if (s.componentBuilds[id]?.status !== "failed") continue;
|
|
131
|
+
const name = s.library?.find((r) => r.id === id)?.name ?? instanceExportOf(id);
|
|
132
|
+
return `A component has a failing build: ${name}. Fix it before exporting.`;
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The app-menu rows each mode keeps, as exact ids or `prefix.` families.
|
|
139
|
+
* AN ALLOWLIST, on `MATERIAL_WORKSPACE_KEYS`' reasoning: a new row is refused
|
|
140
|
+
* in a mode until somebody decides it applies there, which is the safe
|
|
141
|
+
* direction. The rows kept everywhere are about the window, the account and
|
|
142
|
+
* the transport, never about the stage's selection.
|
|
143
|
+
*/
|
|
144
|
+
const EVERYWHERE = [
|
|
145
|
+
"acct.",
|
|
146
|
+
"help",
|
|
147
|
+
"file.save",
|
|
148
|
+
"file.versions",
|
|
149
|
+
"view.panel.",
|
|
150
|
+
"view.badges.",
|
|
151
|
+
"view.appearance.",
|
|
152
|
+
"time.play",
|
|
153
|
+
"time.previewMuted",
|
|
154
|
+
// The Storyboard row is the top bar's segment, which hands over from every
|
|
155
|
+
// mode (`setStoryboardOpen`).
|
|
156
|
+
"ctx.storyboard",
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
const ALLOWED: Record<"material" | "def" | "code", readonly string[]> = {
|
|
160
|
+
// The object's pane is a real working view and the timeline is on screen:
|
|
161
|
+
// the same verbs its keys keep (`MATERIAL_WORKSPACE_KEYS`).
|
|
162
|
+
material: [
|
|
163
|
+
...EVERYWHERE,
|
|
164
|
+
"edit.undo",
|
|
165
|
+
"edit.redo",
|
|
166
|
+
"time.",
|
|
167
|
+
"view.fit",
|
|
168
|
+
"view.fitSelection",
|
|
169
|
+
"view.zoomIn",
|
|
170
|
+
"view.zoomOut",
|
|
171
|
+
"view.ortho",
|
|
172
|
+
"view.orbit.",
|
|
173
|
+
"view.preset.",
|
|
174
|
+
],
|
|
175
|
+
// Components with a definition open on Preview: its own clock runs, and
|
|
176
|
+
// nothing edits the interior until the Edit tab.
|
|
177
|
+
def: [...EVERYWHERE, "edit.undo", "edit.redo", "time.step.", "time.fit", "time.mode."],
|
|
178
|
+
// Components with a code component open, or with nothing open yet. Undo and
|
|
179
|
+
// redo are face-dependent, so `commandBlocker` decides them below.
|
|
180
|
+
code: EVERYWHERE,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const allows = (list: readonly string[], id: string): boolean =>
|
|
184
|
+
list.some((rule) => (rule.endsWith(".") || rule.endsWith(":") ? id.startsWith(rule) : id === rule));
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Why nothing can change for a viewer. Every refusal in a read-only Project
|
|
188
|
+
* says this one sentence: the store's `viewOnly`, the menu, the keys, the
|
|
189
|
+
* tool rail and the panels.
|
|
190
|
+
*/
|
|
191
|
+
export const VIEW_ONLY = "You can view this Project but not edit it. Ask a Project admin for edit access.";
|
|
192
|
+
|
|
193
|
+
/** Why the comment tool and the Comments tab refuse a caller the server says
|
|
194
|
+
* may not comment (`LoadedShot.can.comment === false`). */
|
|
195
|
+
export const NO_COMMENT = "You cannot comment on this Project. Ask a Project admin for access.";
|
|
196
|
+
|
|
197
|
+
/** Why nothing can change while a saved version is on screen. */
|
|
198
|
+
export const PREVIEW_ONLY = "You are looking at a saved version. Go back to the current version, or restore this one, to edit.";
|
|
199
|
+
|
|
200
|
+
/** Why nothing can change while live sync holds the Project (R5). */
|
|
201
|
+
export const SYNC_HOLD_REASON = {
|
|
202
|
+
upgrade: "This Project was upgraded by a newer version of the editor. Reload to keep editing.",
|
|
203
|
+
"older-server": "This editor is newer than the server it reaches. Editing waits until the server catches up.",
|
|
204
|
+
quota: "Edits cannot be stored on this device. Free up space, then try again.",
|
|
205
|
+
} as const;
|
|
206
|
+
|
|
207
|
+
/** The sentence a read-only chrome says: a version preview's, else a sync
|
|
208
|
+
* hold's, else the viewer's. Every reader of `readOnly` that words a refusal
|
|
209
|
+
* asks this. */
|
|
210
|
+
export function readOnlyReason(s: { versionPreview?: unknown; syncStatus?: { hold: keyof typeof SYNC_HOLD_REASON | null } | null }): string {
|
|
211
|
+
if (s.versionPreview != null) return PREVIEW_ONLY;
|
|
212
|
+
const hold = s.syncStatus?.hold ?? null;
|
|
213
|
+
return hold === null ? VIEW_ONLY : SYNC_HOLD_REASON[hold];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The app-menu rows a viewer keeps, as exact ids or `prefix.` families. An
|
|
218
|
+
* allowlist for the reason `ALLOWED` is one: a new row is refused to a viewer
|
|
219
|
+
* until somebody decides it writes nothing. Each row here changes only the
|
|
220
|
+
* window, the view, the playhead or the selection, never the document.
|
|
221
|
+
*/
|
|
222
|
+
const VIEW_ONLY_ALLOWED = [
|
|
223
|
+
"acct.",
|
|
224
|
+
"help",
|
|
225
|
+
"file.export",
|
|
226
|
+
"file.versions",
|
|
227
|
+
"view.panel.",
|
|
228
|
+
"view.badges.",
|
|
229
|
+
"view.appearance.",
|
|
230
|
+
"view.layout.",
|
|
231
|
+
"view.shading.",
|
|
232
|
+
"view.preset.",
|
|
233
|
+
"view.orbit.",
|
|
234
|
+
"view.video",
|
|
235
|
+
"view.ortho",
|
|
236
|
+
"view.grid",
|
|
237
|
+
"view.zoomIn",
|
|
238
|
+
"view.zoomOut",
|
|
239
|
+
"view.zoomReset",
|
|
240
|
+
"view.fit",
|
|
241
|
+
"view.fitSelection",
|
|
242
|
+
"view.selectMovesPlayhead",
|
|
243
|
+
// The board's Project assets panel: a viewer reads it, every row locked.
|
|
244
|
+
"board-assets",
|
|
245
|
+
// A shot's board: a view, which writes nothing for a planned shot.
|
|
246
|
+
"board-open-board",
|
|
247
|
+
// The Preferences page and its View rows write the viewer's own prefs,
|
|
248
|
+
// never the document.
|
|
249
|
+
"prefs.",
|
|
250
|
+
"view.rulers",
|
|
251
|
+
"view.smartGuides",
|
|
252
|
+
"view.motionPaths",
|
|
253
|
+
"time.play",
|
|
254
|
+
"time.previewMuted",
|
|
255
|
+
"time.step.",
|
|
256
|
+
"time.fit",
|
|
257
|
+
"time.mode.",
|
|
258
|
+
"time.workArea.",
|
|
259
|
+
"edit.copy",
|
|
260
|
+
// The motion clip's copies read the document and write nothing to it.
|
|
261
|
+
"edit.copyAnimation",
|
|
262
|
+
"edit.copyEffects",
|
|
263
|
+
"edit.selectAll",
|
|
264
|
+
"edit.deselect",
|
|
265
|
+
"tool.select",
|
|
266
|
+
"ctx.",
|
|
267
|
+
"comp.edit:",
|
|
268
|
+
"layer.isolate.exitAll",
|
|
269
|
+
"isolation",
|
|
270
|
+
"code.path",
|
|
271
|
+
"shot-open",
|
|
272
|
+
"shot-zoom",
|
|
273
|
+
"shot-transition",
|
|
274
|
+
"shot-copy-effects",
|
|
275
|
+
// The right-click rows that only open, select or change the view, and the
|
|
276
|
+
// copies, which write nothing to the document.
|
|
277
|
+
"copy",
|
|
278
|
+
"copy-animation",
|
|
279
|
+
"copy-effects",
|
|
280
|
+
"open-3d",
|
|
281
|
+
"material",
|
|
282
|
+
"graph-open",
|
|
283
|
+
"graph-copy",
|
|
284
|
+
"key-select-track",
|
|
285
|
+
"path-key-seek",
|
|
286
|
+
];
|
|
287
|
+
|
|
288
|
+
/** Why a viewer cannot run this row (by its command id), or null. */
|
|
289
|
+
export function viewOnlyBlocker(id: string): string | null {
|
|
290
|
+
return allows(VIEW_ONLY_ALLOWED, id) ? null : VIEW_ONLY;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Why an app-menu row (by its command id) cannot run in this workspace, or
|
|
295
|
+
* null. The menu and WebMCP's `run_command` both read rows through
|
|
296
|
+
* `appMenuEntries`, which applies this, so the two cannot disagree.
|
|
297
|
+
*/
|
|
298
|
+
export function commandBlocker(
|
|
299
|
+
s: WorkspaceFields &
|
|
300
|
+
Partial<Pick<ShotEditorState, "readOnly" | "versionPreview" | "syncStatus">>,
|
|
301
|
+
id: string,
|
|
302
|
+
): string | null {
|
|
303
|
+
if (s.readOnly === true && viewOnlyBlocker(id) !== null) return readOnlyReason(s);
|
|
304
|
+
const ws = workspaceOf(s);
|
|
305
|
+
if (ws === "stage") return null;
|
|
306
|
+
if (ws === "storyboard") return boardCommandBlocker(id);
|
|
307
|
+
// The Edit tab edits the definition's layers with the stage's own verbs. An
|
|
308
|
+
// export still captures the stage, which shows one component.
|
|
309
|
+
if (componentEditOpen(s)) return id === "file.export" ? exportBlocker(s) : null;
|
|
310
|
+
const rows =
|
|
311
|
+
ws === "material" ? ALLOWED.material : s.codeComponent === null && s.editingDef !== null ? ALLOWED.def : ALLOWED.code;
|
|
312
|
+
if (allows(rows, id)) return null;
|
|
313
|
+
if (id === "file.export") return exportBlocker(s);
|
|
314
|
+
// In Code Studio the menu's undo walks the defaults history (`store.undo`);
|
|
315
|
+
// CodeMirror keeps its own for keys typed in the editor.
|
|
316
|
+
if (ws === "components" && s.codeComponent !== null && (id === "edit.undo" || id === "edit.redo")) return null;
|
|
317
|
+
return `${LEAVE[ws]} first: this acts on the scene, which is not on screen.`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Why a component row cannot open in Components (D8): Dailies components
|
|
321
|
+
* are three only. The Components list, the board's library and the Assets
|
|
322
|
+
* tab say this one sentence on a dom row. */
|
|
323
|
+
export const DOM_NOT_SUPPORTED = "Not supported: Dailies components are React Three Fiber";
|
|
324
|
+
|
|
325
|
+
/** Why a built-in (`ComponentRow.builtin`: it ships inside a package) has no
|
|
326
|
+
* door into Components: there is no source file and no master to open. The
|
|
327
|
+
* doors that would refuse it on a press (Open in Components, Edit master)
|
|
328
|
+
* show disabled with this sentence instead. */
|
|
329
|
+
export const BUILTIN_NO_MASTER = "Built into Dailies: it has no source to open or edit.";
|
|
330
|
+
|
|
331
|
+
/** Whether a registry id names a built-in. */
|
|
332
|
+
export function isBuiltinComponent(library: readonly ComponentRow[], componentId: string | undefined): boolean {
|
|
333
|
+
return componentId !== undefined && library.some((row) => row.id === componentId && row.builtin === true);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** One row of the Components list. A dom code component the Project uses is
|
|
337
|
+
* listed with `refusal` set and never opens. */
|
|
338
|
+
export interface ComponentsListRow {
|
|
339
|
+
sel: ComponentsSelection;
|
|
340
|
+
name: string;
|
|
341
|
+
refusal: string | null;
|
|
342
|
+
/** A Studio-made copy's `origin.source`: the workspace component (or the
|
|
343
|
+
* Community listing) it was copied from. */
|
|
344
|
+
copyOf?: string;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Whether a module path is a Project's generated film (`src/shots/<Name>.tsx`),
|
|
348
|
+
* a build product and never a code component of its own. */
|
|
349
|
+
const isGeneratedFilm = (rel: string): boolean => rel.startsWith(`${GENERATED_SHOTS_DIR}/`);
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* The Components list: this document's Studio-made definitions first, then
|
|
353
|
+
* every component of each code file made for it (`componentFiles`, in list
|
|
354
|
+
* order, registry order within a file), then the code components it uses
|
|
355
|
+
* (instance layers and board cards), in the order the document first uses
|
|
356
|
+
* them, each followed by the other components of its file, each once. A file
|
|
357
|
+
* made for it is listed even when nothing places it:
|
|
358
|
+
* membership is not a use. A built-in has no source file in this Project, a
|
|
359
|
+
* generated film is a build product, and a component the registry does not
|
|
360
|
+
* know is left out until the library answers. The open subject is not added:
|
|
361
|
+
* a component neither section lists has no row, and the crumb names it.
|
|
362
|
+
*/
|
|
363
|
+
export function componentsListOf(doc: Doc3D, library: readonly ComponentRow[]): ComponentsListRow[] {
|
|
364
|
+
const rows: ComponentsListRow[] = Object.values(doc.components ?? {}).map((def) => ({
|
|
365
|
+
sel: { kind: "studio", def: def.id },
|
|
366
|
+
name: def.name,
|
|
367
|
+
refusal: null,
|
|
368
|
+
...(def.origin === undefined ? {} : { copyOf: def.origin.source }),
|
|
369
|
+
}));
|
|
370
|
+
const seen = new Set<string>();
|
|
371
|
+
const add = (row: ComponentRow): void => {
|
|
372
|
+
if (seen.has(row.id) || row.builtin === true) return;
|
|
373
|
+
seen.add(row.id);
|
|
374
|
+
rows.push({
|
|
375
|
+
sel: { kind: "code", module: row.id },
|
|
376
|
+
name: row.name,
|
|
377
|
+
refusal: row.host === "three" ? null : DOM_NOT_SUPPORTED,
|
|
378
|
+
});
|
|
379
|
+
};
|
|
380
|
+
for (const path of componentFilesOf(doc)) {
|
|
381
|
+
if (isGeneratedFilm(path)) continue;
|
|
382
|
+
for (const row of library) if (row.rel === path) add(row);
|
|
383
|
+
}
|
|
384
|
+
for (const use of componentUsesOf(doc)) {
|
|
385
|
+
if (use.component === undefined) continue;
|
|
386
|
+
const row = library.find((r) => r.id === use.component);
|
|
387
|
+
if (row !== undefined) add(row);
|
|
388
|
+
// THE FILE IS THE UNIT: Your library counts a file this Project uses as
|
|
389
|
+
// its own (`ownFilesOf`, and `homePaths` for every other Project), so the
|
|
390
|
+
// file's other components list here too, or they would show in neither
|
|
391
|
+
// section. By the use's module, not its row: a renamed or deleted export
|
|
392
|
+
// leaves a stale id whose file still holds the others.
|
|
393
|
+
const path = instanceModuleOf(use.component);
|
|
394
|
+
for (const mate of library) if (mate.rel === path) add(mate);
|
|
395
|
+
}
|
|
396
|
+
return rows;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** One entry of YOUR LIBRARY (`libraryComponentsOf`): a code component of
|
|
400
|
+
* the registry, or a Studio-made component another Project recorded and
|
|
401
|
+
* someone added to the library. `why` is the rule that lists a code one,
|
|
402
|
+
* the first that holds: a Dailies Library install, code made for no
|
|
403
|
+
* Project, or "In your library" ticked (`WorkspaceComponent.library`). */
|
|
404
|
+
export type LibraryEntry =
|
|
405
|
+
| {
|
|
406
|
+
kind: "code";
|
|
407
|
+
module: string;
|
|
408
|
+
name: string;
|
|
409
|
+
rel: string;
|
|
410
|
+
refusal: string | null;
|
|
411
|
+
why: "unassigned" | "installed" | "added";
|
|
412
|
+
/** An install's publisher, as the registry's provenance says it. */
|
|
413
|
+
publisher?: string;
|
|
414
|
+
/** The workspace store's row for it, once one was recorded. */
|
|
415
|
+
recorded?: WorkspaceComponent;
|
|
416
|
+
}
|
|
417
|
+
| { kind: "studio"; component: WorkspaceComponent };
|
|
418
|
+
|
|
419
|
+
/** Why a component is in Your library whatever its "In your library" box
|
|
420
|
+
* says. The Components panel shows the box ticked with this line. */
|
|
421
|
+
export const LIBRARY_RULE = {
|
|
422
|
+
installed: "Dailies Library installs are always in your library.",
|
|
423
|
+
unassigned: "Code made for no Project is always in your library.",
|
|
424
|
+
copied: "Copied from your library.",
|
|
425
|
+
} as const;
|
|
426
|
+
|
|
427
|
+
/** The module paths `doc` itself lists as made for it or uses: what `homes`
|
|
428
|
+
* says of every OTHER Project, read from the live document for this one, so
|
|
429
|
+
* an edit counts before the server has seen it. */
|
|
430
|
+
function ownFilesOf(doc: Doc3D): Set<string> {
|
|
431
|
+
const files = new Set(componentFilesOf(doc));
|
|
432
|
+
for (const use of componentUsesOf(doc)) if (use.component !== undefined) files.add(instanceModuleOf(use.component));
|
|
433
|
+
return files;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** The files another Project lists or uses. Homes of this Project are left
|
|
437
|
+
* to the live document, and a home the caller may not name (`projectId`
|
|
438
|
+
* null) is still another Project's. */
|
|
439
|
+
function filesElsewhere(doc: Doc3D, homes: readonly ComponentFileHome[]): Set<string> {
|
|
440
|
+
return new Set(homes.filter((h) => h.projectId !== doc.id).map((h) => h.path));
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** CODE MADE FOR NO PROJECT: the user's own three code that no Project lists
|
|
444
|
+
* or uses, never a built-in and never a generated film. */
|
|
445
|
+
function madeForNoProject(row: ComponentRow, own: ReadonlySet<string>, elsewhere: ReadonlySet<string>): boolean {
|
|
446
|
+
if (!isProjectComponent(row) || row.host !== "three" || isGeneratedFilm(row.rel)) return false;
|
|
447
|
+
return !own.has(row.rel) && !elsewhere.has(row.rel);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Case-insensitive by name, then by module or id, so the order never
|
|
451
|
+
* depends on which list a row arrived in. */
|
|
452
|
+
function entryOrder(a: LibraryEntry, b: LibraryEntry): number {
|
|
453
|
+
const key = (e: LibraryEntry): [string, string] =>
|
|
454
|
+
e.kind === "code" ? [e.name.toLowerCase(), e.module] : [e.component.name.toLowerCase(), e.component.id];
|
|
455
|
+
const [an, ak] = key(a);
|
|
456
|
+
const [bn, bk] = key(b);
|
|
457
|
+
if (an !== bn) return an < bn ? -1 : 1;
|
|
458
|
+
return ak < bk ? -1 : ak > bk ? 1 : 0;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* YOUR LIBRARY: what every Project of this workspace offers beside its own.
|
|
463
|
+
*
|
|
464
|
+
* (a) code made for no Project: the user's own three code whose file no
|
|
465
|
+
* Project lists or uses (`homes` for the others, the live document for
|
|
466
|
+
* this one). It waits while `homes` is null, so it never lists a file
|
|
467
|
+
* some Project holds before the index has answered.
|
|
468
|
+
* (b) Dailies Library installs (`library.scope` "shared"), even when a
|
|
469
|
+
* Project lists them.
|
|
470
|
+
* (c) what someone added with "In your library": a Studio-made component
|
|
471
|
+
* of another Project, or code made for one. A flagged code row the
|
|
472
|
+
* registry does not list has nothing to open, so it is left out.
|
|
473
|
+
*
|
|
474
|
+
* Less what This project already shows (`projectRows`): code by module, a
|
|
475
|
+
* definition by the copy that names it (`copyOf`) or by its own row.
|
|
476
|
+
* Sorted by name.
|
|
477
|
+
*/
|
|
478
|
+
export function libraryComponentsOf(
|
|
479
|
+
doc: Doc3D,
|
|
480
|
+
library: readonly ComponentRow[],
|
|
481
|
+
workspace: readonly WorkspaceComponent[] | null,
|
|
482
|
+
homes: readonly ComponentFileHome[] | null,
|
|
483
|
+
projectRows: readonly ComponentsListRow[],
|
|
484
|
+
): LibraryEntry[] {
|
|
485
|
+
const shown = new Set(projectRows.flatMap((r) => (r.sel.kind === "code" ? [r.sel.module] : [])));
|
|
486
|
+
const own = ownFilesOf(doc);
|
|
487
|
+
const elsewhere = homes === null ? null : filesElsewhere(doc, homes);
|
|
488
|
+
const entries: LibraryEntry[] = [];
|
|
489
|
+
for (const row of library) {
|
|
490
|
+
if (shown.has(row.id)) continue;
|
|
491
|
+
const recorded = workspaceRowOf(workspace, doc.id, { kind: "code", module: row.id });
|
|
492
|
+
const why =
|
|
493
|
+
row.library?.scope === "shared"
|
|
494
|
+
? "installed"
|
|
495
|
+
: elsewhere !== null && madeForNoProject(row, own, elsewhere)
|
|
496
|
+
? "unassigned"
|
|
497
|
+
: recorded?.library === true
|
|
498
|
+
? "added"
|
|
499
|
+
: null;
|
|
500
|
+
if (why === null) continue;
|
|
501
|
+
const publisher = why === "installed" ? row.library?.publisher : undefined;
|
|
502
|
+
entries.push({
|
|
503
|
+
kind: "code",
|
|
504
|
+
module: row.id,
|
|
505
|
+
name: row.name,
|
|
506
|
+
rel: row.rel,
|
|
507
|
+
refusal: row.host === "three" ? null : DOM_NOT_SUPPORTED,
|
|
508
|
+
why,
|
|
509
|
+
...(publisher === undefined ? {} : { publisher }),
|
|
510
|
+
...(recorded === undefined ? {} : { recorded }),
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
for (const c of workspace ?? []) {
|
|
514
|
+
if (c.library !== true || !("def" in c.home)) continue;
|
|
515
|
+
// A COPY IS THIS PROJECT'S NOW: a definition whose origin names the row
|
|
516
|
+
// (Use here, or the shot board's picker, made it) lists in the Project.
|
|
517
|
+
if (projectRows.some((r) => r.copyOf === c.id || workspaceRowOf([c], doc.id, r.sel) !== undefined)) continue;
|
|
518
|
+
entries.push({ kind: "studio", component: c });
|
|
519
|
+
}
|
|
520
|
+
return entries.sort(entryOrder);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Why `sel` is in Your library whatever its "In your library" box says, or
|
|
525
|
+
* null when the box decides: an install, code no Project lists or uses
|
|
526
|
+
* (this document counted from the live copy), or a copy of a definition the
|
|
527
|
+
* library holds. The same rules `libraryComponentsOf` lists by, so the box
|
|
528
|
+
* and the list never disagree.
|
|
529
|
+
*/
|
|
530
|
+
export function libraryRuleOf(
|
|
531
|
+
doc: Doc3D,
|
|
532
|
+
sel: ComponentsSelection,
|
|
533
|
+
library: readonly ComponentRow[],
|
|
534
|
+
workspace: readonly WorkspaceComponent[],
|
|
535
|
+
homes: readonly ComponentFileHome[],
|
|
536
|
+
): string | null {
|
|
537
|
+
if (sel.kind === "studio") {
|
|
538
|
+
const source = doc.components?.[sel.def]?.origin?.source;
|
|
539
|
+
return source !== undefined && workspace.some((c) => c.id === source && c.library === true) ? LIBRARY_RULE.copied : null;
|
|
540
|
+
}
|
|
541
|
+
const row = library.find((r) => r.id === sel.module);
|
|
542
|
+
if (row === undefined) return null;
|
|
543
|
+
if (row.library?.scope === "shared") return LIBRARY_RULE.installed;
|
|
544
|
+
return madeForNoProject(row, ownFilesOf(doc), filesElsewhere(doc, homes)) ? LIBRARY_RULE.unassigned : null;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/** A Your library entry's name, whichever kind it is. */
|
|
548
|
+
export function libraryEntryName(entry: LibraryEntry): string {
|
|
549
|
+
return entry.kind === "code" ? entry.name : entry.component.name;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** Where a Your library entry comes from: an install's publisher (else
|
|
553
|
+
* "Dailies Library"), other code's file name, a definition's home Project
|
|
554
|
+
* by name (`homeName`), or "another Project" when the caller may not see
|
|
555
|
+
* that Project. Here, beside the lists, so the shot board's picker reads it
|
|
556
|
+
* without the Components rail. */
|
|
557
|
+
export function libraryHomeLabel(entry: LibraryEntry): string {
|
|
558
|
+
if (entry.kind === "studio") return entry.component.homeName ?? "another Project";
|
|
559
|
+
if (entry.why === "installed") return entry.publisher ?? "Dailies Library";
|
|
560
|
+
return entry.rel.slice(entry.rel.lastIndexOf("/") + 1);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** The workspace component a selection is, by its home. */
|
|
564
|
+
export function workspaceRowOf(
|
|
565
|
+
list: readonly WorkspaceComponent[] | null,
|
|
566
|
+
projectId: string | undefined,
|
|
567
|
+
sel: ComponentsSelection | null,
|
|
568
|
+
): WorkspaceComponent | undefined {
|
|
569
|
+
if (list === null || sel === null) return undefined;
|
|
570
|
+
return list.find((c) =>
|
|
571
|
+
sel.kind === "studio"
|
|
572
|
+
? "def" in c.home && c.home.def === sel.def && c.home.projectId === projectId
|
|
573
|
+
: "component" in c.home && c.home.component === sel.module,
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** The contract keys the previous version did not have (versions newest
|
|
578
|
+
* first, the first being the build on screen). None before a second one. */
|
|
579
|
+
export function newPropKeys(keys: readonly string[], versions: readonly WorkspaceComponentVersionInfo[] | undefined): string[] {
|
|
580
|
+
const previous = versions?.[1];
|
|
581
|
+
if (previous === undefined) return [];
|
|
582
|
+
return keys.filter((k) => !previous.propKeys.includes(k));
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/** Whether two selections name the same component. */
|
|
586
|
+
export function sameComponent(a: ComponentsSelection | null, b: ComponentsSelection | null): boolean {
|
|
587
|
+
if (a === null || b === null) return a === b;
|
|
588
|
+
return a.kind === "studio" ? b.kind === "studio" && a.def === b.def : b.kind === "code" && a.module === b.module;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** The component Components shows now, from the open subject. */
|
|
592
|
+
export function componentsSubject(s: Pick<ShotEditorState, "codeComponent" | "editingDef">): ComponentsSelection | null {
|
|
593
|
+
if (s.codeComponent !== null) return { kind: "code", module: s.codeComponent };
|
|
594
|
+
if (s.editingDef !== null) return { kind: "studio", def: s.editingDef };
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/** The Every use view when it is up for the component Components shows now,
|
|
599
|
+
* else null: a view opened for another subject, or left behind by a door
|
|
600
|
+
* out of Components, shows nothing. */
|
|
601
|
+
export function usesViewOf(
|
|
602
|
+
s: WorkspaceFields & Pick<ShotEditorState, "codeComponent" | "editingDef" | "componentUses">,
|
|
603
|
+
): { subject: ComponentsSelection; use: string | null } | null {
|
|
604
|
+
const view = s.componentUses;
|
|
605
|
+
if (view === null || workspaceOf(s) !== "components") return null;
|
|
606
|
+
return sameComponent(view.subject, componentsSubject(s)) ? view : null;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** Components without the timeline row: nothing open, the Preview tab (its
|
|
610
|
+
* transport sits in the canvas cell), or a code component (its Edit tab has
|
|
611
|
+
* its own footer). Only a Studio-made component's Edit tab keeps it. */
|
|
612
|
+
export function componentsHideTimeline(
|
|
613
|
+
s: WorkspaceFields & Pick<ShotEditorState, "codeComponent" | "editingDef" | "componentsTab">,
|
|
614
|
+
): boolean {
|
|
615
|
+
if (workspaceOf(s) !== "components") return false;
|
|
616
|
+
return s.codeComponent !== null || s.editingDef === null || s.componentsTab === "preview";
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/** The board's half of `commandBlocker`. The board enters no Shot, so the
|
|
620
|
+
* Studio's context rows (All shots) act on a view that is not on screen:
|
|
621
|
+
* they are the Studio's. */
|
|
622
|
+
function boardCommandBlocker(id: string): string | null {
|
|
623
|
+
if (id === "ctx.scene") return "All shots is the Studio's view. Close the Storyboard to see the whole film.";
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** The planned shot whose shot board is on screen, or null: the board is
|
|
628
|
+
* closed, or it shows every shot. A closed board keeps `storyboardPlan` for
|
|
629
|
+
* the way back, so every reader asks this rather than the field. */
|
|
630
|
+
export function shotBoardOf(s: Pick<ShotEditorState, "storyboardOpen" | "storyboardPlan">): string | null {
|
|
631
|
+
return s.storyboardOpen ? s.storyboardPlan : null;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** Which panel the board's right column shows: the agent, the comments, the
|
|
635
|
+
* component sheet (S3: exactly one component card), the
|
|
636
|
+
* inspector (any other card, a gap or a cut), the Plan panel (one shot
|
|
637
|
+
* card), or nothing (no selection: the canvas takes the whole width). On a
|
|
638
|
+
* shot board: the agent, the comments, the item inspector (`"sketch"`, any
|
|
639
|
+
* sketch item selected), Project assets, else the Plan panel. Off the board,
|
|
640
|
+
* the agent, the comments, then in Components the inspector while a
|
|
641
|
+
* component is open and nothing on its home; elsewhere the Brief (S4) or
|
|
642
|
+
* the inspector. */
|
|
643
|
+
export type BoardRightPanel = "agent" | "comments" | "brief" | "component" | "inspector" | "assets" | "plan" | "sketch" | null;
|
|
644
|
+
|
|
645
|
+
export function boardRightPanel(
|
|
646
|
+
s: Pick<
|
|
647
|
+
ShotEditorState,
|
|
648
|
+
| "storyboardOpen"
|
|
649
|
+
| "agentOpen"
|
|
650
|
+
| "commentsOpen"
|
|
651
|
+
| "briefOpen"
|
|
652
|
+
| "canComment"
|
|
653
|
+
| "storyboardSelection"
|
|
654
|
+
| "doc"
|
|
655
|
+
| "boardAssetsOpen"
|
|
656
|
+
| "materialWorkspace"
|
|
657
|
+
| "codeComponent"
|
|
658
|
+
| "editingDef"
|
|
659
|
+
> &
|
|
660
|
+
Partial<Pick<ShotEditorState, "storyboardPlan" | "sketchSelection" | "componentsOpen">>,
|
|
661
|
+
): BoardRightPanel {
|
|
662
|
+
if (s.agentOpen) return "agent";
|
|
663
|
+
// The Comments tab, picked by the author, on the board or off it. Gated by
|
|
664
|
+
// `can.comment`: a caller who may not comment never sees the threads.
|
|
665
|
+
if (s.commentsOpen && s.canComment) return "comments";
|
|
666
|
+
if (!s.storyboardOpen) {
|
|
667
|
+
// COMPONENTS: the open component's inspector (its parts, This use, the
|
|
668
|
+
// Shot door's origin), never the Brief, which is a Shot's plan. The home
|
|
669
|
+
// has nothing to inspect, so its column steps aside like the board's.
|
|
670
|
+
if (workspaceOf(s) === "components") return componentsSubject(s) === null ? null : "inspector";
|
|
671
|
+
return s.briefOpen ? "brief" : "inspector";
|
|
672
|
+
}
|
|
673
|
+
// THE SHOT BOARD always has a panel: what is selected on the sketch, the
|
|
674
|
+
// assets it adds from, else the shot's plan.
|
|
675
|
+
if (s.storyboardPlan != null) {
|
|
676
|
+
if ((s.sketchSelection?.length ?? 0) > 0) return "sketch";
|
|
677
|
+
return s.boardAssetsOpen ? "assets" : "plan";
|
|
678
|
+
}
|
|
679
|
+
const board = s.storyboardSelection;
|
|
680
|
+
if (board.length === 1 && isReferenceId(board[0]!)) {
|
|
681
|
+
const item = s.doc?.resources?.references?.find((r) => r.id === board[0]);
|
|
682
|
+
if (item?.type === "component") return "component";
|
|
683
|
+
}
|
|
684
|
+
if (board.some((id) => isReferenceId(id) || isGapId(id) || isCutId(id))) return "inspector";
|
|
685
|
+
if (board.length === 1 && isPartId(board[0]!)) return "plan";
|
|
686
|
+
// Project assets, under any selected card's own panel: a press on a card
|
|
687
|
+
// shows that card, and a press on the ground brings the assets back.
|
|
688
|
+
if (s.boardAssetsOpen) return "assets";
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// ── Every use (C4) ──────────────────────────────────────────────────────────
|
|
693
|
+
//
|
|
694
|
+
// A use is named by a KEY, so the list, the THIS USE panel and the store verbs
|
|
695
|
+
// agree on it without passing objects around: a board card by its id, a layer
|
|
696
|
+
// by its container (Shot or definition) and its id, since layer ids are only
|
|
697
|
+
// unique inside one container.
|
|
698
|
+
|
|
699
|
+
export const cardUseKey = (referenceId: string): string => `card:${referenceId}`;
|
|
700
|
+
export const layerUseKey = (container: string, layerId: string): string => `layer:${container}:${layerId}`;
|
|
701
|
+
|
|
702
|
+
export function useKeyOf(use: ComponentUse): string {
|
|
703
|
+
return use.referenceId !== undefined
|
|
704
|
+
? cardUseKey(use.referenceId)
|
|
705
|
+
: layerUseKey(use.sceneId ?? use.inDef ?? "", use.layerId ?? "");
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
type UseTarget = { kind: "card"; id: string } | { kind: "layer"; container: string; id: string };
|
|
709
|
+
|
|
710
|
+
function targetOf(key: string): UseTarget | null {
|
|
711
|
+
if (key.startsWith("card:")) return { kind: "card", id: key.slice(5) };
|
|
712
|
+
if (!key.startsWith("layer:")) return null;
|
|
713
|
+
const rest = key.slice(6);
|
|
714
|
+
const cut = rest.lastIndexOf(":");
|
|
715
|
+
return cut <= 0 ? null : { kind: "layer", container: rest.slice(0, cut), id: rest.slice(cut + 1) };
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/** The layer a key names, with its container. */
|
|
719
|
+
export function useLayerOf(key: string): { container: string; layerId: string } | null {
|
|
720
|
+
const target = targetOf(key);
|
|
721
|
+
return target?.kind === "layer" ? { container: target.container, layerId: target.id } : null;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/** The board card a key names. */
|
|
725
|
+
export function useCardOf(key: string): string | null {
|
|
726
|
+
const target = targetOf(key);
|
|
727
|
+
return target?.kind === "card" ? target.id : null;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/** A code component's defaults from its live contract, for `componentUsesOf`,
|
|
731
|
+
* as the engine values a use stores (`coerceValue`): a curve or a gradient
|
|
732
|
+
* default is its decoded object in the contract and its canonical JSON in a
|
|
733
|
+
* use, and comparing the two called every stored composite a change. */
|
|
734
|
+
export function contractDefaultsFrom(
|
|
735
|
+
contracts: Readonly<Record<string, Contract>>,
|
|
736
|
+
): (componentId: string) => Record<string, Value> | null {
|
|
737
|
+
return (id) => {
|
|
738
|
+
const contract = contracts[id];
|
|
739
|
+
if (contract === undefined) return null;
|
|
740
|
+
const out: Record<string, Value> = {};
|
|
741
|
+
for (const spec of Object.values(contract.keys)) {
|
|
742
|
+
const value = "default" in spec ? spec.default : undefined;
|
|
743
|
+
if (value === null || value === undefined) continue;
|
|
744
|
+
try {
|
|
745
|
+
const coerced = coerceValue(spec, value);
|
|
746
|
+
if (coerced.ok) out[spec.key] = coerced.value;
|
|
747
|
+
} catch {
|
|
748
|
+
// A spec this build cannot read has no default a use could match.
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return out;
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
/** Every use of one component in this document, in walk order. The live
|
|
756
|
+
* contracts go in twice: a code use's changes are measured against its
|
|
757
|
+
* contract defaults, and a definition's prop aimed at an interior instance's
|
|
758
|
+
* unset prop draws that prop's contract default (`exposedMasterValue`). */
|
|
759
|
+
export function usesOfSubject(
|
|
760
|
+
doc: Doc3D,
|
|
761
|
+
sel: ComponentsSelection,
|
|
762
|
+
contracts: Readonly<Record<string, Contract>> = {},
|
|
763
|
+
): ComponentUse[] {
|
|
764
|
+
return componentUsesOf(doc, { contractDefaults: contractDefaultsFrom(contracts), contracts }).filter((u) =>
|
|
765
|
+
sel.kind === "studio" ? u.def === sel.def : u.component === sel.module,
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/** The use a key names, or null when it is gone. */
|
|
770
|
+
export function useByKey(
|
|
771
|
+
doc: Doc3D,
|
|
772
|
+
key: string,
|
|
773
|
+
contracts: Readonly<Record<string, Contract>> = {},
|
|
774
|
+
): ComponentUse | null {
|
|
775
|
+
return (
|
|
776
|
+
componentUsesOf(doc, { contractDefaults: contractDefaultsFrom(contracts), contracts }).find(
|
|
777
|
+
(u) => useKeyOf(u) === key,
|
|
778
|
+
) ?? null
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/** The list chip: "No changes", "1 change: Tilt", "2 changes". */
|
|
783
|
+
export function changeLabel(use: Pick<ComponentUse, "overrides">, labelOf: (key: string) => string = (k) => k): string {
|
|
784
|
+
const n = use.overrides.length;
|
|
785
|
+
if (n === 0) return "No changes";
|
|
786
|
+
return n === 1 ? `1 change: ${labelOf(use.overrides[0]!)}` : `${n} changes`;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/** "03 Phone reveal", by the board's shot numbering. */
|
|
790
|
+
export function shotLabel(doc: Doc3D, sceneId: string): string {
|
|
791
|
+
const name = doc.scenesById[sceneId]?.name ?? "";
|
|
792
|
+
const number = shotNumberOf(doc, sceneId);
|
|
793
|
+
return number === null ? name : `${number} ${name}`;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/** Where a board card is pinned, as the list says it. */
|
|
797
|
+
function cardPlace(doc: Doc3D, use: ComponentUse): string {
|
|
798
|
+
const part = use.partId === null ? undefined : doc.storyboard?.parts.find((p) => p.id === use.partId);
|
|
799
|
+
if (part === undefined) {
|
|
800
|
+
return use.sceneId === null ? "Reference on the Style" : `Reference in ${shotLabel(doc, use.sceneId)}`;
|
|
801
|
+
}
|
|
802
|
+
const scene = part.scene !== undefined && doc.scenesById[part.scene] !== undefined ? part.scene : null;
|
|
803
|
+
return `Reference, pinned to ${scene === null ? part.label : shotLabel(doc, scene)}`;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/** The list row's kind badge, title and location for a use in THIS document. */
|
|
807
|
+
export function useRowText(doc: Doc3D, use: ComponentUse): { kind: "BOARD" | "SHOT"; title: string; location: string } {
|
|
808
|
+
if (use.kind === "card") return { kind: "BOARD", title: cardPlace(doc, use), location: "This Project · Storyboard" };
|
|
809
|
+
const layer = `layer "${use.layerName ?? use.layerId ?? ""}"`;
|
|
810
|
+
if (use.sceneId !== null) {
|
|
811
|
+
return { kind: "SHOT", title: `${shotLabel(doc, use.sceneId)} · ${layer}`, location: "This Project · Studio" };
|
|
812
|
+
}
|
|
813
|
+
const def = use.inDef === undefined ? undefined : doc.components?.[use.inDef];
|
|
814
|
+
const owner = def?.name ?? "a component";
|
|
815
|
+
return { kind: "SHOT", title: `${def?.name ?? "Component"} · ${layer}`, location: `This Project · inside ${owner}` };
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/** THIS USE's title: "Shot 03 · Phone hero", "Storyboard · Hero card". */
|
|
819
|
+
export function useTitle(doc: Doc3D, use: ComponentUse): string {
|
|
820
|
+
if (use.kind === "card") return `Storyboard · ${use.referenceName ?? "Reference"}`;
|
|
821
|
+
const name = use.layerName ?? use.layerId ?? "";
|
|
822
|
+
if (use.sceneId !== null) {
|
|
823
|
+
const number = shotNumberOf(doc, use.sceneId);
|
|
824
|
+
return `${number === null ? (use.sceneName ?? "Shot") : `Shot ${number}`} · ${name}`;
|
|
825
|
+
}
|
|
826
|
+
const def = use.inDef === undefined ? undefined : doc.components?.[use.inDef];
|
|
827
|
+
return `${def?.name ?? "Component"} · ${name}`;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/** Why a value cannot be written to this use from a panel, or null. An
|
|
831
|
+
* instance prop with keyframes is animated in its Shot: a base value written
|
|
832
|
+
* with no playhead would change nothing on screen. */
|
|
833
|
+
export function usePropRefusal(doc: Doc3D, key: string, prop: string, label = prop): string | null {
|
|
834
|
+
const target = targetOf(key);
|
|
835
|
+
if (target === null || target.kind === "card") return null;
|
|
836
|
+
const layer = homeOf(doc, target.container)?.layersById[target.id];
|
|
837
|
+
if (layer?.kind === "instance" && layer.tracks?.[`${INSTANCE_PROP_PREFIX}${prop}`] !== undefined) {
|
|
838
|
+
return `${label} is animated here. Change it in the Studio, on its keyframes.`;
|
|
839
|
+
}
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* One prop of one use set, or cleared with null so it follows the master
|
|
845
|
+
* again. The same document when the use is gone or the write is refused.
|
|
846
|
+
*
|
|
847
|
+
* `aliases` are the prop's `renamedFrom` keys. An override stored under one of
|
|
848
|
+
* them still reaches the renderer (`overrideFor`), so an explicit write takes
|
|
849
|
+
* them with it: a value set under the new key would leave the old one behind
|
|
850
|
+
* as dead weight, and a CLEAR that left it would keep showing the old value
|
|
851
|
+
* the author just reset.
|
|
852
|
+
*/
|
|
853
|
+
export function withUseValue(
|
|
854
|
+
doc: Doc3D,
|
|
855
|
+
key: string,
|
|
856
|
+
prop: string,
|
|
857
|
+
value: Value | null,
|
|
858
|
+
aliases: readonly string[] = [],
|
|
859
|
+
): Doc3D {
|
|
860
|
+
const target = targetOf(key);
|
|
861
|
+
if (target === null || usePropRefusal(doc, key, prop) !== null) return doc;
|
|
862
|
+
if (target.kind === "card") {
|
|
863
|
+
const base = aliases.reduce((next, alias) => clearReferenceProp(next, target.id, alias), doc);
|
|
864
|
+
return value === null ? clearReferenceProp(base, target.id, prop) : setReferenceProp(base, target.id, prop, value);
|
|
865
|
+
}
|
|
866
|
+
return patchUseLayer(doc, target.container, target.id, (values) => {
|
|
867
|
+
const stale = aliases.filter((alias) => alias in values);
|
|
868
|
+
const without = (drop: readonly string[]): Record<string, Value> =>
|
|
869
|
+
Object.fromEntries(Object.entries(values).filter(([k]) => !drop.includes(k)));
|
|
870
|
+
// The same object when nothing moves, so an unchanged write is no edit.
|
|
871
|
+
if (value === null) return stale.length === 0 && !(prop in values) ? values : without([prop, ...stale]);
|
|
872
|
+
if (stale.length > 0) return { ...without(stale), [prop]: value };
|
|
873
|
+
return valueEquals(values[prop], value) ? values : { ...values, [prop]: value };
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/** The stored props of a use that carry keyframes: an instance layer's
|
|
878
|
+
* `instance.props.<key>` tracks. Reset all keeps these (`withUseReset`). */
|
|
879
|
+
function animatedPropsOf(doc: Doc3D, key: string): string[] {
|
|
880
|
+
const target = targetOf(key);
|
|
881
|
+
if (target === null || target.kind === "card") return [];
|
|
882
|
+
const layer = homeOf(doc, target.container)?.layersById[target.id];
|
|
883
|
+
if (layer?.kind !== "instance") return [];
|
|
884
|
+
return Object.keys(layer.instance.props).filter((prop) => layer.tracks?.[`${INSTANCE_PROP_PREFIX}${prop}`] !== undefined);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Reset all to master: the use stores no values, apart from an animated
|
|
889
|
+
* instance prop's. Its stored value is the base under its track, the value
|
|
890
|
+
* the track was minted from and the one that returns when the track goes:
|
|
891
|
+
* clearing it left the next edit of that prop unkeyed (the router keys only
|
|
892
|
+
* over a base) and brought the contract default back in place of the authored
|
|
893
|
+
* value when the keyframes were removed. So those are kept, and `resetUse`
|
|
894
|
+
* says which.
|
|
895
|
+
*/
|
|
896
|
+
export function withUseReset(doc: Doc3D, key: string): Doc3D {
|
|
897
|
+
const target = targetOf(key);
|
|
898
|
+
if (target === null) return doc;
|
|
899
|
+
if (target.kind === "card") {
|
|
900
|
+
const stored = Object.keys(useByKey(doc, key)?.values ?? {});
|
|
901
|
+
return stored.reduce((next, prop) => clearReferenceProp(next, target.id, prop), doc);
|
|
902
|
+
}
|
|
903
|
+
const kept = new Set(animatedPropsOf(doc, key));
|
|
904
|
+
return patchUseLayer(doc, target.container, target.id, (values) => {
|
|
905
|
+
const stays = Object.keys(values).filter((prop) => kept.has(prop));
|
|
906
|
+
return stays.length === Object.keys(values).length ? values : Object.fromEntries(stays.map((prop) => [prop, values[prop]!]));
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/** A component layer's overrides or an instance layer's props, rewritten. */
|
|
911
|
+
function patchUseLayer(
|
|
912
|
+
doc: Doc3D,
|
|
913
|
+
container: string,
|
|
914
|
+
layerId: string,
|
|
915
|
+
next: (values: Record<string, Value>) => Record<string, Value>,
|
|
916
|
+
): Doc3D {
|
|
917
|
+
const home = homeOf(doc, container);
|
|
918
|
+
const layer = home?.layersById[layerId];
|
|
919
|
+
if (home === null || home === undefined || layer === undefined) return doc;
|
|
920
|
+
let patched: Layer3D;
|
|
921
|
+
if (layer.kind === "component") {
|
|
922
|
+
const overrides = next(layer.component.overrides);
|
|
923
|
+
if (overrides === layer.component.overrides) return doc;
|
|
924
|
+
patched = { ...layer, component: { ...layer.component, overrides } };
|
|
925
|
+
} else if (layer.kind === "instance") {
|
|
926
|
+
const props = next(layer.instance.props);
|
|
927
|
+
if (props === layer.instance.props) return doc;
|
|
928
|
+
patched = { ...layer, instance: { ...layer.instance, props } };
|
|
929
|
+
} else {
|
|
930
|
+
return doc;
|
|
931
|
+
}
|
|
932
|
+
return patchHome(doc, container, { layers: home.layers, layersById: { ...home.layersById, [layerId]: patched } });
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* The master's value of one prop: what the definition DRAWS for its exposed
|
|
937
|
+
* prop (`exposedMasterValue`: the interior layer's value at the prop's target,
|
|
938
|
+
* else the prop's published `default`), or the code component's contract
|
|
939
|
+
* default. Undefined when it is not known.
|
|
940
|
+
*
|
|
941
|
+
* The layer and not `prop.default`: an un-overridden use renders the layer
|
|
942
|
+
* as it stands, and the published default drifts from it once the field is
|
|
943
|
+
* edited anywhere but the props panel (the layer's own section, a stage drag,
|
|
944
|
+
* an agent's `update_layer`). Measured against the stale default, a use that
|
|
945
|
+
* matched the screen read as changed, and a value typed to match the screen
|
|
946
|
+
* was stored as an override.
|
|
947
|
+
*/
|
|
948
|
+
export function masterValueOf(
|
|
949
|
+
doc: Doc3D,
|
|
950
|
+
contracts: Readonly<Record<string, Contract>>,
|
|
951
|
+
use: UseMaster,
|
|
952
|
+
prop: string,
|
|
953
|
+
): Value | undefined {
|
|
954
|
+
if (use.def !== undefined) {
|
|
955
|
+
const def = doc.components?.[use.def];
|
|
956
|
+
const exposed = def?.exposed.find((p) => p.key === prop);
|
|
957
|
+
return def === undefined || exposed === undefined
|
|
958
|
+
? undefined
|
|
959
|
+
: exposedMasterValue(def, exposed, { materials: doc.materials, contracts });
|
|
960
|
+
}
|
|
961
|
+
const contract = contractOfUse(doc, contracts, use);
|
|
962
|
+
return contract === null ? undefined : (contractDefaultsFrom({ master: contract })("master")?.[prop] ?? undefined);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/** What names a use's master: its definition or its module, and, for a layer,
|
|
966
|
+
* where the layer is (whose snapshot contract stands in for the live one). */
|
|
967
|
+
type UseMaster = Pick<ComponentUse, "def" | "component"> &
|
|
968
|
+
Partial<Pick<ComponentUse, "layerId" | "sceneId" | "inDef">>;
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* The contract a code use's props come from: the live registry's, else, for
|
|
972
|
+
* a placed layer, the snapshot it carries, the fallback the Studio's own rows
|
|
973
|
+
* read (InstanceSection). A component that has left the project keeps its
|
|
974
|
+
* props readable in every panel that shows the use, not only in the Studio.
|
|
975
|
+
*/
|
|
976
|
+
export function contractOfUse(
|
|
977
|
+
doc: Doc3D,
|
|
978
|
+
contracts: Readonly<Record<string, Contract>>,
|
|
979
|
+
use: UseMaster,
|
|
980
|
+
): Contract | null {
|
|
981
|
+
if (use.component === undefined) return null;
|
|
982
|
+
const live = contracts[use.component];
|
|
983
|
+
if (live !== undefined) return live;
|
|
984
|
+
const container = use.sceneId ?? use.inDef;
|
|
985
|
+
if (container === null || container === undefined || use.layerId === undefined) return null;
|
|
986
|
+
const layer = homeOf(doc, container)?.layersById[use.layerId];
|
|
987
|
+
return layer === undefined ? null : layerSnapshotContract(layer);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/** A prop's label as the master names it: the published label, or the
|
|
991
|
+
* contract's (`controlLabel`); else its key humanized, never the raw key. */
|
|
992
|
+
export function propLabelOf(
|
|
993
|
+
doc: Doc3D,
|
|
994
|
+
contracts: Readonly<Record<string, Contract>>,
|
|
995
|
+
use: UseMaster,
|
|
996
|
+
prop: string,
|
|
997
|
+
): string {
|
|
998
|
+
if (use.def !== undefined) {
|
|
999
|
+
const label: unknown = doc.components?.[use.def]?.exposed.find((p) => p.key === prop)?.label;
|
|
1000
|
+
return typeof label === "string" && label.trim() !== "" ? label : humanizeKey(prop);
|
|
1001
|
+
}
|
|
1002
|
+
const spec = contractOfUse(doc, contracts, use)?.keys[prop];
|
|
1003
|
+
return spec === undefined ? humanizeKey(prop) : controlLabel(spec);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/** A colour as one canonical hex, or null when it is no colour this lane
|
|
1007
|
+
* reads (`toTrackHex`'s spellings: hex of any length, `rgb()`, `rgba()`). */
|
|
1008
|
+
function canonicalHex(css: string): string | null {
|
|
1009
|
+
const hex = toTrackHex(css);
|
|
1010
|
+
if (hex === null) return null;
|
|
1011
|
+
try {
|
|
1012
|
+
return formatHex(parseHex(hex));
|
|
1013
|
+
} catch {
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/** The control kind a use's master draws for one prop: the row's own spec
|
|
1019
|
+
* (`exposedSpecOf` for a definition's prop, the contract for a code
|
|
1020
|
+
* component's), else the published record's kind. */
|
|
1021
|
+
function propKindOf(
|
|
1022
|
+
doc: Doc3D,
|
|
1023
|
+
contracts: Readonly<Record<string, Contract>>,
|
|
1024
|
+
use: UseMaster,
|
|
1025
|
+
prop: string,
|
|
1026
|
+
): string | undefined {
|
|
1027
|
+
if (use.def !== undefined) {
|
|
1028
|
+
const def = doc.components?.[use.def];
|
|
1029
|
+
const exposed = def?.exposed.find((p) => p.key === prop);
|
|
1030
|
+
if (def === undefined || exposed === undefined) return undefined;
|
|
1031
|
+
return exposedSpecOf(exposed, def, { materials: doc.materials, contracts })?.spec.kind ?? exposed.kind;
|
|
1032
|
+
}
|
|
1033
|
+
return contractOfUse(doc, contracts, use)?.keys[prop]?.kind;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* Whether a use's value is the master's own. A COLOUR prop's value is
|
|
1038
|
+
* compared as the colour it is: a published fill's default is captured in the
|
|
1039
|
+
* spelling its layer holds (`rgba(30,215,96,0.18)`, `#FFF`), while every
|
|
1040
|
+
* colour control writes `#rrggbb[aa]`, so the master's own colour typed back
|
|
1041
|
+
* must read as the master's and store nothing. Any other kind compares as it
|
|
1042
|
+
* is: a text that happens to spell a colour ("#FFF" over a master's
|
|
1043
|
+
* "#ffffff") is what the author typed, and is kept.
|
|
1044
|
+
*/
|
|
1045
|
+
export function sameUseValue(a: Value, b: Value, kind: string | undefined): boolean {
|
|
1046
|
+
if (valueEquals(a, b)) return true;
|
|
1047
|
+
if (kind !== "color" || typeof a !== "string" || typeof b !== "string") return false;
|
|
1048
|
+
const hex = canonicalHex(a);
|
|
1049
|
+
return hex !== null && hex === canonicalHex(b);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* THE ONE WRITE of a use's prop, for the panel's commits and its scrubs. A
|
|
1054
|
+
* value equal to the master is stored as nothing, so the use keeps following
|
|
1055
|
+
* the master (the rule `setComponentOverride` keeps). A locked layer and an
|
|
1056
|
+
* animated instance prop refuse, with the reason. A definition's prop is
|
|
1057
|
+
* written under its own key, taking any `renamedFrom` key it was stored under
|
|
1058
|
+
* with it (`withUseValue`).
|
|
1059
|
+
*/
|
|
1060
|
+
export function writeUseValue(
|
|
1061
|
+
doc: Doc3D,
|
|
1062
|
+
contracts: Readonly<Record<string, Contract>>,
|
|
1063
|
+
key: string,
|
|
1064
|
+
prop: string,
|
|
1065
|
+
value: Value | null,
|
|
1066
|
+
): { doc: Doc3D; refusal: string | null } {
|
|
1067
|
+
const use = useByKey(doc, key, contracts);
|
|
1068
|
+
if (use === null) return { doc, refusal: null };
|
|
1069
|
+
const label = propLabelOf(doc, contracts, use, prop);
|
|
1070
|
+
const animated = usePropRefusal(doc, key, prop, label);
|
|
1071
|
+
if (animated !== null) return { doc, refusal: animated };
|
|
1072
|
+
const master = masterValueOf(doc, contracts, use, prop);
|
|
1073
|
+
const stored =
|
|
1074
|
+
value !== null && master !== undefined && sameUseValue(value, master, propKindOf(doc, contracts, use, prop))
|
|
1075
|
+
? null
|
|
1076
|
+
: value;
|
|
1077
|
+
const aliases =
|
|
1078
|
+
use.def === undefined ? [] : (doc.components?.[use.def]?.exposed.find((p) => p.key === prop)?.renamedFrom ?? []);
|
|
1079
|
+
return unlockedUse(doc, withUseValue(doc, key, prop, stored, aliases));
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/** Reset all to master, with the same lock rule. An animated instance prop
|
|
1083
|
+
* keeps its stored base (`withUseReset`), and the refusal line says which
|
|
1084
|
+
* were kept, so a reset that left values behind never reads as one that
|
|
1085
|
+
* did nothing. */
|
|
1086
|
+
export function resetUse(
|
|
1087
|
+
doc: Doc3D,
|
|
1088
|
+
key: string,
|
|
1089
|
+
contracts: Readonly<Record<string, Contract>> = {},
|
|
1090
|
+
): { doc: Doc3D; refusal: string | null } {
|
|
1091
|
+
const out = unlockedUse(doc, withUseReset(doc, key));
|
|
1092
|
+
if (out.refusal !== null) return out;
|
|
1093
|
+
const use = useByKey(doc, key, contracts);
|
|
1094
|
+
const kept = use === null ? [] : animatedPropsOf(doc, key).map((prop) => propLabelOf(doc, contracts, use, prop));
|
|
1095
|
+
if (kept.length === 0) return out;
|
|
1096
|
+
const names = kept.length === 1 ? kept[0]! : `${kept.slice(0, -1).join(", ")} and ${kept.at(-1)!}`;
|
|
1097
|
+
const said = kept.length === 1 ? "it is animated, and only its keyframes change it" : "they are animated, and only their keyframes change them";
|
|
1098
|
+
return { doc: out.doc, refusal: `Kept ${names}: ${said}.` };
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/** Code components cannot be detached: their master is source code. */
|
|
1102
|
+
export const DETACH_CODE_REFUSAL = "Code components cannot be detached; fork it to change the code.";
|
|
1103
|
+
/** A board card always shows its master: it has no layers to detach into. */
|
|
1104
|
+
export const DETACH_CARD_REFUSAL = "A board card shows the master. Use it in a shot to detach a copy there.";
|
|
1105
|
+
|
|
1106
|
+
/** Why a use cannot be detached, or null: the store's refusal, which the
|
|
1107
|
+
* Detach button also wears as its reason before it is pressed. */
|
|
1108
|
+
export function detachRefusal(use: Pick<ComponentUse, "component">, key: string): string | null {
|
|
1109
|
+
if (use.component !== undefined) return DETACH_CODE_REFUSAL;
|
|
1110
|
+
return useLayerOf(key) === null ? DETACH_CARD_REFUSAL : null;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function unlockedUse(before: Doc3D, after: Doc3D): { doc: Doc3D; refusal: string | null } {
|
|
1114
|
+
const locked = lockedLayerEdited(before, after);
|
|
1115
|
+
return locked === null ? { doc: after, refusal: null } : { doc: before, refusal: lockRefusal(locked) };
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/** A newer version of where a copied definition came from: the workspace
|
|
1119
|
+
* store's latest (`cpt_` origin) or the Community listing's (`cml-`). Code
|
|
1120
|
+
* components never have one: they are live across the workspace. */
|
|
1121
|
+
export interface CopyUpdate {
|
|
1122
|
+
source: string;
|
|
1123
|
+
from: "workspace" | "community";
|
|
1124
|
+
version: number;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
export function copyUpdateOf(
|
|
1128
|
+
def: ComponentDef3D | undefined,
|
|
1129
|
+
workspace: readonly WorkspaceComponent[] | null,
|
|
1130
|
+
listings: Readonly<Record<string, ComponentListingUpdate>>,
|
|
1131
|
+
): CopyUpdate | null {
|
|
1132
|
+
const origin = def?.origin;
|
|
1133
|
+
if (origin === undefined) return null;
|
|
1134
|
+
if (WORKSPACE_COMPONENT_ID_RE.test(origin.source)) {
|
|
1135
|
+
const row = workspace?.find((c) => c.id === origin.source);
|
|
1136
|
+
return row !== undefined && row.version > origin.version
|
|
1137
|
+
? { source: origin.source, from: "workspace", version: row.version }
|
|
1138
|
+
: null;
|
|
1139
|
+
}
|
|
1140
|
+
if (COMPONENT_LISTING_ID_RE.test(origin.source)) {
|
|
1141
|
+
const listing = listings[origin.source];
|
|
1142
|
+
return listing !== undefined && listing.version > origin.version
|
|
1143
|
+
? { source: origin.source, from: "community", version: listing.version }
|
|
1144
|
+
: null;
|
|
1145
|
+
}
|
|
1146
|
+
return null;
|
|
1147
|
+
}
|