@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
|
@@ -8,47 +8,152 @@
|
|
|
8
8
|
// first section that shows every field is a section nobody can find the model
|
|
9
9
|
// in.
|
|
10
10
|
//
|
|
11
|
+
// ── THE MATERIAL'S OWN PARAMS ARE HERE, BECAUSE THEY WERE NOWHERE ───────
|
|
12
|
+
//
|
|
13
|
+
// The picker is a browse-and-apply grid with no controls in it, so a generator's
|
|
14
|
+
// params were fixed at its contract defaults the moment it was applied and were
|
|
15
|
+
// then unreachable from the entire chrome. On a glow generator that means the
|
|
16
|
+
// layer's COLOUR: the author could see it and could not touch it.
|
|
17
|
+
//
|
|
18
|
+
// The rows are built from the generator's CONTRACT through `PropRow`, the one
|
|
19
|
+
// row every contract's props render in (a placed component's, a definition's,
|
|
20
|
+
// Components' "this use"), so a colour param gets the colour field and a
|
|
21
|
+
// bounded number its slider for exactly the reason they do there. Never from
|
|
22
|
+
// the stored params: those hold only what somebody already changed, with no
|
|
23
|
+
// kinds, no ranges and no labels. A param somebody changed carries the row's
|
|
24
|
+
// reset, which deletes it back to the contract's own default.
|
|
25
|
+
//
|
|
26
|
+
// ── EVERY NUMBER DRAGS LIVE, AND EVERY ROW READS ITS OWN MIXED ──────────
|
|
27
|
+
//
|
|
28
|
+
// A number row is `MeshNumber`: its commit and its drag are ONE builder over
|
|
29
|
+
// each mesh layer's own block, so the stage moves while the pointer is down and
|
|
30
|
+
// a drag's release lands the document its moves did. Mixed is asked per field,
|
|
31
|
+
// over the value the field SHOWS (a shape's depth is the derived min(w, h) when
|
|
32
|
+
// it stores none, so two shapes of different sizes disagree on it).
|
|
33
|
+
//
|
|
11
34
|
// ponytail: add the toon detail rows when someone is actually art-directing
|
|
12
35
|
// one.
|
|
36
|
+
import { useEffect, useMemo } from "react";
|
|
37
|
+
import type { ReactNode } from "react";
|
|
13
38
|
import {
|
|
39
|
+
ColorField,
|
|
14
40
|
Combobox,
|
|
41
|
+
IconClose,
|
|
15
42
|
InspectorRow,
|
|
16
43
|
InspectorSection,
|
|
17
44
|
NumberField,
|
|
45
|
+
PropRow,
|
|
18
46
|
SegmentedControl,
|
|
19
47
|
TextField,
|
|
48
|
+
Toggle,
|
|
49
|
+
useLocked,
|
|
50
|
+
wordOptions,
|
|
20
51
|
} from "@dailies-studio/ui";
|
|
21
|
-
import {
|
|
22
|
-
import type {
|
|
52
|
+
import { orderSpecs } from "@dailies-studio/contract";
|
|
53
|
+
import type { ControlSpec } from "@dailies-studio/contract";
|
|
54
|
+
import { KeyframeDot } from "../KeyframeDot.js";
|
|
55
|
+
import {
|
|
56
|
+
DEFAULT_PRIMITIVE_COLOR,
|
|
57
|
+
DEFAULT_SPLINE_CAPS,
|
|
58
|
+
DEFAULT_SPLINE_UP,
|
|
59
|
+
MAX_SPLINE_ANGLE_DEG,
|
|
60
|
+
MAX_SPLINE_COORDINATE,
|
|
61
|
+
MAX_SPLINE_SCALE,
|
|
62
|
+
MEDIA_LOOPS,
|
|
63
|
+
MESH_PRIMITIVE_KINDS,
|
|
64
|
+
MESH_SHADING_MODES,
|
|
65
|
+
isGraphMaterial,
|
|
66
|
+
meshPrimitiveOf,
|
|
67
|
+
primitiveDepthOf,
|
|
68
|
+
splineVisibleRange,
|
|
69
|
+
// The path table's bounds, read by `MeshNumber` below instead of restated
|
|
70
|
+
// here: `numberRange` is what the keyframe gate clamps against, so a second
|
|
71
|
+
// copy in the chrome is a pill that offers a number the diamond then refuses.
|
|
72
|
+
trackablePath,
|
|
73
|
+
} from "@dailies-studio/shot/doc";
|
|
74
|
+
import type {
|
|
75
|
+
Layer3D,
|
|
76
|
+
MeshLayer3D,
|
|
77
|
+
MediaLoop,
|
|
78
|
+
MeshMaterialParam,
|
|
79
|
+
MeshPrimitive,
|
|
80
|
+
MeshPrimitiveKind,
|
|
81
|
+
MeshShading,
|
|
82
|
+
SplineProfile,
|
|
83
|
+
SplinePrimitive,
|
|
84
|
+
} from "@dailies-studio/shot/doc";
|
|
23
85
|
import { useShotEditor, useStore } from "../../../state/context.js";
|
|
24
|
-
import { useSelectedLayers,
|
|
86
|
+
import { useSelectedLayers, useSelectionScrub, useSelectionWrite } from "../useDocWrite.js";
|
|
87
|
+
import { primitiveForKind } from "../../../state/insert.js";
|
|
25
88
|
|
|
26
89
|
type MeshBlock = MeshLayer3D["mesh"];
|
|
27
90
|
|
|
91
|
+
/** A widget with no `mixed` of its own shows its Mixed this way: a segmented
|
|
92
|
+
* control and a combobox both read an empty value as "none of these", and a
|
|
93
|
+
* combobox shows its placeholder over it. */
|
|
94
|
+
const NO_VALUE = "";
|
|
95
|
+
const MIXED = "Mixed";
|
|
96
|
+
|
|
97
|
+
/** The two parts of a spline profile's offset, one vec2 track keyed together. */
|
|
98
|
+
const OFFSET_PARTS = ["Offset X", "Offset Y"] as const;
|
|
99
|
+
|
|
28
100
|
export function MeshSection() {
|
|
29
101
|
const write = useSelectionWrite();
|
|
30
102
|
const layers = useSelectedLayers();
|
|
31
103
|
const store = useStore();
|
|
32
104
|
const doc = useShotEditor((s) => s.doc);
|
|
33
105
|
const library = useShotEditor((s) => s.library);
|
|
34
|
-
const
|
|
35
|
-
|
|
106
|
+
const contracts = useShotEditor((s) => s.contracts);
|
|
107
|
+
const splineEdit = useShotEditor((s) => s.splineEdit);
|
|
108
|
+
// A VIEWER'S PANEL LOCKS EVERY VERB THAT WRITES. The widgets read the lock
|
|
109
|
+
// themselves; this section's own buttons are plain ones, so they read it
|
|
110
|
+
// here. Opening the node editor only navigates, and stays live.
|
|
111
|
+
const locked = useLocked();
|
|
112
|
+
// THE MESH LAYERS THEMSELVES, not `useSelectionValue`. That hook compares
|
|
113
|
+
// members with `Object.is`, and `layer.mesh` is an object — so every
|
|
114
|
+
// multi-selection was "Mixed" by identity, over values the author could see
|
|
115
|
+
// agree, and the two number fields wore that placeholder permanently. Mixed is
|
|
116
|
+
// asked per FIELD below instead.
|
|
117
|
+
const meshes = layers.filter((l): l is MeshLayer3D => l.kind === "mesh");
|
|
118
|
+
const m = meshes[0]?.mesh;
|
|
119
|
+
// Pushed up by the stage as the model parses — the chrome is three-free and
|
|
120
|
+
// cannot read a glTF's material list itself. Empty for a primitive and for a
|
|
121
|
+
// model that has not loaded yet.
|
|
122
|
+
const slots = useShotEditor((s) => (meshes[0] === undefined ? undefined : s.meshMaterialSlots[meshes[0].id])) ?? [];
|
|
123
|
+
const legacyMaterial = m?.material === undefined || isGraphMaterial(m.material) ? undefined : m.material;
|
|
124
|
+
const graphBinding = m?.material !== undefined && isGraphMaterial(m.material) ? m.material : undefined;
|
|
125
|
+
const graph = graphBinding === undefined ? undefined : doc?.materials?.[graphBinding.materialId];
|
|
126
|
+
const generator = legacyMaterial?.generator;
|
|
127
|
+
// Fetched the moment a material is ON SCREEN rather than when a row asks for
|
|
128
|
+
// it: the rows ARE the contract, so there is nothing to render until it is
|
|
129
|
+
// here. Cached in the store, so re-selecting the same layer costs no request.
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (generator !== undefined) void store.loadContract(generator);
|
|
132
|
+
}, [store, generator]);
|
|
133
|
+
// Model assets only. Offering an image would let an author point a mesh layer
|
|
134
|
+
// at a PNG, which `validateShotDoc` refuses on save — a menu that can only
|
|
135
|
+
// produce an invalid document is worse than a short menu. Memoised on the
|
|
136
|
+
// asset table: the section repaints on every document identity change (once
|
|
137
|
+
// per rAF frame of any drag), and the table itself changes only on an upload.
|
|
138
|
+
const assets = doc?.assets;
|
|
139
|
+
const models = useMemo(
|
|
140
|
+
() =>
|
|
141
|
+
Object.entries(assets ?? {})
|
|
142
|
+
.filter(([, asset]) => asset.kind === "model")
|
|
143
|
+
.map(([id, asset]) => ({ value: id, label: asset.path.split("/").pop() ?? id })),
|
|
144
|
+
[assets],
|
|
145
|
+
);
|
|
146
|
+
if (m === undefined) return null;
|
|
36
147
|
|
|
37
|
-
const m = mesh.value;
|
|
38
148
|
// The catalog's display name, or the raw id when the library has not loaded
|
|
39
149
|
// (or when the document names a generator this build does not ship). Showing
|
|
40
150
|
// the id rather than "Choose…" is what makes the second case visible at all —
|
|
41
151
|
// a material that silently reads as unset is one an author would set twice.
|
|
42
|
-
const materialName =
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
// at a PNG, which `validateShotDoc` refuses on save — a menu that can only
|
|
48
|
-
// produce an invalid document is worse than a short menu.
|
|
49
|
-
const models = Object.entries(doc?.assets ?? {})
|
|
50
|
-
.filter(([, asset]) => asset.kind === "model")
|
|
51
|
-
.map(([id, asset]) => ({ value: id, label: asset.path.split("/").pop() ?? id }));
|
|
152
|
+
const materialName = graph?.name ?? (
|
|
153
|
+
generator === undefined ? null : (library.find((row) => row.id === generator)?.name ?? generator)
|
|
154
|
+
);
|
|
155
|
+
const materialContract = generator === undefined ? undefined : contracts[generator];
|
|
156
|
+
const meshIds = meshes.map((l) => l.id);
|
|
52
157
|
|
|
53
158
|
/** Every write goes through one mapper so a field that CLEARS itself deletes
|
|
54
159
|
* its key rather than setting it to `undefined`: codegen prints the block by
|
|
@@ -59,58 +164,273 @@ export function MeshSection() {
|
|
|
59
164
|
const set = <K extends keyof MeshBlock>(key: K, value: MeshBlock[K]): void =>
|
|
60
165
|
patch((block) => ({ ...block, [key]: value }));
|
|
61
166
|
|
|
167
|
+
/** Does the selection disagree about ONE field? A scalar read per row, which
|
|
168
|
+
* is the only shape `Object.is` answers honestly. */
|
|
169
|
+
const mixedOn = <T,>(read: (block: MeshBlock) => T): boolean =>
|
|
170
|
+
meshes.some((l) => !Object.is(read(l.mesh), read(m)));
|
|
171
|
+
|
|
172
|
+
/** ONE primitive field, edited on each layer's OWN base primitive and only
|
|
173
|
+
* on layers of that kind (`withPrim`). */
|
|
174
|
+
const setPrim = <K extends MeshPrimitive["kind"]>(
|
|
175
|
+
kind: K,
|
|
176
|
+
edit: (p: Extract<MeshPrimitive, { kind: K }>) => MeshPrimitive,
|
|
177
|
+
): void => patch((block) => withPrim(block, kind, edit));
|
|
178
|
+
const primMixed = <K extends MeshPrimitive["kind"]>(
|
|
179
|
+
kind: K,
|
|
180
|
+
read: (p: Extract<MeshPrimitive, { kind: K }>) => unknown,
|
|
181
|
+
): boolean =>
|
|
182
|
+
mixedOn((block) => {
|
|
183
|
+
const p = meshPrimitiveOf(block);
|
|
184
|
+
return p?.kind === kind ? read(p as Extract<MeshPrimitive, { kind: K }>) : undefined;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// A SHAPE section and a MODEL section are one component with one gate: the
|
|
188
|
+
// schema says a mesh is exactly one of the two, so every row below asks
|
|
189
|
+
// which it is dressing. The clip rows hide for a shape (asset-only, inert),
|
|
190
|
+
// and the shape rows hide for a model.
|
|
191
|
+
const primitive = meshPrimitiveOf(m);
|
|
192
|
+
const shapeLayerBox = meshes[0]?.transform.size ?? [300, 300];
|
|
193
|
+
// The depth each layer DRAWS (absent is min(w, h) of its own box), which is
|
|
194
|
+
// both what the field shows and what Mixed is asked of.
|
|
195
|
+
const depthMixed = meshes.some(
|
|
196
|
+
(l) => primitiveDepthOf(l.mesh, l.transform.size) !== primitiveDepthOf(m, shapeLayerBox),
|
|
197
|
+
);
|
|
198
|
+
const kindMixed = mixedOn((block) => meshPrimitiveOf(block)?.kind);
|
|
199
|
+
const shadingMixed = mixedOn((block) => block.shading?.mode ?? "unlit");
|
|
200
|
+
|
|
62
201
|
return (
|
|
63
|
-
<InspectorSection title="Model" persistKey="shot:mesh">
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
202
|
+
<InspectorSection className="insp-object" title={primitive !== null ? "Shape" : "Model"} persistKey="shot:mesh">
|
|
203
|
+
{primitive !== null ? (
|
|
204
|
+
<>
|
|
205
|
+
<InspectorRow label="Shape">
|
|
206
|
+
<Combobox
|
|
207
|
+
options={MESH_PRIMITIVE_KINDS.map((kind) => ({
|
|
208
|
+
value: kind,
|
|
209
|
+
label: kind.charAt(0).toUpperCase() + kind.slice(1),
|
|
210
|
+
}))}
|
|
211
|
+
value={kindMixed ? NO_VALUE : primitive.kind}
|
|
212
|
+
ariaLabel="Shape kind"
|
|
213
|
+
placeholder={MIXED}
|
|
214
|
+
// A FRESH record on a kind switch: per-shape params are the old
|
|
215
|
+
// shape's silhouette, and a torus's arc smuggled into a box is a
|
|
216
|
+
// stale key the author cannot see.
|
|
217
|
+
onChange={(next) => set("primitive", primitiveForKind(next as MeshPrimitiveKind))}
|
|
218
|
+
/>
|
|
219
|
+
</InspectorRow>
|
|
220
|
+
<ShapeColorRow
|
|
221
|
+
value={m.color ?? DEFAULT_PRIMITIVE_COLOR}
|
|
222
|
+
mixed={mixedOn((block) => (block.color ?? DEFAULT_PRIMITIVE_COLOR).toLowerCase())}
|
|
223
|
+
/>
|
|
224
|
+
{primitive.kind === "plane" || primitive.kind === "spline" ? null : (
|
|
225
|
+
<MeshNumber
|
|
226
|
+
label="Depth"
|
|
227
|
+
ariaLabel="Shape depth"
|
|
228
|
+
// The EFFECTIVE depth: absent means min(w, h), and showing that
|
|
229
|
+
// number is what makes the default legible instead of a blank.
|
|
230
|
+
value={primitiveDepthOf(m, shapeLayerBox)}
|
|
231
|
+
mixed={depthMixed}
|
|
232
|
+
unit="px"
|
|
233
|
+
min={1}
|
|
234
|
+
step={1}
|
|
235
|
+
path="mesh.depthPx"
|
|
236
|
+
edit={(block, next) => ({ ...block, depthPx: Math.round(next) })}
|
|
237
|
+
/>
|
|
238
|
+
)}
|
|
239
|
+
{primitive.kind === "box" ? (
|
|
240
|
+
<MeshNumber
|
|
241
|
+
label="Corner"
|
|
242
|
+
ariaLabel="Corner radius"
|
|
243
|
+
value={primitive.cornerPx ?? 0}
|
|
244
|
+
mixed={primMixed("box", (p) => p.cornerPx ?? 0)}
|
|
245
|
+
unit="px"
|
|
246
|
+
min={0}
|
|
247
|
+
// No `max`: primitive.ts clamps the BUILT bevel to half the
|
|
248
|
+
// smallest dimension and keeps the authored number.
|
|
249
|
+
step={1}
|
|
250
|
+
path="mesh.primitive.cornerPx"
|
|
251
|
+
// 3D edge rounding on the BODY, deliberately not paint.radius
|
|
252
|
+
// (paint is inert on every mesh).
|
|
253
|
+
edit={(block, next) => withPrim(block, "box", (p) => ({ ...p, cornerPx: Math.round(next) }))}
|
|
254
|
+
/>
|
|
255
|
+
) : null}
|
|
256
|
+
{primitive.kind === "torus" ? (
|
|
257
|
+
<>
|
|
258
|
+
<MeshNumber
|
|
259
|
+
label="Tube"
|
|
260
|
+
ariaLabel="Tube ratio"
|
|
261
|
+
value={primitive.tubeRatio ?? 0.3}
|
|
262
|
+
mixed={primMixed("torus", (p) => p.tubeRatio ?? 0.3)}
|
|
263
|
+
min={0.01}
|
|
264
|
+
max={0.99}
|
|
265
|
+
step={0.01}
|
|
266
|
+
path="mesh.primitive.tubeRatio"
|
|
267
|
+
edit={(block, next) => withPrim(block, "torus", (p) => ({ ...p, tubeRatio: next }))}
|
|
268
|
+
/>
|
|
269
|
+
{/* THE PROGRESS-RING SWEEP: keyframing this is how a ring
|
|
270
|
+
fills. */}
|
|
271
|
+
<MeshNumber
|
|
272
|
+
label="Arc"
|
|
273
|
+
ariaLabel="Arc degrees"
|
|
274
|
+
value={primitive.arcDeg ?? 360}
|
|
275
|
+
mixed={primMixed("torus", (p) => p.arcDeg ?? 360)}
|
|
276
|
+
unit="°"
|
|
277
|
+
min={1}
|
|
278
|
+
max={360}
|
|
279
|
+
step={1}
|
|
280
|
+
path="mesh.primitive.arcDeg"
|
|
281
|
+
edit={(block, next) => withPrim(block, "torus", (p) => ({ ...p, arcDeg: Math.round(next) }))}
|
|
282
|
+
/>
|
|
283
|
+
</>
|
|
284
|
+
) : null}
|
|
285
|
+
{primitive.kind === "spline" ? (
|
|
286
|
+
<SplineRows
|
|
287
|
+
primitive={primitive}
|
|
288
|
+
hasTaperTracks={meshes.some(
|
|
289
|
+
(layer) =>
|
|
290
|
+
layer.tracks?.["mesh.primitive.startScale"] !== undefined ||
|
|
291
|
+
layer.tracks?.["mesh.primitive.endScale"] !== undefined,
|
|
292
|
+
)}
|
|
293
|
+
hasTwistTrack={meshes.some(
|
|
294
|
+
(layer) => layer.tracks?.["mesh.primitive.twistDeg"] !== undefined,
|
|
295
|
+
)}
|
|
296
|
+
editing={meshes.length === 1 && splineEdit?.layerId === meshes[0]!.id}
|
|
297
|
+
locked={locked}
|
|
298
|
+
onEdit={() =>
|
|
299
|
+
store.setSplineEditContext(
|
|
300
|
+
meshes.length !== 1 || splineEdit?.layerId === meshes[0]!.id
|
|
301
|
+
? null
|
|
302
|
+
: { layerId: meshes[0]!.id, selectedPointIds: [] },
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
onChange={(edit) => setPrim("spline", edit)}
|
|
306
|
+
mixed={(read) => primMixed("spline", read)}
|
|
307
|
+
onRefusal={(message) => store.reportError(message)}
|
|
308
|
+
/>
|
|
309
|
+
) : null}
|
|
310
|
+
</>
|
|
311
|
+
) : (
|
|
312
|
+
<InspectorRow label="Asset">
|
|
313
|
+
<Combobox
|
|
314
|
+
options={models}
|
|
315
|
+
value={mixedOn((block) => block.asset ?? "") ? NO_VALUE : (m.asset ?? "")}
|
|
316
|
+
ariaLabel="Model asset"
|
|
317
|
+
placeholder={mixedOn((block) => block.asset ?? "") ? MIXED : "Search models"}
|
|
318
|
+
onChange={(next) => set("asset", next)}
|
|
319
|
+
/>
|
|
320
|
+
</InspectorRow>
|
|
321
|
+
)}
|
|
73
322
|
|
|
74
|
-
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
323
|
+
{graph !== undefined && graph.model !== "studio" ? null : (
|
|
324
|
+
<InspectorRow label="Shading">
|
|
325
|
+
<SegmentedControl
|
|
326
|
+
ariaLabel="Mesh shading"
|
|
327
|
+
// A disagreeing selection selects NOTHING rather than the lead's
|
|
328
|
+
// mode: an unmatched value is how a segmented control says Mixed.
|
|
329
|
+
value={shadingMixed ? NO_VALUE : (m.shading?.mode ?? "unlit")}
|
|
330
|
+
options={wordOptions(MESH_SHADING_MODES)}
|
|
331
|
+
onChange={(next) => set("shading", { mode: next as MeshShading["mode"] } as MeshShading)}
|
|
332
|
+
/>
|
|
333
|
+
</InspectorRow>
|
|
334
|
+
)}
|
|
82
335
|
|
|
83
|
-
{/*
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
type="button"
|
|
93
|
-
className="mesh-material-name"
|
|
94
|
-
onClick={() => store.openMaterialPicker(layers.filter((l) => l.kind === "mesh").map((l) => l.id))}
|
|
95
|
-
>
|
|
96
|
-
{materialName ?? "Choose…"}
|
|
97
|
-
</button>
|
|
98
|
-
{m.material === undefined ? null : (
|
|
336
|
+
{/* New authoring enters the graph workspace. Legacy catalog bindings stay
|
|
337
|
+
readable below, but the old picker is intentionally no longer the
|
|
338
|
+
primary creation surface. The remove is a verb INSIDE the row's box,
|
|
339
|
+
the house accessory, so the row is as wide with a material as
|
|
340
|
+
without one. */}
|
|
341
|
+
<InspectorRow
|
|
342
|
+
label="Material"
|
|
343
|
+
trailing={
|
|
344
|
+
m.material === undefined ? undefined : (
|
|
99
345
|
<button
|
|
100
346
|
type="button"
|
|
101
|
-
className="
|
|
347
|
+
className="insp-key is-danger"
|
|
102
348
|
aria-label="Remove material"
|
|
103
349
|
title="Back to the model's own materials"
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
350
|
+
disabled={locked}
|
|
351
|
+
onClick={() => store.removeMeshMaterial(meshIds)}
|
|
107
352
|
>
|
|
108
|
-
|
|
353
|
+
<IconClose />
|
|
109
354
|
</button>
|
|
110
|
-
)
|
|
111
|
-
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
>
|
|
358
|
+
<button
|
|
359
|
+
type="button"
|
|
360
|
+
className="text-input mesh-material-name"
|
|
361
|
+
disabled={meshes.length !== 1}
|
|
362
|
+
title={meshes.length === 1 ? "Edit material nodes on this mesh" : "Select one mesh to edit its material"}
|
|
363
|
+
onClick={() => {
|
|
364
|
+
const mesh = meshes[0];
|
|
365
|
+
if (meshes.length === 1 && mesh !== undefined) store.openMaterialWorkspace(mesh.id);
|
|
366
|
+
}}
|
|
367
|
+
>
|
|
368
|
+
{materialName ?? "Set up nodes…"}
|
|
369
|
+
</button>
|
|
112
370
|
</InspectorRow>
|
|
113
371
|
|
|
372
|
+
{/* THE MODEL'S OWN MATERIALS, one row each.
|
|
373
|
+
The list is pushed up by the stage as the glTF parses, so it is absent
|
|
374
|
+
for a primitive and for a model that has not loaded — and absent is
|
|
375
|
+
the right answer in both: a mesh with one surface has nothing to
|
|
376
|
+
choose between, and offering slot rows for a model nobody has seen
|
|
377
|
+
yet would be offering names this chrome does not have. Each row enters
|
|
378
|
+
the workspace ON that slot, which is the only way to author a submesh
|
|
379
|
+
without first opening the whole model and then changing the header. */}
|
|
380
|
+
{meshes.length !== 1 || slots.length === 0 ? null : (
|
|
381
|
+
<div className="mesh-slot-list" role="group" aria-label="Model materials">
|
|
382
|
+
{slots.map((entry) => (
|
|
383
|
+
<button
|
|
384
|
+
type="button"
|
|
385
|
+
key={entry.index}
|
|
386
|
+
className="mesh-slot-row"
|
|
387
|
+
title={`Edit the nodes on ${entry.name.trim() === "" ? `material ${entry.index}` : entry.name}`}
|
|
388
|
+
onClick={() => {
|
|
389
|
+
const mesh = meshes[0];
|
|
390
|
+
if (mesh !== undefined) store.openMaterialWorkspace(mesh.id, { slot: entry.index });
|
|
391
|
+
}}
|
|
392
|
+
>
|
|
393
|
+
<span className="mesh-slot-index">{entry.index}</span>
|
|
394
|
+
<span className="mesh-slot-name">
|
|
395
|
+
{entry.name.trim() === "" ? `Material ${entry.index}` : entry.name}
|
|
396
|
+
</span>
|
|
397
|
+
<small>
|
|
398
|
+
{entry.drawables} mesh{entry.drawables === 1 ? "" : "es"}
|
|
399
|
+
{entry.maps.length === 0 ? "" : ` · ${entry.maps.length} map${entry.maps.length === 1 ? "" : "s"}`}
|
|
400
|
+
</small>
|
|
401
|
+
</button>
|
|
402
|
+
))}
|
|
403
|
+
</div>
|
|
404
|
+
)}
|
|
405
|
+
|
|
406
|
+
{/* ONE MESH AT A TIME, which is InstanceSection's rule for the same
|
|
407
|
+
reason: these keys belong to ONE generator's contract, and a selection
|
|
408
|
+
may wear two different generators — so a fan-out would write keys the
|
|
409
|
+
other material has never heard of. */}
|
|
410
|
+
{m.material === undefined ? null : meshes.length > 1 ? (
|
|
411
|
+
<div className="insp-notice">
|
|
412
|
+
Select one mesh on its own to edit the material's controls.
|
|
413
|
+
</div>
|
|
414
|
+
) : graph !== undefined ? (
|
|
415
|
+
// MaterialSection, below, owns a graph material's controls.
|
|
416
|
+
null
|
|
417
|
+
) : materialContract === undefined ? (
|
|
418
|
+
// Ceiling: `loadContract` swallows its failure by design, so a generator
|
|
419
|
+
// this build does not ship rests here rather than saying so. The Material
|
|
420
|
+
// row above still names it, which is what tells the author which one.
|
|
421
|
+
<div className="insp-notice">Loading the material's controls…</div>
|
|
422
|
+
) : (
|
|
423
|
+
// Contract order: `order` first, then the manifest's own declaration
|
|
424
|
+
// order, the rule every spec-driven panel lists by.
|
|
425
|
+
orderSpecs(materialContract).map((spec) => (
|
|
426
|
+
<MaterialParamRow key={spec.key} spec={spec} value={legacyMaterial?.params?.[spec.key]} />
|
|
427
|
+
))
|
|
428
|
+
)}
|
|
429
|
+
|
|
430
|
+
{/* The clip rows are ASSET-ONLY (a shape has no clips; the fields are
|
|
431
|
+
inert-kept on one), so a shape section simply does not show them. */}
|
|
432
|
+
{primitive !== null ? null : (
|
|
433
|
+
<>
|
|
114
434
|
{/* Free TEXT, not a dropdown. Clip names live inside the GLB and the
|
|
115
435
|
chrome never parses one — it holds a document, not a scene graph — so
|
|
116
436
|
a picker here could only ever offer the name that is already set.
|
|
@@ -118,7 +438,8 @@ export function MeshSection() {
|
|
|
118
438
|
bridge is a feed field, not an inspector change. */}
|
|
119
439
|
<InspectorRow label="Clip">
|
|
120
440
|
<TextField
|
|
121
|
-
value={m.clip ?? ""}
|
|
441
|
+
value={mixedOn((block) => block.clip ?? "") ? "" : (m.clip ?? "")}
|
|
442
|
+
{...(mixedOn((block) => block.clip ?? "") ? { placeholder: MIXED } : {})}
|
|
122
443
|
ariaLabel="Animation clip"
|
|
123
444
|
onCommit={(next) =>
|
|
124
445
|
patch((block) => {
|
|
@@ -134,39 +455,546 @@ export function MeshSection() {
|
|
|
134
455
|
/>
|
|
135
456
|
</InspectorRow>
|
|
136
457
|
|
|
137
|
-
<
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
</InspectorRow>
|
|
458
|
+
<MeshNumber
|
|
459
|
+
label="Start"
|
|
460
|
+
ariaLabel="Clip start frame"
|
|
461
|
+
value={m.clipStartFrame ?? 0}
|
|
462
|
+
mixed={mixedOn((block) => block.clipStartFrame ?? 0)}
|
|
463
|
+
unit="f"
|
|
464
|
+
step={1}
|
|
465
|
+
edit={(block, next) => ({ ...block, clipStartFrame: Math.round(next) })}
|
|
466
|
+
/>
|
|
147
467
|
|
|
148
468
|
<InspectorRow label="Loop">
|
|
149
469
|
<SegmentedControl
|
|
150
470
|
ariaLabel="Clip loop"
|
|
151
|
-
value={m.loop ?? "hold"}
|
|
152
|
-
options={
|
|
153
|
-
onChange={(next) => set("loop", next as
|
|
471
|
+
value={mixedOn((block) => block.loop ?? "hold") ? NO_VALUE : (m.loop ?? "hold")}
|
|
472
|
+
options={wordOptions(MEDIA_LOOPS)}
|
|
473
|
+
onChange={(next) => set("loop", next as MediaLoop)}
|
|
154
474
|
/>
|
|
155
475
|
</InspectorRow>
|
|
156
476
|
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
477
|
+
<MeshNumber
|
|
478
|
+
label="Rate"
|
|
479
|
+
ariaLabel="Clip rate"
|
|
480
|
+
value={m.timeScale ?? 1}
|
|
481
|
+
mixed={mixedOn((block) => block.timeScale ?? 1)}
|
|
482
|
+
// "Times as fast": the unit says multiplier, and the whole box scrubs.
|
|
483
|
+
unit="×"
|
|
484
|
+
step={0.1}
|
|
485
|
+
// Zero is REFUSED by validation: it freezes the clip forever, which
|
|
486
|
+
// is a pose you can already spell by clearing the clip, and it is
|
|
487
|
+
// indistinguishable from a slider that failed to write. Negative
|
|
488
|
+
// plays the clip backwards and is allowed.
|
|
489
|
+
edit={(block, next) => ({ ...block, timeScale: next === 0 ? 1 : next })}
|
|
490
|
+
/>
|
|
491
|
+
</>
|
|
492
|
+
)}
|
|
493
|
+
</InspectorSection>
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/** ONE primitive field's edit, on a block's OWN base primitive and only when it
|
|
498
|
+
* is that kind. Spreading the lead's painted primitive instead turned a
|
|
499
|
+
* sphere into a box, copied one spline's points onto another, and baked the
|
|
500
|
+
* lead's sampled animated fields into every base. */
|
|
501
|
+
function withPrim<K extends MeshPrimitive["kind"]>(
|
|
502
|
+
block: MeshBlock,
|
|
503
|
+
kind: K,
|
|
504
|
+
edit: (p: Extract<MeshPrimitive, { kind: K }>) => MeshPrimitive,
|
|
505
|
+
): MeshBlock {
|
|
506
|
+
const p = meshPrimitiveOf(block);
|
|
507
|
+
return p?.kind === kind ? { ...block, primitive: edit(p as Extract<MeshPrimitive, { kind: K }>) } : block;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* One number of the mesh block, as one box: its HARD limits default to the path
|
|
512
|
+
* table's own `numberRange`, so the field and the keyframe gate cannot answer
|
|
513
|
+
* "what is a legal number here" two ways, and an explicit `min`/`max` still
|
|
514
|
+
* wins for a row whose chrome limit is deliberately tighter than the schema's
|
|
515
|
+
* (a path declaring `exclusiveMin` has to be one of those: a bound of 0 the
|
|
516
|
+
* gate refuses is not a bound a field may offer). `step` stays the caller's,
|
|
517
|
+
* because it is a feel and not a limit.
|
|
518
|
+
*
|
|
519
|
+
* `edit` is the ONE builder for the typed value and the drag, applied to each
|
|
520
|
+
* mesh layer's own block: a capture is handed a layer out of the gesture's
|
|
521
|
+
* BASE document, so a drag never compounds its own moves, and its release
|
|
522
|
+
* lands exactly the document its last move drew.
|
|
523
|
+
*/
|
|
524
|
+
function MeshNumber({
|
|
525
|
+
label,
|
|
526
|
+
ariaLabel,
|
|
527
|
+
value,
|
|
528
|
+
mixed,
|
|
529
|
+
unit,
|
|
530
|
+
min,
|
|
531
|
+
max,
|
|
532
|
+
step,
|
|
533
|
+
path,
|
|
534
|
+
parts,
|
|
535
|
+
pin = true,
|
|
536
|
+
edit,
|
|
537
|
+
}: {
|
|
538
|
+
label: string;
|
|
539
|
+
ariaLabel?: string;
|
|
540
|
+
value: number;
|
|
541
|
+
mixed: boolean;
|
|
542
|
+
unit?: string;
|
|
543
|
+
min?: number;
|
|
544
|
+
max?: number;
|
|
545
|
+
step: number;
|
|
546
|
+
/** The track the value belongs to, which puts its diamond in the box. */
|
|
547
|
+
path?: string;
|
|
548
|
+
/** A vector's parts, keyed together by that diamond. */
|
|
549
|
+
parts?: readonly string[];
|
|
550
|
+
/** False for a vector's later boxes: its first box carries the one pin. */
|
|
551
|
+
pin?: boolean;
|
|
552
|
+
edit: (block: MeshBlock, value: number) => MeshBlock;
|
|
553
|
+
}) {
|
|
554
|
+
const write = useSelectionWrite();
|
|
555
|
+
const build = (layer: Layer3D, next: number): Layer3D =>
|
|
556
|
+
layer.kind === "mesh" ? ({ ...layer, mesh: edit(layer.mesh, next) } as Layer3D) : layer;
|
|
557
|
+
const scrub = useSelectionScrub((layer, raw) => build(layer, Number(raw)));
|
|
558
|
+
const tabled = path === undefined ? undefined : trackablePath(path)?.numberRange;
|
|
559
|
+
const lo = min ?? tabled?.min;
|
|
560
|
+
const hi = max ?? tabled?.max;
|
|
561
|
+
const trailing: ReactNode =
|
|
562
|
+
path === undefined ? undefined : (
|
|
563
|
+
<KeyframeDot path={path} label={label} pin={pin} {...(parts === undefined ? {} : { parts })} />
|
|
564
|
+
);
|
|
565
|
+
return (
|
|
566
|
+
<InspectorRow>
|
|
567
|
+
<NumberField
|
|
568
|
+
label={label}
|
|
569
|
+
value={value}
|
|
570
|
+
mixed={mixed}
|
|
571
|
+
ariaLabel={ariaLabel ?? label}
|
|
572
|
+
{...(unit === undefined ? {} : { unit })}
|
|
573
|
+
{...(lo === undefined ? {} : { min: lo })}
|
|
574
|
+
{...(hi === undefined ? {} : { max: hi })}
|
|
575
|
+
step={step}
|
|
576
|
+
trailing={trailing}
|
|
577
|
+
onCommit={(next) => write((layer) => build(layer, next))}
|
|
578
|
+
onScrubStart={scrub.start}
|
|
579
|
+
onScrub={scrub.move}
|
|
580
|
+
onScrubEnd={scrub.end}
|
|
581
|
+
/>
|
|
582
|
+
</InspectorRow>
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function SplineRows({
|
|
587
|
+
primitive,
|
|
588
|
+
hasTaperTracks,
|
|
589
|
+
hasTwistTrack,
|
|
590
|
+
editing,
|
|
591
|
+
locked,
|
|
592
|
+
onEdit,
|
|
593
|
+
onChange,
|
|
594
|
+
mixed,
|
|
595
|
+
onRefusal,
|
|
596
|
+
}: {
|
|
597
|
+
/** The LEAD spline, for display only. */
|
|
598
|
+
primitive: SplinePrimitive;
|
|
599
|
+
hasTaperTracks: boolean;
|
|
600
|
+
hasTwistTrack: boolean;
|
|
601
|
+
editing: boolean;
|
|
602
|
+
/** A viewer's panel: point editing writes the spline, so it does not open
|
|
603
|
+
* (the stage takes no spline edit from a viewer either). */
|
|
604
|
+
locked: boolean;
|
|
605
|
+
onEdit(): void;
|
|
606
|
+
/** Every row hands an UPDATER, applied to each layer's own spline, so one
|
|
607
|
+
* field edit never carries the lead's points or other fields across. */
|
|
608
|
+
onChange(edit: (p: SplinePrimitive) => SplinePrimitive): void;
|
|
609
|
+
mixed(read: (p: SplinePrimitive) => unknown): boolean;
|
|
610
|
+
onRefusal(message: string): void;
|
|
611
|
+
}) {
|
|
612
|
+
const frame = primitive.frameSize;
|
|
613
|
+
const up = primitive.up ?? DEFAULT_SPLINE_UP;
|
|
614
|
+
const profile = profileOf(primitive);
|
|
615
|
+
const setProfile = (edit: (prof: SplineProfile) => SplineProfile): void =>
|
|
616
|
+
onChange((p) => withProfile(p, edit));
|
|
617
|
+
const offset = profile.offset ?? [0, 0];
|
|
618
|
+
const nonTubeClosed = primitive.closed === true && profile.kind !== "tube";
|
|
619
|
+
const forbiddenCloseTracks = hasTaperTracks || (profile.kind !== "tube" && hasTwistTrack);
|
|
620
|
+
return (
|
|
621
|
+
<>
|
|
622
|
+
<InspectorRow label="Points">
|
|
623
|
+
<button type="button" className="btn" aria-pressed={editing} disabled={locked} onClick={onEdit}>
|
|
624
|
+
{editing ? "Finish editing" : `Edit ${primitive.points.length} points`}
|
|
625
|
+
</button>
|
|
626
|
+
</InspectorRow>
|
|
627
|
+
<InspectorRow label="Basis">
|
|
628
|
+
<SegmentedControl
|
|
629
|
+
ariaLabel="Spline basis"
|
|
630
|
+
value={mixed((p) => p.basis ?? "bezier") ? NO_VALUE : (primitive.basis ?? "bezier")}
|
|
631
|
+
options={[{ value: "bezier", label: "Bezier" }, { value: "catmull-rom", label: "Catmull" }]}
|
|
632
|
+
onChange={(basis) =>
|
|
633
|
+
onChange((p) =>
|
|
634
|
+
basis === "catmull-rom"
|
|
635
|
+
? { ...p, basis, points: p.points.map(({ in: _dropIn, out: _dropOut, ...point }) => point) }
|
|
636
|
+
: { ...p, basis: "bezier" },
|
|
637
|
+
)
|
|
638
|
+
}
|
|
639
|
+
/>
|
|
640
|
+
</InspectorRow>
|
|
641
|
+
<InspectorRow
|
|
642
|
+
label="Closed"
|
|
643
|
+
{...(primitive.points.length < 3
|
|
644
|
+
? { labelTitle: "A closed spline needs at least three points" }
|
|
645
|
+
: forbiddenCloseTracks && primitive.closed !== true
|
|
646
|
+
? {
|
|
647
|
+
labelTitle:
|
|
648
|
+
profile.kind !== "tube" && hasTwistTrack
|
|
649
|
+
? "Remove taper and twist tracks before closing this ribbon or rail"
|
|
650
|
+
: "Remove separate taper tracks before closing the spline",
|
|
651
|
+
}
|
|
652
|
+
: {})}
|
|
653
|
+
>
|
|
654
|
+
<Toggle
|
|
655
|
+
checked={primitive.closed === true}
|
|
656
|
+
mixed={mixed((p) => p.closed === true)}
|
|
657
|
+
ariaLabel="Closed spline"
|
|
658
|
+
disabled={primitive.closed !== true && (primitive.points.length < 3 || forbiddenCloseTracks)}
|
|
659
|
+
onChange={(closed) =>
|
|
660
|
+
onChange((p) => {
|
|
661
|
+
if (!closed) return { ...p, closed: false };
|
|
662
|
+
if (p.points.length < 3) return p;
|
|
663
|
+
const loopScale = Math.max(0, Math.min(MAX_SPLINE_SCALE, p.startScale ?? 1));
|
|
664
|
+
const twistDeg =
|
|
665
|
+
profileOf(p).kind === "tube"
|
|
666
|
+
? p.twistDeg
|
|
667
|
+
: Math.round((p.twistDeg ?? 0) / 360) * 360;
|
|
668
|
+
return {
|
|
669
|
+
...p,
|
|
670
|
+
closed: true,
|
|
671
|
+
startScale: loopScale,
|
|
672
|
+
endScale: loopScale,
|
|
673
|
+
...(twistDeg === undefined ? {} : { twistDeg }),
|
|
674
|
+
};
|
|
675
|
+
})
|
|
676
|
+
}
|
|
677
|
+
/>
|
|
678
|
+
</InspectorRow>
|
|
679
|
+
<SplineNumber label="Frame W" value={frame[0]} mixed={mixed((p) => p.frameSize[0])} min={1} max={MAX_SPLINE_COORDINATE} step={1} unit="px" edit={(p, value) => ({ ...p, frameSize: [value, p.frameSize[1]] })} />
|
|
680
|
+
<SplineNumber label="Frame H" value={frame[1]} mixed={mixed((p) => p.frameSize[1])} min={1} max={MAX_SPLINE_COORDINATE} step={1} unit="px" edit={(p, value) => ({ ...p, frameSize: [p.frameSize[0], value] })} />
|
|
681
|
+
{(["X", "Y", "Z"] as const).map((axis, index) => (
|
|
682
|
+
<SplineNumber
|
|
683
|
+
key={axis}
|
|
684
|
+
label={`Up ${axis}`}
|
|
685
|
+
value={up[index] ?? 0}
|
|
686
|
+
mixed={mixed((p) => (p.up ?? DEFAULT_SPLINE_UP)[index])}
|
|
687
|
+
min={-MAX_SPLINE_COORDINATE}
|
|
688
|
+
max={MAX_SPLINE_COORDINATE}
|
|
162
689
|
step={0.1}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
690
|
+
edit={(p, value) => {
|
|
691
|
+
const own = p.up ?? DEFAULT_SPLINE_UP;
|
|
692
|
+
const next: [number, number, number] = [own[0] ?? 0, own[1] ?? 1, own[2] ?? 0];
|
|
693
|
+
next[index] = value;
|
|
694
|
+
return {
|
|
695
|
+
...p,
|
|
696
|
+
up: next.every((component) => Math.abs(component) < 1e-4)
|
|
697
|
+
? [...DEFAULT_SPLINE_UP]
|
|
698
|
+
: next,
|
|
699
|
+
};
|
|
700
|
+
}}
|
|
701
|
+
/>
|
|
702
|
+
))}
|
|
703
|
+
<InspectorRow label="Profile">
|
|
704
|
+
<SegmentedControl
|
|
705
|
+
ariaLabel="Spline profile"
|
|
706
|
+
value={mixed((p) => profileOf(p).kind) ? NO_VALUE : profile.kind}
|
|
707
|
+
options={[
|
|
708
|
+
{ value: "tube", label: "Tube" },
|
|
709
|
+
{
|
|
710
|
+
value: "ribbon",
|
|
711
|
+
label: "Ribbon",
|
|
712
|
+
...(primitive.closed === true && profile.kind === "tube" && hasTwistTrack
|
|
713
|
+
? { title: "Remove the closed tube's twist track first" }
|
|
714
|
+
: {}),
|
|
715
|
+
},
|
|
716
|
+
{
|
|
717
|
+
value: "rail",
|
|
718
|
+
label: "Rail",
|
|
719
|
+
...(primitive.closed === true && profile.kind === "tube" && hasTwistTrack
|
|
720
|
+
? { title: "Remove the closed tube's twist track first" }
|
|
721
|
+
: {}),
|
|
722
|
+
},
|
|
723
|
+
]}
|
|
724
|
+
onChange={(kind) => {
|
|
725
|
+
if (
|
|
726
|
+
primitive.closed === true &&
|
|
727
|
+
profile.kind === "tube" &&
|
|
728
|
+
kind !== "tube" &&
|
|
729
|
+
hasTwistTrack
|
|
730
|
+
) {
|
|
731
|
+
onRefusal("Remove the closed tube's twist track before switching to ribbon or rail.");
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
setProfile((prof) => {
|
|
735
|
+
if (prof.kind === kind) return prof;
|
|
736
|
+
const common = {
|
|
737
|
+
...(prof.offset === undefined ? {} : { offset: [...prof.offset] as [number, number] }),
|
|
738
|
+
...(prof.angleDeg === undefined ? {} : { angleDeg: prof.angleDeg }),
|
|
739
|
+
};
|
|
740
|
+
return kind === "ribbon"
|
|
741
|
+
? { kind, widthPx: 16, ...common }
|
|
742
|
+
: kind === "rail"
|
|
743
|
+
? { kind, widthPx: 16, heightPx: 8, ...common }
|
|
744
|
+
: { kind: "tube", radiusPx: 8, ...common };
|
|
745
|
+
});
|
|
746
|
+
}}
|
|
168
747
|
/>
|
|
169
748
|
</InspectorRow>
|
|
170
|
-
|
|
749
|
+
{/* The three profile sizes state their own `min` and take their `max` from
|
|
750
|
+
the table: the table's bound is `exclusiveMin: 0`, which a field cannot
|
|
751
|
+
spell (0 would commit and the keyframe gate would then refuse it), so
|
|
752
|
+
0.01 is deliberately TIGHTER than the schema by one quantum. */}
|
|
753
|
+
{profile.kind === "tube" ? (
|
|
754
|
+
<SplineNumber path="mesh.primitive.profile.radiusPx" label="Radius" value={profile.radiusPx ?? 8} mixed={mixed((p) => { const prof = profileOf(p); return prof.kind === "tube" ? (prof.radiusPx ?? 8) : undefined; })} min={0.01} step={1} unit="px" edit={(p, radiusPx) => withProfile(p, (prof) => (prof.kind === "tube" ? { ...prof, radiusPx } : prof))} />
|
|
755
|
+
) : (
|
|
756
|
+
<SplineNumber path="mesh.primitive.profile.widthPx" label="Width" value={profile.widthPx ?? 16} mixed={mixed((p) => { const prof = profileOf(p); return prof.kind === "tube" ? undefined : (prof.widthPx ?? 16); })} min={0.01} step={1} unit="px" edit={(p, widthPx) => withProfile(p, (prof) => (prof.kind === "tube" ? prof : { ...prof, widthPx }))} />
|
|
757
|
+
)}
|
|
758
|
+
{profile.kind === "rail" ? (
|
|
759
|
+
<SplineNumber path="mesh.primitive.profile.heightPx" label="Height" value={profile.heightPx ?? 8} mixed={mixed((p) => { const prof = profileOf(p); return prof.kind === "rail" ? (prof.heightPx ?? 8) : undefined; })} min={0.01} step={1} unit="px" edit={(p, heightPx) => withProfile(p, (prof) => (prof.kind === "rail" ? { ...prof, heightPx } : prof))} />
|
|
760
|
+
) : null}
|
|
761
|
+
{/* ONE TRACK FOR BOTH OFFSETS: the profile offset is a vec2, so each of
|
|
762
|
+
its two boxes carries the vector's diamond and they light together,
|
|
763
|
+
and Offset X's box holds the track's one expose pin. */}
|
|
764
|
+
<SplineNumber path="mesh.primitive.profile.offset" parts={OFFSET_PARTS} label="Offset X" value={offset[0]} mixed={mixed((p) => (profileOf(p).offset ?? [0, 0])[0])} min={-MAX_SPLINE_COORDINATE} max={MAX_SPLINE_COORDINATE} step={1} unit="px" edit={(p, value) => withProfile(p, (prof) => ({ ...prof, offset: [value, (prof.offset ?? [0, 0])[1]] }))} />
|
|
765
|
+
<SplineNumber path="mesh.primitive.profile.offset" parts={OFFSET_PARTS} pin={false} label="Offset Y" value={offset[1]} mixed={mixed((p) => (profileOf(p).offset ?? [0, 0])[1])} min={-MAX_SPLINE_COORDINATE} max={MAX_SPLINE_COORDINATE} step={1} unit="px" edit={(p, value) => withProfile(p, (prof) => ({ ...prof, offset: [(prof.offset ?? [0, 0])[0], value] }))} />
|
|
766
|
+
<SplineNumber path="mesh.primitive.profile.angleDeg" label="Profile angle" value={profile.angleDeg ?? 0} mixed={mixed((p) => profileOf(p).angleDeg ?? 0)} min={-MAX_SPLINE_ANGLE_DEG} max={MAX_SPLINE_ANGLE_DEG} step={1} unit="°" edit={(p, angleDeg) => withProfile(p, (prof) => ({ ...prof, angleDeg }))} />
|
|
767
|
+
{profile.kind === "ribbon" ? (
|
|
768
|
+
<p className="insp-note">Ribbon is double-sided and uncapped.</p>
|
|
769
|
+
) : splineVisibleRange(primitive).fullLoop ? (
|
|
770
|
+
<p className="insp-note">A full closed loop has no ends. Caps apply after opening or trimming it.</p>
|
|
771
|
+
) : (
|
|
772
|
+
<InspectorRow label="Caps">
|
|
773
|
+
<SegmentedControl
|
|
774
|
+
ariaLabel="Spline caps"
|
|
775
|
+
value={mixed((p) => p.caps ?? DEFAULT_SPLINE_CAPS) ? NO_VALUE : (primitive.caps ?? DEFAULT_SPLINE_CAPS)}
|
|
776
|
+
options={[{ value: "none", label: "None" }, { value: "flat", label: "Flat" }, { value: "round", label: "Round" }]}
|
|
777
|
+
onChange={(caps) => onChange((p) => ({ ...p, caps: caps as NonNullable<SplinePrimitive["caps"]> }))}
|
|
778
|
+
/>
|
|
779
|
+
</InspectorRow>
|
|
780
|
+
)}
|
|
781
|
+
<SplineNumber path="mesh.primitive.trimStart" label="Trim start" value={primitive.trimStart ?? 0} mixed={mixed((p) => p.trimStart ?? 0)} step={0.01} edit={(p, trimStart) => ({ ...p, trimStart })} />
|
|
782
|
+
<SplineNumber path="mesh.primitive.trimEnd" label="Trim end" value={primitive.trimEnd ?? 1} mixed={mixed((p) => p.trimEnd ?? 1)} step={0.01} edit={(p, trimEnd) => ({ ...p, trimEnd })} />
|
|
783
|
+
<SplineNumber path="mesh.primitive.trimOffset" label="Trim offset" value={primitive.trimOffset ?? 0} mixed={mixed((p) => p.trimOffset ?? 0)} step={0.01} edit={(p, trimOffset) => ({ ...p, trimOffset })} />
|
|
784
|
+
<SplineNumber
|
|
785
|
+
{...(nonTubeClosed ? {} : { path: "mesh.primitive.twistDeg" as const })}
|
|
786
|
+
label="Twist"
|
|
787
|
+
value={primitive.twistDeg ?? 0}
|
|
788
|
+
mixed={mixed((p) => p.twistDeg ?? 0)}
|
|
789
|
+
min={-MAX_SPLINE_ANGLE_DEG}
|
|
790
|
+
max={MAX_SPLINE_ANGLE_DEG}
|
|
791
|
+
step={nonTubeClosed ? 360 : 1}
|
|
792
|
+
unit="°"
|
|
793
|
+
edit={(p, value) => {
|
|
794
|
+
// A closed ribbon or rail must twist in whole turns, per layer.
|
|
795
|
+
const whole = p.closed === true && profileOf(p).kind !== "tube";
|
|
796
|
+
return { ...p, twistDeg: whole ? Math.round(value / 360) * 360 : value };
|
|
797
|
+
}}
|
|
798
|
+
/>
|
|
799
|
+
{primitive.closed === true ? (
|
|
800
|
+
<SplineNumber
|
|
801
|
+
label="Loop scale"
|
|
802
|
+
value={primitive.startScale ?? 1}
|
|
803
|
+
mixed={mixed((p) => p.startScale ?? 1)}
|
|
804
|
+
min={0}
|
|
805
|
+
max={MAX_SPLINE_SCALE}
|
|
806
|
+
step={0.05}
|
|
807
|
+
edit={(p, value) => ({ ...p, startScale: value, endScale: value })}
|
|
808
|
+
/>
|
|
809
|
+
) : (
|
|
810
|
+
<>
|
|
811
|
+
<SplineNumber path="mesh.primitive.startScale" label="Start scale" value={primitive.startScale ?? 1} mixed={mixed((p) => p.startScale ?? 1)} min={0} max={MAX_SPLINE_SCALE} step={0.05} edit={(p, startScale) => ({ ...p, startScale })} />
|
|
812
|
+
<SplineNumber path="mesh.primitive.endScale" label="End scale" value={primitive.endScale ?? 1} mixed={mixed((p) => p.endScale ?? 1)} min={0} max={MAX_SPLINE_SCALE} step={0.05} edit={(p, endScale) => ({ ...p, endScale })} />
|
|
813
|
+
</>
|
|
814
|
+
)}
|
|
815
|
+
<InspectorRow label="Geometry">
|
|
816
|
+
<Toggle
|
|
817
|
+
checked={primitive.renderGeometry !== false}
|
|
818
|
+
mixed={mixed((p) => p.renderGeometry !== false)}
|
|
819
|
+
ariaLabel="Render spline geometry"
|
|
820
|
+
onChange={(renderGeometry) => onChange((p) => ({ ...p, renderGeometry }))}
|
|
821
|
+
/>
|
|
822
|
+
</InspectorRow>
|
|
823
|
+
</>
|
|
171
824
|
);
|
|
172
825
|
}
|
|
826
|
+
|
|
827
|
+
function profileOf(p: SplinePrimitive): SplineProfile {
|
|
828
|
+
return p.profile ?? { kind: "tube", radiusPx: 8 };
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/** A profile edit on ONE spline: a closed ribbon or rail twists in whole
|
|
832
|
+
* turns, so leaving the tube rounds that spline's own twist to one. */
|
|
833
|
+
function withProfile(p: SplinePrimitive, edit: (prof: SplineProfile) => SplineProfile): SplinePrimitive {
|
|
834
|
+
const next = edit(profileOf(p));
|
|
835
|
+
const twistDeg =
|
|
836
|
+
p.closed === true && next.kind !== "tube"
|
|
837
|
+
? Math.round((p.twistDeg ?? 0) / 360) * 360
|
|
838
|
+
: p.twistDeg;
|
|
839
|
+
return { ...p, profile: next, ...(twistDeg === undefined ? {} : { twistDeg }) };
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/** One spline number: a `MeshNumber` whose edit is over each layer's own
|
|
843
|
+
* spline (`withPrim`), never the lead's. */
|
|
844
|
+
function SplineNumber({
|
|
845
|
+
edit,
|
|
846
|
+
...row
|
|
847
|
+
}: {
|
|
848
|
+
label: string;
|
|
849
|
+
value: number;
|
|
850
|
+
mixed: boolean;
|
|
851
|
+
path?: string;
|
|
852
|
+
parts?: readonly string[];
|
|
853
|
+
pin?: boolean;
|
|
854
|
+
unit?: string;
|
|
855
|
+
min?: number;
|
|
856
|
+
max?: number;
|
|
857
|
+
step: number;
|
|
858
|
+
edit: (p: SplinePrimitive, value: number) => SplinePrimitive;
|
|
859
|
+
}) {
|
|
860
|
+
return <MeshNumber {...row} edit={(block, value) => withPrim(block, "spline", (p) => edit(p, value))} />;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* The shape's base albedo. Its own component for the same reason
|
|
865
|
+
* `MaterialParamRow` is one: `ColorPicker` routes its whole drag through
|
|
866
|
+
* `scrub.move`, so a row without the document bracket is a thumb that moves
|
|
867
|
+
* over a document that never changes.
|
|
868
|
+
*/
|
|
869
|
+
function ShapeColorRow({ value, mixed }: { value: string; mixed: boolean }) {
|
|
870
|
+
const write = useSelectionWrite();
|
|
871
|
+
const scrub = useSelectionScrub((layer, raw) => writeShapeColor(layer, raw));
|
|
872
|
+
return (
|
|
873
|
+
<InspectorRow>
|
|
874
|
+
{/* The hex row Fill and Stroke draw, so the inspector has ONE colour
|
|
875
|
+
grammar, its diamond inside the field. Unlabelled like theirs: the
|
|
876
|
+
swatch is the colour, and the accessible name ("Shape colour") says
|
|
877
|
+
what it colours. OPAQUE, because `mesh.color` is `#rrggbb` only (the
|
|
878
|
+
path's `opaqueHex`): an alpha the picker offered would be a value the
|
|
879
|
+
write below refuses. */}
|
|
880
|
+
<ColorField
|
|
881
|
+
value={value}
|
|
882
|
+
mixed={mixed}
|
|
883
|
+
opaque
|
|
884
|
+
ariaLabel="Shape colour"
|
|
885
|
+
commit={(hex) => write((layer) => writeShapeColor(layer, hex))}
|
|
886
|
+
scrub={scrub}
|
|
887
|
+
trailing={<KeyframeDot path="mesh.color" />}
|
|
888
|
+
/>
|
|
889
|
+
</InspectorRow>
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/** PURE, over the layer it is handed — `writeParam`'s rule: a value the
|
|
894
|
+
* document cannot hold leaves the layer untouched by reference. */
|
|
895
|
+
function writeShapeColor(layer: Layer3D, raw: unknown): Layer3D {
|
|
896
|
+
if (layer.kind !== "mesh") return layer;
|
|
897
|
+
if (typeof raw !== "string" || !OPAQUE_HEX.test(raw)) return layer;
|
|
898
|
+
return { ...layer, mesh: { ...layer.mesh, color: raw.toLowerCase() } };
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/** `#rrggbb`: a mesh colour's and a legacy material colour param's one
|
|
902
|
+
* spelling (validate refuses every other). */
|
|
903
|
+
const OPAQUE_HEX = /^#[0-9a-fA-F]{6}$/;
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* One generator param, as the one contract row.
|
|
907
|
+
*
|
|
908
|
+
* BOTH HALVES ARE WIRED, and the drag half is the one that was missing
|
|
909
|
+
* everywhere: `ColorPicker` routes its SV square, its hue bar and its alpha bar
|
|
910
|
+
* entirely through `scrub.move` and never calls `commit` while the pointer is
|
|
911
|
+
* down, so a row holding the commit half alone has a control whose thumb moves
|
|
912
|
+
* and whose document never changes.
|
|
913
|
+
*
|
|
914
|
+
* A stored param carries the row's RESET, which deletes it: an absent key is
|
|
915
|
+
* "the contract's own default", and with no reset a param once touched could
|
|
916
|
+
* never follow a later manifest edit again. A colour is OPAQUE: a legacy param
|
|
917
|
+
* holds `#rrggbb` only (validate), so the picker offers no alpha it would
|
|
918
|
+
* refuse.
|
|
919
|
+
*/
|
|
920
|
+
function MaterialParamRow(props: { spec: ControlSpec; value: MeshMaterialParam | undefined }) {
|
|
921
|
+
const { spec } = props;
|
|
922
|
+
const write = useSelectionWrite();
|
|
923
|
+
// The builder re-reads its subject from the layer it is HANDED, which every
|
|
924
|
+
// capture re-derives from the gesture's base — closing over a value read at
|
|
925
|
+
// render is what makes a control run away from the cursor.
|
|
926
|
+
const scrub = useSelectionScrub((layer, raw) => writeParam(layer, spec.key, raw));
|
|
927
|
+
// An unset param shows (and edits from) the spec's default, and stores
|
|
928
|
+
// nothing until it is touched: an absent key means "the contract's own
|
|
929
|
+
// default", so baking one in would freeze it against a later manifest edit.
|
|
930
|
+
const value = props.value ?? ("default" in spec ? spec.default : undefined);
|
|
931
|
+
return (
|
|
932
|
+
<PropRow
|
|
933
|
+
spec={spec}
|
|
934
|
+
value={value}
|
|
935
|
+
commit={(raw) => write((layer) => writeParam(layer, spec.key, raw))}
|
|
936
|
+
scrub={scrub}
|
|
937
|
+
{...(spec.kind === "color" ? { opaqueColor: true } : {})}
|
|
938
|
+
{...(props.value === undefined
|
|
939
|
+
? {}
|
|
940
|
+
: {
|
|
941
|
+
reset: {
|
|
942
|
+
onReset: () => write((layer) => withoutParam(layer, spec.key)),
|
|
943
|
+
title: "Back to the material's own default",
|
|
944
|
+
},
|
|
945
|
+
})}
|
|
946
|
+
/>
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/** PURE, and over the layer it is handed — the shape both write paths need. A
|
|
951
|
+
* value the document's own vocabulary cannot hold leaves the layer untouched
|
|
952
|
+
* (by reference, so no history entry and no save), because the alternative is
|
|
953
|
+
* a document the next save refuses. */
|
|
954
|
+
function writeParam(layer: Layer3D, key: string, raw: unknown): Layer3D {
|
|
955
|
+
const value = asParam(raw);
|
|
956
|
+
if (value === null || layer.kind !== "mesh") return layer;
|
|
957
|
+
const material = layer.mesh.material;
|
|
958
|
+
if (material === undefined || isGraphMaterial(material)) return layer;
|
|
959
|
+
return {
|
|
960
|
+
...layer,
|
|
961
|
+
mesh: {
|
|
962
|
+
...layer.mesh,
|
|
963
|
+
material: { ...material, params: { ...material.params, [key]: value } },
|
|
964
|
+
},
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/** The param DELETED, so it follows the contract's default again. Its `params`
|
|
969
|
+
* go too once empty: a material with no changed params is `{ generator }`. */
|
|
970
|
+
function withoutParam(layer: Layer3D, key: string): Layer3D {
|
|
971
|
+
if (layer.kind !== "mesh") return layer;
|
|
972
|
+
const material = layer.mesh.material;
|
|
973
|
+
if (material === undefined || isGraphMaterial(material)) return layer;
|
|
974
|
+
const stored = material.params;
|
|
975
|
+
if (stored === undefined || stored[key] === undefined) return layer;
|
|
976
|
+
const { [key]: _drop, ...params } = stored;
|
|
977
|
+
const { params: _all, ...bare } = material;
|
|
978
|
+
return {
|
|
979
|
+
...layer,
|
|
980
|
+
mesh: { ...layer.mesh, material: Object.keys(params).length === 0 ? bare : { ...bare, params } },
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/** `MeshMaterialParam`, which is exactly the widget vocabulary `effectContract`
|
|
985
|
+
* can produce (number / boolean / colour / vec2 / vec3), and exactly what
|
|
986
|
+
* validate accepts: a colour is `#rrggbb`, and any other string is refused. */
|
|
987
|
+
function asParam(raw: unknown): MeshMaterialParam | null {
|
|
988
|
+
if (typeof raw === "number") return Number.isFinite(raw) ? raw : null;
|
|
989
|
+
if (typeof raw === "boolean") return raw;
|
|
990
|
+
if (typeof raw === "string") return OPAQUE_HEX.test(raw) ? raw : null;
|
|
991
|
+
if (
|
|
992
|
+
Array.isArray(raw) &&
|
|
993
|
+
(raw.length === 2 || raw.length === 3) &&
|
|
994
|
+
raw.every((n) => typeof n === "number" && Number.isFinite(n))
|
|
995
|
+
) {
|
|
996
|
+
const v = raw as number[];
|
|
997
|
+
return v.length === 2 ? [v[0]!, v[1]!] : [v[0]!, v[1]!, v[2]!];
|
|
998
|
+
}
|
|
999
|
+
return null;
|
|
1000
|
+
}
|