2026-03-22 14:56:27 +03:00
|
|
|
|
use acadrust::entities::{
|
|
|
|
|
|
Dimension, DimensionAligned, DimensionAngular2Ln, DimensionAngular3Pt, DimensionBase,
|
|
|
|
|
|
DimensionDiameter, DimensionLinear, DimensionOrdinate, DimensionRadius,
|
|
|
|
|
|
};
|
|
|
|
|
|
use acadrust::Entity;
|
2026-06-24 03:32:11 +03:00
|
|
|
|
use glam::{DVec3, Vec3};
|
2026-03-22 14:56:27 +03:00
|
|
|
|
|
|
|
|
|
|
use crate::command::EntityTransform;
|
2026-05-12 10:36:15 +03:00
|
|
|
|
use crate::entities::common::{
|
refactor(grip): square-only marker set + oriented stretch handles
Phase 1 of bringing the grip vocabulary in line with the standard
CAD convention: drop the per-position-type shape encoding (Diamond
for translate, Circle for parameter) and use a plain square for
every endpoint / vertex / centre grip. The behavioural midpoint vs
vertex distinction stays — it lives on `GripDef::is_midpoint` and
drives `Translate` vs `Absolute` in the grip-edit dispatcher — only
the marker shape collapses.
- `GripShape` keeps `Square`, `Rectangle`, `Triangle`. Diamond /
Circle are gone.
- `GripDef` gains `dir: Option<[f32; 2]>` — a world-XY direction
vector consumed by `Rectangle` to orient the box along its
segment.
- `entities::common` exposes `square_grip`, `center_grip` (same
square marker, flagged as translate), `rectangle_grip(id, world,
dir)` for oriented mid-segment handles, and a kept-for-Phase-2
`triangle_grip`.
- `overlay::GripMarker` / `grips_to_screen[_paper]` thread `dir`
through to the canvas painter; the Rectangle path rotates the
box around the grip centre using the world-XY direction (flipping
the sin for screen-Y).
Every existing `diamond_grip(id, w)` call becomes `center_grip(id,
w)` — the visual change is square-replacing-diamond at line
midpoints / arc midpoints / circle centres / ellipse centres /
viewport / hatch loop centroids / dimension grips / image corners /
multileader / ray / underlay / ole2frame / solid3d.
`LwPolyline` grows a stretch handle on every segment (straight or
arc), drawn as the new oriented rectangle. The grip-edit path
translates both segment endpoints for straight segments and
adjusts the arc bulge from the new midpoint for arc segments.
2026-05-29 02:07:34 +03:00
|
|
|
|
center_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip,
|
2026-05-12 10:36:15 +03:00
|
|
|
|
};
|
Feat: dimension grip editing — all dimension types get stretch grips
Linear/Aligned: first point, second point, definition point, text position
Radius/Diameter: center (angle_vertex), radius point, text position
Angular2Ln/Angular3Pt: vertex, two extension points, arc def point, text
Ordinate: origin, feature location, leader endpoint, text position
Text position grip shared across all types; measurement recalculated after edit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 19:11:21 +03:00
|
|
|
|
use crate::entities::traits::{Grippable, PropertyEditable, Transformable};
|
2026-06-16 11:33:46 +03:00
|
|
|
|
use crate::scene::model::object::{GripApply, GripDef, PropSection};
|
2026-03-22 14:56:27 +03:00
|
|
|
|
|
2026-06-16 11:33:46 +03:00
|
|
|
|
fn base_props(base: &DimensionBase) -> Vec<crate::scene::model::object::Property> {
|
2026-03-22 14:56:27 +03:00
|
|
|
|
vec![
|
2026-06-16 11:33:46 +03:00
|
|
|
|
crate::scene::model::object::Property {
|
2026-03-22 14:56:27 +03:00
|
|
|
|
label: "Text".into(),
|
|
|
|
|
|
field: "text",
|
2026-06-16 11:33:46 +03:00
|
|
|
|
value: crate::scene::model::object::PropValue::EditText(base.text.clone()),
|
2026-03-22 14:56:27 +03:00
|
|
|
|
},
|
2026-06-16 11:33:46 +03:00
|
|
|
|
crate::scene::model::object::Property {
|
2026-03-22 14:56:27 +03:00
|
|
|
|
label: "User Text".into(),
|
|
|
|
|
|
field: "user_text",
|
2026-06-16 11:33:46 +03:00
|
|
|
|
value: crate::scene::model::object::PropValue::EditText(
|
2026-03-22 14:56:27 +03:00
|
|
|
|
base.user_text.clone().unwrap_or_default(),
|
|
|
|
|
|
),
|
|
|
|
|
|
},
|
2026-06-16 11:33:46 +03:00
|
|
|
|
crate::scene::model::object::Property {
|
2026-03-22 14:56:27 +03:00
|
|
|
|
label: "Style".into(),
|
|
|
|
|
|
field: "style_name",
|
2026-06-16 11:33:46 +03:00
|
|
|
|
value: crate::scene::model::object::PropValue::EditText(base.style_name.clone()),
|
2026-03-22 14:56:27 +03:00
|
|
|
|
},
|
|
|
|
|
|
edit("Text X", "text_x", base.text_middle_point.x),
|
|
|
|
|
|
edit("Text Y", "text_y", base.text_middle_point.y),
|
|
|
|
|
|
edit("Text Z", "text_z", base.text_middle_point.z),
|
2026-05-12 10:36:15 +03:00
|
|
|
|
edit(
|
2026-06-27 17:47:50 +03:00
|
|
|
|
"Text Rotation (deg)",
|
|
|
|
|
|
"text_rotation",
|
|
|
|
|
|
base.text_rotation.to_degrees(),
|
|
|
|
|
|
),
|
|
|
|
|
|
edit(
|
|
|
|
|
|
"Horizontal Dir (deg)",
|
2026-05-12 10:36:15 +03:00
|
|
|
|
"horizontal_direction",
|
2026-06-27 17:47:50 +03:00
|
|
|
|
base.horizontal_direction.to_degrees(),
|
2026-05-12 10:36:15 +03:00
|
|
|
|
),
|
|
|
|
|
|
edit(
|
|
|
|
|
|
"Line Spacing",
|
|
|
|
|
|
"line_spacing_factor",
|
|
|
|
|
|
base.line_spacing_factor,
|
|
|
|
|
|
),
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Measurement",
|
|
|
|
|
|
"measurement",
|
|
|
|
|
|
format!("{:.4}", base.actual_measurement),
|
|
|
|
|
|
),
|
2026-03-22 14:56:27 +03:00
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-02 22:46:19 +03:00
|
|
|
|
fn properties(dim: &Dimension) -> Vec<PropSection> {
|
2026-03-22 14:56:27 +03:00
|
|
|
|
let mut props = base_props(dim.base());
|
|
|
|
|
|
match dim {
|
|
|
|
|
|
Dimension::Aligned(d) => {
|
2026-05-12 10:36:15 +03:00
|
|
|
|
props.extend(linear_like_props(
|
|
|
|
|
|
d.first_point,
|
|
|
|
|
|
d.second_point,
|
|
|
|
|
|
d.definition_point,
|
|
|
|
|
|
));
|
|
|
|
|
|
props.push(edit(
|
2026-06-27 17:47:50 +03:00
|
|
|
|
"Ext Rotation (deg)",
|
2026-05-12 10:36:15 +03:00
|
|
|
|
"ext_line_rotation",
|
2026-06-27 17:47:50 +03:00
|
|
|
|
d.ext_line_rotation.to_degrees(),
|
2026-05-12 10:36:15 +03:00
|
|
|
|
));
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
Dimension::Linear(d) => {
|
2026-05-12 10:36:15 +03:00
|
|
|
|
props.extend(linear_like_props(
|
|
|
|
|
|
d.first_point,
|
|
|
|
|
|
d.second_point,
|
|
|
|
|
|
d.definition_point,
|
|
|
|
|
|
));
|
2026-06-27 17:47:50 +03:00
|
|
|
|
props.push(edit("Rotation (deg)", "rotation", d.rotation.to_degrees()));
|
2026-05-12 10:36:15 +03:00
|
|
|
|
props.push(edit(
|
2026-06-27 17:47:50 +03:00
|
|
|
|
"Ext Rotation (deg)",
|
2026-05-12 10:36:15 +03:00
|
|
|
|
"ext_line_rotation",
|
2026-06-27 17:47:50 +03:00
|
|
|
|
d.ext_line_rotation.to_degrees(),
|
2026-05-12 10:36:15 +03:00
|
|
|
|
));
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
Dimension::Radius(d) => {
|
|
|
|
|
|
props.extend(radius_like_props(d.angle_vertex, d.definition_point));
|
|
|
|
|
|
props.push(edit("Leader Length", "leader_length", d.leader_length));
|
|
|
|
|
|
}
|
|
|
|
|
|
Dimension::Diameter(d) => {
|
|
|
|
|
|
props.extend(radius_like_props(d.angle_vertex, d.definition_point));
|
|
|
|
|
|
props.push(edit("Leader Length", "leader_length", d.leader_length));
|
|
|
|
|
|
}
|
|
|
|
|
|
Dimension::Angular2Ln(d) => {
|
|
|
|
|
|
props.extend(angular_props(
|
|
|
|
|
|
d.angle_vertex,
|
|
|
|
|
|
d.first_point,
|
|
|
|
|
|
d.second_point,
|
|
|
|
|
|
d.definition_point,
|
|
|
|
|
|
));
|
|
|
|
|
|
props.push(edit("Arc X", "dimension_arc_x", d.dimension_arc.x));
|
|
|
|
|
|
props.push(edit("Arc Y", "dimension_arc_y", d.dimension_arc.y));
|
|
|
|
|
|
props.push(edit("Arc Z", "dimension_arc_z", d.dimension_arc.z));
|
|
|
|
|
|
}
|
|
|
|
|
|
Dimension::Angular3Pt(d) => {
|
|
|
|
|
|
props.extend(angular_props(
|
|
|
|
|
|
d.angle_vertex,
|
|
|
|
|
|
d.first_point,
|
|
|
|
|
|
d.second_point,
|
|
|
|
|
|
d.definition_point,
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
Dimension::Ordinate(d) => {
|
|
|
|
|
|
props.push(edit("Origin X", "definition_x", d.definition_point.x));
|
|
|
|
|
|
props.push(edit("Origin Y", "definition_y", d.definition_point.y));
|
|
|
|
|
|
props.push(edit("Origin Z", "definition_z", d.definition_point.z));
|
|
|
|
|
|
props.push(edit("Feature X", "feature_x", d.feature_location.x));
|
|
|
|
|
|
props.push(edit("Feature Y", "feature_y", d.feature_location.y));
|
|
|
|
|
|
props.push(edit("Feature Z", "feature_z", d.feature_location.z));
|
|
|
|
|
|
props.push(edit("Leader X", "leader_x", d.leader_endpoint.x));
|
|
|
|
|
|
props.push(edit("Leader Y", "leader_y", d.leader_endpoint.y));
|
|
|
|
|
|
props.push(edit("Leader Z", "leader_z", d.leader_endpoint.z));
|
|
|
|
|
|
props.push(ro(
|
|
|
|
|
|
"Ordinate Type",
|
|
|
|
|
|
"ordinate_type",
|
|
|
|
|
|
if d.is_ordinate_type_x { "X" } else { "Y" },
|
|
|
|
|
|
));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-02 22:46:19 +03:00
|
|
|
|
vec![PropSection {
|
2026-03-22 14:56:27 +03:00
|
|
|
|
title: "Geometry".into(),
|
|
|
|
|
|
props,
|
2026-07-02 22:46:19 +03:00
|
|
|
|
}]
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn linear_like_props(
|
|
|
|
|
|
first: acadrust::types::Vector3,
|
|
|
|
|
|
second: acadrust::types::Vector3,
|
|
|
|
|
|
definition: acadrust::types::Vector3,
|
2026-06-16 11:33:46 +03:00
|
|
|
|
) -> Vec<crate::scene::model::object::Property> {
|
2026-03-22 14:56:27 +03:00
|
|
|
|
vec![
|
|
|
|
|
|
edit("First X", "first_x", first.x),
|
|
|
|
|
|
edit("First Y", "first_y", first.y),
|
|
|
|
|
|
edit("First Z", "first_z", first.z),
|
|
|
|
|
|
edit("Second X", "second_x", second.x),
|
|
|
|
|
|
edit("Second Y", "second_y", second.y),
|
|
|
|
|
|
edit("Second Z", "second_z", second.z),
|
|
|
|
|
|
edit("Definition X", "definition_x", definition.x),
|
|
|
|
|
|
edit("Definition Y", "definition_y", definition.y),
|
|
|
|
|
|
edit("Definition Z", "definition_z", definition.z),
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn radius_like_props(
|
|
|
|
|
|
center: acadrust::types::Vector3,
|
|
|
|
|
|
point: acadrust::types::Vector3,
|
2026-06-16 11:33:46 +03:00
|
|
|
|
) -> Vec<crate::scene::model::object::Property> {
|
2026-03-22 14:56:27 +03:00
|
|
|
|
vec![
|
|
|
|
|
|
edit("Center X", "center_x", center.x),
|
|
|
|
|
|
edit("Center Y", "center_y", center.y),
|
|
|
|
|
|
edit("Center Z", "center_z", center.z),
|
|
|
|
|
|
edit("Point X", "point_x", point.x),
|
|
|
|
|
|
edit("Point Y", "point_y", point.y),
|
|
|
|
|
|
edit("Point Z", "point_z", point.z),
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn angular_props(
|
|
|
|
|
|
vertex: acadrust::types::Vector3,
|
|
|
|
|
|
first: acadrust::types::Vector3,
|
|
|
|
|
|
second: acadrust::types::Vector3,
|
|
|
|
|
|
definition: acadrust::types::Vector3,
|
2026-06-16 11:33:46 +03:00
|
|
|
|
) -> Vec<crate::scene::model::object::Property> {
|
2026-03-22 14:56:27 +03:00
|
|
|
|
vec![
|
|
|
|
|
|
edit("Vertex X", "vertex_x", vertex.x),
|
|
|
|
|
|
edit("Vertex Y", "vertex_y", vertex.y),
|
|
|
|
|
|
edit("Vertex Z", "vertex_z", vertex.z),
|
|
|
|
|
|
edit("First X", "first_x", first.x),
|
|
|
|
|
|
edit("First Y", "first_y", first.y),
|
|
|
|
|
|
edit("First Z", "first_z", first.z),
|
|
|
|
|
|
edit("Second X", "second_x", second.x),
|
|
|
|
|
|
edit("Second Y", "second_y", second.y),
|
|
|
|
|
|
edit("Second Z", "second_z", second.z),
|
|
|
|
|
|
edit("Definition X", "definition_x", definition.x),
|
|
|
|
|
|
edit("Definition Y", "definition_y", definition.y),
|
|
|
|
|
|
edit("Definition Z", "definition_z", definition.z),
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn apply_base_prop(base: &mut DimensionBase, field: &str, value: &str) -> bool {
|
|
|
|
|
|
match field {
|
|
|
|
|
|
"text" => {
|
|
|
|
|
|
base.text = value.to_string();
|
|
|
|
|
|
true
|
|
|
|
|
|
}
|
|
|
|
|
|
"user_text" => {
|
|
|
|
|
|
base.user_text = if value.trim().is_empty() {
|
|
|
|
|
|
None
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Some(value.to_string())
|
|
|
|
|
|
};
|
|
|
|
|
|
true
|
|
|
|
|
|
}
|
|
|
|
|
|
"style_name" => {
|
|
|
|
|
|
base.style_name = value.to_string();
|
|
|
|
|
|
true
|
|
|
|
|
|
}
|
2026-06-14 01:57:58 +03:00
|
|
|
|
// Editing the text position in the properties panel pins it to a
|
|
|
|
|
|
// user-defined location (stops following DIMTAD). See #94.
|
|
|
|
|
|
"text_x" => {
|
|
|
|
|
|
base.text_user_positioned = true;
|
|
|
|
|
|
assign_f64(value, &mut base.text_middle_point.x)
|
|
|
|
|
|
}
|
|
|
|
|
|
"text_y" => {
|
|
|
|
|
|
base.text_user_positioned = true;
|
|
|
|
|
|
assign_f64(value, &mut base.text_middle_point.y)
|
|
|
|
|
|
}
|
|
|
|
|
|
"text_z" => {
|
|
|
|
|
|
base.text_user_positioned = true;
|
|
|
|
|
|
assign_f64(value, &mut base.text_middle_point.z)
|
|
|
|
|
|
}
|
2026-06-27 17:47:50 +03:00
|
|
|
|
"text_rotation" => assign_deg(value, &mut base.text_rotation),
|
|
|
|
|
|
"horizontal_direction" => assign_deg(value, &mut base.horizontal_direction),
|
2026-03-22 14:56:27 +03:00
|
|
|
|
"line_spacing_factor" => assign_f64(value, &mut base.line_spacing_factor),
|
|
|
|
|
|
_ => false,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn assign_f64(value: &str, target: &mut f64) -> bool {
|
|
|
|
|
|
let Some(v) = parse_f64(value) else {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
};
|
|
|
|
|
|
*target = v;
|
|
|
|
|
|
true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-27 17:47:50 +03:00
|
|
|
|
/// Parse a value entered in DEGREES and store it as radians. Dimension angle
|
|
|
|
|
|
/// fields are kept in radians internally but shown/edited in degrees, matching
|
|
|
|
|
|
/// arc.rs / text.rs so the properties panel reads consistently.
|
|
|
|
|
|
fn assign_deg(value: &str, target: &mut f64) -> bool {
|
|
|
|
|
|
let Some(v) = parse_f64(value) else {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
};
|
|
|
|
|
|
*target = v.to_radians();
|
|
|
|
|
|
true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 14:56:27 +03:00
|
|
|
|
fn apply_geom_prop(dim: &mut Dimension, field: &str, value: &str) {
|
|
|
|
|
|
if apply_base_prop(dim.base_mut(), field, value) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
match dim {
|
|
|
|
|
|
Dimension::Aligned(d) => apply_linear_fields_aligned(d, field, value),
|
|
|
|
|
|
Dimension::Linear(d) => apply_linear_fields_linear(d, field, value),
|
|
|
|
|
|
Dimension::Radius(d) => apply_radius_fields(d, field, value),
|
|
|
|
|
|
Dimension::Diameter(d) => apply_diameter_fields(d, field, value),
|
|
|
|
|
|
Dimension::Angular2Ln(d) => apply_angular2_fields(d, field, value),
|
|
|
|
|
|
Dimension::Angular3Pt(d) => apply_angular3_fields(d, field, value),
|
|
|
|
|
|
Dimension::Ordinate(d) => apply_ordinate_fields(d, field, value),
|
|
|
|
|
|
}
|
|
|
|
|
|
dim.base_mut().actual_measurement = dim.measurement();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn apply_linear_fields_aligned(d: &mut DimensionAligned, field: &str, value: &str) {
|
|
|
|
|
|
apply_linear_common(
|
|
|
|
|
|
&mut d.first_point,
|
|
|
|
|
|
&mut d.second_point,
|
|
|
|
|
|
&mut d.definition_point,
|
|
|
|
|
|
field,
|
|
|
|
|
|
value,
|
|
|
|
|
|
);
|
2026-06-27 17:47:50 +03:00
|
|
|
|
// Only the oblique field touches ext_line_rotation — otherwise editing a
|
|
|
|
|
|
// coordinate (e.g. First X) would corrupt the dimension's obliquing. #181.
|
|
|
|
|
|
if field == "ext_line_rotation" {
|
|
|
|
|
|
let _ = assign_deg(value, &mut d.ext_line_rotation);
|
|
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn apply_linear_fields_linear(d: &mut DimensionLinear, field: &str, value: &str) {
|
|
|
|
|
|
apply_linear_common(
|
|
|
|
|
|
&mut d.first_point,
|
|
|
|
|
|
&mut d.second_point,
|
|
|
|
|
|
&mut d.definition_point,
|
|
|
|
|
|
field,
|
|
|
|
|
|
value,
|
|
|
|
|
|
);
|
|
|
|
|
|
match field {
|
|
|
|
|
|
"rotation" => {
|
2026-06-27 17:47:50 +03:00
|
|
|
|
let _ = assign_deg(value, &mut d.rotation);
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
"ext_line_rotation" => {
|
2026-06-27 17:47:50 +03:00
|
|
|
|
let _ = assign_deg(value, &mut d.ext_line_rotation);
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn apply_linear_common(
|
|
|
|
|
|
first: &mut acadrust::types::Vector3,
|
|
|
|
|
|
second: &mut acadrust::types::Vector3,
|
|
|
|
|
|
definition: &mut acadrust::types::Vector3,
|
|
|
|
|
|
field: &str,
|
|
|
|
|
|
value: &str,
|
|
|
|
|
|
) {
|
|
|
|
|
|
match field {
|
|
|
|
|
|
"first_x" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut first.x);
|
|
|
|
|
|
}
|
|
|
|
|
|
"first_y" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut first.y);
|
|
|
|
|
|
}
|
|
|
|
|
|
"first_z" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut first.z);
|
|
|
|
|
|
}
|
|
|
|
|
|
"second_x" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut second.x);
|
|
|
|
|
|
}
|
|
|
|
|
|
"second_y" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut second.y);
|
|
|
|
|
|
}
|
|
|
|
|
|
"second_z" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut second.z);
|
|
|
|
|
|
}
|
|
|
|
|
|
"definition_x" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut definition.x);
|
|
|
|
|
|
}
|
|
|
|
|
|
"definition_y" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut definition.y);
|
|
|
|
|
|
}
|
|
|
|
|
|
"definition_z" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut definition.z);
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn apply_radius_fields(d: &mut DimensionRadius, field: &str, value: &str) {
|
|
|
|
|
|
apply_radius_common(&mut d.angle_vertex, &mut d.definition_point, field, value);
|
|
|
|
|
|
if field == "leader_length" {
|
|
|
|
|
|
let _ = assign_f64(value, &mut d.leader_length);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn apply_diameter_fields(d: &mut DimensionDiameter, field: &str, value: &str) {
|
|
|
|
|
|
apply_radius_common(&mut d.angle_vertex, &mut d.definition_point, field, value);
|
|
|
|
|
|
if field == "leader_length" {
|
|
|
|
|
|
let _ = assign_f64(value, &mut d.leader_length);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn apply_radius_common(
|
|
|
|
|
|
center: &mut acadrust::types::Vector3,
|
|
|
|
|
|
point: &mut acadrust::types::Vector3,
|
|
|
|
|
|
field: &str,
|
|
|
|
|
|
value: &str,
|
|
|
|
|
|
) {
|
|
|
|
|
|
match field {
|
|
|
|
|
|
"center_x" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut center.x);
|
|
|
|
|
|
}
|
|
|
|
|
|
"center_y" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut center.y);
|
|
|
|
|
|
}
|
|
|
|
|
|
"center_z" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut center.z);
|
|
|
|
|
|
}
|
|
|
|
|
|
"point_x" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut point.x);
|
|
|
|
|
|
}
|
|
|
|
|
|
"point_y" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut point.y);
|
|
|
|
|
|
}
|
|
|
|
|
|
"point_z" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut point.z);
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn apply_angular2_fields(d: &mut DimensionAngular2Ln, field: &str, value: &str) {
|
|
|
|
|
|
apply_angular_common(
|
|
|
|
|
|
&mut d.angle_vertex,
|
|
|
|
|
|
&mut d.first_point,
|
|
|
|
|
|
&mut d.second_point,
|
|
|
|
|
|
&mut d.definition_point,
|
|
|
|
|
|
field,
|
|
|
|
|
|
value,
|
|
|
|
|
|
);
|
|
|
|
|
|
match field {
|
|
|
|
|
|
"dimension_arc_x" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut d.dimension_arc.x);
|
|
|
|
|
|
}
|
|
|
|
|
|
"dimension_arc_y" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut d.dimension_arc.y);
|
|
|
|
|
|
}
|
|
|
|
|
|
"dimension_arc_z" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut d.dimension_arc.z);
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn apply_angular3_fields(d: &mut DimensionAngular3Pt, field: &str, value: &str) {
|
|
|
|
|
|
apply_angular_common(
|
|
|
|
|
|
&mut d.angle_vertex,
|
|
|
|
|
|
&mut d.first_point,
|
|
|
|
|
|
&mut d.second_point,
|
|
|
|
|
|
&mut d.definition_point,
|
|
|
|
|
|
field,
|
|
|
|
|
|
value,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn apply_angular_common(
|
|
|
|
|
|
vertex: &mut acadrust::types::Vector3,
|
|
|
|
|
|
first: &mut acadrust::types::Vector3,
|
|
|
|
|
|
second: &mut acadrust::types::Vector3,
|
|
|
|
|
|
definition: &mut acadrust::types::Vector3,
|
|
|
|
|
|
field: &str,
|
|
|
|
|
|
value: &str,
|
|
|
|
|
|
) {
|
|
|
|
|
|
match field {
|
|
|
|
|
|
"vertex_x" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut vertex.x);
|
|
|
|
|
|
}
|
|
|
|
|
|
"vertex_y" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut vertex.y);
|
|
|
|
|
|
}
|
|
|
|
|
|
"vertex_z" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut vertex.z);
|
|
|
|
|
|
}
|
|
|
|
|
|
"first_x" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut first.x);
|
|
|
|
|
|
}
|
|
|
|
|
|
"first_y" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut first.y);
|
|
|
|
|
|
}
|
|
|
|
|
|
"first_z" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut first.z);
|
|
|
|
|
|
}
|
|
|
|
|
|
"second_x" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut second.x);
|
|
|
|
|
|
}
|
|
|
|
|
|
"second_y" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut second.y);
|
|
|
|
|
|
}
|
|
|
|
|
|
"second_z" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut second.z);
|
|
|
|
|
|
}
|
|
|
|
|
|
"definition_x" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut definition.x);
|
|
|
|
|
|
}
|
|
|
|
|
|
"definition_y" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut definition.y);
|
|
|
|
|
|
}
|
|
|
|
|
|
"definition_z" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut definition.z);
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn apply_ordinate_fields(d: &mut DimensionOrdinate, field: &str, value: &str) {
|
|
|
|
|
|
match field {
|
|
|
|
|
|
"definition_x" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut d.definition_point.x);
|
|
|
|
|
|
}
|
|
|
|
|
|
"definition_y" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut d.definition_point.y);
|
|
|
|
|
|
}
|
|
|
|
|
|
"definition_z" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut d.definition_point.z);
|
|
|
|
|
|
}
|
|
|
|
|
|
"feature_x" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut d.feature_location.x);
|
|
|
|
|
|
}
|
|
|
|
|
|
"feature_y" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut d.feature_location.y);
|
|
|
|
|
|
}
|
|
|
|
|
|
"feature_z" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut d.feature_location.z);
|
|
|
|
|
|
}
|
|
|
|
|
|
"leader_x" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut d.leader_endpoint.x);
|
|
|
|
|
|
}
|
|
|
|
|
|
"leader_y" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut d.leader_endpoint.y);
|
|
|
|
|
|
}
|
|
|
|
|
|
"leader_z" => {
|
|
|
|
|
|
let _ = assign_f64(value, &mut d.leader_endpoint.z);
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn apply_transform(dim: &mut Dimension, t: &EntityTransform) {
|
|
|
|
|
|
match t {
|
|
|
|
|
|
EntityTransform::Translate(d) => dim.translate(acadrust::types::Vector3::new(
|
2026-05-12 10:36:15 +03:00
|
|
|
|
d.x as f64, d.y as f64, d.z as f64,
|
2026-03-22 14:56:27 +03:00
|
|
|
|
)),
|
|
|
|
|
|
EntityTransform::Rotate { center, angle_rad } => {
|
|
|
|
|
|
transform_dimension_points(dim, |pt| rotate_point(pt, *center, *angle_rad))
|
|
|
|
|
|
}
|
|
|
|
|
|
EntityTransform::Scale { center, factor } => {
|
|
|
|
|
|
transform_dimension_points(dim, |pt| scale_point(pt, *center, *factor))
|
|
|
|
|
|
}
|
|
|
|
|
|
EntityTransform::Mirror { p1, p2 } => {
|
|
|
|
|
|
transform_dimension_points(dim, |pt| mirror_point(pt, *p1, *p2))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
dim.base_mut().actual_measurement = dim.measurement();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn transform_dimension_points<F>(dim: &mut Dimension, mut f: F)
|
|
|
|
|
|
where
|
|
|
|
|
|
F: FnMut(&mut acadrust::types::Vector3),
|
|
|
|
|
|
{
|
|
|
|
|
|
f(&mut dim.base_mut().text_middle_point);
|
|
|
|
|
|
f(&mut dim.base_mut().insertion_point);
|
|
|
|
|
|
match dim {
|
|
|
|
|
|
Dimension::Aligned(d) => {
|
|
|
|
|
|
f(&mut d.first_point);
|
|
|
|
|
|
f(&mut d.second_point);
|
|
|
|
|
|
f(&mut d.definition_point);
|
|
|
|
|
|
}
|
|
|
|
|
|
Dimension::Linear(d) => {
|
|
|
|
|
|
f(&mut d.first_point);
|
|
|
|
|
|
f(&mut d.second_point);
|
|
|
|
|
|
f(&mut d.definition_point);
|
|
|
|
|
|
}
|
|
|
|
|
|
Dimension::Radius(d) => {
|
|
|
|
|
|
f(&mut d.angle_vertex);
|
|
|
|
|
|
f(&mut d.definition_point);
|
|
|
|
|
|
}
|
|
|
|
|
|
Dimension::Diameter(d) => {
|
|
|
|
|
|
f(&mut d.angle_vertex);
|
|
|
|
|
|
f(&mut d.definition_point);
|
|
|
|
|
|
}
|
|
|
|
|
|
Dimension::Angular2Ln(d) => {
|
|
|
|
|
|
f(&mut d.dimension_arc);
|
|
|
|
|
|
f(&mut d.first_point);
|
|
|
|
|
|
f(&mut d.second_point);
|
|
|
|
|
|
f(&mut d.angle_vertex);
|
|
|
|
|
|
f(&mut d.definition_point);
|
|
|
|
|
|
}
|
|
|
|
|
|
Dimension::Angular3Pt(d) => {
|
|
|
|
|
|
f(&mut d.first_point);
|
|
|
|
|
|
f(&mut d.second_point);
|
|
|
|
|
|
f(&mut d.angle_vertex);
|
|
|
|
|
|
f(&mut d.definition_point);
|
|
|
|
|
|
}
|
|
|
|
|
|
Dimension::Ordinate(d) => {
|
|
|
|
|
|
f(&mut d.definition_point);
|
|
|
|
|
|
f(&mut d.feature_location);
|
|
|
|
|
|
f(&mut d.leader_endpoint);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-12 00:34:38 +03:00
|
|
|
|
fn rotate_point(p: &mut acadrust::types::Vector3, center: DVec3, angle_rad: f64) {
|
2026-06-24 03:32:11 +03:00
|
|
|
|
let dx = p.x - center.x;
|
|
|
|
|
|
let dy = p.y - center.y;
|
2026-07-12 00:34:38 +03:00
|
|
|
|
let (s, c) = angle_rad.sin_cos();
|
2026-06-24 03:32:11 +03:00
|
|
|
|
p.x = center.x + dx * c - dy * s;
|
|
|
|
|
|
p.y = center.y + dx * s + dy * c;
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-12 00:34:38 +03:00
|
|
|
|
fn scale_point(p: &mut acadrust::types::Vector3, center: DVec3, factor: f64) {
|
|
|
|
|
|
let f = factor;
|
2026-06-24 03:32:11 +03:00
|
|
|
|
p.x = center.x + (p.x - center.x) * f;
|
|
|
|
|
|
p.y = center.y + (p.y - center.y) * f;
|
|
|
|
|
|
p.z = center.z + (p.z - center.z) * f;
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-24 03:32:11 +03:00
|
|
|
|
fn mirror_point(p: &mut acadrust::types::Vector3, p1: DVec3, p2: DVec3) {
|
2026-06-16 11:33:46 +03:00
|
|
|
|
crate::scene::view::transform::reflect_xy_point(&mut p.x, &mut p.y, p1, p2);
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl PropertyEditable for Dimension {
|
2026-07-02 22:46:19 +03:00
|
|
|
|
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
2026-03-22 14:56:27 +03:00
|
|
|
|
properties(self)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
|
|
|
|
|
apply_geom_prop(self, field, value);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Transformable for Dimension {
|
|
|
|
|
|
fn apply_transform(&mut self, t: &EntityTransform) {
|
|
|
|
|
|
apply_transform(self, t);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
Feat: dimension grip editing — all dimension types get stretch grips
Linear/Aligned: first point, second point, definition point, text position
Radius/Diameter: center (angle_vertex), radius point, text position
Angular2Ln/Angular3Pt: vertex, two extension points, arc def point, text
Ordinate: origin, feature location, leader endpoint, text position
Text position grip shared across all types; measurement recalculated after edit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 19:11:21 +03:00
|
|
|
|
|
|
|
|
|
|
// ── Grippable ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
2026-06-11 00:04:37 +03:00
|
|
|
|
/// f64 variant for grip positions — grips must not round UTM-scale
|
|
|
|
|
|
/// coordinates through f32 before the world-offset subtraction.
|
|
|
|
|
|
fn dv3(v: &acadrust::types::Vector3) -> glam::DVec3 {
|
|
|
|
|
|
glam::DVec3::new(v.x, v.y, v.z)
|
Feat: dimension grip editing — all dimension types get stretch grips
Linear/Aligned: first point, second point, definition point, text position
Radius/Diameter: center (angle_vertex), radius point, text position
Angular2Ln/Angular3Pt: vertex, two extension points, arc def point, text
Ordinate: origin, feature location, leader endpoint, text position
Text position grip shared across all types; measurement recalculated after edit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 19:11:21 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-24 03:32:11 +03:00
|
|
|
|
fn set_v3(target: &mut acadrust::types::Vector3, p: DVec3) {
|
|
|
|
|
|
target.x = p.x;
|
|
|
|
|
|
target.y = p.y;
|
|
|
|
|
|
target.z = p.z;
|
Feat: dimension grip editing — all dimension types get stretch grips
Linear/Aligned: first point, second point, definition point, text position
Radius/Diameter: center (angle_vertex), radius point, text position
Angular2Ln/Angular3Pt: vertex, two extension points, arc def point, text
Ordinate: origin, feature location, leader endpoint, text position
Text position grip shared across all types; measurement recalculated after edit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 19:11:21 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-24 03:32:11 +03:00
|
|
|
|
fn translate_v3(target: &mut acadrust::types::Vector3, d: DVec3) {
|
|
|
|
|
|
target.x += d.x;
|
|
|
|
|
|
target.y += d.y;
|
|
|
|
|
|
target.z += d.z;
|
Feat: dimension grip editing — all dimension types get stretch grips
Linear/Aligned: first point, second point, definition point, text position
Radius/Diameter: center (angle_vertex), radius point, text position
Angular2Ln/Angular3Pt: vertex, two extension points, arc def point, text
Ordinate: origin, feature location, leader endpoint, text position
Text position grip shared across all types; measurement recalculated after edit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 19:11:21 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn apply_to_v3(target: &mut acadrust::types::Vector3, apply: &GripApply) {
|
|
|
|
|
|
match apply {
|
|
|
|
|
|
GripApply::Absolute(p) => set_v3(target, *p),
|
|
|
|
|
|
GripApply::Translate(d) => translate_v3(target, *d),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl Grippable for Dimension {
|
|
|
|
|
|
fn grips(&self) -> Vec<GripDef> {
|
2026-06-27 17:47:50 +03:00
|
|
|
|
// Auto-placed dimensions carry a zero text_middle_point sentinel; put
|
|
|
|
|
|
// the text grip at the style-default placement (default metrics) instead
|
|
|
|
|
|
// of the world origin, so it stays on the visible text and grabbable.
|
|
|
|
|
|
let text = {
|
|
|
|
|
|
let p = self.base().text_middle_point;
|
|
|
|
|
|
if p.x * p.x + p.y * p.y + p.z * p.z > 1e-16 {
|
|
|
|
|
|
dv3(&p)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
dv3(&dimension_text_pos_f64(self, None, 2.5, 1.0))
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
Feat: dimension grip editing — all dimension types get stretch grips
Linear/Aligned: first point, second point, definition point, text position
Radius/Diameter: center (angle_vertex), radius point, text position
Angular2Ln/Angular3Pt: vertex, two extension points, arc def point, text
Ordinate: origin, feature location, leader endpoint, text position
Text position grip shared across all types; measurement recalculated after edit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 19:11:21 +03:00
|
|
|
|
match self {
|
|
|
|
|
|
Dimension::Linear(d) => vec![
|
2026-06-11 00:04:37 +03:00
|
|
|
|
square_grip(0, dv3(&d.first_point)),
|
|
|
|
|
|
center_grip(1, dv3(&d.second_point)),
|
|
|
|
|
|
center_grip(2, dv3(&d.definition_point)),
|
refactor(grip): square-only marker set + oriented stretch handles
Phase 1 of bringing the grip vocabulary in line with the standard
CAD convention: drop the per-position-type shape encoding (Diamond
for translate, Circle for parameter) and use a plain square for
every endpoint / vertex / centre grip. The behavioural midpoint vs
vertex distinction stays — it lives on `GripDef::is_midpoint` and
drives `Translate` vs `Absolute` in the grip-edit dispatcher — only
the marker shape collapses.
- `GripShape` keeps `Square`, `Rectangle`, `Triangle`. Diamond /
Circle are gone.
- `GripDef` gains `dir: Option<[f32; 2]>` — a world-XY direction
vector consumed by `Rectangle` to orient the box along its
segment.
- `entities::common` exposes `square_grip`, `center_grip` (same
square marker, flagged as translate), `rectangle_grip(id, world,
dir)` for oriented mid-segment handles, and a kept-for-Phase-2
`triangle_grip`.
- `overlay::GripMarker` / `grips_to_screen[_paper]` thread `dir`
through to the canvas painter; the Rectangle path rotates the
box around the grip centre using the world-XY direction (flipping
the sin for screen-Y).
Every existing `diamond_grip(id, w)` call becomes `center_grip(id,
w)` — the visual change is square-replacing-diamond at line
midpoints / arc midpoints / circle centres / ellipse centres /
viewport / hatch loop centroids / dimension grips / image corners /
multileader / ray / underlay / ole2frame / solid3d.
`LwPolyline` grows a stretch handle on every segment (straight or
arc), drawn as the new oriented rectangle. The grip-edit path
translates both segment endpoints for straight segments and
adjusts the arc bulge from the new midpoint for arc segments.
2026-05-29 02:07:34 +03:00
|
|
|
|
center_grip(3, text),
|
Feat: dimension grip editing — all dimension types get stretch grips
Linear/Aligned: first point, second point, definition point, text position
Radius/Diameter: center (angle_vertex), radius point, text position
Angular2Ln/Angular3Pt: vertex, two extension points, arc def point, text
Ordinate: origin, feature location, leader endpoint, text position
Text position grip shared across all types; measurement recalculated after edit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 19:11:21 +03:00
|
|
|
|
],
|
|
|
|
|
|
Dimension::Aligned(d) => vec![
|
2026-06-11 00:04:37 +03:00
|
|
|
|
square_grip(0, dv3(&d.first_point)),
|
|
|
|
|
|
center_grip(1, dv3(&d.second_point)),
|
|
|
|
|
|
center_grip(2, dv3(&d.definition_point)),
|
refactor(grip): square-only marker set + oriented stretch handles
Phase 1 of bringing the grip vocabulary in line with the standard
CAD convention: drop the per-position-type shape encoding (Diamond
for translate, Circle for parameter) and use a plain square for
every endpoint / vertex / centre grip. The behavioural midpoint vs
vertex distinction stays — it lives on `GripDef::is_midpoint` and
drives `Translate` vs `Absolute` in the grip-edit dispatcher — only
the marker shape collapses.
- `GripShape` keeps `Square`, `Rectangle`, `Triangle`. Diamond /
Circle are gone.
- `GripDef` gains `dir: Option<[f32; 2]>` — a world-XY direction
vector consumed by `Rectangle` to orient the box along its
segment.
- `entities::common` exposes `square_grip`, `center_grip` (same
square marker, flagged as translate), `rectangle_grip(id, world,
dir)` for oriented mid-segment handles, and a kept-for-Phase-2
`triangle_grip`.
- `overlay::GripMarker` / `grips_to_screen[_paper]` thread `dir`
through to the canvas painter; the Rectangle path rotates the
box around the grip centre using the world-XY direction (flipping
the sin for screen-Y).
Every existing `diamond_grip(id, w)` call becomes `center_grip(id,
w)` — the visual change is square-replacing-diamond at line
midpoints / arc midpoints / circle centres / ellipse centres /
viewport / hatch loop centroids / dimension grips / image corners /
multileader / ray / underlay / ole2frame / solid3d.
`LwPolyline` grows a stretch handle on every segment (straight or
arc), drawn as the new oriented rectangle. The grip-edit path
translates both segment endpoints for straight segments and
adjusts the arc bulge from the new midpoint for arc segments.
2026-05-29 02:07:34 +03:00
|
|
|
|
center_grip(3, text),
|
Feat: dimension grip editing — all dimension types get stretch grips
Linear/Aligned: first point, second point, definition point, text position
Radius/Diameter: center (angle_vertex), radius point, text position
Angular2Ln/Angular3Pt: vertex, two extension points, arc def point, text
Ordinate: origin, feature location, leader endpoint, text position
Text position grip shared across all types; measurement recalculated after edit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 19:11:21 +03:00
|
|
|
|
],
|
|
|
|
|
|
Dimension::Radius(d) => vec![
|
2026-06-11 00:04:37 +03:00
|
|
|
|
square_grip(0, dv3(&d.angle_vertex)),
|
|
|
|
|
|
center_grip(1, dv3(&d.definition_point)),
|
refactor(grip): square-only marker set + oriented stretch handles
Phase 1 of bringing the grip vocabulary in line with the standard
CAD convention: drop the per-position-type shape encoding (Diamond
for translate, Circle for parameter) and use a plain square for
every endpoint / vertex / centre grip. The behavioural midpoint vs
vertex distinction stays — it lives on `GripDef::is_midpoint` and
drives `Translate` vs `Absolute` in the grip-edit dispatcher — only
the marker shape collapses.
- `GripShape` keeps `Square`, `Rectangle`, `Triangle`. Diamond /
Circle are gone.
- `GripDef` gains `dir: Option<[f32; 2]>` — a world-XY direction
vector consumed by `Rectangle` to orient the box along its
segment.
- `entities::common` exposes `square_grip`, `center_grip` (same
square marker, flagged as translate), `rectangle_grip(id, world,
dir)` for oriented mid-segment handles, and a kept-for-Phase-2
`triangle_grip`.
- `overlay::GripMarker` / `grips_to_screen[_paper]` thread `dir`
through to the canvas painter; the Rectangle path rotates the
box around the grip centre using the world-XY direction (flipping
the sin for screen-Y).
Every existing `diamond_grip(id, w)` call becomes `center_grip(id,
w)` — the visual change is square-replacing-diamond at line
midpoints / arc midpoints / circle centres / ellipse centres /
viewport / hatch loop centroids / dimension grips / image corners /
multileader / ray / underlay / ole2frame / solid3d.
`LwPolyline` grows a stretch handle on every segment (straight or
arc), drawn as the new oriented rectangle. The grip-edit path
translates both segment endpoints for straight segments and
adjusts the arc bulge from the new midpoint for arc segments.
2026-05-29 02:07:34 +03:00
|
|
|
|
center_grip(2, text),
|
Feat: dimension grip editing — all dimension types get stretch grips
Linear/Aligned: first point, second point, definition point, text position
Radius/Diameter: center (angle_vertex), radius point, text position
Angular2Ln/Angular3Pt: vertex, two extension points, arc def point, text
Ordinate: origin, feature location, leader endpoint, text position
Text position grip shared across all types; measurement recalculated after edit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 19:11:21 +03:00
|
|
|
|
],
|
|
|
|
|
|
Dimension::Diameter(d) => vec![
|
2026-06-11 00:04:37 +03:00
|
|
|
|
square_grip(0, dv3(&d.angle_vertex)),
|
|
|
|
|
|
center_grip(1, dv3(&d.definition_point)),
|
refactor(grip): square-only marker set + oriented stretch handles
Phase 1 of bringing the grip vocabulary in line with the standard
CAD convention: drop the per-position-type shape encoding (Diamond
for translate, Circle for parameter) and use a plain square for
every endpoint / vertex / centre grip. The behavioural midpoint vs
vertex distinction stays — it lives on `GripDef::is_midpoint` and
drives `Translate` vs `Absolute` in the grip-edit dispatcher — only
the marker shape collapses.
- `GripShape` keeps `Square`, `Rectangle`, `Triangle`. Diamond /
Circle are gone.
- `GripDef` gains `dir: Option<[f32; 2]>` — a world-XY direction
vector consumed by `Rectangle` to orient the box along its
segment.
- `entities::common` exposes `square_grip`, `center_grip` (same
square marker, flagged as translate), `rectangle_grip(id, world,
dir)` for oriented mid-segment handles, and a kept-for-Phase-2
`triangle_grip`.
- `overlay::GripMarker` / `grips_to_screen[_paper]` thread `dir`
through to the canvas painter; the Rectangle path rotates the
box around the grip centre using the world-XY direction (flipping
the sin for screen-Y).
Every existing `diamond_grip(id, w)` call becomes `center_grip(id,
w)` — the visual change is square-replacing-diamond at line
midpoints / arc midpoints / circle centres / ellipse centres /
viewport / hatch loop centroids / dimension grips / image corners /
multileader / ray / underlay / ole2frame / solid3d.
`LwPolyline` grows a stretch handle on every segment (straight or
arc), drawn as the new oriented rectangle. The grip-edit path
translates both segment endpoints for straight segments and
adjusts the arc bulge from the new midpoint for arc segments.
2026-05-29 02:07:34 +03:00
|
|
|
|
center_grip(2, text),
|
Feat: dimension grip editing — all dimension types get stretch grips
Linear/Aligned: first point, second point, definition point, text position
Radius/Diameter: center (angle_vertex), radius point, text position
Angular2Ln/Angular3Pt: vertex, two extension points, arc def point, text
Ordinate: origin, feature location, leader endpoint, text position
Text position grip shared across all types; measurement recalculated after edit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 19:11:21 +03:00
|
|
|
|
],
|
|
|
|
|
|
Dimension::Angular2Ln(d) => vec![
|
2026-06-11 00:04:37 +03:00
|
|
|
|
square_grip(0, dv3(&d.angle_vertex)),
|
|
|
|
|
|
center_grip(1, dv3(&d.first_point)),
|
|
|
|
|
|
center_grip(2, dv3(&d.second_point)),
|
|
|
|
|
|
center_grip(3, dv3(&d.definition_point)),
|
refactor(grip): square-only marker set + oriented stretch handles
Phase 1 of bringing the grip vocabulary in line with the standard
CAD convention: drop the per-position-type shape encoding (Diamond
for translate, Circle for parameter) and use a plain square for
every endpoint / vertex / centre grip. The behavioural midpoint vs
vertex distinction stays — it lives on `GripDef::is_midpoint` and
drives `Translate` vs `Absolute` in the grip-edit dispatcher — only
the marker shape collapses.
- `GripShape` keeps `Square`, `Rectangle`, `Triangle`. Diamond /
Circle are gone.
- `GripDef` gains `dir: Option<[f32; 2]>` — a world-XY direction
vector consumed by `Rectangle` to orient the box along its
segment.
- `entities::common` exposes `square_grip`, `center_grip` (same
square marker, flagged as translate), `rectangle_grip(id, world,
dir)` for oriented mid-segment handles, and a kept-for-Phase-2
`triangle_grip`.
- `overlay::GripMarker` / `grips_to_screen[_paper]` thread `dir`
through to the canvas painter; the Rectangle path rotates the
box around the grip centre using the world-XY direction (flipping
the sin for screen-Y).
Every existing `diamond_grip(id, w)` call becomes `center_grip(id,
w)` — the visual change is square-replacing-diamond at line
midpoints / arc midpoints / circle centres / ellipse centres /
viewport / hatch loop centroids / dimension grips / image corners /
multileader / ray / underlay / ole2frame / solid3d.
`LwPolyline` grows a stretch handle on every segment (straight or
arc), drawn as the new oriented rectangle. The grip-edit path
translates both segment endpoints for straight segments and
adjusts the arc bulge from the new midpoint for arc segments.
2026-05-29 02:07:34 +03:00
|
|
|
|
center_grip(4, text),
|
Feat: dimension grip editing — all dimension types get stretch grips
Linear/Aligned: first point, second point, definition point, text position
Radius/Diameter: center (angle_vertex), radius point, text position
Angular2Ln/Angular3Pt: vertex, two extension points, arc def point, text
Ordinate: origin, feature location, leader endpoint, text position
Text position grip shared across all types; measurement recalculated after edit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 19:11:21 +03:00
|
|
|
|
],
|
|
|
|
|
|
Dimension::Angular3Pt(d) => vec![
|
2026-06-11 00:04:37 +03:00
|
|
|
|
square_grip(0, dv3(&d.angle_vertex)),
|
|
|
|
|
|
center_grip(1, dv3(&d.first_point)),
|
|
|
|
|
|
center_grip(2, dv3(&d.second_point)),
|
|
|
|
|
|
center_grip(3, dv3(&d.definition_point)),
|
refactor(grip): square-only marker set + oriented stretch handles
Phase 1 of bringing the grip vocabulary in line with the standard
CAD convention: drop the per-position-type shape encoding (Diamond
for translate, Circle for parameter) and use a plain square for
every endpoint / vertex / centre grip. The behavioural midpoint vs
vertex distinction stays — it lives on `GripDef::is_midpoint` and
drives `Translate` vs `Absolute` in the grip-edit dispatcher — only
the marker shape collapses.
- `GripShape` keeps `Square`, `Rectangle`, `Triangle`. Diamond /
Circle are gone.
- `GripDef` gains `dir: Option<[f32; 2]>` — a world-XY direction
vector consumed by `Rectangle` to orient the box along its
segment.
- `entities::common` exposes `square_grip`, `center_grip` (same
square marker, flagged as translate), `rectangle_grip(id, world,
dir)` for oriented mid-segment handles, and a kept-for-Phase-2
`triangle_grip`.
- `overlay::GripMarker` / `grips_to_screen[_paper]` thread `dir`
through to the canvas painter; the Rectangle path rotates the
box around the grip centre using the world-XY direction (flipping
the sin for screen-Y).
Every existing `diamond_grip(id, w)` call becomes `center_grip(id,
w)` — the visual change is square-replacing-diamond at line
midpoints / arc midpoints / circle centres / ellipse centres /
viewport / hatch loop centroids / dimension grips / image corners /
multileader / ray / underlay / ole2frame / solid3d.
`LwPolyline` grows a stretch handle on every segment (straight or
arc), drawn as the new oriented rectangle. The grip-edit path
translates both segment endpoints for straight segments and
adjusts the arc bulge from the new midpoint for arc segments.
2026-05-29 02:07:34 +03:00
|
|
|
|
center_grip(4, text),
|
Feat: dimension grip editing — all dimension types get stretch grips
Linear/Aligned: first point, second point, definition point, text position
Radius/Diameter: center (angle_vertex), radius point, text position
Angular2Ln/Angular3Pt: vertex, two extension points, arc def point, text
Ordinate: origin, feature location, leader endpoint, text position
Text position grip shared across all types; measurement recalculated after edit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 19:11:21 +03:00
|
|
|
|
],
|
|
|
|
|
|
Dimension::Ordinate(d) => vec![
|
2026-06-11 00:04:37 +03:00
|
|
|
|
square_grip(0, dv3(&d.definition_point)),
|
|
|
|
|
|
center_grip(1, dv3(&d.feature_location)),
|
|
|
|
|
|
center_grip(2, dv3(&d.leader_endpoint)),
|
refactor(grip): square-only marker set + oriented stretch handles
Phase 1 of bringing the grip vocabulary in line with the standard
CAD convention: drop the per-position-type shape encoding (Diamond
for translate, Circle for parameter) and use a plain square for
every endpoint / vertex / centre grip. The behavioural midpoint vs
vertex distinction stays — it lives on `GripDef::is_midpoint` and
drives `Translate` vs `Absolute` in the grip-edit dispatcher — only
the marker shape collapses.
- `GripShape` keeps `Square`, `Rectangle`, `Triangle`. Diamond /
Circle are gone.
- `GripDef` gains `dir: Option<[f32; 2]>` — a world-XY direction
vector consumed by `Rectangle` to orient the box along its
segment.
- `entities::common` exposes `square_grip`, `center_grip` (same
square marker, flagged as translate), `rectangle_grip(id, world,
dir)` for oriented mid-segment handles, and a kept-for-Phase-2
`triangle_grip`.
- `overlay::GripMarker` / `grips_to_screen[_paper]` thread `dir`
through to the canvas painter; the Rectangle path rotates the
box around the grip centre using the world-XY direction (flipping
the sin for screen-Y).
Every existing `diamond_grip(id, w)` call becomes `center_grip(id,
w)` — the visual change is square-replacing-diamond at line
midpoints / arc midpoints / circle centres / ellipse centres /
viewport / hatch loop centroids / dimension grips / image corners /
multileader / ray / underlay / ole2frame / solid3d.
`LwPolyline` grows a stretch handle on every segment (straight or
arc), drawn as the new oriented rectangle. The grip-edit path
translates both segment endpoints for straight segments and
adjusts the arc bulge from the new midpoint for arc segments.
2026-05-29 02:07:34 +03:00
|
|
|
|
center_grip(3, text),
|
Feat: dimension grip editing — all dimension types get stretch grips
Linear/Aligned: first point, second point, definition point, text position
Radius/Diameter: center (angle_vertex), radius point, text position
Angular2Ln/Angular3Pt: vertex, two extension points, arc def point, text
Ordinate: origin, feature location, leader endpoint, text position
Text position grip shared across all types; measurement recalculated after edit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 19:11:21 +03:00
|
|
|
|
],
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
|
|
|
|
|
// Last grip always moves the text.
|
|
|
|
|
|
let text_grip = match self {
|
|
|
|
|
|
Dimension::Linear(_) | Dimension::Aligned(_) => 3,
|
|
|
|
|
|
Dimension::Radius(_) | Dimension::Diameter(_) => 2,
|
|
|
|
|
|
Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_) => 4,
|
|
|
|
|
|
Dimension::Ordinate(_) => 3,
|
|
|
|
|
|
};
|
|
|
|
|
|
if grip_id == text_grip {
|
|
|
|
|
|
apply_to_v3(&mut self.base_mut().text_middle_point, &apply);
|
2026-06-14 01:57:58 +03:00
|
|
|
|
// Dragging the text grip pins it to a user-defined location, so it
|
|
|
|
|
|
// no longer follows the style (DIMTAD). See #94.
|
|
|
|
|
|
self.base_mut().text_user_positioned = true;
|
Feat: dimension grip editing — all dimension types get stretch grips
Linear/Aligned: first point, second point, definition point, text position
Radius/Diameter: center (angle_vertex), radius point, text position
Angular2Ln/Angular3Pt: vertex, two extension points, arc def point, text
Ordinate: origin, feature location, leader endpoint, text position
Text position grip shared across all types; measurement recalculated after edit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 19:11:21 +03:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
match self {
|
|
|
|
|
|
Dimension::Linear(d) => match grip_id {
|
|
|
|
|
|
0 => apply_to_v3(&mut d.first_point, &apply),
|
|
|
|
|
|
1 => apply_to_v3(&mut d.second_point, &apply),
|
|
|
|
|
|
2 => apply_to_v3(&mut d.definition_point, &apply),
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
},
|
|
|
|
|
|
Dimension::Aligned(d) => match grip_id {
|
|
|
|
|
|
0 => apply_to_v3(&mut d.first_point, &apply),
|
|
|
|
|
|
1 => apply_to_v3(&mut d.second_point, &apply),
|
|
|
|
|
|
2 => apply_to_v3(&mut d.definition_point, &apply),
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
},
|
|
|
|
|
|
Dimension::Radius(d) => match grip_id {
|
|
|
|
|
|
0 => apply_to_v3(&mut d.angle_vertex, &apply),
|
|
|
|
|
|
1 => apply_to_v3(&mut d.definition_point, &apply),
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
},
|
|
|
|
|
|
Dimension::Diameter(d) => match grip_id {
|
|
|
|
|
|
0 => apply_to_v3(&mut d.angle_vertex, &apply),
|
|
|
|
|
|
1 => apply_to_v3(&mut d.definition_point, &apply),
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
},
|
|
|
|
|
|
Dimension::Angular2Ln(d) => match grip_id {
|
|
|
|
|
|
0 => apply_to_v3(&mut d.angle_vertex, &apply),
|
|
|
|
|
|
1 => apply_to_v3(&mut d.first_point, &apply),
|
|
|
|
|
|
2 => apply_to_v3(&mut d.second_point, &apply),
|
|
|
|
|
|
3 => apply_to_v3(&mut d.definition_point, &apply),
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
},
|
|
|
|
|
|
Dimension::Angular3Pt(d) => match grip_id {
|
|
|
|
|
|
0 => apply_to_v3(&mut d.angle_vertex, &apply),
|
|
|
|
|
|
1 => apply_to_v3(&mut d.first_point, &apply),
|
|
|
|
|
|
2 => apply_to_v3(&mut d.second_point, &apply),
|
|
|
|
|
|
3 => apply_to_v3(&mut d.definition_point, &apply),
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
},
|
|
|
|
|
|
Dimension::Ordinate(d) => match grip_id {
|
|
|
|
|
|
0 => apply_to_v3(&mut d.definition_point, &apply),
|
|
|
|
|
|
1 => apply_to_v3(&mut d.feature_location, &apply),
|
|
|
|
|
|
2 => apply_to_v3(&mut d.leader_endpoint, &apply),
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
self.base_mut().actual_measurement = self.measurement();
|
|
|
|
|
|
}
|
2026-05-29 09:46:05 +03:00
|
|
|
|
|
2026-06-16 11:33:46 +03:00
|
|
|
|
fn grip_menu(&self, grip_id: usize) -> Vec<crate::scene::model::object::GripMenuItem> {
|
|
|
|
|
|
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
|
2026-05-29 09:46:05 +03:00
|
|
|
|
let (dim_line_grip, text_grip) = match self {
|
|
|
|
|
|
Dimension::Linear(_) | Dimension::Aligned(_) => (2, 3),
|
|
|
|
|
|
Dimension::Radius(_) | Dimension::Diameter(_) => (1, 2),
|
|
|
|
|
|
Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_) => (3, 4),
|
|
|
|
|
|
Dimension::Ordinate(_) => (0, 3),
|
|
|
|
|
|
};
|
|
|
|
|
|
if grip_id == text_grip {
|
|
|
|
|
|
vec![
|
2026-06-11 00:04:37 +03:00
|
|
|
|
GripMenuItem {
|
|
|
|
|
|
label: "Stretch",
|
|
|
|
|
|
action: GripMenuAction::Stretch,
|
|
|
|
|
|
},
|
|
|
|
|
|
GripMenuItem {
|
|
|
|
|
|
label: "Move with Dim Line",
|
|
|
|
|
|
action: GripMenuAction::MoveWithDimLine,
|
|
|
|
|
|
},
|
|
|
|
|
|
GripMenuItem {
|
|
|
|
|
|
label: "Move with Leader",
|
|
|
|
|
|
action: GripMenuAction::MoveWithLeader,
|
|
|
|
|
|
},
|
|
|
|
|
|
GripMenuItem {
|
|
|
|
|
|
label: "Move Independent",
|
|
|
|
|
|
action: GripMenuAction::MoveIndependent,
|
|
|
|
|
|
},
|
|
|
|
|
|
GripMenuItem {
|
|
|
|
|
|
label: "Reset Text",
|
|
|
|
|
|
action: GripMenuAction::ResetText,
|
|
|
|
|
|
},
|
|
|
|
|
|
GripMenuItem {
|
|
|
|
|
|
label: "Rotate Text",
|
|
|
|
|
|
action: GripMenuAction::RotateText,
|
|
|
|
|
|
},
|
|
|
|
|
|
GripMenuItem {
|
|
|
|
|
|
label: "Above Dim Line",
|
|
|
|
|
|
action: GripMenuAction::AboveDimLine,
|
|
|
|
|
|
},
|
|
|
|
|
|
GripMenuItem {
|
|
|
|
|
|
label: "Center",
|
|
|
|
|
|
action: GripMenuAction::Center,
|
|
|
|
|
|
},
|
2026-05-29 09:46:05 +03:00
|
|
|
|
]
|
|
|
|
|
|
} else if grip_id == dim_line_grip {
|
|
|
|
|
|
vec![
|
2026-06-11 00:04:37 +03:00
|
|
|
|
GripMenuItem {
|
|
|
|
|
|
label: "Stretch",
|
|
|
|
|
|
action: GripMenuAction::Stretch,
|
|
|
|
|
|
},
|
|
|
|
|
|
GripMenuItem {
|
|
|
|
|
|
label: "Reverse Arrows",
|
|
|
|
|
|
action: GripMenuAction::ReverseArrows,
|
|
|
|
|
|
},
|
2026-05-29 09:46:05 +03:00
|
|
|
|
]
|
|
|
|
|
|
} else {
|
2026-06-11 00:04:37 +03:00
|
|
|
|
vec![GripMenuItem {
|
|
|
|
|
|
label: "Stretch",
|
|
|
|
|
|
action: GripMenuAction::Stretch,
|
|
|
|
|
|
}]
|
2026-05-29 09:46:05 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(commands): expand standard CAD command coverage
Integrate a large batch of standard CAD commands and fix several
name/behaviour mismatches, with no product references in code.
3D solids (reusing the truck B-rep pipeline):
- SLICE, INTERFERE, PRESSPULL, POLYSOLID, PYRAMID, SECTION
- 3DROTATE, 3DMIRROR, 3DALIGN (cached-solid transforms)
Annotation / text:
- ARCTEXT, SPLINEFIT (Catmull-Rom to Bezier), DIMJOGGED, OBJECTSCALE
(XData markers honoured by the existing geometry-gen / tessellator)
Blocks / misc:
- NCOPY (block-content extraction via explode_entity)
- OPTIONS, SYNCPVIEWPORTS, THICKEN, plus earlier toggles, aliases,
system variables, and layer/style/view commands
POINTCLOUD/RECAP and UNDERLAYLAYERS/UOSNAP are intentionally left out:
they require a proprietary binary / PDF-layer parser, a GPU point
renderer, and real test files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 08:23:58 +03:00
|
|
|
|
fn apply_grip_menu(
|
|
|
|
|
|
&mut self,
|
|
|
|
|
|
grip_id: usize,
|
|
|
|
|
|
action: crate::scene::model::object::GripMenuAction,
|
|
|
|
|
|
) {
|
2026-06-16 11:33:46 +03:00
|
|
|
|
use crate::scene::model::object::GripMenuAction as A;
|
2026-05-29 09:46:05 +03:00
|
|
|
|
let (_dim_line_grip, text_grip) = match self {
|
|
|
|
|
|
Dimension::Linear(_) | Dimension::Aligned(_) => (2, 3),
|
|
|
|
|
|
Dimension::Radius(_) | Dimension::Diameter(_) => (1, 2),
|
|
|
|
|
|
Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_) => (3, 4),
|
|
|
|
|
|
Dimension::Ordinate(_) => (0, 3),
|
|
|
|
|
|
};
|
|
|
|
|
|
match action {
|
|
|
|
|
|
A::ResetText if grip_id == text_grip => {
|
|
|
|
|
|
// Drop any text-position override — leave it to the
|
|
|
|
|
|
// renderer to recompute from the dim style.
|
|
|
|
|
|
let b = self.base_mut();
|
|
|
|
|
|
b.text_middle_point.x = 0.0;
|
|
|
|
|
|
b.text_middle_point.y = 0.0;
|
|
|
|
|
|
b.text_middle_point.z = 0.0;
|
|
|
|
|
|
}
|
|
|
|
|
|
A::Center if grip_id == text_grip => {
|
|
|
|
|
|
// Snap text to the centre of the dimension line.
|
|
|
|
|
|
// Approximate as midpoint of first/second extension
|
|
|
|
|
|
// origins for Linear / Aligned dimensions.
|
|
|
|
|
|
match self {
|
|
|
|
|
|
Dimension::Linear(d) => {
|
|
|
|
|
|
let mx = (d.first_point.x + d.second_point.x) * 0.5;
|
|
|
|
|
|
let my = (d.first_point.y + d.second_point.y) * 0.5;
|
|
|
|
|
|
d.base.text_middle_point.x = mx;
|
|
|
|
|
|
d.base.text_middle_point.y = my;
|
|
|
|
|
|
}
|
|
|
|
|
|
Dimension::Aligned(d) => {
|
|
|
|
|
|
let mx = (d.first_point.x + d.second_point.x) * 0.5;
|
|
|
|
|
|
let my = (d.first_point.y + d.second_point.y) * 0.5;
|
|
|
|
|
|
d.base.text_middle_point.x = mx;
|
|
|
|
|
|
d.base.text_middle_point.y = my;
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Stretch / Move-variants / Reverse Arrows / Rotate Text /
|
|
|
|
|
|
// Above Dim Line need either a follow-up drag or a numeric
|
|
|
|
|
|
// prompt — wired to default Stretch behaviour for now.
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
Feat: dimension grip editing — all dimension types get stretch grips
Linear/Aligned: first point, second point, definition point, text position
Radius/Diameter: center (angle_vertex), radius point, text position
Angular2Ln/Angular3Pt: vertex, two extension points, arc def point, text
Ordinate: origin, feature location, leader endpoint, text position
Text position grip shared across all types; measurement recalculated after edit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 19:11:21 +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
|
|
|
|
|
|
|
|
|
|
// ── Tessellation ─────────────────────────────────────────────────────────
|
|
|
|
|
|
//
|
|
|
|
|
|
// Per-entity tessellation entry for `Dimension`. The trait + impl live in
|
|
|
|
|
|
// this file so all dimension tess code stays alongside the entity
|
|
|
|
|
|
// definition. Shared dim machinery (`ArrowKind`, `DimGeom`, `append_arrow`,
|
|
|
|
|
|
// arrow blocks, colour resolution, `add_segment` / `add_polyline`,
|
|
|
|
|
|
// `normalized_or`, `entity_z`, `offset_snap_pts`) lives in
|
2026-06-16 11:33:46 +03:00
|
|
|
|
// `scene::convert::tessellate` and is reused by Leader / MultiLeader too.
|
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 acadrust::entities::{MText, Text};
|
|
|
|
|
|
use acadrust::tables::DimStyle;
|
|
|
|
|
|
use acadrust::types::{Color as AcadColor, Vector3};
|
feat(properties): dimension style-derived property groups
Resolve a dimension's DimStyle from the document and inject its full spec
groups — Lines & Arrows, Text, Fit, Primary Units, Alternate Units,
Tolerances — mirroring the ~80 dimension variables (dimasz, dimclrd, dimtxt,
dimdec, dimtol, dimzin bit flags, …) as read-only rows. The panel already had
doc access for the dim-style picker; extend it to append style_sections(style)
after the dimension's own Misc/Geometry groups.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:17:54 +03:00
|
|
|
|
|
|
|
|
|
|
/// Dimension-style-derived property groups (Lines & Arrows, Text, Fit, Primary
|
|
|
|
|
|
/// Units, Alternate Units, Tolerances) built from the resolved DimStyle. These
|
|
|
|
|
|
/// are read-only mirrors of the style's dimension variables, injected by the
|
|
|
|
|
|
/// panel after the entity's own Misc/Geometry groups.
|
|
|
|
|
|
pub fn style_sections(style: &DimStyle) -> Vec<crate::scene::model::object::PropSection> {
|
|
|
|
|
|
let s = style;
|
|
|
|
|
|
let yn = |b: bool| if b { "Yes" } else { "No" };
|
|
|
|
|
|
let onoff = |b: bool| if b { "On" } else { "Off" };
|
|
|
|
|
|
let f = |v: f64| format!("{v:.4}");
|
|
|
|
|
|
let dsep = {
|
|
|
|
|
|
let c = s.dimdsep as u8 as char;
|
|
|
|
|
|
if s.dimdsep > 0 && !c.is_control() {
|
|
|
|
|
|
c.to_string()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
s.dimdsep.to_string()
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
let tol_display = if s.dimlim {
|
|
|
|
|
|
"Limits"
|
|
|
|
|
|
} else if s.dimtol {
|
|
|
|
|
|
"Deviation"
|
|
|
|
|
|
} else {
|
|
|
|
|
|
"None"
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
vec![
|
|
|
|
|
|
PropSection {
|
|
|
|
|
|
title: "Lines & Arrows".into(),
|
|
|
|
|
|
props: vec![
|
|
|
|
|
|
ro("Arrow size", "dim_arrow_size", f(s.dimasz)),
|
|
|
|
|
|
ro("Dim line color", "dim_line_color", s.dimclrd.to_string()),
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Dim line lineweight",
|
|
|
|
|
|
"dim_line_lineweight",
|
|
|
|
|
|
s.dimlwd.to_string(),
|
|
|
|
|
|
),
|
|
|
|
|
|
ro("Ext line color", "dim_ext_line_color", s.dimclre.to_string()),
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Ext line lineweight",
|
|
|
|
|
|
"dim_ext_line_lineweight",
|
|
|
|
|
|
s.dimlwe.to_string(),
|
|
|
|
|
|
),
|
|
|
|
|
|
ro("Dim line 1", "dim_line_1", onoff(!s.dimsd1)),
|
|
|
|
|
|
ro("Dim line 2", "dim_line_2", onoff(!s.dimsd2)),
|
|
|
|
|
|
ro("Ext line 1", "dim_ext_line_1", onoff(!s.dimse1)),
|
|
|
|
|
|
ro("Ext line 2", "dim_ext_line_2", onoff(!s.dimse2)),
|
|
|
|
|
|
ro("Dim line ext", "dim_line_ext", f(s.dimdle)),
|
|
|
|
|
|
ro("Ext line ext", "dim_ext_line_ext", f(s.dimexe)),
|
|
|
|
|
|
ro("Ext line offset", "dim_ext_line_offset", f(s.dimexo)),
|
|
|
|
|
|
ro("Ext line fixed", "dim_ext_line_fixed", yn(s.dimfxlon)),
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Ext line fixed length",
|
|
|
|
|
|
"dim_ext_line_fixed_length",
|
|
|
|
|
|
f(s.dimfxl),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
PropSection {
|
|
|
|
|
|
title: "Text".into(),
|
|
|
|
|
|
props: vec![
|
|
|
|
|
|
ro("Fill color", "dim_text_fill_color", s.dimtfillclr.to_string()),
|
|
|
|
|
|
ro("Text color", "dim_text_color", s.dimclrt.to_string()),
|
|
|
|
|
|
ro("Text height", "dim_text_height", f(s.dimtxt)),
|
|
|
|
|
|
ro("Text offset", "dim_text_offset", f(s.dimgap)),
|
|
|
|
|
|
ro("Text pos vert", "dim_text_pos_vert", s.dimtad.to_string()),
|
|
|
|
|
|
ro("Text pos hor", "dim_text_pos_hor", s.dimjust.to_string()),
|
|
|
|
|
|
ro("Text outside align", "dim_text_outside_align", yn(s.dimtoh)),
|
|
|
|
|
|
ro("Text inside align", "dim_text_inside_align", yn(s.dimtih)),
|
|
|
|
|
|
ro("Text style", "dim_text_style", s.dimtxsty.clone()),
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
PropSection {
|
|
|
|
|
|
title: "Fit".into(),
|
|
|
|
|
|
props: vec![
|
|
|
|
|
|
ro("Fit", "dim_fit", s.dimatfit.to_string()),
|
|
|
|
|
|
ro("Text inside", "dim_text_inside", yn(s.dimtix)),
|
|
|
|
|
|
ro("Text movement", "dim_text_movement", s.dimtmove.to_string()),
|
|
|
|
|
|
ro("Dim scale overall", "dim_scale_overall", f(s.dimscale)),
|
|
|
|
|
|
ro("Dim line forced", "dim_line_forced", yn(s.dimtofl)),
|
|
|
|
|
|
ro("Dim line inside", "dim_line_inside", yn(s.dimsoxd)),
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
PropSection {
|
|
|
|
|
|
title: "Primary Units".into(),
|
|
|
|
|
|
props: vec![
|
|
|
|
|
|
ro("Dim units", "dim_units", s.dimlunit.to_string()),
|
|
|
|
|
|
ro("Precision", "dim_precision", s.dimdec.to_string()),
|
|
|
|
|
|
ro("Decimal separator", "dim_decimal_separator", dsep),
|
|
|
|
|
|
ro("Dim prefix/suffix", "dim_prefix_suffix", s.dimpost.clone()),
|
|
|
|
|
|
ro("Dim roundoff", "dim_roundoff", f(s.dimrnd)),
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Suppress leading zeros",
|
|
|
|
|
|
"dim_suppress_leading_zeros",
|
|
|
|
|
|
yn(s.dimzin & 4 != 0),
|
|
|
|
|
|
),
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Suppress trailing zeros",
|
|
|
|
|
|
"dim_suppress_trailing_zeros",
|
|
|
|
|
|
yn(s.dimzin & 8 != 0),
|
|
|
|
|
|
),
|
|
|
|
|
|
ro("Dim scale linear", "dim_scale_linear", f(s.dimlfac)),
|
|
|
|
|
|
ro("Angle format", "dim_angle_format", s.dimaunit.to_string()),
|
|
|
|
|
|
ro("Angle precision", "dim_angle_precision", s.dimadec.to_string()),
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
PropSection {
|
|
|
|
|
|
title: "Alternate Units".into(),
|
|
|
|
|
|
props: vec![
|
|
|
|
|
|
ro("Alt enabled", "dim_alt_enabled", yn(s.dimalt)),
|
|
|
|
|
|
ro("Alt format", "dim_alt_format", s.dimaltu.to_string()),
|
|
|
|
|
|
ro("Alt precision", "dim_alt_precision", s.dimaltd.to_string()),
|
|
|
|
|
|
ro("Alt scale factor", "dim_alt_scale_factor", f(s.dimaltf)),
|
|
|
|
|
|
ro("Alt roundoff", "dim_alt_roundoff", f(s.dimaltrnd)),
|
|
|
|
|
|
ro("Alt prefix/suffix", "dim_alt_prefix_suffix", s.dimapost.clone()),
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Alt suppress leading zeros",
|
|
|
|
|
|
"dim_alt_suppress_leading_zeros",
|
|
|
|
|
|
yn(s.dimaltz & 4 != 0),
|
|
|
|
|
|
),
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Alt suppress trailing zeros",
|
|
|
|
|
|
"dim_alt_suppress_trailing_zeros",
|
|
|
|
|
|
yn(s.dimaltz & 8 != 0),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
},
|
|
|
|
|
|
PropSection {
|
|
|
|
|
|
title: "Tolerances".into(),
|
|
|
|
|
|
props: vec![
|
|
|
|
|
|
ro("Tolerance display", "dim_tolerance_display", tol_display),
|
|
|
|
|
|
ro("Tolerance limit lower", "dim_tolerance_limit_lower", f(s.dimtm)),
|
|
|
|
|
|
ro("Tolerance limit upper", "dim_tolerance_limit_upper", f(s.dimtp)),
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Tolerance precision",
|
|
|
|
|
|
"dim_tolerance_precision",
|
|
|
|
|
|
s.dimtdec.to_string(),
|
|
|
|
|
|
),
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Tolerance text height",
|
|
|
|
|
|
"dim_tolerance_text_height",
|
|
|
|
|
|
f(s.dimtfac),
|
|
|
|
|
|
),
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Tolerance pos vert",
|
|
|
|
|
|
"dim_tolerance_pos_vert",
|
|
|
|
|
|
s.dimtolj.to_string(),
|
|
|
|
|
|
),
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Tolerance suppress leading zeros",
|
|
|
|
|
|
"dim_tolerance_suppress_leading_zeros",
|
|
|
|
|
|
yn(s.dimtzin & 4 != 0),
|
|
|
|
|
|
),
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Tolerance suppress trailing zeros",
|
|
|
|
|
|
"dim_tolerance_suppress_trailing_zeros",
|
|
|
|
|
|
yn(s.dimtzin & 8 != 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
|
|
|
|
use acadrust::{CadDocument, EntityType, Handle};
|
|
|
|
|
|
|
2026-06-16 11:33:46 +03:00
|
|
|
|
use crate::scene::convert::tess_util::aci_to_rgba;
|
|
|
|
|
|
use crate::scene::convert::tessellate::{
|
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
|
|
|
|
add_polyline, add_segment, append_arrow, arrow_from_block, normalized_or, ArrowKind, DimGeom,
|
|
|
|
|
|
};
|
2026-06-16 11:33:46 +03:00
|
|
|
|
use crate::scene::model::wire_model::{SnapHint, WireModel};
|
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 trait DimensionTess {
|
|
|
|
|
|
fn tessellate(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
document: &CadDocument,
|
|
|
|
|
|
handle: Handle,
|
|
|
|
|
|
selected: bool,
|
|
|
|
|
|
entity_color: [f32; 4],
|
|
|
|
|
|
line_weight_px: f32,
|
|
|
|
|
|
anno_scale: f32,
|
2026-06-05 00:35:53 +03:00
|
|
|
|
selected_set: &rustc_hash::FxHashSet<acadrust::Handle>,
|
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
|
|
|
|
active_viewport: Option<acadrust::Handle>,
|
|
|
|
|
|
bg_color: [f32; 4],
|
|
|
|
|
|
view_aabb: Option<[f32; 4]>,
|
|
|
|
|
|
world_per_pixel: Option<f32>,
|
|
|
|
|
|
) -> Vec<WireModel>;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl DimensionTess for Dimension {
|
|
|
|
|
|
fn tessellate(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
document: &CadDocument,
|
|
|
|
|
|
handle: Handle,
|
|
|
|
|
|
selected: bool,
|
|
|
|
|
|
entity_color: [f32; 4],
|
|
|
|
|
|
line_weight_px: f32,
|
|
|
|
|
|
anno_scale: f32,
|
2026-06-05 00:35:53 +03:00
|
|
|
|
selected_set: &rustc_hash::FxHashSet<acadrust::Handle>,
|
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
|
|
|
|
active_viewport: Option<acadrust::Handle>,
|
|
|
|
|
|
bg_color: [f32; 4],
|
|
|
|
|
|
view_aabb: Option<[f32; 4]>,
|
|
|
|
|
|
world_per_pixel: Option<f32>,
|
|
|
|
|
|
) -> Vec<WireModel> {
|
|
|
|
|
|
tessellate_dimension_inner(
|
|
|
|
|
|
document,
|
|
|
|
|
|
handle,
|
|
|
|
|
|
self,
|
|
|
|
|
|
selected,
|
|
|
|
|
|
entity_color,
|
|
|
|
|
|
line_weight_px,
|
|
|
|
|
|
anno_scale,
|
|
|
|
|
|
selected_set,
|
|
|
|
|
|
active_viewport,
|
|
|
|
|
|
bg_color,
|
|
|
|
|
|
view_aabb,
|
|
|
|
|
|
world_per_pixel,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn tessellate_dimension_inner(
|
|
|
|
|
|
document: &CadDocument,
|
|
|
|
|
|
handle: Handle,
|
|
|
|
|
|
dim: &Dimension,
|
|
|
|
|
|
selected: bool,
|
|
|
|
|
|
entity_color: [f32; 4],
|
|
|
|
|
|
line_weight_px: f32,
|
|
|
|
|
|
anno_scale: f32,
|
|
|
|
|
|
// LOD hints — when present, synthesised dim text routes through the
|
|
|
|
|
|
// top-level LOD ladder (baseline / greek / full) instead of the truck
|
|
|
|
|
|
// path so far-out drawings collapse to a colored rect or baseline.
|
2026-06-05 00:35:53 +03:00
|
|
|
|
selected_set: &rustc_hash::FxHashSet<acadrust::Handle>,
|
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
|
|
|
|
active_viewport: Option<acadrust::Handle>,
|
|
|
|
|
|
bg_color: [f32; 4],
|
|
|
|
|
|
view_aabb: Option<[f32; 4]>,
|
|
|
|
|
|
world_per_pixel: Option<f32>,
|
|
|
|
|
|
) -> Vec<WireModel> {
|
|
|
|
|
|
let name = handle.value().to_string();
|
|
|
|
|
|
// (Baked-block fast path moved up into scene::tessellate_entity so the
|
|
|
|
|
|
// recursive call goes through the LOD ladder, not the truck path.)
|
|
|
|
|
|
|
|
|
|
|
|
let style_name = &dim.base().style_name;
|
|
|
|
|
|
let style = document.dim_styles.iter().find(|s| {
|
|
|
|
|
|
s.name.eq_ignore_ascii_case(style_name)
|
|
|
|
|
|
|| (style_name.trim().is_empty() && s.name.eq_ignore_ascii_case("Standard"))
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// DIMSCALE rule:
|
|
|
|
|
|
// dimstyle.dimscale > 0 → final multiplier; ignore anno_scale.
|
|
|
|
|
|
// dimstyle.dimscale == 0 → annotative: use anno_scale (= 1/vp_scale).
|
|
|
|
|
|
let dim_scale = style
|
|
|
|
|
|
.map(|s| {
|
|
|
|
|
|
if s.dimscale > 1e-6 {
|
|
|
|
|
|
s.dimscale
|
|
|
|
|
|
} else {
|
|
|
|
|
|
anno_scale as f64
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.unwrap_or(1.0);
|
|
|
|
|
|
|
|
|
|
|
|
let (
|
|
|
|
|
|
dimasz_raw,
|
|
|
|
|
|
dimexo,
|
|
|
|
|
|
dimexe,
|
|
|
|
|
|
dim_txt,
|
|
|
|
|
|
dimtsz_raw,
|
|
|
|
|
|
dimsah,
|
|
|
|
|
|
dimse1,
|
|
|
|
|
|
dimse2,
|
|
|
|
|
|
dimsd1,
|
|
|
|
|
|
dimsd2,
|
|
|
|
|
|
dimdle,
|
|
|
|
|
|
dimfxl,
|
|
|
|
|
|
dimfxlon,
|
|
|
|
|
|
dimsoxd,
|
|
|
|
|
|
dimcen,
|
|
|
|
|
|
) = style
|
|
|
|
|
|
.map(|s| {
|
|
|
|
|
|
(
|
|
|
|
|
|
s.dimasz * dim_scale,
|
|
|
|
|
|
(s.dimexo * dim_scale) as f32,
|
|
|
|
|
|
(s.dimexe * dim_scale) as f32,
|
|
|
|
|
|
s.dimtxt * dim_scale,
|
|
|
|
|
|
s.dimtsz * dim_scale,
|
|
|
|
|
|
s.dimsah,
|
|
|
|
|
|
s.dimse1,
|
|
|
|
|
|
s.dimse2,
|
|
|
|
|
|
s.dimsd1,
|
|
|
|
|
|
s.dimsd2,
|
|
|
|
|
|
(s.dimdle * dim_scale) as f32,
|
|
|
|
|
|
(s.dimfxl * dim_scale) as f32,
|
|
|
|
|
|
s.dimfxlon,
|
|
|
|
|
|
s.dimsoxd,
|
|
|
|
|
|
(s.dimcen * dim_scale) as f32,
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
.unwrap_or((
|
|
|
|
|
|
0.18, 0.0, 0.0, 2.5, 0.0, false, false, false, false, false, 0.0, 1.0, false, false,
|
|
|
|
|
|
0.09,
|
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
|
|
// Arrow selection precedence:
|
|
|
|
|
|
// 1. DIMTSZ>0 → oblique tick (overrides DIMBLK*).
|
|
|
|
|
|
// 2. DIMSAH false → DIMBLK on both ends.
|
|
|
|
|
|
// 3. DIMSAH true → DIMBLK1 (first end), DIMBLK2 (second end).
|
|
|
|
|
|
// Unknown / NULL block handles fall back to ClosedFilled.
|
|
|
|
|
|
let dimasz = (dimasz_raw as f32).max(0.001);
|
|
|
|
|
|
let (arrow1, arrow2) = if dimtsz_raw > 1e-9 {
|
|
|
|
|
|
let t = ArrowKind::Tick {
|
|
|
|
|
|
size: (dimtsz_raw as f32).max(0.001),
|
|
|
|
|
|
};
|
|
|
|
|
|
(t, t)
|
|
|
|
|
|
} else if let Some(s) = style {
|
|
|
|
|
|
if dimsah {
|
|
|
|
|
|
(
|
|
|
|
|
|
arrow_from_block(document, s.dimblk1, dimasz),
|
|
|
|
|
|
arrow_from_block(document, s.dimblk2, dimasz),
|
|
|
|
|
|
)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let a = arrow_from_block(document, s.dimblk, dimasz);
|
|
|
|
|
|
(a, a)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let a = ArrowKind::Triangle {
|
|
|
|
|
|
size: dimasz,
|
|
|
|
|
|
filled: true,
|
|
|
|
|
|
size_mul: 1.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
(a, a)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-06-14 10:09:28 +03:00
|
|
|
|
// Text box (local space) so the dim line can be broken where the text
|
|
|
|
|
|
// crosses it — lets a DIMTFILL background sit over the line. The renderer
|
|
|
|
|
|
// draws 2D fills under all wires, so the line is gapped rather than masked.
|
|
|
|
|
|
let dimgap_local = style.map(|s| (s.dimgap * dim_scale) as f32).unwrap_or(0.09);
|
|
|
|
|
|
let text_break = {
|
feat(commands): expand standard CAD command coverage
Integrate a large batch of standard CAD commands and fix several
name/behaviour mismatches, with no product references in code.
3D solids (reusing the truck B-rep pipeline):
- SLICE, INTERFERE, PRESSPULL, POLYSOLID, PYRAMID, SECTION
- 3DROTATE, 3DMIRROR, 3DALIGN (cached-solid transforms)
Annotation / text:
- ARCTEXT, SPLINEFIT (Catmull-Rom to Bezier), DIMJOGGED, OBJECTSCALE
(XData markers honoured by the existing geometry-gen / tessellator)
Blocks / misc:
- NCOPY (block-content extraction via explode_entity)
- OPTIONS, SYNCPVIEWPORTS, THICKEN, plus earlier toggles, aliases,
system variables, and layer/style/view commands
POINTCLOUD/RECAP and UNDERLAYLAYERS/UOSNAP are intentionally left out:
they require a proprietary binary / PDF-layer parser, a GPU point
renderer, and real test files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 08:23:58 +03:00
|
|
|
|
let tp = vec3_local(dimension_text_pos_f64(dim, style, dim_txt, dim_scale));
|
2026-06-14 10:09:28 +03:00
|
|
|
|
let tw = dimension_text_value(dim, style)
|
|
|
|
|
|
.map(|t| t.chars().count() as f32 * dim_txt as f32 * 0.6)
|
|
|
|
|
|
.unwrap_or(0.0);
|
|
|
|
|
|
if tw > 0.0 {
|
2026-06-18 21:09:56 +03:00
|
|
|
|
// Vertical threshold is the bare text half-height (no DIMGAP): the
|
|
|
|
|
|
// line only breaks when it actually passes under the glyphs. Text
|
|
|
|
|
|
// placed above/below the line (DIMTAD 1/4) sits exactly
|
|
|
|
|
|
// `text_half + DIMGAP` away, so excluding the gap here keeps it
|
|
|
|
|
|
// strictly outside and the line continuous — otherwise the two
|
|
|
|
|
|
// terms cancel at the same scaled value and the gap flickers with
|
|
|
|
|
|
// DIMGAP/DIMSCALE. The horizontal half-width keeps DIMGAP so a
|
|
|
|
|
|
// genuine break still clears the text comfortably. (#94)
|
|
|
|
|
|
Some((tp, tw * 0.5 + dimgap_local, dim_txt as f32 * 0.5))
|
2026-06-14 10:09:28 +03:00
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
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 mut geom = dimension_geometry(
|
|
|
|
|
|
dim,
|
|
|
|
|
|
&arrow1,
|
|
|
|
|
|
&arrow2,
|
|
|
|
|
|
DimLineParams {
|
|
|
|
|
|
dimexo,
|
|
|
|
|
|
dimexe,
|
|
|
|
|
|
dimdle,
|
|
|
|
|
|
dimfxl,
|
|
|
|
|
|
dimfxlon,
|
|
|
|
|
|
dimsoxd,
|
|
|
|
|
|
dimcen,
|
|
|
|
|
|
ticks: dimtsz_raw > 1e-9,
|
2026-06-14 02:13:00 +03:00
|
|
|
|
arrow_len: dimasz,
|
2026-06-14 10:09:28 +03:00
|
|
|
|
text_break,
|
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
|
|
|
|
},
|
|
|
|
|
|
SuppressFlags {
|
|
|
|
|
|
ext1: dimse1,
|
|
|
|
|
|
ext2: dimse2,
|
|
|
|
|
|
dim1: dimsd1,
|
|
|
|
|
|
dim2: dimsd2,
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// DIMTMOVE = 1: when the saved text_middle_point sits far from the
|
|
|
|
|
|
// dim-line anchor, draw a short leader connecting them. (=0 anchors text
|
|
|
|
|
|
// to the dim line — no leader; =2 frees text without a leader.)
|
|
|
|
|
|
if let Some(s) = style {
|
|
|
|
|
|
if s.dimtmove == 1 {
|
refactor: remove dead world_offset plumbing (RTE migration is complete)
Now that geometry reaches the GPU/CPU as absolute coordinates via the
double-single relative-to-eye path, world_offset is always [0,0,0]. Strip
the parameter that was threaded through the whole tessellation / block-
expansion / fallback / camera-decode chain and the ExpandCtx field that
carried it — ~340 references across 20 files, all subtracting zero.
- truck_tess: to_local / to_local_low / tessellate_* drop the offset arg;
to_local is now a pure double-single split.
- tessellate: offset_to_ds → points_to_ds (pure DS split); tessellate(),
tessellate_entity(), fallback_geometry(), solid_wire_fallback(),
entity_aabb(), expand_insert(), expand_block_meshes(), the dimension
helpers (vec3_local, dimension_snap_pts, …), text_support, leader,
multileader, image_model, xclip and camera_from_view all lose the param.
- block_cache: ExpandCtx loses its world_offset field.
- Remove the now-dead offset_snap_pts and the unused set_grid_snap.
Behaviour is unchanged (the offset was zero everywhere); the compiler
verifies every call site.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 18:35:02 +03:00
|
|
|
|
if let Some((anchor, txt)) = dimtmove_leader_endpoints(dim) {
|
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 gap = dim_txt as f32 * 0.5;
|
|
|
|
|
|
if (txt - anchor).length() > gap * 2.0 {
|
|
|
|
|
|
add_segment(&mut geom.dim_lines, anchor, txt);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-14 02:13:00 +03:00
|
|
|
|
// Fit: text that doesn't fit between the extension lines is slid outside
|
|
|
|
|
|
// in the text-placement pass (unless DIMTIX), and arrowheads that don't
|
|
|
|
|
|
// fit are flipped outside in append_linear_dimension. DIMTOFL (force the
|
|
|
|
|
|
// dim line inside) is already satisfied for linear/aligned, whose dim
|
|
|
|
|
|
// line is always drawn between the extension points. DIMATFIT's exact
|
|
|
|
|
|
// mode ordering (arrows-first vs text-first) and DIMUPT (reposition on
|
|
|
|
|
|
// create) aren't differentiated yet — read here for round-trip.
|
2026-06-14 02:03:05 +03:00
|
|
|
|
let _ = (s.dimtofl, s.dimatfit, s.dimupt);
|
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
|
|
|
|
// DIMTXTDIRECTION (RTL) needs per-instance text mirroring on the Text
|
|
|
|
|
|
// entity, which the current text struct can't carry. Tracked: read
|
|
|
|
|
|
// and ignore so the file round-trips on save.
|
|
|
|
|
|
let _ = s.dimtxtdirection;
|
|
|
|
|
|
// DIMARCSYM only applies to arc-length dims; DIMJOGANG only to
|
|
|
|
|
|
// jogged-radius dims. We don't ship those Dimension variants yet,
|
|
|
|
|
|
// so the values are read for round-trip but not drawn.
|
|
|
|
|
|
let _ = (s.dimarcsym, s.dimjogang);
|
|
|
|
|
|
// DIMUNIT is the obsolete pre-R2000 linear unit format; DIMLUNIT
|
|
|
|
|
|
// supersedes it. Read but not honoured.
|
|
|
|
|
|
let _ = s.dimunit;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Dimension entity fields that the render path doesn't yet use but are
|
|
|
|
|
|
// preserved on save:
|
|
|
|
|
|
// - base.insertion_point: legacy anchor reference; render uses
|
|
|
|
|
|
// text_middle_point + dim-line geometry instead.
|
|
|
|
|
|
// - base.block_name: AutoCAD-style "*D..." anonymous block name for
|
|
|
|
|
|
// the dim graphics — we re-tessellate so don't need it.
|
|
|
|
|
|
// - base.version: DXF format marker (metadata only).
|
|
|
|
|
|
let _ = (
|
|
|
|
|
|
dim.base().insertion_point,
|
|
|
|
|
|
&dim.base().block_name,
|
|
|
|
|
|
dim.base().version,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Per-spec colours: DIMCLRD (dim/arrows), DIMCLRE (ext), DIMCLRT (text).
|
2026-07-13 17:31:09 +03:00
|
|
|
|
// 0=ByBlock and 256=ByLayer fall through to entity_color. DIMCLRD also
|
|
|
|
|
|
// honours a per-object ACAD_DSTYLE override (code 176) so an edited
|
|
|
|
|
|
// dim-line colour renders even without touching the style.
|
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 dim_color = if selected {
|
|
|
|
|
|
WireModel::SELECTED
|
|
|
|
|
|
} else {
|
2026-07-13 17:31:09 +03:00
|
|
|
|
let dim_clr = crate::entities::dim_override::int(
|
|
|
|
|
|
&dim.base().common.extended_data,
|
|
|
|
|
|
crate::entities::dim_override::DIMCLRD,
|
|
|
|
|
|
)
|
|
|
|
|
|
.unwrap_or_else(|| style.map(|s| s.dimclrd).unwrap_or(0));
|
|
|
|
|
|
resolve_dim_color(dim_clr, entity_color)
|
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 ext_color = if selected {
|
|
|
|
|
|
WireModel::SELECTED
|
|
|
|
|
|
} else {
|
|
|
|
|
|
resolve_dim_color(style.map(|s| s.dimclre).unwrap_or(0), entity_color)
|
|
|
|
|
|
};
|
|
|
|
|
|
let text_color = if selected {
|
|
|
|
|
|
entity_color // text wire color set by inner tessellate; keep entity tint
|
|
|
|
|
|
} else {
|
|
|
|
|
|
resolve_dim_color(style.map(|s| s.dimclrt).unwrap_or(0), entity_color)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
refactor: remove dead world_offset plumbing (RTE migration is complete)
Now that geometry reaches the GPU/CPU as absolute coordinates via the
double-single relative-to-eye path, world_offset is always [0,0,0]. Strip
the parameter that was threaded through the whole tessellation / block-
expansion / fallback / camera-decode chain and the ExpandCtx field that
carried it — ~340 references across 20 files, all subtracting zero.
- truck_tess: to_local / to_local_low / tessellate_* drop the offset arg;
to_local is now a pure double-single split.
- tessellate: offset_to_ds → points_to_ds (pure DS split); tessellate(),
tessellate_entity(), fallback_geometry(), solid_wire_fallback(),
entity_aabb(), expand_insert(), expand_block_meshes(), the dimension
helpers (vec3_local, dimension_snap_pts, …), text_support, leader,
multileader, image_model, xclip and camera_from_view all lose the param.
- block_cache: ExpandCtx loses its world_offset field.
- Remove the now-dead offset_snap_pts and the unused set_grid_snap.
Behaviour is unchanged (the offset was zero everywhere); the compiler
verifies every call site.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 18:35:02 +03:00
|
|
|
|
let snap_pts = dimension_snap_pts(dim);
|
2026-06-23 22:23:56 +03:00
|
|
|
|
let key_vertices: Vec<[f64; 3]> = geom
|
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
|
|
|
|
.dim_lines
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.chain(geom.ext_lines.iter())
|
|
|
|
|
|
.copied()
|
|
|
|
|
|
.filter(|p| !(p[0].is_nan() || p[1].is_nan() || p[2].is_nan()))
|
2026-06-23 22:23:56 +03:00
|
|
|
|
.map(|[x, y, z]| [x as f64, y as f64, z as f64])
|
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
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
|
|
// DIMLWD (dim line + arrows) and DIMLWE (extension lines). Negative
|
|
|
|
|
|
// codes fall through to the entity's own resolved weight.
|
2026-06-11 00:04:37 +03:00
|
|
|
|
let lw_dim = resolve_dim_lineweight_px(style.map(|s| s.dimlwd).unwrap_or(-2), line_weight_px);
|
|
|
|
|
|
let lw_ext = resolve_dim_lineweight_px(style.map(|s| s.dimlwe).unwrap_or(-2), line_weight_px);
|
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
|
|
|
|
|
|
|
|
|
|
// DIMLTEX (dim line) / DIMLTEX1 (ext1) / DIMLTEX2 (ext2) — linetype
|
|
|
|
|
|
// handles → pattern. Looked up in document.line_types by handle.
|
|
|
|
|
|
let lt_scale = document.header.linetype_scale as f32 * dim.base().common.linetype_scale as f32;
|
|
|
|
|
|
let (dim_pat_len, dim_pat) = style
|
|
|
|
|
|
.map(|s| resolve_pattern_by_handle(document, s.dimltex_handle, lt_scale))
|
|
|
|
|
|
.unwrap_or((0.0, [0.0; 8]));
|
|
|
|
|
|
let (ext1_pat_len, ext1_pat) = style
|
|
|
|
|
|
.map(|s| resolve_pattern_by_handle(document, s.dimltex1_handle, lt_scale))
|
|
|
|
|
|
.unwrap_or((0.0, [0.0; 8]));
|
|
|
|
|
|
let (ext2_pat_len, ext2_pat) = style
|
|
|
|
|
|
.map(|s| resolve_pattern_by_handle(document, s.dimltex2_handle, lt_scale))
|
|
|
|
|
|
.unwrap_or((0.0, [0.0; 8]));
|
|
|
|
|
|
|
|
|
|
|
|
let mut wires = Vec::new();
|
|
|
|
|
|
|
|
|
|
|
|
if !geom.ext_lines.is_empty() {
|
|
|
|
|
|
// If ext1 and ext2 have different linetypes, split into two wires so
|
|
|
|
|
|
// each can carry its own pattern. Otherwise emit as a single wire.
|
|
|
|
|
|
let split = ext1_pat_len != ext2_pat_len || ext1_pat != ext2_pat;
|
|
|
|
|
|
if split {
|
|
|
|
|
|
let (ext1, ext2) = split_ext_lines(&geom.ext_lines);
|
|
|
|
|
|
if !ext1.is_empty() {
|
|
|
|
|
|
wires.push(WireModel {
|
fix(pick): hit-test lineweight width and thickness walls
Selection tested a flat 8 px radius around each wire's centreline and
nothing else, so two things the user can see were not pickable.
A wire renders as a band `line_weight_px` wide, so pick now widens to that
rendered half-width via `pick_tolerance_px`, mirroring the wire shader's
`select(0.5, half_width, lwdisplay_enable)`. Behaviour is unchanged for
every standard weight (the widest, 2.11 mm, renders 7.97 px half-width —
just under the 8 px threshold); this keeps the two sides from drifting
apart if the display boost in `lineweight_to_px` ever changes.
Entities extruded by a DXF thickness draw their wall as four edges with
nothing in between, so the interior was a hole the cursor fell through.
WireModel gains `pick_tris`: triangles hit-testing treats as solid and the
renderer never uploads. Kept apart from `fill_tris` because that channel
reaches the GPU, and a wall that rendered shaded would change how every
thickness drawing looks. Ranked below `fill_tris` — those are drawn, these
are not, so where they overlap the drawn face is what the user means.
Walls are dropped when a wire is projected into a floating viewport:
`points` are reprojected and clipped into paper coords there, so cloned
model-space triangles would hit-test at a model-scale offset. Extruded
entities stay selectable by their edges, which do get projected.
The wall loop runs on every hover that misses everything else, so it
rejects on the wire's AABB first — an extruded circle is ~128 triangles
and a drawing full of thickness would otherwise project every wall on
each mouse move.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 14:58:55 +03:00
|
|
|
|
pick_tris: Vec::new(),
|
|
|
|
|
|
pick_tris_low: Vec::new(),
|
2026-07-17 01:49:18 +03:00
|
|
|
|
dash_from_start: false,
|
2026-07-17 02:59:11 +03:00
|
|
|
|
dash_align_end: None,
|
refactor(text): per-entity SDF text — all text kinds, selection/hover highlight
Rearchitect SDF text from a document-wide collector to per-entity
production during tessellation, so each entity owns its glyph quads and
composite/nested text is covered for free (no re-explosion, no per-frame
document walk).
- WireModel gains `text_verts`: glyph quads ride on the wire, so they are
cached by the tess memo, cloned on hit, and transformed by the block
expand loop exactly like `points`.
- Text arm (tessellate.rs) builds the quads when SDF is on and suppresses
the strokes; render gathers text_verts from the viewport wire set
(`gather_text_verts`, cached on wire content id) — the old collector is
removed. Covers model, dimension, table, block-internal and paper-sheet
text uniformly.
- MLEADER text (lays out via layout_mtext, not the Text arm) gets its own
SDF branch driven by the per-run GlyphRun.
- Pick box comes from the rendered glyph quads (true text extent), not
entity_aabb which mis-places the box for MTEXT; composite paths no longer
clobber the empty text wire's tight AABB (dim pick box hugs the text).
- Selection / hover highlight via a text-highlight overlay (upload_text_
highlight, keyed on selection_generation) mirroring the selected-wire
xray overlay; base glyphs stay neutral so a deselect leaves no stale tint.
- Debug: env OCS_TEXT_BOX draws each run's glyph-bounds rectangle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 17:01:36 +03:00
|
|
|
|
text_verts: Vec::new(),
|
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
|
|
|
|
name: name.clone(),
|
|
|
|
|
|
points: ext1,
|
2026-06-23 17:08:54 +03:00
|
|
|
|
points_low: Vec::new(),
|
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
|
|
|
|
color: ext_color,
|
|
|
|
|
|
selected,
|
|
|
|
|
|
aci: 0,
|
|
|
|
|
|
pattern_length: ext1_pat_len,
|
|
|
|
|
|
pattern: ext1_pat,
|
|
|
|
|
|
line_weight_px: lw_ext,
|
|
|
|
|
|
snap_pts: vec![],
|
|
|
|
|
|
tangent_geoms: vec![],
|
|
|
|
|
|
key_vertices: vec![],
|
|
|
|
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
|
|
|
|
|
plinegen: true,
|
|
|
|
|
|
vp_scissor: None,
|
|
|
|
|
|
fill_tris: vec![],
|
2026-06-23 18:03:40 +03:00
|
|
|
|
fill_tris_low: Vec::new(),
|
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
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
if !ext2.is_empty() {
|
|
|
|
|
|
wires.push(WireModel {
|
fix(pick): hit-test lineweight width and thickness walls
Selection tested a flat 8 px radius around each wire's centreline and
nothing else, so two things the user can see were not pickable.
A wire renders as a band `line_weight_px` wide, so pick now widens to that
rendered half-width via `pick_tolerance_px`, mirroring the wire shader's
`select(0.5, half_width, lwdisplay_enable)`. Behaviour is unchanged for
every standard weight (the widest, 2.11 mm, renders 7.97 px half-width —
just under the 8 px threshold); this keeps the two sides from drifting
apart if the display boost in `lineweight_to_px` ever changes.
Entities extruded by a DXF thickness draw their wall as four edges with
nothing in between, so the interior was a hole the cursor fell through.
WireModel gains `pick_tris`: triangles hit-testing treats as solid and the
renderer never uploads. Kept apart from `fill_tris` because that channel
reaches the GPU, and a wall that rendered shaded would change how every
thickness drawing looks. Ranked below `fill_tris` — those are drawn, these
are not, so where they overlap the drawn face is what the user means.
Walls are dropped when a wire is projected into a floating viewport:
`points` are reprojected and clipped into paper coords there, so cloned
model-space triangles would hit-test at a model-scale offset. Extruded
entities stay selectable by their edges, which do get projected.
The wall loop runs on every hover that misses everything else, so it
rejects on the wire's AABB first — an extruded circle is ~128 triangles
and a drawing full of thickness would otherwise project every wall on
each mouse move.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 14:58:55 +03:00
|
|
|
|
pick_tris: Vec::new(),
|
|
|
|
|
|
pick_tris_low: Vec::new(),
|
2026-07-17 01:49:18 +03:00
|
|
|
|
dash_from_start: false,
|
2026-07-17 02:59:11 +03:00
|
|
|
|
dash_align_end: None,
|
refactor(text): per-entity SDF text — all text kinds, selection/hover highlight
Rearchitect SDF text from a document-wide collector to per-entity
production during tessellation, so each entity owns its glyph quads and
composite/nested text is covered for free (no re-explosion, no per-frame
document walk).
- WireModel gains `text_verts`: glyph quads ride on the wire, so they are
cached by the tess memo, cloned on hit, and transformed by the block
expand loop exactly like `points`.
- Text arm (tessellate.rs) builds the quads when SDF is on and suppresses
the strokes; render gathers text_verts from the viewport wire set
(`gather_text_verts`, cached on wire content id) — the old collector is
removed. Covers model, dimension, table, block-internal and paper-sheet
text uniformly.
- MLEADER text (lays out via layout_mtext, not the Text arm) gets its own
SDF branch driven by the per-run GlyphRun.
- Pick box comes from the rendered glyph quads (true text extent), not
entity_aabb which mis-places the box for MTEXT; composite paths no longer
clobber the empty text wire's tight AABB (dim pick box hugs the text).
- Selection / hover highlight via a text-highlight overlay (upload_text_
highlight, keyed on selection_generation) mirroring the selected-wire
xray overlay; base glyphs stay neutral so a deselect leaves no stale tint.
- Debug: env OCS_TEXT_BOX draws each run's glyph-bounds rectangle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 17:01:36 +03:00
|
|
|
|
text_verts: Vec::new(),
|
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
|
|
|
|
name: name.clone(),
|
|
|
|
|
|
points: ext2,
|
2026-06-23 17:08:54 +03:00
|
|
|
|
points_low: Vec::new(),
|
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
|
|
|
|
color: ext_color,
|
|
|
|
|
|
selected,
|
|
|
|
|
|
aci: 0,
|
|
|
|
|
|
pattern_length: ext2_pat_len,
|
|
|
|
|
|
pattern: ext2_pat,
|
|
|
|
|
|
line_weight_px: lw_ext,
|
|
|
|
|
|
snap_pts: vec![],
|
|
|
|
|
|
tangent_geoms: vec![],
|
|
|
|
|
|
key_vertices: vec![],
|
|
|
|
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
|
|
|
|
|
plinegen: true,
|
|
|
|
|
|
vp_scissor: None,
|
|
|
|
|
|
fill_tris: vec![],
|
2026-06-23 18:03:40 +03:00
|
|
|
|
fill_tris_low: Vec::new(),
|
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
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
wires.push(WireModel {
|
fix(pick): hit-test lineweight width and thickness walls
Selection tested a flat 8 px radius around each wire's centreline and
nothing else, so two things the user can see were not pickable.
A wire renders as a band `line_weight_px` wide, so pick now widens to that
rendered half-width via `pick_tolerance_px`, mirroring the wire shader's
`select(0.5, half_width, lwdisplay_enable)`. Behaviour is unchanged for
every standard weight (the widest, 2.11 mm, renders 7.97 px half-width —
just under the 8 px threshold); this keeps the two sides from drifting
apart if the display boost in `lineweight_to_px` ever changes.
Entities extruded by a DXF thickness draw their wall as four edges with
nothing in between, so the interior was a hole the cursor fell through.
WireModel gains `pick_tris`: triangles hit-testing treats as solid and the
renderer never uploads. Kept apart from `fill_tris` because that channel
reaches the GPU, and a wall that rendered shaded would change how every
thickness drawing looks. Ranked below `fill_tris` — those are drawn, these
are not, so where they overlap the drawn face is what the user means.
Walls are dropped when a wire is projected into a floating viewport:
`points` are reprojected and clipped into paper coords there, so cloned
model-space triangles would hit-test at a model-scale offset. Extruded
entities stay selectable by their edges, which do get projected.
The wall loop runs on every hover that misses everything else, so it
rejects on the wire's AABB first — an extruded circle is ~128 triangles
and a drawing full of thickness would otherwise project every wall on
each mouse move.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 14:58:55 +03:00
|
|
|
|
pick_tris: Vec::new(),
|
|
|
|
|
|
pick_tris_low: Vec::new(),
|
2026-07-17 01:49:18 +03:00
|
|
|
|
dash_from_start: false,
|
2026-07-17 02:59:11 +03:00
|
|
|
|
dash_align_end: None,
|
refactor(text): per-entity SDF text — all text kinds, selection/hover highlight
Rearchitect SDF text from a document-wide collector to per-entity
production during tessellation, so each entity owns its glyph quads and
composite/nested text is covered for free (no re-explosion, no per-frame
document walk).
- WireModel gains `text_verts`: glyph quads ride on the wire, so they are
cached by the tess memo, cloned on hit, and transformed by the block
expand loop exactly like `points`.
- Text arm (tessellate.rs) builds the quads when SDF is on and suppresses
the strokes; render gathers text_verts from the viewport wire set
(`gather_text_verts`, cached on wire content id) — the old collector is
removed. Covers model, dimension, table, block-internal and paper-sheet
text uniformly.
- MLEADER text (lays out via layout_mtext, not the Text arm) gets its own
SDF branch driven by the per-run GlyphRun.
- Pick box comes from the rendered glyph quads (true text extent), not
entity_aabb which mis-places the box for MTEXT; composite paths no longer
clobber the empty text wire's tight AABB (dim pick box hugs the text).
- Selection / hover highlight via a text-highlight overlay (upload_text_
highlight, keyed on selection_generation) mirroring the selected-wire
xray overlay; base glyphs stay neutral so a deselect leaves no stale tint.
- Debug: env OCS_TEXT_BOX draws each run's glyph-bounds rectangle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 17:01:36 +03:00
|
|
|
|
text_verts: Vec::new(),
|
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
|
|
|
|
name: name.clone(),
|
|
|
|
|
|
points: geom.ext_lines,
|
2026-06-23 17:08:54 +03:00
|
|
|
|
points_low: Vec::new(),
|
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
|
|
|
|
color: ext_color,
|
|
|
|
|
|
selected,
|
|
|
|
|
|
aci: 0,
|
|
|
|
|
|
pattern_length: ext1_pat_len,
|
|
|
|
|
|
pattern: ext1_pat,
|
|
|
|
|
|
line_weight_px: lw_ext,
|
|
|
|
|
|
snap_pts: vec![],
|
|
|
|
|
|
tangent_geoms: vec![],
|
|
|
|
|
|
key_vertices: vec![],
|
|
|
|
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
|
|
|
|
|
plinegen: true,
|
|
|
|
|
|
vp_scissor: None,
|
|
|
|
|
|
fill_tris: vec![],
|
2026-06-23 18:03:40 +03:00
|
|
|
|
fill_tris_low: Vec::new(),
|
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
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
wires.push(WireModel {
|
fix(pick): hit-test lineweight width and thickness walls
Selection tested a flat 8 px radius around each wire's centreline and
nothing else, so two things the user can see were not pickable.
A wire renders as a band `line_weight_px` wide, so pick now widens to that
rendered half-width via `pick_tolerance_px`, mirroring the wire shader's
`select(0.5, half_width, lwdisplay_enable)`. Behaviour is unchanged for
every standard weight (the widest, 2.11 mm, renders 7.97 px half-width —
just under the 8 px threshold); this keeps the two sides from drifting
apart if the display boost in `lineweight_to_px` ever changes.
Entities extruded by a DXF thickness draw their wall as four edges with
nothing in between, so the interior was a hole the cursor fell through.
WireModel gains `pick_tris`: triangles hit-testing treats as solid and the
renderer never uploads. Kept apart from `fill_tris` because that channel
reaches the GPU, and a wall that rendered shaded would change how every
thickness drawing looks. Ranked below `fill_tris` — those are drawn, these
are not, so where they overlap the drawn face is what the user means.
Walls are dropped when a wire is projected into a floating viewport:
`points` are reprojected and clipped into paper coords there, so cloned
model-space triangles would hit-test at a model-scale offset. Extruded
entities stay selectable by their edges, which do get projected.
The wall loop runs on every hover that misses everything else, so it
rejects on the wire's AABB first — an extruded circle is ~128 triangles
and a drawing full of thickness would otherwise project every wall on
each mouse move.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 14:58:55 +03:00
|
|
|
|
pick_tris: Vec::new(),
|
|
|
|
|
|
pick_tris_low: Vec::new(),
|
2026-07-17 01:49:18 +03:00
|
|
|
|
dash_from_start: false,
|
2026-07-17 02:59:11 +03:00
|
|
|
|
dash_align_end: None,
|
refactor(text): per-entity SDF text — all text kinds, selection/hover highlight
Rearchitect SDF text from a document-wide collector to per-entity
production during tessellation, so each entity owns its glyph quads and
composite/nested text is covered for free (no re-explosion, no per-frame
document walk).
- WireModel gains `text_verts`: glyph quads ride on the wire, so they are
cached by the tess memo, cloned on hit, and transformed by the block
expand loop exactly like `points`.
- Text arm (tessellate.rs) builds the quads when SDF is on and suppresses
the strokes; render gathers text_verts from the viewport wire set
(`gather_text_verts`, cached on wire content id) — the old collector is
removed. Covers model, dimension, table, block-internal and paper-sheet
text uniformly.
- MLEADER text (lays out via layout_mtext, not the Text arm) gets its own
SDF branch driven by the per-run GlyphRun.
- Pick box comes from the rendered glyph quads (true text extent), not
entity_aabb which mis-places the box for MTEXT; composite paths no longer
clobber the empty text wire's tight AABB (dim pick box hugs the text).
- Selection / hover highlight via a text-highlight overlay (upload_text_
highlight, keyed on selection_generation) mirroring the selected-wire
xray overlay; base glyphs stay neutral so a deselect leaves no stale tint.
- Debug: env OCS_TEXT_BOX draws each run's glyph-bounds rectangle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 17:01:36 +03:00
|
|
|
|
text_verts: Vec::new(),
|
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
|
|
|
|
name: name.clone(),
|
|
|
|
|
|
points: geom.dim_lines,
|
2026-06-23 17:08:54 +03:00
|
|
|
|
points_low: Vec::new(),
|
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
|
|
|
|
color: dim_color,
|
|
|
|
|
|
selected,
|
|
|
|
|
|
aci: 0,
|
|
|
|
|
|
pattern_length: dim_pat_len,
|
|
|
|
|
|
pattern: dim_pat,
|
|
|
|
|
|
line_weight_px: lw_dim,
|
|
|
|
|
|
snap_pts,
|
|
|
|
|
|
tangent_geoms: vec![],
|
|
|
|
|
|
key_vertices,
|
|
|
|
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
|
|
|
|
|
plinegen: true,
|
|
|
|
|
|
vp_scissor: None,
|
|
|
|
|
|
fill_tris: geom.arrow_fill,
|
2026-06-28 21:57:51 +03:00
|
|
|
|
// fill_tris_low intentionally empty: this fill renders on the top-level
|
|
|
|
|
|
// path, where consumers (face3d_gpu, xclip) treat a short low half as
|
|
|
|
|
|
// all-zero, so it draws at f32 precision (sub-metre error at UTM scale)
|
|
|
|
|
|
// — not a crash. Follow-up: double-single-split via points_to_ds to
|
|
|
|
|
|
// match emit_wire's paired fill path.
|
2026-06-23 18:03:40 +03:00
|
|
|
|
fill_tris_low: Vec::new(),
|
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-06-14 09:37:01 +03:00
|
|
|
|
// DIMTFILL: 0=none, 1=drawing background (mask), 2=DIMTFILLCLR.
|
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
|
|
|
|
if let Some(s) = style {
|
2026-06-14 09:37:01 +03:00
|
|
|
|
if s.dimtfill == 1 || s.dimtfill == 2 {
|
refactor: remove dead world_offset plumbing (RTE migration is complete)
Now that geometry reaches the GPU/CPU as absolute coordinates via the
double-single relative-to-eye path, world_offset is always [0,0,0]. Strip
the parameter that was threaded through the whole tessellation / block-
expansion / fallback / camera-decode chain and the ExpandCtx field that
carried it — ~340 references across 20 files, all subtracting zero.
- truck_tess: to_local / to_local_low / tessellate_* drop the offset arg;
to_local is now a pure double-single split.
- tessellate: offset_to_ds → points_to_ds (pure DS split); tessellate(),
tessellate_entity(), fallback_geometry(), solid_wire_fallback(),
entity_aabb(), expand_insert(), expand_block_meshes(), the dimension
helpers (vec3_local, dimension_snap_pts, …), text_support, leader,
multileader, image_model, xclip and camera_from_view all lose the param.
- block_cache: ExpandCtx loses its world_offset field.
- Remove the now-dead offset_snap_pts and the unused set_grid_snap.
Behaviour is unchanged (the offset was zero everywhere); the compiler
verifies every call site.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 18:35:02 +03:00
|
|
|
|
if let Some(rect) = text_fill_rect(dim, style, dim_txt, dim_scale) {
|
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 fill_color = if selected {
|
|
|
|
|
|
WireModel::SELECTED
|
2026-06-14 09:37:01 +03:00
|
|
|
|
} else if s.dimtfill == 1 {
|
|
|
|
|
|
// Drawing-background fill: mask out geometry behind the text.
|
|
|
|
|
|
bg_color
|
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
|
|
|
|
} else {
|
|
|
|
|
|
let c = AcadColor::from_index(s.dimtfillclr);
|
|
|
|
|
|
aci_to_rgba(&c)
|
|
|
|
|
|
};
|
|
|
|
|
|
wires.push(WireModel {
|
fix(pick): hit-test lineweight width and thickness walls
Selection tested a flat 8 px radius around each wire's centreline and
nothing else, so two things the user can see were not pickable.
A wire renders as a band `line_weight_px` wide, so pick now widens to that
rendered half-width via `pick_tolerance_px`, mirroring the wire shader's
`select(0.5, half_width, lwdisplay_enable)`. Behaviour is unchanged for
every standard weight (the widest, 2.11 mm, renders 7.97 px half-width —
just under the 8 px threshold); this keeps the two sides from drifting
apart if the display boost in `lineweight_to_px` ever changes.
Entities extruded by a DXF thickness draw their wall as four edges with
nothing in between, so the interior was a hole the cursor fell through.
WireModel gains `pick_tris`: triangles hit-testing treats as solid and the
renderer never uploads. Kept apart from `fill_tris` because that channel
reaches the GPU, and a wall that rendered shaded would change how every
thickness drawing looks. Ranked below `fill_tris` — those are drawn, these
are not, so where they overlap the drawn face is what the user means.
Walls are dropped when a wire is projected into a floating viewport:
`points` are reprojected and clipped into paper coords there, so cloned
model-space triangles would hit-test at a model-scale offset. Extruded
entities stay selectable by their edges, which do get projected.
The wall loop runs on every hover that misses everything else, so it
rejects on the wire's AABB first — an extruded circle is ~128 triangles
and a drawing full of thickness would otherwise project every wall on
each mouse move.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 14:58:55 +03:00
|
|
|
|
pick_tris: Vec::new(),
|
|
|
|
|
|
pick_tris_low: Vec::new(),
|
2026-07-17 01:49:18 +03:00
|
|
|
|
dash_from_start: false,
|
2026-07-17 02:59:11 +03:00
|
|
|
|
dash_align_end: None,
|
refactor(text): per-entity SDF text — all text kinds, selection/hover highlight
Rearchitect SDF text from a document-wide collector to per-entity
production during tessellation, so each entity owns its glyph quads and
composite/nested text is covered for free (no re-explosion, no per-frame
document walk).
- WireModel gains `text_verts`: glyph quads ride on the wire, so they are
cached by the tess memo, cloned on hit, and transformed by the block
expand loop exactly like `points`.
- Text arm (tessellate.rs) builds the quads when SDF is on and suppresses
the strokes; render gathers text_verts from the viewport wire set
(`gather_text_verts`, cached on wire content id) — the old collector is
removed. Covers model, dimension, table, block-internal and paper-sheet
text uniformly.
- MLEADER text (lays out via layout_mtext, not the Text arm) gets its own
SDF branch driven by the per-run GlyphRun.
- Pick box comes from the rendered glyph quads (true text extent), not
entity_aabb which mis-places the box for MTEXT; composite paths no longer
clobber the empty text wire's tight AABB (dim pick box hugs the text).
- Selection / hover highlight via a text-highlight overlay (upload_text_
highlight, keyed on selection_generation) mirroring the selected-wire
xray overlay; base glyphs stay neutral so a deselect leaves no stale tint.
- Debug: env OCS_TEXT_BOX draws each run's glyph-bounds rectangle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 17:01:36 +03:00
|
|
|
|
text_verts: Vec::new(),
|
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
|
|
|
|
name: name.clone(),
|
|
|
|
|
|
points: vec![],
|
2026-06-23 17:08:54 +03:00
|
|
|
|
points_low: Vec::new(),
|
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
|
|
|
|
color: fill_color,
|
|
|
|
|
|
selected,
|
|
|
|
|
|
aci: 0,
|
|
|
|
|
|
pattern_length: 0.0,
|
|
|
|
|
|
pattern: [0.0; 8],
|
|
|
|
|
|
line_weight_px: 1.0,
|
|
|
|
|
|
snap_pts: vec![],
|
|
|
|
|
|
tangent_geoms: vec![],
|
|
|
|
|
|
key_vertices: vec![],
|
|
|
|
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
|
|
|
|
|
plinegen: true,
|
|
|
|
|
|
vp_scissor: None,
|
2026-06-28 21:57:51 +03:00
|
|
|
|
fill_tris: rect,
|
|
|
|
|
|
// fill_tris_low intentionally empty: this fill renders on the
|
|
|
|
|
|
// top-level path, where consumers (face3d_gpu, xclip) treat a
|
|
|
|
|
|
// short low half as all-zero, so it draws at f32 precision
|
|
|
|
|
|
// (sub-metre error at UTM scale) — not a crash. Follow-up:
|
|
|
|
|
|
// double-single-split via points_to_ds to match emit_wire.
|
|
|
|
|
|
fill_tris_low: Vec::new(),
|
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-06-17 05:32:31 +03:00
|
|
|
|
if let Some(synth_text_entity) = dimension_text_entity(dim, dim_txt, style, document, dim_scale)
|
|
|
|
|
|
{
|
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
|
|
|
|
// Tolerance Text rendered separately so DIMTFAC scales its height
|
|
|
|
|
|
// and DIMTOLJ aligns it vertically against the primary text.
|
|
|
|
|
|
let tol_entity = dimension_tolerance_entity(dim, style, &synth_text_entity, dim_txt);
|
|
|
|
|
|
// Route synthesised dim text through tessellate_entity so the
|
|
|
|
|
|
// baseline/greek/full LOD ladder applies (zoom-out behaviour
|
|
|
|
|
|
// matches top-level Text / MText). The text already has dim_scale
|
|
|
|
|
|
// baked into its height, so anno_scale stays 1.0.
|
|
|
|
|
|
let text_wires = crate::scene::tessellate_entity_dim_text(
|
|
|
|
|
|
document,
|
|
|
|
|
|
selected_set,
|
|
|
|
|
|
active_viewport,
|
|
|
|
|
|
bg_color,
|
|
|
|
|
|
1.0,
|
|
|
|
|
|
&synth_text_entity,
|
|
|
|
|
|
view_aabb,
|
|
|
|
|
|
world_per_pixel,
|
|
|
|
|
|
text_color,
|
|
|
|
|
|
);
|
|
|
|
|
|
for mut w in text_wires {
|
|
|
|
|
|
w.name = name.clone();
|
|
|
|
|
|
wires.push(w);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if let Some(tol_entity_e) = tol_entity {
|
|
|
|
|
|
let tol_wires = crate::scene::tessellate_entity_dim_text(
|
|
|
|
|
|
document,
|
|
|
|
|
|
selected_set,
|
|
|
|
|
|
active_viewport,
|
|
|
|
|
|
bg_color,
|
|
|
|
|
|
1.0,
|
|
|
|
|
|
&tol_entity_e,
|
|
|
|
|
|
view_aabb,
|
|
|
|
|
|
world_per_pixel,
|
|
|
|
|
|
text_color,
|
|
|
|
|
|
);
|
|
|
|
|
|
for mut w in tol_wires {
|
|
|
|
|
|
w.name = name.clone();
|
|
|
|
|
|
wires.push(w);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
wires
|
|
|
|
|
|
}
|
|
|
|
|
|
fn resolve_dim_color(idx: i16, fallback: [f32; 4]) -> [f32; 4] {
|
|
|
|
|
|
// DIMCLR* convention: 0 = BYBLOCK, 256 = BYLAYER → entity colour wins.
|
|
|
|
|
|
if idx == 0 || idx == 256 {
|
|
|
|
|
|
return fallback;
|
|
|
|
|
|
}
|
|
|
|
|
|
aci_to_rgba(&AcadColor::from_index(idx))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Resolve a DIMLWD / DIMLWE table value (the i16 lineweight code) into a
|
|
|
|
|
|
/// pixel width. -1 (ByLayer) / -2 (ByBlock) / -3 (Default) fall through to
|
|
|
|
|
|
/// the entity's already-resolved width.
|
|
|
|
|
|
fn resolve_dim_lineweight_px(code: i16, fallback_px: f32) -> f32 {
|
|
|
|
|
|
const MM_TO_PX: f32 = 96.0 / 25.4;
|
|
|
|
|
|
if code < 0 {
|
|
|
|
|
|
return fallback_px;
|
|
|
|
|
|
}
|
|
|
|
|
|
// i16 value 0..=211 represents 1/100 mm.
|
|
|
|
|
|
let mm = code as f32 / 100.0;
|
|
|
|
|
|
(mm * MM_TO_PX).max(1.0)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Look up a linetype in the document's line_types table by handle and
|
|
|
|
|
|
/// resolve it to a (pattern_length, pattern) pair compatible with WireModel.
|
|
|
|
|
|
fn resolve_pattern_by_handle(
|
|
|
|
|
|
doc: &CadDocument,
|
|
|
|
|
|
handle: acadrust::types::Handle,
|
|
|
|
|
|
scale: f32,
|
|
|
|
|
|
) -> (f32, [f32; 8]) {
|
|
|
|
|
|
if handle.is_null() {
|
|
|
|
|
|
return (0.0, [0.0; 8]);
|
|
|
|
|
|
}
|
|
|
|
|
|
let name = doc
|
|
|
|
|
|
.line_types
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|lt| lt.handle == handle)
|
|
|
|
|
|
.map(|lt| lt.name.clone());
|
|
|
|
|
|
match name {
|
2026-06-16 11:33:46 +03:00
|
|
|
|
Some(n) => crate::scene::view::render::resolve_pattern(&doc.line_types, &n, scale),
|
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
|
|
|
|
None => (0.0, [0.0; 8]),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Split the combined ext-lines point list (NaN-separated segment pairs)
|
|
|
|
|
|
/// into "first" / "second" halves. `append_linear_dimension` writes ext1
|
|
|
|
|
|
/// before ext2, so the first segment is ext1 and the second is ext2.
|
|
|
|
|
|
fn split_ext_lines(points: &[[f32; 3]]) -> (Vec<[f32; 3]>, Vec<[f32; 3]>) {
|
|
|
|
|
|
let mut groups: Vec<Vec<[f32; 3]>> = Vec::new();
|
|
|
|
|
|
let mut current: Vec<[f32; 3]> = Vec::new();
|
|
|
|
|
|
for &p in points {
|
|
|
|
|
|
if p[0].is_nan() {
|
|
|
|
|
|
if !current.is_empty() {
|
|
|
|
|
|
groups.push(std::mem::take(&mut current));
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
current.push(p);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if !current.is_empty() {
|
|
|
|
|
|
groups.push(current);
|
|
|
|
|
|
}
|
|
|
|
|
|
let mut iter = groups.into_iter();
|
|
|
|
|
|
let first = iter.next().unwrap_or_default();
|
|
|
|
|
|
let rest: Vec<[f32; 3]> = iter.flatten().collect();
|
|
|
|
|
|
(first, rest)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Endpoints for the DIMTMOVE=1 leader: (anchor on the dim line, saved
|
|
|
|
|
|
/// text_middle_point). Returns None when the dim has no saved text position
|
|
|
|
|
|
/// or has no well-defined dim-line midpoint (radius/diameter handled by
|
|
|
|
|
|
/// their own leg).
|
refactor: remove dead world_offset plumbing (RTE migration is complete)
Now that geometry reaches the GPU/CPU as absolute coordinates via the
double-single relative-to-eye path, world_offset is always [0,0,0]. Strip
the parameter that was threaded through the whole tessellation / block-
expansion / fallback / camera-decode chain and the ExpandCtx field that
carried it — ~340 references across 20 files, all subtracting zero.
- truck_tess: to_local / to_local_low / tessellate_* drop the offset arg;
to_local is now a pure double-single split.
- tessellate: offset_to_ds → points_to_ds (pure DS split); tessellate(),
tessellate_entity(), fallback_geometry(), solid_wire_fallback(),
entity_aabb(), expand_insert(), expand_block_meshes(), the dimension
helpers (vec3_local, dimension_snap_pts, …), text_support, leader,
multileader, image_model, xclip and camera_from_view all lose the param.
- block_cache: ExpandCtx loses its world_offset field.
- Remove the now-dead offset_snap_pts and the unused set_grid_snap.
Behaviour is unchanged (the offset was zero everywhere); the compiler
verifies every call site.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 18:35:02 +03:00
|
|
|
|
fn dimtmove_leader_endpoints(dim: &Dimension) -> Option<(Vec3, Vec3)> {
|
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 base = dim.base();
|
|
|
|
|
|
let txt = base.text_middle_point;
|
|
|
|
|
|
if txt.x * txt.x + txt.y * txt.y + txt.z * txt.z <= 1e-16 {
|
|
|
|
|
|
return None;
|
|
|
|
|
|
}
|
refactor: remove dead world_offset plumbing (RTE migration is complete)
Now that geometry reaches the GPU/CPU as absolute coordinates via the
double-single relative-to-eye path, world_offset is always [0,0,0]. Strip
the parameter that was threaded through the whole tessellation / block-
expansion / fallback / camera-decode chain and the ExpandCtx field that
carried it — ~340 references across 20 files, all subtracting zero.
- truck_tess: to_local / to_local_low / tessellate_* drop the offset arg;
to_local is now a pure double-single split.
- tessellate: offset_to_ds → points_to_ds (pure DS split); tessellate(),
tessellate_entity(), fallback_geometry(), solid_wire_fallback(),
entity_aabb(), expand_insert(), expand_block_meshes(), the dimension
helpers (vec3_local, dimension_snap_pts, …), text_support, leader,
multileader, image_model, xclip and camera_from_view all lose the param.
- block_cache: ExpandCtx loses its world_offset field.
- Remove the now-dead offset_snap_pts and the unused set_grid_snap.
Behaviour is unchanged (the offset was zero everywhere); the compiler
verifies every call site.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 18:35:02 +03:00
|
|
|
|
let lv = |v| vec3_local(v);
|
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 anchor = match dim {
|
|
|
|
|
|
Dimension::Linear(d) => {
|
|
|
|
|
|
let perp = Vec3::new(-(d.rotation.sin() as f32), d.rotation.cos() as f32, 0.0);
|
|
|
|
|
|
let first = lv(d.first_point);
|
|
|
|
|
|
let second = lv(d.second_point);
|
|
|
|
|
|
let def = lv(d.definition_point);
|
|
|
|
|
|
let off1 = def.dot(perp) - first.dot(perp);
|
|
|
|
|
|
let off2 = def.dot(perp) - second.dot(perp);
|
|
|
|
|
|
(first + perp * off1 + second + perp * off2) * 0.5
|
|
|
|
|
|
}
|
|
|
|
|
|
Dimension::Aligned(d) => {
|
|
|
|
|
|
let first = lv(d.first_point);
|
|
|
|
|
|
let second = lv(d.second_point);
|
|
|
|
|
|
let axis = normalized_or(second - first, Vec3::X);
|
|
|
|
|
|
let perp = Vec3::new(-axis.y, axis.x, 0.0);
|
|
|
|
|
|
let def = lv(d.definition_point);
|
|
|
|
|
|
let off1 = def.dot(perp) - first.dot(perp);
|
|
|
|
|
|
let off2 = def.dot(perp) - second.dot(perp);
|
|
|
|
|
|
(first + perp * off1 + second + perp * off2) * 0.5
|
|
|
|
|
|
}
|
|
|
|
|
|
Dimension::Radius(d) => lv(d.definition_point),
|
|
|
|
|
|
Dimension::Diameter(d) => (lv(d.angle_vertex) + lv(d.definition_point)) * 0.5,
|
|
|
|
|
|
_ => return None,
|
|
|
|
|
|
};
|
|
|
|
|
|
Some((anchor, lv(txt)))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Build a rectangle of filled triangles sitting under the dim text, used
|
|
|
|
|
|
/// when DIMTFILL = 2 (explicit fill colour). The rect width is estimated
|
|
|
|
|
|
/// from the formatted text length × character-cell width; an absolutely
|
|
|
|
|
|
/// correct box would need full text metrics from the font cache.
|
|
|
|
|
|
fn text_fill_rect(
|
|
|
|
|
|
dim: &Dimension,
|
|
|
|
|
|
style: Option<&DimStyle>,
|
|
|
|
|
|
text_height: f64,
|
2026-06-17 05:32:31 +03:00
|
|
|
|
dim_scale: f64,
|
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
|
|
|
|
) -> Option<Vec<[f32; 3]>> {
|
|
|
|
|
|
let value = dimension_text_value(dim, style)?;
|
|
|
|
|
|
if value.is_empty() {
|
|
|
|
|
|
return None;
|
|
|
|
|
|
}
|
2026-06-17 05:32:31 +03:00
|
|
|
|
let pos = dimension_text_pos_f64(dim, style, text_height, dim_scale);
|
|
|
|
|
|
let dimgap = style.map(|s| s.dimgap).unwrap_or(0.0).max(0.0) * dim_scale;
|
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
|
|
|
|
// ~0.6 × text_height per character; matches average glyph aspect for
|
|
|
|
|
|
// the bundled stick fonts. Inflate by 1 DIMGAP on each side.
|
2026-06-11 00:04:37 +03:00
|
|
|
|
let approx_w = value.chars().count() as f64 * text_height * 0.6 + dimgap * 2.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
|
|
|
|
let approx_h = text_height + dimgap * 2.0;
|
|
|
|
|
|
let rot = if dim.base().text_rotation.abs() > 1e-9 {
|
|
|
|
|
|
dim.base().text_rotation
|
|
|
|
|
|
} else {
|
|
|
|
|
|
dimension_text_natural_rotation(dim)
|
|
|
|
|
|
};
|
|
|
|
|
|
let (sr, cr) = rot.sin_cos();
|
|
|
|
|
|
let hx = approx_w * 0.5;
|
|
|
|
|
|
let hy = approx_h * 0.5;
|
2026-06-24 18:38:23 +03:00
|
|
|
|
let cx = (pos.x) as f32;
|
|
|
|
|
|
let cy = (pos.y) as f32;
|
|
|
|
|
|
let cz = (pos.z) as f32;
|
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 corner = |dx: f64, dy: f64| -> [f32; 3] {
|
|
|
|
|
|
let lx = dx * cr - dy * sr;
|
|
|
|
|
|
let ly = dx * sr + dy * cr;
|
|
|
|
|
|
[cx + lx as f32, cy + ly as f32, cz]
|
|
|
|
|
|
};
|
|
|
|
|
|
let p1 = corner(-hx, -hy);
|
|
|
|
|
|
let p2 = corner(hx, -hy);
|
|
|
|
|
|
let p3 = corner(hx, hy);
|
|
|
|
|
|
let p4 = corner(-hx, hy);
|
|
|
|
|
|
Some(vec![p1, p2, p3, p1, p3, p4])
|
|
|
|
|
|
}
|
|
|
|
|
|
struct SuppressFlags {
|
|
|
|
|
|
ext1: bool,
|
|
|
|
|
|
ext2: bool,
|
|
|
|
|
|
dim1: bool,
|
|
|
|
|
|
dim2: bool,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy)]
|
|
|
|
|
|
struct DimLineParams {
|
|
|
|
|
|
dimexo: f32,
|
|
|
|
|
|
dimexe: f32,
|
|
|
|
|
|
dimdle: f32,
|
|
|
|
|
|
dimfxl: f32,
|
|
|
|
|
|
dimfxlon: bool,
|
|
|
|
|
|
dimsoxd: bool,
|
|
|
|
|
|
dimcen: f32,
|
|
|
|
|
|
ticks: bool,
|
2026-06-14 02:13:00 +03:00
|
|
|
|
/// Arrowhead length (DIMASZ, scaled) — used to decide arrow-outside fit.
|
|
|
|
|
|
arrow_len: f32,
|
2026-06-14 10:09:28 +03:00
|
|
|
|
/// Text box (local centre, half-width, half-height) used to break the
|
|
|
|
|
|
/// dimension line where the text sits on it, so a DIMTFILL background reads
|
|
|
|
|
|
/// over the line. None when the text doesn't overlap the line.
|
|
|
|
|
|
text_break: Option<(Vec3, f32, f32)>,
|
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
|
|
|
|
}
|
|
|
|
|
|
fn dimension_geometry(
|
|
|
|
|
|
dim: &Dimension,
|
|
|
|
|
|
arrow1: &ArrowKind,
|
|
|
|
|
|
arrow2: &ArrowKind,
|
|
|
|
|
|
params: DimLineParams,
|
|
|
|
|
|
suppress: SuppressFlags,
|
|
|
|
|
|
) -> DimGeom {
|
refactor: remove dead world_offset plumbing (RTE migration is complete)
Now that geometry reaches the GPU/CPU as absolute coordinates via the
double-single relative-to-eye path, world_offset is always [0,0,0]. Strip
the parameter that was threaded through the whole tessellation / block-
expansion / fallback / camera-decode chain and the ExpandCtx field that
carried it — ~340 references across 20 files, all subtracting zero.
- truck_tess: to_local / to_local_low / tessellate_* drop the offset arg;
to_local is now a pure double-single split.
- tessellate: offset_to_ds → points_to_ds (pure DS split); tessellate(),
tessellate_entity(), fallback_geometry(), solid_wire_fallback(),
entity_aabb(), expand_insert(), expand_block_meshes(), the dimension
helpers (vec3_local, dimension_snap_pts, …), text_support, leader,
multileader, image_model, xclip and camera_from_view all lose the param.
- block_cache: ExpandCtx loses its world_offset field.
- Remove the now-dead offset_snap_pts and the unused set_grid_snap.
Behaviour is unchanged (the offset was zero everywhere); the compiler
verifies every call site.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 18:35:02 +03:00
|
|
|
|
let lv = |v| vec3_local(v);
|
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 mut g = DimGeom::new();
|
|
|
|
|
|
match dim {
|
|
|
|
|
|
Dimension::Aligned(d) => {
|
|
|
|
|
|
let first = lv(d.first_point);
|
|
|
|
|
|
let second = lv(d.second_point);
|
|
|
|
|
|
let def = lv(d.definition_point);
|
|
|
|
|
|
let axis = normalized_or(second - first, Vec3::X);
|
|
|
|
|
|
append_linear_dimension(
|
|
|
|
|
|
&mut g,
|
|
|
|
|
|
first,
|
|
|
|
|
|
second,
|
|
|
|
|
|
def,
|
|
|
|
|
|
axis,
|
|
|
|
|
|
arrow1,
|
|
|
|
|
|
arrow2,
|
|
|
|
|
|
params,
|
|
|
|
|
|
suppress,
|
|
|
|
|
|
d.ext_line_rotation as f32,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
Dimension::Linear(d) => {
|
|
|
|
|
|
let first = lv(d.first_point);
|
|
|
|
|
|
let second = lv(d.second_point);
|
|
|
|
|
|
let def = lv(d.definition_point);
|
|
|
|
|
|
let axis = Vec3::new(d.rotation.cos() as f32, d.rotation.sin() as f32, 0.0);
|
|
|
|
|
|
append_linear_dimension(
|
|
|
|
|
|
&mut g,
|
|
|
|
|
|
first,
|
|
|
|
|
|
second,
|
|
|
|
|
|
def,
|
|
|
|
|
|
normalized_or(axis, Vec3::X),
|
|
|
|
|
|
arrow1,
|
|
|
|
|
|
arrow2,
|
|
|
|
|
|
params,
|
|
|
|
|
|
suppress,
|
|
|
|
|
|
d.ext_line_rotation as f32,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
Dimension::Radius(d) => {
|
|
|
|
|
|
let center = lv(d.angle_vertex);
|
|
|
|
|
|
let point = lv(d.definition_point);
|
refactor: remove dead world_offset plumbing (RTE migration is complete)
Now that geometry reaches the GPU/CPU as absolute coordinates via the
double-single relative-to-eye path, world_offset is always [0,0,0]. Strip
the parameter that was threaded through the whole tessellation / block-
expansion / fallback / camera-decode chain and the ExpandCtx field that
carried it — ~340 references across 20 files, all subtracting zero.
- truck_tess: to_local / to_local_low / tessellate_* drop the offset arg;
to_local is now a pure double-single split.
- tessellate: offset_to_ds → points_to_ds (pure DS split); tessellate(),
tessellate_entity(), fallback_geometry(), solid_wire_fallback(),
entity_aabb(), expand_insert(), expand_block_meshes(), the dimension
helpers (vec3_local, dimension_snap_pts, …), text_support, leader,
multileader, image_model, xclip and camera_from_view all lose the param.
- block_cache: ExpandCtx loses its world_offset field.
- Remove the now-dead offset_snap_pts and the unused set_grid_snap.
Behaviour is unchanged (the offset was zero everywhere); the compiler
verifies every call site.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 18:35:02 +03:00
|
|
|
|
let text = dimension_text_position(dim);
|
feat(commands): expand standard CAD command coverage
Integrate a large batch of standard CAD commands and fix several
name/behaviour mismatches, with no product references in code.
3D solids (reusing the truck B-rep pipeline):
- SLICE, INTERFERE, PRESSPULL, POLYSOLID, PYRAMID, SECTION
- 3DROTATE, 3DMIRROR, 3DALIGN (cached-solid transforms)
Annotation / text:
- ARCTEXT, SPLINEFIT (Catmull-Rom to Bezier), DIMJOGGED, OBJECTSCALE
(XData markers honoured by the existing geometry-gen / tessellator)
Blocks / misc:
- NCOPY (block-content extraction via explode_entity)
- OPTIONS, SYNCPVIEWPORTS, THICKEN, plus earlier toggles, aliases,
system variables, and layer/style/view commands
POINTCLOUD/RECAP and UNDERLAYLAYERS/UOSNAP are intentionally left out:
they require a proprietary binary / PDF-layer parser, a GPU point
renderer, and real test files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 08:23:58 +03:00
|
|
|
|
// A jogged radius dim (marked via XData) replaces the straight radial
|
|
|
|
|
|
// leader with a foreshortened zig-zag at ~45° near its midpoint.
|
|
|
|
|
|
let jogged = dim
|
|
|
|
|
|
.base()
|
|
|
|
|
|
.common
|
|
|
|
|
|
.extended_data
|
|
|
|
|
|
.get_record("OCS_JOGGED")
|
|
|
|
|
|
.is_some();
|
|
|
|
|
|
if jogged {
|
|
|
|
|
|
let delta = point - center;
|
|
|
|
|
|
let dist = delta.length();
|
|
|
|
|
|
let u = normalized_or(delta, Vec3::X);
|
|
|
|
|
|
let perp = Vec3::new(-u.y, u.x, 0.0);
|
|
|
|
|
|
let half = (dist * 0.06).max(1e-3);
|
|
|
|
|
|
let mid = center + u * (dist * 0.5);
|
|
|
|
|
|
let a = mid - u * half + perp * half;
|
|
|
|
|
|
let b = mid + u * half - perp * half;
|
|
|
|
|
|
add_segment(&mut g.dim_lines, center, a);
|
|
|
|
|
|
add_segment(&mut g.dim_lines, a, b);
|
|
|
|
|
|
add_segment(&mut g.dim_lines, b, point);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
add_segment(&mut g.dim_lines, center, point);
|
|
|
|
|
|
}
|
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
|
|
|
|
// Honour leader_length: extend from the arrow tip past it
|
|
|
|
|
|
// toward the text by that distance along (text - point).
|
|
|
|
|
|
let leader_dir = normalized_or(text - point, Vec3::X);
|
|
|
|
|
|
let leader = if d.leader_length.abs() > 1e-9 {
|
|
|
|
|
|
point + leader_dir * (d.leader_length as f32)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
text
|
|
|
|
|
|
};
|
|
|
|
|
|
add_segment(&mut g.dim_lines, point, leader);
|
2026-06-11 00:04:37 +03:00
|
|
|
|
append_arrow(
|
|
|
|
|
|
&mut g,
|
|
|
|
|
|
point,
|
|
|
|
|
|
normalized_or(center - point, Vec3::X),
|
|
|
|
|
|
arrow1,
|
|
|
|
|
|
);
|
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 radius = (point - center).length();
|
|
|
|
|
|
append_center_mark(&mut g, center, params.dimcen, radius);
|
|
|
|
|
|
}
|
|
|
|
|
|
Dimension::Diameter(d) => {
|
2026-06-27 17:47:50 +03:00
|
|
|
|
// angle_vertex is the circle centre and definition_point a point on
|
|
|
|
|
|
// the circle. The diameter line runs edge-to-edge THROUGH the centre
|
|
|
|
|
|
// (far edge → near edge), with arrows pointing inward at each edge.
|
|
|
|
|
|
let center = lv(d.angle_vertex);
|
|
|
|
|
|
let edge = lv(d.definition_point);
|
|
|
|
|
|
let far = center * 2.0 - edge;
|
|
|
|
|
|
add_segment(&mut g.dim_lines, far, edge);
|
|
|
|
|
|
append_arrow(&mut g, edge, normalized_or(far - edge, Vec3::X), arrow1);
|
|
|
|
|
|
append_arrow(&mut g, far, normalized_or(edge - far, Vec3::X), arrow2);
|
|
|
|
|
|
// Diameter leader: continue past the near edge toward the text.
|
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
|
|
|
|
if d.leader_length.abs() > 1e-9 {
|
refactor: remove dead world_offset plumbing (RTE migration is complete)
Now that geometry reaches the GPU/CPU as absolute coordinates via the
double-single relative-to-eye path, world_offset is always [0,0,0]. Strip
the parameter that was threaded through the whole tessellation / block-
expansion / fallback / camera-decode chain and the ExpandCtx field that
carried it — ~340 references across 20 files, all subtracting zero.
- truck_tess: to_local / to_local_low / tessellate_* drop the offset arg;
to_local is now a pure double-single split.
- tessellate: offset_to_ds → points_to_ds (pure DS split); tessellate(),
tessellate_entity(), fallback_geometry(), solid_wire_fallback(),
entity_aabb(), expand_insert(), expand_block_meshes(), the dimension
helpers (vec3_local, dimension_snap_pts, …), text_support, leader,
multileader, image_model, xclip and camera_from_view all lose the param.
- block_cache: ExpandCtx loses its world_offset field.
- Remove the now-dead offset_snap_pts and the unused set_grid_snap.
Behaviour is unchanged (the offset was zero everywhere); the compiler
verifies every call site.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 18:35:02 +03:00
|
|
|
|
let text = dimension_text_position(dim);
|
2026-06-27 17:47:50 +03:00
|
|
|
|
let leader_dir = normalized_or(text - edge, edge - far);
|
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
|
|
|
|
add_segment(
|
|
|
|
|
|
&mut g.dim_lines,
|
2026-06-27 17:47:50 +03:00
|
|
|
|
edge,
|
|
|
|
|
|
edge + leader_dir * (d.leader_length as f32),
|
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-06-27 17:47:50 +03:00
|
|
|
|
let radius = (edge - center).length();
|
|
|
|
|
|
append_center_mark(&mut g, center, params.dimcen, radius);
|
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
|
|
|
|
}
|
|
|
|
|
|
Dimension::Angular2Ln(d) => {
|
|
|
|
|
|
append_angular_dimension(
|
|
|
|
|
|
&mut g,
|
|
|
|
|
|
lv(d.angle_vertex),
|
|
|
|
|
|
lv(d.first_point),
|
|
|
|
|
|
lv(d.second_point),
|
|
|
|
|
|
lv(d.dimension_arc),
|
|
|
|
|
|
arrow1,
|
|
|
|
|
|
arrow2,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
Dimension::Angular3Pt(d) => {
|
|
|
|
|
|
append_angular_dimension(
|
|
|
|
|
|
&mut g,
|
|
|
|
|
|
lv(d.angle_vertex),
|
|
|
|
|
|
lv(d.first_point),
|
|
|
|
|
|
lv(d.second_point),
|
|
|
|
|
|
lv(d.definition_point),
|
|
|
|
|
|
arrow1,
|
|
|
|
|
|
arrow2,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
Dimension::Ordinate(d) => {
|
|
|
|
|
|
add_segment(
|
|
|
|
|
|
&mut g.dim_lines,
|
|
|
|
|
|
lv(d.feature_location),
|
|
|
|
|
|
lv(d.definition_point),
|
|
|
|
|
|
);
|
|
|
|
|
|
add_segment(
|
|
|
|
|
|
&mut g.dim_lines,
|
|
|
|
|
|
lv(d.definition_point),
|
|
|
|
|
|
lv(d.leader_endpoint),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
g
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn append_linear_dimension(
|
|
|
|
|
|
g: &mut DimGeom,
|
|
|
|
|
|
first: Vec3,
|
|
|
|
|
|
second: Vec3,
|
|
|
|
|
|
def: Vec3,
|
|
|
|
|
|
axis: Vec3,
|
|
|
|
|
|
arrow1: &ArrowKind,
|
|
|
|
|
|
arrow2: &ArrowKind,
|
|
|
|
|
|
params: DimLineParams,
|
|
|
|
|
|
suppress: SuppressFlags,
|
|
|
|
|
|
ext_line_rotation: f32,
|
|
|
|
|
|
) {
|
|
|
|
|
|
let perp = Vec3::new(-axis.y, axis.x, 0.0);
|
|
|
|
|
|
let dim_line_pos = def.dot(perp);
|
|
|
|
|
|
let offset1 = dim_line_pos - first.dot(perp);
|
|
|
|
|
|
let offset2 = dim_line_pos - second.dot(perp);
|
|
|
|
|
|
let d1 = first + perp * offset1;
|
|
|
|
|
|
let d2 = second + perp * offset2;
|
|
|
|
|
|
let sign1 = if offset1 >= 0.0 { 1.0_f32 } else { -1.0 };
|
|
|
|
|
|
let sign2 = if offset2 >= 0.0 { 1.0_f32 } else { -1.0 };
|
|
|
|
|
|
|
|
|
|
|
|
// ext_line_rotation (DIMEDIT "Oblique"): rotate the extension lines by
|
|
|
|
|
|
// this angle relative to perpendicular. The ext line still starts at
|
|
|
|
|
|
// the def point; only the direction differs.
|
|
|
|
|
|
let ext_dir = if ext_line_rotation.abs() > 1e-6 {
|
|
|
|
|
|
let c = ext_line_rotation.cos();
|
|
|
|
|
|
let s = ext_line_rotation.sin();
|
|
|
|
|
|
// Rotate `perp` by ext_line_rotation around Z.
|
|
|
|
|
|
Vec3::new(perp.x * c - perp.y * s, perp.x * s + perp.y * c, 0.0)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
perp
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// DIMFXLON / DIMFXL: fixed extension-line length from the dim line back
|
|
|
|
|
|
// toward (but not past) the definition point. Otherwise grow from the
|
|
|
|
|
|
// def point with DIMEXO gap, extending DIMEXE past the dim line.
|
|
|
|
|
|
// When oblique, lengths are measured along ext_dir instead of perp.
|
|
|
|
|
|
let (ext1_start, ext1_end, ext2_start, ext2_end) = if params.dimfxlon {
|
|
|
|
|
|
let fxl = params.dimfxl.max(0.0);
|
|
|
|
|
|
let s1 = d1 - ext_dir * (sign1 * fxl);
|
|
|
|
|
|
let e1 = d1 + ext_dir * (sign1 * params.dimexe);
|
|
|
|
|
|
let s2 = d2 - ext_dir * (sign2 * fxl);
|
|
|
|
|
|
let e2 = d2 + ext_dir * (sign2 * params.dimexe);
|
|
|
|
|
|
(s1, e1, s2, e2)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
(
|
|
|
|
|
|
first + ext_dir * (sign1 * params.dimexo),
|
|
|
|
|
|
d1 + ext_dir * (sign1 * params.dimexe),
|
|
|
|
|
|
second + ext_dir * (sign2 * params.dimexo),
|
|
|
|
|
|
d2 + ext_dir * (sign2 * params.dimexe),
|
|
|
|
|
|
)
|
|
|
|
|
|
};
|
|
|
|
|
|
if !suppress.ext1 {
|
|
|
|
|
|
add_segment(&mut g.ext_lines, ext1_start, ext1_end);
|
|
|
|
|
|
}
|
|
|
|
|
|
if !suppress.ext2 {
|
|
|
|
|
|
add_segment(&mut g.ext_lines, ext2_start, ext2_end);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// DIMDLE: dim line overshoots the ext line by `dimdle` at each end,
|
|
|
|
|
|
// but only when ticks are in use (DIMTSZ > 0). With arrowheads this
|
|
|
|
|
|
// is ignored, matching AutoCAD.
|
|
|
|
|
|
let dle = if params.ticks { params.dimdle } else { 0.0 };
|
|
|
|
|
|
let dir_d1_to_d2 = normalized_or(d2 - d1, axis);
|
|
|
|
|
|
let d1_out = d1 - dir_d1_to_d2 * dle;
|
|
|
|
|
|
let d2_out = d2 + dir_d1_to_d2 * dle;
|
2026-06-14 02:13:00 +03:00
|
|
|
|
|
|
|
|
|
|
// DIMATFIT fit: when the arrowheads don't fit between the extension lines
|
|
|
|
|
|
// they're flipped to the outside (point still on the ext line, body
|
|
|
|
|
|
// outside) with a short stub for them to sit on. DIMSOXD suppresses those
|
|
|
|
|
|
// outer stubs. Ticks always fit, so this only affects arrowheads.
|
|
|
|
|
|
let gap = (d2 - d1).length();
|
|
|
|
|
|
let arrows_outside = !params.ticks && params.arrow_len > 1e-6 && gap < 2.0 * params.arrow_len;
|
|
|
|
|
|
|
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
|
|
|
|
// DIMSD1/DIMSD2: when *both* set, omit the dim line entirely. AutoCAD
|
|
|
|
|
|
// splits at text otherwise — without that pivot info, leave as-is.
|
|
|
|
|
|
if !(suppress.dim1 && suppress.dim2) {
|
2026-06-14 10:09:28 +03:00
|
|
|
|
// Break the dim line where the text sits on it, so a DIMTFILL
|
|
|
|
|
|
// background reads over the line (2-D fills draw under all wires, so the
|
|
|
|
|
|
// line is gapped rather than z-masked).
|
|
|
|
|
|
let mut drew = false;
|
|
|
|
|
|
if let Some((tc, half_w, half_h)) = params.text_break {
|
|
|
|
|
|
let dir = normalized_or(d2_out - d1_out, axis);
|
|
|
|
|
|
let len = (d2_out - d1_out).length();
|
|
|
|
|
|
let along = (tc - d1_out).dot(dir);
|
|
|
|
|
|
let perp_dist = (tc - (d1_out + dir * along)).length();
|
|
|
|
|
|
if perp_dist < half_h && along - half_w > 0.0 && along + half_w < len {
|
|
|
|
|
|
add_segment(&mut g.dim_lines, d1_out, d1_out + dir * (along - half_w));
|
|
|
|
|
|
add_segment(&mut g.dim_lines, d1_out + dir * (along + half_w), d2_out);
|
|
|
|
|
|
drew = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if !drew {
|
|
|
|
|
|
add_segment(&mut g.dim_lines, d1_out, d2_out);
|
|
|
|
|
|
}
|
2026-06-14 02:13:00 +03:00
|
|
|
|
if arrows_outside && !params.dimsoxd {
|
|
|
|
|
|
let stub = params.arrow_len * 2.0;
|
|
|
|
|
|
add_segment(&mut g.dim_lines, d1 - dir_d1_to_d2 * stub, d1);
|
|
|
|
|
|
add_segment(&mut g.dim_lines, d2, d2 + dir_d1_to_d2 * stub);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if arrows_outside {
|
|
|
|
|
|
// Tip on the ext line, body pointing outward.
|
|
|
|
|
|
append_arrow(g, d1, normalized_or(d1 - d2, -axis), arrow1);
|
|
|
|
|
|
append_arrow(g, d2, normalized_or(d2 - d1, axis), arrow2);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
append_arrow(g, d1, normalized_or(d2 - d1, axis), arrow1);
|
|
|
|
|
|
append_arrow(g, d2, normalized_or(d1 - d2, -axis), arrow2);
|
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
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Draw a center mark for radius/diameter dimensions.
|
|
|
|
|
|
/// DIMCEN > 0 → small "+" of half-length |DIMCEN| at the centre.
|
|
|
|
|
|
/// DIMCEN < 0 → small "+" *plus* four line segments extending from the
|
|
|
|
|
|
/// circle (radius - |DIMCEN|) outward to (radius + |DIMCEN|).
|
|
|
|
|
|
/// DIMCEN = 0 → no mark.
|
|
|
|
|
|
fn append_center_mark(g: &mut DimGeom, center: Vec3, dimcen: f32, radius: f32) {
|
|
|
|
|
|
let mag = dimcen.abs();
|
|
|
|
|
|
if mag <= 1e-6 {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Small "+" at the centre.
|
|
|
|
|
|
let h = mag;
|
|
|
|
|
|
add_segment(
|
|
|
|
|
|
&mut g.dim_lines,
|
|
|
|
|
|
Vec3::new(center.x - h, center.y, center.z),
|
|
|
|
|
|
Vec3::new(center.x + h, center.y, center.z),
|
|
|
|
|
|
);
|
|
|
|
|
|
add_segment(
|
|
|
|
|
|
&mut g.dim_lines,
|
|
|
|
|
|
Vec3::new(center.x, center.y - h, center.z),
|
|
|
|
|
|
Vec3::new(center.x, center.y + h, center.z),
|
|
|
|
|
|
);
|
|
|
|
|
|
if dimcen < 0.0 && radius > mag + 1e-6 {
|
|
|
|
|
|
let inner = (radius - mag).max(0.0);
|
|
|
|
|
|
let outer = radius + mag;
|
|
|
|
|
|
// Four short radial strokes spanning the circle edge.
|
|
|
|
|
|
add_segment(
|
|
|
|
|
|
&mut g.dim_lines,
|
|
|
|
|
|
Vec3::new(center.x + inner, center.y, center.z),
|
|
|
|
|
|
Vec3::new(center.x + outer, center.y, center.z),
|
|
|
|
|
|
);
|
|
|
|
|
|
add_segment(
|
|
|
|
|
|
&mut g.dim_lines,
|
|
|
|
|
|
Vec3::new(center.x - inner, center.y, center.z),
|
|
|
|
|
|
Vec3::new(center.x - outer, center.y, center.z),
|
|
|
|
|
|
);
|
|
|
|
|
|
add_segment(
|
|
|
|
|
|
&mut g.dim_lines,
|
|
|
|
|
|
Vec3::new(center.x, center.y + inner, center.z),
|
|
|
|
|
|
Vec3::new(center.x, center.y + outer, center.z),
|
|
|
|
|
|
);
|
|
|
|
|
|
add_segment(
|
|
|
|
|
|
&mut g.dim_lines,
|
|
|
|
|
|
Vec3::new(center.x, center.y - inner, center.z),
|
|
|
|
|
|
Vec3::new(center.x, center.y - outer, center.z),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn append_angular_dimension(
|
|
|
|
|
|
g: &mut DimGeom,
|
|
|
|
|
|
vertex: Vec3,
|
|
|
|
|
|
first: Vec3,
|
|
|
|
|
|
second: Vec3,
|
|
|
|
|
|
arc_point: Vec3,
|
|
|
|
|
|
arrow1: &ArrowKind,
|
|
|
|
|
|
arrow2: &ArrowKind,
|
|
|
|
|
|
) {
|
|
|
|
|
|
let radius = vertex.distance(arc_point);
|
|
|
|
|
|
if radius <= 1e-6 {
|
2026-06-27 17:47:50 +03:00
|
|
|
|
add_segment(&mut g.ext_lines, vertex, first);
|
|
|
|
|
|
add_segment(&mut g.ext_lines, vertex, second);
|
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;
|
|
|
|
|
|
}
|
2026-06-27 17:47:50 +03:00
|
|
|
|
// Extension lines run from each measured point out to the dimension arc so
|
|
|
|
|
|
// there is no gap between a ray and its arc endpoint. (#181 / DIM-027)
|
|
|
|
|
|
let dir1 = normalized_or(first - vertex, Vec3::X);
|
|
|
|
|
|
let dir2 = normalized_or(second - vertex, Vec3::X);
|
|
|
|
|
|
add_segment(&mut g.ext_lines, first, vertex + dir1 * radius);
|
|
|
|
|
|
add_segment(&mut g.ext_lines, second, vertex + dir2 * radius);
|
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 start = (first.y - vertex.y).atan2(first.x - vertex.x);
|
|
|
|
|
|
let mut end = (second.y - vertex.y).atan2(second.x - vertex.x);
|
|
|
|
|
|
let mut delta = end - start;
|
|
|
|
|
|
while delta <= 0.0 {
|
|
|
|
|
|
delta += std::f32::consts::TAU;
|
|
|
|
|
|
}
|
|
|
|
|
|
if delta > std::f32::consts::PI {
|
|
|
|
|
|
end -= std::f32::consts::TAU;
|
|
|
|
|
|
delta = end - start;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let steps = 32;
|
|
|
|
|
|
let mut arc_pts = Vec::with_capacity((steps + 1) as usize);
|
|
|
|
|
|
for i in 0..=steps {
|
|
|
|
|
|
let t = i as f32 / steps as f32;
|
|
|
|
|
|
let a = start + delta * t;
|
|
|
|
|
|
arc_pts.push(vertex + Vec3::new(a.cos() * radius, a.sin() * radius, 0.0));
|
|
|
|
|
|
}
|
|
|
|
|
|
add_polyline(&mut g.dim_lines, &arc_pts);
|
|
|
|
|
|
|
|
|
|
|
|
if arc_pts.len() >= 2 {
|
|
|
|
|
|
append_arrow(
|
|
|
|
|
|
g,
|
|
|
|
|
|
arc_pts[0],
|
|
|
|
|
|
normalized_or(arc_pts[1] - arc_pts[0], Vec3::X),
|
|
|
|
|
|
arrow1,
|
|
|
|
|
|
);
|
|
|
|
|
|
let n = arc_pts.len();
|
|
|
|
|
|
append_arrow(
|
|
|
|
|
|
g,
|
|
|
|
|
|
arc_pts[n - 1],
|
|
|
|
|
|
normalized_or(arc_pts[n - 2] - arc_pts[n - 1], Vec3::X),
|
|
|
|
|
|
arrow2,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
refactor: remove dead world_offset plumbing (RTE migration is complete)
Now that geometry reaches the GPU/CPU as absolute coordinates via the
double-single relative-to-eye path, world_offset is always [0,0,0]. Strip
the parameter that was threaded through the whole tessellation / block-
expansion / fallback / camera-decode chain and the ExpandCtx field that
carried it — ~340 references across 20 files, all subtracting zero.
- truck_tess: to_local / to_local_low / tessellate_* drop the offset arg;
to_local is now a pure double-single split.
- tessellate: offset_to_ds → points_to_ds (pure DS split); tessellate(),
tessellate_entity(), fallback_geometry(), solid_wire_fallback(),
entity_aabb(), expand_insert(), expand_block_meshes(), the dimension
helpers (vec3_local, dimension_snap_pts, …), text_support, leader,
multileader, image_model, xclip and camera_from_view all lose the param.
- block_cache: ExpandCtx loses its world_offset field.
- Remove the now-dead offset_snap_pts and the unused set_grid_snap.
Behaviour is unchanged (the offset was zero everywhere); the compiler
verifies every call site.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 18:35:02 +03:00
|
|
|
|
fn dimension_snap_pts(dim: &Dimension) -> Vec<(glam::DVec3, SnapHint)> {
|
feat(commands): expand standard CAD command coverage
Integrate a large batch of standard CAD commands and fix several
name/behaviour mismatches, with no product references in code.
3D solids (reusing the truck B-rep pipeline):
- SLICE, INTERFERE, PRESSPULL, POLYSOLID, PYRAMID, SECTION
- 3DROTATE, 3DMIRROR, 3DALIGN (cached-solid transforms)
Annotation / text:
- ARCTEXT, SPLINEFIT (Catmull-Rom to Bezier), DIMJOGGED, OBJECTSCALE
(XData markers honoured by the existing geometry-gen / tessellator)
Blocks / misc:
- NCOPY (block-content extraction via explode_entity)
- OPTIONS, SYNCPVIEWPORTS, THICKEN, plus earlier toggles, aliases,
system variables, and layer/style/view commands
POINTCLOUD/RECAP and UNDERLAYLAYERS/UOSNAP are intentionally left out:
they require a proprietary binary / PDF-layer parser, a GPU point
renderer, and real test files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 08:23:58 +03:00
|
|
|
|
let lv = |v: acadrust::types::Vector3| glam::DVec3::new(v.x, v.y, v.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
|
|
|
|
let node = |v: acadrust::types::Vector3| (lv(v), SnapHint::Node);
|
|
|
|
|
|
match dim {
|
|
|
|
|
|
Dimension::Linear(d) => vec![
|
|
|
|
|
|
node(d.first_point),
|
|
|
|
|
|
node(d.second_point),
|
|
|
|
|
|
node(d.definition_point),
|
|
|
|
|
|
],
|
|
|
|
|
|
Dimension::Aligned(d) => vec![
|
|
|
|
|
|
node(d.first_point),
|
|
|
|
|
|
node(d.second_point),
|
|
|
|
|
|
node(d.definition_point),
|
|
|
|
|
|
],
|
|
|
|
|
|
Dimension::Radius(d) => vec![node(d.angle_vertex), node(d.definition_point)],
|
|
|
|
|
|
Dimension::Diameter(d) => vec![node(d.angle_vertex), node(d.definition_point)],
|
|
|
|
|
|
Dimension::Angular2Ln(d) => vec![
|
|
|
|
|
|
node(d.angle_vertex),
|
|
|
|
|
|
node(d.first_point),
|
|
|
|
|
|
node(d.second_point),
|
|
|
|
|
|
node(d.definition_point),
|
|
|
|
|
|
],
|
|
|
|
|
|
Dimension::Angular3Pt(d) => vec![
|
|
|
|
|
|
node(d.angle_vertex),
|
|
|
|
|
|
node(d.first_point),
|
|
|
|
|
|
node(d.second_point),
|
|
|
|
|
|
node(d.definition_point),
|
|
|
|
|
|
],
|
|
|
|
|
|
Dimension::Ordinate(d) => vec![
|
|
|
|
|
|
node(d.definition_point),
|
|
|
|
|
|
node(d.feature_location),
|
|
|
|
|
|
node(d.leader_endpoint),
|
|
|
|
|
|
],
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Cheap heuristic: does this string contain anything the MText parser would
|
|
|
|
|
|
/// interpret? Used by `dimension_text_entity` to pick between a synthetic
|
|
|
|
|
|
/// `Text` (plain DXF special chars only) and a synthetic `MText` (full inline
|
|
|
|
|
|
/// format-code pipeline) for the dim text override.
|
|
|
|
|
|
fn value_has_mtext_codes(s: &str) -> bool {
|
|
|
|
|
|
let mut chars = s.chars().peekable();
|
|
|
|
|
|
while let Some(c) = chars.next() {
|
|
|
|
|
|
if c == '{' || c == '}' {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if c == '\\' {
|
|
|
|
|
|
if let Some(&next) = chars.peek() {
|
|
|
|
|
|
// Any backslash followed by a known MText escape letter.
|
|
|
|
|
|
if matches!(
|
|
|
|
|
|
next,
|
|
|
|
|
|
'H' | 'W'
|
|
|
|
|
|
| 'Q'
|
|
|
|
|
|
| 'T'
|
|
|
|
|
|
| 'A'
|
|
|
|
|
|
| 'C'
|
|
|
|
|
|
| 'c'
|
|
|
|
|
|
| 'f'
|
|
|
|
|
|
| 'F'
|
|
|
|
|
|
| 'p'
|
|
|
|
|
|
| 'L'
|
|
|
|
|
|
| 'l'
|
|
|
|
|
|
| 'O'
|
|
|
|
|
|
| 'o'
|
|
|
|
|
|
| 'K'
|
|
|
|
|
|
| 'k'
|
|
|
|
|
|
| 'S'
|
|
|
|
|
|
| 's'
|
|
|
|
|
|
| 'P'
|
|
|
|
|
|
| 'n'
|
|
|
|
|
|
| 'N'
|
|
|
|
|
|
| 't'
|
|
|
|
|
|
| 'U'
|
|
|
|
|
|
| 'u'
|
|
|
|
|
|
| 'M'
|
|
|
|
|
|
| 'X'
|
|
|
|
|
|
| '~'
|
|
|
|
|
|
| '{'
|
|
|
|
|
|
| '}'
|
|
|
|
|
|
) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn dimension_text_entity(
|
|
|
|
|
|
dim: &Dimension,
|
|
|
|
|
|
text_height: f64,
|
|
|
|
|
|
style: Option<&DimStyle>,
|
|
|
|
|
|
document: &CadDocument,
|
2026-06-17 05:32:31 +03:00
|
|
|
|
dim_scale: f64,
|
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
|
|
|
|
) -> Option<EntityType> {
|
|
|
|
|
|
let value = dimension_text_value(dim, style)?;
|
|
|
|
|
|
// Use f64 position directly to avoid f32 round-trip precision loss at large
|
|
|
|
|
|
// coordinates (e.g. Turkish UTM ~4,000,000 m). tessellate() will apply
|
|
|
|
|
|
// world_offset when rendering this synthetic entity.
|
2026-06-17 05:32:31 +03:00
|
|
|
|
let pos_f64 = dimension_text_pos_f64(dim, style, text_height, dim_scale);
|
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 base = dim.base();
|
|
|
|
|
|
|
2026-06-27 17:47:50 +03:00
|
|
|
|
let rotation = dimension_text_rotation(dim, style);
|
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
|
|
|
|
|
|
|
|
|
|
// Text style resolution priority:
|
|
|
|
|
|
// 1. DIMTXSTY by handle (most reliable; survives rename)
|
|
|
|
|
|
// 2. DIMTXSTY by name
|
|
|
|
|
|
// 3. dim's own style_name (rare fallback)
|
|
|
|
|
|
let style_name = style
|
|
|
|
|
|
.and_then(|s| {
|
|
|
|
|
|
if !s.dimtxsty_handle.is_null() {
|
|
|
|
|
|
document
|
|
|
|
|
|
.text_styles
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|ts| ts.handle == s.dimtxsty_handle)
|
|
|
|
|
|
.map(|ts| ts.name.clone())
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.or_else(|| {
|
|
|
|
|
|
style
|
|
|
|
|
|
.map(|s| s.dimtxsty.clone())
|
|
|
|
|
|
.filter(|n| !n.trim().is_empty())
|
|
|
|
|
|
})
|
|
|
|
|
|
.unwrap_or_else(|| base.style_name.clone());
|
|
|
|
|
|
|
|
|
|
|
|
// Route through MText whenever the value carries inline format codes
|
|
|
|
|
|
// (`\f`, `\C`, `\H`, `\S`, brace scopes, …). Otherwise stay on the Text
|
|
|
|
|
|
// path — single-line dim text doesn't need the full MText pipeline.
|
|
|
|
|
|
if value_has_mtext_codes(&value) {
|
|
|
|
|
|
use acadrust::entities::dimension::AttachmentPointType as DA;
|
|
|
|
|
|
use acadrust::entities::AttachmentPoint as MA;
|
|
|
|
|
|
let attachment_point = match base.attachment_point {
|
|
|
|
|
|
DA::TopLeft => MA::TopLeft,
|
|
|
|
|
|
DA::TopCenter => MA::TopCenter,
|
|
|
|
|
|
DA::TopRight => MA::TopRight,
|
|
|
|
|
|
DA::MiddleLeft => MA::MiddleLeft,
|
|
|
|
|
|
DA::MiddleCenter => MA::MiddleCenter,
|
|
|
|
|
|
DA::MiddleRight => MA::MiddleRight,
|
|
|
|
|
|
DA::BottomLeft => MA::BottomLeft,
|
|
|
|
|
|
DA::BottomCenter => MA::BottomCenter,
|
|
|
|
|
|
DA::BottomRight => MA::BottomRight,
|
|
|
|
|
|
};
|
|
|
|
|
|
let mut mtext = MText::with_value(value, pos_f64);
|
|
|
|
|
|
mtext.height = text_height;
|
|
|
|
|
|
mtext.rotation = rotation;
|
|
|
|
|
|
mtext.style = style_name;
|
|
|
|
|
|
mtext.attachment_point = attachment_point;
|
|
|
|
|
|
if base.line_spacing_factor.abs() > 1e-9 {
|
|
|
|
|
|
mtext.line_spacing_factor = base.line_spacing_factor;
|
|
|
|
|
|
}
|
|
|
|
|
|
mtext.normal = base.normal;
|
|
|
|
|
|
mtext.common = base.common.clone();
|
|
|
|
|
|
return Some(EntityType::MText(mtext));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let mut text = Text::with_value(value, pos_f64)
|
|
|
|
|
|
.with_height(text_height)
|
|
|
|
|
|
.with_rotation(rotation);
|
|
|
|
|
|
text.style = style_name;
|
|
|
|
|
|
|
|
|
|
|
|
// Map AttachmentPointType (1..9 grid) to Text horizontal + vertical
|
|
|
|
|
|
// alignments. 1=TopLeft … 9=BottomRight (column-major).
|
|
|
|
|
|
let (ha, va) = attachment_to_text_align(base.attachment_point);
|
|
|
|
|
|
text.horizontal_alignment = ha;
|
|
|
|
|
|
text.vertical_alignment = va;
|
|
|
|
|
|
// line_spacing_factor controls multi-line text spacing in MText. Our
|
|
|
|
|
|
// synthetic Text is single-line so this is a no-op, but pass through
|
|
|
|
|
|
// for completeness.
|
|
|
|
|
|
let _ = base.line_spacing_factor;
|
|
|
|
|
|
// normal would rotate the dim plane out of XY. The local 2D pipeline
|
|
|
|
|
|
// assumes XY, so non-XY normals are read but not applied here.
|
|
|
|
|
|
let _ = base.normal;
|
|
|
|
|
|
|
|
|
|
|
|
text.common = base.common.clone();
|
|
|
|
|
|
Some(EntityType::Text(text))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn attachment_to_text_align(
|
|
|
|
|
|
attach: acadrust::entities::dimension::AttachmentPointType,
|
|
|
|
|
|
) -> (
|
|
|
|
|
|
acadrust::entities::text::TextHorizontalAlignment,
|
|
|
|
|
|
acadrust::entities::text::TextVerticalAlignment,
|
|
|
|
|
|
) {
|
|
|
|
|
|
use acadrust::entities::dimension::AttachmentPointType as A;
|
|
|
|
|
|
use acadrust::entities::text::{TextHorizontalAlignment as H, TextVerticalAlignment as V};
|
|
|
|
|
|
match attach {
|
|
|
|
|
|
A::TopLeft => (H::Left, V::Top),
|
|
|
|
|
|
A::TopCenter => (H::Center, V::Top),
|
|
|
|
|
|
A::TopRight => (H::Right, V::Top),
|
|
|
|
|
|
A::MiddleLeft => (H::Left, V::Middle),
|
|
|
|
|
|
A::MiddleCenter => (H::Center, V::Middle),
|
|
|
|
|
|
A::MiddleRight => (H::Right, V::Middle),
|
|
|
|
|
|
A::BottomLeft => (H::Left, V::Bottom),
|
|
|
|
|
|
A::BottomCenter => (H::Center, V::Bottom),
|
|
|
|
|
|
A::BottomRight => (H::Right, V::Bottom),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-27 17:47:50 +03:00
|
|
|
|
/// Reading rotation for a dimension's measurement text, resolving the style
|
|
|
|
|
|
/// flags the same way the live renderer does: explicit text_rotation, then
|
|
|
|
|
|
/// horizontal_direction, then DIMTIH/DIMTOH force-horizontal, else the natural
|
|
|
|
|
|
/// dim-line angle (+90° for DIMJUST 3/4). Shared by the live text entity and
|
|
|
|
|
|
/// the bake so a reloaded dimension's text reads at the same angle. (#181)
|
|
|
|
|
|
fn dimension_text_rotation(dim: &Dimension, style: Option<&DimStyle>) -> f64 {
|
|
|
|
|
|
let base = dim.base();
|
|
|
|
|
|
let dimtih = style.map(|s| s.dimtih).unwrap_or(false);
|
|
|
|
|
|
let dimtoh = style.map(|s| s.dimtoh).unwrap_or(false);
|
|
|
|
|
|
let dimjust = style.map(|s| s.dimjust).unwrap_or(0);
|
|
|
|
|
|
if base.text_rotation.abs() > 1e-9 {
|
|
|
|
|
|
base.text_rotation
|
|
|
|
|
|
} else if base.horizontal_direction.abs() > 1e-9 {
|
|
|
|
|
|
base.horizontal_direction
|
|
|
|
|
|
} else if dimtih || dimtoh {
|
|
|
|
|
|
0.0
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let mut r = dimension_text_natural_rotation(dim);
|
|
|
|
|
|
if dimjust == 3 || dimjust == 4 {
|
|
|
|
|
|
r += std::f64::consts::FRAC_PI_2;
|
|
|
|
|
|
}
|
|
|
|
|
|
r
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
fn dimension_text_natural_rotation(dim: &Dimension) -> f64 {
|
|
|
|
|
|
let angle = match dim {
|
|
|
|
|
|
Dimension::Linear(d) => d.rotation,
|
|
|
|
|
|
Dimension::Aligned(d) => {
|
|
|
|
|
|
let dx = d.second_point.x - d.first_point.x;
|
|
|
|
|
|
let dy = d.second_point.y - d.first_point.y;
|
|
|
|
|
|
dy.atan2(dx)
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => 0.0,
|
|
|
|
|
|
};
|
|
|
|
|
|
// Clamp to (-π/2, π/2] so text never appears upside-down.
|
|
|
|
|
|
let pi = std::f64::consts::PI;
|
|
|
|
|
|
if angle > pi / 2.0 {
|
|
|
|
|
|
angle - pi
|
|
|
|
|
|
} else if angle <= -pi / 2.0 {
|
|
|
|
|
|
angle + pi
|
|
|
|
|
|
} else {
|
|
|
|
|
|
angle
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-27 17:47:50 +03:00
|
|
|
|
pub(crate) fn dimension_text_value(dim: &Dimension, style: Option<&DimStyle>) -> Option<String> {
|
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 (main, tol) = dimension_text_parts(dim, style)?;
|
|
|
|
|
|
// Tolerance is appended inline for callers (e.g. fill rect width) that
|
|
|
|
|
|
// don't render a separate tolerance entity. The visual pipeline that
|
|
|
|
|
|
// does emit a separate tolerance text re-derives the parts itself.
|
|
|
|
|
|
match tol {
|
|
|
|
|
|
Some(t) => Some(format!("{} {}", main, t)),
|
|
|
|
|
|
None => Some(main),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Returns (primary_text, tolerance_suffix). The tolerance is emitted as a
|
|
|
|
|
|
/// separate Text entity so DIMTFAC can scale its height and DIMTOLJ can
|
|
|
|
|
|
/// align it vertically against the primary value.
|
|
|
|
|
|
fn dimension_text_parts(
|
|
|
|
|
|
dim: &Dimension,
|
|
|
|
|
|
style: Option<&DimStyle>,
|
|
|
|
|
|
) -> Option<(String, Option<String>)> {
|
|
|
|
|
|
let base = dim.base();
|
|
|
|
|
|
let is_angular = matches!(dim, Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_));
|
|
|
|
|
|
|
|
|
|
|
|
// Auto-generated body (the value AutoCAD would emit if the user did not
|
|
|
|
|
|
// override it). Built first so user_text "<>" substitution can re-use it.
|
|
|
|
|
|
let primary_raw = if is_angular {
|
|
|
|
|
|
format_angular_value(dim.measurement(), style)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let v = format_linear_value(dim.measurement(), style);
|
|
|
|
|
|
match dim {
|
|
|
|
|
|
Dimension::Radius(_) => format!("R{}", v),
|
|
|
|
|
|
Dimension::Diameter(_) => format!("Ø{}", v),
|
|
|
|
|
|
_ => v,
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Build tolerance / limits suffix separately so the caller can render
|
|
|
|
|
|
// it as its own Text entity at DIMTFAC × DIMTXT height.
|
|
|
|
|
|
let tolerance_suffix = build_tolerance_suffix(dim.measurement(), style, is_angular);
|
|
|
|
|
|
let primary = apply_dimpost(&primary_raw, style);
|
|
|
|
|
|
|
|
|
|
|
|
// Alternate units appended in brackets when DIMALT is on (linear only).
|
|
|
|
|
|
let primary = if !is_angular {
|
|
|
|
|
|
match alternate_units_text(dim.measurement(), style) {
|
|
|
|
|
|
Some(alt) => format!("{} [{}]", primary, alt),
|
|
|
|
|
|
None => primary,
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
primary
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Explicit user override (mtext-style "user_text") wins, but "<>" inside
|
|
|
|
|
|
// it substitutes the measured value. " " (single space) suppresses text.
|
|
|
|
|
|
if let Some(user_text) = &base.user_text {
|
|
|
|
|
|
if user_text.is_empty() || user_text.trim().is_empty() {
|
|
|
|
|
|
return None;
|
|
|
|
|
|
}
|
|
|
|
|
|
return Some((user_text.replace("<>", &primary), tolerance_suffix));
|
|
|
|
|
|
}
|
|
|
|
|
|
if !base.text.trim().is_empty() {
|
|
|
|
|
|
return Some((base.text.replace("<>", &primary), tolerance_suffix));
|
|
|
|
|
|
}
|
|
|
|
|
|
Some((primary, tolerance_suffix))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn build_tolerance_suffix(
|
|
|
|
|
|
measurement: f64,
|
|
|
|
|
|
style: Option<&DimStyle>,
|
|
|
|
|
|
is_angular: bool,
|
|
|
|
|
|
) -> Option<String> {
|
|
|
|
|
|
let s = style?;
|
|
|
|
|
|
let dimtdec = s.dimtdec.max(0) as usize;
|
|
|
|
|
|
let dimtzin = s.dimtzin;
|
|
|
|
|
|
let fmt = |v: f64| -> String {
|
|
|
|
|
|
let raw = format!("{:.*}", dimtdec, v);
|
|
|
|
|
|
apply_linear_zero_suppression(&raw, dimtzin)
|
|
|
|
|
|
};
|
|
|
|
|
|
if s.dimlim {
|
|
|
|
|
|
let high = measurement + s.dimtp;
|
|
|
|
|
|
let low = measurement - s.dimtm;
|
|
|
|
|
|
return Some(format!("{}/{}", fmt(high), fmt(low)));
|
|
|
|
|
|
}
|
|
|
|
|
|
if s.dimtol {
|
|
|
|
|
|
let unit = if is_angular { "°" } else { "" };
|
|
|
|
|
|
if (s.dimtp - s.dimtm).abs() < 1e-12 && s.dimtp.abs() > 1e-12 {
|
|
|
|
|
|
return Some(format!("±{}{}", fmt(s.dimtp), unit));
|
|
|
|
|
|
}
|
|
|
|
|
|
if s.dimtp.abs() > 1e-12 || s.dimtm.abs() > 1e-12 {
|
2026-06-11 00:04:37 +03:00
|
|
|
|
return Some(format!(
|
|
|
|
|
|
"+{}{} / -{}{}",
|
|
|
|
|
|
fmt(s.dimtp),
|
|
|
|
|
|
unit,
|
|
|
|
|
|
fmt(s.dimtm),
|
|
|
|
|
|
unit
|
|
|
|
|
|
));
|
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
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Build the bracketed alternate-units suffix when DIMALT is enabled.
|
|
|
|
|
|
/// When DIMTOL is also on, the bracketed text includes the tolerance
|
|
|
|
|
|
/// component formatted with DIMALTTD / DIMALTTZ.
|
|
|
|
|
|
fn alternate_units_text(measurement: f64, style: Option<&DimStyle>) -> Option<String> {
|
|
|
|
|
|
let s = style?;
|
|
|
|
|
|
if !s.dimalt {
|
|
|
|
|
|
return None;
|
|
|
|
|
|
}
|
|
|
|
|
|
let mut v = measurement * s.dimaltf;
|
|
|
|
|
|
if s.dimaltrnd > 1e-12 {
|
|
|
|
|
|
v = (v / s.dimaltrnd).round() * s.dimaltrnd;
|
|
|
|
|
|
}
|
|
|
|
|
|
let dec = s.dimaltd.max(0) as usize;
|
2026-06-14 09:42:11 +03:00
|
|
|
|
let raw = format_with_unit(v, s.dimaltu, dec, s.dimfrac, s.dimaltz);
|
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 suppressed = apply_linear_zero_suppression(&raw, s.dimaltz);
|
|
|
|
|
|
let sep_swapped = swap_decimal_sep(&suppressed, s.dimdsep);
|
|
|
|
|
|
// Alt-unit tolerance suffix using DIMALTTD / DIMALTTZ.
|
|
|
|
|
|
let alt_value = if s.dimtol {
|
|
|
|
|
|
let alttdec = s.dimalttd.max(0) as usize;
|
|
|
|
|
|
let alttzin = s.dimalttz;
|
|
|
|
|
|
let fmt = |x: f64| -> String {
|
|
|
|
|
|
let raw = format!("{:.*}", alttdec, x * s.dimaltf);
|
|
|
|
|
|
swap_decimal_sep(&apply_linear_zero_suppression(&raw, alttzin), s.dimdsep)
|
|
|
|
|
|
};
|
|
|
|
|
|
if (s.dimtp - s.dimtm).abs() < 1e-12 && s.dimtp.abs() > 1e-12 {
|
|
|
|
|
|
format!("{}±{}", sep_swapped, fmt(s.dimtp))
|
|
|
|
|
|
} else if s.dimtp.abs() > 1e-12 || s.dimtm.abs() > 1e-12 {
|
|
|
|
|
|
format!("{} +{} / -{}", sep_swapped, fmt(s.dimtp), fmt(s.dimtm))
|
|
|
|
|
|
} else {
|
|
|
|
|
|
sep_swapped
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if s.dimlim {
|
|
|
|
|
|
let alttdec = s.dimalttd.max(0) as usize;
|
|
|
|
|
|
let alttzin = s.dimalttz;
|
|
|
|
|
|
let fmt = |x: f64| -> String {
|
|
|
|
|
|
let raw = format!("{:.*}", alttdec, x * s.dimaltf);
|
|
|
|
|
|
swap_decimal_sep(&apply_linear_zero_suppression(&raw, alttzin), s.dimdsep)
|
|
|
|
|
|
};
|
2026-06-11 00:04:37 +03:00
|
|
|
|
format!(
|
|
|
|
|
|
"{}/{}",
|
|
|
|
|
|
fmt(measurement + s.dimtp),
|
|
|
|
|
|
fmt(measurement - s.dimtm)
|
|
|
|
|
|
)
|
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
|
|
|
|
} else {
|
|
|
|
|
|
sep_swapped
|
|
|
|
|
|
};
|
|
|
|
|
|
// DIMAPOST wraps the alt value (same "<>" convention as DIMPOST).
|
|
|
|
|
|
let wrapped = if s.dimapost.is_empty() {
|
|
|
|
|
|
alt_value
|
|
|
|
|
|
} else if s.dimapost.contains("<>") {
|
|
|
|
|
|
s.dimapost.replace("<>", &alt_value)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
format!("{}{}", alt_value, s.dimapost)
|
|
|
|
|
|
};
|
|
|
|
|
|
Some(wrapped)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Build the secondary tolerance Text entity at `DIMTXT × DIMTFAC` height,
|
|
|
|
|
|
/// positioned to the right of the primary text and vertically aligned per
|
|
|
|
|
|
/// `DIMTOLJ` (0=bottom, 1=middle, 2=top). Returns None when DIMTOL/DIMLIM
|
|
|
|
|
|
/// produce no tolerance string (e.g. both DIMTP and DIMTM are zero).
|
|
|
|
|
|
fn dimension_tolerance_entity(
|
|
|
|
|
|
dim: &Dimension,
|
|
|
|
|
|
style: Option<&DimStyle>,
|
|
|
|
|
|
primary: &EntityType,
|
|
|
|
|
|
primary_height: f64,
|
|
|
|
|
|
) -> Option<EntityType> {
|
|
|
|
|
|
let s = style?;
|
|
|
|
|
|
let is_angular = matches!(dim, Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_));
|
|
|
|
|
|
let tol = build_tolerance_suffix(dim.measurement(), style, is_angular)?;
|
|
|
|
|
|
let dimtfac = if s.dimtfac.abs() < 1e-12 {
|
|
|
|
|
|
1.0
|
|
|
|
|
|
} else {
|
|
|
|
|
|
s.dimtfac
|
|
|
|
|
|
};
|
|
|
|
|
|
let tol_height = primary_height * dimtfac;
|
|
|
|
|
|
|
|
|
|
|
|
// Pull the geometry we need from the synthetic primary entity (Text or
|
|
|
|
|
|
// MText — `dimension_text_entity` routes to MText when the dim value
|
|
|
|
|
|
// carries inline format codes).
|
|
|
|
|
|
let (primary_value_len, primary_insertion, primary_rotation, primary_style, primary_common) =
|
|
|
|
|
|
match primary {
|
|
|
|
|
|
EntityType::Text(t) => (
|
|
|
|
|
|
t.value.chars().count(),
|
|
|
|
|
|
t.insertion_point,
|
|
|
|
|
|
t.rotation,
|
|
|
|
|
|
t.style.clone(),
|
|
|
|
|
|
t.common.clone(),
|
|
|
|
|
|
),
|
|
|
|
|
|
EntityType::MText(m) => (
|
|
|
|
|
|
m.value.chars().count(),
|
|
|
|
|
|
m.insertion_point,
|
|
|
|
|
|
m.rotation,
|
|
|
|
|
|
m.style.clone(),
|
|
|
|
|
|
m.common.clone(),
|
|
|
|
|
|
),
|
|
|
|
|
|
_ => return None,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Approximate widths from glyph counts (~0.6 × cell size per char).
|
|
|
|
|
|
let primary_w = primary_value_len as f64 * primary_height * 0.6;
|
|
|
|
|
|
let tol_w = tol.chars().count() as f64 * tol_height * 0.6;
|
|
|
|
|
|
let gap = primary_height * 0.2;
|
|
|
|
|
|
let dx_local = primary_w * 0.5 + tol_w * 0.5 + gap;
|
|
|
|
|
|
let dy_local = match s.dimtolj {
|
|
|
|
|
|
0 => -primary_height * 0.5 + tol_height * 0.5, // bottom-aligned with primary baseline
|
|
|
|
|
|
2 => primary_height * 0.5 - tol_height * 0.5, // top-aligned with primary top
|
2026-06-11 00:04:37 +03:00
|
|
|
|
_ => 0.0, // centred (default for ±)
|
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 rot = primary_rotation;
|
|
|
|
|
|
let (sr, cr) = rot.sin_cos();
|
|
|
|
|
|
let pos = Vector3::new(
|
|
|
|
|
|
primary_insertion.x + dx_local * cr - dy_local * sr,
|
|
|
|
|
|
primary_insertion.y + dx_local * sr + dy_local * cr,
|
|
|
|
|
|
primary_insertion.z,
|
|
|
|
|
|
);
|
|
|
|
|
|
let mut t = Text::with_value(tol, pos)
|
|
|
|
|
|
.with_height(tol_height)
|
|
|
|
|
|
.with_rotation(rot);
|
|
|
|
|
|
t.style = primary_style;
|
|
|
|
|
|
t.common = primary_common;
|
|
|
|
|
|
t.horizontal_alignment = acadrust::entities::text::TextHorizontalAlignment::Center;
|
|
|
|
|
|
t.vertical_alignment = acadrust::entities::text::TextVerticalAlignment::Middle;
|
|
|
|
|
|
Some(EntityType::Text(t))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Wrap a measured value with the style's DIMPOST prefix/suffix template.
|
|
|
|
|
|
/// "<>" inside DIMPOST is replaced by the value; absent "<>" appends.
|
|
|
|
|
|
fn apply_dimpost(value: &str, style: Option<&DimStyle>) -> String {
|
|
|
|
|
|
let post = style.map(|s| s.dimpost.as_str()).unwrap_or("");
|
|
|
|
|
|
if post.is_empty() {
|
|
|
|
|
|
return value.to_string();
|
|
|
|
|
|
}
|
|
|
|
|
|
if post.contains("<>") {
|
|
|
|
|
|
post.replace("<>", value)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
format!("{}{}", value, post)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Format a linear measurement honouring DIMLFAC, DIMRND, DIMDEC, DIMZIN, DIMDSEP, DIMLUNIT.
|
|
|
|
|
|
fn format_linear_value(measurement: f64, style: Option<&DimStyle>) -> String {
|
|
|
|
|
|
let (dec, zin, lfac, rnd, dsep, lunit, frac) = style
|
|
|
|
|
|
.map(|s| {
|
|
|
|
|
|
(
|
|
|
|
|
|
s.dimdec, s.dimzin, s.dimlfac, s.dimrnd, s.dimdsep, s.dimlunit, s.dimfrac,
|
|
|
|
|
|
)
|
|
|
|
|
|
})
|
|
|
|
|
|
.unwrap_or((4, 8, 1.0, 0.0, 46, 2, 0));
|
|
|
|
|
|
|
|
|
|
|
|
let lfac = if lfac.abs() < 1e-12 { 1.0 } else { lfac };
|
|
|
|
|
|
let mut v = measurement * lfac;
|
|
|
|
|
|
if rnd > 1e-12 {
|
|
|
|
|
|
v = (v / rnd).round() * rnd;
|
|
|
|
|
|
}
|
|
|
|
|
|
let dec = dec.max(0) as usize;
|
2026-06-14 09:42:11 +03:00
|
|
|
|
let raw = format_with_unit(v, lunit, dec, frac, zin);
|
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 suppressed = apply_linear_zero_suppression(&raw, zin);
|
|
|
|
|
|
swap_decimal_sep(&suppressed, dsep)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Dispatch on DIMLUNIT / DIMALTU.
|
|
|
|
|
|
/// 1 = Scientific
|
|
|
|
|
|
/// 2 = Decimal (default)
|
|
|
|
|
|
/// 3 = Engineering (feet + decimal inches; 1 unit = 1 inch)
|
|
|
|
|
|
/// 4 = Architectural (feet + fractional inches)
|
|
|
|
|
|
/// 5 = Fractional (integer + fractional inches)
|
|
|
|
|
|
/// 6 = Windows desktop → falls back to Decimal
|
|
|
|
|
|
/// `dimfrac` controls denominator power for arch/fractional output (0/1/2);
|
|
|
|
|
|
/// rendered inline as "n/d" (stacked glyphs require MText support).
|
2026-06-14 09:42:11 +03:00
|
|
|
|
fn format_with_unit(value: f64, unit: i16, dec: usize, dimfrac: i16, zin: i16) -> String {
|
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 unit {
|
|
|
|
|
|
1 => format!("{:.*e}", dec, value),
|
|
|
|
|
|
3 => format_engineering(value, dec),
|
2026-06-14 09:42:11 +03:00
|
|
|
|
4 => format_architectural(value, dimfrac, zin),
|
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
|
|
|
|
5 => format_fractional(value, dimfrac),
|
|
|
|
|
|
_ => format!("{:.*}", dec, value),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn format_engineering(inches: f64, dec: usize) -> String {
|
|
|
|
|
|
let sign = if inches < 0.0 { "-" } else { "" };
|
|
|
|
|
|
let abs = inches.abs();
|
|
|
|
|
|
let feet = (abs / 12.0).trunc();
|
|
|
|
|
|
let rem_in = abs - feet * 12.0;
|
|
|
|
|
|
format!("{}{:.0}'-{:.*}\"", sign, feet, dec, rem_in)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-14 09:42:11 +03:00
|
|
|
|
fn format_architectural(inches: f64, dimfrac: i16, zin: i16) -> String {
|
2026-07-14 07:31:09 -05:00
|
|
|
|
let denom = arch_denom(dimfrac);
|
|
|
|
|
|
// Round to the nearest 1/denom inch *first*, in integer ticks, so a fraction
|
|
|
|
|
|
// that rounds up to a whole inch carries into the inches — and on into the
|
|
|
|
|
|
// feet — instead of printing an un-reduced "11 1/1". A 3ft object that
|
|
|
|
|
|
// measures 35.9999" (float) now reads 3'-0", not 2'-11 1".
|
|
|
|
|
|
let ticks = (inches.abs() * denom as f64).round() as u64;
|
|
|
|
|
|
let sign = if inches < 0.0 && ticks != 0 { "-" } else { "" };
|
|
|
|
|
|
let per_foot = 12 * denom;
|
|
|
|
|
|
let feet = ticks / per_foot;
|
|
|
|
|
|
let rem = ticks % per_foot;
|
|
|
|
|
|
let whole = rem / denom;
|
|
|
|
|
|
let frac_str = reduce_fraction(rem % denom, denom);
|
2026-06-14 09:42:11 +03:00
|
|
|
|
|
|
|
|
|
|
// DIMZIN feet/inch suppression:
|
|
|
|
|
|
// 0 suppress zero feet & zero inches, 1 include both,
|
|
|
|
|
|
// 2 include zero feet / suppress zero inches,
|
|
|
|
|
|
// 3 suppress zero feet / include zero inches.
|
|
|
|
|
|
let suppress_zero_feet = zin == 0 || zin == 3;
|
|
|
|
|
|
let suppress_zero_inches = zin == 0 || zin == 2;
|
2026-07-14 07:31:09 -05:00
|
|
|
|
let feet_zero = feet == 0;
|
|
|
|
|
|
let inches_zero = whole == 0 && frac_str.is_empty();
|
2026-06-14 09:42:11 +03:00
|
|
|
|
let show_feet = !feet_zero || !suppress_zero_feet;
|
|
|
|
|
|
let show_inches = !inches_zero || !suppress_zero_inches;
|
|
|
|
|
|
|
|
|
|
|
|
let feet_part = if show_feet {
|
2026-07-14 07:31:09 -05:00
|
|
|
|
format!("{}'", feet)
|
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
|
|
|
|
} else {
|
2026-06-14 09:42:11 +03:00
|
|
|
|
String::new()
|
|
|
|
|
|
};
|
|
|
|
|
|
let inch_part = if show_inches {
|
|
|
|
|
|
if frac_str.is_empty() {
|
2026-07-14 07:31:09 -05:00
|
|
|
|
format!("{}\"", whole)
|
2026-06-14 09:42:11 +03:00
|
|
|
|
} else {
|
2026-07-14 07:31:09 -05:00
|
|
|
|
format!("{} {}\"", whole, frac_str)
|
2026-06-14 09:42:11 +03:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
String::new()
|
|
|
|
|
|
};
|
|
|
|
|
|
let body = match (feet_part.is_empty(), inch_part.is_empty()) {
|
|
|
|
|
|
(false, false) => format!("{}-{}", feet_part, inch_part),
|
|
|
|
|
|
(false, true) => feet_part,
|
|
|
|
|
|
(true, false) => inch_part,
|
|
|
|
|
|
(true, true) => "0\"".to_string(),
|
|
|
|
|
|
};
|
|
|
|
|
|
format!("{}{}", sign, body)
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn format_fractional(value: f64, dimfrac: i16) -> String {
|
2026-07-14 07:31:09 -05:00
|
|
|
|
let denom = arch_denom(dimfrac);
|
|
|
|
|
|
// Round on the fraction grid first (same carry reasoning as architectural).
|
|
|
|
|
|
let ticks = (value.abs() * denom as f64).round() as u64;
|
|
|
|
|
|
let sign = if value < 0.0 && ticks != 0 { "-" } else { "" };
|
|
|
|
|
|
let whole = ticks / denom;
|
|
|
|
|
|
let frac_str = reduce_fraction(ticks % denom, denom);
|
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
|
|
|
|
if frac_str.is_empty() {
|
2026-07-14 07:31:09 -05:00
|
|
|
|
format!("{}{}", sign, whole)
|
|
|
|
|
|
} else if whole == 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
|
|
|
|
format!("{}{}", sign, frac_str)
|
|
|
|
|
|
} else {
|
2026-07-14 07:31:09 -05:00
|
|
|
|
format!("{}{} {}", sign, whole, frac_str)
|
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-07-14 07:31:09 -05:00
|
|
|
|
/// Power-of-two denominator for architectural / fractional output. DIMFRAC is
|
|
|
|
|
|
/// the exponent-ish knob AutoCAD pairs with DIMLUNIT; clamp it to a readable
|
|
|
|
|
|
/// range (16ths … 1024ths).
|
|
|
|
|
|
fn arch_denom(dimfrac: i16) -> u64 {
|
|
|
|
|
|
let exp = (dimfrac.clamp(0, 8) as u32).max(2) + 2; // 4..=10 → 16..=1024
|
|
|
|
|
|
1u64 << exp
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Reduce a power-of-two fraction numer/denom to display form. Empty for a zero
|
|
|
|
|
|
/// numerator. Callers strip the whole units first, so `numer < denom` and the
|
|
|
|
|
|
/// reduced denominator is always ≥ 2 — the carry that used to leak out as a bare
|
|
|
|
|
|
/// "1/1" is handled upstream now.
|
|
|
|
|
|
fn reduce_fraction(mut n: u64, mut d: u64) -> String {
|
|
|
|
|
|
if n == 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
|
|
|
|
return String::new();
|
|
|
|
|
|
}
|
|
|
|
|
|
while n % 2 == 0 && d % 2 == 0 {
|
|
|
|
|
|
n /= 2;
|
|
|
|
|
|
d /= 2;
|
|
|
|
|
|
}
|
2026-07-14 07:31:09 -05:00
|
|
|
|
if d == 1 {
|
|
|
|
|
|
format!("{}", n)
|
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
|
|
|
|
} else {
|
|
|
|
|
|
format!("{}/{}", n, d)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Format an angular measurement (input in degrees as Dimension::measurement
|
|
|
|
|
|
/// returns for angular variants) honouring DIMAUNIT, DIMADEC, DIMAZIN.
|
|
|
|
|
|
fn format_angular_value(measurement_deg: f64, style: Option<&DimStyle>) -> String {
|
|
|
|
|
|
let (aunit, adec, azin) = style
|
|
|
|
|
|
.map(|s| (s.dimaunit, s.dimadec, s.dimazin))
|
|
|
|
|
|
.unwrap_or((0, 2, 0));
|
|
|
|
|
|
let adec = adec.max(0) as usize;
|
|
|
|
|
|
|
|
|
|
|
|
match aunit {
|
|
|
|
|
|
// 1 = Degrees / Minutes / Seconds
|
|
|
|
|
|
1 => format_dms(measurement_deg, adec, azin),
|
|
|
|
|
|
// 2 = Gradians
|
|
|
|
|
|
2 => {
|
|
|
|
|
|
let g = measurement_deg / 0.9;
|
|
|
|
|
|
let raw = format!("{:.*}", adec, g);
|
|
|
|
|
|
format!("{}g", apply_angular_zero_suppression(&raw, azin))
|
|
|
|
|
|
}
|
|
|
|
|
|
// 3 = Radians
|
|
|
|
|
|
3 => {
|
|
|
|
|
|
let r = measurement_deg.to_radians();
|
|
|
|
|
|
let raw = format!("{:.*}", adec, r);
|
|
|
|
|
|
format!("{}r", apply_angular_zero_suppression(&raw, azin))
|
|
|
|
|
|
}
|
|
|
|
|
|
// 0 or unknown = Decimal Degrees
|
|
|
|
|
|
_ => {
|
|
|
|
|
|
let raw = format!("{:.*}", adec, measurement_deg);
|
|
|
|
|
|
format!("{}°", apply_angular_zero_suppression(&raw, azin))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn format_dms(deg: f64, sec_dec: usize, azin: i16) -> String {
|
|
|
|
|
|
let sign = if deg < 0.0 { "-" } else { "" };
|
|
|
|
|
|
let abs = deg.abs();
|
|
|
|
|
|
let d = abs.floor();
|
|
|
|
|
|
let m_full = (abs - d) * 60.0;
|
|
|
|
|
|
let m = m_full.floor();
|
|
|
|
|
|
let s = (m_full - m) * 60.0;
|
|
|
|
|
|
let s_str = format!("{:.*}", sec_dec, s);
|
|
|
|
|
|
let mut out = format!("{}{:.0}°{:.0}'{}\"", sign, d, m, s_str);
|
|
|
|
|
|
if azin & 4 != 0 {
|
|
|
|
|
|
// suppress 0° / 0' parts
|
|
|
|
|
|
if d == 0.0 {
|
|
|
|
|
|
out = out.trim_start_matches('0').to_string();
|
|
|
|
|
|
out = out.replacen("°", "", 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Apply DIMZIN bit flags to a formatted linear value.
|
|
|
|
|
|
/// bit 0 (1) suppress 0' (imperial feet) — not applicable for decimal
|
|
|
|
|
|
/// bit 1 (2) suppress 0" (imperial inches) — not applicable for decimal
|
|
|
|
|
|
/// bit 2 (4) suppress leading zeros (e.g. ".5" not "0.5")
|
|
|
|
|
|
/// bit 3 (8) suppress trailing zeros (e.g. "1.5" not "1.50")
|
|
|
|
|
|
/// Default = 8 (trailing-zero suppression on).
|
|
|
|
|
|
fn apply_linear_zero_suppression(s: &str, zin: i16) -> String {
|
|
|
|
|
|
let mut out = s.to_string();
|
|
|
|
|
|
if zin & 8 != 0 {
|
|
|
|
|
|
out = strip_trailing_zeros(&out);
|
|
|
|
|
|
}
|
|
|
|
|
|
if zin & 4 != 0 {
|
|
|
|
|
|
out = strip_leading_zero(&out);
|
|
|
|
|
|
}
|
|
|
|
|
|
out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn apply_angular_zero_suppression(s: &str, azin: i16) -> String {
|
|
|
|
|
|
// DIMAZIN: 0=neither, 1=leading, 2=trailing, 3=both.
|
|
|
|
|
|
let mut out = s.to_string();
|
|
|
|
|
|
if azin & 2 != 0 {
|
|
|
|
|
|
out = strip_trailing_zeros(&out);
|
|
|
|
|
|
}
|
|
|
|
|
|
if azin & 1 != 0 {
|
|
|
|
|
|
out = strip_leading_zero(&out);
|
|
|
|
|
|
}
|
|
|
|
|
|
out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn strip_trailing_zeros(s: &str) -> String {
|
|
|
|
|
|
if !s.contains('.') {
|
|
|
|
|
|
return s.to_string();
|
|
|
|
|
|
}
|
|
|
|
|
|
let trimmed = s.trim_end_matches('0').trim_end_matches('.');
|
|
|
|
|
|
if trimmed.is_empty() || trimmed == "-" {
|
|
|
|
|
|
"0".to_string()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
trimmed.to_string()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn strip_leading_zero(s: &str) -> String {
|
|
|
|
|
|
// "0.5" → ".5", "-0.5" → "-.5", "0" stays.
|
|
|
|
|
|
if let Some(rest) = s.strip_prefix("-0.") {
|
|
|
|
|
|
return format!("-.{rest}");
|
|
|
|
|
|
}
|
|
|
|
|
|
if let Some(rest) = s.strip_prefix("0.") {
|
|
|
|
|
|
return format!(".{rest}");
|
|
|
|
|
|
}
|
|
|
|
|
|
s.to_string()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn swap_decimal_sep(s: &str, dsep_code: i16) -> String {
|
|
|
|
|
|
// DIMDSEP holds an ASCII code (0 means default '.'). 46='.', 44=',', etc.
|
|
|
|
|
|
if dsep_code <= 0 || dsep_code == 46 {
|
|
|
|
|
|
return s.to_string();
|
|
|
|
|
|
}
|
|
|
|
|
|
let ch = char::from_u32(dsep_code as u32).unwrap_or('.');
|
|
|
|
|
|
s.replace('.', &ch.to_string())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
refactor: remove dead world_offset plumbing (RTE migration is complete)
Now that geometry reaches the GPU/CPU as absolute coordinates via the
double-single relative-to-eye path, world_offset is always [0,0,0]. Strip
the parameter that was threaded through the whole tessellation / block-
expansion / fallback / camera-decode chain and the ExpandCtx field that
carried it — ~340 references across 20 files, all subtracting zero.
- truck_tess: to_local / to_local_low / tessellate_* drop the offset arg;
to_local is now a pure double-single split.
- tessellate: offset_to_ds → points_to_ds (pure DS split); tessellate(),
tessellate_entity(), fallback_geometry(), solid_wire_fallback(),
entity_aabb(), expand_insert(), expand_block_meshes(), the dimension
helpers (vec3_local, dimension_snap_pts, …), text_support, leader,
multileader, image_model, xclip and camera_from_view all lose the param.
- block_cache: ExpandCtx loses its world_offset field.
- Remove the now-dead offset_snap_pts and the unused set_grid_snap.
Behaviour is unchanged (the offset was zero everywhere); the compiler
verifies every call site.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 18:35:02 +03:00
|
|
|
|
fn dimension_text_position(dim: &Dimension) -> Vec3 {
|
|
|
|
|
|
let lv = |v| vec3_local(v);
|
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 base = dim.base();
|
|
|
|
|
|
let pos = lv(base.text_middle_point);
|
|
|
|
|
|
if pos.length_squared() > 1e-8 {
|
|
|
|
|
|
return pos;
|
|
|
|
|
|
}
|
|
|
|
|
|
match dim {
|
|
|
|
|
|
Dimension::Aligned(d) => (lv(d.first_point) + lv(d.second_point)) * 0.5,
|
|
|
|
|
|
Dimension::Linear(d) => (lv(d.first_point) + lv(d.second_point)) * 0.5,
|
|
|
|
|
|
Dimension::Radius(d) => (lv(d.angle_vertex) + lv(d.definition_point)) * 0.5,
|
|
|
|
|
|
Dimension::Diameter(d) => (lv(d.angle_vertex) + lv(d.definition_point)) * 0.5,
|
|
|
|
|
|
Dimension::Angular2Ln(d) => lv(d.dimension_arc),
|
|
|
|
|
|
Dimension::Angular3Pt(d) => lv(d.definition_point),
|
|
|
|
|
|
Dimension::Ordinate(d) => lv(d.leader_endpoint),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
refactor: remove dead world_offset plumbing (RTE migration is complete)
Now that geometry reaches the GPU/CPU as absolute coordinates via the
double-single relative-to-eye path, world_offset is always [0,0,0]. Strip
the parameter that was threaded through the whole tessellation / block-
expansion / fallback / camera-decode chain and the ExpandCtx field that
carried it — ~340 references across 20 files, all subtracting zero.
- truck_tess: to_local / to_local_low / tessellate_* drop the offset arg;
to_local is now a pure double-single split.
- tessellate: offset_to_ds → points_to_ds (pure DS split); tessellate(),
tessellate_entity(), fallback_geometry(), solid_wire_fallback(),
entity_aabb(), expand_insert(), expand_block_meshes(), the dimension
helpers (vec3_local, dimension_snap_pts, …), text_support, leader,
multileader, image_model, xclip and camera_from_view all lose the param.
- block_cache: ExpandCtx loses its world_offset field.
- Remove the now-dead offset_snap_pts and the unused set_grid_snap.
Behaviour is unchanged (the offset was zero everywhere); the compiler
verifies every call site.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 18:35:02 +03:00
|
|
|
|
fn vec3_local(v: Vector3) -> Vec3 {
|
|
|
|
|
|
Vec3::new(v.x as f32, v.y as f32, v.z as f32)
|
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-06-14 01:57:58 +03:00
|
|
|
|
/// Style-driven text anchor on the dimension line: a point on the line through
|
|
|
|
|
|
/// `defpt` parallel to (`ax`,`ay`), slid along it per DIMJUST and lifted by
|
|
|
|
|
|
/// `perp_off` toward the side the dimension line sits on.
|
2026-06-14 02:03:05 +03:00
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
2026-06-14 01:57:58 +03:00
|
|
|
|
fn text_on_dim_line(
|
|
|
|
|
|
first: Vector3,
|
|
|
|
|
|
second: Vector3,
|
|
|
|
|
|
defpt: Vector3,
|
|
|
|
|
|
ax: f64,
|
|
|
|
|
|
ay: f64,
|
|
|
|
|
|
dimjust: i16,
|
|
|
|
|
|
perp_off: f64,
|
2026-06-14 02:03:05 +03:00
|
|
|
|
text_w: f64,
|
|
|
|
|
|
arrow: f64,
|
|
|
|
|
|
dimtix: bool,
|
2026-06-20 17:38:57 +03:00
|
|
|
|
tad: i16,
|
2026-06-14 01:57:58 +03:00
|
|
|
|
) -> Vector3 {
|
2026-06-20 17:38:57 +03:00
|
|
|
|
// The perpendicular "up" side: the perpendicular of the dimension line
|
|
|
|
|
|
// normalised so the text reads left-to-right (or bottom-to-top when
|
|
|
|
|
|
// vertical). DIMTAD "above" (1) and JIS (3) sit on this side.
|
2026-06-20 17:32:56 +03:00
|
|
|
|
let (mut nx, mut ny) = (ax, ay);
|
|
|
|
|
|
if nx < 0.0 || (nx == 0.0 && ny < 0.0) {
|
|
|
|
|
|
nx = -nx;
|
|
|
|
|
|
ny = -ny;
|
|
|
|
|
|
}
|
|
|
|
|
|
let px = -ny;
|
|
|
|
|
|
let py = nx;
|
2026-06-20 17:38:57 +03:00
|
|
|
|
// DIMTAD side:
|
|
|
|
|
|
// 1 (above) / 3 (JIS) / 0 (centred) → the text-up side, independent of the
|
|
|
|
|
|
// object — keying it off the object flipped "above" to "below" whenever
|
|
|
|
|
|
// the dimension ran the opposite way (#144);
|
|
|
|
|
|
// 4 (below) → the opposite side;
|
|
|
|
|
|
// 2 (outside) → the side farthest from the defining points (the object).
|
|
|
|
|
|
let perp_sign = match tad {
|
|
|
|
|
|
4 => -1.0,
|
|
|
|
|
|
2 => {
|
|
|
|
|
|
let off = (defpt.x - first.x) * px + (defpt.y - first.y) * py;
|
|
|
|
|
|
if off >= 0.0 {
|
|
|
|
|
|
1.0
|
|
|
|
|
|
} else {
|
|
|
|
|
|
-1.0
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => 1.0,
|
|
|
|
|
|
};
|
2026-06-14 01:57:58 +03:00
|
|
|
|
// Along-axis positions of the extension points relative to the dim line.
|
|
|
|
|
|
let t1 = (first.x - defpt.x) * ax + (first.y - defpt.y) * ay;
|
|
|
|
|
|
let t2 = (second.x - defpt.x) * ax + (second.y - defpt.y) * ay;
|
|
|
|
|
|
// DIMJUST: 0=centred, 1/3=near first ext, 2/4=near second ext.
|
2026-06-14 02:03:05 +03:00
|
|
|
|
let mut along = match dimjust {
|
2026-06-14 01:57:58 +03:00
|
|
|
|
1 | 3 => t1,
|
|
|
|
|
|
2 | 4 => t2,
|
|
|
|
|
|
_ => (t1 + t2) * 0.5,
|
|
|
|
|
|
};
|
2026-06-14 02:03:05 +03:00
|
|
|
|
// DIMATFIT / DIMTIX fit: when centred text can't fit between the extension
|
|
|
|
|
|
// lines, slide it just past the far one (text-outside placement) unless
|
|
|
|
|
|
// DIMTIX forces it to stay inside.
|
|
|
|
|
|
if dimjust == 0 && !dimtix && text_w > 0.0 {
|
|
|
|
|
|
let lo = t1.min(t2);
|
|
|
|
|
|
let hi = t1.max(t2);
|
|
|
|
|
|
if text_w > (hi - lo) - 2.0 * arrow {
|
|
|
|
|
|
along = hi + arrow + text_w * 0.5;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-14 01:57:58 +03:00
|
|
|
|
let bx = defpt.x + ax * along;
|
|
|
|
|
|
let by = defpt.y + ay * along;
|
|
|
|
|
|
Vector3::new(
|
|
|
|
|
|
bx + px * perp_off * perp_sign,
|
|
|
|
|
|
by + py * perp_off * perp_sign,
|
|
|
|
|
|
defpt.z,
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-17 05:32:31 +03:00
|
|
|
|
fn dimension_text_pos_f64(
|
|
|
|
|
|
dim: &Dimension,
|
|
|
|
|
|
style: Option<&DimStyle>,
|
|
|
|
|
|
text_height: f64,
|
|
|
|
|
|
dim_scale: f64,
|
|
|
|
|
|
) -> Vector3 {
|
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 base = dim.base();
|
|
|
|
|
|
|
2026-06-14 09:42:11 +03:00
|
|
|
|
// DIMTAD: 0=centred (on the line), 1=above, 4=below. 2 (outside, i.e. the
|
|
|
|
|
|
// side farthest from the defining points) and 3 (JIS) both resolve to the
|
|
|
|
|
|
// away-from-object side, which is the same as "above" for 2-D linear dims.
|
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 dimtad = style.map(|s| s.dimtad).unwrap_or(1);
|
2026-06-17 05:32:31 +03:00
|
|
|
|
// DIMGAP scales with DIMSCALE just like DIMTXT, so the text-to-line gap
|
|
|
|
|
|
// stays consistent when DIMSCALE != 1.
|
|
|
|
|
|
let dimgap = style.map(|s| s.dimgap).unwrap_or(0.0) * dim_scale;
|
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 dimjust = style.map(|s| s.dimjust).unwrap_or(0);
|
2026-06-14 02:03:05 +03:00
|
|
|
|
// DIMTIX forces the text to stay between the extension lines.
|
|
|
|
|
|
let dimtix = style.map(|s| s.dimtix).unwrap_or(false);
|
2026-06-14 01:57:58 +03:00
|
|
|
|
// DIMTVP vertical-position multiplier (units of dimtxt). Only honoured when
|
|
|
|
|
|
// DIMTAD == 0; offsets text perpendicular to the dim line.
|
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 dimtvp = style.map(|s| s.dimtvp).unwrap_or(0.0);
|
2026-06-14 01:57:58 +03:00
|
|
|
|
let perp_off = if dimtad == 0 {
|
|
|
|
|
|
dimtvp * text_height
|
|
|
|
|
|
} else {
|
|
|
|
|
|
text_height * 0.5 + dimgap
|
|
|
|
|
|
};
|
2026-06-14 02:03:05 +03:00
|
|
|
|
// Rough text width + arrow allowance, used to decide text-outside fit.
|
|
|
|
|
|
let text_w = dimension_text_value(dim, style)
|
|
|
|
|
|
.map(|t| t.chars().count() as f64 * text_height * 0.6 + 2.0 * dimgap)
|
|
|
|
|
|
.unwrap_or(0.0);
|
|
|
|
|
|
let arrow = text_height; // arrows are roughly text-height sized
|
2026-06-14 01:57:58 +03:00
|
|
|
|
|
|
|
|
|
|
// Explicit per-entity override (text dragged to a custom location): the
|
|
|
|
|
|
// saved point wins. Otherwise the dimension style governs placement.
|
|
|
|
|
|
let use_saved = base.text_user_positioned && {
|
|
|
|
|
|
let p = base.text_middle_point;
|
|
|
|
|
|
p.x * p.x + p.y * p.y + p.z * p.z > 1e-16
|
|
|
|
|
|
};
|
|
|
|
|
|
if use_saved {
|
|
|
|
|
|
return base.text_middle_point;
|
|
|
|
|
|
}
|
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-06-14 01:57:58 +03:00
|
|
|
|
match dim {
|
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
|
|
|
|
Dimension::Linear(d) => {
|
|
|
|
|
|
let ax = d.rotation.cos();
|
|
|
|
|
|
let ay = d.rotation.sin();
|
2026-06-14 01:57:58 +03:00
|
|
|
|
text_on_dim_line(
|
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
|
|
|
|
d.first_point,
|
|
|
|
|
|
d.second_point,
|
2026-06-14 01:57:58 +03:00
|
|
|
|
d.definition_point,
|
|
|
|
|
|
ax,
|
|
|
|
|
|
ay,
|
|
|
|
|
|
dimjust,
|
|
|
|
|
|
perp_off,
|
2026-06-14 02:03:05 +03:00
|
|
|
|
text_w,
|
|
|
|
|
|
arrow,
|
|
|
|
|
|
dimtix,
|
2026-06-20 17:38:57 +03:00
|
|
|
|
dimtad,
|
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
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
Dimension::Aligned(d) => {
|
|
|
|
|
|
let dx = d.second_point.x - d.first_point.x;
|
|
|
|
|
|
let dy = d.second_point.y - d.first_point.y;
|
|
|
|
|
|
let len = (dx * dx + dy * dy).sqrt().max(1e-12);
|
2026-06-14 01:57:58 +03:00
|
|
|
|
text_on_dim_line(
|
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
|
|
|
|
d.first_point,
|
|
|
|
|
|
d.second_point,
|
2026-06-14 01:57:58 +03:00
|
|
|
|
d.definition_point,
|
|
|
|
|
|
dx / len,
|
|
|
|
|
|
dy / len,
|
|
|
|
|
|
dimjust,
|
|
|
|
|
|
perp_off,
|
2026-06-14 02:03:05 +03:00
|
|
|
|
text_w,
|
|
|
|
|
|
arrow,
|
|
|
|
|
|
dimtix,
|
2026-06-20 17:38:57 +03:00
|
|
|
|
dimtad,
|
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-06-27 17:47:50 +03:00
|
|
|
|
// Non-linear (radius / diameter / angular / ordinate): lift the
|
|
|
|
|
|
// natural mid point straight up by the style offset. A user-dragged
|
|
|
|
|
|
// text point is already returned by the `use_saved` gate above, so
|
|
|
|
|
|
// we must NOT short-circuit on a merely-nonzero text_middle_point
|
|
|
|
|
|
// here — that would ignore the style placement for auto-placed dims
|
|
|
|
|
|
// and make a re-style a no-op. (#181)
|
2026-06-14 01:57:58 +03:00
|
|
|
|
let mid = match dim {
|
|
|
|
|
|
Dimension::Radius(d) => Vector3::new(
|
|
|
|
|
|
(d.angle_vertex.x + d.definition_point.x) * 0.5,
|
|
|
|
|
|
(d.angle_vertex.y + d.definition_point.y) * 0.5,
|
|
|
|
|
|
(d.angle_vertex.z + d.definition_point.z) * 0.5,
|
|
|
|
|
|
),
|
|
|
|
|
|
Dimension::Diameter(d) => Vector3::new(
|
|
|
|
|
|
(d.angle_vertex.x + d.definition_point.x) * 0.5,
|
|
|
|
|
|
(d.angle_vertex.y + d.definition_point.y) * 0.5,
|
|
|
|
|
|
(d.angle_vertex.z + d.definition_point.z) * 0.5,
|
|
|
|
|
|
),
|
|
|
|
|
|
Dimension::Angular2Ln(d) => d.dimension_arc,
|
|
|
|
|
|
Dimension::Angular3Pt(d) => d.definition_point,
|
|
|
|
|
|
Dimension::Ordinate(d) => d.leader_endpoint,
|
|
|
|
|
|
_ => base.text_middle_point,
|
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-06-14 01:57:58 +03:00
|
|
|
|
Vector3::new(mid.x, mid.y + perp_off * perp_sign_default(), mid.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
|
|
|
|
}
|
2026-06-14 01:57:58 +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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn perp_sign_default() -> f64 {
|
|
|
|
|
|
1.0
|
|
|
|
|
|
}
|
2026-06-20 17:32:56 +03:00
|
|
|
|
|
fix(dimension): baked *D block matches live render (gaps, arrows, leaders, text)
Brings the save-time *D-block bake into parity with the live render so a
dimension no longer changes when the file is saved and reopened (the reload
draws from the block). Per a bake-vs-live audit:
- text is now the live render's own Text/MText entity (alignment, MText
codes, text style, alignment point) — fixes every label shifting ~half a
glyph on reload
- extension lines carry the DIMEXO start gap + DIMEXE overshoot, and slant
by ext_line_rotation (DIMEDIT Oblique)
- terminators follow the style: oblique tick when DIMTSZ>0, else a closed
filled SOLID arrow (was a generic open stroke)
- angular dims bake the swept arc + tangent terminators, flush extension
lines (matching live exactly)
- radius/diameter bake the leader leg; diameter runs edge-to-edge through
the centre; centre mark honours negative DIMCEN (centre lines)
- per-element colours/lineweights (DIMCLRD/E/T, DIMLWD/E), DIMSE/DIMSD
suppression, DIMDLE tick overshoot
- consistent arc sweep at exactly 180 degrees
Refs #181. Remaining (custom DIMBLK arrow shapes, DIMFXLON, arrow-outside
fit, annotative viewport scale) tracked separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 20:29:35 +03:00
|
|
|
|
/// The measurement-text entity (Text or MText) for a baked `*D` block, built
|
|
|
|
|
|
/// through the SAME `dimension_text_entity` the live renderer uses — so the
|
|
|
|
|
|
/// baked text matches the on-screen value, position, height, rotation,
|
|
|
|
|
|
/// alignment, text style and MText handling, and nothing shifts when the file
|
|
|
|
|
|
/// is saved and reopened (the reload renders from the block). Returns `None`
|
|
|
|
|
|
/// when the text is suppressed (`user_text` is a single space). `anno_scale` is
|
|
|
|
|
|
/// the annotative scale (1.0 for a plain model-space bake).
|
|
|
|
|
|
pub(crate) fn baked_dimension_text_entity(
|
2026-06-27 17:47:50 +03:00
|
|
|
|
dim: &Dimension,
|
|
|
|
|
|
document: &CadDocument,
|
|
|
|
|
|
anno_scale: f64,
|
fix(dimension): baked *D block matches live render (gaps, arrows, leaders, text)
Brings the save-time *D-block bake into parity with the live render so a
dimension no longer changes when the file is saved and reopened (the reload
draws from the block). Per a bake-vs-live audit:
- text is now the live render's own Text/MText entity (alignment, MText
codes, text style, alignment point) — fixes every label shifting ~half a
glyph on reload
- extension lines carry the DIMEXO start gap + DIMEXE overshoot, and slant
by ext_line_rotation (DIMEDIT Oblique)
- terminators follow the style: oblique tick when DIMTSZ>0, else a closed
filled SOLID arrow (was a generic open stroke)
- angular dims bake the swept arc + tangent terminators, flush extension
lines (matching live exactly)
- radius/diameter bake the leader leg; diameter runs edge-to-edge through
the centre; centre mark honours negative DIMCEN (centre lines)
- per-element colours/lineweights (DIMCLRD/E/T, DIMLWD/E), DIMSE/DIMSD
suppression, DIMDLE tick overshoot
- consistent arc sweep at exactly 180 degrees
Refs #181. Remaining (custom DIMBLK arrow shapes, DIMFXLON, arrow-outside
fit, annotative viewport scale) tracked separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 20:29:35 +03:00
|
|
|
|
) -> Option<EntityType> {
|
2026-06-27 17:47:50 +03:00
|
|
|
|
let style_name = dim.base().style_name.as_str();
|
|
|
|
|
|
let style = document.dim_styles.iter().find(|s| {
|
|
|
|
|
|
s.name.eq_ignore_ascii_case(style_name)
|
|
|
|
|
|
|| (style_name.trim().is_empty() && s.name.eq_ignore_ascii_case("Standard"))
|
|
|
|
|
|
});
|
|
|
|
|
|
let dim_scale = style
|
feat(commands): expand standard CAD command coverage
Integrate a large batch of standard CAD commands and fix several
name/behaviour mismatches, with no product references in code.
3D solids (reusing the truck B-rep pipeline):
- SLICE, INTERFERE, PRESSPULL, POLYSOLID, PYRAMID, SECTION
- 3DROTATE, 3DMIRROR, 3DALIGN (cached-solid transforms)
Annotation / text:
- ARCTEXT, SPLINEFIT (Catmull-Rom to Bezier), DIMJOGGED, OBJECTSCALE
(XData markers honoured by the existing geometry-gen / tessellator)
Blocks / misc:
- NCOPY (block-content extraction via explode_entity)
- OPTIONS, SYNCPVIEWPORTS, THICKEN, plus earlier toggles, aliases,
system variables, and layer/style/view commands
POINTCLOUD/RECAP and UNDERLAYLAYERS/UOSNAP are intentionally left out:
they require a proprietary binary / PDF-layer parser, a GPU point
renderer, and real test files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 08:23:58 +03:00
|
|
|
|
.map(|s| {
|
|
|
|
|
|
if s.dimscale > 1e-6 {
|
|
|
|
|
|
s.dimscale
|
|
|
|
|
|
} else {
|
|
|
|
|
|
anno_scale
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-06-27 17:47:50 +03:00
|
|
|
|
.unwrap_or(1.0);
|
feat(commands): expand standard CAD command coverage
Integrate a large batch of standard CAD commands and fix several
name/behaviour mismatches, with no product references in code.
3D solids (reusing the truck B-rep pipeline):
- SLICE, INTERFERE, PRESSPULL, POLYSOLID, PYRAMID, SECTION
- 3DROTATE, 3DMIRROR, 3DALIGN (cached-solid transforms)
Annotation / text:
- ARCTEXT, SPLINEFIT (Catmull-Rom to Bezier), DIMJOGGED, OBJECTSCALE
(XData markers honoured by the existing geometry-gen / tessellator)
Blocks / misc:
- NCOPY (block-content extraction via explode_entity)
- OPTIONS, SYNCPVIEWPORTS, THICKEN, plus earlier toggles, aliases,
system variables, and layer/style/view commands
POINTCLOUD/RECAP and UNDERLAYLAYERS/UOSNAP are intentionally left out:
they require a proprietary binary / PDF-layer parser, a GPU point
renderer, and real test files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 08:23:58 +03:00
|
|
|
|
let dim_txt = style
|
|
|
|
|
|
.map(|s| s.dimtxt * dim_scale)
|
|
|
|
|
|
.unwrap_or(2.5 * dim_scale);
|
fix(dimension): baked *D block matches live render (gaps, arrows, leaders, text)
Brings the save-time *D-block bake into parity with the live render so a
dimension no longer changes when the file is saved and reopened (the reload
draws from the block). Per a bake-vs-live audit:
- text is now the live render's own Text/MText entity (alignment, MText
codes, text style, alignment point) — fixes every label shifting ~half a
glyph on reload
- extension lines carry the DIMEXO start gap + DIMEXE overshoot, and slant
by ext_line_rotation (DIMEDIT Oblique)
- terminators follow the style: oblique tick when DIMTSZ>0, else a closed
filled SOLID arrow (was a generic open stroke)
- angular dims bake the swept arc + tangent terminators, flush extension
lines (matching live exactly)
- radius/diameter bake the leader leg; diameter runs edge-to-edge through
the centre; centre mark honours negative DIMCEN (centre lines)
- per-element colours/lineweights (DIMCLRD/E/T, DIMLWD/E), DIMSE/DIMSD
suppression, DIMDLE tick overshoot
- consistent arc sweep at exactly 180 degrees
Refs #181. Remaining (custom DIMBLK arrow shapes, DIMFXLON, arrow-outside
fit, annotative viewport scale) tracked separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 20:29:35 +03:00
|
|
|
|
let mut ent = dimension_text_entity(dim, dim_txt, style, document, dim_scale)?;
|
|
|
|
|
|
// For a non-default-aligned Text, pin the DXF alignment point (group 11) to
|
|
|
|
|
|
// the insertion point so other CAD programs anchor the centred text where
|
|
|
|
|
|
// OCS does, not at the world origin.
|
|
|
|
|
|
if let EntityType::Text(t) = &mut ent {
|
|
|
|
|
|
t.alignment_point = Some(t.insertion_point);
|
|
|
|
|
|
}
|
|
|
|
|
|
Some(ent)
|
2026-06-27 17:47:50 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-20 17:32:56 +03:00
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
mod dimtad_tests {
|
|
|
|
|
|
use super::text_on_dim_line;
|
|
|
|
|
|
use acadrust::types::Vector3;
|
|
|
|
|
|
|
|
|
|
|
|
fn v(x: f64, y: f64) -> Vector3 {
|
|
|
|
|
|
Vector3::new(x, y, 0.0)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// DIMTAD=Above must place text on the geometric "up" side of a horizontal
|
|
|
|
|
|
// dimension whichever way the dimension runs; DIMTAD=Below flips it. (#144)
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn above_is_up_regardless_of_direction() {
|
|
|
|
|
|
let (first, second, defpt) = (v(0.0, 10.0), v(20.0, 10.0), v(0.0, 0.0));
|
|
|
|
|
|
let perp = 5.0;
|
feat(commands): expand standard CAD command coverage
Integrate a large batch of standard CAD commands and fix several
name/behaviour mismatches, with no product references in code.
3D solids (reusing the truck B-rep pipeline):
- SLICE, INTERFERE, PRESSPULL, POLYSOLID, PYRAMID, SECTION
- 3DROTATE, 3DMIRROR, 3DALIGN (cached-solid transforms)
Annotation / text:
- ARCTEXT, SPLINEFIT (Catmull-Rom to Bezier), DIMJOGGED, OBJECTSCALE
(XData markers honoured by the existing geometry-gen / tessellator)
Blocks / misc:
- NCOPY (block-content extraction via explode_entity)
- OPTIONS, SYNCPVIEWPORTS, THICKEN, plus earlier toggles, aliases,
system variables, and layer/style/view commands
POINTCLOUD/RECAP and UNDERLAYLAYERS/UOSNAP are intentionally left out:
they require a proprietary binary / PDF-layer parser, a GPU point
renderer, and real test files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 08:23:58 +03:00
|
|
|
|
let fwd = text_on_dim_line(first, second, defpt, 1.0, 0.0, 0, perp, 0.0, 1.0, false, 1);
|
|
|
|
|
|
let rev = text_on_dim_line(first, second, defpt, -1.0, 0.0, 0, perp, 0.0, 1.0, false, 1);
|
2026-06-20 17:32:56 +03:00
|
|
|
|
assert!(fwd.y > 0.0, "above must be +Y, got {}", fwd.y);
|
|
|
|
|
|
assert!(rev.y > 0.0, "above must be +Y even reversed, got {}", rev.y);
|
feat(commands): expand standard CAD command coverage
Integrate a large batch of standard CAD commands and fix several
name/behaviour mismatches, with no product references in code.
3D solids (reusing the truck B-rep pipeline):
- SLICE, INTERFERE, PRESSPULL, POLYSOLID, PYRAMID, SECTION
- 3DROTATE, 3DMIRROR, 3DALIGN (cached-solid transforms)
Annotation / text:
- ARCTEXT, SPLINEFIT (Catmull-Rom to Bezier), DIMJOGGED, OBJECTSCALE
(XData markers honoured by the existing geometry-gen / tessellator)
Blocks / misc:
- NCOPY (block-content extraction via explode_entity)
- OPTIONS, SYNCPVIEWPORTS, THICKEN, plus earlier toggles, aliases,
system variables, and layer/style/view commands
POINTCLOUD/RECAP and UNDERLAYLAYERS/UOSNAP are intentionally left out:
they require a proprietary binary / PDF-layer parser, a GPU point
renderer, and real test files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 08:23:58 +03:00
|
|
|
|
let below = text_on_dim_line(first, second, defpt, 1.0, 0.0, 0, perp, 0.0, 1.0, false, 4);
|
2026-06-20 17:32:56 +03:00
|
|
|
|
assert!(below.y < 0.0, "below must be -Y, got {}", below.y);
|
|
|
|
|
|
}
|
2026-06-20 17:38:57 +03:00
|
|
|
|
|
|
|
|
|
|
// DIMTAD=Outside (2) stays on the side farthest from the measured points,
|
|
|
|
|
|
// whichever side of the geometry the dimension line is on.
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn outside_is_away_from_object() {
|
|
|
|
|
|
let perp = 5.0;
|
|
|
|
|
|
// Object at y=0, dim line above it (y=10): away → further above.
|
|
|
|
|
|
let above = text_on_dim_line(
|
feat(commands): expand standard CAD command coverage
Integrate a large batch of standard CAD commands and fix several
name/behaviour mismatches, with no product references in code.
3D solids (reusing the truck B-rep pipeline):
- SLICE, INTERFERE, PRESSPULL, POLYSOLID, PYRAMID, SECTION
- 3DROTATE, 3DMIRROR, 3DALIGN (cached-solid transforms)
Annotation / text:
- ARCTEXT, SPLINEFIT (Catmull-Rom to Bezier), DIMJOGGED, OBJECTSCALE
(XData markers honoured by the existing geometry-gen / tessellator)
Blocks / misc:
- NCOPY (block-content extraction via explode_entity)
- OPTIONS, SYNCPVIEWPORTS, THICKEN, plus earlier toggles, aliases,
system variables, and layer/style/view commands
POINTCLOUD/RECAP and UNDERLAYLAYERS/UOSNAP are intentionally left out:
they require a proprietary binary / PDF-layer parser, a GPU point
renderer, and real test files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 08:23:58 +03:00
|
|
|
|
v(0.0, 0.0),
|
|
|
|
|
|
v(20.0, 0.0),
|
|
|
|
|
|
v(0.0, 10.0),
|
|
|
|
|
|
1.0,
|
|
|
|
|
|
0.0,
|
|
|
|
|
|
0,
|
|
|
|
|
|
perp,
|
|
|
|
|
|
0.0,
|
|
|
|
|
|
1.0,
|
|
|
|
|
|
false,
|
|
|
|
|
|
2,
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
above.y > 10.0,
|
|
|
|
|
|
"outside must clear the object side, got {}",
|
|
|
|
|
|
above.y
|
2026-06-20 17:38:57 +03:00
|
|
|
|
);
|
|
|
|
|
|
// Object at y=0, dim line below it (y=-10): away → further below.
|
|
|
|
|
|
let below = text_on_dim_line(
|
feat(commands): expand standard CAD command coverage
Integrate a large batch of standard CAD commands and fix several
name/behaviour mismatches, with no product references in code.
3D solids (reusing the truck B-rep pipeline):
- SLICE, INTERFERE, PRESSPULL, POLYSOLID, PYRAMID, SECTION
- 3DROTATE, 3DMIRROR, 3DALIGN (cached-solid transforms)
Annotation / text:
- ARCTEXT, SPLINEFIT (Catmull-Rom to Bezier), DIMJOGGED, OBJECTSCALE
(XData markers honoured by the existing geometry-gen / tessellator)
Blocks / misc:
- NCOPY (block-content extraction via explode_entity)
- OPTIONS, SYNCPVIEWPORTS, THICKEN, plus earlier toggles, aliases,
system variables, and layer/style/view commands
POINTCLOUD/RECAP and UNDERLAYLAYERS/UOSNAP are intentionally left out:
they require a proprietary binary / PDF-layer parser, a GPU point
renderer, and real test files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 08:23:58 +03:00
|
|
|
|
v(0.0, 0.0),
|
|
|
|
|
|
v(20.0, 0.0),
|
|
|
|
|
|
v(0.0, -10.0),
|
|
|
|
|
|
1.0,
|
|
|
|
|
|
0.0,
|
|
|
|
|
|
0,
|
|
|
|
|
|
perp,
|
|
|
|
|
|
0.0,
|
|
|
|
|
|
1.0,
|
|
|
|
|
|
false,
|
|
|
|
|
|
2,
|
|
|
|
|
|
);
|
|
|
|
|
|
assert!(
|
|
|
|
|
|
below.y < -10.0,
|
|
|
|
|
|
"outside must clear the object side, got {}",
|
|
|
|
|
|
below.y
|
2026-06-20 17:38:57 +03:00
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-06-20 17:32:56 +03:00
|
|
|
|
}
|
2026-07-14 07:31:09 -05:00
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
|
mod arch_format_tests {
|
|
|
|
|
|
use super::{format_architectural, format_fractional};
|
|
|
|
|
|
|
|
|
|
|
|
// A 3ft object that measures 35.99" (float noise) must carry the rounded
|
|
|
|
|
|
// fraction up through inches into feet — not print "2'-11 1"". (Regression
|
|
|
|
|
|
// for the fraction that reduced to 1/1 and leaked out as a bare "1".)
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn arch_carries_fraction_up_to_feet() {
|
|
|
|
|
|
assert_eq!(format_architectural(35.99, 0, 1), "3'-0\"");
|
|
|
|
|
|
assert_eq!(format_architectural(36.0, 0, 1), "3'-0\"");
|
|
|
|
|
|
// An exact 15/16 must still render as a fraction, no spurious carry.
|
|
|
|
|
|
assert_eq!(format_architectural(35.9375, 0, 1), "2'-11 15/16\"");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Carry that stops at inches (11.999" → 12" → 1'-0", not "0'-11 1"").
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn arch_carries_fraction_up_to_inches() {
|
|
|
|
|
|
assert_eq!(format_architectural(11.999, 0, 1), "1'-0\"");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn arch_normal_values_unchanged() {
|
|
|
|
|
|
assert_eq!(format_architectural(30.5, 0, 1), "2'-6 1/2\"");
|
|
|
|
|
|
assert_eq!(format_architectural(0.0, 0, 1), "0'-0\"");
|
|
|
|
|
|
assert_eq!(format_architectural(-30.25, 0, 1), "-2'-6 1/4\"");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Same carry bug lived in the plain fractional formatter.
|
|
|
|
|
|
#[test]
|
|
|
|
|
|
fn fractional_carries_up() {
|
|
|
|
|
|
assert_eq!(format_fractional(35.9999, 0), "36");
|
|
|
|
|
|
assert_eq!(format_fractional(11.999, 0), "12");
|
|
|
|
|
|
assert_eq!(format_fractional(6.5, 0), "6 1/2");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|