@dailies-studio/shot-editor 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.vite/manifest.json +382 -0
- package/dist/assets/AgentPanel-CQejLbiX.js +2 -0
- package/dist/assets/AgentPanel-DJEphah0.css +1 -0
- package/dist/assets/AgentQuestionDialog-BqkD0kXF.js +1 -0
- package/dist/assets/CodeStudio-Dv4ihHpz.js +23 -0
- package/dist/assets/CommentsPanel-B4yhhT1T.js +1 -0
- package/dist/assets/ComponentLibraryRail-Ch_dsAru.js +1 -0
- package/dist/assets/ComponentStage-BRhRotyH.js +1 -0
- package/dist/assets/CurvePane-BjFVabKX.css +1 -0
- package/dist/assets/CurvePane-R22WzllJ.js +1 -0
- package/dist/assets/ExportDialog-CWLQWTmG.js +2 -0
- package/dist/assets/FontPicker-DWKaYL7F.js +1 -0
- package/dist/assets/FontPicker-v0IOIArM.css +1 -0
- package/dist/assets/GenerateDrawer-Bh3SqDKt.js +2 -0
- package/dist/assets/InsertDrawer-Ce1fod1L.js +1 -0
- package/dist/assets/LibraryBrowser-D9sEGz5w.js +2 -0
- package/dist/assets/MakeItYoursSection-CMO4319_.js +2 -0
- package/dist/assets/MaterialGraphCanvas-BFzl4wWR.js +23 -0
- package/dist/assets/MaterialGraphCanvas-BIUD59ik.css +1 -0
- package/dist/assets/MaterialWorkspace-CqTWqaRA.js +14 -0
- package/dist/assets/PreferencesDialog-CBf9_OoD.js +1 -0
- package/dist/assets/PresetsSection-CUEaoCVU.js +1 -0
- package/dist/assets/SoundHistory-BeS8EW1h.js +1 -0
- package/dist/assets/SoundMode-BmcZru3i.js +3 -0
- package/dist/assets/StockSounds-DdAUGRYK.js +1 -0
- package/dist/assets/TemplateFieldSection-CaeXpLFo.js +1 -0
- package/dist/assets/VersionHistoryPanel-CxG6XhNP.js +1 -0
- package/dist/assets/board-B88EPuLO.css +1 -0
- package/dist/assets/board-CL-he7SL.js +3 -0
- package/dist/assets/components-0oOSPN0l.js +1 -0
- package/dist/assets/googleFontRanges-gp32wJMo.js +1 -0
- package/dist/assets/googleFonts-BNon6M7X.js +1 -0
- package/dist/assets/iconsAndAliases-DL7gXP5j.js +1863 -0
- package/dist/assets/index-4F5NQmI8.css +1 -0
- package/dist/assets/index-BGHQGV5z.js +117 -0
- package/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/assets/libraryModel-CpIwfFha.js +1 -0
- package/dist/assets/lucide-6iK64tTB.js +9319 -0
- package/dist/assets/motionStamp-CH-Hew4E.js +1 -0
- package/dist/assets/takeMeasure-Tdg0TxEi.js +1 -0
- package/dist/assets/webmcp-BsB9gxWC.js +2 -0
- package/dist/index.html +34 -3
- package/package.json +12 -9
- package/src/addFamilies.ts +66 -0
- package/src/api.ts +1631 -55
- package/src/app/App.tsx +977 -198
- package/src/app/lazyPanel.tsx +137 -0
- package/src/app/lazySurfaces.tsx +203 -0
- package/src/audio/index.tsx +41 -10
- package/src/audio/meter.ts +485 -0
- package/src/audio/meterMath.ts +273 -0
- package/src/audio/meterStore.ts +155 -0
- package/src/audio/offlineMix.ts +289 -0
- package/src/audio/preview.ts +928 -118
- package/src/bridge.ts +15 -5
- package/src/bridgeTypes.ts +1611 -36
- package/src/channels.ts +17 -32
- package/src/fonts/googleFonts.ts +4 -0
- package/src/hostApi.ts +174 -0
- package/src/icons.tsx +770 -0
- package/src/layerWords.ts +61 -0
- package/src/main.tsx +56 -4
- package/src/panels/ResizeHandle.tsx +33 -43
- package/src/panels/agent/AgentDots.tsx +36 -0
- package/src/panels/agent/AgentPanel.tsx +119 -43
- package/src/panels/agent/AgentQuestionDialog.tsx +202 -0
- package/src/panels/agent/agent.css +113 -97
- package/src/panels/agent/agentGate.ts +35 -0
- package/src/panels/code/CodeDefaultsPanel.tsx +345 -0
- package/src/panels/code/CodeStudio.tsx +373 -0
- package/src/panels/code/FileTree.tsx +265 -0
- package/src/panels/code/codeBufferGuard.ts +52 -0
- package/src/panels/code/codeParts.tsx +321 -0
- package/src/panels/comments/CommentComposer.tsx +239 -0
- package/src/panels/comments/CommentThread.tsx +346 -0
- package/src/panels/comments/CommentsPanel.tsx +336 -0
- package/src/panels/comments/InboxButton.tsx +131 -0
- package/src/panels/comments/MentionMenu.tsx +98 -0
- package/src/panels/comments/parts.tsx +171 -0
- package/src/panels/export/ExportDialog.tsx +919 -96
- package/src/panels/export/ExportScrim.tsx +57 -0
- package/src/panels/files/AssetsTab.tsx +656 -0
- package/src/panels/files/FilePanel.tsx +1466 -216
- package/src/panels/files/LayerMarks.tsx +40 -0
- package/src/panels/files/LeftColumn.tsx +37 -0
- package/src/panels/files/McpSection.tsx +690 -47
- package/src/panels/files/MissingAssetsNotice.tsx +60 -0
- package/src/panels/files/assetDrag.ts +79 -0
- package/src/panels/files/resourceParts.tsx +255 -0
- package/src/panels/fonts/FontPicker.tsx +762 -0
- package/src/panels/fonts/faceCache.ts +207 -0
- package/src/panels/fonts/fontPicker.css +271 -0
- package/src/panels/fonts/pickerModel.ts +265 -0
- package/src/panels/fonts/pickerRequest.ts +46 -0
- package/src/panels/header/AccountAvatar.tsx +76 -0
- package/src/panels/header/AgentChip.tsx +77 -0
- package/src/panels/header/AppMenu.tsx +213 -0
- package/src/panels/header/Header.tsx +564 -84
- package/src/panels/header/HistoryControls.tsx +31 -5
- package/src/panels/header/PresenceStack.tsx +329 -0
- package/src/panels/header/ProjectTitle.tsx +232 -0
- package/src/panels/header/ShareButton.tsx +59 -0
- package/src/panels/header/StatusBadges.tsx +358 -27
- package/src/panels/header/SyncDeletedDialog.tsx +88 -0
- package/src/panels/header/ZoomControls.tsx +46 -15
- package/src/panels/header/agentHandoff.ts +52 -0
- package/src/panels/header/leave.ts +26 -0
- package/src/panels/help/HelpSection.tsx +189 -163
- package/src/panels/insert/InsertDrawer.tsx +723 -0
- package/src/panels/insert/iconSet.ts +109 -0
- package/src/panels/inspector/ExposeDot.tsx +117 -0
- package/src/panels/inspector/FramingRows.tsx +314 -44
- package/src/panels/inspector/InspectorFooter.tsx +91 -0
- package/src/panels/inspector/InspectorHead.tsx +425 -0
- package/src/panels/inspector/InspectorPanel.tsx +408 -124
- package/src/panels/inspector/KeyframeDot.tsx +373 -0
- package/src/panels/inspector/KeyframeSection.tsx +257 -0
- package/src/panels/inspector/MarkerSection.tsx +55 -11
- package/src/panels/inspector/ParamRow.tsx +84 -53
- package/src/panels/inspector/RightColumn.tsx +152 -0
- package/src/panels/inspector/TimeSection.tsx +866 -0
- package/src/panels/inspector/animate/EaseControl.tsx +244 -0
- package/src/panels/inspector/animate/PresetsSection.tsx +135 -0
- package/src/panels/inspector/animate/ShotSection.tsx +562 -48
- package/src/panels/inspector/design/AlignRow.tsx +95 -31
- package/src/panels/inspector/design/AppearanceSection.tsx +118 -9
- package/src/panels/inspector/design/ClonerSection.tsx +323 -62
- package/src/panels/inspector/design/ComponentSection.tsx +50 -43
- package/src/panels/inspector/design/CornerSection.tsx +45 -14
- package/src/panels/inspector/design/DefinitionProps.tsx +315 -0
- package/src/panels/inspector/design/DefinitionSection.tsx +136 -0
- package/src/panels/inspector/design/DeviceSection.tsx +210 -0
- package/src/panels/inspector/design/FillSection.tsx +83 -51
- package/src/panels/inspector/design/FontField.tsx +196 -0
- package/src/panels/inspector/design/HtmlSection.tsx +23 -12
- package/src/panels/inspector/design/InstanceSection.tsx +373 -154
- package/src/panels/inspector/design/MaskSection.tsx +84 -0
- package/src/panels/inspector/design/MaterialSection.tsx +177 -0
- package/src/panels/inspector/design/MediaSection.tsx +273 -120
- package/src/panels/inspector/design/MeshSection.tsx +910 -82
- package/src/panels/inspector/design/PaintRows.tsx +550 -0
- package/src/panels/inspector/design/PlateStyleSection.tsx +177 -0
- package/src/panels/inspector/design/ShadowSection.tsx +221 -48
- package/src/panels/inspector/design/SplineFollowSection.tsx +235 -0
- package/src/panels/inspector/design/StrokeSection.tsx +228 -43
- package/src/panels/inspector/design/TemplateFieldSection.tsx +57 -0
- package/src/panels/inspector/design/TransformSection.tsx +244 -50
- package/src/panels/inspector/design/TypographySection.tsx +679 -110
- package/src/panels/inspector/layout.ts +295 -0
- package/src/panels/inspector/propValue.ts +199 -0
- package/src/panels/inspector/scene/CameraSection.tsx +232 -22
- package/src/panels/inspector/scene/EffectsSection.tsx +722 -71
- package/src/panels/inspector/scene/MakeItYoursSection.tsx +180 -0
- package/src/panels/inspector/scene/SceneDocSection.tsx +191 -0
- package/src/panels/inspector/scene/SceneSection.tsx +219 -44
- package/src/panels/inspector/scene/TransitionSection.tsx +523 -72
- package/src/panels/inspector/storyboard/StoryboardReferenceSection.tsx +593 -0
- package/src/panels/inspector/storyboard/VideoStill.tsx +120 -0
- package/src/panels/inspector/timeModel.ts +190 -0
- package/src/panels/inspector/useDocWrite.ts +438 -146
- package/src/panels/library/LibraryBrowser.tsx +435 -0
- package/src/panels/library/LibraryGrid.tsx +190 -0
- package/src/panels/library/LibraryPreviewPanel.tsx +349 -0
- package/src/panels/library/LibraryRail.tsx +84 -0
- package/src/panels/library/LibrarySoundPanel.tsx +293 -0
- package/src/panels/library/SoundHistory.tsx +245 -0
- package/src/panels/library/StockSounds.tsx +390 -0
- package/src/panels/library/libraryModel.ts +147 -0
- package/src/panels/library/stockSearch.ts +77 -0
- package/src/panels/maskMarks.tsx +117 -0
- package/src/panels/material/MaterialCommentNode.tsx +54 -0
- package/src/panels/material/MaterialGraphCanvas.tsx +861 -0
- package/src/panels/material/MaterialGraphMenu.tsx +45 -0
- package/src/panels/material/MaterialGroupNode.tsx +56 -0
- package/src/panels/material/MaterialLibrary.tsx +253 -0
- package/src/panels/material/MaterialNode.tsx +453 -0
- package/src/panels/material/MaterialNodePalette.tsx +349 -0
- package/src/panels/material/MaterialParameterControls.tsx +525 -0
- package/src/panels/material/MaterialRerouteNode.tsx +42 -0
- package/src/panels/material/MaterialWorkspace.tsx +501 -0
- package/src/panels/material/NodePreview.tsx +70 -0
- package/src/panels/material/flowModel.ts +441 -0
- package/src/panels/material/parameterValues.ts +231 -0
- package/src/panels/material/previewScheduler.ts +235 -0
- package/src/panels/material/reactFlow.css +6 -0
- package/src/panels/material/socketGlyph.ts +85 -0
- package/src/panels/material/viewportMemory.ts +58 -0
- package/src/panels/preferences/PreferencesDialog.tsx +215 -0
- package/src/panels/share/ShareComponentDialog.tsx +247 -0
- package/src/panels/stage/StageMenu.tsx +35 -5
- package/src/panels/stage/StagePanel.tsx +338 -23
- package/src/panels/stage/cellSlots.tsx +70 -0
- package/src/panels/storyboard/BoardCanvas.tsx +739 -0
- package/src/panels/storyboard/BoardCursors.tsx +158 -0
- package/src/panels/storyboard/BoardDropDialog.tsx +161 -0
- package/src/panels/storyboard/BoardPlan.tsx +42 -0
- package/src/panels/storyboard/ComponentSheet.tsx +187 -0
- package/src/panels/storyboard/ComponentStage.tsx +235 -0
- package/src/panels/storyboard/MiniSketch.tsx +173 -0
- package/src/panels/storyboard/PartPicker.tsx +64 -0
- package/src/panels/storyboard/PlanPanel.tsx +617 -0
- package/src/panels/storyboard/ReferencePreview.tsx +217 -0
- package/src/panels/storyboard/ShotBrief.tsx +360 -0
- package/src/panels/storyboard/ShotCard.tsx +251 -0
- package/src/panels/storyboard/StagePreview.tsx +348 -0
- package/src/panels/storyboard/StillThumbs.tsx +162 -0
- package/src/panels/storyboard/StoryboardView.tsx +79 -0
- package/src/panels/storyboard/board.css +1065 -0
- package/src/panels/storyboard/board.ts +12 -0
- package/src/panels/storyboard/boardClipboard.ts +111 -0
- package/src/panels/storyboard/boardOrder.ts +68 -0
- package/src/panels/storyboard/boardRows.tsx +425 -0
- package/src/panels/storyboard/liveCap.ts +90 -0
- package/src/panels/storyboard/plan.css +295 -0
- package/src/panels/storyboard/posterCache.ts +133 -0
- package/src/panels/storyboard/shotboard/ComponentPicker.tsx +373 -0
- package/src/panels/storyboard/shotboard/ImagePicker.tsx +145 -0
- package/src/panels/storyboard/shotboard/ShotBoard.tsx +292 -0
- package/src/panels/storyboard/shotboard/SketchCanvas.tsx +443 -0
- package/src/panels/storyboard/shotboard/SketchInspector.tsx +471 -0
- package/src/panels/storyboard/shotboard/SketchItem.tsx +240 -0
- package/src/panels/storyboard/shotboard/SketchLayers.tsx +130 -0
- package/src/panels/storyboard/shotboard/SketchPlay.tsx +108 -0
- package/src/panels/storyboard/shotboard/SketchStrip.tsx +178 -0
- package/src/panels/storyboard/shotboard/SketchToolFooter.tsx +99 -0
- package/src/panels/storyboard/shotboard/sketch.css +1392 -0
- package/src/panels/storyboard/shotboard/sketchModel.ts +219 -0
- package/src/panels/storyboard/storyboardDrop.ts +164 -0
- package/src/panels/studio/ComponentLibraryRail.tsx +346 -0
- package/src/panels/studio/ComponentSidePanel.tsx +355 -0
- package/src/panels/studio/ComponentStudio.tsx +243 -0
- package/src/panels/studio/ComponentsHome.tsx +318 -0
- package/src/panels/studio/ComponentsPreview.tsx +179 -0
- package/src/panels/studio/EveryUse.tsx +244 -0
- package/src/panels/studio/ThisUsePanel.tsx +679 -0
- package/src/panels/studio/componentParts.tsx +341 -0
- package/src/panels/studio/components.ts +6 -0
- package/src/panels/studio/newCodeComponent.ts +66 -0
- package/src/panels/timeline/AudioFxButton.tsx +290 -0
- package/src/panels/timeline/AudioLane.tsx +674 -85
- package/src/panels/timeline/BlockRow.tsx +154 -85
- package/src/panels/timeline/ChannelRow.tsx +378 -129
- package/src/panels/timeline/CurvePane.tsx +508 -0
- package/src/panels/timeline/Filmstrip.tsx +361 -0
- package/src/panels/timeline/GenerateDrawer.tsx +407 -0
- package/src/panels/timeline/KeyStretch.tsx +115 -0
- package/src/panels/timeline/LifespanRow.tsx +730 -47
- package/src/panels/timeline/MarkerLane.tsx +46 -8
- package/src/panels/timeline/PlayButton.tsx +91 -0
- package/src/panels/timeline/PropertyPicker.tsx +254 -0
- package/src/panels/timeline/ShotRow.tsx +400 -0
- package/src/panels/timeline/SoundMode.tsx +1406 -0
- package/src/panels/timeline/TimelineDock.tsx +2338 -272
- package/src/panels/timeline/TransportVolume.tsx +163 -0
- package/src/panels/timeline/beatRuler.ts +74 -0
- package/src/panels/timeline/curveModel.ts +500 -0
- package/src/panels/timeline/duckUnderVoice.ts +68 -0
- package/src/panels/timeline/keyframeMenu.ts +138 -0
- package/src/panels/timeline/labelWidth.ts +49 -0
- package/src/panels/timeline/marquee.ts +155 -0
- package/src/panels/timeline/model.ts +1860 -199
- package/src/panels/timeline/snap.ts +125 -43
- package/src/panels/timeline/staggerPreview.tsx +92 -0
- package/src/panels/timeline/takeMeasure.ts +72 -0
- package/src/panels/timeline/useLive.ts +48 -0
- package/src/panels/timeline/viewport.ts +234 -0
- package/src/panels/toolbar/BackChip.tsx +116 -0
- package/src/panels/toolbar/CameraViewBar.tsx +118 -49
- package/src/panels/toolbar/CanvasBar.tsx +117 -0
- package/src/panels/toolbar/InsertButton.tsx +38 -0
- package/src/panels/toolbar/IsolationTrail.tsx +156 -0
- package/src/panels/toolbar/PanelToggles.tsx +86 -0
- package/src/panels/toolbar/ShadingBar.tsx +119 -26
- package/src/panels/toolbar/ToolDockBar.tsx +329 -80
- package/src/panels/toolbar/ToolFooter.tsx +102 -0
- package/src/panels/toolbar/ViewModeBar.tsx +247 -38
- package/src/panels/toolbar/WorkPlaneBar.tsx +94 -0
- package/src/panels/toolbar/useAddRows.tsx +326 -0
- package/src/panels/toolbar/useDismiss.ts +122 -0
- package/src/panels/versions/VersionHistoryPanel.tsx +274 -0
- package/src/panels/versions/versionModel.ts +103 -0
- package/src/panels/virtualWindow.ts +271 -0
- package/src/shotTokens.css +151 -60
- package/src/stage/FrustumGizmo.tsx +504 -84
- package/src/stage/GridPlane.tsx +225 -16
- package/src/stage/NodePreviews.tsx +628 -0
- package/src/stage/PerfHud.tsx +437 -47
- package/src/stage/SplitRenderer.tsx +816 -169
- package/src/stage/StageApp.tsx +4274 -486
- package/src/stage/StageRootBoundary.tsx +114 -0
- package/src/stage/StageTools.tsx +651 -151
- package/src/stage/TransformGizmo.tsx +696 -0
- package/src/stage/adaptivePresentation.ts +145 -0
- package/src/stage/bridgeClient.ts +2522 -41
- package/src/stage/exportWorker.ts +9 -0
- package/src/stage/feed.ts +305 -14
- package/src/stage/fontPreview.ts +176 -0
- package/src/stage/frustumGeometry.ts +319 -1
- package/src/stage/furnitureTokens.ts +33 -0
- package/src/stage/gizmoMath.ts +508 -16
- package/src/stage/gpuTimer.ts +276 -0
- package/src/stage/instanceModules.ts +194 -33
- package/src/stage/interactions/move.ts +24 -5
- package/src/stage/isolation.ts +291 -33
- package/src/stage/jsSelfProfiler.ts +325 -0
- package/src/stage/main.tsx +116 -16
- package/src/stage/modalState.ts +222 -23
- package/src/stage/moduleEpoch.ts +60 -0
- package/src/stage/motionPathStage.ts +279 -0
- package/src/stage/motionPreview.ts +74 -0
- package/src/stage/overlays/Overlays.tsx +1112 -159
- package/src/stage/overlays/Rulers.tsx +495 -0
- package/src/stage/panes.ts +159 -10
- package/src/stage/project.ts +68 -3
- package/src/stage/renderScaleController.ts +730 -0
- package/src/stage/rulers.ts +129 -0
- package/src/stage/sceneExtent.ts +75 -0
- package/src/stage/shading.ts +20 -0
- package/src/stage/splineStage.ts +632 -0
- package/src/stage/stagePerformanceTelemetry.ts +717 -0
- package/src/stage/stageTheme.ts +36 -0
- package/src/stage/stageWarmup.ts +11 -0
- package/src/stage/textAutoSize.ts +124 -0
- package/src/stage/tier1Export.ts +641 -0
- package/src/stage/tokenColor.ts +14 -9
- package/src/stage/view.ts +56 -9
- package/src/stage/workingView.ts +277 -33
- package/src/state/accountPrefs.ts +261 -0
- package/src/state/agentActions.ts +173 -15
- package/src/state/agentActivity.ts +198 -0
- package/src/state/agentQuestion.ts +239 -0
- package/src/state/bootOpen.ts +42 -0
- package/src/state/cameraVerbs.ts +139 -0
- package/src/state/clientId.ts +6 -1
- package/src/state/comments.ts +1099 -0
- package/src/state/context.tsx +19 -1
- package/src/state/editorPrefs.ts +220 -0
- package/src/state/errorReports.ts +121 -0
- package/src/state/fieldWrite.ts +161 -0
- package/src/state/fontRecents.ts +70 -0
- package/src/state/fonts.ts +656 -0
- package/src/state/graphClipboard.ts +414 -0
- package/src/state/imageThumbs.ts +216 -0
- package/src/state/insert.ts +444 -67
- package/src/state/isolationTrail.ts +92 -0
- package/src/state/keyMoves.ts +134 -0
- package/src/state/keyframeClipboard.ts +177 -0
- package/src/state/keymap.ts +1153 -117
- package/src/state/layoutPrefs.ts +272 -0
- package/src/state/layouts.ts +205 -0
- package/src/state/measureMedia.ts +167 -0
- package/src/state/menu.ts +2341 -28
- package/src/state/motionStamp.ts +7 -0
- package/src/state/panelSize.ts +375 -5
- package/src/state/pendingChanges.ts +119 -0
- package/src/state/playHold.ts +78 -0
- package/src/state/playbackPreparation.ts +562 -0
- package/src/state/posterCapture.ts +242 -0
- package/src/state/presence.ts +254 -0
- package/src/state/projectComponents.ts +160 -0
- package/src/state/renderEvents.ts +298 -18
- package/src/state/runtimeErrors.ts +46 -0
- package/src/state/selection.ts +12 -0
- package/src/state/shortcuts.ts +1606 -0
- package/src/state/sketchGuide.ts +51 -0
- package/src/state/splineRefs.ts +87 -0
- package/src/state/stageHealth.ts +259 -0
- package/src/state/stageWatchdog.ts +47 -0
- package/src/state/stillStage.ts +330 -0
- package/src/state/stillThumbs.ts +474 -0
- package/src/state/store.ts +23591 -2982
- package/src/state/storyboardModel.ts +528 -0
- package/src/state/sync.ts +1257 -0
- package/src/state/syncCache.ts +159 -0
- package/src/state/triangles.ts +13 -0
- package/src/state/types.ts +1698 -55
- package/src/state/unsavedGuard.ts +49 -0
- package/src/state/viewMode.ts +200 -0
- package/src/state/webmcp.ts +1128 -0
- package/src/state/workspace.ts +1147 -0
- package/src/styles/assets-tab.css +304 -0
- package/src/styles/canvas.css +545 -0
- package/src/styles/comments.css +1435 -0
- package/src/styles/controls.css +162 -0
- package/src/styles/dialogs.css +390 -0
- package/src/styles/insert-drawer.css +586 -0
- package/src/styles/inspector-board.css +207 -0
- package/src/styles/inspector-object.css +486 -0
- package/src/styles/inspector-shot.css +252 -0
- package/src/styles/inspector.css +360 -0
- package/src/styles/left-column.css +1628 -0
- package/src/styles/library-preview.css +575 -0
- package/src/styles/library.css +512 -0
- package/src/styles/modes/code.css +569 -0
- package/src/styles/modes/component.css +1561 -0
- package/src/styles/modes/material.css +1201 -0
- package/src/styles/modes/storyboard.css +518 -0
- package/src/styles/share-component.css +49 -0
- package/src/styles/shell.css +653 -0
- package/src/styles/shot-brief.css +360 -0
- package/src/styles/states.css +76 -0
- package/src/styles/sync.css +380 -0
- package/src/styles/timeline-graph.css +384 -0
- package/src/styles/timeline-meters.css +89 -0
- package/src/styles/timeline-rows.css +1959 -0
- package/src/styles/timeline-sound.css +1003 -0
- package/src/styles/timeline.css +754 -0
- package/src/styles/tool-footer.css +525 -0
- package/src/styles/topbar.css +886 -0
- package/src/styles/versions.css +262 -0
- package/src/styles.css +72 -2921
- package/src/tools.ts +108 -23
- package/dist/assets/index-B7notge7.css +0 -1
- package/dist/assets/index-BBE6Ta9L.js +0 -52
- package/src/panels/files/icons.tsx +0 -118
- package/src/panels/header/DocSwitcher.tsx +0 -85
- package/src/panels/header/TransportBar.tsx +0 -86
- package/src/panels/inspector/animate/BlockTiming.tsx +0 -341
- package/src/panels/inspector/animate/DuringBlocks.tsx +0 -85
- package/src/panels/inspector/animate/InOutPresets.tsx +0 -57
- package/src/panels/inspector/animate/ResolvedChannels.tsx +0 -150
- package/src/panels/inspector/animate/useBlocks.ts +0 -43
- package/src/panels/material/MaterialPicker.tsx +0 -172
- package/src/panels/timeline/SceneStrip.tsx +0 -145
- package/src/panels/toolbar/AddMenu.tsx +0 -230
- package/src/stage/MoveGizmo.tsx +0 -411
package/src/stage/StageTools.tsx
CHANGED
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
// It bumps React only when a corner moved by more than half a CSS pixel. Below
|
|
16
16
|
// that the SVG rounds to the same path and the render is pure cost, and the
|
|
17
17
|
// camera moves on every frame of a dolly.
|
|
18
|
-
import { useMemo } from "react";
|
|
18
|
+
import { useLayoutEffect, useMemo, useRef } from "react";
|
|
19
19
|
import { useFrame, useThree } from "@react-three/fiber";
|
|
20
|
-
import { OrthographicCamera, PerspectiveCamera, Raycaster, Vector2 } from "three";
|
|
20
|
+
import { Matrix4, OrthographicCamera, PerspectiveCamera, Raycaster, Vector2, Vector3 } from "three";
|
|
21
21
|
import type { Camera, Object3D } from "three";
|
|
22
22
|
import {
|
|
23
23
|
SHOT_LAYER_BOX,
|
|
@@ -25,26 +25,50 @@ import {
|
|
|
25
25
|
SHOT_LAYER_LOCKED,
|
|
26
26
|
SHOT_LAYER_ORDER,
|
|
27
27
|
SHOT_LAYER_PICKABLE,
|
|
28
|
+
layerStampEpoch,
|
|
28
29
|
} from "@dailies-studio/shot";
|
|
29
|
-
import {
|
|
30
|
-
|
|
30
|
+
import {
|
|
31
|
+
cameraClipPlanes,
|
|
32
|
+
layerAliveAt,
|
|
33
|
+
maskTargetOf,
|
|
34
|
+
memoLast,
|
|
35
|
+
meshPrimitiveOf,
|
|
36
|
+
motionPathOf,
|
|
37
|
+
outermost,
|
|
38
|
+
parentOf,
|
|
39
|
+
resolveLayerAt,
|
|
40
|
+
resolveTracksAt,
|
|
41
|
+
} from "@dailies-studio/shot/doc";
|
|
42
|
+
import type { Doc3D, LayerLookup, MotionPath } from "@dailies-studio/shot/doc";
|
|
31
43
|
import { nearestHandle, quadCentre, rotateRingAt } from "./handles.js";
|
|
32
44
|
import {
|
|
45
|
+
maskHitsLast,
|
|
33
46
|
pickInRect,
|
|
34
47
|
pickLayers,
|
|
35
|
-
pickLayersByDistance,
|
|
36
48
|
projectQuad,
|
|
37
49
|
quadMoved,
|
|
50
|
+
rayHitsQuad,
|
|
38
51
|
screenQuad,
|
|
52
|
+
toScreen,
|
|
39
53
|
unionQuad,
|
|
54
|
+
worldQuad,
|
|
40
55
|
} from "./project.js";
|
|
41
56
|
import type { LayerFrame, ScreenQuad } from "./project.js";
|
|
42
|
-
import
|
|
57
|
+
import { projectPeers } from "./feed.js";
|
|
58
|
+
import type { FailedQuad, MaskQuad, PeerQuad, StageFeed } from "./feed.js";
|
|
43
59
|
import type { PaneRect, StagePane } from "./panes.js";
|
|
44
|
-
import { WORKING_FOV_DEG, workingPose } from "./workingView.js";
|
|
60
|
+
import { WORKING_FOV_DEG, orthoEyePushBack, viewBasis, workingPose } from "./workingView.js";
|
|
45
61
|
import type { WorkingView } from "./workingView.js";
|
|
46
|
-
import {
|
|
47
|
-
import
|
|
62
|
+
import type { HeadOnCamera } from "./isolation.js";
|
|
63
|
+
import {
|
|
64
|
+
projectSplineLayer,
|
|
65
|
+
splineScreenBounds,
|
|
66
|
+
splineScreenHit,
|
|
67
|
+
splineScreenIntersectsRect,
|
|
68
|
+
} from "./splineStage.js";
|
|
69
|
+
import type { ScreenSpline } from "./splineStage.js";
|
|
70
|
+
import { motionPathHandleAt, motionPathKeyAt, motionPathsMoved, projectMotionPath } from "./motionPathStage.js";
|
|
71
|
+
import type { ScreenMotionPath } from "./motionPathStage.js";
|
|
48
72
|
|
|
49
73
|
/** Editor-only geometry lives on layer bit 1, and only the working camera sees
|
|
50
74
|
* it. Nothing draws on it yet — the frustum gizmo is the first thing that
|
|
@@ -70,9 +94,147 @@ function unionMoved(a: StageFeed["union"], b: StageFeed["union"]): boolean {
|
|
|
70
94
|
return false;
|
|
71
95
|
}
|
|
72
96
|
|
|
97
|
+
/**
|
|
98
|
+
* The frames this pass brought up to date (`fresh`). Cleared at the top of
|
|
99
|
+
* each pass; one pass at a time, since the stage has one canvas and
|
|
100
|
+
* `useFrame` is never re-entered.
|
|
101
|
+
*/
|
|
102
|
+
const FRESH = new Set<LayerFrame>();
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A frame with its world matrix, and its parents', up to date for this pass.
|
|
106
|
+
*
|
|
107
|
+
* The runtime wrote the pass's positions, but three only folds them into
|
|
108
|
+
* `matrixWorld` when it renders, AFTER the frame loop. Read stale, a box that
|
|
109
|
+
* grew (auto text on commit) drew its new size around its old centre until
|
|
110
|
+
* the next commit. So every projection the pass makes takes its frame through
|
|
111
|
+
* here, and a frame nothing projects is left to the render, which recomposes
|
|
112
|
+
* every one anyway: bringing them all up front was the whole graph twice per
|
|
113
|
+
* frame, cloners included. Parents and self only: children (glyph's commit
|
|
114
|
+
* hook lives in `updateMatrixWorld`) are the renderer's.
|
|
115
|
+
*/
|
|
116
|
+
function fresh(frame: LayerFrame): LayerFrame {
|
|
117
|
+
if (!FRESH.has(frame)) {
|
|
118
|
+
FRESH.add(frame);
|
|
119
|
+
frame.object?.updateWorldMatrix(true, false);
|
|
120
|
+
}
|
|
121
|
+
return frame;
|
|
122
|
+
}
|
|
123
|
+
|
|
73
124
|
/** Reused per frame; picking allocates enough already without a raycaster. */
|
|
74
125
|
const RAYCASTER = new Raycaster();
|
|
75
126
|
const NDC = new Vector2();
|
|
127
|
+
const PANE_AT = new Vector3();
|
|
128
|
+
const IDENTITY = new Matrix4();
|
|
129
|
+
/** How near a key dot a press has to land, in CSS px: the dot is r 4. */
|
|
130
|
+
const PATH_KEY_GRAB_PX = 8;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The matrix a layer's Position is written in: its group's PARENT, which is
|
|
134
|
+
* the parent layer's group, or a wrapper between (a splineFollow rig).
|
|
135
|
+
*
|
|
136
|
+
* A layer with no frame this pass (outside its lifetime, or hidden) still
|
|
137
|
+
* draws its path, dimmed, so the matrix is found another way: the group if it
|
|
138
|
+
* is still mounted, then the parent layer's group, then a mounted sibling's
|
|
139
|
+
* parent, then the scene root.
|
|
140
|
+
*/
|
|
141
|
+
function parentWorldOf(
|
|
142
|
+
id: string,
|
|
143
|
+
byId: ReadonlyMap<string, LayerFrame>,
|
|
144
|
+
root: Object3D,
|
|
145
|
+
layers: LayerLookup,
|
|
146
|
+
): Matrix4 {
|
|
147
|
+
const self = byId.get(id);
|
|
148
|
+
const own = self?.object?.parent;
|
|
149
|
+
if (self !== undefined && own != null) {
|
|
150
|
+
fresh(self);
|
|
151
|
+
return own.matrixWorld;
|
|
152
|
+
}
|
|
153
|
+
let mounted: Object3D | null = null;
|
|
154
|
+
root.traverse((o) => {
|
|
155
|
+
if (mounted === null && o.userData[SHOT_LAYER_KEY] === id) mounted = o;
|
|
156
|
+
});
|
|
157
|
+
const mountedParent = (mounted as Object3D | null)?.parent;
|
|
158
|
+
if (mountedParent != null) {
|
|
159
|
+
mountedParent.updateWorldMatrix(true, false);
|
|
160
|
+
return mountedParent.matrixWorld;
|
|
161
|
+
}
|
|
162
|
+
const parentId = parentOf(layers, id);
|
|
163
|
+
const parentFrame = parentId === null ? undefined : byId.get(parentId);
|
|
164
|
+
if (parentFrame?.object !== undefined) {
|
|
165
|
+
fresh(parentFrame);
|
|
166
|
+
return parentFrame.object.matrixWorld;
|
|
167
|
+
}
|
|
168
|
+
for (const [otherId, frame] of byId) {
|
|
169
|
+
if (otherId !== id && frame.object?.parent != null && parentOf(layers, otherId) === parentId) {
|
|
170
|
+
fresh(frame);
|
|
171
|
+
return frame.object.parent.matrixWorld;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return IDENTITY;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
interface ProjectedSplineFrame {
|
|
178
|
+
frame: LayerFrame;
|
|
179
|
+
screen: ScreenSpline;
|
|
180
|
+
renderGeometry: boolean;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function projectedSplines(
|
|
184
|
+
frames: readonly LayerFrame[],
|
|
185
|
+
layers: LayerLookup | null,
|
|
186
|
+
sceneFrame: number,
|
|
187
|
+
fps: Doc3D["fps"] | null,
|
|
188
|
+
camera: Camera | null,
|
|
189
|
+
width: number,
|
|
190
|
+
height: number,
|
|
191
|
+
): ProjectedSplineFrame[] {
|
|
192
|
+
if (layers === null || camera === null || width <= 0 || height <= 0) return [];
|
|
193
|
+
const out: ProjectedSplineFrame[] = [];
|
|
194
|
+
for (const frame of frames) {
|
|
195
|
+
// THE STORED LAYER SAYS WHETHER IT IS A SPLINE, before any track is
|
|
196
|
+
// sampled: neither the kind nor the primitive's kind is trackable, and
|
|
197
|
+
// every other tracked layer (each cloner clone too) paid a full resolve
|
|
198
|
+
// per pane per frame only to be skipped.
|
|
199
|
+
const stored = layers.layersById[frame.id];
|
|
200
|
+
if (stored?.kind !== "mesh" || meshPrimitiveOf(stored.mesh)?.kind !== "spline") continue;
|
|
201
|
+
const layer = fps === null ? stored : resolveTracksAt(stored, sceneFrame, fps);
|
|
202
|
+
if (layer.kind !== "mesh") continue;
|
|
203
|
+
const primitive = meshPrimitiveOf(layer.mesh);
|
|
204
|
+
if (primitive?.kind !== "spline") continue;
|
|
205
|
+
const renderGeometry = primitive.renderGeometry !== false;
|
|
206
|
+
const screen = projectSplineLayer(primitive, fresh(frame), camera, width, height, [], renderGeometry);
|
|
207
|
+
if (screen !== null) out.push({ frame, screen, renderGeometry });
|
|
208
|
+
}
|
|
209
|
+
return out;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function uniqueIds(rows: readonly { id: string }[]): string[] {
|
|
213
|
+
const seen = new Set<string>();
|
|
214
|
+
const out: string[] = [];
|
|
215
|
+
for (const row of rows) {
|
|
216
|
+
if (seen.has(row.id)) continue;
|
|
217
|
+
seen.add(row.id);
|
|
218
|
+
out.push(row.id);
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function guidesMoved(
|
|
224
|
+
before: StageFeed["splineGuidesWorking"],
|
|
225
|
+
after: StageFeed["splineGuidesWorking"],
|
|
226
|
+
): boolean {
|
|
227
|
+
if (before.length !== after.length) return true;
|
|
228
|
+
for (let index = 0; index < after.length; index += 1) {
|
|
229
|
+
const a = before[index];
|
|
230
|
+
const b = after[index];
|
|
231
|
+
if (a === undefined || b === undefined || a.id !== b.id || a.selected !== b.selected || a.pts.length !== b.pts.length) {
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
if (a.pts.some((value, pointIndex) => Math.abs(value - (b.pts[pointIndex] ?? Number.NaN)) > 0.5)) return true;
|
|
235
|
+
}
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
76
238
|
|
|
77
239
|
/**
|
|
78
240
|
* `collectFrames`, replayed while nothing about the graph's MEMBERSHIP changed.
|
|
@@ -89,9 +251,21 @@ const NDC = new Vector2();
|
|
|
89
251
|
* (the whole store is immutable), and an edit is what changes a layer's box,
|
|
90
252
|
* its lock or its pickability — all of which this walk COPIES rather than
|
|
91
253
|
* referencing;
|
|
92
|
-
* - `
|
|
254
|
+
* - `stamps`, the runtime's stamp counter, because those copied values are also
|
|
255
|
+
* PER-FRAME values: a size track moves the box without touching `layers`;
|
|
93
256
|
* - `epoch`, the mount counter, because a model or an instance module resolving
|
|
94
|
-
* commits a subtree that no value above predicts.
|
|
257
|
+
* commits a subtree that no value above predicts. A lifetime ending is a
|
|
258
|
+
* `LayerNode` unmounting, so it rides this one too.
|
|
259
|
+
*
|
|
260
|
+
* ── WHY `stamps` AND NOT THE PLAYHEAD ────────────────────────────────────
|
|
261
|
+
*
|
|
262
|
+
* The frame used to be the key term standing in for "a copied value moved",
|
|
263
|
+
* which is correct and maximally pessimistic: the playhead changes on every
|
|
264
|
+
* played frame, the stamps change only while something is actually resizing or
|
|
265
|
+
* restacking, so playback of a scene with no size track re-walked the entire
|
|
266
|
+
* graph 30 times a second to rebuild an identical answer. `LayerNode` now
|
|
267
|
+
* compares each stamp before writing it and bumps `layerStampEpoch` only on a
|
|
268
|
+
* real change, which is the same guarantee stated honestly.
|
|
95
269
|
*
|
|
96
270
|
* What is deliberately NOT in the key is motion. `matrixWorld` is stored as a
|
|
97
271
|
* LIVE REFERENCE (see `collectFrames`), so an orbit, a pan, a scrub within one
|
|
@@ -99,20 +273,41 @@ const NDC = new Vector2();
|
|
|
99
273
|
* point at. That is why replaying the walk is exact rather than merely close.
|
|
100
274
|
*/
|
|
101
275
|
const collectFramesCached = memoLast(
|
|
102
|
-
(root: Object3D, _layers: LayerLookup | null,
|
|
276
|
+
(root: Object3D, _layers: LayerLookup | null, _stamps: number, _epoch: number): LayerFrame[] =>
|
|
103
277
|
collectFrames(root),
|
|
104
278
|
);
|
|
105
279
|
|
|
280
|
+
/**
|
|
281
|
+
* The walk's answer by id (the first frame of each, see the pass) and its
|
|
282
|
+
* pickable part, replayed while the walk's answer and the isolation scope
|
|
283
|
+
* hold: they change with nothing else, and rebuilding them was a Map and two
|
|
284
|
+
* arrays the size of the graph on every pass, cloners included.
|
|
285
|
+
*/
|
|
286
|
+
export const indexFrames = memoLast(
|
|
287
|
+
(
|
|
288
|
+
frames: readonly LayerFrame[],
|
|
289
|
+
isolated: ReadonlySet<string> | null,
|
|
290
|
+
): { byId: ReadonlyMap<string, LayerFrame>; pickable: readonly LayerFrame[] } => {
|
|
291
|
+
const byId = new Map<string, LayerFrame>();
|
|
292
|
+
for (const f of frames) if (!byId.has(f.id)) byId.set(f.id, f);
|
|
293
|
+
const pickable = frames.filter((f) => f.pickable !== false && (isolated === null || isolated.has(f.id)));
|
|
294
|
+
return { byId, pickable };
|
|
295
|
+
},
|
|
296
|
+
);
|
|
297
|
+
|
|
106
298
|
export function StageTools({
|
|
107
299
|
feed,
|
|
108
300
|
panes,
|
|
109
301
|
workingView,
|
|
110
302
|
selection,
|
|
111
303
|
layers,
|
|
112
|
-
|
|
304
|
+
sceneFrameAt,
|
|
305
|
+
fps,
|
|
113
306
|
isolationRoot,
|
|
114
307
|
isolated,
|
|
115
|
-
|
|
308
|
+
headOn,
|
|
309
|
+
motionPaths,
|
|
310
|
+
passDrawing,
|
|
116
311
|
}: {
|
|
117
312
|
feed: StageFeed;
|
|
118
313
|
/** The two pane rectangles, in CSS px from the CANVAS top-left. Everything
|
|
@@ -124,18 +319,31 @@ export function StageTools({
|
|
|
124
319
|
* not `scene`, because `scene` in this file is three's own scene graph. Null
|
|
125
320
|
* before a document has arrived, or between scenes. */
|
|
126
321
|
layers: LayerLookup | null;
|
|
127
|
-
/** The playhead, in the scene's own frames
|
|
128
|
-
*
|
|
129
|
-
|
|
322
|
+
/** The playhead, in the scene's own frames, read at FRAME time from the
|
|
323
|
+
* stage's clock (StageApp's `playhead`). Samples the active spline's
|
|
324
|
+
* tracked profile/taper; the frame walk keys on `layerStampEpoch` instead. */
|
|
325
|
+
sceneFrameAt: () => number;
|
|
326
|
+
/** Track sampling clock for active spline profile/taper values. */
|
|
327
|
+
fps: Doc3D["fps"] | null;
|
|
130
328
|
/** The isolated layer itself, which is the BOUNDARY the parent walk stops at
|
|
131
329
|
* — not the same thing as `isolated`, which is its whole subtree. */
|
|
132
330
|
isolationRoot: string | null;
|
|
133
331
|
/** The isolated subtree's ids, or null when nothing is isolated. Picking is
|
|
134
332
|
* scoped to it — see the filter below. */
|
|
135
333
|
isolated: ReadonlySet<string> | null;
|
|
136
|
-
/** The head-on
|
|
137
|
-
*
|
|
138
|
-
|
|
334
|
+
/** The derived head-on camera, or null once the orbit rig owns the pane.
|
|
335
|
+
* Already solved against the chrome inset by `StageApp` — this file installs
|
|
336
|
+
* it and does not re-derive any part of it. Non-null for BOTH head-on modes:
|
|
337
|
+
* an isolated subtree and an open component definition. */
|
|
338
|
+
headOn: HeadOnCamera | null;
|
|
339
|
+
/** Draw the selected layers' motion paths (gap 3). StageApp folds the
|
|
340
|
+
* `motionPaths` pref with every state that hides them: playing, watching,
|
|
341
|
+
* a material preview, an export, and any gesture but a path key drag. */
|
|
342
|
+
motionPaths: boolean;
|
|
343
|
+
/** A preparing pass is drawing its own frames under a frozen picture
|
|
344
|
+
* (bridge v51 `passDrawing`), read at frame time: the client's stable
|
|
345
|
+
* getter, never a prop value a render would have to deliver. */
|
|
346
|
+
passDrawing: () => boolean;
|
|
139
347
|
}): null {
|
|
140
348
|
const scene = useThree((s) => s.scene);
|
|
141
349
|
const camera = useThree((s) => s.camera);
|
|
@@ -170,8 +378,119 @@ export function StageTools({
|
|
|
170
378
|
return cam;
|
|
171
379
|
}, []);
|
|
172
380
|
|
|
381
|
+
/**
|
|
382
|
+
* Pose the camera as a pull-based operation, not only as a side effect of
|
|
383
|
+
* `useFrame`. ClockDriver's compile barrier runs before priority-0 frame
|
|
384
|
+
* subscribers, and SplitRenderer must compile with the same camera that the
|
|
385
|
+
* subscriber below will hand to the draw.
|
|
386
|
+
*/
|
|
387
|
+
const prepareWorkingCamera = (): Camera | null => {
|
|
388
|
+
const workingRect = panes.working;
|
|
389
|
+
if (workingRect === null) {
|
|
390
|
+
feed.workingCamera = null;
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
if (headOn !== null) {
|
|
394
|
+
// A HEAD-ON FRAME TAKES THE WORKING PANE'S SLOT. Not a third pane: a
|
|
395
|
+
// scissored pane per mode is the trap SplitRenderer's header is about,
|
|
396
|
+
// and the two views an author wants while inside a card are "this card,
|
|
397
|
+
// flat" and "the shot". The split that already exists is that view.
|
|
398
|
+
isolationCamera.left = -headOn.halfWidth;
|
|
399
|
+
isolationCamera.right = headOn.halfWidth;
|
|
400
|
+
isolationCamera.top = headOn.halfHeight;
|
|
401
|
+
isolationCamera.bottom = -headOn.halfHeight;
|
|
402
|
+
isolationCamera.near = headOn.near;
|
|
403
|
+
isolationCamera.far = headOn.far;
|
|
404
|
+
isolationCamera.position.set(headOn.eye[0], headOn.eye[1], headOn.eye[2]);
|
|
405
|
+
isolationCamera.up.set(headOn.up[0], headOn.up[1], headOn.up[2]);
|
|
406
|
+
isolationCamera.lookAt(headOn.target[0], headOn.target[1], headOn.target[2]);
|
|
407
|
+
isolationCamera.updateProjectionMatrix();
|
|
408
|
+
isolationCamera.updateMatrixWorld(true);
|
|
409
|
+
feed.workingCamera = isolationCamera;
|
|
410
|
+
return isolationCamera;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Perspective and orthographic views share one pose. The orthographic eye
|
|
414
|
+
// stands back rather than using a negative near plane, so drawing, picking
|
|
415
|
+
// and the compile frustum all see the same positive slab.
|
|
416
|
+
const ortho = workingView.ortho === true;
|
|
417
|
+
const cam = ortho ? workingOrtho : workingCamera;
|
|
418
|
+
const { position } = workingPose(workingView);
|
|
419
|
+
const back = ortho ? orthoEyePushBack(workingView.distance) : 0;
|
|
420
|
+
const dir = ortho
|
|
421
|
+
? [
|
|
422
|
+
(position[0] - workingView.target[0]) / Math.max(workingView.distance, 1),
|
|
423
|
+
(position[1] - workingView.target[1]) / Math.max(workingView.distance, 1),
|
|
424
|
+
(position[2] - workingView.target[2]) / Math.max(workingView.distance, 1),
|
|
425
|
+
]
|
|
426
|
+
: [0, 0, 0];
|
|
427
|
+
cam.position.set(
|
|
428
|
+
position[0] + dir[0]! * back,
|
|
429
|
+
position[1] + dir[1]! * back,
|
|
430
|
+
position[2] + dir[2]! * back,
|
|
431
|
+
);
|
|
432
|
+
const upAt = viewBasis(workingView).up;
|
|
433
|
+
cam.up.set(upAt[0], upAt[1], upAt[2]);
|
|
434
|
+
cam.lookAt(workingView.target[0], workingView.target[1], workingView.target[2]);
|
|
435
|
+
const aspect = workingRect.height === 0 ? 1 : workingRect.width / workingRect.height;
|
|
436
|
+
if (ortho) {
|
|
437
|
+
const halfHeight = workingView.distance * Math.tan((WORKING_FOV_DEG * Math.PI) / 360);
|
|
438
|
+
workingOrtho.top = halfHeight;
|
|
439
|
+
workingOrtho.bottom = -halfHeight;
|
|
440
|
+
workingOrtho.right = halfHeight * aspect;
|
|
441
|
+
workingOrtho.left = -halfHeight * aspect;
|
|
442
|
+
workingOrtho.near = 0.1;
|
|
443
|
+
workingOrtho.far = back * 2 + workingView.distance;
|
|
444
|
+
} else {
|
|
445
|
+
workingCamera.fov = WORKING_FOV_DEG;
|
|
446
|
+
workingCamera.aspect = aspect;
|
|
447
|
+
// The Shot's own rule (`cameraClipPlanes`): a fixed 0.1 against an
|
|
448
|
+
// orbit radius of thousands of px z-fights a model's faces here too.
|
|
449
|
+
// Always the derived near, whatever the Shot holds: this view is the
|
|
450
|
+
// editor's own, never an exported picture, so no film's bytes ride on
|
|
451
|
+
// it, and the clip it costs a Shot of planes is 1/200 of the orbit
|
|
452
|
+
// radius in front of the eye.
|
|
453
|
+
const clip = cameraClipPlanes(workingView.distance, true);
|
|
454
|
+
workingCamera.near = clip.near;
|
|
455
|
+
workingCamera.far = clip.far;
|
|
456
|
+
}
|
|
457
|
+
cam.updateProjectionMatrix();
|
|
458
|
+
cam.updateMatrixWorld(true);
|
|
459
|
+
feed.workingCamera = cam;
|
|
460
|
+
return cam;
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
// The layout effect above poses the camera on every commit, and the frame
|
|
464
|
+
// subscriber below used to pose it AGAIN on the advance that same commit
|
|
465
|
+
// triggers: two `updateProjectionMatrix` + `updateMatrixWorld(true)` per
|
|
466
|
+
// frame for one answer. The flag says "this commit already posed it"; the
|
|
467
|
+
// subscriber consumes it, so a heartbeat advance with no commit in between
|
|
468
|
+
// still poses once, and a commit never poses twice.
|
|
469
|
+
const posedThisCommit = useRef(false);
|
|
470
|
+
useLayoutEffect(() => {
|
|
471
|
+
feed.prepareWorkingCamera = prepareWorkingCamera;
|
|
472
|
+
prepareWorkingCamera();
|
|
473
|
+
posedThisCommit.current = true;
|
|
474
|
+
return () => {
|
|
475
|
+
if (feed.prepareWorkingCamera !== prepareWorkingCamera) return;
|
|
476
|
+
feed.prepareWorkingCamera = null;
|
|
477
|
+
feed.workingCamera = null;
|
|
478
|
+
};
|
|
479
|
+
});
|
|
480
|
+
|
|
173
481
|
useFrame(() => {
|
|
174
|
-
|
|
482
|
+
// ── THE FURNITURE HOLDS STILL WHILE A PASS DRAWS (bridge v51) ─────────
|
|
483
|
+
//
|
|
484
|
+
// A preparing pass draws other frames under the frozen picture, and
|
|
485
|
+
// every projection below would follow them: the outlines, the hints, the
|
|
486
|
+
// motion paths, the closures the pointer lanes pick through. So the feed
|
|
487
|
+
// keeps the frozen frame's answers until the pass publishes the author's
|
|
488
|
+
// frame again; that draw projects everything afresh before the lift.
|
|
489
|
+
if (passDrawing()) return;
|
|
490
|
+
const frames = collectFramesCached(scene, layers, layerStampEpoch(), feed.sceneEpoch);
|
|
491
|
+
// Every projection below brings its own frame's matrix up (`fresh`), and
|
|
492
|
+
// nothing else does: a pass with no selection projects next to nothing.
|
|
493
|
+
FRESH.clear();
|
|
175
494
|
feed.frames = frames;
|
|
176
495
|
|
|
177
496
|
// ── ONE FRAME PER ID, THE FIRST ─────────────────────────────────────
|
|
@@ -194,8 +513,7 @@ export function StageTools({
|
|
|
194
513
|
// plumbing for a case that needs a cloner, the working pane, and a click on
|
|
195
514
|
// a clone that is not the first. Named here; do it when someone reports
|
|
196
515
|
// handles on the wrong clone.
|
|
197
|
-
|
|
198
|
-
for (const f of frames) if (!byId.has(f.id)) byId.set(f.id, f);
|
|
516
|
+
//
|
|
199
517
|
// ISOLATION SCOPES PICKING, and it does it HERE rather than through
|
|
200
518
|
// `SHOT_LAYER_PICKABLE`. The runtime re-stamps that userData on every
|
|
201
519
|
// commit from a dep-less layout effect, so anything the editor wrote onto
|
|
@@ -205,123 +523,12 @@ export function StageTools({
|
|
|
205
523
|
// A press that lands on nothing is what exits: with the surroundings
|
|
206
524
|
// unpickable, clicking outside the subtree already produces an empty hit
|
|
207
525
|
// list, so "click away to leave" needs no second gesture.
|
|
208
|
-
const pickable = frames
|
|
209
|
-
(f) => f.pickable !== false && (isolated === null || isolated.has(f.id)),
|
|
210
|
-
);
|
|
526
|
+
const { byId, pickable } = indexFrames(frames, isolated);
|
|
211
527
|
const cameraRect = panes.camera;
|
|
212
528
|
const workingRect = panes.working;
|
|
213
529
|
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
-
} else if (isolationView !== null) {
|
|
217
|
-
// ISOLATION TAKES THE WORKING PANE'S SLOT. Not a third pane: a scissored
|
|
218
|
-
// pane per mode is the trap SplitRenderer's header is about, and the two
|
|
219
|
-
// views an author wants while inside a card are "this card, flat" and
|
|
220
|
-
// "the shot" — which is the split that already exists.
|
|
221
|
-
//
|
|
222
|
-
// Orthographic and posed from the subtree's OWN basis, so it can roll and
|
|
223
|
-
// can look straight down — neither of which the orbit rig can do (its up
|
|
224
|
-
// is reset to world +y every frame and its polar angle is clamped off
|
|
225
|
-
// both poles).
|
|
226
|
-
const aspect = workingRect.height === 0 ? 1 : workingRect.width / workingRect.height;
|
|
227
|
-
const extent = isolationExtent(isolationView, aspect);
|
|
228
|
-
const { eye, up, far } = isolationEye(isolationView);
|
|
229
|
-
isolationCamera.left = -extent.halfWidth;
|
|
230
|
-
isolationCamera.right = extent.halfWidth;
|
|
231
|
-
isolationCamera.top = extent.halfHeight;
|
|
232
|
-
isolationCamera.bottom = -extent.halfHeight;
|
|
233
|
-
isolationCamera.near = 0.1;
|
|
234
|
-
isolationCamera.far = far;
|
|
235
|
-
isolationCamera.position.set(eye[0], eye[1], eye[2]);
|
|
236
|
-
isolationCamera.up.set(up[0], up[1], up[2]);
|
|
237
|
-
isolationCamera.lookAt(
|
|
238
|
-
isolationView.target[0],
|
|
239
|
-
isolationView.target[1],
|
|
240
|
-
isolationView.target[2],
|
|
241
|
-
);
|
|
242
|
-
isolationCamera.updateProjectionMatrix();
|
|
243
|
-
isolationCamera.updateMatrixWorld(true);
|
|
244
|
-
feed.workingCamera = isolationCamera;
|
|
245
|
-
} else {
|
|
246
|
-
// ── PERSPECTIVE OR ORTHOGRAPHIC, SAME POSE ────────────────────────
|
|
247
|
-
//
|
|
248
|
-
// Numpad-5. The two rigs are POSED identically — same eye, same target,
|
|
249
|
-
// same up — and differ only in the projection, so the toggle turns the
|
|
250
|
-
// vanishing point on and off without moving the camera an inch.
|
|
251
|
-
//
|
|
252
|
-
// The ortho half-height is `distance * tan(fov/2)`, which is exactly what
|
|
253
|
-
// the perspective lens frames AT THE TARGET. So `workingScreenScale`
|
|
254
|
-
// needs no branch: its `2 * distance * tan(fov/2)` is already both
|
|
255
|
-
// answers.
|
|
256
|
-
//
|
|
257
|
-
// The two agree at the target POINT and not everywhere: an ortho
|
|
258
|
-
// projection has no perspective divide, so a layer's corners — which sit
|
|
259
|
-
// at different depths once the view is orbited — are foreshortened by one
|
|
260
|
-
// rig and not the other. The error grows with distance from the target;
|
|
261
|
-
// on it, it is under one percent of the layer's own span
|
|
262
|
-
// (stageGeometry.test.ts pins that), which reads as the same picture and
|
|
263
|
-
// is what makes the toggle a toggle. Far from the target it is a real
|
|
264
|
-
// difference, and it is the whole reason to have an ortho view at all.
|
|
265
|
-
const ortho = workingView.ortho === true;
|
|
266
|
-
const cam = ortho ? workingOrtho : workingCamera;
|
|
267
|
-
const { position } = workingPose(workingView);
|
|
268
|
-
// ── THE ORTHO EYE STANDS BACK, IT DOES NOT REACH FORWARD ──────────
|
|
269
|
-
//
|
|
270
|
-
// An orthographic projection does not depend on where along the view axis
|
|
271
|
-
// the eye sits, so the two ways to cover the scene look identical: a
|
|
272
|
-
// negative near plane, or a positive near plane with the eye pushed back.
|
|
273
|
-
// They are NOT identical to the pointer. `Raycaster.setFromCamera` starts
|
|
274
|
-
// an ortho ray AT the near plane, and `rayHitsTriangle` rejects `t < 0` —
|
|
275
|
-
// so with a negative near plane every layer behind the eye plane drew
|
|
276
|
-
// perfectly and answered no click, no marquee, no gizmo and no frustum
|
|
277
|
-
// hit (`pickInRect`'s guard, `MoveGizmo`'s `behind` and `FrustumGizmo`'s
|
|
278
|
-
// all read view-space z the same way).
|
|
279
|
-
//
|
|
280
|
-
// Standing back is the construction `isolationEye` already uses, and it
|
|
281
|
-
// fixes all four consumers with no branch in any of them.
|
|
282
|
-
const back = ortho ? Math.max(workingView.distance * 8, 10_000) : 0;
|
|
283
|
-
const dir = ortho
|
|
284
|
-
? [
|
|
285
|
-
(position[0] - workingView.target[0]) / Math.max(workingView.distance, 1),
|
|
286
|
-
(position[1] - workingView.target[1]) / Math.max(workingView.distance, 1),
|
|
287
|
-
(position[2] - workingView.target[2]) / Math.max(workingView.distance, 1),
|
|
288
|
-
]
|
|
289
|
-
: [0, 0, 0];
|
|
290
|
-
cam.position.set(
|
|
291
|
-
position[0] + dir[0]! * back,
|
|
292
|
-
position[1] + dir[1]! * back,
|
|
293
|
-
position[2] + dir[2]! * back,
|
|
294
|
-
);
|
|
295
|
-
// Re-set every frame: `lookAt` reads `up`, and leaving it to whatever a
|
|
296
|
-
// previous frame or another consumer left there is how an orbit picks up
|
|
297
|
-
// a roll nobody authored.
|
|
298
|
-
cam.up.set(0, 1, 0);
|
|
299
|
-
cam.lookAt(workingView.target[0], workingView.target[1], workingView.target[2]);
|
|
300
|
-
const aspect = workingRect.height === 0 ? 1 : workingRect.width / workingRect.height;
|
|
301
|
-
if (ortho) {
|
|
302
|
-
const halfHeight = workingView.distance * Math.tan((WORKING_FOV_DEG * Math.PI) / 360);
|
|
303
|
-
workingOrtho.top = halfHeight;
|
|
304
|
-
workingOrtho.bottom = -halfHeight;
|
|
305
|
-
workingOrtho.right = halfHeight * aspect;
|
|
306
|
-
workingOrtho.left = -halfHeight * aspect;
|
|
307
|
-
// A POSITIVE NEAR PLANE, with the eye already pushed back by `back`
|
|
308
|
-
// above. The whole slab is in front of the camera, so every hit test in
|
|
309
|
-
// this lane agrees with what is drawn.
|
|
310
|
-
workingOrtho.near = 0.1;
|
|
311
|
-
workingOrtho.far = back * 2 + workingView.distance;
|
|
312
|
-
} else {
|
|
313
|
-
workingCamera.fov = WORKING_FOV_DEG;
|
|
314
|
-
workingCamera.aspect = aspect;
|
|
315
|
-
// The shot camera's own near/far, for the same reason it can afford
|
|
316
|
-
// them: every material here draws with `depthTest: false`, so depth
|
|
317
|
-
// precision buys nothing and a generous far plane costs nothing.
|
|
318
|
-
workingCamera.near = 0.1;
|
|
319
|
-
workingCamera.far = Math.max(workingView.distance * 8, 10_000);
|
|
320
|
-
}
|
|
321
|
-
cam.updateProjectionMatrix();
|
|
322
|
-
cam.updateMatrixWorld(true);
|
|
323
|
-
feed.workingCamera = cam;
|
|
324
|
-
}
|
|
530
|
+
if (!posedThisCommit.current) prepareWorkingCamera();
|
|
531
|
+
posedThisCommit.current = false;
|
|
325
532
|
|
|
326
533
|
/** The pane's rect and camera, or null when that pane is not on screen. */
|
|
327
534
|
const viewOf = (pane: StagePane): { rect: PaneRect; cam: Camera } | null => {
|
|
@@ -330,6 +537,34 @@ export function StageTools({
|
|
|
330
537
|
? null
|
|
331
538
|
: { rect: workingRect, cam: feed.workingCamera };
|
|
332
539
|
};
|
|
540
|
+
const sceneFrame = sceneFrameAt();
|
|
541
|
+
const cameraSplines = projectedSplines(
|
|
542
|
+
pickable,
|
|
543
|
+
layers,
|
|
544
|
+
sceneFrame,
|
|
545
|
+
fps,
|
|
546
|
+
cameraRect === null ? null : camera,
|
|
547
|
+
cameraRect?.width ?? 0,
|
|
548
|
+
cameraRect?.height ?? 0,
|
|
549
|
+
);
|
|
550
|
+
const workingSplines = projectedSplines(
|
|
551
|
+
pickable,
|
|
552
|
+
layers,
|
|
553
|
+
sceneFrame,
|
|
554
|
+
fps,
|
|
555
|
+
workingRect === null ? null : feed.workingCamera,
|
|
556
|
+
workingRect?.width ?? 0,
|
|
557
|
+
workingRect?.height ?? 0,
|
|
558
|
+
);
|
|
559
|
+
const splineIds = new Set([...cameraSplines, ...workingSplines].map((row) => row.frame.id));
|
|
560
|
+
const ordinaryPickable = splineIds.size === 0 ? pickable : pickable.filter((frame) => !splineIds.has(frame.id));
|
|
561
|
+
const splineMap = (rows: readonly ProjectedSplineFrame[]): Map<string, ProjectedSplineFrame> => {
|
|
562
|
+
const out = new Map<string, ProjectedSplineFrame>();
|
|
563
|
+
for (const row of rows) if (!out.has(row.frame.id)) out.set(row.frame.id, row);
|
|
564
|
+
return out;
|
|
565
|
+
};
|
|
566
|
+
const cameraSplineById = splineMap(cameraSplines);
|
|
567
|
+
const workingSplineById = splineMap(workingSplines);
|
|
333
568
|
|
|
334
569
|
feed.rayAt = (pane, x, y) => {
|
|
335
570
|
const view = viewOf(pane);
|
|
@@ -345,14 +580,52 @@ export function StageTools({
|
|
|
345
580
|
// difference identically zero.
|
|
346
581
|
return RAYCASTER.ray.clone();
|
|
347
582
|
};
|
|
583
|
+
feed.paneAt = (pane, design) => {
|
|
584
|
+
const view = viewOf(pane);
|
|
585
|
+
if (view === null) return null;
|
|
586
|
+
// Design y runs down and world y up: world = (x, -y, 0).
|
|
587
|
+
return toScreen(PANE_AT.set(design[0], -design[1], 0), view.cam, view.rect.width, view.rect.height);
|
|
588
|
+
};
|
|
589
|
+
feed.worldToPane = (pane, world) => {
|
|
590
|
+
const view = viewOf(pane);
|
|
591
|
+
return view === null ? null : toScreen(world, view.cam, view.rect.width, view.rect.height);
|
|
592
|
+
};
|
|
348
593
|
|
|
594
|
+
const isMask = (id: string): boolean => layers?.layersById[id]?.mask !== undefined;
|
|
349
595
|
feed.pick = (pane, x, y) => {
|
|
350
596
|
const ray = feed.rayAt?.(pane, x, y) ?? null;
|
|
351
597
|
if (ray === null) return [];
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
598
|
+
const splineRows = pane === "working" ? workingSplines : cameraSplines;
|
|
599
|
+
if (pane === "camera") {
|
|
600
|
+
const rows = pickLayers(ray, ordinaryPickable).map((id) => ({
|
|
601
|
+
id,
|
|
602
|
+
order: byId.get(id)?.order ?? 0,
|
|
603
|
+
}));
|
|
604
|
+
for (const row of splineRows) {
|
|
605
|
+
if (row.frame.locked !== true && splineScreenHit(row.screen, [x, y])) {
|
|
606
|
+
rows.push({ id: row.frame.id, order: row.frame.order });
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
rows.sort((a, b) => b.order - a.order);
|
|
610
|
+
return maskHitsLast(uniqueIds(rows), isMask);
|
|
611
|
+
}
|
|
612
|
+
// In the free working view, interleave spline centreline hits with plane
|
|
613
|
+
// hits by ray distance. A spline is one-dimensional, so its layer origin
|
|
614
|
+
// is the stable depth tie-breaker for the sampled screen hit.
|
|
615
|
+
const rows: Array<{ id: string; t: number; order: number }> = [];
|
|
616
|
+
for (const frame of ordinaryPickable) {
|
|
617
|
+
if (frame.locked === true) continue;
|
|
618
|
+
const t = rayHitsQuad(ray, worldQuad(frame));
|
|
619
|
+
if (t !== null) rows.push({ id: frame.id, t, order: frame.order });
|
|
620
|
+
}
|
|
621
|
+
for (const row of splineRows) {
|
|
622
|
+
if (row.frame.locked === true || !splineScreenHit(row.screen, [x, y])) continue;
|
|
623
|
+
const origin = new Vector3().setFromMatrixPosition(row.frame.matrixWorld);
|
|
624
|
+
const t = Math.max(0, origin.sub(ray.origin).dot(ray.direction));
|
|
625
|
+
rows.push({ id: row.frame.id, t, order: row.frame.order });
|
|
626
|
+
}
|
|
627
|
+
rows.sort((a, b) => a.t - b.t || b.order - a.order);
|
|
628
|
+
return maskHitsLast(uniqueIds(rows), isMask);
|
|
356
629
|
};
|
|
357
630
|
|
|
358
631
|
// The outline a click would select, projected in the pane it was asked
|
|
@@ -362,14 +635,40 @@ export function StageTools({
|
|
|
362
635
|
const view = viewOf(pane);
|
|
363
636
|
const hit = feed.pick?.(pane, x, y)[0];
|
|
364
637
|
if (view === null || layers === null || hit === undefined) return null;
|
|
365
|
-
const
|
|
366
|
-
|
|
638
|
+
const selectedId = outermost(layers, hit, isolationRoot);
|
|
639
|
+
const spline = (pane === "working" ? workingSplineById : cameraSplineById).get(selectedId);
|
|
640
|
+
if (spline !== undefined) return splineScreenBounds(spline.screen, 4);
|
|
641
|
+
const frame = byId.get(selectedId);
|
|
642
|
+
return frame === undefined ? null : screenQuad(fresh(frame), view.cam, view.rect.width, view.rect.height);
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
// One layer's outline BY ID — `hoverQuad` minus the pick. The chrome-hover
|
|
646
|
+
// sink is its caller (see `feed.quadForLayer`); pull-based, so it answers
|
|
647
|
+
// while the stage is idle.
|
|
648
|
+
const quadForLayer = (pane: StagePane, layerId: string): ScreenQuad | null => {
|
|
649
|
+
const view = viewOf(pane);
|
|
650
|
+
if (view === null) return null;
|
|
651
|
+
const spline = (pane === "working" ? workingSplineById : cameraSplineById).get(layerId);
|
|
652
|
+
if (spline !== undefined) return splineScreenBounds(spline.screen, 4);
|
|
653
|
+
const frame = byId.get(layerId);
|
|
654
|
+
return frame === undefined ? null : screenQuad(fresh(frame), view.cam, view.rect.width, view.rect.height);
|
|
367
655
|
};
|
|
656
|
+
feed.quadForLayer = quadForLayer;
|
|
368
657
|
|
|
369
658
|
feed.pickRect = (pane, rect) => {
|
|
370
659
|
const view = viewOf(pane);
|
|
371
660
|
if (view === null) return [];
|
|
372
|
-
|
|
661
|
+
const rows = pickInRect(ordinaryPickable, view.cam, view.rect.width, view.rect.height, rect).map((id) => ({
|
|
662
|
+
id,
|
|
663
|
+
order: byId.get(id)?.order ?? 0,
|
|
664
|
+
}));
|
|
665
|
+
for (const row of pane === "working" ? workingSplines : cameraSplines) {
|
|
666
|
+
if (row.frame.locked !== true && splineScreenIntersectsRect(row.screen, rect, 2)) {
|
|
667
|
+
rows.push({ id: row.frame.id, order: row.frame.order });
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
rows.sort((a, b) => a.order - b.order);
|
|
671
|
+
return uniqueIds(rows);
|
|
373
672
|
};
|
|
374
673
|
|
|
375
674
|
feed.grabAt = (pane, x, y, selection) => {
|
|
@@ -462,9 +761,10 @@ export function StageTools({
|
|
|
462
761
|
};
|
|
463
762
|
}
|
|
464
763
|
}
|
|
465
|
-
// Then the BODY of anything already selected.
|
|
466
|
-
// layer
|
|
467
|
-
//
|
|
764
|
+
// Then the BODY of anything already selected. A press on an unselected
|
|
765
|
+
// layer answers null here: StageApp's empty-space lane picks it on the
|
|
766
|
+
// press and asks again once the new selection lands, so one press-drag
|
|
767
|
+
// selects and moves.
|
|
468
768
|
const hits = feed.pick?.(pane, x, y) ?? [];
|
|
469
769
|
const hit = hits.find((id) => selection.includes(id));
|
|
470
770
|
const frame = hit === undefined ? undefined : byId.get(hit);
|
|
@@ -486,7 +786,9 @@ export function StageTools({
|
|
|
486
786
|
changed = true;
|
|
487
787
|
continue;
|
|
488
788
|
}
|
|
489
|
-
|
|
789
|
+
// Brought up here, so its quads, the union and TransformGizmo's centre
|
|
790
|
+
// (`feed.frames`, read later in this frame loop) see this pass's pose.
|
|
791
|
+
selected.push(fresh(frame));
|
|
490
792
|
if (feed.ids[i] !== id) changed = true;
|
|
491
793
|
// The lock is a document flag, so it changes only on a commit — but the
|
|
492
794
|
// overlay repaints off `feed.version`, not off React, so it has to be in
|
|
@@ -526,6 +828,165 @@ export function StageTools({
|
|
|
526
828
|
: projectQuad(world, feed.workingCamera, workingRect.width, workingRect.height),
|
|
527
829
|
};
|
|
528
830
|
if (unionMoved(feed.union, union)) changed = true;
|
|
831
|
+
const selectedIds = new Set(selection);
|
|
832
|
+
const splineGuidesWorking = workingSplines
|
|
833
|
+
.filter((row) => !row.renderGeometry || selectedIds.has(row.frame.id))
|
|
834
|
+
.map((row) => ({
|
|
835
|
+
id: row.frame.id,
|
|
836
|
+
pts: row.screen.line.flatMap((point) => [point[0], point[1]]),
|
|
837
|
+
selected: selectedIds.has(row.frame.id),
|
|
838
|
+
}));
|
|
839
|
+
if (guidesMoved(feed.splineGuidesWorking, splineGuidesWorking)) changed = true;
|
|
840
|
+
// ── THE CHROME'S HOVER HINT RIDES THE SAME PASS ───────────────────────
|
|
841
|
+
// The sink seeds these at write time (an idle stage runs no frame); this
|
|
842
|
+
// keeps them true while the camera moves. Same half-pixel gate as the
|
|
843
|
+
// selection quads, so a stationary hint costs nothing.
|
|
844
|
+
const hintCamera = feed.hintId === null ? null : quadForLayer("camera", feed.hintId);
|
|
845
|
+
const hintWorking = feed.hintId === null ? null : quadForLayer("working", feed.hintId);
|
|
846
|
+
if (hintQuadMoved(feed.hintCamera, hintCamera) || hintQuadMoved(feed.hintWorking, hintWorking)) {
|
|
847
|
+
changed = true;
|
|
848
|
+
}
|
|
849
|
+
// ── AND SO DOES A BROKEN PIECE'S PLACEHOLDER ──────────────────────────
|
|
850
|
+
// A layer the runtime isolated keeps its group (its transform and stamps
|
|
851
|
+
// are still written), so its outline is the hole where it would draw.
|
|
852
|
+
const failedCamera: FailedQuad[] = [];
|
|
853
|
+
const failedWorking: FailedQuad[] = [];
|
|
854
|
+
for (const id of feed.failedIds) {
|
|
855
|
+
const label = feed.failedLabels.get(id) ?? "Component";
|
|
856
|
+
const inCamera = quadForLayer("camera", id);
|
|
857
|
+
if (inCamera !== null) failedCamera.push({ quad: inCamera, label });
|
|
858
|
+
const inWorking = quadForLayer("working", id);
|
|
859
|
+
if (inWorking !== null) failedWorking.push({ quad: inWorking, label });
|
|
860
|
+
}
|
|
861
|
+
if (failedMoved(feed.failedCamera, failedCamera) || failedMoved(feed.failedWorking, failedWorking)) {
|
|
862
|
+
changed = true;
|
|
863
|
+
}
|
|
864
|
+
// ── AND SO DOES A SELECTED GROUP'S MASK (gap 7) ───────────────────────
|
|
865
|
+
// The matte that cuts the selection, dashed. Not for a mask that is itself
|
|
866
|
+
// selected: its own outline already says where it is.
|
|
867
|
+
const maskCamera: MaskQuad[] = [];
|
|
868
|
+
const maskWorking: MaskQuad[] = [];
|
|
869
|
+
if (layers !== null) {
|
|
870
|
+
const masks = new Set(selection.map((id) => maskTargetOf(layers, id)));
|
|
871
|
+
for (const id of masks) {
|
|
872
|
+
if (id === null || selectedIds.has(id)) continue;
|
|
873
|
+
const round = layers.layersById[id]?.kind === "ellipse";
|
|
874
|
+
const inCamera = quadForLayer("camera", id);
|
|
875
|
+
if (inCamera !== null) maskCamera.push({ quad: inCamera, round });
|
|
876
|
+
const inWorking = quadForLayer("working", id);
|
|
877
|
+
if (inWorking !== null) maskWorking.push({ quad: inWorking, round });
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
if (masksMoved(feed.maskCamera, maskCamera) || masksMoved(feed.maskWorking, maskWorking)) changed = true;
|
|
881
|
+
// ── AND SO DO OTHER PEOPLE'S SELECTIONS (bridge v43) ──────────────────
|
|
882
|
+
const peersCamera = projectPeers(feed.peerSel, quadForLayer, "camera", ids);
|
|
883
|
+
const peersWorking = projectPeers(feed.peerSel, quadForLayer, "working", ids);
|
|
884
|
+
if (peersMoved(feed.peersCamera, peersCamera) || peersMoved(feed.peersWorking, peersWorking)) changed = true;
|
|
885
|
+
// ── AND SO DOES THE OPEN STUDIO PIN'S LAYER RING (bridge v45) ─────────
|
|
886
|
+
const pinLayerCamera = feed.pinLayerId === null ? null : quadForLayer("camera", feed.pinLayerId);
|
|
887
|
+
if (pinLayerCamera === null ? feed.pinLayerCamera !== null : quadMoved(feed.pinLayerCamera ?? undefined, pinLayerCamera)) {
|
|
888
|
+
changed = true;
|
|
889
|
+
}
|
|
890
|
+
// ── AND SO DOES THE PEN PREVIEW, ON ITS PANE'S CAMERA ALONE ───────────
|
|
891
|
+
// It projects at render time, so a camera that moves under a still cursor
|
|
892
|
+
// (a scrubbed camera move, a wheel zoom) must repaint it. Read only while
|
|
893
|
+
// the pen holds points: otherwise a moving camera costs no repaint. The
|
|
894
|
+
// pen draws on the frame: the camera pane, or with no camera pane on
|
|
895
|
+
// screen a definition's one pane, whose camera is the working one.
|
|
896
|
+
const penCamera = cameraRect !== null ? camera : feed.workingCamera;
|
|
897
|
+
const cameraPose =
|
|
898
|
+
feed.penLive && penCamera !== null
|
|
899
|
+
? [...penCamera.projectionMatrix.elements, ...penCamera.matrixWorldInverse.elements]
|
|
900
|
+
: null;
|
|
901
|
+
if (cameraPose !== null && cameraPose.some((v, i) => v !== feed.cameraPose?.[i])) changed = true;
|
|
902
|
+
// ── AND SO DO THE MOTION PATHS (gap 3) ─────────────────────────────────
|
|
903
|
+
// Projected from the STORED layer, so a key edit redraws on its commit
|
|
904
|
+
// and a scrub between keys moves only the tick and the current dot.
|
|
905
|
+
const motionCamera: ScreenMotionPath[] = [];
|
|
906
|
+
const motionWorking: ScreenMotionPath[] = [];
|
|
907
|
+
const motionParents = new Map<string, { parent: Matrix4; path: MotionPath }>();
|
|
908
|
+
if (motionPaths && layers !== null && fps !== null) {
|
|
909
|
+
for (const id of selection) {
|
|
910
|
+
const stored = layers.layersById[id];
|
|
911
|
+
// A splineFollow layer's position is the spline's: it has no path of its own.
|
|
912
|
+
if (stored === undefined || stored.splineFollow !== undefined) continue;
|
|
913
|
+
if (isolated !== null && !isolated.has(id)) continue;
|
|
914
|
+
const path = motionPathOf(stored, fps);
|
|
915
|
+
if (path === null) continue;
|
|
916
|
+
const parent = parentWorldOf(id, byId, scene, layers);
|
|
917
|
+
motionParents.set(id, { parent, path });
|
|
918
|
+
const dim = !layerAliveAt(stored, sceneFrame);
|
|
919
|
+
const at = (f: number) => resolveLayerAt(stored, f, fps).position;
|
|
920
|
+
const sample =
|
|
921
|
+
sceneFrame < path.from || sceneFrame > path.to
|
|
922
|
+
? null
|
|
923
|
+
: {
|
|
924
|
+
now: at(sceneFrame),
|
|
925
|
+
before: at(Math.max(path.from, sceneFrame - 0.5)),
|
|
926
|
+
after: at(Math.min(path.to, sceneFrame + 0.5)),
|
|
927
|
+
};
|
|
928
|
+
if (cameraRect !== null) {
|
|
929
|
+
motionCamera.push(
|
|
930
|
+
projectMotionPath(id, path, sceneFrame, dim, sample, parent, camera, cameraRect.width, cameraRect.height),
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
if (workingRect !== null && feed.workingCamera !== null) {
|
|
934
|
+
motionWorking.push(
|
|
935
|
+
projectMotionPath(
|
|
936
|
+
id,
|
|
937
|
+
path,
|
|
938
|
+
sceneFrame,
|
|
939
|
+
dim,
|
|
940
|
+
sample,
|
|
941
|
+
parent,
|
|
942
|
+
feed.workingCamera,
|
|
943
|
+
workingRect.width,
|
|
944
|
+
workingRect.height,
|
|
945
|
+
),
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
if (motionPathsMoved(feed.motionPathsCamera, motionCamera) || motionPathsMoved(feed.motionPathsWorking, motionWorking)) {
|
|
951
|
+
changed = true;
|
|
952
|
+
}
|
|
953
|
+
// The selected dot goes with its key: a deleted key, a deselected layer
|
|
954
|
+
// or the pref off leaves no handles to show.
|
|
955
|
+
const picked = feed.selectedPathKey;
|
|
956
|
+
if (picked !== null && !motionParents.get(picked.layerId)?.path.keys.some((k) => k.id === picked.keyId)) {
|
|
957
|
+
feed.selectedPathKey = null;
|
|
958
|
+
changed = true;
|
|
959
|
+
}
|
|
960
|
+
feed.pathKeyHit = (pane, x, y) =>
|
|
961
|
+
motionPathKeyAt(pane === "camera" ? feed.motionPathsCamera : feed.motionPathsWorking, x, y, PATH_KEY_GRAB_PX);
|
|
962
|
+
feed.pathHandleHit = (pane, x, y) =>
|
|
963
|
+
motionPathHandleAt(
|
|
964
|
+
pane === "camera" ? feed.motionPathsCamera : feed.motionPathsWorking,
|
|
965
|
+
feed.selectedPathKey,
|
|
966
|
+
x,
|
|
967
|
+
y,
|
|
968
|
+
PATH_KEY_GRAB_PX,
|
|
969
|
+
);
|
|
970
|
+
feed.pathKeyAnchor = (layerId, keyId) => {
|
|
971
|
+
const row = motionParents.get(layerId);
|
|
972
|
+
const index = row?.path.keys.findIndex((k) => k.id === keyId) ?? -1;
|
|
973
|
+
const key = row?.path.keys[index];
|
|
974
|
+
if (row === undefined || key === undefined) return null;
|
|
975
|
+
const parent = row.parent.clone();
|
|
976
|
+
const world = new Vector3(key.point[0], -key.point[1], -key.point[2]).applyMatrix4(parent);
|
|
977
|
+
const neighbours = [row.path.keys[index - 1], row.path.keys[index + 1]].flatMap((k) =>
|
|
978
|
+
k === undefined ? [] : [k.point],
|
|
979
|
+
);
|
|
980
|
+
return {
|
|
981
|
+
world,
|
|
982
|
+
parent,
|
|
983
|
+
frame: key.frame,
|
|
984
|
+
point: key.point,
|
|
985
|
+
neighbours,
|
|
986
|
+
...(key.spatialIn === undefined ? {} : { spatialIn: key.spatialIn }),
|
|
987
|
+
...(key.spatialOut === undefined ? {} : { spatialOut: key.spatialOut }),
|
|
988
|
+
};
|
|
989
|
+
};
|
|
529
990
|
// A pane appearing or disappearing changes an array's LENGTH without moving
|
|
530
991
|
// any corner, so the emptiness check is what makes a mode switch repaint —
|
|
531
992
|
// otherwise the overlay keeps drawing the pane that just went away.
|
|
@@ -541,6 +1002,19 @@ export function StageTools({
|
|
|
541
1002
|
feed.union = union;
|
|
542
1003
|
feed.quadsCamera = quadsCamera;
|
|
543
1004
|
feed.quadsWorking = quadsWorking;
|
|
1005
|
+
feed.splineGuidesWorking = splineGuidesWorking;
|
|
1006
|
+
feed.hintCamera = hintCamera;
|
|
1007
|
+
feed.hintWorking = hintWorking;
|
|
1008
|
+
feed.failedCamera = failedCamera;
|
|
1009
|
+
feed.failedWorking = failedWorking;
|
|
1010
|
+
feed.maskCamera = maskCamera;
|
|
1011
|
+
feed.maskWorking = maskWorking;
|
|
1012
|
+
feed.peersCamera = peersCamera;
|
|
1013
|
+
feed.peersWorking = peersWorking;
|
|
1014
|
+
feed.pinLayerCamera = pinLayerCamera;
|
|
1015
|
+
feed.cameraPose = cameraPose;
|
|
1016
|
+
feed.motionPathsCamera = motionCamera;
|
|
1017
|
+
feed.motionPathsWorking = motionWorking;
|
|
544
1018
|
feed.version += 1;
|
|
545
1019
|
// The overlay leaf's own subscription (see feed.onOverlay) — never a
|
|
546
1020
|
// StageApp state setter, which re-committed the whole stage per moved
|
|
@@ -551,6 +1025,31 @@ export function StageTools({
|
|
|
551
1025
|
return null;
|
|
552
1026
|
}
|
|
553
1027
|
|
|
1028
|
+
/** `quadMoved` with null on either side: appearing or vanishing IS a move. */
|
|
1029
|
+
function hintQuadMoved(prev: ScreenQuad | null, next: ScreenQuad | null): boolean {
|
|
1030
|
+
if (prev === null || next === null) return prev !== next;
|
|
1031
|
+
return quadMoved(prev, next);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/** A list of quads moved: one appeared, one went, or one moved. */
|
|
1035
|
+
function peersMoved(prev: readonly PeerQuad[], next: readonly PeerQuad[]): boolean {
|
|
1036
|
+
if (prev.length !== next.length) return true;
|
|
1037
|
+
return next.some((row, index) => {
|
|
1038
|
+
const was = prev[index];
|
|
1039
|
+
return was?.name !== row.name || was.color !== row.color || was.tag !== row.tag || was.ring !== row.ring || quadMoved(was.quad, row.quad);
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function masksMoved(prev: readonly MaskQuad[], next: readonly MaskQuad[]): boolean {
|
|
1044
|
+
if (prev.length !== next.length) return true;
|
|
1045
|
+
return next.some((row, index) => prev[index]?.round !== row.round || quadMoved(prev[index]?.quad, row.quad));
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function failedMoved(prev: readonly FailedQuad[], next: readonly FailedQuad[]): boolean {
|
|
1049
|
+
if (prev.length !== next.length) return true;
|
|
1050
|
+
return next.some((row, index) => prev[index]?.label !== row.label || quadMoved(prev[index]?.quad, row.quad));
|
|
1051
|
+
}
|
|
1052
|
+
|
|
554
1053
|
|
|
555
1054
|
/**
|
|
556
1055
|
* Every layer group in the scene, as the geometry the editor reasons about.
|
|
@@ -562,16 +1061,17 @@ export function StageTools({
|
|
|
562
1061
|
*/
|
|
563
1062
|
export function collectFrames(root: Object3D): LayerFrame[] {
|
|
564
1063
|
const out: LayerFrame[] = [];
|
|
565
|
-
root.
|
|
1064
|
+
root.traverseVisible((o) => {
|
|
566
1065
|
const id: unknown = o.userData[SHOT_LAYER_KEY];
|
|
567
1066
|
const box: unknown = o.userData[SHOT_LAYER_BOX];
|
|
568
1067
|
if (typeof id !== "string" || !Array.isArray(box)) return;
|
|
569
1068
|
out.push({
|
|
570
1069
|
id,
|
|
571
1070
|
size: [Number(box[0]), Number(box[1])],
|
|
572
|
-
// Live reference, not a clone
|
|
573
|
-
//
|
|
1071
|
+
// Live reference, not a clone. It is only as fresh as the last
|
|
1072
|
+
// `updateWorldMatrix`, which the overlay pass runs on `object` first.
|
|
574
1073
|
matrixWorld: o.matrixWorld,
|
|
1074
|
+
object: o,
|
|
575
1075
|
order: typeof o.userData[SHOT_LAYER_ORDER] === "number" ? (o.userData[SHOT_LAYER_ORDER] as number) : 0,
|
|
576
1076
|
locked: o.userData[SHOT_LAYER_LOCKED] === true,
|
|
577
1077
|
pickable: o.userData[SHOT_LAYER_PICKABLE] !== false,
|