The web control channel gains a save operation that serializes the
active drawing in the requested format and returns it as base64 in the
reply, clearing the dirty flag, the counterpart of opening from bytes.
A host page can also define window.openCadStudioSaveFile(name, bytes)
to receive every saved or exported file in place of a browser
download; while it is set no file picker opens.
Fixes#1704Fixes#1705
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hatches are drawn before the wires and write depth, so a transparent
hatch above lines in draw order still hid them through the depth test.
Transparent hatches now go to their own renderer drawn after the wires:
the lines behind show through the blend and the lines in front still
win the depth test.
Fixes#1720
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A polyline band was stroked like a pen line, with round caps and round
joins, and each segment as its own path, so corners came out rounded
and overlapping. Bands now use butt caps and mitre joins in one path,
and a tapered band fills each segment's quad so a zero-width segment
stays a thin line.
Fixes#1721
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
*.pat files in <config>/patterns and *.lin files in <config>/linetypes
are read after the bundled catalogs, next to the fonts folder SHX
files already resolve from.
Fixes#1725
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
add_entity_clone reset handle, owner and mode but kept the source extension dictionary handle, so a copy claimed a dictionary its document does not own. The clone now gets no extension dictionary, like baked and transformed-block children. Failing-first test included.
Filleting a polyline segment with a line or arc moved the segment's
vertex and left the polyline closed and the other object separate, so a
closed shape bent into an unexpected outline. The polyline is now
opened if closed, cut at the fillet point keeping the picked side, and
the fillet arc and the trimmed line or arc are joined onto it as one
polyline, as the reference does.
Refs #1422
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A headless plot with plot_style loaded the layout's stored page setup,
which put the layout's CTB back in place and, when that file was not in
the plot styles folder, failed the plot with "not loaded". The
requested table now stays active for every page.
Bump codec ddf0fd2 (viewport on state from DXF group 68, so viewports
written without bit 0x8000 show their contents), kernel b80079a,
graph 45c9649.
Refs #1739
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TRIM already takes objects selected before the command as cutting
edges; EXTEND ignored them and extended to the nearest object in the
path. It now takes them as boundary edges the same way.
Fixes#1732
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pasted block is defined around the clipboard base, so its origin
is already that point, but the insertion still subtracted the base and
landed the block offset from the pick. The base now only positions the
world-space preview, and it stays f64.
Fixes#1731
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sync clients, indexers and virus scanners open a freshly written file
for a moment without delete sharing, which refuses ReplaceFileW with a
sharing violation. Saving straight after Save As into a Dropbox folder
hit this every time. The replace now retries for about two seconds
before reporting the file in use.
Fixes#1712
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The button runs QNEW since #1635, and QNEW was missing from the
commands the Start screen allows, so it was refused until a drawing
was open.
Fixes#1728
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Selection filter: condition rows name INSERT as a block reference, as
the object-type list does since #1726 (shared qselect_type_label).
- MCP: re-pin the tool schema digest for the view_background action
added in #1722.
- -VIEW: fix an indentation slip and wrap two long lines from #1722.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A layout plotted by window drew its sheet at the paper-space origin, so
the title block sat on the grey background away from the paper. The
sheet now centres on the plot window, or sits at the plot offset from it
when the plot is not centred.
Bump codec 8aa574a (polyline vertices in owned-handle order, which fixes
self-crossing viewport clip boundaries), kernel d79bb30, graph 4810ee0.
Fixes#1678
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-authored constraint-group geometry dependencies carried an empty
persistent subentity id, which crashed strict readers loading the file;
they now name the whole curve with a single-edge id.
Bump codec bc350f2 (declare classes of objects added to pre-R2004
drawings), kernel e692b97, graph 37d38eb.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 <noreply@anthropic.com>
- Gradients: the middle colour sits (horizon - 0.5) viewport widths from the
centre and blends over height viewport heights each side; two colours
blend over a viewport height each side.
- Image backgrounds: offset in device pixels (right, down), negative scale
mirrors, fitted with aspect ratio covers the viewport; the dialog keeps an
image's offset and scale.
- Layouts: a model-space view restored on a sheet asks for the viewport,
enters it and gives it the view's camera, background and visual style;
inside a viewport the view goes there; a paper-space view's background is
kept on the sheet viewport but not drawn; floating viewports draw their
background.
- Background objects no view or viewport uses any more are erased; a view
without a background restored where the viewport already is leaves the
viewport's background.
- The viewport's named visual style (Realistic, ...) shows by name and the
render-mode picker records its style on the viewport record.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HostSession::update_entity, update_entities_transaction and remove_entity mutate entities and republish the document view, but never rebuild the app-side selection caches the way the built-in edit paths do. A selected entity updated by a plugin kept its cached grip markers at their pre-edit positions until the next real selection event; regen did not help and HostApi::set_selection cannot work around it (it bypasses the grip cache).
Refresh the grip cache for the session tab after a successful update_entity / update_entities_transaction / remove_entity, mirroring the built-in ERASE path and the other editors, so a plugin-driven edit keeps the cached grips on the refreshed geometry. A session targeting a background tab leaves the active tab cache untouched (found by the civil plugin manual QA, 2026-10-12).
An image background not fitted to the screen keeps its own pixel size
(times its scale), centred on the viewport and moved by its offset, as
the reference draws it; tiling repeats it from there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
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>
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)
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)
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.
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.
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.
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)
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)
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.
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.
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.