Compare commits
17
Commits
6063882192
...
Alpha0.1.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
396b128a2c | ||
|
|
298688269c | ||
|
|
1f08a4885b | ||
|
|
4c014a7200 | ||
|
|
b5ecf0bb0d | ||
|
|
87ed157340 | ||
|
|
56151c9e75 | ||
|
|
03f0b9fd16 | ||
|
|
5914a9f531 | ||
|
|
980d8afa3c | ||
|
|
b498f0e0a4 | ||
|
|
430d8a7254 | ||
|
|
74bda21072 | ||
|
|
0d7cbe135d | ||
|
|
068acb19a8 | ||
|
|
276c8c63a1 | ||
|
|
d11fd2d328 |
Generated
+24
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ContactAssimilate } from "@Behavior/ContactAssimilate";
|
||||
import { DelayAssimilate } from "@Behavior/DelayAssimilate";
|
||||
|
||||
const AllBehaviors: IAnyBehaviorRecorder[] = [
|
||||
new BehaviorRecorder(Template),
|
||||
// new BehaviorRecorder(Template),
|
||||
new BehaviorRecorder(PhysicsDynamics),
|
||||
new BehaviorRecorder(Brownian),
|
||||
new BehaviorRecorder(BoundaryConstraint),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Behavior } from "@Model/Behavior";
|
||||
import Group from "@Model/Group";
|
||||
import Individual from "@Model/Individual";
|
||||
import { Group } from "@Model/Group";
|
||||
import { Individual } from "@Model/Individual";
|
||||
import { Model } from "@Model/Model";
|
||||
|
||||
type IPhysicsDynamicsBehaviorParameter = {
|
||||
|
||||
@@ -8,24 +8,51 @@ import { AllI18nKeys } from "@Component/Localization/Localization";
|
||||
import { SettingPopup } from "@Component/SettingPopup/SettingPopup";
|
||||
import { BehaviorPopup } from "@Component/BehaviorPopup/BehaviorPopup";
|
||||
import { MouseMod } from "@GLRender/ClassicRenderer";
|
||||
import { ArchiveSave } from "@Context/Archive";
|
||||
import "./CommandBar.scss";
|
||||
|
||||
interface ICommandBarProps {
|
||||
width: number;
|
||||
const COMMAND_BAR_WIDTH = 45;
|
||||
|
||||
interface IRenderButtonParameter {
|
||||
i18NKey: AllI18nKeys;
|
||||
iconName?: string;
|
||||
click?: () => void;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
interface ICommandBarState {
|
||||
isSaveRunning: boolean;
|
||||
}
|
||||
|
||||
function getRenderButton(param: IRenderButtonParameter): ReactNode {
|
||||
return <LocalizationTooltipHost
|
||||
i18nKey={param.i18NKey}
|
||||
directionalHint={DirectionalHint.rightCenter}
|
||||
>
|
||||
<IconButton
|
||||
style={{ height: COMMAND_BAR_WIDTH }}
|
||||
iconProps={{ iconName: param.iconName }}
|
||||
onClick={ param.click }
|
||||
className={"command-button on-end" + (param.active ? " active" : "")}
|
||||
/>
|
||||
</LocalizationTooltipHost>
|
||||
}
|
||||
@useSetting
|
||||
@useStatusWithEvent("mouseModChange", "actuatorStartChange")
|
||||
class CommandBar extends Component<ICommandBarProps & IMixinSettingProps & IMixinStatusProps> {
|
||||
class CommandBar extends Component<IMixinSettingProps & IMixinStatusProps, ICommandBarState> {
|
||||
|
||||
render(): ReactNode {
|
||||
public state: Readonly<ICommandBarState> = {
|
||||
isSaveRunning: false
|
||||
};
|
||||
|
||||
public render(): ReactNode {
|
||||
|
||||
const mouseMod = this.props.status?.mouseMod ?? MouseMod.Drag;
|
||||
|
||||
return <Theme
|
||||
className="command-bar"
|
||||
backgroundLevel={BackgroundLevel.Level2}
|
||||
style={{ width: this.props.width }}
|
||||
style={{ width: COMMAND_BAR_WIDTH }}
|
||||
onClick={() => {
|
||||
if (this.props.setting) {
|
||||
this.props.setting.layout.focus("");
|
||||
@@ -33,84 +60,89 @@ class CommandBar extends Component<ICommandBarProps & IMixinSettingProps & IMixi
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{this.getRenderButton({ iconName: "Save", i18NKey: "Command.Bar.Save.Info" })}
|
||||
{this.getRenderButton({
|
||||
|
||||
<ArchiveSave
|
||||
running={this.state.isSaveRunning}
|
||||
afterRunning={() => {
|
||||
this.setState({ isSaveRunning: false });
|
||||
}}
|
||||
/>
|
||||
|
||||
{getRenderButton({
|
||||
iconName: "Save",
|
||||
i18NKey: "Command.Bar.Save.Info",
|
||||
click: () => {
|
||||
this.setState({
|
||||
isSaveRunning: true
|
||||
});
|
||||
}
|
||||
})}
|
||||
|
||||
{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({
|
||||
iconName: "WebAppBuilderFragmentCreate",
|
||||
|
||||
{getRenderButton({
|
||||
iconName: "WebAppBuilderFragment",
|
||||
i18NKey: "Command.Bar.Add.Group.Info",
|
||||
click: () => {
|
||||
this.props.status ? this.props.status.newGroup() : undefined;
|
||||
}
|
||||
})}
|
||||
{this.getRenderButton({
|
||||
iconName: "ProductVariant",
|
||||
|
||||
{getRenderButton({
|
||||
iconName: "CubeShape",
|
||||
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" })} */}
|
||||
</div>
|
||||
<div>
|
||||
{this.getRenderButton({
|
||||
{/* {getRenderButton({
|
||||
iconName: "Settings",
|
||||
i18NKey: "Command.Bar.Setting.Info",
|
||||
click: () => {
|
||||
this.props.status?.popup.showPopup(SettingPopup, {});
|
||||
}
|
||||
})}
|
||||
})} */}
|
||||
</div>
|
||||
</Theme>
|
||||
}
|
||||
|
||||
private getRenderButton(param: {
|
||||
i18NKey: AllI18nKeys;
|
||||
iconName?: string;
|
||||
click?: () => void;
|
||||
active?: boolean;
|
||||
}): ReactNode {
|
||||
return <LocalizationTooltipHost
|
||||
i18nKey={param.i18NKey}
|
||||
directionalHint={DirectionalHint.rightCenter}
|
||||
>
|
||||
<IconButton
|
||||
style={{ height: this.props.width }}
|
||||
iconProps={{ iconName: param.iconName }}
|
||||
onClick={ param.click }
|
||||
className={"command-button on-end" + (param.active ? " active" : "")}
|
||||
/>
|
||||
</LocalizationTooltipHost>
|
||||
}
|
||||
}
|
||||
|
||||
export { CommandBar };
|
||||
@@ -123,9 +123,33 @@ class HeaderWindowsAction extends Component<IMixinElectronProps> {
|
||||
* 头部信息栏
|
||||
*/
|
||||
@useSettingWithEvent("language")
|
||||
@useStatusWithEvent("fileChange")
|
||||
@useStatusWithEvent("fileSave", "fileChange", "fileLoad")
|
||||
class HeaderBar extends Component<IHeaderBarProps & IMixinStatusProps & IMixinSettingProps> {
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 <Theme
|
||||
className="load-file-layer-root"
|
||||
fontLevel={FontLevel.normal}
|
||||
>
|
||||
<div className="drag-icon">
|
||||
<Icon iconName="KnowledgeArticle"/>
|
||||
</div>
|
||||
<div className="drag-title">
|
||||
<Localization i18nKey="Info.Hint.Load.File.Title"/>
|
||||
</div>
|
||||
<div className="drag-intro">
|
||||
<Localization i18nKey="Info.Hint.Load.File.Intro"/>
|
||||
</div>
|
||||
</Theme>;
|
||||
}
|
||||
|
||||
public render(): ReactNode {
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
|
||||
export { LoadFile };
|
||||
@@ -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<string>;
|
||||
}
|
||||
|
||||
interface IRunnerProps {
|
||||
running?: boolean;
|
||||
afterRunning?: () => any;
|
||||
}
|
||||
|
||||
interface ICallBackProps {
|
||||
then: () => any;
|
||||
}
|
||||
|
||||
const ArchiveSaveDownloadView: FunctionComponent<IFileInfo & ICallBackProps> = 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<IMixinSettingProps & IMixinStatusProps & IRunnerProps> = 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 ?
|
||||
<ArchiveSaveDownload {...fileData} then={callBack}/> :
|
||||
<></>
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
const ArchiveSave = useSetting(useStatus(ArchiveSaveView));
|
||||
|
||||
export { ArchiveSave };
|
||||
@@ -37,7 +37,7 @@ class Setting extends Emitter<ISettingEvents> {
|
||||
/**
|
||||
* 语言
|
||||
*/
|
||||
public language: Language = "EN_US";
|
||||
public language: Language = "ZH_CN";
|
||||
|
||||
/**
|
||||
* 布局
|
||||
|
||||
@@ -30,6 +30,8 @@ function randomColor(unNormal: boolean = false) {
|
||||
}
|
||||
|
||||
interface IStatusEvent {
|
||||
fileSave: void;
|
||||
fileLoad: void;
|
||||
fileChange: void;
|
||||
renderLoop: number;
|
||||
physicsLoop: number;
|
||||
@@ -127,11 +129,15 @@ class Status extends Emitter<IStatusEvent> {
|
||||
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 = () => {
|
||||
@@ -145,7 +151,38 @@ class Status extends Emitter<IStatusEvent> {
|
||||
this.on("behaviorAttrChange", updateBehaviorParameter);
|
||||
|
||||
// 映射文件状态改变事件
|
||||
this.archive.on("fileChange", () => this.emit("fileChange"));
|
||||
this.archive.on("fileSave", () => this.emit("fileSave"));
|
||||
|
||||
// 处理存档加载事件
|
||||
this.archive.on("fileLoad", () => {
|
||||
|
||||
// 触发对象修改
|
||||
this.emit("objectChange");
|
||||
this.emit("labelChange");
|
||||
this.emit("behaviorChange");
|
||||
|
||||
// 映射
|
||||
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) {
|
||||
@@ -187,7 +224,6 @@ class Status extends Emitter<IStatusEvent> {
|
||||
if (range && range instanceof Range) {
|
||||
range[key] = val;
|
||||
this.emit("rangeAttrChange");
|
||||
this.model.draw();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +236,6 @@ class Status extends Emitter<IStatusEvent> {
|
||||
if (group && group instanceof Group) {
|
||||
group[key] = val;
|
||||
this.emit("groupAttrChange");
|
||||
this.model.draw();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -19,7 +19,7 @@ class Actuator extends Emitter<IActuatorEvent> {
|
||||
/**
|
||||
* 模拟帧率
|
||||
*/
|
||||
public fps: number = 36;
|
||||
public fps: number = 42;
|
||||
|
||||
/**
|
||||
* 仿真是否进行
|
||||
|
||||
+241
-23
@@ -1,14 +1,28 @@
|
||||
import { Emitter, EventType } from "@Model/Emitter";
|
||||
import { Model } from "./Model";
|
||||
import { CtrlObject, IArchiveCtrlObject } from "@Model/CtrlObject";
|
||||
import { Model } from "@Model/Model";
|
||||
import { IArchiveLabel, Label } from "@Model/Label";
|
||||
import { Group, IArchiveGroup } from "@Model/Group";
|
||||
import { Range } from "@Model/Range";
|
||||
import { IArchiveIndividual, Individual } from "@Model/Individual";
|
||||
import { Behavior, IArchiveBehavior } from "@Model/Behavior";
|
||||
import { getBehaviorById } from "@Behavior/Behavior";
|
||||
import { IArchiveParseFn, IObjectParamArchiveType, IRealObjectType } from "@Model/Parameter";
|
||||
|
||||
interface IArchiveEvent {
|
||||
fileChange: Archive;
|
||||
fileSave: Archive;
|
||||
fileLoad: Archive;
|
||||
fileChange: void;
|
||||
}
|
||||
|
||||
class Archive<
|
||||
M extends any = any,
|
||||
E extends Record<EventType, any> = {}
|
||||
> extends Emitter<E & IArchiveEvent> {
|
||||
interface IArchiveObject {
|
||||
nextIndividualId: number;
|
||||
objectPool: IArchiveCtrlObject[];
|
||||
labelPool: IArchiveLabel[];
|
||||
behaviorPool: IArchiveBehavior[];
|
||||
}
|
||||
|
||||
class Archive extends Emitter<IArchiveEvent> {
|
||||
|
||||
/**
|
||||
* 是否为新文件
|
||||
@@ -26,34 +40,238 @@ class Archive<
|
||||
public isSaved: boolean = false;
|
||||
|
||||
/**
|
||||
* 文件数据
|
||||
* 文件路径
|
||||
*/
|
||||
public fileData?: M;
|
||||
public fileUrl?: string;
|
||||
|
||||
/**
|
||||
* 将模型转换为存档对象
|
||||
*/
|
||||
public parseModel2Archive(model: Model): string {
|
||||
|
||||
// 存贮 CtrlObject
|
||||
const objectPool: IArchiveCtrlObject[] = [];
|
||||
model.objectPool.forEach(obj => {
|
||||
let archiveObject = obj.toArchive();
|
||||
|
||||
// 处理每个群的个体
|
||||
if (archiveObject.objectType === "G") {
|
||||
const archiveGroup: IArchiveGroup = archiveObject as any;
|
||||
const group: Group = obj as Group;
|
||||
|
||||
const individuals: IArchiveIndividual[] = [];
|
||||
group.individuals.forEach((item) => {
|
||||
individuals.push(item.toArchive());
|
||||
});
|
||||
|
||||
archiveGroup.individuals = individuals;
|
||||
}
|
||||
|
||||
objectPool.push(archiveObject);
|
||||
})
|
||||
|
||||
// 存储 Label
|
||||
const labelPool: IArchiveLabel[] = [];
|
||||
model.labelPool.forEach(obj => {
|
||||
labelPool.push(obj.toArchive());
|
||||
});
|
||||
|
||||
// 存储全部行为
|
||||
const behaviorPool: IArchiveBehavior[] = [];
|
||||
model.behaviorPool.forEach(obj => {
|
||||
behaviorPool.push(obj.toArchive());
|
||||
});
|
||||
|
||||
// 生成存档对象
|
||||
const fileData: IArchiveObject = {
|
||||
nextIndividualId: model.nextIndividualId,
|
||||
objectPool: objectPool,
|
||||
labelPool: labelPool,
|
||||
behaviorPool: behaviorPool
|
||||
};
|
||||
|
||||
return JSON.stringify(fileData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载存档到模型
|
||||
*/
|
||||
public loadArchiveIntoModel(model: Model, data: string): void {
|
||||
|
||||
// 解析为 JSON 对象
|
||||
const archive: IArchiveObject = JSON.parse(data);
|
||||
console.log(archive);
|
||||
|
||||
// 实例化全部对象
|
||||
const objectPool: CtrlObject[] = [];
|
||||
const individualPool: Individual[] = [];
|
||||
archive.objectPool.forEach((obj) => {
|
||||
|
||||
let ctrlObject: CtrlObject | undefined = undefined;
|
||||
|
||||
// 处理群
|
||||
if (obj.objectType === "G") {
|
||||
const archiveGroup: IArchiveGroup = obj as any;
|
||||
const newGroup = new Group(model);
|
||||
|
||||
// 实例化全部个体
|
||||
const individuals: Array<Individual> = [];
|
||||
archiveGroup.individuals.forEach((item) => {
|
||||
const newIndividual = new Individual(newGroup);
|
||||
newIndividual.id = item.id;
|
||||
individuals.push(newIndividual);
|
||||
individualPool.push(newIndividual);
|
||||
})
|
||||
|
||||
newGroup.cacheIndividualsArray = individuals;
|
||||
ctrlObject = newGroup;
|
||||
}
|
||||
|
||||
// 处理范围
|
||||
if (obj.objectType === "R") {
|
||||
ctrlObject = new Range(model);
|
||||
}
|
||||
|
||||
if (ctrlObject) {
|
||||
ctrlObject.id = obj.id;
|
||||
objectPool.push(ctrlObject);
|
||||
}
|
||||
});
|
||||
|
||||
// 实例化全部标签
|
||||
const labelPool: Label[] = [];
|
||||
archive.labelPool.forEach((item) => {
|
||||
const newLabel = new Label(model);
|
||||
newLabel.id = item.id;
|
||||
labelPool.push(newLabel);
|
||||
});
|
||||
|
||||
// 实例化全部行为
|
||||
const behaviorPool: Behavior[] = [];
|
||||
archive.behaviorPool.forEach((item) => {
|
||||
const recorder = getBehaviorById(item.behaviorId);
|
||||
const newBehavior = recorder.new();
|
||||
newBehavior.id = item.id;
|
||||
behaviorPool.push(newBehavior);
|
||||
});
|
||||
|
||||
// 内置标签集合
|
||||
const buildInLabel = [model.allGroupLabel, model.allRangeLabel, model.currentGroupLabel]
|
||||
|
||||
const search: <T extends IRealObjectType>(pool: T[], archive: IObjectParamArchiveType) => T | undefined =
|
||||
(pool, archive) => {
|
||||
for (let i = 0; i < pool.length; i++) {
|
||||
if (pool[i].id === archive.__LIVING_TOGETHER_OBJECT_ID) {
|
||||
return pool[i];
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const searchAll: (archive: IObjectParamArchiveType) => IRealObjectType | undefined =
|
||||
(archive) => {
|
||||
return void 0 ??
|
||||
search(individualPool, archive) ??
|
||||
search(objectPool, archive) ??
|
||||
search(labelPool, archive) ??
|
||||
search(buildInLabel, archive) ??
|
||||
search(behaviorPool, archive);
|
||||
}
|
||||
|
||||
// 实例搜索函数
|
||||
const parseFunction: IArchiveParseFn = (archive) => {
|
||||
|
||||
switch (archive.__LIVING_TOGETHER_OBJECT_TYPE) {
|
||||
|
||||
// 在个体池搜索
|
||||
case "Individual":
|
||||
return search(individualPool, archive) ?? searchAll(archive);
|
||||
|
||||
// 对象类型在对象池中搜索
|
||||
case "Range":
|
||||
case "Group":
|
||||
return search(objectPool, archive) ?? searchAll(archive);
|
||||
|
||||
// 在标签池搜索
|
||||
case "Label":
|
||||
return search(labelPool, archive) ?? search(buildInLabel, archive) ?? searchAll(archive);
|
||||
|
||||
// 在标签池搜索
|
||||
case "Behavior":
|
||||
return search(behaviorPool, archive) ?? searchAll(archive);
|
||||
|
||||
// 在全部池子搜索
|
||||
default:
|
||||
return searchAll(archive);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载对象的属性
|
||||
model.objectPool = objectPool.map((obj, index) => {
|
||||
|
||||
// 加载属性
|
||||
obj.fromArchive(archive.objectPool[index], parseFunction);
|
||||
|
||||
// 加载个体属性
|
||||
if (obj instanceof Group) {
|
||||
|
||||
const archiveGroup: IArchiveGroup = archive.objectPool[index] as any;
|
||||
|
||||
obj.individuals = new Set(obj.cacheIndividualsArray.map((item, i) => {
|
||||
item.fromArchive(archiveGroup.individuals[i], parseFunction);
|
||||
return item;
|
||||
}));
|
||||
}
|
||||
|
||||
return obj;
|
||||
});
|
||||
|
||||
// 加载标签属性
|
||||
model.labelPool = labelPool.map((item, index) => {
|
||||
item.fromArchive(archive.labelPool[index]);
|
||||
return item;
|
||||
});
|
||||
|
||||
// 加载行为属性
|
||||
model.behaviorPool = behaviorPool.map((item, index) => {
|
||||
item.fromArchive(archive.behaviorPool[index], parseFunction);
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存文件
|
||||
* 模型转换为文件
|
||||
*/
|
||||
public save(model: Model): string {
|
||||
let fileData: Record<string, any> = {};
|
||||
|
||||
// 保存对象
|
||||
fileData.objects = [];
|
||||
|
||||
// 记录
|
||||
model.objectPool.map((object) => {
|
||||
|
||||
})
|
||||
|
||||
return JSON.stringify(model);
|
||||
this.isSaved = true;
|
||||
this.emit("fileSave", this);
|
||||
return this.parseModel2Archive(model);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载文件为模型
|
||||
* return Model
|
||||
* @return Model
|
||||
*/
|
||||
public load(model: Model, data: string) {};
|
||||
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 };
|
||||
export default Archive;
|
||||
export { Archive };
|
||||
@@ -3,7 +3,11 @@ import { v4 as uuid } from "uuid";
|
||||
import type { Individual } from "@Model/Individual";
|
||||
import type { Group } from "@Model/Group";
|
||||
import type { Model } from "@Model/Model";
|
||||
import { getDefaultValue, IParameter, IParameterOption, IParameterValue } from "@Model/Parameter";
|
||||
import {
|
||||
archiveObject2Parameter,
|
||||
getDefaultValue, IArchiveParameterValue, IArchiveParseFn,
|
||||
IParameter, IParameterOption, IParameterValue, parameter2ArchiveObject
|
||||
} from "@Model/Parameter";
|
||||
|
||||
/**
|
||||
* 行为构造函数类型
|
||||
@@ -113,6 +117,17 @@ class BehaviorRecorder<
|
||||
}
|
||||
}
|
||||
|
||||
interface IArchiveBehavior {
|
||||
behaviorId: string;
|
||||
name: string;
|
||||
id: string;
|
||||
color: number[];
|
||||
priority: number;
|
||||
currentGroupKey: string[];
|
||||
deleteFlag: boolean;
|
||||
parameter: IArchiveParameterValue<IParameter>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 群体的某种行为
|
||||
*/
|
||||
@@ -189,6 +204,33 @@ class Behavior<
|
||||
return this.deleteFlag;
|
||||
}
|
||||
|
||||
public toArchive(): IArchiveBehavior {
|
||||
return {
|
||||
behaviorId: this.behaviorId,
|
||||
name: this.name,
|
||||
id: this.id,
|
||||
color: this.color.concat([]),
|
||||
priority: this.priority,
|
||||
currentGroupKey: this.currentGroupKey.concat([]) as any,
|
||||
deleteFlag: this.deleteFlag,
|
||||
parameter: parameter2ArchiveObject(
|
||||
this.parameter, this.parameterOption
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
public fromArchive(archive: IArchiveBehavior, paster: IArchiveParseFn): void {
|
||||
this.name = archive.name,
|
||||
this.id = archive.id,
|
||||
this.color = archive.color.concat([]),
|
||||
this.priority = archive.priority,
|
||||
this.currentGroupKey = archive.currentGroupKey.concat([]) as any,
|
||||
this.deleteFlag = archive.deleteFlag,
|
||||
this.parameter = archiveObject2Parameter(
|
||||
archive.parameter, paster
|
||||
) as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载时调用
|
||||
*/
|
||||
@@ -241,6 +283,6 @@ class Behavior<
|
||||
type IRenderBehavior = BehaviorInfo | Behavior;
|
||||
|
||||
export {
|
||||
Behavior, BehaviorRecorder, IAnyBehavior, IAnyBehaviorRecorder, BehaviorInfo, IRenderBehavior
|
||||
};
|
||||
export default { Behavior };
|
||||
Behavior, BehaviorRecorder, IAnyBehavior, IAnyBehaviorRecorder,
|
||||
BehaviorInfo, IRenderBehavior, IArchiveBehavior
|
||||
};
|
||||
@@ -1,17 +1,23 @@
|
||||
import { LabelObject } from "@Model/Label"
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { parameter2ArchiveObject, archiveObject2Parameter, IArchiveParseFn } from "@Model/Parameter";
|
||||
import type { IAnyObject, Model } from "@Model/Model";
|
||||
import type { ObjectID } from "@Model/Model";
|
||||
import {
|
||||
parameter2ArchiveObject, archiveObject2Parameter,
|
||||
IArchiveParseFn, IObjectParamArchiveType, object2ArchiveObject,
|
||||
IArchiveParameterValue, IParameter
|
||||
} from "@Model/Parameter";
|
||||
|
||||
interface IArchiveCtrlObject {
|
||||
objectType: "R" | "G";
|
||||
displayName: CtrlObject["displayName"];
|
||||
color: CtrlObject["color"];
|
||||
display: CtrlObject["display"];
|
||||
update: CtrlObject["update"];
|
||||
id: string;
|
||||
renderParameter: any;
|
||||
renderParameter: IArchiveParameterValue<IParameter>;
|
||||
deleteFlag: CtrlObject["deleteFlag"];
|
||||
labels: IObjectParamArchiveType[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -47,7 +53,7 @@ class CtrlObject<A extends IAnyObject = IAnyObject> extends LabelObject {
|
||||
/**
|
||||
* 控制模型
|
||||
*/
|
||||
protected model: Model;
|
||||
public model: Model;
|
||||
|
||||
/**
|
||||
* 渲染数据
|
||||
@@ -117,20 +123,26 @@ class CtrlObject<A extends IAnyObject = IAnyObject> extends LabelObject {
|
||||
update: !!this.update,
|
||||
id: this.id,
|
||||
renderParameter: parameter2ArchiveObject(this.renderParameter),
|
||||
deleteFlag: !!this.deleteFlag
|
||||
deleteFlag: !!this.deleteFlag,
|
||||
labels: this.labels.map((label) => {
|
||||
return object2ArchiveObject(label);
|
||||
})
|
||||
} as any;
|
||||
}
|
||||
|
||||
public fromArchive(archive: IArchiveCtrlObject & A, paster?: IArchiveParseFn): void {
|
||||
public fromArchive(archive: IArchiveCtrlObject & A, paster: IArchiveParseFn): void {
|
||||
this.displayName = archive.displayName;
|
||||
this.color = archive.color.concat([]);
|
||||
this.display = !!archive.display;
|
||||
this.update = !!archive.update;
|
||||
this.id = archive.id;
|
||||
this.renderParameter = archiveObject2Parameter(
|
||||
archive.renderParameter, paster ?? (() => undefined)
|
||||
);
|
||||
this.deleteFlag = !!archive.deleteFlag;
|
||||
this.renderParameter = archiveObject2Parameter(
|
||||
archive.renderParameter, paster
|
||||
);
|
||||
this.labels = archive.labels.map((obj) => {
|
||||
return paster(obj) as any;
|
||||
}).filter((c) => !!c);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+54
-5
@@ -1,26 +1,47 @@
|
||||
import { Individual } from "@Model/Individual";
|
||||
import { CtrlObject } from "@Model/CtrlObject";
|
||||
import { CtrlObject, IArchiveCtrlObject } from "@Model/CtrlObject";
|
||||
import type { Behavior, IAnyBehavior } from "@Model/Behavior";
|
||||
import { Label } from "@Model/Label";
|
||||
import { Range } from "@Model/Range";
|
||||
import { Model, ObjectID } from "@Model/Model";
|
||||
import { getDefaultValue } from "@Model/Parameter";
|
||||
import { IArchiveIndividual } from "@Model/Individual";
|
||||
import {
|
||||
getDefaultValue, IArchiveParseFn,
|
||||
IObjectParamArchiveType, object2ArchiveObject
|
||||
} from "@Model/Parameter";
|
||||
|
||||
enum GenMod {
|
||||
Point = "p",
|
||||
Range = "R"
|
||||
}
|
||||
|
||||
interface IArchiveGroup {
|
||||
individuals: IArchiveIndividual[];
|
||||
genMethod: Group["genMethod"];
|
||||
genPoint: Group["genPoint"];
|
||||
genRange: IObjectParamArchiveType | undefined;
|
||||
genCount: Group["genCount"];
|
||||
genErrorMessage: Group["genErrorMessage"];
|
||||
genErrorMessageShowCount: Group["genErrorMessageShowCount"];
|
||||
killCount: Group["killCount"];
|
||||
behaviors: IObjectParamArchiveType[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 群体类型
|
||||
*/
|
||||
class Group extends CtrlObject {
|
||||
class Group extends CtrlObject<IArchiveGroup> {
|
||||
|
||||
/**
|
||||
* 所有个体
|
||||
*/
|
||||
public individuals: Set<Individual> = new Set();
|
||||
|
||||
/**
|
||||
* 缓存的 individuals 数组, 用于存档加载
|
||||
*/
|
||||
public cacheIndividualsArray: Array<Individual> = [];
|
||||
|
||||
/**
|
||||
* 个体生成方式
|
||||
*/
|
||||
@@ -406,6 +427,35 @@ class Group extends CtrlObject {
|
||||
});
|
||||
return dataBuffer;
|
||||
}
|
||||
|
||||
public override toArchive(): IArchiveCtrlObject & IArchiveGroup {
|
||||
return {
|
||||
...super.toArchive(),
|
||||
objectType: "G",
|
||||
genMethod: this.genMethod,
|
||||
genPoint: this.genPoint.concat([]),
|
||||
genRange: object2ArchiveObject(this.genRange) as IObjectParamArchiveType,
|
||||
genCount: this.genCount,
|
||||
genErrorMessage: this.genErrorMessage,
|
||||
genErrorMessageShowCount: this.genErrorMessageShowCount,
|
||||
killCount: this.killCount,
|
||||
behaviors: object2ArchiveObject(this.behaviors) as IObjectParamArchiveType[]
|
||||
};
|
||||
}
|
||||
|
||||
public override fromArchive(archive: IArchiveCtrlObject & IArchiveGroup, paster: IArchiveParseFn): void {
|
||||
super.fromArchive(archive, paster);
|
||||
this.genMethod = archive.genMethod,
|
||||
this.genPoint = archive.genPoint.concat([]),
|
||||
this.genRange = archive.genRange ? paster(archive.genRange) as any : undefined,
|
||||
this.genCount = archive.genCount,
|
||||
this.genErrorMessage = archive.genErrorMessage,
|
||||
this.genErrorMessageShowCount = archive.genErrorMessageShowCount,
|
||||
this.killCount = archive.killCount,
|
||||
this.behaviors = archive.behaviors.map((item) => {
|
||||
return item ? paster(item) as any : undefined;
|
||||
}).filter(c => !!c);
|
||||
}
|
||||
|
||||
public constructor(model: Model) {
|
||||
|
||||
@@ -417,5 +467,4 @@ class Group extends CtrlObject {
|
||||
}
|
||||
}
|
||||
|
||||
export default Group;
|
||||
export { Group, GenMod };
|
||||
export { Group, GenMod, IArchiveGroup };
|
||||
@@ -1,5 +1,15 @@
|
||||
import type { Group } from "@Model/Group";
|
||||
import { ObjectID } from "@Model/Model";
|
||||
import { IAnyObject, ObjectID } from "@Model/Model";
|
||||
import { IArchiveParseFn, object2ArchiveObject, isArchiveObjectType } from "@Model/Parameter";
|
||||
|
||||
interface IArchiveIndividual {
|
||||
id: string;
|
||||
position: number[];
|
||||
velocity: number[];
|
||||
acceleration: number[];
|
||||
force: number[];
|
||||
metaData: IAnyObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* 群中的个体类型
|
||||
@@ -47,6 +57,8 @@ class Individual {
|
||||
}
|
||||
}
|
||||
|
||||
public id: string;
|
||||
|
||||
/**
|
||||
* 坐标
|
||||
*/
|
||||
@@ -95,6 +107,7 @@ class Individual {
|
||||
*/
|
||||
public constructor(group: Group) {
|
||||
this.group = group;
|
||||
this.id = this.group.model.getNextIndividualId();
|
||||
}
|
||||
|
||||
public isDie(): boolean {
|
||||
@@ -167,7 +180,67 @@ class Individual {
|
||||
this.metaData.set(key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
public toArchive(): IArchiveIndividual {
|
||||
|
||||
const metaDataArchive = {} as IAnyObject;
|
||||
this.metaData.forEach((value, key) => {
|
||||
|
||||
// 处理内置对象
|
||||
let ltObject = object2ArchiveObject(value);
|
||||
if (ltObject) {
|
||||
metaDataArchive[key] = ltObject;
|
||||
}
|
||||
|
||||
// 处理数组
|
||||
else if (Array.isArray(value)) {
|
||||
metaDataArchive[key] = value.concat([]);
|
||||
}
|
||||
|
||||
// 处理值
|
||||
else {
|
||||
metaDataArchive[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
id: this.id,
|
||||
position: this.position.concat([]),
|
||||
velocity: this.velocity.concat([]),
|
||||
acceleration: this.acceleration.concat([]),
|
||||
force: this.force.concat([]),
|
||||
metaData: metaDataArchive
|
||||
};
|
||||
}
|
||||
|
||||
public fromArchive(archive: IArchiveIndividual, paster: IArchiveParseFn): void {
|
||||
|
||||
const metaData = new Map() as Map<ObjectID, any>;
|
||||
for (const key in archive.metaData) {
|
||||
|
||||
const value = archive.metaData[key];
|
||||
|
||||
// 处理内置对象
|
||||
if (value instanceof Object && isArchiveObjectType(value)) {
|
||||
metaData.set(key, paster(value));
|
||||
}
|
||||
|
||||
else if (Array.isArray(value)) {
|
||||
metaData.set(key, value.concat([]));
|
||||
}
|
||||
|
||||
else {
|
||||
metaData.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
this.id = archive.id,
|
||||
this.position = archive.position.concat([]),
|
||||
this.velocity = archive.velocity.concat([]),
|
||||
this.acceleration = archive.acceleration.concat([]),
|
||||
this.force = archive.force.concat([]),
|
||||
this.metaData = metaData;
|
||||
}
|
||||
}
|
||||
|
||||
export default Individual;
|
||||
export { Individual };
|
||||
export { Individual, IArchiveIndividual };
|
||||
+30
-3
@@ -1,6 +1,14 @@
|
||||
import type { Model, ObjectID } from "@Model/Model";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
interface IArchiveLabel {
|
||||
isBuildIn: boolean;
|
||||
id: string;
|
||||
name: string;
|
||||
color: number[];
|
||||
deleteFlag: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据标签
|
||||
*/
|
||||
@@ -78,10 +86,29 @@ class Label {
|
||||
/**
|
||||
* 设置为内置标签
|
||||
*/
|
||||
public setBuildInLabel(): this {
|
||||
public setBuildInLabel(id: string): this {
|
||||
this.id = id;
|
||||
this.isBuildIn = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public toArchive(): IArchiveLabel {
|
||||
return {
|
||||
isBuildIn: this.isBuildIn,
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
color: this.color.concat([]),
|
||||
deleteFlag: this.deleteFlag
|
||||
} as any;
|
||||
}
|
||||
|
||||
public fromArchive(archive: IArchiveLabel): void {
|
||||
this.isBuildIn = archive.isBuildIn,
|
||||
this.id = archive.id,
|
||||
this.name = archive.name,
|
||||
this.color = archive.color.concat([]),
|
||||
this.deleteFlag = archive.deleteFlag
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,7 +119,7 @@ class LabelObject {
|
||||
/**
|
||||
* 标签集合
|
||||
*/
|
||||
private labels: Label[] = [];
|
||||
public labels: Label[] = [];
|
||||
|
||||
/**
|
||||
* 获取全部 Label
|
||||
@@ -132,4 +159,4 @@ class LabelObject {
|
||||
}
|
||||
}
|
||||
|
||||
export { Label, LabelObject };
|
||||
export { Label, LabelObject, IArchiveLabel };
|
||||
+14
-3
@@ -29,6 +29,17 @@ type ModelEvent = {
|
||||
*/
|
||||
class Model extends Emitter<ModelEvent> {
|
||||
|
||||
/**
|
||||
* 个体 ID 生成
|
||||
*/
|
||||
public nextIndividualId: number = 0;
|
||||
|
||||
public getNextIndividualId() {
|
||||
this.nextIndividualId ++;
|
||||
const random = Math.random().toString(36).slice(-8);
|
||||
return `${this.nextIndividualId}-${random}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对象列表
|
||||
*/
|
||||
@@ -50,17 +61,17 @@ class Model extends Emitter<ModelEvent> {
|
||||
/**
|
||||
* 内置标签-全部范围标签
|
||||
*/
|
||||
public allRangeLabel = new Label(this, "AllRange").setBuildInLabel();
|
||||
public allRangeLabel = new Label(this).setBuildInLabel("AllRange");
|
||||
|
||||
/**
|
||||
* 内置标签-全部群标签
|
||||
*/
|
||||
public allGroupLabel = new Label(this, "AllGroup").setBuildInLabel();
|
||||
public allGroupLabel = new Label(this).setBuildInLabel("AllGroup");
|
||||
|
||||
/**
|
||||
* 内置标签-全部群标签
|
||||
*/
|
||||
public currentGroupLabel = new Label(this, "CurrentGroupLabel").setBuildInLabel();
|
||||
public currentGroupLabel = new Label(this).setBuildInLabel("CurrentGroupLabel");
|
||||
|
||||
/**
|
||||
* 添加标签
|
||||
|
||||
+23
-14
@@ -1,11 +1,13 @@
|
||||
import { Group } from "@Model/Group";
|
||||
import { Range } from "@Model/Range";
|
||||
import { Label } from "@Model/Label";
|
||||
import { Behavior } from "@Model/Behavior";
|
||||
import { Behavior, IAnyBehavior } from "@Model/Behavior";
|
||||
import { Individual } from "@Model/Individual";
|
||||
import { CtrlObject } from "@Model/CtrlObject";
|
||||
|
||||
type IObjectParamArchiveType = {
|
||||
__LIVING_TOGETHER_OBJECT_ID: string;
|
||||
__LIVING_TOGETHER_OBJECT_TYPE: string;
|
||||
__LIVING_TOGETHER_OBJECT_TYPE: "Individual" | "Range" | "Group" | "Label" | "Behavior";
|
||||
}
|
||||
|
||||
type IObjectParamCacheType<P, Q = P> = {
|
||||
@@ -224,30 +226,36 @@ function getDefaultValue<P extends IParameter> (option: IParameterOption<P>): IP
|
||||
return defaultObj;
|
||||
}
|
||||
|
||||
type IRealObjectType = Range | Group | Label | Behavior;
|
||||
type IRealObjectType = Range | Group | Label | IAnyBehavior | Individual | CtrlObject;
|
||||
type IArchiveParseFn = (archive: IObjectParamArchiveType) => IRealObjectType | undefined;
|
||||
|
||||
function object2ArchiveObject(object: IRealObjectType | IRealObjectType[] | any, testArray: boolean = true):
|
||||
IObjectParamArchiveType | IObjectParamArchiveType[] | undefined {
|
||||
if (object instanceof Range) {
|
||||
if (object instanceof Individual) {
|
||||
return {
|
||||
__LIVING_TOGETHER_OBJECT_ID: "Range",
|
||||
__LIVING_TOGETHER_OBJECT_TYPE: object.id
|
||||
__LIVING_TOGETHER_OBJECT_ID: object.id,
|
||||
__LIVING_TOGETHER_OBJECT_TYPE: "Individual"
|
||||
}
|
||||
}
|
||||
else if (object instanceof Range) {
|
||||
return {
|
||||
__LIVING_TOGETHER_OBJECT_ID: object.id,
|
||||
__LIVING_TOGETHER_OBJECT_TYPE: "Range"
|
||||
}
|
||||
} else if (object instanceof Group) {
|
||||
return {
|
||||
__LIVING_TOGETHER_OBJECT_ID: "Group",
|
||||
__LIVING_TOGETHER_OBJECT_TYPE: object.id
|
||||
__LIVING_TOGETHER_OBJECT_ID: object.id,
|
||||
__LIVING_TOGETHER_OBJECT_TYPE: "Group"
|
||||
}
|
||||
} else if (object instanceof Label) {
|
||||
return {
|
||||
__LIVING_TOGETHER_OBJECT_ID: "Label",
|
||||
__LIVING_TOGETHER_OBJECT_TYPE: object.id
|
||||
__LIVING_TOGETHER_OBJECT_ID: object.id,
|
||||
__LIVING_TOGETHER_OBJECT_TYPE: "Label"
|
||||
}
|
||||
} else if (object instanceof Behavior) {
|
||||
return {
|
||||
__LIVING_TOGETHER_OBJECT_ID: "Behavior",
|
||||
__LIVING_TOGETHER_OBJECT_TYPE: object.id
|
||||
__LIVING_TOGETHER_OBJECT_ID: object.id,
|
||||
__LIVING_TOGETHER_OBJECT_TYPE: "Behavior"
|
||||
}
|
||||
} else if (Array.isArray(object) && testArray) {
|
||||
const hasValue = (item: any): item is IObjectParamArchiveType => !!item;
|
||||
@@ -315,7 +323,7 @@ function archiveObject2Parameter<P extends IParameter>
|
||||
|
||||
(parameter[key] as any) = {
|
||||
picker: picker ? parse(picker) : picker,
|
||||
objects: objects ? parse(objects) : objects
|
||||
objects: objects ? Array.isArray(objects) ? objects.map(item => parse(item)) : parse(objects) : objects
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,5 +359,6 @@ export {
|
||||
IParamType, IParamValue, isObjectType, isVectorType, getDefaultValue,
|
||||
IParameterOptionItem, IParameter, IParameterOption, IParameterValue,
|
||||
object2ArchiveObject, parameter2ArchiveObject, archiveObject2Parameter,
|
||||
IArchiveParseFn
|
||||
IArchiveParseFn, IObjectParamArchiveType, isArchiveObjectType,
|
||||
IArchiveParameterValue, IRealObjectType
|
||||
}
|
||||
@@ -34,12 +34,13 @@ class Range extends CtrlObject<IArchiveRange> {
|
||||
public override toArchive(): IArchiveCtrlObject & IArchiveRange {
|
||||
return {
|
||||
...super.toArchive(),
|
||||
objectType: "R",
|
||||
position: this.position.concat([]),
|
||||
radius: this.radius.concat([])
|
||||
};
|
||||
}
|
||||
|
||||
public override fromArchive(archive: IArchiveCtrlObject & IArchiveRange, paster?: IArchiveParseFn): void {
|
||||
public override fromArchive(archive: IArchiveCtrlObject & IArchiveRange, paster: IArchiveParseFn): void {
|
||||
super.fromArchive(archive, paster);
|
||||
this.position = archive.position.concat([]),
|
||||
this.radius = archive.radius.concat([])
|
||||
|
||||
@@ -13,6 +13,7 @@ import { HeaderBar } from "@Component/HeaderBar/HeaderBar";
|
||||
import { Popup } from "@Component/Popup/Popup";
|
||||
import { Entry } from "../Entry/Entry";
|
||||
import { Group } from "@Model/Group";
|
||||
import DEMO from "../demo";
|
||||
import "./SimulatorDesktop.scss";
|
||||
|
||||
initializeIcons("./font-icon/");
|
||||
@@ -55,8 +56,14 @@ class SimulatorDesktop extends Component {
|
||||
})
|
||||
};
|
||||
|
||||
(window as any).setting = this.setting;
|
||||
(window as any).status = this.status;
|
||||
setTimeout(() => {
|
||||
this.status.archive.load(this.status.model, DEMO);
|
||||
}, 200);
|
||||
|
||||
(window as any).LT = {
|
||||
status: this.status,
|
||||
setting: this.setting
|
||||
};
|
||||
|
||||
this.electron = {} as ISimulatorAPI;
|
||||
if ((window as any).API) {
|
||||
@@ -117,8 +124,8 @@ class SimulatorDesktop extends Component {
|
||||
<div className="app-root-space" style={{
|
||||
height: `calc( 100% - ${35}px)`
|
||||
}}>
|
||||
<CommandBar width={45}/>
|
||||
<RootContainer />
|
||||
<CommandBar/>
|
||||
<RootContainer/>
|
||||
</div>
|
||||
</Theme>
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@ 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";
|
||||
import { Popup } from "@Component/Popup/Popup";
|
||||
import { Entry } from "../Entry/Entry";
|
||||
import { Group } from "@Model/Group";
|
||||
import DEMO from "../demo";
|
||||
import "./SimulatorWeb.scss";
|
||||
|
||||
initializeIcons("https://img.mrkbear.com/fabric-cdn-prod_20210407.001/");
|
||||
@@ -76,7 +78,7 @@ class SimulatorWeb extends Component {
|
||||
}
|
||||
|
||||
// 鱼群模型测试
|
||||
if (true) {
|
||||
if (false) {
|
||||
let fish1 = this.status.newGroup();
|
||||
let fish2 = this.status.newGroup();
|
||||
let shark = this.status.newGroup();
|
||||
@@ -153,6 +155,10 @@ class SimulatorWeb extends Component {
|
||||
}, 200)
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.status.archive.load(this.status.model, DEMO);
|
||||
}, 200);
|
||||
|
||||
(window as any).LT = {
|
||||
status: this.status,
|
||||
setting: this.setting
|
||||
@@ -203,13 +209,14 @@ class SimulatorWeb extends Component {
|
||||
backgroundLevel={BackgroundLevel.Level5}
|
||||
fontLevel={FontLevel.Level3}
|
||||
>
|
||||
<LoadFile/>
|
||||
<Popup/>
|
||||
<HeaderBar height={45}/>
|
||||
<div className="app-root-space" style={{
|
||||
height: `calc( 100% - ${45}px)`
|
||||
}}>
|
||||
<CommandBar width={45}/>
|
||||
<RootContainer />
|
||||
<CommandBar/>
|
||||
<RootContainer/>
|
||||
</div>
|
||||
</Theme>
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -44,7 +44,7 @@ class ObjectCommand extends Component<IMixinStatusProps> {
|
||||
this.props.status ? this.props.status.newGroup() : undefined;
|
||||
}}
|
||||
>
|
||||
<Icon iconName="WebAppBuilderFragmentCreate"></Icon>
|
||||
<Icon iconName="WebAppBuilderFragment"></Icon>
|
||||
</div>
|
||||
<div
|
||||
className="command-item"
|
||||
@@ -52,7 +52,7 @@ class ObjectCommand extends Component<IMixinStatusProps> {
|
||||
this.props.status ? this.props.status.newRange() : undefined;
|
||||
}}
|
||||
>
|
||||
<Icon iconName="ProductVariant"></Icon>
|
||||
<Icon iconName="CubeShape"></Icon>
|
||||
</div>
|
||||
<div
|
||||
className="command-item red"
|
||||
|
||||
Reference in New Issue
Block a user