35 Commits
Author SHA1 Message Date
MrKBear 396b128a2c Add demo 2022-04-24 14:45:17 +08:00
MrKBear 298688269c Release pre 2022-04-24 14:27:25 +08:00
MrKBear 1f08a4885b Merge pull request 'Add archive save component & shutdown hendel' (#45) from dev-mrkbear into master
Reviewed-on: http://git.mrkbear.com/MrKBear/living-together/pulls/45
2022-04-24 14:13:17 +08:00
MrKBear 4c014a7200 Merge branch 'master' into dev-mrkbear 2022-04-24 14:12:45 +08:00
MrKBear b5ecf0bb0d Add loadfile layer 2022-04-24 14:10:39 +08:00
MrKBear 87ed157340 Add block shutdown hendel 2022-04-24 13:24:55 +08:00
MrKBear 56151c9e75 Add archive save component 2022-04-24 12:55:00 +08:00
MrKBear 03f0b9fd16 Add file download function 2022-04-23 23:28:11 +08:00
MrKBear 5914a9f531 Merge pull request 'Add model & behavior archive function' (#44) from dev-mrkbear into master
Reviewed-on: http://git.mrkbear.com/MrKBear/living-together/pulls/44
2022-04-23 20:33:01 +08:00
MrKBear 980d8afa3c Merge branch 'master' into dev-mrkbear 2022-04-23 20:32:54 +08:00
MrKBear b498f0e0a4 Add archive event 2022-04-23 20:32:04 +08:00
MrKBear 430d8a7254 Add model archive function 2022-04-23 19:52:56 +08:00
MrKBear 74bda21072 Add behavior archive function 2022-04-23 17:42:02 +08:00
MrKBear 0d7cbe135d Merge pull request 'Add group individual label archive function' (#43) from dev-mrkbear into master
Reviewed-on: http://git.mrkbear.com/MrKBear/living-together/pulls/43
2022-04-23 16:56:07 +08:00
MrKBear 068acb19a8 Add label archive function 2022-04-22 23:48:10 +08:00
MrKBear 276c8c63a1 Add group & individual archive function 2022-04-22 20:48:53 +08:00
MrKBear d11fd2d328 Add group archive function 2022-04-21 23:22:38 +08:00
MrKBear 6063882192 Merge pull request 'Add parameter archive func & Add ctrl object archive function' (#42) from dev-mrkbear into master
Reviewed-on: http://git.mrkbear.com/MrKBear/living-together/pulls/42
2022-04-21 19:49:19 +08:00
ben.qin de0dd57a04 Optmize archive function type 2022-04-21 17:25:25 +08:00
ben.qin 1d36aac37d Add range object archive function 2022-04-21 17:20:03 +08:00
ben.qin 597c7e9493 Add ctrl object archive function 2022-04-21 17:03:01 +08:00
MrKBear 21960778f5 Add parameter archive func 2022-04-20 23:23:51 +08:00
MrKBear a2b1819dd8 Change object id into uuid 2022-04-18 17:23:30 +08:00
MrKBear 26837930fe Merge pull request 'Add auto bundle script' (#41) from dev-mrkbear into master
Reviewed-on: http://git.mrkbear.com/MrKBear/living-together/pulls/41
2022-04-17 14:08:04 +08:00
MrKBear a788968525 Add bundle script 2022-04-17 13:50:43 +08:00
MrKBear 4f6b1ebbf2 Add auto bundle script 2022-04-17 01:26:05 +08:00
MrKBear 8b29654fdf Bind windows api 2022-04-16 13:54:56 +08:00
MrKBear 4f175a5505 Add electron api context 2022-04-15 22:51:38 +08:00
MrKBear 77410dbf47 Merge pull request 'Add electron entry' (#40) from dev-mrkbear into master
Reviewed-on: http://git.mrkbear.com/MrKBear/living-together/pulls/40
2022-04-15 20:20:38 +08:00
MrKBear 7e4fee3fe5 Add electron minthread 2022-04-15 17:42:29 +08:00
MrKBear ff88c7047e Add electron min thread entry 2022-04-15 15:31:47 +08:00
MrKBear 61e590df4d Add electron package 2022-04-15 12:43:02 +08:00
MrKBear 6bea46204d Add simulator desktop entry 2022-04-15 10:56:43 +08:00
MrKBear 76bb20d5ab Change package rimraf into fse 2022-04-15 09:38:13 +08:00
MrKBear 0fc5d4c6d4 Merge pull request 'Add tracking assimilate attacking behaviot' (#39) from dev-mrkbear into master
Reviewed-on: http://git.mrkbear.com/MrKBear/living-together/pulls/39
2022-04-12 21:35:54 +08:00
47 changed files with 4266 additions and 262 deletions
+1
View File
@@ -42,6 +42,7 @@ node_modules/
jspm_packages/
build/
out/
bundle/
# TypeScript v1 declaration files
typings/
Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

+68
View File
@@ -0,0 +1,68 @@
const FS = require("fs");
const Path = require("path");
const minimist = require("minimist");
const args = minimist(process.argv.slice(2));
const PackageJSON = JSON.parse(
FS.readFileSync(Path.join(__dirname, "../package.json"))
);
const Config = {
"name": PackageJSON.name,
"productName": PackageJSON.name,
"version": PackageJSON.version,
"description": PackageJSON.description,
"main": "./Electron.js",
"scripts": {
"start": "electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make",
"publish": "electron-forge publish",
"lint": "echo \"No linting configured\""
},
"keywords": PackageJSON.keywords,
"author": {
"name": PackageJSON.author,
"email": "mrkbear@qq.com"
},
"license": PackageJSON.license,
"config": {
"forge": {
"packagerConfig": {
"appBundleId": "com.mrkbear.living-together",
"appCopyright": "2021-2022 © copyright MrKBear",
"download": {
"rejectUnauthorized": false,
"executableName": "LivingTogether",
"mirrorOptions": {
"mirror": 'https://npmmirror.com/mirrors/electron/',
"customDir": '{{ version }}',
}
},
"asar": true,
"icon": "./living-together"
},
"makers": [
{
"name": "@electron-forge/maker-zip",
"platforms": [
"darwin"
]
}
]
}
},
"dependencies": {
"electron-squirrel-startup": "^1.0.0",
"detect-port": PackageJSON.dependencies["detect-port"],
"express": PackageJSON.dependencies["express"],
},
"devDependencies": {
"@electron-forge/cli": "^6.0.0-beta.63",
"@electron-forge/maker-zip": "^6.0.0-beta.63",
"electron": PackageJSON.devDependencies.electron
}
}
FS.writeFileSync(Path.join(Path.resolve("./"), args.out ?? "./", "./package.json"), JSON.stringify(Config, null, 4));
+14 -4
View File
@@ -58,10 +58,6 @@ const Entry = () => ({
import: source("./GLRender/ClassicRenderer.ts")
},
livingTogether: {
import: source("./livingTogether.ts")
},
LaboratoryPage: {
import: source("./Page/Laboratory/Laboratory.tsx"),
dependOn: ["Model", "GLRender"]
@@ -72,6 +68,11 @@ const Entry = () => ({
dependOn: ["Model", "GLRender"]
},
SimulatorDesktop: {
import: source("./Page/SimulatorDesktop/SimulatorDesktop.tsx"),
dependOn: ["Model", "GLRender"]
},
Service: {
import: source("./Service/Service.ts")
},
@@ -79,6 +80,15 @@ const Entry = () => ({
ServiceRunner: {
import: source("./Service/Runner.ts"),
dependOn: ["Service"]
},
Electron: {
import: source("./Electron/Electron.ts"),
dependOn: ["Service"]
},
SimulatorWindow: {
import: source("./Electron/SimulatorWindow.ts"),
}
});
+56
View File
@@ -0,0 +1,56 @@
const {
Entry, Output, resolve, build,
TypeScriptRules, ScssRules,
HTMLPage, CssPlugin, AutoFixCssPlugin
} = require("./webpack.common");
const AllEntry = Entry();
module.exports = (env) => {
const config = {
entry: {
GLRender: AllEntry.GLRender,
Model: AllEntry.Model,
SimulatorDesktop: AllEntry.SimulatorDesktop
},
output: Output("[name].[contenthash].js"),
devtool: 'source-map',
mode: "development",
resolve: resolve(),
optimization: {
runtimeChunk: 'single',
chunkIds: 'named',
moduleIds: 'named',
splitChunks: {
chunks: 'all',
minSize: 1000
}
},
module: {
rules: [
TypeScriptRules(),
ScssRules()
]
},
plugins: [
HTMLPage("index.html", "Living Together | Simulator"),
CssPlugin(),
AutoFixCssPlugin()
],
devServer: {
static: {
directory: build("./"),
},
port: 12000,
}
};
return config;
};
+46
View File
@@ -0,0 +1,46 @@
const { Entry, Output, resolve, TypeScriptRules } = require("./webpack.common");
const nodeExternals = require("webpack-node-externals");
const AllEntry = Entry();
module.exports = (env) => {
const config = {
entry: {
Service: AllEntry.Service,
Electron: AllEntry.Electron,
SimulatorWindow: AllEntry.SimulatorWindow
},
output: Output("[name].js"),
devtool: 'source-map',
mode: "development",
resolve: resolve(),
optimization: {
splitChunks: {
chunks: 'all',
minSize: 1000
}
},
// externals: [nodeExternals({ allowlist: [/^(((?!electron).)*)$/] })],
externals: [nodeExternals()],
module: {
rules: [
TypeScriptRules()
]
},
node: {
__filename: false,
__dirname: false
},
target: 'node'
};
return config;
};
+2483 -48
View File
File diff suppressed because it is too large Load Diff
+30 -5
View File
@@ -4,18 +4,37 @@
"description": "An art interactive works for graduation design.",
"main": "./source/LivingTogether.ts",
"scripts": {
"clean": "rimraf ./build/*",
"clean": "fse emptyDir ./build/",
"hmr-lab": "webpack serve --open --config ./config/webpack.lab.js",
"build-lab": "npm run clean & webpack --mode development --config ./config/webpack.lab.js",
"release-lab": "npm run clean & webpack --mode production --no-devtool --config ./config/webpack.lab.js",
"hmr-web": "webpack serve --open --config ./config/webpack.web.js",
"hmr-desktop": "webpack serve --open --config ./config/webpack.desktop.js",
"build-web": "npm run clean & webpack --mode development --config ./config/webpack.web.js",
"release-web": "npm run clean & webpack --mode production --no-devtool --config ./config/webpack.web.js",
"build-desktop-web": "npm run clean & webpack --mode development --config ./config/webpack.desktop.js",
"release-desktop-web": "npm run clean & webpack --mode production --no-devtool --config ./config/webpack.desktop.js",
"build-service": "webpack --mode development --config ./config/webpack.service.js",
"release-service": "webpack --mode production --no-devtool --config ./config/webpack.service.js",
"run-service": "node ./build/ServiceRunner.js --run --path ./build --port 12000",
"build-run-web": "npm run build-web & npm run build-service & npm run run-service",
"release-run-web": "npm run release-web & npm run release-service & npm run run-service"
"release-run-web": "npm run release-web & npm run release-service & npm run run-service",
"copy-fluent-icon": "fse mkdirp ./build/font-icon/ & fse emptyDir ./build/font-icon/ & fse copy ./node_modules/@fluentui/font-icons-mdl2/fonts/ ./build/font-icon/",
"build-run-desktop-web": "npm run build-desktop-web & npm run copy-fluent-icon & npm run build-service & npm run run-service",
"release-run-desktop-web": "npm run release-desktop-web & npm run copy-fluent-icon & npm run release-service & npm run run-service",
"skip-electron-ci": "set ELECTRON_SKIP_BINARY_DOWNLOAD=1& npm ci",
"build-electron": "webpack --mode development --config ./config/webpack.electron.js",
"release-electron": "webpack --mode production --no-devtool --config ./config/webpack.electron.js",
"electron-cache": "set ELECTRON_SKIP_BINARY_DOWNLOAD=& set ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/& set ELECTRON_CUSTOM_DIR={{ version }}& node ./node_modules/electron/install.js",
"electron": "set LIVING_TOGETHER_BASE_PATH=./build& set LIVING_TOGETHER_WEB_PATH=/& npx electron ./build/Electron.js",
"hmr-electron": "npm run build-electron & set LIVING_TOGETHER_SERVICE=http://127.0.0.1:12000& npm run electron",
"build-run-electron": "npm run build-desktop-web & npm run copy-fluent-icon & npm run build-electron & npm run electron",
"release-run-electron": "npm run release-desktop-web & npm run copy-fluent-icon & npm run release-electron & npm run electron",
"copy-package-json": "fse mkdirp ./bundle/ & node ./config/electron.forge.config.js --out ./bundle",
"copy-build-result": "fse mkdirp ./bundle/ & fse mkdirp ./build/ & fse copy ./build/ ./bundle/",
"copy-electron-icon": "fse mkdirp ./bundle/ & fse copy ./assets/living-together.ico ./bundle/living-together.ico & fse copy ./assets/living-together.icns ./bundle/living-together.icns",
"electron-app-ci": "cd ./bundle & npm install & cd ../",
"gen-bundle": "fse emptyDir ./bundle/ & npm run copy-package-json & npm run copy-electron-icon & npm run electron-app-ci"
},
"keywords": [
"artwork",
@@ -26,31 +45,37 @@
"author": "MrKBear",
"license": "GPL",
"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",
"autoprefixer": "^10.4.2",
"css-loader": "^6.5.1",
"electron": "^18.0.4",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.4.6",
"node-sass": "^7.0.1",
"postcss-loader": "^6.2.1",
"rimraf": "^3.0.2",
"sass-loader": "^12.4.0",
"ts-loader": "^9.2.6",
"tsconfig-paths-webpack-plugin": "^3.5.2",
"typescript": "^4.5.4",
"webpack": "^5.65.0",
"webpack-cli": "^4.9.1",
"webpack-dev-server": "^4.7.2"
"webpack-dev-server": "^4.7.2",
"webpack-node-externals": "^3.0.0"
},
"dependencies": {
"@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",
"react-dom": "^17.0.2"
"react-dom": "^17.0.2",
"uuid": "^8.3.2"
}
}
+1 -1
View File
@@ -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),
+2 -2
View File
@@ -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 = {
@@ -117,10 +117,26 @@ class BehaviorPopupComponent extends Component<
if (this.props.status && recorder instanceof BehaviorRecorder) {
let newBehavior = this.props.status.model.addBehavior(recorder);
// 初始化名字
newBehavior.name = recorder.getTerms(
// 根据用户的命名搜索下一个名字
let searchKey = recorder.getTerms(
recorder.behaviorName, this.props.setting?.language
) + " " + (recorder.nameIndex - 1).toString();
);
let nextIndex = 1;
this.props.status.model.behaviorPool.forEach((obj) => {
if (obj.behaviorId === recorder.behaviorId && obj.name.indexOf(searchKey) >= 0) {
let searchRes = obj.name.match(/(\d+)$/);
if (searchRes) {
let nameNumber = parseInt(searchRes[1]);
if (!isNaN(nameNumber)) {
nextIndex = Math.max(nextIndex, nameNumber + 1);
}
}
}
});
// 初始化名字
newBehavior.name = `${searchKey} ${nextIndex}`;
// 赋予一个随机颜色
newBehavior.color = randomColor(true);
+69 -37
View File
@@ -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 };
+28 -5
View File
@@ -7,6 +7,9 @@ div.header-bar {
justify-content: space-between;
user-select: none;
// 在 Electron 中用于拖拽窗口
-webkit-app-region: drag;
div.title {
padding-left: 3px;
}
@@ -32,17 +35,20 @@ div.header-bar {
div.header-windows-action {
height: 100%;
width: 135px;
min-width: 135px;
width: 145px;
min-width: 145px;
display: flex;
// 在 Electron 中用于拖拽窗口
-webkit-app-region: no-drag;
div.action-button {
width: 45px;
height: 45px;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
font-size: .5em;
font-size: .8em;
}
div.action-button:hover {
@@ -68,4 +74,21 @@ div.header-bar {
background-color: rgba($color: #FFFFFF, $alpha: .1);
}
}
}
div.header-bar.desktop-header-bar {
font-size: .9em;
div.title > i, div.fps-view > i {
font-size: 18px;
}
div.title > span {
display: inline-block;
padding-left: 5px;
}
div.title {
padding-left: 5px;
}
}
+62 -9
View File
@@ -4,6 +4,7 @@ import { useStatusWithEvent, useStatus, IMixinStatusProps } from "@Context/Statu
import { useSettingWithEvent, IMixinSettingProps, Platform } from "@Context/Setting";
import { Theme, BackgroundLevel, FontLevel } from "@Component/Theme/Theme";
import { LocalizationTooltipHost } from "@Component/Localization/LocalizationTooltipHost";
import { useElectronWithEvent, IMixinElectronProps } from "@Context/Electron";
import { I18N } from "@Component/Localization/Localization";
import "./HeaderBar.scss";
@@ -78,18 +79,41 @@ class HeaderFpsView extends Component<IMixinStatusProps & IMixinSettingProps, IH
}
}
class HeaderWindowsAction extends Component {
@useElectronWithEvent("windowsSizeStateChange")
class HeaderWindowsAction extends Component<IMixinElectronProps> {
public render() {
const isMaxSize = this.props.electron?.isMaximized();
return <Theme className="header-windows-action">
<div className="action-button">
<Icon iconName="ChromeMinimize"/>
<div
className="action-button"
onClick={() => {
this.props.electron?.minimize();
}}
>
<Icon iconName="Remove"/>
</div>
<div className="action-button">
<Icon iconName="ChromeRestore"/>
<div
className="action-button"
onClick={() => {
if (isMaxSize) {
this.props.electron?.unMaximize();
} else {
this.props.electron?.maximize();
}
}}
>
<Icon iconName={ isMaxSize ? "ArrangeSendBackward" : "Checkbox"}/>
</div>
<div className="action-button close-button">
<Icon iconName="ChromeClose"/>
<div
className="action-button close-button"
onClick={() => {
this.props.electron?.close()
}}
>
<Icon iconName="Clear"/>
</div>
</Theme>
}
@@ -99,9 +123,33 @@ class HeaderWindowsAction extends Component {
* 头部信息栏
*/
@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;
@@ -115,8 +163,13 @@ class HeaderBar extends Component<IHeaderBarProps & IMixinStatusProps & IMixinSe
isSaved = status.archive.isSaved;
}
const headerBarClassName = ["header-bar"];
if (setting?.platform === Platform.desktop) {
headerBarClassName.push("desktop-header-bar");
}
return <Theme
className="header-bar"
className={headerBarClassName.join(" ")}
backgroundLevel={BackgroundLevel.Level1}
fontLevel={FontLevel.Level3}
style={{ height: this.props.height }}
+38
View File
@@ -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);
}
+31
View File
@@ -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 };
+91
View File
@@ -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 };
+1 -1
View File
@@ -58,7 +58,7 @@ function superConnectWithEvent<C extends Emitter<E>, E extends Record<EventType,
}
}
function superConnect<C extends Emitter<any>>(consumer: Consumer<C>, keyName: string) {
function superConnect<C>(consumer: Consumer<C>, keyName: string) {
return <R extends RenderComponent>(components: R): R => {
return ((props: any) => {
+22
View File
@@ -0,0 +1,22 @@
import { createContext } from "react";
import { superConnect, superConnectWithEvent } from "@Context/Context";
import { ISimulatorAPI, IApiEmitterEvent } from "@Electron/SimulatorAPI";
interface IMixinElectronProps {
electron?: ISimulatorAPI;
}
const ElectronContext = createContext<ISimulatorAPI>((window as any).API ?? {} as ISimulatorAPI);
ElectronContext.displayName = "Electron";
const ElectronProvider = ElectronContext.Provider;
const ElectronConsumer = ElectronContext.Consumer;
/**
* 修饰器
*/
const useElectron = superConnect<ISimulatorAPI>(ElectronConsumer, "electron");
const useElectronWithEvent = superConnectWithEvent<ISimulatorAPI, IApiEmitterEvent>(ElectronConsumer, "electron");
export { useElectron, ElectronProvider, IMixinElectronProps, ISimulatorAPI, useElectronWithEvent };
+1 -1
View File
@@ -37,7 +37,7 @@ class Setting extends Emitter<ISettingEvents> {
/**
* 语言
*/
public language: Language = "EN_US";
public language: Language = "ZH_CN";
/**
* 布局
+84 -17
View File
@@ -30,6 +30,8 @@ function randomColor(unNormal: boolean = false) {
}
interface IStatusEvent {
fileSave: void;
fileLoad: void;
fileChange: void;
renderLoop: number;
physicsLoop: number;
@@ -56,12 +58,6 @@ class Status extends Emitter<IStatusEvent> {
public setting: Setting = undefined as any;
/**
* 对象命名
*/
public objectNameIndex = 1;
public labelNameIndex = 1;
/**
* 渲染器
*/
@@ -133,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 = () => {
@@ -151,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) {
@@ -193,7 +224,6 @@ class Status extends Emitter<IStatusEvent> {
if (range && range instanceof Range) {
range[key] = val;
this.emit("rangeAttrChange");
this.model.draw();
}
}
@@ -206,7 +236,6 @@ class Status extends Emitter<IStatusEvent> {
if (group && group instanceof Group) {
group[key] = val;
this.emit("groupAttrChange");
this.model.draw();
}
}
@@ -300,34 +329,72 @@ class Status extends Emitter<IStatusEvent> {
*/
public mouseMod: MouseMod = MouseMod.Drag;
private readonly SEARCH_NAME_KEY_REG = /(\d+)$/;
private getNextNumber(name: string, searchKey: string): number {
if (name.indexOf(searchKey) < 0) return 1;
let searchRes = name.match(this.SEARCH_NAME_KEY_REG);
if (searchRes) {
let nameNumber = parseInt(searchRes[1]);
if (isNaN(nameNumber)) {
return 1;
} else {
return nameNumber + 1;
}
} else {
return 1;
}
}
public newGroup() {
const group = this.model.addGroup();
group.color = randomColor();
let searchKey = I18N(this.setting.language, "Object.List.New.Group", { id: "" });
let nextIndex = 1;
this.model.objectPool.forEach((obj) => {
if (obj instanceof Group) {
nextIndex = Math.max(nextIndex, this.getNextNumber(
obj.displayName, searchKey
));
}
});
group.displayName = I18N(this.setting.language, "Object.List.New.Group", {
id: this.objectNameIndex.toString()
id: nextIndex.toString()
});
this.objectNameIndex ++;
return group;
}
public newRange() {
const range = this.model.addRange();
range.color = randomColor();
let searchKey = I18N(this.setting.language, "Object.List.New.Range", { id: "" });
let nextIndex = 1;
this.model.objectPool.forEach((obj) => {
if (obj instanceof Range) {
nextIndex = Math.max(nextIndex, this.getNextNumber(
obj.displayName, searchKey
));
}
});
range.displayName = I18N(this.setting.language, "Object.List.New.Range", {
id: this.objectNameIndex.toString()
id: nextIndex.toString()
});
this.objectNameIndex ++;
return range;
}
public newLabel() {
let searchKey = I18N(this.setting.language, "Object.List.New.Label", { id: "" });
let nextIndex = 1;
this.model.labelPool.forEach((obj) => {
nextIndex = Math.max(nextIndex, this.getNextNumber(
obj.name, searchKey
));
});
const label = this.model.addLabel(
I18N(this.setting.language, "Object.List.New.Label", {
id: this.labelNameIndex.toString()
id: nextIndex.toString()
})
);
label.color = randomColor(true);
this.labelNameIndex ++;
return label;
}
+95
View File
@@ -0,0 +1,95 @@
import { app, BrowserWindow, ipcMain } from "electron";
import { Service } from "@Service/Service";
import { join as pathJoin } from "path";
const ENV = process.env ?? {};
class ElectronApp {
public service: Service;
public serviceUrl: string = "http://127.0.0.1";
public constructor() {
this.service = new Service();
}
public async runService() {
if (ENV.LIVING_TOGETHER_SERVICE) {
this.serviceUrl = ENV.LIVING_TOGETHER_SERVICE;
return;
}
let defaultPort: number | undefined = parseInt(ENV.LIVING_TOGETHER_DEFAULT_PORT ?? "");
if (isNaN(defaultPort)) defaultPort = undefined;
this.serviceUrl = await this.service.run(
ENV.LIVING_TOGETHER_BASE_PATH, defaultPort
);
}
public simulatorWindow?: BrowserWindow;
public async runMainThread() {
await app.whenReady();
await this.runService();
let preload = pathJoin(__dirname, "./SimulatorWindow.js");
// if (ENV.LIVING_TOGETHER_BASE_PATH) {
// preload = pathJoin(__dirname, ENV.LIVING_TOGETHER_BASE_PATH, "./SimulatorWindow.js");
// }
this.simulatorWindow = new BrowserWindow({
width: 800,
height: 600,
titleBarStyle: 'hidden',
frame: false,
minWidth: 460,
minHeight: 300,
webPreferences: { preload }
});
this.simulatorWindow.loadURL(this.serviceUrl + (ENV.LIVING_TOGETHER_WEB_PATH ?? "/resources/app.asar/"));
this.handelSimulatorWindowBehavior();
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit()
});
}
private handelSimulatorWindowBehavior() {
ipcMain.on("windows.close", () => {
this.simulatorWindow?.close();
});
ipcMain.on("windows.maximize", () => {
this.simulatorWindow?.maximize();
});
ipcMain.on("windows.unMaximize", () => {
this.simulatorWindow?.unmaximize();
});
ipcMain.on("windows.isMaximized", (event) => {
event.returnValue = this.simulatorWindow?.isMaximized();
});
ipcMain.on("windows.minimize", (event) => {
this.simulatorWindow?.minimize();
});
const sendWindowsChangeMessage = () => {
this.simulatorWindow?.webContents.send("windows.windowsSizeStateChange");
}
this.simulatorWindow?.on("maximize", sendWindowsChangeMessage);
this.simulatorWindow?.on("unmaximize", sendWindowsChangeMessage);
}
}
new ElectronApp().runMainThread();
+35
View File
@@ -0,0 +1,35 @@
import { Emitter } from "@Model/Emitter";
type IApiEmitterEvent = {
windowsSizeStateChange: void;
}
interface ISimulatorAPI extends Emitter<IApiEmitterEvent> {
/**
* 关闭窗口
*/
close: () => void;
/**
* 最大化窗口
*/
maximize: () => void;
/**
* 取消最大化
*/
unMaximize: () => void;
/**
* 是否处于最大化状态
*/
isMaximized: () => boolean;
/**
* 是否处于最大化状态
*/
minimize: () => void;
}
export { ISimulatorAPI, IApiEmitterEvent }
+64
View File
@@ -0,0 +1,64 @@
import { contextBridge, ipcRenderer } from "electron";
import { ISimulatorAPI } from "@Electron/SimulatorAPI"
const emitterMap: Array<[key: string, value: Function[]]> = [];
const queryEmitter = (key: string) => {
let res: (typeof emitterMap)[0] | undefined;
emitterMap.forEach((item) => {
if (item[0] === key) res = item;
});
if (res) {
if (Array.isArray(res[1])) return res[1];
res[1] = [];
return res[1];
}
else {
res = [key, []];
emitterMap.push(res);
return res[1];
}
}
const API: ISimulatorAPI = {
close() {
ipcRenderer.send("windows.close");
},
maximize() {
ipcRenderer.send("windows.maximize");
},
unMaximize() {
ipcRenderer.send("windows.unMaximize");
},
isMaximized() {
return ipcRenderer.sendSync("windows.isMaximized");
},
minimize() {
ipcRenderer.send("windows.minimize");
},
all: new Map() as any,
resetAll: () => emitterMap.splice(0),
reset: (type) => queryEmitter(type).splice(0),
on: (type, handler) => queryEmitter(type).push(handler),
off: (type, handler) => {
const handlers = queryEmitter(type);
handlers.splice(handlers.indexOf(handler!) >>> 0, 1);
},
emit: ((type: string, evt: any) => {
queryEmitter(type).slice().map((handler: any) => { handler(evt) });
}) as any,
}
ipcRenderer.on("windows.windowsSizeStateChange", () => {
API.emit("windowsSizeStateChange");
});
contextBridge.exposeInMainWorld("API", API);
-1
View File
@@ -1 +0,0 @@
export * from "@Model/Model";
+4 -1
View File
@@ -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 -1
View File
@@ -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;
+1 -1
View File
@@ -19,7 +19,7 @@ class Actuator extends Emitter<IActuatorEvent> {
/**
* 模拟帧率
*/
public fps: number = 36;
public fps: number = 42;
/**
* 仿真是否进行
+241 -26
View File
@@ -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,37 +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> = {};
// 保存 Next ID
fileData.idIndex = model.idIndex;
// 保存对象
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 };
+52 -21
View File
@@ -1,8 +1,13 @@
import { Emitter, EventType } from "@Model/Emitter";
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";
/**
* 行为构造函数类型
@@ -10,7 +15,7 @@ import { getDefaultValue, IParameter, IParameterOption, IParameterValue } from "
type IBehaviorConstructor<
P extends IParameter = {},
E extends Record<EventType, any> = {}
> = new (id: string, parameter: IParameterValue<P>) => Behavior<P, E>;
> = new (parameter: IParameterValue<P>) => Behavior<P, E>;
type IAnyBehavior = Behavior<any, any>;
type IAnyBehaviorRecorder = BehaviorRecorder<any, any>;
@@ -76,18 +81,6 @@ class BehaviorRecorder<
E extends Record<EventType, any> = {}
> extends BehaviorInfo<{}> {
/**
* 命名序号
*/
public nameIndex: number = 0;
/**
* 获取下一个 ID
*/
public getNextId() {
return `B-${this.behaviorId}-${this.nameIndex ++}`;
}
/**
* 行为类型
*/
@@ -107,13 +100,13 @@ class BehaviorRecorder<
* 创建一个新的行为实例
*/
public new(): Behavior<P, E> {
return new this.behavior(this.getNextId(), getDefaultValue(this.parameterOption));
return new this.behavior(getDefaultValue(this.parameterOption));
}
public constructor(behavior: IBehaviorConstructor<P, E>) {
super();
this.behavior = behavior;
this.behaviorInstance = new this.behavior(this.getNextId(), {} as any);
this.behaviorInstance = new this.behavior({} as any);
this.parameterOption = this.behaviorInstance.parameterOption;
this.iconName = this.behaviorInstance.iconName;
this.behaviorId = this.behaviorInstance.behaviorId;
@@ -124,6 +117,17 @@ class BehaviorRecorder<
}
}
interface IArchiveBehavior {
behaviorId: string;
name: string;
id: string;
color: number[];
priority: number;
currentGroupKey: string[];
deleteFlag: boolean;
parameter: IArchiveParameterValue<IParameter>;
}
/**
* 群体的某种行为
*/
@@ -168,9 +172,9 @@ class Behavior<
*/
public parameterOption: IParameterOption<P> = {} as any;
public constructor(id: string, parameter: IParameterValue<P>) {
public constructor(parameter: IParameterValue<P>) {
super();
this.id = id;
this.id = uuid();
this.parameter = parameter;
}
@@ -200,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;
}
/**
* 加载时调用
*/
@@ -252,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
};
+53 -6
View File
@@ -1,11 +1,29 @@
import { LabelObject } from "@Model/Label"
import { v4 as uuid } from "uuid";
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: IArchiveParameterValue<IParameter>;
deleteFlag: CtrlObject["deleteFlag"];
labels: IObjectParamArchiveType[];
}
/**
* 可控对象
*/
class CtrlObject extends LabelObject {
class CtrlObject<A extends IAnyObject = IAnyObject> extends LabelObject {
/**
* 显示名称
@@ -35,7 +53,7 @@ class CtrlObject extends LabelObject {
/**
* 控制模型
*/
protected model: Model;
public model: Model;
/**
* 渲染数据
@@ -45,10 +63,10 @@ class CtrlObject extends LabelObject {
/**
* 构造器
*/
public constructor(model: Model, id: ObjectID) {
public constructor(model: Model) {
super();
this.model = model;
this.id = id;
this.id = uuid();
}
/**
@@ -96,7 +114,36 @@ class CtrlObject extends LabelObject {
public isDeleted(): boolean {
return this.deleteFlag;
}
public toArchive(): IArchiveCtrlObject & A {
return {
displayName: this.displayName,
color: this.color.concat([]),
display: !!this.display,
update: !!this.update,
id: this.id,
renderParameter: parameter2ArchiveObject(this.renderParameter),
deleteFlag: !!this.deleteFlag,
labels: this.labels.map((label) => {
return object2ArchiveObject(label);
})
} as any;
}
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.deleteFlag = !!archive.deleteFlag;
this.renderParameter = archiveObject2Parameter(
archive.renderParameter, paster
);
this.labels = archive.labels.map((obj) => {
return paster(obj) as any;
}).filter((c) => !!c);
}
}
export default CtrlObject;
export { CtrlObject };
export { CtrlObject, IArchiveCtrlObject };
+57 -8
View File
@@ -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,10 +427,39 @@ class Group extends CtrlObject {
});
return dataBuffer;
}
public constructor(model: Model, id: ObjectID) {
super(model, id);
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) {
super(model);
if (model.renderer) {
this.renderParameter = getDefaultValue(model.renderer.pointsParameterOption);
@@ -417,5 +467,4 @@ class Group extends CtrlObject {
}
}
export default Group;
export { Group, GenMod };
export { Group, GenMod, IArchiveGroup };
+76 -3
View File
@@ -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 };
+33 -5
View File
@@ -1,4 +1,13 @@
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;
}
/**
* 数据标签
@@ -35,9 +44,9 @@ class Label {
* @param id 标签 ID
* @param name 用户定义的名称
*/
public constructor(model: Model, id: ObjectID, name?: string) {
public constructor(model: Model, name?: string) {
this.model = model;
this.id = id;
this.id = uuid();
this.name = name ?? this.name;
}
@@ -77,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
}
}
/**
@@ -91,7 +119,7 @@ class LabelObject {
/**
* 标签集合
*/
private labels: Label[] = [];
public labels: Label[] = [];
/**
* 获取全部 Label
@@ -131,4 +159,4 @@ class LabelObject {
}
}
export { Label, LabelObject };
export { Label, LabelObject, IArchiveLabel };
+18 -26
View File
@@ -2,9 +2,8 @@ import { Label } from "@Model/Label";
import { Group } from "@Model/Group";
import { Range } from "@Model/Range";
import { IParamValue } from "@Model/Parameter";
import { Individual } from "@Model/Individual";
import { CtrlObject } from "@Model/CtrlObject";
import { Emitter, EventType, EventMixin } from "@Model/Emitter";
import { Emitter } from "@Model/Emitter";
import { AbstractRenderer } from "@Model/Renderer";
import { Behavior, IAnyBehavior, IAnyBehaviorRecorder } from "@Model/Behavior";
@@ -31,11 +30,14 @@ type ModelEvent = {
class Model extends Emitter<ModelEvent> {
/**
* 下一个需要分配的 ID
* 个体 ID 生成
*/
public idIndex: number = 1;
public nextId(label: string = "U"): string {
return `${label}-${this.idIndex ++}`;
public nextIndividualId: number = 0;
public getNextIndividualId() {
this.nextIndividualId ++;
const random = Math.random().toString(36).slice(-8);
return `${this.nextIndividualId}-${random}`;
}
/**
@@ -59,26 +61,26 @@ 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");
/**
* 添加标签
*/
public addLabel(name: string): Label {
console.log(`Model: Creat label with id ${this.idIndex}`);
let label = new Label(this, this.nextId("L"), name);
let label = new Label(this, name);
this.labelPool.push(label);
this.emit("labelChange", this.labelPool);
console.log(`Model: Creat label with id ${label.id}`);
return label;
}
@@ -140,10 +142,10 @@ class Model extends Emitter<ModelEvent> {
* 添加组
*/
public addGroup(): Group {
console.log(`Model: Creat group with id ${this.idIndex}`);
let group = new Group(this, this.nextId("G"));
let group = new Group(this);
this.objectPool.push(group);
this.emit("objectChange", this.objectPool);
console.log(`Model: Creat group with id ${group.id}`);
return group;
}
@@ -151,10 +153,10 @@ class Model extends Emitter<ModelEvent> {
* 添加范围
*/
public addRange(): Range {
console.log(`Model: Creat range with id ${this.idIndex}`);
let range = new Range(this, this.nextId("R"));
let range = new Range(this);
this.objectPool.push(range);
this.emit("objectChange", this.objectPool);
console.log(`Model: Creat range with id ${range.id}`);
return range;
}
@@ -393,14 +395,4 @@ class Model extends Emitter<ModelEvent> {
}
}
export {
Individual,
Group,
Emitter,
EventType,
EventMixin,
Model,
CtrlObject,
ObjectID,
IAnyObject
}
export { Model, ObjectID, IAnyObject }
+175 -10
View File
@@ -1,6 +1,14 @@
import type { Group } from "@Model/Group";
import type { Range } from "@Model/Range";
import type { Label } from "@Model/Label";
import { Group } from "@Model/Group";
import { Range } from "@Model/Range";
import { Label } from "@Model/Label";
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: "Individual" | "Range" | "Group" | "Label" | "Behavior";
}
type IObjectParamCacheType<P, Q = P> = {
picker: P;
@@ -26,6 +34,15 @@ type IMapObjectParamTypeKeyToType = {
"CLG": IObjectParamCacheType<Label | Group | undefined, Group[]>;
}
type IMapArchiveObjectParamTypeKeyToType = {
"R": IObjectParamCacheType<IObjectParamArchiveType | undefined>;
"G": IObjectParamCacheType<IObjectParamArchiveType | undefined>;
"LR": IObjectParamCacheType<IObjectParamArchiveType | undefined, IObjectParamArchiveType[]>;
"LG": IObjectParamCacheType<IObjectParamArchiveType | undefined, IObjectParamArchiveType[]>;
"CG": IObjectParamCacheType<IObjectParamArchiveType | undefined>;
"CLG": IObjectParamCacheType<IObjectParamArchiveType | undefined, IObjectParamArchiveType[]>;
}
type IMapVectorParamTypeKeyToType = {
"vec": number[];
"color": number[];
@@ -35,30 +52,42 @@ type IMapVectorParamTypeKeyToType = {
* 参数类型映射
*/
type AllMapType = IMapBasicParamTypeKeyToType & IMapObjectParamTypeKeyToType & IMapVectorParamTypeKeyToType;
type AllArchiveMapType = IMapBasicParamTypeKeyToType & IMapArchiveObjectParamTypeKeyToType & IMapVectorParamTypeKeyToType;
type IParamType = keyof AllMapType;
type IObjectType = keyof IMapObjectParamTypeKeyToType;
type IVectorType = keyof IMapVectorParamTypeKeyToType;
type IParamValue<K extends IParamType> = AllMapType[K];
type IArchiveParamValue<K extends IParamType> = AllArchiveMapType[K];
/**
* 特殊对象类型判定
*/
const objectTypeListEnumSet = new Set<string>(["R", "G", "LR", "LG", "CG", "CLG"]);
/**
* 对象断言表达式
*/
/**
* 对象断言表达式
*/
function isObjectType(key: string): key is IVectorType {
return objectTypeListEnumSet.has(key);
}
/**
* 向量断言表达式
*/
/**
* 向量断言表达式
*/
function isVectorType(key: string): key is IObjectType {
return key === "vec";
}
/**
* 压缩对象断言表达式
*/
function isArchiveObjectType(key: Object): key is IObjectParamArchiveType {
return !!(
(key as IObjectParamArchiveType).__LIVING_TOGETHER_OBJECT_ID &&
(key as IObjectParamArchiveType).__LIVING_TOGETHER_OBJECT_TYPE
);
}
/**
* 模型参数类型
*/
@@ -144,6 +173,10 @@ type IParameterValue<P extends IParameter> = {
[X in keyof P]: IParamValue<P[X]>
}
type IArchiveParameterValue<P extends IParameter> = {
[X in keyof P]: IArchiveParamValue<P[X]>
}
function getDefaultValue<P extends IParameter> (option: IParameterOption<P>): IParameterValue<P> {
let defaultObj = {} as IParameterValue<P>;
for (let key in option) {
@@ -193,7 +226,139 @@ function getDefaultValue<P extends IParameter> (option: IParameterOption<P>): IP
return defaultObj;
}
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 Individual) {
return {
__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: object.id,
__LIVING_TOGETHER_OBJECT_TYPE: "Group"
}
} else if (object instanceof Label) {
return {
__LIVING_TOGETHER_OBJECT_ID: object.id,
__LIVING_TOGETHER_OBJECT_TYPE: "Label"
}
} else if (object instanceof Behavior) {
return {
__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;
return object.map(item => object2ArchiveObject(item, false)).filter(hasValue);
} else {
return undefined;
}
}
function parameter2ArchiveObject<P extends IParameter>
(value: IParameterValue<P>, option?: IParameterOption<P>): IArchiveParameterValue<P> {
let archive = {} as IArchiveParameterValue<P>;
const handelColl = (key: string, cValue: IParamValue<IParamType>) => {
// 处理对象类型
if (cValue instanceof Object && !Array.isArray(cValue)) {
const picker = (cValue as IObjectParamCacheType<any>).picker;
const objects = (cValue as IObjectParamCacheType<any>).objects;
(archive[key] as any) = {
picker: object2ArchiveObject(picker),
objects: object2ArchiveObject(objects)
}
}
// 处理数组类型
else if (Array.isArray(cValue)) {
(archive[key] as any) = cValue.concat([]);
}
// 处理数值
else {
(archive[key] as any) = cValue;
}
}
// 存在参考设置对象
if (option) {
for (const key in option) {
handelColl(key, value[key]);
}
}
// 不存在设置对象
else {
for (const key in value) {
handelColl(key, value[key]);
}
}
return archive;
}
function archiveObject2Parameter<P extends IParameter>
(value: IArchiveParameterValue<P>, parse: IArchiveParseFn, option?: IParameterOption<P>): IParameterValue<P> {
let parameter = {} as IParameterValue<P>;
const handelColl = (key: string, cValue: IArchiveParamValue<IParamType>) => {
// 处理对象类型
if (cValue instanceof Object && !Array.isArray(cValue)) {
const picker = (cValue as IObjectParamCacheType<IObjectParamArchiveType>).picker;
const objects = (cValue as IObjectParamCacheType<IObjectParamArchiveType>).objects;
(parameter[key] as any) = {
picker: picker ? parse(picker) : picker,
objects: objects ? Array.isArray(objects) ? objects.map(item => parse(item)) : parse(objects) : objects
}
}
// 处理数组类型
else if (Array.isArray(cValue)) {
(parameter[key] as any) = cValue.concat([]);
}
// 处理数值
else {
(parameter[key] as any) = cValue;
}
}
// 存在参考设置对象
if (option) {
for (const key in option) {
handelColl(key, value[key]);
}
}
// 不存在设置对象
else {
for (const key in value) {
handelColl(key, value[key]);
}
}
return parameter;
}
export {
IParamType, IParamValue, isObjectType, isVectorType, getDefaultValue,
IParameterOptionItem, IParameter, IParameterOption, IParameterValue
IParameterOptionItem, IParameter, IParameterOption, IParameterValue,
object2ArchiveObject, parameter2ArchiveObject, archiveObject2Parameter,
IArchiveParseFn, IObjectParamArchiveType, isArchiveObjectType,
IArchiveParameterValue, IRealObjectType
}
+27 -8
View File
@@ -1,11 +1,16 @@
import { CtrlObject } from "@Model/CtrlObject";
import { Model, ObjectID } from "@Model/Model";
import { getDefaultValue } from "@Model/Parameter";
import { CtrlObject, IArchiveCtrlObject } from "@Model/CtrlObject";
import { Model } from "@Model/Model";
import { getDefaultValue, IArchiveParseFn, parameter2ArchiveObject } from "@Model/Parameter";
interface IArchiveRange {
position: Range["position"];
radius: Range["radius"];
}
/**
* 范围
*/
class Range extends CtrlObject {
class Range extends CtrlObject<IArchiveRange> {
/**
* 坐标
@@ -17,16 +22,30 @@ class Range extends CtrlObject {
*/
public radius: number[] = [1, 1, 1];
public constructor(model: Model, id: ObjectID) {
public constructor(model: Model) {
super(model, id);
super(model);
if (model.renderer) {
this.renderParameter = getDefaultValue(model.renderer.cubeParameterOption);
}
}
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 {
super.fromArchive(archive, paster);
this.position = archive.position.concat([]),
this.radius = archive.radius.concat([])
}
}
export default Range;
export { Range };
export { Range, IArchiveRange };
@@ -0,0 +1,11 @@
div.app-root {
width: 100%;
height: 100%;
position: fixed;
overflow: hidden;
div.app-root-space {
height: 100%;
display: flex;
}
}
@@ -0,0 +1,134 @@
import { Component, ReactNode } from "react";
import { SettingProvider, Setting, Platform } from "@Context/Setting";
import { Theme, BackgroundLevel, FontLevel } from "@Component/Theme/Theme";
import { ISimulatorAPI } from "@Electron/SimulatorAPI";
import { StatusProvider, Status } from "@Context/Status";
import { ElectronProvider } from "@Context/Electron";
import { ClassicRenderer } from "@GLRender/ClassicRenderer";
import { initializeIcons } from '@fluentui/font-icons-mdl2';
import { RootContainer } from "@Component/Container/RootContainer";
import { LayoutDirection } from "@Context/Layout";
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 "./SimulatorDesktop.scss";
initializeIcons("./font-icon/");
class SimulatorDesktop extends Component {
/**
* Electron API
*/
public electron: ISimulatorAPI;
/**
* 全局设置
*/
private setting: Setting;
/**
* 全局状态
*/
private status: Status;
public constructor(props: any) {
super(props);
// TODO: 这里要读取设置
this.setting = new Setting();
this.setting.platform = Platform.desktop;
// TODO: 这里要读取存档
const classicRender = new ClassicRenderer().onLoad();
this.status = new Status();
this.status.bindRenderer(classicRender);
this.status.setting = this.setting;
const randomPosition = (group: Group) => {
group.individuals.forEach((individual) => {
individual.position[0] = (Math.random() - .5) * 2;
individual.position[1] = (Math.random() - .5) * 2;
individual.position[2] = (Math.random() - .5) * 2;
})
};
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) {
this.electron = (window as any).API;
} else {
console.error("SimulatorDesktop: Can't find electron API");
}
}
public componentDidMount() {
this.setting.layout.setData({
items: [
{
items: [
{panels: ["RenderView"]},
{
items: [{panels: ["BehaviorList"]}, {panels: ["LabelList"]}],
scale: 80,
layout: LayoutDirection.X
}
],
scale: 60,
layout: LayoutDirection.Y
},
{
items: [{
panels: ["ObjectList"]
}, {
panels: ["GroupDetails", "RangeDetails", "LabelDetails", "BehaviorDetails"]
}],
scale: 30,
layout: LayoutDirection.Y
}
],
scale: 60,
layout: LayoutDirection.X
})
}
public render(): ReactNode {
return <SettingProvider value={this.setting}>
<StatusProvider value={this.status}>
<ElectronProvider value={this.electron}>
{this.renderContent()}
</ElectronProvider>
</StatusProvider>
</SettingProvider>
}
private renderContent(): ReactNode {
return <Theme
className="app-root"
backgroundLevel={BackgroundLevel.Level5}
fontLevel={FontLevel.Level3}
>
<Popup/>
<HeaderBar height={35}/>
<div className="app-root-space" style={{
height: `calc( 100% - ${35}px)`
}}>
<CommandBar/>
<RootContainer/>
</div>
</Theme>
}
}
Entry.renderComponent(SimulatorDesktop);
+14 -4
View File
@@ -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,7 +155,14 @@ class SimulatorWeb extends Component {
}, 200)
}
(window as any).s = this;
setTimeout(() => {
this.status.archive.load(this.status.model, DEMO);
}, 200);
(window as any).LT = {
status: this.status,
setting: this.setting
};
}
public componentDidMount() {
@@ -200,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
+2 -2
View File
@@ -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"
+16 -5
View File
@@ -4,6 +4,8 @@ import { useSetting, IMixinSettingProps } from "@Context/Setting";
import { Localization } from "@Component/Localization/Localization";
import { DetailsList } from "@Component/DetailsList/DetailsList";
import { ObjectID } from "@Model/Model";
import { Group } from "@Model/Group";
import { Range } from "@Model/Range";
import { Icon } from "@fluentui/react";
import "./ObjectList.scss";
@@ -24,12 +26,21 @@ class ObjectList extends Component<IMixinStatusProps & IMixinSettingProps> {
return <DetailsList
className="object-list"
items={objList.concat([]).map((object => {
let objectType = "";
if (object instanceof Group) {
objectType = "G";
} else if (object instanceof Range) {
objectType = "R";
}
return {
key: object.id.toString(),
name: object.displayName,
color: object.color,
display: object.display,
update: object.update,
type: objectType,
select: this.props.status ?
this.props.status.focusObject.has(object.id.toString()) ||
this.props.status.focusObject.has(object.id) :
@@ -41,10 +52,10 @@ class ObjectList extends Component<IMixinStatusProps & IMixinSettingProps> {
this.props.status.setFocusObject(new Set<ObjectID>().add(item.key));
}
if (this.props.setting) {
if (item.key.slice(0, 1) === "R") {
if (item.type === "R") {
this.props.setting.layout.focus("RangeDetails");
}
if (item.key.slice(0, 1) === "G") {
else if (item.type === "G") {
this.props.setting.layout.focus("GroupDetails");
}
this.props.setting.layout.focus("ObjectList");
@@ -71,13 +82,13 @@ class ObjectList extends Component<IMixinStatusProps & IMixinSettingProps> {
}}
renderCheckbox={(item, click) => {
let icon = "CheckMark";
if (item.key.slice(0, 1) === "R") {
if (item.type === "R") {
icon = "CubeShape";
}
if (item.key.slice(0, 1) === "G") {
else if (item.type === "G") {
icon = "WebAppBuilderFragment";
}
return <div
return <div
className="object-list-checkbox details-list-checkbox"
onClick={click}
>
+2
View File
@@ -41,6 +41,8 @@ class Service {
this.app.listen(this.servicePort);
console.log("Service: service run in port " + this.servicePort);
return "http://127.0.0.1:" + this.servicePort;
}
}
+4 -1
View File
@@ -43,7 +43,10 @@
],
"@Service/*": [
"./source/Service/*"
]
],
"@Electron/*": [
"./source/Electron/*"
],
}
},
"include": [