/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ 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' /* 4px */, '@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( (frames, pts, frameIndex) => { if (pts != null && pts.length > 0) { frames.push(frameIndex); } return frames; }, [], ); if (selection === null) { return; } return (
Object {tracklet.id + 1}
{segments.map(segment => { return (
); })} {framesWithPoints.map(index => { return (
{ onSelectFrame?.(tracklet, index); }} {...stylex.props(styles.segmentationPoint)} style={{ left: Math.floor(selection.toPosition(index) - 4), backgroundColor: tracklet.color, }} /> ); })}
); }