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'.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
//! Preview mode — fetch GLB via the shared service, stash it locally, then
//! spawn a tiny HTTP server that the Hydrogen webview can load (webviews
//! can't reach the external service directly; they need the user's own
//! container-proxy URL).
use crate::cli::PreviewArgs;
use crate::client;
use crate::ui::{err, hint, ok, CYAN, RESET};
use serde_json::json;
use std::fs;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use tiny_http::{Header, Method, Request, Response, Server, StatusCode};
const UI_HTML: &str = include_str!("ui.html");
struct State {
glb_path: PathBuf,
source_label: String,
conversion: serde_json::Value,
}
pub fn run(args: PreviewArgs) {
let (glb_path, conversion_json, source_label) = match resolve(&args.source) {
Ok(v) => v,
Err(e) => {
err(format!("preview failed: {e}"));
hint("Run `step2glb health` to verify the service is reachable.");
std::process::exit(4);
}
};
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 state = Arc::new(Mutex::new(State { glb_path, source_label: source_label.clone(), conversion: conversion_json }));
let url = proxy_url(port);
if !args.no_tab {
let name = format!("step2glb — {}", short_label(&source_label));
if let Err(e) = crate::tab::open_webview_tab(&name, &url) {
eprintln!("(warning) could not open Hydrogen tab: {e}");
}
}
ok(format!("preview ready at {CYAN}{url}{RESET} (Ctrl-C to stop)"));
for req in server.incoming_requests() {
handle(req, &state);
}
}
fn resolve(source: &str) -> Result<(PathBuf, serde_json::Value, String), String> {
let as_path = PathBuf::from(source);
if as_path.exists() {
let out = PathBuf::from(format!("/tmp/step2glb-preview-{}.glb",
as_path.file_stem().map(|s|s.to_string_lossy().to_string()).unwrap_or_else(||"in".into())));
let bytes = fs::read(&as_path).map_err(|e| format!("read {}: {e}", as_path.display()))?;
let r = client::convert_bytes(&bytes, &out)?;
let label = as_path.file_name().map(|n|n.to_string_lossy().to_string()).unwrap_or_else(||source.to_string());
Ok((out.clone(), conversion_to_json(&r, &out), label))
} else {
let leaf = source.rsplit('/').next().unwrap_or("part");
let out = PathBuf::from(format!("/tmp/step2glb-preview-{leaf}.glb"));
let r = client::convert_from_library(source, &out)?;
Ok((out.clone(), conversion_to_json(&r, &out), source.to_string()))
}
}
fn conversion_to_json(r: &client::ConvertResult, out: &std::path::Path) -> serde_json::Value {
json!({
"output": out.to_string_lossy(),
"sizeBytes": r.size_bytes,
"meshes": r.meshes,
"nodes": r.nodes,
"materials": r.materials,
"durationMs": r.duration_ms,
})
}
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}/")
}
}
fn short_label(s: &str) -> String {
if s.len() > 40 { format!("…{}", &s[s.len() - 37..]) } else { s.to_string() }
}
fn handle(req: Request, state: &Arc<Mutex<State>>) {
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::Get, "/meta") => {
let s = state.lock().unwrap();
let body = json!({
"source": s.source_label,
"glbPath": s.glb_path.to_string_lossy(),
"conversion": s.conversion,
}).to_string();
let resp = Response::from_string(body)
.with_header(Header::from_bytes("Content-Type", "application/json").unwrap());
let _ = req.respond(resp);
}
(Method::Get, "/model.glb") => {
let path = state.lock().unwrap().glb_path.clone();
match fs::File::open(&path) {
Ok(f) => {
let len = f.metadata().map(|m| m.len() as usize).unwrap_or(0);
let resp = Response::new(
StatusCode(200),
vec![
Header::from_bytes("Content-Type", "model/gltf-binary").unwrap(),
Header::from_bytes("Content-Length", len.to_string()).unwrap(),
],
f,
Some(len),
None,
);
let _ = req.respond(resp);
}
Err(e) => {
let _ = req.respond(Response::from_string(format!("open {path:?}: {e}")).with_status_code(500));
}
}
}
(Method::Post, "/console") => {
let _ = req.respond(Response::empty(204));
}
(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));
}
}
}