{
	"version": "https://jsonfeed.org/version/1.1",
	"title": "ATOMWOLF",
	"language": "en",
	"home_page_url": "https://www.atomwolf.org/",
	"feed_url": "https://www.atomwolf.org/feed/feed.json",
	"description": "A personal site of Adam Wolf&#39;s, containing posts and projects, ideally more digital garden-y than bloggy",
	"author": {
		"name": "Adam Wolf",
		"url": "https://www.atomwolf.org/about/#adam-wolf"
	},
	"items": [
		{
			"id": "https://www.atomwolf.org/posts/e-coli-chemotaxis/",
			"url": "https://www.atomwolf.org/posts/e-coli-chemotaxis/",
			"title": "E. coli chemotaxis: the baffling intelligence of a single cell",
			"content_html": "<div class=\"dotted-metadata-block\">\n<p><span class=\"small-caps\">Summary</span>: \"<a href=\"https://jsomers.net/e-coli-chemotaxis/\">E. coli chemotaxis: the baffling intelligence of a single cell</a>\" is an engaging, interactive systems biology introduction into how E. coli bacteria sense and navigate their world.</p>\n<p>\n\t<span class=\"small-caps\">Tags</span>:\n\t\t<a href=\"https://www.atomwolf.org/tags/links/\" class=\"entry-tag\">links</a>, \n\t\t<a href=\"https://www.atomwolf.org/tags/biology/\" class=\"entry-tag\">biology</a>\n</p>\n<p><span class=\"small-caps\">Created</span>: <time datetime=\"2025-06-25\">25 June 2025</time></p>\n</div>\n<hr><p>“<a href=\"https://jsomers.net/e-coli-chemotaxis/\">E. coli chemotaxis: the baffling intelligence of a single cell</a>” by <a href=\"https://jsomers.net/\">James Somers</a><label for=\"sn-somers\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-somers\" class=\"margin-toggle\">\n<small class=\"sidenote\">Since reading \"<a href=\"https://jsomers.net/i-should-have-loved-biology/\">I should have loved biology</a>\", I try to read everything James Somers writes. That essay introduced me to David Goodsell's amazing book <a href=\"https://link.springer.com/book/10.1007/978-0-387-84925-6\">The Machinery of Life</a>!</small>\nand <a href=\"https://ehmorris.com/\">Edwin Morris</a> is a wonderful article about how E. coli bacteria sense and navigate their world, looking for nutrients. Newcomers should find it friendly, but the interactive diagrams alone should make it interesting to anyone.</p>\n<p>The article is wide in scope, covering, for instance, receptors, and what it’s like inside the cell. It meanders into bacterial individuality and how we discover these things in biology.  The diagrams are pretty good, and some of them are interactive! It ends with an <a href=\"https://en.wikipedia.org/wiki/Annotated_bibliography\">annotated bibliography</a>.<label for=\"sn-bib\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-bib\" class=\"margin-toggle\">\n<small class=\"sidenote\">Annotated bibliographies are undervalued and should be more popular.</small>\n</p>\n<hr><p><a href=\"mailto:feedcomments@atomwolf.org?subject=Feed%20Reply%20for%20E.%20coli%20chemotaxis%3A%20the%20baffling%20intelligence%20of%20a%20single%20cell&amp;body=(From%20feed%20entry%3A%20https%3A%2F%2Fwww.atomwolf.org%2Fposts%2Fe-coli-chemotaxis%2F)\">Reply via email</a></p>",
			"date_published": "2025-06-25T00:00:00Z"
		%}
		,
		{
			"id": "https://www.atomwolf.org/posts/git-missingcommitscheck/",
			"url": "https://www.atomwolf.org/posts/git-missingcommitscheck/",
			"title": "Never lose a git commit while rebasing",
			"content_html": "<div class=\"dotted-metadata-block\">\n<p><span class=\"small-caps\">Summary</span>: <code>rebase.missingCommitsCheck=\"error\"</code> protects against accidentally losing a commit when using <code>git rebase</code>.</p>\n<p><span class=\"small-caps\">Assumed audience</span>: folks who use <a href=\"https://www.atomwolf.org/tags/git/\">git</a></p>\n<p><a class=\"small-caps\" href=\"https://www.atomwolf.org/about/pages/\">Type</a>: tip</p>\n</div>\n<hr><p>I often interactively rebase git commits at the command line. The editor shows a list of commits.  Each entry has one of a few actions. For example, “pick” means to leave the commit alone, while “squash” or “fixup” will combine a commit with another. If you reorder the lines, you reorder the commits.</p>\n<p>If you delete a line from the rebase list, git quietly drops that commit, removing those changes–by default, at least. There’s a configuration option, <code>rebase.missingCommitsCheck</code>, that controls what happens.</p>\n\n<blockquote>\n<p><a href=\"https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt-rebasemissingCommitsCheck\">rebase.missingCommitsCheck</a></p>\n<p>If set to “warn”, <code>git rebase -i</code> will print a warning if some commits are removed (e.g. a line was deleted), however the rebase will still proceed. If set to “error”, it will print the previous warning and stop the rebase, <code>git rebase --edit-todo</code> can then be used to correct the error. If set to “ignore”, no checking is done. To drop a commit without warning or error, use the drop command in the todo list. Defaults to “ignore”.</p>\n</blockquote>\n\n<p>Setting <code>rebase.missingCommitsCheck</code> to “error” means I need to use the word “drop” to remove a commit on purpose. If I delete a line while rebasing, git will just print an error. Great!</p>\n<h2 id=\"how-to-set-it-up\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/git-missingcommitscheck/#how-to-set-it-up\">How to set it up</a></h2>\n<p>You can set <code>rebase.missingCommitsCheck=\"error\"</code> by running <code>git config --global rebase.missingCommitsCheck error</code> in your terminal.<label for=\"sn-gitconfig\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-gitconfig\" class=\"margin-toggle\">\n<small class=\"sidenote\">There are other ways to set configuration options, and the git book has <a href=\"https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration\">a gentle introduction to configuring git</a>.</small>\n</p>\n<p>I added <code>rebase.missingCommitsCheck=\"error\"</code> to my gitconfig years ago, as soon as I found out about it.<label for=\"sn-origins\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-origins\" class=\"margin-toggle\">\n<small class=\"sidenote\">The missing commits check was <a href=\"https://public-inbox.org/git/1435609232-14232-1-git-send-email-remi.galan-alfonso@ensimag.grenoble-inp.fr/T/#me9d66c389fe64438638cea32dc7e54fa082628f3\">added to Git</a> at the end of June 2015, by Galan Rémi. Thanks!</small>\nRecently, I saw the error for the first time, and I was inspired to post this tip. It took an untold number of rebases, but it actually did save me from accidentally dropping a commit!</p>\n<hr><p><a href=\"mailto:feedcomments@atomwolf.org?subject=Feed%20Reply%20for%20Never%20lose%20a%20git%20commit%20while%20rebasing&amp;body=(From%20feed%20entry%3A%20https%3A%2F%2Fwww.atomwolf.org%2Fposts%2Fgit-missingcommitscheck%2F)\">Reply via email</a></p>",
			"date_published": "2025-05-08T00:00:00Z"
		%}
		,
		{
			"id": "https://www.atomwolf.org/posts/post-guidelines/",
			"url": "https://www.atomwolf.org/posts/post-guidelines/",
			"title": "Post Guidelines",
			"content_html": "<div class=\"dotted-metadata-block\">\n<p><span class=\"small-caps\">Summary</span>: I've collected a set of guidelines to maintain consistency in my writing voice and presentation.</p>\n<p><span class=\"small-caps\">Assumed audience</span>: Future me and curious folks</p>\n<p>\n\t<span class=\"small-caps\">Tags</span>:\n\t\t<a href=\"https://www.atomwolf.org/tags/meta/\" class=\"entry-tag\">meta</a>\n</p>\n<p><span class=\"small-caps\">Created</span>: <time datetime=\"2025-04-17\">17 April 2025</time></p>\n</div>\n<hr><p>Editing takes me a long time. Having a checklist and guidelines makes me faster. I like to collect folks’ personal writing guidelines, style guides, and their reasons to write. I found them helpful when writing my own. Some of these may be helpful for yours, too.</p>\n<p>Some of these guidelines, I took from other folks. Some come from deep convictions. If I don’t follow those, you could take that as a sign that I’m being forced to write under duress or have been replaced by an impostor. Others, I view only as suggestions and gentle reminders.</p>\n<h2 id=\"mechanics\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/post-guidelines/#mechanics\">Mechanics</a></h2>\n<ul>\n<li>Read each sentence.</li>\n<li>Is the tense consistent?</li>\n<li>When drafting, I use hyphens to signify various dashes. Are all the hyphens (-), en dashes (–), and em dashes (—) correct?</li>\n<li>Check that phrasal adjectives are hyphenated.</li>\n<li>Check that I’m consistent with abbreviations and initialisms. Does it make sense to use the expanded version the first time it’s used?</li>\n<li>Do I use too many ellipses?</li>\n<li>Do I use too many parentheses?</li>\n</ul>\n<h2 id=\"structure\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/post-guidelines/#structure\">Structure</a></h2>\n<ul>\n<li>Is every piece necessary?</li>\n<li>Is it a short article? It shouldn’t have an Introduction section.</li>\n<li>Are the sections and paragraphs in the best order?</li>\n<li>If the headings are sentences, are they in sentence case?</li>\n<li>Is this part of a project?</li>\n<li>Does it need sidenotes?</li>\n<li>Does it need a code snippet?</li>\n<li>Does it need an up button?</li>\n<li>How does this relate to other things I’ve posted?  How should they be connected?</li>\n</ul>\n<h2 id=\"opening-and-closing\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/post-guidelines/#opening-and-closing\">Opening and Closing</a></h2>\n<ul>\n<li>Review the opening sentence and opening paragraph.</li>\n</ul>\n\n<figure>\n<blockquote>\n<p>[…] your lead must capture the reader immediately and force him to keep reading. It must cajole him with freshness, or novelty, or paradox, or humor, or surprise, or with an unusual idea, or an interesting fact, or a question. Anything will do, as long as it nudges his curiosity and tugs at his sleeve.</p>\n<p>Next the lead must do some real work. It must provide hard details that tell the reader why the piece was written and why he ought to read it. But don’t dwell on the reason. Coax the reader a little more; keep him inquisitive.</p>\n<p>Continue to build. Every paragraph should amplify the one that preceded it. Give more thought to adding solid detail and less to entertaining the reader. But take special care with the last sentence of each paragraph—it’s the crucial springboard to the next paragraph. Try to give that sentence an extra twist of humor or surprise, like the periodic “snapper” in the routine of a stand-up comic. Make the reader smile and you’ve got him for at least one more paragraph.</p>\n</blockquote>\n<figcaption>— William Zinsser, “On Writing Well, 30th Anniversary Edition”, Chapter 9, “The Lead and the Ending”</figcaption>\n</figure>\n\n<ul>\n<li>Review <a href=\"https://maggieappleton.com/openings/\">On Opening Essays, Conference Talks, and Jam Jars</a>.</li>\n<li>Make sure the piece doesn’t open at the beginning of time.</li>\n<li>Make sure the piece doesn’t start with a statement of what I’m going to write about, then a definition.</li>\n<li>Does the opening have tension (paradoxes, unanswered questions, unresolved action)?</li>\n<li>Review the closing sentence and closing paragraph.\n<ul>\n<li>Is it boring? Has it gone on too long?</li>\n</ul>\n</li>\n</ul>\n<h2 id=\"frontmatter\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/post-guidelines/#frontmatter\">Frontmatter</a></h2>\n<ul>\n<li>Is it a draft?</li>\n<li>Review the title.</li>\n<li>Review the description. (It isn’t just internal! It’s used in search results, when the page is unfurled, and when entry lists include descriptions.)</li>\n<li>Do I pull any features that I didn’t use? Table of Contents? Notes?</li>\n</ul>\n<h2 id=\"metadata-block\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/post-guidelines/#metadata-block\">Metadata block</a></h2>\n<ul>\n<li>Should it have a metadata block?</li>\n<li>Review the summary.</li>\n<li>Review the assumed audience.</li>\n<li>Review the tags</li>\n<li>Is it part of a project or series? Is that in the block?</li>\n</ul>\n<h2 id=\"sidenotes\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/post-guidelines/#sidenotes\">Sidenotes</a></h2>\n<ul>\n<li>Do I need sidenotes?</li>\n<li>Are they in the right place?\n<ul>\n<li>On narrow screens, they get smushed right into the text, but with a little box around them.  Be careful especially in the middle of sentences.</li>\n<li>Many feed readers (and reader mode!) tools put them inline in the text with no delimiters. (Should we add parentheses around them in feeds?)</li>\n</ul>\n</li>\n</ul>\n<h2 id=\"images\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/post-guidelines/#images\">Images</a></h2>\n<ul>\n<li>Does the post need an image? Do any paragraphs?</li>\n<li>Is the image necessary?</li>\n<li>Does the alt text convey the purpose of the image?</li>\n<li>Check the alt text against <a href=\"https://www.w3.org/WAI/tutorials/images/decision-tree/\">this W3 alt-text decision tree</a>.</li>\n<li>Check any lightbox image captions. There’s a site bug where Markdown isn’t expanded.</li>\n<li>If there’s a meme or reference to one, do I link to <a href=\"https://knowyourmeme.com/\">Know Your Meme</a>?</li>\n</ul>\n<h2 id=\"videos\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/post-guidelines/#videos\">Videos</a></h2>\n<ul>\n<li>Does it have a video or link to a video?</li>\n<li>Do I include the length of the video?</li>\n</ul>\n<h2 id=\"table-of-contents\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/post-guidelines/#table-of-contents\">Table of Contents</a></h2>\n<ul>\n<li>Should it have a table of contents?</li>\n<li>If it’s long enough to need a table of contents, make sure it has an Introduction section.</li>\n</ul>\n<h2 id=\"things-i-ve-found-links\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/post-guidelines/#things-i-ve-found-links\">Things I’ve Found (Links)</a></h2>\n\n\n<ul>\n<li>Review Simon Willison’s <a href=\"https://simonwillison.net/2024/Dec/22/link-blog/\">approach to running a link blog</a>.</li>\n<li>Add the names of people who created the thing.</li>\n<li>Is there context about why this thing is worth reading?</li>\n<li>If it’s a video, should I include a quote from it?</li>\n<li>Does it tie to other things I’ve posted?</li>\n<li>Have I proved I read it?</li>\n<li>Should I add a screenshot?</li>\n</ul>\n\n\n<h2 id=\"projects\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/post-guidelines/#projects\">Projects</a></h2>\n<ul>\n<li>Do I include an image or a screenshot? (Or an animated one?)</li>\n</ul>\n<h2 id=\"review\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/post-guidelines/#review\">Review</a></h2>\n<ul>\n<li>Use all the static checkers to cover broken links and other style things.</li>\n<li>Look at the page with a range of screen sizes.</li>\n<li>Share the post with early readers and ask what they think.</li>\n<li>Review the feed markup.</li>\n<li>Check how the feed entry looks in a reader.</li>\n</ul>\n<h2 id=\"resources\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/post-guidelines/#resources\">Resources</a></h2>\n<p>Some of these resources have been used to create this checklist.  Others, I still need to review.</p>\n\n<ul>\n<li>On Writing Well, 30th Anniversary Edition by William Zinsser</li>\n<li><a href=\"https://maggieappleton.com/\">Maggie Appleton</a>’s <a href=\"https://maggieappleton.com/openings/\">On Opening Essays, Conference Talks, and Jam Jars</a></li>\n<li><a href=\"https://simonwillison.net/\">Simon Willison</a>’s <a href=\"https://simonwillison.net/2024/Dec/22/link-blog/\">My approach to running a link blog</a></li>\n<li><a href=\"https://robertheaton.com/\">Robert Heaton</a>’s <a href=\"https://robertheaton.com/2018/12/06/a-blogging-style-guide/\">A blogging style guide</a>, <a href=\"https://robertheaton.com/a-blogging-style-guide-vol-2/\">vol. 2</a>, <a href=\"https://robertheaton.com/better-sentences/\">How to write better sentences</a></li>\n<li><a href=\"https://adrianroselli.com/\">Adrian Roselli</a>’s “<a href=\"https://adrianroselli.com/2024/05/my-approach-to-alt-text.html\">My Approach to Alt Text</a>”</li>\n<li><a href=\"https://tomcritchlow.com/\">Tom Critchlow</a>’s “<a href=\"https://x.com/tomcritchlow/status/1770476697814532121\">how to write a good blog post</a>” tweet, archived here</li>\n</ul>\n<figure>\n<a href=\"https://www.atomwolf.org/posts/post-guidelines/images/a-good-blog-post/\" data-lightbox=\"true\">\n<picture><source type=\"image/avif\" srcset=\"https://www.atomwolf.org/img/tom-critchlow-how-to-write-a-good-blog-post-JZcu1X0OMf-256w.avif 256w, https://www.atomwolf.org/img/tom-critchlow-how-to-write-a-good-blog-post-JZcu1X0OMf-410w.avif 410w, https://www.atomwolf.org/img/tom-critchlow-how-to-write-a-good-blog-post-JZcu1X0OMf-500w.avif 500w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/webp\" srcset=\"https://www.atomwolf.org/img/tom-critchlow-how-to-write-a-good-blog-post-JZcu1X0OMf-256w.webp 256w, https://www.atomwolf.org/img/tom-critchlow-how-to-write-a-good-blog-post-JZcu1X0OMf-410w.webp 410w, https://www.atomwolf.org/img/tom-critchlow-how-to-write-a-good-blog-post-JZcu1X0OMf-500w.webp 500w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/png\" srcset=\"https://www.atomwolf.org/img/tom-critchlow-how-to-write-a-good-blog-post-JZcu1X0OMf-256w.png 256w, https://www.atomwolf.org/img/tom-critchlow-how-to-write-a-good-blog-post-JZcu1X0OMf-410w.png 410w, https://www.atomwolf.org/img/tom-critchlow-how-to-write-a-good-blog-post-JZcu1X0OMf-500w.png 500w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><img alt=\"A five part &quot;Vince McMahon Reaction&quot; showing the Vince McMahon's increasingly amazed face. 1. &quot;Here's a quick riff&quot; McMahon sits intrigued. 2. &quot;About a line of inquiry that's alive for me.&quot; He's staring, obviously excited. 3. &quot;This post posts more questions than answers&quot; He leans back, a little overwhelmed. 4. &quot;with some real texture from my own experiences&quot; His mouth is open in surprise. 5. &quot;Here's a quick drawing I made&quot; He's so unbelievably excited, his eyes glow supernaturally, tinting the entire scene red.\" loading=\"lazy\" decoding=\"async\" src=\"https://www.atomwolf.org/img/tom-critchlow-how-to-write-a-good-blog-post-JZcu1X0OMf-256w.jpeg\" width=\"500\" height=\"863\" srcset=\"https://www.atomwolf.org/img/tom-critchlow-how-to-write-a-good-blog-post-JZcu1X0OMf-256w.jpeg 256w, https://www.atomwolf.org/img/tom-critchlow-how-to-write-a-good-blog-post-JZcu1X0OMf-410w.jpeg 410w, https://www.atomwolf.org/img/tom-critchlow-how-to-write-a-good-blog-post-JZcu1X0OMf-500w.jpeg 500w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"></picture></a>\n<figcaption>\n<p>Tom Critchlow’s “<a href=\"https://knowyourmeme.com/memes/vince-mcmahon-reaction\">Vince McMahon Reaction</a>” with “Here’s a quick riff”, “About a line of inquiry that’s alive for me”, “This post poses more questions than answers”, “With some real texture from my own experiences”, and “Here’s a quick drawing I made”.</p>\n</figcaption>\n</figure>\n\n<p><feedback-system endpoint=\"https://adamwolf-receivefeedback.web.val.run\" watch-selection=\"true\" show-global-button=\"true\"></feedback-system></p>\n<hr><p><a href=\"mailto:feedcomments@atomwolf.org?subject=Feed%20Reply%20for%20Post%20Guidelines&amp;body=(From%20feed%20entry%3A%20https%3A%2F%2Fwww.atomwolf.org%2Fposts%2Fpost-guidelines%2F)\">Reply via email</a></p>",
			"date_published": "2025-04-17T00:00:00Z"
		%}
		,
		{
			"id": "https://www.atomwolf.org/posts/rendering-outlines-with-a-post-processing-shader/",
			"url": "https://www.atomwolf.org/posts/rendering-outlines-with-a-post-processing-shader/",
			"title": "Rendering Outlines with a Post-processing Shader",
			"content_html": "<div class=\"dotted-metadata-block\">\n<p><span class=\"small-caps\">Summary</span>: I learn about shaders to make a post-processing effect for model-viewer that adds outlines to highlight detail in 3D CAD models.</p>\n<p><span class=\"small-caps\">Assumed audience</span>: curious folks on the web. You shouldn't need to know already know 3D graphics, shaders, or OpenGL.<label for=\"sn-assumed-audience\" class=\"margin-toggle sidenote-number\"></label><input type=\"checkbox\" id=\"sn-assumed-audience\" class=\"margin-toggle\"><small class=\"sidenote\">Don't lose heart! The world rewards curiosity.</small></p>\n<p><span class=\"small-caps\">Project</span>: part of <a href=\"https://www.atomwolf.org/projects/showcasing-3d-cad-models/\">Showcasing 3D CAD Models on the Web</a></p>\n<p>\n\t<span class=\"small-caps\">Tags</span>:\n\t\t<a href=\"https://www.atomwolf.org/tags/3d-graphics/\" class=\"entry-tag\">3D graphics</a>, \n\t\t<a href=\"https://www.atomwolf.org/tags/web/\" class=\"entry-tag\">web</a>, \n\t\t<a href=\"https://www.atomwolf.org/tags/computers/\" class=\"entry-tag\">computers</a>\n</p>\n<p><span class=\"small-caps\">Created</span>: <time datetime=\"2024-07-18\">18 July 2024</time></p>\n</div>\n<hr><script type=\"module\" src=\"https://www.atomwolf.org/js/super-outline-effect.es.min.js\"></script>\n<p>Outlines are key to discerning details in 3D CAD models. Let’s compare the modeling view from Fusion 360 with a render from <a href=\"https://modelviewer.dev/\">model-viewer</a>:</p>\n<figure>\n<a href=\"https://www.atomwolf.org/posts/rendering-outlines-with-a-post-processing-shader/images/comparing-fusion-360-and-model-viewer/\" data-lightbox=\"true\">\n<picture><source type=\"image/avif\" srcset=\"https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-256w.avif 256w, https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-410w.avif 410w, https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-512w.avif 512w, https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-650w.avif 650w, https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-850w.avif 850w, https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-1075w.avif 1075w, https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-1243w.avif 1243w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/webp\" srcset=\"https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-256w.webp 256w, https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-410w.webp 410w, https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-512w.webp 512w, https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-650w.webp 650w, https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-850w.webp 850w, https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-1075w.webp 1075w, https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-1243w.webp 1243w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/png\" srcset=\"https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-256w.png 256w, https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-410w.png 410w, https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-512w.png 512w, https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-650w.png 650w, https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-850w.png 850w, https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-1075w.png 1075w, https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-1243w.png 1243w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><img alt=\"A comparison of two views of the same plastic case with a window, internal standoffs, and connector holes. The top view is isometric, outlined, and isn't trying to be photorealistic. The bottom view has perspective, isn't outlined, and the materials look more complicated. The bottom view is so evenly lit that it's difficult to ascertain detail.\" loading=\"eager\" src=\"https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-256w.jpeg\" width=\"1243\" height=\"1938\" srcset=\"https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-256w.jpeg 256w, https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-410w.jpeg 410w, https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-512w.jpeg 512w, https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-650w.jpeg 650w, https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-850w.jpeg 850w, https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-1075w.jpeg 1075w, https://www.atomwolf.org/img/comparing-fusion-360-and-model-viewer-hUS_QpJiqD-1243w.jpeg 1243w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"></picture></a>\n<figcaption>\n<p>Comparison of CAD modeling view (top) from Fusion 360 and a rendered view (bottom) from model-viewer</p>\n</figcaption>\n</figure>\n<p>The same model looks different, even ignoring the slight camera position difference due to manual positioning. The lighting and shadows are different, the materials look different, and the colors are different. The <a href=\"https://en.wikipedia.org/wiki/3D_projection\">perspective</a> is different. Even if I adjusted the lighting, added textures, and used realistic material settings, I’d prefer looking at details in the CAD view because of the outlines.<label for=\"sn-previouslyon\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-previouslyon\" class=\"margin-toggle\">\n<small class=\"sidenote\">I've written about this elsewhere in this series, including <a href=\"https://www.atomwolf.org/posts/experimenting-with-cad-models-in-model-viewer/\">Experimenting with CAD models in model-viewer</a>.</small>\n</p>\n<p>Could I add outlines? Maybe! I didn’t know anything about how model-viewer worked. I browsed the documentation around extensions and modifications, then set the project aside to leave an opening for inspiration.</p>\n<h2 id=\"inspired-by-m-bius\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/rendering-outlines-with-a-post-processing-shader/#inspired-by-m-bius\">Inspired by Mœbius</a></h2>\n<p>A few weeks later, I ran into an <a href=\"https://blog.maximeheckel.com/posts/moebius-style-post-processing/\">in-depth article</a> about creating a post-processing effect inspired by the French artist <a href=\"https://en.wikipedia.org/wiki/Jean_Giraud\">Mœbius</a>.</p>\n<figure>\n<a href=\"https://www.atomwolf.org/posts/rendering-outlines-with-a-post-processing-shader/images/voyage-d-hermes/\" data-lightbox=\"true\">\n<picture><source type=\"image/avif\" srcset=\"https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-256w.avif 256w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-410w.avif 410w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-512w.avif 512w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-650w.avif 650w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-850w.avif 850w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-1075w.avif 1075w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-1280w.avif 1280w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-1420w.avif 1420w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-1660w.avif 1660w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-1860w.avif 1860w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-1882w.avif 1882w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/webp\" srcset=\"https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-256w.webp 256w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-410w.webp 410w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-512w.webp 512w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-650w.webp 650w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-850w.webp 850w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-1075w.webp 1075w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-1280w.webp 1280w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-1420w.webp 1420w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-1660w.webp 1660w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-1860w.webp 1860w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-1882w.webp 1882w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><img alt=\"Illustration by Mœbius of a fantastical cityscape in the desert, drawn with fine black lines and even coloring. Large, rounded structures in soft purple hues fill the foreground, adorned with windows and balconies. A golden airship floats between the buildings. Cliffs rise in the background, and the ground is covered in rocks and scrub.\" loading=\"lazy\" decoding=\"async\" src=\"https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-256w.jpeg\" width=\"1882\" height=\"1440\" srcset=\"https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-256w.jpeg 256w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-410w.jpeg 410w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-512w.jpeg 512w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-650w.jpeg 650w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-850w.jpeg 850w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-1075w.jpeg 1075w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-1280w.jpeg 1280w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-1420w.jpeg 1420w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-1660w.jpeg 1660w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-1860w.jpeg 1860w, https://www.atomwolf.org/img/voyagehermes-2-m%C5%93bius-rjf1BBI_zJ-1882w.jpeg 1882w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"></picture></a>\n<figcaption>\n<p>Plate 2 of Voyage d’Hermès, by Mœbius. Image from <a href=\"https://www.sci-fi-o-rama.com/2020/05/15/voyage-dhermes-a-moebius-masterwork/\">Sci-Fi-O-Rama</a>.</p>\n</figcaption>\n</figure>\n<p>This article builds a shader, step-by-step, to add outlines, adjust shadows, and create a hand-drawn appearance to a rendered scene.</p>\n<p>To extend model-viewer, you can build on its foundation, <a href=\"https://threejs.org/\">three.js</a>, or use <a href=\"https://modelviewer.dev/examples/postprocessing/\">model-viewer-effects</a>, which adds post-processing effects. The article used <a href=\"https://github.com/pmndrs/react-three-fiber\">react-three-fiber</a>, a library that connects React to three.js! I couldn’t copy-and-paste the code samples—I couldn’t find an example for model-viewer-effects using a custom shader—but this was exactly the inspiration I needed.</p>\n<h2 id=\"adding-a-custom-shader-to-model-viewer\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/rendering-outlines-with-a-post-processing-shader/#adding-a-custom-shader-to-model-viewer\">Adding a custom shader to model-viewer</a></h2>\n<p>I couldn’t find a demo with a custom shader and model-viewer, so I needed to create one. Per the <a href=\"https://modelviewer.dev/examples/postprocessing/\">documentation</a>, I set up <a href=\"https://github.com/pmndrs/postprocessing\">postprocessing</a> and model-viewer-effects. model-viewer-effect’s documentation on <a href=\"https://modelviewer.dev/examples/postprocessing/#custom-effects\">custom effects</a> aims folks at the Postprocessing <a href=\"https://github.com/pmndrs/postprocessing/wiki/Custom-Effects\">Custom Effects</a> wiki page. After reading and re-reading the Custom Effects wiki page, I had a small understanding of how it works.</p>\n<p>A scene is rendered and given to the custom effects. The effect runs C-like code for each pixel of the source image, outputting a pixel used for the output image.</p>\n<p>With much effort, I had the skeleton of a custom effect rigged up to model-viewer. I was able to use it to add a partial outline to the model. This was a promising start, but I couldn’t make any improvements. I needed a deeper understanding.</p>\n<h2 id=\"background\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/rendering-outlines-with-a-post-processing-shader/#background\">Background</a></h2>\n<ul>\n<li>\n<p><a href=\"https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API\">WebGL</a> is a JavaScript API that uses <a href=\"https://en.wikipedia.org/wiki/OpenGL_ES\">OpenGL ES</a> for high-performance 2D and 3D graphics rendering in the browser.</p>\n</li>\n<li>\n<p><a href=\"https://threejs.org/\">Three.js</a> (<a href=\"https://github.com/mrdoob/three.js/\">GitHub</a>) is a JavaScript library that usually uses WebGL for rendering.</p>\n</li>\n<li>\n<p><a href=\"https://modelviewer.dev/\">model-viewer</a> (<a href=\"https://github.com/google/model-viewer\">GitHub</a>) is a JavaScript library and Web Component that uses three.js to display 3D models in the browser.</p>\n</li>\n<li>\n<p>The Poimandres <a href=\"https://github.com/pmndrs/postprocessing\">postprocessing</a> library adds post-processing effects to three.js.</p>\n</li>\n<li>\n<p><a href=\"https://modelviewer.dev/examples/postprocessing/\">model-viewer-effects</a> (<a href=\"https://github.com/google/model-viewer/tree/master/packages/model-viewer-effects\">GitHub</a>) is a library and Web Component that uses the postprocessing library to add effects to model-viewer.</p>\n</li>\n</ul>\n\n<h3 id=\"what-is-post-processing\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/rendering-outlines-with-a-post-processing-shader/#what-is-post-processing\">What is post-processing?</a></h3>\n\n<p><a href=\"https://en.wikipedia.org/wiki/Video_post-processing#Uses_in_3D_rendering\">Post-processing</a> involves rendering a scene to an intermediate buffer before applying effects. Some effects can be applied directly to the intermediate render. Other effects are more complex, needing multiple passes or additional information.</p>\n\n<h3 id=\"what-is-a-shader\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/rendering-outlines-with-a-post-processing-shader/#what-is-a-shader\">What is a shader?</a></h3>\n\n<p>OpenGL renders images using a <a href=\"https://www.khronos.org/opengl/wiki/Rendering_Pipeline_Overview\">rendering pipeline</a>.<label for=\"sn-opengl\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-opengl\" class=\"margin-toggle\">\n<small class=\"sidenote\">The Khronos Group will <a href=\"https://www.khronos.org/opengl/wiki/FAQ#Where_can_I_download_OpenGL?\">remind you</a> that an OpenGL <i>implementation</i> renders images, while OpenGL is only <a href=\"https://www.khronos.org/opengl/wiki/FAQ#What_is_OpenGL?\">a specification and an API definition</a> for rendering.</small>\nSome pipeline steps run programs called <a href=\"https://en.wikipedia.org/wiki/Shader\">shaders</a>. There are different types of shaders, like vertex shaders, which run for each vertex of the geometry, and fragment shaders, which run for each <a href=\"https://www.khronos.org/opengl/wiki/Fragment\">fragment</a>, which is almost the same as a pixel. Shaders vary, but the prototypical vertex shader transforms a vertex’s 3D coordinates to a 2D screen position, and the prototypical fragment shader sets the color of a pixel. OpenGL shaders are typically written in OpenGL Shading Language (GLSL), similar to C.</p>\n<h2 id=\"creating-the-outline-effect\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/rendering-outlines-with-a-post-processing-shader/#creating-the-outline-effect\">Creating the outline effect</a></h2>\n<p>There are many ways to make outlines in 3D graphics.<label for=\"sn-waystooutline\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-waystooutline\" class=\"margin-toggle\">\n<small class=\"sidenote\">See <a href=\"https://ameye.dev/notes/rendering-outlines/\">5 ways to draw an outline</a> and <a href=\"http://geoffprewett.com/blog/software/opengl-outline/\">Generating Beautiful 3D Outlines</a>, for instance.</small>\nOne method uses a fragment shader. Pixel-by-pixel, it looks for edges and draws outlines on the detected edges. It uses different versions of the scene to detect more edges.</p>\n<p>Based on advice from some of the articles I had found, I started with a grayscale version of the rendered scene.</p>\n<figure>\n      <model-viewer src=\"/files/adding-outlines-in-post-processing/usdz.glb\" camera-orbit=\"47.47deg 49.8deg 0.2154m\" field-of-view=\"30deg\" camera-controls=\"\">\n<img alt=\"A low-contrast gray-on-gray rendering of a rectangular case, against a black background.\" loading=\"lazy\" decoding=\"async\" src=\"https://www.atomwolf.org/img/luma-qas3qVzgXW-256w.webp\" width=\"850\" height=\"850\" srcset=\"https://www.atomwolf.org/img/luma-qas3qVzgXW-256w.webp 256w, https://www.atomwolf.org/img/luma-qas3qVzgXW-410w.webp 410w, https://www.atomwolf.org/img/luma-qas3qVzgXW-512w.webp 512w, https://www.atomwolf.org/img/luma-qas3qVzgXW-650w.webp 650w, https://www.atomwolf.org/img/luma-qas3qVzgXW-850w.webp 850w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\" slot=\"poster\">\n        <effect-composer>\n          <super-outline-effect model-colors=\"luma\" outline-enabled=\"false\"></super-outline-effect>\n          <color-grade-effect></color-grade-effect>\n        </effect-composer>\n      </model-viewer>\n<figcaption>\n<p>The grayscale version of the rendered scene's colors</p>\n</figcaption>\n</figure>\n<p>I use the <a href=\"https://en.wikipedia.org/wiki/Sobel_operator\">Sobel operator</a> to detect edges. For each point in the image, the Sobel operator looks at the eight neighboring points to determine if the center point is an edge.<label for=\"sn-kernel\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-kernel\" class=\"margin-toggle\">\n<small class=\"sidenote\">Many image operations can be done by looking at a small window of pixels around each pixel. Each output pixel is created through some math done on that small window of input pixels. This is called <a href=\"https://en.wikipedia.org/wiki/Kernel_(image_processing)\">convolution</a>. It can be tricky to understand. The Mœbius shader article has <a href=\"https://blog.maximeheckel.com/posts/moebius-style-post-processing/#edge-detection-with-sobel-filters\">an interactive Sobel demo</a>, and I found a similar <a href=\"https://setosa.io/ev/image-kernels/\">interactive sharpening demo</a>. If you know of something better, please let me know!</small>\nThe Sobel operator isn’t perfect, but it’s easy to implement in a fragment shader. If the Sobel operator’s calculated value is higher than some threshold, I count it as an edge and color the pixel black.</p>\n<figure>\n      <model-viewer src=\"/files/adding-outlines-in-post-processing/usdz.glb\" camera-orbit=\"47.47deg 49.8deg 0.2154m\" field-of-view=\"30deg\" camera-controls=\"\">\n<img alt=\"Black edges on a white background, outlining a rectangular case. The outline is somewhat complete.\" loading=\"lazy\" decoding=\"async\" src=\"https://www.atomwolf.org/img/luma-edges-zDqBpKKN32-256w.webp\" width=\"850\" height=\"850\" srcset=\"https://www.atomwolf.org/img/luma-edges-zDqBpKKN32-256w.webp 256w, https://www.atomwolf.org/img/luma-edges-zDqBpKKN32-410w.webp 410w, https://www.atomwolf.org/img/luma-edges-zDqBpKKN32-512w.webp 512w, https://www.atomwolf.org/img/luma-edges-zDqBpKKN32-650w.webp 650w, https://www.atomwolf.org/img/luma-edges-zDqBpKKN32-850w.webp 850w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\" slot=\"poster\">\n        <effect-composer>\n          <super-outline-effect model-colors=\"none\" outline-enabled=\"true\" depth-coefficient=\"0\" normal-coefficient=\"0\" luma-coefficient=\"2.5\"></super-outline-effect>\n          <color-grade-effect></color-grade-effect>\n        </effect-composer>\n      </model-viewer>\n<figcaption>\n<p>Edges detected from the grayscale version of the rendered scene's colors</p>\n</figcaption>\n</figure>\n<p>It does an okay job—better than I was expecting! Much of the interior geometry is lost, but the silhouette and some details are outlined.</p>\n<p>The next aspect used to add edges is depth.  The depth map is a grayscale image where the color represents the distance from the viewpoint to the object.</p>\n<figure>\n      <model-viewer src=\"/files/adding-outlines-in-post-processing/usdz.glb\" camera-orbit=\"47.47deg 49.8deg 0.2154m\" field-of-view=\"30deg\" camera-controls=\"\">\n<img alt=\"A hard to see gray-on-gray rendering of a rectangular case, against a gray background. It is difficult to make out any details.\" loading=\"lazy\" decoding=\"async\" src=\"https://www.atomwolf.org/img/depth-RQlQK_DxS7-256w.webp\" width=\"850\" height=\"850\" srcset=\"https://www.atomwolf.org/img/depth-RQlQK_DxS7-256w.webp 256w, https://www.atomwolf.org/img/depth-RQlQK_DxS7-410w.webp 410w, https://www.atomwolf.org/img/depth-RQlQK_DxS7-512w.webp 512w, https://www.atomwolf.org/img/depth-RQlQK_DxS7-650w.webp 650w, https://www.atomwolf.org/img/depth-RQlQK_DxS7-850w.webp 850w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\" slot=\"poster\">\n        <effect-composer>\n          <super-outline-effect model-colors=\"depth\" outline-enabled=\"false\"></super-outline-effect>\n          <color-grade-effect></color-grade-effect>\n        </effect-composer>\n      </model-viewer>\n<figcaption>\n<p>The depth map, where the pixel brightness represents how far away the point is from the viewpoint</p>\n</figcaption>\n</figure>\n<p>Once again, the Sobel operator detects edges.</p>\n<figure>\n      <model-viewer src=\"/files/adding-outlines-in-post-processing/usdz.glb\" camera-orbit=\"47.47deg 49.8deg 0.2154m\" field-of-view=\"30deg\" camera-controls=\"\">\n<img alt=\"Black edges on a white background, outlining a rectangular case. The outer outline is complete, but the inner details are sparse.\" loading=\"lazy\" decoding=\"async\" src=\"https://www.atomwolf.org/img/depth-edges-pJwgsbI6Nu-256w.webp\" width=\"850\" height=\"850\" srcset=\"https://www.atomwolf.org/img/depth-edges-pJwgsbI6Nu-256w.webp 256w, https://www.atomwolf.org/img/depth-edges-pJwgsbI6Nu-410w.webp 410w, https://www.atomwolf.org/img/depth-edges-pJwgsbI6Nu-512w.webp 512w, https://www.atomwolf.org/img/depth-edges-pJwgsbI6Nu-650w.webp 650w, https://www.atomwolf.org/img/depth-edges-pJwgsbI6Nu-850w.webp 850w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\" slot=\"poster\">\n        <effect-composer>\n          <super-outline-effect model-colors=\"none\" outline-enabled=\"true\" depth-coefficient=\"10\" normal-coefficient=\"0\" luma-coefficient=\"0\"></super-outline-effect>\n          <color-grade-effect></color-grade-effect>\n        </effect-composer>\n      </model-viewer>\n<figcaption>\n<p>Edges detected from the depth map</p>\n</figcaption>\n</figure>\n<p>Using the Sobel operator to detect edges using the depth map reveals edges around the window, the cutouts, and the silhouette. Most other edges are missed.</p>\n<p>The next aspect used to detect edges is the normal map. The <a href=\"https://en.wikipedia.org/wiki/Normal_(geometry)\">normal</a> of a surface is the direction the surface faces, and a <a href=\"https://en.wikipedia.org/wiki/Normal_mapping\">normal map</a> is an image that colors each surface based on the direction the surface faces.</p>\n<figure>\n      <model-viewer src=\"/files/adding-outlines-in-post-processing/usdz.glb\" camera-orbit=\"47.47deg 49.8deg 0.2154m\" field-of-view=\"30deg\" camera-controls=\"\">\n<img alt=\"A rendering of a rectangular case, in a muted pinks, blues, and greens, against a blue background. The colors are super cool.\" loading=\"lazy\" decoding=\"async\" src=\"https://www.atomwolf.org/img/normal-XxiOaiyi8Y-256w.webp\" width=\"850\" height=\"850\" srcset=\"https://www.atomwolf.org/img/normal-XxiOaiyi8Y-256w.webp 256w, https://www.atomwolf.org/img/normal-XxiOaiyi8Y-410w.webp 410w, https://www.atomwolf.org/img/normal-XxiOaiyi8Y-512w.webp 512w, https://www.atomwolf.org/img/normal-XxiOaiyi8Y-650w.webp 650w, https://www.atomwolf.org/img/normal-XxiOaiyi8Y-850w.webp 850w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\" slot=\"poster\">\n        <effect-composer>\n          <super-outline-effect model-colors=\"normals\" outline-enabled=\"false\"></super-outline-effect>\n          <color-grade-effect></color-grade-effect>\n        </effect-composer>\n      </model-viewer>\n<figcaption>\n<p>The normal map, where colors represent the direction the surface faces</p>\n</figcaption>\n</figure>\n<p>Using the Sobel operator, again, we highlight detected edges in the normal map.</p>\n<figure>\n      <model-viewer src=\"/files/adding-outlines-in-post-processing/usdz.glb\" camera-orbit=\"47.47deg 49.8deg 0.2154m\" field-of-view=\"30deg\" camera-controls=\"\">\n<img alt=\"Black edges on a white background, outlining a rectangular case. The outline is mostly complete.\" loading=\"lazy\" decoding=\"async\" src=\"https://www.atomwolf.org/img/normal-edges-p5B4nUvZ_w-256w.webp\" width=\"850\" height=\"850\" srcset=\"https://www.atomwolf.org/img/normal-edges-p5B4nUvZ_w-256w.webp 256w, https://www.atomwolf.org/img/normal-edges-p5B4nUvZ_w-410w.webp 410w, https://www.atomwolf.org/img/normal-edges-p5B4nUvZ_w-512w.webp 512w, https://www.atomwolf.org/img/normal-edges-p5B4nUvZ_w-650w.webp 650w, https://www.atomwolf.org/img/normal-edges-p5B4nUvZ_w-850w.webp 850w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\" slot=\"poster\">\n        <effect-composer>\n          <super-outline-effect model-colors=\"none\" outline-enabled=\"true\" depth-coefficient=\"0\" normal-coefficient=\"10\" luma-coefficient=\"0\"></super-outline-effect>\n          <color-grade-effect></color-grade-effect>\n        </effect-composer>\n      </model-viewer>\n<figcaption>\n<p>Edges detected from the normal map</p>\n</figcaption>\n</figure>\n<p>Wow! For this model, at least, the detected edges create a detailed outline. Parallel surfaces without intermediate geometry seem to be the cause of most missing edges.<label for=\"sn-surfaceids\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-surfaceids\" class=\"margin-toggle\">\n<small class=\"sidenote\">There is at least one clever improvement to this method documented in the <a href=\"https://www.atomwolf.org/posts/rendering-outlines-with-a-post-processing-shader/#resources\">resources</a>.</small>\nFor example, when looking at the top from an angle, the far edges of the buttons usually aren’t outlined.</p>\n<p>Next, I combine all the detected edges.</p>\n<figure>\n      <model-viewer src=\"/files/adding-outlines-in-post-processing/usdz.glb\" camera-orbit=\"47.47deg 49.8deg 0.2154m\" field-of-view=\"30deg\" camera-controls=\"\">\n<img alt=\"Black edges on a white background, outlining a rectangular case. The outline is quite good.\" loading=\"lazy\" decoding=\"async\" src=\"https://www.atomwolf.org/img/combined-edges-lO_KaEUnMA-256w.webp\" width=\"850\" height=\"850\" srcset=\"https://www.atomwolf.org/img/combined-edges-lO_KaEUnMA-256w.webp 256w, https://www.atomwolf.org/img/combined-edges-lO_KaEUnMA-410w.webp 410w, https://www.atomwolf.org/img/combined-edges-lO_KaEUnMA-512w.webp 512w, https://www.atomwolf.org/img/combined-edges-lO_KaEUnMA-650w.webp 650w, https://www.atomwolf.org/img/combined-edges-lO_KaEUnMA-850w.webp 850w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\" slot=\"poster\">\n        <effect-composer>\n          <super-outline-effect model-colors=\"none\" outline-enabled=\"true\" depth-coefficient=\"10\" normal-coefficient=\"10\" luma-coefficient=\"2.5\"></super-outline-effect>\n          <color-grade-effect></color-grade-effect>\n        </effect-composer>\n      </model-viewer>\n<figcaption>\n<p>Edges detected from the grayscale version of the scene's colors, the normal map, or the depth map</p>\n</figcaption>\n</figure>\n<p>This looks pretty good!</p>\n<p>Let’s overlay the edges on the rendered scene.</p>\n<figure>\n      <model-viewer src=\"/files/adding-outlines-in-post-processing/usdz.glb\" camera-orbit=\"47.47deg 49.8deg 0.2154m\" field-of-view=\"30deg\" camera-controls=\"\">\n<img alt=\"A rectangular case in two halves, with a window cutout on top. The top half is pink, and the bottom half is yellow. The geometry is outlined in thin black lines.\" loading=\"lazy\" decoding=\"async\" src=\"https://www.atomwolf.org/img/combined-cS4CwFZQ-Z-256w.webp\" width=\"850\" height=\"850\" srcset=\"https://www.atomwolf.org/img/combined-cS4CwFZQ-Z-256w.webp 256w, https://www.atomwolf.org/img/combined-cS4CwFZQ-Z-410w.webp 410w, https://www.atomwolf.org/img/combined-cS4CwFZQ-Z-512w.webp 512w, https://www.atomwolf.org/img/combined-cS4CwFZQ-Z-650w.webp 650w, https://www.atomwolf.org/img/combined-cS4CwFZQ-Z-850w.webp 850w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\" slot=\"poster\">\n        <effect-composer rendermode=\"quality\">\n          <super-outline-effect model-colors=\"material\" outline-enabled=\"true\" outline-thickness=\"1\" depth-coefficient=\"10\" normal-coefficient=\"10\" luma-coefficient=\"2.5\" outline-color=\"black\" shaky-enabled=\"false\"></super-outline-effect>\n          <color-grade-effect></color-grade-effect>\n        </effect-composer>\n      </model-viewer>\n<figcaption>\n<p>Detected edges overlaid upon the original rendered scene</p>\n</figcaption>\n</figure>\n<p>Looking critically, the A and B labels are missing outlines from most viewpoints. Other edges are missing, and there are often so many outlines drawn on the standoffs that it looks like shading. There’s an issue with aliasing.</p>\n<p>It isn’t perfect, but this looks even better than I had hoped!</p>\n<h2 id=\"demos\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/rendering-outlines-with-a-post-processing-shader/#demos\">Demos</a></h2>\n<p>While working on this article, I made a tool to take screenshots of the shader in various configurations. I kajiggered it into <a href=\"https://outline-experiments-adamwolf.replit.app/playground/\">an interactive demo where you can play with the shader settings</a>. As I was brainstorming ways to present an explanation, I made another demo that shows <a href=\"https://outline-experiments-adamwolf.replit.app/mirror/\">all the views of the model</a> in synchrony.</p>\n<h2 id=\"next-steps\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/rendering-outlines-with-a-post-processing-shader/#next-steps\">Next Steps</a></h2>\n<h3 id=\"shader-improvements\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/rendering-outlines-with-a-post-processing-shader/#shader-improvements\">Shader Improvements</a></h3>\n<p>Assuming this works as well on other models, it’s good enough for me to consider outlines handled. In case I change my mind (or for the inevitable tinkering), I’ve collected some articles that detail improvements and alternate approaches.</p>\n<p>I haven’t yet reviewed the shader with care. I built this shader through experimentation and chose the various settings haphazardly. I’m certain there are real improvements to be made and bugs to fix, even before improving the actual algorithm.</p>\n<h3 id=\"moving-on\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/rendering-outlines-with-a-post-processing-shader/#moving-on\">Moving on</a></h3>\n<p>The next step is to wrap this outline effect into a Web Component that integrates with model-viewer-effects. No one should have to write JavaScript to add this effect. It’ll go inside <code>&lt;model-viewer&gt;</code> tags like the built-in effects.</p>\n<p>Additionally, I want to create a one-page “quick and dirty” example for using a custom shader with model-viewer, and an example model-viewer-effect-alike Web Component. I don’t think custom shaders for model-viewer are commonly asked about, but I want to help document them.</p>\n<h2 id=\"resources\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/rendering-outlines-with-a-post-processing-shader/#resources\">Resources</a></h2>\n \n<ul>\n<li><a href=\"https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API\">WebGL: 2D and 3D graphics for the web</a> Mozilla’s WebGL documentation. It includes reference material, tutorials, guides, articles, and talks. It seems to be a good starting point, and what I’ve read has been clear and helpful.</li>\n<li><a href=\"https://www.khronos.org/opengl/wiki/\">Khronos OpenGL wiki</a> Khronos, the group behind OpenGL, has an in-depth wiki.</li>\n<li><a href=\"https://ameye.dev/notes/rendering-outlines/\">5 ways to draw an outline</a> Overview of five ways to draw outlines in 3D graphics.</li>\n<li><a href=\"http://geoffprewett.com/blog/software/opengl-outline/\">Generating Beautiful 3D Outlines</a> Three ways to draw outlines: offset and scale, stencil and wireframe, and post-processing.</li>\n<li><a href=\"https://blog.maximeheckel.com/posts/moebius-style-post-processing/\">Moebius-style post-processing and other stylized shaders</a> Quite good. Dissects “Mœbius style”, which includes outlines. Uses post-processing with depth and normals. Code is JavaScript, using react-three-fiber, but definitely doesn’t require React knowledge to understand.</li>\n<li><a href=\"https://tympanus.net/codrops/2022/11/29/sketchy-pencil-effect-with-three-js-post-processing/\">Sketchy Pencil Effect with Three.js Post-Processing</a> Uses post-processing with normals and grayscale version of the rendered scene. Code is JavaScript, using three.js.</li>\n<li><a href=\"https://ameye.dev/notes/edge-detection-outlines/\">Edge Detection Outlines</a></li>\n<li><a href=\"https://omar-shehata.medium.com/how-to-render-outlines-in-webgl-8253c14724f9\">How to render outlines in WebGL</a></li>\n<li><a href=\"https://www.ronja-tutorials.com/post/019-postprocessing-outlines/\">Outlines via Postprocessing<br>\n</a> Post-processing, uses depth and normals. Code is C# for Unity.</li>\n<li><a href=\"https://roystan.net/articles/outline-shader/\">Unity Outline Shader</a> A detailed walkthrough creating and improving an outline shader in Unity. Code is C#. It seems to be similar but significantly improved from what I’m doing here.</li>\n<li><a href=\"https://omar-shehata.medium.com/better-outline-rendering-using-surface-ids-with-webgl-e13cdab1fd94\">Better outline rendering using surface IDs with WebGL</a> (<a href=\"https://github.com/OmarShehata/webgl-outlines/\">GitHub</a>) Highlights issues with using normals, and assigns surface IDs instead.</li>\n<li><a href=\"https://www.videopoetics.com/tutorials/pixel-perfect-outline-shaders-unity/\">Pixel-Perfect Outline Shaders for Unity</a> Different method than our post-processing edge-detection, but good article. Uses a vertex shader to add a slightly larger mesh. Code is for Unity.</li>\n<li><a href=\"https://www.youtube.com/watch?v=jlKNOirh66E\">Moebius-style 3D Rendering</a> ~8-minute video. Breaks down the “Mœbius style”, and imitates it in Unity. Helpful explanation and visuals. Beginner-friendly, but it expects 3D rendering knowledge.</li>\n</ul>\n\n<hr><p><a href=\"mailto:feedcomments@atomwolf.org?subject=Feed%20Reply%20for%20Rendering%20Outlines%20with%20a%20Post-processing%20Shader&amp;body=(From%20feed%20entry%3A%20https%3A%2F%2Fwww.atomwolf.org%2Fposts%2Frendering-outlines-with-a-post-processing-shader%2F)\">Reply via email</a></p>",
			"date_published": "2024-07-18T00:00:00Z"
		%}
		,
		{
			"id": "https://www.atomwolf.org/posts/experimenting-with-cad-models-in-model-viewer/",
			"url": "https://www.atomwolf.org/posts/experimenting-with-cad-models-in-model-viewer/",
			"title": "Experimenting with CAD models in model-viewer",
			"content_html": "<div class=\"dotted-metadata-block\">\n<p><span class=\"small-caps\">Summary</span>: I use Blender to export models from Fusion 360 to model-viewer, and I investigate unexpected visual problems.</p>\n<p><span class=\"small-caps\">Assumed audience</span>: folks on the web</p>\n<p><span class=\"small-caps\">Project</span>: part of <a href=\"https://www.atomwolf.org/projects/showcasing-3d-cad-models/\">Showcasing 3D CAD Models on the Web</a></p>\n<p>\n\t<span class=\"small-caps\">Tags</span>:\n\t\t<a href=\"https://www.atomwolf.org/tags/web/\" class=\"entry-tag\">web</a>, \n\t\t<a href=\"https://www.atomwolf.org/tags/computers/\" class=\"entry-tag\">computers</a>, \n\t\t<a href=\"https://www.atomwolf.org/tags/3d-graphics/\" class=\"entry-tag\">3D graphics</a>\n</p>\n<p><span class=\"small-caps\">Created</span>: <time datetime=\"2024-06-10\">10 June 2024</time></p>\n</div>\n<hr><p>After exploring options for showcasing 3D CAD models on the web and compiling a list of desired features, I’m ready to experiment with Google’s model-viewer, using a real model I’ve been working on.</p>\n<p>I’m making a small case for a <a href=\"https://www.lilygo.cc/products/t-display-s3\">T-Display S3</a> with a few buttons, a cutout for the screen and the USB-C port, a small prototyping PCB, and two Molex SL connectors. I exported some images from the modeling view in Fusion 360.<label for=\"sn-fusion360\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-fusion360\" class=\"margin-toggle\">\n<small class=\"sidenote\"><a href=\"https://en.wikipedia.org/wiki/Fusion_360\">Fusion 360</a>, a closed-source CAD suite, is my go-to for engineering 3D models. Although Autodesk recently added some features I've been wanting for years, like <a href=\"https://help.autodesk.com/view/fusion360/ENU/?guid=CFG-OVERVIEW\">Configurations</a>, I worry about its future.  Wikipedia informs me it's been recently rebranded as just \"Fusion\".</small>\n</p>\n<figure>\n<a href=\"https://www.atomwolf.org/posts/experimenting-with-cad-models-in-model-viewer/images/cad-view-with-electronics/\" data-lightbox=\"true\">\n<picture><source type=\"image/avif\" srcset=\"https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-256w.avif 256w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-410w.avif 410w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-512w.avif 512w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-650w.avif 650w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-850w.avif 850w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-1075w.avif 1075w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-1280w.avif 1280w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/webp\" srcset=\"https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-256w.webp 256w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-410w.webp 410w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-512w.webp 512w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-650w.webp 650w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-850w.webp 850w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-1075w.webp 1075w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-1280w.webp 1280w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/png\" srcset=\"https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-256w.png 256w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-410w.png 410w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-512w.png 512w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-650w.png 650w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-850w.png 850w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-1075w.png 1075w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-1280w.png 1280w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><img alt=\"A cropped screenshot from Fusion 360 showing the plastic case, connectors, and the display of the T-Display S3\" loading=\"eager\" src=\"https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-256w.jpeg\" width=\"1280\" height=\"1024\" srcset=\"https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-256w.jpeg 256w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-410w.jpeg 410w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-512w.jpeg 512w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-650w.jpeg 650w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-850w.jpeg 850w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-1075w.jpeg 1075w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-full-fUThdBuhnG-1280w.jpeg 1280w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"></picture></a>\n<figcaption>\n<p>Showing all the components in Fusion 360’s modeling view</p>\n</figcaption>\n</figure>\n<p>To show the case details, I hid the electronics and the connectors.</p>\n<figure>\n<a href=\"https://www.atomwolf.org/posts/experimenting-with-cad-models-in-model-viewer/images/plastics-cad-view/\" data-lightbox=\"true\">\n<picture><source type=\"image/avif\" srcset=\"https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-256w.avif 256w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-410w.avif 410w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-512w.avif 512w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-650w.avif 650w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-850w.avif 850w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-1075w.avif 1075w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-1280w.avif 1280w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/webp\" srcset=\"https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-256w.webp 256w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-410w.webp 410w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-512w.webp 512w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-650w.webp 650w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-850w.webp 850w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-1075w.webp 1075w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-1280w.webp 1280w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/png\" srcset=\"https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-256w.png 256w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-410w.png 410w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-512w.png 512w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-650w.png 650w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-850w.png 850w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-1075w.png 1075w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-1280w.png 1280w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><img alt=\"A cropped screenshot from Fusion 360 showing the plastic case without the electronics or connectors. The cutout for the display reveals internal standoffs.\" loading=\"lazy\" decoding=\"async\" src=\"https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-256w.jpeg\" width=\"1280\" height=\"1024\" srcset=\"https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-256w.jpeg 256w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-410w.jpeg 410w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-512w.jpeg 512w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-650w.jpeg 650w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-850w.jpeg 850w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-1075w.jpeg 1075w, https://www.atomwolf.org/img/Case-(T-Display-S3)-v7-plastics-only-KBDJYaqVnx-1280w.jpeg 1280w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"></picture></a>\n<figcaption>\n<p>Showing only the printed components in Fusion 360’s modeling view</p>\n</figcaption>\n</figure>\n<h2 id=\"exporting-from-fusion-360\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/experimenting-with-cad-models-in-model-viewer/#exporting-from-fusion-360\">Exporting from Fusion 360</a></h2>\n<p>model-viewer requires models in glTF format (or GLB, glTF’s binary equivalent). Fusion 360 doesn’t export to these formats without a plugin.<label for=\"sn-plugin\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-plugin\" class=\"margin-toggle\">\n<small class=\"sidenote\">I found two proprietary glTF exporters, one by <a href=\"https://prototechsolutions.com/3d-products/fusion-360/gltf-exporter/\">ProtoTech Solutions</a> and another by <a href=\"https://www.simlab-soft.com/3d-plugins/Fusion360/GLTF_Exporter_For_Fusion-main.html\">SimLab</a>, but I want to avoid a closed-source plugin.</small>\nBased on the <a href=\"https://modelviewer.dev/docs/faq.html#entrydocs-tools-questions-export\">model-viewer FAQ’s recommendation</a>, I decided to try <a href=\"https://blender.org\">Blender</a> as a converter.<label for=\"sn-blender\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-blender\" class=\"margin-toggle\">\n<small class=\"sidenote\">I like Blender. <a href=\"https://www.blender.org/\">Blender</a> is open source, and it's used by professionals and hobbyists. I haven't spent more than an hour or two with it. though, which isn't even enough time to make <a href=\"https://www.blenderguru.com/\">a good donut</a>.</small>\n</p>\n<p>I exported the model from Fusion 360 in both FBX and USDz formats for comparison.</p>\n<h2 id=\"fbx\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/experimenting-with-cad-models-in-model-viewer/#fbx\">FBX</a></h2>\n<p><a href=\"https://en.wikipedia.org/wiki/FBX\">FBX</a> is a proprietary file format controlled by Autodesk.</p>\n<p>When importing the FBX into Blender using defaults, the model was oddly oriented.</p>\n<figure>\n<a href=\"https://www.atomwolf.org/posts/experimenting-with-cad-models-in-model-viewer/images/blender-fbx-import/\" data-lightbox=\"true\">\n<picture><source type=\"image/avif\" srcset=\"https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-256w.avif 256w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-410w.avif 410w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-512w.avif 512w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-650w.avif 650w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-850w.avif 850w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-1075w.avif 1075w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-1280w.avif 1280w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-1420w.avif 1420w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-1660w.avif 1660w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-1860w.avif 1860w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-2048w.avif 2048w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-3134w.avif 3134w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/webp\" srcset=\"https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-256w.webp 256w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-410w.webp 410w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-512w.webp 512w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-650w.webp 650w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-850w.webp 850w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-1075w.webp 1075w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-1280w.webp 1280w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-1420w.webp 1420w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-1660w.webp 1660w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-1860w.webp 1860w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-2048w.webp 2048w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-3134w.webp 3134w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/png\" srcset=\"https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-256w.png 256w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-410w.png 410w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-512w.png 512w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-650w.png 650w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-850w.png 850w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-1075w.png 1075w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-1280w.png 1280w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-1420w.png 1420w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-1660w.png 1660w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-1860w.png 1860w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-2048w.png 2048w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-3134w.png 3134w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><img alt=\"A screenshot of Blender showing the plastic case on its end.\" loading=\"lazy\" decoding=\"async\" src=\"https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-256w.jpeg\" width=\"3134\" height=\"2112\" srcset=\"https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-256w.jpeg 256w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-410w.jpeg 410w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-512w.jpeg 512w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-650w.jpeg 650w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-850w.jpeg 850w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-1075w.jpeg 1075w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-1280w.jpeg 1280w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-1420w.jpeg 1420w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-1660w.jpeg 1660w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-1860w.jpeg 1860w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-2048w.jpeg 2048w, https://www.atomwolf.org/img/blender-fbx-import-6PsqPryEeX-3134w.jpeg 3134w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"></picture></a>\n<figcaption>\n<p>FBX model imported into Blender</p>\n</figcaption>\n</figure>\n<p>After some tinkering, I found that setting the import options for Manual Orientation to “Y Forward” and “Z Up” in Blender’s FBX import dialog resolved the orientation issues. Exporting to glTF 2.0 with the “+Y Up” option checked produced a correctly oriented model in model-viewer.</p>\n<p>To check scaling, I used the validation report in model-viewer’s editor. The model dimensions matched the dimensions in Fusion 360.</p>\n<figure>\n<a href=\"https://www.atomwolf.org/posts/experimenting-with-cad-models-in-model-viewer/images/fbx-export-details/\" data-lightbox=\"true\">\n<picture><source type=\"image/avif\" srcset=\"https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-256w.avif 256w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-410w.avif 410w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-512w.avif 512w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-650w.avif 650w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-850w.avif 850w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-1075w.avif 1075w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-1280w.avif 1280w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-1420w.avif 1420w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-1660w.avif 1660w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-1860w.avif 1860w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-2048w.avif 2048w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-2862w.avif 2862w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/webp\" srcset=\"https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-256w.webp 256w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-410w.webp 410w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-512w.webp 512w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-650w.webp 650w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-850w.webp 850w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-1075w.webp 1075w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-1280w.webp 1280w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-1420w.webp 1420w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-1660w.webp 1660w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-1860w.webp 1860w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-2048w.webp 2048w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-2862w.webp 2862w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/png\" srcset=\"https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-256w.png 256w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-410w.png 410w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-512w.png 512w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-650w.png 650w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-850w.png 850w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-1075w.png 1075w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-1280w.png 1280w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-1420w.png 1420w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-1660w.png 1660w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-1860w.png 1860w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-2048w.png 2048w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-2862w.png 2862w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><img alt=\"A screenshot of model-viewer's editor showing the plastic case from the side with similar colors as the CAD view. The right side of the editor shows controls and sample HTML.\" loading=\"lazy\" decoding=\"async\" src=\"https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-256w.jpeg\" width=\"2862\" height=\"1662\" srcset=\"https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-256w.jpeg 256w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-410w.jpeg 410w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-512w.jpeg 512w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-650w.jpeg 650w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-850w.jpeg 850w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-1075w.jpeg 1075w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-1280w.jpeg 1280w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-1420w.jpeg 1420w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-1660w.jpeg 1660w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-1860w.jpeg 1860w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-2048w.jpeg 2048w, https://www.atomwolf.org/img/fbx-editor-uCOv9ztGzy-2862w.jpeg 2862w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"></picture></a>\n<figcaption>\n<p>Fusion 360 to FBX to glTF 2.0 via Blender, viewed in model-viewer’s editor</p>\n</figcaption>\n</figure>\n<p>With the Manual Orientation adjustment on importing FBX and the +Y Up option set when exporting to glTF 2.0, the glTF file has the correct orientation and scale.</p>\n<h2 id=\"usdz\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/experimenting-with-cad-models-in-model-viewer/#usdz\">USDz</a></h2>\n<p>USDz is an open-source format developed by Pixar, with wide support in Apple products. It’s part of <a href=\"https://en.wikipedia.org/wiki/Universal_Scene_Description\">Universal Scene Description</a> (USD).</p>\n<p>Importing the USDz file into Blender maintained the correct orientation, but I encountered scaling issues. Setting the import scale to 0.001 in Blender’s import dialog resolved this problem.</p>\n<figure>\n<a href=\"https://www.atomwolf.org/posts/experimenting-with-cad-models-in-model-viewer/images/usdz-export-details/\" data-lightbox=\"true\">\n<picture><source type=\"image/avif\" srcset=\"https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-256w.avif 256w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-410w.avif 410w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-512w.avif 512w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-650w.avif 650w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-850w.avif 850w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-1075w.avif 1075w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-1280w.avif 1280w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-1420w.avif 1420w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-1660w.avif 1660w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-1860w.avif 1860w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-2048w.avif 2048w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-2858w.avif 2858w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/webp\" srcset=\"https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-256w.webp 256w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-410w.webp 410w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-512w.webp 512w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-650w.webp 650w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-850w.webp 850w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-1075w.webp 1075w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-1280w.webp 1280w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-1420w.webp 1420w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-1660w.webp 1660w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-1860w.webp 1860w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-2048w.webp 2048w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-2858w.webp 2858w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/png\" srcset=\"https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-256w.png 256w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-410w.png 410w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-512w.png 512w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-650w.png 650w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-850w.png 850w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-1075w.png 1075w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-1280w.png 1280w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-1420w.png 1420w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-1660w.png 1660w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-1860w.png 1860w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-2048w.png 2048w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-2858w.png 2858w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><img alt=\"A screenshot of model-viewer's editor showing the plastic case from the side with similar colors as the CAD view, but slightly different than the version exported through FBX. The right side of the editor shows controls and sample HTML.\" loading=\"lazy\" decoding=\"async\" src=\"https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-256w.jpeg\" width=\"2858\" height=\"1660\" srcset=\"https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-256w.jpeg 256w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-410w.jpeg 410w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-512w.jpeg 512w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-650w.jpeg 650w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-850w.jpeg 850w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-1075w.jpeg 1075w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-1280w.jpeg 1280w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-1420w.jpeg 1420w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-1660w.jpeg 1660w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-1860w.jpeg 1860w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-2048w.jpeg 2048w, https://www.atomwolf.org/img/usdz-editor-iszvoR9bWQ-2858w.jpeg 2858w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"></picture></a>\n<figcaption>\n<p>Fusion 360 to USDz to glTF 2.0 via Blender, viewed in model-viewer’s editor</p>\n</figcaption>\n</figure>\n<p>With the scale adjustment on importing USDz and the +Y Up option set when exporting to glTF 2.0, the glTF file has the correct orientation and scale.</p>\n<h2 id=\"rendering-challenges\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/experimenting-with-cad-models-in-model-viewer/#rendering-challenges\">Rendering Challenges</a></h2>\n<p>With the model successfully imported into model-viewer, I quickly realized that, compared to Fusion 360’s modeling view, it was difficult to see details in the model in model-viewer.</p>\n<figure>\n<a href=\"https://www.atomwolf.org/posts/experimenting-with-cad-models-in-model-viewer/images/neutral-lighting/\" data-lightbox=\"true\">\n<picture><source type=\"image/avif\" srcset=\"https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-256w.avif 256w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-410w.avif 410w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-512w.avif 512w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-650w.avif 650w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-850w.avif 850w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-1075w.avif 1075w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-1280w.avif 1280w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-1420w.avif 1420w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-1660w.avif 1660w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-1860w.avif 1860w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-2048w.avif 2048w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-2864w.avif 2864w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/webp\" srcset=\"https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-256w.webp 256w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-410w.webp 410w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-512w.webp 512w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-650w.webp 650w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-850w.webp 850w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-1075w.webp 1075w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-1280w.webp 1280w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-1420w.webp 1420w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-1660w.webp 1660w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-1860w.webp 1860w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-2048w.webp 2048w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-2864w.webp 2864w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/png\" srcset=\"https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-256w.png 256w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-410w.png 410w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-512w.png 512w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-650w.png 650w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-850w.png 850w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-1075w.png 1075w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-1280w.png 1280w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-1420w.png 1420w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-1660w.png 1660w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-1860w.png 1860w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-2048w.png 2048w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-2864w.png 2864w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><img alt=\"A screenshot of model-viewer's editor showing the plastic case from above, with uniform coloring and few shadows.\" loading=\"lazy\" decoding=\"async\" src=\"https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-256w.jpeg\" width=\"2864\" height=\"1664\" srcset=\"https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-256w.jpeg 256w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-410w.jpeg 410w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-512w.jpeg 512w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-650w.jpeg 650w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-850w.jpeg 850w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-1075w.jpeg 1075w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-1280w.jpeg 1280w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-1420w.jpeg 1420w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-1660w.jpeg 1660w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-1860w.jpeg 1860w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-2048w.jpeg 2048w, https://www.atomwolf.org/img/neutral-env-JcfXwP-ILc-2864w.jpeg 2864w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"></picture></a>\n<figcaption>\n<p>model-viewer rendering with “neutral” lighting environment</p>\n</figcaption>\n</figure>\n<p>model-viewer lets you control the lighting and environment around the model. The previous image showed the model using the default scene with even lighting on all sides.</p>\n<p>model-viewer also has a “legacy” environment meant for frontward viewing.</p>\n<figure>\n<a href=\"https://www.atomwolf.org/posts/experimenting-with-cad-models-in-model-viewer/images/legacy-lighting/\" data-lightbox=\"true\">\n<picture><source type=\"image/avif\" srcset=\"https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-256w.avif 256w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-410w.avif 410w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-512w.avif 512w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-650w.avif 650w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-850w.avif 850w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-1075w.avif 1075w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-1280w.avif 1280w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-1420w.avif 1420w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-1660w.avif 1660w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-1860w.avif 1860w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-2048w.avif 2048w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-2862w.avif 2862w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/webp\" srcset=\"https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-256w.webp 256w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-410w.webp 410w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-512w.webp 512w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-650w.webp 650w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-850w.webp 850w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-1075w.webp 1075w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-1280w.webp 1280w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-1420w.webp 1420w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-1660w.webp 1660w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-1860w.webp 1860w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-2048w.webp 2048w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-2862w.webp 2862w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/png\" srcset=\"https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-256w.png 256w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-410w.png 410w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-512w.png 512w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-650w.png 650w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-850w.png 850w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-1075w.png 1075w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-1280w.png 1280w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-1420w.png 1420w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-1660w.png 1660w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-1860w.png 1860w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-2048w.png 2048w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-2862w.png 2862w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><img alt=\"A screenshot of model-viewer's editor showing the plastic case from above. The model is well lit, but not uniformly lit, and the front right face is slightly shadowed.\" loading=\"lazy\" decoding=\"async\" src=\"https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-256w.jpeg\" width=\"2862\" height=\"1664\" srcset=\"https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-256w.jpeg 256w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-410w.jpeg 410w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-512w.jpeg 512w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-650w.jpeg 650w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-850w.jpeg 850w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-1075w.jpeg 1075w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-1280w.jpeg 1280w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-1420w.jpeg 1420w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-1660w.jpeg 1660w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-1860w.jpeg 1860w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-2048w.jpeg 2048w, https://www.atomwolf.org/img/legacy-env-2qAhWZq-DJ-2862w.jpeg 2862w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"></picture></a>\n<figcaption>\n<p>model-viewer rendering with “legacy” lighting environment</p>\n</figcaption>\n</figure>\n<p>The “legacy” environment reveals more details than the “neutral” environment. Neither option came close to the clarity of Fusion 360’s modeling view with its outline rendering.</p>\n<h2 id=\"understanding-the-rendering-differences\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/experimenting-with-cad-models-in-model-viewer/#understanding-the-rendering-differences\">Understanding the Rendering Differences</a></h2>\n<p>This isn’t a case of model-viewer or glTF being “bad”. model-viewer <a href=\"https://github.khronos.org/glTF-Render-Fidelity/comparison/\">does a great job rendering glTF</a>.</p>\n<figure>\n<a href=\"https://www.atomwolf.org/posts/experimenting-with-cad-models-in-model-viewer/images/damaged-helmet/\" data-lightbox=\"true\">\n<picture><source type=\"image/avif\" srcset=\"https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-256w.avif 256w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-410w.avif 410w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-512w.avif 512w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-650w.avif 650w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-850w.avif 850w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-1075w.avif 1075w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-1280w.avif 1280w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-1420w.avif 1420w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-1660w.avif 1660w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-1860w.avif 1860w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-2048w.avif 2048w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-2898w.avif 2898w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/webp\" srcset=\"https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-256w.webp 256w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-410w.webp 410w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-512w.webp 512w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-650w.webp 650w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-850w.webp 850w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-1075w.webp 1075w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-1280w.webp 1280w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-1420w.webp 1420w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-1660w.webp 1660w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-1860w.webp 1860w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-2048w.webp 2048w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-2898w.webp 2898w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/png\" srcset=\"https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-256w.png 256w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-410w.png 410w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-512w.png 512w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-650w.png 650w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-850w.png 850w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-1075w.png 1075w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-1280w.png 1280w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-1420w.png 1420w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-1660w.png 1660w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-1860w.png 1860w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-2048w.png 2048w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-2898w.png 2898w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><img alt=\"model-viewer rendering a detailed, futuristic helmet. It looks realistic.\" loading=\"lazy\" decoding=\"async\" src=\"https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-256w.jpeg\" width=\"2898\" height=\"1702\" srcset=\"https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-256w.jpeg 256w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-410w.jpeg 410w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-512w.jpeg 512w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-650w.jpeg 650w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-850w.jpeg 850w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-1075w.jpeg 1075w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-1280w.jpeg 1280w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-1420w.jpeg 1420w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-1660w.jpeg 1660w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-1860w.jpeg 1860w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-2048w.jpeg 2048w, https://www.atomwolf.org/img/damaged-helmet-GcL8iIsTrV-2898w.jpeg 2898w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"></picture></a>\n<figcaption>\n<p>model-viewer rendering a damaged helmet, one of the example models in <a href=\"https://modelviewer.dev/editor/\">model-viewer’s editor</a></p>\n</figcaption>\n</figure>\n<p>glTF aims for photorealistic results through “<a href=\"https://en.wikipedia.org/wiki/Physically_based_rendering\">physically based rendering</a>”.<label for=\"sn-pbr\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-pbr\" class=\"margin-toggle\">\n<small class=\"sidenote\">glTF's physically based rendering (PBR) uses \"Bidirectional Scattering Distribution Functions\" and microfacets to model light interactions with surfaces. glTF includes a consistent set of parameters, helping to standardize PBR across various rendering engines. The Khronos Group, the consortium behind glTF, has <a href=\"https://github.khronos.org/glTF-Tutorials/PBR/\">a detailed explanation of PBR in glTF</a>.</small>\n</p>\n<p>Good results from physically based rendering need good parameters and textures for the materials. In Fusion 360, I don’t usually do much with materials, textures, or colors. For this model, I set the material to “Plastic” and set the colors to some defaults. The materials were exported through an intermediate format into glTF 2.0.<label for=\"sn-color\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-color\" class=\"margin-toggle\">\n<small class=\"sidenote\">Materials in physically based rendering have a base color and some surface parameters, but the rendered color of the surface may be quite different from the base color. Just like in the real world, the color of the light that bounces off a material isn't just the color of the material. It's influenced by the surface details and what the light was like before it interacted with the material. There are interactive lighting and color examples in the <a href=\"https://modelviewer.dev/examples/lightingandenv/\">Lighting &amp; Skybox</a> model-viewer examples section.  model-viewer's \"neutral\" lighting environment was engineered to color materials closely to the material's base colors. For more, read \"<a href=\"https://modelviewer.dev/examples/color.html\">Achieving Color-Accurate Presentation with glTF</a>\" and \"<a href=\"https://modelviewer.dev/examples/tone-mapping\">Tone Mapping Considerations for Physically-Based Rendering</a>\".</small>\nEven if I do well with the materials (and lighting), though, I should expect the render to show detail like a photo. I think the Fusion 360 modeling view does better than a photo.</p>\n<h2 id=\"exploring-solutions\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/experimenting-with-cad-models-in-model-viewer/#exploring-solutions\">Exploring solutions</a></h2>\n<p>Can I turn on outlines like in Fusion 360’s modeling view? model-viewer supports <a href=\"https://modelviewer.dev/examples/postprocessing/\">model-viewer-effects</a>, an extension that changes the visuals. model-viewer-effect has an <a href=\"https://modelviewer.dev/docs/mve#outline-attributes\">outline effect</a>, but it only adds an outer outline around objects, not internal edges.</p>\n<p>Don’t lose hope! All of this is open source. model-viewer-effects uses <a href=\"https://github.com/pmndrs/postprocessing\">postprocessing</a>, a library that makes effects using vertex and fragment shaders. If I can’t use postprocessing, I could <a href=\"https://modelviewer.dev/docs/faq.html#entrydocs-general-questions-three\">plumb through model-viewer into three.js</a>.</p>\n<h2 id=\"next-steps\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/experimenting-with-cad-models-in-model-viewer/#next-steps\">Next steps</a></h2>\n<p>Can I add outlines to model-viewer?<label for=\"sn-programmerscredo\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-programmerscredo\" class=\"margin-toggle\">\n<small class=\"sidenote\">\"The Programmers’ Credo: we do these things not because they are easy, but because we thought they were going to be easy\",  <a href=\"https://twitter.com/pinboard/status/761656824202276864\">pinboard</a></small>\nI have no experience with WebGL, three.js, or postprocessing. I am not sure what a shader is. <a href=\"https://www.atomwolf.org/posts/rendering-outlines-with-a-post-processing-shader/\">Let’s find out!</a></p>\n\n\n<hr><p><a href=\"mailto:feedcomments@atomwolf.org?subject=Feed%20Reply%20for%20Experimenting%20with%20CAD%20models%20in%20model-viewer&amp;body=(From%20feed%20entry%3A%20https%3A%2F%2Fwww.atomwolf.org%2Fposts%2Fexperimenting-with-cad-models-in-model-viewer%2F)\">Reply via email</a></p>",
			"date_published": "2024-06-10T00:00:00Z"
		%}
		,
		{
			"id": "https://www.atomwolf.org/posts/do-the-bell-thing/",
			"url": "https://www.atomwolf.org/posts/do-the-bell-thing/",
			"title": "Do the Bell Thing",
			"content_html": "<div class=\"dotted-metadata-block\">\n<p><span class=\"small-caps\">Assumed audience</span>: Folks who can read English and might be interested in Esperanto</p>\n<p>\n\t<span class=\"small-caps\">Tags</span>:\n\t\t<a href=\"https://www.atomwolf.org/tags/esperanto/\" class=\"entry-tag\">Esperanto</a>, \n\t\t<a href=\"https://www.atomwolf.org/tags/they-might-be-giants/\" class=\"entry-tag\">They Might Be Giants</a>\n</p>\n<p><span class=\"small-caps\">Created</span>: <time datetime=\"2024-06-05\">05 June 2024</time></p>\n</div>\n<hr><p>Esperanto words are often made of roots extended with affixes and an ending.  For instance, <em><a href=\"https://en.wiktionary.org/wiki/arbo#Esperanto\">arbo</a></em>, “tree”, is the root <em>arb</em> with the <em>–o</em> ending for nouns.  <em>–ar–</em> is an affix meaning “group or collection of a bunch of things of the same type”.  We can smush <em>arb–ar–o</em> together to get <em><a href=\"https://en.wiktionary.org/wiki/arbaro#Esperanto\">arbaro</a></em>, which means “forest”.  Similarly, <em><a href=\"https://en.wiktionary.org/wiki/amiko#Esperanto\">amiko</a></em> means “friend”, and <em><a href=\"https://en.wiktionary.org/wiki/amikaro#Esperanto\">amikaro</a></em> is a “circle of friends”.</p>\n<p>There are lots of normal affixes, like <em>–ejo</em> for “place characterized by”, <em>–end–</em> for “mandatory” or “needs to be”, <em>–ul–</em> for “person characterized by”, or <em>re–</em> for “again”—but there’s also <em>–um–</em>.</p>\n<p>Esperanto has a suffix, <em><a href=\"https://bertilow.com/pmeg/vortfarado/afiksoj/sufiksoj/um.html\">–um–</a></em>, that doesn’t have its own meaning.  It’s used to create another almost-root word related to the base word. An example may be helpful. <em><a href=\"https://en.wiktionary.org/wiki/brako#Esperanto\">Brako</a></em> means “arm”. One of the most common <em>–um–</em> constructions is <em><a href=\"https://en.wiktionary.org/wiki/brakumi#Esperanto\">brakumi</a></em>, which means “to hug”.  There are some exceptions, like number bases and clothing named after body parts, but generally, you can’t break down an <em>–um–</em> word to figure out what it means.  Without knowing exactly what <em>brakumi</em> means, the closest you could decipher from its parts would be “to do the arm thing”.<label for=\"sn-armthing\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-armthing\" class=\"margin-toggle\">\n<small class=\"sidenote\">When you break down the Esperanto word for \"to hug\", you get \"to do the arm thing\"? <a href=\"https://english.stackexchange.com/questions/476526/oh-for-cute-grammatical-interpretation\">Oh, for cute!</a></small>\n</p>\n<hr>\n<p>I was listening to <a href=\"https://en.wikipedia.org/wiki/Apollo_18_(album)\">Apollo 18</a> in preparation for an upcoming They Might Be Giants show. One of the tracks, <a href=\"https://tmbw.net/wiki/Dinner_Bell\">Dinner Bell</a>, includes the following lyrics:</p>\n\n\n<blockquote>\n<p>Dinner bell, dinner bell, do the bell thing<br>\nI’m waiting for the dinner bell to do the bell thing<br>\nDinner bell, dinner bell, ding ding ding</p>\n</blockquote>\n\n\n<p>“Do the bell thing”? This is my chance! I’m going to combine They Might Be Giants and Esperanto, and finally show everyone how cool I really am!</p>\n<p>Ahem.</p>\n<p>If this were something formal, I wouldn’t translate “do the bell thing” with <em>–um–</em>.  Folks wouldn’t know what you meant.  However, translating a They Might Be Giants track? You better believe I’m gonna be singing <em>sonorilumu</em>!<label for=\"sn-moreon\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-moreon\" class=\"margin-toggle\">\n<small class=\"sidenote\">For more on <em>–um–</em>, see <a href=\"https://www.atomwolf.org/posts/adventures-in-esperanto/\">Adventures in Esperanto</a>'s \"<a href=\"https://www.adventures-in-esperanto.com/to-word-thingy/\">To word-thingy</a>.</small>\n</p>\n<hr><p><a href=\"mailto:feedcomments@atomwolf.org?subject=Feed%20Reply%20for%20Do%20the%20Bell%20Thing&amp;body=(From%20feed%20entry%3A%20https%3A%2F%2Fwww.atomwolf.org%2Fposts%2Fdo-the-bell-thing%2F)\">Reply via email</a></p>",
			"date_published": "2024-06-05T00:00:00Z"
		%}
		,
		{
			"id": "https://www.atomwolf.org/posts/adventures-in-esperanto/",
			"url": "https://www.atomwolf.org/posts/adventures-in-esperanto/",
			"title": "Adventures in Esperanto",
			"content_html": "<div class=\"dotted-metadata-block\">\n<p><span class=\"small-caps\">Summary</span>: \"<a href=\"https://www.adventures-in-esperanto.com\">Adventures in Esperanto</a>\" is a fun blog written in English about Esperanto, enjoyable for Esperanto enthusiasts and anyone who loves language.</p>\n<p>\n\t<span class=\"small-caps\">Tags</span>:\n\t\t<a href=\"https://www.atomwolf.org/tags/links/\" class=\"entry-tag\">links</a>, \n\t\t<a href=\"https://www.atomwolf.org/tags/esperanto/\" class=\"entry-tag\">Esperanto</a>\n</p>\n<p><span class=\"small-caps\">Created</span>: <time datetime=\"2024-06-03\">03 June 2024</time></p>\n</div>\n<hr><p>If I were to write an Esperanto blog, I would hope it would be like <a href=\"https://www.adventures-in-esperanto.com/\">Adventures in Esperanto</a>.</p>\n<p>The author, Andy, has been writing about Esperanto since at least 2011. The posts are playful and are usually inspired by or citing passages from <a href=\"https://bertilow.com/pmeg/index.html\">Plena Manlibro de Esperanta Gramatiko (PMEG)</a>.  Plena Manlibro de Esperanta Gramatiko is an <a href=\"https://en.wikipedia.org/wiki/Plena_Manlibro_de_Esperanta_Gramatiko\">authoritative guide to Esperanto grammar</a>, written for ordinary Esperanto speakers.  I’ve been studying Esperanto for a while, but I am not an expert, so I really appreciate links to authoritative sources.</p>\n<p>Some of the posts would likely be of interest only to folks who speak Esperanto (or are Esperanto-curious), but others should interest anyone who likes to play with language.</p>\n<p>A few examples:</p>\n<p>In “<a href=\"https://www.adventures-in-esperanto.com/funniest-joke/\">Funniest joke on the internet</a>”, Andy is inspired by an image in <a href=\"https://www.reddit.com/r/linguisticshumor/\">/r/linguisticshumor</a> to translate</p>\n\n<blockquote>\n<p>\"I am the ghost of Christmas Future Imperfect Conditional, said the spirit…</p>\n<p>I bring news of what would have been going to happen, if you were not to have been going to change your ways.\"</p>\n</blockquote>\n\n<p>into Esperanto. I won’t spoil it!</p>\n<p>In “<a href=\"https://www.adventures-in-esperanto.com/shortcut-to-phrasal-freedom/\">Shortcut to Phrasal Freedom</a>”, Andy reviews the PMEG section on making words out of phrases, working through examples like “the back-pocketed phone”.</p>\n<p>In “<a href=\"https://www.adventures-in-esperanto.com/laden-with-apricots/\">Laden with Apricots</a>”, Andy remembers a bottle of Glenmorangie Grand Vintage Malt 1991, and translates the description of its flavor notes.</p>\n<hr><p><a href=\"mailto:feedcomments@atomwolf.org?subject=Feed%20Reply%20for%20Adventures%20in%20Esperanto&amp;body=(From%20feed%20entry%3A%20https%3A%2F%2Fwww.atomwolf.org%2Fposts%2Fadventures-in-esperanto%2F)\">Reply via email</a></p>",
			"date_published": "2024-06-03T00:00:00Z"
		%}
		,
		{
			"id": "https://www.atomwolf.org/posts/invocation-for-beginnings/",
			"url": "https://www.atomwolf.org/posts/invocation-for-beginnings/",
			"title": "An Invocation for Beginnings",
			"content_html": "<p>Ze Frank released a video a day for a year from March 2006 to March 2007.<label for=\"sn-iwasthere\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-iwasthere\" class=\"margin-toggle\">\n<small class=\"sidenote\">This seems like ancient history to many, but &quot;I was there, Gandalf. I was there three thousand years ago.&quot;</small>\nIn retrospect, it may be difficult to see how much credit “<a href=\"https://en.wikipedia.org/wiki/The_show_with_zefrank\">the show with ze frank</a>” deserves, because it was such an influence on the medium. Years after “the show” ended, he launched a new creative endeavour with the following video, “<a href=\"https://www.youtube.com/watch?v=RYlCVwxoL_g\">An Invocation for Beginnings</a>”.</p>\n<figure>\n<lite-youtube videoid=\"RYlCVwxoL_g\" title=\"An Invocation for Beginnings\">\n   <a href=\"https://www.youtube.com/watch?v=RYlCVwxoL_g\" title=\"Play Video: An Invocation for Beginnings\" class=\"lty-playbtn\">\n     <span class=\"lyt-visually-hidden\">Play Video: An Invocation for Beginnings</span>\n  </a>\n</lite-youtube>\n<figcaption><a href=\"https://www.youtube.com/watch?v=RYlCVwxoL_g\">An Invocation for Beginnings</a></figcaption>\n</figure>\n<p>It’s an incredibly inspiring call to action for “doing the thing”, making a creative mark. I’ve often returned to it.</p>\n<p>If I were to list my favorite lines from this video, I’d list the whole thing, albeit out of order, a line at a time.  I’ll highlight just one:</p>\n<figure>\n<blockquote cite=\"https://www.youtube.com/watch?v=RYlCVwxoL_g\">\n<p>There is no need to sharpen my pencils anymore. My pencils are sharp enough. Even the dull ones will make a mark.</p>\n</blockquote>\n<figcaption>— Ze Frank, <a href=\"https://www.youtube.com/watch?v=RYlCVwxoL_g\">An Invocation for Beginnings</a></figcaption>\n</figure>\n<p>If you liked this, you may like a piece I wrote with similar themes, “<a href=\"https://www.feelslikeburning.com/2015/10/05/preparing-to-change/\">Preparing to Change</a>”. It starts with <a href=\"https://www.atomwolf.org/tags/they-might-be-giants\">They Might Be Giants</a> lyrics and includes a <em>different</em> Ze Frank video.</p>\n<hr><p><a href=\"mailto:feedcomments@atomwolf.org?subject=Feed%20Reply%20for%20An%20Invocation%20for%20Beginnings&amp;body=(From%20feed%20entry%3A%20https%3A%2F%2Fwww.atomwolf.org%2Fposts%2Finvocation-for-beginnings%2F)\">Reply via email</a></p>",
			"date_published": "2024-05-31T00:00:00Z"
		%}
		,
		{
			"id": "https://www.atomwolf.org/posts/use-vale-with-eleventy/",
			"url": "https://www.atomwolf.org/posts/use-vale-with-eleventy/",
			"title": "Linting an Eleventy site with Vale",
			"content_html": "<div class=\"dotted-metadata-block\">\n<p><span class=\"small-caps\">Summary</span>: Vale checks prose style rules across plain text and markup files, like Markdown and HTML. In this quick tip, I set Vale up to check this site.</p>\n<p><span class=\"small-caps\">Assumed audience</span>: anyone</p>\n<p><a class=\"small-caps\" href=\"https://www.atomwolf.org/about/pages/\">Type</a>: tip</p>\n</div>\n<hr><p>I came across <a href=\"https://errata-ai.github.io/vale/\">Vale</a>, an open-source command-line tool that checks prose style. It’s like a linter or traditional style checker, but instead of code, it’s for text. I’ve been looking for something like this for a while!</p>\n<p>Vale understands a bunch of different files, like Markdown and HTML, and seems to be able to ignore markup pretty well.</p>\n<p>Once you’ve got it set up, there’s a few ways to integrate it into your work. You can put a Vale step in your review process or build pipeline. For folks on GitHub, there’s a <a href=\"https://github.com/errata-ai/vale-action\">Vale GitHub Action</a>, and for other folks, it shouldn’t be tricky to integrate. (It exits non-zero when it finds issues, and it can print findings in multiple formats.) There’s also a Vale Language Server for editors that use Language Server Protocol things, and of course, you can run Vale on the command line.</p>\n<p>I had some confusion and trouble setting it up. I had to review the entirety of the documentation a few times to get a general idea of how things go together.</p>\n<p>I’m still no expert, but here’s how I set it up:</p>\n<ol>\n<li>I <a href=\"https://vale.sh/docs/vale-cli/installation/\">installed Vale</a>.</li>\n<li>I created a <code>.vale.ini</code> file in the root of the project. I started with the <a href=\"https://vale.sh/generator/\">Config Generator</a>.  After reading the documentation and scratching my head, I ended up with something like:</li>\n</ol>\n\n\n  \n  \n  <pre><code> # See https://vale.sh/docs/topics/config/\n # and https://vale.sh/generator/\n .\n StylesPath = .vale\n \n # Hide some alerts based on severity. Set to suggestion, warning, or error.\n MinAlertLevel = suggestion\n \n # Make a directory at config/vocabularies/&lt;thisvalue&gt;/ in the StylesPath.\n # Vale will look there for an accept.txt and reject.txt, which should\n # consist of one word/phrase (or regular expression) per line.\n # The accept lines will be accepted, and the reject line will be rejected.\n #\n # See https://vale.sh/docs/topics/vocab/\n Vocab = atomwolf.org \n \n [*]\n BasedOnStyles = Vale</code></pre>\n<ol start=\"3\">\n<li>I ran Vale at the command line, telling it to check every file ending in <code>.md</code> in the <code>content</code> directory with <code>vale content/**/*.md</code>.</li>\n<li>I reviewed the output. It flagged a lot of spelling errors, which were mostly legitimate words it didn’t recognize. It did flag a legitimate misspelling. I fixed the misspelling.</li>\n<li>I created an <code>atomwolf.org</code> vocabulary file (<code>.vale/</code><wbr><code>config/</code><wbr><code>vocabularies/</code><wbr><code>atomwolf.org/</code><wbr><code>accept.txt</code>) for the words that Vale flagged that I wanted to keep. I put the words and phrases, one per line, in the <code>accept.txt</code> file. I didn’t have any phrases or words to always reject, so I didn’t make a <code>reject.txt</code>.</li>\n<li>I reran Vale to make sure the vocabulary files worked as expected. Great!</li>\n</ol>\n<h2 id=\"ignoring-nunjucks-directives\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/use-vale-with-eleventy/#ignoring-nunjucks-directives\">Ignoring Nunjucks directives</a></h2>\n<p>Nunjucks is a template engine, “<a href=\"https://mozilla.github.io/nunjucks/templating.html\">essentially a port of Jinja2</a>”. Nunjucks uses brackets to delimit filters, comments, and other directives.  Filters are surrounded by {{ and }}, comments are surrounded by {# and #}, and other directives are surrounded with {% and %}.  One of Vale’s strengths is that it’s supposed to understand markup well enough to work around it, but Vale doesn’t understand Nunjucks yet.</p>\n<p>We can tell Vale to ignore these directives using the <code>.vale.ini</code> configuration file. I added:</p>\n\n\n  \n  \n  <pre><code> \n [*.md]\n # Ignore Nunjucks directives, {{ foo }} {% foo %} or {# foo #}\n BlockIgnores = (?s){{.*?}}|{%.*?%}|{#.*?#}\n </code></pre>\n<p><a href=\"https://vale.sh/docs/topics/config/#blockignores\"><code>BlockIgnores</code></a> tells Vale to ignore everything the regular expression matches—anything inside <code>{%</code> and <code>}%</code>, <code>{{</code> and <code>}}</code>, and <code>{#</code> and <code>#}</code> in Markdown files.  It’s pretty good, but it might not be perfect.  If our directives add text to our output files, that text is going to bypass our Vale checks.</p>\n<h2 id=\"input-vs-output\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/use-vale-with-eleventy/#input-vs-output\">Input vs Output</a></h2>\n<p>This site is made with Eleventy.  Eleventy combines Markdown content files with templates and creates HTML files.  If we want to be really pedantic, we need to check our HTML files, not just our Markdown input files, to check our prose after our templating and any other transformations.</p>\n<p>You can check Markdown files and HTML files at the same time, like <code>vale content/**/*.md _site/**/*.html</code>, but I prefer to check them separately.  I check all the Markdown input files first, and only check and the HTML files if the Markdown files pass.  If I misspell something in a Markdown file, it’s more helpful to flag that error and stop. Showing me the error in the HTML files dilutes the output.  Because Vale exits non-zero when it finds problems, I can run Markdown checks first and HTML checks only if the Markdown checks pass with:</p>\n\n\n  \n  \n  <pre><code> vale content/**/*.md &amp;&amp; vale _site/**/*.html</code></pre>\n<h2 id=\"ignoring-a-directory\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/use-vale-with-eleventy/#ignoring-a-directory\">Ignoring a directory</a></h2>\n<p>I have some files that I don’t want to check in <code>content/articles/style/</code>. (They’re used for automated testing.) I used <code>find</code> to give Vale the list of files I wanted it to check, and excluded the ones that should be skipped, with:</p>\n\n\n  \n  \n  <pre><code> find content -type f -name \\*.md ! -path content/articles/style/\\* | xargs -0 vale</code></pre>\n<p><code>find</code> finds every file in the <code>content</code> directory that has a name that ends in <code>.md</code> and isn’t inside <code>content/articles/style/</code>.  It gives those filenames to <code>xargs</code>, which uses those filenames as arguments and runs <code>vale</code>.</p>\n<h2 id=\"vale-styles\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/use-vale-with-eleventy/#vale-styles\">Vale Styles</a></h2>\n<p>The <a href=\"https://vale.sh/docs/topics/styles/#built-in-style\">built-in Vale style</a> is pretty sparse—just spelling with that <code>accepts.txt</code> and <code>rejects.txt</code>. The Vale folks have created style packages for other style guides, like the Google Developer Documentation Style Guide and the Microsoft Writing Style Guide, and they’ve created packages that have the equivalent (or as close as you can get, I guess) of other prose style checkers and linters, like write-good and proselint.  I found these at the “<a href=\"https://vale.sh/types/style/\">Vale Package Hub</a>”.</p>\n<h2 id=\"commented-sample-configuration\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/use-vale-with-eleventy/#commented-sample-configuration\">Commented sample configuration</a></h2>\n<p>After playing around with Vale, I made a sample <code>.vale.ini</code> config file that may be helpful.</p>\n\n\n  \n  \n  <pre><code> \n # See https://vale.sh/docs/topics/config/\n # and https://vale.sh/generator/\n \n StylesPath = .vale\n \n # Hide some alerts based on severity. Set to suggestion, warning, or error.\n MinAlertLevel = suggestion\n \n # Make a directory at config/vocabularies/&lt;thisvalue&gt;/ in the StylesPath.\n # Vale will look there for an accept.txt and reject.txt, which should\n # consist of one word/phrase (or regular expression) per line.\n # The accept lines will be accepted, and the reject line will be rejected.\n #\n # See https://vale.sh/docs/topics/vocab/\n Vocab = atomwolf.org \n \n # Each package is grabbed from an online repository when you run\n # `vale sync` and placed into the StylesPath.\n #\n # See https://vale.sh/docs/topics/packages/\n # and https://vale.sh/hub/\n \n Packages = Google, write-good\n \n [*]\n # This configuration applies to all files.\n # Apply the rules from these styles\n BasedOnStyles = Vale, Google, write-good\n \n [*.md]\n # This configuration applies to all .md files.\n # Ignore Nunjucks directives, {{ foo }} {% foo %} or {# foo #}\n BlockIgnores = (?s){{.*?}}|{%.*?%}|{#.*?#}\n </code></pre>\n<h2 id=\"next-steps\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/use-vale-with-eleventy/#next-steps\">Next Steps</a></h2>\n<p>I looked through the Google and Microsoft style guides. I don’t want to use them as-is—this site has a different voice than the technical documentation they’re intended for.</p>\n<p>I’d like to look through the style sets the Vale folks have made to mimic other tools, like proselint and write-good, and pull in the rules I like.</p>\n<p>What I am really excited about, though, is making my own style guide, especially for different types of pages.  In just a few minutes, I used <a href=\"https://studio.vale.sh/\">Vale Studio</a> to make a few helpful rules, like requiring particular headers in the dotted metadata block at the top of this page, and requiring that articles begin with an “Introduction” section.</p>\n<hr><p><a href=\"mailto:feedcomments@atomwolf.org?subject=Feed%20Reply%20for%20Linting%20an%20Eleventy%20site%20with%20Vale&amp;body=(From%20feed%20entry%3A%20https%3A%2F%2Fwww.atomwolf.org%2Fposts%2Fuse-vale-with-eleventy%2F)\">Reply via email</a></p>",
			"date_published": "2024-05-16T00:00:00Z"
		%}
		,
		{
			"id": "https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/",
			"url": "https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/",
			"title": "Friendly Binary-to-text Encodings",
			"content_html": "<div class=\"dotted-metadata-block\">\n<p><span class=\"small-caps\">Summary</span>: Choosing a text encoding for friendly, easy-to-say, difficult-to-misunderstand identifiers can be tricky, with important choices in density and characters.</p>\n<p><span class=\"small-caps\">Assumed audience</span>: folks at least somewhat familiar with character sets and encoding data.<label for=\"sn-assumed-audience\" class=\"margin-toggle sidenote-number\"></label><input type=\"checkbox\" id=\"sn-assumed-audience\" class=\"margin-toggle\"><small class=\"sidenote\">If this doesn't describe you, don't lose heart! The world rewards curiosity.</small></p>\n<p>\n\t<span class=\"small-caps\">Tags</span>:\n\t\t<a href=\"https://www.atomwolf.org/tags/math/\" class=\"entry-tag\">math</a>, \n\t\t<a href=\"https://www.atomwolf.org/tags/computers/\" class=\"entry-tag\">computers</a>\n</p>\n<p><span class=\"small-caps\">Created</span>: <time datetime=\"2024-04-14\">14 April 2024</time></p>\n</div>\n<hr><p>I needed to create a bunch of identifiers that people were going to sometimes enter manually into a computer, or read to each other before entering them into a computer.  For my situation, ideally, these identifiers would:</p>\n<ul>\n<li>handle ambiguous characters\n<ul>\n<li>people shouldn’t need to worry about the numeral <code>1</code> vs the letter <code>i</code> vs the letter <code>l</code></li>\n</ul>\n</li>\n<li>be allowable in URLs and in file paths in common file systems\n<ul>\n<li>don’t use <code>/</code> or <code>\\</code>, and don’t use characters reserved in URLs like <code>+</code> or <code>?</code></li>\n</ul>\n</li>\n<li>be quick and easy to say out loud\n<ul>\n<li>using mixed-case letters means people would have to say “uppercase <code>j</code>, <code>3</code>, lowercase <code>q</code>, uppercase <code>N</code>”</li>\n<li>fewer characters is generally better</li>\n</ul>\n</li>\n</ul>\n<h2 id=\"approach\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#approach\">Approach</a></h2>\n<p>This group of goals isn’t unusual. I knew I wasn’t the first to have them. Sometimes, I want to dig right in and solve the problem. Sometimes, I want to skim the literature and grab a solution.  Other times, I want to go deeper and understand the goals and constraints that drove the existing solutions. This can be really rewarding. Comparing goals, constraints, and approaches can reveal <a href=\"https://en.wikipedia.org/wiki/There_are_unknown_unknowns\">parts of your problem you didn’t know you had</a><label for=\"sn-nowords\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-nowords\" class=\"margin-toggle\">\n<small class=\"sidenote\">For instance, some of these systems were designed to never produce <a href=\"https://en.wikipedia.org/wiki/Profanity\">particular words</a> and others were designed not to make words at all. Upon reflection, I still don't think it's a goal here, but it's nice to have considered it.</small>\n, but it often rewards in “non-productive” ways, too.  It’s fun, and you’ll never run out of things to talk about at parties.</p>\n<p>Wikipedia has a decent collection of <a href=\"https://en.wikipedia.org/wiki/Binary-to-text_encoding\">ways to encode binary data into text</a>. Some of them overlap with our goals. For instance, some encodings explicitly cite ambiguous characters when explaining how the alphabet<label for=\"sn-dictionary\" class=\"margin-toggle sidenote-number\"></label><input type=\"checkbox\" id=\"sn-dictionary\" class=\"margin-toggle\"> was chosen. <small class=\"sidenote\">The alphabet, or set of symbols used in an encoding system, is sometimes called the “dictionary” or the “character set”.</small> Some try to handle character ambiguity by skipping characters that are easily confusable. Others specify a canonical encoding character for each group of confusable characters, and specify that all characters in the group decode to the same value.</p>\n<p>There are many encoding systems. I collected my notes on some and have summarized them below.</p>\n<h2 id=\"terminology\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#terminology\">Terminology</a></h2>\n<p>These ideas have been around for a long time and have spawned many variants.  Terminology tends to be a little sloppy, unfortunately.  For instance, the term “Base64” is often used to refer to any number of encodings that use 64 printable characters, and the phrase “base 64” is used to refer to a particular encoding called Base64.</p>\n<p><a href=\"https://datatracker.ietf.org/doc/html/rfc4648\">RFC 4648</a> specifies how to encode input into Base32, specifying not just “base”, but also padding, alignment, the characters with their values,  and other details. You could come up with an alternate alphabet, for instance, and while it would use base 32, it wouldn’t be “base32”. “Decimal” and “hexadecimal” don’t quite belong in the same category as “Base64” and “Crockford’s Base32”. (Note: RFC 4648 also defines “base16” and it’s what you’d expect—<code>0</code>–<code>9</code>, <code>A</code>–<code>F</code>.)</p>\n<h2 id=\"decimal\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#decimal\">Decimal</a></h2>\n<p>My default option for encoding numbers for people is decimal. Decimal numerals (or decimals) are quick to say, easy to type, allowed in URLs and file paths, and already invented. They’re a good baseline.</p>\n<p>Notably, using decimals doesn’t even require knowledge of the English alphabet.</p>\n<p>Decimals aren’t dense compared to other choices.  With only ten options per character, four digits in base 10 get us from 0 to 9,999, covering 10,000 values.</p>\n<p>Examples of decimal identifiers include <code>42</code>, <code>100</code>, and <code>01189998819991197253</code>.</p>\n<h2 id=\"hexadecimal\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#hexadecimal\">Hexadecimal</a></h2>\n\n<p>Another option is hexadecimal, or base 16. <a href=\"https://en.wikipedia.org/wiki/Hexadecimal\">Hexadecimal</a> usually uses <code>A</code>–<code>F</code> as the digits beyond <code>0</code>–<code>9</code>.  These characters are unambiguous, easy to type, and are allowed in URLs and file paths.</p>\n\n<p>Hexadecimal doesn’t require mixed-case characters, so no one needs to signify cases like “uppercase <code>A</code>, <code>7</code>, lowercase <code>b</code>”.</p>\n<p>With decimal, there are ways to read larger numbers, like “one thousand, three hundred and thirty-seven.” So far, none of the proposals for pronouncing hexadecimals have caught on (<a href=\"https://www.bzarg.com/p/how-to-pronounce-hexadecimal/\">1</a>, <a href=\"https://en.wikipedia.org/wiki/Hexadecimal#Verbal_and_digital_representations\">2</a>), so we’re limited to reading the characters one at a time.<label for=\"sn-nine-times-f\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-nine-times-f\" class=\"margin-toggle\">\n<small class=\"sidenote\">In the show \"Silicon Valley\", Erlich Bachman says \"Ask me what nine times <code>F</code> is. It’s fleventy-five.\" Nine times <code>0xF</code> is <code>0x87</code>.  It doesn't make obvious sense to me that you'd pronounce <code>0x87</code> as fleventy-five (unlike, say, maybe <code>0xF5</code>). This might be a joke playing on Erlich's abilities; it may just be a mistake—either way, it's funny how long I spent thinking about it.</small>\n</p>\n<p>Sometimes, to distinguish hexadecimal from decimal or other bases, hexadecimal is written with a leading <code>0x</code>, like <code>0x61</code> or <code>0x4D2</code>. There are other programming notations, like an <code>h</code> suffix or a <code>$</code> prefix, and in math, you may see a subscript after parenthesis, like (1012)<sub>16</sub>, but none of these would be necessary for these identifiers.</p>\n<p>Many <a href=\"https://en.wikipedia.org/wiki/Hexspeak\">English words can be made with the traditional hexadecimal alphabet</a>,<label for=\"sn-hexspeak\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-hexspeak\" class=\"margin-toggle\">\n<small class=\"sidenote\">Lots of folks know about the <abbr title=\"bad dudes\"><code>BAD D00D</code>s</abbr> and <abbr title=\"babes\"><code>BABE</code>s</abbr> at the <abbr title=\"café\"><code>CAFE</code></abbr>, drinking <abbr title=\"coffee\"><code>C0FFEE</code></abbr> (even <abbr title=\"decaf\"><code>DECAF</code></abbr>!). The café serves <abbr title=\"food\"><code>F00D</code></abbr>, too, like <abbr title=\"beef\"><code>BEEF</code></abbr>, but few people know that the café has great <abbr title=\"focaccia\"><code>F0CACC1A</code></abbr> and <abbr title=\"falafel\"><code>FA1AFE1</code></abbr>.</small>\nespecially when you allow for letter substitutions, like <code>0</code> for <code>O</code>.</p>\n<p>With 16 options per character, hexadecimal is denser than decimal.  Four hexadecimal characters cover 65,536 values, from <code>0x0000</code> to <code>0xFFFF</code>.</p>\n<p>Examples of hexadecimal identifiers include <code>35</code>, <code>4D2</code>, and <code>BADDECAFC0FFEE</code>.</p>\n<h2 id=\"base64\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#base64\">Base64</a></h2>\n\n<p><a href=\"https://en.wikipedia.org/wiki/Base64\">Base64</a> is commonly used to transmit binary data via ASCII. To get 64 easily typed symbols, we have to use at least some letters in both upper- and lowercase, which makes it more awkward to read out than previous options.</p>\n\n<p>There are many variations in what folks call “Base64”, but the “standard” “Base64” is defined in <a href=\"https://datatracker.ietf.org/doc/html/rfc4648\">RFC 4648</a>.  The normal alphabet has <code>/</code> in it, which can be a pain in URLs and filenames.</p>\n<p>Base64url, an <a href=\"https://datatracker.ietf.org/doc/html/rfc4648#section-5\">alternate standard also proposed in RFC 4648</a>, becomes file- and URL-safe by replacing <code>+</code> and <code>/</code> with <code>-</code> and <code>_</code>. It still has upper- and lowercase letters.  Eliminating <code>+</code> and <code>/</code> make it easier to use in URLs and file systems, but distinguishing <code>-</code> and <code>_</code> may be problematic! (There are other standard alphabets, too.)</p>\n<p>Base64 is quite dense.  Four characters of Base64 cover 16,777,216 values, from <code>AAAA</code> to <code>////</code>.</p>\n<p>Examples of Base64 include <code>NDI=</code>, <code>MTAw</code>, and <code>MTIzNA==</code>.</p>\n<h2 id=\"base32\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#base32\">Base32</a></h2>\n\n<p><a href=\"https://en.wikipedia.org/wiki/Base32\">Base32</a> seems to be closer to our needs. It uses 32 symbols. There are a few standard dictionaries,  and nothing, really, stopping you from creating your own. Base32 variant dictionaries often only contain one letter case, omit <code>/</code>, and some skip easily confused pairs, like <code>1</code> and lowercase <code>l</code>. The “standard” Base32 definition is in <a href=\"https://datatracker.ietf.org/doc/html/rfc4648\">RFC 4648</a>. The alphabet contains <code>A</code>–<code>Z</code>, <code>2</code>–<code>7</code>, skipping <code>0</code> and <code>1</code>.</p>\n<p>The RFC also defines “base32hex” with an alphabet of <code>0</code>–<code>9</code>, <code>A</code>–<code>V</code>. Base32hex has all the numerals in it, and, unlike Base32, when compared bit-wise, the encoded data sorts the same as the decoded data.</p>\n\n<p>With 32 options per character, a four-character Base32 string covers 1,048,576 values.</p>\n<p>Examples of Base32 identifiers include <code>2C45</code>, <code>42</code>, and <code>WORD</code>.</p>\n<h2 id=\"crockford-s-base32\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#crockford-s-base32\">Crockford’s Base32</a></h2>\n<p>“<a href=\"https://www.crockford.com/base32.html\">Crockford’s Base32</a>” excludes <code>I</code>, <code>L</code>, and <code>O</code> for ambiguity, and <code>U</code> for “accidental obscenity”. It also distinguishes decode symbols from encode symbols, allowing someone to enter a <code>1</code>, <code>i</code>, <code>I</code>, <code>L</code>, or <code>l</code>, and they all mean the same thing—but it will re-encode that symbol to a <code>1</code>. Crockford’s Base32 also allows for an optional checksum, which requires an extra five symbols, <code>*</code>,  <code>~</code>, <code>$</code>, <code>=</code>, and <code>U</code>.</p>\n<p>Like the other Base32 variants here, with 32 options per character, a four-character Crockford’s Base32 string covers 1,048,576 values.</p>\n<p>Examples of Crockford’s Base32 identifiers include <code>6GS0</code> and <code>64SK6DR</code>.</p>\n<h2 id=\"base32h\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#base32h\">Base32H</a></h2>\n<p><a href=\"https://base32h.github.io/\">Base32H</a> is a newer proposal, similar to Crockford’s Base32 in <a href=\"https://base32h.github.io/comparisons#crockfords-base32\">a lot of ways</a>. Base32H was designed assuming it’s always encoded into numerals and uppercase letters. Due to the uppercase assumption, while <code>I</code> and <code>1</code> are aliased together, <code>L</code> isn’t included. It also aliases <code>U</code> with <code>V</code>, <code>1</code> with <code>I</code>, and <code>0</code> with <code>O</code>. It doesn’t have a built-in mechanism for checksums. The canonical uppercase encoding may make it awkward in some places, like URLs.</p>\n<p>Like the other Base32 variants here, with 32 options per character, a four-character Base32H string covers 1,048,576 values.</p>\n\n<p>Examples of Base32H include <code>1A</code> and <code>19R</code>, but any string of <code>0</code>–<code>9</code> and <code>A</code>–<code>Z</code> (and <code>a</code>–<code>z</code>) can be decoded using Base32H.</p>\n\n<h2 id=\"base20-and-open-location-codes\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#base20-and-open-location-codes\">Base20 and Open Location Codes</a></h2>\n<p>Google created the “<a href=\"https://en.wikipedia.org/wiki/Open_Location_Code\">Open Location Code</a>” (also known as <a href=\"https://maps.google.com/pluscodes/\">Plus Code</a>) geocode<label for=\"sn-geocode\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-geocode\" class=\"margin-toggle\">\n<small class=\"sidenote\">I could get lost reading about <a href=\"https://en.wikipedia.org/wiki/Geocode\">geocodes</a>; they're so interesting! See also \"<a href=\"https://en.wikipedia.org/wiki/Discrete_global_grid\">discrete global grid</a>\".</small>\nas an alternative to latitude and longitude coordinates.</p>\n<p>Open Location Codes use a base 20 encoding.  They use 2 through 9, C, F, G, H, J, M, P, Q, R, V, W, and X.  Doug Rinckes on Google’s Travel team says:</p>\n<figure>\n<blockquote cite=\"https://opensource.googleblog.com/2015/04/open-location-code-addresses-for.html\">\n<p>[…] the character set was chosen to avoid spelling words in more than 30 different languages. We removed similar looking characters to reduce confusion and errors, and because they aren’t case-sensitive, they can be easily exchanged over the phone.</p>\n</blockquote>\n<figcaption>— <a href=\"https://opensource.googleblog.com/2015/04/open-location-code-addresses-for.html\">Open Location Code: Addresses for everything, everywhere</a></figcaption>\n</figure>\n<p>Avoiding spelling words across many languages wasn’t one of my goals, but it was interesting enough to include in this round-up!</p>\n<p>Open Location Codes are more complicated than just using that alphabet as a base 20 encoding, but an example Open Location Code is <code>86P8XPC6+39</code>.</p>\n<p>With 20 options per character, four digits in base 20 cover 160,000 values.</p>\n<p>Examples of base 20 identifiers using this character set include <code>555</code>, <code>X0X0</code>, and <code>MP303</code>.</p>\n<h2 id=\"word-safe-base32\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#word-safe-base32\">“Word-safe” Base32</a></h2>\n<p>If you take the characters from Open Location Code from before, and use both uppercase and lowercase versions of the letters, you get 32 characters. This has been called a <a href=\"https://en.wikipedia.org/wiki/Base32#Word-safe_alphabet\">“word-safe” Base32</a>.</p>\n<p>This is mixed-case, of course, but the amount it tries to avoid words seems impressive.</p>\n<p>Like the other Base32 variants here, with 32 options per character, a four-character “word-safe” Base32 string covers 1,048,576 values.</p>\n<p>Examples of “word-safe” Base32 identifiers include <code>23</code> and <code>j37xPX</code>.</p>\n<h2 id=\"potentially-ambiguous-character-pairings\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#potentially-ambiguous-character-pairings\">Potentially ambiguous character pairings</a></h2>\n<div class=\"horizontally-scrolling\">\n<table>\n<thead>\n<tr>\n<th>System</th>\n<th>0/Oo</th>\n<th>1/Ii</th>\n<th>1/Ll</th>\n<th>Ii/Ll</th>\n<th>8/Bb</th>\n<th>Uu/Vv</th>\n<th>5/Ss</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#decimal\">Decimal</a></td>\n<td></td>\n<td></td>\n<td></td>\n<td></td>\n<td></td>\n<td></td>\n<td></td>\n</tr>\n<tr>\n<td><a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#hexadecimal\">Hexadecimal</a></td>\n<td></td>\n<td></td>\n<td></td>\n<td></td>\n<td><span aria-label=\"cross mark\" role=\"img\" title=\"cross mark\">❌</span></td>\n<td></td>\n<td></td>\n</tr>\n<tr>\n<td><a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#base64\">Base64</a></td>\n<td><span aria-label=\"cross mark\" role=\"img\" title=\"cross mark\">❌</span></td>\n<td><span aria-label=\"cross mark\" role=\"img\" title=\"cross mark\">❌</span></td>\n<td><span aria-label=\"cross mark\" role=\"img\" title=\"cross mark\">❌</span></td>\n<td><span aria-label=\"cross mark\" role=\"img\" title=\"cross mark\">❌</span></td>\n<td><span aria-label=\"cross mark\" role=\"img\" title=\"cross mark\">❌</span></td>\n<td><span aria-label=\"cross mark\" role=\"img\" title=\"cross mark\">❌</span></td>\n<td><span aria-label=\"cross mark\" role=\"img\" title=\"cross mark\">❌</span></td>\n</tr>\n<tr>\n<td><a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#base64\">base64url</a></td>\n<td><span aria-label=\"cross mark\" role=\"img\" title=\"cross mark\">❌</span></td>\n<td><span aria-label=\"cross mark\" role=\"img\" title=\"cross mark\">❌</span></td>\n<td><span aria-label=\"cross mark\" role=\"img\" title=\"cross mark\">❌</span></td>\n<td><span aria-label=\"cross mark\" role=\"img\" title=\"cross mark\">❌</span></td>\n<td><span aria-label=\"cross mark\" role=\"img\" title=\"cross mark\">❌</span></td>\n<td><span aria-label=\"cross mark\" role=\"img\" title=\"cross mark\">❌</span></td>\n<td><span aria-label=\"cross mark\" role=\"img\" title=\"cross mark\">❌</span></td>\n</tr>\n<tr>\n<td><a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#base20-and-open-location-codes\">Open Location Code Base 20</a></td>\n<td></td>\n<td></td>\n<td></td>\n<td></td>\n<td></td>\n<td></td>\n<td></td>\n</tr>\n<tr>\n<td><a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#base32\">RFC 4648 Base32</a></td>\n<td></td>\n<td></td>\n<td></td>\n<td></td>\n<td></td>\n<td><span aria-label=\"cross mark\" role=\"img\" title=\"cross mark\">❌</span></td>\n<td><span aria-label=\"cross mark\" role=\"img\" title=\"cross mark\">❌</span></td>\n</tr>\n<tr>\n<td><a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#crockford-s-base32\">Crockford’s Base32</a></td>\n<td></td>\n<td></td>\n<td></td>\n<td></td>\n<td><span aria-label=\"cross mark\" role=\"img\" title=\"cross mark\">❌</span></td>\n<td></td>\n<td><span aria-label=\"cross mark\" role=\"img\" title=\"cross mark\">❌</span></td>\n</tr>\n<tr>\n<td><a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#base32h\">Base32H</a></td>\n<td></td>\n<td></td>\n<td><span aria-label=\"cross mark\" role=\"img\" title=\"cross mark\">❌</span></td>\n<td></td>\n<td><span aria-label=\"cross mark\" role=\"img\" title=\"cross mark\">❌</span></td>\n<td></td>\n<td></td>\n</tr>\n<tr>\n<td><a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#word-safe-base32\">“Word-safe” Base32</a></td>\n<td></td>\n<td></td>\n<td></td>\n<td></td>\n<td></td>\n<td></td>\n<td></td>\n</tr>\n</tbody>\n</table>\n</div>\n<p>Each column represents a character pairing, like <code>0</code> and <code>O</code>/<code>o</code>, and each row represents a particular system. If a cell contains <span aria-label=\"cross mark\" role=\"img\" title=\"cross mark\">❌</span>, that row’s system distinguishes between characters in the first group from characters in the second group.  A row with many <span aria-label=\"cross mark\" role=\"img\" title=\"cross mark\">❌</span>s is more likely to generate confusing outputs (depending upon context like how the output is presented, the font face, the size, and how the media wears over time).</p>\n<h2 id=\"output-characters\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#output-characters\">Output Characters</a></h2>\n<div class=\"horizontally-scrolling\">\n<table>\n<thead>\n<tr>\n<th>System</th>\n<th></th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#decimal\">Decimal</a></td>\n<td><code>0</code> <code>1</code> <code>2</code> <code>4</code> <code>5</code> <code>6</code> <code>7</code> <code>8</code> <code>9</code></td>\n</tr>\n<tr>\n<td><a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#hexadecimal\">Hexadecimal</a></td>\n<td><code>0</code> <code>1</code> <code>2</code> <code>4</code> <code>5</code> <code>6</code> <code>7</code> <code>8</code> <code>9</code> <code>A</code> <code>B</code> <code>C</code> <code>D</code> <code>E</code> <code>F</code></td>\n</tr>\n<tr>\n<td><a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#base64\">RFC 4648 Base64</a></td>\n<td><code>0</code> <code>1</code> <code>2</code> <code>4</code> <code>5</code> <code>6</code> <code>7</code> <code>8</code> <code>9</code> <code>A</code> <code>a</code> <code>B</code> <code>b</code> <code>C</code> <code>c</code> <code>D</code> <code>d</code> <code>E</code> <code>e</code> <code>F</code> <code>f</code> <code>G</code> <code>g</code> <code>H</code> <code>h</code> <code>I</code> <code>i</code> <code>J</code> <code>j</code> <code>K</code> <code>k</code> <code>L</code> <code>l</code> <code>M</code> <code>m</code> <code>N</code> <code>n</code> <code>O</code> <code>o</code> <code>P</code> <code>p</code> <code>Q</code> <code>q</code> <code>R</code> <code>r</code> <code>S</code> <code>s</code> <code>T</code> <code>t</code> <code>U</code> <code>u</code> <code>V</code> <code>v</code> <code>W</code> <code>w</code> <code>X</code> <code>x</code> <code>Y</code> <code>y</code> <code>Z</code> <code>z</code> <code>+</code> <code>/</code> (<code>=</code>)</td>\n</tr>\n<tr>\n<td><a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#base64\">Base64url</a></td>\n<td><code>0</code> <code>1</code> <code>2</code> <code>4</code> <code>5</code> <code>6</code> <code>7</code> <code>8</code> <code>9</code> <code>A</code> <code>a</code> <code>B</code> <code>b</code> <code>C</code> <code>c</code> <code>D</code> <code>d</code> <code>E</code> <code>e</code> <code>F</code> <code>f</code> <code>G</code> <code>g</code> <code>H</code> <code>h</code> <code>I</code> <code>i</code> <code>J</code> <code>j</code> <code>K</code> <code>k</code> <code>L</code> <code>l</code> <code>M</code> <code>m</code> <code>N</code> <code>n</code> <code>O</code> <code>o</code> <code>P</code> <code>p</code> <code>Q</code> <code>q</code> <code>R</code> <code>r</code> <code>S</code> <code>s</code> <code>T</code> <code>t</code> <code>U</code> <code>u</code> <code>V</code> <code>v</code> <code>W</code> <code>w</code> <code>X</code> <code>x</code> <code>Y</code> <code>y</code> <code>Z</code> <code>z</code> <code>-</code> <code>_</code> (<code>=</code>)</td>\n</tr>\n<tr>\n<td><a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#base32\">RFC 4648 Base32</a></td>\n<td><code>2</code> <code>4</code> <code>5</code> <code>6</code> <code>7</code> <code>A</code> <code>B</code> <code>C</code> <code>D</code> <code>E</code> <code>F</code> <code>G</code> <code>H</code> <code>J</code> <code>K</code> <code>M</code> <code>N</code> <code>P</code> <code>Q</code> <code>R</code> <code>S</code> <code>T</code> <code>V</code> <code>W</code> <code>X</code> <code>Y</code> <code>Z</code> (<code>=</code>)</td>\n</tr>\n<tr>\n<td><a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#base32\">Base32hex</a></td>\n<td><code>0</code> <code>1</code> <code>2</code> <code>4</code> <code>5</code> <code>6</code> <code>7</code> <code>8</code> <code>9</code> <code>A</code> <code>B</code> <code>C</code> <code>D</code> <code>E</code> <code>F</code> <code>G</code> <code>H</code> <code>J</code> <code>K</code> <code>L</code> <code>M</code> <code>N</code> <code>P</code> <code>Q</code> <code>R</code> <code>T</code> <code>V</code> (<code>=</code>)</td>\n</tr>\n<tr>\n<td><a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#crockford-s-base32\">Crockford’s Base32</a></td>\n<td><code>0</code> <code>1</code> <code>2</code> <code>4</code> <code>5</code> <code>6</code> <code>7</code> <code>8</code> <code>9</code> <code>A</code> <code>B</code> <code>C</code> <code>D</code> <code>E</code> <code>F</code> <code>G</code> <code>H</code> <code>J</code> <code>K</code> <code>M</code> <code>N</code> <code>P</code> <code>Q</code> <code>R</code> <code>S</code> <code>T</code> <code>V</code> <code>W</code> <code>X</code> <code>Y</code> <code>Z</code> (<code>*</code> <code>~</code> <code>$</code> <code>=</code> <code>U</code>)</td>\n</tr>\n<tr>\n<td><a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#base32h\">Base32H</a></td>\n<td><code>0</code> <code>1</code> <code>2</code> <code>4</code> <code>5</code> <code>6</code> <code>7</code> <code>8</code> <code>9</code> <code>A</code> <code>B</code> <code>C</code> <code>D</code> <code>E</code> <code>F</code> <code>G</code> <code>H</code> <code>J</code> <code>K</code> <code>L</code> <code>M</code> <code>N</code> <code>P</code> <code>Q</code> <code>R</code> <code>T</code> <code>V</code> <code>W</code> <code>X</code> <code>Y</code> <code>Z</code></td>\n</tr>\n<tr>\n<td><a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#base20-and-open-location-codes\">Open Location Code Base 20</a></td>\n<td><code>2</code> <code>4</code> <code>5</code> <code>6</code> <code>7</code> <code>8</code> <code>9</code> <code>C</code> <code>F</code> <code>G</code> <code>H</code> <code>J</code> <code>M</code> <code>P</code> <code>Q</code> <code>R</code> <code>V</code> <code>W</code> <code>X</code></td>\n</tr>\n<tr>\n<td><a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#word-safe-base32\">“Word-safe” Base32</a></td>\n<td><code>2</code> <code>4</code> <code>5</code> <code>6</code> <code>7</code> <code>8</code> <code>9</code> <code>C</code> <code>c</code> <code>F</code> <code>f</code> <code>G</code> <code>g</code> <code>H</code> <code>h</code> <code>J</code> <code>j</code> <code>M</code> <code>m</code> <code>P</code> <code>p</code> <code>Q</code> <code>q</code> <code>R</code> <code>r</code> <code>V</code> <code>v</code> <code>W</code> <code>w</code> <code>X</code> <code>x</code></td>\n</tr>\n</tbody>\n</table>\n</div>\n<h2 id=\"conclusions\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/friendly-binary-to-text-encodings/#conclusions\">Conclusions</a></h2>\n<p>Remember:</p>\n<ul>\n<li>This wasn’t an exhaustive list.</li>\n<li>Your needs are unlikely to be exactly the same as mine.</li>\n<li>Make sure to review your output font while looking at the character tables.</li>\n</ul>\n<p>Both Crockford’s Base32 and Base32H satisfy my original goals reasonably well, but I’m slightly partial to Crockford’s Base32.</p>\n<p>Base32H has a lot of features that seem nice—like “every string of alphanumerics is a valid input”—but I’m not sure that I’ve needed them.  In general, I like the alphabet used in Crockford’s Base32 a little better than Base32H’s. Compared to Crockford’s Base32, Base32H drops <code>S</code> (to reduce confusion with <code>5</code>) and allows both <code>L</code> and <code>1</code>, arguing that by telling folks to always output capital letters, there’s no ambiguity between <code>L</code> and <code>1</code>.</p>\n<p>Ultimately, breaking down this problem into goals and constraints and comparing existing solutions through that lens proved enlightening.</p>\n<hr><p><a href=\"mailto:feedcomments@atomwolf.org?subject=Feed%20Reply%20for%20Friendly%20Binary-to-text%20Encodings&amp;body=(From%20feed%20entry%3A%20https%3A%2F%2Fwww.atomwolf.org%2Fposts%2Ffriendly-binary-to-text-encodings%2F)\">Reply via email</a></p>",
			"date_published": "2024-04-14T00:00:00Z"
		%}
		,
		{
			"id": "https://www.atomwolf.org/posts/goals-for-a-3d-model-viewer/",
			"url": "https://www.atomwolf.org/posts/goals-for-a-3d-model-viewer/",
			"title": "Goals for a 3D Model Viewer",
			"content_html": "<div class=\"dotted-metadata-block\">\n<p><span class=\"small-caps\">Summary</span>: I list goals for a viewer meant for showcasing 3D CAD models on the web.</p>\n<p><span class=\"small-caps\">Assumed audience</span>: folks on the web</p>\n<p><span class=\"small-caps\">Project</span>: part of <a href=\"https://www.atomwolf.org/projects/showcasing-3d-cad-models/\">Showcasing 3D CAD Models on the Web</a></p>\n<p>\n\t<span class=\"small-caps\">Tags</span>:\n\t\t<a href=\"https://www.atomwolf.org/tags/web/\" class=\"entry-tag\">web</a>, \n\t\t<a href=\"https://www.atomwolf.org/tags/computers/\" class=\"entry-tag\">computers</a>\n</p>\n<p><span class=\"small-caps\">Created</span>: <time datetime=\"2024-03-08\">08 March 2024</time></p>\n</div>\n<hr><p>My hopes and dreams for a 3D CAD model viewer may be nitpicky and particular, but they’re mine. I’m planning to write about CAD models, and I want to point to details in an interactive web viewer. I started a list of goals when I <a href=\"https://www.atomwolf.org/posts/choosing-a-3d-cad-viewer/\">decided to begin with model-viewer</a>, and I’ve added to it.</p>\n<ul>\n<li>\n<p>annotations (hotspots)</p>\n<p>I need to be able to add a caption to a model feature and to move the camera to show a specific view of an annotation.</p>\n</li>\n<li>\n<p>component visibility</p>\n<p>I need to be able to hide or show specific components of a model. For instance, I need to be able to show and hide the lid of a box.</p>\n</li>\n<li>\n<p>show dimensions</p>\n<p>I need to be able to add the overall dimensions of a model.</p>\n</li>\n<li>\n<p id=\"banana\">banana for scale</p>\n<p>This is silly, but I’d really like the ability to add a banana next to a model.<label for=\"sn-bananaforscale\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-bananaforscale\" class=\"margin-toggle\">\n<small class=\"sidenote\">Per <a href=\"https://knowyourmeme.com/memes/banana-for-scale\">Know Your Meme</a>, the earliest instance of a person posting a photo online with a banana for scale was in 2005 on the blog Rock Dog Designs. Using the Wayback Machine, I found the <a href=\"https://web.archive.org/web/20060513233537/http://www.rockdogdesigns.com/?p=14\">original post</a>. \"I don’t know how big the screen is, we’re moving and I can’t find the tape measurer. But I do have a banana. For scale. Oh wait, my husband says it’s 19inches [sic].  Oh well, I’ll leave the banana for interest. Please be interested.\"</small>\nOther folks have made an <a href=\"https://andrewsink.github.io/3D-Banana-for-Scale/\">STL viewer with a banana for scale</a>, and you can add a <a href=\"https://github.com/arturo182/kicad-banana\">banana for scale in KiCad</a>.</p>\n</li>\n</ul>\n<ul>\n<li>\n<p>Augmented Reality (AR) / Virtual Reality (VR)</p>\n<p>It’d be nice if it were easy to use a mobile device to see what a model looks like on a desk or in a visitor’s particular environment, and it’d be nice if it were easy to use a VR headset to interact with the model.</p>\n</li>\n<li>\n<p>rendering the models I want to use well</p>\n<p>There’s a lot I don’t know about 3D rendering, and a viewer that does great rendering shoes or models from video games might not automatically do well rendering the CAD models I want to show.</p>\n</li>\n<li>\n<p>open source</p>\n<p>I wrote more about why I want to use an open-source viewer in “<a href=\"https://www.atomwolf.org/posts/choosing-a-3d-cad-viewer/\">Choosing a 3D CAD viewer</a>”, but one reason is that I need to be able to modify the viewer.</p>\n</li>\n<li>\n<p>not reliant on centralized service or hosting</p>\n<p>I want to be able to host my own models and the viewer itself. If the viewer changes, I don’t want to be forced to quickly update my site. I wrote more about this in “<a href=\"https://www.atomwolf.org/posts/choosing-a-3d-cad-viewer/\">Choosing a 3D CAD viewer</a>”.</p>\n</li>\n<li>\n<p>quick to load</p>\n<p>Adding the viewer to a page shouldn’t make the page slower in a way that people can tell. Real-world web performance is more complicated than <a href=\"https://developer.chrome.com/docs/lighthouse/overview\">Lighthouse</a> scores, but the viewer should still allow perfect Lighthouse scores on mobile and desktop.</p>\n</li>\n<li>\n<p>low page weight (data)</p>\n<p>If a visitor doesn’t interact with the viewer, the browser shouldn’t need to download much extra data.</p>\n</li>\n<li>\n<p>accessibility</p>\n<p>This isn’t a complete list, but it should work well for folks without JavaScript, with vision impairments or with a screen reader, with older browsers, with slower connections, with limited or metered data, and even to folks who like printing things.</p>\n</li>\n<li>\n<p>support progressive enhancement</p>\n<p>Adding a 3D viewer to a page shouldn’t break the page for folks who can’t use the viewer. One option has the an image with good alt text and a caption, and the interactive viewer seamlessly replaces the image. If the viewer doesn’t load, the visitor still gets the image.</p>\n</li>\n</ul>\n<h1 id=\"next-steps\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/goals-for-a-3d-model-viewer/#next-steps\">Next Steps</a></h1>\n<p>Next, I’m going to load an example CAD model into model-viewer and <a href=\"https://www.atomwolf.org/posts/experimenting-with-cad-models-in-model-viewer/\">see how it fares</a>.</p>\n<hr><p><a href=\"mailto:feedcomments@atomwolf.org?subject=Feed%20Reply%20for%20Goals%20for%20a%203D%20Model%20Viewer&amp;body=(From%20feed%20entry%3A%20https%3A%2F%2Fwww.atomwolf.org%2Fposts%2Fgoals-for-a-3d-model-viewer%2F)\">Reply via email</a></p>",
			"date_published": "2024-03-08T00:00:00Z"
		%}
		,
		{
			"id": "https://www.atomwolf.org/posts/choosing-a-3d-cad-viewer/",
			"url": "https://www.atomwolf.org/posts/choosing-a-3d-cad-viewer/",
			"title": "Choosing a 3D CAD Viewer",
			"content_html": "<div class=\"dotted-metadata-block\">\n<p><span class=\"small-caps\">Summary</span>: I investigate 3D model viewers for the web, including <a href=\"https://modelviewer.dev/\">model-viewer</a>, <a href=\"https://sketchfab.com/\">Sketchfab</a>, <a href=\"https://naver.github.io/egjs-view3d/\">View3D</a>, and <a href=\"https://threepipe.org/\">ThreePipe</a>, to showcase CAD models on <span class=\"inline-site-name\">ATOM<wbr>WOLF</span>. I decide to start with <a href=\"https://modelviewer.dev/\">model-viewer</a>.</p>\n<p><span class=\"small-caps\">Project</span>: part of <a href=\"https://www.atomwolf.org/projects/showcasing-3d-cad-models/\">Showcasing 3D CAD Models on the Web</a></p>\n<p><span class=\"small-caps\">Assumed audience</span>: folks on the web</p>\n<p>\n\t<span class=\"small-caps\">Tags</span>:\n\t\t<a href=\"https://www.atomwolf.org/tags/web/\" class=\"entry-tag\">web</a>, \n\t\t<a href=\"https://www.atomwolf.org/tags/computers/\" class=\"entry-tag\">computers</a>\n</p>\n<p><span class=\"small-caps\">Created</span>: <time datetime=\"2024-03-06\">06 March 2024</time></p>\n</div>\n<hr><p>We’ve been putting 3D models on the web since <a href=\"https://en.wikipedia.org/wiki/VRML#Emergence,_popularity,_and_rival_technical_upgrade\">1994</a>, but doing it well is tricky. The more I learn, the more complicated it seems.<label for=\"sn-mithelamodelviewer\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-mithelamodelviewer\" class=\"margin-toggle\">\n<small class=\"sidenote\"><a href=\"https://mitxela.com\">mitxela</a> built their own viewer and wrote a <a href=\"https://mitxela.com/projects/model-viewer\">good writeup</a>. Among other things, it reveals a glimpse of the complexity of camera control.</small>\nI want to write articles showcasing CAD models in an interactive web viewer. I looked toward existing options for inspiration.</p>\n<h2 id=\"sketchfab\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/choosing-a-3d-cad-viewer/#sketchfab\">Sketchfab</a></h2>\n<p>The first 3D viewer I found was Sketchfab. Sketchfab says it’s the “best <a href=\"https://sketchfab.com/3d-viewer\">3D viewer</a> on the web”. I don’t know about that, but I do like the annotations.</p>\n<figure>\n<a href=\"https://www.atomwolf.org/posts/choosing-a-3d-cad-viewer/images/sketchfab-annotation/\" data-lightbox=\"true\">\n<picture><source type=\"image/avif\" srcset=\"https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-256w.avif 256w, https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-410w.avif 410w, https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-512w.avif 512w, https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-650w.avif 650w, https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-850w.avif 850w, https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-1075w.avif 1075w, https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-1206w.avif 1206w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/webp\" srcset=\"https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-256w.webp 256w, https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-410w.webp 410w, https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-512w.webp 512w, https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-650w.webp 650w, https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-850w.webp 850w, https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-1075w.webp 1075w, https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-1206w.webp 1206w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/png\" srcset=\"https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-256w.png 256w, https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-410w.png 410w, https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-512w.png 512w, https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-650w.png 650w, https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-850w.png 850w, https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-1075w.png 1075w, https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-1206w.png 1206w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><img alt=\"A 3D model of a bust of Nefertiti, marked with a number and a floating box with text above an image with sunglasses. The text reads &quot;We can also add images and GIFs using Markdown. The one below is being pulled in from giphy.com.&quot;\" loading=\"eager\" src=\"https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-256w.jpeg\" width=\"1206\" height=\"1408\" srcset=\"https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-256w.jpeg 256w, https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-410w.jpeg 410w, https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-512w.jpeg 512w, https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-650w.jpeg 650w, https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-850w.jpeg 850w, https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-1075w.jpeg 1075w, https://www.atomwolf.org/img/sketchfab-annotation-i6h7zmiuoB-1206w.jpeg 1206w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"></picture></a>\n<figcaption>\n<p>A model annotation from the <a href=\"https://sketchfab.com/3d-models/sketchfab-annotations-demo-a81a3dbe18784734bc11b2725cc4044\">Sketchfab Annotation Demo</a></p>\n</figcaption>\n</figure>\n<p>If I were to use Sketchfab, Sketchfab would host my 3D models. I’d add an <code>iframe</code> with a particular Sketchfab URL to a page, and web browsers would create a page-within-a-page to show the viewer. Sketchfab has a JavaScript <a href=\"https://sketchfab.com/developers/viewer\">API</a> that lets developers interact with the viewer in the <code>iframe</code> to develop some custom features.</p>\n<p>Sketchfab’s viewer is hosted by Sketchfab, and my site would just refer to it. If Sketchfab changed their viewer, the viewers on my pages would change, whether I wanted it or not. If Sketchfab had an outage, my pages would be broken. If Sketchfab went out of business, I’d have to scramble to replace them. If the Sketchfab viewer were open source or if I could host it, I’d be less concerned.<label for=\"sn-emergencies\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-emergencies\" class=\"margin-toggle\">\n<small class=\"sidenote\">I've found that relying on this type of service tends to create emergencies for me. The more services like this I use, the more frequently my work is driven by upstream changes. Having something I host, something I update, helps reduce those emergencies. Being able to use a previous version is really helpful!</small>\nFurthermore, my ability to modify the viewer is limited.</p>\n<p>For the features I’d like, Sketchfab seems expensive. I think Sketchfab would cost me at least $129 per month.</p>\n<p>I don’t think Sketchfab is a good fit for a 3D viewer for <span class=\"inline-site-name\">ATOM<wbr>WOLF</span>, but it may be a good fit for others. If I were advising a business that sold 3D models needed to get a 3D viewer out ASAP, I’d check out Sketchfab.</p>\n<h2 id=\"autodesk-viewer\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/choosing-a-3d-cad-viewer/#autodesk-viewer\">Autodesk Viewer</a></h2>\n<p>Autodesk has a <a href=\"https://viewer.autodesk.com/\">web viewer</a>. I can save my Fusion 360 projects to their storage and look at them in the browser. The viewer displays the model to look just like it does in Fusion 360. You can rotate and zoom, measure parts of the model, run saved animations, and show and hide parts of the model. I can make the project public and embed the viewer into a page.</p>\n<p>The Autodesk viewer wouldn’t cost me anything extra, but my other concerns about Sketchfab apply here.</p>\n<h2 id=\"goals\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/choosing-a-3d-cad-viewer/#goals\">Goals</a></h2>\n<p>After Sketchfab and the Autodesk viewer, I had a better sense of what I wanted.</p>\n<ul>\n<li>\n<p>I don’t want to use a viewer that’s an <code>iframe</code> to an external site or one that only shows models hosted at an external site. I want to be able to host the models and the viewer.</p>\n</li>\n<li>\n<p>I want to be able to modify the viewer. It should be open source.</p>\n</li>\n<li>\n<p>I need to add text to call out model features to discuss design decisions. These are called “annotations” or “hotspots”.</p>\n</li>\n</ul>\n<p>After surveying more viewer options, I grouped them into categories.</p>\n<h2 id=\"framework-viewers\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/choosing-a-3d-cad-viewer/#framework-viewers\">Framework viewers</a></h2>\n<p>Framework viewers are more like libraries or examples than applications. They handle many complexities but would require development work to integrate into a site.</p>\n<ul>\n<li>\n<p><a href=\"https://threejs.org/\">Three.js</a></p>\n<p><a href=\"https://threejs.org/\">Three.js</a> is an open-source JavaScript library for 3D graphics in the browser. It uses WebGL, and it can use WebXR to do <a href=\"https://threejs.org/docs/#manual/en/introduction/How-to-create-VR-content\">Virtual Reality</a> (VR) things. It has <a href=\"https://threejs.org/docs/index.html\">good docs</a>, a <a href=\"https://threejs.org/examples/\">bunch of examples</a>, and it’s been around since 2010. It’s pretty awesome. Most of the other options on this page are either built on three.js or built on things built on three.js.</p>\n<p>I’m not sure three.js has a viewer.  They have an <a href=\"https://threejs.org/editor/\">editor</a> and <a href=\"https://threejs.org/examples/?q=loader\">many loader examples</a>. I’d definitely have to build a lot of the features I want.</p>\n</li>\n<li>\n<p><a href=\"https://aframe.io/\">A-Frame</a>’s <a href=\"https://aframe.io/aframe/examples/showcase/model-viewer/\">model viewer</a></p>\n<p><a href=\"https://aframe.io/\">A-Frame</a> is a fun web framework built on top of three.js, meant for VR. My first VR development experience used A-Frame. I made a demo that played videos and used a controller for 3D space data logging. After getting through some conceptual hoops around VR development, using A-Frame was really quick. I liked it, but I don’t think I’d use A-Frame for this project.</p>\n</li>\n<li>\n<p><a href=\"https://www.babylonjs.com/\">Babylon.js</a>’s <a href=\"https://doc.babylonjs.com/features/featuresDeepDive/babylonViewer\">viewer</a> (<a href=\"https://codepen.io/BabylonJS/pen/QxzBPd/\">example</a>)</p>\n<p><a href=\"https://www.babylonjs.com/\">Babylon.js</a> is similar to three.js, but the library is a bit less graphics-y and a little more “game engine-y”. The Babylon.js model viewer has a lot of features: animations, asynchronous loading of models, the <a href=\"https://doc.babylonjs.com/toolsAndResources/inspector\">Babylon.js Inspector</a>. I didn’t see annotations, and the interface would need to be modified.</p>\n</li>\n</ul>\n<h2 id=\"viewer-applications\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/choosing-a-3d-cad-viewer/#viewer-applications\">Viewer “applications”</a></h2>\n<p>These viewers are meant to be used as-is. They vary widely, and I gave some only a glance.</p>\n<ul>\n<li>\n<p>Don McCurdy’s <a href=\"https://gltf-viewer.donmccurdy.com/\">three-gltf-viewer</a> (<a href=\"https://github.com/donmccurdy/three-gltf-viewer\">GitHub</a>)</p>\n<p>This viewer is built on three.js. It supports animations but doesn’t appear to support annotations or VR/AR/WebXR.</p>\n</li>\n<li>\n<p>kovacsv’s <a href=\"https://3dviewer.net/\">Online3DViewer</a> (<a href=\"https://github.com/kovacsv/Online3DViewer\">GitHub</a>)</p>\n<p>I don’t know a lot about kovacsv’s Online3DViewer.  It’s open source and uses three.js.  The viewer is embedded using an iframe.  I don’t think it has annotations or VR/AR/WebXR support.</p>\n</li>\n<li>\n<p><a href=\"https://naver.github.io/egjs-view3d/\">View3D</a> (<a href=\"https://github.com/naver/egjs-view3d\">GitHub</a>)<br>\nView3D is built on three.js. It’s open source. It supports annotations and animations. It supports augmented reality through WebXR, Android’s Scene Viewer, and iOS’s AR Quick Look. It appears to be maintained by a South Korean company called <a href=\"https://en.wikipedia.org/wiki/Naver\">Naver</a>.</p>\n</li>\n<li>\n<p><a href=\"https://threepipe.org/\">ThreePipe</a> (<a href=\"https://github.com/repalash/threepipe\">GitHub</a>))</p>\n<p>ThreePipe is built on three.js. ThreePipe says it has a “simple, intuitive API for creating 3D model viewers/configurators/editors on web pages, with many built-in presets for common workflows and use-cases.” It has plugins, including one for <a href=\"https://github.com/repalash/threepipe?tab=readme-ov-file#threepipeplugin-gaussian-splatting\">Gaussian splats</a>.<label for=\"sn-splats\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-splats\" class=\"margin-toggle\">\n<small class=\"sidenote\"><a href=\"https://en.wikipedia.org/wiki/Gaussian_splatting\">Gaussian splats</a> don't seem helpful for this project, but they're interesting!</small>\nIt doesn’t appear to support WebXR/AR/VR yet, and it doesn’t appear to support annotations.</p>\n</li>\n</ul>\n<ul>\n<li>\n<p><a href=\"https://modelviewer.dev/\">model-viewer</a> (<a href=\"https://github.com/google/model-viewer\">GitHub</a>, <a href=\"https://modelviewer.dev/docs/\">docs</a>)</p>\n<p>model-viewer is a Google project. It’s open source and built on three.js. It’s got a lot of <a href=\"https://modelviewer.dev/examples/loading/\">examples</a>. It supports <a href=\"https://modelviewer.dev/examples/annotations/index.html\">annotations</a>, and <a href=\"https://modelviewer.dev/examples/augmentedreality/\">AR</a> through <a href=\"https://modelviewer.dev/docs/faq.html#entrydocs-general-questions-ar\">WebXR, AR Quick Look, and Scene Viewer</a>. VR support seems to be in progress. It has an <a href=\"https://modelviewer.dev/examples/annotations/index.html#dimensions\">example showing model dimensions</a>. There is good integration between the 3D viewer and the rest of the webpage. It feels “in the spirit of the web”.</p>\n<p>model-viewer has a lot of polish. It can show an “<a href=\"https://modelviewer.dev/docs/index.html#entrydocs-stagingandcameras-attributes-interactionPrompt\">interaction prompt</a>” animation on the model, giving visitors an affordance that the model isn’t a static image. model-viewer has an <a href=\"https://modelviewer.dev/editor/\">editor</a> to help with configuration. The model-viewer folks care about and track [page load performance].(<a href=\"https://modelviewer.dev/examples/lighthouse.html\">https://modelviewer.dev/examples/lighthouse.html</a>)<label for=\"sn-pageload\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-pageload\" class=\"margin-toggle\">\n<small class=\"sidenote\">Asynchronously loading model-viewer decreases its impact on page load performance, but model-viewer is still 60+ KB. <a href=\"https://lite-model-viewer.netlify.app/\">Lite model-viewer</a> is an easy-to-use third-party wrapper that defaults to only loading model-viewer after the visitor interacts with the model image. For a visitor that isn't going to interact with the model, using lite-model-viewer means the page size is only around 3 KB larger than just showing the image!</small>\nmodel-viewer starts with a pre-rendered image of the scene and seamlessly replaces it with the interactive model when the model file loads. This is “in the spirit of the web” and heavily influenced my decision to start with model-viewer.</p>\n<p>Another way model-viewer aligns with the web is that it’s a Web Component. You don’t need to write JavaScript to put a model on your page. You can add a <code>&lt;model-viewer&gt;</code> tag. Awesome!</p>\n<p>model-viewer is open to modifications. <a href=\"https://modelviewer.dev/examples/postprocessing/index.html\">model-viewer-effects</a> provides postprocessing effects, like pixelation, glitching, and antialiasing. Additionally, I could <a href=\"https://modelviewer.dev/docs/faq.html#entrydocs-general-questions-three\">hook into three.js and modify things how I like</a>.</p>\n</li>\n</ul>\n<h2 id=\"conclusion\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/choosing-a-3d-cad-viewer/#conclusion\">Conclusion</a></h2>\n<p>I’m going to start with model-viewer, but not without reservation.</p>\n<p>model-viewer is maintained by folks from Google. While I’m cautious of <a href=\"https://killedbygoogle.com/\">Google’s history of discontinuing projects</a>, the open-source nature of model-viewer gives me reassurance. When Google decides it’s done with model-viewer, the community might maintain it, but I could continue to use the last version while migrating to another option, like <a href=\"https://naver.github.io/egjs-view3d/\">View3D</a> or <a href=\"https://threepipe.org/\">ThreePipe</a>.</p>\n<p>My next step is to <a href=\"https://www.atomwolf.org/posts/goals-for-a-3d-model-viewer\">discover the rest of my finicky requirements</a> as I integrate model-viewer into <span class=\"inline-site-name\">ATOM<wbr>WOLF</span>.</p>\n<hr><p><a href=\"mailto:feedcomments@atomwolf.org?subject=Feed%20Reply%20for%20Choosing%20a%203D%20CAD%20Viewer&amp;body=(From%20feed%20entry%3A%20https%3A%2F%2Fwww.atomwolf.org%2Fposts%2Fchoosing-a-3d-cad-viewer%2F)\">Reply via email</a></p>",
			"date_published": "2024-03-06T00:00:00Z"
		%}
		,
		{
			"id": "https://www.atomwolf.org/posts/git-absorb/",
			"url": "https://www.atomwolf.org/posts/git-absorb/",
			"title": "Update logical commits with `git absorb`",
			"content_html": "<div class=\"dotted-metadata-block\">\n<p><span class=\"small-caps\">Summary</span>: <a href=\"https://github.com/tummychow/git-absorb\">git absorb</a> helps you update a series of git commits.</p>\n<p><span class=\"small-caps\">Assumed audience</span>: folks who use <a href=\"https://www.atomwolf.org/tags/git/\">git</a></p>\n<p><a class=\"small-caps\" href=\"https://www.atomwolf.org/about/pages/\">Type</a>: tip</p>\n</div>\n<hr><p>I sometimes find myself in a particular situation with git.  I have a small series of logical commits in a pull request.  I need to make some edits.</p>\n<p>I could always make the edits on top of the logical commits and push the new commits, but usually, I’d rather edit the logical commits.</p>\n<p>My usual workflow for something like this used to be:</p>\n<ul>\n<li>Make some changes.</li>\n<li>If there’s only one commit, or my changes are all for the last commit, make the changes, and commit with <code>git commit --amend</code>.</li>\n<li>Use <code>git add -p</code> and create a new commit only containing the edits for a single commit. Commit with <code>git commit --fixup fa1afe1</code>.<label for=\"sn-shortsha\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-shortsha\" class=\"margin-toggle\">\n<small class=\"sidenote\">You don't have to use the short SHA-1.  There are <a href=\"https://git-scm.com/book/en/v2/Git-Tools-Revision-Selection\">lots of ways to name a commit</a>, like <code>HEAD~3</code> or <code>:/Add some bugs</code>. <a href=\"https://git-scm.com/docs/git-rev-parse#Documentation/git-rev-parse.txt-emlttextgtemegemfixnastybugem\"><code>:/&lt;regex&gt;</code></a> returns the youngest matching commit.  Some folks like it.</small>\nWith <code>--fixup</code>, git sets the commit message for the new commit to something like <code>fixup! fa1afe1: &lt;original commit message&gt;</code>. Git will use the commit message later to be helpful.<label for=\"sn-commitmsg\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-commitmsg\" class=\"margin-toggle\">\n<small class=\"sidenote\">If the original commit message needs to be changed, I'd probably commit with <code>--squash</code> and add a reminder in the new commit's commit message.  The rebase step will change this commit's action to \"squash\" instead of \"fixup\".  In an interactive rebase, \"squash\" opens an editor with the original commit message on top and the commit message for the to-be-squashed commit on the bottom, and has you edit the message before continuing with the rebase.  (I see there's <a href=\"https://git-scm.com/docs/git-commit/2.43.0#Documentation/git-commit.txt---fixupamendrewordltcommitgt\"><code>git commit --fixup:amend</code></a> now, which looks to be another way to edit the commit message during the rebase.)</small>\n</li>\n<li>Keep running <code>git add -p</code> and committing until there are no more changes left.</li>\n<li>Run <code>git rebase -i --autosquash main</code>.<label for=\"sn-autosquash\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-autosquash\" class=\"margin-toggle\">\n<small class=\"sidenote\">I don't really put <code>--autosquash</code> in the command.  I set <a href=\"https://git-scm.com/docs/git-config#Documentation/git-config.txt-rebaseautoSquash\"><code>rebase.autoSquash</code></a> in my git config.</small>\n. It opens up the editor just like <code>git rebase -i</code>. However, because of autosquash, those <code>fixup! fa1afe1:</code> commits we just made have their action set to “fixup” and they’re reordered to be right after the commits they refer to. I take a quick look, save, and exit.  Git does the rebase, and my local branch is updated.</li>\n<li>Push these changes with <code>git push --force</code>, updating the remote branch.</li>\n</ul>\n<h2 id=\"autosquash\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/git-absorb/#autosquash\">Autosquash</a></h2>\n<p>Autosquash is pretty helpful.  When you rebase, those fixup and squash commits will be reordered and have the appropriate action based on the commit message.</p>\n<figure>\n<blockquote cite=\"https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt---autosquash\">\n<p><code>--autosquash</code><br>\n<code>--no-autosquash</code><br>\nWhen the commit log message begins with “squash! …​” or “fixup! …​” or “amend! …​”, and there is already a commit in the todo list that matches the same <code>...</code>, automatically modify the todo list of <code>rebase -i</code>, so that the commit marked for squashing comes right after the commit to be modified, and change the action of the moved commit from <code>pick</code> to <code>squash</code> or <code>fixup</code> or <code>fixup -C</code> respectively. A commit matches the <code>...</code> if the commit subject matches, or if the <code>...</code> refers to the commit’s hash. As a fall-back, partial matches of the commit subject work, too. The recommended way to create fixup/amend/squash commits is by using the <code>--fixup</code>, <code>--fixup=amend:</code> or <code>--fixup=reword:</code> and <code>--squash</code> options respectively of <a href=\"https://git-scm.com/docs/git-commit\">git-commit</a>.</p>\n</blockquote>\n<figcaption>— <a href=\"https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt---autosquash\">git-rebase</a> v2.43.0, 2024-01-09</figcaption>\n</figure>\n<p>As helpful as autosquash is, it’s relatively transparent.  If you made a regular commit, then did an interactive rebase, reordered that regular commit to be underneath the one you wanted to fixup, and changed the action from “pick” to “fixup”, you end up at the same place.  These special commands get you some automatic human-readable information in the temporary commit, and the rebase part reads that commit message to do a bit of text editing for you.</p>\n<p>I didn’t terribly mind the workflow (<code>git add -p</code>, <code>git commit --fixup</code>, <code>git add -p</code> …, <code>git rebase -i</code>).<label for=\"sn-gitaddp\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-gitaddp\" class=\"margin-toggle\">\n<small class=\"sidenote\">I do find <code>git add -p</code> over and over to be tedious and have long wished for a way to do everything <code>git add -p</code> does, but with deleting/undoing changes and sorting things into multiple changesets in one go.</small>\nI stumbled upon <code>git absorb</code>, though, and have found it even nicer.</p>\n<h2 id=\"git-absorb\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/git-absorb/#git-absorb\">git absorb</a></h2>\n<p>Enter <a href=\"https://github.com/tummychow/git-absorb\">git absorb</a>, a snazzy git plugin. After you install it:</p>\n<ul>\n<li>Make some changes.</li>\n<li>Stage the changes with <code>git add -p</code>.</li>\n<li>Run <code>git absorb</code>.  It checks if the hunks you’ve staged match or are inside hunks you changed in previous commits, and puts those into fixup commits, leaving anything that remains in the staging area.</li>\n<li>Run <code>git rebase -i --autosquash main</code>.</li>\n</ul>\n<p>If you don’t want to review the changes during the interactive rebase, you can <code>git absorb --and-rebase</code>.</p>\n<hr><p><a href=\"mailto:feedcomments@atomwolf.org?subject=Feed%20Reply%20for%20Update%20logical%20commits%20with%20%60git%20absorb%60&amp;body=(From%20feed%20entry%3A%20https%3A%2F%2Fwww.atomwolf.org%2Fposts%2Fgit-absorb%2F)\">Reply via email</a></p>",
			"date_published": "2024-01-09T00:00:00Z"
		%}
		,
		{
			"id": "https://www.atomwolf.org/posts/bouncing-dvd-logo/",
			"url": "https://www.atomwolf.org/posts/bouncing-dvd-logo/",
			"title": "Introducing Bouncing DVD Logo demo",
			"content_html": "<p>I added a fun MakeCode Arcade demo <a href=\"https://www.atomwolf.org/projects/bouncing-dvd-logo/\">Bouncing DVD Logo</a> to <a href=\"https://www.atomwolf.org/projects/\">Projects</a>.</p>\n<figure>\n<a href=\"https://www.atomwolf.org/posts/bouncing-dvd-logo/images/bouncing-dvd-logo/\" data-lightbox=\"true\">\n<picture><source type=\"image/avif\" srcset=\"https://www.atomwolf.org/img/static-dvd-logo-kD63Mx4L0C-256w.avif 256w, https://www.atomwolf.org/img/static-dvd-logo-kD63Mx4L0C-410w.avif 410w, https://www.atomwolf.org/img/static-dvd-logo-kD63Mx4L0C-480w.avif 480w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/webp\" srcset=\"https://www.atomwolf.org/img/static-dvd-logo-kD63Mx4L0C-256w.webp 256w, https://www.atomwolf.org/img/static-dvd-logo-kD63Mx4L0C-410w.webp 410w, https://www.atomwolf.org/img/static-dvd-logo-kD63Mx4L0C-480w.webp 480w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/png\" srcset=\"https://www.atomwolf.org/img/static-dvd-logo-kD63Mx4L0C-256w.png 256w, https://www.atomwolf.org/img/static-dvd-logo-kD63Mx4L0C-410w.png 410w, https://www.atomwolf.org/img/static-dvd-logo-kD63Mx4L0C-480w.png 480w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><img alt=\"A screenshot of the bouncing DVD logo, showing a white DVD logo on a black background.\" loading=\"eager\" src=\"https://www.atomwolf.org/img/static-dvd-logo-kD63Mx4L0C-256w.jpeg\" width=\"480\" height=\"360\" srcset=\"https://www.atomwolf.org/img/static-dvd-logo-kD63Mx4L0C-256w.jpeg 256w, https://www.atomwolf.org/img/static-dvd-logo-kD63Mx4L0C-410w.jpeg 410w, https://www.atomwolf.org/img/static-dvd-logo-kD63Mx4L0C-480w.jpeg 480w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"></picture></a>\n<figcaption>\n<p><a href=\"https://makecode.com/_AsRPeTcTMfrb\">Watch the demo!</a></p>\n</figcaption>\n</figure>\n<p><a href=\"https://arcade.makecode.com/\">MakeCode Arcade</a> can be a slick way to introduce folks to game development.  You write and play games in a browser.  The programs can be loaded onto <a href=\"https://arcade.makecode.com/hardware\">little handheld devices</a> like the <a href=\"https://learn.adafruit.com/adafruit-pybadge\">Adafruit PyBadge</a>.<label for=\"sn-playoptions\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-playoptions\" class=\"margin-toggle\">\n<small class=\"sidenote\">The editor and handheld electronics aren't the only places you can run MakeCode Arcade games.  Their <a href=\"https://arcade.makecode.com/hardware/kiosk\">kiosk</a> provides a simple menu interface to select between games, perfect for something like an arcade machine.</small>\n</p>\n<p>I occasionally run MakeCode Arcade workshops for kids and teens at <a href=\"https://leonardosbasement.org/\">Leonardo’s Basement</a>, and make little demos to show off a feature or a technique.</p>\n<p>Even before I was finished, this demo put a big grin on my face.<label for=\"sn-bouncinglogo\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-bouncinglogo\" class=\"margin-toggle\">\n<small class=\"sidenote\">I was surprised the demo landed as well as it did with the students. They mentioned the episode of US version of <a href=\"https://www.youtube.com/watch?v=QOtuX0jL85Y\">The Office</a> and this (edited, unfortunately) <a href=\"https://www.youtube.com/watch?v=m8NAlDOCG6g\">clip of a bar crowd cheering on the DVD logo</a>.</small>\n</p>\n<p>Read more (and watch it, edit it, and make it your own!) at the <a href=\"https://www.atomwolf.org/projects/bouncing-dvd-logo\">Bouncing DVD Logo</a> project page.</p>\n<hr><p><a href=\"mailto:feedcomments@atomwolf.org?subject=Feed%20Reply%20for%20Introducing%20Bouncing%20DVD%20Logo%20demo&amp;body=(From%20feed%20entry%3A%20https%3A%2F%2Fwww.atomwolf.org%2Fposts%2Fbouncing-dvd-logo%2F)\">Reply via email</a></p>",
			"date_published": "2023-12-20T00:00:00Z"
		%}
		,
		{
			"id": "https://www.atomwolf.org/posts/hacking-handles-into-logseq-citations/",
			"url": "https://www.atomwolf.org/posts/hacking-handles-into-logseq-citations/",
			"title": "Hacking handles into Logseq PDF citations",
			"content_html": "<div class=\"dotted-metadata-block\">\n<p><span class=\"small-caps\">Summary</span>: Logseq PDF citations show the page number and the quotation, but not the document name.  I <a href=\"https://github.com/logseq/logseq/compare/5cab22187c72bc9c39c186efe3f591c5cb31e6df...adamwolf:logseq:pdf-citation-handles\">patched Logseq</a> to include an optional customizable document name.</p>\n<p><span class=\"small-caps\">Assumed audience</span>: <strong>not</strong> Logseq users looking for a maintained, polished plugin</p>\n<p>\n\t<span class=\"small-caps\">Tags</span>:\n\t\t<a href=\"https://www.atomwolf.org/tags/logseq/\" class=\"entry-tag\">Logseq</a>, \n\t\t<a href=\"https://www.atomwolf.org/tags/annotations/\" class=\"entry-tag\">annotations</a>, \n\t\t<a href=\"https://www.atomwolf.org/tags/citations/\" class=\"entry-tag\">citations</a>, \n\t\t<a href=\"https://www.atomwolf.org/tags/open-source/\" class=\"entry-tag\">open source</a>, \n\t\t<a href=\"https://www.atomwolf.org/tags/computers/\" class=\"entry-tag\">computers</a>\n</p>\n<p><span class=\"small-caps\">Created</span>: <time datetime=\"2023-07-21\">21 July 2023</time></p>\n</div>\n<hr><h2 id=\"background\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/hacking-handles-into-logseq-citations/#background\">Background</a></h2>\n<p>Logseq has relatively best-in-class PDF annotation support. My biggest problem was that all PDF citations<label for=\"sn-term\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-term\" class=\"margin-toggle\">\n<small class=\"sidenote\">I don't know that Logseq has a name for the annotation link. Here, I call it a \"citation\".</small>\nonly show the page number of the reference.  If you refer to multiple documents, you need to click through to tell which one refers to which document!</p>\n<h3 id=\"before\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/hacking-handles-into-logseq-citations/#before\">Before</a></h3>\n<figure>\n<a href=\"https://www.atomwolf.org/posts/hacking-handles-into-logseq-citations/images/logseq-citations-before-handles/\" data-lightbox=\"true\">\n<picture><source type=\"image/avif\" srcset=\"https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-256w.avif 256w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-410w.avif 410w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-512w.avif 512w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-650w.avif 650w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-850w.avif 850w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-1075w.avif 1075w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-1280w.avif 1280w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-1420w.avif 1420w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-1660w.avif 1660w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-1706w.avif 1706w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/webp\" srcset=\"https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-256w.webp 256w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-410w.webp 410w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-512w.webp 512w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-650w.webp 650w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-850w.webp 850w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-1075w.webp 1075w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-1280w.webp 1280w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-1420w.webp 1420w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-1660w.webp 1660w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-1706w.webp 1706w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/png\" srcset=\"https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-256w.png 256w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-410w.png 410w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-512w.png 512w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-650w.png 650w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-850w.png 850w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-1075w.png 1075w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-1280w.png 1280w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-1420w.png 1420w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-1660w.png 1660w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-1706w.png 1706w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><img alt=\"Logseq screenshot showing PDF references composed only of the letter P and the page number of the reference.\" loading=\"lazy\" decoding=\"async\" src=\"https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-256w.jpeg\" width=\"1706\" height=\"654\" srcset=\"https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-256w.jpeg 256w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-410w.jpeg 410w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-512w.jpeg 512w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-650w.jpeg 650w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-850w.jpeg 850w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-1075w.jpeg 1075w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-1280w.jpeg 1280w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-1420w.jpeg 1420w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-1660w.jpeg 1660w, https://www.atomwolf.org/img/logseq-citations-before-vYOn0YKcnN-1706w.jpeg 1706w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"></picture></a>\n<figcaption>\n<p>Each yellow circle indicates a PDF citation. The number is the page number of the PDF (a good start!) but <em>which</em> PDF?</p>\n</figcaption>\n</figure>\n<h3 id=\"after\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/hacking-handles-into-logseq-citations/#after\">After</a></h3>\n<figure>\n<a href=\"https://www.atomwolf.org/posts/hacking-handles-into-logseq-citations/images/logseq-citations-with-handles/\" data-lightbox=\"true\">\n<picture><source type=\"image/avif\" srcset=\"https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-256w.avif 256w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-410w.avif 410w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-512w.avif 512w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-650w.avif 650w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-850w.avif 850w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-1075w.avif 1075w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-1280w.avif 1280w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-1420w.avif 1420w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-1660w.avif 1660w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-1724w.avif 1724w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/webp\" srcset=\"https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-256w.webp 256w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-410w.webp 410w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-512w.webp 512w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-650w.webp 650w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-850w.webp 850w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-1075w.webp 1075w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-1280w.webp 1280w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-1420w.webp 1420w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-1660w.webp 1660w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-1724w.webp 1724w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/png\" srcset=\"https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-256w.png 256w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-410w.png 410w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-512w.png 512w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-650w.png 650w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-850w.png 850w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-1075w.png 1075w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-1280w.png 1280w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-1420w.png 1420w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-1660w.png 1660w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-1724w.png 1724w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><img alt=\"Logseq screenshot showing PDF references composed of a handle like &quot;HtN&quot; and &quot;GtN&quot;, the letter P, and the page number of the reference.\" loading=\"lazy\" decoding=\"async\" src=\"https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-256w.jpeg\" width=\"1724\" height=\"640\" srcset=\"https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-256w.jpeg 256w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-410w.jpeg 410w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-512w.jpeg 512w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-650w.jpeg 650w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-850w.jpeg 850w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-1075w.jpeg 1075w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-1280w.jpeg 1280w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-1420w.jpeg 1420w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-1660w.jpeg 1660w, https://www.atomwolf.org/img/logseq-citations-after-WCf4UnXjUm-1724w.jpeg 1724w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"></picture></a>\n<figcaption>\n<p>With a handle like “HtN” and “GtN”, I can tell which citation refers to which document.</p>\n</figcaption>\n</figure>\n<h2 id=\"asking-the-community\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/hacking-handles-into-logseq-citations/#asking-the-community\">Asking the Community</a></h2>\n\n<p>I posted in the Logseq Discord:</p>\n<blockquote>\n<p>Hiya folks! I’m looking to extend the (colored dot) P annotation handle thing.  I really want to pull in a PDF name—maybe defined as a property on the annotation page?  i.e. instead of * P240 “hello world” it may say something like * Logseq for dummies P240 “hello world”.  Is this possible?  Anyone have any tips or pointers?  I am a pretty competent programmer but haven’t done Logseq plugin-y work before.</p>\n</blockquote>\n<p>Someone named Charlie jumped in, and said:</p>\n<blockquote>\n<p>Cool! That’s a great idea! The current plugin SDK doesn’t provide an API to intercept the rendering of PDF annotation blocks. Even if it is possible to achieve through hacking, it may be complicated and have performance issues. We will consider implementing this feature natively or providing a specific plugin API as soon as possible.</p>\n</blockquote>\n<p>Phrased like a seasoned open source developer!</p>\n\n<p>Unfortunately, I have to quickly pick the underlying tool for an annotation project.  There are no clear winners, but Logseq is at the top. This is one of the main blockers—if I can hack this feature in, even in a hard-to-maintain and performance-impacting way, it may be enough to make me feel comfortable using Logseq for the project.  I hope by the time I’m done with the project, Logseq won’t have this problem anymore.</p>\n<h2 id=\"hacking\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/hacking-handles-into-logseq-citations/#hacking\">Hacking</a></h2>\n<p>I haven’t worked with Logseq’s internals before, and I don’t think I’ve worked with Clojure.  I’ve done some work in Lisp-y environments, though, so I opened up the source.  Within a few minutes, I was able to find where the citations get added (in <code>block.cljs</code>).  I could tell that the section didn’t directly have the document or document name, but it did have the document ID.  Time for hacking!</p>\n<p>I set up the development environment.</p>\n<ul>\n<li>I installed Clojure using Homebrew, checked out Logseq, ran <code>yarn</code>, and then <code>yarn watch</code>.</li>\n<li>Installing the yarn dependencies kept failing.  An error message implied the spaces in the directory path were causing a problem.  I moved the project to a different directory without spaces and rerunning the command worked.</li>\n<li>I was able to browse to <code>localhost:3001</code>, but PDF stuff didn’t seem to load right.  I had thought the PDF stuff worked on the web interface, actually, but I’m not sure if it does.  Before digging into that, I decided to try the Electron app.</li>\n<li>I built the dev electron app with <code>yarn watch</code> and waiting for it to get to a steady state, and then opening a new terminal and running <code>yarn dev-electron-app</code>.  I kept both running.  The PDF citation seemed to work like it did in my regular version of Logseq.</li>\n<li>I could make changes in the source directories, and within a few seconds, the changes occurred in the Electron window. If I ran <code>(println \"hello world\")</code> in the program, the output would show up in the browser JavaScript console.</li>\n<li>I opened the <code>logseq</code> directory with PyCharm and installed the Clojure plugin.</li>\n</ul>\n<p>This wasn’t done with any real knowledge of Clojure or Logseq internals, unfortunately, so please don’t take this as a great example of how to do Clojure work!  I saw reference to a REPL, but the Electron change loop was pretty fast, and I wasn’t sure I had the Clojure knowledge to translate things from the REPL into a plugin or patch to the source.</p>\n<p>While looking for examples of getting properties through a database lookup, I found some code in Logseq that was using debug/pprint. The advantage of using <code>pprint</code> over <code>println</code> appeared to be that <code>pprint</code> would indent the output to show the structure of the data better.</p>\n<p>I incrementally found the reference to the PDF document, got a property of that page through a database lookup, and then rendered it in the citation.</p>\n\n\n  \n  \n  <pre><code> (debug/pprint \"adamwolf: t\" t)\n (debug/pprint \"adamwolf: (:block/page t)\"\n               (:block/page t))\n (debug/pprint \"adamwolf xyzzy: (db-utils/pull (:db/id (:block/page t)))\"\n               (db-utils/pull (:db/id (:block/page t))))\n (debug/pprint \"adamwolf (db-utils/pull (:block/page t))\"\n               (:hl-handle\n                (:block/properties\n                 db-utils/pull\n                 (:db/id\n                  (:block/page t)))))</code></pre>\n<p>I added some comments to explain my intent. <label for=\"mn-diff\" class=\"margin-toggle\"></label>\n<input type=\"checkbox\" id=\"mn-diff\" class=\"margin-toggle\">\n<small class=\"marginnote\">You can see <a href=\"https://github.com/logseq/logseq/compare/5cab22187c72bc9c39c186efe3f591c5cb31e6df...adamwolf:logseq:pdf-citation-handles\">the change</a> in context.</small>\n</p>\n\n\n  \n  \n  <pre><code> ;; Get the ID of the page referred to by t's :block/page.\n ;;\n ;; Use that ID to pull the page from the database, and\n ;; only take the :block/properties map.\n ;;\n ;; Look up the :hl-handle in the :block/properties, and\n ;; bind it to awolf-hl-handle.\n ;;\n ;; If any of those things are nil or don't exist,\n ;; awolf-hl-handle is nil.\n ;;\n ;; Then, if awolf-hl-handle is not nil, render a\n ;; span.hl-handle with the handle and a space in it.\n \n (let [awolf-hl-handle\n       (:hl-handle\n        (:block/properties\n         (db-utils/pull [:block/properties]\n                        (:db/id\n                         (:block/page t)))))]\n   (when awolf-hl-handle\n     [:span.hl-handle\n      [:strong.forbid-edit (str awolf-hl-handle \" \")]]))</code></pre>\n<p>Now, if I add a property of <code>hl-handle:: foo</code> to a document page, all the citations for that PDF are formatted like <code>foo P103</code>! Great!</p>\n<h2 id=\"can-i-use-this\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/hacking-handles-into-logseq-citations/#can-i-use-this\">Can I use this?</a></h2>\n<p>This is definitely a good proof of concept.  Is it something I would want to use locally for a bit?</p>\n<ul>\n<li>\n<p>Speed and Scalability</p>\n<p>I’ve added a database lookup for every block title render of a citation, I think, and I don’t think there were any database lookups before. I don’t know Logseq enough to know if this is something to worry about. Is there a cache I should be explicitly using?  Should I pull in a copy of the handle into the highlight’s properties, and then updating all the highlights if the handle changes?  Is the lookup actually fine? <code>¯\\_(ツ)_/¯</code> Rendering speed seems fine on my largest pages on my desktop—which, for a change intended for myself, seems like a reasonable test.</p>\n</li>\n<li>\n<p>Database and Disk Format Agnosticism</p>\n<p>I want to avoid affecting how things are stored in the database or on disk. Locking myself into a custom Logseq sounds like a bad idea!</p>\n<p>I think the change only affects presentation.</p>\n</li>\n<li>\n<p>Maintenance Effort</p>\n<p>As Logseq changes, is this patch something that I could easily bring forward without a lot of work?</p>\n<p>The change is short and doesn’t touch a lot of parts of Logseq. Maybe!</p>\n</li>\n</ul>\n<h2 id=\"wrapping-up\" tabindex=\"-1\"> <a href=\"https://www.atomwolf.org/posts/hacking-handles-into-logseq-citations/#wrapping-up\">Wrapping up</a></h2>\n<p>To close the loop, I posted an update in the Discord.  I wanted to make sure that if someone came across my request in the future, they wouldn’t find a “Oh yeah, I got it working!” message with no details. I also wanted to make sure I didn’t come across expecting my quick-and-dirty change to be adopted across the whole project.</p>\n<p>I posted the following in the Discord thread.</p>\n<blockquote>\n<p>OK, so for posterity’s sake, lemme post what I did.  I added the following to <code>block.cljs</code>.</p>\n\n\n  \n  \n  <pre><code> ;; Get the ID of the page referred to by t's :block/page.\n ;; Use that ID to pull the page from the database, and only\n ;; take the :block/properties map.\n ;; Then, look up the :hl-handle in the :block/properties,\n ;; and bind it to awolf-hl-handle..\n ;; If any of those things are nil or don't exist,\n ;; awolf-hl-handle is nil.\n \n ;; Then, if awolf-hl-handle is not nil, render a\n ;; span.hl-handle with the handle and a space in it.\n (let [awolf-hl-handle (:hl-handle (:block/properties (db-utils/pull [:block/properties] (:db/id (:block/page t)))))]\n \t(when awolf-hl-handle\n \t\t[:span.hl-handle\n \t\t\t[:strong.forbid-edit (str awolf-hl-handle \" \")]]\n \t\t)\n \t)</code></pre>\n<p>This adds a db lookup for every single block render of an annotation, maybe for every block.  The code is probably an abomination.  I haven’t ever used Clojure before, and I don’t really know how Logseq works in any way. This is a little personal proof-of-concept patch.</p>\n<p>Anyway, after that, you can add an <code>hl-handle:: foo</code> property to the annotation page, and then those annotated things will show up with the handle prefix before the page number.</p>\n</blockquote>\n<p>I hope developers add a PDF citation plugin hook.  I’d like to see this feature without maintaining a fork (albeit a tiny one).  I like the document handle, but there’s room for improvement. For instance, supporting custom citation formats!  I’d like to see a chapter number, actually, even if only in exports.<label for=\"sn-citation\" class=\"margin-toggle sidenote-number\"></label>\n<input type=\"checkbox\" id=\"sn-citation\" class=\"margin-toggle\">\n<small class=\"sidenote\">Zotero, for instance, supports thousands of citation formats.  There's even a <a href=\"https://citationstyles.org/\">Citation Style Language</a> supported by multiple tools!</small>\nGiving an export to someone without the PDF becomes more useful when a citation reads “Gideon the Ninth, ch. 32, p. 409” rather than “P409”.</p>\n<hr><p><a href=\"mailto:feedcomments@atomwolf.org?subject=Feed%20Reply%20for%20Hacking%20handles%20into%20Logseq%20PDF%20citations&amp;body=(From%20feed%20entry%3A%20https%3A%2F%2Fwww.atomwolf.org%2Fposts%2Fhacking-handles-into-logseq-citations%2F)\">Reply via email</a></p>",
			"date_published": "2023-07-21T00:00:00Z"
		%}
		,
		{
			"id": "https://www.atomwolf.org/posts/chonky-benchy/",
			"url": "https://www.atomwolf.org/posts/chonky-benchy/",
			"title": "Chonky Benchy",
			"content_html": "<p>I’m working on a commissioned art piece where we’re looking for thick, exaggerated 3D printer layer lines.</p>\n<p>I bought a 1.2 mm high flow nozzle, much bigger than I’ve used before, and started tuning the settings.  For a demo piece, I used a <a href=\"https://www.3dbenchy.com/\">Benchy</a>, scaled 2x.  My first print with the nozzle was with a 0.9 mm layer height.</p>\n<figure>\n<a href=\"https://www.atomwolf.org/posts/chonky-benchy/images/chonky-benchy-in-hand/\" data-lightbox=\"true\">\n<picture><source type=\"image/avif\" srcset=\"https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-256w.avif 256w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-410w.avif 410w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-512w.avif 512w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-650w.avif 650w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-850w.avif 850w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-1075w.avif 1075w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-1280w.avif 1280w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-1420w.avif 1420w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-1660w.avif 1660w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-1860w.avif 1860w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-2048w.avif 2048w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-3629w.avif 3629w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/webp\" srcset=\"https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-256w.webp 256w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-410w.webp 410w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-512w.webp 512w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-650w.webp 650w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-850w.webp 850w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-1075w.webp 1075w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-1280w.webp 1280w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-1420w.webp 1420w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-1660w.webp 1660w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-1860w.webp 1860w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-2048w.webp 2048w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-3629w.webp 3629w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><img alt=\"A large &quot;Benchy&quot;, or 3D-printed tugboat model, in someone's hand. The print's layer lines are substantially thicker than typical.\" loading=\"eager\" src=\"https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-256w.jpeg\" width=\"3629\" height=\"2121\" srcset=\"https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-256w.jpeg 256w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-410w.jpeg 410w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-512w.jpeg 512w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-650w.jpeg 650w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-850w.jpeg 850w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-1075w.jpeg 1075w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-1280w.jpeg 1280w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-1420w.jpeg 1420w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-1660w.jpeg 1660w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-1860w.jpeg 1860w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-2048w.jpeg 2048w, https://www.atomwolf.org/img/chonky-benchy-1-qPIO2DbKDb-3629w.jpeg 3629w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"></picture></a>\n<figcaption>\n<p>The chonky Benchy fills my hand, and feels quite solid.</p>\n</figcaption>\n</figure>\n<figure>\n<a href=\"https://www.atomwolf.org/posts/chonky-benchy/images/chonky-benchy-in-hand-2/\" data-lightbox=\"true\">\n<picture><source type=\"image/avif\" srcset=\"https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-256w.avif 256w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-410w.avif 410w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-512w.avif 512w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-650w.avif 650w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-850w.avif 850w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-1075w.avif 1075w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-1280w.avif 1280w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-1420w.avif 1420w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-1660w.avif 1660w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-1860w.avif 1860w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-2048w.avif 2048w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-3714w.avif 3714w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/webp\" srcset=\"https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-256w.webp 256w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-410w.webp 410w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-512w.webp 512w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-650w.webp 650w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-850w.webp 850w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-1075w.webp 1075w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-1280w.webp 1280w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-1420w.webp 1420w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-1660w.webp 1660w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-1860w.webp 1860w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-2048w.webp 2048w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-3714w.webp 3714w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><img alt=\"Another angle showing the large 3D-printed tugboat model and its thick layer lines in someone's hand.\" loading=\"lazy\" decoding=\"async\" src=\"https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-256w.jpeg\" width=\"3714\" height=\"2268\" srcset=\"https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-256w.jpeg 256w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-410w.jpeg 410w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-512w.jpeg 512w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-650w.jpeg 650w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-850w.jpeg 850w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-1075w.jpeg 1075w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-1280w.jpeg 1280w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-1420w.jpeg 1420w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-1660w.jpeg 1660w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-1860w.jpeg 1860w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-2048w.jpeg 2048w, https://www.atomwolf.org/img/chonky-benchy-2-VP8i_ZV3rm-3714w.jpeg 3714w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"></picture></a>\n<figcaption>\n<p>It turned out great, considering it’s a first print with a new nozzle and a custom configuration.</p>\n</figcaption>\n</figure>\n<figure>\n<a href=\"https://www.atomwolf.org/posts/chonky-benchy/images/chonky-benchy-next-to-drink-can/\" data-lightbox=\"true\">\n<picture><source type=\"image/avif\" srcset=\"https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-256w.avif 256w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-410w.avif 410w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-512w.avif 512w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-650w.avif 650w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-850w.avif 850w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-1075w.avif 1075w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-1280w.avif 1280w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-1420w.avif 1420w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-1660w.avif 1660w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-1860w.avif 1860w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-2048w.avif 2048w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-2061w.avif 2061w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/webp\" srcset=\"https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-256w.webp 256w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-410w.webp 410w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-512w.webp 512w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-650w.webp 650w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-850w.webp 850w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-1075w.webp 1075w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-1280w.webp 1280w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-1420w.webp 1420w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-1660w.webp 1660w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-1860w.webp 1860w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-2048w.webp 2048w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-2061w.webp 2061w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><img alt=\"The large 3D-printed tugboat model on a desk, next to a drink can.\" loading=\"lazy\" decoding=\"async\" src=\"https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-256w.jpeg\" width=\"2061\" height=\"2010\" srcset=\"https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-256w.jpeg 256w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-410w.jpeg 410w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-512w.jpeg 512w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-650w.jpeg 650w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-850w.jpeg 850w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-1075w.jpeg 1075w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-1280w.jpeg 1280w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-1420w.jpeg 1420w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-1660w.jpeg 1660w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-1860w.jpeg 1860w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-2048w.jpeg 2048w, https://www.atomwolf.org/img/chonky-benchy-3-ELUBwS6E6D-2061w.jpeg 2061w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"></picture></a>\n<figcaption>\n<p>“The chonky Benchy with a soda can for comparison”</p>\n</figcaption>\n</figure>\n<p>I really like how it turned out! It’s heavy and feels really solid.</p>\n<p>The exaggerated layer lines remind me of early 3D printing experiments–but it looks so much nicer than my early 3D printing experiments did!</p>\n<hr><p><a href=\"mailto:feedcomments@atomwolf.org?subject=Feed%20Reply%20for%20Chonky%20Benchy&amp;body=(From%20feed%20entry%3A%20https%3A%2F%2Fwww.atomwolf.org%2Fposts%2Fchonky-benchy%2F)\">Reply via email</a></p>",
			"date_published": "2023-06-05T00:00:00Z"
		%}
		,
		{
			"id": "https://www.atomwolf.org/posts/adafruit-esp32-s3-review-tft-feather-case/",
			"url": "https://www.atomwolf.org/posts/adafruit-esp32-s3-review-tft-feather-case/",
			"title": "Introducing a case for the Adafruit ESP32-S3 Reverse TFT Feather",
			"content_html": "<p>I recently designed a case and stand for the <a href=\"https://www.adafruit.com/product/5691\">Adafruit ESP32-S3 Reverse TFT Feather</a>.  The case/stand is a single piece, and snaps onto the board.  The feet hold the electronics off the table whether it’s on its back or on its side.  The little fins prop the board up at an angle when it’s standing.  The buttons on the front flex and do a decent job preserving the clicky feel of the buttons on the board.</p>\n<p>I think it could use another revision with little retention nubs in the corners to hold the board in place better.  I’ve noticed that if I push the buttons too hard without supporting the board, the board pops out.</p>\n<figure>\n<a href=\"https://www.atomwolf.org/posts/adafruit-esp32-s3-review-tft-feather-case/images/case-for-adafruit-esp32-s3-reverse-tft-feather-in-action/\" data-lightbox=\"true\">\n<picture><source type=\"image/avif\" srcset=\"https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-256w.avif 256w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-410w.avif 410w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-512w.avif 512w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-650w.avif 650w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-850w.avif 850w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-1075w.avif 1075w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-1280w.avif 1280w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-1420w.avif 1420w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-1660w.avif 1660w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-1860w.avif 1860w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-2048w.avif 2048w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-4032w.avif 4032w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/webp\" srcset=\"https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-256w.webp 256w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-410w.webp 410w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-512w.webp 512w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-650w.webp 650w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-850w.webp 850w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-1075w.webp 1075w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-1280w.webp 1280w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-1420w.webp 1420w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-1660w.webp 1660w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-1860w.webp 1860w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-2048w.webp 2048w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-4032w.webp 4032w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><img alt=\"A grey 3D printed case for a circuit board, sitting on cork on a table.\" loading=\"lazy\" decoding=\"async\" src=\"https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-256w.jpeg\" width=\"4032\" height=\"2268\" srcset=\"https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-256w.jpeg 256w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-410w.jpeg 410w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-512w.jpeg 512w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-650w.jpeg 650w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-850w.jpeg 850w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-1075w.jpeg 1075w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-1280w.jpeg 1280w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-1420w.jpeg 1420w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-1660w.jpeg 1660w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-1860w.jpeg 1860w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-2048w.jpeg 2048w, https://www.atomwolf.org/img/PXL_20230505_212716003.MP-z_cotE_XN7-4032w.jpeg 4032w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"></picture></a>\n<figcaption>\n<p>3D printed case for the Adafruit ESP32-S3 Reverse TFT Feather in action</p>\n</figcaption>\n</figure>\n<figure>\n<a href=\"https://www.atomwolf.org/posts/adafruit-esp32-s3-review-tft-feather-case/images/back-view/\" data-lightbox=\"true\">\n<picture><source type=\"image/avif\" srcset=\"https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-256w.avif 256w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-410w.avif 410w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-512w.avif 512w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-650w.avif 650w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-850w.avif 850w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-1075w.avif 1075w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-1280w.avif 1280w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-1420w.avif 1420w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-1660w.avif 1660w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-1860w.avif 1860w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-2048w.avif 2048w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-4032w.avif 4032w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/webp\" srcset=\"https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-256w.webp 256w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-410w.webp 410w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-512w.webp 512w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-650w.webp 650w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-850w.webp 850w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-1075w.webp 1075w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-1280w.webp 1280w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-1420w.webp 1420w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-1660w.webp 1660w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-1860w.webp 1860w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-2048w.webp 2048w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-4032w.webp 4032w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><img alt=\"A back view of the 3D printed case and circuit board\" loading=\"lazy\" decoding=\"async\" src=\"https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-256w.jpeg\" width=\"4032\" height=\"2268\" srcset=\"https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-256w.jpeg 256w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-410w.jpeg 410w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-512w.jpeg 512w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-650w.jpeg 650w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-850w.jpeg 850w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-1075w.jpeg 1075w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-1280w.jpeg 1280w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-1420w.jpeg 1420w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-1660w.jpeg 1660w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-1860w.jpeg 1860w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-2048w.jpeg 2048w, https://www.atomwolf.org/img/PXL_20230505_212727584.MP-J5TAh9Da3Y-4032w.jpeg 4032w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"></picture></a>\n<figcaption>\n<p>“Case” may be an overstatement. The whole thing clips on the front of the Feather.</p>\n</figcaption>\n</figure>\n<figure>\n<a href=\"https://www.atomwolf.org/posts/adafruit-esp32-s3-review-tft-feather-case/images/usb-c-end-view/\" data-lightbox=\"true\">\n<picture><source type=\"image/avif\" srcset=\"https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-256w.avif 256w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-410w.avif 410w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-512w.avif 512w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-650w.avif 650w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-850w.avif 850w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-1075w.avif 1075w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-1280w.avif 1280w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-1420w.avif 1420w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-1660w.avif 1660w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-1860w.avif 1860w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-2048w.avif 2048w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-4032w.avif 4032w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/webp\" srcset=\"https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-256w.webp 256w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-410w.webp 410w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-512w.webp 512w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-650w.webp 650w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-850w.webp 850w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-1075w.webp 1075w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-1280w.webp 1280w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-1420w.webp 1420w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-1660w.webp 1660w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-1860w.webp 1860w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-2048w.webp 2048w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-4032w.webp 4032w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><img alt=\"The USB-C port end of the case and circuit board.\" loading=\"lazy\" decoding=\"async\" src=\"https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-256w.jpeg\" width=\"4032\" height=\"2268\" srcset=\"https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-256w.jpeg 256w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-410w.jpeg 410w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-512w.jpeg 512w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-650w.jpeg 650w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-850w.jpeg 850w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-1075w.jpeg 1075w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-1280w.jpeg 1280w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-1420w.jpeg 1420w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-1660w.jpeg 1660w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-1860w.jpeg 1860w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-2048w.jpeg 2048w, https://www.atomwolf.org/img/PXL_20230505_212804161.MP-hGryOJLwGx-4032w.jpeg 4032w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"></picture></a>\n<figcaption>\n<p>This is the USB-C side.</p>\n</figcaption>\n</figure>\n<figure>\n<a href=\"https://www.atomwolf.org/posts/adafruit-esp32-s3-review-tft-feather-case/images/standing-on-its-side/\" data-lightbox=\"true\">\n<picture><source type=\"image/avif\" srcset=\"https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-256w.avif 256w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-410w.avif 410w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-512w.avif 512w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-650w.avif 650w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-850w.avif 850w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-1075w.avif 1075w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-1280w.avif 1280w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-1420w.avif 1420w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-1660w.avif 1660w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-1860w.avif 1860w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-2048w.avif 2048w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-4032w.avif 4032w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/webp\" srcset=\"https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-256w.webp 256w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-410w.webp 410w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-512w.webp 512w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-650w.webp 650w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-850w.webp 850w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-1075w.webp 1075w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-1280w.webp 1280w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-1420w.webp 1420w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-1660w.webp 1660w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-1860w.webp 1860w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-2048w.webp 2048w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-4032w.webp 4032w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><img alt=\"A back view of the 3D printed case, sitting on a table.\" loading=\"lazy\" decoding=\"async\" src=\"https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-256w.jpeg\" width=\"4032\" height=\"2268\" srcset=\"https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-256w.jpeg 256w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-410w.jpeg 410w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-512w.jpeg 512w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-650w.jpeg 650w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-850w.jpeg 850w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-1075w.jpeg 1075w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-1280w.jpeg 1280w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-1420w.jpeg 1420w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-1660w.jpeg 1660w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-1860w.jpeg 1860w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-2048w.jpeg 2048w, https://www.atomwolf.org/img/PXL_20230505_212817721.MP-id9zlkze04-4032w.jpeg 4032w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"></picture></a>\n<figcaption>\n<p>The little stand fins means it can stand angled on its side.</p>\n</figcaption>\n</figure>\n<figure>\n<a href=\"https://www.atomwolf.org/posts/adafruit-esp32-s3-review-tft-feather-case/images/laying-down/\" data-lightbox=\"true\">\n<picture><source type=\"image/avif\" srcset=\"https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-256w.avif 256w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-410w.avif 410w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-512w.avif 512w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-650w.avif 650w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-850w.avif 850w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-1075w.avif 1075w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-1280w.avif 1280w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-1420w.avif 1420w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-1660w.avif 1660w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-1860w.avif 1860w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-2048w.avif 2048w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-4032w.avif 4032w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><source type=\"image/webp\" srcset=\"https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-256w.webp 256w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-410w.webp 410w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-512w.webp 512w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-650w.webp 650w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-850w.webp 850w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-1075w.webp 1075w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-1280w.webp 1280w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-1420w.webp 1420w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-1660w.webp 1660w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-1860w.webp 1860w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-2048w.webp 2048w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-4032w.webp 4032w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"><img alt=\"The 3D printed case flat against a table.\" loading=\"lazy\" decoding=\"async\" src=\"https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-256w.jpeg\" width=\"4032\" height=\"2268\" srcset=\"https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-256w.jpeg 256w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-410w.jpeg 410w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-512w.jpeg 512w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-650w.jpeg 650w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-850w.jpeg 850w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-1075w.jpeg 1075w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-1280w.jpeg 1280w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-1420w.jpeg 1420w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-1660w.jpeg 1660w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-1860w.jpeg 1860w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-2048w.jpeg 2048w, https://www.atomwolf.org/img/PXL_20230505_212833456.MP-uPwJh_bF2O-4032w.jpeg 4032w\" sizes=\"(min-width: 1280px) calc(50vw - 84px), (min-width: 780px) calc(55vw - 84px), (min-width: 640px) calc(80vw - 82px), calc(90vw - 82px)\"></picture></a>\n<figcaption>\n<p>The back legs means it can lay down flat while staying off the table.</p>\n</figcaption>\n</figure>\n<hr><p><a href=\"mailto:feedcomments@atomwolf.org?subject=Feed%20Reply%20for%20Introducing%20a%20case%20for%20the%20Adafruit%20ESP32-S3%20Reverse%20TFT%20Feather&amp;body=(From%20feed%20entry%3A%20https%3A%2F%2Fwww.atomwolf.org%2Fposts%2Fadafruit-esp32-s3-review-tft-feather-case%2F)\">Reply via email</a></p>",
			"date_published": "2023-05-05T00:00:00Z"
		%}
		,
		{
			"id": "https://www.atomwolf.org/posts/search-parse-with-awk/",
			"url": "https://www.atomwolf.org/posts/search-parse-with-awk/",
			"title": "Search and parse a field with Awk",
			"content_html": "<div class=\"dotted-metadata-block\">\n<p><span class=\"small-caps\">Assumed audience</span>: folks who use the <a href=\"https://www.atomwolf.org/tags/command-line\">command line</a></p>\n<p><a class=\"small-caps\" href=\"https://www.atomwolf.org/about/pages/\">Type</a>: tip</p>\n</div>\n<hr><p>I frequently need to do simple parsing. Often I resort to grep and cut, or a regular expression I send through Python. I rarely use something like awk.</p>\n<p>I needed to look for a pattern in some text coming through a pipeline (“Configuration backup archive complete”), split that line up by colons, and print the second field.  This is what I came up with:</p>\n<p><code>| awk -F: '/Configuration backup archive complete/{print $2}'</code></p>\n<hr><p><a href=\"mailto:feedcomments@atomwolf.org?subject=Feed%20Reply%20for%20Search%20and%20parse%20a%20field%20with%20Awk&amp;body=(From%20feed%20entry%3A%20https%3A%2F%2Fwww.atomwolf.org%2Fposts%2Fsearch-parse-with-awk%2F)\">Reply via email</a></p>",
			"date_published": "2018-08-24T00:00:00Z"
		%}
		,
		{
			"id": "https://www.atomwolf.org/posts/follow-file-on-github/",
			"url": "https://www.atomwolf.org/posts/follow-file-on-github/",
			"title": "Watch for file changes on GitHub",
			"content_html": "<div class=\"dotted-metadata-block\">\n<p><span class=\"small-caps\">Assumed audience</span>: folks who use <a href=\"https://www.atomwolf.org/tags/github/\">GitHub</a></p>\n<p><a class=\"small-caps\" href=\"https://www.atomwolf.org/about/pages/\">Type</a>: tip</p>\n</div>\n<hr><p>Sometimes, I want to watch for changes in a particular file on GitHub.</p>\n<p>When I’m part of the development team, sometimes a <a href=\"https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners\">CODEOWNERS</a> file works.  GitHub will automatically flag me on upcoming changes to particular files or directories.</p>\n<p>If I’m not a member of the repository, a CODEOWNERS file won’t work. I found out recently that GitHub has RSS/Atom feeds for the contents of repositories. To watch the file README.rst on the main branch of <a href=\"https://github.com/adafruit/circuitpython\">https://github.com/adafruit/circuitpython</a>, use the RSS feed <a href=\"https://github.com/adafruit/circuitpython/commits/main/README.rst.atom\">https://github.com/adafruit/circuitpython/commits/main/README.rst.atom</a>.</p>\n<p>Thanks, Jake!</p>\n<p>Beyond this, I would love to be able to watch for public pull requests that affect that file. Let me know if you have any ideas!</p>\n<hr><p><a href=\"mailto:feedcomments@atomwolf.org?subject=Feed%20Reply%20for%20Watch%20for%20file%20changes%20on%20GitHub&amp;body=(From%20feed%20entry%3A%20https%3A%2F%2Fwww.atomwolf.org%2Fposts%2Ffollow-file-on-github%2F)\">Reply via email</a></p>",
			"date_published": "2018-08-24T00:00:00Z"
		%}
		
	]
}}
