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
26 changed files with 4907 additions and 9203 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

@ -90,7 +90,7 @@ class Target:
x = (
Cq.Workplane()
.add(x._faces)
.add(x.wires)
.add(x._wires)
.add(x._edges)
)
assert isinstance(x, Cq.Workplane)
@ -214,7 +214,7 @@ class Submodel:
def write_to(self, obj, path: str):
x = self._method(obj)
assert isinstance(x, Model), f"Unexpected type: {type(x)}"
x.build_all(path, prefix=False)
x.build_all(path)
@classmethod
def methods(cls, subject):
@ -271,17 +271,11 @@ class Model:
total += 1
return total
def build_all(
self,
output_dir: Union[Path, str] = "build",
prefix: bool = True,
verbose=1):
def build_all(self, output_dir: Union[Path, str] = "build", verbose=1):
"""
Build all targets in this model and write the results to file
"""
output_dir = Path(output_dir)
if prefix:
output_dir = output_dir / self.name
targets = Target.methods(self)
for t in targets.values():
file_name = t.file_name

View File

@ -84,7 +84,6 @@ class Material(Enum):
ACRYLIC_TRANSLUSCENT = 1.18, _color('ivory2', 0.8)
ACRYLIC_TRANSPARENT = 1.18, _color('ghostwhite', 0.5)
STEEL_SPRING = 7.8, _color('gray', 0.8)
METAL_BRASS = 8.5, _color('gold1', 0.8)
def __init__(self, density: float, color: Cq.Color):
self.density = density
@ -117,9 +116,6 @@ def add_with_material_role(
Cq.Assembly.addS = add_with_material_role
def color_by_material(self: Cq.Assembly) -> Cq.Assembly:
"""
Set colours in an assembly by material
"""
for _, a in self.traverse():
if KEY_MATERIAL not in a.metadata:
continue
@ -127,9 +123,6 @@ def color_by_material(self: Cq.Assembly) -> Cq.Assembly:
return self
Cq.Assembly.color_by_material = color_by_material
def color_by_role(self: Cq.Assembly, avg: bool = True) -> Cq.Assembly:
"""
Set colours in an assembly by role
"""
for _, a in self.traverse():
if KEY_ROLE not in a.metadata:
continue

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)
)

View File

View File

@ -1,146 +0,0 @@
from dataclasses import dataclass
import cadquery as Cq
from nhf import Material, Role
from nhf.build import Model, target, assembly, TargetKind, submodel
from nhf.parts.box import MountingBox, Hole
from nhf.parts.electronics import ArduinoUnoR3
import nhf.utils
@dataclass
class LightPanel(Model):
# Dimensions of the base panel
length: float = 300.0
width: float = 200.0
attach_height: float = 20.0
attach_diam: float = 8.0
attach_depth: float = 12.7
grid_height: float = 20.0
grid_top_height: float = 5.0
# Distance from grid to edge
grid_margin: float = 20.0
# Number of holes in each row of the grid
grid_holes: int = 9
grid_layers: int = 6
grid_hole_width: float = 15.0
base_thickness: float = 25.4/16
grid_thickness: float = 25.4/4
base_material: Material = Material.WOOD_BIRCH
grid_material: Material = Material.ACRYLIC_TRANSPARENT
controller: ArduinoUnoR3 = ArduinoUnoR3()
def __post_init__(self):
assert self.grid_holes >= 2
super().__init__(name="light-panel")
@property
def grid_spacing_y(self) -> float:
return (self.width - 2 * self.grid_margin - self.grid_thickness) / (self.grid_layers - 1)
@target(name="grid", kind=TargetKind.DXF)
def grid_profile(self):
w = self.length - self.grid_margin * 2
h = self.grid_height + self.grid_top_height
# The width of one hole (w0) satisfies
# n * w0 + (n+1) t = w
# where t is the thickness of the edge
n = self.grid_holes
w0 = self.grid_hole_width
t = (w - n * w0) / (n + 1)
# The spacing is such that the first and last holes are a distance `margin`
# away from the edges, so it satisfies
# t + w0/2 + (n-1) * s + w0/2 + t = w
step = (w - t*2 - w0) / (n - 1)
return (
Cq.Sketch()
.push([(0, h/2)])
.rect(w, h)
.push([
(i * step + t + w0/2 - w/2, self.grid_height/2)
for i in range(0, n)
])
.rect(w0, self.grid_height, mode='s')
)
def grid(self) -> Cq.Workplane:
return (
Cq.Workplane('XY')
.placeSketch(self.grid_profile())
.extrude(self.grid_thickness)
)
@submodel(name="base")
def base(self) -> MountingBox:
xshift = self.length / 2 - self.controller.length - self.grid_margin / 2
yshift = self.grid_margin / 2
holes = [
Hole(
x=x + xshift, y=y + yshift,
diam=self.controller.hole_diam,
tag=f"controller_conn{i}",
)
for i, (x, y) in enumerate(self.controller.holes)
]
return MountingBox(
holes=holes,
hole_diam=self.controller.hole_diam,
length=self.length,
width=self.width,
centred=(True, False),
thickness=self.base_thickness,
)
@target(name="attachment")
def attachment(self) -> Cq.Workplane:
l = self.length / 2
w = self.width / 2
return (
Cq.Workplane('XY')
.box(
l, w, self.attach_height,
centered=(True, True, False),
)
.faces(">Z")
.hole(self.attach_diam, self.attach_depth)
)
def assembly(self) -> Cq.Assembly:
assembly = (
Cq.Assembly()
.addS(
self.base().generate(),
name="base",
role=Role.STRUCTURE,
material=self.base_material,
)
)
# Grid thickness t is fixed, so the spacing of the grid satisfies
# margin + t + (n-1) * spacing + margin = width
spacing = self.grid_spacing_y
shift = self.grid_margin + self.grid_thickness / 2
for i in range(self.grid_layers):
assembly = assembly.addS(
self.grid(),
name=f"grid_{i}",
role=Role.STRUCTURE,
material=self.grid_material,
loc=Cq.Location(0, spacing * i + shift, self.base_thickness, 90, 0, 0),
)
return assembly
if __name__ == '__main__':
import sys
p = LightPanel()
print(p.grid_spacing_y)
if len(sys.argv) == 1:
p.build_all()
sys.exit(0)

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

View File

@ -1,40 +0,0 @@
from dataclasses import dataclass, field
import cadquery as Cq
from nhf.build import Model, TargetKind, target, assembly, submodel
import nhf.touhou.shiki_eiki.rod as MR
import nhf.touhou.shiki_eiki.crown as MC
import nhf.touhou.shiki_eiki.epaulette as ME
import nhf.utils
@dataclass
class Parameters(Model):
rod: MR.Rod = field(default_factory=lambda: MR.Rod())
crown: MC.Crown = field(default_factory=lambda: MC.Crown())
epaulette_ze: ME.Epaulette = field(default_factory=lambda: ME.Epaulette(side="ze"))
epaulette_hi: ME.Epaulette = field(default_factory=lambda: ME.Epaulette(side="hi"))
def __post_init__(self):
super().__init__(name="shiki-eiki")
@submodel(name="rod")
def submodel_rod(self) -> Model:
return self.rod
@submodel(name="crown")
def submodel_crown(self) -> Model:
return self.crown
@submodel(name="epaulette_ze")
def submodel_epaulette_ze(self) -> Model:
return self.epaulette_ze
@submodel(name="epaulette_hi")
def submodel_epaulette_hi(self) -> Model:
return self.epaulette_hi
if __name__ == '__main__':
import sys
p = Parameters()
if len(sys.argv) == 1:
p.build_all()
sys.exit(0)

View File

@ -1,758 +0,0 @@
from nhf import Material, Role
from nhf.build import Model, target, assembly, TargetKind
import nhf.utils
import math
from typing import Optional
from dataclasses import dataclass, field
from enum import Enum
import cadquery as Cq
class AttachPoint(Enum):
DOVETAIL_IN = 1
DOVETAIL_OUT = 2
NONE = 3
# Inset slot for front surface attachment j
SLOT = 4
@dataclass
class Crown(Model):
facets: int = 5
# Lower circumference
base_circ: float = 538.0
# Upper circumference, at the middle
tilt_circ: float = 640.0
front_base_circ: float = (640.0 + 538.0) / 2
# Total height
height: float = 120.0
# Front guard has a wing that inserts into the side guards.
front_wing_angle: float = 9.0
front_wing_dh: float = 40.0
front_wing_height: float = 20.0
margin: float = 10.0
thickness: float = 0.4 # 26 Gauge
side_guard_thickness: float = 15.0
side_guard_channel_radius: float = 90
side_guard_channel_height: float = 10
side_guard_hole_height: float = 15.0
side_guard_hole_diam: float = 1.5
side_guard_dovetail_height: float = 30.0
side_guard_slot_width: float = 22.0
side_guard_slot_angle: float = 18.0
# brass insert thickness
slot_thickness: float = 2.0
slot_width: float = 20.0
slot_tilt: float = 60
material: Material = Material.METAL_BRASS
material_side: Material = Material.PLASTIC_PLA
def __post_init__(self):
super().__init__(name="crown")
assert self.tilt_circ > self.base_circ
assert self.facet_width_upper / 2 > self.height / 2, "Top angle must be > 90 degrees"
assert self.side_guard_channel_radius > self.radius_lower
assert self.front_wing_angle < 180 / self.facets
assert self.front_wing_dh + self.front_wing_height < self.height
assert self.slot_phi < 2 * math.pi / self.facets
@property
def facet_width_lower(self):
return self.base_circ / self.facets
@property
def facet_width_upper(self):
return self.tilt_circ / self.facets
@property
def radius_lower(self):
return self.base_circ / (2 * math.pi)
@property
def radius_middle(self):
return self.tilt_circ / (2 * math.pi)
@property
def radius_upper(self):
return (self.tilt_circ + (self.tilt_circ - self.base_circ)) / (2 * math.pi)
@property
def radius_lower_front(self):
return self.front_base_circ / (2 * math.pi)
@property
def radius_middle_front(self):
return self.radius_lower_front + (self.radius_middle - self.radius_lower)
@property
def radius_upper_front(self):
return self.radius_lower_front + (self.radius_upper - self.radius_lower)
@property
def slot_r0(self):
return self.radius_lower + self.thickness / 2
@property
def slot_r1(self):
return self.radius_upper + self.thickness / 2
@property
def slot_h0(self) -> float:
"""
Phantom height formed by similar triangle, i.e. h0 in
(h0 + h) / r2 = h0 / r1
"""
rat = self.slot_r0 / (self.slot_r1 - self.slot_r0)
return self.height * rat
@property
def slot_outer_h0(self):
rat = (self.slot_r0 + self.side_guard_thickness) / (self.slot_r1 - self.slot_r0)
return self.height * rat
@property
def slot_theta(self) -> float:
"""
Cone tilt, related to other quantities by
h0 = r1 * cot theta
"""
h = self.height
return math.atan(self.slot_r0 / (self.height + self.slot_h0))
@property
def slot_phi(self) -> float:
"""
When a slice of the crown is expanded (via Gauss's Theorema Egregium),
it does not form a full circle. phi is the angle of one of the slices.
Note that on the cone itself, the angular slice is `2 pi / n` which `n`
is the number of sides.
"""
arc = self.slot_r0 * math.pi * 2 / self.facets
rho = self.slot_h0 / math.cos(self.slot_theta)
return arc / rho
def profile_base(self) -> Cq.Sketch:
# Generate a conical pentagonal shape
y0 = self.slot_h0 / math.cos(self.slot_theta)
yh = (self.height/2 + self.slot_h0) / math.cos(self.slot_theta)
yq = (self.height*3/4 + self.slot_h0) / math.cos(self.slot_theta)
y1 = (self.height + self.slot_h0) / math.cos(self.slot_theta)
phi2 = self.slot_phi / 2
return (
Cq.Sketch()
.segment(
(y0 * math.sin(phi2), y0 * (-1 + math.cos(phi2))),
(yh * math.sin(phi2), -y0 + yh * math.cos(phi2)),
)
.arc(
(yh * math.sin(phi2), -y0 + yh * math.cos(phi2)),
(yq * math.sin(phi2/2), -y0 + yq * math.cos(phi2/2)),
(0, y1 - y0),
)
.arc(
(-yh * math.sin(phi2), -y0 + yh * math.cos(phi2)),
(-yq * math.sin(phi2/2), -y0 + yq * math.cos(phi2/2)),
(0, y1 - y0),
)
.segment(
(-y0 * math.sin(phi2), y0 * (-1 + math.cos(phi2))),
(-yh * math.sin(phi2), -y0 + yh * math.cos(phi2)),
)
.arc(
(y0 * math.sin(phi2), -y0 + y0 * math.cos(phi2)),
(0, 0),
(-y0 * math.sin(phi2), y0 * (-1 + math.cos(phi2))),
)
.assemble()
)
@target(name="eye", kind=TargetKind.DXF)
def profile_eye(self) -> Cq.Sketch:
"""
deprecated
"""
dy = self.facet_width_upper * 0.1
y_tip = self.height - self.margin
eye = (
Cq.Sketch()
.segment(
(0, y_tip),
(dy, y_tip - dy),
)
.segment(
(0, y_tip),
(-dy, y_tip - dy),
)
.bezier([
(dy, y_tip - dy),
(dy/2, y_tip - dy*.6),
(dy/4, y_tip - dy/2),
(0, y_tip - dy/2),
])
.bezier([
(0, y_tip - dy/2),
(-dy/4, y_tip - dy/2),
(-dy/2, y_tip - dy*.6),
(-dy, y_tip - dy),
])
.assemble()
)
return eye
@target(name="dot", kind=TargetKind.DXF)
def profile_dot(self) -> Cq.Sketch:
return (
Cq.Sketch()
.circle(self.margin / 2)
)
def profile_front_wing(self, mirror: bool) -> Cq.Sketch:
"""
These two wings help the front profile attach
"""
hw = self.front_wing_height / math.cos(self.slot_theta)
hw0 = (self.front_wing_dh + self.slot_h0) / math.cos(self.slot_theta)
hw1 = hw0 + hw
y0 = self.slot_h0 / math.cos(self.slot_theta)
# Calculate angle of wing analogously to `this.slot_phi`. This arc's
# radius is hw0.
wing_arc = self.slot_r0 * math.radians(self.front_wing_angle)
phi_w = wing_arc / hw0
sign = -1 if mirror else 1
phi2 = self.slot_phi / 2
return (
Cq.Sketch()
.segment(
(sign * hw0 * math.sin(phi2), -y0 + hw0 * math.cos(phi2)),
(sign * hw1 * math.sin(phi2), -y0 + hw1 * math.cos(phi2)),
)
.segment(
(sign * hw0 * math.sin(phi2+phi_w), -y0 + hw0 * math.cos(phi2+phi_w)),
(sign * hw1 * math.sin(phi2+phi_w), -y0 + hw1 * math.cos(phi2+phi_w)),
)
.arc(
(sign * hw0 * math.sin(phi2), -y0 + hw0 * math.cos(phi2)),
(sign * hw0 * math.sin(phi2+phi_w/2), -y0 + hw0 * math.cos(phi2+phi_w/2)),
(sign * hw0 * math.sin(phi2+phi_w), -y0 + hw0 * math.cos(phi2+phi_w)),
)
.arc(
(sign * hw1 * math.sin(phi2), -y0 + hw1 * math.cos(phi2)),
(sign * hw1 * math.sin(phi2+phi_w/2), -y0 + hw1 * math.cos(phi2+phi_w/2)),
(sign * hw1 * math.sin(phi2+phi_w), -y0 + hw1 * math.cos(phi2+phi_w)),
)
.assemble()
)
@target(name="front", kind=TargetKind.DXF)
def profile_front(self) -> Cq.Sketch:
"""
Front profile slots into holes on the side guards
"""
profile_base = (
self.profile_base()
.boolean(self.profile_front_wing(False), mode='a')
.boolean(self.profile_front_wing(True), mode='a')
)
dx_l = self.facet_width_lower
dx_u = self.facet_width_upper
dy = self.height
window_length = dy / 5
window_height = self.margin / 2
window = (
Cq.Sketch()
.rect(window_length, window_height)
)
window_p1 = Cq.Location.from2d(
dx_u/2 - self.margin - window_length * 0.4,
dy/2 + self.margin/2,
math.degrees(math.atan2(dy/2, -dx_u/2) * 0.95),
)
window_p2 = Cq.Location.from2d(
dx_l/2 - self.margin + window_length * 0.15,
window_length/2 + self.margin,
math.degrees(math.atan2(dy/2, (dx_u-dx_l)/2)),
)
# Carve the scale
z = dy * 1/32 # "Pen" Thickness
scale_pan_x = dx_l / 2 * 0.6
scale_pan_y = dy / 2 * 0.7
pan_dx = dx_l * 1/4
pan_dy = dy * 1/16
scale_pan = (
Cq.Sketch()
.arc(
(- pan_dx/2, pan_dy),
(0, 0),
(+ pan_dx/2, pan_dy),
)
.segment(
(+pan_dx/2, pan_dy),
(+pan_dx/2 - z, pan_dy),
)
.arc(
(-pan_dx/2 + z, pan_dy),
(0, z),
(+pan_dx/2 - z, pan_dy),
)
.segment(
(-pan_dx/2, pan_dy),
(-pan_dx/2 + z, pan_dy),
)
.assemble()
)
loc_scale_pan = Cq.Location.from2d(scale_pan_x, scale_pan_y)
loc_scale_pan2 = Cq.Location.from2d(-scale_pan_x, scale_pan_y)
scale_base_y = dy / 2 * 0.36
scale_base_x = dx_l / 10
assert scale_base_y < scale_pan_y
assert scale_base_x < scale_pan_x
scale_body = (
Cq.Sketch()
.arc(
(scale_pan_x, scale_pan_y),
(0, scale_base_y),
(-scale_pan_x, scale_pan_y),
)
.segment(
(-scale_pan_x, scale_pan_y),
(-scale_pan_x+z, scale_pan_y+z),
)
.arc(
(scale_pan_x - z, scale_pan_y+z),
(0, scale_base_y + z),
(-scale_pan_x + z, scale_pan_y+z),
)
.segment(
(scale_pan_x, scale_pan_y),
(scale_pan_x-z, scale_pan_y+z),
)
.assemble()
.polygon([
(scale_base_x, scale_base_y + z/2),
(scale_base_x, self.margin),
(scale_base_x-z, self.margin),
(scale_base_x-z, scale_base_y-z),
(-scale_base_x+z, scale_base_y-z),
(-scale_base_x+z, self.margin),
(-scale_base_x, self.margin),
(-scale_base_x, scale_base_y + z/2),
], mode='a')
)
# Needle
needle_y_top = dy - self.margin
needle_y_mid = dy * 0.7
needle_dx = scale_base_x * 2
y_shoulder = needle_y_mid - z * 2
needle = (
Cq.Sketch()
.segment(
(0, needle_y_mid),
(z, y_shoulder),
)
.segment(
(z, y_shoulder),
(z, scale_base_y),
)
.segment(
(z, scale_base_y),
(-z, scale_base_y),
)
.segment(
(-z, y_shoulder),
(-z, scale_base_y),
)
.segment(
(-z, y_shoulder),
(0, needle_y_mid),
)
.assemble()
)
z2 = z * 2
y1 = needle_y_mid + z2
needle_head = (
Cq.Sketch()
.segment(
(z, needle_y_mid),
(z, y1),
)
.segment(
(-z, needle_y_mid),
(-z, y1),
)
# Outer edge
.bezier([
(0, needle_y_top),
(0, (needle_y_top + needle_y_mid)/2),
(needle_dx, (needle_y_top + needle_y_mid)/2),
(z, needle_y_mid),
])
.bezier([
(0, needle_y_top),
(0, (needle_y_top + needle_y_mid)/2),
(-needle_dx, (needle_y_top + needle_y_mid)/2),
(-z, needle_y_mid),
])
# Inner edge
.bezier([
(0, needle_y_top - z2),
(0, (needle_y_top + needle_y_mid)/2),
(needle_dx-z2*2, (needle_y_top + needle_y_mid)/2),
(z, y1),
])
.bezier([
(0, needle_y_top - z2),
(0, (needle_y_top + needle_y_mid)/2),
(-needle_dx+z2*2, (needle_y_top + needle_y_mid)/2),
(-z, y1),
])
.assemble()
)
return (
profile_base
.boolean(window.moved(window_p1), mode='s')
.boolean(window.moved(window_p1.flip_x()), mode='s')
.boolean(window.moved(window_p2), mode='s')
.boolean(window.moved(window_p2.flip_x()), mode='s')
.boolean(scale_pan.moved(loc_scale_pan), mode='s')
.boolean(scale_pan.moved(loc_scale_pan2), mode='s')
.boolean(scale_body, mode='s')
.boolean(needle, mode='s')
.boolean(needle_head, mode='s')
.clean()
)
@target(name="side-guard", kind=TargetKind.DXF)
def profile_side_guard(self) -> Cq.Sketch:
dx = self.facet_width_lower / 2
dy = self.height
# Main control points
p_mid = Cq.Location.from2d(0, 0.5 * dy)
p_mid_v = Cq.Location.from2d(10/57 * dx, 0)
p_top1 = Cq.Location.from2d(0.408 * dx, 5/24 * dy)
p_top1_v = Cq.Location.from2d(0.13 * dx, 0)
p_top2 = Cq.Location.from2d(0.737 * dx, 0.255 * dy)
p_top2_c1 = p_top2 * Cq.Location.from2d(-0.105 * dx, 0.033 * dy)
p_top2_c2 = p_top2 * Cq.Location.from2d(-0.053 * dx, -0.09 * dy)
p_top3 = Cq.Location.from2d(0.929 * dx, 0.145 * dy)
p_top3_v = Cq.Location.from2d(0.066 * dx, 0.033 * dy)
p_top4 = Cq.Location.from2d(0.85 * dx, 0.374 * dy)
p_top4_v = Cq.Location.from2d(-0.053 * dx, 0.008 * dy)
p_top5 = Cq.Location.from2d(0.54 * dx, 0.349 * dy)
p_top5_c1 = p_top5 * Cq.Location.from2d(0.103 * dx, 0.017 * dy)
p_top5_c2 = p_top5 * Cq.Location.from2d(0.158 * dx, 0.034 * dy)
p_base_c = Cq.Location.from2d(1.5 * dx, 0.55 * dy)
y0 = self.slot_outer_h0 / math.cos(self.slot_theta)
phi2 = self.slot_phi / 2
p_base = Cq.Location.from2d(y0 * math.sin(phi2), -y0 + y0 * math.cos(phi2))
bezier_groups = [
[
p_base,
p_base_c,
p_top5_c2,
p_top5,
],
[
p_top5,
p_top5_c1,
p_top4 * p_top4_v,
p_top4,
],
[
p_top4,
p_top4 * p_top4_v.inverse.scale(4),
p_top3 * p_top3_v,
p_top3,
],
[
p_top3,
p_top3 * p_top3_v.inverse,
p_top2_c2,
p_top2,
],
[
p_top2,
p_top2_c1,
p_top1 * p_top1_v,
p_top1,
],
[
p_top1,
p_top1 * p_top1_v.inverse,
p_mid * p_mid_v,
p_mid,
],
]
sketch = (
Cq.Sketch()
.arc(
p_base.to2d_pos(),
(0, 0),
p_base.flip_x().to2d_pos(),
)
)
for bezier_group in bezier_groups:
sketch = (
sketch
.bezier([p.to2d_pos() for p in bezier_group])
.bezier([p.flip_x().to2d_pos() for p in bezier_group])
)
return sketch.assemble()
def side_guard_dovetail(self) -> Cq.Solid:
"""
Generates a dovetail coupling for the side guard
"""
dx = self.side_guard_thickness / 2
wire = Cq.Wire.makePolygon([
(dx * 0.5, 0),
(dx * 0.7, dx),
(-dx * 0.7, dx),
(-dx * 0.5, 0),
], close=True)
return Cq.Solid.extrudeLinear(
wire,
[],
(0,0,dx + self.side_guard_dovetail_height),
).moved((0, 0, -dx))
def side_guard_frontal_slot(self) -> Cq.Workplane:
angle = 360 / self.facets
inner_d = self.thickness / 2 - self.slot_thickness / 2
outer_d = self.thickness / 2 + self.slot_thickness / 2
outer = Cq.Solid.makeCone(
radius1=self.radius_lower_front + outer_d,
radius2=self.radius_upper_front + outer_d,
height=self.height,
angleDegrees=angle,
)
inner = Cq.Solid.makeCone(
radius1=self.radius_lower_front + inner_d,
radius2=self.radius_upper_front + inner_d,
height=self.height,
angleDegrees=angle,
)
shell = (
outer.cut(inner)
.rotate((0,0,0), (0,0,1), -angle/2)
)
# Generate the sector intersector
intersector = Cq.Solid.makeCylinder(
radius=self.radius_upper + self.side_guard_thickness,
height=self.front_wing_height,
angleDegrees=self.front_wing_angle,
).moved(Cq.Location(0,0,self.front_wing_dh,0,0,-self.front_wing_angle/2))
return shell * intersector
def side_guard(
self,
attach_left: AttachPoint,
attach_right: AttachPoint,
) -> Cq.Workplane:
"""
Constructs the side guard using a cone. Via Gauss's Theorema Egregium,
the surface of the cone can be deformed into a plane.
"""
angle_span = 360 / self.facets
outer = Cq.Solid.makeCone(
radius1=self.radius_lower + self.side_guard_thickness,
radius2=self.radius_upper + self.side_guard_thickness,
height=self.height,
angleDegrees=angle_span,
)
inner = Cq.Solid.makeCone(
radius1=self.radius_lower,
radius2=self.radius_upper,
height=self.height,
angleDegrees=angle_span,
)
shell = (outer - inner).rotate((0,0,0), (0,0,1), -angle_span/2)
dx = math.sin(math.radians(angle_span / 2)) * (self.radius_middle + self.side_guard_thickness)
profile = (
Cq.Workplane('YZ')
.polyline([
(0, self.height),
(-dx, self.height / 2),
(-dx, 0),
(dx, 0),
(dx, self.height / 2),
])
.close()
.extrude(self.radius_upper + self.side_guard_thickness)
.val()
)
#channel = (
# Cq.Solid.makeCylinder(
# radius=self.side_guard_channel_radius + 1.0,
# height=self.side_guard_channel_height,
# ) - Cq.Solid.makeCylinder(
# radius=self.side_guard_channel_radius,
# height=self.side_guard_channel_height,
# )
#)
result = shell * profile# - channel
# Create the downward slots
for sign in [-1, 1]:
slot_box = Cq.Solid.makeBox(
length=self.height,
width=self.slot_width,
height=self.slot_thickness,
).moved(
Cq.Location(-self.slot_thickness,-self.slot_width/2, -self.slot_thickness/2)
)
# keyhole for threads to stay in place
slot_cyl = Cq.Solid.makeCylinder(
radius=self.slot_thickness/2,
height=self.height,
pnt=(0,0,self.slot_thickness/2),
dir=(1,0,0),
)
slot = slot_box + slot_cyl
slot = slot.moved(
Cq.Location.rot2d(sign * self.side_guard_slot_angle) *
Cq.Location(self.radius_lower + self.side_guard_thickness/2, 0, 0) *
Cq.Location(0,0,0,0,-180 + self.slot_tilt,0)
)
result = result - slot
radius_attach = self.radius_lower + self.side_guard_thickness / 2
# tilt the dovetail by radius differential
angle_tilt = math.degrees(math.atan2(self.radius_middle - self.radius_lower, self.height / 2))
dovetail = self.side_guard_dovetail()
loc_dovetail_left = Cq.Location.rot2d(angle_span / 2) * Cq.Location(radius_attach, 0, 0, 0, angle_tilt, 0)
loc_dovetail_right = Cq.Location.rot2d(-angle_span / 2) * Cq.Location(radius_attach, 0, 0, 0, angle_tilt, 0)
angle_slot = 180 / self.facets - self.front_wing_angle / 2
match attach_left:
case AttachPoint.DOVETAIL_IN:
loc_dovetail_left *= Cq.Location.rot2d(180)
result = result - dovetail.moved(loc_dovetail_left)
case AttachPoint.DOVETAIL_OUT:
result = result + dovetail.moved(loc_dovetail_left)
case AttachPoint.SLOT:
result = result - self.side_guard_frontal_slot().moved(Cq.Location.rot2d(angle_slot))
case AttachPoint.NONE:
pass
match attach_right:
case AttachPoint.DOVETAIL_IN:
result = result - dovetail.moved(loc_dovetail_right)
case AttachPoint.DOVETAIL_OUT:
loc_dovetail_right *= Cq.Location.rot2d(180)
result = result + dovetail.moved(loc_dovetail_right)
case AttachPoint.SLOT:
result = result - self.side_guard_frontal_slot().moved(Cq.Location.rot2d(-angle_slot))
case AttachPoint.NONE:
pass
# Remove parts below the horizontal
cut_h = self.radius_lower
result -= Cq.Solid.makeCylinder(
radius=self.radius_lower + self.side_guard_thickness,
height=cut_h).moved((0,0,-cut_h))
return result
@target(name="side_guard_1", angularTolerance=0.01)
def side_guard_1(self) -> Cq.Workplane:
return self.side_guard(
attach_left=AttachPoint.SLOT,
attach_right=AttachPoint.DOVETAIL_IN,
)
@target(name="side_guard_2", angularTolerance=0.01)
def side_guard_2(self) -> Cq.Workplane:
return self.side_guard(
attach_left=AttachPoint.DOVETAIL_OUT,
attach_right=AttachPoint.DOVETAIL_IN,
)
@target(name="side_guard_3", angularTolerance=0.01)
def side_guard_3(self) -> Cq.Workplane:
return self.side_guard(
attach_left=AttachPoint.DOVETAIL_OUT,
attach_right=AttachPoint.DOVETAIL_IN,
)
@target(name="side_guard_4", angularTolerance=0.01)
def side_guard_4(self) -> Cq.Workplane:
return self.side_guard(
attach_left=AttachPoint.DOVETAIL_OUT,
attach_right=AttachPoint.SLOT,
)
def front_surrogate(self) -> Cq.Workplane:
"""
Create a surrogate cylindrical section structure for the front since we
cannot bend extrusions
"""
angle = 360 / 5
outer = Cq.Solid.makeCone(
radius1=self.radius_lower_front + self.thickness,
radius2=self.radius_upper_front + self.thickness,
height=self.height,
angleDegrees=angle,
)
inner = Cq.Solid.makeCone(
radius1=self.radius_lower_front,
radius2=self.radius_upper_front,
height=self.height,
angleDegrees=angle,
)
shell = (
outer.cut(inner)
.rotate((0,0,0), (0,0,1), -angle/2)
)
dx = math.sin(math.radians(angle / 2)) * self.radius_middle_front
profile = (
Cq.Workplane('YZ')
.polyline([
(0, self.height),
(-dx, self.height / 2),
(-dx, 0),
(dx, 0),
(dx, self.height / 2),
])
.close()
.extrude(self.radius_upper_front + self.side_guard_thickness)
.val()
)
return shell * profile
def assembly(self) -> Cq.Assembly:
"""
New assembly using conformal mapping on the cone.
"""
side_guards = [
self.side_guard_1(),
self.side_guard_2(),
self.side_guard_3(),
self.side_guard_4(),
]
a = Cq.Assembly()
for i,side_guard in enumerate(side_guards):
angle = -(i+1) * 360 / self.facets
a = a.addS(
side_guard,
name=f"side-{i}",
material=self.material_side,
loc=Cq.Location(rz=angle)
)
a.addS(
self.front_surrogate(),
name="front",
material=self.material,
)
return a

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,36 +0,0 @@
import math
from dataclasses import dataclass, field
from pathlib import Path
import cadquery as Cq
from nhf import Material, Role
from nhf.build import Model, target, assembly
import nhf.utils
@dataclass
class Epaulette(Model):
side: str
diam: float = 100.0
thickness_brass: float = 0.4 # 26 Gauge
thickness_fabric: float = 0.3
material: Material = Material.METAL_BRASS
def __post_init__(self):
super().__init__(name=f"epaulette-{self.side}")
def surface(self) -> Cq.Solid:
path = Path(__file__).resolve().parent / f"epaulette-{self.side}.dxf"
return (
Cq.importers.importDXF(path).wires().toPending().extrude(self.thickness_brass)
)
def assembly(self) -> Cq.Assembly:
assembly = (
Cq.Assembly()
.addS(
self.surface(),
name="surface",
material=self.material,
role=Role.DECORATION,
)
)
return assembly

View File

@ -1,587 +0,0 @@
import math
from dataclasses import dataclass, field
from typing import Tuple
import cadquery as Cq
from nhf import Material, Role
from nhf.build import Model, target, assembly, TargetKind
import nhf.utils
@dataclass
class Rod(Model):
width: float = 120.0
length: float = 550.0
length_tip: float = 100.0
width_tail: float = 60.0
margin: float = 10.0
thickness_top: float = 25.4 / 8
# The side which has mounted hinges must be thicker
thickness_side: float = 25.4 / 4
height_internal: float = 30.0
material_shell: Material = Material.WOOD_BIRCH
# Considering the glyph on the top ...
# counted from middle to the bottom
fac_bar_top: float = 0.1
# counted from bottom to top
fac_window_tsumi_bot: float = 0.63
fac_window_tsumi_top: float = 0.88
fac_window_footer_bot: float = 0.36
fac_window_footer_top: float = 0.6
# Considering the side ...
hinge_plate_pos: list[float] = field(default_factory=lambda: [0.1, 0.9])
hinge_plate_length: float = 30.0
hinge_hole_diam: float = 2.5
# Hole distance to axis
hinge_hole_axis_dist: float = 12.5 / 2
# Distance between holes
hinge_hole_sep: float = 15.89
# Consider the reference objects
ref_object_width: float = 50.0
ref_object_length: float = 50.0
def __post_init__(self):
super().__init__(name="rod")
self.loc_core = Cq.Location.from2d(self.length - self.length_tip, 0)
assert self.length_tip * 2 < self.length
#assert self.fac_bar_top + self.fac_window_tsumi_top < 1
assert self.fac_window_tsumi_bot < self.fac_window_tsumi_top
@property
def length_tail(self):
return self.length - self.length_tip
@property
def _reduced_tip_x(self):
return self.length_tip - self.margin
@property
def _reduced_y(self):
return self.width / 2 - self.margin
@property
def _reduced_tail_y(self):
return self.width_tail / 2 - self.margin
def profile_points(self) -> list[Tuple[str, Tuple[float, float]]]:
"""
Points in polygon line order, labaled
"""
return [
("tip", (self.length, 0)),
("mid_r", (self.length - self.length_tip, self.width/2)),
("bot_r", (0, self.width_tail / 2)),
("bot_l", (0, -self.width_tail / 2)),
("mid_l", (self.length - self.length_tip, -self.width/2)),
]
def _window_tip(self) -> Cq.Sketch:
dxh = self._reduced_tip_x
dy = self._reduced_y
return (
Cq.Sketch()
.segment(
(dxh, 0),
(dxh / 2, dy / 2),
)
.bezier([
(dxh / 2, dy / 2),
(dxh * 0.6, dy * 0.4),
(dxh * 0.6, -dy * 0.4),
(dxh / 2, -dy / 2),
])
.segment(
(dxh, 0),
)
.assemble()
.moved(self.loc_core.to2d_pos())
)
def _window_eye(self, refl: bool = False) -> Cq.Sketch:
sign = -1 if refl else 1
dxh = self._reduced_tip_x
xm = dxh * 0.45
dy = sign * self._reduced_y
fac = 0.05
p1 = Cq.Location.from2d(xm, sign * self.margin / 2)
p2 = Cq.Location.from2d(dxh * 0.1, sign * self.margin / 2)
p3 = Cq.Location.from2d(dxh * 0.15, dy * 0.55)
p4 = Cq.Location.from2d(dxh * 0.4, dy * 0.45)
d4 = Cq.Location.from2d(dxh * fac, -dy * fac)
return (
Cq.Sketch()
.segment(
p1.to2d_pos(),
p2.to2d_pos(),
)
.bezier([
p2.to2d_pos(),
(p2 * Cq.Location.from2d(0, dy * fac)).to2d_pos(),
(p3 * Cq.Location.from2d(-dxh * fac, -dy * fac)).to2d_pos(),
p3.to2d_pos(),
])
.bezier([
p3.to2d_pos(),
(p3 * Cq.Location.from2d(0, dy * fac)).to2d_pos(),
(p4 * d4.inverse).to2d_pos(),
p4.to2d_pos(),
])
.bezier([
p4.to2d_pos(),
(p4 * d4).to2d_pos(),
(p1 * Cq.Location.from2d(0, dy * fac)).to2d_pos(),
p1.to2d_pos(),
])
.assemble()
.moved(self.loc_core.to2d_pos())
)
def _window_bar(self) -> Cq.Sketch():
dxh = self._reduced_tip_x
dy = self._reduced_y
dyt = self._reduced_tail_y
dxt = self.length_tail
ext_fac = self.fac_bar_top
p_corner = Cq.Location.from2d(0, dy)
p_top = Cq.Location.from2d(0.3 * dxh, 0.7 * dy)
p_bot = Cq.Location.from2d(-ext_fac * dxt, dy + ext_fac * (dyt - dy))
p_top_int = p_corner * Cq.Location.from2d(.05 * dxh, -.2 * dy)
p_top_ctrl = Cq.Location.from2d(0, .3 * dy)
p_bot_int = p_corner * Cq.Location.from2d(-.15 * dxh, -.2 * dy)
p_bot_ctrl = Cq.Location.from2d(-.25 * dxh, .3 * dy)
return (
Cq.Sketch()
.segment(
p_corner.to2d_pos(),
p_top.to2d_pos(),
)
.segment(p_top_int.to2d_pos())
.bezier([
p_top_int.to2d_pos(),
p_top_ctrl.to2d_pos(),
p_top_ctrl.flip_y().to2d_pos(),
p_top_int.flip_y().to2d_pos(),
])
.segment(p_top.flip_y().to2d_pos())
.segment(p_corner.flip_y().to2d_pos())
.segment(p_bot.flip_y().to2d_pos())
.segment(p_bot_int.flip_y().to2d_pos())
.bezier([
p_bot_int.flip_y().to2d_pos(),
p_bot_ctrl.flip_y().to2d_pos(),
p_bot_ctrl.to2d_pos(),
p_bot_int.to2d_pos(),
])
.segment(p_bot.to2d_pos())
.segment(p_corner.to2d_pos())
.assemble()
.moved(self.loc_core.to2d_pos())
)
def _window_tsumi(self) -> Cq.Sketch:
dx = (self.fac_window_tsumi_top - self.fac_window_tsumi_bot) * self.length_tail
dy = 2 * self._reduced_y * 0.8
loc = Cq.Location(self.fac_window_tsumi_bot * self.length_tail, 0)
# Construction of the top part of the kanji
dx_top = dx * 0.3
x_top = dx - dx_top / 2
dy_top = dy
dy_eye = dy * 0.2
dy_border = (dy_top - 3 * dy_eye) / 4
# The skip must follow 3 * eye + 4 * border = dy_top
y_skip = dy_eye + dy_border
# Construction of the bottom part
x_bot = dx * 0.65
y3 = dy * 0.4
y2 = dy * 0.2
y1 = dy * 0.1
# x/y-centers of the legs
x_leg0 = x_bot / 14
dx_leg = x_bot / 7
y_leg = (y3 + y1) / 2
return (
Cq.Sketch()
.push([(x_top, 0)])
.rect(dx_top, dy_top)
.push([
(x_top, -y_skip),
(x_top, 0),
(x_top, y_skip),
])
.rect(dx_top / 3, dy_eye, mode='s')
# Construct the two sides
.push([
(x_bot / 2, (y2 + y1) / 2),
(x_bot / 2, -(y2 + y1) / 2),
])
.rect(x_bot, y2 - y1, mode='a')
.push([
(x_leg0 + dx_leg, y_leg),
(x_leg0 + 3 * dx_leg, y_leg),
(x_leg0 + 5 * dx_leg, y_leg),
(x_leg0 + dx_leg, -y_leg),
(x_leg0 + 3 * dx_leg, -y_leg),
(x_leg0 + 5 * dx_leg, -y_leg),
])
.rect(dx_leg, y3 - y1, mode='a')
.moved(loc)
)
def _window_footer(self) -> Cq.Sketch:
x_bot = self.fac_window_footer_bot * self.length_tail
dx = (self.fac_window_footer_top - self.fac_window_footer_bot) * self.length_tail
loc = Cq.Location(x_bot, 0)
dy = self._reduced_y * 0.8
# eyes
eye_y2 = dy * .5
eye_y1 = dy * .2
eye_width = eye_y2 - eye_y1
eye_x = dx - eye_width / 2
# bar polygon
bar_x0 = dx * 0.65
bar_dx = dx * 0.1
bar_x1 = bar_x0 + bar_dx
bar_x2 = bar_x0 + bar_dx * 2
bar_x3 = bar_x0 + bar_dx * 3
bar_y1 = dy * .75
assert bar_y1 > eye_y2
bar_y2 = dy * .9
assert bar_y1 < bar_y2
# Construction of the cross
cross_dx = dx * 0.7 / math.sqrt(2)
cross_dy = dy * 0.2
cross = (
Cq.Sketch()
.rect(cross_dx, cross_dy)
.rect(cross_dy, cross_dx, mode='a')
.moved(Cq.Location.from2d(dx * 0.5, 0, 45))
)
return (
Cq.Sketch()
# eyes
.push([
(eye_x, (eye_y1 + eye_y2)/2),
(eye_x, -(eye_y1 + eye_y2)/2),
])
.rect(eye_width, eye_width, mode='a')
# middle bar
.push([(0,0)])
.polygon([
(bar_x1, bar_y1),
(bar_x0, bar_y1),
(bar_x0, bar_y2),
(bar_x3, bar_y2),
(bar_x3, bar_y1),
(bar_x2, bar_y1),
(bar_x2, -bar_y1),
(bar_x3, -bar_y1),
(bar_x3, -bar_y2),
(bar_x0, -bar_y2),
(bar_x0, -bar_y1),
(bar_x1, -bar_y1),
], mode='a')
# cross
.boolean(cross, mode='a')
#.push([(0,0)])
#.rect(10, 10)
.moved(loc)
)
@target(name="bottom", kind=TargetKind.DXF)
def profile_bottom(self) -> Cq.Sketch:
return (
Cq.Sketch()
.polygon([p for _, p in self.profile_points()])
)
@target(name="top", kind=TargetKind.DXF)
def profile_top(self) -> Cq.Sketch:
return (
self.profile_bottom()
.boolean(self._window_tip(), mode='s')
.boolean(self._window_eye(True), mode='s')
.boolean(self._window_eye(False), mode='s')
.boolean(self._window_bar(), mode='s')
.boolean(self._window_tsumi(), mode='s')
.boolean(self._window_footer(), mode='s')
)
def surface_top(self) -> Cq.Workplane:
return (
Cq.Workplane('XY')
.placeSketch(self.profile_top())
.extrude(self.thickness_top)
)
def surface_bottom(self) -> Cq.Workplane:
surface = (
Cq.Workplane('XY')
.placeSketch(self.profile_bottom())
.extrude(self.thickness_top)
)
plane = surface.faces(">Z").workplane()
for (name, p) in self.profile_points():
plane.moveTo(*p).tagPlane(name)
return surface
# Properties of the side surfaces
@property
def length_edge_tip(self):
return math.sqrt(self.length_tip ** 2 + (self.width / 2) ** 2)
@property
def length_edge_tail(self):
dw = (self.width - self.width_tail) / 2
return math.sqrt(self.length_tail ** 2 + dw ** 2)
@property
def tip_incident_angle(self):
"""
Angle (measuring from vertical) at which the tip edge pieces must be
sanded in order to make them not collide into each other.
"""
return math.atan2(self.length_tip, self.width / 2)
@property
def shoulder_incident_angle(self) -> float:
angle_tip = math.atan2(self.width / 2, self.length_tip)
angle_tail = math.atan2((self.width - self.width_tail) / 2, self.length_tail)
return (angle_tip + angle_tail) / 2
@target(name="ref-tip")
def ref_tip(self) -> Cq.Workplane:
angle = self.tip_incident_angle
w = self.ref_object_width
drop = math.sin(angle) * w
profile = (
Cq.Sketch()
.polygon([
(0, 0),
(0, w),
(w, w),
(w - drop, 0),
])
)
return (
Cq.Workplane()
.placeSketch(profile)
.extrude(self.ref_object_length)
)
@target(name="ref-shoulder")
def ref_shoulder(self) -> Cq.Workplane:
angle = self.shoulder_incident_angle
w = self.ref_object_width
drop = math.sin(angle) * w
profile = (
Cq.Sketch()
.polygon([
(0, 0),
(0, w),
(w, w),
(w - drop, 0),
])
)
return (
Cq.Workplane()
.placeSketch(profile)
.extrude(self.ref_object_length)
)
@target(name="side-tip-2x", kind=TargetKind.DXF)
def profile_side_tip(self):
l = self.length_edge_tip
w = self.height_internal
return (
Cq.Sketch()
.push([(l/2, w/2)])
.rect(l, w)
)
@target(name="side-tail", kind=TargetKind.DXF)
def profile_side_tail(self):
"""
Plain side 2 with no hinge
"""
l = self.length_edge_tail
w = self.height_internal
return (
Cq.Sketch()
.push([(l/2, w/2)])
.rect(l, w)
)
@target(name="side-hinge-plate", kind=TargetKind.DXF)
def profile_side_hinge_plate(self):
l = self.hinge_plate_length
w = self.height_internal / 2
return (
Cq.Sketch()
.push([(0, w/2)])
.rect(l, w)
.push([
(self.hinge_hole_sep / 2, self.hinge_hole_axis_dist),
(-self.hinge_hole_sep / 2, self.hinge_hole_axis_dist),
])
.circle(self.hinge_hole_diam / 2, mode='s')
)
@target(name="side-tail-hinged", kind=TargetKind.DXF)
def profile_side_tail_hinged(self):
"""
Plain side 2 with no hinge
"""
l = self.length_edge_tail
w = self.height_internal
# Holes for hinge
plate_pos = [
(t * l, w * 3/4) for t in self.hinge_plate_pos
]
hole_pos = [
(self.hinge_hole_sep / 2, self.hinge_hole_axis_dist),
(-self.hinge_hole_sep / 2, self.hinge_hole_axis_dist),
]
return (
self.profile_side_tail()
.push(plate_pos)
.rect(self.hinge_plate_length, w/2, mode='s')
.push([
(hx + px, w/2 - hy)
for hx, hy in hole_pos
for px, _ in plate_pos
])
.circle(self.hinge_hole_diam / 2, mode='s')
)
@target(name="side-bot", kind=TargetKind.DXF)
def profile_side_bot(self):
l = self.width_tail - self.thickness_side * 2
w = self.height_internal
return (
Cq.Sketch()
.rect(l, w)
)
def surface_side_tip(self):
result = (
Cq.Workplane('XY')
.placeSketch(self.profile_side_tip())
.extrude(self.thickness_side)
)
plane = result.faces(">Y").workplane()
plane.moveTo(0, 0).tagPlane("bot")
plane.moveTo(-self.length_edge_tip, 0).tagPlane("top")
return result
def surface_side_tail(self):
result = (
Cq.Workplane('XY')
.placeSketch(self.profile_side_tail())
.extrude(self.thickness_side)
)
plane = result.faces(">Y").workplane()
plane.moveTo(0, 0).tagPlane("bot")
plane.moveTo(-self.length_edge_tail, 0).tagPlane("top")
return result
def surface_side_tail_hinged(self):
result = (
Cq.Workplane('XY')
.placeSketch(self.profile_side_tail_hinged())
.extrude(self.thickness_side)
)
plane = result.faces(">Y").workplane()
plane.moveTo(0, 0).tagPlane("bot")
plane.moveTo(-self.length_edge_tail, 0).tagPlane("top")
return result
def surface_side_bot(self):
result = (
Cq.Workplane('XY')
.placeSketch(self.profile_side_bot())
.extrude(self.thickness_side)
)
plane = result.faces(">Y").workplane()
plane.moveTo(self.width_tail / 2, 0).tagPlane("bot")
plane.moveTo(-self.width_tail / 2, 0).tagPlane("top")
return result
@assembly()
def assembly(self) -> Cq.Assembly:
a = (
Cq.Assembly()
.addS(
self.surface_top(),
name="top",
material=self.material_shell,
role=Role.STRUCTURE | Role.DECORATION
)
.constrain("top", "Fixed")
.addS(
self.surface_bottom(),
name="bottom",
material=self.material_shell,
role=Role.STRUCTURE,
loc=Cq.Location(0, 0, -self.thickness_top - self.height_internal)
)
.constrain("bottom", "Fixed")
.addS(
self.surface_side_tip(),
name="side_tip_l",
material=self.material_shell,
role=Role.STRUCTURE,
)
.constrain("bottom?tip", "side_tip_l?top", "Plane")
.constrain("bottom?mid_l", "side_tip_l?bot", "Plane")
.addS(
self.surface_side_tip(),
name="side_tip_r",
material=self.material_shell,
role=Role.STRUCTURE,
)
.constrain("bottom?tip", "side_tip_r?bot", "Plane")
.constrain("bottom?mid_r", "side_tip_r?top", "Plane")
.addS(
self.surface_side_tail(),
name="side_tail_l",
material=self.material_shell,
role=Role.STRUCTURE,
)
.constrain("bottom?mid_l", "side_tail_l?top", "Plane")
.constrain("bottom?bot_l", "side_tail_l?bot", "Plane")
.addS(
self.surface_side_tail_hinged(),
name="side_tail_r",
material=self.material_shell,
role=Role.STRUCTURE,
)
.constrain("bottom?mid_r", "side_tail_r?bot", "Plane")
.constrain("bottom?bot_r", "side_tail_r?top", "Plane")
.addS(
self.surface_side_bot(),
name="side_bot",
material=self.material_shell,
role=Role.STRUCTURE,
)
.constrain("bottom?bot_l", "side_bot?top", "Plane")
.constrain("bottom?bot_r", "side_bot?bot", "Plane")
.solve()
)
return a

View File

@ -1,139 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="100mm"
height="100mm"
viewBox="0 0 100 100"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
sodipodi:docname="zehi.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="1.8706489"
inkscape:cx="171.86549"
inkscape:cy="207.68194"
inkscape:window-width="1640"
inkscape:window-height="962"
inkscape:window-x="20"
inkscape:window-y="40"
inkscape:window-maximized="0"
inkscape:current-layer="layer3"
showguides="true">
<sodipodi:guide
position="50,90.756846"
orientation="-1,0"
id="guide1"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="32.08421,55"
orientation="0,1"
id="guide2"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="60,78.19709"
orientation="-1,0"
id="guide3"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="40,74.842796"
orientation="-1,0"
id="guide4"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="4.9999993,79.999999"
orientation="0,1"
id="guide5"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="-9.7833569,45"
orientation="0,1"
id="guide6"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
</sodipodi:namedview>
<defs
id="defs1" />
<g
inkscape:label="Outline"
inkscape:groupmode="layer"
id="layer1">
<path
id="path1"
style="fill:none;stroke:#000000;stroke-width:0.264453;stroke-opacity:1;-inkscape-stroke:none"
inkscape:label="outer"
d="M 50.000049 0.13229167 A 49.867775 49.867775 0 0 0 0.13229167 50.000049 A 49.867775 49.867775 0 0 0 50.000049 99.867806 A 49.867775 49.867775 0 0 0 99.867806 50.000049 A 49.867775 49.867775 0 0 0 50.000049 0.13229167 z M 50.000049 5.1190674 A 44.880997 44.880997 0 0 1 94.88103 50.000049 A 44.880997 44.880997 0 0 1 50.000049 94.88103 A 44.880997 44.880997 0 0 1 5.1190674 50.000049 A 44.880997 44.880997 0 0 1 50.000049 5.1190674 z " />
</g>
<g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="Cut"
style="display:none">
<circle
style="fill:none;stroke:#000000;stroke-width:0.264453;stroke-opacity:1;-inkscape-stroke:none"
id="circle14"
cx="50"
cy="50"
inkscape:label="outer"
r="49.867775" />
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Ze"
style="display:inline">
<path
id="path10"
style="fill:none;stroke:#144e16;stroke-width:0.290227;stroke-opacity:1;-inkscape-stroke:none"
d="M 50.000049,12.645223 A 37.354885,37.354885 0 0 0 13.002824,44.999837 h 2.546614 2.469617 63.965088 1.438155 3.574976 A 37.354885,37.354885 0 0 0 50.000049,12.645223 Z m 0,4.980575 a 32.374233,32.374233 0 0 1 22.160404,8.817549 H 27.842745 A 32.374233,32.374233 0 0 1 50.000049,17.625798 Z M 23.725167,31.201713 h 52.552864 a 32.374233,32.374233 0 0 1 4.547526,9.039758 H 19.177641 a 32.374233,32.374233 0 0 1 4.547526,-9.039758 z"
inkscape:label="top" />
<path
style="fill:none;stroke:#144e16;stroke-width:0.261252;stroke-opacity:1;-inkscape-stroke:none"
d="m 30.468424,65.042542 -8.764322,8.764322 a 37.141727,37.141731 0 0 0 28.295947,13.12168 37.141727,37.141731 0 0 0 23.236308,-8.258411 L 70.147139,75.252771 C 64.43218,79.841996 57.3295,82.357002 50.000049,82.386702 42.031806,82.356509 34.403936,79.387593 28.529008,74.129842 l 5.513359,-5.513358 z"
id="path14" />
<path
style="fill:none;stroke:#144e16;stroke-width:0.261252;stroke-opacity:1;-inkscape-stroke:none"
d="m 53.049475,54.972872 v 0.02687 H 13.231234 a 37.141727,37.141731 0 0 0 1.076937,5.000211 H 53.049475 V 75.366976 H 58.10343 V 67.69716 H 73.684908 V 62.643205 H 58.10343 v -2.64325 h 27.588497 a 37.141727,37.141731 0 0 0 1.076937,-5.000211 H 58.10343 v -0.02687 z"
id="path11" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Hi"
style="display:inline">
<path
id="rect6"
style="fill:none;stroke:#053efb;stroke-width:0.264583;stroke-opacity:1;-inkscape-stroke:none"
d="m 54.786837,19.999813 v 59.999955 h 5.213118 v -8.94364 H 79.70883 V 62.775496 H 59.999955 V 54.140365 H 79.70883 V 45.859733 H 59.999955 V 37.224601 H 79.70883 V 28.94397 H 59.999955 v -8.944157 z"
inkscape:label="right" />
<path
id="path9"
style="fill:none;stroke:#053efb;stroke-width:0.264583;stroke-opacity:1;-inkscape-stroke:none"
d="M 45.245359,79.999977 V 20.000022 h -5.213118 v 8.94364 H 20.323366 v 8.280632 h 19.708875 v 8.635131 H 20.323366 v 8.280632 h 19.708875 v 8.635132 H 20.323366 v 8.280631 h 19.708875 v 8.944157 z"
inkscape:label="left" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.6 KiB

View File

@ -1,11 +1,13 @@
"""
Utility functions for cadquery objects
"""
import functools, math
from typing import Optional, Union, Tuple, cast
import functools
import math
from typing import Optional
import cadquery as Cq
from cadquery.occ_impl.solver import ConstraintSpec
from nhf import Role
from typing import Union, Tuple, cast
from nhf.materials import KEY_ITEM, KEY_MATERIAL
# Bug fixes
@ -53,11 +55,6 @@ def is2d(self: Cq.Location) -> bool:
return z == 0 and rx == 0 and ry == 0
Cq.Location.is2d = is2d
def scale(self: Cq.Location, fac: float) -> bool:
(x, y, z), (rx, ry, rz) = self.toTuple()
return Cq.Location(x*fac, y*fac, z*fac, rx, ry, rz)
Cq.Location.scale = scale
def to2d(self: Cq.Location) -> Tuple[Tuple[float, float], float]:
"""
Returns position and angle
@ -96,24 +93,17 @@ Cq.Location.with_angle_2d = with_angle_2d
def flip_x(self: Cq.Location) -> Cq.Location:
(x, y), a = self.to2d()
return Cq.Location.from2d(-x, y, 180 - a)
return Cq.Location.from2d(-x, y, 90 - a)
Cq.Location.flip_x = flip_x
def flip_y(self: Cq.Location) -> Cq.Location:
(x, y), a = self.to2d()
return Cq.Location.from2d(x, -y, -a)
Cq.Location.flip_y = flip_y
def boolean(
self: Cq.Sketch,
obj: Union[Cq.Face, Cq.Sketch, Cq.Compound],
**kwargs) -> Cq.Sketch:
"""
Performs Boolean operation between a sketch and a sketch-like object
"""
def boolean(self: Cq.Sketch, obj, **kwargs) -> Cq.Sketch:
return (
self
.reset()
# Has to be 0, 0. Translation doesn't work.
.push([(0, 0)])
.each(lambda _: obj, **kwargs)
)

3923
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,22 +6,15 @@ authors = ["Leni Aniva <v@leni.sh>"]
readme = "README.md"
[tool.poetry.dependencies]
python = ">=3.10,<3.13"
cadquery = "2.5.2"
numpy = ">=2,<3"
python = "^3.10"
cadquery = {git = "https://github.com/CadQuery/cadquery.git"}
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"]