diff --git a/source/Behavior/Behavior.ts b/source/Behavior/Behavior.ts new file mode 100644 index 0000000..847bee2 --- /dev/null +++ b/source/Behavior/Behavior.ts @@ -0,0 +1,8 @@ +import { BehaviorRecorder, IAnyBehaviorRecorder } from "@Model/Behavior"; +import { Template } from "./Template"; + +const AllBehaviors: IAnyBehaviorRecorder[] = [ + new BehaviorRecorder(Template) +] + +export { AllBehaviors }; \ No newline at end of file diff --git a/source/Behavior/Template.ts b/source/Behavior/Template.ts new file mode 100644 index 0000000..ff3a17a --- /dev/null +++ b/source/Behavior/Template.ts @@ -0,0 +1,20 @@ +import { Behavior } from "@Model/Behavior"; + +type ITemplateBehaviorParameter = { + +} + +type ITemplateBehaviorEvent = {} + +class Template extends Behavior { + + 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 }; \ No newline at end of file diff --git a/source/Component/BehaviorPopup/BehaviorPopup.scss b/source/Component/BehaviorPopup/BehaviorPopup.scss new file mode 100644 index 0000000..290e2cf --- /dev/null +++ b/source/Component/BehaviorPopup/BehaviorPopup.scss @@ -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); +} \ No newline at end of file diff --git a/source/Component/BehaviorPopup/BehaviorPopup.tsx b/source/Component/BehaviorPopup/BehaviorPopup.tsx new file mode 100644 index 0000000..53c062f --- /dev/null +++ b/source/Component/BehaviorPopup/BehaviorPopup.tsx @@ -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 { + + public minWidth: number = 400; + public minHeight: number = 300; + public width: number = 600; + public height: number = 450; + + public onRenderHeader(): ReactNode { + return + } + + public render(): ReactNode { + return + } +} + +class BehaviorPopupComponent extends Component { + + state: Readonly = { + searchValue: "" + }; + + private renderHeader = () => { + return
+ { + this.setState({ + searchValue: value + }); + }} + value={this.state.searchValue} + /> +
; + } + + public render(): ReactNode { + return + + + } +} + +export { BehaviorPopup }; \ No newline at end of file diff --git a/source/Component/CommandBar/CommandBar.tsx b/source/Component/CommandBar/CommandBar.tsx index e6d38be..4ae4121 100644 --- a/source/Component/CommandBar/CommandBar.tsx +++ b/source/Component/CommandBar/CommandBar.tsx @@ -4,7 +4,8 @@ import { LocalizationTooltipHost } from "../Localization/LocalizationTooltipHost import { useSetting, IMixinSettingProps } from "@Context/Setting"; import { useStatusWithEvent, IMixinStatusProps } from "@Context/Status"; 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 { MouseMod } from "@GLRender/ClassicRenderer"; import "./CommandBar.scss"; @@ -52,13 +53,19 @@ class CommandBar extends Component { 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({ iconName: "Tag", i18NKey: "Command.Bar.Add.Tag.Info", @@ -73,7 +80,7 @@ class CommandBar extends Component { - // this.props.status?.popup.showPopup(ConfirmPopup, {}); + this.props.status?.popup.showPopup(SettingPopup, {}); } })} diff --git a/source/Component/ConfirmPopup/ConfirmPopup.scss b/source/Component/ConfirmPopup/ConfirmPopup.scss index 76e46ae..76c46d6 100644 --- a/source/Component/ConfirmPopup/ConfirmPopup.scss +++ b/source/Component/ConfirmPopup/ConfirmPopup.scss @@ -4,10 +4,35 @@ div.confirm-root { width: 100%; height: 100%; + div.header-view { + width: 100%; + } + div.content-views { width: 100%; - height: calc( 100% - 36px ); 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; } @@ -35,14 +60,27 @@ div.confirm-root { div.action-button.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.content-views::-webkit-scrollbar-thumb { + background-color: $lt-bg-color-lvl1-dark; + } + div.action-view { - div.action-button { + div.action-button, div.action-button.disable:hover { background-color: $lt-bg-color-lvl3-dark; } @@ -54,9 +92,13 @@ div.dark.confirm-root { div.light.confirm-root { + div.content-views::-webkit-scrollbar-thumb { + background-color: $lt-bg-color-lvl1-light; + } + div.action-view { - div.action-button { + div.action-button, div.action-button.disable:hover { background-color: $lt-bg-color-lvl3-light; } diff --git a/source/Component/ConfirmPopup/ConfirmPopup.tsx b/source/Component/ConfirmPopup/ConfirmPopup.tsx index b530ea8..a3c7aba 100644 --- a/source/Component/ConfirmPopup/ConfirmPopup.tsx +++ b/source/Component/ConfirmPopup/ConfirmPopup.tsx @@ -1,5 +1,5 @@ import { Popup } from "@Context/Popups"; -import { ReactNode } from "react"; +import { Component, ReactNode } from "react"; import { Message } from "@Component/Message/Message"; import { Theme } from "@Component/Theme/Theme"; import { AllI18nKeys, Localization } from "@Component/Localization/Localization"; @@ -7,12 +7,12 @@ import "./ConfirmPopup.scss"; interface IConfirmPopupProps { titleI18N?: AllI18nKeys; - infoI18n: AllI18nKeys; + infoI18n?: AllI18nKeys; yesI18n?: AllI18nKeys; noI18n?: AllI18nKeys; + red?: "yes" | "no"; yes?: () => any; no?: () => any; - red?: "yes" | "no"; } class ConfirmPopup extends Popup { @@ -24,37 +24,139 @@ class ConfirmPopup extends Popup { return } + private genActionClickFunction(fn?: () => any): () => any { + return () => { + if (fn) fn(); + this.close(); + }; + } + public render(): ReactNode { - const yesClassList: string[] = ["action-button", "yes-button"]; - const noClassList: string[] = ["action-button", "no-button"]; - if (this.props.red === "no") { - noClassList.push("red"); + const actionList: IActionButtonProps[] = []; + + if (this.props.yesI18n || this.props.yes) { + 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 + {this.props.infoI18n ? : null} + + } +} + +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; + onClick?: () => void; +} + +class ConfirmContent extends Component { + + 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
+ +
+ } + + private getHeaderHeight(): number { + return this.props.headerHeight ?? 0; + } + + private renderHeader() { + return
+ {this.props.header ? this.props.header() : null} +
+ } + + 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 -
- + + {this.props.header ? this.renderHeader() : null} + +
+ {this.props.children}
+
-
{ - this.props.yes ? this.props.yes() : null; - this.close(); - }}> - -
-
{ - this.props.no ? this.props.no() : null; - this.close(); - }}> - -
+ { + this.props.actions.map((prop, index) => { + return this.renderActionButton(prop, index); + }) + }
; } } -export { ConfirmPopup } \ No newline at end of file +export { ConfirmPopup, ConfirmContent } \ No newline at end of file diff --git a/source/Component/SearchBox/SearchBox.scss b/source/Component/SearchBox/SearchBox.scss new file mode 100644 index 0000000..a7636fd --- /dev/null +++ b/source/Component/SearchBox/SearchBox.scss @@ -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; + } +} \ No newline at end of file diff --git a/source/Component/SearchBox/SearchBox.tsx b/source/Component/SearchBox/SearchBox.tsx new file mode 100644 index 0000000..07c3ceb --- /dev/null +++ b/source/Component/SearchBox/SearchBox.tsx @@ -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 { + + private renderCleanBox() { + return
+
{ + if (this.props.valueChange) { + this.props.valueChange("") + } + }} + > + +
+
; + } + + public render(): ReactNode { + return +
+ +
+
+ { + if (e.target instanceof HTMLInputElement && this.props.valueChange) { + this.props.valueChange(e.target.value) + } + }} + /> +
+ {this.props.value ? this.renderCleanBox() : null} +
+ } +} + +export { SearchBox }; \ No newline at end of file diff --git a/source/Component/SettingPopup/SettingPopup.scss b/source/Component/SettingPopup/SettingPopup.scss new file mode 100644 index 0000000..f5165a4 --- /dev/null +++ b/source/Component/SettingPopup/SettingPopup.scss @@ -0,0 +1,6 @@ +@import "../Theme/Theme.scss"; + +div.setting-popup { + width: 100%; + height: 100%; +} \ No newline at end of file diff --git a/source/Component/SettingPopup/SettingPopup.tsx b/source/Component/SettingPopup/SettingPopup.tsx new file mode 100644 index 0000000..0730082 --- /dev/null +++ b/source/Component/SettingPopup/SettingPopup.tsx @@ -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 { + + public minWidth: number = 400; + public minHeight: number = 300; + public width: number = 600; + public height: number = 450; + + public onRenderHeader(): ReactNode { + return + } + + public render(): ReactNode { + return + } +} + +class SettingPopupComponent extends Component { + + public render(): ReactNode { + return + } +} + +export { SettingPopup }; \ No newline at end of file diff --git a/source/Context/Status.tsx b/source/Context/Status.tsx index b74e557..16f111f 100644 --- a/source/Context/Status.tsx +++ b/source/Context/Status.tsx @@ -83,13 +83,13 @@ class Status extends Emitter { */ public focusLabel?: Label; - private drawtimer?: NodeJS.Timeout; + private drawTimer?: NodeJS.Timeout; private delayDraw = () => { - this.drawtimer ? clearTimeout(this.drawtimer) : null; - this.drawtimer = setTimeout(() => { + this.drawTimer ? clearTimeout(this.drawTimer) : null; + this.drawTimer = setTimeout(() => { this.model.draw(); - this.drawtimer = undefined; + this.drawTimer = undefined; }); } diff --git a/source/Localization/EN-US.ts b/source/Localization/EN-US.ts index 8fbaa52..8db5a9d 100644 --- a/source/Localization/EN-US.ts +++ b/source/Localization/EN-US.ts @@ -51,8 +51,14 @@ const EN_US = { "Popup.Action.Objects.Confirm.Title": "Confirm 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.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.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.Unknown.Error": "Unknown error", "Common.Attr.Title.Basic": "Basic properties", diff --git a/source/Localization/ZH-CN.ts b/source/Localization/ZH-CN.ts index 3e2a883..69ff4ce 100644 --- a/source/Localization/ZH-CN.ts +++ b/source/Localization/ZH-CN.ts @@ -51,8 +51,14 @@ const ZH_CN = { "Popup.Action.Objects.Confirm.Title": "删除确认", "Popup.Action.Objects.Confirm.Delete": "删除", "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.Range": "全部范围", + "Behavior.Template.Title": "行为", + "Behavior.Template.Intro": "这是一个模板行为", + "Common.Search.Placeholder": "在此处搜索...", "Common.No.Data": "暂无数据", "Common.No.Unknown.Error": "未知错误", "Common.Attr.Title.Basic": "基础属性", diff --git a/source/Model/Behavior.ts b/source/Model/Behavior.ts index d86de8c..47e5179 100644 --- a/source/Model/Behavior.ts +++ b/source/Model/Behavior.ts @@ -3,40 +3,330 @@ import { Emitter, EventType } from "./Emitter"; import type { Individual } from "./Individual"; import type { Group } from "./Group"; import type { Model } from "./Model"; +import type { Range } from "./Range"; +import type { Label } from "./Label"; /** - * 群体的某种行为 + * 参数类型 */ -abstract class Behavior< - P extends IAnyObject = {}, +type IMapBasicParamTypeKeyToType = { + "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 = AllMapType[K]; + +/** + * 特殊对象类型判定 + */ +const objectTypeListEnumSet = new Set(["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 { + + /** + * 参数类型 + */ + type: T; + + /** + * 参数默认值 + */ + defaultValue?: IParamValue; + + /** + * 数值变化回调 + */ + onChange?: (value: IParamValue) => any; + + /** + * 名字 + */ + name: string; + + /** + * 字符长度 + */ + stringLength?: number; + + /** + * 数字步长 + */ + numberStep?: number; + + /** + * 最大值最小值 + */ + numberMax?: number; + numberMin?: number; + + /** + * 图标名字 + */ + iconName?: string; +} + +interface IBehaviorParameter { + [x: string]: IParamType; +} + +/** + * 参数类型列表 + */ +type IBehaviorParameterOption

= { + [X in keyof P]: IBehaviorParameterOptionItem; +} + +/** + * 参数类型列表映射到参数对象 + */ +type IBehaviorParameterValue

= { + [X in keyof P]: IParamValue +} + +/** + * 行为构造函数类型 + */ +type IBehaviorConstructor< + P extends IBehaviorParameter = {}, E extends Record = {} -> extends Emitter { +> = new (id: string, parameter: IBehaviorParameterValue

) => Behavior; + +type IAnyBehavior = Behavior; +type IAnyBehaviorRecorder = BehaviorRecorder; + +/** + * 行为的基础信息 + */ +class BehaviorInfo = {}> extends Emitter { + + /** + * 图标名字 + */ + public iconName: string = "" /** * 行为 ID */ - abstract id: string; - + public behaviorId: string = ""; + /** * 行为名称 */ - abstract name: string; + public behaviorName: string = ""; /** * 行为描述 */ public describe?: string = ""; +} + +class BehaviorRecorder< + P extends IBehaviorParameter = {}, + E extends Record = {} +> extends BehaviorInfo<{}> { + + /** + * 命名序号 + */ + public nameIndex: number = 0; + + /** + * 获取下一个 ID + */ + public getNextId() { + return `B-${this.behaviorName}-${this.nameIndex ++}`; + } + + /** + * 行为类型 + */ + public behavior: IBehaviorConstructor; + + /** + * 行为实例 + */ + public behaviorInstance: Behavior; + + /** + * 对象参数列表 + */ + public parameterOption: IBehaviorParameterOption

; + + /** + * 获取参数列表的默认值 + */ + public getDefaultValue(): IBehaviorParameterValue

{ + let defaultObj = {} as IBehaviorParameterValue

; + 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 { + return new this.behavior(this.getNextId(), this.getDefaultValue()); + } + + public constructor(behavior: IBehaviorConstructor) { + 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 = {} +> extends BehaviorInfo { + + /** + * 用户自定义名字 + */ + public name: string = ""; + + /** + * 实例 ID + */ + public id: string = ""; /** * 优先级 * 值越大执行顺序越靠后 */ - public priority?: number = 0; + public priority: number = 0; /** * 行为参数 */ - abstract parameter?: P; + public parameter: IBehaviorParameterValue

; + + /** + * 对象参数列表 + */ + public parameterOption: IBehaviorParameterOption

= {} as any; + + public constructor(id: string, parameter: IBehaviorParameterValue

) { + super(); + this.id = id; + this.parameter = parameter; + } + + /** + * 相等校验 + */ + public equal(behavior: Behavior): 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 }; \ No newline at end of file diff --git a/source/Model/CtrlObject.ts b/source/Model/CtrlObject.ts index 982d20d..a6e810e 100644 --- a/source/Model/CtrlObject.ts +++ b/source/Model/CtrlObject.ts @@ -60,22 +60,36 @@ class CtrlObject extends LabelObject { return this === obj || this.id === obj.id; } + /** + * 标记对象被删除 + */ + public markDelete() { + this.deleteFlag = true; + }; /** * 删除标记 */ private deleteFlag: boolean = false; - /** - * 是否被删除 - */ - public isDeleted(): boolean { - if (this.deleteFlag) return true; + /** + * 检测是否被删除 + */ + public testDelete() { for (let i = 0; i < this.model.objectPool.length; i++) { - if (this.model.objectPool[i].equal(this)) return false; + if (this.model.objectPool[i].equal(this)) { + this.deleteFlag = false; + return; + } } this.deleteFlag = true; - return true; + } + + /** + * 是否被删除 + */ + public isDeleted(): boolean { + return this.deleteFlag; } } diff --git a/source/Model/Group.ts b/source/Model/Group.ts index b536ae5..5e81e0f 100644 --- a/source/Model/Group.ts +++ b/source/Model/Group.ts @@ -22,7 +22,7 @@ class Group extends CtrlObject { /** * 个体生成方式 */ - public genMethod: GenMod = GenMod.Point; + public genMethod: GenMod = GenMod.Range; /** * 生成位置坐标 diff --git a/source/Model/Model.ts b/source/Model/Model.ts index 9e537db..a28f07b 100644 --- a/source/Model/Model.ts +++ b/source/Model/Model.ts @@ -1,4 +1,3 @@ - import { Individual } from "./Individual"; import { Group } from "./Group"; import { Range } from "./Range"; @@ -6,18 +5,14 @@ import { Emitter, EventType, EventMixin } from "./Emitter"; import { CtrlObject } from "./CtrlObject"; import { ObjectID, AbstractRenderer } from "./Renderer"; import { Label } from "./Label"; +import { Behavior, IAnyBehavior, IAnyBehaviorRecorder } from "./Behavior"; type ModelEvent = { loop: number; - groupAdd: Group; - rangeAdd: Range; - labelAdd: Label; - labelDelete: Label; labelChange: Label[]; - objectAdd: CtrlObject; - objectDelete: CtrlObject[]; objectChange: CtrlObject[]; individualChange: Group; + behaviorChange: IAnyBehavior; }; /** @@ -68,7 +63,6 @@ class Model extends Emitter { console.log(`Model: Creat label with id ${this.idIndex}`); let label = new Label(this, this.nextId("L"), name); this.labelPool.push(label); - this.emit("labelAdd", label); this.emit("labelChange", this.labelPool); return label; } @@ -97,7 +91,6 @@ class Model extends Emitter { this.labelPool.splice(index, 1); deletedLabel.testDelete(); console.log(`Model: Delete label ${deletedLabel.name ?? deletedLabel.id}`); - this.emit("labelDelete", deletedLabel); this.emit("labelChange", this.labelPool); } } @@ -135,8 +128,6 @@ class Model extends Emitter { console.log(`Model: Creat group with id ${this.idIndex}`); let group = new Group(this, this.nextId("G")); this.objectPool.push(group); - this.emit("groupAdd", group); - this.emit("objectAdd", group); this.emit("objectChange", this.objectPool); return group; } @@ -148,8 +139,6 @@ class Model extends Emitter { console.log(`Model: Creat range with id ${this.idIndex}`); let range = new Range(this, this.nextId("R")); this.objectPool.push(range); - this.emit("rangeAdd", range); - this.emit("objectAdd", range); this.emit("objectChange", this.objectPool); return range; } @@ -176,6 +165,7 @@ class Model extends Emitter { if (needDeleted) { deletedObject.push(currentObject); + currentObject.markDelete(); return false; } else { return true; @@ -184,12 +174,68 @@ class Model extends Emitter { if (deletedObject.length) { console.log(`Model: Delete object ${deletedObject.map((object) => object.id).join(", ")}`); - this.emit("objectDelete", deletedObject); this.emit("objectChange", this.objectPool); } return deletedObject; } + /** + * 行为池 + */ + public behaviorPool: IAnyBehavior[] = []; + + /** + * 添加一个行为 + */ + public addBehavior(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); + } + } + /** * 渲染器 */ diff --git a/source/Panel/ObjectList/ObjectCommand.tsx b/source/Panel/ObjectList/ObjectCommand.tsx index 0450399..e33ee70 100644 --- a/source/Panel/ObjectList/ObjectCommand.tsx +++ b/source/Panel/ObjectList/ObjectCommand.tsx @@ -52,7 +52,7 @@ class ObjectCommand extends Component { this.props.status ? this.props.status.newRange() : undefined; }} > - +