bschwind / opencascade-rs

Rust bindings to the OpenCascade CAD Kernel
GNU Lesser General Public License v2.1
102 stars 18 forks source link

Support KiCAD import and export #59

Open bschwind opened 1 year ago

bschwind commented 1 year ago

Kicad board file format: https://dev-docs.kicad.org/en/file-formats/sexpr-pcb/

The main things we want:

tuna-f1sh commented 1 year ago

Good points. My initial thought process, considering how I approach this manually:

bschwind commented 1 year ago

Either we try to extract this from STEP or probably better: a flag or board variable

Oh nice, I didn't know about board variables! That seems like it may be the most straightforward approach. If we can set this up so it's more self-contained in the kicad project, I think it introduces less friction than having to export a particular file from kicad every time.

For the mounting holes, I guess we just start with a basic stand-off style extruded from the screw hole to where the courtyard finishes.

Yep, let's start with something simple like that for now.

Here is my recent project with some nice challenging IO...the USB-C connector, plug and SWD header on the top. Not too challenging though.

Oh that's a good sample project, I hadn't even thought of connectors perpendicular to the board but of course those are quite common.

tuna-f1sh commented 1 year ago

I did a bit of a review of any existing KiCad Rust helpers since I think a good first start is to make a box around the board 'Edge.Cuts'. It looks like this:

  (gr_line (start 185 71) (end 151 71)
    (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp 0d0b18b8-29e1-400d-9fe0-0c553320d369))
  (gr_line (start 188.950546 53.08) (end 188.950546 47.524454)
    (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp 19f9a363-e864-4747-9205-86bc18fec056))
  (gr_arc (start 188.675546 47.249454) (mid 188.87 47.33) (end 188.950546 47.524454)
    (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp 27a9dd0c-17b3-494e-8eb1-5591d63b4214))
  (gr_arc (start 188 68) (mid 187.12132 70.12132) (end 185 71)
    (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp 2b4447d8-cc2c-42c4-8c3c-94373f69b331))
  (gr_arc (start 148 41) (mid 148.87868 38.87868) (end 151 38)
    (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp 2c508737-2daa-4bed-82b0-cefc47998f5f))
  (gr_line (start 188 46.973866) (end 188 41)
    (stroke (width 0.1) (type default)) (layer "Edge.Cuts") (tstamp 2fc0c9d8-bd5d-4598-b05c-649a87b15bd8))
...

I'd not heard of S-Expression before but that is what it is. There is a crate specifically for KiCad sexpr that seems quite promising. It's under the org of the promisingly named kicad-rs but seems to be one guy. It lead me to find a more complete file object parser https://github.com/kicad-rs/kicad_files/tree/main/src/symbol - might save making each type? I'm inclined to think since we are only dealing with a few parts of the file maybe just the sexpr parser is helpful.

There is also this crate for S-Expression but from what I read, the Option<> KiCad parsing is messy

I should get chance to try and parse Edge.Cuts tomorrow. Should we make this as a 'kicad' workspace crate? I started that on my local machine just to play with some of these crates.

tuna-f1sh commented 1 year ago

Actually this seems like a good starting point: https://github.com/tachiniererin/kicad-rs/tree/main . It’s using a fork of that lexpr crate https://github.com/tachiniererin/lexpr-rs/tree/kicad_pcb-sexp

bschwind commented 1 year ago

Good finds! serde_kicad_sexpr is one I had starred already from quite awhile ago, I forgot to link it here.

https://github.com/tachiniererin/kicad-rs/tree/main also looks good as a more straightforward approach.

I think we should just try both approaches and see which one ends up being easier/simpler.

To throw a little complication in, I took a look at my keyboard PCB and noticed an item like this:

(footprint "footprints:m2_hex_spacer_passthrough_edge" (layer "F.Cu")
    (tstamp b4dd31d6-cc79-4ad5-8636-5da8aebe3f3d)
    (at 138.125 21.55)
    (property "Sheetfile" "key-ripper.kicad_sch")
    (property "Sheetname" "")
    (path "/398dbff6-894b-4a1a-9bd6-b9b822a75720")
    (attr through_hole)
    (fp_text reference "H13" (at 0.1 3.91 unlocked) (layer "F.SilkS") hide
        (effects (font (size 1 1) (thickness 0.15)))
      (tstamp 2df4343f-334f-47df-8b8c-5a40a413e952)
    )
    (fp_text value "Edge M2 Spacer PassThrough" (at 0 4 unlocked) (layer "F.Fab")
        (effects (font (size 1 1) (thickness 0.15)))
      (tstamp 7479341a-ed18-4ba6-b332-dbc265678dd3)
    )
    (fp_text user "${REFERENCE}" (at 0.1 6.6 unlocked) (layer "F.Fab")
        (effects (font (size 1 1) (thickness 0.15)))
      (tstamp 07a313e6-14b1-4d4b-90ef-bff9df3b5ec9)
    )
    (fp_line (start -2.25 0) (end -2.25 -2.5)
      (stroke (width 0.1) (type solid)) (layer "Edge.Cuts") (tstamp 00897000-2a70-4389-95b6-fe708dc28350))
    (fp_line (start 2.25 0) (end 2.25 -2.5)
      (stroke (width 0.1) (type solid)) (layer "Edge.Cuts") (tstamp 620a80f2-e64d-4258-8999-ebe09546c4e1))
    (fp_arc (start 2.25 0) (mid 0 2.25) (end -2.25 0)
      (stroke (width 0.1) (type solid)) (layer "Edge.Cuts") (tstamp 13eda0d1-6707-40bd-891b-ec9eead7216c))
  )

Footprints can also bring in their own edge cut lines, and apparently they are named fp_line and fp_arc (fp = footprint, gr = graphic?)

The lines there are relative to the footprint location, so we'll have to take the footprint XY position into account as well.

tuna-f1sh commented 1 year ago

@bschwind I did some (very rough) playing with both of those approaches. See the full commit message above for my results. I'm starting to think it only for the sake of KiCad file compatibility (I think 6.0/7.0 have minor but breaking deserializer changes[1]), it might be better to approach this from a .dxf import instead. Maybe then add '.kicad_pcb' down the road. Otherwise we can focus on our own KiCad deserializer?

If you get chance have a play and see what you think. I find it's slow progress trying to debug what value causes a panic when deserializing. If we want to keep the .kicad_pcb import, I think we need to fork https://github.com/kicad-rs/kicad_files and add each part of the file in with unit tests - it has the rest of he geometry etc. but they probably also require modification.

[1] I found (paper "A4") in my 7.0 whilst the test file is (paper A4) for example.

tuna-f1sh commented 1 year ago

I started the fork of kicad_files 🙈 . Getting somewhere with the .kicad_pcb parser. The main oddity is the file layers:

  (layers
    (0 "F.Cu" signal)
    (31 "B.Cu" signal)
    (32 "B.Adhes" user "B.Adhesive")
...
  )

It's the only thing that doesn't seem to adhere to the S-Expression format and has no named key - need to write a custom serializer/deserializer I think. The rest should be quite quick (!) with the existing structs already developed.

I'm mainly referring to the documetation you shared, which does note what I found:

Prior to version 6 of KiCad, strings were only quoted when necessary. Saving an older board file to the latest file format will result in these strings being quoted even though there is no functional change in the board itself.

This is not too bad to fix in the fork - for example I had to edit Paper to contain a String rather than &'de str so the derserializer can fill chars inside the quotes.

bschwind commented 1 year ago

@tuna-f1sh just taking a look at the code now. I'm trying to run it against your minnow board as an example.

Do you think the kicad_sexp code (currently in your branch) won't be viable compared to the kicad_files approach? I see some issues with the current serde_kicad_sexpr::from_str but maybe that has already been abandoned and not worth continuing.

tuna-f1sh commented 1 year ago

Just to be clear, it doesn't work. It's more of a playground seeing what currently exists and whether they can be used to read a file reliably. My conclusion was no and it was going to get messy so I started the approach of adding a PCB struct to kicad_files - currently it only reads footprint and schematic files.

That doesn't read a full file yet either, but is easier to progress by adding tests for each type. The 'layers' break the deserializer because it is a SExpr without a 'identifier'. I spent some time exploring options for deserializing this but think the best is to add something like a IdentlessSExpr to https://github.com/kicad-rs/serde_kicad_sexpr/blob/e06db5b7bf2fd7ef688e16655a1b6e7ae27232bb/src/de/mod.rs#L64

It breaks due to the layers starting with a number. https://github.com/kicad-rs/serde_kicad_sexpr/blob/e06db5b7bf2fd7ef688e16655a1b6e7ae27232bb/src/de/mod.rs#L141C7-L141C7 - I'm not sure whether starting with a number would be enough of a differentiator...

I also feel like there should be a simpler way to approach this with a custom Deserialize, reusing most of the serde_kicad_sexpr deserializers. I have to admit, I find the serde stuff a bit of a mind maze, especially with this S-Expression I've not used before! If you have any ideas let me know.

bschwind commented 1 year ago

Ah yeah the layers issue is what I'm currently running into as well. I'll play around more with the current serde_kicad_sexpr approach to see if I can get something working and report back.

bschwind commented 1 year ago

After looking at this more, I think you're right that serde_kicad_sexpr in its current form won't work, so we would need to modify it. It might not be too much work to do overall though, do you think it would be worth me giving it an attempt? I like the idea of staying in the serde ecosystem, as crazy as some of its code can get. Perhaps we could add a derive-macro attribute to structs which don't expect to have an identifier in their S-expression?

Maybe the best course of action is for you to continue on kicad_files and I'll make an attempt at modifying serde_kicad_sexpr to work with v7 files.