Skip to content

Add STEP Import for Assemblies #1779

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 51 commits into
base: master
Choose a base branch
from
Open

Add STEP Import for Assemblies #1779

wants to merge 51 commits into from

Conversation

jmwright
Copy link
Member

@jmwright jmwright commented Feb 26, 2025

The goal is to make it possible to round-trip assemblies to and from STEP without loss of data. This data can include:

  • Part location
  • Part color
  • Shape colors
  • Shape colors
  • Shape names (advanced face)

Copy link

codecov bot commented Feb 26, 2025

Codecov Report

❌ Patch coverage is 95.93496% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.84%. Comparing base (8431073) to head (1543908).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
cadquery/occ_impl/importers/assembly.py 95.19% 0 Missing and 5 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1779      +/-   ##
==========================================
+ Coverage   95.66%   95.84%   +0.17%     
==========================================
  Files          28       30       +2     
  Lines        7431     7865     +434     
  Branches     1122     1220      +98     
==========================================
+ Hits         7109     7538     +429     
+ Misses        193      192       -1     
- Partials      129      135       +6     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@jmwright
Copy link
Member Author

@adam-urbanczyk In 4045c58 you set the cadquery.Assembly.importStep method up to be a class method, and you construct assy before calling the lower-level method. However, since the Assembly.name property is private and can only be set during instantiation, this creates an issue for me. I need to be able to set the top-level assembly name based on the name set in the STEP file, which requires me to set the name property after instantiation.

We need to decide on the proper way to fix this.

@adam-urbanczyk
Copy link
Member

Hey, I do not understand the issue. You can always do this:

assy = Assembly()
assy.name = '123'

Do you mean that you need to modify AssemblyProtocol to satisify mypy? That also does not sound like an issue to me.

@jmwright
Copy link
Member Author

Correct, mypy complains. I can make that change to make name a public property if you don't see an issue with doing so.

Copy link
Member

@adam-urbanczyk adam-urbanczyk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, 2nd pass


# Process the color for the shape, which could be of different types
color = Quantity_Color()
cq_color = cq.Color(0.50, 0.50, 0.50)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the magic parameters?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are the RGB values that most of the CQ visualization methods seem to be using as a default with assemblies (gray/silver). The values are not standardized as cadquery.vis.show seems to use a gold color IIRC. CQ-editor uses the gray color though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, default color should be None. See:

class Assembly(object):
    """Nested assembly of Workplane and Shape objects defining their relative positions."""

    loc: Location
    name: str
    color: Optional[Color]
    metadata: Dict[str, Any]

    obj: AssemblyObjects
    parent: Optional["Assembly"]
    children: List["Assembly"]

    objects: Dict[str, "Assembly"]
    constraints: List[Constraint]

loc=cq.Location(parent_location),
)
else:
assy.add(cq.Shape.cast(shape), name=name, color=cq_color)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't this covered. Is parent location not optional?

# Pass down the location or other context as needed
# Add the parent location if it exists
if parent_location is not None:
loc = parent_location * loc
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this suspicious. Assy applies locations in a relative sense. It shouldn't be needed to construct absolute locations.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll have to check, but I think I added this because of nested sub-assemblies

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed. If the parent location is not passed through the call stack, nested subassemblies will break. I added the test_nested_subassembly_step_import test to illustrate this. If you disable parent location passing and run that test, it should fail.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, but that is actually suspicious. When you construct an assy you do not need combine (i.e. multiply) the locations explicitly.

@lorenzncode
Copy link
Member

I am still getting a ValueError with a STEP from the tests.

(cqdev) lorenzn@fedora:~/devel/cadquery$ git status -u no
On branch assembly-import
Your branch is up to date with 'upstream/assembly-import'.

nothing to commit, working tree clean
(cqdev) lorenzn@fedora:~/devel/cadquery$ pytest tests/test_assembly.py --basetemp=./tmp2 -k test_colors_assy1 -q --no-summary
...........                                                                                                                                                        [100%]
11 passed, 96 deselected, 13 warnings in 0.58s
sys:1: DeprecationWarning: builtin type swigvarlink has no __module__ attribute
(cqdev) lorenzn@fedora:~/devel/cadquery$ cat test2.py 
from cadquery import Assembly

obj = Assembly.importStep("./tmp2/assembly10/chassis0_assy.step")
(cqdev) lorenzn@fedora:~/devel/cadquery$ python test2.py 
Traceback (most recent call last):
  File "/home/lorenzn/devel/cadquery/test2.py", line 3, in <module>
    obj = Assembly.importStep("./tmp2/assembly10/chassis0_assy.step")
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/lorenzn/devel/cadquery/cadquery/assembly.py", line 622, in importStep
    _importStep(assy, path)
  File "/home/lorenzn/devel/cadquery/cadquery/occ_impl/importers/assembly.py", line 194, in importStep
    process_label(labels.Value(1))
  File "/home/lorenzn/devel/cadquery/cadquery/occ_impl/importers/assembly.py", line 173, in process_label
    process_label(sub_label, loc, name)
  File "/home/lorenzn/devel/cadquery/cadquery/occ_impl/importers/assembly.py", line 149, in process_label
    process_label(ref_label, parent_location, parent_name)
  File "/home/lorenzn/devel/cadquery/cadquery/occ_impl/importers/assembly.py", line 173, in process_label
    process_label(sub_label, loc, name)
  File "/home/lorenzn/devel/cadquery/cadquery/occ_impl/importers/assembly.py", line 149, in process_label
    process_label(ref_label, parent_location, parent_name)
  File "/home/lorenzn/devel/cadquery/cadquery/occ_impl/importers/assembly.py", line 173, in process_label
    process_label(sub_label, loc, name)
  File "/home/lorenzn/devel/cadquery/cadquery/occ_impl/importers/assembly.py", line 149, in process_label
    process_label(ref_label, parent_location, parent_name)
  File "/home/lorenzn/devel/cadquery/cadquery/occ_impl/importers/assembly.py", line 173, in process_label
    process_label(sub_label, loc, name)
  File "/home/lorenzn/devel/cadquery/cadquery/occ_impl/importers/assembly.py", line 149, in process_label
    process_label(ref_label, parent_location, parent_name)
  File "/home/lorenzn/devel/cadquery/cadquery/occ_impl/importers/assembly.py", line 179, in process_label
    _process_simple_shape(label, parent_location, parent_name)
  File "/home/lorenzn/devel/cadquery/cadquery/occ_impl/importers/assembly.py", line 74, in _process_simple_shape
    assy.add(
  File "/home/lorenzn/devel/cadquery/cadquery/assembly.py", line 241, in add
    self.add(assy)
  File "/home/lorenzn/devel/cadquery/cadquery/assembly.py", line 222, in add
    raise ValueError("Unique name is required")
ValueError: Unique name is required

@adam-urbanczyk
Copy link
Member

@jmwright any updates on this PR?

@jmwright
Copy link
Member Author

@adam-urbanczyk Not yet. I got busy with some other things.

@jmwright
Copy link
Member Author

@adam-urbanczyk I'm getting mypy errors now that I did not before. Most of them are probably my fault, but there is this one about the OpenCASCADE code.

cadquery\occ_impl\importers\assembly.py:105: error: "TDF_Attribute" has no attribute "GetFather"  [attr-defined]

Does this have anything to do with the stubs being published as part of OCP 7.9.x?

@adam-urbanczyk
Copy link
Member

Nothing should be published, but let me check

@jmwright
Copy link
Member Author

jmwright commented Jul 29, 2025

I'm having trouble with mypy. It is giving me different errors locally than I get in CI even though I have the same version. I'll create a new environment from scratch tomorrow and try to match the CI environment to see if I can get rid of the discrepancies.

@jmwright
Copy link
Member Author

@adam-urbanczyk @lorenzncode Please have another look when you get a chance. This PR has been significantly reworked, and a test or two added.

@adam-urbanczyk
Copy link
Member

Something is off with top level location handling.

Before importing:
image

After importing:
image

Code:

#%% imports
from cadquery import Assembly, Location, Color, importers
from cadquery.func import box, rect, sphere
from cadquery.vis import show

#%% prepare the model
def make_model(name: str, COPY: bool):
    b = box(1,1,1)
    
    assy_top = Assembly(name='test_assy_top', loc=Location((0,1,1)))
    assy_top.add(sphere(1))
    
    assy = Assembly(name='test_assy', loc=Location((5,5,1)))
    assy.add(box(1,2,5), color=Color('green'))
    
    
    
    for v in rect(10,10).vertices():
        assy.add(
            b.copy() if COPY else b,
            loc=Location(v.Center()),
            color=Color('red')
        )
        
    # show(assy)
    assy_top.add(assy)
    assy_top.export(name)
    
    return assy_top

assy = make_model("test_assy.step", False)

show(assy, Location(),  scale=5)

#%% import the assy without copies - this throws

assy_i = Assembly.importStep("test_assy.step")
wp = importers.importStep("test_assy.step")

show(assy_i, Location(),scale=5)

@jmwright
Copy link
Member Author

jmwright commented Aug 4, 2025

@adam-urbanczyk Thanks, I fixed that bug and modified one of the tests so that I could add an assert to verify.

Copy link
Member

@lorenzncode lorenzncode left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the big effort @jmwright! The error I had before is resolved now when I try importing from the pytest generated step file.

I find import/export round trip changes the assembly as follows. Is this expected?

from cadquery import Assembly, Location
from cadquery.func import box


def round_trip_test(assy, n=3):
    for i in range(n):
        print(f"\n{i=}")
        for name in assy._flatten().keys():
            print(name)
        assy.export(f"out{i}.step")
        assy = Assembly.importStep(f"out{i}.step")


b = box(1, 1, 1)
assy = Assembly(name="assy")
assy.add(b, name="box0")
assy.add(b, name="box1", loc=Location((1, 0, 0)))

round_trip_test(assy, 5)
i=0
assy/box0
assy/box1
assy

i=1
assy/box0/box0_part
assy/box0
assy/box1/box0_part
assy/box1
assy

i=2
assy/box0/box0_part/box0_part_part
assy/box0/box0_part
assy/box0
assy/box1/box0_part/box0_part_part
assy/box1/box0_part
assy/box1
assy

i=3
assy/box0/box0_part/box0_part_part/box0_part_part_part
assy/box0/box0_part/box0_part_part
assy/box0/box0_part
assy/box0
assy/box1/box0_part/box0_part_part/box0_part_part_part
assy/box1/box0_part/box0_part_part
assy/box1/box0_part
assy/box1
assy

i=4
assy/box0/box0_part/box0_part_part/box0_part_part_part/box0_part_part_part_part
assy/box0/box0_part/box0_part_part/box0_part_part_part
assy/box0/box0_part/box0_part_part
assy/box0/box0_part
assy/box0
assy/box1/box0_part/box0_part_part/box0_part_part_part/box0_part_part_part_part
assy/box1/box0_part/box0_part_part/box0_part_part_part
assy/box1/box0_part/box0_part_part
assy/box1/box0_part
assy/box1
assy

assy = subshape_assy()

# Use a temporary directory
tmpdir = tmp_path_factory.mktemp("out")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, you could use the tmpdir fixture (line 42) to reduce boilerplate.

tmpdir = tmp_path_factory.mktemp("out")
plain_step_path = os.path.join(tmpdir, "plain_assembly_step.step")

# Simple cubes
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are existing assembly fixtures that could be used here (and for other tests) instead. OK if you would rather explicitly redefine here.

@adam-urbanczyk
Copy link
Member

adam-urbanczyk commented Aug 9, 2025

Thanks for the big effort @jmwright! The error I had before is resolved now when I try importing from the pytest generated step file.

I find import/export round trip changes the assembly as follows. Is this expected?

That is definitely not OK.

else:
cq_color = None

new_assy.add(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So probably some logic should be added here to handle the name / name_part pairs. E.g. something like:

if ref_name.endswith('_part') and ref_name.startswith(parent_name):
    new_assy[parent_name].obj = cq_shape
else:
    ...

CC @lorenzncode

assert assy.children[0].name == "cube_1"
assert assy.children[1].children[0].name == "cylinder_1"


@pytest.mark.parametrize(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably a test that asserts idempotence of export/import is also needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants