feat: Splined wing profile
This commit is contained in:
parent
2aeeaae061
commit
d43c1944a7
|
@ -548,7 +548,7 @@ class Parameters(Model):
|
||||||
|
|
||||||
@target(name="wing/r1s1", kind=TargetKind.DXF)
|
@target(name="wing/r1s1", kind=TargetKind.DXF)
|
||||||
def wing_r1s1_profile(self) -> Cq.Sketch:
|
def wing_r1s1_profile(self) -> Cq.Sketch:
|
||||||
return self.wing_profile.wing_r1_profile()
|
return self.wing_profile.profile()
|
||||||
|
|
||||||
def wing_r1s1_panel(self, front=True) -> Cq.Workplane:
|
def wing_r1s1_panel(self, front=True) -> Cq.Workplane:
|
||||||
profile = self.wing_r1s1_profile()
|
profile = self.wing_r1s1_profile()
|
||||||
|
|
|
@ -3,7 +3,9 @@ This file describes the shapes of the wing shells. The joints are defined in
|
||||||
`__init__.py`.
|
`__init__.py`.
|
||||||
"""
|
"""
|
||||||
import math
|
import math
|
||||||
|
from enum import Enum
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from typing import Mapping, Tuple
|
||||||
import cadquery as Cq
|
import cadquery as Cq
|
||||||
from nhf import Material, Role
|
from nhf import Material, Role
|
||||||
from nhf.parts.joints import HirthJoint
|
from nhf.parts.joints import HirthJoint
|
||||||
|
@ -235,19 +237,19 @@ class WingProfile:
|
||||||
|
|
||||||
shoulder_height: float = 100
|
shoulder_height: float = 100
|
||||||
|
|
||||||
elbow_height: float = 120
|
elbow_height: float = 100
|
||||||
elbow_x: float = 270
|
elbow_x: float = 240
|
||||||
elbow_y: float = 10
|
elbow_y: float = 30
|
||||||
# Angle of elbow w.r.t. y axis
|
# Tilt of elbow w.r.t. shoulder
|
||||||
elbow_angle: float = -20
|
elbow_angle: float = 20
|
||||||
|
|
||||||
wrist_height: float = 70
|
wrist_height: float = 70
|
||||||
# Bottom point of the wrist
|
# Bottom point of the wrist
|
||||||
wrist_x: float = 400
|
wrist_x: float = 400
|
||||||
wrist_y: float = 200
|
wrist_y: float = 200
|
||||||
|
|
||||||
# Angle of wrist w.r.t. y axis. should be negative
|
# Tile of wrist w.r.t. shoulder
|
||||||
wrist_angle: float = -40
|
wrist_angle: float = 40
|
||||||
|
|
||||||
# Extends from the wrist to the tip of the arrow
|
# Extends from the wrist to the tip of the arrow
|
||||||
arrow_height: float = 300
|
arrow_height: float = 300
|
||||||
|
@ -261,21 +263,45 @@ class WingProfile:
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
assert self.ring_radius > self.ring_radius_inner
|
assert self.ring_radius > self.ring_radius_inner
|
||||||
|
|
||||||
|
self.elbow_theta = math.radians(self.elbow_angle)
|
||||||
|
self.elbow_c = math.cos(self.elbow_theta)
|
||||||
|
self.elbow_s = math.sin(self.elbow_theta)
|
||||||
|
self.elbow_top_x, self.elbow_top_y = self.elbow_to_abs(0, self.elbow_height)
|
||||||
|
self.wrist_theta = math.radians(self.wrist_angle)
|
||||||
|
self.wrist_c = math.cos(self.wrist_theta)
|
||||||
|
self.wrist_s = math.sin(self.wrist_theta)
|
||||||
|
self.wrist_top_x, self.wrist_top_y = self.wrist_to_abs(0, self.wrist_height)
|
||||||
|
self.arrow_theta = math.radians(self.arrow_angle)
|
||||||
|
self.arrow_x, self.arrow_y = self.wrist_to_abs(0, -self.arrow_height)
|
||||||
|
self.arrow_tip_x = self.arrow_x + (self.arrow_height + self.wrist_height) \
|
||||||
|
* math.sin(self.arrow_theta - self.wrist_theta)
|
||||||
|
self.arrow_tip_y = self.arrow_y + (self.arrow_height + self.wrist_height) \
|
||||||
|
* math.cos(self.arrow_theta - self.wrist_theta)
|
||||||
|
# [[c, s], [-s, c]] * [ring_x, ring_y]
|
||||||
|
self.ring_abs_x = self.wrist_top_x + self.wrist_c * self.ring_x - self.wrist_s * self.ring_y
|
||||||
|
self.ring_abs_y = self.wrist_top_y + self.wrist_s * self.ring_x + self.wrist_c * self.ring_y
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ring_radius(self) -> float:
|
def ring_radius(self) -> float:
|
||||||
dx = self.ring_x
|
dx = self.ring_x
|
||||||
dy = self.ring_y
|
dy = self.ring_y
|
||||||
return (dx * dx + dy * dy) ** 0.5
|
return (dx * dx + dy * dy) ** 0.5
|
||||||
|
|
||||||
def wing_r1_profile(self) -> Cq.Sketch:
|
def elbow_to_abs(self, x: float, y: float) -> Tuple[float, float]:
|
||||||
wrist_theta = math.radians(self.wrist_angle)
|
elbow_x = self.elbow_x + x * self.elbow_c - y * self.elbow_s
|
||||||
wrist_s = math.sin(wrist_theta)
|
elbow_y = self.elbow_y + x * self.elbow_s + y * self.elbow_c
|
||||||
wrist_c = math.cos(wrist_theta)
|
print(f"c={self.elbow_c}, s={self.elbow_s}, x={elbow_x}, y={elbow_y}")
|
||||||
wrist_top_x = self.wrist_x + self.wrist_height * wrist_s
|
return elbow_x, elbow_y
|
||||||
wrist_top_y = self.wrist_y + self.wrist_height * wrist_c
|
def wrist_to_abs(self, x: float, y: float) -> Tuple[float, float]:
|
||||||
elbow_theta = math.radians(self.elbow_angle)
|
wrist_x = self.wrist_x + x * self.wrist_c - y * self.wrist_s
|
||||||
elbow_top_x = self.elbow_x + self.elbow_height * math.sin(elbow_theta)
|
wrist_y = self.wrist_y + x * self.wrist_s + y * self.wrist_c
|
||||||
elbow_top_y = self.elbow_y + self.elbow_height * math.cos(elbow_theta)
|
return wrist_x, wrist_y
|
||||||
|
|
||||||
|
|
||||||
|
def profile(self) -> Cq.Sketch:
|
||||||
|
"""
|
||||||
|
Net profile of the wing starting from the wing root with no divisions
|
||||||
|
"""
|
||||||
result = (
|
result = (
|
||||||
Cq.Sketch()
|
Cq.Sketch()
|
||||||
.segment(
|
.segment(
|
||||||
|
@ -284,8 +310,8 @@ class WingProfile:
|
||||||
tag="shoulder")
|
tag="shoulder")
|
||||||
.arc(
|
.arc(
|
||||||
(0, self.shoulder_height),
|
(0, self.shoulder_height),
|
||||||
(elbow_top_x, elbow_top_y),
|
(self.elbow_top_x, self.elbow_top_y),
|
||||||
(wrist_top_x, wrist_top_y),
|
(self.wrist_top_x, self.wrist_top_y),
|
||||||
tag="s1_top")
|
tag="s1_top")
|
||||||
#.segment(
|
#.segment(
|
||||||
# (self.wrist_x, self.wrist_y),
|
# (self.wrist_x, self.wrist_y),
|
||||||
|
@ -297,39 +323,108 @@ class WingProfile:
|
||||||
(self.wrist_x, self.wrist_y),
|
(self.wrist_x, self.wrist_y),
|
||||||
tag="s1_bot")
|
tag="s1_bot")
|
||||||
)
|
)
|
||||||
arrow_theta = math.radians(self.arrow_angle)
|
|
||||||
arrow_x = self.wrist_x - self.arrow_height * wrist_s
|
|
||||||
arrow_y = self.wrist_y - self.arrow_height * wrist_c
|
|
||||||
arrow_tip_x = arrow_x + (self.arrow_height + self.wrist_height) * math.sin(arrow_theta + wrist_theta)
|
|
||||||
arrow_tip_y = arrow_y + (self.arrow_height + self.wrist_height) * math.cos(arrow_theta + wrist_theta)
|
|
||||||
result = (
|
result = (
|
||||||
result
|
result
|
||||||
.segment(
|
.segment(
|
||||||
(self.wrist_x, self.wrist_y),
|
(self.wrist_x, self.wrist_y),
|
||||||
(arrow_x, arrow_y)
|
(self.arrow_x, self.arrow_y)
|
||||||
)
|
)
|
||||||
.segment(
|
.segment(
|
||||||
(arrow_x, arrow_y),
|
(self.arrow_x, self.arrow_y),
|
||||||
(arrow_tip_x, arrow_tip_y)
|
(self.arrow_tip_x, self.arrow_tip_y)
|
||||||
)
|
)
|
||||||
.segment(
|
.segment(
|
||||||
(arrow_tip_x, arrow_tip_y),
|
(self.arrow_tip_x, self.arrow_tip_y),
|
||||||
(wrist_top_x, wrist_top_y)
|
(self.wrist_top_x, self.wrist_top_y)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# Carve out the ring
|
# Carve out the ring
|
||||||
result = result.assemble()
|
result = result.assemble()
|
||||||
ring_x = wrist_top_x + wrist_c * self.ring_x + wrist_s * self.ring_y
|
|
||||||
ring_y = wrist_top_y - wrist_s * self.ring_x + wrist_c * self.ring_y
|
|
||||||
result = (
|
result = (
|
||||||
result
|
result
|
||||||
.push([(ring_x, ring_y)])
|
.push([(self.ring_abs_x, self.ring_abs_y)])
|
||||||
.circle(self.ring_radius, mode='a')
|
.circle(self.ring_radius, mode='a')
|
||||||
.circle(self.ring_radius_inner, mode='s')
|
.circle(self.ring_radius_inner, mode='s')
|
||||||
.clean()
|
.clean()
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def _mask_elbow(self) -> list[Tuple[float, float]]:
|
||||||
|
"""
|
||||||
|
Polygon shape to mask out parts above the elbow
|
||||||
|
"""
|
||||||
|
abscissa = 200
|
||||||
|
return [
|
||||||
|
(0, -abscissa),
|
||||||
|
(self.elbow_x, self.elbow_y),
|
||||||
|
(self.elbow_top_x, self.elbow_top_y),
|
||||||
|
(0, abscissa)
|
||||||
|
]
|
||||||
|
|
||||||
|
def _mask_wrist(self) -> list[Tuple[float, float]]:
|
||||||
|
abscissa = 200
|
||||||
|
return [
|
||||||
|
(0, -abscissa),
|
||||||
|
(self.wrist_x - self.wrist_s * abscissa,
|
||||||
|
self.wrist_y - self.wrist_c * abscissa),
|
||||||
|
(self.wrist_top_x, self.wrist_top_y),
|
||||||
|
(0, abscissa),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def profile_s1(self) -> Cq.Sketch:
|
||||||
|
profile = (
|
||||||
|
self.profile()
|
||||||
|
.reset()
|
||||||
|
.polygon(self._mask_elbow(), mode='i')
|
||||||
|
)
|
||||||
|
return profile
|
||||||
|
def surface_s1(self,
|
||||||
|
thickness:float = 25.4/16,
|
||||||
|
shoulder_mount_inset: float=20,
|
||||||
|
shoulder_joint_child_height: float=80,
|
||||||
|
elbow_mount_inset: float=20,
|
||||||
|
elbow_joint_parent_height: float=60,
|
||||||
|
front: bool=True) -> Cq.Workplane:
|
||||||
|
assert shoulder_joint_child_height < self.shoulder_height
|
||||||
|
assert elbow_joint_parent_height < self.elbow_height
|
||||||
|
h = (self.shoulder_height - shoulder_joint_child_height) / 2
|
||||||
|
tags_shoulder = [
|
||||||
|
("shoulder_bot", (shoulder_mount_inset, h), 90),
|
||||||
|
("shoulder_top", (shoulder_mount_inset, h + shoulder_joint_child_height), 270),
|
||||||
|
]
|
||||||
|
h = (self.elbow_height - elbow_joint_parent_height) / 2
|
||||||
|
tags_elbow = [
|
||||||
|
("elbow_bot",
|
||||||
|
self.elbow_to_abs(-elbow_mount_inset, h),
|
||||||
|
self.elbow_angle + 90),
|
||||||
|
("elbow_top",
|
||||||
|
self.elbow_to_abs(-elbow_mount_inset, h + elbow_joint_parent_height),
|
||||||
|
self.elbow_angle + 270),
|
||||||
|
]
|
||||||
|
profile = self.profile_s1()
|
||||||
|
tags = tags_shoulder + tags_elbow
|
||||||
|
return nhf.utils.extrude_with_markers(profile, thickness, tags, reverse=front)
|
||||||
|
|
||||||
|
|
||||||
|
def profile_s2(self) -> Cq.Sketch:
|
||||||
|
profile = (
|
||||||
|
self.profile()
|
||||||
|
.reset()
|
||||||
|
.polygon(self._mask_wrist(), mode='i')
|
||||||
|
.reset()
|
||||||
|
.polygon(self._mask_elbow(), mode='s')
|
||||||
|
)
|
||||||
|
return profile
|
||||||
|
def profile_s3(self) -> Cq.Sketch:
|
||||||
|
profile = (
|
||||||
|
self.profile()
|
||||||
|
.reset()
|
||||||
|
.polygon(self._mask_wrist(), mode='s')
|
||||||
|
)
|
||||||
|
return profile
|
||||||
|
|
||||||
|
|
||||||
def wing_r1s1_profile(self) -> Cq.Sketch:
|
def wing_r1s1_profile(self) -> Cq.Sketch:
|
||||||
"""
|
"""
|
||||||
Generates the first wing segment profile, with the wing root pointing in
|
Generates the first wing segment profile, with the wing root pointing in
|
||||||
|
|
29
nhf/utils.py
29
nhf/utils.py
|
@ -5,6 +5,7 @@ Adds the functions to `Cq.Workplane`:
|
||||||
1. `tagPoint`
|
1. `tagPoint`
|
||||||
2. `tagPlane`
|
2. `tagPlane`
|
||||||
"""
|
"""
|
||||||
|
import math
|
||||||
import cadquery as Cq
|
import cadquery as Cq
|
||||||
from typing import Union, Tuple
|
from typing import Union, Tuple
|
||||||
|
|
||||||
|
@ -50,3 +51,31 @@ def tagPlane(self, tag: str,
|
||||||
self.eachpoint(edge.moved, useLocalCoordinates=True).tag(tag)
|
self.eachpoint(edge.moved, useLocalCoordinates=True).tag(tag)
|
||||||
|
|
||||||
Cq.Workplane.tagPlane = tagPlane
|
Cq.Workplane.tagPlane = tagPlane
|
||||||
|
|
||||||
|
def extrude_with_markers(sketch: Cq.Sketch,
|
||||||
|
thickness: float,
|
||||||
|
tags: list[Tuple[str, Tuple[float, float], float]],
|
||||||
|
reverse: bool = False):
|
||||||
|
"""
|
||||||
|
Extrudes a sketch and place tags on the sketch for mating.
|
||||||
|
|
||||||
|
Each tag is of the format `(name, (x, y), angle)`, where the angle is
|
||||||
|
specifies in degrees counterclockwise from +X. Two marks are generated for
|
||||||
|
each `name`, "{name}" for the location (with normal) and "{name}_dir" for
|
||||||
|
the directrix specified by the angle.
|
||||||
|
|
||||||
|
This simulates a process of laser cutting and bonding (for wood and acrylic)
|
||||||
|
"""
|
||||||
|
result = (
|
||||||
|
Cq.Workplane('XY')
|
||||||
|
.placeSketch(sketch)
|
||||||
|
.extrude(thickness)
|
||||||
|
)
|
||||||
|
plane = result.faces("<Z" if reverse else ">Z").workplane()
|
||||||
|
sign = -1 if reverse else 1
|
||||||
|
for tag, (px, py), angle in tag:
|
||||||
|
theta = sign * math.radians(angle)
|
||||||
|
direction = (math.cos(theta), math.sin(theta), 0)
|
||||||
|
plane.moveTo(px, sign * py).tagPlane(tag)
|
||||||
|
plane.moveTo(px, sign * py).tagPlane(f"{tag}_dir", direction)
|
||||||
|
return result
|
||||||
|
|
Loading…
Reference in New Issue