use crate::program::Program;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::BTreeMap;
pub const HOST_JS: &str = include_str!("host.js");
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BuiltinPost {
pub id: &'static str,
pub source: &'static str,
}
pub const BUILTIN_POSTS: &[BuiltinPost] = &[
BuiltinPost { id: "fanuc", source: include_str!("../../posts/fanuc.js") },
BuiltinPost { id: "haas", source: include_str!("../../posts/haas.js") },
BuiltinPost { id: "grbl", source: include_str!("../../posts/grbl.js") },
BuiltinPost { id: "linuxcnc", source: include_str!("../../posts/linuxcnc.js") },
BuiltinPost { id: "mach", source: include_str!("../../posts/mach.js") },
BuiltinPost { id: "heidenhain", source: include_str!("../../posts/heidenhain.js") },
BuiltinPost { id: "siemens-840d", source: include_str!("../../posts/siemens-840d.js") },
BuiltinPost { id: "marlin", source: include_str!("../../posts/marlin.js") },
BuiltinPost { id: "okuma", source: include_str!("../../posts/okuma.js") },
BuiltinPost { id: "mazak", source: include_str!("../../posts/mazak.js") },
BuiltinPost { id: "fanuc-lathe", source: include_str!("../../posts/fanuc-lathe.js") },
BuiltinPost { id: "haas-lathe", source: include_str!("../../posts/haas-lathe.js") },
BuiltinPost { id: "okuma-lathe", source: include_str!("../../posts/okuma-lathe.js") },
BuiltinPost { id: "mazak-lathe", source: include_str!("../../posts/mazak-lathe.js") },
];
pub fn builtin(id: &str) -> Option<BuiltinPost> {
BUILTIN_POSTS.iter().copied().find(|p| p.id == id)
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PropertySpec {
pub title: String,
#[serde(default)]
pub description: String,
#[serde(rename = "type")]
pub kind: String,
#[serde(default)]
pub default: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub values: Option<Vec<Value>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub min: Option<f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max: Option<f64>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PostDefinition {
pub id: String,
pub name: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub vendor: String,
#[serde(default = "default_extension")]
pub extension: String,
#[serde(default = "default_units")]
pub units: String,
#[serde(default = "default_machine")]
pub machine: String,
#[serde(default)]
pub capabilities: Value,
#[serde(default)]
pub properties: BTreeMap<String, PropertySpec>,
}
fn default_extension() -> String {
"nc".into()
}
fn default_units() -> String {
"auto".into()
}
fn default_machine() -> String {
"mill".into()
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PostRequest {
pub program: Program,
#[serde(default)]
pub properties: BTreeMap<String, Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub file_name: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PostOutput {
pub text: String,
pub file_name: String,
pub extension: String,
#[serde(default)]
pub warnings: Vec<String>,
#[serde(default)]
pub notes: Vec<String>,
#[serde(default)]
pub properties: BTreeMap<String, Value>,
#[serde(default)]
pub logs: Vec<String>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct PostError {
pub message: String,
pub logs: Vec<String>,
}
impl std::fmt::Display for PostError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for PostError {}
pub fn output_file_name(request: &PostRequest, extension: &str) -> String {
let stem = request.file_name.clone().unwrap_or_else(|| request.program.name.clone());
let stem = if stem.trim().is_empty() { "program".to_string() } else { stem };
let stem: String = stem.chars().map(|c| if c.is_alphanumeric() || "-_. ".contains(c) { c } else { '_' }).collect();
format!("{stem}.{extension}")
}
#[cfg(feature = "post-runtime")]
mod runtime {
use super::*;
fn source_of(script: &str) -> String {
let mut s = String::with_capacity(HOST_JS.len() + script.len() + 2);
s.push_str(HOST_JS);
s.push('\n');
s.push_str(script);
s.push('\n');
s
}
fn call(script: &str, name: &str, function: &str, input: &Value) -> Result<(Value, Vec<String>), PostError> {
let file = format!("{name}.js");
match brep_script_core::call(&source_of(script), &file, function, input, &|_, _| Ok(())) {
Ok(ok) => Ok((ok.value, ok.logs)),
Err(e) => Err(PostError { message: e.message, logs: e.logs }),
}
}
pub fn describe(script: &str, name: &str) -> Result<PostDefinition, PostError> {
let (value, logs) = call(script, name, "__brepDescribePost", &Value::Null)?;
serde_json::from_value(value).map_err(|e| PostError { message: format!("{name}: invalid post definition: {e}"), logs })
}
fn resolve_for_post(script: &str, name: &str, program: &Program) -> Result<Option<Value>, PostError> {
use crate::kinematics::post::{resolve_motions, FeedMode, ResolveOptions};
use crate::kinematics::{Chain, KinematicsOptions};
use crate::model::MachineKind;
let Some(machine) = &program.machine else { return Ok(None) };
if !matches!(machine.kind, MachineKind::Mill4 { .. } | MachineKind::Mill5 { .. } | MachineKind::MillTurn { .. }) {
return Ok(None);
}
if program.motions.iter().any(|m| {
matches!(m.motion.kind, crate::model::MotionKind::PolarInterpolation { .. } | crate::model::MotionKind::CylindricalInterpolation { .. } | crate::model::MotionKind::LinearRotary { .. })
}) {
return Ok(None);
}
let definition = describe(script, name)?;
let caps = definition.capabilities.get("multiAxis").cloned().unwrap_or(Value::Null);
let tcp = caps.get("tcp").and_then(Value::as_bool).unwrap_or(false);
let feed_mode = match caps.get("feedMode").and_then(Value::as_str) {
Some("inverseTime") => FeedMode::InverseTime,
Some("dpm") => FeedMode::DegreesPerMinute,
_ => FeedMode::Standard,
};
let base = ResolveOptions { kinematics: KinematicsOptions { tcp, ..Default::default() }, feed_mode, ..Default::default() };
let fail = |m: String| PostError { message: format!("{name}: kinematics: {m}"), logs: Vec::new() };
let point = |p: crate::model::Point3| serde_json::json!([p.x, p.y, p.z]);
let mut joints: Vec<String> = Vec::new();
let mut blocks: Vec<Value> = Vec::new();
let mut sections: Vec<[usize; 2]> = vec![[0, 0]; program.sections.len()];
let mut start_angles: Option<Vec<f64>> = None;
let mut warnings: Vec<String> = Vec::new();
for setup in &program.setups {
let secs: Vec<&crate::program::Section> = program.sections.iter().filter(|s| s.setup == setup.id).collect();
let (Some(first), Some(last)) = (secs.first(), secs.last()) else { continue };
let lo = first.first_motion;
let hi = last.first_motion + last.motion_count;
if secs.windows(2).any(|w| w[1].first_motion != w[0].first_motion + w[0].motion_count) {
return Err(fail(format!("setup {} sections are not contiguous in the program stream", setup.id)));
}
let chain = Chain::from_machine(machine, setup.wcs.origin).map_err(|e| fail(e.to_string()))?;
let opts = ResolveOptions { start_angles: start_angles.clone(), ..base.clone() };
let r = resolve_motions(&program.motions[lo..hi], &chain, &opts).map_err(|e| fail(e.to_string()))?;
joints = r.joints.clone();
warnings.extend(r.warnings.iter().map(|w| format!("{}: {}", w.code, w.message)));
let section_of = |src: usize| secs.iter().position(|s| src + lo >= s.first_motion && src + lo < s.first_motion + s.motion_count);
let mut owners: Vec<usize> = Vec::with_capacity(r.motions.len());
for (k, b) in r.motions.iter().enumerate() {
let src = b.source.or_else(|| r.motions[k..].iter().find_map(|q| q.source));
let owner = src.and_then(section_of).unwrap_or(secs.len() - 1);
owners.push(owner);
}
if owners.windows(2).any(|w| w[1] < w[0]) {
return Err(fail("resolved blocks are out of section order".into()));
}
for (k, b) in r.motions.iter().enumerate() {
let section = secs[owners[k]];
let entry = &mut sections[section.index];
if entry[1] == 0 {
entry[0] = blocks.len();
}
entry[1] += 1;
blocks.push(serde_json::json!({
"source": b.source.map(|i| i + lo),
"operation": b.operation,
"motion": b.motion,
"linear": point(b.linear),
"center": b.center.map(point),
"rotary": b.rotary,
"inverseTime": b.inverse_time,
"rotaryFeed": b.rotary_feed,
"repositioned": b.repositioned,
}));
}
if let Some(last) = r.motions.last() {
start_angles = Some(last.rotary.iter().map(|w| w.angle_deg).collect());
}
}
Ok(Some(serde_json::json!({ "joints": joints, "motions": blocks, "sections": sections, "warnings": warnings })))
}
pub fn run(script: &str, name: &str, request: &PostRequest) -> Result<PostOutput, PostError> {
let mut input = serde_json::to_value(request).map_err(|e| PostError { message: e.to_string(), logs: Vec::new() })?;
if let Some(resolved) = resolve_for_post(script, name, &request.program)? {
input["resolved"] = resolved;
}
let (value, logs) = call(script, name, "__brepRunPost", &input)?;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct Raw {
text: String,
extension: String,
#[serde(default)]
warnings: Vec<String>,
#[serde(default)]
notes: Vec<String>,
#[serde(default)]
properties: BTreeMap<String, Value>,
}
let raw: Raw = serde_json::from_value(value).map_err(|e| PostError { message: format!("{name}: invalid post result: {e}"), logs: logs.clone() })?;
Ok(PostOutput {
file_name: output_file_name(request, &raw.extension),
text: raw.text,
extension: raw.extension,
warnings: raw.warnings,
notes: raw.notes,
properties: raw.properties,
logs,
})
}
pub fn run_builtin(id: &str, request: &PostRequest) -> Result<PostOutput, PostError> {
let post = builtin(id).ok_or_else(|| PostError { message: format!("unknown built-in post {id:?}"), logs: Vec::new() })?;
run(post.source, post.id, request)
}
pub fn describe_builtins() -> Result<Vec<PostDefinition>, PostError> {
BUILTIN_POSTS.iter().map(|p| describe(p.source, p.id)).collect()
}
}
#[cfg(feature = "post-runtime")]
pub use runtime::{describe, describe_builtins, run, run_builtin};