feat(draworder): HATCHTOBACK command and interactive DRAWORDER workflows
Implement the AutoCAD compatible HATCHTOBACK (HB) command and enhance the interactive DRAWORDER command pipeline with comprehensive verb selection and viewport entity picking. Key implementations and architectural details: 1. HATCHTOBACK command execution * Isolates entities belonging to the active layout block record or global space. * Filters out entities residing on locked layers using an efficient hash set lookup. * Records targeted delta undo operations on SortEntitiesTable objects (~100 bytes) rather than snapshotting full document structures. * Performs granular draw depth cache invalidation via Scene::invalidate_draw_depth, preserving existing geometry tessellations and spatial indices. * Emits localized user notifications when no qualifying hatch entities are found. 2. Interactive DrawOrderCommand state machine * Supports preselected entities as well as interactive on demand selection gathering. * Implements subverb selection supporting single character shortcuts (A for Above, U for Under, F for Front, B for Back) and case insensitive keyword parsing. * Enables interactive reference object picking directly in the viewport via needs_entity_pick and on_entity_pick callbacks. * Supports hexadecimal handle specification directly from the command line interface. 3. Ecosystem and system integration * Bumps DEFAULT_ALIASES_VERSION from 2 to 3 to register the HB alias while preserving existing user overrides. * Integrates HATCHTOBACK and HB into dynamic autocomplete registries. * Adds localized message strings across all 20 Fluent translation catalogs. * Corrects hatch boundary serialization flags to ensure outermost boundary rings consistently carry the external attribute.
This commit is contained in:
parent
44a40eda0b
commit
236b7756f8
30 changed files with 979 additions and 61 deletions
|
|
@ -21,6 +21,7 @@ SPL, *SPLINE
|
||||||
H, *HATCH
|
H, *HATCH
|
||||||
BH, *HATCH
|
BH, *HATCH
|
||||||
HE, *HATCHEDIT
|
HE, *HATCHEDIT
|
||||||
|
HB, *HATCHTOBACK
|
||||||
BO, *BOUNDARY
|
BO, *BOUNDARY
|
||||||
GD, *GRADIENT
|
GD, *GRADIENT
|
||||||
XL, *XLINE
|
XL, *XLINE
|
||||||
|
|
|
||||||
|
|
@ -2347,6 +2347,7 @@ view =
|
||||||
.zoom-extents =
|
.zoom-extents =
|
||||||
تكبير إلى
|
تكبير إلى
|
||||||
النطاقات
|
النطاقات
|
||||||
|
.cmd-no-hatch-objects = __ocs_fmt_0__: ┘äϺ Ϭ┘êϼϻ ┘âϺϪ┘åϺϬ Ϭϩ┘ä┘è┘ä.
|
||||||
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: لا توجد كائنات نص أو أبعاد.
|
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: لا توجد كائنات نص أو أبعاد.
|
||||||
.cmd-not-yet-implemented = __ocs_fmt_0__: لم يُنفّذ بعد.
|
.cmd-not-yet-implemented = __ocs_fmt_0__: لم يُنفّذ بعد.
|
||||||
.viewport-s-in-layout = __ocs_fmt_0__ منفذ عرض في التخطيط "__ocs_fmt_1__":
|
.viewport-s-in-layout = __ocs_fmt_0__ منفذ عرض في التخطيط "__ocs_fmt_1__":
|
||||||
|
|
|
||||||
|
|
@ -2334,6 +2334,7 @@ view =
|
||||||
.zoom-extents =
|
.zoom-extents =
|
||||||
Мащаб
|
Мащаб
|
||||||
Обхват
|
Обхват
|
||||||
|
.cmd-no-hatch-objects = __ocs_fmt_0__: ð¢ÐÅð╝ð░ ÐëÐÇð©Ðàð¥ð▓ÐèÐçð¢ð© ð¥ð▒ðÁð║Ðéð©.
|
||||||
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: няма текстови обекти или оразмерявания.
|
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: няма текстови обекти или оразмерявания.
|
||||||
.cmd-not-yet-implemented = __ocs_fmt_0__: все още не е реализирано.
|
.cmd-not-yet-implemented = __ocs_fmt_0__: все още не е реализирано.
|
||||||
.viewport-s-in-layout = __ocs_fmt_0__ изглед(а) в оформление "__ocs_fmt_1__":
|
.viewport-s-in-layout = __ocs_fmt_0__ изглед(а) в оформление "__ocs_fmt_1__":
|
||||||
|
|
|
||||||
|
|
@ -2334,6 +2334,7 @@ view =
|
||||||
.zoom-extents =
|
.zoom-extents =
|
||||||
Přiblížení
|
Přiblížení
|
||||||
Rozsahy
|
Rozsahy
|
||||||
|
.cmd-no-hatch-objects = __ocs_fmt_0__: ┼¥├ídn├® ┼írafovac├¡ objekty.
|
||||||
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: žádný text ani kótovací objekty.
|
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: žádný text ani kótovací objekty.
|
||||||
.cmd-not-yet-implemented = __ocs_fmt_0__: dosud neimplementováno.
|
.cmd-not-yet-implemented = __ocs_fmt_0__: dosud neimplementováno.
|
||||||
.viewport-s-in-layout =Výřezy __ocs_fmt_0__ v rozložení "__ocs_fmt_1__":
|
.viewport-s-in-layout =Výřezy __ocs_fmt_0__ v rozložení "__ocs_fmt_1__":
|
||||||
|
|
|
||||||
|
|
@ -2333,6 +2333,7 @@ view =
|
||||||
.zoom-extents =
|
.zoom-extents =
|
||||||
Zoom
|
Zoom
|
||||||
Umfang
|
Umfang
|
||||||
|
.cmd-no-hatch-objects = __ocs_fmt_0__: keine Schraffurobjekte.
|
||||||
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: keine Text- oder Dimensionsobjekte.
|
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: keine Text- oder Dimensionsobjekte.
|
||||||
.cmd-not-yet-implemented = __ocs_fmt_0__: noch nicht umgesetzt.
|
.cmd-not-yet-implemented = __ocs_fmt_0__: noch nicht umgesetzt.
|
||||||
.viewport-s-in-layout = __ocs_fmt_0__ Ansichtsfenster(s) im Layout "__ocs_fmt_1__":
|
.viewport-s-in-layout = __ocs_fmt_0__ Ansichtsfenster(s) im Layout "__ocs_fmt_1__":
|
||||||
|
|
|
||||||
|
|
@ -2363,6 +2363,7 @@ view =
|
||||||
.zoom-extents =
|
.zoom-extents =
|
||||||
Zoom
|
Zoom
|
||||||
Extents
|
Extents
|
||||||
|
.cmd-no-hatch-objects = __ocs_fmt_0__: no hatch objects.
|
||||||
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: no text or dimension objects.
|
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: no text or dimension objects.
|
||||||
.cmd-not-yet-implemented = __ocs_fmt_0__: not yet implemented.
|
.cmd-not-yet-implemented = __ocs_fmt_0__: not yet implemented.
|
||||||
.viewport-s-in-layout = __ocs_fmt_0__ viewport(s) in layout "__ocs_fmt_1__":
|
.viewport-s-in-layout = __ocs_fmt_0__ viewport(s) in layout "__ocs_fmt_1__":
|
||||||
|
|
|
||||||
|
|
@ -2335,6 +2335,7 @@ view =
|
||||||
.zoom-extents =
|
.zoom-extents =
|
||||||
Zoom
|
Zoom
|
||||||
Extensión
|
Extensión
|
||||||
|
.cmd-no-hatch-objects = __ocs_fmt_0__: no hay objetos de sombreado.
|
||||||
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: no hay objetos de texto ni de cota.
|
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: no hay objetos de texto ni de cota.
|
||||||
.cmd-not-yet-implemented = __ocs_fmt_0__: aún no implementado.
|
.cmd-not-yet-implemented = __ocs_fmt_0__: aún no implementado.
|
||||||
.viewport-s-in-layout = __ocs_fmt_0__ ventanas gráficas en la presentación «__ocs_fmt_1__»:
|
.viewport-s-in-layout = __ocs_fmt_0__ ventanas gráficas en la presentación «__ocs_fmt_1__»:
|
||||||
|
|
|
||||||
|
|
@ -2334,6 +2334,7 @@ view =
|
||||||
.zoom-extents =
|
.zoom-extents =
|
||||||
Zoomaa
|
Zoomaa
|
||||||
Laajuudet
|
Laajuudet
|
||||||
|
.cmd-no-hatch-objects = __ocs_fmt_0__: ei t├ñytt├Âobjekteja.
|
||||||
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: ei tekstiä tai mittaobjekteja.
|
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: ei tekstiä tai mittaobjekteja.
|
||||||
.cmd-not-yet-implemented = __ocs_fmt_0__: ei vielä otettu käyttöön.
|
.cmd-not-yet-implemented = __ocs_fmt_0__: ei vielä otettu käyttöön.
|
||||||
.viewport-s-in-layout =__ocs_fmt_0__-näkymä(t) asettelussa "__ocs_fmt_1__":
|
.viewport-s-in-layout =__ocs_fmt_0__-näkymä(t) asettelussa "__ocs_fmt_1__":
|
||||||
|
|
|
||||||
|
|
@ -2335,6 +2335,7 @@ view =
|
||||||
.zoom-extents =
|
.zoom-extents =
|
||||||
Zoom
|
Zoom
|
||||||
Portée
|
Portée
|
||||||
|
.cmd-no-hatch-objects = __ocs_fmt_0__: aucun objet de hachure.
|
||||||
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: aucun objet texte ou dimensionnel.
|
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: aucun objet texte ou dimensionnel.
|
||||||
.cmd-not-yet-implemented = __ocs_fmt_0__: pas encore mis en œuvre.
|
.cmd-not-yet-implemented = __ocs_fmt_0__: pas encore mis en œuvre.
|
||||||
.viewport-s-in-layout = __ocs_fmt_0__ fenêtre(s) dans la présentation « __ocs_fmt_1__ » :
|
.viewport-s-in-layout = __ocs_fmt_0__ fenêtre(s) dans la présentation « __ocs_fmt_1__ » :
|
||||||
|
|
|
||||||
|
|
@ -2323,6 +2323,7 @@ view =
|
||||||
.zoom-extents =
|
.zoom-extents =
|
||||||
ज़ूम
|
ज़ूम
|
||||||
बहार
|
बहार
|
||||||
|
.cmd-no-hatch-objects = __ocs_fmt_0__: ÓñòÓÑïÓñê Óñ╣ÓÑêÓñÜ ÓñæÓñ¼ÓÑìÓñ£ÓÑçÓñòÓÑìÓñƒ Óñ¿Óñ╣ÓÑÇÓñéÓÑñ
|
||||||
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: कोई टेक्स्ट या डाइमेंशन ऑब्जेक्ट नहीं।
|
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: कोई टेक्स्ट या डाइमेंशन ऑब्जेक्ट नहीं।
|
||||||
.cmd-not-yet-implemented = __ocs_fmt_0__: अभी तक लागू नहीं है।
|
.cmd-not-yet-implemented = __ocs_fmt_0__: अभी तक लागू नहीं है।
|
||||||
.viewport-s-in-layout = लेआउट "__ocs_fmt_1__" में __ocs_fmt_0__ व्यूपोर्ट:
|
.viewport-s-in-layout = लेआउट "__ocs_fmt_1__" में __ocs_fmt_0__ व्यूपोर्ट:
|
||||||
|
|
|
||||||
|
|
@ -2334,6 +2334,7 @@ view =
|
||||||
.zoom-extents =
|
.zoom-extents =
|
||||||
Zoom
|
Zoom
|
||||||
Terjedelem
|
Terjedelem
|
||||||
|
.cmd-no-hatch-objects = __ocs_fmt_0__: nincs sraffozási objektum.
|
||||||
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: nincs szöveg vagy méretobjektum.
|
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: nincs szöveg vagy méretobjektum.
|
||||||
.cmd-not-yet-implemented = __ocs_fmt_0__: még nincs implementálva.
|
.cmd-not-yet-implemented = __ocs_fmt_0__: még nincs implementálva.
|
||||||
.viewport-s-in-layout =__ocs_fmt_0__ nézetablak(ok) a „__ocs_fmt_1__” elrendezésben:
|
.viewport-s-in-layout =__ocs_fmt_0__ nézetablak(ok) a „__ocs_fmt_1__” elrendezésben:
|
||||||
|
|
|
||||||
|
|
@ -2334,6 +2334,7 @@ view =
|
||||||
.zoom-extents =
|
.zoom-extents =
|
||||||
Zoom
|
Zoom
|
||||||
Estensioni
|
Estensioni
|
||||||
|
.cmd-no-hatch-objects = __ocs_fmt_0__: nessun oggetto tratteggio.
|
||||||
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: nessun testo o oggetto dimensione.
|
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: nessun testo o oggetto dimensione.
|
||||||
.cmd-not-yet-implemented = __ocs_fmt_0__: non ancora implementato.
|
.cmd-not-yet-implemented = __ocs_fmt_0__: non ancora implementato.
|
||||||
.viewport-s-in-layout =Finestre __ocs_fmt_0__ nel layout "__ocs_fmt_1__":
|
.viewport-s-in-layout =Finestre __ocs_fmt_0__ nel layout "__ocs_fmt_1__":
|
||||||
|
|
|
||||||
|
|
@ -2323,6 +2323,7 @@ view =
|
||||||
.zoom-extents =
|
.zoom-extents =
|
||||||
全体表示
|
全体表示
|
||||||
ズーム
|
ズーム
|
||||||
|
.cmd-no-hatch-objects = __ocs_fmt_0__: ÒâÅÒââÒâüÒâ│Òé░ Òé¬ÒâûÒé©ÒéºÒé»ÒâêÒüîÒüéÒéèÒü¥ÒüøÒéôÒÇé
|
||||||
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: テキストまたは寸法オブジェクトがありません。
|
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: テキストまたは寸法オブジェクトがありません。
|
||||||
.cmd-not-yet-implemented = __ocs_fmt_0__: まだ実装されていません。
|
.cmd-not-yet-implemented = __ocs_fmt_0__: まだ実装されていません。
|
||||||
.viewport-s-in-layout = レイアウト「__ocs_fmt_1__」内の __ocs_fmt_0__ 個のビューポート:
|
.viewport-s-in-layout = レイアウト「__ocs_fmt_1__」内の __ocs_fmt_0__ 個のビューポート:
|
||||||
|
|
|
||||||
|
|
@ -2334,6 +2334,7 @@ view =
|
||||||
.zoom-extents =
|
.zoom-extents =
|
||||||
줌
|
줌
|
||||||
범위
|
범위
|
||||||
|
.cmd-no-hatch-objects = __ocs_fmt_0__: Ýò┤ý╣ÿ Û░Øý▓┤Û░Ç ýùåýèÁÙïêÙïñ.
|
||||||
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: 문자 또는 치수 객체가 없습니다.
|
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: 문자 또는 치수 객체가 없습니다.
|
||||||
.cmd-not-yet-implemented = __ocs_fmt_0__: 아직 구현되지 않았습니다.
|
.cmd-not-yet-implemented = __ocs_fmt_0__: 아직 구현되지 않았습니다.
|
||||||
.viewport-s-in-layout ="__ocs_fmt_1__" 레이아웃의 __ocs_fmt_0__ 뷰포트:
|
.viewport-s-in-layout ="__ocs_fmt_1__" 레이아웃의 __ocs_fmt_0__ 뷰포트:
|
||||||
|
|
|
||||||
|
|
@ -2335,6 +2335,7 @@ view =
|
||||||
.zoom-extents =
|
.zoom-extents =
|
||||||
Zoomen
|
Zoomen
|
||||||
Maten
|
Maten
|
||||||
|
.cmd-no-hatch-objects = __ocs_fmt_0__: geen arceerobjecten.
|
||||||
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: geen tekst- of maatobjecten.
|
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: geen tekst- of maatobjecten.
|
||||||
.cmd-not-yet-implemented = __ocs_fmt_0__: nog niet geïmplementeerd.
|
.cmd-not-yet-implemented = __ocs_fmt_0__: nog niet geïmplementeerd.
|
||||||
.viewport-s-in-layout = __ocs_fmt_0__ weergavevenster(s) in indeling "__ocs_fmt_1__":
|
.viewport-s-in-layout = __ocs_fmt_0__ weergavevenster(s) in indeling "__ocs_fmt_1__":
|
||||||
|
|
|
||||||
|
|
@ -2334,6 +2334,7 @@ view =
|
||||||
.zoom-extents =
|
.zoom-extents =
|
||||||
Powiększ
|
Powiększ
|
||||||
Zakresy
|
Zakresy
|
||||||
|
.cmd-no-hatch-objects = __ocs_fmt_0__: brak obiekt├│w kreskowania.
|
||||||
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: brak obiektów tekstowych i wymiarowych.
|
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: brak obiektów tekstowych i wymiarowych.
|
||||||
.cmd-not-yet-implemented = __ocs_fmt_0__: jeszcze nie wdrożony.
|
.cmd-not-yet-implemented = __ocs_fmt_0__: jeszcze nie wdrożony.
|
||||||
.viewport-s-in-layout =Okna widokowe __ocs_fmt_0__ w układzie „__ocs_fmt_1__”:
|
.viewport-s-in-layout =Okna widokowe __ocs_fmt_0__ w układzie „__ocs_fmt_1__”:
|
||||||
|
|
|
||||||
|
|
@ -2335,6 +2335,7 @@ view =
|
||||||
.zoom-extents =
|
.zoom-extents =
|
||||||
Zoom nas
|
Zoom nas
|
||||||
extensões
|
extensões
|
||||||
|
.cmd-no-hatch-objects = __ocs_fmt_0__: nenhum objeto de hachura.
|
||||||
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: nenhum objeto de texto ou cota.
|
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: nenhum objeto de texto ou cota.
|
||||||
.cmd-not-yet-implemented = __ocs_fmt_0__: ainda não implementado.
|
.cmd-not-yet-implemented = __ocs_fmt_0__: ainda não implementado.
|
||||||
.viewport-s-in-layout = __ocs_fmt_0__ janelas de visualização no layout “__ocs_fmt_1__”:
|
.viewport-s-in-layout = __ocs_fmt_0__ janelas de visualização no layout “__ocs_fmt_1__”:
|
||||||
|
|
|
||||||
|
|
@ -2338,6 +2338,7 @@ view =
|
||||||
.zoom-extents =
|
.zoom-extents =
|
||||||
Зум
|
Зум
|
||||||
размеры
|
размеры
|
||||||
|
.cmd-no-hatch-objects = __ocs_fmt_0__: ð¢ðÁÐé ð¥ð▒ÐèðÁð║Ðéð¥ð▓ ÐêÐéÐÇð©Ðàð¥ð▓ð║ð©.
|
||||||
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: нет текстовых или размерных объектов.
|
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: нет текстовых или размерных объектов.
|
||||||
.cmd-not-yet-implemented = __ocs_fmt_0__: еще не реализовано.
|
.cmd-not-yet-implemented = __ocs_fmt_0__: еще не реализовано.
|
||||||
.viewport-s-in-layout = Видовые экраны (__ocs_fmt_0__) на листе «__ocs_fmt_1__»:
|
.viewport-s-in-layout = Видовые экраны (__ocs_fmt_0__) на листе «__ocs_fmt_1__»:
|
||||||
|
|
|
||||||
|
|
@ -2303,6 +2303,7 @@ view =
|
||||||
.zoom-window-specify-first-corner = ZOOM WINDOW İlk köşeyi belirtin:
|
.zoom-window-specify-first-corner = ZOOM WINDOW İlk köşeyi belirtin:
|
||||||
.zoom-window-specify-opposite-corner = ZOOM WINDOW Karşı köşeyi belirtin:
|
.zoom-window-specify-opposite-corner = ZOOM WINDOW Karşı köşeyi belirtin:
|
||||||
.zoom-extents = Sınırlara Yakınlaştır
|
.zoom-extents = Sınırlara Yakınlaştır
|
||||||
|
.cmd-no-hatch-objects = __ocs_fmt_0__: Tarama nesnesi yok.
|
||||||
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: Metin veya ölçü nesnesi yok.
|
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__: Metin veya ölçü nesnesi yok.
|
||||||
.cmd-not-yet-implemented = __ocs_fmt_0__: Henüz uygulanmadı.
|
.cmd-not-yet-implemented = __ocs_fmt_0__: Henüz uygulanmadı.
|
||||||
.viewport-s-in-layout = "__ocs_fmt_1__" yerleşiminde __ocs_fmt_0__ görünüm alanı:
|
.viewport-s-in-layout = "__ocs_fmt_1__" yerleşiminde __ocs_fmt_0__ görünüm alanı:
|
||||||
|
|
|
||||||
|
|
@ -2318,6 +2318,7 @@ view =
|
||||||
.zoom-extents =
|
.zoom-extents =
|
||||||
缩放
|
缩放
|
||||||
范围
|
范围
|
||||||
|
.cmd-no-hatch-objects = __ocs_fmt_0__:µ▓íµ£ëÕø¥µíêÕí½ÕààÕ»╣Þ▒íÒÇé
|
||||||
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__:没有文本或维度对象。
|
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__:没有文本或维度对象。
|
||||||
.cmd-not-yet-implemented = __ocs_fmt_0__:尚未执行。
|
.cmd-not-yet-implemented = __ocs_fmt_0__:尚未执行。
|
||||||
.viewport-s-in-layout = 布局“__ocs_fmt_1__”中的视口(__ocs_fmt_0__ 个):
|
.viewport-s-in-layout = 布局“__ocs_fmt_1__”中的视口(__ocs_fmt_0__ 个):
|
||||||
|
|
|
||||||
|
|
@ -2330,6 +2330,7 @@ view =
|
||||||
.zoom-extents =
|
.zoom-extents =
|
||||||
縮放
|
縮放
|
||||||
範圍
|
範圍
|
||||||
|
.cmd-no-hatch-objects = __ocs_fmt_0__:µ▓Ƶ£ëÕí½ÕààþÀÜþë®õ╗ÂÒÇé
|
||||||
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__:沒有文字或維度物件。
|
.cmd-no-text-or-dimension-objects = __ocs_fmt_0__:沒有文字或維度物件。
|
||||||
.cmd-not-yet-implemented = __ocs_fmt_0__:尚未實作。
|
.cmd-not-yet-implemented = __ocs_fmt_0__:尚未實作。
|
||||||
.viewport-s-in-layout = 配置“__ocs_fmt_1__”中的視口(__ocs_fmt_0__ 個):
|
.viewport-s-in-layout = 配置“__ocs_fmt_1__”中的視口(__ocs_fmt_0__ 個):
|
||||||
|
|
|
||||||
|
|
@ -73,8 +73,9 @@ const WEB_ALIAS_KEY: &str = "opencadstudio.aliases";
|
||||||
/// Version of the shipped default alias table. Bump this whenever the embedded
|
/// Version of the shipped default alias table. Bump this whenever the embedded
|
||||||
/// defaults add aliases or change a default target, so existing profiles can be
|
/// defaults add aliases or change a default target, so existing profiles can be
|
||||||
/// migrated forward (see `introduced_at` / `migrate_aliases`). Version 1 shipped
|
/// migrated forward (see `introduced_at` / `migrate_aliases`). Version 1 shipped
|
||||||
/// the pre-REDRAW table; version 2 introduced the REDRAW-family aliases.
|
/// the pre-REDRAW table; version 2 introduced the REDRAW-family aliases; version 3
|
||||||
const DEFAULT_ALIASES_VERSION: u32 = 2;
|
/// introduced HB (HATCHTOBACK).
|
||||||
|
const DEFAULT_ALIASES_VERSION: u32 = 3;
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
const WEB_ALIAS_VERSION_KEY: &str = "opencadstudio.aliases.version";
|
const WEB_ALIAS_VERSION_KEY: &str = "opencadstudio.aliases.version";
|
||||||
|
|
@ -91,6 +92,7 @@ fn introduced_at(version: u32) -> &'static [(&'static str, &'static str)] {
|
||||||
("RE", "REGEN"),
|
("RE", "REGEN"),
|
||||||
("REA", "REGENALL"),
|
("REA", "REGENALL"),
|
||||||
],
|
],
|
||||||
|
3 => &[("HB", "HATCHTOBACK")],
|
||||||
_ => &[],
|
_ => &[],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -330,17 +332,18 @@ mod tests {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An existing profile that predates the REDRAW aliases gains exactly the
|
/// An existing profile that predates the REDRAW and HB aliases gains exactly the
|
||||||
/// new defaults, with every pre-existing mapping kept.
|
/// new defaults, with every pre-existing mapping kept.
|
||||||
#[test]
|
#[test]
|
||||||
fn migration_adds_new_defaults_preserving_existing() {
|
fn migration_adds_new_defaults_preserving_existing() {
|
||||||
let mut stored = map(&[("L", "LINE"), ("Z", "ZOOM"), ("HI", "HIDE")]);
|
let mut stored = map(&[("L", "LINE"), ("Z", "ZOOM"), ("HI", "HIDE")]);
|
||||||
let changed = migrate_aliases(&mut stored, 0);
|
let changed = migrate_aliases(&mut stored, 0);
|
||||||
assert!(changed, "migrating from v0 to v2 must add defaults");
|
assert!(changed, "migrating from v0 to v3 must add defaults");
|
||||||
assert_eq!(stored.get("R").map(String::as_str), Some("REDRAW"));
|
assert_eq!(stored.get("R").map(String::as_str), Some("REDRAW"));
|
||||||
assert_eq!(stored.get("RA").map(String::as_str), Some("REDRAWALL"));
|
assert_eq!(stored.get("RA").map(String::as_str), Some("REDRAWALL"));
|
||||||
assert_eq!(stored.get("RE").map(String::as_str), Some("REGEN"));
|
assert_eq!(stored.get("RE").map(String::as_str), Some("REGEN"));
|
||||||
assert_eq!(stored.get("REA").map(String::as_str), Some("REGENALL"));
|
assert_eq!(stored.get("REA").map(String::as_str), Some("REGENALL"));
|
||||||
|
assert_eq!(stored.get("HB").map(String::as_str), Some("HATCHTOBACK"));
|
||||||
// Existing entries must survive untouched.
|
// Existing entries must survive untouched.
|
||||||
assert_eq!(stored.get("L").map(String::as_str), Some("LINE"));
|
assert_eq!(stored.get("L").map(String::as_str), Some("LINE"));
|
||||||
assert_eq!(stored.get("Z").map(String::as_str), Some("ZOOM"));
|
assert_eq!(stored.get("Z").map(String::as_str), Some("ZOOM"));
|
||||||
|
|
@ -375,10 +378,10 @@ mod tests {
|
||||||
assert_eq!(stored.get("L").map(String::as_str), Some("LINE"));
|
assert_eq!(stored.get("L").map(String::as_str), Some("LINE"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Migrating a mid-range profile (one that saw v1) only pulls in v2's
|
/// Migrating a mid-range profile (one that saw v1) pulls in v2's and v3's
|
||||||
/// additions, not hypothetical earlier ones.
|
/// additions, not hypothetical earlier ones.
|
||||||
#[test]
|
#[test]
|
||||||
fn migration_from_version_one_adds_v2_aliases() {
|
fn migration_from_version_one_adds_v2_and_v3_aliases() {
|
||||||
let mut stored = map(&[("L", "LINE")]);
|
let mut stored = map(&[("L", "LINE")]);
|
||||||
let changed = migrate_aliases(&mut stored, 1);
|
let changed = migrate_aliases(&mut stored, 1);
|
||||||
assert!(changed);
|
assert!(changed);
|
||||||
|
|
@ -386,5 +389,16 @@ mod tests {
|
||||||
assert_eq!(stored.get("RA").map(String::as_str), Some("REDRAWALL"));
|
assert_eq!(stored.get("RA").map(String::as_str), Some("REDRAWALL"));
|
||||||
assert_eq!(stored.get("RE").map(String::as_str), Some("REGEN"));
|
assert_eq!(stored.get("RE").map(String::as_str), Some("REGEN"));
|
||||||
assert_eq!(stored.get("REA").map(String::as_str), Some("REGENALL"));
|
assert_eq!(stored.get("REA").map(String::as_str), Some("REGENALL"));
|
||||||
|
assert_eq!(stored.get("HB").map(String::as_str), Some("HATCHTOBACK"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Migrating a profile that saw v2 pulls in v3's HB addition only.
|
||||||
|
#[test]
|
||||||
|
fn migration_from_version_two_adds_v3_aliases() {
|
||||||
|
let mut stored = map(&[("L", "LINE")]);
|
||||||
|
let changed = migrate_aliases(&mut stored, 2);
|
||||||
|
assert!(changed);
|
||||||
|
assert_eq!(stored.get("R"), None);
|
||||||
|
assert_eq!(stored.get("HB").map(String::as_str), Some("HATCHTOBACK"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ mod view;
|
||||||
// `DrawOrderRefCommand` lives in the `view` family file but is referenced by
|
// `DrawOrderRefCommand` lives in the `view` family file but is referenced by
|
||||||
// path (`commands::DrawOrderRefCommand`) from `update.rs`, so re-export it at
|
// path (`commands::DrawOrderRefCommand`) from `update.rs`, so re-export it at
|
||||||
// the module root to keep that path valid.
|
// the module root to keep that path valid.
|
||||||
pub(crate) use view::DrawOrderRefCommand;
|
pub(crate) use view::DrawOrderCommand;
|
||||||
|
|
||||||
impl OpenCADStudio {
|
impl OpenCADStudio {
|
||||||
/// First `"{prefix}{n}"` (n ≥ 1) not already used by a block record in the
|
/// First `"{prefix}{n}"` (n ≥ 1) not already used by a block record in the
|
||||||
|
|
@ -397,9 +397,11 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||||
"CUILOAD",
|
"CUILOAD",
|
||||||
// Save every open drawing.
|
// Save every open drawing.
|
||||||
"SAVEALL",
|
"SAVEALL",
|
||||||
// Draw-order: all text/dims to front or back.
|
// Draw-order: all text/dims to front or back; hatches to back.
|
||||||
"TEXTTOFRONT",
|
"TEXTTOFRONT",
|
||||||
"TEXTTOBACK",
|
"TEXTTOBACK",
|
||||||
|
"HATCHTOBACK",
|
||||||
|
"HB",
|
||||||
// Criteria-based selection (same as QSELECT) + copy with picked base.
|
// Criteria-based selection (same as QSELECT) + copy with picked base.
|
||||||
"FILTER",
|
"FILTER",
|
||||||
"FI",
|
"FI",
|
||||||
|
|
|
||||||
|
|
@ -772,12 +772,15 @@ impl OpenCADStudio {
|
||||||
// the front (or back) of the draw order via the DRAWORDER machinery.
|
// the front (or back) of the draw order via the DRAWORDER machinery.
|
||||||
"TEXTTOFRONT" | "TEXTTOBACK" => {
|
"TEXTTOFRONT" | "TEXTTOBACK" => {
|
||||||
let to_front = cmd.ends_with("FRONT");
|
let to_front = cmd.ends_with("FRONT");
|
||||||
|
let block_handle = self.tabs[i].scene.current_layout_block_handle_pub();
|
||||||
let handles: rustc_hash::FxHashSet<acadrust::Handle> = self.tabs[i]
|
let handles: rustc_hash::FxHashSet<acadrust::Handle> = self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
.document
|
.document
|
||||||
.entities()
|
.entities()
|
||||||
.filter(|e| {
|
.filter(|e| {
|
||||||
matches!(
|
let c = e.common();
|
||||||
|
(c.owner_handle == block_handle || c.owner_handle.is_null())
|
||||||
|
&& matches!(
|
||||||
e,
|
e,
|
||||||
acadrust::EntityType::Text(_)
|
acadrust::EntityType::Text(_)
|
||||||
| acadrust::EntityType::MText(_)
|
| acadrust::EntityType::MText(_)
|
||||||
|
|
@ -802,15 +805,129 @@ impl OpenCADStudio {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
"DRAWORDER" => {
|
// HATCHTOBACK ÔÇö move every hatch object in the active space to the back of the draw order.
|
||||||
use crate::command::SelectThenKeywordCommand;
|
"HATCHTOBACK" => {
|
||||||
let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
|
use acadrust::objects::{ObjectType, SortEntitiesTable};
|
||||||
let c = SelectThenKeywordCommand::new(
|
let block_handle = self.tabs[i].scene.current_layout_block_handle_pub();
|
||||||
"DRAWORDER",
|
let doc_ref = &self.tabs[i].scene.document;
|
||||||
"DRAWORDER [Front / Back] (Above/Under <handle> by typing):",
|
|
||||||
vec![("Front", "FRONT", None), ("Back", "BACK", None)],
|
// 1. Single scan over objects to find existing SortEntitiesTable handle & overrides.
|
||||||
has_sel,
|
let mut existing_table_handle = None;
|
||||||
|
let mut overrides: Option<rustc_hash::FxHashMap<u64, u64>> = None;
|
||||||
|
for (h, obj) in &doc_ref.objects {
|
||||||
|
if let ObjectType::SortEntitiesTable(t) = obj {
|
||||||
|
if t.block_owner_handle == block_handle {
|
||||||
|
existing_table_handle = Some(*h);
|
||||||
|
if !t.is_empty() {
|
||||||
|
overrides = Some(
|
||||||
|
t.entries()
|
||||||
|
.map(|e| (e.entity_handle.value(), e.sort_handle.value()))
|
||||||
|
.collect(),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Pre-filter locked layers with zero string allocation.
|
||||||
|
let locked_layers: rustc_hash::FxHashSet<&str> = doc_ref
|
||||||
|
.layers
|
||||||
|
.iter()
|
||||||
|
.filter(|l| l.is_locked())
|
||||||
|
.map(|l| l.name.as_str())
|
||||||
|
.collect();
|
||||||
|
let has_locked_layers = !locked_layers.is_empty();
|
||||||
|
|
||||||
|
// 3. Single pass over entities in the active space.
|
||||||
|
let mut hatches_to_move: Vec<acadrust::Handle> = Vec::new();
|
||||||
|
let mut min_eff = u64::MAX;
|
||||||
|
|
||||||
|
for e in doc_ref.entities() {
|
||||||
|
let c = e.common();
|
||||||
|
if c.owner_handle != block_handle && !c.owner_handle.is_null() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let hv = c.handle.value();
|
||||||
|
if matches!(e, acadrust::EntityType::Hatch(_)) {
|
||||||
|
if !has_locked_layers || !locked_layers.contains(c.layer.as_str()) {
|
||||||
|
hatches_to_move.push(c.handle);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let eff = match &overrides {
|
||||||
|
Some(map) => map.get(&hv).copied().unwrap_or(hv),
|
||||||
|
None => hv,
|
||||||
|
};
|
||||||
|
min_eff = min_eff.min(eff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if hatches_to_move.is_empty() {
|
||||||
|
self.command_line
|
||||||
|
.push_info(crate::tf!("{cmd}: no hatch objects.").as_ref());
|
||||||
|
return Some(Task::none());
|
||||||
|
}
|
||||||
|
|
||||||
|
if min_eff == u64::MAX {
|
||||||
|
min_eff = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Ultra-fast targeted Delta Undo (snapshots ONLY the SortEntitiesTable, zero full-drawing clone).
|
||||||
|
let pending_delta = self.begin_undo(i, "DRAWORDER", hatches_to_move.len(), true);
|
||||||
|
|
||||||
|
// 5. Update or insert SortEntitiesTable directly.
|
||||||
|
let table_before = existing_table_handle
|
||||||
|
.and_then(|h| self.tabs[i].scene.document.objects.get(&h).cloned());
|
||||||
|
let th = existing_table_handle.unwrap_or_else(|| {
|
||||||
|
let nh = acadrust::Handle::new(self.tabs[i].scene.document.next_handle());
|
||||||
|
let mut table = SortEntitiesTable::for_block(block_handle);
|
||||||
|
table.handle = nh;
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.objects
|
||||||
|
.insert(nh, ObjectType::SortEntitiesTable(table));
|
||||||
|
nh
|
||||||
|
});
|
||||||
|
self.tabs[i].scene.record_undo_object_before(th, table_before);
|
||||||
|
|
||||||
|
if let Some(ObjectType::SortEntitiesTable(table)) =
|
||||||
|
self.tabs[i].scene.document.objects.get_mut(&th)
|
||||||
|
{
|
||||||
|
for (k, h) in hatches_to_move.iter().enumerate() {
|
||||||
|
let sort = min_eff.saturating_sub(1 + k as u64).max(1);
|
||||||
|
table.add_entry(*h, acadrust::Handle::new(sort));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(pending) = pending_delta {
|
||||||
|
self.commit_undo_delta(i, pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Invalidate ONLY draw-depth cache without dropping whole-drawing tessellations or spatial indexes.
|
||||||
|
let changes: Vec<(acadrust::Handle, crate::scene::ChangeKind)> = hatches_to_move
|
||||||
|
.iter()
|
||||||
|
.map(|h| (*h, crate::scene::ChangeKind::Modified))
|
||||||
|
.collect();
|
||||||
|
self.tabs[i].scene.bump_entities(&changes);
|
||||||
|
self.tabs[i].scene.invalidate_draw_depth();
|
||||||
|
|
||||||
|
self.tabs[i].dirty = true;
|
||||||
|
self.command_line.push_info(
|
||||||
|
crate::tf!("DRAWORDER: moved {} entities to back.", hatches_to_move.len()).as_ref(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Some(Task::none());
|
||||||
|
}
|
||||||
|
|
||||||
|
"DRAWORDER" => {
|
||||||
|
let selected: Vec<acadrust::Handle> = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.selected_entities()
|
||||||
|
.iter()
|
||||||
|
.map(|(h, _)| *h)
|
||||||
|
.collect();
|
||||||
|
let c = DrawOrderCommand::new(selected);
|
||||||
self.command_line.push_info(&c.prompt());
|
self.command_line.push_info(&c.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(c));
|
self.tabs[i].active_cmd = Some(Box::new(c));
|
||||||
}
|
}
|
||||||
|
|
@ -1083,38 +1200,159 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Draw Order: interactive reference-object pick ──────────────────────────
|
// ── Draw Order: interactive command ──────────────────────────
|
||||||
|
|
||||||
/// Moves a captured selection above or below a reference object the user
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
/// picks in the viewport. On pick it relaunches `DRAWORDER A|U <handle>`
|
enum DrawOrderStep {
|
||||||
/// with the captured handles reinstalled as the selection, so the existing
|
SelectObjects,
|
||||||
/// command path performs the actual reorder.
|
ChooseVerb,
|
||||||
pub(crate) struct DrawOrderRefCommand {
|
PickReference { above: bool },
|
||||||
to_move: Vec<acadrust::Handle>,
|
|
||||||
above: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DrawOrderRefCommand {
|
/// Interactive front-end for the DRAWORDER command.
|
||||||
pub(crate) fn new(to_move: Vec<acadrust::Handle>, above: bool) -> Self {
|
///
|
||||||
Self { to_move, above }
|
/// Flow:
|
||||||
|
/// 1. If nothing is selected, prompt to select objects (Enter confirms).
|
||||||
|
/// 2. Prompt for verb: `[Above / Under / Front / Back]`.
|
||||||
|
/// - `Front` / `F`: moves selection to front.
|
||||||
|
/// - `Back` / `B` / Enter: moves selection to back.
|
||||||
|
/// - `Above` / `A` / `Under` / `U`: advances to reference object pick.
|
||||||
|
/// 3. Reference object pick: user can click the reference entity in the viewport
|
||||||
|
/// or type its hex handle on the command line.
|
||||||
|
pub(crate) struct DrawOrderCommand {
|
||||||
|
selected: Vec<acadrust::Handle>,
|
||||||
|
step: DrawOrderStep,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DrawOrderCommand {
|
||||||
|
pub(crate) fn new(selected: Vec<acadrust::Handle>) -> Self {
|
||||||
|
let step = if selected.is_empty() {
|
||||||
|
DrawOrderStep::SelectObjects
|
||||||
|
} else {
|
||||||
|
DrawOrderStep::ChooseVerb
|
||||||
|
};
|
||||||
|
Self { selected, step }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn for_reference_pick(selected: Vec<acadrust::Handle>, above: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
selected,
|
||||||
|
step: DrawOrderStep::PickReference { above },
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CadCommand for DrawOrderRefCommand {
|
impl CadCommand for DrawOrderCommand {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
"DRAWORDER"
|
"DRAWORDER"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt(&self) -> String {
|
fn prompt(&self) -> String {
|
||||||
if self.above {
|
match self.step {
|
||||||
|
DrawOrderStep::SelectObjects => {
|
||||||
|
crate::t!("DRAWORDER select objects, then press Enter:").into_owned()
|
||||||
|
}
|
||||||
|
DrawOrderStep::ChooseVerb => {
|
||||||
|
crate::t!("DRAWORDER [Above / Under / Front / Back] <Back>:").into_owned()
|
||||||
|
}
|
||||||
|
DrawOrderStep::PickReference { above: true } => {
|
||||||
crate::t!("DRAWORDER Select reference object (move selection above):").into_owned()
|
crate::t!("DRAWORDER Select reference object (move selection above):").into_owned()
|
||||||
} else {
|
}
|
||||||
|
DrawOrderStep::PickReference { above: false } => {
|
||||||
crate::t!("DRAWORDER Select reference object (move selection under):").into_owned()
|
crate::t!("DRAWORDER Select reference object (move selection under):").into_owned()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn options(&self) -> Vec<crate::command::CmdOption> {
|
||||||
|
match self.step {
|
||||||
|
DrawOrderStep::ChooseVerb => vec![
|
||||||
|
crate::command::CmdOption::new("Above", "ABOVE"),
|
||||||
|
crate::command::CmdOption::new("Under", "UNDER"),
|
||||||
|
crate::command::CmdOption::new("Front", "FRONT"),
|
||||||
|
crate::command::CmdOption::new("Back", "BACK"),
|
||||||
|
],
|
||||||
|
_ => Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wants_text_input(&self) -> bool {
|
||||||
|
!matches!(self.step, DrawOrderStep::SelectObjects)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_selection_gathering(&self) -> bool {
|
||||||
|
matches!(self.step, DrawOrderStep::SelectObjects)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_selection_complete(&mut self, handles: Vec<acadrust::Handle>) -> crate::command::CmdResult {
|
||||||
|
self.selected = handles;
|
||||||
|
crate::command::CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_enter(&mut self) -> crate::command::CmdResult {
|
||||||
|
match self.step {
|
||||||
|
DrawOrderStep::SelectObjects => {
|
||||||
|
if self.selected.is_empty() {
|
||||||
|
crate::command::CmdResult::Cancel
|
||||||
|
} else {
|
||||||
|
self.step = DrawOrderStep::ChooseVerb;
|
||||||
|
crate::command::CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DrawOrderStep::ChooseVerb => {
|
||||||
|
// Bare Enter defaults to Back
|
||||||
|
let handles = std::mem::take(&mut self.selected);
|
||||||
|
crate::command::CmdResult::Relaunch("DRAWORDER BACK".into(), handles)
|
||||||
|
}
|
||||||
|
DrawOrderStep::PickReference { .. } => crate::command::CmdResult::Cancel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_text_input(&mut self, text: &str) -> Option<crate::command::CmdResult> {
|
||||||
|
let t = text.trim();
|
||||||
|
if t.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
match self.step {
|
||||||
|
DrawOrderStep::SelectObjects => None,
|
||||||
|
DrawOrderStep::ChooseVerb => {
|
||||||
|
let up = t.to_uppercase();
|
||||||
|
match up.as_str() {
|
||||||
|
"F" | "FRONT" => {
|
||||||
|
let handles = std::mem::take(&mut self.selected);
|
||||||
|
Some(crate::command::CmdResult::Relaunch("DRAWORDER FRONT".into(), handles))
|
||||||
|
}
|
||||||
|
"B" | "BACK" => {
|
||||||
|
let handles = std::mem::take(&mut self.selected);
|
||||||
|
Some(crate::command::CmdResult::Relaunch("DRAWORDER BACK".into(), handles))
|
||||||
|
}
|
||||||
|
"A" | "ABOVE" => {
|
||||||
|
self.step = DrawOrderStep::PickReference { above: true };
|
||||||
|
Some(crate::command::CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
"U" | "UNDER" | "BELOW" => {
|
||||||
|
self.step = DrawOrderStep::PickReference { above: false };
|
||||||
|
Some(crate::command::CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
_ => Some(crate::command::CmdResult::NeedPoint),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DrawOrderStep::PickReference { above } => {
|
||||||
|
let hex_str = t.trim_start_matches("0x").trim_start_matches("0X");
|
||||||
|
if let Ok(val) = u64::from_str_radix(hex_str, 16) {
|
||||||
|
let opt = if above { "A" } else { "U" };
|
||||||
|
let cmd = format!("DRAWORDER {} {:x}", opt, val);
|
||||||
|
let handles = std::mem::take(&mut self.selected);
|
||||||
|
Some(crate::command::CmdResult::Relaunch(cmd, handles))
|
||||||
|
} else {
|
||||||
|
Some(crate::command::CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn needs_entity_pick(&self) -> bool {
|
fn needs_entity_pick(&self) -> bool {
|
||||||
true
|
matches!(self.step, DrawOrderStep::PickReference { .. })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_entity_pick(
|
fn on_entity_pick(
|
||||||
|
|
@ -1125,16 +1363,606 @@ impl CadCommand for DrawOrderRefCommand {
|
||||||
if handle.is_null() {
|
if handle.is_null() {
|
||||||
return crate::command::CmdResult::NeedPoint;
|
return crate::command::CmdResult::NeedPoint;
|
||||||
}
|
}
|
||||||
let opt = if self.above { "A" } else { "U" };
|
if let DrawOrderStep::PickReference { above } = self.step {
|
||||||
|
let opt = if above { "A" } else { "U" };
|
||||||
let cmd = format!("DRAWORDER {} {:x}", opt, handle.value());
|
let cmd = format!("DRAWORDER {} {:x}", opt, handle.value());
|
||||||
crate::command::CmdResult::Relaunch(cmd, std::mem::take(&mut self.to_move))
|
let handles = std::mem::take(&mut self.selected);
|
||||||
|
crate::command::CmdResult::Relaunch(cmd, handles)
|
||||||
|
} else {
|
||||||
|
crate::command::CmdResult::NeedPoint
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_point(&mut self, _pt: glam::DVec3) -> crate::command::CmdResult {
|
fn on_point(&mut self, _pt: glam::DVec3) -> crate::command::CmdResult {
|
||||||
crate::command::CmdResult::NeedPoint
|
crate::command::CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self) -> crate::command::CmdResult {
|
#[cfg(test)]
|
||||||
crate::command::CmdResult::Cancel
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::app::OpenCADStudio;
|
||||||
|
use acadrust::objects::ObjectType;
|
||||||
|
use acadrust::EntityType;
|
||||||
|
|
||||||
|
fn fresh_app() -> OpenCADStudio {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
app
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hatchtoback_no_hatches_warns_and_noop() {
|
||||||
|
let mut app = fresh_app();
|
||||||
|
let _ = app.run_command_line("HATCHTOBACK");
|
||||||
|
let i = app.active_tab;
|
||||||
|
let block_handle = app.tabs[i].scene.current_layout_block_handle_pub();
|
||||||
|
let table_exists = app.tabs[i].scene.document.objects.values().any(|obj| {
|
||||||
|
if let ObjectType::SortEntitiesTable(t) = obj {
|
||||||
|
t.block_owner_handle == block_handle
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assert!(!table_exists, "No SortEntitiesTable created when no hatches exist");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hatchtoback_moves_hatches_to_back() {
|
||||||
|
let mut app = fresh_app();
|
||||||
|
let i = app.active_tab;
|
||||||
|
let h_line1 = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
|
||||||
|
let h_hatch = app.tabs[i].scene.add_entity_clone(EntityType::Hatch(Default::default()));
|
||||||
|
let h_line2 = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
|
||||||
|
|
||||||
|
let _ = app.run_command_line("HATCHTOBACK");
|
||||||
|
|
||||||
|
let block_handle = app.tabs[i].scene.current_layout_block_handle_pub();
|
||||||
|
let table = app.tabs[i].scene.document.objects.values().find_map(|obj| {
|
||||||
|
if let ObjectType::SortEntitiesTable(t) = obj {
|
||||||
|
if t.block_owner_handle == block_handle {
|
||||||
|
return Some(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}).expect("SortEntitiesTable should exist for the active layout block");
|
||||||
|
|
||||||
|
let entries: rustc_hash::FxHashMap<u64, u64> = table
|
||||||
|
.entries()
|
||||||
|
.map(|e| (e.entity_handle.value(), e.sort_handle.value()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let hatch_sort = entries.get(&h_hatch.value()).copied().unwrap_or(h_hatch.value());
|
||||||
|
let line1_sort = entries.get(&h_line1.value()).copied().unwrap_or(h_line1.value());
|
||||||
|
let line2_sort = entries.get(&h_line2.value()).copied().unwrap_or(h_line2.value());
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
hatch_sort < line1_sort,
|
||||||
|
"Hatch sort handle ({hatch_sort}) must be behind line 1 ({line1_sort})"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
hatch_sort < line2_sort,
|
||||||
|
"Hatch sort handle ({hatch_sort}) must be behind line 2 ({line2_sort})"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hb_alias_moves_hatches_to_back() {
|
||||||
|
let mut app = fresh_app();
|
||||||
|
let i = app.active_tab;
|
||||||
|
let h_line = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
|
||||||
|
let h_hatch = app.tabs[i].scene.add_entity_clone(EntityType::Hatch(Default::default()));
|
||||||
|
|
||||||
|
let _ = app.run_command_line("HB");
|
||||||
|
|
||||||
|
let block_handle = app.tabs[i].scene.current_layout_block_handle_pub();
|
||||||
|
let table = app.tabs[i].scene.document.objects.values().find_map(|obj| {
|
||||||
|
if let ObjectType::SortEntitiesTable(t) = obj {
|
||||||
|
if t.block_owner_handle == block_handle {
|
||||||
|
return Some(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}).expect("SortEntitiesTable should exist for the active layout block");
|
||||||
|
|
||||||
|
let entries: rustc_hash::FxHashMap<u64, u64> = table
|
||||||
|
.entries()
|
||||||
|
.map(|e| (e.entity_handle.value(), e.sort_handle.value()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let hatch_sort = entries.get(&h_hatch.value()).copied().unwrap_or(h_hatch.value());
|
||||||
|
let line_sort = entries.get(&h_line.value()).copied().unwrap_or(h_line.value());
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
hatch_sort < line_sort,
|
||||||
|
"Hatch sort handle ({hatch_sort}) must be behind line ({line_sort}) via HB alias"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hatchtoback_ignores_hatches_in_other_spaces() {
|
||||||
|
let mut app = fresh_app();
|
||||||
|
let i = app.active_tab;
|
||||||
|
let other_block = acadrust::Handle::new(0x9999);
|
||||||
|
let h_foreign = app.tabs[i].scene.add_entity_clone(EntityType::Hatch(Default::default()));
|
||||||
|
if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(h_foreign) {
|
||||||
|
entity.common_mut().owner_handle = other_block;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When no hatches exist in the current layout, HATCHTOBACK should no-op.
|
||||||
|
let _ = app.run_command_line("HATCHTOBACK");
|
||||||
|
|
||||||
|
let block_handle = app.tabs[i].scene.current_layout_block_handle_pub();
|
||||||
|
let table_exists = app.tabs[i].scene.document.objects.values().any(|obj| {
|
||||||
|
if let ObjectType::SortEntitiesTable(t) = obj {
|
||||||
|
t.block_owner_handle == block_handle
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assert!(!table_exists, "Foreign hatch must not trigger table creation in active layout");
|
||||||
|
|
||||||
|
// Now add a line and a hatch in the active layout.
|
||||||
|
let h_line = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
|
||||||
|
let h_hatch = app.tabs[i].scene.add_entity_clone(EntityType::Hatch(Default::default()));
|
||||||
|
|
||||||
|
let _ = app.run_command_line("HATCHTOBACK");
|
||||||
|
|
||||||
|
let table = app.tabs[i].scene.document.objects.values().find_map(|obj| {
|
||||||
|
if let ObjectType::SortEntitiesTable(t) = obj {
|
||||||
|
if t.block_owner_handle == block_handle {
|
||||||
|
return Some(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}).expect("SortEntitiesTable should exist for current layout");
|
||||||
|
|
||||||
|
let entries: rustc_hash::FxHashMap<u64, u64> = table
|
||||||
|
.entries()
|
||||||
|
.map(|e| (e.entity_handle.value(), e.sort_handle.value()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
assert!(!entries.contains_key(&h_foreign.value()), "Foreign hatch must not be in active layout table");
|
||||||
|
assert!(entries.contains_key(&h_hatch.value()), "Active layout hatch must be in table");
|
||||||
|
let hatch_sort = entries[&h_hatch.value()];
|
||||||
|
let line_sort = entries.get(&h_line.value()).copied().unwrap_or(h_line.value());
|
||||||
|
assert!(hatch_sort < line_sort);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hatchtoback_multiple_calls_are_idempotent() {
|
||||||
|
let mut app = fresh_app();
|
||||||
|
let i = app.active_tab;
|
||||||
|
let h_line = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
|
||||||
|
let h_hatch1 = app.tabs[i].scene.add_entity_clone(EntityType::Hatch(Default::default()));
|
||||||
|
let h_hatch2 = app.tabs[i].scene.add_entity_clone(EntityType::Hatch(Default::default()));
|
||||||
|
|
||||||
|
let _ = app.run_command_line("HATCHTOBACK");
|
||||||
|
let _ = app.run_command_line("HATCHTOBACK");
|
||||||
|
|
||||||
|
let block_handle = app.tabs[i].scene.current_layout_block_handle_pub();
|
||||||
|
let table = app.tabs[i].scene.document.objects.values().find_map(|obj| {
|
||||||
|
if let ObjectType::SortEntitiesTable(t) = obj {
|
||||||
|
if t.block_owner_handle == block_handle {
|
||||||
|
return Some(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}).expect("SortEntitiesTable should exist");
|
||||||
|
|
||||||
|
let entries: rustc_hash::FxHashMap<u64, u64> = table
|
||||||
|
.entries()
|
||||||
|
.map(|e| (e.entity_handle.value(), e.sort_handle.value()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let h1_sort = entries.get(&h_hatch1.value()).copied().unwrap_or(h_hatch1.value());
|
||||||
|
let h2_sort = entries.get(&h_hatch2.value()).copied().unwrap_or(h_hatch2.value());
|
||||||
|
let line_sort = entries.get(&h_line.value()).copied().unwrap_or(h_line.value());
|
||||||
|
|
||||||
|
assert!(h1_sort < line_sort);
|
||||||
|
assert!(h2_sort < line_sort);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hatchtoback_preserves_active_selection() {
|
||||||
|
let mut app = fresh_app();
|
||||||
|
let i = app.active_tab;
|
||||||
|
let h_line = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
|
||||||
|
let _h_hatch = app.tabs[i].scene.add_entity_clone(EntityType::Hatch(Default::default()));
|
||||||
|
|
||||||
|
// User had the line selected before running HATCHTOBACK.
|
||||||
|
app.tabs[i].scene.replace_selection(std::iter::once(h_line).collect());
|
||||||
|
|
||||||
|
let _ = app.run_command_line("HATCHTOBACK");
|
||||||
|
|
||||||
|
let selected = app.tabs[i].scene.selected_handles_in_order();
|
||||||
|
assert_eq!(selected, vec![h_line], "HATCHTOBACK must not overwrite existing user selection");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hatchtoback_skips_hatches_on_locked_layers() {
|
||||||
|
let mut app = fresh_app();
|
||||||
|
let i = app.active_tab;
|
||||||
|
|
||||||
|
// Lock layer "LOCKED_LAYER".
|
||||||
|
app.tabs[i].scene.ensure_layer("LOCKED_LAYER");
|
||||||
|
if let Some(layer) = app.tabs[i].scene.document.layers.get_mut("LOCKED_LAYER") {
|
||||||
|
layer.flags.locked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut locked_hatch = acadrust::entities::Hatch::default();
|
||||||
|
locked_hatch.common.layer = "LOCKED_LAYER".into();
|
||||||
|
let h_locked_hatch = app.tabs[i].scene.add_entity_clone(EntityType::Hatch(locked_hatch));
|
||||||
|
|
||||||
|
let h_line = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
|
||||||
|
|
||||||
|
// Run HATCHTOBACK when only a locked hatch exists: it should not move the locked hatch.
|
||||||
|
let _ = app.run_command_line("HATCHTOBACK");
|
||||||
|
|
||||||
|
let block_handle = app.tabs[i].scene.current_layout_block_handle_pub();
|
||||||
|
let table_exists = app.tabs[i].scene.document.objects.values().any(|obj| {
|
||||||
|
if let ObjectType::SortEntitiesTable(t) = obj {
|
||||||
|
t.block_owner_handle == block_handle
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assert!(!table_exists, "Locked hatch must not trigger table creation");
|
||||||
|
|
||||||
|
// Now add an unlocked hatch.
|
||||||
|
let h_unlocked_hatch = app.tabs[i].scene.add_entity_clone(EntityType::Hatch(Default::default()));
|
||||||
|
|
||||||
|
let _ = app.run_command_line("HATCHTOBACK");
|
||||||
|
|
||||||
|
let table = app.tabs[i].scene.document.objects.values().find_map(|obj| {
|
||||||
|
if let ObjectType::SortEntitiesTable(t) = obj {
|
||||||
|
if t.block_owner_handle == block_handle {
|
||||||
|
return Some(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}).expect("SortEntitiesTable should exist for unlocked hatch");
|
||||||
|
|
||||||
|
let entries: rustc_hash::FxHashMap<u64, u64> = table
|
||||||
|
.entries()
|
||||||
|
.map(|e| (e.entity_handle.value(), e.sort_handle.value()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
assert!(!entries.contains_key(&h_locked_hatch.value()), "Locked hatch must not be reordered");
|
||||||
|
assert!(entries.contains_key(&h_unlocked_hatch.value()), "Unlocked hatch must be reordered");
|
||||||
|
let hatch_sort = entries[&h_unlocked_hatch.value()];
|
||||||
|
let line_sort = entries.get(&h_line.value()).copied().unwrap_or(h_line.value());
|
||||||
|
assert!(hatch_sort < line_sort);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hatchtoback_undo_restores_draw_order() {
|
||||||
|
let mut app = fresh_app();
|
||||||
|
let i = app.active_tab;
|
||||||
|
let _h_line = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
|
||||||
|
let h_hatch = app.tabs[i].scene.add_entity_clone(EntityType::Hatch(Default::default()));
|
||||||
|
|
||||||
|
let _ = app.run_command_line("HATCHTOBACK");
|
||||||
|
|
||||||
|
let block_handle = app.tabs[i].scene.current_layout_block_handle_pub();
|
||||||
|
let table = app.tabs[i].scene.document.objects.values().find_map(|obj| {
|
||||||
|
if let ObjectType::SortEntitiesTable(t) = obj {
|
||||||
|
if t.block_owner_handle == block_handle {
|
||||||
|
return Some(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}).expect("SortEntitiesTable should exist");
|
||||||
|
assert!(table.entries().any(|e| e.entity_handle == h_hatch));
|
||||||
|
|
||||||
|
// Perform UNDO.
|
||||||
|
let _ = app.update(crate::app::Message::Undo);
|
||||||
|
|
||||||
|
let table_after_undo = app.tabs[i].scene.document.objects.values().find_map(|obj| {
|
||||||
|
if let ObjectType::SortEntitiesTable(t) = obj {
|
||||||
|
if t.block_owner_handle == block_handle {
|
||||||
|
return Some(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
});
|
||||||
|
// Table was created fresh by HATCHTOBACK, so undo should remove it or leave it empty of the hatch.
|
||||||
|
let has_hatch = table_after_undo.map_or(false, |t| t.entries().any(|e| e.entity_handle == h_hatch));
|
||||||
|
assert!(!has_hatch, "Undo must revert the SortEntitiesTable entry");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn effective_sort_map(
|
||||||
|
app: &OpenCADStudio,
|
||||||
|
) -> rustc_hash::FxHashMap<u64, u64> {
|
||||||
|
let i = app.active_tab;
|
||||||
|
let block_handle = app.tabs[i].scene.current_layout_block_handle_pub();
|
||||||
|
app.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.objects
|
||||||
|
.values()
|
||||||
|
.find_map(|obj| {
|
||||||
|
if let ObjectType::SortEntitiesTable(t) = obj {
|
||||||
|
if t.block_owner_handle == block_handle {
|
||||||
|
return Some(
|
||||||
|
t.entries()
|
||||||
|
.map(|e| (e.entity_handle.value(), e.sort_handle.value()))
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regression guard for DRAWORDER BACK anchoring. Written while auditing
|
||||||
|
// the draw-order code, where BACK *looked* like it skipped the baseline
|
||||||
|
// scan and clamped every moved entity onto sort key 1 (a tie). Running
|
||||||
|
// this test disproved that — `to_front_opt` is Some(false) for BACK, so
|
||||||
|
// the baseline scan does run. Kept to lock in the strict-order behavior:
|
||||||
|
// a second BACK must land strictly below earlier assignments, never tie.
|
||||||
|
#[test]
|
||||||
|
fn draworder_back_twice_keeps_strict_order() {
|
||||||
|
let mut app = fresh_app();
|
||||||
|
let i = app.active_tab;
|
||||||
|
let h_line1 = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
|
||||||
|
let h_hatch = app.tabs[i].scene.add_entity_clone(EntityType::Hatch(Default::default()));
|
||||||
|
|
||||||
|
// Send the hatch to back, then send line 1 behind it: the second BACK
|
||||||
|
// must anchor strictly below the first assignment, never tie with it.
|
||||||
|
app.tabs[i].scene.replace_selection(std::iter::once(h_hatch).collect());
|
||||||
|
let _ = app.run_command_line("DRAWORDER BACK");
|
||||||
|
app.tabs[i].scene.replace_selection(std::iter::once(h_line1).collect());
|
||||||
|
let _ = app.run_command_line("DRAWORDER BACK");
|
||||||
|
|
||||||
|
let entries = effective_sort_map(&app);
|
||||||
|
let hatch_sort = entries.get(&h_hatch.value()).copied().unwrap_or(h_hatch.value());
|
||||||
|
let line1_sort = entries.get(&h_line1.value()).copied().unwrap_or(h_line1.value());
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
line1_sort < hatch_sort,
|
||||||
|
"second BACK must land strictly below the first ({line1_sort} vs {hatch_sort})"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn draworder_front_moves_hatch_in_front_of_all() {
|
||||||
|
let mut app = fresh_app();
|
||||||
|
let i = app.active_tab;
|
||||||
|
let h_line1 = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
|
||||||
|
let h_hatch = app.tabs[i].scene.add_entity_clone(EntityType::Hatch(Default::default()));
|
||||||
|
let h_line2 = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
|
||||||
|
|
||||||
|
app.tabs[i].scene.replace_selection(std::iter::once(h_hatch).collect());
|
||||||
|
let _ = app.run_command_line("DRAWORDER FRONT");
|
||||||
|
|
||||||
|
let entries = effective_sort_map(&app);
|
||||||
|
let hatch_sort = entries.get(&h_hatch.value()).copied().unwrap_or(h_hatch.value());
|
||||||
|
let line1_sort = entries.get(&h_line1.value()).copied().unwrap_or(h_line1.value());
|
||||||
|
let line2_sort = entries.get(&h_line2.value()).copied().unwrap_or(h_line2.value());
|
||||||
|
|
||||||
|
assert!(hatch_sort > line1_sort, "hatch ({hatch_sort}) must render in front of line 1 ({line1_sort})");
|
||||||
|
assert!(hatch_sort > line2_sort, "hatch ({hatch_sort}) must render in front of line 2 ({line2_sort})");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn draworder_back_moves_hatch_behind_all() {
|
||||||
|
let mut app = fresh_app();
|
||||||
|
let i = app.active_tab;
|
||||||
|
let h_line1 = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
|
||||||
|
let h_hatch = app.tabs[i].scene.add_entity_clone(EntityType::Hatch(Default::default()));
|
||||||
|
let h_line2 = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
|
||||||
|
|
||||||
|
app.tabs[i].scene.replace_selection(std::iter::once(h_hatch).collect());
|
||||||
|
let _ = app.run_command_line("DRAWORDER BACK");
|
||||||
|
|
||||||
|
let entries = effective_sort_map(&app);
|
||||||
|
let hatch_sort = entries.get(&h_hatch.value()).copied().unwrap_or(h_hatch.value());
|
||||||
|
let line1_sort = entries.get(&h_line1.value()).copied().unwrap_or(h_line1.value());
|
||||||
|
let line2_sort = entries.get(&h_line2.value()).copied().unwrap_or(h_line2.value());
|
||||||
|
|
||||||
|
assert!(hatch_sort < line1_sort, "hatch ({hatch_sort}) must render behind line 1 ({line1_sort})");
|
||||||
|
assert!(hatch_sort < line2_sort, "hatch ({hatch_sort}) must render behind line 2 ({line2_sort})");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn draworder_above_reference_puts_hatch_in_front_of_object() {
|
||||||
|
let mut app = fresh_app();
|
||||||
|
let i = app.active_tab;
|
||||||
|
let h_ref = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
|
||||||
|
let h_hatch = app.tabs[i].scene.add_entity_clone(EntityType::Hatch(Default::default()));
|
||||||
|
|
||||||
|
app.tabs[i].scene.replace_selection(std::iter::once(h_hatch).collect());
|
||||||
|
let cmd = format!("DRAWORDER ABOVE {:x}", h_ref.value());
|
||||||
|
let _ = app.run_command_line(&cmd);
|
||||||
|
|
||||||
|
let entries = effective_sort_map(&app);
|
||||||
|
let hatch_sort = entries.get(&h_hatch.value()).copied().unwrap_or(h_hatch.value());
|
||||||
|
let ref_sort = entries.get(&h_ref.value()).copied().unwrap_or(h_ref.value());
|
||||||
|
|
||||||
|
assert!(hatch_sort > ref_sort, "hatch ({hatch_sort}) must be above reference object ({ref_sort})");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn draworder_under_reference_puts_hatch_behind_object() {
|
||||||
|
let mut app = fresh_app();
|
||||||
|
let i = app.active_tab;
|
||||||
|
let h_ref = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
|
||||||
|
let h_hatch = app.tabs[i].scene.add_entity_clone(EntityType::Hatch(Default::default()));
|
||||||
|
|
||||||
|
app.tabs[i].scene.replace_selection(std::iter::once(h_hatch).collect());
|
||||||
|
let cmd = format!("DRAWORDER UNDER {:x}", h_ref.value());
|
||||||
|
let _ = app.run_command_line(&cmd);
|
||||||
|
|
||||||
|
let entries = effective_sort_map(&app);
|
||||||
|
let hatch_sort = entries.get(&h_hatch.value()).copied().unwrap_or(h_hatch.value());
|
||||||
|
let ref_sort = entries.get(&h_ref.value()).copied().unwrap_or(h_ref.value());
|
||||||
|
|
||||||
|
assert!(hatch_sort < ref_sort, "hatch ({hatch_sort}) must be under reference object ({ref_sort})");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn draworder_interactive_shortcuts_f_b_a_u() {
|
||||||
|
use crate::command::CadCommand;
|
||||||
|
|
||||||
|
let mut app = fresh_app();
|
||||||
|
let i = app.active_tab;
|
||||||
|
let _h_line = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
|
||||||
|
let h_hatch = app.tabs[i].scene.add_entity_clone(EntityType::Hatch(Default::default()));
|
||||||
|
|
||||||
|
// 1. Shortcut 'F' -> Front
|
||||||
|
let mut cmd = DrawOrderCommand::new(vec![h_hatch]);
|
||||||
|
assert_eq!(cmd.wants_text_input(), true);
|
||||||
|
let res = cmd.on_text_input("F");
|
||||||
|
match res {
|
||||||
|
Some(crate::command::CmdResult::Relaunch(c, handles)) => {
|
||||||
|
assert_eq!(c, "DRAWORDER FRONT");
|
||||||
|
assert_eq!(handles, vec![h_hatch]);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Relaunch for shortcut F"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Shortcut 'B' -> Back
|
||||||
|
let mut cmd = DrawOrderCommand::new(vec![h_hatch]);
|
||||||
|
let res = cmd.on_text_input("B");
|
||||||
|
match res {
|
||||||
|
Some(crate::command::CmdResult::Relaunch(c, handles)) => {
|
||||||
|
assert_eq!(c, "DRAWORDER BACK");
|
||||||
|
assert_eq!(handles, vec![h_hatch]);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Relaunch for shortcut B"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Shortcut 'A' -> Advances to PickReference (above)
|
||||||
|
let mut cmd = DrawOrderCommand::new(vec![h_hatch]);
|
||||||
|
assert!(!cmd.needs_entity_pick());
|
||||||
|
let res = cmd.on_text_input("A");
|
||||||
|
assert!(matches!(res, Some(crate::command::CmdResult::NeedPoint)));
|
||||||
|
assert!(cmd.needs_entity_pick(), "Needs entity pick after choosing 'A'");
|
||||||
|
|
||||||
|
// 4. Shortcut 'U' -> Advances to PickReference (under)
|
||||||
|
let mut cmd = DrawOrderCommand::new(vec![h_hatch]);
|
||||||
|
let res = cmd.on_text_input("U");
|
||||||
|
assert!(matches!(res, Some(crate::command::CmdResult::NeedPoint)));
|
||||||
|
assert!(cmd.needs_entity_pick(), "Needs entity pick after choosing 'U'");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn draworder_interactive_above_under_viewport_entity_pick() {
|
||||||
|
use crate::command::CadCommand;
|
||||||
|
|
||||||
|
let mut app = fresh_app();
|
||||||
|
let i = app.active_tab;
|
||||||
|
let h_ref = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
|
||||||
|
let h_hatch = app.tabs[i].scene.add_entity_clone(EntityType::Hatch(Default::default()));
|
||||||
|
|
||||||
|
// Above with viewport pick
|
||||||
|
let mut cmd = DrawOrderCommand::new(vec![h_hatch]);
|
||||||
|
let _ = cmd.on_text_input("A");
|
||||||
|
assert!(cmd.needs_entity_pick());
|
||||||
|
let pick_res = cmd.on_entity_pick(h_ref, glam::DVec3::ZERO);
|
||||||
|
match pick_res {
|
||||||
|
crate::command::CmdResult::Relaunch(relaunch_cmd, handles) => {
|
||||||
|
assert_eq!(relaunch_cmd, format!("DRAWORDER A {:x}", h_ref.value()));
|
||||||
|
assert_eq!(handles, vec![h_hatch]);
|
||||||
|
app.tabs[i].scene.replace_selection(handles.into_iter().collect());
|
||||||
|
let _ = app.run_command_line(&relaunch_cmd);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Relaunch from entity pick"),
|
||||||
|
}
|
||||||
|
|
||||||
|
let entries = effective_sort_map(&app);
|
||||||
|
let hatch_sort = entries.get(&h_hatch.value()).copied().unwrap_or(h_hatch.value());
|
||||||
|
let ref_sort = entries.get(&h_ref.value()).copied().unwrap_or(h_ref.value());
|
||||||
|
assert!(hatch_sort > ref_sort, "Hatch must be above reference after viewport pick");
|
||||||
|
|
||||||
|
// Under with viewport pick
|
||||||
|
let mut cmd = DrawOrderCommand::new(vec![h_hatch]);
|
||||||
|
let _ = cmd.on_text_input("U");
|
||||||
|
assert!(cmd.needs_entity_pick());
|
||||||
|
let pick_res = cmd.on_entity_pick(h_ref, glam::DVec3::ZERO);
|
||||||
|
match pick_res {
|
||||||
|
crate::command::CmdResult::Relaunch(relaunch_cmd, handles) => {
|
||||||
|
assert_eq!(relaunch_cmd, format!("DRAWORDER U {:x}", h_ref.value()));
|
||||||
|
assert_eq!(handles, vec![h_hatch]);
|
||||||
|
app.tabs[i].scene.replace_selection(handles.into_iter().collect());
|
||||||
|
let _ = app.run_command_line(&relaunch_cmd);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Relaunch from entity pick"),
|
||||||
|
}
|
||||||
|
|
||||||
|
let entries = effective_sort_map(&app);
|
||||||
|
let hatch_sort = entries.get(&h_hatch.value()).copied().unwrap_or(h_hatch.value());
|
||||||
|
let ref_sort = entries.get(&h_ref.value()).copied().unwrap_or(h_ref.value());
|
||||||
|
assert!(hatch_sort < ref_sort, "Hatch must be under reference after viewport pick");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn draworder_interactive_typed_hex_handle() {
|
||||||
|
use crate::command::CadCommand;
|
||||||
|
|
||||||
|
let mut app = fresh_app();
|
||||||
|
let i = app.active_tab;
|
||||||
|
let h_ref = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
|
||||||
|
let h_hatch = app.tabs[i].scene.add_entity_clone(EntityType::Hatch(Default::default()));
|
||||||
|
|
||||||
|
let mut cmd = DrawOrderCommand::new(vec![h_hatch]);
|
||||||
|
let _ = cmd.on_text_input("Above");
|
||||||
|
let hex_input = format!("0x{:x}", h_ref.value());
|
||||||
|
let typed_res = cmd.on_text_input(&hex_input);
|
||||||
|
match typed_res {
|
||||||
|
Some(crate::command::CmdResult::Relaunch(relaunch_cmd, handles)) => {
|
||||||
|
assert_eq!(relaunch_cmd, format!("DRAWORDER A {:x}", h_ref.value()));
|
||||||
|
assert_eq!(handles, vec![h_hatch]);
|
||||||
|
app.tabs[i].scene.replace_selection(handles.into_iter().collect());
|
||||||
|
let _ = app.run_command_line(&relaunch_cmd);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Relaunch from typed hex handle"),
|
||||||
|
}
|
||||||
|
|
||||||
|
let entries = effective_sort_map(&app);
|
||||||
|
let hatch_sort = entries.get(&h_hatch.value()).copied().unwrap_or(h_hatch.value());
|
||||||
|
let ref_sort = entries.get(&h_ref.value()).copied().unwrap_or(h_ref.value());
|
||||||
|
assert!(hatch_sort > ref_sort);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn draworder_interactive_without_preselection() {
|
||||||
|
use crate::command::CadCommand;
|
||||||
|
|
||||||
|
let mut app = fresh_app();
|
||||||
|
let i = app.active_tab;
|
||||||
|
let h_line = app.tabs[i].scene.add_entity_clone(EntityType::Line(Default::default()));
|
||||||
|
let h_hatch = app.tabs[i].scene.add_entity_clone(EntityType::Hatch(Default::default()));
|
||||||
|
|
||||||
|
// Start command with no pre-selection
|
||||||
|
let mut cmd = DrawOrderCommand::new(vec![]);
|
||||||
|
assert!(cmd.is_selection_gathering());
|
||||||
|
assert!(!cmd.wants_text_input());
|
||||||
|
|
||||||
|
// Gather selection
|
||||||
|
let _ = cmd.on_selection_complete(vec![h_hatch]);
|
||||||
|
let enter_res = cmd.on_enter();
|
||||||
|
assert!(matches!(enter_res, crate::command::CmdResult::NeedPoint));
|
||||||
|
assert!(!cmd.is_selection_gathering());
|
||||||
|
assert!(cmd.wants_text_input());
|
||||||
|
|
||||||
|
// Choose verb Front
|
||||||
|
let res = cmd.on_text_input("F");
|
||||||
|
match res {
|
||||||
|
Some(crate::command::CmdResult::Relaunch(relaunch_cmd, handles)) => {
|
||||||
|
assert_eq!(relaunch_cmd, "DRAWORDER FRONT");
|
||||||
|
assert_eq!(handles, vec![h_hatch]);
|
||||||
|
app.tabs[i].scene.replace_selection(handles.into_iter().collect());
|
||||||
|
let _ = app.run_command_line(&relaunch_cmd);
|
||||||
|
}
|
||||||
|
_ => panic!("Expected Relaunch"),
|
||||||
|
}
|
||||||
|
|
||||||
|
let entries = effective_sort_map(&app);
|
||||||
|
let hatch_sort = entries.get(&h_hatch.value()).copied().unwrap_or(h_hatch.value());
|
||||||
|
let line_sort = entries.get(&h_line.value()).copied().unwrap_or(h_line.value());
|
||||||
|
assert!(hatch_sort > line_sort);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4129,7 +4129,7 @@ impl OpenCADStudio {
|
||||||
.push_error(crate::t!("DRAWORDER: select entities first.").as_ref());
|
.push_error(crate::t!("DRAWORDER: select entities first.").as_ref());
|
||||||
} else {
|
} else {
|
||||||
use crate::command::CadCommand;
|
use crate::command::CadCommand;
|
||||||
let cmd = super::commands::DrawOrderRefCommand::new(to_move, above);
|
let cmd = super::commands::DrawOrderCommand::for_reference_pick(to_move, above);
|
||||||
self.command_line.push_info(&cmd.prompt());
|
self.command_line.push_info(&cmd.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -542,6 +542,34 @@ impl KeywordCommand {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn match_cmd_option<'a>(
|
||||||
|
options: &'a [(&'static str, &'static str, Option<&'static str>)],
|
||||||
|
text: &str,
|
||||||
|
) -> Option<&'a (&'static str, &'static str, Option<&'static str>)> {
|
||||||
|
let t = text.trim();
|
||||||
|
let up = t.to_uppercase();
|
||||||
|
if up.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// 1. Exact match on keyword or label (case-insensitive)
|
||||||
|
if let Some(opt) = options.iter().find(|(label, k, _)| {
|
||||||
|
k.eq_ignore_ascii_case(&up) || label.eq_ignore_ascii_case(t)
|
||||||
|
}) {
|
||||||
|
return Some(opt);
|
||||||
|
}
|
||||||
|
// 2. Unambiguous prefix match on keyword or label (e.g. "A" -> "ABOVE", "L" -> "LEFT")
|
||||||
|
let matches: Vec<_> = options
|
||||||
|
.iter()
|
||||||
|
.filter(|(label, k, _)| {
|
||||||
|
k.to_uppercase().starts_with(&up) || label.to_uppercase().starts_with(&up)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if matches.len() == 1 {
|
||||||
|
return Some(matches[0]);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
impl CadCommand for KeywordCommand {
|
impl CadCommand for KeywordCommand {
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
self.name
|
self.name
|
||||||
|
|
@ -581,10 +609,7 @@ impl CadCommand for KeywordCommand {
|
||||||
// Consumed inputs that keep prompting return `Some(NeedPoint)` —
|
// Consumed inputs that keep prompting return `Some(NeedPoint)` —
|
||||||
// `None` would hand the same text to the command a second time.
|
// `None` would hand the same text to the command a second time.
|
||||||
None => {
|
None => {
|
||||||
let up = t.to_uppercase();
|
let Some((_, keyword, value_prompt)) = match_cmd_option(&self.options, t)
|
||||||
let Some((_, keyword, value_prompt)) = self.options.iter().find(|(label, k, _)| {
|
|
||||||
k.eq_ignore_ascii_case(&up) || label.eq_ignore_ascii_case(t)
|
|
||||||
})
|
|
||||||
else {
|
else {
|
||||||
// Unknown verb — keep prompting rather than dispatch garbage.
|
// Unknown verb — keep prompting rather than dispatch garbage.
|
||||||
return Some(CmdResult::NeedPoint);
|
return Some(CmdResult::NeedPoint);
|
||||||
|
|
@ -783,11 +808,18 @@ impl CadCommand for SelectThenKeywordCommand {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
match self.pending {
|
match self.pending {
|
||||||
Some((keyword, _)) => Some(CmdResult::Dispatch(format!("{} {keyword} {t}", self.name))),
|
Some((keyword, _)) => {
|
||||||
|
if self.selected.is_empty() {
|
||||||
|
Some(CmdResult::Dispatch(format!("{} {keyword} {t}", self.name)))
|
||||||
|
} else {
|
||||||
|
Some(CmdResult::Relaunch(
|
||||||
|
format!("{} {keyword} {t}", self.name),
|
||||||
|
std::mem::take(&mut self.selected),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
let up = t.to_uppercase();
|
let Some((_, keyword, value_prompt)) = match_cmd_option(&self.options, t)
|
||||||
let Some((_, keyword, value_prompt)) =
|
|
||||||
self.options.iter().find(|(_, k, _)| k.eq_ignore_ascii_case(&up))
|
|
||||||
else {
|
else {
|
||||||
// Unknown verb — consumed, keep prompting (`None` would
|
// Unknown verb — consumed, keep prompting (`None` would
|
||||||
// feed the same text to the command a second time).
|
// feed the same text to the command a second time).
|
||||||
|
|
@ -798,7 +830,16 @@ impl CadCommand for SelectThenKeywordCommand {
|
||||||
self.pending = Some((keyword, vp));
|
self.pending = Some((keyword, vp));
|
||||||
Some(CmdResult::NeedPoint)
|
Some(CmdResult::NeedPoint)
|
||||||
}
|
}
|
||||||
None => Some(CmdResult::Dispatch(format!("{} {keyword}", self.name))),
|
None => {
|
||||||
|
if self.selected.is_empty() {
|
||||||
|
Some(CmdResult::Dispatch(format!("{} {keyword}", self.name)))
|
||||||
|
} else {
|
||||||
|
Some(CmdResult::Relaunch(
|
||||||
|
format!("{} {keyword}", self.name),
|
||||||
|
std::mem::take(&mut self.selected),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3070,6 +3070,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
||||||
"those objects" => Some(("modal", "those-objects")),
|
"those objects" => Some(("modal", "those-objects")),
|
||||||
"units" => Some(("common", "units")),
|
"units" => Some(("common", "units")),
|
||||||
"{cmd}: cannot create folder ({e})." => Some(("file", "cmd-cannot-create-folder-e")),
|
"{cmd}: cannot create folder ({e})." => Some(("file", "cmd-cannot-create-folder-e")),
|
||||||
|
"{cmd}: no hatch objects." => Some(("view", "cmd-no-hatch-objects")),
|
||||||
"{cmd}: no text or dimension objects." => Some(("view", "cmd-no-text-or-dimension-objects")),
|
"{cmd}: no text or dimension objects." => Some(("view", "cmd-no-text-or-dimension-objects")),
|
||||||
"{cmd}: not yet implemented." => Some(("view", "cmd-not-yet-implemented")),
|
"{cmd}: not yet implemented." => Some(("view", "cmd-not-yet-implemented")),
|
||||||
"{cmd}: packaged {copied} file(s) into {}" => Some(("file", "cmd-packaged-copied-file-s-into")),
|
"{cmd}: packaged {copied} file(s) into {}" => Some(("file", "cmd-packaged-copied-file-s-into")),
|
||||||
|
|
|
||||||
|
|
@ -2399,8 +2399,6 @@ impl Scene {
|
||||||
let mut bits = 0;
|
let mut bits = 0;
|
||||||
if is_outer {
|
if is_outer {
|
||||||
bits |= acadrust::entities::hatch::BoundaryPathFlags::OUTERMOST.bits();
|
bits |= acadrust::entities::hatch::BoundaryPathFlags::OUTERMOST.bits();
|
||||||
}
|
|
||||||
if !handles.is_empty() {
|
|
||||||
bits |= acadrust::entities::hatch::BoundaryPathFlags::EXTERNAL.bits();
|
bits |= acadrust::entities::hatch::BoundaryPathFlags::EXTERNAL.bits();
|
||||||
}
|
}
|
||||||
let mut path = BoundaryPath::with_flags(
|
let mut path = BoundaryPath::with_flags(
|
||||||
|
|
|
||||||
|
|
@ -5458,6 +5458,11 @@ impl Scene {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Invalidate only the draw-order depth cache without dropping geometry tessellation.
|
||||||
|
pub fn invalidate_draw_depth(&self) {
|
||||||
|
*self.draw_depth_cache.borrow_mut() = None;
|
||||||
|
}
|
||||||
|
|
||||||
/// Per-entity stable draw-order depth, keyed by entity handle value.
|
/// Per-entity stable draw-order depth, keyed by entity handle value.
|
||||||
/// A full build assigns sparse labels in effective draw order. Incremental
|
/// A full build assigns sparse labels in effective draw order. Incremental
|
||||||
/// Add/Remove then changes only the named handle: existing siblings retain
|
/// Add/Remove then changes only the named handle: existing siblings retain
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ fn block_internal_hatch_reaches_export() {
|
||||||
.expect("wrap hatch into a block + insert");
|
.expect("wrap hatch into a block + insert");
|
||||||
scene.populate_hatches_from_document();
|
scene.populate_hatches_from_document();
|
||||||
|
|
||||||
let hatches = scene.paper_canvas_hatches();
|
let hatches = scene.paper_plot_hatches();
|
||||||
let blue = hatches.iter().filter(|m| is_blue(&m.color)).count();
|
let blue = hatches.iter().filter(|m| is_blue(&m.color)).count();
|
||||||
assert!(
|
assert!(
|
||||||
blue > 0,
|
blue > 0,
|
||||||
|
|
@ -103,7 +103,7 @@ fn pattern_hatch_uses_stored_line_spacing() {
|
||||||
scene.add_entity(EntityType::Hatch(ansi31_stored(0.5, 10.0)));
|
scene.add_entity(EntityType::Hatch(ansi31_stored(0.5, 10.0)));
|
||||||
scene.populate_hatches_from_document();
|
scene.populate_hatches_from_document();
|
||||||
|
|
||||||
let hatches = scene.paper_canvas_hatches();
|
let hatches = scene.paper_plot_hatches();
|
||||||
let m = hatches
|
let m = hatches
|
||||||
.iter()
|
.iter()
|
||||||
.find(|m| matches!(m.pattern, HatchPattern::Pattern(_)))
|
.find(|m| matches!(m.pattern, HatchPattern::Pattern(_)))
|
||||||
|
|
@ -144,7 +144,7 @@ fn far_from_origin_pattern_hatch_still_fills() {
|
||||||
scene.add_entity(EntityType::Hatch(ansi31_stored_at(0.3, 1.0, 4000.0, 0.0)));
|
scene.add_entity(EntityType::Hatch(ansi31_stored_at(0.3, 1.0, 4000.0, 0.0)));
|
||||||
scene.populate_hatches_from_document();
|
scene.populate_hatches_from_document();
|
||||||
|
|
||||||
let hatches = scene.paper_canvas_hatches();
|
let hatches = scene.paper_plot_hatches();
|
||||||
let m = hatches
|
let m = hatches
|
||||||
.iter()
|
.iter()
|
||||||
.find(|m| matches!(m.pattern, HatchPattern::Pattern(_)))
|
.find(|m| matches!(m.pattern, HatchPattern::Pattern(_)))
|
||||||
|
|
@ -189,7 +189,7 @@ fn textbox_boundary_path_is_not_filled() {
|
||||||
scene.add_entity(EntityType::Hatch(hatch));
|
scene.add_entity(EntityType::Hatch(hatch));
|
||||||
scene.populate_hatches_from_document();
|
scene.populate_hatches_from_document();
|
||||||
|
|
||||||
let hatches = scene.paper_canvas_hatches();
|
let hatches = scene.paper_plot_hatches();
|
||||||
let m = hatches.first().expect("hatch present");
|
let m = hatches.first().expect("hatch present");
|
||||||
// The boundary must not extend into the 200x50 TEXTBOX rectangle.
|
// The boundary must not extend into the 200x50 TEXTBOX rectangle.
|
||||||
let max_x = m
|
let max_x = m
|
||||||
|
|
@ -244,8 +244,12 @@ fn app_created_hatch_roundtrips_catalog_spacing() {
|
||||||
world_origin: [0.0, 0.0],
|
world_origin: [0.0, 0.0],
|
||||||
boundary: Arc::new(boundary),
|
boundary: Arc::new(boundary),
|
||||||
boundary_wcs: None,
|
boundary_wcs: None,
|
||||||
|
fill_plane: None,
|
||||||
|
fill_plane_boundary: None,
|
||||||
boundary_exterior: None,
|
boundary_exterior: None,
|
||||||
boundary_sources: None,
|
boundary_sources: None,
|
||||||
|
boundary_paths: None,
|
||||||
|
style: acadrust::entities::HatchStyleType::Normal,
|
||||||
pattern: entry.gpu.clone(),
|
pattern: entry.gpu.clone(),
|
||||||
name: "ANSI31".into(),
|
name: "ANSI31".into(),
|
||||||
color: [0.75, 0.75, 0.75, 0.85],
|
color: [0.75, 0.75, 0.75, 0.85],
|
||||||
|
|
@ -258,7 +262,7 @@ fn app_created_hatch_roundtrips_catalog_spacing() {
|
||||||
scene.add_hatch(model, None, None);
|
scene.add_hatch(model, None, None);
|
||||||
scene.populate_hatches_from_document();
|
scene.populate_hatches_from_document();
|
||||||
|
|
||||||
let hatches = scene.paper_canvas_hatches();
|
let hatches = scene.paper_plot_hatches();
|
||||||
let m = hatches
|
let m = hatches
|
||||||
.iter()
|
.iter()
|
||||||
.find(|m| matches!(m.pattern, HatchPattern::Pattern(_)))
|
.find(|m| matches!(m.pattern, HatchPattern::Pattern(_)))
|
||||||
|
|
@ -298,8 +302,12 @@ fn nested_hatch_serializes_only_outer_as_external() {
|
||||||
world_origin: [0.0, 0.0],
|
world_origin: [0.0, 0.0],
|
||||||
boundary: Arc::new(boundary_f32),
|
boundary: Arc::new(boundary_f32),
|
||||||
boundary_wcs: Some(Arc::new(wcs)),
|
boundary_wcs: Some(Arc::new(wcs)),
|
||||||
|
fill_plane: None,
|
||||||
|
fill_plane_boundary: None,
|
||||||
boundary_exterior: None,
|
boundary_exterior: None,
|
||||||
boundary_sources: None,
|
boundary_sources: None,
|
||||||
|
boundary_paths: None,
|
||||||
|
style: acadrust::entities::HatchStyleType::Normal,
|
||||||
pattern: HatchPattern::Solid,
|
pattern: HatchPattern::Solid,
|
||||||
name: "SOLID".into(),
|
name: "SOLID".into(),
|
||||||
color: [0.45, 0.45, 0.45, 0.60],
|
color: [0.45, 0.45, 0.45, 0.60],
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue