mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-25 01:40:57 +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>
100 lines
4.2 KiB
Python
100 lines
4.2 KiB
Python
"""Flat glyph vs 3D relief, at the elevations that killed the disc — snaporca-wi3z.
|
|
|
|
The flat study collapsed at 16 deg because anything drawn IN the connector's plane foreshortens by
|
|
sin(elevation). This renders the SAME bear as its real relief (1508 facets off the supplied male)
|
|
with a simple lambert shade, so the silhouette does the work at a grazing angle. Two rows, same
|
|
sizes, same elevations, so the comparison is direct.
|
|
"""
|
|
import json, math, os
|
|
from PIL import Image, ImageDraw
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
M = json.load(open(os.path.join(HERE, "bear_mesh.json")))
|
|
V, F = M["v"], M["f"]
|
|
|
|
# Part frame: face carried by X (right) and Z (down-negative), relief along +Y.
|
|
P = [(v[0], -v[2], v[1]) for v in V] # -> (x right, y up, z out of the face)
|
|
xs=[p[0] for p in P]; ys=[p[1] for p in P]; zs=[p[2] for p in P]
|
|
CX,CY,CZ = (min(xs)+max(xs))/2, (min(ys)+max(ys))/2, (min(zs)+max(zs))/2
|
|
SPAN = max(max(xs)-min(xs), max(ys)-min(ys))
|
|
P = [((x-CX)/SPAN, (y-CY)/SPAN, (z-CZ)/SPAN) for x,y,z in P]
|
|
|
|
def shade(px, elev_deg, supersample=8):
|
|
"""Camera orbits down from straight-on (90) to grazing (small). Rotate about the screen x-axis."""
|
|
S = px*supersample
|
|
a = math.radians(elev_deg)
|
|
ca, sa = math.cos(a), math.sin(a)
|
|
# view: rotate the model so the face normal tips away from the camera
|
|
def xf(p):
|
|
x,y,z = p
|
|
return (x, y*sa + z*ca, -y*ca + z*sa) # third component = depth toward camera
|
|
Q = [xf(p) for p in P]
|
|
img = Image.new("L", (S,S), 0)
|
|
d = ImageDraw.Draw(img)
|
|
order = []
|
|
for tri in F:
|
|
a3 = [Q[i] for i in tri]
|
|
order.append((sum(v[2] for v in a3)/3.0, tri, a3))
|
|
order.sort(key=lambda t: t[0]) # painter: far first
|
|
light = (-0.35, 0.55, 0.76)
|
|
for _, tri, a3 in order:
|
|
(x0,y0,z0),(x1,y1,z1),(x2,y2,z2) = a3
|
|
ux,uy,uz = x1-x0, y1-y0, z1-z0
|
|
vx,vy,vz = x2-x0, y2-y0, z2-z0
|
|
nx,ny,nz = uy*vz-uz*vy, uz*vx-ux*vz, ux*vy-uy*vx
|
|
n = math.sqrt(nx*nx+ny*ny+nz*nz) or 1.0
|
|
nx,ny,nz = nx/n, ny/n, nz/n
|
|
if nz < 0: nx,ny,nz = -nx,-ny,-nz # face the camera
|
|
lam = max(0.0, nx*light[0] + ny*light[1] + nz*light[2])
|
|
val = int(70 + 185*lam)
|
|
pts = [(S/2 + x*S*0.92, S/2 - y*S*0.92) for x,y,_ in a3]
|
|
d.polygon(pts, fill=val)
|
|
return img.resize((px,px), Image.LANCZOS)
|
|
|
|
# flat outline, for the side-by-side
|
|
D = json.load(open(os.path.join(HERE, "bear_outline.json")))
|
|
def unit(pts):
|
|
p=[(x,-z) for x,z in pts]
|
|
return [((x-CX)/SPAN,(y-CY)/SPAN) for x,y in p]
|
|
OUT = unit(D["outer"])
|
|
HOLES = [unit(h["pts"]) for h in D["holes"]]
|
|
|
|
def flat(px, elev_deg, supersample=8):
|
|
S=px*supersample
|
|
img=Image.new("L",(S,S),0); d=ImageDraw.Draw(img)
|
|
k=math.sin(math.radians(elev_deg))
|
|
m=lambda p:(S/2+p[0]*S*0.92, S/2-p[1]*S*0.92*k)
|
|
d.polygon([m(p) for p in OUT], fill=255)
|
|
for h in HOLES: d.polygon([m(p) for p in h], fill=0)
|
|
return img.resize((px,px), Image.LANCZOS)
|
|
|
|
SIZES=[22,32,48]; ELEVS=[(90,"flat on"),(47,"47"),(16,"16"),(6,"6")]
|
|
pad,cell=8,58
|
|
W=pad+len(SIZES)*len(ELEVS)*cell+pad; H=pad+2*cell+pad
|
|
sheet=Image.new("RGB",(W,H),(24,27,32))
|
|
for r,fn in enumerate((flat, shade)):
|
|
for ci,(elev,_) in enumerate(ELEVS):
|
|
for si,px in enumerate(SIZES):
|
|
g=fn(px,elev)
|
|
tile=Image.new("RGB",(px,px),(24,27,32))
|
|
if fn is flat:
|
|
tile.paste(Image.new("RGB",(px,px),(237,168,23)),(0,0),g)
|
|
else:
|
|
gg=g.convert("L")
|
|
tile=Image.merge("RGB",(gg.point(lambda v:min(255,int(v*1.00))),
|
|
gg.point(lambda v:int(v*0.71)),
|
|
gg.point(lambda v:int(v*0.16))))
|
|
x=pad+(ci*len(SIZES)+si)*cell+(cell-px)//2
|
|
y=pad+r*cell+(cell-px)//2
|
|
sheet.paste(tile,(x,y))
|
|
sheet.resize((W*2,H*2), Image.NEAREST).save(os.path.join(HERE,"relief-sheet.png"))
|
|
|
|
# how much ink survives — the same measure used on the disc glyph
|
|
print(f"{'elev':>6} {'flat px@32':>11} {'relief px@32':>13}")
|
|
for elev,_ in ELEVS:
|
|
f32=flat(32,elev); s32=shade(32,elev)
|
|
fi=sum(1 for v in f32.getdata() if v>40)
|
|
si=sum(1 for v in s32.getdata() if v>40)
|
|
print(f"{elev:>6} {fi:>11} {si:>13}")
|
|
print("WROTE relief-sheet.png")
|