GithubHelp home page GithubHelp logo

Design Direct API about cadquery HOT 32 OPEN

cadquery avatar cadquery commented on May 13, 2024
Design Direct API

from cadquery.

Comments (32)

dcowden avatar dcowden commented on May 13, 2024

Comment by jmwright
Monday Dec 12, 2016 at 00:11 GMT


@dcowden What's the definition of a "direct API" in this context? Would PythonOCC be considered a direct API?

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by dcowden
Monday Dec 12, 2016 at 00:46 GMT


A direct API is still CQ provided, but not tied into the fluent api stack.

As an example, consider this fluent API code:

facesOfCylinder = Workplane('xy').sketch().circle(2.0).extrude(2.0).faces()

The direct api models the steps as strict inputs and outputs, without the fluent stack to chain things. For example, the direct api for that code might look like this:

ctx = ModellingContext()
p = Workplane('xy')
s = Sketch(p)
s.makeCircle(2.0)
resultingCylinder = ExtrudeOperation(ctx).extrude(sketch=s, distance=1.0)
facesOfCylinder = FaceSelector(resultingCylinder).selectAll()

Of course CQ 2.0 would still provide a fluent api-- but the goal would be for that layer to be strictly implemented in terms of the Direct API. Hopefully the result would be much easier to test and read code.

The resulting code will be much more flexible as well. It should be easier to link new operations into the fluent api, and it should be easier to allow the operations to accept input from external sources. As an example, if an extrudeOperation just needs a sketch object and a distance, it doesnt care where the sketch came from at all.

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by fragmuffin
Saturday Aug 26, 2017 at 19:35 GMT


Yes! I have many +1's for this initiative.

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by dcowden
Saturday Aug 26, 2017 at 20:48 GMT


thanks for commenting! Can you elaborate a bit about your use case? I'd like to learn more about how to organize the direct api to make it nice.

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by jmwright
Saturday Aug 26, 2017 at 21:43 GMT


@fragmuffin Thanks for the feedback. I too am interested in your thoughts. On any new features, the more feedback we can get from the community, the better. We're interested in how people would use the direct API, and how they think it should work.

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by fragmuffin
Sunday Aug 27, 2017 at 13:05 GMT


I'm trying to build something very complex, and I would like to do so by

  • designing it once, and
  • configuring it many times.

Usually this is given the buzz-term "parametric modeling", but in my opinion the only implementation that allows total freedom is in the form of a language, exactly like cadquery; gui's limit flexibility.
I also like the idea of Constructive Solid Geometry (CSG), and cadquery is the best library I've seen that most closely aligns with a capable CAD GUI (FreeCAD) to allow you to verify what you've created (others show you a pretty picture of what you've made, but you can't do much with it)

Just like software development, I'd like to start small, and build up robust, and configuration managed components. Then, to build the entire thing, compile it to output a number of objects:

  • *.stl or *.obj (for export to other displays, such as a threejs web render)
  • *.svg (for web previews)
  • *.dwg (eg: to further process pocket & perimeter tool-paths for fabrication)
  • other data, such as hole locations & diameters (it's python, so anything goes!)

Modelled objects to classes
So I'd like to start this by creating a parts library, and continually add to it.

So for argument's sake, let's say we want to make this pulley into a part that may be used one or a hundred times in a design, each with unique parameters.

Well this is python! simple!, you create a class that holds the design for all pulleys, and each instance of that class is a physical pulley.

But with the current implementation of cadquery, the way to do that would be quite messy, and unintuitive:

import cadquery
from Helpers import show

class Pulley(cadquery.Workplane):
    def __init__(self, radius=20, width=3, wall_height=1, wall_width=1,
                 hole_radius=3.175, key_intrusion=0.92):
        self.radius = float(radius)
        self.width = float(width)
        # ... and so on
        super(Pulley, self).__init__('XY')
        self.build()

    def build(self):
        # note: ignoring the pulley_half concept for now
        self.circle(radius + wall_height).extrude(wall_width) \
            .faces(">Z").workplane() \
            .circle(radius).extrude(width / 2.0)
        # ... and so on, like the linked gist above

    def __copy__(self):
        return self.translate((0, 0, 0))

p = Pulley(width=5)  # a bit thicker than the default
show(p)

but @dcowden 's sample code would yield a much more intuitive and more powerful implementation... which is why I got excited

import cadquery
from Helpers import show
from math import pi

class Pulley(cadquery.Model):
    def __init__(self, radius=20, width=3, wall_height=1, wall_width=1,
                 hole_radius=3.175, key_intrusion=0.92, **kwargs):
        self.radius = float(radius)
        self.width = float(width)
        # ... and so on
        super(Pulley, self).__init__(**kwargs)

    def build(self):
        self.plane = cadquery.Workplane('xy')
        # create keyed hole sketch
        self.hole_sketch = cadquery.Sketch(self.plane)
        # create rolling circumference sketch
        self.rolling_circ_sketch = cadquery.Sketch(self.plane)
        self.rolling_circ_sketch.makeCircle(self.radius)
        # create outer wall sketch
        self.outer_circ_sketch = Sketch(self.plane)
        self.outer_circ_sketch.makeCircle(self.radius + self.wall_height)

        self.extrude(sketch=self.outer_circ_sketch, distance=self.wall_width)
        self.faces(">Z").workplane().extrude(sketch=self.rolling_circ_sketch, distance=self.width)
        self.faces(">Z").workplane().extrude(sketch=self.outer_circ_sketch, distance=self.wall_width)
        self.faces(">Z").workplane().hole(sketch=self.hole_sketch)

p = Pulley(
    width=5,
    origin=(10, 20, 0),  # somewhere else
    rotate=((0, 0, 0), (1, 0, 0), pi/2),  # rotated a bit
)
with open('~/temp/pulley-hole.dwg', 'w') as fh:
    fh.write(p.hole_sketch.dwg_str())
with open('~/temp/pulley-rolling-circumference.dwg', 'w') as fh:
    fh.write(p.rolling_cirk_sketch.dwg_str())
show(p)

I've embellished a bit here; in the above example I'm asuming ExtrudeOperation.extrude function might return a class instance called cadquery.Model (perhaps)
And I rushed toward the end, but the idea would be to have all of this behind a library... maybe cqparts:

import cqparts
show(cqparts.Pulley(width=5))
show(cqparts.bolts.Bolt(thread='M6', length=20, type='cup_head'), origin=(10, 20, 30))
show(cqparts.nuts.Nut(thread='M6', origin=(10, 20 15)))

Anyway!, I've rambled a bit here, but I think you get the idea... I'd love to see this library able to go to extremes... build a whole car!, that sort of thing =).

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by dcowden
Monday Aug 28, 2017 at 00:37 GMT


@fragmuffin have a look at this example from CQ 2.0 and see what you think:

https://github.com/dcowden/cadquery/blob/2_0_branch/examples/direct_api/plate_with_hole.py

The main difference between the simplified examples I gave above and the real CQ 2.0 api is that the direct api contemplates not only performing operations, but performing queries on the result. For example, if you extrude a face into a prism, you probably want to be able to get a reference to the solid and faces that are created, and those that are modified. This is very important because navigating the solid context using selectors is a crucial feature of CQ-- fluent API or not.

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by fragmuffin
Monday Aug 28, 2017 at 03:18 GMT


I think the biggest thing I want from this is to embrace the Object Orientation (OO) of python.

Using OO with actual objects it should be very clear everybody what's happening (see layer_cake example linked below)

Operation clases & instances
in the first block, when creating the rectangle shape, the operation appears to take the role of a factory but only to create a single shape.

Could this code be simplified to this?

#make a rectangle
wire = RectangleWire(
    mincorner=Vector(0, 0, 0),
    maxcorner=Vector(10, 10, 0),
)
rectangle1 = wire.build_shape(name='rect1')  # a list of CreatedShape object
wire.maxcorner += Vector(1, 1, 0) # moves corner out to (11, 11, 0)
rectangle2 = wire.build_shape(name='rect2')

the RectangleWire instance can be re-used, or modified....
I guess this is very similar to what you've written, main difference being, factories usually don't retain references to the things they've created, so they may be created, used, then garbage-collected by whatever requested the instance of the factory.

Remove unclear alterations of models

Something very prevalent in the current API is models changing when it's very unclear in the code.

This example builds a 3 layered circular "cake" (3 layers) gist

import cadquery
from Helpers import show
base = cadquery.Workplane('XY').circle(30).extrude(10)
cake = base.faces(">Z").workplane() \
    .circle(20).extrude(10) \
    .faces(">Z").workplane() \
    .circle(10).extrude(10)
show(base)
show(cake, (200, 200, 200, 0.8))

when displayed, we see that:

  • cake is indeed the 3 layered cake 👍
  • base is not just the first cylinder 👎
  • base is not the same as cake 👎
  • base is actually the first 2 cylinders, but missing the 3rd. 😢

obviously this leads to a lot of trial and error when creating something much more complex (before being used to the languages's nuances)

so could this cut code be equally represented as this?

precut_volumes = (created_solids.volume(), created_holes.volume())
drilled_part = Cut_Operation('drill-hole', created_solids, created_holes).perform()
assert (created_solids.volume(), created_holes.volume()) == precut_volumes, "oops: solids were modified, so cannot be reused"
# note: I'm not implying the `volume()` method exists, it's just to illustrate what I mean

or perhaps

drilled_part = created_solids.cut(created_holes)

# to alter te original
created_solids = created_solids.cut(created_holes)
# or if duplicating parts to cut away is too resource hungry, equivalent:
created_solids.cut(created_holes, change_base=True)  # returns self

Helping?

Is this helping?, or am I just complaining (you can tell me if I'm complaining 😜 )

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by dcowden
Monday Aug 28, 2017 at 10:52 GMT


Thanks for this feedback!

Yes, with the fluent api, there are necessarily some assumptions and state
changes... I think this is unavoidable in this type of API. In some cases,
you want the new variable to be updated to reflect your modifications, and
sometimes you don't. The think the current fluent api is confusing because it is not explicit enough about the user's selection of the scope of each operation, as it relates to the modelling context.

For example, suppose you do this:

base = Workplane('xy').circle(20).extrude(1) top_of_base = base.workplane().circle(10).extrude(1)

You could plausibly want top_of_base to be only the new layer, OR you could want it to refer to the unioned result. You could also conceivably want the base variable to refer to only the original base, as it was before the creation, or the unioned result. cadquery does what many cad packages do by default-- combining things assuming you don't want to deal with all of those intermediate copies, and eliminates the need to manually fuse all of the items together. This is probably what you wanted to do nearly all of the time. But this implicit behavior, which is confusing.

Making the api support both possible ways of working becomes quite a challenge. The current api does it by providing the 'combine=True|False' option, that allows the user to select which-- but its easy to miss.

Regarding the factory, yes, currently operations are designed as single use
factories, not re usable ones. This is primarily because the operation name
itself is later used as a way to select features by the name of the
operation that created them, and the resulting metadata is extremely
nontrivial. If we want a stateless factory here, we would need to return
not just a list of created objects, but also those which were deleted, and
those which were modified. You could argue that the success and errors
could raise an exception.

I suppose an API like that is possible, but it becomes more like returning
back result object not a simple list of shapes.

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by fragmuffin
Monday Aug 28, 2017 at 11:38 GMT


That's ok!, no problem.

I'm looking at the implementation from the top down, so I'm typically going to ask for magic 😉 .

I've started a cqparts library, it's just a concept at the moment, and I don't know how far it'll go, but it's shiny to me at the moment ✨

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by dcowden
Monday Aug 28, 2017 at 12:21 GMT


I certainly appreciate it! More views of what would be perfect is precisely
what we need!

I would be especially interested in 'real' objects you are making and the
associated code and pseudocode. I certainly agree that the power lies in a
huge library of parts that can be re used.

One question you might contemplate along those lines is how different
modules should interface when it comes to references to other geometry.

For example, it's easy to imagine how a pulley object works... But how do
we control where in 3d space it is created? Essentially we end up with the
program equivalent of solidworks mating relationships... Likely, I want to
be able to drop in this object and have it's azis aligned with another axis
in the model.

Another example is how to deal with changes to existing objects. For
example, suppose I have a module that does counter bored holes... I need to
accept some points, and, most importantly, a solid object to drill into.
The target object will be modified, which means:

(1) the user will probably expect that references to the original object
now refer to the changes object. Is that what we want?

(2) how do we accept a reference to the solid we want to modify, without
exposing details that are specific to our underlying implementation? I, we
don't want people passing in an OCC solid or a freecad solid

For the notion of re-usable code to work at scale, the interface design is
key.

What are your thoughts?

On Aug 28, 2017 7:38 AM, "Peter Boin" [email protected] wrote:

That's ok!, no problem.

I'm looking at the implementation from the top down, so I'm typically
going to ask for magic 😉 .

I've started a cqparts https://github.com/fragmuffin/cqparts library,
it's just a concept at the moment, and I don't know how far it'll go, but
it's shiny to me at the moment ✨


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
dcowden/cadquery#167 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/ABPOAzzfpJU1wKDZSMnbOU13sjQ0A7Slks5scqa-gaJpZM4LKCOZ
.

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by fragmuffin
Monday Aug 28, 2017 at 14:13 GMT


@dcowden
Another example could be a plate with a screw or bolt through it...

Operations instantiated from parts
The pulley would have an associated axis with its definition (like you said, to align with another). But the bolt may have an associated cut operation instance, ready to be applied to the plate.
That way, if you wanted to replace a bolt with a countersunk screw, the cut shape would change.
Similarly, whatever the plate is being bolted to would have a smaller hole, for the screw thread to bite into... So a screw would actually have 2 cut operation instances.

Other types
It's difficult to tell what may be needed, but perhaps that's up to the author of each part... The better a part is made, the more usable it is.

Parts Library
I'm writing cqparts as a parts library basis (abstract) that can be used for others to create their own library... perhaps even proprietary parts.
(but also because I don't want to maintain a parts library for a billion boring nuts and bolts 😉 )
It's best explained in the readme

(1) the user will probably expect that references to the original object
now refer to the changes object. Is that what we want?

I think the main thing to aim for is clarity.
I'd expect that with a library like this, the biggest factor for it being adopted is usability, and usability is subjectively determined by the user.
So ultimately, of the user expects it, then that's what they should get... But that can also be changed by the wording of the API... For example.

part1 = union(cube, sphere)
part2 = copy(cube)
part2.add_union(sphere)
# part1 == part2

Also: what are your thoughts on operator overriding?

part1 = cube | sphere
part2 = copy(cube)
part2 |= sphere

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by dcowden
Wednesday Aug 30, 2017 at 11:46 GMT


I'm not a fan of operator overriding for this use case. The number of things times you perform the very basic boolean operations is small, but the readability impact or a novice is large. It doesn't seem worth it to me.

That said it's also a decision that can be deferred. Clearly even if it is supported, it would be done in terms of more verbose functions, so adding the overloads as shorcuts would trivial

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by dcowden
Wednesday Aug 30, 2017 at 11:56 GMT


@fragmuffin in your above code, if we were to add this code after line 1:

part3= cut_hole(part1,....)

Part3 clearly is part 1 but with some kind of hole in it. But what is part1? Was it modified by side affect, or was a copy implicitly created?

Or are you suggesting there would be both make_hole and make_hole_with_copy?

Today, cq assumes in this case that you want to modify the original, and that you want part1 to refer to the same object as part3 in this case.

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by fragmuffin
Wednesday Aug 30, 2017 at 17:48 GMT


Fair enough with operator overriding.

I think I would assume (write or wrong) that part3 = copy(part1), and then part3 is modified with the cut operation.

# create new part with part1 as the template
part3 = cut_hole(part1, hole_obj)
# functionally the same...
part3 = copy(part1) # currently: part1.translate((0, 0, 0))
part3.cut_hole(hole_obj)

make_hole and make_hole_with_copy?...
no, that would have to be done for every operation, very messy...

I can see why self needs to be returned by most class functions, because the vision of this cadquery is to make coding geometric parts similar to the way you'd describe them.

you know what!... I feel like all of this would be made simpler if the __copy__ was implemented for CQ objects.

def __copy__(self):
    return self.translate((0, 0, 0))

I've just been playing around with:

import cadquery as cq

def copy(obj):
    # should be: from copy import copy
    return obj.translate((0, 0, 0))

template = cq.Workplane('XY').box(50, 50, 2, )
bolt = cq.Workplane('XY', origin=(0,0,-10)).circle(1.5).extrude(20)

part1 = copy(template)
part1.cut(bolt)
part2 = copy(template)
part2.cut(bolt.translate((10, 10, 0)))
# it works!

both parts have a single hole in them... but I can only do this if I explicitly create a copy of the template object first.

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by dcowden
Wednesday Aug 30, 2017 at 21:44 GMT


The copy idea is a good one!

So just to make sure I understand, this code ( a very slight change to yours above) would create a part with two holes in it:

def copy(obj):
    # should be: from copy import copy
    return obj.translate((0, 0, 0))

template = cq.Workplane('XY').box(50, 50, 2, )
bolt = cq.Workplane('XY', origin=(0,0,-10)).circle(1.5).extrude(20)

part1 = copy(template)
part1.cut(bolt)
part1.cut(bolt.translate((10, 10, 0)))

is that right?

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by fragmuffin
Thursday Aug 31, 2017 at 05:45 GMT


yes, it does, and that's what I'd expect.

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by dcowden
Thursday Aug 31, 2017 at 10:57 GMT


@fragmuffin ok thanks I understand the semantics you expect, this has been quite helpful.

It's non trivial to achieve the semantics above in complex cases. The main reason is that in the modelling layer, most operations produce new objects, rather than the original. Cadquery currently uses some trickery to ensure that old references to the updated geometry as you expect in this last case. It can be even more complex than it appears, because objects previously defined can sometimes need to be changed, even when they were not part of the current operation.

It's done in cadquery today through the use of a shared reference to a single modelling context.

Consider this example:

template = cq.Workplane('XY').box(50, 50, 2, )
bolt = cq.Workplane('XY', origin=(0,0,-10)).circle(1.5).extrude(20)
top_face=template.faces(">Z")
part1.cut(bolt)
part1.cut(bolt.translate((10, 10, 0)))

Conceptually, top_face refers to a face who's geometry changed in the cut operation. Technically, the two objects are completely separate. There are many other cases in which the top_face object ceased to exist entirely.

The philosophical question i've been struggling with is whether dealing with these types of issues belong in the direct api or not. I think they do, because even when you use the direct api, you still care about how this stuff works. Handling the reference issues like these is why the operations in the direct api cannot be completely stateless.

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by fragmuffin
Thursday Aug 31, 2017 at 13:37 GMT


@dcowden
As I write these opinions I'm still learning the language... I feel I'm only beginning to understand the complexities you describe, which gives me new appreciation for the cadquery and OpenCascade framework.

As for your philosophical question, I don't know which way you should go...
I do like transparency, but not at the cost of usability; if an API is too complicated, I find myself wondering if I need it at all (in this case, opting to just use OpenCASCADE directly).

Semi-related: I'm using the Tower Pro MG90S micro servo as a bit of a case-study for the cqparts lib I'm toying with at the moment... so with that experience, I may have something of more value to say soon ;)

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by dcowden
Thursday Aug 31, 2017 at 19:46 GMT


@fragmuffin understood, thanks! I'm enjoying the interchange, its actually really useful to have your perspective. Mine is permanently confused and influenced by the details.

I've also done a lot of development with Onshape Featurescript, an odd proprietary language that allows automating things in OnShape.

Though i'm not overall a huge fan of FeatureScript, they do have an opinionated approach to this problem. In Featurescript, you're essentially not allowed to have variables that refer to model geometry. When you want to refer to a model thing ( a face, a solid, an edge), you are required to build what's called a query, which runs and selects geometry. Its basically a selection function.

Operations never accept faces, solids, or other geometry-- they always accept queries. Pseudocode would look like:

cq.Workplane('XY').box(name='box' 50, 50, 2, )
cq.Workplane('XY', origin=(0,0,-10)).circle(1.5).extrude(name='bolt',20)
box=query(createdBy='box', type='Solid') #select all solids created by the box operation 
bolt= query(createdBy='bolt', type='Solid') #select all solids created by the bolt operation
cut(box, bolt)

Because the cut operation accepts queries, not actual object references, and because the box and bolt variables are queries, not actual objects, we avoid the confusion of what gets changed. Since you cannot ever have a variable that stores a face or other geometric item that can be out of date, its very clear what happens every time. The other advantage is that because all operations need queries as input, it tends to mean that the ability to run queries to select geometry is front-and-center. That's good, because as you make complex objects, the ability to select features on existing objects becomes key.

The negative ( a HUGE one) is that its extremely confusing and hard to learn and use. Its extremely non-intuitive to figure out how to select the geometry you just made in order to do the next step. But once you do it, the operations themselves are much easier to write.

It would be nice to get the benefits of both approaches, but I've not figured out how to do that.

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by fragmuffin
Monday Sep 04, 2017 at 06:42 GMT


@dcowden
I'm glad I can help... I understand what it's like to design something on your own, having somebody to bounce ideas off is always helpful.

Query vs Instance, like Database ORM's?
What you describe sounds a lot like an ORM used by any scaleable web framework. I'm most familiar with django.

In django a Model is a class-based definition of a database table (a crude explanation, but it'll do for now). A query is built into a QuerySet instance that knows everything needed to create an SQL query (or potentially other languages) to send to the database server.
Instantiating a QuerySet does not automatically touch the database, rather iterating through it (among other commands) causes the fetch of database results.

>>> Person.objects.filter(name='bob')  # returns a QuerySet
>>> print(Person.objects.filter(name='bob').query)
SELECT "people_person"."id", "people_person"."name", "people_person"."age" FROM "people_person" WHERE "people_person"."name" = bob

>>> # The database has not been queried yet... the following send SQL to the db service
>>> Person.objects.filter(name='bob').first()  # returns an instance of Person (if anybody named 'bob' exists)
<Person: bob>
>>> Person.objects.filter(name='bob').count()  # changes underlying query to exploit SQL's COUNT function before the query is sent.
42

>>> # Writing to the database
>>> p = Person(name='billy')
>>> p.save()  # there are more elegant ways to do this, but again, this is just illustrative

The distinction between a QuerySet and an iterable list of Model instances can be confusing to beginners, but when you get used to it, the distinction is not only easy to identify, but can be powerfully exploited.
This way you get both; the query pointing to the object, and the object itself.

Have you used django (or something similar)?

Encouraging Bad Code?
Similarly to django, this may encourage developers to simply work with object instances over their queries because it's conceptually simpler, but I'd argue that that's not always a bad thing.
Something that comes into existence, beyond the inception phase (that may not have existed otherwise) can always be optimised by a more experienced developer. It encourages growth.

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by dcowden
Monday Sep 04, 2017 at 12:50 GMT


@fragmuffin
Yes I'm reasonably familiar with Django, I have used it for a project or two

You are right that this is a similar problem. The analogy holds well for queries, but not as well for models.

I'd argue that the relationship we have here is more like database and SQL itself. On the query side it's just as you have described. But on the update side, it's more like directly running an update statement. You can't really cleanly just update a single instance. You can perform operations, which could affect one or more rows, and to see what happened you have to run another query.

I'm going to refresh myself on Django orm, and then I will post some proposals for how a direct API might work, which combines the thoughts in this thread and what I have already.

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by dcowden
Monday Sep 04, 2017 at 20:47 GMT


@fragmuffin @jmwright
This is definitely not anything more than a start, but see what you think about this document:

https://github.com/dcowden/cadquery/blob/2_0_occ/doc/directapi.md

Here, i've tried to give some example code and concepts to keep the discussion going.
I've decided that the api layer exposed to the user ( which would include both a direct api and a fluent api) cannot be the same as the lower level Operation interface. This is mainly driven by the need to handle references to objects that could be changing via the use of queries.

The way of working may appear to be overdesigned, but it think the layers are necessary because the underlying CAD system always creates copies of objects when they are changed. IE-- at the CAD kernel layer, there is no such thing as a 'modified' object. There are only new ones. In order to allow a user to store a reference to a shape with is later modified, but still have it point to the same shape, we must create a cross-reference to update all of the references in memory. This magic is done with the query layer, through the use of a shared context ( which can track all of the objects created)

Let me know what you think!

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by fragmuffin
Wednesday Sep 06, 2017 at 05:18 GMT


@dcowden
I like it!, just a few questions

Queries & Evaluation

At the end of the file, would the following be true?

assert half_box.modified().solids() is created_solids
assert created_solids.evaluate() != list_of_solids

if so, perhaps the list should actually be a set, since I don't think the order the solids are listed would matter, and the same solid can't be in the list twice.

Custom Operations

Can users of the API make their own operations? eg:

# with a function
def cut_rect_thru(dapi, solids_query, length, width):
    # cut keyhole through object's center along Z axis
    for solid in faces_query.evaluate():
        tool = dapi.box(solid.center, length, width, 999)
        solid = dapi.subtract(solid, tool, update=True)
        solid.cut(tool)

# or inheritance
class MyAPI(cadquery.DirectAPI):
    def cut_rect_thru(self, solids_query, length, width):
        pass # as above

# or by registration
@cadquery.operation("my_op")
class MyOperation(cadquery.Operation):
    def __call__(self, solid, p1, p2):
        pass  # do stuff?

solid1 = dapi.my_op(solid2, param1, param2)  # or whatever

I'm toying with the idea of a list of operations that can embed an appropriate screw drive pattern into a face... similar to cskHole but with specific shapes.
At the moment it's very crude, I think I'll wait for this API before releasing cqparts

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by dcowden
Wednesday Sep 06, 2017 at 15:55 GMT


@fragmuffin thanks for the comments. You are right about the ability to do custom operations. its very important. In my experience , re-usable operations are actually far more valuable than re-usable parts. I definitely need to give more thought to how new operations are added, so that its very easy and clear.

The main value of the library will eventually come from a large collection of shared operations ( boss, rib, CSK hole, imperial/metric sandard holes and taps, mold cavities, flattened sheet metal, screw threads, etc. It needs to be very easy to collect these, and, going a step further, include ones others have written into your installation.

So we need to support two use cases:
(1) as a user, I can author my own operation, which can be added into the direct api,
(2) as a user, i can include custom operations from other users without modifying my source code

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by dcowden
Thursday Sep 07, 2017 at 21:35 GMT


@fragmuffin yes i think the statements you have listed would be true. In both cases, you might get a list of objects, and the entries may be the same, but the list object itself would be different, since it will be newly created. ( IE, evaluate() will always create a new list, but if no change have happened, the contents will be the same )

Still thinkin/working on custom operations

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by dcowden
Thursday Sep 07, 2017 at 21:41 GMT


@fragmuffin @jmwright what plugin mechanism have you seen done in python that you like the best?

i think custom operations need to be easy to add-- even when the source is not yours. I feel like i just want a user to be able to download a pckage that has some operations or point to a url, and have them 'just available'.
In Java land, we'd do this by using annotations and then scanning for classes that advertise that they are plugins, but i'm not familiar with how to do that in python. I guess thats the last option in @fragmuffin 's suggestions-- but how can you do that when the source is loaded via url or something? seems like something like this would be ideal:

import cadquery as cq
cq.import_operation('https://path/to/some/operation')
#now operations magially work as if they were direct

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by adam-urbanczyk
Friday Sep 08, 2017 at 16:43 GMT


@dcowden

Some comments/thoughts on this topic:

  • I do like the idea of clearly separating operations, selectors and solids
  • I do not like the idea of queries - for me it seems like a lot of abstraction with no clear benefit/purpose. I'd be much more happy with the current string based selector syntax
  • I like the idea of splitting operations (e.g. cut) and solids/workplanes. I do not understand why you propose an implicit Perform(). I'd prefer to get a solid as a return value of a cut operation
  • Maybe operations could be methods of the context. This way it would natural to log them into some DAG-like structure (feature tree I guess). This would be nice to have, but might get complicated.
  • With that being said, as a user I prefer to do my bookkeeping explicitly by hand. I.e. if I need an intermediate object I'll keep a reference to it in my code.
  • It would be nice to make an explicit Sketch class that can be reused (e.g. mapped to different faces) and eventually will support constraints
  • Good to keep extensibility in mind, like @fragmuffin mentioned
  • I think it is also essential to not forget about current users and not make any changes to the current (fluent) API

Some pseudocode to illustrate:

import cq.dapi as cq

ctx = cq.context()

s1 = ctx.box(1,1,1) #s1 is a Solid

with cq.Sketch(s1.faces('>Z')) as sketch:  # upon exiting the with block constraints are solved
    c1 = sketch.circle(0.5)
    c2 = sketch.circle(0.1)
    sketch.add_constraint(c1,sketch.parent.vertices('>X and >Y'),'coincide')

s2 = ctx.cut_through_all(sketch)  #s2 is a new Solid
s3 = ctx.difference(s1,s2) #I can make yet another solid by e.g. performing subtraction

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by jmwright
Saturday Sep 09, 2017 at 13:08 GMT


I think it is also essential to not forget about current users and not make any changes to the current (fluent) API

This is an important issue for me. Introducing major breaking changes in the wrong way has caused a lot of problems in other open source projects. I think the idea is to keep CQ 1.0 as-is, while still maintaining it and maybe backporting useful features from CQ 2.0 into it.
I'm nervous about splitting the project like that, but CQ 2.0 is probably going to be too different from CQ 1.0 to keep everything on one silo.

from cadquery.

dcowden avatar dcowden commented on May 13, 2024

Comment by fragmuffin
Saturday Sep 09, 2017 at 14:32 GMT


@dcowden
I think my favourite mechanism for expansion is registration; the decorator example in my last comment.

an expanded version of that:

#!/usr/bin/python
import six
from collections import defaultdict

# ================= LIBRARY CODE =================
# ---- Registration
registered_operations = defaultdict(dict)
def register(obj_type, name):
    assert issubclass(obj_type, ObjectType), "bad object type: %r" % obj_type
    assert isinstance(name, six.string_types), "bad name: %r" % name
    def inner(cls):
        assert issubclass(cls, Operation), "operation classes must inherit from Operation"
        assert name not in registered_operations, "duplicate operation name '%s': %r:%r" % (
            name, cls, registered_operations[name]
        )
        registered_operations[obj_type][name] = cls
        # option to change class before it's returned.
        # futureproofing:
        # good if you want to make class changes in future versions of cadquery
        return cls
    return inner

# ---- Operation (prototype)
class Operation(object):
    def __init__(self, obj):
        self.obj = obj

    def __call__(self, *largs, **kwargs):
        return self.evaluate(*largs, **kwargs)

    def evaluate(self, *largs, **kwargs):
        raise NotImplemented("evaluate not overridden by %r" % self.__class__)

# ---- Objects (shapes, edges, faces, etc)
class ObjectType(object):
    def __getattr__(self, key):
        if key in registered_operations[self.__class__]:
            op = registered_operations[self.__class__][key](self)
            return op

        if key in self.__dict__:
            return self.__dict__[key]

        raise AttributeError("'{cls}' object has no attribute '{key}'".format(
            cls=self.__class__.__name__,
            key=key
        ))
 
class Shape(ObjectType):
    pass

# ================= USER CODE =================
@register(Shape, 'do_stuff')
class DoStuff(Operation):
    def evaluate(self, value):
        # self.obj will always be a Shape instance
        print("doing stuff to %r, with a value of %s" % (self.obj, value))


s = Shape()
s.do_stuff(9001)

output:

doing stuff to <__main__.Shape object at 0x7f77b27ed790>, with a value of 9001

from cadquery.

s-ol avatar s-ol commented on May 13, 2024

What is the status of this? It seems that on the 2.0 branch there was an effort to have both APIs, but now on 2.1 I cannot find any mention of the direct API.

from cadquery.

jmwright avatar jmwright commented on May 13, 2024

What is the status of this?

It's stalled, but the direct API could still be useful. The original idea was to implement the direct API and then build the CadQuery 2.0 fluent API on top of that. CadQuery didn't evolve to/thru version 2.0 that way though. If there is someone who's motivated to pick this up there's still a path to implementing the direct API in parallel to the fluent API, but it's a lot of work. Once a direct API was in place the fluent API could be migrated to it over time, but again, that's a lot of work and most people are going to want to spend their time on adding features and fixing bugs, not reworking the fluent API to rest on top of the direct API. However, if somebody did want to take it on, I think our team would be supportive.

from cadquery.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.