Design: draw the mate connector as a bear face, with the disc kept behind a preference
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>
@@ -0,0 +1,30 @@
|
||||
// Emitted by doc/design/mate-connectors/emit_glyph_table.py from bear.step — do not hand-edit.
|
||||
// Normalised to the part's bounding span and centred: the renderer scales by one radius.
|
||||
static const Vec2d kBearOutline[] = { // 12 verts, RDP eps 0.030, CCW
|
||||
{+0.3842, +0.3294}, {+0.3156, +0.4002}, {+0.2424, +0.3294},
|
||||
{-0.2524, +0.3294}, {-0.3377, +0.3877}, {-0.3693, +0.3298},
|
||||
{-0.3256, +0.2631}, {-0.4893, -0.3337}, {-0.3960, -0.4002},
|
||||
{+0.4151, -0.4002}, {+0.5000, -0.3154}, {+0.3156, +0.2631},
|
||||
};
|
||||
static const Vec2d kBearChin[] = { // the CHIN BAR, flat. The muzzle is relief — see kBearCrest.
|
||||
{-0.2682, -0.3578}, {+0.2628, -0.3578}, {+0.2237, -0.1786},
|
||||
};
|
||||
// {cx, cy, r}: two eyes, then the cheek dot that carries handedness (snaporca-wi3z).
|
||||
static const Vec3d kBearMarks[] = {
|
||||
{-0.1997, +0.1760, +0.0590},
|
||||
{+0.1947, +0.1760, +0.0590},
|
||||
{+0.2797, +0.0760, +0.0380},
|
||||
};
|
||||
// THE MUZZLE, lifted off the mesh: a tapered wedge, base quad + crest edge, 6 facets.
|
||||
// This is the only feature standing along +Z and the only one still legible edge-on.
|
||||
static const double kBearPlateZ = +0.0360;
|
||||
static const Vec2d kBearSnoutBase[] = { // CCW from the nose end
|
||||
{-0.0727, -0.2417},
|
||||
{+0.0630, -0.2417},
|
||||
{+0.0259, +0.1939},
|
||||
{-0.0356, +0.1939},
|
||||
};
|
||||
static const Vec3d kBearCrest[] = { // nose (tall) -> tail (short)
|
||||
{-0.0048, -0.1793, +0.2073},
|
||||
{-0.0048, +0.1605, +0.1279},
|
||||
};
|
||||
@@ -1 +1 @@
|
||||
{"outer": [[42.071, -7.071], [26.711, -55.263], [35.0, -0.0], [42.071, -7.071], [35.0, 0.0], [-32.575, 0.0], [-36.715, -1.715], [-35.828, -0.987], [-34.815, -0.446], [-33.717, -0.112], [-32.575, -0.0], [-36.715, -1.715], [-39.55, -4.55], [-40.988, -10.468], [-41.241, -9.215], [-41.216, -7.937], [-40.914, -6.694], [-40.35, -5.547], [-39.55, -4.55], [-40.988, -10.468], [-26.711, -55.263], [-28.828, -57.312], [-28.944, -64.382], [-29.721, -63.334], [-30.201, -62.12], [-30.35, -60.823], [-30.159, -59.532], [-29.64, -58.335], [-28.828, -57.312], [-28.944, -64.382], [-27.718, -65.649], [-25.67, -65.683], [-26.325, -66.047], [-27.075, -66.035], [-27.718, -65.649], [-25.67, -65.683], [-20.613, -60.789], [20.613, -60.789], [-20.613, -60.789], [26.711, -66.69], [20.613, -60.789], [32.421, -60.789], [26.711, -66.69], [26.711, -55.263], [32.421, -60.789]], "holes": [{"pts": [[19.052, -18.464], [16.474, -9.14], [-0.0, -9.104], [-21.926, -9.104], [-21.926, -3.535], [22.308, -3.535], [19.052, -18.464]], "cx": 4.719, "cz": -10.192, "d": 44.234}, {"pts": [[-11.493, -48.01], [-11.676, -49.341], [-12.211, -50.574], [-13.06, -51.617], [-14.158, -52.392], [-15.424, -52.842], [-16.765, -52.934], [-18.081, -52.66], [-19.274, -52.042], [-20.257, -51.124], [-20.955, -49.976], [-21.318, -48.682], [-21.318, -47.337], [-20.955, -46.043], [-20.257, -44.895], [-19.274, -43.977], [-18.081, -43.359], [-16.765, -43.085], [-15.424, -43.177], [-14.158, -43.627], [-13.06, -44.402], [-12.211, -45.445], [-11.676, -46.678], [-11.493, -48.01]], "cx": -16.223, "cz": -48.01, "d": 9.825}, {"pts": [[21.364, -48.01], [21.181, -49.341], [20.645, -50.574], [19.797, -51.617], [18.699, -52.392], [17.432, -52.842], [16.091, -52.934], [14.775, -52.66], [13.582, -52.042], [12.6, -51.124], [11.901, -49.976], [11.539, -48.682], [11.539, -47.337], [11.901, -46.043], [12.6, -44.895], [13.582, -43.977], [14.775, -43.359], [16.091, -43.085], [17.432, -43.177], [18.699, -43.627], [19.797, -44.402], [20.645, -45.445], [21.181, -46.678], [21.364, -48.01]], "cx": 16.634, "cz": -48.01, "d": 9.825}]}
|
||||
{"outer": [[26.711, -55.263], [42.071, -7.071], [35.0, -0.0], [-32.575, 0.0], [-33.717, -0.112], [-34.815, -0.446], [-35.828, -0.987], [-36.715, -1.715], [-39.55, -4.55], [-40.35, -5.547], [-40.914, -6.694], [-41.216, -7.937], [-41.241, -9.215], [-40.988, -10.468], [-26.711, -55.263], [-28.828, -57.312], [-29.64, -58.335], [-30.159, -59.532], [-30.35, -60.823], [-30.201, -62.12], [-29.721, -63.334], [-28.944, -64.382], [-27.718, -65.649], [-27.075, -66.035], [-26.325, -66.047], [-25.67, -65.683], [-20.613, -60.789], [20.613, -60.789], [26.711, -66.69], [32.421, -60.789], [26.711, -55.263]], "holes": [{"pts": [[19.052, -18.464], [16.474, -9.14], [-0.0, -9.104], [-21.926, -9.104], [-21.926, -3.535], [22.308, -3.535], [19.052, -18.464]], "cx": 4.719, "cz": -10.192, "d": 44.234}, {"pts": [[-11.493, -48.01], [-11.676, -49.341], [-12.211, -50.574], [-13.06, -51.617], [-14.158, -52.392], [-15.424, -52.842], [-16.765, -52.934], [-18.081, -52.66], [-19.274, -52.042], [-20.257, -51.124], [-20.955, -49.976], [-21.318, -48.682], [-21.318, -47.337], [-20.955, -46.043], [-20.257, -44.895], [-19.274, -43.977], [-18.081, -43.359], [-16.765, -43.085], [-15.424, -43.177], [-14.158, -43.627], [-13.06, -44.402], [-12.211, -45.445], [-11.676, -46.678], [-11.493, -48.01]], "cx": -16.223, "cz": -48.01, "d": 9.825}, {"pts": [[21.364, -48.01], [21.181, -49.341], [20.645, -50.574], [19.797, -51.617], [18.699, -52.392], [17.432, -52.842], [16.091, -52.934], [14.775, -52.66], [13.582, -52.042], [12.6, -51.124], [11.901, -49.976], [11.539, -48.682], [11.539, -47.337], [11.901, -46.043], [12.6, -44.895], [13.582, -43.977], [14.775, -43.359], [16.091, -43.085], [17.432, -43.177], [18.699, -43.627], [19.797, -44.402], [20.645, -45.445], [21.181, -46.678], [21.364, -48.01]], "cx": 16.634, "cz": -48.01, "d": 9.825}]}
|
||||
@@ -0,0 +1,97 @@
|
||||
"""Emit the simplified bear as a C++ table for the viewport glyph — snaporca-wi3z.
|
||||
|
||||
Everything is normalised to the part's own bounding span and centred, so the renderer scales by
|
||||
one radius R in screen pixels and nothing here carries millimetres. Emitting rather than
|
||||
hand-authoring keeps the glyph and the printed part from drifting apart: rerun this and the table
|
||||
follows the STEP.
|
||||
"""
|
||||
import json, math, os
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
D = json.load(open(os.path.join(HERE, "bear_outline.json")))
|
||||
|
||||
def unit_frame(pts_sets):
|
||||
allp=[p for s in pts_sets for p in s]
|
||||
xs=[p[0] for p in allp]; ys=[p[1] for p in allp]
|
||||
cx,cy=(min(xs)+max(xs))/2,(min(ys)+max(ys))/2
|
||||
span=max(max(xs)-min(xs), max(ys)-min(ys))
|
||||
return cx,cy,span
|
||||
|
||||
outer=[(x,-z) for x,z in D["outer"]]
|
||||
holes=[[(x,-z) for x,z in h["pts"]] for h in D["holes"]]
|
||||
CX,CY,SPAN = unit_frame([outer]+holes)
|
||||
U=lambda pts:[((x-CX)/SPAN,(y-CY)/SPAN) for x,y in pts]
|
||||
OUT=U(outer)
|
||||
EYES=[U(h) for h,m in zip(holes,D["holes"]) if m["d"]<20]
|
||||
MUZ =U([h for h,m in zip(holes,D["holes"]) if m["d"]>=20][0])
|
||||
|
||||
def rdp(p,eps):
|
||||
if len(p)<3: return p
|
||||
ax,ay=p[0]; bx,by=p[-1]; dx,dy=bx-ax,by-ay; n=math.hypot(dx,dy)
|
||||
best,bi=-1.0,0
|
||||
for i in range(1,len(p)-1):
|
||||
px,py=p[i]
|
||||
d=abs(dx*(ay-py)-(ax-px)*dy)/n if n>1e-12 else math.hypot(px-ax,py-ay)
|
||||
if d>best: best,bi=d,i
|
||||
if best<=eps: return [p[0],p[-1]]
|
||||
return rdp(p[:bi+1],eps)[:-1]+rdp(p[bi:],eps)
|
||||
def simp(p,eps):
|
||||
r=rdp(p+[p[0]],eps); return r[:-1]
|
||||
|
||||
OUT_S = simp(OUT,.030) # 22 verts, the size the study settled on
|
||||
# wind counter-clockwise so the renderer's normals come out facing +Z
|
||||
def area2(p): return sum(p[i][0]*p[(i+1)%len(p)][1]-p[(i+1)%len(p)][0]*p[i][1] for i in range(len(p)))
|
||||
if area2(OUT_S) < 0: OUT_S = OUT_S[::-1]
|
||||
|
||||
def centroid(p): return (sum(q[0] for q in p)/len(p), sum(q[1] for q in p)/len(p))
|
||||
E=[]
|
||||
for e in EYES:
|
||||
c=centroid(e); r=(max(p[0] for p in e)-min(p[0] for p in e))/2
|
||||
E.append((c[0],c[1],r))
|
||||
E.sort()
|
||||
|
||||
lo=min(p[1] for p in MUZ); hi=max(p[1] for p in MUZ)
|
||||
bottom=[p for p in MUZ if p[1] < lo+0.06*(hi-lo)]
|
||||
apex=max(MUZ,key=lambda p:p[1])
|
||||
TRI=[min(bottom),max(bottom),apex]
|
||||
if area2(TRI)<0: TRI=TRI[::-1]
|
||||
|
||||
# the cheek dot: the handedness mark adopted after the mirror-difference study
|
||||
DOT=(E[1][0]+0.085, E[1][1]-0.10, 0.038)
|
||||
|
||||
# THE MUZZLE. Six facets lifted straight off the mesh -- every facet touching anything above the
|
||||
# 3 mm plate. Do NOT recompute the base from height*tan(draft): the first version did and produced
|
||||
# a needle, because the real base OVERHANGS the crest at both ends (0.062 at the nose, 0.034 at the
|
||||
# tail) and it is that overhang that makes it a tapered wedge instead of a blade.
|
||||
PLATE = 0.036 # 3.00 / 83.34
|
||||
SNOUT_BASE = ((-0.0727, -0.2417), (+0.0630, -0.2417), # nose end, 0.136 wide
|
||||
(+0.0259, +0.1939), (-0.0356, +0.1939)) # tail end, 0.062 wide
|
||||
CREST = ((-0.0048, -0.1793, 0.2073), (-0.0048, +0.1605, 0.1279))
|
||||
|
||||
def fmt(v): return f"{v:+.4f}"
|
||||
L=[]
|
||||
L.append(f"// Emitted by doc/design/mate-connectors/emit_glyph_table.py from bear.step — do not hand-edit.")
|
||||
L.append(f"// Normalised to the part's bounding span and centred: the renderer scales by one radius.")
|
||||
L.append(f"static const Vec2d kBearOutline[] = {{ // {len(OUT_S)} verts, RDP eps 0.030, CCW")
|
||||
for i in range(0,len(OUT_S),3):
|
||||
row=", ".join(f"{{{fmt(x)}, {fmt(y)}}}" for x,y in OUT_S[i:i+3])
|
||||
L.append(" "+row+",")
|
||||
L.append("};")
|
||||
L.append(f"static const Vec2d kBearChin[] = {{ // the CHIN BAR, flat. The muzzle is relief — see kBearCrest.")
|
||||
L.append(" "+", ".join(f"{{{fmt(x)}, {fmt(y)}}}" for x,y in TRI)+",")
|
||||
L.append("};")
|
||||
L.append("// {cx, cy, r}: two eyes, then the cheek dot that carries handedness (snaporca-wi3z).")
|
||||
L.append("static const Vec3d kBearMarks[] = {")
|
||||
for cx,cy,r in E: L.append(f" {{{fmt(cx)}, {fmt(cy)}, {fmt(r)}}},")
|
||||
L.append(f" {{{fmt(DOT[0])}, {fmt(DOT[1])}, {fmt(DOT[2])}}},")
|
||||
L.append("};")
|
||||
L.append("// THE MUZZLE, lifted off the mesh: a tapered wedge, base quad + crest edge, 6 facets.")
|
||||
L.append("// This is the only feature standing along +Z and the only one still legible edge-on.")
|
||||
L.append(f"static const double kBearPlateZ = {PLATE:+.4f};")
|
||||
L.append("static const Vec2d kBearSnoutBase[] = { // CCW from the nose end")
|
||||
for x,y in SNOUT_BASE: L.append(f" {{{fmt(x)}, {fmt(y)}}},")
|
||||
L.append("};")
|
||||
L.append("static const Vec3d kBearCrest[] = { // nose (tall) -> tail (short)")
|
||||
for x,y,z in CREST: L.append(f" {{{fmt(x)}, {fmt(y)}, {fmt(z)}}},")
|
||||
L.append("};")
|
||||
open(os.path.join(HERE,"bear_glyph_table.h"),"w").write("\n".join(L)+"\n")
|
||||
print("\n".join(L))
|
||||
@@ -28,9 +28,17 @@ y, face = best
|
||||
print(f"back plate at Y={y:.3f} wires={len(face.Wires)} area={face.Area:.1f} mm2")
|
||||
|
||||
def wire_pts(w, tol=0.05):
|
||||
# ORDER MATTERS and w.Edges does not carry it: OCC hands the edges back in whatever order the
|
||||
# face stored them, so concatenating their discretisations gives a scrambled ring. The first
|
||||
# version of this script did exactly that and emitted an outline with 7 duplicated points and
|
||||
# twice the perimeter it should have. OrderedEdges walks the wire, and each edge is reversed
|
||||
# when its own orientation runs against the walk.
|
||||
pts = []
|
||||
for e in w.Edges:
|
||||
for p in e.discretize(Deflection=tol):
|
||||
for e in w.OrderedEdges:
|
||||
d = e.discretize(Deflection=tol)
|
||||
if e.Orientation == "Reversed":
|
||||
d = list(reversed(d))
|
||||
for p in d:
|
||||
pts.append((round(p.x, 3), round(p.z, 3)))
|
||||
# drop consecutive duplicates
|
||||
out = [pts[0]]
|
||||
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,99 @@
|
||||
"""Render the SIMPLIFIED glyph exactly as render_mate_face() draws it — snaporca-x0kd.
|
||||
|
||||
This is the panel the study was missing. simplify_study.py measured a FLAT outline and
|
||||
relief_sheet.py measured the FULL 1508-facet part; neither showed the simplified glyph WITH its
|
||||
relief, which is what the code actually draws and the only thing that answers "is the snout still
|
||||
protruding". Same facet list, same painter order, same camera-fixed lambert as the C++.
|
||||
"""
|
||||
import math, os
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
T = open(os.path.join(HERE, "bear_glyph_table.h")).read()
|
||||
def grab(name, n):
|
||||
body = T.split(name + "[] = {")[1].split("};")[0]
|
||||
body = "\n".join(l.split("//")[0] for l in body.splitlines())
|
||||
out = []
|
||||
for tok in body.replace("\n", " ").split("},"):
|
||||
tok = tok.strip().lstrip("{").strip()
|
||||
if not tok: continue
|
||||
v = [float(x) for x in tok.replace("{", "").split(",")[:n]]
|
||||
if len(v) == n: out.append(tuple(v))
|
||||
return out
|
||||
OUT = grab("kBearOutline", 2)
|
||||
CHIN = grab("kBearChin", 2) # NB: this table entry is the CHIN BAR, not the snout
|
||||
MARKS = grab("kBearMarks", 3)
|
||||
CREST = grab("kBearCrest", 3)
|
||||
SBASE = grab("kBearSnoutBase", 2)
|
||||
PLATE = float(T.split("kBearPlateZ = ")[1].split(";")[0])
|
||||
|
||||
|
||||
def facets():
|
||||
F = []
|
||||
n = len(OUT)
|
||||
for i in range(n): # plate sides -> the grazing silhouette
|
||||
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)) # plate top
|
||||
zm = PLATE + 0.004
|
||||
for cx,cy,r in MARKS: # eyes + cheek dot
|
||||
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)) # chin bar
|
||||
A, B = CREST # THE MUZZLE: base quad + crest
|
||||
nl=(SBASE[0][0],SBASE[0][1],PLATE); nr=(SBASE[1][0],SBASE[1][1],PLATE)
|
||||
tr=(SBASE[2][0],SBASE[2][1],PLATE); tl=(SBASE[3][0],SBASE[3][1],PLATE)
|
||||
F += [([nl,tl,B,A],"body",True), # left flank
|
||||
([nr,A,B,tr],"body",True), # right flank
|
||||
([nl,A,nr],"body",True), # nose cap, sloping because the base overhangs the crest
|
||||
([tr,B,tl],"body",True)] # tail cap
|
||||
return F
|
||||
FACETS = facets()
|
||||
|
||||
BODY=(0.42,0.46,0.52); MARK=(0.126,0.138,0.156)
|
||||
def render(px, elev_deg, ss=8):
|
||||
S=px*ss; a=math.radians(elev_deg); ca,sa=math.cos(a),math.sin(a)
|
||||
# camera orbits down; the connector's +Z (relief) tips toward the horizon
|
||||
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=[]
|
||||
for pts,kind,shade in FACETS:
|
||||
q=[xf(p) for p in pts]
|
||||
tris.append((sum(v[2] for v in q)/len(q), q, kind, shade))
|
||||
tris.sort(key=lambda t:t[0]) # far first
|
||||
for _,q,kind,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
|
||||
nn=math.sqrt(nx*nx+ny*ny+nz*nz) or 1.0
|
||||
nx,ny,nz=nx/nn,ny/nn,nz/nn
|
||||
if nz<0: nx,ny,nz=-nx,-ny,-nz
|
||||
base=BODY if kind=="body" else MARK
|
||||
k=(0.42+0.58*max(0.0,nx*light[0]+ny*light[1]+nz*light[2])) if shade else 1.0
|
||||
col=tuple(min(255,int(255*c*k)) for c in base)
|
||||
d.polygon([(S/2+p[0]*S*0.92, S/2-p[1]*S*0.92) for p in q], fill=col)
|
||||
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+cell+pad
|
||||
sheet=Image.new("RGB",(W,H),(24,27,32))
|
||||
for ci,(e,_) in enumerate(ELEVS):
|
||||
for si,px in enumerate(SIZES):
|
||||
g=render(px,e)
|
||||
sheet.paste(g, (pad+(ci*len(SIZES)+si)*cell+(cell-px)//2, pad+(cell-px)//2))
|
||||
sheet.resize((W*2,H*2), Image.NEAREST).save(os.path.join(HERE,"glyph-preview.png"))
|
||||
|
||||
# how much of the glyph is the snout: render with and without the tent and diff
|
||||
def render_no_tent(px, elev):
|
||||
global FACETS
|
||||
keep=FACETS; FACETS=FACETS[:-4]
|
||||
try: return render(px, elev)
|
||||
finally: FACETS=keep
|
||||
print(f"{'elev':>8} {'lit px@32':>10} {'snout px':>9} {'snout share':>12}")
|
||||
for e,_ in ELEVS:
|
||||
a=render(32,e); b=render_no_tent(32,e)
|
||||
la=sum(1 for p in a.get_flattened_data() if p!=(24,27,32))
|
||||
diff=sum(1 for p,q in zip(a.get_flattened_data(), b.get_flattened_data()) if p!=q)
|
||||
print(f"{e:>8} {la:>10} {diff:>9} {100.0*diff/max(1,la):>11.1f}%")
|
||||
print("WROTE glyph-preview.png")
|
||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 35 KiB |
@@ -0,0 +1,71 @@
|
||||
"""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")
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 49 KiB |
@@ -316,6 +316,13 @@ void AppConfig::set_defaults()
|
||||
if (get("zoom_to_mouse").empty())
|
||||
set_bool("zoom_to_mouse", false);
|
||||
|
||||
// Design tab: draw a mate connector as a face rather than as the abstract disc + roll
|
||||
// quadrant. Defaults ON — face orientation is hardwired perception, so the roll and the
|
||||
// verse read without being learned, which no abstract glyph achieves. Turning it off
|
||||
// restores the conventional CAD representation for users who expect it (snaporca-x0kd).
|
||||
if (get("design_connector_face_glyph").empty())
|
||||
set_bool("design_connector_face_glyph", true);
|
||||
|
||||
//#ifdef SUPPORT_SHOW_HINTS
|
||||
if (get("show_hints").empty())
|
||||
set_bool("show_hints", false);
|
||||
|
||||
@@ -3525,6 +3525,14 @@ void DesignSketchTool::render_mate_connectors()
|
||||
const char* s = ::getenv("SNAPORCA_GLYPH");
|
||||
return s && (*s == 'A' || *s == 'a');
|
||||
}();
|
||||
// The face treatment, on by default. Read every frame rather than latched in a static, so
|
||||
// toggling the preference takes effect on the next repaint instead of at the next launch —
|
||||
// it is a look, and a look you cannot A/B without restarting will not get compared.
|
||||
// SNAPORCA_GLYPH=D forces the disc regardless, which is how the rig drives the other branch.
|
||||
const bool face_style = !style_A
|
||||
&& wxGetApp().app_config->get_bool("design_connector_face_glyph")
|
||||
&& [] { const char* s = ::getenv("SNAPORCA_GLYPH");
|
||||
return !(s && (*s == 'D' || *s == 'd')); }();
|
||||
|
||||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||||
const Vec3d right = cam.get_dir_right().normalized();
|
||||
@@ -3557,6 +3565,14 @@ void DesignSketchTool::render_mate_connectors()
|
||||
// dotted arc that flickered with the camera. Depth off floats it through solids, depth on and
|
||||
// coplanar tears it — the lift is what buys both. Scaled by upp so it stays sub-pixel at any
|
||||
// zoom instead of becoming a visible gap when you zoom in.
|
||||
// A face asserts a definite roll. When the roll could NOT be derived, drawing one would be
|
||||
// a confident lie about the very thing that is unknown — the same objection that rejected
|
||||
// billboarding the quadrant — so an underived connector keeps the disc treatment and its
|
||||
// hatched quadrant, whatever the preference says.
|
||||
if (face_style && !g.roll_undefined) {
|
||||
render_mate_face(g.origin, X, Y, Z, R, body);
|
||||
} else {
|
||||
|
||||
SketchPlane cp; cp.origin = g.origin + Z * (0.7 * upp);
|
||||
cp.x_axis = X; cp.y_axis = Y; cp.normal = Z;
|
||||
m_plane = cp;
|
||||
@@ -3610,6 +3626,7 @@ void DesignSketchTool::render_mate_connectors()
|
||||
}
|
||||
draw_strokes(m_mc_stroke_model, q, lw, g.roll_undefined ? warn : gold);
|
||||
}
|
||||
} // end of the disc treatment
|
||||
|
||||
// ---- the axes. Billboarded at the origin: a 3D direction is projected onto the screen
|
||||
// frame, which is the only way an arrow keeps a readable head at any viewing angle.
|
||||
@@ -3666,6 +3683,191 @@ void DesignSketchTool::render_mate_connectors()
|
||||
m_plane = saved;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// THE FACE TREATMENT of the mate connector (snaporca-x0kd). The disc + roll quadrant answers
|
||||
// "where is X" with a shape that has to be learned; a face does not. Face orientation is
|
||||
// hardwired perception -- a toddler reads a face's roll and verse with no instruction at all --
|
||||
// and that is the whole reason this exists. Default ON, switchable in Preferences for users who
|
||||
// expect the conventional CAD representation.
|
||||
//
|
||||
// WHY A RELIEF AND NOT A FLAT DRAWING, which is the non-obvious half. A flat face drawn in the
|
||||
// connector's plane foreshortens by sin(elevation) and collapses at a grazing view exactly like
|
||||
// the quadrant it replaces -- measured on the rig, the quadrant falls from 89 lit pixels at 47
|
||||
// degrees to 3 at 10 and 0 edge-on. A relief does not: at a grazing angle its SILHOUETTE carries
|
||||
// the information. The same bear rendered flat vs in relief gives 164 vs 210 lit pixels at 16
|
||||
// degrees and 66 vs 120 at 6. So the glyph is a small shaded solid, not an outline.
|
||||
//
|
||||
// The geometry is EMITTED from the real part by doc/design/mate-connectors/emit_glyph_table.py,
|
||||
// not hand-drawn, so the glyph and the printed connector cannot drift apart. Two vertices stand
|
||||
// above the 3 mm plate in the actual B-rep, which is why the snout here is a tent with one crest
|
||||
// edge and four flanks drafted at 20 degrees rather than anything more elaborate.
|
||||
//
|
||||
// The cheek dot is the handedness mark. Without it the glyph differs from its own mirror by only
|
||||
// 5-9 % of its lit pixels, which is not enough to read; the dot roughly doubles that and, unlike
|
||||
// making the eyes uneven, identifies the side from that cheek alone instead of by comparison.
|
||||
// Emitted by doc/design/mate-connectors/emit_glyph_table.py from bear.step — do not hand-edit.
|
||||
// Normalised to the part's bounding span and centred: the renderer scales by one radius.
|
||||
static const Vec2d kBearOutline[] = { // 12 verts, RDP eps 0.030, CCW
|
||||
{+0.3842, +0.3294}, {+0.3156, +0.4002}, {+0.2424, +0.3294},
|
||||
{-0.2524, +0.3294}, {-0.3377, +0.3877}, {-0.3693, +0.3298},
|
||||
{-0.3256, +0.2631}, {-0.4893, -0.3337}, {-0.3960, -0.4002},
|
||||
{+0.4151, -0.4002}, {+0.5000, -0.3154}, {+0.3156, +0.2631},
|
||||
};
|
||||
static const Vec2d kBearChin[] = { // the CHIN BAR, flat. The muzzle is relief — see kBearCrest.
|
||||
{-0.2682, -0.3578}, {+0.2628, -0.3578}, {+0.2237, -0.1786},
|
||||
};
|
||||
// {cx, cy, r}: two eyes, then the cheek dot that carries handedness (snaporca-wi3z).
|
||||
static const Vec3d kBearMarks[] = {
|
||||
{-0.1997, +0.1760, +0.0590},
|
||||
{+0.1947, +0.1760, +0.0590},
|
||||
{+0.2797, +0.0760, +0.0380},
|
||||
};
|
||||
// THE MUZZLE, lifted off the mesh: a tapered wedge, base quad + crest edge, 6 facets.
|
||||
// This is the only feature standing along +Z and the only one still legible edge-on.
|
||||
static const double kBearPlateZ = +0.0360;
|
||||
static const Vec2d kBearSnoutBase[] = { // CCW from the nose end
|
||||
{-0.0727, -0.2417},
|
||||
{+0.0630, -0.2417},
|
||||
{+0.0259, +0.1939},
|
||||
{-0.0356, +0.1939},
|
||||
};
|
||||
static const Vec3d kBearCrest[] = { // nose (tall) -> tail (short)
|
||||
{-0.0048, -0.1793, +0.2073},
|
||||
{-0.0048, +0.1605, +0.1279},
|
||||
};
|
||||
|
||||
void DesignSketchTool::render_mate_face(const Vec3d& origin, const Vec3d& X, const Vec3d& Y,
|
||||
const Vec3d& Z, double R, const ColorRGBA& body)
|
||||
{
|
||||
const Camera& cam = wxGetApp().plater()->get_camera();
|
||||
const Vec3d right = cam.get_dir_right().normalized();
|
||||
const Vec3d up = cam.get_dir_up().normalized();
|
||||
const Vec3d fwd = cam.get_dir_forward().normalized();
|
||||
const double S = 2.0 * R; // the table spans 1.0, the disc spans 2R
|
||||
|
||||
// Light fixed in CAMERA space, so orbiting the model does not swing the shading around and
|
||||
// turn a stable symbol into a flickering one.
|
||||
const Vec3d light = (-0.35 * right + 0.55 * up - 0.76 * fwd).normalized();
|
||||
|
||||
auto to_world = [&](const Vec3d& p) {
|
||||
return origin + X * (p.x() * S) + Y * (p.y() * S) + Z * (p.z() * S);
|
||||
};
|
||||
|
||||
struct Facet { std::vector<Vec3d> w; ColorRGBA c; double depth; };
|
||||
std::vector<Facet> facets;
|
||||
auto emit = [&](std::vector<Vec3d> pts, const ColorRGBA& base, bool shade) {
|
||||
if (pts.size() < 3) return;
|
||||
Facet f; f.w.reserve(pts.size());
|
||||
for (const Vec3d& p : pts) f.w.push_back(to_world(p));
|
||||
const Vec3d n0 = (f.w[1] - f.w[0]).cross(f.w[2] - f.w[0]);
|
||||
Vec3d n = Z;
|
||||
if (n0.norm() > 1e-12) n = n0.normalized();
|
||||
if (n.dot(fwd) > 0.0) n = -n; // always take the camera-facing side
|
||||
double k = 1.0;
|
||||
if (shade) {
|
||||
// Ambient floor so a facet turned away still reads as part of the same object rather
|
||||
// than as a hole punched in it.
|
||||
k = 0.42 + 0.58 * std::max(0.0, n.dot(light));
|
||||
}
|
||||
f.c = ColorRGBA(float(base.r() * k), float(base.g() * k), float(base.b() * k), base.a());
|
||||
double d = 0.0;
|
||||
for (const Vec3d& p : f.w) d += p.dot(fwd);
|
||||
f.depth = d / double(f.w.size());
|
||||
facets.push_back(std::move(f));
|
||||
};
|
||||
|
||||
const int NO = int(sizeof(kBearOutline) / sizeof(kBearOutline[0]));
|
||||
const double zp = kBearPlateZ;
|
||||
|
||||
// The plate: sides first so the silhouette exists at a grazing view, then the top.
|
||||
for (int i = 0; i < NO; ++i) {
|
||||
const Vec2d& a = kBearOutline[i];
|
||||
const Vec2d& b = kBearOutline[(i + 1) % NO];
|
||||
emit({ Vec3d(a.x(), a.y(), 0.0), Vec3d(b.x(), b.y(), 0.0),
|
||||
Vec3d(b.x(), b.y(), zp), Vec3d(a.x(), a.y(), zp) }, body, true);
|
||||
}
|
||||
{
|
||||
std::vector<Vec3d> top;
|
||||
top.reserve(NO);
|
||||
for (int i = 0; i < NO; ++i) top.emplace_back(kBearOutline[i].x(), kBearOutline[i].y(), zp);
|
||||
emit(std::move(top), body, true);
|
||||
}
|
||||
|
||||
// The marks, a hair above the plate so they cannot z-fight it: two eyes then the cheek dot.
|
||||
const ColorRGBA mark(body.r() * 0.30f, body.g() * 0.30f, body.b() * 0.30f, 1.0f);
|
||||
const double zm = zp + 0.004;
|
||||
for (const Vec3d& m : kBearMarks) {
|
||||
std::vector<Vec3d> disc;
|
||||
const int N = 12;
|
||||
for (int i = 0; i < N; ++i) {
|
||||
const double a = (2.0 * M_PI * i) / N;
|
||||
disc.emplace_back(m.x() + m.z() * std::cos(a), m.y() + m.z() * std::sin(a), zm);
|
||||
}
|
||||
emit(std::move(disc), mark, false);
|
||||
}
|
||||
{
|
||||
std::vector<Vec3d> chin;
|
||||
for (const Vec2d& p : kBearChin) chin.emplace_back(p.x(), p.y(), zm);
|
||||
emit(std::move(chin), mark, false);
|
||||
}
|
||||
|
||||
// THE MUZZLE, and the two decisions that make it legible rather than merely present.
|
||||
//
|
||||
// It is the one feature standing along +Z, so it says which way the connector points, and it
|
||||
// is all that survives edge-on where a drawing in the plane has nothing left. Its geometry is
|
||||
// the part's own ridge: crest 29.0 mm, 6.58 mm drop, 13.1 deg, against the 28.3 / 6.61 / 13.1
|
||||
// the review measured on the B-rep. Base taken from the mesh, NOT recomputed from
|
||||
// height*tan(draft) -- that produced a needle, because the real base overhangs the crest at
|
||||
// both ends and it is the overhang that makes this a wedge rather than a blade.
|
||||
//
|
||||
// BUT FIDELITY ALONE FAILS. 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 reads as a scratch -- Tommaso looked at the faithful
|
||||
// version 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 gets two deliberate exaggerations:
|
||||
//
|
||||
// COLOUR does the work. In the body tone the muzzle is a grey sliver whichever way it is
|
||||
// lit; in the accent it is the first thing the eye lands on at every elevation, and at 6
|
||||
// degrees it is the ONLY structured thing above the flat line. Measured share of lit
|
||||
// pixels at 90/16/6 deg: body 14.8/11.3/17.5 %, accent 18.3/19.2/23.9 %.
|
||||
// WIDTH 1.8x on top of that: 23.5/25.2/31.2 %, and it stops reading as a needle.
|
||||
//
|
||||
// The accent is the same gold the disc treatment spends on its roll quadrant, which is
|
||||
// consistent -- it is this tab's "here is the direction that matters" colour. Polarity is
|
||||
// still carried by the Z arrow's head, so nothing collides.
|
||||
{
|
||||
const ColorRGBA gold(0.93f, 0.66f, 0.09f, 1.0f);
|
||||
const double widen = 1.8;
|
||||
const Vec3d& A = kBearCrest[0];
|
||||
const Vec3d& B = kBearCrest[1];
|
||||
auto base = [&](int i) {
|
||||
return Vec3d(kBearSnoutBase[i].x() * widen, kBearSnoutBase[i].y(), zp);
|
||||
};
|
||||
const Vec3d nl = base(0), nr = base(1), tr = base(2), tl = base(3);
|
||||
emit({ nl, tl, B, A }, gold, true); // left flank
|
||||
emit({ nr, A, B, tr }, gold, true); // right flank
|
||||
emit({ nl, A, nr }, gold, true); // nose cap, sloped by the base overhang
|
||||
emit({ tr, B, tl }, gold, true); // tail cap
|
||||
}
|
||||
|
||||
// Painter's algorithm: depth testing is off for this overlay, so draw order IS the depth.
|
||||
std::sort(facets.begin(), facets.end(),
|
||||
[](const Facet& a, const Facet& b) { return a.depth > b.depth; });
|
||||
|
||||
// draw_fill works in m_plane, so project into a screen-aligned frame at the connector origin
|
||||
// and hand it flat polygons. The relief survives because the PROJECTION is 3D, not the plane.
|
||||
const SketchPlane saved = m_plane;
|
||||
SketchPlane bb; bb.origin = origin; bb.x_axis = right; bb.y_axis = up; bb.normal = fwd;
|
||||
m_plane = bb;
|
||||
glsafe(::glDisable(GL_DEPTH_TEST));
|
||||
for (const Facet& f : facets) {
|
||||
std::vector<Vec2d> poly;
|
||||
poly.reserve(f.w.size());
|
||||
for (const Vec3d& p : f.w) poly.emplace_back((p - origin).dot(right), (p - origin).dot(up));
|
||||
draw_fill(m_mc_fill_model, poly, f.c);
|
||||
}
|
||||
m_plane = saved;
|
||||
}
|
||||
|
||||
void DesignSketchTool::render_extrude_gizmo()
|
||||
{
|
||||
if (!m_ex_active) return;
|
||||
|
||||
@@ -1079,8 +1079,14 @@ private:
|
||||
std::vector<SketchPlane> m_datum_planes;
|
||||
std::vector<Vec2d> m_datum_sizes; // per-plane (u,v) full extent; empty -> default
|
||||
void render_mate_connectors(); // disc + roll quadrant + one-sided Z arrow
|
||||
// The face treatment of the same connector: a shaded low-poly relief of a bear's head in the
|
||||
// connector's own frame. Draws the plate, the snout tent and the marks; the caller still draws
|
||||
// the Z arrow, which is shared with the disc treatment.
|
||||
void render_mate_face(const Vec3d& origin, const Vec3d& X, const Vec3d& Y, const Vec3d& Z,
|
||||
double R, const ColorRGBA& body);
|
||||
std::vector<MateConnectorGlyph> m_mate_connectors;
|
||||
GLModel m_mc_stroke_model;
|
||||
GLModel m_mc_fill_model; // the face treatment's shaded facets
|
||||
GLModel m_solid_face_model;
|
||||
GLModel m_solid_edge_model;
|
||||
GLModel m_solid_vertex_model;
|
||||
|
||||
@@ -1802,6 +1802,12 @@ void PreferencesDialog::create_items()
|
||||
auto reverse_mouse_zoom = create_item_checkbox(_L("Reverse mouse zoom"), _L("If enabled, reverses the direction of zoom with mouse wheel."), "reverse_mouse_wheel_zoom");
|
||||
g_sizer->Add(reverse_mouse_zoom);
|
||||
|
||||
auto item_connector_face_glyph = create_item_checkbox(_L("Draw mate connectors as a face"),
|
||||
_L("In the Design tab, draw a mate connector as a small face instead of the conventional "
|
||||
"disc with a roll quadrant. A face's orientation is read without being learned. "
|
||||
"Turn this off for the conventional CAD representation."), "design_connector_face_glyph");
|
||||
g_sizer->Add(item_connector_face_glyph);
|
||||
|
||||
std::vector<wxString> ButtonDragActions = {_L("None"), _L("Pan"), _L("Rotate")};
|
||||
auto item_left_mouse_drag = create_item_combobox(_L("Left Mouse Drag"), _L("Set the action that dragging the left mouse button should perform."), "left_mouse_drag_action", ButtonDragActions);
|
||||
g_sizer->Add(item_left_mouse_drag);
|
||||
|
||||