Commit Graph
4658 Commits
Author SHA1 Message Date
gianlucafiore 9e8d07270e fix: avoid translated prototype reuse for wipeout-containing block inserts 2026-10-12 17:30:19 -03:00
gianlucafiore 8a65f2e8be fix: preserve block-local draw order for ordinary line members alongside wipeouts 2026-10-12 17:16:52 -03:00
Hakan SevenandClaude Opus 5.5 ae56fab62a Cargo: codec 3ec70a2, kernel e710727, graph 338a973
The codec keeps dynamic-block, associative and material records intact on
DWG save (strict readers rejected re-saved drawings at BLOCKSCALEACTION,
associative action bodies and MATERIAL records). A checker material
texture no longer rewrites the native map source, so a map with a
procedural texture counts as content whatever its source says.

Valuable parts of #1671 (with opencadcodec#100): measured on 235 sample
drawings re-saved after an edit, a strict reader now rejects 4 instead
of 7; the remaining 4 are R14/R2000 tables, which the PR codec also
fails.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 21:22:13 +03:00
Karim Jerbi 8943a6fd39 feat(mcp): serve specialist guides on demand and shorten instructions 2026-10-12 15:20:28 +01:00
Karim Jerbi 50030be8c2 feat(mcp): bind batch parameters to earlier step results 2026-10-12 15:12:59 +01:00
Karim Jerbi 6ff8d8d705 fix(mcp): return resource links for reference-only captures 2026-10-12 15:08:15 +01:00
Hakan SevenandClaude Opus 5.5 b94bb18a44 fix: catalog every ported web string in all locales; drop tests of unported STB parsing
- Translate the strings the #1717 port cataloged in English only
  (CSV picker, plot-preview task, extraction cap, shared-copy warning)
  for every locale, and the whole ported web set for lo-LA, which the
  contributor branch predates.
- hu-HU carried twelve keys twice after the port's merge; keep main's.
- The STB container/refusal tests exercised parsing this port did not
  bring over; the preview-clamp test now only pins the upper bound, since
  main's raster path already clamps preview pages further.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 12:02:06 +03:00
Hakan SevenandClaude Opus 5.5 ce6bd2c73e fix: bound the image decode transient and keep native picks on disk
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 <[email protected]>
2026-10-12 12:02:06 +03:00
Afonsop fb674fc45e fix: recovered_name must not smuggle path separators through the extension
recovered_name reduced only the head to its final path component, so a
stored name like "a.b/c" came back as "a (recovered).b/c". The whole
input is now sanitized first, and the empty-stem fallback uses the
sanitized component, which also restores idempotence for
separator-headed names ("/.dwg" used to alternate across crash cycles).

Ported from 9262d63f (#1717); its integer-precision fix landed with the
d719fb77 port, and the standalone tests/fuzz_round.rs suite (copies of
the functions rather than the code) stays out.

(cherry picked from commit 9262d63f)
2026-10-12 12:02:05 +03:00
Afonsop 1c0963b68d fix: close the adversarial findings on the save/paste wave
Ported from da950460 (#1717).

In-place save:
- A stale retained handle could receive bytes serialized for a
  DIFFERENT format (Save As .dwg->.dxf whose re-pick failed wrote DXF
  bytes through the .dwg handle) or silently save in place while the
  user was told a copy was downloading. The serialized format now
  travels to completion (tab-keyed pending map), the write requires a
  matching handle extension AND a live tab, and a failed re-pick
  drops the old handle before the download fallback.
- Discarding an unsaved tab leaked its handle, and a serialization
  still in flight could then overwrite the real file the user just
  declined to save. Discard drops the handle; a tab-gone completion
  downloads instead of writing.
- Overlapping saves had no guard: a wasm in-flight slot refuses a
  second save with the same localized message, cleared on every exit
  path.

Paste: pasted_image_name built its timestamp from Date::new(0) — a
CONSTANT name, so every paste overwrote every earlier paste's image.
Real clock, milliseconds, and a collision-suffixed unique name now.

Capture: a web capture in a hidden tab, or one with nothing redrawing,
could hold the automation slot forever. It fails fast when the page is
hidden and gives up after 5 s, on main's frame-parked capture.

The XREF manager reports registry-loaded xrefs with their size on the
web instead of NotFound.

Left out: the always-warm print snapshot (a frames subscription that
emits a message on every frame keeps the app redrawing at the display
rate), so the browser-menu print wiring from the previous commit goes
too — its beforeprint readback printed blank pages on WebGL2. The
merge-residue hunks (re-added XREF bind tests, snap endpoint map and
3D-snap gate) belong to code main already has in its own form.

(cherry picked from commit da950460)
2026-10-12 12:02:05 +03:00
Afonsop 835e8d469f feat: in-place save (File System Access), image paste, print wiring, web capture
The last high-severity web gap plus a small integration bundle.

In-place save: with the File System Access API (Chromium/Edge), Save
now works like desktop. Plain Save on a tab with a retained handle
writes straight through — no picker, no download, no "(1)" copies.
First save or format change opens showSaveFilePicker AT the confirm
gesture (the worker serialization that follows can outlast the
transient-activation window; retained handles need no activation to
write). Picker cancel cancels the save like desktop; picker or write
failure falls back to the legacy download with a note; Firefox/Safari
keep the download flow untouched (feature-detected). Handles are
keyed by tab id in a thread-local registry (Message must be Send —
JsValue cannot ride in messages or App state); closing a tab drops
its handle; Save As re-picks. Dirty clears only on successful
persistence, same race guard as before.

Image paste: Ctrl+V of a screenshot inserts it as an image reference
— the clipboard reader falls back to image/* items when neither the
entity clipboard nor text has anything (precedence preserved), the
bytes enter the image source registry as pasted-<timestamp>.<ext>,
and attach reuses the IMAGE command's model-creation path verbatim.

Browser print: beforeprint snapshots the WebGL canvas into a print-
only <img> (the capture_canvas technique), so the browser's own print
menu produces the drawing instead of a blank page.

Automation capture on web: the capture op no longer calls the
synchronous GPU-map screenshot (dead on wasm); main's frame-parked
capture waits for a presented frame and reads via capture_canvas — the
same workaround the save-thumbnail path uses — delivering the PNG as a
download.

(cherry picked from commit 6e89b36d)

Ported onto main (#1717 review): the web capture rides main's existing
frame-parked capture (one ControlCaptureFrame path, ignoring the timer
tick on wasm) instead of a second parked-capture state; closing a tab
drops its file handle inside main's on_tab_close.
2026-10-12 12:02:05 +03:00
Afonsop 0459f20129 fix: close the review findings on the restore-all/deep-links/PWA wave
Two reviewers, no blockers, two majors — all fixed.

- The remembered-tab focus after a Restore-all batch fired at the
  message boundary, but the LAST restore's tab inserts one async round
  later (its OPFS recent-cache write precedes the insertion) and the
  insertion's own activation overwrote the selection. The focus flag
  now also consumes at the insertion point, so the batch focus holds
  in the common all-success case.
- The service worker cached ANY successful in-scope navigation under
  the shell key — a direct navigation to a font or JSON would be
  served as the app shell for every offline navigation until the next
  online visit. Only the scope root caches as the shell now.

Also: '/app/' gets its own web app manifest (the previous commit's
'installable' claim was true only of the marketing site, whose
start_url sits OUTSIDE the service worker's scope — an installed app
opened offline showed an error page). Its icon is the
existing assets/logo.svg, copied next to the page.

(cherry picked from commit 369e2c8b)

Ported onto main (#1717 review): the build-web.sh and README hunks stay
out with the wrapper.
2026-10-12 12:02:05 +03:00
Afonsop 6af8d8ba8c feat: restore-all session recovery, ?file= deep links, offline PWA
Session recovery: more than one pending snapshot adds a 'Restore all'
button to the Recover Drawing modal — every snapshot goes through the
exact same guarded path as a single restore (deleted only after its
own open succeeds). The open-tab list and active tab persist to
localStorage on web (open/save/close) and the remembered tab is
re-selected once a restore batch settles; the modal notes how many
drawings the session had. Nothing is auto-opened.

Deep links: ?file=<url|name> opens a drawing at boot. http(s) URLs
must end in .dwg/.dxf and are fetched with the same streaming,
64 MB-capped, 8 s-deadline reader the remote images use, then go
through the guarded open queue; cross-origin opens announce the
origin first. Bare names resolve from the browser's recent storage,
with a hint when absent. Everything else (file:, javascript:, drive
letters, non-drawing tails) is refused with a console note.

Offline PWA: the app was installable (manifest) but downloaded its
55 MB bundle on every visit. A cache-first service worker now serves
the whole bundle from Cache Storage after one online visit —
navigations network-first so updates land, hashed assets
stale-while-revalidate, non-GET/cross-origin passthrough, old caches
cleaned on activate, and the waiting-worker update flow documented.
The Pages workflow stamps the cache version from the bundle hash and
fails if sw.js is missing or unstamped.

(cherry picked from commit 5f9b345e)

Ported onto main (#1717 review): the cache-version stamp moved from the
skipped build-web.sh wrapper into the Pages workflow; the tab-close hook
lands in main's on_tab_close, which also cancels pinned control requests.
2026-10-12 12:02:05 +03:00
Afonsop 2d2444fcfa fix: close the review findings on the issue wave
Ported from d719fb77 (#1717); the hatch draw-order, PDF and
recovery-settle hunks stay out (main owns those paths).

- La16 was still billed at 4 B/px: the no-conversion special case
  keyed on native_bpp == 4, which La16 also is — but only Rgba8 is a
  real move in into_rgba8, so La16's decode peak is 8. The case now
  keys on the color type, and a table test pins every layout's peak
  (plus GIF's screen+frame 8) so the accounting can't silently drift.
- json_values_match: an integer beyond f64 precision no longer matches
  a float it merely rounds onto; a mixed pair matches only when the
  float is whole and converts back to exactly that integer (also the
  fuzz finding of 9262d63f).
- ocs-control.js resolves `ready` with the API object instead of
  window.ocsControl, which is still undefined when the bridge already
  exists as the script runs (built before the first settle, avoiding
  the temporal-dead-zone throw of a later const); DATALINK on web says
  the typed path is superseded by the picker instead of silently
  discarding it.

(cherry picked from commit d719fb77)
2026-10-12 12:02:05 +03:00
Afonsop db8db14006 feat: web automation wrapper (#1349), DATALINK table on web, System theme; honest decode accounting
Ported from 2c38de53 (#1717). Main already carries the hatch PDF draw
order fix (#1345), the numeric set_properties compare (#1348) and the
web open from bytes, so those parts stay out; what remains:

#1349 — window.ocsControl.ready/request promise wrapper with an
ocs-ready event (web/ocs-control.js, loaded by the web page), and the
startup donation prompt is suppressed once a control request is seen
(opt-in pre-boot via localStorage["opencadstudio.automation"] = "1").
docs/automation/web.md documents both. The control protocol stays at 1.

DATALINK CSV-to-table works on web (picker + bytes through the session
registry — was std::fs, silently dead); COLORSCHEME accepts System
(prefers-color-scheme on web, AppsUseLightTheme on Windows).

Decode accounting, made honest per the fix-of-fix review: into_rgba8
allocates the RGBA destination while the native buffer is alive for
every non-Rgba8 layout (L8 5, La8 6, Rgb8 7, L16 6, La16 8 B/px
peaks — the formula now says so), GIF bills the screen+frame
coexistence at 8 B/px, and TIFF drops the 3x multipliers both ways —
tiff 0.11 splits the given budget between frame and scratch (a total
ceiling), so the old double-billing refused honest 16-bit GeoTIFFs
~2.25x early while admitting hostile chunk transients at 3x.

(cherry picked from commit 2c38de53)
2026-10-12 12:02:05 +03:00
Afonsop f015c3b5fa fix: native-bpp decode accounting, GIF frame bombs, streaming remote bodies
A second adversarial round (with empirical reproduction against the
vendored image 0.25) found the budget contract still had holes:

- The probe billed 4 bytes/px, but 16-bit formats allocate their
  native layout during decode (8/px for RGBA16 PNG; TIFF stacks ~3x
  codec scratch on top). In-budget 16-bit images were falsely refused
  with self-contradictory notes, and the decode transient could exceed
  the budget the tiers claimed to enforce. The probe now reads the
  color type alongside the dimensions and bills the real peak (native
  layout + RGBA where they coexist, plus the TIFF codec factor), so
  refusals are honest and per-platform-correct, and the TIFF allowance
  follows the same accounting.
- GIF: the probe reads the LOGICAL SCREEN, but a crafted frame may
  declare itself arbitrarily larger (empirically: a 10x10 probed
  screen with an 8192x8192 frame — 320 MB and 29 s from a
  probe-claimed-400-byte image). The GIF frame buffer is now capped
  to the probed size plus slack; frame bombs refuse like any other
  oversize.
- Remote bodies on web streamed incrementally with the 64 MB cap
  enforced DURING the read (array_buffer() materialized a hostile
  chunked reply in full before any check could fire), aborting and
  cancelling the reader the moment the cap is crossed.
- Degenerate raster entities (zero/non-finite size or vectors) no
  longer produce NaN placement math that aborted whole PDF exports —
  they resolve to the broken-reference placeholder.
- Same-name resolution now has a deterministic winner (newest
  registration, via insertion order) instead of HashMap iteration
  order; URL query strings no longer break by-name matching; the
  image pick matches its refusal note by the quoted file name
  (substring collisions like a.tif/2011.tif attributed errors to the
  wrong file); a dropped remote outcome at the queue cap triggers one
  unconditional catch-up re-resolution instead of waiting for another
  event; the unknown-size refusal says so instead of printing 0x0;
  and the build script fails cleanly when there are no fonts to
  verify.

(cherry picked from commit c6db94e8)

Ported onto main (#1717 review): the build-web.sh hunk stays out with
the wrapper; the new messages are cataloged (image refusal under the
file group it is looked up in, plus the extraction cap, shared-copy
warning and plot-preview task title that were missing), and the web
SAVEALL failure reuses the existing SAVEALL message.
2026-10-12 12:02:05 +03:00
Afonsop a10d9c6f89 fix: trust the probe for over-budget images; TIFF allowance
Review of the image wave (with empirical reproduction against the
vendored image 0.25) found the budget contract did not hold across
formats: the decoder's Limits reserve checks NATIVE bytes-per-pixel,
so a grayscale/RGB JPEG/PNG/BMP over the RGBA budget decoded anyway
and into_rgba8() then ballooned to up to 4x it — the tab death the
budgets exist to prevent. The probed dimensions already prove an
OverBudget image cannot fit, so it is now refused without decoding,
removing all dependence on per-decoder limit fidelity.

Conversely, image's TIFF codec stacks ~3x scratch buffers on top of
the reserve, so an in-budget TIFF (the flagship GIS scenario) was
refused with a self-contradictory note ('400 MB, over the 512 MB
limit'); the decoder allowance for TIFF is now 3x while tier math
stays on the real budget.

Also: the refusal message saturates instead of overflowing on crafted
headers, the image pick only matches the refusal note naming ITS file
(peek, not drain — other references' notes survive for their own
drains), and the Georgian Supplement range ends at U+2D2F (U+2D30.. is
Tifinagh, which ships no font).

(cherry picked from commit 58d3789bcf)

Ported onto main (#1717 review): the build-web.sh wrapper and its
pages/README/Trunk wiring stay out — it works around a local Windows
trunk dist lock; the Linux Pages build is unaffected.
2026-10-12 12:02:05 +03:00
Afonsop 93e97bdf18 feat: bounded huge-image decoding, webp/gif, remote rasters, Georgian/Armenian fonts
Giant rasters (500 MB aerial/GIS TIFFs) decoded whole: multiple GB of
RGBA on a wasm heap is tab death, and even desktop silently failed
past the image crate's 512 MiB default allocation cap. Decoding now
probes the header for dimensions first and runs in tiers — full decode
under a per-target budget (512 MB wasm / 4 GB native, enforced through
the decoder's allocation limits so oversize fails fast instead of
ballooning), Lanczos3 downsample to a display cap (4096² wasm /
8192² native) when the decode fits but the buffer is oversized, and a
clear localized refusal naming the dimensions and the limit when it
cannot. The probed source dimensions stay the entity's sizing truth,
so a downsampled buffer never shrinks the on-paper footprint; PDF
export self-corrects (effective DPI derives from placement × stored
pixels). Refusals surface once per image through the pick, open, and
drop/re-resolve paths.

Decode coverage gains webp and gif (pure Rust in image 0.25 — no C,
no lockfile churn), with the attach picker and drop filters matching.

Remote http(s) raster references now fetch on web: CORS-classified
failures name CORS, HTTP errors name the status, bodies over 64 MB and
8 s deadlines are bounded, and fetched bytes join the session source
registry (so ARCHIVE can package them and re-resolution reads cache).

Georgian and Armenian Noto subsets ship (generation blocks included),
so those scripts render instead of skipping; scripts still without
subsets remain silently skipped, unchanged.

(cherry picked from commit ca0d7b96)

Ported onto main (#1717 review): Armenian and Georgian take the script
indices after Lao, which main added meanwhile.
2026-10-12 12:02:05 +03:00
Afonsop 8a5d343927 fix: final pre-PR review sweep
The final holistic + quality audit (three reviewers) returned GO with
four majors; all fixed.

- Start-page recents were the last unguarded writer of the open slot
  on web: clicking one under the open overlay clobbered the in-flight
  open and silently lost its bytes. Refused with the same in-progress
  notice the picker uses.
- The web quit-after-save continuation stalled: a Quit pending now
  survives the tab-save AND continues — offering to save the next
  dirty tab or finishing the quit, mirroring native.
- Data-extraction's multi-file pick is capped (16): every picked file
  becomes a full parsed document held for the session, and one giant
  pick could exhaust the wasm heap.
- The xref registry tests raced each other on the process-global
  registry (reproduced 3/10 at high thread counts — CI roulette); they
  now hold a module-level guard.

Minors: the collision warning is cataloged in all 21 locales; the
docs' parallelism detail header no longer contradicts its own table;
the drop-filter and name-hash helpers gained inline tests (their only
prior coverage was a verbatim copy in the red-team suites).

(cherry picked from commit f70d3fda)

Ported onto main (#1717 review): the collision message is filed under
the file group it is looked up in (it sat under view), the data-link
warning reuses that cataloged text, and the command-reference string
that rode along in the locale hunks stays out.
2026-10-12 12:02:05 +03:00
Afonsop 108ef045b8 web: worker-route WBLOCK/SAVEALL/ARCHIVE; close the follow-up review
WBLOCK, SAVEALL, and web ARCHIVE now serialize through the Web Worker
like SAVE/AUTOSAVE: prepare on the main thread, writer pass on the
worker, inline fallback on any worker failure. SAVEALL clears each
tab's dirty flag under the same unchanged-tab race guard the single
save uses; ARCHIVE assembles the ZIP in the completion handler so the
transmittal matches the snapshot taken at command time.

The review of the previous fix commit found the collision refusal
still REPLACED the registry bytes — the exact silent retargeting it
was meant to prevent. The refusal now keeps the old registration, the
extraction settings picker reports refused picks in the wizard's error
slot (previously silent, and the stale collision flag could surface as
a wrong-file warning on an unrelated browse), and the xref registry's
same-name ambiguity is documented as a browser fundamental (native
distinguishes by folder): newest registration wins consistently for
resolution, ARCHIVE, and XRELOAD — matching what the drawing displays.
Also: the Quit pending-close survives a successful web tab-save
(continue the quit instead of dropping it), the last ZIP64 sentinel
(central size) is rejected like the other four, and the new behaviors
gained regression tests (collision keeps old bytes, registry eviction,
including the deadlock fix for the guard-scoping that test exposed).

(cherry picked from commit b635d56c)

Ported onto main (#1717 review): ARCHIVE and SAVEALL serialize
document_for_save() like a single web Save; the ZIP is built with the
zip crate; WBLOCK keeps its native background-task title.
2026-10-12 12:02:04 +03:00
Afonsop 8968e79942 fix: close the review findings on the last web wave
Full verification of the XREF/data/ARCHIVE/worker wave (three
reviewers, one forensic) found no blockers and no crossfire damage;
five majors and several minors are fixed here.

- ARCHIVE now packages XREFs: the bytes registry added by the same
  commit is consulted (the in-code 'no xref store exists' comment was
  false within the commit), and the drawing is packaged only after
  the full save preparation — without it kernel solids shipped
  without their ACIS data.
- PendingClose stores the stable tab id instead of a vector index:
  the web save now completes asynchronously, and closing a lower tab
  during the serialization window shifted indices — closing the WRONG
  tab, or panicking on a stale one. All producers and consumers
  resolve the id at use.
- The xref bytes registry is capped (6 entries, oldest evicted): every
  open, drop, and pick registered full drawing bytes with no bound —
  an OOM waiting to happen. Reopened recents register too, so any
  entry path can serve as another drawing's xref.
- A re-picked data-link source with the same name but different bytes
  no longer silently replaces the first (the browser hands back bare
  names, so two same-named files collided and every reference read
  the newer pick): the pick is refused and the user is told why.
- The unresolved-xref warning lists file names (what to drop on the
  page), not internal block names; the ZIP writer refused the ZIP64
  sentinel values (0xFFFF counts, 0xFFFFFFFF sizes/offsets) instead
  of emitting archives that send strict readers hunting for a ZIP64
  EOCD that is absent; docs note the worker-side saves.

Known follow-ups documented, not fixed here: the serde-skipped
ExtendedEntity/CadDocument raw bytes still drop on worker crossings
(needs codec accessors — upstream), and WBLOCK/SAVEALL/ARCHIVE
still serialize inline (the worker route is a small follow-up).

(cherry picked from commit 6f429f24)

Ported onto main (#1717 review): main already keys PendingClose by tab
id, so those hunks stay as on main; the ZIP64 guard is moot with the zip
crate; the unresolved-xref names split on both path separators, since
Path does not split Windows paths in the browser build.
2026-10-12 12:02:04 +03:00
Afonsop 61c32c7f74 web: XREFs, data links & extraction, ARCHIVE transmittals, worker-side saves
The last four big web-vs-desktop gaps.

XREFs: XATTACH reads the picked drawing's bytes and registers them
name-keyed (the image/PDF-underlay pattern), pre-parses on the web
worker, and the commit merge produces the same xref block flow as
native. Opened drawings resolve xrefs from the registry (trailing-name
match) before cache build; unresolved references produce ONE warning
listing the names and the fix — drop the referenced files on the page
(drops and picker opens now register their bytes) and reopen or
XRELOAD. XRELOAD works off the registry on unsaved drawings too.

Data Link & Extraction: spreadsheet links read picked xlsx/csv bytes
(calamine from slices / CSV from string) through a session registry,
so preview, refresh, and table-insert work on web; extraction takes
external drawings via a multi-file picker parsed on the worker, and
outputs CSV/TXT/.dxex via download_bytes. The disabled 'Add folder'
button explains why.

ARCHIVE/ETRANSMIT: assembles the drawing plus registered images, PDF
underlays, and session plot tables into a ZIP delivered as
{stem}_transmit.zip; skipped dependencies are reported by name.

Save/autosave off the UI thread: the parse worker gains a serialize
action (protocol v5) — the document is prepared once on the main
thread (clone, styles, dimension bake), then the DWG/DXF writer pass,
the dominant cost, runs on the worker; any worker failure falls back
to inline serialization with the identical shared implementation.
The save path also clears the dirty flag only when the tab didn't
change mid-write, and the runtime-fields sidecar now carries
graphic_data, fixing a pre-existing loss of raw DWG round-trip bytes
on web-opened documents.

(cherry picked from commit 2e100634)

Ported onto main (#1717 review): the new ocs_save crate builds on the
pinned codec; the hand-written ZIP writer is replaced by the zip crate
already in the tree; ARCHIVE packs registered xref bytes too and
serializes document_for_save() like a web Save; the web save prepares
document_for_save() (prepare_document_for_save now takes the copy by
value, so no second clone); the wasm resolve_path consults the xref
registry, so every reference lookup — not just resolve — finds dropped
files; XRELOAD keeps main's undo, unload and stale-baseline handling;
native data extraction streams the source drawings one at a time.
2026-10-12 12:02:04 +03:00
Afonsop 9cd898a17a web: raster images, WBLOCK/SAVEALL/SCRIPT/CTB/LAYTRANS/INSERT, filled TTF text
A fresh web-vs-desktop audit found the remaining gaps; this closes the
top ten.

Raster images: IMAGE attach read the picked file via image::open on a
name-only path (dead on wasm), and raster references inside opened
drawings decoded the same way. Picked bytes are now registered in a
name-keyed store (the PDF-underlay pattern), decoding runs from memory,
and dropped .png/.jpg/.bmp/.tif files register by name so unresolved
references in opened drawings re-resolve and render (and export to PDF).

Command gaps fixed with the bytes-at-pick / download_bytes pattern:
- WBLOCK now downloads the extracted block instead of failing on fs.
- SAVEALL saves every dirty tab instead of printing a no-op message.
- SCRIPT picks and reads the .scr file through the browser picker.
- CENTERLTYPEFILE picks the .lin/.dnl file and populates from text.
- Custom .ctb tables picked on web now parse, register for the
  session, appear in the plot dialog list, and apply (load_named
  previously returned builtin-only on wasm).
- LAYTRANS 'Load standards file' parses the picked drawing on the
  existing web worker.
- INSERT 'from file' in the block palette parses picked bytes and
  feeds the shared import path.

Text fidelity: TTF-named styles rendered as thin stroke glyphs on web
because the font database was empty. Fetched Noto faces are now
registered into it with a family alias table (Arial, Times New Roman,
Cambria, ... -> Noto Sans/Serif), so those styles render filled, shape,
export to PDF as vector fills, and appear in the style picker.
ViewCube labels request semibold again on web.

docs/native-vs-web.md: the hatch and font rows were stale (both work on
web now), the GPU row claimed WebGPU is used where available (the
backend is pinned to OpenGL), and the parallelism row missed the parse
worker — all corrected.

(cherry picked from commit c643a10e)

Ported onto main (#1717 review): conflicts resolved toward main — the
font-file lookup, PDF searchable text, purge service, block palette and
plot-style copy stay as they are on main; the web pieces (font registry
and aliases, session plot styles, SCRIPT/SAVEALL/CENTERLTYPEFILE picks,
block-from-file on the web) are layered on top.
2026-10-12 12:02:04 +03:00
Afonsop 69fa7cae4f web: fix red-team breaks in restore, drop-open, and plot paths
Three attack agents (state-machine tracing, executable input fuzzing,
browser-API review) found two breaks and several degradations; all are
fixed here, with the fuzzer's 22 tests kept as regression suites.

Breaks:
- Untitled tabs (and dot-names like ".dxf") autosave as DXF, but the
  restore name defaulted to .dwg — the worker dispatches its reader by
  the name's extension, so the only copy of unsaved work could never
  be restored and was re-offered forever. recovered_name now uses the
  worker's rsplit('.') convention and defaults extensionless names to
  dxf, matching new-document serialization.
- " (recovered)" stacked one suffix per crash-restore cycle; the
  marker is now stripped idempotently before appending.
- Opening a file (Ctrl+O / OPEN / automation) while a web open was in
  flight clobbered it: the in-flight open's bytes were lost and every
  queued restore prompt stalled for the session. on_open_file now
  refuses while an open is running (web only).

Hardening:
- Escape on the Recover Drawing modal defers instead of deleting the
  only snapshot; the Discard button still removes it.
- A cap-rejected restore enqueue warns in-app and re-offers the next
  snapshot; the drop queue cap rises to 32.
- Exiting mid-round no longer races snapshot removals (deferrals are
  drained, the sweep skips while a round is in flight); closing a tab
  with "Don't save" now also removes its web snapshot; the snapshot
  key derivation is shared via web_autosave_key.
- The plot-preview overlay no longer leaks a full PDF copy per preview
  (the Download closure lives in a replaceable registry).
- A corrupt autosave index.json fails the round/boot instead of
  silently resetting every row; plot pages with non-finite or non-
  positive sheet dimensions error instead of emitting a NaN MediaBox.
- The drop filter accepts dot-less "DWG"/"dxf" names the worker
  parses (same rsplit convention on both gates).

(cherry picked from commit 3eab00bfbf)

Ported onto main (#1717 review): the five standalone tests/*_redteam.rs
files stay out — no test target builds them and they exercised copies of
the functions rather than the code itself.
2026-10-12 12:02:04 +03:00
Afonsop 9611f40400 web: never resurface an emptied autosave-restore prompt
A queued AutosaveRestore startup modal can outlive the pending list
that justifies it: a file opened while the prompt is up displaces the
modal, the entry is consumed, and the after-message hook then resurfaces
a stale, empty Recover Drawing prompt. The hook now drops queued
restore prompts whose pending list is empty.

(cherry picked from commit 64160fd68d)
2026-10-12 12:02:04 +03:00
Afonsop 7b467e16ea web: restore deletes its snapshot by open identity, not name
Second review pass found the restore marker matched WebFileOpened by
display name: any same-named open finishing while the recovered one
was still queued deleted the only copy. The snapshot key now travels
with the queued file (tagged queue entries), the marker records the
open id assigned at dispatch, and success, failure, and cancel all
match that id. Cancelling an unrelated open no longer clears a
pending restore marker.

Also: the boot scan removes dead index rows (missing or zero-byte
snapshot files) that no other path could discard, and the drop
listener install only latches when the listeners really registered.

(cherry picked from commit c8777a857b)
2026-10-12 12:02:04 +03:00
Afonsop 3023cefea4 web: harden drop-open and autosave after adversarial review
Review of the drop-open and autosave commits surfaced one blocker and
five majors; all are fixed here, and two claims in the autosave commit
message are corrected by this one: tab close does NOT clear a snapshot
(matches native .sv$ semantics — saving, restoring, discarding, exit,
and the capped boot scan do), and serialization runs on the main
thread between awaits, not off the paint path.

- The restore prompt can no longer appear with no entries (empty
  "Recover Drawing" modal on every boot once the autosave dir exists).
- Restore no longer consumes the snapshot before the open succeeds:
  the recovered drawing goes through the guarded drop queue, and the
  snapshot is deleted only after the tab opens; cancel or parse
  failure keeps the only copy.
- Saving during an autosave round defers its snapshot cleanup until
  the round finishes, so a stale round can no longer resurrect a
  snapshot newer than the save.
- The boot scan no longer deletes snapshots by mtime comparison
  (re-opening a drawing refreshes the recent copy's mtime, which
  silently destroyed wanted snapshots); GC is now a 16-entry cap with
  oldest-first eviction.
- OPFS write failures abort the writable stream (a leaked lock could
  stall the same-key writer and disable autosave for the session), and
  DOMException errors now carry their name/message.
- Empty (crash-window) snapshots are never offered; an unreadable one
  warns and stays discardable instead of looping forever.
- Drop-open polish: reads start inside the handler and complete in
  drop order (legacy WebKit safety, deterministic multi-file order),
  dragenter is canceled too, the install guard only latches on
  success, non-drawing drops don't steal propagation, a lock-poison
  panic can't cross into JS, and re-dropping an open drawing no
  longer risks clobbering an in-flight open (it re-queues).
- The model-space sheet floor is per-axis (a thin 8x400 mm profile
  keeps its length) and centers content on the enlarged page.

(cherry picked from commit 3edbc5b7c8)
2026-10-12 12:02:04 +03:00
Afonsop 12fd04790f web: autosave snapshots with crash-recovery restore, persistent storage
The web build had no autosave: on_autosave was a no-op and the
subscription was native-only. Both gates are gone; every savetime tick
now serializes each dirty tab (same pre-serialize sync as the web save
path, no thumbnail, serialized serially off the critical paint path)
into a dedicated opencadstudio-autosave OPFS directory, keyed by
drawing name, without touching the dirty flag, downloads, or recents.

On boot the web app scans that directory and offers any snapshot
newer than the last saved copy in a localized Recover Drawing modal
(Restore opens it as "name (recovered).dwg" through the standard
open pipeline; Discard removes it; Escape discards). Saving or
closing a drawing clears its snapshot; the boot scan also garbage-
collects stale entries. run_web now requests navigator.storage.persist
so settings and OPFS data survive browser eviction.

(cherry picked from commit 101d167ebc)
2026-10-12 12:02:04 +03:00
Afonsop 6cb8c00fa0 web: enable STL/STEP export and OBJ import, drag-and-drop open
STLOUT/STEPOUT wrote via std::fs and IMPORTOBJ read via std::fs, all
of which fail on wasm32. The export pickers now mirror the PDF path
(pick_*_owned returns a name-only PathBuf; the browser download names
the file) and the write tail swaps fs::write for sys::download_bytes.
OBJ import reads the picked file's bytes directly, like the web open
picker.

Opening by drag-and-drop was dead on the web build: winit never emits
FileDropped there. A wasm-gated io::web_drop module installs DOM
dragover/drop listeners, filters .dwg/.dxf, reads the File bytes
async, and queues them; the existing 50 ms PollWebControl tick drains
the queue into the same open path the picker uses (progress overlay,
recovery, OPFS recent cache).

Also floors the model-space plot sheet: extents smaller than 10 mm
(or the 0x0 paper a fresh unsaved drawing carries) now fall back to
the standard sheet instead of producing a ~3 pt page.

(cherry picked from commit ad60be11ed)
2026-10-12 12:02:04 +03:00
Afonsop 112060e370 web: harden plot preview after adversarial review
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)
2026-10-12 12:02:04 +03:00
Afonsop 8cce362009 web: warn when an opened drawing's name collides with an open tab
Web builds identify drawings by bare file name (the browser picker
exposes no real path), so two tabs with the same name also share the
single browser-stored copy: the last save silently replaces the other
tab's. Warn on open instead of letting that surface later.

(cherry picked from commit 6306359390)
2026-10-12 12:02:04 +03:00
Afonsop 4d1f9d5960 web: render plot preview in-page instead of a new tab
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)
2026-10-12 12:02:04 +03:00
Afonsop c8386c6d3a build: make the Trunk web-worker hook work on Windows
Trunk passes TRUNK_STAGING_DIR as an extended-length path (\?\C:\...)
on Windows, which MSYS sh tools cannot stat, so post_build failed and
the whole web build aborted. Strip the prefix with sed before use;
Linux CI paths are unaffected.

(cherry picked from commit e3c86eca29)
2026-10-12 12:02:04 +03:00
Afonsop 38f0e2b25f app: wire web export, preview, and print flows through the plot UI
- plot dialog: the web build defaults to 'Save to PDF file...' (its only
  output choice); native dropdowns and defaults are untouched.
- on_plot_dlg_commit forces to_file on wasm so a stale page setup can no
  longer route the web build into the (unreachable) printer path; both
  preview branches now go through preview_page_pdf, which is identical to
  the old inline temp-PDF + OS-viewer flow on desktop.
- Print ribbon / PRINTALL on wasm render the PDF and open it in a tab
  instead of erroring; PrintAllPdf picks a virtual path and downloads.
- pdf_plot_options maps the dialog's Quality dropdown into PdfPlotOptions.

(cherry picked from commit 892d693d2d)
2026-10-12 12:02:04 +03:00
Afonsop 978000d37c io: enable the PDF pipeline on wasm32 and add plot quality presets
- 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.
2026-10-12 12:02:04 +03:00
Karim Jerbi 7b68d274ad feat(mcp): add batch summaries with read-only detail retrieval
Add opt-in response_detail=summary with step and change counts, compact state, warnings, and actionable errors or prompts. Return an ocs_read batch_result request for full cached details without resuming mutations.

Keep compact/full behavior and idempotent batch retries intact. Preserve active-step context and report unavailable cached results explicitly. Document cache scope and retention; update schemas and digest.

Validation: 47 MCP unit tests, live stdio smoke test, rustfmt, and diff checks passed.
2026-10-12 09:39:54 +01:00
Karim Jerbi f4b33d5cde perf(mcp): deduplicate operation schemas with shared definitions
Share operation definitions between individual requests and batch steps, reducing the execute request schema by 39%. Place definitions at the tool input schema root so local references resolve correctly.

Update the schema digest and smoke assertions. Verified with 45 MCP tests, the live stdio smoke test, and 228 schema validation comparisons.
2026-10-12 09:05:38 +01:00
Karim Jerbi 2b50b6c858 fix(blocks): one block definition service for delete, rename and purge on every path
new src/scene/block_service.rs (references, delete, rename, purge candidates, name validation, one error type and message). The command-line RENAME, palette Remove, plugin API, control/MCP block_delete and PURGE all use it. Layout, xref, dynamic-block, anonymous and open-in-editor records are protected; PURGE removes an anonymous block only when nothing references it. PROTOCOL CHANGE: control/MCP block_delete refuses a referenced block unless force=true (erases referencing INSERTs in one undo step). New REST route DELETE /api/v1/blocks/{name}?force=true. MCP tool digest re-pinned. Undo labels are Delete block X, Rename block X to Y and Purge.
2026-10-12 07:25:11 +01:00
Karim Jerbi 5dfa41c032 fix(blocks): one base point convention for creation, redefine, insert, explode, XCLIP and REFEDIT
world = insert_point + Rot * Scale * (P - base_point). Attribute placement, EXPLODE, XCLIP storage, REFEDIT's working copy, the UCS reframe of attributes and the dynamic-block visibility grip ignored the base point and were offset by Rot*Scale*base. They now share insert_transform_with_base. Creation stores a ZERO base and redefine resets it; a no-op redefine moves nothing; the Block Definition dialog prefills the base from the definition. XCLIP filters stored by older builds keep their old displacement.
2026-10-12 04:35:19 +01:00
Karim Jerbi a456367b26 fix(save): baked dimension blocks and block clones must not share extension dictionaries
bake_dimension_blocks copied the dimension's whole EntityCommon, including its extension dictionary handle, into every baked child, so many entities claimed one dictionary and file validation reported an error per entity. Baked children now get no extension dictionary. The same defect is fixed in define_transformed_block and the cross-document paste clone. Each has a test.
2026-10-12 04:35:19 +01:00
Hakan SevenandClaude Opus 5.5 cd07141cde fix(fonts): keep missing font names clear of the list's scrollbar
The missing-fonts list is as wide as its longest name, so its scrollbar
was drawn over the end of every name ("ROMANT.sh").

Fixes #1715

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 01:17:20 +03:00
Hakan SevenandClaude Opus 5.5 551d4c1c60 fix(grips): arcs, circles and solids with a flipped normal show grips in place
Their grip code already works in world space, and the dispatch layer
converted the grips through the OCS a second time, so for a normal of
(0,0,-1) the grips (and grip edits) were mirrored across the Y axis.

Fixes #1683

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 01:13:12 +03:00
Hakan SevenandClaude Opus 5.5 15c566c286 fix(render): thickened polylines without width keep their colour
A polyline with a thickness but no width is drawn as its extrusion edges
alone, and those edges were forced black (the outline colour meant for the
filled tube of a wide polyline). On a dark background the polyline looked
black and changing its colour did nothing. Only a wide polyline's tube
edges are black now.

Fixes #1682

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 01:13:12 +03:00
Hakan SevenandClaude Opus 5.5 1973605cb0 test(dock): drive the real view on a large stack, one at a time
The two dock tests that build and drive the real view overflowed a
default test thread's stack, and made in parallel their headless
renderers crashed the test process about one run in three.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 00:49:27 +03:00
Hakan Seven 1333efcad2 Merge pull request #1549 from MarcelP102/codex/graphic-attributes-cleanup
[UX] Introducing graphic attributes panel

# Conflicts:
#	locales/ar-SA/opencadstudio.ftl
#	locales/bg-BG/opencadstudio.ftl
#	locales/cs-CZ/opencadstudio.ftl
#	locales/de-DE/opencadstudio.ftl
#	locales/el-GR/opencadstudio.ftl
#	locales/en-US/opencadstudio.ftl
#	locales/es-ES/opencadstudio.ftl
#	locales/fi-FI/opencadstudio.ftl
#	locales/fr-FR/opencadstudio.ftl
#	locales/hi-IN/opencadstudio.ftl
#	locales/hu-HU/opencadstudio.ftl
#	locales/it-IT/opencadstudio.ftl
#	locales/ja-JP/opencadstudio.ftl
#	locales/ko-KR/opencadstudio.ftl
#	locales/lo-LA/opencadstudio.ftl
#	locales/nl-NL/opencadstudio.ftl
#	locales/pl-PL/opencadstudio.ftl
#	locales/pt-BR/opencadstudio.ftl
#	locales/ru-RU/opencadstudio.ftl
#	locales/tr-TR/opencadstudio.ftl
#	locales/zh-CN/opencadstudio.ftl
#	locales/zh-TW/opencadstudio.ftl
#	src/app/mod.rs
#	src/app/update/dialog.rs
#	src/app/view/mod.rs
#	src/locale_catalog.rs
#	src/ui/dock.rs
2026-10-12 00:49:27 +03:00
Hakan Seven 7ca457a6cc Merge pull request #1702 from MarcelP102/Pallet-placement
[UX] Pallets you can dock, group and float (including LAYERS panel)
2026-10-12 00:27:53 +03:00
Hakan SevenandClaude Opus 5.5 7a942222c9 docs(plugin): describe XRecord storage without a vendor name
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 00:23:46 +03:00
Hakan Seven e917c23b46 Merge pull request #1716 from schoeller/xrecord_crud
Xrecord crud
2026-10-12 00:21:14 +03:00
Hakan Seven f618d695e2 Merge pull request #1684 from turbuczn/hungarian-translation-only
Improve existing Hungarian CAD translations

# Conflicts:
#	locales/hu-HU/opencadstudio.ftl
2026-10-12 00:21:07 +03:00
Hakan SevenandClaude Opus 5.5 64e9b612cb fix(plot): EXPORTPDF proposes PDF after EXPORTSVG; SVG strings in every locale
EXPORTSVG left the plot dialog's SVG device on, so a later EXPORTPDF still
offered a .svg file. The SVG save dialog and plot button texts had no
catalog entries.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-12 00:20:20 +03:00