diff --git a/config/parseSave2Obj.js b/config/parseSave2Obj.js new file mode 100644 index 0000000..af9df20 --- /dev/null +++ b/config/parseSave2Obj.js @@ -0,0 +1,300 @@ +const PATH = require("path"); +const FS = require("fs"); +const MINIMIST = require("minimist"); +const READLINE = require("readline-sync"); + +const ARGS = MINIMIST(process.argv.slice(2), { + alias: {i: ["input", "save"], o: ["output", "obj"]} +}); + +// 获取路径 +const inputFilePath = PATH.resolve(ARGS.i ?? "./save.ltss"); +const outputFilePath = PATH.resolve(ARGS.o ?? "./output.obj"); + +// 读取文件 +const fileString = FS.readFileSync(inputFilePath); +console.log("文件读取成功...\r\n"); + +/** + * 引入所需类型 + * @typedef {import("../source/Model/Archive").IArchiveObject} IArchiveObject + * @typedef {import("../source/Model/Clip").IArchiveClip} IArchiveClip + */ + +/** + * 解析文件 + * @type {IArchiveObject} + */ +const archive = JSON.parse(fileString); +console.log("文件解析成功...\r\n"); + +// 打印全部的剪辑列表 +if (archive.clipPool.length > 0) { + console.log("这个存档中存在以下剪辑:"); +} +archive.clipPool.map((item, index) => { + console.log(" \033[44;30m" + (index + 1) + "\033[40;32m " + item.name + " [" + item.id + "]\033[0m"); +}); + +/** + * 选择剪辑 + * @type {IArchiveClip} + */ +let clip; +if (archive.clipPool.length <= 0) { + console.log("存档中没有剪辑, 退出程序...\r\n"); + process.exit(); +} else if (archive.clipPool.length === 1) { + console.log("\r\n存档中只有一个剪辑, 自动选择...\r\n"); + clip = archive.clipPool[0]; +} else { + console.log("\r\n请选择一个剪辑: "); + let userInput = READLINE.question(); + for (let i = 0; i < archive.clipPool.length; i++) { + if ((i + 1) == userInput) { + clip = archive.clipPool[i]; + break; + } + } +} + +// 选择提示 +if (clip) { + console.log("已选择剪辑: " + clip.name + "\r\n"); +} else { + console.log("没有选择任何剪辑, 退出程序...\r\n"); + process.exit(); +} + +// 解压缩文件 +console.log("正在还原压缩剪辑记录...\r\n"); +const frames = clip.frames; + +/** + * @type {Map { + /** + * @type {IArchiveClip["frames"][number]["commands"]} + */ + const FCS = []; + frame.commands.forEach((command) => { + + // 压缩指令 + const FC = { + id: command.id, + type: command.type + }; + + /** + * 上一帧 + * @type {IArchiveClip["frames"][number]} + */ + const lastFrame = F[F.length - 1]; + + /** + * 上一帧指令 + * @type {IArchiveClip["frames"][number]["commands"][number]} + */ + const lastCommand = lastFrame?.commands.filter((c) => { + if (c.type === command.type && c.id === command.id) { + return true; + } else { + return false; + } + })[0]; + + // 记录 + FC.name = (LastFrameData === command.name) ? lastCommand?.name : command.name; + + FC.data = (LastFrameData === command.data) ? lastCommand?.data : command.data; + + FC.mapId = (LastFrameData === command.mapId) ? lastCommand?.mapId : command.mapId; + + FC.position = (LastFrameData === command.position) ? lastCommand?.position : command.position; + + FC.radius = (LastFrameData === command.radius) ? lastCommand?.radius : command.radius; + + // 获取 Mapper + const mapper = objectMapper.get(FC.id); + if (mapper) { + mapper.type = FC.type ?? mapper.type; + mapper.name = FC.name ?? mapper.name; + } else { + objectMapper.set(FC.id, { + type: FC.type, + name: FC.name + }); + } + + FCS.push(FC); + }); + + F.push({ + duration: frame.duration, + process: frame.process, + commands: FCS + }); +}); + +console.log("剪辑记录还原成功...\r\n"); +console.log("剪辑共 " + F.length + " 帧, 对象 " + objectMapper.size + " 个\r\n"); +if (objectMapper.size) { + console.log("剪辑记录中存在以下对象:"); +} else { + console.log("剪辑记录中没有任何对象,退出程序..."); + process.exit(); +} +let objectMapperForEachIndex = 1; +objectMapper.forEach((item, key) => { + console.log(" \033[44;30m" + (objectMapperForEachIndex ++) + "\033[40;32m " + item.type + " " + item.name + " [" + key + "]\033[0m"); +}); + +/** + * @type {number[]} + */ +const pointMapper = []; + +/** + * @param {number} x + * @param {number} y + * @param {number} z + * @returns {number} + */ +function getPointID(x, y, z) { + let search = -1; + let pointMapperLength = (pointMapper.length / 3); + // for (let i = 0; i < pointMapperLength; i++) { + // if ( + // pointMapper[i * 3 + 0] === x && + // pointMapper[i * 3 + 1] === y && + // pointMapper[i * 3 + 2] === z + // ) { + // search = i; + // } + // } + if (search >= 0) { + return search; + } else { + pointMapper.push(x); + pointMapper.push(y); + pointMapper.push(z); + return pointMapperLength + 1; + } +} + +let frameId = 0; +/** + * @type {Map} + */ +const objectLineMapper = new Map(); +/** + * @param {string} obj + * @param {number} id + * @param {number} point + */ +function recordPoint(obj, id, point) { + let searchObj = objectLineMapper.get(obj); + if (searchObj) { + + /** + * @type {{id: number, start: number, last: number, point: number[]}} + */ + let search; + for (let i = 0; i < searchObj.length; i++) { + if (searchObj[i].id === id && searchObj[i].last === (frameId - 1)) { + search = searchObj[i]; + } + } + + if (search) { + search.point.push(point); + search.last = frameId; + } else { + searchObj.push({ + id: id, + start: frameId, + last: frameId, + point: [point] + }); + } + } else { + objectLineMapper.set(obj, [{ + id: id, + start: frameId, + last: frameId, + point: [point] + }]); + } +} + +console.log("\r\n正在收集多边形数据...\r\n"); +for (frameId = 0; frameId < F.length; frameId ++) { + F[frameId].commands.forEach((command) => { + + if (command.type === "points" && command.mapId && command.data) { + command.mapId.forEach((pid, index) => { + + const x = command.data[index * 3 + 0]; + const y = command.data[index * 3 + 1] + const z = command.data[index * 3 + 2] + + if ( + x !== undefined && + y !== undefined && + z !== undefined + ) { + recordPoint(command.id, pid, getPointID(x, y, z)); + } + }) + } + }); +} + +let pointCount = (pointMapper.length / 3); + +console.log("收集点数据 " + pointCount + "个\r\n"); +console.log("收集样条:"); +let objectLineMapperIndexPrint = 1; +objectLineMapper.forEach((item, key) => { + let iName = objectMapper.get(key).name; + console.log(" \033[44;30m" + (objectLineMapperIndexPrint ++) + "\033[40;32m " + item.length + " " + iName + " [" + key + "]\033[0m"); +}); + +console.log("\r\n正在生成 .obj 文件...\r\n"); + +let fileStr = ""; let fileStrVec = ""; + +objectLineMapper.forEach((item, key) => { + + for (let i = 0; i < item.length; i++) { + fileStr += "\r\n"; + fileStr += ("o " + objectMapper.get(key).name + " " + item[i].id + "\r\n"); + fileStr += "usemtl default\r\n"; + fileStr += "l "; + // fileStr += (getPointID(item[i].id, item[i].start, item[i].last) + " "); + fileStr += item[i].point.join(" "); + fileStr += "\r\n"; + } + + fileStr += "\r\n"; +}); + +pointCount = (pointMapper.length / 3); +for (let i = 0; i < pointCount; i++) { + fileStrVec += ("v " + pointMapper[i * 3 + 0] + " " + pointMapper[i * 3 + 1] + " " + pointMapper[i * 3 + 2] + "\r\n"); +} + +const file = "# Create with Living Together (Parse from .ltss file)\r\n\r\n" + fileStrVec + fileStr; + +console.log("正在生成保存文件...\r\n"); +FS.writeFileSync(outputFilePath, file); +console.log("成功\r\n"); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b8eb1b3..7b31c0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^17.0.2", + "readline-sync": "^1.4.10", "uuid": "^8.3.2" }, "devDependencies": { @@ -6367,6 +6368,14 @@ "node": ">=8.10.0" } }, + "node_modules/readline-sync": { + "version": "1.4.10", + "resolved": "https://registry.npmmirror.com/readline-sync/-/readline-sync-1.4.10.tgz", + "integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/rechoir": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", @@ -13309,6 +13318,11 @@ "picomatch": "^2.2.1" } }, + "readline-sync": { + "version": "1.4.10", + "resolved": "https://registry.npmmirror.com/readline-sync/-/readline-sync-1.4.10.tgz", + "integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==" + }, "rechoir": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", diff --git a/package.json b/package.json index 8a4ba53..afef7db 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^17.0.2", + "readline-sync": "^1.4.10", "uuid": "^8.3.2" } } diff --git a/source/Context/Status.tsx b/source/Context/Status.tsx index adf06fb..3bcb630 100644 --- a/source/Context/Status.tsx +++ b/source/Context/Status.tsx @@ -174,11 +174,13 @@ class Status extends Emitter { this.emit("objectChange"); this.emit("labelChange"); this.emit("behaviorChange"); + this.emit("clipChange"); // 清除焦点对象 this.setBehaviorObject(); this.setFocusObject(new Set()); this.setLabelObject(); + this.setClipObject(); // 映射 this.emit("fileLoad"); @@ -196,11 +198,13 @@ class Status extends Emitter { this.on("objectChange", handelFileChange); this.on("behaviorChange", handelFileChange); this.on("labelChange", handelFileChange); + this.on("clipChange", handelFileChange); this.on("individualChange", handelFileChange); this.on("groupAttrChange", handelFileChange); this.on("rangeAttrChange", handelFileChange); this.on("labelAttrChange", handelFileChange); this.on("behaviorAttrChange", handelFileChange); + this.on("clipAttrChange", handelFileChange); this.on("fileChange", () => this.archive.emit("fileChange")); } diff --git a/source/Model/Archive.ts b/source/Model/Archive.ts index e9e102e..50125ef 100644 --- a/source/Model/Archive.ts +++ b/source/Model/Archive.ts @@ -301,4 +301,4 @@ class Archive extends Emitter { } } -export { Archive }; \ No newline at end of file +export { Archive, IArchiveObject }; \ No newline at end of file diff --git a/source/Model/Clip.ts b/source/Model/Clip.ts index f9cf1cc..b792d43 100644 --- a/source/Model/Clip.ts +++ b/source/Model/Clip.ts @@ -7,7 +7,9 @@ import { archiveObject2Parameter, IArchiveParseFn, parameter2ArchiveObject } fro interface IDrawCommand { type: "points" | "cube"; id: string; + name?: string; data?: Float32Array; + mapId?: number[]; position?: number[]; radius?: number[]; parameter?: IAnyObject; @@ -143,7 +145,7 @@ class Clip { return res; } - public isArrayEqual(a1?: number[], a2?: number[]): boolean { + public isArrayEqual(a1?: Array, a2?: Array): boolean { if ((a1 && !a2) || (!a1 && a2)) { return false; @@ -179,6 +181,26 @@ class Clip { return undefined; } + /** + * ID 映射 + */ + private sorterIdMapper: Map = new Map(); + private sorterIdMapperNextId: number = 1; + + /** + * 获取映射ID + */ + private getMapperId = (id: string): number => { + let mapperId = this.sorterIdMapper.get(id); + if (mapperId === undefined) { + mapperId = this.sorterIdMapperNextId ++; + this.sorterIdMapper.set(id, mapperId); + return mapperId; + } else { + return mapperId; + } + } + /** * 录制一帧 */ @@ -196,10 +218,12 @@ class Clip { const lastCommand = this.getCommandFromLastFrame("points", object.id); // 记录 + const dataBuffer = object.exportPositionId(this.getMapperId); const recodeData: IDrawCommand = { type: "points", id: object.id, - data: object.exportPositionData() + name: object.displayName, + data: dataBuffer[0] } // 对比校验 @@ -209,6 +233,12 @@ class Clip { recodeData.parameter = this.cloneRenderParameter(object.renderParameter); } + if (this.isArrayEqual(dataBuffer[1], lastCommand?.mapId)) { + recodeData.mapId = lastCommand?.mapId; + } else { + recodeData.mapId = dataBuffer[1]; + } + commands.push(recodeData); } @@ -221,7 +251,8 @@ class Clip { // 记录 const recodeData: IDrawCommand = { type: "cube", - id: object.id + id: object.id, + name: object.displayName } // 释放上一帧的内存 @@ -260,7 +291,7 @@ class Clip { return frame; } - public readonly LastFrameData: "@L" = "@L"; + public readonly LastFrameData: "@" = "@"; /** * 压缩帧数据 @@ -287,9 +318,15 @@ class Clip { undefined; // 记录 + command.name = (lastCommand?.name === commands[j].name) ? + this.LastFrameData as any : commands[j].name; + command.data = (lastCommand?.data === commands[j].data) ? this.LastFrameData as any : Array.from(commands[j].data ?? []); + command.mapId = (lastCommand?.mapId === commands[j].mapId) ? + this.LastFrameData as any : commands[j].mapId; + command.position = (lastCommand?.position === commands[j].position) ? this.LastFrameData as any : commands[j].position?.concat([]); @@ -336,12 +373,16 @@ class Clip { this.getCommandFromLastFrame(command.type, command.id, resFrame[resFrame.length - 1]) : undefined; - console.log(lastCommand); - // 记录 + command.name = (this.LastFrameData as any === commands[j].name) ? + lastCommand?.name : commands[j].name; + command.data = (this.LastFrameData as any === commands[j].data) ? lastCommand?.data : new Float32Array(commands[j].data ?? []); + command.mapId = (this.LastFrameData as any === commands[j].mapId) ? + lastCommand?.mapId : commands[j].mapId; + command.position = (this.LastFrameData as any === commands[j].position) ? lastCommand?.position : commands[j].position; diff --git a/source/Model/Group.ts b/source/Model/Group.ts index 024b737..f9f1a63 100644 --- a/source/Model/Group.ts +++ b/source/Model/Group.ts @@ -428,6 +428,22 @@ class Group extends CtrlObject { return dataBuffer; } + /** + * 导出个体id列表 + */ + public exportPositionId(idMapper: (id: string) => number ): [Float32Array, number[]] { + let bi = 0; let ii = 0; + let dataBuffer = new Float32Array(this.individuals.size * 3); + let idBUffer: number[] = new Array(this.individuals.size).fill(""); + this.individuals.forEach((individual) => { + idBUffer[ii ++] = idMapper(individual.id); + dataBuffer[bi ++] = individual.position[0]; + dataBuffer[bi ++] = individual.position[1]; + dataBuffer[bi ++] = individual.position[2]; + }); + return [dataBuffer, idBUffer]; + } + public override toArchive(): IArchiveCtrlObject & IArchiveGroup { return { ...super.toArchive(),