diff --git a/Cargo.lock b/Cargo.lock index 3df83216f..bfb39bf61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -921,7 +921,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=35aa7a5#35aa7a541ab7c020cd23f43978373337523e25df" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=d2a288b#d2a288b5f397ad7941719119f216a048d6cdcc6f" dependencies = [ "acadrust", "cavalier_contours", @@ -932,7 +932,7 @@ dependencies = [ [[package]] name = "cadkernel-constraints" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=35aa7a5#35aa7a541ab7c020cd23f43978373337523e25df" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=d2a288b#d2a288b5f397ad7941719119f216a048d6cdcc6f" dependencies = [ "nalgebra", ] diff --git a/Cargo.toml b/Cargo.toml index fb9fbd71d..062ce7acb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,8 +34,8 @@ clap = { version = "4", features = ["derive", "string"] } env_logger = "0.11" log = "0.4" acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "8a28c21", features = ["serde"] } -cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "35aa7a5", features = ["acis", "offset"] } -cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "35aa7a5" } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "d2a288b", features = ["acis", "offset"] } +cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "d2a288b" } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } diff --git a/assets/icons/constrain/perpendicular.svg b/assets/icons/constrain/perpendicular.svg index 520becabb..c4d2ac1a1 100644 --- a/assets/icons/constrain/perpendicular.svg +++ b/assets/icons/constrain/perpendicular.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/constrain/smooth.svg b/assets/icons/constrain/smooth.svg index 5986ce9d0..7f5425a8a 100644 --- a/assets/icons/constrain/smooth.svg +++ b/assets/icons/constrain/smooth.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/docs/screenshots/dimension-layout/after-diameter.png b/docs/screenshots/dimension-layout/after-diameter.png new file mode 100644 index 000000000..c517f24fc Binary files /dev/null and b/docs/screenshots/dimension-layout/after-diameter.png differ diff --git a/docs/screenshots/dimension-layout/after-linear-fit.png b/docs/screenshots/dimension-layout/after-linear-fit.png new file mode 100644 index 000000000..22fac4c4b Binary files /dev/null and b/docs/screenshots/dimension-layout/after-linear-fit.png differ diff --git a/docs/screenshots/dimension-layout/after-linear-radius.png b/docs/screenshots/dimension-layout/after-linear-radius.png new file mode 100644 index 000000000..bbe802e0e Binary files /dev/null and b/docs/screenshots/dimension-layout/after-linear-radius.png differ diff --git a/docs/screenshots/dimension-layout/after-radial-leaders.png b/docs/screenshots/dimension-layout/after-radial-leaders.png new file mode 100644 index 000000000..61f56aa6e Binary files /dev/null and b/docs/screenshots/dimension-layout/after-radial-leaders.png differ diff --git a/docs/screenshots/dimension-layout/before-diameter.png b/docs/screenshots/dimension-layout/before-diameter.png new file mode 100644 index 000000000..39e5cd8a1 Binary files /dev/null and b/docs/screenshots/dimension-layout/before-diameter.png differ diff --git a/docs/screenshots/dimension-layout/before-linear-fit.png b/docs/screenshots/dimension-layout/before-linear-fit.png new file mode 100644 index 000000000..deaaca293 Binary files /dev/null and b/docs/screenshots/dimension-layout/before-linear-fit.png differ diff --git a/docs/screenshots/dimension-layout/before-linear-radius.png b/docs/screenshots/dimension-layout/before-linear-radius.png new file mode 100644 index 000000000..87e8370ed Binary files /dev/null and b/docs/screenshots/dimension-layout/before-linear-radius.png differ diff --git a/docs/screenshots/dimension-layout/before-radial-leaders.png b/docs/screenshots/dimension-layout/before-radial-leaders.png new file mode 100644 index 000000000..f220ea410 Binary files /dev/null and b/docs/screenshots/dimension-layout/before-radial-leaders.png differ diff --git a/src/app/automation.rs b/src/app/automation.rs index 9ca7e8c73..ff0bc2776 100644 --- a/src/app/automation.rs +++ b/src/app/automation.rs @@ -294,7 +294,7 @@ impl OpenCADStudio { match req["op"].as_str().unwrap_or("") { "new" => { let i = self.active_tab; - self.tabs[i].scene.clear(); + self.tabs[i].scene.reset_to_new_drawing(); self.tabs[i].scene.material_base_dir = None; self.tabs[i].current_path = None; // The headless session starts on the welcome (Start) tab, which diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 2f8a5d3f9..6de0b3768 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -3182,9 +3182,24 @@ impl OpenCADStudio { &refs, driving_param.as_ref(), ) { - self.tabs[i].active_cmd = None; + let keep_smooth_selection = kind + == crate::scene::parametric_constraints::ConstraintKind::Smooth + && self.tabs[i] + .active_cmd + .as_ref() + .is_some_and(|command| command.name() == "GCSMOOTH"); + if !keep_smooth_selection { + self.tabs[i].active_cmd = None; + } self.tabs[i].snap_result = None; self.command_line.push_error(message); + if keep_smooth_selection { + if let Some(prompt) = + self.tabs[i].active_cmd.as_ref().map(|command| command.prompt()) + { + self.command_line.push_info(&prompt); + } + } return Task::none(); } let scope = self.tabs[i].current_parametric_scope(); @@ -3229,6 +3244,150 @@ impl OpenCADStudio { self.commit_undo_delta(i, pd); } } + CmdResult::AddHorizontalConstraint { + selection, + direction, + label, + } => { + use crate::command::HorizontalConstraintSelection; + use crate::scene::parametric_constraints::{ + nearest_parametric_point, nearest_parametric_point_on_entity, ConstraintKind, + DirectionalAxis, ParametricRef, + }; + + let scope = self.tabs[i].current_parametric_scope(); + let to_world = |point: glam::DVec3| { + acadrust::types::Vector3::new(point.x, point.y, point.z) + }; + let (refs, initial_fixed) = match selection { + HorizontalConstraintSelection::Reference(reference) => { + let initial_fixed = match reference.directional_axis() { + Some(DirectionalAxis::EllipseMajor | DirectionalAxis::EllipseMinor) => { + vec![ParametricRef::center(reference.entity)] + } + Some(DirectionalAxis::TextBaseline) => { + vec![ParametricRef::point(reference.entity, 0)] + } + None => Vec::new(), + }; + (vec![reference], initial_fixed) + } + HorizontalConstraintSelection::Points(first, second) => { + let resolve = |pick: crate::command::CoincidentPick| { + if let Some(handle) = pick.handle { + nearest_parametric_point_on_entity( + &self.tabs[i].scene.document, + scope, + handle, + to_world(pick.point), + ) + } else { + nearest_parametric_point( + &self.tabs[i].scene.document, + scope, + to_world(pick.point), + None, + ) + } + }; + let (Some(first_ref), Some(second_ref)) = + (resolve(first), resolve(second)) + else { + self.command_line.push_error( + "Horizontal: select supported endpoints, centers, midpoints, or vertices.", + ); + return Task::none(); + }; + if first_ref == second_ref { + self.command_line + .push_error("Horizontal: select two different points."); + return Task::none(); + } + (vec![first_ref, second_ref], vec![first_ref]) + } + }; + let axis_length = direction.x.hypot(direction.y); + if axis_length <= 1.0e-12 { + self.command_line + .push_error("Horizontal: the current UCS X axis is not supported."); + return Task::none(); + } + let direction = acadrust::types::Vector3::new( + direction.x / axis_length, + direction.y / axis_length, + 0.0, + ); + if let Err(message) = self.tabs[i].scene.validate_parametric_constraint( + ConstraintKind::Horizontal, + &refs, + None, + ) { + self.command_line.push_error(message); + return Task::none(); + } + if self + .tabs[i] + .scene + .parametric_constraint_set(scope) + .is_some_and(|set| { + set.contains_axis_constraint( + ConstraintKind::Horizontal, + &refs, + direction, + ) + }) + { + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.command_line + .push_error("Horizontal constraint is already applied."); + return Task::none(); + } + let constraints_before = self.tabs[i] + .scene + .parametric_constraint_set(scope) + .cloned() + .unwrap_or_else(|| { + crate::scene::parametric_constraints::ParametricConstraintSet::new(scope) + }); + let mut touched = Vec::new(); + for reference in &refs { + if !touched.contains(&reference.entity) { + touched.push(reference.entity); + } + } + let pending = self.begin_undo(i, label, touched.len(), true); + self.tabs[i] + .scene + .record_undo_parametric_constraints_before(scope, constraints_before); + let id = self.tabs[i] + .scene + .parametric_constraint_set_mut(scope) + .add_axis_constraint(ConstraintKind::Horizontal, refs, direction); + self.tabs[i].scene.note_parametric_constraint_applied( + scope, + id, + self.constraint_bar_display, + ); + let changes = touched + .into_iter() + .map(|handle| (handle, crate::scene::ChangeKind::Modified)) + .collect::>(); + self.tabs[i].scene.bump_entities_with_parametric_policy( + &changes, + &initial_fixed, + self.constraint_solve_mode, + ); + self.tabs[i].dirty = true; + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.command_line + .push_output("Horizontal constraint applied."); + self.refresh_properties(); + if let Some(pd) = pending { + self.commit_undo_delta(i, pd); + } + } CmdResult::AddPerpendicularConstraint { first, second, @@ -3280,7 +3439,7 @@ impl OpenCADStudio { .into_iter() .map(|handle| (handle, crate::scene::ChangeKind::Modified)) .collect::>(); - self.tabs[i].scene.bump_entities_with_parametric_policy( + self.tabs[i].scene.bump_entities_with_initial_parametric_policy( &changes, &[first_fixed, second_start], true, @@ -3357,6 +3516,88 @@ impl OpenCADStudio { self.commit_undo_delta(i, pd); } } + CmdResult::AddConcentricConstraint { + first, + second, + label, + } => { + use crate::scene::parametric_constraints::ConstraintKind; + + let refs = vec![first, second]; + if let Err(message) = self.tabs[i].scene.validate_parametric_constraint( + ConstraintKind::Concentric, + &refs, + None, + ) { + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.command_line.push_error(message); + return Task::none(); + } + let scope = self.tabs[i].current_parametric_scope(); + let duplicate = self.tabs[i] + .scene + .parametric_constraint_set(scope) + .is_some_and(|set| { + set.constraints.iter().any(|constraint| { + constraint.enabled + && constraint.kind == ConstraintKind::Concentric + && matches!(constraint.refs.as_slice(), [a, b] + if (*a == first && *b == second) + || (*a == second && *b == first)) + }) + }); + if duplicate { + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.command_line + .push_output("The Concentric constraint already exists."); + return Task::none(); + } + let constraints_before = self.tabs[i] + .scene + .parametric_constraint_set(scope) + .cloned() + .unwrap_or_else(|| { + crate::scene::parametric_constraints::ParametricConstraintSet::new(scope) + }); + let touched = if first.entity == second.entity { + vec![first.entity] + } else { + vec![first.entity, second.entity] + }; + let pending = self.begin_undo(i, label, touched.len(), true); + self.tabs[i] + .scene + .record_undo_parametric_constraints_before(scope, constraints_before); + let id = self.tabs[i].scene.parametric_constraint_set_mut(scope).add( + ConstraintKind::Concentric, + refs, + None, + ); + self.tabs[i].scene.note_parametric_constraint_applied( + scope, + id, + self.constraint_bar_display, + ); + let changes = touched + .into_iter() + .map(|handle| (handle, crate::scene::ChangeKind::Modified)) + .collect::>(); + self.tabs[i].scene.bump_entities_with_initial_parametric_policy( + &changes, + &[first], + true, + ); + self.tabs[i].dirty = true; + self.tabs[i].active_cmd = None; + self.tabs[i].snap_result = None; + self.command_line.push_output("Constraint applied."); + self.refresh_properties(); + if let Some(pd) = pending { + self.commit_undo_delta(i, pd); + } + } CmdResult::AddCoincidentConstraint { first, second, @@ -8450,6 +8691,159 @@ mod parametric_constraint_undo_tests { ); } + #[test] + fn perpendicular_initial_solve_rotates_parallel_second_line_in_kernel() { + let mut app = OpenCADStudio::new_for_test(); + let _ = app.automation_op(r#"{"op":"new"}"#); + let first = add_line(&mut app, 0.0, 0.0, 10.0, 0.0); + let second = add_line(&mut app, 20.0, 0.0, 30.0, 0.0); + + let _ = app.apply_cmd_result(CmdResult::AddPerpendicularConstraint { + first: ParametricRef::whole(first), + second: ParametricRef::whole(second), + first_fixed: ParametricRef::whole(first), + second_start: ParametricRef::point(second, 0), + label: "Perpendicular constraint", + }); + + let line = |handle| match app.tabs[app.active_tab].scene.document.get_entity(handle) { + Some(acadrust::EntityType::Line(line)) => line.clone(), + other => panic!("expected a Line, got {other:?}"), + }; + let fixed = line(first); + let moving = line(second); + assert_eq!(fixed.start, acadrust::types::Vector3::new(0.0, 0.0, 0.0)); + assert_eq!(fixed.end, acadrust::types::Vector3::new(10.0, 0.0, 0.0)); + assert!((moving.start - acadrust::types::Vector3::new(20.0, 0.0, 0.0)).length() < 1.0e-9); + assert!((moving.length() - 10.0).abs() < 1.0e-7); + assert!( + (fixed.end - fixed.start) + .dot(&(moving.end - moving.start)) + .abs() + < 1.0e-7, + "fixed={fixed:?}, moving={moving:?}" + ); + } + + #[test] + fn horizontal_initial_solve_uses_the_captured_axis_in_kernel() { + let mut app = OpenCADStudio::new_for_test(); + let _ = app.automation_op(r#"{"op":"new"}"#); + let handle = add_line(&mut app, 0.0, 0.0, 5.0, 2.0); + let (original_start, original_end, original_length) = match app.tabs[app.active_tab] + .scene + .document + .get_entity(handle) + { + Some(acadrust::EntityType::Line(line)) => (line.start, line.end, line.length()), + other => panic!("expected a Line, got {other:?}"), + }; + let direction = acadrust::types::Vector3::new(3.0, 4.0, 0.0).normalize(); + + let _ = app.apply_cmd_result(CmdResult::AddHorizontalConstraint { + selection: crate::command::HorizontalConstraintSelection::Reference( + ParametricRef::whole(handle), + ), + direction, + label: "Horizontal constraint", + }); + + let line = match app.tabs[app.active_tab].scene.document.get_entity(handle) { + Some(acadrust::EntityType::Line(line)) => line, + other => panic!("expected a Line, got {other:?}"), + }; + let solved = (line.end - line.start).normalize(); + assert!(solved.cross(&direction).length() < 1.0e-7); + assert!((line.length() - original_length).abs() < 1.0e-7); + let constraint = &app.tabs[app.active_tab] + .scene + .parametric_constraint_set(ParametricScope::ModelSpace) + .unwrap() + .constraints[0]; + assert_eq!(constraint.axis_direction, Some(direction)); + + app.undo_steps(1); + let line = match app.tabs[app.active_tab].scene.document.get_entity(handle) { + Some(acadrust::EntityType::Line(line)) => line, + other => panic!("expected a Line after undo, got {other:?}"), + }; + assert_eq!((line.start, line.end), (original_start, original_end)); + assert_eq!( + app.tabs[app.active_tab] + .scene + .parametric_constraint_set(ParametricScope::ModelSpace) + .map(|set| set.constraints.len()) + .unwrap_or(0), + 0 + ); + + app.redo_steps(1); + let line = match app.tabs[app.active_tab].scene.document.get_entity(handle) { + Some(acadrust::EntityType::Line(line)) => line, + other => panic!("expected a Line after redo, got {other:?}"), + }; + assert!((line.end - line.start).normalize().cross(&direction).length() < 1.0e-7); + } + + #[test] + fn horizontal_two_point_solve_keeps_the_first_point_fixed() { + let mut app = OpenCADStudio::new_for_test(); + let _ = app.automation_op(r#"{"op":"new"}"#); + let first = add_line(&mut app, 0.0, 0.0, 0.0, 5.0); + let second = add_line(&mut app, 8.0, 3.0, 8.0, 7.0); + let pick = |handle, point| crate::command::CoincidentPick { + handle: Some(handle), + point, + whole_curve: false, + }; + + let _ = app.apply_cmd_result(CmdResult::AddHorizontalConstraint { + selection: crate::command::HorizontalConstraintSelection::Points( + pick(first, glam::DVec3::ZERO), + pick(second, glam::DVec3::new(8.0, 3.0, 0.0)), + ), + direction: acadrust::types::Vector3::UNIT_X, + label: "Horizontal constraint", + }); + + let line = |handle| match app.tabs[app.active_tab].scene.document.get_entity(handle) { + Some(acadrust::EntityType::Line(line)) => line, + other => panic!("expected a Line, got {other:?}"), + }; + assert_eq!(line(first).start, acadrust::types::Vector3::ZERO); + assert!(line(second).start.y.abs() < 1.0e-8); + } + + #[test] + fn horizontal_minor_axis_rotates_an_ellipse_around_its_center() { + let mut app = OpenCADStudio::new_for_test(); + let _ = app.automation_op(r#"{"op":"new"}"#); + let mut ellipse = acadrust::entities::Ellipse::default(); + ellipse.center = acadrust::types::Vector3::new(3.0, 7.0, 0.0); + ellipse.major_axis = acadrust::types::Vector3::new(3.0, 4.0, 0.0); + ellipse.minor_axis_ratio = 0.4; + let handle = app.tabs[app.active_tab] + .scene + .add_entity(acadrust::EntityType::Ellipse(ellipse)); + + let _ = app.apply_cmd_result(CmdResult::AddHorizontalConstraint { + selection: crate::command::HorizontalConstraintSelection::Reference( + ParametricRef::ellipse_minor_axis(handle), + ), + direction: acadrust::types::Vector3::UNIT_X, + label: "Horizontal constraint", + }); + + let ellipse = match app.tabs[app.active_tab].scene.document.get_entity(handle) { + Some(acadrust::EntityType::Ellipse(ellipse)) => ellipse, + other => panic!("expected an Ellipse, got {other:?}"), + }; + assert!((ellipse.center - acadrust::types::Vector3::new(3.0, 7.0, 0.0)).length() < 1.0e-9); + assert!(ellipse.major_axis.x.abs() < 1.0e-7); + assert!((ellipse.major_axis.length() - 5.0).abs() < 1.0e-7); + assert!((ellipse.minor_axis_ratio - 0.4).abs() < 1.0e-9); + } + /// Drawing commands must not create parametric constraints implicitly. #[test] fn drawing_a_line_onto_an_existing_endpoint_does_not_add_a_constraint() { diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index 67a06ab81..521dd44d4 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -1016,6 +1016,26 @@ impl OpenCADStudio { } // ── Persistent constraints ──────────────────────────────────── + "CONSTRAINTBAR" | "CONSTRAINTBAR_OPTIONS" => { + use crate::modules::parametric::ConstraintBarOptionCommand; + + let handles = self.tabs[i].scene.selected_handles_in_order(); + if handles.is_empty() && cmd == "CONSTRAINTBAR" { + use crate::modules::draw::select::SelectObjectsCommand; + let command = + SelectObjectsCommand::routed("CONSTRAINTBAR", "CONSTRAINTBAR_OPTIONS"); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + } else if handles.is_empty() { + self.command_line.push_output("No objects selected."); + } else { + let command = ConstraintBarOptionCommand::new(handles); + self.command_line.push_info(&command.prompt()); + self.command_line.set_step_options(command.options()); + self.tabs[i].active_cmd = Some(Box::new(command)); + } + } + "GCSHOW" | "GCHIDE" | "DCSHOW" | "DCHIDE" => { let handles = self.tabs[i].scene.selected_handles_in_order(); if handles.is_empty() { @@ -1048,6 +1068,24 @@ impl OpenCADStudio { .push_output(format!("{} constraint indicator(s) reset.", count).as_str()); } + "CONSTRAINTBAR_RESET" => { + let handles = self.tabs[i].scene.selected_handles_in_order(); + if handles.is_empty() { + self.command_line.push_output("No objects selected."); + } else { + let scope = self.tabs[i].current_parametric_scope(); + let count = self.tabs[i].scene.set_parametric_constraint_visibility( + scope, + Some(&handles), + false, + true, + ); + self.command_line.push_output( + format!("{} constraint bar(s) reset.", count).as_str(), + ); + } + } + "GCSHOWALL" | "GCHIDEALL" | "DCSHOWALL" | "DCHIDEALL" => { let scope = self.tabs[i].current_parametric_scope(); let dimensional = cmd.starts_with("DC"); @@ -1206,17 +1244,38 @@ impl OpenCADStudio { } } - "SMOOTHCONSTRAINT" => { + "GCSMOOTH" => { + use crate::modules::parametric::SmoothConstraintCommand; let handles = self.tabs[i].scene.selected_handles_in_order(); if handles.is_empty() { - use crate::modules::draw::select::SelectObjectsCommand; - let sel = SelectObjectsCommand::new(cmd); - self.command_line.push_info(&sel.prompt()); - self.tabs[i].active_cmd = Some(Box::new(sel)); + let command = SmoothConstraintCommand::new(); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + } else if handles.len() != 2 { + self.tabs[i].scene.deselect_all(); + self.command_line.push_error( + "Smooth requires an open spline first and a target curve second.", + ); + let command = SmoothConstraintCommand::new(); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); } else { - let Some(refs) = self.tabs[i].scene.smooth_constraint_refs(&handles) else { - self.command_line - .push_output("Select one open spline and one open target curve."); + let first = self.tabs[i].scene.document.get_entity(handles[0]); + let second = self.tabs[i].scene.document.get_entity(handles[1]); + let refs = first.zip(second).and_then(|(first, second)| { + SmoothConstraintCommand::preselected_refs( + (first, handles[0]), + (second, handles[1]), + ) + }); + let Some(refs) = refs else { + self.tabs[i].scene.deselect_all(); + self.command_line.push_error( + "Smooth requires an open spline first and a line, arc, polyline segment or open spline second.", + ); + let command = SmoothConstraintCommand::new(); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); return None; }; use crate::command::CmdResult; @@ -1318,7 +1377,50 @@ impl OpenCADStudio { } } - "HCONSTRAINT" | "VCONSTRAINT" | "FXCONSTRAINT" => { + "GCHORIZONTAL" => { + use crate::command::{CmdResult, HorizontalConstraintSelection}; + use crate::modules::parametric::HorizontalConstraintCommand; + + let handles = self.tabs[i].scene.selected_handles_in_order(); + if handles.is_empty() { + let command = HorizontalConstraintCommand::new(); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + } else if handles.len() != 1 { + self.command_line + .push_error("Horizontal: select exactly one compatible object."); + } else { + let handle = handles[0]; + let reference = self.tabs[i] + .scene + .document + .get_entity(handle) + .and_then(|entity| { + HorizontalConstraintCommand::preselected_reference(entity, handle) + }); + if let Some(reference) = reference { + let direction = self.tabs[i].ucs_xform().working_plane().x; + return Some(self.apply_cmd_result(CmdResult::AddHorizontalConstraint { + selection: HorizontalConstraintSelection::Reference(reference), + direction: acadrust::types::Vector3::new( + direction.x, + direction.y, + direction.z, + ), + label: "Horizontal constraint", + })); + } + self.tabs[i].scene.deselect_all(); + self.command_line.push_error( + "Horizontal: select a line, straight polyline segment, text, MText, or an ellipse axis.", + ); + let command = HorizontalConstraintCommand::new(); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + } + } + + "VCONSTRAINT" | "FXCONSTRAINT" => { let handles = self.tabs[i].scene.selected_handles_in_order(); if handles.is_empty() { use crate::modules::draw::select::SelectObjectsCommand; @@ -1331,10 +1433,10 @@ impl OpenCADStudio { } else { use crate::command::CmdResult; use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef}; - let (kind, label) = match cmd { - "HCONSTRAINT" => (ConstraintKind::Horizontal, "Horizontal constraint"), - "VCONSTRAINT" => (ConstraintKind::Vertical, "Vertical constraint"), - _ => (ConstraintKind::Fixed, "Fixed constraint"), + let (kind, label) = if cmd == "VCONSTRAINT" { + (ConstraintKind::Vertical, "Vertical constraint") + } else { + (ConstraintKind::Fixed, "Fixed constraint") }; return Some(self.apply_cmd_result(CmdResult::AddParametricConstraint { kind, @@ -1475,29 +1577,41 @@ impl OpenCADStudio { } } - "NCONSTRAINT" => { + "GCCONCENTRIC" => { + use crate::command::CmdResult; + use crate::modules::parametric::ConcentricConstraintCommand; + let handles = self.tabs[i].scene.selected_handles_in_order(); if handles.is_empty() { - use crate::modules::draw::select::SelectObjectsCommand; - let sel = SelectObjectsCommand::new(cmd); - self.command_line.push_info(&sel.prompt()); - self.tabs[i].active_cmd = Some(Box::new(sel)); - } else if handles.len() != 2 { - self.command_line.push_output( - "Select exactly two circles/arcs, then run this constraint again.", - ); + let command = ConcentricConstraintCommand::new(); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); } else { - use crate::command::CmdResult; - use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef}; - return Some(self.apply_cmd_result(CmdResult::AddParametricConstraint { - kind: ConstraintKind::Concentric, - refs: vec![ - ParametricRef::center(handles[0]), - ParametricRef::center(handles[1]), - ], - driving_param: None, - label: "Concentric constraint", - })); + let refs = handles + .iter() + .filter_map(|handle| { + let entity = self.tabs[i].scene.document.get_entity(*handle)?; + ConcentricConstraintCommand::preselected_reference(entity, *handle) + }) + .collect::>(); + if handles.len() == 2 && refs.len() == 2 && refs[0] != refs[1] { + return Some(self.apply_cmd_result(CmdResult::AddConcentricConstraint { + first: refs[0], + second: refs[1], + label: "Concentric constraint", + })); + } + self.tabs[i].scene.deselect_all(); + self.command_line.push_error( + "Invalid selection for Concentric. Select a circle, arc, ellipse or polyline arc segment.", + ); + let command = refs + .first() + .copied() + .map(ConcentricConstraintCommand::with_first) + .unwrap_or_else(ConcentricConstraintCommand::new); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); } } diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index f3dd496cd..ec766449a 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -1051,6 +1051,7 @@ impl OpenCADStudio { | "CONSTRAINTSOLVEMODE" | "CONSTRAINTINFER" | "CONSTRAINTBARDISPLAY" + | "CONSTRAINTBARMODE" ) => { return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i); @@ -1073,7 +1074,7 @@ impl OpenCADStudio { let value = it.next().map(|s| s.trim().to_string()); if name.is_empty() || name == "?" { self.command_line.push_info( - crate::t!("SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)").as_ref(), + crate::t!("SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY CONSTRAINTBARMODE ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)").as_ref(), ); } else { if name == "CETRANSPARENCY" { @@ -1269,15 +1270,23 @@ impl OpenCADStudio { } if matches!( name.as_str(), - "CONSTRAINTSOLVEMODE" | "CONSTRAINTINFER" | "CONSTRAINTBARDISPLAY" + "CONSTRAINTSOLVEMODE" + | "CONSTRAINTINFER" + | "CONSTRAINTBARDISPLAY" + | "CONSTRAINTBARMODE" ) { let current = match name.as_str() { "CONSTRAINTSOLVEMODE" => i16::from(self.constraint_solve_mode), "CONSTRAINTINFER" => i16::from(self.constraint_infer), "CONSTRAINTBARDISPLAY" => self.constraint_bar_display, + "CONSTRAINTBARMODE" => self.constraint_bar_mode, _ => unreachable!(), }; - let maximum = if name == "CONSTRAINTBARDISPLAY" { 3 } else { 1 }; + let maximum = match name.as_str() { + "CONSTRAINTBARDISPLAY" => 3, + "CONSTRAINTBARMODE" => 4095, + _ => 1, + }; if let Some(value) = &value { match value .parse::() @@ -1293,6 +1302,7 @@ impl OpenCADStudio { "CONSTRAINTBARDISPLAY" => { self.constraint_bar_display = mode } + "CONSTRAINTBARMODE" => self.constraint_bar_mode = mode, _ => unreachable!(), } self.persist_settings_if_changed(); diff --git a/src/app/control/mod.rs b/src/app/control/mod.rs index b0da52d1e..3d44a7fb2 100644 --- a/src/app/control/mod.rs +++ b/src/app/control/mod.rs @@ -220,6 +220,25 @@ fn command_category(name: &str) -> &'static str { "view" } else if name.starts_with("DIM") || ["LEADER", "MLEADER", "TOLERANCE"].contains(&name) { "annotate" + } else if [ + "GEOMCONSTRAINT", + "GCCOINCIDENT", + "GCCOLLINEAR", + "GCPERPENDICULAR", + "QCONSTRAINT", + "PCONSTRAINT", + "GCHORIZONTAL", + "VCONSTRAINT", + "ECONSTRAINT", + "TCONSTRAINT", + "GCCONCENTRIC", + "NRCONSTRAINT", + "LCONSTRAINT", + "DELCONSTRAINT", + ] + .contains(&name) + { + "parametric" } else if [ "BOX", "CYLINDER", diff --git a/src/app/document.rs b/src/app/document.rs index 50f80fd09..e7f9041ed 100644 --- a/src/app/document.rs +++ b/src/app/document.rs @@ -1,5 +1,4 @@ use crate::command::CadCommand; -use crate::io::linetypes; use crate::modules::draw::modify::block_edit::BlockEditSession; use crate::modules::draw::modify::refedit::RefEditSession; use crate::scene::pick::grip::GripEdit; @@ -593,24 +592,7 @@ impl DocumentTab { pub(super) fn new_drawing(n: usize) -> Self { let mut scene = Scene::new(); - linetypes::populate_document(&mut scene.document); - // Paper layouts start as A4 landscape. - for obj in scene.document.objects.values_mut() { - if let acadrust::objects::ObjectType::Layout(l) = obj { - if l.name != "Model" { - l.min_limits = (0.0, 0.0); - l.max_limits = (297.0, 210.0); - l.min_extents = (0.0, 0.0, 0.0); - l.max_extents = (297.0, 210.0, 0.0); - l.paper_width = 297.0; - l.paper_height = 210.0; - l.plot_paper_units = 1; - l.plot_scale_numerator = 1.0; - l.plot_scale_denominator = 1.0; - l.paper_size = "ISO_A4_(297.00_x_210.00_MM)".into(); - } - } - } + scene.populate_new_drawing_defaults(); Self { id: NEXT_DOCUMENT_TAB_ID.fetch_add(1, Ordering::Relaxed), scene, diff --git a/src/app/mod.rs b/src/app/mod.rs index 281daca5b..a33e7e081 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -602,6 +602,7 @@ pub(super) struct OpenCADStudio { pub constraint_solve_mode: bool, pub constraint_infer: bool, pub constraint_bar_display: i16, + pub constraint_bar_mode: i16, /// Minutes between autosaves to a `.sv$` recovery file (SAVETIME command); /// 0 disables autosave. pub savetime_min: i32, @@ -3906,6 +3907,7 @@ impl OpenCADStudio { constraint_solve_mode: true, constraint_infer: false, constraint_bar_display: 3, + constraint_bar_mode: 4095, savetime_min: 10, default_bg_color: None, default_paper_bg_color: None, diff --git a/src/app/settings.rs b/src/app/settings.rs index 0be12cc9c..2ddc3c39f 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -465,6 +465,9 @@ pub struct UserSettings { /// Constraint bar display bit mask: 1 after applying, 2 on selection. #[serde(default = "default_constraint_bar_display")] pub constraint_bar_display: i16, + /// Geometric constraint type bit mask (1..2048, combined; default all). + #[serde(default = "default_constraint_bar_mode")] + pub constraint_bar_mode: i16, /// Minutes between autosaves to a `.sv$` recovery file (SAVETIME command). /// 0 disables autosave. pub savetime_min: i32, @@ -595,6 +598,10 @@ fn default_constraint_bar_display() -> i16 { 3 } +fn default_constraint_bar_mode() -> i16 { + 4095 +} + fn deserialize_clipromptlines<'de, D>(de: D) -> Result where D: serde::Deserializer<'de>, @@ -662,6 +669,7 @@ impl Default for UserSettings { constraint_solve_mode: true, constraint_infer: false, constraint_bar_display: 3, + constraint_bar_mode: 4095, savetime_min: 10, default_save_format: crate::io::DEFAULT_SAVE_FORMAT.to_string(), pick_add: true, diff --git a/src/app/update/command.rs b/src/app/update/command.rs index ec238814d..c2e7715eb 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -1739,15 +1739,13 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { .get_entity(popup.handle) .is_some_and(|entity| match (item.action, entity) { (GripMenuAction::ShowFit, acadrust::EntityType::Spline(spline)) => { - !spline.cv_frame_visible - && crate::entities::spline::uses_fit_method(spline) + crate::entities::spline::shows_fit_points(spline) } ( GripMenuAction::ShowControlVertices, acadrust::EntityType::Spline(spline), ) => { - spline.cv_frame_visible - || !crate::entities::spline::uses_fit_method(spline) + crate::entities::spline::shows_control_vertices(spline) } _ => false, }); diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 5d560690d..b2044180c 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -152,6 +152,96 @@ fn paper_name_for_settings( } } +/// The stored rotation for the dialog's sheet on a medium that is itself +/// `natural`-oriented: a quarter turn when the requested orientation differs +/// from the medium's own, and "upside-down" adds a half turn on top. This is +/// how the commercial application writes it — a system printer's portrait +/// `A3` plotted landscape carries 90°, while its PDF driver's landscape +/// `ISO_A4_(297.00_x_210.00_MM)` medium plotted landscape carries 0°. +fn plot_dialog_rotation( + d: &crate::ui::window::plot::PlotDialogState, + natural: crate::io::paper_catalog::Orientation, +) -> acadrust::objects::PlotRotation { + use acadrust::objects::PlotRotation; + match (plot_dialog_orientation(d) != natural, d.upside_down) { + (false, false) => PlotRotation::None, + (true, false) => PlotRotation::Degrees90, + (false, true) => PlotRotation::Degrees180, + (true, true) => PlotRotation::Degrees270, + } +} + +/// Millimetres in the paper unit a page setup counts in. +fn plot_paper_unit_mm(units: acadrust::objects::PlotPaperUnits) -> f64 { + match units { + acadrust::objects::PlotPaperUnits::Inches => 25.4, + _ => 1.0, + } +} + +/// The factor the commercial application stores next to the scale (DXF 147) +/// is the `paper : drawing` ratio times 25.4 on millimetre page setups and +/// the plain ratio on inch ones — observed in its files, whatever the +/// published description says. Both directions of that convention live here. +fn stored_scale_factor(ratio: f64, units: acadrust::objects::PlotPaperUnits) -> f64 { + match units { + acadrust::objects::PlotPaperUnits::Millimeters => ratio * 25.4, + _ => ratio, + } +} + +fn ratio_from_stored_scale_factor(factor: f64, units: acadrust::objects::PlotPaperUnits) -> f64 { + match units { + acadrust::objects::PlotPaperUnits::Millimeters => factor / 25.4, + _ => factor, + } +} + +/// The file format's standard scale for a `paper : drawing` ratio, when it +/// has one. Only the metric ratios exist as standard codes here; an +/// architectural scale is stored as a custom ratio with the same value. +fn standard_scale_for_ratio(paper: f64, drawing: f64) -> Option { + use acadrust::objects::ScaledType as S; + const STANDARD: &[(f64, f64, S)] = &[ + (1.0, 1.0, S::OneToOne), + (1.0, 2.0, S::OneToTwo), + (1.0, 4.0, S::OneToFour), + (1.0, 8.0, S::OneToEight), + (1.0, 10.0, S::OneToTen), + (1.0, 16.0, S::OneToSixteen), + (1.0, 20.0, S::OneToTwenty), + (1.0, 30.0, S::OneToThirty), + (1.0, 40.0, S::OneToForty), + (1.0, 50.0, S::OneToFifty), + (1.0, 100.0, S::OneToHundred), + (2.0, 1.0, S::TwoToOne), + (4.0, 1.0, S::FourToOne), + (8.0, 1.0, S::EightToOne), + (10.0, 1.0, S::TenToOne), + (100.0, 1.0, S::HundredToOne), + ]; + let ratio = paper / drawing; + STANDARD + .iter() + .find(|(p, d, _)| (p / d - ratio).abs() <= 1e-9 * ratio.abs().max(1.0)) + .map(|(_, _, scale)| *scale) +} + +/// A scale-list name for a plot scale the drawing has no name for, spelled as +/// the ratio it is (`1:250`, `25.4:1`) so the value survives in the picker +/// instead of collapsing to 1:1. +fn ratio_scale_name(factor: f64) -> String { + let trim = |value: f64| { + let text = format!("{value:.4}"); + text.trim_end_matches('0').trim_end_matches('.').to_string() + }; + if factor >= 1.0 { + format!("{}:1", trim(factor)) + } else { + format!("1:{}", trim(1.0 / factor)) + } +} + /// The plot device the dialog currently targets. fn plot_dialog_device(d: &crate::ui::window::plot::PlotDialogState) -> crate::io::plot_device::PlotDevice { use crate::io::plot_device::PlotDevice; @@ -521,6 +611,7 @@ impl OpenCADStudio { constraint_solve_mode: self.constraint_solve_mode, constraint_infer: self.constraint_infer, constraint_bar_display: self.constraint_bar_display, + constraint_bar_mode: self.constraint_bar_mode, savetime_min: self.savetime_min, default_save_format: self.default_save_format.clone(), pick_add: self.pick_add, @@ -624,6 +715,7 @@ impl OpenCADStudio { self.constraint_solve_mode = s.constraint_solve_mode; self.constraint_infer = s.constraint_infer; self.constraint_bar_display = s.constraint_bar_display.clamp(0, 3); + self.constraint_bar_mode = s.constraint_bar_mode.clamp(0, 4095); self.savetime_min = s.savetime_min; self.default_save_format = crate::io::canonical_save_format(&s.default_save_format).to_string(); @@ -3004,127 +3096,209 @@ impl OpenCADStudio { iced::exit() } - /// Write the given plot page settings into the active layout. - /// No-op on the Model tab (which has no paper layout). Marks the tab dirty - /// and re-tessellates the sheet. Called by the Plot dialog's Set current action. - #[allow(clippy::too_many_arguments)] - pub(super) fn apply_plot_page_settings( - &mut self, - w: f64, - h: f64, - plot_area: &str, - center: bool, - offset_x: f64, - offset_y: f64, - rotation: i16, - ) { - let i = self.active_tab; - let dialog = self.plot_dialog.clone(); - let layout_name = self.tabs[i].scene.current_layout.clone(); - let plot_window = self.plot_window; - if layout_name != "Model" { - let w: f64 = w.max(1.0); - let h: f64 = h.max(1.0); - use acadrust::objects::{ - PlotRotation, PlotSettings, PlotType, ScaledType, ShadePlotMode, - ShadePlotResolutionLevel, - }; - let mut ps = self - .plot_setup_template - .clone() - .or_else(|| self.tabs[i].scene.plot_settings_for(&layout_name)) - .unwrap_or_else(|| PlotSettings::new("")); - // Names and margins are decided against the stored settings before - // any field is overwritten. - let paper_size = paper_name_for_settings(&dialog, &ps); - let printer_name = device_name_for_settings(&dialog, &ps); - let margins = margins_for_settings(&dialog, &ps); - ps.paper_size = paper_size; - ps.printer_name = printer_name; - ps.margins = margins; - ps.paper_width = w; - ps.paper_height = h; - ps.plot_type = match plot_area { - "Window" => PlotType::Window, - "Display" => PlotType::LastScreenDisplay, - "Extents" => PlotType::Extents, - "Limits" => PlotType::Limits, - area if area.starts_with("View: ") => PlotType::View, - _ => PlotType::Layout, - }; - ps.plot_view_name = plot_area.strip_prefix("View: ").unwrap_or("").to_string(); - if plot_area == "Window" { - if let Some((x0, y0, x1, y1)) = plot_window { - ps.set_plot_window(x0, y0, x1, y1); - } - } - ps.flags.plot_centered = center && plot_area != "Layout"; - ps.origin_x = if plot_area == "Layout" { 0.0 } else { offset_x }; - ps.origin_y = if plot_area == "Layout" { 0.0 } else { offset_y }; - ps.rotation = match rotation { - 90 => PlotRotation::Degrees90, - 180 => PlotRotation::Degrees180, - 270 => PlotRotation::Degrees270, - _ => PlotRotation::None, - }; - if dialog.fit_to_paper && plot_area != "Layout" { - ps.set_scale_to_fit(); - } else if plot_area == "Layout" { - ps.set_standard_scale(ScaledType::OneToOne); - ps.standard_scale_factor = 1.0; - } else { - let factor = plot_dialog_scale_factor(&dialog); - ps.scale_type = ScaledType::CustomScale; - ps.scale_numerator = factor; - ps.scale_denominator = 1.0; - ps.standard_scale_factor = factor; - ps.flags.use_standard_scale = false; - } - ps.current_style_sheet = dialog.style_name.clone(); - ps.flags.scale_lineweights = dialog.scale_lw; - ps.flags.print_lineweights = dialog.lineweights; - ps.flags.plot_plot_styles = dialog.apply_plot_styles && !dialog.style_name.is_empty(); - ps.flags.show_plot_styles = dialog.show_plot_styles && !dialog.style_name.is_empty(); - ps.flags.draw_viewports_first = dialog.paperspace_last; - ps.flags.plot_hidden = dialog.shade == "Hidden Line"; - ps.shade_plot_mode = match dialog.shade.as_str() { - "2D Wireframe" | "3D Wireframe" => ShadePlotMode::Wireframe, - "Hidden Line" => ShadePlotMode::Hidden, - "As displayed" => ShadePlotMode::AsDisplayed, - _ => ShadePlotMode::Rendered, - }; - ps.shade_plot_resolution = match dialog.quality.as_str() { - "Low" => ShadePlotResolutionLevel::Draft, - "High" => ShadePlotResolutionLevel::Presentation, - _ => ShadePlotResolutionLevel::Normal, - }; - ps.shade_plot_dpi = 300; + /// The settings the dialog edits on top of: the named page setup it was + /// loaded from, else the active layout's own, else a fresh metric sheet. + /// Everything the dialog has no control for (viewport-border and + /// paper-update flags, the plot view handle, the paper unit that fixes + /// the layout's coordinate system) is carried over from here untouched. + fn dialog_base_settings(&self) -> acadrust::objects::PlotSettings { + use acadrust::objects::{PlotPaperUnits, PlotSettings}; + let scene = &self.tabs[self.active_tab].scene; + self.plot_setup_template + .clone() + .or_else(|| scene.plot_settings_for(&scene.current_layout)) + .unwrap_or_else(|| { + let mut settings = PlotSettings::new(""); + settings.paper_units = PlotPaperUnits::Millimeters; + settings + }) + } - self.tabs[i] - .scene - .set_layout_plot_settings(&layout_name, &ps); - for obj in self.tabs[i].scene.document.objects.values_mut() { - if let acadrust::objects::ObjectType::Layout(layout) = obj { - if layout.name == layout_name { - layout.min_limits = (0.0, 0.0); - layout.max_limits = (w, h); - layout.min_extents = (0.0, 0.0, 0.0); - layout.max_extents = (w, h, 0.0); - break; + /// The page setup the dialog describes, written the way the file format + /// expects it: the portrait medium with the orientation in `rotation`, + /// standard scales by their code, custom ones as a readable ratio in the + /// layout's own paper unit. Named page setups and the layout's embedded + /// settings both come from here so the two can never drift apart. + fn plot_settings_from_dialog( + &self, + mut ps: acadrust::objects::PlotSettings, + ) -> acadrust::objects::PlotSettings { + use crate::io::paper_catalog::Orientation; + use acadrust::objects::{PlotType, ScaledType, ShadePlotMode, ShadePlotResolutionLevel}; + let d = &self.plot_dialog; + let scene = &self.tabs[self.active_tab].scene; + // Names and margins are decided against the stored settings before + // any field is overwritten. + let paper_size = paper_name_for_settings(d, &ps); + let printer_name = device_name_for_settings(d, &ps); + let margins = margins_for_settings(d, &ps); + let same_medium = paper_unchanged(d, &ps) && ps.paper_width > 0.0 && ps.paper_height > 0.0; + ps.paper_size = paper_size; + ps.printer_name = printer_name; + ps.margins = margins; + // An unchanged sheet keeps the medium exactly as the drawing stores + // it — its driver's precise dimensions in the orientation the driver + // defines it (a PDF driver lists landscape media of its own) — and + // only the rotation says how the sheet is plotted on it. A new + // selection is written as the catalogue's portrait medium. + let natural = if same_medium { + if ps.paper_width > ps.paper_height { + Orientation::Landscape + } else { + Orientation::Portrait + } + } else { + let (width, height) = plot_dialog_paper(d).portrait_mm(); + ps.paper_width = width.max(1.0); + ps.paper_height = height.max(1.0); + Orientation::Portrait + }; + ps.rotation = plot_dialog_rotation(d, natural); + let layout_area = d.area == "Layout"; + ps.plot_type = match d.area.as_str() { + "Window" => PlotType::Window, + "Layout" => PlotType::Layout, + "Display" => PlotType::LastScreenDisplay, + "Limits" => PlotType::Limits, + area if area.starts_with("View: ") => PlotType::View, + _ => PlotType::Extents, + }; + ps.plot_view_name = d.area.strip_prefix("View: ").unwrap_or("").to_string(); + if d.area == "Window" { + if let Some((x0, y0, x1, y1)) = self.plot_window { + ps.set_plot_window(x0, y0, x1, y1); + } + } + ps.flags.plot_centered = d.center && !layout_area; + ps.origin_x = if layout_area { + 0.0 + } else { + d.offset_x.parse::().unwrap_or(0.0) + }; + ps.origin_y = if layout_area { + 0.0 + } else { + d.offset_y.parse::().unwrap_or(0.0) + }; + if d.fit_to_paper && !layout_area { + ps.set_scale_to_fit(); + // The commercial application marks fitted plots with the + // "standard scale" bit and leaves the ratio it computed. + ps.flags.use_standard_scale = true; + } else { + // The dialog's factor is per the drawing's scale-family unit + // (inches for an imperial drawing, else millimetres) and already + // includes the drawing-unit factor; the file stores it per the + // layout's paper unit. A named ratio is kept as written when no + // drawing-unit factor hides inside it — a metre drawing plotting + // "1:100" really plots 10 mm per unit, which is what the file + // must say — and its standard code is kept even across a + // paper-unit conversion, the way the commercial application + // writes "1:1" on an inch page setup over a millimetre paper + // space as `1 in = 25.4 units`. A layout plot goes through the + // same path: the dialog holds it at 1:1, which the conversion + // turns into whatever keeps the paper space's unit intact. + let factor = plot_dialog_scale_factor(d); + let conversion = scene.scale_family_unit_mm() / plot_paper_unit_mm(ps.paper_units); + let named = scene.scale_ratio(&d.scale).filter(|(paper, drawing)| { + (paper / drawing - factor).abs() <= 1e-9 * factor.abs().max(1.0) + }); + let standard = + named.and_then(|(paper, drawing)| standard_scale_for_ratio(paper, drawing)); + let (numerator, denominator) = match named { + Some((paper, drawing)) if (conversion - 1.0).abs() <= 1e-12 => (paper, drawing), + _ => { + let per_unit = (factor * conversion).max(1e-9); + if per_unit >= 1.0 { + (per_unit, 1.0) + } else { + (1.0, 1.0 / per_unit) } } - } - - self.tabs[i].dirty = true; - self.tabs[i].scene.bump_geometry_no_blocks(); - self.command_line.push_info( - crate::tf!( - "Page setup: {w:.1}×{h:.1} mm area={plot_area} \ - center={center} rot={rotation}°" - ) - .as_ref(), - ); + }; + // The commercial application leaves the "standard scale" bit + // clear even for a standard code and keeps the ratio in the + // numerator/denominator pair; readers that trust the bit would + // otherwise take the stored factor as the ratio itself. + ps.scale_type = standard.unwrap_or(ScaledType::CustomScale); + ps.flags.use_standard_scale = false; + ps.scale_numerator = numerator; + ps.scale_denominator = denominator; + ps.standard_scale_factor = + stored_scale_factor(numerator / denominator, ps.paper_units); } + ps.current_style_sheet = d.style_name.clone(); + ps.flags.scale_lineweights = d.scale_lw; + ps.flags.print_lineweights = d.lineweights; + ps.flags.plot_plot_styles = d.apply_plot_styles; + ps.flags.show_plot_styles = d.show_plot_styles; + ps.flags.draw_viewports_first = d.paperspace_last; + ps.flags.plot_hidden = d.shade == "Hidden Line"; + ps.shade_plot_mode = match d.shade.as_str() { + "2D Wireframe" | "3D Wireframe" => ShadePlotMode::Wireframe, + "Hidden Line" => ShadePlotMode::Hidden, + "As displayed" => ShadePlotMode::AsDisplayed, + _ => ShadePlotMode::Rendered, + }; + ps.shade_plot_resolution = match d.quality.as_str() { + "Low" => ShadePlotResolutionLevel::Draft, + "High" => ShadePlotResolutionLevel::Presentation, + _ => ShadePlotResolutionLevel::Normal, + }; + ps.shade_plot_dpi = 300; + ps + } + + /// Write the dialog's page setup into the active layout. No-op on the + /// Model tab (which has no paper layout). Marks the tab dirty and + /// re-tessellates the sheet. Called by the Plot dialog's Set current action. + pub(super) fn apply_plot_page_settings(&mut self) { + let i = self.active_tab; + let layout_name = self.tabs[i].scene.current_layout.clone(); + if layout_name == "Model" { + return; + } + let ps = self.plot_settings_from_dialog(self.dialog_base_settings()); + let (w, h) = plot_dialog_sheet_mm(&self.plot_dialog); + let plot_area = self.plot_dialog.area.clone(); + let center = ps.flags.plot_centered; + let rotation = ps.rotation.to_degrees() as i32; + self.tabs[i] + .scene + .set_layout_plot_settings(&layout_name, &ps); + // Layout limits are the sheet in paper-space units, which the + // settings just written define, placed the way the commercial + // application places it: the printable area's corner is the paper + // space origin, so the sheet starts at minus its displayed margins. + let units_per_mm = self.tabs[i].scene.paper_space_unit_factor().max(1e-9); + let m = ps.margins; + let (left, bottom, ..) = crate::scene::rotated_margins( + (m.left, m.bottom, m.right, m.top), + ps.rotation.to_code(), + ); + let (x0, y0) = (-left * units_per_mm, -bottom * units_per_mm); + let (lw, lh) = (w * units_per_mm, h * units_per_mm); + for obj in self.tabs[i].scene.document.objects.values_mut() { + if let acadrust::objects::ObjectType::Layout(layout) = obj { + if layout.name == layout_name { + layout.min_limits = (x0, y0); + layout.max_limits = (x0 + lw, y0 + lh); + layout.min_extents = (x0, y0, 0.0); + layout.max_extents = (x0 + lw, y0 + lh, 0.0); + break; + } + } + } + + self.tabs[i].dirty = true; + self.tabs[i].scene.bump_geometry_no_blocks(); + self.command_line.push_info( + crate::tf!( + "Page setup: {w:.1}×{h:.1} mm area={plot_area} \ + center={center} rot={rotation}°" + ) + .as_ref(), + ); } pub(in crate::app) fn on_plot_export_path_some( @@ -4552,94 +4726,9 @@ impl OpenCADStudio { /// Build a `PlotSettings` from the current dialog fields (for saving a named /// page setup). + /// The dialog's values as a named page setup. fn dialog_to_plotsettings(&self) -> acadrust::objects::PlotSettings { - use acadrust::objects::{ - PlotPaperUnits, PlotRotation, PlotSettings, PlotType, ScaledType, ShadePlotMode, - ShadePlotResolutionLevel, - }; - let d = &self.plot_dialog; - let (w, h) = plot_dialog_sheet_mm(d); - let mut ps = match self.plot_setup_template.clone() { - Some(settings) => settings, - None => { - let mut settings = PlotSettings::new(""); - settings.paper_units = PlotPaperUnits::Millimeters; - settings - } - }; - let paper_size = paper_name_for_settings(d, &ps); - let printer_name = device_name_for_settings(d, &ps); - let margins = margins_for_settings(d, &ps); - ps.paper_size = paper_size; - ps.printer_name = printer_name; - ps.margins = margins; - ps.paper_width = w; - ps.paper_height = h; - ps.plot_type = match d.area.as_str() { - "Window" => PlotType::Window, - "Layout" => PlotType::Layout, - "Display" => PlotType::LastScreenDisplay, - "Limits" => PlotType::Limits, - area if area.starts_with("View: ") => PlotType::View, - _ => PlotType::Extents, - }; - ps.plot_view_name = d.area.strip_prefix("View: ").unwrap_or("").to_string(); - if d.area == "Window" { - if let Some((x0, y0, x1, y1)) = self.plot_window { - ps.set_plot_window(x0, y0, x1, y1); - } - } - ps.flags.plot_centered = d.center && d.area != "Layout"; - ps.origin_x = if d.area == "Layout" { - 0.0 - } else { - d.offset_x.parse::().unwrap_or(0.0) - }; - ps.origin_y = if d.area == "Layout" { - 0.0 - } else { - d.offset_y.parse::().unwrap_or(0.0) - }; - let rot: i16 = if d.upside_down { 180 } else { 0 }; - ps.rotation = match rot { - 90 => PlotRotation::Degrees90, - 180 => PlotRotation::Degrees180, - 270 => PlotRotation::Degrees270, - _ => PlotRotation::None, - }; - if d.area == "Layout" { - ps.set_standard_scale(ScaledType::OneToOne); - ps.standard_scale_factor = 1.0; - } else if d.fit_to_paper { - ps.set_scale_to_fit(); - } else { - let factor = plot_dialog_scale_factor(d); - ps.scale_type = ScaledType::CustomScale; - ps.scale_numerator = factor; - ps.scale_denominator = 1.0; - ps.standard_scale_factor = factor; - ps.flags.use_standard_scale = false; - } - ps.current_style_sheet = d.style_name.clone(); - ps.flags.scale_lineweights = d.scale_lw; - ps.flags.print_lineweights = d.lineweights; - ps.flags.plot_plot_styles = d.apply_plot_styles && !d.style_name.is_empty(); - ps.flags.show_plot_styles = d.show_plot_styles && !d.style_name.is_empty(); - ps.flags.draw_viewports_first = d.paperspace_last; - ps.flags.plot_hidden = d.shade == "Hidden Line"; - ps.shade_plot_mode = match d.shade.as_str() { - "2D Wireframe" | "3D Wireframe" => ShadePlotMode::Wireframe, - "Hidden Line" => ShadePlotMode::Hidden, - "As displayed" => ShadePlotMode::AsDisplayed, - _ => ShadePlotMode::Rendered, - }; - ps.shade_plot_resolution = match d.quality.as_str() { - "Low" => ShadePlotResolutionLevel::Draft, - "High" => ShadePlotResolutionLevel::Presentation, - _ => ShadePlotResolutionLevel::Normal, - }; - ps.shade_plot_dpi = 300; - ps + self.plot_settings_from_dialog(self.dialog_base_settings()) } /// Load a `PlotSettings` into the dialog editor fields. @@ -4700,6 +4789,7 @@ impl OpenCADStudio { "Portrait" } .to_string(); + let family_unit_mm = self.tabs[self.active_tab].scene.scale_family_unit_mm(); let d = &mut self.plot_dialog; d.paper = paper.canonical.to_string(); d.paper_width_mm = ps.paper_width.max(1.0); @@ -4735,22 +4825,39 @@ impl OpenCADStudio { } d.upside_down = matches!(deg, 180 | 270); d.fit_to_paper = d.area != "Layout" && ps.is_scale_to_fit(); - let target_factor = if d.area == "Layout" { - 1.0 - } else if ps.flags.use_standard_scale { - if ps.standard_scale_factor.is_finite() && ps.standard_scale_factor > 0.0 { - ps.standard_scale_factor - } else { - ps.scale_type.scale_factor() - } - } else if ps.scale_denominator.abs() > 1e-9 { - ps.scale_numerator / ps.scale_denominator + let ratio = ps.scale_numerator / ps.scale_denominator; + let stored_factor = if ratio.is_finite() && ratio > 0.0 { + ratio + } else if ps.flags.use_standard_scale + && ps.standard_scale_factor.is_finite() + && ps.standard_scale_factor > 0.0 + { + ratio_from_stored_scale_factor(ps.standard_scale_factor, ps.paper_units) + } else { + ps.scale_type.scale_factor() + }; + // The file's factor is per the layout's paper unit; the picker's are + // per the drawing's scale-family unit (see `plot_settings_from_dialog`). + let target_factor = if stored_factor.is_finite() && stored_factor > 0.0 { + stored_factor * plot_paper_unit_mm(ps.paper_units) / family_unit_mm } else { 1.0 }; - if !d.fit_to_paper || !d.scales.iter().any(|(name, _)| name == &d.scale) { - d.scale = scale_name_for_factor(&d.scales, target_factor) - .or_else(|| scale_name_for_factor(&d.scales, 1.0)) + if !d.fit_to_paper { + d.scale = scale_name_for_factor(&d.scales, target_factor).unwrap_or_else(|| { + // A scale the drawing has no name for stays what it is rather + // than snapping to 1:1; the ratio entry disappears again with + // the next scale-list refresh once it is no longer selected. + let name = ratio_scale_name(target_factor); + d.scales.push((name.clone(), target_factor)); + d.scales + .sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)); + name + }); + } else if !d.scales.iter().any(|(name, _)| name == &d.scale) { + // A fitted plot's stored ratio is what the fit computed, not a + // choice; the picker just needs a valid entry behind it. + d.scale = scale_name_for_factor(&d.scales, 1.0) .or_else(|| d.scales.first().map(|(name, _)| name.clone())) .unwrap_or_else(|| "1:1".into()); } @@ -4806,12 +4913,7 @@ impl OpenCADStudio { fn apply_dialog_to_layout(&mut self) { self.sync_dialog_plot_runtime(); - let d = self.plot_dialog.clone(); - let (sheet_w, sheet_h) = plot_dialog_sheet_mm(&d); - let rotation: i16 = if d.upside_down { 180 } else { 0 }; - let off_x = d.offset_x.parse::().unwrap_or(0.0); - let off_y = d.offset_y.parse::().unwrap_or(0.0); - self.apply_plot_page_settings(sheet_w, sheet_h, &d.area, d.center, off_x, off_y, rotation); + self.apply_plot_page_settings(); } /// Open a preview PDF, export a PDF, or send the job to the chosen printer. @@ -5239,16 +5341,394 @@ mod plot_paper_tests { ps.paper_size = "A4".into(); ps.paper_width = 297.0; ps.paper_height = 210.0; + // Such drivers spell the orientation in the dimensions, not the rotation. + ps.rotation = acadrust::objects::PlotRotation::None; assert!(app.tabs[i].scene.set_layout_plot_settings("Layout1", &ps)); app } fn layout_paper(app: &OpenCADStudio) -> (String, f64, f64) { - let ps = app.tabs[app.active_tab] + let ps = layout_settings(app); + (ps.paper_size, ps.paper_width, ps.paper_height) + } + + fn layout_settings(app: &OpenCADStudio) -> acadrust::objects::PlotSettings { + app.tabs[app.active_tab] .scene .plot_settings_for("Layout1") - .unwrap(); - (ps.paper_size, ps.paper_width, ps.paper_height) + .unwrap() + } + + fn layout_rotation(app: &OpenCADStudio) -> acadrust::objects::PlotRotation { + layout_settings(app).rotation + } + + /// Store the drawing's plot settings for `Layout1` after `edit` touched them. + fn edit_layout_settings( + app: &mut OpenCADStudio, + edit: impl FnOnce(&mut acadrust::objects::PlotSettings), + ) { + let i = app.active_tab; + let mut ps = app.tabs[i].scene.plot_settings_for("Layout1").unwrap(); + edit(&mut ps); + assert!(app.tabs[i].scene.set_layout_plot_settings("Layout1", &ps)); + } + + #[test] + fn a_new_drawing_starts_with_a_compatible_default_layout() { + use acadrust::objects::{PlotRotation, PlotType, ScaledType}; + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + let _ = app.update(Message::LayoutSwitch("Layout1".into())); + let ps = layout_settings(&app); + assert_eq!(ps.paper_size, "ISO_A4_(210.00_x_297.00_MM)"); + assert_eq!((ps.paper_width, ps.paper_height), (210.0, 297.0)); + assert_eq!(ps.rotation, PlotRotation::Degrees90); + assert_eq!(ps.printer_name, "none_device"); + assert_eq!(ps.plot_type, PlotType::Layout); + assert_eq!(ps.scale_type, ScaledType::OneToOne); + assert!(ps.flags.use_standard_scale && ps.flags.print_lineweights); + // The sheet on screen is the landscape A4 the rotation describes. + let ((x0, y0), (x1, y1)) = app.tabs[app.active_tab].scene.paper_limits().unwrap(); + assert_eq!((x1 - x0, y1 - y0), (297.0, 210.0)); + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + let _ = app.on_plot_dialog_open(); + assert_eq!(app.plot_dialog.orientation, "Landscape"); + assert!(!app.plot_dialog.upside_down); + } + + #[test] + fn orientation_is_stored_as_a_rotation_of_the_medium() { + use acadrust::objects::PlotRotation as R; + use crate::ui::window::plot::PlotFlag; + // A newly picked sheet is the catalogue's portrait medium… + let portrait_medium = [ + ("Portrait", false, R::None), + ("Landscape", false, R::Degrees90), + ("Portrait", true, R::Degrees180), + ("Landscape", true, R::Degrees270), + ]; + // …while the drawing's own landscape `A4` medium is kept, so the + // same choices rotate relative to it. + let landscape_medium = [ + ("Landscape", false, R::None), + ("Portrait", false, R::Degrees90), + ("Landscape", true, R::Degrees180), + ("Portrait", true, R::Degrees270), + ]; + for (pick_a3, cases) in [(true, portrait_medium), (false, landscape_medium)] { + for (orientation, upside_down, rotation) in cases { + let mut app = app_with_printer_named_sheet(); + let _ = app.on_plot_dialog_open(); + if pick_a3 { + let a3 = "ISO_A3_(297.00_x_420.00_MM)"; + let _ = app.on_plot_dlg(PlotDlgMsg::Paper(a3.into())); + } + let _ = app.on_plot_dlg(PlotDlgMsg::Orientation(orientation.into())); + if upside_down { + let _ = app.on_plot_dlg(PlotDlgMsg::Flag(PlotFlag::UpsideDown)); + } + let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent); + let ps = layout_settings(&app); + let medium = if pick_a3 { (297.0, 420.0) } else { (297.0, 210.0) }; + assert_eq!((ps.paper_width, ps.paper_height), medium, "{orientation}"); + assert_eq!(ps.rotation, rotation, "{orientation} upside-down={upside_down}"); + // The next dialog reads the same choices back out of the rotation. + let _ = app.on_plot_dialog_open(); + assert_eq!(app.plot_dialog.orientation, orientation); + assert_eq!(app.plot_dialog.upside_down, upside_down); + let sheet = super::plot_dialog_sheet_mm(&app.plot_dialog); + let long_side = medium.0.max(medium.1); + assert_eq!(sheet.0 > sheet.1, orientation == "Landscape", "{sheet:?}"); + assert_eq!(sheet.0.max(sheet.1), long_side); + } + } + } + + #[test] + fn standard_scales_are_written_by_code_and_others_as_ratios() { + use acadrust::objects::ScaledType; + let mut app = app_with_printer_named_sheet(); + let _ = app.on_plot_dialog_open(); + let _ = app.on_plot_dlg(PlotDlgMsg::Area("Extents".into())); + let _ = app.on_plot_dlg(PlotDlgMsg::Flag(crate::ui::window::plot::PlotFlag::FitToPaper)); + assert!(!app.plot_dialog.fit_to_paper); + let _ = app.on_plot_dlg(PlotDlgMsg::Scale("1:100".into())); + let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent); + let ps = layout_settings(&app); + assert_eq!(ps.scale_type, ScaledType::OneToHundred); + assert!( + !ps.flags.use_standard_scale, + "the bit stays clear, as the source application writes it" + ); + assert_eq!((ps.scale_numerator, ps.scale_denominator), (1.0, 100.0)); + // The stored factor follows the millimetre convention: ratio × 25.4. + assert!((ps.standard_scale_factor - 0.254).abs() < 1e-12); + let _ = app.on_plot_dialog_open(); + assert_eq!(app.plot_dialog.scale, "1:100"); + // A ratio the format has no code for is a custom scale, spelled as the ratio. + let _ = app.on_plot_dlg(PlotDlgMsg::Scale("1:3".into())); + let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent); + let ps = layout_settings(&app); + assert_eq!(ps.scale_type, ScaledType::CustomScale); + assert!(!ps.flags.use_standard_scale); + assert_eq!((ps.scale_numerator, ps.scale_denominator), (1.0, 3.0)); + // …and survives a reopen even though the drawing's scale list lacks it. + let _ = app.on_plot_dialog_open(); + assert_eq!(app.plot_dialog.scale, "1:3"); + assert!((super::plot_dialog_scale_factor(&app.plot_dialog) - 1.0 / 3.0).abs() < 1e-12); + // Fit to paper is its own scale type and carries the bit; a Layout + // plot goes back to the 1:1 the dialog holds it at. + let _ = app.on_plot_dlg(PlotDlgMsg::Flag(crate::ui::window::plot::PlotFlag::FitToPaper)); + let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent); + let ps = layout_settings(&app); + assert_eq!(ps.scale_type, ScaledType::ScaleToFit); + assert!(ps.flags.use_standard_scale); + let _ = app.on_plot_dlg(PlotDlgMsg::Area("Layout".into())); + let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent); + let ps = layout_settings(&app); + assert_eq!(ps.scale_type, ScaledType::OneToOne); + assert!(!ps.flags.use_standard_scale); + assert_eq!((ps.scale_numerator, ps.scale_denominator), (1.0, 1.0)); + assert!((ps.standard_scale_factor - 25.4).abs() < 1e-12); + } + + #[test] + fn a_metre_drawing_stores_the_millimetres_it_really_plots_per_unit() { + use acadrust::objects::ScaledType; + let mut app = app_with_printer_named_sheet(); + // Insertion units: metres. "1:100" then plots 10 mm per drawing unit. + app.tabs[app.active_tab].scene.document.header.insertion_units = 6; + let _ = app.on_plot_dialog_open(); + let _ = app.on_plot_dlg(PlotDlgMsg::Area("Extents".into())); + let _ = app.on_plot_dlg(PlotDlgMsg::Flag(crate::ui::window::plot::PlotFlag::FitToPaper)); + let _ = app.on_plot_dlg(PlotDlgMsg::Scale("1:100".into())); + assert!((super::plot_dialog_scale_factor(&app.plot_dialog) - 10.0).abs() < 1e-9); + let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent); + let ps = layout_settings(&app); + assert_eq!(ps.scale_type, ScaledType::CustomScale, "not the format's 1:100"); + assert_eq!((ps.scale_numerator, ps.scale_denominator), (10.0, 1.0)); + assert!((ps.standard_scale_factor - 254.0).abs() < 1e-9); + let _ = app.on_plot_dialog_open(); + assert_eq!(app.plot_dialog.scale, "1:100"); + } + + #[test] + fn an_inch_layout_keeps_its_unit_and_converts_the_scale() { + use acadrust::objects::{PlotPaperUnits, ScaledType}; + let mut app = app_with_printer_named_sheet(); + edit_layout_settings(&mut app, |ps| ps.paper_units = PlotPaperUnits::Inches); + let _ = app.on_plot_dialog_open(); + let _ = app.on_plot_dlg(PlotDlgMsg::Area("Extents".into())); + let _ = app.on_plot_dlg(PlotDlgMsg::Flag(crate::ui::window::plot::PlotFlag::FitToPaper)); + let _ = app.on_plot_dlg(PlotDlgMsg::Scale("1:100".into())); + let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent); + let ps = layout_settings(&app); + assert_eq!( + ps.paper_units, + PlotPaperUnits::Inches, + "the layout's unit is not ours to change" + ); + // 1 mm = 100 units is 1 inch = 2540 units on an inch page setup; the + // standard code still names the choice, as the source application does. + assert_eq!(ps.scale_type, ScaledType::OneToHundred); + assert_eq!((ps.scale_numerator, ps.scale_denominator), (1.0, 2540.0)); + assert!((ps.standard_scale_factor - 1.0 / 2540.0).abs() < 1e-15); + let _ = app.on_plot_dialog_open(); + assert_eq!(app.plot_dialog.scale, "1:100"); + // A layout plot keeps the paper space in millimetres: "1:1" is + // `1 in = 25.4 units`. + let _ = app.on_plot_dlg(PlotDlgMsg::Area("Layout".into())); + let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent); + let ps = layout_settings(&app); + assert_eq!(ps.scale_type, ScaledType::OneToOne); + assert_eq!((ps.scale_numerator, ps.scale_denominator), (1.0, 25.4)); + assert!((ps.standard_scale_factor - 1.0 / 25.4).abs() < 1e-15); + } + + #[test] + fn a_named_setup_and_the_layout_settings_are_written_alike() { + let mut app = app_with_printer_named_sheet(); + let _ = app.on_plot_dialog_open(); + let _ = app.on_plot_dlg(PlotDlgMsg::Printer(crate::ui::window::plot::OUT_PDF.into())); + let _ = app.on_plot_dlg(PlotDlgMsg::Paper("ISO_A3_(297.00_x_420.00_MM)".into())); + let _ = app.on_plot_dlg(PlotDlgMsg::Area("Extents".into())); + let _ = app.on_plot_dlg(PlotDlgMsg::Flag(crate::ui::window::plot::PlotFlag::FitToPaper)); + let _ = app.on_plot_dlg(PlotDlgMsg::Scale("1:50".into())); + let _ = app.on_plot_dlg(PlotDlgMsg::Flag(crate::ui::window::plot::PlotFlag::UpsideDown)); + let named = app.dialog_to_plotsettings(); + let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent); + let layout = layout_settings(&app); + assert_eq!(layout.paper_size, named.paper_size); + assert_eq!(layout.printer_name, named.printer_name); + assert_eq!(layout.margins, named.margins); + assert_eq!( + (layout.paper_width, layout.paper_height), + (named.paper_width, named.paper_height) + ); + assert_eq!(layout.rotation, named.rotation); + assert_eq!(layout.plot_type, named.plot_type); + assert_eq!((layout.origin_x, layout.origin_y), (named.origin_x, named.origin_y)); + assert_eq!(layout.scale_type, named.scale_type); + assert_eq!( + (layout.scale_numerator, layout.scale_denominator), + (named.scale_numerator, named.scale_denominator) + ); + assert_eq!(layout.standard_scale_factor, named.standard_scale_factor); + assert_eq!(layout.flags, named.flags); + assert_eq!(layout.shade_plot_mode, named.shade_plot_mode); + assert_eq!(layout.shade_plot_resolution, named.shade_plot_resolution); + assert_eq!(named.rotation, acadrust::objects::PlotRotation::Degrees270); + assert_eq!(named.scale_type, acadrust::objects::ScaledType::OneToFifty); + } + + #[test] + fn flags_the_dialog_does_not_edit_survive_a_round_trip() { + let mut app = app_with_printer_named_sheet(); + edit_layout_settings(&mut app, |ps| { + ps.flags.plot_viewport_borders = true; + ps.flags.update_paper = true; + ps.flags.zoom_to_paper_on_update = true; + ps.flags.unknown_bits = 0x8000; + ps.paper_image_origin_x = 3.5; + }); + let _ = app.on_plot_dialog_open(); + let _ = app.on_plot_dlg(PlotDlgMsg::Paper("ISO_A3_(297.00_x_420.00_MM)".into())); + let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent); + let ps = layout_settings(&app); + assert!(ps.flags.plot_viewport_borders); + assert!(ps.flags.update_paper && ps.flags.zoom_to_paper_on_update); + assert_eq!(ps.flags.unknown_bits, 0x8000); + assert_eq!(ps.paper_image_origin_x, 3.5); + } + + /// Open the metric page-setup fixture written by the commercial + /// application (see `tests/fixtures/plot/README.md`). + #[cfg(not(target_arch = "wasm32"))] + fn app_with_fixture() -> OpenCADStudio { + let mut app = OpenCADStudio::new_for_test(); + let path = concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/fixtures/plot/page-setups-metric.dxf" + ); + let reply = app.automation_op(&format!(r#"{{"op":"open","path":"{path}"}}"#)); + assert_eq!(reply["ok"], true, "{reply}"); + app + } + + /// The page-setup fields the file format stores, minus handles. + fn stored_fields(ps: &acadrust::objects::PlotSettings) -> String { + format!( + "{} | {} | {:?} | {:.6} {:.6} | {:.6} {:.6} {:.6} {:.6} | {:.6} {:.6} | {} {} \ + | {:.6} / {:.6} | {:.9} | {:?} | {:?} | {:?} {:?} {}", + ps.printer_name, + ps.paper_size, + ps.paper_units, + ps.paper_width, + ps.paper_height, + ps.margins.left, + ps.margins.bottom, + ps.margins.right, + ps.margins.top, + ps.origin_x, + ps.origin_y, + ps.rotation.to_code(), + ps.plot_type.to_code(), + ps.scale_numerator, + ps.scale_denominator, + ps.standard_scale_factor, + ps.scale_type, + ps.flags, + ps.shade_plot_mode, + ps.shade_plot_resolution, + ps.shade_plot_dpi + ) + } + + /// Every layout of the fixture survives "open the dialog, apply" without + /// a single stored field changing — the dialog reads the source + /// application's conventions and writes them back the same way. + #[cfg(not(target_arch = "wasm32"))] + #[test] + fn fixture_page_setups_round_trip_through_the_dialog_unchanged() { + use acadrust::objects::{PlotRotation, ScaledType}; + let mut app = app_with_fixture(); + let a3_medium = (297.0106506347656, 419.9889831542968); + let arch_d_medium = (914.4000244140626, 609.5999755859375); + let expectations = [ + ("A4 1-100", PlotRotation::None, (297.0, 210.0), true), + ("A3 - plotter", PlotRotation::Degrees90, a3_medium, false), + ("ARCH D - PLOTTER", PlotRotation::None, arch_d_medium, true), + ]; + for (layout, rotation, medium, pdf) in expectations { + let orientation = "Landscape"; + let _ = app.update(Message::LayoutSwitch(layout.into())); + let i = app.active_tab; + let before = app.tabs[i].scene.plot_settings_for(layout).unwrap(); + assert_eq!(before.rotation, rotation, "{layout}"); + assert_eq!((before.paper_width, before.paper_height), medium, "{layout}"); + assert_eq!(before.scale_type, ScaledType::OneToOne, "{layout}"); + let _ = app.on_plot_dialog_open(); + let d = &app.plot_dialog; + assert_eq!(d.orientation, orientation, "{layout}"); + assert!(!d.upside_down); + assert_eq!(d.area, "Layout"); + assert_eq!(d.scale, "1:1", "{layout}: a 1:1 plot in either paper unit"); + assert_eq!(d.to_file, pdf, "{layout}"); + assert_eq!(super::plot_dialog_sheet_mm(d).0 > super::plot_dialog_sheet_mm(d).1, true); + assert!(d.lineweights && d.apply_plot_styles && d.paperspace_last, "{layout}"); + let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent); + let after = app.tabs[i].scene.plot_settings_for(layout).unwrap(); + assert_eq!(stored_fields(&after), stored_fields(&before), "{layout}"); + } + // The system printer's sheet keeps its driver's margins and lands on + // the layout the way the source application places it: the medium's + // top margin on the left, its left margin at the bottom. + let _ = app.update(Message::LayoutSwitch("A3 - plotter".into())); + let ((x0, y0), (x1, y1)) = app.tabs[app.active_tab].scene.paper_limits().unwrap(); + assert!((x0 + 20.02365112304687).abs() < 1e-9 && (y0 + 4.995333194732666).abs() < 1e-9); + assert!((x1 - 399.96533203125).abs() < 1e-6 && (y1 - 292.015317440033).abs() < 1e-6); + } + + /// Picking a new sheet on the fixture's PDF layout writes our canonical + /// portrait medium with the orientation in the rotation and the PDF + /// driver's margins for that medium. + #[cfg(not(target_arch = "wasm32"))] + #[test] + fn fixture_pdf_layout_takes_a_new_sheet_in_canonical_form() { + use acadrust::objects::{PaperMargin, PlotRotation}; + let mut app = app_with_fixture(); + let _ = app.update(Message::LayoutSwitch("A4 1-100".into())); + let _ = app.on_plot_dialog_open(); + let _ = app.on_plot_dlg(PlotDlgMsg::Paper("ISO_A3_(297.00_x_420.00_MM)".into())); + let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent); + let ps = app.tabs[app.active_tab].scene.plot_settings_for("A4 1-100").unwrap(); + assert_eq!(ps.printer_name, "DWG To PDF.pc3"); + assert_eq!(ps.paper_size, "ISO_A3_(297.00_x_420.00_MM)"); + assert_eq!((ps.paper_width, ps.paper_height), (297.0, 420.0)); + assert_eq!(ps.rotation, PlotRotation::Degrees90); + assert_eq!(ps.margins, PaperMargin::new(5.0, 17.0, 6.0, 18.0)); + // The sheet sits at minus its displayed margins: top on the left, + // left at the bottom. + let ((x0, y0), _) = app.tabs[app.active_tab].scene.paper_limits().unwrap(); + assert!((x0 + 18.0).abs() < 1e-9 && (y0 + 5.0).abs() < 1e-9, "{x0} {y0}"); + } + + #[test] + fn scale_names_parse_to_their_ratios() { + use crate::scene::Scene; + assert_eq!(Scene::parse_scale_name_ratio("1:250"), Some((1.0, 250.0))); + assert_eq!(Scene::parse_scale_name_ratio("2:1"), Some((2.0, 1.0))); + assert_eq!(Scene::parse_scale_name_ratio("1/8\" = 1'-0\""), Some((0.125, 12.0))); + assert_eq!(Scene::parse_scale_name_ratio("3/32\" = 1'-0\""), Some((0.09375, 12.0))); + assert_eq!(Scene::parse_scale_name_ratio("1'-0\" = 1'-0\""), Some((12.0, 12.0))); + assert_eq!(Scene::parse_scale_name_ratio("6\" = 1'-0\""), Some((6.0, 12.0))); + assert_eq!(Scene::parse_scale_name_ratio("Fit"), None); + assert_eq!(Scene::parse_scale_name_ratio("1:0"), None); + assert_eq!(super::ratio_scale_name(0.004), "1:250"); + assert_eq!(super::ratio_scale_name(25.4), "25.4:1"); + assert_eq!(super::ratio_scale_name(1.0 / 3.0), "1:3"); } #[test] @@ -5267,7 +5747,9 @@ mod plot_paper_tests { let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent); let (name, w, h) = layout_paper(&app); assert_eq!(name, "A4", "the driver's own spelling must survive the dialog"); + // …and so must the driver's landscape medium: unrotated, as stored. assert_eq!((w, h), (297.0, 210.0)); + assert_eq!(layout_rotation(&app), acadrust::objects::PlotRotation::None); } #[test] @@ -5279,7 +5761,8 @@ mod plot_paper_tests { let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent); let (name, w, h) = layout_paper(&app); assert_eq!(name, "ISO_A3_(297.00_x_420.00_MM)"); - assert_eq!((w, h), (420.0, 297.0)); + assert_eq!((w, h), (297.0, 420.0)); + assert_eq!(layout_rotation(&app), acadrust::objects::PlotRotation::Degrees90); // The next dialog remembers the sheet the user chose. assert_eq!(app.plot_paper.canonical, "ISO_A3_(297.00_x_420.00_MM)"); } @@ -5346,7 +5829,7 @@ mod plot_paper_tests { let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent); let ps = app.tabs[i].scene.plot_settings_for("Layout1").unwrap(); assert_eq!(ps.margins, acadrust::objects::PaperMargin::new(5.0, 17.0, 6.0, 18.0)); - assert_eq!((ps.paper_width, ps.paper_height), (420.0, 297.0)); + assert_eq!((ps.paper_width, ps.paper_height), (297.0, 420.0)); } #[test] @@ -5394,7 +5877,8 @@ mod plot_paper_tests { let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent); let ps = app.tabs[app.active_tab].scene.plot_settings_for("Layout1").unwrap(); assert_eq!(ps.paper_size, "Roll_24_(609.60_x_1500.00_MM)"); - assert_eq!((ps.paper_width, ps.paper_height), (1500.0, 609.6)); + assert_eq!((ps.paper_width, ps.paper_height), (609.6, 1500.0)); + assert_eq!(ps.rotation, acadrust::objects::PlotRotation::Degrees90); assert_eq!(ps.margins, acadrust::objects::PaperMargin::new(3.0, 17.0, 6.0, 4.0)); // Re-adding the same size replaces the definition instead of duplicating it. let _ = app.on_plot_dlg(custom(C::Open)); @@ -5507,7 +5991,11 @@ cupsPrintQuality/Print Quality: *Normal High\n"; assert_eq!(app.plot_dialog.paper, "Roll_24_(609.60_x_1500.00_MM)"); assert_eq!(super::plot_dialog_sheet_mm(&app.plot_dialog), (1500.0, 609.6)); let _ = app.on_plot_dlg(PlotDlgMsg::SetCurrent); - assert_eq!(layout_paper(&app).0, "Roll_24_(609.60_x_1500.00_MM)"); + let (name, w, h) = layout_paper(&app); + assert_eq!(name, "Roll_24_(609.60_x_1500.00_MM)"); + // The drawing's own landscape medium stays as it was stored. + assert_eq!((w, h), (1500.0, 609.6)); + assert_eq!(layout_rotation(&app), acadrust::objects::PlotRotation::None); } } diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 3573234d5..480608dcd 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -1508,8 +1508,7 @@ impl OpenCADStudio { Message::ClearScene => { let i = self.active_tab; self.push_undo_snapshot(i, "CLEAR"); - self.tabs[i].scene.clear(); - crate::io::linetypes::populate_document(&mut self.tabs[i].scene.document); + self.tabs[i].scene.reset_to_new_drawing(); self.tabs[i].properties = PropertiesPanel::empty(); let doc_layers = self.tabs[i].scene.document.layers.clone(); let vp_info = self.tabs[i].scene.viewport_list(); diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index d508a0537..6241fde5b 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -3051,6 +3051,7 @@ impl OpenCADStudio { vp_size, self.show_constraint_values, self.constraint_bar_display, + self.constraint_bar_mode, cursor, )?; let set = self.tabs[i].scene.parametric_constraint_set(scope)?; @@ -4577,6 +4578,7 @@ properties={:.1}ms picked={}", canvas_sz, self.show_constraint_values, self.constraint_bar_display, + self.constraint_bar_mode, p_full, ) }) @@ -6031,24 +6033,7 @@ properties={:.1}ms picked={}", if let acadrust::objects::ObjectType::Layout(l) = obj { if l.name == new_name { l.flags = layout_flags; - l.min_limits = (0.0, 0.0); - l.max_limits = (297.0, 210.0); - l.min_extents = (0.0, 0.0, 0.0); - l.max_extents = (297.0, 210.0, 0.0); - l.paper_width = 297.0; - l.paper_height = 210.0; - l.plot_paper_units = 1; - l.plot_scale_numerator = 1.0; - l.plot_scale_denominator = 1.0; - l.plot_scale_type = 16; - l.plot_scale_factor = 1.0; - l.plot_type = 5; - l.plot_flags.use_standard_scale = true; - l.plot_flags.print_lineweights = true; - l.plot_flags.plot_plot_styles = !plot_style.is_empty(); - l.plot_flags.show_plot_styles = !plot_style.is_empty(); - l.plot_style_sheet = plot_style; - l.paper_size = "ISO_A4_(297.00_x_210.00_MM)".into(); + crate::scene::apply_default_page_setup(l, &plot_style); break; } } @@ -6683,6 +6668,7 @@ mod selection_preview_tests { (800.0, 600.0), true, 3, + 4095, )[0].1; let point = (-30..=30).find_map(|y| { (-30..=30).find_map(|x| { diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 7fefbba3c..52d4e18cb 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -611,7 +611,8 @@ bg={bg_ms:.1}ms n={view_count}" }; let control_polygon = tab.selected_handle.and_then(|handle| { let spline = match tab.scene.document.get_entity(handle) { - Some(acadrust::EntityType::Spline(spline)) if spline.cv_frame_visible => spline, + Some(acadrust::EntityType::Spline(spline)) + if crate::entities::spline::shows_control_vertices(spline) => spline, _ => return None, }; if tab @@ -833,6 +834,23 @@ bg={bg_ms:.1}ms n={view_count}" .active_cmd .as_ref() .is_some_and(|cmd| !cmd.needs_entity_pick() && !cmd.is_selection_gathering()); + let constraint_cursor_badge = if is_paper { + None + } else { + tab.scene.hover_highlight.and_then(|handle| { + tab.scene + .parametric_constraint_set(tab.current_parametric_scope()) + .and_then(|set| { + set.constraints_touching(handle) + .find(|constraint| { + constraint.kind + == crate::scene::parametric_constraints::ConstraintKind::Concentric + }) + .or_else(|| set.constraints_touching(handle).next()) + }) + .map(|constraint| constraint.kind.glyph_symbol().to_string()) + }) + }; let constraint_glyphs: Vec<( iced::Point, [f32; 2], @@ -850,6 +868,7 @@ bg={bg_ms:.1}ms n={view_count}" sel_ref.vp_size, self.show_constraint_values, self.constraint_bar_display, + self.constraint_bar_mode, ) .into_iter() .map(|(id, point, direction, label, is_conflicting, hover_points)| { @@ -910,6 +929,7 @@ bg={bg_ms:.1}ms n={view_count}" constraint_glyphs, self.constraint_glyph_tooltip .map(|kind| crate::t!(kind.label()).into_owned()), + constraint_cursor_badge, ) }; diff --git a/src/command.rs b/src/command.rs index fb7138b02..44852bd88 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1304,6 +1304,16 @@ pub struct CoincidentPick { pub whole_curve: bool, } +/// The two input forms accepted by the Horizontal geometric constraint. +/// Object picks are resolved while the entity snapshot is available; point +/// picks are resolved by the host against the live document so two points on +/// the same entity remain distinguishable. +#[derive(Clone, Copy, Debug)] +pub enum HorizontalConstraintSelection { + Reference(crate::scene::parametric_constraints::ParametricRef), + Points(CoincidentPick, CoincidentPick), +} + /// Construction options shared by SWEEP creation and its live preview. #[derive(Clone, Copy, Debug, PartialEq)] pub struct SweepOptions { @@ -1550,6 +1560,14 @@ pub enum CmdResult { /// Undo-history label, e.g. `"Horizontal constraint"`. label: &'static str, }, + /// Adds a Horizontal relation against the UCS X direction captured when + /// the command starts. The direction is persisted with the constraint so + /// later edits and save/reopen do not silently fall back to world X. + AddHorizontalConstraint { + selection: HorizontalConstraintSelection, + direction: acadrust::types::Vector3, + label: &'static str, + }, /// Adds an ordered perpendicular relation. The first picked direction and /// the second direction's start point stay fixed during the initial solve; /// those temporary anchors are not persisted as geometric constraints. @@ -1568,6 +1586,13 @@ pub enum CmdResult { second: crate::scene::parametric_constraints::ParametricRef, label: &'static str, }, + /// Adds an ordered concentric relation. The first picked center remains + /// fixed during the initial solve while the second curve moves rigidly. + AddConcentricConstraint { + first: crate::scene::parametric_constraints::ParametricRef, + second: crate::scene::parametric_constraints::ParametricRef, + label: &'static str, + }, /// Opens the Auto Constrain settings dialog from the selection prompt. OpenAutoConstrainSettings, /// Adds an ordered Coincident relation. Point/point selections create a @@ -2826,14 +2851,14 @@ mod constraint_registry_tests { "CPCONSTRAINT", "MPCONSTRAINT", "OCCONSTRAINT", - "HCONSTRAINT", + "GCHORIZONTAL", "VCONSTRAINT", "PCONSTRAINT", "QCONSTRAINT", "GCPERPENDICULAR", "ECONSTRAINT", "TCONSTRAINT", - "NCONSTRAINT", + "GCCONCENTRIC", "NRCONSTRAINT", "LCONSTRAINT", "FXCONSTRAINT", diff --git a/src/entities/arc.rs b/src/entities/arc.rs index 3c354c20c..c8315524a 100644 --- a/src/entities/arc.rs +++ b/src/entities/arc.rs @@ -212,9 +212,9 @@ fn properties(arc: &Arc) -> Vec { ro(t!("Total angle").as_ref(), "total_angle", format_angle(total_angle.to_radians())), ro(t!("Arc length").as_ref(), "arc_length", format_length(arc_length)), ro(t!("Area").as_ref(), "area", format_area(area)), - ro(t!("Normal X").as_ref(), "normal_x", format!("{:.4}", arc.normal.x)), - ro(t!("Normal Y").as_ref(), "normal_y", format!("{:.4}", arc.normal.y)), - ro(t!("Normal Z").as_ref(), "normal_z", format!("{:.4}", arc.normal.z)), + edit(t!("Normal X").as_ref(), "normal_x", arc.normal.x), + edit(t!("Normal Y").as_ref(), "normal_y", arc.normal.y), + edit(t!("Normal Z").as_ref(), "normal_z", arc.normal.z), ], }] } @@ -244,6 +244,24 @@ fn apply_geom_prop(arc: &mut Arc, field: &str, value: &str) { "radius" if v > 0.0 => arc.radius = v, "start_angle" => arc.start_angle = v.to_radians(), "end_angle" => arc.end_angle = v.to_radians(), + "normal_x" | "normal_y" | "normal_z" => { + let center = arc.center_wcs(); + let mut normal = cadkernel::space::Vec3::new(arc.normal.x, arc.normal.y, arc.normal.z); + match field { + "normal_x" => normal.x = v, + "normal_y" => normal.y = v, + "normal_z" => normal.z = v, + _ => {} + } + if let Some(normal) = normal.normalize() { + arc.normal = acadrust::types::Vector3::new(normal.x, normal.y, normal.z); + let (x, y, z) = crate::scene::view::transform::wcs_point_to_ocs( + (center.x, center.y, center.z), + (normal.x, normal.y, normal.z), + ); + arc.center = acadrust::types::Vector3::new(x, y, z); + } + } _ => {} } } diff --git a/src/entities/circle.rs b/src/entities/circle.rs index 7d596506e..5cef35386 100644 --- a/src/entities/circle.rs +++ b/src/entities/circle.rs @@ -3,7 +3,7 @@ use crate::t; use crate::command::EntityTransform; use crate::entities::common::{ - center_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip, + center_grip, edit_prop as edit, parse_f64, square_grip, }; use crate::entities::traits::RenderConvertible; use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, RenderObject}; @@ -169,9 +169,9 @@ fn properties(circle: &Circle) -> Vec { edit(t!("Diameter").as_ref(), "diameter", r * 2.0), edit(t!("Circumference").as_ref(), "circumference", 2.0 * PI * r), edit(t!("Area").as_ref(), "area", PI * r * r), - ro(t!("Normal X").as_ref(), "normal_x", format!("{:.4}", circle.normal.x)), - ro(t!("Normal Y").as_ref(), "normal_y", format!("{:.4}", circle.normal.y)), - ro(t!("Normal Z").as_ref(), "normal_z", format!("{:.4}", circle.normal.z)), + edit(t!("Normal X").as_ref(), "normal_x", circle.normal.x), + edit(t!("Normal Y").as_ref(), "normal_y", circle.normal.y), + edit(t!("Normal Z").as_ref(), "normal_z", circle.normal.z), ], }] } @@ -202,6 +202,28 @@ fn apply_geom_prop(circle: &mut Circle, field: &str, value: &str) { "diameter" if v > 0.0 => circle.radius = v / 2.0, "circumference" if v > 0.0 => circle.radius = v / (2.0 * PI), "area" if v > 0.0 => circle.radius = (v / PI).sqrt(), + "normal_x" | "normal_y" | "normal_z" => { + let center = circle.center_wcs(); + let mut normal = cadkernel::space::Vec3::new( + circle.normal.x, + circle.normal.y, + circle.normal.z, + ); + match field { + "normal_x" => normal.x = v, + "normal_y" => normal.y = v, + "normal_z" => normal.z = v, + _ => {} + } + if let Some(normal) = normal.normalize() { + circle.normal = acadrust::types::Vector3::new(normal.x, normal.y, normal.z); + let (x, y, z) = crate::scene::view::transform::wcs_point_to_ocs( + (center.x, center.y, center.z), + (normal.x, normal.y, normal.z), + ); + circle.center = acadrust::types::Vector3::new(x, y, z); + } + } _ => {} } } @@ -358,6 +380,19 @@ impl crate::entities::traits::MassPropsCalc for Circle { mod tests { use super::*; + #[test] + fn editing_normal_preserves_world_center() { + let mut circle = Circle::from_coords(2.0, 3.0, 4.0, 5.0); + let center = circle.center_wcs(); + + apply_geom_prop(&mut circle, "normal_x", "1"); + + assert!((circle.center_wcs() - center).length() < 1.0e-9); + let normal_length = + (circle.normal.x.powi(2) + circle.normal.y.powi(2) + circle.normal.z.powi(2)).sqrt(); + assert!((normal_length - 1.0).abs() < 1.0e-12); + } + #[test] fn quadrant_grips_offer_interactive_radius() { use crate::entities::traits::Grippable; diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index 3f18b4bbb..6cf37e0a5 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -1668,16 +1668,38 @@ pub(crate) fn resolved_dimension_style( } } - // Negative DIMLFAC applies only in paper space; preserve its sign on export. + // Negative DIMLFAC applies only in paper space, and there only to a + // dimension that measures model space through a viewport. The associative + // record stores the applied factor; zero marks paper-space geometry whose + // dimension reads its sheet distance. if style.dimlfac < 0.0 { - style.dimlfac = crate::scene::viewport_ref::MeasurementScale::user_lfac_for_space( - style.dimlfac, - dimension_in_paper_space(dimension, document), - ); + style.dimlfac = match calculated_dimlfac(dimension) { + Some(calculated) if calculated.abs() < 1e-12 => 1.0, + Some(calculated) => calculated.abs(), + None => crate::scene::viewport_ref::MeasurementScale::user_lfac_for_space( + style.dimlfac, + dimension_in_paper_space(dimension, document), + ), + }; } style } +/// The linear factor recorded when the dimension was last regenerated. +fn calculated_dimlfac(dimension: &Dimension) -> Option { + dimension + .base() + .common + .extended_data + .get_record("ACAD_DIMASSOC_CALC_DIMLFAC") + .and_then(|record| { + record.values.iter().find_map(|value| match value { + acadrust::xdata::XDataValue::Real(factor) if factor.is_finite() => Some(*factor), + _ => None, + }) + }) +} + /// Whether the dimension's owner is paper space, even without a Layout object. pub(crate) fn dimension_in_paper_space(dimension: &Dimension, document: &CadDocument) -> bool { let owner = dimension.base().common.owner_handle; @@ -3747,6 +3769,10 @@ fn tessellate_dimension_inner( horizontal_text: text_layout.horizontal, text_movement: style.map(|s| s.dimtmove).unwrap_or(0), text_break: text_layout.break_box, + leader_anchor: style + .filter(|s| s.dimtmove == 1) + .and_then(|_| stored_text_point(dim)) + .map(vec3_local), }, SuppressFlags { ext1: dimse1, @@ -3799,7 +3825,7 @@ fn tessellate_dimension_inner( &mut geom.dim_lines, point, vec3_local(dim.base().normal), - dim_txt as f32 * 0.6, + dim_txt as f32 * CELL_WIDTH as f32, jog_angle, ); } @@ -4411,11 +4437,11 @@ fn text_fill_rect( } let pos = dimension_text_pos_f64(dim, style, text_height, dim_scale); let dimgap = style.map(|s| s.dimgap.abs()).unwrap_or(0.0) * dim_scale; - // ~0.6 × text_height per character; matches average glyph aspect for - // the bundled stick fonts. Inflate by 1 DIMGAP on each side. + // CELL_WIDTH × text_height per character approximates the average glyph + // aspect of the bundled stick fonts. Inflate by 1 DIMGAP on each side. let stack_scale = style.map(dimtfac_or_one).unwrap_or(1.0); - let approx_w = text_cells(&value, stack_scale) * text_height * 0.6 + dimgap * 2.0; - let approx_h = dimension_text_block_height(style, text_height) + dimgap * 2.0; + let approx_w = text_cells(&value, stack_scale) * text_height * CELL_WIDTH + dimgap * 2.0; + let approx_h = dimension_text_extent_height(dim, style, text_height) + dimgap * 2.0; let rot = if dim.base().text_rotation.abs() > 1e-9 { dim.base().text_rotation } else { @@ -4458,7 +4484,7 @@ fn arc_length_symbol_points( let rotation = dimension_text_rotation(dim, style); let (sin_rotation, cos_rotation) = rotation.sin_cos(); let stack_scale = style.map(dimtfac_or_one).unwrap_or(1.0); - let text_width = text_cells(&value, stack_scale) * text_height * 0.6; + let text_width = text_cells(&value, stack_scale) * text_height * CELL_WIDTH; let symbol_width = text_height * 0.62; let symbol_height = text_height * 0.20; let (center_x, center_y) = if symbol_position == 1 { @@ -4524,7 +4550,7 @@ fn dimension_text_layout( .map(|style| (style.dimgap.abs() * dim_scale) as f32) .unwrap_or(0.09); let width = dimension_text_cells(dimension, style) - .map(|cells| cells as f32 * text_height as f32 * 0.6 + gap * 2.0) + .map(|cells| cells as f32 * text_height as f32 * CELL_WIDTH as f32 + gap * 2.0) .unwrap_or(0.0); let position = vec3_local(dimension_text_pos_f64( dimension, @@ -4536,7 +4562,9 @@ fn dimension_text_layout( let break_box = (bare_width > 0.0).then_some(TextBreak { center: position, half_width: bare_width * 0.5, - half_height: text_height as f32 * 0.5, + // A limits stack is two lines tall; the break and the arrow fit both + // have to clear the whole block. + half_height: dimension_text_extent_height(dimension, style, text_height) as f32 * 0.5, padding: gap, rotation: dimension_text_rotation(dimension, style), }); @@ -4572,6 +4600,9 @@ struct DimLineParams { horizontal_text: bool, text_movement: i16, text_break: Option, + /// DIMTMOVE 1: the point the file stores is where the leader's hook starts, + /// so the leader is drawn to it exactly and only the text is estimated. + leader_anchor: Option, } fn dimension_geometry( dim: &Dimension, @@ -4644,43 +4675,22 @@ fn dimension_geometry( add_segment(&mut g.dim_lines, a, b); add_segment(&mut g.dim_lines, b, point); } - } else if !suppress.dim2 { - add_segment(&mut g.dim_lines, center, point); } let radius = (point - center).length(); let text_is_outside = text.distance(center) > radius + 1e-5; + // Text inside the arc: the dimension line runs from the arc point + // to the centre. Text outside: only a leader from the arc point to + // the text, unless DIMTOFL asks for the inside line as well. + if !jogged && !suppress.dim2 && (!text_is_outside || params.dimtofl) { + add_segment(&mut g.dim_lines, center, point); + } if text_is_outside && !suppress.dim2 { - let radial = normalized_or(point - center, Vec3::X); - let angle_from_horizontal = radial.y.abs().atan2(radial.x.abs()); - if params.horizontal_text - && angle_from_horizontal > 15.0_f32.to_radians() - && radial.y.abs() > 1e-6 - { - let travel = (text.y - point.y) / radial.y; - if travel > 0.0 { - let elbow = point + radial * travel; - let landing_gap = params.text_width * 0.5 + params.arrow_len; - let landing = Vec3::new( - text.x - radial.x.signum() * landing_gap, - text.y, - text.z, - ); - add_segment(&mut g.dim_lines, point, elbow); - add_segment(&mut g.dim_lines, elbow, landing); - } else { - add_segment(&mut g.dim_lines, point, text); - } - } else { - add_segment(&mut g.dim_lines, point, text); - } + append_radial_leader(&mut g, point, text, ¶ms); } if !suppress.dim2 { - append_arrow( - &mut g, - point, - normalized_or(center - point, Vec3::X), - arrow1, - ); + // The arrowhead sits on the arc with its body toward the text. + let body = if text_is_outside { point - center } else { center - point }; + append_arrow(&mut g, point, normalized_or(body, Vec3::X), arrow1); } if text_is_outside { append_center_mark(&mut g, center, params.dimcen, radius); @@ -5026,17 +5036,42 @@ fn append_linear_dimension( let d2_out = d2 + dir_d1_to_d2 * dle; // When text plus arrows do not fit, DIMATFIT decides which component moves - // first: 0=both, 1=arrows, 2=text, 3=best fit. + // first: 0=both, 1=arrows, 2=text, 3=best fit. The text is measured along + // the dimension line where it is drawn: horizontal text on a vertical + // dimension takes up its height, and text dragged toward one end runs into + // that arrow long before the sum of widths says the gap is full. let gap = (d2 - d1).length(); + let text_span = params.text_break.map(|text_break| { + let axis_angle = dir_d1_to_d2.y.atan2(dir_d1_to_d2.x) as f64; + let (sin, cos) = (text_break.rotation - axis_angle).sin_cos(); + let half = (text_break.half_width as f64 * cos).abs() + + (text_break.half_height as f64 * sin).abs() + + text_break.padding as f64; + let along = (text_break.center - d1).dot(dir_d1_to_d2) as f64; + (along - half, along + half) + }); + let text_extent = text_span + .map(|(lo, hi)| (hi - lo) as f32) + .unwrap_or(params.text_width); + // Horizontal text beside a non-horizontal dimension is reached by a leg + // and a hook instead of the dimension line itself. + let hooks_out_horizontally = + params.horizontal_text && !params.ticks && dir_d1_to_d2.y.abs() > 1e-3; + let text_hits_an_arrow = text_span.is_some_and(|(lo, hi)| { + let center = (lo + hi) * 0.5; + center > 0.0 + && center < gap as f64 + && (lo < params.arrow_len as f64 || hi > (gap - params.arrow_len) as f64) + }); let arrows_outside = if params.ticks || params.arrow_len <= 1e-6 { false - } else if gap < 2.0 * params.arrow_len { + } else if gap < 2.0 * params.arrow_len || text_hits_an_arrow { true - } else if gap < params.text_width + 2.0 * params.arrow_len { + } else if gap < text_extent + 2.0 * params.arrow_len { match params.dimatfit { 0 | 1 => true, 2 => false, - _ => params.text_width <= gap, + _ => text_extent <= gap, } } else { false @@ -5057,13 +5092,22 @@ fn append_linear_dimension( if draw_inside_line && !suppress.dim2 && line_len - split > 1e-6 { add_segment_with_text_break(&mut g.dim_lines, split_point, d2_out, params.text_break); } - if arrows_outside && !params.dimsoxd { - let stub = params.arrow_len * 2.0; - if !suppress.dim1 { - add_segment(&mut g.dim_lines, d1 - dir_d1_to_d2 * stub, d1); + // Outside the extension lines the dimension line runs two arrow lengths, + // or on to the text when the text sits further out along the line. + let reach = |default: f32, toward_text: Option| toward_text.map_or(default, |t| t.max(default)); + let stub = params.arrow_len * 2.0; + let text_beyond_d1 = text_span + .filter(|(_, hi)| *hi < 0.0 && !hooks_out_horizontally) + .map(|(_, hi)| -hi as f32); + let text_beyond_d2 = text_span + .filter(|(lo, _)| *lo > gap as f64 && !hooks_out_horizontally) + .map(|(lo, _)| (lo - gap as f64) as f32); + if (arrows_outside && !params.dimsoxd) || text_beyond_d1.is_some() || text_beyond_d2.is_some() { + if !suppress.dim1 && (arrows_outside || text_beyond_d1.is_some()) { + add_segment(&mut g.dim_lines, d1 - dir_d1_to_d2 * reach(stub, text_beyond_d1), d1); } - if !suppress.dim2 { - add_segment(&mut g.dim_lines, d2, d2 + dir_d1_to_d2 * stub); + if !suppress.dim2 && (arrows_outside || text_beyond_d2.is_some()) { + add_segment(&mut g.dim_lines, d2, d2 + dir_d1_to_d2 * reach(stub, text_beyond_d2)); } } @@ -5075,6 +5119,61 @@ fn append_linear_dimension( append_arrow(g, d1, normalized_or(d2 - d1, axis), arrow1); append_arrow(g, d2, normalized_or(d1 - d2, -axis), arrow2); } + + // Horizontal text outside the extension lines of a dimension that is not + // horizontal itself: the dimension line runs on to the text's height and + // turns a hook one arrow long toward the text, which sits against it. + if params.horizontal_text && !params.ticks && dir_d1_to_d2.y.abs() > 1e-3 { + if let Some(text_break) = params.text_break { + let along = (text_break.center - d1).dot(dir_d1_to_d2); + let outside = if along < 0.0 { + Some((d1, -dir_d1_to_d2, suppress.dim1)) + } else if along > gap { + Some((d2, dir_d1_to_d2, suppress.dim2)) + } else { + None + }; + if let Some((end, outward, suppressed)) = outside { + let travel = (text_break.center.y - end.y) / outward.y; + if travel > 0.0 && !suppressed { + let elbow = end + outward * travel; + let side = if text_break.center.x >= elbow.x { 1.0 } else { -1.0 }; + let hook_end = Vec3::new(elbow.x + side * params.arrow_len, elbow.y, elbow.z); + add_segment(&mut g.dim_lines, end, elbow); + add_segment(&mut g.dim_lines, elbow, hook_end); + } + } + } + } +} + +/// Leader from a point on the circle to text outside it. Horizontal text gets a +/// hook one arrow long that the text sits against; aligned text is reached by a +/// straight leader that stops a gap short of it. `text_width` already carries +/// a gap on each side. +fn append_radial_leader(g: &mut DimGeom, tip: Vec3, text: Vec3, params: &DimLineParams) { + if params.horizontal_text { + let side = if text.x >= tip.x { 1.0 } else { -1.0 }; + let (hook_start, text_edge) = match params.leader_anchor { + Some(anchor) => (anchor, anchor + Vec3::X * (side * params.arrow_len)), + None => { + let edge = Vec3::new(text.x - side * params.text_width * 0.5, text.y, text.z); + (edge - Vec3::X * (side * params.arrow_len), edge) + } + }; + let slope = (hook_start - tip).y.abs().atan2((hook_start - tip).x.abs()); + // A leader already within 15° of horizontal runs straight to the text. + if slope > 15.0_f32.to_radians() { + add_segment(&mut g.dim_lines, tip, hook_start); + add_segment(&mut g.dim_lines, hook_start, text_edge); + } else { + add_segment(&mut g.dim_lines, tip, text_edge); + } + } else { + let toward = normalized_or(text - tip, Vec3::X); + let reach = ((text - tip).length() - params.text_width * 0.5).max(0.0); + add_segment(&mut g.dim_lines, tip, tip + toward * reach); + } } fn append_diameter_dimension( @@ -5092,6 +5191,28 @@ fn append_diameter_dimension( if diameter <= 1e-6 { return; } + let center = (chord + far_chord) * 0.5; + let text_is_outside = params.text_position.distance(center) > diameter * 0.5 + 1e-5; + if text_is_outside && !params.dimtofl { + // Outside text without DIMTOFL: no line across the circle, just a + // leader from the near side with the arrowhead on the circle and its + // body toward the text. DIMTMOVE 2 keeps the arrowhead and drops the + // leader. + let (tip, suppressed) = if params.text_position.distance_squared(chord) + <= params.text_position.distance_squared(far_chord) + { + (chord, suppress.dim1) + } else { + (far_chord, suppress.dim2) + }; + if !suppressed { + if params.text_movement != 2 { + append_radial_leader(g, tip, params.text_position, ¶ms); + } + append_arrow(g, tip, normalized_or(tip - center, axis), arrow1); + } + return; + } // Text projected outside the diameter requires inward-pointing arrowheads. let text_along = (params.text_position - chord).dot(axis); let text_outside = text_along < 0.0 || text_along > diameter; @@ -6048,8 +6169,8 @@ fn dimension_text_is_outside(dim: &Dimension, style: Option<&DimStyle>) -> bool return false; }; if let Some((vertex, start, end, radius)) = angular_dimension_frame(dim) { - if dim.base().text_user_positioned { - let text = vec3_local(dim.base().text_middle_point); + if let Some(point) = stored_text_point(dim) { + let text = vec3_local(point); let angle = (text.y - vertex.y).atan2(text.x - vertex.x); return (angle - start).rem_euclid(std::f32::consts::TAU) > end - start + 1.0e-6; @@ -6061,7 +6182,7 @@ fn dimension_text_is_outside(dim: &Dimension, style: Option<&DimStyle>) -> bool let height = style.dimtxt * scale; let gap = style.dimgap.abs() * scale; let text_width = dimension_text_cells(dim, Some(style)) - .map(|cells| cells * height * 0.6 + gap * 2.0) + .map(|cells| cells * height * CELL_WIDTH + gap * 2.0) .unwrap_or(0.0); let arrow = style.dimasz * scale; let span = radius as f64 * (end - start).abs() as f64; @@ -6075,11 +6196,8 @@ fn dimension_text_is_outside(dim: &Dimension, style: Option<&DimStyle>) -> bool } if let Dimension::LargeRadial(radial) = dim { let radius = radial.definition_point.distance(&radial.chord_point); - if dim.base().text_user_positioned { - return radial - .definition_point - .distance(&dim.base().text_middle_point) - > radius + 1.0e-9; + if let Some(point) = stored_text_point(dim) { + return radial.definition_point.distance(&point) > radius + 1.0e-9; } if style.dimtix { return false; @@ -6093,7 +6211,7 @@ fn dimension_text_is_outside(dim: &Dimension, style: Option<&DimStyle>) -> bool let height = style.dimtxt * scale; let gap = style.dimgap.abs() * scale; let text_width = dimension_text_cells(dim, Some(style)) - .map(|cells| cells * height * 0.6 + gap * 2.0) + .map(|cells| cells * height * CELL_WIDTH + gap * 2.0) .unwrap_or(0.0); let arrow = style.dimasz * scale; let insufficient = text_width + arrow > available; @@ -6108,8 +6226,7 @@ fn dimension_text_is_outside(dim: &Dimension, style: Option<&DimStyle>) -> bool let dx = radius.definition_point.x - radius.angle_vertex.x; let dy = radius.definition_point.y - radius.angle_vertex.y; let available = dx.hypot(dy); - if dim.base().text_user_positioned { - let text = dim.base().text_middle_point; + if let Some(text) = stored_text_point(dim) { return (text.x - radius.angle_vertex.x) .hypot(text.y - radius.angle_vertex.y) > available + 1e-9; @@ -6121,7 +6238,7 @@ fn dimension_text_is_outside(dim: &Dimension, style: Option<&DimStyle>) -> bool let height = style.dimtxt * scale; let gap = style.dimgap.abs() * scale; let text_width = dimension_text_cells(dim, Some(style)) - .map(|cells| cells * height * 0.6 + gap * 2.0) + .map(|cells| cells * height * CELL_WIDTH + gap * 2.0) .unwrap_or(0.0); let arrow = style.dimasz * scale; let insufficient = text_width + arrow > available; @@ -6154,8 +6271,7 @@ fn dimension_text_is_outside(dim: &Dimension, style: Option<&DimStyle>) -> bool let second_axis = second.x * axis.x + second.y * axis.y; let lo = first_axis.min(second_axis); let hi = first_axis.max(second_axis); - if dim.base().text_user_positioned { - let point = dim.base().text_middle_point; + if let Some(point) = stored_text_point(dim) { let position = point.x * axis.x + point.y * axis.y; return position < lo || position > hi; } @@ -6166,7 +6282,7 @@ fn dimension_text_is_outside(dim: &Dimension, style: Option<&DimStyle>) -> bool let height = style.dimtxt * scale; let gap = style.dimgap.abs() * scale; let text_width = dimension_text_cells(dim, Some(style)) - .map(|cells| cells * height * 0.6 + gap * 2.0) + .map(|cells| cells * height * CELL_WIDTH + gap * 2.0) .unwrap_or(0.0); let arrow = style.dimasz * scale; let span = hi - lo; @@ -6250,7 +6366,9 @@ fn dimension_text_parts( }; let primary_raw = match dim { Dimension::Radius(_) | Dimension::LargeRadial(_) => format!("R{}", value), - Dimension::Diameter(_) => format!("Ø{}", value), + // U+2205 is the native diameter sign and what `%%c` resolves to; the + // bundled dimension fonts carry it where Latin Ø is missing. + Dimension::Diameter(_) => format!("∅{}", value), _ => value, }; @@ -6528,13 +6646,13 @@ fn dimension_tolerance_entity( }; // Approximate widths from glyph counts (~0.6 × cell size per char). - let primary_w = primary_value_len as f64 * primary_height * 0.6; + let primary_w = primary_value_len as f64 * primary_height * CELL_WIDTH; let tol_visible_chars = tol .strip_prefix("\\S") .and_then(|value| value.strip_suffix(';')) .map(|value| value.split('^').map(str::len).max().unwrap_or(0)) .unwrap_or_else(|| tol.chars().count()); - let tol_w = tol_visible_chars as f64 * tol_height * 0.6; + let tol_w = tol_visible_chars as f64 * tol_height * CELL_WIDTH; let gap = primary_height * 0.2; let dx_local = primary_w * 0.5 + tol_w * 0.5 + gap; let dy_local = match s.dimtolj { @@ -6595,6 +6713,12 @@ fn effective_dimlfac(style: Option<&DimStyle>) -> f64 { } } +/// Average advance of one character cell as a fraction of the text height. +/// Measured on the bundled stroke fonts by rendering the text of 106 saved +/// dimension pictures: median 0.66, where the old guess of 0.60 left every +/// fit and hook estimate ten percent narrow. +const CELL_WIDTH: f64 = 0.66; + /// DIMTFAC with the unset value 0 read as 1. fn dimtfac_or_one(s: &DimStyle) -> f64 { if s.dimtfac.abs() < 1e-12 { @@ -6604,15 +6728,28 @@ fn dimtfac_or_one(s: &DimStyle) -> f64 { } } +/// Vertical extent of a dimension's whole value: the first line (or limits +/// stack) plus any `\P` continuation lines at MText's default spacing. +fn dimension_text_extent_height(dim: &Dimension, style: Option<&DimStyle>, text_height: f64) -> f64 { + let lines = dimension_text_value(dim, style) + .map(|value| text_lines(&value)) + .unwrap_or(1); + dimension_text_block_height(style, text_height) + (lines - 1) as f64 * text_height * 5.0 / 3.0 +} + /// Vertical extent of the rendered value: one line, or a DIMLIM stack whose /// halves each sit at DIMTFAC × DIMTXT. fn dimension_text_block_height(style: Option<&DimStyle>, text_height: f64) -> f64 { match style { Some(s) if s.dimlim => { + // From the lower half's baseline to the upper half's cap, as the + // MText stack lays them out. text_height * dimtfac_or_one(s) - * 2.0 - * f64::from(crate::entities::text_support::STACK_HALF_SCALE) + * f64::from( + crate::entities::text_support::STACK_RAISE + + crate::entities::text_support::STACK_HALF_SCALE, + ) } _ => text_height, } @@ -6625,22 +6762,55 @@ pub(crate) fn dimension_text_cells(dim: &Dimension, style: Option<&DimStyle>) -> Some(text_cells(&value, style.map(dimtfac_or_one).unwrap_or(1.0))) } +/// Width in cells of the widest line; `\P` starts a new line. fn text_cells(value: &str, stack_scale: f64) -> f64 { + value + .split(r"\P") + .map(|line| line_cells(line, stack_scale)) + .fold(0.0, f64::max) +} + +/// Number of lines the value renders as. +fn text_lines(value: &str) -> usize { + value.split(r"\P").count().max(1) +} + +/// Visible cells of one line: MText format codes (`\\f...;`, `\\H...;`, `\\C...;`, +/// braces, underline toggles) take no room, a stack is as wide as its wider half. +fn line_cells(value: &str, stack_scale: f64) -> f64 { let mut cells = 0.0; - let mut rest = value; - while let Some(start) = rest.find("\\S") { - cells += rest[..start].chars().count() as f64; - let body = &rest[start + 2..]; - let end = body.find(';').unwrap_or(body.len()); - let widest = body[..end] - .split(['^', '/', '#']) - .map(|half| half.chars().count()) - .max() - .unwrap_or(0); - cells += widest as f64 * stack_scale; - rest = &body[(end + 1).min(body.len())..]; + let mut chars = value.chars().peekable(); + while let Some(c) = chars.next() { + match c { + '{' | '}' => {} + '\\' => match chars.next() { + Some('S') => { + let body: String = chars.by_ref().take_while(|c| *c != ';').collect(); + let widest = body + .split(['^', '/', '#']) + .map(|half| half.chars().count()) + .max() + .unwrap_or(0); + cells += widest as f64 * stack_scale; + } + Some('~') => cells += 1.0, + // Underline, overline and strike toggles take no room; an + // escaped backslash or brace is one visible character. + Some('L' | 'l' | 'O' | 'o' | 'K' | 'k') => {} + Some('\\' | '{' | '}') => cells += 1.0, + Some(_) => { + for c in chars.by_ref() { + if c == ';' { + break; + } + } + } + None => {} + }, + _ => cells += 1.0, + } } - cells + rest.chars().count() as f64 + cells } /// Format a linear measurement honouring DIMLFAC, DIMRND, DIMDEC, DIMZIN, DIMDSEP, DIMLUNIT. @@ -7018,6 +7188,7 @@ fn text_on_dim_line( dimtix: bool, dimatfit: i16, tad: i16, + horizontal_outside: bool, ) -> Vector3 { // The perpendicular "up" side: the perpendicular of the dimension line // normalised so the text reads left-to-right (or bottom-to-top when @@ -7070,7 +7241,24 @@ fn text_on_dim_line( _ => text_w > span, }; if text_outside { - along = hi + arrow + text_w * 0.5; + // Past the two-arrow stub the dimension line grows outside the + // extension line, then the gap that `text_w` already carries. + along = hi + 2.0 * arrow + text_w * 0.5; + if horizontal_outside && ay.abs() > 1e-3 { + // Horizontal text beside a hook: the line runs out two arrow + // lengths, the hook turns one arrow toward the text-up side + // and the text sits against it, centred on the hook. + let elbow_along = hi + 2.0 * arrow; + let side = if px >= 0.0 { 1.0 } else { -1.0 }; + let elbow_x = defpt.x + ax * elbow_along; + let elbow_y = defpt.y + ay * elbow_along; + let vertical = if tad == 0 { 0.0 } else { perp_off * perp_sign }; + return Vector3::new( + elbow_x + side * (arrow + text_w * 0.5), + elbow_y + vertical, + defpt.z, + ); + } } } let bx = defpt.x + ax * along; @@ -7124,7 +7312,7 @@ fn text_on_single_arrow_dim_line( _ => text_width > available, }; let along = if text_outside { - high + arrow + text_width * 0.5 + high + 2.0 * arrow + text_width * 0.5 } else { (first_along + second_along) * 0.5 }; @@ -7135,6 +7323,30 @@ fn text_on_single_arrow_dim_line( ) } +/// The stored text middle point (group 11) applies to automatic and hand-placed +/// text alike. A regenerated dimension keeps any non-zero point; only a cleared +/// point is placed from the style. +fn stored_text_point(dim: &Dimension) -> Option { + let p = dim.base().text_middle_point; + (p.x * p.x + p.y * p.y + p.z * p.z > 1e-16).then_some(p) +} + +/// The point on the circle a radial leader leaves from: the arc point of a +/// radius, or whichever end of a diameter's chord is nearer the text. +fn radial_leader_tip(dim: &Dimension, text: Vector3) -> Vector3 { + match dim { + Dimension::Radius(d) => d.definition_point, + Dimension::Diameter(d) => { + if d.angle_vertex.distance(&text) <= d.definition_point.distance(&text) { + d.angle_vertex + } else { + d.definition_point + } + } + _ => dim.base().text_middle_point, + } +} + fn dimension_text_pos_f64( dim: &Dimension, style: Option<&DimStyle>, @@ -7160,52 +7372,54 @@ fn dimension_text_pos_f64( let perp_off = if dimtad == 0 { dimtvp * text_height } else { - dimension_text_block_height(style, text_height) * 0.5 + dimgap + dimension_text_extent_height(dim, style, text_height) * 0.5 + dimgap }; // Rough text width + arrow allowance, used to decide text-outside fit. let text_w = dimension_text_cells(dim, style) - .map(|cells| cells * text_height * 0.6 + 2.0 * dimgap) + .map(|cells| cells * text_height * CELL_WIDTH + 2.0 * dimgap) .unwrap_or(0.0); - let arrow = if matches!(dim, Dimension::LargeRadial(_)) { - style - .map(|style| style.dimasz * dim_scale) - .unwrap_or(text_height) - } else { - text_height - }; + let arrow = style + .map(|style| style.dimasz * dim_scale) + .filter(|arrow| *arrow > 1e-9) + .unwrap_or(text_height); + // DIMTOH: text that lands outside reads horizontally. + let horizontal_outside = style.is_some_and(|style| style.dimtoh); - // Explicit per-entity override (text dragged to a custom location): the - // saved point wins. Otherwise the dimension style governs placement. - let use_saved = base.text_user_positioned && { - let p = base.text_middle_point; - p.x * p.x + p.y * p.y + p.z * p.z > 1e-16 - }; - if use_saved { - if let Dimension::Diameter(d) = dim { - let dx = d.definition_point.x - d.angle_vertex.x; - let dy = d.definition_point.y - d.angle_vertex.y; - let len = (dx * dx + dy * dy).sqrt().max(1e-12); - - // Match the readable orientation used by text_on_dim_line(). - let (mut nx, mut ny) = (dx / len, dy / len); - if nx < 0.0 || (nx == 0.0 && ny < 0.0) { - nx = -nx; - ny = -ny; + if let Some(point) = stored_text_point(dim) { + return match dim { + // DIMTMOVE 1 stores where the leader's hook starts: one arrow of + // hook, a gap, then the text, all running away from the arc. + // Measured on 39 such dimensions the stored point sits 2.4 text + // heights before the text, which is DIMASZ plus DIMGAP. + Dimension::Radius(_) | Dimension::Diameter(_) + if style.is_some_and(|s| s.dimtmove == 1) => + { + let tip = radial_leader_tip(dim, point); + let side = if point.x >= tip.x { 1.0 } else { -1.0 }; + Vector3::new(point.x + side * (arrow + text_w * 0.5), point.y, point.z) } - - let px = -ny; - let py = nx; - - let perp_sign = if dimtad == 4 { -1.0 } else { 1.0 }; - - return Vector3::new( - base.text_middle_point.x + px * perp_off * perp_sign, - base.text_middle_point.y + py * perp_off * perp_sign, - base.text_middle_point.z, - ); - } - - return base.text_middle_point; + // A diameter whose text the user dragged keeps the grip point on + // the leader and lifts the text by the DIMTAD offset (#1323). + Dimension::Diameter(d) if base.text_user_positioned && dimtad != 0 => { + let dx = d.definition_point.x - d.angle_vertex.x; + let dy = d.definition_point.y - d.angle_vertex.y; + let len = (dx * dx + dy * dy).sqrt().max(1e-12); + // Match the readable orientation used by text_on_dim_line(). + let (mut nx, mut ny) = (dx / len, dy / len); + if nx < 0.0 || (nx == 0.0 && ny < 0.0) { + nx = -nx; + ny = -ny; + } + let (px, py) = (-ny, nx); + let perp_sign = if dimtad == 4 { -1.0 } else { 1.0 }; + Vector3::new( + point.x + px * perp_off * perp_sign, + point.y + py * perp_off * perp_sign, + point.z, + ) + } + _ => point, + }; } match dim { @@ -7225,6 +7439,7 @@ fn dimension_text_pos_f64( dimtix, dimatfit, dimtad, + horizontal_outside, ) } Dimension::Aligned(d) => { @@ -7244,6 +7459,7 @@ fn dimension_text_pos_f64( dimtix, dimatfit, dimtad, + horizontal_outside, ) } Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_) => { @@ -7333,6 +7549,7 @@ fn dimension_text_pos_f64( dimtix, dimatfit, dimtad, + horizontal_outside, ) } Dimension::Ordinate(d) => { @@ -7433,6 +7650,7 @@ pub(crate) fn baked_large_radial_geometry( horizontal_text: text.horizontal, text_movement: style.map(|style| style.dimtmove).unwrap_or(0), text_break: text.break_box, + leader_anchor: None, }, SuppressFlags { ext1: style.is_some_and(|style| style.dimse1), @@ -7553,13 +7771,9 @@ pub(crate) fn dimension_text_grip_position( document: &CadDocument, anno_scale: f64, ) -> Option { - // Once the user has moved the text, its saved point is authoritative. - let base = dim.base(); - if base.text_user_positioned { - let p = base.text_middle_point; - if p.x * p.x + p.y * p.y + p.z * p.z > 1e-16 { - return Some(p); - } + // A stored text point is where the text is drawn; the grip goes there. + if let Some(p) = stored_text_point(dim) { + return Some(p); } // For automatic text, use the same text-building path used by the @@ -7587,11 +7801,11 @@ mod dimtad_tests { fn above_is_up_regardless_of_direction() { let (first, second, defpt) = (v(0.0, 10.0), v(20.0, 10.0), v(0.0, 0.0)); let perp = 5.0; - let fwd = text_on_dim_line(first, second, defpt, 1.0, 0.0, 0, perp, 0.0, 1.0, false, 3, 1); - let rev = text_on_dim_line(first, second, defpt, -1.0, 0.0, 0, perp, 0.0, 1.0, false, 3, 1); + let fwd = text_on_dim_line(first, second, defpt, 1.0, 0.0, 0, perp, 0.0, 1.0, false, 3, 1, false); + let rev = text_on_dim_line(first, second, defpt, -1.0, 0.0, 0, perp, 0.0, 1.0, false, 3, 1, false); assert!(fwd.y > 0.0, "above must be +Y, got {}", fwd.y); assert!(rev.y > 0.0, "above must be +Y even reversed, got {}", rev.y); - let below = text_on_dim_line(first, second, defpt, 1.0, 0.0, 0, perp, 0.0, 1.0, false, 3, 4); + let below = text_on_dim_line(first, second, defpt, 1.0, 0.0, 0, perp, 0.0, 1.0, false, 3, 4, false); assert!(below.y < 0.0, "below must be -Y, got {}", below.y); } @@ -7614,7 +7828,8 @@ mod dimtad_tests { false, 3, 2, - ); + false, + ); assert!( above.y > 10.0, "outside must clear the object side, got {}", @@ -7634,7 +7849,8 @@ mod dimtad_tests { false, 3, 2, - ); + false, + ); assert!( below.y < -10.0, "outside must clear the object side, got {}", @@ -8262,6 +8478,14 @@ mod limits_format_tests { assert_eq!(text_cells("10.000", 0.5), 6.0); assert_eq!(text_cells(r"R\S1.020^0.98;", 0.5), 1.0 + 5.0 * 0.5); assert_eq!(text_cells(r"\S10.020^9.950; REF", 1.0), 6.0 + 4.0); + assert_eq!(text_cells(r"\S1.020^0.980;\PCutouts", 1.0), 7.0, "widest line, not the sum"); + assert_eq!(text_lines(r"4X <>\PNominal"), 2); + assert_eq!( + text_cells(r"{\fArial|b0|i1|c0|p34;Cutouts}", 1.0), + 7.0, + "format codes and braces take no room" + ); + assert_eq!(text_cells(r"\H0.7x;10\L Ref\l", 1.0), 6.0); } // A three-point angular dimension whose first extension point coincides @@ -8326,7 +8550,259 @@ mod limits_format_tests { assert_eq!(dimension_text_block_height(Some(&s), 2.0), 2.0); s.dimlim = true; s.dimtfac = 0.5; - let expected = 2.0 * 0.5 * 2.0 * f64::from(crate::entities::text_support::STACK_HALF_SCALE); + let expected = 2.0 + * 0.5 + * f64::from( + crate::entities::text_support::STACK_RAISE + + crate::entities::text_support::STACK_HALF_SCALE, + ); assert_eq!(dimension_text_block_height(Some(&s), 2.0), expected); } } + +#[cfg(test)] +mod layout_parity_tests { + use super::*; + use acadrust::entities::{DimensionDiameter, DimensionLinear, DimensionRadius}; + use glam::DVec3; + + /// A document whose Standard style has imperial sizes and horizontal text. + fn document() -> CadDocument { + let mut document = CadDocument::new(); + let style = document + .dim_styles + .get_mut("Standard") + .expect("Standard style"); + style.dimasz = 0.18; + style.dimtxt = 0.18; + style.dimgap = 0.09; + style.dimtih = true; + style.dimtoh = true; + style.dimtad = 0; + style.dimdec = 2; + document + } + + fn style(document: &CadDocument) -> DimStyle { + document.dim_styles.get("Standard").expect("Standard style").clone() + } + + /// Stroke endpoints and arrowhead triangle count of the drawn dimension. + fn drawn(document: &CadDocument, dim: &Dimension) -> (Vec, usize) { + let wires = dim.tessellate( + document, + Handle::new(0x40), + false, + [1.0; 4], + 1.0, + 1.0, + &rustc_hash::FxHashSet::default(), + None, + [0.0, 0.0, 0.0, 1.0], + None, + None, + ); + let mut points = Vec::new(); + let mut fills = 0; + for wire in &wires { + for p in wire.points.iter().filter(|p| p[0].is_finite()) { + points.push(DVec3::new(p[0] as f64, p[1] as f64, p[2] as f64)); + } + fills += wire.fill_tris.len() / 3; + } + (points, fills) + } + + fn vertical(top: f64) -> DimensionLinear { + let mut d = DimensionLinear::default(); + d.first_point = Vector3::new(0.0, 0.0, 0.0); + d.second_point = Vector3::new(0.0, top, 0.0); + d.definition_point = Vector3::new(-0.5, top, 0.0); + d.rotation = std::f64::consts::FRAC_PI_2; + d + } + + // Group 11 is authoritative whenever present, whether or not the user moved + // the text. + #[test] + fn stored_text_point_places_automatic_text() { + let document = document(); + let mut d = DimensionLinear::horizontal(Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0)); + d.definition_point = Vector3::new(0.0, 5.0, 0.0); + d.base.text_middle_point = Vector3::new(7.0, 5.2, 0.0); + d.base.text_user_positioned = false; + let dim = Dimension::Linear(d); + let position = dimension_text_pos_f64(&dim, Some(&style(&document)), 0.18, 1.0); + assert_eq!((position.x, position.y), (7.0, 5.2)); + } + + // Text pushed outside by a narrow gap sits past the two-arrow stub and a + // gap, not directly against the arrowhead. + #[test] + fn outside_text_clears_the_stub_and_a_gap() { + let document = document(); + let mut d = DimensionLinear::horizontal(Vector3::new(0.0, 0.0, 0.0), Vector3::new(0.2, 0.0, 0.0)); + d.definition_point = Vector3::new(0.0, 1.0, 0.0); + let dim = Dimension::Linear(d); + let s = style(&document); + let position = dimension_text_pos_f64(&dim, Some(&s), 0.18, 1.0); + let half_width = dimension_text_cells(&dim, Some(&s)).unwrap() * 0.18 * CELL_WIDTH * 0.5; + let text_start = position.x - half_width; + let stub_end = 0.2 + 2.0 * 0.18; + assert!( + (text_start - (stub_end + 0.09)).abs() < 1e-6, + "text starts at {text_start}, expected {}", + stub_end + 0.09 + ); + } + + // Horizontal text near the top of a vertical dimension runs into the top + // arrowhead, so both arrowheads flip outside and stubs grow past the + // extension lines. Text in the middle leaves them inside. + #[test] + fn text_that_hits_an_arrow_moves_the_arrows_outside() { + let document = document(); + let top = 1.12; + let mut near_top = vertical(top); + near_top.base.text_middle_point = Vector3::new(-0.5, 0.98, 0.0); + let (points, _) = drawn(&document, &Dimension::Linear(near_top)); + assert!( + points.iter().any(|p| p.y > top + 0.1) && points.iter().any(|p| p.y < -0.1), + "arrows should be outside with stubs past both extension lines" + ); + + let mut centred = vertical(top); + centred.base.text_middle_point = Vector3::new(-0.5, top * 0.5, 0.0); + let (points, _) = drawn(&document, &Dimension::Linear(centred)); + assert!( + points.iter().all(|p| p.y <= top + 0.01 && p.y >= -0.01), + "centred text leaves the arrows inside" + ); + } + + // A negative DIMLFAC scales only a paper-space dimension that measures + // model space through a viewport. The association record carries the + // applied factor; zero means the dimension reads its sheet distance. + #[test] + fn negative_dimlfac_follows_the_recorded_association_factor() { + use acadrust::xdata::{ExtendedDataRecord, XDataValue}; + let document = CadDocument::new(); + let mut source = style(&document); + source.dimlfac = -4.0; + let mut d = DimensionLinear::horizontal(Vector3::new(0.0, 0.0, 0.0), Vector3::new(4.457, 0.0, 0.0)); + d.definition_point = Vector3::new(0.0, 1.0, 0.0); + let with_factor = |factor: f64| { + let mut d = d.clone(); + let mut record = ExtendedDataRecord::new("ACAD_DIMASSOC_CALC_DIMLFAC"); + record.add_value(XDataValue::Real(factor)); + d.base.common.extended_data.add_record(record); + Dimension::Linear(d) + }; + let sheet = resolved_dimension_style(&source, &with_factor(0.0), &document); + assert_eq!(sheet.dimlfac, 1.0, "no factor applied: sheet distance"); + let through_viewport = resolved_dimension_style(&source, &with_factor(-4.0), &document); + assert_eq!(through_viewport.dimlfac, 4.0, "the recorded factor, made positive"); + let unknown = resolved_dimension_style(&source, &Dimension::Linear(d.clone()), &document); + assert_eq!(unknown.dimlfac, 1.0, "model space without a record: unscaled"); + let invalid = resolved_dimension_style(&source, &with_factor(f64::NAN), &document); + assert_eq!(invalid.dimlfac, 1.0, "invalid recorded factor: unscaled"); + } + + // DIMTMOVE 1: the stored point is where the hook starts. The hook runs + // one arrow toward the text, and the text begins a gap beyond it. + #[test] + fn dimtmove_one_anchors_the_hook_start_at_the_stored_point() { + let mut document = document(); + document.dim_styles.get_mut("Standard").unwrap().dimtmove = 1; + let mut d = DimensionRadius::default(); + d.angle_vertex = Vector3::new(0.0, 0.0, 0.0); + d.definition_point = Vector3::new(1.0, 0.0, 0.0); + d.base.text_middle_point = Vector3::new(3.0, 1.5, 0.0); + let dim = Dimension::Radius(d); + let (points, _) = drawn(&document, &dim); + let hook: Vec = points.iter().filter(|p| (p.y - 1.5).abs() < 1e-6).map(|p| p.x).collect(); + let (lo, hi) = (hook.iter().cloned().fold(f64::MAX, f64::min), hook.iter().cloned().fold(f64::MIN, f64::max)); + assert!((lo - 3.0).abs() < 1e-6 && (hi - 3.18).abs() < 1e-6, "hook from the stored point one arrow toward the text: {lo}..{hi}"); + let s = style(&document); + let position = dimension_text_pos_f64(&dim, Some(&s), 0.18, 1.0); + let half = dimension_text_cells(&dim, Some(&s)).unwrap() * 0.18 * CELL_WIDTH * 0.5; + assert!((position.x - half - 0.09 - 3.18).abs() < 1e-6, "text starts a gap past the hook end, got {}", position.x - half); + } + + // Text stored well past the second extension line of a horizontal + // dimension: the dimension line runs out to the text, not just two arrow + // lengths. + #[test] + fn outside_text_on_the_axis_pulls_the_line_out_to_it() { + let document = document(); + let mut d = DimensionLinear::horizontal(Vector3::new(0.0, 0.0, 0.0), Vector3::new(0.8, 0.0, 0.0)); + d.definition_point = Vector3::new(0.0, 2.0, 0.0); + d.base.text_middle_point = Vector3::new(2.5, 2.0, 0.0); + let (points, _) = drawn(&document, &Dimension::Linear(d)); + let furthest = points.iter().map(|p| p.x).fold(f64::MIN, f64::max); + assert!( + furthest > 2.0 && furthest < 2.5, + "line reaches the text's near edge, got {furthest}" + ); + } + + // Horizontal text stored above a vertical dimension: the dimension line + // runs up to the text's height and hooks one arrow toward it. + #[test] + fn horizontal_outside_text_gets_a_leg_and_a_hook() { + let document = document(); + let top = 0.3; + let mut d = vertical(top); + d.base.text_middle_point = Vector3::new(-0.5 + 0.9, 1.0, 0.0); + let (points, _) = drawn(&document, &Dimension::Linear(d)); + assert!( + points.iter().any(|p| (p.y - 1.0).abs() < 1e-6 && (p.x + 0.5).abs() < 1e-6), + "the dimension line reaches the text height at the extension line" + ); + assert!( + points.iter().any(|p| (p.y - 1.0).abs() < 1e-6 && (p.x - (-0.5 + 0.18)).abs() < 1e-6), + "a hook one arrow long turns toward the text" + ); + } + + // Radius text outside the arc: no line to the centre, a leader from the arc + // point to a hook the text sits against, arrowhead body pointing outward. + #[test] + fn radius_text_outside_draws_a_hooked_leader_only() { + let document = document(); + let mut d = DimensionRadius::default(); + d.angle_vertex = Vector3::new(0.0, 0.0, 0.0); + d.definition_point = Vector3::new(1.0, 0.0, 0.0); + d.base.text_middle_point = Vector3::new(3.0, 0.8, 0.0); + let (points, fills) = drawn(&document, &Dimension::Radius(d)); + assert!( + !points.iter().any(|p| p.length() < 1e-6), + "no dimension line reaches the centre" + ); + let hook: Vec<_> = points.iter().filter(|p| (p.y - 0.8).abs() < 1e-6).collect(); + assert!(hook.len() >= 2, "a horizontal hook at the text height: {hook:?}"); + assert!( + points.iter().any(|p| p.x > 1.05 && p.x < 1.3 && p.y > 0.0), + "arrowhead body lies outside the arc along the leader" + ); + assert_eq!(fills, 1); + } + + // Diameter text outside the circle without DIMTOFL: a single leader from + // the near side, one arrowhead, and no line across the circle. + #[test] + fn diameter_text_outside_draws_one_leader_and_no_chord() { + let document = document(); + let mut d = DimensionDiameter::default(); + d.angle_vertex = Vector3::new(-1.0, 0.0, 0.0); + d.definition_point = Vector3::new(1.0, 0.0, 0.0); + d.base.text_middle_point = Vector3::new(3.0, 0.8, 0.0); + let (points, fills) = drawn(&document, &Dimension::Diameter(d)); + assert!( + !points.iter().any(|p| p.x < -0.5), + "nothing is drawn on the far side of the circle" + ); + assert_eq!(fills, 1, "one arrowhead"); + assert!(points.iter().any(|p| (p.y - 0.8).abs() < 1e-6), "hook at the text height"); + } +} diff --git a/src/entities/ellipse.rs b/src/entities/ellipse.rs index 47ef9d58e..09560fd5c 100644 --- a/src/entities/ellipse.rs +++ b/src/entities/ellipse.rs @@ -187,9 +187,9 @@ fn properties(ell: &Ellipse) -> Vec { ro(t!("Start parameter").as_ref(), "start_param", format!("{:.4}", ell.start_parameter)), ro(t!("End parameter").as_ref(), "end_param", format!("{:.4}", ell.end_parameter)), ro(t!("Length").as_ref(), "length", format_length(props.perimeter)), - ro(t!("Normal X").as_ref(), "normal_x", format!("{:.4}", ell.normal.x)), - ro(t!("Normal Y").as_ref(), "normal_y", format!("{:.4}", ell.normal.y)), - ro(t!("Normal Z").as_ref(), "normal_z", format!("{:.4}", ell.normal.z)), + edit(t!("Normal X").as_ref(), "normal_x", ell.normal.x), + edit(t!("Normal Y").as_ref(), "normal_y", ell.normal.y), + edit(t!("Normal Z").as_ref(), "normal_z", ell.normal.z), ], }] } @@ -220,6 +220,26 @@ fn apply_geom_prop(ell: &mut Ellipse, field: &str, value: &str) { "ratio" if v > 0.0 => ell.minor_axis_ratio = v, "start_angle" => ell.start_parameter = v.to_radians(), "end_angle" => ell.end_parameter = v.to_radians(), + "normal_x" | "normal_y" | "normal_z" => { + let mut normal = [ell.normal.x, ell.normal.y, ell.normal.z]; + match field { + "normal_x" => normal[0] = v, + "normal_y" => normal[1] = v, + "normal_z" => normal[2] = v, + _ => {} + } + let Some(normal) = cadkernel::space::Vec3::from(normal).normalize() else { + return; + }; + let Some(major) = cadkernel::space::reorient_axis_to_plane( + [ell.major_axis.x, ell.major_axis.y, ell.major_axis.z], + normal.to_array(), + ) else { + return; + }; + ell.normal = acadrust::types::Vector3::new(normal.x, normal.y, normal.z); + ell.major_axis = acadrust::types::Vector3::new(major[0], major[1], major[2]); + } _ => {} } } @@ -424,6 +444,19 @@ mod grip_tests { (v.major_axis.x * v.major_axis.x + v.major_axis.y * v.major_axis.y).sqrt() } + #[test] + fn editing_normal_reorients_major_axis_in_kernel() { + let mut e = ell(10.0, 0.5); + + apply_geom_prop(&mut e, "normal_x", "1"); + + let normal = DVec3::new(e.normal.x, e.normal.y, e.normal.z); + let major = DVec3::new(e.major_axis.x, e.major_axis.y, e.major_axis.z); + assert!((normal.length() - 1.0).abs() < 1.0e-12); + assert!((major.length() - 10.0).abs() < 1.0e-9); + assert!(normal.dot(major).abs() < 1.0e-9); + } + #[test] fn minor_grip_below_major_updates_ratio_only() { let mut e = ell(10.0, 0.5); diff --git a/src/entities/spline.rs b/src/entities/spline.rs index 5a8813561..efbbc7e35 100644 --- a/src/entities/spline.rs +++ b/src/entities/spline.rs @@ -18,7 +18,13 @@ pub(crate) fn uses_fit_method(spline: &Spline) -> bool { } pub(crate) fn shows_fit_points(spline: &Spline) -> bool { - uses_fit_method(spline) && (!spline.cv_frame_visible || spline.flags.periodic) + !spline.fit_points.is_empty() && (!spline.cv_frame_visible || spline.flags.periodic) +} + +pub(crate) fn shows_control_vertices(spline: &Spline) -> bool { + !shows_fit_points(spline) + && !(uses_fit_method(spline) && spline.flags.periodic) + && control_vertices(spline).len() >= 2 } pub(crate) fn fit_nurbs3(spline: &Spline) -> Option { @@ -105,12 +111,21 @@ pub(crate) fn spline_knot_points(spline: &Spline) -> Vec { } pub(crate) fn replace_with_nurbs(spline: &mut Spline, curve: &NurbsCurve3) { + let mut fit_points = spline.fit_points.clone(); spline.degree = curve.degree() as i32; spline.knots = curve.knots().to_vec(); spline.control_points = curve.control_points().iter() .map(|point| acadrust::types::Vector3::new(point[0], point[1], point[2])).collect(); spline.weights = curve.weights().to_vec(); - spline.fit_points.clear(); + if !fit_points.is_empty() { + if let (Some(first), Some(point)) = (fit_points.first_mut(), spline.control_points.first()) { + *first = *point; + } + if let (Some(last), Some(point)) = (fit_points.last_mut(), spline.control_points.last()) { + *last = *point; + } + } + spline.fit_points = fit_points; spline.begin_tangent = acadrust::types::Vector3::ZERO; spline.end_tangent = acadrust::types::Vector3::ZERO; spline.flags.rational = spline.weights.windows(2) @@ -615,7 +630,15 @@ fn index_prop(label: &str, field: &'static str, index: usize, count: usize) -> P fn convert_to_control_method(spline: &mut Spline) -> bool { if !uses_fit_method(spline) { - return true; + let valid = spline.control_points.len() >= 2; + if valid { + spline.fit_points.clear(); + spline.begin_tangent = acadrust::types::Vector3::ZERO; + spline.end_tangent = acadrust::types::Vector3::ZERO; + spline.dwg_flags1 &= !1; + spline.dxf_flags &= !(32 | 1024); + } + return valid; } let Some(planar) = crate::entities::curve::spline_curve(spline) else { return false; @@ -647,18 +670,74 @@ fn convert_to_control_method(spline: &mut Spline) -> bool { } fn convert_to_fit_method(spline: &mut Spline) -> bool { - uses_fit_method(spline) + if uses_fit_method(spline) { + return true; + } + if spline.fit_points.len() < 2 { + return false; + } + spline.control_points.clear(); + spline.knots.clear(); + spline.weights.clear(); + spline.flags.rational = false; + spline.dwg_flags1 |= 1; + spline.dxf_flags |= 32 | 1024; + true +} + +fn derived_fit_points(spline: &Spline) -> Vec { + if spline.flags.closed || spline.flags.periodic { + return Vec::new(); + } + let Some(curve) = nurbs3(spline) else { + return Vec::new(); + }; + let (start, end) = curve.domain(); + if !(start.is_finite() && end.is_finite() && end > start) { + return Vec::new(); + } + let tolerance = (end - start).abs().max(1.0) * 1.0e-12; + let mut parameters = vec![start]; + for knot in curve.knots().iter().copied() { + if knot > start + tolerance + && knot < end - tolerance + && parameters + .last() + .is_none_or(|previous| (knot - *previous).abs() > tolerance) + { + parameters.push(knot); + } + } + parameters.push(end); + parameters + .into_iter() + .map(|parameter| { + let point = curve.point_at_knot(parameter); + acadrust::types::Vector3::new(point[0], point[1], point[2]) + }) + .collect() +} + +fn can_show_fit_points(spline: &Spline) -> bool { + spline.fit_points.len() >= 2 || derived_fit_points(spline).len() >= 2 +} + +fn prepare_fit_point_view(spline: &mut Spline) -> bool { + if spline.fit_points.len() < 2 { + spline.fit_points = derived_fit_points(spline); + } + spline.fit_points.len() >= 2 } fn grips(spline: &Spline) -> Vec { - let fit_method = uses_fit_method(spline); let derived_control = control_vertices(spline); - let show_control_vertices = (spline.cv_frame_visible || !fit_method) - && !(fit_method && spline.flags.periodic) + let show_fit = shows_fit_points(spline); + let show_control_vertices = !show_fit + && !(uses_fit_method(spline) && spline.flags.periodic) && derived_control.len() >= 2; let source = if show_control_vertices { derived_control - } else if fit_method { + } else if show_fit { spline.fit_points.clone() } else { spline.control_points.clone() @@ -885,7 +964,7 @@ fn apply_geom_prop(spline: &mut Spline, field: &str, value: &str) { match field { "spline_method" => { if value == "Fit" { - if convert_to_fit_method(spline) { + if prepare_fit_point_view(spline) { spline.cv_frame_visible = false; } } else if value == "Control Vertices" { @@ -896,6 +975,12 @@ fn apply_geom_prop(spline: &mut Spline, field: &str, value: &str) { return; } "knot_param" => { + if shows_fit_points(spline) + && !uses_fit_method(spline) + && !convert_to_fit_method(spline) + { + return; + } spline.knot_parameterization = match value { "Chord" => 0, "Square Root" => 1, @@ -930,9 +1015,30 @@ fn apply_geom_prop(spline: &mut Spline, field: &str, value: &str) { { return; } - if matches!(field, "ctrl_pt_x" | "ctrl_pt_y" | "ctrl_pt_z" | "weight") - && uses_fit_method(spline) - && !convert_to_control_method(spline) + if matches!(field, "ctrl_pt_x" | "ctrl_pt_y" | "ctrl_pt_z" | "weight") { + if !shows_fit_points(spline) && !spline.fit_points.is_empty() { + if !convert_to_control_method(spline) { + return; + } + } else if uses_fit_method(spline) && !convert_to_control_method(spline) { + return; + } + } + if shows_fit_points(spline) + && !uses_fit_method(spline) + && matches!( + field, + "fit_pt_x" + | "fit_pt_y" + | "fit_pt_z" + | "start_tan_x" + | "start_tan_y" + | "start_tan_z" + | "end_tan_x" + | "end_tan_y" + | "end_tan_z" + ) + && !convert_to_fit_method(spline) { return; } @@ -1015,16 +1121,23 @@ fn apply_grip(spline: &mut Spline, grip_id: usize, apply: GripApply) { return; } // First CV edit makes the displayed kernel controls authoritative. - let fit_method = uses_fit_method(spline); - if spline.cv_frame_visible && fit_method && !spline.flags.periodic { + let editing_fit = shows_fit_points(spline); + if editing_fit && !uses_fit_method(spline) { + if !convert_to_fit_method(spline) { + return; + } + } else if !editing_fit + && (uses_fit_method(spline) || !spline.fit_points.is_empty()) + && !spline.flags.periodic + { if !convert_to_control_method(spline) { return; } } - let target = if (spline.cv_frame_visible && !spline.flags.periodic) || !fit_method { - spline.control_points.get_mut(grip_id) - } else { + let target = if editing_fit { spline.fit_points.get_mut(grip_id) + } else { + spline.control_points.get_mut(grip_id) }; if let Some(cp) = target { match apply { @@ -1088,9 +1201,10 @@ impl crate::entities::traits::Grippable for Spline { let fit_method = uses_fit_method(self); let can_show_control = control_vertices(self).len() >= 2 && !(fit_method && self.flags.periodic); + let can_show_fit = can_show_fit_points(self); return if !shows_fit_points(self) { vec![ - (can_show_control && fit_method).then_some(GripMenuItem { + can_show_fit.then_some(GripMenuItem { label: "Fit", action: GripMenuAction::ShowFit, }), @@ -1131,7 +1245,7 @@ impl crate::entities::traits::Grippable for Spline { use crate::scene::model::object::GripMenuAction as A; match action { A::ShowFit => { - if convert_to_fit_method(self) { + if prepare_fit_point_view(self) { self.cv_frame_visible = false; return; } @@ -1276,4 +1390,30 @@ mod tests { acadrust::types::Vector3::new(0.0, 2.5, 0.0) ); } + + #[test] + fn showing_derived_fit_points_does_not_change_the_curve() { + let mut spline = Spline::default(); + spline.degree = 3; + spline.control_points = vec![ + acadrust::types::Vector3::new(0.0, 0.0, 0.0), + acadrust::types::Vector3::new(1.0, 3.0, 0.0), + acadrust::types::Vector3::new(4.0, 3.0, 0.0), + acadrust::types::Vector3::new(5.0, 0.0, 0.0), + ]; + spline.knots = cadkernel::space::clamped_uniform_knots(3, 4); + let before = nurbs3(&spline).unwrap().point_at(0.37); + + assert!(prepare_fit_point_view(&mut spline)); + spline.cv_frame_visible = false; + + assert!(shows_fit_points(&spline)); + assert!(!uses_fit_method(&spline)); + let after = nurbs3(&spline).unwrap().point_at(0.37); + assert!( + cadkernel::space::Vec3::from(before) + .distance(cadkernel::space::Vec3::from(after)) + < 1.0e-12 + ); + } } diff --git a/src/entities/text_support.rs b/src/entities/text_support.rs index f26f7d52a..9ffc247da 100644 --- a/src/entities/text_support.rs +++ b/src/entities/text_support.rs @@ -197,7 +197,7 @@ pub fn text_local_bounds( /// Expand DXF `%%x` special-character sequences that appear in both TEXT and MTEXT values: /// - `%%d` / `%%D` → `°` /// - `%%p` / `%%P` → `±` -/// - `%%c` / `%%C` → `⌀` +/// - `%%c` / `%%C` → `∅` (U+2205, the native diameter sign) /// - `%%u` / `%%U` → underline toggle (stripped — not renderable with stroke fonts) /// - `%%o` / `%%O` → overline toggle (stripped) /// - `%%%%` → `%` @@ -224,7 +224,7 @@ pub fn resolve_dxf_special_chars(s: &str) -> String { } Some('c') => { chars.next(); - out.push('⌀'); + out.push('∅'); } Some('u') | Some('o') => { chars.next(); diff --git a/src/modules/draw/select.rs b/src/modules/draw/select.rs index d695ce6d7..94f78b3bc 100644 --- a/src/modules/draw/select.rs +++ b/src/modules/draw/select.rs @@ -68,6 +68,21 @@ impl SelectObjectsCommand { } } + /// Standard selection behavior routed to a private apply command while + /// retaining the public command name in the visible prompt. + pub fn routed(prompt_cmd: &str, pending_cmd: &str) -> Self { + Self { + prompt_cmd: prompt_cmd.to_string(), + pending_cmd: pending_cmd.to_string(), + handles: Vec::new(), + commit_on_enter: true, + show_options: true, + pick: None, + pick_crossing: true, + auto_constrain_settings: false, + } + } + /// Single-object variant: the first completed selection action applies /// immediately, with no Enter (used by commands that act on one object). pub fn instant(pending_cmd: &str) -> Self { diff --git a/src/modules/parametric/concentric.rs b/src/modules/parametric/concentric.rs new file mode 100644 index 000000000..448c3227c --- /dev/null +++ b/src/modules/parametric/concentric.rs @@ -0,0 +1,185 @@ +use acadrust::{EntityType, Handle}; +use glam::DVec3; + +use crate::command::{CadCommand, CmdResult}; +use crate::scene::parametric_constraints::ParametricRef; +use crate::t; + +pub struct ConcentricConstraintCommand { + first: Option, + picked_entity: Option, +} + +impl ConcentricConstraintCommand { + pub fn new() -> Self { + Self { + first: None, + picked_entity: None, + } + } + + pub fn with_first(first: ParametricRef) -> Self { + Self { + first: Some(first), + picked_entity: None, + } + } + + fn invalid_selection() -> CmdResult { + CmdResult::ReportError( + t!("Invalid selection for Concentric. Select a circle, arc, ellipse or polyline arc segment.") + .into_owned(), + ) + } + + fn polyline_segment_is_arc(entity: &EntityType, index: usize) -> bool { + match entity { + EntityType::LwPolyline(polyline) => polyline + .vertices + .get(index) + .is_some_and(|vertex| vertex.bulge.abs() > 1.0e-12), + EntityType::Polyline2D(polyline) => polyline + .vertices + .get(index) + .is_some_and(|vertex| vertex.bulge.abs() > 1.0e-12), + _ => false, + } + } + + fn picked_reference(entity: &EntityType, handle: Handle, point: DVec3) -> Option { + match entity { + EntityType::Circle(_) | EntityType::Arc(_) | EntityType::Ellipse(_) => { + Some(ParametricRef::center(handle)) + } + EntityType::LwPolyline(_) | EntityType::Polyline2D(_) => { + let planar = crate::entities::curve::entity_curve(entity)?; + let local = planar.plane.project(point.to_array())?; + let segments = planar.curve.segments(); + let (index, _) = cadkernel::geom2d::nearest_of(segments.iter(), local)?; + matches!(segments.get(index), Some(cadkernel::geom2d::Curve::Arc(_))) + .then(|| ParametricRef::segment_center(handle, index)) + } + _ => None, + } + } + + pub fn preselected_reference(entity: &EntityType, handle: Handle) -> Option { + match entity { + EntityType::Circle(_) | EntityType::Arc(_) | EntityType::Ellipse(_) => { + Some(ParametricRef::center(handle)) + } + EntityType::LwPolyline(polyline) => { + let segment_count = if polyline.is_closed { + polyline.vertices.len() + } else { + polyline.vertices.len().saturating_sub(1) + }; + (segment_count == 1 && Self::polyline_segment_is_arc(entity, 0)) + .then(|| ParametricRef::segment_center(handle, 0)) + } + EntityType::Polyline2D(polyline) => { + let segment_count = if polyline.is_closed() { + polyline.vertices.len() + } else { + polyline.vertices.len().saturating_sub(1) + }; + (segment_count == 1 && Self::polyline_segment_is_arc(entity, 0)) + .then(|| ParametricRef::segment_center(handle, 0)) + } + _ => None, + } + } +} + +impl CadCommand for ConcentricConstraintCommand { + fn name(&self) -> &'static str { + "GCCONCENTRIC" + } + + fn prompt(&self) -> String { + if self.first.is_some() { + t!("GCCONCENTRIC Select second object:").into_owned() + } else { + t!("GCCONCENTRIC Select first object:").into_owned() + } + } + + fn needs_entity_pick(&self) -> bool { + true + } + + fn entity_pick_highlights_hover(&self) -> bool { + true + } + + fn inject_before_entity_pick(&self) -> bool { + true + } + + fn inject_picked_entity(&mut self, entity: EntityType) { + self.picked_entity = Some(entity); + } + + fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult { + if handle.is_null() { + return CmdResult::NeedPoint; + } + let Some(entity) = self.picked_entity.take() else { + return CmdResult::NeedPoint; + }; + let Some(reference) = Self::picked_reference(&entity, handle, point) else { + return Self::invalid_selection(); + }; + let Some(first) = self.first else { + self.first = Some(reference); + return CmdResult::NeedPoint; + }; + if first == reference { + return Self::invalid_selection(); + } + CmdResult::AddConcentricConstraint { + first, + second: reference, + label: "Concentric constraint", + } + } + + fn on_point(&mut self, _point: DVec3) -> CmdResult { + CmdResult::NeedPoint + } + + fn on_enter(&mut self) -> CmdResult { + CmdResult::Cancel + } + + fn on_escape(&mut self) -> CmdResult { + CmdResult::Cancel + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn polyline_pick_keeps_the_selected_arc_center() { + let handle = Handle::new(13); + let mut polyline = acadrust::entities::LwPolyline::new(); + polyline.vertices = vec![ + acadrust::entities::LwVertex::from_coords(0.0, 0.0), + acadrust::entities::LwVertex::with_bulge( + acadrust::types::Vector2::new(5.0, 0.0), + 1.0, + ), + acadrust::entities::LwVertex::from_coords(10.0, 0.0), + ]; + + let reference = ConcentricConstraintCommand::picked_reference( + &EntityType::LwPolyline(polyline), + handle, + DVec3::new(8.0, 2.0, 0.0), + ) + .expect("arc segment"); + assert_eq!(reference, ParametricRef::segment_center(handle, 1)); + } +} diff --git a/src/modules/parametric/constraint_bar.rs b/src/modules/parametric/constraint_bar.rs new file mode 100644 index 000000000..76ae1d0a3 --- /dev/null +++ b/src/modules/parametric/constraint_bar.rs @@ -0,0 +1,102 @@ +use acadrust::Handle; +use glam::DVec3; + +use crate::command::{CadCommand, CmdOption, CmdResult}; + +pub struct ConstraintBarOptionCommand { + handles: Vec, +} + +impl ConstraintBarOptionCommand { + pub fn new(handles: Vec) -> Self { + Self { handles } + } + + fn finish(&mut self, command: &str) -> CmdResult { + CmdResult::Relaunch(command.to_string(), std::mem::take(&mut self.handles)) + } +} + +impl CadCommand for ConstraintBarOptionCommand { + fn name(&self) -> &'static str { + "CONSTRAINTBAR" + } + + fn prompt(&self) -> String { + "CONSTRAINTBAR Enter an option [Show/Hide/Reset] :".to_string() + } + + fn options(&self) -> Vec { + vec![ + CmdOption::new("Show", "S"), + CmdOption::new("Hide", "H"), + CmdOption::new("Reset", "R"), + ] + } + + fn wants_text_input(&self) -> bool { + true + } + + fn on_text_input(&mut self, text: &str) -> Option { + let keyword = text.trim().trim_start_matches('_').to_ascii_uppercase(); + match keyword.as_str() { + "S" | "SHOW" => Some(self.finish("GCSHOW")), + "H" | "HIDE" => Some(self.finish("GCHIDE")), + "R" | "RESET" => Some(self.finish("CONSTRAINTBAR_RESET")), + _ => None, + } + } + + fn on_point(&mut self, _point: DVec3) -> CmdResult { + CmdResult::NeedPoint + } + + fn on_enter(&mut self) -> CmdResult { + self.finish("GCSHOW") + } + + fn on_escape(&mut self) -> CmdResult { + CmdResult::Cancel + } +} + +inventory::submit!(crate::command::CommandRegistration { + names: &["CONSTRAINTBAR"] +}); + +#[cfg(test)] +mod tests { + use super::*; + + fn relaunch(result: CmdResult) -> (String, Vec) { + let CmdResult::Relaunch(command, handles) = result else { + panic!("expected relaunch"); + }; + (command, handles) + } + + #[test] + fn enter_defaults_to_show_and_keeps_the_selection() { + let handles = vec![Handle::new(2), Handle::new(5)]; + let mut command = ConstraintBarOptionCommand::new(handles.clone()); + + assert_eq!(relaunch(command.on_enter()), ("GCSHOW".to_string(), handles)); + } + + #[test] + fn hide_and_reset_route_to_their_apply_commands() { + let handle = Handle::new(7); + let mut hide = ConstraintBarOptionCommand::new(vec![handle]); + assert_eq!( + relaunch(hide.on_text_input("hide").unwrap()), + ("GCHIDE".to_string(), vec![handle]) + ); + + let mut reset = ConstraintBarOptionCommand::new(vec![handle]); + assert_eq!( + relaunch(reset.on_text_input("r").unwrap()), + ("CONSTRAINTBAR_RESET".to_string(), vec![handle]) + ); + } +} diff --git a/src/modules/parametric/geom_constraint.rs b/src/modules/parametric/geom_constraint.rs index 199912c87..88a8dc499 100644 --- a/src/modules/parametric/geom_constraint.rs +++ b/src/modules/parametric/geom_constraint.rs @@ -14,14 +14,14 @@ impl GeomConstraintCommand { fn dispatch(keyword: &str) -> Option<&'static str> { Some(match keyword { - "H" | "HORIZONTAL" => "HCONSTRAINT", + "H" | "HORIZONTAL" => "GCHORIZONTAL", "V" | "VERTICAL" => "VCONSTRAINT", "P" | "PERPENDICULAR" => "GCPERPENDICULAR", "PA" | "PARALLEL" => "PCONSTRAINT", "T" | "TANGENT" => "TCONSTRAINT", - "SM" | "SMOOTH" => "SMOOTHCONSTRAINT", + "SM" | "SMOOTH" => "GCSMOOTH", "C" | "COINCIDENT" => "GCCOINCIDENT", - "CON" | "CONCENTRIC" => "NCONSTRAINT", + "CON" | "CONCENTRIC" => "GCCONCENTRIC", "COL" | "COLLINEAR" | "COLINEAR" => "LCONSTRAINT", "SY" | "SYMMETRIC" => "SYCONSTRAINT", "E" | "EQUAL" => "ECONSTRAINT", diff --git a/src/modules/parametric/horizontal.rs b/src/modules/parametric/horizontal.rs new file mode 100644 index 000000000..2b1b9f4ba --- /dev/null +++ b/src/modules/parametric/horizontal.rs @@ -0,0 +1,343 @@ +use acadrust::{EntityType, Handle}; +use glam::DVec3; + +use crate::command::{ + CadCommand, CmdOption, CmdResult, CoincidentPick, HorizontalConstraintSelection, WorkingPlane, +}; +use crate::scene::parametric_constraints::{ + directional_axis_endpoints, is_parametric_point_near, ParametricRef, +}; + +#[derive(Clone, Copy)] +enum Step { + ObjectOrTwoPoints, + FirstPoint, + SecondPoint(CoincidentPick), +} + +/// Interactive front end for the Horizontal geometric constraint. +pub struct HorizontalConstraintCommand { + step: Step, + picked_entity: Option, + plane: WorkingPlane, +} + +impl HorizontalConstraintCommand { + pub fn new() -> Self { + Self { + step: Step::ObjectOrTwoPoints, + picked_entity: None, + plane: WorkingPlane::default(), + } + } + + fn point_pick(handle: Option, point: DVec3) -> CoincidentPick { + CoincidentPick { + handle, + point, + whole_curve: false, + } + } + + fn direction(&self) -> acadrust::types::Vector3 { + let direction = self.plane.x.normalize_or(DVec3::X); + acadrust::types::Vector3::new(direction.x, direction.y, direction.z) + } + + fn invalid_object() -> CmdResult { + CmdResult::ReportError( + "Horizontal: select a line, straight polyline segment, text, MText, or an ellipse axis." + .to_string(), + ) + } + + fn invalid_point() -> CmdResult { + CmdResult::ReportError( + "Horizontal: select an endpoint, center, midpoint, or polyline vertex.".to_string(), + ) + } + + fn segment_is_straight(entity: &EntityType, index: usize) -> bool { + match entity { + EntityType::LwPolyline(polyline) => polyline + .vertices + .get(index) + .is_some_and(|vertex| vertex.bulge.abs() <= 1.0e-12), + EntityType::Polyline2D(polyline) => polyline + .vertices + .get(index) + .is_some_and(|vertex| vertex.bulge.abs() <= 1.0e-12), + _ => false, + } + } + + fn distance_to_axis(point: DVec3, endpoints: [acadrust::types::Vector3; 2]) -> f64 { + cadkernel::space::Vec3::from(point.to_array()) + .distance_to_line( + cadkernel::space::Vec3::new(endpoints[0].x, endpoints[0].y, endpoints[0].z), + cadkernel::space::Vec3::new(endpoints[1].x, endpoints[1].y, endpoints[1].z), + ) + .unwrap_or(f64::INFINITY) + } + + fn ellipse_reference( + entity: &EntityType, + handle: Handle, + point: DVec3, + ) -> Option { + let major = ParametricRef::ellipse_major_axis(handle); + let minor = ParametricRef::ellipse_minor_axis(handle); + let major_distance = + Self::distance_to_axis(point, directional_axis_endpoints(entity, major)?); + let minor_distance = + Self::distance_to_axis(point, directional_axis_endpoints(entity, minor)?); + Some(if minor_distance < major_distance { + minor + } else { + major + }) + } + + fn picked_reference( + entity: &EntityType, + handle: Handle, + point: DVec3, + ) -> Option { + match entity { + EntityType::Line(_) | EntityType::Ray(_) | EntityType::XLine(_) => { + Some(ParametricRef::whole(handle)) + } + EntityType::LwPolyline(_) | EntityType::Polyline2D(_) => { + let (source, _, _) = + crate::scene::centerline::picked_source(entity, handle, point)?; + let index = usize::try_from(source.segment_index).ok()?; + Self::segment_is_straight(entity, index) + .then_some(ParametricRef::segment(handle, index)) + } + EntityType::Text(_) | EntityType::MText(_) => { + Some(ParametricRef::text_baseline(handle)) + } + EntityType::Ellipse(_) => Self::ellipse_reference(entity, handle, point), + _ => None, + } + } + + pub fn preselected_reference(entity: &EntityType, handle: Handle) -> Option { + match entity { + EntityType::Line(_) | EntityType::Ray(_) | EntityType::XLine(_) => { + Some(ParametricRef::whole(handle)) + } + EntityType::Text(_) | EntityType::MText(_) => { + Some(ParametricRef::text_baseline(handle)) + } + EntityType::Ellipse(_) => Some(ParametricRef::ellipse_major_axis(handle)), + EntityType::LwPolyline(polyline) => { + let count = if polyline.is_closed { + polyline.vertices.len() + } else { + polyline.vertices.len().saturating_sub(1) + }; + (count == 1 && Self::segment_is_straight(entity, 0)) + .then_some(ParametricRef::segment(handle, 0)) + } + EntityType::Polyline2D(polyline) => { + let count = if polyline.is_closed() { + polyline.vertices.len() + } else { + polyline.vertices.len().saturating_sub(1) + }; + (count == 1 && Self::segment_is_straight(entity, 0)) + .then_some(ParametricRef::segment(handle, 0)) + } + _ => None, + } + } + + fn finish_points(&self, first: CoincidentPick, second: CoincidentPick) -> CmdResult { + CmdResult::AddHorizontalConstraint { + selection: HorizontalConstraintSelection::Points(first, second), + direction: self.direction(), + label: "Horizontal constraint", + } + } +} + +impl CadCommand for HorizontalConstraintCommand { + fn name(&self) -> &'static str { + "GCHORIZONTAL" + } + + fn prompt(&self) -> String { + match self.step { + Step::ObjectOrTwoPoints => { + "GCHORIZONTAL Select an object or [2Points] <2Points>:".to_string() + } + Step::FirstPoint => "GCHORIZONTAL Select first point:".to_string(), + Step::SecondPoint(_) => "GCHORIZONTAL Select second point:".to_string(), + } + } + + fn options(&self) -> Vec { + matches!(self.step, Step::ObjectOrTwoPoints) + .then(|| vec![CmdOption::new("2Points", "2P")]) + .unwrap_or_default() + } + + fn wants_text_input(&self) -> bool { + true + } + + fn point_step_accepts_keywords(&self) -> bool { + matches!(self.step, Step::ObjectOrTwoPoints) + } + + fn on_text_input(&mut self, text: &str) -> Option { + let keyword = text.trim().trim_start_matches('_').to_ascii_uppercase(); + if matches!(self.step, Step::ObjectOrTwoPoints) + && matches!(keyword.as_str(), "2" | "2P" | "2POINT" | "2POINTS") + { + self.step = Step::FirstPoint; + Some(CmdResult::NeedPoint) + } else { + None + } + } + + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + + fn needs_entity_pick(&self) -> bool { + true + } + + fn entity_pick_accepts_points(&self) -> bool { + true + } + + fn entity_pick_highlights_hover(&self) -> bool { + true + } + + fn inject_before_entity_pick(&self) -> bool { + true + } + + fn inject_picked_entity(&mut self, entity: EntityType) { + self.picked_entity = Some(entity); + } + + fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult { + if handle.is_null() { + return self.on_point(point); + } + let Some(entity) = self.picked_entity.take() else { + return CmdResult::NeedPoint; + }; + match self.step { + Step::ObjectOrTwoPoints => { + let Some(reference) = Self::picked_reference(&entity, handle, point) else { + return Self::invalid_object(); + }; + CmdResult::AddHorizontalConstraint { + selection: HorizontalConstraintSelection::Reference(reference), + direction: self.direction(), + label: "Horizontal constraint", + } + } + Step::FirstPoint | Step::SecondPoint(_) => { + let world = acadrust::types::Vector3::new(point.x, point.y, point.z); + if !is_parametric_point_near(&entity, world) { + return Self::invalid_point(); + } + let pick = Self::point_pick(Some(handle), point); + match self.step { + Step::FirstPoint => { + self.step = Step::SecondPoint(pick); + CmdResult::NeedPoint + } + Step::SecondPoint(first) => self.finish_points(first, pick), + Step::ObjectOrTwoPoints => unreachable!(), + } + } + } + } + + fn on_point(&mut self, point: DVec3) -> CmdResult { + let pick = Self::point_pick(None, point); + match self.step { + Step::ObjectOrTwoPoints | Step::FirstPoint => { + self.step = Step::SecondPoint(pick); + CmdResult::NeedPoint + } + Step::SecondPoint(first) => self.finish_points(first, pick), + } + } + + fn on_enter(&mut self) -> CmdResult { + if matches!(self.step, Step::ObjectOrTwoPoints) { + self.step = Step::FirstPoint; + CmdResult::NeedPoint + } else { + CmdResult::Cancel + } + } + + fn on_escape(&mut self) -> CmdResult { + CmdResult::Cancel + } +} + +#[cfg(test)] +mod tests { + use super::*; + use acadrust::entities::Line; + use acadrust::types::Vector3; + + #[test] + fn object_pick_captures_the_working_x_direction() { + let handle = Handle::new(7); + let mut command = HorizontalConstraintCommand::new(); + command.set_working_plane(WorkingPlane::new(DVec3::ZERO, DVec3::Y, DVec3::NEG_X)); + command.inject_picked_entity(EntityType::Line(Line::from_points( + Vector3::ZERO, + Vector3::new(2.0, 3.0, 0.0), + ))); + + let CmdResult::AddHorizontalConstraint { + selection: HorizontalConstraintSelection::Reference(reference), + direction, + .. + } = command.on_entity_pick(handle, DVec3::ZERO) + else { + panic!("object pick must create the constraint"); + }; + assert_eq!(reference, ParametricRef::whole(handle)); + assert!((direction - Vector3::UNIT_Y).length() < 1.0e-12); + } + + #[test] + fn enter_starts_the_two_point_flow() { + let mut command = HorizontalConstraintCommand::new(); + assert!(matches!(command.on_enter(), CmdResult::NeedPoint)); + assert!(matches!( + command.on_point(DVec3::new(1.0, 2.0, 0.0)), + CmdResult::NeedPoint + )); + let CmdResult::AddHorizontalConstraint { + selection: HorizontalConstraintSelection::Points(first, second), + direction, + .. + } = command.on_point(DVec3::new(4.0, 5.0, 0.0)) + else { + panic!("second point must create the constraint"); + }; + assert_eq!(first.point, DVec3::new(1.0, 2.0, 0.0)); + assert_eq!(second.point, DVec3::new(4.0, 5.0, 0.0)); + assert!((direction - Vector3::UNIT_X).length() < 1.0e-12); + } +} + +inventory::submit!(crate::command::CommandRegistration { + names: &["GCHORIZONTAL"] +}); diff --git a/src/modules/parametric/mod.rs b/src/modules/parametric/mod.rs index 056c17686..f40701cef 100644 --- a/src/modules/parametric/mod.rs +++ b/src/modules/parametric/mod.rs @@ -3,26 +3,35 @@ //! resulting constraints through the geometry kernel. mod coincident; +mod concentric; +mod constraint_bar; mod equal_distance; mod geom_constraint; +#[path = "horizontal.rs"] +mod horizontal_command; #[path = "perpendicular.rs"] mod perpendicular_command; mod point_on_entity; +mod smooth; #[path = "tangent.rs"] mod tangent_command; mod tools; mod value; pub use coincident::{coincident_tool, CoincidentConstraintCommand}; +pub use concentric::ConcentricConstraintCommand; +pub use constraint_bar::ConstraintBarOptionCommand; pub use equal_distance::{equal_distance_tool, EqualDistanceConstraintCommand}; pub use geom_constraint::GeomConstraintCommand; +pub use horizontal_command::HorizontalConstraintCommand; pub use perpendicular_command::{PerpendicularConstraintCommand, PerpendicularPick}; pub use point_on_entity::{ center_point_tool, midpoint_tool, point_on_curve_tool, PointOnEntityConstraintCommand, }; +pub use smooth::SmoothConstraintCommand; pub use tangent_command::TangentConstraintCommand; pub use tools::{ - colinear, concentric, equal, fixed, horizontal, normal, parallel, perpendicular, symmetric, - tangent, vertical, + colinear, concentric as concentric_tool, equal, fixed, horizontal, normal, parallel, + perpendicular, symmetric, tangent, vertical, }; pub use value::{ angle_tool, dimensional_tools, distance_tool, AngleConstraintCommand, @@ -62,10 +71,10 @@ impl CadModule for ParametricModule { RibbonItem::LargeTool(colinear::tool()), RibbonItem::LargeTool(perpendicular::tool()), RibbonItem::LargeTool(command( - "SMOOTHCONSTRAINT", "Smooth", + "GCSMOOTH", "Smooth", include_bytes!("../../../assets/icons/constrain/smooth.svg"), )), - RibbonItem::LargeTool(concentric::tool()), + RibbonItem::LargeTool(concentric_tool::tool()), RibbonItem::LargeTool(horizontal::tool()), RibbonItem::LargeTool(symmetric::tool()), RibbonItem::LargeTool(fixed::tool()), @@ -136,7 +145,7 @@ impl CadModule for ParametricModule { inventory::submit!(crate::command::CommandRegistration { names: &[ - "AUTOCONSTRAIN", "CONSTRAINTSETTINGS", "SMOOTHCONSTRAINT", "GCSHOW", "GCHIDE", "GCRESET", + "AUTOCONSTRAIN", "CONSTRAINTSETTINGS", "GCSMOOTH", "GCSHOW", "GCHIDE", "GCRESET", "GCSHOWALL", "GCHIDEALL", "DCSHOW", "DCHIDE", "DCSHOWALL", "DCHIDEALL", "DCCONVERT", "DELCONSTRAINT", ] @@ -168,8 +177,8 @@ mod tests { groups[0].tools.iter().map(item_id).collect::>(), [ "AUTOCONSTRAIN", "CCONSTRAINT", "PCONSTRAINT", "TCONSTRAINT", - "LCONSTRAINT", "QCONSTRAINT", "SMOOTHCONSTRAINT", "NCONSTRAINT", - "HCONSTRAINT", "SYCONSTRAINT", "FXCONSTRAINT", "VCONSTRAINT", + "LCONSTRAINT", "QCONSTRAINT", "GCSMOOTH", "GCCONCENTRIC", + "GCHORIZONTAL", "SYCONSTRAINT", "FXCONSTRAINT", "VCONSTRAINT", "ECONSTRAINT", "GCVISIBILITY", "GCSHOWALL", "GCHIDEALL", ] ); diff --git a/src/modules/parametric/perpendicular.rs b/src/modules/parametric/perpendicular.rs index b69b5d7d2..19f4d99ba 100644 --- a/src/modules/parametric/perpendicular.rs +++ b/src/modules/parametric/perpendicular.rs @@ -33,24 +33,37 @@ impl PerpendicularConstraintCommand { } fn distance_to_axis(point: DVec3, endpoints: [acadrust::types::Vector3; 2]) -> f64 { - let start = DVec3::new(endpoints[0].x, endpoints[0].y, endpoints[0].z); - let end = DVec3::new(endpoints[1].x, endpoints[1].y, endpoints[1].z); - let direction = end - start; - if direction.length_squared() <= 1.0e-24 { - return f64::INFINITY; - } - (point - start).cross(direction.normalize()).length() + cadkernel::space::Vec3::from(point.to_array()) + .distance_to_line( + cadkernel::space::Vec3::new(endpoints[0].x, endpoints[0].y, endpoints[0].z), + cadkernel::space::Vec3::new(endpoints[1].x, endpoints[1].y, endpoints[1].z), + ) + .unwrap_or(f64::INFINITY) } - fn ellipse_reference(entity: &EntityType, handle: Handle, point: DVec3) -> Option { + fn ellipse_reference( + entity: &EntityType, + handle: Handle, + point: DVec3, + ) -> Option { let major = ParametricRef::ellipse_major_axis(handle); let minor = ParametricRef::ellipse_minor_axis(handle); - let major_distance = Self::distance_to_axis(point, directional_axis_endpoints(entity, major)?); - let minor_distance = Self::distance_to_axis(point, directional_axis_endpoints(entity, minor)?); - Some(if minor_distance < major_distance { minor } else { major }) + let major_distance = + Self::distance_to_axis(point, directional_axis_endpoints(entity, major)?); + let minor_distance = + Self::distance_to_axis(point, directional_axis_endpoints(entity, minor)?); + Some(if minor_distance < major_distance { + minor + } else { + major + }) } - fn picked_reference(entity: &EntityType, handle: Handle, point: DVec3) -> Option { + fn picked_reference( + entity: &EntityType, + handle: Handle, + point: DVec3, + ) -> Option { let (reference, fixed_reference, start_reference) = match entity { EntityType::Line(_) => ( ParametricRef::whole(handle), @@ -58,7 +71,8 @@ impl PerpendicularConstraintCommand { ParametricRef::point(handle, 0), ), EntityType::LwPolyline(_) | EntityType::Polyline2D(_) => { - let (source, _, _) = crate::scene::centerline::picked_source(entity, handle, point)?; + let (source, _, _) = + crate::scene::centerline::picked_source(entity, handle, point)?; let index = usize::try_from(source.segment_index).ok()?; ( ParametricRef::segment(handle, index), @@ -101,12 +115,13 @@ impl PerpendicularConstraintCommand { } else { polyline.vertices.len().saturating_sub(1) }; - (segment_count == 1 && polyline.vertices.first()?.bulge.abs() <= 1.0e-12) - .then_some(PerpendicularPick { - reference: ParametricRef::segment(handle, 0), - fixed_reference: ParametricRef::segment(handle, 0), - start_reference: ParametricRef::point(handle, 0), - }) + let index = (0..segment_count) + .find(|index| polyline.vertices[*index].bulge.abs() <= 1.0e-12)?; + Some(PerpendicularPick { + reference: ParametricRef::segment(handle, index), + fixed_reference: ParametricRef::segment(handle, index), + start_reference: ParametricRef::point(handle, index as i32), + }) } EntityType::Polyline2D(polyline) => { let segment_count = if polyline.is_closed() { @@ -114,12 +129,13 @@ impl PerpendicularConstraintCommand { } else { polyline.vertices.len().saturating_sub(1) }; - (segment_count == 1 && polyline.vertices.first()?.bulge.abs() <= 1.0e-12) - .then_some(PerpendicularPick { - reference: ParametricRef::segment(handle, 0), - fixed_reference: ParametricRef::segment(handle, 0), - start_reference: ParametricRef::point(handle, 0), - }) + let index = (0..segment_count) + .find(|index| polyline.vertices[*index].bulge.abs() <= 1.0e-12)?; + Some(PerpendicularPick { + reference: ParametricRef::segment(handle, index), + fixed_reference: ParametricRef::segment(handle, index), + start_reference: ParametricRef::point(handle, index as i32), + }) } _ => None, } @@ -232,4 +248,23 @@ mod tests { assert_eq!(first_fixed, ParametricRef::whole(first)); assert_eq!(second_start, ParametricRef::point(second, 0)); } + + #[test] + fn preselection_uses_the_first_straight_polyline_segment() { + let handle = Handle::new(11); + let mut polyline = acadrust::entities::LwPolyline::new(); + polyline.vertices = vec![ + acadrust::entities::LwVertex::with_bulge(acadrust::types::Vector2::new(0.0, 0.0), 1.0), + acadrust::entities::LwVertex::from_coords(4.0, 0.0), + acadrust::entities::LwVertex::from_coords(8.0, 0.0), + ]; + + let pick = PerpendicularConstraintCommand::preselected_reference( + &EntityType::LwPolyline(polyline), + handle, + ) + .expect("straight segment"); + assert_eq!(pick.reference, ParametricRef::segment(handle, 1)); + assert_eq!(pick.start_reference, ParametricRef::point(handle, 1)); + } } diff --git a/src/modules/parametric/smooth.rs b/src/modules/parametric/smooth.rs new file mode 100644 index 000000000..e5a9c0dd3 --- /dev/null +++ b/src/modules/parametric/smooth.rs @@ -0,0 +1,261 @@ +use acadrust::{EntityType, Handle}; +use glam::DVec3; + +use crate::command::{CadCommand, CmdResult}; +use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef}; +use crate::t; + +#[derive(Clone, Copy)] +struct SmoothTarget { + endpoint: ParametricRef, + curve: ParametricRef, +} + +pub struct SmoothConstraintCommand { + source: Option, + picked_entity: Option, +} + +impl SmoothConstraintCommand { + pub fn new() -> Self { + Self { + source: None, + picked_entity: None, + } + } + + fn point(point: acadrust::types::Vector3) -> DVec3 { + DVec3::new(point.x, point.y, point.z) + } + + fn nearest_marker(points: &[acadrust::types::Vector3], point: DVec3) -> Option { + points + .iter() + .enumerate() + .min_by(|(_, left), (_, right)| { + (Self::point(**left) - point) + .length_squared() + .total_cmp(&(Self::point(**right) - point).length_squared()) + }) + .map(|(index, _)| index as i32) + } + + fn source_reference(entity: &EntityType, handle: Handle, point: DVec3) -> Option { + let EntityType::Spline(spline) = entity else { + return None; + }; + if spline.flags.closed || spline.flags.periodic { + return None; + } + let points = crate::scene::dimension_assoc::source_points(entity); + let endpoints = [*points.first()?, *points.last()?]; + let marker = Self::nearest_marker(&endpoints, point)?; + Some(ParametricRef::point(handle, marker)) + } + + fn target_reference(entity: &EntityType, handle: Handle, point: DVec3) -> Option { + match entity { + EntityType::Line(_) | EntityType::Arc(_) => { + let points = crate::scene::dimension_assoc::source_points(entity); + let endpoints = [*points.first()?, *points.last()?]; + let marker = Self::nearest_marker(&endpoints, point)?; + Some(SmoothTarget { + endpoint: ParametricRef::point(handle, marker), + curve: ParametricRef::whole(handle), + }) + } + EntityType::Spline(spline) if !spline.flags.closed && !spline.flags.periodic => { + let points = crate::scene::dimension_assoc::source_points(entity); + let endpoints = [*points.first()?, *points.last()?]; + let marker = Self::nearest_marker(&endpoints, point)?; + Some(SmoothTarget { + endpoint: ParametricRef::point(handle, marker), + curve: ParametricRef::whole(handle), + }) + } + EntityType::LwPolyline(_) | EntityType::Polyline2D(_) => { + let planar = crate::entities::curve::entity_curve(entity)?; + let local = planar.plane.project(point.to_array())?; + let segments = planar.curve.segments(); + let (segment, _) = cadkernel::geom2d::nearest_of(segments.iter(), local)?; + let points = crate::scene::dimension_assoc::source_points(entity); + let closed = match entity { + EntityType::LwPolyline(polyline) => polyline.is_closed, + EntityType::Polyline2D(polyline) => polyline.is_closed(), + _ => false, + }; + let end = if segment + 1 < points.len() { + segment + 1 + } else if closed { + 0 + } else { + return None; + }; + let candidates = [points.get(segment).copied()?, points.get(end).copied()?]; + let side = Self::nearest_marker(&candidates, point)? as usize; + let vertex = if side == 0 { segment } else { end }; + Some(SmoothTarget { + endpoint: ParametricRef::point(handle, vertex as i32), + curve: ParametricRef::segment(handle, segment), + }) + } + _ => None, + } + } + + pub fn preselected_refs( + first: (&EntityType, Handle), + second: (&EntityType, Handle), + ) -> Option> { + let source_points = crate::scene::dimension_assoc::source_points(first.0); + let source_ends = [*source_points.first()?, *source_points.last()?]; + let source_point = source_ends + .iter() + .copied() + .min_by(|left, right| { + let target_points = crate::scene::dimension_assoc::source_points(second.0); + let distance = |source: acadrust::types::Vector3| { + target_points + .iter() + .map(|target| (source - *target).length_squared()) + .fold(f64::INFINITY, f64::min) + }; + distance(*left).total_cmp(&distance(*right)) + })?; + let source = Self::source_reference(first.0, first.1, Self::point(source_point))?; + + let target_points = crate::scene::dimension_assoc::source_points(second.0); + let target_point = target_points + .iter() + .copied() + .min_by(|left, right| { + (Self::point(*left) - Self::point(source_point)) + .length_squared() + .total_cmp( + &(Self::point(*right) - Self::point(source_point)).length_squared(), + ) + })?; + let target = Self::target_reference(second.0, second.1, Self::point(target_point))?; + let mut refs = vec![source, target.endpoint]; + if target.curve.marker.is_some() { + refs.push(target.curve); + } + Some(refs) + } + + fn invalid_source() -> CmdResult { + CmdResult::ReportError( + t!("Invalid first selection for Smooth. Select an endpoint of an open spline.") + .into_owned(), + ) + } + + fn invalid_target() -> CmdResult { + CmdResult::ReportError( + t!("Invalid second selection for Smooth. Select an endpoint of a line, arc, polyline segment or open spline.") + .into_owned(), + ) + } +} + +impl CadCommand for SmoothConstraintCommand { + fn name(&self) -> &'static str { + "GCSMOOTH" + } + + fn prompt(&self) -> String { + if self.source.is_some() { + t!("GCSMOOTH Select second curve endpoint:").into_owned() + } else { + t!("GCSMOOTH Select first spline curve endpoint:").into_owned() + } + } + + fn needs_entity_pick(&self) -> bool { + true + } + + fn entity_pick_highlights_hover(&self) -> bool { + true + } + + fn inject_before_entity_pick(&self) -> bool { + true + } + + fn inject_picked_entity(&mut self, entity: EntityType) { + self.picked_entity = Some(entity); + } + + fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult { + if handle.is_null() { + return CmdResult::NeedPoint; + } + let Some(entity) = self.picked_entity.take() else { + return CmdResult::NeedPoint; + }; + let Some(source) = self.source else { + let Some(source) = Self::source_reference(&entity, handle, point) else { + return Self::invalid_source(); + }; + self.source = Some(source); + return CmdResult::NeedPoint; + }; + let Some(target) = Self::target_reference(&entity, handle, point) else { + return Self::invalid_target(); + }; + if source.entity == target.endpoint.entity { + return Self::invalid_target(); + } + let mut refs = vec![source, target.endpoint]; + if target.curve.marker.is_some() { + refs.push(target.curve); + } + CmdResult::AddParametricConstraint { + kind: ConstraintKind::Smooth, + refs, + driving_param: None, + label: "Smooth constraint", + } + } + + fn on_point(&mut self, _point: DVec3) -> CmdResult { + CmdResult::NeedPoint + } + + fn on_enter(&mut self) -> CmdResult { + CmdResult::Cancel + } + + fn on_escape(&mut self) -> CmdResult { + CmdResult::Cancel + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn polyline_target_keeps_the_picked_arc_segment_and_endpoint() { + let handle = Handle::new(7); + let mut polyline = acadrust::entities::LwPolyline::new(); + polyline.vertices = vec![ + acadrust::entities::LwVertex::from_coords(0.0, 0.0), + acadrust::entities::LwVertex::with_bulge( + acadrust::types::Vector2::new(5.0, 0.0), + 1.0, + ), + acadrust::entities::LwVertex::from_coords(10.0, 0.0), + ]; + + let target = SmoothConstraintCommand::target_reference( + &EntityType::LwPolyline(polyline), + handle, + DVec3::new(10.0, 0.0, 0.0), + ) + .expect("arc endpoint"); + assert_eq!(target.curve, ParametricRef::segment(handle, 1)); + assert_eq!(target.endpoint, ParametricRef::point(handle, 2)); + } +} diff --git a/src/modules/parametric/tools.rs b/src/modules/parametric/tools.rs index 49f211727..4d2e8fd94 100644 --- a/src/modules/parametric/tools.rs +++ b/src/modules/parametric/tools.rs @@ -6,12 +6,12 @@ pub mod horizontal { use super::*; pub fn tool() -> ToolDef { ToolDef { - id: "HCONSTRAINT", + id: "GCHORIZONTAL", label: "Horizontal", icon: IconKind::Svg(include_bytes!( "../../../assets/icons/constrain/horizontal.svg" )), - event: ModuleEvent::Command("HCONSTRAINT".to_string()), + event: ModuleEvent::Command("GCHORIZONTAL".to_string()), } } } @@ -88,12 +88,12 @@ pub mod concentric { use super::*; pub fn tool() -> ToolDef { ToolDef { - id: "NCONSTRAINT", + id: "GCCONCENTRIC", label: "Concentric", icon: IconKind::Svg(include_bytes!( "../../../assets/icons/constrain/concentric.svg" )), - event: ModuleEvent::Command("NCONSTRAINT".to_string()), + event: ModuleEvent::Command("GCCONCENTRIC".to_string()), } } } @@ -160,14 +160,14 @@ pub mod symmetric { // ── Autocomplete registry ───────────────────────────────── inventory::submit!(crate::command::CommandRegistration { names: &[ - "HCONSTRAINT", + "GCHORIZONTAL", "VCONSTRAINT", "PCONSTRAINT", "QCONSTRAINT", "GCPERPENDICULAR", "ECONSTRAINT", "TCONSTRAINT", - "NCONSTRAINT", + "GCCONCENTRIC", "NRCONSTRAINT", "LCONSTRAINT", "FXCONSTRAINT", diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 829b20cf1..485b192e9 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -3110,6 +3110,75 @@ mod tests { assert_eq!(line_wire.world_width, 10.0); } + #[test] + fn split_mixed_polyline_keeps_exact_line_intersections() { + let tangents = vec![ + TangentGeom::Line { + p1: [-10.0, 0.0, 0.0], + p2: [0.0, 0.0, 0.0], + }, + TangentGeom::Arc { + center: [0.0, 4.0, 0.0], + axis_x: [1.0, 0.0, 0.0], + axis_y: [0.0, 1.0, 0.0], + radius: 4.0, + start_angle: -std::f64::consts::FRAC_PI_2, + end_angle: std::f64::consts::FRAC_PI_2, + }, + TangentGeom::Line { + p1: [0.0, 8.0, 0.0], + p2: [10.0, 8.0, 0.0], + }, + ]; + let vertices = vec![ + [-10.0, 0.0, 0.0], + [0.0, 0.0, 0.0], + [0.0, 8.0, 0.0], + [10.0, 8.0, 0.0], + ]; + let wires = split_mixed_polyline( + &tangents, + &vertices, + "mixed", + [1.0; 4], + false, + 0.0, + [0.0; 8], + 1.0, + Vec::new(), + None, + true, + &[(0.0, 0.0); 3], + 0.0, + Vec::new(), + Vec::new(), + ); + let line_wire = wires + .iter() + .find(|wire| { + wire.tangent_geoms + .iter() + .all(|geom| matches!(geom, TangentGeom::Line { .. })) + }) + .expect("straight-segment wire"); + let circle = WireModel { + tangent_geoms: vec![TangentGeom::PlanarCircle { + center: [5.0, 4.0, 0.0], + axis_x: [1.0, 0.0, 0.0], + axis_y: [0.0, 1.0, 0.0], + radius: 5.0, + }], + ..Default::default() + }; + + let mut points = crate::snap::exact_curve_intersections(line_wire, &circle) + .expect("analytical intersections"); + points.sort_by(|left, right| left.x.total_cmp(&right.x)); + assert_eq!(points.len(), 2, "unexpected intersections: {points:?}"); + assert!((points[0] - glam::DVec3::new(2.0, 8.0, 0.0)).length() < 1.0e-9); + assert!((points[1] - glam::DVec3::new(8.0, 8.0, 0.0)).length() < 1.0e-9); + } + #[test] fn test_split_mixed_polyline_zero_width_segments_after_tapered_segment() { let tg_line = TangentGeom::Line { diff --git a/src/scene/dwg_native_constraints.rs b/src/scene/dwg_native_constraints.rs index f4d755674..e36ca0c6d 100644 --- a/src/scene/dwg_native_constraints.rs +++ b/src/scene/dwg_native_constraints.rs @@ -56,8 +56,6 @@ struct GroupBuilder<'a> { /// Real entity handles that ended up with a geometry node — what /// `AssocGeomDependency` objects get created for, in first-touch order. referenced_entities: Vec, - horizontal_datum: Option, - vertical_datum: Option, } impl<'a> GroupBuilder<'a> { @@ -68,8 +66,6 @@ impl<'a> GroupBuilder<'a> { next_node_id: FIRST_NODE_ID, entities: FxHashMap::default(), referenced_entities: Vec::new(), - horizontal_datum: None, - vertical_datum: None, } } @@ -454,11 +450,17 @@ impl<'a> GroupBuilder<'a> { return Some(*existing); } let segment_midpoint = ParametricRef::point(handle, marker).segment_midpoint_index(); + let segment_center = ParametricRef::point(handle, marker).segment_center_index(); let polyline = matches!( self.document.get_entity(handle), Some(EntityType::LwPolyline(_) | EntityType::Polyline2D(_)) ); - let (curve_id, point_type) = if let Some(segment) = segment_midpoint { + let (curve_id, point_type) = if let Some(segment) = segment_center { + ( + self.segment_node(handle, segment)?, + implicit_point_type::CENTER, + ) + } else if let Some(segment) = segment_midpoint { ( self.segment_node(handle, segment)?, implicit_point_type::MID, @@ -551,6 +553,9 @@ impl<'a> GroupBuilder<'a> { if r.segment_midpoint_index().is_some() { return self.point_node(r.entity, r.marker?); } + if r.segment_center_index().is_some() { + return self.point_node(r.entity, r.marker?); + } if let Some(segment) = r.segment_index() { return self.segment_node(r.entity, segment); } @@ -653,15 +658,7 @@ impl<'a> GroupBuilder<'a> { Some(node_id) } - fn datum_line(&mut self, horizontal: bool) -> i32 { - let cached = if horizontal { - self.horizontal_datum - } else { - self.vertical_datum - }; - if let Some(node_id) = cached { - return node_id; - } + fn datum_line(&mut self, direction: Vector3) -> i32 { let node_id = self.alloc_node_id(); self.push_node( node_id, @@ -670,18 +667,13 @@ impl<'a> GroupBuilder<'a> { geometry_dependency: Handle::NULL, geometry_node_id: node_id, point: Vector3::ZERO, - direction: if horizontal { - Vector3::UNIT_X + direction: if direction.length_squared() > 1.0e-24 { + direction.normalize() } else { - Vector3::UNIT_Y + Vector3::UNIT_X }, }, ); - if horizontal { - self.horizontal_datum = Some(node_id); - } else { - self.vertical_datum = Some(node_id); - } node_id } @@ -771,7 +763,13 @@ fn constraint_node( ) -> Option { let refs: &[ParametricRef] = &constraint.refs; if constraint.kind == ConstraintKind::Smooth { - let [first, second] = refs else { return None }; + let (first, second, second_curve_ref) = match refs { + [first, second] => (*first, *second, ParametricRef::whole(second.entity)), + [first, second, second_curve] if second.entity == second_curve.entity => { + (*first, *second, *second_curve) + } + _ => return None, + }; let endpoint_parameter = |reference: ParametricRef| { let entity = document.get_entity(reference.entity)?; let EntityType::Spline(spline) = entity else { @@ -791,15 +789,15 @@ fn constraint_node( }?; Some(Some(value)) }; - let first_parameter = endpoint_parameter(*first)?; - let second_parameter = endpoint_parameter(*second)?; + let first_parameter = endpoint_parameter(first)?; + let second_parameter = endpoint_parameter(second)?; if first_parameter.is_none() && second_parameter.is_none() { return None; } let first_curve = builder.geometry_node(first.entity)?; - let second_curve = builder.geometry_node(second.entity)?; - let first_point = builder.ref_node(*first)?; - let second_point = builder.ref_node(*second)?; + let second_curve = builder.ref_node(second_curve_ref)?; + let first_point = builder.ref_node(first)?; + let second_point = builder.ref_node(second)?; let coincidence = builder.alloc_node_id(); builder.push_node( @@ -917,7 +915,12 @@ fn constraint_node( .map(|reference| builder.ref_node(*reference)) .collect::>()?; let horizontal = constraint.kind == ConstraintKind::Horizontal; - let datum = builder.datum_line(horizontal); + let fallback = if horizontal { + Vector3::UNIT_X + } else { + Vector3::UNIT_Y + }; + let datum = builder.datum_line(constraint.axis_direction.unwrap_or(fallback)); let node_id = builder.alloc_node_id(); builder.push_node( node_id, @@ -1647,6 +1650,21 @@ fn group_requires_preservation(document: &CadDocument, group: &Assoc2dConstraint }) } +fn work_plane_vector(work_plane: &[Vector3; 3], local: Vector3) -> Option { + let [origin, axis_x, axis_y] = *work_plane; + let plane = cadkernel::space::Plane::from_axes( + [origin.x, origin.y, origin.z], + [axis_x.x, axis_x.y, axis_x.z], + [axis_y.x, axis_y.y, axis_y.z], + ); + let mut world = cadkernel::space::Vec3::from(plane.vector_at([local.x, local.y])); + if local.z != 0.0 { + world = world + cadkernel::space::Vec3::from(plane.normal()?) * local.z; + } + let world = world.normalize()?; + Some(Vector3::new(world.x, world.y, world.z)) +} + fn polyline_segment_reference( document: &CadDocument, entity: Handle, @@ -2073,6 +2091,9 @@ pub(super) fn native_constraint_set( implicit_point_type::MID => { ParametricRef::segment_midpoint(curve.entity, segment) } + implicit_point_type::CENTER => { + ParametricRef::segment_center(curve.entity, segment) + } _ => continue, } } else { @@ -2130,7 +2151,7 @@ pub(super) fn native_constraint_set( owned_constraint_ids, .. } if kind == ConstraintKind::Smooth => { - let endpoints = owned_constraint_ids + let mut endpoints: Vec = owned_constraint_ids .iter() .filter_map(|id| group.nodes.iter().find(|child| child.node_id == *id)) .find(|child| { @@ -2146,6 +2167,23 @@ pub(super) fn native_constraint_set( .collect() }) .unwrap_or_default(); + if let Some(target) = endpoints.get(1).copied() { + let segment = owned_constraint_ids + .iter() + .filter_map(|id| group.nodes.iter().find(|child| child.node_id == *id)) + .filter(|child| { + child.class_name.eq_ignore_ascii_case("AcTangentConstraint") + }) + .flat_map(|child| child.connections.iter()) + .filter_map(|id| refs.get(id).copied()) + .find(|reference| { + reference.entity == target.entity + && reference.segment_index().is_some() + }); + if let Some(segment) = segment { + endpoints.push(segment); + } + } (endpoints, Vec::new()) } _ => ( @@ -2192,6 +2230,7 @@ pub(super) fn native_constraint_set( ConstraintKind::DistanceDirected => matches!(targets.len(), 2 | 3), ConstraintKind::EqualDistance => targets.len() == 4, ConstraintKind::RigidSet => !targets.is_empty() && rigid_points.len() >= 2, + ConstraintKind::Smooth => matches!(targets.len(), 2 | 3), _ => targets.len() == 2, }; if !valid_target_count { @@ -2215,6 +2254,31 @@ pub(super) fn native_constraint_set( if let Some(constraint) = set.constraints.last_mut() { constraint.enabled = enabled; constraint.rigid_points = rigid_points; + if matches!(kind, ConstraintKind::Horizontal | ConstraintKind::Vertical) { + let datum_id = match &node.data { + AssocConstraintNodeData::Parallel { + datum_line_index, .. + } => *datum_line_index, + _ => None, + }; + let local_direction = datum_id.and_then(|datum_id| { + group.nodes.iter().find_map(|datum| { + (datum.node_id == datum_id).then_some(&datum.data).and_then(|data| { + match data { + AssocConstraintNodeData::Line { direction, .. } => { + Some(*direction) + } + _ => None, + } + }) + }) + }); + if let Some(world) = + local_direction.and_then(|local| work_plane_vector(&group.work_plane, local)) + { + constraint.axis_direction = Some(world); + } + } if let AssocConstraintNodeData::Distance { direction_type, distance, @@ -2845,14 +2909,15 @@ mod tests { fn horizontal_and_vertical_constraints_round_trip_through_dwg_and_dxf() { for ext in ["dwg", "dxf"] { let mut scene = Scene::new(); - let a = line_entity(&mut scene, (0.0, 0.0), (10.0, 0.0)); + let a = line_entity(&mut scene, (0.0, 0.0), (6.0, 8.0)); let b = line_entity(&mut scene, (10.0, 0.0), (10.0, 5.0)); + let horizontal_direction = Vector3::new(0.6, 0.8, 0.0); scene .parametric_constraint_set_mut(ParametricScope::ModelSpace) - .add( + .add_axis_constraint( ConstraintKind::Horizontal, vec![ParametricRef::whole(a)], - None, + horizontal_direction, ); scene .parametric_constraint_set_mut(ParametricScope::ModelSpace) @@ -2918,6 +2983,20 @@ mod tests { assert!(datums.iter().all(|datum| group.nodes.iter().any(|node| { node.node_id == *datum && node.class_name == "AcConstrainedDatumLine" }))); + + let mut restored = Scene::new(); + restored.document = reloaded; + restored.load_parametric_constraints_from_document(); + let horizontal = restored + .parametric_constraint_set(ParametricScope::ModelSpace) + .unwrap() + .constraints + .iter() + .find(|constraint| constraint.kind == ConstraintKind::Horizontal) + .unwrap(); + assert!( + (horizontal.axis_direction.unwrap() - horizontal_direction).length() < 1.0e-12 + ); } } diff --git a/src/scene/entity.rs b/src/scene/entity.rs index 3b31f90ea..2c2d105df 100644 --- a/src/scene/entity.rs +++ b/src/scene/entity.rs @@ -2709,4 +2709,25 @@ impl Scene { self.named_parameters = crate::scene::named_parameters::ParameterTable::new(); self.bump_geometry(); } + + /// Reset to the drawing File → New produces. Every path that starts a + /// fresh drawing (the New tab, CLEAR, the automation `new` op) goes + /// through here so they all agree on what a blank drawing contains. + pub fn reset_to_new_drawing(&mut self) { + self.clear(); + self.populate_new_drawing_defaults(); + } + + /// What a blank document gets on top of the codec's defaults: the + /// standard linetypes and a compatible page setup on every paper layout. + pub fn populate_new_drawing_defaults(&mut self) { + crate::io::linetypes::populate_document(&mut self.document); + for obj in self.document.objects.values_mut() { + if let acadrust::objects::ObjectType::Layout(l) = obj { + if l.name != "Model" { + crate::scene::apply_default_page_setup(l, ""); + } + } + } + } } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 57e7e9264..d8b91f5f2 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -33,6 +33,7 @@ pub(crate) mod centermark; pub(crate) mod dimension_assoc; pub(crate) mod dimension_assoc_chain; pub use dimension_assoc::{ReferenceStatus, ResolvedReference}; +pub use page_setup::{apply_default_page_setup, rotated_margins}; mod dwg_native_constraints; mod entity; #[cfg(test)] @@ -3245,6 +3246,23 @@ impl Scene { ); } + pub fn bump_entities_with_parametric_transform_policy( + &mut self, + changes: &[(Handle, ChangeKind)], + driven_refs: &[parametric_constraints::ParametricRef], + transformed_refs: &[parametric_constraints::ParametricRef], + retained_originals: &[(Handle, EntityType)], + ) { + self.bump_entities_with_solve_policy( + changes, + driven_refs, + true, + retained_originals, + transformed_refs, + true, + ); + } + fn bump_entities_with_solve_policy( &mut self, changes: &[(Handle, ChangeKind)], @@ -4361,13 +4379,8 @@ impl Scene { None })?; // `paper_limits()` already swaps the sheet for a 90°/270° rotation, so the - // margins must rotate to the same edges: a margin on a physical side moves - // to the displayed side that side rotates onto. - let (ml, mb, mr, mt) = match rot { - 1 | 3 => (bottom, left, top, right), - 2 => (right, top, left, bottom), - _ => (left, bottom, right, top), - }; + // margins must rotate to the same edges. + let (ml, mb, mr, mt) = page_setup::rotated_margins((left, bottom, right, top), rot); // Plot margins are millimetres like the paper size; scale them into the // layout's paper-space units so the inset matches the (already scaled) // sheet rect. Without this an inch paper space insets an ~8-inch sheet @@ -4953,6 +4966,78 @@ impl Scene { .then_some(owner) } + /// Millimetres in one unit of the paper side of this drawing's scale + /// family: 25.4 for an imperial drawing (whose `1/8" = 1'-0"` style scales + /// measure paper in inches), 1 for a metric one. Plot-scale factors built + /// from [`Scene::scale_list`] are expressed per this unit; page setups + /// store theirs per the layout's own paper unit, so the two are converted + /// through this value. + pub(crate) fn scale_family_unit_mm(&self) -> f64 { + if self.prefers_imperial_scales() == Some(true) { + 25.4 + } else { + 1.0 + } + } + + /// The `paper : drawing` ratio behind a scale-list name, from the + /// drawing's own `Scale` object when it has one, otherwise from the + /// built-in family. A ratio keeps the plot scale readable in a page setup + /// (`1 : 250` rather than `0.004 : 1`). + pub(crate) fn scale_ratio(&self, name: &str) -> Option<(f64, f64)> { + use acadrust::objects::ObjectType; + let stored = self.document.objects.values().find_map(|o| match o { + ObjectType::Scale(s) if !s.is_temporary && s.name.eq_ignore_ascii_case(name) => { + Some((s.paper_units, s.drawing_units)) + } + _ => None, + }); + stored + .or_else(|| Self::parse_scale_name_ratio(name)) + .filter(|(paper, drawing)| *paper > 0.0 && *drawing > 0.0) + } + + /// The `paper : drawing` ratio spelled by a scale name: `1:250` gives + /// `(1, 250)`, an architectural `1/8" = 1'-0"` gives `(0.125, 12)` in + /// inches. Anything else is not a scale name. + pub(crate) fn parse_scale_name_ratio(name: &str) -> Option<(f64, f64)> { + if let Some((paper, drawing)) = name.split_once(':') { + let paper: f64 = paper.trim().parse().ok()?; + let drawing: f64 = drawing.trim().parse().ok()?; + return (paper > 0.0 && drawing > 0.0).then_some((paper, drawing)); + } + let (paper, drawing) = name.split_once('=')?; + let paper = Self::parse_feet_inches(paper)?; + let drawing = Self::parse_feet_inches(drawing)?; + (paper > 0.0 && drawing > 0.0).then_some((paper, drawing)) + } + + /// Inches in a `F'-I"` / `I"` / `F'` length, where the inch part may be a + /// fraction (`3/32"`). + fn parse_feet_inches(text: &str) -> Option { + let text = text.trim(); + let (feet, inches) = match text.split_once('\'') { + Some((feet, rest)) => ( + feet.trim().parse::().ok()?, + rest.trim_start_matches('-').trim(), + ), + None => (0.0, text), + }; + let inches = inches.trim_end_matches('"').trim(); + let inches = if inches.is_empty() { + 0.0 + } else if let Some((num, den)) = inches.split_once('/') { + let den: f64 = den.trim().parse().ok()?; + if den <= 0.0 { + return None; + } + num.trim().parse::().ok()? / den + } else { + inches.parse::().ok()? + }; + Some(feet * 12.0 + inches) + } + /// Handle of the real (non-temporary) `Scale` object with this display name. /// Scales are matched by their `name` field, not the dictionary key — some /// files key the entries by an internal identifier (`*A1`, `A0`, …) that diff --git a/src/scene/modify.rs b/src/scene/modify.rs index b13a1f853..e4ae6e69c 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -161,6 +161,80 @@ impl Scene { .filter(|&h| !self.is_layer_locked(h)) .collect(); let handles = &handles[..]; + let retained_originals: Vec<(Handle, EntityType)> = handles + .iter() + .filter_map(|handle| { + self.document + .get_entity(*handle) + .cloned() + .map(|entity| (*handle, entity)) + }) + .collect(); + let transformed_refs: Vec<_> = handles + .iter() + .copied() + .map(parametric_constraints::ParametricRef::whole) + .collect(); + let mut driven_refs = Vec::new(); + if !matches!(t, EntityTransform::Translate(_)) { + for set in &self.parametric_constraints { + for constraint in &set.constraints { + if !constraint.enabled + || !matches!( + constraint.kind, + parametric_constraints::ConstraintKind::Parallel + | parametric_constraints::ConstraintKind::Perpendicular + | parametric_constraints::ConstraintKind::Colinear + ) + { + continue; + } + let selected_refs = constraint + .refs + .iter() + .filter(|reference| handles.contains(&reference.entity)) + .count(); + if selected_refs == 0 || selected_refs == constraint.refs.len() { + continue; + } + for reference in constraint + .refs + .iter() + .copied() + .filter(|reference| !handles.contains(&reference.entity)) + { + let anchor = if let Some(index) = reference.segment_index() { + parametric_constraints::ParametricRef::point( + reference.entity, + index as i32, + ) + } else if reference.directional_axis().is_some() + && matches!( + self.document.get_entity(reference.entity), + Some(EntityType::Ellipse(_)) + ) + { + parametric_constraints::ParametricRef::center(reference.entity) + } else { + parametric_constraints::ParametricRef::point(reference.entity, 0) + }; + let follows_selected_point = set.constraints.iter().any(|link| { + link.enabled + && link.kind + == parametric_constraints::ConstraintKind::Coincident + && link.refs.contains(&anchor) + && link + .refs + .iter() + .any(|item| handles.contains(&item.entity)) + }); + if !follows_selected_point && !driven_refs.contains(&anchor) { + driven_refs.push(anchor); + } + } + } + } + } // Preserve readable text orientation when MIRRTEXT is disabled. let preserve_text_orientation = matches!(t, EntityTransform::Mirror { .. }) && !self.document.header.mirror_text; @@ -290,7 +364,12 @@ impl Scene { // per-handle, keeping the block cache + all other memoized wires. let changes: Vec<(Handle, ChangeKind)> = handles.iter().map(|&h| (h, ChangeKind::Modified)).collect(); - self.bump_entities(&changes); + self.bump_entities_with_parametric_transform_policy( + &changes, + &driven_refs, + &transformed_refs, + &retained_originals, + ); self.refresh_meshes_for_handles(&refresh_solid_handles); } diff --git a/src/scene/page_setup.rs b/src/scene/page_setup.rs index a09db2e6c..5f900d181 100644 --- a/src/scene/page_setup.rs +++ b/src/scene/page_setup.rs @@ -5,9 +5,60 @@ //! dictionary + objects consistent so the result round-trips through DWG/DXF. use super::Scene; -use acadrust::objects::{Dictionary, ObjectType, PlotSettings}; +use acadrust::objects::{Dictionary, Layout, ObjectType, PlotSettings}; use acadrust::Handle; +/// Give a freshly created paper layout the page setup a new drawing starts +/// with — ISO A4 landscape on no plotter, plotted 1:1 as a layout — stored the +/// way the file format expects it: the portrait medium under its canonical +/// name with the orientation in the rotation code. `plot_style` names the +/// plot style table to attach, or is empty. +pub fn apply_default_page_setup(layout: &mut Layout, plot_style: &str) { + use acadrust::objects::{PlotPaperUnits, PlotRotation, PlotType, ScaledType}; + let a4 = crate::io::paper_catalog::default_paper(); + let (paper_width, paper_height) = a4.portrait_mm(); + layout.min_limits = (0.0, 0.0); + layout.max_limits = (paper_height, paper_width); + layout.min_extents = (0.0, 0.0, 0.0); + layout.max_extents = (paper_height, paper_width, 0.0); + layout.paper_width = paper_width; + layout.paper_height = paper_height; + layout.plot_rotation = PlotRotation::Degrees90.to_code(); + layout.plot_paper_units = PlotPaperUnits::Millimeters.to_code(); + layout.plot_type = PlotType::Layout.to_code(); + layout.plot_scale_type = ScaledType::OneToOne.to_code(); + layout.plot_scale_numerator = 1.0; + layout.plot_scale_denominator = 1.0; + layout.plot_scale_factor = 1.0; + layout.plot_flags.use_standard_scale = true; + layout.plot_flags.print_lineweights = true; + layout.plot_flags.draw_viewports_first = true; + layout.plot_flags.plot_plot_styles = !plot_style.is_empty(); + layout.plot_flags.show_plot_styles = !plot_style.is_empty(); + layout.plot_style_sheet = plot_style.to_string(); + layout.paper_size = a4.canonical.to_string(); + layout.plot_printer_name = crate::io::plot_device::PlotDevice::None.canonical_name(); +} + +/// The unprintable margins of a sheet as they appear on the rotated layout, +/// `(left, bottom, right, top)`. Stored margins belong to the medium's own +/// edges; a plot rotated by a quarter turn (`rotation` is the file's 0–3 +/// code) shows the medium turned on the layout, so each margin moves to the +/// edge its side lands on — verified against limits written by the +/// commercial application: at 90° the medium's top margin is the layout's +/// left one and its left margin the layout's bottom one. +pub fn rotated_margins( + (left, bottom, right, top): (f64, f64, f64, f64), + rotation: i16, +) -> (f64, f64, f64, f64) { + match rotation { + 1 => (top, left, bottom, right), + 2 => (right, top, left, bottom), + 3 => (bottom, right, top, left), + _ => (left, bottom, right, top), + } +} + impl Scene { /// Handle of the `ACAD_PLOTSETTINGS` dictionary, located robustly. /// diff --git a/src/scene/parametric_constraints.rs b/src/scene/parametric_constraints.rs index 467f8d779..98d983f5c 100644 --- a/src/scene/parametric_constraints.rs +++ b/src/scene/parametric_constraints.rs @@ -14,8 +14,8 @@ use acadrust::types::{Handle, Vector3}; /// ordered per-entity-type point list when non-negative (0/1 = a line's /// start/end, ...), or names a special case when negative (-3 = a /// circle/arc's center; -2 = a bounded curve's midpoint). Polyline segment -/// and segment-midpoint references use private negative ranges so they stay -/// distinct from vertex markers. + /// segment-midpoint, and curved-segment-center references use private + /// negative ranges so they stay distinct from vertex markers. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ParametricRef { pub entity: Handle, @@ -28,6 +28,7 @@ pub struct ParametricRef { const POLYLINE_SEGMENT_MARKER_BASE: i32 = -1_000_000; const POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE: i32 = -2_000_000; +const POLYLINE_SEGMENT_CENTER_MARKER_BASE: i32 = -3_000_000; const ELLIPSE_MAJOR_AXIS_MARKER: i32 = -4; const ELLIPSE_MINOR_AXIS_MARKER: i32 = -5; const TEXT_BASELINE_MARKER: i32 = -6; @@ -88,10 +89,25 @@ impl ParametricRef { pub fn segment_midpoint_index(self) -> Option { let marker = self.marker?; - (marker <= POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE) + (marker <= POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE + && marker > POLYLINE_SEGMENT_CENTER_MARKER_BASE) .then(|| (POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE - marker) as usize) } + /// Select the center of one curved polyline segment. + pub fn segment_center(entity: Handle, index: usize) -> Self { + Self { + entity, + marker: Some(POLYLINE_SEGMENT_CENTER_MARKER_BASE - index as i32), + } + } + + pub fn segment_center_index(self) -> Option { + let marker = self.marker?; + (marker <= POLYLINE_SEGMENT_CENTER_MARKER_BASE) + .then(|| (POLYLINE_SEGMENT_CENTER_MARKER_BASE - marker) as usize) + } + /// Select the displayed baseline of a Text or MText entity. pub fn text_baseline(entity: Handle) -> Self { Self { @@ -373,6 +389,10 @@ pub struct ParametricConstraint { /// identifies the direction line for the line-relative modes. pub(crate) distance_direction_type: u8, pub(crate) distance_direction: Option, + /// World-space datum direction captured for Horizontal/Vertical. Native + /// files store the same vector on the connected constrained datum line. + /// `None` keeps legacy world-X/world-Y behavior. + pub(crate) axis_direction: Option, /// Which of the four directed sectors an angular constraint measures. pub(crate) angle_sector: u8, } @@ -460,11 +480,71 @@ impl ParametricConstraintSet { rigid_points: Vec::new(), distance_direction_type: 0, distance_direction: None, + axis_direction: None, angle_sector: angle_sector::PARALLEL_COUNTERCLOCKWISE, }); id } + /// Appends a Horizontal/Vertical constraint tied to an explicit datum + /// direction rather than silently interpreting it in world coordinates. + pub fn add_axis_constraint( + &mut self, + kind: ConstraintKind, + refs: Vec, + direction: Vector3, + ) -> ConstraintId { + let fallback = if kind == ConstraintKind::Vertical { + Vector3::UNIT_Y + } else { + Vector3::UNIT_X + }; + let direction = if direction.length_squared() > 1.0e-24 { + direction.normalize() + } else { + fallback + }; + let id = self.add(kind, refs, None); + if let Some(constraint) = self.constraints.last_mut() { + constraint.axis_direction = Some(direction); + } + id + } + + pub fn contains_axis_constraint( + &self, + kind: ConstraintKind, + refs: &[ParametricRef], + direction: Vector3, + ) -> bool { + let fallback = if kind == ConstraintKind::Vertical { + Vector3::UNIT_Y + } else { + Vector3::UNIT_X + }; + let direction = if direction.length_squared() > 1.0e-24 { + direction.normalize() + } else { + fallback + }; + self.constraints.iter().any(|constraint| { + if !constraint.enabled || constraint.kind != kind { + return false; + } + let same_refs = constraint.refs == refs + || (refs.len() == 2 + && constraint.refs.len() == 2 + && constraint.refs[0] == refs[1] + && constraint.refs[1] == refs[0]); + if !same_refs { + return false; + } + let existing = constraint.axis_direction.unwrap_or(fallback); + existing.length_squared() > 1.0e-24 + && existing.normalize().dot(&direction).abs() >= 1.0 - 1.0e-10 + }) + } + /// Removes a constraint by id. Returns whether one was actually removed. pub fn remove(&mut self, id: ConstraintId) -> bool { let before = self.constraints.len(); @@ -503,7 +583,7 @@ impl ParametricConstraintSet { /// endpoint's equivalent of `dimension_assoc::resolve_reference`, restricted /// to the marker conventions constraint endpoints actually use (whole-entity /// `None`, an ordinary `source_points()` index, the `-3` center case, or a -/// bounded curve/segment midpoint). +/// bounded curve/segment midpoint, or a curved polyline-segment center). /// /// Solver-side registration reads raw entity fields directly. This helper is /// for UI-side consumers that need the current world-space position. @@ -524,12 +604,20 @@ pub(crate) fn resolve_point(entity: &acadrust::EntityType, marker: i32) -> Optio let point = curve.point_at(0.5); return Some(Vector3::new(point[0], point[1], point[2])); } - let segment = ParametricRef { + let reference = ParametricRef { entity: Handle::NULL, marker: Some(marker), + }; + if let Some(segment) = reference.segment_center_index() { + let planar = crate::entities::curve::entity_curve(entity)?; + let curve = planar.curve.segments().into_iter().nth(segment)?; + let cadkernel::geom2d::Curve::Arc(arc) = curve else { + return None; + }; + let point = planar.plane.point_at(arc.centre); + return Some(Vector3::new(point[0], point[1], point[2])); } - .segment_midpoint_index(); - if let Some(segment) = segment { + if let Some(segment) = reference.segment_midpoint_index() { let planar = crate::entities::curve::entity_curve(entity)?; let curve = planar.curve.segments().into_iter().nth(segment)?; let point = planar.plane.point_at(curve.point_at(0.5)); @@ -687,6 +775,27 @@ pub(crate) fn parametric_curve_ref_for_pick( } impl ConstraintKind { + /// Per-kind visibility bit used by CONSTRAINTBARMODE for the standard + /// geometric-constraint family. Helper relations which have no standard + /// bit stay visible under the normal display policy. + pub const fn bar_mode_bit(self) -> Option { + match self { + Self::Horizontal => Some(1), + Self::Vertical => Some(2), + Self::Perpendicular => Some(4), + Self::Parallel => Some(8), + Self::Tangent => Some(16), + Self::Smooth => Some(32), + Self::Coincident => Some(64), + Self::Concentric => Some(128), + Self::Colinear => Some(256), + Self::Symmetric => Some(512), + Self::Equal => Some(1024), + Self::Fixed => Some(2048), + _ => None, + } + } + pub const fn label(self) -> &'static str { match self { Self::Coincident => "Coincident", @@ -810,6 +919,18 @@ fn glyph_placement_for_reference( let anchor = (start + end) * 0.5; return Some((anchor, segment_normal(start, end))); } + if let Some(segment) = r.segment_center_index() { + let planar = crate::entities::curve::entity_curve(entity)?; + let curve = planar.curve.segments().into_iter().nth(segment)?; + let on_arc = planar.plane.point_at(curve.point_at(0.5)); + let cadkernel::geom2d::Curve::Arc(arc) = curve else { + return None; + }; + let center = planar.plane.point_at(arc.centre); + let center = Vector3::new(center[0], center[1], center[2]); + let on_arc = Vector3::new(on_arc[0], on_arc[1], on_arc[2]); + return Some((on_arc, on_arc - center)); + } if let Some(segment) = r.segment_index() { let anchor = resolve_point( entity, @@ -1083,10 +1204,15 @@ impl super::Scene { &self, scope: ParametricScope, id: ConstraintId, + kind: ConstraintKind, related_entity_selected: bool, display_mode: i16, + bar_mode: i16, ) -> bool { self.is_parametric_constraint_visible(scope, id) + && kind + .bar_mode_bit() + .is_none_or(|bit| bar_mode & bit != 0) && (self.shown_parametric_constraints.contains(&(scope, id)) || (display_mode & 2 != 0 && related_entity_selected)) } @@ -1437,40 +1563,6 @@ impl super::Scene { inferred } - pub fn smooth_constraint_refs(&self, handles: &[Handle]) -> Option> { - let [first, second] = handles else { - return None; - }; - let first_entity = self.document.get_entity(*first)?; - let second_entity = self.document.get_entity(*second)?; - let (spline_handle, spline, target_handle, target) = match (first_entity, second_entity) { - (acadrust::EntityType::Spline(spline), target) => (*first, spline, *second, target), - (target, acadrust::EntityType::Spline(spline)) => (*second, spline, *first, target), - _ => return None, - }; - if spline.flags.closed || spline.flags.periodic { - return None; - } - let spline_points = - super::dimension_assoc::source_points(&acadrust::EntityType::Spline(spline.clone())); - let target_points = super::dimension_assoc::source_points(target); - let spline_ends = [*spline_points.first()?, *spline_points.last()?]; - let target_ends = [*target_points.first()?, *target_points.last()?]; - let mut best = (f64::INFINITY, 0, 0); - for (source_marker, source) in spline_ends.iter().enumerate() { - for (target_marker, target) in target_ends.iter().enumerate() { - let distance = (*source - *target).length_squared(); - if distance < best.0 { - best = (distance, source_marker, target_marker); - } - } - } - Some(vec![ - ParametricRef::point(spline_handle, best.1 as i32), - ParametricRef::point(target_handle, best.2 as i32), - ]) - } - /// The constraint set for `scope`, if one has been created. pub fn parametric_constraint_set( &self, @@ -1521,6 +1613,7 @@ impl super::Scene { vp_size: (f32, f32), show_values: bool, display_mode: i16, + bar_mode: i16, ) -> Vec<( ConstraintId, iced::Point, @@ -1555,7 +1648,14 @@ impl super::Scene { .iter() .any(|reference| self.selected.contains(&reference.entity) || self.preview_hidden.contains(&reference.entity)); - self.should_display_parametric_constraint(scope, c.id, selected, display_mode) + self.should_display_parametric_constraint( + scope, + c.id, + c.kind, + selected, + display_mode, + bar_mode, + ) }) .flat_map(|c| { let is_conflicting = set.conflicts.iter().any(|(id, _)| *id == c.id); @@ -1627,6 +1727,7 @@ impl super::Scene { vp_size: (f32, f32), show_values: bool, display_mode: i16, + bar_mode: i16, p: iced::Point, ) -> Option { let placements = self.constraint_glyph_placements_screen( @@ -1634,6 +1735,7 @@ impl super::Scene { vp_size, show_values, display_mode, + bar_mode, ); let glyphs: Vec<(iced::Point, [f32; 2], String, bool)> = placements .iter() @@ -1671,6 +1773,7 @@ impl super::Scene { ConstraintKind, Vec, Option, + Option, )> = Vec::new(); for (scope_index, set) in self.parametric_constraints.iter().enumerate() { for c in &set.constraints { @@ -1685,16 +1788,27 @@ impl super::Scene { marker: r.marker, }) .collect(); - to_add.push((scope_index, c.kind, new_refs, c.driving_param.clone())); + to_add.push(( + scope_index, + c.kind, + new_refs, + c.driving_param.clone(), + c.axis_direction, + )); } } if to_add.is_empty() { return; } let mut touched: Vec = Vec::new(); - for (scope_index, kind, refs, driving_param) in to_add { + for (scope_index, kind, refs, driving_param, axis_direction) in to_add { touched.extend(refs.iter().map(|r| r.entity)); self.parametric_constraints[scope_index].add(kind, refs, driving_param); + self.parametric_constraints[scope_index] + .constraints + .last_mut() + .expect("the copied constraint was just added") + .axis_direction = axis_direction; } touched.sort(); touched.dedup(); @@ -1790,6 +1904,7 @@ mod tests { rigid_points: Vec::new(), distance_direction_type: 0, distance_direction: None, + axis_direction: None, angle_sector: angle_sector::PARALLEL_COUNTERCLOCKWISE, }; let (anchor, direction) = @@ -1825,6 +1940,7 @@ mod tests { rigid_points: Vec::new(), distance_direction_type: 0, distance_direction: None, + axis_direction: None, angle_sector: angle_sector::PARALLEL_COUNTERCLOCKWISE, }; let tangent = relation( @@ -2213,18 +2329,34 @@ mod tests { vec![ParametricRef::whole(h(1))], None, ); - assert!(!scene.should_display_parametric_constraint(scope, id, false, 3)); - assert!(scene.should_display_parametric_constraint(scope, id, true, 2)); - assert!(!scene.should_display_parametric_constraint(scope, id, true, 1)); + assert!(!scene.should_display_parametric_constraint( + scope, id, ConstraintKind::Horizontal, false, 3, 4095 + )); + assert!(scene.should_display_parametric_constraint( + scope, id, ConstraintKind::Horizontal, true, 2, 4095 + )); + assert!(!scene.should_display_parametric_constraint( + scope, id, ConstraintKind::Horizontal, true, 1, 4095 + )); scene.note_parametric_constraint_applied(scope, id, 1); - assert!(scene.should_display_parametric_constraint(scope, id, false, 0)); + assert!(scene.should_display_parametric_constraint( + scope, id, ConstraintKind::Horizontal, false, 0, 4095 + )); scene.set_parametric_constraint_visibility(scope, None, false, false); - assert!(!scene.should_display_parametric_constraint(scope, id, true, 3)); + assert!(!scene.should_display_parametric_constraint( + scope, id, ConstraintKind::Horizontal, true, 3, 4095 + )); scene.set_parametric_constraint_visibility(scope, None, false, true); - assert!(scene.should_display_parametric_constraint(scope, id, false, 0)); + assert!(scene.should_display_parametric_constraint( + scope, id, ConstraintKind::Horizontal, false, 0, 4095 + )); scene.note_parametric_constraint_applied(scope, id, 0); - assert!(!scene.should_display_parametric_constraint(scope, id, false, 3)); - assert!(scene.should_display_parametric_constraint(scope, id, true, 2)); + assert!(!scene.should_display_parametric_constraint( + scope, id, ConstraintKind::Horizontal, false, 3, 4095 + )); + assert!(scene.should_display_parametric_constraint( + scope, id, ConstraintKind::Horizontal, true, 2, 4095 + )); } #[test] @@ -2238,7 +2370,9 @@ mod tests { ); scene.hidden_parametric_constraints.insert((scope, id)); - assert!(!scene.should_display_parametric_constraint(scope, id, true, 2)); + assert!(!scene.should_display_parametric_constraint( + scope, id, ConstraintKind::Horizontal, true, 2, 4095 + )); } #[test] @@ -2313,4 +2447,37 @@ mod tests { vec![h(1), h(2), h(3)] ); } + + #[test] + fn duplicating_an_axis_constraint_keeps_its_direction() { + let mut scene = super::super::Scene::new(); + let line = |y| { + acadrust::EntityType::Line(acadrust::entities::Line::from_points( + Vector3::new(0.0, y, 0.0), + Vector3::new(4.0, y + 1.0, 0.0), + )) + }; + let source = scene.add_entity(line(0.0)); + let copied = scene.add_entity(line(10.0)); + let direction = Vector3::new(3.0, 4.0, 0.0); + scene + .parametric_constraint_set_mut(ParametricScope::ModelSpace) + .add_axis_constraint( + ConstraintKind::Horizontal, + vec![ParametricRef::whole(source)], + direction, + ); + let mut handle_map = rustc_hash::FxHashMap::default(); + handle_map.insert(source, copied); + + scene.duplicate_parametric_constraints_for(&handle_map); + + let constraints = &scene + .parametric_constraint_set(ParametricScope::ModelSpace) + .unwrap() + .constraints; + assert_eq!(constraints.len(), 2); + assert_eq!(constraints[1].refs, vec![ParametricRef::whole(copied)]); + assert_eq!(constraints[1].axis_direction, Some(direction.normalize())); + } } diff --git a/src/scene/parametric_solve.rs b/src/scene/parametric_solve.rs index 30e56a8fc..fab85de33 100644 --- a/src/scene/parametric_solve.rs +++ b/src/scene/parametric_solve.rs @@ -513,6 +513,12 @@ fn resolve_constraint_point( if let Some(point) = geometry.point_for_marker(marker) { return Some(point); } + if let Some(segment) = reference + .segment_center_index() + .and_then(|index| geometry.arc_segment(index)) + { + return Some(segment.arc.circle.center); + } let midpoint_line = reference .segment_midpoint_index() @@ -794,6 +800,175 @@ fn refs_share_supported_plane(document: &acadrust::CadDocument, refs: &[Parametr plane_z.is_some() } +fn concentric_reference_geometry( + document: &acadrust::CadDocument, + reference: ParametricRef, +) -> Option<(Vector3, cadkernel::space::Plane)> { + let marker = reference.marker?; + if marker != -3 && reference.segment_center_index().is_none() { + return None; + } + let entity = document.get_entity(reference.entity)?; + let center = super::parametric_constraints::resolve_point(entity, marker)?; + let plane = crate::entities::curve::entity_curve(entity)?.plane; + Some((center, plane)) +} + +fn concentric_refs_share_plane( + document: &acadrust::CadDocument, + refs: &[ParametricRef], +) -> bool { + let [first, second] = refs else { + return false; + }; + if first == second { + return false; + } + let Some((first_center, first_plane)) = concentric_reference_geometry(document, *first) else { + return false; + }; + let Some((second_center, second_plane)) = concentric_reference_geometry(document, *second) + else { + return false; + }; + let (Some(first_normal), Some(second_normal)) = (first_plane.normal(), second_plane.normal()) + else { + return false; + }; + let parallel = (first_normal[0] * second_normal[0] + + first_normal[1] * second_normal[1] + + first_normal[2] * second_normal[2]) + .abs() + >= 1.0 - 1.0e-9; + let points = [ + first_plane.origin, + second_plane.origin, + [first_center.x, first_center.y, first_center.z], + [second_center.x, second_center.y, second_center.z], + ]; + let tolerance = cadkernel::space::coplanarity_tolerance(&points); + parallel + && first_plane + .distance_to(second_plane.origin) + .is_some_and(|distance| distance.abs() <= tolerance) + && first_plane + .distance_to([second_center.x, second_center.y, second_center.z]) + .is_some_and(|distance| distance.abs() <= tolerance) +} + +fn ref_is_driven(driven: &[ParametricRef], reference: ParametricRef) -> bool { + driven.iter().any(|candidate| { + candidate.entity == reference.entity + && (candidate.marker == reference.marker || candidate.marker.is_none()) + }) +} + +fn apply_spatial_concentric_constraints( + document: &acadrust::CadDocument, + set: &ParametricConstraintSet, + driven_refs: &[ParametricRef], + initial_fixed_refs: &[ParametricRef], + results: &mut Vec<(Handle, EntityType)>, +) { + let current_entity = |handle: Handle, results: &[(Handle, EntityType)]| { + results + .iter() + .rev() + .find(|(candidate, _)| *candidate == handle) + .map(|(_, entity)| entity.clone()) + .or_else(|| document.get_entity(handle).cloned()) + }; + + let mut handles = Vec::::new(); + let mut relations = Vec::new(); + let mut fixed = Vec::new(); + let mut points = Vec::new(); + for constraint in set.constraints.iter().filter(|constraint| { + constraint.enabled + && constraint.kind == ConstraintKind::Concentric + && !refs_share_supported_plane(document, &constraint.refs) + && concentric_refs_share_plane(document, &constraint.refs) + }) { + let [first, second] = constraint.refs.as_slice() else { + continue; + }; + if first.entity == second.entity { + continue; + } + let (Some(first_entity), Some(second_entity)) = ( + current_entity(first.entity, results), + current_entity(second.entity, results), + ) else { + continue; + }; + let (Some(first_marker), Some(second_marker)) = (first.marker, second.marker) else { + continue; + }; + let (Some(first_center), Some(second_center)) = ( + super::parametric_constraints::resolve_point(&first_entity, first_marker), + super::parametric_constraints::resolve_point(&second_entity, second_marker), + ) else { + continue; + }; + let first_index = handles + .iter() + .position(|handle| *handle == first.entity) + .unwrap_or_else(|| { + handles.push(first.entity); + handles.len() - 1 + }); + let second_index = handles + .iter() + .position(|handle| *handle == second.entity) + .unwrap_or_else(|| { + handles.push(second.entity); + handles.len() - 1 + }); + let first_point = [first_center.x, first_center.y, first_center.z]; + let second_point = [second_center.x, second_center.y, second_center.z]; + relations.push((first_index, first_point, second_index, second_point)); + points.extend([first_point, second_point]); + if ref_is_driven(driven_refs, *first) || ref_is_driven(initial_fixed_refs, *first) { + fixed.push(first_index); + } + if ref_is_driven(driven_refs, *second) || ref_is_driven(initial_fixed_refs, *second) { + fixed.push(second_index); + } + } + if relations.is_empty() { + return; + } + fixed.sort_unstable(); + fixed.dedup(); + let tolerance = cadkernel::space::coplanarity_tolerance(&points).max(MOVE_EPS); + let Some(translations) = cadkernel::space::solve_rigid_point_coincidence( + handles.len(), + &relations, + &fixed, + tolerance, + ) else { + return; + }; + for (handle, translation) in handles.into_iter().zip(translations) { + let translation = glam::DVec3::from_array(translation); + if translation.length() <= tolerance { + continue; + } + let Some(mut entity) = current_entity(handle, results) else { + continue; + }; + crate::scene::view::dispatch::apply_transform( + &mut entity, + &crate::command::EntityTransform::Translate(translation), + ); + if let Some((_, current)) = results.iter_mut().find(|(candidate, _)| *candidate == handle) { + *current = entity; + } else { + results.push((handle, entity)); + } + } +} + fn resolved_target(params: &ParameterTable, constraint: &ParametricConstraint) -> Option { let value = constraint.driving_param.as_ref()?.resolve(params).ok()?; if !value.is_finite() { @@ -901,6 +1076,37 @@ fn build_constraint( let point_ref = |sys: &mut System, cache: &mut HashMap<_, _>, r: ParametricRef| { resolve_constraint_point(document, sys, cache, r) }; + let directional_line = + |sys: &mut System, cache: &mut HashMap<_, _>, r: ParametricRef| { + let geometry = resolve_ref(document, sys, cache, r)?; + let minor_axis = matches!( + r.directional_axis(), + Some(super::parametric_constraints::DirectionalAxis::EllipseMinor) + ); + let line = match geometry { + EntityGeom::TextLine(line) + if matches!( + r.directional_axis(), + Some(super::parametric_constraints::DirectionalAxis::TextBaseline) + ) => line, + EntityGeom::Line(line) | EntityGeom::Ray(line) | EntityGeom::XLine(line) + if r.marker.is_none() => line, + EntityGeom::Polyline { .. } => geometry.line_segment(r.segment_index()?)?, + EntityGeom::Ellipse(ellipse) + if matches!( + r.directional_axis(), + Some( + super::parametric_constraints::DirectionalAxis::EllipseMajor + | super::parametric_constraints::DirectionalAxis::EllipseMinor + ) + ) => GLine { + p1: ellipse.center, + p2: ellipse.focus1, + }, + _ => return None, + }; + Some((line, minor_axis)) + }; let point_on_bounded_arc = |sys: &mut System, point: GPoint, arc: GArc| { let initial = BoundedArcValue::initial_parameter(sys.store(), point, arc); @@ -933,30 +1139,49 @@ fn build_constraint( ConstraintKind::Coincident | ConstraintKind::Concentric | ConstraintKind::CenterPoint => { point_pair_equal(sys, cache, &c.refs) } - ConstraintKind::Horizontal => match c.refs.as_slice() { - [r] => whole_line(sys, cache, *r) - .map(|line| { - vec![Rc::new(Equal::new(line.p1.y, line.p2.y, 1.0)) as Rc] - }) - .unwrap_or_default(), - [a, b] => match (point_ref(sys, cache, *a), point_ref(sys, cache, *b)) { - (Some(a), Some(b)) => vec![Rc::new(Equal::new(a.y, b.y, 1.0))], + ConstraintKind::Horizontal | ConstraintKind::Vertical => { + let fallback = if c.kind == ConstraintKind::Vertical { + Vector3::UNIT_Y + } else { + Vector3::UNIT_X + }; + let direction = c.axis_direction.unwrap_or(fallback); + let length = direction.x.hypot(direction.y); + let (dx, dy) = if length > 1.0e-12 { + (direction.x / length, direction.y / length) + } else { + (fallback.x, fallback.y) + }; + let datum = GLine { + p1: GPoint::new(sys.add_param(0.0, true), sys.add_param(0.0, true)), + p2: GPoint::new(sys.add_param(dx, true), sys.add_param(dy, true)), + }; + match c.refs.as_slice() { + [reference] => directional_line(sys, cache, *reference) + .map(|(line, minor_axis)| { + if minor_axis { + vec![Rc::new(PerpendicularConstraint::new( + sys.store(), + line, + datum, + )) as Rc] + } else { + vec![Rc::new(ParallelConstraint::new(sys.store(), line, datum)) + as Rc] + } + }) + .unwrap_or_default(), + [a, b] => match (point_ref(sys, cache, *a), point_ref(sys, cache, *b)) { + (Some(a), Some(b)) => vec![Rc::new(ParallelConstraint::new( + sys.store(), + GLine { p1: a, p2: b }, + datum, + ))], + _ => Vec::new(), + }, _ => Vec::new(), - }, - _ => Vec::new(), - }, - ConstraintKind::Vertical => match c.refs.as_slice() { - [r] => whole_line(sys, cache, *r) - .map(|line| { - vec![Rc::new(Equal::new(line.p1.x, line.p2.x, 1.0)) as Rc] - }) - .unwrap_or_default(), - [a, b] => match (point_ref(sys, cache, *a), point_ref(sys, cache, *b)) { - (Some(a), Some(b)) => vec![Rc::new(Equal::new(a.x, b.x, 1.0))], - _ => Vec::new(), - }, - _ => Vec::new(), - }, + } + } ConstraintKind::Parallel => { let [a, b] = c.refs.as_slice() else { return Vec::new(); @@ -972,39 +1197,6 @@ fn build_constraint( let [a, b] = c.refs.as_slice() else { return Vec::new(); }; - let directional_line = - |sys: &mut System, cache: &mut HashMap<_, _>, r: ParametricRef| { - let geometry = resolve_ref(document, sys, cache, r)?; - let minor_axis = matches!( - r.directional_axis(), - Some(super::parametric_constraints::DirectionalAxis::EllipseMinor) - ); - let line = match geometry { - EntityGeom::TextLine(line) - if matches!( - r.directional_axis(), - Some(super::parametric_constraints::DirectionalAxis::TextBaseline) - ) => line, - EntityGeom::Line(line) | EntityGeom::Ray(line) | EntityGeom::XLine(line) - if r.marker.is_none() => line, - EntityGeom::Polyline { .. } => { - geometry.line_segment(r.segment_index()?)? - } - EntityGeom::Ellipse(ellipse) - if matches!( - r.directional_axis(), - Some( - super::parametric_constraints::DirectionalAxis::EllipseMajor - | super::parametric_constraints::DirectionalAxis::EllipseMinor - ) - ) => GLine { - p1: ellipse.center, - p2: ellipse.focus1, - }, - _ => return None, - }; - Some((line, minor_axis)) - }; let (Some((fixed, fixed_minor)), Some((moving, moving_minor))) = (directional_line(sys, cache, *a), directional_line(sys, cache, *b)) else { @@ -1697,26 +1889,167 @@ fn build_constraint( } } -fn smooth_target_jet(entity: &EntityType, marker: i32) -> Option { - let parameter = match marker { - 0 => 0.0, - 1 => 1.0, - _ => return None, - }; - let curve = crate::entities::curve::entity_curve(entity)?; - if curve.is_closed() { - return None; +fn smooth_refs(refs: &[ParametricRef]) -> Option<(ParametricRef, ParametricRef, ParametricRef)> { + match refs { + [source, target_endpoint] => Some(( + *source, + *target_endpoint, + ParametricRef::whole(target_endpoint.entity), + )), + [source, target_endpoint, target_curve] + if target_endpoint.entity == target_curve.entity => + { + Some((*source, *target_endpoint, *target_curve)) + } + _ => None, } - let step = if marker == 0 { 1.0e-4 } else { -1.0e-4 }; +} + +fn smooth_target_planar_curve( + entity: &EntityType, + curve_reference: ParametricRef, +) -> Option { + let planar = crate::entities::curve::entity_curve(entity)?; + let Some(segment) = curve_reference.segment_index() else { + return (!planar.is_closed()).then_some(planar); + }; + let curve = planar.curve.segments().into_iter().nth(segment)?; + Some(cadkernel::space::PlanarCurve::new(planar.plane, curve)) +} + +fn smooth_endpoint_parameter( + entity: &EntityType, + endpoint_reference: ParametricRef, + curve: &cadkernel::space::PlanarCurve, +) -> Option { + let marker = endpoint_reference.marker?; + let endpoint = super::parametric_constraints::resolve_point(entity, marker)?; + let endpoint = cadkernel::space::Vec3::new(endpoint.x, endpoint.y, endpoint.z); + let start = cadkernel::space::Vec3::from(curve.point_at(0.0)); + let end = cadkernel::space::Vec3::from(curve.point_at(1.0)); + Some(if endpoint.distance(start) <= endpoint.distance(end) { + 0.0 + } else { + 1.0 + }) +} + +fn smooth_target_jet( + entity: &EntityType, + endpoint_reference: ParametricRef, + curve_reference: ParametricRef, +) -> Option { + if curve_reference.segment_index().is_none() { + if let EntityType::Spline(spline) = entity { + let endpoint = match endpoint_reference.marker { + Some(0) => cadkernel::space::SplineEnd::Start, + Some(1) => cadkernel::space::SplineEnd::End, + _ => return None, + }; + let nurbs = crate::entities::spline::nurbs3(spline)?; + return cadkernel::space::CurveJet::from_nurbs(&nurbs, endpoint); + } + } + + let curve = smooth_target_planar_curve(entity, curve_reference)?; + let parameter = smooth_endpoint_parameter(entity, endpoint_reference, &curve)?; + let point = curve.point_at(parameter); let tangent = curve.tangent_at(parameter); - let next = curve.point_at(parameter + step); - let third = curve.point_at(parameter + 2.0 * step); - Some(cadkernel::space::CurveJet { - point, - tangent, - curvature: cadkernel::space::curve::curvature_through(point, next, third), - }) + match &curve.curve { + cadkernel::geom2d::Curve::Line(_) => { + cadkernel::space::CurveJet::linear(point, tangent) + } + cadkernel::geom2d::Curve::Arc(arc) => cadkernel::space::CurveJet::circular( + point, + tangent, + curve.plane.point_at(arc.centre), + ), + _ => None, + } +} + +fn smooth_spline_plane(spline: &acadrust::entities::Spline) -> Option { + let source = if crate::entities::spline::uses_fit_method(spline) { + &spline.fit_points + } else { + &spline.control_points + }; + let origin = source.first()?; + let origin_vector = cadkernel::space::Vec3::new(origin.x, origin.y, origin.z); + let along = source + .iter() + .skip(1) + .map(|point| cadkernel::space::Vec3::new(point.x, point.y, point.z) - origin_vector) + .find(|vector| vector.length_squared() > 1.0e-24)?; + let normal = source + .iter() + .skip(1) + .map(|point| cadkernel::space::Vec3::new(point.x, point.y, point.z) - origin_vector) + .map(|vector| along.cross(vector)) + .find(|vector| vector.length_squared() > 1.0e-24)?; + let plane = cadkernel::space::Plane::orthonormal( + origin_vector.to_array(), + along.to_array(), + normal.to_array(), + )?; + let points: Vec<_> = source.iter().map(|point| [point.x, point.y, point.z]).collect(); + let tolerance = cadkernel::space::coplanarity_tolerance(&points); + source + .iter() + .all(|point| plane.contains([point.x, point.y, point.z], tolerance)) + .then_some(plane) +} + +fn smooth_entity_plane( + entity: &EntityType, + curve_reference: ParametricRef, +) -> Option { + if let EntityType::Spline(spline) = entity { + return crate::entities::curve::entity_curve(entity) + .map(|curve| curve.plane) + .or_else(|| smooth_spline_plane(spline)); + } + smooth_target_planar_curve(entity, curve_reference).map(|curve| curve.plane) +} + +fn smooth_refs_share_plane( + document: &acadrust::CadDocument, + source_ref: ParametricRef, + target_curve_ref: ParametricRef, +) -> bool { + let Some(source_entity) = document.get_entity(source_ref.entity) else { + return false; + }; + let Some(source_plane) = smooth_entity_plane(source_entity, ParametricRef::whole(source_ref.entity)) + else { + return false; + }; + let Some(target_entity) = document.get_entity(target_curve_ref.entity) else { + return false; + }; + let Some(target_plane) = smooth_entity_plane(target_entity, target_curve_ref) else { + return false; + }; + let points = [source_plane.origin, target_plane.origin]; + let tolerance = cadkernel::space::coplanarity_tolerance(&points); + if let EntityType::Line(line) = target_entity { + return source_plane + .contains([line.start.x, line.start.y, line.start.z], tolerance) + && source_plane + .contains([line.end.x, line.end.y, line.end.z], tolerance); + } + let (Some(source_normal), Some(target_normal)) = + (source_plane.normal(), target_plane.normal()) + else { + return false; + }; + cadkernel::space::Vec3::from(source_normal) + .cross(cadkernel::space::Vec3::from(target_normal)) + .length() + <= 1.0e-9 + && source_plane.contains(target_plane.origin, tolerance) + && target_plane.contains(source_plane.origin, tolerance) } fn apply_smooth_constraints( @@ -1729,7 +2062,9 @@ fn apply_smooth_constraints( .iter() .filter(|constraint| constraint.enabled && constraint.kind == ConstraintKind::Smooth) { - let [source_ref, target_ref] = constraint.refs.as_slice() else { + let Some((source_ref, target_ref, target_curve_ref)) = + smooth_refs(&constraint.refs) + else { continue; }; let source = results @@ -1750,10 +2085,7 @@ fn apply_smooth_constraints( Some(1) => cadkernel::space::SplineEnd::End, _ => continue, }; - let Some(target_marker) = target_ref.marker else { - continue; - }; - let Some(target_jet) = smooth_target_jet(&target, target_marker) else { + let Some(target_jet) = smooth_target_jet(&target, target_ref, target_curve_ref) else { continue; }; let Some(curve) = crate::entities::spline::nurbs3(&spline) else { @@ -1842,6 +2174,7 @@ fn polyline_ref_order( } let position = if let Some(index) = reference.segment_index() .or_else(|| reference.segment_midpoint_index()) + .or_else(|| reference.segment_center_index()) { 2 * index + 1 } else { @@ -1868,7 +2201,21 @@ fn constrained_line_axes(constraints: &[&ParametricConstraint]) -> HashMap 1.0e-12 { + let (x, y) = (direction.x.abs() / length, direction.y.abs() / length); + if x >= 1.0 - 1.0e-10 { + axes.insert(*reference, false); + } else if y >= 1.0 - 1.0e-10 { + axes.insert(*reference, true); + } + } } } } @@ -1976,6 +2323,7 @@ fn solve_scope( rigid_points: Vec::new(), distance_direction_type: distance_direction_type::UNDIRECTED, distance_direction: None, + axis_direction: None, angle_sector: angle_sector::PARALLEL_COUNTERCLOCKWISE, }; for constraint in build_constraint( @@ -1993,7 +2341,13 @@ fn solve_scope( let has_smooth = constraints.iter() .any(|constraint| constraint.enabled && constraint.kind == ConstraintKind::Smooth); - if cache.is_empty() && !has_smooth { + let has_spatial_concentric = constraints.iter().any(|constraint| { + constraint.enabled + && constraint.kind == ConstraintKind::Concentric + && !refs_share_supported_plane(document, &constraint.refs) + && concentric_refs_share_plane(document, &constraint.refs) + }); + if cache.is_empty() && !has_smooth && !has_spatial_concentric { return None; } @@ -2123,7 +2477,7 @@ fn solve_scope( for (handle, geom) in &cache { if initial_fixed_refs .iter() - .any(|reference| reference.entity == *handle) + .any(|reference| reference.entity == *handle && reference.marker.is_none()) { continue; } @@ -2295,7 +2649,12 @@ fn solve_scope( }; let directional = set.constraints.iter().any(|constraint| { constraint.enabled - && constraint.kind == ConstraintKind::Perpendicular + && matches!( + constraint.kind, + ConstraintKind::Horizontal + | ConstraintKind::Vertical + | ConstraintKind::Perpendicular + ) && constraint.refs.iter().any(|reference| { reference.entity == *handle && reference.directional_axis().is_some() }) @@ -2336,7 +2695,12 @@ fn solve_scope( }; let directional = set.constraints.iter().any(|constraint| { constraint.enabled - && constraint.kind == ConstraintKind::Perpendicular + && matches!( + constraint.kind, + ConstraintKind::Horizontal + | ConstraintKind::Vertical + | ConstraintKind::Perpendicular + ) && constraint.refs.iter().any(|reference| { reference.entity == *handle && matches!( @@ -2821,8 +3185,8 @@ fn solve_scope( } } (EntityType::Spline(spline), EntityGeom::Spline { curve, control_z }) => { - let mut changed = - spline.fit_points.len() > 0 || spline.control_points.len() != curve.poles.len(); + let mut changed = crate::entities::spline::uses_fit_method(spline) + || spline.control_points.len() != curve.poles.len(); let control_points: Vec<_> = curve .poles .iter() @@ -2839,11 +3203,25 @@ fn solve_scope( .collect(); if changed { let mut updated = spline.clone(); + if crate::entities::spline::uses_fit_method(spline) { + updated.cv_frame_visible = true; + } updated.degree = curve.degree as i32; updated.knots = curve.knots.clone(); updated.control_points = control_points; updated.weights = curve.weights.iter().map(|id| store.get(*id)).collect(); - updated.fit_points.clear(); + if !updated.fit_points.is_empty() { + if let (Some(first), Some(point)) = + (updated.fit_points.first_mut(), updated.control_points.first()) + { + *first = *point; + } + if let (Some(last), Some(point)) = + (updated.fit_points.last_mut(), updated.control_points.last()) + { + *last = *point; + } + } updated.begin_tangent = Vector3::ZERO; updated.end_tangent = Vector3::ZERO; updated.flags.rational = updated @@ -2859,6 +3237,13 @@ fn solve_scope( } } apply_smooth_constraints(document, set, &mut results); + apply_spatial_concentric_constraints( + document, + set, + driven_refs, + initial_fixed_refs, + &mut results, + ); Some((results, dof, conflicts)) } @@ -2871,13 +3256,8 @@ impl Scene { refs: &[ParametricRef], driving_param: Option<&super::named_parameters::DrivingValue>, ) -> Result<(), &'static str> { - if !refs_share_supported_plane(&self.document, refs) { - return Err( - "Constraint references must be supported entities on the same world-XY plane.", - ); - } if kind == ConstraintKind::Smooth { - let [source_ref, target_ref] = refs else { + let Some((source_ref, target_ref, target_curve_ref)) = smooth_refs(refs) else { return Err("Smooth requires one spline endpoint and one target endpoint."); }; if source_ref.entity == target_ref.entity { @@ -2895,13 +3275,16 @@ impl Scene { Some(1) => cadkernel::space::SplineEnd::End, _ => return Err("Smooth requires a spline endpoint."), }; - let Some(target_marker) = target_ref.marker else { + let Some(_) = target_ref.marker else { return Err("Smooth requires a target endpoint."); }; let Some(target) = self.document.get_entity(target_ref.entity) else { return Err("Smooth target does not exist."); }; - let Some(target_jet) = smooth_target_jet(target, target_marker) else { + if !smooth_refs_share_plane(&self.document, source_ref, target_curve_ref) { + return Err("Smooth requires both curves to lie on the same plane."); + } + let Some(target_jet) = smooth_target_jet(target, target_ref, target_curve_ref) else { return Err("The selected target does not support endpoint smoothing."); }; let Some(curve) = crate::entities::spline::nurbs3(spline) else { @@ -2912,6 +3295,26 @@ impl Scene { } return Ok(()); } + if kind == ConstraintKind::Concentric { + if !concentric_refs_share_plane(&self.document, refs) { + return Err( + "Concentric requires two supported circular curves on the same plane.", + ); + } + if !refs_share_supported_plane(&self.document, refs) { + if refs[0].entity == refs[1].entity { + return Err( + "Concentric cannot move two curved segments of the same object on this plane.", + ); + } + return Ok(()); + } + } + if !refs_share_supported_plane(&self.document, refs) { + return Err( + "Constraint references must be supported entities on the same world-XY plane.", + ); + } let validation_target = match driving_param { Some(super::named_parameters::DrivingValue::Named(name)) if !self.named_parameters.contains(name) => @@ -2930,6 +3333,7 @@ impl Scene { rigid_points: Vec::new(), distance_direction_type: 0, distance_direction: None, + axis_direction: None, angle_sector: angle_sector::PARALLEL_COUNTERCLOCKWISE, }; let mut system = System::new(); @@ -3159,6 +3563,17 @@ mod tests { use acadrust::entities::EntityType; use acadrust::types::Vector3; + fn spatial_circle(center: Vector3, radius: f64) -> acadrust::entities::Circle { + let normal = Vector3::UNIT_Y; + let (x, y, z) = crate::scene::view::transform::wcs_point_to_ocs( + (center.x, center.y, center.z), + (normal.x, normal.y, normal.z), + ); + let mut circle = acadrust::entities::Circle::from_coords(x, y, z, radius); + circle.normal = normal; + circle + } + #[test] fn polyline_constraint_order_follows_stored_vertices_for_open_and_closed_polylines() { use acadrust::entities::{LwPolyline, LwVertex, Polyline2D, Vertex2D}; @@ -3820,4 +4235,173 @@ mod tests { assert!(jet.tangent[1].abs() < 1e-9, "jet={jet:?}"); assert!(cadkernel::space::Vec3::from(jet.curvature).length() < 5e-3); } + + #[test] + fn smooth_constraint_uses_the_selected_polyline_arc_curvature() { + let mut scene = Scene::new(); + let mut target = acadrust::entities::LwPolyline::new(); + target.vertices = vec![ + acadrust::entities::LwVertex::with_bulge( + acadrust::types::Vector2::new(0.0, 0.0), + 1.0, + ), + acadrust::entities::LwVertex::from_coords(10.0, 0.0), + ]; + let target = scene.add_entity(EntityType::LwPolyline(target)); + let mut spline = acadrust::entities::Spline::new(); + spline.degree = 3; + spline.control_points = vec![ + Vector3::new(2.0, 1.0, 0.0), + Vector3::new(3.0, 2.0, 0.0), + Vector3::new(4.0, 3.0, 0.0), + Vector3::new(5.0, 3.0, 0.0), + ]; + spline.knots = cadkernel::space::clamped_uniform_knots(3, 4); + let spline = scene.add_entity(EntityType::Spline(spline)); + scene + .parametric_constraint_set_mut(ParametricScope::ModelSpace) + .add( + ConstraintKind::Smooth, + vec![ + ParametricRef::point(spline, 0), + ParametricRef::point(target, 0), + ParametricRef::segment(target, 0), + ], + None, + ); + + scene.bump_entities(&[(target, super::ChangeKind::Modified)]); + + let EntityType::Spline(spline) = scene.document.get_entity(spline).unwrap() else { + panic!("expected spline"); + }; + let curve = crate::entities::spline::nurbs3(spline).unwrap(); + let jet = + cadkernel::space::CurveJet::from_nurbs(&curve, cadkernel::space::SplineEnd::Start) + .unwrap(); + assert!(cadkernel::space::Vec3::from(jet.point).length() < 1.0e-9); + assert!(jet.tangent[0].abs() < 1.0e-9, "jet={jet:?}"); + assert!( + (cadkernel::space::Vec3::from(jet.curvature).length() - 0.2).abs() < 2.0e-4, + "jet={jet:?}" + ); + } + + #[test] + fn smooth_constraint_accepts_a_stale_normal_on_an_arbitrary_plane() { + let mut scene = Scene::new(); + let target = scene.add_entity(EntityType::Line( + acadrust::entities::Line::from_points( + Vector3::new(0.0, 0.0, 1.0), + Vector3::new(5.0, 0.0, 1.0), + ), + )); + let mut source = acadrust::entities::Spline::new(); + source.degree = 3; + source.normal = Vector3::UNIT_Z; + source.control_points = vec![ + Vector3::new(1.0, 0.0, 0.0), + Vector3::new(2.0, 0.0, 2.0), + Vector3::new(4.0, 0.0, 2.0), + Vector3::new(5.0, 0.0, 3.0), + ]; + source.knots = cadkernel::space::clamped_uniform_knots(3, 4); + let source = scene.add_entity(EntityType::Spline(source)); + let refs = vec![ + ParametricRef::point(source, 0), + ParametricRef::point(target, 0), + ]; + assert!(scene + .validate_parametric_constraint(ConstraintKind::Smooth, &refs, None) + .is_ok()); + scene + .parametric_constraint_set_mut(ParametricScope::ModelSpace) + .add(ConstraintKind::Smooth, refs, None); + + scene.bump_entities(&[(target, super::ChangeKind::Modified)]); + + let EntityType::Spline(source) = scene.document.get_entity(source).unwrap() else { + panic!("expected spline"); + }; + let curve = crate::entities::spline::nurbs3(source).unwrap(); + let jet = + cadkernel::space::CurveJet::from_nurbs(&curve, cadkernel::space::SplineEnd::Start) + .unwrap(); + assert!(cadkernel::space::Vec3::from(jet.point) + .distance(cadkernel::space::Vec3::new(0.0, 0.0, 1.0)) + < 1.0e-9); + assert!(jet.tangent[1].abs() < 1.0e-9 && jet.tangent[2].abs() < 1.0e-9); + } + + #[test] + fn spatial_concentric_initial_solve_keeps_the_first_circle_fixed() { + let mut scene = Scene::new(); + let first = scene.add_entity(EntityType::Circle(spatial_circle(Vector3::ZERO, 2.0))); + let second = scene.add_entity(EntityType::Circle(spatial_circle( + Vector3::new(5.0, 0.0, 3.0), + 4.0, + ))); + let first_ref = ParametricRef::center(first); + let second_ref = ParametricRef::center(second); + scene + .parametric_constraint_set_mut(ParametricScope::ModelSpace) + .add( + ConstraintKind::Concentric, + vec![first_ref, second_ref], + None, + ); + + scene.bump_entities_with_initial_parametric_policy( + &[ + (first, super::ChangeKind::Modified), + (second, super::ChangeKind::Modified), + ], + &[first_ref], + true, + ); + + let EntityType::Circle(first_circle) = scene.document.get_entity(first).unwrap() else { + panic!("expected circle"); + }; + let EntityType::Circle(second_circle) = scene.document.get_entity(second).unwrap() else { + panic!("expected circle"); + }; + assert!(first_circle.center_wcs().length() < 1.0e-9); + assert!(second_circle.center_wcs().length() < 1.0e-9); + assert!((second_circle.radius - 4.0).abs() < 1.0e-12); + } + + #[test] + fn spatial_concentric_chain_follows_the_driven_end() { + let mut scene = Scene::new(); + let handles = [1.0, 2.0, 3.0].map(|radius| { + scene.add_entity(EntityType::Circle(spatial_circle(Vector3::ZERO, radius))) + }); + let set = scene.parametric_constraint_set_mut(ParametricScope::ModelSpace); + set.add( + ConstraintKind::Concentric, + vec![ParametricRef::center(handles[0]), ParametricRef::center(handles[1])], + None, + ); + set.add( + ConstraintKind::Concentric, + vec![ParametricRef::center(handles[1]), ParametricRef::center(handles[2])], + None, + ); + let driven_center = Vector3::new(8.0, 0.0, -3.0); + *scene.document.get_entity_mut(handles[2]).unwrap() = + EntityType::Circle(spatial_circle(driven_center, 3.0)); + + scene.bump_entities_with_parametric_driven( + &[(handles[2], super::ChangeKind::Modified)], + &[ParametricRef::whole(handles[2])], + ); + + for handle in handles { + let EntityType::Circle(circle) = scene.document.get_entity(handle).unwrap() else { + panic!("expected circle"); + }; + assert!((circle.center_wcs() - driven_center).length() < 1.0e-9); + } + } } diff --git a/src/snap.rs b/src/snap.rs index 28f8e0631..eee33903f 100644 --- a/src/snap.rs +++ b/src/snap.rs @@ -2765,131 +2765,202 @@ impl WirePlane { } fn wire_plane(wire: &WireModel) -> Option { - let [geom] = wire.tangent_geoms.as_slice() else { - return None; - }; - let (origin, axis_x, axis_y) = match geom { - TangentGeom::Circle { center, .. } => (DVec3::new(center[0] as f64, center[1] as f64, center[2] as f64), DVec3::X, DVec3::Y), - TangentGeom::PlanarCircle { center, axis_x, axis_y, .. } | TangentGeom::Arc { center, axis_x, axis_y, .. } => ( - DVec3::new(center[0], center[1], center[2]), - DVec3::new(axis_x[0], axis_x[1], axis_x[2]), - DVec3::new(axis_y[0], axis_y[1], axis_y[2]), - ), - TangentGeom::PlanarEllipse { center, major_axis, normal, .. } => { - let origin = DVec3::new(center[0], center[1], center[2]); - let n = DVec3::new(normal[0], normal[1], normal[2]).normalize(); - let major = DVec3::new(major_axis[0], major_axis[1], major_axis[2]); - if major.length() <= 1e-12 { - return None; + for geom in &wire.tangent_geoms { + let plane_axes = match geom { + TangentGeom::Circle { center, .. } => Some(( + DVec3::new(center[0] as f64, center[1] as f64, center[2] as f64), + DVec3::X, + DVec3::Y, + )), + TangentGeom::PlanarCircle { center, axis_x, axis_y, .. } + | TangentGeom::Arc { center, axis_x, axis_y, .. } => Some(( + DVec3::new(center[0], center[1], center[2]), + DVec3::new(axis_x[0], axis_x[1], axis_x[2]), + DVec3::new(axis_y[0], axis_y[1], axis_y[2]), + )), + TangentGeom::PlanarEllipse { center, major_axis, normal, .. } => { + let origin = DVec3::new(center[0], center[1], center[2]); + let n = DVec3::new(normal[0], normal[1], normal[2]).normalize(); + let major = DVec3::new(major_axis[0], major_axis[1], major_axis[2]); + if major.length() <= 1e-12 { + None + } else { + Some((origin, major.normalize(), n.cross(major.normalize()))) + } } - (origin, major.normalize(), n.cross(major.normalize())) + TangentGeom::Line { .. } => None, + }; + if let Some((origin, axis_x, axis_y)) = plane_axes { + return Some(WirePlane { origin, axis_x, axis_y, normal: axis_x.cross(axis_y).normalize() }); } - TangentGeom::Line { .. } => return None, - }; - Some(WirePlane { origin, axis_x, axis_y, normal: axis_x.cross(axis_y).normalize() }) + } + None } -fn curve_in_frame(wire: &WireModel, frame: &WirePlane, tol: f64) -> Option { +fn tangent_line_endpoints( + wire: &WireModel, + p1: [f32; 3], + p2: [f32; 3], +) -> (DVec3, DVec3) { + if wire.points.len() == 2 && wire.tangent_geoms.len() == 1 { + return (wp_f64(wire, 0), wp_f64(wire, 1)); + } + + let matches = |vertex: [f64; 3], point: [f32; 3]| { + (vertex[0] as f32 - point[0]).abs() < 1e-3 + && (vertex[1] as f32 - point[1]).abs() < 1e-3 + && (vertex[2] as f32 - point[2]).abs() < 1e-3 + }; + let count = wire.key_vertices.len(); + if count >= 2 { + for start in 0..count { + let end = (start + 1) % count; + if matches(wire.key_vertices[start], p1) && matches(wire.key_vertices[end], p2) { + return ( + DVec3::from_array(wire.key_vertices[start]), + DVec3::from_array(wire.key_vertices[end]), + ); + } + } + } + + ( + Vec3::from_array(p1).as_dvec3(), + Vec3::from_array(p2).as_dvec3(), + ) +} + +fn curves_in_frame(wire: &WireModel, frame: &WirePlane, tol: f64) -> Option> { use cadkernel::geom2d::{Arc as KArc, Circle as KCircle, Ellipse as KEllipse, EllipseArc as KEllipseArc, Line as KLine}; + let mut curves = Vec::new(); + if wire.tangent_geoms.is_empty() { if wire.points.len() != 2 { return None; } let (p0, p1) = (wp_f64(wire, 0), wp_f64(wire, 1)); - return (frame.contains(p0, tol) && frame.contains(p1, tol)).then(|| Curve::Line(KLine { start: frame.to_2d(p0), end: frame.to_2d(p1) })); + if !frame.contains(p0, tol) || !frame.contains(p1, tol) { + return None; + } + return Some(vec![Curve::Line(KLine { + start: frame.to_2d(p0), + end: frame.to_2d(p1), + })]); } - let [geom] = wire.tangent_geoms.as_slice() else { - return None; - }; let angle_in_frame = |world_point: DVec3, centre: DVec3| { let (p2, c2) = (frame.to_2d(world_point), frame.to_2d(centre)); (p2[1] - c2[1]).atan2(p2[0] - c2[0]) }; - match geom { - TangentGeom::Line { p1, p2 } => { - let (p1, p2) = if wire.points.len() == 2 { - (wp_f64(wire, 0), wp_f64(wire, 1)) - } else { - (Vec3::from_array(*p1).as_dvec3(), Vec3::from_array(*p2).as_dvec3()) - }; - (frame.contains(p1, tol) && frame.contains(p2, tol)).then(|| Curve::Line(KLine { start: frame.to_2d(p1), end: frame.to_2d(p2) })) - } - TangentGeom::Circle { center, radius } => { - let c = DVec3::new(center[0] as f64, center[1] as f64, center[2] as f64); - (DVec3::Z.cross(frame.normal).length() <= tol && frame.contains(c, tol)).then(|| Curve::Circle(KCircle { centre: frame.to_2d(c), radius: *radius as f64 })) - } - TangentGeom::PlanarCircle { center, axis_x, axis_y, radius } => { - let c = DVec3::new(center[0], center[1], center[2]); - let n = DVec3::new(axis_x[0], axis_x[1], axis_x[2]).cross(DVec3::new(axis_y[0], axis_y[1], axis_y[2])).normalize(); - (n.cross(frame.normal).length() <= tol && frame.contains(c, tol)).then(|| Curve::Circle(KCircle { centre: frame.to_2d(c), radius: *radius })) - } - TangentGeom::Arc { center, axis_x, axis_y, radius, start_angle, end_angle } => { - let c = DVec3::new(center[0], center[1], center[2]); - let (ax, ay) = (DVec3::new(axis_x[0], axis_x[1], axis_x[2]), DVec3::new(axis_y[0], axis_y[1], axis_y[2])); - let n = ax.cross(ay).normalize(); - if n.cross(frame.normal).length() > tol || !frame.contains(c, tol) { - return None; + for geom in &wire.tangent_geoms { + match geom { + TangentGeom::Line { p1, p2 } => { + let (p1, p2) = tangent_line_endpoints(wire, *p1, *p2); + if !frame.contains(p1, tol) || !frame.contains(p2, tol) { + return None; + } + curves.push(Curve::Line(KLine { start: frame.to_2d(p1), end: frame.to_2d(p2) })); } - let arc = KArc { centre: frame.to_2d(c), radius: *radius, start_angle: *start_angle, end_angle: *end_angle }; - let sweep = arc.sweep(); - if (sweep - std::f64::consts::TAU).abs() <= 1e-12 { - return Some(Curve::Circle(KCircle { centre: arc.centre, radius: *radius })); + TangentGeom::Circle { center, radius } => { + let c = DVec3::new(center[0] as f64, center[1] as f64, center[2] as f64); + if DVec3::Z.cross(frame.normal).length() > tol || !frame.contains(c, tol) { + return None; + } + curves.push(Curve::Circle(KCircle { centre: frame.to_2d(c), radius: *radius as f64 })); } - let boundary = if n.dot(frame.normal) < 0.0 { *end_angle } else { *start_angle }; - let start_angle = angle_in_frame(c + *radius * (boundary.cos() * ax + boundary.sin() * ay), c); - Some(Curve::Arc(KArc { start_angle, end_angle: start_angle + sweep, ..arc })) - } - - TangentGeom::PlanarEllipse { center, major_axis, normal, minor_axis_ratio, start_param, end_param } => { - let c = DVec3::new(center[0], center[1], center[2]); - let ell_normal = DVec3::new(normal[0], normal[1], normal[2]).normalize(); - if ell_normal.cross(frame.normal).length() > tol || !frame.contains(c, tol) { - return None; + TangentGeom::PlanarCircle { center, axis_x, axis_y, radius } => { + let c = DVec3::new(center[0], center[1], center[2]); + let n = DVec3::new(axis_x[0], axis_x[1], axis_x[2]).cross(DVec3::new(axis_y[0], axis_y[1], axis_y[2])).normalize(); + if n.cross(frame.normal).length() > tol || !frame.contains(c, tol) { + return None; + } + curves.push(Curve::Circle(KCircle { centre: frame.to_2d(c), radius: *radius })); } - let major = DVec3::new(major_axis[0], major_axis[1], major_axis[2]); - let major_radius = major.length(); - if major_radius <= 1e-12 { - return None; + TangentGeom::Arc { center, axis_x, axis_y, radius, start_angle, end_angle } => { + let c = DVec3::new(center[0], center[1], center[2]); + let (ax, ay) = (DVec3::new(axis_x[0], axis_x[1], axis_x[2]), DVec3::new(axis_y[0], axis_y[1], axis_y[2])); + let n = ax.cross(ay).normalize(); + if n.cross(frame.normal).length() > tol || !frame.contains(c, tol) { + return None; + } + let arc = KArc { centre: frame.to_2d(c), radius: *radius, start_angle: *start_angle, end_angle: *end_angle }; + let sweep = arc.sweep(); + if (sweep - std::f64::consts::TAU).abs() <= 1e-12 { + curves.push(Curve::Circle(KCircle { centre: arc.centre, radius: *radius })); + } else { + let boundary = if n.dot(frame.normal) < 0.0 { *end_angle } else { *start_angle }; + let start_angle = angle_in_frame(c + *radius * (boundary.cos() * ax + boundary.sin() * ay), c); + curves.push(Curve::Arc(KArc { start_angle, end_angle: start_angle + sweep, ..arc })); + } } - let minor_radius = major_radius * *minor_axis_ratio; - let dir2d = frame.dir_to_2d(major / major_radius); - let len = (dir2d[0] * dir2d[0] + dir2d[1] * dir2d[1]).sqrt(); - if len <= 1e-12 { - return None; + TangentGeom::PlanarEllipse { center, major_axis, normal, minor_axis_ratio, start_param, end_param } => { + let c = DVec3::new(center[0], center[1], center[2]); + let ell_normal = DVec3::new(normal[0], normal[1], normal[2]).normalize(); + if ell_normal.cross(frame.normal).length() > tol || !frame.contains(c, tol) { + return None; + } + let major = DVec3::new(major_axis[0], major_axis[1], major_axis[2]); + let major_radius = major.length(); + if major_radius <= 1e-12 { + return None; + } + let minor_radius = major_radius * *minor_axis_ratio; + let dir2d = frame.dir_to_2d(major / major_radius); + let len = (dir2d[0] * dir2d[0] + dir2d[1] * dir2d[1]).sqrt(); + if len <= 1e-12 { + return None; + } + let major_axis_2d = [dir2d[0] / len, dir2d[1] / len]; + let (start_parameter, end_parameter) = if ell_normal.dot(frame.normal) < 0.0 { + (-*end_param, -*start_param) + } else { + (*start_param, *end_param) + }; + curves.push(Curve::Ellipse(KEllipseArc { + ellipse: KEllipse { centre: frame.to_2d(c), major_radius, minor_radius, major_axis: major_axis_2d }, + start_parameter, + end_parameter, + })); } - let major_axis_2d = [dir2d[0] / len, dir2d[1] / len]; - let (start_parameter, end_parameter) = if ell_normal.dot(frame.normal) < 0.0 { - (-*end_param, -*start_param) - } else { - (*start_param, *end_param) - }; - Some(Curve::Ellipse(KEllipseArc { - ellipse: KEllipse { centre: frame.to_2d(c), major_radius, minor_radius, major_axis: major_axis_2d }, - start_parameter, - end_parameter, - })) } } + + Some(curves) } -fn exact_curve_intersections(wire_a: &WireModel, wire_b: &WireModel) -> Option> { +pub(crate) fn exact_curve_intersections( + wire_a: &WireModel, + wire_b: &WireModel, +) -> Option> { let frame = wire_plane(wire_a).or_else(|| wire_plane(wire_b))?; const PLANE_TOL: f64 = 1e-7; - let curve_a = curve_in_frame(wire_a, &frame, PLANE_TOL)?; - let curve_b = curve_in_frame(wire_b, &frame, PLANE_TOL)?; + let curves_a = curves_in_frame(wire_a, &frame, PLANE_TOL)?; + let curves_b = curves_in_frame(wire_b, &frame, PLANE_TOL)?; + // Two lines have nothing this path can improve on (the segment sweep is // already exact for a straight pair); avoid the extra work. - if matches!(curve_a, Curve::Line(_)) && matches!(curve_b, Curve::Line(_)) { + let a_all_lines = curves_a.iter().all(|c| matches!(c, Curve::Line(_))); + let b_all_lines = curves_b.iter().all(|c| matches!(c, Curve::Line(_))); + if a_all_lines && b_all_lines { return None; } let tolerance = cadkernel::geom2d::Tolerance::new(1e-9_f64.max(PLANE_TOL)); - let crossings = cadkernel::geom2d::intersect(&curve_a, &curve_b, tolerance); - let points: Vec = crossings.into_iter().map(|c| frame.to_3d(c.point)).collect(); + let mut points: Vec = Vec::new(); + for ca in &curves_a { + for cb in &curves_b { + let crossings = cadkernel::geom2d::intersect(ca, cb, tolerance); + for c in crossings { + let pt = frame.to_3d(c.point); + if !points.iter().any(|existing| existing.distance_squared(pt) <= 1e-12) { + points.push(pt); + } + } + } + } Some(points) } @@ -4182,4 +4253,68 @@ mod ext_tests { assert!((res.world.y - 0.5).abs() < 1e-9, "y rounds to 0.5: {:?}", res.world); } + #[test] + fn exact_curve_intersections_handles_multisegment_polyline_against_circle_at_vertex() { + let polyline = WireModel { + key_vertices: vec![[-10.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 10.0, 0.0]], + tangent_geoms: vec![ + TangentGeom::Line { p1: [-10.0, 0.0, 0.0], p2: [0.0, 0.0, 0.0] }, + TangentGeom::Line { p1: [0.0, 0.0, 0.0], p2: [0.0, 10.0, 0.0] }, + ], + ..Default::default() + }; + let circle = WireModel { + tangent_geoms: vec![TangentGeom::PlanarCircle { + center: [0.0, 5.0, 0.0], + axis_x: [1.0, 0.0, 0.0], + axis_y: [0.0, 1.0, 0.0], + radius: 5.0, + }], + ..Default::default() + }; + let pts = exact_curve_intersections(&polyline, &circle) + .expect("polyline must analytically intersect circle"); + assert_eq!(pts.len(), 2, "expected intersections at (0,0) and (0,10), got {pts:?}"); + let mut ys: Vec = pts.iter().map(|p| p.y).collect(); + ys.sort_by(f64::total_cmp); + assert!((ys[0] - 0.0).abs() < 1e-9, "expected corner intersection at y=0, got {}", ys[0]); + assert!((ys[1] - 10.0).abs() < 1e-9, "expected endpoint intersection at y=10, got {}", ys[1]); + for p in &pts { + assert!(p.x.abs() < 1e-9, "expected x=0, got {}", p.x); + assert!(p.z.abs() < 1e-9, "expected z=0, got {}", p.z); + } + } + + #[test] + fn exact_curve_intersections_handles_polyline_with_bulge_arc_against_circle() { + let polyline = WireModel { + tangent_geoms: vec![ + TangentGeom::Line { p1: [-10.0, 0.0, 0.0], p2: [0.0, 0.0, 0.0] }, + TangentGeom::Arc { + center: [0.0, 4.0, 0.0], + axis_x: [1.0, 0.0, 0.0], + axis_y: [0.0, 1.0, 0.0], + radius: 4.0, + start_angle: -std::f64::consts::FRAC_PI_2, + end_angle: std::f64::consts::FRAC_PI_2, + }, + ], + ..Default::default() + }; + let circle = WireModel { + tangent_geoms: vec![TangentGeom::PlanarCircle { + center: [0.0, 0.0, 0.0], + axis_x: [1.0, 0.0, 0.0], + axis_y: [0.0, 1.0, 0.0], + radius: 5.0, + }], + ..Default::default() + }; + let pts = exact_curve_intersections(&polyline, &circle) + .expect("polyline with arc must intersect circle"); + assert!(!pts.is_empty()); + let line_pt = pts.iter().find(|p| (p.x - -5.0).abs() < 1e-9 && p.y.abs() < 1e-9); + assert!(line_pt.is_some(), "expected line crossing at (-5, 0, 0), got {pts:?}"); + } + } diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index 09f4b3019..1aecaba9e 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -38,12 +38,36 @@ fn constraint_glyph_size(label: &str) -> Size { if is_compact_coincident_glyph(label) { return Size::new(COINCIDENT_GLYPH_SIZE, COINCIDENT_GLYPH_SIZE); } + if label == "G²" { + let side = CONSTRAINT_GLYPH_SIZE + CONSTRAINT_GLYPH_PAD_Y * 2.0; + return Size::new(side, side); + } let w = label.chars().count() as f32 * CONSTRAINT_GLYPH_SIZE * 0.62 + CONSTRAINT_GLYPH_PAD_X * 2.0; let h = CONSTRAINT_GLYPH_SIZE + CONSTRAINT_GLYPH_PAD_Y * 2.0; Size::new(w, h) } +fn draw_smooth_constraint_glyph(frame: &mut canvas::Frame, center: Point, color: Color) { + let curve = canvas::Path::new(|builder| { + for step in 0..=16 { + let x = -7.0 + step as f32 * 0.875; + let y = 0.012 * x * x * x; + let point = Point::new(center.x + x, center.y - y); + if step == 0 { + builder.move_to(point); + } else { + builder.line_to(point); + } + } + }); + frame.stroke( + &curve, + canvas::Stroke::default().with_color(color).with_width(1.45), + ); + frame.fill(&canvas::Path::circle(center, 1.35), color); +} + fn draw_tangent_constraint_glyph( frame: &mut canvas::Frame, center: Point, @@ -60,6 +84,16 @@ fn draw_tangent_constraint_glyph( frame.stroke(&canvas::Path::line(contact, tangent_end), stroke); } +fn draw_concentric_constraint_glyph( + frame: &mut canvas::Frame, + center: Point, + color: Color, +) { + let stroke = canvas::Stroke::default().with_color(color).with_width(1.25); + frame.stroke(&canvas::Path::circle(center, 4.7), stroke.clone()); + frame.stroke(&canvas::Path::circle(center, 2.15), stroke); +} + fn constraint_glyph_box( anchor: Point, outward: [f32; 2], @@ -831,6 +865,7 @@ pub fn selection_overlay<'a>( selection_visual: SelectionVisualOptions, constraint_glyphs: Vec<(Point, [f32; 2], String, bool, bool, Vec)>, constraint_glyph_tooltip: Option, + constraint_cursor_badge: Option, ) -> Element<'a, Message> { canvas(SelectionCanvas { selection, @@ -856,6 +891,7 @@ pub fn selection_overlay<'a>( selection_visual, constraint_glyphs, constraint_glyph_tooltip, + constraint_cursor_badge, }) .width(Length::Fill) .height(Length::Fill) @@ -920,6 +956,9 @@ struct SelectionCanvas { constraint_glyphs: Vec<(Point, [f32; 2], String, bool, bool, Vec)>, /// Localized kind name made visible after the app-level hover dwell. constraint_glyph_tooltip: Option, + /// Symbol shown beside the cursor while it targets an entity that already + /// participates in an enabled geometric constraint. + constraint_cursor_badge: Option, } fn draw_grip_marker( @@ -1818,6 +1857,28 @@ impl canvas::Program for SelectionCanvas { let hole = canvas::Path::circle(Point::new(bx + 6.0, by + 10.5), 1.4); frame.fill(&hole, dark); } + if let Some(label) = &self.constraint_cursor_badge { + let center = Point::new(cp.x + sq + if self.hover_locked { 30.0 } else { 12.0 }, cp.y - sq - 7.0); + let blue = Color::from_rgb8(35, 145, 230); + let badge = canvas::Path::circle(center, 8.5); + frame.fill(&badge, blue); + if label == "◎" { + draw_concentric_constraint_glyph(&mut frame, center, Color::WHITE); + } else if label == "T" { + draw_tangent_constraint_glyph(&mut frame, center, Color::WHITE); + } else { + frame.fill_text(canvas::Text { + content: label.clone(), + position: center, + color: Color::WHITE, + size: iced::Pixels(11.0), + align_x: iced::alignment::Horizontal::Center.into(), + align_y: iced::alignment::Vertical::Center, + shaping: iced::advanced::text::Shaping::Advanced, + ..Default::default() + }); + } + } } } // end !over_viewcube @@ -1905,8 +1966,8 @@ impl canvas::Program for SelectionCanvas { let hovered = cursor .position_in(bounds) .and_then(|point| constraint_glyph_hit_test(&glyphs_for_offsets, point)); - let normal_bg = theme.palette().primary.base.color; - let normal_fg = theme.palette().primary.base.text; + let normal_bg = Color::from_rgb8(103, 109, 118); + let normal_fg = Color::WHITE; // A redundant or conflicting constraint gets the danger palette // instead of the // ordinary primary one — same information a resolver panel @@ -1955,6 +2016,10 @@ impl canvas::Program for SelectionCanvas { ); if label == "T" { draw_tangent_constraint_glyph(&mut frame, glyph_center, fg); + } else if label == "G²" { + draw_smooth_constraint_glyph(&mut frame, glyph_center, fg); + } else if label == "◎" { + draw_concentric_constraint_glyph(&mut frame, glyph_center, fg); } else { frame.fill_text(canvas::Text { content: label.clone(), diff --git a/tests/fixtures/plot/README.md b/tests/fixtures/plot/README.md new file mode 100644 index 000000000..54d2f0873 --- /dev/null +++ b/tests/fixtures/plot/README.md @@ -0,0 +1,29 @@ +Page-setup fixtures saved by a commercial CAD application (2016 release, DXF +AC1027 and the matching DWG). Provided by Panos for the plotting parity track; +the drawing holds one 10 m line (`$INSUNITS` = 6, metres) and three paper +layouts, each with a viewport of that line and its dimension: + +| Layout | Device | Media (`4`) | `44`×`45` | `73` | `72` | `142`/`143` | `147` | `75` | +|---|---|---|---|---|---|---|---|---| +| `A4 1-100` | `DWG To PDF.pc3` | `ISO_A4_(297.00_x_210.00_MM)` | 297 × 210 | 0 | mm | 1 / 1 | 25.4 | 16 | +| `A3 - plotter` | system printer `HPB602DE (HP OfficeJet Pro 7740 series)` | `A3` | 297.01 × 419.99 | 1 | mm | 1 / 1 | 25.4 | 16 | +| `ARCH D - PLOTTER` | the application's second PDF plotter configuration (`… PDF (High Quality Print).pc3`) | `ARCH_D_(36.00_x_24.00_Inches)` | 914.4 × 609.6 | 0 | in | 1 / 25.4 | 0.03937 | 16 | + +What the file establishes (see `src/app/update/file.rs`, `plot_settings_from_dialog`): + +- A landscape sheet is written either as the driver's own landscape medium + with rotation 0 (the PDF driver lists both orientations) or as a portrait + medium with rotation 90 (system printers) — rotation is relative to the + medium's orientation, and the medium's exact driver dimensions are kept. +- Margins (`40`–`43`, mm) belong to the medium's own edges: the PDF driver's + A4 has 5.79375 left/right and 17.79375 bottom/top. Layout limits (`10`/`20`) + put the sheet at minus the *displayed* margins: at rotation 90 the medium's + top margin (20.02) becomes the layout's left one, its left margin (5.00) the + layout's bottom one. +- Plot flags `70` = 672 on every layout: draw viewports first, print + lineweights, plot with plot styles — the "use standard scale" bit stays clear + even with a standard code in `75`; the model tab (fit to paper) sets it. +- `147` is `142/143` × 25.4 on millimetre page setups and `142/143` itself on + inch ones. +- An inch page setup over a millimetre paper space stores "1:1" as + `1 in = 25.4 units` (`75` = 16 kept). diff --git a/tests/fixtures/plot/page-setups-metric.dwg b/tests/fixtures/plot/page-setups-metric.dwg new file mode 100644 index 000000000..2df54fb00 Binary files /dev/null and b/tests/fixtures/plot/page-setups-metric.dwg differ diff --git a/tests/fixtures/plot/page-setups-metric.dxf b/tests/fixtures/plot/page-setups-metric.dxf new file mode 100644 index 000000000..e730c81d6 --- /dev/null +++ b/tests/fixtures/plot/page-setups-metric.dxf @@ -0,0 +1,15882 @@ + 0 +SECTION + 2 +HEADER + 9 +$ACADVER + 1 +AC1027 + 9 +$ACADMAINTVER + 70 + 105 + 9 +$DWGCODEPAGE + 3 +ANSI_1253 + 9 +$LASTSAVEDBY + 1 +Administrator + 9 +$REQUIREDVERSIONS +160 + 0 + 9 +$INSBASE + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$EXTMIN + 10 +14.59833903654579 + 20 +10.82430074186357 + 30 +0.0 + 9 +$EXTMAX + 10 +24.5983390365458 + 20 +12.51140470044061 + 30 +0.0 + 9 +$LIMMIN + 10 +0.0 + 20 +0.0 + 9 +$LIMMAX + 10 +12.0 + 20 +9.0 + 9 +$ORTHOMODE + 70 + 0 + 9 +$REGENMODE + 70 + 1 + 9 +$FILLMODE + 70 + 1 + 9 +$QTEXTMODE + 70 + 0 + 9 +$MIRRTEXT + 70 + 0 + 9 +$LTSCALE + 40 +1.0 + 9 +$ATTMODE + 70 + 1 + 9 +$TEXTSIZE + 40 +0.2 + 9 +$TRACEWID + 40 +0.05 + 9 +$TEXTSTYLE + 7 +Standard + 9 +$CLAYER + 8 +0 + 9 +$CELTYPE + 6 +ByLayer + 9 +$CECOLOR + 62 + 256 + 9 +$CELTSCALE + 40 +1.0 + 9 +$DISPSILH + 70 + 0 + 9 +$DIMSCALE + 40 +1.0 + 9 +$DIMASZ + 40 +0.18 + 9 +$DIMEXO + 40 +0.0625 + 9 +$DIMDLI + 40 +0.38 + 9 +$DIMRND + 40 +0.0 + 9 +$DIMDLE + 40 +0.0 + 9 +$DIMEXE + 40 +0.18 + 9 +$DIMTP + 40 +0.0 + 9 +$DIMTM + 40 +0.0 + 9 +$DIMTXT + 40 +0.18 + 9 +$DIMCEN + 40 +0.09 + 9 +$DIMTSZ + 40 +0.0 + 9 +$DIMTOL + 70 + 0 + 9 +$DIMLIM + 70 + 0 + 9 +$DIMTIH + 70 + 1 + 9 +$DIMTOH + 70 + 1 + 9 +$DIMSE1 + 70 + 0 + 9 +$DIMSE2 + 70 + 0 + 9 +$DIMTAD + 70 + 0 + 9 +$DIMZIN + 70 + 0 + 9 +$DIMBLK + 1 + + 9 +$DIMASO + 70 + 1 + 9 +$DIMSHO + 70 + 1 + 9 +$DIMPOST + 1 + + 9 +$DIMAPOST + 1 + + 9 +$DIMALT + 70 + 0 + 9 +$DIMALTD + 70 + 2 + 9 +$DIMALTF + 40 +25.4 + 9 +$DIMLFAC + 40 +1.0 + 9 +$DIMTOFL + 70 + 0 + 9 +$DIMTVP + 40 +0.0 + 9 +$DIMTIX + 70 + 0 + 9 +$DIMSOXD + 70 + 0 + 9 +$DIMSAH + 70 + 0 + 9 +$DIMBLK1 + 1 + + 9 +$DIMBLK2 + 1 + + 9 +$DIMSTYLE + 2 +Standard + 9 +$DIMCLRD + 70 + 0 + 9 +$DIMCLRE + 70 + 0 + 9 +$DIMCLRT + 70 + 0 + 9 +$DIMTFAC + 40 +1.0 + 9 +$DIMGAP + 40 +0.09 + 9 +$DIMJUST + 70 + 0 + 9 +$DIMSD1 + 70 + 0 + 9 +$DIMSD2 + 70 + 0 + 9 +$DIMTOLJ + 70 + 1 + 9 +$DIMTZIN + 70 + 0 + 9 +$DIMALTZ + 70 + 0 + 9 +$DIMALTTZ + 70 + 0 + 9 +$DIMUPT + 70 + 0 + 9 +$DIMDEC + 70 + 3 + 9 +$DIMTDEC + 70 + 3 + 9 +$DIMALTU + 70 + 2 + 9 +$DIMALTTD + 70 + 2 + 9 +$DIMTXSTY + 7 +Standard + 9 +$DIMAUNIT + 70 + 0 + 9 +$DIMADEC + 70 + 0 + 9 +$DIMALTRND + 40 +0.0 + 9 +$DIMAZIN + 70 + 0 + 9 +$DIMDSEP + 70 + 46 + 9 +$DIMATFIT + 70 + 3 + 9 +$DIMFRAC + 70 + 0 + 9 +$DIMLDRBLK + 1 + + 9 +$DIMLUNIT + 70 + 2 + 9 +$DIMLWD + 70 + -2 + 9 +$DIMLWE + 70 + -2 + 9 +$DIMTMOVE + 70 + 0 + 9 +$DIMFXL + 40 +1.0 + 9 +$DIMFXLON + 70 + 0 + 9 +$DIMJOGANG + 40 +0.7853981633974483 + 9 +$DIMTFILL + 70 + 0 + 9 +$DIMTFILLCLR + 70 + 0 + 9 +$DIMARCSYM + 70 + 0 + 9 +$DIMLTYPE + 6 + + 9 +$DIMLTEX1 + 6 + + 9 +$DIMLTEX2 + 6 + + 9 +$DIMTXTDIRECTION + 70 + 0 + 9 +$LUNITS + 70 + 2 + 9 +$LUPREC + 70 + 3 + 9 +$SKETCHINC + 40 +0.1 + 9 +$FILLETRAD + 40 +0.0 + 9 +$AUNITS + 70 + 0 + 9 +$AUPREC + 70 + 0 + 9 +$MENU + 1 +. + 9 +$ELEVATION + 40 +0.0 + 9 +$PELEVATION + 40 +0.0 + 9 +$THICKNESS + 40 +0.0 + 9 +$LIMCHECK + 70 + 0 + 9 +$CHAMFERA + 40 +0.0 + 9 +$CHAMFERB + 40 +0.0 + 9 +$CHAMFERC + 40 +0.0 + 9 +$CHAMFERD + 40 +0.0 + 9 +$SKPOLY + 70 + 0 + 9 +$TDCREATE + 40 +2461301.715020046 + 9 +$TDUCREATE + 40 +2461301.590020046 + 9 +$TDUPDATE + 40 +2461301.731632813 + 9 +$TDUUPDATE + 40 +2461301.606632813 + 9 +$TDINDWG + 40 +0.0166130903 + 9 +$TDUSRTIMER + 40 +0.0166130440 + 9 +$USRTIMER + 70 + 1 + 9 +$ANGBASE + 50 +0.0 + 9 +$ANGDIR + 70 + 0 + 9 +$PDMODE + 70 + 0 + 9 +$PDSIZE + 40 +0.0 + 9 +$PLINEWID + 40 +0.0 + 9 +$SPLFRAME + 70 + 0 + 9 +$SPLINETYPE + 70 + 6 + 9 +$SPLINESEGS + 70 + 8 + 9 +$HANDSEED + 5 +35D + 9 +$SURFTAB1 + 70 + 6 + 9 +$SURFTAB2 + 70 + 6 + 9 +$SURFTYPE + 70 + 6 + 9 +$SURFU + 70 + 6 + 9 +$SURFV + 70 + 6 + 9 +$UCSBASE + 2 + + 9 +$UCSNAME + 2 + + 9 +$UCSORG + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$UCSXDIR + 10 +1.0 + 20 +0.0 + 30 +0.0 + 9 +$UCSYDIR + 10 +0.0 + 20 +1.0 + 30 +0.0 + 9 +$UCSORTHOREF + 2 + + 9 +$UCSORTHOVIEW + 70 + 0 + 9 +$UCSORGTOP + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$UCSORGBOTTOM + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$UCSORGLEFT + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$UCSORGRIGHT + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$UCSORGFRONT + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$UCSORGBACK + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$PUCSBASE + 2 + + 9 +$PUCSNAME + 2 + + 9 +$PUCSORG + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$PUCSXDIR + 10 +1.0 + 20 +0.0 + 30 +0.0 + 9 +$PUCSYDIR + 10 +0.0 + 20 +1.0 + 30 +0.0 + 9 +$PUCSORTHOREF + 2 + + 9 +$PUCSORTHOVIEW + 70 + 0 + 9 +$PUCSORGTOP + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$PUCSORGBOTTOM + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$PUCSORGLEFT + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$PUCSORGRIGHT + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$PUCSORGFRONT + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$PUCSORGBACK + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$USERI1 + 70 + 0 + 9 +$USERI2 + 70 + 0 + 9 +$USERI3 + 70 + 0 + 9 +$USERI4 + 70 + 0 + 9 +$USERI5 + 70 + 0 + 9 +$USERR1 + 40 +0.0 + 9 +$USERR2 + 40 +0.0 + 9 +$USERR3 + 40 +0.0 + 9 +$USERR4 + 40 +0.0 + 9 +$USERR5 + 40 +0.0 + 9 +$WORLDVIEW + 70 + 1 + 9 +$SHADEDGE + 70 + 3 + 9 +$SHADEDIF + 70 + 70 + 9 +$TILEMODE + 70 + 1 + 9 +$MAXACTVP + 70 + 64 + 9 +$PINSBASE + 10 +0.0 + 20 +0.0 + 30 +0.0 + 9 +$PLIMCHECK + 70 + 0 + 9 +$PEXTMIN + 10 +1.049999952316284 + 20 +0.7999999523162842 + 30 +0.0 + 9 +$PEXTMAX + 10 +239.9322544340976 + 20 +168.782338224235 + 30 +0.0 + 9 +$PLIMMIN + 10 +-5.793749809265136 + 20 +-17.79375076293945 + 9 +$PLIMMAX + 10 +291.2062501907349 + 20 +192.2062492370605 + 9 +$UNITMODE + 70 + 0 + 9 +$VISRETAIN + 70 + 1 + 9 +$PLINEGEN + 70 + 0 + 9 +$PSLTSCALE + 70 + 1 + 9 +$TREEDEPTH + 70 + 3020 + 9 +$CMLSTYLE + 2 +Standard + 9 +$CMLJUST + 70 + 0 + 9 +$CMLSCALE + 40 +1.0 + 9 +$PROXYGRAPHICS + 70 + 1 + 9 +$MEASUREMENT + 70 + 0 + 9 +$CELWEIGHT +370 + -1 + 9 +$ENDCAPS +280 + 0 + 9 +$JOINSTYLE +280 + 0 + 9 +$LWDISPLAY +290 + 0 + 9 +$INSUNITS + 70 + 6 + 9 +$HYPERLINKBASE + 1 + + 9 +$STYLESHEET + 1 + + 9 +$XEDIT +290 + 1 + 9 +$CEPSNTYPE +380 + 0 + 9 +$PSTYLEMODE +290 + 1 + 9 +$FINGERPRINTGUID + 2 +{F3A6E6ED-1342-47C7-994D-2309A1CA9EF6} + 9 +$VERSIONGUID + 2 +{99446C9A-1E85-4B59-9216-C450698EB011} + 9 +$EXTNAMES +290 + 1 + 9 +$PSVPSCALE + 40 +0.0 + 9 +$OLESTARTUP +290 + 0 + 9 +$SORTENTS +280 + 127 + 9 +$INDEXCTL +280 + 0 + 9 +$HIDETEXT +280 + 1 + 9 +$XCLIPFRAME +280 + 2 + 9 +$HALOGAP +280 + 0 + 9 +$OBSCOLOR + 70 + 257 + 9 +$OBSLTYPE +280 + 0 + 9 +$INTERSECTIONDISPLAY +280 + 0 + 9 +$INTERSECTIONCOLOR + 70 + 257 + 9 +$DIMASSOC +280 + 2 + 9 +$PROJECTNAME + 1 + + 9 +$CAMERADISPLAY +290 + 0 + 9 +$LENSLENGTH + 40 +50.0 + 9 +$CAMERAHEIGHT + 40 +0.0 + 9 +$STEPSPERSEC + 40 +2.0 + 9 +$STEPSIZE + 40 +6.0 + 9 +$3DDWFPREC + 40 +2.0 + 9 +$PSOLWIDTH + 40 +0.25 + 9 +$PSOLHEIGHT + 40 +4.0 + 9 +$LOFTANG1 + 40 +1.570796326794896 + 9 +$LOFTANG2 + 40 +1.570796326794896 + 9 +$LOFTMAG1 + 40 +0.0 + 9 +$LOFTMAG2 + 40 +0.0 + 9 +$LOFTPARAM + 70 + 7 + 9 +$LOFTNORMALS +280 + 1 + 9 +$LATITUDE + 40 +37.795 + 9 +$LONGITUDE + 40 +-122.394 + 9 +$NORTHDIRECTION + 40 +0.0 + 9 +$TIMEZONE + 70 + -8000 + 9 +$LIGHTGLYPHDISPLAY +280 + 1 + 9 +$TILEMODELIGHTSYNCH +280 + 1 + 9 +$CMATERIAL +347 +96 + 9 +$SOLIDHIST +280 + 0 + 9 +$SHOWHIST +280 + 1 + 9 +$DWFFRAME +280 + 2 + 9 +$DGNFRAME +280 + 0 + 9 +$REALWORLDSCALE +290 + 1 + 9 +$INTERFERECOLOR + 62 + 1 + 9 +$INTERFEREOBJVS +345 +A3 + 9 +$INTERFEREVPVS +346 +A0 + 9 +$CSHADOW +280 + 0 + 9 +$SHADOWPLANELOCATION + 40 +0.0 + 0 +ENDSEC + 0 +SECTION + 2 +CLASSES + 0 +CLASS + 1 +ACDBDICTIONARYWDFLT + 2 +AcDbDictionaryWithDefault + 3 +ObjectDBX Classes + 90 + 0 + 91 + 1 +280 + 0 +281 + 0 + 0 +CLASS + 1 +DICTIONARYVAR + 2 +AcDbDictionaryVar + 3 +ObjectDBX Classes + 90 + 0 + 91 + 9 +280 + 0 +281 + 0 + 0 +CLASS + 1 +TABLESTYLE + 2 +AcDbTableStyle + 3 +ObjectDBX Classes + 90 + 4095 + 91 + 1 +280 + 0 +281 + 0 + 0 +CLASS + 1 +MATERIAL + 2 +AcDbMaterial + 3 +ObjectDBX Classes + 90 + 1153 + 91 + 3 +280 + 0 +281 + 0 + 0 +CLASS + 1 +VISUALSTYLE + 2 +AcDbVisualStyle + 3 +ObjectDBX Classes + 90 + 4095 + 91 + 24 +280 + 0 +281 + 0 + 0 +CLASS + 1 +SCALE + 2 +AcDbScale + 3 +ObjectDBX Classes + 90 + 1153 + 91 + 33 +280 + 0 +281 + 0 + 0 +CLASS + 1 +MLEADERSTYLE + 2 +AcDbMLeaderStyle + 3 +ACDB_MLEADERSTYLE_CLASS + 90 + 4095 + 91 + 2 +280 + 0 +281 + 0 + 0 +CLASS + 1 +CELLSTYLEMAP + 2 +AcDbCellStyleMap + 3 +ObjectDBX Classes + 90 + 1152 + 91 + 1 +280 + 0 +281 + 0 + 0 +CLASS + 1 +EXACXREFPANELOBJECT + 2 +ExAcXREFPanelObject + 3 +EXAC_ESW + 90 + 1025 + 91 + 0 +280 + 0 +281 + 0 + 0 +CLASS + 1 +NPOCOLLECTION + 2 +AcDbImpNonPersistentObjectsCollection + 3 +ObjectDBX Classes + 90 + 1153 + 91 + 0 +280 + 0 +281 + 0 + 0 +CLASS + 1 +LAYER_INDEX + 2 +AcDbLayerIndex + 3 +ObjectDBX Classes + 90 + 0 + 91 + 0 +280 + 0 +281 + 0 + 0 +CLASS + 1 +SPATIAL_INDEX + 2 +AcDbSpatialIndex + 3 +ObjectDBX Classes + 90 + 0 + 91 + 0 +280 + 0 +281 + 0 + 0 +CLASS + 1 +IDBUFFER + 2 +AcDbIdBuffer + 3 +ObjectDBX Classes + 90 + 0 + 91 + 0 +280 + 0 +281 + 0 + 0 +CLASS + 1 +ACDBSECTIONVIEWSTYLE + 2 +AcDbSectionViewStyle + 3 +ObjectDBX Classes + 90 + 1025 + 91 + 1 +280 + 0 +281 + 0 + 0 +CLASS + 1 +ACDBDETAILVIEWSTYLE + 2 +AcDbDetailViewStyle + 3 +ObjectDBX Classes + 90 + 1025 + 91 + 1 +280 + 0 +281 + 0 + 0 +CLASS + 1 +DIMASSOC + 2 +AcDbDimAssoc + 3 +"AcDbDimAssoc|Product Desc: AcDim ARX App For Dimension|Company: Autodesk, Inc.|WEB Address: www.autodesk.com" + 90 + 0 + 91 + 3 +280 + 0 +281 + 0 + 0 +CLASS + 1 +RAPIDRTRENDERSETTINGS + 2 +AcDbRapidRTRenderSettings + 3 +ObjectDBX Classes + 90 + 1024 + 91 + 6 +280 + 0 +281 + 0 + 0 +ENDSEC + 0 +SECTION + 2 +TABLES + 0 +TABLE + 2 +VPORT + 5 +8 +330 +0 +100 +AcDbSymbolTable + 70 + 1 + 0 +VPORT + 5 +94 +330 +8 +100 +AcDbSymbolTableRecord +100 +AcDbViewportTableRecord + 2 +*Active + 70 + 0 + 10 +0.0 + 20 +0.0 + 11 +1.0 + 21 +1.0 + 12 +43.9091672749249 + 22 +12.33162421725632 + 13 +0.0 + 23 +0.0 + 14 +0.5 + 24 +0.5 + 15 +0.5 + 25 +0.5 + 16 +0.0 + 26 +0.0 + 36 +1.0 + 17 +0.0 + 27 +0.0 + 37 +0.0 + 40 +100.9001375182816 + 41 +2.491869918699186 + 42 +50.0 + 43 +0.0 + 44 +0.0 + 50 +0.0 + 51 +0.0 + 71 + 0 + 72 + 1000 + 73 + 1 + 74 + 3 + 75 + 0 + 76 + 1 + 77 + 0 + 78 + 0 +281 + 0 + 65 + 1 +110 +0.0 +120 +0.0 +130 +0.0 +111 +1.0 +121 +0.0 +131 +0.0 +112 +0.0 +122 +1.0 +132 +0.0 + 79 + 0 +146 +0.0 +348 +9F + 60 + 3 + 61 + 5 +292 + 1 +282 + 1 +141 +0.0 +142 +0.0 + 63 + 250 +421 + 3355443 +1001 +ACAD_NAV_VCDISPLAY +1070 + 3 + 0 +ENDTAB + 0 +TABLE + 2 +LTYPE + 5 +5 +330 +0 +100 +AcDbSymbolTable + 70 + 1 + 0 +LTYPE + 5 +14 +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbLinetypeTableRecord + 2 +ByBlock + 70 + 0 + 3 + + 72 + 65 + 73 + 0 + 40 +0.0 + 0 +LTYPE + 5 +15 +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbLinetypeTableRecord + 2 +ByLayer + 70 + 0 + 3 + + 72 + 65 + 73 + 0 + 40 +0.0 + 0 +LTYPE + 5 +16 +330 +5 +100 +AcDbSymbolTableRecord +100 +AcDbLinetypeTableRecord + 2 +Continuous + 70 + 0 + 3 +Solid line + 72 + 65 + 73 + 0 + 40 +0.0 + 0 +ENDTAB + 0 +TABLE + 2 +LAYER + 5 +2 +102 +{ACAD_XDICTIONARY +360 +18E +102 +} +330 +0 +100 +AcDbSymbolTable + 70 + 2 + 0 +LAYER + 5 +10 +102 +{ACAD_XDICTIONARY +360 +E6 +102 +} +330 +2 +100 +AcDbSymbolTableRecord +100 +AcDbLayerTableRecord + 2 +0 + 70 + 0 + 62 + 7 + 6 +Continuous +370 + -3 +390 +F +347 +98 +348 +0 + 0 +LAYER + 5 +1EC +330 +2 +100 +AcDbSymbolTableRecord +100 +AcDbLayerTableRecord + 2 +Defpoints + 70 + 0 + 62 + 7 + 6 +Continuous +290 + 0 +370 + -3 +390 +F +347 +98 +348 +0 + 0 +ENDTAB + 0 +TABLE + 2 +STYLE + 5 +3 +330 +0 +100 +AcDbSymbolTable + 70 + 7 + 0 +STYLE + 5 +11 +330 +3 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +Standard + 70 + 0 + 40 +0.0 + 41 +1.0 + 50 +0.0 + 71 + 0 + 42 +0.2 + 3 +arial.ttf + 4 + +1001 +ACAD +1000 +Arial +1071 + 34 + 0 +STYLE + 5 +DC +330 +3 +100 +AcDbSymbolTableRecord +100 +AcDbTextStyleTableRecord + 2 +Annotative + 70 + 0 + 40 +0.0 + 41 +1.0 + 50 +0.0 + 71 + 0 + 42 +0.2 + 3 +arial.ttf + 4 + +1001 +AcadAnnotative +1000 +AnnotativeData +1002 +{ +1070 + 1 +1070 + 1 +1002 +} +1001 +ACAD +1000 +Arial +1071 + 34 + 0 +ENDTAB + 0 +TABLE + 2 +VIEW + 5 +6 +330 +0 +100 +AcDbSymbolTable + 70 + 13 + 0 +ENDTAB + 0 +TABLE + 2 +UCS + 5 +7 +330 +0 +100 +AcDbSymbolTable + 70 + 0 + 0 +ENDTAB + 0 +TABLE + 2 +APPID + 5 +9 +330 +0 +100 +AcDbSymbolTable + 70 + 8 + 0 +APPID + 5 +12 +330 +9 +100 +AcDbSymbolTableRecord +100 +AcDbRegAppTableRecord + 2 +ACAD + 70 + 0 + 0 +APPID + 5 +DD +330 +9 +100 +AcDbSymbolTableRecord +100 +AcDbRegAppTableRecord + 2 +AcadAnnoPO + 70 + 0 + 0 +APPID + 5 +DE +330 +9 +100 +AcDbSymbolTableRecord +100 +AcDbRegAppTableRecord + 2 +AcadAnnotative + 70 + 0 + 0 +APPID + 5 +DF +330 +9 +100 +AcDbSymbolTableRecord +100 +AcDbRegAppTableRecord + 2 +ACAD_DSTYLE_DIMJAG + 70 + 0 + 0 +APPID + 5 +E0 +330 +9 +100 +AcDbSymbolTableRecord +100 +AcDbRegAppTableRecord + 2 +ACAD_DSTYLE_DIMTALN + 70 + 0 + 0 +APPID + 5 +107 +330 +9 +100 +AcDbSymbolTableRecord +100 +AcDbRegAppTableRecord + 2 +ACAD_MLEADERVER + 70 + 0 + 0 +APPID + 5 +1A6 +330 +9 +100 +AcDbSymbolTableRecord +100 +AcDbRegAppTableRecord + 2 +ACAD_NAV_VCDISPLAY + 70 + 0 + 0 +APPID + 5 +28D +330 +9 +100 +AcDbSymbolTableRecord +100 +AcDbRegAppTableRecord + 2 +ACAD_PSEXT + 70 + 0 + 0 +ENDTAB + 0 +TABLE + 2 +DIMSTYLE + 5 +A +330 +0 +100 +AcDbSymbolTable + 70 + 7 +100 +AcDbDimStyleTable + 71 + 2 +340 +27 +340 +E1 + 0 +DIMSTYLE +105 +27 +102 +{ACAD_REACTORS +330 +22B +102 +} +330 +A +100 +AcDbSymbolTableRecord +100 +AcDbDimStyleTableRecord + 2 +Standard + 70 + 0 +271 + 3 +272 + 3 +340 +11 +1001 +ACAD_DSTYLE_DIMJAG +1070 + 388 +1040 +1.5 +1001 +ACAD_DSTYLE_DIMTALN +1070 + 392 +1070 + 0 + 0 +DIMSTYLE +105 +E1 +330 +A +100 +AcDbSymbolTableRecord +100 +AcDbDimStyleTableRecord + 2 +Annotative + 70 + 0 + 40 +0.0 +340 +11 +1001 +AcadAnnotative +1000 +AnnotativeData +1002 +{ +1070 + 1 +1070 + 1 +1002 +} +1001 +ACAD_DSTYLE_DIMJAG +1070 + 388 +1040 +1.5 +1001 +ACAD_DSTYLE_DIMTALN +1070 + 392 +1070 + 0 + 0 +ENDTAB + 0 +TABLE + 2 +BLOCK_RECORD + 5 +1 +330 +0 +100 +AcDbSymbolTable + 70 + 11 + 0 +BLOCK_RECORD + 5 +1F +102 +{ACAD_XDICTIONARY +360 +15D +102 +} +330 +1 +100 +AcDbSymbolTableRecord +100 +AcDbBlockTableRecord + 2 +*Model_Space +340 +22 + 70 + 0 +280 + 1 +281 + 0 + 0 +BLOCK_RECORD + 5 +58 +330 +1 +100 +AcDbSymbolTableRecord +100 +AcDbBlockTableRecord + 2 +*Paper_Space +340 +59 + 70 + 0 +280 + 1 +281 + 0 + 0 +BLOCK_RECORD + 5 +2E8 +330 +1 +100 +AcDbSymbolTableRecord +100 +AcDbBlockTableRecord + 2 +*Paper_Space0 +340 +2F1 + 70 + 0 +280 + 1 +281 + 0 + 0 +BLOCK_RECORD + 5 +1ED +330 +1 +100 +AcDbSymbolTableRecord +100 +AcDbBlockTableRecord + 2 +*D1 +340 +0 + 70 + 0 +280 + 1 +281 + 0 + 0 +BLOCK_RECORD + 5 +207 +330 +1 +100 +AcDbSymbolTableRecord +100 +AcDbBlockTableRecord + 2 +*D2 +340 +0 + 70 + 0 +280 + 1 +281 + 0 + 0 +BLOCK_RECORD + 5 +22C +330 +1 +100 +AcDbSymbolTableRecord +100 +AcDbBlockTableRecord + 2 +*D3 +340 +0 + 70 + 0 +280 + 1 +281 + 0 + 0 +BLOCK_RECORD + 5 +2C3 +330 +1 +100 +AcDbSymbolTableRecord +100 +AcDbBlockTableRecord + 2 +*Paper_Space9 +340 +2CC + 70 + 0 +280 + 1 +281 + 0 + 0 +ENDTAB + 0 +ENDSEC + 0 +SECTION + 2 +BLOCKS + 0 +BLOCK + 5 +20 +330 +1F +100 +AcDbEntity + 8 +0 +100 +AcDbBlockBegin + 2 +*Model_Space + 70 + 0 + 10 +0.0 + 20 +0.0 + 30 +0.0 + 3 +*Model_Space + 1 + + 0 +ENDBLK + 5 +21 +330 +1F +100 +AcDbEntity + 8 +0 +100 +AcDbBlockEnd + 0 +BLOCK + 5 +5A +330 +58 +100 +AcDbEntity + 67 + 1 + 8 +0 +100 +AcDbBlockBegin + 2 +*Paper_Space + 70 + 0 + 10 +0.0 + 20 +0.0 + 30 +0.0 + 3 +*Paper_Space + 1 + + 0 +ENDBLK + 5 +5B +330 +58 +100 +AcDbEntity + 67 + 1 + 8 +0 +100 +AcDbBlockEnd + 0 +BLOCK + 5 +2E9 +330 +2E8 +100 +AcDbEntity + 67 + 1 + 8 +0 +100 +AcDbBlockBegin + 2 +*Paper_Space0 + 70 + 0 + 10 +0.0 + 20 +0.0 + 30 +0.0 + 3 +*Paper_Space0 + 1 + + 0 +VIEWPORT + 5 +2EA +102 +{ACAD_XDICTIONARY +360 +2EB +102 +} +330 +2E8 +100 +AcDbEntity + 67 + 1 + 8 +0 +100 +AcDbViewport + 10 +451.4062623977661 + 20 +287.0062370300293 + 30 +0.0 + 40 +1607.148384415293 + 41 +644.9567741699219 + 68 + 1 + 69 + 1 + 12 +451.4062623977661 + 22 +287.0062370300293 + 13 +0.0 + 23 +0.0 + 14 +10.0 + 24 +10.0 + 15 +10.0 + 25 +10.0 + 16 +0.0 + 26 +0.0 + 36 +1.0 + 17 +0.0 + 27 +0.0 + 37 +0.0 + 42 +50.0 + 43 +0.0 + 44 +0.0 + 45 +644.9567741699219 + 50 +0.0 + 51 +0.0 + 72 + 1000 + 90 + 819232 + 1 + +281 + 0 + 71 + 1 + 74 + 0 +110 +0.0 +120 +0.0 +130 +0.0 +111 +1.0 +121 +0.0 +131 +0.0 +112 +0.0 +122 +1.0 +132 +0.0 + 79 + 0 +146 +0.0 +170 + 0 + 61 + 5 +348 +9F +292 + 1 +282 + 1 +141 +0.0 +142 +0.0 + 63 + 250 +421 + 3355443 + 0 +VIEWPORT + 5 +2ED +102 +{ACAD_XDICTIONARY +360 +2EE +102 +} +330 +2E8 +100 +AcDbEntity + 67 + 1 + 8 +0 +100 +AcDbViewport + 10 +449.6130217068744 + 20 +286.5965321160482 + 30 +0.0 + 40 +897.1260435091162 + 41 +571.5930643274638 + 68 + 2 + 69 + 2 + 12 +-3905.922247134887 + 22 +-2994.001929285971 + 13 +0.0 + 23 +0.0 + 14 +0.5 + 24 +0.5 + 15 +0.5 + 25 +0.5 + 16 +0.0 + 26 +0.0 + 36 +44111.74567509561 + 17 +3925.520858223038 + 27 +3005.669898567031 + 37 +0.0 + 42 +50.0 + 43 +0.0 + 44 +0.0 + 45 +6.409292904431198 + 50 +0.0 + 51 +0.0 + 72 + 1000 + 90 + 819296 + 1 + +281 + 0 + 71 + 1 + 74 + 0 +110 +0.0 +120 +0.0 +130 +0.0 +111 +1.0 +121 +0.0 +131 +0.0 +112 +0.0 +122 +1.0 +132 +0.0 + 79 + 0 +146 +0.0 +170 + 0 + 61 + 5 +348 +9F +292 + 1 +282 + 1 +141 +0.0 +142 +0.0 + 63 + 250 +421 + 3355443 + 0 +ENDBLK + 5 +2F0 +330 +2E8 +100 +AcDbEntity + 67 + 1 + 8 +0 +100 +AcDbBlockEnd + 0 +BLOCK + 5 +1EE +330 +1ED +100 +AcDbEntity + 8 +0 +100 +AcDbBlockBegin + 2 +*D1 + 70 + 1 + 10 +0.0 + 20 +0.0 + 30 +0.0 + 3 +*D1 + 1 + + 0 +LINE + 5 +1FB +330 +1ED +100 +AcDbEntity + 8 +0 + 62 + 0 +440 + 16777216 +370 + -2 +100 +AcDbLine + 10 +14.25447586915491 + 20 +11.64902104705702 + 30 +0.0 + 11 +14.25447586915491 + 21 +14.11493427750241 + 31 +0.0 + 0 +LINE + 5 +1FC +330 +1ED +100 +AcDbEntity + 8 +0 + 62 + 0 +440 + 16777216 +370 + -2 +100 +AcDbLine + 10 +24.25447586915492 + 20 +11.64902104705702 + 30 +0.0 + 11 +24.25447586915492 + 21 +14.11493427750241 + 31 +0.0 + 0 +LINE + 5 +1FD +330 +1ED +100 +AcDbEntity + 8 +0 + 62 + 0 +440 + 16777216 +370 + -2 +100 +AcDbLine + 10 +14.43447586915491 + 20 +13.93493427750241 + 30 +0.0 + 11 +18.72970097147415 + 21 +13.93493427750241 + 31 +0.0 + 0 +LINE + 5 +1FE +330 +1ED +100 +AcDbEntity + 8 +0 + 62 + 0 +440 + 16777216 +370 + -2 +100 +AcDbLine + 10 +24.07447586915492 + 20 +13.93493427750241 + 30 +0.0 + 11 +19.77925076683568 + 21 +13.93493427750241 + 31 +0.0 + 0 +SOLID + 5 +1FF +330 +1ED +100 +AcDbEntity + 8 +0 + 62 + 0 +440 + 16777216 +100 +AcDbTrace + 10 +14.43447586915491 + 20 +13.96493427750241 + 30 +0.0 + 11 +14.43447586915491 + 21 +13.90493427750241 + 31 +0.0 + 12 +14.25447586915491 + 22 +13.93493427750241 + 32 +0.0 + 13 +14.25447586915491 + 23 +13.93493427750241 + 33 +0.0 + 0 +SOLID + 5 +200 +330 +1ED +100 +AcDbEntity + 8 +0 + 62 + 0 +440 + 16777216 +100 +AcDbTrace + 10 +24.07447586915492 + 20 +13.96493427750241 + 30 +0.0 + 11 +24.07447586915492 + 21 +13.90493427750241 + 31 +0.0 + 12 +24.25447586915492 + 22 +13.93493427750241 + 32 +0.0 + 13 +24.25447586915492 + 23 +13.93493427750241 + 33 +0.0 + 0 +MTEXT + 5 +201 +330 +1ED +100 +AcDbEntity + 8 +0 + 62 + 0 +440 + 16777216 +100 +AcDbMText + 10 +19.25447586915491 + 20 +13.93493427750241 + 30 +0.0 + 40 +0.18 + 41 +0.0 + 46 +0.0 + 71 + 5 + 72 + 1 + 1 +\A1;10.0000 + 73 + 1 + 44 +1.0 + 0 +POINT + 5 +202 +330 +1ED +100 +AcDbEntity + 8 +Defpoints + 62 + 0 +440 + 16777216 +100 +AcDbPoint + 10 +14.25447586915491 + 20 +11.58652104705702 + 30 +0.0 + 0 +POINT + 5 +203 +330 +1ED +100 +AcDbEntity + 8 +Defpoints + 62 + 0 +440 + 16777216 +100 +AcDbPoint + 10 +24.25447586915492 + 20 +11.58652104705702 + 30 +0.0 + 0 +POINT + 5 +204 +330 +1ED +100 +AcDbEntity + 8 +Defpoints + 62 + 0 +440 + 16777216 +100 +AcDbPoint + 10 +24.25447586915492 + 20 +13.93493427750241 + 30 +0.0 + 0 +ENDBLK + 5 +1EF +330 +1ED +100 +AcDbEntity + 8 +0 +100 +AcDbBlockEnd + 0 +BLOCK + 5 +208 +330 +207 +100 +AcDbEntity + 8 +0 +100 +AcDbBlockBegin + 2 +*D2 + 70 + 1 + 10 +0.0 + 20 +0.0 + 30 +0.0 + 3 +*D2 + 1 + + 0 +LINE + 5 +220 +330 +207 +100 +AcDbEntity + 8 +0 + 62 + 0 +440 + 16777216 +370 + -2 +100 +AcDbLine + 10 +14.25447586915491 + 20 +11.64902104705702 + 30 +0.0 + 11 +14.25447586915491 + 21 +12.71928111055961 + 31 +0.0 + 0 +LINE + 5 +221 +330 +207 +100 +AcDbEntity + 8 +0 + 62 + 0 +440 + 16777216 +370 + -2 +100 +AcDbLine + 10 +24.25447586915492 + 20 +11.64902104705702 + 30 +0.0 + 11 +24.25447586915492 + 21 +12.71928111055961 + 31 +0.0 + 0 +LINE + 5 +222 +330 +207 +100 +AcDbEntity + 8 +0 + 62 + 0 +440 + 16777216 +370 + -2 +100 +AcDbLine + 10 +14.43447586915491 + 20 +12.53928111055961 + 30 +0.0 + 11 +18.72970097147415 + 21 +12.53928111055961 + 31 +0.0 + 0 +LINE + 5 +223 +330 +207 +100 +AcDbEntity + 8 +0 + 62 + 0 +440 + 16777216 +370 + -2 +100 +AcDbLine + 10 +24.07447586915492 + 20 +12.53928111055961 + 30 +0.0 + 11 +19.77925076683568 + 21 +12.53928111055961 + 31 +0.0 + 0 +SOLID + 5 +224 +330 +207 +100 +AcDbEntity + 8 +0 + 62 + 0 +440 + 16777216 +100 +AcDbTrace + 10 +14.43447586915491 + 20 +12.5692811105596 + 30 +0.0 + 11 +14.43447586915491 + 21 +12.50928111055961 + 31 +0.0 + 12 +14.25447586915491 + 22 +12.53928111055961 + 32 +0.0 + 13 +14.25447586915491 + 23 +12.53928111055961 + 33 +0.0 + 0 +SOLID + 5 +225 +330 +207 +100 +AcDbEntity + 8 +0 + 62 + 0 +440 + 16777216 +100 +AcDbTrace + 10 +24.07447586915492 + 20 +12.5692811105596 + 30 +0.0 + 11 +24.07447586915492 + 21 +12.50928111055961 + 31 +0.0 + 12 +24.25447586915492 + 22 +12.53928111055961 + 32 +0.0 + 13 +24.25447586915492 + 23 +12.53928111055961 + 33 +0.0 + 0 +MTEXT + 5 +226 +330 +207 +100 +AcDbEntity + 8 +0 + 62 + 0 +440 + 16777216 +100 +AcDbMText + 10 +19.25447586915491 + 20 +12.53928111055961 + 30 +0.0 + 40 +0.18 + 41 +0.0 + 46 +0.0 + 71 + 5 + 72 + 1 + 1 +\A1;10.0000 + 73 + 1 + 44 +1.0 + 0 +POINT + 5 +227 +330 +207 +100 +AcDbEntity + 8 +Defpoints + 62 + 0 +440 + 16777216 +100 +AcDbPoint + 10 +14.25447586915491 + 20 +11.58652104705702 + 30 +0.0 + 0 +POINT + 5 +228 +330 +207 +100 +AcDbEntity + 8 +Defpoints + 62 + 0 +440 + 16777216 +100 +AcDbPoint + 10 +24.25447586915492 + 20 +11.58652104705702 + 30 +0.0 + 0 +POINT + 5 +229 +330 +207 +100 +AcDbEntity + 8 +Defpoints + 62 + 0 +440 + 16777216 +100 +AcDbPoint + 10 +24.25447586915492 + 20 +12.53928111055961 + 30 +0.0 + 0 +ENDBLK + 5 +209 +330 +207 +100 +AcDbEntity + 8 +0 +100 +AcDbBlockEnd + 0 +BLOCK + 5 +22D +330 +22C +100 +AcDbEntity + 8 +0 +100 +AcDbBlockBegin + 2 +*D3 + 70 + 1 + 10 +0.0 + 20 +0.0 + 30 +0.0 + 3 +*D3 + 1 + + 0 +LINE + 5 +277 +330 +22C +100 +AcDbEntity + 8 +0 + 62 + 0 +440 + 16777216 +370 + -2 +100 +AcDbLine + 10 +14.59833903654579 + 20 +10.88680074186357 + 30 +0.0 + 11 +14.59833903654579 + 21 +12.51140470044061 + 31 +0.0 + 0 +LINE + 5 +278 +330 +22C +100 +AcDbEntity + 8 +0 + 62 + 0 +440 + 16777216 +370 + -2 +100 +AcDbLine + 10 +24.5983390365458 + 20 +10.88680074186357 + 30 +0.0 + 11 +24.5983390365458 + 21 +12.51140470044061 + 31 +0.0 + 0 +LINE + 5 +279 +330 +22C +100 +AcDbEntity + 8 +0 + 62 + 0 +440 + 16777216 +370 + -2 +100 +AcDbLine + 10 +14.77833903654579 + 20 +12.33140470044061 + 30 +0.0 + 11 +19.14348910475862 + 21 +12.33140470044061 + 31 +0.0 + 0 +LINE + 5 +27A +330 +22C +100 +AcDbEntity + 8 +0 + 62 + 0 +440 + 16777216 +370 + -2 +100 +AcDbLine + 10 +24.41833903654579 + 20 +12.33140470044061 + 30 +0.0 + 11 +20.05318896833297 + 21 +12.33140470044061 + 31 +0.0 + 0 +SOLID + 5 +27B +330 +22C +100 +AcDbEntity + 8 +0 + 62 + 0 +440 + 16777216 +100 +AcDbTrace + 10 +14.77833903654579 + 20 +12.36140470044061 + 30 +0.0 + 11 +14.77833903654579 + 21 +12.30140470044061 + 31 +0.0 + 12 +14.59833903654579 + 22 +12.33140470044061 + 32 +0.0 + 13 +14.59833903654579 + 23 +12.33140470044061 + 33 +0.0 + 0 +SOLID + 5 +27C +330 +22C +100 +AcDbEntity + 8 +0 + 62 + 0 +440 + 16777216 +100 +AcDbTrace + 10 +24.41833903654579 + 20 +12.36140470044061 + 30 +0.0 + 11 +24.41833903654579 + 21 +12.30140470044061 + 31 +0.0 + 12 +24.5983390365458 + 22 +12.33140470044061 + 32 +0.0 + 13 +24.5983390365458 + 23 +12.33140470044061 + 33 +0.0 + 0 +MTEXT + 5 +27D +330 +22C +100 +AcDbEntity + 8 +0 + 62 + 0 +440 + 16777216 +100 +AcDbMText + 10 +19.59833903654579 + 20 +12.33140470044061 + 30 +0.0 + 40 +0.18 + 41 +0.0 + 46 +0.0 + 71 + 5 + 72 + 1 + 1 +\A1;10.000 + 73 + 1 + 44 +1.0 + 0 +POINT + 5 +27E +330 +22C +100 +AcDbEntity + 8 +Defpoints + 62 + 0 +440 + 16777216 +100 +AcDbPoint + 10 +14.59833903654579 + 20 +10.82430074186357 + 30 +0.0 + 0 +POINT + 5 +27F +330 +22C +100 +AcDbEntity + 8 +Defpoints + 62 + 0 +440 + 16777216 +100 +AcDbPoint + 10 +24.5983390365458 + 20 +10.82430074186357 + 30 +0.0 + 0 +POINT + 5 +280 +330 +22C +100 +AcDbEntity + 8 +Defpoints + 62 + 0 +440 + 16777216 +100 +AcDbPoint + 10 +24.5983390365458 + 20 +12.33140470044061 + 30 +0.0 + 0 +ENDBLK + 5 +22E +330 +22C +100 +AcDbEntity + 8 +0 +100 +AcDbBlockEnd + 0 +BLOCK + 5 +2C4 +330 +2C3 +100 +AcDbEntity + 67 + 1 + 8 +0 +100 +AcDbBlockBegin + 2 +*Paper_Space9 + 70 + 0 + 10 +0.0 + 20 +0.0 + 30 +0.0 + 3 +*Paper_Space9 + 1 + + 0 +VIEWPORT + 5 +2C5 +102 +{ACAD_XDICTIONARY +360 +2C6 +102 +} +330 +2C3 +100 +AcDbEntity + 67 + 1 + 8 +0 +100 +AcDbViewport + 10 +189.9708404541016 + 20 +143.5099921226501 + 30 +0.0 + 40 +783.0383963893487 + 41 +314.237268371582 + 68 + 1 + 69 + 1 + 12 +189.9708404541016 + 22 +143.5099921226501 + 13 +0.0 + 23 +0.0 + 14 +10.0 + 24 +10.0 + 15 +10.0 + 25 +10.0 + 16 +0.0 + 26 +0.0 + 36 +1.0 + 17 +0.0 + 27 +0.0 + 37 +0.0 + 42 +50.0 + 43 +0.0 + 44 +0.0 + 45 +314.237268371582 + 50 +0.0 + 51 +0.0 + 72 + 1000 + 90 + 819232 + 1 + +281 + 0 + 71 + 1 + 74 + 0 +110 +0.0 +120 +0.0 +130 +0.0 +111 +1.0 +121 +0.0 +131 +0.0 +112 +0.0 +122 +1.0 +132 +0.0 + 79 + 0 +146 +0.0 +170 + 0 + 61 + 5 +348 +9F +292 + 1 +282 + 1 +141 +0.0 +142 +0.0 + 63 + 250 +421 + 3355443 + 0 +VIEWPORT + 5 +2C8 +102 +{ACAD_XDICTIONARY +360 +2C9 +102 +} +330 +2C3 +100 +AcDbEntity + 67 + 1 + 8 +0 +100 +AcDbViewport + 10 +120.4911271932069 + 20 +84.79116908827563 + 30 +0.0 + 40 +238.8822544817813 + 41 +167.9823382719187 + 68 + 2 + 69 + 2 + 12 +-3905.594010084151 + 22 +-2994.633675946912 + 13 +0.0 + 23 +0.0 + 14 +0.5 + 24 +0.5 + 15 +0.5 + 25 +0.5 + 16 +0.0 + 26 +0.0 + 36 +44111.74567509561 + 17 +3925.520858223038 + 27 +3005.669898567031 + 37 +0.0 + 42 +50.0 + 43 +0.0 + 44 +0.0 + 45 +16.79790352501503 + 50 +0.0 + 51 +0.0 + 72 + 1000 + 90 + 819296 + 1 + +281 + 0 + 71 + 1 + 74 + 0 +110 +0.0 +120 +0.0 +130 +0.0 +111 +1.0 +121 +0.0 +131 +0.0 +112 +0.0 +122 +1.0 +132 +0.0 + 79 + 0 +146 +0.0 +170 + 0 + 61 + 5 +348 +9F +292 + 1 +282 + 1 +141 +0.0 +142 +0.0 + 63 + 250 +421 + 3355443 + 0 +ENDBLK + 5 +2CB +330 +2C3 +100 +AcDbEntity + 67 + 1 + 8 +0 +100 +AcDbBlockEnd + 0 +ENDSEC + 0 +SECTION + 2 +ENTITIES + 0 +LINE + 5 +22A +102 +{ACAD_REACTORS +330 +244 +102 +} +330 +1F +100 +AcDbEntity + 8 +0 +100 +AcDbLine + 10 +14.59833903654579 + 20 +10.82430074186357 + 30 +0.0 + 11 +24.5983390365458 + 21 +10.82430074186357 + 31 +0.0 + 0 +DIMENSION + 5 +22B +102 +{ACAD_XDICTIONARY +360 +239 +102 +} +102 +{ACAD_REACTORS +330 +244 +102 +} +330 +1F +100 +AcDbEntity + 8 +0 +100 +AcDbDimension +280 + 0 + 2 +*D3 + 10 +24.5983390365458 + 20 +12.33140470044061 + 30 +0.0 + 11 +19.59833903654579 + 21 +12.33140470044061 + 31 +0.0 + 70 + 32 + 71 + 5 + 42 +10.0 + 73 + 0 + 74 + 0 + 75 + 0 + 3 +Standard +100 +AcDbAlignedDimension + 13 +14.59833903654579 + 23 +10.82430074186357 + 33 +0.0 + 14 +24.5983390365458 + 24 +10.82430074186357 + 34 +0.0 +100 +AcDbRotatedDimension +1001 +ACAD +1000 +DSTYLE +1002 +{ +1070 + 271 +1070 + 3 +1002 +} + 0 +VIEWPORT + 5 +28A +102 +{ACAD_XDICTIONARY +360 +28B +102 +} +330 +58 +100 +AcDbEntity + 67 + 1 + 8 +0 +100 +AcDbViewport + 10 +78.82313944341524 + 20 +77.2092821871629 + 30 +0.0 + 40 +574.3653659734484 + 41 +230.4957259860821 + 68 + 1 + 69 + 1 + 12 +78.82313944341524 + 22 +77.2092821871629 + 13 +0.0 + 23 +0.0 + 14 +10.0 + 24 +10.0 + 15 +10.0 + 25 +10.0 + 16 +0.0 + 26 +0.0 + 36 +1.0 + 17 +0.0 + 27 +0.0 + 37 +0.0 + 42 +50.0 + 43 +0.0 + 44 +0.0 + 45 +230.4957259860821 + 50 +0.0 + 51 +0.0 + 72 + 1000 + 90 + 819232 + 1 + +281 + 0 + 71 + 1 + 74 + 0 +110 +0.0 +120 +0.0 +130 +0.0 +111 +1.0 +121 +0.0 +131 +0.0 +112 +0.0 +122 +1.0 +132 +0.0 + 79 + 0 +146 +0.0 +170 + 0 + 61 + 5 +348 +9F +292 + 1 +282 + 1 +141 +0.0 +142 +0.0 + 63 + 250 +421 + 3355443 + 0 +VIEWPORT + 5 +28F +102 +{ACAD_XDICTIONARY +360 +290 +102 +} +330 +58 +100 +AcDbEntity + 67 + 1 + 8 +0 +100 +AcDbViewport + 10 +120.4911271932069 + 20 +84.79116908827563 + 30 +0.0 + 40 +238.8822544817813 + 41 +167.9823382719187 + 68 + 2 + 69 + 2 + 12 +-3905.594010084151 + 22 +-2994.633675946912 + 13 +0.0 + 23 +0.0 + 14 +0.5 + 24 +0.5 + 15 +0.5 + 25 +0.5 + 16 +0.0 + 26 +0.0 + 36 +44111.74567509561 + 17 +3925.520858223038 + 27 +3005.669898567031 + 37 +0.0 + 42 +50.0 + 43 +0.0 + 44 +0.0 + 45 +16.79790352501503 + 50 +0.0 + 51 +0.0 + 72 + 1000 + 90 + 819296 + 1 + +281 + 0 + 71 + 1 + 74 + 0 +110 +0.0 +120 +0.0 +130 +0.0 +111 +1.0 +121 +0.0 +131 +0.0 +112 +0.0 +122 +1.0 +132 +0.0 + 79 + 0 +146 +0.0 +170 + 0 + 61 + 5 +348 +9F +292 + 1 +282 + 1 +141 +0.0 +142 +0.0 + 63 + 250 +421 + 3355443 + 0 +ENDSEC + 0 +SECTION + 2 +OBJECTS + 0 +DICTIONARY + 5 +C +330 +0 +100 +AcDbDictionary +281 + 1 + 3 +ACAD_CIP_PREVIOUS_PRODUCT_INFO +350 +1A5 + 3 +ACAD_COLOR +350 +73 + 3 +ACAD_DETAILVIEWSTYLE +350 +1AB + 3 +ACAD_GROUP +350 +D + 3 +ACAD_LAYOUT +350 +1A + 3 +ACAD_MATERIAL +350 +72 + 3 +ACAD_MLEADERSTYLE +350 +D7 + 3 +ACAD_MLINESTYLE +350 +17 + 3 +ACAD_PLOTSETTINGS +350 +19 + 3 +ACAD_PLOTSTYLENAME +350 +E + 3 +ACAD_RENDER_RAPIDRT_SETTINGS +350 +292 + 3 +ACAD_SCALELIST +350 +B6 + 3 +ACAD_SECTIONVIEWSTYLE +350 +1A8 + 3 +ACAD_TABLESTYLE +350 +86 + 3 +ACAD_VISUALSTYLE +350 +99 + 3 +ACDB_RECOMPOSE_DATA +350 +35C + 3 +AcDbVariableDictionary +350 +66 + 0 +DICTIONARY + 5 +18E +330 +2 +100 +AcDbDictionary +280 + 1 +281 + 1 + 3 +ACAD_LAYERSTATES +360 +18F + 0 +DICTIONARY + 5 +E6 +330 +10 +100 +AcDbDictionary +280 + 1 +281 + 1 + 0 +DICTIONARY + 5 +15D +330 +1F +100 +AcDbDictionary +280 + 1 +281 + 1 + 0 +DICTIONARY + 5 +2EB +330 +2EA +100 +AcDbDictionary +280 + 1 +281 + 1 + 3 +ASDK_XREC_ANNOTATION_SCALE_INFO +360 +2EC + 0 +DICTIONARY + 5 +2EE +330 +2ED +100 +AcDbDictionary +280 + 1 +281 + 1 + 3 +ASDK_XREC_ANNOTATION_SCALE_INFO +360 +2EF + 0 +DICTIONARY + 5 +2C6 +330 +2C5 +100 +AcDbDictionary +280 + 1 +281 + 1 + 3 +ASDK_XREC_ANNOTATION_SCALE_INFO +360 +2C7 + 0 +DICTIONARY + 5 +2C9 +330 +2C8 +100 +AcDbDictionary +280 + 1 +281 + 1 + 3 +ASDK_XREC_ANNOTATION_SCALE_INFO +360 +2CA + 0 +DICTIONARY + 5 +239 +330 +22B +100 +AcDbDictionary +280 + 1 +281 + 1 + 3 +ACAD_DIMASSOC +360 +244 + 0 +DICTIONARY + 5 +28B +330 +28A +100 +AcDbDictionary +280 + 1 +281 + 1 + 3 +ASDK_XREC_ANNOTATION_SCALE_INFO +360 +28C + 0 +DICTIONARY + 5 +290 +330 +28F +100 +AcDbDictionary +280 + 1 +281 + 1 + 3 +ASDK_XREC_ANNOTATION_SCALE_INFO +360 +291 + 0 +XRECORD + 5 +1A5 +102 +{ACAD_REACTORS +330 +C +102 +} +330 +C +100 +AcDbXrecord +280 + 1 +300 +ACD +300 +2016 +300 +ACD_F_S + 0 +DICTIONARY + 5 +73 +102 +{ACAD_REACTORS +330 +C +102 +} +330 +C +100 +AcDbDictionary +281 + 1 + 0 +DICTIONARY + 5 +1AB +102 +{ACAD_REACTORS +330 +C +102 +} +330 +C +100 +AcDbDictionary +281 + 1 + 3 +Imperial24 +350 +1AC + 0 +DICTIONARY + 5 +D +102 +{ACAD_REACTORS +330 +C +102 +} +330 +C +100 +AcDbDictionary +281 + 1 + 0 +DICTIONARY + 5 +1A +102 +{ACAD_REACTORS +330 +C +102 +} +330 +C +100 +AcDbDictionary +281 + 1 + 3 +A3 - plotter +350 +2CC + 3 +A4 1-100 +350 +59 + 3 +ARCH D - PLOTTER +350 +2F1 + 3 +Model +350 +22 + 0 +DICTIONARY + 5 +72 +102 +{ACAD_REACTORS +330 +C +102 +} +330 +C +100 +AcDbDictionary +281 + 1 + 3 +ByBlock +350 +97 + 3 +ByLayer +350 +96 + 3 +Global +350 +98 + 0 +DICTIONARY + 5 +D7 +102 +{ACAD_REACTORS +330 +C +102 +} +330 +C +100 +AcDbDictionary +281 + 1 + 3 +Annotative +350 +E5 + 3 +Standard +350 +D8 + 0 +DICTIONARY + 5 +17 +102 +{ACAD_REACTORS +330 +C +102 +} +330 +C +100 +AcDbDictionary +281 + 1 + 3 +Standard +350 +18 + 0 +DICTIONARY + 5 +19 +102 +{ACAD_REACTORS +330 +C +102 +} +330 +C +100 +AcDbDictionary +281 + 1 + 0 +ACDBDICTIONARYWDFLT + 5 +E +102 +{ACAD_REACTORS +330 +C +102 +} +330 +C +100 +AcDbDictionary +281 + 1 + 3 +Normal +350 +F +100 +AcDbDictionaryWithDefault +340 +F + 0 +DICTIONARY + 5 +292 +102 +{ACAD_REACTORS +330 +C +102 +} +330 +C +100 +AcDbDictionary +281 + 1 + 3 +Coffee-Break Quality +350 +296 + 3 +High +350 +295 + 3 +Low +350 +293 + 3 +Lunch Quality +350 +297 + 3 +Medium +350 +294 + 3 +Overnight Quality +350 +298 + 0 +DICTIONARY + 5 +B6 +102 +{ACAD_REACTORS +330 +C +102 +} +330 +C +100 +AcDbDictionary +281 + 1 + 3 +A0 +350 +B7 + 3 +A1 +350 +124 + 3 +A2 +350 +125 + 3 +A3 +350 +126 + 3 +A4 +350 +127 + 3 +A5 +350 +128 + 3 +A6 +350 +129 + 3 +A7 +350 +12A + 3 +A8 +350 +12B + 3 +A9 +350 +12C + 3 +B0 +350 +12D + 3 +B1 +350 +12E + 3 +B2 +350 +12F + 3 +B3 +350 +130 + 3 +B4 +350 +131 + 3 +B5 +350 +132 + 3 +B6 +350 +133 + 3 +B7 +350 +134 + 3 +B8 +350 +135 + 3 +B9 +350 +136 + 3 +C0 +350 +137 + 3 +C1 +350 +138 + 3 +C2 +350 +139 + 3 +C3 +350 +13A + 3 +C4 +350 +13B + 3 +C5 +350 +13C + 3 +C6 +350 +13D + 3 +C7 +350 +13E + 3 +C8 +350 +13F + 3 +C9 +350 +140 + 3 +D0 +350 +141 + 3 +D1 +350 +142 + 3 +D2 +350 +143 + 0 +DICTIONARY + 5 +1A8 +102 +{ACAD_REACTORS +330 +C +102 +} +330 +C +100 +AcDbDictionary +281 + 1 + 3 +Imperial24 +350 +1AA + 0 +DICTIONARY + 5 +86 +102 +{ACAD_REACTORS +330 +C +102 +} +330 +C +100 +AcDbDictionary +281 + 1 + 3 +Standard +350 +87 + 0 +DICTIONARY + 5 +99 +102 +{ACAD_REACTORS +330 +C +102 +} +330 +C +100 +AcDbDictionary +281 + 1 + 3 +2dWireframe +350 +9F + 3 +Basic +350 +9E + 3 +Brighten +350 +A5 + 3 +ColorChange +350 +A9 + 3 +Conceptual +350 +A2 + 3 +Dim +350 +A4 + 3 +EdgeColorOff +350 +175 + 3 +Facepattern +350 +A8 + 3 +Flat +350 +9A + 3 +FlatWithEdges +350 +9B + 3 +Gouraud +350 +9C + 3 +GouraudWithEdges +350 +9D + 3 +Hidden +350 +A1 + 3 +JitterOff +350 +173 + 3 +Linepattern +350 +A7 + 3 +OverhangOff +350 +174 + 3 +Realistic +350 +A3 + 3 +Shaded +350 +182 + 3 +Shaded with edges +350 +181 + 3 +Shades of Gray +350 +17E + 3 +Sketchy +350 +17F + 3 +Thicken +350 +A6 + 3 +Wireframe +350 +A0 + 3 +X-Ray +350 +180 + 0 +XRECORD + 5 +35C +102 +{ACAD_REACTORS +330 +C +102 +} +330 +C +100 +AcDbXrecord +280 + 1 + 90 + 1 +330 +87 +330 +1AA +330 +1AC + 0 +DICTIONARY + 5 +66 +102 +{ACAD_REACTORS +330 +C +102 +} +330 +C +100 +AcDbDictionary +281 + 1 + 3 +CANNOSCALE +350 +F0 + 3 +CMLEADERSTYLE +350 +EF + 3 +CTABLESTYLE +350 +89 + 3 +CVIEWDETAILSTYLE +350 +1B7 + 3 +CVIEWSECTIONSTYLE +350 +1B8 + 3 +DIMASSOC +350 +67 + 3 +HIDETEXT +350 +6B + 3 +LAYEREVAL +350 +14D + 3 +LAYERNOTIFY +350 +14E + 0 +DICTIONARY + 5 +18F +102 +{ACAD_REACTORS +330 +18E +102 +} +330 +18E +100 +AcDbDictionary +281 + 1 + 0 +XRECORD + 5 +2EC +102 +{ACAD_REACTORS +330 +2EB +102 +} +330 +2EB +100 +AcDbXrecord +280 + 1 + 90 + 1 +340 +B7 + 0 +XRECORD + 5 +2EF +102 +{ACAD_REACTORS +330 +2EE +102 +} +330 +2EE +100 +AcDbXrecord +280 + 1 + 90 + 1 +340 +B7 + 0 +XRECORD + 5 +2C7 +102 +{ACAD_REACTORS +330 +2C6 +102 +} +330 +2C6 +100 +AcDbXrecord +280 + 1 + 90 + 1 +340 +B7 + 0 +XRECORD + 5 +2CA +102 +{ACAD_REACTORS +330 +2C9 +102 +} +330 +2C9 +100 +AcDbXrecord +280 + 1 + 90 + 1 +340 +B7 + 0 +DIMASSOC + 5 +244 +102 +{ACAD_REACTORS +330 +239 +102 +} +330 +239 +100 +AcDbDimAssoc +330 +22B + 90 + 3 + 70 + 0 + 71 + 0 + 1 +AcDbOsnapPointRef + 72 + 13 +331 +22A + 73 + 2 + 91 + 0 + 40 +0.0 + 10 +14.59833903654579 + 20 +10.82430074186357 + 30 +0.0 + 75 + 0 + 1 +AcDbOsnapPointRef + 72 + 1 +331 +22A + 73 + 2 + 91 + 0 + 40 +1.0 + 10 +24.5983390365458 + 20 +10.82430074186357 + 30 +0.0 + 75 + 0 + 0 +XRECORD + 5 +28C +102 +{ACAD_REACTORS +330 +28B +102 +} +330 +28B +100 +AcDbXrecord +280 + 1 + 90 + 1 +340 +B7 + 0 +XRECORD + 5 +291 +102 +{ACAD_REACTORS +330 +290 +102 +} +330 +290 +100 +AcDbXrecord +280 + 1 + 90 + 1 +340 +B7 + 0 +ACDBDETAILVIEWSTYLE + 5 +1AC +102 +{ACAD_XDICTIONARY +360 +1C6 +102 +} +102 +{ACAD_REACTORS +330 +1AB +102 +} +330 +1AB +100 +AcDbModelDocViewStyle + 70 + 0 + 3 +Imperial24 +290 + 0 +100 +AcDbDetailViewStyle + 70 + 0 + 71 + 0 + 90 + 3 + 71 + 1 +340 +11 + 62 + 256 + 40 +0.24 +340 +0 + 62 + 256 + 40 +0.24 +300 + + 40 +0.36 +280 + 3 + 71 + 2 +340 +16 + 90 + 25 + 62 + 256 + 71 + 3 +340 +11 + 62 + 256 + 40 +0.24 + 90 + 1 + 40 +0.75 + 90 + 1 +300 +%<\AcVar ViewType \f "%tc1">% %<\AcVar ViewDetailId>%\PSCALE %<\AcVar ViewScale \f "%sn">% + 71 + 4 +340 +16 + 90 + 25 + 62 + 256 +340 +16 + 90 + 25 + 62 + 256 +280 + 0 + 0 +LAYOUT + 5 +2CC +102 +{ACAD_REACTORS +330 +1A +102 +} +330 +1A +100 +AcDbPlotSettings + 1 + + 2 +HPB602DE (HP OfficeJet Pro 7740 series) + 4 +A3 + 6 + + 40 +4.995333194732666 + 41 +19.98135375976562 + 42 +5.03765869140625 + 43 +20.02365112304687 + 44 +297.0106506347656 + 45 +419.9889831542968 + 46 +0.0 + 47 +0.0 + 48 +0.0 + 49 +0.0 +140 +0.0 +141 +0.0 +142 +1.0 +143 +1.0 + 70 + 672 + 72 + 1 + 73 + 1 + 74 + 5 + 7 + + 75 + 16 +147 +25.4 + 76 + 0 + 77 + 2 + 78 + 300 +148 +0.0 +149 +0.0 +100 +AcDbLayout + 1 +A3 - plotter + 70 + 1 + 71 + 2 + 10 +-20.02365112304687 + 20 +-4.995333194732666 + 11 +399.96533203125 + 21 +292.015317440033 + 12 +0.0 + 22 +0.0 + 32 +0.0 + 14 +1.049999952316284 + 24 +0.7999999523162842 + 34 +0.0 + 15 +239.9322544340976 + 25 +168.782338224235 + 35 +0.0 +146 +0.0 + 13 +0.0 + 23 +0.0 + 33 +0.0 + 16 +1.0 + 26 +0.0 + 36 +0.0 + 17 +0.0 + 27 +1.0 + 37 +0.0 + 76 + 0 +330 +2C3 +331 +2C5 +1001 +ACAD_PSEXT +1000 +HPB602DE (HP OfficeJet Pro 7740 series) +1000 +HP PCL3 B-size Printer Class Driver +1000 +WSD-435235e4-432b-4da3-b38d-30ad096c097b.0064 +1000 +This is a web services printer +1070 + 0 + 0 +LAYOUT + 5 +59 +102 +{ACAD_REACTORS +330 +1A +102 +} +330 +1A +100 +AcDbPlotSettings + 1 + + 2 +DWG To PDF.pc3 + 4 +ISO_A4_(297.00_x_210.00_MM) + 6 + + 40 +5.793749809265136 + 41 +17.79375076293945 + 42 +5.79376220703125 + 43 +17.79374694824219 + 44 +297.0 + 45 +210.0 + 46 +0.0 + 47 +0.0 + 48 +0.0 + 49 +0.0 +140 +0.0 +141 +0.0 +142 +1.0 +143 +1.0 + 70 + 672 + 72 + 1 + 73 + 0 + 74 + 5 + 7 + + 75 + 16 +147 +25.4 + 76 + 0 + 77 + 2 + 78 + 300 +148 +0.0 +149 +0.0 +100 +AcDbLayout + 1 +A4 1-100 + 70 + 1 + 71 + 1 + 10 +-5.793749809265136 + 20 +-17.79375076293945 + 11 +291.2062501907349 + 21 +192.2062492370605 + 12 +0.0 + 22 +0.0 + 32 +0.0 + 14 +1.049999952316284 + 24 +0.7999999523162842 + 34 +0.0 + 15 +239.9322544340976 + 25 +168.782338224235 + 35 +0.0 +146 +0.0 + 13 +0.0 + 23 +0.0 + 33 +0.0 + 16 +1.0 + 26 +0.0 + 36 +0.0 + 17 +0.0 + 27 +1.0 + 37 +0.0 + 76 + 0 +330 +58 +331 +28A +1001 +ACAD_PSEXT +1000 +DWG To PDF.pc3 +1000 +DWG To PDF +1000 +File +1000 + +1070 + 0 + 0 +LAYOUT + 5 +2F1 +102 +{ACAD_REACTORS +330 +1A +102 +} +330 +1A +100 +AcDbPlotSettings + 1 + + 2 +AutoCAD PDF (High Quality Print).pc3 + 4 +ARCH_D_(36.00_x_24.00_Inches) + 6 + + 40 +5.793749809265136 + 41 +17.79375076293945 + 42 +5.79376220703125 + 43 +17.79376220703125 + 44 +914.4000244140626 + 45 +609.5999755859375 + 46 +0.0 + 47 +0.0 + 48 +0.0 + 49 +0.0 +140 +0.0 +141 +0.0 +142 +1.0 +143 +25.4 + 70 + 672 + 72 + 0 + 73 + 0 + 74 + 5 + 7 + + 75 + 16 +147 +0.0393700787401575 + 76 + 0 + 77 + 2 + 78 + 300 +148 +0.0 +149 +0.0 +100 +AcDbLayout + 1 +ARCH D - PLOTTER + 70 + 1 + 71 + 3 + 10 +-5.793749809265136 + 20 +-17.79375076293945 + 11 +908.6062746047975 + 21 +591.806224822998 + 12 +0.0 + 22 +0.0 + 32 +0.0 + 14 +1.049999952316284 + 24 +0.7999999523162842 + 34 +0.0 + 15 +898.1760434614326 + 25 +572.39306427978 + 35 +0.0 +146 +0.0 + 13 +0.0 + 23 +0.0 + 33 +0.0 + 16 +1.0 + 26 +0.0 + 36 +0.0 + 17 +0.0 + 27 +1.0 + 37 +0.0 + 76 + 0 +330 +2E8 +331 +2EA +1001 +ACAD_PSEXT +1000 +AutoCAD PDF (High Quality Print).pc3 +1000 +DWG To PDF +1000 +File +1000 + +1070 + 0 + 0 +LAYOUT + 5 +22 +102 +{ACAD_XDICTIONARY +360 +194 +102 +} +102 +{ACAD_REACTORS +330 +1A +102 +} +330 +1A +100 +AcDbPlotSettings + 1 + + 2 +none_device + 4 +ANSI_A_(8.50_x_11.00_Inches) + 6 + + 40 +6.349999904632568 + 41 +19.04999923706055 + 42 +6.350006103515625 + 43 +19.04998779296875 + 44 +215.8999938964844 + 45 +279.3999938964844 + 46 +0.0 + 47 +0.0 + 48 +0.0 + 49 +0.0 +140 +0.0 +141 +0.0 +142 +1.0 +143 +2.584895464708373 + 70 + 11952 + 72 + 0 + 73 + 1 + 74 + 0 + 7 + + 75 + 0 +147 +0.3868628397755418 + 76 + 0 + 77 + 2 + 78 + 300 +148 +0.0 +149 +0.0 +100 +AcDbLayout + 1 +Model + 70 + 1 + 71 + 0 + 10 +0.0 + 20 +0.0 + 11 +12.0 + 21 +9.0 + 12 +0.0 + 22 +0.0 + 32 +0.0 + 14 +0.0 + 24 +0.0 + 34 +0.0 + 15 +0.0 + 25 +0.0 + 35 +0.0 +146 +0.0 + 13 +0.0 + 23 +0.0 + 33 +0.0 + 16 +1.0 + 26 +0.0 + 36 +0.0 + 17 +0.0 + 27 +1.0 + 37 +0.0 + 76 + 0 +330 +1F +331 +94 + 0 +MATERIAL + 5 +97 +102 +{ACAD_XDICTIONARY +360 +188 +102 +} +102 +{ACAD_REACTORS +330 +72 +102 +} +330 +72 +100 +AcDbMaterial + 1 +ByBlock + 94 + 63 + 0 +MATERIAL + 5 +96 +102 +{ACAD_XDICTIONARY +360 +186 +102 +} +102 +{ACAD_REACTORS +330 +72 +102 +} +330 +72 +100 +AcDbMaterial + 1 +ByLayer + 94 + 63 + 0 +MATERIAL + 5 +98 +102 +{ACAD_XDICTIONARY +360 +110 +102 +} +102 +{ACAD_REACTORS +330 +72 +102 +} +330 +72 +100 +AcDbMaterial + 1 +Global + 43 +0.0208000000566244 + 43 +0.0 + 43 +0.0 + 43 +0.0 + 43 +0.0 + 43 +0.0208000000566244 + 43 +0.0 + 43 +0.0 + 43 +0.0 + 43 +0.0 + 43 +1.0 + 43 +0.0 + 43 +0.0 + 43 +0.0 + 43 +0.0 + 43 +1.0 + 49 +0.0208000000566244 + 49 +0.0 + 49 +0.0 + 49 +0.0 + 49 +0.0 + 49 +0.0208000000566244 + 49 +0.0 + 49 +0.0 + 49 +0.0 + 49 +0.0 + 49 +1.0 + 49 +0.0 + 49 +0.0 + 49 +0.0 + 49 +0.0 + 49 +1.0 +142 +0.0208000000566244 +142 +0.0 +142 +0.0 +142 +0.0 +142 +0.0 +142 +0.0208000000566244 +142 +0.0 +142 +0.0 +142 +0.0 +142 +0.0 +142 +1.0 +142 +0.0 +142 +0.0 +142 +0.0 +142 +0.0 +142 +1.0 +144 +0.0208000000566244 +144 +0.0 +144 +0.0 +144 +0.0 +144 +0.0 +144 +0.0208000000566244 +144 +0.0 +144 +0.0 +144 +0.0 +144 +0.0 +144 +1.0 +144 +0.0 +144 +0.0 +144 +0.0 +144 +0.0 +144 +1.0 + 94 + 63 +1001 +ACAD +1070 + -1 +1070 + 3 +1070 + 0 +1000 + +1071 + 0 +1070 + 0 + 0 +MLEADERSTYLE + 5 +E5 +102 +{ACAD_REACTORS +330 +D7 +102 +} +330 +D7 +100 +AcDbMLeaderStyle +179 + 2 +170 + 2 +171 + 1 +172 + 0 + 90 + 2 + 40 +0.0 + 41 +0.0 +173 + 1 + 91 +-1056964608 +340 +14 + 92 + -2 +290 + 1 + 42 +0.09 +291 + 1 + 43 +0.36 + 3 +Standard + 44 +0.18 +300 + +342 +11 +174 + 1 +178 + 1 +175 + 1 +176 + 0 + 93 +-1056964608 + 45 +0.18 +292 + 0 +297 + 0 + 46 +0.18 + 94 +-1056964608 + 47 +1.0 + 49 +1.0 +140 +1.0 +293 + 1 +141 +0.0 +294 + 1 +177 + 0 +142 +1.0 +295 + 0 +296 + 1 +143 +0.125 +271 + 0 +272 + 9 +273 + 9 +298 + 0 + 0 +MLEADERSTYLE + 5 +D8 +102 +{ACAD_REACTORS +330 +D7 +102 +} +330 +D7 +100 +AcDbMLeaderStyle +179 + 2 +170 + 2 +171 + 1 +172 + 0 + 90 + 2 + 40 +0.0 + 41 +0.0 +173 + 1 + 91 +-1056964608 +340 +14 + 92 + -2 +290 + 1 + 42 +0.09 +291 + 1 + 43 +0.36 + 3 +Standard + 44 +0.18 +300 + +342 +11 +174 + 1 +178 + 1 +175 + 1 +176 + 0 + 93 +-1056964608 + 45 +0.18 +292 + 0 +297 + 0 + 46 +0.18 + 94 +-1056964608 + 47 +1.0 + 49 +1.0 +140 +1.0 +293 + 1 +141 +0.0 +294 + 1 +177 + 0 +142 +1.0 +295 + 0 +296 + 0 +143 +0.125 +271 + 0 +272 + 9 +273 + 9 +298 + 0 + 0 +MLINESTYLE + 5 +18 +102 +{ACAD_REACTORS +330 +17 +102 +} +330 +17 +100 +AcDbMlineStyle + 2 +STANDARD + 70 + 0 + 3 + + 62 + 256 + 51 +90.0 + 52 +90.0 + 71 + 2 + 49 +0.5 + 62 + 256 + 6 +BYLAYER + 49 +-0.5 + 62 + 256 + 6 +BYLAYER + 0 +ACDBPLACEHOLDER + 5 +F +102 +{ACAD_REACTORS +330 +E +102 +} +330 +E + 0 +RAPIDRTRENDERSETTINGS + 5 +296 +102 +{ACAD_REACTORS +330 +292 +102 +} +330 +292 +100 +AcDbRenderSettings + 90 + 2 + 1 +Coffee-Break Quality +290 + 1 +290 + 0 +290 + 1 +290 + 1 + 1 + + 1 +Renders for 10 minutes + 90 + 3 +100 +AcDbRapidRTRenderSettings + 90 + 3 + 70 + 1 + 90 + 1 + 90 + 10 + 70 + 1 + 70 + 2 + 40 +3.0 + 40 +3.0 +290 + 1 + 0 +RAPIDRTRENDERSETTINGS + 5 +295 +102 +{ACAD_REACTORS +330 +292 +102 +} +330 +292 +100 +AcDbRenderSettings + 90 + 2 + 1 +High +290 + 1 +290 + 0 +290 + 1 +290 + 1 + 1 + + 1 +Applies 10 rendering levels + 90 + 2 +100 +AcDbRapidRTRenderSettings + 90 + 3 + 70 + 0 + 90 + 10 + 90 + 1 + 70 + 2 + 70 + 2 + 40 +3.0 + 40 +3.0 +290 + 1 + 0 +RAPIDRTRENDERSETTINGS + 5 +293 +102 +{ACAD_REACTORS +330 +292 +102 +} +330 +292 +100 +AcDbRenderSettings + 90 + 2 + 1 +Low +290 + 1 +290 + 0 +290 + 1 +290 + 1 + 1 + + 1 +Applies 1 rendering level + 90 + 0 +100 +AcDbRapidRTRenderSettings + 90 + 3 + 70 + 0 + 90 + 1 + 90 + 1 + 70 + 0 + 70 + 2 + 40 +3.0 + 40 +3.0 +290 + 1 + 0 +RAPIDRTRENDERSETTINGS + 5 +297 +102 +{ACAD_REACTORS +330 +292 +102 +} +330 +292 +100 +AcDbRenderSettings + 90 + 2 + 1 +Lunch Quality +290 + 1 +290 + 0 +290 + 1 +290 + 1 + 1 + + 1 +Renders for 60 minutes + 90 + 4 +100 +AcDbRapidRTRenderSettings + 90 + 3 + 70 + 1 + 90 + 1 + 90 + 60 + 70 + 2 + 70 + 2 + 40 +3.0 + 40 +3.0 +290 + 1 + 0 +RAPIDRTRENDERSETTINGS + 5 +294 +102 +{ACAD_REACTORS +330 +292 +102 +} +330 +292 +100 +AcDbRenderSettings + 90 + 2 + 1 +Medium +290 + 1 +290 + 0 +290 + 1 +290 + 1 + 1 + + 1 +Applies 5 rendering levels + 90 + 1 +100 +AcDbRapidRTRenderSettings + 90 + 3 + 70 + 0 + 90 + 5 + 90 + 1 + 70 + 1 + 70 + 2 + 40 +3.0 + 40 +3.0 +290 + 1 + 0 +RAPIDRTRENDERSETTINGS + 5 +298 +102 +{ACAD_REACTORS +330 +292 +102 +} +330 +292 +100 +AcDbRenderSettings + 90 + 2 + 1 +Overnight Quality +290 + 1 +290 + 0 +290 + 1 +290 + 1 + 1 + + 1 +Renders for 12 hours + 90 + 5 +100 +AcDbRapidRTRenderSettings + 90 + 3 + 70 + 1 + 90 + 1 + 90 + 720 + 70 + 2 + 70 + 2 + 40 +3.0 + 40 +3.0 +290 + 1 + 0 +SCALE + 5 +B7 +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +1:1 +140 +1.0 +141 +1.0 +290 + 1 + 0 +SCALE + 5 +124 +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +1:2 +140 +1.0 +141 +2.0 +290 + 0 + 0 +SCALE + 5 +125 +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +1:4 +140 +1.0 +141 +4.0 +290 + 0 + 0 +SCALE + 5 +126 +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +1:5 +140 +1.0 +141 +5.0 +290 + 0 + 0 +SCALE + 5 +127 +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +1:8 +140 +1.0 +141 +8.0 +290 + 0 + 0 +SCALE + 5 +128 +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +1:10 +140 +1.0 +141 +10.0 +290 + 0 + 0 +SCALE + 5 +129 +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +1:16 +140 +1.0 +141 +16.0 +290 + 0 + 0 +SCALE + 5 +12A +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +1:20 +140 +1.0 +141 +20.0 +290 + 0 + 0 +SCALE + 5 +12B +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +1:30 +140 +1.0 +141 +30.0 +290 + 0 + 0 +SCALE + 5 +12C +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +1:40 +140 +1.0 +141 +40.0 +290 + 0 + 0 +SCALE + 5 +12D +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +1:50 +140 +1.0 +141 +50.0 +290 + 0 + 0 +SCALE + 5 +12E +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +1:100 +140 +1.0 +141 +100.0 +290 + 0 + 0 +SCALE + 5 +12F +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +2:1 +140 +2.0 +141 +1.0 +290 + 0 + 0 +SCALE + 5 +130 +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +4:1 +140 +4.0 +141 +1.0 +290 + 0 + 0 +SCALE + 5 +131 +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +8:1 +140 +8.0 +141 +1.0 +290 + 0 + 0 +SCALE + 5 +132 +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +10:1 +140 +10.0 +141 +1.0 +290 + 0 + 0 +SCALE + 5 +133 +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +100:1 +140 +100.0 +141 +1.0 +290 + 0 + 0 +SCALE + 5 +134 +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +1/128" = 1'-0" +140 +0.0078125 +141 +12.0 +290 + 0 + 0 +SCALE + 5 +135 +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +1/64" = 1'-0" +140 +0.015625 +141 +12.0 +290 + 0 + 0 +SCALE + 5 +136 +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +1/32" = 1'-0" +140 +0.03125 +141 +12.0 +290 + 0 + 0 +SCALE + 5 +137 +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +1/16" = 1'-0" +140 +0.0625 +141 +12.0 +290 + 0 + 0 +SCALE + 5 +138 +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +3/32" = 1'-0" +140 +0.09375 +141 +12.0 +290 + 0 + 0 +SCALE + 5 +139 +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +1/8" = 1'-0" +140 +0.125 +141 +12.0 +290 + 0 + 0 +SCALE + 5 +13A +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +3/16" = 1'-0" +140 +0.1875 +141 +12.0 +290 + 0 + 0 +SCALE + 5 +13B +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +1/4" = 1'-0" +140 +0.25 +141 +12.0 +290 + 0 + 0 +SCALE + 5 +13C +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +3/8" = 1'-0" +140 +0.375 +141 +12.0 +290 + 0 + 0 +SCALE + 5 +13D +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +1/2" = 1'-0" +140 +0.5 +141 +12.0 +290 + 0 + 0 +SCALE + 5 +13E +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +3/4" = 1'-0" +140 +0.75 +141 +12.0 +290 + 0 + 0 +SCALE + 5 +13F +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +1" = 1'-0" +140 +1.0 +141 +12.0 +290 + 0 + 0 +SCALE + 5 +140 +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +1-1/2" = 1'-0" +140 +1.5 +141 +12.0 +290 + 0 + 0 +SCALE + 5 +141 +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +3" = 1'-0" +140 +3.0 +141 +12.0 +290 + 0 + 0 +SCALE + 5 +142 +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +6" = 1'-0" +140 +6.0 +141 +12.0 +290 + 0 + 0 +SCALE + 5 +143 +102 +{ACAD_REACTORS +330 +B6 +102 +} +330 +B6 +100 +AcDbScale + 70 + 0 +300 +1'-0" = 1'-0" +140 +12.0 +141 +12.0 +290 + 0 + 0 +ACDBSECTIONVIEWSTYLE + 5 +1AA +102 +{ACAD_XDICTIONARY +360 +1C4 +102 +} +102 +{ACAD_REACTORS +330 +1A8 +102 +} +330 +1A8 +100 +AcDbModelDocViewStyle + 70 + 0 + 3 +Imperial24 +290 + 0 +100 +AcDbSectionViewStyle + 70 + 0 + 71 + 0 + 90 + 78 + 71 + 1 +340 +11 + 62 + 256 + 40 +0.24 +340 +0 +340 +0 + 62 + 256 + 40 +0.24 +300 +I, O, Q, S, X, Z + 40 +0.48 + 90 + 3 + 40 +0.18 + 90 + 1 + 71 + 2 +340 +16 + 90 + 25 + 62 + 256 +340 +16 + 90 + 50 + 62 + 256 + 40 +0.24 + 40 +0.0 + 40 +0.24 + 71 + 3 +340 +11 + 62 + 256 + 40 +0.24 + 90 + 1 + 40 +0.75 + 90 + 1 +300 +%<\AcVar ViewType \f "%tc1">% %<\AcVar ViewSectionStartId>%-%<\AcVar ViewSectionEndId>%\PSCALE %<\AcVar ViewScale \f "%sn">% + 71 + 4 + 62 + 256 + 62 + 257 +300 +ANSI31 + 40 +1.0 + 90 + 0 +290 + 0 +290 + 0 + 90 + 6 + 40 +0.0 + 40 +1.570796326794896 + 40 +0.2617993877991494 + 40 +1.308996938995747 + 40 +-0.2617993877991494 + 40 +1.832595714594046 + 0 +TABLESTYLE + 5 +87 +102 +{ACAD_XDICTIONARY +360 +104 +102 +} +102 +{ACAD_REACTORS +330 +86 +102 +} +330 +86 +100 +AcDbTableStyle +280 + 0 + 3 +Standard + 70 + 0 + 71 + 0 + 40 +0.06 + 41 +0.06 +280 + 0 +281 + 0 + 7 +Standard +140 +0.18 +170 + 2 + 62 + 0 + 63 + 7 +283 + 0 + 90 + 512 + 91 + 0 + 1 + +274 + -2 +284 + 1 + 64 + 0 +275 + -2 +285 + 1 + 65 + 0 +276 + -2 +286 + 1 + 66 + 0 +277 + -2 +287 + 1 + 67 + 0 +278 + -2 +288 + 1 + 68 + 0 +279 + -2 +289 + 1 + 69 + 0 + 7 +Standard +140 +0.25 +170 + 5 + 62 + 0 + 63 + 7 +283 + 0 + 90 + 512 + 91 + 0 + 1 + +274 + -2 +284 + 1 + 64 + 0 +275 + -2 +285 + 1 + 65 + 0 +276 + -2 +286 + 1 + 66 + 0 +277 + -2 +287 + 1 + 67 + 0 +278 + -2 +288 + 1 + 68 + 0 +279 + -2 +289 + 1 + 69 + 0 + 7 +Standard +140 +0.18 +170 + 5 + 62 + 0 + 63 + 7 +283 + 0 + 90 + 512 + 91 + 0 + 1 + +274 + -2 +284 + 1 + 64 + 0 +275 + -2 +285 + 1 + 65 + 0 +276 + -2 +286 + 1 + 66 + 0 +277 + -2 +287 + 1 + 67 + 0 +278 + -2 +288 + 1 + 68 + 0 +279 + -2 +289 + 1 + 69 + 0 + 0 +VISUALSTYLE + 5 +9F +102 +{ACAD_REACTORS +330 +99 +102 +} +330 +99 +100 +AcDbVisualStyle + 2 +2dWireframe + 70 + 4 +177 + 3 +291 + 0 + 70 + 58 + 90 + 0 +176 + 1 + 90 + 2 +176 + 1 + 90 + 1 +176 + 1 + 90 + 0 +176 + 1 + 40 +0.6 +176 + 1 + 40 +30.0 +176 + 1 + 62 + 7 +420 + 16777215 +176 + 1 + 90 + 1 +176 + 1 + 90 + 4 +176 + 1 + 62 + 7 +176 + 1 + 62 + 257 +176 + 1 + 90 + 1 +176 + 1 + 90 + 1 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 257 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 1 +176 + 1 + 90 + 6 +176 + 1 + 90 + 2 +176 + 1 + 62 + 7 +176 + 1 + 90 + 5 +176 + 1 + 90 + 0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 1 +176 + 1 + 40 +0.0 +176 + 1 + 90 + 0 +176 + 1 +290 + 1 +176 + 1 +290 + 1 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 40 +0.0 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 18 +420 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 3 +176 + 1 + 62 + 5 +420 + 255 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 62 + 256 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 2 +176 + 1 + 1 +strokes_ogs.tif +176 + 1 +290 + 0 +176 + 1 + 40 +1.0 +176 + 1 + 40 +1.0 +176 + 1 + 0 +VISUALSTYLE + 5 +9E +102 +{ACAD_REACTORS +330 +99 +102 +} +330 +99 +100 +AcDbVisualStyle + 2 +Basic + 70 + 7 +177 + 3 +291 + 1 + 70 + 58 + 90 + 1 +176 + 1 + 90 + 0 +176 + 1 + 90 + 1 +176 + 1 + 90 + 0 +176 + 1 + 40 +0.6 +176 + 1 + 40 +30.0 +176 + 1 + 62 + 7 +420 + 16777215 +176 + 1 + 90 + 0 +176 + 1 + 90 + 4 +176 + 1 + 62 + 7 +176 + 1 + 62 + 257 +176 + 1 + 90 + 1 +176 + 1 + 90 + 1 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 8 +176 + 1 + 62 + 7 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 1 +176 + 1 + 90 + 6 +176 + 1 + 90 + 2 +176 + 1 + 62 + 7 +176 + 1 + 90 + 5 +176 + 1 + 90 + 0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 1 +176 + 1 + 40 +0.0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 1 +176 + 1 +290 + 1 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 40 +0.0 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 18 +420 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 3 +176 + 1 + 62 + 5 +420 + 255 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 62 + 256 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 2 +176 + 1 + 1 +strokes_ogs.tif +176 + 1 +290 + 0 +176 + 1 + 40 +1.0 +176 + 1 + 40 +1.0 +176 + 1 + 0 +VISUALSTYLE + 5 +A5 +102 +{ACAD_REACTORS +330 +99 +102 +} +330 +99 +100 +AcDbVisualStyle + 2 +Brighten + 70 + 12 +177 + 3 +291 + 1 + 70 + 58 + 90 + 2 +176 + 1 + 90 + 2 +176 + 1 + 90 + 1 +176 + 1 + 90 + 0 +176 + 1 + 40 +0.6 +176 + 1 + 40 +30.0 +176 + 1 + 62 + 7 +420 + 16777215 +176 + 1 + 90 + 1 +176 + 1 + 90 + 4 +176 + 1 + 62 + 7 +176 + 1 + 62 + 257 +176 + 1 + 90 + 1 +176 + 1 + 90 + 1 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 8 +176 + 1 + 62 + 7 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 1 +176 + 1 + 90 + 6 +176 + 1 + 90 + 2 +176 + 1 + 62 + 7 +176 + 1 + 90 + 5 +176 + 1 + 90 + 0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 1 +176 + 1 + 40 +50.0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 1 +176 + 1 +290 + 1 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 40 +0.0 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 18 +420 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 3 +176 + 1 + 62 + 5 +420 + 255 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 62 + 256 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 2 +176 + 1 + 1 +strokes_ogs.tif +176 + 1 +290 + 0 +176 + 1 + 40 +1.0 +176 + 1 + 40 +1.0 +176 + 1 + 0 +VISUALSTYLE + 5 +A9 +102 +{ACAD_REACTORS +330 +99 +102 +} +330 +99 +100 +AcDbVisualStyle + 2 +ColorChange + 70 + 16 +177 + 3 +291 + 1 + 70 + 58 + 90 + 2 +176 + 1 + 90 + 2 +176 + 1 + 90 + 3 +176 + 1 + 90 + 0 +176 + 1 + 40 +0.6 +176 + 1 + 40 +30.0 +176 + 1 + 62 + 8 +420 + 8421504 +176 + 1 + 90 + 1 +176 + 1 + 90 + 4 +176 + 1 + 62 + 7 +176 + 1 + 62 + 257 +176 + 1 + 90 + 1 +176 + 1 + 90 + 1 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 8 +176 + 1 + 62 + 8 +420 + 8421504 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 1 +176 + 1 + 90 + 6 +176 + 1 + 90 + 2 +176 + 1 + 62 + 7 +176 + 1 + 90 + 5 +176 + 1 + 90 + 0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 1 +176 + 1 + 40 +0.0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 1 +176 + 1 +290 + 1 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 40 +0.0 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 18 +420 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 3 +176 + 1 + 62 + 5 +420 + 255 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 62 + 256 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 2 +176 + 1 + 1 +strokes_ogs.tif +176 + 1 +290 + 0 +176 + 1 + 40 +1.0 +176 + 1 + 40 +1.0 +176 + 1 + 0 +VISUALSTYLE + 5 +A2 +102 +{ACAD_REACTORS +330 +99 +102 +} +330 +99 +100 +AcDbVisualStyle + 2 +Conceptual + 70 + 9 +177 + 3 +291 + 0 + 70 + 58 + 90 + 3 +176 + 1 + 90 + 2 +176 + 1 + 90 + 1 +176 + 1 + 90 + 0 +176 + 1 + 40 +0.6 +176 + 1 + 40 +30.0 +176 + 1 + 62 + 7 +420 + 16777215 +176 + 1 + 90 + 2 +176 + 1 + 90 + 2 +176 + 1 + 62 + 7 +176 + 1 + 62 + 257 +176 + 1 + 90 + 1 +176 + 1 + 90 + 1 +176 + 1 + 40 +40.0 +176 + 1 + 90 + 8 +176 + 1 + 62 + 7 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 1 +176 + 1 + 90 + 6 +176 + 1 + 90 + 2 +176 + 1 + 62 + 7 +176 + 1 + 90 + 3 +176 + 1 + 90 + 0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 1 +176 + 1 + 40 +0.0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 1 +176 + 1 +290 + 1 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 40 +0.0 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 18 +420 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 3 +176 + 1 + 62 + 5 +420 + 255 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 62 + 256 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 2 +176 + 1 + 1 +strokes_ogs.tif +176 + 1 +290 + 0 +176 + 1 + 40 +1.0 +176 + 1 + 40 +1.0 +176 + 1 + 0 +VISUALSTYLE + 5 +A4 +102 +{ACAD_REACTORS +330 +99 +102 +} +330 +99 +100 +AcDbVisualStyle + 2 +Dim + 70 + 11 +177 + 3 +291 + 1 + 70 + 58 + 90 + 2 +176 + 1 + 90 + 2 +176 + 1 + 90 + 1 +176 + 1 + 90 + 0 +176 + 1 + 40 +0.6 +176 + 1 + 40 +30.0 +176 + 1 + 62 + 7 +420 + 16777215 +176 + 1 + 90 + 1 +176 + 1 + 90 + 4 +176 + 1 + 62 + 7 +176 + 1 + 62 + 257 +176 + 1 + 90 + 1 +176 + 1 + 90 + 1 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 8 +176 + 1 + 62 + 7 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 1 +176 + 1 + 90 + 6 +176 + 1 + 90 + 2 +176 + 1 + 62 + 7 +176 + 1 + 90 + 5 +176 + 1 + 90 + 0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 1 +176 + 1 + 40 +-50.0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 1 +176 + 1 +290 + 1 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 40 +0.0 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 18 +420 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 3 +176 + 1 + 62 + 5 +420 + 255 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 62 + 256 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 2 +176 + 1 + 1 +strokes_ogs.tif +176 + 1 +290 + 0 +176 + 1 + 40 +1.0 +176 + 1 + 40 +1.0 +176 + 1 + 0 +VISUALSTYLE + 5 +175 +102 +{ACAD_REACTORS +330 +99 +102 +} +330 +99 +100 +AcDbVisualStyle + 2 +EdgeColorOff + 70 + 22 +177 + 3 +291 + 1 + 70 + 58 + 90 + 2 +176 + 0 + 90 + 2 +176 + 0 + 90 + 0 +176 + 0 + 90 + 0 +176 + 0 + 40 +0.6 +176 + 0 + 40 +30.0 +176 + 0 + 62 + 7 +420 + 16777215 +176 + 0 + 90 + 1 +176 + 0 + 90 + 4 +176 + 0 + 62 + 7 +176 + 0 + 62 + 257 +176 + 0 + 90 + 1 +176 + 0 + 90 + 1 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 8 +176 + 2 + 62 + 7 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 1 +176 + 0 + 90 + 6 +176 + 0 + 90 + 2 +176 + 0 + 62 + 7 +176 + 0 + 90 + 5 +176 + 0 + 90 + 0 +176 + 0 + 90 + 0 +176 + 0 +290 + 0 +176 + 0 + 90 + 1 +176 + 0 + 40 +0.0 +176 + 0 + 90 + 0 +176 + 0 +290 + 0 +176 + 1 +290 + 1 +176 + 1 +290 + 1 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 40 +0.0 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 18 +420 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 3 +176 + 1 + 62 + 5 +420 + 255 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 62 + 256 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 2 +176 + 1 + 1 +strokes_ogs.tif +176 + 1 +290 + 0 +176 + 1 + 40 +1.0 +176 + 1 + 40 +1.0 +176 + 1 + 0 +VISUALSTYLE + 5 +A8 +102 +{ACAD_REACTORS +330 +99 +102 +} +330 +99 +100 +AcDbVisualStyle + 2 +Facepattern + 70 + 15 +177 + 3 +291 + 1 + 70 + 58 + 90 + 2 +176 + 1 + 90 + 2 +176 + 1 + 90 + 1 +176 + 1 + 90 + 0 +176 + 1 + 40 +0.6 +176 + 1 + 40 +30.0 +176 + 1 + 62 + 7 +420 + 16777215 +176 + 1 + 90 + 1 +176 + 1 + 90 + 4 +176 + 1 + 62 + 7 +176 + 1 + 62 + 257 +176 + 1 + 90 + 1 +176 + 1 + 90 + 1 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 8 +176 + 1 + 62 + 7 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 1 +176 + 1 + 90 + 6 +176 + 1 + 90 + 2 +176 + 1 + 62 + 7 +176 + 1 + 90 + 5 +176 + 1 + 90 + 0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 1 +176 + 1 + 40 +0.0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 1 +176 + 1 +290 + 1 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 40 +0.0 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 18 +420 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 3 +176 + 1 + 62 + 5 +420 + 255 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 62 + 256 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 2 +176 + 1 + 1 +strokes_ogs.tif +176 + 1 +290 + 0 +176 + 1 + 40 +1.0 +176 + 1 + 40 +1.0 +176 + 1 + 0 +VISUALSTYLE + 5 +9A +102 +{ACAD_REACTORS +330 +99 +102 +} +330 +99 +100 +AcDbVisualStyle + 2 +Flat + 70 + 0 +177 + 3 +291 + 1 + 70 + 58 + 90 + 2 +176 + 1 + 90 + 1 +176 + 1 + 90 + 1 +176 + 1 + 90 + 2 +176 + 1 + 40 +0.6 +176 + 1 + 40 +30.0 +176 + 1 + 62 + 7 +420 + 16777215 +176 + 1 + 90 + 0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 7 +176 + 1 + 62 + 257 +176 + 1 + 90 + 1 +176 + 1 + 90 + 1 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 8 +176 + 1 + 62 + 7 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 1 +176 + 1 + 90 + 6 +176 + 1 + 90 + 2 +176 + 1 + 62 + 7 +176 + 1 + 90 + 5 +176 + 1 + 90 + 0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 13 +176 + 1 + 40 +0.0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 1 +176 + 1 +290 + 1 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 40 +0.0 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 18 +420 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 3 +176 + 1 + 62 + 5 +420 + 255 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 62 + 256 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 2 +176 + 1 + 1 +strokes_ogs.tif +176 + 1 +290 + 0 +176 + 1 + 40 +1.0 +176 + 1 + 40 +1.0 +176 + 1 + 0 +VISUALSTYLE + 5 +9B +102 +{ACAD_REACTORS +330 +99 +102 +} +330 +99 +100 +AcDbVisualStyle + 2 +FlatWithEdges + 70 + 1 +177 + 3 +291 + 1 + 70 + 58 + 90 + 2 +176 + 1 + 90 + 1 +176 + 1 + 90 + 1 +176 + 1 + 90 + 2 +176 + 1 + 40 +0.6 +176 + 1 + 40 +30.0 +176 + 1 + 62 + 7 +420 + 16777215 +176 + 1 + 90 + 1 +176 + 1 + 90 + 0 +176 + 1 + 62 + 7 +176 + 1 + 62 + 257 +176 + 1 + 90 + 1 +176 + 1 + 90 + 1 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 257 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 1 +176 + 1 + 90 + 6 +176 + 1 + 90 + 2 +176 + 1 + 62 + 7 +176 + 1 + 90 + 5 +176 + 1 + 90 + 0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 13 +176 + 1 + 40 +0.0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 1 +176 + 1 +290 + 1 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 40 +0.0 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 18 +420 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 3 +176 + 1 + 62 + 5 +420 + 255 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 62 + 256 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 2 +176 + 1 + 1 +strokes_ogs.tif +176 + 1 +290 + 0 +176 + 1 + 40 +1.0 +176 + 1 + 40 +1.0 +176 + 1 + 0 +VISUALSTYLE + 5 +9C +102 +{ACAD_REACTORS +330 +99 +102 +} +330 +99 +100 +AcDbVisualStyle + 2 +Gouraud + 70 + 2 +177 + 3 +291 + 1 + 70 + 58 + 90 + 2 +176 + 1 + 90 + 2 +176 + 1 + 90 + 1 +176 + 1 + 90 + 2 +176 + 1 + 40 +0.6 +176 + 1 + 40 +30.0 +176 + 1 + 62 + 7 +420 + 16777215 +176 + 1 + 90 + 0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 7 +176 + 1 + 62 + 257 +176 + 1 + 90 + 1 +176 + 1 + 90 + 1 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 7 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 1 +176 + 1 + 90 + 6 +176 + 1 + 90 + 2 +176 + 1 + 62 + 7 +176 + 1 + 90 + 5 +176 + 1 + 90 + 0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 13 +176 + 1 + 40 +0.0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 1 +176 + 1 +290 + 1 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 40 +0.0 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 18 +420 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 3 +176 + 1 + 62 + 5 +420 + 255 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 62 + 256 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 2 +176 + 1 + 1 +strokes_ogs.tif +176 + 1 +290 + 0 +176 + 1 + 40 +1.0 +176 + 1 + 40 +1.0 +176 + 1 + 0 +VISUALSTYLE + 5 +9D +102 +{ACAD_REACTORS +330 +99 +102 +} +330 +99 +100 +AcDbVisualStyle + 2 +GouraudWithEdges + 70 + 3 +177 + 3 +291 + 1 + 70 + 58 + 90 + 2 +176 + 1 + 90 + 2 +176 + 1 + 90 + 1 +176 + 1 + 90 + 2 +176 + 1 + 40 +0.6 +176 + 1 + 40 +30.0 +176 + 1 + 62 + 7 +420 + 16777215 +176 + 1 + 90 + 1 +176 + 1 + 90 + 0 +176 + 1 + 62 + 7 +176 + 1 + 62 + 257 +176 + 1 + 90 + 1 +176 + 1 + 90 + 1 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 257 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 1 +176 + 1 + 90 + 6 +176 + 1 + 90 + 2 +176 + 1 + 62 + 7 +176 + 1 + 90 + 5 +176 + 1 + 90 + 0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 13 +176 + 1 + 40 +0.0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 1 +176 + 1 +290 + 1 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 40 +0.0 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 18 +420 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 3 +176 + 1 + 62 + 5 +420 + 255 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 62 + 256 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 2 +176 + 1 + 1 +strokes_ogs.tif +176 + 1 +290 + 0 +176 + 1 + 40 +1.0 +176 + 1 + 40 +1.0 +176 + 1 + 0 +VISUALSTYLE + 5 +A1 +102 +{ACAD_REACTORS +330 +99 +102 +} +330 +99 +100 +AcDbVisualStyle + 2 +Hidden + 70 + 6 +177 + 3 +291 + 0 + 70 + 58 + 90 + 1 +176 + 1 + 90 + 2 +176 + 1 + 90 + 2 +176 + 1 + 90 + 0 +176 + 1 + 40 +0.6 +176 + 1 + 40 +30.0 +176 + 1 + 62 + 7 +420 + 16777215 +176 + 1 + 90 + 2 +176 + 1 + 90 + 2 +176 + 1 + 62 + 7 +176 + 1 + 62 + 257 +176 + 1 + 90 + 2 +176 + 1 + 90 + 1 +176 + 1 + 40 +40.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 257 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 1 +176 + 1 + 90 + 6 +176 + 1 + 90 + 2 +176 + 1 + 62 + 7 +176 + 1 + 90 + 3 +176 + 1 + 90 + 0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 1 +176 + 1 + 40 +0.0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 1 +176 + 1 +290 + 1 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 40 +0.0 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 18 +420 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 3 +176 + 1 + 62 + 5 +420 + 255 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 62 + 256 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 2 +176 + 1 + 1 +strokes_ogs.tif +176 + 1 +290 + 0 +176 + 1 + 40 +1.0 +176 + 1 + 40 +1.0 +176 + 1 + 0 +VISUALSTYLE + 5 +173 +102 +{ACAD_REACTORS +330 +99 +102 +} +330 +99 +100 +AcDbVisualStyle + 2 +JitterOff + 70 + 20 +177 + 3 +291 + 1 + 70 + 58 + 90 + 2 +176 + 0 + 90 + 2 +176 + 0 + 90 + 0 +176 + 0 + 90 + 0 +176 + 0 + 40 +0.6 +176 + 0 + 40 +30.0 +176 + 0 + 62 + 7 +420 + 16777215 +176 + 0 + 90 + 1 +176 + 0 + 90 + 4 +176 + 0 + 62 + 7 +176 + 0 + 62 + 257 +176 + 0 + 90 + 1 +176 + 0 + 90 + 1 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 10 +176 + 2 + 62 + 7 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 1 +176 + 0 + 90 + 6 +176 + 0 + 90 + 2 +176 + 0 + 62 + 7 +176 + 0 + 90 + 5 +176 + 0 + 90 + 0 +176 + 0 + 90 + 0 +176 + 0 +290 + 0 +176 + 0 + 90 + 1 +176 + 0 + 40 +0.0 +176 + 0 + 90 + 0 +176 + 0 +290 + 0 +176 + 1 +290 + 1 +176 + 1 +290 + 1 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 40 +0.0 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 18 +420 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 3 +176 + 1 + 62 + 5 +420 + 255 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 62 + 256 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 2 +176 + 1 + 1 +strokes_ogs.tif +176 + 1 +290 + 0 +176 + 1 + 40 +1.0 +176 + 1 + 40 +1.0 +176 + 1 + 0 +VISUALSTYLE + 5 +A7 +102 +{ACAD_REACTORS +330 +99 +102 +} +330 +99 +100 +AcDbVisualStyle + 2 +Linepattern + 70 + 14 +177 + 3 +291 + 1 + 70 + 58 + 90 + 2 +176 + 1 + 90 + 2 +176 + 1 + 90 + 1 +176 + 1 + 90 + 0 +176 + 1 + 40 +0.6 +176 + 1 + 40 +30.0 +176 + 1 + 62 + 7 +420 + 16777215 +176 + 1 + 90 + 1 +176 + 1 + 90 + 4 +176 + 1 + 62 + 7 +176 + 1 + 62 + 257 +176 + 1 + 90 + 7 +176 + 1 + 90 + 7 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 8 +176 + 1 + 62 + 7 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 1 +176 + 1 + 90 + 6 +176 + 1 + 90 + 2 +176 + 1 + 62 + 7 +176 + 1 + 90 + 5 +176 + 1 + 90 + 0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 1 +176 + 1 + 40 +0.0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 1 +176 + 1 +290 + 1 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 40 +0.0 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 18 +420 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 3 +176 + 1 + 62 + 5 +420 + 255 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 62 + 256 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 2 +176 + 1 + 1 +strokes_ogs.tif +176 + 1 +290 + 0 +176 + 1 + 40 +1.0 +176 + 1 + 40 +1.0 +176 + 1 + 0 +VISUALSTYLE + 5 +174 +102 +{ACAD_REACTORS +330 +99 +102 +} +330 +99 +100 +AcDbVisualStyle + 2 +OverhangOff + 70 + 21 +177 + 3 +291 + 1 + 70 + 58 + 90 + 2 +176 + 0 + 90 + 2 +176 + 0 + 90 + 0 +176 + 0 + 90 + 0 +176 + 0 + 40 +0.6 +176 + 0 + 40 +30.0 +176 + 0 + 62 + 7 +420 + 16777215 +176 + 0 + 90 + 1 +176 + 0 + 90 + 4 +176 + 0 + 62 + 7 +176 + 0 + 62 + 257 +176 + 0 + 90 + 1 +176 + 0 + 90 + 1 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 9 +176 + 2 + 62 + 7 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 1 +176 + 0 + 90 + 6 +176 + 0 + 90 + 2 +176 + 0 + 62 + 7 +176 + 0 + 90 + 5 +176 + 0 + 90 + 0 +176 + 0 + 90 + 0 +176 + 0 +290 + 0 +176 + 0 + 90 + 1 +176 + 0 + 40 +0.0 +176 + 0 + 90 + 0 +176 + 0 +290 + 0 +176 + 1 +290 + 1 +176 + 1 +290 + 1 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 40 +0.0 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 18 +420 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 3 +176 + 1 + 62 + 5 +420 + 255 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 62 + 256 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 2 +176 + 1 + 1 +strokes_ogs.tif +176 + 1 +290 + 0 +176 + 1 + 40 +1.0 +176 + 1 + 40 +1.0 +176 + 1 + 0 +VISUALSTYLE + 5 +A3 +102 +{ACAD_REACTORS +330 +99 +102 +} +330 +99 +100 +AcDbVisualStyle + 2 +Realistic + 70 + 8 +177 + 3 +291 + 0 + 70 + 58 + 90 + 2 +176 + 1 + 90 + 3 +176 + 1 + 90 + 0 +176 + 1 + 90 + 2 +176 + 1 + 40 +0.6 +176 + 1 + 40 +30.0 +176 + 1 + 62 + 7 +420 + 16777215 +176 + 1 + 90 + 0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 7 +176 + 1 + 62 + 257 +176 + 1 + 90 + 1 +176 + 1 + 90 + 1 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 8 +176 + 1 + 62 + 257 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 1 +176 + 1 + 90 + 6 +176 + 1 + 90 + 2 +176 + 1 + 62 + 7 +176 + 1 + 90 + 3 +176 + 1 + 90 + 0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 13 +176 + 1 + 40 +0.0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 1 +176 + 1 +290 + 1 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 40 +0.0 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 18 +420 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 3 +176 + 1 + 62 + 5 +420 + 255 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 62 + 256 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 2 +176 + 1 + 1 +strokes_ogs.tif +176 + 1 +290 + 0 +176 + 1 + 40 +1.0 +176 + 1 + 40 +1.0 +176 + 1 + 0 +VISUALSTYLE + 5 +182 +102 +{ACAD_REACTORS +330 +99 +102 +} +330 +99 +100 +AcDbVisualStyle + 2 +Shaded + 70 + 27 +177 + 3 +291 + 0 + 70 + 58 + 90 + 2 +176 + 1 + 90 + 2 +176 + 1 + 90 + 1 +176 + 1 + 90 + 2 +176 + 1 + 40 +0.6 +176 + 1 + 40 +30.0 +176 + 1 + 62 + 7 +420 + 16777215 +176 + 1 + 90 + 0 +176 + 1 + 90 + 4 +176 + 1 + 62 + 7 +176 + 1 + 62 + 257 +176 + 1 + 90 + 1 +176 + 1 + 90 + 1 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 8 +176 + 1 + 62 + 257 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 1 +176 + 1 + 90 + 6 +176 + 1 + 90 + 2 +176 + 1 + 62 + 8 +420 + 7895160 +176 + 1 + 90 + 3 +176 + 1 + 90 + 0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 5 +176 + 1 + 40 +0.0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 1 +176 + 1 +290 + 1 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 40 +0.0 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 18 +420 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 3 +176 + 1 + 62 + 5 +420 + 255 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 62 + 256 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 2 +176 + 1 + 1 +strokes_ogs.tif +176 + 1 +290 + 0 +176 + 1 + 40 +1.0 +176 + 1 + 40 +1.0 +176 + 1 + 0 +VISUALSTYLE + 5 +181 +102 +{ACAD_REACTORS +330 +99 +102 +} +330 +99 +100 +AcDbVisualStyle + 2 +Shaded with edges + 70 + 26 +177 + 3 +291 + 0 + 70 + 58 + 90 + 2 +176 + 1 + 90 + 2 +176 + 1 + 90 + 1 +176 + 1 + 90 + 2 +176 + 1 + 40 +0.6 +176 + 1 + 40 +30.0 +176 + 1 + 62 + 7 +420 + 16777215 +176 + 1 + 90 + 1 +176 + 1 + 90 + 10 +176 + 1 + 62 + 7 +176 + 1 + 62 + 257 +176 + 1 + 90 + 2 +176 + 1 + 90 + 1 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 8 +176 + 1 + 62 + 257 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 1 +176 + 1 + 90 + 6 +176 + 1 + 90 + 2 +176 + 1 + 62 + 7 +176 + 1 + 90 + 3 +176 + 1 + 90 + 0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 5 +176 + 1 + 40 +0.0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 1 +176 + 1 +290 + 1 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 40 +0.0 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 18 +420 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 3 +176 + 1 + 62 + 5 +420 + 255 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 62 + 256 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 2 +176 + 1 + 1 +strokes_ogs.tif +176 + 1 +290 + 0 +176 + 1 + 40 +1.0 +176 + 1 + 40 +1.0 +176 + 1 + 0 +VISUALSTYLE + 5 +17E +102 +{ACAD_REACTORS +330 +99 +102 +} +330 +99 +100 +AcDbVisualStyle + 2 +Shades of Gray + 70 + 23 +177 + 3 +291 + 0 + 70 + 58 + 90 + 2 +176 + 1 + 90 + 2 +176 + 1 + 90 + 3 +176 + 1 + 90 + 0 +176 + 1 + 40 +0.6 +176 + 1 + 40 +30.0 +176 + 1 + 62 + 7 +420 + 16777215 +176 + 1 + 90 + 2 +176 + 1 + 90 + 2 +176 + 1 + 62 + 7 +176 + 1 + 62 + 7 +176 + 1 + 90 + 1 +176 + 1 + 90 + 1 +176 + 1 + 40 +40.0 +176 + 1 + 90 + 8 +176 + 1 + 62 + 7 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 1 +176 + 1 + 90 + 6 +176 + 1 + 90 + 2 +176 + 1 + 62 + 7 +176 + 1 + 90 + 3 +176 + 1 + 90 + 0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 1 +176 + 1 + 40 +0.0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 1 +176 + 1 +290 + 1 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 40 +0.0 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 18 +420 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 3 +176 + 1 + 62 + 5 +420 + 255 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 62 + 256 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 2 +176 + 1 + 1 +strokes_ogs.tif +176 + 1 +290 + 0 +176 + 1 + 40 +1.0 +176 + 1 + 40 +1.0 +176 + 1 + 0 +VISUALSTYLE + 5 +17F +102 +{ACAD_REACTORS +330 +99 +102 +} +330 +99 +100 +AcDbVisualStyle + 2 +Sketchy + 70 + 24 +177 + 3 +291 + 0 + 70 + 58 + 90 + 1 +176 + 1 + 90 + 2 +176 + 1 + 90 + 2 +176 + 1 + 90 + 0 +176 + 1 + 40 +0.6 +176 + 1 + 40 +30.0 +176 + 1 + 62 + 7 +420 + 16777215 +176 + 1 + 90 + 2 +176 + 1 + 90 + 2 +176 + 1 + 62 + 7 +176 + 1 + 62 + 7 +176 + 1 + 90 + 1 +176 + 1 + 90 + 1 +176 + 1 + 40 +40.0 +176 + 1 + 90 + 11 +176 + 1 + 62 + 7 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 1 +176 + 1 + 90 + 6 +176 + 1 + 90 + 2 +176 + 1 + 62 + 7 +176 + 1 + 90 + 6 +176 + 1 + 90 + 0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 1 +176 + 1 + 40 +0.0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 1 +176 + 1 +290 + 1 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 40 +0.0 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 18 +420 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 3 +176 + 1 + 62 + 5 +420 + 255 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 62 + 256 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 2 +176 + 1 + 1 +strokes_ogs.tif +176 + 1 +290 + 0 +176 + 1 + 40 +1.0 +176 + 1 + 40 +1.0 +176 + 1 + 0 +VISUALSTYLE + 5 +A6 +102 +{ACAD_REACTORS +330 +99 +102 +} +330 +99 +100 +AcDbVisualStyle + 2 +Thicken + 70 + 13 +177 + 3 +291 + 1 + 70 + 58 + 90 + 2 +176 + 1 + 90 + 2 +176 + 1 + 90 + 1 +176 + 1 + 90 + 0 +176 + 1 + 40 +0.6 +176 + 1 + 40 +30.0 +176 + 1 + 62 + 7 +420 + 16777215 +176 + 1 + 90 + 1 +176 + 1 + 90 + 4 +176 + 1 + 62 + 7 +176 + 1 + 62 + 257 +176 + 1 + 90 + 1 +176 + 1 + 90 + 1 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 12 +176 + 1 + 62 + 7 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 1 +176 + 1 + 90 + 6 +176 + 1 + 90 + 2 +176 + 1 + 62 + 7 +176 + 1 + 90 + 5 +176 + 1 + 90 + 0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 1 +176 + 1 + 40 +0.0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 1 +176 + 1 +290 + 1 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 40 +0.0 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 18 +420 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 3 +176 + 1 + 62 + 5 +420 + 255 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 62 + 256 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 2 +176 + 1 + 1 +strokes_ogs.tif +176 + 1 +290 + 0 +176 + 1 + 40 +1.0 +176 + 1 + 40 +1.0 +176 + 1 + 0 +VISUALSTYLE + 5 +A0 +102 +{ACAD_REACTORS +330 +99 +102 +} +330 +99 +100 +AcDbVisualStyle + 2 +Wireframe + 70 + 5 +177 + 3 +291 + 0 + 70 + 58 + 90 + 0 +176 + 1 + 90 + 2 +176 + 1 + 90 + 0 +176 + 1 + 90 + 0 +176 + 1 + 40 +0.6 +176 + 1 + 40 +30.0 +176 + 1 + 62 + 7 +420 + 16777215 +176 + 1 + 90 + 1 +176 + 1 + 90 + 4 +176 + 1 + 62 + 7 +176 + 1 + 62 + 257 +176 + 1 + 90 + 1 +176 + 1 + 90 + 1 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 257 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 1 +176 + 1 + 90 + 6 +176 + 1 + 90 + 2 +176 + 1 + 62 + 7 +176 + 1 + 90 + 3 +176 + 1 + 90 + 0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 1 +176 + 1 + 40 +0.0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 1 +176 + 1 +290 + 1 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 40 +0.0 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 18 +420 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 3 +176 + 1 + 62 + 5 +420 + 255 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 62 + 256 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 2 +176 + 1 + 1 +strokes_ogs.tif +176 + 1 +290 + 0 +176 + 1 + 40 +1.0 +176 + 1 + 40 +1.0 +176 + 1 + 0 +VISUALSTYLE + 5 +180 +102 +{ACAD_REACTORS +330 +99 +102 +} +330 +99 +100 +AcDbVisualStyle + 2 +X-Ray + 70 + 25 +177 + 3 +291 + 0 + 70 + 58 + 90 + 2 +176 + 1 + 90 + 2 +176 + 1 + 90 + 1 +176 + 1 + 90 + 1 +176 + 1 + 40 +0.5 +176 + 1 + 40 +30.0 +176 + 1 + 62 + 7 +420 + 16777215 +176 + 1 + 90 + 1 +176 + 1 + 90 + 0 +176 + 1 + 62 + 7 +176 + 1 + 62 + 257 +176 + 1 + 90 + 1 +176 + 1 + 90 + 1 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 8 +176 + 1 + 62 + 7 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 1 +176 + 1 + 90 + 6 +176 + 1 + 90 + 2 +176 + 1 + 62 + 7 +176 + 1 + 90 + 3 +176 + 1 + 90 + 0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 13 +176 + 1 + 40 +0.0 +176 + 1 + 90 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 1 +176 + 1 +290 + 1 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 40 +0.0 +176 + 1 + 40 +1.0 +176 + 1 + 90 + 0 +176 + 1 + 62 + 18 +420 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 3 +176 + 1 + 62 + 5 +420 + 255 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 + 90 + 50 +176 + 1 +290 + 0 +176 + 1 + 90 + 50 +176 + 1 + 62 + 256 +176 + 0 + 40 +1.0 +176 + 0 + 90 + 2 +176 + 1 + 1 +strokes_ogs.tif +176 + 1 +290 + 0 +176 + 1 + 40 +1.0 +176 + 1 + 40 +1.0 +176 + 1 + 0 +DICTIONARYVAR + 5 +F0 +102 +{ACAD_REACTORS +330 +66 +102 +} +330 +66 +100 +DictionaryVariables +280 + 0 + 1 +1:1 + 0 +DICTIONARYVAR + 5 +EF +102 +{ACAD_REACTORS +330 +66 +102 +} +330 +66 +100 +DictionaryVariables +280 + 0 + 1 +STANDARD + 0 +DICTIONARYVAR + 5 +89 +102 +{ACAD_REACTORS +330 +66 +102 +} +330 +66 +100 +DictionaryVariables +280 + 0 + 1 +STANDARD + 0 +DICTIONARYVAR + 5 +1B7 +102 +{ACAD_REACTORS +330 +66 +102 +} +330 +66 +100 +DictionaryVariables +280 + 0 + 1 +Imperial24 + 0 +DICTIONARYVAR + 5 +1B8 +102 +{ACAD_REACTORS +330 +66 +102 +} +330 +66 +100 +DictionaryVariables +280 + 0 + 1 +Imperial24 + 0 +DICTIONARYVAR + 5 +67 +102 +{ACAD_REACTORS +330 +66 +102 +} +330 +66 +100 +DictionaryVariables +280 + 0 + 1 +2 + 0 +DICTIONARYVAR + 5 +6B +102 +{ACAD_REACTORS +330 +66 +102 +} +330 +66 +100 +DictionaryVariables +280 + 0 + 1 +1 + 0 +DICTIONARYVAR + 5 +14D +102 +{ACAD_REACTORS +330 +66 +102 +} +330 +66 +100 +DictionaryVariables +280 + 0 + 1 +0 + 0 +DICTIONARYVAR + 5 +14E +102 +{ACAD_REACTORS +330 +66 +102 +} +330 +66 +100 +DictionaryVariables +280 + 0 + 1 +0 + 0 +DICTIONARY + 5 +1C6 +330 +1AC +100 +AcDbDictionary +280 + 1 +281 + 1 + 3 +ACAD_XREC_ROUNDTRIP +360 +35B + 0 +DICTIONARY + 5 +194 +330 +22 +100 +AcDbDictionary +280 + 1 +281 + 1 + 0 +DICTIONARY + 5 +188 +330 +97 +100 +AcDbDictionary +280 + 1 +281 + 1 + 3 +FBXASSET +360 +189 + 0 +DICTIONARY + 5 +186 +330 +96 +100 +AcDbDictionary +280 + 1 +281 + 1 + 3 +FBXASSET +360 +187 + 0 +DICTIONARY + 5 +110 +330 +98 +100 +AcDbDictionary +280 + 1 +281 + 1 + 3 +BUMPTILE +360 +112 + 3 +DIFFUSETILE +360 +111 + 3 +FBXASSET +360 +18A + 3 +OPACITYTILE +360 +113 + 3 +REFLECTIONTILE +360 +114 + 0 +DICTIONARY + 5 +1C4 +330 +1AA +100 +AcDbDictionary +280 + 1 +281 + 1 + 3 +ACAD_XREC_ROUNDTRIP +360 +35A + 0 +DICTIONARY + 5 +104 +330 +87 +100 +AcDbDictionary +280 + 1 +281 + 1 + 3 +ACAD_ROUNDTRIP_2008_TABLESTYLE_CELLSTYLEMAP +360 +359 + 0 +XRECORD + 5 +35B +102 +{ACAD_REACTORS +330 +1C6 +102 +} +330 +1C6 +100 +AcDbXrecord +280 + 1 +102 +DISPLAYNAME + 1 +Imperial24 +102 +FLAGS + 90 + 0 + 0 +XRECORD + 5 +189 +102 +{ACAD_REACTORS +330 +188 +102 +} +330 +188 +100 +AcDbXrecord +280 + 1 + 70 + 1 + 90 +429727718 + 1 +508B35C9-AEE7-456F-BE74-D52651303AC6 +310 +504B03040A0000080000B670623B3DEE336E79000000790000001B0000006175746F6465736B2D64657369676E2D7061636B6167652E786D6C3C3F786D6C2076657273696F6E3D22312E302220656E636F64696E673D227574662D3822203F3E3C666F726D6174733E3C666F726D61743E687474703A2F2F736368656D612E +310 +6175746F6465736B2E636F6D2F64657369676E2D7061636B6167652F323030393C2F666F726D61743E3C2F666F726D6174733E504B0304140006080800B670623BD486FD9CA0000000F4000000130000005B436F6E74656E745F54797065735D2E786D6C7D8EC10E82300C865F65E91D8A1E8C310C0EEA1BF002731658846E +310 +D98AC1B77784ABF1D8FE5FBFBF75BBCE937A534CCEB38643598122B6FEE978D0B0485F9C41B54DDD7D022595594E1A469170414C76A4D9A4D207E29CF43ECE46F218070CC6BECC4078ACAA135ACF422C856C0E68EA1BF5669944DDD7BCDE7B1F8E415D77AECB980613C2E4AC91FC167A2B244592486606FC29C8FD7F045B9A +310 +EF7053A7E60B504B0304140006080800B670623B28ED286BEB0000007C01000008000000636F72652E786D6C8D90416EC3201045AF82665B6183218D6D61A254764FD00B204C5C94182C03558F5F9C3852BBEB6A467FFE9B197D71FA9E6FE8CBACC17AD7012D0820E3B41FAD9B3A48F1826B40272956EFE32F1B20A766D3C1 +310 +268314D3EAD3B24BDABB685CCCAAF3A3D94515828980F42D371D04FD696655A814B3235C8B45E9AB9A4C419F5D8653B2237A1CB163DE672FD6AC209BF3C08E0D1B30EB19C3FC9D72FCC6EB231EFA33EDE96BDD341517E5064B11EDFCBCAF57A3A2194156843498524CAA0FCA5B7268F9E185D42D21A2DCEC7FA0B48CFF8492 +310 +759155FBBB0F2C80A4F991FB408A72CB22977B50B96EB9C91F504B0304140006080800B670623B55019EA25F0000007C00000007000000636E782E786D6C4DCB3B0E80201045D1AD90E915ED2CF8AC45050C11660C8261F98AB1B07AC9C97D42D718D865D3E909258CFD00CCE24AC6E326A164D74DC0B4128928FF32603847 +310 +2BA13128B1252AC747C6BAB984FC68F0B87FE8960A5C09FE86CFB69FBA01504B0304140006080800B670623B7BC41AC871010000D40300000C0000006662782F636F72652E786D6CC553ED6E8320147D15C2DF4501B56B6D90A64BBB27D80B50A5CE54C120983EFEAE1F4DD7A56BBA5FF3879273EEE59E7310BE393735EA95 +310 +ED2AA333CC428A91D2B9292A5D66D8BB63B0C26823B835C67D2BC348CB46657880B1E0A535BE9DA1DC68A7B403549B42CDA0EC3AE530CA6B5864B8CB3F552343E91D5474A7B095F949962A6421D0502778E72C0840D390E18DC5F170E664C205F7BE2A66B62A605A75AC94C522DDEEE3651AEF837817C741F2CE92E02D592D +310 +83FD6ECB76EC7595A651C2C9D02CB8AB9A8BBADC2AE954814544691A3016D0E883256BBA58278B17BA5A53CAC9507ED3E4DBE2C9265F691747B3DCA9ADC382819091F8E1162272A8933DE82182932144F88C09DF060D8140C84E1EEA8B8F1340BDCA9DB1093085F157AA97B557309586747CD0EF0B4EA64E983A6E0EAEEFCE +310 +889E9EF17847386AA94B30211D2C0FDE0D3AF9A4610EAD5536E8DD1969631B593F67E381C33F5157F17725E5A636F6BF1491F980C8FC7B90E13E8A2F504B0304140006080800B670623B70102A487D0000009D0000000B0000006662782F636E782E786D6C4DCCD10A83201886E15B91FFDC95A5C30335ACCDFB886521330D +310 +A76397BF1A1DECE883878F57749FD5A3B74D2F17830472A901D9F088930B8B849267CC01754AA418F3DF0D5018572BE160506249B16C274D761E8BCFBB7A179E27F65CB39A0F14D396184C9921B81F34C3ACA1B4B99ABB6E6F062A25AA5F69DF23ACBE504B0304140006080800B670623B3610CA3329010000120200003100 +310 +00006662782F42384135303843342D343331462D343546312D424341352D3532343432364645413344462F636F72652E786D6C8D515B6EC32010BC0AE22B55850D0E446E848992B43E412FE000495162B07854397EA176A4F62F3F2C9A9D991D58BEBB8F37F0AD7D30CE76905418026DA553C65E3A98E219B510EC04F7CEC5 +310 +3F3408EC30EA0E16180A7EF12E4D0B249D8DDAC68C5AA7F4020E21E80881BCE54B0783FCD2E3500D296646B856D320AFC34557A4CAEDCC133C449F0380794839A138B47B86DB2345744D7A44594FD0E1B867883594369BFE63BF7EEF793D0B054FC9A8456E548E63CE46FB674D8A58F068C6477CE9F510B582A2C1F80D1182 +310 +70F349E816B32D65AFB8DD62CCEB42FF274A937A52948C8DEB66893BCB021484D7734370E5D2E9F608A3EF93F3119479ABA0E50B14B8C298119A43CCC4FCFCECB8A18B6388CEE7EF5D5D4F994BD86CBBA182D76543B9FCAE2FD7B24DF103504B0304140006080800B670623B1B8617914E0100003303000030000000666278 +310 +2F42384135303843342D343331462D343546312D424341352D3532343432364645413344462F636E782E786D6CD593C16EC32010447F0571AD6CE3D489D20813A9877E410FBD62D82434182CC055F3F75D1292E692438FBD206B7698593D61BEFD1E2DF982108D773D6D6B460938E5B571FB9ECE6957AD29D90A1EBC4F7736 +310 +4A9C1CA1A759A682EF839FA72269D8C9D92654AD71C72206887E0E0A22CAF926B15E498B01E0AA19451807D01A34B919F16B070157417138917400F2F6FA41FCF0092A45DEE414C19BDC21783E4BD39D891265658C3D8DEA00A3ACE59CBC8678ACB54CB26EEB6BD7839D7E9348BE70AD9C8321B9BCA7F9A4A26C542347DEE0 +310 +50F09802D22BA6749A800A394DD62899107273365E3C82CFC6A55557BC06C10774B7ABE7AEC3B0F34CF06446280E154026D0542C187BA9DAB6628BF7B6DBB0E5A65B3EB1F58631DE64BBE083F7B65CC218087E22CABB042E51D1F2268F1FE12B3EA24D40D63E9CFE00120194D60B9D5BC67FE6D39CDF37E20AF817881F504B +310 +0304140006080800B670623BBFEF2E3B690500003C100000340000006662782F42384135303843342D343331462D343546312D424341352D3532343432364645413344462F6F626A656374732E786D6CAD575D739B3814FD2B0CFB0C98D84E931D4227B6938EA793B6534F3BFB90198F0099B001899144EAECAFDF231018B0 +310 +D37A3FFC609090EEE7B94757C1FB7D915B2F54C88CB31BDB7727B64559CC938CA53776A576CE956DBD0F03C1B9EA2DB32D460A7A63EB693B0C52C1ABD24C257447AA5C8D66EF177F5832A68CDA569C13296F6C193FD182B8A4523CA1F2D94D88226EBADBBBBEDB2C1C8A4D05299F46424B222853B014F30C52ACC6287F389C +310 +D85E1878FA7B18E87FB36AFAF62AAFF667A83F635211161F291B8999FAB3D5DDC5FCDA99CF6653677679E13B8BFBC5CAF1DF4D27CBCBE5F2FAF6EE1A8AA51290642C117467871F101B91C581D77CEA2C36869F3229E68CD1B8F61F1ED6361B89092D294B90C78C4A68EB793D190E7D1D9BDEE7A91E1B95FDE9F3BCEAEFE809 +310 +6A6D6F9F7D4B79F4275C90E7A04248AA51D5C4C738DAE06F72885AC4796EBE65D22A88A22223B995679120E2D50EB1542F098381188D4EA111AE3DB043FF20AF6F2B4AE3D7D83D65E5AECA730D4D3BAC91BD05FEA992B565D95F549CD6069B90AB2A63EACA38A45E4B6A87D781574F86817E4C2FCCC72690D62E2729520E37 +310 +9BAF234797BAF6D62B3BFC781FED37BA1E4F2B2F052FA9501A3E80569BB8F6A9A364D4FA67654E6F803383981F82F21591FF84F23D9882BC896C3FDBCF8C162508933B2E0AE4C69DD43F6BD2BE38DDDBF10BF86CB4FA784D37F3AF16075E67EC9030DECA1FC0A55373F5DFF3378C591FA9BDF485C10B0A8C8BA909E5262639 +310 +A8E72163488806A595734C81CB77D1DE0ED7604A303B76913CEC0272F412782FF88EFF4636D8AA56DF7B322474C4B767303F625997056C7B032C473C592B36CEBD15F3F9FF5233B7ABCDC74D257624A60F8658567497B14CE1EC3C80B76FD02011038F9EE9EB0F2E12387A9C844D3F09855125116D1D731DF8B43D2CF4E0A0 +310 +79A020C6BE946BCE0B7EA1E1219331CD73C228AF5A2DDD31A4B1DA918CE91350854D40A71723140BFA92E94E62B0626016CEFA5864A50E5977E8753CEDBEE1CBB7F52780D20E17AF0BA0F5F9B02A3AD07DCC732EB6D1EBB661421BE0C39E21BABF0FE16D22EA6929407C8DE596724C88B749B6DB5512BC7B228ADFD7CB7EA6 +310 +3A22397E690BA8D5D8D4E40CEC8CDA23C2C0B7D5898640101CEC9C6D331CE328CB93CAFBBAC70A642DF70DF95155945B52F08AA973C23469FD69ADEF84F7BB18F983A8F8C90E37F5D35980B2139A1C523540813E824EF83448CF7C72B598CE97D7CEEDDDDD3B6736BFBC771677EF66CE6A7E7139F7A793E9EDF2B2B3A8B624 +310 +0CDA13AA5F8388264EC4A0129909F3B7F5A16CEDF08128E97D680ACA4B9BE7B7B58BB638F0B00700EF36AE8B32A705658A68F83EE897FC2B41573190D1CCAF0B92D233A47CFEB019D9809933F67DD1A5467F8CF69AD9A5AE859342D45355448C64F968A371DC2D596ADC6E43D93D1B466F877A64E2893E54F23C4B40388995 +310 +525E5005DA3983ED45412510D3EFCA13737B3863F74BD30F2A12E5B4B5E489A025CE25500DB855B11ACE5BBFA185D35DD48D8DC3CE7DC69F2944DB42AA6F6CF09BCA622ADD08C7A36E7C6AE18000BAB08E040135C2D21C9D75B3A91B77BB746C403F38018DFE6EC939DD92F16B40BC05294B9CDA035AAD6D330AE27FE4786B +310 +8E45145EA34A75BE1C263A676A35A8ACC6297337400BF16C54EF515F3F77CB77EB3583889C754FFA29081AA98E2222ADEF05DA3E63D2EDEA8B8756C05A7FB6CABC4A91CBF325398202CC952E70A008A830324BA200D5E5EF8F5F04C75DB4B0EE3340E071C98B823333B83577596BF3849B69F2085AA0FAFA212F26BEFF5877 +310 +FDE622B28D88A42E49E43326DA82D31E8CB006401E1DA726059ECEC168F97A6599036306A135688F9AD2E61A6EA178F4CD51974A9F2CCB9F36FE5EBD14601068D9C3BF01504B0304140006080000B670623B000000000000000000000000350000006662782F42384135303843342D343331462D343546312D424341352D35 +310 +32343432364645413344462F76657274696365732E62696E504B0304140006080800B670623BF270F1330600000004000000360000006662782F42384135303843342D343331462D343546312D424341352D3532343432364645413344462F747269616E676C65732E62696E636660600000504B0304140006080000B67062 +310 +3B000000000000000000000000370000006662782F42384135303843342D343331462D343546312D424341352D3532343432364645413344462F617474726962757465732E62696E504B0304140006080800B670623B08DA0BD19000000041010000360000006662782F42384135303843342D343331462D343546312D4243 +310 +41352D3532343432364645413344462F6469726563746F72792E786D6C858F410AC3201045AF22B36F53E9A60B35CBEEBAE80D244E44489C6234F4F835464A48035D0DF398FF9F8AF63D0E6CC63039F212F8F9020C7D47C6792B21C5FE7403D62A1188E2E60C98D7234A5830286103A55745067B9D86B8A3CF9C7F90C11DE6 +310 +07FB144396D7B6A82D282E9A152AD114D5EF2CB866EEE831B8EEA07A7BB5A8D7D61A2BAAEB7FD5F70921FF497D00504B0304140006080800B670623B3B4606F3FE000000A00100003B0000006662782F42384135303843342D343331462D343546312D424341352D3532343432364645413344462F7265736F75726365732F +310 +636F72652E786D6C8D504B6E833010BD0A9A6D65B08134808CA354A427E8052C332156828DFCA97AFC9A40A576D7CDCCE87D344F8F9FBEE647F689CE6B6B7A6039850C8DB2A336530F315C4903D94970676DF82583CCC8197B5861107C72362E3BA4AC096842428D1D7107A5F71820538F74F4E0D50D6799CB1892C2DFF345 +310 +AABB9C306779A2934E701F5C0A906D4FD609C2A1B7D129F4BCD858C163D4E3AED163FAA9AF1A1D88F67CA98E6D35906AA82A52BFB39ABCD5CD915C86331BD86BD3B665CD8BD52C78D0F34F46E550061C419494B6843142CB0F5677F4D0D58717DA7494F26295FF31C565FCA7296A13AA728FBBD93C0896823C09C18BB5AFB4 +310 +9E65A6BD762BBE01504B010214000A0000080000B670623B3DEE336E79000000790000001B00000000000000000000000000000000006175746F6465736B2D64657369676E2D7061636B6167652E786D6C504B01021400140006080800B670623BD486FD9CA0000000F40000001300000000000000000000000000B2000000 +310 +5B436F6E74656E745F54797065735D2E786D6C504B01021400140006080800B670623B28ED286BEB0000007C010000080000000000000000000000000083010000636F72652E786D6C504B01021400140006080800B670623B55019EA25F0000007C000000070000000000000000000000000094020000636E782E786D6C50 +310 +4B01021400140006080800B670623B7BC41AC871010000D40300000C00000000000000000000000000180300006662782F636F72652E786D6C504B01021400140006080800B670623B70102A487D0000009D0000000B00000000000000000000000000B30400006662782F636E782E786D6C504B01021400140006080800B6 +310 +70623B3610CA3329010000120200003100000000000000000000000000590500006662782F42384135303843342D343331462D343546312D424341352D3532343432364645413344462F636F72652E786D6C504B01021400140006080800B670623B1B8617914E010000330300003000000000000000000000000000D10600 +310 +006662782F42384135303843342D343331462D343546312D424341352D3532343432364645413344462F636E782E786D6C504B01021400140006080800B670623BBFEF2E3B690500003C10000034000000000000000000000000006D0800006662782F42384135303843342D343331462D343546312D424341352D35323434 +310 +32364645413344462F6F626A656374732E786D6C504B01021400140006080000B670623B0000000000000000000000003500000000000000000000000000280E00006662782F42384135303843342D343331462D343546312D424341352D3532343432364645413344462F76657274696365732E62696E504B010214001400 +310 +06080800B670623BF270F133060000000400000036000000000000000000000000007B0E00006662782F42384135303843342D343331462D343546312D424341352D3532343432364645413344462F747269616E676C65732E62696E504B01021400140006080000B670623B00000000000000000000000037000000000000 +310 +00000000000000D50E00006662782F42384135303843342D343331462D343546312D424341352D3532343432364645413344462F617474726962757465732E62696E504B01021400140006080800B670623B08DA0BD1900000004101000036000000000000000000000000002A0F00006662782F42384135303843342D3433 +310 +31462D343546312D424341352D3532343432364645413344462F6469726563746F72792E786D6C504B01021400140006080800B670623B3B4606F3FE000000A00100003B000000000000000000000000000E1000006662782F42384135303843342D343331462D343546312D424341352D3532343432364645413344462F72 +310 +65736F75726365732F636F72652E786D6C504B0506000000000E000E0080040000651100000000 + 0 +XRECORD + 5 +187 +102 +{ACAD_REACTORS +330 +186 +102 +} +330 +186 +100 +AcDbXrecord +280 + 1 + 70 + 1 + 90 +429727718 + 1 +887D55A9-D0DD-414E-B741-A7DCD4DA2B4C +310 +504B03040A0000080000B670623B3DEE336E79000000790000001B0000006175746F6465736B2D64657369676E2D7061636B6167652E786D6C3C3F786D6C2076657273696F6E3D22312E302220656E636F64696E673D227574662D3822203F3E3C666F726D6174733E3C666F726D61743E687474703A2F2F736368656D612E +310 +6175746F6465736B2E636F6D2F64657369676E2D7061636B6167652F323030393C2F666F726D61743E3C2F666F726D6174733E504B0304140006080800B670623BD486FD9CA0000000F4000000130000005B436F6E74656E745F54797065735D2E786D6C7D8EC10E82300C865F65E91D8A1E8C310C0EEA1BF002731658846E +310 +D98AC1B77784ABF1D8FE5FBFBF75BBCE937A534CCEB38643598122B6FEE978D0B0485F9C41B54DDD7D022595594E1A469170414C76A4D9A4D207E29CF43ECE46F218070CC6BECC4078ACAA135ACF422C856C0E68EA1BF5669944DDD7BCDE7B1F8E415D77AECB980613C2E4AC91FC167A2B244592486606FC29C8FD7F045B9A +310 +EF7053A7E60B504B0304140006080800B670623BBC1638F5EC0000007C01000008000000636F72652E786D6C8D90416EC3201045AF82665B61836BE2D8C24471DB9CA01740401C94182C03558F5F9C3852BBEB6A467FFE9B197D7EF89E6EE8CB2CC17AD7032D0820E394D7D68D3DA478C67B4007C117EFE32F1B202727D3C3 +310 +2A83E0E3E2D3BC49CABB685CCCAAF3DA6CA20CC14440EA969B1E82BA98491632C5EC08D76296EA2A4753D06797E194AC468F2356E77DF66CCD02825543D50CACC62D39ED70FDC68EB8FD68F6F8787A6784342D1DDA1D2F5758F068A7E77DB518198D065111D2624A31A93E69DD11D6D5EC85EC3B4278B9DAFF4069D6FF8492 +310 +75F1B5DADE7D600104CD8FDC0782976B16B9DC83CA75CD4DFC00504B0304140006080800B670623B55019EA25F0000007C00000007000000636E782E786D6C4DCB3B0E80201045D1AD90E915ED2CF8AC45050C11660C8261F98AB1B07AC9C97D42D718D865D3E909258CFD00CCE24AC6E326A164D74DC0B4128928FF326038 +310 +472BA13128B1252AC747C6BAB984FC68F0B87FE8960A5C09FE86CFB69FBA01504B0304140006080800B670623B3586FFB671010000D40300000C0000006662782F636F72652E786D6CC553ED6E8320147D15C2DF4541A76D6D90A6DDD627D80B50A4CE54C120983EFEAE1F4DD7A55BBA5FF3879273EEE59E7310B6393735EA +310 +95ED2AA3731C851423A5A5292A5DE6D8BB63B0C268C39935C67D29C3488B46E578803167A535BE9D2169B453DA01AA4DA16650749D7218C91A1639EEE4876A4428BC838AEE14B6429E44A9C228041AEA38EB9C0501681A32BC313F1ECE8C4C3867DE57C5CC56054CAB8E95B298A7F12E5EEED224C8E87E11242FE936C8DE96 +310 +AB60BB7F4D295D66D12E5B30323473E6AAE6A24E5A259C2A308F29CD82280A68FC1E256B9AAE93F489AED694323294DF34F9B678B0C957DA3DC7B3DCA9ADC33C022123F1CD2D44E450277AD04338234388F01913BE0D1A0281909D38D4171F27807A259DB1093085F157AA17B557309586747CD0CF0B46A64E983A6E0EAEEF +310 +CE881F9EF1FB8E70D44297604238581EBC1B74B249C31C5AAB6CD0BB33D2C636A27ECCC62F0EFF445DC5DF95244D6DEC7F2922F30191F9F720C37DE49F504B0304140006080800B670623B4220A0FF7D0000009D0000000B0000006662782F636E782E786D6C4DCCD10A83201886E15B91FFDCE542CB0335D0EC3E6259C84A +310 +C3E9D8E5AF46073BFAE0E1E315DD675BD1DBA5978F41C2FD4600B9F088930F8B849267CC01754AA418F3DF0D50183727E164506249B1EC174D6E1ECB9A0F5D7D785E68DB8151CD286E5A5363CA6B8A39E90D36D40E96B086F55A43A544F52B1D7B86D517504B0304140006080800B670623B2FA3FEDE290100001202000031 +310 +0000006662782F45374635344235342D363743322D343832342D383044432D4334454645303536354442422F636F72652E786D6C8D515B6EC32010BC0AE22B55850D2E386E848994D7097A0117488A1283C5A3CAF10BB523B57FF961D1ECCCECC0F2ED7DBC816FED8371B687A4C210682B9D32F6D2C314CFA883602BB8772E +310 +FEA141608751F7B0C050F08B77695A20E96CD43666D43AA5177008414708E42D5F7A18E4971E876A483133C2B59A06791D2EBA22556E679EE021FA1C00CC43CA09C5717D6274C7286AD7FB06D1AEA1A8C3873DDAD3E3E98859CB0EBB1DAF67A1E02919B5C88DCA71CCD968FFAC49110B1ECDF8882FBD1EA256503418BF2342 +310 +106E3E08DD60B6A1EC15771B8C795DE8FF4469524F8A92B1F1AD59E2CEB20005E1F5DC105CB9F4797B84D1F7C9F908CABC55D0F2050A5C61CC1AD2F27A26E6E767C7962E8E213A9FBF7775FDCC5CC266DB960A5E970DE5F2BBBE5CCB36C50F504B0304140006080800B670623B1B8617914E01000033030000300000006662 +310 +782F45374635344235342D363743322D343832342D383044432D4334454645303536354442422F636E782E786D6CD593C16EC32010447F0571AD6CE3D489D20813A9877E410FBD62D82434182CC055F3F75D1292E692438FBD206B7698593D61BEFD1E2DF982108D773D6D6B460938E5B571FB9ECE6957AD29D90A1EBC4F77 +310 +364A9C1CA1A759A682EF839FA72269D8C9D92654AD71C72206887E0E0A22CAF926B15E498B01E0AA19451807D01A34B919F16B070157417138917400F2F6FA41FCF0092A45DEE414C19BDC21783E4BD39D891265658C3D8DEA00A3ACE59CBC8678ACB54CB26EEB6BD7839D7E9348BE70AD9C8321B9BCA7F9A4A26C542347DE +310 +E050F09802D22BA6749A800A394DD62899107273365E3C82CFC6A55557BC06C10774B7ABE7AEC3B0F34CF06446280E154026D0542C187BA9DAB6628BF7B6DBB0E5A65B3EB1F58631DE64BBE083F7B65CC218087E22CABB042E51D1F2268F1FE12B3EA24D40D63E9CFE00120194D60B9D5BC67FE6D39CDF37E20AF817881F50 +310 +4B0304140006080800B670623B29652116630500003C100000340000006662782F45374635344235342D363743322D343832342D383044432D4334454645303536354442422F6F626A656374732E786D6CAD575D739B3814FD2B0CFB0C989834C90EA1639B24E3E9A6EDD4D3CE3E64C623834CD8006224913AFDF57B040203 +310 +B65BEF871F0C12D2FD3CF7E8CA7FBFCB33E3957291B2E2D674ED8969D02262715A24B76625B7D6B569BC0F7CCE98EC2D338D82E4F4D654D366E0279C55A59E8AE99654991CCDDECFFF3444440B6A1A514684B83545F44C7362934AB2988A173B2692D8C97667BB76B3702836E1A47C1E092D09A78584A5982F20C5688C7287 +310 +C389E904BEA3BE07BEFAD7ABA6A75739B53F43FD69212429A20365233153D70BEF2E2E6FAC4BCF9B5ADEBB0BD79ADFCF43CBBD9A4E16EF168B9BD9DD0D140BC921495BC2E9D60C1E101B9E46BED37CEA2CD6861F3329624541A3DA7F7858DBAC25C6B4A4458C3CA654405BCFEBC970E8AAD8F43E4FD558ABEC4F9FE7557F47 +310 +4F506B7BFBEC5BCA367FC105710E2AB8A00A554D7CB4A30DFE26FBA86D18CBF4B75418399194A72433B274C3097F33032C554B027F2046A1932B842B0FCCC0DDCBEBDB8AD2F835768F59B9ADB24C41D30C6A64AF817F2A456D59FA83F2E3DA60137255A585BCD60EC9B7929AC18DEFD49381AF1ED30BFDB109A4B1CD488294 +310 +C3CDE6EBC8D185AABD6568061FEE37BB95AAC7E3CA4BCE4ACAA5820FA0D526AE7DAA2869B5EE5999531BE0CC20E6FBA07C41E43FA27CF7A6206F3CDD793B4F6B919C1462CB788EDCD893FA674CDA17AB7B3B7C019F8D561FAEE966FED562DFE98C1D12C6A9FC015C2A35D7FF3D7FC398F591DA4B5FE0BFA2C0189FEA50AE22 +310 +92817A1ED3020951A03432862970F976B33383259812CC8E5D240BBA801CBCF8CE2BBEE3BF910DB6AAD5F79E05123AE2DB33981FB1ACCB02B69D00CB014FD68AB573A7627EF9BFD4CC2C5C7D58557C4B22FAA88925A4DBB44825CECE3D78FB060D1231F0E885BE7D673C86A3874958F593906B5502D1563157814FDAC3420D +310 +F69A070A22EC4B98E23CFF171A1E5311D12C23056555ABA53B8614563B92D17D02AAB009E8F46284624E5F53D5490C560CCCC2591FF1B45421EB0EBD8EA7ED13BE7C5D7E0428CD60FEF60779EB13E6664FF711CB185F6FDED60D139A001FF60CD1FD6D086F1D51474901E26B2CB794A343BC8ED3EDB612E0DD2351FCB65CF4 +310 +33D511C9E14B5B40ADC6A6263DB0336A8F700DDF56271A024E70B0B3629DE21847591E55DED73D56206AB927E46FAABC5C939C55853C274C93D69FD6FA4E78BF8B11DF898C9ECD60553FAD39283BA6F18984AA23E8884F83F45C5F5F859797B31B2B9C84A1E5B9DE9D35BFF25C6B76152E422F9C5DCC3D847F5801ED09D5AF +310 +41441327A25FF15487F9EB725FB666F048A4701E9A827292E6F97569A32DF61DEC01C0BB8DCBBCCC684E0B49147C1FD54BF685A0AB18C868E6973949E819523E3DAC463660E68C7D9F55A9D1EFA3BD7A76A16AE1A810F95CE59B82A4D968A376DC2E8B44BBDD86B27B368CDE0ED548C7137DA860591A83706223A12CA712B4 +310 +7306DBF39C0A20A6DF95C7FAF670C6EED7A61F946493D1D6926782963813403548A78AE470DEF80D2D9CEAA26E4D1C76F60BFE74219A06527D6B82DF641A51616F703CAAC6A7160E08A00BEB4810A0274592A1B36E3675E36E978A0DE80727A0D6DF2D39A75BD27E0D8837276589537B40ABB56D5A41F48F1C6FCD3188C4EB +310 +A6929D2FFB89CE995A4D00AE813BDD032DC48B56BD437DFDDC2DD7AED70C2272D63DE9A72068A45A92F0A4BE17A8A06B9366E16707AD80B1FC649459952097E74BB23805982B55E0401150A165964402AA8BDF9F3E7386BB686EDCA780C0D382E5392BF460A6EFB2C6EA1937D3F809B440D5F5435C4C5CF7A9EEFAF54564BD +310 +2182DA24162F98680B4E7930C21A0079709CEAEB99A372305ABE0C0D7D6078105A83F6A0296DAEE1068A47DD1C55A9F4C9B2FC69E3EFD44B81028E963DF81B504B0304140006080000B670623B000000000000000000000000350000006662782F45374635344235342D363743322D343832342D383044432D433445464530 +310 +3536354442422F76657274696365732E62696E504B0304140006080800B670623BF270F1330600000004000000360000006662782F45374635344235342D363743322D343832342D383044432D4334454645303536354442422F747269616E676C65732E62696E636660600000504B0304140006080000B670623B00000000 +310 +0000000000000000370000006662782F45374635344235342D363743322D343832342D383044432D4334454645303536354442422F617474726962757465732E62696E504B0304140006080800B670623B08DA0BD19000000041010000360000006662782F45374635344235342D363743322D343832342D383044432D4334 +310 +454645303536354442422F6469726563746F72792E786D6C858F410AC3201045AF22B36F53E9A60B35CBEEBAE80D244E44489C6234F4F835464A48035D0DF398FF9F8AF63D0E6CC63039F212F8F9020C7D47C6792B21C5FE7403D62A1188E2E60C98D7234A5830286103A55745067B9D86B8A3CF9C7F90C11DE607FB144396 +310 +D7B6A82D282E9A152AD114D5EF2CB866EEE831B8EEA07A7BB5A8D7D61A2BAAEB7FD5F70921FF497D00504B0304140006080800B670623B6378F24DFF000000A00100003B0000006662782F45374635344235342D363743322D343832342D383044432D4334454645303536354442422F7265736F75726365732F636F72652E +310 +786D6C8D504B6E833010BD0A9A6D65B0094E42641C85B639412F6099496225D8C89FAAC7AF09546A77DDCC8CDE47F3F4C4F16B7C149FE88371B603565228D06A37187BED20C50BD9437194C23B177FC9A0B06AC40E6618A4B87A97A615D2CE46B431A3D60DB8822A048C50E8473E3A08FA86A32A558A5911EEE5A4F45D5DB1 +310 +6465A6B34E8A107D0E502C4FE609D26370C96B0CA25A58295232C3AA3143FE692E063D485EF7F5AEE71BD2D2F39634AFFC44DAF7DD9E9CCE6F9CD25DCBFA762BAAD92C4534E34F46ED51451C40D694B6843142EB0FD61C283F34FC85EE0F948A6A96FF31A569F8A729191B37F51A77B105902C0779125254735F793DCBCC7B +310 +EE567E03504B010214000A0000080000B670623B3DEE336E79000000790000001B00000000000000000000000000000000006175746F6465736B2D64657369676E2D7061636B6167652E786D6C504B01021400140006080800B670623BD486FD9CA0000000F40000001300000000000000000000000000B20000005B436F6E +310 +74656E745F54797065735D2E786D6C504B01021400140006080800B670623BBC1638F5EC0000007C010000080000000000000000000000000083010000636F72652E786D6C504B01021400140006080800B670623B55019EA25F0000007C000000070000000000000000000000000095020000636E782E786D6C504B010214 +310 +00140006080800B670623B3586FFB671010000D40300000C00000000000000000000000000190300006662782F636F72652E786D6C504B01021400140006080800B670623B4220A0FF7D0000009D0000000B00000000000000000000000000B40400006662782F636E782E786D6C504B01021400140006080800B670623B2F +310 +A3FEDE290100001202000031000000000000000000000000005A0500006662782F45374635344235342D363743322D343832342D383044432D4334454645303536354442422F636F72652E786D6C504B01021400140006080800B670623B1B8617914E010000330300003000000000000000000000000000D2060000666278 +310 +2F45374635344235342D363743322D343832342D383044432D4334454645303536354442422F636E782E786D6C504B01021400140006080800B670623B29652116630500003C10000034000000000000000000000000006E0800006662782F45374635344235342D363743322D343832342D383044432D4334454645303536 +310 +354442422F6F626A656374732E786D6C504B01021400140006080000B670623B0000000000000000000000003500000000000000000000000000230E00006662782F45374635344235342D363743322D343832342D383044432D4334454645303536354442422F76657274696365732E62696E504B01021400140006080800 +310 +B670623BF270F13306000000040000003600000000000000000000000000760E00006662782F45374635344235342D363743322D343832342D383044432D4334454645303536354442422F747269616E676C65732E62696E504B01021400140006080000B670623B0000000000000000000000003700000000000000000000 +310 +000000D00E00006662782F45374635344235342D363743322D343832342D383044432D4334454645303536354442422F617474726962757465732E62696E504B01021400140006080800B670623B08DA0BD190000000410100003600000000000000000000000000250F00006662782F45374635344235342D363743322D34 +310 +3832342D383044432D4334454645303536354442422F6469726563746F72792E786D6C504B01021400140006080800B670623B6378F24DFF000000A00100003B00000000000000000000000000091000006662782F45374635344235342D363743322D343832342D383044432D4334454645303536354442422F7265736F75 +310 +726365732F636F72652E786D6C504B0506000000000E000E0080040000611100000000 + 0 +XRECORD + 5 +112 +102 +{ACAD_REACTORS +330 +110 +102 +} +330 +110 +100 +AcDbXrecord +280 + 1 +270 + 1 +271 + 1 + 0 +XRECORD + 5 +111 +102 +{ACAD_REACTORS +330 +110 +102 +} +330 +110 +100 +AcDbXrecord +280 + 1 +270 + 1 +271 + 1 + 0 +XRECORD + 5 +18A +102 +{ACAD_REACTORS +330 +110 +102 +} +330 +110 +100 +AcDbXrecord +280 + 1 + 70 + 0 + 90 +1778669749 + 1 +9BF507EE-FAF7-4A99-A33D-4EE51894BC40 +310 +504B03040A0000080000B670623B3DEE336E79000000790000001B0000006175746F6465736B2D64657369676E2D7061636B6167652E786D6C3C3F786D6C2076657273696F6E3D22312E302220656E636F64696E673D227574662D3822203F3E3C666F726D6174733E3C666F726D61743E687474703A2F2F736368656D612E +310 +6175746F6465736B2E636F6D2F64657369676E2D7061636B6167652F323030393C2F666F726D61743E3C2F666F726D6174733E504B0304140006080800B670623BD486FD9CA0000000F4000000130000005B436F6E74656E745F54797065735D2E786D6C7D8EC10E82300C865F65E91D8A1E8C310C0EEA1BF002731658846E +310 +D98AC1B77784ABF1D8FE5FBFBF75BBCE937A534CCEB38643598122B6FEE978D0B0485F9C41B54DDD7D022595594E1A469170414C76A4D9A4D207E29CF43ECE46F218070CC6BECC4078ACAA135ACF422C856C0E68EA1BF5669944DDD7BCDE7B1F8E415D77AECB980613C2E4AC91FC167A2B244592486606FC29C8FD7F045B9A +310 +EF7053A7E60B504B0304140006080800B670623BCA089DFFEC0000007C01000008000000636F72652E786D6C8D90416E83301045AF62CDB632D8402020E32882F604BD80653BC44AB011B6AB1EBF262152BBEB6A467FFE9B197D76FA9EEFE84BAFDE38DB03CD08206DA553C64E3DC470C1474027CE56E7C22F1B202B66DDC3 +310 +260367D3EAE2B24BD2D9A06D48AA754AEFA2F05E0740F29E9A1EBCBCEA59642286E4F0B76C11F226269DD15797E0188D42CF2346A57DE662F40ABCAEC77628C6120F654571D50C04B7F5B9C1E3997CD4F550BE3745C3F20DE62C98F9755FAE5A04AD801784B498524C8A4F5A75E4D055873772EC0861F966FF03C545FD138A +310 +C686B2D8DF7D621E384D8F3C069CE55B16A93C824A75CB8DFF00504B0304140006080800B670623B55019EA25F0000007C00000007000000636E782E786D6C4DCB3B0E80201045D1AD90E915ED2CF8AC45050C11660C8261F98AB1B07AC9C97D42D718D865D3E909258CFD00CCE24AC6E326A164D74DC0B4128928FF326038 +310 +472BA13128B1252AC747C6BAB984FC68F0B87FE8960A5C09FE86CFB69FBA01504B0304140006080800B670623B68FC0B7272010000D40300000C0000006662782F636F72652E786D6CC5535B6E833010BC8AE5DF0A6C1E8124328E22D29EA01770C0A128602363A31CBFCB234A53A555FA553EC09AD9F5CE8C31DB5DDA060D +310 +D2F4B556190E7C8A9154852E6B5565D8D993B7C668C799D1DA7E29C3488956667884316795D1AE5BA0422B2B950554E9522EA0E87B69312A1A5864B82F3E642B7CE12C54F467BF13C55954D20F7CA0A18EB3DE1A1080E621E31BF3D3F1C2C88C73E65C5D2E6C5DC2B4FA544B8379921C36797888BC3C8A032F4E73EA6D927D +310 +EA1DF6F42D49F2E8350D5346C666CE6CDD5ED515460A2B4BCC434A375E1078347C0FE22D5D6DE3D50B5D6F2965642CBF6B725DF96493AB958DC245EEDCD6631E809089F8E61622B2A81703E8219C913144F84C09DF070D8140C8561C9BAB8F3340832CAC363130A576376A108D933095FA747AD0CF0B46E64E983A6D0EAE1F +310 +CE089F9EF1FB8E70D44255604258581E9D1D75B259C3125A278D37D80B52DAB4A279CEC62F0EFF44DDC43F9454E8469BFF5244960322CBEF41C6FBC83F01504B0304140006080800B670623B0F71DDB67E0000009D0000000B0000006662782F636E782E786D6C4DCCD10A83201886E15B91FFDCA54DB73F502386BB8F5816 +310 +32D3703976F9ABD1C18E3E78F87855FB990379BBFCF2296AE02706C4C5471A7C9C349475A408A4352AA7B4FEDD80C47E761A7606A3A69CCA72D0E0C6BE8475D3E0E3F3409428EA6B27A84079A782899AA2EC1A7AE3ECCC2FD858CB2D544655BFD2B67BD87C01504B0304140006080800B670623BE134B4FB28010000120200 +310 +00310000006662782F38353834323741342D343835462D343034322D383541392D4331303331363839454531452F636F72652E786D6C8D515B6EC32010BC0AE22B55850D0EA424C24451959EA0172086A42831583CAA1CBF503B52FB971F16CDCECC0EACD8DFC71BF836215AEF7A481A0C817183D7D65D7A98D3197108F652 +310 +04EFD31F1A044E8DA6871586525C82CFD3020DDE25E352419DD76601558C264130DCCAA58771F832A36A544E8511AFCDA486ABBA988634A55D7852C4144A00300FA927949C71DABD1D28A29C7D208A6987383B6CD13BC16BB2E1DBE3911C453B0BA5C8D9EA456E758963CFD684674DAA588A64C747FC2118958C86B2C3788B +310 +0841B8FB247487D98EB257CC77188BB6D2FF89F2A49F1465EBD2BA5BE2CEB2082511EDDC9042FB7CBA3DC298FBE4430275DE2A9AE1054ADC60CC08E3A29D89E5F9C5714317C7987C28DFBBBA9E0A97B0D97643A568EB864AF95D5FA9759BF207504B0304140006080800B670623B1B8617914E010000330300003000000066 +310 +62782F38353834323741342D343835462D343034322D383541392D4331303331363839454531452F636E782E786D6CD593C16EC32010447F0571AD6CE3D489D20813A9877E410FBD62D82434182CC055F3F75D1292E692438FBD206B7698593D61BEFD1E2DF982108D773D6D6B460938E5B571FB9ECE6957AD29D90A1EBC4F +310 +77364A9C1CA1A759A682EF839FA72269D8C9D92654AD71C72206887E0E0A22CAF926B15E498B01E0AA19451807D01A34B919F16B070157417138917400F2F6FA41FCF0092A45DEE414C19BDC21783E4BD39D891265658C3D8DEA00A3ACE59CBC8678ACB54CB26EEB6BD7839D7E9348BE70AD9C8321B9BCA7F9A4A26C542347 +310 +DEE050F09802D22BA6749A800A394DD62899107273365E3C82CFC6A55557BC06C10774B7ABE7AEC3B0F34CF06446280E154026D0542C187BA9DAB6628BF7B6DBB0E5A65B3EB1F58631DE64BBE083F7B65CC218087E22CABB042E51D1F2268F1FE12B3EA24D40D63E9CFE00120194D60B9D5BC67FE6D39CDF37E20AF817881F +310 +504B0304140006080800B670623B5933D422630500003B100000340000006662782F38353834323741342D343835462D343034322D383541392D4331303331363839454531452F6F626A656374732E786D6CAD575B6FA33814FE2B887D0642934E9B156594264D158D3A534DD4D13E548A0C38942D60649B4EBABF7E3F8321 +310 +40924EF6928780CDF1B97EE762EFF32E4B8D37CA45C2F21BD3B547A641F39045491EDF98A5DC5AD7A6F1D9F73863B243661A39C9E88DA9B64DDF8B392B0BBD15D12D295339D85DDEFE618890E6D434C2940871638AF08566C426A5641115AF764424B1E3EDCE76ED9AB0CF36E6A47819302D08A7B984A6D8CFC1C5A89572FB +310 +CB91E9F89EA3BEFB9EFAD754E3D3544E654F5F7E920B49F2F040D880CDD89D2CEE2E2EA7D6E56432B6269F2E5CEB7679BBB0DCABF168FE693E9FCEEEA6102C240727AD09A75BD3BF876F78127A4EFDA9D5582B7E4CA590E5390D2BFB6161A5B3E618D182E611E2985001691DAB47FDA5AB7CD3F93C566B2DB2BB7D9E55DD13 +310 +1D468DEECDB3AB290BFE8409E21C54704115AA6AFF68436BFC8DF65E0B184BF5B74418199194272435D224E084BF9B3E481589EFF5D828747285706581E9BB7B7E5D5D911ABFC6EE312DB7659A2A689A7E85EC0DF04FA5A8344BFEA2FCB834E8845895492EAFB541F2BDA0A63FF59C6AD3F7D4637CA13FD68E34B629891172 +310 +98597F1D183A57B9B75A98FE9765B05BAB7C3C2EBCE0ACA05C2AF8005A4DE09AA7F29216EB9E15397500C6F47CBE77CA7778FE2BD277AF0AE2C693DD6437D1522427B9D8329E2136F6A8FA19A3E6C56ADF0E5F50CF06D48734EDCEBF22F69C56D97EC138153F804B85E6FABFC7AFEFB32E523BE1F3BD372418E363EDCA7548 +310 +52949E8724474014288D94610BB57C1BEC4C7F854A89CA8E5324F55B871CBC78CE1BBEE3BFE68D6A5589EF3C730474506FCFA8FCF0659516D0ED04580EEA6425581B77CAE797FF4BCECC16EB2FEB926F49481F746159D06D922712BD730FDEAE42BD40F42C7AA5EF3F198F60E86110D6DD20645A9480B795CF95E3E3A659A8 +310 +C55E724F408873315335CFFB8584874484344D494E59D94869DB90C26A5B64F49C802CAC1D3ABE18A098D3B7444D123D8A9E5AE8F5214F0AE5B2B6E9B575DA3E61CBD3EA2B4009FA940524DD1305FB6A1FB294F14DF0BEA90BA109ECE1481FDC3FFAE8D60E75141700BE82725371B4873751B2DD960265F788137FACE6DD40 +310 +B575E4F0A5C99F46629D92131467A41EE11ABD8D4CCC039CA0AFB37C93A08B232B8F0AEFCA1E0A1015DF13FC83322B362463652ECF71D3A8B1A7D1BE65DE1D62C44F22C317D35F574FEB16153BA2D13E543D10A80E74C4A65E78A6B7CBCBD1D5DD9DB59C2DAFACC96C3AB566E3F1C29ADCDD5DBAD7D3C9ED7C822ED74F80A6 +310 +41755310DE4443F44A9E68373FADF6596BFA0F440AE7BECE2727AE9F4F2B1B53B1E7E00CF0DD1E5C65454A339A4BA2D0FBA05ED2EF0443458F47BDBFCA484CCFE0F2ED7E3DD0013B679C7B5499467F0ECEEADDB9CA85A34CE44B99053949D2C1416DB85DE4B136BB7165FBAC0B7AB3542BED4F8CA182A549847A1319316519 +310 +95A83A67147B9E5101C47487F2485F1ECE38FD568F839204296D3479219888530154036E6528FBFBC66F98E0D4107563A2D7D9AFF8D389681A08F58D89F22693900A3B407754734FC51C10C010D6D640809EE4718AC1BA3ED4AEDB53CA37283F68805A7E4B72CEB0A4EDEAD5DD8C14059A76AFAA56BA6901E13F32BC51C720 +310 +12AF41295B5BF61BAD3195181FB506E6B40F4C10AF5AF40EF9F5B159AE5DD1F43C72D635E94310D45C2D49785C5D0B94D3B54AB3C5A38349C0587D338AB48C11CBF339599C02CCA54A70A008A8D03C0B2201D5F9EFCF8F9CE12A9A19CB0410789EB32C63B95ECCF455D658BFE0621A3DA32C5075FB101723D77DAE867E7D0F +310 +D90444509B44E2151B4DC2290B065803200FBAA9BE9D392A0603F2D5C2D00D6302A615680F66D2FA166E2079D4C551A54AB758161FCEFD4E450A14704CECFEDF504B0304140006080000B670623B000000000000000000000000350000006662782F38353834323741342D343835462D343034322D383541392D4331303331 +310 +363839454531452F76657274696365732E62696E504B0304140006080800B670623BF270F1330600000004000000360000006662782F38353834323741342D343835462D343034322D383541392D4331303331363839454531452F747269616E676C65732E62696E636660600000504B0304140006080000B670623B000000 +310 +000000000000000000370000006662782F38353834323741342D343835462D343034322D383541392D4331303331363839454531452F617474726962757465732E62696E504B0304140006080800B670623B08DA0BD19000000041010000360000006662782F38353834323741342D343835462D343034322D383541392D43 +310 +31303331363839454531452F6469726563746F72792E786D6C858F410AC3201045AF22B36F53E9A60B35CBEEBAE80D244E44489C6234F4F835464A48035D0DF398FF9F8AF63D0E6CC63039F212F8F9020C7D47C6792B21C5FE7403D62A1188E2E60C98D7234A5830286103A55745067B9D86B8A3CF9C7F90C11DE607FB1443 +310 +96D7B6A82D282E9A152AD114D5EF2CB866EEE831B8EEA07A7BB5A8D7D61A2BAAEB7FD5F70921FF497D00504B0304140006080800B670623BD9A3B367FE000000A00100003B0000006662782F38353834323741342D343835462D343034322D383541392D4331303331363839454531452F7265736F75726365732F636F7265 +310 +2E786D6C8D504B6E833010BD0A9A6D65B01D0201194711694FD00B586642AD043BF2A7EAF16B122AB5BB6E6646EFA3797AE2F8B5DC8A4FF4C1383B002B291468B59B8C9D0748F1420E501CA5F0CEC55F3228AC5A708015062966EFD27D83B4B3116DCCA875136EA00A012314FA968F0182FEC045952AC5AC08D7F2AEF455CD +310 +58B232D3592745883E07289E4FD609D26370C96B0CA27AB252A464A64D63A6FCD35C0C7A904D73EE467EE664DCD58CD4ED4849D79C5A723ED1B7A61977AF2D6F45B59AA58866F9C9A83DAA8813484E6947182394BFB3BAA7FBBEDEBFD0434FA9A856F91F53BA4FFF342563E38E6F719FB60092E5200F428A6AED2BAF479979 +310 +AFDDCA6F504B010214000A0000080000B670623B3DEE336E79000000790000001B00000000000000000000000000000000006175746F6465736B2D64657369676E2D7061636B6167652E786D6C504B01021400140006080800B670623BD486FD9CA0000000F40000001300000000000000000000000000B20000005B436F6E +310 +74656E745F54797065735D2E786D6C504B01021400140006080800B670623BCA089DFFEC0000007C010000080000000000000000000000000083010000636F72652E786D6C504B01021400140006080800B670623B55019EA25F0000007C000000070000000000000000000000000095020000636E782E786D6C504B010214 +310 +00140006080800B670623B68FC0B7272010000D40300000C00000000000000000000000000190300006662782F636F72652E786D6C504B01021400140006080800B670623B0F71DDB67E0000009D0000000B00000000000000000000000000B50400006662782F636E782E786D6C504B01021400140006080800B670623BE1 +310 +34B4FB280100001202000031000000000000000000000000005C0500006662782F38353834323741342D343835462D343034322D383541392D4331303331363839454531452F636F72652E786D6C504B01021400140006080800B670623B1B8617914E010000330300003000000000000000000000000000D3060000666278 +310 +2F38353834323741342D343835462D343034322D383541392D4331303331363839454531452F636E782E786D6C504B01021400140006080800B670623B5933D422630500003B10000034000000000000000000000000006F0800006662782F38353834323741342D343835462D343034322D383541392D4331303331363839 +310 +454531452F6F626A656374732E786D6C504B01021400140006080000B670623B0000000000000000000000003500000000000000000000000000240E00006662782F38353834323741342D343835462D343034322D383541392D4331303331363839454531452F76657274696365732E62696E504B01021400140006080800 +310 +B670623BF270F13306000000040000003600000000000000000000000000770E00006662782F38353834323741342D343835462D343034322D383541392D4331303331363839454531452F747269616E676C65732E62696E504B01021400140006080000B670623B0000000000000000000000003700000000000000000000 +310 +000000D10E00006662782F38353834323741342D343835462D343034322D383541392D4331303331363839454531452F617474726962757465732E62696E504B01021400140006080800B670623B08DA0BD190000000410100003600000000000000000000000000260F00006662782F38353834323741342D343835462D34 +310 +3034322D383541392D4331303331363839454531452F6469726563746F72792E786D6C504B01021400140006080800B670623BD9A3B367FE000000A00100003B000000000000000000000000000A1000006662782F38353834323741342D343835462D343034322D383541392D4331303331363839454531452F7265736F75 +310 +726365732F636F72652E786D6C504B0506000000000E000E0080040000611100000000 + 0 +XRECORD + 5 +113 +102 +{ACAD_REACTORS +330 +110 +102 +} +330 +110 +100 +AcDbXrecord +280 + 1 +270 + 1 +271 + 1 + 0 +XRECORD + 5 +114 +102 +{ACAD_REACTORS +330 +110 +102 +} +330 +110 +100 +AcDbXrecord +280 + 1 +270 + 1 +271 + 1 + 0 +XRECORD + 5 +35A +102 +{ACAD_REACTORS +330 +1C4 +102 +} +330 +1C4 +100 +AcDbXrecord +280 + 1 +102 +DISPLAYNAME + 1 +Imperial24 +102 +FLAGS + 90 + 0 + 0 +CELLSTYLEMAP + 5 +359 +102 +{ACAD_REACTORS +330 +104 +102 +} +330 +104 +100 +AcDbCellStyleMap + 90 + 3 +300 +CELLSTYLE + 1 +TABLEFORMAT_BEGIN + 90 + 5 +170 + 1 + 91 + 0 + 92 + 32768 + 62 + 257 + 93 + 1 +300 +CONTENTFORMAT + 1 +CONTENTFORMAT_BEGIN + 90 + 0 + 91 + 0 + 92 + 512 + 93 + 0 +300 + + 40 +0.0 +140 +1.0 + 94 + 5 + 62 + 0 +340 +11 +144 +0.25 +309 +CONTENTFORMAT_END +171 + 1 +301 +MARGIN + 1 +CELLMARGIN_BEGIN + 40 +0.06 + 40 +0.06 + 40 +0.06 + 40 +0.06 + 40 +0.18 + 40 +0.18 +309 +CELLMARGIN_END + 94 + 6 + 95 + 1 +302 +GRIDFORMAT + 1 +GRIDFORMAT_BEGIN + 90 + 0 + 91 + 1 + 62 + 0 + 92 + -2 +340 +0 + 93 + 0 + 40 +0.045 +309 +GRIDFORMAT_END + 95 + 2 +302 +GRIDFORMAT + 1 +GRIDFORMAT_BEGIN + 90 + 0 + 91 + 1 + 62 + 0 + 92 + -2 +340 +0 + 93 + 0 + 40 +0.045 +309 +GRIDFORMAT_END + 95 + 4 +302 +GRIDFORMAT + 1 +GRIDFORMAT_BEGIN + 90 + 0 + 91 + 1 + 62 + 0 + 92 + -2 +340 +0 + 93 + 0 + 40 +0.045 +309 +GRIDFORMAT_END + 95 + 8 +302 +GRIDFORMAT + 1 +GRIDFORMAT_BEGIN + 90 + 0 + 91 + 1 + 62 + 0 + 92 + -2 +340 +0 + 93 + 0 + 40 +0.045 +309 +GRIDFORMAT_END + 95 + 16 +302 +GRIDFORMAT + 1 +GRIDFORMAT_BEGIN + 90 + 0 + 91 + 1 + 62 + 0 + 92 + -2 +340 +0 + 93 + 0 + 40 +0.045 +309 +GRIDFORMAT_END + 95 + 32 +302 +GRIDFORMAT + 1 +GRIDFORMAT_BEGIN + 90 + 0 + 91 + 1 + 62 + 0 + 92 + -2 +340 +0 + 93 + 0 + 40 +0.045 +309 +GRIDFORMAT_END +309 +TABLEFORMAT_END + 1 +CELLSTYLE_BEGIN + 90 + 1 + 91 + 1 +300 +_TITLE +309 +CELLSTYLE_END +300 +CELLSTYLE + 1 +TABLEFORMAT_BEGIN + 90 + 5 +170 + 1 + 91 + 0 + 92 + 0 + 62 + 257 + 93 + 1 +300 +CONTENTFORMAT + 1 +CONTENTFORMAT_BEGIN + 90 + 0 + 91 + 0 + 92 + 512 + 93 + 0 +300 + + 40 +0.0 +140 +1.0 + 94 + 5 + 62 + 0 +340 +11 +144 +0.18 +309 +CONTENTFORMAT_END +171 + 1 +301 +MARGIN + 1 +CELLMARGIN_BEGIN + 40 +0.06 + 40 +0.06 + 40 +0.06 + 40 +0.06 + 40 +0.18 + 40 +0.18 +309 +CELLMARGIN_END + 94 + 6 + 95 + 1 +302 +GRIDFORMAT + 1 +GRIDFORMAT_BEGIN + 90 + 0 + 91 + 1 + 62 + 0 + 92 + -2 +340 +0 + 93 + 0 + 40 +0.045 +309 +GRIDFORMAT_END + 95 + 2 +302 +GRIDFORMAT + 1 +GRIDFORMAT_BEGIN + 90 + 0 + 91 + 1 + 62 + 0 + 92 + -2 +340 +0 + 93 + 0 + 40 +0.045 +309 +GRIDFORMAT_END + 95 + 4 +302 +GRIDFORMAT + 1 +GRIDFORMAT_BEGIN + 90 + 0 + 91 + 1 + 62 + 0 + 92 + -2 +340 +0 + 93 + 0 + 40 +0.045 +309 +GRIDFORMAT_END + 95 + 8 +302 +GRIDFORMAT + 1 +GRIDFORMAT_BEGIN + 90 + 0 + 91 + 1 + 62 + 0 + 92 + -2 +340 +0 + 93 + 0 + 40 +0.045 +309 +GRIDFORMAT_END + 95 + 16 +302 +GRIDFORMAT + 1 +GRIDFORMAT_BEGIN + 90 + 0 + 91 + 1 + 62 + 0 + 92 + -2 +340 +0 + 93 + 0 + 40 +0.045 +309 +GRIDFORMAT_END + 95 + 32 +302 +GRIDFORMAT + 1 +GRIDFORMAT_BEGIN + 90 + 0 + 91 + 1 + 62 + 0 + 92 + -2 +340 +0 + 93 + 0 + 40 +0.045 +309 +GRIDFORMAT_END +309 +TABLEFORMAT_END + 1 +CELLSTYLE_BEGIN + 90 + 2 + 91 + 1 +300 +_HEADER +309 +CELLSTYLE_END +300 +CELLSTYLE + 1 +TABLEFORMAT_BEGIN + 90 + 5 +170 + 1 + 91 + 0 + 92 + 0 + 62 + 257 + 93 + 1 +300 +CONTENTFORMAT + 1 +CONTENTFORMAT_BEGIN + 90 + 0 + 91 + 0 + 92 + 512 + 93 + 0 +300 + + 40 +0.0 +140 +1.0 + 94 + 2 + 62 + 0 +340 +11 +144 +0.18 +309 +CONTENTFORMAT_END +171 + 1 +301 +MARGIN + 1 +CELLMARGIN_BEGIN + 40 +0.06 + 40 +0.06 + 40 +0.06 + 40 +0.06 + 40 +0.18 + 40 +0.18 +309 +CELLMARGIN_END + 94 + 6 + 95 + 1 +302 +GRIDFORMAT + 1 +GRIDFORMAT_BEGIN + 90 + 0 + 91 + 1 + 62 + 0 + 92 + -2 +340 +0 + 93 + 0 + 40 +0.045 +309 +GRIDFORMAT_END + 95 + 2 +302 +GRIDFORMAT + 1 +GRIDFORMAT_BEGIN + 90 + 0 + 91 + 1 + 62 + 0 + 92 + -2 +340 +0 + 93 + 0 + 40 +0.045 +309 +GRIDFORMAT_END + 95 + 4 +302 +GRIDFORMAT + 1 +GRIDFORMAT_BEGIN + 90 + 0 + 91 + 1 + 62 + 0 + 92 + -2 +340 +0 + 93 + 0 + 40 +0.045 +309 +GRIDFORMAT_END + 95 + 8 +302 +GRIDFORMAT + 1 +GRIDFORMAT_BEGIN + 90 + 0 + 91 + 1 + 62 + 0 + 92 + -2 +340 +0 + 93 + 0 + 40 +0.045 +309 +GRIDFORMAT_END + 95 + 16 +302 +GRIDFORMAT + 1 +GRIDFORMAT_BEGIN + 90 + 0 + 91 + 1 + 62 + 0 + 92 + -2 +340 +0 + 93 + 0 + 40 +0.045 +309 +GRIDFORMAT_END + 95 + 32 +302 +GRIDFORMAT + 1 +GRIDFORMAT_BEGIN + 90 + 0 + 91 + 1 + 62 + 0 + 92 + -2 +340 +0 + 93 + 0 + 40 +0.045 +309 +GRIDFORMAT_END +309 +TABLEFORMAT_END + 1 +CELLSTYLE_BEGIN + 90 + 3 + 91 + 2 +300 +_DATA +309 +CELLSTYLE_END + 0 +ENDSEC + 0 +SECTION + 2 +ACDSDATA + 70 + 2 + 71 + 8 + 0 +ACDSSCHEMA + 90 + 0 + 1 +AcDb_Thumbnail_Schema + 2 +AcDbDs::ID +280 + 10 + 91 + 8 + 2 +Thumbnail_Data +280 + 15 + 91 + 0 +101 +ACDSRECORD + 95 + 0 + 90 + 1 + 2 +AcDbDs::TreatedAsObjectData +280 + 1 +291 + 1 +101 +ACDSRECORD + 95 + 0 + 90 + 2 + 2 +AcDbDs::Legacy +280 + 1 +291 + 1 +101 +ACDSRECORD + 1 +AcDbDs::ID + 90 + 3 + 2 +AcDs:Indexable +280 + 1 +291 + 1 +101 +ACDSRECORD + 1 +AcDbDs::ID + 90 + 4 + 2 +AcDbDs::HandleAttribute +280 + 7 +282 + 1 + 0 +ACDSSCHEMA + 90 + 1 + 1 +AcDbDs::TreatedAsObjectDataSchema + 2 +AcDbDs::TreatedAsObjectData +280 + 1 + 91 + 0 + 0 +ACDSSCHEMA + 90 + 2 + 1 +AcDbDs::LegacySchema + 2 +AcDbDs::Legacy +280 + 1 + 91 + 0 + 0 +ACDSSCHEMA + 90 + 3 + 1 +AcDbDs::IndexedPropertySchema + 2 +AcDs:Indexable +280 + 1 + 91 + 0 + 0 +ACDSSCHEMA + 90 + 4 + 1 +AcDbDs::HandleAttributeSchema + 2 +AcDbDs::HandleAttribute +280 + 7 + 91 + 1 +284 + 1 + 0 +ACDSRECORD + 90 + 0 + 2 +AcDbDs::ID +280 + 10 +320 +2CC + 2 +Thumbnail_Data +280 + 15 + 94 + 1159 +310 +89504E470D0A1A0A0000000D4948445200000100000000B5080300000087DCD12600000300504C5445FFFFFF000000C8C8C80000000000000000000000000000000000000000000000000000330000660000990000CC0000FF0033000033330033660033990033CC0033FF0066000066330066660066990066CC0066FF0099 +310 +000099330099660099990099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF0000FF3300FF6600FF9900FFCC00FFFF3300003300333300663300993300CC3300FF3333003333333333663333993333CC3333FF3366003366333366663366993366CC3366FF3399003399333399663399993399CC3399FF33CC00 +310 +33CC3333CC6633CC9933CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF6600006600336600666600996600CC6600FF6633006633336633666633996633CC6633FF6666006666336666666666996666CC6666FF6699006699336699666699996699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066 +310 +FF3366FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF9933009933339933669933999933CC9933FF9966009966339966669966999966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC3399CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFFCC0000CC00 +310 +33CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFFCCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0000FF0033FF0066FF0099FF00CCFF00FFFF3300FF3333 +310 +FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33FFCC66FFCC99FFCCCCFFCCFFFFFF00FFFF33FFFF66FFFF99FFFFCCFFFFFF0000000D0D0D1A1A1A2828283535354343435050505D5D5D6B6B6B787878868686939393A1A1A1AEAEAEBB +310 +BBBBC9C9C9D6D6D6E4E4E4F1F1F1FFFFFF000000000000000000000000000000000000000000000000000000000000B0E2D235000001424944415478DAEDDD3B0AC330144451ED7F99AF145A845DBA116E8C2C31478521A50F999B0F81B4D1B34F1B2DFC00783EA8FB045CD39F016502F30900089F00001104208200441080 +310 +08021041002208400401882000110420820044108008021041002208400401882000110420820044108008021041002208400401882000110420820044108008021041002208400401882080E323B8DD497F150000203C82004C000000002208C0040000002082004C000000002208C0040000002082004C000000002208C0 +310 +040000002082007E9840ADBAA96D0156FDFCF5C467C0EB3D96099C1F4100F1110400004076048F7E23E4B3000000007C2102C0040000002082004C000000002208C0040000002082004C000000002208C0040000002082004C000000002208C004000000208200A226503BFECFCCE7D73940E201D0B34F1BE1E7021039E2C1 +310 +DA17D3CB0000000049454E44AE426082 + 0 +ACDSRECORD + 90 + 0 + 2 +AcDbDs::ID +280 + 10 +320 +59 + 2 +Thumbnail_Data +280 + 15 + 94 + 1167 +310 +89504E470D0A1A0A0000000D4948445200000100000000B5080300000087DCD12600000300504C5445FFFFFF000000C8C8C80000000000000000000000000000000000000000000000000000330000660000990000CC0000FF0033000033330033660033990033CC0033FF0066000066330066660066990066CC0066FF0099 +310 +000099330099660099990099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF0000FF3300FF6600FF9900FFCC00FFFF3300003300333300663300993300CC3300FF3333003333333333663333993333CC3333FF3366003366333366663366993366CC3366FF3399003399333399663399993399CC3399FF33CC00 +310 +33CC3333CC6633CC9933CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF6600006600336600666600996600CC6600FF6633006633336633666633996633CC6633FF6666006666336666666666996666CC6666FF6699006699336699666699996699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066 +310 +FF3366FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF9933009933339933669933999933CC9933FF9966009966339966669966999966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC3399CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFFCC0000CC00 +310 +33CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFFCCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0000FF0033FF0066FF0099FF00CCFF00FFFF3300FF3333 +310 +FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33FFCC66FFCC99FFCCCCFFCCFFFFFF00FFFF33FFFF66FFFF99FFFFCCFFFFFF0000000D0D0D1A1A1A2828283535354343435050505D5D5D6B6B6B787878868686939393A1A1A1AEAEAEBB +310 +BBBBC9C9C9D6D6D6E4E4E4F1F1F1FFFFFF000000000000000000000000000000000000000000000000000000000000B0E2D2350000014A4944415478DAEDDC410AC2301445D1EC7F997F58B208112776D4466853F34E4087A247DF15056D7DCB3EADB7F00300000000000000000000000000C10055EF4BD8F50E20F0A90790 +310 +DE8012C15D04973A3F00ACF8E2CE8D200000004410800900000040040198000000004410800900000040040198000000004410800900000040040198000000DE050098000000004410800900000040040198000000002EEFCD5DB77A6704077FD37BC58F7F67038C389DBDFFB5DC04069FD8D2000000FE2582004C00000000 +310 +22E8C3D0C47F42F27D00000000E645108009000000400401980000000044108009000000400401980000000044108009000000400401980000000044108009000000400401980000000044108009000000400401980000000044108009000000E029115CEC7C1ED1D1F51740F001000000000000000000000000804C802DFB +310 +B41E7E5E76608B6E6FFAA4310000000049454E44AE426082 + 0 +ACDSRECORD + 90 + 0 + 2 +AcDbDs::ID +280 + 10 +320 +2F1 + 2 +Thumbnail_Data +280 + 15 + 94 + 1159 +310 +89504E470D0A1A0A0000000D4948445200000100000000AA0803000000755C616800000300504C5445FFFFFF000000C8C8C80000000000000000000000000000000000000000000000000000330000660000990000CC0000FF0033000033330033660033990033CC0033FF0066000066330066660066990066CC0066FF0099 +310 +000099330099660099990099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF0000FF3300FF6600FF9900FFCC00FFFF3300003300333300663300993300CC3300FF3333003333333333663333993333CC3333FF3366003366333366663366993366CC3366FF3399003399333399663399993399CC3399FF33CC00 +310 +33CC3333CC6633CC9933CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF6600006600336600666600996600CC6600FF6633006633336633666633996633CC6633FF6666006666336666666666996666CC6666FF6699006699336699666699996699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066 +310 +FF3366FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF9933009933339933669933999933CC9933FF9966009966339966669966999966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC3399CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFFCC0000CC00 +310 +33CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFFCCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0000FF0033FF0066FF0099FF00CCFF00FFFF3300FF3333 +310 +FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33FFCC66FFCC99FFCCCCFFCCFFFFFF00FFFF33FFFF66FFFF99FFFFCCFFFFFF0000000D0D0D1A1A1A2828283535354343435050505D5D5D6B6B6B787878868686939393A1A1A1AEAEAEBB +310 +BBBBC9C9C9D6D6D6E4E4E4F1F1F1FFFFFF000000000000000000000000000000000000000000000000000000000000B0E2D235000001424944415478DAEDDD3B6EC4301044C1B9FF3127DE4348D487C9E624A02E02F63AF416D4CF0E4CB87EE1A7A20F000000C64B9F27F2F30DD0B9E701487DFEBB00840334000000441080 +310 +090000004004019800000000441080090000004004019800000000441080090000004004019800000000441080090000004004019800000000441080097CE7FD0300F0025CB7A9D7F6675EE07EAE31AF7FF33D23B8FED2F600A8F9D2D7D75BBE8F9D4FC01FC0E21F00BB1BB077022208004075FAAFC2004C00000011046002 +310 +000000104100260000000011046002000000104100260000000011CC02F02F36721F010000FCB93C0013000000800802300100000088200013000000800802300100000088200013000000800802300100000088200013000000800802300100000088200013000000C0C722987B73F606483E000000181FD1E700152D5D54 +310 +A48935320000000049454E44AE426082 + 0 +ACDSRECORD + 90 + 0 + 2 +AcDbDs::ID +280 + 10 +320 +22 + 2 +Thumbnail_Data +280 + 15 + 94 + 931 +310 +89504E470D0A1A0A0000000D49484452000001000000006608030000001376BFA700000300504C5445212830000000C8C8C80000000000000000000000000000000000000000000000000000330000660000990000CC0000FF0033000033330033660033990033CC0033FF0066000066330066660066990066CC0066FF0099 +310 +000099330099660099990099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF0000FF3300FF6600FF9900FFCC00FFFF3300003300333300663300993300CC3300FF3333003333333333663333993333CC3333FF3366003366333366663366993366CC3366FF3399003399333399663399993399CC3399FF33CC00 +310 +33CC3333CC6633CC9933CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF6600006600336600666600996600CC6600FF6633006633336633666633996633CC6633FF6666006666336666666666996666CC6666FF6699006699336699666699996699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066 +310 +FF3366FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF9933009933339933669933999933CC9933FF9966009966339966669966999966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC3399CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFFCC0000CC00 +310 +33CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFFCCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0000FF0033FF0066FF0099FF00CCFF00FFFF3300FF3333 +310 +FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33FFCC66FFCC99FFCCCCFFCCFFFFFF00FFFF33FFFF66FFFF99FFFFCCFFFFFF0000000D0D0D1A1A1A2828283535354343435050505D5D5D6B6B6B787878868686939393A1A1A1AEAEAEBB +310 +BBBBC9C9C9D6D6D6E4E4E4F1F1F1FFFFFF000000000000000000000000000000000000000000000000000000000000B21171F60000003C4944415478DAEDD01102000008C0C0FEFFDF3C4C9220EA8EC68B00000000000000000000000080A51CEAD7806600000000000000000000000000E70AAB4B16F94F60314000000000 +310 +49454E44AE4260827C8701000000000000000080D2016EDA0276E5C382FC0000000049454E44AE426082 + 0 +ENDSEC + 0 +EOF