Add named parameters, UI scale setting, OPTIONS/OP dispatch, and help manual

Named parameters (docs/named_parameters_design.md, stages 3-4):
- ParameterTable (src/scene/named_parameters.rs): a document-wide table of
  named formulas, each resolved through a shared expression evaluator with
  cross-parameter references, cycle detection, and division-by-zero/parse
  error reporting. SketchConstraint::driving_param becomes DrivingValue
  (Literal | Named) instead of a bare f64, resolved at solve time by
  sketch_solve::build_constraint via Scene::named_parameters.
- Persistence (src/scene/named_parameters_persist.rs): same lazy XRecord
  model sketch_persist.rs already established, synced around open/save.
  Bumps the sketch-constraint XRecord's own format version (1 -> 2) since
  DrivingValue's shape doesn't line up with a version-1 blob's bytes.
- PARAMETERS command and its editor (src/ui/window/named_parameters.rs):
  add/remove/edit rows, live validation, duplicate-name detection: Apply
  defines the table and re-solves every constraint scope with a
  named-parameter-driven constraint (command_driver.rs). Distance/Angle
  constraint prompts (modules/draw/constrain/value.rs) now accept either a
  literal or an existing parameter's name, mirroring how the constraint's
  own value prompt already worked.
- Undo scope-down carried over from this project's own established pattern
  (constraint-system stage 9): the geometry an Apply moves is one undo
  step, but the parameter table edit itself has no undo entry yet — a
  documented follow-up, not a silent gap.

UI scale setting (src/app/settings.rs, app/mod.rs, ui/window/options.rs):
  a whole-interface scale percentage (50-200%), wired directly into iced's
  own per-window scale_factor hook rather than per-panel font-size
  plumbing, so it applies immediately to every panel, dialog, and the
  ribbon at once. Persisted alongside the existing lineweight-display-scale
  setting.

OPTIONS/OP command dispatch (app/commands/view.rs, app/commands/mod.rs):
  OPTIONS was registered for autocomplete but had no dispatch arm — the
  Start-tab welcome screen's own button was the only way to reach it.
  Found live-testing the UI-scale setting, unrelated to that feature
  itself. "OP" is AutoCAD's own short alias.

help/: a new self-contained static user-help manual (manual.md as the
authored source, tools/build_help.py splits it into the committed static
HTML pages plus search index) — not yet wired into the application.

64 new/updated tests across the named-parameters and constraint-prompt
changes.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
Felix RiestraandClaude Sonnet 5 committed 2026-09-09 13:50:08 +02:00
1 parent e2d39b049d
commit 7cddf31bcb
47 files changed
+4136 -52

No files matched your search

+344 -3
View File
@@ -131,6 +131,347 @@ verification where the stage has new UI surface.
## 6. Progress
Nothing built yet. Update this section (not a separate file) after each
stage, matching the reporting pattern used throughout
`docs/parametric_system_design.md`'s own "Progress" section.
**Stage 1 done and tested.** `src/scene/named_parameters.rs`, new module
(registered `pub mod named_parameters;` in `src/scene/mod.rs`, alongside
`sketch_constraints`/`sketch_persist`/`sketch_solve`). Pure data + parser,
no document/UI integration yet, per this stage's own scope.
- **`Expr`**: a small AST (`Number`, `Ref`, `Neg`, `Add`/`Sub`/`Mul`/`Div`/
`Pow`, `Call`) plus a hand-rolled tokenizer/recursive-descent parser
(`parse`) — ordinary precedence (`+`/`-` < `*`/`/` < unary `-` < `^`,
right-associative `^`), parens, and scientific-notation numeric literals
(`1.5e-2`). **Deviation from this doc's own §2 suggestion** to check
`evalexpr`/`meval` first: hand-rolled anyway, because cycle detection
needs the set of names an expression references *before* it's evaluated
(`Expr::refs`), and owning the parse tree gives that for free — treating
evaluation as a black box the way a generic crate is normally used would
just mean re-deriving the same dependency information some other way. No
new external dependency added.
- **§5 open question 2 (expression language surface)**: resolved for v1 as
arithmetic + a small, deliberately conservative builtin function set
(`sqrt`, `abs`, `pow`, `min`, `max` — `call_builtin`), chosen as low-risk,
commonly-needed CAD primitives rather than a considered product decision
on the full surface. The grammar's `Call(String, Vec<Expr>)` node means
adding more later needs no parser rewrite, matching this doc's own
call-out. Builtin names are reserved (`is_reserved_name`) so a parameter
can never collide with one — resolves part of open question 5 (naming
collisions) alongside `is_valid_name`'s identifier rule (leading
letter/underscore, then alphanumeric/underscore).
- **Cycle detection happens at `ParameterTable::set` time, not at
resolve/evaluate time** — the design doc's "must be rejected" read as
real-time rejection (matching a spreadsheet's usual UX), not a
resolve-time error. `set` walks the dependency graph from the candidate
formula's own references, through every already-stored formula, looking
for a path back to the name being defined; a reference to a name that
doesn't exist *yet* just terminates that branch rather than counting as a
cycle (order of definition doesn't matter, same as a spreadsheet formula
referencing an empty cell) — it only becomes
`ParamError::UndefinedReference` if `resolve`/`resolve_all` actually needs
that value later and it's still missing. A `resolve`-time cycle guard
exists too (`resolve_inner`'s `stack` check) but is defense-in-depth only
— unreachable through the public API, since `set` already refuses to
create one; the code comment is explicit that this is a safety net (e.g.
against a foreign/hand-edited file deserialized straight into a table
without going through `set`), not a path the design expects to hit.
- **`resolve_all` isolates a failure to just the parameters it actually
affects**, not the whole table — matching this codebase's established
don't-let-one-bad-item-take-down-everything-else pattern from the
constraint system's own dangling-reference handling.
- `ParameterTable::set` is a single upsert (define-or-redefine), not two
separate "must not exist" / "must already exist" entry points — a later UI
stage that wants add-vs-rename semantics can check `contains` itself
first; not a decision this data-layer stage needed to make.
- 17 unit tests: precedence/associativity, scientific notation, malformed
formulas, builtin arity/unknown-function errors, division by zero, name
validation, literal and chained-reference resolution, undefined-reference
reporting, direct and indirect (closes-later) cycle rejection, redefining
a parameter to *break* a would-be cycle, `resolve_all` isolation,
remove-then-resolve leaving a dependent gracefully undefined (not
panicking), upsert semantics, insertion-order stability across
redefinitions, and a `bincode` round-trip (both `Expr` and `Parameter`
derive `Serialize`/`Deserialize` already, ready for stage 2's XRecord
persistence — not exercised through the real save/load path yet, just
in-memory `bincode::serialize`/`deserialize`, the same distinction stage 4
of the sketch-constraint project drew between its own in-memory-agreement
test and its real-save-path tests).
Full regression check: `cargo test --workspace`, 686 passed / 1 failed (the
same pre-existing, unrelated Turkish-glyph-fallback font test noted
throughout `docs/parametric_system_design.md`) / 13 ignored. `cargo clippy
--lib` shows no new warnings in `named_parameters.rs` (pre-existing warnings
elsewhere untouched).
**Still open after stage 1** (per §5): open question 1 (solid-history
scope), 3 (undo granularity), and 4 (per-document vs per-block-record table
scope) — none needed answering yet since nothing here touches the document
model or undo. Open question 2 and part of 5 got a working v1 answer above,
not a final product ruling.
**Stage 2 done and tested.** `src/scene/named_parameters_persist.rs`, new
module (registered `mod named_parameters_persist;` in `src/scene/mod.rs`),
following `sketch_persist.rs`'s exact precedent: the "lazy" model, a
version-prefixed `bincode` blob in one `Chunk`-typed `XRecordEntry`,
materialized right before save and read back right after load. Also went
ahead with the "wire it into open/save" half of stage 2 (not just the
spike) since it's the same four call sites `sketch_persist` already touches
and there was no reason to leave it half-done:
`OpenCADStudio::prepare_native_save` and the wasm save path
(`src/app/update/file.rs`), `OpenCADStudio::on_file_opened`'s native/web
open path (`src/app/update/file.rs:1393`), and the automation `"open"`/
`"new"` ops (`src/app/automation.rs`) — `named_parameters` lives on `Scene`
(`Scene::named_parameters: named_parameters::ParameterTable`, alongside
`sketch_constraints`, for the same co-location-with-`document` reason
stage 3 of the constraint-solver project gave), with `Scene::
named_parameters`/`named_parameters_mut` accessors.
- **A real round-trip gap found and worked around, not patched**: the
obvious owner handle for a document-wide (not per-scope) XRecord is
`document.header.named_objects_dict_handle` — the vendored `cadcodec`
source's own `extension_dictionary_handle` explicitly resolves an
extension dictionary for a `Dictionary`-type owner, and that crate's own
test suite (`tests/issue51.rs`) uses exactly that handle as an XRecord
owner. In practice this round-tripped through DWG but **silently dropped
the XRecord through DXF** — caught by this stage's own real-bytes save/
load tests, not assumed to work from reading the source alone (the same
discipline `sketch_persist`'s stage 4 applied). Root cause, traced into
the vendored source: `CadDocument::ensure_xrecord` only writes the
*owner's own* `xdictionary_handle` field when `get_entity_mut(owner)`
succeeds (i.e. `owner` is a real "entity"); a `Dictionary` owner like the
root NOD isn't one, so `ensure_xrecord` falls back to a runtime-only side
map (`xdic_by_handle`) instead. The DWG writer consults that side map;
grepping `io/dxf/writer/` for it turns up nothing — the DXF writer only
ever reads the owner's own `xdictionary_handle` field, never the side map,
so a Dictionary-owned extension dictionary is simply invisible to a DXF
save. This is a gap in the vendored crate for this specific owner-type
combination. Per this project's own stated preference (§1.4 of the sibling
design doc: "requires zero changes to the external `acadrust`/`cadkernel`
crates") the fix taken was to pick a different owner rather than patch
the dependency: **`document.header.model_space_block_handle`** — a real
`BlockRecord` entity, the exact anchor `sketch_persist`'s own
`SketchScope::ModelSpace` case already proved clean through both formats.
Not a perfect semantic fit (a parameter table has nothing to do with model
space specifically) but it's guaranteed to exist in every `CadDocument`
and is a real entity, which is the property that actually matters. Worth
revisiting if a later need for a true document-root-anchored XRecord comes
up — the fix would be in `cadcodec` itself (teach `ensure_xrecord` to also
set a `Dictionary`/`Layout`/`XRecord`/`PlotSettings`/`VisualStyle`/
`Material`/`ProxyObject` owner's own `xdictionary_handle` field, not just
entities'), not something to route around a second time.
- 5 new tests (`named_parameters_persist.rs`): in-memory materialize+reload
agreement, real `save_to_bytes`/`load_bytes` round trips for both DXF and
DWG (the two that caught the gap above), an empty table not persisting,
and format-version rejection — same shape as `sketch_persist`'s own test
suite.
Full regression check: `cargo test --workspace`, 691 passed / 1 failed (same
pre-existing, unrelated font test) / 13 ignored. `cargo clippy --lib` shows
no new warnings in either `named_parameters.rs` or
`named_parameters_persist.rs`.
**Stage 3 done and tested.** `SketchConstraint::driving_param` is now
`Option<named_parameters::DrivingValue>` (`sketch_constraints.rs`) instead
of `Option<f64>` — exactly this doc's own §2 suggested shape:
```rust
pub enum DrivingValue {
Literal(f64),
Named(String),
}
```
with `DrivingValue::resolve(&self, table: &ParameterTable) -> Result<f64,
ParamError>` (`named_parameters.rs`) doing the lookup — a literal resolves
to itself, a named reference resolves through the table.
- **Solve-side wiring**: `sketch_solve.rs`'s `build_constraint`/`solve_scope`
now take a `&ParameterTable` (threaded from `Scene::named_parameters` at
both call sites, `refresh_sketch_constraints` and
`solve_sketch_constraints_preview`) and resolve `driving_param` at the
three existing call sites (Distance/Angle/Radius) via `c.driving_param.
as_ref()?.resolve(params).ok()?` — a resolve failure (an undefined
reference, a division by zero; a cycle should be unreachable since `set`
already refuses to create one) is treated exactly like every other
unbuildable-constraint case `build_constraint`'s own doc comment already
documents: the constraint is skipped for this solve, not an error.
- **Judgment call, not dictated by this doc**: how a resolve failure should
surface wasn't an open question this doc raised explicitly, so the
existing precedent decided it — matching a dangling-reference / unbuildable
constraint rather than inventing a new "parameter error" state. A richer
surfacing (e.g. a distinct glyph color, a status-bar warning naming the
broken reference) is possible future work once stage 4's UI exists to
make it discoverable, not something this data/solver-layer stage needed
to add speculatively.
- **`CmdResult::AddSketchConstraint` intentionally left as `Option<f64>`**
(`src/command.rs`) rather than also switching it to `DrivingValue` — no UI
can author a `Named` reference yet (that's stage 4's picker), so every
existing call site (`DistanceConstraintCommand`/`AngleConstraintCommand`
in `src/modules/draw/constrain/value.rs`, the Horizontal/Vertical/etc.
dispatch in `src/app/commands/draw.rs`) needed no changes at all; the one
conversion point is `command_driver.rs`'s `CmdResult::AddSketchConstraint`
handler, which now wraps the literal into `DrivingValue::Literal` right
before calling `SketchConstraintSet::add`. Keeps this stage's diff to the
data/solver layer plus one narrow seam, not a ripple through every typed-
value UI command.
- **`glyph_label`** (`sketch_constraints.rs`) now matches on `&DrivingValue`
instead of a bare `f64`: a literal renders exactly as before, a named
reference renders its name (e.g. "↔ hole_dia") rather than a resolved
number — showing the resolved value too would need threading
`ParameterTable` into the glyph-render call site
(`src/ui/overlay.rs`), which is UI-layer work for stage 4, not this one;
this arm exists only so the match stays exhaustive ahead of that UI (no
command can produce a `Named` driving_param yet, so it isn't reachable
today).
- **`sketch_persist::FORMAT_VERSION` bumped 1 → 2**: `SketchConstraint`'s
serialized shape changed (a bare `f64` became an enum with its own
discriminant), so a version-1 blob's bytes no longer line up with the
current struct — exactly the "detected and gracefully skipped" case that
version byte exists for (§5.2's own call-out: no migration path exists,
bumping the version is the correct response, not a substitute for one). A
previously-saved file's sketch constraints will not reload after this
change; acceptable for personal-use, pre-release local work, not something
a real migration was owed here.
- 2 new tests (`tests/sketch_constraints_solve.rs`): a `Distance`
constraint driven by a named reference solves correctly and re-solves
when the parameter's own value is redefined; a `Distance` constraint
referencing an undefined name is skipped without panicking, leaving the
triggering edit untouched (the same contract the existing "unrelated edit
doesn't trigger a resolve" test already established for dangling
refs). Every existing `driving_param`-touching test (in
`sketch_constraints.rs`, `sketch_persist.rs`,
`tests/sketch_constraints_xrecord_roundtrip.rs`,
`tests/sketch_constraints_solve.rs`) updated to construct
`DrivingValue::Literal(...)` instead of a bare float; all still pass.
- **Testing-methodology note, not a code defect**: discovered while
re-verifying this stage that plain `cargo test --workspace` stops after
the first *crate's* test failures and silently skips the remaining
integration-test binaries in the workspace — the pre-existing, unrelated
font-fallback failure was silently truncating every regression check this
whole project has run with a bare `cargo test --workspace`. Re-ran with
`--no-fail-fast` for this stage's check and confirmed every binary (not
just the lib crate) passes except that one known-unrelated test. Worth
using `--no-fail-fast` for any future full-workspace check in this repo.
Full regression check: `cargo test --workspace --no-fail-fast`, every test
binary passing except the same pre-existing, unrelated font test noted
throughout this document and its sibling. `cargo clippy --lib --tests`
shows no new warnings on any line this stage touched (the two warnings that
do appear in `sketch_solve.rs`/`scene/mod.rs` predate this stage and are on
code this stage didn't modify).
**Stage 4 done and tested (not live-verified in the running app — see the
note below).**
- **The parameters panel** (`src/ui/window/named_parameters.rs`, new): an
in-canvas modal opened by a new `PARAMETERS` command (AutoCAD's own name
for the equivalent feature — `src/app/commands/view.rs`, dispatched
alongside `ALIASEDIT`; registered in the autocomplete/MCP command list
from the start, unlike the six pre-existing constraint commands
`docs/backlog.md` flags as missing that registration). Built by mirroring
`alias_editor.rs`'s buffered-rows pattern as closely as possible rather
than inventing new UI conventions: rows are `(name, formula)`, edited in a
working buffer (`OpenCADStudio::named_parameter_editor_rows`,
`ModalKind::NamedParameters`) and only committed to `Scene::
named_parameters` on Apply — a better fit here than for aliases, even:
`ParameterTable::set` validates (and can reject) a formula immediately, so
writing through to the live table on every keystroke would fight a user
mid-type.
- **Live preview column, beyond what the alias editor has**: each row also
shows its resolved value or its error, recomputed from a scratch
`ParameterTable` built from the *whole buffer's current text* on every
render (`preview`, private to the UI module, unit-tested directly) — not
cached, matching the "cheap enough to rebuild from scratch" philosophy
`ParameterTable`/`sketch_solve.rs` already use throughout. This means a
cross-row problem (a forward reference, a cycle that would close between
two draft rows) shows up immediately, before Apply, not just after a
failed commit.
- **A real gap found while building the preview, not by design**: two rows
typed with the same name would otherwise silently collapse — `set`
upserts by name, so the buffer's later row would just overwrite the
earlier one with no indication anything was discarded. Added
`duplicate_name_rows` (checked first, before any `set` call, by both the
preview and the real Apply) to flag every row sharing a duplicated name as
its own error instead of silently picking one arbitrarily.
- **The other half of stage 4 — "a way to pick a named parameter instead of
a literal"**: resolved as letting the *same* value prompt accept either,
rather than building a separate picker UI. `DistanceConstraintCommand`/
`AngleConstraintCommand` (`src/modules/draw/constrain/value.rs`) now take
a one-time snapshot of the active tab's parameter names at construction
(`known_param_names` — the same `default_value`-is-a-one-time-snapshot
precedent these commands already relied on, since `on_text_input` has no
document access); `parse_driving_value` tries a numeric literal first,
falls back to an exact match against that snapshot, and returns neither if
the typed token is unrecognized (the prompt's existing "invalid input"
behavior, unchanged). This mirrors how AutoCAD's own dimensional-
constraint prompts already accept an expression in place of a bare value,
and needed no new interaction design. `CmdResult::AddSketchConstraint`'s
`driving_param` (left as a bare `Option<f64>` in stage 3, deliberately,
exactly so this stage could revisit it) is now `Option<DrivingValue>`.
- **Apply re-solves what it affects**: because editing a named parameter is
the entire point of the feature ("editing the named value ripples to
every constraint that references it," §1), Apply doesn't just rebuild the
table — it also gathers every entity handle touched by an enabled
constraint whose `driving_param` is `DrivingValue::Named` (across every
scope; the whole table changed, not one isolated value, so there's no
cheaper "which handles actually changed" test worth doing) and runs them
through the same `begin_undo`/`bump_entities`/`commit_undo_delta` bracket
`resolve_one_sketch_conflict` already established
(`OpenCADStudio::apply_named_parameter_editor_rows`, `command_driver.rs`).
- **Judgment call on undo granularity (§5 open question 3), not fully
resolved**: the geometry Apply moves is one undo step (bracketed as
above), matching how every other solve-triggering action in this codebase
behaves. The parameter *table* edit itself has no undo entry of its own
yet — that would need a new `HistorySnapshot` variant mirroring
`SketchConstraints` (the sibling project's own stage 9), which this pass
didn't build. Documented, not silently dropped, exactly like stage 9's own
"two presses, not one" scope-down: undoing after an Apply undoes the
geometry it moved, not the parameter values.
- 15 new tests: 3 for `parse_driving_value` (literal-first, name match,
unrecognized token), 6 for the panel's `preview` (cross-row reference,
forward reference regardless of buffer order, a cycle flagged on the row
that closes it, a blank row left unflagged, a malformed formula, the
duplicate-name fix), and 6 integration tests in `command_driver.rs`
(applying rows defines the parameter *and* re-solves a constraint that
already referenced it by name — including re-applying after redefining the
parameter; a malformed row is reported as an error and doesn't block a
good row alongside it; the duplicate-name fix exercised end to end through
`apply_named_parameter_editor_rows`, not just the UI-layer preview).
- **Testing-methodology note carried over from stage 3**: verified with
`cargo test --workspace --no-fail-fast` throughout this stage too, for the
same reason noted there.
Full regression check: `cargo test --workspace --no-fail-fast`, 703 tests
passing, only the same pre-existing, unrelated font-fallback failure noted
throughout this document and its sibling. `cargo clippy --lib --tests` shows
no new warnings on any line this stage touched.
**Not live-verified in the running app this pass** — a real gap, not a
formality skipped by habit: this is genuinely new UI surface (a whole modal
panel, plus a new branch in an existing text-input prompt), and static
review/unit tests can't catch every real-app integration issue (layout,
widget-API misuse that only manifests at runtime, focus/keyboard handling).
Per `docs/backlog.md`'s own live-testing notes, exercising it for real needs
a release build swapped into a scratch copy of the installed app bundle
(`open_application`/`request_access` always resolve to the `/Applications`
copy, and the debug binary doesn't reliably present a controllable window in
this environment) — a real time investment this pass didn't spend, flagged
explicitly rather than silently assumed to work. First things to check in a
future live-testing pass: the panel actually opens via `PARAMETERS`, the
value column updates live while typing, the duplicate/cycle/malformed-
formula error text renders legibly, Apply visibly moves geometry for a
constraint driven by a named parameter, and typing a parameter's name at a
`DCONSTRAINT`/`ACONSTRAINT` prompt is actually accepted.
## Follow-up work not yet done
- **Solid-history `PROP_*` values** (§2's own flagged scope decision, open
question 1) — named parameters currently only reach `SketchConstraint::
driving_param` (Distance/Angle/Radius), not `solid_history.rs`'s box
length/cylinder radius/etc. Not started; a decision to make, not assumed.
- **Constraint-table-edit undo** (open question 3, only partially resolved —
see above).
- **Reserved-word/name-collision UX polish**: `ParameterTable::set` already
rejects an invalid or builtin-colliding name (stage 1); the panel surfaces
that rejection as the row's error text via `preview`/Apply, but there's no
live per-keystroke nudge beyond that (e.g. graying out `Apply` while any
row has an error) — a small, optional polish, not a functional gap.
- **Live UI verification**, per the note above.
+55
View File
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="OpenCAD Studio user manual — 3D Modeling and Visualization">
<title>3D Modeling and Visualization — OpenCAD Studio Help</title>
<link rel="stylesheet" href="assets/css/core.css">
<link rel="stylesheet" href="assets/css/opencad.css">
<script src="search-index.js" defer></script>
<script src="assets/js/help.js" defer></script>
</head>
<body>
<header class="topbar">
<button class="nav-toggle" type="button" aria-controls="help-sidebar" aria-expanded="true" title="Toggle chapter navigation"><span aria-hidden="true">☰</span><span class="sr-only">Toggle chapter navigation</span></button>
<a class="brand" href="index.html">OpenCAD Studio Help</a>
<div class="search-shell">
<label class="sr-only" for="help-search">Search help</label>
<input id="help-search" class="help-search" type="search" role="combobox" placeholder="Search commands and topics" autocomplete="off" aria-autocomplete="list" aria-controls="search-results" aria-expanded="false">
<span class="search-key" aria-hidden="true">⌘K</span>
<ul id="search-results" class="search-results" role="listbox" hidden></ul>
</div>
</header>
<nav id="help-sidebar" class="sidebar" aria-label="Help chapters">
<div><a class="chapter-link" href="index.html">Help overview</a></div>
<details><summary><a href="getting-started.html">Getting Started</a></summary><ul><li><a href="getting-started.html#what-opencad-studio-does">What OpenCAD Studio does</a></li><li><a href="getting-started.html#a-first-drawing">A first drawing</a></li><li><a href="getting-started.html#how-to-use-this-manual">How to use this manual</a></li></ul></details>
<details><summary><a href="files-drawings.html">Files and Drawings</a></summary><ul><li><a href="files-drawings.html#create-and-open-drawings">Create and open drawings</a></li><li><a href="files-drawings.html#save-and-save-as">Save and Save As</a></li><li><a href="files-drawings.html#recovery-and-backups">Recovery and backups</a></li><li><a href="files-drawings.html#external-resources">External resources</a></li></ul></details>
<details><summary><a href="workspace-navigation.html">Workspace and Navigation</a></summary><ul><li><a href="workspace-navigation.html#model-space-and-paper-space">Model space and paper space</a></li><li><a href="workspace-navigation.html#pan-zoom-and-orbit">Pan, zoom, and orbit</a></li><li><a href="workspace-navigation.html#multiple-model-viewports">Multiple model viewports</a></li><li><a href="workspace-navigation.html#selection-and-object-snaps">Selection and object snaps</a></li></ul></details>
<details><summary><a href="create-edit-geometry.html">Create and Edit Geometry</a></summary><ul><li><a href="create-edit-geometry.html#basic-drawing-commands">Basic drawing commands</a></li><li><a href="create-edit-geometry.html#modify-geometry">Modify geometry</a></li><li><a href="create-edit-geometry.html#grips-and-direct-manipulation">Grips and direct manipulation</a></li><li><a href="create-edit-geometry.html#undo-redo-and-oops">Undo, redo, and OOPS</a></li></ul></details>
<details><summary><a href="layers-properties.html">Layers and Properties</a></summary><ul><li><a href="layers-properties.html#layers">Layers</a></li><li><a href="layers-properties.html#object-properties">Object properties</a></li><li><a href="layers-properties.html#styles">Styles</a></li></ul></details>
<details><summary><a href="blocks-references.html">Blocks and References</a></summary><ul><li><a href="blocks-references.html#create-and-insert-blocks">Create and insert blocks</a></li><li><a href="blocks-references.html#array-inserts">Array inserts</a></li><li><a href="blocks-references.html#attributes-and-external-references">Attributes and external references</a></li></ul></details>
<details><summary><a href="annotation-dimensions.html">Annotation and Dimensions</a></summary><ul><li><a href="annotation-dimensions.html#text-and-annotation">Text and annotation</a></li><li><a href="annotation-dimensions.html#dimensions">Dimensions</a></li><li><a href="annotation-dimensions.html#annotation-scale">Annotation scale</a></li></ul></details>
<details><summary><a href="layouts-plotting.html">Layouts and Plotting</a></summary><ul><li><a href="layouts-plotting.html#layouts-and-viewports">Layouts and viewports</a></li><li><a href="layouts-plotting.html#page-setup-and-plot-preview">Page setup and plot preview</a></li><li><a href="layouts-plotting.html#export-checks">Export checks</a></li></ul></details>
<details class="current-topic" open><summary><a href="3d-modeling.html">3D Modeling and Visualization</a></summary><ul><li><a href="3d-modeling.html#solids-and-surfaces">Solids and surfaces</a></li><li><a href="3d-modeling.html#visual-styles-and-lighting">Visual styles and lighting</a></li><li><a href="3d-modeling.html#tessellation-quality">Tessellation quality</a></li></ul></details>
<details><summary><a href="parametric-constraints.html">Parametric Constraints</a></summary><ul><li><a href="parametric-constraints.html#geometric-constraints">Geometric constraints</a></li><li><a href="parametric-constraints.html#editing-constrained-geometry">Editing constrained geometry</a></li><li><a href="parametric-constraints.html#persistence-and-history-limitations">Persistence and history limitations</a></li></ul></details>
<details><summary><a href="commands-shortcuts.html">Commands and Shortcuts</a></summary><ul><li><a href="commands-shortcuts.html#command-line">Command line</a></li><li><a href="commands-shortcuts.html#discover-commands">Discover commands</a></li><li><a href="commands-shortcuts.html#shortcut-conventions">Shortcut conventions</a></li></ul></details>
<details><summary><a href="automation-plugins.html">Automation and Plugins</a></summary><ul><li><a href="automation-plugins.html#automation-interface">Automation interface</a></li><li><a href="automation-plugins.html#plugins">Plugins</a></li><li><a href="automation-plugins.html#native-and-web-differences">Native and web differences</a></li></ul></details>
<details><summary><a href="troubleshooting-recovery.html">Troubleshooting and Recovery</a></summary><ul><li><a href="troubleshooting-recovery.html#a-drawing-opens-incorrectly">A drawing opens incorrectly</a></li><li><a href="troubleshooting-recovery.html#geometry-is-missing-or-distorted">Geometry is missing or distorted</a></li><li><a href="troubleshooting-recovery.html#rendering-is-incomplete">Rendering is incomplete</a></li><li><a href="troubleshooting-recovery.html#undo-did-not-restore-the-expected-state">Undo did not restore the expected state</a></li><li><a href="troubleshooting-recovery.html#report-a-reproducible-problem">Report a reproducible problem</a></li></ul></details>
</nav>
<main id="main-content">
<div class="content-wrap">
<p class="page-kicker">OpenCAD Studio User Manual</p>
<h1 class="content-title">3D Modeling and Visualization</h1>
<h3 id="solids-and-surfaces">Solids and surfaces</h3>
<p>OpenCAD Studio can create and display solid primitives and modeled features such as extrusions and revolutions. Imported ACIS-backed bodies are tessellated for display while exact body data is retained where supported.</p>
<h3 id="visual-styles-and-lighting">Visual styles and lighting</h3>
<p>Wireframe, shaded, hidden-line, materials, lights, and shadows affect display, not the underlying object geometry. Switch to wireframe when diagnosing missing faces or material problems.</p>
<h3 id="tessellation-quality">Tessellation quality</h3>
<p>Curves and analytic surfaces are converted into display geometry according to view and tolerance settings. A coarse display does not necessarily mean the stored curve or solid is coarse.</p>
<nav class="page-navigation" aria-label="Previous and next chapter"><a rel="prev" href="layouts-plotting.html">← Layouts and Plotting</a><a rel="next" href="parametric-constraints.html">Parametric Constraints →</a></nav>
<p class="date-footer">OpenCAD Studio Help · Generated from <code>help/manual.md</code></p>
</div>
</main>
</body>
</html>
+43
View File
@@ -0,0 +1,43 @@
# OpenCAD Studio Help
This directory is a self-contained, static user-help system. It is intentionally
not wired into the application yet.
## Authoring model
- `manual.md` is the single source of truth for user-facing prose.
- `tools/build_help.py` splits the level-two chapters into static HTML pages.
- `assets/css/` and `assets/js/` provide the shared responsive presentation,
navigation, dark mode, print styling, and local search.
- `context-map.json` reserves stable help targets for future command/dialog
integration without changing application source now.
- `assets/images/` is for screenshots and workflow illustrations referenced by
`manual.md`.
Generated HTML and `search-index.js` are committed so the manual works directly
from disk without a web server. Do not edit generated pages by hand.
## Build and validate
Requirements: Python 3 and Pandoc.
```sh
python3 help/tools/build_help.py
python3 help/tools/build_help.py --check
python3 help/tools/check_help.py
```
Open `help/index.html` in a browser to review the generated manual. When future
application work is safe, package the complete `help/` directory and connect the
Help menu or contextual controls to the URLs in `context-map.json`.
## Content rules
1. Keep level-two headings unique; each one is a chapter boundary.
2. Keep level-three headings stable because their generated IDs are public help
targets.
3. Describe observable behavior, not implementation details.
4. Use task-first procedures and state prerequisites before numbered steps.
5. Mark safety, compatibility, data-loss, and known limitations explicitly.
6. Regenerate and run both checks before committing documentation changes.
+55
View File
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="OpenCAD Studio user manual — Annotation and Dimensions">
<title>Annotation and Dimensions — OpenCAD Studio Help</title>
<link rel="stylesheet" href="assets/css/core.css">
<link rel="stylesheet" href="assets/css/opencad.css">
<script src="search-index.js" defer></script>
<script src="assets/js/help.js" defer></script>
</head>
<body>
<header class="topbar">
<button class="nav-toggle" type="button" aria-controls="help-sidebar" aria-expanded="true" title="Toggle chapter navigation"><span aria-hidden="true">☰</span><span class="sr-only">Toggle chapter navigation</span></button>
<a class="brand" href="index.html">OpenCAD Studio Help</a>
<div class="search-shell">
<label class="sr-only" for="help-search">Search help</label>
<input id="help-search" class="help-search" type="search" role="combobox" placeholder="Search commands and topics" autocomplete="off" aria-autocomplete="list" aria-controls="search-results" aria-expanded="false">
<span class="search-key" aria-hidden="true">⌘K</span>
<ul id="search-results" class="search-results" role="listbox" hidden></ul>
</div>
</header>
<nav id="help-sidebar" class="sidebar" aria-label="Help chapters">
<div><a class="chapter-link" href="index.html">Help overview</a></div>
<details><summary><a href="getting-started.html">Getting Started</a></summary><ul><li><a href="getting-started.html#what-opencad-studio-does">What OpenCAD Studio does</a></li><li><a href="getting-started.html#a-first-drawing">A first drawing</a></li><li><a href="getting-started.html#how-to-use-this-manual">How to use this manual</a></li></ul></details>
<details><summary><a href="files-drawings.html">Files and Drawings</a></summary><ul><li><a href="files-drawings.html#create-and-open-drawings">Create and open drawings</a></li><li><a href="files-drawings.html#save-and-save-as">Save and Save As</a></li><li><a href="files-drawings.html#recovery-and-backups">Recovery and backups</a></li><li><a href="files-drawings.html#external-resources">External resources</a></li></ul></details>
<details><summary><a href="workspace-navigation.html">Workspace and Navigation</a></summary><ul><li><a href="workspace-navigation.html#model-space-and-paper-space">Model space and paper space</a></li><li><a href="workspace-navigation.html#pan-zoom-and-orbit">Pan, zoom, and orbit</a></li><li><a href="workspace-navigation.html#multiple-model-viewports">Multiple model viewports</a></li><li><a href="workspace-navigation.html#selection-and-object-snaps">Selection and object snaps</a></li></ul></details>
<details><summary><a href="create-edit-geometry.html">Create and Edit Geometry</a></summary><ul><li><a href="create-edit-geometry.html#basic-drawing-commands">Basic drawing commands</a></li><li><a href="create-edit-geometry.html#modify-geometry">Modify geometry</a></li><li><a href="create-edit-geometry.html#grips-and-direct-manipulation">Grips and direct manipulation</a></li><li><a href="create-edit-geometry.html#undo-redo-and-oops">Undo, redo, and OOPS</a></li></ul></details>
<details><summary><a href="layers-properties.html">Layers and Properties</a></summary><ul><li><a href="layers-properties.html#layers">Layers</a></li><li><a href="layers-properties.html#object-properties">Object properties</a></li><li><a href="layers-properties.html#styles">Styles</a></li></ul></details>
<details><summary><a href="blocks-references.html">Blocks and References</a></summary><ul><li><a href="blocks-references.html#create-and-insert-blocks">Create and insert blocks</a></li><li><a href="blocks-references.html#array-inserts">Array inserts</a></li><li><a href="blocks-references.html#attributes-and-external-references">Attributes and external references</a></li></ul></details>
<details class="current-topic" open><summary><a href="annotation-dimensions.html">Annotation and Dimensions</a></summary><ul><li><a href="annotation-dimensions.html#text-and-annotation">Text and annotation</a></li><li><a href="annotation-dimensions.html#dimensions">Dimensions</a></li><li><a href="annotation-dimensions.html#annotation-scale">Annotation scale</a></li></ul></details>
<details><summary><a href="layouts-plotting.html">Layouts and Plotting</a></summary><ul><li><a href="layouts-plotting.html#layouts-and-viewports">Layouts and viewports</a></li><li><a href="layouts-plotting.html#page-setup-and-plot-preview">Page setup and plot preview</a></li><li><a href="layouts-plotting.html#export-checks">Export checks</a></li></ul></details>
<details><summary><a href="3d-modeling.html">3D Modeling and Visualization</a></summary><ul><li><a href="3d-modeling.html#solids-and-surfaces">Solids and surfaces</a></li><li><a href="3d-modeling.html#visual-styles-and-lighting">Visual styles and lighting</a></li><li><a href="3d-modeling.html#tessellation-quality">Tessellation quality</a></li></ul></details>
<details><summary><a href="parametric-constraints.html">Parametric Constraints</a></summary><ul><li><a href="parametric-constraints.html#geometric-constraints">Geometric constraints</a></li><li><a href="parametric-constraints.html#editing-constrained-geometry">Editing constrained geometry</a></li><li><a href="parametric-constraints.html#persistence-and-history-limitations">Persistence and history limitations</a></li></ul></details>
<details><summary><a href="commands-shortcuts.html">Commands and Shortcuts</a></summary><ul><li><a href="commands-shortcuts.html#command-line">Command line</a></li><li><a href="commands-shortcuts.html#discover-commands">Discover commands</a></li><li><a href="commands-shortcuts.html#shortcut-conventions">Shortcut conventions</a></li></ul></details>
<details><summary><a href="automation-plugins.html">Automation and Plugins</a></summary><ul><li><a href="automation-plugins.html#automation-interface">Automation interface</a></li><li><a href="automation-plugins.html#plugins">Plugins</a></li><li><a href="automation-plugins.html#native-and-web-differences">Native and web differences</a></li></ul></details>
<details><summary><a href="troubleshooting-recovery.html">Troubleshooting and Recovery</a></summary><ul><li><a href="troubleshooting-recovery.html#a-drawing-opens-incorrectly">A drawing opens incorrectly</a></li><li><a href="troubleshooting-recovery.html#geometry-is-missing-or-distorted">Geometry is missing or distorted</a></li><li><a href="troubleshooting-recovery.html#rendering-is-incomplete">Rendering is incomplete</a></li><li><a href="troubleshooting-recovery.html#undo-did-not-restore-the-expected-state">Undo did not restore the expected state</a></li><li><a href="troubleshooting-recovery.html#report-a-reproducible-problem">Report a reproducible problem</a></li></ul></details>
</nav>
<main id="main-content">
<div class="content-wrap">
<p class="page-kicker">OpenCAD Studio User Manual</p>
<h1 class="content-title">Annotation and Dimensions</h1>
<h3 id="text-and-annotation">Text and annotation</h3>
<p>Single-line text, multiline text, leaders, multileaders, tables, tolerances, center marks, and centerlines communicate design intent. Missing fonts can change layout or substitute glyphs, especially in the web build.</p>
<h3 id="dimensions">Dimensions</h3>
<p>Dimensions combine measured geometry with a dimension style. Associative dimensions update when their source geometry changes; non-associative or partially associated dimensions require manual verification.</p>
<h3 id="annotation-scale">Annotation scale</h3>
<p>Annotative objects can display differently at different viewport scales. Confirm the active annotation scale before creating annotations and inspect all paper-space viewports before plotting.</p>
<nav class="page-navigation" aria-label="Previous and next chapter"><a rel="prev" href="blocks-references.html">← Blocks and References</a><a rel="next" href="layouts-plotting.html">Layouts and Plotting →</a></nav>
<p class="date-footer">OpenCAD Studio Help · Generated from <code>help/manual.md</code></p>
</div>
</main>
</body>
</html>
+190
View File
@@ -0,0 +1,190 @@
:root {
color-scheme: light dark;
--accent: #1677d2;
--accent-strong: #075cae;
--accent-soft: #eaf4ff;
--canvas: #ffffff;
--chrome: rgba(20, 27, 37, 0.96);
--line: #d7dee7;
--muted: #617083;
--sidebar: #f4f7fa;
--text: #16202c;
--warning: #fff4cc;
--danger: #a62b2b;
--sidebar-width: 286px;
--topbar-height: 62px;
}
* { box-sizing: border-box; }
html { scroll-padding-top: calc(var(--topbar-height) + 20px); }
body {
margin: 0;
background: var(--canvas);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 16px;
line-height: 1.62;
}
a { color: var(--accent-strong); text-decoration-thickness: .08em; text-underline-offset: .16em; }
a:hover { color: var(--accent); }
a:focus-visible, button:focus-visible, input:focus-visible {
outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent);
outline-offset: 2px;
}
.topbar {
position: fixed;
inset: 0 0 auto;
z-index: 30;
height: var(--topbar-height);
display: flex;
align-items: center;
gap: 14px;
padding: 0 18px;
background: var(--chrome);
border-bottom: 1px solid rgba(255, 255, 255, .13);
backdrop-filter: blur(16px);
}
.brand { min-width: 190px; color: #fff; font-size: 18px; font-weight: 720; text-decoration: none; }
.brand:hover { color: #fff; }
.nav-toggle {
width: 38px;
height: 38px;
border: 0;
border-radius: 8px;
background: transparent;
color: #fff;
cursor: pointer;
font-size: 22px;
}
.nav-toggle:hover { background: rgba(255, 255, 255, .12); }
.search-shell { position: relative; width: min(520px, 55vw); margin-left: auto; }
.help-search {
width: 100%;
height: 38px;
padding: 0 42px 0 13px;
border: 1px solid rgba(255, 255, 255, .24);
border-radius: 9px;
background: rgba(255, 255, 255, .11);
color: #fff;
font: inherit;
}
.help-search::placeholder { color: rgba(255, 255, 255, .68); }
.search-key { position: absolute; top: 8px; right: 10px; color: rgba(255, 255, 255, .64); font-size: 13px; }
.search-results {
position: absolute;
top: 44px;
right: 0;
width: min(620px, calc(100vw - 24px));
max-height: min(70vh, 560px);
overflow: auto;
margin: 0;
padding: 7px;
border: 1px solid var(--line);
border-radius: 10px;
background: var(--canvas);
box-shadow: 0 18px 50px rgba(0, 0, 0, .28);
list-style: none;
}
.search-results[hidden] { display: none; }
.search-results a { display: block; padding: 10px 11px; border-radius: 7px; color: var(--text); text-decoration: none; }
.search-results a:hover, .search-results a[aria-selected="true"] { background: var(--accent-soft); }
.search-results strong { display: block; color: var(--accent-strong); }
.search-results span { display: block; color: var(--muted); font-size: 13px; }
.search-empty { padding: 12px; color: var(--muted); }
.sidebar {
position: fixed;
z-index: 20;
inset: var(--topbar-height) auto 0 0;
width: var(--sidebar-width);
padding: 16px 13px 36px;
overflow: auto;
background: var(--sidebar);
border-right: 1px solid var(--line);
transition: transform 180ms ease;
}
body.nav-collapsed .sidebar { transform: translateX(-100%); }
.sidebar details, .sidebar > div { margin: 3px 0; border-radius: 8px; }
.sidebar details.current-topic, .sidebar > div.current-topic { background: var(--canvas); box-shadow: 0 0 0 1px var(--line); }
.sidebar summary { padding: 7px 9px; font-weight: 680; cursor: pointer; }
.sidebar .chapter-link { display: block; padding: 7px 9px; font-weight: 680; }
.sidebar ul { margin: 0; padding: 0 9px 9px 27px; list-style: none; }
.sidebar li { margin: 2px 0; line-height: 1.35; }
.sidebar li a { display: block; padding: 3px 0; color: var(--text); font-size: 13px; text-decoration: none; }
.sidebar li a:hover { color: var(--accent); }
#main-content {
min-height: 100vh;
margin-left: var(--sidebar-width);
padding: calc(var(--topbar-height) + 38px) 34px 70px;
transition: margin-left 180ms ease;
}
body.nav-collapsed #main-content { margin-left: 0; }
.content-wrap { max-width: 940px; margin: 0 auto; }
.page-kicker { margin: 0; color: var(--accent-strong); font-size: 12px; font-weight: 760; letter-spacing: .09em; text-transform: uppercase; }
.content-title { margin: 3px 0 30px; font-size: clamp(34px, 6vw, 52px); line-height: 1.06; letter-spacing: -.035em; }
h2 { margin: 44px 0 17px; padding-bottom: 8px; border-bottom: 2px solid var(--accent); font-size: 23px; line-height: 1.3; }
h3 { margin: 34px 0 8px; font-size: 19px; line-height: 1.35; }
p { margin: 9px 0 16px; }
ul, ol { padding-left: 26px; }
li { margin: 5px 0; }
code, pre { font-family: ui-monospace, "SF Mono", Menlo, monospace; }
code { color: #a0440c; font-size: .91em; }
blockquote {
margin: 18px 0;
padding: 12px 15px;
border: 1px solid color-mix(in srgb, #d5a000 45%, var(--line));
border-left: 4px solid #d5a000;
border-radius: 8px;
background: var(--warning);
color: #503f05;
}
blockquote p { margin: 0; }
.page-navigation { display: flex; justify-content: space-between; gap: 20px; margin-top: 58px; padding-top: 18px; border-top: 1px solid var(--line); }
.page-navigation a:last-child { margin-left: auto; }
.date-footer { margin-top: 34px; color: var(--muted); font-size: 13px; text-align: center; }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
@media (max-width: 760px) {
.brand { min-width: 0; font-size: 16px; }
.search-shell { width: min(48vw, 420px); }
.search-key { display: none; }
.sidebar { box-shadow: 12px 0 34px rgba(0, 0, 0, .22); }
body:not(.nav-open) .sidebar { transform: translateX(-100%); }
body.nav-open .sidebar { transform: translateX(0); }
#main-content, body.nav-collapsed #main-content { margin-left: 0; padding: calc(var(--topbar-height) + 28px) 18px 52px; }
}
@media (prefers-color-scheme: dark) {
:root {
--accent: #66b6ff;
--accent-strong: #8bc7ff;
--accent-soft: #17344f;
--canvas: #161b22;
--chrome: rgba(14, 18, 24, .96);
--line: #374252;
--muted: #9eabba;
--sidebar: #1d242d;
--text: #eef3f8;
--warning: #423819;
}
code { color: #ffae79; }
blockquote { color: #f5e6ad; }
}
@media print {
.topbar, .sidebar, .page-navigation { display: none !important; }
#main-content { margin: 0; padding: 0; }
body { color: #000; background: #fff; font-size: 11pt; }
a { color: inherit; }
h2, h3 { break-after: avoid; }
blockquote { break-inside: avoid; }
}
+24
View File
@@ -0,0 +1,24 @@
.hero {
margin: 0 0 38px;
padding: clamp(25px, 6vw, 56px);
border: 1px solid color-mix(in srgb, var(--accent) 30%, var(--line));
border-radius: 18px;
background:
linear-gradient(115deg, color-mix(in srgb, var(--accent-soft) 82%, var(--canvas)), var(--canvas));
}
.hero .eyebrow { margin: 0; color: var(--accent-strong); font-size: 12px; font-weight: 760; letter-spacing: .1em; text-transform: uppercase; }
.hero h2 { margin: 8px 0 14px; padding: 0; border: 0; font-size: clamp(28px, 5vw, 44px); line-height: 1.08; letter-spacing: -.03em; }
.hero p:last-child { max-width: 720px; margin-bottom: 0; font-size: 17px; }
.chapter-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.chapter-card { min-height: 130px; display: flex; flex-direction: column; gap: 7px; padding: 18px; border: 1px solid var(--line); border-radius: 10px; color: var(--text); text-decoration: none; }
.chapter-card:hover { border-color: var(--accent); background: var(--accent-soft); text-decoration: none; }
.chapter-card strong { color: var(--accent-strong); font-size: 18px; }
.chapter-card span { color: var(--muted); }
.quick-paths { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin: 18px 0 28px; }
.quick-paths a { padding: 12px; border: 1px solid var(--line); border-radius: 9px; text-align: center; text-decoration: none; }
.quick-paths a:hover { background: var(--accent-soft); }
@media (max-width: 640px) {
.chapter-grid, .quick-paths { grid-template-columns: 1fr; }
.chapter-card { min-height: 0; }
}
+9
View File
@@ -0,0 +1,9 @@
# Help images
Place optimized screenshots and workflow illustrations in this directory.
Use descriptive lowercase filenames, include meaningful alternative text in
`manual.md`, crop away unrelated desktop content, and avoid confidential drawing
names or paths. Prefer WebP or optimized PNG and keep each image at the smallest
resolution that remains legible on a high-density display.
+120
View File
@@ -0,0 +1,120 @@
(function () {
"use strict";
const body = document.body;
const toggle = document.querySelector(".nav-toggle");
const sidebar = document.getElementById("help-sidebar");
const search = document.getElementById("help-search");
const results = document.getElementById("search-results");
const mobile = window.matchMedia("(max-width: 760px)");
let selected = -1;
function syncToggle() {
const expanded = mobile.matches
? body.classList.contains("nav-open")
: !body.classList.contains("nav-collapsed");
toggle?.setAttribute("aria-expanded", String(expanded));
}
toggle?.addEventListener("click", function () {
body.classList.toggle(mobile.matches ? "nav-open" : "nav-collapsed");
syncToggle();
});
sidebar?.addEventListener("click", function (event) {
if (mobile.matches && event.target.closest("a")) {
body.classList.remove("nav-open");
syncToggle();
}
});
mobile.addEventListener("change", function () {
body.classList.remove("nav-open");
syncToggle();
});
function hideResults() {
results.hidden = true;
results.innerHTML = "";
selected = -1;
search?.setAttribute("aria-expanded", "false");
}
function renderResults(query) {
const terms = query.toLocaleLowerCase().trim().split(/\s+/).filter(Boolean);
if (!terms.length) {
hideResults();
return;
}
const matches = (window.OCS_HELP_INDEX || []).map(function (entry) {
const title = entry.title.toLocaleLowerCase();
const text = entry.text.toLocaleLowerCase();
const score = terms.reduce(function (total, term) {
if (!text.includes(term) && !title.includes(term)) return -1000;
return total + (title.includes(term) ? 8 : 1);
}, 0);
return { entry: entry, score: score };
}).filter(function (item) { return item.score >= 0; })
.sort(function (a, b) { return b.score - a.score || a.entry.title.localeCompare(b.entry.title); })
.slice(0, 12);
results.innerHTML = "";
if (!matches.length) {
const empty = document.createElement("li");
empty.className = "search-empty";
empty.textContent = "No matching help topics.";
results.appendChild(empty);
} else {
matches.forEach(function (item) {
const li = document.createElement("li");
const link = document.createElement("a");
link.href = item.entry.url;
const strong = document.createElement("strong");
strong.textContent = item.entry.title;
const detail = document.createElement("span");
detail.textContent = item.entry.chapter;
link.append(strong, detail);
li.appendChild(link);
results.appendChild(li);
});
}
results.hidden = false;
search.setAttribute("aria-expanded", "true");
selected = -1;
}
search?.addEventListener("input", function () { renderResults(search.value); });
search?.addEventListener("keydown", function (event) {
const links = Array.from(results.querySelectorAll("a"));
if (event.key === "Escape") {
search.value = "";
hideResults();
return;
}
if (!links.length || !["ArrowDown", "ArrowUp", "Enter"].includes(event.key)) return;
event.preventDefault();
if (event.key === "Enter" && selected >= 0) {
links[selected].click();
return;
}
if (event.key === "ArrowDown") selected = (selected + 1) % links.length;
if (event.key === "ArrowUp") selected = selected < 0 ? links.length - 1 : (selected - 1 + links.length) % links.length;
links.forEach(function (link, index) { link.setAttribute("aria-selected", String(index === selected)); });
links[selected]?.scrollIntoView({ block: "nearest" });
});
document.addEventListener("keydown", function (event) {
const shortcut = event.key === "/" || ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k");
if (shortcut && document.activeElement !== search) {
event.preventDefault();
search?.focus();
}
});
document.addEventListener("click", function (event) {
if (!event.target.closest(".search-shell")) hideResults();
});
syncToggle();
}());
+55
View File
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="OpenCAD Studio user manual — Automation and Plugins">
<title>Automation and Plugins — OpenCAD Studio Help</title>
<link rel="stylesheet" href="assets/css/core.css">
<link rel="stylesheet" href="assets/css/opencad.css">
<script src="search-index.js" defer></script>
<script src="assets/js/help.js" defer></script>
</head>
<body>
<header class="topbar">
<button class="nav-toggle" type="button" aria-controls="help-sidebar" aria-expanded="true" title="Toggle chapter navigation"><span aria-hidden="true">☰</span><span class="sr-only">Toggle chapter navigation</span></button>
<a class="brand" href="index.html">OpenCAD Studio Help</a>
<div class="search-shell">
<label class="sr-only" for="help-search">Search help</label>
<input id="help-search" class="help-search" type="search" role="combobox" placeholder="Search commands and topics" autocomplete="off" aria-autocomplete="list" aria-controls="search-results" aria-expanded="false">
<span class="search-key" aria-hidden="true">⌘K</span>
<ul id="search-results" class="search-results" role="listbox" hidden></ul>
</div>
</header>
<nav id="help-sidebar" class="sidebar" aria-label="Help chapters">
<div><a class="chapter-link" href="index.html">Help overview</a></div>
<details><summary><a href="getting-started.html">Getting Started</a></summary><ul><li><a href="getting-started.html#what-opencad-studio-does">What OpenCAD Studio does</a></li><li><a href="getting-started.html#a-first-drawing">A first drawing</a></li><li><a href="getting-started.html#how-to-use-this-manual">How to use this manual</a></li></ul></details>
<details><summary><a href="files-drawings.html">Files and Drawings</a></summary><ul><li><a href="files-drawings.html#create-and-open-drawings">Create and open drawings</a></li><li><a href="files-drawings.html#save-and-save-as">Save and Save As</a></li><li><a href="files-drawings.html#recovery-and-backups">Recovery and backups</a></li><li><a href="files-drawings.html#external-resources">External resources</a></li></ul></details>
<details><summary><a href="workspace-navigation.html">Workspace and Navigation</a></summary><ul><li><a href="workspace-navigation.html#model-space-and-paper-space">Model space and paper space</a></li><li><a href="workspace-navigation.html#pan-zoom-and-orbit">Pan, zoom, and orbit</a></li><li><a href="workspace-navigation.html#multiple-model-viewports">Multiple model viewports</a></li><li><a href="workspace-navigation.html#selection-and-object-snaps">Selection and object snaps</a></li></ul></details>
<details><summary><a href="create-edit-geometry.html">Create and Edit Geometry</a></summary><ul><li><a href="create-edit-geometry.html#basic-drawing-commands">Basic drawing commands</a></li><li><a href="create-edit-geometry.html#modify-geometry">Modify geometry</a></li><li><a href="create-edit-geometry.html#grips-and-direct-manipulation">Grips and direct manipulation</a></li><li><a href="create-edit-geometry.html#undo-redo-and-oops">Undo, redo, and OOPS</a></li></ul></details>
<details><summary><a href="layers-properties.html">Layers and Properties</a></summary><ul><li><a href="layers-properties.html#layers">Layers</a></li><li><a href="layers-properties.html#object-properties">Object properties</a></li><li><a href="layers-properties.html#styles">Styles</a></li></ul></details>
<details><summary><a href="blocks-references.html">Blocks and References</a></summary><ul><li><a href="blocks-references.html#create-and-insert-blocks">Create and insert blocks</a></li><li><a href="blocks-references.html#array-inserts">Array inserts</a></li><li><a href="blocks-references.html#attributes-and-external-references">Attributes and external references</a></li></ul></details>
<details><summary><a href="annotation-dimensions.html">Annotation and Dimensions</a></summary><ul><li><a href="annotation-dimensions.html#text-and-annotation">Text and annotation</a></li><li><a href="annotation-dimensions.html#dimensions">Dimensions</a></li><li><a href="annotation-dimensions.html#annotation-scale">Annotation scale</a></li></ul></details>
<details><summary><a href="layouts-plotting.html">Layouts and Plotting</a></summary><ul><li><a href="layouts-plotting.html#layouts-and-viewports">Layouts and viewports</a></li><li><a href="layouts-plotting.html#page-setup-and-plot-preview">Page setup and plot preview</a></li><li><a href="layouts-plotting.html#export-checks">Export checks</a></li></ul></details>
<details><summary><a href="3d-modeling.html">3D Modeling and Visualization</a></summary><ul><li><a href="3d-modeling.html#solids-and-surfaces">Solids and surfaces</a></li><li><a href="3d-modeling.html#visual-styles-and-lighting">Visual styles and lighting</a></li><li><a href="3d-modeling.html#tessellation-quality">Tessellation quality</a></li></ul></details>
<details><summary><a href="parametric-constraints.html">Parametric Constraints</a></summary><ul><li><a href="parametric-constraints.html#geometric-constraints">Geometric constraints</a></li><li><a href="parametric-constraints.html#editing-constrained-geometry">Editing constrained geometry</a></li><li><a href="parametric-constraints.html#persistence-and-history-limitations">Persistence and history limitations</a></li></ul></details>
<details><summary><a href="commands-shortcuts.html">Commands and Shortcuts</a></summary><ul><li><a href="commands-shortcuts.html#command-line">Command line</a></li><li><a href="commands-shortcuts.html#discover-commands">Discover commands</a></li><li><a href="commands-shortcuts.html#shortcut-conventions">Shortcut conventions</a></li></ul></details>
<details class="current-topic" open><summary><a href="automation-plugins.html">Automation and Plugins</a></summary><ul><li><a href="automation-plugins.html#automation-interface">Automation interface</a></li><li><a href="automation-plugins.html#plugins">Plugins</a></li><li><a href="automation-plugins.html#native-and-web-differences">Native and web differences</a></li></ul></details>
<details><summary><a href="troubleshooting-recovery.html">Troubleshooting and Recovery</a></summary><ul><li><a href="troubleshooting-recovery.html#a-drawing-opens-incorrectly">A drawing opens incorrectly</a></li><li><a href="troubleshooting-recovery.html#geometry-is-missing-or-distorted">Geometry is missing or distorted</a></li><li><a href="troubleshooting-recovery.html#rendering-is-incomplete">Rendering is incomplete</a></li><li><a href="troubleshooting-recovery.html#undo-did-not-restore-the-expected-state">Undo did not restore the expected state</a></li><li><a href="troubleshooting-recovery.html#report-a-reproducible-problem">Report a reproducible problem</a></li></ul></details>
</nav>
<main id="main-content">
<div class="content-wrap">
<p class="page-kicker">OpenCAD Studio User Manual</p>
<h1 class="content-title">Automation and Plugins</h1>
<h3 id="automation-interface">Automation interface</h3>
<p>The automation interface can inspect documents, run commands, query geometry, capture the viewport, and perform batches. Callers should use request IDs and document revisions to avoid repeating or applying work to stale state.</p>
<h3 id="plugins">Plugins</h3>
<p>Native plugins can contribute commands and ribbon surfaces through the plugin host. Install only trusted packages that match the host API, compiler, and CAD model compatibility requirements.</p>
<h3 id="native-and-web-differences">Native and web differences</h3>
<p>The browser build uses in-memory file workflows and cannot load native plugin libraries. Some rendering, font, windowing, and parallel-processing behavior also differs from the desktop build.</p>
<nav class="page-navigation" aria-label="Previous and next chapter"><a rel="prev" href="commands-shortcuts.html">← Commands and Shortcuts</a><a rel="next" href="troubleshooting-recovery.html">Troubleshooting and Recovery →</a></nav>
<p class="date-footer">OpenCAD Studio Help · Generated from <code>help/manual.md</code></p>
</div>
</main>
</body>
</html>
+55
View File
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="OpenCAD Studio user manual — Blocks and References">
<title>Blocks and References — OpenCAD Studio Help</title>
<link rel="stylesheet" href="assets/css/core.css">
<link rel="stylesheet" href="assets/css/opencad.css">
<script src="search-index.js" defer></script>
<script src="assets/js/help.js" defer></script>
</head>
<body>
<header class="topbar">
<button class="nav-toggle" type="button" aria-controls="help-sidebar" aria-expanded="true" title="Toggle chapter navigation"><span aria-hidden="true">☰</span><span class="sr-only">Toggle chapter navigation</span></button>
<a class="brand" href="index.html">OpenCAD Studio Help</a>
<div class="search-shell">
<label class="sr-only" for="help-search">Search help</label>
<input id="help-search" class="help-search" type="search" role="combobox" placeholder="Search commands and topics" autocomplete="off" aria-autocomplete="list" aria-controls="search-results" aria-expanded="false">
<span class="search-key" aria-hidden="true">⌘K</span>
<ul id="search-results" class="search-results" role="listbox" hidden></ul>
</div>
</header>
<nav id="help-sidebar" class="sidebar" aria-label="Help chapters">
<div><a class="chapter-link" href="index.html">Help overview</a></div>
<details><summary><a href="getting-started.html">Getting Started</a></summary><ul><li><a href="getting-started.html#what-opencad-studio-does">What OpenCAD Studio does</a></li><li><a href="getting-started.html#a-first-drawing">A first drawing</a></li><li><a href="getting-started.html#how-to-use-this-manual">How to use this manual</a></li></ul></details>
<details><summary><a href="files-drawings.html">Files and Drawings</a></summary><ul><li><a href="files-drawings.html#create-and-open-drawings">Create and open drawings</a></li><li><a href="files-drawings.html#save-and-save-as">Save and Save As</a></li><li><a href="files-drawings.html#recovery-and-backups">Recovery and backups</a></li><li><a href="files-drawings.html#external-resources">External resources</a></li></ul></details>
<details><summary><a href="workspace-navigation.html">Workspace and Navigation</a></summary><ul><li><a href="workspace-navigation.html#model-space-and-paper-space">Model space and paper space</a></li><li><a href="workspace-navigation.html#pan-zoom-and-orbit">Pan, zoom, and orbit</a></li><li><a href="workspace-navigation.html#multiple-model-viewports">Multiple model viewports</a></li><li><a href="workspace-navigation.html#selection-and-object-snaps">Selection and object snaps</a></li></ul></details>
<details><summary><a href="create-edit-geometry.html">Create and Edit Geometry</a></summary><ul><li><a href="create-edit-geometry.html#basic-drawing-commands">Basic drawing commands</a></li><li><a href="create-edit-geometry.html#modify-geometry">Modify geometry</a></li><li><a href="create-edit-geometry.html#grips-and-direct-manipulation">Grips and direct manipulation</a></li><li><a href="create-edit-geometry.html#undo-redo-and-oops">Undo, redo, and OOPS</a></li></ul></details>
<details><summary><a href="layers-properties.html">Layers and Properties</a></summary><ul><li><a href="layers-properties.html#layers">Layers</a></li><li><a href="layers-properties.html#object-properties">Object properties</a></li><li><a href="layers-properties.html#styles">Styles</a></li></ul></details>
<details class="current-topic" open><summary><a href="blocks-references.html">Blocks and References</a></summary><ul><li><a href="blocks-references.html#create-and-insert-blocks">Create and insert blocks</a></li><li><a href="blocks-references.html#array-inserts">Array inserts</a></li><li><a href="blocks-references.html#attributes-and-external-references">Attributes and external references</a></li></ul></details>
<details><summary><a href="annotation-dimensions.html">Annotation and Dimensions</a></summary><ul><li><a href="annotation-dimensions.html#text-and-annotation">Text and annotation</a></li><li><a href="annotation-dimensions.html#dimensions">Dimensions</a></li><li><a href="annotation-dimensions.html#annotation-scale">Annotation scale</a></li></ul></details>
<details><summary><a href="layouts-plotting.html">Layouts and Plotting</a></summary><ul><li><a href="layouts-plotting.html#layouts-and-viewports">Layouts and viewports</a></li><li><a href="layouts-plotting.html#page-setup-and-plot-preview">Page setup and plot preview</a></li><li><a href="layouts-plotting.html#export-checks">Export checks</a></li></ul></details>
<details><summary><a href="3d-modeling.html">3D Modeling and Visualization</a></summary><ul><li><a href="3d-modeling.html#solids-and-surfaces">Solids and surfaces</a></li><li><a href="3d-modeling.html#visual-styles-and-lighting">Visual styles and lighting</a></li><li><a href="3d-modeling.html#tessellation-quality">Tessellation quality</a></li></ul></details>
<details><summary><a href="parametric-constraints.html">Parametric Constraints</a></summary><ul><li><a href="parametric-constraints.html#geometric-constraints">Geometric constraints</a></li><li><a href="parametric-constraints.html#editing-constrained-geometry">Editing constrained geometry</a></li><li><a href="parametric-constraints.html#persistence-and-history-limitations">Persistence and history limitations</a></li></ul></details>
<details><summary><a href="commands-shortcuts.html">Commands and Shortcuts</a></summary><ul><li><a href="commands-shortcuts.html#command-line">Command line</a></li><li><a href="commands-shortcuts.html#discover-commands">Discover commands</a></li><li><a href="commands-shortcuts.html#shortcut-conventions">Shortcut conventions</a></li></ul></details>
<details><summary><a href="automation-plugins.html">Automation and Plugins</a></summary><ul><li><a href="automation-plugins.html#automation-interface">Automation interface</a></li><li><a href="automation-plugins.html#plugins">Plugins</a></li><li><a href="automation-plugins.html#native-and-web-differences">Native and web differences</a></li></ul></details>
<details><summary><a href="troubleshooting-recovery.html">Troubleshooting and Recovery</a></summary><ul><li><a href="troubleshooting-recovery.html#a-drawing-opens-incorrectly">A drawing opens incorrectly</a></li><li><a href="troubleshooting-recovery.html#geometry-is-missing-or-distorted">Geometry is missing or distorted</a></li><li><a href="troubleshooting-recovery.html#rendering-is-incomplete">Rendering is incomplete</a></li><li><a href="troubleshooting-recovery.html#undo-did-not-restore-the-expected-state">Undo did not restore the expected state</a></li><li><a href="troubleshooting-recovery.html#report-a-reproducible-problem">Report a reproducible problem</a></li></ul></details>
</nav>
<main id="main-content">
<div class="content-wrap">
<p class="page-kicker">OpenCAD Studio User Manual</p>
<h1 class="content-title">Blocks and References</h1>
<h3 id="create-and-insert-blocks">Create and insert blocks</h3>
<p>A block definition groups reusable drawing content; an INSERT places an instance with its own position, rotation, scale, attributes, and optional array settings. Editing a definition affects every reference to it.</p>
<h3 id="array-inserts">Array inserts</h3>
<p>Array inserts place multiple block instances with row and column counts and spacing. Large counts can generate substantial geometry. Validate scaled and rotated arrays when exchanging drawings with another CAD system.</p>
<h3 id="attributes-and-external-references">Attributes and external references</h3>
<p>Attributes attach editable values to block references. External references and underlays retain links to other files; preserve their paths when sharing or archiving a project.</p>
<nav class="page-navigation" aria-label="Previous and next chapter"><a rel="prev" href="layers-properties.html">← Layers and Properties</a><a rel="next" href="annotation-dimensions.html">Annotation and Dimensions →</a></nav>
<p class="date-footer">OpenCAD Studio Help · Generated from <code>help/manual.md</code></p>
</div>
</main>
</body>
</html>
+55
View File
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="OpenCAD Studio user manual — Commands and Shortcuts">
<title>Commands and Shortcuts — OpenCAD Studio Help</title>
<link rel="stylesheet" href="assets/css/core.css">
<link rel="stylesheet" href="assets/css/opencad.css">
<script src="search-index.js" defer></script>
<script src="assets/js/help.js" defer></script>
</head>
<body>
<header class="topbar">
<button class="nav-toggle" type="button" aria-controls="help-sidebar" aria-expanded="true" title="Toggle chapter navigation"><span aria-hidden="true">☰</span><span class="sr-only">Toggle chapter navigation</span></button>
<a class="brand" href="index.html">OpenCAD Studio Help</a>
<div class="search-shell">
<label class="sr-only" for="help-search">Search help</label>
<input id="help-search" class="help-search" type="search" role="combobox" placeholder="Search commands and topics" autocomplete="off" aria-autocomplete="list" aria-controls="search-results" aria-expanded="false">
<span class="search-key" aria-hidden="true">⌘K</span>
<ul id="search-results" class="search-results" role="listbox" hidden></ul>
</div>
</header>
<nav id="help-sidebar" class="sidebar" aria-label="Help chapters">
<div><a class="chapter-link" href="index.html">Help overview</a></div>
<details><summary><a href="getting-started.html">Getting Started</a></summary><ul><li><a href="getting-started.html#what-opencad-studio-does">What OpenCAD Studio does</a></li><li><a href="getting-started.html#a-first-drawing">A first drawing</a></li><li><a href="getting-started.html#how-to-use-this-manual">How to use this manual</a></li></ul></details>
<details><summary><a href="files-drawings.html">Files and Drawings</a></summary><ul><li><a href="files-drawings.html#create-and-open-drawings">Create and open drawings</a></li><li><a href="files-drawings.html#save-and-save-as">Save and Save As</a></li><li><a href="files-drawings.html#recovery-and-backups">Recovery and backups</a></li><li><a href="files-drawings.html#external-resources">External resources</a></li></ul></details>
<details><summary><a href="workspace-navigation.html">Workspace and Navigation</a></summary><ul><li><a href="workspace-navigation.html#model-space-and-paper-space">Model space and paper space</a></li><li><a href="workspace-navigation.html#pan-zoom-and-orbit">Pan, zoom, and orbit</a></li><li><a href="workspace-navigation.html#multiple-model-viewports">Multiple model viewports</a></li><li><a href="workspace-navigation.html#selection-and-object-snaps">Selection and object snaps</a></li></ul></details>
<details><summary><a href="create-edit-geometry.html">Create and Edit Geometry</a></summary><ul><li><a href="create-edit-geometry.html#basic-drawing-commands">Basic drawing commands</a></li><li><a href="create-edit-geometry.html#modify-geometry">Modify geometry</a></li><li><a href="create-edit-geometry.html#grips-and-direct-manipulation">Grips and direct manipulation</a></li><li><a href="create-edit-geometry.html#undo-redo-and-oops">Undo, redo, and OOPS</a></li></ul></details>
<details><summary><a href="layers-properties.html">Layers and Properties</a></summary><ul><li><a href="layers-properties.html#layers">Layers</a></li><li><a href="layers-properties.html#object-properties">Object properties</a></li><li><a href="layers-properties.html#styles">Styles</a></li></ul></details>
<details><summary><a href="blocks-references.html">Blocks and References</a></summary><ul><li><a href="blocks-references.html#create-and-insert-blocks">Create and insert blocks</a></li><li><a href="blocks-references.html#array-inserts">Array inserts</a></li><li><a href="blocks-references.html#attributes-and-external-references">Attributes and external references</a></li></ul></details>
<details><summary><a href="annotation-dimensions.html">Annotation and Dimensions</a></summary><ul><li><a href="annotation-dimensions.html#text-and-annotation">Text and annotation</a></li><li><a href="annotation-dimensions.html#dimensions">Dimensions</a></li><li><a href="annotation-dimensions.html#annotation-scale">Annotation scale</a></li></ul></details>
<details><summary><a href="layouts-plotting.html">Layouts and Plotting</a></summary><ul><li><a href="layouts-plotting.html#layouts-and-viewports">Layouts and viewports</a></li><li><a href="layouts-plotting.html#page-setup-and-plot-preview">Page setup and plot preview</a></li><li><a href="layouts-plotting.html#export-checks">Export checks</a></li></ul></details>
<details><summary><a href="3d-modeling.html">3D Modeling and Visualization</a></summary><ul><li><a href="3d-modeling.html#solids-and-surfaces">Solids and surfaces</a></li><li><a href="3d-modeling.html#visual-styles-and-lighting">Visual styles and lighting</a></li><li><a href="3d-modeling.html#tessellation-quality">Tessellation quality</a></li></ul></details>
<details><summary><a href="parametric-constraints.html">Parametric Constraints</a></summary><ul><li><a href="parametric-constraints.html#geometric-constraints">Geometric constraints</a></li><li><a href="parametric-constraints.html#editing-constrained-geometry">Editing constrained geometry</a></li><li><a href="parametric-constraints.html#persistence-and-history-limitations">Persistence and history limitations</a></li></ul></details>
<details class="current-topic" open><summary><a href="commands-shortcuts.html">Commands and Shortcuts</a></summary><ul><li><a href="commands-shortcuts.html#command-line">Command line</a></li><li><a href="commands-shortcuts.html#discover-commands">Discover commands</a></li><li><a href="commands-shortcuts.html#shortcut-conventions">Shortcut conventions</a></li></ul></details>
<details><summary><a href="automation-plugins.html">Automation and Plugins</a></summary><ul><li><a href="automation-plugins.html#automation-interface">Automation interface</a></li><li><a href="automation-plugins.html#plugins">Plugins</a></li><li><a href="automation-plugins.html#native-and-web-differences">Native and web differences</a></li></ul></details>
<details><summary><a href="troubleshooting-recovery.html">Troubleshooting and Recovery</a></summary><ul><li><a href="troubleshooting-recovery.html#a-drawing-opens-incorrectly">A drawing opens incorrectly</a></li><li><a href="troubleshooting-recovery.html#geometry-is-missing-or-distorted">Geometry is missing or distorted</a></li><li><a href="troubleshooting-recovery.html#rendering-is-incomplete">Rendering is incomplete</a></li><li><a href="troubleshooting-recovery.html#undo-did-not-restore-the-expected-state">Undo did not restore the expected state</a></li><li><a href="troubleshooting-recovery.html#report-a-reproducible-problem">Report a reproducible problem</a></li></ul></details>
</nav>
<main id="main-content">
<div class="content-wrap">
<p class="page-kicker">OpenCAD Studio User Manual</p>
<h1 class="content-title">Commands and Shortcuts</h1>
<h3 id="command-line">Command line</h3>
<p>Enter a command name or alias, then respond to its prompts. Press <code>Escape</code> to cancel the active command and <code>Return</code> to accept an available default. Exact prompt options vary by command and current selection.</p>
<h3 id="discover-commands">Discover commands</h3>
<p>Use command autocomplete and the ribbon to discover available operations. Some commands are one-shot actions; others maintain state across several point, number, keyword, or object-selection inputs.</p>
<h3 id="shortcut-conventions">Shortcut conventions</h3>
<p>Application and operating-system shortcuts can differ between native and web builds. Avoid assigning browser-reserved shortcuts to web-only workflows.</p>
<nav class="page-navigation" aria-label="Previous and next chapter"><a rel="prev" href="parametric-constraints.html">← Parametric Constraints</a><a rel="next" href="automation-plugins.html">Automation and Plugins →</a></nav>
<p class="date-footer">OpenCAD Studio Help · Generated from <code>help/manual.md</code></p>
</div>
</main>
</body>
</html>
+45
View File
@@ -0,0 +1,45 @@
{
"schema_version": 1,
"fallback": "index.html",
"commands": {
"NEW": "files-drawings.html#create-and-open-drawings",
"OPEN": "files-drawings.html#create-and-open-drawings",
"SAVE": "files-drawings.html#save-and-save-as",
"SAVEAS": "files-drawings.html#save-and-save-as",
"RECOVER": "files-drawings.html#recovery-and-backups",
"LINE": "create-edit-geometry.html#basic-drawing-commands",
"CIRCLE": "create-edit-geometry.html#basic-drawing-commands",
"PLINE": "create-edit-geometry.html#basic-drawing-commands",
"ERASE": "create-edit-geometry.html#modify-geometry",
"UNDO": "create-edit-geometry.html#undo-redo-and-oops",
"REDO": "create-edit-geometry.html#undo-redo-and-oops",
"OOPS": "create-edit-geometry.html#undo-redo-and-oops",
"LAYER": "layers-properties.html#layers",
"CHPROP": "layers-properties.html#object-properties",
"BLOCK": "blocks-references.html#create-and-insert-blocks",
"INSERT": "blocks-references.html#create-and-insert-blocks",
"MINSERT": "blocks-references.html#array-inserts",
"DIM": "annotation-dimensions.html#dimensions",
"PLOT": "layouts-plotting.html#page-setup-and-plot-preview",
"EXTRUDE": "3d-modeling.html#solids-and-surfaces",
"REVOLVE": "3d-modeling.html#solids-and-surfaces",
"HCONSTRAINT": "parametric-constraints.html#geometric-constraints",
"VCONSTRAINT": "parametric-constraints.html#geometric-constraints",
"PCONSTRAINT": "parametric-constraints.html#geometric-constraints",
"QCONSTRAINT": "parametric-constraints.html#geometric-constraints",
"ECONSTRAINT": "parametric-constraints.html#geometric-constraints",
"CCONSTRAINT": "parametric-constraints.html#geometric-constraints",
"TCONSTRAINT": "parametric-constraints.html#geometric-constraints"
},
"surfaces": {
"file-open": "files-drawings.html#create-and-open-drawings",
"file-save": "files-drawings.html#save-and-save-as",
"layers": "layers-properties.html#layers",
"properties": "layers-properties.html#object-properties",
"plot": "layouts-plotting.html#page-setup-and-plot-preview",
"plugin-manager": "automation-plugins.html#plugins",
"constraint-conflicts": "parametric-constraints.html#editing-constrained-geometry",
"recovery": "troubleshooting-recovery.html#a-drawing-opens-incorrectly"
}
}
+57
View File
@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="OpenCAD Studio user manual — Create and Edit Geometry">
<title>Create and Edit Geometry — OpenCAD Studio Help</title>
<link rel="stylesheet" href="assets/css/core.css">
<link rel="stylesheet" href="assets/css/opencad.css">
<script src="search-index.js" defer></script>
<script src="assets/js/help.js" defer></script>
</head>
<body>
<header class="topbar">
<button class="nav-toggle" type="button" aria-controls="help-sidebar" aria-expanded="true" title="Toggle chapter navigation"><span aria-hidden="true">☰</span><span class="sr-only">Toggle chapter navigation</span></button>
<a class="brand" href="index.html">OpenCAD Studio Help</a>
<div class="search-shell">
<label class="sr-only" for="help-search">Search help</label>
<input id="help-search" class="help-search" type="search" role="combobox" placeholder="Search commands and topics" autocomplete="off" aria-autocomplete="list" aria-controls="search-results" aria-expanded="false">
<span class="search-key" aria-hidden="true">⌘K</span>
<ul id="search-results" class="search-results" role="listbox" hidden></ul>
</div>
</header>
<nav id="help-sidebar" class="sidebar" aria-label="Help chapters">
<div><a class="chapter-link" href="index.html">Help overview</a></div>
<details><summary><a href="getting-started.html">Getting Started</a></summary><ul><li><a href="getting-started.html#what-opencad-studio-does">What OpenCAD Studio does</a></li><li><a href="getting-started.html#a-first-drawing">A first drawing</a></li><li><a href="getting-started.html#how-to-use-this-manual">How to use this manual</a></li></ul></details>
<details><summary><a href="files-drawings.html">Files and Drawings</a></summary><ul><li><a href="files-drawings.html#create-and-open-drawings">Create and open drawings</a></li><li><a href="files-drawings.html#save-and-save-as">Save and Save As</a></li><li><a href="files-drawings.html#recovery-and-backups">Recovery and backups</a></li><li><a href="files-drawings.html#external-resources">External resources</a></li></ul></details>
<details><summary><a href="workspace-navigation.html">Workspace and Navigation</a></summary><ul><li><a href="workspace-navigation.html#model-space-and-paper-space">Model space and paper space</a></li><li><a href="workspace-navigation.html#pan-zoom-and-orbit">Pan, zoom, and orbit</a></li><li><a href="workspace-navigation.html#multiple-model-viewports">Multiple model viewports</a></li><li><a href="workspace-navigation.html#selection-and-object-snaps">Selection and object snaps</a></li></ul></details>
<details class="current-topic" open><summary><a href="create-edit-geometry.html">Create and Edit Geometry</a></summary><ul><li><a href="create-edit-geometry.html#basic-drawing-commands">Basic drawing commands</a></li><li><a href="create-edit-geometry.html#modify-geometry">Modify geometry</a></li><li><a href="create-edit-geometry.html#grips-and-direct-manipulation">Grips and direct manipulation</a></li><li><a href="create-edit-geometry.html#undo-redo-and-oops">Undo, redo, and OOPS</a></li></ul></details>
<details><summary><a href="layers-properties.html">Layers and Properties</a></summary><ul><li><a href="layers-properties.html#layers">Layers</a></li><li><a href="layers-properties.html#object-properties">Object properties</a></li><li><a href="layers-properties.html#styles">Styles</a></li></ul></details>
<details><summary><a href="blocks-references.html">Blocks and References</a></summary><ul><li><a href="blocks-references.html#create-and-insert-blocks">Create and insert blocks</a></li><li><a href="blocks-references.html#array-inserts">Array inserts</a></li><li><a href="blocks-references.html#attributes-and-external-references">Attributes and external references</a></li></ul></details>
<details><summary><a href="annotation-dimensions.html">Annotation and Dimensions</a></summary><ul><li><a href="annotation-dimensions.html#text-and-annotation">Text and annotation</a></li><li><a href="annotation-dimensions.html#dimensions">Dimensions</a></li><li><a href="annotation-dimensions.html#annotation-scale">Annotation scale</a></li></ul></details>
<details><summary><a href="layouts-plotting.html">Layouts and Plotting</a></summary><ul><li><a href="layouts-plotting.html#layouts-and-viewports">Layouts and viewports</a></li><li><a href="layouts-plotting.html#page-setup-and-plot-preview">Page setup and plot preview</a></li><li><a href="layouts-plotting.html#export-checks">Export checks</a></li></ul></details>
<details><summary><a href="3d-modeling.html">3D Modeling and Visualization</a></summary><ul><li><a href="3d-modeling.html#solids-and-surfaces">Solids and surfaces</a></li><li><a href="3d-modeling.html#visual-styles-and-lighting">Visual styles and lighting</a></li><li><a href="3d-modeling.html#tessellation-quality">Tessellation quality</a></li></ul></details>
<details><summary><a href="parametric-constraints.html">Parametric Constraints</a></summary><ul><li><a href="parametric-constraints.html#geometric-constraints">Geometric constraints</a></li><li><a href="parametric-constraints.html#editing-constrained-geometry">Editing constrained geometry</a></li><li><a href="parametric-constraints.html#persistence-and-history-limitations">Persistence and history limitations</a></li></ul></details>
<details><summary><a href="commands-shortcuts.html">Commands and Shortcuts</a></summary><ul><li><a href="commands-shortcuts.html#command-line">Command line</a></li><li><a href="commands-shortcuts.html#discover-commands">Discover commands</a></li><li><a href="commands-shortcuts.html#shortcut-conventions">Shortcut conventions</a></li></ul></details>
<details><summary><a href="automation-plugins.html">Automation and Plugins</a></summary><ul><li><a href="automation-plugins.html#automation-interface">Automation interface</a></li><li><a href="automation-plugins.html#plugins">Plugins</a></li><li><a href="automation-plugins.html#native-and-web-differences">Native and web differences</a></li></ul></details>
<details><summary><a href="troubleshooting-recovery.html">Troubleshooting and Recovery</a></summary><ul><li><a href="troubleshooting-recovery.html#a-drawing-opens-incorrectly">A drawing opens incorrectly</a></li><li><a href="troubleshooting-recovery.html#geometry-is-missing-or-distorted">Geometry is missing or distorted</a></li><li><a href="troubleshooting-recovery.html#rendering-is-incomplete">Rendering is incomplete</a></li><li><a href="troubleshooting-recovery.html#undo-did-not-restore-the-expected-state">Undo did not restore the expected state</a></li><li><a href="troubleshooting-recovery.html#report-a-reproducible-problem">Report a reproducible problem</a></li></ul></details>
</nav>
<main id="main-content">
<div class="content-wrap">
<p class="page-kicker">OpenCAD Studio User Manual</p>
<h1 class="content-title">Create and Edit Geometry</h1>
<h3 id="basic-drawing-commands">Basic drawing commands</h3>
<p>Create lines, arcs, circles, ellipses, polylines, splines, points, text, hatches, and other supported entities from the ribbon or command line. Follow the active prompt: multi-step commands remain active until they complete or are cancelled.</p>
<h3 id="modify-geometry">Modify geometry</h3>
<p>Common modification workflows include move, copy, rotate, scale, mirror, offset, trim, extend, fillet, chamfer, stretch, join, explode, and erase. Locked layers can prevent modification.</p>
<h3 id="grips-and-direct-manipulation">Grips and direct manipulation</h3>
<p>Select an entity to expose its grips. Dragging a grip previews and then commits a geometric change. Constrained neighboring geometry may update with it.</p>
<h3 id="undo-redo-and-oops">Undo, redo, and OOPS</h3>
<p><strong>Undo</strong> reverses recorded document operations and <strong>Redo</strong> reapplies them. <strong>OOPS</strong> restores the most recently erased entities without reversing later commands. Save before a risky sequence and verify that associated dimensions, hatches, groups, and constraints were restored with their geometry.</p>
<nav class="page-navigation" aria-label="Previous and next chapter"><a rel="prev" href="workspace-navigation.html">← Workspace and Navigation</a><a rel="next" href="layers-properties.html">Layers and Properties →</a></nav>
<p class="date-footer">OpenCAD Studio Help · Generated from <code>help/manual.md</code></p>
</div>
</main>
</body>
</html>
+57
View File
@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="OpenCAD Studio user manual — Files and Drawings">
<title>Files and Drawings — OpenCAD Studio Help</title>
<link rel="stylesheet" href="assets/css/core.css">
<link rel="stylesheet" href="assets/css/opencad.css">
<script src="search-index.js" defer></script>
<script src="assets/js/help.js" defer></script>
</head>
<body>
<header class="topbar">
<button class="nav-toggle" type="button" aria-controls="help-sidebar" aria-expanded="true" title="Toggle chapter navigation"><span aria-hidden="true">☰</span><span class="sr-only">Toggle chapter navigation</span></button>
<a class="brand" href="index.html">OpenCAD Studio Help</a>
<div class="search-shell">
<label class="sr-only" for="help-search">Search help</label>
<input id="help-search" class="help-search" type="search" role="combobox" placeholder="Search commands and topics" autocomplete="off" aria-autocomplete="list" aria-controls="search-results" aria-expanded="false">
<span class="search-key" aria-hidden="true">⌘K</span>
<ul id="search-results" class="search-results" role="listbox" hidden></ul>
</div>
</header>
<nav id="help-sidebar" class="sidebar" aria-label="Help chapters">
<div><a class="chapter-link" href="index.html">Help overview</a></div>
<details><summary><a href="getting-started.html">Getting Started</a></summary><ul><li><a href="getting-started.html#what-opencad-studio-does">What OpenCAD Studio does</a></li><li><a href="getting-started.html#a-first-drawing">A first drawing</a></li><li><a href="getting-started.html#how-to-use-this-manual">How to use this manual</a></li></ul></details>
<details class="current-topic" open><summary><a href="files-drawings.html">Files and Drawings</a></summary><ul><li><a href="files-drawings.html#create-and-open-drawings">Create and open drawings</a></li><li><a href="files-drawings.html#save-and-save-as">Save and Save As</a></li><li><a href="files-drawings.html#recovery-and-backups">Recovery and backups</a></li><li><a href="files-drawings.html#external-resources">External resources</a></li></ul></details>
<details><summary><a href="workspace-navigation.html">Workspace and Navigation</a></summary><ul><li><a href="workspace-navigation.html#model-space-and-paper-space">Model space and paper space</a></li><li><a href="workspace-navigation.html#pan-zoom-and-orbit">Pan, zoom, and orbit</a></li><li><a href="workspace-navigation.html#multiple-model-viewports">Multiple model viewports</a></li><li><a href="workspace-navigation.html#selection-and-object-snaps">Selection and object snaps</a></li></ul></details>
<details><summary><a href="create-edit-geometry.html">Create and Edit Geometry</a></summary><ul><li><a href="create-edit-geometry.html#basic-drawing-commands">Basic drawing commands</a></li><li><a href="create-edit-geometry.html#modify-geometry">Modify geometry</a></li><li><a href="create-edit-geometry.html#grips-and-direct-manipulation">Grips and direct manipulation</a></li><li><a href="create-edit-geometry.html#undo-redo-and-oops">Undo, redo, and OOPS</a></li></ul></details>
<details><summary><a href="layers-properties.html">Layers and Properties</a></summary><ul><li><a href="layers-properties.html#layers">Layers</a></li><li><a href="layers-properties.html#object-properties">Object properties</a></li><li><a href="layers-properties.html#styles">Styles</a></li></ul></details>
<details><summary><a href="blocks-references.html">Blocks and References</a></summary><ul><li><a href="blocks-references.html#create-and-insert-blocks">Create and insert blocks</a></li><li><a href="blocks-references.html#array-inserts">Array inserts</a></li><li><a href="blocks-references.html#attributes-and-external-references">Attributes and external references</a></li></ul></details>
<details><summary><a href="annotation-dimensions.html">Annotation and Dimensions</a></summary><ul><li><a href="annotation-dimensions.html#text-and-annotation">Text and annotation</a></li><li><a href="annotation-dimensions.html#dimensions">Dimensions</a></li><li><a href="annotation-dimensions.html#annotation-scale">Annotation scale</a></li></ul></details>
<details><summary><a href="layouts-plotting.html">Layouts and Plotting</a></summary><ul><li><a href="layouts-plotting.html#layouts-and-viewports">Layouts and viewports</a></li><li><a href="layouts-plotting.html#page-setup-and-plot-preview">Page setup and plot preview</a></li><li><a href="layouts-plotting.html#export-checks">Export checks</a></li></ul></details>
<details><summary><a href="3d-modeling.html">3D Modeling and Visualization</a></summary><ul><li><a href="3d-modeling.html#solids-and-surfaces">Solids and surfaces</a></li><li><a href="3d-modeling.html#visual-styles-and-lighting">Visual styles and lighting</a></li><li><a href="3d-modeling.html#tessellation-quality">Tessellation quality</a></li></ul></details>
<details><summary><a href="parametric-constraints.html">Parametric Constraints</a></summary><ul><li><a href="parametric-constraints.html#geometric-constraints">Geometric constraints</a></li><li><a href="parametric-constraints.html#editing-constrained-geometry">Editing constrained geometry</a></li><li><a href="parametric-constraints.html#persistence-and-history-limitations">Persistence and history limitations</a></li></ul></details>
<details><summary><a href="commands-shortcuts.html">Commands and Shortcuts</a></summary><ul><li><a href="commands-shortcuts.html#command-line">Command line</a></li><li><a href="commands-shortcuts.html#discover-commands">Discover commands</a></li><li><a href="commands-shortcuts.html#shortcut-conventions">Shortcut conventions</a></li></ul></details>
<details><summary><a href="automation-plugins.html">Automation and Plugins</a></summary><ul><li><a href="automation-plugins.html#automation-interface">Automation interface</a></li><li><a href="automation-plugins.html#plugins">Plugins</a></li><li><a href="automation-plugins.html#native-and-web-differences">Native and web differences</a></li></ul></details>
<details><summary><a href="troubleshooting-recovery.html">Troubleshooting and Recovery</a></summary><ul><li><a href="troubleshooting-recovery.html#a-drawing-opens-incorrectly">A drawing opens incorrectly</a></li><li><a href="troubleshooting-recovery.html#geometry-is-missing-or-distorted">Geometry is missing or distorted</a></li><li><a href="troubleshooting-recovery.html#rendering-is-incomplete">Rendering is incomplete</a></li><li><a href="troubleshooting-recovery.html#undo-did-not-restore-the-expected-state">Undo did not restore the expected state</a></li><li><a href="troubleshooting-recovery.html#report-a-reproducible-problem">Report a reproducible problem</a></li></ul></details>
</nav>
<main id="main-content">
<div class="content-wrap">
<p class="page-kicker">OpenCAD Studio User Manual</p>
<h1 class="content-title">Files and Drawings</h1>
<h3 id="create-and-open-drawings">Create and open drawings</h3>
<p>Use <strong>New</strong> for a blank document and <strong>Open</strong> for an existing DWG, DXF, backup, or autosave drawing. After opening a file from another CAD system, inspect its units, layers, blocks, fonts, raster references, layouts, and model extents.</p>
<h3 id="save-and-save-as">Save and Save As</h3>
<p><strong>Save</strong> updates the current drawing. <strong>Save As</strong> changes its path, format, or version. Prefer Save As when validating interoperability or recovering an older drawing. A successful write does not replace visual inspection of a reopened copy.</p>
<h3 id="recovery-and-backups">Recovery and backups</h3>
<p>OpenCAD Studio recognizes drawing backups and autosave-style files where the underlying data is DWG or DXF. If strict loading rejects malformed entities, use the recovery workflow and review what was discarded before continuing work.</p>
<h3 id="external-resources">External resources</h3>
<p>Raster images, underlays, fonts, materials, and external references may depend on paths outside the drawing. Keep referenced files together when moving a project and verify that relative paths still resolve.</p>
<nav class="page-navigation" aria-label="Previous and next chapter"><a rel="prev" href="getting-started.html">← Getting Started</a><a rel="next" href="workspace-navigation.html">Workspace and Navigation →</a></nav>
<p class="date-footer">OpenCAD Studio Help · Generated from <code>help/manual.md</code></p>
</div>
</main>
</body>
</html>
+64
View File
@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="OpenCAD Studio user manual — Getting Started">
<title>Getting Started — OpenCAD Studio Help</title>
<link rel="stylesheet" href="assets/css/core.css">
<link rel="stylesheet" href="assets/css/opencad.css">
<script src="search-index.js" defer></script>
<script src="assets/js/help.js" defer></script>
</head>
<body>
<header class="topbar">
<button class="nav-toggle" type="button" aria-controls="help-sidebar" aria-expanded="true" title="Toggle chapter navigation"><span aria-hidden="true">☰</span><span class="sr-only">Toggle chapter navigation</span></button>
<a class="brand" href="index.html">OpenCAD Studio Help</a>
<div class="search-shell">
<label class="sr-only" for="help-search">Search help</label>
<input id="help-search" class="help-search" type="search" role="combobox" placeholder="Search commands and topics" autocomplete="off" aria-autocomplete="list" aria-controls="search-results" aria-expanded="false">
<span class="search-key" aria-hidden="true">⌘K</span>
<ul id="search-results" class="search-results" role="listbox" hidden></ul>
</div>
</header>
<nav id="help-sidebar" class="sidebar" aria-label="Help chapters">
<div><a class="chapter-link" href="index.html">Help overview</a></div>
<details class="current-topic" open><summary><a href="getting-started.html">Getting Started</a></summary><ul><li><a href="getting-started.html#what-opencad-studio-does">What OpenCAD Studio does</a></li><li><a href="getting-started.html#a-first-drawing">A first drawing</a></li><li><a href="getting-started.html#how-to-use-this-manual">How to use this manual</a></li></ul></details>
<details><summary><a href="files-drawings.html">Files and Drawings</a></summary><ul><li><a href="files-drawings.html#create-and-open-drawings">Create and open drawings</a></li><li><a href="files-drawings.html#save-and-save-as">Save and Save As</a></li><li><a href="files-drawings.html#recovery-and-backups">Recovery and backups</a></li><li><a href="files-drawings.html#external-resources">External resources</a></li></ul></details>
<details><summary><a href="workspace-navigation.html">Workspace and Navigation</a></summary><ul><li><a href="workspace-navigation.html#model-space-and-paper-space">Model space and paper space</a></li><li><a href="workspace-navigation.html#pan-zoom-and-orbit">Pan, zoom, and orbit</a></li><li><a href="workspace-navigation.html#multiple-model-viewports">Multiple model viewports</a></li><li><a href="workspace-navigation.html#selection-and-object-snaps">Selection and object snaps</a></li></ul></details>
<details><summary><a href="create-edit-geometry.html">Create and Edit Geometry</a></summary><ul><li><a href="create-edit-geometry.html#basic-drawing-commands">Basic drawing commands</a></li><li><a href="create-edit-geometry.html#modify-geometry">Modify geometry</a></li><li><a href="create-edit-geometry.html#grips-and-direct-manipulation">Grips and direct manipulation</a></li><li><a href="create-edit-geometry.html#undo-redo-and-oops">Undo, redo, and OOPS</a></li></ul></details>
<details><summary><a href="layers-properties.html">Layers and Properties</a></summary><ul><li><a href="layers-properties.html#layers">Layers</a></li><li><a href="layers-properties.html#object-properties">Object properties</a></li><li><a href="layers-properties.html#styles">Styles</a></li></ul></details>
<details><summary><a href="blocks-references.html">Blocks and References</a></summary><ul><li><a href="blocks-references.html#create-and-insert-blocks">Create and insert blocks</a></li><li><a href="blocks-references.html#array-inserts">Array inserts</a></li><li><a href="blocks-references.html#attributes-and-external-references">Attributes and external references</a></li></ul></details>
<details><summary><a href="annotation-dimensions.html">Annotation and Dimensions</a></summary><ul><li><a href="annotation-dimensions.html#text-and-annotation">Text and annotation</a></li><li><a href="annotation-dimensions.html#dimensions">Dimensions</a></li><li><a href="annotation-dimensions.html#annotation-scale">Annotation scale</a></li></ul></details>
<details><summary><a href="layouts-plotting.html">Layouts and Plotting</a></summary><ul><li><a href="layouts-plotting.html#layouts-and-viewports">Layouts and viewports</a></li><li><a href="layouts-plotting.html#page-setup-and-plot-preview">Page setup and plot preview</a></li><li><a href="layouts-plotting.html#export-checks">Export checks</a></li></ul></details>
<details><summary><a href="3d-modeling.html">3D Modeling and Visualization</a></summary><ul><li><a href="3d-modeling.html#solids-and-surfaces">Solids and surfaces</a></li><li><a href="3d-modeling.html#visual-styles-and-lighting">Visual styles and lighting</a></li><li><a href="3d-modeling.html#tessellation-quality">Tessellation quality</a></li></ul></details>
<details><summary><a href="parametric-constraints.html">Parametric Constraints</a></summary><ul><li><a href="parametric-constraints.html#geometric-constraints">Geometric constraints</a></li><li><a href="parametric-constraints.html#editing-constrained-geometry">Editing constrained geometry</a></li><li><a href="parametric-constraints.html#persistence-and-history-limitations">Persistence and history limitations</a></li></ul></details>
<details><summary><a href="commands-shortcuts.html">Commands and Shortcuts</a></summary><ul><li><a href="commands-shortcuts.html#command-line">Command line</a></li><li><a href="commands-shortcuts.html#discover-commands">Discover commands</a></li><li><a href="commands-shortcuts.html#shortcut-conventions">Shortcut conventions</a></li></ul></details>
<details><summary><a href="automation-plugins.html">Automation and Plugins</a></summary><ul><li><a href="automation-plugins.html#automation-interface">Automation interface</a></li><li><a href="automation-plugins.html#plugins">Plugins</a></li><li><a href="automation-plugins.html#native-and-web-differences">Native and web differences</a></li></ul></details>
<details><summary><a href="troubleshooting-recovery.html">Troubleshooting and Recovery</a></summary><ul><li><a href="troubleshooting-recovery.html#a-drawing-opens-incorrectly">A drawing opens incorrectly</a></li><li><a href="troubleshooting-recovery.html#geometry-is-missing-or-distorted">Geometry is missing or distorted</a></li><li><a href="troubleshooting-recovery.html#rendering-is-incomplete">Rendering is incomplete</a></li><li><a href="troubleshooting-recovery.html#undo-did-not-restore-the-expected-state">Undo did not restore the expected state</a></li><li><a href="troubleshooting-recovery.html#report-a-reproducible-problem">Report a reproducible problem</a></li></ul></details>
</nav>
<main id="main-content">
<div class="content-wrap">
<p class="page-kicker">OpenCAD Studio User Manual</p>
<h1 class="content-title">Getting Started</h1>
<h3 id="what-opencad-studio-does">What OpenCAD Studio does</h3>
<p>OpenCAD Studio is a desktop and web CAD application for opening, creating, editing, visualizing, and saving two- and three-dimensional drawings. It works with DWG and DXF drawings and provides command-line, ribbon, automation, and plugin-oriented workflows.</p>
<h3 id="a-first-drawing">A first drawing</h3>
<ol type="1">
<li>Create a new drawing or open an existing DWG or DXF file.</li>
<li>Confirm the active workspace, units, layer, and object-snap settings.</li>
<li>Create geometry with a drawing command such as <strong>Line</strong>, <strong>Circle</strong>, or <strong>Polyline</strong>.</li>
<li>Edit the result with grips or a modification command.</li>
<li>Save the drawing under a new name until its compatibility has been verified.</li>
</ol>
<blockquote>
<p><strong>Data safety:</strong> Keep an untouched copy of important third-party drawings. Confirm geometry, layouts, external references, and plotted output before replacing the original file.</p>
</blockquote>
<h3 id="how-to-use-this-manual">How to use this manual</h3>
<p>Use the chapter navigation for broad workflows, the search box for a command or concept, and the previous/next links for sequential reading. Command names, system variables, keys, and file extensions appear in <code>monospaced text</code>.</p>
<nav class="page-navigation" aria-label="Previous and next chapter"><a rel="prev" href="index.html">← Help overview</a><a rel="next" href="files-drawings.html">Files and Drawings →</a></nav>
<p class="date-footer">OpenCAD Studio Help · Generated from <code>help/manual.md</code></p>
</div>
</main>
</body>
</html>
+61
View File
@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="OpenCAD Studio user manual — Help overview">
<title>Help overview — OpenCAD Studio Help</title>
<link rel="stylesheet" href="assets/css/core.css">
<link rel="stylesheet" href="assets/css/opencad.css">
<script src="search-index.js" defer></script>
<script src="assets/js/help.js" defer></script>
</head>
<body>
<header class="topbar">
<button class="nav-toggle" type="button" aria-controls="help-sidebar" aria-expanded="true" title="Toggle chapter navigation"><span aria-hidden="true">☰</span><span class="sr-only">Toggle chapter navigation</span></button>
<a class="brand" href="index.html">OpenCAD Studio Help</a>
<div class="search-shell">
<label class="sr-only" for="help-search">Search help</label>
<input id="help-search" class="help-search" type="search" role="combobox" placeholder="Search commands and topics" autocomplete="off" aria-autocomplete="list" aria-controls="search-results" aria-expanded="false">
<span class="search-key" aria-hidden="true">⌘K</span>
<ul id="search-results" class="search-results" role="listbox" hidden></ul>
</div>
</header>
<nav id="help-sidebar" class="sidebar" aria-label="Help chapters">
<div class="current-topic"><a class="chapter-link" href="index.html">Help overview</a></div>
<details><summary><a href="getting-started.html">Getting Started</a></summary><ul><li><a href="getting-started.html#what-opencad-studio-does">What OpenCAD Studio does</a></li><li><a href="getting-started.html#a-first-drawing">A first drawing</a></li><li><a href="getting-started.html#how-to-use-this-manual">How to use this manual</a></li></ul></details>
<details><summary><a href="files-drawings.html">Files and Drawings</a></summary><ul><li><a href="files-drawings.html#create-and-open-drawings">Create and open drawings</a></li><li><a href="files-drawings.html#save-and-save-as">Save and Save As</a></li><li><a href="files-drawings.html#recovery-and-backups">Recovery and backups</a></li><li><a href="files-drawings.html#external-resources">External resources</a></li></ul></details>
<details><summary><a href="workspace-navigation.html">Workspace and Navigation</a></summary><ul><li><a href="workspace-navigation.html#model-space-and-paper-space">Model space and paper space</a></li><li><a href="workspace-navigation.html#pan-zoom-and-orbit">Pan, zoom, and orbit</a></li><li><a href="workspace-navigation.html#multiple-model-viewports">Multiple model viewports</a></li><li><a href="workspace-navigation.html#selection-and-object-snaps">Selection and object snaps</a></li></ul></details>
<details><summary><a href="create-edit-geometry.html">Create and Edit Geometry</a></summary><ul><li><a href="create-edit-geometry.html#basic-drawing-commands">Basic drawing commands</a></li><li><a href="create-edit-geometry.html#modify-geometry">Modify geometry</a></li><li><a href="create-edit-geometry.html#grips-and-direct-manipulation">Grips and direct manipulation</a></li><li><a href="create-edit-geometry.html#undo-redo-and-oops">Undo, redo, and OOPS</a></li></ul></details>
<details><summary><a href="layers-properties.html">Layers and Properties</a></summary><ul><li><a href="layers-properties.html#layers">Layers</a></li><li><a href="layers-properties.html#object-properties">Object properties</a></li><li><a href="layers-properties.html#styles">Styles</a></li></ul></details>
<details><summary><a href="blocks-references.html">Blocks and References</a></summary><ul><li><a href="blocks-references.html#create-and-insert-blocks">Create and insert blocks</a></li><li><a href="blocks-references.html#array-inserts">Array inserts</a></li><li><a href="blocks-references.html#attributes-and-external-references">Attributes and external references</a></li></ul></details>
<details><summary><a href="annotation-dimensions.html">Annotation and Dimensions</a></summary><ul><li><a href="annotation-dimensions.html#text-and-annotation">Text and annotation</a></li><li><a href="annotation-dimensions.html#dimensions">Dimensions</a></li><li><a href="annotation-dimensions.html#annotation-scale">Annotation scale</a></li></ul></details>
<details><summary><a href="layouts-plotting.html">Layouts and Plotting</a></summary><ul><li><a href="layouts-plotting.html#layouts-and-viewports">Layouts and viewports</a></li><li><a href="layouts-plotting.html#page-setup-and-plot-preview">Page setup and plot preview</a></li><li><a href="layouts-plotting.html#export-checks">Export checks</a></li></ul></details>
<details><summary><a href="3d-modeling.html">3D Modeling and Visualization</a></summary><ul><li><a href="3d-modeling.html#solids-and-surfaces">Solids and surfaces</a></li><li><a href="3d-modeling.html#visual-styles-and-lighting">Visual styles and lighting</a></li><li><a href="3d-modeling.html#tessellation-quality">Tessellation quality</a></li></ul></details>
<details><summary><a href="parametric-constraints.html">Parametric Constraints</a></summary><ul><li><a href="parametric-constraints.html#geometric-constraints">Geometric constraints</a></li><li><a href="parametric-constraints.html#editing-constrained-geometry">Editing constrained geometry</a></li><li><a href="parametric-constraints.html#persistence-and-history-limitations">Persistence and history limitations</a></li></ul></details>
<details><summary><a href="commands-shortcuts.html">Commands and Shortcuts</a></summary><ul><li><a href="commands-shortcuts.html#command-line">Command line</a></li><li><a href="commands-shortcuts.html#discover-commands">Discover commands</a></li><li><a href="commands-shortcuts.html#shortcut-conventions">Shortcut conventions</a></li></ul></details>
<details><summary><a href="automation-plugins.html">Automation and Plugins</a></summary><ul><li><a href="automation-plugins.html#automation-interface">Automation interface</a></li><li><a href="automation-plugins.html#plugins">Plugins</a></li><li><a href="automation-plugins.html#native-and-web-differences">Native and web differences</a></li></ul></details>
<details><summary><a href="troubleshooting-recovery.html">Troubleshooting and Recovery</a></summary><ul><li><a href="troubleshooting-recovery.html#a-drawing-opens-incorrectly">A drawing opens incorrectly</a></li><li><a href="troubleshooting-recovery.html#geometry-is-missing-or-distorted">Geometry is missing or distorted</a></li><li><a href="troubleshooting-recovery.html#rendering-is-incomplete">Rendering is incomplete</a></li><li><a href="troubleshooting-recovery.html#undo-did-not-restore-the-expected-state">Undo did not restore the expected state</a></li><li><a href="troubleshooting-recovery.html#report-a-reproducible-problem">Report a reproducible problem</a></li></ul></details>
</nav>
<main id="main-content">
<div class="content-wrap">
<p class="page-kicker">OpenCAD Studio User Manual</p>
<h1 class="content-title">Help overview</h1>
<section class="hero">
<p class="eyebrow">OpenCAD Studio User Manual</p>
<h2>Find the workflow, command, or concept you need</h2>
<p>Learn how to create and edit drawings, exchange DWG and DXF files, prepare layouts, model in 3D, use constraints, automate work, and recover from common problems.</p>
</section>
<div class="quick-paths" aria-label="Common help paths">
<a href="getting-started.html#a-first-drawing">Create a first drawing</a>
<a href="files-drawings.html#recovery-and-backups">Recover a drawing</a>
<a href="troubleshooting-recovery.html#report-a-reproducible-problem">Report a problem</a>
</div>
<h2 id="manual-chapters">Manual chapters</h2>
<div class="chapter-grid"><a class="chapter-card" href="getting-started.html"><strong>Getting Started</strong><span>Begin a drawing safely and learn how to use this manual.</span></a><a class="chapter-card" href="files-drawings.html"><strong>Files and Drawings</strong><span>Open, save, recover, and move drawings with their dependencies.</span></a><a class="chapter-card" href="workspace-navigation.html"><strong>Workspace and Navigation</strong><span>Work with spaces, views, selection, and object snaps.</span></a><a class="chapter-card" href="create-edit-geometry.html"><strong>Create and Edit Geometry</strong><span>Draw, modify, grip-edit, erase, and restore geometry.</span></a><a class="chapter-card" href="layers-properties.html"><strong>Layers and Properties</strong><span>Organize objects and control their inherited appearance.</span></a><a class="chapter-card" href="blocks-references.html"><strong>Blocks and References</strong><span>Create reusable content, arrays, attributes, and file references.</span></a><a class="chapter-card" href="annotation-dimensions.html"><strong>Annotation and Dimensions</strong><span>Add readable text, dimensions, leaders, and scaled annotation.</span></a><a class="chapter-card" href="layouts-plotting.html"><strong>Layouts and Plotting</strong><span>Prepare paper-space sheets and verify plotted output.</span></a><a class="chapter-card" href="3d-modeling.html"><strong>3D Modeling and Visualization</strong><span>Create and inspect solids, surfaces, materials, and visual styles.</span></a><a class="chapter-card" href="parametric-constraints.html"><strong>Parametric Constraints</strong><span>Capture geometric intent and diagnose solve or history issues.</span></a><a class="chapter-card" href="commands-shortcuts.html"><strong>Commands and Shortcuts</strong><span>Use prompts, autocomplete, aliases, and keyboard conventions.</span></a><a class="chapter-card" href="automation-plugins.html"><strong>Automation and Plugins</strong><span>Control drawings programmatically and extend the native app.</span></a><a class="chapter-card" href="troubleshooting-recovery.html"><strong>Troubleshooting and Recovery</strong><span>Diagnose file, geometry, rendering, and history problems.</span></a></div>
<nav class="page-navigation" aria-label="Previous and next chapter"><a rel="next" href="getting-started.html">Getting Started →</a></nav>
<p class="date-footer">OpenCAD Studio Help · Generated from <code>help/manual.md</code></p>
</div>
</main>
</body>
</html>
+55
View File
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="OpenCAD Studio user manual — Layers and Properties">
<title>Layers and Properties — OpenCAD Studio Help</title>
<link rel="stylesheet" href="assets/css/core.css">
<link rel="stylesheet" href="assets/css/opencad.css">
<script src="search-index.js" defer></script>
<script src="assets/js/help.js" defer></script>
</head>
<body>
<header class="topbar">
<button class="nav-toggle" type="button" aria-controls="help-sidebar" aria-expanded="true" title="Toggle chapter navigation"><span aria-hidden="true">☰</span><span class="sr-only">Toggle chapter navigation</span></button>
<a class="brand" href="index.html">OpenCAD Studio Help</a>
<div class="search-shell">
<label class="sr-only" for="help-search">Search help</label>
<input id="help-search" class="help-search" type="search" role="combobox" placeholder="Search commands and topics" autocomplete="off" aria-autocomplete="list" aria-controls="search-results" aria-expanded="false">
<span class="search-key" aria-hidden="true">⌘K</span>
<ul id="search-results" class="search-results" role="listbox" hidden></ul>
</div>
</header>
<nav id="help-sidebar" class="sidebar" aria-label="Help chapters">
<div><a class="chapter-link" href="index.html">Help overview</a></div>
<details><summary><a href="getting-started.html">Getting Started</a></summary><ul><li><a href="getting-started.html#what-opencad-studio-does">What OpenCAD Studio does</a></li><li><a href="getting-started.html#a-first-drawing">A first drawing</a></li><li><a href="getting-started.html#how-to-use-this-manual">How to use this manual</a></li></ul></details>
<details><summary><a href="files-drawings.html">Files and Drawings</a></summary><ul><li><a href="files-drawings.html#create-and-open-drawings">Create and open drawings</a></li><li><a href="files-drawings.html#save-and-save-as">Save and Save As</a></li><li><a href="files-drawings.html#recovery-and-backups">Recovery and backups</a></li><li><a href="files-drawings.html#external-resources">External resources</a></li></ul></details>
<details><summary><a href="workspace-navigation.html">Workspace and Navigation</a></summary><ul><li><a href="workspace-navigation.html#model-space-and-paper-space">Model space and paper space</a></li><li><a href="workspace-navigation.html#pan-zoom-and-orbit">Pan, zoom, and orbit</a></li><li><a href="workspace-navigation.html#multiple-model-viewports">Multiple model viewports</a></li><li><a href="workspace-navigation.html#selection-and-object-snaps">Selection and object snaps</a></li></ul></details>
<details><summary><a href="create-edit-geometry.html">Create and Edit Geometry</a></summary><ul><li><a href="create-edit-geometry.html#basic-drawing-commands">Basic drawing commands</a></li><li><a href="create-edit-geometry.html#modify-geometry">Modify geometry</a></li><li><a href="create-edit-geometry.html#grips-and-direct-manipulation">Grips and direct manipulation</a></li><li><a href="create-edit-geometry.html#undo-redo-and-oops">Undo, redo, and OOPS</a></li></ul></details>
<details class="current-topic" open><summary><a href="layers-properties.html">Layers and Properties</a></summary><ul><li><a href="layers-properties.html#layers">Layers</a></li><li><a href="layers-properties.html#object-properties">Object properties</a></li><li><a href="layers-properties.html#styles">Styles</a></li></ul></details>
<details><summary><a href="blocks-references.html">Blocks and References</a></summary><ul><li><a href="blocks-references.html#create-and-insert-blocks">Create and insert blocks</a></li><li><a href="blocks-references.html#array-inserts">Array inserts</a></li><li><a href="blocks-references.html#attributes-and-external-references">Attributes and external references</a></li></ul></details>
<details><summary><a href="annotation-dimensions.html">Annotation and Dimensions</a></summary><ul><li><a href="annotation-dimensions.html#text-and-annotation">Text and annotation</a></li><li><a href="annotation-dimensions.html#dimensions">Dimensions</a></li><li><a href="annotation-dimensions.html#annotation-scale">Annotation scale</a></li></ul></details>
<details><summary><a href="layouts-plotting.html">Layouts and Plotting</a></summary><ul><li><a href="layouts-plotting.html#layouts-and-viewports">Layouts and viewports</a></li><li><a href="layouts-plotting.html#page-setup-and-plot-preview">Page setup and plot preview</a></li><li><a href="layouts-plotting.html#export-checks">Export checks</a></li></ul></details>
<details><summary><a href="3d-modeling.html">3D Modeling and Visualization</a></summary><ul><li><a href="3d-modeling.html#solids-and-surfaces">Solids and surfaces</a></li><li><a href="3d-modeling.html#visual-styles-and-lighting">Visual styles and lighting</a></li><li><a href="3d-modeling.html#tessellation-quality">Tessellation quality</a></li></ul></details>
<details><summary><a href="parametric-constraints.html">Parametric Constraints</a></summary><ul><li><a href="parametric-constraints.html#geometric-constraints">Geometric constraints</a></li><li><a href="parametric-constraints.html#editing-constrained-geometry">Editing constrained geometry</a></li><li><a href="parametric-constraints.html#persistence-and-history-limitations">Persistence and history limitations</a></li></ul></details>
<details><summary><a href="commands-shortcuts.html">Commands and Shortcuts</a></summary><ul><li><a href="commands-shortcuts.html#command-line">Command line</a></li><li><a href="commands-shortcuts.html#discover-commands">Discover commands</a></li><li><a href="commands-shortcuts.html#shortcut-conventions">Shortcut conventions</a></li></ul></details>
<details><summary><a href="automation-plugins.html">Automation and Plugins</a></summary><ul><li><a href="automation-plugins.html#automation-interface">Automation interface</a></li><li><a href="automation-plugins.html#plugins">Plugins</a></li><li><a href="automation-plugins.html#native-and-web-differences">Native and web differences</a></li></ul></details>
<details><summary><a href="troubleshooting-recovery.html">Troubleshooting and Recovery</a></summary><ul><li><a href="troubleshooting-recovery.html#a-drawing-opens-incorrectly">A drawing opens incorrectly</a></li><li><a href="troubleshooting-recovery.html#geometry-is-missing-or-distorted">Geometry is missing or distorted</a></li><li><a href="troubleshooting-recovery.html#rendering-is-incomplete">Rendering is incomplete</a></li><li><a href="troubleshooting-recovery.html#undo-did-not-restore-the-expected-state">Undo did not restore the expected state</a></li><li><a href="troubleshooting-recovery.html#report-a-reproducible-problem">Report a reproducible problem</a></li></ul></details>
</nav>
<main id="main-content">
<div class="content-wrap">
<p class="page-kicker">OpenCAD Studio User Manual</p>
<h1 class="content-title">Layers and Properties</h1>
<h3 id="layers">Layers</h3>
<p>Layers organize geometry and control visibility, locking, color, line type, line weight, and plot behavior. Set the intended active layer before creating objects. Avoid editing objects on locked layers.</p>
<h3 id="object-properties">Object properties</h3>
<p>Properties can be assigned explicitly or inherited <strong>ByLayer</strong> or <strong>ByBlock</strong>. Use the Properties surface or commands such as <code>CHPROP</code> to inspect and change selected objects.</p>
<h3 id="styles">Styles</h3>
<p>Text, dimension, multiline, table, and visual styles centralize presentation. Changing a shared style can update many objects, so inspect affected layouts before saving.</p>
<nav class="page-navigation" aria-label="Previous and next chapter"><a rel="prev" href="create-edit-geometry.html">← Create and Edit Geometry</a><a rel="next" href="blocks-references.html">Blocks and References →</a></nav>
<p class="date-footer">OpenCAD Studio Help · Generated from <code>help/manual.md</code></p>
</div>
</main>
</body>
</html>
+55
View File
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="OpenCAD Studio user manual — Layouts and Plotting">
<title>Layouts and Plotting — OpenCAD Studio Help</title>
<link rel="stylesheet" href="assets/css/core.css">
<link rel="stylesheet" href="assets/css/opencad.css">
<script src="search-index.js" defer></script>
<script src="assets/js/help.js" defer></script>
</head>
<body>
<header class="topbar">
<button class="nav-toggle" type="button" aria-controls="help-sidebar" aria-expanded="true" title="Toggle chapter navigation"><span aria-hidden="true">☰</span><span class="sr-only">Toggle chapter navigation</span></button>
<a class="brand" href="index.html">OpenCAD Studio Help</a>
<div class="search-shell">
<label class="sr-only" for="help-search">Search help</label>
<input id="help-search" class="help-search" type="search" role="combobox" placeholder="Search commands and topics" autocomplete="off" aria-autocomplete="list" aria-controls="search-results" aria-expanded="false">
<span class="search-key" aria-hidden="true">⌘K</span>
<ul id="search-results" class="search-results" role="listbox" hidden></ul>
</div>
</header>
<nav id="help-sidebar" class="sidebar" aria-label="Help chapters">
<div><a class="chapter-link" href="index.html">Help overview</a></div>
<details><summary><a href="getting-started.html">Getting Started</a></summary><ul><li><a href="getting-started.html#what-opencad-studio-does">What OpenCAD Studio does</a></li><li><a href="getting-started.html#a-first-drawing">A first drawing</a></li><li><a href="getting-started.html#how-to-use-this-manual">How to use this manual</a></li></ul></details>
<details><summary><a href="files-drawings.html">Files and Drawings</a></summary><ul><li><a href="files-drawings.html#create-and-open-drawings">Create and open drawings</a></li><li><a href="files-drawings.html#save-and-save-as">Save and Save As</a></li><li><a href="files-drawings.html#recovery-and-backups">Recovery and backups</a></li><li><a href="files-drawings.html#external-resources">External resources</a></li></ul></details>
<details><summary><a href="workspace-navigation.html">Workspace and Navigation</a></summary><ul><li><a href="workspace-navigation.html#model-space-and-paper-space">Model space and paper space</a></li><li><a href="workspace-navigation.html#pan-zoom-and-orbit">Pan, zoom, and orbit</a></li><li><a href="workspace-navigation.html#multiple-model-viewports">Multiple model viewports</a></li><li><a href="workspace-navigation.html#selection-and-object-snaps">Selection and object snaps</a></li></ul></details>
<details><summary><a href="create-edit-geometry.html">Create and Edit Geometry</a></summary><ul><li><a href="create-edit-geometry.html#basic-drawing-commands">Basic drawing commands</a></li><li><a href="create-edit-geometry.html#modify-geometry">Modify geometry</a></li><li><a href="create-edit-geometry.html#grips-and-direct-manipulation">Grips and direct manipulation</a></li><li><a href="create-edit-geometry.html#undo-redo-and-oops">Undo, redo, and OOPS</a></li></ul></details>
<details><summary><a href="layers-properties.html">Layers and Properties</a></summary><ul><li><a href="layers-properties.html#layers">Layers</a></li><li><a href="layers-properties.html#object-properties">Object properties</a></li><li><a href="layers-properties.html#styles">Styles</a></li></ul></details>
<details><summary><a href="blocks-references.html">Blocks and References</a></summary><ul><li><a href="blocks-references.html#create-and-insert-blocks">Create and insert blocks</a></li><li><a href="blocks-references.html#array-inserts">Array inserts</a></li><li><a href="blocks-references.html#attributes-and-external-references">Attributes and external references</a></li></ul></details>
<details><summary><a href="annotation-dimensions.html">Annotation and Dimensions</a></summary><ul><li><a href="annotation-dimensions.html#text-and-annotation">Text and annotation</a></li><li><a href="annotation-dimensions.html#dimensions">Dimensions</a></li><li><a href="annotation-dimensions.html#annotation-scale">Annotation scale</a></li></ul></details>
<details class="current-topic" open><summary><a href="layouts-plotting.html">Layouts and Plotting</a></summary><ul><li><a href="layouts-plotting.html#layouts-and-viewports">Layouts and viewports</a></li><li><a href="layouts-plotting.html#page-setup-and-plot-preview">Page setup and plot preview</a></li><li><a href="layouts-plotting.html#export-checks">Export checks</a></li></ul></details>
<details><summary><a href="3d-modeling.html">3D Modeling and Visualization</a></summary><ul><li><a href="3d-modeling.html#solids-and-surfaces">Solids and surfaces</a></li><li><a href="3d-modeling.html#visual-styles-and-lighting">Visual styles and lighting</a></li><li><a href="3d-modeling.html#tessellation-quality">Tessellation quality</a></li></ul></details>
<details><summary><a href="parametric-constraints.html">Parametric Constraints</a></summary><ul><li><a href="parametric-constraints.html#geometric-constraints">Geometric constraints</a></li><li><a href="parametric-constraints.html#editing-constrained-geometry">Editing constrained geometry</a></li><li><a href="parametric-constraints.html#persistence-and-history-limitations">Persistence and history limitations</a></li></ul></details>
<details><summary><a href="commands-shortcuts.html">Commands and Shortcuts</a></summary><ul><li><a href="commands-shortcuts.html#command-line">Command line</a></li><li><a href="commands-shortcuts.html#discover-commands">Discover commands</a></li><li><a href="commands-shortcuts.html#shortcut-conventions">Shortcut conventions</a></li></ul></details>
<details><summary><a href="automation-plugins.html">Automation and Plugins</a></summary><ul><li><a href="automation-plugins.html#automation-interface">Automation interface</a></li><li><a href="automation-plugins.html#plugins">Plugins</a></li><li><a href="automation-plugins.html#native-and-web-differences">Native and web differences</a></li></ul></details>
<details><summary><a href="troubleshooting-recovery.html">Troubleshooting and Recovery</a></summary><ul><li><a href="troubleshooting-recovery.html#a-drawing-opens-incorrectly">A drawing opens incorrectly</a></li><li><a href="troubleshooting-recovery.html#geometry-is-missing-or-distorted">Geometry is missing or distorted</a></li><li><a href="troubleshooting-recovery.html#rendering-is-incomplete">Rendering is incomplete</a></li><li><a href="troubleshooting-recovery.html#undo-did-not-restore-the-expected-state">Undo did not restore the expected state</a></li><li><a href="troubleshooting-recovery.html#report-a-reproducible-problem">Report a reproducible problem</a></li></ul></details>
</nav>
<main id="main-content">
<div class="content-wrap">
<p class="page-kicker">OpenCAD Studio User Manual</p>
<h1 class="content-title">Layouts and Plotting</h1>
<h3 id="layouts-and-viewports">Layouts and viewports</h3>
<p>Layouts represent sheets. Paper-space viewports expose model-space views at controlled scales. Lock finished viewports to avoid accidental view changes.</p>
<h3 id="page-setup-and-plot-preview">Page setup and plot preview</h3>
<p>Choose the target device or file format, paper size, orientation, plot area, scale, style behavior, and output quality. Use preview to catch clipping, incorrect line weights, hidden layers, or an unintended plot area.</p>
<h3 id="export-checks">Export checks</h3>
<p>Before distributing a PDF or plotted file, compare it with the drawing at the intended sheet size and verify text, dimensions, raster content, transparency, line types, and viewport scales.</p>
<nav class="page-navigation" aria-label="Previous and next chapter"><a rel="prev" href="annotation-dimensions.html">← Annotation and Dimensions</a><a rel="next" href="3d-modeling.html">3D Modeling and Visualization →</a></nav>
<p class="date-footer">OpenCAD Studio Help · Generated from <code>help/manual.md</code></p>
</div>
</main>
</body>
</html>
+305
View File
@@ -0,0 +1,305 @@
# OpenCAD Studio User Manual
This manual describes the user-facing workflows of OpenCAD Studio. The initial
version establishes the help structure and high-level guidance; individual
commands should be expanded and verified against released behavior before the
manual is published as complete.
## Getting Started
### What OpenCAD Studio does
OpenCAD Studio is a desktop and web CAD application for opening, creating,
editing, visualizing, and saving two- and three-dimensional drawings. It works
with DWG and DXF drawings and provides command-line, ribbon, automation, and
plugin-oriented workflows.
### A first drawing
1. Create a new drawing or open an existing DWG or DXF file.
2. Confirm the active workspace, units, layer, and object-snap settings.
3. Create geometry with a drawing command such as **Line**, **Circle**, or
**Polyline**.
4. Edit the result with grips or a modification command.
5. Save the drawing under a new name until its compatibility has been verified.
> **Data safety:** Keep an untouched copy of important third-party drawings.
> Confirm geometry, layouts, external references, and plotted output before
> replacing the original file.
### How to use this manual
Use the chapter navigation for broad workflows, the search box for a command or
concept, and the previous/next links for sequential reading. Command names,
system variables, keys, and file extensions appear in `monospaced text`.
## Files and Drawings
### Create and open drawings
Use **New** for a blank document and **Open** for an existing DWG, DXF, backup,
or autosave drawing. After opening a file from another CAD system, inspect its
units, layers, blocks, fonts, raster references, layouts, and model extents.
### Save and Save As
**Save** updates the current drawing. **Save As** changes its path, format, or
version. Prefer Save As when validating interoperability or recovering an older
drawing. A successful write does not replace visual inspection of a reopened
copy.
### Recovery and backups
OpenCAD Studio recognizes drawing backups and autosave-style files where the
underlying data is DWG or DXF. If strict loading rejects malformed entities, use
the recovery workflow and review what was discarded before continuing work.
### External resources
Raster images, underlays, fonts, materials, and external references may depend
on paths outside the drawing. Keep referenced files together when moving a
project and verify that relative paths still resolve.
## Workspace and Navigation
### Model space and paper space
Model space is the primary drawing environment. Paper-space layouts arrange
views for plotting through layout viewports. Always confirm the active space
before drawing or editing.
### Pan, zoom, and orbit
Use pan and zoom to navigate without changing drawing geometry. Use orbit and
view controls for three-dimensional inspection. **Zoom Extents** is useful when
the drawing appears empty or geometry is far from the expected location.
### Multiple model viewports
Model viewports can show different views of the same document. Grid, snap,
camera, and render mode may vary by viewport; confirm which viewport is active
before entering coordinates.
### Selection and object snaps
Selection identifies the objects a command will modify. Object snaps target
precise geometric locations such as endpoints, midpoints, centers, and
intersections. Zoom in and verify the snap marker when precision matters.
## Create and Edit Geometry
### Basic drawing commands
Create lines, arcs, circles, ellipses, polylines, splines, points, text, hatches,
and other supported entities from the ribbon or command line. Follow the active
prompt: multi-step commands remain active until they complete or are cancelled.
### Modify geometry
Common modification workflows include move, copy, rotate, scale, mirror,
offset, trim, extend, fillet, chamfer, stretch, join, explode, and erase. Locked
layers can prevent modification.
### Grips and direct manipulation
Select an entity to expose its grips. Dragging a grip previews and then commits
a geometric change. Constrained neighboring geometry may update with it.
### Undo, redo, and OOPS
**Undo** reverses recorded document operations and **Redo** reapplies them.
**OOPS** restores the most recently erased entities without reversing later
commands. Save before a risky sequence and verify that associated dimensions,
hatches, groups, and constraints were restored with their geometry.
## Layers and Properties
### Layers
Layers organize geometry and control visibility, locking, color, line type,
line weight, and plot behavior. Set the intended active layer before creating
objects. Avoid editing objects on locked layers.
### Object properties
Properties can be assigned explicitly or inherited **ByLayer** or **ByBlock**.
Use the Properties surface or commands such as `CHPROP` to inspect and change
selected objects.
### Styles
Text, dimension, multiline, table, and visual styles centralize presentation.
Changing a shared style can update many objects, so inspect affected layouts
before saving.
## Blocks and References
### Create and insert blocks
A block definition groups reusable drawing content; an INSERT places an
instance with its own position, rotation, scale, attributes, and optional array
settings. Editing a definition affects every reference to it.
### Array inserts
Array inserts place multiple block instances with row and column counts and
spacing. Large counts can generate substantial geometry. Validate scaled and
rotated arrays when exchanging drawings with another CAD system.
### Attributes and external references
Attributes attach editable values to block references. External references and
underlays retain links to other files; preserve their paths when sharing or
archiving a project.
## Annotation and Dimensions
### Text and annotation
Single-line text, multiline text, leaders, multileaders, tables, tolerances,
center marks, and centerlines communicate design intent. Missing fonts can
change layout or substitute glyphs, especially in the web build.
### Dimensions
Dimensions combine measured geometry with a dimension style. Associative
dimensions update when their source geometry changes; non-associative or
partially associated dimensions require manual verification.
### Annotation scale
Annotative objects can display differently at different viewport scales.
Confirm the active annotation scale before creating annotations and inspect all
paper-space viewports before plotting.
## Layouts and Plotting
### Layouts and viewports
Layouts represent sheets. Paper-space viewports expose model-space views at
controlled scales. Lock finished viewports to avoid accidental view changes.
### Page setup and plot preview
Choose the target device or file format, paper size, orientation, plot area,
scale, style behavior, and output quality. Use preview to catch clipping,
incorrect line weights, hidden layers, or an unintended plot area.
### Export checks
Before distributing a PDF or plotted file, compare it with the drawing at the
intended sheet size and verify text, dimensions, raster content, transparency,
line types, and viewport scales.
## 3D Modeling and Visualization
### Solids and surfaces
OpenCAD Studio can create and display solid primitives and modeled features such
as extrusions and revolutions. Imported ACIS-backed bodies are tessellated for
display while exact body data is retained where supported.
### Visual styles and lighting
Wireframe, shaded, hidden-line, materials, lights, and shadows affect display,
not the underlying object geometry. Switch to wireframe when diagnosing missing
faces or material problems.
### Tessellation quality
Curves and analytic surfaces are converted into display geometry according to
view and tolerance settings. A coarse display does not necessarily mean the
stored curve or solid is coarse.
## Parametric Constraints
### Geometric constraints
Constraints capture relationships such as coincident, horizontal, vertical,
parallel, perpendicular, equal, tangent, distance, angle, and radius. Use the
degree-of-freedom and conflict indicators to understand the sketch state.
### Editing constrained geometry
Moving or grip-editing constrained geometry can move neighboring objects. If a
solve fails or becomes redundant, inspect the constraint glyphs and conflict
status before deleting geometry.
### Persistence and history limitations
Constraint data is stored as OpenCAD Studio extension data inside DWG and DXF.
Other applications may preserve it without interpreting it. Constraint edits
and geometry changes may currently occupy adjacent history entries; verify the
result after undo, redo, erase, copy, and reopen operations.
## Commands and Shortcuts
### Command line
Enter a command name or alias, then respond to its prompts. Press `Escape` to
cancel the active command and `Return` to accept an available default. Exact
prompt options vary by command and current selection.
### Discover commands
Use command autocomplete and the ribbon to discover available operations. Some
commands are one-shot actions; others maintain state across several point,
number, keyword, or object-selection inputs.
### Shortcut conventions
Application and operating-system shortcuts can differ between native and web
builds. Avoid assigning browser-reserved shortcuts to web-only workflows.
## Automation and Plugins
### Automation interface
The automation interface can inspect documents, run commands, query geometry,
capture the viewport, and perform batches. Callers should use request IDs and
document revisions to avoid repeating or applying work to stale state.
### Plugins
Native plugins can contribute commands and ribbon surfaces through the plugin
host. Install only trusted packages that match the host API, compiler, and CAD
model compatibility requirements.
### Native and web differences
The browser build uses in-memory file workflows and cannot load native plugin
libraries. Some rendering, font, windowing, and parallel-processing behavior
also differs from the desktop build.
## Troubleshooting and Recovery
### A drawing opens incorrectly
Check units, extents, layers, layouts, block placement, missing references, and
fonts. Reopen a copy after saving to detect serialization problems early. Keep
the original file available for comparison.
### Geometry is missing or distorted
Try Zoom Extents, enable relevant layers, switch to wireframe, and inspect block
scale, array settings, clipping boundaries, object normals, and coordinates far
from the origin.
### Rendering is incomplete
Reduce unusually large images or material textures, simplify extreme hatch or
array inputs, and compare another visual style. Record the graphics backend and
the smallest drawing that reproduces the issue.
### Undo did not restore the expected state
Stop editing and save a diagnostic copy. Try the next adjacent undo entry only
when you understand what it represents. Compare the result with autosave or
backup data before overwriting the original drawing.
### Report a reproducible problem
Include the OpenCAD Studio version, platform, native or web build, exact steps,
expected and observed results, and a minimal sanitized drawing when possible.
Do not publish confidential drawings or external-reference paths.
+55
View File
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="OpenCAD Studio user manual — Parametric Constraints">
<title>Parametric Constraints — OpenCAD Studio Help</title>
<link rel="stylesheet" href="assets/css/core.css">
<link rel="stylesheet" href="assets/css/opencad.css">
<script src="search-index.js" defer></script>
<script src="assets/js/help.js" defer></script>
</head>
<body>
<header class="topbar">
<button class="nav-toggle" type="button" aria-controls="help-sidebar" aria-expanded="true" title="Toggle chapter navigation"><span aria-hidden="true">☰</span><span class="sr-only">Toggle chapter navigation</span></button>
<a class="brand" href="index.html">OpenCAD Studio Help</a>
<div class="search-shell">
<label class="sr-only" for="help-search">Search help</label>
<input id="help-search" class="help-search" type="search" role="combobox" placeholder="Search commands and topics" autocomplete="off" aria-autocomplete="list" aria-controls="search-results" aria-expanded="false">
<span class="search-key" aria-hidden="true">⌘K</span>
<ul id="search-results" class="search-results" role="listbox" hidden></ul>
</div>
</header>
<nav id="help-sidebar" class="sidebar" aria-label="Help chapters">
<div><a class="chapter-link" href="index.html">Help overview</a></div>
<details><summary><a href="getting-started.html">Getting Started</a></summary><ul><li><a href="getting-started.html#what-opencad-studio-does">What OpenCAD Studio does</a></li><li><a href="getting-started.html#a-first-drawing">A first drawing</a></li><li><a href="getting-started.html#how-to-use-this-manual">How to use this manual</a></li></ul></details>
<details><summary><a href="files-drawings.html">Files and Drawings</a></summary><ul><li><a href="files-drawings.html#create-and-open-drawings">Create and open drawings</a></li><li><a href="files-drawings.html#save-and-save-as">Save and Save As</a></li><li><a href="files-drawings.html#recovery-and-backups">Recovery and backups</a></li><li><a href="files-drawings.html#external-resources">External resources</a></li></ul></details>
<details><summary><a href="workspace-navigation.html">Workspace and Navigation</a></summary><ul><li><a href="workspace-navigation.html#model-space-and-paper-space">Model space and paper space</a></li><li><a href="workspace-navigation.html#pan-zoom-and-orbit">Pan, zoom, and orbit</a></li><li><a href="workspace-navigation.html#multiple-model-viewports">Multiple model viewports</a></li><li><a href="workspace-navigation.html#selection-and-object-snaps">Selection and object snaps</a></li></ul></details>
<details><summary><a href="create-edit-geometry.html">Create and Edit Geometry</a></summary><ul><li><a href="create-edit-geometry.html#basic-drawing-commands">Basic drawing commands</a></li><li><a href="create-edit-geometry.html#modify-geometry">Modify geometry</a></li><li><a href="create-edit-geometry.html#grips-and-direct-manipulation">Grips and direct manipulation</a></li><li><a href="create-edit-geometry.html#undo-redo-and-oops">Undo, redo, and OOPS</a></li></ul></details>
<details><summary><a href="layers-properties.html">Layers and Properties</a></summary><ul><li><a href="layers-properties.html#layers">Layers</a></li><li><a href="layers-properties.html#object-properties">Object properties</a></li><li><a href="layers-properties.html#styles">Styles</a></li></ul></details>
<details><summary><a href="blocks-references.html">Blocks and References</a></summary><ul><li><a href="blocks-references.html#create-and-insert-blocks">Create and insert blocks</a></li><li><a href="blocks-references.html#array-inserts">Array inserts</a></li><li><a href="blocks-references.html#attributes-and-external-references">Attributes and external references</a></li></ul></details>
<details><summary><a href="annotation-dimensions.html">Annotation and Dimensions</a></summary><ul><li><a href="annotation-dimensions.html#text-and-annotation">Text and annotation</a></li><li><a href="annotation-dimensions.html#dimensions">Dimensions</a></li><li><a href="annotation-dimensions.html#annotation-scale">Annotation scale</a></li></ul></details>
<details><summary><a href="layouts-plotting.html">Layouts and Plotting</a></summary><ul><li><a href="layouts-plotting.html#layouts-and-viewports">Layouts and viewports</a></li><li><a href="layouts-plotting.html#page-setup-and-plot-preview">Page setup and plot preview</a></li><li><a href="layouts-plotting.html#export-checks">Export checks</a></li></ul></details>
<details><summary><a href="3d-modeling.html">3D Modeling and Visualization</a></summary><ul><li><a href="3d-modeling.html#solids-and-surfaces">Solids and surfaces</a></li><li><a href="3d-modeling.html#visual-styles-and-lighting">Visual styles and lighting</a></li><li><a href="3d-modeling.html#tessellation-quality">Tessellation quality</a></li></ul></details>
<details class="current-topic" open><summary><a href="parametric-constraints.html">Parametric Constraints</a></summary><ul><li><a href="parametric-constraints.html#geometric-constraints">Geometric constraints</a></li><li><a href="parametric-constraints.html#editing-constrained-geometry">Editing constrained geometry</a></li><li><a href="parametric-constraints.html#persistence-and-history-limitations">Persistence and history limitations</a></li></ul></details>
<details><summary><a href="commands-shortcuts.html">Commands and Shortcuts</a></summary><ul><li><a href="commands-shortcuts.html#command-line">Command line</a></li><li><a href="commands-shortcuts.html#discover-commands">Discover commands</a></li><li><a href="commands-shortcuts.html#shortcut-conventions">Shortcut conventions</a></li></ul></details>
<details><summary><a href="automation-plugins.html">Automation and Plugins</a></summary><ul><li><a href="automation-plugins.html#automation-interface">Automation interface</a></li><li><a href="automation-plugins.html#plugins">Plugins</a></li><li><a href="automation-plugins.html#native-and-web-differences">Native and web differences</a></li></ul></details>
<details><summary><a href="troubleshooting-recovery.html">Troubleshooting and Recovery</a></summary><ul><li><a href="troubleshooting-recovery.html#a-drawing-opens-incorrectly">A drawing opens incorrectly</a></li><li><a href="troubleshooting-recovery.html#geometry-is-missing-or-distorted">Geometry is missing or distorted</a></li><li><a href="troubleshooting-recovery.html#rendering-is-incomplete">Rendering is incomplete</a></li><li><a href="troubleshooting-recovery.html#undo-did-not-restore-the-expected-state">Undo did not restore the expected state</a></li><li><a href="troubleshooting-recovery.html#report-a-reproducible-problem">Report a reproducible problem</a></li></ul></details>
</nav>
<main id="main-content">
<div class="content-wrap">
<p class="page-kicker">OpenCAD Studio User Manual</p>
<h1 class="content-title">Parametric Constraints</h1>
<h3 id="geometric-constraints">Geometric constraints</h3>
<p>Constraints capture relationships such as coincident, horizontal, vertical, parallel, perpendicular, equal, tangent, distance, angle, and radius. Use the degree-of-freedom and conflict indicators to understand the sketch state.</p>
<h3 id="editing-constrained-geometry">Editing constrained geometry</h3>
<p>Moving or grip-editing constrained geometry can move neighboring objects. If a solve fails or becomes redundant, inspect the constraint glyphs and conflict status before deleting geometry.</p>
<h3 id="persistence-and-history-limitations">Persistence and history limitations</h3>
<p>Constraint data is stored as OpenCAD Studio extension data inside DWG and DXF. Other applications may preserve it without interpreting it. Constraint edits and geometry changes may currently occupy adjacent history entries; verify the result after undo, redo, erase, copy, and reopen operations.</p>
<nav class="page-navigation" aria-label="Previous and next chapter"><a rel="prev" href="3d-modeling.html">← 3D Modeling and Visualization</a><a rel="next" href="commands-shortcuts.html">Commands and Shortcuts →</a></nav>
<p class="date-footer">OpenCAD Studio Help · Generated from <code>help/manual.md</code></p>
</div>
</main>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
window.OCS_HELP_INDEX = [{"title":"What OpenCAD Studio does","chapter":"Getting Started","url":"getting-started.html#what-opencad-studio-does","text":"What OpenCAD Studio does OpenCAD Studio is a desktop and web CAD application for opening, creating, editing, visualizing, and saving two- and three-dimensional drawings. It works with DWG and DXF drawings and provides command-line, ribbon, automation, and plugin-oriented workflows."},{"title":"A first drawing","chapter":"Getting Started","url":"getting-started.html#a-first-drawing","text":"A first drawing Create a new drawing or open an existing DWG or DXF file. Confirm the active workspace, units, layer, and object-snap settings. Create geometry with a drawing command such as Line , Circle , or Polyline . Edit the result with grips or a modification command. Save the drawing under a new name until its compatibility has been verified. Data safety: Keep an untouched copy of important third-party drawings. Confirm geometry, layouts, external references, and plotted output before replacing the original file."},{"title":"How to use this manual","chapter":"Getting Started","url":"getting-started.html#how-to-use-this-manual","text":"How to use this manual Use the chapter navigation for broad workflows, the search box for a command or concept, and the previous/next links for sequential reading. Command names, system variables, keys, and file extensions appear in monospaced text ."},{"title":"Create and open drawings","chapter":"Files and Drawings","url":"files-drawings.html#create-and-open-drawings","text":"Create and open drawings Use New for a blank document and Open for an existing DWG, DXF, backup, or autosave drawing. After opening a file from another CAD system, inspect its units, layers, blocks, fonts, raster references, layouts, and model extents."},{"title":"Save and Save As","chapter":"Files and Drawings","url":"files-drawings.html#save-and-save-as","text":"Save and Save As Save updates the current drawing. Save As changes its path, format, or version. Prefer Save As when validating interoperability or recovering an older drawing. A successful write does not replace visual inspection of a reopened copy."},{"title":"Recovery and backups","chapter":"Files and Drawings","url":"files-drawings.html#recovery-and-backups","text":"Recovery and backups OpenCAD Studio recognizes drawing backups and autosave-style files where the underlying data is DWG or DXF. If strict loading rejects malformed entities, use the recovery workflow and review what was discarded before continuing work."},{"title":"External resources","chapter":"Files and Drawings","url":"files-drawings.html#external-resources","text":"External resources Raster images, underlays, fonts, materials, and external references may depend on paths outside the drawing. Keep referenced files together when moving a project and verify that relative paths still resolve."},{"title":"Model space and paper space","chapter":"Workspace and Navigation","url":"workspace-navigation.html#model-space-and-paper-space","text":"Model space and paper space Model space is the primary drawing environment. Paper-space layouts arrange views for plotting through layout viewports. Always confirm the active space before drawing or editing."},{"title":"Pan, zoom, and orbit","chapter":"Workspace and Navigation","url":"workspace-navigation.html#pan-zoom-and-orbit","text":"Pan, zoom, and orbit Use pan and zoom to navigate without changing drawing geometry. Use orbit and view controls for three-dimensional inspection. Zoom Extents is useful when the drawing appears empty or geometry is far from the expected location."},{"title":"Multiple model viewports","chapter":"Workspace and Navigation","url":"workspace-navigation.html#multiple-model-viewports","text":"Multiple model viewports Model viewports can show different views of the same document. Grid, snap, camera, and render mode may vary by viewport; confirm which viewport is active before entering coordinates."},{"title":"Selection and object snaps","chapter":"Workspace and Navigation","url":"workspace-navigation.html#selection-and-object-snaps","text":"Selection and object snaps Selection identifies the objects a command will modify. Object snaps target precise geometric locations such as endpoints, midpoints, centers, and intersections. Zoom in and verify the snap marker when precision matters."},{"title":"Basic drawing commands","chapter":"Create and Edit Geometry","url":"create-edit-geometry.html#basic-drawing-commands","text":"Basic drawing commands Create lines, arcs, circles, ellipses, polylines, splines, points, text, hatches, and other supported entities from the ribbon or command line. Follow the active prompt: multi-step commands remain active until they complete or are cancelled."},{"title":"Modify geometry","chapter":"Create and Edit Geometry","url":"create-edit-geometry.html#modify-geometry","text":"Modify geometry Common modification workflows include move, copy, rotate, scaLine truncated
+256
View File
@@ -0,0 +1,256 @@
#!/usr/bin/env python3
"""Build the static OpenCAD Studio HTML help system from help/manual.md."""
from __future__ import annotations
import argparse
import html
import json
import re
import subprocess
import sys
from dataclasses import dataclass
from html.parser import HTMLParser
from pathlib import Path
HELP = Path(__file__).resolve().parents[1]
SOURCE = HELP / "manual.md"
@dataclass(frozen=True)
class Page:
filename: str
title: str
summary: str
PAGES = (
Page("getting-started.html", "Getting Started", "Begin a drawing safely and learn how to use this manual."),
Page("files-drawings.html", "Files and Drawings", "Open, save, recover, and move drawings with their dependencies."),
Page("workspace-navigation.html", "Workspace and Navigation", "Work with spaces, views, selection, and object snaps."),
Page("create-edit-geometry.html", "Create and Edit Geometry", "Draw, modify, grip-edit, erase, and restore geometry."),
Page("layers-properties.html", "Layers and Properties", "Organize objects and control their inherited appearance."),
Page("blocks-references.html", "Blocks and References", "Create reusable content, arrays, attributes, and file references."),
Page("annotation-dimensions.html", "Annotation and Dimensions", "Add readable text, dimensions, leaders, and scaled annotation."),
Page("layouts-plotting.html", "Layouts and Plotting", "Prepare paper-space sheets and verify plotted output."),
Page("3d-modeling.html", "3D Modeling and Visualization", "Create and inspect solids, surfaces, materials, and visual styles."),
Page("parametric-constraints.html", "Parametric Constraints", "Capture geometric intent and diagnose solve or history issues."),
Page("commands-shortcuts.html", "Commands and Shortcuts", "Use prompts, autocomplete, aliases, and keyboard conventions."),
Page("automation-plugins.html", "Automation and Plugins", "Control drawings programmatically and extend the native app."),
Page("troubleshooting-recovery.html", "Troubleshooting and Recovery", "Diagnose file, geometry, rendering, and history problems."),
)
def slug(text: str) -> str:
value = re.sub(r"[^\w\s-]", "", text.lower(), flags=re.UNICODE)
value = re.sub(r"[\s_]+", "-", value)
return value.strip("-")
def split_chapters(markdown: str) -> dict[str, str]:
chapters: dict[str, list[str]] = {}
current: list[str] | None = None
for line in markdown.splitlines():
match = re.match(r"^## (.+)$", line)
if match:
title = match.group(1).strip()
if title in chapters:
raise RuntimeError(f"Duplicate chapter: {title}")
current = [line]
chapters[title] = current
elif current is not None:
current.append(line)
return {title: "\n".join(lines) for title, lines in chapters.items()}
def pandoc(markdown: str) -> str:
process = subprocess.run(
["pandoc", "--from=gfm", "--to=html5", "--wrap=none"],
input=markdown,
text=True,
capture_output=True,
check=True,
)
return process.stdout.strip()
def headings(markdown: str) -> list[tuple[str, str]]:
output: list[tuple[str, str]] = []
for line in markdown.splitlines():
match = re.match(r"^### (.+)$", line)
if match:
title = match.group(1).strip()
output.append((slug(title), title))
return output
def sidebar(current: Page | None, chapters: dict[str, str]) -> str:
parts = ['<nav id="help-sidebar" class="sidebar" aria-label="Help chapters">']
index_class = ' class="current-topic"' if current is None else ""
parts.append(f'<div{index_class}><a class="chapter-link" href="index.html">Help overview</a></div>')
for page in PAGES:
classes = ' class="current-topic"' if page == current else ""
opened = " open" if page == current else ""
links = "".join(
f'<li><a href="{page.filename}#{anchor}">{html.escape(title)}</a></li>'
for anchor, title in headings(chapters[page.title])
)
parts.append(
f'<details{classes}{opened}><summary><a href="{page.filename}">{html.escape(page.title)}</a></summary>'
f'<ul>{links}</ul></details>'
)
parts.append("</nav>")
return "\n".join(parts)
def overview() -> str:
cards = "".join(
f'<a class="chapter-card" href="{page.filename}"><strong>{html.escape(page.title)}</strong>'
f'<span>{html.escape(page.summary)}</span></a>'
for page in PAGES
)
return f"""
<section class="hero">
<p class="eyebrow">OpenCAD Studio User Manual</p>
<h2>Find the workflow, command, or concept you need</h2>
<p>Learn how to create and edit drawings, exchange DWG and DXF files, prepare layouts, model in 3D, use constraints, automate work, and recover from common problems.</p>
</section>
<div class="quick-paths" aria-label="Common help paths">
<a href="getting-started.html#a-first-drawing">Create a first drawing</a>
<a href="files-drawings.html#recovery-and-backups">Recover a drawing</a>
<a href="troubleshooting-recovery.html#report-a-reproducible-problem">Report a problem</a>
</div>
<h2 id="manual-chapters">Manual chapters</h2>
<div class="chapter-grid">{cards}</div>
""".strip()
def shell(page: Page | None, title: str, body: str, chapters: dict[str, str]) -> str:
previous_link = ""
next_link = ""
if page is not None:
index = PAGES.index(page)
previous = PAGES[index - 1] if index else None
following = PAGES[index + 1] if index + 1 < len(PAGES) else None
previous_link = (
f'<a rel="prev" href="{previous.filename}">← {html.escape(previous.title)}</a>'
if previous else '<a rel="prev" href="index.html">← Help overview</a>'
)
next_link = (
f'<a rel="next" href="{following.filename}">{html.escape(following.title)} →</a>'
if following else ""
)
elif PAGES:
next_link = f'<a rel="next" href="{PAGES[0].filename}">{html.escape(PAGES[0].title)} →</a>'
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="OpenCAD Studio user manual — {html.escape(title)}">
<title>{html.escape(title)} — OpenCAD Studio Help</title>
<link rel="stylesheet" href="assets/css/core.css">
<link rel="stylesheet" href="assets/css/opencad.css">
<script src="search-index.js" defer></script>
<script src="assets/js/help.js" defer></script>
</head>
<body>
<header class="topbar">
<button class="nav-toggle" type="button" aria-controls="help-sidebar" aria-expanded="true" title="Toggle chapter navigation"><span aria-hidden="true">☰</span><span class="sr-only">Toggle chapter navigation</span></button>
<a class="brand" href="index.html">OpenCAD Studio Help</a>
<div class="search-shell">
<label class="sr-only" for="help-search">Search help</label>
<input id="help-search" class="help-search" type="search" role="combobox" placeholder="Search commands and topics" autocomplete="off" aria-autocomplete="list" aria-controls="search-results" aria-expanded="false">
<span class="search-key" aria-hidden="true">⌘K</span>
<ul id="search-results" class="search-results" role="listbox" hidden></ul>
</div>
</header>
{sidebar(page, chapters)}
<main id="main-content">
<div class="content-wrap">
<p class="page-kicker">OpenCAD Studio User Manual</p>
<h1 class="content-title">{html.escape(title)}</h1>
{body}
<nav class="page-navigation" aria-label="Previous and next chapter">{previous_link}{next_link}</nav>
<p class="date-footer">OpenCAD Studio Help · Generated from <code>help/manual.md</code></p>
</div>
</main>
</body>
</html>
"""
class TextExtractor(HTMLParser):
def __init__(self) -> None:
super().__init__()
self.parts: list[str] = []
def handle_data(self, data: str) -> None:
if data.strip():
self.parts.append(data.strip())
def search_entries(chapters: dict[str, str]) -> list[dict[str, str]]:
entries: list[dict[str, str]] = []
for page in PAGES:
chapter = chapters[page.title]
sections = re.split(r"(?=^### )", chapter, flags=re.MULTILINE)
for section in sections:
match = re.match(r"^### (.+)$", section.splitlines()[0] if section.splitlines() else "")
if not match:
continue
title = match.group(1).strip()
extractor = TextExtractor()
extractor.feed(pandoc(section))
entries.append({
"title": title,
"chapter": page.title,
"url": f"{page.filename}#{slug(title)}",
"text": " ".join(extractor.parts),
})
return entries
def generated_outputs() -> dict[str, str]:
markdown = SOURCE.read_text(encoding="utf-8")
chapters = split_chapters(markdown)
expected = {page.title for page in PAGES}
if set(chapters) != expected:
raise RuntimeError(
f"Manual chapter mapping mismatch. Missing: {sorted(expected - set(chapters))}; "
f"extra: {sorted(set(chapters) - expected)}"
)
output = {"index.html": shell(None, "Help overview", overview(), chapters)}
for page in PAGES:
chapter_body = "\n".join(chapters[page.title].splitlines()[1:]).lstrip()
output[page.filename] = shell(page, page.title, pandoc(chapter_body), chapters)
index_json = json.dumps(search_entries(chapters), ensure_ascii=False, separators=(",", ":"))
output["search-index.js"] = f"window.OCS_HELP_INDEX = {index_json};\n"
return output
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--check", action="store_true", help="Fail if generated output is stale.")
args = parser.parse_args()
output = generated_outputs()
if args.check:
stale = [name for name, content in output.items() if not (HELP / name).is_file() or (HELP / name).read_text(encoding="utf-8") != content]
if stale:
print("Stale generated help: " + ", ".join(stale), file=sys.stderr)
return 1
print(f"Generated help is current ({len(output) - 1} pages plus search index).")
return 0
for name, content in output.items():
(HELP / name).write_text(content, encoding="utf-8")
print(f"Generated {len(output) - 1} pages and search-index.js in {HELP}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
"""Validate local links, anchors, assets, context targets, and basic HTML structure."""
from __future__ import annotations
import json
import sys
from html.parser import HTMLParser
from pathlib import Path
from urllib.parse import unquote
HELP = Path(__file__).resolve().parents[1]
class Document(HTMLParser):
def __init__(self) -> None:
super().__init__()
self.ids: set[str] = set()
self.links: list[str] = []
self.assets: list[str] = []
self.has_main = False
self.has_title = False
self.images_without_alt = 0
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
values = dict(attrs)
if values.get("id"):
self.ids.add(values["id"] or "")
if tag == "a" and values.get("href"):
self.links.append(values["href"] or "")
if tag in {"img", "script", "link"}:
target = values.get("src") or values.get("href")
if target:
self.assets.append(target)
if tag == "img" and "alt" not in values:
self.images_without_alt += 1
self.has_main |= tag == "main"
self.has_title |= tag == "title"
def load_documents() -> dict[str, Document]:
documents: dict[str, Document] = {}
for path in sorted(HELP.glob("*.html")):
document = Document()
document.feed(path.read_text(encoding="utf-8"))
documents[path.name] = document
return documents
def validate_target(source: str, target: str, documents: dict[str, Document], errors: list[str]) -> None:
if target.startswith(("http:", "https:", "mailto:", "javascript:")):
return
filename, _, fragment = target.partition("#")
filename = unquote(filename) or source
destination = HELP / filename
if not destination.is_file():
errors.append(f"{source}: missing target {target}")
return
if fragment and destination.suffix == ".html":
document = documents.get(destination.name)
if document is None or fragment not in document.ids:
errors.append(f"{source}: missing anchor {target}")
def main() -> int:
documents = load_documents()
errors: list[str] = []
if not documents:
errors.append("No generated HTML pages found.")
for name, document in documents.items():
if not document.has_main:
errors.append(f"{name}: missing <main>")
if not document.has_title:
errors.append(f"{name}: missing <title>")
if document.images_without_alt:
errors.append(f"{name}: {document.images_without_alt} image(s) missing alt")
for target in document.links + document.assets:
validate_target(name, target, documents, errors)
context = json.loads((HELP / "context-map.json").read_text(encoding="utf-8"))
for group in ("commands", "surfaces"):
for key, target in context.get(group, {}).items():
validate_target(f"context-map.json:{group}.{key}", target, documents, errors)
if errors:
print("Help validation failed:", file=sys.stderr)
for error in errors:
print(f"- {error}", file=sys.stderr)
return 1
links = sum(len(document.links) for document in documents.values())
print(f"Validated {len(documents)} pages, {links} links, and {sum(len(context.get(group, {})) for group in ('commands', 'surfaces'))} context targets.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+59
View File
@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="OpenCAD Studio user manual — Troubleshooting and Recovery">
<title>Troubleshooting and Recovery — OpenCAD Studio Help</title>
<link rel="stylesheet" href="assets/css/core.css">
<link rel="stylesheet" href="assets/css/opencad.css">
<script src="search-index.js" defer></script>
<script src="assets/js/help.js" defer></script>
</head>
<body>
<header class="topbar">
<button class="nav-toggle" type="button" aria-controls="help-sidebar" aria-expanded="true" title="Toggle chapter navigation"><span aria-hidden="true">☰</span><span class="sr-only">Toggle chapter navigation</span></button>
<a class="brand" href="index.html">OpenCAD Studio Help</a>
<div class="search-shell">
<label class="sr-only" for="help-search">Search help</label>
<input id="help-search" class="help-search" type="search" role="combobox" placeholder="Search commands and topics" autocomplete="off" aria-autocomplete="list" aria-controls="search-results" aria-expanded="false">
<span class="search-key" aria-hidden="true">⌘K</span>
<ul id="search-results" class="search-results" role="listbox" hidden></ul>
</div>
</header>
<nav id="help-sidebar" class="sidebar" aria-label="Help chapters">
<div><a class="chapter-link" href="index.html">Help overview</a></div>
<details><summary><a href="getting-started.html">Getting Started</a></summary><ul><li><a href="getting-started.html#what-opencad-studio-does">What OpenCAD Studio does</a></li><li><a href="getting-started.html#a-first-drawing">A first drawing</a></li><li><a href="getting-started.html#how-to-use-this-manual">How to use this manual</a></li></ul></details>
<details><summary><a href="files-drawings.html">Files and Drawings</a></summary><ul><li><a href="files-drawings.html#create-and-open-drawings">Create and open drawings</a></li><li><a href="files-drawings.html#save-and-save-as">Save and Save As</a></li><li><a href="files-drawings.html#recovery-and-backups">Recovery and backups</a></li><li><a href="files-drawings.html#external-resources">External resources</a></li></ul></details>
<details><summary><a href="workspace-navigation.html">Workspace and Navigation</a></summary><ul><li><a href="workspace-navigation.html#model-space-and-paper-space">Model space and paper space</a></li><li><a href="workspace-navigation.html#pan-zoom-and-orbit">Pan, zoom, and orbit</a></li><li><a href="workspace-navigation.html#multiple-model-viewports">Multiple model viewports</a></li><li><a href="workspace-navigation.html#selection-and-object-snaps">Selection and object snaps</a></li></ul></details>
<details><summary><a href="create-edit-geometry.html">Create and Edit Geometry</a></summary><ul><li><a href="create-edit-geometry.html#basic-drawing-commands">Basic drawing commands</a></li><li><a href="create-edit-geometry.html#modify-geometry">Modify geometry</a></li><li><a href="create-edit-geometry.html#grips-and-direct-manipulation">Grips and direct manipulation</a></li><li><a href="create-edit-geometry.html#undo-redo-and-oops">Undo, redo, and OOPS</a></li></ul></details>
<details><summary><a href="layers-properties.html">Layers and Properties</a></summary><ul><li><a href="layers-properties.html#layers">Layers</a></li><li><a href="layers-properties.html#object-properties">Object properties</a></li><li><a href="layers-properties.html#styles">Styles</a></li></ul></details>
<details><summary><a href="blocks-references.html">Blocks and References</a></summary><ul><li><a href="blocks-references.html#create-and-insert-blocks">Create and insert blocks</a></li><li><a href="blocks-references.html#array-inserts">Array inserts</a></li><li><a href="blocks-references.html#attributes-and-external-references">Attributes and external references</a></li></ul></details>
<details><summary><a href="annotation-dimensions.html">Annotation and Dimensions</a></summary><ul><li><a href="annotation-dimensions.html#text-and-annotation">Text and annotation</a></li><li><a href="annotation-dimensions.html#dimensions">Dimensions</a></li><li><a href="annotation-dimensions.html#annotation-scale">Annotation scale</a></li></ul></details>
<details><summary><a href="layouts-plotting.html">Layouts and Plotting</a></summary><ul><li><a href="layouts-plotting.html#layouts-and-viewports">Layouts and viewports</a></li><li><a href="layouts-plotting.html#page-setup-and-plot-preview">Page setup and plot preview</a></li><li><a href="layouts-plotting.html#export-checks">Export checks</a></li></ul></details>
<details><summary><a href="3d-modeling.html">3D Modeling and Visualization</a></summary><ul><li><a href="3d-modeling.html#solids-and-surfaces">Solids and surfaces</a></li><li><a href="3d-modeling.html#visual-styles-and-lighting">Visual styles and lighting</a></li><li><a href="3d-modeling.html#tessellation-quality">Tessellation quality</a></li></ul></details>
<details><summary><a href="parametric-constraints.html">Parametric Constraints</a></summary><ul><li><a href="parametric-constraints.html#geometric-constraints">Geometric constraints</a></li><li><a href="parametric-constraints.html#editing-constrained-geometry">Editing constrained geometry</a></li><li><a href="parametric-constraints.html#persistence-and-history-limitations">Persistence and history limitations</a></li></ul></details>
<details><summary><a href="commands-shortcuts.html">Commands and Shortcuts</a></summary><ul><li><a href="commands-shortcuts.html#command-line">Command line</a></li><li><a href="commands-shortcuts.html#discover-commands">Discover commands</a></li><li><a href="commands-shortcuts.html#shortcut-conventions">Shortcut conventions</a></li></ul></details>
<details><summary><a href="automation-plugins.html">Automation and Plugins</a></summary><ul><li><a href="automation-plugins.html#automation-interface">Automation interface</a></li><li><a href="automation-plugins.html#plugins">Plugins</a></li><li><a href="automation-plugins.html#native-and-web-differences">Native and web differences</a></li></ul></details>
<details class="current-topic" open><summary><a href="troubleshooting-recovery.html">Troubleshooting and Recovery</a></summary><ul><li><a href="troubleshooting-recovery.html#a-drawing-opens-incorrectly">A drawing opens incorrectly</a></li><li><a href="troubleshooting-recovery.html#geometry-is-missing-or-distorted">Geometry is missing or distorted</a></li><li><a href="troubleshooting-recovery.html#rendering-is-incomplete">Rendering is incomplete</a></li><li><a href="troubleshooting-recovery.html#undo-did-not-restore-the-expected-state">Undo did not restore the expected state</a></li><li><a href="troubleshooting-recovery.html#report-a-reproducible-problem">Report a reproducible problem</a></li></ul></details>
</nav>
<main id="main-content">
<div class="content-wrap">
<p class="page-kicker">OpenCAD Studio User Manual</p>
<h1 class="content-title">Troubleshooting and Recovery</h1>
<h3 id="a-drawing-opens-incorrectly">A drawing opens incorrectly</h3>
<p>Check units, extents, layers, layouts, block placement, missing references, and fonts. Reopen a copy after saving to detect serialization problems early. Keep the original file available for comparison.</p>
<h3 id="geometry-is-missing-or-distorted">Geometry is missing or distorted</h3>
<p>Try Zoom Extents, enable relevant layers, switch to wireframe, and inspect block scale, array settings, clipping boundaries, object normals, and coordinates far from the origin.</p>
<h3 id="rendering-is-incomplete">Rendering is incomplete</h3>
<p>Reduce unusually large images or material textures, simplify extreme hatch or array inputs, and compare another visual style. Record the graphics backend and the smallest drawing that reproduces the issue.</p>
<h3 id="undo-did-not-restore-the-expected-state">Undo did not restore the expected state</h3>
<p>Stop editing and save a diagnostic copy. Try the next adjacent undo entry only when you understand what it represents. Compare the result with autosave or backup data before overwriting the original drawing.</p>
<h3 id="report-a-reproducible-problem">Report a reproducible problem</h3>
<p>Include the OpenCAD Studio version, platform, native or web build, exact steps, expected and observed results, and a minimal sanitized drawing when possible. Do not publish confidential drawings or external-reference paths.</p>
<nav class="page-navigation" aria-label="Previous and next chapter"><a rel="prev" href="automation-plugins.html">← Automation and Plugins</a></nav>
<p class="date-footer">OpenCAD Studio Help · Generated from <code>help/manual.md</code></p>
</div>
</main>
</body>
</html>
+57
View File
@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="OpenCAD Studio user manual — Workspace and Navigation">
<title>Workspace and Navigation — OpenCAD Studio Help</title>
<link rel="stylesheet" href="assets/css/core.css">
<link rel="stylesheet" href="assets/css/opencad.css">
<script src="search-index.js" defer></script>
<script src="assets/js/help.js" defer></script>
</head>
<body>
<header class="topbar">
<button class="nav-toggle" type="button" aria-controls="help-sidebar" aria-expanded="true" title="Toggle chapter navigation"><span aria-hidden="true">☰</span><span class="sr-only">Toggle chapter navigation</span></button>
<a class="brand" href="index.html">OpenCAD Studio Help</a>
<div class="search-shell">
<label class="sr-only" for="help-search">Search help</label>
<input id="help-search" class="help-search" type="search" role="combobox" placeholder="Search commands and topics" autocomplete="off" aria-autocomplete="list" aria-controls="search-results" aria-expanded="false">
<span class="search-key" aria-hidden="true">⌘K</span>
<ul id="search-results" class="search-results" role="listbox" hidden></ul>
</div>
</header>
<nav id="help-sidebar" class="sidebar" aria-label="Help chapters">
<div><a class="chapter-link" href="index.html">Help overview</a></div>
<details><summary><a href="getting-started.html">Getting Started</a></summary><ul><li><a href="getting-started.html#what-opencad-studio-does">What OpenCAD Studio does</a></li><li><a href="getting-started.html#a-first-drawing">A first drawing</a></li><li><a href="getting-started.html#how-to-use-this-manual">How to use this manual</a></li></ul></details>
<details><summary><a href="files-drawings.html">Files and Drawings</a></summary><ul><li><a href="files-drawings.html#create-and-open-drawings">Create and open drawings</a></li><li><a href="files-drawings.html#save-and-save-as">Save and Save As</a></li><li><a href="files-drawings.html#recovery-and-backups">Recovery and backups</a></li><li><a href="files-drawings.html#external-resources">External resources</a></li></ul></details>
<details class="current-topic" open><summary><a href="workspace-navigation.html">Workspace and Navigation</a></summary><ul><li><a href="workspace-navigation.html#model-space-and-paper-space">Model space and paper space</a></li><li><a href="workspace-navigation.html#pan-zoom-and-orbit">Pan, zoom, and orbit</a></li><li><a href="workspace-navigation.html#multiple-model-viewports">Multiple model viewports</a></li><li><a href="workspace-navigation.html#selection-and-object-snaps">Selection and object snaps</a></li></ul></details>
<details><summary><a href="create-edit-geometry.html">Create and Edit Geometry</a></summary><ul><li><a href="create-edit-geometry.html#basic-drawing-commands">Basic drawing commands</a></li><li><a href="create-edit-geometry.html#modify-geometry">Modify geometry</a></li><li><a href="create-edit-geometry.html#grips-and-direct-manipulation">Grips and direct manipulation</a></li><li><a href="create-edit-geometry.html#undo-redo-and-oops">Undo, redo, and OOPS</a></li></ul></details>
<details><summary><a href="layers-properties.html">Layers and Properties</a></summary><ul><li><a href="layers-properties.html#layers">Layers</a></li><li><a href="layers-properties.html#object-properties">Object properties</a></li><li><a href="layers-properties.html#styles">Styles</a></li></ul></details>
<details><summary><a href="blocks-references.html">Blocks and References</a></summary><ul><li><a href="blocks-references.html#create-and-insert-blocks">Create and insert blocks</a></li><li><a href="blocks-references.html#array-inserts">Array inserts</a></li><li><a href="blocks-references.html#attributes-and-external-references">Attributes and external references</a></li></ul></details>
<details><summary><a href="annotation-dimensions.html">Annotation and Dimensions</a></summary><ul><li><a href="annotation-dimensions.html#text-and-annotation">Text and annotation</a></li><li><a href="annotation-dimensions.html#dimensions">Dimensions</a></li><li><a href="annotation-dimensions.html#annotation-scale">Annotation scale</a></li></ul></details>
<details><summary><a href="layouts-plotting.html">Layouts and Plotting</a></summary><ul><li><a href="layouts-plotting.html#layouts-and-viewports">Layouts and viewports</a></li><li><a href="layouts-plotting.html#page-setup-and-plot-preview">Page setup and plot preview</a></li><li><a href="layouts-plotting.html#export-checks">Export checks</a></li></ul></details>
<details><summary><a href="3d-modeling.html">3D Modeling and Visualization</a></summary><ul><li><a href="3d-modeling.html#solids-and-surfaces">Solids and surfaces</a></li><li><a href="3d-modeling.html#visual-styles-and-lighting">Visual styles and lighting</a></li><li><a href="3d-modeling.html#tessellation-quality">Tessellation quality</a></li></ul></details>
<details><summary><a href="parametric-constraints.html">Parametric Constraints</a></summary><ul><li><a href="parametric-constraints.html#geometric-constraints">Geometric constraints</a></li><li><a href="parametric-constraints.html#editing-constrained-geometry">Editing constrained geometry</a></li><li><a href="parametric-constraints.html#persistence-and-history-limitations">Persistence and history limitations</a></li></ul></details>
<details><summary><a href="commands-shortcuts.html">Commands and Shortcuts</a></summary><ul><li><a href="commands-shortcuts.html#command-line">Command line</a></li><li><a href="commands-shortcuts.html#discover-commands">Discover commands</a></li><li><a href="commands-shortcuts.html#shortcut-conventions">Shortcut conventions</a></li></ul></details>
<details><summary><a href="automation-plugins.html">Automation and Plugins</a></summary><ul><li><a href="automation-plugins.html#automation-interface">Automation interface</a></li><li><a href="automation-plugins.html#plugins">Plugins</a></li><li><a href="automation-plugins.html#native-and-web-differences">Native and web differences</a></li></ul></details>
<details><summary><a href="troubleshooting-recovery.html">Troubleshooting and Recovery</a></summary><ul><li><a href="troubleshooting-recovery.html#a-drawing-opens-incorrectly">A drawing opens incorrectly</a></li><li><a href="troubleshooting-recovery.html#geometry-is-missing-or-distorted">Geometry is missing or distorted</a></li><li><a href="troubleshooting-recovery.html#rendering-is-incomplete">Rendering is incomplete</a></li><li><a href="troubleshooting-recovery.html#undo-did-not-restore-the-expected-state">Undo did not restore the expected state</a></li><li><a href="troubleshooting-recovery.html#report-a-reproducible-problem">Report a reproducible problem</a></li></ul></details>
</nav>
<main id="main-content">
<div class="content-wrap">
<p class="page-kicker">OpenCAD Studio User Manual</p>
<h1 class="content-title">Workspace and Navigation</h1>
<h3 id="model-space-and-paper-space">Model space and paper space</h3>
<p>Model space is the primary drawing environment. Paper-space layouts arrange views for plotting through layout viewports. Always confirm the active space before drawing or editing.</p>
<h3 id="pan-zoom-and-orbit">Pan, zoom, and orbit</h3>
<p>Use pan and zoom to navigate without changing drawing geometry. Use orbit and view controls for three-dimensional inspection. <strong>Zoom Extents</strong> is useful when the drawing appears empty or geometry is far from the expected location.</p>
<h3 id="multiple-model-viewports">Multiple model viewports</h3>
<p>Model viewports can show different views of the same document. Grid, snap, camera, and render mode may vary by viewport; confirm which viewport is active before entering coordinates.</p>
<h3 id="selection-and-object-snaps">Selection and object snaps</h3>
<p>Selection identifies the objects a command will modify. Object snaps target precise geometric locations such as endpoints, midpoints, centers, and intersections. Zoom in and verify the snap marker when precision matters.</p>
<nav class="page-navigation" aria-label="Previous and next chapter"><a rel="prev" href="files-drawings.html">← Files and Drawings</a><a rel="next" href="create-edit-geometry.html">Create and Edit Geometry →</a></nav>
<p class="date-footer">OpenCAD Studio Help · Generated from <code>help/manual.md</code></p>
</div>
</main>
</body>
</html>
+3
View File
@@ -302,6 +302,7 @@ impl OpenCADStudio {
let i = self.active_tab;
self.tabs[i].scene.document = acadrust::CadDocument::new();
self.tabs[i].scene.sketch_constraints.clear();
self.tabs[i].scene.named_parameters = crate::scene::named_parameters::ParameterTable::new();
self.tabs[i].scene.deselect_all();
self.tabs[i].current_path = None;
// The headless session starts on the welcome (Start) tab, which
@@ -331,6 +332,7 @@ impl OpenCADStudio {
let i = self.active_tab;
self.tabs[i].scene.document = doc;
self.tabs[i].scene.load_sketch_constraints_from_document();
self.tabs[i].scene.load_named_parameters_from_document();
self.tabs[i].scene.deselect_all();
crate::app::style_ops::ensure_standard_styles(
&mut self.tabs[i].scene.document,
@@ -1063,6 +1065,7 @@ mod tests {
"CHANGELOG",
"CUI",
"ALIASEDIT",
"OPTIONS",
];
for cmd in standalone {
assert!(
+162
View File
@@ -951,6 +951,81 @@ impl OpenCADStudio {
self.push_sketch_constraints_history(i, label, scope, constraints_before);
}
/// PARAMETERS' Apply button (`docs/named_parameters_design.md` stage 4):
/// rebuilds the active tab's `Scene::named_parameters` from the
/// editor's working buffer, reports any row that failed to validate via
/// the command line, and re-solves every scope with a constraint driven
/// by a named parameter — the entire point of a *named* parameter is
/// that editing it ripples to whatever references it, the same way
/// editing a constraint's literal value already does.
///
/// **Deliberate scope-down, matching this project's established
/// treatment of undo-granularity questions** (design doc §5 open
/// question 3, and the sibling constraint-system project's stage 9): the
/// geometry this ripple moves is bracketed into one undo step below
/// (same `begin_undo`/`bump_entities`/`commit_undo_delta` pattern
/// `resolve_one_sketch_conflict` above uses), but the parameter *table*
/// edit itself has no undo entry of its own yet — that would need a new
/// `HistorySnapshot` variant mirroring `SketchConstraints`
/// (`push_sketch_constraints_history`). Left for a follow-up, not
/// silently dropped: undoing after an Apply undoes the geometry it
/// moved, not the parameter values themselves.
pub(super) fn apply_named_parameter_editor_rows(&mut self) {
let i = self.active_tab;
// A name shared by more than one row can't be resolved by picking
// one arbitrarily (`ParameterTable::set` would just let the last
// one silently win) -- refuse every row sharing that name up front,
// same as `named_parameters::preview`'s live check.
let duplicates = crate::ui::window::named_parameters::duplicate_name_rows(&self.named_parameter_editor_rows);
let mut table = crate::scene::named_parameters::ParameterTable::new();
let mut failed = 0usize;
for (idx, row) in self.named_parameter_editor_rows.iter().enumerate() {
let name = row.name.trim();
if name.is_empty() || duplicates.contains(&idx) {
continue;
}
if let Err(e) = table.set(name, row.formula.trim()) {
self.command_line.push_error(crate::tf!("Parameter '{}': {}", name, e).as_ref());
failed += 1;
}
}
if !duplicates.is_empty() {
self.command_line.push_error(crate::tf!("{} row(s) skipped: duplicate parameter name.", duplicates.len()).as_ref());
failed += duplicates.len();
}
let param_count = table.len();
self.tabs[i].scene.named_parameters = table;
// The whole table just changed, not one isolated value, so there is
// no cheaper "which handles actually moved" test worth doing here —
// re-solve every scope with a constraint driven by *any* named
// parameter (mirrors `solve_scope`'s own "full rebuild, not
// incremental" philosophy).
let touched: Vec<Handle> = self.tabs[i]
.scene
.sketch_constraints
.iter()
.flat_map(|set| set.constraints.iter())
.filter(|c| c.enabled && matches!(c.driving_param, Some(crate::scene::named_parameters::DrivingValue::Named(_))))
.flat_map(|c| c.refs.iter().map(|r| r.entity))
.collect();
if !touched.is_empty() {
let pending = self.begin_undo(i, "Apply named parameters", touched.len(), true);
let changes: Vec<(Handle, crate::scene::ChangeKind)> =
touched.into_iter().map(|h| (h, crate::scene::ChangeKind::Modified)).collect();
self.tabs[i].scene.bump_entities(&changes);
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
}
self.tabs[i].dirty = true;
if failed == 0 {
self.command_line.push_info(crate::tf!("{} parameter(s) applied.", param_count).as_ref());
}
}
fn apply_cmd_result_inner(&mut self, result: CmdResult) -> Task<Message> {
let i = self.active_tab;
let preserve_commit_layer = self.tabs[i]
@@ -6075,6 +6150,93 @@ mod sketch_constraint_undo_tests {
assert_eq!(set.constraints.len(), 2, "undo should restore the removed constraint");
}
/// `named_parameters_design.md` stage 4: applying the PARAMETERS editor's
/// working buffer both defines the parameter and re-solves whatever
/// constraint already references it by name — the entire point of a
/// *named* parameter, not just data plumbing.
#[test]
fn applying_named_parameter_rows_defines_and_resolves_referencing_constraints() {
let mut app = OpenCADStudio::new_for_test();
let _ = app.automation_op(r#"{"op":"new"}"#);
let line = add_line(&mut app, 0.0, 0.0, 10.0, 0.0);
// The constraint is added *before* the parameter exists -- allowed,
// same as `ParameterTable::set`'s own forward-reference tolerance;
// it just doesn't resolve to anything until the parameter is defined.
let _ = app.apply_cmd_result(CmdResult::AddSketchConstraint {
kind: ConstraintKind::Distance,
refs: vec![SketchRef::point(line, 0), SketchRef::point(line, 1)],
driving_param: Some(crate::scene::named_parameters::DrivingValue::Named("target_len".to_string())),
label: "Distance constraint",
});
app.named_parameter_editor_rows =
vec![crate::ui::window::named_parameters::ParamEditorRow { name: "target_len".to_string(), formula: "8".to_string() }];
app.apply_named_parameter_editor_rows();
assert_eq!(app.tabs[app.active_tab].scene.named_parameters().resolve("target_len"), Ok(8.0));
let (start, end) = match app.tabs[app.active_tab].scene.document.get_entity(line) {
Some(acadrust::EntityType::Line(l)) => (l.start, l.end),
other => panic!("expected a Line, got {other:?}"),
};
let len = ((end.x - start.x).powi(2) + (end.y - start.y).powi(2)).sqrt();
assert!((len - 8.0).abs() < 1e-6, "length should track the applied parameter, got {len}");
// Redefining the parameter and re-applying should ripple again.
app.named_parameter_editor_rows[0].formula = "3".to_string();
app.apply_named_parameter_editor_rows();
let (start, end) = match app.tabs[app.active_tab].scene.document.get_entity(line) {
Some(acadrust::EntityType::Line(l)) => (l.start, l.end),
other => panic!("expected a Line, got {other:?}"),
};
let len = ((end.x - start.x).powi(2) + (end.y - start.y).powi(2)).sqrt();
assert!((len - 3.0).abs() < 1e-6, "length should track the redefined parameter, got {len}");
}
/// A row that fails to validate (here, a formula that doesn't parse)
/// must not silently vanish or corrupt the table: it's reported as an
/// error and simply isn't added, while a valid row alongside it still
/// commits — the same isolate-the-failure treatment
/// `ParameterTable::resolve_all` already gives a bad reference.
#[test]
fn applying_a_row_with_a_malformed_formula_reports_an_error_and_keeps_the_good_row() {
let mut app = OpenCADStudio::new_for_test();
let _ = app.automation_op(r#"{"op":"new"}"#);
app.named_parameter_editor_rows = vec![
crate::ui::window::named_parameters::ParamEditorRow { name: "good".to_string(), formula: "10".to_string() },
crate::ui::window::named_parameters::ParamEditorRow { name: "bad".to_string(), formula: "1 +".to_string() },
];
app.apply_named_parameter_editor_rows();
assert_eq!(app.tabs[app.active_tab].scene.named_parameters().resolve("good"), Ok(10.0));
assert!(!app.tabs[app.active_tab].scene.named_parameters().contains("bad"), "a malformed row must not be added");
assert!(
app.command_line.history.iter().any(|e| e.kind == crate::ui::command_line::EntryKind::Error),
"a malformed row must be reported as an error, not silently dropped"
);
}
/// Two rows typed with the same name must not silently collapse into
/// "whichever `set` call happens last wins" — both are refused, and an
/// unrelated row still commits normally.
#[test]
fn applying_rows_with_a_duplicate_name_refuses_both_and_keeps_the_unrelated_row() {
let mut app = OpenCADStudio::new_for_test();
let _ = app.automation_op(r#"{"op":"new"}"#);
app.named_parameter_editor_rows = vec![
crate::ui::window::named_parameters::ParamEditorRow { name: "x".to_string(), formula: "1".to_string() },
crate::ui::window::named_parameters::ParamEditorRow { name: "x".to_string(), formula: "2".to_string() },
crate::ui::window::named_parameters::ParamEditorRow { name: "y".to_string(), formula: "3".to_string() },
];
app.apply_named_parameter_editor_rows();
assert!(!app.tabs[app.active_tab].scene.named_parameters().contains("x"), "a duplicated name must not be added at all");
assert_eq!(app.tabs[app.active_tab].scene.named_parameters().resolve("y"), Ok(3.0));
assert!(app.command_line.history.iter().any(|e| e.kind == crate::ui::command_line::EntryKind::Error));
}
/// Design doc §6.1's manual Coincident UI: two picks landing on two
/// different lines' endpoints should resolve to a real `SketchRef` pair
/// and add a Coincident constraint — the same `AddCoincidentConstraint`
+3
View File
@@ -292,6 +292,8 @@ pub fn start_allowed(cmd: &str) -> bool {
| "PERF"
| "CUI"
| "ALIASEDIT"
| "OPTIONS"
| "OP"
| "CUILOAD"
| "CUIIMPORT"
)
@@ -312,6 +314,7 @@ inventory::submit!(crate::command::CommandRegistration {
"CLEANSCREEN",
"CUI",
"DSETTINGS",
"PARAMETERS",
"GRID",
"ISODRAFT",
"ISOPLANE",
+19
View File
@@ -240,6 +240,25 @@ impl OpenCADStudio {
return Some(Task::done(Message::AliasEditorOpen));
}
// ── OPTIONS — app preferences dialog ────────────────────────────
// Registered for autocomplete (`commands/mod.rs`'s "one-shot
// commands" list) but had no actual dispatch arm anywhere — the
// Start-tab welcome screen's "Options" button emits
// `Message::OptionsOpen` directly and was the only way to reach
// it. Found while live-testing the new UI-scale setting (nothing
// to do with that feature itself, just discovered along the
// way). "OP" is AutoCAD's own short alias for this command.
"OPTIONS" | "OP" => {
return Some(Task::done(Message::OptionsOpen));
}
// ── PARAMETERS — named-parameter/expression table editor ───────
// `docs/named_parameters_design.md` stage 4. AutoCAD's own
// command name for the equivalent feature (Parameters Manager).
"PARAMETERS" => {
return Some(Task::done(Message::NamedParametersOpen));
}
// ── CUI — keyboard shortcut / key-binding editor ───────────────
"CUI" => {
return Some(Task::done(Message::ShortcutsPanelOpen));
+40
View File
@@ -504,6 +504,12 @@ pub(super) struct OpenCADStudio {
crosshair_color_input: String,
/// Model-space lineweight preview scale, in percent (25..=200).
lineweight_display_scale: i32,
/// Whole-interface scale, in percent (50..=200) — drives iced's own
/// per-window `scale_factor` (`run`'s `.scale_factor(...)` hook), so it
/// resizes every panel, dialog, the ribbon, and their text uniformly
/// rather than needing a separate font-size knob threaded through each
/// panel's own view code.
ui_scale: i32,
/// Isometric drafting state and active axis pair.
isometric_drafting: bool,
iso_plane: settings::IsoPlane,
@@ -1018,6 +1024,15 @@ pub(super) struct OpenCADStudio {
/// edited. Seeded from `command_aliases` on open, committed back on close.
alias_editor_rows: Vec<(String, String)>,
// ── Named Parameters (docs/named_parameters_design.md, stage 4) ────────
/// Working buffer for the PARAMETERS modal. Unlike `alias_editor_rows`,
/// this isn't a copy of a separate app-level store — the real state
/// lives per-document at `Scene::named_parameters`; this buffer is
/// seeded from the active tab's table on open and only written back to
/// it on Apply (`apply_named_parameter_editor_rows`,
/// `src/app/named_parameters.rs`).
named_parameter_editor_rows: Vec<crate::ui::window::named_parameters::ParamEditorRow>,
// ── Layout Manager Panel ──────────────────────────────────────────────
layout_manager_selected: String,
layout_manager_rename_buf: String,
@@ -1695,6 +1710,7 @@ pub enum ModalKind {
AttributeEditor,
LayerDeleteWarning,
Aliases,
NamedParameters,
ScaleManager,
/// Add / remove the annotation scales a single selected object has a
/// per-object representation for.
@@ -1937,6 +1953,8 @@ pub enum Message {
CursorTypeChanged(settings::CursorType),
/// Set the model-space lineweight preview scale from Options.
LineweightDisplayScaleChanged(i32),
/// Set the whole-interface scale (`ui_scale`) from Options.
UiScaleChanged(i32),
/// Set the ribbon's large-button icon-label font size from Options.
RibbonLabelFontSizeChanged(i32),
/// Set the ribbon's panel-title ("Draw", "Modify", …) font size from Options.
@@ -2706,6 +2724,24 @@ pub enum Message {
AliasEditorRemove(usize),
/// Commit the edited rows to the alias table (Apply button); stays open.
AliasEditorApply,
// ── Named Parameters (PARAMETERS) ───────────────────────────────────
/// Open the named-parameter editor, seeding rows from the active tab's
/// `Scene::named_parameters`.
NamedParametersOpen,
/// Live edit of the name or formula in row `idx`.
NamedParametersInput {
idx: usize,
field: crate::ui::window::named_parameters::ParamField,
value: String,
},
/// Append a blank parameter row.
NamedParametersAdd,
/// Remove parameter row `idx`.
NamedParametersRemove(usize),
/// Commit the edited rows to `Scene::named_parameters` (Apply button)
/// and re-solve every constraint that reads a named parameter; stays
/// open.
NamedParametersApply,
// ── About window ────────────────────────────────────────────────────
AboutOpen,
/// Close whatever in-canvas modal dialog is open (Plan B).
@@ -3378,6 +3414,7 @@ impl OpenCADStudio {
crosshair_color: None,
crosshair_color_input: String::new(),
lineweight_display_scale: 100,
ui_scale: 100,
isometric_drafting: false,
iso_plane: settings::IsoPlane::Left,
snap_angle_deg: 0.0,
@@ -3579,6 +3616,7 @@ impl OpenCADStudio {
// Command aliases (populated from ocad.pgp just after construction)
command_aliases: rustc_hash::FxHashMap::default(),
alias_editor_rows: Vec::new(),
named_parameter_editor_rows: Vec::new(),
// Layout Manager
layout_manager_selected: "Model".to_string(),
layer_state_selected: None,
@@ -3978,6 +4016,7 @@ pub fn run() -> iced::Result {
..iced::Settings::default()
})
.subscription(OpenCADStudio::subscription)
.scale_factor(|state: &OpenCADStudio, _window: window::Id| state.ui_scale as f32 / 100.0)
.title(|state: &OpenCADStudio, window_id: window::Id| {
let _ = window_id; // all dialogs are in-canvas modals now
if let Some(tab) = state.tabs.get(state.active_tab) {
@@ -4022,6 +4061,7 @@ pub fn run_web() -> iced::Result {
OpenCADStudio::view_main,
)
.subscription(OpenCADStudio::subscription)
.scale_factor(|state: &OpenCADStudio| state.ui_scale as f32 / 100.0)
.title(|_state: &OpenCADStudio| {
concat!("Open CAD Studio ", env!("OCS_APP_VERSION")).to_string()
})
+9
View File
@@ -166,6 +166,14 @@ pub struct UserSettings {
pub crosshair_color: Option<[u8; 3]>,
/// Model-space lineweight preview scale as a percentage.
pub lineweight_display_scale: i32,
/// Whole-interface scale, as a percentage — every panel, dialog, the
/// ribbon, and their text, not just one widget's font size. Wired
/// straight into iced's own per-window `scale_factor` hook
/// (`OpenCADStudio::run`), so a change takes effect immediately with no
/// per-panel plumbing needed. Relies on the struct-level `#[serde(default)]`
/// (via `impl Default` below) to backfill this field for a config file
/// saved before this setting existed, same as every other field here.
pub ui_scale: i32,
/// Isometric drafting changes the grid and crosshair to the active axis pair.
pub isometric_drafting: bool,
pub iso_plane: IsoPlane,
@@ -310,6 +318,7 @@ impl Default for UserSettings {
cursor_type: CursorType::Crosshair,
crosshair_color: None,
lineweight_display_scale: 100,
ui_scale: 100,
isometric_drafting: false,
iso_plane: IsoPlane::Left,
snap_angle_deg: 0.0,
+9
View File
@@ -368,6 +368,7 @@ impl OpenCADStudio {
cursor_type: self.cursor_type,
crosshair_color: self.crosshair_color,
lineweight_display_scale: self.lineweight_display_scale,
ui_scale: self.ui_scale,
isometric_drafting: self.isometric_drafting,
iso_plane: self.iso_plane,
snap_angle_deg: self.snap_angle_deg,
@@ -425,6 +426,7 @@ impl OpenCADStudio {
.map(crate::app::config::rgb_to_hex)
.unwrap_or_default();
self.lineweight_display_scale = s.lineweight_display_scale.clamp(25, 200);
self.ui_scale = s.ui_scale.clamp(50, 200);
self.isometric_drafting = s.isometric_drafting;
self.iso_plane = s.iso_plane;
self.snap_angle_deg = if s.snap_angle_deg.is_finite() {
@@ -1394,6 +1396,9 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
// Design doc §8 stage 4: read back any persisted sketch
// constraint sets right after the document is installed.
self.tabs[i].scene.load_sketch_constraints_from_document();
// named_parameters_design.md stage 2: same load-time hook,
// for the document-wide parameter table.
self.tabs[i].scene.load_named_parameters_from_document();
self.tabs[i].active_layer = self.tabs[i]
.scene
.document
@@ -1660,6 +1665,9 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
// Design doc §8 stage 4: write any sketch constraint sets into the
// document right before it's serialized.
self.tabs[i].scene.materialize_sketch_constraints_for_save();
// named_parameters_design.md stage 2: same save-time hook, for the
// document-wide parameter table.
self.tabs[i].scene.materialize_named_parameters_for_save();
}
#[cfg(not(target_arch = "wasm32"))]
@@ -2611,6 +2619,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
self.stamp_header_sysvars(i);
self.sync_solid_models_for_save(i);
self.tabs[i].scene.materialize_sketch_constraints_for_save();
self.tabs[i].scene.materialize_named_parameters_for_save();
let tab_id = self.tabs[i].id;
let bounds = crate::ui::wrap_bar::dropdown_bounds(
crate::app::view::VIEWPORT_CAPTURE_BOUNDS_ID,
+43
View File
@@ -5861,6 +5861,43 @@ impl OpenCADStudio {
Task::none()
}
// ── Named Parameters (PARAMETERS) ─────────────────────────────────
Message::NamedParametersOpen => {
let i = self.active_tab;
self.named_parameter_editor_rows = self.tabs[i]
.scene
.named_parameters()
.iter()
.map(|p| crate::ui::window::named_parameters::ParamEditorRow { name: p.name.clone(), formula: p.source.clone() })
.collect();
self.active_modal = Some(super::ModalKind::NamedParameters);
Task::none()
}
Message::NamedParametersInput { idx, field, value } => {
use crate::ui::window::named_parameters::ParamField;
if let Some(row) = self.named_parameter_editor_rows.get_mut(idx) {
match field {
ParamField::Name => row.name = value,
ParamField::Formula => row.formula = value,
}
}
Task::none()
}
Message::NamedParametersAdd => {
self.named_parameter_editor_rows.push(crate::ui::window::named_parameters::ParamEditorRow::default());
Task::none()
}
Message::NamedParametersRemove(idx) => {
if idx < self.named_parameter_editor_rows.len() {
self.named_parameter_editor_rows.remove(idx);
}
Task::none()
}
Message::NamedParametersApply => {
self.apply_named_parameter_editor_rows();
Task::none()
}
// ── Options / About windows ───────────────────────────────────
Message::OptionsOpen => {
self.active_modal = Some(super::ModalKind::Options);
@@ -5900,6 +5937,12 @@ impl OpenCADStudio {
Task::none()
}
Message::UiScaleChanged(value) => {
self.ui_scale = value.clamp(50, 200);
self.persist_settings_if_changed();
Task::none()
}
Message::RibbonLabelFontSizeChanged(value) => {
self.ribbon.set_label_font_size(value);
self.save_config();
+15
View File
@@ -13,6 +13,7 @@ impl OpenCADStudio {
Some(K::About) => crate::tr!("modal", "about"),
Some(K::Shortcuts) => crate::tr!("modal", "keyboard-shortcuts"),
Some(K::Aliases) => crate::tr!("modal", "command-aliases"),
Some(K::NamedParameters) => crate::t!("Named Parameters").into_owned(),
Some(K::Options) => crate::tr!("action", "options"),
Some(K::FindReplace) => crate::tr!("modal", "find-replace"),
Some(K::PluginManager) => crate::tr!("modal", "plugin-manager"),
@@ -173,6 +174,19 @@ impl OpenCADStudio {
},
)
}
super::super::ModalKind::NamedParameters => {
sized_flow(
ex,
620,
520,
|flow| {
crate::ui::window::named_parameters::view_window(
&self.named_parameter_editor_rows,
flow,
)
},
)
}
super::super::ModalKind::Options => sized_flow(
ex,
540,
@@ -191,6 +205,7 @@ impl OpenCADStudio {
self.crosshair_color,
&self.crosshair_color_input,
self.lineweight_display_scale,
self.ui_scale,
self.ribbon.label_font_size(),
self.ribbon.group_title_font_size(),
&self.model_space,
+8 -3
View File
@@ -1336,9 +1336,14 @@ pub enum CmdResult {
AddSketchConstraint {
kind: crate::scene::sketch_constraints::ConstraintKind,
refs: Vec<crate::scene::sketch_constraints::SketchRef>,
/// The typed target for a dimensional kind (Distance/Angle/Radius);
/// `None` for a purely geometric one.
driving_param: Option<f64>,
/// The typed target for a dimensional kind (Distance/Angle/Radius) —
/// a literal number, or a named-parameter reference
/// (`docs/named_parameters_design.md` stage 4, recognized by
/// `DistanceConstraintCommand`/`AngleConstraintCommand::
/// on_text_input` when the typed token matches a known parameter
/// name instead of parsing as a number); `None` for a purely
/// geometric kind.
driving_param: Option<crate::scene::named_parameters::DrivingValue>,
/// Undo-history label, e.g. `"Horizontal constraint"`.
label: &'static str,
},
+69 -11
View File
@@ -19,9 +19,30 @@ use glam::DVec3;
use crate::command::{CadCommand, CmdResult, InputKind};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::named_parameters::DrivingValue;
use crate::scene::sketch_constraints::{ConstraintKind, SketchRef};
use crate::scene::Scene;
/// Recognizes a typed prompt token as either a numeric literal or a
/// reference to an existing named parameter (`docs/
/// named_parameters_design.md` stage 4's "a way to pick a named parameter
/// instead of typing a literal" — the same prompt takes either, mirroring
/// how AutoCAD's own dimensional-constraint prompts accept an expression in
/// place of a bare value). `known_names` is a one-time snapshot taken at
/// command construction (`DistanceConstraintCommand`/`AngleConstraintCommand
/// ::new`, which already has `&Scene`) since `on_text_input` itself has no
/// document access — the same constraint `default_value` already works
/// around. Only *existence* is checked here; the actual value is resolved
/// fresh at solve time (`sketch_solve::build_constraint`), consistent with
/// this project's "no incremental/cached resolution" approach throughout.
fn parse_driving_value(text: &str, known_names: &[String]) -> Option<DrivingValue> {
let text = text.trim();
if let Ok(value) = text.parse::<f64>() {
return Some(DrivingValue::Literal(value));
}
known_names.iter().find(|n| n.as_str() == text).map(|n| DrivingValue::Named(n.clone()))
}
pub mod distance_tool {
use super::*;
pub fn tool() -> ToolDef {
@@ -55,6 +76,9 @@ pub struct DistanceConstraintCommand {
/// split between line-length (`Distance`) and circle-radius (`Radius`).
is_circle: bool,
default_value: f64,
/// Snapshot of `Scene::named_parameters`' names at construction time —
/// see `parse_driving_value`'s doc comment for why a snapshot.
known_param_names: Vec<String>,
}
impl DistanceConstraintCommand {
@@ -70,12 +94,15 @@ impl DistanceConstraintCommand {
EntityType::Circle(c) => (true, c.radius),
_ => return None,
};
Some(Self { handle, is_circle, default_value })
let known_param_names = scene.named_parameters().iter().map(|p| p.name.clone()).collect();
Some(Self { handle, is_circle, default_value, known_param_names })
}
fn build(&self, target: f64) -> Option<CmdResult> {
if target <= 0.0 {
return None;
fn build(&self, target: DrivingValue) -> Option<CmdResult> {
if let DrivingValue::Literal(v) = target {
if v <= 0.0 {
return None;
}
}
let (kind, refs, label) = if self.is_circle {
(ConstraintKind::Radius, vec![SketchRef::whole(self.handle)], "Radius constraint")
@@ -108,11 +135,11 @@ impl CadCommand for DistanceConstraintCommand {
}
fn on_enter(&mut self) -> CmdResult {
self.build(self.default_value).unwrap_or(CmdResult::Cancel)
self.build(DrivingValue::Literal(self.default_value)).unwrap_or(CmdResult::Cancel)
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let value: f64 = text.trim().parse().ok()?;
let value = parse_driving_value(text, &self.known_param_names)?;
self.build(value)
}
@@ -126,6 +153,9 @@ pub struct AngleConstraintCommand {
fixed_handle: Handle,
moving_handle: Handle,
default_value: f64,
/// Snapshot of `Scene::named_parameters`' names at construction time —
/// see `parse_driving_value`'s doc comment for why a snapshot.
known_param_names: Vec<String>,
}
impl AngleConstraintCommand {
@@ -139,14 +169,15 @@ impl AngleConstraintCommand {
let a1 = (f.end.y - f.start.y).atan2(f.end.x - f.start.x);
let a2 = (m.end.y - m.start.y).atan2(m.end.x - m.start.x);
let default_value = (a2 - a1).to_degrees();
Some(Self { fixed_handle: fixed, moving_handle: moving, default_value })
let known_param_names = scene.named_parameters().iter().map(|p| p.name.clone()).collect();
Some(Self { fixed_handle: fixed, moving_handle: moving, default_value, known_param_names })
}
fn build(&self, target_degrees: f64) -> Option<CmdResult> {
fn build(&self, target: DrivingValue) -> Option<CmdResult> {
Some(CmdResult::AddSketchConstraint {
kind: ConstraintKind::Angle,
refs: vec![SketchRef::whole(self.fixed_handle), SketchRef::whole(self.moving_handle)],
driving_param: Some(target_degrees),
driving_param: Some(target),
label: "Angle constraint",
})
}
@@ -170,11 +201,11 @@ impl CadCommand for AngleConstraintCommand {
}
fn on_enter(&mut self) -> CmdResult {
self.build(self.default_value).unwrap_or(CmdResult::Cancel)
self.build(DrivingValue::Literal(self.default_value)).unwrap_or(CmdResult::Cancel)
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let value: f64 = text.trim().parse().ok()?;
let value = parse_driving_value(text, &self.known_param_names)?;
self.build(value)
}
@@ -182,3 +213,30 @@ impl CadCommand for AngleConstraintCommand {
CmdResult::Cancel
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_driving_value_prefers_a_numeric_literal() {
let known = vec!["hole_dia".to_string()];
assert_eq!(parse_driving_value("12.5", &known), Some(DrivingValue::Literal(12.5)));
// Distance's own `build` rejects a non-positive target later; the
// parse step itself accepts any number, negative included.
assert_eq!(parse_driving_value("-3", &known), Some(DrivingValue::Literal(-3.0)));
}
#[test]
fn parse_driving_value_recognizes_a_known_parameter_name() {
let known = vec!["hole_dia".to_string(), "plate_len".to_string()];
assert_eq!(parse_driving_value("hole_dia", &known), Some(DrivingValue::Named("hole_dia".to_string())));
}
#[test]
fn parse_driving_value_rejects_an_unknown_token() {
let known = vec!["hole_dia".to_string()];
assert_eq!(parse_driving_value("bogus", &known), None);
assert_eq!(parse_driving_value("", &known), None);
}
}
+14
View File
@@ -36,6 +36,8 @@ mod group_layer;
mod layout;
mod limits;
mod modify;
pub mod named_parameters;
mod named_parameters_persist;
mod mspace;
mod page_setup;
mod paper;
@@ -1934,6 +1936,17 @@ pub struct Scene {
/// touches directly, materialized into `document.objects` only right
/// before a save and read back right after a load.
pub(crate) sketch_constraints: Vec<sketch_constraints::SketchConstraintSet>,
/// Document-wide named-parameter/expression table (`docs/
/// named_parameters_design.md`). One table per document (not per-scope,
/// unlike `sketch_constraints` — a parameter is meant to be shared
/// across the whole drawing), lives on `Scene` for the same reason
/// `sketch_constraints` does: co-located with the `document: CadDocument`
/// it's synced against. Synced with a single document-level
/// `OCS_NAMED_PARAMETERS` XRecord (hung off `document.header.
/// named_objects_dict_handle`, not a per-`BlockRecord` one) around
/// open/save (`named_parameters_persist.rs`, same "lazy" model as
/// `sketch_persist.rs`).
pub(crate) named_parameters: named_parameters::ParameterTable,
/// Tessellated block definitions in block-local coords, keyed by render
/// background and block epoch. Model and Paper adapt black/white colours
/// differently; retaining both variants prevents a full block rebuild on
@@ -2167,6 +2180,7 @@ impl Scene {
dependency_index_cache: RefCell::new(None),
associative_hatch_source_cache: RefCell::new(None),
sketch_constraints: Vec::new(),
named_parameters: named_parameters::ParameterTable::new(),
has_associative_centers: std::cell::Cell::new(None),
block_defn_cache: RefCell::new(HashMap::default()),
entity_index_cache: RefCell::new(None),
+898
View File
@@ -0,0 +1,898 @@
//! Named, formula-capable document parameters — data model.
//!
//! Design: `docs/named_parameters_design.md`. This is stage 1 of that doc's
//! §4 staged plan: the parameter table type plus a small expression
//! parser/evaluator, unit-tested in isolation — no document/UI integration
//! yet (that's stage 2's XRecord persistence, mirroring
//! [`super::sketch_persist`], and stage 3's
//! `SketchConstraint::driving_param` wiring).
//!
//! A [`Parameter`] is a name plus a formula: either a bare literal (`12.5`)
//! or an expression referencing other parameters by name (`2 * hole_dia +
//! 1.5`). A [`ParameterTable`] holds every parameter for one document (per
//! the design doc's §2: one table per document, not per-[`super::
//! sketch_constraints::SketchScope`]) and resolves each one's numeric value,
//! rejecting a circular reference at the moment it would be introduced
//! rather than at evaluation time — the design doc's explicit requirement
//! ("A referencing B referencing A must be rejected, not silently
//! infinite-loop or panic"), and the same real-time-rejection behavior a
//! spreadsheet gives for a circular formula.
//!
//! No existing workspace dependency parses expressions (the design doc's §2
//! flags checking `evalexpr`/`meval` before hand-rolling one). Hand-rolled
//! here instead: cycle detection needs the set of names an expression
//! references *before* it's evaluated, which means owning the parse tree —
//! treating evaluation as a black box, the way a generic expression-eval
//! crate is normally used, would just mean re-deriving that same dependency
//! information by other means. [`Expr::refs`] *is* that dependency
//! information, not extra work spent duplicating what such a crate computes
//! internally anyway.
use super::Scene;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
/// A parsed formula. Ordinary arithmetic plus [`Expr::Ref`] (a reference to
/// another parameter by name — what makes this a *named*-parameter system
/// rather than a plain calculator) and [`Expr::Call`] (a function call).
/// `Call` is in the grammar now so it doesn't need a rewrite if/when more
/// functions are wanted later (design doc §5 open question 2 leaves the
/// exact v1 function set undecided); only a small, conservative builtin set
/// is actually wired up for now (see `call_builtin`).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum Expr {
Number(f64),
Ref(String),
Neg(Box<Expr>),
Add(Box<Expr>, Box<Expr>),
Sub(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
Div(Box<Expr>, Box<Expr>),
Pow(Box<Expr>, Box<Expr>),
Call(String, Vec<Expr>),
}
impl Expr {
/// Every parameter name this expression reads, deduplicated in
/// first-seen order. This is the entire basis for cycle detection
/// (`ParameterTable::set`): one dependency edge per name returned here.
fn refs(&self, out: &mut Vec<String>) {
match self {
Expr::Number(_) => {}
Expr::Ref(name) => {
if !out.contains(name) {
out.push(name.clone());
}
}
Expr::Neg(a) => a.refs(out),
Expr::Add(a, b) | Expr::Sub(a, b) | Expr::Mul(a, b) | Expr::Div(a, b) | Expr::Pow(a, b) => {
a.refs(out);
b.refs(out);
}
Expr::Call(_, args) => {
for arg in args {
arg.refs(out);
}
}
}
}
}
/// Everything that can go wrong parsing, defining, or resolving a
/// parameter. Every case is reported back to the caller (and, in later
/// stages, the UI) rather than panicking — matching this codebase's
/// established pattern of isolating a bad derived value instead of letting
/// it take down an unrelated computation (e.g. dangling-constraint handling
/// in `sketch_solve.rs`).
#[derive(Debug, Clone, PartialEq)]
pub enum ParamError {
/// A formula's text doesn't parse. The `String` is a short, human-
/// readable reason, not a machine-parseable code.
Parse(String),
/// Not a valid identifier, or collides with a builtin function name
/// (`is_reserved_name`).
InvalidName(String),
/// Accepting this formula would close a circular reference right now.
/// The path reads start-to-finish, e.g. `["a", "b", "a"]` for `a = b`,
/// `b = a`.
Cycle(Vec<String>),
UnknownFunction(String),
WrongArgCount { function: String, expected: usize, found: usize },
/// Raised by `resolve`/`resolve_all`, not `set` — a formula may
/// legitimately reference a parameter that doesn't exist *yet* (order of
/// definition shouldn't matter, mirroring a spreadsheet); this only
/// surfaces once something actually tries to read the missing value.
UndefinedReference(String),
DivisionByZero,
}
impl fmt::Display for ParamError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParamError::Parse(msg) => write!(f, "formula error: {msg}"),
ParamError::InvalidName(name) => write!(f, "'{name}' is not a valid parameter name"),
ParamError::Cycle(path) => write!(f, "circular reference: {}", path.join(" -> ")),
ParamError::UnknownFunction(name) => write!(f, "unknown function '{name}'"),
ParamError::WrongArgCount { function, expected, found } => {
write!(f, "{function}() expects {expected} argument(s), found {found}")
}
ParamError::UndefinedReference(name) => write!(f, "'{name}' is not defined"),
ParamError::DivisionByZero => write!(f, "division by zero"),
}
}
}
impl std::error::Error for ParamError {}
/// The v1 builtin function set — deliberately small (design doc §5 open
/// question 2 defers the full surface). Also doubles as the reserved-word
/// list `is_reserved_name` checks a parameter name against, so a formula
/// like `sqrt(x)` is never ambiguous between "call the builtin" and
/// "multiply by a parameter named sqrt".
const BUILTIN_FUNCTIONS: &[&str] = &["sqrt", "abs", "pow", "min", "max"];
pub fn is_reserved_name(name: &str) -> bool {
BUILTIN_FUNCTIONS.contains(&name)
}
/// A valid parameter name: starts with a letter or underscore, followed by
/// letters/digits/underscores. (Design doc §5 open question 5.)
pub fn is_valid_name(name: &str) -> bool {
let mut chars = name.chars();
match chars.next() {
Some(c) if c.is_alphabetic() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_alphanumeric() || c == '_')
}
/// A dimensional constraint's driving value (design doc §2's own suggested
/// shape for `SketchConstraint::driving_param`, stage 3 of the named-
/// parameters staged plan): either a literal number typed directly, or a
/// reference to a named parameter resolved through a [`ParameterTable`] at
/// solve time. Lives here rather than in `sketch_constraints.rs` since
/// resolving `Named` needs `ParameterTable`, which this module owns.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum DrivingValue {
Literal(f64),
Named(String),
}
impl DrivingValue {
/// Resolves to a number: a literal resolves to itself (never fails); a
/// named reference resolves through `table`, which can fail the same
/// ways any [`ParameterTable::resolve`] call can (undefined reference,
/// division by zero — a cycle should be unreachable here since `set`
/// already refuses to create one).
pub fn resolve(&self, table: &ParameterTable) -> Result<f64, ParamError> {
match self {
DrivingValue::Literal(value) => Ok(*value),
DrivingValue::Named(name) => table.resolve(name),
}
}
}
impl From<f64> for DrivingValue {
fn from(value: f64) -> Self {
DrivingValue::Literal(value)
}
}
fn call_builtin(name: &str, args: &[f64]) -> Result<f64, ParamError> {
fn check_arity(name: &str, args: &[f64], expected: usize) -> Result<(), ParamError> {
if args.len() != expected {
return Err(ParamError::WrongArgCount { function: name.to_string(), expected, found: args.len() });
}
Ok(())
}
match name {
"sqrt" => {
check_arity(name, args, 1)?;
Ok(args[0].sqrt())
}
"abs" => {
check_arity(name, args, 1)?;
Ok(args[0].abs())
}
"pow" => {
check_arity(name, args, 2)?;
Ok(args[0].powf(args[1]))
}
"min" => {
check_arity(name, args, 2)?;
Ok(args[0].min(args[1]))
}
"max" => {
check_arity(name, args, 2)?;
Ok(args[0].max(args[1]))
}
_ => Err(ParamError::UnknownFunction(name.to_string())),
}
}
#[derive(Debug, Clone, PartialEq)]
enum Token {
Number(f64),
Ident(String),
Plus,
Minus,
Star,
Slash,
Caret,
LParen,
RParen,
Comma,
}
fn tokenize(src: &str) -> Result<Vec<Token>, ParamError> {
let chars: Vec<char> = src.chars().collect();
let mut tokens = Vec::new();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if c.is_whitespace() {
i += 1;
continue;
}
match c {
'+' => {
tokens.push(Token::Plus);
i += 1;
}
'-' => {
tokens.push(Token::Minus);
i += 1;
}
'*' => {
tokens.push(Token::Star);
i += 1;
}
'/' => {
tokens.push(Token::Slash);
i += 1;
}
'^' => {
tokens.push(Token::Caret);
i += 1;
}
'(' => {
tokens.push(Token::LParen);
i += 1;
}
')' => {
tokens.push(Token::RParen);
i += 1;
}
',' => {
tokens.push(Token::Comma);
i += 1;
}
_ if c.is_ascii_digit() || c == '.' => {
let start = i;
let mut seen_dot = c == '.';
i += 1;
while i < chars.len() {
let d = chars[i];
if d.is_ascii_digit() {
i += 1;
} else if d == '.' && !seen_dot {
seen_dot = true;
i += 1;
} else {
break;
}
}
// Scientific notation: 1e-3, 2.5E10. Only consumed when a
// digit (optionally signed) actually follows the e/E, so a
// bare trailing "e" is left for the identifier lexer instead
// (not reachable today since no builtin/constant is named
// "e", but keeps this branch from eating an identifier that
// happens to start with it, e.g. a formula like `2 * ecc`).
if i < chars.len() && (chars[i] == 'e' || chars[i] == 'E') {
let mut j = i + 1;
if j < chars.len() && (chars[j] == '+' || chars[j] == '-') {
j += 1;
}
if j < chars.len() && chars[j].is_ascii_digit() {
while j < chars.len() && chars[j].is_ascii_digit() {
j += 1;
}
i = j;
}
}
let text: String = chars[start..i].iter().collect();
let value: f64 = text.parse().map_err(|_| ParamError::Parse(format!("invalid number '{text}'")))?;
tokens.push(Token::Number(value));
}
_ if c.is_alphabetic() || c == '_' => {
let start = i;
i += 1;
while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
i += 1;
}
tokens.push(Token::Ident(chars[start..i].iter().collect()));
}
other => return Err(ParamError::Parse(format!("unexpected character '{other}'"))),
}
}
Ok(tokens)
}
struct Parser<'a> {
tokens: &'a [Token],
pos: usize,
}
impl<'a> Parser<'a> {
fn peek(&self) -> Option<&Token> {
self.tokens.get(self.pos)
}
fn bump(&mut self) -> Option<Token> {
let t = self.tokens.get(self.pos).cloned();
if t.is_some() {
self.pos += 1;
}
t
}
fn expect(&mut self, expected: Token) -> Result<(), ParamError> {
match self.bump() {
Some(t) if t == expected => Ok(()),
Some(t) => Err(ParamError::Parse(format!("expected {expected:?}, found {t:?}"))),
None => Err(ParamError::Parse(format!("expected {expected:?}, found end of formula"))),
}
}
// Precedence, low to high: expr (+ -) > term (* /) > unary (-) > power (^) > primary.
// `^` binds tighter than unary minus on its left operand but is
// right-associative on its own exponent (`parse_unary` recursing back
// into `parse_power` covers `2^-2` == `2^(-2)`, and `2^3^2` ==
// `2^(3^2)`).
fn parse_expr(&mut self) -> Result<Expr, ParamError> {
let mut lhs = self.parse_term()?;
loop {
match self.peek() {
Some(Token::Plus) => {
self.pos += 1;
lhs = Expr::Add(Box::new(lhs), Box::new(self.parse_term()?));
}
Some(Token::Minus) => {
self.pos += 1;
lhs = Expr::Sub(Box::new(lhs), Box::new(self.parse_term()?));
}
_ => break,
}
}
Ok(lhs)
}
fn parse_term(&mut self) -> Result<Expr, ParamError> {
let mut lhs = self.parse_unary()?;
loop {
match self.peek() {
Some(Token::Star) => {
self.pos += 1;
lhs = Expr::Mul(Box::new(lhs), Box::new(self.parse_unary()?));
}
Some(Token::Slash) => {
self.pos += 1;
lhs = Expr::Div(Box::new(lhs), Box::new(self.parse_unary()?));
}
_ => break,
}
}
Ok(lhs)
}
fn parse_unary(&mut self) -> Result<Expr, ParamError> {
match self.peek() {
Some(Token::Minus) => {
self.pos += 1;
Ok(Expr::Neg(Box::new(self.parse_unary()?)))
}
Some(Token::Plus) => {
self.pos += 1;
self.parse_unary()
}
_ => self.parse_power(),
}
}
fn parse_power(&mut self) -> Result<Expr, ParamError> {
let base = self.parse_primary()?;
if let Some(Token::Caret) = self.peek() {
self.pos += 1;
let exponent = self.parse_unary()?;
return Ok(Expr::Pow(Box::new(base), Box::new(exponent)));
}
Ok(base)
}
fn parse_primary(&mut self) -> Result<Expr, ParamError> {
match self.bump() {
Some(Token::Number(n)) => Ok(Expr::Number(n)),
Some(Token::Ident(name)) => {
if matches!(self.peek(), Some(Token::LParen)) {
self.pos += 1;
let mut args = Vec::new();
if !matches!(self.peek(), Some(Token::RParen)) {
loop {
args.push(self.parse_expr()?);
if matches!(self.peek(), Some(Token::Comma)) {
self.pos += 1;
} else {
break;
}
}
}
self.expect(Token::RParen)?;
Ok(Expr::Call(name, args))
} else {
Ok(Expr::Ref(name))
}
}
Some(Token::LParen) => {
let inner = self.parse_expr()?;
self.expect(Token::RParen)?;
Ok(inner)
}
Some(other) => Err(ParamError::Parse(format!("unexpected token {other:?}"))),
None => Err(ParamError::Parse("unexpected end of formula".to_string())),
}
}
}
/// Parses a formula's text into an [`Expr`], without evaluating or
/// resolving any reference. `ParameterTable::set` is the usual entry point;
/// exposed standalone so a UI stage can validate-as-you-type without
/// touching the table.
pub fn parse(source: &str) -> Result<Expr, ParamError> {
let tokens = tokenize(source)?;
if tokens.is_empty() {
return Err(ParamError::Parse("empty formula".to_string()));
}
let mut parser = Parser { tokens: &tokens, pos: 0 };
let expr = parser.parse_expr()?;
if parser.pos != tokens.len() {
return Err(ParamError::Parse(format!("unexpected trailing token {:?}", tokens[parser.pos])));
}
Ok(expr)
}
/// One named parameter: a name plus its formula, both the original typed
/// text (`source`, for redisplay/re-editing) and the parsed [`Expr`]
/// (`expr`, for evaluation).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Parameter {
pub name: String,
pub source: String,
pub expr: Expr,
}
/// Every named parameter for one document (design doc §2: per-document, not
/// per-[`super::sketch_constraints::SketchScope`] — a block/xref does not
/// get its own independent table; open question 4 leaves whether that's the
/// final answer, but it's this stage's working assumption).
///
/// `parameters` is intentionally not `pub` (unlike the sibling
/// `SketchConstraintSet::constraints`): this table's entire value is that
/// every stored formula is already known to be parse-valid and cycle-free,
/// an invariant only `set`/`remove` maintain. A direct external push could
/// silently violate it.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ParameterTable {
parameters: Vec<Parameter>,
}
impl ParameterTable {
pub fn new() -> Self {
Self::default()
}
pub fn is_empty(&self) -> bool {
self.parameters.is_empty()
}
pub fn len(&self) -> usize {
self.parameters.len()
}
/// Insertion order — the order parameters were first defined in, stable
/// across edits to existing ones. The natural listing order for a
/// future parameters panel.
pub fn iter(&self) -> impl Iterator<Item = &Parameter> {
self.parameters.iter()
}
pub fn get(&self, name: &str) -> Option<&Parameter> {
self.parameters.iter().find(|p| p.name == name)
}
pub fn contains(&self, name: &str) -> bool {
self.get(name).is_some()
}
fn index_of(&self, name: &str) -> Option<usize> {
self.parameters.iter().position(|p| p.name == name)
}
/// Defines a new parameter or redefines an existing one's formula (one
/// upsert operation — there is no separate "must not already exist"
/// entry point; a future UI stage that wants add-vs-rename semantics can
/// check `contains` itself first).
///
/// Rejects: an invalid identifier or one colliding with a builtin
/// function name (`is_valid_name`/`is_reserved_name`), a formula that
/// fails to parse, and — the design doc's explicit requirement — a
/// formula that would close a circular reference right now. A reference
/// to a parameter that doesn't exist *yet* is not rejected (order of
/// definition shouldn't matter, mirroring a spreadsheet); it only
/// surfaces as [`ParamError::UndefinedReference`] when [`Self::resolve`]
/// actually needs that value.
pub fn set(&mut self, name: &str, source: &str) -> Result<(), ParamError> {
if !is_valid_name(name) || is_reserved_name(name) {
return Err(ParamError::InvalidName(name.to_string()));
}
let expr = parse(source)?;
self.check_no_cycle(name, &expr)?;
match self.index_of(name) {
Some(i) => {
self.parameters[i].source = source.to_string();
self.parameters[i].expr = expr;
}
None => self.parameters.push(Parameter { name: name.to_string(), source: source.to_string(), expr }),
}
Ok(())
}
/// Removes a parameter by name. Returns whether one was actually
/// removed. Deliberately does not check whether another parameter's
/// formula still references `name` — that becomes an
/// [`ParamError::UndefinedReference`] for the referencing parameter the
/// next time it's resolved, the same graceful-degradation treatment an
/// unresolvable reference gets everywhere else in this module, rather
/// than a special "can't delete, still in use" rule this stage doesn't
/// need to invent (a future UI stage may still want to warn before
/// deleting — that's a UI-layer decision, not a data-layer one).
pub fn remove(&mut self, name: &str) -> bool {
match self.index_of(name) {
Some(i) => {
self.parameters.remove(i);
true
}
None => false,
}
}
/// Would accepting `new_expr` as `name`'s formula close a circular
/// reference right now? Walks the dependency graph from each of
/// `new_expr`'s direct references, following each visited parameter's
/// *existing* stored formula, looking for a path back to `name`. A
/// reference to a name not currently in the table just terminates that
/// branch — no cycle through something that doesn't exist yet.
fn check_no_cycle(&self, name: &str, new_expr: &Expr) -> Result<(), ParamError> {
let mut direct_refs = Vec::new();
new_expr.refs(&mut direct_refs);
for start in &direct_refs {
let mut visiting = Vec::new();
if let Some(mut path) = self.find_path(start, name, &mut visiting) {
let mut full = vec![name.to_string()];
full.append(&mut path);
return Err(ParamError::Cycle(full));
}
}
Ok(())
}
/// Depth-first search from `current` to `target`, following each
/// visited parameter's existing formula's references. Returns the path
/// (starting with `current`) if `target` is reachable.
fn find_path(&self, current: &str, target: &str, visiting: &mut Vec<String>) -> Option<Vec<String>> {
if current == target {
return Some(vec![current.to_string()]);
}
if visiting.iter().any(|v| v == current) {
// A cycle that doesn't involve `target` shouldn't be reachable
// (`set` refuses to create one), but bail out rather than loop
// forever if that invariant is ever violated some other way
// (e.g. a foreign/hand-edited file deserialized straight into a
// table without going through `set`).
return None;
}
let param = self.get(current)?;
visiting.push(current.to_string());
let mut refs = Vec::new();
param.expr.refs(&mut refs);
for r in &refs {
if let Some(mut rest) = self.find_path(r, target, visiting) {
let mut path = vec![current.to_string()];
path.append(&mut rest);
visiting.pop();
return Some(path);
}
}
visiting.pop();
None
}
/// Resolves one parameter's numeric value, evaluating its dependency
/// chain as needed. No state is kept between calls (a fresh memo cache
/// every call) — mirrors `refresh_sketch_constraints`'s
/// rebuild-from-scratch-per-trigger model (design doc §2): this stage
/// has no document/UI integration yet to decide when a cached value
/// would go stale, so it simply never caches across calls.
pub fn resolve(&self, name: &str) -> Result<f64, ParamError> {
let mut cache = HashMap::new();
let mut stack = Vec::new();
self.resolve_inner(name, &mut cache, &mut stack)
}
/// Resolves every parameter, isolating a failure to just the parameters
/// it actually affects rather than aborting the whole table — the same
/// don't-let-one-bad-item-take-down-everything-else treatment this
/// codebase already gives dangling constraints and similar derived-data
/// failures elsewhere.
pub fn resolve_all(&self) -> HashMap<String, Result<f64, ParamError>> {
let mut out = HashMap::with_capacity(self.parameters.len());
for param in &self.parameters {
out.insert(param.name.clone(), self.resolve(&param.name));
}
out
}
fn resolve_inner(&self, name: &str, cache: &mut HashMap<String, f64>, stack: &mut Vec<String>) -> Result<f64, ParamError> {
if let Some(v) = cache.get(name) {
return Ok(*v);
}
if stack.iter().any(|s| s == name) {
// Defense in depth only — `set` already refuses to create a
// cycle; see `find_path`'s doc comment for how one could still
// end up here.
let mut cyc = stack.clone();
cyc.push(name.to_string());
return Err(ParamError::Cycle(cyc));
}
let param = self.get(name).ok_or_else(|| ParamError::UndefinedReference(name.to_string()))?;
stack.push(name.to_string());
let value = self.eval(&param.expr, cache, stack)?;
stack.pop();
cache.insert(name.to_string(), value);
Ok(value)
}
fn eval(&self, expr: &Expr, cache: &mut HashMap<String, f64>, stack: &mut Vec<String>) -> Result<f64, ParamError> {
Ok(match expr {
Expr::Number(n) => *n,
Expr::Ref(name) => self.resolve_inner(name, cache, stack)?,
Expr::Neg(a) => -self.eval(a, cache, stack)?,
Expr::Add(a, b) => self.eval(a, cache, stack)? + self.eval(b, cache, stack)?,
Expr::Sub(a, b) => self.eval(a, cache, stack)? - self.eval(b, cache, stack)?,
Expr::Mul(a, b) => self.eval(a, cache, stack)? * self.eval(b, cache, stack)?,
Expr::Div(a, b) => {
let denom = self.eval(b, cache, stack)?;
if denom == 0.0 {
return Err(ParamError::DivisionByZero);
}
self.eval(a, cache, stack)? / denom
}
Expr::Pow(a, b) => self.eval(a, cache, stack)?.powf(self.eval(b, cache, stack)?),
Expr::Call(fname, args) => {
let mut values = Vec::with_capacity(args.len());
for arg in args {
values.push(self.eval(arg, cache, stack)?);
}
call_builtin(fname, &values)?
}
})
}
}
impl Scene {
/// The document's named-parameter table (design doc §2: one per
/// document). Read-only accessor; use [`Scene::named_parameters_mut`] to
/// define/redefine/remove a parameter.
pub fn named_parameters(&self) -> &ParameterTable {
&self.named_parameters
}
pub fn named_parameters_mut(&mut self) -> &mut ParameterTable {
&mut self.named_parameters
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_arithmetic_with_ordinary_precedence() {
// 2 + 3 * 4 == 14, not 20 -- proves * binds tighter than +.
assert_eq!(eval_literal("2 + 3 * 4"), 14.0);
// (2 + 3) * 4 == 20 -- parens override precedence.
assert_eq!(eval_literal("(2 + 3) * 4"), 20.0);
// 2 ^ 3 ^ 2 == 2 ^ (3 ^ 2) == 512 -- right-associative power.
assert_eq!(eval_literal("2 ^ 3 ^ 2"), 512.0);
// Unary minus binds tighter than power's left operand is *read*,
// but -2^2 must still equal -(2^2) = -4 under this grammar (unary
// parses before power), matching common calculator convention.
assert_eq!(eval_literal("-2^2"), -4.0);
assert_eq!(eval_literal("2^-2"), 0.25);
assert_eq!(eval_literal("10 / 4"), 2.5);
assert_eq!(eval_literal("-(3 + 4)"), -7.0);
}
fn eval_literal(src: &str) -> f64 {
let table = ParameterTable::new();
let expr = parse(src).unwrap_or_else(|e| panic!("parse '{src}': {e}"));
table.eval(&expr, &mut HashMap::new(), &mut Vec::new()).unwrap_or_else(|e| panic!("eval '{src}': {e}"))
}
#[test]
fn parses_scientific_notation() {
assert_eq!(eval_literal("1e3"), 1000.0);
assert_eq!(eval_literal("1.5e-2"), 0.015);
}
#[test]
fn rejects_malformed_formulas() {
assert!(matches!(parse(""), Err(ParamError::Parse(_))));
assert!(matches!(parse("1 +"), Err(ParamError::Parse(_))));
assert!(matches!(parse("(1 + 2"), Err(ParamError::Parse(_))));
assert!(matches!(parse("1 2"), Err(ParamError::Parse(_))));
assert!(matches!(parse("1 @ 2"), Err(ParamError::Parse(_))));
}
#[test]
fn builtin_functions_evaluate_and_check_arity() {
assert_eq!(eval_literal("sqrt(16)"), 4.0);
assert_eq!(eval_literal("abs(-5)"), 5.0);
assert_eq!(eval_literal("min(3, 7)"), 3.0);
assert_eq!(eval_literal("max(3, 7)"), 7.0);
assert_eq!(eval_literal("pow(2, 10)"), 1024.0);
let table = ParameterTable::new();
let expr = parse("sqrt(1, 2)").unwrap();
let err = table.eval(&expr, &mut HashMap::new(), &mut Vec::new()).unwrap_err();
assert_eq!(err, ParamError::WrongArgCount { function: "sqrt".to_string(), expected: 1, found: 2 });
let expr = parse("bogus(1)").unwrap();
let err = table.eval(&expr, &mut HashMap::new(), &mut Vec::new()).unwrap_err();
assert_eq!(err, ParamError::UnknownFunction("bogus".to_string()));
}
#[test]
fn division_by_zero_is_reported_not_panicked() {
let table = ParameterTable::new();
let expr = parse("1 / 0").unwrap();
assert_eq!(table.eval(&expr, &mut HashMap::new(), &mut Vec::new()), Err(ParamError::DivisionByZero));
}
#[test]
fn set_rejects_invalid_and_reserved_names() {
let mut table = ParameterTable::new();
assert!(matches!(table.set("2bad", "1"), Err(ParamError::InvalidName(_))));
assert!(matches!(table.set("has space", "1"), Err(ParamError::InvalidName(_))));
assert!(matches!(table.set("", "1"), Err(ParamError::InvalidName(_))));
assert!(matches!(table.set("sqrt", "1"), Err(ParamError::InvalidName(_))));
assert!(table.set("_valid1", "1").is_ok());
}
#[test]
fn a_literal_parameter_resolves_to_its_number() {
let mut table = ParameterTable::new();
table.set("plate_len", "42.5").unwrap();
assert_eq!(table.resolve("plate_len"), Ok(42.5));
}
#[test]
fn a_formula_resolves_through_a_reference_chain() {
let mut table = ParameterTable::new();
table.set("hole_dia", "5").unwrap();
table.set("hole_spacing", "2 * hole_dia + 1.5").unwrap();
table.set("row_span", "3 * hole_spacing").unwrap();
assert_eq!(table.resolve("hole_spacing"), Ok(11.5));
assert_eq!(table.resolve("row_span"), Ok(34.5));
}
#[test]
fn resolving_an_undefined_reference_reports_which_name_is_missing() {
let mut table = ParameterTable::new();
table.set("a", "b + 1").unwrap();
assert_eq!(table.resolve("a"), Err(ParamError::UndefinedReference("b".to_string())));
}
#[test]
fn set_rejects_a_direct_self_reference() {
let mut table = ParameterTable::new();
let err = table.set("a", "a + 1").unwrap_err();
assert!(matches!(err, ParamError::Cycle(_)));
assert!(!table.contains("a"), "a rejected formula must not be stored");
}
#[test]
fn set_rejects_an_indirect_cycle_at_the_definition_that_closes_it() {
let mut table = ParameterTable::new();
table.set("a", "b").unwrap();
// b doesn't exist yet -- not a cycle, a is just unresolved for now.
assert_eq!(table.resolve("a"), Err(ParamError::UndefinedReference("b".to_string())));
// Defining b = a closes the loop right here and must be rejected,
// leaving b undefined rather than silently accepted.
let err = table.set("b", "a").unwrap_err();
assert!(matches!(err, ParamError::Cycle(_)));
assert!(!table.contains("b"));
// a's formula (still "b") is untouched -- it stays unresolved, not
// corrupted by the rejected attempt.
assert_eq!(table.resolve("a"), Err(ParamError::UndefinedReference("b".to_string())));
}
#[test]
fn set_allows_redefining_an_existing_parameter_to_break_a_would_be_cycle() {
let mut table = ParameterTable::new();
table.set("a", "1").unwrap();
table.set("b", "a + 1").unwrap();
// Redefining a in terms of b must be rejected (a <- b <- a)...
assert!(table.set("a", "b + 1").is_err());
// ...but redefining a to something unrelated is always fine.
table.set("a", "5").unwrap();
assert_eq!(table.resolve("a"), Ok(5.0));
assert_eq!(table.resolve("b"), Ok(6.0));
}
#[test]
fn resolve_all_isolates_a_failure_to_the_affected_parameters_only() {
let mut table = ParameterTable::new();
table.set("good", "10").unwrap();
table.set("bad", "missing + 1").unwrap();
table.set("depends_on_bad", "bad * 2").unwrap();
let results = table.resolve_all();
assert_eq!(results.get("good"), Some(&Ok(10.0)));
assert_eq!(results.get("bad"), Some(&Err(ParamError::UndefinedReference("missing".to_string()))));
assert_eq!(results.get("depends_on_bad"), Some(&Err(ParamError::UndefinedReference("missing".to_string()))));
}
#[test]
fn remove_drops_a_parameter_and_leaves_dependents_undefined_not_panicking() {
let mut table = ParameterTable::new();
table.set("a", "1").unwrap();
table.set("b", "a + 1").unwrap();
assert!(table.remove("a"));
assert!(!table.remove("a"), "removing an already-removed name reports false");
assert_eq!(table.resolve("b"), Err(ParamError::UndefinedReference("a".to_string())));
}
#[test]
fn set_upserts_an_existing_parameters_formula() {
let mut table = ParameterTable::new();
table.set("a", "1").unwrap();
table.set("a", "2").unwrap();
assert_eq!(table.len(), 1, "redefining must not create a second entry");
assert_eq!(table.resolve("a"), Ok(2.0));
}
#[test]
fn table_round_trips_through_bincode() {
let mut table = ParameterTable::new();
table.set("hole_dia", "5").unwrap();
table.set("hole_spacing", "2 * hole_dia + 1.5").unwrap();
let bytes = bincode::serialize(&table).expect("serialize");
let restored: ParameterTable = bincode::deserialize(&bytes).expect("deserialize");
assert_eq!(restored.len(), 2);
assert_eq!(restored.resolve("hole_spacing"), Ok(11.5));
assert_eq!(restored.get("hole_dia").unwrap().source, "5");
}
#[test]
fn iter_preserves_insertion_order_across_redefinitions() {
let mut table = ParameterTable::new();
table.set("z", "1").unwrap();
table.set("a", "2").unwrap();
table.set("z", "3").unwrap(); // redefine, should not move to the end
let names: Vec<&str> = table.iter().map(|p| p.name.as_str()).collect();
assert_eq!(names, vec!["z", "a"]);
}
}
+201
View File
@@ -0,0 +1,201 @@
//! Save/load integration for [`super::named_parameters::ParameterTable`] —
//! design doc §4 stage 2 (the XRecord persistence spike) plus enough of
//! stage 2's "wire it into open/save" to actually be useful, following
//! [`super::sketch_persist`]'s exact precedent (itself the sketch-constraint
//! project's stage 4/§5.2): the "lazy" model, where the table lives only in
//! `Scene::named_parameters` in-memory during editing and is materialized
//! into `document.objects` as one XRecord right before a save, then read
//! back right after a load.
//!
//! The one structural difference from `sketch_persist`: a
//! [`super::named_parameters::ParameterTable`] is document-wide, not
//! per-[`super::sketch_constraints::SketchScope`], so there is exactly one
//! XRecord for the whole document rather than one per scope owner.
//!
//! **Owner handle, and a real round-trip gap found while building this**:
//! the obvious choice was `document.header.named_objects_dict_handle` (the
//! document's own root Named Objects Dictionary) — `extension_dictionary_
//! handle` in the vendored `cadcodec` source explicitly resolves an
//! extension dictionary for a `Dictionary`-type owner (`ObjectType::
//! Dictionary(value) => value.xdictionary_handle`), and that crate's own
//! tests (e.g. `tests/issue51.rs`) use `named_objects_dict_handle` as an
//! XRecord owner directly. In practice this round-tripped through DWG but
//! **silently lost the XRecord through DXF** (caught by this module's own
//! save/load tests, not assumed): `CadDocument::ensure_xrecord` only writes
//! the owner's *own* `xdictionary_handle` field when `get_entity_mut(owner)`
//! succeeds (a real "entity"); a `Dictionary` owner like the root NOD isn't
//! one, so `ensure_xrecord` falls back to a runtime-only side map
//! (`xdic_by_handle`) instead. The DWG writer consults that side map
//! (`io/dwg/dwg_document_builder.rs` et al.); the DXF writer never does (no
//! `xdic_by_handle` reference anywhere under `io/dxf/writer/`) — so a
//! Dictionary-owned extension dictionary is invisible to a DXF save. This
//! is a gap in the vendored crate for this specific owner-type combination,
//! not something introduced here; per this project's own stated preference
//! to avoid needing changes to `acadrust`/`cadkernel`, the fix is to pick a
//! different, already-proven owner rather than patch it.
//!
//! **Owner used instead**: `document.header.model_space_block_handle` — a
//! real `BlockRecord` entity, the exact anchor `sketch_persist`'s own
//! `SketchScope::ModelSpace` case already proved round-trips cleanly
//! through both DWG and DXF. Model space is not a semantically perfect fit
//! for "the whole document" (a parameter table has nothing to do with model
//! space specifically), but it is guaranteed to exist in every
//! `CadDocument` and is a real entity, which is the property that actually
//! matters here.
use super::named_parameters::ParameterTable;
use super::Scene;
const XRECORD_KEY: &str = "OCS_NAMED_PARAMETERS";
/// Prefixed onto the serialized blob so a future schema change can be
/// detected and gracefully skipped rather than silently misreading bytes —
/// same call-out as `sketch_persist::FORMAT_VERSION`, same caveat that the
/// migration path itself is still undesigned.
const FORMAT_VERSION: u8 = 1;
fn encode(table: &ParameterTable) -> Option<Vec<u8>> {
let mut bytes = vec![FORMAT_VERSION];
bytes.extend(bincode::serialize(table).ok()?);
Some(bytes)
}
fn decode(bytes: &[u8]) -> Option<ParameterTable> {
let (&version, body) = bytes.split_first()?;
if version != FORMAT_VERSION {
return None;
}
bincode::deserialize(body).ok()
}
impl Scene {
/// Writes `named_parameters` into the document's `OCS_NAMED_PARAMETERS`
/// XRecord, ready for whatever save call happens next to serialize
/// `self.document` as-is. Called from the same save entry points as
/// `materialize_sketch_constraints_for_save`
/// (`OpenCADStudio::prepare_native_save`, the wasm save path).
///
/// An empty table is skipped rather than writing an empty XRecord — an
/// unconstrained-by-parameters drawing (the common case, at least until
/// this feature is used) should not gain persisted-but-empty
/// bookkeeping on every save.
pub(crate) fn materialize_named_parameters_for_save(&mut self) {
if self.named_parameters.is_empty() {
return;
}
let Some(bytes) = encode(&self.named_parameters) else { return };
let owner = self.document.header.model_space_block_handle;
if owner.is_null() {
return;
}
self.document.ensure_xrecord(owner, XRECORD_KEY);
if let Some(record) = self.document.xrecord_mut(owner, XRECORD_KEY) {
// Overwrite, not append: a resave must replace the prior blob,
// not accumulate one more Chunk entry every time.
record.entries.clear();
record
.entries
.push(acadrust::objects::XRecordEntry::new(310, acadrust::objects::XRecordValue::Chunk(bytes)));
}
}
/// Populates `named_parameters` from the just-loaded `self.document`'s
/// `OCS_NAMED_PARAMETERS` XRecord, if any — called right after a
/// document open installs its `CadDocument` into this `Scene`, the same
/// call sites `load_sketch_constraints_from_document` already has
/// (`OpenCADStudio::on_file_opened`, the automation `"open"`/`"new"`
/// ops). Replaces whatever was already in `named_parameters` (a fresh
/// table for a real open; `"new"`'s blank document has nothing to find
/// anyway).
pub(crate) fn load_named_parameters_from_document(&mut self) {
self.named_parameters = ParameterTable::new();
let owner = self.document.header.model_space_block_handle;
if owner.is_null() {
return;
}
let Some(record) = self.document.xrecord(owner, XRECORD_KEY) else { return };
let Some(bytes) = record.entries.iter().find_map(|entry| match &entry.value {
acadrust::objects::XRecordValue::Chunk(bytes) => Some(bytes.clone()),
_ => None,
}) else {
return;
};
if let Some(table) = decode(&bytes) {
self.named_parameters = table;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_parameter_table_survives_materialize_and_reload() {
let mut scene = Scene::new();
scene.named_parameters_mut().set("hole_dia", "5").unwrap();
scene.named_parameters_mut().set("hole_spacing", "2 * hole_dia + 1.5").unwrap();
scene.materialize_named_parameters_for_save();
// Simulate the load-time side of a save/reload round trip: drop the
// in-memory table (a fresh open starts empty) and repopulate purely
// from what materialize just wrote into `document.objects`.
scene.named_parameters = ParameterTable::new();
scene.load_named_parameters_from_document();
assert_eq!(scene.named_parameters().len(), 2);
assert_eq!(scene.named_parameters().resolve("hole_spacing"), Ok(11.5));
assert_eq!(scene.named_parameters().get("hole_dia").unwrap().source, "5");
}
/// The above test only proves `materialize`/`load` agree with each
/// other on the same in-memory `CadDocument` — this drives a real
/// `save_to_bytes`/`load_bytes` round trip (the same primitives
/// `OpenCADStudio::on_file_opened`'s native save/open path uses),
/// through both supported formats, mirroring
/// `sketch_persist`'s own real-bytes tests.
fn full_bytes_roundtrip(ext: &str) -> ParameterTable {
let mut scene = Scene::new();
scene.named_parameters_mut().set("plate_len", "42.5").unwrap();
scene.named_parameters_mut().set("plate_width", "plate_len / 2").unwrap();
scene.materialize_named_parameters_for_save();
let bytes = crate::io::save_to_bytes(&scene.document, ext, scene.document.version)
.unwrap_or_else(|e| panic!("save to {ext} bytes: {e}"));
let mut reloaded_scene = Scene::new();
reloaded_scene.document = crate::io::load_bytes(&format!("named_params_roundtrip.{ext}"), bytes)
.unwrap_or_else(|e| panic!("reload {ext} bytes: {e}"));
reloaded_scene.load_named_parameters_from_document();
reloaded_scene.named_parameters
}
#[test]
fn a_parameter_table_survives_a_real_dxf_save_and_load_round_trip() {
let restored = full_bytes_roundtrip("dxf");
assert_eq!(restored.len(), 2);
assert_eq!(restored.resolve("plate_width"), Ok(21.25));
}
#[test]
fn a_parameter_table_survives_a_real_dwg_save_and_load_round_trip() {
let restored = full_bytes_roundtrip("dwg");
assert_eq!(restored.len(), 2);
assert_eq!(restored.resolve("plate_width"), Ok(21.25));
}
#[test]
fn an_empty_table_is_not_materialized() {
let mut scene = Scene::new();
scene.materialize_named_parameters_for_save();
let owner = scene.document.header.model_space_block_handle;
assert!(scene.document.xrecord(owner, XRECORD_KEY).is_none(), "an empty table should not create an XRecord");
}
#[test]
fn decode_rejects_a_mismatched_format_version() {
let mut bytes = vec![FORMAT_VERSION.wrapping_add(1)];
bytes.extend(bincode::serialize(&ParameterTable::new()).unwrap());
assert!(decode(&bytes).is_none(), "a future/unknown format version must be rejected, not misread");
}
}
+27 -15
View File
@@ -11,6 +11,7 @@
//! entities and is meant to be re-solved every time referenced geometry
//! changes, not applied once.
use super::named_parameters::DrivingValue;
use acadrust::types::{Handle, Vector3};
use serde::{Deserialize, Serialize};
@@ -90,11 +91,14 @@ pub struct SketchConstraint {
pub id: ConstraintId,
pub kind: ConstraintKind,
pub refs: Vec<SketchRef>,
/// The typed target for a dimensional constraint (a `Distance`'s
/// length, an `Angle`'s degrees, a `Radius`'s radius). `None` for every
/// purely-geometric kind (Coincident, Horizontal, Vertical, Parallel,
/// Perpendicular, Equal, Tangent).
pub driving_param: Option<f64>,
/// The target for a dimensional constraint (a `Distance`'s length, an
/// `Angle`'s degrees, a `Radius`'s radius) — a literal number or a
/// named-parameter reference (`docs/named_parameters_design.md`,
/// resolved through `Scene::named_parameters` at solve time by
/// `sketch_solve::build_constraint`). `None` for every purely-geometric
/// kind (Coincident, Horizontal, Vertical, Parallel, Perpendicular,
/// Equal, Tangent).
pub driving_param: Option<DrivingValue>,
/// Lets a user suppress a constraint without losing it — a re-solve
/// skips a disabled constraint entirely.
pub enabled: bool,
@@ -160,7 +164,7 @@ impl SketchConstraintSet {
}
/// Appends a constraint, assigning it a fresh id unique within this set.
pub fn add(&mut self, kind: ConstraintKind, refs: Vec<SketchRef>, driving_param: Option<f64>) -> ConstraintId {
pub fn add(&mut self, kind: ConstraintKind, refs: Vec<SketchRef>, driving_param: Option<DrivingValue>) -> ConstraintId {
let id = self.next_id;
self.next_id += 1;
self.constraints.push(SketchConstraint { id, kind, refs, driving_param, enabled: true });
@@ -348,9 +352,17 @@ impl ConstraintKind {
/// The full glyph text for one constraint: its symbol, plus the driving
/// value for a dimensional kind (Distance/Angle/Radius).
pub(crate) fn glyph_label(constraint: &SketchConstraint) -> String {
match (constraint.kind, constraint.driving_param) {
(ConstraintKind::Angle, Some(value)) => format!("{} {value:.1}°", constraint.kind.glyph_symbol()),
(_, Some(value)) => format!("{} {value:.2}", constraint.kind.glyph_symbol()),
match (constraint.kind, &constraint.driving_param) {
(ConstraintKind::Angle, Some(DrivingValue::Literal(value))) => format!("{} {value:.1}°", constraint.kind.glyph_symbol()),
(_, Some(DrivingValue::Literal(value))) => format!("{} {value:.2}", constraint.kind.glyph_symbol()),
// A named reference has no single resolved number to show without
// threading `ParameterTable` into every glyph-render call site
// (`src/ui/overlay.rs`) — showing the name itself is enough for now;
// stage 4's parameters panel is the natural place to reconsider this
// once a named `driving_param` can actually be authored through the
// UI (nothing can yet — this arm exists so the match is exhaustive
// and correct ahead of that UI, not because it's reachable today).
(_, Some(DrivingValue::Named(name))) => format!("{} {name}", constraint.kind.glyph_symbol()),
(_, None) => constraint.kind.glyph_symbol().to_string(),
}
}
@@ -415,7 +427,7 @@ impl super::Scene {
if handle_map.is_empty() {
return;
}
let mut to_add: Vec<(usize, ConstraintKind, Vec<SketchRef>, Option<f64>)> = Vec::new();
let mut to_add: Vec<(usize, ConstraintKind, Vec<SketchRef>, Option<DrivingValue>)> = Vec::new();
for (scope_index, set) in self.sketch_constraints.iter().enumerate() {
for c in &set.constraints {
if !c.enabled || !c.refs.iter().all(|r| handle_map.contains_key(&r.entity)) {
@@ -423,7 +435,7 @@ impl super::Scene {
}
let new_refs: Vec<SketchRef> =
c.refs.iter().map(|r| SketchRef { entity: handle_map[&r.entity], marker: r.marker }).collect();
to_add.push((scope_index, c.kind, new_refs, c.driving_param));
to_add.push((scope_index, c.kind, new_refs, c.driving_param.clone()));
}
}
if to_add.is_empty() {
@@ -453,10 +465,10 @@ mod tests {
fn add_assigns_increasing_ids_and_get_finds_them() {
let mut set = SketchConstraintSet::new(SketchScope::ModelSpace);
let a = set.add(ConstraintKind::Horizontal, vec![SketchRef::whole(h(1))], None);
let b = set.add(ConstraintKind::Distance, vec![SketchRef::whole(h(1))], Some(25.0));
let b = set.add(ConstraintKind::Distance, vec![SketchRef::whole(h(1))], Some(DrivingValue::Literal(25.0)));
assert_ne!(a, b);
assert_eq!(set.get(a).unwrap().kind, ConstraintKind::Horizontal);
assert_eq!(set.get(b).unwrap().driving_param, Some(25.0));
assert_eq!(set.get(b).unwrap().driving_param, Some(DrivingValue::Literal(25.0)));
}
#[test]
@@ -521,14 +533,14 @@ mod tests {
fn sketch_constraint_set_round_trips_through_bincode() {
let mut set = SketchConstraintSet::new(SketchScope::Block(h(5)));
set.add(ConstraintKind::Coincident, vec![SketchRef::point(h(1), 0), SketchRef::point(h(2), 1)], None);
set.add(ConstraintKind::Distance, vec![SketchRef::whole(h(3))], Some(12.5));
set.add(ConstraintKind::Distance, vec![SketchRef::whole(h(3))], Some(DrivingValue::Literal(12.5)));
let bytes = bincode::serialize(&set).expect("serialize");
let restored: SketchConstraintSet = bincode::deserialize(&bytes).expect("deserialize");
assert_eq!(restored.scope, set.scope);
assert_eq!(restored.constraints.len(), set.constraints.len());
assert_eq!(restored.constraints[1].driving_param, Some(12.5));
assert_eq!(restored.constraints[1].driving_param, Some(DrivingValue::Literal(12.5)));
assert_eq!(restored.constraints[0].refs, set.constraints[0].refs);
}
}
+16 -4
View File
@@ -48,7 +48,15 @@ fn remove_xrecord(document: &mut CadDocument, owner: Handle, key: &str) {
/// or panicking — design doc §5.2's explicit call-out, and §7's flagged
/// "no migration path designed yet": bumping this is the signal a real
/// migration is needed, not a substitute for one.
const FORMAT_VERSION: u8 = 1;
///
/// Bumped 1 -> 2 by `named_parameters_design.md` stage 3:
/// `SketchConstraint::driving_param` changed from a bare `Option<f64>` to
/// `Option<named_parameters::DrivingValue>` (an enum, with its own
/// discriminant), so a version-1 blob's bytes no longer line up with the
/// current struct shape — exactly the "detected and gracefully skipped"
/// case this version byte exists for, not something to migrate (no
/// migration path exists yet, per the call-out above).
const FORMAT_VERSION: u8 = 2;
fn encode(set: &SketchConstraintSet) -> Option<Vec<u8>> {
let mut bytes = vec![FORMAT_VERSION];
@@ -192,7 +200,11 @@ mod tests {
vec![SketchRef::point(a, 1), SketchRef::point(b, 0)],
None,
);
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(ConstraintKind::Distance, vec![SketchRef::whole(b)], Some(12.5));
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
ConstraintKind::Distance,
vec![SketchRef::whole(b)],
Some(crate::scene::named_parameters::DrivingValue::Literal(12.5)),
);
scene.materialize_sketch_constraints_for_save();
let bytes = crate::io::save_to_bytes(&scene.document, ext, scene.document.version)
@@ -213,7 +225,7 @@ mod tests {
assert_eq!(restored.constraints.len(), 2);
assert_eq!(restored.constraints[0].kind, ConstraintKind::Coincident);
assert_eq!(restored.constraints[1].kind, ConstraintKind::Distance);
assert_eq!(restored.constraints[1].driving_param, Some(12.5));
assert_eq!(restored.constraints[1].driving_param, Some(crate::scene::named_parameters::DrivingValue::Literal(12.5)));
}
#[test]
@@ -222,7 +234,7 @@ mod tests {
assert_eq!(restored.constraints.len(), 2);
assert_eq!(restored.constraints[0].kind, ConstraintKind::Coincident);
assert_eq!(restored.constraints[1].kind, ConstraintKind::Distance);
assert_eq!(restored.constraints[1].driving_param, Some(12.5));
assert_eq!(restored.constraints[1].driving_param, Some(crate::scene::named_parameters::DrivingValue::Literal(12.5)));
}
#[test]
+21 -10
View File
@@ -38,6 +38,7 @@ use ocs_gcs::geo::{Circle as GCircle, Line as GLine, Point as GPoint};
use ocs_gcs::solvers::dogleg::solve_dl;
use ocs_gcs::system::System;
use super::named_parameters::ParameterTable;
use super::sketch_constraints::{ConstraintId, ConstraintKind, SketchConstraint, SketchConstraintSet, SketchRef};
use super::{ChangeKind, Scene};
@@ -113,14 +114,21 @@ fn resolve_ref(
/// One [`SketchConstraint`]'s `ocs_gcs` construction, or `None` if it can't
/// be built (an unsupported/not-yet-mapped kind, a dangling ref, a ref
/// shape the kind doesn't expect — e.g. `Parallel` needs two whole-line
/// refs). A constraint that can't be built is simply skipped for this
/// solve, not an error: geometry it would have constrained is left alone,
/// matching how a dangling associative-dimension reference degrades today
/// rather than aborting the whole recompute.
/// refs — or, for a dimensional kind, a `driving_param` that fails to
/// resolve: an undefined named-parameter reference or a division by zero in
/// its formula, design doc `named_parameters_design.md` stage 3). A
/// constraint that can't be built is simply skipped for this solve, not an
/// error: geometry it would have constrained is left alone, matching how a
/// dangling associative-dimension reference degrades today rather than
/// aborting the whole recompute. (A named-parameter *cycle* should be
/// unreachable here — `ParameterTable::set` already refuses to create one —
/// so it gets the same treatment as any other resolve failure rather than a
/// special case.)
fn build_constraint(
document: &acadrust::CadDocument,
sys: &mut System,
cache: &mut HashMap<Handle, EntityGeom>,
params: &ParameterTable,
c: &SketchConstraint,
) -> Option<Rc<dyn Constraint>> {
if !c.enabled {
@@ -181,18 +189,18 @@ fn build_constraint(
ConstraintKind::Distance => {
let [a, b] = c.refs.as_slice() else { return None };
let (pa, pb) = (point_ref(sys, cache, *a)?, point_ref(sys, cache, *b)?);
let target = sys.add_param(c.driving_param?, true);
let target = sys.add_param(c.driving_param.as_ref()?.resolve(params).ok()?, true);
Some(Rc::new(P2PDistance::new(pa, pb, target)))
}
ConstraintKind::Angle => {
let [a, b] = c.refs.as_slice() else { return None };
let (fixed, moving) = (whole_line(sys, cache, *a)?, whole_line(sys, cache, *b)?);
let angle = sys.add_param(c.driving_param?.to_radians(), true);
let angle = sys.add_param(c.driving_param.as_ref()?.resolve(params).ok()?.to_radians(), true);
Some(Rc::new(L2LAngle::new(fixed, moving, angle)))
}
ConstraintKind::Radius => {
let circle = whole_circle(sys, cache, *c.refs.first()?)?;
let target = sys.add_param(c.driving_param?, true);
let target = sys.add_param(c.driving_param.as_ref()?.resolve(params).ok()?, true);
Some(Rc::new(Equal::new(circle.rad, target, 1.0)))
}
ConstraintKind::Tangent => {
@@ -252,6 +260,7 @@ const MOVE_EPS: f64 = 1e-9;
/// on a successful solve.
fn solve_scope(
document: &acadrust::CadDocument,
params: &ParameterTable,
set: &SketchConstraintSet,
) -> Option<(Vec<(Handle, EntityType)>, usize, Vec<(ConstraintId, ocs_gcs::diagnosis::RedundancyKind)>)> {
let mut sys = System::new();
@@ -266,7 +275,7 @@ fn solve_scope(
let mut owner: Vec<(Rc<dyn Constraint>, ConstraintId)> = Vec::new();
for c in &set.constraints {
let Some(constraint) = build_constraint(document, &mut sys, &mut cache, c) else { continue };
let Some(constraint) = build_constraint(document, &mut sys, &mut cache, params, c) else { continue };
owner.push((constraint.clone(), c.id));
sys.add_constraint(constraint);
// Coincident needs both X and Y equal; `build_constraint` returns
@@ -412,7 +421,9 @@ impl Scene {
if !touched {
continue;
}
let Some((solved, dof, conflicts)) = solve_scope(&self.document, &self.sketch_constraints[i]) else { continue };
let Some((solved, dof, conflicts)) = solve_scope(&self.document, &self.named_parameters, &self.sketch_constraints[i]) else {
continue;
};
self.sketch_constraints[i].dof = Some(dof);
self.sketch_constraints[i].conflicts = conflicts;
for (handle, new_entity) in solved {
@@ -470,7 +481,7 @@ impl Scene {
if !is_touched {
continue;
}
let Some((solved, _dof, _conflicts)) = solve_scope(&self.document, set) else { continue };
let Some((solved, _dof, _conflicts)) = solve_scope(&self.document, &self.named_parameters, set) else { continue };
result.extend(solved);
}
result
+1
View File
@@ -18,3 +18,4 @@ pub mod options;
pub mod attribute_editor;
pub mod alias_editor;
pub mod find_replace;
pub mod named_parameters;
+258
View File
@@ -0,0 +1,258 @@
//! Named-parameter table editor — an in-canvas modal (Plan B), mirroring
//! `alias_editor.rs`'s buffered-rows pattern exactly. Opened by PARAMETERS
//! (`docs/named_parameters_design.md` stage 4). Rows are `(name, formula)`,
//! edited in a working buffer (`OpenCADStudio::named_parameter_editor_rows`)
//! and committed to `Scene::named_parameters` only on Apply — matching the
//! alias editor's "closing discards unapplied edits" convention, which
//! suits a formula table even better: `ParameterTable::set` validates (and
//! can reject) a formula immediately, so typing a not-yet-finished name or
//! formula must not touch the live table on every keystroke.
//!
//! Unlike the alias editor, each row also shows a live-computed resolved
//! value or error (`preview`, below) — built fresh from the whole buffer on
//! every render, not cached: the same "cheap enough to recompute from
//! scratch" philosophy `ParameterTable`/`sketch_solve.rs` already use
//! throughout this project, and it means the preview reflects circular/
//! undefined-reference problems across rows immediately, before Apply.
use crate::app::Message;
use crate::scene::named_parameters::ParameterTable;
use crate::t;
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
use iced::{Background, Element, Length, Theme};
/// Which column of a parameter row a text edit targets.
#[derive(Clone, Copy, Debug)]
pub enum ParamField {
Name,
Formula,
}
/// One working-buffer row. Free text until Apply — `ParameterTable::set`
/// does the real validation then, not on every keystroke.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ParamEditorRow {
pub name: String,
pub formula: String,
}
/// Right-hand lane reserved for the scrollbar so it never overlaps the ✕
/// column — same convention and value as `alias_editor::GUTTER`.
const GUTTER: f32 = 16.0;
fn muted_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style { color: Some(theme.palette().background.base.text.scale_alpha(0.68)) }
}
fn danger_style(theme: &Theme) -> iced::widget::text::Style {
iced::widget::text::Style { color: Some(theme.palette().danger.base.color) }
}
/// Every buffer index sharing a (trimmed) name with at least one other
/// non-blank row — `ParameterTable::set` upserts by name, so two rows typed
/// with the same name would otherwise silently collapse into "whichever one
/// happens to `set` last wins," discarding the other's formula with no
/// indication anything was lost. Both `preview` and the real Apply
/// (`OpenCADStudio::apply_named_parameter_editor_rows`) check this first and
/// refuse every row in a name collision rather than guessing which one the
/// user meant.
pub(crate) fn duplicate_name_rows(rows: &[ParamEditorRow]) -> std::collections::HashSet<usize> {
let mut by_name: std::collections::HashMap<&str, Vec<usize>> = std::collections::HashMap::new();
for (idx, row) in rows.iter().enumerate() {
let name = row.name.trim();
if !name.is_empty() {
by_name.entry(name).or_default().push(idx);
}
}
by_name.into_values().filter(|idxs| idxs.len() > 1).flatten().collect()
}
/// Builds a scratch table from every row's *current* text and resolves each
/// row's own value against it, so the preview reflects cross-row references
/// (including a row referencing one defined further down — order in the
/// buffer doesn't matter, same as it doesn't for the real table). Returns
/// one `Result` per row, in row order:
/// - `Err` from a blank name is suppressed (`Ok` with no text shown instead,
/// handled by the caller) — a fresh row from "+ Add parameter" before the
/// user has typed a name yet isn't an error to flag.
/// - A name shared with another row is its own error (`duplicate_name_rows`)
/// before anything is even attempted.
/// - `Err` from `ParameterTable::set` itself (bad name, parse error, a
/// formula that would close a cycle right now) is that row's own problem.
/// - `Err(UndefinedReference)`/other resolve failure means the row's own
/// formula is fine but something *it* depends on isn't.
fn preview(rows: &[ParamEditorRow]) -> Vec<Option<Result<f64, String>>> {
let duplicates = duplicate_name_rows(rows);
let mut table = ParameterTable::new();
let mut set_errors: Vec<Option<String>> = vec![None; rows.len()];
for (idx, row) in rows.iter().enumerate() {
let name = row.name.trim();
if name.is_empty() || duplicates.contains(&idx) {
continue;
}
if let Err(e) = table.set(name, row.formula.trim()) {
set_errors[idx] = Some(e.to_string());
}
}
rows.iter()
.enumerate()
.map(|(idx, row)| {
let name = row.name.trim();
if name.is_empty() {
return None;
}
if duplicates.contains(&idx) {
return Some(Err(format!("duplicate name '{name}'")));
}
if let Some(err) = &set_errors[idx] {
return Some(Err(err.clone()));
}
Some(table.resolve(name).map_err(|e| e.to_string()))
})
.collect()
}
/// Build the named-parameter editor content. `rows` is the live working
/// buffer.
pub fn view_window(rows: &[ParamEditorRow], sizing: crate::ui::modal::ModalSizing) -> Element<'_, Message> {
let title = text(t!("Named Parameters")).size(15);
let hint = text(t!(
"Type a name and a formula (e.g. hole_dia = 12, hole_spacing = 2 * hole_dia + 1.5). Apply to save and re-solve; closing discards unapplied edits."
))
.size(11)
.style(muted_style);
let gutter = iced::Padding { top: 0.0, right: GUTTER, bottom: 0.0, left: 0.0 };
const NAME_WIDTH: f32 = 120.0;
const VALUE_WIDTH: f32 = 140.0;
let head = container(
row![
container(text(t!("Name")).size(11).style(muted_style)).width(Length::Fixed(NAME_WIDTH)),
container(text(t!("Formula")).size(11).style(muted_style)).width(sizing.width),
container(text(t!("Value")).size(11).style(muted_style)).width(Length::Fixed(VALUE_WIDTH)),
Space::new().width(Length::Fixed(30.0)),
]
.spacing(8),
)
.padding(gutter);
let results = preview(rows);
let mut list = column![].spacing(3);
for (idx, row) in rows.iter().enumerate() {
let name_box = text_input(t!("name").as_ref(), &row.name)
.on_input(move |v| Message::NamedParametersInput { idx, field: ParamField::Name, value: v })
.size(13)
.padding([3, 6])
.width(Length::Fixed(NAME_WIDTH));
let formula_box = text_input(t!("formula").as_ref(), &row.formula)
.on_input(move |v| Message::NamedParametersInput { idx, field: ParamField::Formula, value: v })
.size(13)
.padding([3, 6])
.width(sizing.width);
let value_cell: Element<'_, Message> = match results.get(idx).cloned().flatten() {
Some(Ok(value)) => container(text(format!("= {value:.4}")).size(12).style(muted_style)).into(),
Some(Err(msg)) => container(text(msg).size(11).style(danger_style)).into(),
None => Space::new().into(),
};
let del = button(crate::ui::icons::themed_danger_text(crate::ui::icons::CLOSE, 12.0))
.on_press(Message::NamedParametersRemove(idx))
.padding([2, 6])
.style(button::danger);
list = list.push(
row![name_box, formula_box, container(value_cell).width(Length::Fixed(VALUE_WIDTH)), del]
.spacing(8)
.align_y(iced::Center),
);
}
let add = button(text(t!("+ Add parameter")).size(12))
.on_press(Message::NamedParametersAdd)
.padding([4, 10])
.style(button::secondary);
let apply = button(text(t!("Apply")).size(12))
.on_press(Message::NamedParametersApply)
.padding([4, 16])
.style(button::primary);
container(
column![
title,
hint,
Space::new().height(6),
head,
scrollable(container(list).padding(gutter)).height(sizing.height),
Space::new().height(6),
row![add, Space::new().width(sizing.width), apply].align_y(iced::Center),
]
.spacing(6)
.width(sizing.width)
.height(sizing.height),
)
.padding(12)
.width(sizing.width)
.height(sizing.height)
.style(|theme: &Theme| container::Style { background: Some(Background::Color(theme.palette().background.base.color)), ..Default::default() })
.into()
}
#[cfg(test)]
mod tests {
use super::*;
fn row(name: &str, formula: &str) -> ParamEditorRow {
ParamEditorRow { name: name.to_string(), formula: formula.to_string() }
}
#[test]
fn preview_resolves_a_literal_and_a_cross_row_reference() {
let rows = vec![row("hole_dia", "5"), row("hole_spacing", "2 * hole_dia + 1.5")];
let results = preview(&rows);
assert_eq!(results[0], Some(Ok(5.0)));
assert_eq!(results[1], Some(Ok(11.5)));
}
#[test]
fn preview_resolves_a_forward_reference_regardless_of_row_order() {
// `plate_width` (row 0) references `plate_len` (row 1) -- defined
// *after* it in the buffer. Order in the editor must not matter, same
// as it doesn't for the real `ParameterTable`.
let rows = vec![row("plate_width", "plate_len / 2"), row("plate_len", "20")];
let results = preview(&rows);
assert_eq!(results[0], Some(Ok(10.0)));
assert_eq!(results[1], Some(Ok(20.0)));
}
#[test]
fn preview_reports_a_cycle_on_the_row_that_closes_it() {
let rows = vec![row("a", "b"), row("b", "a")];
let results = preview(&rows);
// Row 0 (a = b) is accepted first (b doesn't exist yet, no cycle);
// row 1 (b = a) is the one that would close the loop and is rejected.
assert!(matches!(results[0], Some(Ok(_))) || matches!(&results[0], Some(Err(e)) if e.contains("not defined")));
assert!(matches!(&results[1], Some(Err(e)) if e.to_lowercase().contains("circular")));
}
#[test]
fn preview_leaves_a_blank_row_without_a_name_unflagged() {
let rows = vec![row("", "")];
assert_eq!(preview(&rows), vec![None]);
}
#[test]
fn preview_reports_a_malformed_formula_on_its_own_row() {
let rows = vec![row("bad", "1 +")];
let results = preview(&rows);
assert!(matches!(&results[0], Some(Err(e)) if e.contains("formula error")));
}
#[test]
fn preview_flags_every_row_sharing_a_duplicate_name_instead_of_picking_one() {
let rows = vec![row("x", "1"), row("x", "2"), row("y", "3")];
let results = preview(&rows);
assert!(matches!(&results[0], Some(Err(e)) if e.contains("duplicate")));
assert!(matches!(&results[1], Some(Err(e)) if e.contains("duplicate")));
assert_eq!(results[2], Some(Ok(3.0)), "an unrelated row's name must not be affected");
}
}
+19
View File
@@ -41,6 +41,7 @@ pub fn view_window<'a>(
crosshair_color: Option<[u8; 3]>,
crosshair_color_input: &'a str,
lineweight_display_scale: i32,
ui_scale: i32,
ribbon_label_font_size: i32,
ribbon_group_title_font_size: i32,
model_space: &'a crate::app::config::ModelSpaceThemeConfig,
@@ -349,6 +350,24 @@ pub fn view_window<'a>(
});
let mut display = column![
text(crate::t!("UI Scale")).size(15),
Space::new().height(10),
row![
text(crate::t!("Overall size")).size(12).width(150),
slider(50..=200, ui_scale.clamp(50, 200), Message::UiScaleChanged)
.step(5)
.width(Fill),
text(format!("{}%", ui_scale.clamp(50, 200))).size(11).width(44),
]
.spacing(12)
.align_y(iced::Center),
Space::new().height(8),
text(crate::t!(
"Scales the whole interface at once — every panel, dialog, the ribbon, and their text — rather than one widget's font size. Takes effect immediately."
))
.size(11)
.width(sizing.width),
Space::new().height(24),
text(crate::tr!("options", "theme-section")).size(15),
Space::new().height(10),
row![
+61 -3
View File
@@ -5,6 +5,7 @@
// constrained neighbors, produces the expected change list, and an
// unrelated edit doesn't touch anything (§4.3's "what does NOT trigger").
use OpenCADStudio::scene::named_parameters::DrivingValue;
use OpenCADStudio::scene::sketch_constraints::{ConstraintKind, SketchRef, SketchScope};
use OpenCADStudio::scene::{ChangeKind, Scene};
use acadrust::entities::EntityType;
@@ -96,7 +97,7 @@ fn distance_constraint_holds_the_target_length_after_an_unrelated_endpoint_edit(
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
ConstraintKind::Distance,
vec![SketchRef::point(line, 0), SketchRef::point(line, 1)],
Some(20.0),
Some(DrivingValue::Literal(20.0)),
);
set_line_end(&mut scene, line, Vector3::new(5.0, 5.0, 0.0)); // arbitrary edit, not length=20
@@ -107,6 +108,63 @@ fn distance_constraint_holds_the_target_length_after_an_unrelated_endpoint_edit(
assert!((len - 20.0).abs() < 1e-6, "length should have been solved to the driving value, got {len}");
}
/// `named_parameters_design.md` stage 3: a `Distance` constraint's driving
/// value can be a named-parameter reference instead of a literal, resolved
/// through `Scene::named_parameters` at solve time.
#[test]
fn distance_constraint_resolves_a_named_parameter_reference() {
let mut scene = Scene::new();
let line = add_line(&mut scene, 0.0, 0.0, 10.0, 0.0);
scene.named_parameters_mut().set("target_len", "20").unwrap();
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
ConstraintKind::Distance,
vec![SketchRef::point(line, 0), SketchRef::point(line, 1)],
Some(DrivingValue::Named("target_len".to_string())),
);
set_line_end(&mut scene, line, Vector3::new(5.0, 5.0, 0.0)); // arbitrary edit, not length=20
scene.bump_entities(&[(line, ChangeKind::Modified)]);
let (start, end) = line_endpoints(&scene, line);
let len = ((end.x - start.x).powi(2) + (end.y - start.y).powi(2)).sqrt();
assert!((len - 20.0).abs() < 1e-6, "length should have resolved the named parameter, got {len}");
// Editing the parameter itself and touching the scope again should
// ripple through, same as editing a literal driving value would.
scene.named_parameters_mut().set("target_len", "8").unwrap();
scene.bump_entities(&[(line, ChangeKind::Modified)]);
let (start, end) = line_endpoints(&scene, line);
let len = ((end.x - start.x).powi(2) + (end.y - start.y).powi(2)).sqrt();
assert!((len - 8.0).abs() < 1e-6, "length should track the redefined parameter value, got {len}");
}
/// A `driving_param` referencing a named parameter that doesn't (or no
/// longer) exists must not panic the solve — same "skip, don't panic"
/// contract `build_constraint`'s doc comment gives every other unbuildable
/// constraint. The referenced line's two endpoint coordinates are still
/// registered (via `point_ref`) even though the constraint itself can't be
/// built, so they're just left free — the edit that triggered this solve is
/// not undone or altered.
#[test]
fn distance_constraint_with_an_undefined_named_reference_is_skipped_not_panicked() {
let mut scene = Scene::new();
let line = add_line(&mut scene, 0.0, 0.0, 10.0, 0.0);
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
ConstraintKind::Distance,
vec![SketchRef::point(line, 0), SketchRef::point(line, 1)],
Some(DrivingValue::Named("does_not_exist".to_string())),
);
let edited_end = Vector3::new(5.0, 5.0, 0.0);
set_line_end(&mut scene, line, edited_end);
scene.bump_entities(&[(line, ChangeKind::Modified)]); // must not panic
let (_, end) = line_endpoints(&scene, line);
assert_eq!(end, edited_end, "an unresolvable driving reference must leave the edit alone, not move it toward a phantom target");
}
#[test]
fn tangent_constraint_between_a_line_and_a_circle_solves_to_touching() {
let mut scene = Scene::new();
@@ -285,8 +343,8 @@ fn two_conflicting_distance_targets_are_reported_as_conflicting() {
let mut scene = Scene::new();
let a = add_line(&mut scene, 0.0, 0.0, 10.0, 0.0);
let set = scene.sketch_constraint_set_mut(SketchScope::ModelSpace);
let first = set.add(ConstraintKind::Distance, vec![SketchRef::point(a, 0), SketchRef::point(a, 1)], Some(20.0));
let second = set.add(ConstraintKind::Distance, vec![SketchRef::point(a, 0), SketchRef::point(a, 1)], Some(50.0));
let first = set.add(ConstraintKind::Distance, vec![SketchRef::point(a, 0), SketchRef::point(a, 1)], Some(DrivingValue::Literal(20.0)));
let second = set.add(ConstraintKind::Distance, vec![SketchRef::point(a, 0), SketchRef::point(a, 1)], Some(DrivingValue::Literal(50.0)));
scene.bump_entities(&[(a, ChangeKind::Modified)]);
@@ -5,6 +5,7 @@
// (save/load integration, the "lazy" persistence model) is gated on this
// test passing — see the design doc §8 stage 2 and §7 open question 1.
use OpenCADStudio::scene::named_parameters::DrivingValue;
use OpenCADStudio::scene::sketch_constraints::{ConstraintKind, SketchConstraintSet, SketchRef, SketchScope};
use OpenCADStudio::scene::Scene;
use acadrust::entities::EntityType;
@@ -20,9 +21,13 @@ fn sample_set(scope: SketchScope) -> SketchConstraintSet {
vec![SketchRef::point(Handle::new(101), 0), SketchRef::point(Handle::new(102), 1)],
None,
);
set.add(ConstraintKind::Distance, vec![SketchRef::whole(Handle::new(103))], Some(25.0));
set.add(ConstraintKind::Angle, vec![SketchRef::whole(Handle::new(104)), SketchRef::whole(Handle::new(105))], Some(45.0));
set.add(ConstraintKind::Radius, vec![SketchRef::whole(Handle::new(106))], Some(3.5));
set.add(ConstraintKind::Distance, vec![SketchRef::whole(Handle::new(103))], Some(DrivingValue::Literal(25.0)));
set.add(
ConstraintKind::Angle,
vec![SketchRef::whole(Handle::new(104)), SketchRef::whole(Handle::new(105))],
Some(DrivingValue::Literal(45.0)),
);
set.add(ConstraintKind::Radius, vec![SketchRef::whole(Handle::new(106))], Some(DrivingValue::Literal(3.5)));
set
}