Merge remote-tracking branch 'origin/main' into plugin/host-model-api

# Conflicts:
#	crates/ocs_plugin_api/src/process/v4.rs
#	src/app/update/mod.rs
This commit is contained in:
Felix Riestra committed 2026-09-19 23:58:01 +02:00
commit 8648225ac5
309 files changed
+81166 -4157

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

+38
View File
@@ -0,0 +1,38 @@
# Perspective draw-order clipping regression
Switching a flat model view from orthographic to perspective could hide otherwise
visible lines. The camera applied an OpenGL-to-WebGPU depth conversion to a
projection that already used WebGPU's depth range. The shaders then applied a
fixed draw-order offset that could push geometry past the far clipping boundary.
The camera now uses the DirectX/WebGPU projection directly. The shared
`draw_order.wgsl` function keeps the existing offset away from clipping planes
and reduces it near either plane. Its adjustment is monotonic in draw order,
leaves neutral geometric depth unchanged, and does not move clipped vertices
back into range. It is shared by the native and compatibility shader paths.
## GPU reproduction
These are offscreen renders from the application's wire pipelines. All 17 lines
are on the same plane; only their draw-order values differ. The first two lines
(red when visible) were clipped in perspective. Both projections now show all
17 lines. This synthetic drawing contains no user CAD geometry.
| Before | After |
| --- | --- |
| ![Two leftmost lines missing](images/perspective-depth-before.png) | ![All 17 lines visible](images/perspective-depth-after.png) |
## Validation
```sh
cargo test --locked --lib
cargo test --locked --lib scene::pipeline::depth_tests -- --ignored --nocapture
```
The GPU tests exercise storage and packed wire pipelines, near/far clipping,
coplanar draw order in both submission orders, and wire occlusion by a triangle
mesh. Normal tests check camera depth endpoints, picking, and validation of all
14 composed draw-order shaders.
Set `OCS_DEPTH_TEST_IMAGES` to an output directory to save the orthographic and
perspective reproduction images while running the GPU tests.
+41 -11
View File
@@ -101,13 +101,16 @@ affect whether a prebuilt plugin can safely be loaded:
A plugin's `plugin.toml` declares the same values under `[opencad]`. For API v4
and later, `rustc_version` is required and the host refuses to load the plugin
unless it matches exactly. This turns the current silent runner crash into a
one-line diagnosis such as:
unless it matches exactly — omitting it is treated as a mismatch, not skipped.
This turns the current silent runner crash into a one-line diagnosis such as:
```
Plugin built with rustc 1.96.0, host requires 1.98.0 - rebuild required
```
`acadrust_source` is also required for API v4 and later. Missing, empty, or
mismatched metadata makes the package incompatible.
### `PluginManifest`
```rust
@@ -202,11 +205,14 @@ struct ExampleModule;
impl CadModule for ExampleModule {
fn id(&self) -> &'static str { "example" }
fn title(&self) -> &'static str { "Example" }
fn ribbon_groups(&self) -> Vec<RibbonGroup> {
vec![RibbonGroup { title: "Demo", tools: vec![RibbonItem::LargeTool(ToolDef {
// Returns a slice, not an owned Vec — build once into a static and hand
// back a reference, rather than rebuilding (and dropping) it every call.
fn ribbon_groups(&self) -> &[RibbonGroup] {
static GROUPS: std::sync::OnceLock<Vec<RibbonGroup>> = std::sync::OnceLock::new();
GROUPS.get_or_init(|| vec![RibbonGroup { title: "Demo", tools: vec![RibbonItem::LargeTool(ToolDef {
id: "EX_HELLO", label: "Hello", icon: IconKind::Glyph("◆"),
event: ModuleEvent::Command("EX_HELLO".to_string()),
})]}]
})]}]})
}
}
@@ -235,8 +241,9 @@ api_version = 5
ribbon_order = 50
command_prefixes = ["EX_"]
xdata_apps = []
# Filled with the exact compiler output while staging the release asset.
# Both are filled at release-build time; see the CI step below.
rustc_version = "rustc 1.98.0 (hash date)"
acadrust_source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=<short>#<full-40-char-commit>"
```
The full, buildable scaffold is in [`docs/plugin-template/`](plugin-template);
@@ -255,6 +262,10 @@ tool fires `ModuleEvent::Command("EX_FOO")`, which round-trips to
tool request a native file picker; on selection the host dispatches
`"<command> <path>"` back with the path's original case preserved.
A leading `>` or the persistent literal-input toggle preserves both spaces
and case. Automation and `ModuleEvent::PluginFileDialog` already preserve
case without this prefix.
### Interactive (click-to-place) commands
For tools that collect points — placing a structure, drawing a pipe — call
@@ -306,7 +317,18 @@ plugin.toml
```
A GitHub Actions matrix workflow (see the example repo / template) cross-builds
and uploads these on a `v*` tag.
and uploads these on a `v*` tag. It also fills `plugin.toml`'s `rustc_version`
and `acadrust_source` placeholders from the CI run's actual toolchain and
resolved dependency, rather than hand-editing them per release — see
`docs/plugin-template/.github/workflows/release.yml` for the exact `sed`
substitutions, including reading `acadrust_source` out of `Cargo.lock`'s
resolved `acadrust` package entry (`source = "git+...#<hash>"`) since that's
the same value the host embeds at its own build time. The template's CI pins
`rust-toolchain@stable`, which is a floating target — a real release pipeline
should pin the exact toolchain version the host was built with (check
`crates/ocs_plugin_api/src/version_info.rs`'s embedded value, or the host's
own CI config) rather than trusting "whatever's current stable today" to
keep matching it.
---
@@ -383,6 +405,9 @@ dynamic libraries:
- **Enable/disable** — toggles a loaded plugin's ribbon tab + dispatch without
uninstalling.
One linked repository is one marketplace entry. Publish separate plugins from
separate repositories so release and update histories stay unambiguous.
---
## Compatibility & ABI
@@ -391,9 +416,9 @@ Plugins are loaded as `cdylib`s by a plugin-runner child process. The host spawn
this child from its own executable (`--ocs-plugin-runner` mode), so the runner
and host always share the same `ocs_plugin_api` build. The runner checks
`ocs_plugin_api_version` before any plugin code runs. Each plugin runs in its
own OS process, so the host is protected from plugin crashes and memory
corruption. Process isolation removes the need for the host and plugin to share
a Rust toolchain ABI beyond the stable `ocs_plugin_api` contract.
own OS process, so a plugin crash or memory-corruption bug cannot take down the
host process itself. The runner loads the plugin library in-process, however,
so API v4+ requires matching `rustc_version` and `acadrust_source` metadata.
A future hardening step is a `#[repr(C)]` vtable (a true C ABI) so binaries built
by any toolchain interoperate — required before trusting prebuilt binaries from
@@ -414,6 +439,12 @@ Done:
reinstall / uninstall, enable/disable.
- [x] Interactive command round-trip over IPC (prompt, point/enter/object-pick).
- [x] Spawn and per-call IPC timeouts so a stuck runner cannot freeze the host.
- [x] External automation (`--mcp` — local stdio MCP/JSON-RPC server for AI
clients; `--serve` — headless JSON automation server; both drive OCS
from a separate process without the GUI, per `src/cli.rs`/`src/mcp.rs`/
`app::serve`). This closes the "drive OCS headless from a process" goal
of issue #29; this line previously and incorrectly listed it as not yet
started.
Next:
@@ -421,7 +452,6 @@ Next:
- [ ] `#[repr(C)]` vtable / strict handshake for cross-toolchain binaries.
- [ ] Trust: checksums / signatures before spawning plugin processes.
- [ ] Interchange (LandXML / SWMM) and live `on_entity_committed` hooks.
- [ ] External automation API (drive OCS headless from a process) — issue #29.
---
+1 -1
View File
@@ -12,4 +12,4 @@ crate-type = ["cdylib"]
[dependencies]
ocs_plugin_api = { path = "../../crates/ocs_plugin_api", features = ["host"] }
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "568a12c" }
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "8a28c21" }
+4 -2
View File
@@ -64,8 +64,10 @@ From the workspace root:
cargo build -p plugin-template-v2
```
The resulting cdylib plus `plugin.toml` can be installed into the host's
plugins folder (see `docs/plugin-architecture.md`).
The resulting cdylib plus `plugin.toml` (checked in alongside this file) can
be installed into the host's plugins folder (see `docs/plugin-architecture.md`).
Replace `plugin.toml`'s `rustc_version` and `acadrust_source` placeholders in
your packaging step. The release workflow in `docs/plugin-template` shows how.
---
+23
View File
@@ -0,0 +1,23 @@
[plugin]
id = "com.example.plugin-template-v2"
name = "Plugin Template v2"
version = "0.1.0"
description = "Demonstrates read/write object round-trips with the host."
author = "Your Name"
license = "GPL-3.0-only"
repository = "your-account/your-plugin-repo"
[opencad]
api_version = 5
# Replace both placeholders in the packaging step. The v1 template's
# release workflow contains a working example.
rustc_version = "__RUSTC_VERSION__"
acadrust_source = "__ACADRUST_SOURCE__"
ribbon_order = 100
command_prefixes = [
"COUNT_SURVEY_POINTS",
"ADD_SURVEY_POINT",
"MARK_FIRST_SURVEY_POINT",
"COUNT_MARKED_SURVEY_POINTS",
]
xdata_apps = ["SURVEYMARK"]
+13 -1
View File
@@ -29,6 +29,14 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
# NOTE: @stable floats to whatever's current when this job runs, which
# is NOT the same thing as "matches the host's rustc exactly" — the API
# v4+ load check compares plugin.toml's rustc_version to the host's
# build-time compiler byte-for-byte (see plugin-architecture.md's
# "Build-time ABI metadata"). Pin this to the host's actual toolchain
# version (check crates/ocs_plugin_api/src/version_info.rs's embedded
# value, or the host's own CI config) before a real release, or your
# binary may be refused at load with a version-mismatch error.
- uses: dtolnay/rust-toolchain@stable
- run: cargo build --release
- name: Stage release asset
@@ -36,7 +44,11 @@ jobs:
run: |
mkdir -p dist
cp target/release/*.${{ matrix.ext }} "dist/${{ matrix.asset }}"
sed "s|__RUSTC_VERSION__|$(rustc --version)|" plugin.toml > dist/plugin.toml
acadrust_source=$(grep -A3 '^name = "acadrust"' Cargo.lock | grep '^source' | head -1 | sed 's/^source = "//;s/"$//')
sed \
-e "s|__RUSTC_VERSION__|$(rustc --version)|" \
-e "s|__ACADRUST_SOURCE__|$acadrust_source|" \
plugin.toml > dist/plugin.toml
- uses: softprops/action-gh-release@v2
with:
files: |
+2
View File
@@ -10,6 +10,8 @@ repository = "your-account/your-plugin-repo"
[opencad]
api_version = 5
rustc_version = "__RUSTC_VERSION__"
# release.yml fills both placeholders from the compiler and Cargo.lock.
acadrust_source = "__ACADRUST_SOURCE__"
ribbon_order = 60
command_prefixes = ["MP_"]
xdata_apps = ["MYPLUGIN_RECORD"]
Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

+145
View File
@@ -0,0 +1,145 @@
# SpaceMouse
The Windows desktop app supports 3Dconnexion SpaceMouse devices through the
installed [3DxWare driver](https://3dconnexion.com/drivers/). Open CAD Studio
loads NavLib from the Windows system directory at runtime. An SDK installation
is not needed to build or run the app. macOS, Linux, and the web build currently
show an unavailable state; their device adapters are not implemented.
## Navigation
Open **Options → User Preferences → SpaceMouse**, or enter `SPACEMOUSE`.
| Mode | Behavior |
| --- | --- |
| Follow context | Pan and zoom on a paper sheet; pan, zoom, and orbit in model views. |
| Pan only | Push or tilt the puck to pan in the current screen plane. Twist and vertical pressure are ignored. Scale, depth, and orientation stay fixed, including in perspective. |
| Pan and zoom | Uses the same push/tilt panning as Pan only. Lift or press the puck to zoom about the view center. Twist is ignored and orientation stays fixed. |
| 3D navigation | Pan, zoom, and orbit in model views. Paper sheets keep orientation fixed. |
For a flat drawing, choose **Pan only** to use the puck as a panning joystick.
Push or tilt right/left to move the drawing right/left; push or tilt away/toward
you to move it up/down. Pushing and tilting together does not double the speed.
Enable **Reverse pan direction** if you prefer moving the viewpoint in the same
direction as the puck. **Reverse pan direction** and **Pan speed** apply to both
flat-navigation modes, including paper sheets in Follow context. A small dead zone
filters incidental forces, and releasing the puck stops movement.
The app does not infer that a model-space file is 2D from its extension or from
the current camera angle. These preferences belong to the user and persist
across drawings and sessions.
The status bar shows the effective mode after a device connects, and keeps the
control available after disconnection. An explicitly selected navigation mode
also makes it visible. Its menu provides mode selection, Pause/Resume, preferences,
and a shortcut to 3Dconnexion settings. Status-bar customization can hide it.
Only the focused drawing receives input. Dialogs suspend navigation. The
view metadata remains available to the driver while its settings UI has focus.
Native 3DxWare chooses the foreground application; the adapter marks its view
active without forcing the NLServer-specific keyboard-focus property. Entered
paper-space viewports use their own cameras; their display locks are respected.
The active model tile receives navigation in a split view. Moving the puck
preserves an unfinished drawing command, refreshes its preview at a stationary
pointer, and keeps an active selection-box anchor attached to the drawing.
![SpaceMouse menu with Pan only selected](images/spacemouse-menu.png)
![SpaceMouse settings in User Preferences](images/spacemouse-preferences.png)
## Buttons and radial menus
Use **Open 3Dconnexion settings…** to assign buttons and radial-menu entries.
The app exports registered CAD command IDs and reuses ribbon labels and icons
where available. Speed, axis inversion, dominant-axis filtering, and radial-menu
layout for SDK navigation remain in the driver settings. Pan only and Pan and zoom
share the app's push/tilt mapping and Pan speed control; driver axis remappings do
not apply to these flat-navigation modes. Button assignments continue to work in
all modes.
For orbiting with a fixed up direction, enable **Lock Horizon** in the driver's
Advanced Settings for Open CAD Studio. The adapter reads `settings.LockHorizon`
through NavLib at connection/focus and after `settings.changed` notifications.
When enabled, accepted orbit matrices remove roll relative to world Z. Pitch
stops just short of the top/bottom pole, retaining heading instead of flipping
the view. In the driver's Object mode the eye receives the same pitch correction
around the SDK's orbit pivot, so the object stays in place; Camera/Helicopter
modes retain their proposed eye position. Disabling Lock Horizon restores
unrestricted driver rotation.
The app does not duplicate this control, the driver's rotation lock, or
its object/camera navigation modes.
Useful actions include Undo, Redo, Cancel, Fit view, Top view, and these commands:
| Command | Action |
| --- | --- |
| `SPACEMOUSE` | Open SpaceMouse preferences |
| `SPACEMOUSEPAUSE` | Pause/resume puck navigation for this session |
| `SPACEMOUSEAUTO` | Follow context |
| `SPACEMOUSEPAN` | Pan only |
| `SPACEMOUSEPANZOOM` | Pan and zoom |
| `SPACEMOUSE3D` | 3D navigation |
Fit view (`SPACEMOUSEFIT`) and Top view (`SPACEMOUSETOP`) are also available as
button/keyboard actions. Button actions share the keyboard dispatcher: for
example, Undo removes the last entered point of an unfinished polyline. A
Pause/Resume button remains usable while puck navigation is paused.
## Implementation
- `src/input/spacemouse/`: portable preferences, camera snapshots, and a bounded
thread-safe bridge. `navlib.rs` contains the Windows ABI and runtime loading.
`raw_input.rs` receives SpaceMouse multi-axis HID reports for planar navigation;
`pan.rs` maps pushes and tilts to screen velocity and vertical pressure to zoom.
The planar path suppresses SDK camera updates so one gesture cannot be applied
twice. Only 3Dconnexion/legacy Logitech multi-axis
controllers are accepted. Other HID devices and report types are ignored.
`worker.rs` waits for app work and dispatches Windows messages on the SDK's
owning thread, including settings changes while the drawing is unfocused.
- `src/app/navigation.rs`: focus/context policy, cached selection bounds, camera
application, pivot feedback, and the action catalog.
- `src/scene/mspace.rs`: routing to the active model or floating-viewport camera.
- `src/ui/window/options/spacemouse.rs` and `src/ui/statusbar/spacemouse.rs`:
controls within the existing Options and status-menu systems.
- `src/app/settings.rs`: preferences; `src/app/shortcuts.rs`: shared actions.
Driver callbacks operate on snapshots and never access the scene or GUI. A
navigation transaction is coalesced into one pending camera update. Target
stamps discard late motion after a tab, layout, viewport, tile, or camera change.
Acknowledging an applied frame retains newer callbacks from the same gesture;
an external camera change resets the SDK navigation model. Raw pan state is
cleared on context/focus changes, pause, disconnection, and stale reports.
Button requests retain their context across camera revisions. Losing focus or
changing drawing context discards queued input. Connection shutdown closes
NavLib before releasing callback, command, image, or library storage.
The subscription joins its worker at shutdown so NavLib can finish persisting
the driver's per-application preferences before the process exits.
For driver-setting diagnostics, set
`RUST_LOG=OpenCADStudio::input::spacemouse=debug` before launching. The trace
records settings-change notifications and the Lock Horizon value read from
NavLib; it does not change the user's driver settings.
Iced supplies monotonic frame timestamps while navigation is active. Idle
drawings do not request continuous rendering. Driver-originated camera updates
are not echoed back as external-camera notifications, which would reset the
driver's navigation model during a held input.
The optional SDK `view.target` property returns no-data: the app's camera center
does not represent the SDK's separate target-camera navigation model.
The adapter uses the NavLib C ABI described in the 3DxWare SDK headers
(`navlib.h`, `navlib_types.h`, and `siappcmd_types.h`). No vendor headers or
runtime binaries are included. SpaceMouse and 3Dconnexion are trademarks of
3Dconnexion.
## Validation
```sh
cargo test --locked --lib spacemouse
```
An opt-in smoke test checks the installed driver, command/icon export, device
presence, and reading its Lock Horizon setting:
```sh
cargo test --locked --lib installed_driver_connects_and_reports_device_presence -- --ignored --nocapture
```
+19
View File
@@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="350" viewBox="0 0 900 350">
<style>text{font-family:Arial,sans-serif;fill:#263238} .title{font-size:22px;font-weight:600} .label{font-size:16px} .small{font-size:13px;fill:#54616a} .model{stroke:#1479b8;stroke-width:4;fill:none} .paper{stroke:#a94610;stroke-width:2;stroke-dasharray:9 5} .dim{stroke:#263238;stroke-width:1.5;fill:none}</style>
<defs><marker id="arrow" viewBox="0 0 8 8" refX="4" refY="4" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M8 0L0 4L8 8" fill="#263238"/></marker></defs>
<rect width="900" height="350" rx="10" fill="white"/>
<text class="title" x="30" y="38">The dimension follows its paper-space centerline</text>
<rect x="30" y="65" width="400" height="220" fill="#f5f8fa" stroke="#c3ced5"/>
<rect x="470" y="65" width="400" height="220" fill="#f5f8fa" stroke="#c3ced5"/>
<text class="label" x="48" y="93">Before</text><text class="label" x="488" y="93">Centerline moved 1 paper unit right</text>
<path class="model" d="M85 160H385M525 160H825"/>
<circle cx="385" cy="160" r="5" fill="#1479b8"/><circle cx="825" cy="160" r="5" fill="#1479b8"/>
<path class="paper" d="M235 113V257M705 113V257"/>
<path class="dim" d="M385 171V244M825 171V244"/>
<path class="dim" d="M235 232H385" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
<path class="dim" d="M705 232H825" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
<rect x="291" y="217" width="38" height="28" fill="#f5f8fa"/><rect x="746" y="217" width="38" height="28" fill="#f5f8fa"/>
<text class="label" x="310" y="237" text-anchor="middle">50</text><text class="label" x="765" y="237" text-anchor="middle">40</text>
<text class="label" x="30" y="316">The model endpoint stays fixed. The dimension updates from 50 to 40.</text>
<text class="small" x="30" y="340">Association schematic; viewport 1:10, dimension style unit factor = 1, DIMASSOC = 2.</text>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+22
View File
@@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="350" viewBox="0 0 900 350">
<style>text{font-family:Arial,sans-serif;fill:#263238} .title{font-size:22px;font-weight:600} .label{font-size:16px} .small{font-size:13px;fill:#54616a} .model{stroke:#1479b8;stroke-width:4;fill:none} .paper{stroke:#a94610;stroke-width:2;stroke-dasharray:9 5} .dim{stroke:#263238;stroke-width:1.5;fill:none}</style>
<defs><marker id="arrow" viewBox="0 0 8 8" refX="4" refY="4" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M8 0L0 4L8 8" fill="#263238"/></marker></defs>
<rect width="900" height="350" rx="10" fill="white"/>
<text class="title" x="30" y="38">Dimension from a paper centerline to a model endpoint</text>
<rect x="30" y="65" width="430" height="245" fill="#f5f8fa" stroke="#c3ced5"/>
<text class="small" x="45" y="89">Viewport 1:10</text>
<path class="model" d="M100 160H400"/>
<circle cx="400" cy="160" r="5" fill="#1479b8"/>
<path class="paper" d="M250 110V260"/>
<text class="small" x="100" y="144">Model geometry</text>
<path class="dim" d="M400 171V244"/>
<path class="dim" d="M250 232H400" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
<rect x="306" y="217" width="38" height="28" fill="#f5f8fa"/>
<text class="label" x="325" y="237" text-anchor="middle">50</text>
<text class="small" x="45" y="290">Brown dashed line: paper-space centerline</text>
<text class="label" x="500" y="116">5 paper units between the picked points</text>
<text class="label" x="500" y="155">5 × 10 = 50 model units</text>
<text class="label" x="500" y="206">The viewport supplies the length scale.</text>
<text class="label" x="500" y="240">Either pick order gives the same value.</text>
<text class="small" x="30" y="336">Measurement schematic; dimension style unit factor = 1.</text>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+74
View File
@@ -0,0 +1,74 @@
# Dimensions through layout viewports
In paper space, snap to model geometry displayed through a planar, orthographic
viewport. `DIMLINEAR`, `DIMALIGNED`, `DIMANGULAR`, `DIMRADIUS`, and `DIMDIAMETER`
also support their usual object-pick modes through these viewports.
## Measurement
- Model geometry supplies the viewport scale. A 100-unit model length displayed
at 1:10 measures 100, although it occupies 10 paper units.
- A paper-space centerline or free sheet point can be combined with model
geometry through one viewport, in either pick order. The paper-space span is
measured using that viewport's scale.
- Sheet-only dimensions measure paper units, even inside a viewport rectangle.
- Model picks from a second viewport are rejected without advancing the command.
Dimension-line and text placement do not change the measurement scale.
- The current dimension style's unit conversion is preserved. Angular dimensions
do not use length compensation. `DIMASSOC=0` explodes the compensated dimension.
Snapping and object picking respect visibility, clipping, and viewport draw order.
Paper geometry has priority in object picks. Ordinary selection uses the active
drawing space. Object picks reuse the resident interaction index and resolve
nested block transforms.
![Measurement schematic: a five-unit sheet span measures 50 through a 1:10 viewport.](viewport-dimension-creation.svg)
## Saved scale
Length dimensions store paper-space definition points and a negative `DIMLFAC`
override: user unit factor multiplied by `1 / viewport scale`.
`OCS_VIEWPORT_MEASUREMENT` XData stores version `1` (integer16), the positive user
factor (real), and viewport compensation (real). Keeping the factors separate
allows compensation to change without losing the user's unit conversion.
Both the override and XData survive DXF/DWG saves and undo/redo.
## Associations
With `DIMASSOC=2`, acquired features retain their viewport, block-instance path,
source entity, and snap feature. Intersections retain both source chains.
Source edits and viewport changes update the dimension and its compensation,
preserving style overrides and user-positioned text on the sheet.
A sheet centerline stays on the sheet; a model endpoint follows its viewport.
Moving the viewport therefore changes the distance between them. Undo/redo and
DXF/DWG saves preserve the references.
Properties shows association status. If a required reference is broken or cannot
be resolved, the entire dimension keeps its last valid geometry and reference
records. A successful refresh regenerates its displayed and exported geometry.
![Association schematic: moving the sheet centerline changes the dimension from 50 to 40.](viewport-dimension-association.svg)
## Limits
Unsupported cases preserve saved dimensions:
- Perspective or oblique views; missing or malformed reference chains.
- Array instances without an addressable instance index, nesting beyond eight
levels, and radial measurements of nonuniformly scaled circles or arcs.
- Intersections without two identifiable source chains inside a batched block.
- Ambiguous imported endpoint, midpoint, or quadrant markers. A stored point can
validate a feature; it cannot establish an otherwise unknown identity.
- Tangency without a verified solution, or two perpendicular/tangent references
requiring a coupled solve.
- Imported viewport scaling without OCS scale metadata or
`ACAD_DIMASSOC_CALC_DIMLFAC` identifying the saved compensation.
## Tests
Run `cargo test --locked --lib viewport_dimension`. Tests cover the five commands,
mixed sheet/model input, styles, clipping, object picks, source/viewport edits,
snap features, nested paths, intersections, preserved overrides, undo/redo, and
DXF/DWG persistence. The synthetic association fixture is in
`tests/fixtures/dim_assoc/`.