diff --git a/_projects/bike_lights.md b/_projects/bike_lights.md index d0f80da..5a5e665 100644 --- a/_projects/bike_lights.md +++ b/_projects/bike_lights.md @@ -25,7 +25,7 @@ head: | --- - +

Loading model...

diff --git a/_projects/helmet_lights.md b/_projects/helmet_lights.md index f9dc494..785a985 100644 --- a/_projects/helmet_lights.md +++ b/_projects/helmet_lights.md @@ -26,7 +26,7 @@ head: | --- - +

Loading model...

diff --git a/_projects/lasercut_stool.md b/_projects/lasercut_stool.md index 2157799..16b73e3 100644 --- a/_projects/lasercut_stool.md +++ b/_projects/lasercut_stool.md @@ -25,7 +25,7 @@ head: | --- - +

Loading model...

\ 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