Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: LEAP-1599: Allow to edit TimelineRegion spans #6577

Merged
merged 12 commits into from
Nov 6, 2024
1 change: 1 addition & 0 deletions web/libs/editor/src/assets/icons/timeline/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ export { ReactComponent as IconConfig } from "./config.svg";
export { ReactComponent as IconSoundConfig } from "./sound.svg";
export { ReactComponent as IconSoundMutedConfig } from "./sound_muted.svg";
export { ReactComponent as IconInfoConfig } from "./info.svg";
export { ReactComponent as IconTimelineRegion } from "./region.svg";
3 changes: 3 additions & 0 deletions web/libs/editor/src/assets/icons/timeline/region.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions web/libs/editor/src/components/Node/Node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
IconText,
IconWarning,
} from "../../assets/icons";
import { IconTimelineRegion } from "../../assets/icons/timeline";
import { NodeView } from "./NodeView";
import { Tooltip } from "../../common/Tooltip/Tooltip";

Expand Down Expand Up @@ -114,6 +115,11 @@ const NodeViews = {
name: "Input",
icon: MessageOutlined,
}),

TimelineRegionModel: NodeView({
name: "Timeline Span",
icon: IconTimelineRegion,
}),
};

const NodeDebug: FC<any> = observer(({ className, node }) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,6 @@
import { observe } from "mobx";
import { observer } from "mobx-react";
import {
getType,
type IAnyType,
isLiteralType,
isOptionalType,
isPrimitiveType,
isUnionType,
types,
} from "mobx-state-tree";
import { type IAnyType, isLiteralType, isOptionalType, isPrimitiveType, isUnionType, types } from "mobx-state-tree";
import {
type ChangeEvent,
type FC,
Expand All @@ -20,15 +12,16 @@ import {
useMemo,
useState,
} from "react";
import { Checkbox } from "@humansignal/ui";
import { IconPropertyAngle } from "../../../assets/icons";
import { Block, Elem, useBEM } from "../../../utils/bem";
import "./RegionEditor.scss";
import { TimeDurationControl } from "../../TimeDurationControl/TimeDurationControl";
import { FF_DEV_2715, isFF } from "../../../utils/feature-flags";
import { Checkbox } from "@humansignal/ui";
import { TimeDurationControl } from "../../TimeDurationControl/TimeDurationControl";
import { TimelineRegionEditor } from "./TimelineRegionEditor";
import "./RegionEditor.scss";

interface RegionEditorProps {
region: any;
region: MSTRegion;
}

const getPrimitiveType = (type: IAnyType) => {
Expand Down Expand Up @@ -59,21 +52,24 @@ const IconMapping = {
};

const RegionEditorComponent: FC<RegionEditorProps> = ({ region }) => {
const fields: any[] = region.editableFields ?? [];
const isAudioModel = getType(region).name === "AudioRegionModel";
const isAudioRegion = isFF(FF_DEV_2715) && region.type === "audioregion";
const isTimelineRegion = region.type === "timelineregion";
const Component = isTimelineRegion ? TimelineRegionEditor : isAudioRegion ? AudioRegionProperties : RegionProperties;

const changeStartTimeHandler = (value: number) => {
region.setProperty("start", value);
};
return (
<Block name="region-editor" mod={{ disabled: region.isReadOnly() }}>
<Component region={region} />
</Block>
);
};

const changeEndTimeHandler = (value: number) => {
region.setProperty("end", value);
};
const RegionProperties = ({ region }: RegionEditorProps) => {
const fields = region.editableFields ?? [];

const renderRegionProperty = () => (
return (
<Elem name="wrapper">
{region.editorEnabled &&
fields.map((field: any, i) => {
fields.map((field, i) => {
return (
<RegionProperty
key={`${field.property}-${i}`}
Expand All @@ -85,46 +81,46 @@ const RegionEditorComponent: FC<RegionEditorProps> = ({ region }) => {
})}
</Elem>
);
};

const renderAudioTimeControls = () => {
return (
<Elem name="wrapper-time-control">
<TimeDurationControl
startTime={region.start}
endTime={region.end}
minTime={0}
maxTime={region?._ws_region?.duration}
isSidepanel={true}
onChangeStartTime={changeStartTimeHandler}
onChangeEndTime={changeEndTimeHandler}
showLabels
showDuration
/>
</Elem>
);
const AudioRegionProperties = ({ region }: { region: any }) => {
const changeStartTimeHandler = (value: number) => {
region.setProperty("start", value);
};

const changeEndTimeHandler = (value: number) => {
region.setProperty("end", value);
};

return (
<Block name="region-editor" mod={{ disabled: region.isReadOnly() }}>
{isAudioModel && isFF(FF_DEV_2715) ? renderAudioTimeControls() : renderRegionProperty()}
</Block>
<Elem name="wrapper-time-control">
<TimeDurationControl
startTime={region.start}
endTime={region.end}
minTime={0}
maxTime={region?._ws_region?.duration}
isSidepanel={true}
onChangeStartTime={changeStartTimeHandler}
onChangeEndTime={changeEndTimeHandler}
showLabels
showDuration
/>
</Elem>
);
};

interface RegionPropertyProps {
property: string;
label: string;
region: any;
region: MSTRegion;
}

const RegionProperty: FC<RegionPropertyProps> = ({ property, label, region }) => {
const block = useBEM();
const [value, setValue] = useState(region.getProperty(property));

const propertyType = useMemo(() => {
const regionType = getType(region);

return (regionType as any).properties[property];
return region.getPropertyType(property);
}, [region, property]);

const isPrimitive = useMemo(() => {
Expand All @@ -134,7 +130,7 @@ const RegionProperty: FC<RegionPropertyProps> = ({ property, label, region }) =>
const options = useMemo(() => {
if (isPrimitive) return null;

let result: any[] | null = null;
let result: string[] | null = null;
const isEnum = isUnionType(propertyType);

if (isEnum) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
.container {
display: grid;
width: 100%;
grid-gap: 4px;
grid-template-columns: repeat(var(--col-count), minmax(0, 1fr));
}

.labelText {
font-size: 12px;
opacity: 0.5;
}

.input {
display: block;
border: 1px solid var(--sand_300);
border-radius: 4px;
padding: 0 0 0 8px;
width: 80px;
font-size: 14px;
text-align: right;

&[readonly] {
background-color: var(--sand_100);
color: var(--sand_600);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { observer } from "mobx-react";
import styles from "./TimelineRegionEditor.module.scss";

export const TimelineRegionEditor = observer(({ region }: { region: any }) => {
const { start, end } = region.ranges[0];
const length = region.object.length;

const changeStartTimeHandler = (value: number) => {
if (+value === region.ranges[0].start) return;
region.setRanges([+value, region.ranges[0].end]);
};

const changeEndTimeHandler = (value: number) => {
if (+value === region.ranges[0].end) return;
region.setRanges([region.ranges[0].start, +value]);
};

return (
<div className={styles.container}>
<Field label="Start frame" value={start} onChange={changeStartTimeHandler} region={region} min={1} max={end} />
<Field label="End frame" value={end} onChange={changeEndTimeHandler} region={region} min={start} max={length} />
<Field label="Duration" value={end - start + 1} region={region} />
</div>
);
});

type FieldProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, "onChange"> & {
label: string;
value: number;
onChange?: (value: number) => void;
region: any;
};

const Field = ({ label, value: originalValue, onChange: saveValue, region, min, max, ...rest }: FieldProps) => {
const readonly = !saveValue;

const onKeyDown = (e) => {
if (e.key === "Enter") {
e.currentTarget.blur();
}
};

const onChange = (e) => {
let value = +e.target.value;
if (min && value < +min) {
e.target.value = min;
value = +min;
}
if (max && value > +max) {
e.target.value = max;
value = +max;
}
saveValue?.(value);
};

return (
<label className={styles.label}>
<span className={styles.labelText}>{label}</span>
<input
className={styles.input}
type="number"
step={1}
readOnly={readonly}
onBlur={onChange}
onClick={onChange} // to handle clicks on +/- buttons
onKeyDown={onKeyDown}
// readonly field should be controlled to update value on region change.
// editable field is not controlled to not validate value on typing.
{...{ [readonly ? "value" : "defaultValue"]: originalValue }}
min={min}
max={max}
{...rest}
/>
</label>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,12 @@
transform: translate3d(-100%, -50%, 0);
}

&_timeline &__point:last-child {
&_timeline &__point_last {
transform: translate3d(0, -50%, 0);
}

/** instant */
&_timeline &__point:first-child:last-child {
&_timeline &__lifespan_instant &__point {
width: 6px;
transform: translate3d(-50%, -50%, 0);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,11 @@ const LifespanItem: FC<LifespanItemProps> = memo(
}, [left, right, finalWidth]);

return (
<Elem name="lifespan" mod={{ hidden: !visible }} style={style}>
<Elem name="lifespan" mod={{ hidden: !visible, instant: !width }} style={style}>
{points.map((frame, i) => {
const left = (frame - start) * step;

return <Elem key={i} name="point" style={{ left }} />;
return <Elem key={i} name="point" style={{ left }} mod={{ last: !!left }} />;
})}
</Elem>
);
Expand Down
6 changes: 5 additions & 1 deletion web/libs/editor/src/regions/EditableRegion.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { types } from "mobx-state-tree";
import { getType, types } from "mobx-state-tree";

export const EditableRegion = types
.model("EditableRegion")
Expand All @@ -17,6 +17,10 @@ export const EditableRegion = types
return self[name];
},

getPropertyType(name) {
return getType(self).properties[name];
},

isPropertyEditable(name) {
return self.editableFields.some((f) => f.property === name);
},
Expand Down
14 changes: 13 additions & 1 deletion web/libs/editor/src/regions/TimelineRegion.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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),
Expand Down Expand Up @@ -48,6 +49,10 @@ const Model = types
})
.volatile(() => ({
hideable: true,
editableFields: [
{ property: "start", label: "Start frame" },
{ property: "end", label: "End frame" },
],
}))
.views((self) => ({
get parent() {
Expand Down Expand Up @@ -100,7 +105,14 @@ const Model = types
},
}));

const TimelineRegionModel = types.compose("TimelineRegionModel", RegionsMixin, AreaMixin, NormalizationMixin, Model);
const TimelineRegionModel = types.compose(
"TimelineRegionModel",
RegionsMixin,
AreaMixin,
NormalizationMixin,
EditableRegion,
Model,
);

Registry.addRegionType(TimelineRegionModel, "video");

Expand Down
19 changes: 18 additions & 1 deletion web/libs/editor/src/stores/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ type MixinMSTRegion = {
dynamic: boolean;
origin: "prediction" | "prediction-changed" | "manual";
item_index: number | null;
type: string;
isReadOnly: () => boolean;
};

type MixinMSTRegionVolatile = {
Expand All @@ -59,7 +61,22 @@ type MixinMSTRegionVolatile = {
drawingTimeout: null;
};

type MSTRegion = MixinMSTArea & MixinMSTRegion & MixinMSTRegionVolatile;
type MSTEditableRegionPropertyDefinition = {
property: string;
label: string;
};

type MSTEditableRegion = {
editorEnabled: boolean;
editableFields: MSTEditableRegionPropertyDefinition[];
hasEditableFields: boolean;
getProperty: (string) => any;
getPropertyType: (string) => any;
isPropertyEditable: (string) => boolean;
setProperty: (string, any) => void;
};

type MSTRegion = MixinMSTArea & MixinMSTRegion & MixinMSTRegionVolatile & MSTEditableRegion;

type MSTAnnotation = {
id: string;
Expand Down
Loading