2026-06-11 00:04:37 +03:00
|
|
|
|
use acadrust::entities::{LeaderContentType, MultiLeader, MultiLeaderPathType, TextAttachmentType};
|
fix(multileader): render text properly — strip MText codes, honor full layout
Multileader text was emitted as a single literal stroke pass with no MText
code handling, no rotation, no attachment, no line breaks, no per-piece
color and bad f32 precision under world_offset. Two separate render paths
both had the same bug, and one was masking the fix in the other.
This rewrite, applied to both paths:
- Strips MText format codes (`{\fArial Black|b0|i0|c162|p34;...}`) so the
rendered text is the actual content, not the markup.
- Honors text_rotation (preferring text_direction so transforms survive),
text_attachment_point (Left/Center/Right horizontal anchor), and
text_left_attachment for vertical alignment via a shared
v_offset_for_attachment helper.
- Splits on \P/\n/\N and word-wraps to text_width.
- Applies line_spacing_factor with the same 5/3 × height convention as
MText.
- Resolves the text style via text_style_handle (fallback STANDARD).
- Applies ml.scale_factor everywhere (arrow size, dogleg length, text
height, text width). anno_scale is now only applied when
enable_annotation_scale is set.
- Splits the scene-level output into separate WireModels so leader,
text, frame and background fill each carry their own colour
(ctx.text_color, ml.text_frame, ctx.background_fill_color).
- Mirrors text_direction, base_direction and root.direction in
apply_transform so a mirrored multileader's text follows the geometry
instead of staying at its original rotation.
- Computes the text origin and frame corners in offset-relative f32
space — drawings sit at large absolute coordinates and casting to f32
before subtracting world_offset was wrecking the rotated sub-glyph
precision.
2026-05-12 23:27:53 +03:00
|
|
|
|
|
feat(text): route every text-bearing entity through the run-aware MText pipeline
Lift the MText parser + renderer into a reusable `layout_mtext` helper in
`text_support` and re-point the remaining text-bearing entities at it, so
every inline format code (`\f`, `\C`/`\c`, `\H`, `\W`, `\Q`, `\T`, `\A`,
`\p…`, decorations, stacked fractions, paragraph indents, tab stops, …)
reaches the stroke output uniformly.
Per-entity changes:
- TABLE cell ([entities/table.rs]) — anchor + alignment math now feeds
`layout_mtext`; `\C7;Red`-style codes no longer render as literal
glyph noise.
- MLEADER MText content (both entity path in [entities/multileader.rs]
and the LOD-aware path in [scene/tessellate.rs]) — drops the
`strip_mtext_codes` + per-line `tessellate_text_ex` chain; the LOD 0
baseline / LOD 1 greek rect / LOD 2 stroke ladder and the text-frame /
bg-fill rectangle now read line widths from the layout.
- ATTRIB / ATTDEF — when `mtext_flag` is MultiLine / ConstantMultiLine
the value goes through the MText pipeline (SingleLine stays on the
Text-style anchor maths because the DXF spec disallows codes there).
- DIMENSION text_override — `dimension_text_entity` emits a synthetic
MText (instead of Text) whenever the value contains MText escapes;
the LOD ladder via `tessellate_entity_dim_text` handles both kinds.
Infrastructure:
- `MTextVAnchor` gains `MiddleOfTopLine`, `MiddleOfBottomLine`,
`BottomOfTopLine` so MLEADER's 9 attachment variants all map cleanly.
- `MTextLayout` returns `line_widths` / `line_height` / `v_offset` so
callers that draw chrome (frames, fills, baselines, greek rects)
share the same numbers as the rendered strokes.
- `mtext_line_count` re-routed through the new parser so LOD row counts
and rendered row counts can never disagree.
- Legacy helpers (`strip_mtext_codes`, `split_mtext_lines`,
`measure_mtext_chars`, `word_wrap`, `cxf::measure_text`, the
`v_offset_for_attachment` MLEADER offset table, `MTextLine::plain_text`)
deleted — all callers migrated.
Audit (TEXT_FORMAT_CODE_INTEGRATION.md) removed; its plan is implemented
here.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 05:53:01 +03:00
|
|
|
|
use crate::entities::text_support::{
|
2026-06-03 16:17:29 +03:00
|
|
|
|
layout_mtext, resolve_text_style, MTextRenderOpts, MTextVAnchor, ResolvedTextStyle,
|
feat(text): route every text-bearing entity through the run-aware MText pipeline
Lift the MText parser + renderer into a reusable `layout_mtext` helper in
`text_support` and re-point the remaining text-bearing entities at it, so
every inline format code (`\f`, `\C`/`\c`, `\H`, `\W`, `\Q`, `\T`, `\A`,
`\p…`, decorations, stacked fractions, paragraph indents, tab stops, …)
reaches the stroke output uniformly.
Per-entity changes:
- TABLE cell ([entities/table.rs]) — anchor + alignment math now feeds
`layout_mtext`; `\C7;Red`-style codes no longer render as literal
glyph noise.
- MLEADER MText content (both entity path in [entities/multileader.rs]
and the LOD-aware path in [scene/tessellate.rs]) — drops the
`strip_mtext_codes` + per-line `tessellate_text_ex` chain; the LOD 0
baseline / LOD 1 greek rect / LOD 2 stroke ladder and the text-frame /
bg-fill rectangle now read line widths from the layout.
- ATTRIB / ATTDEF — when `mtext_flag` is MultiLine / ConstantMultiLine
the value goes through the MText pipeline (SingleLine stays on the
Text-style anchor maths because the DXF spec disallows codes there).
- DIMENSION text_override — `dimension_text_entity` emits a synthetic
MText (instead of Text) whenever the value contains MText escapes;
the LOD ladder via `tessellate_entity_dim_text` handles both kinds.
Infrastructure:
- `MTextVAnchor` gains `MiddleOfTopLine`, `MiddleOfBottomLine`,
`BottomOfTopLine` so MLEADER's 9 attachment variants all map cleanly.
- `MTextLayout` returns `line_widths` / `line_height` / `v_offset` so
callers that draw chrome (frames, fills, baselines, greek rects)
share the same numbers as the rendered strokes.
- `mtext_line_count` re-routed through the new parser so LOD row counts
and rendered row counts can never disagree.
- Legacy helpers (`strip_mtext_codes`, `split_mtext_lines`,
`measure_mtext_chars`, `word_wrap`, `cxf::measure_text`, the
`v_offset_for_attachment` MLEADER offset table, `MTextLine::plain_text`)
deleted — all callers migrated.
Audit (TEXT_FORMAT_CODE_INTEGRATION.md) removed; its plan is implemented
here.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 05:53:01 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/// Map MLEADER's vertical attachment enum onto the shared `MTextVAnchor`
|
|
|
|
|
|
/// used by `layout_mtext`. Replaces the old `v_offset_for_attachment`
|
|
|
|
|
|
/// (which inlined the offset math); the shared pipeline now derives the
|
|
|
|
|
|
/// offset from the variant + n_lines / line_h.
|
|
|
|
|
|
pub(crate) fn mleader_v_anchor(attach: TextAttachmentType) -> MTextVAnchor {
|
fix(multileader): render text properly — strip MText codes, honor full layout
Multileader text was emitted as a single literal stroke pass with no MText
code handling, no rotation, no attachment, no line breaks, no per-piece
color and bad f32 precision under world_offset. Two separate render paths
both had the same bug, and one was masking the fix in the other.
This rewrite, applied to both paths:
- Strips MText format codes (`{\fArial Black|b0|i0|c162|p34;...}`) so the
rendered text is the actual content, not the markup.
- Honors text_rotation (preferring text_direction so transforms survive),
text_attachment_point (Left/Center/Right horizontal anchor), and
text_left_attachment for vertical alignment via a shared
v_offset_for_attachment helper.
- Splits on \P/\n/\N and word-wraps to text_width.
- Applies line_spacing_factor with the same 5/3 × height convention as
MText.
- Resolves the text style via text_style_handle (fallback STANDARD).
- Applies ml.scale_factor everywhere (arrow size, dogleg length, text
height, text width). anno_scale is now only applied when
enable_annotation_scale is set.
- Splits the scene-level output into separate WireModels so leader,
text, frame and background fill each carry their own colour
(ctx.text_color, ml.text_frame, ctx.background_fill_color).
- Mirrors text_direction, base_direction and root.direction in
apply_transform so a mirrored multileader's text follows the geometry
instead of staying at its original rotation.
- Computes the text origin and frame corners in offset-relative f32
space — drawings sit at large absolute coordinates and casting to f32
before subtracting world_offset was wrecking the rotated sub-glyph
precision.
2026-05-12 23:27:53 +03:00
|
|
|
|
match attach {
|
feat(text): route every text-bearing entity through the run-aware MText pipeline
Lift the MText parser + renderer into a reusable `layout_mtext` helper in
`text_support` and re-point the remaining text-bearing entities at it, so
every inline format code (`\f`, `\C`/`\c`, `\H`, `\W`, `\Q`, `\T`, `\A`,
`\p…`, decorations, stacked fractions, paragraph indents, tab stops, …)
reaches the stroke output uniformly.
Per-entity changes:
- TABLE cell ([entities/table.rs]) — anchor + alignment math now feeds
`layout_mtext`; `\C7;Red`-style codes no longer render as literal
glyph noise.
- MLEADER MText content (both entity path in [entities/multileader.rs]
and the LOD-aware path in [scene/tessellate.rs]) — drops the
`strip_mtext_codes` + per-line `tessellate_text_ex` chain; the LOD 0
baseline / LOD 1 greek rect / LOD 2 stroke ladder and the text-frame /
bg-fill rectangle now read line widths from the layout.
- ATTRIB / ATTDEF — when `mtext_flag` is MultiLine / ConstantMultiLine
the value goes through the MText pipeline (SingleLine stays on the
Text-style anchor maths because the DXF spec disallows codes there).
- DIMENSION text_override — `dimension_text_entity` emits a synthetic
MText (instead of Text) whenever the value contains MText escapes;
the LOD ladder via `tessellate_entity_dim_text` handles both kinds.
Infrastructure:
- `MTextVAnchor` gains `MiddleOfTopLine`, `MiddleOfBottomLine`,
`BottomOfTopLine` so MLEADER's 9 attachment variants all map cleanly.
- `MTextLayout` returns `line_widths` / `line_height` / `v_offset` so
callers that draw chrome (frames, fills, baselines, greek rects)
share the same numbers as the rendered strokes.
- `mtext_line_count` re-routed through the new parser so LOD row counts
and rendered row counts can never disagree.
- Legacy helpers (`strip_mtext_codes`, `split_mtext_lines`,
`measure_mtext_chars`, `word_wrap`, `cxf::measure_text`, the
`v_offset_for_attachment` MLEADER offset table, `MTextLine::plain_text`)
deleted — all callers migrated.
Audit (TEXT_FORMAT_CODE_INTEGRATION.md) removed; its plan is implemented
here.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 05:53:01 +03:00
|
|
|
|
TextAttachmentType::TopOfTopLine => MTextVAnchor::Top,
|
|
|
|
|
|
TextAttachmentType::MiddleOfTopLine => MTextVAnchor::MiddleOfTopLine,
|
fix(multileader): render text properly — strip MText codes, honor full layout
Multileader text was emitted as a single literal stroke pass with no MText
code handling, no rotation, no attachment, no line breaks, no per-piece
color and bad f32 precision under world_offset. Two separate render paths
both had the same bug, and one was masking the fix in the other.
This rewrite, applied to both paths:
- Strips MText format codes (`{\fArial Black|b0|i0|c162|p34;...}`) so the
rendered text is the actual content, not the markup.
- Honors text_rotation (preferring text_direction so transforms survive),
text_attachment_point (Left/Center/Right horizontal anchor), and
text_left_attachment for vertical alignment via a shared
v_offset_for_attachment helper.
- Splits on \P/\n/\N and word-wraps to text_width.
- Applies line_spacing_factor with the same 5/3 × height convention as
MText.
- Resolves the text style via text_style_handle (fallback STANDARD).
- Applies ml.scale_factor everywhere (arrow size, dogleg length, text
height, text width). anno_scale is now only applied when
enable_annotation_scale is set.
- Splits the scene-level output into separate WireModels so leader,
text, frame and background fill each carry their own colour
(ctx.text_color, ml.text_frame, ctx.background_fill_color).
- Mirrors text_direction, base_direction and root.direction in
apply_transform so a mirrored multileader's text follows the geometry
instead of staying at its original rotation.
- Computes the text origin and frame corners in offset-relative f32
space — drawings sit at large absolute coordinates and casting to f32
before subtracting world_offset was wrecking the rotated sub-glyph
precision.
2026-05-12 23:27:53 +03:00
|
|
|
|
TextAttachmentType::MiddleOfText
|
|
|
|
|
|
| TextAttachmentType::CenterOfText
|
feat(text): route every text-bearing entity through the run-aware MText pipeline
Lift the MText parser + renderer into a reusable `layout_mtext` helper in
`text_support` and re-point the remaining text-bearing entities at it, so
every inline format code (`\f`, `\C`/`\c`, `\H`, `\W`, `\Q`, `\T`, `\A`,
`\p…`, decorations, stacked fractions, paragraph indents, tab stops, …)
reaches the stroke output uniformly.
Per-entity changes:
- TABLE cell ([entities/table.rs]) — anchor + alignment math now feeds
`layout_mtext`; `\C7;Red`-style codes no longer render as literal
glyph noise.
- MLEADER MText content (both entity path in [entities/multileader.rs]
and the LOD-aware path in [scene/tessellate.rs]) — drops the
`strip_mtext_codes` + per-line `tessellate_text_ex` chain; the LOD 0
baseline / LOD 1 greek rect / LOD 2 stroke ladder and the text-frame /
bg-fill rectangle now read line widths from the layout.
- ATTRIB / ATTDEF — when `mtext_flag` is MultiLine / ConstantMultiLine
the value goes through the MText pipeline (SingleLine stays on the
Text-style anchor maths because the DXF spec disallows codes there).
- DIMENSION text_override — `dimension_text_entity` emits a synthetic
MText (instead of Text) whenever the value contains MText escapes;
the LOD ladder via `tessellate_entity_dim_text` handles both kinds.
Infrastructure:
- `MTextVAnchor` gains `MiddleOfTopLine`, `MiddleOfBottomLine`,
`BottomOfTopLine` so MLEADER's 9 attachment variants all map cleanly.
- `MTextLayout` returns `line_widths` / `line_height` / `v_offset` so
callers that draw chrome (frames, fills, baselines, greek rects)
share the same numbers as the rendered strokes.
- `mtext_line_count` re-routed through the new parser so LOD row counts
and rendered row counts can never disagree.
- Legacy helpers (`strip_mtext_codes`, `split_mtext_lines`,
`measure_mtext_chars`, `word_wrap`, `cxf::measure_text`, the
`v_offset_for_attachment` MLEADER offset table, `MTextLine::plain_text`)
deleted — all callers migrated.
Audit (TEXT_FORMAT_CODE_INTEGRATION.md) removed; its plan is implemented
here.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 05:53:01 +03:00
|
|
|
|
| TextAttachmentType::CenterOfTextOverline => MTextVAnchor::Middle,
|
|
|
|
|
|
TextAttachmentType::MiddleOfBottomLine => MTextVAnchor::MiddleOfBottomLine,
|
fix(multileader): render text properly — strip MText codes, honor full layout
Multileader text was emitted as a single literal stroke pass with no MText
code handling, no rotation, no attachment, no line breaks, no per-piece
color and bad f32 precision under world_offset. Two separate render paths
both had the same bug, and one was masking the fix in the other.
This rewrite, applied to both paths:
- Strips MText format codes (`{\fArial Black|b0|i0|c162|p34;...}`) so the
rendered text is the actual content, not the markup.
- Honors text_rotation (preferring text_direction so transforms survive),
text_attachment_point (Left/Center/Right horizontal anchor), and
text_left_attachment for vertical alignment via a shared
v_offset_for_attachment helper.
- Splits on \P/\n/\N and word-wraps to text_width.
- Applies line_spacing_factor with the same 5/3 × height convention as
MText.
- Resolves the text style via text_style_handle (fallback STANDARD).
- Applies ml.scale_factor everywhere (arrow size, dogleg length, text
height, text width). anno_scale is now only applied when
enable_annotation_scale is set.
- Splits the scene-level output into separate WireModels so leader,
text, frame and background fill each carry their own colour
(ctx.text_color, ml.text_frame, ctx.background_fill_color).
- Mirrors text_direction, base_direction and root.direction in
apply_transform so a mirrored multileader's text follows the geometry
instead of staying at its original rotation.
- Computes the text origin and frame corners in offset-relative f32
space — drawings sit at large absolute coordinates and casting to f32
before subtracting world_offset was wrecking the rotated sub-glyph
precision.
2026-05-12 23:27:53 +03:00
|
|
|
|
TextAttachmentType::BottomOfBottomLine | TextAttachmentType::BottomLine => {
|
feat(text): route every text-bearing entity through the run-aware MText pipeline
Lift the MText parser + renderer into a reusable `layout_mtext` helper in
`text_support` and re-point the remaining text-bearing entities at it, so
every inline format code (`\f`, `\C`/`\c`, `\H`, `\W`, `\Q`, `\T`, `\A`,
`\p…`, decorations, stacked fractions, paragraph indents, tab stops, …)
reaches the stroke output uniformly.
Per-entity changes:
- TABLE cell ([entities/table.rs]) — anchor + alignment math now feeds
`layout_mtext`; `\C7;Red`-style codes no longer render as literal
glyph noise.
- MLEADER MText content (both entity path in [entities/multileader.rs]
and the LOD-aware path in [scene/tessellate.rs]) — drops the
`strip_mtext_codes` + per-line `tessellate_text_ex` chain; the LOD 0
baseline / LOD 1 greek rect / LOD 2 stroke ladder and the text-frame /
bg-fill rectangle now read line widths from the layout.
- ATTRIB / ATTDEF — when `mtext_flag` is MultiLine / ConstantMultiLine
the value goes through the MText pipeline (SingleLine stays on the
Text-style anchor maths because the DXF spec disallows codes there).
- DIMENSION text_override — `dimension_text_entity` emits a synthetic
MText (instead of Text) whenever the value contains MText escapes;
the LOD ladder via `tessellate_entity_dim_text` handles both kinds.
Infrastructure:
- `MTextVAnchor` gains `MiddleOfTopLine`, `MiddleOfBottomLine`,
`BottomOfTopLine` so MLEADER's 9 attachment variants all map cleanly.
- `MTextLayout` returns `line_widths` / `line_height` / `v_offset` so
callers that draw chrome (frames, fills, baselines, greek rects)
share the same numbers as the rendered strokes.
- `mtext_line_count` re-routed through the new parser so LOD row counts
and rendered row counts can never disagree.
- Legacy helpers (`strip_mtext_codes`, `split_mtext_lines`,
`measure_mtext_chars`, `word_wrap`, `cxf::measure_text`, the
`v_offset_for_attachment` MLEADER offset table, `MTextLine::plain_text`)
deleted — all callers migrated.
Audit (TEXT_FORMAT_CODE_INTEGRATION.md) removed; its plan is implemented
here.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 05:53:01 +03:00
|
|
|
|
MTextVAnchor::Bottom
|
fix(multileader): render text properly — strip MText codes, honor full layout
Multileader text was emitted as a single literal stroke pass with no MText
code handling, no rotation, no attachment, no line breaks, no per-piece
color and bad f32 precision under world_offset. Two separate render paths
both had the same bug, and one was masking the fix in the other.
This rewrite, applied to both paths:
- Strips MText format codes (`{\fArial Black|b0|i0|c162|p34;...}`) so the
rendered text is the actual content, not the markup.
- Honors text_rotation (preferring text_direction so transforms survive),
text_attachment_point (Left/Center/Right horizontal anchor), and
text_left_attachment for vertical alignment via a shared
v_offset_for_attachment helper.
- Splits on \P/\n/\N and word-wraps to text_width.
- Applies line_spacing_factor with the same 5/3 × height convention as
MText.
- Resolves the text style via text_style_handle (fallback STANDARD).
- Applies ml.scale_factor everywhere (arrow size, dogleg length, text
height, text width). anno_scale is now only applied when
enable_annotation_scale is set.
- Splits the scene-level output into separate WireModels so leader,
text, frame and background fill each carry their own colour
(ctx.text_color, ml.text_frame, ctx.background_fill_color).
- Mirrors text_direction, base_direction and root.direction in
apply_transform so a mirrored multileader's text follows the geometry
instead of staying at its original rotation.
- Computes the text origin and frame corners in offset-relative f32
space — drawings sit at large absolute coordinates and casting to f32
before subtracting world_offset was wrecking the rotated sub-glyph
precision.
2026-05-12 23:27:53 +03:00
|
|
|
|
}
|
|
|
|
|
|
TextAttachmentType::BottomOfTopLineUnderlineBottomLine
|
|
|
|
|
|
| TextAttachmentType::BottomOfTopLineUnderlineTopLine
|
feat(text): route every text-bearing entity through the run-aware MText pipeline
Lift the MText parser + renderer into a reusable `layout_mtext` helper in
`text_support` and re-point the remaining text-bearing entities at it, so
every inline format code (`\f`, `\C`/`\c`, `\H`, `\W`, `\Q`, `\T`, `\A`,
`\p…`, decorations, stacked fractions, paragraph indents, tab stops, …)
reaches the stroke output uniformly.
Per-entity changes:
- TABLE cell ([entities/table.rs]) — anchor + alignment math now feeds
`layout_mtext`; `\C7;Red`-style codes no longer render as literal
glyph noise.
- MLEADER MText content (both entity path in [entities/multileader.rs]
and the LOD-aware path in [scene/tessellate.rs]) — drops the
`strip_mtext_codes` + per-line `tessellate_text_ex` chain; the LOD 0
baseline / LOD 1 greek rect / LOD 2 stroke ladder and the text-frame /
bg-fill rectangle now read line widths from the layout.
- ATTRIB / ATTDEF — when `mtext_flag` is MultiLine / ConstantMultiLine
the value goes through the MText pipeline (SingleLine stays on the
Text-style anchor maths because the DXF spec disallows codes there).
- DIMENSION text_override — `dimension_text_entity` emits a synthetic
MText (instead of Text) whenever the value contains MText escapes;
the LOD ladder via `tessellate_entity_dim_text` handles both kinds.
Infrastructure:
- `MTextVAnchor` gains `MiddleOfTopLine`, `MiddleOfBottomLine`,
`BottomOfTopLine` so MLEADER's 9 attachment variants all map cleanly.
- `MTextLayout` returns `line_widths` / `line_height` / `v_offset` so
callers that draw chrome (frames, fills, baselines, greek rects)
share the same numbers as the rendered strokes.
- `mtext_line_count` re-routed through the new parser so LOD row counts
and rendered row counts can never disagree.
- Legacy helpers (`strip_mtext_codes`, `split_mtext_lines`,
`measure_mtext_chars`, `word_wrap`, `cxf::measure_text`, the
`v_offset_for_attachment` MLEADER offset table, `MTextLine::plain_text`)
deleted — all callers migrated.
Audit (TEXT_FORMAT_CODE_INTEGRATION.md) removed; its plan is implemented
here.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 05:53:01 +03:00
|
|
|
|
| TextAttachmentType::BottomOfTopLineUnderlineAll => MTextVAnchor::BottomOfTopLine,
|
fix(multileader): render text properly — strip MText codes, honor full layout
Multileader text was emitted as a single literal stroke pass with no MText
code handling, no rotation, no attachment, no line breaks, no per-piece
color and bad f32 precision under world_offset. Two separate render paths
both had the same bug, and one was masking the fix in the other.
This rewrite, applied to both paths:
- Strips MText format codes (`{\fArial Black|b0|i0|c162|p34;...}`) so the
rendered text is the actual content, not the markup.
- Honors text_rotation (preferring text_direction so transforms survive),
text_attachment_point (Left/Center/Right horizontal anchor), and
text_left_attachment for vertical alignment via a shared
v_offset_for_attachment helper.
- Splits on \P/\n/\N and word-wraps to text_width.
- Applies line_spacing_factor with the same 5/3 × height convention as
MText.
- Resolves the text style via text_style_handle (fallback STANDARD).
- Applies ml.scale_factor everywhere (arrow size, dogleg length, text
height, text width). anno_scale is now only applied when
enable_annotation_scale is set.
- Splits the scene-level output into separate WireModels so leader,
text, frame and background fill each carry their own colour
(ctx.text_color, ml.text_frame, ctx.background_fill_color).
- Mirrors text_direction, base_direction and root.direction in
apply_transform so a mirrored multileader's text follows the geometry
instead of staying at its original rotation.
- Computes the text origin and frame corners in offset-relative f32
space — drawings sit at large absolute coordinates and casting to f32
before subtracting world_offset was wrecking the rotated sub-glyph
precision.
2026-05-12 23:27:53 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
feat(f64): command layer compiles on DVec3
Whole command/snap/grip/cursor chain now builds on f64 DVec3 end to end,
so picked points, snaps and grip drags stay precise at UTM-scale
coordinates instead of quantizing to ~0.5 m through f32.
- All ~80 command files migrated: draw (line/circle/arc/ellipse/shapes
in full f64; others via point methods), modify, annotate, insert, view,
inquiry, layers, groups, clipboard, layout.
- Entity apply_grip impls updated for GripApply::{Absolute,Translate}(DVec3).
- Construction sites (EntityTransform/CmdResult/DynAnchor) emit DVec3;
wire-preview boundaries cast to f32 only at the GPU edge.
- Modify/annotate commands keep internal Vec3 geometry where precision is
non-critical, casting at the DVec3 boundary; key draw commands store f64.
Builds clean (no warnings). Visual verification of crosshair/snap/grip at
extreme UTM zoom pending.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 03:51:50 +03:00
|
|
|
|
use glam::DVec3;
|
2026-03-22 14:56:27 +03:00
|
|
|
|
|
|
|
|
|
|
use crate::command::EntityTransform;
|
2026-06-03 16:17:29 +03:00
|
|
|
|
use crate::entities::common::{
|
|
|
|
|
|
center_grip, edit_prop as edit, ro_prop as ro, square_grip, triangle_grip,
|
|
|
|
|
|
};
|
2026-06-11 00:04:37 +03:00
|
|
|
|
use crate::entities::traits::TruckConvertible;
|
2026-06-16 11:33:46 +03:00
|
|
|
|
use crate::scene::convert::acad_to_truck::{TruckEntity, TruckObject};
|
|
|
|
|
|
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
|
|
|
|
|
use crate::scene::model::wire_model::{SnapHint, TangentGeom};
|
2026-03-22 14:56:27 +03:00
|
|
|
|
|
|
|
|
|
|
// ── TruckConvertible ────────────────────────────────────────────────────────
|
|
|
|
|
|
|
2026-04-27 02:29:41 +03:00
|
|
|
|
/// Catmull-Rom spline tessellation through `ctrl` points, `segs_per_span` segments each.
|
2026-05-12 23:47:40 +03:00
|
|
|
|
/// Operates in f64 so it can be applied to either WCS-direct coordinates (entity path)
|
|
|
|
|
|
/// or offset-relative coordinates (scene path) without precision loss.
|
|
|
|
|
|
pub(crate) fn catmull_rom_pts(ctrl: &[[f64; 3]], segs_per_span: u32) -> Vec<[f64; 3]> {
|
2026-04-27 02:29:41 +03:00
|
|
|
|
let n = ctrl.len();
|
|
|
|
|
|
let mut out = Vec::new();
|
|
|
|
|
|
for i in 0..n.saturating_sub(1) {
|
|
|
|
|
|
let p0 = if i == 0 { ctrl[0] } else { ctrl[i - 1] };
|
|
|
|
|
|
let p1 = ctrl[i];
|
|
|
|
|
|
let p2 = ctrl[i + 1];
|
|
|
|
|
|
let p3 = if i + 2 < n { ctrl[i + 2] } else { ctrl[n - 1] };
|
|
|
|
|
|
for j in 0..=segs_per_span {
|
2026-05-12 23:47:40 +03:00
|
|
|
|
let t = j as f64 / segs_per_span as f64;
|
2026-04-27 02:29:41 +03:00
|
|
|
|
let t2 = t * t;
|
|
|
|
|
|
let t3 = t2 * t;
|
2026-05-12 23:47:40 +03:00
|
|
|
|
let mut pt = [0.0_f64; 3];
|
2026-04-27 02:29:41 +03:00
|
|
|
|
for k in 0..3 {
|
2026-05-12 10:36:15 +03:00
|
|
|
|
pt[k] = 0.5
|
|
|
|
|
|
* ((2.0 * p1[k])
|
|
|
|
|
|
+ (-p0[k] + p2[k]) * t
|
|
|
|
|
|
+ (2.0 * p0[k] - 5.0 * p1[k] + 4.0 * p2[k] - p3[k]) * t2
|
|
|
|
|
|
+ (-p0[k] + 3.0 * p1[k] - 3.0 * p2[k] + p3[k]) * t3);
|
2026-04-27 02:29:41 +03:00
|
|
|
|
}
|
|
|
|
|
|
out.push(pt);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
out
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 14:56:27 +03:00
|
|
|
|
fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
2026-05-12 23:47:40 +03:00
|
|
|
|
let nan = [f64::NAN; 3];
|
|
|
|
|
|
let p3 = |v: &acadrust::types::Vector3| -> [f64; 3] { [v.x, v.y, v.z] };
|
2026-03-22 14:56:27 +03:00
|
|
|
|
|
2026-05-12 23:47:40 +03:00
|
|
|
|
let arrow_size = ml.arrowhead_size;
|
2026-03-22 14:56:27 +03:00
|
|
|
|
let draw_arrow = arrow_size > 0.0;
|
|
|
|
|
|
let invisible = ml.path_type == MultiLeaderPathType::Invisible;
|
|
|
|
|
|
|
2026-05-12 23:47:40 +03:00
|
|
|
|
let mut points: Vec<[f64; 3]> = Vec::new();
|
2026-03-22 14:56:27 +03:00
|
|
|
|
let mut tangents: Vec<TangentGeom> = Vec::new();
|
2026-05-12 23:47:40 +03:00
|
|
|
|
let mut key_verts: Vec<[f64; 3]> = Vec::new();
|
refactor(snap): producers emit f64 snap points (world_offset removal P3)
TruckEntity.snap_pts is now DVec3 and every entity producer builds snap
candidates straight from the acadrust f64 coordinates instead of casting
to f32 first (text, mtext, arc, circle, ellipse, point, shape, solid,
mline, mtext, attribute, tolerance, table, underlay, ole2frame, mesh,
multileader, dimension, block-cache). offset_snap_pts stays f64
throughout. Still offset-relative (inert) — but snap precision is no
longer capped at ~0.5 m by an early f32 cast, which fixes a latent
UTM-scale snap error and sets up the absolute-coordinate switch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 22:10:29 +03:00
|
|
|
|
let mut snap_pts: Vec<(glam::DVec3, SnapHint)> = Vec::new();
|
2026-03-22 14:56:27 +03:00
|
|
|
|
let mut first = true;
|
|
|
|
|
|
|
refactor(snap): producers emit f64 snap points (world_offset removal P3)
TruckEntity.snap_pts is now DVec3 and every entity producer builds snap
candidates straight from the acadrust f64 coordinates instead of casting
to f32 first (text, mtext, arc, circle, ellipse, point, shape, solid,
mline, mtext, attribute, tolerance, table, underlay, ole2frame, mesh,
multileader, dimension, block-cache). offset_snap_pts stays f64
throughout. Still offset-relative (inert) — but snap precision is no
longer capped at ~0.5 m by an early f32 cast, which fixes a latent
UTM-scale snap error and sets up the absolute-coordinate switch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-23 22:10:29 +03:00
|
|
|
|
let node = |arr: [f64; 3]| (glam::DVec3::new(arr[0], arr[1], arr[2]), SnapHint::Node);
|
2026-05-02 19:59:11 +03:00
|
|
|
|
|
2026-06-03 16:17:29 +03:00
|
|
|
|
// Text-side geometry, recomputed every frame so dragging the arrow or the
|
|
|
|
|
|
// text re-mirrors the whole layout. The text block is centred on its grip
|
|
|
|
|
|
// (text_location); the landing meets the text's near edge and the leader
|
|
|
|
|
|
// line ends one dogleg before that.
|
|
|
|
|
|
let text_loc = ml.context.text_location;
|
|
|
|
|
|
let leader_ref = ml
|
|
|
|
|
|
.context
|
|
|
|
|
|
.leader_roots
|
|
|
|
|
|
.first()
|
|
|
|
|
|
.and_then(|r| r.lines.first())
|
|
|
|
|
|
.and_then(|l| l.points.last())
|
|
|
|
|
|
.copied()
|
|
|
|
|
|
.or_else(|| ml.context.leader_roots.first().map(|r| r.connection_point))
|
|
|
|
|
|
.unwrap_or(text_loc);
|
2026-06-22 18:18:35 +03:00
|
|
|
|
// Reading direction of the text / landing — the stored text_direction
|
|
|
|
|
|
// (UCS X for UCS-placed leaders, world X otherwise), so the whole text-side
|
|
|
|
|
|
// layout follows the UCS instead of world horizontal.
|
|
|
|
|
|
let (tdx, tdy) = {
|
|
|
|
|
|
let td = ml.context.text_direction;
|
|
|
|
|
|
let l = (td.x * td.x + td.y * td.y).sqrt();
|
|
|
|
|
|
if l > 1e-9 {
|
|
|
|
|
|
(td.x / l, td.y / l)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
let a = ml.context.text_rotation;
|
|
|
|
|
|
(a.cos(), a.sin())
|
|
|
|
|
|
}
|
2026-06-11 00:04:37 +03:00
|
|
|
|
};
|
2026-06-22 18:18:35 +03:00
|
|
|
|
// Which side of the leader the text sits on, measured along that direction.
|
|
|
|
|
|
let rel_dot = (text_loc.x - leader_ref.x) * tdx + (text_loc.y - leader_ref.y) * tdy;
|
|
|
|
|
|
let text_sign: f64 = if rel_dot >= 0.0 { 1.0 } else { -1.0 };
|
2026-06-03 16:17:29 +03:00
|
|
|
|
|
|
|
|
|
|
// Lay the text out once, up front: its width fixes where the landing meets
|
|
|
|
|
|
// the text. The same layout is reused below to emit the glyph strokes.
|
2026-06-11 00:04:37 +03:00
|
|
|
|
let text_layout =
|
|
|
|
|
|
if ml.content_type == LeaderContentType::MText && !ml.context.text_string.is_empty() {
|
|
|
|
|
|
let ctx = &ml.context;
|
|
|
|
|
|
let height = if ctx.text_height > 0.0 {
|
|
|
|
|
|
ctx.text_height as f32
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ml.text_height as f32 * ml.scale_factor as f32
|
|
|
|
|
|
};
|
|
|
|
|
|
let td = ctx.text_direction;
|
|
|
|
|
|
let mut rot = if td.x.abs() > 1e-9 || td.y.abs() > 1e-9 {
|
|
|
|
|
|
(td.y as f32).atan2(td.x as f32)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ctx.text_rotation as f32
|
|
|
|
|
|
};
|
|
|
|
|
|
let style_name = ctx
|
|
|
|
|
|
.text_style_handle
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.and_then(|h| {
|
|
|
|
|
|
document
|
|
|
|
|
|
.text_styles
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|s| s.handle == *h)
|
|
|
|
|
|
.map(|s| s.name.clone())
|
|
|
|
|
|
})
|
|
|
|
|
|
.unwrap_or_else(|| "STANDARD".to_string());
|
|
|
|
|
|
let resolved = resolve_text_style(&style_name, document);
|
|
|
|
|
|
if resolved.is_upside_down {
|
|
|
|
|
|
rot += std::f32::consts::PI;
|
|
|
|
|
|
}
|
|
|
|
|
|
Some(layout_mtext(&MTextRenderOpts {
|
|
|
|
|
|
value: &ctx.text_string,
|
|
|
|
|
|
insertion: [text_loc.x, text_loc.y, text_loc.z],
|
|
|
|
|
|
height,
|
|
|
|
|
|
rect_w: ctx.text_width as f32,
|
|
|
|
|
|
rotation: rot,
|
|
|
|
|
|
style: &resolved,
|
|
|
|
|
|
// Side-anchored on the leader-facing edge so the text reads
|
|
|
|
|
|
// outward; flips live with the leader/text side.
|
|
|
|
|
|
attach_h_anchor: if text_sign >= 0.0 { 0.0 } else { 1.0 },
|
|
|
|
|
|
v_anchor: mleader_v_anchor(ctx.text_left_attachment),
|
|
|
|
|
|
line_spacing_factor: ctx.line_spacing_factor as f32,
|
|
|
|
|
|
vertical_text: false,
|
|
|
|
|
|
want_glyph_boxes: false,
|
|
|
|
|
|
}))
|
2026-06-03 16:17:29 +03:00
|
|
|
|
} else {
|
2026-06-11 00:04:37 +03:00
|
|
|
|
None
|
2026-06-03 16:17:29 +03:00
|
|
|
|
};
|
|
|
|
|
|
let dogleg = if ml.enable_landing && ml.enable_dogleg {
|
|
|
|
|
|
ml.dogleg_length.max(0.0)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
0.0
|
|
|
|
|
|
};
|
|
|
|
|
|
// text_location is the leader-facing (near) edge; the landing runs one
|
|
|
|
|
|
// dogleg from there back toward the leader.
|
2026-06-22 18:18:35 +03:00
|
|
|
|
let landing_pt = [
|
|
|
|
|
|
text_loc.x - text_sign * dogleg * tdx,
|
|
|
|
|
|
text_loc.y - text_sign * dogleg * tdy,
|
|
|
|
|
|
text_loc.z,
|
|
|
|
|
|
];
|
2026-06-03 16:17:29 +03:00
|
|
|
|
|
2026-03-22 14:56:27 +03:00
|
|
|
|
for root in &ml.context.leader_roots {
|
|
|
|
|
|
let cp = &root.connection_point;
|
|
|
|
|
|
let cp_f = p3(cp);
|
2026-05-02 19:59:11 +03:00
|
|
|
|
snap_pts.push(node(cp_f));
|
2026-03-22 14:56:27 +03:00
|
|
|
|
|
|
|
|
|
|
for line in &root.lines {
|
2026-05-12 10:36:15 +03:00
|
|
|
|
if line.points.is_empty() {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
|
|
|
|
|
|
|
if !invisible {
|
2026-05-12 10:36:15 +03:00
|
|
|
|
if !first {
|
|
|
|
|
|
points.push(nan);
|
|
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
|
first = false;
|
|
|
|
|
|
|
2026-06-03 16:17:29 +03:00
|
|
|
|
// Build the full control-point list: line.points + landing point
|
2026-05-12 23:47:40 +03:00
|
|
|
|
let mut ctrl: Vec<[f64; 3]> = line.points.iter().map(|p| p3(p)).collect();
|
2026-06-03 16:17:29 +03:00
|
|
|
|
let last_f = *ctrl.last().unwrap_or(&landing_pt);
|
2026-06-11 00:04:37 +03:00
|
|
|
|
let dist = ((last_f[0] - landing_pt[0]).powi(2)
|
|
|
|
|
|
+ (last_f[1] - landing_pt[1]).powi(2))
|
|
|
|
|
|
.sqrt();
|
2026-03-22 14:56:27 +03:00
|
|
|
|
if dist > 1e-9 {
|
2026-06-03 16:17:29 +03:00
|
|
|
|
ctrl.push(landing_pt);
|
2026-04-27 02:29:41 +03:00
|
|
|
|
}
|
|
|
|
|
|
for &c in &ctrl {
|
|
|
|
|
|
key_verts.push(c);
|
2026-05-02 19:59:11 +03:00
|
|
|
|
snap_pts.push(node(c));
|
2026-04-27 02:29:41 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ml.path_type == MultiLeaderPathType::Spline && ctrl.len() >= 2 {
|
|
|
|
|
|
// Catmull-Rom spline through the bend points.
|
|
|
|
|
|
let pts = catmull_rom_pts(&ctrl, 8);
|
|
|
|
|
|
for &pt in &pts {
|
|
|
|
|
|
points.push(pt);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
for &c in &ctrl {
|
|
|
|
|
|
points.push(c);
|
|
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-27 02:29:41 +03:00
|
|
|
|
for i in 0..ctrl.len().saturating_sub(1) {
|
2026-05-12 23:47:40 +03:00
|
|
|
|
let a = ctrl[i];
|
|
|
|
|
|
let b = ctrl[i + 1];
|
2026-05-12 10:36:15 +03:00
|
|
|
|
tangents.push(TangentGeom::Line {
|
2026-05-12 23:47:40 +03:00
|
|
|
|
p1: [a[0] as f32, a[1] as f32, a[2] as f32],
|
|
|
|
|
|
p2: [b[0] as f32, b[1] as f32, b[2] as f32],
|
2026-05-12 10:36:15 +03:00
|
|
|
|
});
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Arrowhead
|
|
|
|
|
|
if draw_arrow {
|
|
|
|
|
|
let tip = &line.points[0];
|
|
|
|
|
|
let tip_f = p3(tip);
|
2026-05-12 10:36:15 +03:00
|
|
|
|
let next = if line.points.len() >= 2 {
|
|
|
|
|
|
line.points[1]
|
|
|
|
|
|
} else {
|
|
|
|
|
|
*cp
|
|
|
|
|
|
};
|
2026-05-12 23:47:40 +03:00
|
|
|
|
let dx = next.x - tip.x;
|
|
|
|
|
|
let dy = next.y - tip.y;
|
2026-05-12 10:36:15 +03:00
|
|
|
|
let dl = (dx * dx + dy * dy).sqrt().max(1e-9);
|
2026-03-22 14:56:27 +03:00
|
|
|
|
let (dx, dy) = (dx / dl, dy / dl);
|
2026-05-12 23:47:40 +03:00
|
|
|
|
let a = std::f64::consts::PI / 6.0;
|
2026-03-22 14:56:27 +03:00
|
|
|
|
let (s, c) = a.sin_cos();
|
|
|
|
|
|
points.push(nan);
|
2026-05-12 10:36:15 +03:00
|
|
|
|
points.push([
|
|
|
|
|
|
tip_f[0] + (dx * c - dy * s) * arrow_size,
|
|
|
|
|
|
tip_f[1] + (dx * s + dy * c) * arrow_size,
|
|
|
|
|
|
tip_f[2],
|
|
|
|
|
|
]);
|
2026-03-22 14:56:27 +03:00
|
|
|
|
points.push(tip_f);
|
2026-05-12 10:36:15 +03:00
|
|
|
|
points.push([
|
|
|
|
|
|
tip_f[0] + (dx * c + dy * s) * arrow_size,
|
|
|
|
|
|
tip_f[1] + (-dx * s + dy * c) * arrow_size,
|
|
|
|
|
|
tip_f[2],
|
|
|
|
|
|
]);
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 16:17:29 +03:00
|
|
|
|
// Horizontal landing from the leader end to the text's near edge.
|
|
|
|
|
|
if dogleg > 0.0 {
|
2026-03-22 14:56:27 +03:00
|
|
|
|
points.push(nan);
|
2026-06-03 16:17:29 +03:00
|
|
|
|
points.push(landing_pt);
|
|
|
|
|
|
points.push([text_loc.x, text_loc.y, text_loc.z]);
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 16:17:29 +03:00
|
|
|
|
// Text strokes, drawn from the layout computed up front (centred on the
|
|
|
|
|
|
// text grip). The snap node is the grip itself.
|
|
|
|
|
|
if let Some(layout) = &text_layout {
|
|
|
|
|
|
snap_pts.push(node([text_loc.x, text_loc.y, text_loc.z]));
|
feat(text): route every text-bearing entity through the run-aware MText pipeline
Lift the MText parser + renderer into a reusable `layout_mtext` helper in
`text_support` and re-point the remaining text-bearing entities at it, so
every inline format code (`\f`, `\C`/`\c`, `\H`, `\W`, `\Q`, `\T`, `\A`,
`\p…`, decorations, stacked fractions, paragraph indents, tab stops, …)
reaches the stroke output uniformly.
Per-entity changes:
- TABLE cell ([entities/table.rs]) — anchor + alignment math now feeds
`layout_mtext`; `\C7;Red`-style codes no longer render as literal
glyph noise.
- MLEADER MText content (both entity path in [entities/multileader.rs]
and the LOD-aware path in [scene/tessellate.rs]) — drops the
`strip_mtext_codes` + per-line `tessellate_text_ex` chain; the LOD 0
baseline / LOD 1 greek rect / LOD 2 stroke ladder and the text-frame /
bg-fill rectangle now read line widths from the layout.
- ATTRIB / ATTDEF — when `mtext_flag` is MultiLine / ConstantMultiLine
the value goes through the MText pipeline (SingleLine stays on the
Text-style anchor maths because the DXF spec disallows codes there).
- DIMENSION text_override — `dimension_text_entity` emits a synthetic
MText (instead of Text) whenever the value contains MText escapes;
the LOD ladder via `tessellate_entity_dim_text` handles both kinds.
Infrastructure:
- `MTextVAnchor` gains `MiddleOfTopLine`, `MiddleOfBottomLine`,
`BottomOfTopLine` so MLEADER's 9 attachment variants all map cleanly.
- `MTextLayout` returns `line_widths` / `line_height` / `v_offset` so
callers that draw chrome (frames, fills, baselines, greek rects)
share the same numbers as the rendered strokes.
- `mtext_line_count` re-routed through the new parser so LOD row counts
and rendered row counts can never disagree.
- Legacy helpers (`strip_mtext_codes`, `split_mtext_lines`,
`measure_mtext_chars`, `word_wrap`, `cxf::measure_text`, the
`v_offset_for_attachment` MLEADER offset table, `MTextLine::plain_text`)
deleted — all callers migrated.
Audit (TEXT_FORMAT_CODE_INTEGRATION.md) removed; its plan is implemented
here.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 05:53:01 +03:00
|
|
|
|
for ts in &layout.strokes {
|
|
|
|
|
|
let ox = ts.origin[0];
|
|
|
|
|
|
let oy = ts.origin[1];
|
|
|
|
|
|
for stroke in &ts.strokes {
|
fix(multileader): render text properly — strip MText codes, honor full layout
Multileader text was emitted as a single literal stroke pass with no MText
code handling, no rotation, no attachment, no line breaks, no per-piece
color and bad f32 precision under world_offset. Two separate render paths
both had the same bug, and one was masking the fix in the other.
This rewrite, applied to both paths:
- Strips MText format codes (`{\fArial Black|b0|i0|c162|p34;...}`) so the
rendered text is the actual content, not the markup.
- Honors text_rotation (preferring text_direction so transforms survive),
text_attachment_point (Left/Center/Right horizontal anchor), and
text_left_attachment for vertical alignment via a shared
v_offset_for_attachment helper.
- Splits on \P/\n/\N and word-wraps to text_width.
- Applies line_spacing_factor with the same 5/3 × height convention as
MText.
- Resolves the text style via text_style_handle (fallback STANDARD).
- Applies ml.scale_factor everywhere (arrow size, dogleg length, text
height, text width). anno_scale is now only applied when
enable_annotation_scale is set.
- Splits the scene-level output into separate WireModels so leader,
text, frame and background fill each carry their own colour
(ctx.text_color, ml.text_frame, ctx.background_fill_color).
- Mirrors text_direction, base_direction and root.direction in
apply_transform so a mirrored multileader's text follows the geometry
instead of staying at its original rotation.
- Computes the text origin and frame corners in offset-relative f32
space — drawings sit at large absolute coordinates and casting to f32
before subtracting world_offset was wrecking the rotated sub-glyph
precision.
2026-05-12 23:27:53 +03:00
|
|
|
|
if stroke.len() < 2 {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
points.push(nan);
|
|
|
|
|
|
for &[x, y] in stroke {
|
2026-06-03 16:17:29 +03:00
|
|
|
|
points.push([ox + x as f64, oy + y as f64, text_loc.z]);
|
fix(multileader): render text properly — strip MText codes, honor full layout
Multileader text was emitted as a single literal stroke pass with no MText
code handling, no rotation, no attachment, no line breaks, no per-piece
color and bad f32 precision under world_offset. Two separate render paths
both had the same bug, and one was masking the fix in the other.
This rewrite, applied to both paths:
- Strips MText format codes (`{\fArial Black|b0|i0|c162|p34;...}`) so the
rendered text is the actual content, not the markup.
- Honors text_rotation (preferring text_direction so transforms survive),
text_attachment_point (Left/Center/Right horizontal anchor), and
text_left_attachment for vertical alignment via a shared
v_offset_for_attachment helper.
- Splits on \P/\n/\N and word-wraps to text_width.
- Applies line_spacing_factor with the same 5/3 × height convention as
MText.
- Resolves the text style via text_style_handle (fallback STANDARD).
- Applies ml.scale_factor everywhere (arrow size, dogleg length, text
height, text width). anno_scale is now only applied when
enable_annotation_scale is set.
- Splits the scene-level output into separate WireModels so leader,
text, frame and background fill each carry their own colour
(ctx.text_color, ml.text_frame, ctx.background_fill_color).
- Mirrors text_direction, base_direction and root.direction in
apply_transform so a mirrored multileader's text follows the geometry
instead of staying at its original rotation.
- Computes the text origin and frame corners in offset-relative f32
space — drawings sit at large absolute coordinates and casting to f32
before subtracting world_offset was wrecking the rotated sub-glyph
precision.
2026-05-12 23:27:53 +03:00
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 10:36:15 +03:00
|
|
|
|
if points.is_empty() {
|
|
|
|
|
|
return None;
|
|
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
|
|
|
|
|
|
|
Some(TruckEntity {
|
|
|
|
|
|
object: TruckObject::Lines(points),
|
2026-05-02 19:59:11 +03:00
|
|
|
|
snap_pts,
|
2026-03-22 14:56:27 +03:00
|
|
|
|
tangent_geoms: tangents,
|
|
|
|
|
|
key_vertices: key_verts,
|
2026-05-06 23:24:26 +03:00
|
|
|
|
fill_tris: vec![],
|
2026-03-22 14:56:27 +03:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Grips ──────────────────────────────────────────────────────────────────
|
|
|
|
|
|
//
|
|
|
|
|
|
// IDs are assigned in two passes:
|
|
|
|
|
|
// 0 .. total_line_pts - 1 : square grips on every LeaderLine vertex
|
|
|
|
|
|
// total_line_pts : diamond grip on context.text_location (if MText)
|
|
|
|
|
|
|
2026-06-03 16:17:29 +03:00
|
|
|
|
/// Side the text reads toward (+1 right / -1 left) and the text box's far
|
|
|
|
|
|
/// edge (the wrap-width grip position). The box width is the explicit wrap
|
|
|
|
|
|
/// width when set, else the natural laid-out width.
|
|
|
|
|
|
fn text_box_geom(ml: &MultiLeader) -> (f64, [f64; 3]) {
|
|
|
|
|
|
let tl = ml.context.text_location;
|
|
|
|
|
|
let leader_ref = ml
|
|
|
|
|
|
.context
|
|
|
|
|
|
.leader_roots
|
|
|
|
|
|
.first()
|
|
|
|
|
|
.and_then(|r| r.lines.first())
|
|
|
|
|
|
.and_then(|l| l.points.last())
|
|
|
|
|
|
.copied()
|
|
|
|
|
|
.or_else(|| ml.context.leader_roots.first().map(|r| r.connection_point))
|
|
|
|
|
|
.unwrap_or(tl);
|
|
|
|
|
|
let sign = if tl.x >= leader_ref.x { 1.0 } else { -1.0 };
|
|
|
|
|
|
let height = if ml.context.text_height > 0.0 {
|
|
|
|
|
|
ml.context.text_height as f32
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ml.text_height as f32 * ml.scale_factor as f32
|
|
|
|
|
|
};
|
|
|
|
|
|
let style = ResolvedTextStyle {
|
|
|
|
|
|
font_name: "STANDARD".to_string(),
|
|
|
|
|
|
width_factor: 1.0,
|
|
|
|
|
|
oblique_angle: 0.0,
|
|
|
|
|
|
is_backward: false,
|
|
|
|
|
|
is_upside_down: false,
|
|
|
|
|
|
};
|
|
|
|
|
|
let layout = layout_mtext(&MTextRenderOpts {
|
|
|
|
|
|
value: &ml.context.text_string,
|
|
|
|
|
|
insertion: [0.0, 0.0, 0.0],
|
|
|
|
|
|
height,
|
|
|
|
|
|
rect_w: ml.context.text_width as f32,
|
|
|
|
|
|
rotation: 0.0,
|
|
|
|
|
|
style: &style,
|
|
|
|
|
|
attach_h_anchor: if sign >= 0.0 { 0.0 } else { 1.0 },
|
|
|
|
|
|
v_anchor: MTextVAnchor::Middle,
|
|
|
|
|
|
line_spacing_factor: ml.context.line_spacing_factor as f32,
|
|
|
|
|
|
vertical_text: false,
|
|
|
|
|
|
want_glyph_boxes: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
let nat_w = layout.line_widths.iter().cloned().fold(0.0_f32, f32::max) as f64;
|
|
|
|
|
|
let box_w = if ml.context.text_width > 1e-6 {
|
|
|
|
|
|
ml.context.text_width
|
|
|
|
|
|
} else {
|
|
|
|
|
|
nat_w.max(height as f64)
|
|
|
|
|
|
};
|
|
|
|
|
|
(sign, [tl.x + sign * box_w, tl.y, tl.z])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 14:56:27 +03:00
|
|
|
|
fn grips(ml: &MultiLeader) -> Vec<GripDef> {
|
|
|
|
|
|
let mut result: Vec<GripDef> = Vec::new();
|
|
|
|
|
|
let mut id = 0usize;
|
|
|
|
|
|
|
|
|
|
|
|
for root in &ml.context.leader_roots {
|
|
|
|
|
|
for line in &root.lines {
|
|
|
|
|
|
for p in &line.points {
|
2026-06-11 00:04:37 +03:00
|
|
|
|
result.push(square_grip(id, glam::DVec3::new(p.x, p.y, p.z)));
|
2026-03-22 14:56:27 +03:00
|
|
|
|
id += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ml.content_type == LeaderContentType::MText {
|
|
|
|
|
|
let tl = &ml.context.text_location;
|
2026-06-03 16:17:29 +03:00
|
|
|
|
// Text-location grip, then the wrap-width grip at the box's far edge.
|
2026-06-11 00:04:37 +03:00
|
|
|
|
result.push(center_grip(id, glam::DVec3::new(tl.x, tl.y, tl.z)));
|
2026-06-03 16:17:29 +03:00
|
|
|
|
id += 1;
|
|
|
|
|
|
let (_, far) = text_box_geom(ml);
|
2026-06-11 00:04:37 +03:00
|
|
|
|
result.push(triangle_grip(id, glam::DVec3::new(far[0], far[1], far[2])));
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
result
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 16:17:29 +03:00
|
|
|
|
/// Sentinel grip id meaning "translate the whole multileader" — used by the
|
|
|
|
|
|
/// text grip's "Move with Leader" action so the leader follows the text.
|
|
|
|
|
|
pub(crate) const MOVE_ALL_GRIP: usize = usize::MAX;
|
|
|
|
|
|
|
2026-03-22 14:56:27 +03:00
|
|
|
|
fn apply_grip(ml: &mut MultiLeader, grip_id: usize, apply: GripApply) {
|
2026-06-03 16:17:29 +03:00
|
|
|
|
if grip_id == MOVE_ALL_GRIP {
|
|
|
|
|
|
let (dx, dy, dz) = match apply {
|
|
|
|
|
|
GripApply::Translate(d) => (d.x as f64, d.y as f64, d.z as f64),
|
|
|
|
|
|
GripApply::Absolute(a) => (
|
|
|
|
|
|
a.x as f64 - ml.context.text_location.x,
|
|
|
|
|
|
a.y as f64 - ml.context.text_location.y,
|
|
|
|
|
|
a.z as f64 - ml.context.text_location.z,
|
|
|
|
|
|
),
|
|
|
|
|
|
};
|
|
|
|
|
|
for root in &mut ml.context.leader_roots {
|
|
|
|
|
|
for line in &mut root.lines {
|
|
|
|
|
|
for p in &mut line.points {
|
|
|
|
|
|
p.x += dx;
|
|
|
|
|
|
p.y += dy;
|
|
|
|
|
|
p.z += dz;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
root.connection_point.x += dx;
|
|
|
|
|
|
root.connection_point.y += dy;
|
|
|
|
|
|
root.connection_point.z += dz;
|
|
|
|
|
|
}
|
|
|
|
|
|
ml.context.text_location.x += dx;
|
|
|
|
|
|
ml.context.text_location.y += dy;
|
|
|
|
|
|
ml.context.text_location.z += dz;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 14:56:27 +03:00
|
|
|
|
let mut idx = 0usize;
|
|
|
|
|
|
|
|
|
|
|
|
for root in &mut ml.context.leader_roots {
|
|
|
|
|
|
for line in &mut root.lines {
|
|
|
|
|
|
for p in &mut line.points {
|
|
|
|
|
|
if idx == grip_id {
|
|
|
|
|
|
match apply {
|
|
|
|
|
|
GripApply::Absolute(a) => {
|
|
|
|
|
|
p.x = a.x as f64;
|
|
|
|
|
|
p.y = a.y as f64;
|
|
|
|
|
|
p.z = a.z as f64;
|
|
|
|
|
|
}
|
|
|
|
|
|
GripApply::Translate(d) => {
|
|
|
|
|
|
p.x += d.x as f64;
|
|
|
|
|
|
p.y += d.y as f64;
|
|
|
|
|
|
p.z += d.z as f64;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
idx += 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 16:17:29 +03:00
|
|
|
|
// Text-location grip (idx == n_vertices), then the wrap-width grip.
|
|
|
|
|
|
if ml.content_type == LeaderContentType::MText {
|
|
|
|
|
|
if idx == grip_id {
|
|
|
|
|
|
let tl = &mut ml.context.text_location;
|
|
|
|
|
|
match apply {
|
|
|
|
|
|
GripApply::Absolute(a) => {
|
|
|
|
|
|
tl.x = a.x as f64;
|
|
|
|
|
|
tl.y = a.y as f64;
|
|
|
|
|
|
tl.z = a.z as f64;
|
|
|
|
|
|
}
|
|
|
|
|
|
GripApply::Translate(d) => {
|
|
|
|
|
|
tl.x += d.x as f64;
|
|
|
|
|
|
tl.y += d.y as f64;
|
|
|
|
|
|
tl.z += d.z as f64;
|
|
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
2026-06-03 16:17:29 +03:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
idx += 1;
|
|
|
|
|
|
if idx == grip_id {
|
|
|
|
|
|
// Dragging the box edge sets the MText wrap width.
|
|
|
|
|
|
let (sign, far) = text_box_geom(ml);
|
|
|
|
|
|
let new_far_x = match apply {
|
|
|
|
|
|
GripApply::Absolute(a) => a.x as f64,
|
|
|
|
|
|
GripApply::Translate(d) => far[0] + d.x as f64,
|
|
|
|
|
|
};
|
|
|
|
|
|
let min_w = ml.text_height.max(1.0) * 0.5;
|
|
|
|
|
|
ml.context.text_width = (sign * (new_far_x - ml.context.text_location.x)).max(min_w);
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Properties ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
fn content_type_str(ct: &LeaderContentType) -> &'static str {
|
|
|
|
|
|
match ct {
|
|
|
|
|
|
LeaderContentType::None => "None",
|
|
|
|
|
|
LeaderContentType::Block => "Block",
|
|
|
|
|
|
LeaderContentType::MText => "MText",
|
|
|
|
|
|
LeaderContentType::Tolerance => "Tolerance",
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn path_type_str(pt: &MultiLeaderPathType) -> &'static str {
|
|
|
|
|
|
match pt {
|
|
|
|
|
|
MultiLeaderPathType::Invisible => "Invisible",
|
|
|
|
|
|
MultiLeaderPathType::StraightLineSegments => "Straight",
|
|
|
|
|
|
MultiLeaderPathType::Spline => "Spline",
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn attachment_str(a: &TextAttachmentType) -> &'static str {
|
|
|
|
|
|
match a {
|
|
|
|
|
|
TextAttachmentType::TopOfTopLine => "Top of Top",
|
|
|
|
|
|
TextAttachmentType::MiddleOfTopLine => "Mid of Top",
|
|
|
|
|
|
TextAttachmentType::MiddleOfText => "Mid of Text",
|
|
|
|
|
|
TextAttachmentType::MiddleOfBottomLine => "Mid of Bot",
|
|
|
|
|
|
TextAttachmentType::BottomOfBottomLine => "Bot of Bot",
|
|
|
|
|
|
TextAttachmentType::BottomLine => "Bottom Line",
|
|
|
|
|
|
_ => "Other",
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn bool_toggle(label: &str, field: &'static str, value: bool) -> Property {
|
|
|
|
|
|
Property {
|
|
|
|
|
|
label: label.into(),
|
|
|
|
|
|
field,
|
|
|
|
|
|
value: PropValue::BoolToggle { field, value },
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn choice(label: &str, field: &'static str, selected: &str, opts: &[&str]) -> Property {
|
|
|
|
|
|
Property {
|
|
|
|
|
|
label: label.into(),
|
|
|
|
|
|
field,
|
|
|
|
|
|
value: PropValue::Choice {
|
|
|
|
|
|
selected: selected.to_string(),
|
|
|
|
|
|
options: opts.iter().map(|s| s.to_string()).collect(),
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn properties(ml: &MultiLeader) -> PropSection {
|
|
|
|
|
|
let ctx = &ml.context;
|
|
|
|
|
|
let total_pts: usize = ctx
|
|
|
|
|
|
.leader_roots
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.flat_map(|r| r.lines.iter())
|
|
|
|
|
|
.map(|l| l.points.len())
|
|
|
|
|
|
.sum();
|
|
|
|
|
|
|
|
|
|
|
|
let mut props = vec![
|
|
|
|
|
|
// Content
|
2026-05-12 10:36:15 +03:00
|
|
|
|
choice(
|
|
|
|
|
|
"Content Type",
|
|
|
|
|
|
"content_type",
|
|
|
|
|
|
content_type_str(&ml.content_type),
|
|
|
|
|
|
&["None", "MText", "Block", "Tolerance"],
|
|
|
|
|
|
),
|
2026-03-22 14:56:27 +03:00
|
|
|
|
Property {
|
|
|
|
|
|
label: "Text".into(),
|
|
|
|
|
|
field: "text_string",
|
|
|
|
|
|
value: PropValue::EditText(ctx.text_string.clone()),
|
|
|
|
|
|
},
|
|
|
|
|
|
edit("Text Height", "text_height", ml.text_height),
|
|
|
|
|
|
edit("Text X", "text_x", ctx.text_location.x),
|
|
|
|
|
|
edit("Text Y", "text_y", ctx.text_location.y),
|
|
|
|
|
|
edit("Text Z", "text_z", ctx.text_location.z),
|
|
|
|
|
|
bool_toggle("Text Frame", "text_frame", ml.text_frame),
|
|
|
|
|
|
// Leader line
|
2026-05-12 10:36:15 +03:00
|
|
|
|
choice(
|
|
|
|
|
|
"Path Type",
|
|
|
|
|
|
"path_type",
|
|
|
|
|
|
path_type_str(&ml.path_type),
|
|
|
|
|
|
&["Straight", "Spline", "Invisible"],
|
|
|
|
|
|
),
|
2026-03-22 14:56:27 +03:00
|
|
|
|
bool_toggle("Landing", "enable_landing", ml.enable_landing),
|
|
|
|
|
|
bool_toggle("Dogleg", "enable_dogleg", ml.enable_dogleg),
|
|
|
|
|
|
edit("Dogleg Length", "dogleg_length", ml.dogleg_length),
|
|
|
|
|
|
edit("Arrow Size", "arrowhead_size", ml.arrowhead_size),
|
|
|
|
|
|
edit("Scale", "scale_factor", ml.scale_factor),
|
2026-05-12 10:36:15 +03:00
|
|
|
|
bool_toggle(
|
|
|
|
|
|
"Annotation Scale",
|
|
|
|
|
|
"enable_annotation_scale",
|
|
|
|
|
|
ml.enable_annotation_scale,
|
|
|
|
|
|
),
|
2026-03-22 14:56:27 +03:00
|
|
|
|
// Text attachment
|
2026-05-12 10:36:15 +03:00
|
|
|
|
choice(
|
|
|
|
|
|
"Left Attach",
|
|
|
|
|
|
"text_left_attachment",
|
|
|
|
|
|
attachment_str(&ml.text_left_attachment),
|
|
|
|
|
|
&[
|
|
|
|
|
|
"Top of Top",
|
|
|
|
|
|
"Mid of Top",
|
|
|
|
|
|
"Mid of Text",
|
|
|
|
|
|
"Mid of Bot",
|
|
|
|
|
|
"Bot of Bot",
|
|
|
|
|
|
"Bottom Line",
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
choice(
|
|
|
|
|
|
"Right Attach",
|
|
|
|
|
|
"text_right_attachment",
|
|
|
|
|
|
attachment_str(&ml.text_right_attachment),
|
|
|
|
|
|
&[
|
|
|
|
|
|
"Top of Top",
|
|
|
|
|
|
"Mid of Top",
|
|
|
|
|
|
"Mid of Text",
|
|
|
|
|
|
"Mid of Bot",
|
|
|
|
|
|
"Bot of Bot",
|
|
|
|
|
|
"Bottom Line",
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
feat(entities): integrate remaining fields on block-based entities
MultiLeader and Table were ignoring most of the styling and content
fields that the parser populated. Bring both up to file-driven render:
MultiLeader
- line_color: leader stroke / arrow colour with ByBlock/ByLayer fallback
- line_type_handle: dashed leader via line_types lookup by handle
- line_weight (ml.line_weight): per-leader pixel width override
- arrowhead_handle: arrow shape resolved through the dim arrow emitter
(Closed Filled / Open / Dot / Origin / Box / Datum / Tick / None)
- extend_leader_to_text: continue the leader past the landing to the
text insertion point
- text_angle_type: Horizontal forces 0, Optimized clamps to a readable
half-plane, ParallelToLastLeaderLine keeps the stored direction
- text_direction_negative: adds π to the rotation
- text_alignment: drives the horizontal anchor when more specific than
the context's text_attachment_point
- text_attachment_direction + text_top_attachment + text_bottom_attachment:
Vertical mode uses the top / bottom attachment values chosen from the
first root's direction.y sign
- has_block_contents + block_content_handle + block_content_location +
block_rotation + block_scale + block_content_color: synthesise an
Insert and explode it through the standard tessellator so block-
content MultiLeaders render their referenced block
- style_handle, arrowhead_handle, line_type_handle, text_style_handle,
block_content_handle, property_override_flags, text_align_in_ipe,
block_scale, block_rotation, extend_leader_to_text,
text_direction_negative, text_top_attachment, text_bottom_attachment
surfaced in the Properties panel
Table
- Per-cell borders: drop the always-grid pass, walk the cells and
emit each border honouring its `invisible` flag with dedup so shared
edges aren't doubled
- table_style_handle, block_record_handle, data_version, value_flags,
override_flag / override_border_* flags, break_options /
break_flow_direction / break_spacing, normal surfaced in the
Properties panel
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 21:17:05 +03:00
|
|
|
|
// Top / Bottom attach (used when text_attachment_direction = Vertical)
|
|
|
|
|
|
choice(
|
|
|
|
|
|
"Top Attach",
|
|
|
|
|
|
"text_top_attachment",
|
|
|
|
|
|
attachment_str(&ml.text_top_attachment),
|
|
|
|
|
|
&[
|
|
|
|
|
|
"Top of Top",
|
|
|
|
|
|
"Mid of Top",
|
|
|
|
|
|
"Mid of Text",
|
|
|
|
|
|
"Mid of Bot",
|
|
|
|
|
|
"Bot of Bot",
|
|
|
|
|
|
"Bottom Line",
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
choice(
|
|
|
|
|
|
"Bottom Attach",
|
|
|
|
|
|
"text_bottom_attachment",
|
|
|
|
|
|
attachment_str(&ml.text_bottom_attachment),
|
|
|
|
|
|
&[
|
|
|
|
|
|
"Top of Top",
|
|
|
|
|
|
"Mid of Top",
|
|
|
|
|
|
"Mid of Text",
|
|
|
|
|
|
"Mid of Bot",
|
|
|
|
|
|
"Bot of Bot",
|
|
|
|
|
|
"Bottom Line",
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
2026-03-22 14:56:27 +03:00
|
|
|
|
// Stats
|
|
|
|
|
|
ro("Leader Pts", "total_pts", total_pts.to_string()),
|
|
|
|
|
|
ro("Roots", "root_count", ctx.leader_roots.len().to_string()),
|
feat(entities): integrate remaining fields on block-based entities
MultiLeader and Table were ignoring most of the styling and content
fields that the parser populated. Bring both up to file-driven render:
MultiLeader
- line_color: leader stroke / arrow colour with ByBlock/ByLayer fallback
- line_type_handle: dashed leader via line_types lookup by handle
- line_weight (ml.line_weight): per-leader pixel width override
- arrowhead_handle: arrow shape resolved through the dim arrow emitter
(Closed Filled / Open / Dot / Origin / Box / Datum / Tick / None)
- extend_leader_to_text: continue the leader past the landing to the
text insertion point
- text_angle_type: Horizontal forces 0, Optimized clamps to a readable
half-plane, ParallelToLastLeaderLine keeps the stored direction
- text_direction_negative: adds π to the rotation
- text_alignment: drives the horizontal anchor when more specific than
the context's text_attachment_point
- text_attachment_direction + text_top_attachment + text_bottom_attachment:
Vertical mode uses the top / bottom attachment values chosen from the
first root's direction.y sign
- has_block_contents + block_content_handle + block_content_location +
block_rotation + block_scale + block_content_color: synthesise an
Insert and explode it through the standard tessellator so block-
content MultiLeaders render their referenced block
- style_handle, arrowhead_handle, line_type_handle, text_style_handle,
block_content_handle, property_override_flags, text_align_in_ipe,
block_scale, block_rotation, extend_leader_to_text,
text_direction_negative, text_top_attachment, text_bottom_attachment
surfaced in the Properties panel
Table
- Per-cell borders: drop the always-grid pass, walk the cells and
emit each border honouring its `invisible` flag with dedup so shared
edges aren't doubled
- table_style_handle, block_record_handle, data_version, value_flags,
override_flag / override_border_* flags, break_options /
break_flow_direction / break_spacing, normal surfaced in the
Properties panel
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 21:17:05 +03:00
|
|
|
|
// Style references / handles (read-only — the multileader's own
|
|
|
|
|
|
// copies of the style values are the authoritative render inputs).
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Style Handle",
|
|
|
|
|
|
"style_handle",
|
|
|
|
|
|
match ml.style_handle {
|
|
|
|
|
|
Some(h) if !h.is_null() => format!("{:X}", h.value()),
|
|
|
|
|
|
_ => "(none)".to_string(),
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Text Style Handle",
|
|
|
|
|
|
"text_style_handle",
|
|
|
|
|
|
match ml.text_style_handle {
|
|
|
|
|
|
Some(h) if !h.is_null() => format!("{:X}", h.value()),
|
|
|
|
|
|
_ => "(none)".to_string(),
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Arrow Handle",
|
|
|
|
|
|
"arrowhead_handle",
|
|
|
|
|
|
match ml.arrowhead_handle {
|
|
|
|
|
|
Some(h) if !h.is_null() => format!("{:X}", h.value()),
|
|
|
|
|
|
_ => "(none)".to_string(),
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Line Type Handle",
|
|
|
|
|
|
"line_type_handle",
|
|
|
|
|
|
match ml.line_type_handle {
|
|
|
|
|
|
Some(h) if !h.is_null() => format!("{:X}", h.value()),
|
|
|
|
|
|
_ => "(none)".to_string(),
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Block Content Handle",
|
|
|
|
|
|
"block_content_handle",
|
|
|
|
|
|
match ml.block_content_handle {
|
|
|
|
|
|
Some(h) if !h.is_null() => format!("{:X}", h.value()),
|
|
|
|
|
|
_ => "(none)".to_string(),
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
// Less common toggles surfaced read-only.
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Extend Leader",
|
|
|
|
|
|
"extend_leader_to_text",
|
2026-06-11 00:04:37 +03:00
|
|
|
|
if ml.extend_leader_to_text {
|
|
|
|
|
|
"Yes"
|
|
|
|
|
|
} else {
|
|
|
|
|
|
"No"
|
|
|
|
|
|
},
|
feat(entities): integrate remaining fields on block-based entities
MultiLeader and Table were ignoring most of the styling and content
fields that the parser populated. Bring both up to file-driven render:
MultiLeader
- line_color: leader stroke / arrow colour with ByBlock/ByLayer fallback
- line_type_handle: dashed leader via line_types lookup by handle
- line_weight (ml.line_weight): per-leader pixel width override
- arrowhead_handle: arrow shape resolved through the dim arrow emitter
(Closed Filled / Open / Dot / Origin / Box / Datum / Tick / None)
- extend_leader_to_text: continue the leader past the landing to the
text insertion point
- text_angle_type: Horizontal forces 0, Optimized clamps to a readable
half-plane, ParallelToLastLeaderLine keeps the stored direction
- text_direction_negative: adds π to the rotation
- text_alignment: drives the horizontal anchor when more specific than
the context's text_attachment_point
- text_attachment_direction + text_top_attachment + text_bottom_attachment:
Vertical mode uses the top / bottom attachment values chosen from the
first root's direction.y sign
- has_block_contents + block_content_handle + block_content_location +
block_rotation + block_scale + block_content_color: synthesise an
Insert and explode it through the standard tessellator so block-
content MultiLeaders render their referenced block
- style_handle, arrowhead_handle, line_type_handle, text_style_handle,
block_content_handle, property_override_flags, text_align_in_ipe,
block_scale, block_rotation, extend_leader_to_text,
text_direction_negative, text_top_attachment, text_bottom_attachment
surfaced in the Properties panel
Table
- Per-cell borders: drop the always-grid pass, walk the cells and
emit each border honouring its `invisible` flag with dedup so shared
edges aren't doubled
- table_style_handle, block_record_handle, data_version, value_flags,
override_flag / override_border_* flags, break_options /
break_flow_direction / break_spacing, normal surfaced in the
Properties panel
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 21:17:05 +03:00
|
|
|
|
),
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Text Direction Negative",
|
|
|
|
|
|
"text_direction_negative",
|
2026-06-11 00:04:37 +03:00
|
|
|
|
if ml.text_direction_negative {
|
|
|
|
|
|
"Yes"
|
|
|
|
|
|
} else {
|
|
|
|
|
|
"No"
|
|
|
|
|
|
},
|
feat(entities): integrate remaining fields on block-based entities
MultiLeader and Table were ignoring most of the styling and content
fields that the parser populated. Bring both up to file-driven render:
MultiLeader
- line_color: leader stroke / arrow colour with ByBlock/ByLayer fallback
- line_type_handle: dashed leader via line_types lookup by handle
- line_weight (ml.line_weight): per-leader pixel width override
- arrowhead_handle: arrow shape resolved through the dim arrow emitter
(Closed Filled / Open / Dot / Origin / Box / Datum / Tick / None)
- extend_leader_to_text: continue the leader past the landing to the
text insertion point
- text_angle_type: Horizontal forces 0, Optimized clamps to a readable
half-plane, ParallelToLastLeaderLine keeps the stored direction
- text_direction_negative: adds π to the rotation
- text_alignment: drives the horizontal anchor when more specific than
the context's text_attachment_point
- text_attachment_direction + text_top_attachment + text_bottom_attachment:
Vertical mode uses the top / bottom attachment values chosen from the
first root's direction.y sign
- has_block_contents + block_content_handle + block_content_location +
block_rotation + block_scale + block_content_color: synthesise an
Insert and explode it through the standard tessellator so block-
content MultiLeaders render their referenced block
- style_handle, arrowhead_handle, line_type_handle, text_style_handle,
block_content_handle, property_override_flags, text_align_in_ipe,
block_scale, block_rotation, extend_leader_to_text,
text_direction_negative, text_top_attachment, text_bottom_attachment
surfaced in the Properties panel
Table
- Per-cell borders: drop the always-grid pass, walk the cells and
emit each border honouring its `invisible` flag with dedup so shared
edges aren't doubled
- table_style_handle, block_record_handle, data_version, value_flags,
override_flag / override_border_* flags, break_options /
break_flow_direction / break_spacing, normal surfaced in the
Properties panel
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 21:17:05 +03:00
|
|
|
|
),
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Text Align In IPE",
|
|
|
|
|
|
"text_align_in_ipe",
|
|
|
|
|
|
ml.text_align_in_ipe.to_string(),
|
|
|
|
|
|
),
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Property Override Flags",
|
|
|
|
|
|
"property_override_flags",
|
|
|
|
|
|
format!("{:#018b}", ml.property_override_flags.bits()),
|
|
|
|
|
|
),
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Block Scale",
|
|
|
|
|
|
"block_scale",
|
|
|
|
|
|
format!(
|
|
|
|
|
|
"{:.3} × {:.3} × {:.3}",
|
|
|
|
|
|
ml.block_scale.x, ml.block_scale.y, ml.block_scale.z
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
ro(
|
|
|
|
|
|
"Block Rotation",
|
|
|
|
|
|
"block_rotation",
|
|
|
|
|
|
format!("{:.3}°", ml.block_rotation.to_degrees()),
|
|
|
|
|
|
),
|
2026-03-22 14:56:27 +03:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// Connection point for first root (most common case)
|
|
|
|
|
|
if let Some(root) = ctx.leader_roots.first() {
|
|
|
|
|
|
props.push(edit("Root Conn X", "conn_x", root.connection_point.x));
|
|
|
|
|
|
props.push(edit("Root Conn Y", "conn_y", root.connection_point.y));
|
|
|
|
|
|
props.push(edit("Root Conn Z", "conn_z", root.connection_point.z));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-12 10:36:15 +03:00
|
|
|
|
PropSection {
|
|
|
|
|
|
title: "Geometry".into(),
|
|
|
|
|
|
props,
|
|
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn apply_geom_prop(ml: &mut MultiLeader, field: &str, value: &str) {
|
|
|
|
|
|
let f64 = |s: &str| -> Option<f64> { s.trim().parse().ok() };
|
|
|
|
|
|
|
|
|
|
|
|
match field {
|
|
|
|
|
|
"content_type" => {
|
|
|
|
|
|
ml.content_type = match value {
|
|
|
|
|
|
"Block" => LeaderContentType::Block,
|
|
|
|
|
|
"MText" => LeaderContentType::MText,
|
|
|
|
|
|
"Tolerance" => LeaderContentType::Tolerance,
|
|
|
|
|
|
_ => LeaderContentType::None,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
"text_string" => ml.context.text_string = value.to_string(),
|
|
|
|
|
|
"text_height" => {
|
2026-05-12 10:36:15 +03:00
|
|
|
|
if let Some(v) = f64(value) {
|
|
|
|
|
|
ml.text_height = v;
|
|
|
|
|
|
ml.context.text_height = v;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"text_x" => {
|
|
|
|
|
|
if let Some(v) = f64(value) {
|
|
|
|
|
|
ml.context.text_location.x = v;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"text_y" => {
|
|
|
|
|
|
if let Some(v) = f64(value) {
|
|
|
|
|
|
ml.context.text_location.y = v;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"text_z" => {
|
|
|
|
|
|
if let Some(v) = f64(value) {
|
|
|
|
|
|
ml.context.text_location.z = v;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"text_frame" => {
|
|
|
|
|
|
ml.text_frame = if value == "toggle" {
|
|
|
|
|
|
!ml.text_frame
|
|
|
|
|
|
} else {
|
|
|
|
|
|
value == "true"
|
|
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
|
|
|
|
|
"path_type" => {
|
|
|
|
|
|
ml.path_type = match value {
|
|
|
|
|
|
"Spline" => MultiLeaderPathType::Spline,
|
|
|
|
|
|
"Invisible" => MultiLeaderPathType::Invisible,
|
|
|
|
|
|
_ => MultiLeaderPathType::StraightLineSegments,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-05-12 10:36:15 +03:00
|
|
|
|
"enable_landing" => {
|
|
|
|
|
|
ml.enable_landing = if value == "toggle" {
|
|
|
|
|
|
!ml.enable_landing
|
|
|
|
|
|
} else {
|
|
|
|
|
|
value == "true"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"enable_dogleg" => {
|
|
|
|
|
|
ml.enable_dogleg = if value == "toggle" {
|
|
|
|
|
|
!ml.enable_dogleg
|
|
|
|
|
|
} else {
|
|
|
|
|
|
value == "true"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"enable_annotation_scale" => {
|
|
|
|
|
|
ml.enable_annotation_scale = if value == "toggle" {
|
|
|
|
|
|
!ml.enable_annotation_scale
|
|
|
|
|
|
} else {
|
|
|
|
|
|
value == "true"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"dogleg_length" => {
|
|
|
|
|
|
if let Some(v) = f64(value) {
|
|
|
|
|
|
ml.dogleg_length = v;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"arrowhead_size" => {
|
|
|
|
|
|
if let Some(v) = f64(value) {
|
|
|
|
|
|
ml.arrowhead_size = v;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"scale_factor" => {
|
|
|
|
|
|
if let Some(v) = f64(value) {
|
|
|
|
|
|
ml.scale_factor = v;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
|
"conn_x" => {
|
|
|
|
|
|
if let (Some(v), Some(root)) = (f64(value), ml.context.leader_roots.first_mut()) {
|
|
|
|
|
|
root.connection_point.x = v;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"conn_y" => {
|
|
|
|
|
|
if let (Some(v), Some(root)) = (f64(value), ml.context.leader_roots.first_mut()) {
|
|
|
|
|
|
root.connection_point.y = v;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"conn_z" => {
|
|
|
|
|
|
if let (Some(v), Some(root)) = (f64(value), ml.context.leader_roots.first_mut()) {
|
|
|
|
|
|
root.connection_point.z = v;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
"text_left_attachment" => {
|
|
|
|
|
|
ml.text_left_attachment = parse_attachment(value);
|
|
|
|
|
|
ml.context.text_left_attachment = parse_attachment(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
"text_right_attachment" => {
|
|
|
|
|
|
ml.text_right_attachment = parse_attachment(value);
|
|
|
|
|
|
ml.context.text_right_attachment = parse_attachment(value);
|
|
|
|
|
|
}
|
feat(entities): integrate remaining fields on block-based entities
MultiLeader and Table were ignoring most of the styling and content
fields that the parser populated. Bring both up to file-driven render:
MultiLeader
- line_color: leader stroke / arrow colour with ByBlock/ByLayer fallback
- line_type_handle: dashed leader via line_types lookup by handle
- line_weight (ml.line_weight): per-leader pixel width override
- arrowhead_handle: arrow shape resolved through the dim arrow emitter
(Closed Filled / Open / Dot / Origin / Box / Datum / Tick / None)
- extend_leader_to_text: continue the leader past the landing to the
text insertion point
- text_angle_type: Horizontal forces 0, Optimized clamps to a readable
half-plane, ParallelToLastLeaderLine keeps the stored direction
- text_direction_negative: adds π to the rotation
- text_alignment: drives the horizontal anchor when more specific than
the context's text_attachment_point
- text_attachment_direction + text_top_attachment + text_bottom_attachment:
Vertical mode uses the top / bottom attachment values chosen from the
first root's direction.y sign
- has_block_contents + block_content_handle + block_content_location +
block_rotation + block_scale + block_content_color: synthesise an
Insert and explode it through the standard tessellator so block-
content MultiLeaders render their referenced block
- style_handle, arrowhead_handle, line_type_handle, text_style_handle,
block_content_handle, property_override_flags, text_align_in_ipe,
block_scale, block_rotation, extend_leader_to_text,
text_direction_negative, text_top_attachment, text_bottom_attachment
surfaced in the Properties panel
Table
- Per-cell borders: drop the always-grid pass, walk the cells and
emit each border honouring its `invisible` flag with dedup so shared
edges aren't doubled
- table_style_handle, block_record_handle, data_version, value_flags,
override_flag / override_border_* flags, break_options /
break_flow_direction / break_spacing, normal surfaced in the
Properties panel
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 21:17:05 +03:00
|
|
|
|
"text_top_attachment" => {
|
|
|
|
|
|
ml.text_top_attachment = parse_attachment(value);
|
|
|
|
|
|
ml.context.text_top_attachment = parse_attachment(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
"text_bottom_attachment" => {
|
|
|
|
|
|
ml.text_bottom_attachment = parse_attachment(value);
|
|
|
|
|
|
ml.context.text_bottom_attachment = parse_attachment(value);
|
|
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
fn parse_attachment(s: &str) -> TextAttachmentType {
|
|
|
|
|
|
match s {
|
|
|
|
|
|
"Top of Top" => TextAttachmentType::TopOfTopLine,
|
|
|
|
|
|
"Mid of Top" => TextAttachmentType::MiddleOfTopLine,
|
|
|
|
|
|
"Mid of Bot" => TextAttachmentType::MiddleOfBottomLine,
|
|
|
|
|
|
"Bot of Bot" => TextAttachmentType::BottomOfBottomLine,
|
|
|
|
|
|
"Bottom Line" => TextAttachmentType::BottomLine,
|
|
|
|
|
|
_ => TextAttachmentType::MiddleOfText,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Transform ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
fn apply_transform(ml: &mut MultiLeader, t: &EntityTransform) {
|
2026-06-16 11:33:46 +03:00
|
|
|
|
crate::scene::view::transform::apply_standard_entity_transform(ml, t, |entity, p1, p2| {
|
fix(multileader): render text properly — strip MText codes, honor full layout
Multileader text was emitted as a single literal stroke pass with no MText
code handling, no rotation, no attachment, no line breaks, no per-piece
color and bad f32 precision under world_offset. Two separate render paths
both had the same bug, and one was masking the fix in the other.
This rewrite, applied to both paths:
- Strips MText format codes (`{\fArial Black|b0|i0|c162|p34;...}`) so the
rendered text is the actual content, not the markup.
- Honors text_rotation (preferring text_direction so transforms survive),
text_attachment_point (Left/Center/Right horizontal anchor), and
text_left_attachment for vertical alignment via a shared
v_offset_for_attachment helper.
- Splits on \P/\n/\N and word-wraps to text_width.
- Applies line_spacing_factor with the same 5/3 × height convention as
MText.
- Resolves the text style via text_style_handle (fallback STANDARD).
- Applies ml.scale_factor everywhere (arrow size, dogleg length, text
height, text width). anno_scale is now only applied when
enable_annotation_scale is set.
- Splits the scene-level output into separate WireModels so leader,
text, frame and background fill each carry their own colour
(ctx.text_color, ml.text_frame, ctx.background_fill_color).
- Mirrors text_direction, base_direction and root.direction in
apply_transform so a mirrored multileader's text follows the geometry
instead of staying at its original rotation.
- Computes the text origin and frame corners in offset-relative f32
space — drawings sit at large absolute coordinates and casting to f32
before subtracting world_offset was wrecking the rotated sub-glyph
precision.
2026-05-12 23:27:53 +03:00
|
|
|
|
// Reflect every point on the leader (line points, connection points,
|
|
|
|
|
|
// break-point endpoints) AND every direction vector that drives the
|
|
|
|
|
|
// text orientation. Without the direction reflection text would keep
|
|
|
|
|
|
// its original rotation while the leader appears mirrored.
|
2026-03-22 14:56:27 +03:00
|
|
|
|
for root in &mut entity.context.leader_roots {
|
|
|
|
|
|
for line in &mut root.lines {
|
|
|
|
|
|
for p in &mut line.points {
|
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
|
|
|
|
}
|
fix(multileader): render text properly — strip MText codes, honor full layout
Multileader text was emitted as a single literal stroke pass with no MText
code handling, no rotation, no attachment, no line breaks, no per-piece
color and bad f32 precision under world_offset. Two separate render paths
both had the same bug, and one was masking the fix in the other.
This rewrite, applied to both paths:
- Strips MText format codes (`{\fArial Black|b0|i0|c162|p34;...}`) so the
rendered text is the actual content, not the markup.
- Honors text_rotation (preferring text_direction so transforms survive),
text_attachment_point (Left/Center/Right horizontal anchor), and
text_left_attachment for vertical alignment via a shared
v_offset_for_attachment helper.
- Splits on \P/\n/\N and word-wraps to text_width.
- Applies line_spacing_factor with the same 5/3 × height convention as
MText.
- Resolves the text style via text_style_handle (fallback STANDARD).
- Applies ml.scale_factor everywhere (arrow size, dogleg length, text
height, text width). anno_scale is now only applied when
enable_annotation_scale is set.
- Splits the scene-level output into separate WireModels so leader,
text, frame and background fill each carry their own colour
(ctx.text_color, ml.text_frame, ctx.background_fill_color).
- Mirrors text_direction, base_direction and root.direction in
apply_transform so a mirrored multileader's text follows the geometry
instead of staying at its original rotation.
- Computes the text origin and frame corners in offset-relative f32
space — drawings sit at large absolute coordinates and casting to f32
before subtracting world_offset was wrecking the rotated sub-glyph
precision.
2026-05-12 23:27:53 +03:00
|
|
|
|
for bp in &mut line.break_points {
|
2026-06-16 11:33:46 +03:00
|
|
|
|
crate::scene::view::transform::reflect_xy_point(
|
fix(multileader): render text properly — strip MText codes, honor full layout
Multileader text was emitted as a single literal stroke pass with no MText
code handling, no rotation, no attachment, no line breaks, no per-piece
color and bad f32 precision under world_offset. Two separate render paths
both had the same bug, and one was masking the fix in the other.
This rewrite, applied to both paths:
- Strips MText format codes (`{\fArial Black|b0|i0|c162|p34;...}`) so the
rendered text is the actual content, not the markup.
- Honors text_rotation (preferring text_direction so transforms survive),
text_attachment_point (Left/Center/Right horizontal anchor), and
text_left_attachment for vertical alignment via a shared
v_offset_for_attachment helper.
- Splits on \P/\n/\N and word-wraps to text_width.
- Applies line_spacing_factor with the same 5/3 × height convention as
MText.
- Resolves the text style via text_style_handle (fallback STANDARD).
- Applies ml.scale_factor everywhere (arrow size, dogleg length, text
height, text width). anno_scale is now only applied when
enable_annotation_scale is set.
- Splits the scene-level output into separate WireModels so leader,
text, frame and background fill each carry their own colour
(ctx.text_color, ml.text_frame, ctx.background_fill_color).
- Mirrors text_direction, base_direction and root.direction in
apply_transform so a mirrored multileader's text follows the geometry
instead of staying at its original rotation.
- Computes the text origin and frame corners in offset-relative f32
space — drawings sit at large absolute coordinates and casting to f32
before subtracting world_offset was wrecking the rotated sub-glyph
precision.
2026-05-12 23:27:53 +03:00
|
|
|
|
&mut bp.start_point.x,
|
|
|
|
|
|
&mut bp.start_point.y,
|
|
|
|
|
|
p1,
|
|
|
|
|
|
p2,
|
|
|
|
|
|
);
|
2026-06-16 11:33:46 +03:00
|
|
|
|
crate::scene::view::transform::reflect_xy_point(
|
fix(multileader): render text properly — strip MText codes, honor full layout
Multileader text was emitted as a single literal stroke pass with no MText
code handling, no rotation, no attachment, no line breaks, no per-piece
color and bad f32 precision under world_offset. Two separate render paths
both had the same bug, and one was masking the fix in the other.
This rewrite, applied to both paths:
- Strips MText format codes (`{\fArial Black|b0|i0|c162|p34;...}`) so the
rendered text is the actual content, not the markup.
- Honors text_rotation (preferring text_direction so transforms survive),
text_attachment_point (Left/Center/Right horizontal anchor), and
text_left_attachment for vertical alignment via a shared
v_offset_for_attachment helper.
- Splits on \P/\n/\N and word-wraps to text_width.
- Applies line_spacing_factor with the same 5/3 × height convention as
MText.
- Resolves the text style via text_style_handle (fallback STANDARD).
- Applies ml.scale_factor everywhere (arrow size, dogleg length, text
height, text width). anno_scale is now only applied when
enable_annotation_scale is set.
- Splits the scene-level output into separate WireModels so leader,
text, frame and background fill each carry their own colour
(ctx.text_color, ml.text_frame, ctx.background_fill_color).
- Mirrors text_direction, base_direction and root.direction in
apply_transform so a mirrored multileader's text follows the geometry
instead of staying at its original rotation.
- Computes the text origin and frame corners in offset-relative f32
space — drawings sit at large absolute coordinates and casting to f32
before subtracting world_offset was wrecking the rotated sub-glyph
precision.
2026-05-12 23:27:53 +03:00
|
|
|
|
&mut bp.end_point.x,
|
|
|
|
|
|
&mut bp.end_point.y,
|
|
|
|
|
|
p1,
|
|
|
|
|
|
p2,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
2026-06-16 11:33:46 +03:00
|
|
|
|
crate::scene::view::transform::reflect_xy_point(
|
2026-05-12 10:36:15 +03:00
|
|
|
|
&mut root.connection_point.x,
|
|
|
|
|
|
&mut root.connection_point.y,
|
|
|
|
|
|
p1,
|
|
|
|
|
|
p2,
|
|
|
|
|
|
);
|
fix(multileader): render text properly — strip MText codes, honor full layout
Multileader text was emitted as a single literal stroke pass with no MText
code handling, no rotation, no attachment, no line breaks, no per-piece
color and bad f32 precision under world_offset. Two separate render paths
both had the same bug, and one was masking the fix in the other.
This rewrite, applied to both paths:
- Strips MText format codes (`{\fArial Black|b0|i0|c162|p34;...}`) so the
rendered text is the actual content, not the markup.
- Honors text_rotation (preferring text_direction so transforms survive),
text_attachment_point (Left/Center/Right horizontal anchor), and
text_left_attachment for vertical alignment via a shared
v_offset_for_attachment helper.
- Splits on \P/\n/\N and word-wraps to text_width.
- Applies line_spacing_factor with the same 5/3 × height convention as
MText.
- Resolves the text style via text_style_handle (fallback STANDARD).
- Applies ml.scale_factor everywhere (arrow size, dogleg length, text
height, text width). anno_scale is now only applied when
enable_annotation_scale is set.
- Splits the scene-level output into separate WireModels so leader,
text, frame and background fill each carry their own colour
(ctx.text_color, ml.text_frame, ctx.background_fill_color).
- Mirrors text_direction, base_direction and root.direction in
apply_transform so a mirrored multileader's text follows the geometry
instead of staying at its original rotation.
- Computes the text origin and frame corners in offset-relative f32
space — drawings sit at large absolute coordinates and casting to f32
before subtracting world_offset was wrecking the rotated sub-glyph
precision.
2026-05-12 23:27:53 +03:00
|
|
|
|
reflect_xy_direction(&mut root.direction.x, &mut root.direction.y, p1, p2);
|
|
|
|
|
|
for bp in &mut root.break_points {
|
2026-06-16 11:33:46 +03:00
|
|
|
|
crate::scene::view::transform::reflect_xy_point(
|
fix(multileader): render text properly — strip MText codes, honor full layout
Multileader text was emitted as a single literal stroke pass with no MText
code handling, no rotation, no attachment, no line breaks, no per-piece
color and bad f32 precision under world_offset. Two separate render paths
both had the same bug, and one was masking the fix in the other.
This rewrite, applied to both paths:
- Strips MText format codes (`{\fArial Black|b0|i0|c162|p34;...}`) so the
rendered text is the actual content, not the markup.
- Honors text_rotation (preferring text_direction so transforms survive),
text_attachment_point (Left/Center/Right horizontal anchor), and
text_left_attachment for vertical alignment via a shared
v_offset_for_attachment helper.
- Splits on \P/\n/\N and word-wraps to text_width.
- Applies line_spacing_factor with the same 5/3 × height convention as
MText.
- Resolves the text style via text_style_handle (fallback STANDARD).
- Applies ml.scale_factor everywhere (arrow size, dogleg length, text
height, text width). anno_scale is now only applied when
enable_annotation_scale is set.
- Splits the scene-level output into separate WireModels so leader,
text, frame and background fill each carry their own colour
(ctx.text_color, ml.text_frame, ctx.background_fill_color).
- Mirrors text_direction, base_direction and root.direction in
apply_transform so a mirrored multileader's text follows the geometry
instead of staying at its original rotation.
- Computes the text origin and frame corners in offset-relative f32
space — drawings sit at large absolute coordinates and casting to f32
before subtracting world_offset was wrecking the rotated sub-glyph
precision.
2026-05-12 23:27:53 +03:00
|
|
|
|
&mut bp.start_point.x,
|
|
|
|
|
|
&mut bp.start_point.y,
|
|
|
|
|
|
p1,
|
|
|
|
|
|
p2,
|
|
|
|
|
|
);
|
2026-06-16 11:33:46 +03:00
|
|
|
|
crate::scene::view::transform::reflect_xy_point(
|
fix(multileader): render text properly — strip MText codes, honor full layout
Multileader text was emitted as a single literal stroke pass with no MText
code handling, no rotation, no attachment, no line breaks, no per-piece
color and bad f32 precision under world_offset. Two separate render paths
both had the same bug, and one was masking the fix in the other.
This rewrite, applied to both paths:
- Strips MText format codes (`{\fArial Black|b0|i0|c162|p34;...}`) so the
rendered text is the actual content, not the markup.
- Honors text_rotation (preferring text_direction so transforms survive),
text_attachment_point (Left/Center/Right horizontal anchor), and
text_left_attachment for vertical alignment via a shared
v_offset_for_attachment helper.
- Splits on \P/\n/\N and word-wraps to text_width.
- Applies line_spacing_factor with the same 5/3 × height convention as
MText.
- Resolves the text style via text_style_handle (fallback STANDARD).
- Applies ml.scale_factor everywhere (arrow size, dogleg length, text
height, text width). anno_scale is now only applied when
enable_annotation_scale is set.
- Splits the scene-level output into separate WireModels so leader,
text, frame and background fill each carry their own colour
(ctx.text_color, ml.text_frame, ctx.background_fill_color).
- Mirrors text_direction, base_direction and root.direction in
apply_transform so a mirrored multileader's text follows the geometry
instead of staying at its original rotation.
- Computes the text origin and frame corners in offset-relative f32
space — drawings sit at large absolute coordinates and casting to f32
before subtracting world_offset was wrecking the rotated sub-glyph
precision.
2026-05-12 23:27:53 +03:00
|
|
|
|
&mut bp.end_point.x,
|
|
|
|
|
|
&mut bp.end_point.y,
|
|
|
|
|
|
p1,
|
|
|
|
|
|
p2,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
|
}
|
2026-06-16 11:33:46 +03:00
|
|
|
|
crate::scene::view::transform::reflect_xy_point(
|
2026-05-12 10:36:15 +03:00
|
|
|
|
&mut entity.context.text_location.x,
|
|
|
|
|
|
&mut entity.context.text_location.y,
|
|
|
|
|
|
p1,
|
|
|
|
|
|
p2,
|
|
|
|
|
|
);
|
fix(multileader): render text properly — strip MText codes, honor full layout
Multileader text was emitted as a single literal stroke pass with no MText
code handling, no rotation, no attachment, no line breaks, no per-piece
color and bad f32 precision under world_offset. Two separate render paths
both had the same bug, and one was masking the fix in the other.
This rewrite, applied to both paths:
- Strips MText format codes (`{\fArial Black|b0|i0|c162|p34;...}`) so the
rendered text is the actual content, not the markup.
- Honors text_rotation (preferring text_direction so transforms survive),
text_attachment_point (Left/Center/Right horizontal anchor), and
text_left_attachment for vertical alignment via a shared
v_offset_for_attachment helper.
- Splits on \P/\n/\N and word-wraps to text_width.
- Applies line_spacing_factor with the same 5/3 × height convention as
MText.
- Resolves the text style via text_style_handle (fallback STANDARD).
- Applies ml.scale_factor everywhere (arrow size, dogleg length, text
height, text width). anno_scale is now only applied when
enable_annotation_scale is set.
- Splits the scene-level output into separate WireModels so leader,
text, frame and background fill each carry their own colour
(ctx.text_color, ml.text_frame, ctx.background_fill_color).
- Mirrors text_direction, base_direction and root.direction in
apply_transform so a mirrored multileader's text follows the geometry
instead of staying at its original rotation.
- Computes the text origin and frame corners in offset-relative f32
space — drawings sit at large absolute coordinates and casting to f32
before subtracting world_offset was wrecking the rotated sub-glyph
precision.
2026-05-12 23:27:53 +03:00
|
|
|
|
reflect_xy_direction(
|
|
|
|
|
|
&mut entity.context.text_direction.x,
|
|
|
|
|
|
&mut entity.context.text_direction.y,
|
|
|
|
|
|
p1,
|
|
|
|
|
|
p2,
|
|
|
|
|
|
);
|
|
|
|
|
|
reflect_xy_direction(
|
|
|
|
|
|
&mut entity.context.base_direction.x,
|
|
|
|
|
|
&mut entity.context.base_direction.y,
|
|
|
|
|
|
p1,
|
|
|
|
|
|
p2,
|
|
|
|
|
|
);
|
2026-03-22 14:56:27 +03:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
fix(multileader): render text properly — strip MText codes, honor full layout
Multileader text was emitted as a single literal stroke pass with no MText
code handling, no rotation, no attachment, no line breaks, no per-piece
color and bad f32 precision under world_offset. Two separate render paths
both had the same bug, and one was masking the fix in the other.
This rewrite, applied to both paths:
- Strips MText format codes (`{\fArial Black|b0|i0|c162|p34;...}`) so the
rendered text is the actual content, not the markup.
- Honors text_rotation (preferring text_direction so transforms survive),
text_attachment_point (Left/Center/Right horizontal anchor), and
text_left_attachment for vertical alignment via a shared
v_offset_for_attachment helper.
- Splits on \P/\n/\N and word-wraps to text_width.
- Applies line_spacing_factor with the same 5/3 × height convention as
MText.
- Resolves the text style via text_style_handle (fallback STANDARD).
- Applies ml.scale_factor everywhere (arrow size, dogleg length, text
height, text width). anno_scale is now only applied when
enable_annotation_scale is set.
- Splits the scene-level output into separate WireModels so leader,
text, frame and background fill each carry their own colour
(ctx.text_color, ml.text_frame, ctx.background_fill_color).
- Mirrors text_direction, base_direction and root.direction in
apply_transform so a mirrored multileader's text follows the geometry
instead of staying at its original rotation.
- Computes the text origin and frame corners in offset-relative f32
space — drawings sit at large absolute coordinates and casting to f32
before subtracting world_offset was wrecking the rotated sub-glyph
precision.
2026-05-12 23:27:53 +03:00
|
|
|
|
/// Reflect a direction (not position) vector across the mirror line p1→p2.
|
|
|
|
|
|
/// Reflecting a direction is the same as reflecting `p1 + dir` then subtracting
|
|
|
|
|
|
/// the reflection of `p1`, which simplifies to reflecting around the origin.
|
2026-06-24 03:32:11 +03:00
|
|
|
|
fn reflect_xy_direction(dx: &mut f64, dy: &mut f64, p1: DVec3, p2: DVec3) {
|
|
|
|
|
|
let zero = DVec3::ZERO;
|
|
|
|
|
|
let p2_rel = DVec3::new(p2.x - p1.x, p2.y - p1.y, 0.0);
|
fix(multileader): render text properly — strip MText codes, honor full layout
Multileader text was emitted as a single literal stroke pass with no MText
code handling, no rotation, no attachment, no line breaks, no per-piece
color and bad f32 precision under world_offset. Two separate render paths
both had the same bug, and one was masking the fix in the other.
This rewrite, applied to both paths:
- Strips MText format codes (`{\fArial Black|b0|i0|c162|p34;...}`) so the
rendered text is the actual content, not the markup.
- Honors text_rotation (preferring text_direction so transforms survive),
text_attachment_point (Left/Center/Right horizontal anchor), and
text_left_attachment for vertical alignment via a shared
v_offset_for_attachment helper.
- Splits on \P/\n/\N and word-wraps to text_width.
- Applies line_spacing_factor with the same 5/3 × height convention as
MText.
- Resolves the text style via text_style_handle (fallback STANDARD).
- Applies ml.scale_factor everywhere (arrow size, dogleg length, text
height, text width). anno_scale is now only applied when
enable_annotation_scale is set.
- Splits the scene-level output into separate WireModels so leader,
text, frame and background fill each carry their own colour
(ctx.text_color, ml.text_frame, ctx.background_fill_color).
- Mirrors text_direction, base_direction and root.direction in
apply_transform so a mirrored multileader's text follows the geometry
instead of staying at its original rotation.
- Computes the text origin and frame corners in offset-relative f32
space — drawings sit at large absolute coordinates and casting to f32
before subtracting world_offset was wrecking the rotated sub-glyph
precision.
2026-05-12 23:27:53 +03:00
|
|
|
|
let mut tip_x = *dx;
|
|
|
|
|
|
let mut tip_y = *dy;
|
2026-06-16 11:33:46 +03:00
|
|
|
|
crate::scene::view::transform::reflect_xy_point(&mut tip_x, &mut tip_y, zero, p2_rel);
|
fix(multileader): render text properly — strip MText codes, honor full layout
Multileader text was emitted as a single literal stroke pass with no MText
code handling, no rotation, no attachment, no line breaks, no per-piece
color and bad f32 precision under world_offset. Two separate render paths
both had the same bug, and one was masking the fix in the other.
This rewrite, applied to both paths:
- Strips MText format codes (`{\fArial Black|b0|i0|c162|p34;...}`) so the
rendered text is the actual content, not the markup.
- Honors text_rotation (preferring text_direction so transforms survive),
text_attachment_point (Left/Center/Right horizontal anchor), and
text_left_attachment for vertical alignment via a shared
v_offset_for_attachment helper.
- Splits on \P/\n/\N and word-wraps to text_width.
- Applies line_spacing_factor with the same 5/3 × height convention as
MText.
- Resolves the text style via text_style_handle (fallback STANDARD).
- Applies ml.scale_factor everywhere (arrow size, dogleg length, text
height, text width). anno_scale is now only applied when
enable_annotation_scale is set.
- Splits the scene-level output into separate WireModels so leader,
text, frame and background fill each carry their own colour
(ctx.text_color, ml.text_frame, ctx.background_fill_color).
- Mirrors text_direction, base_direction and root.direction in
apply_transform so a mirrored multileader's text follows the geometry
instead of staying at its original rotation.
- Computes the text origin and frame corners in offset-relative f32
space — drawings sit at large absolute coordinates and casting to f32
before subtracting world_offset was wrecking the rotated sub-glyph
precision.
2026-05-12 23:27:53 +03:00
|
|
|
|
*dx = tip_x;
|
|
|
|
|
|
*dy = tip_y;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 14:56:27 +03:00
|
|
|
|
// ── Trait impls ────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
impl TruckConvertible for MultiLeader {
|
|
|
|
|
|
fn to_truck(&self, document: &acadrust::CadDocument) -> Option<TruckEntity> {
|
|
|
|
|
|
to_truck(self, document)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(grip): popup menus for polylines, spline, mline, hatch, leaders, text
Vocabulary added: AddFitPoint, RemoveFitPoint, Refit, RefineVertices,
TangentDirection, MoveWithText, StackText, UnstackText.
Polyline/Polyline2D/Polyline3D: vertex Add/Remove. MLine: same.
Spline: CV Add/Remove plus Refine Vertices (chord-midpoint insertion,
weights kept in sync, knots cleared so to_truck rebuilds uniform vec).
Hatch: centroid Origin/Angle/Scale (Angle and Scale prompt for value).
Leader: vertex Add/Remove (arrow + centroid stay Stretch-only).
MultiLeader: vertex Add/Remove Leader; MText location Move-with-Leader /
Move-Independent (menu live, structural edits stubbed). Text/MText:
Rotate prompts for degrees and sets rotation.
Spline, Leader, MultiLeader switched from impl_entity_basics! to manual
trait impls so grip_menu can be overridden.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:14:43 +03:00
|
|
|
|
impl crate::entities::traits::Grippable for MultiLeader {
|
|
|
|
|
|
fn grips(&self) -> Vec<GripDef> {
|
|
|
|
|
|
grips(self)
|
|
|
|
|
|
}
|
|
|
|
|
|
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
|
|
|
|
|
apply_grip(self, grip_id, apply);
|
|
|
|
|
|
}
|
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};
|
feat(grip): popup menus for polylines, spline, mline, hatch, leaders, text
Vocabulary added: AddFitPoint, RemoveFitPoint, Refit, RefineVertices,
TangentDirection, MoveWithText, StackText, UnstackText.
Polyline/Polyline2D/Polyline3D: vertex Add/Remove. MLine: same.
Spline: CV Add/Remove plus Refine Vertices (chord-midpoint insertion,
weights kept in sync, knots cleared so to_truck rebuilds uniform vec).
Hatch: centroid Origin/Angle/Scale (Angle and Scale prompt for value).
Leader: vertex Add/Remove (arrow + centroid stay Stretch-only).
MultiLeader: vertex Add/Remove Leader; MText location Move-with-Leader /
Move-Independent (menu live, structural edits stubbed). Text/MText:
Rotate prompts for degrees and sets rotation.
Spline, Leader, MultiLeader switched from impl_entity_basics! to manual
trait impls so grip_menu can be overridden.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:14:43 +03:00
|
|
|
|
let n_vertices: usize = self
|
|
|
|
|
|
.context
|
|
|
|
|
|
.leader_roots
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.flat_map(|r| r.lines.iter())
|
|
|
|
|
|
.map(|l| l.points.len())
|
|
|
|
|
|
.sum();
|
2026-06-03 16:17:29 +03:00
|
|
|
|
if self.content_type == LeaderContentType::MText && grip_id >= n_vertices {
|
|
|
|
|
|
if grip_id == n_vertices {
|
|
|
|
|
|
// Text-location grip.
|
|
|
|
|
|
vec![
|
2026-06-11 00:04:37 +03:00
|
|
|
|
GripMenuItem {
|
|
|
|
|
|
label: "Stretch",
|
|
|
|
|
|
action: GripMenuAction::Stretch,
|
|
|
|
|
|
},
|
2026-06-03 16:17:29 +03:00
|
|
|
|
GripMenuItem {
|
|
|
|
|
|
label: "Move with Leader",
|
|
|
|
|
|
action: GripMenuAction::MoveWithLeader,
|
|
|
|
|
|
},
|
|
|
|
|
|
GripMenuItem {
|
|
|
|
|
|
label: "Move Independent",
|
|
|
|
|
|
action: GripMenuAction::MoveIndependent,
|
|
|
|
|
|
},
|
|
|
|
|
|
]
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Wrap-width grip: drag only.
|
|
|
|
|
|
Vec::new()
|
|
|
|
|
|
}
|
feat(grip): popup menus for polylines, spline, mline, hatch, leaders, text
Vocabulary added: AddFitPoint, RemoveFitPoint, Refit, RefineVertices,
TangentDirection, MoveWithText, StackText, UnstackText.
Polyline/Polyline2D/Polyline3D: vertex Add/Remove. MLine: same.
Spline: CV Add/Remove plus Refine Vertices (chord-midpoint insertion,
weights kept in sync, knots cleared so to_truck rebuilds uniform vec).
Hatch: centroid Origin/Angle/Scale (Angle and Scale prompt for value).
Leader: vertex Add/Remove (arrow + centroid stay Stretch-only).
MultiLeader: vertex Add/Remove Leader; MText location Move-with-Leader /
Move-Independent (menu live, structural edits stubbed). Text/MText:
Rotate prompts for degrees and sets rotation.
Spline, Leader, MultiLeader switched from impl_entity_basics! to manual
trait impls so grip_menu can be overridden.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:14:43 +03:00
|
|
|
|
} else {
|
2026-06-03 16:17:29 +03:00
|
|
|
|
// Leader-line vertex.
|
feat(grip): popup menus for polylines, spline, mline, hatch, leaders, text
Vocabulary added: AddFitPoint, RemoveFitPoint, Refit, RefineVertices,
TangentDirection, MoveWithText, StackText, UnstackText.
Polyline/Polyline2D/Polyline3D: vertex Add/Remove. MLine: same.
Spline: CV Add/Remove plus Refine Vertices (chord-midpoint insertion,
weights kept in sync, knots cleared so to_truck rebuilds uniform vec).
Hatch: centroid Origin/Angle/Scale (Angle and Scale prompt for value).
Leader: vertex Add/Remove (arrow + centroid stay Stretch-only).
MultiLeader: vertex Add/Remove Leader; MText location Move-with-Leader /
Move-Independent (menu live, structural edits stubbed). Text/MText:
Rotate prompts for degrees and sets rotation.
Spline, Leader, MultiLeader switched from impl_entity_basics! to manual
trait impls so grip_menu can be overridden.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:14:43 +03:00
|
|
|
|
vec![
|
2026-06-11 00:04:37 +03:00
|
|
|
|
GripMenuItem {
|
|
|
|
|
|
label: "Stretch",
|
|
|
|
|
|
action: GripMenuAction::Stretch,
|
|
|
|
|
|
},
|
|
|
|
|
|
GripMenuItem {
|
|
|
|
|
|
label: "Add Leader",
|
|
|
|
|
|
action: GripMenuAction::AddLeader,
|
|
|
|
|
|
},
|
|
|
|
|
|
GripMenuItem {
|
|
|
|
|
|
label: "Remove Leader",
|
|
|
|
|
|
action: GripMenuAction::RemoveLeader,
|
|
|
|
|
|
},
|
feat(grip): popup menus for polylines, spline, mline, hatch, leaders, text
Vocabulary added: AddFitPoint, RemoveFitPoint, Refit, RefineVertices,
TangentDirection, MoveWithText, StackText, UnstackText.
Polyline/Polyline2D/Polyline3D: vertex Add/Remove. MLine: same.
Spline: CV Add/Remove plus Refine Vertices (chord-midpoint insertion,
weights kept in sync, knots cleared so to_truck rebuilds uniform vec).
Hatch: centroid Origin/Angle/Scale (Angle and Scale prompt for value).
Leader: vertex Add/Remove (arrow + centroid stay Stretch-only).
MultiLeader: vertex Add/Remove Leader; MText location Move-with-Leader /
Move-Independent (menu live, structural edits stubbed). Text/MText:
Rotate prompts for degrees and sets rotation.
Spline, Leader, MultiLeader switched from impl_entity_basics! to manual
trait impls so grip_menu can be overridden.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:14:43 +03:00
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-16 11:33:46 +03:00
|
|
|
|
fn apply_grip_menu(&mut self, grip_id: usize, action: crate::scene::model::object::GripMenuAction) {
|
|
|
|
|
|
use crate::scene::model::object::GripMenuAction as A;
|
2026-06-03 16:17:29 +03:00
|
|
|
|
// Locate the (root, line) and vertex position owning this grip id.
|
|
|
|
|
|
let mut idx = 0usize;
|
|
|
|
|
|
let mut loc: Option<(usize, usize, acadrust::types::Vector3)> = None;
|
|
|
|
|
|
'find: for (ri, root) in self.context.leader_roots.iter().enumerate() {
|
|
|
|
|
|
for (li, line) in root.lines.iter().enumerate() {
|
|
|
|
|
|
let n = line.points.len();
|
|
|
|
|
|
if grip_id < idx + n {
|
|
|
|
|
|
loc = Some((ri, li, line.points[grip_id - idx]));
|
|
|
|
|
|
break 'find;
|
|
|
|
|
|
}
|
|
|
|
|
|
idx += n;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
let Some((ri, li, vpos)) = loc else { return };
|
|
|
|
|
|
match action {
|
|
|
|
|
|
A::RemoveLeader => {
|
2026-06-11 00:04:37 +03:00
|
|
|
|
let total: usize = self
|
|
|
|
|
|
.context
|
|
|
|
|
|
.leader_roots
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|r| r.lines.len())
|
|
|
|
|
|
.sum();
|
2026-06-03 16:17:29 +03:00
|
|
|
|
if total > 1 {
|
|
|
|
|
|
self.context.leader_roots[ri].lines.remove(li);
|
|
|
|
|
|
if self.context.leader_roots[ri].lines.is_empty()
|
|
|
|
|
|
&& self.context.leader_roots.len() > 1
|
|
|
|
|
|
{
|
|
|
|
|
|
self.context.leader_roots.remove(ri);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
A::AddLeader => {
|
|
|
|
|
|
// Append to the last root so the new arrow is the last grip id
|
|
|
|
|
|
// (so the caller can immediately grab it for placement). Seed it
|
|
|
|
|
|
// below the picked vertex; the user drags it to the final spot.
|
|
|
|
|
|
let _ = ri;
|
|
|
|
|
|
let off = self.text_height.max(1.0) * 4.0;
|
|
|
|
|
|
let arrow = acadrust::types::Vector3::new(vpos.x, vpos.y - off, vpos.z);
|
|
|
|
|
|
if let Some(root) = self.context.leader_roots.last_mut() {
|
|
|
|
|
|
root.create_line(vec![arrow]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => {}
|
|
|
|
|
|
}
|
feat(grip): popup menus for polylines, spline, mline, hatch, leaders, text
Vocabulary added: AddFitPoint, RemoveFitPoint, Refit, RefineVertices,
TangentDirection, MoveWithText, StackText, UnstackText.
Polyline/Polyline2D/Polyline3D: vertex Add/Remove. MLine: same.
Spline: CV Add/Remove plus Refine Vertices (chord-midpoint insertion,
weights kept in sync, knots cleared so to_truck rebuilds uniform vec).
Hatch: centroid Origin/Angle/Scale (Angle and Scale prompt for value).
Leader: vertex Add/Remove (arrow + centroid stay Stretch-only).
MultiLeader: vertex Add/Remove Leader; MText location Move-with-Leader /
Move-Independent (menu live, structural edits stubbed). Text/MText:
Rotate prompts for degrees and sets rotation.
Spline, Leader, MultiLeader switched from impl_entity_basics! to manual
trait impls so grip_menu can be overridden.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:14:43 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl crate::entities::traits::PropertyEditable for MultiLeader {
|
2026-06-11 00:04:37 +03:00
|
|
|
|
fn geometry_properties(&self, _text_style_names: &[String]) -> PropSection {
|
feat(grip): popup menus for polylines, spline, mline, hatch, leaders, text
Vocabulary added: AddFitPoint, RemoveFitPoint, Refit, RefineVertices,
TangentDirection, MoveWithText, StackText, UnstackText.
Polyline/Polyline2D/Polyline3D: vertex Add/Remove. MLine: same.
Spline: CV Add/Remove plus Refine Vertices (chord-midpoint insertion,
weights kept in sync, knots cleared so to_truck rebuilds uniform vec).
Hatch: centroid Origin/Angle/Scale (Angle and Scale prompt for value).
Leader: vertex Add/Remove (arrow + centroid stay Stretch-only).
MultiLeader: vertex Add/Remove Leader; MText location Move-with-Leader /
Move-Independent (menu live, structural edits stubbed). Text/MText:
Rotate prompts for degrees and sets rotation.
Spline, Leader, MultiLeader switched from impl_entity_basics! to manual
trait impls so grip_menu can be overridden.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:14:43 +03:00
|
|
|
|
properties(self)
|
|
|
|
|
|
}
|
|
|
|
|
|
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
|
|
|
|
|
apply_geom_prop(self, field, value);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl crate::entities::traits::Transformable for MultiLeader {
|
|
|
|
|
|
fn apply_transform(&mut self, t: &EntityTransform) {
|
|
|
|
|
|
apply_transform(self, t);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
|
|
|
|
|
|
|
/// Per-entity tessellation entry for `MultiLeader`. Returns multiple
|
|
|
|
|
|
/// `WireModel`s (leader/dogleg + arrow fill, optional block content,
|
|
|
|
|
|
/// text strokes, frame, background fill) so each piece can carry its
|
|
|
|
|
|
/// own colour.
|
|
|
|
|
|
pub trait MultiLeaderTess {
|
|
|
|
|
|
fn tessellate(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
document: &acadrust::CadDocument,
|
|
|
|
|
|
handle: acadrust::Handle,
|
|
|
|
|
|
selected: bool,
|
|
|
|
|
|
entity_color: [f32; 4],
|
|
|
|
|
|
line_weight_px: f32,
|
|
|
|
|
|
world_offset: [f64; 3],
|
|
|
|
|
|
anno_scale: f32,
|
|
|
|
|
|
world_per_pixel: Option<f32>,
|
2026-06-16 11:33:46 +03:00
|
|
|
|
) -> Vec<crate::scene::model::wire_model::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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
impl MultiLeaderTess for MultiLeader {
|
|
|
|
|
|
fn tessellate(
|
|
|
|
|
|
&self,
|
|
|
|
|
|
document: &acadrust::CadDocument,
|
|
|
|
|
|
handle: acadrust::Handle,
|
|
|
|
|
|
selected: bool,
|
|
|
|
|
|
entity_color: [f32; 4],
|
|
|
|
|
|
line_weight_px: f32,
|
|
|
|
|
|
world_offset: [f64; 3],
|
|
|
|
|
|
anno_scale: f32,
|
|
|
|
|
|
world_per_pixel: Option<f32>,
|
2026-06-16 11:33:46 +03:00
|
|
|
|
) -> Vec<crate::scene::model::wire_model::WireModel> {
|
|
|
|
|
|
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
|
|
|
|
append_arrow, arrow_from_block, color_or_inherit, tessellate, ArrowKind, DimGeom,
|
|
|
|
|
|
};
|
2026-06-16 11:33:46 +03:00
|
|
|
|
use crate::scene::model::wire_model::{SnapHint, TangentGeom, 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
|
|
|
|
use glam::Vec3;
|
|
|
|
|
|
let ml = self;
|
|
|
|
|
|
// line_color falls back to the entity colour when the MultiLeader's own
|
|
|
|
|
|
// colour is ByBlock/ByLayer; otherwise the leader uses its stored hue.
|
|
|
|
|
|
let line_color = if selected {
|
|
|
|
|
|
WireModel::SELECTED
|
|
|
|
|
|
} else {
|
|
|
|
|
|
color_or_inherit(&ml.line_color, entity_color)
|
|
|
|
|
|
};
|
|
|
|
|
|
// ml.line_weight is the leader-line weight override. Negative codes
|
|
|
|
|
|
// (ByLayer/ByBlock/Default) fall through to the entity's already-resolved
|
|
|
|
|
|
// pixel width.
|
|
|
|
|
|
let leader_lw_px = match ml.line_weight {
|
2026-06-11 00:04:37 +03:00
|
|
|
|
acadrust::types::LineWeight::Value(v) if v >= 0 => (v as f32 / 100.0) * (96.0 / 25.4),
|
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
|
|
|
|
_ => line_weight_px,
|
|
|
|
|
|
};
|
|
|
|
|
|
// ml.line_type_handle — resolve via line_types table by handle and apply
|
|
|
|
|
|
// the resulting dash pattern to the leader wire.
|
|
|
|
|
|
let lt_scale = document.header.linetype_scale as f32 * ml.common.linetype_scale as f32;
|
|
|
|
|
|
let (leader_pat_len, leader_pat) = match ml.line_type_handle {
|
|
|
|
|
|
Some(h) if !h.is_null() => {
|
|
|
|
|
|
let name = document
|
|
|
|
|
|
.line_types
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|lt| lt.handle == h)
|
|
|
|
|
|
.map(|lt| lt.name.clone());
|
|
|
|
|
|
match name {
|
2026-06-11 00:04:37 +03:00
|
|
|
|
Some(n) => {
|
2026-06-16 11:33:46 +03:00
|
|
|
|
crate::scene::view::render::resolve_pattern(&document.line_types, &n, lt_scale)
|
2026-06-11 00:04:37 +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
|
|
|
|
None => (0.0, [0.0; 8]),
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_ => (0.0, [0.0; 8]),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let name = handle.value().to_string();
|
|
|
|
|
|
let nan = [f32::NAN; 3];
|
|
|
|
|
|
let [ox, oy, oz] = world_offset;
|
|
|
|
|
|
let p3 = |v: &acadrust::types::Vector3| -> [f32; 3] {
|
|
|
|
|
|
[(v.x - ox) as f32, (v.y - oy) as f32, (v.z - oz) as f32]
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ── Scaling ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
// ml.scale_factor is always applied; anno_scale is only applied when the
|
|
|
|
|
|
// multileader is marked annotative.
|
|
|
|
|
|
let effective_scale = (ml.scale_factor as f32)
|
|
|
|
|
|
* if ml.enable_annotation_scale {
|
|
|
|
|
|
anno_scale
|
|
|
|
|
|
} else {
|
|
|
|
|
|
1.0
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let arrow_size = ml.arrowhead_size as f32 * effective_scale;
|
|
|
|
|
|
let draw_arrow = arrow_size > 0.0;
|
|
|
|
|
|
let invisible = ml.path_type == MultiLeaderPathType::Invisible;
|
|
|
|
|
|
// arrowhead_handle resolves through the block records to a named arrow
|
|
|
|
|
|
// block (matches DIMLDRBLK on Dimension). Null handle / unknown name →
|
|
|
|
|
|
// ClosedFilled triangle.
|
|
|
|
|
|
let arrow_kind = match ml.arrowhead_handle {
|
|
|
|
|
|
Some(h) if !h.is_null() => arrow_from_block(document, h, arrow_size.max(0.001)),
|
|
|
|
|
|
_ => ArrowKind::Triangle {
|
|
|
|
|
|
size: arrow_size.max(0.001),
|
|
|
|
|
|
filled: true,
|
|
|
|
|
|
size_mul: 1.0,
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ── Leader / arrow / dogleg points ───────────────────────────────────────
|
|
|
|
|
|
let mut points: Vec<[f32; 3]> = Vec::new();
|
2026-06-23 22:23:56 +03:00
|
|
|
|
let mut key_verts: Vec<[f64; 3]> = Vec::new();
|
2026-06-23 18:52:42 +03:00
|
|
|
|
let mut snap_pts: Vec<(glam::DVec3, SnapHint)> = 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
|
|
|
|
let mut tangents: Vec<TangentGeom> = Vec::new();
|
|
|
|
|
|
let mut arrow_fill: Vec<[f32; 3]> = Vec::new();
|
|
|
|
|
|
let mut first = true;
|
|
|
|
|
|
|
2026-06-03 16:17:29 +03:00
|
|
|
|
// Which side the text grip sits on, recomputed every frame so the text
|
|
|
|
|
|
// alignment and the landing mirror live when the arrow or text moves.
|
|
|
|
|
|
let text_loc_w = ml.context.text_location;
|
|
|
|
|
|
let leader_ref_w = ml
|
|
|
|
|
|
.context
|
|
|
|
|
|
.leader_roots
|
|
|
|
|
|
.first()
|
|
|
|
|
|
.and_then(|r| r.lines.first())
|
|
|
|
|
|
.and_then(|l| l.points.last())
|
|
|
|
|
|
.copied()
|
|
|
|
|
|
.or_else(|| ml.context.leader_roots.first().map(|r| r.connection_point))
|
|
|
|
|
|
.unwrap_or(text_loc_w);
|
2026-06-11 00:04:37 +03:00
|
|
|
|
let text_sign_w: f64 = if text_loc_w.x >= leader_ref_w.x {
|
|
|
|
|
|
1.0
|
|
|
|
|
|
} else {
|
|
|
|
|
|
-1.0
|
|
|
|
|
|
};
|
2026-06-03 16:17:29 +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
|
|
|
|
for root in &ml.context.leader_roots {
|
|
|
|
|
|
let cp = &root.connection_point;
|
|
|
|
|
|
let cp_f = p3(cp);
|
2026-06-23 18:52:42 +03:00
|
|
|
|
snap_pts.push((Vec3::from(cp_f).as_dvec3(), SnapHint::Node));
|
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
|
|
|
|
|
|
|
|
|
|
for line in &root.lines {
|
|
|
|
|
|
if line.points.is_empty() {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if !invisible {
|
|
|
|
|
|
if !first {
|
|
|
|
|
|
points.push(nan);
|
|
|
|
|
|
}
|
|
|
|
|
|
first = false;
|
|
|
|
|
|
|
|
|
|
|
|
let mut ctrl: Vec<[f32; 3]> = line.points.iter().map(|p| p3(p)).collect();
|
|
|
|
|
|
let last_f = *ctrl.last().unwrap_or(&cp_f);
|
2026-06-11 00:04:37 +03:00
|
|
|
|
let dist =
|
|
|
|
|
|
((last_f[0] - cp_f[0]).powi(2) + (last_f[1] - cp_f[1]).powi(2)).sqrt();
|
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 dist > 1e-9 {
|
|
|
|
|
|
ctrl.push(cp_f);
|
|
|
|
|
|
}
|
|
|
|
|
|
for &c in &ctrl {
|
2026-06-23 22:23:56 +03:00
|
|
|
|
key_verts.push([c[0] as f64, c[1] as f64, c[2] as f64]);
|
2026-06-23 18:52:42 +03:00
|
|
|
|
snap_pts.push((Vec3::from(c).as_dvec3(), SnapHint::Node));
|
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 ml.path_type == MultiLeaderPathType::Spline && ctrl.len() >= 2 {
|
|
|
|
|
|
let ctrl_f64: Vec<[f64; 3]> = ctrl
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.map(|c| [c[0] as f64, c[1] as f64, c[2] as f64])
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
for pt in catmull_rom_pts(&ctrl_f64, 8) {
|
|
|
|
|
|
points.push([pt[0] as f32, pt[1] as f32, pt[2] as f32]);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
for &c in &ctrl {
|
|
|
|
|
|
points.push(c);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
for i in 0..ctrl.len().saturating_sub(1) {
|
|
|
|
|
|
tangents.push(TangentGeom::Line {
|
|
|
|
|
|
p1: ctrl[i],
|
|
|
|
|
|
p2: ctrl[i + 1],
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if draw_arrow {
|
|
|
|
|
|
let tip = &line.points[0];
|
|
|
|
|
|
let tip_f = p3(tip);
|
|
|
|
|
|
let next = if line.points.len() >= 2 {
|
|
|
|
|
|
line.points[1]
|
|
|
|
|
|
} else {
|
|
|
|
|
|
*cp
|
|
|
|
|
|
};
|
|
|
|
|
|
let dx = (next.x - tip.x) as f32;
|
|
|
|
|
|
let dy = (next.y - tip.y) as f32;
|
|
|
|
|
|
let dl = (dx * dx + dy * dy).sqrt().max(1e-9);
|
|
|
|
|
|
let dir = Vec3::new(dx / dl, dy / dl, 0.0);
|
|
|
|
|
|
let tip_v = Vec3::new(tip_f[0], tip_f[1], tip_f[2]);
|
|
|
|
|
|
// Reuse the dim/leader arrow emitter so MultiLeader's arrow
|
|
|
|
|
|
// matches the block referenced by arrowhead_handle.
|
|
|
|
|
|
let mut arrow_geom = DimGeom::new();
|
|
|
|
|
|
append_arrow(&mut arrow_geom, tip_v, dir, &arrow_kind);
|
|
|
|
|
|
if !arrow_geom.dim_lines.is_empty() {
|
|
|
|
|
|
points.push(nan);
|
|
|
|
|
|
points.extend(arrow_geom.dim_lines);
|
|
|
|
|
|
}
|
|
|
|
|
|
arrow_fill.extend(arrow_geom.arrow_fill);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ml.enable_landing && ml.enable_dogleg && ml.dogleg_length > 0.0 {
|
2026-06-03 16:17:29 +03:00
|
|
|
|
// Horizontal landing toward the text's side (mirrors live).
|
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 d = ml.dogleg_length * effective_scale as f64;
|
|
|
|
|
|
let landing_end = [
|
2026-06-03 16:17:29 +03:00
|
|
|
|
(cp.x + text_sign_w * d - ox) as f32,
|
|
|
|
|
|
(cp.y - oy) 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
|
|
|
|
cp_f[2],
|
|
|
|
|
|
];
|
|
|
|
|
|
points.push(nan);
|
|
|
|
|
|
points.push(cp_f);
|
|
|
|
|
|
points.push(landing_end);
|
2026-06-03 16:17:29 +03:00
|
|
|
|
// Continue the landing to the text's near edge so it stays
|
|
|
|
|
|
// attached as the text moves.
|
2026-06-11 00:04:37 +03:00
|
|
|
|
if ml.content_type == LeaderContentType::MText && !ml.context.text_string.is_empty()
|
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 tx = (ml.context.text_location.x - ox) as f32;
|
|
|
|
|
|
let ty = (ml.context.text_location.y - oy) as f32;
|
|
|
|
|
|
points.push(landing_end);
|
|
|
|
|
|
points.push([tx, ty, cp_f[2]]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// The leader/arrow/dogleg wire goes out as a single WireModel. Text, frame,
|
|
|
|
|
|
// and background fill (each with their own color) are appended as separate
|
|
|
|
|
|
// WireModels so the renderer respects per-piece coloring.
|
|
|
|
|
|
let mut wires: Vec<WireModel> = Vec::new();
|
|
|
|
|
|
wires.push(WireModel {
|
|
|
|
|
|
name: name.clone(),
|
|
|
|
|
|
points,
|
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: line_color,
|
|
|
|
|
|
selected,
|
|
|
|
|
|
aci: 0,
|
|
|
|
|
|
pattern_length: leader_pat_len,
|
|
|
|
|
|
pattern: leader_pat,
|
|
|
|
|
|
line_weight_px: leader_lw_px,
|
|
|
|
|
|
snap_pts,
|
|
|
|
|
|
tangent_geoms: tangents,
|
|
|
|
|
|
key_vertices: key_verts,
|
|
|
|
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
|
|
|
|
|
plinegen: true,
|
|
|
|
|
|
vp_scissor: None,
|
|
|
|
|
|
fill_tris: arrow_fill,
|
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
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ── Block content ───────────────────────────────────────────────────────
|
|
|
|
|
|
// When content_type == Block, the MultiLeader displays a block reference
|
|
|
|
|
|
// at block_content_location with the recorded rotation/scale. Synthesize
|
|
|
|
|
|
// an Insert and explode it through the standard tessellator. The block
|
|
|
|
|
|
// resolves via block_content_handle (handle → block_record name).
|
|
|
|
|
|
if ml.content_type == LeaderContentType::Block && ml.context.has_block_contents {
|
|
|
|
|
|
let block_name = match ml.block_content_handle {
|
|
|
|
|
|
Some(h) if !h.is_null() => document
|
|
|
|
|
|
.block_records
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|br| br.handle == h)
|
|
|
|
|
|
.map(|br| br.name.clone()),
|
|
|
|
|
|
_ => None,
|
|
|
|
|
|
};
|
|
|
|
|
|
if let Some(block_name) = block_name {
|
|
|
|
|
|
let block_color = if selected {
|
|
|
|
|
|
line_color
|
|
|
|
|
|
} else {
|
|
|
|
|
|
color_or_inherit(&ml.block_content_color, entity_color)
|
|
|
|
|
|
};
|
|
|
|
|
|
let mut synth_ins =
|
|
|
|
|
|
acadrust::entities::Insert::new(block_name, ml.context.block_content_location);
|
|
|
|
|
|
synth_ins.set_x_scale(ml.block_scale.x);
|
|
|
|
|
|
synth_ins.set_y_scale(ml.block_scale.y);
|
|
|
|
|
|
synth_ins.set_z_scale(ml.block_scale.z);
|
|
|
|
|
|
synth_ins.rotation = ml.block_rotation;
|
|
|
|
|
|
synth_ins.common.layer = ml.common.layer.clone();
|
|
|
|
|
|
// block_connection_type (BlockExtents vs BasePoint) chooses the
|
|
|
|
|
|
// anchor when *creating* the multileader; at render time the
|
|
|
|
|
|
// file's stored leader endpoints already encode that choice.
|
|
|
|
|
|
let _ = ml.block_connection_type;
|
|
|
|
|
|
for sub in synth_ins.explode_from_document(document) {
|
|
|
|
|
|
let normalized =
|
2026-06-16 11:49:14 +03:00
|
|
|
|
crate::modules::draw::modify::explode::normalize_insert_entity(sub);
|
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 sub_wires = tessellate(
|
|
|
|
|
|
document,
|
|
|
|
|
|
handle,
|
|
|
|
|
|
&normalized,
|
|
|
|
|
|
selected,
|
|
|
|
|
|
block_color,
|
|
|
|
|
|
leader_pat_len,
|
|
|
|
|
|
leader_pat,
|
|
|
|
|
|
leader_lw_px,
|
|
|
|
|
|
world_offset,
|
|
|
|
|
|
1.0,
|
2026-06-17 11:13:12 +03:00
|
|
|
|
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
|
|
|
|
);
|
|
|
|
|
|
for w in &mut sub_wires {
|
|
|
|
|
|
w.name = name.clone();
|
|
|
|
|
|
}
|
|
|
|
|
|
wires.extend(sub_wires);
|
|
|
|
|
|
}
|
|
|
|
|
|
// Block attributes attached to the multileader — render each as
|
|
|
|
|
|
// its own attribute entity at WCS location like INSERT does.
|
|
|
|
|
|
for ba in &ml.block_attributes {
|
|
|
|
|
|
let _ = ba; // BlockAttribute carries only the value override
|
|
|
|
|
|
// string; we'd need the AttributeDefinition handle
|
|
|
|
|
|
// to materialise it as ATTRIB geometry. Skipped
|
|
|
|
|
|
// until that wiring exists.
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── Text strokes / frame / background fill ──────────────────────────────
|
|
|
|
|
|
// Strip inline format codes, split / word-wrap into lines, then place each
|
|
|
|
|
|
// line according to text_attachment_point (horizontal) and
|
|
|
|
|
|
// text_left_attachment (vertical), with text_rotation/text_direction applied.
|
|
|
|
|
|
if ml.content_type == LeaderContentType::MText && !ml.context.text_string.is_empty() {
|
|
|
|
|
|
let ctx = &ml.context;
|
|
|
|
|
|
// `ctx.text_height` (when > 0) is the already-resolved WCS text height
|
|
|
|
|
|
// stored in the per-instance context — style × scale_factor × anno
|
|
|
|
|
|
// scale are all already baked in. Multiplying by `effective_scale`
|
|
|
|
|
|
// again would double-scale (e.g., a context height of 100 in a file
|
|
|
|
|
|
// with scale_factor=20 was rendering at 2000 units — 20× too big).
|
|
|
|
|
|
// Only the fallback path (file omits the context value) needs
|
|
|
|
|
|
// scale_factor + annotation scale applied.
|
|
|
|
|
|
let height = if ctx.text_height > 0.0 {
|
|
|
|
|
|
ctx.text_height as f32
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ml.text_height as f32 * effective_scale
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let ins = &ctx.text_location;
|
|
|
|
|
|
// Subtract world_offset in f64 before casting to f32: drawings often
|
|
|
|
|
|
// sit at large absolute coordinates and casting first then subtracting
|
|
|
|
|
|
// throws away the precision needed for the rotated sub-glyph offsets.
|
|
|
|
|
|
let local_ins_x = (ins.x - ox) as f32;
|
|
|
|
|
|
let local_ins_y = (ins.y - oy) as f32;
|
|
|
|
|
|
let z = (ins.z - oz) as f32;
|
|
|
|
|
|
|
|
|
|
|
|
// Rotation: prefer text_direction (transforms survive rotations /
|
|
|
|
|
|
// mirrors when acadrust updates it) and fall back to text_rotation.
|
|
|
|
|
|
// ml.text_angle_type then constrains the final angle:
|
|
|
|
|
|
// - Horizontal: always 0
|
|
|
|
|
|
// - ParallelToLastLeaderLine: keep stored direction
|
|
|
|
|
|
// - Optimized: clamp to (-π/2, π/2] (flip upside-down)
|
|
|
|
|
|
// text_direction_negative finally adds π so reading goes the other way.
|
|
|
|
|
|
let td = ctx.text_direction;
|
|
|
|
|
|
let mut rot = if td.x.abs() > 1e-9 || td.y.abs() > 1e-9 {
|
|
|
|
|
|
(td.y as f32).atan2(td.x as f32)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ctx.text_rotation as f32
|
|
|
|
|
|
};
|
|
|
|
|
|
match ml.text_angle_type {
|
|
|
|
|
|
acadrust::entities::multileader::TextAngleType::Horizontal => rot = 0.0,
|
|
|
|
|
|
acadrust::entities::multileader::TextAngleType::Optimized => {
|
|
|
|
|
|
let pi = std::f32::consts::PI;
|
|
|
|
|
|
if rot > pi / 2.0 {
|
|
|
|
|
|
rot -= pi;
|
|
|
|
|
|
} else if rot <= -pi / 2.0 {
|
|
|
|
|
|
rot += pi;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
acadrust::entities::multileader::TextAngleType::ParallelToLastLeaderLine => {}
|
|
|
|
|
|
}
|
|
|
|
|
|
if ml.text_direction_negative {
|
|
|
|
|
|
rot += std::f32::consts::PI;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Resolve text style via handle when available, falling back to STANDARD.
|
|
|
|
|
|
let style_name = ctx
|
|
|
|
|
|
.text_style_handle
|
|
|
|
|
|
.as_ref()
|
|
|
|
|
|
.and_then(|h| {
|
|
|
|
|
|
document
|
|
|
|
|
|
.text_styles
|
|
|
|
|
|
.iter()
|
|
|
|
|
|
.find(|s| s.handle == *h)
|
|
|
|
|
|
.map(|s| s.name.clone())
|
|
|
|
|
|
})
|
|
|
|
|
|
.unwrap_or_else(|| "STANDARD".to_string());
|
|
|
|
|
|
let style = resolve_text_style(&style_name, document);
|
|
|
|
|
|
let mut rot = rot;
|
|
|
|
|
|
if style.is_upside_down {
|
|
|
|
|
|
rot += std::f32::consts::PI;
|
|
|
|
|
|
}
|
|
|
|
|
|
let (cos_r, sin_r) = (rot.cos(), rot.sin());
|
|
|
|
|
|
|
2026-06-03 16:17:29 +03:00
|
|
|
|
// Side-anchored on the leader-facing edge so the text reads
|
|
|
|
|
|
// outward; flips live with the leader/text side.
|
|
|
|
|
|
use acadrust::entities::multileader::TextAttachmentDirectionType;
|
|
|
|
|
|
let h_anchor: f32 = if text_sign_w >= 0.0 { 0.0 } else { 1.0 };
|
refactor(tess): move per-entity tessellation into entities/
Each entity's tessellation now lives alongside its definition in
entities/*.rs, attached to the entity type via a local trait:
LegacyTess — Viewport / Insert / Hatch / Ole2Frame
(fallback path for entities not on the truck pipeline)
LeaderTess — Leader
MultiLeaderTess — MultiLeader
DimensionTess — Dimension (carries its dim-only helpers too:
~1700 lines of dim_geometry, dim_text_*, format_*,
tolerance, suppression, etc.)
scene/tessellate.rs keeps only the cross-entity dispatcher plus the
shared dim machinery reused by leader/multileader/dimension
(ArrowKind, DimGeom, append_arrow, arrow_from_block, add_segment,
add_polyline, normalized_or, push_tri, color_or_inherit, entity_z,
offset_snap_pts). Cross-entity arc helpers + aci_to_rgba moved to
scene/tess_util.rs.
scene/tessellate.rs shrinks from ~3300 to 809 lines. Compile clean,
all 9 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:12:27 +03:00
|
|
|
|
// Pick the vertical-anchor attachment based on text_attachment_direction:
|
|
|
|
|
|
// Horizontal — leader attaches left/right; use ml.text_left_attachment
|
|
|
|
|
|
// (matches the file's stored ctx.text_left_attachment).
|
|
|
|
|
|
// Vertical — leader attaches top/bottom; use ml.text_top_attachment
|
|
|
|
|
|
// or ml.text_bottom_attachment depending on which side
|
|
|
|
|
|
// the leader is coming from (chosen via the first
|
|
|
|
|
|
// root.direction.y sign).
|
|
|
|
|
|
let leader_from_top = ml
|
|
|
|
|
|
.context
|
|
|
|
|
|
.leader_roots
|
|
|
|
|
|
.first()
|
|
|
|
|
|
.map(|r| r.direction.y < 0.0)
|
|
|
|
|
|
.unwrap_or(true);
|
|
|
|
|
|
let vertical_attach = match ml.text_attachment_direction {
|
|
|
|
|
|
TextAttachmentDirectionType::Vertical => {
|
|
|
|
|
|
if leader_from_top {
|
|
|
|
|
|
ml.text_top_attachment
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ml.text_bottom_attachment
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
TextAttachmentDirectionType::Horizontal => ctx.text_left_attachment,
|
|
|
|
|
|
};
|
|
|
|
|
|
let v_anchor = mleader_v_anchor(vertical_attach);
|
|
|
|
|
|
|
|
|
|
|
|
// Shared MText pipeline — every inline format code (`\f`, `\C`/`\c`,
|
|
|
|
|
|
// `\H`, `\W`, `\Q`, `\T`, `\A`, `\p…`, decorations, stacked
|
|
|
|
|
|
// fractions, …) reaches the stroke output. Stroke origins are
|
|
|
|
|
|
// already in offset-relative space because we pass local_ins_x/y.
|
|
|
|
|
|
let layout = layout_mtext(&MTextRenderOpts {
|
|
|
|
|
|
value: &ctx.text_string,
|
|
|
|
|
|
insertion: [local_ins_x as f64, local_ins_y as f64, z as f64],
|
|
|
|
|
|
height,
|
|
|
|
|
|
rect_w: ctx.text_width as f32,
|
|
|
|
|
|
rotation: rot,
|
|
|
|
|
|
style: &style,
|
|
|
|
|
|
attach_h_anchor: h_anchor,
|
|
|
|
|
|
v_anchor,
|
|
|
|
|
|
line_spacing_factor: ctx.line_spacing_factor as f32,
|
|
|
|
|
|
vertical_text: false,
|
2026-05-31 02:33:47 +03:00
|
|
|
|
want_glyph_boxes: false,
|
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 line_widths = &layout.line_widths;
|
|
|
|
|
|
let max_line_w = line_widths.iter().cloned().fold(0.0_f32, f32::max);
|
|
|
|
|
|
let line_h = layout.line_height;
|
|
|
|
|
|
let v_offset = layout.v_offset;
|
|
|
|
|
|
let n_lines = layout.line_count.max(1) as f32;
|
|
|
|
|
|
|
|
|
|
|
|
// Resolve text color (falls back to entity color for ByLayer / ByBlock).
|
|
|
|
|
|
let text_color = if selected {
|
|
|
|
|
|
line_color
|
|
|
|
|
|
} else {
|
|
|
|
|
|
color_or_inherit(&ctx.text_color, entity_color)
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Same LOD ladder used for top-level Text / MText (see scene/mod.rs):
|
|
|
|
|
|
// h_px < 1 → baseline line (skip glyphs)
|
|
|
|
|
|
// 1 ≤ h < 5 → greeked rect in text color (skip glyphs)
|
|
|
|
|
|
// h_px ≥ 5 → full per-glyph stroke tessellation
|
|
|
|
|
|
let lod_h_px = world_per_pixel.map(|wpp| height / wpp);
|
|
|
|
|
|
let lod_mode = match lod_h_px {
|
|
|
|
|
|
Some(h) if h < 1.0 => 0,
|
|
|
|
|
|
Some(h) if h < 5.0 => 1,
|
|
|
|
|
|
_ => 2,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Helper: map a (local_x, local_y) in the text's pre-rotation frame
|
|
|
|
|
|
// (origin at the insertion point) into WCS render space.
|
|
|
|
|
|
let to_wcs = |lx: f32, ly: f32| -> [f32; 3] {
|
|
|
|
|
|
[
|
|
|
|
|
|
local_ins_x + lx * cos_r - ly * sin_r,
|
|
|
|
|
|
local_ins_y + lx * sin_r + ly * cos_r,
|
|
|
|
|
|
z,
|
|
|
|
|
|
]
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if lod_mode == 0 {
|
|
|
|
|
|
// Baseline of the top line only.
|
|
|
|
|
|
let line_w = line_widths.first().copied().unwrap_or(0.0);
|
|
|
|
|
|
let len_px = world_per_pixel
|
|
|
|
|
|
.map(|wpp| line_w / wpp)
|
|
|
|
|
|
.unwrap_or(f32::INFINITY);
|
|
|
|
|
|
if len_px >= 2.0 {
|
|
|
|
|
|
let line_y_local = v_offset;
|
|
|
|
|
|
let p0 = to_wcs(-line_w * h_anchor, line_y_local);
|
|
|
|
|
|
let p1 = to_wcs(line_w * (1.0 - h_anchor), line_y_local);
|
|
|
|
|
|
wires.push(WireModel {
|
|
|
|
|
|
name: name.clone(),
|
|
|
|
|
|
points: vec![p0, p1],
|
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: text_color,
|
|
|
|
|
|
selected,
|
|
|
|
|
|
aci: 0,
|
|
|
|
|
|
pattern_length: 0.0,
|
|
|
|
|
|
pattern: [0.0; 8],
|
|
|
|
|
|
line_weight_px,
|
2026-06-23 18:52:42 +03:00
|
|
|
|
snap_pts: vec![(glam::DVec3::new(local_ins_x as f64, local_ins_y as f64, z as f64), SnapHint::Node)],
|
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
|
|
|
|
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 if lod_mode == 1 {
|
|
|
|
|
|
// One filled rect per line — keeps the visual "text lives here
|
|
|
|
|
|
// per row" hint that multi-line MText carries, in the text's
|
|
|
|
|
|
// own color. Empty `points` opts out of the face3d 0.45 dim so
|
|
|
|
|
|
// the fill renders at full intensity.
|
|
|
|
|
|
let mut greek_tris: Vec<[f32; 3]> = Vec::with_capacity(line_widths.len() * 6);
|
|
|
|
|
|
for (i, &line_w) in line_widths.iter().enumerate() {
|
|
|
|
|
|
let li = i as f32;
|
|
|
|
|
|
let line_y_bottom = -li * line_h + v_offset;
|
|
|
|
|
|
let line_y_top = line_y_bottom + height;
|
|
|
|
|
|
if line_w <= 0.0 {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
let left = -line_w * h_anchor;
|
|
|
|
|
|
let right = line_w * (1.0 - h_anchor);
|
|
|
|
|
|
let bl = to_wcs(left, line_y_bottom);
|
|
|
|
|
|
let br = to_wcs(right, line_y_bottom);
|
|
|
|
|
|
let tr = to_wcs(right, line_y_top);
|
|
|
|
|
|
let tl = to_wcs(left, line_y_top);
|
|
|
|
|
|
greek_tris.extend_from_slice(&[bl, br, tr, bl, tr, tl]);
|
|
|
|
|
|
}
|
|
|
|
|
|
if !greek_tris.is_empty() {
|
|
|
|
|
|
wires.push(WireModel {
|
|
|
|
|
|
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: text_color,
|
|
|
|
|
|
selected,
|
|
|
|
|
|
aci: 0,
|
|
|
|
|
|
pattern_length: 0.0,
|
|
|
|
|
|
pattern: [0.0; 8],
|
|
|
|
|
|
line_weight_px: 1.0,
|
2026-06-23 18:52:42 +03:00
|
|
|
|
snap_pts: vec![(glam::DVec3::new(local_ins_x as f64, local_ins_y as f64, z as f64), SnapHint::Node)],
|
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
|
|
|
|
tangent_geoms: vec![],
|
|
|
|
|
|
key_vertices: vec![],
|
|
|
|
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
|
|
|
|
|
plinegen: true,
|
|
|
|
|
|
vp_scissor: None,
|
|
|
|
|
|
fill_tris: greek_tris,
|
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 {
|
|
|
|
|
|
// Pre-tessellated by `layout_mtext`. Each TextStroke is in
|
|
|
|
|
|
// local glyph space with its world origin (already offset-
|
|
|
|
|
|
// relative because we passed local_ins_x/y) stored as f64.
|
|
|
|
|
|
let mut text_points: Vec<[f32; 3]> = Vec::new();
|
|
|
|
|
|
for ts in &layout.strokes {
|
|
|
|
|
|
let ox = ts.origin[0] as f32;
|
|
|
|
|
|
let oy = ts.origin[1] as f32;
|
|
|
|
|
|
for stroke in &ts.strokes {
|
|
|
|
|
|
if stroke.len() < 2 {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
text_points.push(nan);
|
|
|
|
|
|
for &[x, y] in stroke {
|
|
|
|
|
|
text_points.push([x + ox, y + oy, z]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if !text_points.is_empty() {
|
|
|
|
|
|
wires.push(WireModel {
|
|
|
|
|
|
name: name.clone(),
|
|
|
|
|
|
points: text_points,
|
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: text_color,
|
|
|
|
|
|
selected,
|
|
|
|
|
|
aci: 0,
|
|
|
|
|
|
pattern_length: 0.0,
|
|
|
|
|
|
pattern: [0.0; 8],
|
|
|
|
|
|
line_weight_px,
|
2026-06-23 18:52:42 +03:00
|
|
|
|
snap_pts: vec![(glam::DVec3::new(local_ins_x as f64, local_ins_y as f64, z as f64), SnapHint::Node)],
|
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
|
|
|
|
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
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Text frame / background-fill rectangle in local frame, then rotated to WCS.
|
|
|
|
|
|
if ml.text_frame || ctx.background_fill_enabled {
|
|
|
|
|
|
// Visual gap so the frame/fill doesn't touch glyph caps.
|
|
|
|
|
|
let pad = height * 0.25;
|
|
|
|
|
|
let block_top = v_offset + height + pad;
|
|
|
|
|
|
let block_bottom = v_offset - (n_lines - 1.0) * line_h - pad;
|
|
|
|
|
|
let block_left = -max_line_w * h_anchor - pad;
|
|
|
|
|
|
let block_right = max_line_w * (1.0 - h_anchor) + pad;
|
|
|
|
|
|
let local_corners: [[f32; 2]; 4] = [
|
|
|
|
|
|
[block_left, block_bottom],
|
|
|
|
|
|
[block_right, block_bottom],
|
|
|
|
|
|
[block_right, block_top],
|
|
|
|
|
|
[block_left, block_top],
|
|
|
|
|
|
];
|
|
|
|
|
|
let wcs_corners: [[f32; 3]; 4] = std::array::from_fn(|i| {
|
|
|
|
|
|
let lx = local_corners[i][0];
|
|
|
|
|
|
let ly = local_corners[i][1];
|
|
|
|
|
|
let wx = local_ins_x + lx * cos_r - ly * sin_r;
|
|
|
|
|
|
let wy = local_ins_y + lx * sin_r + ly * cos_r;
|
|
|
|
|
|
[wx, wy, z]
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// Background fill — emit two triangles; renders under the text strokes.
|
|
|
|
|
|
if ctx.background_fill_enabled {
|
|
|
|
|
|
let fill_color = if selected {
|
|
|
|
|
|
line_color
|
|
|
|
|
|
} else {
|
|
|
|
|
|
color_or_inherit(&ctx.background_fill_color, entity_color)
|
|
|
|
|
|
};
|
|
|
|
|
|
let fill_tris: Vec<[f32; 3]> = vec![
|
|
|
|
|
|
wcs_corners[0],
|
|
|
|
|
|
wcs_corners[1],
|
|
|
|
|
|
wcs_corners[2],
|
|
|
|
|
|
wcs_corners[0],
|
|
|
|
|
|
wcs_corners[2],
|
|
|
|
|
|
wcs_corners[3],
|
|
|
|
|
|
];
|
|
|
|
|
|
wires.push(WireModel {
|
|
|
|
|
|
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,
|
|
|
|
|
|
fill_tris,
|
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
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Text frame — closed rectangle, matches text color.
|
|
|
|
|
|
if ml.text_frame {
|
|
|
|
|
|
let frame_points: Vec<[f32; 3]> = vec![
|
|
|
|
|
|
wcs_corners[0],
|
|
|
|
|
|
wcs_corners[1],
|
|
|
|
|
|
wcs_corners[2],
|
|
|
|
|
|
wcs_corners[3],
|
|
|
|
|
|
wcs_corners[0],
|
|
|
|
|
|
];
|
|
|
|
|
|
wires.push(WireModel {
|
|
|
|
|
|
name,
|
|
|
|
|
|
points: frame_points,
|
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: text_color,
|
|
|
|
|
|
selected,
|
|
|
|
|
|
aci: 0,
|
|
|
|
|
|
pattern_length: 0.0,
|
|
|
|
|
|
pattern: [0.0; 8],
|
|
|
|
|
|
line_weight_px,
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|