app
Adom Step2GLB - STEP to GLB converter
Public Made by Adomby adom
Color-preserving STEP (.step/.stp) to GLB converter. Thin Rust CLI shelling to a shared OCCT XCAF service. Formerly 'step2glb'.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
//! Thumbnail playground — drag-drop a STEP into a Hydrogen webview, see all
//! 6 × 3 = 18 PNGs populate live. Hits the /thumbnail-batch endpoint via
//! a tiny local proxy (avoids CORS — webview can't talk to the external
//! service directly, has to go through the user's container-proxy URL).
use crate::client;
use crate::ui::{err, hint, ok, CYAN, RESET};
use std::io::Read;
use tiny_http::{Header, Method, Request, Response, Server};
const UI_HTML: &str = include_str!("playground.html");
#[derive(clap::Args, Debug)]
pub struct PlaygroundArgs {
/// Port for the local HTTP server. 0 = auto-pick a free port.
#[arg(long, default_value_t = 0)]
pub port: u16,
/// Skip opening the Hydrogen webview tab (still prints the proxy URL).
#[arg(long)]
pub no_tab: bool,
}
pub fn run(args: PlaygroundArgs) {
let port = if args.port == 0 { pick_free_port() } else { args.port };
let bind = format!("127.0.0.1:{port}");
let server = match Server::http(&bind) {
Ok(s) => s,
Err(e) => {
err(format!("bind {bind}: {e}"));
std::process::exit(4);
}
};
let url = proxy_url(port);
if !args.no_tab {
if let Err(e) = crate::tab::open_webview_tab("step2glb thumb playground", &url) {
eprintln!("(warning) could not open Hydrogen tab: {e}");
}
}
ok(format!(
"thumb playground ready at {CYAN}{url}{RESET} (Ctrl-C to stop)"
));
for req in server.incoming_requests() {
handle(req);
}
}
fn handle(mut req: Request) {
let full = req.url().to_string();
let path = full.split('?').next().unwrap_or(&full).to_string();
match (req.method(), path.as_str()) {
(Method::Get, "/") | (Method::Get, "/index.html") => {
let resp = Response::from_string(UI_HTML)
.with_header(Header::from_bytes("Content-Type", "text/html; charset=utf-8").unwrap());
let _ = req.respond(resp);
}
(Method::Post, "/render") => {
// Body is raw STEP bytes (the page POSTs the file's ArrayBuffer).
let mut body = Vec::with_capacity(1024 * 64);
if let Err(e) = req.as_reader().read_to_end(&mut body) {
let _ = req.respond(
Response::from_string(format!("read body: {e}")).with_status_code(400),
);
return;
}
// Forward to /thumbnail-batch with all-orientations, all-sizes.
// Result includes 18 base64 PNGs.
let result = client::thumbnail_batch_bytes(&body, "all", "all", "iso");
let body_json = match result {
Ok(r) => serde_json::to_string(&serde_json::json!({
"ok": true,
"srcKind": r.src_kind,
"pose": r.pose,
"bboxMm": r.bbox_mm,
"loadDurationMs": r.load_duration_ms,
"totalDurationMs": r.total_duration_ms,
"thumbnailCount": r.thumbnail_count,
"renderWarnings": r.render_warnings,
"thumbnails": r.thumbnails.iter().map(|t| serde_json::json!({
"upAxis": t.up_axis,
"size": t.size,
"width": t.width,
"height": t.height,
"sizeBytes": t.size_bytes,
"dataB64": t.data_b64,
})).collect::<Vec<_>>(),
})).unwrap_or_default(),
Err(e) => serde_json::json!({"ok": false, "error": e}).to_string(),
};
let resp = Response::from_string(body_json)
.with_header(Header::from_bytes("Content-Type", "application/json").unwrap());
let _ = req.respond(resp);
}
(Method::Post, "/shutdown") => {
let _ = req.respond(Response::from_string("bye"));
std::process::exit(0);
}
_ => {
let _ = req.respond(Response::from_string("not found").with_status_code(404));
}
}
}
fn pick_free_port() -> u16 {
let l = std::net::TcpListener::bind("127.0.0.1:0").expect("bind 0");
l.local_addr().unwrap().port()
}
fn proxy_url(port: u16) -> String {
if let Ok(proxy) = std::env::var("VSCODE_PROXY_URI") {
proxy.replace("{{port}}", &port.to_string())
} else {
format!("http://127.0.0.1:{port}/")
}
}