36 Commits

Author SHA1 Message Date
495330dbf4 renabled a missing animation 2024-09-12 13:16:56 -04:00
Quinn
e9a8ae1a22 Merge pull request #18 from Block-Party-VR/develop
Bugfix: Removed static com ports for talking with the board
2024-09-11 19:36:28 -04:00
f3c9aea558 updated SerialMessage submodule 2024-09-11 19:32:51 -04:00
f471e9ab56 removed monitor port secifiers in platformio ini 2024-09-11 19:30:58 -04:00
Quinn
a632e91b1c Merge pull request #17 from Block-Party-VR/develop
MIT Museum Release
2024-09-10 18:01:24 -04:00
8b5cd5323b updated the animaiton tool readme 2024-08-28 23:20:48 -04:00
Quinn
37fc5e4dcf Merge pull request #15 from Block-Party-VR/14-create-an-animation-studio-tool-for-generating-animations-on-the-board
14 create an animation studio tool for generating animations on the board
2024-08-28 22:57:03 -04:00
2f8469ef6c finished the animator tool's UI 2024-08-28 22:55:52 -04:00
ecd516bc6f Added a frame counter 2024-08-28 22:33:50 -04:00
12193dc560 added dropdown menus 2024-08-28 22:16:11 -04:00
5cb8101495 added a UI class to better organize UI creation 2024-08-28 20:10:17 -04:00
9d6c19cfd5 You can now generate many consecutive frames 2024-08-27 23:21:18 -04:00
e4506bd9f0 Got a single frame exporting correctly 2024-08-27 21:54:56 -04:00
7a454edc46 Organized modules into individual files 2024-08-27 18:17:27 -04:00
7a7494beb2 Set up virtual environment 2024-08-27 18:05:09 -04:00
77ddc5c7fd starting to build the naimation tool 2024-08-27 18:01:26 -04:00
Quinn
5ca4ea2410 Merge pull request #10 from Block-Party-VR/5-create-more-eye-catching-idle-animations
Refactoring the board tracking code and adding new functionality to make it easy to make animations for the board
2024-08-26 18:50:51 -04:00
Quinn
4196fdef23 Merge pull request #12 from Block-Party-VR/5-create-cool-animations
Added two new animations to use
2024-08-26 18:50:22 -04:00
5237de0539 minor bug fixes and a new animation 2024-08-26 18:49:24 -04:00
Quinn
35a94f7b5a Merge pull request #11 from Block-Party-VR/5-impliment-the-animator
Impliment the animator
2024-08-25 16:28:44 -04:00
67c5cc7c19 Got the animator completed tested and working 2024-08-25 16:25:59 -04:00
14ac96988a Got the animator fades working 2024-08-25 16:11:06 -04:00
240d4866aa Got the animator running 2024-08-25 14:30:08 -04:00
61ff0bbbfd The board now works as expected but without idle animations 2024-08-25 12:12:46 -04:00
4d47b68600 Fixed several buffer overflow issues 2024-08-25 12:06:52 -04:00
a59a6657e8 Finished commenting code changes 2024-08-24 14:07:12 -04:00
5caa0a8a61 Added comments to boarddriver.h 2024-08-23 17:48:54 -04:00
5ba5924a29 Added comments to the functions in board.h 2024-08-23 17:37:30 -04:00
3a49761b66 Got the refactor building 2024-08-22 23:00:55 -04:00
3e4f0124db Completed the board manager rewrite 2024-08-22 21:07:32 -04:00
48f83eee38 Refactoring how board control works 2024-08-22 18:17:35 -04:00
Quinn
a29ccbd2c8 Merge pull request #9 from Block-Party-VR/6-switch-over-to-using-freertos-tasks
6 switch over to using freertos tasks
2024-08-20 18:29:52 -04:00
2d3b1392d0 Added framework for users to edit the paltformio file for machine differences without uploading the changes to github. 2024-08-20 18:28:40 -04:00
bf852ffc81 Added note to readme about initializing submodules 2024-08-20 18:12:00 -04:00
64e84ad531 Small code refactor and began using freertos tasks 2024-08-20 18:06:07 -04:00
Quinn
2074fd6b73 Merge pull request #7 from Block-Party-VR/2-electrical-schematics-v1-design-fixes
2 electrical schematics v1 design fixes
2024-08-20 11:27:48 -04:00
36 changed files with 2309 additions and 503 deletions

2
.gitignore vendored
View File

@@ -9,3 +9,5 @@ Schematics/Block-Party/Block-Party-Cube-Bottom-Board/production
Schematics/Block-Party/Block-Party-Main-Board/Block-Party-Main-Board-backups/
Schematics/Block-Party/Block-Party-Cube-Bottom-Board/Block-Party-Bottom-Board-backups/
Schematics/Block-Party/Block-Party-Cube-Top-Board/Block-Party-Cube-Top-Board-backups/
platformio-local.ini

View File

@@ -15,6 +15,11 @@ When the physical user removes a block from the board, the corresponding digital
When the VR user hovers over or picks up a digital cube, the corresponding physical cube changes color to show the physical user what the VR user is doing.
## Setup
### Repository Setup
When the repository is first cloned you will need to initialize all of its submodules.
In the terminal type: `git submodule update --init --recursive` which will initalize all of the submodules.
### Board Assemble
#### Parts List
- Foam core (scavenged)
@@ -53,7 +58,7 @@ wiring diagram for the cube is in the `documentation` directory.
- Arduino libraries
### Bluetooth Module
On v0.1 of the baord, a seperate bluetooth module (HC-05) is being used for bluetooth. This module needs to be programmed when first plugged in. To program the module, disconnect it from power and hold the "EN" button on the module. (The button should be the only button on the HC-05 module). While still holding down the button, reconnect the module to the ESP32 and press the ESP32's reset button. Wait 5 seconds while still holding down the "EN" button on the module, then release the button, power cycle the module, and you're done.
On v0.1 of the board, a seperate bluetooth module (HC-05) is being used for bluetooth. This module needs to be programmed when first plugged in. To program the module, disconnect it from power and hold the "EN" button on the module. (The button should be the only button on the HC-05 module). While still holding down the button, reconnect the module to the ESP32. Let go of the button and then press the ESP32's reset button. Wait 5 seconds while still holding down the "EN" button on the module, then release the button, power cycle the module, and you're done.
## Run
- Power up the board and pair it with the headset over bluetooth.

View File

@@ -4,32 +4,67 @@
*/
#pragma once
#include <cstdint>
#include <array>
#include "PINOUT.h"
#include "CubeStack.h"
#include "BoardTypes.h"
#include "Vector3D.h"
// define some important buffer sizes
static constexpr uint32_t SERIAL_ARG_LENGTH{15};
static constexpr uint32_t SERIAL_CHAR_LENGTH{SERIAL_ARG_LENGTH*10};
// define the physical dimensions of the board
#define BOARD_WIDTH 3
#define BOARD_LENGTH 3
#define BOARD_HEIGHT 3
static constexpr uint32_t BOARD_WIDTH{3};
static constexpr uint32_t BOARD_LENGTH{3};
static constexpr uint32_t BOARD_HEIGHT{3};
static constexpr V3D<uint32_t> BOARD_DIMENSIONS{BOARD_WIDTH, BOARD_LENGTH, BOARD_HEIGHT};
// define the number of stacks
#define NUMBER_STACKS BOARD_WIDTH * BOARD_LENGTH
static constexpr uint32_t NUMBER_STACKS{BOARD_WIDTH * BOARD_LENGTH};
// define the CubeStacks
CubeStack stack1(STACK1_ADC_PIN, STACK1_LED_PIN, BOARD_HEIGHT);
CubeStack stack2(STACK2_ADC_PIN, STACK2_LED_PIN, BOARD_HEIGHT);
CubeStack stack3(STACK3_ADC_PIN, STACK3_LED_PIN, BOARD_HEIGHT);
CubeStack stack4(STACK4_ADC_PIN, STACK4_LED_PIN, BOARD_HEIGHT);
CubeStack stack5(STACK5_ADC_PIN, STACK5_LED_PIN, BOARD_HEIGHT);
CubeStack stack6(STACK6_ADC_PIN, STACK6_LED_PIN, BOARD_HEIGHT);
CubeStack stack7(STACK7_ADC_PIN, STACK7_LED_PIN, BOARD_HEIGHT);
CubeStack stack8(STACK8_ADC_PIN, STACK8_LED_PIN, BOARD_HEIGHT);
CubeStack stack9(STACK9_ADC_PIN, STACK9_LED_PIN, BOARD_HEIGHT);
// define the array of stacks
CubeStack * stacks[] = {
&stack1, &stack2, &stack3,
&stack4, &stack5, &stack6,
&stack7, &stack8, &stack9
static BOARD_TYPES::CubeStack stack1{
.adcPin=STACK1_ADC_PIN,
.ledPin=STACK1_LED_PIN
};
static BOARD_TYPES::CubeStack stack2{
.adcPin=STACK2_ADC_PIN,
.ledPin=STACK2_LED_PIN
};
static BOARD_TYPES::CubeStack stack3{
.adcPin=STACK3_ADC_PIN,
.ledPin=STACK3_LED_PIN
};
static BOARD_TYPES::CubeStack stack4{
.adcPin=STACK4_ADC_PIN,
.ledPin=STACK4_LED_PIN
};
static BOARD_TYPES::CubeStack stack5{
.adcPin=STACK5_ADC_PIN,
.ledPin=STACK5_LED_PIN
};
static BOARD_TYPES::CubeStack stack6{
.adcPin=STACK6_ADC_PIN,
.ledPin=STACK6_LED_PIN
};
static BOARD_TYPES::CubeStack stack7{
.adcPin=STACK7_ADC_PIN,
.ledPin=STACK7_LED_PIN
};
static BOARD_TYPES::CubeStack stack8{
.adcPin=STACK8_ADC_PIN,
.ledPin=STACK8_LED_PIN
};
static BOARD_TYPES::CubeStack stack9{
.adcPin=STACK9_ADC_PIN,
.ledPin=STACK9_LED_PIN
};
static std::array<BOARD_TYPES::CubeStack, NUMBER_STACKS> stacks{
stack1, stack2, stack3,
stack4, stack5, stack6,
stack7, stack8, stack9
};

13
include/COMMANDS.h Normal file
View File

@@ -0,0 +1,13 @@
#pragma once
#include <cstdint>
/**
* @brief These are the serial commands that the board supports
*/
enum Commands : uint8_t{
BoardState = 0,
PING = 1,
SetStackColors = 2,
GoToIdle = 3
};

View File

@@ -5,45 +5,47 @@
#pragma once
#include <cstdint>
// Stack pins
// Stack 1 pins
#define STACK1_ADC_PIN 6
#define STACK1_LED_PIN 11
static constexpr uint8_t STACK1_ADC_PIN{6};
static constexpr uint8_t STACK1_LED_PIN{11};
// Stack 2 pins
#define STACK2_ADC_PIN 16
#define STACK2_LED_PIN 14
static constexpr uint8_t STACK2_ADC_PIN{16};
static constexpr uint8_t STACK2_LED_PIN{14};
// Stack 3 pins
#define STACK3_ADC_PIN 8
#define STACK3_LED_PIN 45
static constexpr uint8_t STACK3_ADC_PIN{8};
static constexpr uint8_t STACK3_LED_PIN{45};
// Stack 4 pins
#define STACK4_ADC_PIN 5
#define STACK4_LED_PIN 10
static constexpr uint8_t STACK4_ADC_PIN{5};
static constexpr uint8_t STACK4_LED_PIN{10};
// Stack 5 pins
#define STACK5_ADC_PIN 15
#define STACK5_LED_PIN 13
static constexpr uint8_t STACK5_ADC_PIN{15};
static constexpr uint8_t STACK5_LED_PIN{13};
// Stack 6 pins
#define STACK6_ADC_PIN 18
#define STACK6_LED_PIN 38
static constexpr uint8_t STACK6_ADC_PIN{18};
static constexpr uint8_t STACK6_LED_PIN{38};
// Stack 7 pins
#define STACK7_ADC_PIN 4
#define STACK7_LED_PIN 9
static constexpr uint8_t STACK7_ADC_PIN{4};
static constexpr uint8_t STACK7_LED_PIN{9};
// Stack 8 pins
#define STACK8_ADC_PIN 7
#define STACK8_LED_PIN 12
static constexpr uint8_t STACK8_ADC_PIN{7};
static constexpr uint8_t STACK8_LED_PIN{12};
// Stack 9 pins
#define STACK9_ADC_PIN 17
#define STACK9_LED_PIN 37
static constexpr uint8_t STACK9_ADC_PIN{17};
static constexpr uint8_t STACK9_LED_PIN{37};
// Bluetooth module configuration pins
#define BT_STATE_PIN 2
#define BT_EN_PIN 3
static constexpr uint8_t BT_STATE_PIN{2};
static constexpr uint8_t BT_EN_PIN{3};

80
include/Vector3D.h Normal file
View File

@@ -0,0 +1,80 @@
#pragma once
#include <cstdint>
#include <cmath>
template <typename Type>
class V3D{
public:
constexpr V3D(const V3D& other):
x(other.x),
y(other.y),
z(other.z){
static_assert(std::is_arithmetic<Type>::value, "Type must be a number");
}
constexpr V3D(Type x=0, Type y=0, Type z=0):
x(x),
y(y),
z(z){
static_assert(std::is_arithmetic<Type>::value, "Type must be a number");
}
template <typename OtherType>
constexpr V3D(const V3D<OtherType> other):
x(static_cast<Type>(other.x)),
y(static_cast<Type>(other.y)),
z(static_cast<Type>(other.z)){
static_assert(std::is_arithmetic<Type>::value, "Type must be a number");
static_assert(std::is_arithmetic<OtherType>::value, "OtherType must be a number");
}
V3D& operator=(const V3D &other){
this->x = other.x;
this->y = other.y;
this->z = other.z;
return *this;
}
V3D& operator+=(const V3D &other){
this->x += other.x;
this->y += other.y;
this->z += other.z;
return *this;
}
V3D& operator-=(const V3D &other){
this->x -= other.x;
this->y -= other.y;
this->z -= other.z;
return *this;
}
V3D& operator/=(const Type scalar){
if(scalar == 0){
return *this;
}
this->x /= scalar;
this->y /= scalar;
this->z /= scalar;
return *this;
}
V3D& operator*=(const Type scalar){
this->x *= scalar;
this->y *= scalar;
this->z *= scalar;
return *this;
}
bool operator==(const V3D &other){
return this->x == other.x && this->y == other.y && this->z == other.z;
}
float magnitude(){
return std::sqrt(static_cast<float>(this->x * this->x + this->y * this->y + this->z * this->z));
}
Type x;
Type y;
Type z;
};

252
lib/Animator/Animation.h Normal file
View File

@@ -0,0 +1,252 @@
#pragma once
#include "AnimationTypes.h"
#include "Vector3D.h"
#include "Animator.h"
using namespace ANIMATION_TYPES;
namespace AnimationHelpers{
V3D<uint8_t> red{255,0,0};
V3D<uint8_t> green{0,255,0};
V3D<uint8_t> blue{0,0,255};
V3D<uint8_t> cyan{0,255,255};
V3D<uint8_t> magenta{255,0,255};
Cell CreateCell(float x_percent, float y_percent, float z_percent, const V3D<uint8_t> &color){
float continuousMaxValue{static_cast<float>(std::numeric_limits<uint32_t>::max())};
Cell cell{
.position = V3D<uint32_t>{
static_cast<uint32_t>(continuousMaxValue*x_percent),
static_cast<uint32_t>(continuousMaxValue*y_percent),
static_cast<uint32_t>(continuousMaxValue*z_percent)
},
.color = color
};
return cell;
}
}
namespace RotatingCubes{
using namespace AnimationHelpers;
AnimationFrame frame1{
.frame = {
CreateCell(0,0,0,red),
CreateCell(1,0.5,0,green),
CreateCell(0,1,0,blue)
},
.fillInterpolation = FillInterpolation::CLOSEST_COLOR,
.frameInterpolation = FrameInterpolation::FADE,
.delay = std::chrono::milliseconds(1000)
};
AnimationFrame frame2{
.frame = {
CreateCell(0,0.5,0,red),
CreateCell(1,0,0,green),
CreateCell(0.5,1,0,blue)
},
.fillInterpolation = FillInterpolation::CLOSEST_COLOR,
.frameInterpolation = FrameInterpolation::FADE,
.delay = std::chrono::milliseconds(500)
};
AnimationFrame frame3{
.frame = {
CreateCell(0,1,0,red),
CreateCell(0.5,0,0,green),
CreateCell(1,1,0,blue)
},
.fillInterpolation = FillInterpolation::CLOSEST_COLOR,
.frameInterpolation = FrameInterpolation::FADE,
.delay = std::chrono::milliseconds(1000)
};
AnimationFrame frame4{
.frame = {
CreateCell(0.5,1,0,red),
CreateCell(0,0,0,green),
CreateCell(1,0.5,0,blue)
},
.fillInterpolation = FillInterpolation::CLOSEST_COLOR,
.frameInterpolation = FrameInterpolation::FADE,
.delay = std::chrono::milliseconds(500)
};
AnimationFrame frame5{
.frame = {
CreateCell(1,1,0,red),
CreateCell(0,0.5,0,green),
CreateCell(1,0,0,blue)
},
.fillInterpolation = FillInterpolation::CLOSEST_COLOR,
.frameInterpolation = FrameInterpolation::FADE,
.delay = std::chrono::milliseconds(1000)
};
AnimationFrame frame6{
.frame = {
CreateCell(1,0.5,0,red),
CreateCell(0,1,0,green),
CreateCell(0.5,0,0,blue)
},
.fillInterpolation = FillInterpolation::CLOSEST_COLOR,
.frameInterpolation = FrameInterpolation::FADE,
.delay = std::chrono::milliseconds(500)
};
AnimationFrame frame7{
.frame = {
CreateCell(1,0,0,red),
CreateCell(0.5,1,0,green),
CreateCell(0,0,0,blue)
},
.fillInterpolation = FillInterpolation::CLOSEST_COLOR,
.frameInterpolation = FrameInterpolation::FADE,
.delay = std::chrono::milliseconds(1000)
};
AnimationFrame frame8{
.frame = {
CreateCell(0.5,0,0,red),
CreateCell(1,1,0,green),
CreateCell(0,0.5,0,blue)
},
.fillInterpolation = FillInterpolation::CLOSEST_COLOR,
.frameInterpolation = FrameInterpolation::FADE,
.delay = std::chrono::milliseconds(500)
};
AnimationFrame frame9{
.frame = {
CreateCell(0,0,0,red),
CreateCell(1,0.5,0,green),
CreateCell(0,1,0,blue)
},
.fillInterpolation = FillInterpolation::CLOSEST_COLOR,
.frameInterpolation = FrameInterpolation::FADE,
.delay = std::chrono::milliseconds(1)
};
std::vector<AnimationFrame> rotating{
frame1, // 0
frame2, // 1
frame3, // 2
frame4, // 3
frame5, // 4
frame6, // 5
frame7, // 6
frame8, // 7
frame9, // 8
};
}
namespace RisingCubes{
using namespace AnimationHelpers;
AnimationFrame frame1{
.frame = {
CreateCell(0,0,0,cyan),
CreateCell(0,1,0.5,green),
CreateCell(1,0,1,blue),
CreateCell(0.5,0.5,0.5,red),
CreateCell(1,1,0,magenta)
},
.fillInterpolation = FillInterpolation::LINEAR_WEIGHTED_DISTANCE,
.frameInterpolation = FrameInterpolation::FADE,
.delay = std::chrono::milliseconds(800)
};
AnimationFrame frame2{
.frame = {
CreateCell(0,0,0.5,cyan),
CreateCell(0,1,1,green),
CreateCell(1,0,0.5,blue),
CreateCell(0.5,0.5,0,red),
CreateCell(1,1,0.5,magenta)
},
.fillInterpolation = FillInterpolation::LINEAR_WEIGHTED_DISTANCE,
.frameInterpolation = FrameInterpolation::FADE,
.delay = std::chrono::milliseconds(800)
};
AnimationFrame frame3{
.frame = {
CreateCell(0,0,1,cyan),
CreateCell(0,1,0.5,green),
CreateCell(1,0,0,blue),
CreateCell(0.5,0.5,0.5,red),
CreateCell(1,1,1,magenta)
},
.fillInterpolation = FillInterpolation::LINEAR_WEIGHTED_DISTANCE,
.frameInterpolation = FrameInterpolation::FADE,
.delay = std::chrono::milliseconds(800)
};
AnimationFrame frame4{
.frame = {
CreateCell(0,0,0.5,cyan),
CreateCell(0,1,0,green),
CreateCell(1,0,0.5,blue),
CreateCell(0.5,0.5,1,red),
CreateCell(1,1,0.5,magenta)
},
.fillInterpolation = FillInterpolation::LINEAR_WEIGHTED_DISTANCE,
.frameInterpolation = FrameInterpolation::FADE,
.delay = std::chrono::milliseconds(800)
};
AnimationFrame frame5{
.frame = {
CreateCell(0,0,0,cyan),
CreateCell(0,1,0.5,green),
CreateCell(1,0,1,blue),
CreateCell(0.5,0.5,0.5,red),
CreateCell(1,1,0,magenta)
},
.fillInterpolation = FillInterpolation::LINEAR_WEIGHTED_DISTANCE,
.frameInterpolation = FrameInterpolation::FADE,
.delay = std::chrono::milliseconds(1)
};
AnimationFrame frame00{
.frame = {
CreateCell(0.0,0.0,1.0,V3D<uint8_t>(128.0,128.0,255.0)),
CreateCell(0.5,0.0,1.0,V3D<uint8_t>(128.0,128.0,255.0)),
CreateCell(1.0,0.0,0.0,V3D<uint8_t>(255.0,118.0,205.0)),
CreateCell(1.0,0.0,0.5,V3D<uint8_t>(255.0,118.0,205.0)),
CreateCell(1.0,0.0,1.0,V3D<uint8_t>(255.0,116.0,0.0)),
CreateCell(1.0,0.5,1.0,V3D<uint8_t>(183.0,0.0,255.0)),
CreateCell(1.0,1.0,1.0,V3D<uint8_t>(183.0,0.0,255.0))
},
.fillInterpolation = FillInterpolation::CLOSEST_COLOR,
.frameInterpolation = FrameInterpolation::FADE,
.delay = std::chrono::milliseconds(1000)
};
AnimationFrame frame01{
.frame = {
CreateCell(0.0,0.0,1.0,V3D<uint8_t>(255.0,255.0,171.0)),
CreateCell(0.5,0.0,1.0,V3D<uint8_t>(255.0,255.0,171.0)),
CreateCell(1.0,0.0,0.0,V3D<uint8_t>(0.0,195.0,88.0)),
CreateCell(1.0,0.0,0.5,V3D<uint8_t>(0.0,195.0,88.0)),
CreateCell(1.0,0.0,1.0,V3D<uint8_t>(0.0,195.0,88.0)),
CreateCell(1.0,0.5,1.0,V3D<uint8_t>(112.0,222.0,255.0)),
CreateCell(1.0,1.0,1.0,V3D<uint8_t>(112.0,222.0,255.0))
},
.fillInterpolation = FillInterpolation::CLOSEST_COLOR,
.frameInterpolation = FrameInterpolation::FADE,
.delay = std::chrono::milliseconds(1000)
};
std::vector<AnimationFrame> rising{
frame00, frame01, frame00
// frame1, // 0
// frame2, // 1
// frame3, // 2
// frame4, // 3
// frame5
};
}

View File

@@ -0,0 +1,33 @@
#pragma once
#include "Vector3D.h"
#include <vector>
#include <chrono>
namespace ANIMATION_TYPES{
// for cube spots which aren't defined in a key frame,
// you can have the controller automatically interpolate a color
enum FillInterpolation{
NO_FILL, // if not specified, the cube color will be black
CLOSEST_COLOR, // The cube color will be the same color as the cube closest to it
LINEAR_WEIGHTED_DISTANCE, // the cube color will be an average of all specified cube colors weighted by the linear distance to this cube
SQUARE_WEIGHTED_DISTANCE // same as linear, but further colors have exponentially less impact on the color
};
enum FrameInterpolation{
SNAP, // After the delay, snap to the next key frame
FADE // over the course of the delay, fade to the next frame
};
struct Cell{
V3D<uint32_t> position;
V3D<uint32_t> color;
};
// this contains all of the information to specify exactly how a single frame should look and fade to the next frame
struct AnimationFrame{
std::vector<Cell> frame;
FillInterpolation fillInterpolation;
FrameInterpolation frameInterpolation;
std::chrono::milliseconds delay;
};
};

293
lib/Animator/Animator.h Normal file
View File

@@ -0,0 +1,293 @@
#pragma once
#include <cstdint>
#include <array>
#include <vector>
#include <chrono>
#include <cstring>
#include <cmath>
#include <climits>
#include "Vector3D.h"
#include "AnimationTypes.h"
using namespace ANIMATION_TYPES;
template <const V3D<uint32_t> &BOARD_DIMS>
class Animator{
public:
typedef std::array<std::array<std::array<V3D<uint32_t>, BOARD_DIMS.z>, BOARD_DIMS.y>, BOARD_DIMS.x> Frame;
void StartAnimation(const std::vector<AnimationFrame> *animationSequence);
void RunAnimation(const std::chrono::milliseconds& timePassed);
void SetLoop(bool isLooping);
Frame &GetInterpolatedFrame(){return this->interpolatedFrame;}
bool isEnabled{true};
bool interpolatedFrameHasChanged{false};
private:
bool isLooping{true};
// these are the uncompressed frames you get by following the key colors and interpolation instructions of an animation frame
Frame startFrame;
Frame interpolatedFrame;
Frame endFrame;
std::chrono::milliseconds timeElapsed;
const std::vector<AnimationFrame> *animationSequence;
uint32_t animationIndex{0};
void incrimentAnimationIndex();
void uncompressFrame(const AnimationFrame &keyFrame, Frame &frameBuffer);
void copyFrame(Frame &copyFrom, Frame &copyTo){
std::memcpy(&copyTo, &copyFrom, sizeof(Frame));
}
V3D<uint32_t> getInterpolatedColor(const AnimationFrame &keyFrame, V3D<uint32_t> position);
V3D<uint32_t> keyFrame2BoardCoords(const V3D<uint32_t> &keyFramePosition);
V3D<uint32_t> noFillInterpolate(const AnimationFrame &keyFrame, V3D<uint32_t> position);
V3D<uint32_t> closestColorInterpolate(const AnimationFrame &keyFrame, V3D<uint32_t> position);
V3D<uint32_t> linearInterpolate(const AnimationFrame &keyFrame, V3D<uint32_t> position);
V3D<uint32_t> squareInterpolate(const AnimationFrame &keyFrame, V3D<uint32_t> position);
void PrintUncompressedFrame(){
for(uint32_t x = 0; x < BOARD_DIMS.x; x++){
for(uint32_t y = 0; y < BOARD_DIMS.y; y++){
for(uint32_t z = 0; z < BOARD_DIMS.z; z++){
auto color = this->startFrame[x][y][z];
Serial.print("Cube X:" + String(x) + ",Y:" + String(y) + ",Z:" + String(z));
Serial.println("\tColor R:" + String(color.x) + ",G:" + String(color.y) + ",B:" + String(color.z));
}
}
}
}
};
template <const V3D<uint32_t> &BOARD_DIMS>
void Animator<BOARD_DIMS>::StartAnimation(const std::vector<AnimationFrame> *animationSequence){
this->animationSequence = animationSequence;
this->animationIndex = 0;
this->timeElapsed = std::chrono::milliseconds(0);
if(animationSequence->size() == 0){
return;
}
else if(animationSequence->size() == 1){
AnimationFrame frame{((*this->animationSequence)[0])};
this->uncompressFrame(frame, this->startFrame);
this->copyFrame(this->startFrame, this->interpolatedFrame);
this->copyFrame(this->startFrame, this->endFrame);
}
else{
this->uncompressFrame((*this->animationSequence)[0], this->startFrame);
this->copyFrame(this->startFrame, this->interpolatedFrame);
this->uncompressFrame((*this->animationSequence)[1], this->endFrame);
}
this->interpolatedFrameHasChanged = true;
}
template <const V3D<uint32_t> &BOARD_DIMS>
void Animator<BOARD_DIMS>::RunAnimation(const std::chrono::milliseconds& timePassed){
if(!(this->isEnabled)){
return;
}
auto delayTime = (*this->animationSequence)[this->animationIndex].delay;
this->timeElapsed += timePassed;
// load in the next frame if we're done with this transition
if(this->timeElapsed >= delayTime){
this->incrimentAnimationIndex();
}
// don't do frame interpolations if we're doing snap fades
if((*this->animationSequence)[this->animationIndex].frameInterpolation == FrameInterpolation::SNAP){
return;
}
// linearly interpolate between the two uncompressed frames
for(uint32_t x = 0; x < BOARD_DIMS.x; x++){
for(uint32_t y = 0; y < BOARD_DIMS.y; y++){
for(uint32_t z = 0; z < BOARD_DIMS.z; z++){
V3D<float> startColor{this->startFrame[x][y][z]};
V3D<float> endColor{this->endFrame[x][y][z]};
V3D<float> difference{endColor};
difference -= startColor;
V3D<float> interpolatedColor{difference};
interpolatedColor *= this->timeElapsed.count();
interpolatedColor /= delayTime.count();
interpolatedColor += startColor;
this->interpolatedFrame[x][y][z] = interpolatedColor;
}
}
}
this->interpolatedFrameHasChanged = true;
}
template <const V3D<uint32_t> &BOARD_DIMS>
void Animator<BOARD_DIMS>::SetLoop(bool isLooping){
this->isLooping = isLooping;
}
template <const V3D<uint32_t> &BOARD_DIMS>
void Animator<BOARD_DIMS>::incrimentAnimationIndex(){
if(this->animationIndex < this->animationSequence->size() - 2){
this->animationIndex++;
this->timeElapsed = std::chrono::milliseconds(0);
this->uncompressFrame((*this->animationSequence)[this->animationIndex], this->startFrame);
this->copyFrame(this->startFrame, this->interpolatedFrame);
this->uncompressFrame((*this->animationSequence)[this->animationIndex + 1], this->endFrame);
}
else{
this->StartAnimation(this->animationSequence);
}
this->interpolatedFrameHasChanged = true;
}
template <const V3D<uint32_t> &BOARD_DIMS>
void Animator<BOARD_DIMS>::uncompressFrame(const AnimationFrame &keyFrame, Frame &frameBuffer){
for(uint32_t x = 0; x < BOARD_DIMS.x; x++){
for(uint32_t y = 0; y < BOARD_DIMS.y; y++){
for(uint32_t z = 0; z < BOARD_DIMS.z; z++){
frameBuffer[x][y][z] = getInterpolatedColor(keyFrame, V3D<uint32_t>(x, y, z));
}
}
}
}
template <const V3D<uint32_t> &BOARD_DIMS>
V3D<uint32_t> Animator<BOARD_DIMS>::getInterpolatedColor(const AnimationFrame &keyFrame, V3D<uint32_t> position){
switch(keyFrame.fillInterpolation){
case FillInterpolation::NO_FILL:
return noFillInterpolate(keyFrame, position);
case FillInterpolation::CLOSEST_COLOR:
return closestColorInterpolate(keyFrame, position);
case FillInterpolation::LINEAR_WEIGHTED_DISTANCE:
return linearInterpolate(keyFrame, position);
case FillInterpolation::SQUARE_WEIGHTED_DISTANCE:
return squareInterpolate(keyFrame, position);
default:
V3D<uint32_t> black{};
return black;
}
}
template <const V3D<uint32_t> &BOARD_DIMS>
V3D<uint32_t> Animator<BOARD_DIMS>::keyFrame2BoardCoords(const V3D<uint32_t> &keyFramePosition){
V3D<uint32_t> returnValue{};
float maxValue{static_cast<float>(std::numeric_limits<uint32_t>::max())};
// scale the key frame values down to be within board coordinates
float keyFrame_X = static_cast<float>(BOARD_DIMS.x - 1) * static_cast<float>(keyFramePosition.x) / maxValue;
float keyFrame_Y = static_cast<float>(BOARD_DIMS.y - 1) * static_cast<float>(keyFramePosition.y) / maxValue;
float keyFrame_Z = static_cast<float>(BOARD_DIMS.z - 1) * static_cast<float>(keyFramePosition.z) / maxValue;
// carefully quantize the float values back into ints with a precise rounding operation
if(keyFrame_X - std::floor(keyFrame_X) < 0.5f){
returnValue.x = static_cast<uint32_t>(keyFrame_X);
}
else{
returnValue.x = static_cast<uint32_t>(keyFrame_X) + 1;
}
if(keyFrame_Y - std::floor(keyFrame_Y) < 0.5f){
returnValue.y = static_cast<uint32_t>(keyFrame_Y);
}
else{
returnValue.y = static_cast<uint32_t>(keyFrame_Y) + 1;
}
if(keyFrame_Z - std::floor(keyFrame_Z) < 0.5f){
returnValue.z = static_cast<uint32_t>(keyFrame_Z);
}
else{
returnValue.z = static_cast<uint32_t>(keyFrame_Z) + 1;
}
return returnValue;
}
template <const V3D<uint32_t> &BOARD_DIMS>
V3D<uint32_t> Animator<BOARD_DIMS>::noFillInterpolate(const AnimationFrame &keyFrame, V3D<uint32_t> position){
V3D<uint32_t> returnColor{};
for(Cell cell : keyFrame.frame){
if(keyFrame2BoardCoords(cell.position) == position){
returnColor = cell.color;
}
}
return returnColor;
}
template <const V3D<uint32_t> &BOARD_DIMS>
V3D<uint32_t> Animator<BOARD_DIMS>::closestColorInterpolate(const AnimationFrame &keyFrame, V3D<uint32_t> cubePosition){
V3D<uint32_t> returnColor{keyFrame.frame[0].color};
V3D<uint32_t> distance{keyFrame.frame[0].position};
distance -= cubePosition;
float closestDistance = distance.magnitude();
for(Cell cell : keyFrame.frame){
distance = keyFrame2BoardCoords(cell.position);
distance -= cubePosition;
float euclidDistance = distance.magnitude();
if(euclidDistance < closestDistance){
returnColor = cell.color;
closestDistance = euclidDistance;
}
}
return returnColor;
}
template <const V3D<uint32_t> &BOARD_DIMS>
V3D<uint32_t> Animator<BOARD_DIMS>::linearInterpolate(const AnimationFrame &keyFrame, V3D<uint32_t> position){
V3D<uint32_t> returnColor{};
for(Cell cell : keyFrame.frame){
V3D<uint32_t> vectorDistance{keyFrame2BoardCoords(cell.position)};
vectorDistance -= position;
float distance = vectorDistance.magnitude();
if(distance == 0) return cell.color;
returnColor += cell.color;
returnColor /= distance;
}
returnColor /= keyFrame.frame.size();
return returnColor;
}
template <const V3D<uint32_t> &BOARD_DIMS>
V3D<uint32_t> Animator<BOARD_DIMS>::squareInterpolate(const AnimationFrame &keyFrame, V3D<uint32_t> position){
V3D<uint32_t> returnColor{};
for(Cell cell : keyFrame.frame){
V3D<uint32_t> vectorDistance{keyFrame2BoardCoords(cell.position)};
vectorDistance -= position;
uint32_t distance = static_cast<uint32_t>(vectorDistance.magnitude());
distance *= distance;
if(distance == 0) return cell.color;
returnColor += cell.color;
returnColor /= distance;
}
returnColor /= keyFrame.frame.size();
return returnColor;
}

169
lib/Animator/TestFrames.h Normal file
View File

@@ -0,0 +1,169 @@
#pragma once
#include "AnimationTypes.h"
#include "Vector3D.h"
#include "Animator.h"
using namespace ANIMATION_TYPES;
namespace TestFrames{
V3D<uint32_t> red{255,0,0};
V3D<uint32_t> green{0,255,0};
V3D<uint32_t> blue{0,0,255};
uint32_t maxValue{std::numeric_limits<uint32_t>::max()};
Cell CreateCell(float x_percent, float y_percent, float z_percent, V3D<uint32_t> &color){
float continuousMaxValue{static_cast<float>(std::numeric_limits<uint32_t>::max())};
Cell cell{
.position = V3D<uint32_t>{
static_cast<uint32_t>(continuousMaxValue*x_percent),
static_cast<uint32_t>(continuousMaxValue*y_percent),
static_cast<uint32_t>(continuousMaxValue*z_percent)
},
.color = color
};
return cell;
}
AnimationFrame noFillFrame{
.frame = {
CreateCell(0,0,0,red),
CreateCell(0.5,0.5,0.5,green),
CreateCell(1,1,1,blue)
},
.fillInterpolation = FillInterpolation::NO_FILL,
.frameInterpolation = FrameInterpolation::SNAP,
.delay = std::chrono::milliseconds(10000)
};
AnimationFrame closestColorFrame{
.frame = {
CreateCell(0,0,0,red),
CreateCell(0.5,0.5,0.5,green),
CreateCell(1,1,1,blue)
},
.fillInterpolation = FillInterpolation::CLOSEST_COLOR,
.frameInterpolation = FrameInterpolation::SNAP,
.delay = std::chrono::milliseconds(10000)
};
AnimationFrame linearFillFrame{
.frame = {
CreateCell(0,0,0,red),
CreateCell(0.5,0.5,0.5,green),
CreateCell(1,1,1,blue)
},
.fillInterpolation = FillInterpolation::LINEAR_WEIGHTED_DISTANCE,
.frameInterpolation = FrameInterpolation::SNAP,
.delay = std::chrono::milliseconds(10000)
};
AnimationFrame squareFillFrame{
.frame = {
CreateCell(0,0,0,red),
CreateCell(0.5,0.5,0.5,green),
CreateCell(1,1,1,blue)
},
.fillInterpolation = FillInterpolation::SQUARE_WEIGHTED_DISTANCE,
.frameInterpolation = FrameInterpolation::SNAP,
.delay = std::chrono::milliseconds(10000)
};
AnimationFrame noFillFadeFrame{
.frame = {
CreateCell(0,0,0,red),
CreateCell(0.5,0.5,0.5,green),
CreateCell(1,1,1,blue)
},
.fillInterpolation = FillInterpolation::NO_FILL,
.frameInterpolation = FrameInterpolation::FADE,
.delay = std::chrono::milliseconds(10000)
};
AnimationFrame closestColorFadeFrame{
.frame = {
CreateCell(0,0,0,red),
CreateCell(0.5,0.5,0.5,green),
CreateCell(1,1,1,blue)
},
.fillInterpolation = FillInterpolation::CLOSEST_COLOR,
.frameInterpolation = FrameInterpolation::FADE,
.delay = std::chrono::milliseconds(10000)
};
AnimationFrame linearFillFadeFrame{
.frame = {
CreateCell(0,0,0,red),
CreateCell(0.5,0.5,0.5,green),
CreateCell(1,1,1,blue)
},
.fillInterpolation = FillInterpolation::LINEAR_WEIGHTED_DISTANCE,
.frameInterpolation = FrameInterpolation::FADE,
.delay = std::chrono::milliseconds(10000)
};
AnimationFrame squareFillFadeFrame{
.frame = {
CreateCell(0,0,0,red),
CreateCell(0.5,0.5,0.5,green),
CreateCell(1,1,1,blue)
},
.fillInterpolation = FillInterpolation::SQUARE_WEIGHTED_DISTANCE,
.frameInterpolation = FrameInterpolation::FADE,
.delay = std::chrono::milliseconds(10000)
};
std::vector<AnimationFrame> testAnimationSequence2{
noFillFrame, // 0
closestColorFrame, // 1
linearFillFrame, // 2
squareFillFrame, // 3
noFillFadeFrame, // 4
closestColorFadeFrame, // 5
linearFillFadeFrame, // 6
squareFillFadeFrame, // 7
noFillFrame // 8
};
AnimationFrame testFrame1{
.frame = {
CreateCell(0,0,0,red),
// CreateCell(0.5,0.5,0,green),
CreateCell(1,1,0,blue)
},
.fillInterpolation = FillInterpolation::NO_FILL,
.frameInterpolation = FrameInterpolation::FADE,
.delay = std::chrono::milliseconds(10000)
};
AnimationFrame testFrame2{
.frame = {
CreateCell(0,1,0,red),
// CreateCell(0.5,0.5,0,green),
CreateCell(1,0,0,green)
},
.fillInterpolation = FillInterpolation::NO_FILL,
.frameInterpolation = FrameInterpolation::FADE,
.delay = std::chrono::milliseconds(10000)
};
AnimationFrame testFrame3{
.frame = {
CreateCell(0.5,0.5,0,red),
// CreateCell(0.5,0.5,0,green),
CreateCell(0,1,0,blue)
},
.fillInterpolation = FillInterpolation::NO_FILL,
.frameInterpolation = FrameInterpolation::FADE,
.delay = std::chrono::milliseconds(10000)
};
std::vector<AnimationFrame> testAnimationSequence1{
testFrame1,
testFrame2,
testFrame3,
testFrame1
};
}

205
lib/Board/Board.h Normal file
View File

@@ -0,0 +1,205 @@
/**
* @file Board.h
* @brief The Board class is meant to store data about each cube on the board and
* provide an easy way to organize and access that data in a 3-dimensional context.
*/
#pragma once
#include <cstdint>
#include <array>
#include <WString.h>
#include "BoardTypes.h"
#include "Vector3D.h"
template <const V3D<uint32_t> &BOARD_DIMS>
class Board{
public:
Board() = default;
~Board() = default;
constexpr const V3D<uint32_t> &GetSize() const{return BOARD_DIMS;}
constexpr uint32_t GetNumberCubes() const{return BOARD_DIMS.x * BOARD_DIMS.y * BOARD_DIMS.z;}
constexpr uint32_t GetMaxDimension(){return std::max(std::max(BOARD_DIMS.x, BOARD_DIMS.y), BOARD_DIMS.z);}
/**
* @brief Returns a string in the format:
* !a,b,c,d,e,f,g,h,i;
* Where each letter is the number of cubes in a given position's stack
* @param stringBuffer the buffer to write the string into
*/
void ToStackString(String& stringBuffer) const;
/**
* @brief fill the entire board with the given color
* @param color the color to fill the board with
*/
void FillColor(const V3D<uint32_t> &color);
/**
* @brief Set the color of the cube at the given position.
* Even if the cube is not occupied, it will remember this color
* and turn that color if it becomes occupied.
* @param position the position of the cube.
* @param color the color you want the cube to be
*/
void SetCubeColor(const V3D<uint32_t> &position, const V3D<uint32_t> &color);
/**
* @brief Set the occupation status of the cube at a given position
* @param position the position of the cube
* @param the occupation status of the cube
* @post if the new occupation status of the cube is different than
* the old occupation status, this will enable boardStateHasChanged.
*/
void SetCubeOccupation(const V3D<uint32_t> &position, bool occupation);
/**
* @returns true if the board state has changed since this flag was last set to false
*/
bool BoardStateChanged(){return this->boardStateHasChanged;}
/**
* @brief set the board state status.
* @note you should probably only use this to clear the baordState flag
*/
void SetStateChanged(bool boardState){this->boardStateHasChanged = boardState;}
/**
* @brief Get a column along any axis read into the sliceBuffer
* @param column .z specifies the normal direction of the plane (see PLANE_NORMAL), and
* the x,y values specify the location of the column in that plane
* to fill. IE To fill one stack at 0,2 I would say give V3D<uint32_t>(0,2,PLANE_NORMAL::Z)
* @param sliceBuffer an array of pointers to the cubes along that column
* @returns the number of elements written into the slice buffer
* @note That array is stored locally and will be overwritten everytime this function is called.
* Also, any unused spots at the end of the array will be nullptrs
* @warning allocate the size of the slice buffer using GetMaxDimension if you don't know what you're doing!
*/
uint32_t SliceBoard(const V3D<uint32_t> &column, BOARD_TYPES::Cube ** sliceBuffer);
void PrintEntireBoard() const;
void UpdateAllColors(const std::array<std::array<std::array<V3D<uint32_t>, BOARD_DIMS.z>, BOARD_DIMS.y>, BOARD_DIMS.x>& colorFrame){
for(uint32_t x = 0; x < BOARD_DIMS.x; x++){
for(uint32_t y = 0; y < BOARD_DIMS.y; y++){
for(uint32_t z = 0; z < BOARD_DIMS.z; z++){
this->cubes[x][y][z].color = colorFrame[x][y][z];
}
}
}
}
private:
// this is a 3d array of cubes to represent the board. Good luck visualizing it
/* _____________
/ /|
/ / |
/ / |
/____________/ |
| | |
| | /
| | /
|____________|/
*/
std::array<std::array<std::array<BOARD_TYPES::Cube, BOARD_DIMS.z>, BOARD_DIMS.y>, BOARD_DIMS.x> cubes;
bool boardStateHasChanged;
};
template <const V3D<uint32_t> &BOARD_DIMS>
void Board<BOARD_DIMS>::ToStackString(String &stringBuffer) const{
std::array<uint32_t, BOARD_DIMS.x * BOARD_DIMS.y> linearizedBoard;
for(uint32_t x{0}; x < BOARD_DIMS.x; x++){
for(uint32_t y{0}; y < BOARD_DIMS.y; y++){
uint32_t boardIndex{x + y*3};
linearizedBoard[boardIndex] = 0;
for(uint32_t z{0}; z < BOARD_DIMS.z; z++){
linearizedBoard[boardIndex] += this->cubes[x][y][z].isOccupied;
}
}
}
stringBuffer += String(linearizedBoard[0]);
for(uint32_t i = 1; i < BOARD_DIMS.x * BOARD_DIMS.y; i++){
stringBuffer += "," + String(linearizedBoard[i]);
}
}
template <const V3D<uint32_t> &BOARD_DIMS>
void Board<BOARD_DIMS>::FillColor(const V3D<uint32_t> &color){
for(uint32_t x{0}; x < BOARD_DIMS.x; x++){
for(uint32_t y{0}; y < BOARD_DIMS.y; y++){
for(uint32_t z{0}; z < BOARD_DIMS.z; z++){
this->cubes[x][y][z].color = color;
}
}
}
}
template <const V3D<uint32_t> &BOARD_DIMS>
void Board<BOARD_DIMS>::SetCubeColor(const V3D<uint32_t> &position, const V3D<uint32_t> &color){
this->cubes[position.x][position.y][position.z].color = color;
}
template <const V3D<uint32_t> &BOARD_DIMS>
void Board<BOARD_DIMS>::SetCubeOccupation(const V3D<uint32_t> &position, bool occupation){
bool oldOccupation{this->cubes[position.x][position.y][position.z].isOccupied};
this->cubes[position.x][position.y][position.z].isOccupied = occupation;
if(occupation != oldOccupation) this->boardStateHasChanged = true;
}
template <const V3D<uint32_t> &BOARD_DIMS>
uint32_t Board<BOARD_DIMS>::SliceBoard(const V3D<uint32_t> &column, BOARD_TYPES::Cube ** sliceBuffer){
uint32_t columnLength{0};
V3D<uint32_t> indexIncrimentVector{};
V3D<uint32_t> indexVector{};
switch(column.z){
case BOARD_TYPES::PLANE_NORMAL::X:
columnLength = BOARD_DIMS.x;
indexIncrimentVector.x = 1;
indexVector.z = column.x;
indexVector.y = column.y;
break;
case BOARD_TYPES::PLANE_NORMAL::Y:
columnLength = BOARD_DIMS.y;
indexIncrimentVector.y = 1;
indexVector.x = column.x;
indexVector.z = column.y;
break;
default:
case BOARD_TYPES::PLANE_NORMAL::Z:
columnLength = BOARD_DIMS.z;
indexIncrimentVector.z = 1;
indexVector.x = column.x;
indexVector.y = column.y;
break;
}
for(uint32_t i = 0; i < columnLength; i++){
if(indexVector.x >= BOARD_DIMS.x || indexVector.y >= BOARD_DIMS.y || indexVector.z >= BOARD_DIMS.z){
Serial.println("Board::SliceBoard: Index Out of Bounds:" + String(indexVector.x) + "," + String(indexVector.y) + "," + String(indexVector.z));
return 0;
}
sliceBuffer[i] = &(this->cubes[indexVector.x][indexVector.y][indexVector.z]);
indexVector += indexIncrimentVector;
}
return columnLength;
}
template <const V3D<uint32_t> &BOARD_DIMS>
void Board<BOARD_DIMS>::PrintEntireBoard() const{
for(uint32_t x = 0; x < BOARD_DIMS.x; x++){
for(uint32_t y = 0; y < BOARD_DIMS.y; y++){
for(uint32_t z = 0; z < BOARD_DIMS.z; z++){
const BOARD_TYPES::Cube &cube = this->cubes[x][y][z];
Serial.print("Cube X:" + String(x) + ",Y:" + String(y) + ",Z:" + String(z));
Serial.print("\tColor R:" + String(cube.color.x) + ",G:" + String(cube.color.y) + ",B:" + String(cube.color.z));
Serial.println("\tOccupied? " + String(cube.isOccupied));
}
}
}
}

187
lib/Board/BoardDriver.h Normal file
View File

@@ -0,0 +1,187 @@
/**
* @file BoardDriver.h
* @brief the board driver provides helpful methods to read the physical board's state,
* filter those results, and send color data to the board
*/
#pragma once
#include <cstdint>
#include <array>
#include <Adafruit_NeoPixel.h>
#include <Arduino.h>
#include "BoardTypes.h"
#include "BoardTypes.h"
template <uint32_t NUM_STACKS>
class BoardDriver{
public:
/**
* @param stacks a reference to an array which contains information about the pins which control each cube stack.
* @param pixelController a reference to a pre-constructed neopixel controller which will allow this driver to control neopixels
*/
BoardDriver(
std::array<BOARD_TYPES::CubeStack, NUM_STACKS> &stacks,
Adafruit_NeoPixel &pixelController
);
~BoardDriver() = default;
/**
* @brief Initializes all of the pins necessary to read/write to the board
*/
void Init();
/**
* @param numXStacks the width of the board along the x dimension.
* @param X_COORD the x coordinate of the stack you want information about
* @param Y_COORD the y coordinate of the stack you want information about
* @returns Get the number of cubes present in a stack
*/
uint32_t GetNumberCubes(uint32_t numXStacks, uint32_t X_COORD, uint32_t Y_COORD);
/**
* @param stackIndex the index of the stack that you want to know the height of
* @returns Get the number of cubes present in a stack
*/
uint32_t GetNumberCubes(uint32_t stackIndex);
/**
* @brief stream data to a single cube stack on the board
* @param numXStacks the width of the board along the x dimension.
* @param X_COORD the x coordinate of the stack you want information about
* @param Y_COORD the y coordinate of the stack you want information about
* @param cubes an array of cube pointers which contain the color data you want to stream
* @param numCubes the number of cubes in the cubes array
*/
void UpdateStackLEDs(
uint32_t numXStacks,
uint32_t X_COORD,
uint32_t Y_COORD,
BOARD_TYPES::Cube* cubes[],
uint32_t numCubes
);
/**
* @brief stream data to a single cube stack on the board
* @param stackIndex the index of the stack you want to update the color of
* @param cubes an array of cube pointers which contain the color data you want to stream
* @param numCubes the number of cubes in the cubes array
*/
void UpdateStackLEDs(
uint32_t stackIndex,
BOARD_TYPES::Cube* cubes[],
uint32_t numCubes
);
private:
std::array<BOARD_TYPES::CubeStack, NUM_STACKS> &stacks;
Adafruit_NeoPixel &pixelController;
std::array<uint16_t, NUM_STACKS> filteredReadings;
uint32_t xy2StackIndex(uint32_t x_coord, uint32_t y_coord, uint32_t numXStacks){
return x_coord + y_coord*numXStacks;
}
};
template<uint32_t NUM_STACKS>
void BoardDriver<NUM_STACKS>::Init(){
for(uint32_t i = 0; i < NUM_STACKS; i++){
pinMode(this->stacks[i].ledPin, OUTPUT);
filteredReadings[i] = 0;
}
// begin doesn't really do anything besides setting the pinmode
this->pixelController.begin();
}
template<uint32_t NUM_STACKS>
BoardDriver<NUM_STACKS>::BoardDriver(
std::array<BOARD_TYPES::CubeStack, NUM_STACKS> &stacks,
Adafruit_NeoPixel &pixelController
):
stacks(stacks),
pixelController(pixelController)
{
for(uint32_t i = 0; i < NUM_STACKS; i++){
this->filteredReadings[i] = 0;
}
}
template<uint32_t NUM_STACKS>
void BoardDriver<NUM_STACKS>::UpdateStackLEDs(
uint32_t numXStacks,
uint32_t X_COORD,
uint32_t Y_COORD,
BOARD_TYPES::Cube* cubes[],
uint32_t numCubes
){
this->UpdateStackLEDs(this->xy2StackIndex(X_COORD, Y_COORD, numXStacks), cubes, numCubes);
}
template<uint32_t NUM_STACKS>
void BoardDriver<NUM_STACKS>::UpdateStackLEDs(
uint32_t stackIndex,
BOARD_TYPES::Cube* cubes[],
uint32_t numCubes
){
this->pixelController.setPin(this->stacks[stackIndex].ledPin);
for(int i = 0; i < numCubes; i++){
V3D<uint32_t> color{cubes[i]->color};
this->pixelController.setPixelColor(i*2, this->pixelController.Color(color.x, color.y, color.z));
this->pixelController.setPixelColor((i*2 + 1), this->pixelController.Color(color.x, color.y, color.z));
}
this->pixelController.show();
}
template<uint32_t NUM_STACKS>
uint32_t BoardDriver<NUM_STACKS>::GetNumberCubes(
uint32_t numXStacks,
uint32_t X_COORD,
uint32_t Y_COORD
){
return this->GetNumberCubes(this->xy2StackIndex(X_COORD, Y_COORD, numXStacks));
}
template<uint32_t NUM_STACKS>
uint32_t BoardDriver<NUM_STACKS>::GetNumberCubes(uint32_t stackIndex){
// read the ADC and return the number of cubes
/*
0 cubes: 1 : 4095-3400
1 cube: 1/2 3400-2500
2 cubes: 1/3 2500-1850
3 cubes: 1/4 1850-0
*/
uint16_t value = analogRead(this->stacks[stackIndex].adcPin);
uint16_t lowPassADCRead =
static_cast<uint16_t>(
(static_cast<float>(this->filteredReadings[stackIndex]) * 0.9)
+ (static_cast<float>(value) * 0.1)
);
// temporary definitions to define value ranges:
uint16_t zeroCubesHigh = 4095;
uint16_t zeroCubesLow = 3400;
uint16_t oneCubeLow = 2500;
uint16_t twoCubesLow = 1850;
uint16_t threeCubesLow = 0;
uint8_t stackHeight = 0;
if(lowPassADCRead >= zeroCubesLow && lowPassADCRead <= zeroCubesHigh){
stackHeight = 0;
}
else if(lowPassADCRead >= oneCubeLow){
stackHeight = 1;
}
else if(lowPassADCRead >= twoCubesLow){
stackHeight = 2;
}
else if(lowPassADCRead >= threeCubesLow){
stackHeight = 3;
}
this->filteredReadings[stackIndex] = lowPassADCRead;
return stackHeight;
}

View File

@@ -1,38 +0,0 @@
#include "BoardLayout.h"
uint8_t BoardLayout::GetNumberStacks(){
return this->boardWidth * this->boardHeight;
}
void BoardLayout::SetStackColors(uint8_t stackNum, Color * colors){
CubeStack * stack = this->stacks[stackNum];
stack->SetLEDColors(colors, this->boardHeight);
}
bool BoardLayout::BoardStateHasChanged(){
uint16_t boardState[this->boardWidth * this->boardLength];
this->GetBoardState(boardState);
// compare the board state to the last board state
for(int i = 0; i < (this->boardWidth * this->boardLength); i++){
uint16_t stackState = boardState[i];
uint16_t lastStackState = (this->lastBoardState)[i];
if(stackState != lastStackState){
// copy the board state into the last board state
for(int k = 0; k < (this->boardWidth * this->boardLength); k++){
this->lastBoardState[k] = boardState[k];
}
return true;
}
}
return false;
}
void BoardLayout::GetBoardState(uint16_t * boardStateBuffer){
for(int i = 0; i < (this->boardLength * this->boardWidth); i++){
CubeStack * stack = this->stacks[i];
stack->SendLEDData(); // Enable this if you want to constantly stream LED data
boardStateBuffer[i] = stack->GetNumberCubes();
}
}

View File

@@ -1,69 +0,0 @@
/**
* @brief This is the full board manager which handles the state of every stack on the board
*/
#pragma once
#include "CubeStack.h"
#include "Color.h"
class BoardLayout{
public:
/**
* @brief BoardLayout COnstructor
*/
BoardLayout(uint8_t boardWidth, uint8_t boardLength, uint8_t boardHeight, CubeStack ** stacks) :
boardWidth(boardWidth),
boardLength(boardLength),
boardHeight(boardHeight),
stacks(stacks)
{
this->lastBoardState = new uint16_t[boardWidth * boardLength];
}
/**
* @brief Check if our board state has changed
* @return true if the board state has changed, false otherwise
*/
bool BoardStateHasChanged();
/**
* @brief Get the Number of Stacks
* @return the number of stacks
*/
uint8_t GetNumberStacks();
/**
* @brief Set the LED Colors
* @param stackNum the stack index you would like to address.
* From top left to bottom right, the stack numbers are as follows:
* | 0 1 2 |
* | 3 4 5 |
* | 6 7 8 |
* @param Colors the array of colors to set the LEDs in a stack to
*/
void SetStackColors(uint8_t stackNum, Color * colors);
/**
* @brief Get the board population state
* @param boardStateBuffer the buffer to write the board state to. It must be at least boardWidth * boardLength in length
*/
void GetBoardState(uint16_t * boardStateBuffer);
private:
uint8_t boardWidth;
uint8_t boardLength;
uint8_t boardHeight;
/*
An array of arrays of stacks
[ [stack1, stack2, stack3],
[stack4, stack5, stack6],
[stack7, stack8, stack9] ]
etc
*/
CubeStack ** stacks;
// records the last known board state
uint16_t * lastBoardState;
};

197
lib/Board/BoardManager.h Normal file
View File

@@ -0,0 +1,197 @@
/**
* @file BoardManager.h
* @brief Provides an easy way to manage a board
*/
#pragma once
#include "Board.h"
#include "BoardDriver.h"
#include "Vector3D.h"
#include "Animator.h"
template <const V3D<uint32_t> &BOARD_DIMS>
class BoardManager{
public:
BoardManager(BoardDriver<BOARD_WIDTH*BOARD_LENGTH> &boardDriver, Animator<BOARD_DIMS> &animator);
~BoardManager() = default;
/**
* @brief Initialize the board control. This hsould be called during setup
*/
void Init();
/**
* @brief Read the board state and update the LED colors
*/
void Update();
/**
* @brief Set the color of one cube
* @param position the position of the cube
* @param color the oclor you want the cube to be
*/
void SetCubeColor(const V3D<uint32_t> &position, const V3D<uint32_t> &color);
/**
* @brief Set the color of one column of cubes.
* A column is defined as x,y,z where z is the normal vector direction to a plane.
* x,y is the column coordinate on the plane.
* @param column the column vector
* @param color the color you want the column to be
*/
void SetColumnColors(const V3D<uint32_t> &column, const V3D<uint32_t> *color, uint32_t numColors);
/**
* @brief Fill a column along any axis with a color
* @param column .z specifies the normal direction of the plane (see PLANE_NORMAL), and
* the x,y values specify the location of the column in that plane
* to fill. IE To fill one stack at 0,2 I would say give V3D<uint32_t>(0,2,PLANE_NORMAL::Z)
* @param color the color you want to fill the column with
*/
void FillColumnColor(const V3D<uint32_t> &column, const V3D<uint32_t> &color);
/**
* @returns true if the board has changed state
*/
bool HasBoardChanged();
/**
* @brief Clear the board state changed flag
*/
void ClearBoardChanged();
/**
* @brief Get the board occupation state returned in the format a,b,c,d....
*/
void Board2StackString(String& messageBuffer);
void FillColor(const V3D<uint32_t> &color){this->board.FillColor(color);}
void PrintColorState(){this->board.PrintEntireBoard();}
private:
BoardDriver<BOARD_WIDTH*BOARD_LENGTH> &driver;
Board<BOARD_DIMS> board{};
Animator<BOARD_DIMS> &animator;
void updateStackColors(const V3D<uint32_t> &column);
uint32_t getColumnHeight(BOARD_TYPES::PLANE_NORMAL normal){
switch(normal){
case BOARD_TYPES::PLANE_NORMAL::X:
return BOARD_DIMS.x;
break;
case BOARD_TYPES::PLANE_NORMAL::Y:
return BOARD_DIMS.y;
break;
case BOARD_TYPES::PLANE_NORMAL::Z:
return BOARD_DIMS.z;
break;
default:
return 0;
}
}
void updateColorsFromAnimator();
};
template <const V3D<uint32_t> &BOARD_DIMS>
void BoardManager<BOARD_DIMS>::updateColorsFromAnimator(){
if(this->animator.interpolatedFrameHasChanged){
this->board.UpdateAllColors(this->animator.GetInterpolatedFrame());
this->animator.interpolatedFrameHasChanged = false;
}
}
template <const V3D<uint32_t> &BOARD_DIMS>
BoardManager<BOARD_DIMS>::BoardManager(BoardDriver<BOARD_WIDTH*BOARD_LENGTH> &boardDriver, Animator<BOARD_DIMS> &animator):
driver(boardDriver),
animator(animator){}
template <const V3D<uint32_t> &BOARD_DIMS>
void BoardManager<BOARD_DIMS>::Init(){
this->driver.Init();
}
template <const V3D<uint32_t> &BOARD_DIMS>
void BoardManager<BOARD_DIMS>::Update(){
this->updateColorsFromAnimator();
// update the occupied cubes on the board and the cube colors
for(uint32_t x = 0; x < BOARD_DIMS.x; x++){
for(uint32_t y = 0; y < BOARD_DIMS.y; y++){
uint32_t stackIndex{y * BOARD_DIMS.x + x};
uint32_t numCubes{this->driver.GetNumberCubes(stackIndex)};
for(uint32_t z = 0; z < BOARD_DIMS.z; z++){
V3D<uint32_t> cubePosition{x, y, z};
// update the cube's occupation
this->board.SetCubeOccupation(cubePosition, z < numCubes);
}
// create the column vector for the slice direction
V3D<uint32_t> sliceVector{x,y,BOARD_TYPES::PLANE_NORMAL::Z};
// create a cube slice array buffer
BOARD_TYPES::Cube* sliceBuffer[BOARD_DIMS.z];
// have the board slice get read into our buffer
this->board.SliceBoard(sliceVector, sliceBuffer);
// send the board slice to the driver to update its LED colors
this->driver.UpdateStackLEDs(stackIndex, sliceBuffer, BOARD_DIMS.z);
}
}
}
template <const V3D<uint32_t> &BOARD_DIMS>
void BoardManager<BOARD_DIMS>::updateStackColors(const V3D<uint32_t> &column){
// the only column type allowed here is z.
V3D<uint32_t> sliceVector{column.x, column.y, BOARD_TYPES::Z};
// create a buffer for slice board to write the cube slice into
BOARD_TYPES::Cube * cubeSlice[BOARD_DIMS.z];
this->board.SliceBoard(column, cubeSlice);
uint32_t numCubes{this->getColumnHeight(static_cast<BOARD_TYPES::PLANE_NORMAL>(column.z))};
this->driver.UpdateStackLEDs(BOARD_DIMS.x, cubeSlice, numCubes);
}
template <const V3D<uint32_t> &BOARD_DIMS>
void BoardManager<BOARD_DIMS>::SetCubeColor(const V3D<uint32_t> &position, const V3D<uint32_t> &color){
this->board.SetCubeColor(position, color);
V3D<uint32_t> slice{position.x, position.y, BOARD_TYPES::PLANE_NORMAL::Z};
this->updateStackColors(slice);
}
template <const V3D<uint32_t> &BOARD_DIMS>
void BoardManager<BOARD_DIMS>::SetColumnColors(const V3D<uint32_t> &column, const V3D<uint32_t> *color, uint32_t numColors){
uint32_t columnHeight{this->getColumnHeight(static_cast<BOARD_TYPES::PLANE_NORMAL>(column.z))};
// create a cube pointer buffer and store a board slice into it
BOARD_TYPES::Cube * slicedBoard[columnHeight];
uint32_t sliceLength{this->board.SliceBoard(column, slicedBoard)};
uint32_t maxIndex{std::min(numColors, columnHeight)};
for(uint32_t i = 0; i < maxIndex; i++){
slicedBoard[i]->color = color[i];
}
}
template <const V3D<uint32_t> &BOARD_DIMS>
void BoardManager<BOARD_DIMS>::FillColumnColor(const V3D<uint32_t> &column, const V3D<uint32_t> &color){
uint32_t columnHeight{this->getColumnHeight(column.z)};
V3D<uint32_t> colors[columnHeight];
for(uint32_t i = 0; i < columnHeight; i++){
colors[i] = color;
}
this->SetColumnColors(column, colors);
}
template <const V3D<uint32_t> &BOARD_DIMS>
bool BoardManager<BOARD_DIMS>::HasBoardChanged(){return this->board.BoardStateChanged();}
template <const V3D<uint32_t> &BOARD_DIMS>
void BoardManager<BOARD_DIMS>::ClearBoardChanged(){this->board.SetStateChanged(false);}
template <const V3D<uint32_t> &BOARD_DIMS>
void BoardManager<BOARD_DIMS>::Board2StackString(String& messageBuffer){
this->board.ToStackString(messageBuffer);
}

26
lib/Board/BoardTypes.h Normal file
View File

@@ -0,0 +1,26 @@
/**
* @file BoardTypes.h
* @brief Some types to that you'll need to define and control the board
*/
#pragma once
#include <cstdint>
#include "Vector3D.h"
namespace BOARD_TYPES{
struct CubeStack{
uint8_t adcPin;
uint8_t ledPin;
};
enum PLANE_NORMAL : uint32_t{
X = 0,
Y,
Z
};
struct Cube{
V3D<uint32_t> color;
bool isOccupied{false};
};
};

View File

@@ -1,21 +0,0 @@
/**
* @file Color.h
* @brief This file contains the color struct
*/
#pragma once
#include <Arduino.h>
// store a color
struct Color{
// create a constructor for this struct
Color(uint8_t red, uint8_t green, uint8_t blue) :
red(red),
green(green),
blue(blue)
{}
Color() = default;
uint8_t red{0};
uint8_t green{0};
uint8_t blue{0};
};

View File

@@ -1,79 +0,0 @@
#include "CubeStack.h"
CubeStack::CubeStack(uint16_t ADCPin, uint16_t ledPin, uint8_t numLEDs){
this->ADCPin = ADCPin;
this->blockLights = *(new Adafruit_NeoPixel(numLEDs*2, ledPin, NEO_GRB + NEO_KHZ800));
this->ledColors = new Color[numLEDs];
this->numLEDs = numLEDs;
// initialize the LED colors to off
for(int i = 0; i < numLEDs; i++){
this->ledColors[i] = *(new Color(0, 0, 0));
}
};
uint8_t CubeStack::GetNumberCubes(){
// read the ADC and return the number of cubes
/*
0 cubes: 1 : 4095-3400
1 cube: 1/2 3400-2500
2 cubes: 1/3 2500-1850
3 cubes: 1/4 1850-0
*/
uint16_t value = analogRead(this->ADCPin);
this->lowPassADCRead = static_cast<uint16_t>((static_cast<float>(this->lowPassADCRead) * 0.9) + (static_cast<float>(value) * 0.1));
if(this->lowPassADCRead < 2500 && false){
Serial.println("ADC Pin:" + String(this->ADCPin) + " Value: " + String(value) + " Low Pass: " + String(this->lowPassADCRead));
}
// temporary definitions to define value ranges:
uint16_t zeroCubesHigh = 4095;
uint16_t zeroCubesLow = 3400;
uint16_t oneCubeLow = 2500;
uint16_t twoCubesLow = 1850;
uint16_t threeCubesLow = 0;
uint8_t stackHeight = 0;
if(this->lowPassADCRead >= zeroCubesLow && this->lowPassADCRead <= zeroCubesHigh){
stackHeight = 0;
}
else if(this->lowPassADCRead >= oneCubeLow){
stackHeight = 1;
}
else if(this->lowPassADCRead >= twoCubesLow){
stackHeight = 2;
}
else if(this->lowPassADCRead >= threeCubesLow){
stackHeight = 3;
}
if(this->lastStackHeight != stackHeight){
this->lastStackHeight = stackHeight;
this->SendLEDData();
}
return stackHeight;
}
void CubeStack::SetLEDColors(Color * colors, uint8_t numColors){
// copy the colors into the ledColors array
for(int i = 0; i < numColors; i++){
this->ledColors[i].red = colors[i].red;
this->ledColors[i].green = colors[i].green;
this->ledColors[i].blue = colors[i].blue;
}
this->SendLEDData();
}
void CubeStack::SendLEDData(){
// we always initialize before we do anything because other CubeStacks could be hogging the hardware
// between our writes
this->blockLights.begin();
// set the LED colors
for(int i = 0; i < this->numLEDs; i++){
this->blockLights.setPixelColor(i*2, this->blockLights.Color(this->ledColors[i].red, this->ledColors[i].green, this->ledColors[i].blue));
this->blockLights.setPixelColor((i*2 + 1), this->blockLights.Color(this->ledColors[i].red, this->ledColors[i].green, this->ledColors[i].blue));
}
this->blockLights.show();
}

View File

@@ -1,52 +0,0 @@
/**
* @brief this manages a single cube stack and the lighting / detecting of how many cubes
*/
#pragma once
#include <Arduino.h>
#include <Adafruit_NeoPixel.h>
#include "Color.h"
class CubeStack{
public:
/**
* @brief Construct a new Cube Stack object
* @param ADCPin the pin that the ADC is connected to
* @param ledPin the pin that the LED is connected to
*/
CubeStack(uint16_t ADCPin, uint16_t ledPin, uint8_t numLEDs);
/**
* @brief Returns the number of cubes in the stack
* @return the number of cubes in the stack
*/
uint8_t GetNumberCubes();
/**
* @brief Set the led color array and then send the data to the LED strip
* @param colors the array of colors to set the LEDs to
* @param numColors the number of colors in the array
*/
void SetLEDColors(Color * colors, uint8_t numColors);
/**
* @brief sends the LED data to the LED strip
*/
void SendLEDData();
private:
uint8_t ADCPin;
// we will probably need a pointer to a fastled object here
Adafruit_NeoPixel blockLights;
uint16_t lowPassADCRead{0};
// store the Color of each LED
Color * ledColors;
uint8_t numLEDs;
uint8_t lastStackHeight{0};
};

View File

@@ -1,48 +0,0 @@
#include "ColorManager.h"
void ColorManager::Update(){
if(!(this->enabled)){
return;
}
// go through our colors and have them fade from r->g->b->r
for(uint8_t i = 0; i < 9; i++){
for(uint8_t j = 0; j < 3; j++){
Color * color = this->colors[i][j];
// fade from red to green
if(color->red > 0 && color->green >= 0 && color->blue == 0){
color->red--;
color->green++;
}
// fade from green to blue
else if(color->green > 0 && color->blue >= 0 && color->red == 0){
color->green--;
color->blue++;
}
// fade from blue to red
else if(color->blue > 0 && color->red >= 0 && color->green == 0){
color->blue--;
color->red++;
}
}
}
// set the colors
for(uint8_t i = 0; i < 9; i++){
Color temp_colors[3] = {*(this->colors[i][0]), *(this->colors[i][1]), *(this->colors[i][2])};
this->board->SetStackColors(i, temp_colors);
}
}
void ColorManager::Enable(bool enable){
this->enabled = enable;
if(this->enabled == false){
// set all the colors to black
Color black(0, 0, 0);
Color temp_colors[3] = {black, black, black};
// set the colors
for(uint8_t i = 0; i < 9; i++){
this->board->SetStackColors(i, temp_colors);
}
}
}

View File

@@ -1,44 +0,0 @@
/**
* @file ColorManager.h
* @brief Generate pretty colors for the board and make it do something when unity isn't controlling it
*/
#pragma once
#include "BoardLayout.h"
#include "Color.h"
class ColorManager{
public:
ColorManager(BoardLayout * board) :
board(board)
{}
/**
* @brief Allows the color manager to update the board colors
*/
void Update();
/**
* @brief Enables or disables the color manager
* @param enable true to enable, false to disable
*/
void Enable(bool enable);
private:
BoardLayout * board;
bool enabled{true};
Color * colors[9][3] = {
{new Color(255, 0, 0), new Color(0, 255, 0), new Color(0, 0, 255)},
{new Color(255, 0, 0), new Color(0, 255, 0), new Color(0, 0, 255)},
{new Color(255, 0, 0), new Color(0, 255, 0), new Color(0, 0, 255)},
{new Color(255, 0, 0), new Color(0, 255, 0), new Color(0, 0, 255)},
{new Color(255, 0, 0), new Color(0, 255, 0), new Color(0, 0, 255)},
{new Color(255, 0, 0), new Color(0, 255, 0), new Color(0, 0, 255)},
{new Color(255, 0, 0), new Color(0, 255, 0), new Color(0, 0, 255)},
{new Color(255, 0, 0), new Color(0, 255, 0), new Color(0, 0, 255)},
{new Color(255, 0, 0), new Color(0, 255, 0), new Color(0, 0, 255)}
};
};

View File

@@ -0,0 +1,28 @@
/**
* @file GlobalPrint.h
* @brief a method of printing serial data to all outgoing communication methods
*/
#pragma once
#include <Arduino.h>
namespace GlobalPrint{
static void Print(const char * s){
Serial.print(s);
}
static void Print(const String &s){
GlobalPrint::Print(s.c_str());
}
static void Println(const char * s){
GlobalPrint::Print(s);
GlobalPrint::Print("\n");
}
static void Println(const String &s){
GlobalPrint::Println(s.c_str());
}
}

View File

@@ -7,22 +7,23 @@
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[platformio]
default_envs = esp32s3_release ; this ensures that only this environment is built for anything but the debug build
default_envs = esp32s3_release
extra_configs = platformio-local.ini
[env]
; platform = espressif32
upload_protocol = esptool
platform = https://github.com/platformio/platform-espressif32.git
board = esp32-s3-devkitc-1
framework = arduino
build_flags = -Iinclude
; lib_ldf_mode = chain+
monitor_speed = 9600
monitor_filters = esp32_exception_decoder, colorize, send_on_enter
upload_speed = 2000000 ;ESP32S3 USB-Serial Converter maximum 2000000bps
lib_deps = adafruit/Adafruit NeoPixel@^1.12.0
monitor_filters = colorize, send_on_enter
upload_speed = 2000000
lib_deps =
adafruit/Adafruit NeoPixel@^1.12.3
fastled/FastLED@^3.7.3
[env:esp32s3_release]
@@ -33,6 +34,6 @@ debug_init_break = tbreak setup
debug_tool = esp-builtin
build_type = debug
debug_speed = 20000
; debug_port = COM7
; monitor_port = COM14
build_flags = -O1 -Iinclude
build_flags =
-D DEBUG = 1
-I include

View File

@@ -1,43 +1,58 @@
// Other peoples libraries
#include <Arduino.h>
#include <BluetoothSerial.h>
#include <FreeRTOS.h>
#include <Adafruit_NeoPixel.h>
#include <chrono>
#include <array>
// Static Defines
#include "PINOUT.h"
#include "BOARD-DEFINITIONS.h"
#include "COMMANDS.h"
// project specific libraries
#include "BluetoothSerial.h"
#include "SerialMessage.h"
#include "BoardLayout.h"
#include "BOARD-DEFINITIONS.h"
#include "Color.h"
#include "ColorManager.h"
#include "GlobalPrint.h"
// --------------------------------------------------
// ----------------- Types ----------------------
// --------------------------------------------------
enum Commands : uint8_t{
BoardState = 0,
PING = 1,
SetStackColors = 2,
GoToIdle = 3
};
#include "BoardManager.h"
#include "BoardDriver.h"
#include "BoardTypes.h"
#include "Animator.h"
#include "TestFrames.h"
#include "Animation.h"
// --------------------------------------------------
// ----------------- VARIABLES ----------------------
// --------------------------------------------------
uint32_t boardStateTimer{0};
bool boardStateHasChanged{false};
uint32_t boardStateMaxUpdatePeriod{34}; // this is a little slower than 30fps
TaskHandle_t updateCommunicaitonTask;
TaskHandle_t updateBoardTask;
// WARNING! This array size should always be equal to the number of entries in it!!
std::array<std::vector<AnimationFrame>*, 2> animations = {
&RisingCubes::rising,
&RotatingCubes::rotating,
};
// BluetoothSerial SerialBT;
// BluetoothSerialMessage serialMessageBT(&SerialBT);
SerialMessage<500> serialMessage(&Serial);
BoardLayout board(BOARD_WIDTH, BOARD_LENGTH, BOARD_HEIGHT, stacks);
SerialMessage<SERIAL_CHAR_LENGTH, SERIAL_ARG_LENGTH> serialMessage(&Serial);
// Temporary thing until we can get bluetooth color management working on the quest
ColorManager colorManager(&board);
Adafruit_NeoPixel pixelController{BOARD_HEIGHT*2, STACK1_LED_PIN, NEO_GRB + NEO_KHZ800};
Animator<BOARD_DIMENSIONS> animator{};
BoardDriver<BOARD_WIDTH*BOARD_LENGTH> boardDriver{stacks, pixelController};
BoardManager<BOARD_DIMENSIONS> boardManager{boardDriver, animator};
// --------------------------------------------------
// ----------------- FUNCTIONS ----------------------
// --------------------------------------------------
/**
* @brief Send programming commands to the serial to bluetooth adapter so
* it is set up as expected for the VR headset
* @post the serial baud rate will be set to 9600
*/
void SetupBluetoothModule(){
Serial.begin(38400);
Serial.print("AT+UART=9600,0,0\r\n"); // set baud rate to 9600
@@ -52,123 +67,169 @@ void SetupBluetoothModule(){
Serial.print("AT+ROLE=0\r\n"); // set to slave
delay(100);
// exit at mode and go into pairing mode
Serial.print("AT+INIT\r\n");
Serial.begin(9600);
delay(100);
}
void printBoardState(){
// create a buffer to hold the board state
uint16_t boardState[BOARD_WIDTH * BOARD_LENGTH];
// read in the board state
board.GetBoardState(boardState);
Serial.print("!0,");
// SerialBT.print("!0,");
for(int i = 0; i < (BOARD_WIDTH * BOARD_LENGTH); i++){
Serial.print(boardState[i]);
// SerialBT.print(boardState[i]);
if(i == (BOARD_WIDTH * BOARD_LENGTH) - 1){
break;
}
Serial.print(",");
// SerialBT.print(",");
}
Serial.println(";");
// SerialBT.println(";");
GlobalPrint::Print("!0,");
String boardString;
boardManager.Board2StackString(boardString);
GlobalPrint::Print(boardString);
GlobalPrint::Println(";");
}
void setStackColor(uint32_t * args, int argsLength){
void SetStackColor(uint32_t * args, int argsLength){
uint32_t stackNum = args[1];
uint32_t X_COORD{stackNum};
while(X_COORD > BOARD_DIMENSIONS.x - 1){
X_COORD -= BOARD_DIMENSIONS.x;
}
uint32_t Y_COORD{(stackNum - X_COORD) / BOARD_DIMENSIONS.y};
uint32_t numColors = (argsLength - 2) / 3;
Color colors[numColors];
V3D<uint32_t> colors[numColors];
for(int i = 0; i < numColors; i++){
int red = args[2 + (i * 3)];
int green = args[3 + (i * 3)];
int blue = args[4 + (i * 3)];
colors[i] = Color(red, green, blue);
uint32_t red = args[2 + (i * 3)];
uint32_t green = args[3 + (i * 3)];
uint32_t blue = args[4 + (i * 3)];
colors[i] = V3D<uint32_t>{red, green, blue};
}
board.SetStackColors(stackNum, colors);
boardManager.SetColumnColors(V3D<uint32_t>{X_COORD, Y_COORD, BOARD_TYPES::PLANE_NORMAL::Z}, colors, numColors);
}
void parseData(uint32_t * args, int argsLength){
void parseData(Message<SERIAL_CHAR_LENGTH, SERIAL_ARG_LENGTH> &message){
int32_t * args{message.GetArgs()};
uint32_t argsLength{message.GetPopulatedArgs()};
uint32_t command = args[0];
switch(command){
case Commands::BoardState:
case Commands::BoardState:{
printBoardState();
break;
case Commands::PING:
Serial.print("!");
// SerialBT.print("!");
Serial.print(Commands::PING);
// SerialBT.print(Commands::PING);
Serial.println(";");
// SerialBT.println(";");
break;
case Commands::SetStackColors:
Serial.println("!2;");
// SerialBT.println("!2;");
colorManager.Enable(false);
setStackColor(args, argsLength);
break;
case Commands::GoToIdle:
Serial.println("!3;");
// SerialBT.println("!3;");
colorManager.Enable(true);
break;
default:
Serial.println("INVALID COMMAND");
// SerialBT.println("INVALID COMMAND");
}
case Commands::PING:{
GlobalPrint::Println("!" + String(Commands::PING) + ";");
boardManager.PrintColorState();
break;
}
case Commands::SetStackColors:{
GlobalPrint::Println("!2;");
animator.isEnabled = false;
V3D<uint32_t> black{};
boardManager.FillColor(black);
SetStackColor(reinterpret_cast<uint32_t *>(args), argsLength);
break;
}
case Commands::GoToIdle:{
GlobalPrint::Println("!3;");
animator.isEnabled = true;
break;
}
default:{
GlobalPrint::Println("INVALID COMMAND");
break;
}
}
// now that we have run the command we can clear the data for the next command.
serialMessage.ClearNewData();
}
// --------------------------------------------------
// ----------------- SETUP AND LOOP -----------------
// ----------------- FREERTOS TASKS -----------------
// --------------------------------------------------
void setup() {
delay(1000);
SetupBluetoothModule();
Serial.begin(9600);
// SerialBT.begin("blockPartyBT");
Color colors[] = {Color(255, 0, 0), Color(0, 0, 0), Color(0, 0, 0)};
board.SetStackColors(2, colors);
boardStateTimer = millis();
}
void loop() {
if(board.BoardStateHasChanged()){
boardStateHasChanged = true;
}
if(millis() - boardStateTimer > boardStateMaxUpdatePeriod && boardStateHasChanged){
boardStateTimer = millis();
printBoardState();
boardStateHasChanged = false;
}
void UpdateCommunication(void * params){
Serial.println("Spawning UpdateCommunication task");
for(;;){
// DO serial processing
serialMessage.Update();
if(serialMessage.IsNewData()){
parseData(serialMessage.GetArgs(), serialMessage.GetArgsLength());
serialMessage.ClearNewData();
parseData(serialMessage);
}
// serialMessageBT.Update();
// if(serialMessageBT.IsNewData()){
// parseData(serialMessageBT.GetArgs(), serialMessageBT.GetArgsLength());
// serialMessageBT.ClearNewData();
// }
colorManager.Update();
vTaskDelay(3);
}
Serial.println("UpdateCommunication task has ended unexpectedly!");
}
void UpdateBoard(void * params){
Serial.println("Spawning UpdateBoard task");
auto updateTickRate{std::chrono::milliseconds(8)};
auto boardStateTimer{std::chrono::milliseconds(0)};
auto boardStateMaxUpdatePeriod{std::chrono::milliseconds(34)}; // this is a little slower than 30fps
unsigned long accurateTimer{millis()};
auto changeAnimationTimer{std::chrono::milliseconds(0)};
uint8_t currentAnimation{0};
for(;;){
auto actualTimePassed{std::chrono::milliseconds(millis() - accurateTimer)};
accurateTimer = millis();
if(boardStateTimer >= boardStateMaxUpdatePeriod && boardManager.HasBoardChanged()){
printBoardState();
boardManager.ClearBoardChanged();
boardStateTimer = std::chrono::milliseconds(0);
}
if(changeAnimationTimer >= std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::minutes(1))){
changeAnimationTimer = std::chrono::milliseconds(0);
currentAnimation++;
if(currentAnimation >= animations.size()){
currentAnimation = 0;
}
animator.StartAnimation(animations[currentAnimation]);
}
animator.RunAnimation(actualTimePassed);
boardManager.Update();
boardStateTimer += actualTimePassed;
changeAnimationTimer += actualTimePassed;
vTaskDelay(updateTickRate.count());
}
Serial.println("UpdateBoard task has ended unexpectedly!");
}
// --------------------------------------------------
// ----------------- SETUP AND LOOP -----------------
// --------------------------------------------------
void setup() {
// delay a little bit to get the serial monitor a chance to capture the next log messages.
delay(1000);
Serial.begin(9600);
Serial.println("Beginning Setup");
Serial.println("Configuring Bluetooth Adapter");
SetupBluetoothModule();
Serial.begin(9600);
Serial.println("Configuring communication methods");
serialMessage.Init(9600);
// SerialBT.begin("blockPartyBT");
xTaskCreate(UpdateCommunication, "UpdateCommunication", 10000, NULL, 0, &updateCommunicaitonTask);
Serial.println("Beginning Board Initializaiton");
boardManager.Init();
animator.SetLoop(true);
animator.StartAnimation(animations[0]);
xTaskCreate(UpdateBoard, "UpdateBoard", 10000, NULL, 0, &updateBoardTask);
Serial.println("Setup Complete");
}
void loop() {
// delete the loop task because we don't use it
vTaskDelete(NULL);
}

View File

@@ -0,0 +1,23 @@
; This is a template that will allow you to add additional configuration
; options to the platformio.ini file without changing anything in git.
; options like upload/monitor ports are great things to put here
; because those will vary from computer to computer.
; To use this configuration, make a copy of this file and rename it to "platformio-local.ini"
; and then uncomment some of the following code or add your own options
; Feel free to modify the code as needed
; this will add additional configuration options to esp32s3_release environment.
; This environment is the default for building/sending code to the baord
; so this should be the first thing you change/modify
; Uncomment the following code as needed:
; [env:esp32s3_release]
; monitor_port = COM10
; upload_port = COM11
; monitor_speed = 115200
; this is the debug configuration which you probably don't need
; unless you're doing some complicated firmware development.
; [env:esp32s3_debug]
; debug_port = COM7
; monitor_port = COM14
; upload_port = COM12

3
tools/animation-tools/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
venv/
__pycache__/
output.txt

View File

@@ -0,0 +1,78 @@
from Scene import Scene
from enum import StrEnum
from pygame.math import Vector3
from Shapes import Mesh
class FillInterpolation(StrEnum):
NO_FILL = "NO_FILL"
CLOSEST_COLOR = "CLOSEST_COLOR"
LINEAR_WEIGHTED_DISTANCE = "LINEAR_WEIGHTED_DISTANCE"
SQUARE_WEIGHTED_DISTANCE = "SQUARE_WEIGHTED_DISTANCE"
# options = [NO_FILL, CLOSEST_COLOR, LINEAR_WEIGHTED_DISTANCE, SQUARE_WEIGHTED_DISTANCE]
class FrameInterpolation(StrEnum):
SNAP = "SNAP"
FADE = "FADE"
# options = [SNAP, FADE]
class Cell:
def __init__(self, position: Vector3, color: Vector3):
self.position: Vector3 = position
self.color: Vector3 = color
def to_string(self):
message = f"CreateCell({self.position.x},{self.position.y},{self.position.z}"
message += f",V3D<uint8_t>({self.color.x},{self.color.y},{self.color.z})),"
return message
class AnimationFrame:
def __init__(self, cells: list[Cell], fill_interpolation: FillInterpolation, frame_interpolation: FrameInterpolation, delay: int):
self.cells = cells
self.fill = fill_interpolation
self.frame_interp = frame_interpolation
self.delay = delay
def to_string(self, frame_number: int):
cell_str = ""
for i, cell in enumerate(self.cells):
if cell.color.x != 0 or cell.color.y != 0 or cell.color.z != 0:
if i != 0:
cell_str += "\t\t"
cell_str += cell.to_string() + "\n\t"
cell_str = cell_str[:-3]
message = f"""
AnimationFrame frame{frame_number}{{
.frame = {{
{cell_str}
}},
.fillInterpolation = FillInterpolation::{self.fill},
.frameInterpolation = FrameInterpolation::{self.frame_interp},
.delay = std::chrono::milliseconds({self.delay})
}};
"""
return message
#TODO: Impliment this
def generate_animation_frame_struct(animation_types_path: str):
with open(animation_types_path, 'r') as AnimationTypes:
with open("AnimationTypes.py", 'w') as output:
pass
def mesh_to_cell(mesh: Mesh) -> Cell:
if mesh.face_color[0] != 0 or mesh.face_color[1] != 0 or mesh.face_color[2] != 0:
pass
pos = (mesh.get_average_position() + Vector3(1,1,1)) / 2
# need to swap z and y for the coordinate system
z = pos.y
pos.y = pos.z
pos.z = z
testVector = Vector3(0.1,0.5,0.9)
cell = Cell(pos, Vector3(mesh.face_color))
return cell
def scene_to_frame(scene: Scene, fill_interpolation: FillInterpolation, frame_interpolation: FrameInterpolation, delay: int, scene_number: int) -> str:
cells = [mesh_to_cell(cube) for cube in scene.meshes]
frame = AnimationFrame(cells, fill_interpolation, frame_interpolation, delay)
return frame.to_string(scene_number)

View File

@@ -0,0 +1,17 @@
import math
from pygame.math import Vector3
def project(vector, w, h, fov, distance):
factor = math.atan(fov / 2 * math.pi / 180) / (distance + vector.z)
x = vector.x * factor * w + w / 2
y = -vector.y * factor * w + h / 2
return Vector3(x, y, vector.z)
def rotate_vertices(vertices, angle, axis):
return [v.rotate(angle, axis) for v in vertices]
def scale_vertices(vertices, s):
return [Vector3(v[0]*s[0], v[1]*s[1], v[2]*s[2]) for v in vertices]
def translate_vertices(vertices, t):
return [v + Vector3(t) for v in vertices]
def project_vertices(vertices, w, h, fov, distance):
return [project(v, w, h, fov, distance) for v in vertices]

View File

@@ -0,0 +1,80 @@
# Setup
In order to run this tool follow these steps:
1. ensure you're using python 3.12+. You can verify this by running
```
python --version
```
in a terminal.
2. Inside of a terminal in the
```
tools/animation-tools
```
folder, run:
```
python -m venv venv
```
to create your virtual environment.
3. Now activate the virtual environment. In powershell run
```
venv\Scripts\activate
```
You may need to fix permission issues if this is your first time using a virtual environment. For git bash just run:
```
source venv/Scripts/activate
```
4. Run
```
pip install -r requirements.txt
```
# How to Use
The animation tool was made to allow anyone to easily create animaitons without much practice. You can create a sequence of frames in the studio, export them and then copy them over to the firmware
## Creating a Frame
When you first run the program the window will look like this:
![Startup](images\open-animator.png)
### Rotating the view
Press and hold the middle mouse button and move around the mouse to rotate the view.
### Changing Cube Colors
Click on a cube to select it. Selected cubes will be highlighted in red. All selected cubes will be set to the current color picker color. You can deselect a block by clicking on it again.
The color picker consists of 3 sliders on the left of the screen. A preview of the color is in a small box underneath the sliders. Move the sliders around to change colors.
### Fill Options
Fill options tell the board how to color cubes which are set to black in the animator. There are four options:
#### No Fill
Cubes that are set to black will be off
#### Closest Color
Cubes that are set to black will become the same color as their closest colored neighbor
#### Linear
Cubes that are set to black will become a distance weighted average of all of the colored cubes
#### Square
Same as linear, but the distance weighting is squared. This means a cube that's twice as far away will have only 1/4 the effect.
### Fade options
The fade options tell the board how to transition between frames
#### Snap
The colors will remain constant until the delay time for the current frame has passed. At that point, the frame will immediatly switch to the next frame.
#### Fade
The colors will linearly transition from one frame to the next. This often results in very interesting colors and patterns emerging.
### Creating more than one frame
An animation isn't really an animation with only one frame. You can add more frames to your animation with the "Next Frame" button. This will create a new frame for you to edit. You'll also notice the frame counter above the next frame button will go up. That shows you the current frame number that you're editing. You can view previous frames but clicking on the last frame button.
### Exporting your Animation
Now that you're done creating your lovely animation you can export it! Click the save button and C++ code will automatically be generated which describes all of the frames you just created. The code is saved in output.txt in the animation-tools folder. How to use that code is beyond the scope of this README. Reach out to Quinn for more instruction.
# Future features
Eventually I would like to add the ability to send the animations directly to the board without having to mess with the firmware at all. This will take a pretty big rework though.

View File

@@ -0,0 +1,70 @@
from Shapes import Mesh
from MatrixMath import *
import pygame
class Scene:
def __init__(self, meshes, fov, distance):
self.meshes: list[Mesh] = meshes
self.fov = fov
self.distance = distance
self.euler_angles = [0, 0, 0]
def transform_vertices(self, vertices, width, height):
transformed_vertices = vertices
axis_list = [(1, 0, 0), (0, 1, 0), (0, 0, 1)]
for angle, axis in reversed(list(zip(list(self.euler_angles), axis_list))):
transformed_vertices = rotate_vertices(transformed_vertices, angle, axis)
transformed_vertices = project_vertices(transformed_vertices, width, height, self.fov, self.distance)
return transformed_vertices
def point_in_polygon(self, point, polygon):
""" Determine if the point (x, y) is inside the polygon """
x, y = point
n = len(polygon)
inside = False
p1x, p1y = polygon[0]
for i in range(n + 1):
p2x, p2y = polygon[i % n]
if y > min(p1y, p2y):
if y <= max(p1y, p2y):
if x <= max(p1x, p2x):
if p1y != p2y:
xints = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
if p1x == p2x or x <= xints:
inside = not inside
p1x, p1y = p2x, p2y
return inside
def get_mesh_from_xy(self, pos: tuple[int]) -> Mesh:
x, y = pos
closest_mesh = None
closest_z = float('inf')
for mesh in self.meshes:
transformed_vertices = self.transform_vertices(mesh.get_vertices(), *pygame.display.get_surface().get_size())
avg_z = mesh.calculate_average_z(transformed_vertices)
for i, avg_z_value in avg_z:
polygon = mesh.create_polygon(mesh.get_face(i), transformed_vertices)
if self.point_in_polygon((x, y), polygon):
if avg_z_value < closest_z:
closest_z = avg_z_value
closest_mesh = mesh
return closest_mesh
def draw(self, surface):
polygons = []
for mesh in self.meshes:
transformed_vertices = self.transform_vertices(mesh.get_vertices(), *surface.get_size())
avg_z = mesh.calculate_average_z(transformed_vertices)
for z in avg_z:
#for z in sorted(avg_z, key=lambda x: x[1], reverse=True):
pointlist = mesh.create_polygon(mesh.get_face(z[0]), transformed_vertices)
polygons.append((pointlist, z[1], mesh.face_color, mesh.edge_color))
#pygame.draw.polygon(surface, (128, 128, 192), pointlist)
#pygame.draw.polygon(surface, (0, 0, 0), pointlist, 3)
for poly in sorted(polygons, key=lambda x: x[1], reverse=True):
pygame.draw.polygon(surface, poly[2], poly[0])
pygame.draw.polygon(surface, poly[3], poly[0], 3)

View File

@@ -0,0 +1,48 @@
from MatrixMath import *
from pygame.math import Vector3
class Mesh():
def __init__(self, vertices, faces):
self.__vertices = [Vector3(v) for v in vertices]
self.__faces = faces
self.face_color = (0, 0, 0)
self.edge_color = (0,0,0)
def rotate(self, angle, axis):
self.__vertices = rotate_vertices(self.__vertices, angle, axis)
def scale(self, s):
self.__vertices = scale_vertices(self.__vertices, s)
def translate(self, t):
self.__vertices = translate_vertices(self.__vertices, t)
def calculate_average_z(self, vertices):
return [(i, sum([vertices[j].z for j in f]) / len(f)) for i, f in enumerate(self.__faces)]
def get_average_position(self):
vertex_sum: Vector3 = Vector3()
for vertex in self.__vertices:
vertex_sum += vertex
return vertex_sum / len(self.__vertices)
def get_face(self, index):
return self.__faces[index]
def get_vertices(self):
return self.__vertices
def create_polygon(self, face, vertices):
return [(vertices[i].x, vertices[i].y) for i in [*face, face[0]]]
def set_face_color(self, color : tuple[int]):
self.face_color = color
def set_edge_color(self, color : tuple[int]):
self.edge_color = color
class Cube(Mesh):
def __init__(self):
vertices = [(-1,-1,1), (1,-1,1), (1,1,1), (-1,1,1), (-1,-1,-1), (1,-1,-1), (1,1,-1), (-1,1,-1)]
faces = [(0,1,2,3), (1,5,6,2), (5,4,7,6), (4,0,3,7), (3,2,6,7), (1,0,4,5)]
super().__init__(vertices, faces)
def set_position(self, position : tuple):
super().translate(position)

210
tools/animation-tools/UI.py Normal file
View File

@@ -0,0 +1,210 @@
from itertools import product
from pygame_widgets.button import Button
from pygame_widgets.slider import Slider
from pygame_widgets.dropdown import Dropdown
from pygame_widgets.textbox import TextBox
from Scene import Scene
from Shapes import *
import pygame
from AnimationExporter import *
class ColorPicker:
def __init__(self, screen, x_pos: int, y_pos: int, width: int, height: int):
width = max(50, width)
slider_colors = ((255,0,0),(0,255,0),(0,0,255))
self.sliders: list[Slider] = [
Slider(screen, int(x_pos + i*(width/3)), y_pos, int((width-50)/3), height, min=0, max=255, step=1, vertical=True, handleColour=slider_colors[i]) for i in range(3)
]
for slider in self.sliders:
slider.enable()
def get_color(self) -> tuple[int]:
return tuple([slider.getValue() for slider in self.sliders])
def set_color(self, color: tuple[int]):
for i, slider in enumerate(self.sliders):
slider.setValue(color[i])
class SceneStore:
def __init__(self, scene: Scene, fill: FillInterpolation, fade: FrameInterpolation, delay: int):
self.scene: Scene = scene
self.fill: FillInterpolation = fill
self.fade: FrameInterpolation = fade
self.delay: int = delay
class SceneManager:
def __init__(self, window, color_picker: ColorPicker):
self.file_data: str = ""
self._scenes: list[SceneStore] = [SceneStore(self.new_scene(), FillInterpolation.NO_FILL, FrameInterpolation.FADE, 1000)]
self._current_scene_idx: int = 0
self.window = window
self.color_picker = color_picker
self._selected_meshes: list[Mesh] = []
def save_frame_to_file(self):
with open("tools/animation-tools/output.txt", 'w') as file:
for i, scene in enumerate(self._scenes):
file.write(scene_to_frame(scene.scene, scene.fill, scene.fade, scene.delay, i))
def generate_meshes(self) -> list[Mesh]:
# generate a list of cubes
meshes: list[Mesh] = []
for origin in product([-1, 0, 1],[-1, 0, 1],[-1, 0, 1]):
cube = Cube()
cube.scale((0.35, 0.35, 0.35))
cube.set_position(origin)
cube.set_face_color((0, 0, 0))
meshes.append(cube)
return meshes
def new_scene(self) -> Scene:
return Scene(self.generate_meshes(), 90, 5)
def draw(self):
for mesh in self._selected_meshes:
mesh.set_face_color(self.color_picker.get_color())
self.get_current_scene().scene.draw(self.window)
def get_current_scene(self) -> SceneStore:
return self._scenes[self._current_scene_idx]
def click_mesh(self, coordinates: tuple[int, int]):
mesh = self.get_current_scene().scene.get_mesh_from_xy(coordinates)
if mesh == None:
return
if mesh in self._selected_meshes:
mesh.set_edge_color((0,0,0))
self._selected_meshes.remove(mesh)
else:
mesh.set_edge_color((255,0,0))
self._selected_meshes.append(mesh)
def deselect_all_mesh(self):
for mesh in self._selected_meshes:
mesh.set_edge_color((0,0,0))
self._selected_meshes = []
def next_scene(self):
self.deselect_all_mesh()
current_angles = self.get_current_scene().scene.euler_angles
if len(self._scenes)-1 == self._current_scene_idx:
self._scenes.append(SceneStore(self.new_scene(), FillInterpolation.NO_FILL, FrameInterpolation.FADE, 1000))
self._current_scene_idx += 1
self.get_current_scene().scene.euler_angles = [angle for angle in current_angles]
def last_scene(self):
if self._current_scene_idx > 0:
current_angles = self.get_current_scene().scene.euler_angles
self._current_scene_idx -= 1
self.get_current_scene().scene.euler_angles = [angle for angle in current_angles]
self.deselect_all_mesh()
def update_scene_options(self, fill: FillInterpolation, fade: FrameInterpolation, delay: int):
cur_scene: SceneStore = self.get_current_scene()
cur_scene.fill = fill
cur_scene.fade = fade
cur_scene.delay = delay
class AnimatorUI:
def __init__(self, window):
scr_wdt, scr_hgt = window.get_size()
self.window = window
self.colorPicker: ColorPicker = ColorPicker(self.window, *self.rel2abs(5, 5, 20, 60))
self.sceneManager: SceneManager = SceneManager(window, self.colorPicker)
self.save_button: Button = Button(self.window, *self.rel2abs(5, 90, 10, 10), text="Save",onClick=self.sceneManager.save_frame_to_file)
self.next_frame_button: Button = Button(self.window, *self.rel2abs(75, 95, 25, 5), text="Next Frame",onClick=self._next_scene)
self.last_frame_button: Button = Button(self.window, *self.rel2abs(50, 95, 25, 5), text="last Frame",onClick=self._last_scene)
self.trackMouseMotion: bool = False
self.fill_dropdown: Dropdown = Dropdown(self.window, *self.rel2abs(30, 0, 40, 5), name="Fill Type",
choices=[option.value for option in FillInterpolation], onRelease=self.set_update_options_flag)
self.fade_dropdown: Dropdown = Dropdown(self.window, *self.rel2abs(70, 0, 20, 5), name="Fade Type",
choices=[option.value for option in FrameInterpolation], onRelease=self.set_update_options_flag)
self.updateOptions = False
self._set_dropdown_options()
self.delay_text_entry: TextBox = TextBox(self.window, *self.rel2abs(30, 5, 60, 7), placeholderText="Transition Time (ms)", onSubmit=self.set_update_options_flag)
# Make a frame counter as a button but make it not look like a button
default_color=(150,150,150)
self.frame_counter_text: Button = Button(self.window, *self.rel2abs(70, 85, 10, 10), text="0", inactiveColour=default_color, hoverColour=default_color, pressedColour=default_color)
def rel2abs(self, x: float, y: float, width: float, height: float) -> tuple[int,int,int,int]:
scr_wdt, scr_hgt = self.window.get_size()
x_abs = int(x*scr_wdt/100)
y_abs = int(y*scr_hgt/100)
w_abs = int(width*scr_wdt/100)
h_abs = int(height*scr_hgt/100)
return (x_abs, y_abs, w_abs, h_abs)
def update_interaction(self, game_event):
if not self.trackMouseMotion:
pygame.mouse.get_rel()
if game_event.type == pygame.MOUSEBUTTONDOWN:
if game_event.button == 2: # middle mouse click
self.trackMouseMotion = True
elif game_event.button == 1: # left click
self.sceneManager.click_mesh(pygame.mouse.get_pos())
elif game_event.type == pygame.MOUSEBUTTONUP:
if game_event.button == 2: # middle mouse release
self.trackMouseMotion = False
elif self.trackMouseMotion and game_event.type == pygame.MOUSEMOTION:
mouseMovement = pygame.mouse.get_rel()
current_scene = self.sceneManager.get_current_scene()
current_scene.scene.euler_angles[0] -= mouseMovement[1]
current_scene.scene.euler_angles[1] -= mouseMovement[0]
def draw(self):
if self.updateOptions:
self.set_scene_options()
# make the preview window for the color picker
pygame.draw.rect(self.window, self.colorPicker.get_color(), self.rel2abs(11, 70, 5, 5))
pygame.draw.rect(self.window, (0,0,0), self.rel2abs(11, 70, 5, 5), 2)
self.sceneManager.draw()
def _next_scene(self):
self.sceneManager.next_scene()
self._set_dropdown_options()
self.frame_counter_text.setText(str(self.sceneManager._current_scene_idx))
def _last_scene(self):
self.sceneManager.last_scene()
self._set_dropdown_options()
self.frame_counter_text.setText(str(self.sceneManager._current_scene_idx))
def _set_dropdown_options(self):
# cursed method to set dropdown options because for some reason pygame_widgets dropdown doesn't allow you to manually set the dropdown option
scene = self.sceneManager.get_current_scene()
for choice in self.fill_dropdown._Dropdown__choices:
if choice.text.find(scene.fill) != -1:
self.fill_dropdown.chosen = choice
break
for choice in self.fade_dropdown._Dropdown__choices:
if choice.text.find(scene.fade) != -1:
self.fade_dropdown.chosen = choice
break
def set_update_options_flag(self):
self.updateOptions = True
def set_scene_options(self):
self.updateOptions = False
delay_time = 1000
try:
delay_time = int(self.delay_text_entry.getText())
except ValueError:
self.delay_text_entry.setText(str(delay_time))
self.sceneManager.update_scene_options(self.fill_dropdown.getSelected(), self.fade_dropdown.getSelected(), delay_time)

View File

@@ -0,0 +1,37 @@
import pygame
import pygame_widgets
from Shapes import *
from UI import AnimatorUI
WINDOW_W = 500
WINDOW_H = 500
file_data = ""
pygame.init()
window = pygame.display.set_mode((WINDOW_W, WINDOW_H))
ui = AnimatorUI(window)
clock = pygame.time.Clock()
run = True
while run:
clock.tick(60)
window.fill((255, 255, 255))
events = pygame.event.get()
for event in events:
if event.type == pygame.QUIT:
run = False
# if the event isn't handled above as a global event, let the ui handle it
else:
ui.update_interaction(event)
pygame_widgets.update(events)
ui.draw()
pygame.display.flip()
pygame.quit()

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,2 @@
pygame==2.6.0
pygame-widgets==1.1.5