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:
Hakan Seven committed 2026-08-03 02:01:47 +03:00
1 parent d06a1022f2
commit 25515fa33c
27 files changed
+5506 -1662

No files matched your search

+43 -76
View File
@@ -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
View File
@@ -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
+69 -24
View File
@@ -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
View File
@@ -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;
}