Merge pull request #890 from ramox81/codex/fix-jogged-radius-properties

Fix jogged radius properties and layout
This commit is contained in:
Hakan Seven authored and GitHub committed 2026-08-28 00:35:06 +03:00
commit dffae3f16d
6 files changed
+1113 -608

No files matched your search

-10
View File
@@ -1063,16 +1063,6 @@ impl OpenCADStudio {
&self.tabs[i].scene.document,
));
if matches!(d, acadrust::entities::Dimension::LargeRadial(_)) {
if let Some(index) = sections
.iter()
.position(|section| section.title == t!("Misc").as_ref())
{
let misc = sections.remove(index);
sections.push(misc);
}
}
// Prefer entity-level dimension-variable overrides;
// fall back to the assigned style values.
use crate::entities::dim_override as dov;
+37 -2
View File
@@ -1981,12 +1981,20 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
self.tabs[i].scene.document.get_entity(handle),
Some(acadrust::EntityType::Dimension(_))
) {
crate::entities::dim_override::set_property(
let applied = crate::entities::dim_override::set_property(
&mut self.tabs[i].scene.document,
handle,
field,
&value,
);
if applied && field == "dim_text_inside" {
if let Some(acadrust::EntityType::Dimension(
acadrust::entities::Dimension::LargeRadial(dimension),
)) = self.tabs[i].scene.document.get_entity_mut(handle)
{
dimension.base.text_user_positioned = false;
}
}
}
}
} else if field == "vscale_std" {
@@ -2403,6 +2411,13 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
}
_ => None,
});
if matches!(field, "text_x" | "text_y") {
crate::entities::dimension::materialize_large_radial_text_position(
&mut self.tabs[i].scene.document,
handle,
&value,
);
}
if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle)
{
crate::scene::view::dispatch::apply_geom_prop_in_working_plane(
@@ -2649,12 +2664,25 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
self.tabs[i].scene.document.get_entity(handle),
Some(acadrust::EntityType::Dimension(_))
) {
crate::entities::dim_override::set_property(
let applied = crate::entities::dim_override::set_property(
&mut self.tabs[i].scene.document,
handle,
field,
&val,
);
if applied && field == "dim_text_inside" {
if let Some(acadrust::EntityType::Dimension(
acadrust::entities::Dimension::LargeRadial(
dimension,
),
)) = self.tabs[i]
.scene
.document
.get_entity_mut(handle)
{
dimension.base.text_user_positioned = false;
}
}
}
}
"hyperlink" => {
@@ -2751,6 +2779,13 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
}
_ => None,
});
if matches!(field, "text_x" | "text_y") {
crate::entities::dimension::materialize_large_radial_text_position(
&mut self.tabs[i].scene.document,
handle,
&val,
);
}
if let Some(entity) = self.tabs[i]
.scene
.document
+20 -5
View File
@@ -403,8 +403,8 @@ pub fn set_property(
let size = current.abs().max(0.01);
let value = match trimmed.to_ascii_lowercase().as_str() {
"none" => 0.0,
"center marks" => size,
"centerlines" => -size,
"mark" | "center marks" => size,
"line" | "centerlines" => -size,
_ => return false,
};
set(doc, handle, DIMCEN, Some(XDataValue::Real(value)));
@@ -423,6 +423,7 @@ pub fn set_property(
return true;
}
let handle_field = match field {
"dim_radial_arrow" => Some(DIMLDRBLK),
"dim_arrowhead_1" => Some(DIMBLK1),
"dim_arrowhead_2" => Some(DIMBLK2),
"dim_linetype" => Some(DIMLTYPE),
@@ -433,7 +434,7 @@ pub fn set_property(
};
if let Some(code) = handle_field {
let resolved = match field {
"dim_arrowhead_1" | "dim_arrowhead_2" => {
"dim_radial_arrow" | "dim_arrowhead_1" | "dim_arrowhead_2" => {
if trimmed == "Closed filled" {
Some(Handle::NULL)
} else {
@@ -638,6 +639,10 @@ pub fn set_property(
"dim_line_lineweight" | "dim_ext_line_lineweight" => {
parse_lineweight_label(trimmed)
}
"dim_precision"
| "dim_alt_precision"
| "dim_tolerance_precision"
| "dim_alt_tolerance_precision" => parse_precision_label(trimmed),
"dim_ext_line_fixed"
| "dim_text_outside_align"
| "dim_text_inside_align"
@@ -729,8 +734,8 @@ pub fn set_property(
_ => trimmed.parse().ok(),
},
"dim_tolerance_alignment" => match trimmed.to_ascii_lowercase().as_str() {
"align decimal separators" => Some(0),
"align operational symbols" => Some(1),
"decimal separator" | "align decimal separators" => Some(0),
"operational symbols" | "align operational symbols" => Some(1),
_ => trimmed.parse().ok(),
},
_ => trimmed.parse().ok(),
@@ -744,6 +749,16 @@ pub fn set_property(
false
}
fn parse_precision_label(value: &str) -> Option<i16> {
if let Some(decimals) = value.strip_prefix("0.") {
return (!decimals.is_empty()
&& decimals.len() <= 8
&& decimals.bytes().all(|digit| digit == b'0'))
.then_some(decimals.len() as i16);
}
value.parse().ok()
}
fn parse_lineweight_label(value: &str) -> Option<i16> {
match value.trim() {
"ByLayer" => Some(-1),
+982 -396
View File
File diff suppressed because it is too large. Load diff
+74 -187
View File
@@ -356,6 +356,38 @@ fn dim_seg(a: Vector3, b: Vector3, common: &acadrust::entities::EntityCommon) ->
})
}
fn dim_geom_entities(
geometry: &crate::scene::convert::tessellate::DimGeom,
ext_common: &acadrust::entities::EntityCommon,
dim_common: &acadrust::entities::EntityCommon,
) -> Vec<EntityType> {
let mut entities = Vec::new();
let point = |value: [f32; 3]| {
Vector3::new(value[0] as f64, value[1] as f64, value[2] as f64)
};
for (points, common) in [
(geometry.ext_lines.as_slice(), ext_common),
(geometry.dim_lines.as_slice(), dim_common),
] {
for run in points.split(|point| point[0].is_nan()) {
for pair in run.windows(2) {
entities.push(dim_seg(point(pair[0]), point(pair[1]), common));
}
}
}
for triangle in geometry.arrow_fill.chunks_exact(3) {
let mut solid = acadrust::entities::Solid::triangle(
point(triangle[0]),
point(triangle[1]),
point(triangle[2]),
);
solid.common = dim_common.clone();
solid.common.handle = Handle::NULL;
entities.push(EntityType::Solid(solid));
}
entities
}
/// A dimension-line terminator at `tip`, body extending back along the unit
/// vector `(dx,dy)` (toward the dim line). When DIMTSZ>0 it's an oblique 45°
/// tick; otherwise a closed *filled* arrowhead (DXF SOLID) of length DIMASZ —
@@ -597,80 +629,23 @@ struct DimMetrics {
dimclrt: i16,
dimlwd: i16,
dimlwe: i16,
/// Resolved terminator shapes for the first / second end (DIMTSZ tick,
/// DIMBLK/DIMBLK1/DIMBLK2 per DIMSAH, else closed-filled), so the bake
/// reproduces the style's actual arrow type.
dimltype: Handle,
dimltex1: Handle,
arrow1: crate::scene::convert::tessellate::ArrowKind,
arrow2: crate::scene::convert::tessellate::ArrowKind,
}
/// Metrics from the dim's style, mirroring what the live renderer applies, so a
/// baked block reproduces the same gaps, arrow type, suppression, colours and
/// lineweights: DIMASZ (arrow), DIMCEN (centre mark), DIMEXO/DIMEXE (extension
/// gap/overshoot), DIMTSZ (oblique tick; >0 = ticks not arrows), DIMDLE (dim
/// line overshoot past ticks), DIMSE1/2 + DIMSD1/2 (extension / dim-line
/// suppression), DIMCLRD/E/T (colours) and DIMLWD/E (lineweights).
/// Resolve style metrics used by saved dimension geometry.
fn dim_metrics(dim: &Dimension, doc: &CadDocument) -> DimMetrics {
let name = dim.base().style_name.as_str();
let mut effective_style = doc.dim_styles.iter().find(|s| {
s.name.eq_ignore_ascii_case(name)
|| (name.trim().is_empty() && s.name.eq_ignore_ascii_case("Standard"))
}).cloned();
if let Some(style) = &mut effective_style {
use crate::entities::dim_override as ov;
let data = &dim.base().common.extended_data;
macro_rules! real {
($field:ident, $code:ident) => {
if let Some(value) = ov::real(data, ov::$code) {
style.$field = value;
}
};
}
macro_rules! int {
($field:ident, $code:ident) => {
if let Some(value) = ov::int(data, ov::$code) {
style.$field = value;
}
};
}
macro_rules! flag {
($field:ident, $code:ident) => {
if let Some(value) = ov::int(data, ov::$code) {
style.$field = value != 0;
}
};
}
macro_rules! handle {
($field:ident, $code:ident) => {
if let Some(value) = ov::handle(data, ov::$code) {
style.$field = value;
}
};
}
real!(dimscale, DIMSCALE);
real!(dimasz, DIMASZ);
real!(dimcen, DIMCEN);
real!(dimexo, DIMEXO);
real!(dimexe, DIMEXE);
real!(dimtsz, DIMTSZ);
real!(dimdle, DIMDLE);
real!(dimfxl, DIMFXL);
flag!(dimfxlon, DIMFXLON);
flag!(dimse1, DIMSE1);
flag!(dimse2, DIMSE2);
flag!(dimsd1, DIMSD1);
flag!(dimsd2, DIMSD2);
flag!(dimsoxd, DIMSOXD);
flag!(dimsah, DIMSAH);
int!(dimclrd, DIMCLRD);
int!(dimclre, DIMCLRE);
int!(dimclrt, DIMCLRT);
int!(dimlwd, DIMLWD);
int!(dimlwe, DIMLWE);
handle!(dimblk, DIMBLK);
handle!(dimblk1, DIMBLK1);
handle!(dimblk2, DIMBLK2);
}
let effective_style = doc
.dim_styles
.iter()
.find(|style| {
style.name.eq_ignore_ascii_case(name)
|| (name.trim().is_empty() && style.name.eq_ignore_ascii_case("Standard"))
})
.map(|style| crate::entities::dimension::resolved_dimension_style(style, dim, doc));
let style = effective_style.as_ref();
let scale = style
.map(|s| if s.dimscale > 1e-6 { s.dimscale } else { 1.0 })
@@ -679,14 +654,14 @@ fn dim_metrics(dim: &Dimension, doc: &CadDocument) -> DimMetrics {
let dimasz = style.map(|s| s.dimasz * scale).unwrap_or(0.18 * scale).max(1e-6);
let dimtsz = style.map(|s| s.dimtsz * scale).unwrap_or(0.0);
let asz = dimasz as f32;
// Resolve the terminator shapes exactly like the live render: ticks when
// DIMTSZ>0, otherwise the DIMBLK / DIMBLK1+DIMBLK2 (per DIMSAH) arrow blocks,
// else closed-filled.
let (arrow1, arrow2) = if dimtsz > 1e-9 {
let t = ArrowKind::Tick { size: dimtsz as f32 };
(t.clone(), t)
} else if let Some(s) = style {
if matches!(dim, Dimension::Diameter(_)) {
if matches!(dim, Dimension::Radius(_) | Dimension::LargeRadial(_)) {
let arrow = arrow_from_block(doc, s.dimldrblk, asz);
(arrow.clone(), arrow)
} else if matches!(dim, Dimension::Diameter(_)) {
use crate::entities::dim_override as ov;
let data = &dim.base().common.extended_data;
let first = ov::handle(data, ov::DIMBLK1)
@@ -723,6 +698,8 @@ fn dim_metrics(dim: &Dimension, doc: &CadDocument) -> DimMetrics {
dimclrt: style.map(|s| s.dimclrt).unwrap_or(0),
dimlwd: style.map(|s| s.dimlwd).unwrap_or(-2),
dimlwe: style.map(|s| s.dimlwe).unwrap_or(-2),
dimltype: style.map(|s| s.dimltex_handle).unwrap_or(Handle::NULL),
dimltex1: style.map(|s| s.dimltex1_handle).unwrap_or(Handle::NULL),
arrow1,
arrow2,
}
@@ -744,6 +721,21 @@ fn dim_common(base: &acadrust::entities::EntityCommon, clr: i16, lw: i16) -> aca
c
}
fn with_dim_linetype(
mut common: acadrust::entities::EntityCommon,
doc: &CadDocument,
handle: Handle,
) -> acadrust::entities::EntityCommon {
common.linetype = doc
.line_types
.iter()
.find(|line_type| line_type.handle == handle)
.map(|line_type| line_type.name.clone())
.unwrap_or_else(|| "Continuous".to_string());
common.linetype_handle = (!handle.is_null()).then_some(handle);
common
}
/// Baked geometry for an angular dimension, matching the live render exactly:
/// an extension line from each measured ray point flush out to the dimension
/// arc, the swept arc, and a terminator at each arc end. (The live angular path
@@ -806,49 +798,6 @@ fn angular_block_segs(
out
}
fn jogged_radial_break(
chord: Vector3,
jog: Vector3,
override_center: Vector3,
jog_angle: f64,
) -> (Vector3, Vector3) {
let radial = norm2(
chord.x - override_center.x,
chord.y - override_center.y,
1.0,
0.0,
);
let (sin, cos) = jog_angle.sin_cos();
let transverse = (
radial.0 * cos - radial.1 * sin,
radial.0 * sin + radial.1 * cos,
);
let length = ((chord.x - override_center.x).powi(2)
+ (chord.y - override_center.y).powi(2))
.sqrt();
let half = (length * 0.04).max(1e-6);
let first = Vector3::new(
jog.x - transverse.0 * half,
jog.y - transverse.1 * half,
jog.z,
);
let second = Vector3::new(
jog.x + transverse.0 * half,
jog.y + transverse.1 * half,
jog.z,
);
let distance_squared = |point: Vector3| {
(point.x - chord.x).powi(2)
+ (point.y - chord.y).powi(2)
+ (point.z - chord.z).powi(2)
};
if distance_squared(first) <= distance_squared(second) {
(first, second)
} else {
(second, first)
}
}
/// The text anchor for a radial leader: the saved text middle point when set,
/// else the midpoint of `a` and `b` — mirroring the live `dimension_text_position`.
fn dim_text_anchor(
@@ -1246,38 +1195,14 @@ fn explode_dimension(dim: &Dimension, doc: &CadDocument) -> Vec<EntityType> {
));
}
}
Dimension::LargeRadial(d) => {
let (near, far) = jogged_radial_break(
d.chord_point,
d.jog_point,
d.override_center,
d.jog_angle,
);
if !met.dimsd1 {
result.push(make_seg(&d.chord_point, &near, &dim_c));
result.push(make_seg(&near, &far, &dim_c));
result.push(make_seg(&far, &d.override_center, &dim_c));
Dimension::LargeRadial(_) => {
if let Some(geometry) =
crate::entities::dimension::baked_large_radial_geometry(dim, doc)
{
let radial_ext_c = with_dim_linetype(ext_c.clone(), doc, met.dimltex1);
let radial_dim_c = with_dim_linetype(dim_c.clone(), doc, met.dimltype);
result.extend(dim_geom_entities(&geometry, &radial_ext_c, &radial_dim_c));
}
let len = ((near.x - d.chord_point.x).powi(2)
+ (near.y - d.chord_point.y).powi(2))
.sqrt()
.max(1e-12);
result.extend(dim_terminator(
d.chord_point,
(near.x - d.chord_point.x) / len,
(near.y - d.chord_point.y) / len,
&met.arrow1,
&dim_c,
));
let radius = ((d.definition_point.x - d.chord_point.x).powi(2)
+ (d.definition_point.y - d.chord_point.y).powi(2))
.sqrt();
result.extend(dim_center_mark(
d.definition_point,
met.dimcen,
radius,
&dim_c,
));
}
}
@@ -1371,23 +1296,7 @@ fn next_dimension_block_name(doc: &CadDocument, next: &mut u64) -> String {
}
}
/// Bake an anonymous `*D<n>` geometry block for every DIMENSION that doesn't
/// already own one, so the file is valid for AutoCAD-family readers.
///
/// OCS renders dimensions by re-tessellating them on the fly and never
/// materialises the `*D` block that a DWG `DIMENSION` is supposed to reference
/// (the lines / arrows / text that AutoCAD actually draws). A dimension created
/// in OCS therefore goes out referencing a block that doesn't exist, and the
/// writer emits a null block handle — strict readers (DWG TrueView, QCAD) drop
/// the dimension or demand a recovery, and lenient ones (BricsCAD) regenerate it
/// at a different position. Call this on the document about to be written so each
/// such dimension gets a real block built from its exploded geometry (extension
/// lines + dimension line + measurement text, the same decomposition EXPLODE
/// uses) and its `block_name` points at it.
///
/// Dimensions that already reference an existing block (e.g. imported from a real
/// DWG, or copied via the `*D`-cloning copy path) are left untouched so their
/// original graphics are preserved.
/// Build missing anonymous dimension geometry blocks before writing.
pub fn bake_dimension_blocks(doc: &mut CadDocument) {
// Keep group-10 in step and find missing blocks in one entity pass. Existing
// `*D` blocks are the save cache: only invalidated/new dimensions enter the
@@ -1451,35 +1360,13 @@ pub fn bake_dimension_blocks(doc: &mut CadDocument) {
if let Some(EntityType::Dimension(d)) = doc.get_entity_mut(handle) {
d.base_mut().block_name = name;
// The block we just baked holds the dimension graphics in absolute
// WCS, so the DWG group-12 insertion point (base.insertion_point)
// MUST be the origin. A reader that positions the *D block by that
// point (BricsCAD / ODA) otherwise draws it shifted by the offset,
// while OCS — which renders the block in place — shows it correctly.
// OCS's dimension commands seed insertion_point with the text
// anchor; reset it here so the saved dimension lands identically in
// every application. (#181)
// Baked geometry uses absolute coordinates.
d.base_mut().insertion_point = Vector3::new(0.0, 0.0, 0.0);
}
}
}
/// Drop a dimension's baked `*D` block so the next save regenerates it from the
/// dimension's current definition points / text / style.
///
/// OCS renders a dimension live from its definition points, but exports a baked
/// `*D` block that other applications (BricsCAD / ODA) draw instead. An in-place
/// edit — grip drag, DIMTEDIT, restyle, text edit, DIMSPACE — changes the
/// definition points while leaving the old block, so without this the export
/// keeps the pre-edit graphics and the dimension appears wrong everywhere but in
/// OCS. Call this after any such edit; [`bake_dimension_blocks`] then rebuilds a
/// fresh block on save.
///
/// The transform path (MOVE / COPY / PASTE) keeps its own block in sync via
/// `define_transformed_block` and must NOT call this. The removed block's record
/// and owned entities are deleted too, so re-baking on every edit can't
/// accumulate orphan `*D` blocks. No-op when the dimension has no baked block
/// yet (the pending path bakes it fresh on save). (#181)
/// Drop baked dimension geometry so the next save regenerates it.
pub fn invalidate_dim_block(doc: &mut CadDocument, handle: Handle) {
let bn = match doc.get_entity(handle) {
Some(EntityType::Dimension(d)) => d.base().block_name.clone(),
-8
View File
@@ -89,14 +89,6 @@ pub fn general_section(entity: &EntityType) -> PropSection {
],
};
if matches!(entity, EntityType::Point(_)) {
let hyperlink = section.props.iter().position(|prop| prop.field == "hyperlink");
let transparency = section.props.iter().position(|prop| prop.field == "transparency");
if let (Some(hyperlink), Some(transparency)) = (hyperlink, transparency) {
section.props.swap(hyperlink, transparency);
}
}
// Thickness (DXF 39) is a General-group property, but only the entity
// types that carry an extrusion thickness expose it (line, circle, arc,
// polyline, text, 2D solid, …). Show it right after Hyperlink for those.