New project widgets
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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 }}
|
@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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
@ -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>
|
@ -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>
|
||||||
|
@ -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>
|
@ -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>
|
||||||
|
@ -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>
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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%;
|
||||||
|
}
|
||||||
|
}
|
@ -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),
|
||||||
},
|
},
|
||||||
|
98
assets/js/outline-model-viewer/LoadGLTF.js
Normal 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();
|
||||||
|
}
|
287
assets/js/outline-model-viewer/OutlineViewer.js
Normal 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;
|
177
assets/js/outline-model-viewer/ScrollLockedViewer.js
Normal 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;
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
|
||||||
|
76
assets/js/projects_viewer_animation.js
Normal 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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
3033
assets/projects/elegoo_mount/Elegoo neptune v7.step
Normal file
BIN
assets/projects/elegoo_mount/hotend_front.png
Normal file
After Width: | Height: | Size: 154 KiB |
BIN
assets/projects/elegoo_mount/hotend_side.png
Normal file
After Width: | Height: | Size: 127 KiB |
BIN
assets/projects/elegoo_mount/model.glb
Normal file
BIN
assets/projects/elegoo_mount/side_shot.jpg
Normal file
After Width: | Height: | Size: 296 KiB |
BIN
assets/projects/elegoo_mount/thumbnail.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
4156
assets/projects/projector_mount/Projector Mount.step
Normal file
BIN
assets/projects/projector_mount/front.jpg
Normal file
After Width: | Height: | Size: 332 KiB |
BIN
assets/projects/projector_mount/isometric.jpg
Normal file
After Width: | Height: | Size: 270 KiB |
BIN
assets/projects/projector_mount/model.glb
Normal file
BIN
assets/projects/projector_mount/side.jpg
Normal file
After Width: | Height: | Size: 199 KiB |
@ -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>
|
||||||
|
@ -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
|
||||||
|