diff --git a/package-lock.json b/package-lock.json index 7b31c0c..a850dcb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,11 +11,13 @@ "dependencies": { "@fluentui/react": "^8.56.0", "@juggle/resize-observer": "^3.3.1", + "chart.js": "^3.7.1", "detect-port": "^1.3.0", "downloadjs": "^1.4.7", "express": "^4.17.3", "gl-matrix": "^3.4.3", "react": "^17.0.2", + "react-chartjs-2": "^4.1.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^17.0.2", @@ -2200,6 +2202,11 @@ "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": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -6210,6 +6217,15 @@ "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": { "version": "16.0.1", "resolved": "https://registry.npmmirror.com/react-dnd/-/react-dnd-16.0.1.tgz", @@ -10113,6 +10129,11 @@ "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": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -13200,6 +13221,12 @@ "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": { "version": "16.0.1", "resolved": "https://registry.npmmirror.com/react-dnd/-/react-dnd-16.0.1.tgz", diff --git a/package.json b/package.json index afef7db..c919826 100644 --- a/package.json +++ b/package.json @@ -71,11 +71,13 @@ "dependencies": { "@fluentui/react": "^8.56.0", "@juggle/resize-observer": "^3.3.1", + "chart.js": "^3.7.1", "detect-port": "^1.3.0", "downloadjs": "^1.4.7", "express": "^4.17.3", "gl-matrix": "^3.4.3", "react": "^17.0.2", + "react-chartjs-2": "^4.1.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^17.0.2", diff --git a/source/Context/Setting.tsx b/source/Context/Setting.tsx index 5996ff4..e58d435 100644 --- a/source/Context/Setting.tsx +++ b/source/Context/Setting.tsx @@ -44,6 +44,11 @@ class Setting extends Emitter { */ public layout: Layout = new Layout(); + /** + * 是否显示线性图表 + */ + public lineChartType: boolean = false; + /** * 设置参数 */ diff --git a/source/Context/Status.tsx b/source/Context/Status.tsx index 3bcb630..74520e3 100644 --- a/source/Context/Status.tsx +++ b/source/Context/Status.tsx @@ -38,6 +38,7 @@ interface IStatusEvent { physicsLoop: number; recordLoop: number; offlineLoop: number; + modelUpdate: void; mouseModChange: void; focusObjectChange: void; focusLabelChange: void; @@ -132,6 +133,7 @@ class Status extends Emitter { this.actuator.on("loop", (t) => { this.emit("physicsLoop", t) }); this.actuator.on("record", (t) => { this.emit("recordLoop", t) }); this.actuator.on("offline", (t) => { this.emit("offlineLoop", t) }); + this.actuator.on("modelUpdate", () => { this.emit("modelUpdate") }); // 对象变化事件 this.model.on("objectChange", () => this.emit("objectChange")); diff --git a/source/Localization/EN-US.ts b/source/Localization/EN-US.ts index cda3522..d8473ee 100644 --- a/source/Localization/EN-US.ts +++ b/source/Localization/EN-US.ts @@ -170,6 +170,7 @@ const EN_US = { "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.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.Load.File.Title": "Load save", "Info.Hint.Load.File.Intro": "Release to load the dragged save file", diff --git a/source/Localization/ZH-CN.ts b/source/Localization/ZH-CN.ts index 99b8a19..f1661a8 100644 --- a/source/Localization/ZH-CN.ts +++ b/source/Localization/ZH-CN.ts @@ -170,6 +170,7 @@ const ZH_CN = { "Panel.Info.Behavior.Details.Parameter.Key.Vec.Z": "{key} Z 坐标", "Panel.Info.Clip.List.Error.Nodata": "没有剪辑片段,请点击录制按钮录制,或者点击加号创建", "Panel.Info.Clip.Details.Error.Nodata": "请指定一个剪辑片段以查看属性", + "Panel.Info.Statistics.Nodata": "模型或剪辑中不存在任何群", "Info.Hint.Save.After.Close": "任何未保存的进度都会丢失, 确定要继续吗?", "Info.Hint.Load.File.Title": "加载存档", "Info.Hint.Load.File.Intro": "释放以加载拽入的存档", diff --git a/source/Model/Actuator.ts b/source/Model/Actuator.ts index 20314c4..e59b76f 100644 --- a/source/Model/Actuator.ts +++ b/source/Model/Actuator.ts @@ -14,6 +14,7 @@ interface IActuatorEvent { record: number; loop: number; offline: number; + modelUpdate: void; } /** @@ -401,6 +402,7 @@ class Actuator extends Emitter { } this.emit("loop", this.alignTimer); + this.emit("modelUpdate"); this.alignTimer = 0; } } diff --git a/source/Panel/Statistics/Statistics.scss b/source/Panel/Statistics/Statistics.scss index 20a0d20..9e2b23e 100644 --- a/source/Panel/Statistics/Statistics.scss +++ b/source/Panel/Statistics/Statistics.scss @@ -2,7 +2,62 @@ div.statistics-panel { width: 100%; + 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; + } } \ No newline at end of file diff --git a/source/Panel/Statistics/Statistics.tsx b/source/Panel/Statistics/Statistics.tsx index a893fb3..b8c64e4 100644 --- a/source/Panel/Statistics/Statistics.tsx +++ b/source/Panel/Statistics/Statistics.tsx @@ -1,20 +1,139 @@ import { Component, ReactNode } from "react"; import { useStatusWithEvent, IMixinStatusProps } from "@Context/Status"; -import { useSetting, IMixinSettingProps } from "@Context/Setting"; -import Theme from "@Component/Theme/Theme"; +import { useSettingWithEvent, IMixinSettingProps, Themes } from "@Context/Setting"; +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"; +ChartJS.register( + CategoryScale, + LinearScale, + BarElement, + Tooltip, + Legend +); + +enum ChartType { + +} + interface IStatisticsProps { } -@useSetting -@useStatusWithEvent("labelChange", "focusLabelChange", "labelAttrChange") +@useSettingWithEvent("themes", "language", "lineChartType") +@useStatusWithEvent("focusClipChange", "actuatorStartChange", "fileLoad", "modelUpdate", "individualChange") class Statistics extends Component { + 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 + } + + return + } + + 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 { return + { + ( + this.props.status?.focusClip || + this.props.status?.actuator.mod === ActuatorModel.Record || + this.props.status?.actuator.mod === ActuatorModel.Offline + ) ? + +
+
{ + this.props.setting?.setProps("lineChartType", !this.props.setting?.lineChartType); + }}> + +
+
+ + : null + } + +
+ { this.renderChart() } +
; } }