fix: Directrix labeling in torsion joint
This commit is contained in:
parent
53c143e0b7
commit
876571418c
|
@ -100,7 +100,7 @@ class HirthJoint:
|
|||
(
|
||||
result
|
||||
.polyline([(0, 0, 0), (1, 0, 0)], forConstruction=True)
|
||||
.tag("directrix")
|
||||
.tag("dir")
|
||||
)
|
||||
return result
|
||||
|
||||
|
@ -132,7 +132,7 @@ class HirthJoint:
|
|||
.add(obj2, name="obj2", color=Role.CHILD.color)
|
||||
.constrain("obj1", "Fixed")
|
||||
.constrain("obj1?mate", "obj2?mate", "Plane")
|
||||
.constrain("obj1?directrix", "obj2?directrix", "Axis", param=angle)
|
||||
.constrain("obj1?dir", "obj2?dir", "Axis", param=angle)
|
||||
.solve()
|
||||
)
|
||||
return result
|
||||
|
@ -249,7 +249,8 @@ class TorsionJoint:
|
|||
"""
|
||||
|
||||
# Radius limit for rotating components
|
||||
radius: float = 40
|
||||
radius_track: float = 40
|
||||
radius_rider: float = 38
|
||||
track_disk_height: float = 10
|
||||
rider_disk_height: float = 8
|
||||
|
||||
|
@ -281,7 +282,8 @@ class TorsionJoint:
|
|||
|
||||
|
||||
def __post_init__(self):
|
||||
assert self.radius > self.groove_radius_outer
|
||||
assert self.radius_track > self.groove_radius_outer
|
||||
assert self.radius_rider > self.groove_radius_outer
|
||||
assert self.groove_radius_outer > self.groove_radius_inner
|
||||
assert self.groove_radius_inner > self.radius_spring
|
||||
assert self.spring_height > self.groove_depth, "Groove is too deep"
|
||||
|
@ -289,8 +291,18 @@ class TorsionJoint:
|
|||
|
||||
@property
|
||||
def total_height(self):
|
||||
"""
|
||||
Total height counting from bottom to top
|
||||
"""
|
||||
return self.track_disk_height + self.rider_disk_height + self.spring_height
|
||||
|
||||
@property
|
||||
def radius(self):
|
||||
"""
|
||||
Maximum radius of this joint
|
||||
"""
|
||||
return max(self.radius_rider, self.radius_track)
|
||||
|
||||
@property
|
||||
def _radius_spring_internal(self):
|
||||
return self.radius_spring - self.spring_thickness
|
||||
|
@ -335,14 +347,14 @@ class TorsionJoint:
|
|||
# TODO: Cover outer part of track only. Can we do this?
|
||||
groove_profile = (
|
||||
Cq.Sketch()
|
||||
.circle(self.radius)
|
||||
.circle(self.radius_track)
|
||||
.circle(self.groove_radius_outer, mode='s')
|
||||
.circle(self.groove_radius_inner, mode='a')
|
||||
.circle(self.radius_spring, mode='s')
|
||||
)
|
||||
spring_hole_profile = (
|
||||
Cq.Sketch()
|
||||
.circle(self.radius)
|
||||
.circle(self.radius_track)
|
||||
.circle(self.radius_spring, mode='s')
|
||||
)
|
||||
slot_height = self.spring_thickness
|
||||
|
@ -359,7 +371,7 @@ class TorsionJoint:
|
|||
result = (
|
||||
Cq.Workplane('XY')
|
||||
.cylinder(
|
||||
radius=self.radius,
|
||||
radius=self.radius_track,
|
||||
height=self.track_disk_height,
|
||||
centered=(True, True, False))
|
||||
.faces('>Z')
|
||||
|
@ -375,12 +387,12 @@ class TorsionJoint:
|
|||
.hole(self.radius_axle * 2)
|
||||
.cut(slot.moved(Cq.Location((0, 0, self.track_disk_height))))
|
||||
)
|
||||
# Insert directrix`
|
||||
# Insert directrix
|
||||
result.polyline(self._directrix(self.track_disk_height),
|
||||
forConstruction=True).tag("directrix")
|
||||
forConstruction=True).tag("dir")
|
||||
return result
|
||||
|
||||
def rider(self, rider_slot_begin=None):
|
||||
def rider(self, rider_slot_begin=None, reverse_directrix_label=False):
|
||||
if not rider_slot_begin:
|
||||
rider_slot_begin = self.rider_slot_begin
|
||||
def slot(loc):
|
||||
|
@ -389,11 +401,11 @@ class TorsionJoint:
|
|||
return face.located(loc)
|
||||
wall_profile = (
|
||||
Cq.Sketch()
|
||||
.circle(self.radius, mode='a')
|
||||
.circle(self.radius_rider, mode='a')
|
||||
.circle(self.radius_spring, mode='s')
|
||||
.parray(
|
||||
r=0,
|
||||
a1=self.rider_slot_begin,
|
||||
a1=rider_slot_begin,
|
||||
da=self.rider_slot_span,
|
||||
n=self.rider_n_slots)
|
||||
.each(slot, mode='s')
|
||||
|
@ -409,7 +421,7 @@ class TorsionJoint:
|
|||
contact_profile
|
||||
.parray(
|
||||
r=0,
|
||||
a1=self.rider_slot_begin,
|
||||
a1=rider_slot_begin,
|
||||
da=self.rider_slot_span,
|
||||
n=self.rider_n_slots)
|
||||
.each(slot, mode='s')
|
||||
|
@ -420,7 +432,7 @@ class TorsionJoint:
|
|||
result = (
|
||||
Cq.Workplane('XY')
|
||||
.cylinder(
|
||||
radius=self.radius,
|
||||
radius=self.radius_rider,
|
||||
height=self.rider_disk_height,
|
||||
centered=(True, True, False))
|
||||
.faces('>Z')
|
||||
|
@ -442,16 +454,17 @@ class TorsionJoint:
|
|||
#.workplane()
|
||||
.hole(self.radius_axle * 2)
|
||||
)
|
||||
theta_begin = math.radians(self.rider_slot_begin) + math.pi
|
||||
theta_begin = math.radians(rider_slot_begin)
|
||||
theta_span = math.radians(self.rider_slot_span)
|
||||
if abs(math.remainder(self.rider_slot_span, 360)) < TOL:
|
||||
theta_step = theta_span / self.rider_n_slots
|
||||
else:
|
||||
theta_step = theta_span / (self.rider_n_slots - 1)
|
||||
for i in range(self.rider_n_slots):
|
||||
theta = theta_begin - i * theta_step
|
||||
theta = theta_begin + i * theta_step
|
||||
j = self.rider_n_slots - i - 1 if reverse_directrix_label else i
|
||||
result.polyline(self._directrix(self.rider_disk_height, theta),
|
||||
forConstruction=True).tag(f"directrix{i}")
|
||||
forConstruction=True).tag(f"dir{j}")
|
||||
return result
|
||||
|
||||
def rider_track_assembly(self, directrix=0):
|
||||
|
@ -462,11 +475,26 @@ class TorsionJoint:
|
|||
Cq.Assembly()
|
||||
.add(spring, name="spring", color=Role.DAMPING.color)
|
||||
.add(track, name="track", color=Role.PARENT.color)
|
||||
.constrain("track?spring", "spring?top", "Plane")
|
||||
.constrain("track?directrix", "spring?directrix_bot", "Axis")
|
||||
.add(rider, name="rider", color=Role.CHILD.color)
|
||||
.constrain("rider?spring", "spring?bot", "Plane")
|
||||
.constrain(f"rider?directrix{directrix}", "spring?directrix_top", "Axis")
|
||||
.solve()
|
||||
.add(rider, name="rider", color=Role.PARENT.color)
|
||||
)
|
||||
TorsionJoint.add_constraints(result,
|
||||
rider="rider", track="track", spring="spring",
|
||||
directrix=directrix)
|
||||
return result.solve()
|
||||
|
||||
@staticmethod
|
||||
def add_constraints(assembly: Cq.Assembly,
|
||||
rider: str, track: str, spring: str,
|
||||
directrix: int = 0):
|
||||
"""
|
||||
Add the necessary constraints to a RT assembly
|
||||
"""
|
||||
(
|
||||
assembly
|
||||
.constrain(f"{track}?spring", f"{spring}?top", "Plane")
|
||||
.constrain(f"{track}?dir", f"{spring}?dir_top",
|
||||
"Axis", param=0)
|
||||
.constrain(f"{rider}?spring", f"{spring}?bot", "Plane")
|
||||
.constrain(f"{rider}?dir{directrix}", f"{spring}?dir_bot",
|
||||
"Axis", param=0)
|
||||
)
|
||||
return result
|
||||
|
|
|
@ -46,11 +46,14 @@ def torsion_spring(radius=12,
|
|||
centered=False)
|
||||
)
|
||||
r = -radius if right_handed else radius
|
||||
result.polyline([(0, r, 0), (tail_length, r, 0)],
|
||||
forConstruction=True).tag("directrix_bot")
|
||||
c, s = math.cos(omega * math.pi / 180), math.sin(omega * math.pi / 180)
|
||||
result.polyline([
|
||||
(s * tail_length, c * r - s * tail_length, height),
|
||||
(c * tail_length + s * r, c * r - s * tail_length, height)],
|
||||
forConstruction=True).tag("directrix_top")
|
||||
plane = result.copyWorkplane(Cq.Workplane('XY'))
|
||||
plane.polyline([(0, r, 0), (tail_length, r, 0)],
|
||||
forConstruction=True).tag("dir_bot")
|
||||
omega = math.radians(omega)
|
||||
c, s = math.cos(omega), math.sin(omega)
|
||||
l = -tail_length
|
||||
plane.polyline([
|
||||
(-s * r, c * r, height),
|
||||
(c * l - s * r, c * r + s * l, height)],
|
||||
forConstruction=True).tag("dir_top")
|
||||
return result
|
||||
|
|
|
@ -35,11 +35,13 @@ class TestJoints(unittest.TestCase):
|
|||
def test_torsion_joint(self):
|
||||
j = joints.TorsionJoint()
|
||||
for slot in range(j.rider_n_slots):
|
||||
with self.subTest(slot=slot):
|
||||
with self.subTest(slot=slot, right_handed=False):
|
||||
self.torsion_joint_case(j, slot)
|
||||
def test_torsion_joint_right_handed(self):
|
||||
j = joints.TorsionJoint(right_handed=True)
|
||||
self.torsion_joint_case(j, 1)
|
||||
for slot in range(j.rider_n_slots):
|
||||
with self.subTest(slot=slot, right_handed=True):
|
||||
self.torsion_joint_case(j, slot)
|
||||
def test_torsion_joint_covered(self):
|
||||
j = joints.TorsionJoint(
|
||||
spring_hole_cover_track=True,
|
||||
|
|
|
@ -30,7 +30,6 @@ s1, s2, s3. The joints are named (from root to tip)
|
|||
shoulder, elbow, wrist in analogy with human anatomy.
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
import unittest
|
||||
import cadquery as Cq
|
||||
from nhf import Material, Role
|
||||
from nhf.build import Model, TargetKind, target, assembly
|
||||
|
@ -93,6 +92,9 @@ class Parameters(Model):
|
|||
wing_root_wall_thickness: float = 8
|
||||
|
||||
shoulder_torsion_joint: TorsionJoint = field(default_factory=lambda: TorsionJoint(
|
||||
radius_track=35,
|
||||
radius_rider=35,
|
||||
groove_radius_outer=32,
|
||||
track_disk_height=5.0,
|
||||
rider_disk_height=7.0,
|
||||
radius_axle=8.0,
|
||||
|
@ -114,6 +116,9 @@ class Parameters(Model):
|
|||
wing_s1_thickness: float = 20
|
||||
wing_s1_spacer_thickness: float = 25.4 / 8
|
||||
wing_s1_spacer_width: float = 20
|
||||
wing_s1_spacer_hole_diam: float = 8
|
||||
wing_s1_shoulder_spacer_hole_dist: float = 20
|
||||
wing_s1_shoulder_spacer_width: float = 60
|
||||
|
||||
trident_handle: Handle = field(default_factory=lambda: Handle(
|
||||
diam=38,
|
||||
|
@ -132,6 +137,8 @@ class Parameters(Model):
|
|||
super().__init__(name="houjuu-nue")
|
||||
assert self.wing_root_radius > self.hs_hirth_joint.radius,\
|
||||
"Wing root must be large enough to accomodate joint"
|
||||
assert self.wing_s1_shoulder_spacer_hole_dist > self.wing_s1_spacer_hole_diam, \
|
||||
"Spacer holes are too close to each other"
|
||||
|
||||
@target(name="trident/handle-connector")
|
||||
def handle_connector(self):
|
||||
|
@ -304,14 +311,32 @@ class Parameters(Model):
|
|||
conn_thickness=self.wing_s0_thickness,
|
||||
)
|
||||
|
||||
@target(name="shoulder_parent")
|
||||
def shoulder_parent_joint(self) -> Cq.Workplane:
|
||||
@target(name="shoulder_joint_parent")
|
||||
def shoulder_joint_parent(self) -> Cq.Workplane:
|
||||
joint = self.shoulder_torsion_joint
|
||||
# Thickness of the lip connecting this joint to the wing root
|
||||
lip_thickness = 10
|
||||
lip_width = 25
|
||||
lip_guard_ext = 40
|
||||
lip_guard_height = self.wing_root_wall_thickness + lip_thickness
|
||||
assert lip_guard_ext > joint.radius_track
|
||||
|
||||
lip_guard = (
|
||||
Cq.Solid.makeBox(lip_guard_ext, lip_width, lip_guard_height)
|
||||
.located(Cq.Location((0, -lip_width/2 , 0)))
|
||||
.cut(Cq.Solid.makeCylinder(joint.radius_track, lip_guard_height))
|
||||
)
|
||||
result = (
|
||||
self.shoulder_torsion_joint.rider()
|
||||
joint.track()
|
||||
.union(lip_guard, tol=1e-6)
|
||||
|
||||
# Extrude the handle
|
||||
.copyWorkplane(Cq.Workplane(
|
||||
'YZ', origin=Cq.Vector((88, 0, self.wing_root_wall_thickness))))
|
||||
.rect(25, 7, centered=(True, False))
|
||||
.rect(lip_width, lip_thickness, centered=(True, False))
|
||||
.extrude("next")
|
||||
|
||||
# Connector holes on the lip
|
||||
.copyWorkplane(Cq.Workplane(
|
||||
'YX', origin=Cq.Vector((57, 0, self.wing_root_wall_thickness))))
|
||||
.hole(self.shoulder_attach_diam)
|
||||
|
@ -322,27 +347,117 @@ class Parameters(Model):
|
|||
result.moveTo(0, self.shoulder_attach_dist).tagPlane('conn1')
|
||||
return result
|
||||
|
||||
@target(name="shoulder_child")
|
||||
def shoulder_child_joint(self) -> Cq.Assembly:
|
||||
# FIXME: half of conn_height
|
||||
h = 100 / 2
|
||||
dh = h - self.shoulder_torsion_joint.total_height
|
||||
@target(name="shoulder_joint_child")
|
||||
def shoulder_joint_child(self) -> Cq.Assembly:
|
||||
"""
|
||||
Creates the top/bottom shoulder child joint
|
||||
"""
|
||||
|
||||
joint = self.shoulder_torsion_joint
|
||||
# Half of the height of the bridging cylinder
|
||||
dh = self.wing_s0_height / 2 - joint.total_height
|
||||
core_start_angle = 30
|
||||
core_end_angle1 = 90
|
||||
core_end_angle2 = 180
|
||||
core_thickness = 2
|
||||
|
||||
core_profile1 = (
|
||||
Cq.Sketch()
|
||||
.arc((0, 0), joint.radius_rider, core_start_angle, core_end_angle1-core_start_angle)
|
||||
.segment((0, 0))
|
||||
.close()
|
||||
.assemble()
|
||||
.circle(joint.radius_rider - core_thickness, mode='s')
|
||||
)
|
||||
core_profile2 = (
|
||||
Cq.Sketch()
|
||||
.arc((0, 0), joint.radius_rider, -core_start_angle, -(core_end_angle2-core_start_angle))
|
||||
.segment((0, 0))
|
||||
.close()
|
||||
.assemble()
|
||||
.circle(joint.radius_rider - core_thickness, mode='s')
|
||||
)
|
||||
core = (
|
||||
Cq.Workplane('XY')
|
||||
.moveTo(0, 15)
|
||||
.box(50, 40, 2 * dh, centered=(True, False, True))
|
||||
.placeSketch(core_profile1)
|
||||
.toPending()
|
||||
.extrude(dh * 2)
|
||||
.copyWorkplane(Cq.Workplane('XY'))
|
||||
.placeSketch(core_profile2)
|
||||
.toPending()
|
||||
.extrude(dh * 2)
|
||||
.translate(Cq.Vector(0, 0, -dh))
|
||||
)
|
||||
# Create the upper and lower lips
|
||||
lip_height = self.wing_s1_thickness
|
||||
lip_thickness = joint.rider_disk_height
|
||||
lip_ext = 40 + joint.radius_rider
|
||||
hole_dx = self.wing_s1_shoulder_spacer_hole_dist
|
||||
assert lip_height / 2 <= joint.radius_rider
|
||||
lip = (
|
||||
Cq.Workplane('XY')
|
||||
.box(lip_ext, lip_height, lip_thickness,
|
||||
centered=(False, True, False))
|
||||
.copyWorkplane(Cq.Workplane('XY'))
|
||||
.cylinder(radius=joint.radius_rider, height=lip_thickness,
|
||||
centered=(True, True, False),
|
||||
combine='cut')
|
||||
.faces(">Z")
|
||||
.workplane()
|
||||
)
|
||||
hole_x = lip_ext - hole_dx / 2
|
||||
for i in range(2):
|
||||
plane = (
|
||||
lip
|
||||
.moveTo(hole_x - i * hole_dx, 0)
|
||||
)
|
||||
lip = plane.hole(self.wing_s1_spacer_hole_diam)
|
||||
plane.tagPlane(f"hole{i}")
|
||||
|
||||
loc_rotate = Cq.Location((0, 0, 0), (1, 0, 0), 180)
|
||||
result = (
|
||||
Cq.Assembly()
|
||||
.add(core, name="core", loc=Cq.Location())
|
||||
.add(self.shoulder_torsion_joint.track(), name="track_top",
|
||||
.add(joint.rider(rider_slot_begin=-90, reverse_directrix_label=True), name="rider_top",
|
||||
loc=Cq.Location((0, 0, dh), (0, 0, 1), -90))
|
||||
.add(self.shoulder_torsion_joint.track(), name="track_bot",
|
||||
.add(joint.rider(rider_slot_begin=180), name="rider_bot",
|
||||
loc=Cq.Location((0, 0, -dh), (0, 0, 1), -90) * loc_rotate)
|
||||
.add(lip, name="lip_top",
|
||||
loc=Cq.Location((0, 0, dh)))
|
||||
.add(lip, name="lip_bot",
|
||||
loc=Cq.Location((0, 0, -dh)) * loc_rotate)
|
||||
)
|
||||
return result
|
||||
|
||||
@assembly()
|
||||
def shoulder_assembly(self) -> Cq.Assembly:
|
||||
directrix = 0
|
||||
result = (
|
||||
Cq.Assembly()
|
||||
.add(self.shoulder_joint_child(), name="child",
|
||||
color=Role.CHILD.color)
|
||||
.constrain("child/core", "Fixed")
|
||||
.add(self.shoulder_torsion_joint.spring(), name="spring_top",
|
||||
color=Role.DAMPING.color)
|
||||
.add(self.shoulder_joint_parent(), name="parent_top",
|
||||
color=Role.PARENT.color)
|
||||
.add(self.shoulder_torsion_joint.spring(), name="spring_bot",
|
||||
color=Role.DAMPING.color)
|
||||
.add(self.shoulder_joint_parent(), name="parent_bot",
|
||||
color=Role.PARENT.color)
|
||||
)
|
||||
TorsionJoint.add_constraints(result,
|
||||
rider="child/rider_top",
|
||||
track="parent_top",
|
||||
spring="spring_top",
|
||||
directrix=directrix)
|
||||
TorsionJoint.add_constraints(result,
|
||||
rider="child/rider_bot",
|
||||
track="parent_bot",
|
||||
spring="spring_bot",
|
||||
directrix=directrix)
|
||||
return result.solve()
|
||||
|
||||
@target(name="wing/s1-spacer", kind=TargetKind.DXF)
|
||||
def wing_s1_spacer(self) -> Cq.Workplane:
|
||||
result = (
|
||||
|
@ -357,6 +472,36 @@ class Parameters(Model):
|
|||
result.faces(">Y").tag("dir")
|
||||
return result
|
||||
|
||||
@target(name="wing/s1-shoulder-spacer", kind=TargetKind.DXF)
|
||||
def wing_s1_shoulder_spacer(self) -> Cq.Workplane:
|
||||
dx = self.wing_s1_shoulder_spacer_hole_dist
|
||||
result = (
|
||||
Cq.Workplane('XZ')
|
||||
.sketch()
|
||||
.rect(self.wing_s1_shoulder_spacer_width,
|
||||
self.wing_s1_thickness)
|
||||
.push([
|
||||
(0, 0),
|
||||
(dx, 0),
|
||||
])
|
||||
.circle(self.wing_s1_spacer_hole_diam / 2, mode='s')
|
||||
.finalize()
|
||||
.extrude(self.wing_s1_spacer_thickness)
|
||||
)
|
||||
# Tag the mating surfaces to be glued
|
||||
result.faces("<Z").tag("mate1")
|
||||
result.faces(">Z").tag("mate2")
|
||||
|
||||
# Tag the directrix
|
||||
result.faces(">Y").tag("dir")
|
||||
|
||||
# Tag the holes
|
||||
plane = result.faces(">Y").workplane()
|
||||
# Side closer to the parent is 0
|
||||
plane.moveTo(dx, 0).tagPlane("hole0")
|
||||
plane.tagPlane("hole1")
|
||||
return result
|
||||
|
||||
@target(name="wing/r1s1", kind=TargetKind.DXF)
|
||||
def wing_r1s1_profile(self) -> Cq.Sketch:
|
||||
return MW.wing_r1s1_profile()
|
||||
|
@ -379,35 +524,6 @@ class Parameters(Model):
|
|||
plane.moveTo(px, sign * py).tagPlane(name)
|
||||
return result
|
||||
|
||||
@assembly()
|
||||
def shoulder_assembly(self) -> Cq.Assembly:
|
||||
result = (
|
||||
Cq.Assembly()
|
||||
.add(self.shoulder_child_joint(), name="child",
|
||||
color=Role.CHILD.color)
|
||||
.constrain("child/core", "Fixed")
|
||||
# Top parent joint
|
||||
.add(self.shoulder_torsion_joint.spring(), name="spring_top",
|
||||
color=Role.DAMPING.color)
|
||||
.constrain("child/track_top?spring", "spring_top?top", "Plane")
|
||||
.constrain("child/track_top?directrix", "spring_top?directrix_bot", "Axis")
|
||||
.add(self.shoulder_parent_joint(), name="parent_top",
|
||||
color=Role.PARENT.color)
|
||||
.constrain("parent_top?spring", "spring_top?bot", "Plane")
|
||||
.constrain("parent_top?directrix0", "spring_top?directrix_top", "Axis")
|
||||
# Bottom parent joint
|
||||
.add(self.shoulder_torsion_joint.spring(), name="spring_bot",
|
||||
color=Role.DAMPING.color)
|
||||
.constrain("child/track_bot?spring", "spring_bot?top", "Plane")
|
||||
.constrain("child/track_bot?directrix", "spring_bot?directrix_bot", "Axis")
|
||||
.add(self.shoulder_parent_joint(), name="parent_bot",
|
||||
color=Role.PARENT.color)
|
||||
.constrain("parent_bot?spring", "spring_bot?bot", "Plane")
|
||||
.constrain("parent_bot?directrix0", "spring_bot?directrix_top", "Axis")
|
||||
.solve()
|
||||
)
|
||||
return result
|
||||
|
||||
@assembly()
|
||||
def wing_r1s1_assembly(self) -> Cq.Assembly:
|
||||
result = (
|
||||
|
@ -427,6 +543,13 @@ class Parameters(Model):
|
|||
.constrain(f"panel_front?{tag}", f"{tag}_spacer?mate1", "Plane")
|
||||
.constrain(f"panel_back?{tag}", f"{tag}_spacer?mate2", "Plane")
|
||||
)
|
||||
(
|
||||
result
|
||||
.add(self.shoulder_assembly(), name="shoulder")
|
||||
.constrain("shoulder_bot_spacer?dir",
|
||||
"shoulder/child/core?mate_bot",
|
||||
"Plane")
|
||||
)
|
||||
|
||||
result.solve()
|
||||
return result
|
||||
|
|
Loading…
Reference in New Issue