{"pageProps":{"posts":{"status":"ok","feed":{"url":"https://medium.com/feed/@christian.marques","title":"Stories by Christian Marques on Medium","link":"https://medium.com/@christian.marques?source=rss-7a313bdb96ad------2","author":"","description":"Stories by Christian Marques on Medium","image":"https://cdn-images-1.medium.com/fit/c/150/150/1*4ch7lE_fgrfofv3rO5tN7A.jpeg"},"items":[{"title":"The ultimate hamster monitoring system","pubDate":"2025-06-01 11:35:24","link":"https://medium.com/@christian.marques/the-ultimate-hamster-monitoring-system-45ddec50009a?source=rss-7a313bdb96ad------2","guid":"https://medium.com/p/45ddec50009a","author":"Christian Marques","thumbnail":"","description":"\n
What happens when your daughter’s hamster runs marathons in the middle of the night? If you’re Mooey Maria Hazel — a very active and curious little hamster, with intrigued and inventive owners — you get your very own custom monitoring system.
\nThis post walks through a project that began as a simple Raspberry Pi sensor logger and grew into a full-stack dashboard with (quasi) real-time insights into Mooey’s nightly adventures. Whether you’re into hardware hacking, full-stack development, or just enjoy a good hamster story, there might be something here for you.
\nEvery night, Mooey leapt onto her wheel like a tiny Forrest Gump on a mission — running not from something, but for the sheer joy of it. She’d spin for hours, paws a-blur, eyes gleaming with mysterious purpose, the only sign of her journey the soft noise of paws on cork, engraved in our memory. But how far was she actually going? How fast? One evening, as her wheel squeaked into overdrive, a thought struck us: What if we could track her workouts — like a little hamster WHOOP?
\nThat single question kicked off a project that combines sensors, automation, databases, and modern web technologies to create something both cute and wholesome.
\nWe started by building a lightweight sensor logger with the Raspberry Pi.
\nThe total cost of the material for the project went around 52€, but you can re-use a lot of this stuff for other projects too (I’ve used the Raspberry PI for a bunch of different things from a homemade retro arcade, to a web-server, among others). You can also considerably reduce the costs by using a cheaper micro-controller like a Raspberry Pico, Arduino, or even simpler ones such as the Seeed or Keyestudio micro-controllers.
\nDISCLAIMER: This was pretty much our first time setting up such an electronics schematic, so apologies in advance for any obvious mistakes (and we welcome feedback on this, it’s probably offensive to the connoisseurs).
\nHere are some of the lessons that were learned while researching for this first-time electronics adventure:
\nHere’s a peek at the core functionality:
\n# hamster_session.py\n
import RPi.GPIO as GPIO
import time
from sensors import get_temperature, get_humidity
from session import HamsterSession
# --- Configuration ---
SENSOR_PIN = 4 # BCM pin for hall sensor
INACTIVITY_TIMEOUT = 60 # seconds
# --- Main logic ---
GPIO.setmode(GPIO.BCM)
GPIO.setup(SENSOR_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
print(\"Hamster Wheel Session Logger (CTRL+C to exit)\")
session = HamsterSession()
try:
while True:
value = GPIO.input(SENSOR_PIN)
if value == GPIO.LOW:
now = time.time()
if not session.active:
session.start_session(now)
temp = get_temperature()
hum = get_humidity()
session.log_rotation(now, temp, hum)
# Debounce: wait for sensor to go HIGH
while GPIO.input(SENSOR_PIN) == GPIO.LOW:
time.sleep(0.01)
else:
if session.active and session.last_activity and (time.time() - session.last_activity > INACTIVITY_TIMEOUT):
session.end_session()
time.sleep(0.05)
except KeyboardInterrupt:
print(\"Exiting...\")
finally:
GPIO.cleanup()
An interesting thing that I found was that I wasn’t able to use the Adafruit_DHT library with our Raspberry Pi, so I defaulted to using the dtoverlay Linux driver, which allows you to interface with the temperature and humidity sensor in a different way (but you need to make sure to load the driver before the program runs with sudo dtoverlay dht11 gpiopin=YOUR_SENSOR_PIN
\n# sensors.py\n
# Utility functions for reading temperature and humidity
# This requires to run \"sudo dtoverlay dht11 gpiopin=YOUR_SENSOR_PIN\"
TEMP_PATH = \"/sys/bus/iio/devices/iio:device0/in_temp_input\"
HUMIDITY_PATH = \"/sys/bus/iio/devices/iio:device0/in_humidityrelative_input\"
def read_first_line(filename):
try:
with open(filename, \"rt\") as f:
value = int(f.readline())
return True, value
except Exception:
return False, -1
def get_temperature():
flag, temp = read_first_line(TEMP_PATH)
return temp // 1000 if flag else None
def get_humidity():
flag, hum = read_first_line(HUMIDITY_PATH)
return hum // 1000 if flag else None
import os\n
import time
import requests
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
API_URL = os.getenv(\"API_URL\")
API_SECRET_TOKEN = os.getenv(\"API_SECRET_TOKEN\")
class HamsterSession:
def __init__(self):
self.active = False
self.rotations = 0
self.rotation_log = []
self.start = None
self.last_activity = None
self.last_temp = None
self.last_hum = None
def start_session(self, now):
self.active = True
self.start = now
self.rotations = 0
self.rotation_log = []
self.last_temp = None
self.last_hum = None
print(\"Session started at\", time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(now)))
def log_rotation(self, now, temp, hum):
if temp is None:
temp = self.last_temp
else:
self.last_temp = temp
if hum is None:
hum = self.last_hum
else:
self.last_hum = hum
temp = float(temp) if temp is not None else -1.0
hum = float(hum) if hum is not None else -1.0
self.rotations += 1
self.rotation_log.append({
\"timestamp\": float(now),
\"temperature\": temp,
\"humidity\": hum
})
self.last_activity = now
print(f\"Rotation {self.rotations}: Temp={temp}°C, Humidity={hum}%\")
def end_session(self):
session_end = self.last_activity
if self.rotations < 5:
print(f\"Session discarded — only {self.rotations} rotations\")
self._reset()
return
session_data = {
\"images\": [],
\"rotationLog\": self.rotation_log
}
print(\"Session ended at\", time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(session_end)))
print(\"Session data:\", session_data)
try:
headers = {
\"Authorization\": f\"Bearer {API_SECRET_TOKEN}\",
\"Content-Type\": \"application/json\"
}
response = requests.post(API_URL, json=session_data, headers=headers)
print(\"Posted to API, status:\", response.status_code)
except Exception as e:
print(\"Failed to post to API:\", e)
self._reset()
def _reset(self):
self.active = False
self.start = None
self.last_activity = None
To keep things reliable, the logger runs as a systemd service, so it’s always on — even if the Raspberry Pi reboots.
\nYou can find the full code solution in this repository.
\nEven though I’m quite good with software, I have an Achilles’ heel when needing to manually build things (especially fragile things, as I’m sort of a brute), so this was one of the hardest parts of the project, only made possible due to my wife’s talent with handy-works.
\nOne important consideration was that we had to make sure that Mooey wouldn’t be able to access any wiring or the magnet itself, because she would definitely be curious enough to go on and try to eat them. The second important thing was aesthetics as wanted the setup to be minimally visible from the outside (hiding it as much as possible).
\nWe ended up using a lot of masking and double-sided tape keep the Raspberry Pi on the ceiling of the cage and the sensors in the right place - it didn’t look amazing, but we stuck with it (no pun intended) for the first days, while we tested the setup. But, as expected, this turned out to not be reliable, as the glue from the tape started becoming less effective as the temperatures rose, so we ended up using plastic clamps to keep everything as stable as possible, with a single cable passed through the top of the cage to feed the Pi, and no cables at the ground level at all. (Kudos to Angharad for the patience and excellent work on this)
\nWith data flowing in, we needed a place to explore it. Enter the 🐹 Mooey Maria Hazel 🐹 Monitoring Dashboard — a place where we can comprehensively explore all the data we’ve gathered!
\nWe used Next.js for this purpose since it provides some useful things out of the box, such as API routes, a built-in feature to handle backend logic, and the ease of free hosting on Vercel (the creators of Next.js).
\nThe setup is pretty simple, using a couple of API routes (GET and POST) to create and display sessions, and managing the front-end completely as a client side app. We also used MUI (funny, it really sounds like Mooey) to get a simple, clean UI going, and this is also great because you can use the integrated MUI X Charts to do some simple yet effective plotting.
\nYou can visit the web-app through this link -> https://hamster-red.vercel.app/ (it seems like there was already a hamster.vercel.app so Vercel gave us the hamster-red domain name…)
\nEverything runs autonomously — once set up, the logger collects and uploads data without manual intervention, and the dashboard stays in sync, handling most of the data transformations.
\nYou can find the full code solution in this repository.
\nThis is the data model we’re currently at — we tried making it as minimal and flexible as possible (essentially just logging the data without any inference) as to be able to scale it and infer the data we want in the web-app instead. With this simple model just logging sessions (from first rotation until 60 seconds of inactivity) for each rotation of the wheel a timestamp plus the temperature and humidity levels at that time, we can easily infer the distance (wheel’s circumference x rotations), the speed (distance / duration in seconds), duration of sessions, rotations per minute (rotations / 60) and under which environmental conditions Mooey runs faster, or further (among other things).
\nDuring just the span of a weekend, it was possible to create this whole project with a lot of cool features, such as:
\nHere is a condensed version of the lessons learned during this short project:
\nAfter just a week of running the system, Mooey had logged:
\nWe learned that she runs more when the room is slightly cooler (this was interesting to observe, as we had a sort of “heat wave” and Mooey’s performance dropped considerably during these days — particularly when temperature was high and humidity was low — so drier weather, even though Syrian hamsters come from the region of Aleppo, their natural habitat is the mountain, so they prefer cool weather), and that her favorite activity window is between 11 PM and 4 AM.
\nWe will definitely enjoy continuing gathering data and might do an edit, or a follow-up post in some months with further analysis if something interesting comes up!
\nThere are still many, many things that would be fun to do in such a project:
\n(not sure a hamster lives long enough for us to have enough time to implement all of these 🙈)
\nWhat started as a silly idea became a fun and rewarding full-stack hardware/software project. More importantly, it gave us a new way to connect with Mooey and understand her little world a bit better.
\nIf you’re looking for a fun side project with just the right mix of code, electronics, and adorable outcomes — build your hamster a dashboard.
\nAll in all, this project was possible to build within the space of a weekend, with some minor tweaks across the first week to fix some small issues that popped up here and there.
\nGot questions or want to build your own hamster tracker?
Feel free to fork the code or reach out! Mooey would be thrilled to inspire more data-driven adventures. 🐹📈
What happens when your daughter’s hamster runs marathons in the middle of the night? If you’re Mooey Maria Hazel — a very active and curious little hamster, with intrigued and inventive owners — you get your very own custom monitoring system.
\nThis post walks through a project that began as a simple Raspberry Pi sensor logger and grew into a full-stack dashboard with (quasi) real-time insights into Mooey’s nightly adventures. Whether you’re into hardware hacking, full-stack development, or just enjoy a good hamster story, there might be something here for you.
\nEvery night, Mooey leapt onto her wheel like a tiny Forrest Gump on a mission — running not from something, but for the sheer joy of it. She’d spin for hours, paws a-blur, eyes gleaming with mysterious purpose, the only sign of her journey the soft noise of paws on cork, engraved in our memory. But how far was she actually going? How fast? One evening, as her wheel squeaked into overdrive, a thought struck us: What if we could track her workouts — like a little hamster WHOOP?
\nThat single question kicked off a project that combines sensors, automation, databases, and modern web technologies to create something both cute and wholesome.
\nWe started by building a lightweight sensor logger with the Raspberry Pi.
\nThe total cost of the material for the project went around 52€, but you can re-use a lot of this stuff for other projects too (I’ve used the Raspberry PI for a bunch of different things from a homemade retro arcade, to a web-server, among others). You can also considerably reduce the costs by using a cheaper micro-controller like a Raspberry Pico, Arduino, or even simpler ones such as the Seeed or Keyestudio micro-controllers.
\nDISCLAIMER: This was pretty much our first time setting up such an electronics schematic, so apologies in advance for any obvious mistakes (and we welcome feedback on this, it’s probably offensive to the connoisseurs).
\nHere are some of the lessons that were learned while researching for this first-time electronics adventure:
\nHere’s a peek at the core functionality:
\n# hamster_session.py\n
import RPi.GPIO as GPIO
import time
from sensors import get_temperature, get_humidity
from session import HamsterSession
# --- Configuration ---
SENSOR_PIN = 4 # BCM pin for hall sensor
INACTIVITY_TIMEOUT = 60 # seconds
# --- Main logic ---
GPIO.setmode(GPIO.BCM)
GPIO.setup(SENSOR_PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
print(\"Hamster Wheel Session Logger (CTRL+C to exit)\")
session = HamsterSession()
try:
while True:
value = GPIO.input(SENSOR_PIN)
if value == GPIO.LOW:
now = time.time()
if not session.active:
session.start_session(now)
temp = get_temperature()
hum = get_humidity()
session.log_rotation(now, temp, hum)
# Debounce: wait for sensor to go HIGH
while GPIO.input(SENSOR_PIN) == GPIO.LOW:
time.sleep(0.01)
else:
if session.active and session.last_activity and (time.time() - session.last_activity > INACTIVITY_TIMEOUT):
session.end_session()
time.sleep(0.05)
except KeyboardInterrupt:
print(\"Exiting...\")
finally:
GPIO.cleanup()
An interesting thing that I found was that I wasn’t able to use the Adafruit_DHT library with our Raspberry Pi, so I defaulted to using the dtoverlay Linux driver, which allows you to interface with the temperature and humidity sensor in a different way (but you need to make sure to load the driver before the program runs with sudo dtoverlay dht11 gpiopin=YOUR_SENSOR_PIN
\n# sensors.py\n
# Utility functions for reading temperature and humidity
# This requires to run \"sudo dtoverlay dht11 gpiopin=YOUR_SENSOR_PIN\"
TEMP_PATH = \"/sys/bus/iio/devices/iio:device0/in_temp_input\"
HUMIDITY_PATH = \"/sys/bus/iio/devices/iio:device0/in_humidityrelative_input\"
def read_first_line(filename):
try:
with open(filename, \"rt\") as f:
value = int(f.readline())
return True, value
except Exception:
return False, -1
def get_temperature():
flag, temp = read_first_line(TEMP_PATH)
return temp // 1000 if flag else None
def get_humidity():
flag, hum = read_first_line(HUMIDITY_PATH)
return hum // 1000 if flag else None
import os\n
import time
import requests
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
API_URL = os.getenv(\"API_URL\")
API_SECRET_TOKEN = os.getenv(\"API_SECRET_TOKEN\")
class HamsterSession:
def __init__(self):
self.active = False
self.rotations = 0
self.rotation_log = []
self.start = None
self.last_activity = None
self.last_temp = None
self.last_hum = None
def start_session(self, now):
self.active = True
self.start = now
self.rotations = 0
self.rotation_log = []
self.last_temp = None
self.last_hum = None
print(\"Session started at\", time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(now)))
def log_rotation(self, now, temp, hum):
if temp is None:
temp = self.last_temp
else:
self.last_temp = temp
if hum is None:
hum = self.last_hum
else:
self.last_hum = hum
temp = float(temp) if temp is not None else -1.0
hum = float(hum) if hum is not None else -1.0
self.rotations += 1
self.rotation_log.append({
\"timestamp\": float(now),
\"temperature\": temp,
\"humidity\": hum
})
self.last_activity = now
print(f\"Rotation {self.rotations}: Temp={temp}°C, Humidity={hum}%\")
def end_session(self):
session_end = self.last_activity
if self.rotations < 5:
print(f\"Session discarded — only {self.rotations} rotations\")
self._reset()
return
session_data = {
\"images\": [],
\"rotationLog\": self.rotation_log
}
print(\"Session ended at\", time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(session_end)))
print(\"Session data:\", session_data)
try:
headers = {
\"Authorization\": f\"Bearer {API_SECRET_TOKEN}\",
\"Content-Type\": \"application/json\"
}
response = requests.post(API_URL, json=session_data, headers=headers)
print(\"Posted to API, status:\", response.status_code)
except Exception as e:
print(\"Failed to post to API:\", e)
self._reset()
def _reset(self):
self.active = False
self.start = None
self.last_activity = None
To keep things reliable, the logger runs as a systemd service, so it’s always on — even if the Raspberry Pi reboots.
\nYou can find the full code solution in this repository.
\nEven though I’m quite good with software, I have an Achilles’ heel when needing to manually build things (especially fragile things, as I’m sort of a brute), so this was one of the hardest parts of the project, only made possible due to my wife’s talent with handy-works.
\nOne important consideration was that we had to make sure that Mooey wouldn’t be able to access any wiring or the magnet itself, because she would definitely be curious enough to go on and try to eat them. The second important thing was aesthetics as wanted the setup to be minimally visible from the outside (hiding it as much as possible).
\nWe ended up using a lot of masking and double-sided tape keep the Raspberry Pi on the ceiling of the cage and the sensors in the right place - it didn’t look amazing, but we stuck with it (no pun intended) for the first days, while we tested the setup. But, as expected, this turned out to not be reliable, as the glue from the tape started becoming less effective as the temperatures rose, so we ended up using plastic clamps to keep everything as stable as possible, with a single cable passed through the top of the cage to feed the Pi, and no cables at the ground level at all. (Kudos to Angharad for the patience and excellent work on this)
\nWith data flowing in, we needed a place to explore it. Enter the 🐹 Mooey Maria Hazel 🐹 Monitoring Dashboard — a place where we can comprehensively explore all the data we’ve gathered!
\nWe used Next.js for this purpose since it provides some useful things out of the box, such as API routes, a built-in feature to handle backend logic, and the ease of free hosting on Vercel (the creators of Next.js).
\nThe setup is pretty simple, using a couple of API routes (GET and POST) to create and display sessions, and managing the front-end completely as a client side app. We also used MUI (funny, it really sounds like Mooey) to get a simple, clean UI going, and this is also great because you can use the integrated MUI X Charts to do some simple yet effective plotting.
\nYou can visit the web-app through this link -> https://hamster-red.vercel.app/ (it seems like there was already a hamster.vercel.app so Vercel gave us the hamster-red domain name…)
\nEverything runs autonomously — once set up, the logger collects and uploads data without manual intervention, and the dashboard stays in sync, handling most of the data transformations.
\nYou can find the full code solution in this repository.
\nThis is the data model we’re currently at — we tried making it as minimal and flexible as possible (essentially just logging the data without any inference) as to be able to scale it and infer the data we want in the web-app instead. With this simple model just logging sessions (from first rotation until 60 seconds of inactivity) for each rotation of the wheel a timestamp plus the temperature and humidity levels at that time, we can easily infer the distance (wheel’s circumference x rotations), the speed (distance / duration in seconds), duration of sessions, rotations per minute (rotations / 60) and under which environmental conditions Mooey runs faster, or further (among other things).
\nDuring just the span of a weekend, it was possible to create this whole project with a lot of cool features, such as:
\nHere is a condensed version of the lessons learned during this short project:
\nAfter just a week of running the system, Mooey had logged:
\nWe learned that she runs more when the room is slightly cooler (this was interesting to observe, as we had a sort of “heat wave” and Mooey’s performance dropped considerably during these days — particularly when temperature was high and humidity was low — so drier weather, even though Syrian hamsters come from the region of Aleppo, their natural habitat is the mountain, so they prefer cool weather), and that her favorite activity window is between 11 PM and 4 AM.
\nWe will definitely enjoy continuing gathering data and might do an edit, or a follow-up post in some months with further analysis if something interesting comes up!
\nThere are still many, many things that would be fun to do in such a project:
\n(not sure a hamster lives long enough for us to have enough time to implement all of these 🙈)
\nWhat started as a silly idea became a fun and rewarding full-stack hardware/software project. More importantly, it gave us a new way to connect with Mooey and understand her little world a bit better.
\nIf you’re looking for a fun side project with just the right mix of code, electronics, and adorable outcomes — build your hamster a dashboard.
\nAll in all, this project was possible to build within the space of a weekend, with some minor tweaks across the first week to fix some small issues that popped up here and there.
\nGot questions or want to build your own hamster tracker?
Feel free to fork the code or reach out! Mooey would be thrilled to inspire more data-driven adventures. 🐹📈
As we are working with 3D dental data at Promaton, we recently had the chance to tackle an interesting project, where we got to explore a solution for finding the shortest path on the surface of a mesh, using our stack (React / Three.js / R3F). After researching and exploring this topic, we quickly realised that we’d have to dive into a custom implementation to better fit our needs, given that the available implementations using three.js (our preferred 3D library) seemed not to fit our specific needs. We are now proud to be sharing our journey through this fascinating topic, together with a step-by-step description of our implementation, the demo we created in the process and some interesting conclusions.
\nIn the dental domain, we are developing tools to edit the boundaries of a mesh. At first glance, this might seem straightforward, but it inevitably leads to a complex problem in mathematics and software engineering. How can we draw a path between two vertices on a mesh? Which is the most efficient path? These are the types of questions we found ourselves having to answer.
\nThere are several approaches to solving the problem of path-finding on mesh surfaces. One approach involves adapting traditional grid-based path-finding algorithms [1] [2], such as A* search or breadth-first search, to operate on a mesh representation. However, these methods may encounter challenges in accurately representing the irregular geometry of mesh surfaces and may require extensive preprocessing or manual subdivision of the mesh. Additionally, machine learning techniques [3], such as reinforcement learning or neural networks, offer another avenue for path-finding on meshes by learning navigation policies from data. However, these approaches may require significant computational resources for training and may not always generalize well to diverse mesh environments. Alternatively to these, there is also Dijkstra’s algorithm (using priority queues and weighted edges), which can efficiently handle the irregularities of mesh geometries and provide a straightforward implementation that guarantees the shortest path without extensive preprocessing, offering better scalability and adaptability with lower computational overhead compared to machine learning approaches.
\nAs we pondered on all the options described in the previous section and considered our use case and particular circumstances, we chose to approach this problem by implementing a simple node graph with priority queues and weighted nodes to represent mesh vertices. Using Dijkstra’s algorithm, we managed to efficiently calculate the shortest path between points, ensuring both accuracy (Dijkstra always guarantees the shortest path) and real-time performance. By utilizing priority queues (based on binary heaps) to efficiently explore neighbouring nodes, we were able to minimize computational overhead, rendering this solution scalable and adaptable to various mesh complexities, particularly the large and complex STL files from intra-oral scanners (as these are designed to capture the maximum amount of possible data) that we have to deal with.
\nThis code snippet defines classes for creating a graph representation of a mesh surface to facilitate path-finding. The Node class represents individual vertices on the mesh, storing their index, position (as a Vector3), and connections to neighbouring nodes. Edges between nodes are defined by the Edge class, which includes references to the connecting nodes and a weight indicating the distance between them. The Graph class manages the graph structure, storing node adjacency and providing methods for adding nodes and edges. Additionally, it includes functionality to check graph connectivity and compute the graph from a target mesh. During graph computation, vertex positions are extracted from the mesh geometry, and edges are created between adjacent vertices, with weights determined by their Euclidean distances. This code lays the foundation for efficient path-finding algorithms to navigate the complexities of the mesh surface. It can definitely be improved (for example by implementing a half-edge data structure) to further maximise the efficiency of traversal, something we will be looking into in the near future.
\n// Define a class to represent individual vertices on the mesh\n
class Node:
index: Integer
value: Vector3
edges: List of Edge
method constructor(index: Integer, value: Vector3, edges: List of Edge = empty list):
this.index = index
this.value = value
this.edges = edges
// Define a class to represent edges between nodes
class Edge:
node1: Node
node2: Node
weight: Number
method constructor(node1: Node, node2: Node, weight: Number):
this.node1 = node1
this.node2 = node2
this.weight = weight
// Define a class to manage the graph structure
class Graph:
adjacencyList: Map of Integer to List of Integer
nodes: Map of Integer to Node
target: Mesh (optional)
method constructor():
this.adjacencyList = empty map
this.nodes = empty map
method clear():
clear this.nodes
clear this.adjacencyList
// Method to add a new node to the graph
method addNode(index: Integer, value: Vector3):
if index not in this.nodes:
newNode = new Node(index, value)
this.nodes[index] = newNode
this.adjacencyList[index] = empty list
// Method to add a new edge to the graph
method addEdge(index1: Integer, index2: Integer, weight: Number = 1):
node1 = this.nodes[index1]
node2 = this.nodes[index2]
if node1 is null or node2 is null:
throw error \"Node not found\"
newEdge = new Edge(node1, node2, weight)
node1.edges.append(newEdge)
node2.edges.append(newEdge)
if index1 not in this.adjacencyList:
this.adjacencyList[index1] = empty list
if index2 not in this.adjacencyList:
this.adjacencyList[index2] = empty list
this.adjacencyList[index1].append(index2)
this.adjacencyList[index2].append(index1)
// Method to compute the graph from the target mesh
method computeGraph():
if this.target is null:
return
positions = get attribute \"position\" from this.target.geometry
indices = get index from this.target.geometry
if indices is null:
throw error \"Mesh geometry is not indexed. Can only compute indexed geometries.\"
vertex = new Vector3()
addedEdges = new Set()
// Add nodes to the graph
for i from 0 to count of positions:
vertex = get vertex from positions at index i
apply matrix of this.target to vertex
addNode(i, clone of vertex)
// Add edges to the graph
for i from 0 to count of indices with step 3:
indexA = get value from indices at index i
indexB = get value from indices at index i+1
indexC = get value from indices at index i+2
edgeAB = min(indexA, indexB) + \"-\" + max(indexA, indexB)
edgeBC = min(indexB, indexC) + \"-\" + max(indexB, indexC)
edgeCA = min(indexC, indexA) + \"-\" + max(indexC, indexA)
if edgeAB not in addedEdges:
weightAB = distance between nodes at indexA and indexB
addEdge(indexA, indexB, weightAB)
addedEdges.add(edgeAB)
if edgeBC not in addedEdges:
weightBC = distance between nodes at indexB and indexC
addEdge(indexB, indexC, weightBC)
addedEdges.add(edgeBC)
if edgeCA not in addedEdges:
weightCA = distance between nodes at indexC and indexA
addEdge(indexC, indexA, weightCA)
addedEdges.add(edgeCA)
\nCode-block #1 — Pseudo-code representation of a mesh as a graph structure with weighted edges. For the Typescript implementation please refer to the demo.\n\n
Dijkstra’s algorithm (/ˈdaɪkstrəz/ DYKE-strəz) is an algorithm for finding the shortest paths between nodes in a weighted graph, which may represent, for example, road networks. It was conceived by computer scientist Edsger W. Dijkstra in 1956 and published three years later.
\nDijkstra’s algorithm finds the shortest path from a given source node to every other node. It can also be used to find the shortest path to a specific destination node, by terminating the algorithm once the shortest path to the destination node is known. For example, if the nodes of the graph represent cities, and the costs of edges represent the average distances between pairs of cities connected by a direct road, then Dijkstra’s algorithm can be used to find the shortest route between one city and all other cities.
\nThe nature of Dijkstra’s algorithm makes it perfect for our use case [4], as this is exactly what we intend to do, finding the shortest path between two nodes of a graph, but in this particular case, the graph is our mesh as described in the previous section.
\nThe following snippet of code defines a function getShortestPath, that calculates the shortest path between two nodes on a graph, using Dijkstra’s algorithm. The function takes the start and end node indices, along with a map of nodes, where the key is the node index and the value is the node object. Inside the function, a priority queue is used to track nodes with their distances from the start node. The algorithm iterates through the graph, updating distances and previous nodes as it explores the graph. Once the shortest path is found, it constructs and returns an array of nodes representing the path. Additionally, the code includes a PriorityQueue class, implemented as a binary heap, which is used internally by the getShortestPath function to efficiently manage node priorities during traversal. If you’d like to read more about the Binary Heap Priority Queue implementation, please refer to this article, which served as our inspiration.
\n// Import the Node class\n
import Node from graph
/**
* Calculates the shortest path between two nodes using Dijkstra's algorithm.
* @param startIndex - The index of the start node.
* @param endIndex - The index of the end node.
* @param nodes - A map of nodes where the key is the node index and the value is the node itself.
* @returns An array of node values representing the shortest path, or null if no path is found.
*/
function getShortestPath(startIndex, endIndex, nodes):
startNode = nodes.get(startIndex)
endNode = nodes.get(endIndex)
if startNode is null or endNode is null:
print \"Start or end node is undefined\"
return null
distances = new Map()
previousNodes = new Map()
queue = new PriorityQueue()
distances.set(startNode, 0)
queue.enqueue(startNode, 0)
while not queue.isEmpty():
currentNode = queue.dequeue()
if currentNode == endNode:
path = empty list
node = currentNode
while node is not null:
path.insert_at_start(node)
node = previousNodes.get(node)
return path
for each edge in currentNode.edges:
adjacentNode = edge.node1 if edge.node1 != currentNode else edge.node2
currentDistance = distances.get(currentNode)
if currentDistance is null:
continue
newDistance = currentDistance + edge.weight
adjacentNodeDistance = distances.get(adjacentNode)
if adjacentNodeDistance is null or newDistance < adjacentNodeDistance:
distances.set(adjacentNode, newDistance)
previousNodes.set(adjacentNode, currentNode)
queue.enqueue(adjacentNode, newDistance)
print \"Failed to find path\"
return null
/**
* A priority queue implemented as a binary heap.
*/
class PriorityQueue:
elements = empty list of {node, priority}
/**
* Adds a node to the queue with a given priority.
* @param node - The node to add.
* @param priority - The priority of the node.
*/
method enqueue(node, priority):
elements.append({node, priority})
bubbleUp(size of elements - 1)
/**
* Removes and returns the node with the highest priority.
* @returns The node with the highest priority.
*/
method dequeue():
first = elements[0]
last = elements.pop()
if size of elements > 0:
elements[0] = last
bubbleDown(0)
return first.node
/**
* Checks if the queue is empty.
* @returns True if the queue is empty, false otherwise.
*/
method isEmpty():
return size of elements == 0
/**
* Moves a node up in the tree, i.e., towards the root.
* @param index - The index of the node to move up.
*/
method bubbleUp(index):
while index > 0:
parentIndex = floor((index - 1) / 2)
if elements[parentIndex].priority <= elements[index].priority:
break
swap elements[parentIndex] with elements[index]
index = parentIndex
/**
* Moves a node down in the tree, i.e., away from the root.
* @param index - The index of the node to move down.
*/
method bubbleDown(index):
length = size of elements
element = elements[index]
swapIndex = null
do:
swapIndex = null
leftChildIndex = 2 * index + 1
rightChildIndex = 2 * index + 2
if leftChildIndex < length and elements[leftChildIndex].priority < element.priority:
swapIndex = leftChildIndex
if rightChildIndex < length:
if swapIndex is null and elements[rightChildIndex].priority < element.priority:
swapIndex = rightChildIndex
elif swapIndex is not null and elements[rightChildIndex].priority < elements[leftChildIndex].priority:
swapIndex = rightChildIndex
if swapIndex is not null:
elements[index] = elements[swapIndex]
elements[swapIndex] = element
index = swapIndex
while swapIndex is not null
\nCode-block #2 — Pseudo-code for a Dijkstra’s algorithm implementation and a Priority Queue with Binary Heap. For the Typescript implementation please refer to the demo.\n\n
Now that we have our mesh represented as a graph, and our simple implementation of Dijkstra’s algorithm, we can set up a plain scene (in our case we’ve generated a basic mountainous terrain from a custom BufferGeometry acting as a Plane to use as our mesh — also using simplex noise and some logic to generate peaks and valleys and colour them accordingly) and apply our solution to this mesh. We’ve also added some interaction which allows users to simply click two consecutive vertices on this scene (those would be the little points displayed all over the mesh) and see the shortest path being generated using this solution.
\nhttps://medium.com/media/f2e386ad0d6292cdd5bc759574770426/hrefTo wrap this up, this implementation offers a robust and efficient solution for path-finding on mesh surfaces. By combining a simple node graph representation with Dijkstra’s algorithm, we’ve adapted a proven idea, using different technologies to handle the intricacies of mesh geometry with ease. This solution not only achieves the primary objective of finding the shortest path but also sets the stage for further optimizations and enhancements (for example by enhancing the graph structure to use a half-edge structure instead, or by improving the algorithm to follow the groves by enhancing the weighted edges with more information).
\nThis path-finding solution is already widely used across various domains. In the field of computer graphics and gaming, it can enhance the realism of virtual environments by enabling precise navigation on complex terrains. In robotics and autonomous systems, it can facilitate path planning on irregular surfaces, and in architectural design and urban planning, it might aid in visualizing spatial layouts and simulating movement. Additionally, it is used in scientific simulations, geological modelling, and medical imaging, where accurate path-finding is crucial. In our particular use case, it is helping us find the shortest path in 3D meshes representing medical data, enabling us to build precise medical editing tools.
\nOverall, we’re excited to see our solution re-used in other three.js projects that might need such a solution and look forward to the advancements it may inspire in the field of mesh surface path-finding.
\n[1] — Corey Trevena (2015), Algorithms Used in Pathfinding and Navigation Meshes 3 Concepts and Terminology: Basic Visual Representations. https://www.semanticscholar.org/paper/Algorithms-Used-in-Pathfinding-and-Navigation-3-and-Trevena/39a49c8b9863362a8fafa7fac4eab2ae2aff017d
\n[2] — Chia-Man Hung & Ruoqi He (2018), Pathfinding in
\n3D Space. https://ascane.github.io/assets/portfolio/pathfinding3d-slides-en.pdf
\n[3] — Fatima-Zahra, H., Farhaoui, Y. (2024). Analyzing the Shortest Path in a 3D Object Using a Reinforcement Learning Approach. In: Farhaoui, Y., Hussain, A., Saba, T., Taherdoost, H., Verma, A. (eds) Artificial Intelligence, Data Science and Applications. ICAISE 2023. Lecture Notes in Networks and Systems, vol 837. Springer, Cham. https://doi.org/10.1007/978-3-031-48465-0_74
\n[4] — Estefania Cassingena Navone (2020), Dijkstra’s Shortest Path Algorithm — A Detailed and Visual Introduction. https://www.freecodecamp.org/news/dijkstras-shortest-path-algorithm-visual-introduction/
\nOptimal Route Crafting: Finding the shortest path on the surface of a mesh was originally published in Promaton on Medium, where people are continuing the conversation by highlighting and responding to this story.
\n","content":"\nAs we are working with 3D dental data at Promaton, we recently had the chance to tackle an interesting project, where we got to explore a solution for finding the shortest path on the surface of a mesh, using our stack (React / Three.js / R3F). After researching and exploring this topic, we quickly realised that we’d have to dive into a custom implementation to better fit our needs, given that the available implementations using three.js (our preferred 3D library) seemed not to fit our specific needs. We are now proud to be sharing our journey through this fascinating topic, together with a step-by-step description of our implementation, the demo we created in the process and some interesting conclusions.
\nIn the dental domain, we are developing tools to edit the boundaries of a mesh. At first glance, this might seem straightforward, but it inevitably leads to a complex problem in mathematics and software engineering. How can we draw a path between two vertices on a mesh? Which is the most efficient path? These are the types of questions we found ourselves having to answer.
\nThere are several approaches to solving the problem of path-finding on mesh surfaces. One approach involves adapting traditional grid-based path-finding algorithms [1] [2], such as A* search or breadth-first search, to operate on a mesh representation. However, these methods may encounter challenges in accurately representing the irregular geometry of mesh surfaces and may require extensive preprocessing or manual subdivision of the mesh. Additionally, machine learning techniques [3], such as reinforcement learning or neural networks, offer another avenue for path-finding on meshes by learning navigation policies from data. However, these approaches may require significant computational resources for training and may not always generalize well to diverse mesh environments. Alternatively to these, there is also Dijkstra’s algorithm (using priority queues and weighted edges), which can efficiently handle the irregularities of mesh geometries and provide a straightforward implementation that guarantees the shortest path without extensive preprocessing, offering better scalability and adaptability with lower computational overhead compared to machine learning approaches.
\nAs we pondered on all the options described in the previous section and considered our use case and particular circumstances, we chose to approach this problem by implementing a simple node graph with priority queues and weighted nodes to represent mesh vertices. Using Dijkstra’s algorithm, we managed to efficiently calculate the shortest path between points, ensuring both accuracy (Dijkstra always guarantees the shortest path) and real-time performance. By utilizing priority queues (based on binary heaps) to efficiently explore neighbouring nodes, we were able to minimize computational overhead, rendering this solution scalable and adaptable to various mesh complexities, particularly the large and complex STL files from intra-oral scanners (as these are designed to capture the maximum amount of possible data) that we have to deal with.
\nThis code snippet defines classes for creating a graph representation of a mesh surface to facilitate path-finding. The Node class represents individual vertices on the mesh, storing their index, position (as a Vector3), and connections to neighbouring nodes. Edges between nodes are defined by the Edge class, which includes references to the connecting nodes and a weight indicating the distance between them. The Graph class manages the graph structure, storing node adjacency and providing methods for adding nodes and edges. Additionally, it includes functionality to check graph connectivity and compute the graph from a target mesh. During graph computation, vertex positions are extracted from the mesh geometry, and edges are created between adjacent vertices, with weights determined by their Euclidean distances. This code lays the foundation for efficient path-finding algorithms to navigate the complexities of the mesh surface. It can definitely be improved (for example by implementing a half-edge data structure) to further maximise the efficiency of traversal, something we will be looking into in the near future.
\n// Define a class to represent individual vertices on the mesh\n
class Node:
index: Integer
value: Vector3
edges: List of Edge
method constructor(index: Integer, value: Vector3, edges: List of Edge = empty list):
this.index = index
this.value = value
this.edges = edges
// Define a class to represent edges between nodes
class Edge:
node1: Node
node2: Node
weight: Number
method constructor(node1: Node, node2: Node, weight: Number):
this.node1 = node1
this.node2 = node2
this.weight = weight
// Define a class to manage the graph structure
class Graph:
adjacencyList: Map of Integer to List of Integer
nodes: Map of Integer to Node
target: Mesh (optional)
method constructor():
this.adjacencyList = empty map
this.nodes = empty map
method clear():
clear this.nodes
clear this.adjacencyList
// Method to add a new node to the graph
method addNode(index: Integer, value: Vector3):
if index not in this.nodes:
newNode = new Node(index, value)
this.nodes[index] = newNode
this.adjacencyList[index] = empty list
// Method to add a new edge to the graph
method addEdge(index1: Integer, index2: Integer, weight: Number = 1):
node1 = this.nodes[index1]
node2 = this.nodes[index2]
if node1 is null or node2 is null:
throw error \"Node not found\"
newEdge = new Edge(node1, node2, weight)
node1.edges.append(newEdge)
node2.edges.append(newEdge)
if index1 not in this.adjacencyList:
this.adjacencyList[index1] = empty list
if index2 not in this.adjacencyList:
this.adjacencyList[index2] = empty list
this.adjacencyList[index1].append(index2)
this.adjacencyList[index2].append(index1)
// Method to compute the graph from the target mesh
method computeGraph():
if this.target is null:
return
positions = get attribute \"position\" from this.target.geometry
indices = get index from this.target.geometry
if indices is null:
throw error \"Mesh geometry is not indexed. Can only compute indexed geometries.\"
vertex = new Vector3()
addedEdges = new Set()
// Add nodes to the graph
for i from 0 to count of positions:
vertex = get vertex from positions at index i
apply matrix of this.target to vertex
addNode(i, clone of vertex)
// Add edges to the graph
for i from 0 to count of indices with step 3:
indexA = get value from indices at index i
indexB = get value from indices at index i+1
indexC = get value from indices at index i+2
edgeAB = min(indexA, indexB) + \"-\" + max(indexA, indexB)
edgeBC = min(indexB, indexC) + \"-\" + max(indexB, indexC)
edgeCA = min(indexC, indexA) + \"-\" + max(indexC, indexA)
if edgeAB not in addedEdges:
weightAB = distance between nodes at indexA and indexB
addEdge(indexA, indexB, weightAB)
addedEdges.add(edgeAB)
if edgeBC not in addedEdges:
weightBC = distance between nodes at indexB and indexC
addEdge(indexB, indexC, weightBC)
addedEdges.add(edgeBC)
if edgeCA not in addedEdges:
weightCA = distance between nodes at indexC and indexA
addEdge(indexC, indexA, weightCA)
addedEdges.add(edgeCA)
\nCode-block #1 — Pseudo-code representation of a mesh as a graph structure with weighted edges. For the Typescript implementation please refer to the demo.\n\n
Dijkstra’s algorithm (/ˈdaɪkstrəz/ DYKE-strəz) is an algorithm for finding the shortest paths between nodes in a weighted graph, which may represent, for example, road networks. It was conceived by computer scientist Edsger W. Dijkstra in 1956 and published three years later.
\nDijkstra’s algorithm finds the shortest path from a given source node to every other node. It can also be used to find the shortest path to a specific destination node, by terminating the algorithm once the shortest path to the destination node is known. For example, if the nodes of the graph represent cities, and the costs of edges represent the average distances between pairs of cities connected by a direct road, then Dijkstra’s algorithm can be used to find the shortest route between one city and all other cities.
\nThe nature of Dijkstra’s algorithm makes it perfect for our use case [4], as this is exactly what we intend to do, finding the shortest path between two nodes of a graph, but in this particular case, the graph is our mesh as described in the previous section.
\nThe following snippet of code defines a function getShortestPath, that calculates the shortest path between two nodes on a graph, using Dijkstra’s algorithm. The function takes the start and end node indices, along with a map of nodes, where the key is the node index and the value is the node object. Inside the function, a priority queue is used to track nodes with their distances from the start node. The algorithm iterates through the graph, updating distances and previous nodes as it explores the graph. Once the shortest path is found, it constructs and returns an array of nodes representing the path. Additionally, the code includes a PriorityQueue class, implemented as a binary heap, which is used internally by the getShortestPath function to efficiently manage node priorities during traversal. If you’d like to read more about the Binary Heap Priority Queue implementation, please refer to this article, which served as our inspiration.
\n// Import the Node class\n
import Node from graph
/**
* Calculates the shortest path between two nodes using Dijkstra's algorithm.
* @param startIndex - The index of the start node.
* @param endIndex - The index of the end node.
* @param nodes - A map of nodes where the key is the node index and the value is the node itself.
* @returns An array of node values representing the shortest path, or null if no path is found.
*/
function getShortestPath(startIndex, endIndex, nodes):
startNode = nodes.get(startIndex)
endNode = nodes.get(endIndex)
if startNode is null or endNode is null:
print \"Start or end node is undefined\"
return null
distances = new Map()
previousNodes = new Map()
queue = new PriorityQueue()
distances.set(startNode, 0)
queue.enqueue(startNode, 0)
while not queue.isEmpty():
currentNode = queue.dequeue()
if currentNode == endNode:
path = empty list
node = currentNode
while node is not null:
path.insert_at_start(node)
node = previousNodes.get(node)
return path
for each edge in currentNode.edges:
adjacentNode = edge.node1 if edge.node1 != currentNode else edge.node2
currentDistance = distances.get(currentNode)
if currentDistance is null:
continue
newDistance = currentDistance + edge.weight
adjacentNodeDistance = distances.get(adjacentNode)
if adjacentNodeDistance is null or newDistance < adjacentNodeDistance:
distances.set(adjacentNode, newDistance)
previousNodes.set(adjacentNode, currentNode)
queue.enqueue(adjacentNode, newDistance)
print \"Failed to find path\"
return null
/**
* A priority queue implemented as a binary heap.
*/
class PriorityQueue:
elements = empty list of {node, priority}
/**
* Adds a node to the queue with a given priority.
* @param node - The node to add.
* @param priority - The priority of the node.
*/
method enqueue(node, priority):
elements.append({node, priority})
bubbleUp(size of elements - 1)
/**
* Removes and returns the node with the highest priority.
* @returns The node with the highest priority.
*/
method dequeue():
first = elements[0]
last = elements.pop()
if size of elements > 0:
elements[0] = last
bubbleDown(0)
return first.node
/**
* Checks if the queue is empty.
* @returns True if the queue is empty, false otherwise.
*/
method isEmpty():
return size of elements == 0
/**
* Moves a node up in the tree, i.e., towards the root.
* @param index - The index of the node to move up.
*/
method bubbleUp(index):
while index > 0:
parentIndex = floor((index - 1) / 2)
if elements[parentIndex].priority <= elements[index].priority:
break
swap elements[parentIndex] with elements[index]
index = parentIndex
/**
* Moves a node down in the tree, i.e., away from the root.
* @param index - The index of the node to move down.
*/
method bubbleDown(index):
length = size of elements
element = elements[index]
swapIndex = null
do:
swapIndex = null
leftChildIndex = 2 * index + 1
rightChildIndex = 2 * index + 2
if leftChildIndex < length and elements[leftChildIndex].priority < element.priority:
swapIndex = leftChildIndex
if rightChildIndex < length:
if swapIndex is null and elements[rightChildIndex].priority < element.priority:
swapIndex = rightChildIndex
elif swapIndex is not null and elements[rightChildIndex].priority < elements[leftChildIndex].priority:
swapIndex = rightChildIndex
if swapIndex is not null:
elements[index] = elements[swapIndex]
elements[swapIndex] = element
index = swapIndex
while swapIndex is not null
\nCode-block #2 — Pseudo-code for a Dijkstra’s algorithm implementation and a Priority Queue with Binary Heap. For the Typescript implementation please refer to the demo.\n\n
Now that we have our mesh represented as a graph, and our simple implementation of Dijkstra’s algorithm, we can set up a plain scene (in our case we’ve generated a basic mountainous terrain from a custom BufferGeometry acting as a Plane to use as our mesh — also using simplex noise and some logic to generate peaks and valleys and colour them accordingly) and apply our solution to this mesh. We’ve also added some interaction which allows users to simply click two consecutive vertices on this scene (those would be the little points displayed all over the mesh) and see the shortest path being generated using this solution.
\nhttps://medium.com/media/f2e386ad0d6292cdd5bc759574770426/hrefTo wrap this up, this implementation offers a robust and efficient solution for path-finding on mesh surfaces. By combining a simple node graph representation with Dijkstra’s algorithm, we’ve adapted a proven idea, using different technologies to handle the intricacies of mesh geometry with ease. This solution not only achieves the primary objective of finding the shortest path but also sets the stage for further optimizations and enhancements (for example by enhancing the graph structure to use a half-edge structure instead, or by improving the algorithm to follow the groves by enhancing the weighted edges with more information).
\nThis path-finding solution is already widely used across various domains. In the field of computer graphics and gaming, it can enhance the realism of virtual environments by enabling precise navigation on complex terrains. In robotics and autonomous systems, it can facilitate path planning on irregular surfaces, and in architectural design and urban planning, it might aid in visualizing spatial layouts and simulating movement. Additionally, it is used in scientific simulations, geological modelling, and medical imaging, where accurate path-finding is crucial. In our particular use case, it is helping us find the shortest path in 3D meshes representing medical data, enabling us to build precise medical editing tools.
\nOverall, we’re excited to see our solution re-used in other three.js projects that might need such a solution and look forward to the advancements it may inspire in the field of mesh surface path-finding.
\n[1] — Corey Trevena (2015), Algorithms Used in Pathfinding and Navigation Meshes 3 Concepts and Terminology: Basic Visual Representations. https://www.semanticscholar.org/paper/Algorithms-Used-in-Pathfinding-and-Navigation-3-and-Trevena/39a49c8b9863362a8fafa7fac4eab2ae2aff017d
\n[2] — Chia-Man Hung & Ruoqi He (2018), Pathfinding in
\n3D Space. https://ascane.github.io/assets/portfolio/pathfinding3d-slides-en.pdf
\n[3] — Fatima-Zahra, H., Farhaoui, Y. (2024). Analyzing the Shortest Path in a 3D Object Using a Reinforcement Learning Approach. In: Farhaoui, Y., Hussain, A., Saba, T., Taherdoost, H., Verma, A. (eds) Artificial Intelligence, Data Science and Applications. ICAISE 2023. Lecture Notes in Networks and Systems, vol 837. Springer, Cham. https://doi.org/10.1007/978-3-031-48465-0_74
\n[4] — Estefania Cassingena Navone (2020), Dijkstra’s Shortest Path Algorithm — A Detailed and Visual Introduction. https://www.freecodecamp.org/news/dijkstras-shortest-path-algorithm-visual-introduction/
\nOptimal Route Crafting: Finding the shortest path on the surface of a mesh was originally published in Promaton on Medium, where people are continuing the conversation by highlighting and responding to this story.
\n","enclosure":{},"categories":["shortest-path","dijkstras-algorithm","tech","computer-graphics","threejs"]},{"title":"The onboarding ascent: climbing the peaks of ownership and trust","pubDate":"2023-11-20 12:05:04","link":"https://blog.promaton.com/the-on-boarding-ascent-climbing-the-peaks-of-ownership-and-trust-172df3b5b037?source=rss-7a313bdb96ad------2","guid":"https://medium.com/p/172df3b5b037","author":"Christian Marques","thumbnail":"","description":"\nIt’s now roughly six months since I joined Promaton as a front-end engineer working intimately with WebGL/three.js. Despite the course of time, I’m still feeling the excitement from my first weeks, which I’d love to share with you, as it has been one of the most interesting and thrilling onboarding experiences of my career.
\nNew joiners at Promaton are usually invited to tackle an onboarding project, for about a month’s time, which is typically suggested by the team they will be working with. Generally, it is a relatively open-ended assignment.
\nThe reasoning behind this is that new joiners can start their life at Promaton by getting familiar with the stack, technologies, processes, and team without the pressure of meddling in production code. This also provides them with more agency, ownership, and accountability for their project, on which they have to work by themselves.
\nMy onboarding assignment was to build a prototype (or rather proof-of-concept) based on a series of hypotheses that had been determined by my team. The structure of the assignment presented the problems (mostly user experience pain points) and some of the hypotheses that had been brainstormed by the team in order to attempt and improve on these issues. From there, I was given total autonomy to consider how it would be approached. I approached it by researching the existing experience, outlining the user journeys and verifying the described pain points — which then led me to start writing detailed technical documentation, illustrating how the problem would be approached (Fig.1 & 2). I was given the freedom to choose some parts of the stack and design the architecture I felt most comfortable with, and had some technical discussions with the team, helping to validate that the path forward would be a fruitful one.
\nDuring the course of a month, I was given the opportunity, freedom and ownership to build a prototype tackling some high-impact user experience issues. This is because we believe that you need to empower people if you want them to reach high-impact goals. Without wanting to go too much into detail, we’re talking about a web application that leverages Promaton’s AI and 3D visualisations to greatly improve dental clinicians’ lives, by saving them time and effort by automating implant and restorative treatment planning and design. I also had a huge amount of support from the extended team, who very promptly were ready to brainstorm, answer questions, suggest solutions and help with various infrastructure-related hurdles.
\nDuring this time, I had the possibility and agency to build this piece of software and experiment with different technologies I had been curious about. I had the chance to decide on the technical options, and in that process also learned a lot about the business context of Promaton, but also about the products, not only the ones owned by my team but also the wider-range portfolio of products, some of which I had to integrate.
\nRoughly one month later, I was presenting the prototype (still quite incipient at this point, as you might imagine, Fig.4) to the broader Promaton team, who provided very constructive feedback and widely celebrated the efforts made in this journey. The first iteration was completed successfully and well received. In the middle of this journey, there was also a very fun week-long hackathon, where I completely changed the context for a full week and made a little interactive map for displaying the geolocation data of our API usage — (Fig.3)
\nLittle did I know that this onboarding project would turn out to be so relevant, that after this intense month, we’d continue to work on it.
\nWe performed a series of user interviews with real clinicians, and based on that feedback, continued iterating on the prototype for the next couple of months.
\nAt a point, it became ready (we could consider it a completed second iteration, Fig. 5) to be presented to the extended team and technology leadership of our parent company.
\nThis led to a brief moment of nervousness for me: showcasing the prototype I had been working on in an all-hands meeting with nearly a hundred people watching.
\nHowever, this quickly dissipated as the team continued to be supportive and constantly reiterated the practical value and potential of the work that had been done.
\nIt ended up being a successful presentation, the feedback was overwhelmingly positive.
\nWe now continue to work on this project with greater support from our parent company and continuous positive reception, which is where we are at the moment: performing user interviews and working on a third iteration based on that feedback.
\nThis leads me to the end of the story per se, but I still want to reflect on this journey.
\nI believe it not only very well represents a lot of the Promaton values, but also constitutes an interesting onboarding experience that is rarely seen throughout our industry, as usually, engineers joining a new company are typically given menial and routine tasks to start with and get accustomed to the codebase, the processes and the team.
\nWith this very particular format of onboarding, Promaton is successfully promoting key pillars of its (engineering) culture:
\nFeeling the real impact of your work, being given the opportunity to demonstrate your value and getting overwhelming support from your peers to achieve great results: this is certainly one of the best and most exciting ways to start working in a new company.
\nThe onboarding ascent: climbing the peaks of ownership and trust was originally published in Promaton on Medium, where people are continuing the conversation by highlighting and responding to this story.
\n","content":"\nIt’s now roughly six months since I joined Promaton as a front-end engineer working intimately with WebGL/three.js. Despite the course of time, I’m still feeling the excitement from my first weeks, which I’d love to share with you, as it has been one of the most interesting and thrilling onboarding experiences of my career.
\nNew joiners at Promaton are usually invited to tackle an onboarding project, for about a month’s time, which is typically suggested by the team they will be working with. Generally, it is a relatively open-ended assignment.
\nThe reasoning behind this is that new joiners can start their life at Promaton by getting familiar with the stack, technologies, processes, and team without the pressure of meddling in production code. This also provides them with more agency, ownership, and accountability for their project, on which they have to work by themselves.
\nMy onboarding assignment was to build a prototype (or rather proof-of-concept) based on a series of hypotheses that had been determined by my team. The structure of the assignment presented the problems (mostly user experience pain points) and some of the hypotheses that had been brainstormed by the team in order to attempt and improve on these issues. From there, I was given total autonomy to consider how it would be approached. I approached it by researching the existing experience, outlining the user journeys and verifying the described pain points — which then led me to start writing detailed technical documentation, illustrating how the problem would be approached (Fig.1 & 2). I was given the freedom to choose some parts of the stack and design the architecture I felt most comfortable with, and had some technical discussions with the team, helping to validate that the path forward would be a fruitful one.
\nDuring the course of a month, I was given the opportunity, freedom and ownership to build a prototype tackling some high-impact user experience issues. This is because we believe that you need to empower people if you want them to reach high-impact goals. Without wanting to go too much into detail, we’re talking about a web application that leverages Promaton’s AI and 3D visualisations to greatly improve dental clinicians’ lives, by saving them time and effort by automating implant and restorative treatment planning and design. I also had a huge amount of support from the extended team, who very promptly were ready to brainstorm, answer questions, suggest solutions and help with various infrastructure-related hurdles.
\nDuring this time, I had the possibility and agency to build this piece of software and experiment with different technologies I had been curious about. I had the chance to decide on the technical options, and in that process also learned a lot about the business context of Promaton, but also about the products, not only the ones owned by my team but also the wider-range portfolio of products, some of which I had to integrate.
\nRoughly one month later, I was presenting the prototype (still quite incipient at this point, as you might imagine, Fig.4) to the broader Promaton team, who provided very constructive feedback and widely celebrated the efforts made in this journey. The first iteration was completed successfully and well received. In the middle of this journey, there was also a very fun week-long hackathon, where I completely changed the context for a full week and made a little interactive map for displaying the geolocation data of our API usage — (Fig.3)
\nLittle did I know that this onboarding project would turn out to be so relevant, that after this intense month, we’d continue to work on it.
\nWe performed a series of user interviews with real clinicians, and based on that feedback, continued iterating on the prototype for the next couple of months.
\nAt a point, it became ready (we could consider it a completed second iteration, Fig. 5) to be presented to the extended team and technology leadership of our parent company.
\nThis led to a brief moment of nervousness for me: showcasing the prototype I had been working on in an all-hands meeting with nearly a hundred people watching.
\nHowever, this quickly dissipated as the team continued to be supportive and constantly reiterated the practical value and potential of the work that had been done.
\nIt ended up being a successful presentation, the feedback was overwhelmingly positive.
\nWe now continue to work on this project with greater support from our parent company and continuous positive reception, which is where we are at the moment: performing user interviews and working on a third iteration based on that feedback.
\nThis leads me to the end of the story per se, but I still want to reflect on this journey.
\nI believe it not only very well represents a lot of the Promaton values, but also constitutes an interesting onboarding experience that is rarely seen throughout our industry, as usually, engineers joining a new company are typically given menial and routine tasks to start with and get accustomed to the codebase, the processes and the team.
\nWith this very particular format of onboarding, Promaton is successfully promoting key pillars of its (engineering) culture:
\nFeeling the real impact of your work, being given the opportunity to demonstrate your value and getting overwhelming support from your peers to achieve great results: this is certainly one of the best and most exciting ways to start working in a new company.
\nThe onboarding ascent: climbing the peaks of ownership and trust was originally published in Promaton on Medium, where people are continuing the conversation by highlighting and responding to this story.
\n","enclosure":{},"categories":["front-end-development","onboarding","company-culture","visualization","3d"]},{"title":"Finding Peace in the Silence: A Zen Approach to Being Ghosted by a Recruiter","pubDate":"2023-06-13 14:36:00","link":"https://medium.com/@christian.marques/finding-peace-in-the-silence-a-zen-approach-to-being-ghosted-by-a-recruiter-750a7465b888?source=rss-7a313bdb96ad------2","guid":"https://medium.com/p/750a7465b888","author":"Christian Marques","thumbnail":"","description":"\nI’ve recently had a couple of experiences in the job market as a software engineer where after an entire recruitment process, the feedback just didn’t arrive, and emails weren’t replied to, which left me confused and struggling with unexpected feelings of frustration, rejection and self-worth.
\nIn the world of job searching, it’s not uncommon for candidates to go through an entire recruitment process, only to receive no feedback and have their emails go unanswered. However, there are ways to deal with these situations and let go of negative feelings. Some have found that Eastern philosophies, particularly Zen Buddhism, offer useful philosophical lessons and approaches that can be applied to such situations, as well as many other complex, fast-moving and frustrating situations in life.
\nZen Buddhism is a school of Buddhism that originated in China during the Tang dynasty, and later spread to other parts of East Asia, including Japan. It emphasizes the practice of meditation and mindfulness as a means of attaining enlightenment, or a state of deep awareness and understanding.
\nZen Buddhism is often associated with simplicity, directness, and a focus on the present moment. Practitioners of Zen Buddhism seek to cultivate a sense of peace, clarity, and compassion in their lives, and often use meditation and other practices to help them achieve these goals.
\nIn Zen Buddhism, there is also an emphasis on non-dualistic thinking, or the idea that there is no inherent separation between oneself and the world around them. This perspective encourages practitioners to see the interconnectedness of all things and to approach life with a sense of openness and curiosity.
\nOverall, Zen Buddhism is a philosophy and practice that emphasizes mindfulness, compassion, and non-attachment as a means of achieving inner peace and enlightenment. Its principles can be applied to many aspects of life, including dealing with difficult situations like being ghosted by a recruiter.
\nZen Buddhism emphasizes the importance of letting go of attachment to outcomes and accepting things as they are. Try to let go of any expectations you had about the recruiter getting back to you, and accept that they may not have been the right fit for you.
\nPay attention to your thoughts and emotions as they arise, without judgment or attachment. Observe any feelings of disappointment or frustration that come up and let them pass without clinging to them. Understand that these feelings are natural and part of being a human being, but practice and direct your non-attachment efforts towards them.
\nRather than dwelling on the past or worrying about the future, focus on the present moment. This can help you stay grounded and centered, and may also help you notice other opportunities that arise. Past and future are, in many cases, constructions our minds create. By immersing ourselves fully in the present moment, we cultivate a sense of awareness and clarity that transcends the limitations of time and learn to appreciate the beauty and richness of each passing moment, realizing that the present is the only moment that truly exists. Embracing the present moment allows us to live authentically and fully, unfettered by regrets or anxieties, and to discover the peace that lies within.
\nRemember that the recruiter who ghosted you is also a human being, with their own struggles and challenges. Try to cultivate compassion for them, rather than anger or resentment. A good way to do so is to try and put yourself in their shoes and empathize with their experience. Recognize that they may be dealing with their own pressures, deadlines, or personal difficulties that led to their actions. By shifting our perspective from one of judgment to understanding, we open ourselves to a deeper sense of compassion.
\nFocus on the things in your life that you are grateful for, rather than dwelling on the disappointment of being ghosted by a recruiter. This can help you maintain a positive outlook and cultivate a sense of contentment and joy. Fortunately, the software industry is a thriving one and even if you didn’t land this particular job, there are still numerous opportunities awaiting you. Instead of fixating on the missed opportunity, redirect your attention towards gratitude for the skills and experiences you possess, as well as the potential for growth and new possibilities. By embracing gratitude, you open yourself up to a world of endless potential and cultivate a resilient spirit that can navigate through life’s challenges with grace and optimism, which definitely result in eventually landing the job you were looking forward to.
\nBeing ghosted by a recruiter can be frustrating and disappointing, but it’s important to remember that it’s a common occurrence in the job search process. Essentially, being ghosted means that a recruiter you’ve been in contact with suddenly stops responding to your emails or calls, leaving you unsure about the status of your job application or the recruitment process. This can happen for a variety of reasons, such as the recruiter getting busy, the position being filled, or the company changing their hiring plans.
\nHowever, approaching the situation from a Zen Buddhism perspective can help you find peace and acceptance. Zen Buddhism emphasizes the importance of non-attachment to outcomes and accepting things as they are. To deal with being ghosted in a Zen way, you can practice non-attachment by letting go of any expectations you had about the recruiter getting back to you, and accepting that they may not have been the right fit for you. You can also practice mindfulness by paying attention to your thoughts and emotions as they arise, without judgment or attachment, and focus on the present moment rather than dwelling on the past or worrying about the future. Cultivating compassion for the recruiter who ghosted you can also help you move on in a positive way.
\nIt’s important to remember that being ghosted by a recruiter is not a reflection of your worth or abilities. Instead, it’s a common occurrence that can happen to anyone during the job search process. While it’s understandable to feel frustrated and disappointed, taking advantage of Zen Buddhism techniques can help you find peace and acceptance, and move forward with a positive outlook, stronger than ever.
\nI’ve recently had a couple of experiences in the job market as a software engineer where after an entire recruitment process, the feedback just didn’t arrive, and emails weren’t replied to, which left me confused and struggling with unexpected feelings of frustration, rejection and self-worth.
\nIn the world of job searching, it’s not uncommon for candidates to go through an entire recruitment process, only to receive no feedback and have their emails go unanswered. However, there are ways to deal with these situations and let go of negative feelings. Some have found that Eastern philosophies, particularly Zen Buddhism, offer useful philosophical lessons and approaches that can be applied to such situations, as well as many other complex, fast-moving and frustrating situations in life.
\nZen Buddhism is a school of Buddhism that originated in China during the Tang dynasty, and later spread to other parts of East Asia, including Japan. It emphasizes the practice of meditation and mindfulness as a means of attaining enlightenment, or a state of deep awareness and understanding.
\nZen Buddhism is often associated with simplicity, directness, and a focus on the present moment. Practitioners of Zen Buddhism seek to cultivate a sense of peace, clarity, and compassion in their lives, and often use meditation and other practices to help them achieve these goals.
\nIn Zen Buddhism, there is also an emphasis on non-dualistic thinking, or the idea that there is no inherent separation between oneself and the world around them. This perspective encourages practitioners to see the interconnectedness of all things and to approach life with a sense of openness and curiosity.
\nOverall, Zen Buddhism is a philosophy and practice that emphasizes mindfulness, compassion, and non-attachment as a means of achieving inner peace and enlightenment. Its principles can be applied to many aspects of life, including dealing with difficult situations like being ghosted by a recruiter.
\nZen Buddhism emphasizes the importance of letting go of attachment to outcomes and accepting things as they are. Try to let go of any expectations you had about the recruiter getting back to you, and accept that they may not have been the right fit for you.
\nPay attention to your thoughts and emotions as they arise, without judgment or attachment. Observe any feelings of disappointment or frustration that come up and let them pass without clinging to them. Understand that these feelings are natural and part of being a human being, but practice and direct your non-attachment efforts towards them.
\nRather than dwelling on the past or worrying about the future, focus on the present moment. This can help you stay grounded and centered, and may also help you notice other opportunities that arise. Past and future are, in many cases, constructions our minds create. By immersing ourselves fully in the present moment, we cultivate a sense of awareness and clarity that transcends the limitations of time and learn to appreciate the beauty and richness of each passing moment, realizing that the present is the only moment that truly exists. Embracing the present moment allows us to live authentically and fully, unfettered by regrets or anxieties, and to discover the peace that lies within.
\nRemember that the recruiter who ghosted you is also a human being, with their own struggles and challenges. Try to cultivate compassion for them, rather than anger or resentment. A good way to do so is to try and put yourself in their shoes and empathize with their experience. Recognize that they may be dealing with their own pressures, deadlines, or personal difficulties that led to their actions. By shifting our perspective from one of judgment to understanding, we open ourselves to a deeper sense of compassion.
\nFocus on the things in your life that you are grateful for, rather than dwelling on the disappointment of being ghosted by a recruiter. This can help you maintain a positive outlook and cultivate a sense of contentment and joy. Fortunately, the software industry is a thriving one and even if you didn’t land this particular job, there are still numerous opportunities awaiting you. Instead of fixating on the missed opportunity, redirect your attention towards gratitude for the skills and experiences you possess, as well as the potential for growth and new possibilities. By embracing gratitude, you open yourself up to a world of endless potential and cultivate a resilient spirit that can navigate through life’s challenges with grace and optimism, which definitely result in eventually landing the job you were looking forward to.
\nBeing ghosted by a recruiter can be frustrating and disappointing, but it’s important to remember that it’s a common occurrence in the job search process. Essentially, being ghosted means that a recruiter you’ve been in contact with suddenly stops responding to your emails or calls, leaving you unsure about the status of your job application or the recruitment process. This can happen for a variety of reasons, such as the recruiter getting busy, the position being filled, or the company changing their hiring plans.
\nHowever, approaching the situation from a Zen Buddhism perspective can help you find peace and acceptance. Zen Buddhism emphasizes the importance of non-attachment to outcomes and accepting things as they are. To deal with being ghosted in a Zen way, you can practice non-attachment by letting go of any expectations you had about the recruiter getting back to you, and accepting that they may not have been the right fit for you. You can also practice mindfulness by paying attention to your thoughts and emotions as they arise, without judgment or attachment, and focus on the present moment rather than dwelling on the past or worrying about the future. Cultivating compassion for the recruiter who ghosted you can also help you move on in a positive way.
\nIt’s important to remember that being ghosted by a recruiter is not a reflection of your worth or abilities. Instead, it’s a common occurrence that can happen to anyone during the job search process. While it’s understandable to feel frustrated and disappointed, taking advantage of Zen Buddhism techniques can help you find peace and acceptance, and move forward with a positive outlook, stronger than ever.
\n