feat(block-editor): wire Rotation, Alignment and Flip parameter placement

Enable the existing palette rows and BPARAMETER choices. Rotation accepts a
center, picked or numeric radius, and picked or numeric default angle.
Alignment places a tangent direction from its base point. Flip places a
reflection line and state label, with native grip and state expressions.

Extend shared graph append/capacity checks for Flip's additional state node
while preserving existing subgraphs. Keep Polar/Rotation angle labels unique
in either placement order. Add editor graphics, extents and definition
reframing through the existing overlay/history/save paths.

This milestone implements native parameter placement and persistence.
Rotate/Flip action association, reference alignment/grip editing, parameter
property editing and parameter sets remain separate work.

Validation covers command and palette placement, cancellation, local/parent
undo, save/discard, native DWG/DXF objects, existing graph preservation and
the 17 research fixtures. Broader block/dock regressions and editor renders
are also checked.
This commit is contained in:
Karim Jerbi committed 2026-10-15 16:50:47 +01:00
1 parent 6dab5a7229
commit 72c762e944
19 files changed
+1483 -25

No files matched your search

+63 -13
View File
@@ -183,6 +183,30 @@ pub(super) fn prepare(
Ok((handle, slots(None)?))
}
/// Preflight the larger subgraph needed by parameters with state expressions.
pub(super) fn capacity(
document: &CadDocument,
definition: Handle,
nodes: i32,
edges: i32,
) -> Result<(), &'static str> {
let graph = graph(document, definition)?.and_then(|handle| match &document.objects[&handle] {
ObjectType::DynamicBlock(object) => match &object.data {
DynamicBlockData::EvaluationGraph(graph) => Some(graph),
_ => None,
},
_ => None,
});
let start = slots(graph)?;
if start.node.checked_add(nodes - 1).is_none()
|| start.eval.checked_add(nodes - 1).is_none()
|| start.edge.checked_add(edges - 1).is_none()
{
return Err("The block authoring graph has no free node IDs.");
}
Ok(())
}
/// A parameter, its single grip and two position expressions. Supports one-point
/// parameters and two-point parameters configured with one end grip.
/// Graph node IDs, expression IDs and edge IDs are separate namespaces.
@@ -191,6 +215,18 @@ pub(super) fn append(
handle: Handle,
slots: Slots,
expressions: [Handle; 4],
) {
append_components(document, handle, slots, &expressions, &[2, 3]);
}
/// Append a parameter and grip, followed by its location/state expressions.
/// `components` gives the expression offsets in native dependency order.
pub(super) fn append_components(
document: &mut CadDocument,
handle: Handle,
slots: Slots,
expressions: &[Handle],
components: &[usize],
) {
use codec::objects::{BlockEvaluationEdge, BlockEvaluationNode};
let ObjectType::DynamicBlock(object) = document.objects.get_mut(&handle).unwrap() else {
@@ -201,13 +237,14 @@ pub(super) fn append(
};
let n = slots.node;
let e = slots.edge;
let links = [
[e, e, e + 1, e + 2],
[-1, -1, e, e],
[e + 1, e + 1, -1, -1],
[e + 2, e + 2, -1, -1],
];
for (offset, expression) in expressions.into_iter().enumerate() {
let mut links = vec![[-1; 4]; expressions.len()];
links[0] = [e, e, e + 1, e + components.len() as i32];
links[1] = [-1, -1, e, e];
for (index, component) in components.iter().enumerate() {
let edge = e + 1 + index as i32;
links[*component] = [edge, edge, -1, -1];
}
for (offset, expression) in expressions.iter().copied().enumerate() {
graph.nodes.push(BlockEvaluationNode {
id: n + offset as i32,
edge_flags: 32,
@@ -217,11 +254,24 @@ pub(super) fn append(
active_cycles: None,
});
}
for (offset, source_node, destination_node, outgoing_edges) in [
(0, n + 1, n, [-1; 5]),
(1, n, n + 2, [-1, -1, -1, e + 2, -1]),
(2, n, n + 3, [-1, -1, e + 1, -1, -1]),
] {
let dependencies = std::iter::once((0, n + 1, n, [-1; 5])).chain(
components.iter().enumerate().map(|(index, component)| {
let offset = index as i32 + 1;
let previous = if index == 0 { -1 } else { e + offset - 1 };
let next = if index + 1 == components.len() {
-1
} else {
e + offset + 1
};
(
offset,
n,
n + *component as i32,
[-1, -1, previous, next, -1],
)
}),
);
for (offset, source_node, destination_node, outgoing_edges) in dependencies {
graph.edges.push(BlockEvaluationEdge {
id: e + offset,
next_id: 0,
@@ -231,7 +281,7 @@ pub(super) fn append(
outgoing_edges,
});
}
graph.first_node_id = slots.eval + 3;
graph.first_node_id = slots.eval + (expressions.len() as i32 - 1);
graph.first_node_id_copy = graph.first_node_id;
}
+7 -1
View File
@@ -176,10 +176,15 @@ pub(crate) fn create(
if let Some(error) = points_error(kind, base, end, label) {
return Err(error);
}
let names = parameters(document, definition)
let mut names = parameters(document, definition)
.into_iter()
.flat_map(|p| p.labels)
.collect::<Vec<_>>();
names.extend(
super::block_orientation_parameter::parameters(document, definition)
.into_iter()
.map(|p| p.label),
);
let unique = |prefix: &str| {
(1..)
.map(|n| format!("{prefix}{n}"))
@@ -329,6 +334,7 @@ pub(crate) fn create(
#[derive(Default)]
pub(crate) struct Graphic {
pub lines: Vec<[Vector3; 2]>,
pub round_grips: Vec<Vector3>,
pub dimensions: Vec<[Vector3; 2]>,
pub labels: Vec<(Vector3, String)>,
pub grips: Vec<(Vector3, bool)>, // Linear uses a direction arrow; others a square.
+528
View File
@@ -0,0 +1,528 @@
//! Native Rotation, Alignment and Flip parameter placement.
use super::block_authoring::{self as authoring, *};
use super::block_distance_parameter::Graphic;
use codec::objects::{
BlockAlignmentParameter, BlockConnection, BlockElement, BlockEvalValue, BlockFlipGrip,
BlockFlipParameter, BlockGrip, BlockGripExpression, BlockOrientedGrip, BlockParameter,
BlockParameterProperty, BlockParameterValueSet, BlockRotationParameter, BlockTwoPointParameter,
DynamicBlockData, ObjectType,
};
use codec::types::{Transform, Vector3};
use codec::{CadDocument, Handle};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Kind {
Rotation,
Alignment,
Flip,
}
impl Kind {
pub(crate) fn parse(text: &str) -> Option<Self> {
match text.to_ascii_uppercase().as_str() {
"ROTATION" | "R" => Some(Self::Rotation),
"ALIGNMENT" | "A" => Some(Self::Alignment),
"FLIP" | "F" => Some(Self::Flip),
_ => None,
}
}
pub(crate) fn name(self) -> &'static str {
match self {
Self::Rotation => "Rotation",
Self::Alignment => "Alignment",
Self::Flip => "Flip",
}
}
fn classes(self) -> (&'static str, &'static str, &'static str, &'static str) {
match self {
Self::Rotation => (
"BLOCKROTATIONPARAMETER",
"AcDbBlockRotationParameter",
"BLOCKROTATIONGRIP",
"AcDbBlockRotationGrip",
),
Self::Alignment => (
"BLOCKALIGNMENTPARAMETER",
"AcDbBlockAlignmentParameter",
"BLOCKALIGNMENTGRIP",
"AcDbBlockAlignmentGrip",
),
Self::Flip => (
"BLOCKFLIPPARAMETER",
"AcDbBlockFlipParameter",
"BLOCKFLIPGRIP",
"AcDbBlockFlipGrip",
),
}
}
}
pub(crate) struct Parameter<'a> {
pub handle: Handle,
pub kind: Kind,
pub native: &'a BlockTwoPointParameter,
pub reference: Vector3,
pub label: String,
pub offset: f64,
}
fn graph_handle(document: &CadDocument, definition: Handle) -> Option<Handle> {
match document
.objects
.get(&document.extension_dictionary_handle(definition)?)?
{
ObjectType::Dictionary(dictionary) => dictionary.get("ACAD_ENHANCEDBLOCK"),
_ => None,
}
}
pub(crate) fn parameters(document: &CadDocument, definition: Handle) -> Vec<Parameter<'_>> {
let Some(graph) = graph_handle(document, definition) else {
return vec![];
};
let Some(ObjectType::DynamicBlock(object)) = document.objects.get(&graph) else {
return vec![];
};
let DynamicBlockData::EvaluationGraph(data) = &object.data else {
return vec![];
};
data.nodes
.iter()
.filter_map(|node| {
let ObjectType::DynamicBlock(object) = document.objects.get(&node.expression)? else {
return None;
};
if object.owner != graph {
return None;
}
let (kind, native, reference, label, offset) = match &object.data {
DynamicBlockData::RotationParameter(p) => (
Kind::Rotation,
&p.parameter,
p.definition_base_angle_point,
p.angle_name.clone(),
p.angle,
),
DynamicBlockData::AlignmentParameter(p) => (
Kind::Alignment,
&p.parameter,
p.parameter.definition_end_point,
p.parameter.parameter.element.name.clone(),
0.0,
),
DynamicBlockData::FlipParameter(p) => (
Kind::Flip,
&p.parameter,
p.definition_label_point,
p.flip_label.clone(),
0.0,
),
_ => return None,
};
Some(Parameter {
handle: object.handle,
kind,
native,
reference,
label,
offset,
})
})
.collect()
}
pub(crate) fn placement_error(
document: &CadDocument,
definition: Handle,
kind: Kind,
) -> Option<&'static str> {
let nodes = if kind == Kind::Flip { 5 } else { 4 };
authoring::capacity(document, definition, nodes, nodes - 1).err()
}
pub(crate) fn points_error(
kind: Kind,
base: Vector3,
end: Vector3,
reference: Vector3,
) -> Option<&'static str> {
if ![base, end, reference]
.iter()
.all(|p| [p.x, p.y, p.z].iter().all(|v| v.is_finite()))
{
return Some("Specify finite parameter locations.");
}
let d = end - base;
let length = d.x.hypot(d.y);
if !length.is_finite() {
return Some("The parameter dimensions are too large.");
}
if length <= 1e-10 || (end.z - base.z).abs() > 1e-8 || (reference.z - base.z).abs() > 1e-8 {
return Some("Specify distinct parameter points in the same block XY plane.");
}
if kind == Kind::Rotation {
let r = reference - base;
if !r.length().is_finite() || !d.length().is_finite() {
return Some("The parameter dimensions are too large.");
}
if !r.x.hypot(r.y).is_finite() || (r.x.hypot(r.y) - length).abs() > length.max(1.0) * 1e-8 {
return Some("The rotation grip and base-angle point must share the specified radius.");
}
}
None
}
pub(crate) fn create(
document: &mut CadDocument,
definition: Handle,
kind: Kind,
base: Vector3,
end: Vector3,
reference: Vector3,
) -> Result<Handle, &'static str> {
if let Some(error) = points_error(kind, base, end, reference)
.or_else(|| placement_error(document, definition, kind))
{
return Err(error);
}
let mut names = super::block_distance_parameter::parameters(document, definition)
.into_iter()
.flat_map(|p| p.labels)
.collect::<Vec<_>>();
names.extend(
parameters(document, definition)
.into_iter()
.map(|p| p.label),
);
let prefix = match kind {
Kind::Rotation => "Angle",
Kind::Alignment => "Alignment",
Kind::Flip => "Flip state",
};
let label = (1..)
.map(|n| format!("{prefix}{n}"))
.find(|name| names.iter().all(|old| !old.eq_ignore_ascii_case(name)))
.unwrap();
let (graph, slots) = authoring::prepare(document, definition)?;
let (class, cpp, grip_class, grip_cpp) = kind.classes();
register_classes(
document,
&[
(class, cpp, 55),
(grip_class, grip_cpp, 55),
("BLOCKGRIPLOCATIONCOMPONENT", "AcDbBlockGripExpr", 20),
],
);
let count = if kind == Kind::Flip { 5 } else { 4 };
let handles = (0..count)
.map(|_| document.allocate_handle())
.collect::<Vec<_>>();
let element = |offset, name: &str| BlockElement {
eval: expression(slots.eval + offset, NO_VALUE, BlockEvalValue::None),
name: name.into(),
major: MAJOR,
minor: MINOR,
eed_1071: 0,
};
let native = BlockTwoPointParameter {
parameter: BlockParameter {
element: element(
0,
if kind == Kind::Alignment {
&label
} else {
kind.name()
},
),
show_properties: true,
chain_actions: false,
},
definition_base_point: base,
definition_end_point: end,
properties: std::array::from_fn(|i| BlockParameterProperty {
connections: if kind == Kind::Rotation && i >= 2 {
vec![BlockConnection {
code: slots.eval + 1,
name: if i == 2 {
"DisplacementX"
} else {
"DisplacementY"
}
.into(),
}]
} else {
vec![]
},
}),
property_states: if kind == Kind::Rotation {
[0, slots.eval + 1, 0, 0]
} else {
[slots.eval + 1, 0, 0, 0]
},
..Default::default()
};
let data = match kind {
Kind::Rotation => DynamicBlockData::RotationParameter(BlockRotationParameter {
parameter: native,
definition_base_angle_point: reference,
angle_name: label,
angle: (end - base).length() * 0.15,
value_set: BlockParameterValueSet {
flags: 3,
maximum: std::f64::consts::TAU,
..Default::default()
},
..Default::default()
}),
Kind::Alignment => DynamicBlockData::AlignmentParameter(BlockAlignmentParameter {
parameter: native,
align_perpendicular: false,
}),
Kind::Flip => DynamicBlockData::FlipParameter(BlockFlipParameter {
parameter: native,
flip_label: label,
base_state_label: "Not flipped".into(),
flipped_state_label: "Flipped".into(),
definition_label_point: reference,
flags_96: slots.eval + 1,
tooltip: "UpdatedFlip".into(),
..Default::default()
}),
};
document.objects.insert(
handles[0],
ObjectType::DynamicBlock(dynamic(handles[0], graph, class, cpp, data)),
);
let x_offset = if kind == Kind::Flip { 3 } else { 2 };
let grip = BlockGrip {
element: element(1, "Grip"),
flags_91: slots.eval + x_offset,
flags_92: slots.eval + x_offset + 1,
location: if kind == Kind::Rotation { end } else { base },
insert_cycling: kind == Kind::Alignment,
insert_cycling_weight: -1,
};
let data = match kind {
Kind::Rotation => DynamicBlockData::RotationGrip(grip),
Kind::Alignment => DynamicBlockData::AlignmentGrip(BlockOrientedGrip {
grip,
orientation: end - base,
}),
Kind::Flip => DynamicBlockData::FlipGrip(BlockFlipGrip {
grip,
combined_state: slots.eval + 2,
orientation: end - base,
}),
};
document.objects.insert(
handles[1],
ObjectType::DynamicBlock(dynamic(handles[1], graph, grip_class, grip_cpp, data)),
);
for (offset, axis, code, value) in [
(
x_offset,
if kind == Kind::Rotation {
"UpdatedEndX"
} else {
"UpdatedBaseX"
},
40,
BlockEvalValue::Real(0.0),
),
(
x_offset + 1,
if kind == Kind::Rotation {
"UpdatedEndY"
} else {
"UpdatedBaseY"
},
40,
BlockEvalValue::Real(0.0),
),
]
.into_iter()
.chain((kind == Kind::Flip).then_some((2, "UpdatedFlip", 70, BlockEvalValue::Short(0))))
{
let handle = handles[offset as usize];
document.objects.insert(
handle,
ObjectType::DynamicBlock(dynamic(
handle,
graph,
"BLOCKGRIPLOCATIONCOMPONENT",
"AcDbBlockGripExpr",
DynamicBlockData::GripLocationComponent(BlockGripExpression {
eval: expression(slots.eval + offset, code, value),
grip_type: slots.eval,
expression: axis.into(),
}),
)),
);
}
let components: &[usize] = if kind == Kind::Flip {
&[3, 4, 2]
} else {
&[2, 3]
};
authoring::append_components(document, graph, slots, &handles, components);
Ok(handles[0])
}
pub(crate) fn graphic(parameter: &Parameter<'_>) -> Graphic {
let base = parameter.native.definition_base_point;
let end = parameter.native.definition_end_point;
let d = end - base;
let radius = d.x.hypot(d.y).hypot(d.z);
let mut result = Graphic::default();
if !radius.is_finite() || radius <= 1e-10 {
return result;
}
match parameter.kind {
Kind::Rotation => {
let reference = parameter.reference - base;
let u = reference.normalize();
let mut v = if u.z.abs() < 1e-10 && d.z.abs() < 1e-10 {
Vector3::new(-u.y, u.x, 0.0)
} else {
d - u * d.dot(&u)
};
if v.length() <= 1e-10 {
v = Vector3::new(-u.y, u.x, 0.0);
}
if v.length() <= 1e-10 {
v = Vector3::UNIT_Y;
}
let v = v.normalize();
let mut angle = d.dot(&v).atan2(d.dot(&u)).rem_euclid(std::f64::consts::TAU);
if angle.abs() <= 1e-10 {
angle = std::f64::consts::TAU;
}
let at = |angle: f64, r: f64| base + (u * angle.cos() + v * angle.sin()) * r;
result
.lines
.extend([[base, parameter.reference], [base, end]]);
for step in 0..48 {
result.lines.push([
at(angle * step as f64 / 48.0, radius),
at(angle * (step + 1) as f64 / 48.0, radius),
]);
}
result.labels.push((
at(angle * 0.5, radius + parameter.offset),
parameter.label.clone(),
));
result.round_grips.push(end);
}
Kind::Alignment => {
result.lines.push([base, end]);
result.labels.push((end, parameter.label.clone()));
result.grips.push((base, true));
}
Kind::Flip => {
for step in (0..24).step_by(2) {
result.lines.push([
base + d * (step as f64 / 24.0),
base + d * ((step + 1) as f64 / 24.0),
]);
}
result.lines.push([end, parameter.reference]);
result
.labels
.push((parameter.reference, parameter.label.clone()));
result.grips.push((base, true));
}
}
result
}
/// Tail position used to orient a base grip in the shared screen overlay.
pub(crate) fn marker_tail(parameter: &Parameter<'_>) -> Vector3 {
let base = parameter.native.definition_base_point;
let d = parameter.native.definition_end_point - base;
if parameter.kind == Kind::Flip {
base - Vector3::new(-d.y, d.x, 0.0)
} else {
base - d
}
}
pub(crate) fn reframe(
document: &mut CadDocument,
definition: Handle,
transform: &Transform,
) -> usize {
let Some(graph) = graph_handle(document, definition) else {
return 0;
};
let owned = parameters(document, definition)
.iter()
.map(|p| (p.handle, p.native.property_states))
.collect::<Vec<_>>();
let mut changed = 0;
for (handle, ids) in owned {
if let Some(ObjectType::DynamicBlock(object)) = document.objects.get_mut(&handle) {
let native = match &mut object.data {
DynamicBlockData::RotationParameter(p) => {
p.definition_base_angle_point = transform.apply(p.definition_base_angle_point);
&mut p.parameter
}
DynamicBlockData::AlignmentParameter(p) => &mut p.parameter,
DynamicBlockData::FlipParameter(p) => {
p.definition_label_point = transform.apply(p.definition_label_point);
&mut p.parameter
}
_ => continue,
};
native.definition_base_point = transform.apply(native.definition_base_point);
native.definition_end_point = transform.apply(native.definition_end_point);
for point in [
&mut native.updated_base_point,
&mut native.base_point,
&mut native.updated_end_point,
&mut native.end_point,
]
.into_iter()
.flatten()
{
*point = transform.apply(*point);
}
changed += 1;
}
for object in document.objects.values_mut() {
let ObjectType::DynamicBlock(object) = object else {
continue;
};
if object.owner != graph {
continue;
}
let grip = match &mut object.data {
DynamicBlockData::RotationGrip(g)
if g.element.eval.node_id > 0 && ids.contains(&g.element.eval.node_id) =>
{
g
}
DynamicBlockData::AlignmentGrip(g)
if g.grip.element.eval.node_id > 0
&& ids.contains(&g.grip.element.eval.node_id) =>
{
g.orientation = transform.apply_rotation(g.orientation);
&mut g.grip
}
DynamicBlockData::FlipGrip(g)
if g.grip.element.eval.node_id > 0
&& ids.contains(&g.grip.element.eval.node_id) =>
{
g.orientation = transform.apply_rotation(g.orientation);
&mut g.grip
}
_ => continue,
};
grip.location = transform.apply(grip.location);
changed += 1;
}
}
changed
}
#[cfg(test)]
mod tests;
@@ -0,0 +1,328 @@
use super::*;
use crate::scene::{block_distance_parameter, block_point_parameter, block_visibility, Scene};
use codec::{entities::Line, EntityType};
fn fixture() -> (Scene, Handle) {
let mut scene = Scene::new();
scene.define_block_raw(
"PARAMETERS",
Vector3::ZERO,
vec![EntityType::Line(Line::from_points(
Vector3::ZERO,
Vector3::UNIT_X,
))],
);
let handle = scene
.document
.block_records
.get("PARAMETERS")
.unwrap()
.handle;
(scene, handle)
}
fn append_batch(document: &mut CadDocument, definition: Handle) {
for kind in [Kind::Rotation, Kind::Alignment, Kind::Flip] {
create(
document,
definition,
kind,
Vector3::ZERO,
Vector3::new(0.0, 2.0, 0.0),
if kind == Kind::Rotation {
Vector3::new(2.0, 0.0, 0.0)
} else {
Vector3::new(3.0, 3.0, 0.0)
},
)
.unwrap();
}
}
fn graph_data(document: &CadDocument, definition: Handle) -> &codec::objects::BlockEvaluationGraph {
let ObjectType::DynamicBlock(o) =
&document.objects[&graph_handle(document, definition).unwrap()]
else {
panic!()
};
let DynamicBlockData::EvaluationGraph(g) = &o.data else {
panic!()
};
g
}
fn assert_roundtrips(document: &CadDocument, definition: Handle, before: &CadDocument) {
let graph = graph_handle(document, definition).unwrap();
if let Some(old_graph) = graph_handle(before, definition) {
for (h, o) in &before.objects {
if *h != old_graph {
assert_eq!(document.objects.get(h), Some(o), "preserved {h}");
}
}
let old = graph_data(before, definition);
let new = graph_data(document, definition);
assert_eq!(&new.nodes[..old.nodes.len()], &old.nodes);
assert_eq!(&new.edges[..old.edges.len()], &old.edges);
}
let objects = document
.objects
.iter()
.filter_map(|(h, o)| match o {
ObjectType::DynamicBlock(o) if o.owner == graph && !before.objects.contains_key(h) => {
Some((*h, o.data.clone()))
}
_ => None,
})
.collect::<Vec<_>>();
assert_eq!(objects.len(), 13);
let g = graph_data(document, definition);
for node in g
.nodes
.iter()
.filter(|n| objects.iter().any(|(h, _)| *h == n.expression))
{
let incoming = g
.edges
.iter()
.filter(|e| e.destination_node == node.id)
.map(|e| e.id)
.collect::<Vec<_>>();
let outgoing = g
.edges
.iter()
.filter(|e| e.source_node == node.id)
.map(|e| e.id)
.collect::<Vec<_>>();
assert_eq!(
node.node_data,
[
incoming.first().copied().unwrap_or(-1),
incoming.last().copied().unwrap_or(-1),
outgoing.first().copied().unwrap_or(-1),
outgoing.last().copied().unwrap_or(-1)
]
);
}
for extension in ["dwg", "dxf"] {
let bytes = crate::io::save_to_bytes(document, extension, document.version).unwrap();
let read = crate::io::load_bytes(&format!("orientation.{extension}"), bytes).unwrap();
for (h, data) in &objects {
let ObjectType::DynamicBlock(object) = &read.objects[h] else {
panic!("{extension}: native {h}")
};
assert_eq!(&object.data, data, "{extension}: {h}");
assert_eq!(object.owner, graph);
}
assert!(authoring::graph(&read, definition).is_ok());
}
}
#[test]
fn orientation_parameter_native_batch_coexists_and_roundtrips_complete_objects() {
let (mut scene, definition) = fixture();
block_point_parameter::create(
&mut scene.document,
definition,
Vector3::ZERO,
Vector3::UNIT_X,
)
.unwrap();
block_visibility::create(&mut scene.document, definition, Vector3::ZERO).unwrap();
block_distance_parameter::create(
&mut scene.document,
definition,
block_distance_parameter::Kind::Polar,
Vector3::ZERO,
Vector3::UNIT_Y,
Vector3::UNIT_X,
)
.unwrap();
let before = scene.document.clone();
append_batch(&mut scene.document, definition);
assert_roundtrips(&scene.document, definition, &before);
let parameters = parameters(&scene.document, definition);
assert_eq!(parameters[0].label, "Angle2", "share labels with Polar");
for p in &parameters {
let id = p.native.parameter.element.eval.node_id;
assert_eq!(
p.native.property_states,
if p.kind == Kind::Rotation {
[0, id + 1, 0, 0]
} else {
[id + 1, 0, 0, 0]
}
);
}
let flip = parameters.iter().find(|p| p.kind == Kind::Flip).unwrap();
let id = flip.native.parameter.element.eval.node_id;
assert!(scene.document.objects.values().any(|o|matches!(o,ObjectType::DynamicBlock(o) if matches!(&o.data,DynamicBlockData::FlipGrip(g) if g.combined_state==id+2&&g.grip.flags_91==id+3&&g.grip.flags_92==id+4))));
assert!(scene.document.objects.values().any(|o|matches!(o,ObjectType::DynamicBlock(o) if matches!(&o.data,DynamicBlockData::GripLocationComponent(c) if c.expression=="UpdatedFlip"&&c.grip_type==id&&c.eval.value_code==70&&c.eval.value==BlockEvalValue::Short(0)))));
}
#[test]
fn orientation_parameter_rotation_and_polar_share_unique_labels_in_both_orders() {
for rotation_first in [false, true] {
let (mut scene, definition) = fixture();
for rotation in [rotation_first, !rotation_first] {
if rotation {
create(
&mut scene.document,
definition,
Kind::Rotation,
Vector3::ZERO,
Vector3::UNIT_Y,
Vector3::UNIT_X,
)
.unwrap();
} else {
block_distance_parameter::create(
&mut scene.document,
definition,
block_distance_parameter::Kind::Polar,
Vector3::ZERO,
Vector3::UNIT_Y,
Vector3::UNIT_X,
)
.unwrap();
}
}
let rotation = parameters(&scene.document, definition)[0].label.clone();
let polar =
block_distance_parameter::parameters(&scene.document, definition)[0].labels[1].clone();
assert_ne!(rotation, polar);
}
}
#[test]
fn orientation_parameter_flip_capacity_and_invalid_points_leave_document_intact() {
let (mut scene, definition) = fixture();
for kind in [Kind::Rotation, Kind::Alignment, Kind::Flip] {
for end in [
Vector3::ZERO,
Vector3::new(f64::NAN, 1.0, 0.0),
Vector3::new(1.0, 1.0, 1.0),
] {
let before = scene.document.clone();
assert!(create(
&mut scene.document,
definition,
kind,
Vector3::ZERO,
end,
Vector3::UNIT_X
)
.is_err());
assert_eq!(scene.document, before);
}
}
append_batch(&mut scene.document, definition);
let graph = graph_handle(&scene.document, definition).unwrap();
if let ObjectType::DynamicBlock(o) = scene.document.objects.get_mut(&graph).unwrap() {
if let DynamicBlockData::EvaluationGraph(g) = &mut o.data {
g.first_node_id = i32::MAX - 4;
}
}
let before = scene.document.clone();
assert!(placement_error(&scene.document, definition, Kind::Flip).is_some());
assert!(create(
&mut scene.document,
definition,
Kind::Flip,
Vector3::ZERO,
Vector3::UNIT_X,
Vector3::ZERO
)
.is_err());
assert_eq!(scene.document, before);
assert!(placement_error(&scene.document, definition, Kind::Rotation).is_none());
create(
&mut scene.document,
definition,
Kind::Rotation,
Vector3::ZERO,
Vector3::UNIT_Y,
Vector3::UNIT_X,
)
.unwrap();
assert_eq!(
graph_data(&scene.document, definition).first_node_id,
i32::MAX
);
}
#[test]
fn orientation_parameter_reframe_and_editor_extents_preserve_native_points() {
let (mut scene, definition) = fixture();
append_batch(&mut scene.document, definition);
let original = parameters(&scene.document, definition)
.iter()
.map(|p| {
(
p.native.definition_base_point,
p.native.definition_end_point,
p.reference,
)
})
.collect::<Vec<_>>();
let transform = Transform::from_translation(Vector3::new(10.0, 20.0, 0.0));
assert_eq!(reframe(&mut scene.document, definition, &transform), 6);
for (p, (base, end, reference)) in parameters(&scene.document, definition).iter().zip(original)
{
assert_eq!(p.native.definition_base_point, transform.apply(base));
assert_eq!(p.native.definition_end_point, transform.apply(end));
assert_eq!(p.reference, transform.apply(reference));
assert!(graphic(p)
.lines
.into_iter()
.flatten()
.all(|p| p.x.is_finite() && p.y.is_finite() && p.z.is_finite()));
}
scene.block_edit_block = Some(definition);
let (_, max) = scene.visible_block_bounds(definition).unwrap();
assert!(max.x >= 13.0 && max.y >= 23.0);
scene.block_edit_block = None;
let (_, max) = scene.visible_block_bounds(definition).unwrap();
assert!(max.x <= 1.0 && max.y < 1.0);
}
#[test]
fn orientation_parameter_rotation_270_graphic_retains_counterclockwise_arc() {
let (mut scene, definition) = fixture();
create(
&mut scene.document,
definition,
Kind::Rotation,
Vector3::ZERO,
-Vector3::UNIT_Y,
Vector3::UNIT_X,
)
.unwrap();
let g = graphic(&parameters(&scene.document, definition)[0]);
assert!(g.lines.iter().flatten().any(|p| p.x < -0.99));
}
#[test]
#[ignore = "requires OCS_DYNAMIC_BLOCK_FIXTURES with local DWG fixtures"]
fn orientation_parameter_fixture_batch_roundtrips() {
use sha2::{Digest, Sha256};
let directory = std::env::var_os("OCS_DYNAMIC_BLOCK_FIXTURES").expect("fixture directory");
let mut paths = std::fs::read_dir(directory)
.unwrap()
.map(|p| p.unwrap().path())
.filter(|p| p.extension().is_some_and(|e| e.eq_ignore_ascii_case("dwg")))
.collect::<Vec<_>>();
paths.sort();
assert_eq!(paths.len(), 17);
for path in paths {
let bytes = std::fs::read(&path).unwrap();
let digest = Sha256::digest(&bytes);
let filename = path.file_name().unwrap().to_string_lossy();
let mut document = crate::io::load_bytes(&filename, bytes).unwrap();
let definition = document
.block_records
.iter()
.map(|b| b.handle)
.find(|h| {
authoring::graph(&document, *h).is_ok_and(|g| g.is_some())
&& placement_error(&document, *h, Kind::Flip).is_none()
})
.unwrap();
let before = document.clone();
append_batch(&mut document, definition);
assert_roundtrips(&document, definition, &before);
assert_eq!(Sha256::digest(std::fs::read(&path).unwrap()), digest);
eprintln!("{filename}: Rotation/Alignment/Flip native DWG/DXF objects and graph append passed; original graph/objects/checksum preserved");
}
}
+16
View File
@@ -1687,6 +1687,22 @@ impl Scene {
fold!(Some((point, point)));
}
}
for parameter in super::block_orientation_parameter::parameters(&self.document, block) {
let graphic = super::block_orientation_parameter::graphic(&parameter);
for point in graphic
.lines
.into_iter()
.flatten()
.chain(graphic.labels.into_iter().map(|(p, _)| p))
.chain([
parameter.native.definition_base_point,
parameter.native.definition_end_point,
])
{
let point = glam::DVec3::new(point.x, point.y, point.z);
fold!(Some((point, point)));
}
}
if let Some(parameter) = super::block_visibility::parameter(&self.document, block) {
let point = parameter.def_point;
let point = glam::DVec3::new(point.x, point.y, point.z);
+1
View File
@@ -13,6 +13,7 @@ pub mod block_visibility;
pub(crate) mod block_authoring;
pub(crate) mod block_point_parameter;
pub(crate) mod block_distance_parameter;
pub(crate) mod block_orientation_parameter;
pub mod cache;
pub mod compare;
pub mod convert;
+6
View File
@@ -565,6 +565,12 @@ impl Scene {
block_record,
local_from_old,
);
let parameter_changes = parameter_changes
+ super::block_orientation_parameter::reframe(
&mut self.document,
block_record,
local_from_old,
);
if !changed.is_empty() || parameter_changes > 0 {
self.rebuild_derived_caches();
}