Follow-up to the #1717 port:
- The downsample tier holds the decoded RGBA, the resize's vertical-pass
intermediate (width x new height in RGBA f32) and the output at once;
that sum now has to fit the decode budget, or the image is refused up
front instead of overshooting it several times over. The native
budget drops from 4 GiB to 2 GiB.
- A native IMAGE pick decodes from the picked file instead of copying
its bytes into the session registry, so an edited file reloads and the
bytes are not held for the session. The web keeps registering them.
- Reword a plot-preview comment.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three reviewers (security, correctness, performance) converged on:
- Download PDF died on the second click: Closure::once_into_js throws
when invoked twice. The handler is now a plain FnMut kept for the
overlay's lifetime.
- Rasterisation was bounded by page count, not pixels: 12 A0 sheets at
150 DPI peaked near 1.8 GB of RGBA plus ~1.7 GB of decoded images,
and a drawing declaring absurd paper sizes made the rasteriser
saturate its u16 dimensions into a ~17 GB allocation that aborts the
tab. Preview now computes a uniform DPI under a 64-Mpx total budget
(floor 24 DPI) from the page tree before allocating, with a per-page
4096-px edge clamp.
- Overlays stacked when previewing twice and their blob URLs were only
revoked via Close. The overlay is now single-instance (id-tagged);
a replacement or Close revokes every URL through a shared registry.
- emit_image resampled before probing the XObject cache, so a title
block placed on 12 layouts ran 12 Lanczos passes to embed once.
The cache is now probed with the target dimensions first.
- Preview downloads are named {stem}_preview.pdf instead of a constant
plot_preview.pdf, and the page label says 'first N of M' when the
preview is capped while the PDF keeps every page.
(cherry picked from commit 2a05479d69)
A plot preview should show the plotted result on screen instead of
producing a file. The web build now does: the plotted PDF bytes
are rasterised with the hayro pipeline already used for PDF underlays
(150 DPI, first 12 pages) and shown in an in-page overlay with
Download-PDF and Close actions.
The previous strategy opened a new tab via window.open from the async
plot task; browsers withhold popups without a live user gesture, and
embedded hosts ship no PDF viewer plugin at all — so the fallback
download fired and preview behaved like an export. Plain PNG <img>
elements need neither permission.
(cherry picked from commit 8722d84910)
- pdf_export: drop the stale wasm32 gates (printpdf 0.9.1 compiles for
wasm; the memchr/lopdf incompatibility is gone with memchr 2.8.3).
Rendering is portable via render_pdf_bytes; only persistence differs:
atomic temp-file write on desktop, browser download on the web.
pick_pdf_path_owned on wasm returns a virtual path so the existing
message flow works unchanged.
- printpdf moves from the native-only dependency table to the shared one.
- print_to_printer: preview_page_pdf / preview_pages_pdf render the same
bytes and open them in a browser tab on the web (the browser's print
dialog takes over); desktop keeps the temp-PDF + OS-viewer flow.
- sys: open_bytes_in_new_tab (blob URL, popup-blocked falls back to a
download).
- New PlotQuality preset caps embedded raster resolution (High = native,
Normal = 300 DPI, Low = 150 DPI effective); vector output is untouched.
The image resource cache keys on the post-resample dims so one pixels
Arc placed at different effective DPIs embeds per-placement XObjects.
(cherry picked from commit 4812bc775d)
Ported onto main (#1717 review): the plot model lives in io::plot; Normal
keeps native pixels like High (only Low caps images at 150 DPI, so desktop
output is unchanged); the searchable TrueType text layer and its allsorts
subsetting now build on the web too; the plot stamp reads the time through
js_sys on wasm.
`with_printer_devmode` sized the driver's DEVMODE with a
`DocumentPropertiesW` call that passed `DM_OUT_BUFFER` and no output
buffer to write into. That combination is not a size query: drivers
answer it with -1, the `needed <= 0` guard returned early, and the
function handed back `None` for every printer on every machine.
Measured here on Windows 11 x86-64, both an installed virtual printer
and a physical one:
fMode = 0 -> 5428 / 2684 bytes (the real size)
fMode = DM_OUT_BUFFER, NULL out -> -1
Two things were broken by it, both silently:
- `oriented_printer_devmode` builds the DEVMODE a print job is spooled
with. Getting `None` back, the direct GDI path fell through to
"CreateDC then falls back to the driver default", so the plot's
orientation and sheet never reached the job. A landscape plot came
out however the driver happened to be set. This is the route users
without a PDF application take.
- `PRINTERS <name>` could never name the driver's default sheet,
because the id it matches on comes from that same DEVMODE.
With the size query asking for a size, both work: the report now says
"Default sheet: ISO A4 (210 x 297 mm)" for both printers here, and a
landscape plot leaves `dmOrientation = DMORIENT_LANDSCAPE` set in the
job's DEVMODE.
The two new tests both fail with the old flag and pass with it removed.
The existing GDI ink tests do not catch this: they accept the raster
rotation fallback as an equally good outcome, so the page looked right
while the DEVMODE was empty.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Properties… in the Plot dialog froze the application on Windows.
`on_printer_properties` called `DocumentPropertiesW` with `DM_IN_PROMPT`
from inside the closure handed to `iced::window::run`, and iced runs that
closure synchronously on the winit event-loop thread. The driver's
property sheet wants to run its own modal message loop on that same
thread, so the two loops collide: the sheet is never created at all, the
window stops painting, and the thread spins at 100% of one core with no
way back.
Measured on Windows 11 x86-64: the pinned thread is the window's own UI
thread, and compstui.dll plus the driver's PrintConfig.dll are loaded
into the process, so execution does reach the driver's property-sheet
code and never leaves it. It reproduces with a physical printer as
readily as with Microsoft Print to PDF, so the mechanism was broken for
every printer, not only the virtual one named in the report.
The system's own printing-preferences UI is now started as a separate
process, which pumps its own messages and leaves ours free, so the
freeze is impossible by construction rather than merely unlikely. The
command that does it already existed for the case where no printer is
selected; it now serves a named printer too, and the in-process modal
path is gone. Preferences still reach the job: the sheet writes the
per-user DEVMODE that the plot path reads back through
`DocumentPropertiesW`, which was confirmed by changing the orientation
in the sheet and watching the value the plot path reads change with it.
The new test pins down that the printer name must reach `printui` as a
single argument: split across several it matches nothing and the process
exits 0 without drawing anything, which would be a failure with nothing
to show the user.
Fixes#1474
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- tests/fixtures/plot/.page-setups-metric.dxf.ocs.lock was the app's own
single-instance lock file, committed by accident in #1343; remove it and
ignore *.ocs.lock.
- load_plotsettings_into_dialog: #1343's refresh_custom_scale_fields borrowed
self while #1360 still wrote through the plot_dialog borrow; write the
style error first.
- The wasm list_printers stub returns the Result #1358 gave the native one.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The plot dialog asks CUPS printers for their sheets and margins; on Windows
the query was a stub, so every printer showed the paper catalogue with
unverified margins. The driver is now asked the way the plot dialog expects:
- windows_media (compiled and tested everywhere): the three parallel
DeviceCapabilities tables (ids, sizes in tenths of a millimetre, 64-WCHAR
names) become one PrinterMedia per distinct size — a rotated or
borderless twin folds onto its sheet, catalogue sheets keep their name,
unknown sizes carry the driver's — with the device's margins on every
entry and the driver's default sheet found by id or by user size.
- print_to_printer (Windows): DeviceCapabilitiesW for the tables, the
default DEVMODE for the default sheet, one portrait printer DC for the
unprintable margins (physical offsets and extents, per device — a
documented approximation). The DEVMODE fetch is shared with the print
path (with_printer_devmode).
- A direct print now asks the driver for the plot's own sheet (by id, or
as a user-defined size), so an A3 plot prints on A3 rather than shrunk
into the driver's default; a driver that refuses the sheet is asked for
the orientation alone.
- The capability cache remembers printers that could not be asked, so a
slow failing query runs once per session, not on every dialog open.
- PRINTERS <name> reports a printer's sheets, margins and default sheet.
Tests: names (NUL trim, full slot, lossy), media folding and dropping,
default by id / user size / none, DC margins, paper id lookup.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
On Windows a plot with no printer chosen went to the PDF application's
bare "print" verb — whatever that application considered its default —
and a viewer without a "printto" verb (Edge, store readers) silently
dropped the printer the dialog had chosen. The dialog also never named the
system default, and a failed printer enumeration left only "Default" and
"PDF" with the reason written to a console nobody sees.
- The default printer is looked up by name (GetDefaultPrinterW) both for
the dialog's "System default printer (…)" entry and before every job.
- A pure route decision (plan_print_route) resolves "Default" to that
name and uses the PDF application only when it registered a "printto"
verb for .pdf (AssocQueryStringW); otherwise the job goes straight to
the spooler through the existing GDI path. The report says which:
"Sent to printer: NAME (direct print | via the PDF application)".
- list_printers reports the system's own error; the plot dialog shows it
under the printer row and the command line warns once. A desktop without
CUPS is an empty list, not an error.
- PRINTERS command: default printer, listed printers (or the error) and,
on Windows, the print route — a report to paste into an issue.
- Properties… on the Default entry opens that printer's preferences sheet.
- GDI path: the driver's dmSize is kept (its private data lives behind
it), the print raster is no longer memoised for the session, and a
spooled temporary PDF is removed.
Tests: route planning (named / blank / no default / printto or not), the
PRINTERS command; the two Windows-only GDI tests pass the resolved name.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Properties… next to the printer used to open the CUPS web interface in a
browser. It now edits the driver options of the *selected* printer the
way LibreOffice's Properties… does, without leaving the plot dialog:
- CUPS platforms (Linux, any desktop; macOS): an in-line editor lists the
printer's PPD options as `lpoptions -l` reports them — media type,
colour mode, quality, duplex, trays, scaling — with readable choice
labels, Reset / Cancel / Apply. Apply keeps only the choices that differ
from the driver defaults, per printer, in the plot config, and every job
for that printer passes them as `lp -o key=value`. Where CUPS cannot
answer, the desktop's printer settings panel opens instead of a browser.
- Windows: the driver's document-properties sheet (`DocumentPropertiesW`
with prompt, owned by the main window) and, on OK, the result stored as
the user's printing preferences for that printer (`SetPrinter` level 9),
which the job honours. `printui /e` (printing preferences) replaces the
device-properties sheet as the fallback.
The desktop print portal was tried first and dropped: GTK 4's print
dialog loses the preselected printer when the default queue is listed
before it, so the dialog always opened on the wrong printer.
Dialog layout: the modal grows to 940×620; Printer / plotter and Paper
size share a line with their pickers (long queue and media names now fit);
Copies + Properties… and Custom… sit beneath; the custom-sheet editor
labels each margin (Left / Right, Top / Bottom) and shows the unit.
Also recognises `none_device`, AutoCAD's stored spelling for "no plotter",
and writes it back unchanged.
Tests: `lpoptions` parsing and keyword humanising, the editor flow (load,
stale answer ignored, set, apply, remember, reset, error, cancel), device
name round-trips including `none_device`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- IMAGEEMBED: pack raster images into OLE2FRAME entities so a .dwg is
self-contained; 'Embed Image' entry in the XREF attach menu; control/MCP
op embed_image for external tools; dxf_name for OLE2FRAME queries.
- Scene: populate_images_from_document now rebuilds OLE2FRAME and PDF
underlay models too (they used to vanish after any XREF op).
- Printing: add PRINTER_ENUM_NETWORK pass when listing printers; GDI
spooler fallback (hayro raster at 300 DPI + StretchDIBits) for hosts
whose PDF handler registers no print verb; DEVMODE orientation with a
90-degree raster rotation fallback for v4 drivers; fix the fit-scale
precedence bug that printed blank pages.
- PDF export dialogs on Windows use the async file dialog so the native
overwrite confirmation no longer deadlocks the UI thread.
- Plot dialog: persist the chosen printer (saved on selection) and the
picked plot window across sessions; page setups with no device no
longer reset the output device.
- Community fonts: detect unresolvable .shx references on open and offer
a download from the repository's fonts/ folder (contribution guide in
fonts/README.md); custom font source URL setting for intranet font
servers; downloaded fonts resolve from the per-user fonts directory.
Include decoded IMAGE/OLE content in the shared PDF plotting path,
with clipping, transforms, transparency, draw order, and resource reuse.
Use named page content for export and printing.
Fixes#1266
Populate the plot-dialog printer list from the Windows spooler cache.
Validate record counts and UTF-16 name bounds before reading the buffer.
Refs #1261
Style selectors (Standard, layer, Color/Linetype/Lineweight) sat at
different heights across ribbon panels because each combo panel
vertically centered its content: panels with fewer icon rows
(Multiline Text, Table) centered their dropdown differently than
panels with two rows (Dimensions, Multileader).
- Pin all combo panels (Layers, Properties, style combos) to the top
of the fixed 3-row panel with a shared COMBO_PANEL_PAD, so every
primary dropdown shares one baseline regardless of icon-row count
- Extract the duplicated tool-row and panel-container code into
tool_row() / combo_panel_col() helpers + named spacing constants
- Bundle the per-view render state into RenderCtx, shrinking
render_large/render_large_dropdown from 12-14 positional args to
(item, &ctx)
- print_to_printer: silence the Windows-only dead-code warning on
PrintOptions::quality with the platform split documented (the field
is read by the CUPS `lp -o print-quality` path)
- unify Model and Paper Space plot area behavior
- add CTB discovery, read/write, and bundled defaults
- apply scale, render mode, and output options consistently
Use the same area pipeline as model space while keeping Layout bound to the physical sheet and printable margins. Share scale, rotation, clipping, and viewport fills across preview, PDF, and printer output.
entity_wires() cloned the entire wire set — every point, fill, and snap
buffer of every entity — on each call, although the set is already
memoized behind an Arc and none of the 18 call sites mutates it. On
large mesh imports (#358: 42M faces, ~210 MB of wire content per 1M
triangles) that is a multi-GB allocation + memcpy on every pick, snap,
selection, or plot pass.
Return the Arc instead. Call sites iterate as before; the two
window-plot paths that build a culled owned subset now clone only the
wires inside the plot window, and the print pipeline moves the Arc into
its async task.
Measured (tests/mem_probe_meshent.rs, 1M-triangle MESH, release):
resident after entity_wires() drops 758 -> 547 MB (866 -> 646 B/face).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A UTM drawing plotted deformed — both geometry and text — while the same
drawing near the origin came out clean. Every pass of the exporter narrowed
an absolute world coordinate to f32 before the offset had a chance to cancel
it, and f32 resolution at UTM is ~0.03 m on easting and ~0.5 m on northing,
so the whole plot snapped onto that grid.
Four places did it, all now folding in f64 and narrowing only once the
result is a single-digit sheet-mm value:
- The wire pass never read `points_low` at all — the word did not appear in
the file — so it plotted the double-single "high" half alone and dropped
every residual.
- emit_hatch cast the f64 `world_origin` to f32, re-introducing at the last
step exactly the quantisation the boundary-relative encoding exists to
avoid.
- pattern_segments rasterised entirely in absolute f32. The pattern is
anchored at world (0,0), so a hatch at UTM runs a line index k of ~1e7:
`origin + k · step` and the `edge − line` intersections both fall apart
there. k also becomes i64 — `as i32` saturates silently, which would
collapse the range to a bogus run from i32::MIN rather than drop the fill.
- emit_text folded `pos + pos_low` in f32, which rounds straight back to
`pos`: the residual the pair exists to carry was discarded at precisely
the coordinates it was carried for.
The offsets themselves were also narrowed by the callers, so the value meant
to cancel a UTM coordinate was quantised before it could. Widen ox/oy to f64
through export_pdf, build_pdf, print_wires and the two plot-param builders;
printing shared the same path and the same bug.
Pre-existing, not from #390 — that change only routed text through the same
faulty pipe the wires were already in.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
WebGL2 has no vertex/fragment storage buffers, so the batched storage-buffer
hatch renderer (hatch_gpu) is disabled on wasm and real hatch fills never
rendered on the web build — the pick highlighted the boundary and the command
then appeared to cancel (issue #204).
Add a wasm-only per-hatch renderer (hatch_web_gpu + hatch_web.wgsl) that packs
the variable-length boundary / family / dash arrays into one RGBA32F data
texture read with textureLoad, instead of the fixed-size uniforms of the
wipeout path. This removes the MAX_FAMILIES (16) / MAX_HATCH_BOUNDARY_VERTS
(1024) / MAX_DASHES (128) caps, so every hatch — solid, gradient, and
arbitrarily complex line patterns — renders on the web. The fill algorithm
(in_polygon, per-family line/dash/dot, gradient, LOD, double-single vertex
transform) is identical to the desktop path; only the array ACCESS changes.
Pattern phase is anchored at world_origin with the boundary stored raw, and
scale / gradient-range clamped, to match the desktop batched renderer exactly.
Native (hatch_gpu + hatch.wgsl) and the wipeout mask renderer are untouched;
the new module and pipeline are gated on target_arch = "wasm32".
Also silence the resulting (and pre-existing) wasm-only dead-code warnings for
native-only paths so both the native and web builds compile clean.
Closes#204.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the old Page Setup panel with a single OCS-styled plot/print
dialog (dropdowns for printer, paper, orientation, rotation, plot area,
scale, quality, shaded plot; checkboxes for plot options). PLOT / PRINT
/ PAGESETUP all open it.
- Printer output via lp: -d printer, -n copies, -o ColorModel=Gray,
print-quality, Resolution; Windows printto verb; list_printers via
lpstat. Preview opens a temp PDF in the OS viewer (never a save
dialog). PDF export reuses the tested pipeline.
- Window pick returns to the new dialog (was the old Page Setup) and the
clipped window plot drives PDF / printer / preview.
- Remember the user's print preferences across sessions in
config/plot.txt (printer, copies, quality, output options, area,
scale); drawing-specific fields (offset/center/rotation) seed from the
layout. Default plot area Window, scale Fit.
- Delete src/ui/window/page_setup.rs and its ModalKind / Message /
buffer state; fold on_page_setup_commit into apply_plot_page_settings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pick a rectangular window in model space with the existing PLOTWINDOW command (now enabled in model space, previously paper-space only) and export it to a PDF at a chosen ISO paper size (A4-A0), orientation and scale (fit or ratio). The window is a hard clip boundary. Adds paper-size presets, a window-to-sheet transform, scale + clip support in the PDF exporter, and Page Setup controls (format/orientation/pick window).
The web build had bit-rotted: native-only features added since it last
compiled pulled crates that don't build for wasm32-unknown-unknown.
- printpdf (PDF export / printing) → native-only target; the web build gets
stub export_pdf / print_wires. It pulls a wasm-incompatible memchr 1.0.2
via lopdf → nom_locate, and the web has no filesystem.
- getrandom 0.3 (via ahash via acadrust) → enable its wasm_js feature and add
the matching --cfg getrandom_backend rustflag in .cargo/config.toml.
- ocs_plugin_api "host" feature (out-of-process plugin runtime: interprocess,
libloading, memmap2, rkyv) → native-only target; the web gets the
dependency-free manifest/ribbon contract. The host code was already
wasm-gated; only the Cargo wiring pulled it in unconditionally.
`trunk build --release` succeeds again.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the 30 flat modules under src/scene/ into six folders by role:
convert (entity→geometry), text (fonts/shaping), model (render models),
pick (hit-test/selection/grips/xclip), view (camera/transform/render),
cache (block + property caches). pipeline/ is unchanged.
Full path migration — all `scene::X` references become
`scene::<group>::X`, no compatibility re-exports. render.rs items that
were `pub(super)` (parent was scene) become `pub(in crate::scene)` to
keep their original scene-wide visibility.
No behavior change; build and tests are green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Joining the OpenAEC Foundation (open-aec.com). Crate/struct become
OpenCADStudio, user-facing strings use the spaced "Open CAD Studio"
form, repo URLs point to HakanSeven12/open-cad-studio. Logo (window
icon + assets/logo.svg) redrawn with OCS initials. Drop examples/
binaries — unused.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The PDF / print pipeline only walked `WireModel.points` (lines), so every
hatched region disappeared the moment a sheet left H7CAD. Route the same
`paper_canvas_hatches()` / `paper_canvas_wipeouts()` lists that the
on-screen canvas consumes into `pdf_export::export_pdf` and
`print_to_printer::print_wires`, and emit each one as an
`Op::DrawPolygon` with the EvenOdd winding rule so multi-ring boundaries
(islands / holes) render correctly:
- Solid → fill the boundary polygon in the hatch colour.
- Pattern → stroke the outline (PAT line families stay procedural;
follow-up work will rasterise them into segments).
- Gradient → fill with the averaged colour, mirroring paper_canvas.
Wipeouts emit first, then hatches, then wires — same z-order as the
on-screen canvas, so wires draw on top of the fills they belong to.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The PRINT command renders the current layout to a temporary PDF (reusing
the existing PDF export pipeline) and dispatches it to the OS default
printer:
- Linux/macOS: `lp` (CUPS) with `lpr` as fallback.
- Windows: ShellExecute "print" verb (compiled-in, unused on Linux).
PLOT and EXPORT still open the save-file dialog as before.
Status messages are shown in the command line during the async job.
Closes ROADMAP 1.8.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>