Skip to content

Commit f47ad65

Browse files
Fix export of constraints for Fusion and SolidWorks.
Add documentation of SolidWorks COM quirks.
1 parent 01ed6e5 commit f47ad65

5 files changed

Lines changed: 788 additions & 55 deletions

File tree

sketch_adapter_fusion/adapter.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"""
99

1010
import math
11+
import uuid
1112
from typing import Any
1213

1314
from sketch_canonical.adapter import (
@@ -1389,12 +1390,17 @@ def _point_to_ref(self, point, element_id: str) -> PointRef:
13891390

13901391
return PointRef(element_id, PointType.CENTER)
13911392

1393+
def _generate_constraint_id(self) -> str:
1394+
"""Generate a unique constraint ID."""
1395+
return f"C_{uuid.uuid4().hex[:8]}"
1396+
13921397
def _convert_horizontal(self, constraint) -> SketchConstraint | None:
13931398
"""Convert a horizontal constraint."""
13941399
entity = constraint.line
13951400
entity_id = self._get_id_for_entity(entity)
13961401
if entity_id:
13971402
return SketchConstraint(
1403+
id=self._generate_constraint_id(),
13981404
constraint_type=ConstraintType.HORIZONTAL,
13991405
references=[entity_id]
14001406
)
@@ -1406,6 +1412,7 @@ def _convert_vertical(self, constraint) -> SketchConstraint | None:
14061412
entity_id = self._get_id_for_entity(entity)
14071413
if entity_id:
14081414
return SketchConstraint(
1415+
id=self._generate_constraint_id(),
14091416
constraint_type=ConstraintType.VERTICAL,
14101417
references=[entity_id]
14111418
)
@@ -1419,6 +1426,7 @@ def _convert_parallel(self, constraint) -> SketchConstraint | None:
14191426
id2 = self._get_id_for_entity(line2)
14201427
if id1 and id2:
14211428
return SketchConstraint(
1429+
id=self._generate_constraint_id(),
14221430
constraint_type=ConstraintType.PARALLEL,
14231431
references=[id1, id2]
14241432
)
@@ -1432,6 +1440,7 @@ def _convert_perpendicular(self, constraint) -> SketchConstraint | None:
14321440
id2 = self._get_id_for_entity(line2)
14331441
if id1 and id2:
14341442
return SketchConstraint(
1443+
id=self._generate_constraint_id(),
14351444
constraint_type=ConstraintType.PERPENDICULAR,
14361445
references=[id1, id2]
14371446
)
@@ -1445,6 +1454,7 @@ def _convert_tangent(self, constraint) -> SketchConstraint | None:
14451454
id2 = self._get_id_for_entity(curve2)
14461455
if id1 and id2:
14471456
return SketchConstraint(
1457+
id=self._generate_constraint_id(),
14481458
constraint_type=ConstraintType.TANGENT,
14491459
references=[id1, id2]
14501460
)
@@ -1458,6 +1468,7 @@ def _convert_equal(self, constraint) -> SketchConstraint | None:
14581468
id2 = self._get_id_for_entity(curve2)
14591469
if id1 and id2:
14601470
return SketchConstraint(
1471+
id=self._generate_constraint_id(),
14611472
constraint_type=ConstraintType.EQUAL,
14621473
references=[id1, id2]
14631474
)
@@ -1471,6 +1482,7 @@ def _convert_concentric(self, constraint) -> SketchConstraint | None:
14711482
id2 = self._get_id_for_entity(entity2)
14721483
if id1 and id2:
14731484
return SketchConstraint(
1485+
id=self._generate_constraint_id(),
14741486
constraint_type=ConstraintType.CONCENTRIC,
14751487
references=[id1, id2]
14761488
)
@@ -1484,6 +1496,7 @@ def _convert_collinear(self, constraint) -> SketchConstraint | None:
14841496
id2 = self._get_id_for_entity(line2)
14851497
if id1 and id2:
14861498
return SketchConstraint(
1499+
id=self._generate_constraint_id(),
14871500
constraint_type=ConstraintType.COLLINEAR,
14881501
references=[id1, id2]
14891502
)
@@ -1495,6 +1508,7 @@ def _convert_fixed(self, constraint) -> SketchConstraint | None:
14951508
entity_id = self._get_id_for_entity(entity)
14961509
if entity_id:
14971510
return SketchConstraint(
1511+
id=self._generate_constraint_id(),
14981512
constraint_type=ConstraintType.FIXED,
14991513
references=[entity_id]
15001514
)
@@ -1510,6 +1524,7 @@ def _convert_symmetric(self, constraint) -> SketchConstraint | None:
15101524
line_id = self._get_id_for_entity(line)
15111525
if id1 and id2 and line_id:
15121526
return SketchConstraint(
1527+
id=self._generate_constraint_id(),
15131528
constraint_type=ConstraintType.SYMMETRIC,
15141529
references=[id1, id2, line_id]
15151530
)
@@ -1524,6 +1539,7 @@ def _convert_midpoint(self, constraint) -> SketchConstraint | None:
15241539
if point_id and line_id:
15251540
ref = self._point_to_ref(point, point_id)
15261541
return SketchConstraint(
1542+
id=self._generate_constraint_id(),
15271543
constraint_type=ConstraintType.MIDPOINT,
15281544
references=[ref, line_id]
15291545
)
@@ -1556,6 +1572,8 @@ def _convert_dimensional_constraint(self, dim) -> SketchConstraint | None:
15561572
return self._convert_diameter_dimension(dim, value_mm)
15571573
elif "SketchAngularDimension" in obj_type:
15581574
return self._convert_angular_dimension(dim)
1575+
elif "SketchOffsetDimension" in obj_type:
1576+
return self._convert_offset_dimension(dim, value_mm)
15591577
else:
15601578
return None
15611579
except Exception:
@@ -1580,18 +1598,21 @@ def _convert_linear_dimension(self, dim, value: float) -> SketchConstraint | Non
15801598
# Check orientation for X/Y constraints
15811599
if orientation == self._adsk_fusion.DimensionOrientations.HorizontalDimensionOrientation:
15821600
return SketchConstraint(
1601+
id=self._generate_constraint_id(),
15831602
constraint_type=ConstraintType.DISTANCE_X,
15841603
references=[ref1, ref2],
15851604
value=value
15861605
)
15871606
elif orientation == self._adsk_fusion.DimensionOrientations.VerticalDimensionOrientation:
15881607
return SketchConstraint(
1608+
id=self._generate_constraint_id(),
15891609
constraint_type=ConstraintType.DISTANCE_Y,
15901610
references=[ref1, ref2],
15911611
value=value
15921612
)
15931613
else:
15941614
return SketchConstraint(
1615+
id=self._generate_constraint_id(),
15951616
constraint_type=ConstraintType.DISTANCE,
15961617
references=[ref1, ref2],
15971618
value=value
@@ -1601,6 +1622,7 @@ def _convert_linear_dimension(self, dim, value: float) -> SketchConstraint | Non
16011622
entity_id = self._get_id_for_entity(entity1)
16021623
if entity_id:
16031624
return SketchConstraint(
1625+
id=self._generate_constraint_id(),
16041626
constraint_type=ConstraintType.LENGTH,
16051627
references=[entity_id],
16061628
value=value
@@ -1614,6 +1636,7 @@ def _convert_radial_dimension(self, dim, value: float) -> SketchConstraint | Non
16141636
entity_id = self._get_id_for_entity(entity)
16151637
if entity_id:
16161638
return SketchConstraint(
1639+
id=self._generate_constraint_id(),
16171640
constraint_type=ConstraintType.RADIUS,
16181641
references=[entity_id],
16191642
value=value
@@ -1626,6 +1649,7 @@ def _convert_diameter_dimension(self, dim, value: float) -> SketchConstraint | N
16261649
entity_id = self._get_id_for_entity(entity)
16271650
if entity_id:
16281651
return SketchConstraint(
1652+
id=self._generate_constraint_id(),
16291653
constraint_type=ConstraintType.DIAMETER,
16301654
references=[entity_id],
16311655
value=value
@@ -1645,8 +1669,46 @@ def _convert_angular_dimension(self, dim) -> SketchConstraint | None:
16451669

16461670
if id1 and id2:
16471671
return SketchConstraint(
1672+
id=self._generate_constraint_id(),
16481673
constraint_type=ConstraintType.ANGLE,
16491674
references=[id1, id2],
16501675
value=value_deg
16511676
)
16521677
return None
1678+
1679+
def _convert_offset_dimension(self, dim, value: float) -> SketchConstraint | None:
1680+
"""Convert an offset dimension constraint (distance from origin to line)."""
1681+
# SketchOffsetDimension uses .line property, not .entity
1682+
entity = dim.line
1683+
entity_id = self._get_id_for_entity(entity)
1684+
1685+
if entity_id:
1686+
# Offset dimensions are typically from origin to a line
1687+
# Use the isHorizontal property to determine constraint type
1688+
try:
1689+
is_horizontal = dim.isHorizontal
1690+
if is_horizontal:
1691+
# Horizontal offset means vertical distance (Y constraint)
1692+
return SketchConstraint(
1693+
id=self._generate_constraint_id(),
1694+
constraint_type=ConstraintType.DISTANCE_Y,
1695+
references=[entity_id],
1696+
value=value
1697+
)
1698+
else:
1699+
# Vertical offset means horizontal distance (X constraint)
1700+
return SketchConstraint(
1701+
id=self._generate_constraint_id(),
1702+
constraint_type=ConstraintType.DISTANCE_X,
1703+
references=[entity_id],
1704+
value=value
1705+
)
1706+
except AttributeError:
1707+
# If isHorizontal not available, use generic distance
1708+
return SketchConstraint(
1709+
id=self._generate_constraint_id(),
1710+
constraint_type=ConstraintType.DISTANCE,
1711+
references=[entity_id],
1712+
value=value
1713+
)
1714+
return None

sketch_adapter_fusion/server.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,170 @@ def do_open() -> bool:
618618
return _execute_on_main_thread(do_open)
619619

620620

621+
def probe_constraints(sketch_name: str) -> dict:
622+
"""Probe a sketch to find what constraints exist.
623+
624+
DEBUG FUNCTION: This can be removed once constraint export is stable.
625+
"""
626+
if not FUSION_AVAILABLE:
627+
raise RuntimeError("Fusion 360 is not available")
628+
629+
def do_probe() -> dict:
630+
app = adsk.core.Application.get()
631+
if not app:
632+
return {"error": "Could not get Fusion 360 application"}
633+
634+
design = adsk.fusion.Design.cast(app.activeProduct)
635+
if not design:
636+
return {"error": "No active design"}
637+
638+
# Find the sketch
639+
root_comp = design.rootComponent
640+
sketch_obj = None
641+
for i in range(root_comp.sketches.count):
642+
sketch = root_comp.sketches.item(i)
643+
if sketch.name == sketch_name:
644+
sketch_obj = sketch
645+
break
646+
647+
if sketch_obj is None:
648+
return {"error": f"Sketch '{sketch_name}' not found"}
649+
650+
result = {"sketch_name": sketch_name}
651+
652+
# Count geometric constraints
653+
try:
654+
geo_constraints = sketch_obj.geometricConstraints
655+
result["geometric_constraint_count"] = geo_constraints.count
656+
657+
# List constraint types
658+
constraint_types = []
659+
for i in range(geo_constraints.count):
660+
c = geo_constraints.item(i)
661+
constraint_types.append(c.objectType)
662+
result["geometric_constraints"] = constraint_types[:20] # Limit
663+
except Exception as e:
664+
result["geometric_error"] = str(e)
665+
666+
# Count sketch dimensions (dimensional constraints)
667+
try:
668+
dimensions = sketch_obj.sketchDimensions
669+
result["dimension_count"] = dimensions.count
670+
671+
dim_types = []
672+
for i in range(dimensions.count):
673+
d = dimensions.item(i)
674+
dim_types.append(d.objectType)
675+
result["dimensions"] = dim_types[:20]
676+
except Exception as e:
677+
result["dimension_error"] = str(e)
678+
679+
# Check entity mapping - do a full export to populate mappings
680+
try:
681+
adapter = _get_adapter()
682+
adapter._sketch = sketch_obj
683+
684+
# Do the export to populate entity mappings
685+
exported = adapter.export_sketch()
686+
687+
result["entity_to_id_count"] = len(adapter._entity_to_id)
688+
result["exported_primitive_count"] = len(exported.primitives)
689+
result["exported_constraint_count"] = len(exported.constraints)
690+
691+
# Check if constraint entities can be found
692+
geo_constraints = sketch_obj.geometricConstraints
693+
constraint_entity_check = []
694+
for i in range(min(geo_constraints.count, 5)):
695+
c = geo_constraints.item(i)
696+
obj_type = c.objectType
697+
698+
check = {"type": obj_type.split("::")[-1]}
699+
700+
# Try to get the entity and its token
701+
if "HorizontalConstraint" in obj_type or "VerticalConstraint" in obj_type:
702+
try:
703+
line = c.line
704+
token = line.entityToken
705+
check["entity_token"] = token[:50] if token else "None"
706+
check["found_in_mapping"] = token in adapter._entity_to_id
707+
except Exception as e:
708+
check["error"] = str(e)[:50]
709+
710+
constraint_entity_check.append(check)
711+
712+
result["constraint_entity_check"] = constraint_entity_check
713+
714+
# Try to manually convert a constraint to see what fails
715+
if geo_constraints.count > 0:
716+
c = geo_constraints.item(0)
717+
718+
# Check if _get_id_for_entity works
719+
try:
720+
line = c.line
721+
entity_id = adapter._get_id_for_entity(line)
722+
result["get_id_result"] = entity_id if entity_id else "None returned"
723+
except Exception as e:
724+
result["get_id_error"] = str(e)
725+
726+
# Try direct horizontal conversion
727+
try:
728+
from sketch_canonical import SketchConstraint, ConstraintType
729+
line = c.line
730+
entity_id = adapter._get_id_for_entity(line)
731+
result["direct_entity_id"] = entity_id
732+
if entity_id:
733+
sc = SketchConstraint(
734+
constraint_type=ConstraintType.HORIZONTAL,
735+
references=[entity_id]
736+
)
737+
result["direct_constraint"] = str(sc)
738+
except Exception as e:
739+
import traceback
740+
result["direct_convert_error"] = traceback.format_exc()[-500:]
741+
742+
# Try adapter's convert method with exception detail
743+
try:
744+
converted = adapter._convert_horizontal(c)
745+
result["convert_horizontal_result"] = str(converted) if converted else "None"
746+
except Exception as e:
747+
import traceback
748+
result["convert_horizontal_error"] = traceback.format_exc()[-500:]
749+
750+
# Check offset dimensions specifically
751+
dims = sketch_obj.sketchDimensions
752+
offset_dim_info = []
753+
for i in range(dims.count):
754+
dim = dims.item(i)
755+
if "SketchOffsetDimension" in dim.objectType:
756+
dim_info = {"index": i}
757+
try:
758+
# SketchOffsetDimension uses .line property
759+
entity = dim.line
760+
dim_info["entity_type"] = entity.objectType if entity else "None"
761+
if entity:
762+
token = getattr(entity, "entityToken", None)
763+
dim_info["has_token"] = token is not None
764+
dim_info["in_mapping"] = token in adapter._entity_to_id if token else False
765+
entity_id = adapter._get_id_for_entity(entity)
766+
dim_info["entity_id"] = entity_id if entity_id else "None"
767+
dim_info["is_horizontal"] = getattr(dim, "isHorizontal", "N/A")
768+
except Exception as e:
769+
dim_info["error"] = str(e)[:100]
770+
try:
771+
dim_info["value"] = dim.parameter.value * 10 # cm to mm
772+
except:
773+
pass
774+
offset_dim_info.append(dim_info)
775+
result["offset_dimensions"] = offset_dim_info
776+
777+
except Exception as e:
778+
result["adapter_error"] = str(e)
779+
780+
return result
781+
782+
return _execute_on_main_thread(do_probe)
783+
784+
621785
def start_server(
622786
host: str = DEFAULT_HOST, port: int = DEFAULT_PORT, blocking: bool = True
623787
) -> bool:
@@ -660,6 +824,7 @@ def start_server(
660824
_server.register_function(get_status, "get_status")
661825
_server.register_function(ping, "ping")
662826
_server.register_function(open_sketch_in_edit_mode, "open_sketch_in_sketcher")
827+
_server.register_function(probe_constraints, "probe_constraints")
663828

664829
print(f"Fusion 360 sketch server started on {host}:{port}")
665830

0 commit comments

Comments
 (0)