Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Decisions

Resolved design decisions and their rationale.

UI Framework

Decision: Dioxus 0.7+ with web as primary target.

Rationale: Single Rust codebase for web (WASM), desktop, and mobile. React-like RSX syntax with Tailwind CSS styling. Web target compiles to static files (HTML + JS + WASM), hostable on GitHub Pages with zero backend. Largest Rust GUI community, YC-backed with full-time team. See Requirements.

Alternative considered: egui – simpler, faster to prototype, but produces developer-tool aesthetics, not consumer-facing web apps. If Dioxus proves too painful, falling back to egui + eframe is reasonable.

Image Processing Library

Decision: imageproc 0.26 with default-features = false.

Rationale: Provides Canny edge detection, Gaussian blur, and contour tracing out of the box. WASM-compatible when default features (rayon, fft) are disabled. Maintainers actively test against WASM. See Requirements.

Path Simplification

Decision: Hand-written Ramer-Douglas-Peucker (~30 lines).

Rationale: RDP is a trivial algorithm. Importing the geo crate for a single algorithm pulls in a large dependency tree. Self-implementation avoids unnecessary dependencies and keeps the WASM binary small.

File Upload

Decision: Dioxus built-in file events (onchange/ondrop).

Rationale: Cross-platform, zero extra dependencies. Dioxus wraps the HTML <input type="file"> and drag-drop events with a unified API that works on both web and desktop. No need for the rfd crate.

Preview Rendering

Decision: Inline SVG elements in the Dioxus DOM.

Rationale: Simplest approach – no HTML canvas, no JavaScript interop, no web-sys for rendering. Dioxus supports SVG elements natively in RSX. Vector rendering is crisp at any zoom level. For very complex paths, the display version uses higher RDP tolerance to keep the DOM lightweight.

File Downloads (WASM)

Decision: web-sys Blob + object URL + temporary <a> click.

Rationale: Type-safe Rust bindings. Handles both string and binary data natively. No JavaScript string escaping issues (unlike document::eval).

Threading Strategy

Decision: Main thread for MVP, web workers deferred.

Rationale: Simpler setup. Loading indicator shown during processing. Move to web workers if UI blocking proves unacceptable with real-world images.

Path Optimization

Decision: Nearest-neighbor TSP on contours with direction reversal.

Rationale: Simple, effective for sand table output quality. Image2Sand uses the same approach. 2-opt improvement deferred as a future enhancement.

Deployment Target

Decision: GitHub Pages with custom domain (mujou.art), app at /app/ path.

Rationale: Simplest option that avoids adding a vendor. The code is already on GitHub, so Pages is a setting on the same repo rather than a new account and billing relationship. Free tier (100GB bandwidth/mo) is sufficient for a niche tool. Build pipeline uses GitHub Actions, which is needed for CI anyway. Static sites are inherently portable – if GitHub Pages becomes insufficient, migrating to any other static host requires only changing the deploy target.

Alternatives considered:

  • Cloudflare Pages – faster CDN, generous free tier, pairs with R2 for blob storage. Rejected to avoid Cloudflare platform lock-in; each additional Cloudflare service (Workers, KV, R2, D1) increases coupling. The performance difference is negligible for a niche tool.
  • Netlify – best deploy DX (preview deploys, form handling, split testing). Rejected because its differentiating features (forms, serverless functions) are unused by mujou, and it adds a vendor for no capability gain over GitHub Pages. Bandwidth overages are billed at $55/100GB.
  • Subdomains (app.mujou.art) – preferred over path-based routing but requires either two repos (one per GitHub Pages site) or a different hosting provider. Path-based (/app/) is acceptable and keeps everything in one repo. Revisit if hosting provider changes.

Configuration: See Requirements.

Reference Target Device

Decision: Default pipeline resolution and parameters target a ~34“ (850mm) diameter sand table with a ~5mm effective track width.

Rationale:

Market survey (Feb 2026)

The kinetic sand table market spans desktop toys to furniture-scale pieces. Key products by sand area diameter:

BrandModelSand DiameterPriceNotes
Oasis MiniDesktop9“ / 23cm$129-149Best seller, 50k+ units shipped
SANDSARA miniDesktop~8“ / 20cm$169-180
Sisyphus Mini ESDesktop9.9“ / 25cm$690
SANDSARA Dark WalnutDesktop~14“ / 36cm$750
HoMedics Drift 16“Desktop16“ / 41cm$319
Sisyphus Metal SideSide table16“ / 41cm$1,780
Oasis Side TableSide table20“ / 50cm$399-499Pre-order, ships 2026
HoMedics Drift 21“Desktop21“ / 53cm$500
Sisyphus Metal CoffeeCoffee table27.25“ / 69cm$2,640
Oasis Coffee TableCoffee table34“ / 85cm$799-999Pre-order, ships 2026

The Oasis Coffee Table (34“ / 850mm) is the largest mainstream table. It is also under $1,000, making it the largest table likely to see significant volume.

Effective track width (~5mm)

All these tables use a steel ball (~12mm diameter) dragged magnetically through sand. The ball is a sphere, so the groove it carves is narrower than the ball diameter – only the contact chord at the depth the ball sinks matters:

  • 0.5mm sink depth: track width ≈ 4.8mm
  • 1.0mm sink depth: track width ≈ 6.6mm
  • 2.0mm sink depth: track width ≈ 9.0mm

We use 5mm as the working estimate for effective track width.

Resolvable detail vs. positional precision

The 5mm track width constrains two different things:

  1. Minimum line spacing – two parallel lines must be ≥5mm apart to read as distinct features. For a 34“ (850mm) table this gives ~170 independent lines across the diameter.

  2. Positional precision – a single line’s position can be controlled much finer than 5mm. A gently curving or slightly angled line benefits from sub-track-width resolution, the same way anti-aliased text benefits from sub-pixel positioning. Coarse quantization would produce visible staircase artifacts on gentle curves.

This means the useful processing resolution is higher than 170px – we need enough resolution for smooth contour positioning, even though the output can’t resolve features closer than ~5mm.

Resolvable lines per table

At 5mm track width:

TableDiameterIndependent lines across
Oasis Mini9“ / 230mm~46
Oasis Side Table20“ / 500mm~100
Sisyphus Metal Coffee27.25“ / 690mm~138
Oasis Coffee Table34“ / 850mm~170

Pipeline resolution strategy

MVP approach: Downsample the input image early (after decode) to a working resolution of ~256px on the long axis. Run the full pipeline (grayscale, blur, Canny, contour tracing, simplification, masking, joining) at this resolution. This is ~1.5x oversampling relative to the ~170 resolvable lines on the largest common table, which provides some headroom for smooth contour positioning without processing pixels that can never produce visible detail.

At 256x256 (65k pixels) vs 1024x1024 (1M pixels), the expensive stages (blur, Canny) should run ~16x faster.

Positional precision on gentle curves will be limited to the ~3.3mm grid spacing (850mm / 256px). This may produce visible staircase artifacts on the largest tables. Acceptable for MVP; evaluate with real output.

Deferred: coarse-then-fine with region masking. A coarse pass at low resolution identifies where edges exist, producing a binary mask of “interesting” regions. A second fine-resolution pass runs only in unmasked regions, skipping the ~99% of the image that is featureless. This avoids full-image high-res cost while preserving sub-pixel positional precision where edges actually occur. Simpler than a tiling approach (no stitching across tile boundaries). See Open Questions.

Project Architecture

Decision: Sans-IO with three-layer Cargo workspace. See Principles and Architecture.

Rationale: Matches patterns established in the onshape-mcp project. Core crates are testable without a browser or WASM runtime. Clear separation between pure logic and platform I/O.