diff --git a/README.md b/README.md
index 0d87e7ba..8db51588 100644
--- a/README.md
+++ b/README.md
@@ -47,7 +47,7 @@
-
+
## Overview
@@ -66,7 +66,7 @@ The project is under active development. Keep backups of important production dr
- **Extensible workflows** — native plugins, command scripts, headless conversion, and a line-based JSON automation API.
-
+
## File workflows
diff --git a/docs/readme/README.ar.md b/docs/readme/README.ar.md
index 02f7e6c7..c9e70d2f 100644
--- a/docs/readme/README.ar.md
+++ b/docs/readme/README.ar.md
@@ -19,7 +19,7 @@
الانضمام إلى النقاش
-
+
## نظرة عامة
@@ -36,7 +36,7 @@ Open CAD Studio تطبيق متعدد المنصات للرسم التقني و
- **تصيير عبر GPU** — إطارات عرض ثنائية وثلاثية الأبعاد مسرّعة بواسطة `wgpu` مع كاميرات متعامدة ومنظورية.
- **سير عمل قابل للتوسعة** — إضافات أصلية ونصوص أوامر وتحويل من دون واجهة وواجهة JSON للأتمتة تعتمد على الأسطر.
-
+
## سير عمل الملفات
diff --git a/docs/readme/README.bg.md b/docs/readme/README.bg.md
index 27b40e06..9762e47e 100644
--- a/docs/readme/README.bg.md
+++ b/docs/readme/README.bg.md
@@ -21,7 +21,7 @@
Присъединете се към дискусията
-
+
## Общ преглед
@@ -38,7 +38,7 @@ Open CAD Studio е междуплатформено приложение за т
- **GPU визуализация** — ускорени 2D и 3D изгледи чрез `wgpu`, с ортографски и перспективни камери.
- **Разширяеми работни процеси** — локални приставки, командни скриптове, преобразуване без графичен интерфейс и редово ориентиран JSON API за автоматизация.
-
+
## Работа с файлове
diff --git a/docs/readme/README.cs.md b/docs/readme/README.cs.md
index d8cd178e..7fd0758a 100644
--- a/docs/readme/README.cs.md
+++ b/docs/readme/README.cs.md
@@ -19,7 +19,7 @@
Zapojit se do diskuse
-
+
## Přehled
@@ -36,7 +36,7 @@ Projekt je aktivně vyvíjen. Důležité produkční výkresy zálohujte a repr
- **Vykreslování přes GPU** — akcelerované 2D a 3D pohledy pomocí `wgpu`, s ortografickou a perspektivní kamerou.
- **Rozšiřitelné postupy** — nativní pluginy, příkazové skripty, bezobslužná konverze a řádkové JSON automatizační API.
-
+
## Práce se soubory
diff --git a/docs/readme/README.de.md b/docs/readme/README.de.md
index c5f065cc..3deabd15 100644
--- a/docs/readme/README.de.md
+++ b/docs/readme/README.de.md
@@ -19,7 +19,7 @@
An Diskussionen teilnehmen
-
+
## Überblick
@@ -36,7 +36,7 @@ Das Projekt wird aktiv entwickelt. Bewahre Sicherungskopien wichtiger Produktion
- **GPU-Darstellung** — beschleunigte 2D- und 3D-Ansichten über `wgpu` mit orthografischen und perspektivischen Kameras.
- **Erweiterbare Abläufe** — native Plugins, Befehlsskripte, Konvertierung ohne Oberfläche und zeilenbasierte JSON-Automatisierungs-API.
-
+
## Dateiabläufe
diff --git a/docs/readme/README.es.md b/docs/readme/README.es.md
index 7ddd2d37..9c7a09e3 100644
--- a/docs/readme/README.es.md
+++ b/docs/readme/README.es.md
@@ -19,7 +19,7 @@
Participar en la conversación
-
+
## Descripción general
@@ -36,7 +36,7 @@ El proyecto está en desarrollo activo. Conserva copias de seguridad de los dibu
- **Renderizado por GPU** — vistas 2D y 3D aceleradas mediante `wgpu`, con cámaras ortográfica y en perspectiva.
- **Flujos ampliables** — complementos nativos, scripts de comandos, conversión sin interfaz y una API de automatización JSON basada en líneas.
-
+
## Flujos de archivos
diff --git a/docs/readme/README.fi.md b/docs/readme/README.fi.md
index 8107e52a..800c9d18 100644
--- a/docs/readme/README.fi.md
+++ b/docs/readme/README.fi.md
@@ -19,7 +19,7 @@
Osallistu keskusteluun
-
+
## Yleiskatsaus
@@ -36,7 +36,7 @@ Projektia kehitetään aktiivisesti. Säilytä tärkeistä tuotantopiirustuksist
- **GPU-renderöinti** — `wgpu`:n kiihdyttämät 2D- ja 3D-näkymät sekä ortografiset ja perspektiivikamerat.
- **Laajennettavat työnkulut** — natiiviliitännäiset, komentoskriptit, käyttöliittymätön muunnos ja rivipohjainen JSON-automaatiorajapinta.
-
+
## Tiedostotyönkulut
diff --git a/docs/readme/README.fr.md b/docs/readme/README.fr.md
index 411a195d..14e32d3c 100644
--- a/docs/readme/README.fr.md
+++ b/docs/readme/README.fr.md
@@ -19,7 +19,7 @@
Participer aux discussions
-
+
## Présentation
@@ -36,7 +36,7 @@ Le projet est en développement actif. Conservez des sauvegardes des dessins de
- **Rendu GPU** — vues 2D et 3D accélérées par `wgpu`, avec caméras orthographique et perspective.
- **Flux extensibles** — plugins natifs, scripts de commandes, conversion sans interface et API d’automatisation JSON ligne par ligne.
-
+
## Flux de fichiers
diff --git a/docs/readme/README.hi.md b/docs/readme/README.hi.md
index c90cef61..fe46322f 100644
--- a/docs/readme/README.hi.md
+++ b/docs/readme/README.hi.md
@@ -19,7 +19,7 @@
चर्चा में शामिल हों
-
+
## परिचय
@@ -36,7 +36,7 @@ Open CAD Studio तकनीकी ड्राइंग, लेआउट का
- **GPU रेंडरिंग** — `wgpu` से तेज़ किए गए 2D और 3D व्यूपोर्ट, ऑर्थोग्राफ़िक और पर्सपेक्टिव कैमरा सहित।
- **विस्तार योग्य कार्यप्रवाह** — मूल प्लगइन, कमांड स्क्रिप्ट, हेडलेस रूपांतरण और पंक्ति-आधारित JSON स्वचालन API।
-
+
## फ़ाइल कार्यप्रवाह
diff --git a/docs/readme/README.hu.md b/docs/readme/README.hu.md
index 4b9a100e..803c56da 100644
--- a/docs/readme/README.hu.md
+++ b/docs/readme/README.hu.md
@@ -19,7 +19,7 @@
Csatlakozás a beszélgetéshez
-
+
## Áttekintés
@@ -36,7 +36,7 @@ A projekt aktív fejlesztés alatt áll. A fontos gyártási rajzokról tarts bi
- **GPU-megjelenítés** — `wgpu` által gyorsított 2D és 3D nézetek, ortografikus és perspektivikus kamerákkal.
- **Bővíthető munkafolyamatok** — natív bővítmények, parancsfájlok, felület nélküli konverzió és soralapú JSON automatizálási API.
-
+
## Fájlmunkafolyamatok
diff --git a/docs/readme/README.it.md b/docs/readme/README.it.md
index b7539995..ea727d5e 100644
--- a/docs/readme/README.it.md
+++ b/docs/readme/README.it.md
@@ -19,7 +19,7 @@
Partecipa alla discussione
-
+
## Panoramica
@@ -36,7 +36,7 @@ Il progetto è in sviluppo attivo. Conserva copie di sicurezza dei disegni di pr
- **Rendering GPU** — viste 2D e 3D accelerate tramite `wgpu`, con telecamere ortografiche e prospettiche.
- **Flussi estensibili** — plugin nativi, script di comandi, conversione headless e API di automazione JSON basata su righe.
-
+
## Flussi dei file
diff --git a/docs/readme/README.ja.md b/docs/readme/README.ja.md
index eafce1d5..d81babe5 100644
--- a/docs/readme/README.ja.md
+++ b/docs/readme/README.ja.md
@@ -19,7 +19,7 @@
ディスカッションに参加
-
+
## 概要
@@ -36,7 +36,7 @@ Open CAD Studio は、技術製図、レイアウト作業、ソリッドモデ
- **GPU レンダリング** — `wgpu` による高速な 2D/3D ビューポートと、正投影・透視投影カメラ。
- **拡張可能なワークフロー** — ネイティブプラグイン、コマンドスクリプト、ヘッドレス変換、行単位の JSON 自動化 API。
-
+
## ファイルワークフロー
diff --git a/docs/readme/README.ko.md b/docs/readme/README.ko.md
index 0f8b209f..56375770 100644
--- a/docs/readme/README.ko.md
+++ b/docs/readme/README.ko.md
@@ -19,7 +19,7 @@
토론 참여
-
+
## 개요
@@ -36,7 +36,7 @@ Open CAD Studio는 기술 도면 작성, 배치 작업, 솔리드 모델링을
- **GPU 렌더링** — `wgpu`로 가속된 2D 및 3D 뷰포트와 직교·원근 카메라를 제공합니다.
- **확장 가능한 작업 흐름** — 네이티브 플러그인, 명령 스크립트, 헤드리스 변환, 줄 단위 JSON 자동화 API를 지원합니다.
-
+
## 파일 작업 흐름
diff --git a/docs/readme/README.nl.md b/docs/readme/README.nl.md
index f70daf63..8c03857b 100644
--- a/docs/readme/README.nl.md
+++ b/docs/readme/README.nl.md
@@ -19,7 +19,7 @@
Deelnemen aan discussies
-
+
## Overzicht
@@ -36,7 +36,7 @@ Het project wordt actief ontwikkeld. Bewaar reservekopieën van belangrijke prod
- **GPU-rendering** — versnelde 2D- en 3D-viewports via `wgpu`, met orthografische en perspectiefcamera's.
- **Uitbreidbare workflows** — native plug-ins, opdrachtscripts, headless conversie en een regelgebaseerde JSON-automatiserings-API.
-
+
## Bestandsworkflows
diff --git a/docs/readme/README.pl.md b/docs/readme/README.pl.md
index 0eeccd18..9d154aa8 100644
--- a/docs/readme/README.pl.md
+++ b/docs/readme/README.pl.md
@@ -19,7 +19,7 @@
Dołącz do dyskusji
-
+
## Przegląd
@@ -36,7 +36,7 @@ Projekt jest aktywnie rozwijany. Zachowuj kopie zapasowe ważnych rysunków prod
- **Renderowanie GPU** — przyspieszone widoki 2D i 3D przez `wgpu`, z kamerą ortograficzną i perspektywiczną.
- **Rozszerzalne przepływy pracy** — natywne wtyczki, skrypty poleceń, konwersja bez interfejsu i wierszowe API automatyzacji JSON.
-
+
## Przepływy plików
diff --git a/docs/readme/README.pt-BR.md b/docs/readme/README.pt-BR.md
index f86626f2..267100ea 100644
--- a/docs/readme/README.pt-BR.md
+++ b/docs/readme/README.pt-BR.md
@@ -19,7 +19,7 @@
Participar da discussão
-
+
## Visão geral
@@ -36,7 +36,7 @@ O projeto está em desenvolvimento ativo. Mantenha cópias de segurança de dese
- **Renderização por GPU** — viewports 2D e 3D aceleradas por `wgpu`, com câmeras ortográfica e em perspectiva.
- **Fluxos extensíveis** — plugins nativos, scripts de comandos, conversão sem interface e API de automação JSON baseada em linhas.
-
+
## Fluxos de arquivos
diff --git a/docs/readme/README.ru.md b/docs/readme/README.ru.md
index a44d7d04..da2a25b5 100644
--- a/docs/readme/README.ru.md
+++ b/docs/readme/README.ru.md
@@ -19,7 +19,7 @@
Присоединиться к обсуждению
-
+
## Обзор
@@ -36,7 +36,7 @@ Open CAD Studio — кроссплатформенное приложение д
- **Отрисовка на GPU** — ускоренные 2D- и 3D-виды через `wgpu`, ортографическая и перспективная камеры.
- **Расширяемые процессы** — нативные плагины, сценарии команд, преобразование без интерфейса и построчный JSON API автоматизации.
-
+
## Работа с файлами
diff --git a/docs/readme/README.tr.md b/docs/readme/README.tr.md
index 9d0f0e8f..6086fe56 100644
--- a/docs/readme/README.tr.md
+++ b/docs/readme/README.tr.md
@@ -21,7 +21,7 @@
Tartışmalara katıl
-
+
## Genel bakış
@@ -38,7 +38,7 @@ Proje aktif olarak geliştirilmektedir. Önemli üretim çizimlerinin yedeklerin
- **GPU ile görüntüleme** — `wgpu` üzerinden hızlandırılmış 2B ve 3B görünüm pencereleri; ortografik ve perspektif kameralar.
- **Genişletilebilir iş akışları** — yerel eklentiler, komut betikleri, arayüzsüz dönüştürme ve satır tabanlı JSON otomasyon API'si.
-
+
## Dosya iş akışları
diff --git a/docs/readme/README.zh-CN.md b/docs/readme/README.zh-CN.md
index b167735c..49c526e0 100644
--- a/docs/readme/README.zh-CN.md
+++ b/docs/readme/README.zh-CN.md
@@ -19,7 +19,7 @@
参与讨论
-
+
## 概览
@@ -36,7 +36,7 @@ Open CAD Studio 是一款用于技术绘图、布局设计和实体建模的跨
- **GPU 渲染** — 通过 `wgpu` 加速二维和三维视口,并支持正交与透视相机。
- **可扩展工作流** — 支持原生插件、命令脚本、无界面转换和逐行 JSON 自动化 API。
-
+
## 文件工作流
diff --git a/docs/readme/README.zh-TW.md b/docs/readme/README.zh-TW.md
index 43c1aa7c..dcf57282 100644
--- a/docs/readme/README.zh-TW.md
+++ b/docs/readme/README.zh-TW.md
@@ -19,7 +19,7 @@
參與討論
-
+
## 概覽
@@ -36,7 +36,7 @@ Open CAD Studio 是用於技術繪圖、配置作業和實體建模的跨平台
- **GPU 轉譯** — 透過 `wgpu` 加速二維和三維視埠,並支援正投影與透視相機。
- **可擴充工作流程** — 支援原生外掛程式、指令碼、無介面轉換和逐行 JSON 自動化 API。
-
+
## 檔案工作流程
diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl
index a6fe3839..54252271 100644
--- a/locales/ar-SA/opencadstudio.ftl
+++ b/locales/ar-SA/opencadstudio.ftl
@@ -3086,7 +3086,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 009dd60e..5da17064 100644
--- a/locales/bg-BG/opencadstudio.ftl
+++ b/locales/bg-BG/opencadstudio.ftl
@@ -3074,7 +3074,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 68a7b23d..7c2337f8 100644
--- a/locales/cs-CZ/opencadstudio.ftl
+++ b/locales/cs-CZ/opencadstudio.ftl
@@ -3073,7 +3073,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 9d41feb7..c17b0730 100644
--- a/locales/de-DE/opencadstudio.ftl
+++ b/locales/de-DE/opencadstudio.ftl
@@ -3072,7 +3072,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 df7541e1..12e6b94b 100644
--- a/locales/en-US/opencadstudio.ftl
+++ b/locales/en-US/opencadstudio.ftl
@@ -3100,7 +3100,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 ff99a804..cf6101bd 100644
--- a/locales/es-ES/opencadstudio.ftl
+++ b/locales/es-ES/opencadstudio.ftl
@@ -3074,7 +3074,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 fc2f9337..01752032 100644
--- a/locales/fi-FI/opencadstudio.ftl
+++ b/locales/fi-FI/opencadstudio.ftl
@@ -3073,7 +3073,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 4c0b91a0..8270c93d 100644
--- a/locales/fr-FR/opencadstudio.ftl
+++ b/locales/fr-FR/opencadstudio.ftl
@@ -3074,7 +3074,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 df1390a3..abf200ab 100644
--- a/locales/hi-IN/opencadstudio.ftl
+++ b/locales/hi-IN/opencadstudio.ftl
@@ -3062,7 +3062,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 c6f97b55..13e4c28c 100644
--- a/locales/hu-HU/opencadstudio.ftl
+++ b/locales/hu-HU/opencadstudio.ftl
@@ -3073,7 +3073,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 2eafa978..5acada86 100644
--- a/locales/it-IT/opencadstudio.ftl
+++ b/locales/it-IT/opencadstudio.ftl
@@ -3073,7 +3073,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 399b2da6..c35743c2 100644
--- a/locales/ja-JP/opencadstudio.ftl
+++ b/locales/ja-JP/opencadstudio.ftl
@@ -3062,7 +3062,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 17538d9a..d2c649d8 100644
--- a/locales/ko-KR/opencadstudio.ftl
+++ b/locales/ko-KR/opencadstudio.ftl
@@ -3073,7 +3073,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 cfca8e14..246e073f 100644
--- a/locales/nl-NL/opencadstudio.ftl
+++ b/locales/nl-NL/opencadstudio.ftl
@@ -3074,7 +3074,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 975a5efe..cf5a981e 100644
--- a/locales/pl-PL/opencadstudio.ftl
+++ b/locales/pl-PL/opencadstudio.ftl
@@ -3073,7 +3073,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 1824a4ec..128a43dc 100644
--- a/locales/pt-BR/opencadstudio.ftl
+++ b/locales/pt-BR/opencadstudio.ftl
@@ -3074,7 +3074,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 d098d736..0f154f49 100644
--- a/locales/ru-RU/opencadstudio.ftl
+++ b/locales/ru-RU/opencadstudio.ftl
@@ -3077,7 +3077,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 82d1d035..520a80b9 100644
--- a/locales/tr-TR/opencadstudio.ftl
+++ b/locales/tr-TR/opencadstudio.ftl
@@ -3040,7 +3040,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 190584b3..3440c473 100644
--- a/locales/zh-CN/opencadstudio.ftl
+++ b/locales/zh-CN/opencadstudio.ftl
@@ -3057,7 +3057,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 206fbe04..fb781a28 100644
--- a/locales/zh-TW/opencadstudio.ftl
+++ b/locales/zh-TW/opencadstudio.ftl
@@ -3069,7 +3069,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/site/modeling.png b/site/modeling.png
index 3e89b21a..869a6661 100644
Binary files a/site/modeling.png and b/site/modeling.png differ
diff --git a/site/workspace.png b/site/workspace.png
index 6dc88c50..eda63ed4 100644
Binary files a/site/workspace.png and b/site/workspace.png differ
diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs
index 6114e3b0..0009e0ba 100644
--- a/src/app/commands/draw.rs
+++ b/src/app/commands/draw.rs
@@ -674,9 +674,13 @@ impl OpenCADStudio {
}
}
- "POINT" => {
+ "POINT" | "MULTIPOINT" => {
use crate::modules::draw::draw::point::PointCommand;
- let new_cmd = PointCommand::new();
+ let new_cmd = if cmd == "MULTIPOINT" {
+ PointCommand::multiple()
+ } else {
+ PointCommand::new()
+ };
self.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}
diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs
index de326abb..774f5cc8 100644
--- a/src/app/commands/styleprops.rs
+++ b/src/app/commands/styleprops.rs
@@ -2309,7 +2309,7 @@ impl OpenCADStudio {
use crate::command::ValuePromptCommand;
let c = ValuePromptCommand::new(
"PDSIZE",
- "PDSIZE new point size (0 = 5% of viewport, <0 = absolute):",
+ "PDSIZE new point size (0 = 5% of viewport, >0 = absolute, <0 = viewport percentage):",
);
self.command_line.push_info(&c.prompt());
self.tabs[i].active_cmd = Some(Box::new(c));
diff --git a/src/app/update/file.rs b/src/app/update/file.rs
index 720f86cc..20cb1700 100644
--- a/src/app/update/file.rs
+++ b/src/app/update/file.rs
@@ -93,7 +93,7 @@ fn native_paths_match(left: &std::path::Path, right: &std::path::Path) -> bool {
}
type LayoutPlotParams = (
- std::sync::Arc>,
+ std::sync::Arc>,
Vec,
Vec,
crate::io::pdf_export::PlotGroupSplits,
@@ -107,7 +107,7 @@ type LayoutPlotParams = (
);
type ClippedPlotParams = (
- Vec,
+ Vec,
Vec,
Vec,
crate::io::pdf_export::PlotGroupSplits,
@@ -121,7 +121,7 @@ type ClippedPlotParams = (
);
fn plot_content_extents(
- wires: &[crate::scene::WireModel],
+ wires: &[crate::io::pdf_export::PlotWire],
hatches: &[crate::scene::model::hatch_model::HatchModel],
wipeouts: &[crate::scene::model::hatch_model::HatchModel],
) -> Option<(f64, f64, f64, f64)> {
@@ -163,7 +163,7 @@ fn plot_scene_content(
paper_space_last: bool,
render_mode_override: Option,
) -> (
- std::sync::Arc>,
+ std::sync::Arc>,
Vec,
Vec,
crate::io::pdf_export::PlotGroupSplits,
@@ -171,6 +171,16 @@ fn plot_scene_content(
let (mut paper_wires, mut model_wires) = scene.plot_wire_groups(render_mode_override);
paper_wires.retain(|wire| wire.plot_visible);
model_wires.retain(|wire| wire.plot_visible);
+ let with_depth = |wires: Vec| {
+ let depths = scene.plot_wire_depths(&wires);
+ wires
+ .into_iter()
+ .zip(depths)
+ .map(|(wire, draw_depth)| crate::io::pdf_export::PlotWire { wire, draw_depth })
+ .collect::>()
+ };
+ let paper_wires = with_depth(paper_wires);
+ let model_wires = with_depth(model_wires);
let paper_hatches = scene.paper_plot_hatches().as_ref().clone();
let paper_wipeouts = scene.paper_plot_wipeouts().as_ref().clone();
if scene.current_layout == "Model" {
@@ -188,7 +198,11 @@ fn plot_scene_content(
}
let (mut model_pattern_wires, model_hatches, model_wipeouts) =
scene.viewport_plot_fills();
- model_pattern_wires.retain(|wire| wire.plot_visible);
+ model_pattern_wires.retain(|(wire, _)| wire.plot_visible);
+ let model_pattern_wires = model_pattern_wires
+ .into_iter()
+ .map(|(wire, draw_depth)| crate::io::pdf_export::PlotWire { wire, draw_depth })
+ .collect::>();
let (wires, hatches, wipeouts, splits) = if paper_space_last {
let splits = crate::io::pdf_export::PlotGroupSplits {
diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs
index ef0015f4..35bcecbd 100644
--- a/src/app/update/viewport.rs
+++ b/src/app/update/viewport.rs
@@ -341,8 +341,9 @@ impl OpenCADStudio {
.abs();
if !self.point_size_relative && mag == 0.0 {
let wpp = self.tabs[i].scene.world_per_pixel().unwrap_or(0.0);
+ let viewport_height = self.tabs[i].scene.selection.borrow().vp_size.1;
mag = if wpp > 0.0 {
- crate::entities::point::relative_world_size(0.0, wpp)
+ crate::entities::point::relative_world_size(0.0, wpp, viewport_height)
} else {
1.0
};
@@ -2237,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 8ae8f044..adda3e8f 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 bd14abfa..53525918 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 6cdfcf09..d2568013 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 2b831345..ba920466 100644
--- a/src/entities/point.rs
+++ b/src/entities/point.rs
@@ -1,23 +1,18 @@
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;
-
-/// Nominal viewport height (px) used to turn a relative PDSIZE percentage into
-/// an on-screen pixel size. The exact viewport height isn't threaded into
-/// tessellation; this reference keeps relative points a roughly constant
-/// fraction of the screen across zoom levels.
-const REL_REF_PX: f64 = 600.0;
+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`] when a world-per-pixel factor is available;
-/// without one it falls back to a small fixed world size.
+/// is handled by [`relative_render`].
fn pdsize_world(pdsize: f64) -> f64 {
if pdsize > 0.0 {
pdsize
@@ -35,7 +30,9 @@ fn point_render(pt: &Point, pdmode: i16, s: f64) -> RenderEntity {
// mirrored points (normal 0,0,-1) to the wrong side of the drawing.
let (wx, wy, wz) = (pt.location.x, pt.location.y, pt.location.z);
let snap = glam::DVec3::new(wx, wy, wz);
- if pdmode == 0 {
+ let normal = point_normal(pt);
+ let top = snap + normal * pt.thickness;
+ if pdmode == 0 && pt.thickness.abs() <= 1.0e-10 {
// Default: a single position (the driver sizes the dot in pixels).
return RenderEntity {
pick_tris: Vec::new(),
@@ -46,7 +43,7 @@ fn point_render(pt: &Point, pdmode: i16, s: f64) -> RenderEntity {
fill_tris: vec![],
};
}
- let pts = point_glyph(wx, wy, wz, pdmode, s);
+ let pts = point_glyph(pt, pdmode, s);
if pts.is_empty() {
// PDMODE 1 = nothing — emit an empty Lines wire so picking still works.
return RenderEntity {
@@ -63,20 +60,22 @@ fn point_render(pt: &Point, pdmode: i16, s: f64) -> RenderEntity {
object: RenderObject::Lines(pts),
snap_pts: vec![(snap, SnapHint::Node)],
tangent_geoms: vec![],
- key_vertices: vec![[wx, wy, wz]],
+ key_vertices: if pt.thickness.abs() > 1.0e-10 {
+ vec![[wx, wy, wz], [top.x, top.y, top.z]]
+ } else {
+ vec![[wx, wy, wz]]
+ },
fill_tris: vec![],
}
}
-/// Viewport-aware override for a relative (≤ 0) PDSIZE: size the glyph from the
-/// world-per-pixel factor so the point stays a roughly constant on-screen size
-/// across zoom. Returns `None` for an absolute PDSIZE, the size-independent
-/// default dot (PDMODE 0), or when no `wpp` is available — the caller then uses
-/// the normal header-driven path.
+/// Build a unit-sized glyph for a relative (≤ 0) PDSIZE. The wire shader scales
+/// its planar displacement from the point origin using the live viewport size,
+/// so zooming and resizing do not require retessellation.
pub fn relative_render(
entity: &EntityType,
document: &acadrust::CadDocument,
- wpp: Option,
+ _wpp: Option,
) -> Option {
let EntityType::Point(pt) = entity else {
return None;
@@ -86,20 +85,69 @@ pub fn relative_render(
if pdsize > 0.0 || pdmode == 0 {
return None;
}
- let wpp = wpp.filter(|w| *w > 0.0)?;
- Some(point_render(pt, pdmode, relative_world_size(pdsize, wpp) * 0.5))
+ Some(point_render(pt, pdmode, 0.5))
}
-/// Full on-screen glyph size (world units) for a relative (≤ 0) PDSIZE at the
-/// given world-per-pixel factor. PDSIZE 0 is the 5% default; negative is the
-/// percentage. Used both for rendering and to seed an absolute size when the
-/// user switches the Point Style dialog to absolute units.
-pub fn relative_world_size(pdsize: f64, wpp: f32) -> f64 {
+/// Full glyph size in world units for a relative PDSIZE at the current
+/// viewport height. Used when the style dialog converts a relative value to an
+/// absolute one.
+pub fn relative_world_size(pdsize: f64, wpp: f32, viewport_height_px: f32) -> f64 {
let pct = if pdsize == 0.0 { 5.0 } else { -pdsize };
- (pct / 100.0) * REL_REF_PX * wpp as f64
+ (pct / 100.0) * viewport_height_px.max(1.0) as f64 * wpp as f64
}
-fn point_glyph(cx: f64, cy: f64, z: f64, pdmode: i16, s_half: f64) -> Vec<[f64; 3]> {
+/// Percentage and plane normal encoded into the point wire for live GPU
+/// viewport scaling. A zero PDSIZE means the standard five-percent size.
+pub fn relative_marker_spec(
+ entity: &EntityType,
+ document: &acadrust::CadDocument,
+) -> Option {
+ let EntityType::Point(pt) = entity else {
+ return None;
+ };
+ let size = document.header.point_display_size;
+ if size > 0.0 || effective_pdmode(pt, document.header.point_display_mode) == 0 {
+ return None;
+ }
+ 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 {
+ 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_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]),
+ )
+}
+
+fn point_glyph(pt: &Point, pdmode: i16, s_half: f64) -> Vec<[f64; 3]> {
// PDMODE bits:
// shape: 0=dot, 1=nothing, 2='+', 3='×', 4='|'
// +32 = enclose in a circle
@@ -112,65 +160,68 @@ fn point_glyph(cx: f64, cy: f64, z: f64, pdmode: i16, s_half: f64) -> Vec<[f64;
// 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 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([cx - d, cy, z], [cx + d, cy, z]);
- push_seg([cx, cy - d, z], [cx, cy + d, z]);
+ 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([cx - arm, cy, z], [cx + arm, cy, z]);
- push_seg([cx, cy - arm, z], [cx, cy + arm, z]);
+ curves.push(line([-arm, 0.0], [arm, 0.0]));
+ curves.push(line([0.0, -arm], [0.0, arm]));
}
3 => {
- push_seg([cx - arm, cy - arm, z], [cx + arm, cy + arm, z]);
- push_seg([cx - arm, cy + arm, z], [cx + arm, cy - arm, z]);
+ 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([cx, cy, z], [cx, cy + s, z]);
+ curves.push(line([0.0, 0.0], [0.0, s]));
}
_ => {
- push_seg([cx - s, cy, z], [cx + s, cy, z]);
- push_seg([cx, cy - s, z], [cx, cy + s, z]);
+ curves.push(line([-s, 0.0], [s, 0.0]));
+ curves.push(line([0.0, -s], [0.0, s]));
}
}
if circle {
- // 16-segment polyline circle.
- const N: usize = 16;
- 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([cx + a.cos() * s, cy + a.sin() * s, z]);
- }
- 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 = [cx - s, cy - s, z];
- let p2 = [cx + s, cy - s, z];
- let p3 = [cx + s, cy + s, z];
- let p4 = [cx - s, cy + s, z];
- 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]));
}
- pts
+
+ 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)
+ }));
+ paths.push(bezier_points(&[plane.origin, top_origin], 1));
+ }
+ 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 aa6a6ff6..67acd76b 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 d7923f49..8200a893 100644
--- a/src/io/pdf_export.rs
+++ b/src/io/pdf_export.rs
@@ -24,12 +24,26 @@ use printpdf::{
use std::io::Write;
use std::path::Path;
+#[derive(Clone, Debug)]
+pub struct PlotWire {
+ pub wire: WireModel,
+ pub draw_depth: f32,
+}
+
+impl std::ops::Deref for PlotWire {
+ type Target = WireModel;
+
+ fn deref(&self) -> &Self::Target {
+ &self.wire
+ }
+}
+
// The web build has no `printpdf` (it pulls a wasm-incompatible `memchr` via
// lopdf → nom_locate) and no filesystem, so PDF export is native-only; the web
// build gets these stubs so the call sites still compile.
#[cfg(target_arch = "wasm32")]
pub fn export_pdf(
- _wires: &[WireModel],
+ _wires: &[PlotWire],
_hatches: &[HatchModel],
_wipeouts: &[HatchModel],
_paper_w: f64,
@@ -95,7 +109,7 @@ pub struct PlotGroupSplits {
/// Owned render data for one page in a multi-page PDF.
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
pub struct PdfPageInput {
- pub wires: std::sync::Arc>,
+ pub wires: std::sync::Arc>,
pub hatches: Vec,
pub wipeouts: Vec,
pub paper_w: f64,
@@ -131,7 +145,7 @@ impl Default for PdfPlotOptions {
/// - `rotation_deg`: 0 | 90 | 180 | 270 — rotates the entire drawing on the page.
#[cfg(not(target_arch = "wasm32"))]
pub fn export_pdf(
- wires: &[WireModel],
+ wires: &[PlotWire],
hatches: &[HatchModel],
wipeouts: &[HatchModel],
paper_w: f64,
@@ -204,7 +218,7 @@ pub fn pick_pdf_path_owned(
#[cfg(not(target_arch = "wasm32"))]
fn build_pdf(
- wires: &[WireModel],
+ wires: &[PlotWire],
hatches: &[HatchModel],
wipeouts: &[HatchModel],
paper_w: f32,
@@ -267,7 +281,7 @@ fn build_pdf_pages(pages: &[PdfPageInput], plot_style: Option<&PlotStyleTable>)
#[cfg(not(target_arch = "wasm32"))]
fn append_pdf_page(
doc: &mut PdfDocument,
- wires: &[WireModel],
+ wires: &[PlotWire],
hatches: &[HatchModel],
wipeouts: &[HatchModel],
paper_w: f32,
@@ -393,29 +407,92 @@ fn append_pdf_page(
(first_wires, first_hatches, first_wipeouts),
(second_wires, second_hatches, second_wipeouts),
] {
- emit_wire_fills(&mut ops, wires, ox, oy, plot_style, options);
-
- // Hatch / wipeout fills render before wires so linework stays visible.
- for hatch in wipeouts.iter().chain(hatches.iter()) {
- emit_hatch(
- &mut ops,
- hatch,
- ox,
- oy,
- plot_style,
- scale,
- options,
- normal_blend.as_ref(),
- );
+ enum DrawItem<'a> {
+ WireFill(&'a PlotWire),
+ Hatch(&'a HatchModel),
+ Wire(&'a PlotWire),
+ Text(&'a PlotWire),
}
+ let mut draw_items = Vec::with_capacity(wires.len() * 2 + hatches.len() + wipeouts.len());
+ let mut sequence = 0usize;
+ for wire in wires {
+ if !wire.fill_tris.is_empty() {
+ draw_items.push((wire.draw_depth, 0u8, sequence, DrawItem::WireFill(wire)));
+ sequence += 1;
+ }
+ draw_items.push((wire.draw_depth, 2u8, sequence, DrawItem::Wire(wire)));
+ sequence += 1;
+ if !wire.text_verts.is_empty() {
+ draw_items.push((wire.draw_depth, 3u8, sequence, DrawItem::Text(wire)));
+ sequence += 1;
+ }
+ }
+ for hatch in wipeouts.iter().chain(hatches.iter()) {
+ draw_items.push((hatch.draw_depth, 1u8, sequence, DrawItem::Hatch(hatch)));
+ sequence += 1;
+ }
+ draw_items.sort_by(|a, b| {
+ a.0.total_cmp(&b.0)
+ .then_with(|| a.1.cmp(&b.1))
+ .then_with(|| a.2.cmp(&b.2))
+ });
+
let mut last_color: Option<[f32; 3]> = None;
let mut last_lw: Option = None;
// Current PDF dash array (empty = solid). Tracked so the dash op is only
// re-emitted when it actually changes between wires.
let mut last_dash: Option> = None;
- for wire in wires {
+ for (_, _, _, item) in draw_items {
+ let wire = match item {
+ DrawItem::WireFill(wire) => {
+ emit_wire_fills(
+ &mut ops,
+ std::slice::from_ref(&wire.wire),
+ ox,
+ oy,
+ plot_style,
+ options,
+ );
+ last_color = None;
+ last_lw = None;
+ last_dash = None;
+ continue;
+ }
+ DrawItem::Hatch(hatch) => {
+ emit_hatch(
+ &mut ops,
+ hatch,
+ ox,
+ oy,
+ plot_style,
+ scale,
+ options,
+ normal_blend.as_ref(),
+ );
+ last_color = None;
+ last_lw = None;
+ last_dash = None;
+ continue;
+ }
+ DrawItem::Text(wire) => {
+ emit_text(
+ &mut ops,
+ std::slice::from_ref(&wire.wire),
+ ox,
+ oy,
+ scale,
+ plot_style,
+ options,
+ );
+ last_color = None;
+ last_lw = None;
+ last_dash = None;
+ continue;
+ }
+ DrawItem::Wire(wire) => wire,
+ };
let [mut r, mut g, mut b, a] = wire.color;
if a < 0.01 {
continue;
@@ -541,9 +618,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,
@@ -552,13 +629,6 @@ fn append_pdf_page(
}
flush_line(&mut ops, &segment, dot_radius);
}
-
- // Text (SDF glyph quads) — re-emitted as vector strokes / fills. Text now
- // renders on-screen only as textured SDF quads (`wire.text_verts`), which
- // this CPU exporter can't sample, so without this pass all text — including
- // dimension text — is missing from the PDF (issue #385). Drawn after the
- // wires (on top) and under the same rotation/scale/clip CTM.
- emit_text(&mut ops, wires, ox, oy, scale, plot_style, options);
}
if needs_state {
diff --git a/src/io/print_to_printer.rs b/src/io/print_to_printer.rs
index 29606def..61084e33 100644
--- a/src/io/print_to_printer.rs
+++ b/src/io/print_to_printer.rs
@@ -11,7 +11,7 @@
use crate::io::pdf_export;
use crate::io::plot_style::PlotStyleTable;
use crate::scene::model::hatch_model::HatchModel;
-use crate::scene::WireModel;
+use crate::io::pdf_export::PlotWire;
/// Extra options for a print job. On CUPS (Linux/macOS) these map to `lp`
/// flags / `-o` options. On Windows the generated PDF already carries render
@@ -38,7 +38,7 @@ pub fn list_printers() -> Vec {
#[cfg(target_arch = "wasm32")]
#[allow(clippy::too_many_arguments)]
pub async fn print_wires_with(
- _wires: std::sync::Arc>,
+ _wires: std::sync::Arc>,
_hatches: Vec,
_wipeouts: Vec,
_paper_w: f64,
@@ -153,7 +153,7 @@ pub fn open_printer_properties(_printer: Option<&str>) -> Result<(), String> {
#[cfg(not(target_arch = "wasm32"))]
#[allow(clippy::too_many_arguments)]
pub async fn print_wires_with(
- wires: std::sync::Arc>,
+ wires: std::sync::Arc>,
hatches: Vec,
wipeouts: Vec,
paper_w: f64,
diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs
index a2ea8773..0f86e646 100644
--- a/src/locale_catalog.rs
+++ b/src/locale_catalog.rs
@@ -1843,7 +1843,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):" => Some(("styles", "pdsize-new-point-size-0-5-of-viewport-0-absolu")),
+ "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 68ff5061..5a415426 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 1b93dd70..b526532d 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 e319c171..f0a570d6 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 28d35e7b..b3871839 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 695c8b15..d2371aac 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 ab314a01..ebaaa8b4 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 891ff95f..e8421320 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 9ef884b6..1e13d936 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 fbc6c3d3..f2af91e0 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 896e2fd4..383a8d66 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 b91e9d2e..e3f19c23 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 df3564fc..331677a3 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 e9320510..4c1596a1 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 6aa43017..e91ca64e 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 4173d763..c243e2b6 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 18c11839..3033e969 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 fa2c38dd..33551f5a 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 120840e5..1be63334 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/point.rs b/src/modules/draw/draw/point.rs
index 1dadbca1..0a721a9b 100644
--- a/src/modules/draw/draw/point.rs
+++ b/src/modules/draw/draw/point.rs
@@ -1,7 +1,7 @@
// Point tool — ribbon definition + interactive command.
//
// Command: POINT (PO)
-// Single click → commits EntityType::Point. Stays active for more points.
+// POINT commits one entity and exits. MULTIPOINT stays active.
use acadrust::types::Vector3;
use acadrust::{EntityType, Point as CadPoint};
@@ -22,25 +22,35 @@ pub fn tool() -> ToolDef {
}
}
-pub struct PointCommand;
+pub struct PointCommand {
+ multiple: bool,
+}
impl PointCommand {
pub fn new() -> Self {
- Self
+ Self { multiple: false }
+ }
+
+ pub fn multiple() -> Self {
+ Self { multiple: true }
}
}
impl CadCommand for PointCommand {
fn name(&self) -> &'static str {
- "POINT"
+ if self.multiple { "MULTIPOINT" } else { "POINT" }
}
fn prompt(&self) -> String {
crate::t!("POINT Specify point:").into_owned()
}
fn options(&self) -> Vec {
- use crate::command::CmdOption;
- vec![CmdOption::enter(t!("Done").as_ref())]
+ if self.multiple {
+ use crate::command::CmdOption;
+ vec![CmdOption::enter(t!("Done").as_ref())]
+ } else {
+ Vec::new()
+ }
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
@@ -48,7 +58,11 @@ impl CadCommand for PointCommand {
location: Vector3::new(pt.x as f64, pt.y as f64, pt.z as f64),
..Default::default()
};
- CmdResult::CommitEntity(EntityType::Point(p))
+ if self.multiple {
+ CmdResult::CommitEntity(EntityType::Point(p))
+ } else {
+ CmdResult::CommitAndExit(EntityType::Point(p))
+ }
}
fn on_enter(&mut self) -> CmdResult {
@@ -64,7 +78,7 @@ impl CadCommand for PointCommand {
// ── Autocomplete registry ─────────────────────────────────
-inventory::submit!(crate::command::CommandRegistration { names: &["POINT"] }); // PointCommand
+inventory::submit!(crate::command::CommandRegistration { names: &["POINT", "MULTIPOINT"] }); // PointCommand
// Point display style system variables + dialog.
inventory::submit!(crate::command::CommandRegistration {
names: &["PDMODE", "PDSIZE", "DDPTYPE"]
diff --git a/src/modules/draw/draw/raster_image.rs b/src/modules/draw/draw/raster_image.rs
index 7f97be78..ac973aca 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 14696aec..47bcf9a8 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 5a260824..6ae6386e 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 157318ca..d0a3ac0f 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 d9eaa56a..17d5d72c 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 00d0485e..a04b94aa 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 5eb27f24..66941b70 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 31a888de..4f86e8cf 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 dd91f094..3e666cc7 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 c63bd1d3..95886daf 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/cache/properties.rs b/src/scene/cache/properties.rs
index 19f79e26..b65880a6 100644
--- a/src/scene/cache/properties.rs
+++ b/src/scene/cache/properties.rs
@@ -95,6 +95,15 @@ pub fn general_section(entity: &EntityType) -> PropSection {
section.props.retain(|prop| prop.field != "handle");
}
+ if matches!(entity, EntityType::Point(_)) {
+ section.props.retain(|prop| prop.field != "handle");
+ let hyperlink = section.props.iter().position(|prop| prop.field == "hyperlink");
+ let transparency = section.props.iter().position(|prop| prop.field == "transparency");
+ if let (Some(hyperlink), Some(transparency)) = (hyperlink, transparency) {
+ section.props.swap(hyperlink, transparency);
+ }
+ }
+
// Thickness (DXF 39) is a General-group property, but only the entity
// types that carry an extrusion thickness expose it (line, circle, arc,
// polyline, text, 2D solid, …). Show it right after Hyperlink for those.
diff --git a/src/scene/convert/tess.rs b/src/scene/convert/tess.rs
index f7272b93..eab11c21 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 1c4dfa83..3980d710 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,7 +1321,10 @@ pub fn tessellate(
} else {
(Vec::new(), Vec::new(), Vec::new())
};
+ let point_marker =
+ crate::entities::point::relative_marker_spec(entity, document);
out.push(WireModel {
+ point_marker,
taper_widths: Vec::new(),
world_width: polyline_band_width(entity),
depth_override: None,
@@ -1354,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(),
@@ -1388,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,
@@ -1435,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,
@@ -1483,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,
@@ -1604,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 db72b008..1f7e6f67 100644
--- a/src/scene/mod.rs
+++ b/src/scene/mod.rs
@@ -5454,6 +5454,14 @@ impl Scene {
(self.paper_sheet_wires_arc().as_ref().clone(), model_wires)
}
+ pub(crate) fn plot_wire_depths(&self, wires: &[WireModel]) -> Vec {
+ let depths = self.draw_depth_map();
+ wires
+ .iter()
+ .map(|wire| pipeline::wire_gpu::wire_draw_depth(wire, depths.as_ref()))
+ .collect()
+ }
+
/// Per-entity stable draw-order depth, keyed by entity handle value.
/// A full build assigns sparse labels in effective draw order. Incremental
/// Add/Remove then changes only the named handle: existing siblings retain
@@ -7359,14 +7367,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 +7548,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 +7622,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 87f8a894..5e16de1f 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 42f1c956..857ff942 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 99a220a8..4e1f6987 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 da47cf08..2a060f13 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 2de66884..7187320d 100644
--- a/src/scene/pipeline/wire_gpu.rs
+++ b/src/scene/pipeline/wire_gpu.rs
@@ -111,7 +111,7 @@ impl WireInstance {
}
/// Per-wire constants shared by every segment of a wire (storage path). std430
-/// layout: three vec4 then eight scalars = 80 B, matching `WireConst` in
+/// layout: three vec4, eight scalars, then three vec4 = 128 B, matching `WireConst` in
/// wire_indexed.wgsl. `align_end` / `align_total` carry the "A"-type endpoint
/// alignment (see `wire_distances`); 0.0 total = no alignment.
#[repr(C)]
@@ -132,6 +132,11 @@ pub struct WireConst {
pub world_half_width: f32,
pub _pad1: f32,
pub _pad2: f32,
+ /// Point-marker origin as a double-single pair. `marker_normal_scale.w`
+ /// stores the viewport-height percentage; zero disables marker scaling.
+ pub marker_origin_high: [f32; 4],
+ pub marker_origin_low: [f32; 4],
+ pub marker_normal_scale: [f32; 4],
}
impl WireConst {
@@ -189,6 +194,9 @@ pub struct PackedWireInstance {
/// `world_half_width`). The shader interpolates across the segment.
pub world_hw_a: f32,
pub world_hw_b: f32,
+ pub marker_origin_high: [f32; 4],
+ pub marker_origin_low: [f32; 4],
+ pub marker_normal_scale: [f32; 4],
}
impl PackedWireInstance {
@@ -213,6 +221,9 @@ impl PackedWireInstance {
wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, pos_b_low) as u64, shader_location: 8, format: wgpu::VertexFormat::Float32x3 },
// taper = (world_hw_a, world_hw_b)
wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, world_hw_a) as u64, shader_location: 9, format: wgpu::VertexFormat::Float32x2 },
+ wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, marker_origin_high) as u64, shader_location: 10, format: wgpu::VertexFormat::Float32x4 },
+ wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, marker_origin_low) as u64, shader_location: 11, format: wgpu::VertexFormat::Float32x4 },
+ wgpu::VertexAttribute { offset: std::mem::offset_of!(PackedWireInstance, marker_normal_scale) as u64, shader_location: 12, format: wgpu::VertexFormat::Float32x4 },
];
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::() as u64,
@@ -509,6 +520,22 @@ fn finite3(p: [f32; 3]) -> bool {
p[0].is_finite() && p[1].is_finite() && p[2].is_finite()
}
+fn marker_metadata(wire: &WireModel) -> ([f32; 4], [f32; 4], [f32; 4]) {
+ let Some(marker) = wire.point_marker else {
+ return ([0.0; 4], [0.0; 4], [0.0; 4]);
+ };
+ 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 = 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, marker.viewport_percent],
+ )
+}
+
/// Emit packed per-segment instances (each carries the wire's constants).
pub(crate) fn emit_wire_packed(
wire: &WireModel,
@@ -525,6 +552,7 @@ pub(crate) fn emit_wire_packed(
return Vec::new();
}
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 { wire.taper_widths.get(i).copied().unwrap_or(0.0) * 0.5 };
let mut instances: Vec = Vec::with_capacity(seg_count);
@@ -552,6 +580,9 @@ pub(crate) fn emit_wire_packed(
world_half_width: wire.world_width * 0.5,
world_hw_a: tw(i),
world_hw_b: tw(i + 1),
+ marker_origin_high,
+ marker_origin_low,
+ marker_normal_scale,
});
}
instances
@@ -566,6 +597,7 @@ pub(crate) fn emit_wire_native(
draw_depth: f32,
) -> (Vec, WireConst) {
let (dists, align_end, align_total) = wire_distances(wire);
+ let (marker_origin_high, marker_origin_low, marker_normal_scale) = marker_metadata(wire);
let cst = WireConst {
color,
pat0: [wire.pattern[0], wire.pattern[1], wire.pattern[2], wire.pattern[3]],
@@ -578,6 +610,9 @@ pub(crate) fn emit_wire_native(
world_half_width: wire.world_width * 0.5,
_pad1: 0.0,
_pad2: 0.0,
+ marker_origin_high,
+ marker_origin_low,
+ marker_normal_scale,
};
let n = wire.points.len();
let seg_count = n.saturating_sub(1);
diff --git a/src/scene/project.rs b/src/scene/project.rs
index e1b4ff77..791529b9 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
@@ -354,7 +368,7 @@ impl Scene {
/// needs the equivalent paper-space geometry explicitly.
pub fn viewport_plot_fills(
&self,
- ) -> (Vec, Vec, Vec) {
+ ) -> (Vec<(WireModel, f32)>, Vec, Vec) {
use acadrust::entities::Viewport;
use model::hatch_model::HatchPattern;
@@ -478,7 +492,7 @@ impl Scene {
wire.aci = hatch.aci;
wire.line_weight_px = hatch.line_weight_px;
wire.aabb = aabb;
- pattern_wires.push(wire);
+ pattern_wires.push((wire, hatch.draw_depth));
}
continue;
}
diff --git a/src/scene/text/complex_lt.rs b/src/scene/text/complex_lt.rs
index 5d39e48a..dfad53c7 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 c33c9fa1..29e5bd25 100644
--- a/src/shaders/block_wire.wgsl
+++ b/src/shaders/block_wire.wgsl
@@ -26,6 +26,9 @@ struct WireConst {
world_half_width: f32,
_pad1: f32,
_pad2: f32,
+ marker_origin_high: vec4,
+ marker_origin_low: vec4,
+ marker_normal_scale: vec4,
}
@group(1) @binding(0) var wire_const: WireConst;
@@ -67,6 +70,35 @@ fn resolve_hw(taper_ratio: f32, world_hw: f32, px_hw: f32) -> f32 {
return select(0.5, px_hw, u.lwdisplay_enable > 0.5);
}
+fn marker_relative(
+ position_high: vec3,
+ position_low: vec3,
+ translation: vec3,
+ translation_low: vec3,
+) -> vec3 {
+ if wire_const.marker_normal_scale.w <= 0.0 {
+ return (position_high + translation - u.eye_high)
+ + (position_low + translation_low - u.eye_low);
+ }
+ let origin_high = wire_const.marker_origin_high.xyz;
+ let origin_low = wire_const.marker_origin_low.xyz;
+ let origin_relative = (origin_high + translation - u.eye_high)
+ + (origin_low + translation_low - u.eye_low);
+ let delta = (position_high - origin_high) + (position_low - origin_low);
+ let normal = normalize(wire_const.marker_normal_scale.xyz);
+ let axial = normal * dot(delta, normal);
+ let planar = delta - axial;
+ 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;
+}
+
@vertex
fn vs_main(@builtin(vertex_index) vertex_index: u32, in: VertexIn) -> VertexOut {
let corner = vertex_index % 6u;
@@ -75,10 +107,8 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32, in: VertexIn) -> VertexOut
let which_end = which_end_arr[corner];
let side = side_arr[corner];
- let rel_a = (in.pos_a + in.translation - u.eye_high)
- + (in.pos_a_low + in.translation_low - u.eye_low);
- let rel_b = (in.pos_b + in.translation - u.eye_high)
- + (in.pos_b_low + in.translation_low - u.eye_low);
+ let rel_a = marker_relative(in.pos_a, in.pos_a_low, in.translation, in.translation_low);
+ let rel_b = marker_relative(in.pos_b, in.pos_b_low, in.translation, in.translation_low);
let clip_a = u.view_rot * vec4(rel_a, 1.0);
let clip_b = u.view_rot * vec4(rel_b, 1.0);
let screen_a = clip_a.xy / clip_a.w * u.viewport_size * 0.5;
diff --git a/src/shaders/wire.wgsl b/src/shaders/wire.wgsl
index c684aef5..c8fac652 100644
--- a/src/shaders/wire.wgsl
+++ b/src/shaders/wire.wgsl
@@ -62,6 +62,9 @@ struct InstanceIn {
@location(8) pos_b_low: vec3,
// Per-endpoint world half-width for a tapered band (0 = use the constant).
@location(9) taper: vec2,
+ @location(10) marker_origin_high: vec4,
+ @location(11) marker_origin_low: vec4,
+ @location(12) marker_normal_scale: vec4,
}
// Draw-order depth bias: shifts clip-space z so 2D entities of different
@@ -102,6 +105,28 @@ fn resolve_hw(taper: f32, world_hw: f32, px_hw: f32) -> f32 {
return select(0.5, px_hw, u.lwdisplay_enable > 0.5);
}
+fn marker_relative(position_high: vec3, position_low: vec3, instance: InstanceIn) -> vec3 {
+ if instance.marker_normal_scale.w <= 0.0 {
+ return (position_high - u.eye_high) + (position_low - u.eye_low);
+ }
+ let origin_high = instance.marker_origin_high.xyz;
+ let origin_low = instance.marker_origin_low.xyz;
+ let origin_relative = (origin_high - u.eye_high) + (origin_low - u.eye_low);
+ let delta = (position_high - origin_high) + (position_low - origin_low);
+ let normal = normalize(instance.marker_normal_scale.xyz);
+ let axial = normal * dot(delta, normal);
+ let planar = delta - axial;
+ 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;
+}
+
@vertex fn vs_main(@builtin(vertex_index) vid: u32, in: InstanceIn) -> VertexOut {
// Two-triangle quad corner table:
// vid 0,1,2 = (A,-1) (B,-1) (B,+1)
@@ -117,8 +142,8 @@ fn resolve_hw(taper: f32, world_hw: f32, px_hw: f32) -> f32 {
// operands (Sterbenz); adding (pos_low − eye_low) restores the residual both
// the vertex and the eye would otherwise lose — so geometry stays put at
// UTM-scale coordinates and after a cross-drawing paste, with no jitter.
- let rel_a = (in.pos_a - u.eye_high) + (in.pos_a_low - u.eye_low);
- let rel_b = (in.pos_b - u.eye_high) + (in.pos_b_low - u.eye_low);
+ let rel_a = marker_relative(in.pos_a, in.pos_a_low, in);
+ let rel_b = marker_relative(in.pos_b, in.pos_b_low, in);
let clip_a = u.view_rot * vec4(rel_a, 1.0);
let clip_b = u.view_rot * vec4(rel_b, 1.0);
diff --git a/src/shaders/wire_indexed.wgsl b/src/shaders/wire_indexed.wgsl
index 9e1bd174..c62d92bd 100644
--- a/src/shaders/wire_indexed.wgsl
+++ b/src/shaders/wire_indexed.wgsl
@@ -34,6 +34,9 @@ struct WireConst {
world_half_width: f32,
_pad1: f32,
_pad2: f32,
+ marker_origin_high: vec4,
+ marker_origin_low: vec4,
+ marker_normal_scale: vec4,
}
@group(1) @binding(0) var wire_consts: array;
@@ -81,6 +84,28 @@ fn resolve_hw(taper_ratio: f32, world_hw: f32, px_hw: f32) -> f32 {
return select(0.5, px_hw, u.lwdisplay_enable > 0.5);
}
+fn marker_relative(position_high: vec3, position_low: vec3, c: WireConst) -> vec3 {
+ if c.marker_normal_scale.w <= 0.0 {
+ return (position_high - u.eye_high) + (position_low - u.eye_low);
+ }
+ let origin_high = c.marker_origin_high.xyz;
+ let origin_low = c.marker_origin_low.xyz;
+ let origin_relative = (origin_high - u.eye_high) + (origin_low - u.eye_low);
+ let delta = (position_high - origin_high) + (position_low - origin_low);
+ let normal = normalize(c.marker_normal_scale.xyz);
+ let axial = normal * dot(delta, normal);
+ let planar = delta - axial;
+ 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;
+}
+
@vertex fn vs_main(@builtin(vertex_index) vid: u32, in: InstanceIn) -> VertexOut {
let c = wire_consts[in.wire_id];
@@ -89,8 +114,8 @@ fn resolve_hw(taper_ratio: f32, world_hw: f32, px_hw: f32) -> f32 {
let which_end = which_end_arr[vid];
let side = side_arr[vid];
- let rel_a = (in.pos_a - u.eye_high) + (in.pos_a_low - u.eye_low);
- let rel_b = (in.pos_b - u.eye_high) + (in.pos_b_low - u.eye_low);
+ let rel_a = marker_relative(in.pos_a, in.pos_a_low, c);
+ let rel_b = marker_relative(in.pos_b, in.pos_b_low, c);
let clip_a = u.view_rot * vec4(rel_a, 1.0);
let clip_b = u.view_rot * vec4(rel_b, 1.0);
diff --git a/src/ui/style/point_style.rs b/src/ui/style/point_style.rs
index ee3607cd..14091e40 100644
--- a/src/ui/style/point_style.rs
+++ b/src/ui/style/point_style.rs
@@ -63,7 +63,7 @@ impl canvas::Program for GlyphCanvas {
stroke.clone(),
);
}
- 4 => frame.stroke(&line(Point::new(cx, cy - r), Point::new(cx, cy + r)), stroke.clone()),
+ 4 => frame.stroke(&line(Point::new(cx, cy), Point::new(cx, cy - r)), stroke.clone()),
_ => {}
}
if self.mode & 32 != 0 {