Commit Graph
2299 Commits
Author SHA1 Message Date
Hakan SevenandClaude Opus 5.5 57eb7fe4db feat(layout): MVSETUP sets a drawing up for plotting
In model space MVSETUP draws the sheet border at the drawing scale
(scale factor, paper width and height) and zooms to it, or switches to
a layout. On a layout it creates viewports in a bounding area (a single
one, a 2x2 engineering set of top/front/right/isometric views, or an
array with spacing), deletes picked objects, and scales picked
viewports to a paper/model unit ratio.

A viewport's fit to the model now measures the model along the
viewport's own view direction, so side and isometric views frame it
too.

Refs #1501

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 15:21:26 +03:00
Hakan SevenandClaude Opus 5.5 b1d4c9bd9b feat(clipboard): PASTESPEC pastes in a chosen format
PASTESPEC offers the formats on the clipboard: Objects and Block for
objects copied in the application (the PASTECLIP and PASTEBLOCK paths),
and Text for the system clipboard's text, placed as multi-line text in
the current text style with its lines as paragraphs.

Refs #645

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 15:06:58 +03:00
Hakan SevenandClaude Opus 5.5 4ffae00199 feat(view): DVIEW twists the view about its line of sight
DVIEW (alias DV) offers TWist and Undo. TWist rolls the active view
or viewport camera to the typed angle; Undo steps back through the
twists made in the session. The new alias reaches existing profiles
through the alias migration table.

Refs #638

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:47:34 +03:00
Hakan SevenandClaude Opus 5.5 6490a625f2 feat(hatch): add and remove boundary vertices from the grip menu
A non-associative hatch's boundary vertex grips now offer Add Vertex and
Remove Vertex next to Stretch, on polyline boundaries and on boundaries
made of line edges alike. Add Vertex inserts the vertex after the picked
one, halfway along the next segment (on the arc for a bulged polyline
segment, splitting its bulge), and lets it be placed like any grip.
Remove Vertex drops it while keeping at least a triangle.

Line-edge boundaries used to show two grips at every corner (one line's
end, the next one's start), so dragging one opened the loop; each corner
is now one grip that moves both ends.

The automation grip_menu_pick action chooses an entry of the open grip
menu, so these edits can be scripted and checked.

Fixes #1058

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:38:58 +03:00
Hakan SevenandClaude Opus 5.5 4518569b61 feat(plot): PUBLISH opens the batch plot of the drawing's layouts
PUBLISH now runs what PRINTALL does: pick the layouts, each plotted with
its own page setup, into one multi-page PDF or to the printer.

Refs #1124

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:24:34 +03:00
Hakan SevenandClaude Opus 5.5 0eec9c450f feat(input): lock the next point to a typed <angle
Typing <angle at a point prompt (e.g. <30) now fixes the direction of
the next point from the last one until a point is placed: the cursor
moves along that direction, and a bare number entered next is the
distance along it. The angle is read in the drawing's angle base and
direction and turned into the current UCS. Works from the command line
and from scripts or the automation token input alike.

Fixes #1464

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:21:50 +03:00
Karim Jerbi 9c08e8a3ed fix(blocks): prevent circular block nesting and cut render cycles
Introduce would_create_cycle in block_service to prevent recursive block definitions with case-insensitive matching across block records. Enforce cycle refusal at INSERT commit in BEDIT and during block redefinition (redefine_block_raw) with a shared error message. Ensure render-time recursion truncates safely with a one-time session warning on cyclic documents.
2026-10-13 11:10:11 +01:00
Karim Jerbi 59bb6faabd fix(blocks): enforce constant attributes in ATTEDIT and restore ATTDEFs on explode
Enforce constant attribute flag in ATTEDIT dialog by presenting constant attributes as disabled and ignoring modifications. When exploding an insert, restore block-definition attribute definitions as transformed ATTDEF entities while dropping instance attribute values.
2026-10-13 11:10:11 +01:00
Hakan SevenandClaude Opus 5.5 9cc890c911 fix(grips): drag model grips in the drawing plane, not onto themselves
In a tilted 3D view a grip followed the cursor onto the screen plane
through the orbit target, so a circle or arc jumped to another radius and
a polyline vertex left its plane. A model-space grip now moves in the UCS
XY plane at its own elevation (the screen plane only when seen edge-on).
A solid stays drawn while its grip moves, and its own edges were still
snap candidates: a cone's base radius landed on its own side. The edited
entities are left out of the grip's snap set.

Fixes #1033

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:12:27 +03:00
Hakan SevenandClaude Opus 5.5 df94117fe5 feat(automation): replay pointer events on the canvas
The new pointer op sends moves (canvas pixels, or a world point projected
through the camera the cursor edits with), button presses/releases and
wheel steps in order, and reports the cursor's world point after each, so
grip drags and picks go through the same cursor path as the mouse.
click now projects through that editing camera too (it used the model
camera inside an entered viewport), and the grips read reports each
grip's constraint axis.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:12:27 +03:00
ramox81andClaude Opus 5.5 c55dbb886e Layouts: first opening follows the page setup; viewports picked by their frame
- A layout opens for the first time when its page setup was never
  initialized (no paper size, no media name) or it was created in this
  session; its page setup is initialized then, and with
  LAYOUTCREATEVIEWPORT it gets its first viewport. A layout never opened
  is saved with an uninitialized page setup and no viewports, so it gets
  its viewport when the file is opened again, here or in the reference.
- Select Viewport for view: a viewport is selected when the pick box
  (PICKBOX pixels at the sheet's zoom) touches its frame; a pick well
  inside or outside selects nothing and the prompt repeats.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:02:26 +03:00
Karim Jerbi 97afb57333 feat(mcp): expose entity creation schemas on demand
Expose creation fields, constraints and examples for the 11 supported types without expanding startup tool schemas. Share the creation contract and constructor with execution to keep accepted inputs consistent.

Validation: MCP, contract/factory and entity creation regression tests.
2026-10-13 09:33:49 +01:00
Hakan Seven 798014c773 Merge pull request #1741 from ramox81/codex/view-background-followups 2026-10-13 11:27:33 +03:00
Hakan Seven 7095802e8e Merge pull request #1743 from ramox81/codex/layout-create-viewport 2026-10-13 11:27:33 +03:00
Hakan SevenandClaude Opus 5.5 f6503ad4d3 fix(ui): keep a typed layer or style name when the field loses focus
Renaming a layer or a style was only applied on Enter; clicking anywhere
else dropped the typed name. A click that moves focus off a field whose
text changed now commits it. New and Copy in the style managers open the
generated name for renaming, so a new style can be named right away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:19:12 +03:00
ramox81andClaude Opus 5.5 e3a0ff1bad Layouts: a viewport on first opening (LAYOUTCREATEVIEWPORT)
A layout opened for the first time - one whose sheet viewport had to be
made, or one created in this session - gets a viewport centred on the
printable area, 0.8 of its width and height, zoomed to the model's
extents, while LAYOUTCREATEVIEWPORT is on. The profile variable takes
ON/OFF or 1/0 and is kept with the settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:09:03 +03:00
ramox81andClaude Opus 5.5 2a808c4526 Background dialog preview: mirrored images, device pixels; faint inactive values
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:05:01 +03:00
Hakan SevenandClaude Opus 5.5 5d0390bcdf fix(annotative): start new drawings at the unit-aware annotation scale
A new drawing is metric with metre insertion units, but its annotation
multiplier stayed at Scene::new()'s unitless 1 while opened files and
CANNOSCALE convert paper millimetres into drawing units (0.001 at 1:1).
Annotative text and dimensions were drawn 1000x too large until the
first scale change, then shrank to their real size and looked gone,
also after switching back to 1:1.

One helper now derives the multiplier from the header; new drawings,
file open and every INSUNITS change (UNITS, the units popup, DWGUNITS,
SETVAR) go through it.

Fixes #1674

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:41:00 +03:00
ramox81andClaude Opus 5.5 0ee95fa4ba Background dialog: adjust an image's offset and scale; visual style names
- The Image tab gains an Adjust card: X/Y offset in pixels (-2000..2000)
  and X/Y scale (-10..10, tenth steps, a negative scale mirrors, zero keeps
  the image's scale) with spinners and Reset, inactive while the image is
  stretched; Keep aspect ratio is a switch and makes both scales follow.
- The preview shows the image at its offset and scale, tiled when tiled,
  and a stretched image kept to its aspect ratio covers the view.
- Automation: step=<field>,+1|-1 and reset; offset/scale take any numbers.
- Shaded with edges, Shades of Gray and Sketchy have catalog entries in
  every language.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:28:18 +03:00
Hakan SevenandClaude Opus 5.5 85627ebc8a feat(web): return saved drawings to an embedding page
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 #1704
Fixes #1705

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:12:07 +03:00
ramox81andClaude Opus 5.5 064da8829e View backgrounds: image colours as stored, viewport style name, adjust automation
- The background image texture is not an sRGB view: the image is drawn with
  its own colours, as the solid and gradient colours are.
- An entered paper-space viewport shows its named visual style (Realistic,
  ...) in the render-mode picker, and a mode picked there is recorded as the
  viewport's visual style.
- The view_background automation action takes offset=x,y and scale=x,y.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:44:49 +03:00
Hakan SevenandClaude Opus 5.5 468d1e4f43 fix(plot): honour the requested plot style over the layout's own
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>
2026-10-13 08:18:37 +03:00
Hakan SevenandClaude Opus 5.5 3840630ab4 fix(extend): use preselected objects as boundary edges
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>
2026-10-13 08:04:13 +03:00
Hakan SevenandClaude Opus 5.5 925c91ccfa fix(pasteblock): place the block at the picked point
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>
2026-10-13 08:04:13 +03:00
Hakan SevenandClaude Opus 5.5 88803cd692 fix(start): let the quick-access New button work on the Start screen
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>
2026-10-13 08:04:13 +03:00
Hakan SevenandClaude Opus 5.5 74bffe77be fix: follow-ups to the merged PRs
- 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>
2026-10-13 07:50:30 +03:00
Hakan Seven 03445a540d Merge pull request #1722 from ramox81/codex/view-background
# 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
2026-10-13 07:44:01 +03:00
Hakan Seven 5d279d4e0c Merge pull request #1726 from turbuczn/hungarian-localization-v2 2026-10-13 07:41:57 +03:00
Hakan Seven a8af415976 Merge pull request #1736 from turbuczn/feature/selection-filter-list 2026-10-13 07:39:32 +03:00
Hakan Seven a60ef6dc7d Merge pull request #1724 from schoeller/plugin_grip_refresh 2026-10-13 07:38:55 +03:00
Hakan Seven 3006d93ee5 Merge pull request #1718 from ramox81/codex/region-clears-selection 2026-10-13 07:27:29 +03:00
turbuczn 09cf99f85c feat: Add advanced selection filter 2026-10-12 22:27:19 +02:00
turbuczn 7c61583ac2 Refactor FILTER validation and operator normalization (v2.2) 2026-10-12 22:27:19 +02:00
ramox81andClaude Opus 5.5 9ef8f4bedf View backgrounds: reference blend, image adjust, layouts, cleanup, style name
- 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>
2026-10-12 17:31:01 +03:00
turbuczn 49f12e010f Complete Hungarian localization and improve block reference labels 2026-10-12 15:21:05 +02:00
Sebastian c6dcee2b1d fix: refresh selected grip markers after plugin-driven entity edits
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).
2026-10-12 12:30:19 +02: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 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