forked from 1b-agent/opencadstudio-upstream
feat(modify): TXTEXP turns text into its character outlines
TXTEXP replaces each selected TEXT and MTEXT with one polyline per glyph contour, on the text's layer and with its properties: a lightweight polyline for flat text, a 3D polyline for tilted text. The outlines come from the same glyph tessellation the text renders with, so stroke fonts give their strokes and outline fonts their closed contours. Refs D#1665 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
57eb7fe4db
commit
dd45ac4039
28 files changed
+147
-1
No files matched your search
@@ -294,6 +294,7 @@ command-copy =
|
||||
.mode = COPY أدخل خيار وضع النسخ [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.txtexp-no-text-selected = TXTEXP: لم يتم تحديد نص.
|
||||
.angle-override-shown = تجاوز الزاوية: __ocs_fmt_0__
|
||||
.plot-preview-task = معاينة الطباعة
|
||||
.link-a-csv-data-source = ربط مصدر بيانات CSV
|
||||
|
||||
@@ -281,6 +281,7 @@ command-copy =
|
||||
.displacement = COPY Задайте преместване:
|
||||
.mode = COPY Въведете режим на копиране [Single/Multiple] <Multiple>:
|
||||
common =
|
||||
.txtexp-no-text-selected = TXTEXP: няма избран текст.
|
||||
.angle-override-shown = Замяна на ъгъла: __ocs_fmt_0__
|
||||
.plot-preview-task = Преглед на печата
|
||||
.link-a-csv-data-source = Свързване на CSV източник на данни
|
||||
|
||||
@@ -281,6 +281,7 @@ command-copy =
|
||||
.mode = COPY Zadejte režim kopírování [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.txtexp-no-text-selected = TXTEXP: není vybrán žádný text.
|
||||
.angle-override-shown = Přepsání úhlu: __ocs_fmt_0__
|
||||
.plot-preview-task = Náhled tisku
|
||||
.link-a-csv-data-source = Propojit zdroj dat CSV
|
||||
|
||||
@@ -282,6 +282,7 @@ command-copy =
|
||||
.mode = COPY Kopiermodus eingeben [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.txtexp-no-text-selected = TXTEXP: kein Text gewählt.
|
||||
.angle-override-shown = Winkelvorgabe: __ocs_fmt_0__
|
||||
.plot-preview-task = Plotvorschau
|
||||
.link-a-csv-data-source = CSV-Datenquelle verknüpfen
|
||||
|
||||
@@ -281,6 +281,7 @@ command-copy =
|
||||
.mode = COPY Επιλέξτε λειτουργία αντιγραφής [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.txtexp-no-text-selected = TXTEXP: δεν επιλέχθηκε κείμενο.
|
||||
.angle-override-shown = Παράκαμψη γωνίας: __ocs_fmt_0__
|
||||
.plot-preview-task = Προεπισκόπηση εκτύπωσης
|
||||
.link-a-csv-data-source = Σύνδεση πηγής δεδομένων CSV
|
||||
|
||||
@@ -281,6 +281,7 @@ command-copy =
|
||||
.mode = COPY Enter a copy mode option [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.txtexp-no-text-selected = TXTEXP: no text selected.
|
||||
.angle-override-shown = Angle Override: __ocs_fmt_0__
|
||||
.browser-cannot-open-typed-path = The browser cannot open a typed path; pick the file instead.
|
||||
.recover-drawing = Recover Drawing
|
||||
|
||||
@@ -282,6 +282,7 @@ command-copy =
|
||||
.mode = COPY Indique modo de copia [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.txtexp-no-text-selected = TXTEXP: no se ha seleccionado ningún texto.
|
||||
.angle-override-shown = Modificación de ángulo: __ocs_fmt_0__
|
||||
.plot-preview-task = Vista previa de trazado
|
||||
.link-a-csv-data-source = Vincular un origen de datos CSV
|
||||
|
||||
@@ -281,6 +281,7 @@ command-copy =
|
||||
.mode = COPY Valitse kopiointitila [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.txtexp-no-text-selected = TXTEXP: tekstiä ei ole valittu.
|
||||
.angle-override-shown = Kulman ohitus: __ocs_fmt_0__
|
||||
.plot-preview-task = Tulostuksen esikatselu
|
||||
.link-a-csv-data-source = Linkitä CSV-tietolähde
|
||||
|
||||
@@ -282,6 +282,7 @@ command-copy =
|
||||
.mode = COPY Entrez le mode de copie [Single/Multiple] <Multiple> :
|
||||
|
||||
common =
|
||||
.txtexp-no-text-selected = TXTEXP : aucun texte sélectionné.
|
||||
.angle-override-shown = Remplacement d’angle : __ocs_fmt_0__
|
||||
.plot-preview-task = Aperçu du tracé
|
||||
.link-a-csv-data-source = Lier une source de données CSV
|
||||
|
||||
@@ -270,6 +270,7 @@ command-copy =
|
||||
.mode = COPY कॉपी मोड विकल्प दर्ज करें [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.txtexp-no-text-selected = TXTEXP: कोई टेक्स्ट चयनित नहीं।
|
||||
.angle-override-shown = कोण ओवरराइड: __ocs_fmt_0__
|
||||
.plot-preview-task = प्लॉट पूर्वावलोकन
|
||||
.link-a-csv-data-source = CSV डेटा स्रोत लिंक करें
|
||||
|
||||
@@ -280,6 +280,7 @@ command-copy =
|
||||
.mode = COPY Adja meg a másolási módot [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.txtexp-no-text-selected = TXTEXP: nincs kijelölt szöveg.
|
||||
.angle-override-shown = Szög felülbírálása: __ocs_fmt_0__
|
||||
.parametric-coincident = Egybeeső
|
||||
.parametric-colinear = Egy egyenesbe eső
|
||||
|
||||
@@ -281,6 +281,7 @@ command-copy =
|
||||
.mode = COPY Specificare modalità di copia [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.txtexp-no-text-selected = TXTEXP: nessun testo selezionato.
|
||||
.angle-override-shown = Sostituzione angolo: __ocs_fmt_0__
|
||||
.plot-preview-task = Anteprima di stampa
|
||||
.link-a-csv-data-source = Collega un'origine dati CSV
|
||||
|
||||
@@ -272,6 +272,7 @@ command-copy =
|
||||
.mode = COPY コピーモードを入力 [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.txtexp-no-text-selected = TXTEXP: 文字が選択されていません。
|
||||
.angle-override-shown = 角度の優先: __ocs_fmt_0__
|
||||
.plot-preview-task = 印刷プレビュー
|
||||
.link-a-csv-data-source = CSV データ ソースをリンク
|
||||
|
||||
@@ -281,6 +281,7 @@ command-copy =
|
||||
.mode = COPY 복사 모드 옵션 입력 [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.txtexp-no-text-selected = TXTEXP: 선택된 문자가 없습니다.
|
||||
.angle-override-shown = 각도 재지정: __ocs_fmt_0__
|
||||
.plot-preview-task = 플롯 미리 보기
|
||||
.link-a-csv-data-source = CSV 데이터 원본 연결
|
||||
|
||||
@@ -281,6 +281,7 @@ command-copy =
|
||||
.mode = COPY ປ້ອນຕົວເລືອກໂໝດການຄັດລອກ [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.txtexp-no-text-selected = TXTEXP: ບໍ່ໄດ້ເລືອກຂໍ້ຄວາມ.
|
||||
.angle-override-shown = ກຳນົດມຸມແທນ: __ocs_fmt_0__
|
||||
.remote-image-url-could-not-be-fetched-the-request-timed-out = ບໍ່ສາມາດດຶງຮູບພາບທາງໄກ "__ocs_fmt_0__" ໄດ້: ການຮ້ອງຂໍໝົດເວລາ.
|
||||
.remote-image-url-was-not-loaded-it-exceeds-the-64-mb-remote-image-limit = ບໍ່ໄດ້ໂຫຼດຮູບພາບທາງໄກ "__ocs_fmt_0__": ມັນເກີນຂີດຈຳກັດ 64 MB ຂອງຮູບພາບທາງໄກ.
|
||||
|
||||
@@ -282,6 +282,7 @@ command-copy =
|
||||
.mode = COPY Geef kopieermodus op [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.txtexp-no-text-selected = TXTEXP: geen tekst geselecteerd.
|
||||
.angle-override-shown = Hoekoverschrijving: __ocs_fmt_0__
|
||||
.plot-preview-task = Plotvoorbeeld
|
||||
.link-a-csv-data-source = CSV-gegevensbron koppelen
|
||||
|
||||
@@ -281,6 +281,7 @@ command-copy =
|
||||
.mode = COPY Podaj tryb kopiowania [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.txtexp-no-text-selected = TXTEXP: nie wybrano tekstu.
|
||||
.angle-override-shown = Zastąpienie kąta: __ocs_fmt_0__
|
||||
.plot-preview-task = Podgląd wydruku
|
||||
.link-a-csv-data-source = Połącz źródło danych CSV
|
||||
|
||||
@@ -282,6 +282,7 @@ command-copy =
|
||||
.mode = COPY Informe o modo de cópia [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.txtexp-no-text-selected = TXTEXP: nenhum texto selecionado.
|
||||
.angle-override-shown = Substituição de ângulo: __ocs_fmt_0__
|
||||
.plot-preview-task = Visualização da plotagem
|
||||
.link-a-csv-data-source = Vincular uma fonte de dados CSV
|
||||
|
||||
@@ -285,6 +285,7 @@ command-copy =
|
||||
.mode = COPY Задайте режим копирования [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.txtexp-no-text-selected = TXTEXP: текст не выбран.
|
||||
.angle-override-shown = Переопределение угла: __ocs_fmt_0__
|
||||
.plot-preview-task = Предварительный просмотр печати
|
||||
.link-a-csv-data-source = Связать источник данных CSV
|
||||
|
||||
@@ -267,6 +267,7 @@ command-copy =
|
||||
.mode = COPY Kopyalama modu seçin [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.txtexp-no-text-selected = TXTEXP: metin seçilmedi.
|
||||
.angle-override-shown = Açı geçersiz kılma: __ocs_fmt_0__
|
||||
.plot-preview-task = Çizim önizleme
|
||||
.link-a-csv-data-source = CSV veri kaynağı bağla
|
||||
|
||||
@@ -269,6 +269,7 @@ command-copy =
|
||||
.mode = COPY 输入复制模式选项 [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.txtexp-no-text-selected = TXTEXP: 未选择文字。
|
||||
.angle-override-shown = 角度替代:__ocs_fmt_0__
|
||||
.plot-preview-task = 打印预览
|
||||
.link-a-csv-data-source = 链接 CSV 数据源
|
||||
|
||||
@@ -281,6 +281,7 @@ command-copy =
|
||||
.mode = COPY 輸入複製模式選項 [Single/Multiple] <Multiple>:
|
||||
|
||||
common =
|
||||
.txtexp-no-text-selected = TXTEXP: 未選擇文字。
|
||||
.angle-override-shown = 角度取代:__ocs_fmt_0__
|
||||
.plot-preview-task = 出圖預覽
|
||||
.link-a-csv-data-source = 連結 CSV 資料來源
|
||||
|
||||
@@ -1303,6 +1303,38 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
// TXTEXP — text becomes the polylines of its character outlines.
|
||||
"TXTEXP" => {
|
||||
let selected: Vec<_> = self.tabs[i]
|
||||
.scene
|
||||
.selected_entities()
|
||||
.into_iter()
|
||||
.filter(|(handle, _)| !self.tabs[i].scene.is_layer_locked(*handle))
|
||||
.collect();
|
||||
if selected.is_empty() {
|
||||
use crate::modules::draw::select::SelectObjectsCommand;
|
||||
let cmd = SelectObjectsCommand::new("TXTEXP");
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
} else {
|
||||
use crate::modules::draw::modify::explode::apply_explode_replacements;
|
||||
use crate::modules::draw::modify::txtexp::plan_text_explode;
|
||||
let replacements = plan_text_explode(&selected, &self.tabs[i].scene.document);
|
||||
let exploded = replacements.len();
|
||||
if exploded > 0 {
|
||||
self.push_undo_snapshot(i, "TXTEXP");
|
||||
apply_explode_replacements(&mut self.tabs[i].scene, replacements);
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
self.command_line
|
||||
.push_output(crate::tf!("{exploded} object(s) exploded.").as_ref());
|
||||
} else {
|
||||
self.command_line
|
||||
.push_info(crate::t!("TXTEXP: no text selected.").as_ref());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"OFFSET" => {
|
||||
use crate::modules::draw::modify::offset::{is_offsettable, OffsetCommand};
|
||||
let all_entities: Vec<_> = self.tabs[i]
|
||||
|
||||
@@ -277,6 +277,7 @@ fn command_category(name: &str) -> &'static str {
|
||||
"BREAK",
|
||||
"JOIN",
|
||||
"EXPLODE",
|
||||
"TXTEXP",
|
||||
"STRETCH",
|
||||
"ARRAY",
|
||||
"PEDIT",
|
||||
@@ -349,6 +350,7 @@ fn command_selection_policy(name: &str) -> &'static str {
|
||||
"CUTCLIP",
|
||||
"ERASE",
|
||||
"EXPLODE",
|
||||
"TXTEXP",
|
||||
"GROUP",
|
||||
"LAYFRZ",
|
||||
"LAYLCK",
|
||||
|
||||
@@ -3697,6 +3697,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
||||
"Width factor" => Some(("common", "width-factor-sentence-case")),
|
||||
"Width: %{value}" => Some(("ui", "width-value")),
|
||||
"Window" => Some(("common", "window")),
|
||||
"TXTEXP: no text selected." => Some(("common", "txtexp-no-text-selected")),
|
||||
"Windows desktop" => Some(("styles", "windows-desktop")),
|
||||
"Wipeout" => Some(("common", "wipeout")),
|
||||
"Wireframe" => Some(("common", "wireframe")),
|
||||
|
||||
@@ -30,3 +30,4 @@ pub mod stretch;
|
||||
pub mod torient;
|
||||
pub mod translate;
|
||||
pub mod trim;
|
||||
pub mod txtexp;
|
||||
@@ -0,0 +1,87 @@
|
||||
// TXTEXP — replace text with the outlines of its characters.
|
||||
//
|
||||
// Each glyph contour of a TEXT or MTEXT becomes a polyline on the text's
|
||||
// layer and with its properties: a lightweight polyline when the text lies
|
||||
// flat, a 3D polyline when it is tilted.
|
||||
|
||||
use codec::entities::{LwPolyline, LwVertex, Polyline3D, Vertex3DPolyline};
|
||||
use codec::types::Vector2;
|
||||
use codec::{CadDocument, EntityType, Handle};
|
||||
|
||||
use crate::scene::convert::acad_to_render::{convert, RenderObject};
|
||||
|
||||
/// The outlines replacing each explodable text of `selected`, as
|
||||
/// (text, polylines) pairs ready for `apply_explode_replacements`.
|
||||
pub fn plan_text_explode(
|
||||
selected: &[(Handle, &EntityType)],
|
||||
document: &CadDocument,
|
||||
) -> Vec<(Handle, Vec<EntityType>)> {
|
||||
selected
|
||||
.iter()
|
||||
.filter_map(|(handle, entity)| {
|
||||
let outlines = text_outlines(entity, document);
|
||||
(!outlines.is_empty()).then_some((*handle, outlines))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The character outlines of a TEXT or MTEXT; empty for anything else.
|
||||
pub fn text_outlines(entity: &EntityType, document: &CadDocument) -> Vec<EntityType> {
|
||||
let (common, elevation) = match entity {
|
||||
EntityType::Text(text) => (&text.common, text.insertion_point.z),
|
||||
EntityType::MText(text) => (&text.common, text.insertion_point.z),
|
||||
_ => return Vec::new(),
|
||||
};
|
||||
let Some(RenderObject::Text(groups)) = convert(entity, document).map(|render| render.object)
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut common = common.clone();
|
||||
common.handle = Handle::NULL;
|
||||
let mut outlines = Vec::new();
|
||||
for group in &groups {
|
||||
for stroke in group.strokes.iter().filter(|stroke| stroke.len() >= 2) {
|
||||
let mut points: Vec<[f64; 3]> = stroke
|
||||
.iter()
|
||||
.map(|&[x, y]| {
|
||||
let (x, y) = (x as f64, y as f64);
|
||||
match group.plane {
|
||||
Some(plane) => std::array::from_fn(|axis| {
|
||||
plane.origin[axis] + plane.x_axis[axis] * x + plane.y_axis[axis] * y
|
||||
}),
|
||||
None => [group.origin[0] + x, group.origin[1] + y, elevation],
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let closed = points.len() > 2 && {
|
||||
let (first, last) = (points[0], points[points.len() - 1]);
|
||||
(0..3).all(|axis| (first[axis] - last[axis]).abs() <= 1e-9)
|
||||
};
|
||||
if closed {
|
||||
points.pop();
|
||||
}
|
||||
let z = points[0][2];
|
||||
if points.iter().all(|point| (point[2] - z).abs() <= 1e-9) {
|
||||
let mut polyline = LwPolyline::new();
|
||||
polyline.common = common.clone();
|
||||
polyline.is_closed = closed;
|
||||
polyline.elevation = z;
|
||||
polyline.vertices = points
|
||||
.iter()
|
||||
.map(|point| LwVertex::new(Vector2::new(point[0], point[1])))
|
||||
.collect();
|
||||
outlines.push(EntityType::LwPolyline(polyline));
|
||||
} else {
|
||||
let mut polyline = Polyline3D::new();
|
||||
polyline.common = common.clone();
|
||||
polyline.flags.closed = closed;
|
||||
polyline.vertices = points
|
||||
.iter()
|
||||
.map(|point| Vertex3DPolyline::from_xyz(point[0], point[1], point[2]))
|
||||
.collect();
|
||||
outlines.push(EntityType::Polyline3D(polyline));
|
||||
}
|
||||
}
|
||||
}
|
||||
outlines
|
||||
}
|
||||
@@ -360,6 +360,7 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"MOVE",
|
||||
"ROTATE",
|
||||
"SCALE",
|
||||
"STRETCH"
|
||||
"STRETCH",
|
||||
"TXTEXP"
|
||||
]
|
||||
}); // SelectObjectsCommand
|
||||
Reference in new issue
Block a user