2026-03-22 14:56:27 +03:00
|
|
|
// Tessellation — convert acadrust EntityType to GPU-ready WireModel or MeshModel.
|
|
|
|
|
//
|
|
|
|
|
// Flow:
|
|
|
|
|
// EntityType
|
|
|
|
|
// ↓ acad_to_truck::convert()
|
|
|
|
|
// TruckEntity { object: TruckObject, snap_pts, tangent_geoms, key_vertices }
|
|
|
|
|
// ↓ truck_tess::tessellate_*()
|
|
|
|
|
// TruckTessResult::Lines → WireModel
|
|
|
|
|
// TruckTessResult::Point → WireModel (small cross)
|
|
|
|
|
// TruckTessResult::Mesh → MeshModel
|
|
|
|
|
// TruckObject::Text → one WireModel per glyph stroke (elevation from entity Z)
|
|
|
|
|
//
|
2026-05-23 23:16:57 +03:00
|
|
|
// Entities not handled by acad_to_truck (Viewport, Insert, Hatch, Ole2Frame)
|
|
|
|
|
// are tessellated by the FallbackTess fallback_geometry() path.
|
2026-03-22 14:56:27 +03:00
|
|
|
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
use crate::entities::leader::LeaderTess;
|
|
|
|
|
use acadrust::types::Color as AcadColor;
|
2026-03-22 14:56:27 +03:00
|
|
|
use acadrust::{CadDocument, EntityType, Handle};
|
|
|
|
|
use glam::Vec3;
|
|
|
|
|
|
|
|
|
|
use crate::scene::acad_to_truck::{convert, TruckObject};
|
|
|
|
|
use crate::scene::truck_tess::{
|
2026-05-16 00:10:20 +03:00
|
|
|
tessellate_edge, tessellate_vertex, tessellate_wire, TruckTessResult,
|
2026-03-22 14:56:27 +03:00
|
|
|
};
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
use crate::scene::wire_model::{SnapHint, WireModel};
|
2026-03-22 14:56:27 +03:00
|
|
|
|
|
|
|
|
// ── Public entry points ────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/// Tessellate one entity into a WireModel.
|
|
|
|
|
/// For Text/MText entities this produces one WireModel with all glyph strokes
|
|
|
|
|
/// encoded as NaN-separated segments (wire_gpu skips NaN pairs).
|
2026-05-16 00:10:20 +03:00
|
|
|
/// For Solid3D entities this returns an empty wire; mesh tessellation lives
|
|
|
|
|
/// in `solid3d_tess` and is uploaded via the mesh pipeline instead.
|
2026-03-22 14:56:27 +03:00
|
|
|
pub fn tessellate(
|
|
|
|
|
document: &CadDocument,
|
|
|
|
|
handle: Handle,
|
|
|
|
|
entity: &EntityType,
|
|
|
|
|
selected: bool,
|
|
|
|
|
entity_color: [f32; 4],
|
|
|
|
|
pattern_length: f32,
|
|
|
|
|
pattern: [f32; 8],
|
|
|
|
|
line_weight_px: f32,
|
2026-04-27 07:41:38 +03:00
|
|
|
world_offset: [f64; 3],
|
2026-05-06 17:50:46 +03:00
|
|
|
anno_scale: f32,
|
2026-05-21 23:42:22 +03:00
|
|
|
) -> Vec<WireModel> {
|
2026-03-22 14:56:27 +03:00
|
|
|
let color = if selected {
|
|
|
|
|
WireModel::SELECTED
|
|
|
|
|
} else {
|
|
|
|
|
entity_color
|
|
|
|
|
};
|
|
|
|
|
let name = handle.value().to_string();
|
|
|
|
|
|
2026-05-06 22:19:28 +03:00
|
|
|
// Determine effective annotation scale for this entity.
|
2026-05-19 00:26:08 +03:00
|
|
|
//
|
|
|
|
|
// AutoCAD's R2007+ "annotative" system marks objects via extension-
|
|
|
|
|
// dictionary records or "AcAnnoPO" / "AcAnnotativeData" xdata. Only
|
|
|
|
|
// entities so marked should be auto-scaled by the viewport's
|
|
|
|
|
// paper-scale; everything else is treated as manually pre-scaled
|
|
|
|
|
// (old DXF/DWG convention with $DIMSCALE and oversized text).
|
|
|
|
|
//
|
|
|
|
|
// Default: NOT annotative (anno_scale = 1.0). Opt-in via explicit
|
|
|
|
|
// xdata marker. Files that mark every entity annotative are rare; the
|
|
|
|
|
// pre-R2007 manual-scale convention is far more common in field data.
|
2026-05-06 22:19:28 +03:00
|
|
|
let anno_scale = {
|
|
|
|
|
let xdata = &entity.common().extended_data;
|
2026-05-19 00:26:08 +03:00
|
|
|
let is_annotative = xdata.get_record("AcAnnoPO").is_some()
|
|
|
|
|
|| xdata.get_record("AcAnnotativeData").is_some();
|
2026-05-12 10:36:15 +03:00
|
|
|
if is_annotative {
|
|
|
|
|
anno_scale
|
|
|
|
|
} else {
|
|
|
|
|
1.0
|
|
|
|
|
}
|
2026-05-06 22:19:28 +03:00
|
|
|
};
|
|
|
|
|
|
fix(multileader): render text properly — strip MText codes, honor full layout
Multileader text was emitted as a single literal stroke pass with no MText
code handling, no rotation, no attachment, no line breaks, no per-piece
color and bad f32 precision under world_offset. Two separate render paths
both had the same bug, and one was masking the fix in the other.
This rewrite, applied to both paths:
- Strips MText format codes (`{\fArial Black|b0|i0|c162|p34;...}`) so the
rendered text is the actual content, not the markup.
- Honors text_rotation (preferring text_direction so transforms survive),
text_attachment_point (Left/Center/Right horizontal anchor), and
text_left_attachment for vertical alignment via a shared
v_offset_for_attachment helper.
- Splits on \P/\n/\N and word-wraps to text_width.
- Applies line_spacing_factor with the same 5/3 × height convention as
MText.
- Resolves the text style via text_style_handle (fallback STANDARD).
- Applies ml.scale_factor everywhere (arrow size, dogleg length, text
height, text width). anno_scale is now only applied when
enable_annotation_scale is set.
- Splits the scene-level output into separate WireModels so leader,
text, frame and background fill each carry their own colour
(ctx.text_color, ml.text_frame, ctx.background_fill_color).
- Mirrors text_direction, base_direction and root.direction in
apply_transform so a mirrored multileader's text follows the geometry
instead of staying at its original rotation.
- Computes the text origin and frame corners in offset-relative f32
space — drawings sit at large absolute coordinates and casting to f32
before subtracting world_offset was wrecking the rotated sub-glyph
precision.
2026-05-12 23:27:53 +03:00
|
|
|
// MultiLeader is handled by scene/mod.rs since it emits multiple WireModels
|
|
|
|
|
// (leader, text, frame, fill) with distinct colors.
|
2026-05-06 17:50:46 +03:00
|
|
|
if let EntityType::Leader(leader) = entity {
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
return vec![leader.tessellate(
|
2026-05-19 13:37:37 +03:00
|
|
|
document,
|
2026-05-12 10:36:15 +03:00
|
|
|
handle,
|
|
|
|
|
selected,
|
|
|
|
|
entity_color,
|
|
|
|
|
line_weight_px,
|
|
|
|
|
world_offset,
|
|
|
|
|
anno_scale,
|
2026-05-21 23:42:22 +03:00
|
|
|
)];
|
2026-05-06 17:50:46 +03:00
|
|
|
}
|
|
|
|
|
|
2026-03-22 14:56:27 +03:00
|
|
|
// ── Try the truck path first ───────────────────────────────────────────
|
|
|
|
|
if let Some(te) = convert(entity, document) {
|
|
|
|
|
match te.object {
|
|
|
|
|
// ── Text / MText: pre-tessellated glyph strokes ───────────────
|
2026-05-21 23:42:22 +03:00
|
|
|
//
|
|
|
|
|
// Strokes are pre-grouped by world origin (one TextStroke per
|
|
|
|
|
// line / per run / per fragment), each carrying an optional
|
|
|
|
|
// colour override produced by MTEXT inline `\C` / `\c`. We bin
|
|
|
|
|
// groups by override colour and emit one WireModel per bin so a
|
|
|
|
|
// single MTEXT can hand back N colour-distinct wires when the
|
|
|
|
|
// value mixes inline colours.
|
2026-04-29 20:58:44 +03:00
|
|
|
TruckObject::Text(stroke_groups) => {
|
2026-04-27 18:04:35 +03:00
|
|
|
let [ox, oy, oz] = world_offset;
|
|
|
|
|
let elev = entity_z(entity) - oz as f32;
|
2026-03-22 14:56:27 +03:00
|
|
|
|
2026-05-21 23:42:22 +03:00
|
|
|
// anno_scale anchors at the first group's origin so multi-line
|
|
|
|
|
// MText lines spread apart correctly as they grow.
|
2026-05-12 10:36:15 +03:00
|
|
|
let ref_origin = stroke_groups
|
|
|
|
|
.first()
|
|
|
|
|
.map(|g| g.origin)
|
|
|
|
|
.unwrap_or([0.0, 0.0]);
|
2026-05-06 17:50:46 +03:00
|
|
|
let ref_lx = (ref_origin[0] - ox) as f32;
|
|
|
|
|
let ref_ly = (ref_origin[1] - oy) as f32;
|
2026-05-21 23:42:22 +03:00
|
|
|
|
|
|
|
|
// Selection forces a single uniform colour — never split.
|
|
|
|
|
let split_by_color = !selected;
|
|
|
|
|
|
|
|
|
|
// Bins: key = Some(rgb) or None (= inherit entity colour).
|
|
|
|
|
// Preserve insertion order so wire-emit ordering tracks the
|
|
|
|
|
// original glyph stream (useful for stable hit-test indexing).
|
|
|
|
|
let mut bins: Vec<(Option<[f32; 3]>, Vec<[f32; 3]>)> = Vec::new();
|
|
|
|
|
let mut bin_first: Vec<bool> = Vec::new();
|
|
|
|
|
let find_or_make =
|
|
|
|
|
|key: Option<[f32; 3]>, bins: &mut Vec<(Option<[f32; 3]>, Vec<[f32; 3]>)>, firsts: &mut Vec<bool>| -> usize {
|
|
|
|
|
if let Some(i) = bins.iter().position(|(k, _)| *k == key) {
|
|
|
|
|
i
|
|
|
|
|
} else {
|
|
|
|
|
bins.push((key, Vec::new()));
|
|
|
|
|
firsts.push(true);
|
|
|
|
|
bins.len() - 1
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-29 20:58:44 +03:00
|
|
|
for group in &stroke_groups {
|
|
|
|
|
let lx = (group.origin[0] - ox) as f32;
|
|
|
|
|
let ly = (group.origin[1] - oy) as f32;
|
2026-05-06 17:50:46 +03:00
|
|
|
let slx = (lx - ref_lx) * anno_scale + ref_lx;
|
|
|
|
|
let sly = (ly - ref_ly) * anno_scale + ref_ly;
|
2026-05-21 23:42:22 +03:00
|
|
|
let bin_key = if split_by_color { group.color } else { None };
|
|
|
|
|
let bi = find_or_make(bin_key, &mut bins, &mut bin_first);
|
|
|
|
|
let pts = &mut bins[bi].1;
|
2026-04-29 20:58:44 +03:00
|
|
|
for stroke in &group.strokes {
|
|
|
|
|
if stroke.len() < 2 {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-05-21 23:42:22 +03:00
|
|
|
if !bin_first[bi] && !pts.is_empty() {
|
|
|
|
|
pts.push([f32::NAN, f32::NAN, f32::NAN]);
|
2026-04-29 20:58:44 +03:00
|
|
|
}
|
2026-05-21 23:42:22 +03:00
|
|
|
bin_first[bi] = false;
|
2026-04-29 20:58:44 +03:00
|
|
|
for &[x, y] in stroke {
|
2026-05-21 23:42:22 +03:00
|
|
|
pts.push([x * anno_scale + slx, y * anno_scale + sly, elev]);
|
2026-04-29 20:58:44 +03:00
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 18:04:35 +03:00
|
|
|
let snap_pts = offset_snap_pts(te.snap_pts, world_offset);
|
2026-05-12 23:47:40 +03:00
|
|
|
let key_vertices: Vec<[f32; 3]> = te
|
|
|
|
|
.key_vertices
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|[x, y, z]| [(x - ox) as f32, (y - oy) as f32, (z - oz) as f32])
|
|
|
|
|
.collect();
|
2026-05-21 23:42:22 +03:00
|
|
|
|
|
|
|
|
// Empty input (no glyphs) → emit a single empty wire so the
|
|
|
|
|
// entity still has a hit-test target via snap_pts.
|
|
|
|
|
if bins.is_empty() {
|
|
|
|
|
return vec![WireModel {
|
|
|
|
|
name,
|
|
|
|
|
points: Vec::new(),
|
|
|
|
|
color,
|
|
|
|
|
selected,
|
|
|
|
|
pattern_length: 0.0,
|
|
|
|
|
pattern: [0.0; 8],
|
|
|
|
|
line_weight_px,
|
|
|
|
|
snap_pts,
|
|
|
|
|
tangent_geoms: te.tangent_geoms,
|
|
|
|
|
aci: 0,
|
|
|
|
|
key_vertices,
|
|
|
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
|
|
|
|
plinegen: true,
|
|
|
|
|
vp_scissor: None,
|
|
|
|
|
fill_tris: vec![],
|
|
|
|
|
}];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let bin_count = bins.len();
|
|
|
|
|
let mut out: Vec<WireModel> = Vec::with_capacity(bin_count);
|
|
|
|
|
for (idx, (override_rgb, pts)) in bins.into_iter().enumerate() {
|
|
|
|
|
let wire_color = match override_rgb {
|
|
|
|
|
Some([r, g, b]) => [r, g, b, color[3]],
|
|
|
|
|
None => color,
|
|
|
|
|
};
|
|
|
|
|
// Snap points and key vertices belong to the entity as a
|
|
|
|
|
// whole — attach them only to the first emitted wire so
|
|
|
|
|
// pickers / hover don't double-count.
|
|
|
|
|
let (snap, keys, tangents) = if idx == 0 {
|
|
|
|
|
(
|
|
|
|
|
snap_pts.clone(),
|
|
|
|
|
key_vertices.clone(),
|
|
|
|
|
te.tangent_geoms.clone(),
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
(Vec::new(), Vec::new(), Vec::new())
|
|
|
|
|
};
|
|
|
|
|
out.push(WireModel {
|
|
|
|
|
name: name.clone(),
|
|
|
|
|
points: pts,
|
|
|
|
|
color: wire_color,
|
|
|
|
|
selected,
|
|
|
|
|
pattern_length: 0.0,
|
|
|
|
|
pattern: [0.0; 8],
|
|
|
|
|
line_weight_px,
|
|
|
|
|
snap_pts: snap,
|
|
|
|
|
tangent_geoms: tangents,
|
|
|
|
|
aci: 0,
|
|
|
|
|
key_vertices: keys,
|
|
|
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
|
|
|
|
plinegen: true,
|
|
|
|
|
vp_scissor: None,
|
|
|
|
|
fill_tris: vec![],
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return out;
|
2026-03-22 14:56:27 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Standard topology objects ─────────────────────────────────
|
|
|
|
|
TruckObject::Point(v) => {
|
2026-04-27 07:41:38 +03:00
|
|
|
let result = tessellate_vertex(&v, world_offset);
|
2026-03-22 14:56:27 +03:00
|
|
|
match result {
|
|
|
|
|
TruckTessResult::Point([x, y, z]) => {
|
|
|
|
|
let s = 0.1_f32;
|
2026-04-27 18:04:35 +03:00
|
|
|
let snap_pts = offset_snap_pts(te.snap_pts, world_offset);
|
2026-05-12 23:47:40 +03:00
|
|
|
let [ox, oy, oz] = world_offset;
|
|
|
|
|
let key_vertices: Vec<[f32; 3]> = te
|
|
|
|
|
.key_vertices
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|[kx, ky, kz]| {
|
|
|
|
|
[(kx - ox) as f32, (ky - oy) as f32, (kz - oz) as f32]
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
2026-05-21 23:42:22 +03:00
|
|
|
return vec![WireModel {
|
2026-03-22 14:56:27 +03:00
|
|
|
name,
|
|
|
|
|
points: vec![
|
|
|
|
|
[x - s, y, z],
|
|
|
|
|
[x + s, y, z],
|
|
|
|
|
[x, y - s, z],
|
|
|
|
|
[x, y + s, z],
|
|
|
|
|
],
|
|
|
|
|
color,
|
|
|
|
|
selected,
|
|
|
|
|
pattern_length: 0.0,
|
|
|
|
|
pattern: [0.0; 8],
|
|
|
|
|
line_weight_px: 1.0,
|
2026-04-27 18:04:35 +03:00
|
|
|
snap_pts,
|
2026-03-22 14:56:27 +03:00
|
|
|
tangent_geoms: te.tangent_geoms,
|
2026-04-06 14:37:23 +03:00
|
|
|
aci: 0,
|
2026-05-12 23:47:40 +03:00
|
|
|
key_vertices,
|
2026-04-27 18:04:35 +03:00
|
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
2026-05-12 10:36:15 +03:00
|
|
|
plinegen: true,
|
|
|
|
|
vp_scissor: None,
|
|
|
|
|
fill_tris: vec![],
|
2026-05-21 23:42:22 +03:00
|
|
|
}];
|
2026-03-22 14:56:27 +03:00
|
|
|
}
|
|
|
|
|
_ => {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TruckObject::Curve(e) => {
|
2026-04-27 07:41:38 +03:00
|
|
|
if let TruckTessResult::Lines(points) = tessellate_edge(&e, world_offset) {
|
2026-04-27 18:04:35 +03:00
|
|
|
let [ox, oy, oz] = world_offset;
|
|
|
|
|
let snap_pts = offset_snap_pts(te.snap_pts, world_offset);
|
2026-05-12 10:36:15 +03:00
|
|
|
let key_vertices: Vec<[f32; 3]> = te
|
|
|
|
|
.key_vertices
|
|
|
|
|
.into_iter()
|
2026-05-12 23:47:40 +03:00
|
|
|
.map(|[x, y, z]| [(x - ox) as f32, (y - oy) as f32, (z - oz) as f32])
|
2026-04-27 18:04:35 +03:00
|
|
|
.collect();
|
2026-05-21 23:42:22 +03:00
|
|
|
return vec![WireModel {
|
2026-03-22 14:56:27 +03:00
|
|
|
name,
|
|
|
|
|
points,
|
|
|
|
|
color,
|
|
|
|
|
selected,
|
|
|
|
|
pattern_length,
|
|
|
|
|
pattern,
|
|
|
|
|
line_weight_px,
|
2026-04-27 18:04:35 +03:00
|
|
|
snap_pts,
|
2026-03-22 14:56:27 +03:00
|
|
|
tangent_geoms: te.tangent_geoms,
|
2026-04-06 14:37:23 +03:00
|
|
|
aci: 0,
|
2026-04-27 18:04:35 +03:00
|
|
|
key_vertices,
|
|
|
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
2026-05-12 10:36:15 +03:00
|
|
|
plinegen: true,
|
|
|
|
|
vp_scissor: None,
|
|
|
|
|
fill_tris: vec![],
|
2026-05-21 23:42:22 +03:00
|
|
|
}];
|
2026-03-22 14:56:27 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TruckObject::Contour(w) => {
|
2026-04-27 07:41:38 +03:00
|
|
|
if let TruckTessResult::Lines(points) = tessellate_wire(&w, world_offset) {
|
2026-04-27 18:04:35 +03:00
|
|
|
let [ox, oy, oz] = world_offset;
|
|
|
|
|
let snap_pts = offset_snap_pts(te.snap_pts, world_offset);
|
2026-05-12 10:36:15 +03:00
|
|
|
let key_vertices: Vec<[f32; 3]> = te
|
|
|
|
|
.key_vertices
|
|
|
|
|
.into_iter()
|
2026-05-12 23:47:40 +03:00
|
|
|
.map(|[x, y, z]| [(x - ox) as f32, (y - oy) as f32, (z - oz) as f32])
|
2026-04-27 18:04:35 +03:00
|
|
|
.collect();
|
2026-05-21 23:42:22 +03:00
|
|
|
return vec![WireModel {
|
2026-03-22 14:56:27 +03:00
|
|
|
name,
|
|
|
|
|
points,
|
|
|
|
|
color,
|
|
|
|
|
selected,
|
|
|
|
|
pattern_length,
|
|
|
|
|
pattern,
|
|
|
|
|
line_weight_px,
|
2026-04-27 18:04:35 +03:00
|
|
|
snap_pts,
|
2026-03-22 14:56:27 +03:00
|
|
|
tangent_geoms: te.tangent_geoms,
|
2026-04-06 14:37:23 +03:00
|
|
|
aci: 0,
|
2026-04-27 18:04:35 +03:00
|
|
|
key_vertices,
|
|
|
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
2026-05-12 10:36:15 +03:00
|
|
|
plinegen: true,
|
|
|
|
|
vp_scissor: None,
|
|
|
|
|
fill_tris: vec![],
|
2026-05-21 23:42:22 +03:00
|
|
|
}];
|
2026-03-22 14:56:27 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TruckObject::Lines(points) => {
|
2026-05-12 23:47:40 +03:00
|
|
|
// Points are world-space f64 from entity converters (polyline,
|
|
|
|
|
// leader, mesh, solid2d, etc.). Subtract world_offset in f64
|
|
|
|
|
// before casting to f32 so drawings at large UTM-style
|
|
|
|
|
// coordinates keep sub-unit precision in the wire model.
|
2026-04-27 18:04:35 +03:00
|
|
|
let [ox, oy, oz] = world_offset;
|
2026-05-12 10:36:15 +03:00
|
|
|
let local_pts: Vec<[f32; 3]> = points
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|[x, y, z]| {
|
|
|
|
|
if x.is_nan() {
|
2026-05-12 23:47:40 +03:00
|
|
|
[f32::NAN, f32::NAN, f32::NAN]
|
2026-05-12 10:36:15 +03:00
|
|
|
} else {
|
2026-05-12 23:47:40 +03:00
|
|
|
[(x - ox) as f32, (y - oy) as f32, (z - oz) as f32]
|
2026-05-12 10:36:15 +03:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
2026-04-27 18:04:35 +03:00
|
|
|
let snap_pts = offset_snap_pts(te.snap_pts, world_offset);
|
2026-05-12 10:36:15 +03:00
|
|
|
let key_vertices: Vec<[f32; 3]> = te
|
|
|
|
|
.key_vertices
|
|
|
|
|
.into_iter()
|
2026-05-12 23:47:40 +03:00
|
|
|
.map(|[x, y, z]| [(x - ox) as f32, (y - oy) as f32, (z - oz) as f32])
|
2026-04-27 18:04:35 +03:00
|
|
|
.collect();
|
2026-05-12 10:36:15 +03:00
|
|
|
let fill_tris: Vec<[f32; 3]> = te
|
|
|
|
|
.fill_tris
|
|
|
|
|
.into_iter()
|
2026-05-12 23:47:40 +03:00
|
|
|
.map(|[x, y, z]| [(x - ox) as f32, (y - oy) as f32, (z - oz) as f32])
|
2026-05-06 23:24:26 +03:00
|
|
|
.collect();
|
2026-05-21 23:42:22 +03:00
|
|
|
return vec![WireModel {
|
2026-03-22 14:56:27 +03:00
|
|
|
name,
|
2026-04-27 18:04:35 +03:00
|
|
|
points: local_pts,
|
2026-03-22 14:56:27 +03:00
|
|
|
color,
|
|
|
|
|
selected,
|
|
|
|
|
pattern_length: 0.0,
|
|
|
|
|
pattern: [0.0; 8],
|
|
|
|
|
line_weight_px,
|
2026-04-27 18:04:35 +03:00
|
|
|
snap_pts,
|
2026-03-22 14:56:27 +03:00
|
|
|
tangent_geoms: te.tangent_geoms,
|
2026-04-06 14:37:23 +03:00
|
|
|
aci: 0,
|
2026-04-27 18:04:35 +03:00
|
|
|
key_vertices,
|
|
|
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
2026-05-06 06:55:28 +03:00
|
|
|
plinegen: true,
|
2026-05-06 23:24:26 +03:00
|
|
|
vp_scissor: None,
|
|
|
|
|
fill_tris,
|
2026-05-21 23:42:22 +03:00
|
|
|
}];
|
2026-05-06 06:55:28 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TruckObject::SegmentedLines(points) => {
|
|
|
|
|
let [ox, oy, oz] = world_offset;
|
2026-05-12 10:36:15 +03:00
|
|
|
let local_pts: Vec<[f32; 3]> = points
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|[x, y, z]| {
|
|
|
|
|
if x.is_nan() {
|
2026-05-12 23:47:40 +03:00
|
|
|
[f32::NAN, f32::NAN, f32::NAN]
|
2026-05-12 10:36:15 +03:00
|
|
|
} else {
|
2026-05-12 23:47:40 +03:00
|
|
|
[(x - ox) as f32, (y - oy) as f32, (z - oz) as f32]
|
2026-05-12 10:36:15 +03:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.collect();
|
2026-05-06 06:55:28 +03:00
|
|
|
let snap_pts = offset_snap_pts(te.snap_pts, world_offset);
|
2026-05-12 10:36:15 +03:00
|
|
|
let key_vertices: Vec<[f32; 3]> = te
|
|
|
|
|
.key_vertices
|
|
|
|
|
.into_iter()
|
2026-05-12 23:47:40 +03:00
|
|
|
.map(|[x, y, z]| [(x - ox) as f32, (y - oy) as f32, (z - oz) as f32])
|
2026-05-06 06:55:28 +03:00
|
|
|
.collect();
|
2026-05-21 23:42:22 +03:00
|
|
|
return vec![WireModel {
|
2026-05-06 06:55:28 +03:00
|
|
|
name,
|
|
|
|
|
points: local_pts,
|
|
|
|
|
color,
|
|
|
|
|
selected,
|
|
|
|
|
pattern_length,
|
|
|
|
|
pattern,
|
|
|
|
|
line_weight_px,
|
|
|
|
|
snap_pts,
|
|
|
|
|
tangent_geoms: te.tangent_geoms,
|
|
|
|
|
aci: 0,
|
|
|
|
|
key_vertices,
|
|
|
|
|
plinegen: false,
|
2026-05-12 10:36:15 +03:00
|
|
|
vp_scissor: None,
|
2026-05-06 06:55:28 +03:00
|
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
2026-05-06 23:24:26 +03:00
|
|
|
fill_tris: vec![],
|
2026-05-21 23:42:22 +03:00
|
|
|
}];
|
2026-03-22 14:56:27 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TruckObject::Volume(_) => {
|
2026-05-16 00:10:20 +03:00
|
|
|
// Solid3D / Region / Body → mesh tessellation lives in
|
|
|
|
|
// `solid3d_tess`. As a wire fallback, render the pre-computed
|
|
|
|
|
// edge wires stored in the entity when present (e.g. from
|
|
|
|
|
// SOLVIEW output or when the SAT kernel cannot parse the
|
|
|
|
|
// ACIS data).
|
2026-04-27 07:41:38 +03:00
|
|
|
let wire_pts = solid_wire_fallback(entity, world_offset);
|
2026-05-06 10:00:37 +03:00
|
|
|
let mut wm = WireModel::solid(name, wire_pts, color, selected);
|
|
|
|
|
// Add insertion snap at point_of_reference.
|
|
|
|
|
let [ox, oy, oz] = world_offset;
|
2026-05-26 17:22:02 +03:00
|
|
|
if let Some(p) = crate::entities::solid3d::point_of_reference(entity) {
|
2026-05-12 10:36:15 +03:00
|
|
|
let sp = Vec3::new((p.x - ox) as f32, (p.y - oy) as f32, (p.z - oz) as f32);
|
2026-05-06 10:00:37 +03:00
|
|
|
wm.snap_pts.push((sp, SnapHint::Insertion));
|
|
|
|
|
}
|
2026-05-21 23:42:22 +03:00
|
|
|
return vec![wm];
|
2026-03-22 14:56:27 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 23:16:57 +03:00
|
|
|
// ── Fallback for Viewport / Insert / Hatch / Ole2Frame ────────────────
|
|
|
|
|
let (points, snap_pts, tangent_geoms, key_vertices) = fallback_geometry(entity, world_offset);
|
2026-05-21 23:42:22 +03:00
|
|
|
vec![WireModel {
|
2026-03-22 14:56:27 +03:00
|
|
|
name,
|
|
|
|
|
points,
|
|
|
|
|
color,
|
|
|
|
|
selected,
|
2026-04-06 14:37:23 +03:00
|
|
|
aci: 0,
|
2026-03-22 14:56:27 +03:00
|
|
|
pattern_length,
|
|
|
|
|
pattern,
|
|
|
|
|
line_weight_px,
|
|
|
|
|
snap_pts,
|
|
|
|
|
tangent_geoms,
|
|
|
|
|
key_vertices,
|
2026-04-26 12:21:51 +03:00
|
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
2026-05-12 10:36:15 +03:00
|
|
|
plinegen: true,
|
|
|
|
|
vp_scissor: None,
|
|
|
|
|
fill_tris: vec![],
|
2026-05-21 23:42:22 +03:00
|
|
|
}]
|
2026-03-22 14:56:27 +03:00
|
|
|
}
|
|
|
|
|
|
2026-05-19 13:37:37 +03:00
|
|
|
|
2026-05-19 09:44:59 +03:00
|
|
|
|
feat(scene): render dimensions from DimStyle values read from file
Dimension tessellation was hardcoding arrow shape (open-V line pair) and
text format ("{:.4}"), ignoring almost every DimStyle field the parser
populated. Drives the wire/text geometry from the resolved DimStyle:
- text: DIMDEC, DIMZIN, DIMLFAC, DIMRND, DIMDSEP, DIMPOST, DIMADEC,
DIMAZIN, DIMAUNIT (decimal/DMS/grad/rad), DIMTXSTY
- arrows: DIMBLK/DIMBLK1/DIMBLK2/DIMSAH resolution against block_records,
supporting ClosedFilled (default), Closed/Closed-Blank, Small, Open
(30/90), Dot (small/blank), Origin, Oblique/ArchTick, Box, Datum, None;
DIMTSZ overrides to oblique ticks
- colours: DIMCLRD / DIMCLRE / DIMCLRT split into three wires (ext, dim,
text); BYBLOCK/BYLAYER fall through to entity colour
- weights: DIMLWD / DIMLWE drive line_weight_px per wire
- suppress: DIMSE1/DIMSE2 (ext) and DIMSD1+DIMSD2 (dim) honoured
- text placement: DIMTAD vertical offset using DIMGAP + text height when
text_middle_point is unset; DIMTIH/DIMTOH force horizontal text
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 09:34:27 +03:00
|
|
|
#[derive(Clone, Copy)]
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
pub(crate) enum ArrowKind {
|
feat(scene): render dimensions from DimStyle values read from file
Dimension tessellation was hardcoding arrow shape (open-V line pair) and
text format ("{:.4}"), ignoring almost every DimStyle field the parser
populated. Drives the wire/text geometry from the resolved DimStyle:
- text: DIMDEC, DIMZIN, DIMLFAC, DIMRND, DIMDSEP, DIMPOST, DIMADEC,
DIMAZIN, DIMAUNIT (decimal/DMS/grad/rad), DIMTXSTY
- arrows: DIMBLK/DIMBLK1/DIMBLK2/DIMSAH resolution against block_records,
supporting ClosedFilled (default), Closed/Closed-Blank, Small, Open
(30/90), Dot (small/blank), Origin, Oblique/ArchTick, Box, Datum, None;
DIMTSZ overrides to oblique ticks
- colours: DIMCLRD / DIMCLRE / DIMCLRT split into three wires (ext, dim,
text); BYBLOCK/BYLAYER fall through to entity colour
- weights: DIMLWD / DIMLWE drive line_weight_px per wire
- suppress: DIMSE1/DIMSE2 (ext) and DIMSD1+DIMSD2 (dim) honoured
- text placement: DIMTAD vertical offset using DIMGAP + text height when
text_middle_point is unset; DIMTIH/DIMTOH force horizontal text
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 09:34:27 +03:00
|
|
|
None,
|
|
|
|
|
Triangle { size: f32, filled: bool, size_mul: f32 },
|
|
|
|
|
Tick { size: f32 },
|
|
|
|
|
Open { size: f32, half_angle: f32 },
|
|
|
|
|
Dot { size: f32, filled: bool },
|
|
|
|
|
Origin { size: f32 },
|
|
|
|
|
Box_ { size: f32, filled: bool },
|
|
|
|
|
Datum { size: f32, filled: bool },
|
|
|
|
|
}
|
|
|
|
|
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
pub(crate) fn arrow_from_block(
|
feat(scene): render dimensions from DimStyle values read from file
Dimension tessellation was hardcoding arrow shape (open-V line pair) and
text format ("{:.4}"), ignoring almost every DimStyle field the parser
populated. Drives the wire/text geometry from the resolved DimStyle:
- text: DIMDEC, DIMZIN, DIMLFAC, DIMRND, DIMDSEP, DIMPOST, DIMADEC,
DIMAZIN, DIMAUNIT (decimal/DMS/grad/rad), DIMTXSTY
- arrows: DIMBLK/DIMBLK1/DIMBLK2/DIMSAH resolution against block_records,
supporting ClosedFilled (default), Closed/Closed-Blank, Small, Open
(30/90), Dot (small/blank), Origin, Oblique/ArchTick, Box, Datum, None;
DIMTSZ overrides to oblique ticks
- colours: DIMCLRD / DIMCLRE / DIMCLRT split into three wires (ext, dim,
text); BYBLOCK/BYLAYER fall through to entity colour
- weights: DIMLWD / DIMLWE drive line_weight_px per wire
- suppress: DIMSE1/DIMSE2 (ext) and DIMSD1+DIMSD2 (dim) honoured
- text placement: DIMTAD vertical offset using DIMGAP + text height when
text_middle_point is unset; DIMTIH/DIMTOH force horizontal text
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 09:34:27 +03:00
|
|
|
doc: &CadDocument,
|
|
|
|
|
handle: acadrust::types::Handle,
|
|
|
|
|
dimasz: f32,
|
|
|
|
|
) -> ArrowKind {
|
|
|
|
|
let name = if handle.is_null() {
|
|
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
doc.block_records
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|b| b.handle == handle)
|
|
|
|
|
.map(|b| b.name.as_str())
|
|
|
|
|
};
|
|
|
|
|
arrow_from_block_name(name, dimasz)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn arrow_from_block_name(name: Option<&str>, dimasz: f32) -> ArrowKind {
|
|
|
|
|
// AutoCAD's standard arrow blocks are prefixed with "_" (e.g. "_OPEN").
|
|
|
|
|
// Strip the prefix, upper-case, and switch on canonical names. Unknown
|
|
|
|
|
// / missing names default to ClosedFilled.
|
|
|
|
|
let n = name
|
|
|
|
|
.map(|s| s.trim().trim_start_matches('_').to_ascii_uppercase())
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
match n.as_str() {
|
|
|
|
|
"" | "CLOSEDFILLED" => ArrowKind::Triangle {
|
|
|
|
|
size: dimasz,
|
|
|
|
|
filled: true,
|
|
|
|
|
size_mul: 1.0,
|
|
|
|
|
},
|
|
|
|
|
"CLOSED" | "CLOSEDBLANK" => ArrowKind::Triangle {
|
|
|
|
|
size: dimasz,
|
|
|
|
|
filled: false,
|
|
|
|
|
size_mul: 1.0,
|
|
|
|
|
},
|
|
|
|
|
"SMALL" => ArrowKind::Triangle {
|
|
|
|
|
size: dimasz,
|
|
|
|
|
filled: true,
|
|
|
|
|
size_mul: 0.5,
|
|
|
|
|
},
|
|
|
|
|
"OPEN" => ArrowKind::Open {
|
|
|
|
|
size: dimasz,
|
|
|
|
|
half_angle: 9.5_f32.to_radians(),
|
|
|
|
|
},
|
|
|
|
|
"OPEN30" => ArrowKind::Open {
|
|
|
|
|
size: dimasz,
|
|
|
|
|
half_angle: 15.0_f32.to_radians(),
|
|
|
|
|
},
|
|
|
|
|
"OPEN90" => ArrowKind::Open {
|
|
|
|
|
size: dimasz,
|
|
|
|
|
half_angle: 45.0_f32.to_radians(),
|
|
|
|
|
},
|
|
|
|
|
"DOT" => ArrowKind::Dot {
|
|
|
|
|
size: dimasz,
|
|
|
|
|
filled: true,
|
|
|
|
|
},
|
|
|
|
|
"DOTSMALL" => ArrowKind::Dot {
|
|
|
|
|
size: dimasz * 0.5,
|
|
|
|
|
filled: true,
|
|
|
|
|
},
|
|
|
|
|
"DOTBLANK" => ArrowKind::Dot {
|
|
|
|
|
size: dimasz,
|
|
|
|
|
filled: false,
|
|
|
|
|
},
|
|
|
|
|
"DOTSMALLBLANK" => ArrowKind::Dot {
|
|
|
|
|
size: dimasz * 0.5,
|
|
|
|
|
filled: false,
|
|
|
|
|
},
|
|
|
|
|
"ORIGIN" | "ORIGIN2" | "ORIGININDICATOR" | "ORIGININDICATOR2" => {
|
|
|
|
|
ArrowKind::Origin { size: dimasz }
|
|
|
|
|
}
|
|
|
|
|
"OBLIQUE" | "ARCHTICK" => ArrowKind::Tick { size: dimasz },
|
|
|
|
|
"BOXFILLED" => ArrowKind::Box_ {
|
|
|
|
|
size: dimasz,
|
|
|
|
|
filled: true,
|
|
|
|
|
},
|
|
|
|
|
"BOXBLANK" | "BOX" => ArrowKind::Box_ {
|
|
|
|
|
size: dimasz,
|
|
|
|
|
filled: false,
|
|
|
|
|
},
|
|
|
|
|
"DATUMFILLED" | "DATUMTRIANGLEFILLED" => ArrowKind::Datum {
|
|
|
|
|
size: dimasz,
|
|
|
|
|
filled: true,
|
|
|
|
|
},
|
|
|
|
|
"DATUMBLANK" | "DATUMTRIANGLE" => ArrowKind::Datum {
|
|
|
|
|
size: dimasz,
|
|
|
|
|
filled: false,
|
|
|
|
|
},
|
|
|
|
|
"NONE" => ArrowKind::None,
|
|
|
|
|
// INTEGRAL and other complex glyphs aren't reproduced here; fall through.
|
|
|
|
|
_ => ArrowKind::Triangle {
|
|
|
|
|
size: dimasz,
|
|
|
|
|
filled: true,
|
|
|
|
|
size_mul: 1.0,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
pub(crate) struct DimGeom {
|
|
|
|
|
pub(crate) ext_lines: Vec<[f32; 3]>,
|
|
|
|
|
pub(crate) dim_lines: Vec<[f32; 3]>,
|
|
|
|
|
pub(crate) arrow_fill: Vec<[f32; 3]>,
|
feat(scene): render dimensions from DimStyle values read from file
Dimension tessellation was hardcoding arrow shape (open-V line pair) and
text format ("{:.4}"), ignoring almost every DimStyle field the parser
populated. Drives the wire/text geometry from the resolved DimStyle:
- text: DIMDEC, DIMZIN, DIMLFAC, DIMRND, DIMDSEP, DIMPOST, DIMADEC,
DIMAZIN, DIMAUNIT (decimal/DMS/grad/rad), DIMTXSTY
- arrows: DIMBLK/DIMBLK1/DIMBLK2/DIMSAH resolution against block_records,
supporting ClosedFilled (default), Closed/Closed-Blank, Small, Open
(30/90), Dot (small/blank), Origin, Oblique/ArchTick, Box, Datum, None;
DIMTSZ overrides to oblique ticks
- colours: DIMCLRD / DIMCLRE / DIMCLRT split into three wires (ext, dim,
text); BYBLOCK/BYLAYER fall through to entity colour
- weights: DIMLWD / DIMLWE drive line_weight_px per wire
- suppress: DIMSE1/DIMSE2 (ext) and DIMSD1+DIMSD2 (dim) honoured
- text placement: DIMTAD vertical offset using DIMGAP + text height when
text_middle_point is unset; DIMTIH/DIMTOH force horizontal text
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 09:34:27 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl DimGeom {
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
pub(crate) fn new() -> Self {
|
feat(scene): render dimensions from DimStyle values read from file
Dimension tessellation was hardcoding arrow shape (open-V line pair) and
text format ("{:.4}"), ignoring almost every DimStyle field the parser
populated. Drives the wire/text geometry from the resolved DimStyle:
- text: DIMDEC, DIMZIN, DIMLFAC, DIMRND, DIMDSEP, DIMPOST, DIMADEC,
DIMAZIN, DIMAUNIT (decimal/DMS/grad/rad), DIMTXSTY
- arrows: DIMBLK/DIMBLK1/DIMBLK2/DIMSAH resolution against block_records,
supporting ClosedFilled (default), Closed/Closed-Blank, Small, Open
(30/90), Dot (small/blank), Origin, Oblique/ArchTick, Box, Datum, None;
DIMTSZ overrides to oblique ticks
- colours: DIMCLRD / DIMCLRE / DIMCLRT split into three wires (ext, dim,
text); BYBLOCK/BYLAYER fall through to entity colour
- weights: DIMLWD / DIMLWE drive line_weight_px per wire
- suppress: DIMSE1/DIMSE2 (ext) and DIMSD1+DIMSD2 (dim) honoured
- text placement: DIMTAD vertical offset using DIMGAP + text height when
text_middle_point is unset; DIMTIH/DIMTOH force horizontal text
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 09:34:27 +03:00
|
|
|
Self {
|
|
|
|
|
ext_lines: Vec::new(),
|
|
|
|
|
dim_lines: Vec::new(),
|
|
|
|
|
arrow_fill: Vec::new(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 14:56:27 +03:00
|
|
|
|
fix(multileader): render text properly — strip MText codes, honor full layout
Multileader text was emitted as a single literal stroke pass with no MText
code handling, no rotation, no attachment, no line breaks, no per-piece
color and bad f32 precision under world_offset. Two separate render paths
both had the same bug, and one was masking the fix in the other.
This rewrite, applied to both paths:
- Strips MText format codes (`{\fArial Black|b0|i0|c162|p34;...}`) so the
rendered text is the actual content, not the markup.
- Honors text_rotation (preferring text_direction so transforms survive),
text_attachment_point (Left/Center/Right horizontal anchor), and
text_left_attachment for vertical alignment via a shared
v_offset_for_attachment helper.
- Splits on \P/\n/\N and word-wraps to text_width.
- Applies line_spacing_factor with the same 5/3 × height convention as
MText.
- Resolves the text style via text_style_handle (fallback STANDARD).
- Applies ml.scale_factor everywhere (arrow size, dogleg length, text
height, text width). anno_scale is now only applied when
enable_annotation_scale is set.
- Splits the scene-level output into separate WireModels so leader,
text, frame and background fill each carry their own colour
(ctx.text_color, ml.text_frame, ctx.background_fill_color).
- Mirrors text_direction, base_direction and root.direction in
apply_transform so a mirrored multileader's text follows the geometry
instead of staying at its original rotation.
- Computes the text origin and frame corners in offset-relative f32
space — drawings sit at large absolute coordinates and casting to f32
before subtracting world_offset was wrecking the rotated sub-glyph
precision.
2026-05-12 23:27:53 +03:00
|
|
|
/// Convert an acadrust `Color` to RGBA, falling back to `inherited` for
|
|
|
|
|
/// `ByLayer` / `ByBlock` (assumes those are already resolved upstream).
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
pub(crate) fn color_or_inherit(c: &AcadColor, inherited: [f32; 4]) -> [f32; 4] {
|
fix(multileader): render text properly — strip MText codes, honor full layout
Multileader text was emitted as a single literal stroke pass with no MText
code handling, no rotation, no attachment, no line breaks, no per-piece
color and bad f32 precision under world_offset. Two separate render paths
both had the same bug, and one was masking the fix in the other.
This rewrite, applied to both paths:
- Strips MText format codes (`{\fArial Black|b0|i0|c162|p34;...}`) so the
rendered text is the actual content, not the markup.
- Honors text_rotation (preferring text_direction so transforms survive),
text_attachment_point (Left/Center/Right horizontal anchor), and
text_left_attachment for vertical alignment via a shared
v_offset_for_attachment helper.
- Splits on \P/\n/\N and word-wraps to text_width.
- Applies line_spacing_factor with the same 5/3 × height convention as
MText.
- Resolves the text style via text_style_handle (fallback STANDARD).
- Applies ml.scale_factor everywhere (arrow size, dogleg length, text
height, text width). anno_scale is now only applied when
enable_annotation_scale is set.
- Splits the scene-level output into separate WireModels so leader,
text, frame and background fill each carry their own colour
(ctx.text_color, ml.text_frame, ctx.background_fill_color).
- Mirrors text_direction, base_direction and root.direction in
apply_transform so a mirrored multileader's text follows the geometry
instead of staying at its original rotation.
- Computes the text origin and frame corners in offset-relative f32
space — drawings sit at large absolute coordinates and casting to f32
before subtracting world_offset was wrecking the rotated sub-glyph
precision.
2026-05-12 23:27:53 +03:00
|
|
|
match c.rgb() {
|
|
|
|
|
Some((r, g, b)) => [
|
|
|
|
|
r as f32 / 255.0,
|
|
|
|
|
g as f32 / 255.0,
|
|
|
|
|
b as f32 / 255.0,
|
|
|
|
|
inherited[3],
|
|
|
|
|
],
|
|
|
|
|
None => inherited,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat(entities): integrate remaining fields on block-based entities
MultiLeader and Table were ignoring most of the styling and content
fields that the parser populated. Bring both up to file-driven render:
MultiLeader
- line_color: leader stroke / arrow colour with ByBlock/ByLayer fallback
- line_type_handle: dashed leader via line_types lookup by handle
- line_weight (ml.line_weight): per-leader pixel width override
- arrowhead_handle: arrow shape resolved through the dim arrow emitter
(Closed Filled / Open / Dot / Origin / Box / Datum / Tick / None)
- extend_leader_to_text: continue the leader past the landing to the
text insertion point
- text_angle_type: Horizontal forces 0, Optimized clamps to a readable
half-plane, ParallelToLastLeaderLine keeps the stored direction
- text_direction_negative: adds π to the rotation
- text_alignment: drives the horizontal anchor when more specific than
the context's text_attachment_point
- text_attachment_direction + text_top_attachment + text_bottom_attachment:
Vertical mode uses the top / bottom attachment values chosen from the
first root's direction.y sign
- has_block_contents + block_content_handle + block_content_location +
block_rotation + block_scale + block_content_color: synthesise an
Insert and explode it through the standard tessellator so block-
content MultiLeaders render their referenced block
- style_handle, arrowhead_handle, line_type_handle, text_style_handle,
block_content_handle, property_override_flags, text_align_in_ipe,
block_scale, block_rotation, extend_leader_to_text,
text_direction_negative, text_top_attachment, text_bottom_attachment
surfaced in the Properties panel
Table
- Per-cell borders: drop the always-grid pass, walk the cells and
emit each border honouring its `invisible` flag with dedup so shared
edges aren't doubled
- table_style_handle, block_record_handle, data_version, value_flags,
override_flag / override_border_* flags, break_options /
break_flow_direction / break_spacing, normal surfaced in the
Properties panel
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 21:17:05 +03:00
|
|
|
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
// ── Entity Z helper ───────────────────────────────────────────────────────
|
2026-03-22 14:56:27 +03:00
|
|
|
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
/// Extract the Z elevation from a text/mtext entity.
|
|
|
|
|
pub(crate) fn entity_z(entity: &EntityType) -> f32 {
|
|
|
|
|
match entity {
|
|
|
|
|
EntityType::Text(t) => t.insertion_point.z as f32,
|
|
|
|
|
EntityType::MText(t) => t.insertion_point.z as f32,
|
|
|
|
|
_ => 0.0,
|
|
|
|
|
}
|
|
|
|
|
}
|
fix(multileader): render text properly — strip MText codes, honor full layout
Multileader text was emitted as a single literal stroke pass with no MText
code handling, no rotation, no attachment, no line breaks, no per-piece
color and bad f32 precision under world_offset. Two separate render paths
both had the same bug, and one was masking the fix in the other.
This rewrite, applied to both paths:
- Strips MText format codes (`{\fArial Black|b0|i0|c162|p34;...}`) so the
rendered text is the actual content, not the markup.
- Honors text_rotation (preferring text_direction so transforms survive),
text_attachment_point (Left/Center/Right horizontal anchor), and
text_left_attachment for vertical alignment via a shared
v_offset_for_attachment helper.
- Splits on \P/\n/\N and word-wraps to text_width.
- Applies line_spacing_factor with the same 5/3 × height convention as
MText.
- Resolves the text style via text_style_handle (fallback STANDARD).
- Applies ml.scale_factor everywhere (arrow size, dogleg length, text
height, text width). anno_scale is now only applied when
enable_annotation_scale is set.
- Splits the scene-level output into separate WireModels so leader,
text, frame and background fill each carry their own colour
(ctx.text_color, ml.text_frame, ctx.background_fill_color).
- Mirrors text_direction, base_direction and root.direction in
apply_transform so a mirrored multileader's text follows the geometry
instead of staying at its original rotation.
- Computes the text origin and frame corners in offset-relative f32
space — drawings sit at large absolute coordinates and casting to f32
before subtracting world_offset was wrecking the rotated sub-glyph
precision.
2026-05-12 23:27:53 +03:00
|
|
|
|
2026-05-23 23:16:57 +03:00
|
|
|
// ── Fallback geometry (Viewport, Insert, Hatch outline, Ole2Frame) ───────
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
//
|
|
|
|
|
// Per-entity blocks have moved to their respective `entities/*.rs` files
|
2026-05-23 23:16:57 +03:00
|
|
|
// (Viewport, Insert, Hatch, Ole2Frame) via the `FallbackTess` trait. This
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
// function stays as the dispatcher used by the main `tessellate()` path.
|
2026-03-22 14:56:27 +03:00
|
|
|
|
2026-05-23 23:16:57 +03:00
|
|
|
use crate::entities::traits::FallbackTess;
|
|
|
|
|
use crate::scene::tess_util::FallbackGeometry as Geometry;
|
2026-03-22 14:56:27 +03:00
|
|
|
|
2026-05-23 23:16:57 +03:00
|
|
|
fn fallback_geometry(entity: &EntityType, world_offset: [f64; 3]) -> Geometry {
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
match entity {
|
2026-05-23 23:16:57 +03:00
|
|
|
EntityType::Viewport(vp) => vp.fallback_geometry(world_offset),
|
|
|
|
|
EntityType::Insert(ins) => ins.fallback_geometry(world_offset),
|
|
|
|
|
EntityType::Hatch(h) => h.fallback_geometry(world_offset),
|
|
|
|
|
EntityType::Ole2Frame(ole) => ole.fallback_geometry(world_offset),
|
2026-06-12 00:04:55 +03:00
|
|
|
// Modeler solids render as meshes (solid3d_tess). Their wire path
|
|
|
|
|
// contributes only the pre-computed edge wires (empty for binary SAB)
|
|
|
|
|
// plus an insertion snap — never the placeholder segment below, which
|
|
|
|
|
// would otherwise draw a stray 1-unit line at the origin next to the
|
|
|
|
|
// solid.
|
2026-06-14 20:11:44 +03:00
|
|
|
EntityType::Solid3D(_)
|
|
|
|
|
| EntityType::Region(_)
|
|
|
|
|
| EntityType::Body(_)
|
|
|
|
|
| EntityType::Surface(_) => {
|
2026-06-12 00:04:55 +03:00
|
|
|
let pts = solid_wire_fallback(entity, world_offset);
|
|
|
|
|
let mut snap = vec![];
|
|
|
|
|
if let Some(p) = crate::entities::solid3d::point_of_reference(entity) {
|
|
|
|
|
let [ox, oy, oz] = world_offset;
|
|
|
|
|
snap.push((
|
|
|
|
|
Vec3::new((p.x - ox) as f32, (p.y - oy) as f32, (p.z - oz) as f32),
|
|
|
|
|
SnapHint::Insertion,
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
(pts, snap, vec![], vec![])
|
|
|
|
|
}
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
_ => {
|
|
|
|
|
let s = 0.5_f32;
|
|
|
|
|
(vec![[-s, 0.0, 0.0], [s, 0.0, 0.0]], vec![], vec![], vec![])
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
/// Extract pre-computed edge-wire points from Solid3D / Region / Body entities.
|
|
|
|
|
///
|
|
|
|
|
/// AutoCAD stores explicit wire geometry (from SOLVIEW / 3DPLOT) 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, world_offset: [f64; 3]) -> Vec<[f32; 3]> {
|
|
|
|
|
let [ox, oy, oz] = world_offset;
|
2026-05-26 17:22:02 +03:00
|
|
|
let Some(wires) = crate::entities::solid3d::fallback_wires(entity) else {
|
|
|
|
|
return vec![];
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
};
|
2026-04-08 22:16:58 +03:00
|
|
|
if wires.is_empty() {
|
|
|
|
|
return vec![];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut pts: Vec<[f32; 3]> = Vec::new();
|
|
|
|
|
for wire in wires {
|
|
|
|
|
if wire.points.len() < 2 {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-04-27 07:41:38 +03:00
|
|
|
for v in &wire.points {
|
|
|
|
|
pts.push([(v.x - ox) as f32, (v.y - oy) as f32, (v.z - oz) as f32]);
|
2026-04-08 22:16:58 +03:00
|
|
|
}
|
|
|
|
|
// NaN sentinel separates distinct wire segments.
|
|
|
|
|
pts.push([f32::NAN, f32::NAN, f32::NAN]);
|
|
|
|
|
}
|
|
|
|
|
pts
|
|
|
|
|
}
|
|
|
|
|
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
pub(crate) fn push_tri(out: &mut Vec<[f32; 3]>, a: Vec3, b: Vec3, c: Vec3) {
|
feat(scene): render dimensions from DimStyle values read from file
Dimension tessellation was hardcoding arrow shape (open-V line pair) and
text format ("{:.4}"), ignoring almost every DimStyle field the parser
populated. Drives the wire/text geometry from the resolved DimStyle:
- text: DIMDEC, DIMZIN, DIMLFAC, DIMRND, DIMDSEP, DIMPOST, DIMADEC,
DIMAZIN, DIMAUNIT (decimal/DMS/grad/rad), DIMTXSTY
- arrows: DIMBLK/DIMBLK1/DIMBLK2/DIMSAH resolution against block_records,
supporting ClosedFilled (default), Closed/Closed-Blank, Small, Open
(30/90), Dot (small/blank), Origin, Oblique/ArchTick, Box, Datum, None;
DIMTSZ overrides to oblique ticks
- colours: DIMCLRD / DIMCLRE / DIMCLRT split into three wires (ext, dim,
text); BYBLOCK/BYLAYER fall through to entity colour
- weights: DIMLWD / DIMLWE drive line_weight_px per wire
- suppress: DIMSE1/DIMSE2 (ext) and DIMSD1+DIMSD2 (dim) honoured
- text placement: DIMTAD vertical offset using DIMGAP + text height when
text_middle_point is unset; DIMTIH/DIMTOH force horizontal text
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 09:34:27 +03:00
|
|
|
out.push([a.x, a.y, a.z]);
|
|
|
|
|
out.push([b.x, b.y, b.z]);
|
|
|
|
|
out.push([c.x, c.y, c.z]);
|
|
|
|
|
}
|
|
|
|
|
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
pub(crate) fn append_arrow(g: &mut DimGeom, tip: Vec3, dir: Vec3, arrow: &ArrowKind) {
|
feat(scene): render dimensions from DimStyle values read from file
Dimension tessellation was hardcoding arrow shape (open-V line pair) and
text format ("{:.4}"), ignoring almost every DimStyle field the parser
populated. Drives the wire/text geometry from the resolved DimStyle:
- text: DIMDEC, DIMZIN, DIMLFAC, DIMRND, DIMDSEP, DIMPOST, DIMADEC,
DIMAZIN, DIMAUNIT (decimal/DMS/grad/rad), DIMTXSTY
- arrows: DIMBLK/DIMBLK1/DIMBLK2/DIMSAH resolution against block_records,
supporting ClosedFilled (default), Closed/Closed-Blank, Small, Open
(30/90), Dot (small/blank), Origin, Oblique/ArchTick, Box, Datum, None;
DIMTSZ overrides to oblique ticks
- colours: DIMCLRD / DIMCLRE / DIMCLRT split into three wires (ext, dim,
text); BYBLOCK/BYLAYER fall through to entity colour
- weights: DIMLWD / DIMLWE drive line_weight_px per wire
- suppress: DIMSE1/DIMSE2 (ext) and DIMSD1+DIMSD2 (dim) honoured
- text placement: DIMTAD vertical offset using DIMGAP + text height when
text_middle_point is unset; DIMTIH/DIMTOH force horizontal text
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 09:34:27 +03:00
|
|
|
let dir = normalized_or(dir, Vec3::X);
|
|
|
|
|
let perp = Vec3::new(-dir.y, dir.x, 0.0);
|
|
|
|
|
match *arrow {
|
|
|
|
|
ArrowKind::None => {}
|
|
|
|
|
ArrowKind::Triangle {
|
|
|
|
|
size,
|
|
|
|
|
filled,
|
|
|
|
|
size_mul,
|
|
|
|
|
} => {
|
|
|
|
|
let size = size * size_mul;
|
|
|
|
|
let base = tip + dir * size;
|
|
|
|
|
// ~1:6 length:half-width ratio (≈9.5° half-angle) matches
|
|
|
|
|
// AutoCAD's standard ClosedFilled block.
|
|
|
|
|
let half_w = size / 6.0;
|
|
|
|
|
let left = base + perp * half_w;
|
|
|
|
|
let right = base - perp * half_w;
|
|
|
|
|
add_segment(&mut g.dim_lines, tip, left);
|
|
|
|
|
add_segment(&mut g.dim_lines, left, right);
|
|
|
|
|
add_segment(&mut g.dim_lines, right, tip);
|
|
|
|
|
if filled {
|
|
|
|
|
push_tri(&mut g.arrow_fill, tip, left, right);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ArrowKind::Tick { size } => {
|
|
|
|
|
// 45° oblique tick crossing the dim line at the tip; `size` is
|
|
|
|
|
// the half-length (matches AutoCAD's DIMTSZ semantics).
|
|
|
|
|
let off = (dir + perp).normalize_or_zero() * size;
|
|
|
|
|
add_segment(&mut g.dim_lines, tip - off, tip + off);
|
|
|
|
|
}
|
|
|
|
|
ArrowKind::Open { size, half_angle } => {
|
|
|
|
|
let base = tip + dir * size;
|
|
|
|
|
let half_w = size * half_angle.tan();
|
|
|
|
|
let left = base + perp * half_w;
|
|
|
|
|
let right = base - perp * half_w;
|
|
|
|
|
add_segment(&mut g.dim_lines, tip, left);
|
|
|
|
|
add_segment(&mut g.dim_lines, tip, right);
|
|
|
|
|
}
|
|
|
|
|
ArrowKind::Dot { size, filled } => {
|
|
|
|
|
let r = size * 0.5;
|
|
|
|
|
const N: usize = 16;
|
|
|
|
|
let mut ring: Vec<Vec3> = Vec::with_capacity(N + 1);
|
|
|
|
|
for i in 0..=N {
|
|
|
|
|
let a = i as f32 * std::f32::consts::TAU / N as f32;
|
|
|
|
|
ring.push(tip + Vec3::new(a.cos() * r, a.sin() * r, 0.0));
|
|
|
|
|
}
|
|
|
|
|
add_polyline(&mut g.dim_lines, &ring);
|
|
|
|
|
if filled {
|
|
|
|
|
for i in 0..N {
|
|
|
|
|
push_tri(&mut g.arrow_fill, tip, ring[i], ring[i + 1]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ArrowKind::Origin { size } => {
|
|
|
|
|
// Small filled dot at the tip with a perpendicular tick crossing
|
|
|
|
|
// the dim line — matches "_ORIGIN" / "_ORIGIN2" blocks.
|
|
|
|
|
let r = size * 0.25;
|
|
|
|
|
const N: usize = 12;
|
|
|
|
|
let mut ring: Vec<Vec3> = Vec::with_capacity(N + 1);
|
|
|
|
|
for i in 0..=N {
|
|
|
|
|
let a = i as f32 * std::f32::consts::TAU / N as f32;
|
|
|
|
|
ring.push(tip + Vec3::new(a.cos() * r, a.sin() * r, 0.0));
|
|
|
|
|
}
|
|
|
|
|
add_polyline(&mut g.dim_lines, &ring);
|
|
|
|
|
for i in 0..N {
|
|
|
|
|
push_tri(&mut g.arrow_fill, tip, ring[i], ring[i + 1]);
|
|
|
|
|
}
|
|
|
|
|
let half = size * 0.5;
|
|
|
|
|
add_segment(&mut g.dim_lines, tip - perp * half, tip + perp * half);
|
|
|
|
|
}
|
|
|
|
|
ArrowKind::Box_ { size, filled } => {
|
|
|
|
|
let half = size * 0.5;
|
|
|
|
|
let p1 = tip - dir * half - perp * half;
|
|
|
|
|
let p2 = tip + dir * half - perp * half;
|
|
|
|
|
let p3 = tip + dir * half + perp * half;
|
|
|
|
|
let p4 = tip - dir * half + perp * half;
|
|
|
|
|
add_segment(&mut g.dim_lines, p1, p2);
|
|
|
|
|
add_segment(&mut g.dim_lines, p2, p3);
|
|
|
|
|
add_segment(&mut g.dim_lines, p3, p4);
|
|
|
|
|
add_segment(&mut g.dim_lines, p4, p1);
|
|
|
|
|
if filled {
|
|
|
|
|
push_tri(&mut g.arrow_fill, p1, p2, p3);
|
|
|
|
|
push_tri(&mut g.arrow_fill, p1, p3, p4);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ArrowKind::Datum { size, filled } => {
|
|
|
|
|
// Right-pointing triangle with the base perpendicular to the dim
|
|
|
|
|
// line at the tip and the apex along +dir.
|
|
|
|
|
let half = size * 0.5;
|
|
|
|
|
let base_a = tip + perp * half;
|
|
|
|
|
let base_b = tip - perp * half;
|
|
|
|
|
let apex = tip + dir * size;
|
|
|
|
|
add_segment(&mut g.dim_lines, base_a, apex);
|
|
|
|
|
add_segment(&mut g.dim_lines, apex, base_b);
|
|
|
|
|
add_segment(&mut g.dim_lines, base_b, base_a);
|
|
|
|
|
if filled {
|
|
|
|
|
push_tri(&mut g.arrow_fill, base_a, apex, base_b);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
}
|
|
|
|
|
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
pub(crate) fn add_segment(points: &mut Vec<[f32; 3]>, a: Vec3, b: Vec3) {
|
2026-03-22 14:56:27 +03:00
|
|
|
if !points.is_empty() {
|
|
|
|
|
points.push([f32::NAN, f32::NAN, f32::NAN]);
|
|
|
|
|
}
|
|
|
|
|
points.push([a.x, a.y, a.z]);
|
|
|
|
|
points.push([b.x, b.y, b.z]);
|
|
|
|
|
}
|
|
|
|
|
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
pub(crate) fn add_polyline(points: &mut Vec<[f32; 3]>, polyline: &[Vec3]) {
|
2026-03-22 14:56:27 +03:00
|
|
|
if polyline.len() < 2 {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if !points.is_empty() {
|
|
|
|
|
points.push([f32::NAN, f32::NAN, f32::NAN]);
|
|
|
|
|
}
|
|
|
|
|
points.extend(polyline.iter().map(|p| [p.x, p.y, p.z]));
|
|
|
|
|
}
|
|
|
|
|
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
pub(crate) fn offset_snap_pts(pts: Vec<(Vec3, SnapHint)>, off: [f64; 3]) -> Vec<(Vec3, SnapHint)> {
|
2026-04-27 18:04:35 +03:00
|
|
|
let [ox, oy, oz] = off;
|
|
|
|
|
pts.into_iter()
|
2026-05-12 10:36:15 +03:00
|
|
|
.map(|(p, h)| {
|
|
|
|
|
(
|
|
|
|
|
Vec3::new(p.x - ox as f32, p.y - oy as f32, p.z - oz as f32),
|
|
|
|
|
h,
|
|
|
|
|
)
|
|
|
|
|
})
|
2026-04-27 18:04:35 +03:00
|
|
|
.collect()
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 07:41:38 +03:00
|
|
|
/// 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.
|
feat(scene): render dimensions from DimStyle values read from file
Dimension tessellation was hardcoding arrow shape (open-V line pair) and
text format ("{:.4}"), ignoring almost every DimStyle field the parser
populated. Drives the wire/text geometry from the resolved DimStyle:
- text: DIMDEC, DIMZIN, DIMLFAC, DIMRND, DIMDSEP, DIMPOST, DIMADEC,
DIMAZIN, DIMAUNIT (decimal/DMS/grad/rad), DIMTXSTY
- arrows: DIMBLK/DIMBLK1/DIMBLK2/DIMSAH resolution against block_records,
supporting ClosedFilled (default), Closed/Closed-Blank, Small, Open
(30/90), Dot (small/blank), Origin, Oblique/ArchTick, Box, Datum, None;
DIMTSZ overrides to oblique ticks
- colours: DIMCLRD / DIMCLRE / DIMCLRT split into three wires (ext, dim,
text); BYBLOCK/BYLAYER fall through to entity colour
- weights: DIMLWD / DIMLWE drive line_weight_px per wire
- suppress: DIMSE1/DIMSE2 (ext) and DIMSD1+DIMSD2 (dim) honoured
- text placement: DIMTAD vertical offset using DIMGAP + text height when
text_middle_point is unset; DIMTIH/DIMTOH force horizontal text
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 09:34:27 +03:00
|
|
|
/// When the saved `text_middle_point` is zero (i.e. AutoCAD never wrote one),
|
|
|
|
|
/// computes a fallback from the dim geometry and applies DIMTAD/DIMGAP.
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
pub(crate) fn normalized_or(v: Vec3, fallback: Vec3) -> Vec3 {
|
2026-03-22 14:56:27 +03:00
|
|
|
if v.length_squared() <= 1e-12 {
|
|
|
|
|
fallback
|
|
|
|
|
} else {
|
|
|
|
|
v.normalize()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|