@@ -76,7 +76,14 @@ import { isProxyFrameCountComplete } from '../../stores/mediaStore/helpers/proxy
7676
7777const log = Logger . create ( 'MediaPanel' ) ;
7878import { useMediaStore } from '../../stores/mediaStore' ;
79- import type { MediaFile , Composition , ProjectItem , SolidItem , CameraItem } from '../../stores/mediaStore' ;
79+ import type {
80+ CameraItem ,
81+ Composition ,
82+ MediaFile ,
83+ MotionShapeItem ,
84+ ProjectItem ,
85+ SolidItem ,
86+ } from '../../stores/mediaStore' ;
8087import { useTimelineStore } from '../../stores/timeline' ;
8188import { useDockStore } from '../../stores/dockStore' ;
8289import { useContextMenuPosition } from '../../hooks/useContextMenuPosition' ;
@@ -384,10 +391,12 @@ export function MediaPanel() {
384391 const compositions = useMediaStore ( state => state . compositions ) ;
385392 const folders = useMediaStore ( state => state . folders ) ;
386393 const textItems = useMediaStore ( state => state . textItems ) ;
387- const solidItems = useMediaStore ( state => state . solidItems ) ;
388- const meshItems = useMediaStore ( state => state . meshItems ) ;
389- const cameraItems = useMediaStore ( state => state . cameraItems ) ;
390- const splatEffectorItems = useMediaStore ( state => state . splatEffectorItems ) ;
394+ const solidItems = useMediaStore ( state => state . solidItems ?? [ ] ) ;
395+ const meshItems = useMediaStore ( state => state . meshItems ?? [ ] ) ;
396+ const cameraItems = useMediaStore ( state => state . cameraItems ?? [ ] ) ;
397+ const splatEffectorItems = useMediaStore ( state => state . splatEffectorItems ?? [ ] ) ;
398+ const mathSceneItems = useMediaStore ( state => state . mathSceneItems ?? [ ] ) ;
399+ const motionShapeItems = useMediaStore ( state => state . motionShapeItems ?? [ ] ) ;
391400 const selectedIds = useMediaStore ( state => state . selectedIds ) ;
392401 const expandedFolderIds = useMediaStore ( state => state . expandedFolderIds ) ;
393402 const fileSystemSupported = useMediaStore ( state => state . fileSystemSupported ) ;
@@ -423,6 +432,7 @@ export function MediaPanel() {
423432 removeTextItem,
424433 createSolidItem,
425434 getOrCreateSolidFolder,
435+ removeSolidItem,
426436 updateSolidItem,
427437 createMeshItem,
428438 getOrCreateMeshFolder,
@@ -433,6 +443,12 @@ export function MediaPanel() {
433443 createSplatEffectorItem,
434444 getOrCreateSplatEffectorFolder,
435445 removeSplatEffectorItem,
446+ createMathSceneItem,
447+ getOrCreateMathSceneFolder,
448+ removeMathSceneItem,
449+ createMotionShapeItem,
450+ getOrCreateMotionShapeFolder,
451+ removeMotionShapeItem,
436452 setLabelColor,
437453 importGaussianSplat,
438454 } = useMediaStore . getState ( ) ;
@@ -1319,12 +1335,15 @@ export function MediaPanel() {
13191335 else if ( compositions . find ( c => c . id === id ) ) removeComposition ( id ) ;
13201336 else if ( folders . find ( f => f . id === id ) ) removeFolder ( id ) ;
13211337 else if ( textItems . find ( t => t . id === id ) ) removeTextItem ( id ) ;
1338+ else if ( solidItems . find ( s => s . id === id ) ) removeSolidItem ( id ) ;
13221339 else if ( meshItems . find ( m => m . id === id ) ) removeMeshItem ( id ) ;
13231340 else if ( cameraItems . find ( c => c . id === id ) ) removeCameraItem ( id ) ;
13241341 else if ( splatEffectorItems . find ( e => e . id === id ) ) removeSplatEffectorItem ( id ) ;
1342+ else if ( mathSceneItems . find ( m => m . id === id ) ) removeMathSceneItem ( id ) ;
1343+ else if ( motionShapeItems . find ( m => m . id === id ) ) removeMotionShapeItem ( id ) ;
13251344 } ) ;
13261345 closeContextMenu ( ) ;
1327- } , [ selectedIds , files , compositions , folders , textItems , meshItems , cameraItems , splatEffectorItems , removeFile , removeComposition , removeFolder , removeTextItem , removeMeshItem , removeCameraItem , removeSplatEffectorItem , closeContextMenu ] ) ;
1346+ } , [ selectedIds , files , compositions , folders , textItems , solidItems , meshItems , cameraItems , splatEffectorItems , mathSceneItems , motionShapeItems , removeFile , removeComposition , removeFolder , removeTextItem , removeSolidItem , removeMeshItem , removeCameraItem , removeSplatEffectorItem , removeMathSceneItem , removeMotionShapeItem , closeContextMenu ] ) ;
13281347
13291348 // Get the active parent folder (icons view: current open folder, classic/board view: selected folder or null)
13301349 const getActiveParentId = useCallback ( ( ) : string | null => {
@@ -1389,6 +1408,18 @@ export function MediaPanel() {
13891408 closeContextMenu ( ) ;
13901409 } , [ createSplatEffectorItem , getOrCreateSplatEffectorFolder , closeContextMenu ] ) ;
13911410
1411+ const handleNewMathScene = useCallback ( ( ) => {
1412+ const mathFolderId = getOrCreateMathSceneFolder ( ) ;
1413+ createMathSceneItem ( undefined , mathFolderId ) ;
1414+ closeContextMenu ( ) ;
1415+ } , [ createMathSceneItem , getOrCreateMathSceneFolder , closeContextMenu ] ) ;
1416+
1417+ const handleNewMotionShape = useCallback ( ( primitive : import ( '../../types/motionDesign' ) . ShapePrimitive ) => {
1418+ const motionFolderId = getOrCreateMotionShapeFolder ( ) ;
1419+ createMotionShapeItem ( primitive , undefined , motionFolderId ) ;
1420+ closeContextMenu ( ) ;
1421+ } , [ createMotionShapeItem , getOrCreateMotionShapeFolder , closeContextMenu ] ) ;
1422+
13921423 // Import Gaussian Avatar (.zip) — opens file picker, imports with forced gaussian-avatar type
13931424 const handleImportGaussianSplat = useCallback ( ( ) => {
13941425 const input = document . createElement ( 'input' ) ;
@@ -1572,6 +1603,42 @@ export function MediaPanel() {
15721603 return ;
15731604 }
15741605
1606+ if ( item . type === 'math-scene' ) {
1607+ setExternalDragPayload ( {
1608+ kind : 'math-scene' ,
1609+ id : item . id ,
1610+ duration : item . duration ,
1611+ hasAudio : false ,
1612+ isAudio : false ,
1613+ isVideo : true ,
1614+ } ) ;
1615+ e . dataTransfer . setData ( 'application/x-math-scene-item-id' , item . id ) ;
1616+ e . dataTransfer . effectAllowed = 'copyMove' ;
1617+ if ( e . currentTarget instanceof HTMLElement ) {
1618+ e . dataTransfer . setDragImage ( e . currentTarget , 10 , 10 ) ;
1619+ }
1620+ return ;
1621+ }
1622+
1623+ if ( item . type === 'motion-shape' ) {
1624+ const motionShape = item as MotionShapeItem ;
1625+ setExternalDragPayload ( {
1626+ kind : 'motion-shape' ,
1627+ id : item . id ,
1628+ duration : motionShape . duration ,
1629+ hasAudio : false ,
1630+ isAudio : false ,
1631+ isVideo : true ,
1632+ primitive : motionShape . primitive ,
1633+ } ) ;
1634+ e . dataTransfer . setData ( 'application/x-motion-shape-item-id' , item . id ) ;
1635+ e . dataTransfer . effectAllowed = 'copyMove' ;
1636+ if ( e . currentTarget instanceof HTMLElement ) {
1637+ e . dataTransfer . setDragImage ( e . currentTarget , 10 , 10 ) ;
1638+ }
1639+ return ;
1640+ }
1641+
15751642 // Handle media file drag
15761643 const mediaFile = item as MediaFile ;
15771644 if ( mediaFile . isImporting || mediaNeedsRelink ( mediaFile ) ) {
@@ -2048,6 +2115,7 @@ export function MediaPanel() {
20482115 if ( mf . fps ) parts . push ( `${ mf . fps } fps` ) ;
20492116 if ( mf . fileSize ) parts . push ( formatFileSize ( mf . fileSize ) ) ;
20502117 if ( mf . bitrate ) parts . push ( formatBitrate ( mf . bitrate ) ) ;
2118+ if ( ! mf . duration && 'duration' in item && item . duration ) parts . push ( formatDuration ( item . duration ) ) ;
20512119 }
20522120
20532121 return parts . join ( '\n' ) ;
@@ -2067,7 +2135,7 @@ export function MediaPanel() {
20672135 const importProgress = getItemImportProgress ( item ) ;
20682136
20692137 // Duration badge: videos + compositions
2070- const duration = mediaFile ?. duration || comp ?. duration ;
2138+ const duration = mediaFile ?. duration || comp ?. duration || ( 'duration' in item ? item . duration : undefined ) ;
20712139
20722140 // Folder item count
20732141 const folderCount = isFolder ? getItemsForParent ( item . id ) . length : 0 ;
@@ -2126,7 +2194,9 @@ export function MediaPanel() {
21262194 ...meshItems ,
21272195 ...cameraItems ,
21282196 ...splatEffectorItems ,
2129- ] ) , [ files , compositions , folders , textItems , solidItems , meshItems , cameraItems , splatEffectorItems ] ) ;
2197+ ...mathSceneItems ,
2198+ ...motionShapeItems ,
2199+ ] ) , [ files , compositions , folders , textItems , solidItems , meshItems , cameraItems , splatEffectorItems , mathSceneItems , motionShapeItems ] ) ;
21302200
21312201 const projectListItems = useMemo < ProjectItem [ ] > ( ( ) => ( [
21322202 ...folders ,
@@ -2136,8 +2206,10 @@ export function MediaPanel() {
21362206 ...meshItems ,
21372207 ...cameraItems ,
21382208 ...splatEffectorItems ,
2209+ ...mathSceneItems ,
2210+ ...motionShapeItems ,
21392211 ...files ,
2140- ] ) , [ folders , compositions , textItems , solidItems , meshItems , cameraItems , splatEffectorItems , files ] ) ;
2212+ ] ) , [ folders , compositions , textItems , solidItems , meshItems , cameraItems , splatEffectorItems , mathSceneItems , motionShapeItems , files ] ) ;
21412213
21422214 const allProjectItemsById = useMemo ( ( ) => new Map ( allProjectItems . map ( ( item ) => [ item . id , item ] ) ) , [ allProjectItems ] ) ;
21432215 const totalItems = allProjectItems . length ;
@@ -3051,6 +3123,29 @@ export function MediaPanel() {
30513123 } ;
30523124 }
30533125
3126+ if ( item . type === 'math-scene' ) {
3127+ return {
3128+ kind : 'math-scene' ,
3129+ id : item . id ,
3130+ duration : item . duration ,
3131+ hasAudio : false ,
3132+ isAudio : false ,
3133+ isVideo : true ,
3134+ } ;
3135+ }
3136+
3137+ if ( item . type === 'motion-shape' ) {
3138+ return {
3139+ kind : 'motion-shape' ,
3140+ id : item . id ,
3141+ duration : item . duration ,
3142+ hasAudio : false ,
3143+ isAudio : false ,
3144+ isVideo : true ,
3145+ primitive : item . primitive ,
3146+ } ;
3147+ }
3148+
30543149 if ( isImportedMediaFileItem ( item ) && item . file && ! item . isImporting ) {
30553150 const isAudioOnly =
30563151 item . file . type . startsWith ( 'audio/' ) ||
@@ -3896,6 +3991,24 @@ export function MediaPanel() {
38963991 < span className = "add-dropdown-icon" > < FileTypeIcon type = "splat-effector" /> </ span >
38973992 < span > 3D Effector</ span >
38983993 </ div >
3994+ < div className = "add-dropdown-separator" />
3995+ < div className = "add-dropdown-item" onClick = { ( ) => { handleNewMathScene ( ) ; setAddDropdownOpen ( false ) ; } } >
3996+ < span className = "add-dropdown-icon" > < FileTypeIcon type = "math-scene" /> </ span >
3997+ < span > Math Scene</ span >
3998+ </ div >
3999+ < div className = "add-dropdown-item has-submenu" onMouseEnter = { handleSubmenuHover } onMouseLeave = { handleSubmenuLeave } >
4000+ < span className = "add-dropdown-icon" > < FileTypeIcon type = "motion-shape" /> </ span >
4001+ < span > Motion Shape</ span >
4002+ < span className = "submenu-arrow" > ▶</ span >
4003+ < div className = "add-dropdown-submenu" >
4004+ < div className = "add-dropdown-item" onClick = { ( ) => { handleNewMotionShape ( 'rectangle' ) ; setAddDropdownOpen ( false ) ; } } >
4005+ < span > Rectangle</ span >
4006+ </ div >
4007+ < div className = "add-dropdown-item" onClick = { ( ) => { handleNewMotionShape ( 'ellipse' ) ; setAddDropdownOpen ( false ) ; } } >
4008+ < span > Ellipse</ span >
4009+ </ div >
4010+ </ div >
4011+ </ div >
38994012 < div className = "add-dropdown-item has-submenu" onMouseEnter = { handleSubmenuHover } onMouseLeave = { handleSubmenuLeave } >
39004013 < span className = "add-dropdown-icon" > < FileTypeIcon type = "mesh" /> </ span >
39014014 < span > Mesh</ span >
@@ -4095,7 +4208,9 @@ export function MediaPanel() {
40954208 solidItems . find ( s => s . id === contextMenu . itemId ) ||
40964209 meshItems . find ( m => m . id === contextMenu . itemId ) ||
40974210 cameraItems . find ( c => c . id === contextMenu . itemId ) ||
4098- splatEffectorItems . find ( e => e . id === contextMenu . itemId )
4211+ splatEffectorItems . find ( e => e . id === contextMenu . itemId ) ||
4212+ mathSceneItems . find ( m => m . id === contextMenu . itemId ) ||
4213+ motionShapeItems . find ( m => m . id === contextMenu . itemId )
40994214 : null ;
41004215 const isVideoFile = selectedItem && 'type' in selectedItem && selectedItem . type === 'video' ;
41014216 const isComposition = selectedItem && 'type' in selectedItem && selectedItem . type === 'composition' ;
@@ -4153,6 +4268,24 @@ export function MediaPanel() {
41534268 < span className = "context-menu-icon" > < FileTypeIcon type = "splat-effector" /> </ span >
41544269 3D Effector
41554270 </ div >
4271+ < div className = "context-menu-separator" />
4272+ < div className = "context-menu-item" onClick = { ( ) => { handleNewMathScene ( ) ; closeContextMenu ( ) ; } } >
4273+ < span className = "context-menu-icon" > < FileTypeIcon type = "math-scene" /> </ span >
4274+ Math Scene
4275+ </ div >
4276+ < div className = "context-menu-item has-submenu" onMouseEnter = { handleSubmenuHover } onMouseLeave = { handleSubmenuLeave } >
4277+ < span className = "context-menu-icon" > < FileTypeIcon type = "motion-shape" /> </ span >
4278+ < span > Motion Shape</ span >
4279+ < span className = "submenu-arrow" > ▶</ span >
4280+ < div className = "context-submenu" >
4281+ < div className = "context-menu-item" onClick = { ( ) => { handleNewMotionShape ( 'rectangle' ) ; closeContextMenu ( ) ; } } >
4282+ Rectangle
4283+ </ div >
4284+ < div className = "context-menu-item" onClick = { ( ) => { handleNewMotionShape ( 'ellipse' ) ; closeContextMenu ( ) ; } } >
4285+ Ellipse
4286+ </ div >
4287+ </ div >
4288+ </ div >
41564289 < div className = "context-menu-item has-submenu" onMouseEnter = { handleSubmenuHover } onMouseLeave = { handleSubmenuLeave } >
41574290 < span className = "context-menu-icon" > < FileTypeIcon type = "mesh" /> </ span >
41584291 < span > Mesh</ span >
0 commit comments