Compare commits
35
Commits
3220764677
...
Alpha0.1.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
396b128a2c | ||
|
|
298688269c | ||
|
|
1f08a4885b | ||
|
|
4c014a7200 | ||
|
|
b5ecf0bb0d | ||
|
|
87ed157340 | ||
|
|
56151c9e75 | ||
|
|
03f0b9fd16 | ||
|
|
5914a9f531 | ||
|
|
980d8afa3c | ||
|
|
b498f0e0a4 | ||
|
|
430d8a7254 | ||
|
|
74bda21072 | ||
|
|
0d7cbe135d | ||
|
|
068acb19a8 | ||
|
|
276c8c63a1 | ||
|
|
d11fd2d328 | ||
|
|
6063882192 | ||
|
|
de0dd57a04 | ||
|
|
1d36aac37d | ||
|
|
597c7e9493 | ||
|
|
21960778f5 | ||
|
|
a2b1819dd8 | ||
|
|
26837930fe | ||
|
|
a788968525 | ||
|
|
4f6b1ebbf2 | ||
|
|
8b29654fdf | ||
|
|
4f175a5505 | ||
|
|
77410dbf47 | ||
|
|
7e4fee3fe5 | ||
|
|
ff88c7047e | ||
|
|
61e590df4d | ||
|
|
6bea46204d | ||
|
|
76bb20d5ab | ||
|
|
0fc5d4c6d4 |
@@ -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 |
@@ -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));
|
||||
@@ -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"),
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
Generated
+2483
-48
File diff suppressed because it is too large
Load Diff
+30
-5
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 };
|
||||
@@ -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) => {
|
||||
|
||||
|
||||
@@ -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 };
|
||||
@@ -37,7 +37,7 @@ class Setting extends Emitter<ISettingEvents> {
|
||||
/**
|
||||
* 语言
|
||||
*/
|
||||
public language: Language = "EN_US";
|
||||
public language: Language = "ZH_CN";
|
||||
|
||||
/**
|
||||
* 布局
|
||||
|
||||
+84
-17
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
@@ -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 }
|
||||
@@ -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 +0,0 @@
|
||||
export * from "@Model/Model";
|
||||
@@ -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
-26
@@ -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
@@ -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
|
||||
};
|
||||
@@ -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
@@ -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 };
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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
@@ -43,7 +43,10 @@
|
||||
],
|
||||
"@Service/*": [
|
||||
"./source/Service/*"
|
||||
]
|
||||
],
|
||||
"@Electron/*": [
|
||||
"./source/Electron/*"
|
||||
],
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
|
||||
Reference in New Issue
Block a user