diff --git a/source/Component/ClipList/ClipList.tsx b/source/Component/ClipList/ClipList.tsx index bb19940..e3d58b0 100644 --- a/source/Component/ClipList/ClipList.tsx +++ b/source/Component/ClipList/ClipList.tsx @@ -39,7 +39,7 @@ class ClipList extends Component { } private getClipInfo(clip: Clip): string { - let fps = Math.floor(clip.frames.length / clip.time); + let fps = Math.round((clip.frames.length - 1) / clip.time); if (isNaN(fps)) fps = 0; return `${this.parseTime(clip.time)} ${fps}fps`; } diff --git a/source/Component/CommandBar/CommandBar.tsx b/source/Component/CommandBar/CommandBar.tsx index 59b68a4..cf2b39d 100644 --- a/source/Component/CommandBar/CommandBar.tsx +++ b/source/Component/CommandBar/CommandBar.tsx @@ -9,6 +9,7 @@ import { SettingPopup } from "@Component/SettingPopup/SettingPopup"; import { BehaviorPopup } from "@Component/BehaviorPopup/BehaviorPopup"; import { MouseMod } from "@GLRender/ClassicRenderer"; import { ArchiveSave } from "@Context/Archive"; +import { ActuatorModel } from "@Model/Actuator"; import "./CommandBar.scss"; const COMMAND_BAR_WIDTH = 45; @@ -50,6 +51,62 @@ class CommandBar extends Component any = () => {}; + + // 播放模式 + if (this.props.status?.focusClip) { + + // 暂停播放 + if (this.props.status?.actuator.mod === ActuatorModel.Play) { + icon = "Pause"; + handel = () => { + this.props.status?.actuator.pausePlay(); + console.log("ClipRecorder: Pause play..."); + }; + } + + // 开始播放 + else { + icon = "Play"; + handel = () => { + this.props.status?.actuator.playing(); + console.log("ClipRecorder: Play start..."); + }; + } + } + + // 正在录制中 + else if ( + this.props.status?.actuator.mod === ActuatorModel.Record || + this.props.status?.actuator.mod === ActuatorModel.Offline + ) { + + // 暂停录制 + icon = "Stop"; + handel = () => { + this.props.status?.actuator.endRecord(); + console.log("ClipRecorder: Rec end..."); + }; + } + + // 正常控制主时钟 + else { + icon = this.props.status?.actuator.start() ? "Pause" : "Play"; + handel = () => this.props.status?.actuator.start( + !this.props.status?.actuator.start() + ); + } + + return ; + } + public render(): ReactNode { const mouseMod = this.props.status?.mouseMod ?? MouseMod.Drag; @@ -84,13 +141,7 @@ class CommandBar extends Component - this.props.status ? this.props.status.actuator.start( - !this.props.status.actuator.start() - ) : undefined} - /> + {this.renderPlayActionButton()} any; +} + +interface IOfflineRenderState { + time: number; + fps: number; + name: string; +} + +class OfflineRender extends Popup { + + public minWidth: number = 250; + public minHeight: number = 150; + public width: number = 400; + public height: number = 300; + + public maskForSelf: boolean = true; + + public onRenderHeader(): ReactNode { + return + } + + public render(): ReactNode { + return { + this.close(); + }}/> + } +} + +@useStatusWithEvent() +class OfflineRenderComponent extends Component { + + public constructor(props: IOfflineRenderProps & IMixinStatusProps) { + super(props); + this.state = { + name: this.props.status?.getNewClipName() ?? "", + time: 10, + fps: 60 + } + } + + public render(): ReactNode { + return { + + // 获取新实例 + let newClip = this.props.status?.newClip(); + + if (newClip) { + newClip.name = this.state.name; + this.props.status?.actuator.offlineRender(newClip, this.state.time, this.state.fps); + + // 开启进度条弹窗 + this.props.status?.popup.showPopup(ProcessPopup, {}); + } + + // 关闭这个弹窗 + this.props.close && this.props.close(); + } + }]} + > + + + + { + this.setState({ + name: val + }); + }} + /> + + { + this.setState({ + time: parseFloat(val) + }); + }} + /> + + { + this.setState({ + fps: parseFloat(val) + }); + }} + /> + + + } +} + +export { OfflineRender }; \ No newline at end of file diff --git a/source/Component/ProcessPopup/ProcessPopup.scss b/source/Component/ProcessPopup/ProcessPopup.scss new file mode 100644 index 0000000..380bf32 --- /dev/null +++ b/source/Component/ProcessPopup/ProcessPopup.scss @@ -0,0 +1,42 @@ +@import "../Theme/Theme.scss"; + +div.process-popup { + width: 100%; + height: 100%; + box-sizing: border-box; + padding: 10px; + + div.ms-ProgressIndicator { + transform: none; + + div.ms-ProgressIndicator-progressTrack { + transform: none; + } + + div.ms-ProgressIndicator-progressBar { + transform: none; + } + } +} + +div.confirm-root.dark div.ms-ProgressIndicator { + + div.ms-ProgressIndicator-progressTrack { + background-color: $lt-bg-color-lvl3-dark; + } + + div.ms-ProgressIndicator-progressBar { + background-color: $lt-font-color-normal-dark; + } +} + +div.confirm-root.light div.ms-ProgressIndicator { + + div.ms-ProgressIndicator-progressTrack { + background-color: $lt-bg-color-lvl3-light; + } + + div.ms-ProgressIndicator-progressBar { + background-color: $lt-font-color-normal-light; + } +} \ No newline at end of file diff --git a/source/Component/ProcessPopup/ProcessPopup.tsx b/source/Component/ProcessPopup/ProcessPopup.tsx new file mode 100644 index 0000000..8a5500f --- /dev/null +++ b/source/Component/ProcessPopup/ProcessPopup.tsx @@ -0,0 +1,89 @@ +import { Component, ReactNode } from "react"; +import { Popup } from "@Context/Popups"; +import { Localization } from "@Component/Localization/Localization"; +import { ConfirmContent } from "@Component/ConfirmPopup/ConfirmPopup"; +import { useStatusWithEvent, IMixinStatusProps } from "@Context/Status"; +import { ProgressIndicator } from "@fluentui/react"; +import { ActuatorModel } from "@Model/Actuator"; +import "./ProcessPopup.scss"; + +interface IProcessPopupProps { + close?: () => void; +} + +class ProcessPopup extends Popup { + + public minWidth: number = 400; + public minHeight: number = 150; + public width: number = 400; + public height: number = 150; + + public maskForSelf: boolean = true; + + public onClose(): void {} + + public onRenderHeader(): ReactNode { + return + } + + public render(): ReactNode { + return this.close()}/> + } +} + +@useStatusWithEvent("offlineLoop", "actuatorStartChange", "recordLoop") +class ProcessPopupComponent extends Component { + + public render(): ReactNode { + + let current = this.props.status?.actuator.offlineCurrentFrame ?? 0; + let all = this.props.status?.actuator.offlineAllFrame ?? 0; + + const isRendering = this.props.status?.actuator.mod === ActuatorModel.Offline; + let i18nKey = ""; + let color: undefined | "red"; + let onClick = () => {}; + + if (isRendering) { + i18nKey = "Popup.Offline.Render.Input.End"; + color = "red"; + onClick = () => { + this.props.status?.actuator.endOfflineRender(); + this.forceUpdate(); + } + } + + else { + i18nKey = "Popup.Offline.Render.Input.Finished"; + onClick = () => { + this.props.close && this.props.close(); + } + } + + return + + + + + + + } +} + +export { ProcessPopup }; \ No newline at end of file diff --git a/source/Component/Recorder/Recorder.scss b/source/Component/Recorder/Recorder.scss index 2f03595..1bb6b31 100644 --- a/source/Component/Recorder/Recorder.scss +++ b/source/Component/Recorder/Recorder.scss @@ -7,9 +7,11 @@ div.recorder-root { div.recorder-slider { width: 100%; + transition: none; div.ms-Slider-slideBox { height: 16px; + transition: none; } span.ms-Slider-thumb { @@ -17,15 +19,18 @@ div.recorder-root { height: 12px; line-height: 16px; border-width: 3px; + transition: none; top: -4px; } span.ms-Slider-active { height: 3px; + transition: none; } span.ms-Slider-inactive { height: 3px; + transition: none; } } diff --git a/source/Component/Recorder/Recorder.tsx b/source/Component/Recorder/Recorder.tsx index d75e3d9..542354a 100644 --- a/source/Component/Recorder/Recorder.tsx +++ b/source/Component/Recorder/Recorder.tsx @@ -14,6 +14,7 @@ interface IRecorderProps { allTime?: number; currentTime?: number; action?: () => void; + valueChange?: (value: number) => any; } class Recorder extends Component { @@ -85,19 +86,38 @@ class Recorder extends Component { max={this.props.allFrame} className={"recorder-slider" + (isSliderDisable ? " disable" : "")} showValue={false} + onChange={(value) => { + if (this.props.valueChange && !isSliderDisable) { + this.props.valueChange(value); + } + }} />
{this.getRecordInfo()}
-
+
{ + if (this.props.valueChange && !isJumpDisable && this.props.currentFrame !== undefined) { + this.props.valueChange(this.props.currentFrame - 1); + } + }} + >
-
+
{ + if (this.props.valueChange && !isJumpDisable && this.props.currentFrame !== undefined) { + this.props.valueChange(this.props.currentFrame + 1); + } + }} + >
diff --git a/source/Context/Status.tsx b/source/Context/Status.tsx index 2ac2e14..868ea03 100644 --- a/source/Context/Status.tsx +++ b/source/Context/Status.tsx @@ -37,6 +37,7 @@ interface IStatusEvent { renderLoop: number; physicsLoop: number; recordLoop: number; + offlineLoop: number; mouseModChange: void; focusObjectChange: void; focusLabelChange: void; @@ -129,6 +130,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.model.on("objectChange", () => this.emit("objectChange")); @@ -424,7 +426,7 @@ class Status extends Emitter { return label; } - public newClip() { + public getNewClipName() { let searchKey = I18N(this.setting.language, "Object.List.New.Clip", { id: "" }); let nextIndex = 1; this.model.clipPool.forEach((obj) => { @@ -432,11 +434,13 @@ class Status extends Emitter { obj.name, searchKey )); }); - const clip = this.model.addClip( - I18N(this.setting.language, "Object.List.New.Clip", { - id: nextIndex.toString() - }) - ); + return I18N(this.setting.language, "Object.List.New.Clip", { + id: nextIndex.toString() + }); + } + + public newClip() { + const clip = this.model.addClip(this.getNewClipName()); return clip; } diff --git a/source/Localization/EN-US.ts b/source/Localization/EN-US.ts index d748ce2..2c9b0f6 100644 --- a/source/Localization/EN-US.ts +++ b/source/Localization/EN-US.ts @@ -69,8 +69,19 @@ const EN_US = { "Popup.Action.Objects.Confirm.Restore": "Restore", "Popup.Delete.Objects.Confirm": "Are you sure you want to delete this object(s)? The object is deleted and cannot be recalled.", "Popup.Delete.Behavior.Confirm": "Are you sure you want to delete this behavior? The behavior is deleted and cannot be recalled.", + "Popup.Delete.Clip.Confirm": "Are you sure you want to delete this clip? The clip cannot be restored after deletion.", "Popup.Restore.Behavior.Confirm": "Are you sure you want to reset all parameters of this behavior? This operation cannot be recalled.", "Popup.Setting.Title": "Preferences setting", + "Popup.Offline.Render.Title": "Offline rendering", + "Popup.Offline.Render.Process.Title": "Rendering progress", + "Popup.Offline.Render.Message": "Rendering Parameters", + "Popup.Offline.Render.Input.Name": "Clip name", + "Popup.Offline.Render.Input.Time": "Duration (s)", + "Popup.Offline.Render.Input.Fps": "FPS (f/s)", + "Popup.Offline.Render.Input.Start": "Start rendering", + "Popup.Offline.Render.Input.End": "Terminate rendering", + "Popup.Offline.Render.Input.Finished": "Finished", + "Popup.Offline.Render.Process": "Number of frames completed: {current} / {all}", "Popup.Load.Save.Title": "Load save", "Popup.Load.Save.confirm": "Got it", "Popup.Load.Save.Overwrite": "Overwrite and continue", diff --git a/source/Localization/ZH-CN.ts b/source/Localization/ZH-CN.ts index 77c55ea..d01f108 100644 --- a/source/Localization/ZH-CN.ts +++ b/source/Localization/ZH-CN.ts @@ -69,8 +69,19 @@ const ZH_CN = { "Popup.Action.Objects.Confirm.Restore": "重置", "Popup.Delete.Objects.Confirm": "你确定要删除这个(些)对象吗?对象被删除将无法撤回。", "Popup.Delete.Behavior.Confirm": "你确定要删除这个行为吗?行为被删除将无法撤回。", + "Popup.Delete.Clip.Confirm": "你确定删除这个剪辑片段,剪辑片段删除后将无法恢复。", "Popup.Restore.Behavior.Confirm": "你确定要重置此行为的全部参数吗?此操作无法撤回。", "Popup.Setting.Title": "首选项设置", + "Popup.Offline.Render.Title": "离线渲染", + "Popup.Offline.Render.Process.Title": "渲染进度", + "Popup.Offline.Render.Message": "渲染参数", + "Popup.Offline.Render.Input.Name": "剪辑名称", + "Popup.Offline.Render.Input.Time": "时长 (s)", + "Popup.Offline.Render.Input.Fps": "帧率 (f/s)", + "Popup.Offline.Render.Input.Start": "开始渲染", + "Popup.Offline.Render.Input.End": "终止渲染", + "Popup.Offline.Render.Input.Finished": "完成", + "Popup.Offline.Render.Process": "完成帧数: {current} / {all}", "Popup.Load.Save.Title": "加载存档", "Popup.Load.Save.confirm": "我知道了", "Popup.Load.Save.Overwrite": "覆盖并继续", diff --git a/source/Model/Actuator.ts b/source/Model/Actuator.ts index cc57d27..20314c4 100644 --- a/source/Model/Actuator.ts +++ b/source/Model/Actuator.ts @@ -1,6 +1,6 @@ import { Model } from "@Model/Model"; import { Emitter } from "@Model/Emitter"; -import { Clip } from "@Model/Clip"; +import { Clip, IFrame } from "@Model/Clip"; enum ActuatorModel { Play = 1, @@ -13,6 +13,7 @@ interface IActuatorEvent { startChange: boolean; record: number; loop: number; + offline: number; } /** @@ -45,6 +46,21 @@ class Actuator extends Emitter { */ public recordClip?: Clip; + /** + * 播放剪辑 + */ + public playClip?: Clip; + + /** + * 播放帧 + */ + public playFrame?: IFrame; + + /** + * 播放帧数 + */ + public playFrameId: number = 0; + /** * 开始录制 */ @@ -76,6 +92,98 @@ class Actuator extends Emitter { this.mod = ActuatorModel.View; } + public startPlay(clip: Clip) { + + // 如果仿真正在进行,停止仿真 + if (this.start()) this.start(false); + + // 如果正在录制,阻止播放 + if (this.mod === ActuatorModel.Record) { + return; + } + + // 如果正在播放,暂停播放 + if (this.mod === ActuatorModel.Play) { + this.pausePlay(); + } + + // 设置播放对象 + this.playClip = clip; + + // 设置播放帧数 + this.playFrameId = 0; + this.playFrame = clip.frames[this.playFrameId]; + + // 播放第一帧 + clip.play(this.playFrame); + + // 激发时钟状态事件 + this.emit("startChange", true); + } + + public endPlay() { + + // 如果正在播放,暂停播放 + if (this.mod === ActuatorModel.Play) { + this.pausePlay(); + } + + // 更新模式 + this.mod = ActuatorModel.View; + + // 清除状态 + this.playClip = undefined; + this.playFrameId = 0; + this.playFrame = undefined; + + // 渲染模型 + this.model.draw(); + + // 激发时钟状态事件 + this.emit("startChange", false); + } + + /** + * 是否播放完毕 + */ + public isPlayEnd() { + if (this.playClip && this.playFrame) { + if (this.playFrameId >= (this.playClip.frames.length - 1)) { + return true; + } else { + return false; + } + } else { + return true; + } + } + + public playing() { + + // 如果播放完毕了,从头开始播放 + if (this.isPlayEnd() && this.playClip) { + this.startPlay(this.playClip); + } + + // 更新模式 + this.mod = ActuatorModel.Play; + + // 启动播放时钟 + this.playTicker(); + + // 激发时钟状态事件 + this.emit("startChange", false); + } + + public pausePlay() { + + // 更新模式 + this.mod = ActuatorModel.View; + + // 激发时钟状态事件 + this.emit("startChange", false); + } + /** * 主时钟状态控制 */ @@ -108,6 +216,160 @@ class Actuator extends Emitter { public tickerType: 1 | 2 = 2; + private playTickerTimer?: number; + + /** + * 设置播放进度 + */ + public setPlayProcess(id: number) { + if (this.playClip && id >= 0 && id < this.playClip.frames.length) { + + // 跳转值这帧 + this.playFrameId = id; + this.playFrame = this.playClip.frames[this.playFrameId]; + this.emit("record", this.playFrame.duration); + + if (this.mod !== ActuatorModel.Play) { + this.playClip.play(this.playFrame); + } + } + } + + /** + * 离线渲染参数 + */ + public offlineAllFrame: number = 0; + public offlineCurrentFrame: number = 0; + private offlineRenderTickTimer?: number; + + /** + * 关闭离线渲染 + */ + public endOfflineRender() { + + // 清除 timer + clearTimeout(this.offlineRenderTickTimer); + + this.recordClip && (this.recordClip.isRecording = false); + this.recordClip = undefined; + + // 设置状态 + this.mod = ActuatorModel.View; + + // 激发结束事件 + this.start(false); + this.emit("record", 0); + } + + /** + * 离线渲染 tick + */ + private offlineRenderTick(dt: number) { + + if (this.mod !== ActuatorModel.Offline) { + return; + } + + if (this.offlineCurrentFrame >= this.offlineAllFrame) { + return this.endOfflineRender(); + } + + // 更新模型 + this.model.update(dt); + + // 录制 + this.recordClip?.record(dt); + + // 限制更新频率 + if (this.offlineCurrentFrame % 10 === 0) { + this.emit("offline", dt); + } + + this.offlineCurrentFrame++ + + if (this.offlineCurrentFrame <= this.offlineAllFrame) { + + // 下一个 tick + this.offlineRenderTickTimer = setTimeout(() => this.offlineRenderTick(dt)) as any; + + } else { + this.endOfflineRender(); + } + } + + /** + * 离线渲染 + */ + public offlineRender(clip: Clip, time: number, fps: number) { + + // 记录录制片段 + this.recordClip = clip; + clip.isRecording = true; + + // 如果仿真正在进行,停止仿真 + if (this.start()) this.start(false); + + // 如果正在录制,阻止 + if (this.mod === ActuatorModel.Record || this.mod === ActuatorModel.Offline) { + return; + } + + // 如果正在播放,暂停播放 + if (this.mod === ActuatorModel.Play) { + this.pausePlay(); + } + + // 设置状态 + this.mod = ActuatorModel.Offline; + + // 计算帧数 + this.offlineCurrentFrame = 0; + this.offlineAllFrame = Math.round(time * fps) - 1; + let dt = time / this.offlineAllFrame; + + // 第一帧渲染 + clip.record(0); + + // 开启时钟 + this.offlineRenderTick(dt); + + this.emit("record", dt); + } + + /** + * 播放时钟 + */ + private playTicker() { + + if (this.playClip && this.playFrame && this.mod === ActuatorModel.Play) { + + // 播放当前帧 + this.playClip.play(this.playFrame); + + // 没有完成播放,继续播放 + if (!this.isPlayEnd()) { + + // 跳转值下一帧 + this.playFrameId ++; + this.playFrame = this.playClip.frames[this.playFrameId]; + this.emit("record", this.playFrame.duration); + + // 清除计时器,保证时钟唯一性 + clearTimeout(this.playTickerTimer); + + // 延时 + this.playTickerTimer = setTimeout(() => { + this.playTicker(); + }, this.playFrame.duration * 1000) as any; + + } else { + this.pausePlay(); + } + } else { + this.pausePlay(); + } + } + private ticker(t: number) { if (this.startFlag && t !== 0) { if (this.lastTime === 0) { diff --git a/source/Model/Clip.ts b/source/Model/Clip.ts index 64008b8..be292d6 100644 --- a/source/Model/Clip.ts +++ b/source/Model/Clip.ts @@ -15,6 +15,7 @@ interface IDrawCommand { interface IFrame { commands: IDrawCommand[]; duration: number; + process: number; } /** @@ -81,17 +82,41 @@ class Clip { } } + const dt = this.frames.length <= 0 ? 0 : t; + this.time += dt; + const frame: IFrame = { commands: commands, - duration: t + duration: dt, + process: this.time }; - - this.time += t; + this.frames.push(frame); - return frame; } + /** + * 播放一帧 + */ + public play(frame: IFrame) { + + // 清除全部渲染状态 + this.model.renderer.clean(); + + // 执行全部渲染指令 + for (let i = 0; i < frame.commands.length; i++) { + const command: IDrawCommand = frame.commands[i]; + + if (command.type === "cube") { + this.model.renderer.cube(command.id, command.position, command.radius, command.parameter); + } + + else if (frame.commands[i].type === "points") { + this.model.renderer.points(command.id, command.data, command.parameter); + } + } + } + public equal(clip?: Clip) { return clip === this || clip?.id === this.id; } @@ -102,4 +127,4 @@ class Clip { } } -export { Clip }; \ No newline at end of file +export { Clip, IFrame }; \ No newline at end of file diff --git a/source/Model/Model.ts b/source/Model/Model.ts index 83012fa..612939a 100644 --- a/source/Model/Model.ts +++ b/source/Model/Model.ts @@ -371,7 +371,7 @@ class Model extends Emitter { } if (deletedClip) { - this.behaviorPool.splice(index, 1); + this.clipPool.splice(index, 1); console.log(`Model: Delete clip ${deletedClip.name ?? deletedClip.id}`); this.emit("clipChange", this.clipPool); } diff --git a/source/Panel/ClipPlayer/ClipPlayer.tsx b/source/Panel/ClipPlayer/ClipPlayer.tsx index 79feaa4..9d27929 100644 --- a/source/Panel/ClipPlayer/ClipPlayer.tsx +++ b/source/Panel/ClipPlayer/ClipPlayer.tsx @@ -1,38 +1,90 @@ import { Component, ReactNode } from "react"; import { ClipList } from "@Component/ClipList/ClipList"; import { useStatusWithEvent, IMixinStatusProps } from "@Context/Status"; -import { Theme } from "@Component/Theme/Theme"; +import { BackgroundLevel, FontLevel, Theme } from "@Component/Theme/Theme"; import { Message } from "@Input/Message/Message"; import { Clip } from "@Model/Clip"; import { ActuatorModel } from "@Model/Actuator"; +import { ConfirmPopup } from "@Component/ConfirmPopup/ConfirmPopup"; +import { OfflineRender } from "@Component/OfflineRender/OfflineRender" import "./ClipPlayer.scss"; @useStatusWithEvent("clipChange", "focusClipChange", "actuatorStartChange") class ClipPlayer extends Component { + private isInnerClick: boolean = false; + private renderMessage(): ReactNode { return ; } + private isClipListDisable() { + return !this.props.status?.focusClip && + ( + this.props.status?.actuator.mod === ActuatorModel.Record || + this.props.status?.actuator.mod === ActuatorModel.Offline + ); + } + private renderClipList(clipList: Clip[]): ReactNode { - const disable = - !this.props.status?.focusClip && - ( - this.props.status?.actuator.mod === ActuatorModel.Record || - this.props.status?.actuator.mod === ActuatorModel.Offline - ); - return { + this.isInnerClick = true; + const status = this.props.status; + if (status) { + status.popup.showPopup(ConfirmPopup, { + infoI18n: "Popup.Delete.Clip.Confirm", + titleI18N: "Popup.Action.Objects.Confirm.Title", + yesI18n: "Popup.Action.Objects.Confirm.Delete", + red: "yes", + yes: () => { + status.setClipObject(); + this.props.status?.actuator.endPlay(); + status.model.deleteClip(clip.id); + } + }); + } + }} + add={() => { + this.isInnerClick = true; + this.props.status?.popup.showPopup(OfflineRender, {}); + }} + click={(clip) => { + this.isInnerClick = true; + this.props.status?.setClipObject(clip); + this.props.status?.actuator.startPlay(clip); + }} />; } public render(): ReactNode { const clipList = this.props.status?.model.clipPool ?? []; - return + return { + + // 拦截禁用状态的事件 + if (this.isClipListDisable()) { + return; + } + + if (this.isInnerClick) { + this.isInnerClick = false; + } + + else { + this.props.status?.setClipObject(); + this.props.status?.actuator.endPlay(); + } + }} + > { clipList.length > 0 ? null : this.renderMessage() } { this.renderClipList(clipList) } ; diff --git a/source/Panel/ClipPlayer/ClipRecorder.tsx b/source/Panel/ClipPlayer/ClipRecorder.tsx index 8115f5d..46fda3f 100644 --- a/source/Panel/ClipPlayer/ClipRecorder.tsx +++ b/source/Panel/ClipPlayer/ClipRecorder.tsx @@ -9,7 +9,12 @@ class ClipRecorder extends Component { let mod: "P" | "R" = this.props.status?.focusClip ? "P" : "R"; let runner: boolean = false; - let currentTime: number = 0; + let currentTime: number | undefined = 0; + let allTime: number | undefined = 0; + let name: string | undefined; + let currentFrame: number | undefined = 0; + let allFrame: number | undefined = 0; + let fps: number | undefined = 0; // 是否开始录制 if (mod === "R") { @@ -19,18 +24,38 @@ class ClipRecorder extends Component { this.props.status?.actuator.mod === ActuatorModel.Offline; currentTime = this.props.status?.actuator.recordClip?.time ?? 0; + + name = this.props.status?.actuator.recordClip?.name; } else if (mod === "P") { // 是否正在播放 runner = this.props.status?.actuator.mod === ActuatorModel.Play; + name = this.props.status?.focusClip?.name; + allTime = this.props.status?.focusClip?.time; + allFrame = this.props.status?.focusClip?.frames.length; + currentFrame = this.props.status?.actuator.playFrameId; + currentTime = this.props.status?.actuator.playFrame?.process; + + if (allFrame !== undefined) { + allFrame --; + } + + if (allTime !== undefined && allFrame !== undefined) { + fps = Math.round(allFrame / allTime); + } } return { // 开启录制 @@ -51,6 +76,25 @@ class ClipRecorder extends Component { this.props.status?.actuator.endRecord(); console.log("ClipRecorder: Rec end..."); } + + // 开始播放 + if (mod === "P" && !runner) { + + // 启动播放时钟 + this.props.status?.actuator.playing(); + console.log("ClipRecorder: Play start..."); + } + + // 暂停播放 + if (mod === "P" && runner) { + + // 启动播放时钟 + this.props.status?.actuator.pausePlay(); + console.log("ClipRecorder: Pause play..."); + } + }} + valueChange={(value) => { + this.props.status?.actuator.setPlayProcess(value); }} /> }