Wednesday, November 19, 2025

SVG contour map clipping

I've gone back to trying to figure out how to generate topographical data with contour maps.

I found a youtube that ended up pointing me to contours.axismaps.com, where I could generate a topographical map of an area, and download it as SVG.

On inspecting the SVG output, it was pretty simple.  It was constructed as an <svg> tag followed by a number of individual <path> tags.  Each <path> has a "d" attribute that contains all the Move and Line (M x,y L x,y L x,y L x,y L x,y L x,y...) definitions.  A "d" attribute in SVG can have all sorts of other things in it, like cubic Beziers, but this particular app doesn't generate them.

The "d" attribute actually also could contain multiple MLLLL values, so really, there were subpaths within the path.  The <path> tag has an "id" attribute that declares the elevation for that path (and all its subpaths).

The "d" attribute also is highly granular.  If you look at it, you'll find that it has tons of L movements that are only 1 numeric value apart.

If you generate a contour that, for example, has 100 foot elevation separations, and you're rendering a somewhat hilly region, you'll end up with a very large output file.  Mine was 57 MB.

As well, with that many segments, it makes Inkscape crawl.

Here's what it looks like when rendered.

 

What the picture above doesn't show is how slow it is to read and render.  It's really slow. 

I had a goal to clip the contours to a geometry, perhaps to use the lines for laser etching.  But due to the level of detail in the contour lines, it would still be slow when clipped.

Clipping

My initial attempt at clipping the lines with my own code was just to use a circle, and do a really crappy approach.  Since it seemed the segments were really small, I made the code include segments where both start and end points were within a given radius of a center point, and exclude others.  That isn't really "clipping".  But it's close enough for what I'm doing.

In addition, to address the contour line detail level, I chose to output only a subset of the segments.

The overall sequence was like this:

  • Read the file using  svgelements.SVG.parse.
  • Emit the <svg> element
  • For each <path> element, find its "d" attribute, and break the MLLLL values into subpaths.
  • For each subpath, clip the subpath according to the circle.  The result will result in zero, one, or multiple subsubpaths. 
  • For each subsubpath, emit a different set of MLLLL values.  In the end, I chose to keep the first 8 and last 8 vectors, and only every 8th segment in between.  This made it so that small islands would be preserved, but long ones would drop to about 1/8th their precision (but keep precise points at the ends). 
  • Close off the paths
  • Add an extra SVG element to show the clipping object. 

Here's the result of the crude, circular clip.

 

Later, I changed the "clip" to extend clipped segments to the circle edge but still in a cheap way, so it was not mathematically correct, but would yield segments that would truly join to the circle.

The next thing I did was to do rectangular clip.  Again, I did a cheap clipping mechanism, just checking to see if both endpoints of a segment were within the (x1,y1) to (x2,y2) definition of the rectangle.  Still, not mathematically clipping.

Here's the rectangle clip result:

 

Finally, I read that there is another system called shapely that provides a native mechanism for clipping.  It could do rectangle clipping, or arbitrary polygon clipping.  For rectangle clipping, it provides shapely.clip_by_rect, which does an efficient clip.  This probably uses the classic computer graphics algorithm for intersection detection, edge crossing, and real clip computation.

To use this, I had to switch the coordinate system to be shapely-friendly.  One of the really odd things about the svgelements library is that (x,y) values are represented as a single, complex number (e.g., (100,200) is 100.0+200.0j).  

shapely, on the other hand, depends on having LineString, MultiLineString, and Polygon geometries.  Each of those has coordinates, but the coordinates are in a more typical format of an array of (x,y) values. 

Here's the result.

 

The difference is negligible, both in computation time and result, but the shapely approach seems much more reliable.

 Later, I also tried the arbitrary polygon clipping mechanism, using a manually defined set of coordinates.  Here's the result.

 

I'm now trying to move to reading an existing SVG polygon to use for clipping, but handling holes as "interior" vs "exterior" polygon geometries is proving to be difficult.

 

 

Laser marking tile

 CO2 Laser marking on tile

Below are some results I've gotten when laser marking tile.

I'm using

  • 4"x4" glossy, white tiles
  • Full Spectrum Laser hobby laser (about 14" x 9.5" bed) 
  • Full Spectrum Retina Engrave (RE)

For engraving, the settings have varied.

  1. The native dithering algorithm provided by RE doesn't have a declared name, but because of that, and because it doesn't yield a high contrast image, I think we can guess it's Floyd-Steinberg.
  2. RE allows for 250dpi, 500dpi, and 1000dpi, but if you're not careful in your order of operations, you can get stuck with 250dpi when you want it higher.
  3. Each test is run with raster speed (defined in RE), raster power (defined in RE), and also the current (set using a multiturn pot on the main board).  Based on experiments, I'm guessing that "raster power" in RE actually controls things using PWM, but somehow the current setting does not.  I got some odd banding using 80% raster power, but when I bumped it to 100%, the banding went away. 

In early experiments, I tried the "Norton" method that involves creating a mixture of food-grade TiO2 and other substances (typically isopropyl alcohol, maybe some Elmer's glue), spraying that onto the tile, and letting it dry.  I found that approach to be difficult, mainly because it was clumping and would kill my spray bottles.

I switched to using a white spray paint that contains TiO2, or at least the Internet said it does.  

Spray paint: Rust-oleum Professional High Performance Enamel, 15 minute fast dry, Any-Angle Spray. 

For the image, I chose an old black and white photo, and brought it into Photoshop Elements to force it to 500 dpi.  Then, I pulled that directly into RE (that is, I did not read it in Inkscape and use the FSL print driver to get it to RE), and made sure to set the rastering DPI to 500. 

Here's the first set of results.

 

These images used the native RE dithering.  The left side used 80% power, whereas the right side had 100% power.  If you look closely, you'll see vertical banding.

In each case, the tile was sprayed, dried, and then "engraved".  Then, I would let it cool, wipe with paint thinner (using a paper towel), and then clean with soap and water.

I noticed that the 100% power tile wasn't all that great, so I tried using multiple spray levels.  Here's what I got:

In this example, the left side is the same as what I'd had in the prior photo.  But the right side had three layers of paint.  That came out much better.  However, it still didn't have the dark blacks I wanted.

Right side settings:

  • 4" x 4" glossy white tile
  • 3 coats of Rust-oleum Professional High Performance Enamel
  • RE-native dithering (likely Floyd-Steinberg)
  • Engrave at 500 dpi 
  • Raster power 100%
  • Speed 70%
  • 7 turns (70% current) 

I switched two things for the next experiment.  First, I'd read that the Atkinson Dithering method would be provide greater contrast.  I wrote a Python program that did the Atkinson dithering for me, based on a number of web examples.  The program I settled on used Numpy and numba for speed.  One trick to this is that you have to do extra steps to write out the file (from Python) with a DPI declaration, or else RE will assume a different resolution.

With the image at 500 DPI and dithered using Atkinson, I would then read the file into RE, but set its image rendering to "raw", so it just treats each black and white pixel as it is, and doesn't do any further dithering.

Then, I changed the paint to a primer that I'd heard about online.

Paint: Zinsser Bulls Eye 1-2-3 Primer for all projects, Bright White, Interior/Exterior.  (This still is a Rust-Oleum product.)

Here's the result:

 

The left side is the same photo from earlier, using RE dithering and the prior paint.  The right side is Atkinson with the Zinsser Bulls Eye 1-2-3.  The right-side tile has not been cleaned much at all.

Overally, I'm getting much blacker blacks with the Zinsser.

In one not-shown experiment, I painted with Zinsser, lased, and then wiped with 70% isopropyl alcohol instead of paint thinner.  It pretty much wiped off the marked image!  That's why I haven't (yet) wiped off the newly marked tile.

In the end, the good result appears to be:

  • 4" x 4" glossy white tile
  • 3 coats of Zinsser Bulls Eye 1-2-3
  • Atkinson dithering (local Python program); render as "raw" in RE
  • Engrave at 500 dpi 
  • Raster power 100%
  • Raster speed 70%
  • 7 turns of the current pot (which equates to 70% current) 

I've heard that a good result can be had by using two layers of Zinsser and one layer of High Performance, so I'll try that, too. 

Sunday, May 4, 2025

XTool Smoke Purifier filter hack

 I got an XTool Smoke Purifier recently, but it was quite dirty.


 

After opening it, you can remove the filter stack from inside.  Within the filter stack, you'll find a mesh-type pre-filter (kind of like thick wool), then what appears to be 2" HEPA filter, and then probably some kind of other filter with activated carbon below.


 

I took out the pre-filter and washed it out, and then dried it.  It was absolutely filthy.  I've seen a YouTube page that suggests getting aquarium filter mesh for that, and I might yet do that.

After I put the cleaned (somewhat clean) pre-filter, I put it back in, and tried running the extractor.  But I found that there wasn't any air coming out the exhaust.  I tried doing the same without the HEPA filter portion, and got good air flow, so that meant the HEPA filter part was gunked up beyond repair, and had to be replaced.

The overall stack can run about $100, bought new.  I didn't yet want to spend that kind of money, so I searched around quite a bit, and finally found a filter that was close in size, but much cheaper.  The original filter was around 14" long, 6.5" wide, 2" deep.

The replacement filter I chose was a KJ190.  It cost $12.99 and comes with spare pre-filter layers.

The KJ190 is about 13.4" x 10.6" x 1.33".  I couldn't find anything similar and cheap for the 2" depth.  

After a bit of deconstruction, I found filter part is more like 13-3/16" long (est 335mm), not 13.4".  I'm pretty sure they state 13.4" because that includes the size of a thin insulation strip.

The overall construction is like this:

The inbound layer is a thin mesh of some sort as a pre-filter.  It's held to the main frame by Velcro strips that are glued the main frame's surface. 


The frame itself has a kind of cardboard frame.  Inside it, you have the HEPA paper-type pleat.  

Beneath the HEPA part, there's a plastic honeycomb that is sandwiched between nylon-type mesh.  Each honeycomb cell holds activate carbon bits (typically 3 to 5 little pegs per cell).  The mesh is glued to the honeycomb to keep the carbon bits from wandering.

For my purposes, I wanted to cut the filter down from 10.6" to 6.5" width.

In order to do that, the first step was to cut through the outermost nylon mesh, and peel it back.  I measured the size roughly by putting the old filter atop the new one, and making marks.  Removing some of the nylon, allowed me to empty out most of the activated carbon pegs.  I say "most", because there were still honeycomb cells under the paper frame that I couldn't get to.


 

I used a box cutter to cut through the paper frame at the mark points.  On each surface, I cut at a 45 degree angle in anticipation of rebuilding that edge.  Then, I peeled back the side frame, and removed any remaining carbon bits.

Next, I did a test cut of the filter, using my bandsaw.  Then, satisfied with how the test went, I cut through the filter from end to end, again using the bandsaw.  That cut could be done pretty easily manually without any kind of sled or guide.

This is the part that was cut off with the carbon pellets removed.


 

With that done, I tore apart the portion that I wasn't using, and pulled off the Velcro strip.

At this point, I did a test fit, putting it back into the original filter stack.


 

Next, I modeled a piece of for the side so that I could replace the frame edge.  It started as a rectangle, 32mm (filter thickness) x 335mm length.  Then, I added 12.6mm-wide strips to the top and bottom, but cut them off at a 45 degree angle (so the top and bottom each have a trapezoid added).

 

I exported that as .svg, and then added a "score" box.  When doing the laser cut, I run the score line first, followed by the cut.


I had an old piece of black posterboard from the Dollar Tree, so I cut a chunk of that so it'd fit in the FSL Hobby Laser (CO2).


 

For engraving, I used 90% speed, 5% power.  For cutting, I used 80% speed, 15% power.  In both cases, I had the current meter turned 4 full revolutions.  That cut all the way through most of the time.  At 20% power, it cuts through cleanly, for sure, but there's more significant flaming.  I forgot to turn on air assist, but it cut through well.

This picture shows two cuts that made it through, and one that had the cut power too low.

Scoring close-up


 

I pre-folded the piece along the score lines, ending up putting the score lines on the inside for aesthetic reasons.

I tried different ways of gluing it, but in the end I think I could have done better this way:

1. Remove the current pre-filter so that it's not in the way.  (In my initial run on this, I only peeled it back, and then used a binder clip to keep it peeled out of the way while gluing.)

2. Cut off a small portion of the Velcro on each side -- enough so that the salvaged long Velcro strip could be glued over the corner later, without running into the side Velcro.

3.  Tape the end of the laser-cut strip on one side in order to maintain alignment

4.  Hot-glue the middle section (the long rectangle) of the strip to the filter paper

5.  Tape down the not-yet-taped end of the strip to prevent movement.

6.  Bend each folded portion of the strip outward, apply hot glue, and then bring it down to the honeycomb.  On each side, the layering is strip, hot glue, nylon, honeycomb.

7.  Glue the long strip of honeycomb back on to the long end.  It should run from end to end, going over the 45-degree cut lines.


 

8.  Apply dryer duct tape (the thin, silvery kind) where necessary to tighten things up.

With all that done, I had a pretty good replacement filter, except that 

1.  It's not as tall/thick as the original.  That's ok.  I can run with a thin one for now, or add another one later.

2.  It's not long enough.  Mine is 13-3/16" long, but the original was more like 14".

To fix the gaps in the length, I had some spare 3/8"-thick, adhesive insulation strips that I got for free at a garage sale.  I cut three sections to the needed filter width.  I added two on one end, and one on the other, and ended up with a decent fit.


 

 Finished replacement, installed in stack


 All said and done, I turned on the fan, and got good airflow.  Now, whether or not that's really "good" remains a question.  In the end, I was able to fit the KJ190 filter into the original stack quite well, but it's running with a much thinner HEPA layer than the original.  Still, I was able to do this at about 1/8th the price of a whole new stack, and I ended up getting a few spare pre-filter layers.

Next steps

1. Perhaps get a second one and double-stack it

2. Do some test cutting to see how effective it really is at clearing smoke.  Plus, see how dirty it gets over time.

3. Consider replacing the original "wool" pre-filter layer with the aquarium filter material described in YouTube. 

Another option

Well, after doing what I did to the KJ190, I felt like it freed me up to tear apart the filter that normally comes inside the original filter stack, and see what makes it tick.

As it turns out, it's a much simpler thing.  Many filters you buy these days, the KJ190 included, have a "2-in-1" or "3-in-1" marketing tag.  The KJ190 is a 3-in-1, in that it has a pre-filter, the HEPA area, and the carbon pellets.  The trade-off is that you end up with a thinner HEPA pleat.  I think mine is probably only about 0.5" thick.

When I tore apart the original, I found that it's just a box containing a much thicker HEPA pleat -- probably somewhere a little under 2" -- and two metal grates to keep it in place on both sides.

As such, I think I could use an F1 filter.  That one is touted to be 6.7" or 6.75" wide, 12 inches long, and 1.7" thick.  It appears to be a plain old paper frame with a HEPA pleat inside.  That's much closer to the original.  I think one of those could be plopped right into the filter stack, and then I'd need 2" (1" on each end) of padding to fill out to the original 14" dimensions, and then maybe some thin insulation strip around the edges in case there's a gap between the 6.7" F1 box and the original.

The only catch is that in some cases, if you try to order the wrong one, it will say that it cannot be shipped to California because of some clean air law.  But, if you click around a few times, you still might find one (or a pack of them) that doesn't trigger that limitation in Amazon.

The F1 filter

I ordered a set of F1 filters, and they're pretty darned close and should be a lot easier to use than the KJ190. 


 

First off, the KJ190 is a 3-in-1 filter.  The thing I'm replacing is just the HEPA part.  That's what the F1 filter is.  It's 12" x 6.5" x 1.7", roughly.  As such, it is almost a perfect width-fit compared to the original, and probably close enough for thickness to match the original.  (The original had a good, hard metal mesh on both sides, acconuting for some of the difference in thickness.)  


 

This is an example drop-in of the F1 inside the original filter stack box. 


With inner calipers, I measured the gap at 2.2" in the "length" dimension.  

There also was a bit of wiggle in the "width" dimension, but that can be solved with my existing insulation.

I have two options for fixing the length.  One is to cut something out -- maybe garage door insulation foam, or even a hunk of wood -- to the 2.2" x 6.5" x 1.7+" dimensions.  Maybe cut that in half so I have two spacers, and then seal things up with dryer duct tape.

Another is to take a second F1 filter, and hack away a 2.2" piece from it, re-doing the laser-cut solution I had for the KJ190.  That would be kind of brutal.  On the downside, I'd give up a full filter, and I'd lose about 0.5" to the paper borders.  But on the positive side, I'd add 1.25" to 1.5" of extra air flow, compared to just using spacers.