diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index d92776f0a..10a6c1685 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -3073,7 +3073,7 @@ styles = .pdmode-new-value-0-dot-1-none-2-3-x-4-tick-32 = PDMODE القيمة الجديدة [0=نقطة 1=لا شيء 2=+ 3=x 4=شرطة؛ +32 دائرة +64 مربع]: .pdmode-v = PDMODE = __ocs_fmt_0__ .pdmode-set-to-v = عُيّن PDMODE إلى __ocs_fmt_0__ - .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE حجم النقطة الجديد (0 = 5% من منفذ العرض، <0 = مطلق): + .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE حجم النقطة الجديد (0 = 5% من منفذ العرض، >0 = مطلق، <0 = نسبة مئوية من منفذ العرض): .pdsize-v-4 = PDSIZE = __ocs_fmt_0__ .pdsize-set-to-v-4 = عُيّن PDSIZE إلى __ocs_fmt_0__ .pickadd = PICKADD = __ocs_fmt_0__ (__ocs_fmt_1__) diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 59905288a..2c828215f 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -3061,7 +3061,7 @@ styles = .pdmode-new-value-0-dot-1-none-2-3-x-4-tick-32 = PDMODE нова стойност [0=точка 1=нищо 2=+ 3=x 4=чертичка; +32 окръжност +64 квадрат]: .pdmode-v = PDMODE = __ocs_fmt_0__ .pdmode-set-to-v = PDMODE зададен на __ocs_fmt_0__ - .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE нов размер на точка (0 = 5% от изгледа, <0 = абсолютно): + .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE нов размер на точка (0 = 5% от изгледа, >0 = абсолютен, <0 = процент от изгледа): .pdsize-v-4 = PDSIZE = __ocs_fmt_0__ .pdsize-set-to-v-4 = PDSIZE зададен на __ocs_fmt_0__ .pickadd = PICKADD = __ocs_fmt_0__ (__ocs_fmt_1__) diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index c7480eb16..60f9e032f 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -3060,7 +3060,7 @@ styles = .pdmode-new-value-0-dot-1-none-2-3-x-4-tick-32 = PDMODE nová hodnota [0=dot 1=none 2=+ 3=x 4=tick; +32 kruh +64 čtverec]: .pdmode-v = PDMODE = __ocs_fmt_0__ .pdmode-set-to-v = PDMODE nastaveno na __ocs_fmt_0__ - .pdsize-new-point-size-0-5-of-viewport-0-absolu = Nová velikost bodu PDSIZE (0 = 5 % výřezu, <0 = absolutní): + .pdsize-new-point-size-0-5-of-viewport-0-absolu = Nová velikost bodu PDSIZE (0 = 5 % výřezu, >0 = absolutní, <0 = procento výřezu): .pdsize-v-4 = PDSIZE = __ocs_fmt_0__ .pdsize-set-to-v-4 = PDSIZE nastaveno na __ocs_fmt_0__ .pickadd = PICKADD = __ocs_fmt_0__ (__ocs_fmt_1__) diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index cc7d2bd64..e5631d614 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -3059,7 +3059,7 @@ styles = .pdmode-new-value-0-dot-1-none-2-3-x-4-tick-32 = PDMODE Neuer Wert [0=Punkt, 1=Kein Symbol, 2=+, 3=×, 4=Strich; +32=Kreis, +64=Quadrat]: .pdmode-v = PDMODE = __ocs_fmt_0__ .pdmode-set-to-v = PDMODE auf __ocs_fmt_0__ gesetzt - .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE Neue Punktgröße (0 = 5 % des Ansichtsfensters, < 0 = absolut): + .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE Neue Punktgröße (0 = 5 % des Ansichtsfensters, > 0 = absolut, < 0 = Ansichtsfenster-Prozentwert): .pdsize-v-4 = PDSIZE = __ocs_fmt_0__ .pdsize-set-to-v-4 = PDSIZE auf __ocs_fmt_0__ gesetzt .pickadd = PICKADD = __ocs_fmt_0__ (__ocs_fmt_1__) diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 14e069c8b..62ad9cb4d 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -3087,7 +3087,7 @@ styles = .pdmode-new-value-0-dot-1-none-2-3-x-4-tick-32 = PDMODE new value [0=dot 1=none 2=+ 3=x 4=tick; +32 circle +64 square]: .pdmode-v = PDMODE = __ocs_fmt_0__ .pdmode-set-to-v = PDMODE set to __ocs_fmt_0__ - .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE new point size (0 = 5% of viewport, <0 = absolute): + .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE new point size (0 = 5% of viewport, >0 = absolute, <0 = viewport percentage): .pdsize-v-4 = PDSIZE = __ocs_fmt_0__ .pdsize-set-to-v-4 = PDSIZE set to __ocs_fmt_0__ .pickadd = PICKADD = __ocs_fmt_0__ (__ocs_fmt_1__) diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index b1cd44aa7..162e2b2df 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -3061,7 +3061,7 @@ styles = .pdmode-new-value-0-dot-1-none-2-3-x-4-tick-32 = PDMODE nuevo valor [0=punto 1=ninguno 2=+ 3=x 4=marca; +32 círculo +64 cuadrado]: .pdmode-v = PDMODE = __ocs_fmt_0__ .pdmode-set-to-v = PDMODE establecido en __ocs_fmt_0__ - .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE nuevo tamaño de punto (0 = 5 % de la ventana gráfica, <0 = absoluto): + .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE nuevo tamaño de punto (0 = 5 % de la ventana gráfica, >0 = absoluto, <0 = porcentaje de la ventana gráfica): .pdsize-v-4 = PDSIZE = __ocs_fmt_0__ .pdsize-set-to-v-4 = PDSIZE establecido en __ocs_fmt_0__ .pickadd = PICKADD = __ocs_fmt_0__ (__ocs_fmt_1__) diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index e5f60b3f0..c037195f7 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -3060,7 +3060,7 @@ styles = .pdmode-new-value-0-dot-1-none-2-3-x-4-tick-32 = PDMODE uusi arvo [0=dot 1=none 2=+ 3=x 4=tick; +32 ympyrä +64 neliö]: .pdmode-v = PDMODE = __ocs_fmt_0__ .pdmode-set-to-v = PDMODE asetettu arvoon __ocs_fmt_0__ - .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE uusi pistekoko (0 = 5 % kuvaportista, <0 = absoluuttinen): + .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE uusi pistekoko (0 = 5 % kuvaportista, >0 = absoluuttinen, <0 = kuvaportin prosenttiosuus): .pdsize-v-4 = PDSIZE = __ocs_fmt_0__ .pdsize-set-to-v-4 = PDSIZE asetettu arvoon __ocs_fmt_0__ .pickadd = PICKADD = __ocs_fmt_0__ (__ocs_fmt_1__) diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 895e20256..53fd01193 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -3061,7 +3061,7 @@ styles = .pdmode-new-value-0-dot-1-none-2-3-x-4-tick-32 = PDMODE nouvelle valeur [0=dot 1=none 2=+ 3=x 4=tick; +32 cercle +64 carré]: .pdmode-v = PDMODE = __ocs_fmt_0__ .pdmode-set-to-v = PDMODE réglé sur __ocs_fmt_0__ - .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE Nouvelle taille de point (0 = 5 % de la fenêtre, < 0 = absolue) : + .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE Nouvelle taille de point (0 = 5 % de la fenêtre, > 0 = absolue, < 0 = pourcentage de la fenêtre) : .pdsize-v-4 = PDSIZE = __ocs_fmt_0__ .pdsize-set-to-v-4 = PDSIZE réglé sur __ocs_fmt_0__ .pickadd = PICKADD = __ocs_fmt_0__ (__ocs_fmt_1__) diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 083cdad03..11d804572 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -3049,7 +3049,7 @@ styles = .pdmode-new-value-0-dot-1-none-2-3-x-4-tick-32 = PDMODE नया मान [0=बिंदु 1=कोई नहीं 2=+ 3=× 4=टिक; +32 वृत्त +64 वर्ग]: .pdmode-v = PDMODE = __ocs_fmt_0__ .pdmode-set-to-v = PDMODE सेट करने के लिए __ocs_fmt_0__ - .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE नए बिंदु आकार (0 = 5% व्यूपोर्ट, <0 = निरपेक्ष): + .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE नया बिंदु आकार (0 = व्यूपोर्ट का 5%, >0 = निरपेक्ष, <0 = व्यूपोर्ट प्रतिशत): .pdsize-v-4 = PDSIZE = __ocs_fmt_0__ .pdsize-set-to-v-4 = PDSIZE सेट करने के लिए __ocs_fmt_0__ .pickadd = PICKADD = __ocs_fmt_0__ (__ocs_fmt_1__) diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index f6c62247a..5b51c6c85 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -3060,7 +3060,7 @@ styles = .pdmode-new-value-0-dot-1-none-2-3-x-4-tick-32 = PDMODE új érték [0=dot 1=none 2=+ 3=x 4=tick; +32 kör +64 négyzet]: .pdmode-v = PDMODE = __ocs_fmt_0__ .pdmode-set-to-v = A PDMODE __ocs_fmt_0__ értékre van állítva - .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE új pontméret (0 = a nézetablak 5%-a, <0 = abszolút): + .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE új pontméret (0 = a nézetablak 5%-a, >0 = abszolút, <0 = a nézetablak százaléka): .pdsize-v-4 = PDSIZE = __ocs_fmt_0__ .pdsize-set-to-v-4 = A PDSIZE __ocs_fmt_0__ értékre van állítva .pickadd = PICKADD = __ocs_fmt_0__ (__ocs_fmt_1__) diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 7285efa08..16bf92147 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -3060,7 +3060,7 @@ styles = .pdmode-new-value-0-dot-1-none-2-3-x-4-tick-32 = PDMODE nuovo valore [0=punto 1=none 2=+ 3=x 4=tick; +32 cerchio +64 quadrato]: .pdmode-v = PDMODE = __ocs_fmt_0__ .pdmode-set-to-v = PDMODE impostato su __ocs_fmt_0__ - .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE nuova dimensione in punti (0 = 5% dell'area visibile, <0 = assoluto): + .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE nuova dimensione del punto (0 = 5% dell'area visibile, >0 = assoluto, <0 = percentuale dell'area visibile): .pdsize-v-4 = PDSIZE = __ocs_fmt_0__ .pdsize-set-to-v-4 = PDSIZE impostato su __ocs_fmt_0__ .pickadd = PICKADD = __ocs_fmt_0__ (__ocs_fmt_1__) diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index 1228ae232..9882b99a6 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -3049,7 +3049,7 @@ styles = .pdmode-new-value-0-dot-1-none-2-3-x-4-tick-32 = PDMODE 新しい値 [0=点 1=なし 2=+ 3=x 4=目盛り、+32 円 +64 正方形]: .pdmode-v = PDMODE = __ocs_fmt_0__ .pdmode-set-to-v = PDMODE を __ocs_fmt_0__ に設定しました - .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE 新しい点サイズ(0=ビューポートの 5%、<0=絶対値): + .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE 新しい点サイズ(0=ビューポートの 5%、>0=絶対値、<0=ビューポートの割合): .pdsize-v-4 = PDSIZE = __ocs_fmt_0__ .pdsize-set-to-v-4 = PDSIZE を __ocs_fmt_0__ に設定しました .pickadd = PICKADD = __ocs_fmt_0__ (__ocs_fmt_1__) diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 133efd9da..3a70c1e8c 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -3060,7 +3060,7 @@ styles = .pdmode-new-value-0-dot-1-none-2-3-x-4-tick-32 = PDMODE 새 값 [0=dot 1=none 2=+ 3=x 4=tick; +32원 +64사각형]: .pdmode-v = PDMODE = __ocs_fmt_0__ .pdmode-set-to-v = PDMODE가 __ocs_fmt_0__로 설정됨 - .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE 새 포인트 크기(0 = 뷰포트의 5%, <0 = 절대값): + .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE 새 포인트 크기(0 = 뷰포트의 5%, >0 = 절대값, <0 = 뷰포트 백분율): .pdsize-v-4 = PDSIZE = __ocs_fmt_0__ .pdsize-set-to-v-4 = PDSIZE가 __ocs_fmt_0__로 설정됨 .pickadd = PICKADD = __ocs_fmt_0__ (__ocs_fmt_1__) diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 61f1391bd..86df2f9f6 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -3061,7 +3061,7 @@ styles = .pdmode-new-value-0-dot-1-none-2-3-x-4-tick-32 = PDMODE nieuwe waarde [0=punt 1=geen 2=+ 3=x 4=streepje; +32 cirkel +64 vierkant]: .pdmode-v = PDMODE = __ocs_fmt_0__ .pdmode-set-to-v = PDMODE ingesteld op __ocs_fmt_0__ - .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE nieuwe puntgrootte (0 = 5% van weergavevenster, <0 = absoluut): + .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE nieuwe puntgrootte (0 = 5% van weergavevenster, >0 = absoluut, <0 = percentage van weergavevenster): .pdsize-v-4 = PDSIZE = __ocs_fmt_0__ .pdsize-set-to-v-4 = PDSIZE ingesteld op __ocs_fmt_0__ .pickadd = PICKADD = __ocs_fmt_0__ (__ocs_fmt_1__) diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 8966f79ca..dbdbd8818 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -3060,7 +3060,7 @@ styles = .pdmode-new-value-0-dot-1-none-2-3-x-4-tick-32 = PDMODE nowa wartość [0=dot 1=none 2=+ 3=x 4=tick; +32 koło +64 kwadraty]: .pdmode-v = PDMODE = __ocs_fmt_0__ .pdmode-set-to-v = PDMODE ustawione na __ocs_fmt_0__ - .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE nowy rozmiar punktu (0 = 5% rzutni, <0 = bezwzględnie): + .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE nowy rozmiar punktu (0 = 5% rzutni, >0 = bezwzględnie, <0 = procent rzutni): .pdsize-v-4 = PDSIZE = __ocs_fmt_0__ .pdsize-set-to-v-4 = PDSIZE ustawione na __ocs_fmt_0__ .pickadd = PICKADD = __ocs_fmt_0__ (__ocs_fmt_1__) diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 6f6352a7e..57e8f09c2 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -3061,7 +3061,7 @@ styles = .pdmode-new-value-0-dot-1-none-2-3-x-4-tick-32 = PDMODE novo valor [0=ponto 1=nenhum 2=+ 3=x 4=marca; +32 círculo +64 quadrado]: .pdmode-v = PDMODE = __ocs_fmt_0__ .pdmode-set-to-v = PDMODE definido como __ocs_fmt_0__ - .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE novo tamanho do ponto (0 = 5% da janela de visualização, <0 = absoluto): + .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE novo tamanho do ponto (0 = 5% da janela de visualização, >0 = absoluto, <0 = porcentagem da janela de visualização): .pdsize-v-4 = PDSIZE = __ocs_fmt_0__ .pdsize-set-to-v-4 = PDSIZE definido como __ocs_fmt_0__ .pickadd = PICKADD = __ocs_fmt_0__ (__ocs_fmt_1__) diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index b66f4e06c..beb8b62c4 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -3064,7 +3064,7 @@ styles = .pdmode-new-value-0-dot-1-none-2-3-x-4-tick-32 = PDMODE Новое значение [0=точка 1=нет 2=+ 3=× 4=штрих; +32 окружность +64 квадрат]: .pdmode-v = PDMODE = __ocs_fmt_0__ .pdmode-set-to-v = PDMODE, установленный на __ocs_fmt_0__ - .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE новый размер точки (0 = 5% от площади обзора, <0 = абсолютный): + .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE новый размер точки (0 = 5% от области просмотра, >0 = абсолютный, <0 = процент области просмотра): .pdsize-v-4 = PDSIZE = __ocs_fmt_0__ .pdsize-set-to-v-4 = PDSIZE, установленный на __ocs_fmt_0__ .pickadd = PICKADD = __ocs_fmt_0__ (__ocs_fmt_1__) diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 8235a7ccb..e286badcf 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -3027,7 +3027,7 @@ styles = .pdmode-new-value-0-dot-1-none-2-3-x-4-tick-32 = PDMODE Yeni değer [0=nokta 1=yok 2=+ 3=x 4=çentik; +32 çember +64 kare]: .pdmode-v = PDMODE = __ocs_fmt_0__ .pdmode-set-to-v = PDMODE __ocs_fmt_0__ olarak ayarlandı - .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE Yeni nokta boyutu (0 = görünüm alanının %5'i, <0 = mutlak): + .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE Yeni nokta boyutu (0 = görünüm alanının %5'i, >0 = mutlak, <0 = görünüm alanı yüzdesi): .pdsize-v-4 = PDSIZE = __ocs_fmt_0__ .pdsize-set-to-v-4 = PDSIZE __ocs_fmt_0__ olarak ayarlandı .pickadd = PICKADD = __ocs_fmt_0__ (__ocs_fmt_1__) diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 7ae663903..aa6c9247a 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -3044,7 +3044,7 @@ styles = .pdmode-new-value-0-dot-1-none-2-3-x-4-tick-32 = PDMODE 新值 [0=点 1=无 2=+ 3=× 4=短划;+32 圆 +64 方框]: .pdmode-v = PDMODE = __ocs_fmt_0__ 时间轴 : .pdmode-set-to-v = PDMODE 设置为__ocs_fmt_0__ - .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE 指定新点大小(0 = 视口的 5%,<0 = 绝对值): + .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE 指定新点大小(0 = 视口的 5%,>0 = 绝对值,<0 = 视口百分比): .pdsize-v-4 = PDSIZE = __ocs_fmt_0__ 时间轴 : .pdsize-set-to-v-4 = PDSIZE 设置为__ocs_fmt_0__ .pickadd = PICKADD = __ocs_fmt_0__ (__ocs_fmt_1__) diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index d2e6823d1..e6eac9b29 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -3056,7 +3056,7 @@ styles = .pdmode-new-value-0-dot-1-none-2-3-x-4-tick-32 = PDMODE 新值 [0=點 1=無 2=+ 3=× 4=短划;+32 圓 +64 方框]: .pdmode-v = PDMODE = __ocs_fmt_0__ 時間軸 : .pdmode-set-to-v = PDMODE 設定為__ocs_fmt_0__ - .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE 指定新點大小(0 = 視口的 5%,<0 = 絕對值): + .pdsize-new-point-size-0-5-of-viewport-0-absolu = PDSIZE 指定新點大小(0 = 視口的 5%,>0 = 絕對值,<0 = 視口百分比): .pdsize-v-4 = PDSIZE = __ocs_fmt_0__ 時間軸 : .pdsize-set-to-v-4 = PDSIZE 設定為__ocs_fmt_0__ .pickadd = PICKADD = __ocs_fmt_0__ (__ocs_fmt_1__) diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 739b0748d..35bcecbda 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -2238,6 +2238,7 @@ impl OpenCADStudio { let far_pos = base + dir * far; let far_neg = base - dir * far; let guide = crate::scene::WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index 8ae8f0448..adda3e8f2 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -2066,6 +2066,7 @@ fn tessellate_dimension_inner( let (ext1, ext2) = split_ext_lines(&geom.ext_lines); if !ext1.is_empty() { wires.push(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, @@ -2099,6 +2100,7 @@ fn tessellate_dimension_inner( } if !ext2.is_empty() { wires.push(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, @@ -2132,6 +2134,7 @@ fn tessellate_dimension_inner( } } else { wires.push(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, @@ -2166,6 +2169,7 @@ fn tessellate_dimension_inner( } wires.push(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, @@ -2216,6 +2220,7 @@ fn tessellate_dimension_inner( aci_to_rgba(&c) }; wires.push(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/entities/leader.rs b/src/entities/leader.rs index bd14abfa3..535259180 100644 --- a/src/entities/leader.rs +++ b/src/entities/leader.rs @@ -674,6 +674,7 @@ impl LeaderTess for Leader { if verts.len() < 2 { return WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, @@ -852,6 +853,7 @@ impl LeaderTess for Leader { crate::scene::convert::tessellate::points_to_ds(fill_tris); WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/entities/multileader.rs b/src/entities/multileader.rs index 6cdfcf091..d25680135 100644 --- a/src/entities/multileader.rs +++ b/src/entities/multileader.rs @@ -1503,6 +1503,7 @@ impl MultiLeaderTess for MultiLeader { // WireModels so the renderer respects per-piece coloring. let mut wires: Vec = Vec::new(); wires.push(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, @@ -1842,6 +1843,7 @@ impl MultiLeaderTess for MultiLeader { xy = xy.max(p[1] as f64); } wires.push(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, @@ -1914,6 +1916,7 @@ impl MultiLeaderTess for MultiLeader { pts.push([bx, by, z]); } wires.push(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, @@ -2001,6 +2004,7 @@ impl MultiLeaderTess for MultiLeader { wcs_corners[3], ]; wires.push(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, @@ -2043,6 +2047,7 @@ impl MultiLeaderTess for MultiLeader { wcs_corners[0], ]; wires.push(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/entities/point.rs b/src/entities/point.rs index 2d4894d69..ba920466a 100644 --- a/src/entities/point.rs +++ b/src/entities/point.rs @@ -1,13 +1,15 @@ use acadrust::entities::Point; use acadrust::EntityType; -use crate::t; +use cadkernel::geom2d::{Circle, Curve, Line}; +use cadkernel::space::{curve::bezier_points, PlanarCurve, Plane, Vec3}; +use crate::t; use crate::command::EntityTransform; use crate::entities::common::{edit_prop as edit, parse_f64, square_grip}; use crate::entities::traits::RenderConvertible; use crate::scene::convert::acad_to_render::{RenderEntity, RenderObject}; use crate::scene::model::object::{GripApply, GripDef, PropSection}; -use crate::scene::model::wire_model::SnapHint; +use crate::scene::model::wire_model::{PointMarker, SnapHint}; /// Resolve a positive (absolute) PDSIZE to a world size. Relative/zero PDSIZE /// is handled by [`relative_render`]. @@ -99,7 +101,7 @@ pub fn relative_world_size(pdsize: f64, wpp: f32, viewport_height_px: f32) -> f6 pub fn relative_marker_spec( entity: &EntityType, document: &acadrust::CadDocument, -) -> Option<(f32, [f32; 3])> { +) -> Option { let EntityType::Point(pt) = entity else { return None; }; @@ -107,27 +109,41 @@ pub fn relative_marker_spec( if size > 0.0 || effective_pdmode(pt, document.header.point_display_mode) == 0 { return None; } - let n = point_normal(pt); - Some(( - if size == 0.0 { 5.0 } else { -size as f32 }, - [n.x as f32, n.y as f32, n.z as f32], - )) + let plane = point_plane(pt); + let normal = Vec3::from(plane.normal().unwrap_or([0.0, 0.0, 1.0])); + Some(PointMarker { + origin: glam::DVec3::new(pt.location.x, pt.location.y, pt.location.z), + normal: glam::DVec3::new(normal.x, normal.y, normal.z), + axis_x: glam::DVec3::from_array(plane.x_axis), + axis_y: glam::DVec3::from_array(plane.y_axis), + viewport_percent: if size == 0.0 { 5.0 } else { -size as f32 }, + }) } fn point_normal(pt: &Point) -> glam::DVec3 { - glam::DVec3::new(pt.normal.x, pt.normal.y, pt.normal.z).normalize_or(glam::DVec3::Z) + let normal = Vec3::new(pt.normal.x, pt.normal.y, pt.normal.z) + .normalize() + .unwrap_or(Vec3::Z); + glam::DVec3::new(normal.x, normal.y, normal.z) } -fn point_axes(pt: &Point) -> (glam::DVec3, glam::DVec3, glam::DVec3) { - let n = point_normal(pt); - let (ax, ay) = crate::scene::view::transform::ocs_axes((n.x, n.y, n.z)); - let base_x = glam::DVec3::new(ax.0, ax.1, ax.2); - let base_y = glam::DVec3::new(ay.0, ay.1, ay.2); - let (sin, cos) = pt.x_axis_angle.to_radians().sin_cos(); - ( - base_x * cos + base_y * sin, - -base_x * sin + base_y * cos, - n, +fn point_plane(pt: &Point) -> Plane { + let origin = [pt.location.x, pt.location.y, pt.location.z]; + let normal = Vec3::new(pt.normal.x, pt.normal.y, pt.normal.z) + .normalize() + .unwrap_or(Vec3::Z); + let x_seed = if normal.x.abs() < 1.0 / 64.0 && normal.y.abs() < 1.0 / 64.0 { + Vec3::Y.cross(normal) + } else { + Vec3::Z.cross(normal) + }; + let base = Plane::orthonormal(origin, x_seed.to_array(), normal.to_array()) + .unwrap_or(Plane::XY); + let (sin, cos) = pt.x_axis_angle.sin_cos(); + Plane::from_axes( + origin, + base.vector_at([cos, sin]), + base.vector_at([-sin, cos]), ) } @@ -144,88 +160,68 @@ fn point_glyph(pt: &Point, pdmode: i16, s_half: f64) -> Vec<[f64; 3]> { // The '+' and '×' arms reach the full PDSIZE (twice the radius), so the // cross pokes out past any enclosing circle/square, which sit at the radius. let arm = 2.0 * s_half; - let nan = [f64::NAN, f64::NAN, f64::NAN]; - let center = glam::DVec3::new(pt.location.x, pt.location.y, pt.location.z); - let (axis_x, axis_y, normal) = point_axes(pt); - let map = |x: f64, y: f64, height: f64| { - let p = center + axis_x * x + axis_y * y + normal * height; - [p.x, p.y, p.z] - }; - let mut pts: Vec<[f64; 3]> = Vec::new(); - let mut push_seg = |a: [f64; 3], b: [f64; 3]| { - if !pts.is_empty() { - pts.push(nan); - } - pts.push(a); - pts.push(b); - }; + let mut curves = Vec::new(); + let line = |start, end| Curve::Line(Line { start, end }); match shape { - // 0 = single dot — emit a tiny "+" so it's visible at any zoom. 0 => { let d = s * 0.05; - push_seg(map(-d, 0.0, 0.0), map(d, 0.0, 0.0)); - push_seg(map(0.0, -d, 0.0), map(0.0, d, 0.0)); + curves.push(line([-d, 0.0], [d, 0.0])); + curves.push(line([0.0, -d], [0.0, d])); } - 1 => {} // explicit nothing + 1 => {} 2 => { - push_seg(map(-arm, 0.0, 0.0), map(arm, 0.0, 0.0)); - push_seg(map(0.0, -arm, 0.0), map(0.0, arm, 0.0)); + curves.push(line([-arm, 0.0], [arm, 0.0])); + curves.push(line([0.0, -arm], [0.0, arm])); } 3 => { - push_seg(map(-arm, -arm, 0.0), map(arm, arm, 0.0)); - push_seg(map(-arm, arm, 0.0), map(arm, -arm, 0.0)); + curves.push(line([-arm, -arm], [arm, arm])); + curves.push(line([-arm, arm], [arm, -arm])); } 4 => { - // Upward tick rising from the point (length = PDSIZE/2), not a - // vertical line centred on it. - push_seg(map(0.0, 0.0, 0.0), map(0.0, s, 0.0)); + curves.push(line([0.0, 0.0], [0.0, s])); } _ => { - push_seg(map(-s, 0.0, 0.0), map(s, 0.0, 0.0)); - push_seg(map(0.0, -s, 0.0), map(0.0, s, 0.0)); + curves.push(line([-s, 0.0], [s, 0.0])); + curves.push(line([0.0, -s], [0.0, s])); } } if circle { - const N: usize = 64; - let mut ring: Vec<[f64; 3]> = Vec::with_capacity(N + 1); - for i in 0..=N { - let a = i as f64 * std::f64::consts::TAU / N as f64; - ring.push(map(a.cos() * s, a.sin() * s, 0.0)); - } - if !pts.is_empty() { - pts.push(nan); - } - pts.extend(ring); + curves.push(Curve::Circle(Circle { + centre: [0.0, 0.0], + radius: s, + })); } if square { - let p1 = map(-s, -s, 0.0); - let p2 = map(s, -s, 0.0); - let p3 = map(s, s, 0.0); - let p4 = map(-s, s, 0.0); - if !pts.is_empty() { - pts.push(nan); - } - pts.extend_from_slice(&[p1, p2, p3, p4, p1]); + curves.push(line([-s, -s], [s, -s])); + curves.push(line([s, -s], [s, s])); + curves.push(line([s, s], [-s, s])); + curves.push(line([-s, s], [-s, -s])); } - if pt.thickness.abs() > 1.0e-10 && !pts.is_empty() { - let base = pts.clone(); - pts.push(nan); - pts.extend(base.iter().map(|p| { - if p[0].is_finite() { - [ - p[0] + normal.x * pt.thickness, - p[1] + normal.y * pt.thickness, - p[2] + normal.z * pt.thickness, - ] - } else { - nan - } + + let plane = point_plane(pt); + let nan = [f64::NAN; 3]; + let mut paths: Vec> = curves + .iter() + .map(|curve| PlanarCurve::new(plane, curve.clone()).tessellate(64.0 / std::f64::consts::TAU)) + .collect(); + if pt.thickness.abs() > 1.0e-10 && !curves.is_empty() { + let normal = Vec3::from(plane.normal().unwrap_or([0.0, 0.0, 1.0])); + let top_origin = (Vec3::from(plane.origin) + normal * pt.thickness).to_array(); + let top_plane = Plane::from_axes(top_origin, plane.x_axis, plane.y_axis); + paths.extend(curves.iter().map(|curve| { + PlanarCurve::new(top_plane, curve.clone()) + .tessellate(64.0 / std::f64::consts::TAU) })); - pts.push(nan); - pts.push(map(0.0, 0.0, 0.0)); - pts.push(map(0.0, 0.0, pt.thickness)); + paths.push(bezier_points(&[plane.origin, top_origin], 1)); } - pts + let mut points = Vec::new(); + for path in paths { + if !points.is_empty() { + points.push(nan); + } + points.extend(path); + } + points } fn is_defpoints_layer(layer: &str) -> bool { diff --git a/src/entities/table.rs b/src/entities/table.rs index aa6a6ff65..67acd76b9 100644 --- a/src/entities/table.rs +++ b/src/entities/table.rs @@ -1667,6 +1667,7 @@ pub fn tessellate_table( let mk = |color: [f32; 4], points: Vec<[f32; 3]>, fill_tris: Vec<[f32; 3]>, lw: f32| -> WireModel { WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/io/pdf_export.rs b/src/io/pdf_export.rs index d7923f49e..f8a440366 100644 --- a/src/io/pdf_export.rs +++ b/src/io/pdf_export.rs @@ -541,9 +541,9 @@ fn append_pdf_page( flush_line(&mut ops, &segment, dot_radius); segment.clear(); } else { - let lo = wire.points_low.get(pi).copied().unwrap_or([0.0; 3]); - let wx = (x as f64 + lo[0] as f64 + ox) as f32; - let wy = (y as f64 + lo[1] as f64 + oy) as f32; + let point = wire.point_world(pi, paper_h as f64 / scale.max(1e-6) as f64); + let wx = (point.x + ox) as f32; + let wy = (point.y + oy) as f32; segment.push(LinePoint { p: Point::new(Mm(wx), Mm(wy)), bezier: false, diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 73422d22b..b8ee4016c 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -1842,6 +1842,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "PDMODE new value [0=dot 1=none 2=+ 3=x 4=tick; +32 circle +64 square]:" => Some(("styles", "pdmode-new-value-0-dot-1-none-2-3-x-4-tick-32")), "PDMODE = {v}" => Some(("styles", "pdmode-v")), "PDMODE set to {v}" => Some(("styles", "pdmode-set-to-v")), + "PDSIZE new point size (0 = 5% of viewport, >0 = absolute, <0 = viewport percentage):" => Some(("styles", "pdsize-new-point-size-0-5-of-viewport-0-absolu")), "PDSIZE = {v:.4}" => Some(("styles", "pdsize-v-4")), "PDSIZE set to {v:.4}" => Some(("styles", "pdsize-set-to-v-4")), "PEDIT Enter option:" => Some(("modify", "pedit-enter-option")), diff --git a/src/modules/annotate/aligned_dim.rs b/src/modules/annotate/aligned_dim.rs index 68ff50610..5a4154262 100644 --- a/src/modules/annotate/aligned_dim.rs +++ b/src/modules/annotate/aligned_dim.rs @@ -220,6 +220,7 @@ impl CadCommand for AlignedDimensionCommand { let p1 = p1.as_vec3(); let p2 = p2.as_vec3(); Some(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, @@ -278,6 +279,7 @@ fn preview_aligned(p1: DVec3, p2: DVec3, dim_pt: DVec3) -> WireModel { let d1 = d1.as_vec3(); let d2 = d2.as_vec3(); WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/modules/annotate/angular_dim.rs b/src/modules/annotate/angular_dim.rs index 1b93dd707..b526532d5 100644 --- a/src/modules/annotate/angular_dim.rs +++ b/src/modules/annotate/angular_dim.rs @@ -161,6 +161,7 @@ fn v3(pt: DVec3) -> Vector3 { fn preview_wire(points: Vec) -> WireModel { WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/modules/annotate/diameter_dim.rs b/src/modules/annotate/diameter_dim.rs index e319c1717..f0a570d65 100644 --- a/src/modules/annotate/diameter_dim.rs +++ b/src/modules/annotate/diameter_dim.rs @@ -192,6 +192,7 @@ fn v3(p: DVec3) -> Vector3 { fn preview_line(a: Vec3, b: Vec3) -> WireModel { WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/modules/annotate/dim_baseline.rs b/src/modules/annotate/dim_baseline.rs index 28d35e7b9..b38718395 100644 --- a/src/modules/annotate/dim_baseline.rs +++ b/src/modules/annotate/dim_baseline.rs @@ -180,6 +180,7 @@ impl CadCommand for DimBaselineCommand { let dim_line_pt2 = dim_line_pt2.as_vec3(); let pt = pt.as_vec3(); Some(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/modules/annotate/dim_continue.rs b/src/modules/annotate/dim_continue.rs index 695c8b152..d2371aac9 100644 --- a/src/modules/annotate/dim_continue.rs +++ b/src/modules/annotate/dim_continue.rs @@ -160,6 +160,7 @@ impl CadCommand for DimContinueCommand { let dim_line_pt = p1 + perp * (dim_line_perp - p1.dot(perp)); let dim_line_pt2 = pt + perp * (dim_line_perp - pt.dot(perp)); Some(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/modules/annotate/dimjogline.rs b/src/modules/annotate/dimjogline.rs index ab314a016..ebaaa8b49 100644 --- a/src/modules/annotate/dimjogline.rs +++ b/src/modules/annotate/dimjogline.rs @@ -86,6 +86,7 @@ impl CadCommand for DimJogLineCommand { } let d = 0.3_f32; Some(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/modules/annotate/dimtedit.rs b/src/modules/annotate/dimtedit.rs index 891ff95f2..e84213202 100644 --- a/src/modules/annotate/dimtedit.rs +++ b/src/modules/annotate/dimtedit.rs @@ -103,6 +103,7 @@ impl CadCommand for DimTeditCommand { } let d = 0.2_f32; Some(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/modules/annotate/leader_cmd.rs b/src/modules/annotate/leader_cmd.rs index 9ef884b69..1e13d936f 100644 --- a/src/modules/annotate/leader_cmd.rs +++ b/src/modules/annotate/leader_cmd.rs @@ -300,6 +300,7 @@ fn preview_wire(pts: &[Vec3], arrow_size: f32) -> WireModel { points.push([w2.x, w2.y, w2.z]); } WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/modules/annotate/linear_dim.rs b/src/modules/annotate/linear_dim.rs index fbc6c3d3d..f2af91e0a 100644 --- a/src/modules/annotate/linear_dim.rs +++ b/src/modules/annotate/linear_dim.rs @@ -237,6 +237,7 @@ fn v3(pt: DVec3) -> Vector3 { fn preview_wire(points: Vec) -> WireModel { WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/modules/annotate/mleader_cmd.rs b/src/modules/annotate/mleader_cmd.rs index 896e2fd47..383a8d666 100644 --- a/src/modules/annotate/mleader_cmd.rs +++ b/src/modules/annotate/mleader_cmd.rs @@ -227,6 +227,7 @@ fn preview_wire(pts: &[Vec3], arrow_size: f32) -> WireModel { points.push([w2.x, w2.y, w2.z]); } WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/modules/annotate/mleader_edit.rs b/src/modules/annotate/mleader_edit.rs index b91e9d2ee..e3f19c235 100644 --- a/src/modules/annotate/mleader_edit.rs +++ b/src/modules/annotate/mleader_edit.rs @@ -462,6 +462,7 @@ impl CadCommand for MLeaderCollectCommand { fn preview_wire(pts: &[Vec3]) -> WireModel { WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/modules/annotate/ordinate_dim.rs b/src/modules/annotate/ordinate_dim.rs index df3564fcd..331677a34 100644 --- a/src/modules/annotate/ordinate_dim.rs +++ b/src/modules/annotate/ordinate_dim.rs @@ -137,6 +137,7 @@ fn ordinate_elbow(feature: DVec3, leader: DVec3, is_x: bool) -> DVec3 { fn preview_wire(points: Vec) -> WireModel { WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/modules/annotate/qdim.rs b/src/modules/annotate/qdim.rs index e93205102..4c1596a1d 100644 --- a/src/modules/annotate/qdim.rs +++ b/src/modules/annotate/qdim.rs @@ -93,6 +93,7 @@ impl CadCommand for QdimCommand { } let d = 0.5_f32; Some(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/modules/annotate/radius_dim.rs b/src/modules/annotate/radius_dim.rs index 6aa43017a..e91ca64e9 100644 --- a/src/modules/annotate/radius_dim.rs +++ b/src/modules/annotate/radius_dim.rs @@ -197,6 +197,7 @@ fn v3(pt: DVec3) -> Vector3 { fn preview_wire(points: Vec) -> WireModel { WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/modules/annotate/table_cmd.rs b/src/modules/annotate/table_cmd.rs index 4173d7635..c243e2b60 100644 --- a/src/modules/annotate/table_cmd.rs +++ b/src/modules/annotate/table_cmd.rs @@ -199,6 +199,7 @@ impl CadCommand for TableCommand { .map(|corner| self.plane.to_world(corner).as_vec3().to_array()); let [p0, p1, p2, p3] = corners; Some(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/modules/annotate/tolerance_cmd.rs b/src/modules/annotate/tolerance_cmd.rs index 18c11839a..3033e969e 100644 --- a/src/modules/annotate/tolerance_cmd.rs +++ b/src/modules/annotate/tolerance_cmd.rs @@ -103,6 +103,7 @@ impl CadCommand for ToleranceCommand { pt + self.plane.y * d, ]; Some(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/modules/draw/draw/attdef.rs b/src/modules/draw/draw/attdef.rs index fa2c38dd6..33551f5a7 100644 --- a/src/modules/draw/draw/attdef.rs +++ b/src/modules/draw/draw/attdef.rs @@ -201,6 +201,7 @@ impl CadCommand for AttdefCommand { pt + self.plane.y * d, ]; Some(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/modules/draw/draw/mline.rs b/src/modules/draw/draw/mline.rs index 120840e59..1be63334e 100644 --- a/src/modules/draw/draw/mline.rs +++ b/src/modules/draw/draw/mline.rs @@ -181,6 +181,7 @@ impl CadCommand for MlineCommand { .collect(); pts.push([pt.x, pt.y, pt.z]); Some(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/modules/draw/draw/raster_image.rs b/src/modules/draw/draw/raster_image.rs index 7f97be788..ac973aca2 100644 --- a/src/modules/draw/draw/raster_image.rs +++ b/src/modules/draw/draw/raster_image.rs @@ -123,6 +123,7 @@ impl CadCommand for ImageCommand { let [p0, p1, p2, p3] = corners; Some(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/modules/draw/draw/ray.rs b/src/modules/draw/draw/ray.rs index 14696aec2..47bcf9a81 100644 --- a/src/modules/draw/draw/ray.rs +++ b/src/modules/draw/draw/ray.rs @@ -85,6 +85,7 @@ impl CadCommand for RayCommand { let dir = (pt - base).normalize_or_zero(); let far = base + dir * DISPLAY_EXTENT; Some(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, @@ -187,6 +188,7 @@ impl CadCommand for XLineCommand { let far_pos = base + dir * DISPLAY_EXTENT; let far_neg = base - dir * DISPLAY_EXTENT; Some(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/modules/draw/inquiry/area.rs b/src/modules/draw/inquiry/area.rs index 5a260824b..6ae6386e4 100644 --- a/src/modules/draw/inquiry/area.rs +++ b/src/modules/draw/inquiry/area.rs @@ -362,6 +362,7 @@ impl CadCommand for AreaCommand { points.push(to_render(point)); points.push(to_render(self.points[0])); Some(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/modules/draw/inquiry/dist.rs b/src/modules/draw/inquiry/dist.rs index 157318ca2..d0a3ac0f4 100644 --- a/src/modules/draw/inquiry/dist.rs +++ b/src/modules/draw/inquiry/dist.rs @@ -85,6 +85,7 @@ impl CadCommand for DistCommand { let p1 = self.first?; // The preview wire is purely visual, so f32 vertices are fine here. Some(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/modules/draw/inquiry/measuregeom.rs b/src/modules/draw/inquiry/measuregeom.rs index d9eaa56a3..17d5d72cb 100644 --- a/src/modules/draw/inquiry/measuregeom.rs +++ b/src/modules/draw/inquiry/measuregeom.rs @@ -70,6 +70,7 @@ impl MeasureGeomCommand { /// Build a cyan preview wire connecting the picked points and the cursor. fn preview_wire(name: &str, pts: Vec<[f32; 3]>) -> WireModel { WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/modules/draw/modify/trim.rs b/src/modules/draw/modify/trim.rs index 00d0485e4..a04b94aaf 100644 --- a/src/modules/draw/modify/trim.rs +++ b/src/modules/draw/modify/trim.rs @@ -3719,6 +3719,7 @@ fn collect_runs(pts: &[[f64; 2]], take: &dyn Fn([f64; 2]) -> bool, out: &mut Vec /// Build a preview wire from a NaN-break point list. fn preview_wire(points: Vec<[f32; 3]>, color: [f32; 4], name: &str) -> WireModel { WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/modules/model/primitive_cmd.rs b/src/modules/model/primitive_cmd.rs index 5eb27f242..66941b701 100644 --- a/src/modules/model/primitive_cmd.rs +++ b/src/modules/model/primitive_cmd.rs @@ -511,6 +511,7 @@ inventory::submit!(crate::command::CommandRegistration { fn wire(name: &str, points: Vec<[f32; 3]>) -> WireModel { WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/modules/view/plot_window.rs b/src/modules/view/plot_window.rs index 31a888de8..4f86e8cf6 100644 --- a/src/modules/view/plot_window.rs +++ b/src/modules/view/plot_window.rs @@ -54,6 +54,7 @@ impl CadCommand for PlotWindowCommand { let p1 = self.p1?.as_vec3(); // Draw the selection rectangle. Some(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/modules/view/zoom_window.rs b/src/modules/view/zoom_window.rs index dd91f0941..3e666cc77 100644 --- a/src/modules/view/zoom_window.rs +++ b/src/modules/view/zoom_window.rs @@ -136,6 +136,7 @@ impl CadCommand for ZoomWindowCommand { let max = p1.max(pt); // Draw a rectangle preview Some(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index c63bd1d31..95886daf0 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -20,9 +20,10 @@ use std::sync::Arc; use acadrust::types::{Color as AcadColor, LineWeight, Transform, Vector3}; use acadrust::{CadDocument, EntityType, Handle}; +use cadkernel::space::{Plane as KernelPlane, Vec3 as KernelVec3}; use crate::scene::convert::tessellate; -use crate::scene::model::wire_model::{SnapHint, TangentGeom, WireModel}; +use crate::scene::model::wire_model::{PointMarker, SnapHint, TangentGeom, WireModel}; const MAX_NESTING_DEPTH: usize = 32; /// Skip wires whose world-AABB projects to fewer than this many pixels in @@ -38,6 +39,7 @@ pub struct LocalWire { /// Low-bit residual paired with `points` so block-instance wires keep /// sub-f32 precision once the renderer translates them to world space. pub points_low: Vec<[f32; 3]>, + pub point_marker: Option, /// SDF glyph quads for block-internal text, in block-local coordinates. /// Non-empty only when SDF text is on and this sub is a TEXT/MTEXT. The /// expand-time transform (`emit_wire`) maps each vertex to world exactly @@ -132,6 +134,7 @@ pub enum LocalSub { pub struct BlockDefn { pub subs: Vec, pub base_point: Vector3, + pub has_point_marker: bool, /// Union of every sub's local AABB (including nested-INSERT contributions /// resolved at expand time via their own defn's `aabb_local`). XY only — /// the wire renderer is 2D-dominant. Expressed in this defn's *offset* @@ -312,23 +315,49 @@ impl BlockCache { // many parents is re-walked per parent — real work on block-heavy // drawings — and the per-name walks are independent, so they fan out. let this: &Self = self; - let resolved: Vec<(&String, [f32; 4])> = names + let resolved: Vec<(&String, [f32; 4], bool)> = names .par_iter() .map(|name| { let mut visited: Vec = Vec::new(); - (name, this.defn_aabb_recursive(name, &mut visited)) + let aabb = this.defn_aabb_recursive(name, &mut visited); + visited.clear(); + let has_marker = this.defn_has_point_marker_recursive(name, &mut visited); + (name, aabb, has_marker) }) .collect(); // Phase 2 (serial): store the AABB back into each defn. - for (name, aabb) in resolved { + for (name, aabb, has_marker) in resolved { if let Some(defn_arc) = self.defns.get_mut(name) { let mut defn = (**defn_arc).clone(); defn.aabb_local = aabb; + defn.has_point_marker = has_marker; *defn_arc = Arc::new(defn); } } } + fn defn_has_point_marker_recursive( + &self, + block_name: &str, + visited: &mut Vec, + ) -> bool { + if visited.iter().any(|name| name == block_name) { + return false; + } + let Some(defn) = self.defns.get(block_name) else { + return false; + }; + visited.push(block_name.to_string()); + let found = defn.subs.iter().any(|sub| match sub { + LocalSub::Wire(wire) => wire.point_marker.is_some(), + LocalSub::Nested(insert) => { + self.defn_has_point_marker_recursive(&insert.block_name, visited) + } + }); + visited.pop(); + found + } + /// Returns the union AABB for `block_name`'s defn, expressed in **that /// defn's offset frame** (so its caller can store it in /// `BlockDefn.aabb_local` without a coordinate-frame mismatch). @@ -619,6 +648,7 @@ fn build_defn( BlockDefn { subs, base_point: crate::scene::render_graph::block_base_point(doc, block_name), + has_point_marker: false, aabb_local: [0.0; 4], child_count: br.entity_handles.len(), } @@ -816,6 +846,7 @@ fn tessellate_sub_local( result.push(LocalWire { points: wire.points, points_low: wire.points_low, + point_marker: wire.point_marker, text_verts: wire.text_verts, key_vertices: wire.key_vertices, snap_pts: wire.snap_pts, @@ -1069,7 +1100,7 @@ pub fn expand_insert( // Whole-Insert pixel-size LOD: if the entire Insert footprint projects // to sub-pixel size, skip it entirely. if let Some(wpp) = world_per_pixel { - if aabb_pixel_size(insert_local, wpp) < MIN_PIXEL_SIZE { + if !defn.has_point_marker && aabb_pixel_size(insert_local, wpp) < MIN_PIXEL_SIZE { return Some(vec![]); } } @@ -1244,6 +1275,9 @@ fn translated_prototype_wire( translate_double_single(&mut wire.points, &mut wire.points_low, delta); translate_double_single(&mut wire.fill_tris, &mut wire.fill_tris_low, delta); translate_double_single(&mut wire.pick_tris, &mut wire.pick_tris_low, delta); + if let Some(marker) = &mut wire.point_marker { + marker.origin += glam::DVec3::from_array(delta); + } for (point, _) in &mut wire.snap_pts { point.x += delta[0]; point.y += delta[1]; @@ -1403,6 +1437,7 @@ struct StyleKey { /// bands vs thin wires of the same colour/style — in separate batches so the /// finalized WireModel carries one correct `world_width`. world_width: u32, + point_marker: Option, aci: u8, plinegen: bool, /// Marks batches that emit only `fill_tris` with no wire `points`. The @@ -1422,6 +1457,15 @@ struct StyleKey { depth_bits: u32, } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +struct PointMarkerKey { + origin: [u64; 3], + normal: [u64; 3], + axis_x: [u64; 3], + axis_y: [u64; 3], + viewport_percent: u32, +} + #[derive(Default, Debug)] struct BatchEntry { color: [f32; 4], @@ -1432,6 +1476,7 @@ struct BatchEntry { pattern: [f32; 8], line_weight_px: f32, world_width: f32, + point_marker: Option, aci: u8, plinegen: bool, /// Composed block-local draw-order offset for a band batch (see @@ -1511,6 +1556,7 @@ impl BatchEntry { pat: [f32; 8], lw_px: f32, world_width: f32, + point_marker: Option, aci: u8, plinegen: bool, _is_fill_only: bool, @@ -1533,6 +1579,7 @@ impl BatchEntry { pattern: pat, line_weight_px: lw_px, world_width, + point_marker, aci, plinegen, fill_is_2d_solid, @@ -1582,6 +1629,7 @@ impl Batches { } } WireModel { + point_marker: b.point_marker, taper_widths: Vec::new(), world_width: b.world_width, depth_override: b.local_depth, @@ -1634,6 +1682,7 @@ fn style_key( pat: [f32; 8], lw_px: f32, world_width: f32, + point_marker: Option, aci: u8, plinegen: bool, is_fill_only: bool, @@ -1666,6 +1715,13 @@ fn style_key( ], line_weight_px: lw_px.to_bits(), world_width: world_width.to_bits(), + point_marker: point_marker.map(|marker| PointMarkerKey { + origin: marker.origin.to_array().map(f64::to_bits), + normal: marker.normal.to_array().map(f64::to_bits), + axis_x: marker.axis_x.to_array().map(f64::to_bits), + axis_y: marker.axis_y.to_array().map(f64::to_bits), + viewport_percent: marker.viewport_percent.to_bits(), + }), aci, plinegen, is_fill_only, @@ -2004,6 +2060,69 @@ fn transformed_wire_length_scale(lw: &LocalWire, xform: &Transform) -> f32 { } } +fn transformed_point_marker(marker: PointMarker, xform: &Transform) -> (PointMarker, f64) { + let origin = xform.apply(Vector3::new(marker.origin.x, marker.origin.y, marker.origin.z)); + let map_direction = |direction: glam::DVec3| { + let mapped = xform.matrix.transform_direction(Vector3::new( + direction.x, + direction.y, + direction.z, + )); + KernelVec3::new(mapped.x, mapped.y, mapped.z) + }; + let mapped_normal = map_direction(marker.normal); + let normal_scale = mapped_normal.length().max(f64::MIN_POSITIVE); + let normal = mapped_normal.normalize().unwrap_or(KernelVec3::Z); + let mapped_x = map_direction(marker.axis_x); + let axis_x = (mapped_x - normal * mapped_x.dot(normal)) + .normalize() + .unwrap_or(KernelVec3::X); + let mapped_y = map_direction(marker.axis_y); + let axis_y = (mapped_y - normal * mapped_y.dot(normal) - axis_x * mapped_y.dot(axis_x)) + .normalize() + .or_else(|| normal.cross(axis_x).normalize()) + .unwrap_or(KernelVec3::Y); + ( + PointMarker { + origin: glam::DVec3::new(origin.x, origin.y, origin.z), + normal: glam::DVec3::from_array(normal.to_array()), + axis_x: glam::DVec3::from_array(axis_x.to_array()), + axis_y: glam::DVec3::from_array(axis_y.to_array()), + viewport_percent: marker.viewport_percent, + }, + normal_scale, + ) +} + +fn transformed_marker_point( + point: KernelVec3, + source: PointMarker, + target: PointMarker, + axial_scale: f64, +) -> KernelVec3 { + let source_origin = KernelVec3::from(source.origin.to_array()); + let source_normal = KernelVec3::from(source.normal.to_array()) + .normalize() + .unwrap_or(KernelVec3::Z); + let delta = point - source_origin; + let axial = delta.dot(source_normal); + let source_plane = KernelPlane::from_axes( + source.origin.to_array(), + source.axis_x.to_array(), + source.axis_y.to_array(), + ); + let uv = source_plane + .project_vector((delta - source_normal * axial).to_array()) + .unwrap_or([0.0; 2]); + let target_plane = KernelPlane::from_axes( + target.origin.to_array(), + target.axis_x.to_array(), + target.axis_y.to_array(), + ); + KernelVec3::from(target_plane.point_at(uv)) + + KernelVec3::from(target.normal.to_array()) * axial * axial_scale +} + fn emit_wire( lw: &LocalWire, accum_xform: &Transform, @@ -2069,6 +2188,10 @@ fn emit_wire( } else { 0.0 }; + let marker_transform = lw + .point_marker + .map(|marker| transformed_point_marker(marker, accum_xform)); + let point_marker = marker_transform.map(|(marker, _)| marker); // Only band wires take a per-child composed depth: their solid area is // what covers siblings, and their width already splits them into their own @@ -2089,6 +2212,7 @@ fn emit_wire( final_pat, final_lw_px, final_world_width, + point_marker, final_aci, lw.plinegen, lw.is_fill_only, @@ -2118,6 +2242,7 @@ fn emit_wire( final_pat, final_lw_px, final_world_width, + point_marker, final_aci, lw.plinegen, lw.is_fill_only, @@ -2151,11 +2276,20 @@ fn emit_wire( let pl = lw.points_low.get(idx).copied().unwrap_or([0.0; 3]); // Reconstruct the f64 source from (high, low) before applying the // insert transform — otherwise the low half is silently dropped. - let v = accum_xform.apply(Vector3::new( + let source = KernelVec3::new( p[0] as f64 + pl[0] as f64, p[1] as f64 + pl[1] as f64, p[2] as f64 + pl[2] as f64, - )); + ); + let mapped = if let (Some(source_marker), Some((target_marker, axial_scale))) = + (lw.point_marker, marker_transform) + { + transformed_marker_point(source, source_marker, target_marker, axial_scale) + } else { + let point = accum_xform.apply(Vector3::new(source.x, source.y, source.z)); + KernelVec3::new(point.x, point.y, point.z) + }; + let v = Vector3::new(mapped.x, mapped.y, mapped.z); let qx = (v.x) as f32; let qy = (v.y) as f32; let qz = (v.z) as f32; diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs index f7272b935..eab11c215 100644 --- a/src/scene/convert/tess.rs +++ b/src/scene/convert/tess.rs @@ -748,6 +748,7 @@ fn tessellate_entity_inner( Vec::new() }; return vec![WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, @@ -1107,6 +1108,7 @@ fn tessellate_entity_inner( (ins.insert_point.z) as f32, ); let marker = WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, @@ -1504,6 +1506,7 @@ fn lod_stub_wire( // LOD boundary. #19. let stored_color = if selected { WireModel::SELECTED } else { color }; WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, @@ -1597,6 +1600,7 @@ fn lod_stub_wire_3d( } let stored_color = if selected { WireModel::SELECTED } else { color }; WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/scene/convert/tessellate.rs b/src/scene/convert/tessellate.rs index 243958bbd..3980d7103 100644 --- a/src/scene/convert/tessellate.rs +++ b/src/scene/convert/tessellate.rs @@ -145,6 +145,7 @@ fn point_cloud_wires( }; let (points, points_low) = points_to_ds(body_points); let mut wires = vec![WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, @@ -500,6 +501,7 @@ pub fn tessellate( None }; elem_wires.push(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, @@ -807,6 +809,7 @@ pub fn tessellate( ftl.push(lo); } wires.push(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, @@ -856,6 +859,7 @@ pub fn tessellate( fpl.push(lo); } wires.push(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, @@ -907,6 +911,7 @@ pub fn tessellate( low.push(ll); } wires.push(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, @@ -939,6 +944,7 @@ pub fn tessellate( }); } wires.push(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, @@ -1001,6 +1007,7 @@ pub fn tessellate( (Vec::new(), Vec::new(), Vec::new()) }; out.push(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, @@ -1045,6 +1052,7 @@ pub fn tessellate( (Vec::new(), Vec::new(), Vec::new()) }; out.push(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, @@ -1086,6 +1094,7 @@ pub fn tessellate( // are empty → the early-return path above). if !sdf_verts.is_empty() { out.push(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, @@ -1120,6 +1129,7 @@ pub fn tessellate( if out.is_empty() { out.push(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, @@ -1188,6 +1198,7 @@ pub fn tessellate( .map(|[kx, ky, kz]| [kx, ky, kz]) .collect(); return vec![WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, @@ -1310,20 +1321,12 @@ pub fn tessellate( } else { (Vec::new(), Vec::new(), Vec::new()) }; - let relative_marker = + let point_marker = crate::entities::point::relative_marker_spec(entity, document); - let (taper_widths, world_width) = if let Some((percent, normal)) = relative_marker { - // A negative width is reserved for viewport-relative - // point markers. The first three taper entries carry - // the marker-plane normal; normal taper processing is - // disabled for the sentinel by the wire uploader. - (normal.to_vec(), -percent) - } else { - (Vec::new(), polyline_band_width(entity)) - }; out.push(WireModel { - taper_widths, - world_width, + point_marker, + taper_widths: Vec::new(), + world_width: polyline_band_width(entity), depth_override: None, display_visible: true, plot_visible: true, @@ -1365,6 +1368,7 @@ pub fn tessellate( (Vec::new(), Vec::new(), Vec::new()) }; out.push(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, pick_tris: Vec::new(), @@ -1399,6 +1403,7 @@ pub fn tessellate( if out.is_empty() { out.push(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, @@ -1446,6 +1451,7 @@ pub fn tessellate( // treatment as the Contour arm, restarting the dash per segment. let (pick_tris, pick_tris_low) = points_to_ds(te.pick_tris); return vec![WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: polyline_band_width(entity), depth_override: None, @@ -1494,6 +1500,7 @@ pub fn tessellate( let (pick_tris, pick_tris_low) = points_to_ds(te.pick_tris); let world_width = widths.iter().copied().fold(0.0f32, f32::max); return vec![WireModel { + point_marker: None, taper_widths: widths, world_width, depth_override: None, @@ -1615,6 +1622,7 @@ pub fn tessellate( _ => (Vec::new(), Vec::new()), }; vec![WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/scene/mod.rs b/src/scene/mod.rs index db72b0084..63cc998ea 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -7359,14 +7359,30 @@ impl Scene { &self, wires: &Arc>, base_radius_px: f32, + viewport_height_px: f32, + include_line_weight: bool, ) -> f32 { if let Some((base_epoch, base, changes)) = self.interaction_overlay_base() { let (_, changed) = self.interaction_overlay_changed_index(base_epoch, &changes); - base.pick_radius_px(changed.pick_radius_px(base_radius_px)) + base.pick_radius_px( + changed.pick_radius_px( + base_radius_px, + viewport_height_px, + include_line_weight, + ), + viewport_height_px, + include_line_weight, + ) } else { self.cached_interaction_index(wires) - .map_or(base_radius_px, |index| index.pick_radius_px(base_radius_px)) + .map_or(base_radius_px, |index| { + index.pick_radius_px( + base_radius_px, + viewport_height_px, + include_line_weight, + ) + }) } } @@ -7524,11 +7540,12 @@ impl Scene { { return crate::scene::pick::interaction_index::InteractionCandidates::all(wires); } - let radius_px = if include_line_weight { - self.indexed_interaction_pick_radius(&wires, radius_px) - } else { - radius_px - }; + let radius_px = self.indexed_interaction_pick_radius( + &wires, + radius_px, + bounds.height, + include_line_weight, + ); let flat_ortho = view_rot.z_axis.x.abs() < 1e-9 && view_rot.z_axis.y.abs() < 1e-9 && (view_rot.w_axis.w - 1.0).abs() < 1e-6; @@ -7597,12 +7614,35 @@ impl Scene { { return crate::scene::pick::interaction_index::InteractionCandidates::all(wires); } + let pad_px = + self.indexed_interaction_pick_radius(&wires, 0.0, bounds.height, true); if flat_ortho { - self.indexed_interaction_candidates_xy(wires, aabb, false) + let world_x_px = ((view_rot.x_axis.x * bounds.width * 0.5).powi(2) + + (view_rot.x_axis.y * bounds.height * 0.5).powi(2)) + .sqrt(); + let world_y_px = ((view_rot.y_axis.x * bounds.width * 0.5).powi(2) + + (view_rot.y_axis.y * bounds.height * 0.5).powi(2)) + .sqrt(); + let scale = world_x_px.min(world_y_px); + let pad = if scale > 1e-6 { + pad_px as f64 / scale as f64 + } else { + 0.0 + }; + self.indexed_interaction_candidates_xy( + wires, + [aabb[0] - pad, aabb[1] - pad, aabb[2] + pad, aabb[3] + pad], + false, + ) } else { self.indexed_interaction_candidates_screen( wires, - screen_rect, + [ + screen_rect[0] - pad_px, + screen_rect[1] - pad_px, + screen_rect[2] + pad_px, + screen_rect[3] + pad_px, + ], view_rot, eye, bounds, diff --git a/src/scene/model/wire_model.rs b/src/scene/model/wire_model.rs index 87f8a894c..5e16de1fd 100644 --- a/src/scene/model/wire_model.rs +++ b/src/scene/model/wire_model.rs @@ -49,6 +49,26 @@ pub enum TangentGeom { }, } +/// Viewport-relative display transform for a point marker. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct PointMarker { + pub origin: glam::DVec3, + pub normal: glam::DVec3, + pub axis_x: glam::DVec3, + pub axis_y: glam::DVec3, + pub viewport_percent: f32, +} + +impl PointMarker { + pub fn resolve(self, point: glam::DVec3, view_height: f64) -> glam::DVec3 { + let normal = self.normal.normalize_or(glam::DVec3::Z); + let delta = point - self.origin; + let axial = normal * delta.dot(normal); + let scale = self.viewport_percent.max(0.0) as f64 * 0.01 * view_height.max(0.0); + self.origin + axial + (delta - axial) * scale + } +} + /// A 1-D entity (line, arc, polyline) represented as an ordered set of /// world-space points rendered as a quad strip (TriangleList). /// @@ -66,6 +86,8 @@ pub struct WireModel { /// "all-zero residual" (interactive draw / preview wires whose coordinates /// don't need sub-f32 precision). Tessellation from CAD f64 fills it. pub points_low: Vec<[f32; 3]>, + /// Point-marker scaling shared by the GPU, picking, and plotting paths. + pub point_marker: Option, /// RGBA colour in [0, 1]. pub color: [f32; 4], /// Whether this wire is currently selected. @@ -225,6 +247,7 @@ impl WireModel { /// Create a solid wire (no dash pattern, 1px weight). pub fn solid(name: String, points: Vec<[f32; 3]>, color: [f32; 4], selected: bool) -> Self { Self { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, @@ -263,10 +286,11 @@ impl WireModel { out.name = format!("preview_{}", self.name); out.color = Self::CYAN; out.selected = false; - for p in &mut out.points { - p[0] += delta.x; - p[1] += delta.y; - p[2] += delta.z; + map_points(&mut out.points, &mut out.points_low, |point| { + point + delta.as_dvec3() + }); + if let Some(marker) = &mut out.point_marker { + marker.origin += delta.as_dvec3(); } if !out.text_verts.is_empty() { let (dx, dy, dz) = (delta.x as f64, delta.y as f64, delta.z as f64); @@ -301,6 +325,17 @@ impl WireModel { for p in &mut out.points_low { *p = (rotation * glam::Vec3::from_array(*p)).to_array(); } + if let Some(marker) = &mut out.point_marker { + let center = center.as_dvec3(); + let rotation = glam::DQuat::from_axis_angle( + axis.as_dvec3().normalize_or(glam::DVec3::Z), + angle_rad as f64, + ); + marker.origin = center + rotation * (marker.origin - center); + marker.normal = (rotation * marker.normal).normalize_or(glam::DVec3::Z); + marker.axis_x = (rotation * marker.axis_x).normalize_or(glam::DVec3::X); + marker.axis_y = (rotation * marker.axis_y).normalize_or(glam::DVec3::Y); + } if !out.text_verts.is_empty() { let center = center.as_dvec3(); let axis = axis.as_dvec3().normalize_or_zero(); @@ -323,10 +358,23 @@ impl WireModel { out.name = format!("preview_{}", self.name); out.color = Self::CYAN; out.selected = false; - for p in &mut out.points { - p[0] = center.x + (p[0] - center.x) * factor; - p[1] = center.y + (p[1] - center.y) * factor; - p[2] = center.z + (p[2] - center.z) * factor; + if let Some(marker) = out.point_marker { + let center = center.as_dvec3(); + let normal = marker.normal.normalize_or(glam::DVec3::Z); + let target_origin = center + (marker.origin - center) * factor as f64; + map_points(&mut out.points, &mut out.points_low, |point| { + let delta = point - marker.origin; + let axial = normal * delta.dot(normal); + target_origin + (delta - axial) + axial * factor as f64 + }); + if let Some(target) = &mut out.point_marker { + target.origin = target_origin; + } + } else { + let center = center.as_dvec3(); + map_points(&mut out.points, &mut out.points_low, |point| { + center + (point - center) * factor as f64 + }); } if !out.text_verts.is_empty() { let (cx, cy, cz) = (center.x as f64, center.y as f64, center.z as f64); @@ -373,11 +421,22 @@ pub fn stretched_windows( }) }; - for p in &mut out.points { - if inside(p[0], p[1]) { - p[0] += delta.x; - p[1] += delta.y; - p[2] += delta.z; + if let Some(marker) = out.point_marker { + if inside(marker.origin.x as f32, marker.origin.y as f32) { + map_points(&mut out.points, &mut out.points_low, |point| { + point + delta.as_dvec3() + }); + if let Some(target) = &mut out.point_marker { + target.origin += delta.as_dvec3(); + } + } + } else { + for p in &mut out.points { + if inside(p[0], p[1]) { + p[0] += delta.x; + p[1] += delta.y; + p[2] += delta.z; + } } } @@ -438,6 +497,20 @@ pub fn stretched_windows( let point = glam::Vec3::from_array(*p); *p = (point - 2.0 * plane_normal.dot(point) * plane_normal).to_array(); } + if let Some(marker) = &mut out.point_marker { + let p1 = p1.as_dvec3(); + let normal = plane_normal.as_dvec3(); + marker.origin -= 2.0 * normal.dot(marker.origin - p1) * normal; + marker.normal = + (marker.normal - 2.0 * normal.dot(marker.normal) * normal) + .normalize_or(glam::DVec3::Z); + marker.axis_x = + (marker.axis_x - 2.0 * normal.dot(marker.axis_x) * normal) + .normalize_or(glam::DVec3::X); + marker.axis_y = + (marker.axis_y - 2.0 * normal.dot(marker.axis_y) * normal) + .normalize_or(glam::DVec3::Y); + } // Glyph quads reflect wholesale (true mirror) — the caller only routes // text through here for MIRRTEXT-on; MIRRTEXT-off relocates via // `translated` so glyphs stay readable. @@ -466,6 +539,44 @@ pub fn stretched_windows( }) .sum() } + + pub fn point_world(&self, index: usize, view_height: f64) -> glam::DVec3 { + let high = self.points[index]; + let low = self.points_low.get(index).copied().unwrap_or([0.0; 3]); + let point = glam::DVec3::new( + high[0] as f64 + low[0] as f64, + high[1] as f64 + low[1] as f64, + high[2] as f64 + low[2] as f64, + ); + self.point_marker + .map_or(point, |marker| marker.resolve(point, view_height)) + } +} + +fn map_points( + points: &mut [[f32; 3]], + points_low: &mut Vec<[f32; 3]>, + map: impl Fn(glam::DVec3) -> glam::DVec3, +) { + let old_low = std::mem::take(points_low); + points_low.reserve(points.len()); + for (index, high) in points.iter_mut().enumerate() { + if !high[0].is_finite() || !high[1].is_finite() || !high[2].is_finite() { + points_low.push([0.0; 3]); + continue; + } + let low = old_low.get(index).copied().unwrap_or([0.0; 3]); + let mapped = map(glam::DVec3::new( + high[0] as f64 + low[0] as f64, + high[1] as f64 + low[1] as f64, + high[2] as f64 + low[2] as f64, + )); + let (hx, lx) = WireModel::split_ds(mapped.x); + let (hy, ly) = WireModel::split_ds(mapped.y); + let (hz, lz) = WireModel::split_ds(mapped.z); + *high = [hx, hy, hz]; + points_low.push([lx, ly, lz]); + } } /// Map every glyph vertex's double-single world position through `f`, re- @@ -502,6 +613,7 @@ pub(crate) fn map_text_verts( impl Default for WireModel { fn default() -> Self { Self { + point_marker: None, text_verts: Vec::new(), name: String::new(), points: Vec::new(), diff --git a/src/scene/pick/hit_test.rs b/src/scene/pick/hit_test.rs index 42f1c9561..857ff942f 100644 --- a/src/scene/pick/hit_test.rs +++ b/src/scene/pick/hit_test.rs @@ -208,6 +208,31 @@ pub fn click_hit<'a, W: WireSource + ?Sized>( let mut best_dist = f32::MAX; let mut best: Option<&str> = None; + for wire in wires.iter().filter(|wire| wire.point_marker.is_some()) { + let tolerance = pick_tolerance_px(wire, lw_display, base_radius_px); + let mut previous = None; + for (index, point) in wire.points.iter().enumerate() { + if !point[0].is_finite() { + previous = None; + continue; + } + let screen = world_to_screen( + wire_point_world(wire, index, view_rot, eye), + view_rot, + eye, + bounds, + ); + if let Some(start) = previous { + let distance = dist_point_to_segment(cursor, start, screen); + if distance < tolerance && distance < best_dist { + best_dist = distance; + best = Some(wire.name.as_str()); + } + } + previous = Some(screen); + } + } + // World z only shifts the *screen* x/y when the view is tilted (orbit / // perspective). In the flat top-down ortho view — the case where hover lag // on large drawings actually bites — a wire's screen position depends only @@ -224,18 +249,21 @@ pub fn click_hit<'a, W: WireSource + ?Sized>( let Some(wire) = wires.source_wire(segment.wire) else { continue; }; + if wire.point_marker.is_some() { + continue; + } let start = segment.start as usize; if start + 1 >= wire.points.len() { continue; } let p0 = world_to_screen( - wp64(wire.points[start], &wire.points_low, start), + wire_point_world(wire, start, view_rot, eye), view_rot, eye, bounds, ); let p1 = world_to_screen( - wp64(wire.points[start + 1], &wire.points_low, start + 1), + wire_point_world(wire, start + 1, view_rot, eye), view_rot, eye, bounds, @@ -249,23 +277,27 @@ pub fn click_hit<'a, W: WireSource + ?Sized>( } else { // Q: lazy projection — no Vec allocation per wire; NaN resets the segment chain. for wire in wires.iter() { + if wire.point_marker.is_some() { + continue; + } let tol = pick_tolerance_px(wire, lw_display, base_radius_px); // Cheap AABB pre-reject (flat view only; never for the unbounded // sentinel used by previews / greeked text). if z_flat + && wire.point_marker.is_none() && wire.aabb != WireModel::UNBOUNDED_AABB && aabb_rejects(wire.aabb, cursor, tol, view_rot, eye, bounds) { continue; } let mut prev: Option = None; - for (i, &[px, py, pz]) in wire.points.iter().enumerate() { + for (i, &[px, _, _]) in wire.points.iter().enumerate() { if px.is_nan() { prev = None; continue; } let cur = world_to_screen( - wp64([px, py, pz], &wire.points_low, i), + wire_point_world(wire, i, view_rot, eye), view_rot, eye, bounds, @@ -450,24 +482,55 @@ pub fn click_hits_all<'a, W: WireSource + ?Sized>( return Vec::new(); } let mut hits: Vec<(f32, &str)> = Vec::new(); + let mut marker_hits: HashMap<&str, f32> = HashMap::default(); + for wire in wires.iter().filter(|wire| wire.point_marker.is_some()) { + let tolerance = pick_tolerance_px(wire, lw_display, base_radius_px); + let mut previous = None; + for (index, point) in wire.points.iter().enumerate() { + if !point[0].is_finite() { + previous = None; + continue; + } + let screen = world_to_screen( + wire_point_world(wire, index, view_rot, eye), + view_rot, + eye, + bounds, + ); + if let Some(start) = previous { + let distance = dist_point_to_segment(cursor, start, screen); + if distance < tolerance { + marker_hits + .entry(wire.name.as_str()) + .and_modify(|best| *best = best.min(distance)) + .or_insert(distance); + } + } + previous = Some(screen); + } + } + hits.extend(marker_hits.into_iter().map(|(name, distance)| (distance, name))); if let Some(segments) = wires.segments() { let mut best_by_wire: HashMap = HashMap::default(); for segment in segments { let Some(wire) = wires.source_wire(segment.wire) else { continue; }; + if wire.point_marker.is_some() { + continue; + } let start = segment.start as usize; if start + 1 >= wire.points.len() { continue; } let p0 = world_to_screen( - wp64(wire.points[start], &wire.points_low, start), + wire_point_world(wire, start, view_rot, eye), view_rot, eye, bounds, ); let p1 = world_to_screen( - wp64(wire.points[start + 1], &wire.points_low, start + 1), + wire_point_world(wire, start + 1, view_rot, eye), view_rot, eye, bounds, @@ -487,17 +550,20 @@ pub fn click_hits_all<'a, W: WireSource + ?Sized>( })); } else { for wire in wires.iter() { + if wire.point_marker.is_some() { + continue; + } let tol = pick_tolerance_px(wire, lw_display, base_radius_px); let mut prev: Option = None; let mut best_for_wire = tol; let mut hit = false; - for (i, &[px, py, pz]) in wire.points.iter().enumerate() { + for (i, &[px, _, _]) in wire.points.iter().enumerate() { if px.is_nan() { prev = None; continue; } let cur = world_to_screen( - wp64([px, py, pz], &wire.points_low, i), + wire_point_world(wire, i, view_rot, eye), view_rot, eye, bounds, @@ -1104,13 +1170,13 @@ fn indexed_box_crossing_hits<'a, W: WireSource + ?Sized>( continue; } let a = world_to_screen( - wp64(wire.points[start], &wire.points_low, start), + wire_point_world(wire, start, view_rot, eye), view_rot, eye, bounds, ); let b = world_to_screen( - wp64(wire.points[start + 1], &wire.points_low, start + 1), + wire_point_world(wire, start + 1, view_rot, eye), view_rot, eye, bounds, @@ -1119,6 +1185,14 @@ fn indexed_box_crossing_hits<'a, W: WireSource + ?Sized>( out.push(wire.name.as_str()); } } + for wire in wires.iter().filter(|wire| wire.point_marker.is_some()) { + if !seen.contains(wire.name.as_str()) + && marker_segment_hit(wire, view_rot, eye, bounds, |a, b| segment_hits(a, b)) + && seen.insert(wire.name.as_str()) + { + out.push(wire.name.as_str()); + } + } for (triangles, pick_only) in [ (wires.fill_triangles().unwrap_or_default(), false), (wires.pick_triangles().unwrap_or_default(), true), @@ -1181,7 +1255,7 @@ fn indexed_box_crossing_hits<'a, W: WireSource + ?Sized>( if wire.points.iter().enumerate().any(|(index, &point)| { point[0].is_finite() && inside(world_to_screen( - wp64(point, &wire.points_low, index), + wire_point_world(wire, index, view_rot, eye), view_rot, eye, bounds, @@ -1231,13 +1305,13 @@ pub fn poly_fence_hit<'a, W: WireSource + ?Sized>( continue; } let a = world_to_screen( - wp64(wire.points[start], &wire.points_low, start), + wire_point_world(wire, start, view_rot, eye), view_rot, eye, bounds, ); let b = world_to_screen( - wp64(wire.points[start + 1], &wire.points_low, start + 1), + wire_point_world(wire, start + 1, view_rot, eye), view_rot, eye, bounds, @@ -1246,6 +1320,14 @@ pub fn poly_fence_hit<'a, W: WireSource + ?Sized>( out.push(wire.name.as_str()); } } + for wire in wires.iter().filter(|wire| wire.point_marker.is_some()) { + if !seen.contains(wire.name.as_str()) + && marker_segment_hit(wire, view_rot, eye, bounds, |a, b| cuts(a, b)) + && seen.insert(wire.name.as_str()) + { + out.push(wire.name.as_str()); + } + } return out; } for wire in wires.iter() { @@ -1254,13 +1336,13 @@ pub fn poly_fence_hit<'a, W: WireSource + ?Sized>( } let hit = (0..wire.points.len() - 1).any(|k| { let a = world_to_screen( - wp64(wire.points[k], &wire.points_low, k), + wire_point_world(wire, k, view_rot, eye), view_rot, eye, bounds, ); let b = world_to_screen( - wp64(wire.points[k + 1], &wire.points_low, k + 1), + wire_point_world(wire, k + 1, view_rot, eye), view_rot, eye, bounds, @@ -1292,13 +1374,13 @@ fn indexed_polygon_crossing_hits<'a, W: WireSource + ?Sized>( continue; } let a = world_to_screen( - wp64(wire.points[start], &wire.points_low, start), + wire_point_world(wire, start, view_rot, eye), view_rot, eye, bounds, ); let b = world_to_screen( - wp64(wire.points[start + 1], &wire.points_low, start + 1), + wire_point_world(wire, start + 1, view_rot, eye), view_rot, eye, bounds, @@ -1311,6 +1393,18 @@ fn indexed_polygon_crossing_hits<'a, W: WireSource + ?Sized>( out.push(wire.name.as_str()); } } + for wire in wires.iter().filter(|wire| wire.point_marker.is_some()) { + if !seen.contains(wire.name.as_str()) + && marker_segment_hit(wire, view_rot, eye, bounds, |a, b| { + point_in_polygon(a, poly) + || point_in_polygon(b, poly) + || segment_crosses_polygon(a, b, poly) + }) + && seen.insert(wire.name.as_str()) + { + out.push(wire.name.as_str()); + } + } for (triangles, pick_only) in [ (wires.fill_triangles().unwrap_or_default(), false), (wires.pick_triangles().unwrap_or_default(), true), @@ -1371,7 +1465,12 @@ fn indexed_polygon_crossing_hits<'a, W: WireSource + ?Sized>( if wire.points.iter().enumerate().any(|(index, &point)| { point[0].is_finite() && point_in_polygon( - world_to_screen(wp64(point, &wire.points_low, index), view_rot, eye, bounds), + world_to_screen( + wire_point_world(wire, index, view_rot, eye), + view_rot, + eye, + bounds, + ), poly, ) }) && seen.insert(wire.name.as_str()) @@ -1473,7 +1572,12 @@ pub fn box_hit<'a, W: WireSource + ?Sized>( prev = None; continue; } - let sp = world_to_screen(wp64([px, py, pz], low, i), view_rot, eye, bounds); + let world = if !wire.points.is_empty() { + wire_point_world(wire, i, view_rot, eye) + } else { + wp64([px, py, pz], low, i) + }; + let sp = world_to_screen(world, view_rot, eye, bounds); if crossing { if inside(sp) { hit = true; @@ -1595,7 +1699,12 @@ pub fn poly_hit<'a, W: WireSource + ?Sized>( prev = None; continue; } - let sp = world_to_screen(wp64([px, py, pz], low, i), view_rot, eye, bounds); + let world = if !wire.points.is_empty() { + wire_point_world(wire, i, view_rot, eye) + } else { + wp64([px, py, pz], low, i) + }; + let sp = world_to_screen(world, view_rot, eye, bounds); // Reject points the GPU scissored out of a floating viewport so // the lasso can't reach clipped geometry. No-op in model space. if sp.x < 0.0 || sp.x > bounds.width || sp.y < 0.0 || sp.y > bounds.height { @@ -1694,6 +1803,53 @@ fn wp64(hi: [f32; 3], low: &[[f32; 3]], i: usize) -> glam::DVec3 { ) } +fn wire_point_world( + wire: &WireModel, + index: usize, + view_rot: Mat4, + eye: glam::DVec3, +) -> glam::DVec3 { + let Some(marker) = wire.point_marker else { + return wire.point_world(index, 0.0); + }; + let row_y = glam::DVec3::new( + view_rot.x_axis.y as f64, + view_rot.y_axis.y as f64, + view_rot.z_axis.y as f64, + ); + let projection_scale = row_y.length().max(1.0e-12); + let relative = (marker.origin - eye).as_vec3().extend(1.0); + let clip_w = (view_rot * relative).w.abs().max(1.0e-6); + wire.point_world(index, 2.0 * clip_w as f64 / projection_scale) +} + +fn marker_segment_hit( + wire: &WireModel, + view_rot: Mat4, + eye: glam::DVec3, + bounds: Rectangle, + mut hit: impl FnMut(Point, Point) -> bool, +) -> bool { + let mut previous = None; + for (index, point) in wire.points.iter().enumerate() { + if !point[0].is_finite() { + previous = None; + continue; + } + let screen = world_to_screen( + wire_point_world(wire, index, view_rot, eye), + view_rot, + eye, + bounds, + ); + if previous.is_some_and(|start| hit(start, screen)) { + return true; + } + previous = Some(screen); + } + false +} + /// Even-odd ray-casting test: is `p` inside the polygon? /// /// Handles multi-path boundaries: NaN points (used as path separators by diff --git a/src/scene/pick/interaction_index.rs b/src/scene/pick/interaction_index.rs index 99a220a83..4e1f6987a 100644 --- a/src/scene/pick/interaction_index.rs +++ b/src/scene/pick/interaction_index.rs @@ -557,7 +557,9 @@ pub struct InteractionIndex { pick_triangles: SpatialSet, glyphs: SpatialSet, unbounded_wires: Vec, + screen_unbounded_wires: Vec, max_line_half_width_px: f32, + max_marker_radius_fraction: f32, } pub struct InteractionHandleIndex { @@ -574,7 +576,9 @@ struct WireIndexEntries { pick_triangles: Vec>, glyphs: Vec>, unbounded: bool, + screen_unbounded: bool, max_line_half_width_px: f32, + max_marker_radius_fraction: f32, } fn collect_wire_index_entries(wire_idx: u32, wire: &WireModel) -> WireIndexEntries { @@ -591,28 +595,34 @@ fn collect_wire_index_entries(wire_idx: u32, wire: &WireModel) -> WireIndexEntri pick_triangles: Vec::with_capacity(wire.pick_tris.len() / 3), glyphs: Vec::with_capacity(wire.text_verts.len() / 6), unbounded: false, + screen_unbounded: wire.point_marker.is_some(), max_line_half_width_px: if wire.line_weight_px.is_finite() { (wire.line_weight_px * 0.5).max(0.0) } else { 0.0 }, + max_marker_radius_fraction: wire.point_marker.map_or(0.0, |marker| { + marker.viewport_percent.max(0.0) * 0.01 * std::f32::consts::SQRT_2 + }), }; entries.unbounded = entries.wire.is_none(); - for start in 0..wire.points.len().saturating_sub(1) { - let Some(aabb) = points_aabb3([ - wire_point(wire, start), - wire_point(wire, start + 1), - ]) else { - continue; - }; - entries.segments.push(Entry3 { - aabb, - value: SegmentRef { - wire: wire_idx, - start: start as u32, - }, - }); + if wire.point_marker.is_none() { + for start in 0..wire.points.len().saturating_sub(1) { + let Some(aabb) = points_aabb3([ + wire_point(wire, start), + wire_point(wire, start + 1), + ]) else { + continue; + }; + entries.segments.push(Entry3 { + aabb, + value: SegmentRef { + wire: wire_idx, + start: start as u32, + }, + }); + } } for (index, (point, _)) in wire.snap_pts.iter().enumerate() { if point.is_finite() { @@ -773,7 +783,9 @@ impl InteractionIndex { let collect_started = iced::time::Instant::now(); let mut wire_entries = Vec::with_capacity(wires.len()); let mut unbounded_wires = Vec::new(); + let mut screen_unbounded_wires = Vec::new(); let mut max_line_half_width_px = 0.0f32; + let mut max_marker_radius_fraction = 0.0f32; #[cfg(not(target_arch = "wasm32"))] let per_wire: Vec = { @@ -804,12 +816,17 @@ impl InteractionIndex { for (wire_idx, entries) in per_wire.into_iter().enumerate() { max_line_half_width_px = max_line_half_width_px.max(entries.max_line_half_width_px); + max_marker_radius_fraction = + max_marker_radius_fraction.max(entries.max_marker_radius_fraction); if let Some(entry) = entries.wire { wire_entries.push(entry); } if entries.unbounded { unbounded_wires.push(wire_idx as u32); } + if entries.screen_unbounded { + screen_unbounded_wires.push(wire_idx as u32); + } segment_parts.push(entries.segments); snap_point_parts.push(entries.snap_points); key_vertex_parts.push(entries.key_vertices); @@ -952,7 +969,9 @@ impl InteractionIndex { pick_triangles, glyphs, unbounded_wires, + screen_unbounded_wires, max_line_half_width_px, + max_marker_radius_fraction, } } @@ -1002,12 +1021,30 @@ impl InteractionIndex { } } - pub fn pick_radius_px(&self, base_radius_px: f32) -> f32 { - base_radius_px.max(self.max_line_half_width_px) + pub fn pick_radius_px( + &self, + base_radius_px: f32, + viewport_height_px: f32, + include_line_weight: bool, + ) -> f32 { + let radius = base_radius_px + .max(self.max_marker_radius_fraction * viewport_height_px.max(0.0)); + if include_line_weight { + radius.max(self.max_line_half_width_px) + } else { + radius + } } - fn queried_wire_keys(&self, mut indices: Vec) -> Vec<(u64, u32)> { + fn queried_wire_keys( + &self, + mut indices: Vec, + include_screen_unbounded: bool, + ) -> Vec<(u64, u32)> { indices.extend_from_slice(&self.unbounded_wires); + if include_screen_unbounded { + indices.extend_from_slice(&self.screen_unbounded_wires); + } let mut keys: Vec<(u64, u32)> = indices .into_iter() .filter_map(|index| { @@ -1029,7 +1066,7 @@ impl InteractionIndex { } pub fn query_wire_keys_xy(&self, aabb: [f64; 4]) -> Vec<(u64, u32)> { - self.queried_wire_keys(self.wires.query_xy(aabb)) + self.queried_wire_keys(self.wires.query_xy(aabb), false) } pub fn query_wire_keys_screen( @@ -1039,7 +1076,10 @@ impl InteractionIndex { eye: DVec3, bounds: Rectangle, ) -> Vec<(u64, u32)> { - self.queried_wire_keys(self.wires.query_screen(screen_rect, view_rot, eye, bounds)) + self.queried_wire_keys( + self.wires.query_screen(screen_rect, view_rot, eye, bounds), + true, + ) } fn remap_wire_index( @@ -1064,8 +1104,12 @@ impl InteractionIndex { &self, mut indices: Vec, slots: &rustc_hash::FxHashMap<(u64, u32), u32>, + include_screen_unbounded: bool, ) -> Vec { indices.extend_from_slice(&self.unbounded_wires); + if include_screen_unbounded { + indices.extend_from_slice(&self.screen_unbounded_wires); + } let mut local: Vec = indices .into_iter() .filter_map(|index| self.remap_wire_index(index, slots)) @@ -1100,7 +1144,11 @@ impl InteractionIndex { ) -> InteractionCandidates { InteractionCandidates { wires, - wire_indices: Some(self.remap_wire_indices(self.wires.query_xy(aabb), slots)), + wire_indices: Some(self.remap_wire_indices( + self.wires.query_xy(aabb), + slots, + false, + )), segments: Some(self.remap_refs( self.segments.query_xy(aabb), slots, @@ -1164,6 +1212,7 @@ impl InteractionIndex { self.wires .query_screen(screen_rect, view_rot, eye, bounds), slots, + true, )), segments: Some(self.remap_refs( self.segments @@ -1251,6 +1300,7 @@ impl InteractionIndex { ) -> InteractionCandidates { let mut wire_indices = self.wires.query_screen(screen_rect, view_rot, eye, bounds); wire_indices.extend_from_slice(&self.unbounded_wires); + wire_indices.extend_from_slice(&self.screen_unbounded_wires); wire_indices.sort_unstable(); wire_indices.dedup(); InteractionCandidates { @@ -1601,6 +1651,26 @@ impl<'a> Iterator for WireIter<'a> { } fn finite_wire_aabb3(wire: &WireModel) -> Option<[f64; 6]> { + if let Some(marker) = wire.point_marker { + if !marker.origin.is_finite() { + return None; + } + let normal = marker.normal.try_normalize().unwrap_or(DVec3::Z); + let mut min_axial = 0.0f64; + let mut max_axial = 0.0f64; + for index in 0..wire.points.len() { + let point = DVec3::from_array(wire_point(wire, index)); + if point.is_finite() { + let axial = (point - marker.origin).dot(normal); + min_axial = min_axial.min(axial); + max_axial = max_axial.max(axial); + } + } + return points_aabb3([ + (marker.origin + normal * min_axial).to_array(), + (marker.origin + normal * max_axial).to_array(), + ]); + } let [min_x, min_y, max_x, max_y] = wire.aabb; if !min_x.is_finite() || !min_y.is_finite() || !max_x.is_finite() || !max_y.is_finite() { return None; diff --git a/src/scene/pick/xclip.rs b/src/scene/pick/xclip.rs index da47cf084..2a060f135 100644 --- a/src/scene/pick/xclip.rs +++ b/src/scene/pick/xclip.rs @@ -14,6 +14,7 @@ use acadrust::entities::Insert; use acadrust::objects::{ObjectType, SpatialFilter}; use acadrust::types::{Handle, Transform, Vector3}; use acadrust::CadDocument; +use cadkernel::geom2d::{contains, Curve, Line, Tolerance}; use crate::scene::model::wire_model::WireModel; @@ -116,6 +117,12 @@ pub fn clip_wires(wires: &mut Vec, poly: &[[f64; 2]]) { .iter() .map(|&[x, y]| [(x - rx) as f32, (y - ry) as f32]) .collect(); + let boundary: Vec = poly + .iter() + .zip(poly.iter().cycle().skip(1)) + .take(poly.len()) + .map(|(&start, &end)| Curve::Line(Line { start, end })) + .collect(); // Reconstruct an absolute-f64 wire point from its high/low pair, NaN-safe. let abs = |hi: [f32; 3], lo: [f32; 3]| -> [f64; 3] { @@ -127,7 +134,18 @@ pub fn clip_wires(wires: &mut Vec, poly: &[[f64; 2]]) { }; for w in wires.iter_mut() { - if !w.points.is_empty() { + let keep_marker = w.point_marker.map(|marker| { + contains( + &boundary, + [marker.origin.x, marker.origin.y], + Tolerance::default(), + ) + }); + if keep_marker == Some(false) { + w.points.clear(); + w.points_low.clear(); + w.point_marker = None; + } else if keep_marker.is_none() && !w.points.is_empty() { // Absolute → local f32 (NaN separators preserved). let local: Vec<[f32; 3]> = (0..w.points.len()) .map(|i| { @@ -257,6 +275,7 @@ pub fn frame_wire( lo.push([lx, ly, 0.0]); } WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/scene/pipeline/wire_gpu.rs b/src/scene/pipeline/wire_gpu.rs index 3f3efb798..7187320de 100644 --- a/src/scene/pipeline/wire_gpu.rs +++ b/src/scene/pipeline/wire_gpu.rs @@ -521,29 +521,18 @@ fn finite3(p: [f32; 3]) -> bool { } fn marker_metadata(wire: &WireModel) -> ([f32; 4], [f32; 4], [f32; 4]) { - if wire.world_width >= 0.0 { + let Some(marker) = wire.point_marker else { return ([0.0; 4], [0.0; 4], [0.0; 4]); - } - let origin = wire - .snap_pts - .iter() - .find_map(|(point, hint)| { - matches!(hint, crate::scene::model::wire_model::SnapHint::Node).then_some(*point) - }) - .unwrap_or(glam::DVec3::ZERO); + }; + let origin = marker.origin; let (hx, lx) = WireModel::split_ds(origin.x); let (hy, ly) = WireModel::split_ds(origin.y); let (hz, lz) = WireModel::split_ds(origin.z); - let normal = glam::Vec3::new( - wire.taper_widths.first().copied().unwrap_or(0.0), - wire.taper_widths.get(1).copied().unwrap_or(0.0), - wire.taper_widths.get(2).copied().unwrap_or(1.0), - ) - .normalize_or(glam::Vec3::Z); + let normal = marker.normal.as_vec3().normalize_or(glam::Vec3::Z); ( [hx, hy, hz, 0.0], [lx, ly, lz, 0.0], - [normal.x, normal.y, normal.z, -wire.world_width], + [normal.x, normal.y, normal.z, marker.viewport_percent], ) } @@ -565,13 +554,7 @@ pub(crate) fn emit_wire_packed( let (dists, align_end, align_total) = wire_distances(wire); let (marker_origin_high, marker_origin_low, marker_normal_scale) = marker_metadata(wire); let low = |i: usize| -> [f32; 3] { wire.points_low.get(i).copied().unwrap_or([0.0; 3]) }; - let tw = |i: usize| -> f32 { - if wire.world_width < 0.0 { - 0.0 - } else { - wire.taper_widths.get(i).copied().unwrap_or(0.0) * 0.5 - } - }; + let tw = |i: usize| -> f32 { wire.taper_widths.get(i).copied().unwrap_or(0.0) * 0.5 }; let mut instances: Vec = Vec::with_capacity(seg_count); for i in 0..seg_count { let a = wire.points[i]; @@ -594,7 +577,7 @@ pub(crate) fn emit_wire_packed( draw_depth, align_end, align_total, - world_half_width: wire.world_width.max(0.0) * 0.5, + world_half_width: wire.world_width * 0.5, world_hw_a: tw(i), world_hw_b: tw(i + 1), marker_origin_high, @@ -624,7 +607,7 @@ pub(crate) fn emit_wire_native( draw_depth, align_end, align_total, - world_half_width: wire.world_width.max(0.0) * 0.5, + world_half_width: wire.world_width * 0.5, _pad1: 0.0, _pad2: 0.0, marker_origin_high, diff --git a/src/scene/project.rs b/src/scene/project.rs index e1b4ff775..0c687a771 100644 --- a/src/scene/project.rs +++ b/src/scene/project.rs @@ -160,6 +160,22 @@ impl Scene { let in_vp = |x: f32, y: f32| x >= vp_x0 && x <= vp_x1 && y >= vp_y0 && y <= vp_y1; for wire in model_wires.iter() { + let marker_view_height = wire.point_marker.map_or(view_height_eff, |marker| { + if !use_perspective { + return view_height_eff; + } + let relative = marker.origin + - glam::DVec3::new( + display_center_x, + display_center_y, + display_center_z, + ); + let depth = relative.x * view_fwd_d.0 + + relative.y * view_fwd_d.1 + + relative.z * view_fwd_d.2; + view_height_eff * (camera_dist_d - depth).max(0.001) + / camera_dist_d.max(0.001) + }); let projected_pts: Vec<[f32; 3]> = wire .points .iter() @@ -168,11 +184,8 @@ impl Scene { if mx.is_nan() || my.is_nan() || mz.is_nan() { return [f32::NAN; 3]; } - // Reconstruct absolute WCS from the double-single high - // (`points`) + low (`points_low`) pair — the high f32 - // alone is ~0.5 m off at UTM scale. - let lo = wire.points_low.get(pi).copied().unwrap_or([0.0; 3]); - proj_abs(mx as f64 + lo[0] as f64, my as f64 + lo[1] as f64, mz as f64 + lo[2] as f64) + let point = wire.point_world(pi, marker_view_height); + proj_abs(point.x, point.y, point.z) }) .collect(); @@ -304,6 +317,7 @@ impl Scene { // residual is needed, and keeping the model wire's points_low // here would add a model-scale offset to the paper points. out.points_low = Vec::new(); + out.point_marker = None; out.snap_pts = snap_pts; out.key_vertices = key_vertices; // Tangent geometry is in model space and can't be trivially diff --git a/src/scene/text/complex_lt.rs b/src/scene/text/complex_lt.rs index 5d39e48a6..dfad53c7e 100644 --- a/src/scene/text/complex_lt.rs +++ b/src/scene/text/complex_lt.rs @@ -309,6 +309,7 @@ pub fn apply_along( .into_iter() .filter(|s| s.len() >= 2) .map(|pts| WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, @@ -355,6 +356,7 @@ pub fn apply_along( xy = xy.max(y); } out.push(WireModel { + point_marker: None, taper_widths: Vec::new(), world_width: 0.0, depth_override: None, diff --git a/src/shaders/block_wire.wgsl b/src/shaders/block_wire.wgsl index b357e7846..29e5bd256 100644 --- a/src/shaders/block_wire.wgsl +++ b/src/shaders/block_wire.wgsl @@ -88,8 +88,14 @@ fn marker_relative( let normal = normalize(wire_const.marker_normal_scale.xyz); let axial = normal * dot(delta, normal); let planar = delta - axial; - let world_size = wire_const.marker_normal_scale.w * 0.01 - * u.world_per_pixel * u.viewport_size.y; + let origin_clip = u.view_rot * vec4(origin_relative, 1.0); + let projection_scale = max(length(vec3( + u.view_rot[0].y, + u.view_rot[1].y, + u.view_rot[2].y, + )), 1e-12); + let view_height = 2.0 * max(abs(origin_clip.w), 1e-6) / projection_scale; + let world_size = wire_const.marker_normal_scale.w * 0.01 * view_height; return origin_relative + axial + planar * world_size; } diff --git a/src/shaders/wire.wgsl b/src/shaders/wire.wgsl index 2a974c46e..c8fac6521 100644 --- a/src/shaders/wire.wgsl +++ b/src/shaders/wire.wgsl @@ -116,7 +116,14 @@ fn marker_relative(position_high: vec3, position_low: vec3, instance: let normal = normalize(instance.marker_normal_scale.xyz); let axial = normal * dot(delta, normal); let planar = delta - axial; - let world_size = instance.marker_normal_scale.w * 0.01 * u.world_per_pixel * u.viewport_size.y; + let origin_clip = u.view_rot * vec4(origin_relative, 1.0); + let projection_scale = max(length(vec3( + u.view_rot[0].y, + u.view_rot[1].y, + u.view_rot[2].y, + )), 1e-12); + let view_height = 2.0 * max(abs(origin_clip.w), 1e-6) / projection_scale; + let world_size = instance.marker_normal_scale.w * 0.01 * view_height; return origin_relative + axial + planar * world_size; } diff --git a/src/shaders/wire_indexed.wgsl b/src/shaders/wire_indexed.wgsl index b638a5d74..c62d92bd6 100644 --- a/src/shaders/wire_indexed.wgsl +++ b/src/shaders/wire_indexed.wgsl @@ -95,7 +95,14 @@ fn marker_relative(position_high: vec3, position_low: vec3, c: WireCon let normal = normalize(c.marker_normal_scale.xyz); let axial = normal * dot(delta, normal); let planar = delta - axial; - let world_size = c.marker_normal_scale.w * 0.01 * u.world_per_pixel * u.viewport_size.y; + let origin_clip = u.view_rot * vec4(origin_relative, 1.0); + let projection_scale = max(length(vec3( + u.view_rot[0].y, + u.view_rot[1].y, + u.view_rot[2].y, + )), 1e-12); + let view_height = 2.0 * max(abs(origin_clip.w), 1e-6) / projection_scale; + let world_size = c.marker_normal_scale.w * 0.01 * view_height; return origin_relative + axial + planar * world_size; }