Skip to content
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

Topology traversal selection #1013

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

Jojain
Copy link
Contributor

@Jojain Jojain commented Feb 23, 2022

Hello I've made a minimal working example of a topology traversal capability for CQ. It aims to answer #565 ( and eventually #371 )
Since the design has not been discussed that much in the issues I propose the following design. It it currently limited in capability but if we have an agreement on the design I will add all the needed cases (considering objects connected not necessarily by vertices)

The proposed design would allow to do such thing :
box = cq.Workplane().box(10, 10, 10).faces(">Z").connected("Edge")
Which gives the following result :
image

Or :
box = cq.Workplane().box(10, 10, 10).faces(">Z").connected("Face", include_child_shape=True)
image

Let me know if you have comments or recommandation

minimal working example
@adam-urbanczyk
Copy link
Member

Thanks @Jojain - let me digest this. I must say I planned to implement this in a completely different way - by refactoring of the selectors.

@Jojain
Copy link
Contributor Author

Jojain commented Feb 24, 2022

No problem, there isn't much code and I think it's self explaining enough but if you want me to explain feel free to ask.

The main point of this is that it doesn't restrain selections to be thinning down the numbers of objects on the stack.
With this you can select the top Z face and with chaining operations of selection you can get the bottom Z face.

This is particularly useful on complex model where it is a lot easier for the human brain to think of a succession of simple selection rather than coming up with only one selection call that would target the desired shape.

I would also be happy to know how you would want to refactor selectors to see if I can be of any help !

@snoyer
Copy link

snoyer commented Apr 4, 2022

The main point of this is that it doesn't restrain selections to be thinning down the numbers of objects on the stack. With this you can select the top Z face and with chaining operations of selection you can get the bottom Z face.

Do selectors have to thin down the number of objects, though? Right now they technically do as they are implemented as filters, but is that a design principle or just an implementation limitation/oversight?

Currently thing.edges(selector) tells the selector "I've listed the edges of thing for you, look through them and decide which ones to keep".
If we were willing to make it mean "here's thing, find any of its edges as you see fit" then this connected() feature could be implemented with these new selectors.
box.faces(">Z").connected("Edge") would become box.faces(">Z").edges(Connected()) for example.

Here's a dirty monkey-patched proof-of-concept:

from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, Iterable, Optional, Union

import cadquery as cq
from cadquery import Selector, Shape, Workplane
from topo_explorer import ConnectedShapesExplorer


# new selector API

@dataclass(frozen=True)
class SelectionContext(): #TODO better name?
    workplane: Workplane
    objType: Any
    tag: Optional[str]

    def collectProperty(self, objType:Optional[Any]=None):
        cq_obj = self.workplane._getTagged(self.tag) if self.tag else self.workplane
        return cq_obj._collectProperty(objType if objType else self.objType)

    #TODO additional useful stuff would go here to keep the actual NewSelector class simple/stable

class NewSelector(ABC):
    @abstractmethod
    def select(self, ctx: SelectionContext) -> Iterable[Shape]:
        pass


# monkey-patching it in

_old_selectObjects = Workplane._selectObjects

def _new_selectObjects(
        self: Workplane,
        objType: Any,
        selector: Optional[Union[Selector, str, NewSelector]] = None,
        tag: Optional[str] = None,
    ) -> Workplane:
        if isinstance(selector, NewSelector):
            ctx = SelectionContext(workplane=self, objType=objType, tag=tag)
            return self.newObject(selector.select(ctx))
        else:
            return _old_selectObjects(self, objType, selector, tag)

Workplane._selectObjects = _new_selectObjects


# example usage

class Connected(NewSelector):
    def __init__(self, include_source: bool=False) -> None:
        self.include_child_shape = include_source
    
    def select(self, ctx: SelectionContext) -> Iterable[Shape]:
        types = {
            'Edges': 'Edge',
            'Faces': 'Face',
            #TODO add the rest or change `topo_explorer.ENUM_MAPPING` to match
        }
        solid = ctx.workplane.findSolid()
        for object in ctx.workplane.objects:
            explorer = ConnectedShapesExplorer(solid, object)
            yield from explorer.search(types[ctx.objType], self.include_child_shape)


boxes = cq.Workplane().box(10, 10, 10) - cq.Workplane().box(2, 10, 10)
edges = boxes.faces("<Z").faces('<X').edges(Connected())
faces = boxes.faces("<Z").faces('>X').faces(Connected(include_source=True))

I don't use cadquery enough to know if this is the one true way to refactor selectors but something along those lines would be an improvement for topology-based use cases.

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