diff --git a/CHANGELOG.ar.md b/CHANGELOG.ar.md index c135848..76bac34 100644 --- a/CHANGELOG.ar.md +++ b/CHANGELOG.ar.md @@ -1,5 +1,17 @@ # سجل التغييرات +## 0.30.1 +- **مُصلح: ملفات G-code من Creality Print كانت بلا صورة معاينة**، في صفحة البداية وفي اللوحة الجانبية. + يكتب Creality Print حجم الصورة بصيغة `300*300` أو `300 300`، ولم تكن هذه الصيغة معروفة. +- **ملفات ZIP لها معاينة** في صفحة البداية: صورة المصمّم أو العرض المرسوم الموجود في الملف، وإلا فصورة + أول ملف 3MF أو G-code فيه. ولا تُقرأ إلا تلك الصورة وحدها. +- **مُصلح: أسماء الملفات في صفحة البداية**: كان الاسم الطويل الخالي من المسافات يُقطع عند حافة بطاقة + المعاينة بدل أن يلتفّ إلى سطر ثانٍ، وكانت علامة `&` في الاسم تظهر حرفاً مسطّراً في قائمة الملفات + الأخيرة وقائمة الأخيرة. +- **الإعدادات ← العارض** أصبحت في أربعة مربعات: **لوحة الطباعة**، و**القياس والعرض والفحص** (كان + *أخرى*)، و**صفحة البداية** الجديد، و**الرسوميات**. ويمكن من **صفحة البداية** إيقاف بطاقة المعاينة، + وعندها لا يُقرأ أي ملف حتى تفتحه (مفيد مع محرك شبكة بطيء). + ## 0.30.0 - **اختر أزرار شريط الأدوات** (**الإعدادات ← شريط الأدوات**، أو **☰ ← تخصيص شريط الأدوات…** أو النقر بالزر الأيمن على شريط الأدوات): أخفِ الأزرار التي لا تستخدمها: الرئيسية، فتح، فحص، العرض، قياس، لقطة diff --git a/CHANGELOG.md b/CHANGELOG.md index 05556a8..77b9407 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 0.30.1 +- **Fixed: Creality Print G-code had no preview picture**, on the start page and in the side panel. + Creality Print writes the picture size as `300*300` or `300 300`, which wasn't recognised. +- **ZIP files get a preview** on the start page: the maker's photo or render inside the ZIP, or else + the picture of its first 3MF or G-code. Only that one picture is read. +- **Fixed: file names on the start page**: a long name without spaces was cut off at the edge of the + preview card instead of wrapping, and an `&` in a name showed as an underlined letter in the + recent list and the Recent menu. +- **Settings → Viewer** is now in four boxes: **Build plate**, **Measure, View and Check** (was + *Other*), the new **Start page**, and **Graphics**. **Start page** can switch the preview card + off; then no file is read until you open it (handy on a slow network drive). + ## 0.30.0 - **Choose your toolbar** (**Settings → Toolbar**, also **☰ → Customize toolbar…** or a right-click on the toolbar): hide the buttons you don't use: Home, Open, Check, View, Measure, Screenshot, Theme diff --git a/CHANGELOG.nl.md b/CHANGELOG.nl.md index bd9f90d..bc6acbe 100644 --- a/CHANGELOG.nl.md +++ b/CHANGELOG.nl.md @@ -1,5 +1,20 @@ # Changelog +## 0.30.1 +- **Opgelost: G-code van Creality Print had geen voorbeeldafbeelding**, op de startpagina en in het + zijpaneel. Creality Print schrijft de grootte van de afbeelding als `300*300` of `300 300`, en dat + werd niet herkend. +- **ZIP-bestanden krijgen een voorbeeld** op de startpagina: de foto of render van de maker die erin + zit, en anders de afbeelding van de eerste 3MF of G-code erin. Alleen die ene afbeelding wordt + gelezen. +- **Opgelost: bestandsnamen op de startpagina**: een lange naam zonder spaties werd afgekapt aan de + rand van het voorbeeldkaartje in plaats van door te lopen op een tweede regel, en een `&` in een + naam verscheen als onderstreepte letter in de lijst met recente bestanden en het menu Recent. +- **Instellingen → Weergave** staat nu in vier vakken: **Printplaat**, **Meten, weergave en + controleren** (was *Overig*), het nieuwe **Startpagina**, en **Grafische kaart**. Met + **Startpagina** zet je het voorbeeldkaartje uit; dan wordt geen enkel bestand gelezen tot je het + opent (handig bij een trage netwerkschijf). + ## 0.30.0 - **Kies je werkbalk** (**Instellingen → Werkbalk**, ook **☰ → Werkbalk aanpassen…** of klik met rechts op de werkbalk): verberg de knoppen die je niet gebruikt: Start, Openen, Controleren, Weergave, diff --git a/README.md b/README.md index aee22ba..eb72b73 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ without exporting an STL first. | **One window** | A file you double-click while PrintView3D is open is shown in that window, instead of opening another one. Prefer a window per file? **Settings → Files → Open each file in a new window**. | | **Look inside** | **View** → wireframe, X-ray (see-through), clip from the top with a height slider and a cut that faces you as you turn the view, on their own or both at once, with the cut-away part as a see-through ghost, to check meshes, hidden parts and hollow walls. **Measure** distances, angles and circles. [Details](#view-wireframe-x-ray-and-clip). | | **Check before printing** | **Check** → overhangs, thin walls, holes, tangled (non-manifold) edges, inside-out faces and loose parts, coloured on the model with a count for each. [Details](#check-before-printing). | +| **Start page** | Your recent files, with a preview card for the one under the mouse: the picture saved in a 3MF, G-code (Creality Print too) or FreeCAD file, or the maker's photo in a ZIP, with its name and folder. Can be switched off. [Details](#using-printview3d). | | **Your toolbar** | Hide the buttons you don't use, shorten the view buttons to a name, a key or an icon, and join them into one group (**Settings → Toolbar**). Hidden buttons keep their keys and stay in the **☰** menu. [Details](#toolbar-buttons). | | **Comfortable to use** | Slicer-style camera, drag & drop, recent files, open a whole folder, screenshots, millimetres or inches, and keyboard shortcuts you can change. | @@ -251,7 +252,12 @@ G-code or ZIP. On the start page, hold the mouse over a recent file and a card in the bottom right corner shows its preview picture (3MF projects, G-code and FreeCAD files that have one), its name and the folder it is in. -The card stays in the same place for every file, so you can run down the list and compare. +A ZIP shows the maker's photo or render inside it, or else the picture of its first 3MF or G-code. +The card stays in the same place for every file, so you can run down the list and compare. Don't +want it? Untick **Settings → Viewer → Start page → Show a preview of recent files when the mouse is +over them**; then the files are not read until you open them. + +![The start page: recent files, with the preview card of the one under the mouse](docs/screenshot-start-page.png) **A whole folder:** **☰ → Open folder…** lists the 3D files of a folder in the side panel; step through them with `↑` / `↓` or a click. diff --git a/docs/index.ar.md b/docs/index.ar.md index e46e64d..837b1bf 100644 --- a/docs/index.ar.md +++ b/docs/index.ar.md @@ -69,6 +69,7 @@ hide: | **الصور المصغّرة** | يعرض مستكشف الملفات في Windows معاينات لملفات 3MF وSTL وG-code وFreeCAD (تُثبَّت مع PrintView3D، اختيارية). وعلى macOS (تجريبي) يعرض Finder صوراً مصغّرة لملفات 3MF وSTL وG-code وFreeCAD وOBJ وPLY، ويعرض **Quick Look** (مفتاح المسافة) نماذج STL و3MF وOBJ وPLY بشكل ثلاثي الأبعاد. [التفاصيل](install.md#quick-look). | | **التحديثات** | **التحقق من التحديثات** من الإعدادات، أو مرة يوميًا إذا سمحت بذلك. على Windows تقوم نقرة واحدة بتنزيل الإصدار الجديد والتحقق منه وتثبيته. يظهر بجانب رقم الإصدار في الزاوية نقطة حمراء عند توفر تحديث. [ما الذي يُرسَل](use.md#updates-and-privacy). | | **نافذة واحدة** | الملف الذي تنقر عليه نقرًا مزدوجًا أثناء فتح PrintView3D يظهر في النافذة نفسها بدل فتح نافذة أخرى. تفضّل نافذة لكل ملف؟ **الإعدادات ← الملفات ← فتح كل ملف في نافذة جديدة**. | +| **صفحة البداية** | ملفاتك الأخيرة، مع بطاقة معاينة للملف الذي يمرّ فوقه الماوس: الصورة المحفوظة في ملف 3MF أو G-code (ومنها ملفات Creality Print) أو FreeCAD، أو صورة المصمّم في ملف ZIP، مع اسمه ومجلده. ويمكن إيقافها. [التفاصيل](use.md). | | **شريط أدواتك** | أخفِ الأزرار التي لا تستخدمها، واختصر أزرار العرض إلى اسم أو اختصار أو أيقونة، واضممها في مجموعة واحدة (**الإعدادات ← شريط الأدوات**). تحتفظ الأزرار المخفية باختصاراتها وتبقى في القائمة **☰**. [التفاصيل](use.md#toolbar-buttons). | | **سهل الاستخدام** | كاميرا بأسلوب برامج التقطيع، والسحب والإفلات، والملفات الأخيرة، ولقطات الشاشة. | diff --git a/docs/index.md b/docs/index.md index f22d7d9..e0839b7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -71,6 +71,7 @@ each object a colour of your own, and see the size of every object. | **Thumbnails** | Windows File Explorer shows previews of 3MF, STL, G-code and FreeCAD files (installed with PrintView3D, optional). On macOS (beta), Finder shows thumbnails of 3MF, STL, G-code, FreeCAD, OBJ and PLY files, and **Quick Look** (space bar) shows STL, 3MF, OBJ and PLY models in 3D. [Details](install.md#quick-look). | | **Updates** | **Check for updates** in Settings, or once a day if you allow it. On Windows one click downloads, checks and installs the new version. The version in the corner gets a red dot when an update is ready. [What is sent](use.md#updates-and-privacy). | | **One window** | A file you double-click while PrintView3D is open is shown in that window, instead of opening another one. Prefer a window per file? **Settings → Files → Open each file in a new window**. | +| **Start page** | Your recent files, with a preview card for the one under the mouse: the picture saved in a 3MF, G-code (Creality Print too) or FreeCAD file, or the maker's photo in a ZIP, with its name and folder. Can be switched off. [Details](use.md). | | **Your toolbar** | Hide the buttons you don't use, shorten the view buttons to a name, a key or an icon, and join them into one group (**Settings → Toolbar**). Hidden buttons keep their keys and stay in the **☰** menu. [Details](use.md#toolbar-buttons). | | **Comfortable to use** | Slicer-style camera, drag & drop, recent files, screenshots. | diff --git a/docs/index.nl.md b/docs/index.nl.md index 52e7218..e01b836 100644 --- a/docs/index.nl.md +++ b/docs/index.nl.md @@ -72,6 +72,7 @@ doorzichtige schim. Maak het doorzichtig, geef elk object een eigen kleur en zie | **Miniaturen** | Windows Verkenner toont voorbeelden van 3MF-, STL-, G-code- en FreeCAD-bestanden (geïnstalleerd met PrintView3D, optioneel). Op macOS (bèta) toont de Finder miniaturen van 3MF-, STL-, G-code-, FreeCAD-, OBJ- en PLY-bestanden, en **Quick Look** (spatiebalk) toont STL-, 3MF-, OBJ- en PLY-modellen in 3D. [Details](install.md#quick-look). | | **Updates** | **Controleren op updates** in Instellingen, of één keer per dag als je dat toestaat. Op Windows downloadt, controleert en installeert één klik de nieuwe versie. De versie in de hoek krijgt een rode stip als er een update klaarstaat. [Wat er verstuurd wordt](use.md#updates-and-privacy). | | **Eén venster** | Een bestand waarop je dubbelklikt terwijl PrintView3D open is, verschijnt in dat venster in plaats van in een nieuw venster. Liever een venster per bestand? **Instellingen → Bestanden → Elk bestand in een nieuw venster openen**. | +| **Startpagina** | Je recente bestanden, met een voorbeeldkaartje van het bestand onder de muis: de afbeelding in een 3MF-, G-code- (ook van Creality Print) of FreeCAD-bestand, of de foto van de maker in een ZIP, met de naam en de map. Kan uit. [Details](use.md). | | **Je eigen werkbalk** | Verberg de knoppen die je niet gebruikt, maak de weergaveknoppen korter tot een naam, een toets of een pictogram, en voeg ze samen tot één groep (**Instellingen → Werkbalk**). Verborgen knoppen houden hun toets en blijven in het **☰**-menu. [Details](use.md#toolbar-buttons). | | **Prettig in gebruik** | Camera zoals in een slicer, slepen en neerzetten, recente bestanden, schermafbeeldingen. | diff --git a/docs/inspect.md b/docs/inspect.md index becb437..dcad2ab 100644 --- a/docs/inspect.md +++ b/docs/inspect.md @@ -163,7 +163,7 @@ found (thin walls and the mesh problems; not overhangs, which most models have). Hover over a name to see what it means and its technical term. The highlights follow the cuts and the objects you hide. Which checks are on, the angle, the nozzle width and the loose-part size are remembered. The colours go back to their defaults when you restart, unless you tick **Settings → Viewer → Remember the colours of the Check -section on restart** (in its **Other** box). *New in PrintView3D 0.29.* +section on restart** (in its **Measure, View and Check** box). *New in PrintView3D 0.29.* ![Check model with X-ray: the overhangs inside the housing in red, the 1 mm wall in front of the hidden pocket as a thin wall (with a 1.2 mm nozzle) in orange](assets/inside/check.png) diff --git a/docs/screenshot-start-page--ember.webp b/docs/screenshot-start-page--ember.webp new file mode 100644 index 0000000..be51216 Binary files /dev/null and b/docs/screenshot-start-page--ember.webp differ diff --git a/docs/screenshot-start-page--light.webp b/docs/screenshot-start-page--light.webp new file mode 100644 index 0000000..106a9d1 Binary files /dev/null and b/docs/screenshot-start-page--light.webp differ diff --git a/docs/screenshot-start-page.png b/docs/screenshot-start-page.png new file mode 100644 index 0000000..cfce296 Binary files /dev/null and b/docs/screenshot-start-page.png differ diff --git a/docs/screenshot-theme-picker--ember.webp b/docs/screenshot-theme-picker--ember.webp new file mode 100644 index 0000000..041fe8e Binary files /dev/null and b/docs/screenshot-theme-picker--ember.webp differ diff --git a/docs/screenshot-theme-picker--light.webp b/docs/screenshot-theme-picker--light.webp new file mode 100644 index 0000000..13d7464 Binary files /dev/null and b/docs/screenshot-theme-picker--light.webp differ diff --git a/docs/screenshot-theme-picker.png b/docs/screenshot-theme-picker.png index 9aa8365..5cc2f31 100644 Binary files a/docs/screenshot-theme-picker.png and b/docs/screenshot-theme-picker.png differ diff --git a/docs/use.ar.md b/docs/use.ar.md index bad129a..9aac0b7 100644 --- a/docs/use.ar.md +++ b/docs/use.ar.md @@ -7,8 +7,12 @@ ويعيد أيضًا الذاكرة التي استخدمها الملف، وهذا يُحدث فرقًا خاصةً بعد ملف G-code أو ZIP كبير. في صفحة البداية، مرّر المؤشر فوق ملف حديث فتُظهر بطاقة في الزاوية السفلية صورة معاينته (لمشاريع 3MF -وملفات G-code وFreeCAD التي تحتوي على صورة)، واسمه والمجلد الذي يوجد فيه. وتبقى البطاقة في المكان نفسه -لكل ملف، فتستطيع المرور على القائمة والمقارنة. +وملفات G-code وFreeCAD التي تحتوي على صورة)، واسمه والمجلد الذي يوجد فيه. ويُظهر ملف ZIP صورة المصمّم +أو العرض المرسوم الموجود فيه، وإلا فصورة أول ملف 3MF أو G-code فيه. وتبقى البطاقة في المكان نفسه +لكل ملف، فتستطيع المرور على القائمة والمقارنة. لا تريدها؟ ألغِ تحديد **الإعدادات ← العارض ← صفحة البداية ← +عرض معاينة للملفات الأخيرة عند مرور الماوس فوقها**؛ وعندها لا تُقرأ الملفات حتى تفتحها. + +![صفحة البداية: الملفات الأخيرة، مع بطاقة معاينة الملف الذي يمرّ فوقه الماوس](screenshot-start-page.png) **مجلد كامل:** يعرض **☰ ← فتح مجلد…** الملفات ثلاثية الأبعاد في مجلد داخل اللوحة الجانبية؛ تنقّل بينها بـ `↑` / `↓` أو بالنقر. @@ -212,6 +216,8 @@ التبديل فوري؛ تتغيّر معه خلفية العرض ثلاثي الأبعاد واللوحة والشبكة، بينما تبقى ألوان G-code والنماذج كما هي لتطابق برنامج التقطيع دائمًا. +![الإعدادات ← المظهر، مع بطاقة مصوّرة لكل نمط من أنماط السمات](screenshot-theme-picker.png) + ![قائمة السمات في شريط الأدوات](screenshot-theme-menu.png) ![ملف G-code نفسه بالسمات الداكنة والرمادية والغرافيت والفاتحة](screenshot-themes.png) diff --git a/docs/use.md b/docs/use.md index a6688d8..4c1a431 100644 --- a/docs/use.md +++ b/docs/use.md @@ -9,7 +9,12 @@ G-code or ZIP. On the start page, hold the mouse over a recent file and a card in the bottom right corner shows its preview picture (3MF projects, G-code and FreeCAD files that have one), its name and the folder it is in. -The card stays in the same place for every file, so you can run down the list and compare. +A ZIP shows the maker's photo or render inside it, or else the picture of its first 3MF or G-code. +The card stays in the same place for every file, so you can run down the list and compare. Don't +want it? Untick **Settings → Viewer → Start page → Show a preview of recent files when the mouse is +over them**; then the files are not read until you open them. + +![The start page: recent files, with the preview card of the one under the mouse](screenshot-start-page.png) **A whole folder:** **☰ → Open folder…** lists the 3D files of a folder in the side panel; step through them with `↑` / `↓` or a click. @@ -227,6 +232,8 @@ next style; **`Ctrl+Shift+T`** switches between light and dark. The switch is immediate; the 3D background, plate and grid change with it, while G-code and model colours stay the same so they always match your slicer. +![Settings → Appearance with a picture card for every theme style](screenshot-theme-picker.png) + ![The theme menu on the toolbar](screenshot-theme-menu.png) ![The same G-code in the Dark, Gray, Graphite and Light themes](screenshot-themes.png) diff --git a/docs/use.nl.md b/docs/use.nl.md index 0a59003..50a583f 100644 --- a/docs/use.nl.md +++ b/docs/use.nl.md @@ -9,8 +9,13 @@ verschil na een grote G-code of ZIP. Houd op de startpagina de muis boven een recent bestand en een kaartje rechtsonder toont de voorbeeldafbeelding (bij 3MF-projecten, G-code en FreeCAD-bestanden die er een hebben), de naam en de -map waarin het staat. Het kaartje staat voor elk bestand op dezelfde plek, zodat je de lijst kunt -aflopen en vergelijken. +map waarin het staat. Een ZIP toont de foto of render van de maker die erin zit, en anders de +afbeelding van de eerste 3MF of G-code erin. Het kaartje staat voor elk bestand op dezelfde plek, +zodat je de lijst kunt aflopen en vergelijken. Liever niet? Vink **Instellingen → Weergave → +Startpagina → Een voorbeeld van recente bestanden tonen als de muis erboven is** uit; dan worden de +bestanden pas gelezen als je ze opent. + +![De startpagina: recente bestanden, met het voorbeeldkaartje van het bestand onder de muis](screenshot-start-page.png) **Een hele map:** **☰ → Map openen…** zet de 3D-bestanden van een map in het zijpaneel; ga erdoor met `↑` / `↓` of een klik. @@ -229,6 +234,8 @@ volgende. Dezelfde keuze staat in **Instellingen → Uiterlijk → Thema**. Het wisselen gaat meteen; de 3D-achtergrond, de plaat en het raster veranderen mee, terwijl de kleuren van de G-code en het model gelijk blijven, zodat ze altijd overeenkomen met je slicer. +![Instellingen → Uiterlijk, met een kaart met afbeelding voor elke themastijl](screenshot-theme-picker.png) + ![Het themamenu in de werkbalk](screenshot-theme-menu.png) ![Dezelfde G-code in de thema's Donker, Grijs, Grafiet en Licht](screenshot-themes.png) diff --git a/pyproject.toml b/pyproject.toml index 9b2c6dd..82197e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "printview3d" -version = "0.30.0" +version = "0.30.1" description = "Lightweight viewer for STL, 3MF, G-code and FreeCAD files with one-click hand-off to your slicer; FreeCAD files go to the slicer as a model directly" readme = "README.md" license = { text = "MIT" } diff --git a/src/printview3d/__init__.py b/src/printview3d/__init__.py index cc0ace9..9339a92 100644 --- a/src/printview3d/__init__.py +++ b/src/printview3d/__init__.py @@ -1,5 +1,5 @@ """PrintView3D - a lightweight viewer for STL, 3MF and G-code files.""" -__version__ = "0.30.0" +__version__ = "0.30.1" APP_NAME = "PrintView3D" APP_ID = "PrintView3D" diff --git a/src/printview3d/loaders/__init__.py b/src/printview3d/loaders/__init__.py index 9d999bd..2f69253 100644 --- a/src/printview3d/loaders/__init__.py +++ b/src/printview3d/loaders/__init__.py @@ -1,6 +1,7 @@ """File loaders: STL, 3MF, OBJ, PLY, OFF, glTF/GLB, G-code, FreeCAD documents and STEP.""" import os +from typing import Optional from .gcode import FEATURES, GcodeData, parse_gcode_file, parse_gcode_text from .formats import load_gltf, load_obj, load_off, load_ply @@ -44,13 +45,77 @@ def load_file(path: str, progress=None, freecad_path: str = ""): raise ValueError(f"Unsupported file type: {os.path.basename(path)}") +# A picture file inside a ZIP: at least this big (smaller ones are icons and logos), at most this +# big (anything larger is no picture to preview). +_ZIP_PICTURE_MIN = 2 * 1024 +_ZIP_3MF_MAX = 32 * 1024 ** 2 # a 3MF inside a ZIP is read for its picture up to this size +_PICTURE_FOLDERS = ("images", "image", "pictures", "picture", "photos", "photo", "renders", "render") + + +def _is_picture(data: bytes) -> bool: + return data[:8] == b"\x89PNG\r\n\x1a\n" or data[:3] == b"\xff\xd8\xff" + + +def zip_picture(path: str) -> Optional[bytes]: + """A preview for a ZIP of 3D files (Printables, Thingiverse, ...), first that it has: a picture + file in it (the maker's photo or render, from a folder such as images/ first), the picture of + its first 3MF, then that of its first G-code. Only the one part needed is read, within the + same limits as opening the ZIP.""" + import io + import re + import zipfile + from . import gcode, mesh, zipsafe + + def natural(text): + return [int(t) if t.isdigit() else t.lower() for t in re.split(r"(\d+)", text)] + + with zipfile.ZipFile(path) as z: + infos = [] + for info in z.infolist(): + parts = [p for p in info.filename.replace("\\", "/").split("/") if p] + if info.is_dir() or not parts or any(p == "__MACOSX" or p.startswith(".") for p in parts): + continue + infos.append((info, [p.lower() for p in parts])) + + pictures = [(i, p) for i, p in infos if p[-1].endswith((".png", ".jpg", ".jpeg")) + and _ZIP_PICTURE_MIN <= i.file_size <= zipsafe.MAX_SMALL_BYTES] + # Pictures in a folder such as images/ first; otherwise as the ZIP lists them (the download + # sites put the main picture first). + pictures.sort(key=lambda ip: not any(f in _PICTURE_FOLDERS for f in ip[1][:-1])) + for info, _ in pictures: + data = zipsafe.read(z, info.filename, zipsafe.MAX_SMALL_BYTES) + if _is_picture(data): + return data + + by_name = sorted(infos, key=lambda ip: natural(ip[0].filename)) + for info, parts in by_name: + if parts[-1].endswith(".3mf") and not zipsafe.too_big(info, _ZIP_3MF_MAX): + try: + with zipfile.ZipFile(io.BytesIO(zipsafe.read(z, info.filename, _ZIP_3MF_MAX))) as inner: + data = mesh._3mf_picture(inner, inner.namelist()) + except (zipfile.BadZipFile, ValueError, KeyError, OSError): + data = None + if data: + return data + for info, parts in by_name: + if parts[-1].endswith(GCODE_EXTENSIONS) and not zipsafe.too_big(info): + with z.open(info) as f: + data = gcode.thumbnail_from_head(f.read(gcode.THUMBNAIL_HEAD)) + if data: + return data + return None + + def read_thumbnail(path: str): """Only the preview picture saved in a file (PNG or JPEG bytes), or None: 3MF projects, - G-code and FreeCAD documents can have one. Quick: the model itself is not read.""" + G-code and FreeCAD documents can have one, and a ZIP of 3D files gets one from what is in it + (zip_picture). Quick: the model itself is not read.""" import zipfile from . import gcode, mesh, zipsafe kind = file_kind(path) try: + if path.lower().endswith(".zip"): + return zip_picture(path) if kind == "3mf": return mesh.read_3mf_thumbnail(path) if kind == "gcode": diff --git a/src/printview3d/loaders/gcode.py b/src/printview3d/loaders/gcode.py index 3ecf46b..c71e04d 100644 --- a/src/printview3d/loaders/gcode.py +++ b/src/printview3d/loaders/gcode.py @@ -101,7 +101,9 @@ def classify_feature(name: str) -> int: ("_bed", re.compile(r"^\s*(?:printable_area|bed_shape)\s*=\s*(.+)$", re.I)), ] -_THUMB_BEGIN = re.compile(r"^\s*(thumbnail|thumbnail_png|thumbnail_jpg|png|jpg|jpeg)\s+begin\s+(\d+)\s*x\s*(\d+)", re.I) +# "thumbnail begin 300x300 1234" (PrusaSlicer, OrcaSlicer, ...); Creality Print writes the size as +# "300*300" (png / jpg blocks for its printers' screens) or "300 300" (thumbnail block). +_THUMB_BEGIN = re.compile(r"^\s*(thumbnail|thumbnail_png|thumbnail_jpg|png|jpg|jpeg)\s+begin\s+(\d+)\s*(?:[x*]\s*|\s+)(\d+)", re.I) def parse_bed_shape(value) -> Optional[tuple[float, float, float, float]]: @@ -176,11 +178,19 @@ def n_segments(self) -> int: # -------------------------------------------------------------------------- # Parser # -------------------------------------------------------------------------- -def read_thumbnail(path: str, limit: int = 4 * 1024 * 1024) -> Optional[bytes]: +THUMBNAIL_HEAD = 4 * 1024 * 1024 # slicers write the pictures at the top: this much is read + + +def read_thumbnail(path: str, limit: int = THUMBNAIL_HEAD) -> Optional[bytes]: """The largest preview picture in a G-code file's header, without reading the toolpath. Slicers write the pictures at the top, so only the first `limit` bytes are read.""" with open(path, "rb") as f: - head = f.read(limit).decode("utf-8", errors="replace") + return thumbnail_from_head(f.read(limit)) + + +def thumbnail_from_head(raw_head: bytes) -> Optional[bytes]: + """The largest preview picture in the first part of a G-code file (also one inside a ZIP).""" + head = raw_head.decode("utf-8", errors="replace") best: tuple[int, bytes] | None = None word, size, buf = None, 0, [] for raw in head.splitlines(): diff --git a/src/printview3d/settings.py b/src/printview3d/settings.py index 89a848f..9884a79 100644 --- a/src/printview3d/settings.py +++ b/src/printview3d/settings.py @@ -49,6 +49,7 @@ "check_nozzle": 0.4, # Check: thin walls are thinner than this (mm) "check_show": {}, # Check: which highlights are on (kind -> bool; missing: the default) "check_keep_colors": False, # Settings -> Viewer: remember the Check colours (else: defaults at every start) + "recent_preview": True, # Settings -> Viewer -> Start page: the preview card of a recent file on hover "check_colors": {}, # Check: the colours picked, kept only with check_keep_colors "check_loose_max": 5.0, # Check: loose parts are at most this big (mm, their longest side) "units": "mm", # Settings -> Language and units: "mm" or "in" (inches) diff --git a/src/printview3d/ui/main_window.py b/src/printview3d/ui/main_window.py index c470a8e..1def670 100644 --- a/src/printview3d/ui/main_window.py +++ b/src/printview3d/ui/main_window.py @@ -13,7 +13,7 @@ import numpy as np from PySide6.QtCore import QEvent, QObject, QSize, Qt, QTimer, QUrl, Signal from PySide6.QtGui import (QAction, QActionGroup, QColor, QDesktopServices, QFont, QFontMetrics, QGuiApplication, QIcon, - QImage, QKeySequence, QPixmap, QShortcut) + QImage, QKeySequence, QPixmap, QShortcut, QTextLayout, QTextOption) from PySide6.QtWidgets import ( QAbstractItemView, QApplication, QCheckBox, QColorDialog, QDialog, QDialogButtonBox, QFileDialog, QFormLayout, QFrame, QGridLayout, QGroupBox, QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QMainWindow, QMenu, QMessageBox, QProgressBar, QPushButton, QScrollArea, @@ -166,6 +166,33 @@ def recent_picture(path: str) -> Optional[QImage]: return _recent_pics[key] +def wrap_lines(text: str, font: QFont, width: int) -> str: + """`text` with a line break wherever it wraps in `width`: at a space (or a zero-width space) + where possible, inside a long word where not, the same rule fit_lines measures with. A label + wraps only at spaces, so a long name without any ("3DBenchy_PLA_K1C_220_60.gcode") was cut off + at the card's edge.""" + layout = QTextLayout(text, font) + opt = QTextOption() + opt.setWrapMode(QTextOption.WrapAtWordBoundaryOrAnywhere) + layout.setTextOption(opt) + layout.beginLayout() + parts = [] + while True: + line = layout.createLine() + if not line.isValid(): + break + line.setLineWidth(width) + parts.append(text[line.textStart():line.textStart() + line.textLength()]) + layout.endLayout() + return "\n".join(parts) + + +def plain_menu_text(text: str) -> str: + """A file name for a button or menu: "&" stays "&" (Qt reads a single "&" as a key marker, + so "Tom & Jerry.stl" showed as "Tom _Jerry.stl").""" + return text.replace("&", "&&") + + def breakable_path(text: str) -> str: """A path that a label may wrap after each folder separator (a zero-width space after it).""" return text.replace("\\", "\\\u200b").replace("/", "/\u200b") @@ -195,7 +222,7 @@ class _RecentButton(QPushButton): start page shows its preview card; the picture is only read then.""" def __init__(self, path: str, on_hover=None, parent=None): - super().__init__(os.path.basename(path), parent) + super().__init__(plain_menu_text(os.path.basename(path)), parent) self.path = path self.on_hover = on_hover @@ -239,9 +266,11 @@ def __init__(self, parent=None): self.picture.setAlignment(Qt.AlignCenter) self.picture.setWordWrap(True) lay.addWidget(self.picture) + # File names are plain text (a name like "x.gcode" is no formatting) with the line breaks + # put in by wrap_lines. self.name = QLabel() self.name.setObjectName("recentPreviewName") - self.name.setWordWrap(True) + self.name.setTextFormat(Qt.PlainText) self.name.setAlignment(Qt.AlignLeft | Qt.AlignTop) lay.addWidget(self.name) self.folder_title = QLabel() @@ -249,7 +278,7 @@ def __init__(self, parent=None): lay.addWidget(self.folder_title) self.folder = QLabel() self.folder.setObjectName("recentPreviewFolder") - self.folder.setWordWrap(True) + self.folder.setTextFormat(Qt.PlainText) self.folder.setAlignment(Qt.AlignLeft | Qt.AlignTop) lay.addWidget(self.folder) self.setFixedWidth(PREVIEW_PX + 24) @@ -264,6 +293,11 @@ def _fix_heights(self): def show_file(self, path: str): self.path = path + # The theme's fonts (the name is bold and larger) before anything is measured: a card that + # was never shown still has the default font, and the name ran over its edge. + self.ensurePolished() + for label in (self.name, self.folder, self.folder_title): + label.ensurePolished() self.folder_title.setText(tr("Folder")) self._fix_heights() img = recent_picture(path) @@ -279,10 +313,13 @@ def show_file(self, path: str): pm.setDevicePixelRatio(dpr) self.picture.setText("") self.picture.setPixmap(pm) - width = PREVIEW_PX - self.name.setText(fit_lines(os.path.basename(path), self.name.fontMetrics(), width, PREVIEW_NAME_LINES)) - self.folder.setText(breakable_path(fit_lines(os.path.dirname(path), self.folder.fontMetrics(), width, - PREVIEW_FOLDER_LINES))) + # The text's width: the card's border leaves the labels 2 px narrower than the picture, + # and a few pixels of room keep the longest line off the edge. + width = PREVIEW_PX - 6 + name = fit_lines(os.path.basename(path), self.name.fontMetrics(), width, PREVIEW_NAME_LINES) + self.name.setText(wrap_lines(name, self.name.font(), width)) + folder = fit_lines(os.path.dirname(path), self.folder.fontMetrics(), width, PREVIEW_FOLDER_LINES) + self.folder.setText(wrap_lines(breakable_path(folder), self.folder.font(), width)) self.adjustSize() @@ -4052,6 +4089,8 @@ def _settings_closed(self, dlg, result: int): def _settings_applied(self): """Settings saved (OK or Apply): show what changed.""" self._update_slicer_button() + if not self.settings.get("recent_preview", True): + self.recent_preview.hide() # Settings -> Viewer -> Start page: switched off self.apply_toolbar_layout() # FreeCAD just chosen: show the open FreeCAD file in 3D now. freecad_before = getattr(self, "_freecad_before", self.settings.get("freecad_path", "")) @@ -4235,7 +4274,7 @@ def _refresh_recent_menu(self): hdr.setAlignment(Qt.AlignCenter) self.welcome_recent.addWidget(hdr) for i, p in enumerate(recent): - self.recent_menu.addAction(f"{os.path.basename(p)} — {os.path.dirname(p)}", + self.recent_menu.addAction(plain_menu_text(f"{os.path.basename(p)} — {os.path.dirname(p)}"), lambda path=p: self.load(path)) if i < 6: b = _RecentButton(p, self._recent_hover) # hover: the preview card @@ -4249,9 +4288,10 @@ def _refresh_recent_menu(self): def _recent_hover(self, path: Optional[str]): """The mouse is over a recent file on the start page (None: over none): show its preview - card, or hide it.""" + card, or hide it. Settings -> Viewer -> Start page can switch the card off; then nothing + is read.""" card = self.recent_preview - if not path: + if not path or not self.settings.get("recent_preview", True): card.hide() return card.show_file(path) diff --git a/src/printview3d/ui/settings_dialog.py b/src/printview3d/ui/settings_dialog.py index 9e33e4d..955720d 100644 --- a/src/printview3d/ui/settings_dialog.py +++ b/src/printview3d/ui/settings_dialog.py @@ -361,7 +361,8 @@ def __init__(self, settings, parent=None, gpu_info=None, page: str = ""): # 3D view: travel moves, view cube, Measure, Clip, Check) and Graphics. g3 = QGroupBox(tr("Build plate")) l3 = QVBoxLayout(g3) - g_other = QGroupBox(tr("Other")) + # The settings of the toolbar's Measure, View and Check (named after those buttons). + g_other = QGroupBox(tr("Measure, View and Check")) l_other = QVBoxLayout(g_other) self.travel_cb = QCheckBox(tr("Show travel moves when opening G-code")) self.travel_cb.setChecked(bool(settings.get("show_travel"))) @@ -511,6 +512,16 @@ def __init__(self, settings, parent=None, gpu_info=None, page: str = ""): for k in keys_col: k.setMinimumWidth(width) + # ---------------- Start page ---------------- + g_start = QGroupBox(tr("Start page")) + l_start = QVBoxLayout(g_start) + self.recent_preview_cb = QCheckBox(tr("Show a preview of recent files when the mouse is over them")) + self.recent_preview_cb.setChecked(bool(settings.get("recent_preview", True))) + self.recent_preview_cb.setToolTip( + tr("Off: no preview card, and the files are not read until you open them (handy on a slow " + "network drive).")) + l_start.addWidget(self.recent_preview_cb) + # ---------------- Graphics ---------------- g4 = QGroupBox(tr("Graphics")) l4 = QVBoxLayout(g4) @@ -538,7 +549,7 @@ def __init__(self, settings, parent=None, gpu_info=None, page: str = ""): self.gpu_hint.setWordWrap(True) self.gpu_hint.setObjectName("muted") l4.addWidget(self.gpu_hint) - self._add_page("viewer", tr("Viewer"), [g3, g_other, g4], keep_titles=True) + self._add_page("viewer", tr("Viewer"), [g3, g_other, g_start, g4], keep_titles=True) self._add_page("keys", tr("Keyboard and mouse"), [self._build_mouse(), self._build_keys()], keep_titles=True) @@ -1314,6 +1325,7 @@ def _save(self, close: bool = True): self.settings.set("clip_handles", self.clip_handles_cb.isChecked()) keys.save(self.settings, self._key_values) self.settings.set("check_keep_colors", self.check_colors_cb.isChecked()) + self.settings.set("recent_preview", self.recent_preview_cb.isChecked()) self.settings.set("plate_mode", self.plate_combo.currentData()) self.settings.set("plate_size", [round(self.plate_w_spin.mm(), 3), round(self.plate_d_spin.mm(), 3)]) self.settings.set("show_view_cube", self.cube_cb.isChecked()) diff --git a/tests/test_cursors_and_settings.py b/tests/test_cursors_and_settings.py index 46b1c6c..b6c8bc8 100644 --- a/tests/test_cursors_and_settings.py +++ b/tests/test_cursors_and_settings.py @@ -213,15 +213,16 @@ def test_settings_files_page_has_the_screenshot_folder(window, app): # ------------------------------------------------------------ the Viewer page -def test_viewer_page_is_build_plate_other_graphics(window): +def test_viewer_page_is_build_plate_tools_start_page_graphics(window): from PySide6.QtWidgets import QGroupBox from printview3d.ui.settings_dialog import SettingsDialog w = window dlg = SettingsDialog(w.settings, w, {}, page="viewer") page = dlg.pages.widget(dlg._page_keys.index("viewer")).widget() boxes = [b for b in page.findChildren(QGroupBox) if b.parent() is page] - assert [b.title() for b in boxes] == ["Build plate", "Other", "Graphics"] - plate, other = boxes[0], boxes[1] + assert [b.title() for b in boxes] == ["Build plate", "Measure, View and Check", "Start page", "Graphics"] + plate, other, start = boxes[0], boxes[1], boxes[2] + assert start.isAncestorOf(dlg.recent_preview_cb) for cb in (dlg.bed_cb, dlg.plate_axes_cb, dlg.grid_labels_cb, dlg.plate_combo): assert plate.isAncestorOf(cb) for cb in (dlg.travel_cb, dlg.cube_cb, dlg.line_slider, dlg.ghost_slider, dlg.plane_slider, diff --git a/tests/test_keys.py b/tests/test_keys.py index 93b3eb5..eb50410 100644 --- a/tests/test_keys.py +++ b/tests/test_keys.py @@ -216,10 +216,10 @@ def test_recent_file_preview_card_stays_in_one_place(app, window, tmp_path): # The whole name where it fits on its lines; shortened in the middle only where it doesn't # (that depends on the system's font: the CI machines' differ from Windows'). name = os.path.basename(p) - assert card.name.text() == mw.fit_lines(name, card.name.fontMetrics(), mw.PREVIEW_PX, mw.PREVIEW_NAME_LINES) - shown = card.name.text() + shown = card.name.text().replace("\n", "") # wrap_lines' line breaks taken out + assert shown == mw.fit_lines(name, card.name.fontMetrics(), mw.PREVIEW_PX - 6, mw.PREVIEW_NAME_LINES) assert shown == name or (shown.startswith(name[:6]) and shown.endswith(name[-6:]) and "\u2026" in shown) - assert card.folder.text().replace("\u200b", "") == os.path.dirname(os.path.abspath(p)) + assert card.folder.text().replace("\u200b", "").replace("\n", "") == os.path.dirname(os.path.abspath(p)) places.append((card.geometry().getRect(), card.picture.geometry().getRect())) assert len(set(places)) == 1 # same place, same size, every file # In the start page's bottom right corner. diff --git a/tests/test_recent_preview.py b/tests/test_recent_preview.py new file mode 100644 index 0000000..6585545 --- /dev/null +++ b/tests/test_recent_preview.py @@ -0,0 +1,196 @@ +"""The start page's recent files and their preview card: Creality Print's G-code pictures, file names +with "&" or "<", and long names without spaces.""" + +import base64 +import os + +import pytest + +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + +from printview3d.settings import Settings # noqa: E402 + +# A 2 x 1 PNG (red, blue) and a 4 x 4 PNG. +PNG_2x1 = base64.b64decode("iVBORw0KGgoAAAANSUhEUgAAAAIAAAABCAIAAAB7QOjdAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAAD0lE" + "QVQImWP4z8DAwPAfAAcAAf/89wURAAAAAElFTkSuQmCC") + + +@pytest.fixture +def app(): + from PySide6.QtWidgets import QApplication + return QApplication.instance() or QApplication([]) + + +def _png(w: int, h: int) -> bytes: + from PySide6.QtCore import QBuffer, QByteArray, QIODevice + from PySide6.QtGui import QColor, QImage + img = QImage(w, h, QImage.Format_RGB32) + img.fill(QColor("teal")) + ba = QByteArray() + buf = QBuffer(ba) + buf.open(QIODevice.WriteOnly) + img.save(buf, "PNG") + return bytes(ba) + + +def _photo(w: int, h: int) -> bytes: + """A picture with detail (a photo-sized PNG; a plain colour packs to under 2 KB, like an icon).""" + import random + from PySide6.QtCore import QBuffer, QByteArray, QIODevice + from PySide6.QtGui import QImage + rnd = random.Random(1) + img = QImage(w, h, QImage.Format_RGB32) + for y in range(h): + for x in range(w): + img.setPixel(x, y, rnd.getrandbits(24)) + ba = QByteArray() + buf = QBuffer(ba) + buf.open(QIODevice.WriteOnly) + img.save(buf, "PNG") + return bytes(ba) + + +def _block(word: str, size: str, data: bytes) -> str: + return f"; {word} begin {size} {len(data)}\n; " + base64.b64encode(data).decode() + f"\n; {word} end\n" + + +def test_creality_print_pictures_are_found(app, tmp_path): + """Creality Print writes "png begin 96*96 ..." and "thumbnail begin 300 300 ..." (no "x"): the + pictures were missed, so its G-code had no picture on the start page nor in the side panel.""" + from PySide6.QtGui import QImage + from printview3d.loaders import read_thumbnail + from printview3d.loaders.gcode import parse_gcode_file + small, big = _png(2, 2), _png(6, 6) + g = tmp_path / "creality.gcode" + g.write_text(_block("png", "2*2 7757 0 95 192", small) + _block("thumbnail", "6 6", big) + + "G1 X1 Y1 E1\nG1 X2 Y2 E2\n", encoding="utf-8") + for data in (read_thumbnail(str(g)), parse_gcode_file(str(g)).thumbnail): + img = QImage.fromData(data) + assert (img.width(), img.height()) == (6, 6) # the largest of the two + + +def test_long_names_without_spaces_wrap_instead_of_being_cut(app): + from PySide6.QtGui import QFont, QFontMetrics + from printview3d.ui import main_window as mw + f = QFont() + f.setPixelSize(14) + fm = QFontMetrics(f) + name = "3DBenchy_PLA_K1C_220_60_with_a_much_longer_name.gcode" + out = mw.wrap_lines(name, f, 200) + lines = out.split("\n") + assert len(lines) > 1 and "".join(lines) == name + assert all(fm.horizontalAdvance(line) <= 200 for line in lines) + words = mw.wrap_lines("a printed file with spaces in its rather long name.gcode", f, 200).split("\n") + assert len(words) > 1 and all(not w.startswith(" ") or w.strip() for w in words) + + +def test_names_with_ampersand_and_angle_brackets_show_as_they_are(app, tmp_path): + from printview3d.ui import main_window as mw + folder = tmp_path / "Tom & Jerry" + folder.mkdir() + amp = folder / "Tom & Jerry plate.gcode" + amp.write_text("G1 X1 Y1\n", encoding="utf-8") + tag = tmp_path / "bold vase.gcode" + try: + tag.write_text("G1 X1 Y1\n", encoding="utf-8") + except OSError: # Windows doesn't allow < > in names + tag = None + s = Settings(str(tmp_path / "settings.json")) + for p in [amp] + ([tag] if tag else []): + s.add_recent(str(p)) + w = mw.MainWindow(s) + try: + w.resize(1280, 800) + w.show() + app.processEvents() + buttons = {os.path.basename(b.path): b for b in w.findChildren(mw._RecentButton)} + assert buttons["Tom & Jerry plate.gcode"].text() == "Tom && Jerry plate.gcode" # shows one "&" + menu = [a.text() for a in w.recent_menu.actions() if "Jerry" in a.text()] + assert menu and menu[0].startswith("Tom && Jerry plate.gcode") + card = w.recent_preview + w._recent_hover(str(amp)) + assert card.name.text().replace("\n", "") == "Tom & Jerry plate.gcode" + if tag: + from PySide6.QtCore import Qt + w._recent_hover(str(tag)) + assert card.name.textFormat() == Qt.PlainText + assert card.name.text().replace("\n", "") == "bold vase.gcode" + finally: + w.close() + + +# ------------------------------------------------------------------ ZIP files +def _zip(path, files: dict): + import zipfile + with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as z: + for name, data in files.items(): + z.writestr(name, data) + return str(path) + + +def _3mf_bytes(picture: bytes) -> bytes: + import io + import zipfile + buf = io.BytesIO() + with zipfile.ZipFile(buf, "w") as z: + z.writestr("3D/3dmodel.model", "") + z.writestr("Metadata/thumbnail.png", picture) + return buf.getvalue() + + +def test_zip_preview_prefers_the_makers_picture_then_3mf_then_gcode(app, tmp_path): + from PySide6.QtGui import QImage + from printview3d.loaders import read_thumbnail + photo, plate, sliced = _photo(64, 48), _png(30, 30), _png(20, 10) + icon = _png(4, 4) # under 2 KB: an icon, skipped + + def size(data): + img = QImage.fromData(data) + return img.width(), img.height() + + both = _zip(tmp_path / "printables.zip", {"files/part.3mf": _3mf_bytes(plate), "images/photo.png": photo}) + assert size(read_thumbnail(both)) == (64, 48) + only_3mf = _zip(tmp_path / "k2.zip", {"logo.png": icon, "Tray.3mf": _3mf_bytes(plate)}) + assert size(read_thumbnail(only_3mf)) == (30, 30) + gcode = _block("png", "20*10 1 2 3", sliced) + "G1 X1 Y1 E1\n" + only_gcode = _zip(tmp_path / "print.zip", {"model.stl": b"solid x\nendsolid x\n", "print.gcode": gcode}) + assert size(read_thumbnail(only_gcode)) == (20, 10) + nothing = _zip(tmp_path / "plain.zip", {"model.stl": b"solid x\nendsolid x\n", "readme.txt": b"hi", + "__MACOSX/._photo.png": photo}) + assert read_thumbnail(nothing) is None + broken = tmp_path / "broken.zip" + broken.write_bytes(b"PK\x03\x04 not really a zip") + assert read_thumbnail(str(broken)) is None + + +def test_the_preview_card_can_be_switched_off(app, tmp_path): + from printview3d.ui import main_window as mw + from printview3d.ui.settings_dialog import SettingsDialog + g = tmp_path / "x.gcode" + g.write_text(_block("thumbnail", "6x6", _png(6, 6)) + "G1 X1 Y1\n", encoding="utf-8") + s = Settings(str(tmp_path / "settings.json")) + s.add_recent(str(g)) + w = mw.MainWindow(s) + try: + w.resize(1280, 800) + w.show() + app.processEvents() + w._recent_hover(str(g)) + assert w.recent_preview.isVisible() + # Opened directly with a stand-in GPU name, like the other Settings tests: asking the + # graphics driver (open_settings does) crashes on the headless macOS CI machines. + d = SettingsDialog(s, w, {}, page="viewer") + d.accepted.connect(w._settings_applied) # as open_settings connects OK + titles = [b.title() for b in d.pages.currentWidget().widget().findChildren(type(d.findChild( + type(d.toolbar_joined_cb.parentWidget()))))] + assert titles.index("Build plate") < titles.index("Measure, View and Check") \ + < titles.index("Start page") < titles.index("Graphics") + assert d.recent_preview_cb.isChecked() + d.recent_preview_cb.setChecked(False) + d._save() # OK + app.processEvents() + assert s.get("recent_preview") is False and not w.recent_preview.isVisible() + w._recent_hover(str(g)) + assert not w.recent_preview.isVisible() # stays hidden, nothing read + finally: + w.close() diff --git a/tools/make_screenshots.py b/tools/make_screenshots.py index 5293d16..cb5eaae 100644 --- a/tools/make_screenshots.py +++ b/tools/make_screenshots.py @@ -38,8 +38,18 @@ def themed_name(name: str, theme_name: str) -> str: return f"{stem.replace('.', '-')}--{theme_name}.webp" +ONLY: set[str] = set() # --only: the pictures to make (names without extension); empty: all + + +def wanted(path: str) -> bool: + stem = os.path.splitext(os.path.basename(path))[0].split("--")[0] + return not ONLY or stem in ONLY + + def save_image(img, path: str): """Save a QImage/QPixmap; .webp through Pillow (smaller than PNG, sharp enough for screenshots).""" + if not wanted(path): + return if not path.endswith(".webp"): img.save(path) return @@ -64,7 +74,9 @@ def main(): ap = argparse.ArgumentParser() ap.add_argument("--theme", default="dark", choices=["dark", "light", "ember"]) ap.add_argument("--no-demo", action="store_true", help="don't rewrite the examples' thumbnails") + ap.add_argument("--only", default="", help="only these pictures, e.g. screenshot-start-page,screenshot-theme-picker") args = ap.parse_args() + ONLY.update(n.strip() for n in args.only.split(",") if n.strip()) dark = args.theme == "dark" app = QApplication([]) from printview3d.settings import Settings @@ -216,7 +228,36 @@ def camera(azim, elev, zoom=1.0): for _ in range(5): app.processEvents() - if not dark: # the theme pictures and FreeCAD: Dark only + # 7b) Settings -> Appearance: a picture card for every theme style (the one shown chosen) + settings.set("theme", args.theme) + d = SettingsDialog(settings, w, {}, page="appearance") + d.show() + for _ in range(5): + app.processEvents() + plain_buttons(d) + app.processEvents() + save_image(d.grab(), os.path.join(DOCS, themed_name("screenshot-theme-picker.png", args.theme))) + print("saved theme picker") + d.reject() + app.processEvents() + + # 7c) The start page: recent files, with the preview card of the one under the mouse + # Copies in a plainly named folder, so the card shows "3D prints" rather than this machine's path. + import shutil + prints = os.path.join(tempfile.gettempdir(), "3D prints") + os.makedirs(prints, exist_ok=True) + for f in ("demo_vase.stl", "demo_vase.gcode", "demo_housing.3mf", "demo_plate.3mf"): + shutil.copy2(os.path.join(EX, f), prints) + settings.add_recent(os.path.join(prints, f)) + w.go_home() + w._refresh_recent_menu() + for _ in range(5): + app.processEvents() + w._recent_hover(os.path.join(prints, "demo_plate.3mf")) + shot("screenshot-start-page.png") + w._recent_hover(None) + + if not dark or ONLY: # the theme pictures and FreeCAD: Dark only, and not with --only w.close() return diff --git a/translations/printview3d.ts b/translations/printview3d.ts index 0d35799..7d0a160 100644 --- a/translations/printview3d.ts +++ b/translations/printview3d.ts @@ -2199,7 +2199,7 @@ - Other + Measure, View and Check @@ -2330,6 +2330,21 @@ Off: the Check colours you pick last until PrintView3D closes. + + + Start page + + + + + Show a preview of recent files when the mouse is over them + + + + + Off: no preview card, and the files are not read until you open them (handy on a slow network drive). + + Graphics diff --git a/translations/printview3d_ar.ts b/translations/printview3d_ar.ts index c241b7e..1007a3e 100644 --- a/translations/printview3d_ar.ts +++ b/translations/printview3d_ar.ts @@ -2199,8 +2199,8 @@ - Other - أخرى + Measure, View and Check + القياس والعرض والفحص @@ -2330,6 +2330,21 @@ Off: the Check colours you pick last until PrintView3D closes. عند إيقافه: تبقى ألوان الفحص التي تختارها حتى إغلاق PrintView3D. + + + Start page + صفحة البداية + + + + Show a preview of recent files when the mouse is over them + عرض معاينة للملفات الأخيرة عند مرور الماوس فوقها + + + + Off: no preview card, and the files are not read until you open them (handy on a slow network drive). + عند الإيقاف: لا تظهر بطاقة المعاينة، ولا تُقرأ الملفات حتى تفتحها (مفيد مع محرك شبكة بطيء). + Graphics @@ -2967,6 +2982,10 @@ {v} cm³ {v} سم³ + + Other + أخرى + Buttons (left to right) الأزرار (من اليسار إلى اليمين) diff --git a/translations/printview3d_de.ts b/translations/printview3d_de.ts index 8a4230e..68ffb05 100644 --- a/translations/printview3d_de.ts +++ b/translations/printview3d_de.ts @@ -2199,8 +2199,8 @@ - Other - Sonstiges + Measure, View and Check + Messen, Ansicht und Prüfen @@ -2330,6 +2330,21 @@ Off: the Check colours you pick last until PrintView3D closes. Aus: Die gewählten Prüffarben gelten, bis PrintView3D geschlossen wird. + + + Start page + Startseite + + + + Show a preview of recent files when the mouse is over them + Eine Vorschau zuletzt geöffneter Dateien zeigen, wenn die Maus darüber ist + + + + Off: no preview card, and the files are not read until you open them (handy on a slow network drive). + Aus: keine Vorschaukarte, und die Dateien werden erst beim Öffnen gelesen (praktisch bei einem langsamen Netzlaufwerk). + Graphics @@ -2967,6 +2982,10 @@ {v} cm³ {v} cm³ + + Other + Sonstiges + Buttons (left to right) Schaltflächen (von links nach rechts) diff --git a/translations/printview3d_es.ts b/translations/printview3d_es.ts index 24efe3a..1258b4f 100644 --- a/translations/printview3d_es.ts +++ b/translations/printview3d_es.ts @@ -2199,8 +2199,8 @@ - Other - Otros + Measure, View and Check + Medir, vista y comprobar @@ -2330,6 +2330,21 @@ Off: the Check colours you pick last until PrintView3D closes. Desactivado: los colores de Comprobar que elijas duran hasta que se cierre PrintView3D. + + + Start page + Página de inicio + + + + Show a preview of recent files when the mouse is over them + Mostrar una vista previa de los archivos recientes al pasar el ratón + + + + Off: no preview card, and the files are not read until you open them (handy on a slow network drive). + Desactivado: sin tarjeta de vista previa, y los archivos no se leen hasta que los abres (útil en una unidad de red lenta). + Graphics @@ -2967,6 +2982,10 @@ {v} cm³ {v} cm³ + + Other + Otros + Buttons (left to right) Botones (de izquierda a derecha) diff --git a/translations/printview3d_fr.ts b/translations/printview3d_fr.ts index fb45008..93245c2 100644 --- a/translations/printview3d_fr.ts +++ b/translations/printview3d_fr.ts @@ -2199,8 +2199,8 @@ - Other - Autres + Measure, View and Check + Mesurer, affichage et vérifier @@ -2330,6 +2330,21 @@ Off: the Check colours you pick last until PrintView3D closes. Désactivé : les couleurs de Vérifier que vous choisissez durent jusqu'à la fermeture de PrintView3D. + + + Start page + Page d'accueil + + + + Show a preview of recent files when the mouse is over them + Afficher un aperçu des fichiers récents au survol de la souris + + + + Off: no preview card, and the files are not read until you open them (handy on a slow network drive). + Désactivé : pas de carte d'aperçu, et les fichiers ne sont lus qu'à leur ouverture (pratique sur un lecteur réseau lent). + Graphics @@ -2967,6 +2982,10 @@ {v} cm³ {v} cm³ + + Other + Autres + Buttons (left to right) Boutons (de gauche à droite) diff --git a/translations/printview3d_nl.ts b/translations/printview3d_nl.ts index 3bec073..1b85418 100644 --- a/translations/printview3d_nl.ts +++ b/translations/printview3d_nl.ts @@ -2199,8 +2199,8 @@ - Other - Overig + Measure, View and Check + Meten, weergave en controleren @@ -2330,6 +2330,21 @@ Off: the Check colours you pick last until PrintView3D closes. Uit: de gekozen kleuren van Controleren gelden tot PrintView3D sluit. + + + Start page + Startpagina + + + + Show a preview of recent files when the mouse is over them + Een voorbeeld van recente bestanden tonen als de muis erboven is + + + + Off: no preview card, and the files are not read until you open them (handy on a slow network drive). + Uit: geen voorbeeldkaart, en de bestanden worden pas gelezen als je ze opent (handig bij een trage netwerkschijf). + Graphics @@ -2967,6 +2982,10 @@ {v} cm³ {v} cm³ + + Other + Overig + Buttons (left to right) Knoppen (van links naar rechts) diff --git a/translations/printview3d_ro.ts b/translations/printview3d_ro.ts index 27e0862..62a9931 100644 --- a/translations/printview3d_ro.ts +++ b/translations/printview3d_ro.ts @@ -2199,8 +2199,8 @@ - Other - Altele + Measure, View and Check + Măsurare, vizualizare și verificare @@ -2330,6 +2330,21 @@ Off: the Check colours you pick last until PrintView3D closes. Oprit: culorile alese în Verifică rămân până se închide PrintView3D. + + + Start page + Pagina de start + + + + Show a preview of recent files when the mouse is over them + Arată o previzualizare a fișierelor recente când mouse-ul este deasupra lor + + + + Off: no preview card, and the files are not read until you open them (handy on a slow network drive). + Oprit: fără card de previzualizare, iar fișierele nu sunt citite până nu le deschizi (util pe o unitate de rețea lentă). + Graphics @@ -2967,6 +2982,10 @@ {v} cm³ {v} cm³ + + Other + Altele + Buttons (left to right) Butoane (de la stânga la dreapta)