mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-20 23:42:54 +00:00
The connector work has lived outside the code since 2026-08-05, in a workspace repo with no
remote. It is the basis of a decision that now shapes the Design tab, so it belongs here.
docs/design/mate-connectors/
DESIGN_MATE_CONNECTORS.md seven CAD systems surveyed; the frame-pair model this kernel
already matches; sections 8b/8c on the glyph, and section 9's
four open decisions (D1-D4) still awaiting Tommaso.
bear.step the male, Onshape 2026-08-05T08:27Z, md5 faf228326ee3f971
BearConnector_Female*.step/.stl, BearConnector_Cutter.step
built by make_female.py FROM the real male B-rep rather than
re-modelled, so the pocket is complementary by construction
including every deliberate asymmetry. Fit measured at exactly
0.2000 mm, zero interference, mated hosts proven coplanar.
BEAR_CONNECTOR_REVIEW.md the symmetry-group result: identity 81/81 edges, mirror-x 0/81,
mirror-y 0/81, rot180Z 0/81, rot90Z 0/81, diagonal 0/81 at
0.1 mm. Trivial group, so every PARTIAL view fixes orientation.
extract_outline.py, simplify_study.py, relief_sheet.py, handedness.py, make_female.py,
trim_female.py, fit_check.py, verify_trimmed.py, coplanar_test.py + their sheets
THE DECISION THIS SUPPORTS (snaporca-x0kd): the mate connector is drawn as a simplified BEAR
FACE by default, with the standard disc + roll quadrant + Z arrow kept behind a preference.
Face orientation is hardwired perception -- a toddler reads a face's roll and verse with no
instruction -- and no abstract glyph earns that. Measured against the alternative: the disc's
gold quadrant+tick falls 89 -> 66 -> 37 -> 20 -> 3 -> 0 lit pixels as the camera drops from
47 deg to edge-on, and is a shapeless blob by 16 deg.
WHAT THE SIMPLIFICATION STUDY SETTLED (snaporca-wi3z), all measured off the real B-rep:
The eyes are load-bearing. Same outline and muzzle with the eyes removed stops reading as
a face at every size. Whatever else goes, they stay.
45 -> 22 outline vertices with no loss of read at 22 / 32 / 48 px; the muzzle reduces to
one filled triangle. Three marks plus a cheek dot.
Drawn FLAT the face fails exactly where the disc fails: in the connector's plane everything
foreshortens by sin(elevation). Rendered as its real relief instead, lit pixels at 32 px go
164 -> 210 at 16 deg and 69 -> 120 at 6 deg, and the snout ridge stands proud as a profile
rather than smearing. The glyph must be a shaded relief, not an outline.
Handedness already reads without any added mark -- 32 to 35 % of lit pixels differ from the
mirror, and re-registering by best whole-pixel translation returns offset (0,0), so it is
real shape asymmetry. But it reads only BY COMPARISON. A dot on one cheek makes it local:
34.5 / 37.0 / 36.4 %, and unlike uneven eyes (42 %) it does not read as a defect.
Tommaso's calls: it stays a bear, and handedness must read.
The scripts were repointed at the co-located male and extract_outline.py re-run from here to
prove it -- same 45 outline points, same three inner wires, same 3829.5 mm2 back plate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
77 lines
2.8 KiB
Python
77 lines
2.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Flat-shade an ASCII/binary STL from several directions.
|
|
|
|
Used to answer one question with a picture instead of an argument: does a RECESSED faceted
|
|
pocket read as an oriented feature, or does a concave feature collapse into a dark hole?
|
|
"""
|
|
import struct
|
|
import sys
|
|
import numpy as np
|
|
from PIL import Image, ImageDraw
|
|
|
|
LIGHT = np.array([0.35, -0.5, 0.78]); LIGHT /= np.linalg.norm(LIGHT)
|
|
|
|
|
|
def load_stl(path):
|
|
data = open(path, "rb").read()
|
|
if data[:5] == b"solid" and b"facet" in data[:2000]:
|
|
tris, cur = [], []
|
|
for line in data.decode("ascii", "ignore").splitlines():
|
|
s = line.split()
|
|
if s and s[0] == "vertex":
|
|
cur.append([float(x) for x in s[1:4]])
|
|
if len(cur) == 3:
|
|
tris.append(cur); cur = []
|
|
return np.array(tris, dtype=float)
|
|
n = struct.unpack("<I", data[80:84])[0]
|
|
tris = np.empty((n, 3, 3), dtype=float)
|
|
off = 84
|
|
for i in range(n):
|
|
v = struct.unpack("<12f", data[off:off + 48])
|
|
tris[i] = np.array(v[3:12]).reshape(3, 3)
|
|
off += 50
|
|
return tris
|
|
|
|
|
|
def render(tris, eye, target, path, size=(620, 460), scale=14.0, label=""):
|
|
eye = np.array(eye, float); target = np.array(target, float)
|
|
f = target - eye; f /= np.linalg.norm(f)
|
|
up = np.array([0, 0, 1.0])
|
|
if abs(np.dot(f, up)) > 0.999: up = np.array([0, 1.0, 0])
|
|
r = np.cross(f, up); r /= np.linalg.norm(r)
|
|
u = np.cross(r, f)
|
|
cam = np.stack([r, u, f])
|
|
|
|
w, h = size
|
|
img = Image.new("RGB", size, (238, 240, 243)); d = ImageDraw.Draw(img)
|
|
faces = []
|
|
for t in tris:
|
|
n = np.cross(t[1] - t[0], t[2] - t[0])
|
|
ln = np.linalg.norm(n)
|
|
if ln < 1e-12: continue
|
|
n /= ln
|
|
c = t.mean(axis=0)
|
|
if np.dot(n, c - eye) > 0: continue # cull back faces
|
|
P = (t - eye) @ cam.T
|
|
lam = max(0.0, float(np.dot(n, LIGHT)))
|
|
shade = 0.20 + 0.80 * lam
|
|
col = tuple(int(255 * shade * ch) for ch in (0.86, 0.72, 0.35))
|
|
poly = [(w / 2 + p[0] * scale, h / 2 - p[1] * scale) for p in P]
|
|
faces.append((P[:, 2].mean(), poly, col))
|
|
for _, poly, col in sorted(faces, key=lambda x: -x[0]):
|
|
d.polygon(poly, fill=col)
|
|
if label:
|
|
d.rectangle([8, 8, 8 + 9 * len(label), 30], fill=(255, 255, 255))
|
|
d.text((14, 14), label, fill=(20, 20, 20))
|
|
img.save(path)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
tris = load_stl(sys.argv[1])
|
|
print("triangles:", len(tris))
|
|
views = [((26, -22, 20), "iso"), ((0, 0, 34), "straight down +Z"),
|
|
((4, -30, 9), "grazing"), ((-28, -10, 12), "from the tall end")]
|
|
for i, (eye, lab) in enumerate(views):
|
|
render(tris, eye, (0, 0, 0), f"fem-{i}.png", label=f"FEMALE POCKET — {lab}")
|
|
print(f"fem-{i}.png")
|