Hakan (upstream maintainer) asked for the sketch-constraint system to
persist using the constraint object model already exposed in acadrust/
cadcodec (Assoc2dConstraintGroup, AssocConstraintNode), so a file saved
by OpenCADStudio carries constraints AutoCAD/BricsCAD recognize as
their own native objects, not just an opaque custom XRecord blob.
Covers:
- The full ConstraintKind -> AssocConstraintNodeData mapping table,
grounded in build_constraint's actual refs shapes (the real contract,
not the doc comments).
- The geometry-node mapping for SketchRef (line/circle/arc/point
references) onto Line/BoundedLine/Circle/Arc/ImplicitPoint nodes.
- The AssocNetwork -> AssocAction -> Assoc2dConstraintGroup graph shape
actually needed, not just a flat data swap.
- A proposed additive persistence strategy: write both our own
OCS_SKETCH_CONSTRAINTS XRecord and the native graph, since our model
carries things (DrivingValue::Named) the native graph can't yet
losslessly express.
- Known gaps: Equal has no length/radius sub-kind split natively,
several AutoCAD constraint types we don't model at all, Tangent
involving an Arc (a pre-existing gap in the constraint system,
unrelated to DWG compatibility).
Three of five open questions are resolved against Autodesk's own
primary sources rather than left as inference: the ObjectARX 2012 SDK's
own C++ headers (mirrored on GitHub, Autodesk's own copyright notice
grants free use/distribution -- it's the SDK Autodesk publishes for
third-party interop) and Autodesk's live public ObjectARX reference
site. Confirmed: the associative network's dictionary key
("ACAD_ASSOCNETWORK"), the AcDb::ImplicitPointType enum settling how
SketchRef's point markers map to constraint geometry nodes, and that
AssocVariable's evaluator field can be left empty (Autodesk's own doc:
"the default evaluator for current acad version will be used"). Two
questions remain and need a real write-then-open test rather than more
reading: whether AutoCAD tolerates a partially-populated graph, and
DWG version gating on fields not yet checked.
Suggested staged approach: prove out Horizontal/Vertical on a single
Line first (sidesteps the value-dependency wiring), verify it opens
cleanly in real AutoCAD/BricsCAD or a DWG inspector, before writing
more of the graph on an unverified foundation. Design only -- no
implementation yet.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
23 KiB
DWG-Native Constraint Compatibility — Design
1. What this is, and why
Hakan (upstream maintainer) reviewed the sketch-constraint system
(docs/parametric_system_design.md) and asked for it to persist using the
constraint object model already exposed in acadrust/cadcodec (the pinned
CAD kernel), so a file saved by OpenCADStudio carries constraints AutoCAD or
BricsCAD would recognize as their own native objects — not just an opaque
custom blob only this app understands.
This is purely a persistence-format question, not a solver question.
ocs_gcs (the FreeCAD planegcs port: crates/ocs_gcs/src/solvers/{bfgs,dogleg,lm}.rs)
is the numerical engine that computes geometry from a constraint system —
that stays exactly as it is regardless of what this document decides. What
changes is only how SketchConstraintSet gets written to and read from a
CadDocument, in src/scene/sketch_persist.rs, currently a custom
bincode-in-XRecord blob keyed OCS_SKETCH_CONSTRAINTS.
2. What acadrust already provides (confirmed from source)
All paths below are relative to the pinned checkout
(~/.cargo/git/checkouts/cadcodec-*/*/src/, rev pinned in Cargo.toml).
objects/associative.rsdefines AutoCAD's real 2D geometric-constraint object graph as plain Rust types, reachable generically throughObjectType::Associative(AssociativeObject { data: AssociativeData::ConstraintGroup(Assoc2dConstraintGroup), .. })— the samedocument.objects: HashMap<Handle, ObjectType>mapsketch_persist.rsalready insertsXRecord/Dictionaryentries into.- Both DWG (
io/dwg/dwg_stream_{readers,writers}/object_{reader,writer}/associative.rs, ~1240/~870 lines) and DXF (io/dxf/{reader,writer}/section_{reader,writer}/associative.rs) have complete read and write implementations for every type below — confirmed by reading the actual field-by-field (de)serialization code, not inferred from the type definitions alone. classes/mod.rsregisters the DXF class names every one of these needs when written ((DXF_NAME, "AcDbClassName", ...)tuples):AcDbAssocDependency,AcDbAssocGeomDependency,AcDbAssocAction,AcDbAssocNetwork,AcDbAssoc2dConstraintGroup,AcDbAssocVariable,AcDbAssocPersSubentManager.
The graph shape
Not a flat list — a small object graph, matching AutoCAD's real associative framework:
AssocNetwork (per block/space that owns constrained geometry)
└─ owned_actions: Vec<Handle> ─→ AssocAction
├─ action_body: Handle ─→ Assoc2dConstraintGroup
│ ├─ work_plane: [Vector3; 3]
│ └─ nodes: Vec<AssocConstraintNode>
│ (both the *constraint* nodes
│ and the *geometry* nodes live
│ in this one Vec — see §3/§4)
└─ dependencies: Vec<AssocActionDependency>
─→ AssocGeomDependency
├─ dependency: AssocDependency
│ └─ dependent_on: Handle
│ (the real Line/Circle/... entity)
└─ persistent_subent: AssocPersistentSubentId
AssocConstraintNode { node_id: i32, status: u8, connections: Vec<i32>, class_name: String, registry_flag: bool, data: AssocConstraintNodeData } —
node_id/connections link nodes to each other within the group (a
constraint node's owner_id fields point at a geometry node's node_id);
class_name picks which AssocConstraintNodeData variant applies on
read (read_constraint_node_data(reader, class_name),
dwg_stream_readers/object_reader/associative.rs:663).
2b. Primary sources beyond cadcodec's own code
cadcodec ships no constraint-bearing test fixture and its own source only
shows what bytes get read, not what they mean semantically — §7 (now
mostly resolved) needed Autodesk's own interface documentation to answer.
Two real, first-party sources turned out to be publicly available with no
sample DWG file needed at all:
- The ObjectARX 2012 SDK's own C++ headers, mirrored on GitHub at
Stalso/CadPlugin
— Autodesk's own copyright header on each file explicitly grants "permission
to use, copy, modify, and distribute this software... for any purpose and
without fee", and this is the SDK Autodesk itself publishes specifically so
third parties can build interoperable tools — not leaked or decompiled
source.
AcDbAssoc2dConstraintGroup.h,AcDbAssocNetwork.h,AcDbAssocManager.h,AcDbAssocVariable.h,AcConstrainedGeometry.hwere all read directly; their class-level doc comments describe the exact same object graphcadcodecimplements, in Autodesk's own words. - Autodesk's live public ObjectARX reference
(
help.autodesk.com/view/OARXMAC/2025/...) — confirmed theAcDb::ImplicitPointTypeenum's exact values and per-member descriptions (used for §4'spoint_typemapping) andAcDbAssocNetwork'sgetInstanceFromObject/getInstanceFromDatabasedictionary-key default (used for §7 question 1). No login or SDK download needed — the reference guide is indexed and publicly browsable.
3. Constraint-kind mapping
Our ConstraintKind (src/scene/sketch_constraints.rs) against
AssocConstraintNodeData (objects/associative.rs:885). The "refs shape"
column is copied from sketch_solve::build_constraint's actual match
(src/scene/sketch_solve.rs:151-208) — the ground truth for what each kind
expects, not the doc comment.
ConstraintKind |
refs shape (from build_constraint) |
AssocConstraintNodeData |
Notes |
|---|---|---|---|
Coincident |
[point, point] |
Geometrical + class_name = "ACPOINTCOINCIDENCECONSTRAINT" |
|
Horizontal |
[whole_line] |
Geometrical + "ACHORIZONTALCONSTRAINT" |
|
Vertical |
[whole_line] |
Geometrical + "ACVERTICALCONSTRAINT" |
|
Parallel |
[whole_line, whole_line] |
Parallel { datum_line_index: None, .. } |
dedicated variant, not Geometrical |
Perpendicular |
[whole_line, whole_line] |
Geometrical + "ACPERPENDICULARCONSTRAINT" |
|
Equal (line case) |
[whole_line, whole_line] |
Geometrical + "ACEQUALLENGTHCONSTRAINT" |
|
Equal (circle/arc case) |
[whole_circle, whole_circle] |
Geometrical + "ACEQUALRADIUSCONSTRAINT" |
our Equal doesn't distinguish these — see §5 |
Distance |
[point, point] |
Distance { direction_type, distance: Option<Vector3>, value_dependency, dimension_dependency, .. } |
needs an AssocVariable/value dependency for the driving value — see §5 |
Angle |
[whole_line, whole_line] |
Angle { sector_type, value_dependency, dimension_dependency, .. } |
same value-dependency need |
Radius |
[whole_circle] |
RadiusDiameter { mode, value_dependency, dimension_dependency, .. } |
mode presumably distinguishes radius vs. diameter — we're always radius |
Tangent |
[circle, circle] or [circle, line]/[line, circle] — Line-Line and anything involving an Arc are explicitly unsupported today (sketch_solve.rs:239-241, Arc isn't a supported EntityGeom variant yet) |
Geometrical + "ACTANGENTCONSTRAINT" |
our own Arc-tangent gap predates this document, unrelated to DWG compatibility |
AutoCAD's is_plain_geometrical_constraint list also includes several kinds
we have no ConstraintKind for at all: ACCENTERPOINTCONSTRAINT,
ACCOLINEARCONSTRAINT, ACCONCENTRICCONSTRAINT, ACEQUALCURVATURECONSTRAINT,
ACEQUALDISTANCECONSTRAINT, ACEQUALHELPPARAMETERCONSTRAINT,
ACFIXEDCONSTRAINT, ACMIDPOINTCONSTRAINT, ACNORMALCONSTRAINT,
ACPOINTCURVECONSTRAINT, ACSYMMETRICCONSTRAINT — genuinely nothing to map
to yet, not a mapping gap (§5).
4. Geometry-node mapping
Every entity a constraint references also needs its own node in the same
nodes: Vec<AssocConstraintNode>, so constraint nodes have something to
point owner_id at, and a paired AssocGeomDependency (in the owning
AssocAction's dependencies) so the graph — not just the constraint data
— actually references the real Line/Circle/... entity by Handle.
| Our reference | AssocConstraintNodeData geometry variant |
|---|---|
SketchRef::whole(line) |
BoundedLine { point, direction, is_ray: false, start_point, end_point, .. } — confirmed a bounded line is the right choice: Line/BoundedLine's split exists because AutoCAD's own constrained-geometry set includes true infinite construction lines (ACCONSTRAINEDCONSTRUCTIONLINE/ACCONSTRAINEDDATUMLINE, read_constraint_node_data, associative.rs:711-714) as a separate case from an ordinary bounded segment; nothing we draw is an infinite line |
SketchRef::point(line, 0|1) |
ImplicitPoint { point_type: kStartImplicit(0)|kEndImplicit(1), point_index: -1, curve_id: <the Line/BoundedLine node's node_id>, .. } — confirmed, AcDb::ImplicitPointType enum, kStartImplicit/kEndImplicit/kMidImplicit/kCenterImplicit/kDefineImplicit (0-4), Autodesk's own live ObjectARX reference (§2b). point_index is -1 for every case except kDefineImplicit (a spline control point, which we don't reference); curve_id is presumably the owning geometry node's own node_id (not yet cross-checked against a real file — the values are confirmed, this one wiring detail isn't) |
SketchRef::whole(circle) |
Circle { center, normal, direction, radius, start_parameter: 0.0, end_parameter: TAU, .. } |
SketchRef::whole(arc) |
Arc { .. same fields .. , start_point, end_point } |
SketchRef::center(circle|arc) |
ImplicitPoint { point_type: kCenterImplicit(3), point_index: -1, curve_id: <the Circle/Arc node's node_id>, .. } — confirmed by the same enum (kCenterImplicit: "Center point of a circle, arc, ellipse or bounded ellipse") — not the separate Point/ACCONSTRAINEDPOINT variant, which the ObjectARX docs' own class hierarchy (§2b) show is for a free-standing constrained point, not one implicitly defined by another curve |
SketchRef::point(_, -2) (reserved, unused today) |
would be kMidImplicit if ever used — "Mid point of a bounded line ... or arc" is exactly what marker -2's doc comment already reserved this for |
SketchRef and this graph already agree on one thing directly: our own doc
comment (src/scene/sketch_constraints.rs:20-22) says the marker
convention was deliberately borrowed from
AssocDimensionReference::main_gs_marker (src/scene/dimension_assoc.rs,
already shipping, already reading/writing real Assoc* objects for
associative dimensions). That's a different, lighter mechanism than the
constraint graph (an AcDbOsnapPointRef chain attached directly to a
Dimension entity, not the AssocNetwork/AssocAction framework — see
AssocDimensionAssociation, objects/associative.rs:1200), so it's a
precedent for how this codebase already talks to Assoc* types, not a
template for the constraint graph's own wiring.
5. Known gaps (not addressed by this document)
-
Equalis oneConstraintKind, AutoCAD has (at least) two:ACEQUALLENGTHCONSTRAINT(lines) vs.ACEQUALRADIUSCONSTRAINT(circles/arcs), plusACEQUALCURVATURECONSTRAINT/ACEQUALDISTANCECONSTRAINTwe don't model at all.build_constraintalready branches onrefs' resolved shape at solve time (sketch_solve.rs:181-188), so the same branch can pick the write-sideclass_name— no data-model change needed for the length/radius split; the other two AutoCAD sub-kinds are a real, separate gap. -
DrivingValue::Named— confirmed, has a real native equivalent, and the full wiring is now traced end to end.AcDbAssocVariable(objects/associative.rs:1024:name,expression,evaluator,description, cachedvalue) is fully read and written on both DWG and DXF (confirmed indwg_stream_readers/object_reader/associative.rs:1031-1063and the DXF reader/writer equivalents) — it's the same associative-action graph a constraint group lives in (AssocVariablewraps anAssocAction, exactly likeAssoc2dConstraintGroupdoes), not a separate table.The two handles on
Distance/Angle/RadiusDiameter(read_explicit_constraint,associative.rs:652-661— reads them in this order:value_dependencyfirst,dimension_dependencysecond) resolve to two different, unrelated things:value_dependency→ anAssocValueDependencyobject ({ dependency: AssocDependency, name, value: AssocEvalVariant },objects/associative.rs:554). Its ownvalue: AssocEvalVariant({ code: i16 /* DXF resbuf type */, value: AssocEvalValue::Real(f64) }, confirmedassociative.rs:579-596) is the cached, already-resolved number — always present, read fast without evaluating anything. The embeddedAssocDependency.dependent_on: Handle(read_dependency,associative.rs:83-109) is what actually links to a named parameter:Handle::NULLwhen the driving value is a bare literal, or anAssocVariable's handle when it's driven by one. This gives a direct, confirmed mapping:DrivingValue::Literal(v)→AssocValueDependencywithvalue = Real(v),dependency.dependent_on = Handle::NULL.DrivingValue::Named(name)→ the same, butdependency.dependent_onpoints at anAssocVariable{ name, expression: <our formula>, .. }object (create once per distinct name, reused by every constraint referencing it —valuestill carries the current resolved number, kept in sync withParameterTable's last evaluation, same duty our owndof/conflictscaches already have).
dimension_dependency→ a separate dependency chain (AssocAnnotationDependency/AssocDimDependencyBody,associative.rs:744-751and:1077-1082) linking to the actualDimensionentity AutoCAD draws next to a constraint to show its current value on screen — nothing to do with named parameters at all. Not required for our purposes unless we also want the constraint to carry a visible on-canvas dimension the way AutoCAD's does (open question, not yet decided — §7).
Remaining gap before mapping directly: AutoCAD's
expressionis evaluated by a named, extensibleevaluator(not fixed) — real-file survey needed to know what evaluator name(s) actually appear in practice (§7 open question 4), ours has one hardcoded evaluator. -
Missing
ConstraintKinds: Concentric, Colinear, Symmetric, Fixed, Midpoint, Normal, CenterPoint, PointOnCurve — real AutoCAD constraint types with noConstraintKindvariant to map from at all. Out of scope until/unless the sketch-constraint system itself grows them. -
Tangentinvolving anArchas no native mapping to design, sincebuild_constraintdoesn't support it at all yet (sketch_solve.rs:239-241—Arcisn't a supportedEntityGeomvariant in that function). A pre-existing gap in the constraint system itself, unrelated to and not blocking this document.
6. Persistence architecture: additive, not a replacement
Proposal: write both. Keep OCS_SKETCH_CONSTRAINTS as the authoritative
round-trip format OpenCADStudio itself reads back
(sketch_persist::load_sketch_constraints_from_document), and additionally
materialize the AssocNetwork/.../Assoc2dConstraintGroup graph alongside
it on save, for other applications to read.
Reasoning:
- Our own model carries information the native graph can't represent
losslessly today —
DrivingValue::Namedchief among them (§5), alsoenabled(a suppressed-but-kept constraint) which may or may not have a clean native equivalent (AssocConstraintNodeData'sis_activefield on several variants looks like the right fit, but unconfirmed, §7). Round-tripping through the native format only and reconstructing our model from it risks silent data loss on every save/reopen cycle. - The native graph's own geometry-node fields (
center,radius,point, ...) are snapshots of the current solved geometry, not derived at load time — so they need to be kept in sync with whateverocs_gcslast solved, same as we already do for our own tessellated wire geometry. Two representations of "the same" constraint, kept consistent by one save path, is a smaller invariant to maintain than trying to make the native graph the only source of truth for a system it can't fully express. - This is reversible: if the native graph turns out to work well enough
(once tested against real AutoCAD/BricsCAD opens, §7) to become the
primary format later, dropping the redundant
OCS_SKETCH_CONSTRAINTSwrite is a small follow-up, not a rewrite.
7. Open questions
Three of the original five are now resolved from Autodesk's own primary
sources (§2b), not just inferred from cadcodec's struct shapes — kept
here with their answers so the reasoning stays visible, not deleted.
cadcodec itself still ships no constraint-bearing DWG/DXF fixture and no
constraint-specific test (checked: no hits for Assoc2dConstraintGroup/
AssocConstraintNode under any tests/ directory or fixture path in the
dependency), so even the resolved ones haven't been checked against a real
written-and-reopened file yet — that step still matters (§8 stage 1) for
confirming implementation correctness, just not for the design questions
below anymore.
- Resolved. Where the top-level
AssocNetworkattaches:AcDbAssocNetwork::getInstanceFromDatabase/getInstanceFromObject's own doc comments (§2b) state the default sub-dictionary key directly —"ACAD_ASSOCNETWORK"— used both for the one top-level network (owned by the database's Named Object Dictionary) and for each per-block-record network (owned by thatAcDbBlockTableRecord's own extension dictionary — confirming the parallel toOCS_SKETCH_CONSTRAINTS's own attachment was right). The two are linked: creating a per-block network withaddToTopLevelNetwork = true(the default) also registers it as an action inside the top-level network, so evaluating the top-level network cascades through every block's sub-network. - Resolved. Sub-entity addressing for
ImplicitPoint: confirmed the real addressing lives entirely in the constraint-node's ownpoint_type/point_index/curve_idfields, not inAssocPersistentSubentId(that type turned out to be an abstract C++ base class for a different, more general subentity-tracking need elsewhere in the associative framework — irrelevant to 2D constraint geometry).AcDb::ImplicitPointType's exact enum (§4) settles the mapping. - Still open. Whether a partially-populated graph (e.g. omitting
AssocPersSubentManager, or leaving someAssocActionbookkeeping fields at theirDefault) still opens cleanly in real AutoCAD, or whether it requires every field correctly populated to avoid a repair prompt or silent constraint loss. Genuinely needs a real write-then-open test, not documentation — this is a robustness/tolerance question, not a structural one. - Narrowed to almost nothing.
AssocVariable'sevaluatorfield:AcDbAssocVariable.h's own doc comment (§2b) says plainly — "If empty, the default evaluator for current acad version will be used." Leaving it as an empty string is officially-supported behavior per the SDK itself, not a guess. No further investigation needed unless a real file later shows this doesn't round-trip as expected. - Still open. DWG version gating —
AssocConstraintNode's own reader already branches onversion.r2013_plus(dxf_version)for one field (dwg_stream_readers/object_reader/associative.rs:604); unclear whether other fields need per-version handling this document hasn't looked for. Likely answerable the same way as questions 1/2/4 were (Autodesk's own docs usually note version-specific members) — just not done yet. - Still open, low priority. Whether to bother wiring
dimension_dependencyat all (§5) — it drives the small on-canvas dimension label AutoCAD shows next to a constrained value, cosmetic rather than structural. Leaving itHandle::NULLis presumably safe (the constraint itself is still fully defined without it) but unconfirmed; deferring this to a later stage either way (§8).
8. Suggested staged approach
- Proof of concept, narrowest possible slice:
Horizontal/Verticalon a singleLine, no dimensional constraints (sidesteps theAssocVariable/value-dependency wiring entirely, though that's now fully designed too — §5). Write the graph —AssocNetworkat"ACAD_ASSOCNETWORK"on the scope owner's extension dictionary (§7.1), oneAssoc2dConstraintGroupaction, oneBoundedLinegeometry node, oneGeometrical/"ACHORIZONTALCONSTRAINT"(or vertical) constraint node — and open the file in whatever real-DWG-reading tool is available, to confirm AutoCAD/BricsCAD actually shows a constraint icon on the line and doesn't flag the file as corrupt. This is what answers open question 3 (partial-graph tolerance) — the one remaining question documentation alone can't settle. - Geometric constraints, still no driving value: add
Coincident(needsImplicitPoint/kStartImplicit/kEndImplicit, §4, now confirmed),Parallel,Perpendicular,Equal(both sub-kinds) — verify against the same real-file test from stage 1. - Dimensional constraints:
Distance,Angle,Radius, using the now fully-tracedvalue_dependency → AssocValueDependency → AssocVariablechain (§5) — including deciding whetherDrivingValue::Namedgets the real native mapping designed there, or stays a resolved-literal degrade for a first pass. - Everything else:
Tangent's non-circle-circle cases, the missingConstraintKinds (§5) if/when the sketch-constraint system grows them,dimension_dependency(§7.6) if a visible on-canvas dimension label turns out to matter.
Each stage's file should be hand-inspected (or opened in real software) before moving to the next — the graph's correctness genuinely can't be verified by this codebase's own round-trip tests alone, since a bug that's symmetric (write it wrong, read the same wrongness back) would pass here and still fail to open correctly elsewhere.
9. Progress
Design only — no implementation yet. Of the original five open questions,
three are now resolved from Autodesk's own primary documentation (§7.1, 7.2,
7.4) rather than inference — the network's dictionary key, ImplicitPoint's
addressing enum, and the AssocVariable evaluator default are no longer
guesses. Two remain (§7.3 partial-graph tolerance, §7.5 version gating) and
both genuinely need a real write-then-open test rather than more reading.
Next action per §8 is stage 1's narrowest slice — the design for it no
longer rests on an unconfirmed foundation, only on an unverified one.