mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-19 06:53:02 +00:00
Tommaso's decision (snaporca-x0kd): face orientation is hardwired perception -- a toddler reads
a face's roll and verse with no instruction -- so the connector is a face by default and the
conventional disc + roll quadrant stays, selectable, for users who expect it.
Preferences > Control > Camera > "Draw mate connectors as a face", default ON, key
design_connector_face_glyph. Read every frame rather than latched, so toggling takes effect on
the next repaint -- a look you cannot A/B without restarting will not get compared. Verified on
the rig: unchecking it switches the viewport to the disc live, no restart.
WHY A RELIEF AND NOT A DRAWING. A flat face in the connector's plane foreshortens by
sin(elevation) and collapses at a grazing view exactly like the quadrant it replaces -- measured,
the quadrant falls 89 -> 20 -> 3 -> 0 lit pixels from 47 degrees to edge-on. The relief does not:
its silhouette carries the information. So the glyph is a small shaded solid, painter-sorted,
lambert-shaded against a light fixed in CAMERA space so orbiting does not swing the shading.
THE MUZZLE, AND THE MISTAKE THAT NEARLY LOST IT. It is the only feature standing along +Z, so it
says which way the connector points and it is all that survives edge-on. Two errors on the way:
1. I built its footprint from height*tan(draft) and got a needle. The real base OVERHANGS the
crest at both ends (0.062 nose, 0.034 tail) and that overhang is what makes it a wedge. Base
now lifted straight off the mesh.
2. Worse, I chased fidelity. Scaled honestly the ridge is 11.3 mm on an 83.3 mm face -- 13.6 %
of the width -- and at 22-48 px that is a scratch. Tommaso looked at it and could not find
the muzzle at all, which is the only test that counts. A glyph is a symbol, not a scale
model, so it now gets two deliberate exaggerations, and COLOUR does most of the work:
muzzle share of lit pixels at 90/16/6 deg -- body tone 14.8/11.3/17.5 %, accent gold
18.3/19.2/23.9 %, accent gold at 1.8x width 23.5/25.2/31.2 %.
The accent is the same gold the disc spends on its roll quadrant, so it stays this tab's "here
is the direction that matters" colour. Polarity is still on the Z arrow's head; nothing collides.
A connector whose ROLL COULD NOT BE DERIVED keeps the disc treatment whatever the preference says.
A face asserts a definite orientation, and asserting one for a roll that was never derived is the
same confident lie that got billboarding rejected.
Geometry is emitted from the part by docs/design/mate-connectors/emit_glyph_table.py, not
hand-drawn, so glyph and printed connector cannot drift: 12-vertex outline, two eyes, chin bar,
cheek dot, and the snout wedge. Crest 29.0 mm / 6.58 mm drop / 13.1 deg against the review's
28.3 / 6.61 / 13.1 on the B-rep.
Also fixes extract_outline.py, which walked w.Edges: OCC returns them in storage order, not ring
order, ignoring per-edge orientation, so the outline was scrambled -- 45 points and perimeter
6.380 where a clean ring gives 31 and 3.335. Every measurement in the design notes was re-run.
The correction reversed one earlier finding: handedness does NOT read on its own (5.4/8.0/9.1 %
different from its mirror, not the 32-35 % the scrambled ring produced), so the cheek dot is
required rather than merely nice.
RIG-VERIFIED on Xvfb :12 against a 60x40x10 box with a face+edge connector: the face renders with
both eyes, ears, chin bar, cheek dot and a gold muzzle standing proud; the Z arrow degenerates to
its ring when viewed down the axis; and the preference switches to the disc live.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
72 lines
3.3 KiB
Python
72 lines
3.3 KiB
Python
"""The muzzle has to READ, not just be present — snaporca-wi3z.
|
|
|
|
Faithfully scaled, the part's ridge is 11.3 mm on an 83 mm face: 13.6 % of the width. At glyph
|
|
size that is a scratch. A glyph is a symbol, not a scale model, so the question is how much
|
|
emphasis it takes before the only +Z feature actually reads. Variants, all with the same crest
|
|
geometry, differing only in width and colour.
|
|
"""
|
|
import math, os, importlib.util
|
|
from PIL import Image, ImageDraw
|
|
spec=importlib.util.spec_from_file_location("gp","glyph_preview.py")
|
|
gp=importlib.util.module_from_spec(spec); spec.loader.exec_module(gp)
|
|
|
|
OUT, CHIN, MARKS, CREST, SBASE, PLATE = gp.OUT, gp.CHIN, gp.MARKS, gp.CREST, gp.SBASE, gp.PLATE
|
|
BODY=(0.42,0.46,0.52); MARK=(0.126,0.138,0.156); GOLD=(0.93,0.66,0.09)
|
|
|
|
def facets(widen=1.0, muzzle_gold=False):
|
|
F=[]; n=len(OUT)
|
|
for i in range(n):
|
|
a,b=OUT[i],OUT[(i+1)%n]
|
|
F.append(([(a[0],a[1],0.0),(b[0],b[1],0.0),(b[0],b[1],PLATE),(a[0],a[1],PLATE)],BODY,True))
|
|
F.append(([(x,y,PLATE) for x,y in OUT],BODY,True))
|
|
zm=PLATE+0.004
|
|
for cx,cy,r in MARKS:
|
|
F.append(([(cx+r*math.cos(2*math.pi*i/12),cy+r*math.sin(2*math.pi*i/12),zm) for i in range(12)],MARK,False))
|
|
F.append(([(x,y,zm) for x,y in CHIN],MARK,False))
|
|
A,B=CREST
|
|
w=lambda p:(p[0]*widen,p[1],PLATE)
|
|
nl,nr,tr,tl=(w(SBASE[0]),w(SBASE[1]),w(SBASE[2]),w(SBASE[3]))
|
|
col = GOLD if muzzle_gold else BODY
|
|
F+=[([nl,tl,B,A],col,True),([nr,A,B,tr],col,True),
|
|
([nl,A,nr],col,True), ([tr,B,tl],col,True)]
|
|
return F
|
|
|
|
def render(F, px, elev, ss=8):
|
|
S=px*ss; a=math.radians(elev); ca,sa=math.cos(a),math.sin(a)
|
|
xf=lambda p:(p[0],p[1]*sa+p[2]*ca,-p[1]*ca+p[2]*sa)
|
|
light=(-0.70,0.30,0.45)
|
|
img=Image.new("RGB",(S,S),(24,27,32)); d=ImageDraw.Draw(img)
|
|
tris=sorted(((sum(v[2] for v in [xf(q) for q in pts])/len(pts),[xf(q) for q in pts],c,sh)
|
|
for pts,c,sh in F), key=lambda t:t[0])
|
|
for _,q,base,shade in tris:
|
|
(x0,y0,z0),(x1,y1,z1),(x2,y2,z2)=q[0],q[1],q[2]
|
|
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
|
|
L=math.sqrt(nx*nx+ny*ny+nz*nz) or 1.0; nx,ny,nz=nx/L,ny/L,nz/L
|
|
if nz<0: nx,ny,nz=-nx,-ny,-nz
|
|
k=(0.42+0.58*max(0.0,nx*light[0]+ny*light[1]+nz*light[2])) if shade else 1.0
|
|
d.polygon([(S/2+p[0]*S*0.92,S/2-p[1]*S*0.92) for p in q],
|
|
fill=tuple(min(255,int(255*c*k)) for c in base))
|
|
return img.resize((px,px),Image.LANCZOS)
|
|
|
|
VAR=[("V1 faithful", 1.0, False),
|
|
("V2 gold muzzle", 1.0, True),
|
|
("V3 gold + 1.8x wide",1.8, True),
|
|
("V4 body + 1.8x wide",1.8, False)]
|
|
big=Image.new("RGB",(4*250+30,4*140+30),(24,27,32))
|
|
for r,(name,wd,gold) in enumerate(VAR):
|
|
F=facets(wd,gold)
|
|
for c,e in enumerate((90,47,16,6)):
|
|
big.paste(render(F,120,e),(15+c*250+60,15+r*140+10))
|
|
big.save("/tmp/muzzle-variants.png")
|
|
for name,wd,gold in VAR:
|
|
F=facets(wd,gold); F0=[f for f in F][:-4]
|
|
row=[]
|
|
for e in (90,16,6):
|
|
a=render(F,32,e); b=render(F0,32,e)
|
|
la=sum(1 for p in a.get_flattened_data() if p!=(24,27,32))
|
|
df=sum(1 for p,q in zip(a.get_flattened_data(),b.get_flattened_data()) if p!=q)
|
|
row.append(f"{100.0*df/max(1,la):5.1f}%")
|
|
print(f"{name:22} muzzle share at 90/16/6 deg: " + " ".join(row))
|
|
print("WROTE /tmp/muzzle-variants.png")
|