Compare commits

..

189 Commits

Author SHA1 Message Date
Leni Aniva 851907aa7b
Merge branch 'main' into touhou/houjuu-nue 2024-11-13 22:24:42 -08:00
Leni Aniva 03a8f3b653
feat: Model export 2024-10-21 22:20:29 -07:00
Leni Aniva 114aec4944
fix: Mount output 2024-09-04 16:27:51 -07:00
Leni Aniva 66cdd1b359
feat: Shrink the mount 2024-08-15 11:06:39 -07:00
Leni Aniva 17001f87da
fix: s3 blade geometry 2024-08-12 00:30:14 -07:00
Leni Aniva 363a67841e
fix: Tongue overthick problem 2024-08-07 09:18:23 -07:00
Leni Aniva dfc745617f
fix: Spool obstruction on right 2024-08-07 08:16:44 -07:00
Leni Aniva 749d7ebf42
fix: Axle diameter 2024-08-06 00:09:08 -07:00
Leni Aniva b3db6ab004
fix: Add missing s3 extra output 2024-08-05 11:08:50 -07:00
Leni Aniva 8d613a3d08
feat: Use same tongue thickness for wrist 2024-08-05 09:28:24 -07:00
Leni Aniva b101340a7d
feat: Smaller elbow joints 2024-08-05 09:09:28 -07:00
Leni Aniva 96f546b8b1
feat: Move actuator and controller 2024-08-04 14:35:44 -07:00
Leni Aniva 556a35392d
feat: Truncate inner panel to avoid collision 2024-08-04 14:32:10 -07:00
Leni Aniva c4a5f5770f
feat: Produce outer and inner shell for s0 2024-08-04 14:21:11 -07:00
Leni Aniva 7212a2b0e8
fix: Resolve winch collision 2024-08-04 10:38:50 -07:00
Leni Aniva 8efea1d038
feat: Add arduino uno form factor 2024-08-04 01:03:28 -07:00
Leni Aniva 23ad93c9d4
feat: Make space for shoulder parent joint 2024-08-04 00:25:47 -07:00
Leni Aniva 7c2054e465
fix: Overthick elbow and wrist 2024-08-03 23:56:55 -07:00
Leni Aniva 5eeeace852
fix: Spool obstruction problem, elbow fasteners 2024-08-03 23:31:36 -07:00
Leni Aniva c2161b6171
feat: Move hole position to avoid collision 2024-08-02 01:43:37 -07:00
Leni Aniva 40ebf93dc5
feat: Simplify geometry in electronic mount 2024-08-02 00:28:12 -07:00
Leni Aniva 0ad5b17f07
feat: Use double layer electronic mount 2024-08-02 00:24:25 -07:00
Leni Aniva 2ccb4160db
feat: Use double layer for electronic mount 2024-08-01 22:39:20 -07:00
Leni Aniva 7307ae213d
feat: Move shoulder actuator position 2024-08-01 10:57:43 -07:00
Leni Aniva f1f10369b4
fix: Spool retainment issue in shoulder 2024-07-31 12:42:55 -07:00
Leni Aniva c846c04932
feat: Use a low profile cut to route cables 2024-07-29 10:49:03 -07:00
Leni Aniva f33224c216
feat: Use 10mm actuator on both sides 2024-07-29 10:41:02 -07:00
Leni Aniva 1f99b5e41f
fix: Spring slot in disk 2024-07-27 19:28:42 -07:00
Leni Aniva aa1f4efa98
fix: Brittle shoulder joint problem 2024-07-26 14:07:33 -07:00
Leni Aniva 7fc0499ebe
feat: Add support structure to wing 2024-07-25 22:40:44 -07:00
Leni Aniva 57deefbd5f
fix: Elbow joint output problem 2024-07-25 13:27:25 -07:00
Leni Aniva b7ca30bc28
fix: joint flipping problem on the left 2024-07-25 13:19:56 -07:00
Leni Aniva 5db1f0ce79
feat: Pre stress joints 2024-07-25 13:05:12 -07:00
Leni Aniva 0094e19d3a
feat: Carve channels on sides of connectors 2024-07-25 12:57:33 -07:00
Leni Aniva fc154a9810
fix: Left side joint alignment 2024-07-25 12:42:35 -07:00
Leni Aniva f951f69d62
fix: left elbow joint spaces 2024-07-25 12:38:05 -07:00
Leni Aniva 33e32aa14b
fix: Left side wrist joint 2024-07-25 12:35:36 -07:00
Leni Aniva 720de20b85
feat: Right side wrist joint all clear 2024-07-25 12:31:49 -07:00
Leni Aniva 82570528da
feat: Detach mount point from joint 2024-07-25 10:41:58 -07:00
Leni Aniva c1107aed2e
feat: Bridging joints for root 2024-07-25 09:47:41 -07:00
Leni Aniva bbfeb50f8e
feat: Tension fibre 2024-07-25 00:09:16 -07:00
Leni Aniva a6ddfec552
refactor: Separate options for Wing{L,R} 2024-07-24 22:24:23 -07:00
Leni Aniva ac6710eeeb
feat: Solve actuator position with variable r 2024-07-24 21:49:54 -07:00
Leni Aniva 57262f542f
feat: Measurements for 30mm and 50mm actuators 2024-07-24 18:17:39 -07:00
Leni Aniva 98a93cc651
feat: Elbow joint connectedness 2024-07-24 18:03:43 -07:00
Leni Aniva 7508d47265
fix: Tag direction in HS joint holes 2024-07-24 16:21:46 -07:00
Leni Aniva 8b5906948e
feat: Simpler wrist and elbow mounts 2024-07-24 16:17:07 -07:00
Leni Aniva b6d429d272
fix: Orientation of left wing 2024-07-24 15:15:58 -07:00
Leni Aniva b8c6fb51fd
feat: Torsion resistant shoulder 2024-07-24 12:45:38 -07:00
Leni Aniva a9b3aa8f70
feat: Spool for wires 2024-07-24 01:35:23 -07:00
Leni Aniva 45213adda7
fix: Collision of wing geometry 2024-07-24 01:12:02 -07:00
Leni Aniva a010baa099
fix: Remove extraneous print 2024-07-23 22:52:06 -07:00
Leni Aniva da58eeafe6
feat: Option to simplify geometry 2024-07-23 22:40:49 -07:00
Leni Aniva 4e04d30ee2
feat: Use simple joint overlaps, not bridges 2024-07-23 22:12:46 -07:00
Leni Aniva 656a2ae5bb
feat: Stable positioning of actuators 2024-07-23 19:13:06 -07:00
Leni Aniva ac509a1625
feat: Anti-collision shoulder joint 2024-07-23 16:49:25 -07:00
Leni Aniva be118be6cc
feat: Use bezier curve for inner s0 2024-07-22 15:20:09 -07:00
Leni Aniva ddbf904f58
feat: Electronic board assembly 2024-07-22 15:02:26 -07:00
Leni Aniva 07fb43cd01
feat: Actuator position to minimize tangential 2024-07-22 13:26:37 -07:00
Leni Aniva ddeaf1194f
feat: Optional actuator on wrist 2024-07-22 09:49:16 -07:00
Leni Aniva 7371333a84
refactor: Actuator arm position into its own class 2024-07-22 01:28:58 -07:00
Leni Aniva f665d0d53e
feat: Add mount for onboard electronics 2024-07-21 23:34:02 -07:00
Leni Aniva d898df6165
feat: Add battery box item 2024-07-21 22:34:19 -07:00
Leni Aniva c878f65b47
feat: Leave movement gap for cushion 2024-07-21 22:16:18 -07:00
Leni Aniva 340aa7c6da
feat: Subduct s2 into s1. Off-centre elbow 2024-07-21 22:13:56 -07:00
Leni Aniva 9ab6a1aa69
feat: Actuator mount position rel. to parent 2024-07-21 21:49:28 -07:00
Leni Aniva 71da0c10a7
fix: Elbow joint z offset problem 2024-07-21 18:49:07 -07:00
Leni Aniva 2bdae6df01
feat: Linear actuator in joint (preliminary) 2024-07-21 18:45:13 -07:00
Leni Aniva b3a472add4
feat: Linear actuator assembly 2024-07-21 05:46:18 -07:00
Leni Aniva 579c10e373
fix: Polygon sliver on left s3 2024-07-21 00:17:43 -07:00
Leni Aniva aba1ce0f3e
feat: Compute centre of mass on wings 2024-07-21 00:08:14 -07:00
Leni Aniva e23cb5cc47
feat: Centre of mass 2024-07-21 00:04:59 -07:00
Leni Aniva 3ad17f0c3e
fix: get_abs_location partial 2024-07-20 23:52:11 -07:00
Leni Aniva a47f56d41e
feat: Measurements for knob 2024-07-20 23:11:42 -07:00
Leni Aniva 0b385bdab5
fix: Extension profiles for the left side 2024-07-20 23:08:32 -07:00
Leni Aniva 82d8cf9599
feat: Extension profile on both sides 2024-07-20 22:55:43 -07:00
Leni Aniva f75375e384
feat: Nue right side blade
fix: `Cq.Location.to2d_rot()` signature
2024-07-19 23:49:38 -07:00
Leni Aniva d3a6f1e1c5
feat: Cut polygons to remove joint conflict 2024-07-19 22:29:57 -07:00
Leni Aniva f5b048d0b9
feat: Add linear actuator component 2024-07-19 21:00:10 -07:00
Leni Aniva 39110d0785
feat: Adjust shape to be closer to Nue left 2024-07-19 18:59:58 -07:00
Leni Aniva 560e9b54dd
feat: Child guard to prevent collision in shoulder 2024-07-19 16:37:47 -07:00
Leni Aniva 433a553957
fix: Missing mass argument, extranous print 2024-07-19 16:17:48 -07:00
Leni Aniva 34f6b40093
feat: s0 support in the middle 2024-07-19 16:13:33 -07:00
Leni Aniva 4b6b05853e
refactor: HS Joint into its own class 2024-07-19 15:06:57 -07:00
Leni Aniva 3e5fe7bc5e
fix: Shoulder joint axle 2024-07-19 14:06:13 -07:00
Leni Aniva 7cb00c0794
feat: Item baseclass, and fasteners 2024-07-19 00:58:10 -07:00
Leni Aniva dccae49b9d
feat: Spread the wing roots apart to make space 2024-07-18 21:40:47 -07:00
Leni Aniva 7cfc6f46fe
feat: Add mannequin to show perspective 2024-07-18 21:33:17 -07:00
Leni Aniva 7e7b9e1f64
fix: Incorrect folding on left side and on wrist 2024-07-18 21:07:08 -07:00
Leni Aniva 0ed1a1a5a4
feat: Add deflection parameter to assembly 2024-07-18 14:41:29 -07:00
Leni Aniva 052575017a
feat: Rotated wrist joint on left side 2024-07-18 14:09:53 -07:00
Leni Aniva 4c5985fa08
feat: Bent elbow joint 2024-07-18 14:03:01 -07:00
Leni Aniva 9795f7b714
fix: Wing s1 tangent to shoulder 2024-07-18 11:08:34 -07:00
Leni Aniva e73c6c0fed
feat: Reduce the number of slots on shoulder
Previously every shoulder joint was the same with two slots that specify
the neutral position. Experiment reveals this to be too fragile.
2024-07-17 21:37:08 -07:00
Leni Aniva 6c6c17ea07
refactor: Use 2d locations for wing tags 2024-07-17 21:17:50 -07:00
Leni Aniva 6d72749c9b
refactor: Use 2d location in extrusion argument 2024-07-17 19:28:56 -07:00
Leni Aniva 014784be34
feat: Calculation of total mass 2024-07-17 19:13:06 -07:00
Leni Aniva 9de4159166
feat: 2d location 2024-07-17 14:47:34 -07:00
Leni Aniva eb445b3d8b
fix: Housing wall location 2024-07-17 14:47:22 -07:00
Leni Aniva bbe24091da
fix: Target name 2024-07-17 13:27:48 -07:00
Leni Aniva 3aa4a592f0
fix: Collision between spring and track 2024-07-17 13:20:06 -07:00
Leni Aniva 77cc69acfb
fix: Arm radius in elbow and shoulder joints 2024-07-17 13:09:46 -07:00
Leni Aniva 348799c46e
fix: Tag points on wing 2024-07-17 12:11:08 -07:00
Leni Aniva 21e5ad0b82
feat: Simplify elbow joint 2024-07-17 10:22:59 -07:00
Leni Aniva b86904bd96
feat: Smaller disk for wrist joint 2024-07-17 01:22:05 -07:00
Leni Aniva d668fb1966
fix: Splitting line for each wing 2024-07-17 01:19:17 -07:00
Leni Aniva 53b3a2bd34
feat: Shoulder joint follow wing direction 2024-07-17 01:06:52 -07:00
Leni Aniva 572c39d31f
fix: H-S and shoulder joint locations 2024-07-17 00:30:41 -07:00
Leni Aniva 3adb887ef5
fix: Incorrect staggering of left wings 2024-07-16 23:32:23 -07:00
Leni Aniva c12ccf3495
feat: Staggered shoulder joint 2024-07-16 22:26:06 -07:00
Leni Aniva 2a968f9446
feat: Improved H-S joint and harness geometry 2024-07-16 21:20:45 -07:00
Leni Aniva bc5a7df30f
feat: Left side wing 2024-07-16 17:18:28 -07:00
Leni Aniva 66b26fa056
feat: Submodel in build system 2024-07-16 15:42:39 -07:00
Leni Aniva 0cc6100d0e
refactor: Move flip to ElbowJoint 2024-07-16 14:25:17 -07:00
Leni Aniva ef6b2a8663
refactor: Create class for torsion spring 2024-07-16 13:28:53 -07:00
Leni Aniva cdb46263f8
fix: Ambiguous rotation in Hirth Joint 2024-07-16 12:03:51 -07:00
Leni Aniva c73675bbe3
feat: Colouring assembly by role and material 2024-07-16 11:55:38 -07:00
Leni Aniva 027eec7264
refactor: Move wings to its own class with joints 2024-07-15 22:57:38 -07:00
Leni Aniva bc8cda2eec
refactor: Move harness to its own class 2024-07-14 23:58:42 -07:00
Leni Aniva 1f5a65c43f
fix: _subloc patch, wing root strut 2024-07-14 17:56:02 -07:00
Leni Aniva 1bcb27c711
feat: Wing root class 2024-07-14 00:47:44 -07:00
Leni Aniva a0ae8c91eb
feat: Remove fixed rotation constraints
There is currently a bug when it comes to solving deeply nested
assemblies. We need to come up with a solution.
2024-07-13 16:19:17 -07:00
Leni Aniva e744250c6c
fix: Use non-fixed constraints for disk joint 2024-07-13 12:57:17 -07:00
Leni Aniva 641755314e
refactor: Factor out parts of the wing assembly 2024-07-12 23:16:04 -07:00
Leni Aniva 9f41f2ea3c
feat: Wing anchors for right side 2024-07-12 11:04:28 -07:00
Leni Aniva 9f9946569d
feat: Elbow joint 2024-07-11 22:29:05 -07:00
Leni Aniva d43c1944a7
feat: Splined wing profile 2024-07-11 16:02:54 -07:00
Leni Aniva 2aeeaae061
feat: Movement span on disk joint 2024-07-11 08:42:13 -07:00
Leni Aniva d8a62d3352
feat: Disk joint for wrist and elbow 2024-07-10 16:21:11 -07:00
Leni Aniva 2cf03eae09
feat: Add inner gap to torsion joint
This is for easing movement
2024-07-10 16:20:52 -07:00
Leni Aniva 6de1c3bc39
feat: Finalize handle properties 2024-07-10 16:20:33 -07:00
Leni Aniva 86a5d6e6bf
fix: Size of torsion joint cf. spring 2024-07-10 11:58:31 -07:00
Leni Aniva bf299d338c
fix: Torsion joint slot labeling 2024-07-10 10:52:48 -07:00
Leni Aniva 2395c46839
fix: Torsion joint rider must have through hole 2024-07-10 10:34:31 -07:00
Leni Aniva 056f6bb085
feat: Gap in bayonet mount 2024-07-09 22:30:29 -07:00
Leni Aniva dcb3c31c1d
feat: Prototype flag, spring re-parameter 2024-07-09 22:22:48 -07:00
Leni Aniva 539a5d1229
feat: Bayonet mount 2024-07-09 22:09:16 -07:00
Leni Aniva b441789c9f
refactor: Use proper "mount" terminology 2024-07-09 21:34:06 -07:00
Leni Aniva 840995d82b
fix: Use insertion length for threads 2024-07-09 21:32:29 -07:00
Leni Aniva 8b0c9a000d
refactor: Allow different types of handle joints 2024-07-09 21:31:00 -07:00
Leni Aniva 27ce94124f
feat: Right side wing profile 2024-07-09 21:13:16 -07:00
Leni Aniva 48cfd52455
refactor: Wing profile class 2024-07-09 19:57:54 -07:00
Leni Aniva 234e1b7adc
feat: Add shell to wing joint, wing assembly 2024-07-08 22:32:49 -07:00
Leni Aniva 876571418c
fix: Directrix labeling in torsion joint 2024-07-08 21:46:35 -07:00
Leni Aniva 53c143e0b7
test: Check assembly collision 2024-07-07 21:45:10 -07:00
Leni Aniva d43482f77d
feat: 2nd wing segment with spacer 2024-07-07 21:01:40 -07:00
Leni Aniva 54593b9a4e
feat: Shoulder parent joint 2024-07-07 12:15:47 -07:00
Leni Aniva fc0edd995b
fix: Torsion joint directrix 2024-07-07 09:44:54 -07:00
Leni Aniva dc6e2a8933
test: Torsion joint covered 2024-07-06 23:53:57 -07:00
Leni Aniva 800b658410
feat: Right-handed spring 2024-07-06 23:50:10 -07:00
Leni Aniva 58028579a9
fix: Torsion joint directrix and collision problem 2024-07-06 23:43:55 -07:00
Leni Aniva 9e7369c6f8
feat: Tag point and tag plane for mating 2024-07-06 16:41:13 -07:00
Leni Aniva 8711ed54a4
feat: Connectors on wing root 2024-07-04 17:50:11 -07:00
Leni Aniva 1794729890
fix: Use subassemblies for wings and harnesses 2024-07-04 12:03:38 -07:00
Leni Aniva 89c6a39c2f
feat: Name in target 2024-07-04 10:02:58 -07:00
Leni Aniva d69cf014a1
chore: Clean up import 2024-07-04 01:16:01 -07:00
Leni Aniva 80fb2e997d
feat: Build trident handle 2024-07-04 01:13:22 -07:00
Leni Aniva 66fc02ef44
feat: Export DXF in build system 2024-07-04 01:11:16 -07:00
Leni Aniva 5bceb6180e
refactor: Move parts into nhf.parts 2024-07-04 00:42:14 -07:00
Leni Aniva 46161ba82e
fix: Decorated target not directly callable 2024-07-04 00:24:14 -07:00
Leni Aniva 6201683c00
feat: Add build system 2024-07-03 23:15:39 -07:00
Leni Aniva e75e640623
fix: Type missing in dataclass 2024-07-03 18:45:16 -07:00
Leni Aniva 2af1499bd5
fix: One sided connector 2024-07-02 19:59:09 -07:00
Leni Aniva 1710f0db36
feat: Improve model for printing 2024-07-01 17:59:42 -07:00
Leni Aniva 59bcc9914c
fix: Remove tag prefix in favour of subassembly 2024-06-30 19:03:16 -07:00
Leni Aniva af56e28ac3
fix: Hirth joint mating 2024-06-30 14:28:42 -07:00
Leni Aniva 3170a025a1
refactor: Combine Hirth Joint into one class 2024-06-28 23:12:11 -04:00
Leni Aniva 87e99ac4ce
fix: Collision problem with Hirth joints 2024-06-28 21:59:09 -04:00
Leni Aniva 914bc23582
feat: Add directrix tag to hirth joint 2024-06-28 17:21:30 -04:00
Leni Aniva 53ef5e454f
Merge branch 'util/material' into touhou/houjuu-nue 2024-06-28 07:55:51 -04:00
Leni Aniva 4dd43f7151
refactor: Separate H-S joint component 2024-06-27 23:22:54 -04:00
Leni Aniva 0bee80f582
fix: use `.located` to move threads 2024-06-26 19:27:36 -04:00
Leni Aniva 53c204eb20
feat: Torsion joint 2024-06-26 15:57:22 -04:00
Leni Aniva 9fda02ed9d
feat: Thread on handle terminal piece 2024-06-26 12:01:01 -04:00
Leni Aniva d823a58d88
feat: Metric threads on handle 2024-06-26 11:28:25 -04:00
Leni Aniva 0c42f71c9f
fix: Don't user `assert` in unit tests 2024-06-26 09:44:02 -04:00
Leni Aniva cec2f4da55
test: Trident length 2024-06-26 09:42:50 -04:00
Leni Aniva a41906d130
feat: Trident handle 2024-06-25 09:14:34 -04:00
Leni Aniva 32e5f543d9
feat: 2 segment wing root 2024-06-24 16:13:15 -07:00
Leni Aniva caf8fb477a
test: Joint integrity 2024-06-24 11:16:25 -07:00
Leni Aniva d5ddbc4186
feat: Use M12 centre hole for H-S joint 2024-06-24 11:05:03 -07:00
Leni Aniva eb8a48fe77
feat: Harness assembly 2024-06-23 22:27:15 -07:00
Leni Aniva 376580003e
feat: Base of Houjuu-Scarlett joint 2024-06-22 13:40:06 -07:00
Leni Aniva 0e5445ebb5
feat: Nue wing R1 2024-06-20 23:45:24 -07:00
Leni Aniva 133c69b846
feat: Wing profile, unit testing 2024-06-20 23:29:18 -07:00
Leni Aniva 8ad5eb9fe6
feat: Comma joint, Nue wing root stub 2024-06-19 21:23:41 -07:00
Leni Aniva 75c06585ed
fix: Hirth joint mating line 2024-06-19 16:14:49 -07:00
Leni Aniva a3f2b01b8c
fix: Extraneous printing 2024-06-19 15:54:42 -07:00
Leni Aniva 4613247e1b
feat: Hirth Joint for wing root 2024-06-19 15:54:09 -07:00
15 changed files with 4879 additions and 3529 deletions

View File

@ -1,7 +1,6 @@
# Cosplay
This is the design repository for NorCal Hakkero Factory No. 1, where we use
parametric CAD to make cosplay props.
This is the design repository for NorCal Hakkero Factory No. 1.
## Development
@ -16,12 +15,6 @@ and this should succeed
python3 -c "import nhf"
```
To visualize an object, create a file `visualize.py`, and run `cq-editor`:
``` sh
python3 -m cq_editor visualize.py
```
## Testing
Run all tests with

View File

@ -1,12 +0,0 @@
import cadquery as Cq
def mystery():
return (
Cq.Workplane("XY")
.box(10, 5, 5)
.faces(">Z")
.workplane()
.hole(1)
.edges("|Z")
.fillet(2)
)

0
nhf/touhou/__init__.py Normal file
View File

View File

@ -0,0 +1,14 @@
#+title: Cosplay: Houjuu Nue
* Controller
This part describes the electrical connections and the microcontroller code.
* Structure
This part describes the 3d printed and laser cut structures. ~structure.blend~
is an overall sketch of the shapes and looks of the wing.
* Pattern
This part describes the sewing patterns.

View File

@ -0,0 +1,204 @@
"""
To build, execute
```
python3 nhf/touhou/houjuu_nue/__init__.py
```
This cosplay consists of 3 components:
## Trident
The trident is composed of individual segments, made of acrylic, and a 3D
printed head (convention rule prohibits metal) with a metallic paint. To ease
transportation, the trident handle has individual segments with threads and can
be assembled on site.
## Snake
A 3D printed snake with a soft material so it can wrap around and bend
## Wings
This is the crux of the cosplay and the most complex component. The wings mount
on a wearable harness. Each wing consists of 4 segments with 3 joints. Parts of
the wing which demands transluscency are created from 1/16" acrylic panels.
These panels serve double duty as the exoskeleton.
The wings are labeled r1,r2,r3,l1,l2,l3. The segments of the wings are labeled
from root to tip s0 (root),
s1, s2, s3. The joints are named (from root to tip)
shoulder, elbow, wrist in analogy with human anatomy.
"""
from dataclasses import dataclass, field
from typing import Optional
import cadquery as Cq
from nhf.build import Model, TargetKind, target, assembly, submodel
import nhf.touhou.houjuu_nue.wing as MW
import nhf.touhou.houjuu_nue.trident as MT
import nhf.touhou.houjuu_nue.joints as MJ
import nhf.touhou.houjuu_nue.harness as MH
import nhf.touhou.houjuu_nue.electronics as ME
from nhf.parts.item import Item
import nhf.utils
WING_DEFLECT_ODD = 0.0
WING_DEFLECT_EVEN = 25.0
@dataclass
class Parameters(Model):
"""
Defines dimensions for the Houjuu Nue cosplay
"""
harness: MH.Harness = field(default_factory=lambda: MH.Harness())
wing_r1: MW.WingR = field(default_factory=lambda: MW.WingR(
name="r1",
root_joint=MJ.RootJoint(
parent_substrate_cull_corners=(0,1,1,1),
parent_substrate_cull_edges=(0,0,1,0),
),
shoulder_angle_bias=WING_DEFLECT_ODD,
s0_top_hole=False,
s0_bot_hole=True,
arrow_height=350.0
))
wing_r2: MW.WingR = field(default_factory=lambda: MW.WingR(
name="r2",
root_joint=MJ.RootJoint(
parent_substrate_cull_corners=(1,1,1,1),
parent_substrate_cull_edges=(0,0,1,0),
),
electronic_board=ME.ElectronicBoardControl(),
shoulder_angle_bias=WING_DEFLECT_EVEN,
s0_top_hole=True,
s0_bot_hole=True,
))
wing_r3: MW.WingR = field(default_factory=lambda: MW.WingR(
name="r3",
root_joint=MJ.RootJoint(
parent_substrate_cull_corners=(1,1,1,0),
parent_substrate_cull_edges=(0,0,1,0),
),
shoulder_angle_bias=WING_DEFLECT_ODD,
s0_top_hole=True,
s0_bot_hole=False,
))
wing_l1: MW.WingL = field(default_factory=lambda: MW.WingL(
name="l1",
root_joint=MJ.RootJoint(
parent_substrate_cull_corners=(1,0,1,1),
parent_substrate_cull_edges=(1,0,0,0),
),
shoulder_angle_bias=WING_DEFLECT_EVEN,
wrist_angle=-60.0,
s0_top_hole=False,
s0_bot_hole=True,
))
wing_l2: MW.WingL = field(default_factory=lambda: MW.WingL(
name="l2",
root_joint=MJ.RootJoint(
parent_substrate_cull_corners=(1,1,1,1),
parent_substrate_cull_edges=(1,0,0,0),
),
wrist_angle=-30.0,
shoulder_angle_bias=WING_DEFLECT_ODD,
s0_top_hole=True,
s0_bot_hole=True,
))
wing_l3: MW.WingL = field(default_factory=lambda: MW.WingL(
name="l3",
root_joint=MJ.RootJoint(
parent_substrate_cull_corners=(1,1,0,1),
parent_substrate_cull_edges=(1,0,0,0),
),
shoulder_angle_bias=WING_DEFLECT_EVEN,
wrist_angle=-0.0,
s0_top_hole=True,
s0_bot_hole=False,
))
trident: MT.Trident = field(default_factory=lambda: MT.Trident())
def __post_init__(self):
super().__init__(name="houjuu-nue")
@submodel(name="harness")
def submodel_harness(self) -> Model:
return self.harness
@submodel(name="wing-r1")
def submodel_wing_r1(self) -> Model:
return self.wing_r1
@submodel(name="wing-r2")
def submodel_wing_r2(self) -> Model:
return self.wing_r2
@submodel(name="wing-r3")
def submodel_wing_r3(self) -> Model:
return self.wing_r3
@submodel(name="wing-l1")
def submodel_wing_l1(self) -> Model:
return self.wing_l1
@submodel(name="wing-l2")
def submodel_wing_l2(self) -> Model:
return self.wing_l2
@submodel(name="wing-l3")
def submodel_wing_l3(self) -> Model:
return self.wing_l3
@assembly()
def wings_harness_assembly(self,
parts: Optional[list[str]] = None,
**kwargs) -> Cq.Assembly:
"""
Assembly of harness with all the wings
"""
result = (
Cq.Assembly()
.add(self.harness.assembly(), name="harness", loc=Cq.Location((0, 0, 0)))
.add(self.wing_r1.assembly(parts, root_offset=9, **kwargs), name="wing_r1")
.add(self.wing_r2.assembly(parts, root_offset=7, **kwargs), name="wing_r2")
.add(self.wing_r3.assembly(parts, root_offset=6, **kwargs), name="wing_r3")
.add(self.wing_l1.assembly(parts, root_offset=19, **kwargs), name="wing_l1")
.add(self.wing_l2.assembly(parts, root_offset=20, **kwargs), name="wing_l2")
.add(self.wing_l3.assembly(parts, root_offset=21, **kwargs), name="wing_l3")
)
for tag in ["r1", "r2", "r3", "l1", "l2", "l3"]:
self.harness.add_root_joint_constraint(
result,
"harness/base",
f"wing_{tag}/root",
tag
)
return result.solve()
@submodel(name="trident")
def submodel_trident(self) -> Model:
return self.trident
def stat(self) -> dict[str, float]:
a = self.wings_harness_assembly()
bbox = a.toCompound().BoundingBox()
return {
"wing-span": bbox.xlen,
"wing-depth": bbox.ylen,
"wing-height": bbox.zlen,
"wing-mass": a.total_mass(),
"wing-centre-of-mass": a.centre_of_mass().toTuple(),
"items": Item.count(a),
}
if __name__ == '__main__':
import sys
p = Parameters()
if len(sys.argv) == 1:
p.build_all()
sys.exit(0)
if sys.argv[1] == 'stat':
print(p.stat())
elif sys.argv[1] == 'model':
file_name = sys.argv[2]
a = p.wings_harness_assembly()
a.save(file_name, exportType='STEP')

View File

@ -0,0 +1,18 @@
from nhf.parts.fasteners import FlatHeadBolt, HexNut, ThreaddedKnob
NUT_COMMON = HexNut(
# FIXME: measure
mass=0.0,
diam_thread=4.0,
pitch=0.7,
thickness=3.2,
width=7.0,
)
BOLT_COMMON = FlatHeadBolt(
# FIXME: measure
mass=0.0,
diam_head=8.0,
height_head=2.0,
diam_thread=4.0,
height_thread=20.0,
)

View File

@ -0,0 +1,68 @@
#include <FastLED.h>
// Main LED strip setup
#define LED_PIN 5
#define NUM_LEDS 100
#define LED_PART 50
#define BRIGHTNESS 250
#define LED_TYPE WS2811
CRGB leds[NUM_LEDS];
CRGB color_red;
CRGB color_blue;
CRGB color_green;
#define DIAG_PIN 6
void setup() {
// Calculate colors
hsv2rgb_spectrum(CHSV(4, 255, 100), color_red);
hsv2rgb_spectrum(CHSV(170, 255, 100), color_blue);
hsv2rgb_spectrum(CHSV(90, 255, 100), color_green);
pinMode(LED_BUILTIN, OUTPUT);
pinMode(LED_PIN, OUTPUT);
pinMode(DIAG_PIN, OUTPUT);
// Main LED strip
FastLED.addLeds<LED_TYPE, LED_PIN, RGB>(leds, NUM_LEDS);
}
void loop() {
fill_segmented(CRGB::Green, CRGB::Orange);
delay(500);
flash(leds, NUM_LEDS, color_red, 10, 20);
delay(500);
flash(leds, NUM_LEDS, color_blue, 10, 20);
delay(500);
}
void fill_segmented(CRGB c1, CRGB c2)
{
//fill_solid(leds, LED_PART, c1);
fill_gradient_RGB(leds, LED_PART, CRGB::Black ,c1);
fill_gradient_RGB(leds + LED_PART, NUM_LEDS - LED_PART, CRGB::Black, c2);
FastLED.show();
}
void flash(CRGB *ptr, uint16_t num, CRGB const& lead, int steps, int step_time)
{
digitalWrite(LED_BUILTIN, LOW);
//fill_solid(leds, NUM_LEDS, CRGB::Black);
for (int i = 0; i < steps; ++i)
{
uint8_t factor = 255 * i / steps;
analogWrite(DIAG_PIN, factor);
CRGB tail = blend(lead, CRGB::Black, factor);
uint16_t front = factor * (int) num / 255;
fill_solid(ptr, front, tail);
//fill_gradient_RGB(ptr, front, tail, lead);
//fill_solid(leds + front, NUM_LEDS - front, CRGB::Black);
FastLED.show();
delay(step_time);
}
fill_gradient_RGB(ptr, num, CRGB::Black, lead);
FastLED.show();
analogWrite(DIAG_PIN, LOW);
}

View File

@ -0,0 +1,540 @@
"""
Electronic components
"""
from dataclasses import dataclass, field
from typing import Optional, Tuple
import math
import cadquery as Cq
from nhf.build import Model, TargetKind, target, assembly, submodel
from nhf.materials import Role, Material
from nhf.parts.box import MountingBox, Hole
from nhf.parts.fibre import tension_fibre
from nhf.parts.item import Item
from nhf.parts.fasteners import FlatHeadBolt, HexNut
from nhf.parts.electronics import ArduinoUnoR3, BatteryBox18650
from nhf.touhou.houjuu_nue.common import NUT_COMMON, BOLT_COMMON
import nhf.utils
@dataclass(frozen=True)
class LinearActuator(Item):
stroke_length: float
shaft_diam: float = 9.04
front_hole_ext: float = 4.41
front_hole_diam: float = 4.41
front_length: float = 9.55
front_width: float = 9.24
front_height: float = 5.98
segment1_length: float = 37.54
segment1_width: float = 15.95
segment1_height: float = 11.94
segment2_length: float = 37.37
segment2_width: float = 20.03
segment2_height: float = 15.03
back_hole_ext: float = 4.58
back_hole_diam: float = 4.18
back_length: float = 9.27
back_width: float = 10.16
back_height: float = 8.12
@property
def name(self) -> str:
return f"LinearActuator {self.stroke_length}mm"
@property
def role(self) -> Role:
return Role.MOTION
@property
def conn_length(self):
return self.segment1_length + self.segment2_length + self.front_hole_ext + self.back_hole_ext
def generate(self, pos: float=0) -> Cq.Assembly:
assert -1e-6 <= pos <= 1 + 1e-6, f"Illegal position: {pos}"
stroke_x = pos * self.stroke_length
front = (
Cq.Workplane('XZ')
.cylinder(
radius=self.front_width / 2,
height=self.front_height,
centered=True,
)
.box(
length=self.front_hole_ext,
width=self.front_width,
height=self.front_height,
combine=True,
centered=(False, True, True)
)
.copyWorkplane(Cq.Workplane('XZ'))
.cylinder(
radius=self.front_hole_diam / 2,
height=self.front_height,
centered=True,
combine='cut',
)
)
front.copyWorkplane(Cq.Workplane('XZ')).tagPlane('conn')
if stroke_x > 0:
shaft = (
Cq.Workplane('YZ')
.cylinder(
radius=self.shaft_diam / 2,
height=stroke_x,
centered=(True, True, False)
)
)
else:
shaft = None
segment1 = (
Cq.Workplane()
.box(
length=self.segment1_length,
height=self.segment1_width,
width=self.segment1_height,
centered=(False, True, True),
)
)
segment2 = (
Cq.Workplane()
.box(
length=self.segment2_length,
height=self.segment2_width,
width=self.segment2_height,
centered=(False, True, True),
)
)
back = (
Cq.Workplane('XZ')
.cylinder(
radius=self.back_width / 2,
height=self.back_height,
centered=True,
)
.box(
length=self.back_hole_ext,
width=self.back_width,
height=self.back_height,
combine=True,
centered=(False, True, True)
)
.copyWorkplane(Cq.Workplane('XZ'))
.cylinder(
radius=self.back_hole_diam / 2,
height=self.back_height,
centered=True,
combine='cut',
)
)
back.faces(">X").tag("dir")
back.copyWorkplane(Cq.Workplane('XZ')).tagPlane('conn')
result = (
Cq.Assembly()
.add(front, name="front",
loc=Cq.Location((-self.front_hole_ext, 0, 0)))
.add(segment1, name="segment1",
loc=Cq.Location((stroke_x, 0, 0)))
.add(segment2, name="segment2",
loc=Cq.Location((stroke_x + self.segment1_length, 0, 0)))
.add(back, name="back",
loc=Cq.Location((stroke_x + self.segment1_length + self.segment2_length + self.back_hole_ext, 0, 0), (0, 1, 0), 180))
)
if shaft:
result.add(shaft, name="shaft")
return result
@dataclass(frozen=True)
class MountingBracket(Item):
"""
Mounting bracket for a linear actuator
"""
mass: float = 1.6
hole_diam: float = 4.0
width: float = 8.0
height: float = 12.20
thickness: float = 0.98
length: float = 13.00
hole_to_side_ext: float = 8.25
def __post_init__(self):
assert self.hole_to_side_ext - self.hole_diam / 2 > 0
@property
def name(self) -> str:
return f"MountingBracket M{int(self.hole_diam)}"
@property
def role(self) -> Role:
return Role.MOTION
def generate(self) -> Cq.Workplane:
result = (
Cq.Workplane('XY')
.box(
length=self.hole_to_side_ext,
width=self.width,
height=self.height,
centered=(False, True, True)
)
.copyWorkplane(Cq.Workplane('XY'))
.cylinder(
height=self.height,
radius=self.width / 2,
combine=True,
)
.copyWorkplane(Cq.Workplane('XY'))
.box(
length=2 * (self.hole_to_side_ext - self.thickness),
width=self.width,
height=self.height - self.thickness * 2,
combine='cut',
)
.copyWorkplane(Cq.Workplane('XY'))
.cylinder(
height=self.height,
radius=self.hole_diam / 2,
combine='cut'
)
.copyWorkplane(Cq.Workplane('YZ'))
.cylinder(
height=self.hole_to_side_ext * 2,
radius=self.hole_diam / 2,
combine='cut'
)
)
result.copyWorkplane(Cq.Workplane('YZ', origin=(self.hole_to_side_ext, 0, 0))).tagPlane("conn_side")
result.copyWorkplane(Cq.Workplane('XY', origin=(0, 0, self.height/2))).tagPlane("conn_top")
result.copyWorkplane(Cq.Workplane('YX', origin=(0, 0, -self.height/2))).tagPlane("conn_bot")
result.copyWorkplane(Cq.Workplane('XY')).tagPlane("conn_mid")
return result
LINEAR_ACTUATOR_50 = LinearActuator(
mass=40.8,
stroke_length=50,
shaft_diam=9.05,
front_hole_ext=4.32,
back_hole_ext=4.54,
segment1_length=57.35,
segment1_width=15.97,
segment1_height=11.95,
segment2_length=37.69,
segment2_width=19.97,
segment2_height=14.96,
front_length=9.40,
front_width=9.17,
front_height=6.12,
back_length=9.18,
back_width=10.07,
back_height=8.06,
)
LINEAR_ACTUATOR_30 = LinearActuator(
mass=34.0,
stroke_length=30,
)
LINEAR_ACTUATOR_21 = LinearActuator(
# FIXME: Measure
mass=0.0,
stroke_length=21,
front_hole_ext=4,
back_hole_ext=4,
segment1_length=34,
segment2_length=34,
)
LINEAR_ACTUATOR_10 = LinearActuator(
mass=41.3,
stroke_length=10,
front_hole_ext=4.02,
back_hole_ext=4.67,
segment1_length=13.29,
segment1_width=15.88,
segment1_height=12.07,
segment2_length=42.52,
segment2_width=20.98,
segment2_height=14.84,
)
LINEAR_ACTUATOR_HEX_NUT = HexNut(
mass=0.8,
diam_thread=4,
pitch=0.7,
thickness=4.16,
width=6.79,
)
LINEAR_ACTUATOR_BOLT = FlatHeadBolt(
mass=1.7,
diam_head=6.68,
height_head=2.98,
diam_thread=4.0,
height_thread=15.83,
)
LINEAR_ACTUATOR_BRACKET = MountingBracket()
BATTERY_BOX = BatteryBox18650()
# Acrylic hex nut
ELECTRONIC_MOUNT_HEXNUT = HexNut(
mass=0.8,
diam_thread=4,
pitch=0.7,
thickness=3.57,
width=6.81,
)
@dataclass(kw_only=True, frozen=True)
class Winch:
linear_motion_span: float
actuator: LinearActuator = LINEAR_ACTUATOR_21
nut: HexNut = LINEAR_ACTUATOR_HEX_NUT
bolt: FlatHeadBolt = LINEAR_ACTUATOR_BOLT
bracket: MountingBracket = LINEAR_ACTUATOR_BRACKET
@dataclass(kw_only=True)
class Flexor:
"""
Actuator assembly which flexes, similar to biceps
"""
motion_span: float
arm_radius: Optional[float] = None
pos_smaller: bool = True
actuator: LinearActuator = LINEAR_ACTUATOR_50
nut: HexNut = LINEAR_ACTUATOR_HEX_NUT
bolt: FlatHeadBolt = LINEAR_ACTUATOR_BOLT
bracket: MountingBracket = LINEAR_ACTUATOR_BRACKET
# Length of line attached to the flexor
line_length: float = 0.0
line_thickness: float = 0.5
# By how much is the line permitted to slack. This reduces the effective stroke length
line_slack: float = 0.0
def __post_init__(self):
assert self.line_slack <= self.line_length, f"Insufficient length: {self.line_slack} >= {self.line_length}"
assert self.line_slack < self.actuator.stroke_length
@property
def mount_height(self):
return self.bracket.hole_to_side_ext
@property
def d_open(self):
return self.actuator.conn_length + self.actuator.stroke_length + self.line_length - self.line_slack
@property
def d_closed(self):
return self.actuator.conn_length + self.line_length
def open_pos(self) -> Tuple[float, float, float]:
r, phi, r_ = nhf.geometry.contraction_span_pos_from_radius(
d_open=self.d_open,
d_closed=self.d_closed,
theta=math.radians(self.motion_span),
r=self.arm_radius,
smaller=self.pos_smaller,
)
return r, math.degrees(phi), r_
def target_length_at_angle(
self,
angle: float = 0.0
) -> float:
"""
Length of the actuator at some angle
"""
assert 0 <= angle <= self.motion_span
r, phi, rp = self.open_pos()
th = math.radians(phi - angle)
result = math.sqrt(r * r + rp * rp - 2 * r * rp * math.cos(th))
#result = math.sqrt((r * math.cos(th) - rp) ** 2 + (r * math.sin(th)) ** 2)
assert self.d_closed -1e-6 <= result <= self.d_open + 1e-6,\
f"Illegal length: {result} not in [{self.d_closed}, {self.d_open}]"
return result
def add_to(
self,
a: Cq.Assembly,
target_length: float,
tag_prefix: Optional[str] = None,
tag_hole_front: Optional[str] = None,
tag_hole_back: Optional[str] = None,
tag_dir: Optional[str] = None):
"""
Adds the necessary mechanical components to this assembly. Does not
invoke `a.solve()`.
"""
draft = max(0, target_length - self.d_closed - self.line_length)
pos = draft / self.actuator.stroke_length
line_l = target_length - draft - self.actuator.conn_length
if tag_prefix:
tag_prefix = tag_prefix + "_"
else:
tag_prefix = ""
name_actuator = f"{tag_prefix}actuator"
name_bracket_front = f"{tag_prefix}bracket_front"
name_bracket_back = f"{tag_prefix}bracket_back"
name_bolt_front = f"{tag_prefix}front_bolt"
name_bolt_back = f"{tag_prefix}back_bolt"
name_nut_front = f"{tag_prefix}front_nut"
name_nut_back = f"{tag_prefix}back_nut"
(
a
.add(self.actuator.assembly(pos=pos), name=name_actuator)
.add(self.bracket.assembly(), name=name_bracket_front)
.add(self.bolt.assembly(), name=name_bolt_front)
.add(self.nut.assembly(), name=name_nut_front)
.constrain(f"{name_bolt_front}?root", f"{name_bracket_front}?conn_top",
"Plane", param=0)
.constrain(f"{name_nut_front}?bot", f"{name_bracket_front}?conn_bot",
"Plane")
.add(self.bracket.assembly(), name=name_bracket_back)
.add(self.bolt.assembly(), name=name_bolt_back)
.add(self.nut.assembly(), name=name_nut_back)
.constrain(f"{name_actuator}/back?conn", f"{name_bracket_back}?conn_mid",
"Plane", param=0)
.constrain(f"{name_bolt_back}?root", f"{name_bracket_back}?conn_top",
"Plane", param=0)
.constrain(f"{name_nut_back}?bot", f"{name_bracket_back}?conn_bot",
"Plane")
)
if self.line_length == 0.0:
a.constrain(
f"{name_actuator}/front?conn",
f"{name_bracket_front}?conn_mid",
"Plane", param=0)
else:
(
a
.addS(tension_fibre(
length=line_l,
hole_diam=self.nut.diam_thread,
thickness=self.line_thickness,
), name="fibre", role=Role.CONNECTION)
.constrain(
f"{name_actuator}/front?conn",
"fibre?male",
"Plane"
)
.constrain(
f"{name_bracket_front}?conn_mid",
"fibre?female",
"Plane"
)
)
if tag_hole_front:
a.constrain(tag_hole_front, f"{name_bracket_front}?conn_side", "Plane")
if tag_hole_back:
a.constrain(tag_hole_back, f"{name_bracket_back}?conn_side", "Plane")
if tag_dir:
a.constrain(tag_dir, f"{name_bracket_front}?conn_mid", "Axis", param=0)
@dataclass
class ElectronicBoard(Model):
name: str = "electronic-board"
nut: HexNut = NUT_COMMON
bolt: FlatHeadBolt = BOLT_COMMON
length: float = 70.0
width: float = 170.0
mount_holes: list[Hole] = field(default_factory=lambda: [
Hole(x=25, y=75),
Hole(x=25, y=-75),
Hole(x=-25, y=75),
Hole(x=-25, y=-75),
])
panel_thickness: float = 25.4 / 16
mount_panel_thickness: float = 25.4 / 4
material: Material = Material.WOOD_BIRCH
@property
def mount_hole_diam(self) -> float:
return self.bolt.diam_thread
def __post_init__(self):
super().__init__(name=self.name)
def panel(self) -> MountingBox:
return MountingBox(
holes=self.mount_holes,
hole_diam=self.mount_hole_diam,
length=self.length,
width=self.width,
centred=(True, True),
thickness=self.panel_thickness,
generate_reverse_tags=True,
)
def assembly(self) -> Cq.Assembly:
panel = self.panel()
result = (
Cq.Assembly()
.addS(panel.generate(), name="panel",
role=Role.ELECTRONIC | Role.STRUCTURE, material=self.material)
)
for hole in self.mount_holes:
bolt_name = f"{hole.tag}_bolt"
(
result
.add(self.bolt.assembly(), name=bolt_name)
.constrain(
f"{bolt_name}?root",
f"panel?{hole.tag}",
"Plane", param=0
)
)
return result.solve()
@dataclass
class ElectronicBoardBattery(ElectronicBoard):
name: str = "electronic-board-battery"
battery_box: BatteryBox18650 = BATTERY_BOX
@submodel(name="panel")
def panel_out(self) -> MountingBox:
return self.panel()
@dataclass
class ElectronicBoardControl(ElectronicBoard):
name: str = "electronic-board-control"
controller_datum: Cq.Location = Cq.Location.from2d(-25, 23, -90)
controller: ArduinoUnoR3 = ArduinoUnoR3()
def panel(self) -> MountingBox:
box = super().panel()
def transform(i, x, y):
pos = self.controller_datum * Cq.Location.from2d(x, self.controller.width - y)
x, y = pos.to2d_pos()
return Hole(
x=x, y=y,
diam=self.controller.hole_diam,
tag=f"controller_conn{i}",
)
box.holes = box.holes.copy() + [
transform(i, x, y)
for i, (x, y) in enumerate(self.controller.holes)
]
return box
@submodel(name="panel")
def panel_out(self) -> MountingBox:
return self.panel()
def assembly(self) -> Cq.Assembly:
result = super().assembly()
result.add(self.controller.assembly(), name="controller")
for i in range(len(self.controller.holes)):
result.constrain(f"controller?conn{i}", f"panel?controller_conn{i}", "Plane")
return result.solve()
@dataclass(frozen=True)
class LightStrip:
width: float = 10.0
height: float = 4.5

View File

@ -0,0 +1,204 @@
from dataclasses import dataclass, field
import cadquery as Cq
from nhf.parts.joints import HirthJoint
from nhf import Material, Role
from nhf.build import Model, TargetKind, target, assembly, submodel
from nhf.touhou.houjuu_nue.joints import RootJoint
from nhf.parts.box import MountingBox
import nhf.utils
@dataclass(frozen=True, kw_only=True)
class Mannequin:
"""
A mannequin for calibration
"""
shoulder_width: float = 400
shoulder_to_waist: float = 440
waist_width: float = 250
head_height: float = 220.0
neck_height: float = 105.0
neck_diam: float = 140
head_diam: float = 210
torso_thickness: float = 150
def generate(self) -> Cq.Workplane:
head_neck = (
Cq.Workplane("XY")
.cylinder(
radius=self.neck_diam/2,
height=self.neck_height,
centered=(True, True, False))
.faces(">Z")
.workplane()
.cylinder(
radius=self.head_diam/2,
height=self.head_height,
combine=True, centered=(True, True, False))
)
result = (
Cq.Workplane("XY")
.rect(self.waist_width, self.torso_thickness)
.workplane(offset=self.shoulder_to_waist)
.rect(self.shoulder_width, self.torso_thickness)
.loft(combine=True)
.union(head_neck.translate((0, 0, self.shoulder_to_waist)))
)
return result.translate((0, self.torso_thickness / 2, 0))
BASE_POS_X = 70.0
BASE_POS_Y = 100.0
@dataclass(kw_only=True)
class Harness(Model):
thickness: float = 25.4 / 8
width: float = 200.0
height: float = 304.8
fillet: float = 10.0
wing_base_pos: list[tuple[str, float, float]] = field(default_factory=lambda: [
("r1", BASE_POS_X, BASE_POS_Y),
("l1", -BASE_POS_X, BASE_POS_Y),
("r2", BASE_POS_X, 0),
("l2", -BASE_POS_X, 0),
("r3", BASE_POS_X, -BASE_POS_Y),
("l3", -BASE_POS_X, -BASE_POS_Y),
])
root_joint: RootJoint = field(default_factory=lambda: RootJoint())
mannequin: Mannequin = Mannequin()
def __post_init__(self):
super().__init__(name="harness")
@submodel(name="bridge-pair-horizontal")
def bridge_pair_horizontal(self) -> MountingBox:
return self.root_joint.bridge_pair_horizontal(centre_dx=BASE_POS_X * 2)
@submodel(name="bridge-pair-vertical")
def bridge_pair_vertical(self) -> MountingBox:
return self.root_joint.bridge_pair_vertical(centre_dy=BASE_POS_Y)
@target(name="profile", kind=TargetKind.DXF)
def profile(self) -> Cq.Sketch:
"""
Creates the harness shape
"""
w, h = self.width / 2, self.height / 2
sketch = (
Cq.Sketch()
.polygon([
(w, h),
(w, -h),
(-w, -h),
(-w, h),
#(0.7 * w, h),
#(w, 0),
#(0.7 * w, -h),
#(0.7 * -w, -h),
#(-w, 0),
#(0.7 * -w, h),
])
#.rect(self.harness_width, self.harness_height)
.vertices()
.fillet(self.fillet)
)
for tag, x, y in self.wing_base_pos:
conn = [(px + x, py + y) for px, py in self.root_joint.corner_pos()]
sketch = (
sketch
.push(conn)
.tag(tag)
.circle(self.root_joint.corner_hole_diam / 2, mode='s')
.reset()
)
return sketch
def surface(self) -> Cq.Workplane:
"""
Creates the harness shape
"""
result = (
Cq.Workplane('XZ')
.placeSketch(self.profile())
.extrude(self.thickness)
)
result.faces(">Y").tag("mount")
plane = result.faces(">Y").workplane()
for tag, x, y in self.wing_base_pos:
conn = [(px + x, py + y) for px, py
in self.root_joint.corner_pos()]
for i, (px, py) in enumerate(conn):
plane.moveTo(px, py).tagPlane(f"{tag}_{i}")
return result
def add_root_joint_constraint(
self,
a: Cq.Assembly,
harness_tag: str,
joint_tag: str,
mount_tag: str):
for i in range(4):
a.constrain(f"{harness_tag}?{mount_tag}_{i}", f"{joint_tag}/parent?h{i}", "Point")
@assembly()
def assembly(self, with_root_joint: bool = False) -> Cq.Assembly:
harness = self.surface()
mannequin_z = self.mannequin.shoulder_to_waist * 0.6
result = (
Cq.Assembly()
.addS(
harness, name="base",
material=Material.WOOD_BIRCH,
role=Role.STRUCTURE)
.constrain("base", "Fixed")
.addS(
self.mannequin.generate(),
name="mannequin",
role=Role.FIXTURE,
loc=Cq.Location((0, -self.thickness, -mannequin_z), (0, 0, 1), 180))
.constrain("mannequin", "Fixed")
)
bridge_h = self.bridge_pair_horizontal().generate()
for i in [1,2,3]:
name = f"r{i}l{i}_bridge"
(
result
.addS(
bridge_h, name=name,
role=Role.FIXTURE,
material=Material.WOOD_BIRCH,
)
.constrain(f"{name}?conn0_rev", f"base?r{i}_1", "Point")
.constrain(f"{name}?conn1_rev", f"base?l{i}_0", "Point")
.constrain(f"{name}?conn2_rev", f"base?l{i}_3", "Point")
.constrain(f"{name}?conn3_rev", f"base?r{i}_2", "Point")
)
bridge_v = self.bridge_pair_vertical().generate()
(
result
.addS(bridge_v, name="r1_bridge", role=Role.FIXTURE, material=Material.WOOD_BIRCH)
.constrain("r1_bridge?conn0_rev", "base?r1_3", 'Plane')
.constrain("r1_bridge?conn1_rev", "base?r2_0", 'Plane')
.addS(bridge_v, name="r2_bridge", role=Role.FIXTURE, material=Material.WOOD_BIRCH)
.constrain("r2_bridge?conn0_rev", "base?r2_3", 'Plane')
.constrain("r2_bridge?conn1_rev", "base?r3_0", 'Plane')
.addS(bridge_v, name="l1_bridge", role=Role.FIXTURE, material=Material.WOOD_BIRCH)
.constrain("l1_bridge?conn0_rev", "base?l1_2", 'Plane')
.constrain("l1_bridge?conn1_rev", "base?l2_1", 'Plane')
.addS(bridge_v, name="l2_bridge", role=Role.FIXTURE, material=Material.WOOD_BIRCH)
.constrain("l2_bridge?conn0_rev", "base?l2_2", 'Plane')
.constrain("l2_bridge?conn1_rev", "base?l3_1", 'Plane')
)
if with_root_joint:
for name in ["l1", "l2", "l3", "r1", "r2", "r3"]:
result.addS(
self.root_joint.assembly(), name=name,
role=Role.PARENT,
material=Material.PLASTIC_PLA)
self.add_root_joint_constraint(result, "base", name, name)
result.solve()
return result

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,130 @@
import unittest
import cadquery as Cq
import nhf.touhou.houjuu_nue as M
import nhf.touhou.houjuu_nue.joints as MJ
import nhf.touhou.houjuu_nue.electronics as ME
from nhf.checks import pairwise_intersection
class TestElectronics(unittest.TestCase):
def test_actuator_length(self):
self.assertAlmostEqual(
ME.LINEAR_ACTUATOR_50.conn_length, 103.9
)
self.assertAlmostEqual(
ME.LINEAR_ACTUATOR_30.conn_length, 83.9
)
self.assertAlmostEqual(
ME.LINEAR_ACTUATOR_10.conn_length, 64.5
)
self.assertAlmostEqual(
ME.LINEAR_ACTUATOR_21.conn_length, 76.0
)
def test_flexor(self):
flexor = ME.Flexor(
motion_span=60,
)
self.assertAlmostEqual(
flexor.target_length_at_angle(0),
flexor.actuator.stroke_length + flexor.actuator.conn_length)
self.assertAlmostEqual(
flexor.target_length_at_angle(flexor.motion_span),
flexor.actuator.conn_length)
class TestJoints(unittest.TestCase):
def test_shoulder_collision_of_torsion_joint(self):
j = MJ.ShoulderJoint()
assembly = j.torsion_joint.rider_track_assembly()
self.assertEqual(pairwise_intersection(assembly), [])
def test_shoulder_collision_0(self):
j = MJ.ShoulderJoint()
assembly = j.assembly()
self.assertEqual(pairwise_intersection(assembly), [])
def test_shoulder_align(self):
j = MJ.ShoulderJoint()
a = j.assembly()
l_t_c0 = a.get_abs_location("parent_top/lip?conn0")
l_b_c0 = a.get_abs_location("parent_bot/lip?conn0")
v = l_t_c0 - l_b_c0
self.assertAlmostEqual(v.x, 0)
self.assertAlmostEqual(v.y, 0)
def test_shoulder_joint_dist(self):
"""
Tests the arm radius
"""
j = MJ.ShoulderJoint()
for deflection in [0, 40, j.angle_max_deflection]:
with self.subTest(deflection=deflection):
a = j.assembly(deflection=deflection)
# Axle
o = a.get_abs_location("parent_top/track?spring")
l_c1 = a.get_abs_location("parent_top/lip?conn0")
l_c2= a.get_abs_location("parent_top/lip?conn1")
v_c = 0.5 * ((l_c1 - o) + (l_c2 - o))
v_c.z = 0
self.assertAlmostEqual(v_c.Length, j.parent_lip_ext)
def test_disk_collision_0(self):
j = MJ.DiskJoint()
assembly = j.assembly(angle=0)
self.assertEqual(pairwise_intersection(assembly), [])
def test_disk_collision_mid(self):
j = MJ.DiskJoint()
assembly = j.assembly(angle=j.movement_angle / 2)
self.assertEqual(pairwise_intersection(assembly), [])
def test_disk_collision_max(self):
j = MJ.DiskJoint()
assembly = j.assembly(angle=j.movement_angle)
self.assertEqual(pairwise_intersection(assembly), [])
def test_elbow_joint_dist(self):
"""
Tests the arm radius
"""
j = MJ.ElbowJoint()
for angle in [0, 10, 20, j.disk_joint.movement_angle]:
with self.subTest(angle=angle):
a = j.assembly(angle=angle)
o = a.get_abs_location("child/disk?mate_bot")
l_c1 = a.get_abs_location("child/lip?conn_top0")
l_c2 = a.get_abs_location("child/lip?conn_bot0")
v_c = 0.5 * ((l_c1 - o) + (l_c2 - o))
v_c.z = 0
self.assertAlmostEqual(v_c.Length, j.child_arm_radius)
l_p1 = a.get_abs_location("parent_upper/lip?conn_top0")
l_p2 = a.get_abs_location("parent_upper/lip?conn_bot0")
v_p = 0.5 * ((l_p1 - o) + (l_p2 - o))
v_p.z = 0
self.assertAlmostEqual(v_p.Length, j.parent_arm_radius)
class Test(unittest.TestCase):
def test_hs_joint_parent(self):
p = M.Parameters()
obj = p.harness.hs_joint_parent()
self.assertIsInstance(obj.val().solids(), Cq.Solid, msg="H-S joint must be in one piece")
def test_wings_assembly(self):
p = M.Parameters()
p.wings_harness_assembly()
def test_trident_assembly(self):
p = M.Parameters()
assembly = p.trident.assembly()
bbox = assembly.toCompound().BoundingBox()
length = bbox.zlen
self.assertGreater(length, 1300)
self.assertLess(length, 1700)
#def test_assemblies(self):
# p = M.Parameters()
# p.check_all()
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,88 @@
import math
from dataclasses import dataclass, field
import cadquery as Cq
from nhf import Material, Role
from nhf.parts.handle import Handle, BayonetMount
from nhf.build import Model, target, assembly
import nhf.utils
@dataclass
class Trident(Model):
handle: Handle = field(default_factory=lambda: Handle(
diam=38,
diam_inner=38-2 * 25.4/8,
diam_connector_internal=18,
simplify_geometry=False,
mount=BayonetMount(n_pin=3),
))
terminal_height: float = 80
terminal_hole_diam: float = 24
terminal_bottom_thickness: float = 10
segment_length: float = 24 * 25.4
@target(name="handle-connector")
def handle_connector(self):
return self.handle.connector()
@target(name="handle-insertion")
def handle_insertion(self):
return self.handle.insertion()
@target(name="proto-handle-terminal-connector", prototype=True)
def proto_handle_connector(self):
return self.handle.one_side_connector(height=15)
@target(name="handle-terminal-connector")
def handle_terminal_connector(self):
result = self.handle.one_side_connector(height=self.terminal_height)
#result.faces("<Z").circle(radius=25/2).cutThruAll()
h = self.terminal_height + self.handle.insertion_length - self.terminal_bottom_thickness
result = result.faces(">Z").hole(self.terminal_hole_diam, depth=h)
return result
@assembly()
def assembly(self):
def segment():
return self.handle.segment(self.segment_length)
terminal = (
self.handle
.one_side_connector(height=self.terminal_height)
.faces(">Z")
.hole(15, self.terminal_height + self.handle.insertion_length - 10)
)
mat_c = Material.PLASTIC_PLA
mat_i = Material.RESIN_TOUGH_1500
mat_s = Material.ACRYLIC_BLACK
role_i = Role.CONNECTION
role_c = Role.CONNECTION
role_s = Role.STRUCTURE
a = (
Cq.Assembly()
.addS(self.handle.insertion(), name="i0",
material=mat_i, role=role_i)
.constrain("i0", "Fixed")
.addS(segment(), name="s1",
material=mat_s, role=role_s)
.constrain("i0?rim", "s1?mate1", "Plane", param=0)
.addS(self.handle.insertion(), name="i1",
material=mat_i, role=role_i)
.addS(self.handle.connector(), name="c1",
material=mat_c, role=role_c)
.addS(self.handle.insertion(), name="i2",
material=mat_i, role=role_i)
.constrain("s1?mate2", "i1?rim", "Plane", param=0)
.constrain("i1?mate", "c1?mate1", "Plane")
.constrain("i2?mate", "c1?mate2", "Plane")
.addS(segment(), name="s2",
material=mat_s, role=role_s)
.constrain("i2?rim", "s2?mate1", "Plane", param=0)
.addS(self.handle.insertion(), name="i3",
material=mat_i, role=role_i)
.constrain("s2?mate2", "i3?rim", "Plane", param=0)
.addS(self.handle.one_side_connector(), name="head",
material=mat_c, role=role_c)
.constrain("i3?mate", "head?mate", "Plane")
.addS(terminal, name="terminal",
material=mat_c, role=role_c)
.constrain("i0?mate", "terminal?mate", "Plane")
)
return a.solve()

File diff suppressed because it is too large Load Diff

3861
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -8,21 +8,13 @@ readme = "README.md"
[tool.poetry.dependencies]
python = "^3.10"
cadquery = {git = "https://github.com/CadQuery/cadquery.git"}
#build123d = "^0.5.0"
numpy = ">=2,<3"
build123d = "^0.5.0"
numpy = "^1.26.4"
colorama = "^0.4.6"
# cadquery dependency
multimethod = "^1.12"
scipy = "^1.14.0"
typish = "^1.9.3"
[tool.poetry.group.dev.dependencies]
cq-editor = {git = "https://github.com/CadQuery/CQ-editor.git"}
pyqt5 = "^5.15.11"
logbook = "^1.8.0"
spyder = "^5"
pyqtgraph = "^0.13.7"
[build-system]
requires = ["poetry-core"]