| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import useSelectedFrameHelper from '@/common/components/video/filmstrip/useSelectedFrameHelper'; |
| import {BaseTracklet, DatalessMask} from '@/common/tracker/Tracker'; |
| import {spacing, w} from '@/theme/tokens.stylex'; |
| import stylex from '@stylexjs/stylex'; |
| import {useMemo} from 'react'; |
|
|
| const styles = stylex.create({ |
| container: { |
| display: 'flex', |
| alignItems: 'center', |
| gap: spacing[4], |
| width: '100%', |
| }, |
| trackletNameContainer: { |
| width: w[12], |
| textAlign: 'center', |
| fontSize: '10px', |
| color: 'white', |
| }, |
| swimlaneContainer: { |
| flexGrow: 1, |
| position: 'relative', |
| display: 'flex', |
| height: 12, |
| marginVertical: '0.25rem' , |
| '@media screen and (max-width: 768px)': { |
| marginVertical: 0, |
| }, |
| }, |
| swimlane: { |
| position: 'absolute', |
| left: 0, |
| top: '50%', |
| width: '100%', |
| height: 1, |
| transform: 'translate3d(0, -50%, 0)', |
| opacity: 0.4, |
| }, |
| segment: { |
| position: 'absolute', |
| top: '50%', |
| height: 1, |
| transform: 'translate3d(0, -50%, 0)', |
| }, |
| segmentationPoint: { |
| position: 'absolute', |
| top: '50%', |
| transform: 'translate3d(0, -50%, 0)', |
| borderRadius: '50%', |
| cursor: 'pointer', |
| width: 12, |
| height: 12, |
| '@media screen and (max-width: 768px)': { |
| width: 8, |
| height: 8, |
| }, |
| }, |
| }); |
|
|
| type SwimlineSegment = { |
| left: number; |
| width: number; |
| }; |
|
|
| type Props = { |
| tracklet: BaseTracklet; |
| onSelectFrame: (tracklet: BaseTracklet, index: number) => void; |
| }; |
|
|
| function getSwimlaneSegments(masks: DatalessMask[]): SwimlineSegment[] { |
| if (masks.length === 0) { |
| return []; |
| } |
|
|
| const swimlineSegments: SwimlineSegment[] = []; |
| let left = -1; |
|
|
| for (let frameIndex = 0; frameIndex < masks.length; ++frameIndex) { |
| const isEmpty = masks?.[frameIndex]?.isEmpty ?? true; |
| if (left === -1 && !isEmpty) { |
| left = frameIndex; |
| } else if (left !== -1 && (isEmpty || frameIndex == masks.length - 1)) { |
| swimlineSegments.push({ |
| left, |
| width: frameIndex - left + 1, |
| }); |
| left = -1; |
| } |
| } |
|
|
| return swimlineSegments; |
| } |
|
|
| export default function TrackletSwimlane({tracklet, onSelectFrame}: Props) { |
| const selection = useSelectedFrameHelper(); |
|
|
| const segments = useMemo(() => { |
| return getSwimlaneSegments(tracklet.masks); |
| }, [tracklet.masks]); |
|
|
| const framesWithPoints = tracklet.points.reduce<number[]>( |
| (frames, pts, frameIndex) => { |
| if (pts != null && pts.length > 0) { |
| frames.push(frameIndex); |
| } |
| return frames; |
| }, |
| [], |
| ); |
|
|
| if (selection === null) { |
| return; |
| } |
|
|
| return ( |
| <div {...stylex.props(styles.container)}> |
| <div {...stylex.props(styles.trackletNameContainer)}> |
| Object {tracklet.id + 1} |
| </div> |
| <div {...stylex.props(styles.swimlaneContainer)}> |
| <div |
| {...stylex.props(styles.swimlane)} |
| style={{ |
| backgroundColor: tracklet.color, |
| }} |
| /> |
| {segments.map(segment => { |
| return ( |
| <div |
| key={segment.left} |
| {...stylex.props(styles.segment)} |
| style={{ |
| backgroundColor: tracklet.color, |
| left: selection.toPosition(segment.left), |
| width: selection.toPosition(segment.width), |
| }} |
| /> |
| ); |
| })} |
| {framesWithPoints.map(index => { |
| return ( |
| <div |
| key={`frame${index}`} |
| onClick={() => { |
| onSelectFrame?.(tracklet, index); |
| }} |
| {...stylex.props(styles.segmentationPoint)} |
| style={{ |
| left: Math.floor(selection.toPosition(index) - 4), |
| backgroundColor: tracklet.color, |
| }} |
| /> |
| ); |
| })} |
| </div> |
| </div> |
| ); |
| } |
|
|