Add statistics panel

This commit is contained in:
MrKBear 2022-05-25 22:00:13 +08:00
parent ce99b17fcb
commit 2dbbd0952a
9 changed files with 220 additions and 6 deletions

27
package-lock.json generated
View File

@ -11,11 +11,13 @@
"dependencies": { "dependencies": {
"@fluentui/react": "^8.56.0", "@fluentui/react": "^8.56.0",
"@juggle/resize-observer": "^3.3.1", "@juggle/resize-observer": "^3.3.1",
"chart.js": "^3.7.1",
"detect-port": "^1.3.0", "detect-port": "^1.3.0",
"downloadjs": "^1.4.7", "downloadjs": "^1.4.7",
"express": "^4.17.3", "express": "^4.17.3",
"gl-matrix": "^3.4.3", "gl-matrix": "^3.4.3",
"react": "^17.0.2", "react": "^17.0.2",
"react-chartjs-2": "^4.1.0",
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1", "react-dnd-html5-backend": "^16.0.1",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
@ -2200,6 +2202,11 @@
"url": "https://github.com/chalk/chalk?sponsor=1" "url": "https://github.com/chalk/chalk?sponsor=1"
} }
}, },
"node_modules/chart.js": {
"version": "3.7.1",
"resolved": "https://registry.npmmirror.com/chart.js/-/chart.js-3.7.1.tgz",
"integrity": "sha512-8knRegQLFnPQAheZV8MjxIXc5gQEfDFD897BJgv/klO/vtIyFFmgMXrNfgrXpbTr/XbTturxRgxIXx/Y+ASJBA=="
},
"node_modules/chokidar": { "node_modules/chokidar": {
"version": "3.5.3", "version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
@ -6210,6 +6217,15 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-chartjs-2": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/react-chartjs-2/-/react-chartjs-2-4.1.0.tgz",
"integrity": "sha512-AsUihxEp8Jm1oBhbEovE+w50m9PVNhz1sfwEIT4hZduRC0m14gHWHd0cUaxkFDb8HNkdMIGzsNlmVqKiOpU74g==",
"peerDependencies": {
"chart.js": "^3.5.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-dnd": { "node_modules/react-dnd": {
"version": "16.0.1", "version": "16.0.1",
"resolved": "https://registry.npmmirror.com/react-dnd/-/react-dnd-16.0.1.tgz", "resolved": "https://registry.npmmirror.com/react-dnd/-/react-dnd-16.0.1.tgz",
@ -10113,6 +10129,11 @@
"supports-color": "^7.1.0" "supports-color": "^7.1.0"
} }
}, },
"chart.js": {
"version": "3.7.1",
"resolved": "https://registry.npmmirror.com/chart.js/-/chart.js-3.7.1.tgz",
"integrity": "sha512-8knRegQLFnPQAheZV8MjxIXc5gQEfDFD897BJgv/klO/vtIyFFmgMXrNfgrXpbTr/XbTturxRgxIXx/Y+ASJBA=="
},
"chokidar": { "chokidar": {
"version": "3.5.3", "version": "3.5.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
@ -13200,6 +13221,12 @@
"object-assign": "^4.1.1" "object-assign": "^4.1.1"
} }
}, },
"react-chartjs-2": {
"version": "4.1.0",
"resolved": "https://registry.npmmirror.com/react-chartjs-2/-/react-chartjs-2-4.1.0.tgz",
"integrity": "sha512-AsUihxEp8Jm1oBhbEovE+w50m9PVNhz1sfwEIT4hZduRC0m14gHWHd0cUaxkFDb8HNkdMIGzsNlmVqKiOpU74g==",
"requires": {}
},
"react-dnd": { "react-dnd": {
"version": "16.0.1", "version": "16.0.1",
"resolved": "https://registry.npmmirror.com/react-dnd/-/react-dnd-16.0.1.tgz", "resolved": "https://registry.npmmirror.com/react-dnd/-/react-dnd-16.0.1.tgz",

View File

@ -71,11 +71,13 @@
"dependencies": { "dependencies": {
"@fluentui/react": "^8.56.0", "@fluentui/react": "^8.56.0",
"@juggle/resize-observer": "^3.3.1", "@juggle/resize-observer": "^3.3.1",
"chart.js": "^3.7.1",
"detect-port": "^1.3.0", "detect-port": "^1.3.0",
"downloadjs": "^1.4.7", "downloadjs": "^1.4.7",
"express": "^4.17.3", "express": "^4.17.3",
"gl-matrix": "^3.4.3", "gl-matrix": "^3.4.3",
"react": "^17.0.2", "react": "^17.0.2",
"react-chartjs-2": "^4.1.0",
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1", "react-dnd-html5-backend": "^16.0.1",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",

View File

@ -44,6 +44,11 @@ class Setting extends Emitter<ISettingEvents> {
*/ */
public layout: Layout = new Layout(); public layout: Layout = new Layout();
/**
* 线
*/
public lineChartType: boolean = false;
/** /**
* *
*/ */

View File

@ -38,6 +38,7 @@ interface IStatusEvent {
physicsLoop: number; physicsLoop: number;
recordLoop: number; recordLoop: number;
offlineLoop: number; offlineLoop: number;
modelUpdate: void;
mouseModChange: void; mouseModChange: void;
focusObjectChange: void; focusObjectChange: void;
focusLabelChange: void; focusLabelChange: void;
@ -132,6 +133,7 @@ class Status extends Emitter<IStatusEvent> {
this.actuator.on("loop", (t) => { this.emit("physicsLoop", t) }); this.actuator.on("loop", (t) => { this.emit("physicsLoop", t) });
this.actuator.on("record", (t) => { this.emit("recordLoop", t) }); this.actuator.on("record", (t) => { this.emit("recordLoop", t) });
this.actuator.on("offline", (t) => { this.emit("offlineLoop", t) }); this.actuator.on("offline", (t) => { this.emit("offlineLoop", t) });
this.actuator.on("modelUpdate", () => { this.emit("modelUpdate") });
// 对象变化事件 // 对象变化事件
this.model.on("objectChange", () => this.emit("objectChange")); this.model.on("objectChange", () => this.emit("objectChange"));

View File

@ -170,6 +170,7 @@ const EN_US = {
"Panel.Info.Behavior.Details.Parameter.Key.Vec.Z": "{key} Z", "Panel.Info.Behavior.Details.Parameter.Key.Vec.Z": "{key} Z",
"Panel.Info.Clip.List.Error.Nodata": "There is no clip, please click the record button to record, or click the plus sign to create", "Panel.Info.Clip.List.Error.Nodata": "There is no clip, please click the record button to record, or click the plus sign to create",
"Panel.Info.Clip.Details.Error.Nodata": "Specify a clip to view an attribute", "Panel.Info.Clip.Details.Error.Nodata": "Specify a clip to view an attribute",
"Panel.Info.Statistics.Nodata": "There are no groups in the model or clip",
"Info.Hint.Save.After.Close": "Any unsaved progress will be lost. Are you sure you want to continue?", "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.Title": "Load save",
"Info.Hint.Load.File.Intro": "Release to load the dragged save file", "Info.Hint.Load.File.Intro": "Release to load the dragged save file",

View File

@ -170,6 +170,7 @@ const ZH_CN = {
"Panel.Info.Behavior.Details.Parameter.Key.Vec.Z": "{key} Z 坐标", "Panel.Info.Behavior.Details.Parameter.Key.Vec.Z": "{key} Z 坐标",
"Panel.Info.Clip.List.Error.Nodata": "没有剪辑片段,请点击录制按钮录制,或者点击加号创建", "Panel.Info.Clip.List.Error.Nodata": "没有剪辑片段,请点击录制按钮录制,或者点击加号创建",
"Panel.Info.Clip.Details.Error.Nodata": "请指定一个剪辑片段以查看属性", "Panel.Info.Clip.Details.Error.Nodata": "请指定一个剪辑片段以查看属性",
"Panel.Info.Statistics.Nodata": "模型或剪辑中不存在任何群",
"Info.Hint.Save.After.Close": "任何未保存的进度都会丢失, 确定要继续吗?", "Info.Hint.Save.After.Close": "任何未保存的进度都会丢失, 确定要继续吗?",
"Info.Hint.Load.File.Title": "加载存档", "Info.Hint.Load.File.Title": "加载存档",
"Info.Hint.Load.File.Intro": "释放以加载拽入的存档", "Info.Hint.Load.File.Intro": "释放以加载拽入的存档",

View File

@ -14,6 +14,7 @@ interface IActuatorEvent {
record: number; record: number;
loop: number; loop: number;
offline: number; offline: number;
modelUpdate: void;
} }
/** /**
@ -401,6 +402,7 @@ class Actuator extends Emitter<IActuatorEvent> {
} }
this.emit("loop", this.alignTimer); this.emit("loop", this.alignTimer);
this.emit("modelUpdate");
this.alignTimer = 0; this.alignTimer = 0;
} }
} }

View File

@ -2,7 +2,62 @@
div.statistics-panel { div.statistics-panel {
width: 100%; width: 100%;
height: 100%;
min-height: 100%; min-height: 100%;
padding: 10px;
box-sizing: border-box; div.statistics-chart {
box-sizing: border-box;
padding-top: 10px;
max-width: 300px;
min-height: 100%;
height: 100%;
width: 100%;
}
div.statistics-switch {
width: 100%;
height: 0;
display: flex;
justify-content: flex-end;
div.switch-button {
width: 24px;
height: 24px;
position: relative;
user-select: none;
right: -10px;
top: -2px;
border-radius: 3px;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
}
}
}
div.statistics-panel.dark {
div.switch-button {
background-color: $lt-bg-color-lvl2-dark;
color: $lt-font-color-lvl2-dark;
}
div.switch-button:hover {
background-color: $lt-bg-color-lvl1-dark;
color: $lt-font-color-lvl1-dark;
}
}
div.statistics-panel.light {
div.switch-button {
background-color: $lt-bg-color-lvl2-light;
color: $lt-font-color-lvl2-light;
}
div.switch-button:hover {
background-color: $lt-bg-color-lvl1-light;
color: $lt-font-color-lvl1-light;
}
} }

View File

@ -1,20 +1,139 @@
import { Component, ReactNode } from "react"; import { Component, ReactNode } from "react";
import { useStatusWithEvent, IMixinStatusProps } from "@Context/Status"; import { useStatusWithEvent, IMixinStatusProps } from "@Context/Status";
import { useSetting, IMixinSettingProps } from "@Context/Setting"; import { useSettingWithEvent, IMixinSettingProps, Themes } from "@Context/Setting";
import Theme from "@Component/Theme/Theme"; import {
Chart as ChartJS, CategoryScale, LinearScale,
BarElement, Tooltip, Legend
} from 'chart.js';
import { Bar } from 'react-chartjs-2';
import { Theme } from "@Component/Theme/Theme";
import { Icon } from "@fluentui/react";
import { Model } from "@Model/Model";
import { Group } from "@Model/Group";
import { ActuatorModel } from "@Model/Actuator";
import { Message } from "@Input/Message/Message";
import "./Statistics.scss"; import "./Statistics.scss";
ChartJS.register(
CategoryScale,
LinearScale,
BarElement,
Tooltip,
Legend
);
enum ChartType {
}
interface IStatisticsProps { interface IStatisticsProps {
} }
@useSetting @useSettingWithEvent("themes", "language", "lineChartType")
@useStatusWithEvent("labelChange", "focusLabelChange", "labelAttrChange") @useStatusWithEvent("focusClipChange", "actuatorStartChange", "fileLoad", "modelUpdate", "individualChange")
class Statistics extends Component<IStatisticsProps & IMixinStatusProps & IMixinSettingProps> { class Statistics extends Component<IStatisticsProps & IMixinStatusProps & IMixinSettingProps> {
public barDarkOption = {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: {
position: 'bottom' as const,
labels: { boxWidth: 10, boxHeight: 10, color: 'rgba(255, 255, 255, .5)' }
}},
scales: {
x: { grid: { color: 'rgba(255, 255, 255, .2)' }, title: { color: 'rgba(255, 255, 255, .5)'} },
y: { grid: { color: 'rgba(255, 255, 255, .2)', borderDash: [3, 3] }, title: { color: 'rgba(255, 255, 255, .5)'} }
}
};
public barLightOption = {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: {
position: 'bottom' as const,
labels: { boxWidth: 10, boxHeight: 10, color: 'rgba(0, 0, 0, .5)' }
}},
scales: {
x: { grid: { color: 'rgba(0, 0, 0, .2)' }, title: { color: 'rgba(0, 0, 0, .5)'} },
y: { grid: { color: 'rgba(0, 0, 0, .2)', borderDash: [3, 3] }, title: { color: 'rgba(0, 0, 0, .5)'} }
}
};
private modelBarChart(model: Model, theme: boolean) {
const datasets: any[] = [];
const labels: any[] = ["Group"];
// 收集数据
model.objectPool.forEach((obj) => {
let label = obj.displayName;
let color = `rgb(${obj.color.map((v) => Math.floor(v * 255)).join(",")})`;
if (obj instanceof Group) {
datasets.push({label, data: [obj.individuals.size], backgroundColor: color});
}
});
if (datasets.length <= 0) {
return <Message i18nKey="Panel.Info.Statistics.Nodata"/>
}
return <Bar
data={{datasets, labels}}
options={theme ? this.barLightOption : this.barDarkOption }
/>
}
private renderChart() {
let themes = this.props.setting?.themes === Themes.light;
// 播放模式
if (this.props.status?.focusClip) {
return this.modelBarChart(this.props.status.model, themes);
}
// 正在录制中
else if (
this.props.status?.actuator.mod === ActuatorModel.Record ||
this.props.status?.actuator.mod === ActuatorModel.Offline
) {
return this.modelBarChart(this.props.status.model, themes);
}
// 主时钟运行
else if (this.props.status) {
return this.modelBarChart(this.props.status.model, themes);
}
}
public render(): ReactNode { public render(): ReactNode {
return <Theme className="statistics-panel"> return <Theme className="statistics-panel">
{
(
this.props.status?.focusClip ||
this.props.status?.actuator.mod === ActuatorModel.Record ||
this.props.status?.actuator.mod === ActuatorModel.Offline
) ?
<div className="statistics-switch">
<div className="switch-button" onClick={() => {
this.props.setting?.setProps("lineChartType", !this.props.setting?.lineChartType);
}}>
<Icon iconName={
this.props.setting?.lineChartType ? "BarChartVertical" : "LineChart"
}/>
</div>
</div>
: null
}
<div className="statistics-chart">
{ this.renderChart() }
</div>
</Theme>; </Theme>;
} }
} }