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
No files matched your search
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 166 KiB |
@@ -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 |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|
||||
## 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.
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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" }
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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"]
|
||||
@@ -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: |
|
||||
|
||||
@@ -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"]
|
||||
|
After Width: | Height: | Size: 195 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 207 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 207 KiB |
|
After Width: | Height: | Size: 191 KiB |
|
After Width: | Height: | Size: 202 KiB |
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 136 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 133 KiB |
@@ -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.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 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
|
||||
```
|
||||
@@ -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 |
@@ -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 |
@@ -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.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
## 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/`.
|
||||