<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: PartFit 3D</title>
    <description>The latest articles on DEV Community by PartFit 3D (@partfit3d).</description>
    <link>https://dev.to/partfit3d</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4062018%2Fd2c15e02-0289-4db7-b7ae-149575fa1ecd.png</url>
      <title>DEV Community: PartFit 3D</title>
      <link>https://dev.to/partfit3d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/partfit3d"/>
    <language>en</language>
    <item>
      <title>Running Spotify's basic-pitch in the browser for audio-to-MIDI</title>
      <dc:creator>PartFit 3D</dc:creator>
      <pubDate>Wed, 30 Sep 2026 06:57:40 +0000</pubDate>
      <link>https://dev.to/partfit3d/running-spotifys-basic-pitch-in-the-browser-for-audio-to-midi-44bc</link>
      <guid>https://dev.to/partfit3d/running-spotifys-basic-pitch-in-the-browser-for-audio-to-midi-44bc</guid>
      <description>&lt;p&gt;I wanted a quick MIDI sketch of a melody I had sung or played, without uploading the recording to anyone's server. Spotify's &lt;a href="https://github.com/spotify/basic-pitch" rel="noopener noreferrer"&gt;basic-pitch&lt;/a&gt; model ships a TensorFlow.js build, so the whole thing can run in a browser tab. Here is what it took to make that feel responsive, and where it still falls short.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the model out of the first page load
&lt;/h2&gt;

&lt;p&gt;TensorFlow.js plus the model is too heavy for the initial bundle, and most visitors read the page before they drop a file. So the library is only loaded through a dynamic &lt;code&gt;import()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;modulePromise&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@spotify/basic-pitch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadModule&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;modulePromise&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;modulePromise&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@spotify/basic-pitch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;modulePromise&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model weights (about 1 MB) are served as static files and cached after the first run. The download starts as soon as someone picks a file, so it overlaps with them choosing which part of the audio to convert.&lt;/p&gt;

&lt;p&gt;One trap: basic-pitch pins its own TensorFlow.js version. Adding &lt;code&gt;@tensorflow/tfjs&lt;/code&gt; as a separate dependency loads a second copy that re-registers every kernel, so don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Split inference from note extraction
&lt;/h2&gt;

&lt;p&gt;The model produces three per-frame matrices: note activations (&lt;code&gt;frames&lt;/code&gt;), onsets, and pitch contours. Turning those into notes is a separate, cheap step controlled by two thresholds and a minimum note length.&lt;/p&gt;

&lt;p&gt;That split matters for the interface. Running the model over a 30 second clip takes a few seconds; turning its output into notes takes milliseconds. So the raw output is kept, and moving a sensitivity slider only re-runs the second step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Runs once per segment&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;audioBuffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Runs on every slider change&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;notes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;notesFromOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;onsetThreshold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;frameThreshold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;minNoteLengthMs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;minPitchMidi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;maxPitchMidi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One detail: &lt;code&gt;outputToNotesPoly&lt;/code&gt; mutates the frame and onset arrays when you pass a pitch range, so you need to copy them before each call if you want to reuse the originals.&lt;/p&gt;

&lt;p&gt;The model expects mono audio at 22050 Hz. &lt;code&gt;OfflineAudioContext&lt;/code&gt; handles the mixdown and resampling without any extra library.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let people hear the mistakes
&lt;/h2&gt;

&lt;p&gt;A piano roll doesn't tell you much about whether the transcription is right. What helped more was an A/B player: the original audio and a simple WebAudio synth of the detected notes play on one shared playhead, and you switch between them. A wrong octave or a missing note is obvious by ear in a second.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clean up with plain functions
&lt;/h2&gt;

&lt;p&gt;Model output needs tidying before it is useful in a DAW. Each cleanup step is a pure function over a note array, which keeps them easy to test and combine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;remove notes shorter than a threshold&lt;/li&gt;
&lt;li&gt;remove notes much quieter than the rest&lt;/li&gt;
&lt;li&gt;merge repeated notes separated by tiny gaps&lt;/li&gt;
&lt;li&gt;trim overlaps so a monophonic line stays monophonic&lt;/li&gt;
&lt;li&gt;quantize to a grid based on an estimated tempo&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Export uses &lt;code&gt;@tonejs/midi&lt;/code&gt; to write a standard &lt;code&gt;.mid&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it doesn't work
&lt;/h2&gt;

&lt;p&gt;basic-pitch does well on a single line: a sung melody, a guitar riff, a piano part. On a dense full mix it gives you something rough at best. Limiting a run to 15 to 60 seconds also keeps memory in check on phones, where long clips can run out of resources.&lt;/p&gt;

&lt;p&gt;The result is &lt;a href="https://mididraft.com" rel="noopener noreferrer"&gt;MidiDraft&lt;/a&gt;, if you want to try it on your own recordings. The examples page includes one clip it deliberately gets wrong, so you can see what failure looks like before trusting it with your own audio.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webaudio</category>
      <category>machinelearning</category>
      <category>music</category>
    </item>
    <item>
      <title>How Browser-Local Mesh Processing Keeps STL and 3MF Files Private</title>
      <dc:creator>PartFit 3D</dc:creator>
      <pubDate>Tue, 04 Aug 2026 08:11:09 +0000</pubDate>
      <link>https://dev.to/partfit3d/how-browser-local-mesh-processing-keeps-stl-and-3mf-files-private-he3</link>
      <guid>https://dev.to/partfit3d/how-browser-local-mesh-processing-keeps-stl-and-3mf-files-private-he3</guid>
      <description>&lt;p&gt;When a maker opens a model-preparation tool, the file may contain more than triangles. A prototype can reveal dimensions, mechanical interfaces, product direction, or customer work. That makes the architecture of the tool—not just its privacy policy—important.&lt;/p&gt;

&lt;p&gt;A browser-local workflow keeps the model on the user's device. The page itself is downloaded from a server, but parsing, geometry operations, fit checks, and export happen inside the browser. This is a useful pattern for STL and 3MF utilities because modern browsers already provide the APIs and performance needed for substantial client-side work.&lt;/p&gt;

&lt;p&gt;This article walks through the main pieces of that architecture and the trade-offs to expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Treat the file picker as a local boundary
&lt;/h2&gt;

&lt;p&gt;The browser's File API gives an application access only to files the user explicitly chooses. Reading a selected file with &lt;code&gt;arrayBuffer()&lt;/code&gt; does not upload it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fileInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point the data is in the tab's memory. It crosses the network only if application code sends it with &lt;code&gt;fetch&lt;/code&gt;, a form submission, a WebSocket, or another network API.&lt;/p&gt;

&lt;p&gt;That distinction should be visible in both the implementation and the product copy. “Runs in your browser” is not automatically the same as “never uploads your model.” A trustworthy design avoids model-upload endpoints, does not place raw geometry in analytics events, and makes network behavior easy to inspect.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Parse STL and 3MF without assuming they are equivalent
&lt;/h2&gt;

&lt;p&gt;STL is comparatively simple: it represents a triangle surface, usually in binary form, with no standard unit metadata. A parser typically produces a flat list of vertex positions and, optionally, normals.&lt;/p&gt;

&lt;p&gt;3MF is a ZIP-based package containing XML and related assets. It can preserve units, object structure, transforms, and other information that STL loses. A browser-local 3MF pipeline therefore needs to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;read the package in memory;&lt;/li&gt;
&lt;li&gt;locate the model relationships and XML parts;&lt;/li&gt;
&lt;li&gt;apply declared units and object transforms;&lt;/li&gt;
&lt;li&gt;convert referenced meshes into a consistent internal representation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The safest internal representation is explicit about units and coordinate systems. Converting everything to millimeters early prevents subtle fit-check errors later.&lt;/p&gt;

&lt;p&gt;For large files, parsing and mesh construction should run in a Web Worker. That keeps the main thread responsive while the worker turns bytes into typed arrays. Transferable &lt;code&gt;ArrayBuffer&lt;/code&gt; objects can move data between the worker and UI without expensive copies.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Fit checking is a geometry problem, not a slicer
&lt;/h2&gt;

&lt;p&gt;A build-volume check starts with the model's axis-aligned bounding box. For every vertex, track the minimum and maximum coordinate on each axis:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;sizeX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;maxX&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;minX&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;sizeY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;maxY&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;minY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;sizeZ&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;maxZ&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;minZ&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those dimensions can be compared with a printer's usable width, depth, and height. A configurable safety margin is worth including because nominal build volume is not always the practical printable volume.&lt;/p&gt;

&lt;p&gt;Orientation matters. Rotating the model changes its axis-aligned bounds, sometimes enough to make it fit without a cut. The UI should therefore recompute dimensions after each transform and show the result clearly—ideally as a per-part &lt;code&gt;Fit&lt;/code&gt; or &lt;code&gt;Too Large&lt;/code&gt; status.&lt;/p&gt;

&lt;p&gt;This remains model preparation, not slicing. The browser tool can orient geometry, split it, inspect the mesh, and export parts. A slicer such as Bambu Studio, Cura, or PrusaSlicer still turns those parts into toolpaths and G-code.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. A plane cut needs more than deleting triangles
&lt;/h2&gt;

&lt;p&gt;Splitting a triangle mesh by a plane sounds simple until a triangle crosses the plane. A robust cut pipeline generally does the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;classify each triangle vertex by signed distance to the plane;&lt;/li&gt;
&lt;li&gt;keep triangles entirely on either side;&lt;/li&gt;
&lt;li&gt;split crossing triangles at their plane intersections;&lt;/li&gt;
&lt;li&gt;collect the resulting boundary segments;&lt;/li&gt;
&lt;li&gt;join segments into closed loops;&lt;/li&gt;
&lt;li&gt;triangulate those loops to cap the two open cut surfaces.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Floating-point tolerance is critical. Vertices that are mathematically on the plane may land slightly above or below it in binary arithmetic. Using an epsilon consistently avoids cracks, duplicated slivers, and unstable classifications.&lt;/p&gt;

&lt;p&gt;Repeated cuts add another requirement: every resulting mesh needs its own identity, transform, dimensions, and validation state. An auditable parts list is much easier to reason about than a destructive “latest result only” workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Validate topology before export
&lt;/h2&gt;

&lt;p&gt;A file can look fine in a preview while still causing trouble downstream. Useful browser-side checks include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;degenerate triangles with near-zero area;&lt;/li&gt;
&lt;li&gt;boundary edges that appear only once;&lt;/li&gt;
&lt;li&gt;non-manifold edges shared by more than two faces;&lt;/li&gt;
&lt;li&gt;disconnected components;&lt;/li&gt;
&lt;li&gt;inconsistent winding or normals;&lt;/li&gt;
&lt;li&gt;dimensions that still exceed the selected build volume.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These checks do not guarantee that every slicer will accept every model, but they expose common failure modes before the user downloads several parts.&lt;/p&gt;

&lt;p&gt;Export should preserve the same units and geometry the fit checker evaluated. STL export is widely compatible but loses metadata. 3MF can retain richer structure and is often a better handoff format when the downstream slicer supports it.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Make privacy testable
&lt;/h2&gt;

&lt;p&gt;Privacy claims become more credible when users can verify them. A browser-local model tool can support that in several ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep core processing usable without an account;&lt;/li&gt;
&lt;li&gt;avoid model-related network requests;&lt;/li&gt;
&lt;li&gt;document which telemetry, if any, is collected;&lt;/li&gt;
&lt;li&gt;keep filenames and model-derived measurements out of analytics;&lt;/li&gt;
&lt;li&gt;clear in-memory state when the user resets the workspace;&lt;/li&gt;
&lt;li&gt;use a restrictive Content Security Policy where practical.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The browser's developer tools should show the page assets loading, but no request containing the selected model. That is a concrete, testable promise.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Know the limits of the approach
&lt;/h2&gt;

&lt;p&gt;Browser-local processing is not free of constraints. Memory is finite, mobile devices vary widely, and a complex cut can create many new triangles. Large files benefit from workers, typed arrays, progress reporting, cancellation, and early estimates of memory use.&lt;/p&gt;

&lt;p&gt;The browser is also not a full CAD kernel. Plane cuts and mesh checks are realistic; exact solid modeling, feature-history editing, and repair of every pathological mesh are different problems.&lt;/p&gt;

&lt;p&gt;The best architecture is honest about that boundary: do focused model preparation locally, then hand the results to a dedicated slicer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thought
&lt;/h2&gt;

&lt;p&gt;Local-first design is especially well matched to 3D-printing files. It reduces upload latency, keeps prototypes under the user's control, and makes privacy a property of the system rather than only a policy statement.&lt;/p&gt;

&lt;p&gt;We used these principles while building &lt;a href="https://partfit3d.com/" rel="noopener noreferrer"&gt;PartFit 3D&lt;/a&gt;, a browser-based STL and 3MF splitter with build-volume checks, repeated plane cuts, per-part fit status, mesh checks, and slicer-ready export. The broader pattern applies to many file-processing tools: if the browser can do the work safely and responsively, the user's data may not need to leave the device at all.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>privacy</category>
      <category>3dprinting</category>
    </item>
  </channel>
</rss>
