Linux Mint/Ubuntu desktop fixes + plugin-API extensions + NEN 1414 stamp fix (v1.46.0)#220
Conversation
The frontend gated appWindow.show() on a double requestAnimationFrame to wait for the first paint. On WebKitGTK builds where the accelerated compositor stalls before the first paint (observed on Linux Mint 22.3 + Mesa 25.2.8 + Intel UHD CML GT2) the inner rAF never fires, the outer Promise never resolves, and show() is never called. The main process survives with an unmapped window permanently hidden. Remove the rAF gate and call show() directly after init. Mirrors the pattern already used by open-speech-studio (src/App.tsx:270). The silent try/catch around show() is also dropped so failures surface instead of being swallowed. Refs OpenAEC-Foundation#217
Parsing these flags inside tauri::Builder::default().run() caused the full event loop to start and the single-instance DBus name to be registered. The process then neither printed anything nor exited, and because the DBus name was squatted, subsequent normal launches would silently exit as if another instance were running. Parse --version / -V and --help / -h from std::env::args() before any Tauri initialisation, print the expected output and exit(0). The argv-filter for PDF file paths (used for file associations) is kept unchanged. Refs OpenAEC-Foundation#219
The fileAssociations entry for .pdf was missing the mimeType field, so tauri-bundler could not emit `MimeType=application/pdf` into the .desktop file on Linux. As a result, xdg-mime had no record of Open PDF Studio being able to open PDFs, and the in-app "Set as default" button had nothing to point at. Add mimeType and a short description. The Tauri bundler translates this per platform: .desktop MimeType on Linux, UTType/Info.plist on macOS, registry ProgID on Windows. No platform-conditional code required.
is_default_pdf_app and open_default_apps_settings were Windows-only, so the in-app "Set as default" banner was permanently displayed on Linux (is_default_pdf_app always returned false) and the button did nothing (open_default_apps_settings returned Ok(false) without any side effect). Add Linux branches using xdg-mime. This is desktop-environment agnostic (works on Cinnamon, GNOME, KDE, MATE, XFCE) and distro agnostic (Mint, Ubuntu, Fedora, Arch). The Windows branch is unchanged and the macOS / BSD / other fallback remains a graceful no-op, so cross-platform behaviour is preserved.
Vite inlines PNGs <4KB als data: URIs. rasterSvg() prependde
window.location.origin aan alle non-http URLs, waardoor data:
URIs corrupt werden ("http://localhost:3041data:image/png;...").
- Check op leading "/" (root-relative), niet op "http"
- Update glob naar Vite 7 syntax (query/import i.p.v. deprecated as)
- Bump versie 1.45.0 -> 1.46.0
Plugins can now persist their custom annotation types into saved PDFs by
implementing an optional serializeToPdf method on their AnnotationHandler.
When the saver encounters an annotation whose type is not in the built-in
switch (e.g. symitech.titlebar), it now looks the type up in the plugin
annotation-type registry and, if a handler with serializeToPdf is found,
awaits a call passing { pdfDoc, page, annotation, convertX, convertY }.
Plugins use the shared pdf-lib pdfDoc + page to draw via standard pdf-lib
APIs.
Without this hook, plugin annotations were silently dropped at save (the
switch had no default case). Existing behavior for plugins that do NOT
provide serializeToPdf is preserved (still dropped) so this change is
strictly additive.
Errors thrown by plugin serializers are logged but do not abort the save
pipeline - other annotations and pages still serialize.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plugins drawing custom annotations need page-pt dimensions to position
content reliably. Deriving these from the canvas DOM via getTransform()
or canvas.clientWidth gave wrong results when loading existing PDFs (the
ctx scale matrix did not always reflect doc.scale * devicePixelRatio).
The pluginClickTool now augments the state object passed to
typeHandler.create() with:
- pageWidth / pageHeight (in PDF points, derived from the active doc's
canvasEl using doc.scale * window.devicePixelRatio)
- docScale (current per-document zoom)
- devicePixelRatio (so plugins can refine if needed)
- currentPage (already used downstream; surfaced for parity)
Existing fields on `state` are preserved (spread first), so this is
backwards compatible for plugins that ignore the new keys.
Verified: Symitech titlebar plugin now renders correctly both in "Nieuw
blanco" and "open existing PDF" flows. Without this change, loaded PDFs
caused titlebar layout to overflow page bounds because the plugin
inferred a 1750-pt page width from canvas.clientWidth.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Blank "Nieuw" documents have doc.filePath === null, which gated the
vector render path (renderer.js) where setPage() is normally called.
Without setPage(), window.__pdfViewport.pageW / pageH stayed at the
initial 0 values, so any consumer reading those (notably plugin
annotation handlers via plugin-tool.js) got zero and was forced to
fall back to A4 defaults regardless of the actual page format
(A3 portrait, landscape, etc.).
Fix is in two parts:
1. renderer.js — call setPage() with page.view dimensions when
doc.filePath is missing, using a stable per-doc surrogate key
`__memory__${doc.id}` so setPage's "isNewDocument" detection
still triggers fitToViewport on first render.
2. plugin-tool.js — read page dimensions from window.__pdfViewport
directly (the canonical source) instead of doc.canvasEl, which
was never assigned anywhere in the codebase. Fail loud with a
console.warn if the singleton is not initialized.
Reproduced with the Symitech Tekentool plugin: A4 portrait worked by
coincidence (matched fallback), A3 landscape placed the titelblok on
the right half of the page constrained to A4 dimensions. After fix,
debug log confirms vp.pageW = 1191, vp.pageH = 842 for A3 landscape,
and titelblok layout matches the actual page.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three connected fixes for blank ("Nieuw") documents:
1. doc.pageDims cache (loader.js + renderer.js + plugin-tool.js)
Plugin annotation handlers need page dimensions in PDF points at
click time. Previously they read window.__pdfViewport.pageW/pageH,
which is initialized to 0 for blank docs because the vector path
that calls setPage() is gated by `_hasFilePath`. Move the source of
truth to doc.pageDims[pageNum] = { widthPt, heightPt }, populated
by createBlankPDF (with the user-chosen dimensions, available
immediately) and by every renderPage from page.view.
2. Zoom controls call renderPage instead of renderPageOffscreen
(renderer.js: zoomIn / zoomOut / setZoom / _applyZoom / actualSize)
The legacy zoom path (used when window.__pdfViewport.active is
false, i.e. blank docs) was calling renderPageOffscreen, which is
for export rendering and never updates the visible canvas. Switch
the legacy branch to renderPage so the visible canvas actually
re-renders at the new scale.
3. Ctrl+wheel zoom for blank docs (navigation-events.js)
The wheel handler returned early when viewport.active was false,
leaving ctrl+wheel zoom completely broken for blank docs. Route
that case through zoomIn / zoomOut so the same legacy doc.scale
path is exercised.
4. Page centering when smaller than viewport (styles/layout.css)
#pdf-container.visible used align-items: flex-start, which pinned
small canvases (the typical blank-doc case at low zoom) to the top
of the container. Use `align-items: safe center` and
`justify-content: safe center` so the page is centered in both
axes when it fits, and falls back to top/left when it overflows so
scroll origin stays reachable.
Together these make blank-doc placement of plugin annotations
(Symitech titelblok) work correctly for A4 and A3 in both portrait
and landscape, with zoom controls behaving as expected.
This supersedes commit c313bd7 (the earlier setPage-based fix),
which left the viewport in a half-active state for blank docs and
caused annotation transforms to drift from the bitmap pdf-canvas at
non-default zoom levels.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plugins kunnen nu een eigen renderer registreren voor het Eigenschappen-paneel
van hun custom annotation-types. Wanneer zo'n annotation geselecteerd wordt,
mount OPPS de plugin-DOM in plaats van de built-in sections.
API:
api.registerPropertyPanel(typeName, renderFn)
renderFn(annotation, updateAnnotProp, onCommit, onCancel) -> HTMLElement
Files:
- js/plugins/property-panel-registry.js (nieuw, registry-map)
- js/plugins/plugin-api.js (registerPropertyPanel + cleanup)
- js/solid/stores/propertiesStore.js (customPanelRender signal +
set in storeShowProperties / storeHideProperties)
- js/solid/components/properties-panel/CustomPluginPanel.jsx (nieuw,
Solid-component die plugin-DOM mount via createEffect)
- js/solid/components/properties-panel/PropertiesPanel.jsx (slot na
CustomFieldsSection)
Branch: linux-mint-fix (geen upstream-PR, lokaal voor Symitech-plugin v0.2.2 B2)
Spec: Symitech-Tekentool-V2/docs/superpowers/specs/2026-04-29-symitech-titelblok-edit-mode-v022.md
Plugin custom panels (registered via registerPropertyPanel) need to write nested annotation fields like 'data.address.email'. The default branch previously did `currentAnnotation[key] = value`, creating a literal "data.address.email" property instead of walking the chain. Now: when key contains '.', we split, walk/create intermediate objects, write to the leaf. Mirroring into the flat Solid store is skipped for dot-paths since plugin panels read from their own form-state. Used by Symitech tekentool plugin for live re-render of titelblok edits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plugins die een AnnotationType registreren met `drawMode: 'polyline'`
worden nu correct gerouteerd door de tool-dispatcher en de polyline-tool,
zodat de plugin-handler.create() wordt aangeroepen op finish met de
verzamelde polylinePoints in enrichedState. Voorheen viel polyline-mode
ten onrechte terug op de shape-tool ('box'), waardoor een drag-rechthoek
i.p.v. een multi-click polyline werd gestart, en op _finishPolyline werd
altijd een generiek `type: 'polyline'`-annotation gemaakt zonder de
plugin-create() ooit aan te roepen.
Dispatch-flow na patch:
1. tool-dispatcher.js handlePointerDown / handlePointerMove / handlePointerUp:
- drawMode === 'click' -> _plugin_click tool (ongewijzigd)
- drawMode === 'polyline' -> native polyline-tool (nieuw)
- andere drag-modes -> shape-tool 'box' (ongewijzigd)
- polyline pointer-up -> no-op (placement is click-driven, finish
via right-click / dblclick in polyline-tool zelf)
2. polyline-tool.js _finishPolyline:
- Plugin-handler met drawMode='polyline' + create() -> typeHandler.create(
0, 0, 0, 0, null, enrichedState) waarbij enrichedState polylinePoints,
docScale, devicePixelRatio, pageWidth/Height en currentPage bevat.
Resultaat wordt door ctx.createAnnotation() verrijkt met page +
state.toolOverrides.
- Anders -> generieke 'polyline'-annotation (legacy gedrag, identiek aan
voor de patch).
Additief: zonder geregistreerde plugin-handler voor state.currentTool blijft
het gedrag exact gelijk aan voor deze commit. Geen fallbacks of
silent-catches: als typeHandler.create() null teruggeeft wordt er simpelweg
geen annotation gemaakt (guard `if (annProps)`).
Gebruikt door de Symitech-plugin (v0.2.2) voor o.a. `symitech.scheur` en
`symitech.vloer-contour`, zodat die multi-click polyline kunnen tekenen
in plaats van drag-rechthoeken.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…sClass Drie clean toevoegingen aan plugin-API zodat plugins UI buiten het native eigenschappen-paneel kunnen mounten zonder DOM-scraping of MutationObserver- fallbacks: - selection-listener-registry.js (NEW): registerSelectionListener(typeName, fn) geeft plugins een directe selectie-event-channel. fn(annotation) bij select, fn(null) bij deselect/multi-select/close. fired vanuit propertiesStore bij elk currentAnnotation-mutatiepunt (showProperties, hideProperties, closePanel, showMultiSelection). - nativePanelHidden signal: api.setNativePanelVisible(false) verbergt PropertiesPanel volledig zodat plugin alle controles in eigen palette/UI kan tonen. Auto-restore bij plugin-deactivate via _cleanup. CustomPluginPanel vuurt extra deselect-signal (renderFn(null)) zodat plugin externe DOM kan unmounten. - PaletteDescriptor.cssClass: optioneel extra class op .tp-docked/.tp-float root, zodat plugin-CSS direct kan targeten (.symitech-plugin) zonder MutationObserver-tag-hack. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…bnails Voor de paginaminiaturen-paneel rendert OPPS de PDF via de Rust backend (skip_images=true) of via de PDF.js fallback met annotationMode=0. Beide paden lieten plugin- en Solid-store-annotations weg, wat resulteerde in lege/onvolledige preview-thumbnails (Symitech-titelblok, schade-symbolen etc. waren onzichtbaar in het linker-paneel). Fix: na elke render-pad een overlay-stap die alle annotations voor die page filtert + drawAnnotation aanroept op het thumbnail-canvas met ctx.scale(scale). Verwerkt: - Rust-pad: image inladen op nieuw canvas, drawImage, overlay, re-encode - PDF.js-pad: direct op dezelfde ctx vóór toDataURL Tolerant op individuele render-fouten (try/catch per annotation) zodat 1 stuk-annotation niet de hele thumbnail breekt. Sidenote: live-reflection van edits in thumbnail-panel volgt later. Huidige flow refresht thumbnail bij page-reload of expliciete invalidateThumbnail-aanroep, niet per keystroke (zou expensive zijn).
Foundation for the upcoming "tool group" feature: a ToolDefinition can
declare an optional `subTools` array, turning the entry into a sub-menu
group. The host (G4b) will render such a tool as a single palette button
that morphs to show the active sub-tool's icon and opens a pop-out on
click. This commit is data + state layer ONLY — no rendering yet.
What:
- palette-registry.js: extend ToolDefinition JSDoc with `subTools?` field
and a paragraph documenting tool-group rendering semantics.
- tool-group-state.js: new module. Reactive Solid signal storing
Map<groupId, currentSubToolId>. Exports getActiveSubToolId,
getActiveSubTool (with subTools[0] fallback), setActiveSubTool,
clearAllToolGroups, getToolGroupStateSnapshot. In-memory only;
no localStorage by design.
- plugin-api.js: add `api.features = { toolGroups: true }` so plugins
can feature-detect support via `api.features?.toolGroups === true`.
Wire `clearAllToolGroups()` into the existing _cleanup() path so
plugin-deactivate resets stale per-group selections.
Why:
- Establishes a stable contract before UI work, so plugins can already
emit `subTools` and feature-detect support without depending on the
host actually rendering pop-outs yet.
- Keeps the data layer testable and isolated from JSX (G4b).
ExtensionToolPalette now renders any ToolDefinition with a non-empty
`subTools` array as a tool-group: a single morphing main button (icon =
active sub-tool icon, reactive via getActiveSubTool) plus a small
chevron marker, and a pop-out sub-menu that lists the sub-tools as
icon buttons.
Behavior:
- Click main button: activates current sub-tool (default = subTools[0])
and toggles the pop-out open/closed (B1+P1).
- Click sub-tool: setActiveSubTool + setTool + apply overrides + close.
- Click outside or press Escape: closes the menu (mode persists).
- Only one sub-menu open at a time across the app (single openGroupId
signal).
- Pop-out side-aware: docked-left palette pops out to the right,
docked-right pops out to the left, floating pops out to the right.
Implementation:
- New ExtToolGroupBtn component selected via <Show when={isGroup}> in
ExtToolList; flat ExtToolBtn rendering preserved unchanged.
- side prop threaded from DockedExtPalette/FloatingExtPalette through
ExtToolList into ExtToolGroupBtn.
- Outside-click + keydown listeners attached/detached reactively via
createEffect, cleaned up via onCleanup.
CSS additions in styles/tool-palette.css cover .tp-btn-chevron,
.tp-submenu (with from-docked-left/from-docked-right/from-float
positioning variants), and .tp-btn-group-wrap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address review feedback on 6a01125: - CRITICAL: Sub-menu was clipped by docked-palette overflow:hidden because position:absolute does not escape clipping ancestors. Fix: render the menu via Solid <Portal> with position:fixed and coordinates computed from the wrap's getBoundingClientRect() on open. Recompute on resize while open. - Replace [data-tool-group-id] outside-click selector with wrapRef.contains() and drop the data-tool-group-id attribute (no longer needed); the menuRef.contains() check covers the portaled menu. - Drop the inline style="position:relative; display:inline-flex" on the wrap; CSS already declares those rules. Keep position:relative on the wrap purely to anchor the absolutely-positioned chevron. - Extract overridesEqual() to module scope and reuse in ExtToolBtn, ExtToolGroupBtn.isActive, and the per-sub subActive accessor (3 sites, identical null-handling preserved). - Add aria-haspopup="menu" + aria-expanded to the main group button. - CSS: remove .from-docked-{left,right,float} positioning rules (left:100% / right:100% / margin-*); positioning is now driven by inline style. Keep visual rules (background, border, shadow, padding, z-index, button size override). ExtToolBtn behavior preserved (isActive uses the new helper but returns identical results for all input shapes including both-null). Verified: vite build succeeds, esbuild syntax-check passes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- CustomPluginPanel.jsx: read annotProps.id reactively inside createEffect so the renderer re-mounts when selection swaps to another annotation of the same plugin-type. customPanelRender() alone stops tracking after the first dispatch on the same renderFn reference, leaving the panel stale on A→B swaps within one type. - propertiesStore.js: document the dot-path contract on updateAnnotProp, including the literal "." restriction on plugin field-names and the no-mirror-into-annotProps behaviour, so plugin authors can rely on it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Per-doc per-page generation counter, bumped on invalidateThumbnail. Render-completions whose generation no longer matches are discarded so a stale snapshot (e.g. older annotation state from an in-flight render) cannot overwrite a newer cache entry on rapid invalidate sequences. - Replace empty catches in both Rust and PDF.js overlay paths with console.warn that includes pageNum + annotation id/type, so a single broken plugin annotation can no longer fail silently in production. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Distinguish two failure modes: - xdg-utils not installed → return install hint (`apt install xdg-utils`) instead of raw NotFound IO error. - xdg-mime non-zero exit → mention the most likely cause (missing /usr/share/applications/Open PDF Studio.desktop because the app was started from a dev build rather than the bundled .deb/.AppImage). Both paths used to surface as opaque error strings in the settings dialog, leaving users without a clear next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Linux/Chromium ignores background-color/color on native <select> and falls back to the OS widget, which leaves the new-document dropdown unreadable in dark-theme. Force appearance:none + draw the dropdown arrow as a CSS gradient so the theme tokens (--theme-surface, --theme-text) are honoured. Apply matching colours on <option> and <optgroup> for consistent open-state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Quality-audit follow-up (4 commits)After re-auditing the plugin-API and thumbnail-overlay commits we pushed earlier, four issues were worth fixing before this PR is reviewed. All four are additive and do not change any existing public contract.
No behavioural changes to the existing public APIs introduced by the earlier commits in this PR. All fixes are scoped to a single file each. CI is rerunning on macOS, Ubuntu 22.04 and Windows. |
|
Ready for review @mojtabakarimi — no rush, please review whenever it suits your schedule. CI is green on all three platforms and the PR description has a structured walkthrough (sections A/B/C) to make it easier to navigate. Happy to address any feedback. |
…plugins Plugins that build analytics widgets (counts, dashboards, summaries) need read-access to the active document's annotations without subscribing to every mutation. Two new optional methods on the plugin API: - api.getAnnotations() - returns the live store-array of annotations on the active document (each entry has a 1-indexed `page` field), or [] when no document is open. Read-only snapshot intended for polling consumers. - api.getPageCount() - returns the active document's total page-count (>= 1) or 0 when no document is open. Combined with getCurrentPage() this is enough for per-page analytics. Both are additive and feature-detectable. The Symitech inspectie-dashboard (symitech.tools v0.2.2) needs this — without it, its count-by-type widget read window.app.documentManager which OPPS does not expose, so counts silently stayed at zero. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plugin-API addition (1 commit)Field-testing the Symitech inspectie-dashboard plugin against this branch surfaced one missing API. Adding it here keeps the PR self-contained.
CI is rerunning; expect the same green result as before. |
Wraps buildAnnotationProps call met try/finally; flag wordt gelezen door Symitech SP2 plugin (v0.2.4) om counter-allocators te skippen tijdens de per-pointer-move drag-create preview-render-loop. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…allback Plugin-annotations (Symitech SP2: scheur, doorvoer-polyline-closed, doorvoer-line-contour, niet-onderzocht-polyline-closed, vloer-contour) krijgen automatisch per-vertex polyline_node-handles zodra ze een points-array exposeren. Vereist voor SP2 v0.2.4 vertex-edit-gizmos (Task 4). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pplyResize Plugin-annotations met points-array reageren op polyline_node_<i>-handle drag identiek aan builtin polyline-case. Inclusief shift-snap (45 graden) en auto bbox-recalc voor types die x/y/width/height tracken. Vereist voor SP2 v0.2.4 vertex-edit-gizmos (Task 5). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e-snap Bij e.shiftKey + actieve tool's plugin handler met snapHook-functie: nieuwe vertex (en preview-vertex) snapt naar laatst-gecommitteerde vertex via handler.snapHook(last.x, last.y, cur.x, cur.y). Toegepast in onPointerDown (commit) en onPointerMove (preview). Voor SP2 v0.2.4 shift-snap met 0/30/45/60/90-per-kwadrant set (Tasks 7-9). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tijdens polyline-drawing: als cursor < 8/scale screen-px van eerste vertex (en >=3 vertices al gezet) → snap cursor naar eerste vertex + toon cyan cirkel-indicator. Klik in deze state = sluit polygon (state.closed = true wordt aan plugin's typeHandler.create doorgegeven via _finishPolyline). Voor SP2 v0.2.4 close-contour-snap (Task 10). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ypes Plugin-annotations (e.g. symitech.schade, symitech.scheur, symitech.vloer-contour) couldn't be moved with the hand-tool because the applyMove switch had no default branch — drag-state was set up correctly but the actual coordinate-shift was a no-op for any unknown type. Add a default case that shifts whichever well-known position-bearing fields are present on the annotation: x/y, startX/Y + endX/Y, points[], path[]. Plugins that need custom semantics can still opt out via annotation.locked = true (handled at the top of the function). This unblocks the entire Symitech plugin's interaction model: schade- markers, reeks-markers, scheur-polylines, doorvoer/niet-onderzocht shapes and vloer-contour all become draggable without each plugin having to monkey-patch transforms.js or register its own move-handler. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…case
Plugin point-marker types (symitech.schade, symitech.reeks,
symitech.doorvoer.point-marker) store their position as
`annotation.at = {x, y}` rather than top-level x/y. Previous
default-case only shifted top-level x/y/startX/etc, so dragging
these annotations with the hand-tool silently no-op'd.
Also handle annotation.cx/cy for circle/ellipse-shaped plugin types.
Plugin rect-area + oval-area types (symitech.doorvoer.rect-area,
symitech.doorvoer.oval-area, symitech.niet-onderzocht.rect-area,
symitech.niet-onderzocht.oval-area) slaan geometry op als {x, y, w, h}
i.p.v. builtin width/height. Default-case in handles.js zag dat niet
en gaf alleen polyline-node handles als er points-array was, dus
rect/oval-area types kregen GEEN handles na placement.
Fix:
- handles.js: default-case detecteert {x,y,w,h} en emit 4 corner +
4 edge handles.
- transforms.js applyResize: default-case interpreteert TL/TR/BL/BR/T/
B/L/R en muteert annotation.w/h met dezelfde logica als de built-in
box-case (incl. min-10 size-guard).
User-eis: "vormen na plaatsing controlpoints geven om grootte te
tweaken", uitgesloten van resize blijven point-marker types
(symitech.schade, symitech.reeks, symitech.doorvoer.point-marker)
omdat die via annotation.at niet via {x,y,w,h} of points werken.
Plugin polyline-handlers krijgen nu de volledige polylinePoints array mee als 5e argument van snapHook. Maakt segment-relatieve snap mogelijk (perpendicular/parallel op het laatste afgesloten segment) zodat rect/cube tekenen onder een willekeurige starthoek met shift haaks gehouden blijft. Pre-existing handlers die alleen de eerste 4 args gebruiken blijven functioneel: 5e arg is optioneel.
User: "als ik een scheur teken kost het 1 rechtermuisklik om af te sluiten maar dan opent direct het selectiemenu". Race tussen pointerdown(button=2) en contextmenu-event: - pointerdown finished polyline en zet isDrawingPolyline=false - daarna fired contextmenu, ziet drawing-flag false en opent menu Fix: polyline-tool zet `state._suppressNextContextmenu = true` zodra het rechtermuisklik-finish afhandelt. canvas-contextmenu-handler ziet flag, slikt het volgende contextmenu in (preventDefault) en wist de flag. 2e rechtermuisklik (na sluiten) opent normaal het selectie-menu.
Built-in polyline auto-resette naar select-tool na _finishPolyline; voor plugin-types met drawMode='polyline' (symitech.scheur, vloer-contour, doorvoer-polyline-closed/line-contour, niet-onderzocht.polyline-closed) hindert dat de workflow: gebruiker wil meerdere scheuren/contouren achter elkaar tekenen zonder elke keer de tool opnieuw te kiezen. Fix: detecteer of de huidige tool een plugin-handler met drawMode= 'polyline' is; sla in dat geval de auto-reset over zodat de tool actief blijft.
Why this PR exists
Open PDF Studio is the substrate we use to build a domain-specific draftboard for Symitech (NL inspection-engineering firm). To make that possible, the original PR opened in April was scoped to fix three independent issues we ran into while bringing the app up on Linux Mint 22.3:
While iterating on that work the same branch grew the plumbing needed by a real plugin (
symitech.tools) until the plumbing itself stabilised. We are surfacing the whole branch now because the parts are tightly co-evolved: the Linux fixes unblock daily work for our team, the plugin-API additions are pure additions that other plugin authors can use today, and the NEN 1414 stamp fix is a one-line glob update riding along because it shares the version bump. CI is green on all three platforms.Table of contents
A. Linux Mint/Ubuntu desktop fixes (4 commits)
Independent desktop-integration fixes. Each lands a real bug we hit on Mint 22.3 / Ubuntu 24.04 daily-driver setups; CI confirms they do not regress Windows or macOS.
e9fab7b5fix: show window immediately after init on LinuxTauri::run(), but the window never becomes visible. Mouse-in-tray confirms the process is alive.appWindow.show()on a doublerequestAnimationFrameto wait for first paint. On WebKitGTK builds where the accelerated compositor stalls before the first paint (reproduced on Mint 22.3 + Mesa 25.2.8 + Intel UHD CML GT2), the inner rAF never fires, the outer Promise never resolves, andshow()is never called.appWindow.show()directly after init. The pre-paint flash users were trying to avoid is barely visible in practice and a permanently hidden window is strictly worse.js/main.js(-12 / +7)MESA_LOADER_DRIVER_OVERRIDE=iris, run a debug build, confirm the window appears.ae1bb2b6fix: handle--versionand--helpbefore starting the event loopopen-pdf-studio --versionprinted nothing, exited with code 0, and any subsequent normal launch silently exited as if another instance were running.tauri::Builder::default().run(). The full event loop started, the single-instance DBus name was registered, the process then exited without printing, and the squatted DBus name made later launches think a peer instance owned the slot.--version/-V/--help/-hfromstd::env::args()before any Tauri builder runs. Print +std::process::exit(0).src-tauri/src/lib.rs(-1 / +27)target/release/open-pdf-studio --versionprints the version and exits cleanly. Immediately afterwards a normal launch creates a window without "another instance is already running" complaints.d4a606c8fix: registerapplication/pdfMIME type for Linux file associationsxdg-mimehad no record of Open PDF Studio claiming PDFs.fileAssociationsentry for.pdfwas missing themimeTypefield, sotauri-bundlercould not emitMimeType=application/pdfinto the generated.desktopfile.mimeType: ["application/pdf"]and a short description. The Tauri bundler translates this into the rightMimeType=line, after whichxdg-mime query default application/pdfcan resolve toopen-pdf-studio.desktop.src-tauri/tauri.conf.json(+2).deb, install, runxdg-mime query default application/pdfafter pressing the in-app "Set as default" button.4a894a27feat(linux): implement default PDF handler viaxdg-mimeis_default_pdf_appandopen_default_apps_settingswere Windows-only. The "Set as default" banner was permanently shown on Linux (becauseis_default_pdf_appalways returnedfalse), and clicking the button returnedOk(false)without any side effect.is_default_pdf_appshells out toxdg-mime query default application/pdfand matches againstopen-pdf-studio.desktop.open_default_apps_settingscallsxdg-mime default open-pdf-studio.desktop application/pdfso the in-app button can complete the round-trip without dragging users into a settings dialog.src-tauri/src/lib.rs(-3 / +35)xdg-mime query default application/pdfreturnsopen-pdf-studio.desktopand the banner disappears on next launch.B. NEN 1414 stamp + Vite 7 (1 commit)
3ae68466fix: NEN 1414 symbols below 4 KB rendered as broken stampsimage not foundplaceholders in the stamp tool. The set varied with build mode (dev vs prod).data:URIs.rasterSvg()was prependingwindow.location.originto every URL that did not start withhttp, producing corrupt strings such ashttp://localhost:3041data:image/png;base64,..../). Update the glob to Vite 7 syntax (import.meta.glob('...', { query: '?url', import: 'default' })) sinceas: 'url'is deprecated. Bumppackage.jsonfrom1.45.0to1.46.0to mark the user-visible fix.js/solid/data/nen1414Library.js,package.json,package-lock.json,Cargo.toml,Cargo.lock,tauri.conf.json(-10 / +11)C. Plugin-API extensions (10 commits)
Plugins author custom annotation types today, but until this branch they could not render those annotations into thumbnails, persist them into the saved PDF, mount their own UI in place of the built-in property panel, or react to selection events without scraping the DOM. Each addition below is a pure addition: existing plugins are unaffected, all new fields and methods are optional.
These extensions are in production use by the private Symitech Tekentool V2 plugin (
symitech.tools). We can demo it on request.Foundations (4 commits)
9621cfc8serializeToPdfhook on plugin handlersjs/pdf/saver.jsfalls back to the plugin annotation-type registry for unknown types and awaits an optionalserializeToPdf({ pdfDoc, page, annotation, convertX, convertY }).884db08dstatepluginClickToolaugments the state object passed totypeHandler.create()withpageWidth,pageHeight,docScale, anddevicePixelRatio. Eliminates fragile ctx-matrix sniffing.c313bd78__pdfViewportsingleton sync for blank docssetPage(), sowindow.__pdfViewport.pageW/pageHstayed at 0 and plugin handlers fell back to A4 defaults regardless of actual page size. Now the singleton is updated for blank docs too.b15ded06doc.pageDimscache + zoom controls + page centeringNew hooks (6 commits)
b844e54fapi.registerPropertyPanel(typeName, renderFn)Plugins replace the built-in properties panel for their own annotation types.
renderFnreturns the root element, OPPS mounts it into the panel slot.nullfalls through to default rendering.Files:
js/plugins/plugin-api.js,js/plugins/property-panel-registry.js(NEW),js/solid/components/properties-panel/CustomPluginPanel.jsx(NEW),js/solid/components/properties-panel/PropertiesPanel.jsx,js/solid/stores/propertiesStore.js.1ea2964aDot-path support in defaultupdateAnnotPropbranchPlugin custom panels need to write nested annotation fields. Before this commit,
updateAnnotProp('data.address.email', x)created a literal string-keyed property. Now: when the key contains., the default branch splits, walks/creates intermediate objects, and writes to the leaf. Mirroring into the flat Solid store is skipped for nested writes (those are plugin-owned).Files:
js/solid/stores/propertiesStore.js(-2 / +22).f041bd09drawMode: 'polyline'for plugin handlersPlugins registering a handler with
drawMode: 'polyline'are now correctly routed by the tool-dispatcher and the polyline-tool. On finish, the plugin handler'screate()is called withstate.polylinePointsset. Previously polyline-mode fell through to the box-tool, producing a drag-rectangle and a generictype: 'polyline'annotation that ignored the plugin handler.Files:
js/tools/tool-dispatcher.js,js/tools/tools/polyline-tool.js.2a0ecc71registerSelectionListener+setNativePanelVisible+ descriptorcssClassThree small additions that let plugins mount UI outside the native properties panel without DOM-scraping or MutationObserver fallbacks.
fireSelectionChange()is called frompropertiesStoreonstoreShowProperties/storeHideProperties/storeClosePanel/storeShowMultiSelection.cssClassremoves the need for plugins to scan the DOM looking for their own palette root.Files:
js/plugins/plugin-api.js,js/plugins/selection-listener-registry.js(NEW),js/plugins/palette-registry.js,js/solid/components/ExtensionToolPalette.jsx,js/solid/components/properties-panel/PropertiesPanel.jsx,js/solid/components/properties-panel/CustomPluginPanel.jsx,js/solid/stores/propertiesStore.js.3d1063bdOverlay plugin/Solid-store annotations on page-thumbnailsThe thumbnails panel renders pages via the Rust backend (
render_thumbnailinvoke withskip_images=true) or via the PDF.js fallback withannotationMode: 0. Both paths skipped plugin and Solid-store annotations, so previews showed empty pages.After each render path,
renderThumbnailToDataURLnow overlays all annotations for that page on the thumbnail canvas at thumbnail scale. Tolerant per-annotation try/catch so one broken handler does not break the whole thumbnail. Zero-cost early-exit when the page has no annotations.Files:
js/ui/panels/left-panel.js(-1 / +63).686c3bf6Tool-group state +features.toolGroupsflagFoundation for sub-menu palette buttons. A
ToolDefinitioncan now declare an optionalsubTools: ToolDefinition[], turning the entry into a sub-menu group.tool-group-state.js(NEW) tracks the active sub-tool per group. The host gate (features.toolGroups) defaults tofalseso this is a no-op for plugins that do not opt in. Render layer (G4b) follows in a later PR.Files:
js/plugins/palette-registry.js,js/plugins/plugin-api.js,js/plugins/tool-group-state.js(NEW).Test coverage
ubuntu-22.04,windows-latest,macos-latestRisk and limitations
e9fab7b5) drops a defensive gatexdg-mimeshell-out (4a894a27) assumesxdg-utilsis installedOk(false)if missing.falseon macOS, banner stays visible. Not regressing existing behaviour.3ae68466clashes with upstream release flowFollow-up roadmap
Items intentionally left out of this PR, in rough priority order:
is_default_pdf_app/open_default_apps_settings(LaunchServices viaLSCopyDefaultApplicationURLForContentType+ adefaults writeround-trip).show(), addressing reviewer concern aboute9fab7b5.registerPropertyPanel,registerSelectionListener,serializeToPdf, dot-pathupdateAnnotProp).Suggested review path
The branch is structured so that each commit compiles and ships independently. Suggested order for reviewers who want to land this incrementally:
9621cfc8,884db08d,c313bd78,b15ded06) are mechanically obvious; the new hooks (b844e54f,1ea2964a,f041bd09,2a0ecc71,3d1063bd,686c3bf6) are each self-contained and have a real plugin (Symitech) exercising them.If splitting this PR into three is preferred, happy to do so; the branch was kept whole because the parts were authored in a tight loop and CI is already validating them together.