forked from 1b-agent/opencadstudio-upstream
fix(dim): draw a dimension from the picture it carries
A DIMENSION references the block "that contains the entities that make up the dimension picture", and that block is the picture: AutoCAD requires it and draws it, BricsCAD draws it when present and only renders from the dimension variables when it is missing. OCS re-derived the picture from DIMVARS every time and ignored the block entirely. That works while a style agrees with what the drawing actually drew. A Revit-exported schedule here does not: its style stores DIMTXT already in drawing units beside a DIMSCALE meant to scale it, so the text came out three hundred times too tall, and each dimension carries a DIMFXL override that is likewise already in drawing units, so the extension lines ran nearly a million units past the points they measure. Neither is guesswork about units now. The block holds the geometry the drawing was authored with, so it is drawn: the contents are already in world space and the block's base point is the origin, and the identity instance that carries them exists only to reuse the scene graph's ByBlock / layer-0 colour resolution. Every wire answers to the dimension, so a pick still selects the dimension rather than one of its pieces. Re-deriving stays as the fallback for a dimension with no block. Annotative dimensions keep the old path: their several representations are separate blocks and choosing between them is what the annotation machinery already does, while the doctrine above assumes one picture. A picture is only good while its inputs hold, so it is dropped when they change: editing a dimension's properties, and saving the dimension style it is drawn with. Entity edits already dropped it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
039f703d7f
commit
188d1a9ccb
3 files changed
+141
No files matched your search
@@ -1828,6 +1828,16 @@ impl OpenCADStudio {
|
|||||||
pub(super) fn invalidate_property_targets(&mut self, i: usize, handles: &[Handle]) {
|
pub(super) fn invalidate_property_targets(&mut self, i: usize, handles: &[Handle]) {
|
||||||
let mut context_object_changed = false;
|
let mut context_object_changed = false;
|
||||||
for &handle in handles {
|
for &handle in handles {
|
||||||
|
// A dimension is drawn from the block holding its picture, and that
|
||||||
|
// picture was made under the settings just edited. Drop it so the
|
||||||
|
// dimension is drawn afresh — otherwise the edit changes the stored
|
||||||
|
// variables and nothing on screen.
|
||||||
|
if matches!(
|
||||||
|
self.tabs[i].scene.document.get_entity(handle),
|
||||||
|
Some(acadrust::EntityType::Dimension(_))
|
||||||
|
) {
|
||||||
|
self.tabs[i].scene.invalidate_dim_block_recorded(handle);
|
||||||
|
}
|
||||||
context_object_changed |= self.tabs[i]
|
context_object_changed |= self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
.sync_displayed_annotation_context(handle);
|
.sync_displayed_annotation_context(handle);
|
||||||
|
|||||||
@@ -467,6 +467,29 @@ impl OpenCADStudio {
|
|||||||
ds.dimtofl = self.ds_dimtofl;
|
ds.dimtofl = self.ds_dimtofl;
|
||||||
ds.dimalt = self.ds_dimalt;
|
ds.dimalt = self.ds_dimalt;
|
||||||
ds.dimapost = self.ds_dimapost.clone();
|
ds.dimapost = self.ds_dimapost.clone();
|
||||||
|
|
||||||
|
// Same reason as a per-dimension edit: every dimension on this style is
|
||||||
|
// drawn from a block made under the settings that just changed, so the
|
||||||
|
// pictures are stale. Drop them and let each be drawn again. Without
|
||||||
|
// this an edit here would move the numbers and leave the drawing alone.
|
||||||
|
let edited = self.dimstyle_selected.clone();
|
||||||
|
let stale: Vec<acadrust::Handle> = self.tabs[tab]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.entities()
|
||||||
|
.filter_map(|entity| match entity {
|
||||||
|
acadrust::EntityType::Dimension(dim)
|
||||||
|
if dim.base().style_name.eq_ignore_ascii_case(&edited) =>
|
||||||
|
{
|
||||||
|
Some(entity.common().handle)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
for handle in stale {
|
||||||
|
self.tabs[tab].scene.invalidate_dim_block_recorded(handle);
|
||||||
|
}
|
||||||
|
|
||||||
self.command_line
|
self.command_line
|
||||||
.push_output(crate::tf!("DimStyle '{}' updated.", self.dimstyle_selected).as_ref());
|
.push_output(crate::tf!("DimStyle '{}' updated.", self.dimstyle_selected).as_ref());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -737,6 +737,114 @@ pub(crate) fn tessellate_entity(
|
|||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Dimension baked-block fast path ─────────────────────────────────────
|
||||||
|
//
|
||||||
|
// A DIMENSION carries the block "that contains the entities that make up
|
||||||
|
// the dimension picture" (DXF group 2), and that block IS the picture:
|
||||||
|
// AutoCAD requires it and draws it, BricsCAD draws it when present and only
|
||||||
|
// falls back to rendering from the dimension variables when it is missing.
|
||||||
|
// OCS re-derived the picture from DIMVARS every time instead, which means a
|
||||||
|
// drawing whose style disagrees with what it actually drew comes out wrong
|
||||||
|
// — a DIMTXT stored in different units from the DIMSCALE applied to it, or
|
||||||
|
// a per-object override that is already in drawing units, and the text and
|
||||||
|
// extension lines land hundreds of times too large.
|
||||||
|
//
|
||||||
|
// Drawing the block puts OCS on the same footing as the CAD that wrote the
|
||||||
|
// file: it shows what the file says it looks like. Re-deriving stays as the
|
||||||
|
// fallback, for a dimension with no block (one OCS just created, or one
|
||||||
|
// whose block was dropped because it was edited).
|
||||||
|
//
|
||||||
|
// Annotative dimensions keep the old path: their several representations
|
||||||
|
// are separate blocks, and choosing between them is what the annotation
|
||||||
|
// machinery already does. The doctrine above assumes one picture.
|
||||||
|
if let EntityType::Dimension(dim) = e {
|
||||||
|
let baked = Some(dim.base().block_name.trim())
|
||||||
|
.filter(|name| !name.is_empty())
|
||||||
|
.filter(|_| !crate::scene::annotative::is_annotative(document, e))
|
||||||
|
.and_then(|name| {
|
||||||
|
document
|
||||||
|
.block_records
|
||||||
|
.iter()
|
||||||
|
.find(|record| record.name.eq_ignore_ascii_case(name))
|
||||||
|
})
|
||||||
|
.filter(|record| !record.entity_handles.is_empty());
|
||||||
|
if let Some(record) = baked {
|
||||||
|
// The block's contents are already in world space and its base
|
||||||
|
// point is the origin, so the instance that carries them is the
|
||||||
|
// identity — it exists only to reuse the graph's ByBlock / layer-0
|
||||||
|
// colour resolution and its handling of anything nested inside.
|
||||||
|
let mut insert = acadrust::entities::Insert::new(
|
||||||
|
record.name.clone(),
|
||||||
|
acadrust::types::Vector3::ZERO,
|
||||||
|
);
|
||||||
|
insert.common = dim.base().common.clone();
|
||||||
|
|
||||||
|
let depths = rustc_hash::FxHashMap::default();
|
||||||
|
let graph = crate::scene::render_graph::RenderSceneGraph::new(
|
||||||
|
document,
|
||||||
|
None,
|
||||||
|
annotation_scale_handle,
|
||||||
|
true,
|
||||||
|
&depths,
|
||||||
|
);
|
||||||
|
let mut wires = Vec::new();
|
||||||
|
graph.walk_insert(
|
||||||
|
&insert,
|
||||||
|
h,
|
||||||
|
|_, _| true,
|
||||||
|
|sub, context| {
|
||||||
|
let has_book_color = view::render::has_resolved_book_color(document, sub);
|
||||||
|
let color_byblock =
|
||||||
|
!has_book_color && sub.common().color == acadrust::types::Color::ByBlock;
|
||||||
|
let color_layer0 = !has_book_color
|
||||||
|
&& view::render::is_effective_layer_zero(&sub.common().layer)
|
||||||
|
&& sub.common().color == acadrust::types::Color::ByLayer;
|
||||||
|
let style = context.style_for(document, sub);
|
||||||
|
let mut placed = sub.clone();
|
||||||
|
placed.apply_transform(&context.transform);
|
||||||
|
let sub_wires = tessellate_entity(
|
||||||
|
document,
|
||||||
|
selected,
|
||||||
|
active_viewport,
|
||||||
|
bg_color,
|
||||||
|
anno_scale,
|
||||||
|
annotation_scale_handle,
|
||||||
|
&placed,
|
||||||
|
block_cache,
|
||||||
|
view_aabb,
|
||||||
|
world_per_pixel,
|
||||||
|
paper_space,
|
||||||
|
);
|
||||||
|
for mut wire in sub_wires {
|
||||||
|
// Every wire answers to the dimension, so a pick selects
|
||||||
|
// the dimension rather than one of its pieces.
|
||||||
|
wire.name = h.value().to_string();
|
||||||
|
if sel {
|
||||||
|
wire.selected = true;
|
||||||
|
wire.color = WireModel::SELECTED;
|
||||||
|
} else if color_byblock || color_layer0 {
|
||||||
|
wire.color = view::render::adapt_to_bg(style.0, bg_color);
|
||||||
|
wire.aci = style.4;
|
||||||
|
}
|
||||||
|
wires.push(wire);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if !wires.is_empty() {
|
||||||
|
let aabb = entity_aabb(e);
|
||||||
|
for wire in &mut wires {
|
||||||
|
// As below: only stroke / fill wires take the whole-dimension
|
||||||
|
// box. An SDF-text wire keeps its own tight glyph box so the
|
||||||
|
// text pick area hugs the text.
|
||||||
|
if !wire.points.is_empty() || !wire.fill_tris.is_empty() {
|
||||||
|
set_wire_aabb(wire, aabb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return wires;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let EntityType::Dimension(dim) = e {
|
if let EntityType::Dimension(dim) = e {
|
||||||
let aabb = entity_aabb(e);
|
let aabb = entity_aabb(e);
|
||||||
use crate::entities::dimension::DimensionTess;
|
use crate::entities::dimension::DimensionTess;
|
||||||
|
|||||||
Reference in new issue
Block a user