diff --git a/express.js b/express.js index e991a07a..23301197 100644 --- a/express.js +++ b/express.js @@ -451,4 +451,4 @@ app.listen(config.server.port, () => { function setCrossOriginIsolationHeaders(res) { res.header("Cross-Origin-Opener-Policy", "same-origin"); res.header("Cross-Origin-Embedder-Policy", "require-corp"); -} \ No newline at end of file +} diff --git a/src/components/World/NodeSettings.tsx b/src/components/World/NodeSettings.tsx index a3bce576..72ace020 100644 --- a/src/components/World/NodeSettings.tsx +++ b/src/components/World/NodeSettings.tsx @@ -58,10 +58,10 @@ const TEMPLATES = { ['Blue Drink', 'drink_blue'], ['Green Drink', 'drink_green'], ['Pink Drink', 'drink_pink'], - ['Blue Pom (Ice Cube)', 'pom_blue'], - ['Orange Pom (Hot Sauce)', 'pom_orange'], - ['Red Pom (Ketchup)', 'pom_red'], - ['Yellow Pom (Mustard)', 'pom_yellow'], + ['Blue Pom', 'pom_blue'], + ['Orange Pom', 'pom_orange'], + ['Red Pom', 'pom_red'], + ['Yellow Pom', 'pom_yellow'], ['French Fry', 'french_fry'], ['Hamburger', 'hamburger'], ['Hotdog', 'hotdog'], @@ -85,8 +85,12 @@ const TEMPLATES = { ['Pallet', 'pallet'], ['Blue Pom', 'pomBlue2In'], ['Traffic Cone', 'trafficCone'], - ['2 Inch PVC', 'pcv2In'] - + ['2 Inch PVC', 'pcv2In'], + ['Sliding Door', 'slidingDoor'], + ['2 Inch Blue PVC', 'pvc2inBlue'], + ['2 Inch Pink PVC', 'pvc2inPink'], + ['Dropper', 'dropper26'], + ['Gate', 'gate'], ], RADIATION: [ // ['Radiation Science Pack - Low', 'noradscience'], diff --git a/src/simulator/definitions/nodes/2026gameTableTemplates.ts b/src/simulator/definitions/nodes/2026gameTableTemplates.ts index ee0fb26d..3df5d1df 100644 --- a/src/simulator/definitions/nodes/2026gameTableTemplates.ts +++ b/src/simulator/definitions/nodes/2026gameTableTemplates.ts @@ -6,9 +6,19 @@ import { PhysicsMotionType } from "@babylonjs/core"; // TODO: Consider deep-freezing all of these objects -const gameTable2026Template: Node.TemplatedNode = { +const fallTable26Template: Node.TemplatedNode = { type: 'object', - geometryId: 'gameTable2026', + geometryId: 'fallTable26', + physics: { + type: 'mesh', + motionType: PhysicsMotionType.STATIC, + restitution: .2, + friction: 1, + }, +}; +const springTable26Template: Node.TemplatedNode = { + type: 'object', + geometryId: 'springTable26', physics: { type: 'mesh', motionType: PhysicsMotionType.STATIC, @@ -24,13 +34,13 @@ const basketTemplate: Node.TemplatedNode = { motionType: PhysicsMotionType.DYNAMIC, mass: Mass.grams(30), restitution: 0.2, - friction: 2 + friction: .3 } }; const CUBEPHYSICS: Node.Physics = { type: 'box', motionType: PhysicsMotionType.DYNAMIC, - mass: Mass.grams(5), + mass: Mass.grams(2), restitution: 0.4, friction: 4 }; @@ -68,9 +78,9 @@ const palletTemplate: Node.TemplatedNode = { type: 'object', geometryId: 'pallet', physics: { - type: 'mesh', + type: 'box', motionType: PhysicsMotionType.DYNAMIC, - mass: Mass.grams(5), + mass: Mass.grams(1), restitution: 0.2, friction: 4 } @@ -108,9 +118,65 @@ const pcv2inTemplate: Node.TemplatedNode = { friction: 3.75 } }; +const slidingDoorTemplate: Node.TemplatedNode = { + type: 'object', + geometryId: 'slidingDoor', + physics: { + type: 'mesh', + motionType: PhysicsMotionType.DYNAMIC, + mass: Mass.grams(11), + restitution: 0.2, + friction: 0.3 + } +}; +const pvc2inBlueTemplate: Node.TemplatedNode = { + type: 'object', + geometryId: 'pvc2inBlue', + physics: { + type: 'cylinder', + motionType: PhysicsMotionType.DYNAMIC, + mass: Mass.grams(11), + restitution: 0.2, + friction: 0.3 + } +}; +const pvc2inPinkTemplate: Node.TemplatedNode = { + type: 'object', + geometryId: 'pvc2inPink', + physics: { + type: 'cylinder', + motionType: PhysicsMotionType.DYNAMIC, + mass: Mass.grams(11), + restitution: 0.2, + friction: 0.3 + } +}; +const dropper26Template: Node.TemplatedNode = { + type: 'object', + geometryId: 'dropper', + physics: { + type: 'mesh', + motionType: PhysicsMotionType.STATIC, + mass: Mass.grams(11), + restitution: 0.3, + friction: 0.2 + } +}; +const gateTemplate: Node.TemplatedNode = { + type: 'object', + geometryId: 'gate', + physics: { + type: 'mesh', + motionType: PhysicsMotionType.STATIC, + mass: Mass.grams(11), + restitution: 0, + friction: 1 + } +}; export const BB2026Templates = Object.freeze>>({ - 'gameTable2026': gameTable2026Template, + 'fallTable26': fallTable26Template, + 'springTable26': springTable26Template, 'basket': basketTemplate, 'cubeBrown4In': cubeBrown4inTemplate, 'cubeGreen2In': cubeGreen2inTemplate, @@ -121,15 +187,24 @@ export const BB2026Templates = Object.freeze>>({ 'pallet': palletTemplate, 'pomBlue2In': pomBlue2inTemplate, 'trafficCone': trafficConeTemplate, - 'pcv2In': pcv2inTemplate + 'pcv2In': pcv2inTemplate, + 'slidingDoor': slidingDoorTemplate, + 'pvc2inBlue': pvc2inBlueTemplate, + 'pvc2inPink': pvc2inPinkTemplate, + 'dropper26': dropper26Template, + 'gate': gateTemplate }); export const BB2026Geometries = Object.freeze>({ - 'gameTable2026': { + 'fallTable26': { type: 'file', uri: '/static/object_binaries/2026_Fall_Table.glb', }, + 'springTable26': { + type: 'file', + uri: '/static/object_binaries/2026_Table.glb', + }, 'basket': { type: 'file', uri: '/static/object_binaries/Basket.glb' @@ -173,5 +248,25 @@ export const BB2026Geometries = Object.freeze>({ 'pvc2In': { type: 'file', uri: '/static/object_binaries/PVC2in.glb' - } + }, + 'slidingDoor': { + type: 'file', + uri: '/static/object_binaries/Sliding_Door.glb' + }, + 'pvc2inBlue': { + type: 'file', + uri: '/static/object_binaries/PVC2in_Blue.glb' + }, + 'pvc2inPink': { + type: 'file', + uri: '/static/object_binaries/PVC2in_Pink.glb' + }, + 'dropper': { + type: 'file', + uri: '/static/object_binaries/2026_Dropper.glb' + }, + 'gate': { + type: 'file', + uri: '/static/object_binaries/gate.glb' + }, }); diff --git a/src/simulator/definitions/robots/demobot_no_reflectance.ts b/src/simulator/definitions/robots/demobot_no_reflectance.ts new file mode 100644 index 00000000..f83cb76a --- /dev/null +++ b/src/simulator/definitions/robots/demobot_no_reflectance.ts @@ -0,0 +1,16 @@ +import Robot from "../../../state/State/Robot"; +import Geometry from "../../../state/State/Robot/Geometry"; +import { DEMOBOT } from "./demobot"; + +/** + * Special demobot with no collision box over the reflectance sensor. + * When it has the collider, it can't drive straight up the ramp because the + * wheels always leave the ground. + */ +export const DEMOBOT_NO_REFLECTANCE: Robot = { + ...DEMOBOT, + geometry: { + ...DEMOBOT.geometry, + chassis_link: Geometry.remoteMesh({ uri: '/static/object_binaries/chassis_no_reflectance.glb' }), + }, +}; diff --git a/src/simulator/definitions/robots/index.ts b/src/simulator/definitions/robots/index.ts index 0f302dd9..f13b9c54 100644 --- a/src/simulator/definitions/robots/index.ts +++ b/src/simulator/definitions/robots/index.ts @@ -1,2 +1,3 @@ export * from './demobot'; -export * from './createbot'; \ No newline at end of file +export * from './createbot'; +export * from './demobot_no_reflectance'; diff --git a/src/simulator/definitions/scenes/26fallTableBase.ts b/src/simulator/definitions/scenes/26fallTableBase.ts index 097ae338..4b484e08 100644 --- a/src/simulator/definitions/scenes/26fallTableBase.ts +++ b/src/simulator/definitions/scenes/26fallTableBase.ts @@ -37,8 +37,8 @@ const ROBOT: Node.Robot = { const GAME_TABLE_2026: Node.FromBBTemplate = { type: 'from-bb-template', - name: tr('2026 Fall Game Table'), - templateId: 'gameTable2026', + name: tr('2026 Botball Fall Game Table'), + templateId: 'fallTable26', visible: true, editable: false, startingOrigin: GAME_TABLE_ORIGIN, diff --git a/src/simulator/definitions/scenes/26springTableBase.ts b/src/simulator/definitions/scenes/26springTableBase.ts new file mode 100644 index 00000000..5158b53a --- /dev/null +++ b/src/simulator/definitions/scenes/26springTableBase.ts @@ -0,0 +1,89 @@ +import { ReferenceFramewUnits, RotationwUnits, Vector3wUnits } from '../../../util/math/unitMath'; +import { Distance } from '../../../util'; +import Node from '../../../state/State/Scene/Node'; +import Camera from '../../../state/State/Scene/Camera'; +import Scene from '../../../state/State/Scene'; +import AbstractRobot from '../../../programming/AbstractRobot'; +import Author from '../../../db/Author'; + +import tr from '@i18n'; + +const ROBOT_ORIGIN: ReferenceFramewUnits = { + position: Vector3wUnits.centimeters(3, 0, 0), + // position: Vector3wUnits.centimeters(80, 0, 62.5), + orientation: RotationwUnits.eulerDegrees(0, 0, 0), +}; + +const GAME_TABLE_ORIGIN: ReferenceFramewUnits = { + position: Vector3wUnits.centimeters(45, -6, 100), + orientation: RotationwUnits.eulerDegrees(0, 180, 0), + // scale: { x: 100, y: 100, z: 100 }, +}; + +const LIGHT_ORIGIN: ReferenceFramewUnits = { + position: Vector3wUnits.centimeters(50, 90, 50) +}; + +/** + * Special demobot with no collision box over the reflectance sensor. + * When it has the collider, it can't drive straight up the ramp because the + * wheels always leave the ground. + */ +const ROBOT: Node.Robot = { + type: 'robot', + name: tr('Robot'), + robotId: 'demobot_no_reflectance', + state: AbstractRobot.Stateless.NIL, + visible: true, + startingOrigin: ROBOT_ORIGIN, + origin: ROBOT_ORIGIN +}; + +const GAME_TABLE_2026: Node.FromBBTemplate = { + type: 'from-bb-template', + name: tr('2026 Botball Spring Game Table'), + templateId: 'springTable26', + visible: true, + editable: false, + startingOrigin: GAME_TABLE_ORIGIN, + origin: GAME_TABLE_ORIGIN +}; + +export function createBaseSceneSurface(): Scene { + return { + name: tr('Base Scene - 2026 Botball Game Table'), + description: tr('A base scene. Intended to be augmented to create the full game table'), + author: Author.organization('kipr'), + geometry: {}, + nodes: { + 'robot': ROBOT, + 'game_table_2026': GAME_TABLE_2026, + 'light0': { + type: 'point-light', + intensity: 0.8, + name: tr('Light'), + startingOrigin: LIGHT_ORIGIN, + origin: LIGHT_ORIGIN, + visible: true + }, + }, + camera: Camera.arcRotate({ + radius: Distance.meters(5), + target: { + x: Distance.meters(0), + y: Distance.meters(0.05), + z: Distance.meters(0), + }, + position: { + x: Distance.meters(-0.75), + y: Distance.meters(0.75), + z: Distance.meters(-1.25), + } + }), + gravity: { + x: Distance.meters(0), + y: Distance.meters(-9.8 * 0.4), + z: Distance.meters(0), + } + }; +} diff --git a/src/simulator/definitions/scenes/26springTableSandbox.ts b/src/simulator/definitions/scenes/26springTableSandbox.ts new file mode 100644 index 00000000..84774297 --- /dev/null +++ b/src/simulator/definitions/scenes/26springTableSandbox.ts @@ -0,0 +1,658 @@ +import Scene from '../../../state/State/Scene'; +import Node from '../../../state/State/Scene/Node'; +import { ReferenceFramewUnits, RotationwUnits, Vector3wUnits } from '../../../util/math/unitMath'; +import Dict from '../../../util/objectOps/Dict'; +import { createBaseSceneSurface } from './26springTableBase'; +import Script from '../../../state/State/Scene/Script'; +import { sprintf } from 'sprintf-js'; + +import tr from '@i18n'; +import { setNodeVisible } from './jbcCommonComponents'; + +const baseScene = createBaseSceneSurface(); + +const DOOR_X = 97; +const DOOR_Y = 9; +const DOOR1_Z = 90; +const DOOR_ORIENTATION = RotationwUnits.eulerDegrees(0, 0, 91); +const DOOR1_ORIGIN: ReferenceFramewUnits = { + position: Vector3wUnits.centimeters(DOOR_X, DOOR_Y, DOOR1_Z), + orientation: DOOR_ORIENTATION +}; +const DOOR2_ORIGIN: ReferenceFramewUnits = { + position: Vector3wUnits.centimeters(DOOR_X, DOOR_Y, DOOR1_Z + 20.32), + orientation: DOOR_ORIENTATION +}; + +const DOORS: Dict = { + dock_pallet_1: { + type: 'from-bb-template', + name: tr('Sliding Door #1'), + templateId: 'slidingDoor', + visible: true, + editable: true, + startingOrigin: DOOR1_ORIGIN, + origin: DOOR1_ORIGIN + }, + dock_pallet_2: { + type: 'from-bb-template', + name: tr('Sliding Door #2'), + templateId: 'slidingDoor', + visible: true, + editable: true, + startingOrigin: DOOR2_ORIGIN, + origin: DOOR2_ORIGIN + } +}; +// This rotation prevents clipping into the pipe +const POM_ORIENTATION: RotationwUnits = RotationwUnits.eulerDegrees(0, 90, 0); +const LO_Y = -3; +const POM_Z_GAP = 6 * 2.54; +const LO_Z_1 = -17.4 + POM_Z_GAP; + +const LO_BLUE_POMS: Dict = {}; +for (let i = 0; i < 6; i++) { + const origin: ReferenceFramewUnits = { + position: Vector3wUnits.centimeters(65, LO_Y, LO_Z_1 + POM_Z_GAP * i), + orientation: POM_ORIENTATION + }; + LO_BLUE_POMS[`loBlue${i}`] = { + type: 'from-bb-template', + name: Dict.map(tr('Low Blue Pom #%d'), (str: string) => sprintf(str, i + 1)), + templateId: 'pomBlue2In', + visible: true, + editable: true, + startingOrigin: origin, + origin + }; +} +const LO_ORANGE_POMS: Dict = {}; +for (let i = 0; i < 6; i++) { + const origin: ReferenceFramewUnits = { + position: Vector3wUnits.centimeters(32, LO_Y, LO_Z_1 + POM_Z_GAP * i), + orientation: POM_ORIENTATION + }; + LO_ORANGE_POMS[`loOrange${i}`] = { + type: 'from-bb-template', + name: Dict.map(tr('Low Orange Pom #%d'), (str: string) => sprintf(str, i + 1)), + templateId: 'pom_orange', + visible: true, + editable: true, + startingOrigin: origin, + origin + }; +} + +const HI_BLUE_X = 3.9; +const HI_Y = 12.5; +const HI_Z_1 = 204.5 - POM_Z_GAP; +const HI_BLUE_POMS: Dict = {}; +for (let i = 0; i < 6; i++) { + const origin: ReferenceFramewUnits = { + position: Vector3wUnits.centimeters(HI_BLUE_X, HI_Y, HI_Z_1 - POM_Z_GAP * i), + orientation: POM_ORIENTATION + }; + HI_BLUE_POMS[`hiBlue${i}`] = { + type: 'from-bb-template', + name: Dict.map(tr('High Blue Pom #%d'), (str: string) => sprintf(str, i + 1)), + templateId: 'pomBlue2In', + visible: true, + editable: true, + startingOrigin: origin, + origin + }; +} +const HI_ORANGE_X = HI_BLUE_X - 2 * 2.54; +const HI_ORANGE_POMS: Dict = {}; +for (let i = 0; i < 6; i++) { + const origin: ReferenceFramewUnits = { + position: Vector3wUnits.centimeters(HI_ORANGE_X, HI_Y, HI_Z_1 - POM_Z_GAP * i), + orientation: POM_ORIENTATION + }; + HI_ORANGE_POMS[`hiOrange${i}`] = { + type: 'from-bb-template', + name: Dict.map(tr('High Orange Pom #%d'), (str: string) => sprintf(str, i + 1)), + templateId: 'pom_orange', + visible: true, + editable: true, + startingOrigin: origin, + origin + }; +} + +const BASKET_ORIENTATION = RotationwUnits.eulerDegrees(0, 90, 0); +const BASKET_X = 2; +const BASKET_Y = -5; +const BASKET_Z = 208; +const BASKET_ORIGINS: ReferenceFramewUnits[] = [ + { + position: Vector3wUnits.centimeters(BASKET_X, BASKET_Y, BASKET_Z), + orientation: BASKET_ORIENTATION + }, + { + position: Vector3wUnits.centimeters(BASKET_X, BASKET_Y, BASKET_Z - 33.7), + orientation: BASKET_ORIENTATION + } +]; +const BASKETS: Dict = { + leftBasket: { + type: 'from-bb-template', + name: tr('Left Basket'), + templateId: 'basket', + visible: true, + editable: true, + startingOrigin: BASKET_ORIGINS[0], + origin: BASKET_ORIGINS[0] + }, + rightBasket: { + type: 'from-bb-template', + name: tr('Right Basket'), + templateId: 'basket', + visible: true, + editable: true, + startingOrigin: BASKET_ORIGINS[1], + origin: BASKET_ORIGINS[1] + } +}; + +const PIPE_2IN_CUBE_X = 91; +const PIPE_2IN_CUBE_Y = -2.8; +const PIPE_2IN_CUBE_GAP = 8.534 + 2 * 2.54; +const RED_Z_1 = 56.2; +const GREEN_Z_1 = 130.1; +const LOW_2IN_CUBE_ORIGINS: ReferenceFramewUnits[] = [ + { position: Vector3wUnits.centimeters(PIPE_2IN_CUBE_X, PIPE_2IN_CUBE_Y, RED_Z_1) }, + { position: Vector3wUnits.centimeters(PIPE_2IN_CUBE_X, PIPE_2IN_CUBE_Y, RED_Z_1 + PIPE_2IN_CUBE_GAP) }, + { position: Vector3wUnits.centimeters(PIPE_2IN_CUBE_X, PIPE_2IN_CUBE_Y, GREEN_Z_1) }, + { position: Vector3wUnits.centimeters(PIPE_2IN_CUBE_X, PIPE_2IN_CUBE_Y, GREEN_Z_1 + PIPE_2IN_CUBE_GAP) } +]; +const LOW_2IN_CUBES: Dict = { + leftRed: { + type: 'from-bb-template', + name: tr('Left Red Cube (2 inch)'), + templateId: 'cubeRed2In', + visible: true, + editable: true, + startingOrigin: LOW_2IN_CUBE_ORIGINS[0], + origin: LOW_2IN_CUBE_ORIGINS[0] + }, + rightRed: { + type: 'from-bb-template', + name: tr('Right Red Cube (2 inch)'), + templateId: 'cubeRed2In', + visible: true, + editable: true, + startingOrigin: LOW_2IN_CUBE_ORIGINS[1], + origin: LOW_2IN_CUBE_ORIGINS[1] + }, + leftGreen: { + type: 'from-bb-template', + name: tr('Left Green Cube (2 inch)'), + templateId: 'cubeGreen2In', + visible: true, + editable: true, + startingOrigin: LOW_2IN_CUBE_ORIGINS[2], + origin: LOW_2IN_CUBE_ORIGINS[2] + }, + rightGreen: { + type: 'from-bb-template', + name: tr('Right Green Cube (2 inch)'), + templateId: 'cubeGreen2In', + visible: true, + editable: true, + startingOrigin: LOW_2IN_CUBE_ORIGINS[3], + origin: LOW_2IN_CUBE_ORIGINS[3] + } +}; + +const LM_PALLET_X = 24.5375; +const LOW_PALLET_Y = -5; +const LOW_PALLET_Z = 47.9; +const PALLET_H = 1.8; +const PALLET_ORIGINS: ReferenceFramewUnits[] = [ + { position: Vector3wUnits.centimeters(LM_PALLET_X, LOW_PALLET_Y, LOW_PALLET_Z) }, + { position: Vector3wUnits.centimeters(LM_PALLET_X, LOW_PALLET_Y + PALLET_H, LOW_PALLET_Z) }, + { position: Vector3wUnits.centimeters(LM_PALLET_X + 0.225, 0, 61.8) }, + { position: Vector3wUnits.centimeters(22.25, 10, 106.9) } +]; + +const PALLETS: Dict = {}; +for (const [i, origin] of PALLET_ORIGINS.entries()) { + PALLETS[`pallet${i}`] = { + type: 'from-bb-template', + name: Dict.map(tr('Pallet #%d'), (str: string) => sprintf(str, i + 1)), + templateId: 'pallet', + visible: true, + editable: true, + startingOrigin: origin, + origin + }; +} + +const YELLOW_CUBE_ORIGINS: ReferenceFramewUnits[] = [ + { position: Vector3wUnits.add(PALLET_ORIGINS[1].position, Vector3wUnits.centimeters(0, 2.54 + PALLET_H, 0)) }, + { position: Vector3wUnits.add(PALLET_ORIGINS[1].position, Vector3wUnits.centimeters(0, PALLET_H + 3 * 2.54, 0)) } +]; + +const YELLOW_2IN_CUBES: Dict = { + lower: { + type: 'from-bb-template', + name: tr('Lower Yellow Cube (2 inch)'), + templateId: 'cubeYellow2In', + visible: true, + editable: true, + startingOrigin: YELLOW_CUBE_ORIGINS[0], + origin: YELLOW_CUBE_ORIGINS[0] + }, + upper: { + type: 'from-bb-template', + name: tr('Upper Yellow Cube (2 inch)'), + templateId: 'cubeYellow2In', + visible: true, + editable: true, + startingOrigin: YELLOW_CUBE_ORIGINS[1], + origin: YELLOW_CUBE_ORIGINS[1] + } +}; + +const MIDLINE_X = 106; +const BROWN_CUBE_Y = 0; +const BROWN_CUBE_LEFT_Z = RED_Z_1 + PIPE_2IN_CUBE_GAP + 5; +const BROWN_CUBE_RIGHT_Z = GREEN_Z_1 - 5; +const BROWN_CUBE_ORIGINS: ReferenceFramewUnits[] = [ + { position: Vector3wUnits.centimeters(MIDLINE_X, BROWN_CUBE_Y, BROWN_CUBE_LEFT_Z) }, + { position: Vector3wUnits.centimeters(MIDLINE_X, BROWN_CUBE_Y, BROWN_CUBE_RIGHT_Z) } +]; + +const BROWN_CUBES: Dict = { + left: { + type: 'from-bb-template', + name: tr('Middle Left Brown Cube'), + templateId: 'cubeBrown4In', + visible: true, + editable: true, + startingOrigin: BROWN_CUBE_ORIGINS[0], + origin: BROWN_CUBE_ORIGINS[0] + }, + right: { + type: 'from-bb-template', + name: tr('Middle Right Brown Cube'), + templateId: 'cubeBrown4In', + visible: true, + editable: true, + startingOrigin: BROWN_CUBE_ORIGINS[1], + origin: BROWN_CUBE_ORIGINS[1] + } +}; + +const MIDLINE_Z = (BROWN_CUBE_LEFT_Z + BROWN_CUBE_RIGHT_Z - 2) / 2; +const BOTGUY_ORIGIN: ReferenceFramewUnits = { + position: Vector3wUnits.centimeters(MIDLINE_X, 4, MIDLINE_Z), + orientation: RotationwUnits.eulerDegrees(0, 90, 0) +}; +const BOTGUY: Node = { + type: 'from-bb-template', + name: tr('Botguy'), + templateId: 'botguy_gamepiece', + visible: true, + editable: true, + startingOrigin: BOTGUY_ORIGIN, + origin: BOTGUY_ORIGIN +}; + +const RAND_4IN_ORIGINS: ReferenceFramewUnits[] = [ + { position: Vector3wUnits.centimeters(PIPE_2IN_CUBE_X - 2.54, 0, MIDLINE_Z) }, + { position: Vector3wUnits.add(PALLET_ORIGINS[3].position, Vector3wUnits.centimeters(0, PALLET_H + 2 * 2.54, 0)) } +].sort((_a, _b) => Math.random() - 0.5); + +const RAND_4IN_CUBES: Dict = { + green: { + type: 'from-bb-template', + name: tr('Green Cube'), + templateId: 'cubeGreen4In', + visible: true, + editable: true, + startingOrigin: RAND_4IN_ORIGINS[0], + origin: RAND_4IN_ORIGINS[0] + }, + red: { + type: 'from-bb-template', + name: tr('Red Cube'), + templateId: 'cubeRed4In', + visible: true, + editable: true, + startingOrigin: RAND_4IN_ORIGINS[1], + origin: RAND_4IN_ORIGINS[1] + } +}; + +const STACK_HEIGHTS = [LO_Y, LO_Y + 2 * 2.54, LO_Y + 4 * 2.54].sort((a, b) => Math.random() - 0.5); +const STACK_ORDER = ['Red', 'Yellow', 'Green']; +const NEAR_STACK_ORIGINS: ReferenceFramewUnits[] = STACK_HEIGHTS.map((y) => ReferenceFramewUnits.create(Vector3wUnits.centimeters(-9, y, 8.645))); +const NEAR_STACK: Dict = {}; +for (const [i, origin] of NEAR_STACK_ORIGINS.entries()) { + const color = STACK_ORDER[i]; + NEAR_STACK[`near${color}`] = { + type: 'from-bb-template', + name: Dict.map(tr('Near Stack %s Cube'), (str: string) => sprintf(str, color)), + templateId: `cube${color}2In`, + visible: true, + editable: true, + startingOrigin: origin, + origin + }; +} +const FAR_STACK_ORIGINS: ReferenceFramewUnits[] = STACK_HEIGHTS.map((y) => ReferenceFramewUnits.create(Vector3wUnits.centimeters(64.894, y, 215.135))); +const FAR_STACK: Dict = {}; +for (const [i, origin] of FAR_STACK_ORIGINS.entries()) { + const color = STACK_ORDER[i]; + FAR_STACK[`far${color}`] = { + type: 'from-bb-template', + name: Dict.map(tr('Far Stack %s Cube'), (str: string) => sprintf(str, color)), + templateId: `cube${color}2In`, + visible: true, + editable: true, + startingOrigin: origin, + origin + }; +} + +const CONE_X = 65; +const CONE_ORIGINS: ReferenceFramewUnits[] = [ + { position: Vector3wUnits.centimeters(CONE_X, -5, 100) }, + { position: Vector3wUnits.centimeters(CONE_X, -5, 161) } +]; +const CONES: Dict = { + rightCone: { + type: 'from-bb-template', + name: tr('Right Traffic Cone'), + templateId: 'trafficCone', + visible: true, + editable: true, + startingOrigin: CONE_ORIGINS[0], + origin: CONE_ORIGINS[0] + }, + leftCone: { + type: 'from-bb-template', + name: tr('Left Traffic Cone'), + templateId: 'trafficCone', + visible: true, + editable: true, + startingOrigin: CONE_ORIGINS[1], + origin: CONE_ORIGINS[1] + } +}; +const DROPPER_X = 106; +const DROPPER_Y = 4; +const DROPPER1_Z = -16.8; +const DROPPER1_ORIENTATION = RotationwUnits.eulerDegrees(0, 0, 0); +const DROPPER2_ORIENTATION = RotationwUnits.eulerDegrees(0, 180, 0); +const DROPPER1_ORIGIN: ReferenceFramewUnits = { + position: Vector3wUnits.centimeters(DROPPER_X, DROPPER_Y, DROPPER1_Z), + orientation: DROPPER1_ORIENTATION +}; +const DROPPER2_ORIGIN: ReferenceFramewUnits = { + position: Vector3wUnits.centimeters(DROPPER_X, DROPPER_Y, DROPPER1_Z + 233.7), + orientation: DROPPER2_ORIENTATION +}; +const DROPPERS: Dict = { + dropper1: { + type: 'from-bb-template', + name: tr('Dropper #1'), + templateId: 'dropper26', + visible: true, + editable: true, + startingOrigin: DROPPER1_ORIGIN, + origin: DROPPER1_ORIGIN + }, + dropper2: { + type: 'from-bb-template', + name: tr('Dropper #2'), + templateId: 'dropper26', + visible: true, + editable: true, + startingOrigin: DROPPER2_ORIGIN, + origin: DROPPER2_ORIGIN + } +}; + +const PVC1_ORIENTATION = RotationwUnits.eulerDegrees(0, 0, 90); +const PVC_LEFT_1_ORIGIN: ReferenceFramewUnits = { + position: Vector3wUnits.centimeters(108, -0.5, -14), + orientation: PVC1_ORIENTATION +}; +const PVC_OFFSET_CM_VALUES: [number, number, number][] = [ + [0, 0, -1.8], + [0, 1.5, -4.75], + [0, 3.5, -7.5], + [0, 7, -7.75], + [0, 10, -7.75], + [0, 12.5, -5.25], + [0, 12.75, -1.5], + [0, 13.5, 3] +]; +const PVC_LEFT_ORIGINS: ReferenceFramewUnits[] = PVC_OFFSET_CM_VALUES.map( + ([x, y, z]) => ({ + position: Vector3wUnits.add( + PVC_LEFT_1_ORIGIN.position, + Vector3wUnits.centimeters(x, y, z) + ), + orientation: PVC1_ORIENTATION + }) +).sort((_a, _b) => Math.random() - 0.5); + +const PVC_TEMPLATES = ['pvc2inBlue', 'pvc2inPink']; +const PVCS_LEFT: Dict = {}; +for (const [i, origin] of PVC_LEFT_ORIGINS.entries()) { + PVCS_LEFT[`pvc${i}_left`] = { + type: 'from-bb-template', + name: tr('Barrel (2in PVC)'), + templateId: PVC_TEMPLATES[i % 2], + visible: true, + editable: true, + origin, + startingOrigin: origin + }; +} +const PVC_RIGHT_1_ORIGIN: ReferenceFramewUnits = { + position: Vector3wUnits.centimeters(108, -0.5, 214), + orientation: PVC1_ORIENTATION +}; +const PVC_RIGHT_ORIGINS: ReferenceFramewUnits[] = PVC_OFFSET_CM_VALUES.map( + ([x, y, z]) => ({ + position: Vector3wUnits.add( + PVC_RIGHT_1_ORIGIN.position, + Vector3wUnits.centimeters(x, y, -1 * z) + ), + orientation: PVC1_ORIENTATION + }) +).sort((_a, _b) => Math.random() - 0.5); +const PVCS_RIGHT: Dict = {}; +for (const [i, origin] of PVC_RIGHT_ORIGINS.entries()) { + PVCS_RIGHT[`pvc${i}_right`] = { + type: 'from-bb-template', + name: tr('Barrel (2in PVC)'), + templateId: PVC_TEMPLATES[i % 2], + visible: true, + editable: true, + origin, + startingOrigin: origin + }; +} + +const GATE1_ORIENTATION = RotationwUnits.eulerDegrees(120, 0, 0); +const GATE_LEFT_ORIGIN: ReferenceFramewUnits = { + position: Vector3wUnits.centimeters(106, -2, -13), + orientation: GATE1_ORIENTATION +}; +const GATE_OFFSET_CM_VALUES: [number, number, number][] = [ + [0, 1, -1.5], + [0, 2, -4.75], + [0, 3.25, -8], + [0, 6.5, -8.5], + [0, 9.75, -8.5], + [0, 12.5, -7.25], + [0, 13, -4.3], + [0, 14, -0.5] +]; +const GATE_ORIENTATIONS: number[] = [ + 120, + 120, + 135, + 0, + 0, + 45, + 90, + 90 +]; +const GATE_LEFT_ORIGINS: ReferenceFramewUnits[] = GATE_OFFSET_CM_VALUES.map( + ([x, y, z], i) => ({ + position: Vector3wUnits.add( + GATE_LEFT_ORIGIN.position, + Vector3wUnits.centimeters(x, y, z) + ), + orientation: RotationwUnits.eulerDegrees(GATE_ORIENTATIONS[i], 0, 0) + }) +); + +const GATES_LEFT: Dict = {}; +for (const [i, origin] of GATE_LEFT_ORIGINS.entries()) { + GATES_LEFT[`gate_left${i}`] = { + type: 'from-bb-template', + name: tr('Dropper Gate'), + templateId: 'gate', + visible: true, + editable: true, + origin, + startingOrigin: origin + }; +} + +const GATE_RIGHT_ORIGIN: ReferenceFramewUnits = { + position: Vector3wUnits.centimeters(106, -2, 213), + orientation: GATE1_ORIENTATION +}; +const GATE_RIGHT_ORIGINS: ReferenceFramewUnits[] = GATE_OFFSET_CM_VALUES.map( + ([x, y, z], i) => ({ + position: Vector3wUnits.add( + GATE_RIGHT_ORIGIN.position, + Vector3wUnits.centimeters(x, y, -1 * z) + ), + orientation: RotationwUnits.eulerDegrees(180 - GATE_ORIENTATIONS[i], 0, 0) + }) +); +const GATES_RIGHT: Dict = {}; +for (const [i, origin] of GATE_RIGHT_ORIGINS.entries()) { + GATES_RIGHT[`gate${i}_right`] = { + type: 'from-bb-template', + name: tr('Dropper Gate'), + templateId: 'gate', + visible: true, + editable: true, + origin, + startingOrigin: origin + }; +} + +const DROPPER = ` +${setNodeVisible} + +// After ten seconds, hide each left/right gate pair for one second every seven seconds (one pair at a time) +const intervalMs = 7000; +const hiddenDurationMs = 1100; +const gateDurationsMs = [ + 900, // gate0 + 1000, // gate1 + 600, // gate2 (shortened) + 400, // gate3 (shortened) + 400, // gate4 (shortened) + hiddenDurationMs, // gate5 + hiddenDurationMs, // gate6 + hiddenDurationMs, // gate7 +]; +const gatePairs = gateDurationsMs.map((_duration, index) => [ + 'gate_left' + index, + 'gate' + index + '_right', +]); +const setGatePairVisible = (gateIndex, visible) => { + gatePairs[gateIndex].forEach((nodeId) => setNodeVisible(nodeId, visible)); +}; + +let startTimeMs = Date.now(); +let currentGateIndex = null; +let hideStartMs = null; +let waitingComplete = false; +let inCycle = false; +let nextGate0Time = null; + +scene.addOnRenderListener(() => { + let now = Date.now(); + + if (!waitingComplete) { + if (now - startTimeMs >= 10000) { + waitingComplete = true; + nextGate0Time = now; + } + return; + } + + if (!inCycle && nextGate0Time !== null && now >= nextGate0Time) { + inCycle = true; + currentGateIndex = 0; + hideStartMs = now; + setGatePairVisible(currentGateIndex, false); + nextGate0Time += intervalMs; + } + + while (inCycle && currentGateIndex !== null && hideStartMs !== null && now - hideStartMs >= gateDurationsMs[currentGateIndex]) { + setGatePairVisible(currentGateIndex, true); + currentGateIndex++; + + if (currentGateIndex >= gatePairs.length) { + inCycle = false; + currentGateIndex = null; + hideStartMs = null; + break; + } + + // Immediately hide the next gate + hideStartMs = now = Date.now(); + setGatePairVisible(currentGateIndex, false); + } +}); +`; + +export const SPRING_26_SANDBOX: Scene = { + ...baseScene, + name: tr('2026 Botball Game Table Sandbox'), + description: tr('2026 Botball game table sandbox. One robot and one side of the table are available. Barrels (2in PVC) drop every 7 seconds, starting 10 seconds after the scene is loaded.'), + geometry: { + ...baseScene.geometry, + }, + scripts: { + dropper: Script.ecmaScript('Dropper Script', DROPPER) + }, + nodes: { + ...baseScene.nodes, + ...DOORS, + ...LO_BLUE_POMS, + ...LO_ORANGE_POMS, + ...HI_BLUE_POMS, + ...HI_ORANGE_POMS, + ...BASKETS, + ...LOW_2IN_CUBES, + ...PALLETS, + ...YELLOW_2IN_CUBES, + ...BROWN_CUBES, + ...RAND_4IN_CUBES, + ...NEAR_STACK, + ...FAR_STACK, + ...CONES, + BOTGUY, + ...DROPPERS, + ...PVCS_LEFT, + ...PVCS_RIGHT, + ...GATES_LEFT, + ...GATES_RIGHT + } +}; diff --git a/src/simulator/definitions/scenes/index.ts b/src/simulator/definitions/scenes/index.ts index 61667283..2b6a7fcb 100644 --- a/src/simulator/definitions/scenes/index.ts +++ b/src/simulator/definitions/scenes/index.ts @@ -26,6 +26,7 @@ export * from './jbc24-Walk-the-Line'; export * from './moonSandbox'; export * from './tableSandbox'; export * from './26fallTableSandbox'; +export * from './26springTableSandbox'; export * from './gcer25'; // export * from './jbcGcer25-Find-The-Black-Line'; // export * from './jbcGcer25-Sense-The-Can'; diff --git a/src/state/State/Scene/Node.ts b/src/state/State/Scene/Node.ts index 49a1bee3..7f158dda 100644 --- a/src/state/State/Scene/Node.ts +++ b/src/state/State/Scene/Node.ts @@ -182,9 +182,9 @@ namespace Node { /** * The name of the Geometry of this node. * The Geometry variable must be accessable to the Node somehow. - * See /src/simulator/definitions/scenes/tableBase.ts for an example of including it locally, - * or /src/simulator/definitions/nodes/2025gameTableTemplates.ts and - * /src/simulator/definitions/scenes/tableSandbox.ts for an example of using templates. + * See `/src/simulator/definitions/scenes/tableBase.ts` for an example of including it locally, + * or `/src/simulator/definitions/nodes/2025gameTableTemplates.ts` and + * `/src/simulator/definitions/scenes/tableSandbox.ts` for an example of using templates. */ geometryId: string; /** diff --git a/src/state/reducer/scenes.ts b/src/state/reducer/scenes.ts index 0db542a2..cce4d7a2 100644 --- a/src/state/reducer/scenes.ts +++ b/src/state/reducer/scenes.ts @@ -288,8 +288,7 @@ export type ScenesAction = ( const DEFAULT_SCENES: Scenes = { jbcSandbox: Async.loaded({ value: JBC_SCENES.JBC_Sandbox }), moonSandbox: Async.loaded({ value: JBC_SCENES.Moon_Sandbox }), - fall26TableSandbox: Async.loaded({ value: JBC_SCENES.FALL_26_SANDBOX }), - tableSandbox: Async.loaded({ value: JBC_SCENES.Table_Sandbox }), + spring26TableSandbox: Async.loaded({ value: JBC_SCENES.SPRING_26_SANDBOX }), jbc0: Async.loaded({ value: JBC_SCENES.JBC_0 }), jbc1: Async.loaded({ value: JBC_SCENES.JBC_1 }), jbc2: Async.loaded({ value: JBC_SCENES.JBC_2 }), @@ -331,6 +330,8 @@ const DEFAULT_SCENES: Scenes = { jbc22: Async.loaded({ value: JBC_SCENES.JBC_22 }), jbc23: Async.loaded({ value: JBC_SCENES.JBC_23 }), jbc24: Async.loaded({ value: JBC_SCENES.JBC_24 }), + fall26TableSandbox: Async.loaded({ value: JBC_SCENES.FALL_26_SANDBOX }), + tableSandbox: Async.loaded({ value: JBC_SCENES.Table_Sandbox }), Find_The_Black_Line: Async.loaded({ value: JBC_SCENES.Find_The_Black_Line }), Sense_The_Can: Async.loaded({ value: JBC_SCENES.Sense_The_Can }), Ice_Ice_Botguy: Async.loaded({ value: JBC_SCENES.Ice_Ice_Botguy }), diff --git a/static/object_binaries/2026_Dropper.glb b/static/object_binaries/2026_Dropper.glb new file mode 100644 index 00000000..6d78b48b --- /dev/null +++ b/static/object_binaries/2026_Dropper.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffb3c653cd37a5360cbce769640d27e128c6a81704e886354b1a3a2acac9aac1 +size 18944 diff --git a/static/object_binaries/2026_Table.glb b/static/object_binaries/2026_Table.glb new file mode 100644 index 00000000..3778d196 --- /dev/null +++ b/static/object_binaries/2026_Table.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39cbba952739cce5c3a92c3cdd7dbe4b7b8afa1486efd3a67cc4a2918720d843 +size 29300068 diff --git a/static/object_binaries/PVC2in_Blue.glb b/static/object_binaries/PVC2in_Blue.glb new file mode 100644 index 00000000..b7f6b984 --- /dev/null +++ b/static/object_binaries/PVC2in_Blue.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c62461ab750e31ff510099d8c48c79e86a42ebaa2640b9cd0f29a254e8dd0a89 +size 184780 diff --git a/static/object_binaries/PVC2in_Pink.glb b/static/object_binaries/PVC2in_Pink.glb new file mode 100644 index 00000000..edb3f1eb --- /dev/null +++ b/static/object_binaries/PVC2in_Pink.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e90e8022e64c3567bbf4f3de6f7bc5ed8ef8c98818c78e2759b3d6b672b9ed1 +size 184820 diff --git a/static/object_binaries/Sliding_Door.glb b/static/object_binaries/Sliding_Door.glb new file mode 100644 index 00000000..24044694 --- /dev/null +++ b/static/object_binaries/Sliding_Door.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c0ffa6db2020a8eba999db4bc634afa0621b18149ea5e92f7dbc5ba4b84ae204 +size 103180 diff --git a/static/object_binaries/botguy.glb b/static/object_binaries/botguy.glb index 9005064f..68c98acc 100644 --- a/static/object_binaries/botguy.glb +++ b/static/object_binaries/botguy.glb @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6062d4f5b6d300aad2710963657049970078f3cdb4fa6e93574c9fee893c7f61 -size 3486516 +oid sha256:adeed0c0f618b8379b394e25f03dcc88255e1a2556d568f801778cdbf7ea730d +size 3811628 diff --git a/static/object_binaries/chassis_no_reflectance.glb b/static/object_binaries/chassis_no_reflectance.glb new file mode 100644 index 00000000..41afb938 --- /dev/null +++ b/static/object_binaries/chassis_no_reflectance.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9d7dcffb178d5738a526947950ca5fb6dc889aee2b073e23684f7e4d6005f48 +size 6234368 diff --git a/static/object_binaries/gate.glb b/static/object_binaries/gate.glb new file mode 100644 index 00000000..6c26984a --- /dev/null +++ b/static/object_binaries/gate.glb @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8b62b13305a3a9d49d364f999f4dd6990b8ee0a6a9927fae9e4ff6c186df1c7e +size 1964