import { types } from "mobx-state-tree";
|
|
import { guidGenerator } from "../core/Helpers";
|
import { AreaMixin } from "../mixins/AreaMixin";
|
import { AnnotationMixin } from "../mixins/AnnotationMixin";
|
import NormalizationMixin from "../mixins/Normalization";
|
import RegionsMixin from "../mixins/Regions";
|
import { VideoModel } from "../tags/object/Video";
|
|
export const onlyProps = (props, obj) => {
|
return Object.fromEntries(props.map((prop) => [prop, obj[prop]]));
|
};
|
|
const Model = types
|
.model("VideoRegionModel", {
|
id: types.optional(types.identifier, guidGenerator),
|
pid: types.optional(types.string, guidGenerator),
|
object: types.late(() => types.reference(VideoModel)),
|
|
sequence: types.frozen([]),
|
})
|
.preProcessSnapshot((snapshot) => {
|
return { ...snapshot, sequence: snapshot.sequence || snapshot.value.sequence };
|
})
|
.volatile(() => ({
|
hideable: true,
|
}))
|
.views((self) => ({
|
get parent() {
|
return self.object;
|
},
|
|
getShape() {
|
throw new Error("Method getShape be implemented on a shape level");
|
},
|
|
getVisibility() {
|
return true;
|
},
|
}))
|
.actions((self) => ({
|
updateShape() {
|
throw new Error("Method updateShape must be implemented on a shape level");
|
},
|
|
onSelectInOutliner() {
|
// skip video to the first frame of this region
|
// @todo hidden/disabled timespans?
|
self.object.setFrame(self.sequence[0].frame);
|
},
|
|
serialize() {
|
const { framerate, length: framesCount } = self.object;
|
|
const duration = self.object?.ref?.current?.duration ?? 0;
|
|
const value = {
|
framesCount,
|
duration,
|
sequence: self.sequence.map((keyframe) => {
|
return { ...keyframe, time: keyframe.frame / framerate };
|
}),
|
};
|
|
return { value };
|
},
|
|
toggleLifespan(frame) {
|
const keypoint = self.closestKeypoint(frame, true);
|
|
if (keypoint) {
|
const index = self.sequence.indexOf(keypoint);
|
|
self.sequence = [
|
...self.sequence.slice(0, index),
|
{ ...keypoint, enabled: !keypoint.enabled },
|
...self.sequence.slice(index + 1),
|
];
|
}
|
},
|
|
addKeypoint(frame) {
|
const sequence = Array.from(self.sequence);
|
const closestKeypoint = self.closestKeypoint(frame);
|
const newKeypoint = {
|
...(self.getShape(frame) ??
|
closestKeypoint ?? {
|
x: 0,
|
y: 0,
|
}),
|
enabled: closestKeypoint?.enabled ?? true,
|
frame,
|
};
|
|
sequence.push(newKeypoint);
|
|
sequence.sort((a, b) => a.frame - b.frame);
|
|
self.sequence = sequence;
|
|
self.updateShape(
|
{
|
...newKeypoint,
|
},
|
newKeypoint.frame,
|
);
|
},
|
|
removeKeypoint(frame) {
|
self.sequence = self.sequence.filter((closestKeypoint) => closestKeypoint.frame !== frame);
|
},
|
|
isInLifespan(targetFrame) {
|
const closestKeypoint = self.closestKeypoint(targetFrame);
|
|
if (closestKeypoint) {
|
const { enabled, frame } = closestKeypoint;
|
|
if (frame === targetFrame && !enabled) return true;
|
return enabled;
|
}
|
return false;
|
},
|
|
closestKeypoint(targetFrame, onlyPrevious = false) {
|
const seq = self.sequence;
|
let result;
|
|
const keypoints = seq.filter(({ frame }) => frame <= targetFrame);
|
|
result = keypoints[keypoints.length - 1];
|
|
if (!result && onlyPrevious !== true) {
|
result = seq.find(({ frame }) => frame >= targetFrame);
|
}
|
|
return result;
|
},
|
}));
|
|
const VideoRegion = types.compose(
|
"VideoRegionModel",
|
RegionsMixin,
|
AreaMixin,
|
AnnotationMixin,
|
NormalizationMixin,
|
Model,
|
);
|
|
export { VideoRegion };
|