Compare commits
	
		
			9 Commits
		
	
	
		
			0a5bcd76f0
			...
			5b0635270c
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 5b0635270c | |||
| 6c6ea52eff | |||
| c067088157 | |||
| 53e6c9db9c | |||
| 41fe51ec9c | |||
| 5a3ec7d2af | |||
| 776a5f571e | |||
| bc67782365 | |||
| c923efcd99 | 
							
								
								
									
										148
									
								
								source/Component/ClipList/ClipList.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								source/Component/ClipList/ClipList.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,148 @@ | ||||
| @import "../Theme/Theme.scss"; | ||||
| 
 | ||||
| $clip-item-height: 45px; | ||||
| 
 | ||||
| div.clip-list-root { | ||||
| 	margin: -5px; | ||||
| 	display: flex; | ||||
| 	flex-wrap: wrap; | ||||
| 
 | ||||
| 	div.clip-item { | ||||
| 		margin: 5px; | ||||
| 		height: $clip-item-height; | ||||
| 		user-select: none; | ||||
| 		border-radius: 3px; | ||||
|         overflow: hidden; | ||||
| 		cursor: pointer; | ||||
| 		display: flex; | ||||
| 
 | ||||
| 		div.clip-item-hole-view { | ||||
| 			height: 100%; | ||||
| 			width: 10px; | ||||
| 			box-sizing: border-box; | ||||
| 			display: flex; | ||||
| 			flex-direction: column; | ||||
| 			justify-items: center; | ||||
| 			justify-content: space-between; | ||||
| 			padding: 5px; | ||||
| 			padding-right: 0; | ||||
| 
 | ||||
| 			div.clip-item-hole { | ||||
| 				width: 5px; | ||||
| 				height: 5px; | ||||
| 				background-color: #000000; | ||||
| 				flex-shrink: 0; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		div.clip-icon-view { | ||||
| 			width: $clip-item-height; | ||||
| 			height: $clip-item-height; | ||||
| 			display: flex; | ||||
| 			justify-content: center; | ||||
| 			align-items: center; | ||||
| 
 | ||||
| 			i.icon { | ||||
| 				display: inline-block; | ||||
| 				font-size: 25px; | ||||
| 			} | ||||
| 
 | ||||
| 			i.delete { | ||||
| 				display: none; | ||||
| 			} | ||||
| 
 | ||||
| 			i.delete:hover { | ||||
| 				color: $lt-red; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		div.clip-item-content { | ||||
| 			width: calc( 100% - 65px ); | ||||
| 			padding-right: 10px; | ||||
| 			max-width: 125px; | ||||
| 			height: $clip-item-height; | ||||
| 			display: flex; | ||||
| 			flex-direction: column; | ||||
| 			justify-content: center; | ||||
| 
 | ||||
| 			div { | ||||
| 				white-space: nowrap; | ||||
| 				text-overflow: ellipsis; | ||||
| 				overflow: hidden; | ||||
| 				width: 100%; | ||||
| 			} | ||||
| 
 | ||||
| 			div.info { | ||||
| 				opacity: .75; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	div.clip-item.disable { | ||||
| 		cursor: not-allowed; | ||||
| 	} | ||||
| 
 | ||||
| 	div.clip-item.able:hover { | ||||
| 
 | ||||
| 		div.clip-icon-view { | ||||
| 
 | ||||
| 			i.icon { | ||||
| 				display: none; | ||||
| 			} | ||||
| 
 | ||||
| 			i.delete { | ||||
| 				display: inline-block; | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	div.add-button { | ||||
|         width: 26px; | ||||
|         height: 26px; | ||||
|         display: flex; | ||||
|         justify-content: center; | ||||
|         align-items: center; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| div.dark.clip-list-root { | ||||
| 
 | ||||
| 	div.clip-item { | ||||
| 		background-color: $lt-bg-color-lvl3-dark; | ||||
| 
 | ||||
| 		div.clip-item-hole-view div.clip-item-hole { | ||||
| 			background-color: $lt-bg-color-lvl4-dark; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	div.clip-item.able:hover { | ||||
| 		color: $lt-font-color-lvl2-dark; | ||||
| 		background-color: $lt-bg-color-lvl2-dark; | ||||
| 	} | ||||
| 
 | ||||
| 	div.clip-item.focus { | ||||
| 		color: $lt-font-color-lvl1-dark; | ||||
|     	background-color: $lt-bg-color-lvl1-dark; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| div.light.clip-list-root { | ||||
| 	 | ||||
| 	div.clip-item { | ||||
| 		background-color: $lt-bg-color-lvl3-light; | ||||
| 
 | ||||
| 		div.clip-item-hole-view div.clip-item-hole { | ||||
| 			background-color: $lt-bg-color-lvl4-light; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	div.clip-item.able:hover { | ||||
| 		color: $lt-font-color-lvl2-light; | ||||
| 		background-color: $lt-bg-color-lvl2-light; | ||||
| 	} | ||||
| 
 | ||||
| 	div.clip-item.focus { | ||||
| 		color: $lt-font-color-lvl1-light; | ||||
|     	background-color: $lt-bg-color-lvl1-light; | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										130
									
								
								source/Component/ClipList/ClipList.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								source/Component/ClipList/ClipList.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,130 @@ | ||||
| import { Localization } from "@Component/Localization/Localization"; | ||||
| import { Theme } from "@Component/Theme/Theme"; | ||||
| import { Icon } from "@fluentui/react"; | ||||
| import { Clip } from "@Model/Clip"; | ||||
| import { Component, ReactNode } from "react"; | ||||
| import "./ClipList.scss"; | ||||
| 
 | ||||
| interface IClipListProps { | ||||
| 	clips: Clip[]; | ||||
| 	focus?: Clip; | ||||
| 	disable?: boolean; | ||||
| 	add?: () => any; | ||||
| 	click?: (clip: Clip) => any; | ||||
| 	delete?: (clip: Clip) => any; | ||||
| } | ||||
| 
 | ||||
| class ClipList extends Component<IClipListProps> { | ||||
| 
 | ||||
| 	private isInnerClick: boolean = false; | ||||
| 
 | ||||
| 	private resolveCallback(fn?: (p: any) => any, p?: any): any { | ||||
| 		if (this.props.disable) { | ||||
| 			return false; | ||||
| 		} | ||||
| 		if (fn) { | ||||
| 			return fn(p); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	private parseTime(time?: number): string { | ||||
| 		if (time === undefined) { | ||||
| 			return "0:0:0:0"; | ||||
| 		} | ||||
| 		const h = Math.floor(time / 3600); | ||||
| 		const m = Math.floor((time % 3600) / 60); | ||||
| 		const s = Math.floor((time % 3600) % 60); | ||||
| 		const ms = Math.floor((time % 1) * 1000); | ||||
| 		return `${h}:${m}:${s}:${ms}`; | ||||
| 	} | ||||
| 
 | ||||
| 	private getClipInfo(clip: Clip): string { | ||||
| 		let fps = Math.floor(clip.frames.length / clip.time); | ||||
| 		if (isNaN(fps)) fps = 0; | ||||
| 		return `${this.parseTime(clip.time)} ${fps}fps`; | ||||
| 	} | ||||
| 
 | ||||
| 	private renderClip(clip: Clip) { | ||||
| 
 | ||||
| 		const focus = clip.equal(this.props.focus); | ||||
| 		const disable = this.props.disable; | ||||
| 		const classList = ["clip-item"]; | ||||
| 
 | ||||
| 		if (focus) { | ||||
| 			classList.push("focus"); | ||||
| 		} | ||||
| 
 | ||||
| 		if (disable) { | ||||
| 			classList.push("disable"); | ||||
| 		} else { | ||||
| 			classList.push("able"); | ||||
| 		} | ||||
| 
 | ||||
| 		return <div | ||||
| 			key={clip.id} | ||||
| 			className={classList.join(" ")} | ||||
| 			onClick={() => { | ||||
| 				if (this.isInnerClick) { | ||||
| 					this.isInnerClick = false; | ||||
| 				} else { | ||||
| 					this.resolveCallback(this.props.click, clip); | ||||
| 				} | ||||
| 			}} | ||||
| 		> | ||||
| 			<div className="clip-item-hole-view"> | ||||
| 				{new Array(4).fill(0).map((_, index) => { | ||||
| 					return <div className="clip-item-hole" key={index}/> | ||||
| 				})} | ||||
| 			</div> | ||||
| 			<div className="clip-icon-view"> | ||||
| 				<Icon iconName="MyMoviesTV" className="icon"/> | ||||
| 				<Icon | ||||
| 					iconName="Delete" | ||||
| 					className="delete" | ||||
| 					onClick={() => { | ||||
| 						this.isInnerClick = true; | ||||
| 						this.resolveCallback(this.props.delete, clip); | ||||
| 					}} | ||||
| 				/> | ||||
| 			</div> | ||||
| 			<div className="clip-item-content"> | ||||
| 				<div className="title">{clip.name}</div> | ||||
| 				<div className="info">{ | ||||
| 					clip.isRecording ? | ||||
| 						<Localization i18nKey="Panel.Info.Behavior.Clip.Uname.Clip"/> : | ||||
| 						this.getClipInfo(clip) | ||||
| 				}</div> | ||||
| 			</div> | ||||
| 		</div>; | ||||
| 	} | ||||
| 
 | ||||
| 	private renderAddButton(): ReactNode { | ||||
| 
 | ||||
| 		const classList = ["clip-item", "add-button"]; | ||||
| 
 | ||||
| 		if (this.props.disable) { | ||||
| 			classList.push("disable"); | ||||
| 		} else { | ||||
| 			classList.push("able"); | ||||
| 		} | ||||
| 
 | ||||
| 		return <div | ||||
| 			key="ADD_BUTTON" | ||||
| 			className={classList.join(" ")} | ||||
| 			onClick={() => this.resolveCallback(this.props.add)} | ||||
| 		> | ||||
|             <Icon iconName="Add"/> | ||||
|         </div> | ||||
| 	} | ||||
| 
 | ||||
| 	public render(): ReactNode { | ||||
| 		return <Theme className="clip-list-root"> | ||||
| 			{ this.props.clips.map((clip => { | ||||
| 				return this.renderClip(clip); | ||||
| 			})) } | ||||
| 			{ this.renderAddButton() } | ||||
| 		</Theme>; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| export { ClipList }; | ||||
							
								
								
									
										155
									
								
								source/Component/Recorder/Recorder.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								source/Component/Recorder/Recorder.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,155 @@ | ||||
| @import "../Theme/Theme.scss"; | ||||
| 
 | ||||
| div.recorder-root { | ||||
| 	width: 100%; | ||||
| 	box-sizing: border-box; | ||||
| 	padding: 10px 10px 0 10px; | ||||
| 
 | ||||
| 	div.recorder-slider { | ||||
| 		width: 100%; | ||||
| 
 | ||||
| 		div.ms-Slider-slideBox { | ||||
| 			height: 16px; | ||||
| 		} | ||||
| 
 | ||||
| 		span.ms-Slider-thumb { | ||||
| 			width: 12px; | ||||
| 			height: 12px; | ||||
| 			line-height: 16px; | ||||
| 			border-width: 3px; | ||||
| 			top: -4px; | ||||
| 		} | ||||
| 
 | ||||
| 		span.ms-Slider-active { | ||||
| 			height: 3px; | ||||
| 		} | ||||
| 		 | ||||
| 		span.ms-Slider-inactive { | ||||
| 			height: 3px; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	div.recorder-slider.disable { | ||||
| 		opacity: .6; | ||||
| 	} | ||||
| 
 | ||||
| 	div.recorder-content { | ||||
| 		width: 100%; | ||||
| 		height: 32px; | ||||
| 		display: flex; | ||||
| 		justify-content: space-between; | ||||
| 		align-items: center; | ||||
| 		box-sizing: border-box; | ||||
| 		padding: 0 8px; | ||||
| 
 | ||||
| 		div.time-view { | ||||
| 			flex-shrink: 1; | ||||
| 			width: 50%; | ||||
| 			text-align: left; | ||||
| 			white-space: nowrap; | ||||
| 			overflow: hidden; | ||||
| 			text-overflow: ellipsis; | ||||
| 		} | ||||
| 
 | ||||
| 		div.ctrl-button { | ||||
| 			cursor: pointer; | ||||
| 			user-select: none; | ||||
| 			width: 96px; | ||||
| 			flex-shrink: 0; | ||||
| 			text-align: center; | ||||
| 			display: flex; | ||||
| 			justify-content: center; | ||||
| 			align-items: center; | ||||
| 
 | ||||
| 			div.ctrl-action { | ||||
| 				width: 32px; | ||||
| 				height: 32px; | ||||
| 				display: flex; | ||||
| 				justify-content: center; | ||||
| 				align-items: center; | ||||
| 			} | ||||
| 
 | ||||
| 			div.ctrl-action-main { | ||||
| 				font-size: 1.5em; | ||||
| 			} | ||||
| 
 | ||||
| 			div.ctrl-action.disable { | ||||
| 				cursor: not-allowed; | ||||
| 				opacity: .6; | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		div.speed-view { | ||||
| 			flex-shrink: 1; | ||||
| 			width: 50%; | ||||
| 			text-align: right; | ||||
| 			white-space: nowrap; | ||||
| 			overflow: hidden; | ||||
| 			text-overflow: ellipsis; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| div.recorder-root.light { | ||||
| 
 | ||||
| 	div.recorder-slider { | ||||
| 
 | ||||
| 		span.ms-Slider-thumb { | ||||
| 			background-color: $lt-bg-color-lvl1-light; | ||||
| 			border-color: $lt-font-color-normal-light; | ||||
| 		} | ||||
| 
 | ||||
| 		span.ms-Slider-active { | ||||
| 			background-color: $lt-font-color-normal-light; | ||||
| 		} | ||||
| 		 | ||||
| 		span.ms-Slider-inactive { | ||||
| 			background-color: $lt-bg-color-lvl1-light; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	div.recorder-content { | ||||
| 
 | ||||
| 		div.ctrl-button div.ctrl-action:hover { | ||||
| 			background-color: $lt-bg-color-lvl3-light; | ||||
| 			color: $lt-font-color-lvl1-light; | ||||
| 		} | ||||
| 
 | ||||
| 		div.ctrl-button div.ctrl-action.disable:hover { | ||||
| 			background-color: $lt-bg-color-lvl4-light; | ||||
| 			color: $lt-font-color-normal-light; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| div.recorder-root.dark { | ||||
| 	 | ||||
| 	div.recorder-slider { | ||||
| 
 | ||||
| 		span.ms-Slider-thumb { | ||||
| 			background-color: $lt-bg-color-lvl1-dark; | ||||
| 			border-color: $lt-font-color-normal-dark; | ||||
| 		} | ||||
| 
 | ||||
| 		span.ms-Slider-active { | ||||
| 			background-color: $lt-font-color-normal-dark; | ||||
| 		} | ||||
| 		 | ||||
| 		span.ms-Slider-inactive { | ||||
| 			background-color: $lt-bg-color-lvl1-dark; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	div.recorder-content { | ||||
| 
 | ||||
| 		div.ctrl-button div.ctrl-action:hover { | ||||
| 			background-color: $lt-bg-color-lvl3-dark; | ||||
| 			color: $lt-font-color-lvl1-dark; | ||||
| 		} | ||||
| 
 | ||||
| 		div.ctrl-button div.ctrl-action.disable:hover { | ||||
| 			background-color: $lt-bg-color-lvl4-dark; | ||||
| 			color: $lt-font-color-normal-dark; | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										116
									
								
								source/Component/Recorder/Recorder.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								source/Component/Recorder/Recorder.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,116 @@ | ||||
| import { Localization } from "@Component/Localization/Localization"; | ||||
| import { BackgroundLevel, FontLevel, Theme } from "@Component/Theme/Theme"; | ||||
| import { Icon, Slider } from "@fluentui/react"; | ||||
| import { Component, ReactNode } from "react"; | ||||
| import "./Recorder.scss"; | ||||
| 
 | ||||
| interface IRecorderProps { | ||||
| 	mode: "P" | "R", | ||||
| 	running?: boolean, | ||||
| 	name?: string; | ||||
| 	fps?: number; | ||||
| 	allFrame?: number; | ||||
| 	currentFrame?: number; | ||||
| 	allTime?: number; | ||||
| 	currentTime?: number; | ||||
| 	action?: () => void; | ||||
| } | ||||
| 
 | ||||
| class Recorder extends Component<IRecorderProps> { | ||||
| 
 | ||||
| 	private parseTime(time?: number): string { | ||||
| 		if (time === undefined) { | ||||
| 			return "0:0:0:0"; | ||||
| 		} | ||||
| 		const h = Math.floor(time / 3600); | ||||
| 		const m = Math.floor((time % 3600) / 60); | ||||
| 		const s = Math.floor((time % 3600) % 60); | ||||
| 		const ms = Math.floor((time % 1) * 1000); | ||||
| 		return `${h}:${m}:${s}:${ms}`; | ||||
| 	} | ||||
| 
 | ||||
| 	private getRecordInfo(): ReactNode { | ||||
| 		if (this.props.mode === "P") { | ||||
| 			return <Localization | ||||
| 				i18nKey="Panel.Info.Behavior.Clip.Time.Formate" | ||||
| 				options={{ | ||||
| 					current: this.parseTime(this.props.currentTime), | ||||
| 					all: this.parseTime(this.props.allTime), | ||||
| 					fps: this.props.fps ? this.props.fps.toString() : "0" | ||||
| 				}} | ||||
| 			/>; | ||||
| 		} | ||||
| 		else if (this.props.mode === "R") { | ||||
| 			return <Localization | ||||
| 				i18nKey="Panel.Info.Behavior.Clip.Record.Formate" | ||||
| 				options={{ | ||||
| 					time: this.parseTime(this.props.currentTime), | ||||
| 				}} | ||||
| 			/>; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	private getActionIcon(): string { | ||||
| 		if (this.props.mode === "P") { | ||||
| 			if (this.props.running) { | ||||
| 				return "Pause"; | ||||
| 			} else { | ||||
| 				return "Play"; | ||||
| 			} | ||||
| 		} | ||||
| 		else if (this.props.mode === "R") { | ||||
| 			if (this.props.running) { | ||||
| 				return "Stop"; | ||||
| 			} else { | ||||
| 				return "StatusCircleRing"; | ||||
| 			} | ||||
| 		} | ||||
| 		return "Play"; | ||||
| 	} | ||||
| 
 | ||||
| 	public render(): ReactNode { | ||||
| 
 | ||||
| 		const isSliderDisable = this.props.mode === "R"; | ||||
| 		const isJumpDisable = this.props.mode === "R"; | ||||
| 
 | ||||
| 		return <Theme | ||||
| 			className="recorder-root" | ||||
| 			backgroundLevel={BackgroundLevel.Level4} | ||||
| 			fontLevel={FontLevel.normal} | ||||
| 		> | ||||
| 			<Slider | ||||
| 				min={0} | ||||
| 				disabled={isSliderDisable} | ||||
| 				value={this.props.currentFrame} | ||||
| 				max={this.props.allFrame} | ||||
| 				className={"recorder-slider" + (isSliderDisable ? " disable" : "")} | ||||
| 				showValue={false} | ||||
| 			/> | ||||
| 			<div className="recorder-content"> | ||||
| 				<div className="time-view"> | ||||
| 					{this.getRecordInfo()} | ||||
| 				</div> | ||||
| 				<div className="ctrl-button"> | ||||
| 					<div className={"ctrl-action" + (isJumpDisable ? " disable" : "")}> | ||||
| 						<Icon iconName="Back"/> | ||||
| 					</div> | ||||
| 					<div className="ctrl-action ctrl-action-main" onClick={this.props.action}> | ||||
| 						<Icon iconName={this.getActionIcon()}/> | ||||
| 					</div> | ||||
| 					<div className={"ctrl-action" + (isJumpDisable ? " disable" : "")}> | ||||
| 						<Icon iconName="Forward"/> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<div className="speed-view"> | ||||
| 					{ | ||||
| 						this.props.name ? | ||||
| 							<span>{this.props.name}</span> : | ||||
| 							<Localization i18nKey="Panel.Info.Behavior.Clip.Uname.Clip"/> | ||||
| 					} | ||||
| 				</div> | ||||
| 			</div> | ||||
| 		</Theme>; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| export { Recorder }; | ||||
| @ -14,6 +14,7 @@ import { PopupController } from "@Context/Popups"; | ||||
| import { Behavior } from "@Model/Behavior"; | ||||
| import { IParameter, IParamValue } from "@Model/Parameter"; | ||||
| import { Actuator } from "@Model/Actuator"; | ||||
| import { Clip } from "@Model/Clip"; | ||||
| 
 | ||||
| function randomColor(unNormal: boolean = false) { | ||||
|     const color = [ | ||||
| @ -35,14 +36,17 @@ interface IStatusEvent { | ||||
|     fileChange: void; | ||||
|     renderLoop: number; | ||||
|     physicsLoop: number; | ||||
|     recordLoop: number; | ||||
|     mouseModChange: void; | ||||
|     focusObjectChange: void; | ||||
|     focusLabelChange: void; | ||||
|     focusBehaviorChange: void; | ||||
|     objectChange: void; | ||||
|     focusClipChange: void; | ||||
|     rangeLabelChange: void; | ||||
|     groupLabelChange: void; | ||||
|     groupBehaviorChange: void; | ||||
|     clipChange: void; | ||||
|     labelChange: void; | ||||
|     rangeAttrChange: void; | ||||
|     labelAttrChange: void; | ||||
| @ -98,6 +102,11 @@ class Status extends Emitter<IStatusEvent> { | ||||
|      */ | ||||
|     public focusBehavior?: Behavior; | ||||
| 
 | ||||
|     /** | ||||
|      * 焦点行为 | ||||
|      */ | ||||
|     public focusClip?: Clip; | ||||
| 
 | ||||
|     private drawTimer?: NodeJS.Timeout; | ||||
| 
 | ||||
|     private delayDraw = () => { | ||||
| @ -119,11 +128,13 @@ class Status extends Emitter<IStatusEvent> { | ||||
| 
 | ||||
|         // 循环事件
 | ||||
|         this.actuator.on("loop", (t) => { this.emit("physicsLoop", t) }); | ||||
|         this.actuator.on("record", (t) => { this.emit("recordLoop", t) }); | ||||
| 
 | ||||
|         // 对象变化事件
 | ||||
|         this.model.on("objectChange", () => this.emit("objectChange")); | ||||
|         this.model.on("labelChange", () => this.emit("labelChange")); | ||||
|         this.model.on("behaviorChange", () => this.emit("behaviorChange")); | ||||
|         this.model.on("clipChange", () => this.emit("clipChange")); | ||||
| 
 | ||||
|         // 弹窗事件
 | ||||
|         this.popup.on("popupChange", () => this.emit("popupChange")); | ||||
| @ -220,6 +231,16 @@ class Status extends Emitter<IStatusEvent> { | ||||
|         this.emit("focusBehaviorChange"); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 更新焦点行为 | ||||
|      */ | ||||
|     public setClipObject(clip?: Clip) { | ||||
|         if (this.focusClip !== clip) { | ||||
|             this.focusClip = clip; | ||||
|         } | ||||
|         this.emit("focusClipChange"); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 修改范围属性 | ||||
|      */ | ||||
| @ -403,6 +424,22 @@ class Status extends Emitter<IStatusEvent> { | ||||
|         return label; | ||||
|     } | ||||
| 
 | ||||
|     public newClip() { | ||||
|         let searchKey = I18N(this.setting.language, "Object.List.New.Clip", { id: "" }); | ||||
|         let nextIndex = 1; | ||||
|         this.model.clipPool.forEach((obj) => { | ||||
|             nextIndex = Math.max(nextIndex, this.getNextNumber( | ||||
|                 obj.name, searchKey | ||||
|             )); | ||||
|         }); | ||||
|         const clip = this.model.addClip( | ||||
|             I18N(this.setting.language, "Object.List.New.Clip", { | ||||
|                 id: nextIndex.toString() | ||||
|             }) | ||||
|         ); | ||||
|         return clip; | ||||
|     } | ||||
| 
 | ||||
|     public setMouseMod(mod: MouseMod) { | ||||
|         this.mouseMod = mod; | ||||
|         if (this.renderer instanceof ClassicRenderer) { | ||||
|  | ||||
| @ -31,6 +31,7 @@ const EN_US = { | ||||
|     "Object.List.New.Group": "Group object {id}", | ||||
|     "Object.List.New.Range": "Range object {id}", | ||||
|     "Object.List.New.Label": "Label {id}", | ||||
|     "Object.List.New.Clip": "Clip {id}", | ||||
|     "Object.List.No.Data": "There are no objects in the model, click the button to create it", | ||||
|     "Object.Picker.List.No.Data": "There is no model in the model for this option", | ||||
|     "Behavior.Picker.Add.Button": "Click here to assign behavior to this group", | ||||
| @ -53,6 +54,11 @@ const EN_US = { | ||||
|     "Panel.Info.Behavior.List.View": "Edit view behavior list", | ||||
|     "Panel.Title.Behavior.Details.View": "Behavior", | ||||
|     "Panel.Info.Behavior.Details.View": "Edit view Behavior attributes", | ||||
|     "Panel.Title.Behavior.Clip.Player": "Recording", | ||||
|     "Panel.Info.Behavior.Clip.Player": "Pre render recorded data", | ||||
|     "Panel.Info.Behavior.Clip.Time.Formate": "{current} / {all} / {fps}fps", | ||||
|     "Panel.Info.Behavior.Clip.Record.Formate": "Record: {time}", | ||||
|     "Panel.Info.Behavior.Clip.Uname.Clip": "Waiting for recording...", | ||||
|     "Popup.Title.Unnamed": "Popup message", | ||||
|     "Popup.Title.Confirm": "Confirm message", | ||||
|     "Popup.Action.Yes": "Confirm", | ||||
| @ -143,6 +149,7 @@ 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", | ||||
|     "Panel.Info.Clip.List.Error.Nodata": "There is no clip, please click the record button to record, or click the plus sign to create", | ||||
|     "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", | ||||
|  | ||||
| @ -31,6 +31,7 @@ const ZH_CN = { | ||||
|     "Object.List.New.Group": "群对象 {id}", | ||||
|     "Object.List.New.Range": "范围对象 {id}", | ||||
|     "Object.List.New.Label": "标签 {id}", | ||||
|     "Object.List.New.Clip": "剪辑片段 {id}", | ||||
|     "Object.List.No.Data": "模型中没有任何对象,点击按钮以创建", | ||||
|     "Object.Picker.List.No.Data": "模型中没有合适此选项的模型", | ||||
|     "Behavior.Picker.Add.Button": "点击此处以赋予行为到此群", | ||||
| @ -53,6 +54,11 @@ const ZH_CN = { | ||||
|     "Panel.Info.Behavior.List.View": "编辑查看行为列表", | ||||
|     "Panel.Title.Behavior.Details.View": "行为", | ||||
|     "Panel.Info.Behavior.Details.View": "编辑查看行为属性", | ||||
|     "Panel.Title.Behavior.Clip.Player": "录制", | ||||
|     "Panel.Info.Behavior.Clip.Player": "预渲染录制数据", | ||||
|     "Panel.Info.Behavior.Clip.Time.Formate": "{current} / {all} / {fps} fps", | ||||
|     "Panel.Info.Behavior.Clip.Record.Formate": "录制: {time}", | ||||
|     "Panel.Info.Behavior.Clip.Uname.Clip": "等待录制...", | ||||
|     "Popup.Title.Unnamed": "弹窗消息", | ||||
|     "Popup.Title.Confirm": "确认消息", | ||||
|     "Popup.Action.Yes": "确定", | ||||
| @ -143,6 +149,7 @@ 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 坐标", | ||||
|     "Panel.Info.Clip.List.Error.Nodata": "没有剪辑片段,请点击录制按钮录制,或者点击加号创建", | ||||
|     "Info.Hint.Save.After.Close": "任何未保存的进度都会丢失, 确定要继续吗?", | ||||
|     "Info.Hint.Load.File.Title": "加载存档", | ||||
|     "Info.Hint.Load.File.Intro": "释放以加载拽入的存档", | ||||
|  | ||||
| @ -1,8 +1,17 @@ | ||||
| import { Model } from "@Model/Model"; | ||||
| import { Emitter } from "@Model/Emitter"; | ||||
| import { Clip } from "@Model/Clip"; | ||||
| 
 | ||||
| enum ActuatorModel { | ||||
| 	Play = 1, | ||||
| 	Record = 2, | ||||
| 	View = 3, | ||||
| 	Offline = 4 | ||||
| } | ||||
| 
 | ||||
| interface IActuatorEvent { | ||||
| 	startChange: boolean; | ||||
| 	record: number; | ||||
| 	loop: number; | ||||
| } | ||||
| 
 | ||||
| @ -26,6 +35,47 @@ class Actuator extends Emitter<IActuatorEvent> { | ||||
| 	 */ | ||||
| 	private startFlag: boolean = false; | ||||
| 
 | ||||
| 	/** | ||||
| 	 * 模式 | ||||
| 	 */ | ||||
| 	public mod: ActuatorModel = ActuatorModel.View; | ||||
| 
 | ||||
| 	/** | ||||
| 	 * 录制剪辑 | ||||
| 	 */ | ||||
| 	public recordClip?: Clip; | ||||
| 
 | ||||
| 	/** | ||||
| 	 * 开始录制 | ||||
| 	 */ | ||||
| 	public startRecord(clip: Clip) { | ||||
| 
 | ||||
| 		// 记录录制片段
 | ||||
| 		this.recordClip = clip; | ||||
| 		clip.isRecording = true; | ||||
| 
 | ||||
| 		// 如果仿真未开启,开启仿真
 | ||||
| 		if (!this.start()) this.start(true); | ||||
| 
 | ||||
| 		// 设置状态
 | ||||
| 		this.mod = ActuatorModel.Record; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * 结束录制 | ||||
| 	 */ | ||||
| 	public endRecord() { | ||||
| 
 | ||||
| 		this.recordClip && (this.recordClip.isRecording = false); | ||||
| 		this.recordClip = undefined; | ||||
| 
 | ||||
| 		// 如果仿真未停止,停止仿真
 | ||||
| 		if (this.start()) this.start(false); | ||||
| 
 | ||||
| 		// 设置状态
 | ||||
| 		this.mod = ActuatorModel.View; | ||||
| 	} | ||||
| 
 | ||||
| 	/** | ||||
| 	 * 主时钟状态控制 | ||||
| 	 */ | ||||
| @ -72,13 +122,30 @@ class Actuator extends Emitter<IActuatorEvent> { | ||||
| 				} else { | ||||
| 					this.alignTimer += durTime; | ||||
| 					if (this.alignTimer > (1 / this.fps)) { | ||||
| 
 | ||||
| 						// 更新模型
 | ||||
| 						this.model.update(this.alignTimer * this.speed); | ||||
| 
 | ||||
| 						// 绘制模型
 | ||||
| 						this.model.draw(); | ||||
| 
 | ||||
| 						// 录制模型
 | ||||
| 						if ( | ||||
| 							this.mod === ActuatorModel.Record || | ||||
| 							this.mod === ActuatorModel.Offline | ||||
| 						) { | ||||
| 							this.recordClip?.record(this.alignTimer * this.speed); | ||||
| 							this.emit("record", this.alignTimer); | ||||
| 						} | ||||
| 
 | ||||
| 						this.emit("loop", this.alignTimer); | ||||
| 						this.alignTimer = 0; | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
| 		} else { | ||||
| 		} | ||||
| 		 | ||||
| 		else { | ||||
| 			this.emit("loop", Infinity); | ||||
| 		} | ||||
| 	} | ||||
| @ -122,4 +189,4 @@ class Actuator extends Emitter<IActuatorEvent> { | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| export { Actuator } | ||||
| export { Actuator, ActuatorModel } | ||||
							
								
								
									
										105
									
								
								source/Model/Clip.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								source/Model/Clip.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,105 @@ | ||||
| import { IAnyObject, Model } from "@Model/Model"; | ||||
| import { v4 as uuid } from "uuid"; | ||||
| import { Group } from "@Model/Group"; | ||||
| import { Range } from "@Model/Range"; | ||||
| 
 | ||||
| interface IDrawCommand { | ||||
| 	type: "points" | "cube"; | ||||
| 	id: string; | ||||
| 	data?: Float32Array; | ||||
| 	position?: number[]; | ||||
| 	radius?: number[]; | ||||
| 	parameter?: IAnyObject; | ||||
| } | ||||
| 
 | ||||
| interface IFrame { | ||||
| 	commands: IDrawCommand[]; | ||||
| 	duration: number; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * 剪辑片段 | ||||
|  */ | ||||
| class Clip { | ||||
| 
 | ||||
| 	public id: string; | ||||
| 
 | ||||
| 	/** | ||||
| 	 * 时间 | ||||
| 	 */ | ||||
| 	public time: number = 0; | ||||
| 
 | ||||
| 	/** | ||||
| 	 * 用户自定义名称 | ||||
| 	 */ | ||||
| 	public name: string = ""; | ||||
| 
 | ||||
| 	/** | ||||
| 	 * 模型 | ||||
| 	 */ | ||||
| 	public model: Model; | ||||
| 
 | ||||
| 	/** | ||||
| 	 * 全部帧 | ||||
| 	 */ | ||||
| 	public frames: IFrame[] = []; | ||||
| 
 | ||||
| 	/** | ||||
| 	 * 是否正在录制 | ||||
| 	 */ | ||||
| 	public isRecording: boolean = false; | ||||
| 
 | ||||
| 	/** | ||||
| 	 * 录制一帧 | ||||
| 	 */ | ||||
| 	public record(t: number): IFrame { | ||||
| 		const commands: IDrawCommand[] = []; | ||||
| 
 | ||||
| 		for (let i = 0; i < this.model.objectPool.length; i++) { | ||||
| 
 | ||||
|             let object = this.model.objectPool[i]; | ||||
|             object.renderParameter.color = object.color; | ||||
| 
 | ||||
|             if (object.display && object instanceof Group) { | ||||
| 				commands.push({ | ||||
| 					type: "points", | ||||
| 					id: object.id, | ||||
| 					data: object.exportPositionData(), | ||||
| 					parameter: object.renderParameter | ||||
| 				}); | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             if (object.display && object instanceof Range) { | ||||
| 				commands.push({ | ||||
| 					type: "cube", | ||||
| 					id: object.id, | ||||
| 					position: object.position, | ||||
| 					radius: object.radius, | ||||
| 					parameter: object.renderParameter | ||||
| 				}); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| 		const frame: IFrame = { | ||||
| 			commands: commands, | ||||
| 			duration: t | ||||
| 		}; | ||||
| 
 | ||||
| 		this.time += t; | ||||
| 		this.frames.push(frame); | ||||
| 
 | ||||
| 		return frame; | ||||
| 	} | ||||
| 
 | ||||
| 	public equal(clip?: Clip) { | ||||
| 		return clip === this || clip?.id === this.id; | ||||
| 	} | ||||
| 
 | ||||
| 	public constructor(model: Model) { | ||||
| 		this.model = model; | ||||
| 		this.id = uuid(); | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| export { Clip }; | ||||
| @ -5,6 +5,7 @@ import { IParamValue } from "@Model/Parameter"; | ||||
| import { CtrlObject } from "@Model/CtrlObject"; | ||||
| import { Emitter } from "@Model/Emitter"; | ||||
| import { AbstractRenderer } from "@Model/Renderer"; | ||||
| import { Clip } from "@Model/Clip"; | ||||
| import { Behavior, IAnyBehavior, IAnyBehaviorRecorder } from "@Model/Behavior"; | ||||
| 
 | ||||
| /** | ||||
| @ -22,6 +23,7 @@ type ModelEvent = { | ||||
|     objectChange: CtrlObject[]; | ||||
|     individualChange: Group; | ||||
|     behaviorChange: IAnyBehavior; | ||||
|     clipChange: Clip[]; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
| @ -330,6 +332,51 @@ class Model extends Emitter<ModelEvent> { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 剪辑数据 | ||||
|      */ | ||||
|     public clipPool: Clip[] = []; | ||||
| 
 | ||||
|     /** | ||||
|      * 新建剪辑片段 | ||||
|      */ | ||||
|     public addClip(name?: string): Clip { | ||||
|         let newClip = new Clip(this); | ||||
|         newClip.name = name ?? ""; | ||||
|         this.clipPool.push(newClip); | ||||
|         console.log(`Model: Create clip ${name ?? newClip.id}`); | ||||
|         this.emit("clipChange", this.clipPool); | ||||
|         return newClip; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 删除一个剪辑片段 | ||||
|      */ | ||||
|     public deleteClip(name: ObjectID | Clip) { | ||||
|         let deletedClip: Clip | undefined; | ||||
|         let index = 0; | ||||
| 
 | ||||
|         for (let i = 0; i < this.clipPool.length; i++) { | ||||
|             if (name instanceof Clip) { | ||||
|                 if (this.clipPool[i].equal(name)) { | ||||
|                     deletedClip = this.clipPool[i]; | ||||
|                     index = i; | ||||
|                 } | ||||
|             } | ||||
|              | ||||
|             else if (name === this.clipPool[i].id) { | ||||
|                 deletedClip = this.clipPool[i]; | ||||
|                 index = i; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         if (deletedClip) { | ||||
|             this.behaviorPool.splice(index, 1); | ||||
|             console.log(`Model: Delete clip ${deletedClip.name ?? deletedClip.id}`); | ||||
|             this.emit("clipChange", this.clipPool); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 渲染器 | ||||
|      */ | ||||
| @ -372,8 +419,6 @@ class Model extends Emitter<ModelEvent> { | ||||
|                 object.runner(t, "finalEffect"); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         this.draw(); | ||||
|     } | ||||
| 
 | ||||
|     public draw() { | ||||
|  | ||||
| @ -55,7 +55,7 @@ class SimulatorWeb extends Component { | ||||
|                     items: [ | ||||
|                         {panels: ["RenderView"]}, | ||||
|                         { | ||||
|                             items: [{panels: ["BehaviorList"]}, {panels: ["LabelList"]}], | ||||
|                             items: [{panels: ["ClipPlayer", "BehaviorList"]}, {panels: ["LabelList"]}], | ||||
|                             scale: 80, | ||||
|                             layout: LayoutDirection.X | ||||
|                         } | ||||
|  | ||||
							
								
								
									
										6
									
								
								source/Panel/ClipPlayer/ClipPlayer.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								source/Panel/ClipPlayer/ClipPlayer.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | ||||
| div.Clip-player-clip-list-root { | ||||
| 	width: 100%; | ||||
| 	height: 100%; | ||||
| 	box-sizing: border-box; | ||||
| 	padding: 10px; | ||||
| } | ||||
							
								
								
									
										42
									
								
								source/Panel/ClipPlayer/ClipPlayer.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								source/Panel/ClipPlayer/ClipPlayer.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | ||||
| import { Component, ReactNode } from "react"; | ||||
| import { ClipList } from "@Component/ClipList/ClipList"; | ||||
| import { useStatusWithEvent, IMixinStatusProps } from "@Context/Status"; | ||||
| import { Theme } from "@Component/Theme/Theme"; | ||||
| import { Message } from "@Input/Message/Message"; | ||||
| import { Clip } from "@Model/Clip"; | ||||
| import { ActuatorModel } from "@Model/Actuator"; | ||||
| import "./ClipPlayer.scss"; | ||||
| 
 | ||||
| @useStatusWithEvent("clipChange", "focusClipChange", "actuatorStartChange") | ||||
| class ClipPlayer extends Component<IMixinStatusProps> { | ||||
| 
 | ||||
| 	private renderMessage(): ReactNode { | ||||
| 		return <Message i18nKey="Panel.Info.Clip.List.Error.Nodata"/>; | ||||
| 	} | ||||
| 
 | ||||
| 	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 <ClipList | ||||
| 			clips={clipList} | ||||
| 			disable={disable} | ||||
| 		/>; | ||||
| 	} | ||||
| 
 | ||||
| 	public render(): ReactNode { | ||||
| 		const clipList = this.props.status?.model.clipPool ?? []; | ||||
| 
 | ||||
| 		return <Theme className="Clip-player-clip-list-root"> | ||||
| 			{ clipList.length > 0 ? null : this.renderMessage() } | ||||
| 			{ this.renderClipList(clipList) } | ||||
| 		</Theme>; | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| export { ClipPlayer }; | ||||
							
								
								
									
										59
									
								
								source/Panel/ClipPlayer/ClipRecorder.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								source/Panel/ClipPlayer/ClipRecorder.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,59 @@ | ||||
| import { Component, ReactNode } from "react"; | ||||
| import { useStatusWithEvent, IMixinStatusProps } from "@Context/Status"; | ||||
| import { Recorder } from "@Component/Recorder/Recorder"; | ||||
| import { ActuatorModel } from "@Model/Actuator"; | ||||
| 
 | ||||
| @useStatusWithEvent("actuatorStartChange", "focusClipChange", "recordLoop") | ||||
| class ClipRecorder extends Component<IMixinStatusProps> { | ||||
| 	public render(): ReactNode { | ||||
| 
 | ||||
| 		let mod: "P" | "R" = this.props.status?.focusClip ? "P" : "R"; | ||||
| 		let runner: boolean = false; | ||||
| 		let currentTime: number = 0; | ||||
| 
 | ||||
| 		// 是否开始录制
 | ||||
| 		if (mod === "R") { | ||||
| 
 | ||||
| 			// 是否正在录制
 | ||||
| 			runner = this.props.status?.actuator.mod === ActuatorModel.Record || | ||||
| 			this.props.status?.actuator.mod === ActuatorModel.Offline; | ||||
| 
 | ||||
| 			currentTime = this.props.status?.actuator.recordClip?.time ?? 0; | ||||
| 		} | ||||
| 
 | ||||
| 		else if (mod === "P") { | ||||
| 
 | ||||
| 			// 是否正在播放
 | ||||
| 			runner = this.props.status?.actuator.mod === ActuatorModel.Play; | ||||
| 		} | ||||
| 
 | ||||
| 		return <Recorder | ||||
| 			currentTime={currentTime} | ||||
| 			mode={mod} | ||||
| 			running={runner} | ||||
| 			action={() => { | ||||
| 
 | ||||
| 				// 开启录制
 | ||||
| 				if (mod === "R" && !runner) { | ||||
| 
 | ||||
| 					// 获取新实例
 | ||||
| 					let newClip = this.props.status?.newClip(); | ||||
| 					 | ||||
| 					// 开启录制时钟
 | ||||
| 					this.props.status?.actuator.startRecord(newClip!); | ||||
| 					console.log("ClipRecorder: Rec start..."); | ||||
| 				} | ||||
| 
 | ||||
| 				// 暂停录制
 | ||||
| 				if (mod === "R" && runner) { | ||||
| 
 | ||||
| 					// 暂停录制时钟
 | ||||
| 					this.props.status?.actuator.endRecord(); | ||||
| 					console.log("ClipRecorder: Rec end..."); | ||||
| 				} | ||||
| 			}} | ||||
| 		/> | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| export { ClipRecorder }; | ||||
| @ -10,6 +10,8 @@ import { LabelDetails } from "@Panel/LabelDetails/LabelDetails"; | ||||
| import { GroupDetails } from "@Panel/GroupDetails/GroupDetails"; | ||||
| import { BehaviorList } from "@Panel/BehaviorList/BehaviorList"; | ||||
| import { BehaviorDetails } from "@Panel/BehaviorDetails/BehaviorDetails"; | ||||
| import { ClipPlayer } from "@Panel/ClipPlayer/ClipPlayer"; | ||||
| import { ClipRecorder } from "@Panel/ClipPlayer/ClipRecorder"; | ||||
| 
 | ||||
| interface IPanelInfo { | ||||
| 	nameKey: string; | ||||
| @ -31,6 +33,7 @@ type PanelId = "" | ||||
| | "GroupDetails" // 群属性
 | ||||
| | "BehaviorList" // 行为列表
 | ||||
| | "BehaviorDetails" // 行为属性
 | ||||
| | "ClipPlayer" // 剪辑影片
 | ||||
| ; | ||||
| 
 | ||||
| const PanelInfoMap = new Map<PanelId, IPanelInfo>(); | ||||
| @ -66,6 +69,10 @@ PanelInfoMap.set("BehaviorDetails", { | ||||
|     nameKey: "Panel.Title.Behavior.Details.View", introKay: "Panel.Info.Behavior.Details.View", | ||||
|     class: BehaviorDetails | ||||
| }); | ||||
| PanelInfoMap.set("ClipPlayer", { | ||||
|     nameKey: "Panel.Title.Behavior.Clip.Player", introKay: "Panel.Info.Behavior.Clip.Player", | ||||
|     class: ClipPlayer, header: ClipRecorder, hidePadding: true | ||||
| }); | ||||
| 
 | ||||
| function getPanelById(panelId: PanelId): ReactNode { | ||||
| 	switch (panelId) { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user