Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f21530c
Add ConstraintLens architectural spec
claude May 21, 2026
de15db0
Lock spec decisions: latest Fusion only, GitHub Releases, Solid only
claude May 21, 2026
7151cd7
Add fixture and spike-probe scripts
claude May 21, 2026
87bf2ea
Scaffold ConstraintLens add-in (MVP code, awaiting PC validation)
claude May 21, 2026
6e7d418
Harden scaffold, add M-1 fixture, PC validation checklist, MIT license
claude May 21, 2026
b54d817
Fix OffsetConstraint accessor crash, auto-load timing, and script naming
claude May 21, 2026
01e16a0
Bump version to 0.1.2 for second test session
claude May 21, 2026
f042105
Close Q4: entityToken stable across save-reload (confirmed PC test 2)
claude May 21, 2026
eb88e86
Close Q1/Q2/Q3/Q5: all open questions resolved by spike probe (PC tes…
claude May 21, 2026
cd195f6
Fix three bugs from PC test 1: spline selection, offset-dim highlight…
claude May 21, 2026
d335fab
Add CLAUDE.md state file for cross-session continuity
claude May 21, 2026
5e5d0c7
Add PreToolUse hook: remind to update CLAUDE.md before git push
claude May 21, 2026
290abda
Update CLAUDE.md: awaiting PC test 3 before GitHub Release
claude May 21, 2026
f525552
0.1.4: OffsetConstraint chips + SketchOffsetCurvesDimension fallback
claude May 21, 2026
38235e9
0.1.5: multi-strategy match for SketchOffsetCurvesDimension
claude May 21, 2026
52382ed
PC test 5 passed: mark all tests complete, update README for release
claude May 21, 2026
da880ae
Add .gitignore: exclude release zips, pycache, .DS_Store
claude May 21, 2026
c4513c5
Backlog #10 and #11: dimension entity labels + fully-constrained veri…
claude May 21, 2026
457a2ec
CLAUDE.md: v0.1.5 released, pivot to v1 backlog
claude May 21, 2026
bfca8f2
Mark backlog #11 verified: fully-constrained green status confirmed i…
claude May 21, 2026
0482bd5
v0.1.6: button relocation, Show u/c, filter bar, label polish (#1 #2 …
May 22, 2026
b837baa
v0.1.7: bulk delete with confirmation, invisible entity badges (#5 #8)
May 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(git push*)",
"command": "BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null); ORIGIN=\"origin/$BRANCH\"; if git rev-parse \"$ORIGIN\" >/dev/null 2>&1; then BASE=\"$ORIGIN\"; else BASE=$(git rev-list --max-parents=0 HEAD 2>/dev/null); fi; COUNT=$(git log \"$BASE..HEAD\" --oneline 2>/dev/null | wc -l | tr -d ' '); if [ \"$COUNT\" -gt 0 ] && ! git log \"$BASE..HEAD\" --name-only --format=\"\" 2>/dev/null | grep -q \"^CLAUDE\\.md$\"; then printf '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"additionalContext\":\"REMINDER: CLAUDE.md has not been updated in the commits being pushed. Update the Current Status section (Working on / Next step / Blocked by) and commit it before pushing.\"}}'; fi",
"statusMessage": "Checking CLAUDE.md is up to date…"
}
]
}
]
}
}
16 changes: 16 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Release artifacts — built locally, uploaded to GitHub Releases
*.zip

# Python
__pycache__/
*.pyc
*.pyo

# macOS
.DS_Store

# Ruff / linter caches
.ruff_cache/

# IDE
.vscode/
120 changes: 120 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# FusionConstraints — ConstraintLens Add-in

## Current Status

**Working on:** v1 polish backlog.
**Version:** 0.1.7 released — backlog #5, #8 shipped. Previous: 0.1.6 (backlog #1, #2, #3, #9, #10).
**Next step:** Pick next backlog item (see v1 Polish Backlog below).
**Blocked by:** Nothing.

### What's verified working (PC tests 1–5 + v0.1.6 session)
- Add-in loads, palette docks, populates without Refresh click.
- Geometric constraints list with click-to-select, ⌖ select-constraint, × delete + auto-refresh.
- Dimensions list (Angular, Linear, Diameter, etc.) with parameter expression.
- Implicit endpoint joins as pseudo-rows with implicit badge AND ⊘ lock icon + tooltip (0.1.3).
- Tangent spline+line row highlights both objects on click (0.1.3 — token-based selection).
- OffsetConstraint row lists curve chips (0.1.4).
- SketchOffsetCurvesDimension row lists curve chips AND highlights on click (0.1.5). ✓ PC test 5
- Sketch status banner (name, component, fully/under-constrained).
- M-1 defensive guard (MidPoint accessor) — both rows render; no crash.
- OffsetConstraint ACCESSOR error fixed in 0.1.2.
- Auto-load fixed in 0.1.2 (palette populated without Refresh click).
- Button relocated to `SketchConstraintsPanel` (Sketch tab → Constraints panel). ✓ backlog #1
- "Show u/c" button highlights underconstrained entities on canvas via `executeTextCommand("Sketch.ShowUnderconstrained")`; shows result string as toast. ✓ backlog #2
- Filter bar narrows rows by label or constraint type (case-insensitive, client-side); section headers show filtered count. ✓ backlog #3
- OffsetConstraint label normalised: `Offset (1→1 curves, 30 mm)` style. ✓ backlog #9
- Dimension entity chips show friendly names ("Line 2 → Line 3") for Angular, Diameter, Radial and other type-specific-accessor subtypes. ✓ backlog #10
- Bulk delete: checkboxes on deletable rows, "Delete N" + "Clear" buttons in toolbar, confirm dialog, Python loops deletions. Single × button removed. ✓ backlog #5
- Invisible entity chips (e.g. hidden spline-offset control geometry) rendered dimmed with dashed border and "hidden" badge. ✓ backlog #8

### What was fixed in 0.1.5 (verified PC test 5)
- **SketchOffsetCurvesDimension matching, hardened** — `_find_offset_constraint_for_dim` stacks four strategies because `OffsetConstraint.distance` returns None on the January 2026 build: (1) parameter entityToken, (2) parameter name, (3) positional pairing, (4) single-constraint fallback. Once matched, constraint's `parentCurves` + `childCurves` become the dimension's chips.

### Known sub-issues to keep on radar
- Offset-of-spline creates internal control geometry that Fusion doesn't render. User reports a tangent constraint on a line that isn't visible on the canvas. The row still appears in ConstraintLens but the line can't be selected by the user. See backlog #8.
- `OffsetConstraint.distance` returns None in the January 2026 build. The label-only consequence is now fully resolved via the matched dimension's parameter expression (backlog #9 fix).

---

## Project Overview

A Fusion 360 Python add-in that docks a panel listing every constraint in the active sketch — with click-to-select, delete, and over/under-constrained status. Fills the UX gap of having to hunt tiny on-canvas glyphs to audit a sketch.

- **Language:** Python 3.14 (Fusion January 2026 build), vanilla JS palette (no framework).
- **Distribution:** GitHub Releases only (zipped `ConstraintLens/` folder). No App Store.
- **Workspace:** Solid workspace only (MVP). Button lives in `SketchConstraintsPanel` (Sketch tab → Constraints panel).
- **Spec:** `SPEC.md` — complete, all 5 open questions resolved.

---

## Architecture

```
ConstraintLens/
├── ConstraintLens.manifest Fusion add-in manifest (id, version, runOnStartup)
├── ConstraintLens.py Entry point — delegates to lib/lifecycle only
└── lib/
├── lifecycle.py Command + palette creation, message routing
├── events.py GC-safe event handler registry (M-7 guard)
├── dispatch.py 21-row constraint type dispatch table + dimension dispatch
├── scanner.py Sketch enumeration → JSON payload
├── labels.py EntityLabeler: token→"Line 3" map per scan
├── selection.py ui.activeSelections helpers
├── actions.py delete_constraint() with isDeletable check
├── tokens.py token_of() / resolve() wrappers
└── messaging.py palette.sendInfoToHTML / parse_incoming + M-8 guard
palette/
├── index.html Shell; initial "Loading…" state
├── app.js Vanilla JS render loop + message handler
└── styles.css Dark theme matching Fusion
tests/
├── fixture_sketch/ Creates ConstraintLens_Fixture (4 constraints, 2 dims)
├── spike_probe/ API feasibility probe (all 5 Qs answered — run again after Fusion updates)
└── fixture_midpoint/ M-1 trigger fixture (midpoint-to-midpoint)
```

### Key conventions
- **Collections:** `SketchCurveVector` (from `.parentCurves`, `.childCurves`, `.curves`) uses `len()` + iteration, not `.count`. `ObjectCollection` uses `.count` + `.item(i)`.
- **Event handlers:** Always appended to `events._handlers` list (M-7). Never instantiate a handler without pinning it.
- **Palette sends:** Always gated on `palette.isVisible` (M-8 guard in `messaging.send()`).
- **Entity selection:** JS sends `entityTokens` list; Python resolves each via `tokens.resolve()` (primary path). `_entities_for_row()` accessor re-scan is fallback only.
- **Test scripts:** Each must be in a same-named subfolder (e.g. `tests/fixture_sketch/fixture_sketch.py`) — Fusion requirement.

---

## Resolved Open Questions (SPEC.md §10)

| # | Question | Answer |
|---|---|---|
| Q1 | Panel id | `SolidScriptsAddinsPanel` confirmed. `SketchConstraintsPanel` exists for v1 relocation. |
| Q2 | ShowUnderconstrained precondition | Requires sketch edit context. Returns plain string `'Under constrained points: N, under constrained curves: N'`. |
| Q3 | Palette `shown` event | No `shown`/`opened` event exists. `commandTerminated` is the only refresh trigger after restore. |
| Q4 | entityToken stability | Stable across save-reload. `findEntityByToken` returns non-empty `BaseVector`. |
| Q5 | VerticalConstraint enumerated? | Yes — `adsk::fusion::VerticalConstraint` appears in `geometricConstraints` iteration. |

---

## Known Remaining Limitations (MVP scope, documented in README)

- No granular undo for delete — Fusion `Ctrl+Z` reverts the whole sketch-edit chunk.
- Implicit coincident joins cannot be deleted from the panel (shared `SketchPoint`, not a real constraint).
- `CircularPatternConstraint` / `RectangularPatternConstraint`: Delete only; no entity accessor.
- `AssemblyConstraint` not supported (preview API, January 2026).
- Palette has no `shown` event — stale data after minimize/restore until next `commandTerminated`.

---

## v1 Polish Backlog (post-MVP, not started)

1. ~~Move button to `SketchConstraintsPanel` for in-sketch discoverability.~~ **DONE ✓**
2. ~~"Show underconstrained" button~~ — **DONE ✓** "Show u/c" button in toolbar; calls `executeTextCommand("Sketch.ShowUnderconstrained")`; result surfaced as toast.
3. ~~Filter / search by constraint type~~ — **DONE ✓** Filter bar below toolbar; client-side filtering by label/kind; section headers show `(N of M)` when active.
4. Constraint icons matching Fusion's own glyph set.
5. ~~Bulk delete with confirmation.~~ **DONE ✓** Checkboxes on deletable rows; "Delete N" + "Clear" toolbar buttons; Ctrl+Z note in confirm dialog; single × button removed.
6. Inline editable dimension expression.
7. **Sketch-→-palette reverse lookup** — user picks an entity on the canvas, ConstraintLens scrolls to / highlights every row that references it. Lets the user start from the geometry rather than the list.
8. ~~**Mark invisible / unselectable entities**~~ — **DONE ✓** `chip_for()` checks `entity.isVisible`; invisible chips rendered dimmed + dashed border + "hidden" badge. Note: clicking a row still selects/reveals the hidden entity on canvas — this is Fusion's native behaviour.
9. ~~**Normalize OffsetConstraint label**~~ — **DONE ✓** Label is now `Offset (1→1 curves, 30 mm)` style, pulling expression from the matched SketchOffsetCurvesDimension.
10. ~~**Dimension entity chip labels — show "Line 2" not "SketchLine"**~~ — **DONE ✓** `_DIM_ACCESSORS` map added to `dispatch.py`; Angular/Diameter/Radial and others now use type-specific accessors with `entityOne`/`entityTwo` fallback.
11. ~~**Verify fully-constrained green status**~~ — **VERIFIED PC test (session 5+).** Banner turns green and reads "— fully constrained" correctly.
12. **Canvas-to-palette entity name lookup** — user clicks a sketch entity on the canvas, sees its ConstraintLens name (e.g. "Line 3") somewhere in the UI, then can type that name into the filter bar to find all rows that reference it. Complement to backlog #7 (reverse lookup that auto-scrolls); this simpler variant just exposes the name. Could be implemented as a hover tooltip on canvas selection events, a small "selected entity" readout in the palette toolbar, or by reacting to Fusion's `activeSelections` change event and displaying the resolved label.
13 changes: 13 additions & 0 deletions ConstraintLens/ConstraintLens.manifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"autodeskProduct": "Fusion360",
"type": "addin",
"id": "8a3f4d5e-1c2b-4f6a-9e0d-7b5c8a2d1e3f",
"author": "ConstraintLens contributors",
"description": {
"": "Docked panel listing every sketch constraint in the active sketch — click to select, delete, and diagnose."
},
"version": "0.1.7",
"runOnStartup": false,
"supportedOS": "windows|mac",
"editEnabled": true
}
33 changes: 33 additions & 0 deletions ConstraintLens/ConstraintLens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# ConstraintLens — Fusion 360 add-in entry point (see SPEC.md section 4).
#
# Owns only run(context) / stop(context). All logic lives under ./lib.

import os
import sys
import traceback

import adsk.core

_ADDIN_DIR = os.path.dirname(os.path.realpath(__file__))
if _ADDIN_DIR not in sys.path:
sys.path.insert(0, _ADDIN_DIR)

from lib import lifecycle # noqa: E402


def run(context):
try:
lifecycle.start(_ADDIN_DIR)
except Exception:
ui = adsk.core.Application.get().userInterface
if ui:
ui.messageBox("ConstraintLens failed to start:\n" + traceback.format_exc())


def stop(context):
try:
lifecycle.stop()
except Exception:
ui = adsk.core.Application.get().userInterface
if ui:
ui.messageBox("ConstraintLens failed to stop cleanly:\n" + traceback.format_exc())
Empty file added ConstraintLens/lib/__init__.py
Empty file.
36 changes: 36 additions & 0 deletions ConstraintLens/lib/actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# lib/actions.py — destructive operations (SPEC.md section 4).

from dataclasses import dataclass

import adsk.core
import adsk.fusion

from . import tokens


@dataclass(frozen=True)
class ActionResult:
ok: bool
message: str


def delete_constraint(app: adsk.core.Application, token: str) -> ActionResult:
design = adsk.fusion.Design.cast(app.activeProduct)
if not design:
return ActionResult(False, "No active design.")
entity = tokens.resolve(design, token)
if entity is None:
return ActionResult(False, "Constraint not found (token unresolved).")
try:
is_deletable = bool(entity.isDeletable)
except Exception:
is_deletable = True
if not is_deletable:
return ActionResult(False, "Constraint reports isDeletable == False.")
try:
ok = bool(entity.deleteMe())
except Exception as exc:
return ActionResult(False, f"deleteMe() raised: {exc}")
if not ok:
return ActionResult(False, "deleteMe() returned False.")
return ActionResult(True, "Deleted.")
Loading
Loading