- {this.getRenderButton({
+ {getRenderButton({
iconName: "Settings",
i18NKey: "Command.Bar.Setting.Info",
click: () => {
@@ -98,25 +143,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..b85b21c 100644
--- a/source/Component/HeaderBar/HeaderBar.tsx
+++ b/source/Component/HeaderBar/HeaderBar.tsx
@@ -123,9 +123,33 @@ class HeaderWindowsAction extends Component {
* 头部信息栏
*/
@useSettingWithEvent("language")
-@useStatusWithEvent("fileSave")
+@useStatusWithEvent("fileSave", "fileChange", "fileLoad")
class HeaderBar extends Component {
+ private showCloseMessage = (e: BeforeUnloadEvent) => {
+ if (!this.props.status?.archive.isSaved) {
+ const message = I18N(this.props, "Info.Hint.Save.After.Close");
+ (e || window.event).returnValue = message; // 兼容 Gecko + IE
+ return message; // 兼容 Gecko + Webkit, Safari, Chrome
+ }
+ }
+
+ public componentDidMount() {
+
+ if (this.props.setting?.platform === Platform.web) {
+ // 阻止页面关闭
+ window.addEventListener("beforeunload", this.showCloseMessage);
+ }
+ }
+
+ public componentWillUnmount() {
+
+ if (this.props.setting?.platform === Platform.web) {
+ // 阻止页面关闭
+ window.removeEventListener("beforeunload", this.showCloseMessage);
+ }
+ }
+
public render(): ReactNode {
const { status, setting } = this.props;
diff --git a/source/Component/LoadFile/LoadFile.scss b/source/Component/LoadFile/LoadFile.scss
new file mode 100644
index 0000000..5d29e0e
--- /dev/null
+++ b/source/Component/LoadFile/LoadFile.scss
@@ -0,0 +1,38 @@
+@import "../Theme/Theme.scss";
+
+div.load-file-layer-root {
+ position: fixed;
+ z-index: 1000;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-content: center;
+ justify-content: center;
+ align-items: center;
+ flex-wrap: wrap;
+
+ div {
+ user-select: none;
+ text-align: center;
+ width: 100%;
+ }
+
+ div.drag-icon {
+ font-weight: 200;
+ font-size: 2.8em;
+ }
+
+ div.drag-title {
+ margin-top: 5px;
+ margin-bottom: 5px;
+ font-size: 1.5em;
+ }
+}
+
+div.load-file-layer-root.light {
+ background-color: rgba($color: #FFFFFF, $alpha: .75);
+}
+
+div.load-file-layer-root.dark {
+ background-color: rgba($color: #000000, $alpha: .75);
+}
\ No newline at end of file
diff --git a/source/Component/LoadFile/LoadFile.tsx b/source/Component/LoadFile/LoadFile.tsx
new file mode 100644
index 0000000..432b6bc
--- /dev/null
+++ b/source/Component/LoadFile/LoadFile.tsx
@@ -0,0 +1,31 @@
+import { Localization } from "@Component/Localization/Localization";
+import { FontLevel, Theme } from "@Component/Theme/Theme";
+import { Icon } from "@fluentui/react";
+import { Component, ReactNode } from "react";
+import "./LoadFile.scss";
+
+class LoadFile extends Component {
+
+ private renderMask() {
+ return
+
+
+
+
+
+
+
+
+
+ ;
+ }
+
+ public render(): ReactNode {
+ return <>>;
+ }
+}
+
+export { LoadFile };
\ No newline at end of file
diff --git a/source/Context/Archive.tsx b/source/Context/Archive.tsx
new file mode 100644
index 0000000..cef0fe8
--- /dev/null
+++ b/source/Context/Archive.tsx
@@ -0,0 +1,91 @@
+import { FunctionComponent, useEffect } from "react";
+import * as download from "downloadjs";
+import { useSetting, IMixinSettingProps, Platform } from "@Context/Setting";
+import { useStatus, IMixinStatusProps } from "@Context/Status";
+import { I18N } from "@Component/Localization/Localization";
+
+interface IFileInfo {
+ fileName: string;
+ isNewFile: boolean;
+ isSaved: boolean;
+ fileUrl?: string;
+ fileData: () => Promise;
+}
+
+interface IRunnerProps {
+ running?: boolean;
+ afterRunning?: () => any;
+}
+
+interface ICallBackProps {
+ then: () => any;
+}
+
+const ArchiveSaveDownloadView: FunctionComponent = function ArchiveSave(props) {
+
+ const runner = async () => {
+ const file = await props.fileData();
+ setTimeout(() => {
+ download(file, props.fileName, "text/json");
+ props.then();
+ }, 100);
+ }
+
+ useEffect(() => { runner() }, []);
+
+ return <>>;
+}
+
+const ArchiveSaveDownload = ArchiveSaveDownloadView;
+
+/**
+ * 保存存档文件
+ */
+const ArchiveSaveView: FunctionComponent = function ArchiveSave(props) {
+
+ if (!props.running) {
+ return <>>;
+ }
+
+ const fileData: IFileInfo = {
+ fileName: "",
+ isNewFile: true,
+ isSaved: false,
+ fileUrl: undefined,
+ fileData: async () => `{"nextIndividualId":0,"objectPool":[],"labelPool":[],"behaviorPool":[]}`
+ }
+
+ if (props.status) {
+ fileData.isNewFile = props.status.archive.isNewFile;
+ fileData.fileName = props.status.archive.fileName ?? "";
+ fileData.isSaved = props.status.archive.isSaved;
+ fileData.fileUrl = props.status.archive.fileUrl;
+ }
+
+ if (fileData.isNewFile) {
+ fileData.fileName = I18N(props, "Header.Bar.New.File.Name");
+ }
+
+ // 生成存档文件
+ fileData.fileData = async () => {
+ return props.status?.archive.save(props.status.model) ?? "";
+ };
+
+ const callBack = () => {
+ if (props.afterRunning) {
+ props.afterRunning();
+ }
+ }
+
+ return <>
+ {
+ props.setting?.platform === Platform.web ?
+ :
+ <>>
+ }
+ >
+}
+
+const ArchiveSave = useSetting(useStatus(ArchiveSaveView));
+
+export { ArchiveSave };
\ No newline at end of file
diff --git a/source/Context/Status.tsx b/source/Context/Status.tsx
index 69f0dea..0d702ca 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,26 @@ 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 +224,6 @@ class Status extends Emitter {
if (range && range instanceof Range) {
range[key] = val;
this.emit("rangeAttrChange");
- this.model.draw();
}
}
@@ -213,7 +236,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..e8e756b 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}",
@@ -133,5 +133,8 @@ const EN_US = {
"Panel.Info.Behavior.Details.Parameter.Key.Vec.X": "{key} X",
"Panel.Info.Behavior.Details.Parameter.Key.Vec.Y": "{key} Y",
"Panel.Info.Behavior.Details.Parameter.Key.Vec.Z": "{key} Z",
+ "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",
}
export default EN_US;
\ No newline at end of file
diff --git a/source/Localization/ZH-CN.ts b/source/Localization/ZH-CN.ts
index f44fbc4..6b24c28 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}",
@@ -133,5 +133,8 @@ const ZH_CN = {
"Panel.Info.Behavior.Details.Parameter.Key.Vec.X": "{key} X 坐标",
"Panel.Info.Behavior.Details.Parameter.Key.Vec.Y": "{key} Y 坐标",
"Panel.Info.Behavior.Details.Parameter.Key.Vec.Z": "{key} Z 坐标",
+ "Info.Hint.Save.After.Close": "任何未保存的进度都会丢失, 确定要继续吗?",
+ "Info.Hint.Load.File.Title": "加载存档",
+ "Info.Hint.Load.File.Intro": "释放以加载拽入的存档",
}
export default ZH_CN;
\ No newline at end of file
diff --git a/source/Model/Archive.ts b/source/Model/Archive.ts
index a0e1f84..c4328e4 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 {
@@ -21,7 +22,7 @@ interface IArchiveObject {
behaviorPool: IArchiveBehavior[];
}
-class Archive extends Emitter {
+class Archive extends Emitter {
/**
* 是否为新文件
@@ -39,9 +40,9 @@ class Archive extends Emitter {
public isSaved: boolean = false;
/**
- * 文件数据
+ * 文件路径
*/
- public fileData?: M;
+ public fileUrl?: string;
/**
* 将模型转换为存档对象
@@ -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..53271a4 100644
--- a/source/Page/SimulatorWeb/SimulatorWeb.tsx
+++ b/source/Page/SimulatorWeb/SimulatorWeb.tsx
@@ -6,6 +6,7 @@ import { ClassicRenderer } from "@GLRender/ClassicRenderer";
import { initializeIcons } from '@fluentui/font-icons-mdl2';
import { RootContainer } from "@Component/Container/RootContainer";
import { LayoutDirection } from "@Context/Layout";
+import { LoadFile } from "@Component/LoadFile/LoadFile";
import { AllBehaviors, getBehaviorById } from "@Behavior/Behavior";
import { CommandBar } from "@Component/CommandBar/CommandBar";
import { HeaderBar } from "@Component/HeaderBar/HeaderBar";
@@ -203,13 +204,14 @@ class SimulatorWeb extends Component {
backgroundLevel={BackgroundLevel.Level5}
fontLevel={FontLevel.Level3}
>
+
-
-
+
+
}