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>
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>
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>
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 recent-file buttons dispatched the open command without closing the app
menu, so it stayed open over the loaded drawing. Use CloseAppMenuAndRun so the
menu closes first, matching the other menu entries. Also commits the Cargo.lock
update from dropping iced's debug feature.
Fixes#103.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Block references carrying an XCLIP spatial filter were drawn in full,
ignoring the clip boundary. Resolve the filter through the INSERT's
extension dictionary (ACAD_FILTER -> SPATIAL) and clip the expanded
block geometry to the boundary polygon: polylines are split into the
inside runs, fill triangles are clipped, and snap/key vertices outside
the boundary are dropped.
The boundary vertices live in the clip-definition space, so they are
placed with world = T_insert * (inverse_block_transform * vert); this
keeps the clip correct even when the insert was rescaled after the clip
was created. Bumps acadrust to the build that decodes SPATIAL_FILTER.
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>
ACAD_SURFACE entities now load and render. Their NURBS faces are
tessellated by building a truck BSplineSurface from the ACIS `spline-surface`
control net and knot vectors, then sampling its parametric grid — reusing the
same NURBS kernel the Model tab already builds on. Analytic faces
(plane/cone/sphere/torus) continue through the existing tessellator.
- New spline_tess.rs: parse the ACIS `nubs` block (degrees, knots with the
clamped end-multiplicity fix, control grid) into a truck BSplineSurface and
mesh it.
- tessellate_surface + Surface wired through the mesh-build, fallback-wire,
and entity-classification paths.
- Bump acadrust to the commit that adds the Surface entity.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dimension text position ignored the style: the renderer returned the
saved text_middle_point whenever it was non-zero, which it always is for
app-created dims, so DIMTAD ("text over the dimension line" / ISO) and
friends had no effect — text could only be moved by grips.
Use the new acadrust text_user_positioned flag: when the text wasn't
user-placed, compute the position from the style (DIMTAD/DIMTVP/DIMJUST)
anchored on the dimension line (definition_point); when it was (grip drag
or properties edit, both now set the flag), keep the saved point.
Imported drawings keep their fidelity via the flag round-tripping through
DXF/DWG.
Also fixes linear/aligned text anchoring at the measured midpoint instead
of the actual dimension line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Binary (SAB/ASM) solids no longer collapse every vertex to the origin,
so 3D solids from AutoCAD 2013+ render with real geometry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Loads ACIS/ASM modeler geometry for 3DSOLID/REGION/BODY from the AcDs
data store (AutoCAD 2013+), so solids stored there now render instead of
coming through empty.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Brings in AcDbBlockVisibilityParameter / AcDbBlockRepresentationData
parsing and the dynamic_visibility_for_insert resolver, the foundation
for the upcoming visibility-state UI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Picks up the AC18 section-name fix (cut at first null), so DWGs whose
writer left garbage after the name terminator load their entities
instead of opening with zero. Also rolls in the intervening upstream
ellipse-WCS and reflecting-transform fixes already on main.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hatches inside blocks (xref_Plan-Sade) drew some boundary arcs as their
near-full-circle complement, covering huge areas — in the pattern clip
when rendered through the INSERT, and in both pattern and boundary wires
after EXPLODE.
Root cause (acadrust 64efaf8): the stored sweep of a boundary arc is
always forward and AutoCAD encodes a wrap through 0 by writing the end
angle ABOVE 2π (5.81 → 6.64 for a 0.83 rad arc). transform_hatch
normalized angles into [0, 2π), turning such arcs into their 5.46 rad
complement on ANY insert pass-through — even a pure translation, which
is why the same hatch looked fine outside the block. The fix transforms
only the start angle and carries the stored sweep over unchanged
(invariant under rotation and mirror); RAW vs exploded edge angles are
now bit-identical on the reporting file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hatches inside mirrored INSERTs (x_scale = -1, e.g. the *U blocks in
xref_Plan-Sade) drew some boundary arcs swept the wrong way, covering
huge complementary regions — in the pattern clip before EXPLODE, and in
both pattern and boundary wires after (the exploded entity bakes the
broken angles).
Root cause was in acadrust's transform_hatch, not the renderer: DXF
stores CW (ccw=false) boundary-arc angles MIRRORED (verified against
AutoCAD output by endpoint continuity — mirrored interpretation gives
Δ=0.0 against adjacent edges), and the transform stored geometric angles
after flipping the direction flag. The legacy (TAU-θ) sampling flip in
arc_signed_span is exactly right for file data and stays; its comment now
records the verification so the convention isn't "fixed" away again.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Picks up the fork's merge of upstream acadrust (geodata decode, DXF
roundtrip newline sanitization, DWG alignment) on top of the current
table/multileader style work.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bump acadrust to pick up the new $CTABLESTYLE / $CMLEADERSTYLE header
variables, then on load seed the ribbon's active table style and the
tab's active multileader style from the document header so the ✓ marks
the style the file actually declares (DXF). Set Current writes the header
field as the round-trip source of truth, and the style-manager staging
snapshot now covers it. sync_ribbon_styles honours the active table style
instead of always falling back to the first one.
DWG still leaves these at "Standard" until acadrust parses them from the
binary header.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Picks up 5352477: STYLE/DIMSTYLE annotative now round-trips through DWG
via AcadAnnotative EED (ODA-spec encoding), matching the DXF side.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Picks up bc711f0: annotative is now persisted the standard way
(AcadAnnotative XDATA for STYLE/DIMSTYLE/TABLESTYLE, native group 296 +
DWG bit for MLEADERSTYLE), superseding the earlier non-standard marker.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Creating a new MLeader/Table/MLine style (and a new PlotSettings) pulled
the document's next handle without advancing the counter, so two records
created in one session collided on the same handle and the second
overwrote the first. Use allocate_handle(), which reserves and advances.
Also bumps acadrust to pick up DXF annotative-flag persistence.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Expands the Text Style manager beyond font/width/oblique: big-font and
TrueType font fields, fixed text height, Backward / Upside-down flags, and
an Annotative toggle (applied live). Bumps acadrust for the annotative
field.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each model-space tile now carries its own visual style instead of one
global mode shared by every pane:
- ModelTile gains a `render_mode`; the active tile renders with the live
picker mode, inactive tiles keep their stored style, so changing one
tile no longer changes the others. Split inherits the source style.
- SetRenderMode writes only the active tile; switching the active tile
mirrors its style back into the tab (picker + live render).
- Persisted through the *Active VPORT entries (acadrust now round-trips
the render mode via DXF code 281 / DWG RC 281), restored on open for
both tiled and single-tile model layouts.
- Bump acadrust to pick up the VPORT render-mode field.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pull acadrust b6834f2, which stops the DWG writer from collapsing every
duplicate *Active viewport to a single zoom-extents view. Fixes tiled
model layouts all sharing one camera after save→reopen.
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>