diff --git a/package-lock.json b/package-lock.json index 4288663..c38adc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@fluentui/react": "^8.56.0", "@juggle/resize-observer": "^3.3.1", "detect-port": "^1.3.0", + "downloadjs": "^1.4.7", "express": "^4.17.3", "gl-matrix": "^3.4.3", "react": "^17.0.2", @@ -21,6 +22,7 @@ "devDependencies": { "@atao60/fse-cli": "^0.1.7", "@types/detect-port": "^1.3.2", + "@types/downloadjs": "^1.4.3", "@types/react": "^17.0.38", "@types/react-dom": "^17.0.11", "@types/uuid": "^8.3.4", @@ -1095,6 +1097,12 @@ "integrity": "sha512-xxgAGA2SAU4111QefXPSp5eGbDm/hW6zhvYl9IeEPZEry9F4d66QAHm5qpUXjb6IsevZV/7emAEx5MhP6O192g==", "dev": true }, + "node_modules/@types/downloadjs": { + "version": "1.4.3", + "resolved": "https://registry.npmmirror.com/@types/downloadjs/-/downloadjs-1.4.3.tgz", + "integrity": "sha512-MjJepFle/tLtT2/jmDNth6ZnwWzEhm40L+olE5HKR70ISUCfgT55eqreeHldAzFLY2HDUGsn8zgyto8KygN0CA==", + "dev": true + }, "node_modules/@types/eslint": { "version": "8.4.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz", @@ -2908,6 +2916,11 @@ "tslib": "^2.0.3" } }, + "node_modules/downloadjs": { + "version": "1.4.7", + "resolved": "https://registry.npmmirror.com/downloadjs/-/downloadjs-1.4.7.tgz", + "integrity": "sha512-LN1gO7+u9xjU5oEScGFKvXhYf7Y/empUIIEAGBs1LzUq/rg5duiDrkuH5A2lQGd5jfMOb9X9usDa2oVXwJ0U/Q==" + }, "node_modules/duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmmirror.com/duplexer3/-/duplexer3-0.1.4.tgz", @@ -9065,6 +9078,12 @@ "integrity": "sha512-xxgAGA2SAU4111QefXPSp5eGbDm/hW6zhvYl9IeEPZEry9F4d66QAHm5qpUXjb6IsevZV/7emAEx5MhP6O192g==", "dev": true }, + "@types/downloadjs": { + "version": "1.4.3", + "resolved": "https://registry.npmmirror.com/@types/downloadjs/-/downloadjs-1.4.3.tgz", + "integrity": "sha512-MjJepFle/tLtT2/jmDNth6ZnwWzEhm40L+olE5HKR70ISUCfgT55eqreeHldAzFLY2HDUGsn8zgyto8KygN0CA==", + "dev": true + }, "@types/eslint": { "version": "8.4.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz", @@ -10550,6 +10569,11 @@ "tslib": "^2.0.3" } }, + "downloadjs": { + "version": "1.4.7", + "resolved": "https://registry.npmmirror.com/downloadjs/-/downloadjs-1.4.7.tgz", + "integrity": "sha512-LN1gO7+u9xjU5oEScGFKvXhYf7Y/empUIIEAGBs1LzUq/rg5duiDrkuH5A2lQGd5jfMOb9X9usDa2oVXwJ0U/Q==" + }, "duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmmirror.com/duplexer3/-/duplexer3-0.1.4.tgz", diff --git a/package.json b/package.json index fcb77ec..9ccdc33 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "devDependencies": { "@atao60/fse-cli": "^0.1.7", "@types/detect-port": "^1.3.2", + "@types/downloadjs": "^1.4.3", "@types/react": "^17.0.38", "@types/react-dom": "^17.0.11", "@types/uuid": "^8.3.4", @@ -70,6 +71,7 @@ "@fluentui/react": "^8.56.0", "@juggle/resize-observer": "^3.3.1", "detect-port": "^1.3.0", + "downloadjs": "^1.4.7", "express": "^4.17.3", "gl-matrix": "^3.4.3", "react": "^17.0.2", diff --git a/source/Component/CommandBar/CommandBar.tsx b/source/Component/CommandBar/CommandBar.tsx index b6faf75..23a1bb6 100644 --- a/source/Component/CommandBar/CommandBar.tsx +++ b/source/Component/CommandBar/CommandBar.tsx @@ -1,22 +1,68 @@ import { Component, ReactNode } from "react"; import { DirectionalHint, IconButton } from "@fluentui/react"; -import { useSetting, IMixinSettingProps } from "@Context/Setting"; +import { useSetting, useSettingWithEvent, IMixinSettingProps } from "@Context/Setting"; import { useStatusWithEvent, IMixinStatusProps } from "@Context/Status"; import { BackgroundLevel, Theme } from "@Component/Theme/Theme"; import { LocalizationTooltipHost } from "@Component/Localization/LocalizationTooltipHost"; -import { AllI18nKeys } from "@Component/Localization/Localization"; +import { AllI18nKeys, I18N } from "@Component/Localization/Localization"; import { SettingPopup } from "@Component/SettingPopup/SettingPopup"; import { BehaviorPopup } from "@Component/BehaviorPopup/BehaviorPopup"; import { MouseMod } from "@GLRender/ClassicRenderer"; +import * as download from "downloadjs"; import "./CommandBar.scss"; -interface ICommandBarProps { - width: number; +const COMMAND_BAR_WIDTH = 45; + +function getRenderButton(param: { + i18NKey: AllI18nKeys; + iconName?: string; + click?: () => void; + active?: boolean; +}): ReactNode { + return + + +} + +@useSettingWithEvent("language") +@useStatusWithEvent() +class SaveCommandView extends Component { + + public render(): ReactNode { + return getRenderButton({ + iconName: "Save", + i18NKey: "Command.Bar.Save.Info", + click: () => { + + let fileName: string = ""; + let isNewFile: boolean = true; + let isSaved: boolean = false; + + if (this.props.status) { + isNewFile = this.props.status.archive.isNewFile; + fileName = this.props.status.archive.fileName ?? ""; + isSaved = this.props.status.archive.isSaved; + } + + const file = this.props.status?.archive.save(this.props.status.model) ?? ""; + fileName = isNewFile ? I18N(this.props, "Header.Bar.New.File.Name") : fileName; + download(file, fileName, "text/json"); + } + }) + } } @useSetting @useStatusWithEvent("mouseModChange", "actuatorStartChange") -class CommandBar extends Component { +class CommandBar extends Component { render(): ReactNode { @@ -25,7 +71,7 @@ class CommandBar extends Component { if (this.props.setting) { this.props.setting.layout.focus(""); @@ -33,62 +79,65 @@ class CommandBar extends Component
- {this.getRenderButton({ - iconName: "Save", - i18NKey: "Command.Bar.Save.Info", - click: () => { - this.props.status?.archive.save(this.props.status.model); - } - })} - {this.getRenderButton({ + + + + {getRenderButton({ iconName: this.props.status?.actuator.start() ? "Pause" : "Play", i18NKey: "Command.Bar.Play.Info", click: () => this.props.status ? this.props.status.actuator.start( !this.props.status.actuator.start() ) : undefined })} - {this.getRenderButton({ + + {getRenderButton({ iconName: "HandsFree", i18NKey: "Command.Bar.Drag.Info", active: mouseMod === MouseMod.Drag, click: () => this.props.status ? this.props.status.setMouseMod(MouseMod.Drag) : undefined })} - {this.getRenderButton({ + + {getRenderButton({ iconName: "TouchPointer", i18NKey: "Command.Bar.Select.Info", active: mouseMod === MouseMod.click, click: () => this.props.status ? this.props.status.setMouseMod(MouseMod.click) : undefined })} - {this.getRenderButton({ + + {getRenderButton({ iconName: "WebAppBuilderFragmentCreate", i18NKey: "Command.Bar.Add.Group.Info", click: () => { this.props.status ? this.props.status.newGroup() : undefined; } })} - {this.getRenderButton({ + + {getRenderButton({ iconName: "ProductVariant", i18NKey: "Command.Bar.Add.Range.Info", click: () => { this.props.status ? this.props.status.newRange() : undefined; } })} - {this.getRenderButton({ + + {getRenderButton({ iconName: "Running", i18NKey: "Command.Bar.Add.Behavior.Info", click: () => { this.props.status?.popup.showPopup(BehaviorPopup, {}); } })} - {this.getRenderButton({ + + {getRenderButton({ iconName: "Tag", i18NKey: "Command.Bar.Add.Tag.Info", click: () => { this.props.status ? this.props.status.newLabel() : undefined; } })} - {this.getRenderButton({ iconName: "Camera", i18NKey: "Command.Bar.Camera.Info" })} + + {getRenderButton({ iconName: "Camera", i18NKey: "Command.Bar.Camera.Info" })}
- {this.getRenderButton({ + {getRenderButton({ iconName: "Settings", i18NKey: "Command.Bar.Setting.Info", click: () => { @@ -98,25 +147,6 @@ class CommandBar extends Component } - - private getRenderButton(param: { - i18NKey: AllI18nKeys; - iconName?: string; - click?: () => void; - active?: boolean; - }): ReactNode { - return - - - } } export { CommandBar }; \ No newline at end of file diff --git a/source/Component/HeaderBar/HeaderBar.tsx b/source/Component/HeaderBar/HeaderBar.tsx index 8322029..8009871 100644 --- a/source/Component/HeaderBar/HeaderBar.tsx +++ b/source/Component/HeaderBar/HeaderBar.tsx @@ -123,7 +123,7 @@ class HeaderWindowsAction extends Component { * 头部信息栏 */ @useSettingWithEvent("language") -@useStatusWithEvent("fileSave") +@useStatusWithEvent("fileSave", "fileChange", "fileLoad") class HeaderBar extends Component { public render(): ReactNode { diff --git a/source/Context/Status.tsx b/source/Context/Status.tsx index 69f0dea..ac88446 100644 --- a/source/Context/Status.tsx +++ b/source/Context/Status.tsx @@ -32,6 +32,7 @@ function randomColor(unNormal: boolean = false) { interface IStatusEvent { fileSave: void; fileLoad: void; + fileChange: void; renderLoop: number; physicsLoop: number; mouseModChange: void; @@ -128,11 +129,15 @@ class Status extends Emitter { this.popup.on("popupChange", () => this.emit("popupChange")); // 对象变换时执行渲染,更新渲染器数据 - this.on("objectChange", this.delayDraw); - this.model.on("individualChange", this.delayDraw); this.model.on("individualChange", () => { this.emit("individualChange"); }); + + // 渲染器重绘 + this.on("objectChange", this.delayDraw); + this.on("individualChange", this.delayDraw); + this.on("groupAttrChange", this.delayDraw); + this.on("rangeAttrChange", this.delayDraw); // 当模型中的标签和对象改变时,更新全部行为参数中的受控对象 const updateBehaviorParameter = () => { @@ -158,7 +163,24 @@ class Status extends Emitter { // 映射 this.emit("fileLoad"); - }) + }); + + + // 处理存档事件 + const handelFileChange = () => { + if (this.archive.isSaved) { + this.emit("fileChange"); + } + } + this.on("objectChange", handelFileChange); + this.on("behaviorChange", handelFileChange); + this.on("labelChange", handelFileChange); + this.on("individualChange", handelFileChange); + this.on("groupAttrChange", handelFileChange); + this.on("rangeAttrChange", handelFileChange); + this.on("labelAttrChange", handelFileChange); + this.on("behaviorAttrChange", handelFileChange); + this.on("fileChange", () => this.archive.emit("fileChange")); } public bindRenderer(renderer: AbstractRenderer) { @@ -200,7 +222,6 @@ class Status extends Emitter { if (range && range instanceof Range) { range[key] = val; this.emit("rangeAttrChange"); - this.model.draw(); } } @@ -213,7 +234,6 @@ class Status extends Emitter { if (group && group instanceof Group) { group[key] = val; this.emit("groupAttrChange"); - this.model.draw(); } } diff --git a/source/Localization/EN-US.ts b/source/Localization/EN-US.ts index cba84c0..0fb6cdd 100644 --- a/source/Localization/EN-US.ts +++ b/source/Localization/EN-US.ts @@ -4,7 +4,7 @@ const EN_US = { "Header.Bar.Title": "Living Together | Emulator", "Header.Bar.Title.Info": "Group Behavior Research Emulator", "Header.Bar.File.Name.Info": "{file} ({status})", - "Header.Bar.New.File.Name": "New File", + "Header.Bar.New.File.Name": "NewFile.ltss", "Header.Bar.File.Save.Status.Saved": "Saved", "Header.Bar.File.Save.Status.Unsaved": "UnSaved", "Header.Bar.Fps": "FPS: {renderFps} | {physicsFps}", diff --git a/source/Localization/ZH-CN.ts b/source/Localization/ZH-CN.ts index f44fbc4..0933c15 100644 --- a/source/Localization/ZH-CN.ts +++ b/source/Localization/ZH-CN.ts @@ -4,7 +4,7 @@ const ZH_CN = { "Header.Bar.Title": "群生共进 | 仿真器", "Header.Bar.Title.Info": "群体行为研究仿真器", "Header.Bar.File.Name.Info": "{file} ({status})", - "Header.Bar.New.File.Name": "新存档", + "Header.Bar.New.File.Name": "新存档.ltss", "Header.Bar.File.Save.Status.Saved": "已保存", "Header.Bar.File.Save.Status.Unsaved": "未保存", "Header.Bar.Fps": "帧率: {renderFps} | {physicsFps}", diff --git a/source/Model/Archive.ts b/source/Model/Archive.ts index a0e1f84..edf5577 100644 --- a/source/Model/Archive.ts +++ b/source/Model/Archive.ts @@ -12,6 +12,7 @@ import { IArchiveParseFn, IObjectParamArchiveType, IRealObjectType } from "@Mode interface IArchiveEvent { fileSave: Archive; fileLoad: Archive; + fileChange: void; } interface IArchiveObject { @@ -243,25 +244,34 @@ class Archive extends Emitter { * 保存文件 * 模型转换为文件 */ - public save(model: Model): void { - - console.log(this.parseModel2Archive(model)); - + public save(model: Model): string { this.isSaved = true; this.emit("fileSave", this); + return this.parseModel2Archive(model); } /** * 加载文件为模型 * @return Model */ - public load(model: Model, data: string) { - - this.loadArchiveIntoModel(model, data); + public load(model: Model, data: string): string | undefined { + try { + this.loadArchiveIntoModel(model, data); + } catch (e) { + return e as string; + } + this.isSaved = true; this.emit("fileLoad", this); }; + + public constructor() { + super(); + this.on("fileChange", () => { + this.isSaved = false; + }) + } } export { Archive }; \ No newline at end of file diff --git a/source/Page/SimulatorDesktop/SimulatorDesktop.tsx b/source/Page/SimulatorDesktop/SimulatorDesktop.tsx index 7cb8347..b1f74a7 100644 --- a/source/Page/SimulatorDesktop/SimulatorDesktop.tsx +++ b/source/Page/SimulatorDesktop/SimulatorDesktop.tsx @@ -119,8 +119,8 @@ class SimulatorDesktop extends Component {
- - + +
} diff --git a/source/Page/SimulatorWeb/SimulatorWeb.tsx b/source/Page/SimulatorWeb/SimulatorWeb.tsx index 44b475d..09fab25 100644 --- a/source/Page/SimulatorWeb/SimulatorWeb.tsx +++ b/source/Page/SimulatorWeb/SimulatorWeb.tsx @@ -208,8 +208,8 @@ class SimulatorWeb extends Component {
- - + +
}