forked from 1b-agent/opencadstudio-upstream
feat(control,mcp): add reference scale calibration and layer auto-placement to embed_image
This commit is contained in:
1 parent
b8def31145
commit
f7f0770d42
2 files changed
+167
-51
No files matched your search
+162
-49
@@ -134,63 +134,130 @@ fn parse_point_2d(val: &Value) -> Result<[f64; 2], Value> {
|
|||||||
Ok([x, y])
|
Ok([x, y])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_calibrate(
|
||||||
|
cal: &Value,
|
||||||
|
pixel_width: u32,
|
||||||
|
pixel_height: u32,
|
||||||
|
) -> Result<(codec::types::Vector3, f64, f64), Value> {
|
||||||
|
let pt_a = cal
|
||||||
|
.get("point_a")
|
||||||
|
.or_else(|| cal.get("pixel_a"))
|
||||||
|
.or_else(|| cal.get("p1"))
|
||||||
|
.or_else(|| cal.get("source_a"));
|
||||||
|
let pt_b = cal
|
||||||
|
.get("point_b")
|
||||||
|
.or_else(|| cal.get("pixel_b"))
|
||||||
|
.or_else(|| cal.get("p2"))
|
||||||
|
.or_else(|| cal.get("source_b"));
|
||||||
|
let dist = cal
|
||||||
|
.get("distance")
|
||||||
|
.or_else(|| cal.get("real_distance"))
|
||||||
|
.or_else(|| cal.get("length"))
|
||||||
|
.and_then(Value::as_f64);
|
||||||
|
|
||||||
|
if let (Some(pa), Some(pb), Some(d_real)) = (pt_a, pt_b, dist) {
|
||||||
|
if !d_real.is_finite() || d_real <= 0.0 {
|
||||||
|
return Err(failure(
|
||||||
|
"invalid_distance",
|
||||||
|
"Calibration distance must be positive",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let p1 = parse_point_2d(pa)?;
|
||||||
|
let p2 = parse_point_2d(pb)?;
|
||||||
|
let d_px = ((p2[0] - p1[0]).powi(2) + (p2[1] - p1[1]).powi(2)).sqrt();
|
||||||
|
if d_px <= 1e-6 {
|
||||||
|
return Err(failure(
|
||||||
|
"invalid_points",
|
||||||
|
"Calibration reference points must be distinct",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let scale = d_real / d_px;
|
||||||
|
let width = pixel_width as f64 * scale;
|
||||||
|
|
||||||
|
let align_to = cal
|
||||||
|
.get("align_to")
|
||||||
|
.or_else(|| cal.get("origin"))
|
||||||
|
.or_else(|| cal.get("target_a"))
|
||||||
|
.map(parse_point_2d)
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or([0.0, 0.0]);
|
||||||
|
|
||||||
|
let x_ll = align_to[0] - p1[0] * scale;
|
||||||
|
let y_ll = align_to[1] - (pixel_height as f64 - p1[1]) * scale;
|
||||||
|
return Ok((codec::types::Vector3::new(x_ll, y_ll, 0.0), width, scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let (Some(src_pts), Some(tgt_pts)) = (
|
||||||
|
cal.get("source_points").and_then(Value::as_array),
|
||||||
|
cal.get("target_points").and_then(Value::as_array),
|
||||||
|
) {
|
||||||
|
if src_pts.len() >= 2 && tgt_pts.len() >= 2 {
|
||||||
|
let p_s1 = parse_point_2d(&src_pts[0])?;
|
||||||
|
let p_s2 = parse_point_2d(&src_pts[1])?;
|
||||||
|
let p_t1 = parse_point_2d(&tgt_pts[0])?;
|
||||||
|
let p_t2 = parse_point_2d(&tgt_pts[1])?;
|
||||||
|
let d_px = ((p_s2[0] - p_s1[0]).powi(2) + (p_s2[1] - p_s1[1]).powi(2)).sqrt();
|
||||||
|
let d_cad = ((p_t2[0] - p_t1[0]).powi(2) + (p_t2[1] - p_t1[1]).powi(2)).sqrt();
|
||||||
|
if d_px <= 1e-6 {
|
||||||
|
return Err(failure(
|
||||||
|
"invalid_points",
|
||||||
|
"Source calibration points must be distinct",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let scale = d_cad / d_px;
|
||||||
|
let width = pixel_width as f64 * scale;
|
||||||
|
let x_ll = p_t1[0] - p_s1[0] * scale;
|
||||||
|
let y_ll = p_t1[1] - (pixel_height as f64 - p_s1[1]) * scale;
|
||||||
|
return Ok((codec::types::Vector3::new(x_ll, y_ll, 0.0), width, scale));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(failure(
|
||||||
|
"invalid_calibrate",
|
||||||
|
"calibrate requires point_a:[x,y], point_b:[x,y], distance:number, and optional align_to:[x,y]",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
impl OpenCADStudio {
|
impl OpenCADStudio {
|
||||||
/// `embed_image` — pack the picture at `path` into an OLE2FRAME placed
|
/// `embed_image` — pack the picture at `path` into an OLE2FRAME or linked RasterImage
|
||||||
/// with its lower-left corner at `at` (default width ≈ pixel_width/100,
|
/// placed with its lower-left corner at `at` (default width ≈ pixel_width/100).
|
||||||
/// like the interactive command's Enter answer) and commit it as one
|
/// Supports automatic reference scaling and calibration via `calibrate`:
|
||||||
/// undo step. The drawing stays self-contained: no external file to lose.
|
/// `{"point_a": [px1, py1], "point_b": [px2, py2], "distance": 6500.0, "align_to": [0, 0]}`.
|
||||||
/// Supports automatic 2-point calibration via `source_points` and `target_points`.
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub(super) fn control_embed_image(&mut self, req: &Value) -> Result<Task<Message>, Value> {
|
pub(super) fn control_embed_image(&mut self, req: &Value) -> Result<Task<Message>, Value> {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let path = string(req, "path")?;
|
let path = string(req, "path")?;
|
||||||
let image = crate::io::ole_embed::EmbeddedImage::from_file(std::path::Path::new(&path))
|
let image = crate::io::ole_embed::EmbeddedImage::from_file(std::path::Path::new(&path))
|
||||||
.map_err(|e| failure("embed_failed", e))?;
|
.map_err(|e| failure("embed_failed", e))?;
|
||||||
let (at, width) = if let (Some(src_pts), Some(tgt_pts)) = (
|
|
||||||
|
let (at, width, scale) = if let Some(cal) = req.get("calibrate") {
|
||||||
|
parse_calibrate(cal, image.pixel_width, image.pixel_height)?
|
||||||
|
} else if let (Some(src_pts), Some(tgt_pts)) = (
|
||||||
req.get("source_points").and_then(Value::as_array),
|
req.get("source_points").and_then(Value::as_array),
|
||||||
req.get("target_points").and_then(Value::as_array),
|
req.get("target_points").and_then(Value::as_array),
|
||||||
) {
|
) {
|
||||||
if src_pts.len() >= 2 && tgt_pts.len() >= 2 {
|
let dummy = json!({"source_points": src_pts, "target_points": tgt_pts});
|
||||||
let p_s1 = parse_point_2d(&src_pts[0])?;
|
parse_calibrate(&dummy, image.pixel_width, image.pixel_height)?
|
||||||
let p_s2 = parse_point_2d(&src_pts[1])?;
|
|
||||||
let p_t1 = parse_point_2d(&tgt_pts[0])?;
|
|
||||||
let p_t2 = parse_point_2d(&tgt_pts[1])?;
|
|
||||||
let d_px = ((p_s2[0] - p_s1[0]).powi(2) + (p_s2[1] - p_s1[1]).powi(2)).sqrt();
|
|
||||||
let d_cad = ((p_t2[0] - p_t1[0]).powi(2) + (p_t2[1] - p_t1[1]).powi(2)).sqrt();
|
|
||||||
if d_px <= 1e-6 {
|
|
||||||
return Err(failure(
|
|
||||||
"invalid_points",
|
|
||||||
"Source calibration points must be distinct",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let scale = d_cad / d_px;
|
|
||||||
let w = image.pixel_width as f64 * scale;
|
|
||||||
// In image pixel coords, y is 0 at top and pixel_height at bottom.
|
|
||||||
// In CAD coordinates, lower-left is (x_ll, y_ll).
|
|
||||||
// x_target = x_ll + px * scale => x_ll = x_target - px * scale
|
|
||||||
// y_target = y_ll + (pixel_height - py) * scale => y_ll = y_target - (pixel_height - py) * scale
|
|
||||||
let x_ll = p_t1[0] - p_s1[0] * scale;
|
|
||||||
let y_ll = p_t1[1] - (image.pixel_height as f64 - p_s1[1]) * scale;
|
|
||||||
let calc_at = codec::types::Vector3::new(x_ll, y_ll, 0.0);
|
|
||||||
let final_at = embed_point(req).unwrap_or(calc_at);
|
|
||||||
let final_w = req["width"].as_f64().filter(|w| *w > 0.0).unwrap_or(w);
|
|
||||||
(final_at, final_w)
|
|
||||||
} else {
|
|
||||||
let default_width = (image.pixel_width as f64 / 100.0).max(1.0);
|
|
||||||
let width = req["width"].as_f64().filter(|w| *w > 0.0).unwrap_or(default_width);
|
|
||||||
(embed_point(req)?, width)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
let default_width = (image.pixel_width as f64 / 100.0).max(1.0);
|
let default_width = (image.pixel_width as f64 / 100.0).max(1.0);
|
||||||
let width = req["width"].as_f64().filter(|w| *w > 0.0).unwrap_or(default_width);
|
let width = req["width"].as_f64().filter(|w| *w > 0.0).unwrap_or(default_width);
|
||||||
(embed_point(req)?, width)
|
let scale = width / image.pixel_width as f64;
|
||||||
|
(embed_point(req)?, width, scale)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let final_at = if req.get("calibrate").is_none() && req.get("source_points").is_none() {
|
||||||
|
at
|
||||||
|
} else {
|
||||||
|
embed_point(req).unwrap_or(at)
|
||||||
|
};
|
||||||
|
let final_width = req["width"].as_f64().filter(|w| *w > 0.0).unwrap_or(width);
|
||||||
|
let height = final_width * image.pixel_height as f64 / image.pixel_width as f64;
|
||||||
|
|
||||||
|
let layer_name = req["layer"].as_str().filter(|l| !l.is_empty());
|
||||||
|
let lock_layer = req["lock_layer"].as_bool().unwrap_or(false);
|
||||||
|
|
||||||
if req["linked"].as_bool().unwrap_or(false) {
|
if req["linked"].as_bool().unwrap_or(false) {
|
||||||
// Path-linked RasterImage + ImageDefinition: the drawing stores
|
|
||||||
// only the file path, so the picture file must travel with the
|
|
||||||
// drawing (unlike the embedded OLE2FRAME default below).
|
|
||||||
self.push_undo_snapshot(i, "IMAGEATTACH");
|
self.push_undo_snapshot(i, "IMAGEATTACH");
|
||||||
let height = width * image.pixel_height as f64 / image.pixel_width as f64;
|
|
||||||
let handle = {
|
let handle = {
|
||||||
let document = &mut self.tabs[i].scene.document;
|
let document = &mut self.tabs[i].scene.document;
|
||||||
let definition_handle = document.allocate_handle();
|
let definition_handle = document.allocate_handle();
|
||||||
@@ -206,45 +273,91 @@ impl OpenCADStudio {
|
|||||||
);
|
);
|
||||||
let mut entity = codec::entities::RasterImage::with_size(
|
let mut entity = codec::entities::RasterImage::with_size(
|
||||||
path,
|
path,
|
||||||
at,
|
final_at,
|
||||||
image.pixel_width as f64,
|
image.pixel_width as f64,
|
||||||
image.pixel_height as f64,
|
image.pixel_height as f64,
|
||||||
width,
|
final_width,
|
||||||
height,
|
height,
|
||||||
);
|
);
|
||||||
entity.definition_handle = Some(definition_handle);
|
entity.definition_handle = Some(definition_handle);
|
||||||
|
if let Some(l_name) = layer_name {
|
||||||
|
entity.common.layer = l_name.to_string();
|
||||||
|
if !document.layers.contains(l_name) {
|
||||||
|
let mut layer_record = codec::tables::Layer::new(l_name.to_string());
|
||||||
|
layer_record.handle = document.allocate_handle();
|
||||||
|
if lock_layer {
|
||||||
|
layer_record.flags.locked = true;
|
||||||
|
}
|
||||||
|
let _ = document.layers.add(layer_record);
|
||||||
|
} else if lock_layer {
|
||||||
|
if let Some(l) = document.layers.get_mut(l_name) {
|
||||||
|
l.flags.locked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
document
|
document
|
||||||
.add_entity(codec::EntityType::RasterImage(entity))
|
.add_entity(codec::EntityType::RasterImage(entity))
|
||||||
.map_err(|e| failure("embed_failed", e))?
|
.map_err(|e| failure("embed_failed", e))?
|
||||||
};
|
};
|
||||||
self.tabs[i].scene.populate_images_from_document();
|
self.tabs[i].scene.populate_images_from_document();
|
||||||
|
if layer_name.is_some() {
|
||||||
|
self.refresh_layer_panel();
|
||||||
|
}
|
||||||
self.post_ref_op(i);
|
self.post_ref_op(i);
|
||||||
self.set_control_result(json!({
|
self.set_control_result(json!({
|
||||||
"handle": format!("{:X}", handle.value()),
|
"handle": format!("{:X}", handle.value()),
|
||||||
"kind": "RasterImage",
|
"kind": "RasterImage",
|
||||||
"path": path,
|
"path": path,
|
||||||
"at": [at.x, at.y, at.z],
|
"at": [final_at.x, final_at.y, final_at.z],
|
||||||
"width": width,
|
"width": final_width,
|
||||||
"height": height,
|
"height": height,
|
||||||
|
"scale": scale,
|
||||||
"linked": true,
|
"linked": true,
|
||||||
|
"layer": layer_name.unwrap_or("0"),
|
||||||
}));
|
}));
|
||||||
return Ok(Task::none());
|
return Ok(Task::none());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.push_undo_snapshot(i, "IMAGEEMBED");
|
self.push_undo_snapshot(i, "IMAGEEMBED");
|
||||||
let handle = crate::io::ole_embed::add_embedded_image(
|
let handle = crate::io::ole_embed::add_embedded_image(
|
||||||
&mut self.tabs[i].scene.document,
|
&mut self.tabs[i].scene.document,
|
||||||
&image,
|
&image,
|
||||||
at,
|
final_at,
|
||||||
width,
|
final_width,
|
||||||
)
|
)
|
||||||
.map_err(|e| failure("embed_failed", e))?;
|
.map_err(|e| failure("embed_failed", e))?;
|
||||||
|
|
||||||
|
if let Some(l_name) = layer_name {
|
||||||
|
let document = &mut self.tabs[i].scene.document;
|
||||||
|
if !document.layers.contains(l_name) {
|
||||||
|
let mut layer_record = codec::tables::Layer::new(l_name.to_string());
|
||||||
|
layer_record.handle = document.allocate_handle();
|
||||||
|
if lock_layer {
|
||||||
|
layer_record.flags.locked = true;
|
||||||
|
}
|
||||||
|
let _ = document.layers.add(layer_record);
|
||||||
|
} else if lock_layer {
|
||||||
|
if let Some(l) = document.layers.get_mut(l_name) {
|
||||||
|
l.flags.locked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(ent) = document.get_entity_mut(handle) {
|
||||||
|
ent.common_mut().layer = l_name.to_string();
|
||||||
|
}
|
||||||
|
self.refresh_layer_panel();
|
||||||
|
}
|
||||||
|
|
||||||
self.post_ref_op(i);
|
self.post_ref_op(i);
|
||||||
self.set_control_result(json!({
|
self.set_control_result(json!({
|
||||||
"handle": format!("{:X}", handle.value()),
|
"handle": format!("{:X}", handle.value()),
|
||||||
"kind": "Ole2Frame",
|
"kind": "Ole2Frame",
|
||||||
"path": path,
|
"path": path,
|
||||||
"at": [at.x, at.y, at.z],
|
"at": [final_at.x, final_at.y, final_at.z],
|
||||||
"width": width,
|
"width": final_width,
|
||||||
|
"height": height,
|
||||||
|
"scale": scale,
|
||||||
|
"linked": false,
|
||||||
|
"layer": layer_name.unwrap_or("0"),
|
||||||
}));
|
}));
|
||||||
Ok(Task::none())
|
Ok(Task::none())
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-2
@@ -249,18 +249,21 @@ pub static ACTION: OpDef = OpDef {
|
|||||||
|
|
||||||
pub static EMBED_IMAGE: OpDef = OpDef {
|
pub static EMBED_IMAGE: OpDef = OpDef {
|
||||||
name: "embed_image",
|
name: "embed_image",
|
||||||
doc: "Embed or link a raster image into the drawing.",
|
doc: "Embed or link a raster image into the drawing with optional reference scaling and calibration.",
|
||||||
batchable: true,
|
batchable: true,
|
||||||
params: &[
|
params: &[
|
||||||
req("path", Ty::Str, "Path to the image file."),
|
req("path", Ty::Str, "Path to the image file."),
|
||||||
opt("at", Ty::Point, "Lower-left corner [x, y] or [x, y, z]."),
|
opt("at", Ty::Point, "Lower-left corner [x, y] or [x, y, z]."),
|
||||||
opt("width", Ty::Num, "World width of the image."),
|
opt("width", Ty::Num, "World width of the image."),
|
||||||
opt("linked", Ty::Bool, "Store as path-linked RasterImage instead of embedded OLE2FRAME."),
|
opt("linked", Ty::Bool, "Store as path-linked RasterImage instead of embedded OLE2FRAME."),
|
||||||
|
opt("calibrate", Ty::Raw(r#"{"type":"object","description":"Automatic 2-point reference calibration: {point_a:[px1,py1], point_b:[px2,py2], distance:650.0, align_to:[0,0]}"}"#), "Automatic 2-point reference calibration: {point_a:[px1,py1], point_b:[px2,py2], distance:650.0, align_to:[0,0]}."),
|
||||||
|
opt("layer", Ty::Str, "Layer name to place the image on (auto-created if missing, e.g. '_XREF')."),
|
||||||
|
opt("lock_layer", Ty::Bool, "Lock the target layer after attaching."),
|
||||||
opt("source_points", Ty::ArrayOf(&Ty::Point), "Pixel points in image [[px1,py1],[px2,py2]] for 2-point alignment."),
|
opt("source_points", Ty::ArrayOf(&Ty::Point), "Pixel points in image [[px1,py1],[px2,py2]] for 2-point alignment."),
|
||||||
opt("target_points", Ty::ArrayOf(&Ty::Point), "CAD world points [[x1,y1],[x2,y2]] for 2-point alignment."),
|
opt("target_points", Ty::ArrayOf(&Ty::Point), "CAD world points [[x1,y1],[x2,y2]] for 2-point alignment."),
|
||||||
],
|
],
|
||||||
rules: &[],
|
rules: &[],
|
||||||
example: r#"{"op":"embed_image","path":"logo.png","at":[0,0]}"#,
|
example: r#"{"op":"embed_image","path":"plan.png","linked":true,"calibrate":{"point_a":[52,910],"point_b":[450,910],"distance":6500},"layer":"_XREF","lock_layer":true}"#,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub static WBLOCK: OpDef = OpDef {
|
pub static WBLOCK: OpDef = OpDef {
|
||||||
|
|||||||
Reference in new issue
Block a user