app
Adom Chip Thumbnailer
Public Made by Adomby adom
The chip-icon factory: from a STEP file it renders the complete family of chip icons, shaded 3D orientations, transparent icons, name-lasered variants, and 11 vector outline styles, in a live web app + CLI.
master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
#!/usr/bin/env python3
"""Pure-text STEP transforms (no OpenCascade): the mark planner and the etch run on the shared services,
and the only local geometry work they need is exact and small enough to do on the STEP text itself.
* rotate the whole model by a multiple of 90 degrees about X, Y or Z (stand a Y-up model up, turn a
side face to the top, pose the part so its mark reads along +X): every 3-D CARTESIAN_POINT and
DIRECTION is permuted and negated, which is exact, and conjugates every assembly placement, so
colours, names and the assembly tree are untouched;
* move one product's geometry (the Adom.LaserEtch mark) by turn + uniform scale + translation about a
centre: its own points, directions and VECTOR magnitudes change, pcurve (2-D) points scale with it,
and entities shared with other products (the frame origin) are left alone;
* read a product's points per face (glyph centroids and extents for the legibility gate).
Python API: Step(path_or_text) .rotate90(axis, quarter_turns) .transform_product(name, ...) .text
CLI: step_text.py rotate in.step out.step <x|y|z> <quarter_turns>
step_text.py products in.step"""
import math, re, sys
_ENT = re.compile(r"#(\d+)\s*=\s*", re.S)
_REF = re.compile(r"#(\d+)")
def _split_statements(data):
"""Split the DATA section into statements at ';' outside quoted strings."""
out, buf, i, n, inq = [], [], 0, len(data), False
start = 0
while i < n:
c = data[i]
if inq:
if c == "'":
if i + 1 < n and data[i + 1] == "'":
i += 2; continue
inq = False
elif c == "'":
inq = True
elif c == ";":
out.append(data[start:i]); start = i + 1
i += 1
tail = data[start:]
return out, tail
def fmt(v):
"""A STEP REAL: always with a decimal point, exponent as E."""
if v == 0:
return "0."
s = repr(float(v))
if "e" in s or "E" in s:
m, e = s.lower().split("e")
if "." not in m:
m += "."
return f"{m}E{int(e):+03d}".replace("E+", "E+")
return s if "." in s else s + "."
def _nums(body):
"""The numbers of the coordinate list in CARTESIAN_POINT('n',(a,b,c)) / DIRECTION('n',(a,b,c))."""
m = re.search(r"\(\s*'(?:[^']|'')*'\s*,\s*\(([^()]*)\)\s*\)\s*$", body, re.S)
if not m:
return None, None
return [float(t) for t in m.group(1).replace("\n", " ").split(",")], m.span(1)
class Step:
def __init__(self, src):
text = src if "ISO-10303-21" in src[:200] else open(src, encoding="latin-1").read()
i = text.index("DATA;") + 5
j = text.index("ENDSEC;", i)
self.head, self.tail = text[:i], text[j:]
stmts, rest = _split_statements(text[i:j])
self.order, self.body, self.lead = [], {}, {}
for s in stmts:
m = _ENT.search(s)
if not m:
continue
k = int(m.group(1))
self.order.append(k); self.body[k] = s[m.end():].strip(); self.lead[k] = s[:m.start()]
self.rest = rest
self._rev = None
# ---- graph -------------------------------------------------------------------------------
def kind(self, k):
b = self.body[k]
return b[: b.index("(")].strip() if not b.startswith("(") else ""
def refs(self, k):
return [int(x) for x in _REF.findall(re.sub(r"'(?:[^']|'')*'", "", self.body[k]))]
def rev(self):
if self._rev is None:
r = {}
for k in self.order:
for x in self.refs(k):
r.setdefault(x, []).append(k)
self._rev = r
return self._rev
def reach(self, roots, stop=()):
seen, st = set(), list(roots)
while st:
k = st.pop()
if k in seen or k not in self.body or k in stop:
continue
seen.add(k); st.extend(self.refs(k))
return seen
def products(self):
out = []
for k in self.order:
if self.kind(k) == "PRODUCT":
m = re.match(r"PRODUCT\s*\(\s*'((?:[^']|'')*)'", self.body[k])
out.append((k, m.group(1).replace("''", "'") if m else ""))
return out
def _up(self, k, kind_prefix):
return [x for x in self.rev().get(k, []) if self.kind(x).startswith(kind_prefix) and self.kind(x) == kind_prefix]
def product_reps(self, pid):
"""Representations (and SRR-linked breps) that carry this product's own geometry."""
reps = []
for pdf in self._up(pid, "PRODUCT_DEFINITION_FORMATION") + self._up(pid, "PRODUCT_DEFINITION_FORMATION_WITH_SPECIFIED_SOURCE"):
for pd in self._up(pdf, "PRODUCT_DEFINITION"):
for pds in self._up(pd, "PRODUCT_DEFINITION_SHAPE"):
for sdr in self._up(pds, "SHAPE_DEFINITION_REPRESENTATION"):
reps.append(self.refs(sdr)[-1])
more = list(reps)
while more:
r = more.pop()
for x in self.rev().get(r, []):
if self.kind(x) == "SHAPE_REPRESENTATION_RELATIONSHIP":
a = self.refs(x)
for y in a:
if y != r and y not in reps:
reps.append(y); more.append(y)
return reps
def _rep_items(self, rep):
"""Items of a representation, without its context (last ref)."""
r = self.refs(rep)
return r[:-1]
def product_entities(self, name, pids=None):
"""Entities that belong only to the named product's geometry (shared frames and contexts excluded)."""
prods = self.products()
mine = list(pids) if pids else [p for p, nm in prods if nm == name]
if not mine:
return None
my_reps = set(r for p in mine for r in self.product_reps(p))
other_reps = set(r for p, nm in prods if p not in mine for r in self.product_reps(p)) - my_reps
roots_m = [i for r in my_reps for i in self._rep_items(r)]
roots_o = [i for r in other_reps for i in self._rep_items(r)]
# anything referenced from outside the product reps (assembly placements, styling) is shared too
a = self.reach(roots_m)
b = self.reach(roots_o)
shared = set()
for k in self.order:
if k in a or k in b:
continue
kd = self.kind(k)
if kd == "ITEM_DEFINED_TRANSFORMATION": # assembly placements: frames shared with the parent
shared |= self.reach(self.refs(k))
return a - b - shared
# ---- numeric access ----------------------------------------------------------------------
def coords(self, k):
return _nums(self.body[k])[0]
def set_coords(self, k, vals):
nums, span = _nums(self.body[k])
b = self.body[k]
self.body[k] = b[: span[0]] + ",".join(fmt(v) for v in vals) + b[span[1]:]
def faces_points(self, ents):
"""For each ADVANCED_FACE (or FACE_SURFACE) in `ents`, in file order: its 3-D boundary points."""
out = []
for k in self.order:
if k in ents and self.kind(k) in ("ADVANCED_FACE", "FACE_SURFACE"):
pts = []
fb = [x for x in self.refs(k) if self.kind(x) in ("FACE_BOUND", "FACE_OUTER_BOUND")]
for x in self.reach(fb):
if self.kind(x) == "CARTESIAN_POINT":
c = self.coords(x)
if c and len(c) == 3:
pts.append(c)
out.append(dict(id=k, points=pts))
return out
# ---- transforms --------------------------------------------------------------------------
# ---- frames and flattening ---------------------------------------------------------------
def _new_id(self):
self._next = max(getattr(self, "_next", 0), max(self.order)) + 1
return self._next
def _add(self, body, after=None):
k = self._new_id(); self.body[k] = body; self.lead[k] = ""
self.order.append(k); self._rev = None
return k
def rep_frames(self):
"""AXIS2_PLACEMENT_3D entities listed directly among a representation's items (its own frame and
the frames assembly transforms point at)."""
out = set()
for k in self.order:
if self.kind(k).endswith("REPRESENTATION") and "SHAPE" in self.kind(k) or self.kind(k) == "SHAPE_REPRESENTATION":
for x in self._rep_items(k):
if x in self.body and self.kind(x) == "AXIS2_PLACEMENT_3D":
out.add(x)
return out
def _frame(self, x):
o, z, xd = [self.coords(r) for r in self.refs(x)[:3]] if len(self.refs(x)) >= 3 else (self.coords(self.refs(x)[0]), [0, 0, 1], [1, 0, 0])
def nrm(v):
l = math.sqrt(sum(c * c for c in v)) or 1.0
return [c / l for c in v]
z = nrm(z); xd = [xd[i] - sum(xd[j] * z[j] for j in range(3)) * z[i] for i in range(3)]; xd = nrm(xd)
y = [z[1] * xd[2] - z[2] * xd[1], z[2] * xd[0] - z[0] * xd[2], z[0] * xd[1] - z[1] * xd[0]]
return o, [xd, y, z] # columns: local X, Y, Z in the parent
@staticmethod
def _mat(F):
o, (X, Y, Z) = F
return [[X[0], Y[0], Z[0], o[0]], [X[1], Y[1], Z[1], o[1]], [X[2], Y[2], Z[2], o[2]], [0, 0, 0, 1]]
@staticmethod
def _mul(A, B):
return [[sum(A[i][k] * B[k][j] for k in range(4)) for j in range(4)] for i in range(4)]
@staticmethod
def _inv(M):
R = [[M[j][i] for j in range(3)] for i in range(3)]
t = [-sum(R[i][k] * M[k][3] for k in range(3)) for i in range(3)]
return [R[0] + [t[0]], R[1] + [t[1]], R[2] + [t[2]], [0, 0, 0, 1]]
@staticmethod
def _is_identity(M, tol=1e-9):
return all(abs(M[i][j] - (1.0 if i == j else 0.0)) < tol for i in range(4) for j in range(4))
def _isolate_frames(self):
"""Give every representation frame its own point and directions, so geometry never shares them."""
for f in self.rep_frames():
refs = self.refs(f)
b = self.body[f]
for r in refs:
nk = self._add(self.body[r])
b = re.sub(rf"#{r}(?!\d)", f"#{nk}", b, count=1)
self.body[f] = b
self._rev = None
return self.rep_frames()
def _frame_ents(self, frames):
return set(x for f in frames for x in self.reach([f]))
def flatten(self):
"""Bake assembly transforms (and a root representation's own frame, which OpenCascade applies as
a placement) into the geometry and reset every frame to the identity. Afterwards local == world,
so turning the part and moving the mark are plain coordinate edits. Refuses instanced parts."""
frames = self._isolate_frames()
fents = self._frame_ents(frames)
rels = [] # (child rep, parent rep, child frame, parent frame)
for k in self.order:
b = self.body[k]
if self.kind(k) == "" and "REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION" in b:
m = re.search(r"REPRESENTATION_RELATIONSHIP\s*\(\s*'(?:[^']|'')*'\s*,\s*'(?:[^']|'')*'\s*,\s*#(\d+)\s*,\s*#(\d+)", b)
t = re.search(r"REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION\s*\(\s*#(\d+)", b)
if m and t and self.kind(int(t.group(1))) == "ITEM_DEFINED_TRANSFORMATION":
a1, a2 = self.refs(int(t.group(1)))[:2]
rels.append((int(m.group(1)), int(m.group(2)), a1, a2))
if any(self.kind(k) in ("MAPPED_ITEM", "REPRESENTATION_MAP") for k in self.order):
raise SystemExit("step_text: MAPPED_ITEM instancing is not supported; pass a flattened STEP")
parent = {}
for c, p, a1, a2 in rels:
if c in parent and parent[c][0] != p:
raise SystemExit("step_text: an instanced sub-shape (used twice) cannot be flattened as text")
T = self._mul(self._mat(self._frame(a2)), self._inv(self._mat(self._frame(a1))))
parent[c] = (p, T)
reps = [k for k in self.order if (self.kind(k).endswith("REPRESENTATION") and "SHAPE" in self.kind(k)) or self.kind(k) == "SHAPE_REPRESENTATION"]
reps = [r for r in reps if self.kind(r) not in ("SHAPE_DEFINITION_REPRESENTATION", "CONTEXT_DEPENDENT_SHAPE_REPRESENTATION")]
# SRR-linked reps (shape rep <-> its brep) share the frame of their partner
srr = {}
for k in self.order:
if self.kind(k) == "SHAPE_REPRESENTATION_RELATIONSHIP":
r = self.refs(k)
if len(r) >= 2:
srr.setdefault(r[1], r[0]); srr.setdefault(r[0], r[1])
def own_frame(r):
fs = [x for x in self._rep_items(r) if x in frames]
return self._mat(self._frame(fs[0])) if fs else None
def world(r, depth=0):
if depth > 32:
raise SystemExit("step_text: assembly loop")
if r in parent:
p, T = parent[r]
return self._mul(world(p, depth + 1), T)
if r in srr and srr[r] in parent:
return world(srr[r], depth + 1)
F = own_frame(r)
if r in srr and F is None:
F = own_frame(srr[r])
# a root frame that no transform uses is applied by OpenCascade as the root placement
used = set(x for _, _, a1, a2 in rels for x in (a1, a2))
fs = [x for x in self._rep_items(r) if x in frames]
if F is not None and not (fs and fs[0] in used):
return F
return [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]
owner = {}
for r in reps:
M = world(r)
geo = self.reach([x for x in self._rep_items(r) if x not in frames]) - fents
for k in geo:
if k in owner and owner[k][0] != r and not self._is_identity(owner[k][1]) or (k in owner and owner[k][0] != r and not self._is_identity(M)):
if self.kind(k) in ("CARTESIAN_POINT", "DIRECTION"):
raise SystemExit("step_text: geometry shared between placed parts; pass a flattened STEP")
owner.setdefault(k, (r, M))
for k, (r, M) in owner.items():
if self._is_identity(M):
continue
kd = self.kind(k)
if kd in ("CARTESIAN_POINT", "DIRECTION"):
c = self.coords(k)
if c and len(c) == 3:
if kd == "CARTESIAN_POINT":
v = [sum(M[i][j] * c[j] for j in range(3)) + M[i][3] for i in range(3)]
else:
v = [sum(M[i][j] * c[j] for j in range(3)) for i in range(3)]
self.set_coords(k, v)
ident = {0: [0.0, 0.0, 0.0], 1: [0.0, 0.0, 1.0], 2: [1.0, 0.0, 0.0]}
for f in frames:
for i, r in enumerate(self.refs(f)[:3]):
self.set_coords(r, ident[i])
self._flat = True
return self
def rotate90(self, axis, q):
"""Rotate the whole model by q quarter turns (counter-clockwise looking down +axis). The file is
flattened first (frames at identity), then only geometry turns, so every STEP reader agrees."""
q %= 4
if not getattr(self, "_flat", False):
self.flatten()
if q == 0:
return self
def rot(v):
x, y, z = v
for _ in range(q):
if axis == "z": x, y = -y, x
elif axis == "x": y, z = -z, y
elif axis == "y": z, x = -x, z
return [x, y, z]
fents = self._frame_ents(self.rep_frames())
for k in self.order:
kd = self.kind(k)
if kd in ("CARTESIAN_POINT", "DIRECTION") and k not in fents:
c = self.coords(k)
if c and len(c) == 3:
self.set_coords(k, rot(c))
return self
def transform_product(self, name, centre, scale=1.0, quarter_turns_z=0, translate=(0, 0, 0), set_z=None):
"""p' = centre + scale * Rz(q) (p - centre) + translate for the product's own points; optional
set_z replaces the z of every 3-D point (a flat mark seated on its face). 2-D (pcurve) points scale
about the origin, directions turn, VECTOR magnitudes scale."""
if not getattr(self, "_flat", False):
self.flatten()
ents = self.product_entities(name)
if ents is not None:
ents -= self._frame_ents(self.rep_frames())
if not ents:
raise SystemExit(f"step_text: no product named {name!r}")
q = quarter_turns_z % 4
def rz(x, y):
for _ in range(q):
x, y = -y, x
return x, y
cx, cy, cz = centre
for k in ents:
kd = self.kind(k)
if kd == "CARTESIAN_POINT":
c = self.coords(k)
if c is None:
continue
if len(c) == 3:
x, y = rz(c[0] - cx, c[1] - cy)
z = c[2] - cz
nz = (set_z if set_z is not None else cz + scale * z + translate[2])
self.set_coords(k, [cx + scale * x + translate[0], cy + scale * y + translate[1], nz])
elif len(c) == 2:
self.set_coords(k, [scale * c[0], scale * c[1]])
elif kd == "DIRECTION":
c = self.coords(k)
if c and len(c) == 3 and q:
x, y = rz(c[0], c[1]); self.set_coords(k, [x, y, c[2]])
elif kd == "VECTOR" and scale != 1.0:
m = re.match(r"(VECTOR\s*\(\s*'(?:[^']|'')*'\s*,\s*#\d+\s*,\s*)([^)]+)(\)\s*)$", self.body[k], re.S)
if m:
self.body[k] = m.group(1) + fmt(float(m.group(2)) * scale) + m.group(3)
elif kd in ("CIRCLE", "CYLINDRICAL_SURFACE", "CONICAL_SURFACE", "SPHERICAL_SURFACE", "ELLIPSE", "TOROIDAL_SURFACE") and scale != 1.0:
raise SystemExit(f"step_text: product {name!r} has a {kd}; scaling it is not supported (flat glyph marks are lines and B-splines)")
return self
def faces_subset_entities(self, name, face_ids):
"""Entities that belong to the given faces of a product and to no other face of it."""
ents = self.product_entities(name) - self._frame_ents(self.rep_frames())
allf = [k for k in ents if self.kind(k) in ("ADVANCED_FACE", "FACE_SURFACE")]
mine = self.reach(face_ids) & ents
others = self.reach([f for f in allf if f not in face_ids]) & ents
return mine - others
def transform_entities(self, ents, centre, scale=1.0, translate=(0, 0, 0), set_z=None):
"""Uniform scale about centre + translation for a set of entities (one line of a mark)."""
cx, cy, cz = centre
for k in ents:
kd = self.kind(k)
if kd == "CARTESIAN_POINT":
c = self.coords(k)
if c and len(c) == 3:
nz = set_z if set_z is not None else cz + scale * (c[2] - cz) + translate[2]
self.set_coords(k, [cx + scale * (c[0] - cx) + translate[0], cy + scale * (c[1] - cy) + translate[1], nz])
elif c and len(c) == 2:
self.set_coords(k, [scale * c[0], scale * c[1]])
elif kd == "VECTOR" and scale != 1.0:
m = re.match(r"(VECTOR\s*\(\s*'(?:[^']|'')*'\s*,\s*#\d+\s*,\s*)([^)]+)(\)\s*)$", self.body[k], re.S)
if m:
self.body[k] = m.group(1) + fmt(float(m.group(2)) * scale) + m.group(3)
return self
def remove_product(self, name, pids=None):
"""Delete a product (an old laser mark) and everything only it uses: its assembly occurrence,
placement, representation, geometry and styling. References to it inside lists are dropped."""
if not getattr(self, "_flat", False):
self.flatten()
pids = list(pids) if pids else [p for p, nm in self.products() if nm == name]
if not pids:
return self
self._orig_refd = set(x for k in self.order for x in self.refs(k))
own = set(self.product_entities(name, pids) or set())
dead = set(pids)
for p in pids:
for r in self.product_reps(p):
dead.add(r)
dead |= own
def top_args(body):
"""Top-level argument strings of SIMPLE(...)."""
i = body.index("(") + 1; depth = 0; inq = False; cur = ""; args = []
for c in body[i:]:
if inq:
cur += c
if c == "'": inq = False
continue
if c == "'": inq = True; cur += c; continue
if c == "(":
depth += 1
elif c == ")":
if depth == 0:
args.append(cur); break
depth -= 1
if c == "," and depth == 0:
args.append(cur); cur = ""; continue
cur += c
return args
changed = True
while changed:
changed = False
for k in list(self.order):
if k in dead:
continue
refs = set(self.refs(k))
if not refs & dead:
continue
b = self.body[k]
if self.kind(k) == "":
dead.add(k); changed = True; continue
args = top_args(b)
direct = [a.strip() for a in args if a.strip().startswith("#")]
if any(int(a[1:]) in dead for a in direct if a[1:].isdigit()):
dead.add(k); changed = True; continue
# drop dead refs from lists
nb = b
for d in refs & dead:
nb = re.sub(rf"\s*,\s*#{d}(?!\d)|#{d}(?!\d)\s*,\s*|#{d}(?!\d)", "", nb, count=0)
if re.search(r"\(\s*\)", nb) and self.kind(k) in ("PRODUCT_RELATED_PRODUCT_CATEGORY", "MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION", "PRESENTATION_LAYER_ASSIGNMENT", "DRAUGHTING_MODEL"):
if self.kind(k) != "MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION":
dead.add(k); changed = True; continue
self.body[k] = nb
# garbage collection: keep what the file's original roots (entities nobody referenced) still reach
referenced = set(x for k in self.order for x in self.refs(k)) if not hasattr(self, "_orig_refd") else self._orig_refd
roots = [k for k in self.order if k not in dead and k not in referenced]
self._orig_refd = referenced
live = self.reach(roots, stop=dead)
self.order = [k for k in self.order if k in live]
for k in list(self.body):
if k not in live:
self.body.pop(k, None)
self._rev = None
return self
# ---- proxy etch support ------------------------------------------------------------------
def scale_axes(self, x0, y0, z0, sx, sy, sz):
"""Map a unit-box template onto [x0, x0+sx] x [y0, y0+sy] x [z0, z0+sz] (template has no pcurves)."""
fents = self._frame_ents(self.rep_frames())
for k in self.order:
if self.kind(k) == "CARTESIAN_POINT" and k not in fents:
c = self.coords(k)
if c and len(c) == 3:
self.set_coords(k, [x0 + sx * c[0], y0 + sy * c[1], z0 + sz * c[2]])
return self
def _pd_of(self, pid):
for pdf in self._up(pid, "PRODUCT_DEFINITION_FORMATION") + self._up(pid, "PRODUCT_DEFINITION_FORMATION_WITH_SPECIFIED_SOURCE"):
for pd in self._up(pdf, "PRODUCT_DEFINITION"):
return pd
return None
def _root_products(self):
related = set()
for k in self.order:
if self.kind(k) == "NEXT_ASSEMBLY_USAGE_OCCURRENCE":
related.add(self.refs(k)[1])
return [(p, n) for p, n in self.products() if self._pd_of(p) not in related]
@staticmethod
def _remap(body, off):
out, i, n, inq = [], 0, len(body), False
while i < n:
c = body[i]
if inq:
out.append(c)
if c == "'":
if i + 1 < n and body[i + 1] == "'":
out.append("'"); i += 2; continue
inq = False
i += 1; continue
if c == "'":
inq = True; out.append(c); i += 1; continue
if c == "#":
j = i + 1
while j < n and body[j].isdigit():
j += 1
out.append("#" + str(int(body[i + 1:j]) + off)); i = j; continue
out.append(c); i += 1
return "".join(out)
def graft(self, donor, under="Adom.Chip"):
"""Replace the single child of product `under` with the donor file's whole model (its root
product and everything below it, colours included). Both files must be flattened."""
chip = [p for p, n in self.products() if n == under]
if not chip:
raise SystemExit(f"step_text: no product {under!r} to graft under")
chip_pd = self._pd_of(chip[0])
nauo = [k for k in self.order if self.kind(k) == "NEXT_ASSEMBLY_USAGE_OCCURRENCE" and self.refs(k)[0] == chip_pd]
if len(nauo) != 1:
raise SystemExit(f"step_text: {under} has {len(nauo)} children; expected the proxy only")
nauo = nauo[0]
old_pd = self.refs(nauo)[1]
pds = [k for k in self.rev().get(nauo, []) if self.kind(k) == "PRODUCT_DEFINITION_SHAPE"][0]
cdsr = [k for k in self.rev().get(pds, []) if self.kind(k) == "CONTEXT_DEPENDENT_SHAPE_REPRESENTATION"][0]
rr = self.refs(cdsr)[0]
m = re.search(r"REPRESENTATION_RELATIONSHIP\s*\(\s*'(?:[^']|'')*'\s*,\s*'(?:[^']|'')*'\s*,\s*#(\d+)", self.body[rr])
old_rep = int(m.group(1))
idt = int(re.search(r"REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION\s*\(\s*#(\d+)", self.body[rr]).group(1))
old_frame = self.refs(idt)[0]
old_pid = [p for p, n in self.products() if self._pd_of(p) == old_pd][0]
# donor root
roots = donor._root_products()
if len(roots) != 1:
raise SystemExit(f"step_text: the model has {len(roots)} root products; expected one")
r_pid, r_name = roots[0]
r_pd = donor._pd_of(r_pid)
r_pds = [k for k in donor.rev().get(r_pd, []) if donor.kind(k) == "PRODUCT_DEFINITION_SHAPE"][0]
r_rep = [donor.refs(k)[-1] for k in donor.rev().get(r_pds, []) if donor.kind(k) == "SHAPE_DEFINITION_REPRESENTATION"][0]
fr = [x for x in donor._rep_items(r_rep) if donor.kind(x) == "AXIS2_PLACEMENT_3D"]
if fr:
r_frame = fr[0]
else: # give the root representation a frame to be placed by
pk = donor._add("CARTESIAN_POINT('',(0.,0.,0.))"); dz = donor._add("DIRECTION('',(0.,0.,1.))"); dx = donor._add("DIRECTION('',(1.,0.,0.))")
r_frame = donor._add(f"AXIS2_PLACEMENT_3D('',#{pk},#{dz},#{dx})")
b = donor.body[r_rep]
i = b.index("(", b.index("(") + 1)
donor.body[r_rep] = b[: i + 1] + f"#{r_frame}," + b[i + 1:]
off = max(self.order) + 10
for k in donor.order:
nk = k + off
self.body[nk] = self._remap(donor.body[k], off); self.lead[nk] = ""; self.order.append(nk)
self._rev = None
rep = lambda b, a, z: re.sub(rf"#{a}(?!\d)", f"#{z}", b, count=1)
self.body[nauo] = rep(self.body[nauo], old_pd, r_pd + off)
self.body[rr] = rep(self.body[rr], old_rep, r_rep + off)
self.body[idt] = rep(self.body[idt], old_frame, r_frame + off)
self._rev = None
self.remove_product(None, [old_pid])
# the root keeps the model's name
for p, n in self._root_products():
if n == "adom_mark_proxy":
self.body[p] = self.body[p].replace("'adom_mark_proxy'", "'" + r_name.replace("'", "''") + "'")
self._rev = None
return self
def rename_product(self, old, new):
for k in self.order:
if self.kind(k) == "PRODUCT":
self.body[k] = self.body[k].replace(f"'{old}'", f"'{new}'")
return self
@property
def text(self):
parts = [self.head, "\n"]
for k in self.order:
parts.append(f"#{k} = {self.body[k]};\n")
parts.append(self.tail)
return "".join(parts)
def write(self, path):
open(path, "w", encoding="latin-1").write(self.text)
return path
def bbox(points):
xs, ys, zs = zip(*points)
return [min(xs), min(ys), min(zs), max(xs), max(ys), max(zs)]
if __name__ == "__main__":
a = sys.argv[1:]
if a and a[0] == "rotate":
Step(a[1]).rotate90(a[3], int(a[4])).write(a[2])
elif a and a[0] == "products":
s = Step(a[1])
for k, n in s.products():
print(k, n)
else:
print(__doc__); sys.exit(2)