forked from 1b-agent/opencadstudio-upstream
fix(dimarc): complete merge integration
Preserve source-plane geometry and associative arc parameters while combining the latest properties and tolerance workflows.
This commit is contained in:
commit
6ef2d75e84
67 files changed
+4773
-883
No files matched your search
@@ -200,6 +200,7 @@ fn build_attr_render(input: AttrTextInputs<'_>, document: &acadrust::CadDocument
|
||||
oblique_angle,
|
||||
is_backward: width_factor < 0.0,
|
||||
is_upside_down: false,
|
||||
is_vertical: false,
|
||||
};
|
||||
let layout = layout_mtext(&MTextRenderOpts {
|
||||
// Not an MTEXT: text in a fixed box, never columnar.
|
||||
@@ -213,6 +214,8 @@ fn build_attr_render(input: AttrTextInputs<'_>, document: &acadrust::CadDocument
|
||||
attach_h_anchor,
|
||||
v_anchor,
|
||||
line_spacing_factor: 1.0,
|
||||
exact_line_spacing: false,
|
||||
rectangle_height: 0.0,
|
||||
vertical_text: false,
|
||||
want_glyph_boxes: false,
|
||||
});
|
||||
@@ -304,6 +307,7 @@ fn build_attr_render(input: AttrTextInputs<'_>, document: &acadrust::CadDocument
|
||||
origin,
|
||||
color: None,
|
||||
fill_tris,
|
||||
plane: None,
|
||||
// Carry a GlyphRun so the attribute renders as SDF glyph quads
|
||||
// (same as TEXT); the strokes above are the historical fallback and
|
||||
// are suppressed by the per-group SDF path.
|
||||
|
||||
+156
-1
@@ -2911,7 +2911,7 @@ use crate::scene::convert::tessellate::{
|
||||
add_polyline, add_segment, append_arrow, arrow_from_block_with_deferred_hatch,
|
||||
normalized_or, ArrowKind, DimGeom,
|
||||
};
|
||||
use crate::scene::model::wire_model::{SnapHint, WireModel};
|
||||
use crate::scene::model::wire_model::{SnapHint, TangentGeom, WireModel};
|
||||
|
||||
fn apply_dimension_breaks(
|
||||
document: &CadDocument,
|
||||
@@ -3075,6 +3075,28 @@ fn tessellate_dimension_inner(
|
||||
view_aabb: Option<[f32; 4]>,
|
||||
world_per_pixel: Option<f32>,
|
||||
) -> Vec<WireModel> {
|
||||
if let Dimension::Arc(arc) = dim {
|
||||
if let Some((local_arc, normal)) = arc_dimension_in_ocs(arc) {
|
||||
let mut wires = tessellate_dimension_inner(
|
||||
document,
|
||||
handle,
|
||||
&Dimension::Arc(local_arc),
|
||||
selected,
|
||||
entity_color,
|
||||
line_weight_px,
|
||||
anno_scale,
|
||||
selected_set,
|
||||
active_viewport,
|
||||
bg_color,
|
||||
None,
|
||||
world_per_pixel,
|
||||
);
|
||||
for wire in &mut wires {
|
||||
map_wire_ocs_to_wcs(wire, normal);
|
||||
}
|
||||
return wires;
|
||||
}
|
||||
}
|
||||
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 kernel path.)
|
||||
@@ -4728,6 +4750,139 @@ pub(crate) fn arc_dimension_angles(dimension: &DimensionArc) -> Option<(f32, f32
|
||||
(sweep > 1.0e-12).then_some((start as f32, (start + sweep) as f32))
|
||||
}
|
||||
|
||||
pub(crate) fn arc_dimension_in_ocs(
|
||||
dimension: &DimensionArc,
|
||||
) -> Option<(DimensionArc, Vector3)> {
|
||||
let length = dimension.base.normal.length();
|
||||
if !length.is_finite() || length <= 1.0e-12 {
|
||||
return None;
|
||||
}
|
||||
let normal = dimension.base.normal / length;
|
||||
if (normal - Vector3::UNIT_Z).length() <= 1.0e-12 {
|
||||
return None;
|
||||
}
|
||||
let normal_tuple = (normal.x, normal.y, normal.z);
|
||||
let to_ocs = |point: Vector3| {
|
||||
let point = crate::scene::view::transform::wcs_point_to_ocs(
|
||||
(point.x, point.y, point.z),
|
||||
normal_tuple,
|
||||
);
|
||||
Vector3::new(point.0, point.1, point.2)
|
||||
};
|
||||
let mut local = dimension.clone();
|
||||
local.center_point = to_ocs(dimension.center_point);
|
||||
let elevation = local.center_point.z;
|
||||
let on_plane = |point: Vector3| {
|
||||
let mut point = to_ocs(point);
|
||||
point.z = elevation;
|
||||
point
|
||||
};
|
||||
local.definition_point = on_plane(dimension.definition_point);
|
||||
local.first_extension_point = on_plane(dimension.first_extension_point);
|
||||
local.second_extension_point = on_plane(dimension.second_extension_point);
|
||||
local.first_leader_point = on_plane(dimension.first_leader_point);
|
||||
local.second_leader_point = on_plane(dimension.second_leader_point);
|
||||
local.base.definition_point = local.definition_point;
|
||||
local.base.text_middle_point = on_plane(dimension.base.text_middle_point);
|
||||
local.base.insertion_point = on_plane(dimension.base.insertion_point);
|
||||
local.base.normal = Vector3::UNIT_Z;
|
||||
Some((local, normal))
|
||||
}
|
||||
|
||||
fn map_wire_ocs_to_wcs(wire: &mut WireModel, normal: Vector3) {
|
||||
let normal_tuple = (normal.x, normal.y, normal.z);
|
||||
let map = |x: f64, y: f64, z: f64| {
|
||||
crate::scene::view::transform::ocs_point_to_wcs((x, y, z), normal_tuple)
|
||||
};
|
||||
let map_split = |high: &mut Vec<[f32; 3]>, low: &mut Vec<[f32; 3]>| {
|
||||
let mut mapped_high = Vec::with_capacity(high.len());
|
||||
let mut mapped_low = Vec::with_capacity(high.len());
|
||||
for (index, point) in high.iter().enumerate() {
|
||||
if point[0].is_nan() {
|
||||
mapped_high.push(*point);
|
||||
mapped_low.push([0.0; 3]);
|
||||
continue;
|
||||
}
|
||||
let residual = low.get(index).copied().unwrap_or([0.0; 3]);
|
||||
let point = map(
|
||||
point[0] as f64 + residual[0] as f64,
|
||||
point[1] as f64 + residual[1] as f64,
|
||||
point[2] as f64 + residual[2] as f64,
|
||||
);
|
||||
let (xh, xl) = WireModel::split_ds(point.0);
|
||||
let (yh, yl) = WireModel::split_ds(point.1);
|
||||
let (zh, zl) = WireModel::split_ds(point.2);
|
||||
mapped_high.push([xh, yh, zh]);
|
||||
mapped_low.push([xl, yl, zl]);
|
||||
}
|
||||
*high = mapped_high;
|
||||
*low = mapped_low;
|
||||
};
|
||||
|
||||
map_split(&mut wire.points, &mut wire.points_low);
|
||||
map_split(&mut wire.fill_tris, &mut wire.fill_tris_low);
|
||||
map_split(&mut wire.pick_tris, &mut wire.pick_tris_low);
|
||||
wire.text_verts = crate::scene::model::wire_model::map_text_verts(
|
||||
&wire.text_verts,
|
||||
map,
|
||||
);
|
||||
for (point, _) in &mut wire.snap_pts {
|
||||
let mapped = map(point.x, point.y, point.z);
|
||||
*point = DVec3::new(mapped.0, mapped.1, mapped.2);
|
||||
}
|
||||
for point in &mut wire.key_vertices {
|
||||
let mapped = map(point[0], point[1], point[2]);
|
||||
*point = [mapped.0, mapped.1, mapped.2];
|
||||
}
|
||||
if let Some(marker) = &mut wire.point_marker {
|
||||
let origin = map(marker.origin.x, marker.origin.y, marker.origin.z);
|
||||
let vector = |value: DVec3| {
|
||||
let mapped = map(value.x, value.y, value.z);
|
||||
DVec3::new(mapped.0, mapped.1, mapped.2)
|
||||
};
|
||||
marker.origin = DVec3::new(origin.0, origin.1, origin.2);
|
||||
marker.normal = vector(marker.normal).normalize_or(DVec3::Z);
|
||||
marker.axis_x = vector(marker.axis_x).normalize_or(DVec3::X);
|
||||
marker.axis_y = vector(marker.axis_y).normalize_or(DVec3::Y);
|
||||
}
|
||||
for tangent in &mut wire.tangent_geoms {
|
||||
match tangent {
|
||||
TangentGeom::Line { p1, p2 } => {
|
||||
let first = map(p1[0] as f64, p1[1] as f64, p1[2] as f64);
|
||||
let second = map(p2[0] as f64, p2[1] as f64, p2[2] as f64);
|
||||
*p1 = [first.0 as f32, first.1 as f32, first.2 as f32];
|
||||
*p2 = [second.0 as f32, second.1 as f32, second.2 as f32];
|
||||
}
|
||||
TangentGeom::Circle { center, radius } => {
|
||||
let center = map(
|
||||
center[0] as f64,
|
||||
center[1] as f64,
|
||||
center[2] as f64,
|
||||
);
|
||||
let (x_axis, y_axis) = crate::scene::view::transform::ocs_axes(normal_tuple);
|
||||
*tangent = TangentGeom::PlanarCircle {
|
||||
center: [center.0, center.1, center.2],
|
||||
axis_x: [x_axis.0, x_axis.1, x_axis.2],
|
||||
axis_y: [y_axis.0, y_axis.1, y_axis.2],
|
||||
radius: *radius as f64,
|
||||
};
|
||||
}
|
||||
TangentGeom::PlanarCircle { center, axis_x, axis_y, .. }
|
||||
| TangentGeom::Arc { center, axis_x, axis_y, .. } => {
|
||||
let mapped = map(center[0], center[1], center[2]);
|
||||
let map_vector = |axis: [f64; 3]| {
|
||||
let mapped = map(axis[0], axis[1], axis[2]);
|
||||
[mapped.0, mapped.1, mapped.2]
|
||||
};
|
||||
*center = [mapped.0, mapped.1, mapped.2];
|
||||
*axis_x = map_vector(*axis_x);
|
||||
*axis_y = map_vector(*axis_y);
|
||||
}
|
||||
}
|
||||
}
|
||||
wire.aabb = WireModel::UNBOUNDED_AABB;
|
||||
}
|
||||
|
||||
fn angular_dimension_frame(dim: &Dimension) -> Option<(Vec3, f32, f32, f32)> {
|
||||
let (vertex, start, end, arc_point) = match dim {
|
||||
Dimension::Angular2Ln(value) => {
|
||||
|
||||
+128
-32
@@ -5,7 +5,8 @@ use crate::entities::common::{
|
||||
edit_angle_prop as edit_angle, edit_prop as edit, num_prop as num_row, ro_prop as ro, square_grip, triangle_grip,
|
||||
};
|
||||
use crate::entities::text_support::{
|
||||
layout_mtext, resolve_text_style, GlyphBox, MTextColumns, MTextRenderOpts, MTextVAnchor,
|
||||
clamp_mtext_column_count, layout_mtext, resolve_text_style, GlyphBox, MTextColumns,
|
||||
MTextRenderOpts, MTextVAnchor,
|
||||
};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
|
||||
use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject};
|
||||
@@ -24,10 +25,19 @@ fn columns_of(t: &MText) -> MTextColumns {
|
||||
if c.column_type == 0 {
|
||||
return MTextColumns::default();
|
||||
}
|
||||
let count = clamp_mtext_column_count(c.column_count) as usize;
|
||||
MTextColumns {
|
||||
count: c.column_count.max(0) as usize,
|
||||
count,
|
||||
width: c.width as f32,
|
||||
gutter: c.gutter as f32,
|
||||
flow_reversed: c.flow_reversed,
|
||||
auto_height: c.auto_height,
|
||||
heights: c
|
||||
.heights
|
||||
.iter()
|
||||
.take(count)
|
||||
.map(|height| *height as f32)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +130,14 @@ pub fn glyph_boxes(t: &MText, document: &acadrust::CadDocument) -> Vec<GlyphBox>
|
||||
attach_h_anchor,
|
||||
v_anchor,
|
||||
line_spacing_factor: t.line_spacing_factor as f32,
|
||||
vertical_text: matches!(t.drawing_direction, DrawingDirection::TopToBottom),
|
||||
exact_line_spacing: matches!(
|
||||
t.line_spacing_style,
|
||||
acadrust::entities::LineSpacingStyle::Exactly
|
||||
),
|
||||
rectangle_height: t.rectangle_height.unwrap_or(0.0) as f32,
|
||||
vertical_text: matches!(t.drawing_direction, DrawingDirection::TopToBottom)
|
||||
|| matches!(t.drawing_direction, DrawingDirection::ByStyle)
|
||||
&& resolved_style.is_vertical,
|
||||
want_glyph_boxes: true,
|
||||
columns: columns_of(t),
|
||||
});
|
||||
@@ -169,7 +186,14 @@ fn to_render(t: &MText, document: &acadrust::CadDocument) -> RenderEntity {
|
||||
attach_h_anchor,
|
||||
v_anchor,
|
||||
line_spacing_factor: t.line_spacing_factor as f32,
|
||||
vertical_text: matches!(t.drawing_direction, DrawingDirection::TopToBottom),
|
||||
exact_line_spacing: matches!(
|
||||
t.line_spacing_style,
|
||||
acadrust::entities::LineSpacingStyle::Exactly
|
||||
),
|
||||
rectangle_height: t.rectangle_height.unwrap_or(0.0) as f32,
|
||||
vertical_text: matches!(t.drawing_direction, DrawingDirection::TopToBottom)
|
||||
|| matches!(t.drawing_direction, DrawingDirection::ByStyle)
|
||||
&& resolved_style.is_vertical,
|
||||
want_glyph_boxes: false,
|
||||
columns: columns_of(t),
|
||||
});
|
||||
@@ -231,8 +255,43 @@ fn grips(t: &MText) -> Vec<GripDef> {
|
||||
t.insertion_point.z,
|
||||
);
|
||||
let (dir, k) = width_grip_axis(t);
|
||||
let width_grip = p + dir * (k * t.rectangle_width.max(0.0));
|
||||
vec![square_grip(0, p), triangle_grip(1, width_grip)]
|
||||
let column_count = clamp_mtext_column_count(t.column_data.column_count);
|
||||
let columns_active =
|
||||
t.column_data.column_type != 0 && column_count > 1 && t.column_data.width > 0.0;
|
||||
let block_width = if columns_active {
|
||||
t.column_data.width * column_count as f64
|
||||
+ t.column_data.gutter * (column_count - 1) as f64
|
||||
} else {
|
||||
t.rectangle_width.max(0.0)
|
||||
};
|
||||
let width_grip = p + dir * (k * block_width);
|
||||
let mut result = vec![square_grip(0, p), triangle_grip(1, width_grip)];
|
||||
|
||||
let height = t
|
||||
.rectangle_height
|
||||
.or_else(|| t.column_data.heights.first().copied())
|
||||
.unwrap_or(0.0);
|
||||
if height > 0.0 && !(columns_active && t.column_data.auto_height) {
|
||||
let (sin, cos) = t.rotation.sin_cos();
|
||||
let down = glam::DVec3::new(sin, -cos, 0.0);
|
||||
let (_, vertical) = attach_anchors(t);
|
||||
let factor = match vertical {
|
||||
MTextVAnchor::Top
|
||||
| MTextVAnchor::MiddleOfTopLine
|
||||
| MTextVAnchor::BottomOfTopLine => 1.0,
|
||||
MTextVAnchor::Middle => 0.5,
|
||||
MTextVAnchor::Bottom | MTextVAnchor::MiddleOfBottomLine => -1.0,
|
||||
};
|
||||
result.push(triangle_grip(2, p + down * (factor * height)));
|
||||
}
|
||||
if columns_active {
|
||||
result.push(triangle_grip(3, p + dir * (k * t.column_data.width)));
|
||||
result.push(triangle_grip(
|
||||
4,
|
||||
p + dir * (k * (t.column_data.width + t.column_data.gutter)),
|
||||
));
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn columns_str(c: &acadrust::entities::MTextColumnData) -> &'static str {
|
||||
@@ -248,10 +307,9 @@ fn properties(t: &MText, text_style_names: &[String]) -> Vec<PropSection> {
|
||||
// by the line-spacing factor.
|
||||
let line_space_distance = t.height * 1.666_666_666_666_667 * t.line_spacing_factor;
|
||||
let text_frame_on = (t.background_fill_flags & 0x10) != 0;
|
||||
// Defined width is only live without columns; defined height is live for
|
||||
// static columns or manual-height dynamic columns, grayed otherwise.
|
||||
// Defined width is reported by Properties but is not edited there. Defined
|
||||
// height remains live for static columns or manual-height dynamic columns.
|
||||
let col_type = t.column_data.column_type;
|
||||
let width_editable = col_type == 0;
|
||||
let height_editable = col_type == 1 || (col_type == 2 && !t.column_data.auto_height);
|
||||
vec![
|
||||
PropSection {
|
||||
@@ -353,7 +411,7 @@ fn properties(t: &MText, text_style_names: &[String]) -> Vec<PropSection> {
|
||||
.collect(),
|
||||
},
|
||||
},
|
||||
num_row(t!("Defined width").as_ref(), "rect_w", t.rectangle_width, width_editable),
|
||||
num_row(t!("Defined width").as_ref(), "rect_w", t.rectangle_width, false),
|
||||
num_row(
|
||||
t!("Defined height").as_ref(),
|
||||
"rect_h",
|
||||
@@ -371,20 +429,6 @@ fn properties(t: &MText, text_style_names: &[String]) -> Vec<PropSection> {
|
||||
.collect(),
|
||||
},
|
||||
},
|
||||
// Count / width / gutter are live only when columns are on.
|
||||
num_row(
|
||||
t!("Column count").as_ref(),
|
||||
"col_count",
|
||||
t.column_data.column_count as f64,
|
||||
col_type != 0,
|
||||
),
|
||||
num_row(t!("Column width").as_ref(), "col_width", t.column_data.width, col_type != 0),
|
||||
num_row(
|
||||
t!("Column gutter").as_ref(),
|
||||
"col_gutter",
|
||||
t.column_data.gutter,
|
||||
col_type != 0,
|
||||
),
|
||||
Property {
|
||||
label: t!("Text frame").into_owned(),
|
||||
field: "text_frame",
|
||||
@@ -502,21 +546,20 @@ fn apply_geom_prop(t: &mut MText, field: &str, value: &str) {
|
||||
"ins_y" => t.insertion_point.y = v,
|
||||
"ins_z" => t.insertion_point.z = v,
|
||||
"height" if v > 0.0 => t.height = v,
|
||||
"rect_w" if v > 0.0 => t.rectangle_width = v,
|
||||
"rect_h" if v > 0.0 => t.rectangle_height = Some(v),
|
||||
"rect_h" if v >= 0.0 => t.rectangle_height = (v > 0.0).then_some(v),
|
||||
"rotation" => t.rotation = v.to_radians(),
|
||||
"line_spacing" if v > 0.0 => t.line_spacing_factor = v,
|
||||
"line_spacing" if (0.25..=4.0).contains(&v) => t.line_spacing_factor = v,
|
||||
// Editing the absolute distance back-solves the line-spacing factor so
|
||||
// the two stay consistent (distance = height × 5/3 × factor).
|
||||
"line_space_distance" if v > 0.0 => {
|
||||
let denom = t.height * 1.666_666_666_666_667;
|
||||
if denom > 0.0 {
|
||||
t.line_spacing_factor = v / denom;
|
||||
let factor = v / denom;
|
||||
if (0.25..=4.0).contains(&factor) {
|
||||
t.line_spacing_factor = factor;
|
||||
}
|
||||
}
|
||||
}
|
||||
"col_count" if v >= 1.0 => t.column_data.column_count = v.round() as i32,
|
||||
"col_width" if v > 0.0 => t.column_data.width = v,
|
||||
"col_gutter" if v >= 0.0 => t.column_data.gutter = v,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -541,7 +584,60 @@ fn apply_grip(t: &mut MText, grip_id: usize, apply: GripApply) {
|
||||
let dx = p.x as f64 - t.insertion_point.x;
|
||||
let dy = p.y as f64 - t.insertion_point.y;
|
||||
let projected = dx * dir.x + dy * dir.y;
|
||||
t.rectangle_width = (projected / k).max(0.01);
|
||||
let width = (projected / k).max(0.0);
|
||||
let column_count = clamp_mtext_column_count(t.column_data.column_count);
|
||||
if t.column_data.column_type != 0 && column_count > 1 {
|
||||
t.column_data.column_count = column_count;
|
||||
let gaps = (column_count - 1) as f64;
|
||||
t.column_data.width =
|
||||
((width - t.column_data.gutter * gaps) / column_count as f64)
|
||||
.max(0.01);
|
||||
} else {
|
||||
t.rectangle_width = width;
|
||||
}
|
||||
}
|
||||
(2, GripApply::Absolute(p)) => {
|
||||
let (sin, cos) = t.rotation.sin_cos();
|
||||
let down = glam::DVec3::new(sin, -cos, 0.0);
|
||||
let delta = glam::DVec3::new(
|
||||
p.x as f64 - t.insertion_point.x,
|
||||
p.y as f64 - t.insertion_point.y,
|
||||
p.z as f64 - t.insertion_point.z,
|
||||
);
|
||||
let (_, vertical) = attach_anchors(t);
|
||||
let factor = match vertical {
|
||||
MTextVAnchor::Top
|
||||
| MTextVAnchor::MiddleOfTopLine
|
||||
| MTextVAnchor::BottomOfTopLine => 1.0,
|
||||
MTextVAnchor::Middle => 0.5,
|
||||
MTextVAnchor::Bottom | MTextVAnchor::MiddleOfBottomLine => -1.0,
|
||||
};
|
||||
let height = (delta.dot(down) / factor).max(0.01);
|
||||
t.rectangle_height = Some(height);
|
||||
if t.column_data.column_type != 0 && !t.column_data.auto_height {
|
||||
let column_count = clamp_mtext_column_count(t.column_data.column_count);
|
||||
t.column_data.column_count = column_count;
|
||||
t.column_data.heights = vec![height; column_count as usize];
|
||||
}
|
||||
}
|
||||
(3, GripApply::Absolute(p)) => {
|
||||
let (dir, k) = width_grip_axis(t);
|
||||
let delta = glam::DVec3::new(
|
||||
p.x as f64 - t.insertion_point.x,
|
||||
p.y as f64 - t.insertion_point.y,
|
||||
0.0,
|
||||
);
|
||||
t.column_data.width = (delta.dot(dir) / k).max(0.01);
|
||||
}
|
||||
(4, GripApply::Absolute(p)) => {
|
||||
let (dir, k) = width_grip_axis(t);
|
||||
let delta = glam::DVec3::new(
|
||||
p.x as f64 - t.insertion_point.x,
|
||||
p.y as f64 - t.insertion_point.y,
|
||||
0.0,
|
||||
);
|
||||
t.column_data.gutter =
|
||||
(delta.dot(dir) / k - t.column_data.width).max(0.0);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -195,6 +195,11 @@ fn to_render(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<Rende
|
||||
},
|
||||
),
|
||||
line_spacing_factor: ctx.line_spacing_factor as f32,
|
||||
exact_line_spacing: matches!(
|
||||
ctx.line_spacing_style,
|
||||
acadrust::entities::LineSpacingStyle::Exactly
|
||||
),
|
||||
rectangle_height: 0.0,
|
||||
vertical_text: false,
|
||||
want_glyph_boxes: false,
|
||||
}))
|
||||
@@ -412,6 +417,7 @@ fn text_box_geom(ml: &MultiLeader) -> ([f64; 2], [f64; 3]) {
|
||||
oblique_angle: 0.0,
|
||||
is_backward: false,
|
||||
is_upside_down: false,
|
||||
is_vertical: false,
|
||||
};
|
||||
let layout = layout_mtext(&MTextRenderOpts {
|
||||
columns: Default::default(),
|
||||
@@ -424,6 +430,11 @@ fn text_box_geom(ml: &MultiLeader) -> ([f64; 2], [f64; 3]) {
|
||||
attach_h_anchor: 0.0,
|
||||
v_anchor: MTextVAnchor::Top,
|
||||
line_spacing_factor: ctx.line_spacing_factor as f32,
|
||||
exact_line_spacing: matches!(
|
||||
ctx.line_spacing_style,
|
||||
acadrust::entities::LineSpacingStyle::Exactly
|
||||
),
|
||||
rectangle_height: 0.0,
|
||||
vertical_text: vertical,
|
||||
want_glyph_boxes: false,
|
||||
});
|
||||
@@ -2039,6 +2050,11 @@ impl MultiLeaderTess for MultiLeader {
|
||||
attach_h_anchor: h_anchor,
|
||||
v_anchor,
|
||||
line_spacing_factor: ctx.line_spacing_factor as f32,
|
||||
exact_line_spacing: matches!(
|
||||
ctx.line_spacing_style,
|
||||
acadrust::entities::LineSpacingStyle::Exactly
|
||||
),
|
||||
rectangle_height: 0.0,
|
||||
// Vertical flow (top-to-bottom) is stored per-context.
|
||||
vertical_text: matches!(
|
||||
ctx.text_flow_direction,
|
||||
|
||||
@@ -180,6 +180,7 @@ impl RenderConvertible for RasterImage {
|
||||
origin: ins,
|
||||
color: None,
|
||||
fill_tris: vec![],
|
||||
plane: None,
|
||||
run: None,
|
||||
}];
|
||||
|
||||
@@ -218,6 +219,7 @@ impl RenderConvertible for RasterImage {
|
||||
origin,
|
||||
color: None,
|
||||
fill_tris,
|
||||
plane: None,
|
||||
run: Some(GlyphRun {
|
||||
text: path.to_string(),
|
||||
font,
|
||||
|
||||
@@ -1187,6 +1187,7 @@ impl RenderConvertible for Table {
|
||||
oblique_angle: style.map(|s| s.oblique_angle as f32).unwrap_or(0.0),
|
||||
is_backward: style.map(|s| s.is_backward()).unwrap_or(false),
|
||||
is_upside_down: style.map(|s| s.is_upside_down()).unwrap_or(false),
|
||||
is_vertical: style.map(|s| s.is_vertical).unwrap_or(false),
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1287,6 +1288,8 @@ impl RenderConvertible for Table {
|
||||
attach_h_anchor,
|
||||
v_anchor,
|
||||
line_spacing_factor: 1.0,
|
||||
exact_line_spacing: false,
|
||||
rectangle_height: 0.0,
|
||||
vertical_text: false,
|
||||
want_glyph_boxes: false,
|
||||
});
|
||||
@@ -1453,6 +1456,7 @@ pub fn tessellate_table(
|
||||
oblique_angle: style.map(|s| s.oblique_angle as f32).unwrap_or(0.0),
|
||||
is_backward: style.map(|s| s.is_backward()).unwrap_or(false),
|
||||
is_upside_down: style.map(|s| s.is_upside_down()).unwrap_or(false),
|
||||
is_vertical: style.map(|s| s.is_vertical).unwrap_or(false),
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1920,6 +1924,8 @@ pub fn tessellate_table(
|
||||
attach_h_anchor,
|
||||
v_anchor,
|
||||
line_spacing_factor: 1.0,
|
||||
exact_line_spacing: false,
|
||||
rectangle_height: 0.0,
|
||||
vertical_text: false,
|
||||
want_glyph_boxes: false,
|
||||
});
|
||||
|
||||
+182
-55
@@ -43,7 +43,24 @@ pub(crate) fn sync_text_alignment_point(t: &mut Text) {
|
||||
(HA::Left, VA::Baseline)
|
||||
);
|
||||
if needs_alignment_point {
|
||||
if t.alignment_point.is_none() {
|
||||
if matches!(t.horizontal_alignment, HA::Aligned | HA::Fit) {
|
||||
let point = t.alignment_point.unwrap_or(t.insertion_point);
|
||||
let dx = point.x - t.insertion_point.x;
|
||||
let dy = point.y - t.insertion_point.y;
|
||||
if dx.hypot(dy) <= 1.0e-9 {
|
||||
let span = t.height.max(1.0e-6)
|
||||
* t.width_factor.abs().max(0.01)
|
||||
* t.value.chars().count().max(1) as f64
|
||||
* 0.6;
|
||||
t.alignment_point = Some(acadrust::types::Vector3::new(
|
||||
t.insertion_point.x + t.rotation.cos() * span,
|
||||
t.insertion_point.y + t.rotation.sin() * span,
|
||||
t.insertion_point.z,
|
||||
));
|
||||
} else {
|
||||
t.alignment_point = Some(point);
|
||||
}
|
||||
} else if t.alignment_point.is_none() {
|
||||
t.alignment_point = Some(t.insertion_point);
|
||||
}
|
||||
} else {
|
||||
@@ -51,6 +68,21 @@ pub(crate) fn sync_text_alignment_point(t: &mut Text) {
|
||||
}
|
||||
}
|
||||
|
||||
fn two_point_span(t: &Text) -> Option<(f64, f64)> {
|
||||
if !matches!(t.horizontal_alignment, HA::Aligned | HA::Fit) {
|
||||
return None;
|
||||
}
|
||||
let point = t.alignment_point?;
|
||||
let dx = point.x - t.insertion_point.x;
|
||||
let dy = point.y - t.insertion_point.y;
|
||||
let distance = dx.hypot(dy);
|
||||
(distance > 1.0e-9).then(|| (distance, dy.atan2(dx)))
|
||||
}
|
||||
|
||||
fn displayed_rotation(t: &Text) -> f64 {
|
||||
two_point_span(t).map_or(t.rotation, |(_, angle)| angle)
|
||||
}
|
||||
|
||||
/// Resolved placement of a TEXT run: the baseline-anchored run origin (WCS xy)
|
||||
/// plus every parameter needed to lay the glyphs out. Shared by `to_render` (the
|
||||
/// stroke path) and the SDF-quad text collector so both place text identically.
|
||||
@@ -98,8 +130,12 @@ pub(crate) fn acad_text_encode(value: &str) -> String {
|
||||
out
|
||||
}
|
||||
|
||||
fn to_render(t: &Text, document: &acadrust::CadDocument) -> RenderEntity {
|
||||
let p = text_run_placement(t, document);
|
||||
pub(crate) fn to_render_at_scale(
|
||||
t: &Text,
|
||||
document: &acadrust::CadDocument,
|
||||
annotation_scale: f32,
|
||||
) -> RenderEntity {
|
||||
let p = text_run_placement_at_scale(t, document, annotation_scale);
|
||||
let snap_pt = glam::DVec3::new(p.wcs_insertion[0], p.wcs_insertion[1], p.wcs_insertion[2]);
|
||||
// Parse `%%` codes via acadrust, re-encoded for the stroke tessellator.
|
||||
let value = acad_text_encode(&p.value);
|
||||
@@ -120,6 +156,7 @@ fn to_render(t: &Text, document: &acadrust::CadDocument) -> RenderEntity {
|
||||
origin: p.origin,
|
||||
color: None,
|
||||
fill_tris,
|
||||
plane: None,
|
||||
run: Some(GlyphRun {
|
||||
text: value,
|
||||
font: p.font.clone(),
|
||||
@@ -140,7 +177,16 @@ fn to_render(t: &Text, document: &acadrust::CadDocument) -> RenderEntity {
|
||||
|
||||
/// Compute a TEXT entity's run placement (origin + layout params). Extracted
|
||||
/// from `to_render` verbatim so the stroke and SDF-quad paths agree exactly.
|
||||
pub fn text_run_placement(t: &Text, document: &acadrust::CadDocument) -> TextPlacement {
|
||||
pub fn text_run_placement_at_scale(
|
||||
t: &Text,
|
||||
document: &acadrust::CadDocument,
|
||||
annotation_scale: f32,
|
||||
) -> TextPlacement {
|
||||
let annotation_scale = if annotation_scale.is_finite() && annotation_scale > 1.0e-9 {
|
||||
annotation_scale
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let normal = (t.normal.x, t.normal.y, t.normal.z);
|
||||
let (wsx, wsy, wsz) = crate::scene::view::transform::ocs_point_to_wcs(
|
||||
(
|
||||
@@ -152,10 +198,9 @@ pub fn text_run_placement(t: &Text, document: &acadrust::CadDocument) -> TextPla
|
||||
);
|
||||
let resolved_style = resolve_text_style(&t.style, document);
|
||||
let font_name = resolved_style.font_name;
|
||||
// AutoCAD text geometry rule: the entity stores the FINAL width factor /
|
||||
// oblique angle, copied from the style at creation and persisting through
|
||||
// style edits. Use it as-is. Only fall back to the style when the entity
|
||||
// value is missing (the parser reports 0.0 for default-omitted fields).
|
||||
// The entity stores the final width factor and oblique angle copied from
|
||||
// its style at creation. Only fall back to the style when an omitted field
|
||||
// was read as zero.
|
||||
let base_wf = if t.width_factor.abs() > 1e-9 {
|
||||
(t.width_factor as f32).clamp(0.01, 100.0)
|
||||
} else {
|
||||
@@ -168,17 +213,44 @@ pub fn text_run_placement(t: &Text, document: &acadrust::CadDocument) -> TextPla
|
||||
// mirror, and XOR keeps a double mirror an involution.
|
||||
let eff_backward = resolved_style.is_backward ^ (t.generation_flags & 0x2 != 0);
|
||||
let eff_upside = resolved_style.is_upside_down ^ (t.generation_flags & 0x4 != 0);
|
||||
let width_factor = if eff_backward { -base_wf } else { base_wf };
|
||||
let rotation = if eff_upside {
|
||||
t.rotation as f32 + std::f32::consts::PI
|
||||
} else {
|
||||
t.rotation as f32
|
||||
};
|
||||
let mut width_factor = if eff_backward { -base_wf } else { base_wf };
|
||||
let oblique_angle = if t.oblique_angle.abs() > 1e-9 {
|
||||
t.oblique_angle as f32
|
||||
} else {
|
||||
resolved_style.oblique_angle
|
||||
};
|
||||
let value_for_bounds = resolve_dxf_special_chars(&t.value);
|
||||
let mut height = t.height.max(1.0e-9) as f32;
|
||||
let mut base_rotation = t.rotation as f32;
|
||||
|
||||
// Aligned and Fit are true two-point modes. Both derive their baseline
|
||||
// direction from the endpoints. Aligned scales height uniformly; Fit keeps
|
||||
// the height and changes only the horizontal factor.
|
||||
if let Some((span, angle)) = two_point_span(t) {
|
||||
base_rotation = angle as f32;
|
||||
if let Some(base_bounds) = text_local_bounds(
|
||||
&font_name,
|
||||
&value_for_bounds,
|
||||
height,
|
||||
width_factor,
|
||||
oblique_angle,
|
||||
) {
|
||||
if base_bounds.advance > 1.0e-6 {
|
||||
let scale =
|
||||
(span as f32 / annotation_scale / base_bounds.advance).max(1.0e-6);
|
||||
if matches!(t.horizontal_alignment, HA::Aligned) {
|
||||
height *= scale;
|
||||
} else {
|
||||
width_factor *= scale;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let rotation = if eff_upside {
|
||||
base_rotation + std::f32::consts::PI
|
||||
} else {
|
||||
base_rotation
|
||||
};
|
||||
// Anchor stays f64: large coordinates (UTM etc.) lose ~0.5 units of
|
||||
// precision when cast to f32, which snaps text baselines onto a coarse
|
||||
// grid and makes adjacent rows collide. Only the small local offsets
|
||||
@@ -188,17 +260,16 @@ pub fn text_run_placement(t: &Text, document: &acadrust::CadDocument) -> TextPla
|
||||
&t.vertical_alignment,
|
||||
&t.alignment_point,
|
||||
) {
|
||||
(HA::Aligned | HA::Middle | HA::Fit, _, Some(a)) => [a.x, a.y],
|
||||
(HA::Aligned | HA::Fit, _, _) => [t.insertion_point.x, t.insertion_point.y],
|
||||
(HA::Middle, _, Some(a)) => [a.x, a.y],
|
||||
(HA::Center | HA::Right, _, Some(a)) => [a.x, a.y],
|
||||
(_, VA::Bottom | VA::Middle | VA::Top, Some(a)) => [a.x, a.y],
|
||||
_ => [t.insertion_point.x, t.insertion_point.y],
|
||||
};
|
||||
// Strip %%u/%%o for bounds (they add no width); resolve %%d/%%c/%%p for correct advance.
|
||||
let value_for_bounds = resolve_dxf_special_chars(&t.value);
|
||||
let bounds = text_local_bounds(
|
||||
&font_name,
|
||||
&value_for_bounds,
|
||||
t.height as f32,
|
||||
height,
|
||||
width_factor,
|
||||
oblique_angle,
|
||||
);
|
||||
@@ -213,7 +284,8 @@ pub fn text_run_placement(t: &Text, document: &acadrust::CadDocument) -> TextPla
|
||||
let ax = match t.horizontal_alignment {
|
||||
HA::Left => 0.0,
|
||||
HA::Center | HA::Middle => b.advance * 0.5 * sign,
|
||||
HA::Right | HA::Aligned | HA::Fit => b.advance * sign,
|
||||
HA::Right => b.advance * sign,
|
||||
HA::Aligned | HA::Fit => 0.0,
|
||||
};
|
||||
// Vertical anchor uses the inked extent (cap / baseline geometry).
|
||||
let ay = match t.vertical_alignment {
|
||||
@@ -236,7 +308,7 @@ pub fn text_run_placement(t: &Text, document: &acadrust::CadDocument) -> TextPla
|
||||
];
|
||||
TextPlacement {
|
||||
origin,
|
||||
height: t.height as f32,
|
||||
height,
|
||||
rotation,
|
||||
width_factor,
|
||||
oblique_angle,
|
||||
@@ -247,12 +319,19 @@ pub fn text_run_placement(t: &Text, document: &acadrust::CadDocument) -> TextPla
|
||||
}
|
||||
|
||||
fn grips(t: &Text) -> Vec<GripDef> {
|
||||
let p = glam::DVec3::new(
|
||||
let insertion = glam::DVec3::new(
|
||||
t.insertion_point.x,
|
||||
t.insertion_point.y,
|
||||
t.insertion_point.z,
|
||||
);
|
||||
vec![square_grip(0, p)]
|
||||
let mut grips = vec![square_grip(0, insertion)];
|
||||
if let Some(point) = t.alignment_point {
|
||||
grips.push(square_grip(
|
||||
1,
|
||||
glam::DVec3::new(point.x, point.y, point.z),
|
||||
));
|
||||
}
|
||||
grips
|
||||
}
|
||||
|
||||
fn properties(t: &Text, text_style_names: &[String]) -> Vec<PropSection> {
|
||||
@@ -263,15 +342,25 @@ fn properties(t: &Text, text_style_names: &[String]) -> Vec<PropSection> {
|
||||
// both points are live.
|
||||
let is_plain_left = matches!(t.horizontal_alignment, HA::Left)
|
||||
&& matches!(t.vertical_alignment, VA::Baseline);
|
||||
let pos_editable = is_plain_left || matches!(t.horizontal_alignment, HA::Aligned | HA::Fit);
|
||||
let align_editable = !is_plain_left;
|
||||
// The alignment point is meaningless (reset to the origin) for plain-Left text.
|
||||
let is_aligned = matches!(t.horizontal_alignment, HA::Aligned);
|
||||
let is_two_point = matches!(t.horizontal_alignment, HA::Aligned | HA::Fit);
|
||||
// The visible Properties palette exposes Text alignment as calculated
|
||||
// coordinates and Position as the editable placement point. For aligned
|
||||
// and fit text Position targets the first endpoint; for every other
|
||||
// non-left mode it targets the alignment anchor that actually places the
|
||||
// glyph run.
|
||||
let position_uses_alignment = !is_plain_left && !is_two_point;
|
||||
let ap = t.alignment_point.unwrap_or(t.insertion_point);
|
||||
let (ax, ay, az) = if align_editable {
|
||||
let (ax, ay, az) = if !is_plain_left {
|
||||
(ap.x, ap.y, ap.z)
|
||||
} else {
|
||||
(0.0, 0.0, 0.0)
|
||||
};
|
||||
let position = if position_uses_alignment {
|
||||
ap
|
||||
} else {
|
||||
t.insertion_point
|
||||
};
|
||||
vec![
|
||||
PropSection {
|
||||
title: t!("Text").into_owned(),
|
||||
@@ -331,22 +420,31 @@ fn properties(t: &Text, text_style_names: &[String]) -> Vec<PropSection> {
|
||||
t!("Height").as_ref(),
|
||||
"height",
|
||||
t.height,
|
||||
crate::entities::common::style_fixed_height(&t.style).is_none(),
|
||||
!is_aligned
|
||||
&& crate::entities::common::style_fixed_height(&t.style).is_none(),
|
||||
),
|
||||
edit_angle(t!("Rotation").as_ref(), "rotation", t.rotation.to_degrees()),
|
||||
if is_two_point {
|
||||
ro(
|
||||
t!("Rotation").as_ref(),
|
||||
"rotation",
|
||||
crate::entities::common::format_angle(displayed_rotation(t)),
|
||||
)
|
||||
} else {
|
||||
edit_angle(t!("Rotation").as_ref(), "rotation", t.rotation.to_degrees())
|
||||
},
|
||||
edit(t!("Width factor").as_ref(), "width_factor", t.width_factor),
|
||||
edit_angle(t!("Obliquing").as_ref(), "oblique_angle", t.oblique_angle.to_degrees()),
|
||||
num_row(t!("Text alignment X").as_ref(), "align_x", ax, align_editable),
|
||||
num_row(t!("Text alignment Y").as_ref(), "align_y", ay, align_editable),
|
||||
num_row(t!("Text alignment Z").as_ref(), "align_z", az, align_editable),
|
||||
num_row(t!("Text alignment X").as_ref(), "align_x", ax, false),
|
||||
num_row(t!("Text alignment Y").as_ref(), "align_y", ay, false),
|
||||
num_row(t!("Text alignment Z").as_ref(), "align_z", az, false),
|
||||
],
|
||||
},
|
||||
PropSection {
|
||||
title: t!("Geometry").into_owned(),
|
||||
props: vec![
|
||||
num_row(t!("Position X").as_ref(), "ins_x", t.insertion_point.x, pos_editable),
|
||||
num_row(t!("Position Y").as_ref(), "ins_y", t.insertion_point.y, pos_editable),
|
||||
num_row(t!("Position Z").as_ref(), "ins_z", t.insertion_point.z, pos_editable),
|
||||
num_row(t!("Position X").as_ref(), "ins_x", position.x, true),
|
||||
num_row(t!("Position Y").as_ref(), "ins_y", position.y, true),
|
||||
num_row(t!("Position Z").as_ref(), "ins_z", position.z, true),
|
||||
],
|
||||
},
|
||||
PropSection {
|
||||
@@ -439,37 +537,64 @@ fn apply_geom_prop(t: &mut Text, field: &str, value: &str) {
|
||||
return;
|
||||
};
|
||||
match field {
|
||||
"ins_x" => t.insertion_point.x = v,
|
||||
"ins_y" => t.insertion_point.y = v,
|
||||
"ins_z" => t.insertion_point.z = v,
|
||||
"align_x" | "align_y" | "align_z" => {
|
||||
let ins = t.insertion_point;
|
||||
let ap = t.alignment_point.get_or_insert(ins);
|
||||
"ins_x" | "ins_y" | "ins_z" => {
|
||||
let plain_left = matches!(t.horizontal_alignment, HA::Left)
|
||||
&& matches!(t.vertical_alignment, VA::Baseline);
|
||||
let two_point = matches!(t.horizontal_alignment, HA::Aligned | HA::Fit);
|
||||
let target = if !plain_left && !two_point {
|
||||
let insertion = t.insertion_point;
|
||||
t.alignment_point.get_or_insert(insertion)
|
||||
} else {
|
||||
&mut t.insertion_point
|
||||
};
|
||||
match field {
|
||||
"align_x" => ap.x = v,
|
||||
"align_y" => ap.y = v,
|
||||
_ => ap.z = v,
|
||||
"ins_x" => target.x = v,
|
||||
"ins_y" => target.y = v,
|
||||
_ => target.z = v,
|
||||
}
|
||||
}
|
||||
"height" if v > 0.0 => t.height = v,
|
||||
"rotation" => t.rotation = v.to_radians(),
|
||||
"align_x" | "align_y" | "align_z" => {
|
||||
// Calculated display rows are intentionally not writable.
|
||||
return;
|
||||
}
|
||||
"height"
|
||||
if v > 0.0 && !matches!(t.horizontal_alignment, HA::Aligned) =>
|
||||
{
|
||||
t.height = v
|
||||
}
|
||||
"rotation" if !matches!(t.horizontal_alignment, HA::Aligned | HA::Fit) => {
|
||||
t.rotation = v.to_radians()
|
||||
}
|
||||
"width_factor" if v > 0.0 => t.width_factor = v,
|
||||
"oblique_angle" => t.oblique_angle = v.to_radians(),
|
||||
"oblique_angle" if (-85.0..=85.0).contains(&v) => {
|
||||
t.oblique_angle = v.to_radians()
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_grip(t: &mut Text, _grip_id: usize, apply: GripApply) {
|
||||
fn apply_grip(t: &mut Text, grip_id: usize, apply: GripApply) {
|
||||
match apply {
|
||||
GripApply::Absolute(p) => {
|
||||
t.insertion_point.x = p.x as f64;
|
||||
t.insertion_point.y = p.y as f64;
|
||||
t.insertion_point.z = p.z as f64;
|
||||
let target = if grip_id == 1 {
|
||||
let insertion = t.insertion_point;
|
||||
t.alignment_point.get_or_insert(insertion)
|
||||
} else {
|
||||
&mut t.insertion_point
|
||||
};
|
||||
target.x = p.x;
|
||||
target.y = p.y;
|
||||
target.z = p.z;
|
||||
}
|
||||
GripApply::Translate(d) => {
|
||||
t.insertion_point.x += d.x as f64;
|
||||
t.insertion_point.y += d.y as f64;
|
||||
t.insertion_point.z += d.z as f64;
|
||||
t.insertion_point.x += d.x;
|
||||
t.insertion_point.y += d.y;
|
||||
t.insertion_point.z += d.z;
|
||||
if let Some(point) = t.alignment_point.as_mut() {
|
||||
point.x += d.x;
|
||||
point.y += d.y;
|
||||
point.z += d.z;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -495,7 +620,7 @@ fn apply_transform(t: &mut Text, tr: &EntityTransform) {
|
||||
|
||||
impl RenderConvertible for Text {
|
||||
fn to_render(&self, document: &acadrust::CadDocument) -> Option<RenderEntity> {
|
||||
Some(to_render(self, document))
|
||||
Some(to_render_at_scale(self, document, 1.0))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -551,7 +676,9 @@ impl Grippable for Text {
|
||||
value: f64,
|
||||
) {
|
||||
use crate::scene::model::object::GripMenuAction as A;
|
||||
if matches!(action, A::RotateText) {
|
||||
if matches!(action, A::RotateText)
|
||||
&& !matches!(self.horizontal_alignment, HA::Aligned | HA::Fit)
|
||||
{
|
||||
self.rotation = value.to_radians();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ pub struct ResolvedTextStyle {
|
||||
pub oblique_angle: f32,
|
||||
pub is_backward: bool,
|
||||
pub is_upside_down: bool,
|
||||
pub is_vertical: bool,
|
||||
}
|
||||
|
||||
pub fn resolve_text_style(style_name: &str, document: &CadDocument) -> ResolvedTextStyle {
|
||||
@@ -76,6 +77,7 @@ pub fn resolve_text_style(style_name: &str, document: &CadDocument) -> ResolvedT
|
||||
oblique_angle: style.map(|s| s.oblique_angle as f32).unwrap_or(0.0),
|
||||
is_backward: style.map(|s| s.is_backward()).unwrap_or(false),
|
||||
is_upside_down: style.map(|s| s.is_upside_down()).unwrap_or(false),
|
||||
is_vertical: style.map(|s| s.is_vertical).unwrap_or(false),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1012,6 +1014,11 @@ pub struct MTextRenderOpts<'a> {
|
||||
pub v_anchor: MTextVAnchor,
|
||||
/// DXF code 44 — multiplier on the default 5/3-em baseline gap.
|
||||
pub line_spacing_factor: f32,
|
||||
/// `true` fixes every baseline advance to the entity spacing. `false`
|
||||
/// treats it as a minimum and expands for taller inline runs.
|
||||
pub exact_line_spacing: bool,
|
||||
/// User-defined text-box/column height. Zero means content-driven.
|
||||
pub rectangle_height: f32,
|
||||
/// `true` when the entity is laid out top-to-bottom (DXF code 71 = 2).
|
||||
pub vertical_text: bool,
|
||||
/// When true, `layout_mtext` also fills `MTextLayout::glyph_boxes` with
|
||||
@@ -1025,10 +1032,15 @@ pub struct MTextRenderOpts<'a> {
|
||||
|
||||
/// An MTEXT's column layout, flattened from its `column_data`.
|
||||
///
|
||||
/// Content moves to the next column at a `\N` break. Filling a column and
|
||||
/// spilling into the next on its own is not modelled: `heights` / `auto_height`
|
||||
/// are not read, so a column runs as long as its content does.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
/// Content moves to the next column at an explicit `\N` break or when the
|
||||
/// configured manual/automatic column height is exhausted.
|
||||
pub(crate) const MAX_MTEXT_COLUMNS: i32 = 256;
|
||||
|
||||
pub(crate) fn clamp_mtext_column_count(count: i32) -> i32 {
|
||||
count.clamp(1, MAX_MTEXT_COLUMNS)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MTextColumns {
|
||||
/// Column count. 0 or 1 both mean "no column layout".
|
||||
pub count: usize,
|
||||
@@ -1036,6 +1048,12 @@ pub struct MTextColumns {
|
||||
pub width: f32,
|
||||
/// Gap between two adjacent columns, world units.
|
||||
pub gutter: f32,
|
||||
/// Whether logical column flow starts at the opposite side.
|
||||
pub flow_reversed: bool,
|
||||
/// Dynamic-column height is balanced from content when true.
|
||||
pub auto_height: bool,
|
||||
/// Optional manual height for each logical column.
|
||||
pub heights: Vec<f32>,
|
||||
}
|
||||
|
||||
impl MTextColumns {
|
||||
@@ -1046,7 +1064,17 @@ impl MTextColumns {
|
||||
|
||||
/// Left edge of column `i`, relative to the text block's own left edge.
|
||||
pub fn offset_of(&self, i: usize) -> f32 {
|
||||
i as f32 * (self.width + self.gutter)
|
||||
let physical = if self.flow_reversed {
|
||||
self.count.saturating_sub(1).saturating_sub(i)
|
||||
} else {
|
||||
i
|
||||
};
|
||||
physical as f32 * (self.width + self.gutter)
|
||||
}
|
||||
|
||||
fn total_width(&self) -> f32 {
|
||||
self.width * self.count as f32
|
||||
+ self.gutter.max(0.0) * self.count.saturating_sub(1) as f32
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1118,6 +1146,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
||||
indent_right: f32,
|
||||
tab_stops: Vec<TabStop>,
|
||||
is_first_in_paragraph: bool,
|
||||
starts_column: bool,
|
||||
/// Which column this line lives in; always 0 without a column layout.
|
||||
column: usize,
|
||||
/// Extra gap above (paragraph's first wrapped line only) / below (last
|
||||
@@ -1128,7 +1157,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
||||
line_spacing: Option<ParaLineSpacing>,
|
||||
}
|
||||
|
||||
let cols = opts.columns;
|
||||
let cols = opts.columns.clone();
|
||||
// A `\N` past the last column has nowhere to go — keep those lines in the
|
||||
// last one rather than laying them out beyond the block.
|
||||
let last_col = cols.count.saturating_sub(1);
|
||||
@@ -1255,6 +1284,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
||||
indent_right: para.indent_right,
|
||||
tab_stops: para.tab_stops.clone(),
|
||||
is_first_in_paragraph: idx == 0,
|
||||
starts_column: idx == 0 && para.starts_column,
|
||||
column,
|
||||
// The gap above rides the first wrapped line; the gap below the
|
||||
// last, so an interior wrap keeps normal single spacing.
|
||||
@@ -1277,6 +1307,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
||||
indent_right: 0.0,
|
||||
tab_stops: Vec::new(),
|
||||
is_first_in_paragraph: true,
|
||||
starts_column: false,
|
||||
column: 0,
|
||||
space_before: 0.0,
|
||||
space_after: 0.0,
|
||||
@@ -1366,15 +1397,86 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
||||
};
|
||||
// A paragraph's own `\psm#`/`\pse#` overrides the entity factor for
|
||||
// its lines; `Exact` fixes the baseline gap outright.
|
||||
let entity_gap = entity_h * ls_factor * (5.0 / 3.0) * base_font.line_spacing();
|
||||
match s.line_spacing {
|
||||
Some(ParaLineSpacing::Exact(e)) if e > 0.0 => e,
|
||||
Some(ParaLineSpacing::Multiple(m)) if m > 0.0 => {
|
||||
mh * m * (5.0 / 3.0) * base_font.line_spacing()
|
||||
}
|
||||
_ => mh * ls_factor * (5.0 / 3.0) * base_font.line_spacing(),
|
||||
_ if opts.exact_line_spacing => entity_gap,
|
||||
_ => (mh * ls_factor * (5.0 / 3.0) * base_font.line_spacing())
|
||||
.max(entity_gap),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Flow lines into the next column when the active column's configured
|
||||
// height is exhausted. Explicit `\N` breaks remain authoritative. Dynamic
|
||||
// auto-height balances the content; static/manual columns use their stored
|
||||
// height (falling back to the entity rectangle height).
|
||||
if cols.active() {
|
||||
let mut pending_after = 0.0_f32;
|
||||
let total_advance: f32 = sub_lines
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, line)| {
|
||||
if line.starts_column {
|
||||
pending_after = 0.0;
|
||||
}
|
||||
let spacing = if line.is_first_in_paragraph {
|
||||
line.space_before + pending_after
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
pending_after = line.space_after;
|
||||
per_line_h.get(index).copied().unwrap_or(entity_h) + spacing
|
||||
})
|
||||
.sum();
|
||||
let balanced = (total_advance / cols.count.max(1) as f32).max(entity_h);
|
||||
let mut current_column = 0usize;
|
||||
let mut used = 0.0_f32;
|
||||
let mut pending_after = 0.0_f32;
|
||||
for (index, line) in sub_lines.iter_mut().enumerate() {
|
||||
if line.starts_column {
|
||||
current_column = (current_column + 1).min(cols.count - 1);
|
||||
used = 0.0;
|
||||
pending_after = 0.0;
|
||||
}
|
||||
let limit = if cols.auto_height {
|
||||
balanced
|
||||
} else {
|
||||
cols.heights
|
||||
.get(current_column)
|
||||
.copied()
|
||||
.filter(|height| *height > 0.0)
|
||||
.unwrap_or(opts.rectangle_height)
|
||||
};
|
||||
let line_advance = per_line_h.get(index).copied().unwrap_or(entity_h);
|
||||
let spacing = if line.is_first_in_paragraph {
|
||||
line.space_before + pending_after
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let mut advance = line_advance + spacing;
|
||||
if limit > 0.0
|
||||
&& used > 0.0
|
||||
&& used + advance > limit
|
||||
&& current_column + 1 < cols.count
|
||||
{
|
||||
current_column += 1;
|
||||
used = 0.0;
|
||||
advance = line_advance
|
||||
+ if line.is_first_in_paragraph {
|
||||
line.space_before
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
}
|
||||
line.column = current_column;
|
||||
used += advance;
|
||||
pending_after = line.space_after;
|
||||
}
|
||||
}
|
||||
// Per-line text height — the tallest Word on the line, an empty line
|
||||
// inheriting the previous line's height (same rule as `per_line_h`). Unlike
|
||||
// `line_max_h` it has no `entity_h` floor, so it reflects the ACTUAL text
|
||||
@@ -1442,7 +1544,12 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
||||
MTextVAnchor::BottomOfTopLine => 0.0,
|
||||
};
|
||||
let attach_h_anchor = opts.attach_h_anchor;
|
||||
let box_left = -attach_h_anchor * rect_w;
|
||||
let block_width = if cols.active() {
|
||||
cols.total_width()
|
||||
} else {
|
||||
rect_w
|
||||
};
|
||||
let box_left = -attach_h_anchor * block_width;
|
||||
let rot = opts.rotation;
|
||||
let (cos_r, sin_r) = (rot.cos(), rot.sin());
|
||||
let ins_x = opts.insertion[0];
|
||||
@@ -1734,6 +1841,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
||||
origin,
|
||||
color,
|
||||
fill_tris,
|
||||
plane: None,
|
||||
run: Some(GlyphRun {
|
||||
text: s,
|
||||
font: font_name.to_string(),
|
||||
@@ -1806,6 +1914,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
||||
origin,
|
||||
color,
|
||||
fill_tris,
|
||||
plane: None,
|
||||
run: Some(GlyphRun {
|
||||
text: text.clone(),
|
||||
font: font_name.to_string(),
|
||||
@@ -1840,6 +1949,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
||||
origin,
|
||||
color,
|
||||
fill_tris: vec![],
|
||||
plane: None,
|
||||
run: None,
|
||||
});
|
||||
}
|
||||
@@ -1938,6 +2048,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
||||
origin,
|
||||
color,
|
||||
fill_tris,
|
||||
plane: None,
|
||||
run: Some(GlyphRun {
|
||||
text: text.to_string(),
|
||||
font: font_name.to_string(),
|
||||
@@ -1953,6 +2064,49 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
||||
emit(denominator, valign_dy);
|
||||
emit(numerator, valign_dy + run_h * STACK_RAISE);
|
||||
|
||||
if opts.want_glyph_boxes {
|
||||
let slots = numerator.chars().count()
|
||||
+ denominator.chars().count()
|
||||
+ usize::from(!denominator.is_empty());
|
||||
if slots > 0 {
|
||||
let top = valign_dy + run_h * (STACK_RAISE + STACK_HALF_SCALE);
|
||||
for index in 0..slots {
|
||||
let x0 = cursor_x + slot_w * index as f32 / slots as f32;
|
||||
let x1 = cursor_x + slot_w * (index + 1) as f32 / slots as f32;
|
||||
let corners = [
|
||||
to_world(line_base_x, line_base_y, x0, valign_dy),
|
||||
to_world(line_base_x, line_base_y, x1, valign_dy),
|
||||
to_world(line_base_x, line_base_y, x0, top),
|
||||
to_world(line_base_x, line_base_y, x1, top),
|
||||
];
|
||||
let xmin = corners
|
||||
.iter()
|
||||
.map(|p| p.0)
|
||||
.fold(f32::INFINITY, f32::min);
|
||||
let xmax = corners
|
||||
.iter()
|
||||
.map(|p| p.0)
|
||||
.fold(f32::NEG_INFINITY, f32::max);
|
||||
let ymin = corners
|
||||
.iter()
|
||||
.map(|p| p.1)
|
||||
.fold(f32::INFINITY, f32::min);
|
||||
let ymax = corners
|
||||
.iter()
|
||||
.map(|p| p.1)
|
||||
.fold(f32::NEG_INFINITY, f32::max);
|
||||
glyph_boxes.push(GlyphBox {
|
||||
vis,
|
||||
xmin,
|
||||
xmax,
|
||||
ymin,
|
||||
ymax,
|
||||
});
|
||||
vis += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if *bar && slot_w > 0.0 {
|
||||
// The rule is plain geometry, not a glyph, so it rides a
|
||||
// run-less group — those keep their strokes, while a
|
||||
@@ -1972,9 +2126,15 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
||||
origin: [ox, oy],
|
||||
color,
|
||||
fill_tris: Vec::new(),
|
||||
plane: None,
|
||||
run: None,
|
||||
});
|
||||
}
|
||||
local_bounds[0] = local_bounds[0].min(cursor_x);
|
||||
local_bounds[1] = local_bounds[1].min(line_ly + valign_dy);
|
||||
local_bounds[2] = local_bounds[2].max(cursor_x + slot_w);
|
||||
local_bounds[3] = local_bounds[3]
|
||||
.max(line_ly + valign_dy + run_h * (STACK_RAISE + STACK_HALF_SCALE));
|
||||
cursor_x += slot_w;
|
||||
}
|
||||
AtomKind::Space => {
|
||||
@@ -2041,6 +2201,7 @@ pub fn layout_mtext(opts: &MTextRenderOpts) -> MTextLayout {
|
||||
origin,
|
||||
color,
|
||||
fill_tris: vec![],
|
||||
plane: None,
|
||||
run: None,
|
||||
});
|
||||
}
|
||||
@@ -2169,6 +2330,7 @@ mod tests {
|
||||
oblique_angle: 0.0,
|
||||
is_backward: false,
|
||||
is_upside_down: false,
|
||||
is_vertical: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2199,6 +2361,8 @@ mod tests {
|
||||
attach_h_anchor: 0.0,
|
||||
v_anchor: MTextVAnchor::Top,
|
||||
line_spacing_factor: 1.0,
|
||||
exact_line_spacing: false,
|
||||
rectangle_height: 0.0,
|
||||
vertical_text: false,
|
||||
want_glyph_boxes: false,
|
||||
});
|
||||
@@ -2222,6 +2386,8 @@ mod tests {
|
||||
attach_h_anchor: 0.0,
|
||||
v_anchor: MTextVAnchor::Top,
|
||||
line_spacing_factor: 1.0,
|
||||
exact_line_spacing: false,
|
||||
rectangle_height: 0.0,
|
||||
vertical_text: false,
|
||||
want_glyph_boxes: false,
|
||||
});
|
||||
@@ -2378,6 +2544,7 @@ mod v_anchor_tests {
|
||||
oblique_angle: 0.0,
|
||||
is_backward: false,
|
||||
is_upside_down: false,
|
||||
is_vertical: false,
|
||||
};
|
||||
layout_mtext(&MTextRenderOpts {
|
||||
columns: Default::default(),
|
||||
@@ -2390,6 +2557,8 @@ mod v_anchor_tests {
|
||||
attach_h_anchor: 0.0,
|
||||
v_anchor: MTextVAnchor::Top,
|
||||
line_spacing_factor: 1.0,
|
||||
exact_line_spacing: false,
|
||||
rectangle_height: 0.0,
|
||||
vertical_text: false,
|
||||
want_glyph_boxes: true,
|
||||
})
|
||||
|
||||
+232
-124
@@ -3,8 +3,10 @@ use acadrust::entities::Tolerance;
|
||||
use crate::command::EntityTransform;
|
||||
use crate::entities::common::{edit_prop as edit, ro_prop as ro, square_grip};
|
||||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
|
||||
use crate::scene::convert::acad_to_render::{GlyphRun, TextStroke, RenderEntity, RenderObject};
|
||||
use crate::scene::model::object::{GripApply, GripDef, PropSection};
|
||||
use crate::scene::convert::acad_to_render::{
|
||||
GlyphRun, RenderEntity, RenderObject, TextPlane, TextStroke,
|
||||
};
|
||||
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
||||
use crate::scene::model::wire_model::SnapHint;
|
||||
use crate::scene::text::lff;
|
||||
use crate::scene::view::transform;
|
||||
@@ -131,7 +133,7 @@ fn parse_cell(s: &str) -> Cell {
|
||||
/// (`\Fgdt.shx|b0|i0|c134|p6;j`), so match the name and skip to the `;` rather
|
||||
/// than demanding the bare `\Fgdt;` form — the parameterised spelling is legal
|
||||
/// and would otherwise drop the symbol silently.
|
||||
fn symbol_font_switch(inner: &str) -> Option<char> {
|
||||
pub(crate) fn symbol_font_switch(inner: &str) -> Option<char> {
|
||||
let rest = inner
|
||||
.strip_prefix("\\F")
|
||||
.or_else(|| inner.strip_prefix("\\f"))?;
|
||||
@@ -144,51 +146,6 @@ fn symbol_font_switch(inner: &str) -> Option<char> {
|
||||
tail.chars().next()
|
||||
}
|
||||
|
||||
/// Per-entity overrides of individual dimension-style variables, carried as
|
||||
/// extended data under the `DSTYLE` application.
|
||||
///
|
||||
/// An entity may keep its style yet override single variables on itself, as
|
||||
/// `(variable group code, value)` pairs. Reading the style but ignoring these
|
||||
/// draws the frame at the style's size rather than its own — which is how this
|
||||
/// one, overriding its text height to a fraction of the style's, came out far
|
||||
/// too large.
|
||||
///
|
||||
/// Only the character height is read — it is the frame's single geometric
|
||||
/// input (see `tessellate_tolerance`); every other variable is left to the style.
|
||||
fn dstyle_overrides(tol: &Tolerance) -> Option<f64> {
|
||||
// The only variable the frame is built from.
|
||||
const DIMTXT: i16 = 140;
|
||||
|
||||
use acadrust::xdata::XDataValue as V;
|
||||
|
||||
// The record belongs to the shared "ACAD" application and names itself in
|
||||
// its FIRST STRING VALUE. There is no record called "DSTYLE" — asking for
|
||||
// one finds nothing and leaves every override silently unread.
|
||||
let rec = tol.common.extended_data.get_record("ACAD")?;
|
||||
let mut vals = rec.values.iter();
|
||||
match vals.next() {
|
||||
Some(V::String(s)) if s == "DSTYLE" => {}
|
||||
_ => return None,
|
||||
}
|
||||
|
||||
let mut txt = None;
|
||||
let mut pending: Option<i16> = None;
|
||||
for v in vals {
|
||||
match v {
|
||||
V::Integer16(code) => pending = Some(*code),
|
||||
V::Real(value) | V::Distance(value) => {
|
||||
if pending.take() == Some(DIMTXT) {
|
||||
txt = Some(*value);
|
||||
}
|
||||
}
|
||||
// Braces and anything else just delimit; a non-numeric value also
|
||||
// ends the pair we were waiting on.
|
||||
_ => pending = None,
|
||||
}
|
||||
}
|
||||
txt
|
||||
}
|
||||
|
||||
/// The tolerance's dimension style — by handle first, then by name.
|
||||
///
|
||||
/// The order matters and is not interchangeable: a DWG records the style's
|
||||
@@ -196,7 +153,7 @@ fn dstyle_overrides(tol: &Tolerance) -> Option<f64> {
|
||||
/// leaves the handle empty. Matching on the name alone — the shape used
|
||||
/// elsewhere for entities that only ever carry one — would silently resolve
|
||||
/// every DWG-read tolerance to "Standard" and pick the wrong metrics.
|
||||
fn resolve_dim_style<'a>(
|
||||
pub(crate) fn resolve_dim_style<'a>(
|
||||
tol: &Tolerance,
|
||||
doc: &'a acadrust::CadDocument,
|
||||
) -> Option<&'a acadrust::tables::DimStyle> {
|
||||
@@ -222,8 +179,20 @@ fn resolve_dim_style<'a>(
|
||||
///
|
||||
/// This is where the pen LANDS, so it carries the font's letter spacing past
|
||||
/// the final glyph — which is what puts the gap between one run and the next.
|
||||
fn run_advance(text: &str, font: &str, height: f32) -> f32 {
|
||||
crate::entities::text_support::text_local_bounds(font, text, height, 1.0, 0.0)
|
||||
fn run_advance(
|
||||
text: &str,
|
||||
font: &str,
|
||||
height: f32,
|
||||
width_factor: f32,
|
||||
oblique: f32,
|
||||
) -> f32 {
|
||||
crate::entities::text_support::text_local_bounds(
|
||||
font,
|
||||
text,
|
||||
height,
|
||||
width_factor,
|
||||
oblique,
|
||||
)
|
||||
.map(|b| b.advance)
|
||||
.unwrap_or(0.0)
|
||||
}
|
||||
@@ -232,8 +201,11 @@ fn run_advance(text: &str, font: &str, height: f32) -> f32 {
|
||||
///
|
||||
/// Glyph geometry is authored against a 9-unit cap height, so a font's spacing
|
||||
/// scales with the character height like everything else.
|
||||
fn letter_spacing(font: &str, height: f32) -> f32 {
|
||||
crate::scene::text::font_face::Face::resolve(font).letter_spacing() * height / 9.0
|
||||
fn letter_spacing(font: &str, height: f32, width_factor: f32) -> f32 {
|
||||
crate::scene::text::font_face::Face::resolve(font).letter_spacing()
|
||||
* height
|
||||
/ 9.0
|
||||
* width_factor
|
||||
}
|
||||
|
||||
/// How wide a compartment's content actually draws.
|
||||
@@ -243,36 +215,56 @@ fn letter_spacing(font: &str, height: f32) -> f32 {
|
||||
/// over-measures by exactly one spacing — the gaps between runs are real, the
|
||||
/// one hanging off the end is not — and every compartment came out that much
|
||||
/// too wide.
|
||||
fn content_width(cell: &Cell, height: f32) -> f32 {
|
||||
fn content_width(
|
||||
cell: &Cell,
|
||||
height: f32,
|
||||
text_style: &crate::entities::text_support::ResolvedTextStyle,
|
||||
) -> f32 {
|
||||
let Some(last) = cell.last() else {
|
||||
return 0.0;
|
||||
};
|
||||
let pen: f32 = cell
|
||||
.iter()
|
||||
.map(|r| run_advance(&r.text, r.font, height))
|
||||
.map(|run| {
|
||||
let (font, width, oblique) = if run.font == SYMBOL_FONT {
|
||||
(SYMBOL_FONT, 1.0, 0.0)
|
||||
} else {
|
||||
(
|
||||
text_style.font_name.as_str(),
|
||||
text_style.width_factor,
|
||||
text_style.oblique_angle,
|
||||
)
|
||||
};
|
||||
run_advance(&run.text, font, height, width, oblique)
|
||||
})
|
||||
.sum();
|
||||
(pen - letter_spacing(last.font, height)).max(0.0)
|
||||
let (last_font, last_width) = if last.font == SYMBOL_FONT {
|
||||
(SYMBOL_FONT, 1.0)
|
||||
} else {
|
||||
(text_style.font_name.as_str(), text_style.width_factor)
|
||||
};
|
||||
(pen - letter_spacing(last_font, height, last_width)).max(0.0)
|
||||
}
|
||||
|
||||
/// One text run of a feature-control frame, ready to become a `TextStroke`
|
||||
/// with a `GlyphRun` so the run can render as SDF glyph quads (or, when
|
||||
/// SDF is off, from `strokes`). `origin` is relative to the tolerance insertion
|
||||
/// point (already rotated); `strokes` are the glyph polylines rotated about the
|
||||
/// origin (no origin translation — the wire-builder adds the origin).
|
||||
/// point; `strokes` remain in the frame's local plane.
|
||||
struct TolCell {
|
||||
text: String,
|
||||
font: &'static str,
|
||||
font: String,
|
||||
origin: [f32; 2],
|
||||
strokes: Vec<Vec<[f32; 2]>>,
|
||||
height: f32,
|
||||
rotation: f32,
|
||||
width_factor: f32,
|
||||
oblique: f32,
|
||||
}
|
||||
|
||||
/// Tessellate a Tolerance entity's feature-control frame.
|
||||
///
|
||||
/// Returns (`box_strokes`, `cells`):
|
||||
/// - `box_strokes` — the outer border, row separators and column dividers as
|
||||
/// 2-D polylines (rotated; run-less, so they always render as strokes)
|
||||
/// - `box_strokes` — the outer border, row separators and column dividers in
|
||||
/// frame-local coordinates
|
||||
/// - `cells` — one [`TolCell`] per non-empty cell, carrying its text + a
|
||||
/// `GlyphRun` so the cell renders as SDF text (frame stays geometry).
|
||||
fn tessellate_tolerance(
|
||||
@@ -296,17 +288,29 @@ fn tessellate_tolerance(
|
||||
// and the frame draws about ten times too small. Falling back to them only
|
||||
// when no style resolves keeps DXF-read entities working.
|
||||
let style = resolve_dim_style(tol, doc);
|
||||
let scale = style
|
||||
.map(|s| if s.dimscale > 1e-6 { s.dimscale } else { 1.0 })
|
||||
let xd = &tol.common.extended_data;
|
||||
let scale = crate::entities::dim_override::real(xd, crate::entities::dim_override::DIMSCALE)
|
||||
.or_else(|| style.map(|s| s.dimscale))
|
||||
.map(|value| if value > 1e-6 { value } else { 1.0 })
|
||||
.unwrap_or(1.0);
|
||||
// The entity's own overrides win over its style; the style wins over the
|
||||
// entity's constructed defaults.
|
||||
let h = dstyle_overrides(tol)
|
||||
let h = crate::entities::dim_override::real(xd, crate::entities::dim_override::DIMTXT)
|
||||
.or(style.map(|s| s.dimtxt))
|
||||
.map(|v| v * scale)
|
||||
.unwrap_or(tol.text_height) as f32;
|
||||
let h = if h > 1e-6 { h } else { 2.5_f32 };
|
||||
|
||||
let text_style_name = crate::entities::dim_override::handle(
|
||||
xd,
|
||||
crate::entities::dim_override::DIMTXSTY,
|
||||
)
|
||||
.and_then(|handle| doc.text_styles.iter().find(|entry| entry.handle == handle))
|
||||
.map(|entry| entry.name.as_str())
|
||||
.or_else(|| style.map(|style| style.dimtxsty.as_str()))
|
||||
.unwrap_or("Standard");
|
||||
let text_style = crate::entities::text_support::resolve_text_style(text_style_name, doc);
|
||||
|
||||
// A compartment is a PROPORTION of the character height: it spans -h..+h
|
||||
// about the row's centreline, so it is exactly 2h tall and the h/2 margin
|
||||
// falls out of that rather than being an input.
|
||||
@@ -329,19 +333,14 @@ fn tessellate_tolerance(
|
||||
// (`len()`) would make each one two or three cells wide.
|
||||
// A compartment's runs come from different fonts, so each is measured in its
|
||||
// own before they are summed.
|
||||
let cell_width = |cell: &Cell| -> f32 { (content_width(cell, h) + 2.0 * pad).max(min_cell_w) };
|
||||
let cell_width = |cell: &Cell| -> f32 {
|
||||
(content_width(cell, h, &text_style) + 2.0 * pad).max(min_cell_w)
|
||||
};
|
||||
let row_widths: Vec<Vec<f32>> = rows
|
||||
.iter()
|
||||
.map(|row| row.iter().map(|c| cell_width(c)).collect())
|
||||
.collect();
|
||||
|
||||
// ── Transform helpers (local space — translation applied in tessellate.rs) ──
|
||||
let angle = (tol.direction.y as f32).atan2(tol.direction.x as f32);
|
||||
let (sa, ca) = angle.sin_cos();
|
||||
|
||||
// Rotate only; origin is kept as f64 and applied later with full precision.
|
||||
let rot = |x: f32, y: f32| -> [f32; 2] { [x * ca - y * sa, x * sa + y * ca] };
|
||||
|
||||
let mut box_out: Vec<Vec<[f32; 2]>> = Vec::new();
|
||||
let mut cells: Vec<TolCell> = Vec::new();
|
||||
|
||||
@@ -364,11 +363,11 @@ fn tessellate_tolerance(
|
||||
let y0 = row_bottom(ri);
|
||||
let y1 = y0 + cell_h;
|
||||
box_out.push(vec![
|
||||
rot(0.0, y0),
|
||||
rot(rw, y0),
|
||||
rot(rw, y1),
|
||||
rot(0.0, y1),
|
||||
rot(0.0, y0),
|
||||
[0.0, y0],
|
||||
[rw, y0],
|
||||
[rw, y1],
|
||||
[0.0, y1],
|
||||
[0.0, y0],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -379,15 +378,13 @@ fn tessellate_tolerance(
|
||||
let mut x_cursor = 0.0_f32;
|
||||
for w in widths.iter().take(widths.len().saturating_sub(1)) {
|
||||
x_cursor += w;
|
||||
box_out.push(vec![rot(x_cursor, y0), rot(x_cursor, y1)]);
|
||||
box_out.push(vec![[x_cursor, y0], [x_cursor, y1]]);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Text content per cell ─────────────────────────────────────────────
|
||||
// Each cell becomes a TolCell: its origin is the (rotated) cell position
|
||||
// relative to the insertion point; its `strokes` are the glyph polylines
|
||||
// rotated about that origin (used only when SDF is off). The GlyphRun the
|
||||
// caller attaches lets the cell render as SDF text.
|
||||
// Each cell stays in frame-local coordinates; the renderer maps the frame
|
||||
// into its world-space plane.
|
||||
for (ri, row) in rows.iter().enumerate() {
|
||||
// The compartment is 2h tall and the character h, so an h/2 margin
|
||||
// centres the text on the row's centreline.
|
||||
@@ -399,26 +396,41 @@ fn tessellate_tolerance(
|
||||
// Centre the compartment's whole content, then lay its runs out
|
||||
// left to right — each in its own font, each advancing the pen
|
||||
// by what that font actually measures.
|
||||
let mut run_x = cell_x + (cw - content_width(cell, h)) * 0.5;
|
||||
let mut run_x = cell_x + (cw - content_width(cell, h, &text_style)) * 0.5;
|
||||
for run in cell {
|
||||
let (text, font) = (run.text.clone(), run.font);
|
||||
let text = run.text.clone();
|
||||
let (font, width_factor, oblique) = if run.font == SYMBOL_FONT {
|
||||
(SYMBOL_FONT.to_string(), 1.0, 0.0)
|
||||
} else {
|
||||
(
|
||||
text_style.font_name.clone(),
|
||||
text_style.width_factor,
|
||||
text_style.oblique_angle,
|
||||
)
|
||||
};
|
||||
let (local_strokes, _) =
|
||||
lff::tessellate_text_ex([0.0, 0.0], h, 0.0, 1.0, 0.0, font, &text);
|
||||
// Glyph polylines rotated about the run's origin (no origin
|
||||
// translation — the wire-builder adds `origin`).
|
||||
lff::tessellate_text_ex(
|
||||
[0.0, 0.0],
|
||||
h,
|
||||
0.0,
|
||||
width_factor,
|
||||
oblique,
|
||||
&font,
|
||||
&text,
|
||||
);
|
||||
let strokes: Vec<Vec<[f32; 2]>> = local_strokes
|
||||
.into_iter()
|
||||
.map(|pl| pl.into_iter().map(|[px, py]| rot(px, py)).collect())
|
||||
.filter(|pl: &Vec<[f32; 2]>| !pl.is_empty())
|
||||
.collect();
|
||||
let advance = run_advance(&text, font, h);
|
||||
let advance = run_advance(&text, &font, h, width_factor, oblique);
|
||||
cells.push(TolCell {
|
||||
text,
|
||||
font,
|
||||
origin: rot(run_x, row_y),
|
||||
origin: [run_x, row_y],
|
||||
strokes,
|
||||
height: h,
|
||||
rotation: angle,
|
||||
width_factor,
|
||||
oblique,
|
||||
});
|
||||
run_x += advance;
|
||||
}
|
||||
@@ -430,6 +442,24 @@ fn tessellate_tolerance(
|
||||
(box_out, cells)
|
||||
}
|
||||
|
||||
pub(crate) fn preview_strokes(
|
||||
tol: &Tolerance,
|
||||
doc: &acadrust::CadDocument,
|
||||
) -> Vec<Vec<[f32; 2]>> {
|
||||
tessellate_tolerance(tol, doc).0
|
||||
}
|
||||
|
||||
fn frame_basis(tol: &Tolerance) -> (glam::DVec3, glam::DVec3) {
|
||||
let normal = glam::DVec3::new(tol.normal.x, tol.normal.y, tol.normal.z)
|
||||
.normalize_or(glam::DVec3::Z);
|
||||
let direction = glam::DVec3::new(tol.direction.x, tol.direction.y, tol.direction.z);
|
||||
let fallback = crate::scene::view::transform::ocs_axes((normal.x, normal.y, normal.z)).0;
|
||||
let fallback = glam::DVec3::new(fallback.0, fallback.1, fallback.2);
|
||||
let x_axis = (direction - normal * direction.dot(normal)).normalize_or(fallback);
|
||||
let y_axis = normal.cross(x_axis).normalize_or(glam::DVec3::Y);
|
||||
(x_axis, y_axis)
|
||||
}
|
||||
|
||||
// ── RenderConvertible ──────────────────────────────────────────────────────────
|
||||
|
||||
impl RenderConvertible for Tolerance {
|
||||
@@ -446,7 +476,57 @@ impl RenderConvertible for Tolerance {
|
||||
|
||||
// Build the feature-control frame in local space; origin stored as f64.
|
||||
let (box_strokes, cells) = tessellate_tolerance(self, document);
|
||||
let ins = [self.insertion_point.x, self.insertion_point.y];
|
||||
let ins = glam::DVec3::new(
|
||||
self.insertion_point.x,
|
||||
self.insertion_point.y,
|
||||
self.insertion_point.z,
|
||||
);
|
||||
let (x_axis, y_axis) = frame_basis(self);
|
||||
let plane = |origin: glam::DVec3| TextPlane {
|
||||
origin: origin.to_array(),
|
||||
scale_origin: ins.to_array(),
|
||||
x_axis: x_axis.to_array(),
|
||||
y_axis: y_axis.to_array(),
|
||||
};
|
||||
let style = resolve_dim_style(self, document);
|
||||
let xd = &self.common.extended_data;
|
||||
let explicit_color = |index: Option<i16>| {
|
||||
index.filter(|value| !matches!(value, 0 | 256)).map(|value| {
|
||||
let [red, green, blue, _] = crate::scene::convert::tess_util::aci_to_rgba(
|
||||
&acadrust::types::Color::from_index(value),
|
||||
);
|
||||
[red, green, blue]
|
||||
})
|
||||
};
|
||||
let color_value = |color: &acadrust::types::Color| {
|
||||
let [red, green, blue, _] =
|
||||
crate::scene::convert::tess_util::aci_to_rgba(color);
|
||||
[red, green, blue]
|
||||
};
|
||||
let frame_color = if let Some(index) = crate::entities::dim_override::int(
|
||||
xd,
|
||||
crate::entities::dim_override::DIMCLRD,
|
||||
) {
|
||||
explicit_color(Some(index))
|
||||
} else if let Some(color) =
|
||||
style.and_then(|entry| entry.dimclrd_true_color.as_ref())
|
||||
{
|
||||
Some(color_value(color))
|
||||
} else {
|
||||
explicit_color(style.map(|entry| entry.dimclrd))
|
||||
};
|
||||
let text_color = if let Some(index) = crate::entities::dim_override::int(
|
||||
xd,
|
||||
crate::entities::dim_override::DIMCLRT,
|
||||
) {
|
||||
explicit_color(Some(index))
|
||||
} else if let Some(color) =
|
||||
style.and_then(|entry| entry.dimclrt_true_color.as_ref())
|
||||
{
|
||||
Some(color_value(color))
|
||||
} else {
|
||||
explicit_color(style.map(|entry| entry.dimclrt))
|
||||
};
|
||||
|
||||
// Frame geometry first (run-less → always strokes; also the anchor
|
||||
// group so its origin = the insertion point), then one run-group per
|
||||
@@ -456,9 +536,10 @@ impl RenderConvertible for Tolerance {
|
||||
if !box_strokes.is_empty() {
|
||||
groups.push(TextStroke {
|
||||
strokes: box_strokes,
|
||||
origin: ins,
|
||||
color: None,
|
||||
origin: [ins.x, ins.y],
|
||||
color: frame_color,
|
||||
fill_tris: vec![],
|
||||
plane: Some(plane(ins)),
|
||||
run: None,
|
||||
});
|
||||
}
|
||||
@@ -466,18 +547,23 @@ impl RenderConvertible for Tolerance {
|
||||
groups.push(TextStroke {
|
||||
strokes: cell.strokes,
|
||||
origin: [
|
||||
ins[0] + cell.origin[0] as f64,
|
||||
ins[1] + cell.origin[1] as f64,
|
||||
ins.x + x_axis.x * cell.origin[0] as f64
|
||||
+ y_axis.x * cell.origin[1] as f64,
|
||||
ins.y + x_axis.y * cell.origin[0] as f64
|
||||
+ y_axis.y * cell.origin[1] as f64,
|
||||
],
|
||||
color: None,
|
||||
color: text_color,
|
||||
fill_tris: vec![],
|
||||
plane: Some(plane(
|
||||
ins + x_axis * cell.origin[0] as f64 + y_axis * cell.origin[1] as f64,
|
||||
)),
|
||||
run: Some(GlyphRun {
|
||||
text: cell.text,
|
||||
font: cell.font.to_string(),
|
||||
font: cell.font,
|
||||
height: cell.height,
|
||||
rotation: cell.rotation,
|
||||
width_factor: 1.0,
|
||||
oblique: 0.0,
|
||||
rotation: 0.0,
|
||||
width_factor: cell.width_factor,
|
||||
oblique: cell.oblique,
|
||||
// `tracking` scales the font's own letter spacing, so 0
|
||||
// collapses the gap between glyphs and the characters run
|
||||
// together. Every other text-bearing entity passes 1.0, and
|
||||
@@ -539,12 +625,19 @@ impl Grippable for Tolerance {
|
||||
// ── PropertyEditable ──────────────────────────────────────────────────────────
|
||||
|
||||
impl PropertyEditable for Tolerance {
|
||||
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
||||
fn geometry_properties(&self, text_style_names: &[String]) -> Vec<PropSection> {
|
||||
vec![
|
||||
PropSection {
|
||||
title: t!("Text").into_owned(),
|
||||
props: vec![
|
||||
ro(t!("Text style").as_ref(), "tol_text_style", String::new()),
|
||||
Property {
|
||||
label: t!("Text style").into_owned(),
|
||||
field: "tol_text_style",
|
||||
value: PropValue::Choice {
|
||||
selected: String::new(),
|
||||
options: text_style_names.to_vec(),
|
||||
},
|
||||
},
|
||||
edit(t!("Text height").as_ref(), "tol_text_height", self.text_height),
|
||||
],
|
||||
},
|
||||
@@ -558,20 +651,15 @@ impl PropertyEditable for Tolerance {
|
||||
},
|
||||
PropSection {
|
||||
title: t!("Misc").into_owned(),
|
||||
props: vec![
|
||||
ro(
|
||||
t!("Dimension style").as_ref(),
|
||||
"tol_dim_style",
|
||||
if self.dimension_style_name.is_empty() {
|
||||
"(default)".to_string()
|
||||
} else {
|
||||
self.dimension_style_name.clone()
|
||||
},
|
||||
),
|
||||
edit(t!("Direction X").as_ref(), "tol_dir_x", self.direction.x),
|
||||
edit(t!("Direction Y").as_ref(), "tol_dir_y", self.direction.y),
|
||||
edit(t!("Direction Z").as_ref(), "tol_dir_z", self.direction.z),
|
||||
],
|
||||
props: vec![ro(
|
||||
t!("Dim style").as_ref(),
|
||||
"tol_dim_style",
|
||||
if self.dimension_style_name.is_empty() {
|
||||
"(default)".to_string()
|
||||
} else {
|
||||
self.dimension_style_name.clone()
|
||||
},
|
||||
)],
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -585,9 +673,6 @@ impl PropertyEditable for Tolerance {
|
||||
"tol_iy" => self.insertion_point.y = v,
|
||||
"tol_iz" => self.insertion_point.z = v,
|
||||
"tol_text_height" if v > 0.0 => self.text_height = v,
|
||||
"tol_dir_x" => self.direction.x = v,
|
||||
"tol_dir_y" => self.direction.y = v,
|
||||
"tol_dir_z" => self.direction.z = v,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -923,9 +1008,26 @@ mod tests {
|
||||
fn compartments_are_measured_by_their_ink_not_the_trailing_pen_gap() {
|
||||
// Cap height, so glyph units and world units line up.
|
||||
let h = 9.0_f32;
|
||||
let document = acadrust::CadDocument::new();
|
||||
let text_style = crate::entities::text_support::resolve_text_style("Standard", &document);
|
||||
for src in ["{\\Fgdt;r}", "{\\Fgdt;n}tol{\\Fgdt;m}", "1{\\Fgdt;m}", "A"] {
|
||||
let cell = parse_cell(src);
|
||||
let pen: f32 = cell.iter().map(|r| run_advance(&r.text, r.font, h)).sum();
|
||||
let pen: f32 = cell
|
||||
.iter()
|
||||
.map(|run| {
|
||||
if run.font == SYMBOL_FONT {
|
||||
run_advance(&run.text, SYMBOL_FONT, h, 1.0, 0.0)
|
||||
} else {
|
||||
run_advance(
|
||||
&run.text,
|
||||
&text_style.font_name,
|
||||
h,
|
||||
text_style.width_factor,
|
||||
text_style.oblique_angle,
|
||||
)
|
||||
}
|
||||
})
|
||||
.sum();
|
||||
let ink: f32 = cell
|
||||
.iter()
|
||||
.map(|r| {
|
||||
@@ -934,11 +1036,17 @@ mod tests {
|
||||
.unwrap_or(0.0)
|
||||
})
|
||||
.sum();
|
||||
let got = content_width(&cell, h);
|
||||
let got = content_width(&cell, h, &text_style);
|
||||
|
||||
// Exactly one trailing gap comes off — the gaps BETWEEN runs are
|
||||
// real spacing and must stay.
|
||||
let spacing = letter_spacing(cell.last().unwrap().font, h);
|
||||
let last = cell.last().unwrap();
|
||||
let (font, width) = if last.font == SYMBOL_FONT {
|
||||
(SYMBOL_FONT, 1.0)
|
||||
} else {
|
||||
(text_style.font_name.as_str(), text_style.width_factor)
|
||||
};
|
||||
let spacing = letter_spacing(font, h, width);
|
||||
assert!(
|
||||
(got - (pen - spacing)).abs() < 1e-3,
|
||||
"{src:?}: content {got} should be pen {pen} less one {spacing} gap"
|
||||
|
||||
Reference in new issue
Block a user