forked from 1b-agent/opencadstudio-upstream
feat(styles): overhaul style managers
Unify editor layouts, live previews, comparisons, read-only behavior, and localized controls across all named style managers. Refs #629
This commit is contained in:
1 parent
d06a1022f2
commit
25515fa33c
27 files changed
+5506
-1662
No files matched your search
Vendored
+43
-76
@@ -28,7 +28,7 @@ const MAX_NESTING_DEPTH: usize = 32;
|
||||
/// Skip wires whose world-AABB projects to fewer than this many pixels in
|
||||
/// the active view. Picks up tiny detail at zoom-out so the tessellator
|
||||
/// doesn't waste time on geometry that contributes a few sub-pixel marks
|
||||
/// to the final image. 2 px is the AutoCAD-default "small element" floor
|
||||
/// to the final image. Two pixels is a practical small-element floor:
|
||||
/// — visibly the same image, dramatically fewer wires.
|
||||
const MIN_PIXEL_SIZE: f32 = 2.0;
|
||||
|
||||
@@ -426,66 +426,16 @@ fn build_defn(
|
||||
nested_ins, doc, bg_color, depth_map,
|
||||
)));
|
||||
}
|
||||
// A dimension nested in a block bakes its geometry (extension /
|
||||
// dim lines, arrows, text) into a per-instance `*D` block, exactly
|
||||
// like a top-level dimension. The top-level path expands that block
|
||||
// in `tessellate_entity`; the block-expand path calls the plain
|
||||
// `tessellate::tessellate`, which does NOT, so nested dimensions
|
||||
// drew nothing. Expand the `*D` block's entities here as block-local
|
||||
// subs so they transform with the parent insert. (Empty block_name
|
||||
// — a non-baked dimension — falls through to the default arm.)
|
||||
EntityType::Dimension(dim)
|
||||
if !dim.base().block_name.trim().is_empty()
|
||||
&& !crate::entities::dimension::uses_custom_arrow_blocks(doc, dim) =>
|
||||
{
|
||||
let dblk = doc
|
||||
.block_records
|
||||
.iter()
|
||||
.find(|br| br.name.eq_ignore_ascii_case(&dim.base().block_name));
|
||||
if let Some(dblk) = dblk {
|
||||
// The `*D` block content is baked in the coordinate space it
|
||||
// occupied when the dimension was created — for a dimension
|
||||
// inside a block that is often the ORIGINAL WCS, not the
|
||||
// block-local space. The dimension's insertion_point (DXF
|
||||
// 12) is the offset that maps the baked content into the
|
||||
// dimension's own (block-local) space; it is zero for
|
||||
// dimensions baked in place, so this is a no-op there.
|
||||
let ins = dim.base().insertion_point;
|
||||
for &deh in &dblk.entity_handles {
|
||||
let Some(dsub) = doc.get_entity(deh) else {
|
||||
continue;
|
||||
};
|
||||
// Definition points are baked as POINTs on Defpoints —
|
||||
// grip markers, never drawn (matches the top-level path).
|
||||
if matches!(dsub, EntityType::Point(_)) {
|
||||
continue;
|
||||
}
|
||||
if dsub.common().invisible || layer_hidden(doc, &dsub.common().layer) {
|
||||
continue;
|
||||
}
|
||||
let mut placed = dsub.clone();
|
||||
placed.as_entity_mut().translate(ins);
|
||||
// An arrowhead is baked as a nested INSERT of an arrow
|
||||
// block — route it through the nested-ref machinery so
|
||||
// the arrow block expands (tessellate_sub_local doesn't
|
||||
// expand inserts).
|
||||
if let EntityType::Insert(arrow) = &placed {
|
||||
subs.push(LocalSub::Nested(build_nested_ref(
|
||||
arrow, doc, bg_color, depth_map,
|
||||
)));
|
||||
} else {
|
||||
for lw in tessellate_sub_local(
|
||||
doc,
|
||||
&placed,
|
||||
anno_scale,
|
||||
annotation_scale_handle,
|
||||
bg_color,
|
||||
depth_map,
|
||||
) {
|
||||
subs.push(LocalSub::Wire(lw));
|
||||
}
|
||||
}
|
||||
}
|
||||
EntityType::Dimension(_) => {
|
||||
for wire in tessellate_sub_local(
|
||||
doc,
|
||||
entity,
|
||||
anno_scale,
|
||||
annotation_scale_handle,
|
||||
bg_color,
|
||||
depth_map,
|
||||
) {
|
||||
subs.push(LocalSub::Wire(wire));
|
||||
}
|
||||
}
|
||||
// A table nested in a block bakes its geometry (gridlines, cell
|
||||
@@ -706,21 +656,38 @@ fn tessellate_sub_local(
|
||||
// Pass `local_offset` as the f64 world-offset so tessellate subtracts it
|
||||
// before casting to f32 — same precision-preservation trick used for
|
||||
// top-level entities, applied per-defn.
|
||||
let wires_out = tessellate::tessellate(
|
||||
doc,
|
||||
h,
|
||||
sub,
|
||||
false,
|
||||
sub_color,
|
||||
pat_len,
|
||||
pat,
|
||||
lw_px,
|
||||
anno_scale,
|
||||
annotation_scale_handle,
|
||||
None,
|
||||
bg_color,
|
||||
false,
|
||||
);
|
||||
let wires_out = if let EntityType::Dimension(dimension) = sub {
|
||||
use crate::entities::dimension::DimensionTess;
|
||||
dimension.tessellate(
|
||||
doc,
|
||||
h,
|
||||
false,
|
||||
sub_color,
|
||||
lw_px,
|
||||
anno_scale,
|
||||
&HashSet::default(),
|
||||
None,
|
||||
bg_color,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
} else {
|
||||
tessellate::tessellate(
|
||||
doc,
|
||||
h,
|
||||
sub,
|
||||
false,
|
||||
sub_color,
|
||||
pat_len,
|
||||
pat,
|
||||
lw_px,
|
||||
anno_scale,
|
||||
annotation_scale_handle,
|
||||
None,
|
||||
bg_color,
|
||||
false,
|
||||
)
|
||||
};
|
||||
if wires_out.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
|
||||
+4
-116
@@ -543,8 +543,8 @@ pub(crate) fn tessellate_entity(
|
||||
// An entity from an application we have no reader for (e.g. an Autodesk
|
||||
// Raster Design embedded raster image) arrives as `Unknown`. Its own data is
|
||||
// a private format we cannot decode — but it usually ships a proxy-graphics
|
||||
// blob, the vector preview its author cached for exactly this case. AutoCAD
|
||||
// draws that when the object enabler is missing; draw it too, so the entity
|
||||
// blob, the vector preview its author cached for exactly this case. Draw it
|
||||
// when the object enabler is missing, so the entity
|
||||
// occupies its real place instead of silently disappearing.
|
||||
let proxy_blob: Option<std::borrow::Cow<'_, [u8]>> = match e {
|
||||
EntityType::Unknown(_) => e
|
||||
@@ -665,7 +665,7 @@ pub(crate) fn tessellate_entity(
|
||||
//
|
||||
// The DWG reader decodes the two cut-line endpoints, the signed end ticks
|
||||
// and the identifier. Synthesize the end ticks + arrowheads + label glyphs
|
||||
// so the mark is visible on the layout, the way AutoCAD draws it. The raw
|
||||
// so the mark is visible on the layout. The raw
|
||||
// record is still preserved for lossless write-back.
|
||||
if let EntityType::SectionSymbol(s) = e {
|
||||
return section_symbol_wires(
|
||||
@@ -736,118 +736,6 @@ pub(crate) fn tessellate_entity(
|
||||
}];
|
||||
}
|
||||
|
||||
// ── Dimension baked-block fast path ─────────────────────────────────────
|
||||
//
|
||||
// A saved dimension may carry final geometry (extension lines, dim line,
|
||||
// arrows and text) in a per-instance block — usually
|
||||
// `*D<n>`, but custom names like `DIMBLOCK###-4NP` also occur. When the
|
||||
// block exists we render its contents through `tessellate_entity` so
|
||||
// sub-Text/MText get the standard baseline/greek/full LOD ladder, and
|
||||
// DIMTXT × DIMSCALE isn't re-applied on already-baked geometry.
|
||||
if let EntityType::Dimension(dim) = e {
|
||||
let block_name = &dim.base().block_name;
|
||||
if !block_name.trim().is_empty()
|
||||
&& !crate::entities::dimension::uses_custom_arrow_blocks(document, dim)
|
||||
{
|
||||
if let Some(br) = document
|
||||
.block_records
|
||||
.iter()
|
||||
.find(|br| br.name.eq_ignore_ascii_case(block_name))
|
||||
{
|
||||
if !br.entity_handles.is_empty() {
|
||||
let mut wires: Vec<WireModel> = Vec::with_capacity(br.entity_handles.len());
|
||||
// The Dimension's own layer style — layer-0 inheritance
|
||||
// target for baked sub-entities on layer "0" (#221).
|
||||
let dim_l0_color = view::render::adapt_to_bg(
|
||||
view::render::layer_render_style(document, &e.common().layer).color,
|
||||
bg_color,
|
||||
);
|
||||
let dim_l0_aci = document
|
||||
.layers
|
||||
.get(&e.common().layer)
|
||||
.map(|l| match &l.color {
|
||||
acadrust::types::Color::Index(i) => *i,
|
||||
_ => 0,
|
||||
})
|
||||
.unwrap_or(0);
|
||||
for &eh in &br.entity_handles {
|
||||
let Some(sub) = document.get_entity(eh) else {
|
||||
continue;
|
||||
};
|
||||
// A dimension's definition points are baked into the
|
||||
// block as POINTs on the Defpoints layer. They are grip
|
||||
// markers rather than PDMODE display geometry, so
|
||||
// geometry — so rendering them adds a stray tick at each
|
||||
// measured point that makes the extension lines look like
|
||||
// they run past the geometry. Skip them.
|
||||
if matches!(sub, EntityType::Point(_)) {
|
||||
continue;
|
||||
}
|
||||
// Sub-entities inside *D### / DIMBLOCK## blocks
|
||||
// typically use ByBlock color/linetype/lineweight —
|
||||
// they should inherit from the Dimension entity.
|
||||
let has_book_color =
|
||||
view::render::has_resolved_book_color(document, sub);
|
||||
let sub_color_is_byblock = !has_book_color
|
||||
&& sub.common().color == acadrust::types::Color::ByBlock;
|
||||
let sub_is_l0_bylayer =
|
||||
!has_book_color
|
||||
&& view::render::is_effective_layer_zero(&sub.common().layer)
|
||||
&& sub.common().color == acadrust::types::Color::ByLayer;
|
||||
let sub_wires = tessellate_entity(
|
||||
document,
|
||||
selected,
|
||||
active_viewport,
|
||||
bg_color,
|
||||
// Block contents are baked at the final WCS size —
|
||||
// don't let downstream paths re-apply anno_scale.
|
||||
1.0,
|
||||
None,
|
||||
sub,
|
||||
block_cache,
|
||||
view_aabb,
|
||||
world_per_pixel,
|
||||
paper_space,
|
||||
);
|
||||
for mut w in sub_wires {
|
||||
w.name = h.value().to_string();
|
||||
// Override ByBlock colour with the dim's resolved
|
||||
// colour so text matches `DIMCLRT`-style behaviour
|
||||
// (or layer colour) instead of the raw ByBlock
|
||||
// fallback that render_style_for produces. A layer-0
|
||||
// sub inherits the dim's layer colour instead.
|
||||
if sub_color_is_byblock {
|
||||
w.color = if sel {
|
||||
WireModel::SELECTED
|
||||
} else {
|
||||
entity_color
|
||||
};
|
||||
w.aci = aci;
|
||||
} else if sub_is_l0_bylayer && !sel {
|
||||
w.color = dim_l0_color;
|
||||
w.aci = dim_l0_aci;
|
||||
}
|
||||
wires.push(w);
|
||||
}
|
||||
}
|
||||
if !wires.is_empty() {
|
||||
let aabb = entity_aabb(e);
|
||||
for w in &mut wires {
|
||||
// Empty SDF-text cells keep their tight glyph-box
|
||||
// AABB; only stroke/fill wires take the whole-block
|
||||
// box as a broad-phase pick hint.
|
||||
if !w.points.is_empty() || !w.fill_tris.is_empty() {
|
||||
set_wire_aabb(w, aabb);
|
||||
}
|
||||
}
|
||||
return wires;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fall through to the synthesis path below when no block is attached.
|
||||
}
|
||||
|
||||
if let EntityType::Dimension(dim) = e {
|
||||
let aabb = entity_aabb(e);
|
||||
use crate::entities::dimension::DimensionTess;
|
||||
@@ -904,7 +792,7 @@ pub(crate) fn tessellate_entity(
|
||||
|
||||
// ── Table baked-block fast path ─────────────────────────────────────────
|
||||
//
|
||||
// AutoCAD bakes a Table's final rendered geometry (cell text, gridlines,
|
||||
// A table may store final rendered geometry (cell text, gridlines,
|
||||
// fill) into a per-instance block (usually `*T###`) referenced through
|
||||
// `table.block_record_handle`. The block's text uses the *displayed*
|
||||
// height; synthesising cells from `self.rows + TableStyle` instead would
|
||||
|
||||
@@ -1538,28 +1538,6 @@ pub(crate) fn arrow_from_block(
|
||||
.unwrap_or_else(|| arrow_from_block_name(None, dimasz))
|
||||
}
|
||||
|
||||
pub(crate) fn arrow_block_is_custom(
|
||||
doc: &CadDocument,
|
||||
handle: acadrust::types::Handle,
|
||||
) -> bool {
|
||||
if handle.is_null() {
|
||||
return false;
|
||||
}
|
||||
doc.block_records
|
||||
.iter()
|
||||
.find(|record| record.handle == handle)
|
||||
.is_some_and(|record| {
|
||||
!record.entity_handles.is_empty()
|
||||
&& !record.is_layout()
|
||||
&& !record.is_model_space()
|
||||
&& !record.is_paper_space()
|
||||
&& !record.flags.is_xref
|
||||
&& !record.flags.is_xref_overlay
|
||||
&& !record.flags.is_external
|
||||
&& builtin_arrow_from_block_name(&record.name, 1.0).is_none()
|
||||
})
|
||||
}
|
||||
|
||||
fn arrow_from_block_name(name: Option<&str>, dimasz: f32) -> ArrowKind {
|
||||
name.and_then(|name| builtin_arrow_from_block_name(name, dimasz))
|
||||
.unwrap_or(ArrowKind::Triangle {
|
||||
@@ -1742,6 +1720,10 @@ fn collect_custom_arrow_entity(
|
||||
stack.pop();
|
||||
return;
|
||||
}
|
||||
EntityType::Hatch(hatch) => {
|
||||
append_custom_hatch_fill(hatch, root_base, fill);
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -1766,6 +1748,69 @@ fn collect_custom_arrow_entity(
|
||||
}
|
||||
}
|
||||
|
||||
fn append_custom_hatch_fill(
|
||||
hatch: &acadrust::entities::Hatch,
|
||||
base: acadrust::types::Vector3,
|
||||
out: &mut Vec<[f32; 3]>,
|
||||
) {
|
||||
use lyon_tessellation::math::point;
|
||||
use lyon_tessellation::path::Path;
|
||||
use lyon_tessellation::{
|
||||
BuffersBuilder, FillOptions, FillRule, FillTessellator, FillVertex, VertexBuffers,
|
||||
};
|
||||
|
||||
let Some(model) = crate::scene::Scene::hatch_model_from_dxf(hatch, [1.0; 4]) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut builder = Path::builder();
|
||||
let mut ring: Vec<[f32; 2]> = Vec::new();
|
||||
let mut finish_ring = |ring: &mut Vec<[f32; 2]>| {
|
||||
if ring.len() >= 3 {
|
||||
builder.begin(point(ring[0][0], ring[0][1]));
|
||||
for p in &ring[1..] {
|
||||
builder.line_to(point(p[0], p[1]));
|
||||
}
|
||||
builder.end(true);
|
||||
}
|
||||
ring.clear();
|
||||
};
|
||||
for &[x, y] in model.boundary.iter() {
|
||||
if x.is_nan() || y.is_nan() {
|
||||
finish_ring(&mut ring);
|
||||
continue;
|
||||
}
|
||||
ring.push([
|
||||
(model.world_origin[0] + x as f64 - base.x) as f32,
|
||||
(model.world_origin[1] + y as f64 - base.y) as f32,
|
||||
]);
|
||||
}
|
||||
finish_ring(&mut ring);
|
||||
|
||||
let mut geometry: VertexBuffers<[f32; 2], u32> = VertexBuffers::new();
|
||||
let mut tessellator = FillTessellator::new();
|
||||
if tessellator
|
||||
.tessellate_path(
|
||||
&builder.build(),
|
||||
&FillOptions::default().with_fill_rule(FillRule::EvenOdd),
|
||||
&mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| {
|
||||
vertex.position().to_array()
|
||||
}),
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
let z = -base.z as f32;
|
||||
out.extend(
|
||||
geometry
|
||||
.indices
|
||||
.iter()
|
||||
.filter_map(|&index| geometry.vertices.get(index as usize))
|
||||
.map(|&[x, y]| [x, y, z]),
|
||||
);
|
||||
}
|
||||
|
||||
fn append_custom_wire_points(
|
||||
points: &[[f32; 3]],
|
||||
points_low: &[[f32; 3]],
|
||||
@@ -1928,7 +1973,7 @@ fn fallback_geometry(entity: &EntityType) -> Geometry {
|
||||
|
||||
/// Extract pre-computed edge-wire points from Solid3D / Region / Body entities.
|
||||
///
|
||||
/// AutoCAD stores explicit wire geometry (from SOLVIEW / 3DPLOT) alongside the
|
||||
/// Some drawings store explicit wire geometry alongside the
|
||||
/// ACIS data. We use this as a visible fallback when the SAT tessellator
|
||||
/// produces no mesh (e.g. binary SAB data or unsupported geometry).
|
||||
fn solid_wire_fallback(entity: &EntityType) -> Vec<[f64; 3]> {
|
||||
@@ -2117,7 +2162,7 @@ pub(crate) fn add_polyline(points: &mut Vec<[f32; 3]>, polyline: &[Vec3]) {
|
||||
|
||||
/// Returns the text position of a dimension in DXF world-space (f64, no offset applied).
|
||||
/// Used when building a synthetic Text entity so tessellate() can apply world_offset itself.
|
||||
/// When the saved `text_middle_point` is zero (i.e. AutoCAD never wrote one),
|
||||
/// When the saved `text_middle_point` is zero (no explicit point was written),
|
||||
/// computes a fallback from the dim geometry and applies DIMTAD/DIMGAP.
|
||||
pub(crate) fn normalized_or(v: Vec3, fallback: Vec3) -> Vec3 {
|
||||
if v.length_squared() <= 1e-12 {
|
||||
|
||||
+3
-3
@@ -1791,7 +1791,7 @@ impl Scene {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn hatch_model_from_dxf(
|
||||
pub(crate) fn hatch_model_from_dxf(
|
||||
dxf: &DxfHatch,
|
||||
color: [f32; 4],
|
||||
) -> Option<HatchModel> {
|
||||
@@ -1814,9 +1814,9 @@ impl Scene {
|
||||
|
||||
for path in &dxf.paths {
|
||||
// Skip TEXTBOX boundary paths (flag bit 3). These are text
|
||||
// bounding-boxes AutoCAD derives for island detection; they are
|
||||
// derived bounding boxes used for island detection; they are
|
||||
// never drawn or filled. Treating one as a fill boundary paints its
|
||||
// rectangle solid — a phantom bar AutoCAD never shows.
|
||||
// rectangle solid and creates a phantom bar.
|
||||
if path.flags.bits() & 8 != 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user