\ No newline at end of file
diff --git a/_projects/toothbrush_shelf.md b/_projects/toothbrush_shelf.md
index 3d2d230..854ea0e 100644
--- a/_projects/toothbrush_shelf.md
+++ b/_projects/toothbrush_shelf.md
@@ -25,7 +25,7 @@ head: |
---
-
+
Loading model...
\ No newline at end of file
diff --git a/_projects/usbc_power_station.md b/_projects/usbc_power_station.md
index 69bc3f5..de64158 100644
--- a/_projects/usbc_power_station.md
+++ b/_projects/usbc_power_station.md
@@ -85,35 +85,39 @@ I've put a 240x240 pixel colour screen on the front to show metrics like total c
## Electronics
-Because I am taking this way to far, I wanted to do per port enable/disable and current monitoring. To implement this I'm designing a PCB with 5 channels where each channel consists of this schematic.
+Because I am taking this way too far, I wanted to do per port enable/disable and current monitoring. To implement this I'm designing a PCB with 5 channels where each channel consists of this schematic.
-There's an INA219 and a shunt resistor for current and voltage monitoring and a chunky MOSFET for enabling and disabling the channel.
-
-TODO:
-Check the power dissipated in the MOSFET when the gate is driven at 3.3V
-
-Check the inrush current when the MOSFET switches on and off, could potentially limit this by using a larger gate resistor to turn the MOSFET on more slowly.
+There's an INA219 and a shunt resistor for current and voltage monitoring and a chunky MOSFET for enabling and disabling the channel.
+For now I've broken the functionality for one channel out into a test board that I've sent off to JLCPB for manufacturing with and to be populated with SMT components. This ended up costing about 50 dollars for 5 boards. In future I want to have a go at doing the component placement and reflow myself.
+
+
+
+
Loading model...
+
+
## Software
In other posts I've described how I made this simulator the test out possible GUIs for this thing.
TODO: Add some knobs to the simulator so you can test different conditions such as overcurrent, overtemp, sleep, nightmode etc.
-
+
-
+
+
diff --git a/_projects/vector_magnet.md b/_projects/vector_magnet.md
index 5e5739e..8f73edd 100644
--- a/_projects/vector_magnet.md
+++ b/_projects/vector_magnet.md
@@ -24,7 +24,7 @@ head: |
---
-
+
Loading model...
@@ -35,7 +35,7 @@ Check out a little interactive model of the magnetometer below. The device has t
Here's a cutaway view of the interior.
-
+
Loading model...
diff --git a/assets/blog/micropython/simulator.js b/assets/blog/micropython/simulator.js
index 8c0be92..2c16f83 100644
--- a/assets/blog/micropython/simulator.js
+++ b/assets/blog/micropython/simulator.js
@@ -95,6 +95,24 @@ class USBCPowerSupplySimulator extends HTMLElement {
run_button.onclick = runPython;
if (editor_disabled) run_button.style.display = "none";
runPython();
+
+ // Only start simulation when the element is visible
+ const observer = new IntersectionObserver((entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting) {
+ console.log(
+ "Micropython simulator is visible. Starting execution..."
+ );
+ runPython();
+ } else {
+ console.log(
+ "Micropython simulator is not visible. Would pause if I could..."
+ );
+ }
+ });
+ });
+
+ observer.observe(this);
}
constructor() {
diff --git a/assets/js/outline-model-viewer/FindSurfaces.js b/assets/js/outline-model-viewer/FindSurfaces.js
index ca26bff..dc343a0 100644
--- a/assets/js/outline-model-viewer/FindSurfaces.js
+++ b/assets/js/outline-model-viewer/FindSurfaces.js
@@ -12,7 +12,7 @@ class FindSurfaces {
constructor() {
// This identifier, must be globally unique for each surface
// across all geometry rendered on screen
- this.surfaceId = 0;
+ this.surfaceId = 10;
}
/*
@@ -21,7 +21,18 @@ class FindSurfaces {
getSurfaceIdAttribute(mesh) {
const bufferGeometry = mesh.geometry;
const numVertices = bufferGeometry.attributes.position.count;
- const vertexIdToSurfaceId = this._generateSurfaceIds(mesh);
+
+ // Check if "track" or "pad" is in the name of the mesh
+ let idOverride = null;
+ if (
+ mesh.name.includes("track") ||
+ mesh.name.includes("pad") ||
+ mesh.name.includes("zone")
+ ) {
+ idOverride = 1;
+ }
+
+ const vertexIdToSurfaceId = this._generateSurfaceIds(mesh, idOverride);
const colors = [];
for (let i = 0; i < numVertices; i++) {
@@ -39,7 +50,7 @@ class FindSurfaces {
* Returns a `vertexIdToSurfaceId` map
* given a vertex, returns the surfaceId
*/
- _generateSurfaceIds(mesh) {
+ _generateSurfaceIds(mesh, idOverride = null) {
const bufferGeometry = mesh.geometry;
const numVertices = bufferGeometry.attributes.position.count;
const numIndices = bufferGeometry.index.count;
@@ -78,7 +89,7 @@ class FindSurfaces {
// Mark them as explored
for (let v of surfaceVertices) {
exploredNodes[v] = true;
- vertexIdToSurfaceId[v] = this.surfaceId;
+ vertexIdToSurfaceId[v] = idOverride || this.surfaceId;
}
this.surfaceId += 1;
@@ -166,4 +177,4 @@ function getFragmentShader() {
gl_FragColor = vec4(surfaceId, 0.0, 0.0, 1.0);
}
`;
-}
\ No newline at end of file
+}
diff --git a/assets/js/outline-model-viewer/index.js b/assets/js/outline-model-viewer/index.js
index 85a7546..6908502 100644
--- a/assets/js/outline-model-viewer/index.js
+++ b/assets/js/outline-model-viewer/index.js
@@ -10,42 +10,95 @@ import { RenderPass } from "three/addons/postprocessing/RenderPass.js";
import { ShaderPass } from "three/addons/postprocessing/ShaderPass.js";
import { FXAAShader } from "three/addons/shaders/FXAAShader.js";
-import GUI from 'lil-gui'
+import GUI from "lil-gui";
import { CustomOutlinePass } from "./CustomOutlinePass.js";
import FindSurfaces from "./FindSurfaces.js";
-
// Todo:
// Swap in the version of this code that has a debug GUI behind a flag
// Consider support for transparent objects by rendering them as a wireframe in the color and excluding them from the edge pass.
-// Fix the cetnering and scaling
// Switch to an angled isometric camera to match the style from the main page.
+// Function to lighten or darken a color based on brightness
+function adjustColor(color, factor) {
+ const hsl = color.getHSL({}); // Get the HSL values of the current color
+ if (hsl.l > 0.7) {
+ // If the color is light, darken it
+ hsl.l = Math.max(0, hsl.l - factor);
+ } else {
+ // If the color is dark, lighten it
+ hsl.l = Math.min(1, hsl.l + factor);
+ }
+ color.setHSL(hsl.h, hsl.s, hsl.l); // Set the adjusted color
+}
+
+function printGLTFScene(scene, maxDepth = 3, depth = 0, indent = 0) {
+ // Helper function to format the output
+ if (depth > maxDepth) {
+ return;
+ }
+ const pad = (level) => " ".repeat(level * 2);
+
+ // Recursively print scene contents
+ scene.traverse((object) => {
+ console.log(
+ `${pad(indent)}- ${object.type} ${
+ object.name || "(unnamed)"
+ } | Position: (${object.position.x.toFixed(
+ 2
+ )}, ${object.position.y.toFixed(2)}, ${object.position.z.toFixed(2)})`
+ );
+
+ if (object.children && object.children.length > 0) {
+ console.log(`${pad(indent + 1)}Children:`);
+ object.children.forEach((child) =>
+ printGLTFScene(child, maxDepth, depth + 1, indent + 2)
+ );
+ }
+ });
+}
+
const serialiseCamera = (camera, controls) => {
const position = Object.values(camera.position);
- const extractXYZ = ({_x, _y, _z}) => [_x, _y, _z];
+ const extractXYZ = ({ _x, _y, _z }) => [_x, _y, _z];
const rotation = extractXYZ(camera.rotation);
- const fixed = (l) => l.map( x => parseFloat(x.toPrecision(4)))
+ const fixed = (l) => l.map((x) => parseFloat(x.toPrecision(4)));
return JSON.stringify({
- position: fixed(position),
- rotation: fixed(rotation),
- zoom: camera.zoom,
- target: fixed(Object.values(controls.target)),
- });
+ position: fixed(position),
+ rotation: fixed(rotation),
+ zoom: camera.zoom,
+ target: fixed(Object.values(controls.target)),
+ });
};
class OutlineModelViewer extends HTMLElement {
constructor() {
super();
-
- let component_rect = this.getBoundingClientRect();
- console.log("component_rect", component_rect);
-
+ this.isVisible = true; // Track visibility
this.shadow = this.attachShadow({ mode: "open" });
+
+ // Mouse and raycaster
+ this.raycaster = new THREE.Raycaster();
+ this.mouse = new THREE.Vector2();
+ this.intersectedObject = null; // Store currently intersected object
+ }
+
+ // Handle mouse movement and update mouse coordinates
+ onMouseMove(event, canvas) {
+ const rect = canvas.getBoundingClientRect();
+ this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
+ this.mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
+ }
+
+ connectedCallback() {
+ let component_rect = this.getBoundingClientRect();
+
this.render(component_rect.height);
- const model_path = this.getAttribute("model") || "/assets/projects/bike_lights/models/bigger.glb";
- const spin = (this.getAttribute("spin") || 'true') === 'true'
+ const model_path =
+ this.getAttribute("model") ||
+ "/assets/projects/bike_lights/models/bigger.glb";
+ const spin = (this.getAttribute("spin") || "true") === "true";
const container = this.shadow.querySelector("div#container");
const canvas = this.shadow.querySelector("canvas");
@@ -53,7 +106,7 @@ class OutlineModelViewer extends HTMLElement {
let canvas_rect = canvas.getBoundingClientRect();
console.log(canvas_rect);
- // determine the outline and bg colors
+ // determine the outline and bg colors
const body = document.getElementsByTagName("body")[0];
const style = window.getComputedStyle(body);
const outline_color = style.getPropertyValue("--theme-model-line-color");
@@ -61,8 +114,15 @@ class OutlineModelViewer extends HTMLElement {
// // Init scene
// const camera = new THREE.PerspectiveCamera(70, canvas_rect.width / canvas_rect.height, 0.1, 100);
- const camera = new THREE.OrthographicCamera( canvas_rect.width / - 2, canvas_rect.width / 2, canvas_rect.height / 2, canvas_rect.height / - 2, 1, 1000 );
- camera.zoom = parseFloat(this.getAttribute("zoom") || "1")
+ const camera = new THREE.OrthographicCamera(
+ canvas_rect.width / -2,
+ canvas_rect.width / 2,
+ canvas_rect.height / 2,
+ canvas_rect.height / -2,
+ 1,
+ 1000
+ );
+ camera.zoom = parseFloat(this.getAttribute("zoom") || "1");
camera.position.set(10, 2.5, 4);
// create the scene and the camera
@@ -75,7 +135,7 @@ class OutlineModelViewer extends HTMLElement {
// renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize(canvas_rect.width, canvas_rect.height, false);
-
+
const light = new THREE.DirectionalLight(0xffffff, 2);
scene.add(light);
light.position.set(1.7, 1, -1);
@@ -87,8 +147,8 @@ class OutlineModelViewer extends HTMLElement {
// See: https://threejs.org/docs/index.html#api/en/renderers/WebGLRenderTarget.depthBuffer
const depthTexture = new THREE.DepthTexture();
const renderTarget = new THREE.WebGLRenderTarget(
- 2*canvas_rect.width,
- 2*canvas_rect.height,
+ 2 * canvas_rect.width,
+ 2 * canvas_rect.height,
{
depthTexture: depthTexture,
depthBuffer: true,
@@ -102,18 +162,18 @@ class OutlineModelViewer extends HTMLElement {
// Outline pass.
const customOutline = new CustomOutlinePass(
- new THREE.Vector2(2*canvas_rect.width, 2*canvas_rect.height),
+ new THREE.Vector2(2 * canvas_rect.width, 2 * canvas_rect.height),
scene,
camera,
- outline_color,
+ outline_color
);
composer.addPass(customOutline);
// Antialias pass.
const effectFXAA = new ShaderPass(FXAAShader);
effectFXAA.uniforms["resolution"].value.set(
- .5 / canvas_rect.width,
- .5 / canvas_rect.height
+ 0.5 / canvas_rect.width,
+ 0.5 / canvas_rect.height
);
composer.addPass(effectFXAA);
@@ -121,14 +181,31 @@ class OutlineModelViewer extends HTMLElement {
// Load model
const loader = new GLTFLoader();
const dracoLoader = new DRACOLoader();
- dracoLoader.setDecoderConfig({ type: 'js' });
- dracoLoader.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/');
- loader.setDRACOLoader( dracoLoader );
+ dracoLoader.setDecoderConfig({ type: "js" });
+ dracoLoader.setDecoderPath("https://www.gstatic.com/draco/v1/decoders/");
+ loader.setDRACOLoader(dracoLoader);
loader.load(model_path, (gltf) => {
scene.add(gltf.scene);
surfaceFinder.surfaceId = 0;
+ // Compute bounding box
+ let box = new THREE.Box3().setFromObject(gltf.scene);
+
+ // Scale the model to fit into a unit cube
+ const size = new THREE.Vector3();
+ box.getSize(size); // Get the size of the bounding box
+ const maxDim = Math.max(size.x, size.y, size.z); // Find the largest dimension
+ const scaleFactor = 1 / maxDim; // Calculate the scaling factor
+ gltf.scene.scale.set(scaleFactor, scaleFactor, scaleFactor); // Apply the scale uniformly
+
+ // Reposition the model so that its center is at the origin
+ let box2 = new THREE.Box3().setFromObject(gltf.scene);
+ const center = new THREE.Vector3();
+ box2.getCenter(center); // Get the center of the bounding box
+ gltf.scene.position.sub(center); // Subtract the center from the position
+
+ // Modify the materials to support surface coloring
scene.traverse((node) => {
if (node.type == "Mesh") {
const colorsTypedArray = surfaceFinder.getSurfaceIdAttribute(node);
@@ -136,14 +213,34 @@ class OutlineModelViewer extends HTMLElement {
"color",
new THREE.BufferAttribute(colorsTypedArray, 4)
);
-
- const material_params = this.getAttribute("true-color") ? {color: node.material.color} : {emissive: model_color};
+
+ let material_params = this.getAttribute("true-color")
+ ? { color: node.material.color }
+ : { emissive: model_color };
+
+ if (node.name.includes("track") || node.name.includes("zone")) {
+ //set to a copper colour
+ // #c87533
+ material_params = {
+ color: new THREE.Color(0x558855),
+ };
+ node.position.y += 0.00001;
+ }
+ if (node.name.includes("pad")) {
+ material_params = {
+ color: new THREE.Color(0xaaaaaa),
+ };
+ node.position.y += 0.00002;
+ }
// override materials
node.material = new THREE.MeshStandardMaterial(material_params);
}
});
customOutline.updateMaxSurfaceId(surfaceFinder.surfaceId + 1);
+
+ // Print out the scene structure to the console
+ // printGLTFScene(gltf.scene, 1);
});
// Set up orbital camera controls.
@@ -151,7 +248,7 @@ class OutlineModelViewer extends HTMLElement {
controls.autoRotate = spin;
controls.update();
- if(this.getAttribute("camera")) {
+ if (this.getAttribute("camera")) {
const cameraState = JSON.parse(this.getAttribute("camera"));
camera.zoom = cameraState.zoom;
camera.position.set(...cameraState.position);
@@ -159,31 +256,102 @@ class OutlineModelViewer extends HTMLElement {
controls.target.set(...cameraState.target);
}
+ // Event listener for mouse movement
+ canvas.addEventListener("mousemove", (event) =>
+ this.onMouseMove(event, canvas)
+ );
+
+ let intersects = [];
+ const doRayCast = () => {
+ // Perform raycasting for hovering
+ this.raycaster.setFromCamera(this.mouse, camera);
+
+ intersects.length = 0;
+ this.raycaster.intersectObjects(scene.children, true, intersects);
+
+ if (intersects.length > 0) {
+ const object = intersects[0].object;
+
+ // If the intersected object has changed
+ if (this.intersectedObject !== object) {
+ if (this.intersectedObject) {
+ // Reset the color of the previously hovered object
+ this.intersectedObject.material.emissive.setHex(
+ this.intersectedObject.currentHex
+ );
+ }
+ // Store the current hex color and set highlight color
+ this.intersectedObject = object;
+ this.intersectedObject.currentHex =
+ this.intersectedObject.material.emissive.getHex();
+
+ // Adjust the emissive color based on current brightness
+ const currentColor = new THREE.Color(
+ this.intersectedObject.material.emissive.getHex()
+ );
+ adjustColor(currentColor, 0.2); // Lighten or darken based on brightness
+ this.intersectedObject.material.emissive.set(currentColor);
+
+ // Print the name of the intersected object
+ params.selectedObject = object.name || "(unnamed object)";
+ }
+ } else if (this.intersectedObject) {
+ // Reset the color if the mouse is no longer hovering over any object
+ this.intersectedObject.material.emissive.setHex(
+ this.intersectedObject.currentHex
+ );
+ this.intersectedObject = null;
+ params.selectedObject = "";
+ }
+ };
+ window.addEventListener("click", doRayCast);
+
// Render loop
- function update() {
- requestAnimationFrame(update);
- controls.update();
- composer.render();
- }
+ const update = () => {
+ if (this.isVisible) {
+ requestAnimationFrame(update);
+ controls.update();
+ composer.render();
+ // doRayCast();
+ }
+ };
update();
+ // Pausing/resuming the render loop when element visibility changes
+ const observer = new IntersectionObserver((entries) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting) {
+ console.log("Model Viewer Element is visible. Resuming rendering...");
+ this.isVisible = true;
+ update(); // Resume the loop
+ } else {
+ console.log(
+ "Model Viewer Element is not visible. Pausing rendering..."
+ );
+ this.isVisible = false; // Pauses rendering
+ }
+ });
+ });
+
+ // Observe this element for visibility changes
+ observer.observe(this);
+
function onWindowResize() {
canvas_rect = canvas.getBoundingClientRect();
camera.aspect = canvas_rect.width / canvas_rect.height;
camera.updateProjectionMatrix();
renderer.setSize(canvas_rect.width, canvas_rect.height, false);
- composer.setSize(2*canvas_rect.width, 2*canvas_rect.height);
- effectFXAA.setSize(2*canvas_rect.width, 2*canvas_rect.height);
- customOutline.setSize(2*canvas_rect.width, 2*canvas_rect.height);
+ composer.setSize(2 * canvas_rect.width, 2 * canvas_rect.height);
+ effectFXAA.setSize(2 * canvas_rect.width, 2 * canvas_rect.height);
+ customOutline.setSize(2 * canvas_rect.width, 2 * canvas_rect.height);
effectFXAA.uniforms["resolution"].value.set(
- .5 / canvas_rect.width,
- .5 / canvas_rect.height
+ 0.5 / canvas_rect.width,
+ 0.5 / canvas_rect.height
);
}
window.addEventListener("resize", onWindowResize, false);
-
const gui = new GUI({
title: "Settings",
container: container,
@@ -191,43 +359,49 @@ class OutlineModelViewer extends HTMLElement {
closeFolders: true,
});
gui.close();
-
+
const uniforms = customOutline.fsQuad.material.uniforms;
const params = {
+ selectedObject: "None",
+ spin: controls.autoRotate,
mode: { Mode: 0 },
depthBias: uniforms.multiplierParameters.value.x,
depthMult: uniforms.multiplierParameters.value.y,
FXAA_resolution: 0.5,
printCamera: () => console.log(serialiseCamera(camera, controls)),
};
-
- gui.add(params, 'printCamera' );
-
+
+ gui.add(params, "selectedObject").listen();
+ gui.add(params, "spin").onChange((value) => {
+ controls.autoRotate = value;
+ });
+ gui.add(params, "printCamera");
+
gui
.add(params.mode, "Mode", {
"Outlines + Shaded (default)": 0,
- "Shaded": 2,
+ Shaded: 2,
"Depth buffer": 3,
"SurfaceID buffer": 4,
- "Outlines": 5,
+ Outlines: 5,
})
.onChange(function (value) {
uniforms.debugVisualize.value = value;
});
-
- gui.add(params, "depthBias", 0.0, 5).onChange(function (value) {
- uniforms.multiplierParameters.value.x = value;
- });
- gui.add(params, "depthMult", 0.0, 20).onChange(function (value) {
- uniforms.multiplierParameters.value.y = value;
- });
-
- gui.add(params, "FXAA_resolution", 0.0, 2).onChange(value => {
- effectFXAA.uniforms["resolution"].value.set(
+
+ gui.add(params, "depthBias", 0.0, 5).onChange(function (value) {
+ uniforms.multiplierParameters.value.x = value;
+ });
+ gui.add(params, "depthMult", 0.0, 20).onChange(function (value) {
+ uniforms.multiplierParameters.value.y = value;
+ });
+
+ gui.add(params, "FXAA_resolution", 0.0, 2).onChange((value) => {
+ effectFXAA.uniforms["resolution"].value.set(
value / canvas_rect.width,
value / canvas_rect.height
- );})
-
+ );
+ });
}
render(height) {
@@ -266,6 +440,11 @@ class OutlineModelViewer extends HTMLElement {
border: var(--theme-subtle-outline) 1px solid;
}
+ .lil-gui .controller.string input {
+ background-color: var(--theme-subtle-outline);
+ color: var(--theme-text-color);
+ }
+
canvas {
position: absolute;
width: 100%;
@@ -277,4 +456,4 @@ class OutlineModelViewer extends HTMLElement {
}
}
-customElements.define("outline-model-viewer", OutlineModelViewer);
\ No newline at end of file
+customElements.define("outline-model-viewer", OutlineModelViewer);
diff --git a/assets/projects/usbc_power_supply/test_board.glb b/assets/projects/usbc_power_supply/test_board.glb
new file mode 100644
index 0000000..1a2085a
Binary files /dev/null and b/assets/projects/usbc_power_supply/test_board.glb differ
diff --git a/highlights.md b/highlights.md
index e2ebce6..9266654 100644
--- a/highlights.md
+++ b/highlights.md
@@ -38,7 +38,8 @@ Welcome to my little home on the web! Below you'll find recent blog posts, proje
Last Modified
-{% for post in site.projects limit:5 %}
+{% assign projects = site.projects | sort_natural: "last_modified_at"%}
+{% for post in projects limit:5 %}
{% include project_summary.html %}
{% endfor %}
More