import { isAlive, types } from "mobx-state-tree";
|
|
import Registry from "../core/Registry";
|
import { AreaMixin } from "../mixins/AreaMixin";
|
import NormalizationMixin from "../mixins/Normalization";
|
import RegionsMixin from "../mixins/Regions";
|
import { VideoModel } from "../tags/object/Video/Video";
|
import { isDefined } from "../utils/utilities";
|
import { EditableRegion } from "./EditableRegion";
|
|
const TimelineRange = types.model("TimelineRange", {
|
start: types.maybeNull(types.integer),
|
end: types.maybeNull(types.integer),
|
});
|
|
// convert range to internal video timeline format
|
// it's used in `flatMap()`, so it can return both object and array of objects
|
function rangeToSequence(range) {
|
const { start, end } = range;
|
|
if (!isDefined(start)) {
|
if (!isDefined(end)) return [];
|
return { frame: end, enabled: false };
|
}
|
if (!isDefined(end)) {
|
return { frame: start, enabled: true };
|
}
|
if (start === end) {
|
return { frame: start, enabled: false };
|
}
|
return [
|
{
|
frame: start,
|
enabled: true,
|
},
|
{
|
frame: end,
|
enabled: false,
|
},
|
];
|
}
|
|
/**
|
* TimelineRegion, a region on the video timeline.
|
* @see Timeline/Views/Frames#onFrameScrub() - this method creates a region by drawing on the timeline
|
*/
|
const Model = types
|
.model("TimelineRegionModel", {
|
type: "timelineregion",
|
object: types.late(() => types.reference(VideoModel)),
|
|
ranges: types.array(TimelineRange),
|
})
|
.volatile(() => ({
|
hideable: true,
|
editableFields: [
|
{ property: "start", label: "Start frame" },
|
{ property: "end", label: "End frame" },
|
],
|
}))
|
.views((self) => ({
|
get parent() {
|
return isAlive(self) ? self.object : null;
|
},
|
get sequence() {
|
return self.ranges.flatMap(rangeToSequence);
|
},
|
getShape() {
|
return null;
|
},
|
}))
|
.actions((self) => ({
|
/**
|
* @example
|
* {
|
* "value": {
|
* "ranges": [{"start": 3, "end": 5}],
|
* "timelinelabels": ["Moving"]
|
* }
|
* }
|
* @typedef {Object} TimelineRegionResult
|
* @property {Object} value
|
* @property {object[]} value.ranges Array of ranges, each range is an object with `start` and `end` properties. One range per region.
|
* @property {string[]} [value.timelinelabels] Regions are created by `TimelineLabels`, and the corresponding label is listed here.
|
*/
|
|
onSelectInOutliner() {
|
// skip video to the first frame of this region
|
// @todo hidden/disabled timespans?
|
self.object.setFrame(self.ranges[0].start);
|
},
|
|
/**
|
* @return {TimelineRegionResult}
|
*/
|
serialize() {
|
return {
|
value: {
|
ranges: self.ranges,
|
},
|
};
|
},
|
isInLifespan(targetFrame) {
|
return true;
|
},
|
/**
|
* Set range for the region, only one frame for now,
|
* could be extended to multiple frames in a future in a form of (...ranges)
|
* @param {number[]} [start, end] Start and end frames
|
* @param {Object} [options]
|
* @param {"new" | "edit" | undefined} [options.mode] Do we dynamically change the region ("new" one or "edit" existing one) or just edit it precisely (undefined)?
|
* In first two cases we need to update undo history only once
|
*/
|
setRange([start, end], { mode } = {}) {
|
if (self.locked) return;
|
if (mode === "new") {
|
// we need to update existing history item while drawing a new region
|
self.parent.annotation.history.setReplaceNextUndoState();
|
} else if (mode === "edit") {
|
// we need to skip updating history item while editing existing region and record the state when we finish editing
|
/** @see Video#finishDrawing() */
|
self.parent.annotation.history.setSkipNextUndoState();
|
}
|
self.ranges = [{ start, end }];
|
},
|
}));
|
|
const TimelineRegionModel = types.compose(
|
"TimelineRegionModel",
|
RegionsMixin,
|
AreaMixin,
|
NormalizationMixin,
|
EditableRegion,
|
Model,
|
);
|
|
Registry.addRegionType(TimelineRegionModel, "video");
|
|
export { TimelineRegionModel };
|