Files
OneCAD-Draft/src/modules/insert/wblock.rs
T
ramox81andClaude Opus 5.5 d583d9d421 Block Definition command-line forms, block record data and dialog defaults
-BLOCK, -WBLOCK, -BEDIT, ATTSYNC, BASE and INSBASE follow the reference
prompts and messages; empty blocks; WBLOCK keeps object coordinates and
stores INSBASE; annotative and hyperlink data on the block record; block
preview images; remembered dialog choices; block aliases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 17:30:21 +03:00

257 lines
8.3 KiB
Rust

// WBLOCK — write a block (or selected entities) to an external DWG/DXF file.
//
// Two modes:
// block name → copies the named block definition to a new document
// * → copies currently selected model-space entities
use crate::t;
use codec::{CadDocument, EntityType};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
#[allow(dead_code)]
pub fn tool() -> ToolDef {
ToolDef {
id: "WBLOCK",
label: "Write Block",
icon: IconKind::Svg(include_bytes!("../../../assets/icons/blocks/insert.svg")),
event: ModuleEvent::Command("WBLOCK".to_string()),
}
}
/// Build a standalone `CadDocument` containing the named block's entities
/// extracted into model space.
///
/// Returns `Err` if the block is not found or has no entities.
pub fn extract_block_to_doc(src: &CadDocument, block_name: &str) -> Result<CadDocument, String> {
let mut out = CadDocument::new();
extract_block_into(src, block_name, &mut out)?;
Ok(out)
}
/// Extract the block's entities into `out` (a fresh document or a loaded
/// template whose tables/styles should survive — Catalog §2.1).
pub fn extract_block_into(
src: &CadDocument,
block_name: &str,
out: &mut CadDocument,
) -> Result<(), String> {
let br = src
.block_records
.get(block_name)
.ok_or_else(|| t!("Block \"%{name}\" not found.", name = block_name).into_owned())?;
let handles = br.entity_handles.clone();
if handles.is_empty() {
return Err(t!("Block \"%{name}\" has no entities.", name = block_name).into_owned());
}
copy_with_dependencies(src, &handles, out, None);
if out.entities().count() == 0 {
return Err(t!(
"Block \"%{name}\" produced no exportable entities.",
name = block_name
)
.into_owned());
}
Ok(())
}
/// Build a standalone `CadDocument` from an explicit list of entity handles
/// (the "selected entities" mode, `*`).
pub fn extract_entities_to_doc(
src: &CadDocument,
handles: &[codec::Handle],
) -> Result<CadDocument, String> {
let mut out = CadDocument::new();
extract_entities_into(src, handles, &mut out)?;
Ok(out)
}
/// Extract the listed entities into `out` (fresh document or template base).
pub fn extract_entities_into(
src: &CadDocument,
handles: &[codec::Handle],
out: &mut CadDocument,
) -> Result<(), String> {
if handles.is_empty() {
return Err(t!("No entities selected for WBLOCK.").into_owned());
}
copy_with_dependencies(src, handles, out, None);
if out.entities().count() == 0 {
return Err(t!("None of the selected entities could be exported.").into_owned());
}
Ok(())
}
/// Copy entities into `out` the way a cross-drawing paste does: fresh handles,
/// plus the layers, linetypes, styles, block definitions and baked dimension
/// blocks they use — without those an exported INSERT points at nothing and
/// the file opens empty or needs recovery. (#1111)
fn copy_with_dependencies(
src: &CadDocument,
handles: &[codec::Handle],
out: &mut CadDocument,
translate: Option<&crate::command::EntityTransform>,
) {
let handles: Vec<codec::Handle> = handles
.iter()
.copied()
.filter(|&handle| {
!matches!(
src.get_entity(handle),
Some(EntityType::Block(_) | EntityType::BlockEnd(_))
)
})
.collect();
let (entities, deps) = crate::app::copy_to_clipboard_kernel(src, &handles);
crate::app::paste_entities_kernel(
out,
&entities,
&deps,
translate,
crate::app::PasteSpace::Model,
);
}
/// Translate every entity of `out` so the overall bounds minimum lands on
/// the origin — SPM.ACAD's clone flow normalizes the new drawing to 0,0,0
/// after the copy (Catalog §2.1/CF-01.2). Opt-in per request; the
/// interactive WBLOCK export keeps the source coordinates.
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
pub fn normalize_to_origin(out: &mut CadDocument) {
let mut min = [f64::INFINITY; 3];
for entity in out.entities() {
let (lo, _) = crate::scene::convert::tess::entity_bounds(entity);
for axis in 0..3 {
if lo[axis] < min[axis] {
min[axis] = lo[axis];
}
}
}
if min.iter().any(|v| !v.is_finite()) {
return;
}
let shift = crate::command::EntityTransform::Affine(codec::types::Transform::from_translation(
codec::types::Vector3::new(-min[0], -min[1], -min[2]),
));
for entity in out.entities_mut() {
crate::scene::view::dispatch::apply_transform(entity, &shift);
}
}
/// One-shot point picker for the Write Block dialog's "Pick point" button.
pub struct WblockPickBasePointCommand;
impl crate::command::CadCommand for WblockPickBasePointCommand {
fn name(&self) -> &'static str {
"WBLOCK"
}
fn prompt(&self) -> String {
crate::t!("WBLOCK Specify insertion base point:").into_owned()
}
fn on_point(&mut self, pt: glam::DVec3) -> crate::command::CmdResult {
crate::command::CmdResult::Dispatch(format!(
"WBLOCK_POINT_PICKED {} {} {}",
pt.x, pt.y, pt.z
))
}
fn on_enter(&mut self) -> crate::command::CmdResult {
crate::command::CmdResult::Dispatch("WBLOCK_POINT_CANCELLED".to_string())
}
fn on_escape(&mut self) -> crate::command::CmdResult {
crate::command::CmdResult::Dispatch("WBLOCK_POINT_CANCELLED".to_string())
}
}
/// Build a standalone `CadDocument` from an explicit list of entity handles,
/// applying `base_point` translation and `unit` insertion units.
pub fn extract_entities_to_doc_with_base(
src: &CadDocument,
handles: &[codec::Handle],
base_point: glam::DVec3,
unit: i16,
) -> Result<CadDocument, String> {
let mut out = CadDocument::new();
out.header.insertion_units = unit;
if handles.is_empty() {
return Err(t!("No entities selected for WBLOCK.").into_owned());
}
// The objects keep their coordinates; the base point becomes the new
// drawing's insertion base (INSBASE).
copy_with_dependencies(src, handles, &mut out, None);
out.header.model_space_insertion_base =
codec::types::Vector3::new(base_point.x, base_point.y, base_point.z);
if out.entities().count() == 0 {
return Err(t!("None of the selected entities could be exported.").into_owned());
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use codec::entities::Line;
use codec::types::Vector3;
#[test]
fn test_extract_entities_with_base_point_and_units() {
let mut doc = CadDocument::new();
let mut line = Line::new();
line.start = Vector3::new(10.0, 20.0, 0.0);
line.end = Vector3::new(30.0, 40.0, 0.0);
let h = doc.add_entity(EntityType::Line(line)).unwrap();
let out = extract_entities_to_doc_with_base(
&doc,
&[h],
glam::DVec3::new(10.0, 20.0, 0.0),
4, // Millimeters
)
.expect("extract should succeed");
assert_eq!(out.header.insertion_units, 4);
assert_eq!(out.entities().count(), 1);
assert_eq!(
out.header.model_space_insertion_base,
Vector3::new(10.0, 20.0, 0.0)
);
let e = out.entities().next().unwrap();
if let EntityType::Line(l) = e {
assert!((l.start.x - 10.0).abs() < 1e-6);
assert!((l.start.y - 20.0).abs() < 1e-6);
assert!((l.end.x - 30.0).abs() < 1e-6);
assert!((l.end.y - 40.0).abs() < 1e-6);
} else {
panic!("Expected Line entity");
}
}
#[test]
fn test_extract_block_to_doc() {
let mut doc = CadDocument::new();
let mut line = Line::new();
line.start = Vector3::new(1.0, 2.0, 0.0);
line.end = Vector3::new(3.0, 4.0, 0.0);
let h = doc.add_entity(EntityType::Line(line)).unwrap();
let mut block_record = codec::tables::BlockRecord::new("MY_BLOCK".to_string());
block_record.entity_handles.push(h);
let _ = doc.block_records.add(block_record);
let out = extract_block_to_doc(&doc, "MY_BLOCK").expect("block extract should succeed");
assert_eq!(out.entities().count(), 1);
assert!(extract_block_to_doc(&doc, "NON_EXISTENT").is_err());
}
}