diff --git a/src/builder/RobotBuilder.ts b/src/builder/RobotBuilder.ts new file mode 100644 index 0000000..f5bd7fe --- /dev/null +++ b/src/builder/RobotBuilder.ts @@ -0,0 +1,378 @@ +import { + IRobotSpec, + IRobotWheelGroup, + IMotorGroup, + IRobotWheelAndMount, + WheelMountingPoint, + IMotorSpec, + IRobotWheelSpec, + BasicSensorSpec, + IContactSensorSpec, + SensorMountingFace, + IDistanceSensorSpec, +} from "../engine/specs/RobotSpecs"; +import { Vector3d } from "../engine/SimTypes"; + +/** + * Utility class to declaratively create a robot spec + */ +export class Builder { + private _spec: IRobotSpec = { + type: "robot", + dimensions: { x: 0, y: 0, z: 0 }, + drivetrain: { + wheelGroups: [], + motorGroups: [], + }, + basicSensors: [], + }; + + private _wheelGroups: Map = new Map< + string, + IRobotWheelGroup + >(); + private _motorGroups: Map = new Map< + string, + IMotorGroup + >(); + + setDimensions(dimensions: Vector3d): Builder { + this._spec.dimensions = dimensions; + return this; + } + + addWheel(wheelGroupId: string, wheelSpec: IRobotWheelAndMount): Builder { + let wheelGroup: IRobotWheelGroup; + + if (!this._wheelGroups.has(wheelGroupId)) { + wheelGroup = { + id: wheelGroupId, + wheels: [], + }; + + this._wheelGroups.set(wheelGroupId, wheelGroup); + this._spec.drivetrain.wheelGroups.push(wheelGroup); + } else { + wheelGroup = this._wheelGroups.get(wheelGroupId); + } + + wheelGroup.wheels.push(wheelSpec); + + return this; + } + + addWheels(wheelGroupId: string, wheels: IRobotWheelAndMount[]): Builder { + wheels.forEach((wheel) => { + this.addWheel(wheelGroupId, wheel); + }); + return this; + } + + addMotor(wheelGroupId: string, motorSpec: IMotorSpec): Builder { + let motorGroup: IMotorGroup; + + if (!this._motorGroups.has(wheelGroupId)) { + motorGroup = { + wheelGroup: wheelGroupId, + motors: [], + }; + + this._motorGroups.set(wheelGroupId, motorGroup); + this._spec.drivetrain.motorGroups.push(motorGroup); + } else { + motorGroup = this._motorGroups.get(wheelGroupId); + } + + motorGroup.motors.push(motorSpec); + + return this; + } + + addMotors(wheelGroupId: string, motors: IMotorSpec[]): Builder { + motors.forEach((motor) => { + this.addMotor(wheelGroupId, motor); + }); + return this; + } + + addBasicSensor(sensor: BasicSensorSpec): Builder { + this._spec.basicSensors.push(sensor); + return this; + } + + generateSpec(): IRobotSpec { + return this._spec; + } +} + +export class MotorBuilder implements IMotorSpec { + private _spec: IMotorSpec = { + channel: 0, + maxForce: 0, + }; + + constructor(channelOrSpec?: number | IMotorSpec, maxForce?: number) { + if (typeof channelOrSpec === "number") { + this._spec.channel = channelOrSpec; + if (maxForce !== undefined) { + this._spec.maxForce = maxForce; + } + } else if (channelOrSpec !== undefined) { + this._spec = channelOrSpec; + } + } + + setChannel(channel: number): MotorBuilder { + this._spec.channel = channel; + return this; + } + + setMaxForce(maxForce: number): MotorBuilder { + this._spec.maxForce = maxForce; + return this; + } + + get channel(): number { + return this._spec.channel; + } + + get maxForce(): number { + return this._spec.maxForce; + } + + copy(): MotorBuilder { + const newSpec: IMotorSpec = JSON.parse(JSON.stringify(this._spec)); + return new MotorBuilder(newSpec); + } +} + +export class WheelBuilder implements IRobotWheelAndMount { + private _spec: IRobotWheelAndMount = { + wheel: { + type: "robot-wheel", + radius: 0, + }, + mountPoint: WheelMountingPoint.LEFT_CENTER, + }; + + constructor( + diameterOrSpec: number | IRobotWheelAndMount, + thickness?: number + ) { + if (typeof diameterOrSpec === "number") { + this._spec.wheel.radius = diameterOrSpec / 2; + this._spec.wheel.thickness = thickness; + } else { + this._spec = diameterOrSpec; + } + } + + get wheel(): IRobotWheelSpec { + return this._spec.wheel; + } + + get mountPoint(): WheelMountingPoint { + return this._spec.mountPoint; + } + + get offset(): Vector3d | undefined { + return this._spec.offset; + } + + setIsOmni(isOmni: boolean): WheelBuilder { + this._spec.wheel.isOmni = isOmni; + return this; + } + + setMountPoint(mountPoint: WheelMountingPoint): WheelBuilder { + this._spec.mountPoint = mountPoint; + + return this; + } + + setMountOffset(offset: Vector3d): WheelBuilder { + this._spec.offset = offset; + return this; + } + + copy(): WheelBuilder { + const newSpec: IRobotWheelAndMount = JSON.parse(JSON.stringify(this._spec)); + return new WheelBuilder(newSpec); + } +} + +export class ContactSensorBuilder implements IContactSensorSpec { + private _spec: IContactSensorSpec = { + type: "contact-sensor", + channel: 0, + mountFace: SensorMountingFace.FRONT, + width: 0, + range: 0, + }; + + constructor(channelOrSpec?: number | IContactSensorSpec) { + if (typeof channelOrSpec === "number") { + this._spec.channel = channelOrSpec; + } else if (channelOrSpec !== undefined) { + this._spec = channelOrSpec; + } + } + + setChannel(ch: number): ContactSensorBuilder { + this._spec.channel = ch; + return this; + } + + setMountFace(face: SensorMountingFace): ContactSensorBuilder { + this._spec.mountFace = face; + return this; + } + + setMountOffset(offset: Vector3d): ContactSensorBuilder { + this._spec.mountOffset = offset; + return this; + } + + setRender(render: boolean): ContactSensorBuilder { + this._spec.render = render; + return this; + } + + setWidth(w: number): ContactSensorBuilder { + this._spec.width = w; + return this; + } + + setRange(range: number): ContactSensorBuilder { + this._spec.range = range; + return this; + } + + copy(): ContactSensorBuilder { + const newParams = JSON.parse( + JSON.stringify(this._spec) + ) as IContactSensorSpec; + return new ContactSensorBuilder(newParams); + } + + get type(): "contact-sensor" { + return this._spec.type; + } + + get channel(): number { + return this._spec.channel; + } + + get mountFace(): SensorMountingFace { + return this._spec.mountFace; + } + + get mountOffset(): Vector3d | undefined { + return this._spec.mountOffset; + } + + get render(): boolean | undefined { + return this._spec.render; + } + + get width(): number { + return this._spec.width; + } + + get range(): number { + return this._spec.range; + } +} + +export class DistanceSensorBuilder implements IDistanceSensorSpec { + private _spec: IDistanceSensorSpec = { + type: "distance-sensor", + channel: 0, + mountFace: SensorMountingFace.FRONT, + minRange: 0, + maxRange: 0, + detectionAngle: 0, + }; + + constructor(channelOrSpec?: number | IDistanceSensorSpec) { + if (typeof channelOrSpec === "number") { + this._spec.channel = channelOrSpec; + } else if (channelOrSpec !== undefined) { + this._spec = channelOrSpec; + } + } + + setChannel(ch: number): DistanceSensorBuilder { + this._spec.channel = ch; + return this; + } + + setMountFace(face: SensorMountingFace): DistanceSensorBuilder { + this._spec.mountFace = face; + return this; + } + + setMountOffset(offset: Vector3d): DistanceSensorBuilder { + this._spec.mountOffset = offset; + return this; + } + + setRender(render: boolean): DistanceSensorBuilder { + this._spec.render = render; + return this; + } + + setMinRange(minRange: number): DistanceSensorBuilder { + this._spec.minRange = minRange; + return this; + } + + setMaxRange(maxRange: number): DistanceSensorBuilder { + this._spec.maxRange = maxRange; + return this; + } + + setDetectionAngle(angle: number): DistanceSensorBuilder { + this._spec.detectionAngle = angle; + return this; + } + + copy(): DistanceSensorBuilder { + const newParams = JSON.parse( + JSON.stringify(this._spec) + ) as IDistanceSensorSpec; + return new DistanceSensorBuilder(newParams); + } + + get type(): "distance-sensor" { + return this._spec.type; + } + + get channel(): number { + return this._spec.channel; + } + + get mountFace(): SensorMountingFace { + return this._spec.mountFace; + } + + get mountOffset(): Vector3d | undefined { + return this._spec.mountOffset; + } + + get render(): boolean | undefined { + return this._spec.render; + } + + get minRange(): number | undefined { + return this._spec.minRange; + } + + get maxRange(): number { + return this._spec.maxRange; + } + + get detectionAngle(): number | undefined { + return this._spec.detectionAngle; + } +} diff --git a/src/demos/demo1.ts b/src/demos/demo1.ts index e1d9fad..507e7ed 100644 --- a/src/demos/demo1.ts +++ b/src/demos/demo1.ts @@ -1,4 +1,4 @@ -import { Sim3D, SimulatorConfig, RobotSpecs } from "../index"; +import { Sim3D, SimulatorConfig, RobotSpecs, RobotBuilder } from "../index"; let simulator: Sim3D; @@ -93,122 +93,72 @@ function main() { // } // }, 2000); - const robotSpec: RobotSpecs.IRobotSpec = { - type: "robot", - dimensions: { x: 2, y: 1, z: 3 }, - basicSensors: [ - // { - // type: "contact-sensor", - // channel: 0, - // mountFace: RobotSpecs.SensorMountingFace.FRONT, - // render: true, - // width: 1.75, - // range: 0.1, - // }, - // { - // type: "contact-sensor", - // channel: 1, - // mountFace: RobotSpecs.SensorMountingFace.REAR, - // render: true, - // width: 1.75, - // range: 0.1, - // }, - { - type: "distance-sensor", - channel: 0, - mountFace: RobotSpecs.SensorMountingFace.FRONT, - minRange: 0, - maxRange: 10, - }, - { - type: "distance-sensor", - channel: 1, - mountFace: RobotSpecs.SensorMountingFace.REAR, - minRange: 0, - maxRange: 10, - }, - ], - drivetrain: { - motorGroups: [ - { - wheelGroup: "left-drive", - motors: [{ channel: 0, maxForce: 5 }], - }, - { - wheelGroup: "right-drive", - motors: [{ channel: 1, maxForce: 5 }], - }, - ], - wheelGroups: [ - { - id: "left-drive", - wheels: [ - { - wheel: { - type: "robot-wheel", - radius: 0.5, - thickness: 0.15, - }, - mountPoint: RobotSpecs.WheelMountingPoint.LEFT_FRONT, - offset: { x: -0.075, y: -0.25, z: 0.5 }, - }, - { - wheel: { - type: "robot-wheel", - radius: 0.5, - thickness: 0.15, - }, - mountPoint: RobotSpecs.WheelMountingPoint.LEFT_REAR, - offset: { x: -0.075, y: -0.25, z: -0.5 }, - }, - ], - }, - { - id: "right-drive", - wheels: [ - { - wheel: { - type: "robot-wheel", - radius: 0.5, - thickness: 0.15, - }, - mountPoint: RobotSpecs.WheelMountingPoint.RIGHT_FRONT, - offset: { x: 0.075, y: -0.25, z: 0.5 }, - }, - { - wheel: { - type: "robot-wheel", - radius: 0.5, - thickness: 0.15, - }, - mountPoint: RobotSpecs.WheelMountingPoint.RIGHT_REAR, - offset: { x: 0.075, y: -0.25, z: -0.5 }, - }, - ], - }, - ], - }, - }; - const robot = simulator.addRobot(robotSpec); + const robotBuilder = new RobotBuilder.Builder(); + const wheel = new RobotBuilder.WheelBuilder(1) + .setMountPoint(RobotSpecs.WheelMountingPoint.LEFT_FRONT) + .setMountOffset({ x: -0.075, y: -0.25, z: 0.5 }); + const motor = new RobotBuilder.MotorBuilder().setChannel(0).setMaxForce(5); + + const distanceSensor = new RobotBuilder.DistanceSensorBuilder(0) + .setMountFace(RobotSpecs.SensorMountingFace.FRONT) + .setMaxRange(5); + + robotBuilder + .setDimensions({ x: 2, y: 1, z: 3 }) + .addBasicSensor(distanceSensor) + .addBasicSensor( + distanceSensor + .copy() + .setChannel(1) + .setMountFace(RobotSpecs.SensorMountingFace.REAR) + ) + .addWheel("left-drive", wheel) + .addWheel( + "left-drive", + wheel + .copy() + .setMountPoint(RobotSpecs.WheelMountingPoint.LEFT_REAR) + .setMountOffset({ x: -0.075, y: -0.25, z: -0.5 }) + ) + .addWheel( + "right-drive", + wheel + .copy() + .setMountPoint(RobotSpecs.WheelMountingPoint.RIGHT_FRONT) + .setMountOffset({ x: 0.075, y: -0.25, z: 0.5 }) + ) + .addWheel( + "right-drive", + wheel + .copy() + .setMountPoint(RobotSpecs.WheelMountingPoint.RIGHT_REAR) + .setMountOffset({ x: 0.075, y: -0.25, z: -0.5 }) + ) + .addMotor("left-drive", motor) + .addMotor("right-drive", motor.copy().setChannel(1)); + + const robot = simulator.addRobot(robotBuilder.generateSpec()); let isGoingForward = true; - // robot.setMotorPower(0, -0.2); - // robot.setMotorPower(1, 0.2); - - // setInterval(() => { - // console.log("Distance: ", robot.getAnalogInput(0)); - // }, 200); robot.setMotorPower(0, 0.5); robot.setMotorPower(1, 0.5); setInterval(() => { - if (isGoingForward && robot.getAnalogInput(0) < 0.15) { + if ( + isGoingForward && + robot.getAnalogInput(0) > 0 && + robot.getAnalogInput(0) < 0.15 + ) { isGoingForward = false; robot.setMotorPower(0, -0.5); robot.setMotorPower(1, -0.5); } - if (!isGoingForward && robot.getAnalogInput(1) < 0.15) { + if ( + !isGoingForward && + robot.getAnalogInput(1) > 0 && + robot.getAnalogInput(1) < 0.15 + ) { isGoingForward = true; robot.setMotorPower(0, 0.5); robot.setMotorPower(1, 0.5); diff --git a/src/index.ts b/src/index.ts index 5268cef..c345ca7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,3 +12,6 @@ export { CoreSpecs }; import * as RobotSpecs from "./engine/specs/RobotSpecs"; export { RobotSpecs }; + +import * as RobotBuilder from "./builder/RobotBuilder"; +export { RobotBuilder };