Add behavior model & search box & setting add behavior popup #26

Merged
MrKBear merged 8 commits from dev-mrkbear into master 2022-03-26 19:00:52 +08:00
19 changed files with 883 additions and 68 deletions

View File

@ -0,0 +1,8 @@
import { BehaviorRecorder, IAnyBehaviorRecorder } from "@Model/Behavior";
import { Template } from "./Template";
const AllBehaviors: IAnyBehaviorRecorder[] = [
new BehaviorRecorder(Template)
]
export { AllBehaviors };

View File

@ -0,0 +1,20 @@
import { Behavior } from "@Model/Behavior";
type ITemplateBehaviorParameter = {
}
type ITemplateBehaviorEvent = {}
class Template extends Behavior<ITemplateBehaviorParameter, ITemplateBehaviorEvent> {
public override behaviorId: string = "Template";
public override behaviorName: string = "Behavior.Template.Title";
public override iconName: string = "Running";
public override describe: string = "Behavior.Template.Intro";
}
export { Template };

View File

@ -0,0 +1,11 @@
@import "../Theme/Theme.scss";
div.behavior-popup {
width: 100%;
height: 100%;
}
div.behavior-popup-search-box {
padding: 10px 0 0 10px;
width: calc(100% - 10px);
}

View File

@ -0,0 +1,65 @@
import { Component, ReactNode } from "react";
import { Popup } from "@Context/Popups";
import { Localization } from "@Component/Localization/Localization";
import { SearchBox } from "@Component/SearchBox/SearchBox";
import { ConfirmContent } from "@Component/ConfirmPopup/ConfirmPopup";
import "./BehaviorPopup.scss";
interface IBehaviorPopupProps {
}
interface IBehaviorPopupState {
searchValue: string;
}
class BehaviorPopup extends Popup<IBehaviorPopupProps> {
public minWidth: number = 400;
public minHeight: number = 300;
public width: number = 600;
public height: number = 450;
public onRenderHeader(): ReactNode {
return <Localization i18nKey="Popup.Add.Behavior.Title"/>
}
public render(): ReactNode {
return <BehaviorPopupComponent {...this.props}/>
}
}
class BehaviorPopupComponent extends Component<IBehaviorPopupProps, IBehaviorPopupState> {
state: Readonly<IBehaviorPopupState> = {
searchValue: ""
};
private renderHeader = () => {
return <div className="behavior-popup-search-box">
<SearchBox
valueChange={(value) => {
this.setState({
searchValue: value
});
}}
value={this.state.searchValue}
/>
</div>;
}
public render(): ReactNode {
return <ConfirmContent
className="behavior-popup"
actions={[{
i18nKey: "Popup.Add.Behavior.Action.Add"
}]}
header={this.renderHeader}
headerHeight={36}
>
</ConfirmContent>
}
}
export { BehaviorPopup };

View File

@ -4,7 +4,8 @@ import { LocalizationTooltipHost } from "../Localization/LocalizationTooltipHost
import { useSetting, IMixinSettingProps } from "@Context/Setting"; import { useSetting, IMixinSettingProps } from "@Context/Setting";
import { useStatusWithEvent, IMixinStatusProps } from "@Context/Status"; import { useStatusWithEvent, IMixinStatusProps } from "@Context/Status";
import { AllI18nKeys } from "../Localization/Localization"; import { AllI18nKeys } from "../Localization/Localization";
import { ConfirmPopup } from "@Component/ConfirmPopup/ConfirmPopup"; import { SettingPopup } from "../SettingPopup/SettingPopup";
import { BehaviorPopup } from "../BehaviorPopup/BehaviorPopup";
import { Component, ReactNode } from "react"; import { Component, ReactNode } from "react";
import { MouseMod } from "@GLRender/ClassicRenderer"; import { MouseMod } from "@GLRender/ClassicRenderer";
import "./CommandBar.scss"; import "./CommandBar.scss";
@ -52,13 +53,19 @@ class CommandBar extends Component<ICommandBarProps & IMixinSettingProps & IMixi
} }
})} })}
{this.getRenderButton({ {this.getRenderButton({
iconName: "CubeShape", iconName: "ProductVariant",
i18NKey: "Command.Bar.Add.Range.Info", i18NKey: "Command.Bar.Add.Range.Info",
click: () => { click: () => {
this.props.status ? this.props.status.newRange() : undefined; this.props.status ? this.props.status.newRange() : undefined;
} }
})} })}
{this.getRenderButton({ iconName: "StepSharedAdd", i18NKey: "Command.Bar.Add.Behavior.Info" })} {this.getRenderButton({
iconName: "Running",
i18NKey: "Command.Bar.Add.Behavior.Info",
click: () => {
this.props.status?.popup.showPopup(BehaviorPopup, {});
}
})}
{this.getRenderButton({ {this.getRenderButton({
iconName: "Tag", iconName: "Tag",
i18NKey: "Command.Bar.Add.Tag.Info", i18NKey: "Command.Bar.Add.Tag.Info",
@ -73,7 +80,7 @@ class CommandBar extends Component<ICommandBarProps & IMixinSettingProps & IMixi
iconName: "Settings", iconName: "Settings",
i18NKey: "Command.Bar.Setting.Info", i18NKey: "Command.Bar.Setting.Info",
click: () => { click: () => {
// this.props.status?.popup.showPopup(ConfirmPopup, {}); this.props.status?.popup.showPopup(SettingPopup, {});
} }
})} })}
</div> </div>

View File

@ -4,10 +4,35 @@ div.confirm-root {
width: 100%; width: 100%;
height: 100%; height: 100%;
div.header-view {
width: 100%;
}
div.content-views { div.content-views {
width: 100%; width: 100%;
height: calc( 100% - 36px );
box-sizing: border-box; box-sizing: border-box;
overflow: scroll;
-ms-overflow-style: none;
flex-shrink: 1;
}
div.content-views::-webkit-scrollbar {
width : 8px; /*高宽分别对应横竖滚动条的尺寸*/
height: 0;
}
div.content-views::-webkit-scrollbar-thumb {
/*滚动条里面小方块*/
border-radius: 8px;
}
div.content-views::-webkit-scrollbar-track {
/*滚动条里面轨道*/
border-radius: 8px;
background-color: rgba($color: #000000, $alpha: 0);
}
div.content-views.has-padding {
padding: 10px; padding: 10px;
} }
@ -35,14 +60,27 @@ div.confirm-root {
div.action-button.red { div.action-button.red {
color: $lt-red; color: $lt-red;
} }
div.action-button.blue {
color: $lt-blue;
}
div.action-button.disable {
opacity: .75;
cursor: not-allowed;
}
} }
} }
div.dark.confirm-root { div.dark.confirm-root {
div.content-views::-webkit-scrollbar-thumb {
background-color: $lt-bg-color-lvl1-dark;
}
div.action-view { div.action-view {
div.action-button { div.action-button, div.action-button.disable:hover {
background-color: $lt-bg-color-lvl3-dark; background-color: $lt-bg-color-lvl3-dark;
} }
@ -54,9 +92,13 @@ div.dark.confirm-root {
div.light.confirm-root { div.light.confirm-root {
div.content-views::-webkit-scrollbar-thumb {
background-color: $lt-bg-color-lvl1-light;
}
div.action-view { div.action-view {
div.action-button { div.action-button, div.action-button.disable:hover {
background-color: $lt-bg-color-lvl3-light; background-color: $lt-bg-color-lvl3-light;
} }

View File

@ -1,5 +1,5 @@
import { Popup } from "@Context/Popups"; import { Popup } from "@Context/Popups";
import { ReactNode } from "react"; import { Component, ReactNode } from "react";
import { Message } from "@Component/Message/Message"; import { Message } from "@Component/Message/Message";
import { Theme } from "@Component/Theme/Theme"; import { Theme } from "@Component/Theme/Theme";
import { AllI18nKeys, Localization } from "@Component/Localization/Localization"; import { AllI18nKeys, Localization } from "@Component/Localization/Localization";
@ -7,12 +7,12 @@ import "./ConfirmPopup.scss";
interface IConfirmPopupProps { interface IConfirmPopupProps {
titleI18N?: AllI18nKeys; titleI18N?: AllI18nKeys;
infoI18n: AllI18nKeys; infoI18n?: AllI18nKeys;
yesI18n?: AllI18nKeys; yesI18n?: AllI18nKeys;
noI18n?: AllI18nKeys; noI18n?: AllI18nKeys;
red?: "yes" | "no";
yes?: () => any; yes?: () => any;
no?: () => any; no?: () => any;
red?: "yes" | "no";
} }
class ConfirmPopup extends Popup<IConfirmPopupProps> { class ConfirmPopup extends Popup<IConfirmPopupProps> {
@ -24,37 +24,139 @@ class ConfirmPopup extends Popup<IConfirmPopupProps> {
return <Localization i18nKey={this.props.titleI18N ?? "Popup.Title.Confirm"}/> return <Localization i18nKey={this.props.titleI18N ?? "Popup.Title.Confirm"}/>
} }
private genActionClickFunction(fn?: () => any): () => any {
return () => {
if (fn) fn();
this.close();
};
}
public render(): ReactNode { public render(): ReactNode {
const yesClassList: string[] = ["action-button", "yes-button"]; const actionList: IActionButtonProps[] = [];
const noClassList: string[] = ["action-button", "no-button"];
if (this.props.red === "no") { if (this.props.yesI18n || this.props.yes) {
noClassList.push("red"); actionList.push({
i18nKey: this.props.yesI18n ?? "Popup.Action.Yes",
onClick: this.genActionClickFunction(this.props.yes),
color: this.props.red === "yes" ? "red" : undefined
});
} }
if (this.props.red === "yes") {
yesClassList.push("red"); if (this.props.noI18n || this.props.no) {
actionList.push({
i18nKey: this.props.noI18n ?? "Popup.Action.Yes",
onClick: this.genActionClickFunction(this.props.no),
color: this.props.red === "no" ? "red" : undefined
});
}
return <ConfirmContent
actions={actionList}
>
{this.props.infoI18n ? <Message i18nKey={this.props.infoI18n}/> : null}
</ConfirmContent>
}
}
interface IConfirmContentProps {
hidePadding?: boolean;
className?: string;
actions: IActionButtonProps[];
header?: () => ReactNode;
headerHeight?: number;
}
interface IActionButtonProps {
className?: string;
disable?: boolean;
color?: "red" | "blue";
i18nKey: AllI18nKeys;
i18nOption?: Record<string, string>;
onClick?: () => void;
}
class ConfirmContent extends Component<IConfirmContentProps> {
public renderActionButton(props: IActionButtonProps, key: number): ReactNode {
const classList = ["action-button"];
if (props.className) {
classList.push(props.className);
}
if (props.color === "red") {
classList.push("red");
}
if (props.color === "blue") {
classList.push("blue");
}
if (props.disable) {
classList.push("disable");
}
return <div
className={classList.join(" ")}
onClick={props.disable ? undefined : props.onClick}
key={key}
>
<Localization i18nKey={props.i18nKey} options={props.i18nOption}/>
</div>
}
private getHeaderHeight(): number {
return this.props.headerHeight ?? 0;
}
private renderHeader() {
return <div
className="header-view"
style={{
maxHeight: this.getHeaderHeight(),
minHeight: this.getHeaderHeight(),
height: this.getHeaderHeight()
}}
>
{this.props.header ? this.props.header() : null}
</div>
}
public render(): ReactNode {
const contentClassNameList: string[] = ["content-views"];
if (this.props.className) {
contentClassNameList.push(this.props.className);
}
if (!this.props.hidePadding) {
contentClassNameList.push("has-padding");
} }
return <Theme className="confirm-root"> return <Theme className="confirm-root">
<div className="content-views">
<Message i18nKey={this.props.infoI18n}/> {this.props.header ? this.renderHeader() : null}
<div
className={contentClassNameList.join(" ")}
style={{
height: `calc( 100% - ${this.getHeaderHeight() + 36}px )`
}}
>
{this.props.children}
</div> </div>
<div className="action-view"> <div className="action-view">
<div className={yesClassList.join(" ")} onClick={() => { {
this.props.yes ? this.props.yes() : null; this.props.actions.map((prop, index) => {
this.close(); return this.renderActionButton(prop, index);
}}> })
<Localization i18nKey={this.props.yesI18n ?? "Popup.Action.Yes"}/> }
</div>
<div className={noClassList.join(" ")} onClick={() => {
this.props.no ? this.props.no() : null;
this.close();
}}>
<Localization i18nKey={this.props.noI18n ?? "Popup.Action.No"}/>
</div>
</div> </div>
</Theme>; </Theme>;
} }
} }
export { ConfirmPopup } export { ConfirmPopup, ConfirmContent }

View File

@ -0,0 +1,95 @@
@import "../Theme/Theme.scss";
$search-box-height: 26px;
div.search-box-root {
min-height: $search-box-height;
max-width: 280px;
width: 100%;
border-radius: 3px;
display: flex;
cursor: pointer;
overflow: hidden;
div.search-icon {
min-width: $search-box-height;
height: $search-box-height;
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
user-select: none;
}
div.input-box {
width: calc(100% - 26px);
height: $search-box-height;
input {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
border: 0;
outline: none;
background-color: transparent;
vertical-align: middle;
}
}
div.clean-box {
height: $search-box-height;
width: 0;
display: flex;
align-items: center;
div.clean-box-view {
flex-shrink: 0;
height: 24px;
width: 24px;
display: flex;
justify-content: center;
align-items: center;
position: relative;
right: 24px;
border-radius: 3px;
user-select: none;
}
}
}
div.dark.search-box-root {
div.clean-box {
div.clean-box-view:hover {
background-color: $lt-bg-color-lvl2-dark;
}
div.clean-box-view {
background-color: $lt-bg-color-lvl3-dark;
}
}
div.input-box input {
color: $lt-font-color-normal-dark;
}
}
div.light.search-box-root {
div.clean-box {
div.clean-box-view:hover {
background-color: $lt-bg-color-lvl3-light;
}
div.clean-box-view {
background-color: $lt-bg-color-lvl2-light;
}
}
div.input-box input {
color: $lt-font-color-normal-light;
}
}

View File

@ -0,0 +1,60 @@
import { AllI18nKeys, I18N } from "@Component/Localization/Localization";
import { BackgroundLevel, FontLevel, Theme } from "@Component/Theme/Theme";
import { useSettingWithEvent, IMixinSettingProps } from "@Context/Setting";
import { Icon } from "@fluentui/react";
import { Component, ReactNode } from "react";
import "./SearchBox.scss";
interface ISearchBoxProps {
value?: string;
valueChange?: (value: string) => void;
placeholderI18N?: AllI18nKeys;
className?: string;
}
@useSettingWithEvent("language")
class SearchBox extends Component<ISearchBoxProps & IMixinSettingProps> {
private renderCleanBox() {
return <div className="clean-box">
<div
className="clean-box-view"
onClick={() => {
if (this.props.valueChange) {
this.props.valueChange("")
}
}}
>
<Icon iconName="CalculatorMultiply"/>
</div>
</div>;
}
public render(): ReactNode {
return <Theme
className={"search-box-root" + (this.props.className ? ` ${this.props.className}` : "")}
backgroundLevel={BackgroundLevel.Level3}
fontLevel={FontLevel.normal}
>
<div className="search-icon">
<Icon iconName="search"/>
</div>
<div className="input-box">
<input
value={this.props.value}
placeholder={
I18N(this.props, this.props.placeholderI18N ?? "Common.Search.Placeholder")
}
onInput={(e) => {
if (e.target instanceof HTMLInputElement && this.props.valueChange) {
this.props.valueChange(e.target.value)
}
}}
/>
</div>
{this.props.value ? this.renderCleanBox() : null}
</Theme>
}
}
export { SearchBox };

View File

@ -0,0 +1,6 @@
@import "../Theme/Theme.scss";
div.setting-popup {
width: 100%;
height: 100%;
}

View File

@ -0,0 +1,34 @@
import { Component, ReactNode } from "react";
import { Popup } from "@Context/Popups";
import { Theme } from "@Component/Theme/Theme";
import { Localization } from "@Component/Localization/Localization";
import "./SettingPopup.scss";
interface ISettingPopupProps {
}
class SettingPopup extends Popup<ISettingPopupProps> {
public minWidth: number = 400;
public minHeight: number = 300;
public width: number = 600;
public height: number = 450;
public onRenderHeader(): ReactNode {
return <Localization i18nKey="Popup.Setting.Title"/>
}
public render(): ReactNode {
return <SettingPopupComponent {...this.props}/>
}
}
class SettingPopupComponent extends Component<ISettingPopupProps> {
public render(): ReactNode {
return <Theme className="setting-popup"></Theme>
}
}
export { SettingPopup };

View File

@ -83,13 +83,13 @@ class Status extends Emitter<IStatusEvent> {
*/ */
public focusLabel?: Label; public focusLabel?: Label;
private drawtimer?: NodeJS.Timeout; private drawTimer?: NodeJS.Timeout;
private delayDraw = () => { private delayDraw = () => {
this.drawtimer ? clearTimeout(this.drawtimer) : null; this.drawTimer ? clearTimeout(this.drawTimer) : null;
this.drawtimer = setTimeout(() => { this.drawTimer = setTimeout(() => {
this.model.draw(); this.model.draw();
this.drawtimer = undefined; this.drawTimer = undefined;
}); });
} }

View File

@ -51,8 +51,14 @@ const EN_US = {
"Popup.Action.Objects.Confirm.Title": "Confirm Delete", "Popup.Action.Objects.Confirm.Title": "Confirm Delete",
"Popup.Action.Objects.Confirm.Delete": "Delete", "Popup.Action.Objects.Confirm.Delete": "Delete",
"Popup.Delete.Objects.Confirm": "Are you sure you want to delete this object(s)? The object is deleted and cannot be recalled.", "Popup.Delete.Objects.Confirm": "Are you sure you want to delete this object(s)? The object is deleted and cannot be recalled.",
"Popup.Setting.Title": "Preferences setting",
"Popup.Add.Behavior.Title": "Add behavior",
"Popup.Add.Behavior.Action.Add": "Add all select behavior",
"Build.In.Label.Name.All.Group": "All group", "Build.In.Label.Name.All.Group": "All group",
"Build.In.Label.Name.All.Range": "All range", "Build.In.Label.Name.All.Range": "All range",
"Behavior.Template.Title": "Behavior",
"Behavior.Template.Intro": "This is a template behavior",
"Common.Search.Placeholder": "Search in here...",
"Common.No.Data": "No Data", "Common.No.Data": "No Data",
"Common.No.Unknown.Error": "Unknown error", "Common.No.Unknown.Error": "Unknown error",
"Common.Attr.Title.Basic": "Basic properties", "Common.Attr.Title.Basic": "Basic properties",

View File

@ -51,8 +51,14 @@ const ZH_CN = {
"Popup.Action.Objects.Confirm.Title": "删除确认", "Popup.Action.Objects.Confirm.Title": "删除确认",
"Popup.Action.Objects.Confirm.Delete": "删除", "Popup.Action.Objects.Confirm.Delete": "删除",
"Popup.Delete.Objects.Confirm": "你确定要删除这个(些)对象吗?对象被删除将无法撤回。", "Popup.Delete.Objects.Confirm": "你确定要删除这个(些)对象吗?对象被删除将无法撤回。",
"Popup.Setting.Title": "首选项设置",
"Popup.Add.Behavior.Title": "添加行为",
"Popup.Add.Behavior.Action.Add": "添加全部选中行为",
"Build.In.Label.Name.All.Group": "全部群", "Build.In.Label.Name.All.Group": "全部群",
"Build.In.Label.Name.All.Range": "全部范围", "Build.In.Label.Name.All.Range": "全部范围",
"Behavior.Template.Title": "行为",
"Behavior.Template.Intro": "这是一个模板行为",
"Common.Search.Placeholder": "在此处搜索...",
"Common.No.Data": "暂无数据", "Common.No.Data": "暂无数据",
"Common.No.Unknown.Error": "未知错误", "Common.No.Unknown.Error": "未知错误",
"Common.Attr.Title.Basic": "基础属性", "Common.Attr.Title.Basic": "基础属性",

View File

@ -3,40 +3,330 @@ import { Emitter, EventType } from "./Emitter";
import type { Individual } from "./Individual"; import type { Individual } from "./Individual";
import type { Group } from "./Group"; import type { Group } from "./Group";
import type { Model } from "./Model"; import type { Model } from "./Model";
import type { Range } from "./Range";
import type { Label } from "./Label";
/** /**
* *
*/ */
abstract class Behavior< type IMapBasicParamTypeKeyToType = {
P extends IAnyObject = {}, "number": number;
"string": string;
"boolean": boolean;
}
type IMapObjectParamTypeKeyToType = {
"R"?: Range;
"G"?: Group;
"GR"?: Group | Range;
"LR"?: Label | Range;
"LG"?: Label | Group;
"LGR"?: Label | Group | Range;
}
type IMapVectorParamTypeKeyToType = {
"vec": number[];
}
/**
*
*/
type AllMapType = IMapBasicParamTypeKeyToType & IMapObjectParamTypeKeyToType & IMapVectorParamTypeKeyToType;
type IParamType = keyof AllMapType;
type IObjectType = keyof IMapObjectParamTypeKeyToType;
type IVectorType = keyof IMapVectorParamTypeKeyToType;
type IParamValue<K extends IParamType> = AllMapType[K];
/**
*
*/
const objectTypeListEnumSet = new Set<IParamType>(["R", "G", "GR", "LR", "LG", "LGR"]);
/**
*
*/
function isObjectType(key: IParamType): key is IVectorType {
return objectTypeListEnumSet.has(key);
}
/**
*
*/
function isVectorType(key: IParamType): key is IObjectType {
return key === "vec";
}
/**
*
*/
interface IBehaviorParameterOptionItem<T extends IParamType = IParamType> {
/**
*
*/
type: T;
/**
*
*/
defaultValue?: IParamValue<T>;
/**
*
*/
onChange?: (value: IParamValue<T>) => any;
/**
*
*/
name: string;
/**
*
*/
stringLength?: number;
/**
*
*/
numberStep?: number;
/**
*
*/
numberMax?: number;
numberMin?: number;
/**
*
*/
iconName?: string;
}
interface IBehaviorParameter {
[x: string]: IParamType;
}
/**
*
*/
type IBehaviorParameterOption<P extends IBehaviorParameter> = {
[X in keyof P]: IBehaviorParameterOptionItem<P[X]>;
}
/**
*
*/
type IBehaviorParameterValue<P extends IBehaviorParameter> = {
[X in keyof P]: IParamValue<P[X]>
}
/**
*
*/
type IBehaviorConstructor<
P extends IBehaviorParameter = {},
E extends Record<EventType, any> = {} E extends Record<EventType, any> = {}
> extends Emitter<E> { > = new (id: string, parameter: IBehaviorParameterValue<P>) => Behavior<P, E>;
type IAnyBehavior = Behavior<any, any>;
type IAnyBehaviorRecorder = BehaviorRecorder<any, any>;
/**
*
*/
class BehaviorInfo<E extends Record<EventType, any> = {}> extends Emitter<E> {
/**
*
*/
public iconName: string = ""
/** /**
* ID * ID
*/ */
abstract id: string; public behaviorId: string = "";
/** /**
* *
*/ */
abstract name: string; public behaviorName: string = "";
/** /**
* *
*/ */
public describe?: string = ""; public describe?: string = "";
}
class BehaviorRecorder<
P extends IBehaviorParameter = {},
E extends Record<EventType, any> = {}
> extends BehaviorInfo<{}> {
/**
*
*/
public nameIndex: number = 0;
/**
* ID
*/
public getNextId() {
return `B-${this.behaviorName}-${this.nameIndex ++}`;
}
/**
*
*/
public behavior: IBehaviorConstructor<P, E>;
/**
*
*/
public behaviorInstance: Behavior<P, E>;
/**
*
*/
public parameterOption: IBehaviorParameterOption<P>;
/**
*
*/
public getDefaultValue(): IBehaviorParameterValue<P> {
let defaultObj = {} as IBehaviorParameterValue<P>;
for (let key in this.parameterOption) {
let defaultVal = this.parameterOption[key].defaultValue;
defaultObj[key] = defaultVal as any;
if (defaultObj[key] === undefined) {
switch (this.parameterOption[key].type) {
case "string":
defaultObj[key] = "" as any;
break;
case "number":
defaultObj[key] = 0 as any;
break;
case "boolean":
defaultObj[key] = false as any;
break;
case "vec":
defaultObj[key] = [0, 0, 0] as any;
break;
}
}
}
return defaultObj;
}
/**
*
*/
public new(): Behavior<P, E> {
return new this.behavior(this.getNextId(), this.getDefaultValue());
}
public constructor(behavior: IBehaviorConstructor<P, E>) {
super();
this.behavior = behavior;
this.behaviorInstance = new this.behavior(this.getNextId(), {} as any);
this.parameterOption = this.behaviorInstance.parameterOption;
this.iconName = this.behaviorInstance.iconName;
this.behaviorId = this.behaviorInstance.behaviorId;
this.behaviorName = this.behaviorInstance.behaviorName;
this.describe = this.behaviorInstance.describe;
}
}
/**
*
*/
class Behavior<
P extends IBehaviorParameter = {},
E extends Record<EventType, any> = {}
> extends BehaviorInfo<E> {
/**
*
*/
public name: string = "";
/**
* ID
*/
public id: string = "";
/** /**
* *
* *
*/ */
public priority?: number = 0; public priority: number = 0;
/** /**
* *
*/ */
abstract parameter?: P; public parameter: IBehaviorParameterValue<P>;
/**
*
*/
public parameterOption: IBehaviorParameterOption<P> = {} as any;
public constructor(id: string, parameter: IBehaviorParameterValue<P>) {
super();
this.id = id;
this.parameter = parameter;
}
/**
*
*/
public equal(behavior: Behavior<any, any>): boolean {
return this === behavior || this.id === behavior.id;
};
/**
*
*/
private deleteFlag: boolean = false;
/**
*
*/
public markDelete() {
this.deleteFlag = true;
};
/**
*
*/
public isDeleted(): boolean {
return this.deleteFlag;
}
/**
*
*/
public load(model: Model): void {}
/**
*
*/
public unload(model: Model): void {}
/**
*
*/
public mount(group: Group, model: Model): void {}
/**
*
*/
public unmount(group: Group, model: Model): void {}
/** /**
* *
@ -67,5 +357,8 @@ abstract class Behavior<
} }
export { Behavior }; export {
Behavior, BehaviorRecorder, IBehaviorParameterOption, IBehaviorParameterOptionItem,
IAnyBehavior, IAnyBehaviorRecorder
};
export default { Behavior }; export default { Behavior };

View File

@ -60,22 +60,36 @@ class CtrlObject extends LabelObject {
return this === obj || this.id === obj.id; return this === obj || this.id === obj.id;
} }
/**
*
*/
public markDelete() {
this.deleteFlag = true;
};
/** /**
* *
*/ */
private deleteFlag: boolean = false; private deleteFlag: boolean = false;
/**
*
*/
public testDelete() {
for (let i = 0; i < this.model.objectPool.length; i++) {
if (this.model.objectPool[i].equal(this)) {
this.deleteFlag = false;
return;
}
}
this.deleteFlag = true;
}
/** /**
* *
*/ */
public isDeleted(): boolean { public isDeleted(): boolean {
if (this.deleteFlag) return true; return this.deleteFlag;
for (let i = 0; i < this.model.objectPool.length; i++) {
if (this.model.objectPool[i].equal(this)) return false;
}
this.deleteFlag = true;
return true;
} }
} }

View File

@ -22,7 +22,7 @@ class Group extends CtrlObject {
/** /**
* *
*/ */
public genMethod: GenMod = GenMod.Point; public genMethod: GenMod = GenMod.Range;
/** /**
* *

View File

@ -1,4 +1,3 @@
import { Individual } from "./Individual"; import { Individual } from "./Individual";
import { Group } from "./Group"; import { Group } from "./Group";
import { Range } from "./Range"; import { Range } from "./Range";
@ -6,18 +5,14 @@ import { Emitter, EventType, EventMixin } from "./Emitter";
import { CtrlObject } from "./CtrlObject"; import { CtrlObject } from "./CtrlObject";
import { ObjectID, AbstractRenderer } from "./Renderer"; import { ObjectID, AbstractRenderer } from "./Renderer";
import { Label } from "./Label"; import { Label } from "./Label";
import { Behavior, IAnyBehavior, IAnyBehaviorRecorder } from "./Behavior";
type ModelEvent = { type ModelEvent = {
loop: number; loop: number;
groupAdd: Group;
rangeAdd: Range;
labelAdd: Label;
labelDelete: Label;
labelChange: Label[]; labelChange: Label[];
objectAdd: CtrlObject;
objectDelete: CtrlObject[];
objectChange: CtrlObject[]; objectChange: CtrlObject[];
individualChange: Group; individualChange: Group;
behaviorChange: IAnyBehavior;
}; };
/** /**
@ -68,7 +63,6 @@ class Model extends Emitter<ModelEvent> {
console.log(`Model: Creat label with id ${this.idIndex}`); console.log(`Model: Creat label with id ${this.idIndex}`);
let label = new Label(this, this.nextId("L"), name); let label = new Label(this, this.nextId("L"), name);
this.labelPool.push(label); this.labelPool.push(label);
this.emit("labelAdd", label);
this.emit("labelChange", this.labelPool); this.emit("labelChange", this.labelPool);
return label; return label;
} }
@ -97,7 +91,6 @@ class Model extends Emitter<ModelEvent> {
this.labelPool.splice(index, 1); this.labelPool.splice(index, 1);
deletedLabel.testDelete(); deletedLabel.testDelete();
console.log(`Model: Delete label ${deletedLabel.name ?? deletedLabel.id}`); console.log(`Model: Delete label ${deletedLabel.name ?? deletedLabel.id}`);
this.emit("labelDelete", deletedLabel);
this.emit("labelChange", this.labelPool); this.emit("labelChange", this.labelPool);
} }
} }
@ -135,8 +128,6 @@ class Model extends Emitter<ModelEvent> {
console.log(`Model: Creat group with id ${this.idIndex}`); console.log(`Model: Creat group with id ${this.idIndex}`);
let group = new Group(this, this.nextId("G")); let group = new Group(this, this.nextId("G"));
this.objectPool.push(group); this.objectPool.push(group);
this.emit("groupAdd", group);
this.emit("objectAdd", group);
this.emit("objectChange", this.objectPool); this.emit("objectChange", this.objectPool);
return group; return group;
} }
@ -148,8 +139,6 @@ class Model extends Emitter<ModelEvent> {
console.log(`Model: Creat range with id ${this.idIndex}`); console.log(`Model: Creat range with id ${this.idIndex}`);
let range = new Range(this, this.nextId("R")); let range = new Range(this, this.nextId("R"));
this.objectPool.push(range); this.objectPool.push(range);
this.emit("rangeAdd", range);
this.emit("objectAdd", range);
this.emit("objectChange", this.objectPool); this.emit("objectChange", this.objectPool);
return range; return range;
} }
@ -176,6 +165,7 @@ class Model extends Emitter<ModelEvent> {
if (needDeleted) { if (needDeleted) {
deletedObject.push(currentObject); deletedObject.push(currentObject);
currentObject.markDelete();
return false; return false;
} else { } else {
return true; return true;
@ -184,12 +174,68 @@ class Model extends Emitter<ModelEvent> {
if (deletedObject.length) { if (deletedObject.length) {
console.log(`Model: Delete object ${deletedObject.map((object) => object.id).join(", ")}`); console.log(`Model: Delete object ${deletedObject.map((object) => object.id).join(", ")}`);
this.emit("objectDelete", deletedObject);
this.emit("objectChange", this.objectPool); this.emit("objectChange", this.objectPool);
} }
return deletedObject; return deletedObject;
} }
/**
*
*/
public behaviorPool: IAnyBehavior[] = [];
/**
*
*/
public addBehavior<B extends IAnyBehaviorRecorder>(recorder: B): B["behaviorInstance"] {
let behavior = recorder.new();
behavior.load(this);
this.behaviorPool.push(behavior);
console.log(`Model: Add ${behavior.behaviorName} behavior ${behavior.id}`);
this.emit("behaviorChange", behavior);
return behavior;
};
/**
* ID
*/
public getBehaviorById(id: ObjectID): IAnyBehavior | undefined {
for (let i = 0; i < this.behaviorPool.length; i++) {
if (this.behaviorPool[i].id.toString() === id.toString()) {
return this.behaviorPool[i];
}
}
}
/**
* Behavior
* @param name
*/
public deleteBehavior(name: IAnyBehavior | ObjectID) {
let deletedBehavior: IAnyBehavior | undefined;
let index = 0;
for (let i = 0; i < this.behaviorPool.length; i++) {
if (name instanceof Behavior) {
if (this.behaviorPool[i].equal(name)) {
deletedBehavior = this.behaviorPool[i];
index = i;
}
} else if (name === this.behaviorPool[i].id) {
deletedBehavior = this.behaviorPool[i];
index = i;
}
}
if (deletedBehavior) {
this.behaviorPool.splice(index, 1);
deletedBehavior.unload(this);
deletedBehavior.markDelete();
console.log(`Model: Delete behavior ${deletedBehavior.name ?? deletedBehavior.id}`);
this.emit("behaviorChange", deletedBehavior);
}
}
/** /**
* *
*/ */

View File

@ -52,7 +52,7 @@ class ObjectCommand extends Component<IMixinStatusProps> {
this.props.status ? this.props.status.newRange() : undefined; this.props.status ? this.props.status.newRange() : undefined;
}} }}
> >
<Icon iconName="CubeShape"></Icon> <Icon iconName="ProductVariant"></Icon>
</div> </div>
<div <div
className="command-item red" className="command-item red"