--- jupyter: description: 'An inclined block as a 3D CAD exercise: a dimensioned drawing with a view onto the slope to model in planex2d or any CAD system, a solution with a counterbored hole sketched on the slope and dimensioned from its edges, and the volume, mass and center of mass to check a model against.' --- # Inclined block An inclined block holds a part at an angle to its base, such as a sensor that looks at a workpiece from the side. The part is screwed to the slope through a counterbored hole square to it, and the block is bolted down through two holes in its top. A sketch on the slope has the slope's own plane, in which the global origin and axes are of no help: the counterbore is dimensioned from the slope's edges. Model the block from the drawing in planex2d or in any CAD system, then check its volume, mass and center of mass against the values at the end of the page. A solution in planex2d follows. ![The inclined block exercise: a dimensioned drawing of a block with a slope at 30 degrees, a counterbored hole square to the slope and two holes in the top, in a front view, a top view, a view onto the slope and an isometric](../_static/gallery/inclined-block.png){.result} ```python import math import netgen.occ as occ import planex as px ``` ```python nbsphinx="hidden" from docs_figures import Row, Views ``` ## The profile The profile is the front view, sketched in the front plane, the plane with its normal toward the viewer, $-y$. It has $x$ to the right and $z$ up, with the origin at the block's bottom left front corner. The outline runs from the origin along the bottom, up the right end, up the slope to the left, along the top and down the left end. With `auto_constraint={"horizontal": True, "vertical": True}`, the lines drawn level or plumb stay so. The slope and the top get names, which the faces padded from them carry. The block is 80 long and 45 high at its left end and 20 high at its right end. The slope falls from the top at 30°, the angle that `px.Angle` measures between the slope and the top, and the length of the top follows from these. With them, nothing in the sketch is left free (Figure 1): ```python front = px.XZ profile = px.Sketch(front, auto_constraint={"horizontal": True, "vertical": True}) profile.Line(80, id="bottom") profile.Turn(90).Line(20, id="right_end") # Turn(90) turns the pen left, Turn(-90) right profile.Turn(60).Line(50, id="slope", name="slope") profile.Turn(30).Line(37, id="top", name="top") profile.Close(px.Vertical, id="left_end") # drawn only roughly plumb: the flag holds it profile.Constrain("bottom.start", "origin", px.Coincident) profile.Constrain("bottom", px.Distance, 80, id="length") profile.Constrain("left_end", px.Distance, 45, id="height") profile.Constrain("right_end", px.Distance, 20, id="end_height") profile.Constrain("slope", "top", px.Angle, 30, id="slope_angle") # in degrees print("status", profile.status, "| degrees of freedom", profile.dof) print("length of the top", round(profile.Measure("top")["length"], 3), "| 80 - 25 / tan 30°", round(80 - 25 / math.tan(math.radians(30)), 3)) ``` ```python nbsphinx="hide-input" Row((profile, "Every dimension and tie of the profile", {"constraints": True}), caption="The fully constrained profile") ``` ## The block `px.Pad` extrudes the profile 40 deep, with `reverse=True` away from the viewer, so that the block lies behind the front plane, from $y = 0$ to 40 (Figure 2): ```python block = px.Pad(profile, 40, reverse=True) print("volume", block.mass, "area × 40", profile.Face().mass * 40) ``` ```python nbsphinx="hide-input" Views(("The profile extruded 40 deep", [(block, "shade"), (block, "ink")]), caption="The block") ``` ## The counterbore The counterbore is sketched on the slope. A sketch on a face takes the face's plane, with the global origin projected onto it and $x$ projected as its $h$ axis, so on the slope $h$ runs down the slope and the origin lies off the part: ```python slope = block.faces["slope"][0] # the one face of that name counterbore = px.Sketch(slope) print("(0, 0) lies at", counterbore.ToXYZ((0, 0)), "| (10, 0) at", counterbore.ToXYZ((10, 0))) ``` The counterbore is dimensioned from the slope's edges, the lower one, lowest in $z$, and the front one, lowest in $y$, both projected into the sketch. The counterbore, 11 across, starts at the middle of the face, where `ToHV` gives the face's center in the sketch's coordinates. Its dimensions move it to 20 from the lower edge and 20 from the front edge. It is pocketed 6.5 deep, square to the slope (Figure 3): ```python counterbore.Project(slope.edges.Min(occ.Z), id="lower_edge") counterbore.Project(slope.edges.Min(occ.Y), id="front_edge") counterbore.Circle(11 / 2, center=counterbore.ToHV(tuple(slope.center)), id="counterbore") # the face's center, as (h, v) counterbore.Constrain("counterbore", px.Diameter, 11, id="counterbore_d") counterbore.Constrain("counterbore.center", "lower_edge.Edge.0", px.Distance, 20, id="from_lower") counterbore.Constrain("counterbore.center", "front_edge.Edge.0", px.Distance, 20, id="from_front") print("status", counterbore.status, "| degrees of freedom", counterbore.dof) block = px.Pocket(block, counterbore, 6.5) # 6.5 deep, against the face's normal: into the block ``` ```python nbsphinx="hide-input" Row((counterbore, "The counterbore on the slope, in the sketch's axes", {"plot": {"show_parent": block, "show_constraints": True}}), (block, "The counterbore in the slope", {"views": [(block, "shade"), (block, "ink")], "angles": (-50, 0, -40)}), caption="The counterbore") ``` The hole for the screw, 6.6 across, goes through the counterbore's center, square to the slope too, and `until=px.last` cuts it on down through the bottom (Figure 4): ```python hole = px.Sketch(slope) hole.Circle(6.6 / 2, center=counterbore.Location("counterbore.center"), id="hole") # the solved center hole.Constrain("hole", px.Diameter, 6.6, id="hole_d") block = px.Pocket(block, hole, until=px.last) # through to the last flat face it crosses ``` ```python nbsphinx="hide-input" Views(("The hole from the counterbore through the bottom", [(block, "shade"), (block, "ink")]), angles=(-50, 0, -40), caption="The hole") ``` ## The holes in the top The holes for the bolts, 9 across, are sketched on the top and dimensioned from its left and front edges. Both are 12 from the left end, the front one 10 from the front edge and the other 20 behind it (Figure 5): ```python top = block.faces["top"][0] holes = px.Sketch(top) holes.Project(top.edges.Min(occ.X), id="left_edge") holes.Project(top.edges.Min(occ.Y), id="front_edge") holes.Circle(9 / 2, center=(10, 10), id="front_hole") holes.Circle(9 / 2, center=(10, 30), id="back_hole") holes.Constrain("front_hole", px.Diameter, 9, id="top_hole_d") holes.Constrain("back_hole", "front_hole", px.Equal) holes.Constrain("front_hole.center", "left_edge.Edge.0", px.Distance, 12, id="top_hole_x") holes.Constrain("back_hole.center", "left_edge.Edge.0", px.Distance, 12) holes.Constrain("front_hole.center", "front_edge.Edge.0", px.Distance, 10, id="top_hole_y") holes.Constrain("front_hole.center", "back_hole.center", px.Distance, 20, id="top_spacing") print("status", holes.status, "| degrees of freedom", holes.dof) block = px.Pocket(block, holes, until=px.last) ``` ```python nbsphinx="hide-input" Row((holes, "The holes on the top, the block under it", {"plot": {"show_parent": block, "show_constraints": True}}), (block, "Both holes through the block", {"views": [(block, "shade"), (block, "ink")], "angles": (-50, 0, -40)}), caption="The holes in the top") ``` ## The checks These are the volume, the mass in steel S235 (7.85 g/cm³) and the center of mass, with the origin at the bottom left front corner, to compare with your own model. The volume is the profile's area, the rectangle 80 by 45 less the triangle above the slope, times 40, less the holes. The screw hole's axis meets the slope $20 + 20 \sin 30^\circ$ above the bottom and runs at 30° to the vertical, so its length up to the slope is $(20 + 20 \sin 30^\circ) / \cos 30^\circ$, of which the counterbore takes 6.5: ```python t = math.radians(30) area = 80 * 45 - 25 * (25 / math.tan(t)) / 2 axis = (20 + 20 * math.sin(t)) / math.cos(t) exact = area * 40 - 2 * math.pi * 4.5**2 * 45 - math.pi * 5.5**2 * 6.5 - math.pi * 3.3**2 * (axis - 6.5) c = block.center print("volume", round(block.mass, 1), "mm³, exact", round(exact, 1)) print("mass", round(block.mass * 7.85e-6, 3), "kg") print("center of mass", tuple(round(v, 2) for v in (c.x, c.y, c.z))) ``` The block as STEP, for another CAD system: ```python block.WriteStep("inclined_block.step") ``` The block as STEP: [inclined_block.step](inclined_block.step) ```python nbsphinx="hidden" import sys sys.path.insert(0, "..") import matplotlib.pyplot as plt import numpy as np from drawings import Pictorial, SheetFigure, View from gallery import save_figure # the views drawn from the block; view A looks square onto the slope, its h along y and its v up the slope s0, s1 = (np.array(profile.Location("slope" + e)) for e in (".start", ".end")) up = (s1 - s0) / np.linalg.norm(s1 - s0) normal = (up[1], 0, -up[0]) # the slope's normal, out of the block, in 3D (x, y, z) view_a = px.Plane(origin=(s0[0], 0, s0[1]), normal=normal, axis=(0, 1, 0)) cb = np.array(counterbore.Measure("counterbore")["center"]) floor = min(block.faces, key=lambda f: math.dist((f.center.x, f.center.y, f.center.z), cb - 6.5 * np.array(normal))) middle = (s0 + s1) / 2 + 3 * np.array([normal[0], normal[2]]) bolts = [holes.Measure(h)["center"] for h in ("front_hole", "back_hole")] front_view = View([(profile, {"length": -12, "height": -12, "end_height": 92, "slope_angle": 22})], solid=block, plane=front, arrows=[(middle, (-normal[0], -normal[2]), "A")], axes=[((b[0], -3), (b[0], 48)) for b in bolts[:1]] + [((cb[0] + 4 * normal[0], cb[2] + 4 * normal[2]), (cb[0] - (axis + 3) * normal[0], cb[2] - (axis + 3) * normal[2]))]) top_view = View([(holes, {"top_hole_x": 52, "top_hole_y": -12, "top_spacing": -12, "top_hole_d": (300, "leader", 2)})], solid=block, plane=px.XY, at=(0, -100)) slope_view = View([(counterbore, {"from_lower": -10, "from_front": -8, "counterbore_d": (45, "leader", None, "↧6,5")}), (hole, {"hole_d": (135, "leader")}, {})], solid=block, plane=view_a, faces=[block.faces["slope"][0], floor], hidden_edges=False, at=(125, -5), label="A") pictorial = Pictorial(block, at=(135, -85), width=70) drawing = SheetFigure([front_view, top_view, slope_view, pictorial], margin=25, notes=("planex2d.org", "3D exercise 05"), first_angle=True) print("scale", drawing.planex_scale, "| paper", [round(v * 25.4) for v in drawing.get_size_inches()], "mm") save_figure(drawing, "inclined-block", dpi=200) plt.close(drawing) ```