Add clip archive function & optmize clip frame structor & clip details panel #50
@ -53,6 +53,7 @@ interface IStatusEvent {
|
||||
labelAttrChange: void;
|
||||
groupAttrChange: void;
|
||||
behaviorAttrChange: void;
|
||||
clipAttrChange: void;
|
||||
individualChange: void;
|
||||
behaviorChange: void;
|
||||
popupChange: void;
|
||||
@ -286,6 +287,18 @@ class Status extends Emitter<IStatusEvent> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改剪辑属性
|
||||
*/
|
||||
public changeClipAttrib<K extends keyof Clip>
|
||||
(id: ObjectID, key: K, val: Clip[K]) {
|
||||
const clip = this.model.getClipById(id);
|
||||
if (clip && clip instanceof Clip) {
|
||||
clip[key] = val;
|
||||
this.emit("clipAttrChange");
|
||||
}
|
||||
}
|
||||
|
||||
public addGroupBehavior(id: ObjectID, val: Behavior) {
|
||||
const group = this.model.getObjectById(id);
|
||||
if (group && group instanceof Group) {
|
||||
|
@ -56,6 +56,8 @@ const EN_US = {
|
||||
"Panel.Info.Behavior.Details.View": "Edit view Behavior attributes",
|
||||
"Panel.Title.Behavior.Clip.Player": "Recording",
|
||||
"Panel.Info.Behavior.Clip.Player": "Pre render recorded data",
|
||||
"Panel.Title.Behavior.Clip.Details": "Clip",
|
||||
"Panel.Info.Behavior.Clip.Details": "Edit view clip attributes",
|
||||
"Panel.Info.Behavior.Clip.Time.Formate": "{current} / {all} / {fps}fps",
|
||||
"Panel.Info.Behavior.Clip.Record.Formate": "Record: {time}",
|
||||
"Panel.Info.Behavior.Clip.Uname.Clip": "Waiting for recording...",
|
||||
@ -161,6 +163,7 @@ const EN_US = {
|
||||
"Panel.Info.Behavior.Details.Parameter.Key.Vec.Y": "{key} Y",
|
||||
"Panel.Info.Behavior.Details.Parameter.Key.Vec.Z": "{key} Z",
|
||||
"Panel.Info.Clip.List.Error.Nodata": "There is no clip, please click the record button to record, or click the plus sign to create",
|
||||
"Panel.Info.Clip.Details.Error.Nodata": "Specify a clip to view an attribute",
|
||||
"Info.Hint.Save.After.Close": "Any unsaved progress will be lost. Are you sure you want to continue?",
|
||||
"Info.Hint.Load.File.Title": "Load save",
|
||||
"Info.Hint.Load.File.Intro": "Release to load the dragged save file",
|
||||
|
@ -56,6 +56,8 @@ const ZH_CN = {
|
||||
"Panel.Info.Behavior.Details.View": "编辑查看行为属性",
|
||||
"Panel.Title.Behavior.Clip.Player": "录制",
|
||||
"Panel.Info.Behavior.Clip.Player": "预渲染录制数据",
|
||||
"Panel.Title.Behavior.Clip.Details": "剪辑",
|
||||
"Panel.Info.Behavior.Clip.Details": "编辑查看剪辑片段属性",
|
||||
"Panel.Info.Behavior.Clip.Time.Formate": "{current} / {all} / {fps} fps",
|
||||
"Panel.Info.Behavior.Clip.Record.Formate": "录制: {time}",
|
||||
"Panel.Info.Behavior.Clip.Uname.Clip": "等待录制...",
|
||||
@ -161,6 +163,7 @@ const ZH_CN = {
|
||||
"Panel.Info.Behavior.Details.Parameter.Key.Vec.Y": "{key} Y 坐标",
|
||||
"Panel.Info.Behavior.Details.Parameter.Key.Vec.Z": "{key} Z 坐标",
|
||||
"Panel.Info.Clip.List.Error.Nodata": "没有剪辑片段,请点击录制按钮录制,或者点击加号创建",
|
||||
"Panel.Info.Clip.Details.Error.Nodata": "请指定一个剪辑片段以查看属性",
|
||||
"Info.Hint.Save.After.Close": "任何未保存的进度都会丢失, 确定要继续吗?",
|
||||
"Info.Hint.Load.File.Title": "加载存档",
|
||||
"Info.Hint.Load.File.Intro": "释放以加载拽入的存档",
|
||||
|
@ -8,6 +8,7 @@ import { IArchiveIndividual, Individual } from "@Model/Individual";
|
||||
import { Behavior, IArchiveBehavior } from "@Model/Behavior";
|
||||
import { getBehaviorById } from "@Behavior/Behavior";
|
||||
import { IArchiveParseFn, IObjectParamArchiveType, IRealObjectType } from "@Model/Parameter";
|
||||
import { Clip, IArchiveClip } from "@Model/Clip";
|
||||
|
||||
interface IArchiveEvent {
|
||||
fileSave: Archive;
|
||||
@ -20,6 +21,7 @@ interface IArchiveObject {
|
||||
objectPool: IArchiveCtrlObject[];
|
||||
labelPool: IArchiveLabel[];
|
||||
behaviorPool: IArchiveBehavior[];
|
||||
clipPool: IArchiveClip[];
|
||||
}
|
||||
|
||||
class Archive extends Emitter<IArchiveEvent> {
|
||||
@ -51,7 +53,7 @@ class Archive extends Emitter<IArchiveEvent> {
|
||||
|
||||
// 存贮 CtrlObject
|
||||
const objectPool: IArchiveCtrlObject[] = [];
|
||||
model.objectPool.forEach(obj => {
|
||||
model.objectPool?.forEach(obj => {
|
||||
let archiveObject = obj.toArchive();
|
||||
|
||||
// 处理每个群的个体
|
||||
@ -60,7 +62,7 @@ class Archive extends Emitter<IArchiveEvent> {
|
||||
const group: Group = obj as Group;
|
||||
|
||||
const individuals: IArchiveIndividual[] = [];
|
||||
group.individuals.forEach((item) => {
|
||||
group.individuals?.forEach((item) => {
|
||||
individuals.push(item.toArchive());
|
||||
});
|
||||
|
||||
@ -72,22 +74,29 @@ class Archive extends Emitter<IArchiveEvent> {
|
||||
|
||||
// 存储 Label
|
||||
const labelPool: IArchiveLabel[] = [];
|
||||
model.labelPool.forEach(obj => {
|
||||
model.labelPool?.forEach(obj => {
|
||||
labelPool.push(obj.toArchive());
|
||||
});
|
||||
|
||||
// 存储全部行为
|
||||
const behaviorPool: IArchiveBehavior[] = [];
|
||||
model.behaviorPool.forEach(obj => {
|
||||
model.behaviorPool?.forEach(obj => {
|
||||
behaviorPool.push(obj.toArchive());
|
||||
});
|
||||
|
||||
// 存储全部剪辑片段
|
||||
const clipPool: IArchiveClip[] = [];
|
||||
model.clipPool?.forEach(obj => {
|
||||
clipPool.push(obj.toArchive());
|
||||
});
|
||||
|
||||
// 生成存档对象
|
||||
const fileData: IArchiveObject = {
|
||||
nextIndividualId: model.nextIndividualId,
|
||||
objectPool: objectPool,
|
||||
labelPool: labelPool,
|
||||
behaviorPool: behaviorPool
|
||||
behaviorPool: behaviorPool,
|
||||
clipPool: clipPool
|
||||
};
|
||||
|
||||
return JSON.stringify(fileData);
|
||||
@ -105,7 +114,7 @@ class Archive extends Emitter<IArchiveEvent> {
|
||||
// 实例化全部对象
|
||||
const objectPool: CtrlObject[] = [];
|
||||
const individualPool: Individual[] = [];
|
||||
archive.objectPool.forEach((obj) => {
|
||||
archive.objectPool?.forEach((obj) => {
|
||||
|
||||
let ctrlObject: CtrlObject | undefined = undefined;
|
||||
|
||||
@ -116,7 +125,7 @@ class Archive extends Emitter<IArchiveEvent> {
|
||||
|
||||
// 实例化全部个体
|
||||
const individuals: Array<Individual> = [];
|
||||
archiveGroup.individuals.forEach((item) => {
|
||||
archiveGroup.individuals?.forEach((item) => {
|
||||
const newIndividual = new Individual(newGroup);
|
||||
newIndividual.id = item.id;
|
||||
individuals.push(newIndividual);
|
||||
@ -140,7 +149,7 @@ class Archive extends Emitter<IArchiveEvent> {
|
||||
|
||||
// 实例化全部标签
|
||||
const labelPool: Label[] = [];
|
||||
archive.labelPool.forEach((item) => {
|
||||
archive.labelPool?.forEach((item) => {
|
||||
const newLabel = new Label(model);
|
||||
newLabel.id = item.id;
|
||||
labelPool.push(newLabel);
|
||||
@ -148,13 +157,21 @@ class Archive extends Emitter<IArchiveEvent> {
|
||||
|
||||
// 实例化全部行为
|
||||
const behaviorPool: Behavior[] = [];
|
||||
archive.behaviorPool.forEach((item) => {
|
||||
archive.behaviorPool?.forEach((item) => {
|
||||
const recorder = getBehaviorById(item.behaviorId);
|
||||
const newBehavior = recorder.new();
|
||||
newBehavior.id = item.id;
|
||||
behaviorPool.push(newBehavior);
|
||||
});
|
||||
|
||||
// 实例化全部剪辑
|
||||
const clipPool: Clip[] = [];
|
||||
archive.clipPool?.forEach((item) => {
|
||||
const newClip = new Clip(model);
|
||||
newClip.id = item.id;
|
||||
clipPool.push(newClip);
|
||||
});
|
||||
|
||||
// 内置标签集合
|
||||
const buildInLabel = [model.allGroupLabel, model.allRangeLabel, model.currentGroupLabel]
|
||||
|
||||
@ -238,6 +255,12 @@ class Archive extends Emitter<IArchiveEvent> {
|
||||
item.fromArchive(archive.behaviorPool[index], parseFunction);
|
||||
return item;
|
||||
});
|
||||
|
||||
// 加载剪辑
|
||||
model.clipPool = clipPool.map((item, index) => {
|
||||
item.fromArchive(archive.clipPool[index], parseFunction);
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2,6 +2,7 @@ import { IAnyObject, Model } from "@Model/Model";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { Group } from "@Model/Group";
|
||||
import { Range } from "@Model/Range";
|
||||
import { archiveObject2Parameter, IArchiveParseFn, parameter2ArchiveObject } from "@Model/Parameter";
|
||||
|
||||
interface IDrawCommand {
|
||||
type: "points" | "cube";
|
||||
@ -18,6 +19,13 @@ interface IFrame {
|
||||
process: number;
|
||||
}
|
||||
|
||||
interface IArchiveClip {
|
||||
id: string;
|
||||
time: number;
|
||||
name: string;
|
||||
frames: IFrame[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 剪辑片段
|
||||
*/
|
||||
@ -50,6 +58,127 @@ class Clip {
|
||||
*/
|
||||
public isRecording: boolean = false;
|
||||
|
||||
/**
|
||||
* 判断两个 RenderParameter 是否相同
|
||||
*/
|
||||
public isRenderParameterEqual(p1?: IAnyObject, p2?: IAnyObject, r: boolean = true): boolean {
|
||||
|
||||
if ((p1 && !p2) || (!p1 && p2)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!p1 && !p2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (let key in p1!) {
|
||||
|
||||
// 对象递归校验
|
||||
if (typeof p1[key] === "object" && !Array.isArray(p1[key])) {
|
||||
|
||||
if (!(typeof (p2 as any)[key] === "object" && !Array.isArray((p2 as any)[key]))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 递归校验
|
||||
if (r) {
|
||||
if (!this.isRenderParameterEqual(p1[key], (p2 as any)[key], false)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 浅校验
|
||||
else {
|
||||
if (p1[key] !== (p2 as any)[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 数组遍历校验
|
||||
else if (Array.isArray(p1[key])) {
|
||||
|
||||
if (!Array.isArray((p2 as any)[key])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let j = 0; j < p1[key].length; j++) {
|
||||
if (p1[key][j] !== (p2 as any)[key][j]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 数值直接校验
|
||||
else if (p1[key] !== (p2 as any)[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public cloneRenderParameter(p?: IAnyObject, res: IAnyObject = {}, r: boolean = true): IAnyObject | undefined {
|
||||
|
||||
if (!p) return undefined;
|
||||
|
||||
for (let key in p) {
|
||||
|
||||
// 对象递归克隆
|
||||
if (typeof p[key] === "object" && !Array.isArray(p[key]) && r) {
|
||||
this.cloneRenderParameter(p[key], res, false);
|
||||
}
|
||||
|
||||
// 数组克隆
|
||||
else if (Array.isArray(p[key])) {
|
||||
(res as any)[key] = p[key].concat([]);
|
||||
}
|
||||
|
||||
// 数值克隆
|
||||
else {
|
||||
(res as any)[key] = p[key];
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public isArrayEqual(a1?: number[], a2?: number[]): boolean {
|
||||
|
||||
if ((a1 && !a2) || (!a1 && a2)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!a1 && !a2) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (let i = 0; i < a1!.length; i++) {
|
||||
if (a1![i] !== a2![i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从上一帧获取指令数据
|
||||
*/
|
||||
public getCommandFromLastFrame(type: IDrawCommand["type"], id: string, frame?: IFrame): IDrawCommand | undefined {
|
||||
let lastCommand: IDrawCommand[] = (frame ?? this.frames[this.frames.length - 1])?.commands;
|
||||
|
||||
if (lastCommand) {
|
||||
for (let i = 0; i < lastCommand.length; i++) {
|
||||
if (type === lastCommand[i].type && id === lastCommand[i].id) {
|
||||
return lastCommand[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 录制一帧
|
||||
*/
|
||||
@ -62,23 +191,59 @@ class Clip {
|
||||
object.renderParameter.color = object.color;
|
||||
|
||||
if (object.display && object instanceof Group) {
|
||||
commands.push({
|
||||
|
||||
// 获取上一帧指令
|
||||
const lastCommand = this.getCommandFromLastFrame("points", object.id);
|
||||
|
||||
// 记录
|
||||
const recodeData: IDrawCommand = {
|
||||
type: "points",
|
||||
id: object.id,
|
||||
data: object.exportPositionData(),
|
||||
parameter: object.renderParameter
|
||||
});
|
||||
data: object.exportPositionData()
|
||||
}
|
||||
|
||||
// 对比校验
|
||||
if (this.isRenderParameterEqual(object.renderParameter, lastCommand?.parameter)) {
|
||||
recodeData.parameter = lastCommand?.parameter;
|
||||
} else {
|
||||
recodeData.parameter = this.cloneRenderParameter(object.renderParameter);
|
||||
}
|
||||
|
||||
commands.push(recodeData);
|
||||
}
|
||||
|
||||
|
||||
if (object.display && object instanceof Range) {
|
||||
commands.push({
|
||||
|
||||
// 获取上一帧指令
|
||||
const lastCommand = this.getCommandFromLastFrame("cube", object.id);
|
||||
|
||||
// 记录
|
||||
const recodeData: IDrawCommand = {
|
||||
type: "cube",
|
||||
id: object.id,
|
||||
position: object.position,
|
||||
radius: object.radius,
|
||||
parameter: object.renderParameter
|
||||
});
|
||||
id: object.id
|
||||
}
|
||||
|
||||
// 释放上一帧的内存
|
||||
if (this.isArrayEqual(object.position, lastCommand?.position)) {
|
||||
recodeData.position = lastCommand?.position;
|
||||
} else {
|
||||
recodeData.position = object.position.concat([]);
|
||||
}
|
||||
|
||||
if (this.isArrayEqual(object.radius, lastCommand?.radius)) {
|
||||
recodeData.radius = lastCommand?.radius;
|
||||
} else {
|
||||
recodeData.radius = object.radius.concat([]);
|
||||
}
|
||||
|
||||
if (this.isRenderParameterEqual(object.renderParameter, lastCommand?.parameter)) {
|
||||
recodeData.parameter = lastCommand?.parameter;
|
||||
} else {
|
||||
recodeData.parameter = this.cloneRenderParameter(object.renderParameter);
|
||||
}
|
||||
|
||||
commands.push(recodeData);
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,6 +260,110 @@ class Clip {
|
||||
return frame;
|
||||
}
|
||||
|
||||
public readonly LastFrameData: "@L" = "@L";
|
||||
|
||||
/**
|
||||
* 压缩帧数据
|
||||
*/
|
||||
public compressed(): IFrame[] {
|
||||
const resFrame: IFrame[] = [];
|
||||
|
||||
for (let i = 0; i < this.frames.length; i++) {
|
||||
const commands = this.frames[i].commands;
|
||||
const res: IDrawCommand[] = [];
|
||||
|
||||
// 处理指令
|
||||
for (let j = 0; j < commands.length; j++) {
|
||||
|
||||
// 压缩指令
|
||||
const command: IDrawCommand = {
|
||||
id: commands[j].id,
|
||||
type: commands[j].type
|
||||
};
|
||||
|
||||
// 搜索上一帧相同指令
|
||||
const lastCommand = this.frames[i - 1] ?
|
||||
this.getCommandFromLastFrame(command.type, command.id, this.frames[i - 1]) :
|
||||
undefined;
|
||||
|
||||
// 记录
|
||||
command.data = (lastCommand?.data === commands[j].data) ?
|
||||
this.LastFrameData as any : Array.from(commands[j].data ?? []);
|
||||
|
||||
command.position = (lastCommand?.position === commands[j].position) ?
|
||||
this.LastFrameData as any : commands[j].position?.concat([]);
|
||||
|
||||
command.radius = (lastCommand?.radius === commands[j].radius) ?
|
||||
this.LastFrameData as any : commands[j].radius?.concat([]);
|
||||
|
||||
command.parameter = (lastCommand?.parameter === commands[j].parameter) ?
|
||||
this.LastFrameData as any : parameter2ArchiveObject(commands[j].parameter as any);
|
||||
|
||||
res.push(command);
|
||||
}
|
||||
|
||||
resFrame.push({
|
||||
duration: this.frames[i].duration,
|
||||
process: this.frames[i].process,
|
||||
commands: res
|
||||
})
|
||||
}
|
||||
|
||||
return resFrame;
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载压缩帧数据
|
||||
*/
|
||||
public uncompressed(frames: IFrame[], paster: IArchiveParseFn): IFrame[] {
|
||||
const resFrame: IFrame[] = [];
|
||||
|
||||
for (let i = 0; i < frames.length; i++) {
|
||||
const commands = frames[i].commands;
|
||||
const res: IDrawCommand[] = [];
|
||||
|
||||
// 处理指令
|
||||
for (let j = 0; j < commands.length; j++) {
|
||||
|
||||
// 压缩指令
|
||||
const command: IDrawCommand = {
|
||||
id: commands[j].id,
|
||||
type: commands[j].type
|
||||
};
|
||||
|
||||
// 搜索上一帧相同指令
|
||||
const lastCommand = resFrame[resFrame.length - 1] ?
|
||||
this.getCommandFromLastFrame(command.type, command.id, resFrame[resFrame.length - 1]) :
|
||||
undefined;
|
||||
|
||||
console.log(lastCommand);
|
||||
|
||||
// 记录
|
||||
command.data = (this.LastFrameData as any === commands[j].data) ?
|
||||
lastCommand?.data : new Float32Array(commands[j].data ?? []);
|
||||
|
||||
command.position = (this.LastFrameData as any === commands[j].position) ?
|
||||
lastCommand?.position : commands[j].position;
|
||||
|
||||
command.radius = (this.LastFrameData as any === commands[j].radius) ?
|
||||
lastCommand?.radius : commands[j].radius;
|
||||
|
||||
command.parameter = (this.LastFrameData as any === commands[j].parameter) ?
|
||||
lastCommand?.parameter : archiveObject2Parameter(commands[j].parameter as any, paster);
|
||||
|
||||
res.push(command);
|
||||
}
|
||||
|
||||
resFrame.push({
|
||||
duration: frames[i].duration,
|
||||
process: frames[i].process,
|
||||
commands: res
|
||||
})
|
||||
}
|
||||
|
||||
return resFrame;
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放一帧
|
||||
*/
|
||||
@ -125,6 +394,22 @@ class Clip {
|
||||
this.model = model;
|
||||
this.id = uuid();
|
||||
}
|
||||
|
||||
public toArchive(): IArchiveClip {
|
||||
return {
|
||||
id: this.id,
|
||||
time: this.time,
|
||||
name: this.name,
|
||||
frames: this.compressed()
|
||||
};
|
||||
}
|
||||
|
||||
public fromArchive(archive: IArchiveClip, paster: IArchiveParseFn): void {
|
||||
this.id = archive.id,
|
||||
this.time = archive.time,
|
||||
this.name = archive.name,
|
||||
this.frames = this.uncompressed(archive.frames, paster);
|
||||
}
|
||||
}
|
||||
|
||||
export { Clip, IFrame };
|
||||
export { Clip, IFrame, IArchiveClip };
|
@ -349,6 +349,14 @@ class Model extends Emitter<ModelEvent> {
|
||||
return newClip;
|
||||
}
|
||||
|
||||
public getClipById(id: ObjectID): Clip | undefined {
|
||||
for (let i = 0; i < this.clipPool.length; i++) {
|
||||
if (this.clipPool[i].id.toString() === id.toString()) {
|
||||
return this.clipPool[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除一个剪辑片段
|
||||
*/
|
||||
|
@ -64,7 +64,7 @@ class SimulatorDesktop extends Component {
|
||||
items: [
|
||||
{panels: ["RenderView"]},
|
||||
{
|
||||
items: [{panels: ["BehaviorList"]}, {panels: ["LabelList"]}],
|
||||
items: [{panels: ["BehaviorList", "ClipPlayer"]}, {panels: ["LabelList"]}],
|
||||
scale: 80,
|
||||
layout: LayoutDirection.X
|
||||
}
|
||||
@ -76,7 +76,7 @@ class SimulatorDesktop extends Component {
|
||||
items: [{
|
||||
panels: ["ObjectList"]
|
||||
}, {
|
||||
panels: ["GroupDetails", "RangeDetails", "LabelDetails", "BehaviorDetails"]
|
||||
panels: ["GroupDetails", "RangeDetails", "LabelDetails", "BehaviorDetails", "ClipDetails"]
|
||||
}],
|
||||
scale: 30,
|
||||
layout: LayoutDirection.Y
|
||||
|
@ -55,7 +55,7 @@ class SimulatorWeb extends Component {
|
||||
items: [
|
||||
{panels: ["RenderView"]},
|
||||
{
|
||||
items: [{panels: ["ClipPlayer", "BehaviorList"]}, {panels: ["LabelList"]}],
|
||||
items: [{panels: ["BehaviorList", "ClipPlayer"]}, {panels: ["LabelList"]}],
|
||||
scale: 80,
|
||||
layout: LayoutDirection.X
|
||||
}
|
||||
@ -67,7 +67,7 @@ class SimulatorWeb extends Component {
|
||||
items: [{
|
||||
panels: ["ObjectList"]
|
||||
}, {
|
||||
panels: ["GroupDetails", "RangeDetails", "LabelDetails", "BehaviorDetails"]
|
||||
panels: ["GroupDetails", "RangeDetails", "LabelDetails", "BehaviorDetails", "ClipDetails"]
|
||||
}],
|
||||
scale: 30,
|
||||
layout: LayoutDirection.Y
|
||||
|
0
source/Panel/ClipDetails/ClipDetails.scss
Normal file
0
source/Panel/ClipDetails/ClipDetails.scss
Normal file
60
source/Panel/ClipDetails/ClipDetails.tsx
Normal file
60
source/Panel/ClipDetails/ClipDetails.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import { Component, ReactNode } from "react";
|
||||
import { useStatusWithEvent, IMixinStatusProps } from "@Context/Status";
|
||||
import { TogglesInput } from "@Input/TogglesInput/TogglesInput";
|
||||
import { ConfirmPopup } from "@Component/ConfirmPopup/ConfirmPopup";
|
||||
import { AttrInput } from "@Input/AttrInput/AttrInput";
|
||||
import { Message } from "@Input/Message/Message";
|
||||
import { Clip } from "@Model/Clip";
|
||||
import "./ClipDetails.scss";
|
||||
|
||||
@useStatusWithEvent("focusClipChange", "clipAttrChange")
|
||||
class ClipDetails extends Component<IMixinStatusProps> {
|
||||
|
||||
private renderFrom(clip: Clip) {
|
||||
return <>
|
||||
|
||||
<Message i18nKey="Common.Attr.Title.Basic" isTitle first/>
|
||||
|
||||
<AttrInput
|
||||
keyI18n="Common.Attr.Key.Display.Name"
|
||||
maxLength={15}
|
||||
value={clip.name}
|
||||
valueChange={(value) => {
|
||||
if (this.props.status) {
|
||||
this.props.status.changeClipAttrib(clip.id, "name", value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<TogglesInput
|
||||
keyI18n="Common.Attr.Key.Delete" onIconName="delete" red
|
||||
offIconName="delete" valueChange={() => {
|
||||
const status = this.props.status;
|
||||
if (status) {
|
||||
status.popup.showPopup(ConfirmPopup, {
|
||||
infoI18n: "Popup.Delete.Clip.Confirm",
|
||||
titleI18N: "Popup.Action.Objects.Confirm.Title",
|
||||
yesI18n: "Popup.Action.Objects.Confirm.Delete",
|
||||
red: "yes",
|
||||
yes: () => {
|
||||
status.setClipObject();
|
||||
this.props.status?.actuator.endPlay();
|
||||
status.model.deleteClip(clip.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
</>;
|
||||
}
|
||||
|
||||
public render(): ReactNode {
|
||||
if (this.props.status && this.props.status.focusClip) {
|
||||
return this.renderFrom(this.props.status.focusClip);
|
||||
}
|
||||
return <Message i18nKey="Panel.Info.Clip.Details.Error.Nodata"/>;
|
||||
}
|
||||
}
|
||||
|
||||
export { ClipDetails };
|
@ -1,6 +1,7 @@
|
||||
import { Component, ReactNode } from "react";
|
||||
import { ClipList } from "@Component/ClipList/ClipList";
|
||||
import { useStatusWithEvent, IMixinStatusProps } from "@Context/Status";
|
||||
import { useSetting, IMixinSettingProps } from "@Context/Setting";
|
||||
import { BackgroundLevel, FontLevel, Theme } from "@Component/Theme/Theme";
|
||||
import { Message } from "@Input/Message/Message";
|
||||
import { Clip } from "@Model/Clip";
|
||||
@ -9,8 +10,9 @@ import { ConfirmPopup } from "@Component/ConfirmPopup/ConfirmPopup";
|
||||
import { OfflineRender } from "@Component/OfflineRender/OfflineRender"
|
||||
import "./ClipPlayer.scss";
|
||||
|
||||
@useStatusWithEvent("clipChange", "focusClipChange", "actuatorStartChange")
|
||||
class ClipPlayer extends Component<IMixinStatusProps> {
|
||||
@useSetting
|
||||
@useStatusWithEvent("clipChange", "focusClipChange", "actuatorStartChange", "clipAttrChange")
|
||||
class ClipPlayer extends Component<IMixinStatusProps & IMixinSettingProps> {
|
||||
|
||||
private isInnerClick: boolean = false;
|
||||
|
||||
@ -57,6 +59,7 @@ class ClipPlayer extends Component<IMixinStatusProps> {
|
||||
this.isInnerClick = true;
|
||||
this.props.status?.setClipObject(clip);
|
||||
this.props.status?.actuator.startPlay(clip);
|
||||
this.props.setting?.layout.focus("ClipDetails");
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { useStatusWithEvent, IMixinStatusProps } from "@Context/Status";
|
||||
import { Recorder } from "@Component/Recorder/Recorder";
|
||||
import { ActuatorModel } from "@Model/Actuator";
|
||||
|
||||
@useStatusWithEvent("actuatorStartChange", "focusClipChange", "recordLoop")
|
||||
@useStatusWithEvent("actuatorStartChange", "focusClipChange", "recordLoop", "clipAttrChange")
|
||||
class ClipRecorder extends Component<IMixinStatusProps> {
|
||||
public render(): ReactNode {
|
||||
|
||||
|
@ -12,6 +12,7 @@ import { BehaviorList } from "@Panel/BehaviorList/BehaviorList";
|
||||
import { BehaviorDetails } from "@Panel/BehaviorDetails/BehaviorDetails";
|
||||
import { ClipPlayer } from "@Panel/ClipPlayer/ClipPlayer";
|
||||
import { ClipRecorder } from "@Panel/ClipPlayer/ClipRecorder";
|
||||
import { ClipDetails } from "@Panel/ClipDetails/ClipDetails";
|
||||
|
||||
interface IPanelInfo {
|
||||
nameKey: string;
|
||||
@ -34,6 +35,7 @@ type PanelId = ""
|
||||
| "BehaviorList" // 行为列表
|
||||
| "BehaviorDetails" // 行为属性
|
||||
| "ClipPlayer" // 剪辑影片
|
||||
| "ClipDetails" // 剪辑详情
|
||||
;
|
||||
|
||||
const PanelInfoMap = new Map<PanelId, IPanelInfo>();
|
||||
@ -73,6 +75,10 @@ PanelInfoMap.set("ClipPlayer", {
|
||||
nameKey: "Panel.Title.Behavior.Clip.Player", introKay: "Panel.Info.Behavior.Clip.Player",
|
||||
class: ClipPlayer, header: ClipRecorder, hidePadding: true
|
||||
});
|
||||
PanelInfoMap.set("ClipDetails", {
|
||||
nameKey: "Panel.Title.Behavior.Clip.Details", introKay: "Panel.Info.Behavior.Clip.Details",
|
||||
class: ClipDetails
|
||||
});
|
||||
|
||||
function getPanelById(panelId: PanelId): ReactNode {
|
||||
switch (panelId) {
|
||||
|
Loading…
Reference in New Issue
Block a user