3d-card 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
3d_card-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Konstantinos Georgiou
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
3d_card-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.4
2
+ Name: 3d-card
3
+ Version: 0.1.0
4
+ Summary: Turn a set of 3D-printable parts into one printable card: parts gated to a frame like a model-kit sprue, with a check that every part comes off clean. Includes print-in-place joints and printability checks
5
+ Author: Konstantinos Georgiou
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/drkostas/3d-card
8
+ Keywords: 3d-printing,sprue,kit card,model kit,joints,print-in-place,trimesh,build123d
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: 3d-base>=0.1.0
15
+ Requires-Dist: numpy>=1.24
16
+ Requires-Dist: trimesh>=4.0
17
+ Requires-Dist: manifold3d>=2.4
18
+ Requires-Dist: scipy>=1.10
19
+ Requires-Dist: shapely>=2.0
20
+ Requires-Dist: matplotlib>=3.7
21
+ Requires-Dist: build123d>=0.8
22
+ Requires-Dist: rtree>=1.0
23
+ Requires-Dist: networkx>=3.0
24
+ Provides-Extra: sim
25
+ Requires-Dist: mujoco>=3.0; extra == "sim"
26
+ Requires-Dist: coacd>=1.0; extra == "sim"
27
+ Provides-Extra: test
28
+ Requires-Dist: pytest>=8; extra == "test"
29
+ Dynamic: license-file
30
+
31
+ # 3d-card
32
+
33
+ 3d-card turns a set of 3D-printable parts into one printable card, like the sprue of a plastic model kit. The parts sit on a flat frame, each held by small gates at the bottom, and you print the whole card in one go, cut the parts off and put them together. Before it gives you the card, it cuts every gate itself and checks that every part comes back whole and loose (the round trip).
34
+
35
+ I wrote it for a posable toy figure I designed and printed, and I could not find a public tool that makes this kind of card. It also has the print-in-place joints and the printability checks I needed on the way.
36
+
37
+ ![A made-up kit of a block, a rod on a peg and two wheels, laid out on one card](docs/example.png)
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pip install 3d-card
43
+ pip install "3d-card[sim]" # adds the MuJoCo stand-up test and convex decomposition
44
+ ```
45
+
46
+ In Python it is imported as `card3d`. It uses build123d (the CAD kernel), trimesh and manifold3d for the geometry, and [3d-base](https://github.com/drkostas/3d-base) for mesh loading and pictures.
47
+
48
+ ## Make a card
49
+
50
+ You give it a function that returns your parts, the joints between them and a list of notes.
51
+
52
+ ```python
53
+ import trimesh
54
+ from card3d import tosprue
55
+
56
+ def source():
57
+ parts = {"body": trimesh.load("body.stl"), "arm": trimesh.load("arm.stl"), "wheel": trimesh.load("wheel.stl")}
58
+ joints = [] # optional, see below
59
+ return parts, joints, []
60
+
61
+ got, report = tosprue.best_card(source=source)
62
+ print(got["size"], got["fits_bed"], report["ok"])
63
+ got["card"].export("card.stl")
64
+ ```
65
+
66
+ `best_card` tries a few layouts and keeps the smallest one whose round trip passes. `build` makes one card with the options you pass. `export(out="card", name="card.stl", ...)` builds and writes it.
67
+
68
+ What it does, in order.
69
+
70
+ 1. Each part is turned so it rests on a face with enough first layer. A part with only a few square millimetres on the plate comes loose in the first layers and the nozzle drags it away.
71
+ 2. The parts are laid out in rows, with a runner bar under each row.
72
+ 3. Gates are placed at the bottom of each part, in the frame's own plane, where the part comes closest to its bar. A gate higher up would print in the air.
73
+ 4. Each part gets a small number stamped on the frame next to it.
74
+ 5. The card is made into one solid, the gates are cut, and every piece is matched back to its part by area and size.
75
+
76
+ The card is as tall as its tallest part, and it is checked against your printer's bed (`bed=(width, depth)` in mm, or set `card3d.sprue.BED`).
77
+
78
+ ### A card you can read
79
+
80
+ By default the parts go into one row after another. To make the card read like the finished model (head at the top, legs lower down, and so on), pass the rows yourself.
81
+
82
+ ```python
83
+ tosprue.build(source=source,
84
+ rows=[["head"], ["arm_left", "body", "arm_right"], ["leg_left", "leg_right"]],
85
+ down=["arm_left", "arm_right", "leg_left", "leg_right"], # long axis down the card
86
+ across=["head"]) # long axis across it
87
+ ```
88
+
89
+ Parts you do not name go into a last row. Someone can then put the kit together without instructions.
90
+
91
+ ### Joints
92
+
93
+ `card3d.joints` makes the two halves of a joint (`male` is what sticks out, `female` is what is cut away) for pegs, axles, pivots, ball joints, tabs and cross laps. The clearances were measured on a real printer with the two test prints in `card3d.coupons` (0.30 mm for push fits, 0.20 mm for a pivot that holds its pose). Print the coupons on your own printer and change `joints.CLEAR` and `joints.PIVOT_CLEAR` if yours differ.
94
+
95
+ If you list your joints in the source as `{"type": "peg", "size": 3.0, "axis": (1, 0, 0), "at": (x, y, z), "child": "arm", "parent": "body"}`, the card keeps gates away from them and does not cut a part's sole through a peg.
96
+
97
+ ## Flat cards and checks
98
+
99
+ - `card3d.sprue.make_card` makes the flat version, a kit card of flat plates of one thickness, and `card3d.roundtrip.check` cuts it and checks every plate comes back.
100
+ - `card3d.simulator` reports overhangs and where supports would leave marks, sweeps a peg into its socket to find collisions, finds the best print orientation, and with the `sim` extra decomposes a part into convex pieces and tests in MuJoCo whether the assembled model stands.
101
+ - `card3d.labels` makes small 3D numbers for parts and cards.
102
+
103
+ ## Claude Code skill
104
+
105
+ ```bash
106
+ 3d-card skill # copies it to ~/.claude/skills/3d-card
107
+ ```
108
+
109
+ The skill gives Claude the whole procedure we used, in order. It starts with measuring clearances on your printer and goes through joints, building the card, reading its notes and the round trip, and checking the slice, up to the first physical print. It ends with a table of the failures we met (a card that printed as spaghetti, gates that cut a joint, parts that stayed on the frame) with how to check and fix each one.
110
+
111
+ ## Limits
112
+
113
+ - It was built for one toy with about twenty parts. Cards with many more parts work, but the layout is a row packer, not an optimiser.
114
+ - The round trip proves the gates can be cut in the model. Gate size still depends on your material and cutters, so print one card before printing many.
115
+ - The clearances are from one printer and PLA.
116
+
117
+ ## Development
118
+
119
+ ```bash
120
+ python -m venv .venv && .venv/bin/pip install -e '.[test,sim]'
121
+ .venv/bin/pytest
122
+ python examples/make_example.py # rebuilds docs/example.png
123
+ ```
124
+
125
+ ## License
126
+
127
+ MIT
@@ -0,0 +1,97 @@
1
+ # 3d-card
2
+
3
+ 3d-card turns a set of 3D-printable parts into one printable card, like the sprue of a plastic model kit. The parts sit on a flat frame, each held by small gates at the bottom, and you print the whole card in one go, cut the parts off and put them together. Before it gives you the card, it cuts every gate itself and checks that every part comes back whole and loose (the round trip).
4
+
5
+ I wrote it for a posable toy figure I designed and printed, and I could not find a public tool that makes this kind of card. It also has the print-in-place joints and the printability checks I needed on the way.
6
+
7
+ ![A made-up kit of a block, a rod on a peg and two wheels, laid out on one card](docs/example.png)
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install 3d-card
13
+ pip install "3d-card[sim]" # adds the MuJoCo stand-up test and convex decomposition
14
+ ```
15
+
16
+ In Python it is imported as `card3d`. It uses build123d (the CAD kernel), trimesh and manifold3d for the geometry, and [3d-base](https://github.com/drkostas/3d-base) for mesh loading and pictures.
17
+
18
+ ## Make a card
19
+
20
+ You give it a function that returns your parts, the joints between them and a list of notes.
21
+
22
+ ```python
23
+ import trimesh
24
+ from card3d import tosprue
25
+
26
+ def source():
27
+ parts = {"body": trimesh.load("body.stl"), "arm": trimesh.load("arm.stl"), "wheel": trimesh.load("wheel.stl")}
28
+ joints = [] # optional, see below
29
+ return parts, joints, []
30
+
31
+ got, report = tosprue.best_card(source=source)
32
+ print(got["size"], got["fits_bed"], report["ok"])
33
+ got["card"].export("card.stl")
34
+ ```
35
+
36
+ `best_card` tries a few layouts and keeps the smallest one whose round trip passes. `build` makes one card with the options you pass. `export(out="card", name="card.stl", ...)` builds and writes it.
37
+
38
+ What it does, in order.
39
+
40
+ 1. Each part is turned so it rests on a face with enough first layer. A part with only a few square millimetres on the plate comes loose in the first layers and the nozzle drags it away.
41
+ 2. The parts are laid out in rows, with a runner bar under each row.
42
+ 3. Gates are placed at the bottom of each part, in the frame's own plane, where the part comes closest to its bar. A gate higher up would print in the air.
43
+ 4. Each part gets a small number stamped on the frame next to it.
44
+ 5. The card is made into one solid, the gates are cut, and every piece is matched back to its part by area and size.
45
+
46
+ The card is as tall as its tallest part, and it is checked against your printer's bed (`bed=(width, depth)` in mm, or set `card3d.sprue.BED`).
47
+
48
+ ### A card you can read
49
+
50
+ By default the parts go into one row after another. To make the card read like the finished model (head at the top, legs lower down, and so on), pass the rows yourself.
51
+
52
+ ```python
53
+ tosprue.build(source=source,
54
+ rows=[["head"], ["arm_left", "body", "arm_right"], ["leg_left", "leg_right"]],
55
+ down=["arm_left", "arm_right", "leg_left", "leg_right"], # long axis down the card
56
+ across=["head"]) # long axis across it
57
+ ```
58
+
59
+ Parts you do not name go into a last row. Someone can then put the kit together without instructions.
60
+
61
+ ### Joints
62
+
63
+ `card3d.joints` makes the two halves of a joint (`male` is what sticks out, `female` is what is cut away) for pegs, axles, pivots, ball joints, tabs and cross laps. The clearances were measured on a real printer with the two test prints in `card3d.coupons` (0.30 mm for push fits, 0.20 mm for a pivot that holds its pose). Print the coupons on your own printer and change `joints.CLEAR` and `joints.PIVOT_CLEAR` if yours differ.
64
+
65
+ If you list your joints in the source as `{"type": "peg", "size": 3.0, "axis": (1, 0, 0), "at": (x, y, z), "child": "arm", "parent": "body"}`, the card keeps gates away from them and does not cut a part's sole through a peg.
66
+
67
+ ## Flat cards and checks
68
+
69
+ - `card3d.sprue.make_card` makes the flat version, a kit card of flat plates of one thickness, and `card3d.roundtrip.check` cuts it and checks every plate comes back.
70
+ - `card3d.simulator` reports overhangs and where supports would leave marks, sweeps a peg into its socket to find collisions, finds the best print orientation, and with the `sim` extra decomposes a part into convex pieces and tests in MuJoCo whether the assembled model stands.
71
+ - `card3d.labels` makes small 3D numbers for parts and cards.
72
+
73
+ ## Claude Code skill
74
+
75
+ ```bash
76
+ 3d-card skill # copies it to ~/.claude/skills/3d-card
77
+ ```
78
+
79
+ The skill gives Claude the whole procedure we used, in order. It starts with measuring clearances on your printer and goes through joints, building the card, reading its notes and the round trip, and checking the slice, up to the first physical print. It ends with a table of the failures we met (a card that printed as spaghetti, gates that cut a joint, parts that stayed on the frame) with how to check and fix each one.
80
+
81
+ ## Limits
82
+
83
+ - It was built for one toy with about twenty parts. Cards with many more parts work, but the layout is a row packer, not an optimiser.
84
+ - The round trip proves the gates can be cut in the model. Gate size still depends on your material and cutters, so print one card before printing many.
85
+ - The clearances are from one printer and PLA.
86
+
87
+ ## Development
88
+
89
+ ```bash
90
+ python -m venv .venv && .venv/bin/pip install -e '.[test,sim]'
91
+ .venv/bin/pytest
92
+ python examples/make_example.py # rebuilds docs/example.png
93
+ ```
94
+
95
+ ## License
96
+
97
+ MIT
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "3d-card"
7
+ version = "0.1.0"
8
+ description = "Turn a set of 3D-printable parts into one printable card: parts gated to a frame like a model-kit sprue, with a check that every part comes off clean. Includes print-in-place joints and printability checks"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{ name = "Konstantinos Georgiou" }]
13
+ dependencies = [
14
+ "3d-base>=0.1.0",
15
+ "numpy>=1.24",
16
+ "trimesh>=4.0",
17
+ "manifold3d>=2.4",
18
+ "scipy>=1.10",
19
+ "shapely>=2.0",
20
+ "matplotlib>=3.7",
21
+ "build123d>=0.8",
22
+ "rtree>=1.0",
23
+ "networkx>=3.0",
24
+ ]
25
+ keywords = ["3d-printing", "sprue", "kit card", "model kit", "joints", "print-in-place", "trimesh", "build123d"]
26
+ classifiers = ["Programming Language :: Python :: 3", "Topic :: Multimedia :: Graphics :: 3D Modeling"]
27
+
28
+ [project.urls]
29
+ Homepage = "https://github.com/drkostas/3d-card"
30
+
31
+ [project.scripts]
32
+ 3d-card = "card3d.cli:main"
33
+
34
+ [project.optional-dependencies]
35
+ sim = ["mujoco>=3.0", "coacd>=1.0"]
36
+ test = ["pytest>=8"]
37
+
38
+ [tool.setuptools.packages.find]
39
+ where = ["src"]
40
+
41
+ [tool.setuptools.package-data]
42
+ card3d = ["skill/SKILL.md"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.4
2
+ Name: 3d-card
3
+ Version: 0.1.0
4
+ Summary: Turn a set of 3D-printable parts into one printable card: parts gated to a frame like a model-kit sprue, with a check that every part comes off clean. Includes print-in-place joints and printability checks
5
+ Author: Konstantinos Georgiou
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/drkostas/3d-card
8
+ Keywords: 3d-printing,sprue,kit card,model kit,joints,print-in-place,trimesh,build123d
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: 3d-base>=0.1.0
15
+ Requires-Dist: numpy>=1.24
16
+ Requires-Dist: trimesh>=4.0
17
+ Requires-Dist: manifold3d>=2.4
18
+ Requires-Dist: scipy>=1.10
19
+ Requires-Dist: shapely>=2.0
20
+ Requires-Dist: matplotlib>=3.7
21
+ Requires-Dist: build123d>=0.8
22
+ Requires-Dist: rtree>=1.0
23
+ Requires-Dist: networkx>=3.0
24
+ Provides-Extra: sim
25
+ Requires-Dist: mujoco>=3.0; extra == "sim"
26
+ Requires-Dist: coacd>=1.0; extra == "sim"
27
+ Provides-Extra: test
28
+ Requires-Dist: pytest>=8; extra == "test"
29
+ Dynamic: license-file
30
+
31
+ # 3d-card
32
+
33
+ 3d-card turns a set of 3D-printable parts into one printable card, like the sprue of a plastic model kit. The parts sit on a flat frame, each held by small gates at the bottom, and you print the whole card in one go, cut the parts off and put them together. Before it gives you the card, it cuts every gate itself and checks that every part comes back whole and loose (the round trip).
34
+
35
+ I wrote it for a posable toy figure I designed and printed, and I could not find a public tool that makes this kind of card. It also has the print-in-place joints and the printability checks I needed on the way.
36
+
37
+ ![A made-up kit of a block, a rod on a peg and two wheels, laid out on one card](docs/example.png)
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pip install 3d-card
43
+ pip install "3d-card[sim]" # adds the MuJoCo stand-up test and convex decomposition
44
+ ```
45
+
46
+ In Python it is imported as `card3d`. It uses build123d (the CAD kernel), trimesh and manifold3d for the geometry, and [3d-base](https://github.com/drkostas/3d-base) for mesh loading and pictures.
47
+
48
+ ## Make a card
49
+
50
+ You give it a function that returns your parts, the joints between them and a list of notes.
51
+
52
+ ```python
53
+ import trimesh
54
+ from card3d import tosprue
55
+
56
+ def source():
57
+ parts = {"body": trimesh.load("body.stl"), "arm": trimesh.load("arm.stl"), "wheel": trimesh.load("wheel.stl")}
58
+ joints = [] # optional, see below
59
+ return parts, joints, []
60
+
61
+ got, report = tosprue.best_card(source=source)
62
+ print(got["size"], got["fits_bed"], report["ok"])
63
+ got["card"].export("card.stl")
64
+ ```
65
+
66
+ `best_card` tries a few layouts and keeps the smallest one whose round trip passes. `build` makes one card with the options you pass. `export(out="card", name="card.stl", ...)` builds and writes it.
67
+
68
+ What it does, in order.
69
+
70
+ 1. Each part is turned so it rests on a face with enough first layer. A part with only a few square millimetres on the plate comes loose in the first layers and the nozzle drags it away.
71
+ 2. The parts are laid out in rows, with a runner bar under each row.
72
+ 3. Gates are placed at the bottom of each part, in the frame's own plane, where the part comes closest to its bar. A gate higher up would print in the air.
73
+ 4. Each part gets a small number stamped on the frame next to it.
74
+ 5. The card is made into one solid, the gates are cut, and every piece is matched back to its part by area and size.
75
+
76
+ The card is as tall as its tallest part, and it is checked against your printer's bed (`bed=(width, depth)` in mm, or set `card3d.sprue.BED`).
77
+
78
+ ### A card you can read
79
+
80
+ By default the parts go into one row after another. To make the card read like the finished model (head at the top, legs lower down, and so on), pass the rows yourself.
81
+
82
+ ```python
83
+ tosprue.build(source=source,
84
+ rows=[["head"], ["arm_left", "body", "arm_right"], ["leg_left", "leg_right"]],
85
+ down=["arm_left", "arm_right", "leg_left", "leg_right"], # long axis down the card
86
+ across=["head"]) # long axis across it
87
+ ```
88
+
89
+ Parts you do not name go into a last row. Someone can then put the kit together without instructions.
90
+
91
+ ### Joints
92
+
93
+ `card3d.joints` makes the two halves of a joint (`male` is what sticks out, `female` is what is cut away) for pegs, axles, pivots, ball joints, tabs and cross laps. The clearances were measured on a real printer with the two test prints in `card3d.coupons` (0.30 mm for push fits, 0.20 mm for a pivot that holds its pose). Print the coupons on your own printer and change `joints.CLEAR` and `joints.PIVOT_CLEAR` if yours differ.
94
+
95
+ If you list your joints in the source as `{"type": "peg", "size": 3.0, "axis": (1, 0, 0), "at": (x, y, z), "child": "arm", "parent": "body"}`, the card keeps gates away from them and does not cut a part's sole through a peg.
96
+
97
+ ## Flat cards and checks
98
+
99
+ - `card3d.sprue.make_card` makes the flat version, a kit card of flat plates of one thickness, and `card3d.roundtrip.check` cuts it and checks every plate comes back.
100
+ - `card3d.simulator` reports overhangs and where supports would leave marks, sweeps a peg into its socket to find collisions, finds the best print orientation, and with the `sim` extra decomposes a part into convex pieces and tests in MuJoCo whether the assembled model stands.
101
+ - `card3d.labels` makes small 3D numbers for parts and cards.
102
+
103
+ ## Claude Code skill
104
+
105
+ ```bash
106
+ 3d-card skill # copies it to ~/.claude/skills/3d-card
107
+ ```
108
+
109
+ The skill gives Claude the whole procedure we used, in order. It starts with measuring clearances on your printer and goes through joints, building the card, reading its notes and the round trip, and checking the slice, up to the first physical print. It ends with a table of the failures we met (a card that printed as spaghetti, gates that cut a joint, parts that stayed on the frame) with how to check and fix each one.
110
+
111
+ ## Limits
112
+
113
+ - It was built for one toy with about twenty parts. Cards with many more parts work, but the layout is a row packer, not an optimiser.
114
+ - The round trip proves the gates can be cut in the model. Gate size still depends on your material and cutters, so print one card before printing many.
115
+ - The clearances are from one printer and PLA.
116
+
117
+ ## Development
118
+
119
+ ```bash
120
+ python -m venv .venv && .venv/bin/pip install -e '.[test,sim]'
121
+ .venv/bin/pytest
122
+ python examples/make_example.py # rebuilds docs/example.png
123
+ ```
124
+
125
+ ## License
126
+
127
+ MIT
@@ -0,0 +1,24 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/3d_card.egg-info/PKG-INFO
5
+ src/3d_card.egg-info/SOURCES.txt
6
+ src/3d_card.egg-info/dependency_links.txt
7
+ src/3d_card.egg-info/entry_points.txt
8
+ src/3d_card.egg-info/requires.txt
9
+ src/3d_card.egg-info/top_level.txt
10
+ src/card3d/__init__.py
11
+ src/card3d/cli.py
12
+ src/card3d/joints.py
13
+ src/card3d/labels.py
14
+ src/card3d/roundtrip.py
15
+ src/card3d/simulator.py
16
+ src/card3d/sprue.py
17
+ src/card3d/tosprue.py
18
+ src/card3d/coupons/__init__.py
19
+ src/card3d/coupons/ball_coupon.py
20
+ src/card3d/coupons/fit_coupon.py
21
+ src/card3d/skill/SKILL.md
22
+ tests/test_card.py
23
+ tests/test_cli.py
24
+ tests/test_parts.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ 3d-card = card3d.cli:main
@@ -0,0 +1,17 @@
1
+ 3d-base>=0.1.0
2
+ numpy>=1.24
3
+ trimesh>=4.0
4
+ manifold3d>=2.4
5
+ scipy>=1.10
6
+ shapely>=2.0
7
+ matplotlib>=3.7
8
+ build123d>=0.8
9
+ rtree>=1.0
10
+ networkx>=3.0
11
+
12
+ [sim]
13
+ mujoco>=3.0
14
+ coacd>=1.0
15
+
16
+ [test]
17
+ pytest>=8
@@ -0,0 +1 @@
1
+ card3d
@@ -0,0 +1,2 @@
1
+ """Printable kit cards from 3D parts, with print-in-place joints and printability checks."""
2
+ __version__ = "0.1.0"
@@ -0,0 +1,27 @@
1
+ """3d-card command line."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import shutil
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from . import __version__
10
+
11
+
12
+ def main(argv=None) -> int:
13
+ p = argparse.ArgumentParser(prog="3d-card", description="Printable kit cards from 3D parts.")
14
+ p.add_argument("--version", action="version", version=f"3d-card {__version__}")
15
+ sub = p.add_subparsers(dest="cmd", required=True)
16
+ k = sub.add_parser("skill", help="install the Claude Code skill for making kit cards")
17
+ k.add_argument("--dir", default="~/.claude/skills")
18
+ a = p.parse_args(argv)
19
+ dest = Path(a.dir).expanduser() / "3d-card"
20
+ dest.mkdir(parents=True, exist_ok=True)
21
+ shutil.copyfile(Path(__file__).parent / "skill" / "SKILL.md", dest / "SKILL.md")
22
+ print(f"installed {dest / 'SKILL.md'}")
23
+ return 0
24
+
25
+
26
+ if __name__ == "__main__":
27
+ sys.exit(main())
File without changes
@@ -0,0 +1,110 @@
1
+ """Ball-joint coupon. Checks that a ball and socket assemble, hold, and turn, by measuring them.
2
+
3
+ For each ball size the check builds the socket in a small block and the ball on a stem, using the
4
+ joint geometry in card3d.joints, and measures four things.
5
+
6
+ 1. The socket is one solid with no sealed voids inside it.
7
+ 2. The socket rim is narrower than the ball, so something retains the ball.
8
+ 3. Pulling the ball out of the socket meets interference, so the ball does not fall out.
9
+ 4. The seated ball tilts at least 30 degrees in every direction without clashing.
10
+
11
+ To use it, run `python -m card3d.coupons.ball_coupon`. It reports each size from 3 mm to 6 mm. Then
12
+ print the joints at the sizes you need, push each ball into its socket by hand and test the twist.
13
+ If a ball is too tight or falls out, adjust the clearance in card3d.joints and run the check again.
14
+
15
+ The checks encode the following lessons.
16
+
17
+ - A ball joint for a kit that is printed flat and assembled by hand needs a mouth the ball can be
18
+ pushed through. A cavity whose only opening is a stem bore narrower than the ball cannot be
19
+ assembled.
20
+ - `trimesh.util.concatenate` merges face lists and does not perform a union. Cutters combined that
21
+ way remain overlapping shells, and subtracting them leaves walls where they cross. The socket can
22
+ then contain sealed voids and still report `is_watertight`. Check 1 catches this.
23
+ - A cylindrical mouth through a full spherical cavity cannot retain anything. The cavity is the
24
+ ball plus clearance, so wherever the bore is narrower than the cavity it is also narrower than
25
+ the ball. Check 2 catches this.
26
+ - Retention is not measured on the seated ball. A seated ball should not touch the rim, so zero
27
+ interference there is correct. The test pulls the ball out and measures what it has to pass on
28
+ the way, which is check 3.
29
+ """
30
+ import numpy as np
31
+ import trimesh
32
+
33
+ from card3d import joints as J
34
+
35
+
36
+ def socket_block(size, axis, at):
37
+ """Return a block standing in for the parent part, with just enough material to hold the cup."""
38
+ R = size / 2.0
39
+ c = J.ball_centre(size, axis, at)
40
+ blk = trimesh.creation.cylinder(radius=R + J.BALL_CLEAR + J.BALL_WALL,
41
+ height=(R + J.BALL_CLEAR) * 2 + J.BALL_WALL * 2)
42
+ blk.apply_translation(c)
43
+ return blk.difference(J.female("ball", size, axis, at))
44
+
45
+
46
+ def check(size=5.0, verbose=True):
47
+ """Measure one ball size and return the results, with "ok" true when all four checks pass."""
48
+ axis, at = np.array([0.0, 0.0, 1.0]), np.zeros(3)
49
+ R = size / 2.0
50
+ c = float(J.ball_centre(size, axis, at)[2])
51
+ depth = J.ball_depth(size)
52
+ rim = float(np.sqrt((R + J.BALL_CLEAR) ** 2 - depth ** 2))
53
+
54
+ sock = socket_block(size, axis, at)
55
+ ball = J.male("ball", size, axis, at, backset=3.0)
56
+
57
+ pieces = sock.split(only_watertight=False)
58
+ solids = [p for p in pieces if p.volume > 0]
59
+ voids = [p for p in pieces if p.volume < 0]
60
+
61
+ # 1. the socket must be one solid with nothing sealed inside it
62
+ sound = len(solids) == 1 and not voids
63
+
64
+ # 2. the rim must be narrower than the ball, or nothing holds it in
65
+ grips = rim < R
66
+
67
+ # 3. the ball must resist being pulled out
68
+ peak = 0.0
69
+ for d in np.arange(0.0, R * 1.2, R * 0.12):
70
+ b = ball.copy()
71
+ b.apply_translation([0, 0, -float(d)])
72
+ peak = max(peak, abs(sock.intersection(b).volume))
73
+
74
+ # 4. seated, the ball must turn. Two rotational degrees of freedom are the purpose of a ball joint
75
+ swing = {}
76
+ for deg in (15, 30, 45):
77
+ worst = 0.0
78
+ for az in range(0, 360, 45):
79
+ T = (trimesh.transformations.rotation_matrix(np.radians(az), [0, 0, 1], [0, 0, c])
80
+ @ trimesh.transformations.rotation_matrix(np.radians(deg), [1, 0, 0], [0, 0, c]))
81
+ b = ball.copy()
82
+ b.apply_transform(T)
83
+ worst = max(worst, abs(sock.intersection(b).volume))
84
+ swing[deg] = worst
85
+ free_to = max([d for d, v in swing.items() if v < 1.0], default=0)
86
+
87
+ if verbose:
88
+ print(f" ball {size:.1f} mm, cup reaches {depth:.2f} mm past the equator")
89
+ print(f" socket is one sound solid {'yes' if sound else 'NO'}"
90
+ f" ({len(solids)} solid, {len(voids)} sealed void)")
91
+ print(f" rim {rim:.3f} vs ball radius {R:.2f} "
92
+ f"{'grips by ' + format(R - rim, '.3f') + ' mm/side' if grips else 'DOES NOT GRIP'}")
93
+ print(f" resists being pulled out peak {peak:.3f} mm3"
94
+ f" {'' if peak > 0.05 else ' <- FALLS OUT'}")
95
+ for d, v in swing.items():
96
+ print(f" seated, tilted {d:2d} deg all round clash {v:6.3f} mm3")
97
+ print(f" turns freely to at least {free_to} deg")
98
+ return {"sound": sound, "grips": grips, "retain_mm3": peak,
99
+ "free_deg": free_to, "rim": rim, "ball_r": R,
100
+ "ok": bool(sound and grips and peak > 0.05 and free_to >= 30)}
101
+
102
+
103
+ if __name__ == "__main__":
104
+ print("ball and socket, as it would print and as it would be handled")
105
+ allok = True
106
+ for s in (3.0, 4.0, 5.0, 6.0):
107
+ r = check(s)
108
+ allok = allok and r["ok"]
109
+ print()
110
+ print("EVERY SIZE HOLDS AND TURNS" if allok else "SOME SIZE FAILS, see above")