atproto Thingiverse but good
10

Configure Feed

Select the types of activity you want to include in your feed.

PM-49: client-side 3MF parsing

Orual (Jun 29, 2026, 7:06 PM EDT) 8a297da9 52d381d3

+94 -187
+2 -1
AGENTS.md
··· 67 67 68 68 - **Run `just build-renderer-worker` before `dx serve`** (or rely on the `just serve` guard, which builds the worker if `public/renderer_worker_bg.wasm` is missing). Without the worker artifacts, the viewer route will fail silently. 69 69 - **Worker artifacts:** `public/renderer_worker.js` + `public/renderer_worker_bg.wasm` are generated by `wasm-bindgen --target web`; `public/renderer_worker_loader.js` is hand-authored (the ES-module wrapper that calls `init()`). The `.js` and `.wasm` are gitignored; the loader is committed. 70 - - **Crate structure:** `polymodel-renderer-protocol` (leaf: `MeshFormat`/`LengthUnit`/`Units`/message types + postcard) → `polymodel-mesh` (STL/OBJ/glTF parsing, depends on `three-d-asset`) → `polymodel-renderer-worker` (`[[bin]]` wasm target, depends on `three-d` + `polymodel-mesh`). The app depends only on `polymodel-renderer-protocol`. 70 + - **Crate structure:** `polymodel-renderer-protocol` (leaf: `MeshFormat`/`LengthUnit`/`Units`/message types + postcard) → `polymodel-mesh` (STL/OBJ/glTF/3MF parsing, depends on `three-d-asset`) → `polymodel-renderer-worker` (`[[bin]]` wasm target, depends on `three-d` + `polymodel-mesh`). The app depends only on `polymodel-renderer-protocol`. 71 71 - **Invariant:** `cargo tree -p polymodel --target wasm32-unknown-unknown --features web --edges normal` must show zero `three-d`, `three-d-asset`, `stl_io`, or `polymodel-mesh` entries. `just verify-renderer-split` checks this. 72 72 - **Build profile:** `worker-release` (inherits `release`, `opt-level = 3`, `lto = "fat"`, `panic = "abort"`) — the worker renders frames, so hot-path speed matters more than initial bytes. 73 + - **Supported viewer formats:** STL, OBJ, glTF (GLB/embedded/multi-file), and 3MF all parse client-side in the worker via `three-d-asset`. STEP converts server-side to glTF via opencascade.js. The `RemoteMeshParser` seam in `polymodel-mesh` is reserved for future server-side format additions. 73 74 74 75 ### Front-end implementation notes 75 76
crates/polymodel-mesh/public/models/3DBenchy.3mf

This is a binary file and will not be displayed.

crates/polymodel-mesh/public/models/cylinder.3mf

This is a binary file and will not be displayed.

crates/polymodel-mesh/public/models/pyramid_vertexcolor.3mf

This is a binary file and will not be displayed.

+3 -2
crates/polymodel-mesh/src/contract.rs
··· 70 70 /// non-finite position, or a point-sized bounding box. 71 71 #[error("mesh is degenerate")] 72 72 Degenerate, 73 - /// The format is not supported by the active parser. `Threemf` returns this when no 74 - /// remote parser is wired in (PM-49 will supply one). 73 + /// The format is not supported by the active parser. Returned by the 74 + /// [`RemoteMeshParser`](crate::parser::RemoteMeshParser) seam when no conversion is 75 + /// wired in (reserved for future server-side formats like STEP). 75 76 #[error("unsupported mesh format")] 76 77 UnsupportedFormat, 77 78 }
+7 -5
crates/polymodel-mesh/src/lib.rs
··· 1 - //! Multi-format mesh ingestion: STL/OBJ/glTF parsing into a renderer-agnostic [`TriMesh`]. 1 + //! Multi-format mesh ingestion: STL/OBJ/glTF/3MF parsing into a renderer-agnostic 2 + //! [`TriMesh`]. 2 3 //! 3 4 //! Defines [`ModelMesh`] (in [`contract`]), a thin Polymodel wrapper over 4 5 //! [`three_d_asset::TriMesh`] that carries format provenance, unit assumptions, and a 5 - //! camera framing hint. STL, OBJ, and glTF (GLB, embedded-base64, or multi-file `.gltf` 6 - //! with external companions) are parsed from in-memory bytes via `three-d-asset`, so this 7 - //! crate compiles to `wasm32-unknown-unknown`. 3MF cannot target `wasm32` and is routed 8 - //! to a remote parser (PM-49); the parser seam in [`parser`] is remote-ready. 6 + //! camera framing hint. STL, OBJ, glTF (GLB, embedded-base64, or multi-file `.gltf` 7 + //! with external companions), and 3MF are all parsed from in-memory bytes via 8 + //! `three-d-asset`, so this crate compiles to `wasm32-unknown-unknown`. The parser seam 9 + //! in [`parser`] retains a [`RemoteMeshParser`](parser::RemoteMeshParser) trait for 10 + //! future server-side formats (e.g. STEP via opencascade.js). 9 11 //! 10 12 //! [`MeshFormat`] / [`LengthUnit`] / [`Units`] are owned by [`polymodel_renderer_protocol`] 11 13 //! (the leaf crate shared with the app) to avoid a circular dependency.
+36 -60
crates/polymodel-mesh/src/parser.rs
··· 1 - //! Parser seam — shared parse boundary, format dispatch, and the remote drop-in hook. 1 + //! Parser seam — shared parse boundary and format dispatch. 2 2 //! 3 3 //! The data types here ([`MeshResources`], [`MeshSource`]) are the only thing that 4 4 //! crosses the parse boundary. The dispatcher [`load_mesh`] is synchronous; the viewer is 5 - //! the sole async layer (it prefetches glTF companions). 3MF cannot compile to `wasm32`, 6 - //! so it is served by a remote parser (PM-49); [`RemoteMeshParser`] + the `Threemf` arm 7 - //! are the drop-in site, and wiring them in does not change the [`MeshParser`] trait 8 - //! shape or the viewer pipeline. 5 + //! the sole async layer (it prefetches glTF companions). All four formats 6 + //! (STL/OBJ/glTF/3MF) parse client-side in the worker via `three-d-asset`. 9 7 10 8 use crate::contract::{MeshFormat, MeshLoadError, ModelMesh}; 11 9 ··· 55 53 56 54 /// Synchronously parse a [`MeshSource`] into a [`ModelMesh`], dispatching by format. 57 55 /// 58 - /// `Stl`/`Obj`/`Gltf` are parsed from the in-memory bytes. `Threemf` is not supported 59 - /// here — it is routed to a remote parser (PM-49) by [`DefaultMeshParser`]. 56 + /// `Stl`/`Obj`/`Gltf`/`Threemf` are all parsed from the in-memory bytes via 57 + /// `three-d-asset`. 60 58 pub fn load_mesh(source: &MeshSource<'_>) -> Result<ModelMesh, MeshLoadError> { 61 59 match source.format { 62 60 MeshFormat::Stl => crate::stl::load_stl(source.primary), 63 61 MeshFormat::Obj => crate::obj::load_obj(source.primary), 64 62 MeshFormat::Gltf => crate::gltf::load_gltf(source), 65 - MeshFormat::Threemf => Err(MeshLoadError::UnsupportedFormat), 63 + MeshFormat::Threemf => crate::threemf::load_threemf(source.primary), 66 64 } 67 65 } 68 66 ··· 79 77 async fn parse(&self, source: &MeshSource<'_>) -> Result<ModelMesh, MeshLoadError>; 80 78 } 81 79 82 - /// A remote (server-side) parser for formats that cannot compile to `wasm32` (3MF). 80 + /// A remote (server-side) parser for formats that require conversion before rendering. 81 + /// 82 + /// Reserved for future server-side formats (e.g. STEP via opencascade.js → GLB). Not used 83 + /// by any current format — STL/OBJ/glTF/3MF all parse client-side. 83 84 /// 84 85 /// **Contract:** [`RemoteMeshParser::convert`] MUST return a **self-contained GLB** 85 - /// (embedded buffer), because the `Threemf` arm re-enters the pipeline via `Gltf` with no 86 + /// (embedded buffer), because the remote arm re-enters the pipeline via `Gltf` with no 86 87 /// companion resources. 87 88 pub trait RemoteMeshParser { 88 89 /// Convert the source bytes (e.g. a `.3mf`) into self-contained GLB bytes. ··· 90 91 async fn convert(&self, bytes: &[u8]) -> Result<Vec<u8>, MeshLoadError>; 91 92 } 92 93 93 - /// No remote parser wired in: `Threemf` is unsupported until PM-49 supplies one. 94 + /// No remote parser wired in: the seam returns [`MeshLoadError::UnsupportedFormat`] until 95 + /// a server-side format (e.g. STEP) supplies one. 94 96 pub struct Local; 95 97 96 98 impl RemoteMeshParser for Local { ··· 99 101 } 100 102 } 101 103 102 - /// Production parser: routes by format. `Stl`/`Obj`/`Gltf` parse in-memory; `Threemf` 103 - /// delegates to a [`RemoteMeshParser`] (converting to a self-contained GLB, then 104 - /// re-entering via `Gltf` with no companions). 104 + /// Production parser: routes by format. All formats (`Stl`/`Obj`/`Gltf`/`Threemf`) 105 + /// parse in-memory via `three-d-asset`. Kept for compatibility with call sites that 106 + /// construct it; `load_mesh` is the direct path. 105 107 pub struct DefaultMeshParser<R: RemoteMeshParser = Local> { 108 + /// Reserved for server-side formats (STEP via opencascade.js). Not read by any 109 + /// current `MeshFormat` arm — all four parse client-side. 110 + #[allow(dead_code)] 106 111 remote: R, 107 112 } 108 113 ··· 122 127 impl<R: RemoteMeshParser> MeshParser for DefaultMeshParser<R> { 123 128 async fn parse(&self, source: &MeshSource<'_>) -> Result<ModelMesh, MeshLoadError> { 124 129 match source.format { 125 - MeshFormat::Stl | MeshFormat::Obj | MeshFormat::Gltf => load_mesh(source), 126 - MeshFormat::Threemf => { 127 - let glb = self.remote.convert(source.primary).await?; 128 - let reentry = MeshSource { 129 - format: MeshFormat::Gltf, 130 - primary: &glb, 131 - resources: MeshResources::empty(), 132 - }; 133 - load_mesh(&reentry) 130 + MeshFormat::Stl | MeshFormat::Obj | MeshFormat::Gltf | MeshFormat::Threemf => { 131 + load_mesh(source) 134 132 } 135 133 } 136 134 } ··· 148 146 } 149 147 150 148 #[test] 151 - fn load_mesh_dispatches_each_format_and_rejects_threemf() { 149 + fn load_mesh_dispatches_each_format() { 152 150 let mut stl = Vec::new(); 153 151 stl.extend_from_slice(&[0u8; 80]); 154 152 stl.extend_from_slice(&4u32.to_le_bytes()); ··· 193 191 .expect("GLB dispatches to load_gltf"); 194 192 assert_eq!(gltf_mesh.format, MeshFormat::Gltf); 195 193 196 - assert!(matches!( 197 - load_mesh(&MeshSource { 198 - format: MeshFormat::Threemf, 199 - primary: b"not real 3mf", 200 - resources: MeshResources::empty(), 201 - }), 202 - Err(MeshLoadError::UnsupportedFormat) 203 - )); 204 - } 205 - 206 - #[test] 207 - fn production_parser_with_no_remote_rejects_threemf() { 208 - let parser = DefaultMeshParser::default(); 209 - let source = MeshSource { 194 + let threemf = fixture("cube_gears.3mf"); 195 + let threemf_mesh = load_mesh(&MeshSource { 210 196 format: MeshFormat::Threemf, 211 - primary: b"not real 3mf", 197 + primary: &threemf, 212 198 resources: MeshResources::empty(), 213 - }; 214 - let rt = tokio::runtime::Builder::new_current_thread() 215 - .enable_all() 216 - .build() 217 - .unwrap(); 218 - let result = rt.block_on(parser.parse(&source)); 219 - assert!(matches!(result, Err(MeshLoadError::UnsupportedFormat))); 220 - } 221 - 222 - struct StubRemote; 223 - 224 - impl RemoteMeshParser for StubRemote { 225 - async fn convert(&self, _bytes: &[u8]) -> Result<Vec<u8>, MeshLoadError> { 226 - Ok(fixture("box.glb")) 227 - } 199 + }) 200 + .expect("3MF dispatches to load_threemf"); 201 + assert_eq!(threemf_mesh.format, MeshFormat::Threemf); 202 + assert_eq!(threemf_mesh.trimesh.triangle_count(), 25692); 228 203 } 229 204 230 205 #[test] 231 - fn production_parser_with_stub_remote_parses_threemf_via_glb() { 232 - let parser = DefaultMeshParser::new(StubRemote); 206 + fn production_parser_parses_threemf_locally() { 207 + let parser = DefaultMeshParser::default(); 208 + let threemf = fixture("cube_gears.3mf"); 233 209 let source = MeshSource { 234 210 format: MeshFormat::Threemf, 235 - primary: b"fake 3mf bytes", 211 + primary: &threemf, 236 212 resources: MeshResources::empty(), 237 213 }; 238 214 let rt = tokio::runtime::Builder::new_current_thread() ··· 241 217 .unwrap(); 242 218 let mesh = rt 243 219 .block_on(parser.parse(&source)) 244 - .expect("stub remote -> GLB -> ModelMesh"); 245 - assert_eq!(mesh.format, MeshFormat::Gltf); 246 - assert_eq!(mesh.trimesh.triangle_count(), 12); 220 + .expect("3MF parses locally"); 221 + assert_eq!(mesh.format, MeshFormat::Threemf); 222 + assert_eq!(mesh.trimesh.triangle_count(), 25692); 247 223 } 248 224 }
+23 -102
crates/polymodel-mesh/src/threemf.rs
··· 1 1 //! 3MF ingestion: parse in-memory bytes into a [`ModelMesh`] via `three-d-asset`. 2 2 //! 3 + //! `three-d-asset` dispatches `.3mf` through its `3mf` feature (`lib3mf`, pure Rust), 4 + //! which reads the OPC container, parses the model XML, applies object transforms, and 5 + //! produces a [`TriMesh`](three_d_asset::TriMesh) with `Positions::F32` and 6 + //! `Indices::U32`. Multi-object builds, transforms, and components are merged into a 7 + //! single [`Model`] by `three-d-asset`; `flatten_model` concatenates them. 8 + //! 3 9 //! 3MF's spec-default unit is the millimetre, and `three-d-asset` does not surface 4 10 //! `model.unit` on the `Scene`, so the loader reports [`Units::declared`] millimetres — 5 - //! correct for the vast majority of 3MF files. Multi-object builds, transforms, and 6 - //! components are merged by `three-d-asset` into a single [`Model`]; colors, textures, and 7 - //! beam lattices render as geometry only in v1. There is no filesystem or network access — 8 - //! this is safe to call from a `wasm32-unknown-unknown` build. 11 + //! correct for the vast majority of 3MF files. Colors, textures, and beam lattices render 12 + //! as geometry only in v1 (the worker's `PhysicalMaterial` ignores them). There is no 13 + //! filesystem or network access — this is safe to call from a `wasm32-unknown-unknown` 14 + //! build. 9 15 10 16 use three_d_asset::io; 11 17 ··· 13 19 14 20 /// Parse 3MF bytes into a [`ModelMesh`]. 15 21 /// 16 - /// `three-d-asset` dispatches `.3mf` through its `3mf` feature (`lib3mf`), which reads the 17 - /// OPC container, parses the model XML, applies object transforms, and produces a 18 - /// [`TriMesh`](three_d_asset::TriMesh) with `Positions::F32` and `Indices::U32`. The shared 19 - /// validator derives geometry normals and checks for degeneracy. 22 + /// The shared validator derives geometry normals and checks for degeneracy (no triangles, 23 + /// non-finite positions, point-sized AABB). 3MF declares its unit in the model XML; the 24 + /// spec default is millimetres. `three-d-asset` does not surface the unit attribute, so the 25 + /// declared default is reported. 20 26 pub fn load_threemf(bytes: &[u8]) -> Result<ModelMesh, MeshLoadError> { 21 27 if bytes.is_empty() { 22 28 return Err(MeshLoadError::Empty); ··· 45 51 } 46 52 47 53 #[test] 48 - fn parses_consortium_cylinder_fixture() { 49 - let model = load_threemf(&fixture("cylinder.3mf")).expect("cylinder.3mf should parse"); 54 + fn parses_cube_gears_fixture() { 55 + let model = load_threemf(&fixture("cube_gears.3mf")).expect("cube_gears.3mf should parse"); 50 56 assert_eq!(model.format, MeshFormat::Threemf); 51 - assert_eq!( 52 - model.units, 53 - Units::declared(LengthUnit::Millimeter) 54 - ); 55 - assert!(model.trimesh.triangle_count() > 0); 57 + assert_eq!(model.units, Units::declared(LengthUnit::Millimeter)); 58 + // cube_gears is a 17-object build; after flattening it's 25,692 triangles. 59 + assert_eq!(model.trimesh.triangle_count(), 25692); 56 60 assert!(matches!(model.trimesh.positions, Positions::F32(_))); 57 61 let aabb = model.aabb(); 58 62 let size = aabb.size(); ··· 82 86 #[test] 83 87 fn error_parse_garbage() { 84 88 let garbage = b"this is definitely not a valid 3MF file !@#$"; 85 - assert!(matches!(load_threemf(garbage), Err(MeshLoadError::Decode(_)))); 86 - } 87 - 88 - /// Real-world correctness spike: a 3MF with 82,122 triangles from real scan geometry 89 - /// (`body_f_chest-v4.stl` converted to 3MF). The gate is correct geometry through the 90 - /// shipping loader path. Run explicitly: `cargo test -p polymodel-mesh threemf_spike -- --ignored --nocapture`. 91 - #[test] 92 - #[ignore] 93 - fn threemf_spike_real_world_mesh() { 94 - let bytes = fixture("body_chest.3mf"); 95 - let model = load_threemf(&bytes).expect("real-world 3MF should parse"); 96 - 97 - assert_eq!(model.format, MeshFormat::Threemf); 98 - assert_eq!(model.units, Units::declared(LengthUnit::Millimeter)); 99 - 100 - let triangles = model.triangle_count(); 101 - let vertices = model.vertex_count(); 102 - eprintln!("body_chest.3mf: {vertices} vertices, {triangles} triangles"); 103 - 104 - // The source STL has 82,122 triangles and 41,051 unique vertices. 105 - assert_eq!(triangles, 82122, "triangle count must match source STL"); 106 - assert_eq!(vertices, 41051, "vertex count must match source STL"); 107 - 108 - // Finite AABB 109 - let aabb = model.aabb(); 110 - let center = aabb.center(); 111 - let size = aabb.size(); 112 - eprintln!("AABB center: {center:?}, size: {size:?}"); 113 - assert!( 114 - center.x.is_finite() && center.y.is_finite() && center.z.is_finite(), 115 - "AABB center must be finite" 116 - ); 117 - assert!( 118 - size.x > 0.0 && size.y > 0.0 && size.z > 0.0, 119 - "AABB must be non-degenerate" 120 - ); 121 - 122 - // Normals populated and finite 123 - let normals = model 124 - .trimesh 125 - .normals 126 - .as_ref() 127 - .expect("normals populated"); 128 - assert_eq!(normals.len(), vertices as usize); 129 - assert!( 130 - normals 131 - .iter() 132 - .all(|n| n.x.is_finite() && n.y.is_finite() && n.z.is_finite()), 133 - "all normals must be finite" 134 - ); 135 - } 136 - } 137 - 138 - #[cfg(test)] 139 - mod spike_debug { 140 - use three_d_asset::io; 141 - use three_d_asset::{Geometry, Model}; 142 - 143 - fn fixture(name: &str) -> Vec<u8> { 144 - let path = format!("{}/public/models/{name}", env!("CARGO_MANIFEST_DIR")); 145 - std::fs::read(&path).unwrap_or_else(|_| panic!("fixture {name} at {path}")) 146 - } 147 - 148 - #[test] 149 - #[ignore] 150 - fn debug_parse_all_3mf() { 151 - for name in &["cylinder.3mf", "cube_gears.3mf", "pyramid_vertexcolor.3mf", "3DBenchy.3mf"] { 152 - let bytes = fixture(name); 153 - match io::deserialize::<Model>("asset.3mf", bytes) { 154 - Ok(model) => { 155 - let tv: usize = model.geometries.iter().map(|p| match &p.geometry { 156 - Geometry::Triangles(m) => m.positions.len(), 157 - _ => 0, 158 - }).sum(); 159 - let tt: usize = model 160 - .geometries 161 - .iter() 162 - .filter_map(|p| match &p.geometry { 163 - Geometry::Triangles(m) => Some(m.triangle_count()), 164 - _ => None, 165 - }) 166 - .sum(); 167 - eprintln!("{name}: OK — {} geometries, {} verts, {} tris", model.geometries.len(), tv, tt); 168 - } 169 - Err(e) => eprintln!("{name}: ERR — {e}"), 170 - } 171 - } 89 + assert!(matches!( 90 + load_threemf(garbage), 91 + Err(MeshLoadError::Decode(_)) 92 + )); 172 93 } 173 94 }
+6 -6
crates/polymodel-renderer-protocol/src/lib.rs
··· 17 17 18 18 /// Source format of a loaded mesh. 19 19 /// 20 - /// STL, OBJ, and glTF (GLB, embedded, or multi-file `.gltf` with external buffers) are 21 - /// parsed in the worker. `Threemf` is reserved: 3MF cannot compile to `wasm32`, so it is 22 - /// served by a remote parser (PM-49). There is no binary/ASCII sub-label for STL — 23 - /// reliably telling them apart is hard, and both yield the same mesh. 20 + /// STL, OBJ, glTF (GLB, embedded, or multi-file `.gltf` with external buffers), and 3MF 21 + /// are all parsed client-side in the worker via `three-d-asset`. There is no 22 + /// binary/ASCII sub-label for STL — reliably telling them apart is hard, and both yield 23 + /// the same mesh. 24 24 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 25 25 pub enum MeshFormat { 26 26 /// Stereolithography (binary or ASCII). ··· 29 29 Obj, 30 30 /// glTF 2.0 — GLB, embedded-base64 `.gltf`, or multi-file `.gltf` + companions. 31 31 Gltf, 32 - /// 3D Manufacturing Format. Not parsed client-side; routed to a remote parser 33 - /// (PM-49). Kept here so format detection and the parser seam are remote-ready. 32 + /// 3D Manufacturing Format. Parsed client-side in the worker via `three-d-asset`'s 33 + /// `3mf` feature (`lib3mf`, pure Rust). 34 34 Threemf, 35 35 } 36 36
+2 -7
crates/polymodel-renderer-worker/src/worker.rs
··· 17 17 18 18 use polymodel_mesh::companions::{discover_companion_uris, is_safe_relative_uri}; 19 19 use polymodel_mesh::contract::ModelMesh; 20 - use polymodel_mesh::parser::{DefaultMeshParser, Local, MeshParser, MeshResources, MeshSource}; 20 + use polymodel_mesh::parser::{MeshResources, MeshSource, load_mesh}; 21 21 use polymodel_renderer_protocol::{ 22 22 KeyModifiers, MeshFormat, MeshStats, RendererCommand, RendererEvent, deserialize_command, 23 23 serialize_event, ··· 424 424 } 425 425 426 426 async fn load_mesh_async(primary_url: &str, format: MeshFormat) -> Result<ModelMesh, String> { 427 - if format == MeshFormat::Threemf { 428 - return Err("unsupported mesh format".into()); 429 - } 430 - 431 427 let primary_bytes = fetch_bytes(primary_url) 432 428 .await 433 429 .map_err(|e| format!("fetch primary: {e}"))?; ··· 464 460 resources: MeshResources::new(&companions), 465 461 }; 466 462 467 - let parser = DefaultMeshParser::<Local>::default(); 468 - parser.parse(&source).await.map_err(|e| e.to_string()) 463 + load_mesh(&source).map_err(|e| e.to_string()) 469 464 } 470 465 471 466 // ---------------------------------------------------------------------------
public/models/cube_gears.3mf

This is a binary file and will not be displayed.

+6 -3
src/main.rs
··· 5 5 use polymodel_api::space_polymodel::actor::get_session::{GetSession, GetSessionOutput}; 6 6 7 7 mod about; 8 - mod author_byline; 9 8 mod auth; 9 + mod author_byline; 10 10 mod browse; 11 11 mod client; 12 12 mod examples; ··· 66 66 let _ = dotenvy::dotenv(); 67 67 let _ = tracing_subscriber::fmt() 68 68 .with_env_filter( 69 - tracing_subscriber::EnvFilter::try_from_default_env() 70 - .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("debug,hydrant=warn")), 69 + tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| { 70 + tracing_subscriber::EnvFilter::new( 71 + "debug,hydrant=warn,h2=warn,hyper_util=warn,rustls=warn", 72 + ) 73 + }), 71 74 ) 72 75 .try_init(); 73 76 // The closure runs inside the server's Tokio runtime at startup, so the
+9 -1
src/viewer.rs
··· 94 94 } 95 95 } 96 96 97 - const DEMO_MODELS: [DemoModel; 2] = [ 97 + const DEMO_MODELS: [DemoModel; 3] = [ 98 98 DemoModel { 99 99 id: "body-f-chest-v4", 100 100 name: "Body F chest v4", ··· 110 110 generation: 1, 111 111 vertices: 18271, 112 112 triangles: 36542, 113 + }, 114 + DemoModel { 115 + id: "cube-gears-3mf", 116 + name: "Cube Gears (3MF)", 117 + asset_path: "/models/cube_gears.3mf", 118 + generation: 1, 119 + vertices: 12864, 120 + triangles: 25692, 113 121 }, 114 122 ]; 115 123