diff --git a/source/Model/Archive.ts b/source/Model/Archive.ts index 92688a1..e9e102e 100644 --- a/source/Model/Archive.ts +++ b/source/Model/Archive.ts @@ -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 { @@ -51,7 +53,7 @@ class Archive extends Emitter { // 存贮 CtrlObject const objectPool: IArchiveCtrlObject[] = []; - model.objectPool.forEach(obj => { + model.objectPool?.forEach(obj => { let archiveObject = obj.toArchive(); // 处理每个群的个体 @@ -60,7 +62,7 @@ class Archive extends Emitter { 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 { // 存储 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 { // 实例化全部对象 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 { // 实例化全部个体 const individuals: Array = []; - 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 { // 实例化全部标签 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 { // 实例化全部行为 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 { item.fromArchive(archive.behaviorPool[index], parseFunction); return item; }); + + // 加载剪辑 + model.clipPool = clipPool.map((item, index) => { + item.fromArchive(archive.clipPool[index], parseFunction); + return item; + }); } /** diff --git a/source/Model/Clip.ts b/source/Model/Clip.ts index be292d6..f9cf1cc 100644 --- a/source/Model/Clip.ts +++ b/source/Model/Clip.ts @@ -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 }; \ No newline at end of file +export { Clip, IFrame, IArchiveClip }; \ No newline at end of file