diff --git a/scripts/generators/prism.py b/scripts/generators/prism.py new file mode 100644 index 0000000..566d824 --- /dev/null +++ b/scripts/generators/prism.py @@ -0,0 +1,393 @@ +"""Planar prism solids written as STEP (AP214) or IGES 5.3 (186 manifold solid B-reps), with +assemblies: STEP next-assembly-usage occurrences placed by item-defined transformations, IGES +subfigure definitions (308) placed by singular subfigure instances (408). + +Standard library only. Every model here is generated from code, so the files are reproducible: +run the model's script and compare the SHA-256 with its README. + +A part is a polygon (counter-clockwise, in the XY plane, millimetres) extruded along +Z by a +height. Faces: bottom, top and one side per polygon edge, all planar, normals outward. +""" + +import math + + +# ---------- geometry ---------- + +def regular(n, r, phase=0.0): + return [(r * math.cos(phase + 2 * math.pi * i / n), r * math.sin(phase + 2 * math.pi * i / n)) for i in range(n)] + + +def rect(w, d): + return [(-w / 2, -d / 2), (w / 2, -d / 2), (w / 2, d / 2), (-w / 2, d / 2)] + + +def l_section(w, d, t): + """An L: a w x d bounding box with legs t thick (not convex).""" + return [(0, 0), (w, 0), (w, t), (t, t), (t, d), (0, d)] + + +def area2(poly): + return sum(poly[i][0] * poly[(i + 1) % len(poly)][1] - poly[(i + 1) % len(poly)][0] * poly[i][1] for i in range(len(poly))) + + +class Part: + def __init__(self, name, poly, height): + if area2(poly) <= 0: + raise ValueError(f"{name}: polygon must be counter-clockwise") + self.name, self.poly, self.h = name, poly, height + + @property + def faces(self): + return len(self.poly) + 2 + + @property + def edges(self): + return 3 * len(self.poly) + + @property + def vertices(self): + return 2 * len(self.poly) + + +# A placement is (rotation 3x3 as rows, translation). Rotations are built from axis angles. + +def rot(axis, deg): + a = math.radians(deg) + c, s = math.cos(a), math.sin(a) + x, y, z = axis + n = math.sqrt(x * x + y * y + z * z) + x, y, z = x / n, y / n, z / n + return [[c + x * x * (1 - c), x * y * (1 - c) - z * s, x * z * (1 - c) + y * s], + [y * x * (1 - c) + z * s, c + y * y * (1 - c), y * z * (1 - c) - x * s], + [z * x * (1 - c) - y * s, z * y * (1 - c) + x * s, c + z * z * (1 - c)]] + + +IDENTITY = [[1, 0, 0], [0, 1, 0], [0, 0, 1]] + + +def matmul(a, b): + return [[sum(a[i][k] * b[k][j] for k in range(3)) for j in range(3)] for i in range(3)] + + +class Node: + """An assembly: a name and children (Part or Node), each with a rotation and translation.""" + + def __init__(self, name, children=None): + self.name = name + self.children = children or [] # [(Part | Node, R, t)] + + def add(self, child, R=IDENTITY, t=(0, 0, 0)): + self.children.append((child, R, tuple(t))) + return self + + +def count(node): + """(solid instances, face instances, distinct parts, assembly nodes, occurrences).""" + parts, nodes = set(), set() + def walk(n): + nodes.add(id(n)) + s = f = o = 0 + for c, _, _ in n.children: + o += 1 + if isinstance(c, Part): + parts.add(id(c)); s += 1; f += c.faces + else: + a, b, d = walk(c); s += a; f += b; o += d + return s, f, o + s, f, o = walk(node) + return dict(solids=s, faces=f, distinct_parts=len(parts), assemblies=len(nodes), occurrences=o) + + +def f6(x): + """A real in the shortest form that round-trips at 1e-9 mm, never '-0.'.""" + v = round(x, 9) + if v == 0: + return "0." + s = repr(v) + if "e" in s or "E" in s: + s = f"{v:.12E}" + return s + return s if "." in s else s + "." + + +# ---------- STEP AP214 ---------- + +def step_text(root, file_name, description): + out = [] + + def add(s): + out.append(s) + return len(out) + + def pt(p): + return add(f"CARTESIAN_POINT('',({f6(p[0])},{f6(p[1])},{f6(p[2])}))") + + def dr(d): + return add(f"DIRECTION('',({f6(d[0])},{f6(d[1])},{f6(d[2])}))") + + def ax(o, z, x): + return add(f"AXIS2_PLACEMENT_3D('',#{pt(o)},#{dr(z)},#{dr(x)})") + + # context + mm = add("(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.))") + rad = add("(NAMED_UNIT(*)PLANE_ANGLE_UNIT()SI_UNIT($,.RADIAN.))") + sr = add("(NAMED_UNIT(*)SI_UNIT($,.STERADIAN.)SOLID_ANGLE_UNIT())") + unc = add(f"UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-06),#{mm},'distance_accuracy_value','')") + ctx = add(f"(GEOMETRIC_REPRESENTATION_CONTEXT(3)GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#{unc}))" + f"GLOBAL_UNIT_ASSIGNED_CONTEXT((#{mm},#{rad},#{sr}))REPRESENTATION_CONTEXT('',''))") + appctx = add("APPLICATION_CONTEXT('core data for automotive mechanical design processes')") + add(f"APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',2000,#{appctx})") + pctx = add(f"PRODUCT_CONTEXT('',#{appctx},'mechanical')") + pdctx = add(f"PRODUCT_DEFINITION_CONTEXT('part definition',#{appctx},'design')") + + def product(name): + p = add(f"PRODUCT('{name}','{name}','',(#{pctx}))") + add(f"PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#{p}))") + f = add(f"PRODUCT_DEFINITION_FORMATION('','',#{p})") + pd = add(f"PRODUCT_DEFINITION('design','',#{f},#{pdctx})") + pds = add(f"PRODUCT_DEFINITION_SHAPE('','',#{pd})") + return pd, pds + + origin = None + + def brep(part): + n, h = len(part.poly), part.h + P = [(x, y, 0.0) for x, y in part.poly] + [(x, y, h) for x, y in part.poly] + V = [add(f"VERTEX_POINT('',#{pt(p)})") for p in P] + + def edge(a, b): + pa, pb = P[a], P[b] + d = [pb[k] - pa[k] for k in range(3)] + L = math.sqrt(sum(x * x for x in d)) + u = [x / L for x in d] + vec = add(f"VECTOR('',#{dr(u)},{f6(L)})") + line = add(f"LINE('',#{pt(pa)},#{vec})") + return add(f"EDGE_CURVE('',#{V[a]},#{V[b]},#{line},.T.)") + + bot = [edge(i, (i + 1) % n) for i in range(n)] + top = [edge(n + i, n + (i + 1) % n) for i in range(n)] + ver = [edge(i, n + i) for i in range(n)] + + def oe(e, fwd): + return f"#{add(f'ORIENTED_EDGE({chr(39)}{chr(39)},*,*,#{e},{'.T.' if fwd else '.F.'})')}" + + def face(oes, o, z, x): + loop = add(f"EDGE_LOOP('',({','.join(oes)}))") + bound = add(f"FACE_OUTER_BOUND('',#{loop},.T.)") + plane = add(f"PLANE('',#{ax(o, z, x)})") + return add(f"ADVANCED_FACE('',(#{bound}),#{plane},.T.)") + + faces = [face([oe(bot[i], False) for i in reversed(range(n))], P[0], (0, 0, -1), (1, 0, 0)), + face([oe(top[i], True) for i in range(n)], P[n], (0, 0, 1), (1, 0, 0))] + for i in range(n): + j = (i + 1) % n + dx, dy = P[j][0] - P[i][0], P[j][1] - P[i][1] + L = math.hypot(dx, dy) + faces.append(face([oe(bot[i], True), oe(ver[j], True), oe(top[i], False), oe(ver[i], False)], + P[i], (dy / L, -dx / L, 0), (dx / L, dy / L, 0))) + shell = add(f"CLOSED_SHELL('',({','.join(f'#{f}' for f in faces)}))") + return add(f"MANIFOLD_SOLID_BREP('{part.name}',#{shell})") + + reps, pds_of = {}, {} + + def define(node): + """Product + shape representation for a part or an assembly (once each).""" + nonlocal origin + if id(node) in reps: + return reps[id(node)] + pd, pds = product(node.name) + o = ax((0, 0, 0), (0, 0, 1), (1, 0, 0)) + if isinstance(node, Part): + rep = add(f"ADVANCED_BREP_SHAPE_REPRESENTATION('{node.name}',(#{o},#{brep(node)}),#{ctx})") + else: + rep = add(f"SHAPE_REPRESENTATION('{node.name}',(#{o}),#{ctx})") + add(f"SHAPE_DEFINITION_REPRESENTATION(#{pds},#{rep})") + reps[id(node)] = (pd, rep, o) + if not isinstance(node, Part): + k = 0 + for child, R, t in node.children: + cpd, crep, co = define(child) + k += 1 + nm = f"{child.name}:{k}" + nauo = add(f"NEXT_ASSEMBLY_USAGE_OCCURRENCE('{nm}','{nm}','',#{pd},#{cpd},$)") + npds = add(f"PRODUCT_DEFINITION_SHAPE('','',#{nauo})") + z = (R[0][2], R[1][2], R[2][2]) + x = (R[0][0], R[1][0], R[2][0]) + tgt = ax(t, z, x) + # the child's origin (in the child's representation) maps to the placement in the parent's + idt = add(f"ITEM_DEFINED_TRANSFORMATION('','',#{co},#{tgt})") + rr = add(f"(REPRESENTATION_RELATIONSHIP('','',#{crep},#{rep})REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#{idt})SHAPE_REPRESENTATION_RELATIONSHIP())") + add(f"CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#{rr},#{npds})") + return reps[id(node)] + + define(root) + head = ("ISO-10303-21;\nHEADER;\n" + f"FILE_DESCRIPTION(('{description}'),'2;1');\n" + f"FILE_NAME('{file_name}','2026-10-20T00:00:00',('raykholo'),(''),'tangency-labs/models generator','python3 scripts/generators','');\n" + "FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));\nENDSEC;\nDATA;\n") + body = "".join(f"#{i + 1}={s};\n" for i, s in enumerate(out)) + return head + body + "ENDSEC;\nEND-ISO-10303-21;\n" + + +# ---------- IGES 5.3 ---------- + +class Iges: + """Directory entries and parameter data, written in fixed 80-column records.""" + + def __init__(self): + self.ents = [] # (type, form, params list, status, matrix_de, label) + + def add(self, typ, params, form=0, status="00000000", matrix=0, label=""): + self.ents.append([typ, form, params, status, matrix, label]) + return 2 * len(self.ents) - 1 # the entity's DE sequence number + + def set_status(self, de, status): + self.ents[(de - 1) // 2][3] = status + + def text(self, file_name, max_coord): + def h(s): + return f"{len(s)}H{s}" + g = [h(","), h(";"), h("tangency-labs/models generator"), h(file_name), h("python3 scripts/generators"), + h("prism.py IGES 5.3"), "32", "38", "6", "308", "15", h(file_name), "1.", "2", h("MM"), "1", "1.", + h("20261020.000000"), "1.E-06", f6(max_coord), h("raykholo"), h(""), "11", "0", h("20261020.000000")] + s_lines = ["Generated by scripts/generators (github.com/tangency-labs/models), CC0"] + + def wrap(items, width, end): + """Join parameters with ',' and end with ';', split into lines of at most width chars.""" + lines, cur = [], "" + for i, it in enumerate(items): + tok = it + (end if i == len(items) - 1 else ",") + if len(cur) + len(tok) > width: + lines.append(cur); cur = "" + cur += tok + lines.append(cur) + return lines + + g_lines = wrap(g, 72, ";") + d_lines, p_lines = [], [] + for idx, (typ, form, params, status, matrix, label) in enumerate(self.ents): + de = 2 * idx + 1 + pl = wrap([str(typ)] + [str(p) for p in params], 64, ";") + start = len(p_lines) + 1 + for line in pl: + p_lines.append(f"{line:<64}{de:>8}") + d_lines.append(f"{typ:>8}{start:>8}{0:>8}{0:>8}{0:>8}{0:>8}{matrix:>8}{0:>8}{status:>8}") + d_lines.append(f"{typ:>8}{0:>8}{0:>8}{len(pl):>8}{form:>8}{'':>8}{'':>8}{label[:8]:>8}{0:>8}") + rec = [] + rec += [f"{l:<72}S{i + 1:>7}" for i, l in enumerate(s_lines)] + rec += [f"{l:<72}G{i + 1:>7}" for i, l in enumerate(g_lines)] + rec += [f"{l:<72}D{i + 1:>7}" for i, l in enumerate(d_lines)] + rec += [f"{l:<72}P{i + 1:>7}" for i, l in enumerate(p_lines)] + rec.append(f"S{len(s_lines):>7}G{len(g_lines):>7}D{len(d_lines):>7}P{len(p_lines):>7}{'':<40}T{1:>7}") + return "\n".join(rec) + "\n" + + +DEP = "00010000" # physically dependent +DEF = "00000200" # independent, used as a definition (subfigure definitions) + + +def iges_solid(ig, part): + """A 186 manifold solid B-rep for the prism; every entity under it is physically dependent.""" + n, hgt = len(part.poly), part.h + P = [(x, y, 0.0) for x, y in part.poly] + [(x, y, hgt) for x, y in part.poly] + vl = ig.add(502, [len(P)] + [f6(c) for p in P for c in p], form=1, status=DEP) + edges = [] # (a, b) vertex indices, 1-based in the vertex list + + def line(a, b): + pa, pb = P[a], P[b] + return ig.add(110, [f6(c) for c in pa + pb], status=DEP) + + E = [(i, (i + 1) % n) for i in range(n)] + [(n + i, n + (i + 1) % n) for i in range(n)] + [(i, n + i) for i in range(n)] + curves = [line(a, b) for a, b in E] + params = [len(E)] + for (a, b), c in zip(E, curves): + params += [c, vl, a + 1, vl, b + 1] + el = ig.add(504, params, form=1, status=DEP) + bot = list(range(n)); top = [n + i for i in range(n)]; ver = [2 * n + i for i in range(n)] + + def loop(es): # [(edge index 0-based, forward)] + p = [len(es)] + for e, fwd in es: + p += [0, el, e + 1, 1 if fwd else 0, 0] + return ig.add(508, p, form=1, status=DEP) + + def plane(o, z): + pt = ig.add(116, [f6(c) for c in o], status=DEP) + dn = ig.add(123, [f6(c) for c in z], status=DEP) + return ig.add(190, [pt, dn], status=DEP) + + def face(es, o, z): + return ig.add(510, [plane(o, z), 1, 1, loop(es)], form=1, status=DEP) + + faces = [face([(bot[i], False) for i in reversed(range(n))], P[0], (0, 0, -1)), + face([(top[i], True) for i in range(n)], P[n], (0, 0, 1))] + for i in range(n): + j = (i + 1) % n + dx, dy = P[j][0] - P[i][0], P[j][1] - P[i][1] + L = math.hypot(dx, dy) + faces.append(face([(bot[i], True), (ver[j], True), (top[i], False), (ver[i], False)], P[i], (dy / L, -dx / L, 0))) + shell = ig.add(514, [len(faces)] + [x for f in faces for x in (f, 1)], form=1, status=DEP) + return ig.add(186, [shell, 1, 0], status=DEP, label=part.name[:8].upper()) + + +def iges_text(root, file_name): + """root: a Node. Every Node becomes a 308 definition (deepest first); every child placement a 408. + The top Node's children are written as independent 408 instances (the file's assembly).""" + ig = Iges() + solid_de, sub_de, depth = {}, {}, {} + + def matrix(R, t): + return ig.add(124, [f6(R[i][j]) if j < 3 else f6(t[i]) for i in range(3) for j in range(4)], status=DEP) + + def part_sub(part): + # a part becomes a one-member subfigure, so every placement is a 408 + if id(part) not in sub_de: + s = iges_solid(ig, part) + sub_de[id(part)] = ig.add(308, [0, f"{len(part.name)}H{part.name}", 1, s], status=DEF) + depth[id(part)] = 0 + return sub_de[id(part)] + + def instance(child, R, t, status): + d = part_sub(child) if isinstance(child, Part) else node_sub(child) + # an unrotated placement uses the 408's own X, Y, Z; a rotated one puts rotation and translation in + # the 408's transformation matrix (124), so both placement mechanisms are exercised (X' = M (S x + T)). + if R == IDENTITY: + return ig.add(408, [d, f6(t[0]), f6(t[1]), f6(t[2]), "1."], status=status) + m = matrix(R, t) + return ig.add(408, [d, "0.", "0.", "0.", "1."], status=status, matrix=m) + + def node_sub(node): + if id(node) not in sub_de: + members = [instance(c, R, t, DEP) for c, R, t in node.children] + dmax = 1 + max(depth[id(c)] for c, _, _ in node.children) + sub_de[id(node)] = ig.add(308, [dmax, f"{len(node.name)}H{node.name}", len(members)] + members, status=DEF) + depth[id(node)] = dmax + return sub_de[id(node)] + + for c, R, t in root.children: + instance(c, R, t, "00000000") + return ig.text(file_name, 1000.0) + + +def expected(root): + """Analytic totals for checking a reader: solid count, total volume (mm3), world bounding box.""" + vol, lo, hi, n = 0.0, [math.inf] * 3, [-math.inf] * 3, 0 + + def walk(node, R, t): + nonlocal vol, n + for c, Rc, tc in node.children: + R2 = matmul(R, Rc) + t2 = [sum(R[i][k] * tc[k] for k in range(3)) + t[i] for i in range(3)] + if isinstance(c, Part): + n += 1 + vol += abs(area2(c.poly)) / 2 * c.h + for x, y in c.poly: + for z in (0.0, c.h): + p = [sum(R2[i][k] * (x, y, z)[k] for k in range(3)) + t2[i] for i in range(3)] + for i in range(3): + lo[i], hi[i] = min(lo[i], p[i]), max(hi[i], p[i]) + else: + walk(c, R2, t2) + walk(root, IDENTITY, [0, 0, 0]) + return dict(solids=n, volume=round(vol, 3), bbox=[round(v, 3) for v in lo + hi]) diff --git a/scripts/generators/untrimmed_surface_body.py b/scripts/generators/untrimmed_surface_body.py new file mode 100644 index 0000000..507abb1 --- /dev/null +++ b/scripts/generators/untrimmed_surface_body.py @@ -0,0 +1,105 @@ +"""step/untrimmed-surface-body: an open surface body (an open box, 100 x 80 x 50 mm, no lid) whose +floor lies on a B-spline surface 4,000 mm square, trimmed to the 100 x 80 mm floor. The body's +bounding box is the trimmed faces' box; a reader that measures or frames the underlying surface +instead reports a box 40 times too wide. + + python3 scripts/generators/untrimmed_surface_body.py step/untrimmed-surface-body +""" + +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from prism import f6 # noqa: E402 + +W, D, H = 100.0, 80.0, 50.0 +BIG = 2000.0 # the floor's surface spans -BIG..BIG in X and Y + + +def step(): + out = [] + + def add(s): + out.append(s) + return len(out) + + def pt(p): + return add(f"CARTESIAN_POINT('',({f6(p[0])},{f6(p[1])},{f6(p[2])}))") + + def dr(d): + return add(f"DIRECTION('',({f6(d[0])},{f6(d[1])},{f6(d[2])}))") + + mm = add("(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.))") + rad = add("(NAMED_UNIT(*)PLANE_ANGLE_UNIT()SI_UNIT($,.RADIAN.))") + sr = add("(NAMED_UNIT(*)SI_UNIT($,.STERADIAN.)SOLID_ANGLE_UNIT())") + unc = add(f"UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-06),#{mm},'distance_accuracy_value','')") + ctx = add(f"(GEOMETRIC_REPRESENTATION_CONTEXT(3)GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#{unc}))" + f"GLOBAL_UNIT_ASSIGNED_CONTEXT((#{mm},#{rad},#{sr}))REPRESENTATION_CONTEXT('',''))") + app = add("APPLICATION_CONTEXT('core data for automotive mechanical design processes')") + add(f"APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',2000,#{app})") + pc = add(f"PRODUCT_CONTEXT('',#{app},'mechanical')") + pdc = add(f"PRODUCT_DEFINITION_CONTEXT('part definition',#{app},'design')") + prod = add(f"PRODUCT('untrimmed-surface-body','untrimmed-surface-body','',(#{pc}))") + add(f"PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#{prod}))") + pdf = add(f"PRODUCT_DEFINITION_FORMATION('','',#{prod})") + pd = add(f"PRODUCT_DEFINITION('design','',#{pdf},#{pdc})") + pds = add(f"PRODUCT_DEFINITION_SHAPE('','',#{pd})") + + # the open box: floor at z=0, four walls up to z=H; vertices bottom 0-3, top 4-7 + P = [(0, 0, 0), (W, 0, 0), (W, D, 0), (0, D, 0), (0, 0, H), (W, 0, H), (W, D, H), (0, D, H)] + V = [add(f"VERTEX_POINT('',#{pt(p)})") for p in P] + E = {} + + def edge(a, b): + if (a, b) in E: + return E[(a, b)], True + if (b, a) in E: + return E[(b, a)], False + pa, pb = P[a], P[b] + d = [pb[k] - pa[k] for k in range(3)] + L = sum(x * x for x in d) ** 0.5 + vec = add(f"VECTOR('',#{dr([x / L for x in d])},{f6(L)})") + line = add(f"LINE('',#{pt(pa)},#{vec})") + E[(a, b)] = add(f"EDGE_CURVE('',#{V[a]},#{V[b]},#{line},.T.)") + return E[(a, b)], True + + def loop(vs): + oes = [] + for i in range(len(vs)): + e, fwd = edge(vs[i], vs[(i + 1) % len(vs)]) + oes.append(f"#{add(f'ORIENTED_EDGE({chr(39)}{chr(39)},*,*,#{e},{chr(46)}{chr(84) if fwd else chr(70)}{chr(46)})')}") + lp = add(f"EDGE_LOOP('',({','.join(oes)}))") + return add(f"FACE_OUTER_BOUND('',#{lp},.T.)") + + def plane(o, z, x): + a = add(f"AXIS2_PLACEMENT_3D('',#{pt(o)},#{dr(z)},#{dr(x)})") + return add(f"PLANE('',#{a})") + + # the floor's surface: a flat bilinear B-spline patch, 4,000 mm square, normal -Z (outward, down) + cp = [[pt((-BIG, -BIG, 0)), pt((BIG, -BIG, 0))], [pt((-BIG, BIG, 0)), pt((BIG, BIG, 0))]] + rows = ",".join("(" + ",".join(f"#{c}" for c in r) + ")" for r in cp) + # u along +Y, v along +X: du x dv = Y x X = -Z, so the surface normal points out of the box + bss = add(f"B_SPLINE_SURFACE_WITH_KNOTS('',1,1,({rows}),.PLANE_SURF.,.F.,.F.,.F.,(2,2),(2,2),(0.,1.),(0.,1.),.UNSPECIFIED.)") + faces = [add(f"ADVANCED_FACE('floor',(#{loop([0, 3, 2, 1])}),#{bss},.T.)")] + walls = [([0, 1, 5, 4], (0, -1, 0), (1, 0, 0)), ([1, 2, 6, 5], (1, 0, 0), (0, 1, 0)), + ([2, 3, 7, 6], (0, 1, 0), (-1, 0, 0)), ([3, 0, 4, 7], (-1, 0, 0), (0, -1, 0))] + for vs, n, x in walls: + faces.append(add(f"ADVANCED_FACE('',(#{loop(vs)}),#{plane(P[vs[0]], n, x)},.T.)")) + shell = add(f"OPEN_SHELL('',({','.join(f'#{f}' for f in faces)}))") + model = add(f"SHELL_BASED_SURFACE_MODEL('untrimmed-surface-body',(#{shell}))") + o = add(f"AXIS2_PLACEMENT_3D('',#{pt((0, 0, 0))},#{dr((0, 0, 1))},#{dr((1, 0, 0))})") + rep = add(f"MANIFOLD_SURFACE_SHAPE_REPRESENTATION('untrimmed-surface-body',(#{o},#{model}),#{ctx})") + add(f"SHAPE_DEFINITION_REPRESENTATION(#{pds},#{rep})") + head = ("ISO-10303-21;\nHEADER;\nFILE_DESCRIPTION(('open surface body on a much larger surface'),'2;1');\n" + "FILE_NAME('untrimmed-surface-body.step','2026-10-20T00:00:00',('raykholo'),(''),'tangency-labs/models generator','python3 scripts/generators','');\n" + "FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));\nENDSEC;\nDATA;\n") + return head + "".join(f"#{i + 1}={s};\n" for i, s in enumerate(out)) + "ENDSEC;\nEND-ISO-10303-21;\n" + + +if __name__ == "__main__": + out = sys.argv[1] if len(sys.argv) > 1 else "." + os.makedirs(out, exist_ok=True) + path = os.path.join(out, "untrimmed-surface-body.step") + with open(path, "w", newline="\n") as f: + f.write(step()) + print(path, dict(faces=5, area=W * D + 2 * (W + D) * H, bbox=[0, 0, 0, W, D, H], surface_extent=[-BIG, -BIG, BIG, BIG])) diff --git a/step/untrimmed-surface-body/README.md b/step/untrimmed-surface-body/README.md new file mode 100644 index 0000000..b9219cb --- /dev/null +++ b/step/untrimmed-surface-body/README.md @@ -0,0 +1,34 @@ +# untrimmed-surface-body + +An open surface body (a box without a lid, 100 × 80 × 50 mm) whose floor lies on a B-spline surface +4,000 mm square, trimmed to the 100 × 80 mm floor. The body's bounding box is 100 × 80 × 50 mm; +its floor's underlying surface is 40 times wider. + +- **File:** `untrimmed-surface-body.step` +- **Author:** raykholo +- **Made with:** `scripts/generators/untrimmed_surface_body.py` (Python 3, standard library only). + It writes STEP AP214 (`AUTOMOTIVE_DESIGN`) directly, in millimetres, with a distance accuracy of + 1e-6 mm. Run `python3 scripts/generators/untrimmed_surface_body.py step/untrimmed-surface-body` to + reproduce the file byte for byte. +- **Application protocol:** AP214, as a manifold surface shape representation (one shell-based + surface model, one open shell). +- **Size:** 1 surface body; 5 faces (4 planes and 1 bilinear B-spline surface), 12 edges, 8 + vertices. 6.3 KB. +- **SHA-256:** `2decd6561114628a67d4e3ee396f56ee4ae1dfbdacc2ef4acfe1b5841ca4552b` + +## Why it is interesting + +Surface models exported from CAD often keep a face's whole underlying surface, much larger than the +trimmed face. A reader that measures, frames or bounds the surface instead of the trimmed face +reports a box of about 4,000 × 4,000 × 50 mm, zooms the view out to it, or sizes meshing and +tolerances from it. The faces themselves are simple, so any difference comes from the bounding. + +## Check values + +| | | +|---|---| +| Bodies | 1 open surface body (no solid) | +| Faces | 5 | +| Area | 26,000 mm² | +| Bounding box | x 0 … 100, y 0 … 80, z 0 … 50 mm | +| Floor's underlying surface | x −2,000 … 2,000, y −2,000 … 2,000 mm at z 0 | diff --git a/step/untrimmed-surface-body/untrimmed-surface-body.step b/step/untrimmed-surface-body/untrimmed-surface-body.step new file mode 100644 index 0000000..76debca --- /dev/null +++ b/step/untrimmed-surface-body/untrimmed-surface-body.step @@ -0,0 +1,167 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('open surface body on a much larger surface'),'2;1'); +FILE_NAME('untrimmed-surface-body.step','2026-10-20T00:00:00',('raykholo'),(''),'tangency-labs/models generator','python3 scripts/generators',''); +FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }')); +ENDSEC; +DATA; +#1=(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.)); +#2=(NAMED_UNIT(*)PLANE_ANGLE_UNIT()SI_UNIT($,.RADIAN.)); +#3=(NAMED_UNIT(*)SI_UNIT($,.STERADIAN.)SOLID_ANGLE_UNIT()); +#4=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-06),#1,'distance_accuracy_value',''); +#5=(GEOMETRIC_REPRESENTATION_CONTEXT(3)GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#4))GLOBAL_UNIT_ASSIGNED_CONTEXT((#1,#2,#3))REPRESENTATION_CONTEXT('','')); +#6=APPLICATION_CONTEXT('core data for automotive mechanical design processes'); +#7=APPLICATION_PROTOCOL_DEFINITION('international standard','automotive_design',2000,#6); +#8=PRODUCT_CONTEXT('',#6,'mechanical'); +#9=PRODUCT_DEFINITION_CONTEXT('part definition',#6,'design'); +#10=PRODUCT('untrimmed-surface-body','untrimmed-surface-body','',(#8)); +#11=PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#10)); +#12=PRODUCT_DEFINITION_FORMATION('','',#10); +#13=PRODUCT_DEFINITION('design','',#12,#9); +#14=PRODUCT_DEFINITION_SHAPE('','',#13); +#15=CARTESIAN_POINT('',(0.,0.,0.)); +#16=VERTEX_POINT('',#15); +#17=CARTESIAN_POINT('',(100.0,0.,0.)); +#18=VERTEX_POINT('',#17); +#19=CARTESIAN_POINT('',(100.0,80.0,0.)); +#20=VERTEX_POINT('',#19); +#21=CARTESIAN_POINT('',(0.,80.0,0.)); +#22=VERTEX_POINT('',#21); +#23=CARTESIAN_POINT('',(0.,0.,50.0)); +#24=VERTEX_POINT('',#23); +#25=CARTESIAN_POINT('',(100.0,0.,50.0)); +#26=VERTEX_POINT('',#25); +#27=CARTESIAN_POINT('',(100.0,80.0,50.0)); +#28=VERTEX_POINT('',#27); +#29=CARTESIAN_POINT('',(0.,80.0,50.0)); +#30=VERTEX_POINT('',#29); +#31=CARTESIAN_POINT('',(-2000.0,-2000.0,0.)); +#32=CARTESIAN_POINT('',(2000.0,-2000.0,0.)); +#33=CARTESIAN_POINT('',(-2000.0,2000.0,0.)); +#34=CARTESIAN_POINT('',(2000.0,2000.0,0.)); +#35=B_SPLINE_SURFACE_WITH_KNOTS('',1,1,((#31,#32),(#33,#34)),.PLANE_SURF.,.F.,.F.,.F.,(2,2),(2,2),(0.,1.),(0.,1.),.UNSPECIFIED.); +#36=DIRECTION('',(0.,1.0,0.)); +#37=VECTOR('',#36,80.0); +#38=CARTESIAN_POINT('',(0.,0.,0.)); +#39=LINE('',#38,#37); +#40=EDGE_CURVE('',#16,#22,#39,.T.); +#41=ORIENTED_EDGE('',*,*,#40,.T.); +#42=DIRECTION('',(1.0,0.,0.)); +#43=VECTOR('',#42,100.0); +#44=CARTESIAN_POINT('',(0.,80.0,0.)); +#45=LINE('',#44,#43); +#46=EDGE_CURVE('',#22,#20,#45,.T.); +#47=ORIENTED_EDGE('',*,*,#46,.T.); +#48=DIRECTION('',(0.,-1.0,0.)); +#49=VECTOR('',#48,80.0); +#50=CARTESIAN_POINT('',(100.0,80.0,0.)); +#51=LINE('',#50,#49); +#52=EDGE_CURVE('',#20,#18,#51,.T.); +#53=ORIENTED_EDGE('',*,*,#52,.T.); +#54=DIRECTION('',(-1.0,0.,0.)); +#55=VECTOR('',#54,100.0); +#56=CARTESIAN_POINT('',(100.0,0.,0.)); +#57=LINE('',#56,#55); +#58=EDGE_CURVE('',#18,#16,#57,.T.); +#59=ORIENTED_EDGE('',*,*,#58,.T.); +#60=EDGE_LOOP('',(#41,#47,#53,#59)); +#61=FACE_OUTER_BOUND('',#60,.T.); +#62=ADVANCED_FACE('floor',(#61),#35,.T.); +#63=ORIENTED_EDGE('',*,*,#58,.F.); +#64=DIRECTION('',(0.,0.,1.0)); +#65=VECTOR('',#64,50.0); +#66=CARTESIAN_POINT('',(100.0,0.,0.)); +#67=LINE('',#66,#65); +#68=EDGE_CURVE('',#18,#26,#67,.T.); +#69=ORIENTED_EDGE('',*,*,#68,.T.); +#70=DIRECTION('',(-1.0,0.,0.)); +#71=VECTOR('',#70,100.0); +#72=CARTESIAN_POINT('',(100.0,0.,50.0)); +#73=LINE('',#72,#71); +#74=EDGE_CURVE('',#26,#24,#73,.T.); +#75=ORIENTED_EDGE('',*,*,#74,.T.); +#76=DIRECTION('',(0.,0.,-1.0)); +#77=VECTOR('',#76,50.0); +#78=CARTESIAN_POINT('',(0.,0.,50.0)); +#79=LINE('',#78,#77); +#80=EDGE_CURVE('',#24,#16,#79,.T.); +#81=ORIENTED_EDGE('',*,*,#80,.T.); +#82=EDGE_LOOP('',(#63,#69,#75,#81)); +#83=FACE_OUTER_BOUND('',#82,.T.); +#84=CARTESIAN_POINT('',(0.,0.,0.)); +#85=DIRECTION('',(0.,-1.,0.)); +#86=DIRECTION('',(1.,0.,0.)); +#87=AXIS2_PLACEMENT_3D('',#84,#85,#86); +#88=PLANE('',#87); +#89=ADVANCED_FACE('',(#83),#88,.T.); +#90=ORIENTED_EDGE('',*,*,#52,.F.); +#91=DIRECTION('',(0.,0.,1.0)); +#92=VECTOR('',#91,50.0); +#93=CARTESIAN_POINT('',(100.0,80.0,0.)); +#94=LINE('',#93,#92); +#95=EDGE_CURVE('',#20,#28,#94,.T.); +#96=ORIENTED_EDGE('',*,*,#95,.T.); +#97=DIRECTION('',(0.,-1.0,0.)); +#98=VECTOR('',#97,80.0); +#99=CARTESIAN_POINT('',(100.0,80.0,50.0)); +#100=LINE('',#99,#98); +#101=EDGE_CURVE('',#28,#26,#100,.T.); +#102=ORIENTED_EDGE('',*,*,#101,.T.); +#103=ORIENTED_EDGE('',*,*,#68,.F.); +#104=EDGE_LOOP('',(#90,#96,#102,#103)); +#105=FACE_OUTER_BOUND('',#104,.T.); +#106=CARTESIAN_POINT('',(100.0,0.,0.)); +#107=DIRECTION('',(1.,0.,0.)); +#108=DIRECTION('',(0.,1.,0.)); +#109=AXIS2_PLACEMENT_3D('',#106,#107,#108); +#110=PLANE('',#109); +#111=ADVANCED_FACE('',(#105),#110,.T.); +#112=ORIENTED_EDGE('',*,*,#46,.F.); +#113=DIRECTION('',(0.,0.,1.0)); +#114=VECTOR('',#113,50.0); +#115=CARTESIAN_POINT('',(0.,80.0,0.)); +#116=LINE('',#115,#114); +#117=EDGE_CURVE('',#22,#30,#116,.T.); +#118=ORIENTED_EDGE('',*,*,#117,.T.); +#119=DIRECTION('',(1.0,0.,0.)); +#120=VECTOR('',#119,100.0); +#121=CARTESIAN_POINT('',(0.,80.0,50.0)); +#122=LINE('',#121,#120); +#123=EDGE_CURVE('',#30,#28,#122,.T.); +#124=ORIENTED_EDGE('',*,*,#123,.T.); +#125=ORIENTED_EDGE('',*,*,#95,.F.); +#126=EDGE_LOOP('',(#112,#118,#124,#125)); +#127=FACE_OUTER_BOUND('',#126,.T.); +#128=CARTESIAN_POINT('',(100.0,80.0,0.)); +#129=DIRECTION('',(0.,1.,0.)); +#130=DIRECTION('',(-1.,0.,0.)); +#131=AXIS2_PLACEMENT_3D('',#128,#129,#130); +#132=PLANE('',#131); +#133=ADVANCED_FACE('',(#127),#132,.T.); +#134=ORIENTED_EDGE('',*,*,#40,.F.); +#135=ORIENTED_EDGE('',*,*,#80,.F.); +#136=DIRECTION('',(0.,1.0,0.)); +#137=VECTOR('',#136,80.0); +#138=CARTESIAN_POINT('',(0.,0.,50.0)); +#139=LINE('',#138,#137); +#140=EDGE_CURVE('',#24,#30,#139,.T.); +#141=ORIENTED_EDGE('',*,*,#140,.T.); +#142=ORIENTED_EDGE('',*,*,#117,.F.); +#143=EDGE_LOOP('',(#134,#135,#141,#142)); +#144=FACE_OUTER_BOUND('',#143,.T.); +#145=CARTESIAN_POINT('',(0.,80.0,0.)); +#146=DIRECTION('',(-1.,0.,0.)); +#147=DIRECTION('',(0.,-1.,0.)); +#148=AXIS2_PLACEMENT_3D('',#145,#146,#147); +#149=PLANE('',#148); +#150=ADVANCED_FACE('',(#144),#149,.T.); +#151=OPEN_SHELL('',(#62,#89,#111,#133,#150)); +#152=SHELL_BASED_SURFACE_MODEL('untrimmed-surface-body',(#151)); +#153=CARTESIAN_POINT('',(0.,0.,0.)); +#154=DIRECTION('',(0.,0.,1.)); +#155=DIRECTION('',(1.,0.,0.)); +#156=AXIS2_PLACEMENT_3D('',#153,#154,#155); +#157=MANIFOLD_SURFACE_SHAPE_REPRESENTATION('untrimmed-surface-body',(#156,#152),#5); +#158=SHAPE_DEFINITION_REPRESENTATION(#14,#157); +ENDSEC; +END-ISO-10303-21;