@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
|
@@ -40,9 +40,15 @@ import type { PathPoint } from "@dailies-studio/shot/doc";
|
|
|
40
40
|
import type { NavBall } from "../gizmoMath.js";
|
|
41
41
|
import type { ModalAxis, ModalMode } from "../modalState.js";
|
|
42
42
|
import { HANDLE_SITES, handlePoint } from "../handles.js";
|
|
43
|
-
import type { StageInset } from "../../bridgeTypes.js";
|
|
43
|
+
import type { ShotBoardPin, ShotSketchGuide, StageInset } from "../../bridgeTypes.js";
|
|
44
|
+
import { clearOf } from "../panes.js";
|
|
44
45
|
import type { PaneRect, StagePane } from "../panes.js";
|
|
46
|
+
import type { FailedQuad, MaskQuad, PathHandleRef, PathKeyRef, PeerQuad } from "../feed.js";
|
|
47
|
+
import { PATH_HANDLE_MIN_PX } from "../motionPathStage.js";
|
|
48
|
+
import type { ScreenMotionPath, ScreenPathKey } from "../motionPathStage.js";
|
|
45
49
|
import type { ScreenQuad } from "../project.js";
|
|
50
|
+
import type { ScreenSpline } from "../splineStage.js";
|
|
51
|
+
import type { Viewport } from "../view.js";
|
|
46
52
|
|
|
47
53
|
/** A smart guide, already converted to CSS px by the caller — the overlay does
|
|
48
54
|
* no coordinate maths of its own, so there is one conversion in the lane. */
|
|
@@ -63,6 +69,20 @@ export interface ScreenMarquee {
|
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
export interface OverlayProps {
|
|
72
|
+
/**
|
|
73
|
+
* THE AUTHOR IS WATCHING: draw the frame, and nothing else in it.
|
|
74
|
+
*
|
|
75
|
+
* Most of the furniture is already gone by the time it reaches here: the
|
|
76
|
+
* selection quads because the stage empties the selection it projects, the
|
|
77
|
+
* marquee and the pen because those lanes never run, the modal badge because
|
|
78
|
+
* the draft is abandoned, the isolation badge because the chrome leaves
|
|
79
|
+
* isolation on the way in. This prop is for the two that ARE still live:
|
|
80
|
+
* the chrome's row-hover outline (`hintCamera`, pushed by a panel that is
|
|
81
|
+
* still on screen and still hovering) and the snap guides. It is also the
|
|
82
|
+
* honest way to say the rule once, instead of leaving it as an emergent
|
|
83
|
+
* property of six other things being empty.
|
|
84
|
+
*/
|
|
85
|
+
watching: boolean;
|
|
66
86
|
width: number;
|
|
67
87
|
height: number;
|
|
68
88
|
panes: { working: PaneRect | null; camera: PaneRect | null };
|
|
@@ -95,22 +115,79 @@ export interface OverlayProps {
|
|
|
95
115
|
* which in a group tree means undoing a selection they did not want.
|
|
96
116
|
*/
|
|
97
117
|
hover: { pane: StagePane; quad: ScreenQuad } | null;
|
|
98
|
-
/**
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
|
|
104
|
-
|
|
118
|
+
/**
|
|
119
|
+
* The CHROME's hover hint — the layer a timeline row under the cursor names,
|
|
120
|
+
* projected per pane by the stage (bridge `hoverLayer`, v22). Distinct from
|
|
121
|
+
* `hover` (the stage's own pointer preview) and drawn identically: pointing
|
|
122
|
+
* at either representation of a layer lights the other.
|
|
123
|
+
*/
|
|
124
|
+
hintCamera: ScreenQuad | null;
|
|
125
|
+
hintWorking: ScreenQuad | null;
|
|
126
|
+
/**
|
|
127
|
+
* A BROKEN PIECE'S PLACEHOLDER: the outlines of the layers the runtime
|
|
128
|
+
* isolated because they threw, per pane, and one line naming what failed
|
|
129
|
+
* (every isolated piece, layer or not), or null when nothing did. Drawn in
|
|
130
|
+
* the danger colour, dashed, over a faint wash, so a hole in the picture
|
|
131
|
+
* reads as a failure and never as a layer meant to be empty. Editor paint
|
|
132
|
+
* only: the export draws none of it, and refuses the frame instead.
|
|
133
|
+
*/
|
|
134
|
+
failedCamera: readonly FailedQuad[];
|
|
135
|
+
failedWorking: readonly FailedQuad[];
|
|
136
|
+
/** A selected Mask group's mask, dashed (gap 7). See `feed.maskCamera`. */
|
|
137
|
+
maskCamera: readonly MaskQuad[];
|
|
138
|
+
maskWorking: readonly MaskQuad[];
|
|
139
|
+
/**
|
|
140
|
+
* OTHER PEOPLE'S SELECTIONS (bridge v43, Paper page 21): an outline in each
|
|
141
|
+
* person's colour, their name tagged above one layer. Under our own
|
|
142
|
+
* selection, and ringing it when we both hold the same layer.
|
|
143
|
+
*/
|
|
144
|
+
peersCamera: readonly PeerQuad[];
|
|
145
|
+
peersWorking: readonly PeerQuad[];
|
|
146
|
+
failure: string | null;
|
|
147
|
+
/**
|
|
148
|
+
* THE STUDIO'S COMMENT PINS (bridge v45) on the frame at the playhead, with
|
|
149
|
+
* what a pin click calls, or null. Furniture, after everything the author
|
|
150
|
+
* is editing through.
|
|
151
|
+
*/
|
|
152
|
+
comments: CommentOverlay | null;
|
|
153
|
+
/**
|
|
154
|
+
* THE SKETCH GUIDE (bridge v47) in camera-pane CSS px, or null: one
|
|
155
|
+
* storyboard sketch as dashed outlines with label chips, laid into the
|
|
156
|
+
* exportable frame by `sketchGuideScreen`, so it moves with the frame.
|
|
157
|
+
* Furniture, under the selection.
|
|
158
|
+
*/
|
|
159
|
+
sketchGuide: ScreenSketchGuide | null;
|
|
160
|
+
/** The open Studio pin's layer in the camera pane (S5), ringed dashed.
|
|
161
|
+
* Drawn only while `comments` is up. */
|
|
162
|
+
pinLayer?: ScreenQuad | null;
|
|
163
|
+
/** The empty Shot's one line of help ("Press R for a rectangle..."), centred
|
|
164
|
+
* in the frame, or null once the Shot has a layer or a gesture is live.
|
|
165
|
+
* Furniture, so it never reaches an export or a still. */
|
|
166
|
+
emptyHint: string | null;
|
|
167
|
+
/**
|
|
168
|
+
* PILOTING the shot camera from the Camera pane, or null.
|
|
169
|
+
*
|
|
170
|
+
* The badge is drawn INSIDE the pane whose navigation is writing the
|
|
171
|
+
* document, with its EJECT on it: a mode the author armed has to be
|
|
172
|
+
* leavable from the picture it is changing (the isolation badge's rule).
|
|
173
|
+
* `text` is composed by the chrome, which knows what holds the camera.
|
|
174
|
+
*/
|
|
175
|
+
pilot: { text: string; onEject: () => void } | null;
|
|
176
|
+
/**
|
|
177
|
+
* The component driving the render camera, by name, or null while the
|
|
178
|
+
* document's rig does. Said INSIDE the 3D pane, where the camera object
|
|
179
|
+
* follows a camera nothing in the document describes: without it, the
|
|
180
|
+
* gizmo's grips going dead and the inspector's fields moving nothing read
|
|
181
|
+
* as two unrelated breakages.
|
|
182
|
+
*/
|
|
183
|
+
driver: string | null;
|
|
184
|
+
/** A subtree is isolated (and still in the scene). Isolation REPLACES the
|
|
185
|
+
* working pane's camera with a derived head-on orthographic rig
|
|
186
|
+
* (isolation.ts), so the pane wears a ring that says it is in a mode. The
|
|
187
|
+
* NAME of the subtree and the way out are the chrome's breadcrumb. */
|
|
188
|
+
isolating: boolean;
|
|
105
189
|
marquee: ScreenMarquee | null;
|
|
106
190
|
guides: readonly ScreenGuide[];
|
|
107
|
-
/** The pen's path so far, in CAMERA-PANE CSS px, or null when no path is
|
|
108
|
-
* being drawn. Converted by the caller, like the guides — the overlay does
|
|
109
|
-
* no coordinate maths of its own.
|
|
110
|
-
*
|
|
111
|
-
* `cursor` is where the rubber band ends; `closeHot` is true when a press
|
|
112
|
-
* there would CLOSE the loop, which the preview says by filling the first
|
|
113
|
-
* anchor rather than by moving anything. */
|
|
114
191
|
/**
|
|
115
192
|
* The same guides, PROJECTED, for the working pane.
|
|
116
193
|
*
|
|
@@ -125,11 +202,33 @@ export interface OverlayProps {
|
|
|
125
202
|
* this lane keeps removing.
|
|
126
203
|
*/
|
|
127
204
|
guidesWorking: readonly { a: readonly [number, number]; b: readonly [number, number] }[];
|
|
205
|
+
splineGuides: readonly { id: string; pts: readonly number[]; selected: boolean }[];
|
|
206
|
+
/** The selected layers' motion paths per pane (gap 3), and the key dot the
|
|
207
|
+
* pointer is over or dragging. */
|
|
208
|
+
motionPathsCamera: readonly ScreenMotionPath[];
|
|
209
|
+
motionPathsWorking: readonly ScreenMotionPath[];
|
|
210
|
+
pathKeyHover: PathKeyRef | null;
|
|
211
|
+
/** The dot whose Bezier handles show (schema v40), and the handle the
|
|
212
|
+
* pointer is over or dragging. Both null in a read-only viewer. */
|
|
213
|
+
pathKeySelected: PathKeyRef | null;
|
|
214
|
+
pathHandleHover: PathHandleRef | null;
|
|
215
|
+
/** The pen's path so far, in the CSS px of its pane (`ScreenPen.pane`), or
|
|
216
|
+
* null when no path is being drawn. Converted by the caller, like the
|
|
217
|
+
* guides: the overlay does no coordinate maths of its own.
|
|
218
|
+
*
|
|
219
|
+
* `cursor` is where the rubber band ends; `closeHot` is true when a press
|
|
220
|
+
* there would CLOSE the loop, which the preview says by filling the first
|
|
221
|
+
* anchor rather than by moving anything. */
|
|
128
222
|
pen: ScreenPen | null;
|
|
223
|
+
/** A 3D spline draft or point-edit overlay, in working-pane CSS px. */
|
|
224
|
+
spline: ScreenSpline | null;
|
|
129
225
|
/** A live modal transform's pill, or null. In-pane for the FollowBadge's
|
|
130
226
|
* reason: a mode the author is inside belongs over the picture it is
|
|
131
227
|
* changing, not in a panel they are not looking at. */
|
|
132
228
|
modal: ScreenModal | null;
|
|
229
|
+
/** The constraint axis through the pivot, while a gizmo arm is held. Working
|
|
230
|
+
* pane only, because that is the only pane the gizmo is drawn in. */
|
|
231
|
+
axisGuide: ScreenAxisGuide | null;
|
|
133
232
|
/** The working pane's axis indicator: six projected axis ends, and what a
|
|
134
233
|
* click on one should do. Null when there is no working pane. */
|
|
135
234
|
nav: ScreenNav | null;
|
|
@@ -138,17 +237,26 @@ export interface OverlayProps {
|
|
|
138
237
|
* the camera pane has no way to show one. */
|
|
139
238
|
keyLight: ScreenKeyLight | null;
|
|
140
239
|
/**
|
|
141
|
-
* How far the chrome's
|
|
240
|
+
* How far the chrome's docked panels reach into the stage, per side.
|
|
142
241
|
*
|
|
143
242
|
* EVERY piece of overlay furniture lays out inside it — the mode bars, the
|
|
144
|
-
* modal pill, the nav gizmo — because the panes
|
|
145
|
-
* chrome
|
|
146
|
-
* the
|
|
147
|
-
*
|
|
148
|
-
*
|
|
243
|
+
* modal pill, the nav gizmo — because the panes TILE the whole window and the
|
|
244
|
+
* chrome's opaque cells sit on top of them. In a split they tile it between
|
|
245
|
+
* the two of them, which is why no piece of furniture may read a raw edge
|
|
246
|
+
* (below). Before this, two mode bars drew entirely behind the opaque shell
|
|
247
|
+
* header and the modal pill drew behind the left rail, so the only thing
|
|
248
|
+
* that said a G / R / S transform was live had never been visible in the
|
|
249
|
+
* default layout.
|
|
250
|
+
*
|
|
251
|
+
* NO FURNITURE SUBTRACTS THESE EDGES ITSELF. The inset describes the WINDOW's
|
|
252
|
+
* clear band, not each pane's, so it is only ever read through `clearOf`
|
|
253
|
+
* below — see its header for the two pieces that hand-rolled the subtraction
|
|
254
|
+
* and both landed a whole panel's width away from where they meant to be.
|
|
149
255
|
*
|
|
150
|
-
* The pane RECTS
|
|
151
|
-
*
|
|
256
|
+
* The pane RECTS still partition the whole viewport: the scissor draws
|
|
257
|
+
* full-bleed under the docked chrome, so nothing here shrinks the rendered
|
|
258
|
+
* picture or moves the hit test. (`paneRects` does take the inset now, but
|
|
259
|
+
* for one thing only — where the split SEAM falls.)
|
|
152
260
|
*/
|
|
153
261
|
inset: StageInset;
|
|
154
262
|
}
|
|
@@ -214,6 +322,17 @@ export interface ScreenModal {
|
|
|
214
322
|
* "S then X" says which direction is about to move without a second look at
|
|
215
323
|
* the label. */
|
|
216
324
|
axis: ModalAxis;
|
|
325
|
+
/**
|
|
326
|
+
* Which pane the pill belongs in.
|
|
327
|
+
*
|
|
328
|
+
* A MODAL is working-pane-only and always says `"working"`, but this badge is
|
|
329
|
+
* now also every POINTER drag's readout (StageApp's `DRAG_BADGES`), and a
|
|
330
|
+
* body drag or a resize happens in whichever pane the author pressed. Pinning
|
|
331
|
+
* it to the working pane would have put the numbers for a camera-pane resize
|
|
332
|
+
* in a pane the author is not looking at — or, in the camera-only layout,
|
|
333
|
+
* nowhere at all.
|
|
334
|
+
*/
|
|
335
|
+
pane: StagePane;
|
|
217
336
|
}
|
|
218
337
|
|
|
219
338
|
/**
|
|
@@ -230,14 +349,82 @@ export interface ScreenModal {
|
|
|
230
349
|
*/
|
|
231
350
|
export const modalReadoutSink: { write: ((text: string) => void) | null } = { write: null };
|
|
232
351
|
|
|
352
|
+
/**
|
|
353
|
+
* The constraint axis, drawn through the pivot while a gesture is locked to it.
|
|
354
|
+
*
|
|
355
|
+
* ── WHY BLENDER DRAWS THIS AND WHY IT MATTERS HERE ──────────────────────
|
|
356
|
+
*
|
|
357
|
+
* A locked transform's only cue was the tinted glyph in the pill, at the far
|
|
358
|
+
* corner of the pane. The line puts the constraint AT THE POINT OF ACTION: it
|
|
359
|
+
* says which way the motion runs, where it runs off screen, and — once the
|
|
360
|
+
* layer has moved — how far off the axis the cursor has wandered while the
|
|
361
|
+
* layer stayed on it (which is exactly what an axis lock is for).
|
|
362
|
+
*
|
|
363
|
+
* A screen segment and not a world one: a perspective projection maps the 3D
|
|
364
|
+
* axis to a 2D line, so this IS the axis, without near-plane clipping. The
|
|
365
|
+
* pane's own clip path trims it.
|
|
366
|
+
*/
|
|
367
|
+
export interface ScreenAxisGuide {
|
|
368
|
+
a: readonly [number, number];
|
|
369
|
+
b: readonly [number, number];
|
|
370
|
+
axis: "x" | "y" | "z";
|
|
371
|
+
/** The pane it is drawn in. The gizmo's guide is the working pane's; a
|
|
372
|
+
* motion path key drag (gap 3) draws in whichever pane it started in. */
|
|
373
|
+
pane?: StagePane;
|
|
374
|
+
}
|
|
375
|
+
|
|
233
376
|
export interface ScreenPen {
|
|
377
|
+
/** The pane the pen draws on, the frame pane: the camera pane over a Shot,
|
|
378
|
+
* a definition's one pane inside it. `ScreenModal.pane`'s rule. */
|
|
379
|
+
pane: StagePane;
|
|
234
380
|
points: readonly PathPoint[];
|
|
235
381
|
closed: boolean;
|
|
236
382
|
cursor: readonly [number, number] | null;
|
|
237
383
|
closeHot: boolean;
|
|
238
384
|
}
|
|
239
385
|
|
|
386
|
+
/** The sketch guide laid into the camera pane: CSS px from its top-left. */
|
|
387
|
+
export interface ScreenSketchGuide {
|
|
388
|
+
rects: readonly { x: number; y: number; width: number; height: number; radius: number; label?: string }[];
|
|
389
|
+
arrows: readonly { x1: number; y1: number; x2: number; y2: number; label?: string }[];
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* A guide's design px (origin at the canvas centre, +y down) laid into the
|
|
394
|
+
* exportable frame's rect, the one conversion the guide takes: a sketch is a
|
|
395
|
+
* picture of the frame, so it scales and moves with the frame and never with
|
|
396
|
+
* a layer.
|
|
397
|
+
*/
|
|
398
|
+
export function sketchGuideScreen(
|
|
399
|
+
guide: ShotSketchGuide,
|
|
400
|
+
frame: OverlayProps["frame"],
|
|
401
|
+
size: { width: number; height: number },
|
|
402
|
+
): ScreenSketchGuide {
|
|
403
|
+
const kx = frame.width / size.width;
|
|
404
|
+
const ky = frame.height / size.height;
|
|
405
|
+
const x = (dx: number): number => frame.x + (dx + size.width / 2) * kx;
|
|
406
|
+
const y = (dy: number): number => frame.y + (dy + size.height / 2) * ky;
|
|
407
|
+
return {
|
|
408
|
+
rects: guide.rects.map((r) => ({
|
|
409
|
+
x: x(r.at[0] - r.size[0] / 2),
|
|
410
|
+
y: y(r.at[1] - r.size[1] / 2),
|
|
411
|
+
width: r.size[0] * kx,
|
|
412
|
+
height: r.size[1] * ky,
|
|
413
|
+
radius: (r.radius ?? 0) * Math.min(kx, ky),
|
|
414
|
+
...(r.label === undefined ? {} : { label: r.label }),
|
|
415
|
+
})),
|
|
416
|
+
arrows: guide.arrows.map((a) => ({
|
|
417
|
+
x1: x(a.from[0]),
|
|
418
|
+
y1: y(a.from[1]),
|
|
419
|
+
x2: x(a.to[0]),
|
|
420
|
+
y2: y(a.to[1]),
|
|
421
|
+
...(a.label === undefined ? {} : { label: a.label }),
|
|
422
|
+
})),
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
240
426
|
export function Overlays({
|
|
427
|
+
watching,
|
|
241
428
|
width,
|
|
242
429
|
height,
|
|
243
430
|
panes,
|
|
@@ -248,21 +435,51 @@ export function Overlays({
|
|
|
248
435
|
unionCamera,
|
|
249
436
|
unionWorking,
|
|
250
437
|
hover,
|
|
251
|
-
|
|
252
|
-
|
|
438
|
+
hintCamera,
|
|
439
|
+
hintWorking,
|
|
440
|
+
failedCamera,
|
|
441
|
+
failedWorking,
|
|
442
|
+
maskCamera,
|
|
443
|
+
maskWorking,
|
|
444
|
+
peersCamera,
|
|
445
|
+
peersWorking,
|
|
446
|
+
failure,
|
|
447
|
+
comments,
|
|
448
|
+
sketchGuide,
|
|
449
|
+
pinLayer,
|
|
450
|
+
emptyHint,
|
|
451
|
+
pilot,
|
|
452
|
+
driver,
|
|
453
|
+
isolating,
|
|
253
454
|
marquee,
|
|
254
455
|
guides,
|
|
255
456
|
guidesWorking,
|
|
457
|
+
splineGuides,
|
|
458
|
+
motionPathsCamera,
|
|
459
|
+
motionPathsWorking,
|
|
460
|
+
pathKeyHover,
|
|
461
|
+
pathKeySelected,
|
|
462
|
+
pathHandleHover,
|
|
256
463
|
pen,
|
|
464
|
+
spline,
|
|
257
465
|
modal,
|
|
466
|
+
axisGuide,
|
|
258
467
|
nav,
|
|
259
468
|
keyLight,
|
|
260
469
|
inset,
|
|
261
470
|
}: OverlayProps) {
|
|
262
471
|
const { working, camera } = panes;
|
|
472
|
+
// ONE reading of the inset per pane, and every piece of furniture below lays
|
|
473
|
+
// out inside it. See `clearOf`: the two sites that did this arithmetic
|
|
474
|
+
// themselves both charged their pane for a panel on the other side of the
|
|
475
|
+
// seam.
|
|
476
|
+
const viewport: Viewport = { width, height };
|
|
477
|
+
const clearWorking = working === null ? null : clearOf(working, inset, viewport);
|
|
478
|
+
const clearCamera = camera === null ? null : clearOf(camera, inset, viewport);
|
|
263
479
|
return (
|
|
264
480
|
<svg
|
|
265
481
|
className="stage-overlay"
|
|
482
|
+
data-dailies-stage-furniture=""
|
|
266
483
|
width={width}
|
|
267
484
|
height={height}
|
|
268
485
|
viewBox={`0 0 ${width} ${height}`}
|
|
@@ -275,7 +492,7 @@ export function Overlays({
|
|
|
275
492
|
{camera === null ? null : <PaneClip id="shot-pane-camera" rect={camera} />}
|
|
276
493
|
</defs>
|
|
277
494
|
|
|
278
|
-
{working === null ? null : (
|
|
495
|
+
{working === null || clearWorking === null ? null : (
|
|
279
496
|
<g clipPath="url(#shot-pane-working)" transform={`translate(${working.x} ${working.y})`}>
|
|
280
497
|
{/* NO FRAME BORDER. The exportable frame is a property of the shot
|
|
281
498
|
camera's projection; drawn in an orbit view it would be a
|
|
@@ -283,6 +500,36 @@ export function Overlays({
|
|
|
283
500
|
{/* UNDER the selection: a layer that is both hovered and selected
|
|
284
501
|
must read as selected, not as a heavier version of hovered. */}
|
|
285
502
|
{hover?.pane === "working" ? <HoverQuad quad={hover.quad} /> : null}
|
|
503
|
+
{/* The chrome's row-hover, same treatment: which layer is this. */}
|
|
504
|
+
{hintWorking === null ? null : <HoverQuad quad={hintWorking} />}
|
|
505
|
+
{peersWorking.map((row, i) => (
|
|
506
|
+
<PeerOutline key={`peer-${i}`} {...row} />
|
|
507
|
+
))}
|
|
508
|
+
{/* Under the selection, like the hover: a broken layer that is
|
|
509
|
+
selected must still read as selected. */}
|
|
510
|
+
{failedWorking.map((row, i) => (
|
|
511
|
+
<FailedPlaceholder key={`failed-${i}`} {...row} />
|
|
512
|
+
))}
|
|
513
|
+
{/* UNDER everything the author is judging — it is a reference line,
|
|
514
|
+
not a mark. Dashed and half-lit for the same reason: at full
|
|
515
|
+
weight a line clean across the pane reads as geometry. */}
|
|
516
|
+
{axisGuide === null || (axisGuide.pane ?? "working") !== "working" ? null : (
|
|
517
|
+
<AxisGuideLine guide={axisGuide} />
|
|
518
|
+
)}
|
|
519
|
+
{splineGuides.map((guide) => (
|
|
520
|
+
<polyline
|
|
521
|
+
key={`spline-guide-${guide.id}`}
|
|
522
|
+
points={flatPolyline(guide.pts)}
|
|
523
|
+
fill="none"
|
|
524
|
+
stroke="var(--accent)"
|
|
525
|
+
strokeWidth={guide.selected ? 1.5 : 1}
|
|
526
|
+
strokeDasharray={guide.selected ? undefined : "4 4"}
|
|
527
|
+
opacity={guide.selected ? 1 : 0.72}
|
|
528
|
+
/>
|
|
529
|
+
))}
|
|
530
|
+
{maskWorking.map((row, i) => (
|
|
531
|
+
<MaskOutline key={`mask-${i}`} {...row} />
|
|
532
|
+
))}
|
|
286
533
|
{quadsWorking.map((q, i) => (
|
|
287
534
|
// HANDLES HERE TOO, since `runTransformGesture` learned to measure
|
|
288
535
|
// a resize by unprojecting two rays onto the layer's own plane. The
|
|
@@ -296,6 +543,16 @@ export function Overlays({
|
|
|
296
543
|
/>
|
|
297
544
|
))}
|
|
298
545
|
{unionWorking === null ? null : <UnionBox quad={unionWorking} />}
|
|
546
|
+
{motionPathsWorking.map((path) => (
|
|
547
|
+
<MotionPathMark
|
|
548
|
+
key={`path-${path.layerId}`}
|
|
549
|
+
path={path}
|
|
550
|
+
hover={pathKeyHover}
|
|
551
|
+
selected={pathKeySelected}
|
|
552
|
+
handleHover={pathHandleHover}
|
|
553
|
+
/>
|
|
554
|
+
))}
|
|
555
|
+
{spline === null ? null : <SplinePreview spline={spline} />}
|
|
299
556
|
{/* The snap guides, as the SEGMENTS they are once the plane is seen
|
|
300
557
|
at an angle. Same colour and weight as the camera pane's, because
|
|
301
558
|
they are the same guides. */}
|
|
@@ -306,43 +563,107 @@ export function Overlays({
|
|
|
306
563
|
y1={g.a[1]}
|
|
307
564
|
x2={g.b[0]}
|
|
308
565
|
y2={g.b[1]}
|
|
309
|
-
stroke="var(--guide, #
|
|
566
|
+
stroke="var(--guide, #f5a524)"
|
|
310
567
|
strokeWidth={1}
|
|
311
568
|
/>
|
|
312
569
|
))}
|
|
313
570
|
{marquee?.pane === "working" ? <Marquee rect={marquee} /> : null}
|
|
571
|
+
{pen?.pane === "working" ? <PenPreview pen={pen} /> : null}
|
|
572
|
+
{isolating ? <IsolationRing clear={clearWorking} /> : null}
|
|
314
573
|
{/* The mode bars STACK DOWN FROM the chrome's bottom edge, so they
|
|
315
574
|
are the first thing under the header rather than behind it. */}
|
|
316
|
-
{
|
|
317
|
-
<
|
|
575
|
+
{driver === null ? null : (
|
|
576
|
+
<ModeBar
|
|
318
577
|
width={working.width}
|
|
319
|
-
y={
|
|
320
|
-
left={
|
|
321
|
-
|
|
578
|
+
y={clearWorking.y}
|
|
579
|
+
left={clearWorking.x}
|
|
580
|
+
color="var(--frustum, #eab308)"
|
|
581
|
+
text={`CAMERA DRIVEN BY ${driver.toUpperCase()}`}
|
|
322
582
|
/>
|
|
323
583
|
)}
|
|
324
|
-
{
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
{
|
|
328
|
-
|
|
584
|
+
{keyLight === null ? null : <KeyLightGizmo light={keyLight} />}
|
|
585
|
+
{modal?.pane === "working" ? <ModalBadge modal={modal} clear={clearWorking} /> : null}
|
|
586
|
+
{nav === null ? null : <NavGizmo nav={nav} clear={clearWorking} />}
|
|
587
|
+
{/* The failure bar in the one pane there is, when there is no
|
|
588
|
+
camera pane to carry it. */}
|
|
589
|
+
{failure === null || camera !== null ? null : (
|
|
590
|
+
<ModeBar
|
|
329
591
|
width={working.width}
|
|
330
|
-
y={
|
|
331
|
-
left={
|
|
592
|
+
y={clearWorking.y + clearWorking.height - BAR_H}
|
|
593
|
+
left={clearWorking.x}
|
|
594
|
+
color="var(--danger, #ff5c69)"
|
|
595
|
+
text={failure}
|
|
332
596
|
/>
|
|
333
|
-
) : null}
|
|
334
|
-
{keyLight === null ? null : <KeyLightGizmo light={keyLight} />}
|
|
335
|
-
{modal === null ? null : <ModalBadge modal={modal} rect={working} inset={inset} />}
|
|
336
|
-
{nav === null ? null : (
|
|
337
|
-
<NavGizmo nav={nav} width={working.width} top={inset.top} right={inset.right} />
|
|
338
597
|
)}
|
|
339
598
|
</g>
|
|
340
599
|
)}
|
|
341
600
|
|
|
342
|
-
{camera === null ? null : (
|
|
601
|
+
{camera === null || clearCamera === null ? null : (
|
|
343
602
|
<g clipPath="url(#shot-pane-camera)" transform={`translate(${camera.x} ${camera.y})`}>
|
|
603
|
+
{/* THE FRAME BORDER SURVIVES VIDEO, and nothing else does. It is not
|
|
604
|
+
furniture: it is the edge of the picture being watched, and
|
|
605
|
+
without it a letterboxed shot has no visible boundary at all. */}
|
|
344
606
|
<FrameBorder rect={frame} width={camera.width} height={camera.height} />
|
|
607
|
+
{/* A BROKEN PIECE SURVIVES VIDEO TOO: watching a picture with a hole
|
|
608
|
+
in it is exactly when the author must be told it has one. At the
|
|
609
|
+
bottom of the clear band, where no mode bar stacks. */}
|
|
610
|
+
{failedCamera.map((row, i) => (
|
|
611
|
+
<FailedPlaceholder key={`failed-${i}`} {...row} />
|
|
612
|
+
))}
|
|
613
|
+
{failure === null ? null : (
|
|
614
|
+
<ModeBar
|
|
615
|
+
width={camera.width}
|
|
616
|
+
y={clearCamera.y + clearCamera.height - BAR_H}
|
|
617
|
+
left={clearCamera.x}
|
|
618
|
+
color="var(--danger, #ff5c69)"
|
|
619
|
+
text={failure}
|
|
620
|
+
/>
|
|
621
|
+
)}
|
|
622
|
+
{watching ? null : (
|
|
623
|
+
<>
|
|
624
|
+
{/* PILOTING is this pane's mode: its middle-drag, shift-drag and
|
|
625
|
+
wheel are writing the shot camera. Top of the clear band. */}
|
|
626
|
+
{pilot === null ? null : (
|
|
627
|
+
<PilotBadge
|
|
628
|
+
width={camera.width}
|
|
629
|
+
y={clearCamera.y}
|
|
630
|
+
left={clearCamera.x}
|
|
631
|
+
right={clearCamera.x + clearCamera.width}
|
|
632
|
+
text={pilot.text}
|
|
633
|
+
onEject={pilot.onEject}
|
|
634
|
+
/>
|
|
635
|
+
)}
|
|
636
|
+
{emptyHint === null ? null : (
|
|
637
|
+
<text
|
|
638
|
+
data-testid="empty-hint"
|
|
639
|
+
x={frame.x + frame.width / 2}
|
|
640
|
+
y={frame.y + frame.height / 2}
|
|
641
|
+
textAnchor="middle"
|
|
642
|
+
dominantBaseline="middle"
|
|
643
|
+
fill="var(--ink-3, #9c9c9c)"
|
|
644
|
+
// A ground under the words, painted first: the frame is
|
|
645
|
+
// the Shot's background, which can be any colour.
|
|
646
|
+
stroke="var(--surface-app, #080808)"
|
|
647
|
+
strokeWidth={3}
|
|
648
|
+
paintOrder="stroke"
|
|
649
|
+
fontFamily="var(--font-sans, system-ui)"
|
|
650
|
+
fontSize={13}
|
|
651
|
+
>
|
|
652
|
+
{emptyHint}
|
|
653
|
+
</text>
|
|
654
|
+
)}
|
|
655
|
+
{/* THE SKETCH GUIDE (bridge v47), first: a reference to animate
|
|
656
|
+
toward, under everything the author is editing. */}
|
|
657
|
+
{sketchGuide === null ? null : <SketchGuideOverlay guide={sketchGuide} />}
|
|
345
658
|
{hover?.pane === "camera" ? <HoverQuad quad={hover.quad} /> : null}
|
|
659
|
+
{/* The chrome's row-hover, same treatment: which layer is this. */}
|
|
660
|
+
{hintCamera === null ? null : <HoverQuad quad={hintCamera} />}
|
|
661
|
+
{peersCamera.map((row, i) => (
|
|
662
|
+
<PeerOutline key={`peer-${i}`} {...row} />
|
|
663
|
+
))}
|
|
664
|
+
{maskCamera.map((row, i) => (
|
|
665
|
+
<MaskOutline key={`mask-${i}`} {...row} />
|
|
666
|
+
))}
|
|
346
667
|
{quadsCamera.map((q, i) => (
|
|
347
668
|
// A LOCKED LAYER DRAWS NO HANDLES, matching `grabAt`, which refuses
|
|
348
669
|
// to answer one. A handle that cannot be grabbed is a handle the
|
|
@@ -354,6 +675,16 @@ export function Overlays({
|
|
|
354
675
|
<SelectionQuad key={i} quad={q} handles={locked[i] !== true && unionCamera === null} />
|
|
355
676
|
))}
|
|
356
677
|
{unionCamera === null ? null : <UnionBox quad={unionCamera} />}
|
|
678
|
+
{axisGuide?.pane !== "camera" ? null : <AxisGuideLine guide={axisGuide} />}
|
|
679
|
+
{motionPathsCamera.map((path) => (
|
|
680
|
+
<MotionPathMark
|
|
681
|
+
key={`path-${path.layerId}`}
|
|
682
|
+
path={path}
|
|
683
|
+
hover={pathKeyHover}
|
|
684
|
+
selected={pathKeySelected}
|
|
685
|
+
handleHover={pathHandleHover}
|
|
686
|
+
/>
|
|
687
|
+
))}
|
|
357
688
|
{guides.map((g, i) =>
|
|
358
689
|
g.axis === "x" ? (
|
|
359
690
|
<line
|
|
@@ -362,7 +693,7 @@ export function Overlays({
|
|
|
362
693
|
y1={0}
|
|
363
694
|
x2={g.at}
|
|
364
695
|
y2={camera.height}
|
|
365
|
-
stroke="var(--guide, #
|
|
696
|
+
stroke="var(--guide, #f5a524)"
|
|
366
697
|
strokeWidth={1}
|
|
367
698
|
/>
|
|
368
699
|
) : (
|
|
@@ -372,13 +703,16 @@ export function Overlays({
|
|
|
372
703
|
y1={g.at}
|
|
373
704
|
x2={camera.width}
|
|
374
705
|
y2={g.at}
|
|
375
|
-
stroke="var(--guide, #
|
|
706
|
+
stroke="var(--guide, #f5a524)"
|
|
376
707
|
strokeWidth={1}
|
|
377
708
|
/>
|
|
378
709
|
),
|
|
379
710
|
)}
|
|
380
711
|
{marquee?.pane === "camera" ? <Marquee rect={marquee} /> : null}
|
|
381
|
-
{pen ===
|
|
712
|
+
{pen?.pane === "camera" ? <PenPreview pen={pen} /> : null}
|
|
713
|
+
{/* THE COMMENT PINS (bridge v45), over what the author is editing
|
|
714
|
+
through and never under it. */}
|
|
715
|
+
{comments === null ? null : <CommentPinsOverlay comments={comments} pinLayer={pinLayer ?? null} frame={frame} />}
|
|
382
716
|
{/* THE BADGE FOLLOWS THE MODE, NOT THE PANE. Camera-only plus
|
|
383
717
|
isolated is a legitimate state — the author can switch layout
|
|
384
718
|
while inside a subtree — and it had zero indicators while the dim
|
|
@@ -386,13 +720,12 @@ export function Overlays({
|
|
|
386
720
|
outside the card did nothing with nothing to say why. Drawn here
|
|
387
721
|
only when the working pane is off screen, or the two would both
|
|
388
722
|
render it. */}
|
|
389
|
-
{
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
/>
|
|
723
|
+
{isolating && working === null ? <IsolationRing clear={clearCamera} /> : null}
|
|
724
|
+
{/* A drag that STARTED HERE reads its numbers here. Modals never do
|
|
725
|
+
— the keymap arms them over the working pane alone — so in
|
|
726
|
+
practice this is the resize / body-move / roll readout. */}
|
|
727
|
+
{modal?.pane === "camera" ? <ModalBadge modal={modal} clear={clearCamera} /> : null}
|
|
728
|
+
</>
|
|
396
729
|
)}
|
|
397
730
|
</g>
|
|
398
731
|
)}
|
|
@@ -426,57 +759,115 @@ function PaneClip({ id, rect }: { id: string; rect: PaneRect }) {
|
|
|
426
759
|
* A bar and not a border: a border here would be one more rectangle in a pane
|
|
427
760
|
* that already draws a frustum, a selection and a divider.
|
|
428
761
|
*/
|
|
429
|
-
/** Height of a mode bar. Shared so
|
|
430
|
-
*
|
|
762
|
+
/** Height of a mode bar. Shared so the bars that stack can do so without
|
|
763
|
+
* restating each other's geometry. */
|
|
431
764
|
const BAR_H = 22;
|
|
432
765
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
y={y}
|
|
438
|
-
left={left}
|
|
439
|
-
color="var(--frustum, #eab308)"
|
|
440
|
-
text="CAMERA FOLLOWS THIS VIEW"
|
|
441
|
-
/>
|
|
442
|
-
);
|
|
443
|
-
}
|
|
766
|
+
/** How wide the EJECT plate on the pilot badge is, and its inset from the
|
|
767
|
+
* clear band's right edge. */
|
|
768
|
+
const EJECT_W = 58;
|
|
769
|
+
const EJECT_GAP = 8;
|
|
444
770
|
|
|
445
771
|
/**
|
|
446
|
-
* "You are
|
|
772
|
+
* "You are piloting the shot camera", said in the pane whose navigation is
|
|
773
|
+
* writing it, with the way out on the badge.
|
|
447
774
|
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
* THE WAY OUT IS IN THE BADGE, for the reason the badge exists at all: a mode
|
|
456
|
-
* the author entered by accident has to be leavable without finding the panel
|
|
457
|
-
* that documents it.
|
|
458
|
-
*
|
|
459
|
-
* The accent, not the frustum's yellow: the subject is a LAYER, which is the
|
|
460
|
-
* same subject-colour rule ModalBadge follows.
|
|
775
|
+
* Unreal's pilot banner: the piloted thing named top-left and an eject beside
|
|
776
|
+
* it, because a mode that turns every drag into a document write is the one
|
|
777
|
+
* an author must never have to hunt for the exit of. The text is the chrome's
|
|
778
|
+
* (it knows whether a move or a component holds the camera); the plate is the
|
|
779
|
+
* one piece of this bar that takes pointer events, on the nav gizmo's pattern
|
|
780
|
+
* (`pointerEvents="auto"` opting back in under a `none` root).
|
|
461
781
|
*/
|
|
462
|
-
function
|
|
782
|
+
function PilotBadge({
|
|
463
783
|
width,
|
|
464
784
|
y,
|
|
465
|
-
label,
|
|
466
785
|
left,
|
|
786
|
+
right,
|
|
787
|
+
text,
|
|
788
|
+
onEject,
|
|
467
789
|
}: {
|
|
468
790
|
width: number;
|
|
469
791
|
y: number;
|
|
470
|
-
label: string;
|
|
471
792
|
left: number;
|
|
793
|
+
right: number;
|
|
794
|
+
text: string;
|
|
795
|
+
onEject: () => void;
|
|
472
796
|
}) {
|
|
797
|
+
const x = Math.max(left, right - EJECT_GAP - EJECT_W);
|
|
473
798
|
return (
|
|
474
|
-
<
|
|
475
|
-
width={width}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
799
|
+
<g data-testid="pilot-badge">
|
|
800
|
+
<ModeBar width={width} y={y} left={left} color="var(--frustum, #eab308)" text={text} />
|
|
801
|
+
<g
|
|
802
|
+
pointerEvents="auto"
|
|
803
|
+
style={{ cursor: "pointer" }}
|
|
804
|
+
data-testid="pilot-eject"
|
|
805
|
+
onPointerDown={(e) => {
|
|
806
|
+
// The press is CONSUMED here: the stage's own lane would otherwise
|
|
807
|
+
// read it as a camera-pane press and start a marquee under the badge.
|
|
808
|
+
e.stopPropagation();
|
|
809
|
+
e.preventDefault();
|
|
810
|
+
onEject();
|
|
811
|
+
}}
|
|
812
|
+
>
|
|
813
|
+
<rect
|
|
814
|
+
x={x}
|
|
815
|
+
y={y + 3}
|
|
816
|
+
width={EJECT_W}
|
|
817
|
+
height={BAR_H - 6}
|
|
818
|
+
rx={3}
|
|
819
|
+
fill="var(--frustum, #eab308)"
|
|
820
|
+
fillOpacity={0.28}
|
|
821
|
+
/>
|
|
822
|
+
<text
|
|
823
|
+
x={x + EJECT_W / 2}
|
|
824
|
+
y={y + 15}
|
|
825
|
+
textAnchor="middle"
|
|
826
|
+
fill="var(--frustum, #eab308)"
|
|
827
|
+
fontFamily="var(--font-sans, system-ui)"
|
|
828
|
+
fontSize={10}
|
|
829
|
+
fontWeight={700}
|
|
830
|
+
letterSpacing={0.6}
|
|
831
|
+
>
|
|
832
|
+
EJECT
|
|
833
|
+
</text>
|
|
834
|
+
</g>
|
|
835
|
+
</g>
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* "You are inside a layer", said INSIDE the pane it is true of, as a ring.
|
|
841
|
+
*
|
|
842
|
+
* Isolation is the largest mode this pane has: `StageTools` swaps a derived
|
|
843
|
+
* orthographic head-on rig into the working camera's slot, and picking is
|
|
844
|
+
* scoped to the subtree. With no in-pane sign, that reads as the view having
|
|
845
|
+
* frozen. The sign was once a tinted bar of capitals across the top of the
|
|
846
|
+
* picture ("INSIDE CARD. ESC TO LEAVE"), which the author read as cheap and
|
|
847
|
+
* which covered the top of the very subject they had entered to edit.
|
|
848
|
+
*
|
|
849
|
+
* The ring says only "a mode is on". WHICH layer, the trail back up and the
|
|
850
|
+
* way out are the breadcrumb in the stage header (`IsolationTrail`), where
|
|
851
|
+
* every other piece of navigation lives. It traces the clear band, so it sits
|
|
852
|
+
* inside the docked chrome rather than under it.
|
|
853
|
+
*
|
|
854
|
+
* The accent, not the frustum's yellow: the subject is a LAYER, which is the
|
|
855
|
+
* same subject-colour rule ModalBadge follows.
|
|
856
|
+
*/
|
|
857
|
+
function IsolationRing({ clear }: { clear: PaneRect }) {
|
|
858
|
+
return (
|
|
859
|
+
<rect
|
|
860
|
+
data-testid="isolation-ring"
|
|
861
|
+
x={clear.x + 1}
|
|
862
|
+
y={clear.y + 1}
|
|
863
|
+
width={Math.max(0, clear.width - 2)}
|
|
864
|
+
height={Math.max(0, clear.height - 2)}
|
|
865
|
+
rx={6}
|
|
866
|
+
fill="none"
|
|
867
|
+
stroke="var(--accent, #0099ff)"
|
|
868
|
+
strokeOpacity={0.55}
|
|
869
|
+
strokeWidth={2}
|
|
870
|
+
pointerEvents="none"
|
|
480
871
|
/>
|
|
481
872
|
);
|
|
482
873
|
}
|
|
@@ -486,11 +877,11 @@ function ModeBar({
|
|
|
486
877
|
y,
|
|
487
878
|
color,
|
|
488
879
|
text,
|
|
489
|
-
/**
|
|
490
|
-
* ONLY PART THAT MOVES: the tinted band is a full-width mode
|
|
491
|
-
* reads correctly under a panel, but the words did not — they
|
|
492
|
-
* behind an opaque rail, so the only thing naming isolation
|
|
493
|
-
* was invisible at every default panel width. */
|
|
880
|
+
/** The left edge of this pane's clear band, pane-local (`clearOf`). THE TEXT
|
|
881
|
+
* IS THE ONLY PART THAT MOVES: the tinted band is a full-width mode
|
|
882
|
+
* indicator and reads correctly under a panel, but the words did not — they
|
|
883
|
+
* drew at x = 10, behind an opaque rail, so the only thing naming isolation
|
|
884
|
+
* or follow mode was invisible at every default panel width. */
|
|
494
885
|
left,
|
|
495
886
|
}: {
|
|
496
887
|
width: number;
|
|
@@ -507,7 +898,7 @@ function ModeBar({
|
|
|
507
898
|
x={left + 10}
|
|
508
899
|
y={y + 16}
|
|
509
900
|
fill={color}
|
|
510
|
-
fontFamily="var(--font-
|
|
901
|
+
fontFamily="var(--font-sans, system-ui)"
|
|
511
902
|
fontSize={11}
|
|
512
903
|
fontWeight={600}
|
|
513
904
|
letterSpacing={0.4}
|
|
@@ -545,26 +936,33 @@ const MODAL_GLYPHS: Record<ModalMode, string> = {
|
|
|
545
936
|
const GLYPH_PX = 14;
|
|
546
937
|
const GLYPH_GAP_PX = 6;
|
|
547
938
|
|
|
548
|
-
/** The axis palette, the same tokens the nav gizmo and the move arms wear.
|
|
939
|
+
/** The axis palette, the same tokens the nav gizmo and the move arms wear. ONE
|
|
940
|
+
* map for both overlay sites: the nav ball kept its own copy of the three hexes
|
|
941
|
+
* and drew from those instead, so re-pointing an --axis-* token moved the modal
|
|
942
|
+
* badge's glyph and left the ball behind. */
|
|
549
943
|
const AXIS_COLORS: Record<"x" | "y" | "z", string> = {
|
|
550
944
|
x: "var(--axis-x, #ef4444)",
|
|
551
945
|
y: "var(--axis-y, #22c55e)",
|
|
552
946
|
z: "var(--axis-z, #3b82f6)",
|
|
553
947
|
};
|
|
554
948
|
|
|
555
|
-
function ModalBadge({ modal,
|
|
556
|
-
const color = modal.camera ? "var(--frustum, #eab308)" : "var(--accent, #
|
|
949
|
+
function ModalBadge({ modal, clear }: { modal: ScreenModal; clear: PaneRect }) {
|
|
950
|
+
const color = modal.camera ? "var(--frustum, #eab308)" : "var(--accent, #0099ff)";
|
|
557
951
|
// The glyph wears the AXIS's colour when one is locked, and the pill's own
|
|
558
952
|
// otherwise. The lock is the thing most worth seeing at a glance, and it is
|
|
559
953
|
// the one piece of modal state that changes mid-gesture.
|
|
560
954
|
const glyphColor = modal.axis === null ? color : AXIS_COLORS[modal.axis];
|
|
561
955
|
const shift = GLYPH_PX + GLYPH_GAP_PX;
|
|
562
|
-
// INSIDE
|
|
563
|
-
// is behind the 232px left rail and behind the
|
|
564
|
-
// thing that says a transform is live
|
|
565
|
-
// layout.
|
|
566
|
-
|
|
567
|
-
|
|
956
|
+
// INSIDE THIS PANE'S CLEAR BAND, on both axes. It sat at x = 10 and
|
|
957
|
+
// y = height - 34, which is behind the 232px left rail and behind the
|
|
958
|
+
// timeline dock — so the only thing that says a transform is live had never
|
|
959
|
+
// been visible in the default layout. It then sat at `inset.left + 10`, which
|
|
960
|
+
// is right for the working pane (whose origin IS the window's) and wrong for
|
|
961
|
+
// a camera pane in a split, where pane-local x is measured from the seam: a
|
|
962
|
+
// camera-pane resize put its numbers a rail's width in from an edge no rail
|
|
963
|
+
// touches. `clear` is the answer for whichever pane this is. See `clearOf`.
|
|
964
|
+
const x = clear.x + 10;
|
|
965
|
+
const y = clear.y + clear.height - 34;
|
|
568
966
|
// The live numbers, written straight into the node. See `modalReadoutSink`:
|
|
569
967
|
// the readout changes per pointermove and the pill is memoised on state, so
|
|
570
968
|
// the two have to meet outside React or motion costs a stage commit.
|
|
@@ -578,7 +976,7 @@ function ModalBadge({ modal, rect, inset }: { modal: ScreenModal; rect: PaneRect
|
|
|
578
976
|
};
|
|
579
977
|
}, []);
|
|
580
978
|
return (
|
|
581
|
-
<g pointerEvents="none">
|
|
979
|
+
<g data-testid="modal-badge" pointerEvents="none">
|
|
582
980
|
<rect
|
|
583
981
|
x={x}
|
|
584
982
|
y={y}
|
|
@@ -588,7 +986,7 @@ function ModalBadge({ modal, rect, inset }: { modal: ScreenModal; rect: PaneRect
|
|
|
588
986
|
// plate; a pill too NARROW would clip the figure the author is reading.
|
|
589
987
|
width={modal.label.length * 7.5 + 96 + shift}
|
|
590
988
|
height={24}
|
|
591
|
-
fill="var(--panel, #
|
|
989
|
+
fill="var(--surface-panel, #111111)"
|
|
592
990
|
fillOpacity={0.88}
|
|
593
991
|
stroke={color}
|
|
594
992
|
strokeWidth={1}
|
|
@@ -609,7 +1007,7 @@ function ModalBadge({ modal, rect, inset }: { modal: ScreenModal; rect: PaneRect
|
|
|
609
1007
|
x={x + 10 + shift}
|
|
610
1008
|
y={y + 16}
|
|
611
1009
|
fill={color}
|
|
612
|
-
fontFamily="var(--font-
|
|
1010
|
+
fontFamily="var(--font-sans, system-ui)"
|
|
613
1011
|
fontSize={11}
|
|
614
1012
|
fontWeight={600}
|
|
615
1013
|
letterSpacing={0.6}
|
|
@@ -620,7 +1018,7 @@ function ModalBadge({ modal, rect, inset }: { modal: ScreenModal; rect: PaneRect
|
|
|
620
1018
|
ref={readout}
|
|
621
1019
|
x={x + modal.label.length * 7.5 + 22 + shift}
|
|
622
1020
|
y={y + 16}
|
|
623
|
-
fill="var(--
|
|
1021
|
+
fill="var(--ink-1, #f5f5f5)"
|
|
624
1022
|
fontFamily="var(--font-mono, ui-monospace, monospace)"
|
|
625
1023
|
fontSize={11}
|
|
626
1024
|
fontWeight={500}
|
|
@@ -659,7 +1057,7 @@ function KeyLightGizmo({ light }: { light: ScreenKeyLight }) {
|
|
|
659
1057
|
style={{ cursor: "grab" }}
|
|
660
1058
|
onPointerDown={light.onGrab}
|
|
661
1059
|
>
|
|
662
|
-
<circle r={KEY_DISC_PX} fill="var(--panel, #
|
|
1060
|
+
<circle r={KEY_DISC_PX} fill="var(--surface-panel, #111111)" fillOpacity={0.9} stroke={color} strokeWidth={1.5} />
|
|
663
1061
|
{/* Four short rays, so the disc reads as a key rather than as another
|
|
664
1062
|
handle. Drawn at the diagonals to stay clear of the arm. */}
|
|
665
1063
|
{[45, 135, 225, 315].map((deg) => {
|
|
@@ -710,29 +1108,23 @@ const NAV_MARGIN = 16;
|
|
|
710
1108
|
* letter Y on the arm that moves a layer the opposite way from the number the
|
|
711
1109
|
* author is watching.
|
|
712
1110
|
*
|
|
713
|
-
* Top-right
|
|
714
|
-
*
|
|
715
|
-
*
|
|
1111
|
+
* Top-right of the part of this pane the chrome is not covering, which is where
|
|
1112
|
+
* every 3D tool puts it. Staying clear of the panels is not cosmetic here: the
|
|
1113
|
+
* gizmo is the only piece of overlay furniture that TAKES POINTER EVENTS, so
|
|
1114
|
+
* drawing it under an opaque panel makes it unclickable rather than merely
|
|
1115
|
+
* hidden. That reach used to be a hardcoded 116 for the top edge, which a
|
|
716
1116
|
* wrapping control strip on a narrow window grew straight past.
|
|
1117
|
+
*
|
|
1118
|
+
* IT ASKS FOR A CLEAR RECT AND SUBTRACTS NOTHING. It used to compute
|
|
1119
|
+
* `width - inset.right - gap` from the pane's own width, and in a split that is
|
|
1120
|
+
* a double count: the inspector sits on the far side of the SEAM and covers no
|
|
1121
|
+
* part of the working pane, so the gizmo was shoved an inspector's width left,
|
|
1122
|
+
* into the middle of the picture it is meant to sit beside. See `clearOf`.
|
|
717
1123
|
*/
|
|
718
|
-
function NavGizmo({
|
|
719
|
-
nav,
|
|
720
|
-
width,
|
|
721
|
-
top,
|
|
722
|
-
/** How far the inspector reaches into this pane from the right. Unlike the
|
|
723
|
-
* mode bars', this inset is not cosmetic: the gizmo is the only piece of
|
|
724
|
-
* overlay furniture that TAKES POINTER EVENTS, so drawing it under an opaque
|
|
725
|
-
* panel makes it unclickable rather than merely hidden. */
|
|
726
|
-
right,
|
|
727
|
-
}: {
|
|
728
|
-
nav: ScreenNav;
|
|
729
|
-
width: number;
|
|
730
|
-
top: number;
|
|
731
|
-
right: number;
|
|
732
|
-
}) {
|
|
1124
|
+
function NavGizmo({ nav, clear }: { nav: ScreenNav; clear: PaneRect }) {
|
|
733
1125
|
const gap = NAV_RADIUS + NAV_BALL + NAV_MARGIN;
|
|
734
|
-
const cx =
|
|
735
|
-
const cy =
|
|
1126
|
+
const cx = clear.x + clear.width - gap;
|
|
1127
|
+
const cy = clear.y + gap;
|
|
736
1128
|
// FAR FIRST. A flat drawing has no depth buffer, so paint order is the only
|
|
737
1129
|
// cue for which end of an axis is pointing at the author — and without it a
|
|
738
1130
|
// gizmo seen from behind reads exactly like one seen from the front.
|
|
@@ -746,7 +1138,7 @@ function NavGizmo({
|
|
|
746
1138
|
cx={cx}
|
|
747
1139
|
cy={cy}
|
|
748
1140
|
r={NAV_RADIUS + NAV_BALL}
|
|
749
|
-
fill="var(--panel, #
|
|
1141
|
+
fill="var(--surface-panel, #111111)"
|
|
750
1142
|
fillOpacity={0.6}
|
|
751
1143
|
pointerEvents="auto"
|
|
752
1144
|
style={{ cursor: "grab" }}
|
|
@@ -755,7 +1147,7 @@ function NavGizmo({
|
|
|
755
1147
|
{balls.map((ball) => {
|
|
756
1148
|
const x = cx + ball.x * NAV_RADIUS;
|
|
757
1149
|
const y = cy + ball.y * NAV_RADIUS;
|
|
758
|
-
const color =
|
|
1150
|
+
const color = AXIS_COLORS[ball.axis];
|
|
759
1151
|
return (
|
|
760
1152
|
<g
|
|
761
1153
|
key={`${ball.axis}${ball.positive ? "+" : "-"}`}
|
|
@@ -780,7 +1172,7 @@ function NavGizmo({
|
|
|
780
1172
|
// FILLED positive, HOLLOW negative — the one thing that tells the
|
|
781
1173
|
// author whether they are looking at +Z or at -Z when the two
|
|
782
1174
|
// project to the same place.
|
|
783
|
-
fill={ball.positive ? color : "var(--panel, #
|
|
1175
|
+
fill={ball.positive ? color : "var(--surface-panel, #111111)"}
|
|
784
1176
|
stroke={color}
|
|
785
1177
|
strokeWidth={1.5}
|
|
786
1178
|
/>
|
|
@@ -789,8 +1181,8 @@ function NavGizmo({
|
|
|
789
1181
|
x={x}
|
|
790
1182
|
y={y + 3.5}
|
|
791
1183
|
textAnchor="middle"
|
|
792
|
-
fill="var(--
|
|
793
|
-
fontFamily="var(--font-
|
|
1184
|
+
fill="var(--surface-app, #080808)"
|
|
1185
|
+
fontFamily="var(--font-sans, system-ui)"
|
|
794
1186
|
fontSize={10}
|
|
795
1187
|
fontWeight={700}
|
|
796
1188
|
>
|
|
@@ -804,12 +1196,6 @@ function NavGizmo({
|
|
|
804
1196
|
);
|
|
805
1197
|
}
|
|
806
1198
|
|
|
807
|
-
const NAV_FALLBACK: Readonly<Record<"x" | "y" | "z", string>> = {
|
|
808
|
-
x: "#ef4444",
|
|
809
|
-
y: "#22c55e",
|
|
810
|
-
z: "#3b82f6",
|
|
811
|
-
};
|
|
812
|
-
|
|
813
1199
|
/** The seam between the panes. It belongs to the CAMERA pane's first column,
|
|
814
1200
|
* which is what `paneAt` decides, so it is drawn ON that column rather than
|
|
815
1201
|
* straddling it — a half-pixel straddle would put the drawn edge and the
|
|
@@ -821,7 +1207,7 @@ function Divider({ x, height }: { x: number; height: number }) {
|
|
|
821
1207
|
y={0}
|
|
822
1208
|
width={1}
|
|
823
1209
|
height={height}
|
|
824
|
-
fill="var(--stage-divider, #
|
|
1210
|
+
fill="var(--stage-divider, #2a2a2a)"
|
|
825
1211
|
shapeRendering="crispEdges"
|
|
826
1212
|
/>
|
|
827
1213
|
);
|
|
@@ -857,12 +1243,13 @@ function FrameBorder({
|
|
|
857
1243
|
fill="rgba(0,0,0,0.45)"
|
|
858
1244
|
/>
|
|
859
1245
|
<rect
|
|
1246
|
+
data-testid="stage-frame-border"
|
|
860
1247
|
x={rect.x}
|
|
861
1248
|
y={rect.y}
|
|
862
1249
|
width={rect.width}
|
|
863
1250
|
height={rect.height}
|
|
864
1251
|
fill="none"
|
|
865
|
-
stroke="var(--frame-border,
|
|
1252
|
+
stroke="var(--frame-border, rgba(255, 255, 255, 0.22))"
|
|
866
1253
|
strokeWidth={1}
|
|
867
1254
|
shapeRendering="crispEdges"
|
|
868
1255
|
/>
|
|
@@ -878,18 +1265,290 @@ function HoverQuad({ quad }: { quad: ScreenQuad }) {
|
|
|
878
1265
|
<polygon
|
|
879
1266
|
points={pointsOf(quad)}
|
|
880
1267
|
fill="none"
|
|
881
|
-
stroke="var(--accent, #
|
|
1268
|
+
stroke="var(--accent, #0099ff)"
|
|
882
1269
|
strokeOpacity={0.5}
|
|
883
1270
|
strokeWidth={1}
|
|
884
1271
|
/>
|
|
885
1272
|
);
|
|
886
1273
|
}
|
|
887
1274
|
|
|
1275
|
+
/** Paper page 21: a peer's tag is 19 px tall, 6 px each side, sitting on the
|
|
1276
|
+
* outline's top-left corner, which it squares. */
|
|
1277
|
+
const PEER_TAG_H = 19;
|
|
1278
|
+
const PEER_RING = 3;
|
|
1279
|
+
|
|
1280
|
+
/** A peer's outline in their colour, and when it carries the tag, their name
|
|
1281
|
+
* on that colour above it. A layer we hold too gets the outline as a ring
|
|
1282
|
+
* `PEER_RING` px outside our own, so both read. */
|
|
1283
|
+
function PeerOutline({ quad, name, color, tag, ring }: PeerQuad) {
|
|
1284
|
+
const shape = ring ? ringQuad(quad, PEER_RING) : quad;
|
|
1285
|
+
const x = Math.min(shape[0], shape[2], shape[4], shape[6]);
|
|
1286
|
+
const y = Math.min(shape[1], shape[3], shape[5], shape[7]);
|
|
1287
|
+
const label = peerLabel(name);
|
|
1288
|
+
const width = label.length * PLACEHOLDER_CHAR_PX + 12;
|
|
1289
|
+
return (
|
|
1290
|
+
<g data-testid="stage-peer-selection" data-peer={name}>
|
|
1291
|
+
<polygon points={pointsOf(shape)} fill="none" stroke={color} strokeWidth={1.5} />
|
|
1292
|
+
{tag ? (
|
|
1293
|
+
<>
|
|
1294
|
+
<rect x={x - 0.75} y={y - PEER_TAG_H} width={width} height={PEER_TAG_H} rx={4} fill={color} />
|
|
1295
|
+
{/* The corner on the outline is square. */}
|
|
1296
|
+
<rect x={x - 0.75} y={y - 8} width={8} height={8} fill={color} />
|
|
1297
|
+
<text
|
|
1298
|
+
x={x + 5.25}
|
|
1299
|
+
y={y - 5}
|
|
1300
|
+
fill="#0B0B0B"
|
|
1301
|
+
fontFamily="var(--font-sans, Inter, system-ui)"
|
|
1302
|
+
fontSize={11}
|
|
1303
|
+
fontWeight={600}
|
|
1304
|
+
>
|
|
1305
|
+
{label}
|
|
1306
|
+
</text>
|
|
1307
|
+
</>
|
|
1308
|
+
) : null}
|
|
1309
|
+
</g>
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
/** A name fits a tag of at most ~200 px. */
|
|
1314
|
+
const PEER_NAME_MAX = 28;
|
|
1315
|
+
export function peerLabel(name: string): string {
|
|
1316
|
+
return name.length <= PEER_NAME_MAX ? name : `${name.slice(0, PEER_NAME_MAX - 1).trimEnd()}…`;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
/** The quad grown by `by` px on every side: each corner pushed away from the
|
|
1320
|
+
* centre along both axes. Exact for an upright box, close for a turned one. */
|
|
1321
|
+
export function ringQuad(quad: ScreenQuad, by: number): ScreenQuad {
|
|
1322
|
+
const cx = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
|
|
1323
|
+
const cy = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
|
|
1324
|
+
const out: number[] = [...quad];
|
|
1325
|
+
for (let i = 0; i < 8; i += 2) {
|
|
1326
|
+
out[i] = quad[i]! + Math.sign(quad[i]! - cx) * by;
|
|
1327
|
+
out[i + 1] = quad[i + 1]! + Math.sign(quad[i + 1]! - cy) * by;
|
|
1328
|
+
}
|
|
1329
|
+
return out as unknown as ScreenQuad;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
/** Where an isolated layer would draw: a dashed danger outline over a faint
|
|
1333
|
+
* danger wash, and a short label in its top-left corner. Dashed so it never
|
|
1334
|
+
* reads as a selection, washed so the hole reads as a place and not only an
|
|
1335
|
+
* edge, labelled so an empty box is never mistaken for an empty layer. The
|
|
1336
|
+
* reason is the inspector's. The layer stays selectable, movable and
|
|
1337
|
+
* deletable: picking reads the layer's frame, which the runtime still writes,
|
|
1338
|
+
* never what it drew. */
|
|
1339
|
+
function FailedPlaceholder({ quad, label }: FailedQuad) {
|
|
1340
|
+
const x = Math.min(quad[0], quad[2], quad[4], quad[6]);
|
|
1341
|
+
const y = Math.min(quad[1], quad[3], quad[5], quad[7]);
|
|
1342
|
+
const width = Math.max(quad[0], quad[2], quad[4], quad[6]) - x;
|
|
1343
|
+
return (
|
|
1344
|
+
<g data-testid="stage-failed-piece">
|
|
1345
|
+
<polygon
|
|
1346
|
+
points={pointsOf(quad)}
|
|
1347
|
+
fill="var(--danger, #ff5c69)"
|
|
1348
|
+
fillOpacity={0.1}
|
|
1349
|
+
stroke="var(--danger, #ff5c69)"
|
|
1350
|
+
strokeWidth={1.5}
|
|
1351
|
+
strokeDasharray="6 4"
|
|
1352
|
+
/>
|
|
1353
|
+
<text
|
|
1354
|
+
x={x + 6}
|
|
1355
|
+
y={y + 16}
|
|
1356
|
+
fill="var(--danger, #ff5c69)"
|
|
1357
|
+
fontFamily="var(--font-sans, system-ui)"
|
|
1358
|
+
fontSize={11}
|
|
1359
|
+
fontWeight={600}
|
|
1360
|
+
>
|
|
1361
|
+
{placeholderLine(label, width - 12)}
|
|
1362
|
+
</text>
|
|
1363
|
+
</g>
|
|
1364
|
+
);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/** About one 11 px semibold sans character. An estimate: SVG text has no
|
|
1368
|
+
* ellipsis, and measuring would need a layout pass per frame. */
|
|
1369
|
+
const PLACEHOLDER_CHAR_PX = 6.6;
|
|
1370
|
+
const DID_NOT_LOAD = " did not load";
|
|
1371
|
+
|
|
1372
|
+
/** "<name> did not load", cut to `room` px. The name gives way first, so the
|
|
1373
|
+
* line keeps saying what happened for as long as it fits. */
|
|
1374
|
+
export function placeholderLine(label: string, room: number): string {
|
|
1375
|
+
const chars = Math.floor(room / PLACEHOLDER_CHAR_PX);
|
|
1376
|
+
const line = `${label}${DID_NOT_LOAD}`;
|
|
1377
|
+
if (line.length <= chars) return line;
|
|
1378
|
+
const nameRoom = chars - DID_NOT_LOAD.length - 1;
|
|
1379
|
+
if (nameRoom >= 1) return `${label.slice(0, nameRoom).trimEnd()}…${DID_NOT_LOAD}`;
|
|
1380
|
+
return chars >= 2 ? `${line.slice(0, chars - 1).trimEnd()}…` : "";
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
/** A selected Mask group's mask (Paper page 25, "Mask outline (dashed)"): the
|
|
1384
|
+
* selection's accent at its 1.5 px, dashed so it reads as the matte's edge
|
|
1385
|
+
* and never as a second selection. */
|
|
1386
|
+
function MaskOutline({ quad, round }: MaskQuad) {
|
|
1387
|
+
return round ? (
|
|
1388
|
+
<path
|
|
1389
|
+
data-testid="stage-mask-outline"
|
|
1390
|
+
d={inscribedEllipse(quad)}
|
|
1391
|
+
fill="none"
|
|
1392
|
+
stroke="var(--accent, #0099ff)"
|
|
1393
|
+
strokeWidth={1.5}
|
|
1394
|
+
strokeDasharray="4 3"
|
|
1395
|
+
/>
|
|
1396
|
+
) : (
|
|
1397
|
+
<polygon
|
|
1398
|
+
data-testid="stage-mask-outline"
|
|
1399
|
+
points={pointsOf(quad)}
|
|
1400
|
+
fill="none"
|
|
1401
|
+
stroke="var(--accent, #0099ff)"
|
|
1402
|
+
strokeWidth={1.5}
|
|
1403
|
+
strokeDasharray="4 3"
|
|
1404
|
+
/>
|
|
1405
|
+
);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
/** The ellipse inscribed in a quad, as four cubics through its edge midpoints.
|
|
1409
|
+
* Each control point sits `KAPPA` of the way from a midpoint to the corner it
|
|
1410
|
+
* turns about, the circle's cubic, and an affine map keeps it exact for any
|
|
1411
|
+
* parallelogram. A perspective quad gets a close approximation. */
|
|
1412
|
+
const KAPPA = 0.5523;
|
|
1413
|
+
export function inscribedEllipse(quad: ScreenQuad): string {
|
|
1414
|
+
const pt = (i: number): [number, number] => [quad[(i % 4) * 2]!, quad[(i % 4) * 2 + 1]!];
|
|
1415
|
+
const mid = (i: number): [number, number] => {
|
|
1416
|
+
const [ax, ay] = pt(i);
|
|
1417
|
+
const [bx, by] = pt(i + 1);
|
|
1418
|
+
return [(ax + bx) / 2, (ay + by) / 2];
|
|
1419
|
+
};
|
|
1420
|
+
const toward = (m: [number, number], c: [number, number]): string =>
|
|
1421
|
+
`${m[0] + KAPPA * (c[0] - m[0])} ${m[1] + KAPPA * (c[1] - m[1])}`;
|
|
1422
|
+
let d = `M ${mid(0)[0]} ${mid(0)[1]}`;
|
|
1423
|
+
for (let i = 0; i < 4; i++) {
|
|
1424
|
+
// From the midpoint of edge i, about corner i + 1, to the midpoint of edge i + 1.
|
|
1425
|
+
const corner = pt(i + 1);
|
|
1426
|
+
const to = mid(i + 1);
|
|
1427
|
+
d += ` C ${toward(mid(i), corner)} ${toward(to, corner)} ${to[0]} ${to[1]}`;
|
|
1428
|
+
}
|
|
1429
|
+
return `${d} Z`;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
/** Half the current-frame tick's length, CSS px. */
|
|
1433
|
+
const PATH_TICK_HALF = 6;
|
|
1434
|
+
|
|
1435
|
+
function AxisGuideLine({ guide }: { guide: ScreenAxisGuide }) {
|
|
1436
|
+
return (
|
|
1437
|
+
<line
|
|
1438
|
+
x1={guide.a[0]}
|
|
1439
|
+
y1={guide.a[1]}
|
|
1440
|
+
x2={guide.b[0]}
|
|
1441
|
+
y2={guide.b[1]}
|
|
1442
|
+
stroke={AXIS_COLORS[guide.axis]}
|
|
1443
|
+
strokeWidth={1}
|
|
1444
|
+
strokeDasharray="6 5"
|
|
1445
|
+
opacity={0.7}
|
|
1446
|
+
/>
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
/**
|
|
1451
|
+
* One layer's motion path (gap 3): a 1px accent line through hollow key dots,
|
|
1452
|
+
* the key under the playhead filled, a hold's step dashed, and a short tick
|
|
1453
|
+
* across the line where the layer is now. A path outside the layer's lifetime
|
|
1454
|
+
* draws dimmed, since the layer itself is not on screen.
|
|
1455
|
+
*/
|
|
1456
|
+
function MotionPathMark({
|
|
1457
|
+
path,
|
|
1458
|
+
hover,
|
|
1459
|
+
selected,
|
|
1460
|
+
handleHover,
|
|
1461
|
+
}: {
|
|
1462
|
+
path: ScreenMotionPath;
|
|
1463
|
+
hover: PathKeyRef | null;
|
|
1464
|
+
selected: PathKeyRef | null;
|
|
1465
|
+
handleHover: PathHandleRef | null;
|
|
1466
|
+
}) {
|
|
1467
|
+
const picked = selected !== null && selected.layerId === path.layerId ? selected.keyId : null;
|
|
1468
|
+
const pickedKey = picked === null ? undefined : path.keys.find((k) => k.id === picked);
|
|
1469
|
+
return (
|
|
1470
|
+
<g data-testid="motion-path" data-layer={path.layerId} opacity={path.dim ? 0.4 : 1}>
|
|
1471
|
+
{path.runs.map((run, i) => (
|
|
1472
|
+
<polyline
|
|
1473
|
+
key={i}
|
|
1474
|
+
points={flatPolyline(run.pts)}
|
|
1475
|
+
fill="none"
|
|
1476
|
+
stroke="var(--accent, #0099ff)"
|
|
1477
|
+
strokeWidth={1}
|
|
1478
|
+
strokeDasharray={run.jump ? "4 4" : undefined}
|
|
1479
|
+
/>
|
|
1480
|
+
))}
|
|
1481
|
+
{path.tick === null ? null : (
|
|
1482
|
+
<line
|
|
1483
|
+
x1={path.tick.x - path.tick.nx * PATH_TICK_HALF}
|
|
1484
|
+
y1={path.tick.y - path.tick.ny * PATH_TICK_HALF}
|
|
1485
|
+
x2={path.tick.x + path.tick.nx * PATH_TICK_HALF}
|
|
1486
|
+
y2={path.tick.y + path.tick.ny * PATH_TICK_HALF}
|
|
1487
|
+
stroke="var(--ink-1, #f5f5f5)"
|
|
1488
|
+
strokeWidth={1.5}
|
|
1489
|
+
/>
|
|
1490
|
+
)}
|
|
1491
|
+
{/* Off the bezier arm a dot has no handles, so nothing draws. */}
|
|
1492
|
+
{pickedKey === undefined || (pickedKey.in === undefined && pickedKey.out === undefined) ? null : (
|
|
1493
|
+
<PathKeyHandles
|
|
1494
|
+
dot={pickedKey}
|
|
1495
|
+
hot={handleHover !== null && handleHover.layerId === path.layerId && handleHover.keyId === pickedKey.id ? handleHover.part : null}
|
|
1496
|
+
/>
|
|
1497
|
+
)}
|
|
1498
|
+
{path.keys.map((key) => {
|
|
1499
|
+
const hot =
|
|
1500
|
+
(hover !== null && hover.layerId === path.layerId && hover.keyId === key.id) || key.id === picked;
|
|
1501
|
+
return (
|
|
1502
|
+
<circle
|
|
1503
|
+
key={key.id}
|
|
1504
|
+
data-testid="motion-path-key"
|
|
1505
|
+
data-current={key.current ? "" : undefined}
|
|
1506
|
+
cx={key.x}
|
|
1507
|
+
cy={key.y}
|
|
1508
|
+
r={key.current ? 4.5 : hot ? 5 : 4}
|
|
1509
|
+
fill={key.current ? "var(--accent, #0099ff)" : "var(--surface-panel, #111111)"}
|
|
1510
|
+
stroke={key.current || hot ? "var(--ink-1, #f5f5f5)" : "var(--accent, #0099ff)"}
|
|
1511
|
+
strokeWidth={hot ? 2 : 1.5}
|
|
1512
|
+
/>
|
|
1513
|
+
);
|
|
1514
|
+
})}
|
|
1515
|
+
</g>
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
/** The selected dot's Bezier handles (schema v40), in the spline editor's
|
|
1520
|
+
* look. A tip on its dot (a corner) is not drawn: the dot is what a press
|
|
1521
|
+
* there grabs, and `motionPathHandleAt` skips it by the same distance. */
|
|
1522
|
+
function PathKeyHandles({ dot, hot }: { dot: ScreenPathKey; hot: "in" | "out" | null }) {
|
|
1523
|
+
return (
|
|
1524
|
+
<g data-testid="motion-path-handles">
|
|
1525
|
+
{(["in", "out"] as const).map((part) => {
|
|
1526
|
+
const tip = dot[part];
|
|
1527
|
+
if (tip === undefined || Math.hypot(tip.x - dot.x, tip.y - dot.y) < PATH_HANDLE_MIN_PX) return null;
|
|
1528
|
+
return (
|
|
1529
|
+
<g key={part} data-testid={`motion-path-handle-${part}`}>
|
|
1530
|
+
<line
|
|
1531
|
+
x1={dot.x}
|
|
1532
|
+
y1={dot.y}
|
|
1533
|
+
x2={tip.x}
|
|
1534
|
+
y2={tip.y}
|
|
1535
|
+
stroke="var(--accent, #0099ff)"
|
|
1536
|
+
strokeWidth={1}
|
|
1537
|
+
opacity={0.64}
|
|
1538
|
+
/>
|
|
1539
|
+
<SplineHandle at={[tip.x, tip.y]} selected={hot === part} />
|
|
1540
|
+
</g>
|
|
1541
|
+
);
|
|
1542
|
+
})}
|
|
1543
|
+
</g>
|
|
1544
|
+
);
|
|
1545
|
+
}
|
|
1546
|
+
|
|
888
1547
|
function SelectionQuad({ quad, handles }: { quad: ScreenQuad; handles?: boolean }) {
|
|
889
1548
|
const points = pointsOf(quad);
|
|
890
1549
|
return (
|
|
891
1550
|
<>
|
|
892
|
-
<polygon points={points} fill="none" stroke="var(--accent, #
|
|
1551
|
+
<polygon points={points} fill="none" stroke="var(--accent, #0099ff)" strokeWidth={1.5} />
|
|
893
1552
|
{/* THE SAME TABLE `grabAt` HIT-TESTS, imported rather than restated: a
|
|
894
1553
|
drawn handle the hit test does not answer, or an answered one nobody
|
|
895
1554
|
drew, is the exact bug the hover cursor exists to make impossible. */}
|
|
@@ -904,8 +1563,8 @@ function SelectionQuad({ quad, handles }: { quad: ScreenQuad; handles?: boolean
|
|
|
904
1563
|
y={y - 3.5}
|
|
905
1564
|
width={7}
|
|
906
1565
|
height={7}
|
|
907
|
-
fill="var(--panel, #
|
|
908
|
-
stroke="var(--accent, #
|
|
1566
|
+
fill="var(--surface-panel, #111111)"
|
|
1567
|
+
stroke="var(--accent, #0099ff)"
|
|
909
1568
|
strokeWidth={1.5}
|
|
910
1569
|
/>
|
|
911
1570
|
);
|
|
@@ -934,7 +1593,7 @@ function UnionBox({ quad }: { quad: ScreenQuad }) {
|
|
|
934
1593
|
<polygon
|
|
935
1594
|
points={pointsOf(quad)}
|
|
936
1595
|
fill="none"
|
|
937
|
-
stroke="var(--accent, #
|
|
1596
|
+
stroke="var(--accent, #0099ff)"
|
|
938
1597
|
strokeWidth={1}
|
|
939
1598
|
strokeDasharray="4 3"
|
|
940
1599
|
/>
|
|
@@ -947,8 +1606,8 @@ function UnionBox({ quad }: { quad: ScreenQuad }) {
|
|
|
947
1606
|
y={y - 3.5}
|
|
948
1607
|
width={7}
|
|
949
1608
|
height={7}
|
|
950
|
-
fill="var(--panel, #
|
|
951
|
-
stroke="var(--accent, #
|
|
1609
|
+
fill="var(--surface-panel, #111111)"
|
|
1610
|
+
stroke="var(--accent, #0099ff)"
|
|
952
1611
|
strokeWidth={1.5}
|
|
953
1612
|
/>
|
|
954
1613
|
);
|
|
@@ -957,6 +1616,206 @@ function UnionBox({ quad }: { quad: ScreenQuad }) {
|
|
|
957
1616
|
);
|
|
958
1617
|
}
|
|
959
1618
|
|
|
1619
|
+
/** The comment pins on the frame, with what a pin click calls. */
|
|
1620
|
+
export interface CommentOverlay {
|
|
1621
|
+
pins: readonly ShotBoardPin[];
|
|
1622
|
+
onPin: (threadId: string) => void;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
/** A pin's outline (S5): a 28px teardrop, radii 14/14/14/3, the square
|
|
1626
|
+
* corner bottom-left, sitting ON the point it marks. */
|
|
1627
|
+
const PIN_PATH = "M0 -14 A14 14 0 0 1 14 -28 A14 14 0 0 1 28 -14 A14 14 0 0 1 14 0 H3 A3 3 0 0 1 0 -3 Z";
|
|
1628
|
+
|
|
1629
|
+
/** S5's ring around the open Studio pin's layer: dashed, 4px out. */
|
|
1630
|
+
const PIN_LAYER_OFFSET = 4;
|
|
1631
|
+
|
|
1632
|
+
/** `quad` grown by `d` px on every side: each edge pushed out along its
|
|
1633
|
+
* normal, corners where the pushed edges meet. Right for a rotated or
|
|
1634
|
+
* skewed layer, which an axis box is not. */
|
|
1635
|
+
export function outsetQuad(quad: ScreenQuad, d: number): ScreenQuad {
|
|
1636
|
+
const pts = [0, 1, 2, 3].map((i) => [quad[i * 2]!, quad[i * 2 + 1]!] as const);
|
|
1637
|
+
let area = 0;
|
|
1638
|
+
for (let i = 0; i < 4; i++) {
|
|
1639
|
+
const [x0, y0] = pts[i]!;
|
|
1640
|
+
const [x1, y1] = pts[(i + 1) % 4]!;
|
|
1641
|
+
area += x0 * y1 - x1 * y0;
|
|
1642
|
+
}
|
|
1643
|
+
const side = area >= 0 ? 1 : -1;
|
|
1644
|
+
const normal = (a: readonly [number, number], b: readonly [number, number]): [number, number] => {
|
|
1645
|
+
const dx = b[0] - a[0];
|
|
1646
|
+
const dy = b[1] - a[1];
|
|
1647
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
1648
|
+
return [(dy / len) * side, (-dx / len) * side];
|
|
1649
|
+
};
|
|
1650
|
+
const out: number[] = [];
|
|
1651
|
+
for (let i = 0; i < 4; i++) {
|
|
1652
|
+
const prev = pts[(i + 3) % 4]!;
|
|
1653
|
+
const here = pts[i]!;
|
|
1654
|
+
const next = pts[(i + 1) % 4]!;
|
|
1655
|
+
const n1 = normal(prev, here);
|
|
1656
|
+
const n2 = normal(here, next);
|
|
1657
|
+
const k = d / Math.max(0.2, 1 + n1[0] * n2[0] + n1[1] * n2[1]);
|
|
1658
|
+
out.push(here[0] + (n1[0] + n2[0]) * k, here[1] + (n1[1] + n2[1]) * k);
|
|
1659
|
+
}
|
|
1660
|
+
return out as unknown as ScreenQuad;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
/** The label chip's text size and the room it takes, CSS px (Paper SB6:
|
|
1664
|
+
* 10/14 mono on the accent, padding 1 / 5). */
|
|
1665
|
+
const GUIDE_CHIP_FONT = 10;
|
|
1666
|
+
const GUIDE_CHIP_H = 16;
|
|
1667
|
+
const GUIDE_CHIP_PAD = 5;
|
|
1668
|
+
/** A monospace glyph's advance at that size, to size a chip without
|
|
1669
|
+
* measuring text. */
|
|
1670
|
+
const GUIDE_CHIP_ADVANCE = 6.1;
|
|
1671
|
+
|
|
1672
|
+
/** A label chip's width for `label`: mono advance plus padding. */
|
|
1673
|
+
export function guideChipWidth(label: string): number {
|
|
1674
|
+
return Math.ceil(label.length * GUIDE_CHIP_ADVANCE) + GUIDE_CHIP_PAD * 2;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
function GuideChip({ x, y, label }: { x: number; y: number; label: string }) {
|
|
1678
|
+
return (
|
|
1679
|
+
<g transform={`translate(${x} ${y})`}>
|
|
1680
|
+
<rect width={guideChipWidth(label)} height={GUIDE_CHIP_H} rx={3} fill="var(--accent, #0099ff)" />
|
|
1681
|
+
<text
|
|
1682
|
+
x={GUIDE_CHIP_PAD}
|
|
1683
|
+
y={GUIDE_CHIP_H / 2}
|
|
1684
|
+
dominantBaseline="central"
|
|
1685
|
+
fill="var(--on-accent, #ffffff)"
|
|
1686
|
+
fontFamily="var(--font-numeric, ui-monospace, monospace)"
|
|
1687
|
+
fontSize={GUIDE_CHIP_FONT}
|
|
1688
|
+
>
|
|
1689
|
+
{label}
|
|
1690
|
+
</text>
|
|
1691
|
+
</g>
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
/**
|
|
1696
|
+
* THE SKETCH GUIDE (bridge v47): each sketch item as a 1px dashed accent
|
|
1697
|
+
* outline with its own corner, its label chip on the outline's top edge, and
|
|
1698
|
+
* each arrow as a dashed line with a head. Paint only.
|
|
1699
|
+
*/
|
|
1700
|
+
function SketchGuideOverlay({ guide }: { guide: ScreenSketchGuide }) {
|
|
1701
|
+
return (
|
|
1702
|
+
<g data-testid="sketch-guide" pointerEvents="none">
|
|
1703
|
+
{guide.rects.map((r, i) => (
|
|
1704
|
+
<g key={`r-${i}`}>
|
|
1705
|
+
<rect
|
|
1706
|
+
x={r.x}
|
|
1707
|
+
y={r.y}
|
|
1708
|
+
width={r.width}
|
|
1709
|
+
height={r.height}
|
|
1710
|
+
rx={r.radius}
|
|
1711
|
+
fill="none"
|
|
1712
|
+
stroke="var(--accent, #0099ff)"
|
|
1713
|
+
strokeWidth={1}
|
|
1714
|
+
strokeDasharray="6 4"
|
|
1715
|
+
/>
|
|
1716
|
+
{r.label === undefined ? null : <GuideChip x={r.x} y={r.y - GUIDE_CHIP_H - 2} label={r.label} />}
|
|
1717
|
+
</g>
|
|
1718
|
+
))}
|
|
1719
|
+
{guide.arrows.map((a, i) => {
|
|
1720
|
+
const len = Math.hypot(a.x2 - a.x1, a.y2 - a.y1) || 1;
|
|
1721
|
+
const ux = (a.x2 - a.x1) / len;
|
|
1722
|
+
const uy = (a.y2 - a.y1) / len;
|
|
1723
|
+
const head = `${a.x2},${a.y2} ${a.x2 - ux * 8 - uy * 4},${a.y2 - uy * 8 + ux * 4} ${a.x2 - ux * 8 + uy * 4},${a.y2 - uy * 8 - ux * 4}`;
|
|
1724
|
+
return (
|
|
1725
|
+
<g key={`a-${i}`}>
|
|
1726
|
+
<line x1={a.x1} y1={a.y1} x2={a.x2} y2={a.y2} stroke="var(--accent, #0099ff)" strokeWidth={1} strokeDasharray="6 4" />
|
|
1727
|
+
<polygon points={head} fill="var(--accent, #0099ff)" />
|
|
1728
|
+
{a.label === undefined ? null : (
|
|
1729
|
+
<GuideChip x={(a.x1 + a.x2) / 2 - guideChipWidth(a.label) / 2} y={(a.y1 + a.y2) / 2 - GUIDE_CHIP_H - 4} label={a.label} />
|
|
1730
|
+
)}
|
|
1731
|
+
</g>
|
|
1732
|
+
);
|
|
1733
|
+
})}
|
|
1734
|
+
</g>
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
/** One comment pin (bridge v42). The one thing on the frame's comment layer
|
|
1739
|
+
* that takes the pointer: the press is consumed, like the pilot badge's
|
|
1740
|
+
* eject, so it opens the thread and never reaches a pick under it. */
|
|
1741
|
+
function CommentPin({ pin, x, y, onPin }: { pin: ShotBoardPin; x: number; y: number; onPin: (threadId: string) => void }) {
|
|
1742
|
+
return (
|
|
1743
|
+
<g
|
|
1744
|
+
data-testid="comment-pin"
|
|
1745
|
+
data-thread={pin.threadId}
|
|
1746
|
+
role="button"
|
|
1747
|
+
aria-label={pin.label}
|
|
1748
|
+
transform={`translate(${x} ${y})`}
|
|
1749
|
+
pointerEvents="auto"
|
|
1750
|
+
style={{ cursor: "pointer" }}
|
|
1751
|
+
onPointerDown={(e) => {
|
|
1752
|
+
e.stopPropagation();
|
|
1753
|
+
e.preventDefault();
|
|
1754
|
+
onPin(pin.threadId);
|
|
1755
|
+
}}
|
|
1756
|
+
>
|
|
1757
|
+
<title>{pin.label}</title>
|
|
1758
|
+
{pin.active ? (
|
|
1759
|
+
<path d={PIN_PATH} fill="none" stroke="var(--accent, #0099ff)" strokeOpacity={0.35} strokeWidth={12} />
|
|
1760
|
+
) : null}
|
|
1761
|
+
{/* S5: the accent on a 2px white border, so a pin never reads white
|
|
1762
|
+
on a white picture. */}
|
|
1763
|
+
<path d={PIN_PATH} fill="var(--accent, #0099ff)" stroke="#ffffff" strokeWidth={2} />
|
|
1764
|
+
<text
|
|
1765
|
+
x={14}
|
|
1766
|
+
y={-9.5}
|
|
1767
|
+
textAnchor="middle"
|
|
1768
|
+
fill="var(--on-accent, #ffffff)"
|
|
1769
|
+
fontFamily="var(--font-sans, system-ui)"
|
|
1770
|
+
fontSize={12}
|
|
1771
|
+
fontWeight={600}
|
|
1772
|
+
>
|
|
1773
|
+
{pin.initials}
|
|
1774
|
+
</text>
|
|
1775
|
+
</g>
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
/**
|
|
1780
|
+
* The Studio's comment layer over the camera frame (bridge v45): the open
|
|
1781
|
+
* pin's layer ringed dashed, and each pin at its `u, v` of the frame rect.
|
|
1782
|
+
* ALL PAINT but the pins: the root takes no pointer events and only a pin
|
|
1783
|
+
* opts back in, so every other click still lands on the layer under it.
|
|
1784
|
+
*/
|
|
1785
|
+
function CommentPinsOverlay({
|
|
1786
|
+
comments,
|
|
1787
|
+
pinLayer,
|
|
1788
|
+
frame,
|
|
1789
|
+
}: {
|
|
1790
|
+
comments: CommentOverlay;
|
|
1791
|
+
pinLayer: ScreenQuad | null;
|
|
1792
|
+
frame: OverlayProps["frame"];
|
|
1793
|
+
}) {
|
|
1794
|
+
return (
|
|
1795
|
+
<g data-testid="comment-pins" pointerEvents="none">
|
|
1796
|
+
{pinLayer === null ? null : (
|
|
1797
|
+
<polygon
|
|
1798
|
+
data-testid="studio-pin-layer"
|
|
1799
|
+
points={pointsOf(outsetQuad(pinLayer, PIN_LAYER_OFFSET))}
|
|
1800
|
+
fill="none"
|
|
1801
|
+
stroke="var(--accent, #0099ff)"
|
|
1802
|
+
strokeWidth={1.5}
|
|
1803
|
+
strokeDasharray="4 3"
|
|
1804
|
+
/>
|
|
1805
|
+
)}
|
|
1806
|
+
{comments.pins.map((pin) => (
|
|
1807
|
+
<CommentPin
|
|
1808
|
+
key={pin.threadId}
|
|
1809
|
+
pin={pin}
|
|
1810
|
+
x={frame.x + pin.u * frame.width}
|
|
1811
|
+
y={frame.y + pin.v * frame.height}
|
|
1812
|
+
onPin={comments.onPin}
|
|
1813
|
+
/>
|
|
1814
|
+
))}
|
|
1815
|
+
</g>
|
|
1816
|
+
);
|
|
1817
|
+
}
|
|
1818
|
+
|
|
960
1819
|
/**
|
|
961
1820
|
* The path as it is being drawn, through THE SAME PRINTER the committed layer
|
|
962
1821
|
* uses.
|
|
@@ -980,7 +1839,7 @@ function PenPreview({ pen }: { pen: ScreenPen }) {
|
|
|
980
1839
|
return (
|
|
981
1840
|
<g pointerEvents="none">
|
|
982
1841
|
{points.length >= 2 ? (
|
|
983
|
-
<path d={pathToD({ points: [...points], closed: pen.closed })} fill="none" stroke="var(--accent, #
|
|
1842
|
+
<path d={pathToD({ points: [...points], closed: pen.closed })} fill="none" stroke="var(--accent, #0099ff)" strokeWidth={1.5} />
|
|
984
1843
|
) : null}
|
|
985
1844
|
{/* The rubber band to the cursor, drawn as the segment it WOULD become:
|
|
986
1845
|
curved when the anchor behind it carries an outgoing handle. */}
|
|
@@ -988,7 +1847,7 @@ function PenPreview({ pen }: { pen: ScreenPen }) {
|
|
|
988
1847
|
<path
|
|
989
1848
|
d={pathToD({ points: [last, { p: [cursor[0], cursor[1]] }], closed: false })}
|
|
990
1849
|
fill="none"
|
|
991
|
-
stroke="var(--accent, #
|
|
1850
|
+
stroke="var(--accent, #0099ff)"
|
|
992
1851
|
strokeWidth={1}
|
|
993
1852
|
strokeDasharray="4 3"
|
|
994
1853
|
opacity={0.7}
|
|
@@ -998,10 +1857,10 @@ function PenPreview({ pen }: { pen: ScreenPen }) {
|
|
|
998
1857
|
{points.map((p, i) => (
|
|
999
1858
|
<g key={`h${i}`}>
|
|
1000
1859
|
{p.out === undefined ? null : (
|
|
1001
|
-
<line x1={p.p[0]} y1={p.p[1]} x2={p.out[0]} y2={p.out[1]} stroke="var(--accent, #
|
|
1860
|
+
<line x1={p.p[0]} y1={p.p[1]} x2={p.out[0]} y2={p.out[1]} stroke="var(--accent, #0099ff)" strokeWidth={1} opacity={0.6} />
|
|
1002
1861
|
)}
|
|
1003
1862
|
{p.in === undefined ? null : (
|
|
1004
|
-
<line x1={p.p[0]} y1={p.p[1]} x2={p.in[0]} y2={p.in[1]} stroke="var(--accent, #
|
|
1863
|
+
<line x1={p.p[0]} y1={p.p[1]} x2={p.in[0]} y2={p.in[1]} stroke="var(--accent, #0099ff)" strokeWidth={1} opacity={0.6} />
|
|
1005
1864
|
)}
|
|
1006
1865
|
</g>
|
|
1007
1866
|
))}
|
|
@@ -1013,8 +1872,8 @@ function PenPreview({ pen }: { pen: ScreenPen }) {
|
|
|
1013
1872
|
r={i === 0 && pen.closeHot ? 5 : 3}
|
|
1014
1873
|
// A FILLED first anchor is the whole close affordance: the author
|
|
1015
1874
|
// needs to know the next click ends the path before they make it.
|
|
1016
|
-
fill={i === 0 && pen.closeHot ? "var(--accent, #
|
|
1017
|
-
stroke="var(--accent, #
|
|
1875
|
+
fill={i === 0 && pen.closeHot ? "var(--accent, #0099ff)" : "var(--surface-panel, #111111)"}
|
|
1876
|
+
stroke="var(--accent, #0099ff)"
|
|
1018
1877
|
strokeWidth={1.5}
|
|
1019
1878
|
/>
|
|
1020
1879
|
))}
|
|
@@ -1022,6 +1881,100 @@ function PenPreview({ pen }: { pen: ScreenPen }) {
|
|
|
1022
1881
|
);
|
|
1023
1882
|
}
|
|
1024
1883
|
|
|
1884
|
+
function SplinePreview({ spline }: { spline: ScreenSpline }) {
|
|
1885
|
+
const last = spline.points[spline.points.length - 1];
|
|
1886
|
+
return (
|
|
1887
|
+
<g pointerEvents="none">
|
|
1888
|
+
{spline.line.length >= 2 ? (
|
|
1889
|
+
<polyline
|
|
1890
|
+
points={spline.line.map((point) => `${point[0]},${point[1]}`).join(" ")}
|
|
1891
|
+
fill="none"
|
|
1892
|
+
stroke="var(--accent, #0099ff)"
|
|
1893
|
+
strokeWidth={1.5}
|
|
1894
|
+
strokeLinejoin="round"
|
|
1895
|
+
/>
|
|
1896
|
+
) : null}
|
|
1897
|
+
{!spline.editing && last !== undefined && spline.cursor !== null && !spline.closed ? (
|
|
1898
|
+
<line
|
|
1899
|
+
x1={last.p[0]}
|
|
1900
|
+
y1={last.p[1]}
|
|
1901
|
+
x2={spline.cursor[0]}
|
|
1902
|
+
y2={spline.cursor[1]}
|
|
1903
|
+
stroke="var(--accent, #0099ff)"
|
|
1904
|
+
strokeWidth={1}
|
|
1905
|
+
strokeDasharray="4 4"
|
|
1906
|
+
opacity={0.72}
|
|
1907
|
+
/>
|
|
1908
|
+
) : null}
|
|
1909
|
+
{spline.points.map((point) => (
|
|
1910
|
+
<g key={`arms-${point.id}`}>
|
|
1911
|
+
{point.in === undefined ? null : (
|
|
1912
|
+
<line
|
|
1913
|
+
x1={point.p[0]}
|
|
1914
|
+
y1={point.p[1]}
|
|
1915
|
+
x2={point.in[0]}
|
|
1916
|
+
y2={point.in[1]}
|
|
1917
|
+
stroke="var(--accent, #0099ff)"
|
|
1918
|
+
strokeWidth={1}
|
|
1919
|
+
opacity={0.64}
|
|
1920
|
+
/>
|
|
1921
|
+
)}
|
|
1922
|
+
{point.out === undefined ? null : (
|
|
1923
|
+
<line
|
|
1924
|
+
x1={point.p[0]}
|
|
1925
|
+
y1={point.p[1]}
|
|
1926
|
+
x2={point.out[0]}
|
|
1927
|
+
y2={point.out[1]}
|
|
1928
|
+
stroke="var(--accent, #0099ff)"
|
|
1929
|
+
strokeWidth={1}
|
|
1930
|
+
opacity={0.64}
|
|
1931
|
+
/>
|
|
1932
|
+
)}
|
|
1933
|
+
{point.in === undefined ? null : <SplineHandle at={point.in} selected={point.selected} />}
|
|
1934
|
+
{point.out === undefined ? null : <SplineHandle at={point.out} selected={point.selected} />}
|
|
1935
|
+
</g>
|
|
1936
|
+
))}
|
|
1937
|
+
{spline.points.map((point, index) => (
|
|
1938
|
+
<circle
|
|
1939
|
+
key={point.id}
|
|
1940
|
+
cx={point.p[0]}
|
|
1941
|
+
cy={point.p[1]}
|
|
1942
|
+
r={!spline.editing && index === 0 && spline.closeHot ? 5 : point.selected ? 4 : 3.5}
|
|
1943
|
+
fill={
|
|
1944
|
+
(!spline.editing && index === 0 && spline.closeHot) || point.selected
|
|
1945
|
+
? "var(--accent, #0099ff)"
|
|
1946
|
+
: "var(--surface-panel, #111111)"
|
|
1947
|
+
}
|
|
1948
|
+
stroke="var(--accent, #0099ff)"
|
|
1949
|
+
strokeWidth={1.5}
|
|
1950
|
+
/>
|
|
1951
|
+
))}
|
|
1952
|
+
</g>
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
function flatPolyline(points: readonly number[]): string {
|
|
1957
|
+
const out: string[] = [];
|
|
1958
|
+
for (let index = 0; index + 1 < points.length; index += 2) {
|
|
1959
|
+
out.push(`${points[index]},${points[index + 1]}`);
|
|
1960
|
+
}
|
|
1961
|
+
return out.join(" ");
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
function SplineHandle({ at, selected }: { at: readonly [number, number]; selected: boolean }) {
|
|
1965
|
+
return (
|
|
1966
|
+
<rect
|
|
1967
|
+
x={at[0] - 3}
|
|
1968
|
+
y={at[1] - 3}
|
|
1969
|
+
width={6}
|
|
1970
|
+
height={6}
|
|
1971
|
+
fill={selected ? "var(--accent, #0099ff)" : "var(--surface-panel, #111111)"}
|
|
1972
|
+
stroke="var(--accent, #0099ff)"
|
|
1973
|
+
strokeWidth={1.25}
|
|
1974
|
+
/>
|
|
1975
|
+
);
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1025
1978
|
function Marquee({ rect }: { rect: ScreenMarquee }) {
|
|
1026
1979
|
const x = Math.min(rect.x0, rect.x1);
|
|
1027
1980
|
const y = Math.min(rect.y0, rect.y1);
|
|
@@ -1031,9 +1984,9 @@ function Marquee({ rect }: { rect: ScreenMarquee }) {
|
|
|
1031
1984
|
y={y}
|
|
1032
1985
|
width={Math.abs(rect.x1 - rect.x0)}
|
|
1033
1986
|
height={Math.abs(rect.y1 - rect.y0)}
|
|
1034
|
-
fill="var(--accent, #
|
|
1987
|
+
fill="var(--accent, #0099ff)"
|
|
1035
1988
|
fillOpacity={0.12}
|
|
1036
|
-
stroke="var(--accent, #
|
|
1989
|
+
stroke="var(--accent, #0099ff)"
|
|
1037
1990
|
strokeWidth={1}
|
|
1038
1991
|
/>
|
|
1039
1992
|
);
|