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
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-22 14:56:27 +03:00
|
|
|
|
use glam::Vec3;
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
2026-05-02 19:59:11 +03:00
|
|
|
|
let mut snap_pts: Vec<(Vec3, SnapHint)> = Vec::new();
|
2026-03-22 14:56:27 +03:00
|
|
|
|
let mut first = true;
|
|
|
|
|
|
|
2026-05-12 23:47:40 +03:00
|
|
|
|
// snap_pts uses f32 (UI-only); cast at construction.
|
|
|
|
|
|
let node = |arr: [f64; 3]| {
|
|
|
|
|
|
(
|
|
|
|
|
|
Vec3::new(arr[0] as f32, arr[1] as f32, arr[2] as f32),
|
|
|
|
|
|
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.
|
|
|
|
|
|
fn reflect_xy_direction(dx: &mut f64, dy: &mut f64, p1: Vec3, p2: Vec3) {
|
|
|
|
|
|
let zero = Vec3::ZERO;
|
|
|
|
|
|
let p2_rel = Vec3::new(p2.x - p1.x, p2.y - p1.y, 0.0);
|
|
|
|
|
|
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();
|
|
|
|
|
|
let mut key_verts: Vec<[f32; 3]> = Vec::new();
|
|
|
|
|
|
let mut snap_pts: Vec<(Vec3, SnapHint)> = Vec::new();
|
|
|
|
|
|
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);
|
|
|
|
|
|
snap_pts.push((Vec3::from(cp_f), SnapHint::Node));
|
|
|
|
|
|
|
|
|
|
|
|
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 {
|
|
|
|
|
|
key_verts.push(c);
|
|
|
|
|
|
snap_pts.push((Vec3::from(c), SnapHint::Node));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ── 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-11 00:04:37 +03:00
|
|
|
|
snap_pts: vec![(Vec3::new(local_ins_x, local_ins_y, z), 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![],
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} 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-11 00:04:37 +03:00
|
|
|
|
snap_pts: vec![(Vec3::new(local_ins_x, local_ins_y, z), 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,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} 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-11 00:04:37 +03:00
|
|
|
|
snap_pts: vec![(Vec3::new(local_ins_x, local_ins_y, z), 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![],
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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![],
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
wires
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|