app
Adom Step - File Viewer
Public Made by Adomby adom
Fusion 360-style viewer for STEP / STP files in a Hydrogen webview. Components outline, smart-pick measure (mm + mils + angle), hover-inspect HUD, and geometry-based pin / contact detection that handl
← Commit history
open-source: publish full source under adom org
8 files changed
+271
src/cli/hide.rsadded+22@@ -0,0 +1,22 @@+use crate::cli::{eval, ok, DEFAULT_PORT};+use anyhow::Result;+use clap::Parser;++#[derive(Parser)]+pub struct Args {+ pub name: String,+ #[arg(long, default_value_t = 5)]+ pub timeout_secs: u64,+ #[arg(long, default_value_t = DEFAULT_PORT)]+ pub port: u16,+}++pub fn run(args: Args) -> Result<()> {+ let code = format!(+ "return window.adomStep ? window.adomStep.hide({}) : {{ error: 'adomStep not loaded' }};",+ serde_json::to_string(&args.name).unwrap()+ );+ eval::eval_snippet(&code, args.timeout_secs, args.port, true)?;+ ok(format!("hide {}", args.name));+ Ok(())+}
src/cli/mod.rsadded+45@@ -0,0 +1,45 @@+pub mod console;+pub mod eval;+pub mod frame;+pub mod health;+pub mod hide;+pub mod install;+pub mod isolate;+pub mod list_components;+pub mod measure;+pub mod open;+pub mod pins;+pub mod screenshot;+pub mod show;+pub mod show_all;+pub mod status;+pub mod stop;+pub mod view;+pub mod view_cube;+pub mod view_library;++use colored::Colorize;++pub fn ok(msg: impl AsRef<str>) {+ println!("{} {}", "OK:".green().bold(), msg.as_ref());+}++pub fn err(msg: impl AsRef<str>) {+ eprintln!("{} {}", "ERROR:".red().bold(), msg.as_ref());+}++pub fn hint(msg: impl AsRef<str>) {+ eprintln!("{} {}", "Hint:".yellow().bold(), msg.as_ref());+}++pub const DEFAULT_PORT: u16 = 8851;++/// Service-step2glb base URL. Override at runtime with `STEP2GLB_SERVICE_API`.+pub const DEFAULT_STEP2GLB_URL: &str = "https://step2glb-gmdoncpxdwx0.adom.cloud";++pub fn step2glb_service_url() -> String {+ std::env::var("STEP2GLB_SERVICE_API")+ .unwrap_or_else(|_| DEFAULT_STEP2GLB_URL.to_string())+ .trim_end_matches('/')+ .to_string()+}
src/cli/screenshot.rsadded+89@@ -0,0 +1,89 @@+use crate::cli::{eval, err, ok, DEFAULT_PORT};+use anyhow::Result;+use clap::Parser;+use std::path::PathBuf;++#[derive(Parser)]+pub struct Args {+ /// Output PNG path. Defaults to /tmp/adom-step-shot.png.+ #[arg(long)]+ pub out: Option<PathBuf>,++ #[arg(long, default_value_t = 1600)]+ pub width: u32,++ #[arg(long, default_value_t = 1000)]+ pub height: u32,++ #[arg(long, default_value_t = 10)]+ pub timeout_secs: u64,++ #[arg(long, default_value_t = DEFAULT_PORT)]+ pub port: u16,+}++pub fn run(args: Args) -> Result<()> {+ let out = args+ .out+ .unwrap_or_else(|| PathBuf::from("/tmp/adom-step-shot.png"));++ let code = format!(+ "return window.adomStep ? await window.adomStep.screenshot({{width: {}, height: {}}}) : {{ error: 'adomStep not loaded' }};",+ args.width, args.height+ );+ let result = eval::eval_snippet(&code, args.timeout_secs, args.port, true)?;+ if let Some(e) = result.get("error").and_then(|v| v.as_str()) {+ err(format!("screenshot failed: {e}"));+ return Err(anyhow::anyhow!("{e}"));+ }+ let data_url = result.as_str().unwrap_or("");+ let prefix = "data:image/png;base64,";+ if !data_url.starts_with(prefix) {+ err(format!("unexpected screenshot payload: {data_url}"));+ return Err(anyhow::anyhow!("bad payload"));+ }+ let b64 = &data_url[prefix.len()..];+ let bytes = base64_decode(b64)?;+ std::fs::write(&out, &bytes)?;+ ok(format!(+ "wrote {} ({} KB)",+ out.display(),+ bytes.len() / 1024+ ));+ Ok(())+}++/// Lean base64 decoder so we don't pull in the `base64` crate for one use.+fn base64_decode(s: &str) -> Result<Vec<u8>> {+ let s: String = s.chars().filter(|c| !c.is_whitespace()).collect();+ let map = |c: char| -> Result<u32> {+ Ok(match c {+ 'A'..='Z' => c as u32 - 'A' as u32,+ 'a'..='z' => c as u32 - 'a' as u32 + 26,+ '0'..='9' => c as u32 - '0' as u32 + 52,+ '+' => 62,+ '/' => 63,+ '=' => 0,+ _ => return Err(anyhow::anyhow!("bad base64 char: {c}")),+ })+ };+ let bytes = s.as_bytes();+ let mut out = Vec::with_capacity(bytes.len() * 3 / 4);+ let mut i = 0;+ while i + 3 < bytes.len() {+ let a = map(bytes[i] as char)?;+ let b = map(bytes[i + 1] as char)?;+ let c = map(bytes[i + 2] as char)?;+ let d = map(bytes[i + 3] as char)?;+ let v = (a << 18) | (b << 12) | (c << 6) | d;+ out.push((v >> 16) as u8);+ if bytes[i + 2] != b'=' {+ out.push(((v >> 8) & 0xFF) as u8);+ }+ if bytes[i + 3] != b'=' {+ out.push((v & 0xFF) as u8);+ }+ i += 4;+ }+ Ok(out)+}
src/cli/show.rsadded+22@@ -0,0 +1,22 @@+use crate::cli::{eval, ok, DEFAULT_PORT};+use anyhow::Result;+use clap::Parser;++#[derive(Parser)]+pub struct Args {+ pub name: String,+ #[arg(long, default_value_t = 5)]+ pub timeout_secs: u64,+ #[arg(long, default_value_t = DEFAULT_PORT)]+ pub port: u16,+}++pub fn run(args: Args) -> Result<()> {+ let code = format!(+ "return window.adomStep ? window.adomStep.show({}) : {{ error: 'adomStep not loaded' }};",+ serde_json::to_string(&args.name).unwrap()+ );+ eval::eval_snippet(&code, args.timeout_secs, args.port, true)?;+ ok(format!("show {}", args.name));+ Ok(())+}
src/cli/show_all.rsadded+18@@ -0,0 +1,18 @@+use crate::cli::{eval, ok, DEFAULT_PORT};+use anyhow::Result;+use clap::Parser;++#[derive(Parser)]+pub struct Args {+ #[arg(long, default_value_t = 5)]+ pub timeout_secs: u64,+ #[arg(long, default_value_t = DEFAULT_PORT)]+ pub port: u16,+}++pub fn run(args: Args) -> Result<()> {+ let code = "return window.adomStep ? window.adomStep.showAll() : { error: 'adomStep not loaded' };";+ eval::eval_snippet(code, args.timeout_secs, args.port, true)?;+ ok("showed all components");+ Ok(())+}
src/cli/status.rsadded+25@@ -0,0 +1,25 @@+use crate::cli::{err, ok, DEFAULT_PORT};+use anyhow::Result;+use clap::Parser;+use std::time::Duration;++#[derive(Parser)]+pub struct Args {+ #[arg(long, default_value_t = DEFAULT_PORT)]+ pub port: u16,+}++pub fn run(args: Args) -> Result<()> {+ let url = format!("http://127.0.0.1:{}/state", args.port);+ match ureq::get(&url).timeout(Duration::from_secs(2)).call() {+ Ok(r) => {+ let body: serde_json::Value = r.into_json()?;+ ok(serde_json::to_string_pretty(&body).unwrap_or_default());+ Ok(())+ }+ Err(e) => {+ err(format!("not running on port {}: {e}", args.port));+ Err(anyhow::anyhow!("not running"))+ }+ }+}
src/cli/stop.rsadded+19@@ -0,0 +1,19 @@+use crate::cli::{ok, DEFAULT_PORT};+use anyhow::Result;+use clap::Parser;+use std::time::Duration;++#[derive(Parser)]+pub struct Args {+ #[arg(long, default_value_t = DEFAULT_PORT)]+ pub port: u16,+}++pub fn run(args: Args) -> Result<()> {+ let url = format!("http://127.0.0.1:{}/shutdown", args.port);+ let _ = ureq::post(&url)+ .timeout(Duration::from_secs(2))+ .send_string("");+ ok(format!("sent shutdown to port {}", args.port));+ Ok(())+}
src/cli/view_cube.rsadded+31@@ -0,0 +1,31 @@+use crate::cli::{eval, err, ok, DEFAULT_PORT};+use anyhow::Result;+use clap::Parser;++#[derive(Parser)]+pub struct Args {+ /// View name: front | back | left | right | top | bottom | iso | home.+ pub view: String,+ #[arg(long, default_value_t = 5)]+ pub timeout_secs: u64,+ #[arg(long, default_value_t = DEFAULT_PORT)]+ pub port: u16,+}++pub fn run(args: Args) -> Result<()> {+ let v = args.view.trim().to_lowercase();+ if !matches!(+ v.as_str(),+ "front" | "back" | "left" | "right" | "top" | "bottom" | "iso" | "home"+ ) {+ err(format!("unknown view '{v}'"));+ return Err(anyhow::anyhow!("bad view"));+ }+ let code = format!(+ "return window.adomStep ? window.adomStep.setView({}) : {{ error: 'adomStep not loaded' }};",+ serde_json::to_string(&v).unwrap()+ );+ eval::eval_snippet(&code, args.timeout_secs, args.port, true)?;+ ok(format!("set view: {v}"));+ Ok(())+}