Ctrl+V was globally bound to the entity paste command, so it never pasted
into an open text editor, and even when routed there iced's clipboard read
is a no-op on the web. Route Ctrl+V to the focused editor and, on the web,
read the system clipboard via the browser's async Clipboard API (the
keypress is a user gesture, so the read is permitted):
- MText editor: insert at the caret (web + native).
- TEXT field: append on the web; native lets the text_input paste itself.
- No editor open: falls through to the entity paste command as before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The web build has no system-font access, so CAD text in non-Latin scripts
(Cyrillic, Greek, CJK, …) rendered as nothing — build_fallback was a hard
None on wasm (#141). Instead, ship per-script Noto subsets under web/fonts
(one alphabet per file, SIL OFL 1.1) and fetch them lazily over HTTP the
first time a drawing uses that script, then outline glyphs with ttf-parser.
- web_font: Script enum + script_of (char → script font), a per-script
store with lazy fetch, and a pending queue drained by the app loop.
- CJK is split by language (chinese/japanese/korean). Han ideographs share
code points but differ by language, so the shared block is routed by the
document's $DWGCODEPAGE (932→JP, 949→KR, GB/936→CN); kana is always
Japanese, Hangul always Korean. Re-tessellates when the language changes.
- build_fallback (wasm) outlines from the fetched subset; clear_fallback_cache
lets glyphs that missed while a font was in flight reappear on load.
- PollWebFonts subscription (web only) + WebFontLoaded message drive fetches.
- Trunk copy-dir serves web/fonts; nothing is bundled into the native binary
(desktop keeps using system fonts).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the ad-hoc `--serve` arg sniff with a clap-parsed CLI:
- positional <file> to open at startup (also the OS file-association path)
- --new, --read-only, --backend, --safe-mode/--no-gpu
- --serve/--port (headless JSON automation, unchanged behaviour)
- --export IN OUT (headless format convert via the automation load/save infra)
- --script FILE (run command lines at startup)
- --log/RUST_LOG, plus --version/--help for free
GUI-only options pass to app::boot through a OnceLock config; read-only
gates the save paths. The web build skips parsing (no args).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
OpenCADStudio no longer bundles plugins — every add-on is an external
cdylib loaded from the plugins folder or installed via the marketplace.
- Remove the demo plugin and the in-tree ocs_example_plugin crate.
- Remove the inventory-based built-in registration path (PluginRegistration,
all_plugins, installed_manifests); registry now serves core ribbon tabs
plus loaded external plugins, and try_dispatch routes only to externals.
- Plugin Manager drops the 'compiled into this build' section; the
enable/disable toggle moves onto loaded external cards.
- Prune the now-dead HostSession/DocumentTab helpers and re-exports.
Part of the #100 extensibility epic (phase 2).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a Plugin Manager marketplace that installs external add-ons from a
GitHub repo's Releases. A curated registry (plugins/registry.json in this
repo, extended by PR) lists discoverable plugins; the host fetches it,
shows each entry with a release dropdown, and on install downloads the
asset matching the user's platform plus plugin.toml into the plugins
folder (API-version checked first). Users can also link arbitrary
owner/repo manually. Linked repos persist in settings.
Desktop only (ureq + serde_json); the install path reuses the phase-2
loader. Part of the #100 extensibility epic (phase 2).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move BuiltinPlugin into ocs_plugin_api (host feature) so out-of-tree
crates can implement it, and add export_plugin! to emit the two C
symbols a cdylib exposes: ocs_plugin_api_version (checked first, so an
ABI-incompatible build never runs) and ocs_plugin_register -> boxed
BuiltinPlugin. The host loads every compatible package from the plugins
folder at startup via libloading (desktop only), keeps the library
resident for the session, and merges its ribbon tab + command dispatch
into the same paths as built-ins (honouring enable/disable). The Plugin
Manager shows external packages with a Loaded / incompatible status.
Approach B: the plugin hands back a boxed trait object, assuming a
matching toolchain + ocs_plugin_api version (the version symbol enforces
the latter). crates/ocs_example_plugin is the reference cdylib.
Part of the #100 extensibility epic (phase 2).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plugins targeted the host's concrete HostSession type. Add a HostApi
trait in ocs_plugin_api behind an optional `host` feature (the only thing
that pulls acadrust, so the core crate stays dependency-free). HostSession
implements it and BuiltinPlugin::dispatch now takes `&mut dyn HostApi`, so
an out-of-tree add-on compiles against the contract crate alone. Per-tab
plugin state is reached through object-safe plugin_state* helpers.
Completes the phase-1 host-surface extraction in the #100 epic.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add an "OCS Web" button (bright ribbon-blue, desktop only) to the Start
page that opens the web build at hakanseven12.github.io/OpenCADStudio.
- Send Feedback now opens the GitHub issue with the body pre-filled:
app version + platform (OS/arch on desktop, browser user-agent on web),
via sys::platform_info + sys::percent_encode.
- Remove the redundant About button from the Start page (ABOUT command /
menu still opens it).
Native + web build; 75 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On the web, Save and Save As now open the in-app Save dialog (filename +
format) like the desktop, and the unsaved-changes prompt's Save routes
through it too — instead of an immediate rfd download. Confirm serializes
to bytes and downloads directly via a Blob + hidden `<a download>` click
(a user gesture), so there is no intermediate "click to download" link.
Replaces the rfd-based download_web / WebSaveResult round-trip with a
synchronous sys::download_bytes. Native save is unchanged.
Native + web build; 75 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Add DONATE to the Start-tab command allowlist; it was blocked there
with "No drawing open" while REPORT/CHANGELOG/ABOUT were allowed, so
the Donate button did nothing on the welcome screen.
- Implement sys::open_url on wasm via web_sys window.open(_blank) — it
was a no-op stub, so Donate, Release Notes and Send Feedback opened
nothing on the web. The click is a user gesture, so the pop-up blocker
allows it.
Native + web build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
With the wasm bundle building, get it rendering in a browser:
- Enable iced `webgl` so wgpu uses WebGL2 where WebGPU is absent
(e.g. Firefox by default), and `fira-sans` so UI text has an
embedded font (the web has no system fonts) — without it every
label was blank.
- Replace `std::time::Instant` with `iced::time::Instant` (web-safe);
`Instant::now()` panics ("time not implemented") on wasm and crashed
at boot via the command line.
- Skip the batched-hatch pipeline on wasm: its bind group needs
vertex-stage storage buffers, which WebGL2 lacks (validation error
on "New drawing"). Fields become Option; upload/draw guard on them.
Hatches don't draw on the web for now; everything else does.
- Fix the trunk feature flag (`data-cargo-no-default-features`) and
ignore the `dist/` build output.
Native build + 75 tests unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
wasm32 has no threads, so rayon's global pool would panic at runtime.
Add `crate::par::prelude`: native re-exports `rayon::prelude`, wasm
provides sequential `std`-iterator equivalents (par_iter / into_par_iter
/ flat_map_iter over slices, Vec, HashMap, HashSet). All call sites now
`use crate::par::prelude::*;` instead of rayon directly.
- wire_gpu's rayon `reduce` (2-arg, no std equivalent) is cfg-split to
a sequential flat_map concat on wasm.
- rayon moves to a non-wasm target dependency.
Native still runs in parallel; all 75 tests pass. wasm bin compiles.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The binary now compiles for wasm32 as a runnable web target:
- Split the main-window render into `view_main`, bypassing the
multi-window id dispatch the browser cannot use.
- Add `run_web` / `boot_web`: a single-window `iced::application`
entry for wasm (the browser canvas is the only window). Native
keeps the multi-window `iced::daemon` via cfg-split in `main`.
- Add a `console_error_panic_hook` (wasm-only) for readable panics.
- Add `index.html` + `Trunk.toml` so `trunk serve` builds the wasm
bundle (no-default-features → no solid3d) with COOP/COEP headers.
Native build and all 75 tests unchanged. Browser runtime (rayon
threads, canvas attach, real file I/O) is the next iteration.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First step toward a web build: the library now compiles for
wasm32-unknown-unknown with --no-default-features.
- Add a `solid3d` feature (default on) gating truck-meshalgo /
truck-shapeops / lzma-sys. These pull vtkio → xz2 → lzma-sys (C),
which cannot cross-compile to wasm. Without the feature the 3-D
solid path (tessellation, booleans, ACIS import) becomes no-ops;
2-D CAD is unaffected. Native builds keep the feature by default.
- Move native-only crates (ureq, open) to non-wasm target deps and
add cfg shims: update-check is a no-op on wasm, and a new `sys`
module provides open_url + handle_path (rfd FileHandle → PathBuf;
synthesizes a name-based path on the web).
Native build + all 75 tests unchanged. Browser runtime wiring
(entry point, trunk, real file I/O) is a follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a TrueType/OpenType render path alongside the LFF stroke engine so
users can pick fonts installed on their machine for text and MText.
- Discover system families via fontdb (sysfont) and list them in the
text-style picker.
- Extract glyph outlines with ttf-parser, flattened into wire strokes
normalized to the same 9-unit cap-height space as LFF glyphs.
- Shape runs with cosmic-text (ligatures, Arabic joining, kerning, bidi)
plus automatic font fallback: a glyph missing from the chosen family
is taken from another installed font.
- Unify both sources behind a single Face type; a shared run tokenizer
drives the stroke (per-glyph) and shaped (TTF) renderers identically.
- Stroke fonts now fall back to a system TTF (cosmic-text picks it) for
characters no LFF font covers, replacing the unicode/iso3098 chain.
Also trim the embedded LFF set from 47 to 26, keeping the AutoCAD-SHX
equivalents and dropping LibreCAD-only fonts now covered by TrueType.
LFF builtins keep priority over same-named system fonts so DXF text
renders identically across machines.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Windows executable carried no icon, so Explorer, the taskbar, the
Start-menu tile and file-association entries showed a generic icon. Embed
AppIcon.ico via winresource in build.rs, and generate the .ico from the logo
before the build (it was produced after the build, too late to embed). Local
and non-Windows builds skip the step when the .ico is absent.
Fixes#107.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The `debug` feature on iced installs a built-in F12 devtools hook that opens
the comet debugger (and prompts to install it when missing). That intercepted
F12 before OCS could toggle dynamic input. Drop the feature so F12 reaches the
app's own handler.
Fixes the F12 part of #101.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the dependency-free, semver-versioned half of the plugin contract into a
standalone workspace crate `crates/ocs_plugin_api`:
- manifest types: PluginManifest, ApiVersion, API_VERSION
- ribbon vocabulary: CadModule trait + ToolDef/RibbonGroup/RibbonItem/IconKind/
ModuleEvent/StyleKey
The host re-exports them from `crate::plugin::manifest` and `crate::modules`, so
every existing call site is unchanged. The acadrust-typed runtime surface
(HostSession) stays in the host binary; lifting it behind a HostApi trait in the
same crate is the remaining phase-1b step.
Part of #100. Docs updated in docs/plugin-architecture.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Installed builds already register a handler (MSI ProgIDs, the Linux
.desktop MimeType, the macOS Info.plist), but the portable .exe and the
AppImage register nothing, so they never appear in the OS "Open with"
list. Add a best-effort, idempotent runtime registration that runs
silently on a detached thread at startup:
* Windows — writes HKCU\Software\Classes\Applications\OpenCADStudio.exe
(open command + FriendlyAppName + SupportedTypes .dwg/.dxf), which is
what lists the exe in "Open with → Choose another app".
* Linux — writes a user-level .desktop pointing at the running binary
($APPIMAGE when set) and refreshes the desktop database. Skipped when
a system package already provides the desktop entry, so we never
shadow a distro install.
* macOS — no-op; .app bundles auto-register with LaunchServices.
Unlike the default-app prompt this changes no defaults and shows no UI;
it only makes the binary discoverable as a handler.
https://claude.ai/code/session_01DFVSKgViFaxEjZD2KsTdtt
On the first launch that hasn't answered it yet, the app shows a small
dialog offering to become the default handler for .dwg and .dxf files.
The choice is persisted (default_assoc_prompted) so the prompt never
reappears.
The platform plumbing lives in io/file_association.rs:
* Windows — opens the OS's own per-app default-programs dialog via
IApplicationAssociationRegistrationUI::LaunchAdvancedAssociationUI,
passing the "Open CAD Studio" RegisteredApplications name the MSI
registers. Modern Windows guards the actual default behind a
UserChoice hash, so this is the supported consent path.
* Linux — xdg-mime default against the installed .desktop file, whose
MimeType entries already cover image/vnd.dwg and image/vnd.dxf.
* macOS — LSSetDefaultRoleHandlerForContentType binds the DWG/DXF UTIs
declared in Info.plist to the app bundle id.
The work runs on a dedicated thread (the Windows dialog is modal) with
the result delivered back through a oneshot channel and surfaced in the
command line.
https://claude.ai/code/session_01DFVSKgViFaxEjZD2KsTdtt
Phase 4.1. Handle / u64 keys dominate the hot maps (block_defn,
hatch / image / mesh caches, draw-depth map, viewport wire caches);
the default SipHash is wasted work on integer keys. Alias the std
types to rustc_hash::FxHashMap / FxHashSet crate-wide so every map and
set — including the cross-module draw-depth map shared between scene
and the GPU pipelines — uses the faster hasher consistently.
Mechanical: no behavioural change. Constructors moved from ::new() to
::default() (Fx variants carry no RandomState). Build + 13 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Read the model-space annotation scale from the standard CANNOSCALE /
CANNOSCALEVALUE header variables instead of a user-variable hack, and
populate both the status-bar scale picker and the viewport Scale
property purely from the drawing's ACAD_SCALELIST — no built-in
fallback set. Xref-derived scales (`_XREF` suffix, `name|` prefix)
are filtered out so only the host drawing's own scales appear.
Bumps the patched acadrust to the commit that adds CANNOSCALE/
CANNOSCALEVALUE read/write.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
vtkio (via truck-meshalgo → xz2 → lzma-sys) linked the system liblzma
dynamically, baking a Homebrew path (/opt/homebrew/.../liblzma.5.dylib)
into the macOS binary. The .app then crashed on launch on any Mac
without that exact library. Enable lzma-sys's `static` feature so
liblzma is compiled from the bundled source into the binary, making it
self-contained on every platform (verified: no dynamic liblzma link).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rename the Home tab to "Draw" and add a Model tab beside it with two
groups:
- Model: BOX / CYLINDER / CONE / SPHERE / WEDGE / TORUS, placed with a
footprint + height. Each is built as a real ACIS Solid3D (acadrust
primitive builders) plus a truck B-rep, tessellated into the shaded
mesh pipeline, with edge wires for picking + wireframe.
- Design: UNION / SUBTRACT / INTERSECT via truck-shapeops.
A session-only Scene.model_solids cache holds each solid's truck B-rep
so the boolean tools can combine and chain them. Supersedes the old
command-line BOX/SPHERE/CYLINDER (insert::solid3d_cmds keeps EXTRUDE/
REVOLVE/SWEEP/LOFT).
Known limitations: booleans need solids created this session; curved
ACIS primitives render in-session but not after reload (the SAT
tessellator handles only planar caps); geometry assumes world_offset 0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Type directly into the rendered MText preview: click to position the
caret, insert/delete, Enter for a new line, Space for a literal space,
and arrow keys to move. The caret blinks when idle.
Keep the trailing empty paragraph in the editor so the caret appears on a
fresh line immediately after Enter, before any typing. parse_mtext_paragraphs
split into a _ex variant; layout_mtext skips the blank-edge trim when
emitting glyph boxes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Type-as-you-go suggestion popup above the input that filters known
command names by substring (so `LEADER` surfaces both LEADER and
MLEADER). Up / Down navigates the list, Enter dispatches the
highlighted entry, clicking a row dispatches directly. Esc on a
non-empty input clears the buffer instead of escalating to
command-cancel.
The name corpus comes from `inventory::submit!` blocks placed next
to each `CadCommand` impl rather than a hand-maintained static list.
Adding a new interactive command means dropping one `inventory::submit!`
line in its own module file — the autocomplete picks it up at the
next compile. 98 submissions seeded across 60 module files;
`command::all_registered_command_names()` flattens them at runtime.
One-shot dispatch arms in `app/commands.rs` (NEW, OPEN, SAVE, UNDO,
…) don't have a `CadCommand` impl and are therefore absent from
autocomplete by design.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bumps bytemuck 1.14->1.25, glam 0.27->0.33, rfd 0.15->0.17,
windows-sys 0.59->0.61, ureq 2->3. ureq 3 changed the agent API
and feature flags, so update_check.rs switches to Agent::config_builder
and body_mut().read_to_string(), and the feature toggles from tls
to rustls.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>