Merge pull request 'Add actuator & dynamics behavior & brownian behavior & boundary constraint behavior' (#30) from dev-mrkbear into master
All checks were successful
continuous-integration/drone/push Build is passing

Reviewed-on: http://git.mrkbear.com/MrKBear/living-together/pulls/30
This commit is contained in:
MrKBear 2022-03-30 19:27:12 +08:00
commit 0f7657ad83
15 changed files with 552 additions and 33 deletions

View File

@ -1,13 +1,14 @@
import { BehaviorRecorder, IAnyBehaviorRecorder } from "@Model/Behavior";
import { Template } from "./Template";
import { Dynamics } from "./Dynamics";
import { Brownian } from "./Brownian";
import { BoundaryConstraint } from "./BoundaryConstraint";
const AllBehaviors: IAnyBehaviorRecorder[] = new Array(4).fill(0).map((_, i) => {
let behavior = new BehaviorRecorder(Template);
behavior.behaviorId = behavior.behaviorId + i;
behavior.behaviorName = behavior.behaviorName + Math.random().toString(36).slice(-6);
behavior.category = "Category" + Math.floor(Math.random() * 3).toString();
return behavior;
});
const AllBehaviors: IAnyBehaviorRecorder[] = [
new BehaviorRecorder(Dynamics),
new BehaviorRecorder(Brownian),
new BehaviorRecorder(BoundaryConstraint)
]
/**
*

View File

@ -0,0 +1,74 @@
import { Behavior } from "@Model/Behavior";
import { Group } from "@Model/Group";
import { Individual } from "@Model/Individual";
import { Label } from "@Model/Label";
import { Model } from "@Model/Model";
import { Range } from "@Model/Range";
type IBoundaryConstraintBehaviorParameter = {
range: "LR"
}
type IBoundaryConstraintBehaviorEvent = {}
class BoundaryConstraint extends Behavior<IBoundaryConstraintBehaviorParameter, IBoundaryConstraintBehaviorEvent> {
public override behaviorId: string = "BoundaryConstraint";
public override behaviorName: string = "$Title";
public override iconName: string = "Running";
public override describe: string = "$Intro";
public override category: string = "$Physics";
public override parameterOption = {
range: {
type: "LR",
name: "$range",
defaultValue: undefined
}
};
public override terms: Record<string, Record<string, string>> = {
"$Title": {
"ZH_CN": "边界约束",
"EN_US": "Boundary constraint"
},
"$Intro": {
"ZH_CN": "个体越出边界后将主动返回",
"EN_US": "Individuals will return actively after crossing the border"
}
};
public effect(individual: Individual, group: Group, model: Model, t: number): void {
let rangeList: Range[] = [];
if (this.parameter.range instanceof Range) {
rangeList.push(this.parameter.range);
}
if (this.parameter.range instanceof Label) {
rangeList = model.getObjectByLabel(this.parameter.range).filter((obj) => {
return obj instanceof Range
}) as any;
}
for (let i = 0; i < rangeList.length; i++) {
let rx = rangeList[i].position[0] - individual.position[0];
let ry = rangeList[i].position[1] - individual.position[1];
let rz = rangeList[i].position[2] - individual.position[2];
let ox = Math.abs(rx) > rangeList[i].radius[0];
let oy = Math.abs(ry) > rangeList[i].radius[1];
let oz = Math.abs(rz) > rangeList[i].radius[2];
individual.applyForce(
ox ? rx : 0,
oy ? ry : 0,
oz ? rz : 0
)
}
}
}
export { BoundaryConstraint };

109
source/Behavior/Brownian.ts Normal file
View File

@ -0,0 +1,109 @@
import { Behavior } from "@Model/Behavior";
import { Group } from "@Model/Group";
import { Individual } from "@Model/Individual";
import { Model } from "@Model/Model";
type IBrownianBehaviorParameter = {
maxFrequency: "number",
minFrequency: "number",
maxStrength: "number",
minStrength: "number"
}
type IBrownianBehaviorEvent = {}
class Brownian extends Behavior<IBrownianBehaviorParameter, IBrownianBehaviorEvent> {
public override behaviorId: string = "Brownian";
public override behaviorName: string = "$Title";
public override iconName: string = "Running";
public override describe: string = "$Intro";
public override category: string = "$Physics";
public override parameterOption = {
maxFrequency: {
type: "number",
name: "$Max.Frequency",
defaultValue: 5,
numberStep: .1,
numberMin: 0
},
minFrequency: {
type: "number",
name: "$Min.Frequency",
defaultValue: 0,
numberStep: .1,
numberMin: 0
},
maxStrength: {
type: "number",
name: "$Max.Strength",
defaultValue: 10,
numberStep: .01,
numberMin: 0
},
minStrength: {
type: "number",
name: "$Min.Strength",
defaultValue: 0,
numberStep: .01,
numberMin: 0
}
};
public override terms: Record<string, Record<string, string>> = {
"$Title": {
"ZH_CN": "布朗运动",
"EN_US": "Brownian motion"
},
"$Intro": {
"ZH_CN": "一种无规则的随机运动",
"EN_US": "An irregular random motion"
},
"$Max.Frequency": {
"ZH_CN": "最大频率",
"EN_US": "Maximum frequency"
},
"$Min.Frequency": {
"ZH_CN": "最小频率",
"EN_US": "Minimum frequency"
},
"$Max.Strength": {
"ZH_CN": "最大强度",
"EN_US": "Maximum strength"
},
"$Min.Strength": {
"ZH_CN": "最小强度",
"EN_US": "Minimum strength"
}
};
public effect(individual: Individual, group: Group, model: Model, t: number): void {
const {maxFrequency, minFrequency, maxStrength, minStrength} = this.parameter;
let nextTime = individual.getData("Brownian.nextTime") ??
minFrequency + Math.random() * (maxFrequency - minFrequency);
let currentTime = individual.getData("Brownian.currentTime") ?? 0;
currentTime += t;
if (currentTime > nextTime) {
individual.applyForce(
minStrength + (Math.random() * 2 - 1) * (maxStrength - minStrength),
minStrength + (Math.random() * 2 - 1) * (maxStrength - minStrength),
minStrength + (Math.random() * 2 - 1) * (maxStrength - minStrength)
);
nextTime = minFrequency + Math.random() * (maxFrequency - minFrequency);
currentTime = 0;
}
individual.setData("Brownian.nextTime", nextTime);
individual.setData("Brownian.currentTime", currentTime);
}
}
export { Brownian };

140
source/Behavior/Dynamics.ts Normal file
View File

@ -0,0 +1,140 @@
import { Behavior } from "@Model/Behavior";
import Group from "@Model/Group";
import Individual from "@Model/Individual";
import { Model } from "@Model/Model";
type IDynamicsBehaviorParameter = {
mass: "number",
maxAcceleration: "number",
maxVelocity: "number",
resistance: "number"
}
type IDynamicsBehaviorEvent = {}
class Dynamics extends Behavior<IDynamicsBehaviorParameter, IDynamicsBehaviorEvent> {
public override behaviorId: string = "Dynamics";
public override behaviorName: string = "$Title";
public override iconName: string = "Running";
public override describe: string = "$Intro";
public override category: string = "$Physics";
public override parameterOption = {
mass: {
name: "$Mass",
type: "number",
defaultValue: 1,
numberStep: .01,
numberMin: .001
},
maxAcceleration: {
name: "$Max.Acceleration",
type: "number",
defaultValue: 5,
numberStep: .1,
numberMin: 0
},
maxVelocity: {
name: "$Max.Velocity",
type: "number",
defaultValue: 10,
numberStep: .1,
numberMin: 0
},
resistance: {
name: "$Resistance",
type: "number",
defaultValue: 0.1,
numberStep: .1,
numberMin: 0
}
};
public override terms: Record<string, Record<string, string>> = {
"$Title": {
"ZH_CN": "动力学",
"EN_US": "Dynamics"
},
"$Intro": {
"ZH_CN": "一切可以运动物体的必要行为,执行物理法则。",
"EN_US": "All necessary behaviors that can move objects and implement the laws of physics."
},
"$Mass": {
"ZH_CN": "质量 (Kg)",
"EN_US": "Mass (Kg)"
},
"$Max.Acceleration": {
"ZH_CN": "最大加速度 (m/s²)",
"EN_US": "Maximum acceleration (m/s²)"
},
"$Max.Velocity": {
"ZH_CN": "最大速度 (m/s)",
"EN_US": "Maximum velocity (m/s)"
},
"$Physics": {
"ZH_CN": "物理",
"EN_US": "Physics"
}
};
public override finalEffect(individual: Individual, group: Group, model: Model, t: number): void {
// 计算当前速度
const currentV = individual.vectorLength(individual.velocity);
// 计算阻力
const resistance = currentV * currentV * this.parameter.resistance;
// 应用阻力
if (currentV) {
individual.applyForce(
(- individual.velocity[0] / currentV) * resistance,
(- individual.velocity[1] / currentV) * resistance,
(- individual.velocity[2] / currentV) * resistance
);
}
// 计算加速度
individual.acceleration[0] = individual.force[0] / this.parameter.mass;
individual.acceleration[1] = individual.force[1] / this.parameter.mass;
individual.acceleration[2] = individual.force[2] / this.parameter.mass;
// 加速度约束
const lengthA = individual.vectorLength(individual.acceleration);
if (lengthA > this.parameter.maxAcceleration) {
individual.acceleration[0] = individual.acceleration[0] * this.parameter.maxAcceleration / lengthA;
individual.acceleration[1] = individual.acceleration[1] * this.parameter.maxAcceleration / lengthA;
individual.acceleration[2] = individual.acceleration[2] * this.parameter.maxAcceleration / lengthA;
}
// 计算速度
individual.velocity[0] = individual.velocity[0] + individual.acceleration[0] * t;
individual.velocity[1] = individual.velocity[1] + individual.acceleration[1] * t;
individual.velocity[2] = individual.velocity[2] + individual.acceleration[2] * t;
// 速度约束
const lengthV = individual.vectorLength(individual.velocity);
if (lengthV > this.parameter.maxVelocity) {
individual.velocity[0] = individual.velocity[0] * this.parameter.maxVelocity / lengthV;
individual.velocity[1] = individual.velocity[1] * this.parameter.maxVelocity / lengthV;
individual.velocity[2] = individual.velocity[2] * this.parameter.maxVelocity / lengthV;
}
// 应用速度
individual.position[0] = individual.position[0] + individual.velocity[0] * t;
individual.position[1] = individual.position[1] + individual.velocity[1] * t;
individual.position[2] = individual.position[2] + individual.velocity[2] * t;
// 清除受力
individual.force[0] = 0;
individual.force[1] = 0;
individual.force[2] = 0;
};
}
export { Dynamics };

View File

@ -1,4 +1,7 @@
import { Behavior } from "@Model/Behavior";
import { Group } from "@Model/Group";
import { Individual } from "@Model/Individual";
import { Model } from "@Model/Model";
type ITemplateBehaviorParameter = {
@ -16,7 +19,9 @@ class Template extends Behavior<ITemplateBehaviorParameter, ITemplateBehaviorEve
public override describe: string = "$Intro";
terms: Record<string, Record<string, string>> = {
public override category: string = "$Category";
public override terms: Record<string, Record<string, string>> = {
"$Title": {
"ZH_CN": "行为",
"EN_US": "Behavior"
@ -26,6 +31,10 @@ class Template extends Behavior<ITemplateBehaviorParameter, ITemplateBehaviorEve
"EN_US": "This is a template behavior"
}
};
public effect(individual: Individual, group: Group, model: Model, t: number): void {
}
}
export { Template };

View File

@ -120,7 +120,7 @@ class BehaviorList extends Component<IBehaviorListProps & IMixinSettingProps> {
</div>
<div className="behavior-content-view">
{this.renderTerm(behavior, name, "title-view", needLocal)}
{this.renderTerm(behavior, info, "info-view", needLocal)}
{this.renderTerm(behavior, info, "info-view", true)}
</div>
<div className="behavior-action-view">
{this.renderActionButton(behavior)}

View File

@ -15,7 +15,7 @@ interface ICommandBarProps {
}
@useSetting
@useStatusWithEvent("mouseModChange")
@useStatusWithEvent("mouseModChange", "actuatorStartChange")
class CommandBar extends Component<ICommandBarProps & IMixinSettingProps & IMixinStatusProps> {
render(): ReactNode {
@ -34,7 +34,13 @@ class CommandBar extends Component<ICommandBarProps & IMixinSettingProps & IMixi
>
<div>
{this.getRenderButton({ iconName: "Save", i18NKey: "Command.Bar.Save.Info" })}
{this.getRenderButton({ iconName: "Play", i18NKey: "Command.Bar.Play.Info" })}
{this.getRenderButton({
iconName: this.props.status?.actuator.start() ? "Pause" : "Play",
i18NKey: "Command.Bar.Play.Info",
click: () => this.props.status ? this.props.status.actuator.start(
!this.props.status.actuator.start()
) : undefined
})}
{this.getRenderButton({
iconName: "HandsFree", i18NKey: "Command.Bar.Drag.Info",
active: mouseMod === MouseMod.Drag,

View File

@ -42,7 +42,7 @@ class HeaderBar extends Component<
return (t: number) => {
let newState: HeaderBarState = {} as any;
newState[type] = 1 / t;
if (this.updateTime > 60) {
if (this.updateTime > 20) {
this.updateTime = 0;
this.setState(newState);
}

View File

@ -12,6 +12,7 @@ import { I18N } from "@Component/Localization/Localization";
import { superConnectWithEvent, superConnect } from "./Context";
import { PopupController } from "./Popups";
import { Behavior } from "@Model/Behavior";
import { Actuator } from "@Model/Actuator";
function randomColor(unNormal: boolean = false) {
const color = [
@ -44,6 +45,7 @@ interface IStatusEvent {
individualChange: void;
behaviorChange: void;
popupChange: void;
actuatorStartChange: void;
}
class Status extends Emitter<IStatusEvent> {
@ -71,6 +73,11 @@ class Status extends Emitter<IStatusEvent> {
*/
public model: Model = new Model();
/**
*
*/
public actuator: Actuator;
/**
*
*/
@ -104,8 +111,14 @@ class Status extends Emitter<IStatusEvent> {
public constructor() {
super();
// 初始化执行器
this.actuator = new Actuator(this.model);
// 执行器开启事件
this.actuator.on("startChange", () => { this.emit("actuatorStartChange") });
// 循环事件
this.model.on("loop", (t) => { this.emit("physicsLoop", t) });
this.actuator.on("loop", (t) => { this.emit("physicsLoop", t) });
// 对象变化事件
this.model.on("objectChange", () => this.emit("objectChange"));

125
source/Model/Actuator.ts Normal file
View File

@ -0,0 +1,125 @@
import { Model } from "./Model";
import { Emitter } from "./Emitter";
interface IActuatorEvent {
startChange: boolean;
loop: number;
}
/**
*
*/
class Actuator extends Emitter<IActuatorEvent> {
/**
*
*/
public speed: number = 1;
/**
*
*/
public fps: number = 36;
/**
* 仿
*/
private startFlag: boolean = false;
/**
*
*/
public start(start?: boolean): boolean {
if (start === undefined) {
return this.startFlag;
} else {
this.startFlag = start;
this.lastTime = 0;
this.alignTimer = 0;
this.emit("startChange", start);
return start;
}
}
/**
*
*/
public model: Model;
/**
*
*/
private lastTime: number = 0;
/**
*
*/
private alignTimer: number = 0;
public tickerType: 1 | 2 = 2;
private ticker(t: number) {
if (this.startFlag && t !== 0) {
if (this.lastTime === 0) {
this.lastTime = t;
} else {
let durTime = (t - this.lastTime) / 1000;
this.lastTime = t;
// 丢帧判定
if (durTime > 0.1) {
console.log("Actuator: Ticker dur time error. dropping...")
}
this.alignTimer += durTime;
if (this.alignTimer > (1 / this.fps)) {
this.model.update(this.alignTimer * this.speed);
this.emit("loop", this.alignTimer);
this.alignTimer = 0;
}
}
} else {
this.emit("loop", Infinity);
}
}
/**
*
* 1使 requestAnimationFrame
* 2 60
* 3
*/
private tickerAlign = (t: number) => {
this.ticker(t);
requestAnimationFrame(this.tickerAlign);
}
/**
*
*/
private tickerExp = () => {
this.ticker(window.performance.now());
setTimeout(this.tickerExp, (1 / this.fps) * 1000);
}
/**
*
*/
private runTicker = (t: number) => {
if (this.tickerType === 1) {
this.ticker(t);
requestAnimationFrame(this.runTicker);
} else {
this.ticker(window.performance.now());
setTimeout(this.runTicker, (1 / this.fps) * 1000);
}
}
public constructor(model: Model) {
super();
this.model = model;
this.runTicker(0);
}
}
export { Actuator }

View File

@ -64,7 +64,7 @@ interface IBehaviorParameterOptionItem<T extends IParamType = IParamType> {
/**
*
*/
type: T;
type: T | string;
/**
*
@ -202,7 +202,7 @@ class BehaviorRecorder<
* ID
*/
public getNextId() {
return `B-${this.behaviorName}-${this.nameIndex ++}`;
return `B-${this.behaviorId}-${this.nameIndex ++}`;
}
/**
@ -269,6 +269,7 @@ class BehaviorRecorder<
this.behaviorId = this.behaviorInstance.behaviorId;
this.behaviorName = this.behaviorInstance.behaviorName;
this.describe = this.behaviorInstance.describe;
this.category = this.behaviorInstance.category;
this.terms = this.behaviorInstance.terms;
}
}
@ -371,7 +372,7 @@ class Behavior<
* @param model
* @param t
*/
public beforeEffect(individual: Individual, group: Group, model: Model, t: number): void {};
public effect(individual: Individual, group: Group, model: Model, t: number): void {};
/**
*
@ -380,7 +381,7 @@ class Behavior<
* @param model
* @param t
*/
public effect(individual: Individual, group: Group, model: Model, t: number): void {};
public afterEffect(individual: Individual, group: Group, model: Model, t: number): void {};
/**
*
@ -389,7 +390,7 @@ class Behavior<
* @param model
* @param t
*/
public afterEffect(individual: Individual, group: Group, model: Model, t: number): void {};
public finalEffect(individual: Individual, group: Group, model: Model, t: number): void {};
}

View File

@ -330,7 +330,7 @@ class Group extends CtrlObject {
*
* @param
*/
public runner(t: number, effectType: "beforeEffect" | "effect" | "afterEffect" ): void {
public runner(t: number, effectType: "finalEffect" | "effect" | "afterEffect" ): void {
this.individuals.forEach((individual) => {
for(let j = 0; j < this.behaviors.length; j++) {
this.behaviors[j][effectType](individual, this, this.model, t);

View File

@ -12,9 +12,9 @@ class Individual {
* @param y y
* @param z z
*/
public static vectorLength(x: number[]): number;
public static vectorLength(x: number, y: number, z: number): number;
public static vectorLength(x: number | number[], y?: number, z?: number): number {
public vectorLength(x: number[]): number;
public vectorLength(x: number, y: number, z: number): number;
public vectorLength(x: number | number[], y?: number, z?: number): number {
if (Array.isArray(x)) {
return ((x[0] ?? 0)**2 + (x[1] ?? 0)**2 + (x[2] ?? 0)**2)**.5;
} else {
@ -28,10 +28,10 @@ class Individual {
* @param y y
* @param z z
*/
public static vectorNormalize(x: number[]): [number, number, number];
public static vectorNormalize(x: number, y: number, z: number): [number, number, number];
public static vectorNormalize(x: number | number[], y?: number, z?: number): [number, number, number] {
let length = Individual.vectorLength(x as number, y as number, z as number);
public vectorNormalize(x: number[]): [number, number, number];
public vectorNormalize(x: number, y: number, z: number): [number, number, number];
public vectorNormalize(x: number | number[], y?: number, z?: number): [number, number, number] {
let length = this.vectorLength(x as number, y as number, z as number);
if (Array.isArray(x)) {
return [
(x[0] ?? 0) / length,
@ -52,6 +52,39 @@ class Individual {
*/
public position: number[] = [0, 0, 0];
/**
*
*/
public velocity: number[] = [0, 0, 0];
/**
*
*/
public acceleration: number[] = [0, 0, 0];
/**
*
*/
public force: number[] = [0, 0, 0];
/**
*
*/
public applyForce(x: number[]): [number, number, number];
public applyForce(x: number, y: number, z: number): [number, number, number];
public applyForce(x: number | number[], y?: number, z?: number): [number, number, number] {
if (Array.isArray(x)) {
this.force[0] += x[0] ?? 0;
this.force[1] += x[1] ?? 0;
this.force[2] += x[2] ?? 0;
} else {
this.force[0] += x ?? 0;
this.force[1] += y ?? 0;
this.force[2] += z ?? 0;
}
return this.force as [number, number, number];
}
/**
*
*/
@ -107,7 +140,7 @@ class Individual {
* @param position
*/
public distanceTo(position: Individual | number[]): number {
return Individual.vectorLength(this.vectorTo(position));
return this.vectorLength(this.vectorTo(position));
}
/**

View File

@ -8,7 +8,6 @@ import { Label } from "./Label";
import { Behavior, IAnyBehavior, IAnyBehaviorRecorder } from "./Behavior";
type ModelEvent = {
loop: number;
labelChange: Label[];
objectChange: CtrlObject[];
individualChange: Group;
@ -259,7 +258,7 @@ class Model extends Emitter<ModelEvent> {
for (let i = 0; i < this.objectPool.length; i++) {
let object = this.objectPool[i];
if (object instanceof Group && object.update) {
object.runner(t, "beforeEffect");
object.runner(t, "effect");
}
}
@ -267,7 +266,7 @@ class Model extends Emitter<ModelEvent> {
for (let i = 0; i < this.objectPool.length; i++) {
let object = this.objectPool[i];
if (object instanceof Group && object.update) {
object.runner(t, "effect");
object.runner(t, "afterEffect");
}
}
@ -275,13 +274,11 @@ class Model extends Emitter<ModelEvent> {
for (let i = 0; i < this.objectPool.length; i++) {
let object = this.objectPool[i];
if (object instanceof Group && object.update) {
object.runner(t, "afterEffect");
object.runner(t, "finalEffect");
}
}
this.draw();
this.emit("loop", t);
}
public draw() {

View File

@ -10,6 +10,7 @@ import { RootContainer } from "@Component/Container/RootContainer";
import { LayoutDirection } from "@Context/Layout";
import { CommandBar } from "@Component/CommandBar/CommandBar";
import { Popup } from "@Component/Popup/Popup";
import { AllBehaviors } from "@Behavior/Behavior";
import "./SimulatorWeb.scss";
initializeIcons("https://img.mrkbear.com/fabric-cdn-prod_20210407.001/");
@ -54,6 +55,16 @@ class SimulatorWeb extends Component {
this.status.model.update(0);
this.status.newLabel().name = "New Label";
this.status.newLabel().name = "Test Label 01";
let dynamic = this.status.model.addBehavior(AllBehaviors[0]);
dynamic.name = "dynamic";
let brownian = this.status.model.addBehavior(AllBehaviors[1]);
brownian.name = "brownian";
let boundary = this.status.model.addBehavior(AllBehaviors[2]);
boundary.name = "boundary";
boundary.parameter.range = this.status.model.allRangeLabel;
group.addBehavior(dynamic);
group.addBehavior(brownian);
group.addBehavior(boundary);
}
(window as any).s = this;