New project widgets

This commit is contained in:
Tom 2025-04-07 20:09:47 +02:00
parent 4b473c4bf3
commit 4e795f82ef
37 changed files with 8027 additions and 528 deletions

View File

@ -17,7 +17,7 @@
</p> </p>
{% include sidebar.html%} {% include sidebar.html%}
<div class="user-toggle"> <div class="light-dark-toggle">
<div role="status" class="visually-hidden js-mode-status"></div> <div role="status" class="visually-hidden js-mode-status"></div>
<button class="toggle-button js-mode-toggle" aria-label="Night Mode Toggle"> <button class="toggle-button js-mode-toggle" aria-label="Night Mode Toggle">
<span class="toggle-button__icon" aria-hidden="true"></span> <span class="toggle-button__icon" aria-hidden="true"></span>

View File

@ -15,16 +15,18 @@
<main> <main>
<article class="h-entry"> <article class="h-entry">
<section class="header"> <section class="header">
<section class="title-date-container"> <section class="title-icon-container">
<h1 class = "p-name highlights">{{ page.title }}</h1> <h1 class = "p-name highlights">{{ page.title }}</h1>
<time class="dt-label dt-published" datetime="{{ page.date | date_to_xmlschema }}">{{ page.date | date_to_string }}</time> <div class = "icon-container"></div>
</section> </section>
<hr class="byline"> <hr class="byline">
</section>
<section class="byline-time">
<section class="byline"> <section class="byline">
{{page.excerpt}} {{page.excerpt}}
</section> </section>
<time class="dt-label dt-published" datetime="{{ page.date | date_to_xmlschema }}">{{ page.date | date_to_string }}</time>
</section> </section>
<summary style="display:none" class="p-summary">{{ page.excerpt }}</summary>
<div class="e-content"> <div class="e-content">
{{ content }} {{ content }}
</div> </div>

View File

@ -13,6 +13,7 @@ head: |
} }
</script> </script>
<script src="/assets/js/outline-model-viewer/index.js" type="module"></script> <script src="/assets/js/outline-model-viewer/index.js" type="module"></script>
<script src="/assets/js/projects_viewer_animation.js" defer></script>
--- ---
{{ content }} {{ content }}

View File

@ -17,9 +17,8 @@ We got this [IKEA shower shelf thing][shelf] from Ikea which has two sticky pads
<!-- {% include mastodon_post.html post_id = "111822564173512216" %} --> <!-- {% include mastodon_post.html post_id = "111822564173512216" %} -->
<outline-model-viewer model = "/assets/projects/bathroom_shelf/models/model.glb" camera='{"position":[-10.52,2.5,2.313],"rotation":[-0.8243,-1.258,-0.7995],"zoom":436.67440926643843,"target":[0,0,0]}'> <outline-model-viewer model = "/assets/projects/bathroom_shelf/models/model.glb" camera='{"type":"perspective","fov":30,"near":10,"far":10000,"position":[364.9,307.2,459.7],"rotation":[-0.5891,0.5833,0.3527],"zoom":250,"target":[0,0,0]}'>
<img class="outline-model-poster no-wc" src = "/assets/projects/bike_lights/thumbnail.svg"> <img class="outline-model-poster hs-wc" src = "{{page.assets}}/thumbnail.svg">
<p class="has-wc">Loading model...</p>
</outline-model-viewer> </outline-model-viewer>

View File

@ -13,11 +13,14 @@ social_image: /assets/projects/bike_lights/thumbnail.png
model: /assets/projects/bike_lights/model model: /assets/projects/bike_lights/model
--- ---
<outline-model-viewer model = "/assets/projects/bike_lights/models/bigger.glb" camera='{"position":[-7.434,5.128,-6.379],"rotation":[-2.464,-0.7373,-2.646],"zoom":303.06369033128976,"target":[0,0,0]}'>
<outline-model-viewer model = "/assets/projects/bike_lights/models/bigger.glb" camera='{"type":"perspective","fov":30,"near":10,"far":10000,"position":[848.5,470.2,-294.9],"rotation":[-2.131,0.9915,2.214],"zoom":303.06369033128976,"target":[0,0,0]}'>
<img class="outline-model-poster no-wc" src = "/assets/projects/bike_lights/thumbnail.svg"> <img class="outline-model-poster no-wc" src = "/assets/projects/bike_lights/thumbnail.svg">
<p class="has-wc">Loading model...</p> <p class="has-wc">Loading model...</p>
</outline-model-viewer> </outline-model-viewer>
I've been playing around with making dynamo bike lights for my bike for a while now. I've been playing around with making dynamo bike lights for my bike for a while now.
The first iteration, the imperfect but actually on my bike and works version... is a rectifier and dc-dc converter soldered to some perfboard covered in a ziplock bag with some holes pocked in it. The DC-DC converter is actually a lipo battery charger with potentiometers for setting the maximum current and voltage. I just hooked it up to a power resistor and fiddled until I got it in current limited mode pushing about 2-3W through one of these 3W white LEDs you can get for peanuts on ebay. The first iteration, the imperfect but actually on my bike and works version... is a rectifier and dc-dc converter soldered to some perfboard covered in a ziplock bag with some holes pocked in it. The DC-DC converter is actually a lipo battery charger with potentiometers for setting the maximum current and voltage. I just hooked it up to a power resistor and fiddled until I got it in current limited mode pushing about 2-3W through one of these 3W white LEDs you can get for peanuts on ebay.

View File

@ -25,7 +25,7 @@ model: /assets/projects/ceramics/pots/pots.glb
</figure> </figure>
<figure> <figure>
<outline-model-viewer model = "{{page.models}}/pots/pots.glb" materials=keep mode=1 camera='{"position":[-5.155,2.5,-9.456],"rotation":[-2.883,-0.4851,-3.019],"zoom":268,"target":[0,0,0]}' ambient-light="6" directional-light="0.8"> <outline-model-viewer model = "{{page.models}}/pots/pots.glb" camera='{"type":"perspective","fov":30,"near":10,"far":10000,"position":[364.9,307.2,459.7],"rotation":[-0.5891,0.5833,0.3527],"zoom":250,"target":[0,0,0]}' materials=keep mode=1 ambient-light="6" directional-light="0.8">
<img class="outline-model-poster no-wc" src = "{{page.models}}/pots/pots.png"> <img class="outline-model-poster no-wc" src = "{{page.models}}/pots/pots.png">
<p class="has-wc">Loading model...</p> <p class="has-wc">Loading model...</p>
</outline-model-viewer> </outline-model-viewer>

33
_projects/elegoo_mount.md Normal file
View File

@ -0,0 +1,33 @@
---
title: Elegoo Neptune Hotend Mount
layout: project
excerpt: A quick mount for a new hotend on an Elegoo Neptune 3D printer.
permalink: /projects/elegoo_mount
assets: /assets/projects/elegoo_mount
img:
alt: A CAD model of a 3D printable mount for hotend on a 3D printer.
class: invertable
src: /assets/projects/elegoo_mount/thumbnail.png
social_image: /assets/projects/elegoo_mount/thumbnail.png
---
This is just a quick mount for a [BIQU H2V2](https://biqu.equipment/products/biqu-h2-v2-0-extruder) hotend on an Elegoo Neptune 2.
<outline-model-viewer model = "{{page.assets}}/model.glb" camera='{"type":"perspective","fov":30,"near":10,"far":10000,"position":[364.9,307.2,459.7],"rotation":[-0.5891,0.5833,0.3527],"zoom":250,"target":[0,0,0]}'>
<img class="outline-model-poster" src = "{{page.assets}}/thumbnail.png">
</outline-model-viewer>
The mounting holes don't really line up a simple manner so I made this side arms that attach with some heat set inserts. When it's all tightened down it's rigid but I'm nevertheless having some issues with this the nozzle of this printer lifting up a fraction of a millimeter when it pulls in the filament and dropping back down on z retraction.
That should be solvable with some additional z-hop in the slicer and perhaps a bowden tube the lifting force to the frame of the printer.
<figure class="two-wide">
<img src="{{ page.assets }}/hotend_side.png">
<img src="{{ page.assets }}/hotend_front.png">
</figure>
<figure>
<img src="{{ page.assets }}/side_shot.jpg">
</figure>

View File

@ -14,7 +14,7 @@ models: /assets/projects/helmet_lights/models
--- ---
<outline-model-viewer model = "{{page.models}}/model.glb" camera='{"position":[6.039,6.456,-6.641],"rotation":[-2.37,0.5778,2.654],"zoom":309.7389923355519,"target":[0,0,0]}'> <outline-model-viewer model = "{{page.models}}/model.glb" camera='{"type":"perspective","fov":30,"near":100,"far":1000,"position":[364.9,307.2,459.7],"rotation":[-0.5891,0.5833,0.3527],"zoom":250,"target":[0,0,0]}'>
<img class="outline-model-poster no-wc" src = "{{page.img.src}}"> <img class="outline-model-poster no-wc" src = "{{page.img.src}}">
<p class="has-wc">Loading model...</p> <p class="has-wc">Loading model...</p>
</outline-model-viewer> </outline-model-viewer>

View File

@ -13,7 +13,7 @@ img:
model: /assets/blog/weekend_builds_1/pot.glb model: /assets/blog/weekend_builds_1/pot.glb
--- ---
<outline-model-viewer model = "{{page.model}}" camera='{"position":[7.699,4.641,6.436],"rotation":[-0.6243,0.7663,0.4633],"zoom":229.77238881409951,"target":[0,0,0]}'> <outline-model-viewer model = "{{page.model}}" camera='{"type":"perspective","fov":30,"near":10,"far":10000,"position":[364.9,307.2,459.7],"rotation":[-0.5891,0.5833,0.3527],"zoom":250,"target":[0,0,0]}'>
<img class="outline-model-poster no-wc" src = "{{page.img.src}}"> <img class="outline-model-poster no-wc" src = "{{page.img.src}}">
<p class="has-wc">Loading model...</p> <p class="has-wc">Loading model...</p>
</outline-model-viewer> </outline-model-viewer>

View File

@ -11,3 +11,16 @@ img:
social_image: /assets/projects/projector_mount/thumbnail.png social_image: /assets/projects/projector_mount/thumbnail.png
--- ---
<outline-model-viewer model = "{{page.assets}}/model.glb" camera='{"type":"perspective","fov":30,"near":10,"far":10000,"position":[364.9,307.2,459.7],"rotation":[-0.5891,0.5833,0.3527],"zoom":250,"target":[0,0,0]}'>
<img class="outline-model-poster no-wc" src = "{{page.assets}}/thumbnail.svg">
<p class="has-wc">Loading model...</p>
</outline-model-viewer>
We wanted to mount our projector in our shared flat and this shelf just bag the perfect non-destructive mount point. This has been probably one of my more successful projects in that I build it and haven't needed to fix it at all for two or three years. There's enough friction in the joints that you can adjust it and it stays put.
<figure class="multiple">
<img src="{{ page.assets }}/side.jpg">
<img src="{{ page.assets }}/front.jpg">
<img src="{{ page.assets }}/isometric.jpg">
</figure>

View File

@ -14,7 +14,7 @@ model: /assets/blog/toothbrush_shelf/model/toothbrush_shelf.glb
--- ---
<outline-model-viewer model = "{{page.model}}" camera='{"position":[7.699,4.641,6.436],"rotation":[-0.6243,0.7663,0.4633],"zoom":229,"target":[0.0,0,0]}'> <outline-model-viewer model = "{{page.model}}" camera='{"type":"perspective","fov":30,"near":10,"far":10000,"position":[364.9,307.2,459.7],"rotation":[-0.5891,0.5833,0.3527],"zoom":250,"target":[0,0,0]}'>
<img class="outline-model-poster no-wc" src = "{{page.img.src}}"> <img class="outline-model-poster no-wc" src = "{{page.img.src}}">
<p class="has-wc">Loading model...</p> <p class="has-wc">Loading model...</p>
</outline-model-viewer> </outline-model-viewer>

View File

@ -84,11 +84,10 @@ There's an INA219 and a shunt resistor for current and voltage monitoring and a
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. 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.
<outline-model-viewer model = "{{page.assets}}/test_board.glb" materials=flat spin=true camera='{"position":[4.016,7.557,6.841],"rotation":[-0.8351,0.3753,0.3848],"zoom":241.86567243589988,"target":[0,0,0]}' ambient-light="5" directional-light="5"> <outline-model-viewer model = "{{page.assets}}/test_board.glb" materials=flat spin=true camera='{"type":"perspective","fov":30,"near":100,"far":1000,"position":[200.3,404.2,-394.6],"rotation":[-2.344,0.3407,2.812],"zoom":241,"target":[0,0,0]}' ambient-light="5" directional-light="5">
<img class="outline-model-poster no-wc" src = "{{page.img.src}}"> <img class="outline-model-poster no-wc" src = "{{page.img.src}}">
<p class="has-wc">Loading model...</p> <p class="has-wc">Loading model...</p>
</outline-model-viewer> </outline-model-viewer>
And here's the board as it arrived in the post. And here's the board as it arrived in the post.
<figure class="two-wide"> <figure class="two-wide">

View File

@ -12,7 +12,7 @@ img:
social_image: /assets/projects/vector_magnet/thumbnail.png social_image: /assets/projects/vector_magnet/thumbnail.png
--- ---
<outline-model-viewer model = "/assets/blog/vector_magnet/vector_magnet.glb" zoom=500 camera='{"position":[3.118,3.203,10.1],"rotation":[-0.3104,0.2858,0.0902],"zoom":428.68750000000136,"target":[0,0,0]}'> <outline-model-viewer model = "/assets/blog/vector_magnet/vector_magnet.glb" camera='{"type":"perspective","fov":30,"near":100,"far":10000,"position":[13.73,540.1,-1020],"rotation":[-2.655,0.0119,3.135],"zoom":428,"target":[0,0,0]}'>
<img class="outline-model-poster no-wc" src = "/assets/projects/bike_lights/thumbnail.svg"> <img class="outline-model-poster no-wc" src = "/assets/projects/bike_lights/thumbnail.svg">
<p class="has-wc">Loading model...</p> <p class="has-wc">Loading model...</p>
</outline-model-viewer> </outline-model-viewer>
@ -23,7 +23,7 @@ Check out a little interactive model of the magnetometer below. The device has t
Here's a cutaway view of the interior. Here's a cutaway view of the interior.
<outline-model-viewer model = "/assets/blog/vector_magnet/vector_magnet_section.glb" spin=false camera='{"position":[-3.069,3.172,10.17],"rotation":[-0.3052,-0.2811,-0.08718],"zoom":2860.0091628398345,"target":[0.007077,-0.02863,0.01116]}' materials=flat> <outline-model-viewer model = "/assets/blog/vector_magnet/vector_magnet_section.glb" spin=false camera='{"type":"perspective","fov":30,"near":100,"far":1000,"position":[-253.2,261.7,839],"rotation":[-0.3023,-0.2805,-0.08613],"zoom":2860.0091628398345,"target":[0,0,0]}' materials=flat ambient-light="5" directional-light="5">
<img class="outline-model-poster no-wc" src = "/assets/projects/bike_lights/thumbnail.svg"> <img class="outline-model-poster no-wc" src = "/assets/projects/bike_lights/thumbnail.svg">
<p class="has-wc">Loading model...</p> <p class="has-wc">Loading model...</p>
</outline-model-viewer> </outline-model-viewer>

View File

@ -44,6 +44,9 @@
--body-width: min(100vw, 900px); --body-width: min(100vw, 900px);
--body-margin: calc((100vw - var(--body-width)) / 2); --body-margin: calc((100vw - var(--body-width)) / 2);
// max 30px, min 30px, min happens at 375px
--title-font-size: clamp(20px, 20px * 100vw / 375px, 30px);
--color-mode: "light"; --color-mode: "light";
--color-dark: #141414; --color-dark: #141414;
--color-dark-alpha: rgba(0, 0, 0, 0.1); --color-dark-alpha: rgba(0, 0, 0, 0.1);
@ -143,6 +146,17 @@ section.title-date-container {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: baseline; align-items: baseline;
}
section.title-icon-container {
display: flex;
justify-content: space-between;
align-items: baseline;
}
section.byline-time {
display: flex;
time { time {
text-align: right; text-align: right;
@include time-text; @include time-text;
@ -153,6 +167,7 @@ hr.byline {
margin-top: 0.2em; margin-top: 0.2em;
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
section.byline { section.byline {
font-style: italic; font-style: italic;
margin-bottom: 2em; margin-bottom: 2em;
@ -386,9 +401,8 @@ body:not(.has-wc) .has-wc {
position: absolute; position: absolute;
} }
// Add transitions for things that will be affected by night mode // Add transitions for the background night mode
body, * {
a {
transition: background var(--night-mode-fade-time) ease-in-out, transition: background var(--night-mode-fade-time) ease-in-out,
color var(--night-mode-fade-time) ease-in-out; color var(--night-mode-fade-time) ease-in-out;
} }

View File

@ -139,4 +139,11 @@ header {
nav a { nav a {
margin-left: 1em; margin-left: 1em;
} }
.light-dark-toggle {
position: absolute;
top: 1em;
right: 1em;
padding-top: 0!important;
}
} }

View File

@ -1,8 +1,8 @@
.no-js .user-toggle { .no-js .light-dark-toggle {
display: none; display: none;
} }
.user-toggle { .light-dark-toggle {
display: inline; display: inline;
padding-top: 0.5rem; padding-top: 0.5rem;
} }

View File

@ -29,3 +29,37 @@ article.project {
width: 100%; width: 100%;
} }
} }
section.header {
h1 {
font-size: clamp(20px, 20px * 100vw / 300px, 25px);
}
.icon-container {
height: clamp(30px, 80px * 100vw / 375px, 50px);
aspect-ratio: 1 / 1;
overflow: clip;
}
}
section.header.sticky {
width: 100%;
gap: 1em;
position: sticky;
top: 0px;
background: var(--theme-bg-color);
z-index: 11;
margin-top: 0.5rem;
}
.icon-container {
canvas {
width: 100%;
height: 100%;
position: relative;
left: 100%;
transition: left 0.3s ease-in-out,
}
canvas.revealed {
left: 0%;
}
}

View File

@ -6,13 +6,14 @@ import { getSurfaceIdMaterial } from "./FindSurfaces.js";
// Follows the structure of // Follows the structure of
// https://github.com/mrdoob/three.js/blob/master/examples/jsm/postprocessing/OutlinePass.js // https://github.com/mrdoob/three.js/blob/master/examples/jsm/postprocessing/OutlinePass.js
class CustomOutlinePass extends Pass { class CustomOutlinePass extends Pass {
constructor(resolution, scene, camera, outlineColor) { constructor(resolution, scene, camera, outlineColor, edgeThickness) {
super(); super();
this.renderScene = scene; this.renderScene = scene;
this.renderCamera = camera; this.renderCamera = camera;
this.resolution = new THREE.Vector2(resolution.x, resolution.y); this.resolution = new THREE.Vector2(resolution.x, resolution.y);
this.outlineColor = outlineColor; this.outlineColor = outlineColor;
this.edgeThickness = edgeThickness; // If rendering at N times final size, set to N
this.fsQuad = new FullScreenQuad(null); this.fsQuad = new FullScreenQuad(null);
this.fsQuad.material = this.createOutlinePostProcessMaterial(); this.fsQuad.material = this.createOutlinePostProcessMaterial();
@ -39,6 +40,12 @@ class CustomOutlinePass extends Pass {
this.fsQuad.dispose(); this.fsQuad.dispose();
} }
updateEdgeThickness(edgeThickness) {
this.edgeThickness = edgeThickness;
console.log("Updating edge thickness to", this.edgeThickness);
this.fsQuad.material.uniforms.edgeThickness.value = edgeThickness;
}
updateMaxSurfaceId(maxSurfaceId) { updateMaxSurfaceId(maxSurfaceId) {
this.surfaceIdOverrideMaterial.uniforms.maxSurfaceId.value = maxSurfaceId; this.surfaceIdOverrideMaterial.uniforms.maxSurfaceId.value = maxSurfaceId;
} }
@ -115,6 +122,13 @@ class CustomOutlinePass extends Pass {
uniform vec4 screenSize; uniform vec4 screenSize;
uniform vec3 outlineColor; uniform vec3 outlineColor;
uniform vec3 multiplierParameters; uniform vec3 multiplierParameters;
// How many pixels away to sample for edges
// Larger value give thicker lines
// If rendering at N times the final display size
// Set this to N for lines whose thickness doesn't depend on N
uniform int edgeThickness;
uniform int debugVisualize; uniform int debugVisualize;
varying vec2 vUv; varying vec2 vUv;
@ -153,15 +167,16 @@ class CustomOutlinePass extends Pass {
float getSurfaceIdDiff(vec3 surfaceValue) { float getSurfaceIdDiff(vec3 surfaceValue) {
float surfaceIdDiff = 0.0; float surfaceIdDiff = 0.0;
surfaceIdDiff += any(notEqual(surfaceValue, getSurfaceValue(1, 0))) ? 1.0 : 0.0; int e = edgeThickness;
surfaceIdDiff += any(notEqual(surfaceValue, getSurfaceValue(0, 1))) ? 1.0 : 0.0; surfaceIdDiff += any(notEqual(surfaceValue, getSurfaceValue(e, 0))) ? 1.0 : 0.0;
surfaceIdDiff += any(notEqual(surfaceValue, getSurfaceValue(-1, 0))) ? 1.0 : 0.0; surfaceIdDiff += any(notEqual(surfaceValue, getSurfaceValue(0, e))) ? 1.0 : 0.0;
surfaceIdDiff += any(notEqual(surfaceValue, getSurfaceValue(0, -1))) ? 1.0 : 0.0; surfaceIdDiff += any(notEqual(surfaceValue, getSurfaceValue(-e, 0))) ? 1.0 : 0.0;
surfaceIdDiff += any(notEqual(surfaceValue, getSurfaceValue(0, -e))) ? 1.0 : 0.0;
surfaceIdDiff += any(notEqual(surfaceValue, getSurfaceValue(1, 1))) ? 1.0 : 0.0; surfaceIdDiff += any(notEqual(surfaceValue, getSurfaceValue(e, e))) ? 1.0 : 0.0;
surfaceIdDiff += any(notEqual(surfaceValue, getSurfaceValue(1, -1))) ? 1.0 : 0.0; surfaceIdDiff += any(notEqual(surfaceValue, getSurfaceValue(e, -e))) ? 1.0 : 0.0;
surfaceIdDiff += any(notEqual(surfaceValue, getSurfaceValue(-1, 1))) ? 1.0 : 0.0; surfaceIdDiff += any(notEqual(surfaceValue, getSurfaceValue(-e, e))) ? 1.0 : 0.0;
surfaceIdDiff += any(notEqual(surfaceValue, getSurfaceValue(-1, -1))) ? 1.0 : 0.0; surfaceIdDiff += any(notEqual(surfaceValue, getSurfaceValue(-e, -e))) ? 1.0 : 0.0;
return surfaceIdDiff; return surfaceIdDiff;
} }
@ -267,6 +282,7 @@ class CustomOutlinePass extends Pass {
depthBuffer: {}, depthBuffer: {},
surfaceBuffer: {}, surfaceBuffer: {},
outlineColor: { value: new THREE.Color(this.outlineColor) }, outlineColor: { value: new THREE.Color(this.outlineColor) },
edgeThickness: { value: this.edgeThickness },
multiplierParameters: { multiplierParameters: {
value: new THREE.Vector3(0.9, 20, 0.5), value: new THREE.Vector3(0.9, 20, 0.5),
}, },

View File

@ -0,0 +1,98 @@
import * as THREE from "three";
export function load_gltf(
element,
scene,
surfaceFinder,
model_color,
customOutline,
gltf
) {
scene.add(gltf.scene);
// 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") {
// Add surface ID attribute to the geometry
const colorsTypedArray = surfaceFinder.getSurfaceIdAttribute(node);
node.surfaceId = colorsTypedArray;
node.geometry.setAttribute(
"color",
new THREE.BufferAttribute(colorsTypedArray, 4)
);
// Hack specific to kicad models to make the tracks and zones look good
if (node.name.includes("track") || node.name.includes("zone")) {
//set to a copper colour
// #c87533
node.material = new THREE.MeshStandardMaterial({
color: new THREE.Color(0x558855),
});
node.position.y += 0.00001;
}
// Hack specific to kicad models to make the tracks and zones look good
if (node.name.includes("pad")) {
node.material = new THREE.MeshStandardMaterial({
color: new THREE.Color(0xaaaaaa),
});
node.position.y += 0.00002;
}
if (node.name.includes("PCB")) {
node.material = new THREE.MeshStandardMaterial({
color: new THREE.Color(0x446644),
});
}
// override materials for different purposes
// materials = outlines
// sets the material to be emissive to the background colour of the page
// This makes for nice two colour rendering with no shading
// material = flat overides all the materials to just be flat with the base colour
// material = keep uses whatever material is defined in the gltf
console.log(
`element.getAttribute("materials") ${element.getAttribute("materials")}`
);
const material_mode = element.getAttribute("materials") || "outlines";
if (material_mode === "outlines") {
node.material = new THREE.MeshStandardMaterial({
emissive: model_color,
});
} else if (material_mode === "flat") {
node.material = new THREE.MeshStandardMaterial({
color: node.material.color,
});
} else if (material_mode === "keep") {
// Do nothing, leave the material as set in the GLTF file
} else {
throw new Error(
"Invalid material mode, should be outlines, flat or keep."
);
}
}
});
customOutline.updateMaxSurfaceId(surfaceFinder.surfaceId + 1);
element.component.controls.update();
element.component.composer.render();
}

View File

@ -0,0 +1,287 @@
import * as THREE from "three";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { DRACOLoader } from "three/addons/loaders/DRACOLoader.js";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
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 { Timer } from "three/addons/Addons.js";
import { setupThreeJS, serialiseCamera } from "./helpers.js";
import { CustomOutlinePass } from "./CustomOutlinePass.js";
import FindSurfaces from "./FindSurfaces.js";
import { load_gltf } from "./LoadGLTF.js";
export class OutlineModelViewer extends HTMLElement {
constructor() {
super();
this.isVisible = true;
this.shadow = this.attachShadow({ mode: "open" });
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;
}
hideUI() {
console.log("Hiding UI");
this.component.hideUI();
}
updatePixelRatio(r) {
this.pixelRatio = r;
this.component.renderer.setPixelRatio(r);
this.customOutline.updateEdgeThickness(
this.pixelRatio * this.edgeThickness
);
}
updateEdgeThickness(t) {
this.edgeThickness = t;
this.customOutline.updateEdgeThickness(
this.pixelRatio * this.edgeThickness
);
}
connectedCallback() {
let element = this;
let component = setupThreeJS(this);
this.component = component;
const { canvas, camera, scene, renderer, gui } = component;
const model_path = this.getAttribute("model");
const spin = (this.getAttribute("spin") || "true") === "true";
// 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");
const model_color = style.getPropertyValue("--theme-model-bg-color");
const directionalLight = new THREE.DirectionalLight(
0xffffff,
this.getAttribute("directional-light") || 2
);
scene.add(directionalLight);
directionalLight.position.set(1.7, 1, -1);
const ambientLight = new THREE.AmbientLight(
0xffffff,
this.getAttribute("ambient-light") || 0.5
);
scene.add(ambientLight);
// Set up post processing
// Create a render target that holds a depthTexture so we can use it in the outline pass
// See: https://threejs.org/docs/index.html#api/en/renderers/WebGLRenderTarget.depthBuffer
const depthTexture = new THREE.DepthTexture();
const renderTarget = new THREE.WebGLRenderTarget(
canvas.width,
canvas.height,
{
depthTexture: depthTexture,
depthBuffer: true,
}
);
// Initial render pass.
const composer = new EffectComposer(renderer, renderTarget);
component.composer = composer;
const pass = new RenderPass(scene, camera);
composer.addPass(pass);
// Outline pass.
const customOutline = new CustomOutlinePass(
new THREE.Vector2(canvas.width, canvas.height),
scene,
camera,
outline_color,
this.edgeThickness * this.pixelRatio
);
composer.addPass(customOutline);
this.customOutline = customOutline;
// Antialias pass.
const effectFXAA = new ShaderPass(FXAAShader);
effectFXAA.uniforms["resolution"].value.set(
1.0 / canvas.width,
1.0 / canvas.height
);
// composer.addPass(effectFXAA);
// Set over sampling ratio
this.updateEdgeThickness(1);
this.updatePixelRatio(window.devicePixelRatio);
renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);
component.render = composer.render;
// 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);
const surfaceFinder = new FindSurfaces();
loader.load(model_path, (gltf) =>
load_gltf(this, scene, surfaceFinder, model_color, customOutline, gltf)
);
// Set up orbital camera controls.
let controls = new OrbitControls(camera, renderer.domElement);
component.controls = controls;
controls.autoRotate = spin;
controls.update();
// Event listener for mouse movement
canvas.addEventListener("mousemove", (event) =>
this.onMouseMove(event, canvas)
);
let intersects = [];
const doRayCast = () => {
// Perform raycasting for a click
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
);
}
this.shadow.querySelector(
"#clicked-item"
).innerText = `${object.name}`;
}
} else if (this.intersectedObject) {
this.intersectedObject = null;
}
if (intersects.length === 0) {
this.shadow.querySelector("#clicked-item").innerText = "";
}
};
window.addEventListener("click", doRayCast);
// Render loop
this.render_loop = true;
const timer = new Timer();
const update = () => {
if (this.isVisible && this.render_loop) {
timer.update();
const delta = timer.getDelta();
requestAnimationFrame(update);
controls.update(delta);
composer.render();
}
};
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() {
// Update the internal dimensions of the canvas
canvas.width = canvas.clientWidth * element.pixelRatio;
canvas.height = canvas.clientHeight * element.pixelRatio;
// Recompute the camera matrix
camera.aspect = canvas.width / canvas.height;
camera.updateProjectionMatrix();
// Resive the various render targets
renderer.setSize(canvas.width, canvas.height, false);
composer.setSize(canvas.width, canvas.height);
effectFXAA.setSize(canvas.width, canvas.height);
customOutline.setSize(canvas.width, canvas.height);
//
effectFXAA.uniforms["resolution"].value.set(
1.0 / canvas.width,
1.0 / canvas.height
);
}
this.onWindowResize = onWindowResize;
onWindowResize();
const uniforms = customOutline.fsQuad.material.uniforms;
uniforms.debugVisualize.value = parseInt(this.getAttribute("mode")) || 0;
const params = {
spin: controls.autoRotate,
ambientLight: parseFloat(ambientLight.intensity),
directionalLight: parseFloat(directionalLight.intensity),
mode: { Mode: uniforms.debugVisualize.value },
depthBias: uniforms.multiplierParameters.value.x,
depthMult: uniforms.multiplierParameters.value.y,
lerp: uniforms.multiplierParameters.value.z,
edgeThickness: this.edgeThickness,
pixelRatio: this.pixelRatio,
};
gui.add(params, "spin").onChange((value) => {
controls.autoRotate = value;
});
gui
.add(params.mode, "Mode", {
"Outlines + Shaded (default)": 0,
"Just Outlines": 5,
"Only outer outlines + shading": 1,
"Only shading": 2,
"(Debug) SurfaceID buffer": 4,
"(Debug) Depth buffer": 3,
"(Debug) Depth Difference (external edges / outline)": 6,
"(Debug) SurfaceID Difference (internal edges)": 7,
})
.onChange(function (value) {
uniforms.debugVisualize.value = value;
});
gui.add(params, "edgeThickness", 1, 10).onChange(function (value) {
element.updateEdgeThickness(value);
});
gui.add(params, "pixelRatio", 1, 8, 1).onChange(function (value) {
element.updatePixelRatio(value);
element.onWindowResize();
});
window.addEventListener("resize", onWindowResize, false);
}
}
export default OutlineModelViewer;

View File

@ -0,0 +1,177 @@
import * as THREE from "three";
// import * as dat from 'dat.gui';
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { DRACOLoader } from "three/addons/loaders/DRACOLoader.js";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
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 { Timer } from "three/addons/Addons.js";
import { setupThreeJS, serialiseCamera } from "./helpers.js";
import { CustomOutlinePass } from "./CustomOutlinePass.js";
import FindSurfaces from "./FindSurfaces.js";
import { load_gltf } from "./LoadGLTF.js";
// let ticking = false;
// let attached = false;
// function render() {
// const there =
// viewer.getBoundingClientRect().bottom <
// sticky.getBoundingClientRect().bottom;
// const delta = (window.scrollY - last_scroll_pos) / 30;
// if (there && !attached) {
// console.log("attaching");
// sticky.appendChild(viewer);
// viewer.hide_ui();
// viewer.style.height = "100px";
// viewer.style.width = "100px";
// viewer.style["min-height"] = "unset";
// viewer.component.canvas.style.height = "100%";
// viewer.style.border = "unset";
// viewer.onWindowResize();
// viewer.component.render_loop = false;
// attached = true;
// }
// if ((window.scrollY < transition_scroll_pos) && attached) {
// console.log("detaching");
// viewer.replaceWith(date);
// byline.style.display = "unset";
// attached = false;
// }
export class ScrollLockedViewer extends HTMLElement {
constructor() {
super();
this.shadow = this.attachShadow({ mode: "open" });
}
connectedCallback() {
let component = setupThreeJS(this);
this.component = component;
const { canvas, camera, scene, renderer } = component;
component.hide_ui();
this.style.display = "block";
// this.style.height = "100px";
this.style["aspect-ratio"] = "1 / 1";
this.style["min-height"] = "unset";
component.canvas.style.height = "100%";
component.container.style.height = "100%";
// this.style.border = "unset";
const render_size_multiplier = 4;
renderer.setPixelRatio(render_size_multiplier);
renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);
const model_path = this.getAttribute("model");
const spin = (this.getAttribute("spin") || "true") === "true";
// 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");
const model_color = style.getPropertyValue("--theme-model-bg-color");
const directionalLight = new THREE.DirectionalLight(
0xffffff,
this.getAttribute("directional-light") || 2
);
scene.add(directionalLight);
directionalLight.position.set(1.7, 1, -1);
const ambientLight = new THREE.AmbientLight(
0xffffff,
this.getAttribute("ambient-light") || 0.5
);
scene.add(ambientLight);
// Set up post processing
// Create a render target that holds a depthTexture so we can use it in the outline pass
// See: https://threejs.org/docs/index.html#api/en/renderers/WebGLRenderTarget.depthBuffer
console.log(`clientHeight ${canvas.clientHeight} height ${canvas.height}`);
console.log(`clientWidth ${canvas.clientWidth} width ${canvas.width}`);
const depthTexture = new THREE.DepthTexture();
const renderTarget = new THREE.WebGLRenderTarget(
canvas.width,
canvas.height,
{
depthTexture: depthTexture,
depthBuffer: true,
}
);
// Initial render pass.
const composer = new EffectComposer(renderer, renderTarget);
component.composer = composer;
const pass = new RenderPass(scene, camera);
composer.addPass(pass);
// Outline pass.
const customOutline = new CustomOutlinePass(
new THREE.Vector2(canvas.width, canvas.height),
scene,
camera,
outline_color,
render_size_multiplier
);
composer.addPass(customOutline);
// Antialias pass.
// const effectFXAA = new ShaderPass(FXAAShader);
// effectFXAA.uniforms["resolution"].value.set(
// 1.0 / canvas.width,
// 1.0 / canvas.height
// );
// composer.addPass(effectFXAA);
// 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);
const surfaceFinder = new FindSurfaces();
loader.load(model_path, (gltf) =>
load_gltf(this, scene, surfaceFinder, model_color, customOutline, gltf)
);
// Set up orbital camera controls.
let controls = new OrbitControls(camera, renderer.domElement);
component.controls = controls;
controls.autoRotate = spin;
controls.update();
let last_scroll_pos = 0;
const onscroll = () => {
const delta = (window.scrollY - last_scroll_pos) / 30;
if (Math.abs(delta) > 0.1) {
controls.update(delta);
composer.render();
last_scroll_pos = window.scrollY;
}
};
let ticking = false;
document.addEventListener("scroll", (event) => {
if (!ticking) {
window.requestAnimationFrame(() => {
onscroll();
ticking = false;
});
ticking = true;
}
});
this.render = composer.render;
}
}
export default ScrollLockedViewer;

View File

@ -26,8 +26,8 @@ export function deserialiseCamera(component) {
const camera = new THREE.PerspectiveCamera(30, aspect, 0.01, 40); const camera = new THREE.PerspectiveCamera(30, aspect, 0.01, 40);
if (!initial_camera_state) return; if (!initial_camera_state) return camera;
if (initial_camera_state.type !== "perspective") return; if (initial_camera_state.type !== "perspective") return camera;
if (initial_camera_state.fov) camera.fov = initial_camera_state.fov; if (initial_camera_state.fov) camera.fov = initial_camera_state.fov;
if (initial_camera_state.near) camera.near = initial_camera_state.near; if (initial_camera_state.near) camera.near = initial_camera_state.near;
if (initial_camera_state.far) camera.far = initial_camera_state.far; if (initial_camera_state.far) camera.far = initial_camera_state.far;
@ -227,6 +227,7 @@ function setupThreeJS(component) {
component.renderer = new THREE.WebGLRenderer({ component.renderer = new THREE.WebGLRenderer({
canvas: component.canvas, canvas: component.canvas,
alpha: true, alpha: true,
antialias: true,
}); });
component.renderer.setPixelRatio(window.devicePixelRatio); component.renderer.setPixelRatio(window.devicePixelRatio);
@ -289,6 +290,26 @@ function setupThreeJS(component) {
const fullScreenButton = component.shadow.querySelector("#fullscreen-btn"); const fullScreenButton = component.shadow.querySelector("#fullscreen-btn");
fullScreenButton.addEventListener("click", component.toggleFullScreen); fullScreenButton.addEventListener("click", component.toggleFullScreen);
component.hideUI = () => {
component.gui.hide();
component.shadow.querySelector("#fullscreen-btn").style.display = "none";
component.shadow.querySelector("#clicked-item").style.display = "none";
component.canvas.style.position = "static";
};
// // Handle fullscreen change events triggerd through various means
// function onFullScreenChange() {
// if (document.fullscreenElement) {
// canvas.style.height = "100%";
// lil_gui.style.marginTop = "0";
// } else {
// canvas.style.height = canvas_height;
// lil_gui.style.marginTop = lil_gui_margin_top;
// }
// onWindowResize();
// }
// document.addEventListener("fullscreenchange", onFullScreenChange);
return component; return component;
} }

View File

@ -1,491 +1,9 @@
import * as THREE from "three";
// import * as dat from 'dat.gui';
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { DRACOLoader } from "three/addons/loaders/DRACOLoader.js";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { EffectComposer } from "three/addons/postprocessing/EffectComposer.js";
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 { Timer } from "three/addons/Addons.js";
import GUI from "lil-gui";
import { componentHTML, setupThreeJS, serialiseCamera } from "./helpers.js";
import { CustomOutlinePass } from "./CustomOutlinePass.js";
import FindSurfaces from "./FindSurfaces.js";
import { PointCloudViewer } from "./PointCloudViewer.js"; import { PointCloudViewer } from "./PointCloudViewer.js";
import { VolumeViewer } from "./VolumeViewer.js"; import { VolumeViewer } from "./VolumeViewer.js";
import { ScrollLockedViewer } from "./ScrollLockedViewer.js";
import { OutlineModelViewer } from "./OutlineViewer.js";
customElements.define("point-cloud-viewer", PointCloudViewer); customElements.define("point-cloud-viewer", PointCloudViewer);
customElements.define("volume-viewer", VolumeViewer); customElements.define("volume-viewer", VolumeViewer);
customElements.define("scroll-locked-viewer", ScrollLockedViewer);
// 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.
// 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)
);
}
});
}
export class OutlineModelViewer extends HTMLElement {
constructor() {
super();
this.isVisible = true;
this.shadow = this.attachShadow({ mode: "open" });
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() {
const mul = 2;
let component_rect = this.getBoundingClientRect();
this.shadow.innerHTML = componentHTML(component_rect);
const model_path = this.getAttribute("model");
const spin = (this.getAttribute("spin") || "true") === "true";
const container = this.shadow.querySelector("div#container");
const canvas = this.shadow.querySelector("canvas");
let canvas_rect = canvas.getBoundingClientRect();
// 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");
const model_color = style.getPropertyValue("--theme-model-bg-color");
// // 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");
camera.position.set(10, 2.5, 4);
// create the scene and the camera
const scene = new THREE.Scene();
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
alpha: true,
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(canvas_rect.width, canvas_rect.height, false);
const directionalLight = new THREE.DirectionalLight(
0xffffff,
this.getAttribute("directional-light") || 2
);
scene.add(directionalLight);
directionalLight.position.set(1.7, 1, -1);
const ambientLight = new THREE.AmbientLight(
0xffffff,
this.getAttribute("ambient-light") || 0.5
);
scene.add(ambientLight);
// Set up post processing
// Create a render target that holds a depthTexture so we can use it in the outline pass
// See: https://threejs.org/docs/index.html#api/en/renderers/WebGLRenderTarget.depthBuffer
const depthTexture = new THREE.DepthTexture();
const renderTarget = new THREE.WebGLRenderTarget(
mul * canvas_rect.width,
mul * canvas_rect.height,
{
depthTexture: depthTexture,
depthBuffer: true,
}
);
// Initial render pass.
const composer = new EffectComposer(renderer, renderTarget);
const pass = new RenderPass(scene, camera);
composer.addPass(pass);
// Outline pass.
const customOutline = new CustomOutlinePass(
new THREE.Vector2(mul * canvas_rect.width, mul * canvas_rect.height),
scene,
camera,
outline_color
);
composer.addPass(customOutline);
// Antialias pass.
const effectFXAA = new ShaderPass(FXAAShader);
effectFXAA.uniforms["resolution"].value.set(
1.0 / canvas_rect.width / mul,
1.0 / canvas_rect.height / mul
);
composer.addPass(effectFXAA);
const surfaceFinder = new FindSurfaces();
// 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);
loader.load(model_path, (gltf) => {
scene.add(gltf.scene);
// 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") {
// Add surface ID attribute to the geometry
const colorsTypedArray = surfaceFinder.getSurfaceIdAttribute(node);
node.surfaceId = colorsTypedArray;
node.geometry.setAttribute(
"color",
new THREE.BufferAttribute(colorsTypedArray, 4)
);
// Hack specific to kicad models to make the tracks and zones look good
if (node.name.includes("track") || node.name.includes("zone")) {
//set to a copper colour
// #c87533
node.material = new THREE.MeshStandardMaterial({
color: new THREE.Color(0x558855),
});
node.position.y += 0.00001;
}
// Hack specific to kicad models to make the tracks and zones look good
if (node.name.includes("pad")) {
node.material = new THREE.MeshStandardMaterial({
color: new THREE.Color(0xaaaaaa),
});
node.position.y += 0.00002;
}
if (node.name.includes("PCB")) {
node.material = new THREE.MeshStandardMaterial({
color: new THREE.Color(0x446644),
});
}
// override materials for different purposes
// materials = outlines
// sets the material to be emissive to the background colour of the page
// This makes for nice two colour rendering with no shading
// material = flat overides all the materials to just be flat with the base colour
// material = keep uses whatever material is defined in the gltf
const material_mode = this.getAttribute("materials") || "outlines";
if (material_mode === "outlines") {
node.material = new THREE.MeshStandardMaterial({
emissive: model_color,
});
} else if (material_mode === "flat") {
node.material = new THREE.MeshStandardMaterial({
color: node.material.color,
});
} else if (material_mode === "keep") {
// Do nothing, leave the material as set in the GLTF file
} else {
throw new Error(
"Invalid material mode, should be outlines, flat or keep."
);
}
}
});
customOutline.updateMaxSurfaceId(surfaceFinder.surfaceId + 1);
// Print out the scene structure to the console
// printGLTFScene(gltf.scene, 1);
});
// Set up orbital camera controls.
let controls = new OrbitControls(camera, renderer.domElement);
controls.autoRotate = spin;
controls.update();
if (this.getAttribute("camera")) {
const cameraState = JSON.parse(this.getAttribute("camera"));
camera.zoom = cameraState.zoom;
camera.position.set(...cameraState.position);
camera.rotation.set(...cameraState.rotation);
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 a click
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
);
}
this.shadow.querySelector(
"#clicked-item"
).innerText = `${object.name}`;
}
} else if (this.intersectedObject) {
this.intersectedObject = null;
}
if (intersects.length === 0) {
this.shadow.querySelector("#clicked-item").innerText = "";
}
};
window.addEventListener("click", doRayCast);
// Render loop
const timer = new Timer();
const update = () => {
if (this.isVisible) {
timer.update();
const delta = timer.getDelta();
// this.shadow.querySelector("#clicked-item").innerText = `${1 / delta}`;
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(
mul * canvas_rect.width,
mul * canvas_rect.height,
false
);
composer.setSize(mul * canvas_rect.width, mul * canvas_rect.height);
effectFXAA.setSize(mul * canvas_rect.width, mul * canvas_rect.height);
customOutline.setSize(mul * canvas_rect.width, mul * canvas_rect.height);
effectFXAA.uniforms["resolution"].value.set(
1.0 / canvas_rect.width / mul,
1.0 / canvas_rect.height / mul
);
}
onWindowResize();
const gui = new GUI({
title: "Settings",
container: container,
injectStyles: false,
closeFolders: true,
});
if ((this.getAttribute("debug") || "closed") !== "open") gui.close();
const uniforms = customOutline.fsQuad.material.uniforms;
uniforms.debugVisualize.value = parseInt(this.getAttribute("mode")) || 0;
const params = {
spin: controls.autoRotate,
ambientLight: parseFloat(ambientLight.intensity),
directionalLight: parseFloat(directionalLight.intensity),
mode: { Mode: uniforms.debugVisualize.value },
depthBias: uniforms.multiplierParameters.value.x,
depthMult: uniforms.multiplierParameters.value.y,
lerp: uniforms.multiplierParameters.value.z,
printCamera: () => console.log(serialiseCamera(camera, controls)),
};
gui.add(params, "spin").onChange((value) => {
controls.autoRotate = value;
});
gui.add(params, "printCamera");
gui
.add(params.mode, "Mode", {
"Outlines + Shaded (default)": 0,
"Just Outlines": 5,
"Only outer outlines + shading": 1,
"Only shading": 2,
"(Debug) SurfaceID buffer": 4,
"(Debug) Depth buffer": 3,
"(Debug) Depth Difference (external edges / outline)": 6,
"(Debug) SurfaceID Difference (internal edges)": 7,
})
.onChange(function (value) {
uniforms.debugVisualize.value = value;
});
gui.add(params, "ambientLight", 0.0, 10.0).onChange(function (value) {
ambientLight.intensity = value;
});
gui.add(params, "directionalLight", 0.0, 10.0).onChange(function (value) {
directionalLight.intensity = value;
});
gui.add(params, "depthBias", 0.0, 5).onChange(function (value) {
uniforms.multiplierParameters.value.x = value;
});
gui.add(params, "depthMult", 0.0, 40.0).onChange(function (value) {
uniforms.multiplierParameters.value.y = value;
});
gui.add(params, "lerp", 0.0, 1.0).onChange(function (value) {
uniforms.multiplierParameters.value.z = value;
});
// Toggle fullscreen mode
const shadow = this.shadow;
const canvas_height = canvas.style.height;
const lil_gui = shadow.querySelector(".lil-gui.root");
const lil_gui_margin_top = lil_gui.style.marginTop;
function toggleFullScreen() {
if (!document.fullscreenElement) {
if (container.requestFullscreen) {
container.requestFullscreen();
} else if (container.mozRequestFullScreen) {
// Firefox
container.mozRequestFullScreen();
} else if (container.webkitRequestFullscreen) {
// Chrome, Safari and Opera
container.webkitRequestFullscreen();
} else if (container.msRequestFullscreen) {
// IE/Edge
container.msRequestFullscreen();
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
}
}
}
// const fullScreenButton = this.shadow.querySelector("#fullscreen-btn");
// fullScreenButton.addEventListener("click", () => toggleFullScreen());
window.addEventListener("resize", onWindowResize, false);
// Handle fullscreen change events triggerd through various means
function onFullScreenChange() {
if (document.fullscreenElement) {
canvas.style.height = "100%";
lil_gui.style.marginTop = "0";
} else {
canvas.style.height = canvas_height;
lil_gui.style.marginTop = lil_gui_margin_top;
}
onWindowResize();
}
document.addEventListener("fullscreenchange", onFullScreenChange);
}
}
customElements.define("outline-model-viewer", OutlineModelViewer); customElements.define("outline-model-viewer", OutlineModelViewer);
export default OutlineModelViewer;

View File

@ -0,0 +1,76 @@
const icon_container = document.querySelector(".icon-container");
const inline_viewer = document.querySelector("outline-model-viewer");
const canvas = inline_viewer.component.canvas;
const header = document.querySelector("section.header");
if (inline_viewer) {
let last_scroll_pos = 0;
let mode = "inline";
let { controls, composer } = inline_viewer.component;
let margin = 50; // in pixels
let original = {};
header.classList.add("sticky");
const onscroll = () => {
const delta =
icon_container.getBoundingClientRect().bottom -
inline_viewer.getBoundingClientRect().bottom;
if (mode === "inline" && delta > margin) {
console.log(`Moving canvas to icon delta ${delta} ${window.scrollY}`);
icon_container.appendChild(canvas);
mode = "icon";
original.autoRotate = controls.autoRotate;
controls.autoRotate = true;
inline_viewer.onWindowResize();
inline_viewer.updateEdgeThickness(0.5);
canvas.classList.add("revealed");
}
if (mode === "icon" && delta > 2 * margin) {
canvas.classList.add("revealed");
}
if (mode === "icon" && delta < 0) {
console.log(
`Moving canvas to inline viewer delta ${delta} ${window.scrollY}`
);
inline_viewer.component.container.insertBefore(
canvas,
inline_viewer.component.gui.domElement
);
controls.autoRotate = original.autoRotate;
mode = "inline";
inline_viewer.onWindowResize();
inline_viewer.updateEdgeThickness(1);
canvas.classList.remove("revealed");
}
if (mode === "icon" && delta < 2 * margin) {
canvas.classList.remove("revealed");
}
if (mode == "icon") {
const delta = (window.scrollY - last_scroll_pos) / 30;
if (Math.abs(delta) > 0.1) {
controls.update(delta);
composer.render();
last_scroll_pos = window.scrollY;
}
}
};
let ticking = false;
document.addEventListener("scroll", (event) => {
if (!ticking) {
window.requestAnimationFrame(() => {
onscroll();
ticking = false;
});
ticking = true;
}
});
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

View File

@ -67,7 +67,7 @@ header h1 {
margin-bottom: 10vh; margin-bottom: 10vh;
} }
.user-toggle { .light-dark-toggle {
position: absolute; position: absolute;
top: 0px; top: 0px;
left: 100%; left: 100%;
@ -196,7 +196,7 @@ header h1 {
</div> </div>
<div class="user-toggle"> <div class="light-dark-toggle">
<div role="status" class="visually-hidden js-mode-status"></div> <div role="status" class="visually-hidden js-mode-status"></div>
<button class="toggle-button js-mode-toggle" aria-label="Night Mode Toggle"> <button class="toggle-button js-mode-toggle" aria-label="Night Mode Toggle">
<span class="toggle-button__icon" aria-hidden="true"></span> <span class="toggle-button__icon" aria-hidden="true"></span>

View File

@ -9,6 +9,18 @@ mathjax: false
# Project Ideas # Project Ideas
* Write some new firmware for the [DMM SAO](https://github.com/flummer/dmm-sao/blob/main/firmware/code.py) I got at the hackaday supercon.
## 3D models of vertebrates
https://www.morphosource.org/
https://sketchfab.com/FloridaMuseum/models
https://www.morphosource.org/concern/media/000727033?locale=en
https://www.morphosource.org/concern/media/000658134?locale=en
[Triangulation Theodolite By Hildebrand Freiberg, 1902](https://www.morphosource.org/concern/media/000657801?locale=en)
[Cool Green Theodolite](https://www.morphosource.org/concern/media/000655681?locale=en)
[Nice Lumbar](https://www.morphosource.org/concern/media/000532571?locale=en)
[Shark Head](https://www.morphosource.org/concern/media/000677539?locale=en)
## Ferrofluid music thing ## Ferrofluid music thing
Put that ferro fluid in a round bottom flask and put a coil nearby Put that ferro fluid in a round bottom flask and put a coil nearby