201 خطوط
14 KiB
Python
201 خطوط
14 KiB
Python
"""Decode actual symbol geometry and document-defined electrical marker styles."""
|
|
import math
|
|
import textwrap
|
|
from app.models.schematic import Primitive, Point
|
|
from .geometry import number, integer, coordinate, point, flag, bounds, color, poly_path, transform_point
|
|
|
|
KINDS={1:'component',2:'pin',3:'ieee_symbol',4:'label',5:'bezier',6:'polyline',7:'polygon',8:'ellipse',9:'pie',10:'round_rectangle',11:'elliptical_arc',12:'arc',13:'line',14:'rectangle',15:'sheet_symbol',16:'sheet_entry',17:'power_port',18:'port',22:'no_erc',25:'net_label',26:'bus',27:'wire',28:'text_frame',29:'junction',30:'image',31:'sheet',32:'sheet_name',33:'sheet_filename',34:'designator',37:'bus_entry',39:'template',41:'parameter',43:'parameter_set',44:'implementation_list',45:'implementation',46:'pin_map',47:'pin_map',48:'implementation_parameter',209:'text_frame',225:'dashed_rectangle'}
|
|
SUPPORTED=set(KINDS)-{3,30,43}
|
|
|
|
|
|
def decode(record, component_id, sheet, warn, records):
|
|
p=record.properties
|
|
t=integer(p,'RECORD')
|
|
pos=point(p)
|
|
output=[]
|
|
stroke=color(p.get('COLOR','8388608' if t in (25,26,27) else '128'))
|
|
fill=color(p.get('AREACOLOR','16777215')) if flag(p,'ISSOLID') and not flag(p,'TRANSPARENT') else 'none'
|
|
width=(0.4,1,2,4)[min(3,max(0,integer(p,'LINEWIDTH')))]
|
|
dash=[4,2] if integer(p,'LINESTYLE') or t==225 else []
|
|
if integer(p,'LINESTYLE'): warn('dash_metrics','Non-solid line dash spacing requires Altium reference validation',record)
|
|
def path(points=None,d=None,closed=False,fill_=None,stroke_=None,width_=None,box=None):
|
|
output.append(Primitive(id=f'{record.id}:p{len(output)}',kind='path',
|
|
object_id=record.id,component_id=component_id,path=d if d is not None else poly_path(points or [],closed),
|
|
bounds=box or bounds(points or [],width+1),stroke=stroke_ or stroke,fill=fill if fill_ is None else fill_,
|
|
width=width_ or width,dash=dash))
|
|
def text(value,at=pos,angle=None,font=None,anchor=None,baseline=None,frame=None,wrap_width=None):
|
|
if not value: return
|
|
fid=font or integer(p,'FONTID',integer(sheet,'SYSTEMFONT',1))
|
|
# Browser em size approximation: Altium font size is not a CSS point size.
|
|
# This yields glyph cap heights close to the KiCad importer's size/2 stroke text.
|
|
size=max(1,min(300,number(sheet,f'SIZE{fid}',10)))*0.9
|
|
just=integer(p,'JUSTIFICATION')
|
|
a=anchor or ('start','middle','end')[just%3]
|
|
base=baseline or ('alphabetic','middle','hanging')[min(2,max(0,just//3))]
|
|
rotation=-90*integer(p,'ORIENTATION') if angle is None else angle
|
|
raw=value.replace('~1','\n')
|
|
# Altium backslash suffix marks an overbar on the preceding character.
|
|
clean=raw.replace('\\','')
|
|
if raw.startswith('='):
|
|
key=raw[1:].upper()
|
|
resolved=p.get('_RESOLVED_TEXT')
|
|
if resolved is not None: clean=resolved
|
|
else: warn('unresolved_text',f'Unresolved parameter reference {raw}',record)
|
|
lines=clean.splitlines() or ['']
|
|
if wrap_width is not None:
|
|
columns=max(1,int(wrap_width/(size*0.55)))
|
|
lines=[line for paragraph in lines for line in (textwrap.wrap(paragraph,width=columns,break_long_words=True,break_on_hyphens=False,replace_whitespace=False) or [''])]
|
|
for line_i,line in enumerate(lines):
|
|
loc=Point(x=at.x,y=at.y+line_i*size*1.2)
|
|
w=max(size*.6,len(line)*size*.8)
|
|
x={'start':0,'middle':-w/2,'end':-w}[a]
|
|
y={'alphabetic':-size,'middle':-size/2,'hanging':0}[base]
|
|
corners=[transform_point(Point(x=cx,y=cy),loc,-rotation,False) for cx,cy in [(x,y),(x+w,y),(x+w,y+size*1.3),(x,y+size*1.3)]]
|
|
box=bounds(corners,2)
|
|
if frame:
|
|
x0,y0=max(box.x,frame.x),max(box.y,frame.y)
|
|
x1,y1=min(box.x+box.width,frame.x+frame.width),min(box.y+box.height,frame.y+frame.height)
|
|
if x1<=x0 or y1<=y0: continue
|
|
from app.models.schematic import Bounds
|
|
box=Bounds(x=x0,y=y0,width=x1-x0,height=y1-y0)
|
|
output.append(Primitive(id=f'{record.id}:p{len(output)}',kind='text',object_id=record.id,
|
|
component_id=component_id,text=line,position=loc,rotation=rotation,mirrored=False,
|
|
bounds=box,clip=frame,stroke=color(p.get('TEXTCOLOR',p.get('COLOR','128'))),fill='none',
|
|
font_size=size,font_family=sheet.get(f'FONTNAME{fid}','Arial'),bold=flag(sheet,f'BOLD{fid}'),
|
|
italic=flag(sheet,f'ITALIC{fid}'),underline=flag(sheet,f'UNDERLINE{fid}'),anchor=a,baseline=base))
|
|
if '\\' in raw:
|
|
warn('text_overbar','Overbar text is displayed without its per-character overline',record)
|
|
def local(points,origin=pos,rotation=None,closed=False,fill_=None):
|
|
pts=[transform_point(Point(x=x,y=y),origin,90*integer(p,'ORIENTATION') if rotation is None else rotation) for x,y in points]
|
|
path(pts,closed=closed,fill_=fill_)
|
|
def ellipse(cx,cy,rx,ry,solid='none'):
|
|
if rx<=0 or ry<=0: return
|
|
d=f'M {cx-rx:.15g},{cy:.15g} A {rx:.15g},{ry:.15g} 0 1 0 {cx+rx:.15g},{cy:.15g} A {rx:.15g},{ry:.15g} 0 1 0 {cx-rx:.15g},{cy:.15g} Z'
|
|
path(d=d,fill_=solid,box=bounds([Point(x=cx-rx,y=cy-ry),Point(x=cx+rx,y=cy+ry)],width))
|
|
|
|
if t in (5,6,7,26,27):
|
|
n=integer(p,'LOCATIONCOUNT')
|
|
extra=integer(p,'EXTRALOCATIONCOUNT')
|
|
if n<0 or extra<0 or n+extra>20000: raise ValueError('Invalid point count')
|
|
pts=[Point(x=coordinate(p,f'X{i}'),y=-coordinate(p,f'Y{i}')) for i in range(1,n+1)]
|
|
pts += [Point(x=coordinate(p,f'EX{i}'),y=-coordinate(p,f'EY{i}')) for i in range(n+1,n+extra+1)]
|
|
if t==5:
|
|
if len(pts)<4 or (len(pts)-1)%3: warn('bezier_points','Non-cubic Bezier point sequence is unsupported',record)
|
|
else:
|
|
d=f'M{pts[0].x:.15g},{pts[0].y:.15g} '
|
|
for i in range(1,len(pts),3): d+='C'+' '.join(f'{q.x:.15g},{q.y:.15g}' for q in pts[i:i+3])+' '
|
|
path(pts,d=d,fill_='none')
|
|
elif len(pts)>1: path(pts,closed=t==7,fill_=fill if t==7 else 'none')
|
|
if integer(p,'STARTLINESHAPE') or integer(p,'ENDLINESHAPE'):
|
|
warn('line_decoration','Line end decorations are not rendered',record)
|
|
elif t in (13,37): path([pos,point(p,'CORNER')],fill_='none')
|
|
elif t in (10,14,15,28,209,225):
|
|
corner=point(p,'CORNER')
|
|
if t==15: corner=Point(x=pos.x+coordinate(p,'XSIZE'),y=pos.y+coordinate(p,'YSIZE'))
|
|
x,y=min(pos.x,corner.x),min(pos.y,corner.y); w,h=abs(pos.x-corner.x),abs(pos.y-corner.y)
|
|
pts=[Point(x=x,y=y),Point(x=x+w,y=y),Point(x=x+w,y=y+h),Point(x=x,y=y+h)]
|
|
if t==10:
|
|
rx=min(w/2,abs(coordinate(p,'CORNERXRADIUS'))); ry=min(h/2,abs(coordinate(p,'CORNERYRADIUS')))
|
|
if rx and ry:
|
|
d=f'M{x+rx},{y} H{x+w-rx} A{rx},{ry} 0 0 1 {x+w},{y+ry} V{y+h-ry} A{rx},{ry} 0 0 1 {x+w-rx},{y+h} H{x+rx} A{rx},{ry} 0 0 1 {x},{y+h-ry} V{y+ry} A{rx},{ry} 0 0 1 {x+rx},{y} Z'
|
|
path(pts,d=d)
|
|
else: path(pts,closed=True)
|
|
elif t not in (28,209) or flag(p,'SHOWBORDER') or fill!='none': path(pts,closed=True,fill_=fill,stroke_=stroke if t not in (28,209) or flag(p,'SHOWBORDER') else fill)
|
|
if t in (28,209):
|
|
frame=bounds(pts) if flag(p,'CLIPTORECT') else None
|
|
text(p.get('TEXT',''),Point(x=x+2,y=y+2),angle=0,baseline='hanging',frame=frame,wrap_width=max(1,w-4) if flag(p,'WORDWRAP') else None)
|
|
if flag(p,'WORDWRAP'): warn('text_frame_metrics','Text frame wrapping uses estimated font advances; clipping is preserved',record)
|
|
if flag(p,'COLLAPSED'): warn('collapsed_note','Collapsed note is rendered as a clipped text frame',record)
|
|
elif t in (8,9,11,12):
|
|
rx=abs(coordinate(p,'RADIUS')); ry=abs(coordinate(p,'SECONDARYRADIUS')) or rx
|
|
start=number(p,'STARTANGLE'); end=number(p,'ENDANGLE',360)
|
|
if t==8 or abs(end-start)>=359.999 or start==end:
|
|
ellipse(pos.x,pos.y,rx,ry,fill if t in (8,9) else 'none')
|
|
elif rx and ry:
|
|
def pt(deg):
|
|
a=math.radians(deg); a=math.atan2(rx*math.sin(a),ry*math.cos(a))
|
|
return Point(x=pos.x+rx*math.cos(a),y=pos.y-ry*math.sin(a))
|
|
a,b=pt(start),pt(end); sweep=(end-start)%360
|
|
d=f'M{a.x:.15g},{a.y:.15g} A{rx:.15g},{ry:.15g} 0 {1 if sweep>180 else 0} 0 {b.x:.15g},{b.y:.15g}'
|
|
if t==9: d+=f' L{pos.x},{pos.y} Z'
|
|
path(d=d,fill_=fill if t==9 else 'none',box=bounds([Point(x=pos.x-rx,y=pos.y-ry),Point(x=pos.x+rx,y=pos.y+ry)],width))
|
|
elif t==2:
|
|
bits=integer(p,'PINCONGLOMERATE'); rot=(bits&3)*90
|
|
length=coordinate(p,'PINLENGTH')
|
|
end=transform_point(Point(x=length,y=0),pos,rot)
|
|
path([pos,end],fill_='none')
|
|
# Name extends into body, number lies over the pin. Keep labels upright.
|
|
horizontal=(bits&3)%2==0
|
|
direction=1 if (bits&3) in (0,3) else -1
|
|
if bits&8:
|
|
at=Point(x=pos.x-(4*direction if horizontal else 0),y=pos.y+(0 if horizontal else -4*direction))
|
|
text(p.get('NAME',''),at,angle=0 if horizontal else -90,font=integer(p,'NAME_CUSTOMFONTID',1),anchor='end' if direction>0 else 'start',baseline='middle')
|
|
if bits&16:
|
|
at=Point(x=(pos.x+end.x)/2-(0 if horizontal else 3),y=(pos.y+end.y)/2-(3 if horizontal else 0))
|
|
text(p.get('DESIGNATOR',''),at,angle=0 if horizontal else -90,font=integer(p,'DESIGNATOR_CUSTOMFONTID',1),anchor='middle')
|
|
if integer(p,'SYMBOL_OUTEREDGE')==1: ellipse(pos.x,pos.y,2,2)
|
|
elif integer(p,'SYMBOL_OUTEREDGE'): warn('pin_decoration','Unsupported outer-edge pin symbol',record)
|
|
if integer(p,'SYMBOL_INNEREDGE')==3:
|
|
local([(0,-3),(-4,0),(0,3)],rotation=rot,fill_='none')
|
|
elif integer(p,'SYMBOL_INNEREDGE'): warn('pin_decoration','Unsupported inner-edge pin symbol',record)
|
|
if integer(p,'SYMBOL_OUTER') or integer(p,'SYMBOL_INNER'): warn('pin_decoration','Additional pin decorations are not rendered',record)
|
|
if any(k in p for k in ('NAME_CUSTOMPOSITION_MARGIN','DESIGNATOR_CUSTOMPOSITION_MARGIN')) or integer(p,'PINNAME_POSITIONCONGLOMERATE') or integer(p,'PINDESIGNATOR_POSITIONCONGLOMERATE'):
|
|
warn('pin_text_position','Custom pin text positions use standard inferred offsets',record)
|
|
elif t in (4,25,32,33,34,41):
|
|
value=p.get('TEXT','')
|
|
if t==34 and component_id:
|
|
parent=records.get((record.stream,int(component_id.rsplit(':',1)[-1])))
|
|
if parent and integer(parent.properties,'PARTCOUNT')>2:
|
|
unit=integer(parent.properties,'CURRENTPARTID',1)
|
|
if 1<=unit<=26: value+=chr(64+unit)
|
|
else: warn('unit_suffix','Unit suffix above Z is unsupported',record)
|
|
if t==41 and flag(p,'SHOWNAME'): value=p.get('NAME','')+': '+value
|
|
# Parameter labels use upright text with opposite anchors at orientations 2/3.
|
|
if t in (34,41) and integer(p,'ORIENTATION') in (2,3):
|
|
text(value,angle=0 if integer(p,'ORIENTATION')==2 else -90,anchor='end',baseline='hanging')
|
|
elif t==4 and integer(p,'ORIENTATION')==2:
|
|
just=max(0,min(8,integer(p,'JUSTIFICATION')))
|
|
text(value,angle=0,anchor=('end','middle','start')[just%3],baseline=('hanging','middle','alphabetic')[just//3])
|
|
else: text(value)
|
|
elif t==29: ellipse(pos.x,pos.y,1.5,1.5,stroke)
|
|
elif t==22:
|
|
path([Point(x=pos.x-2,y=pos.y-2),Point(x=pos.x+2,y=pos.y+2)],fill_='none')
|
|
path([Point(x=pos.x-2,y=pos.y+2),Point(x=pos.x+2,y=pos.y-2)],fill_='none')
|
|
elif t==17:
|
|
style=integer(p,'STYLE'); rot=90*integer(p,'ORIENTATION')
|
|
if flag(p,'ISCROSSSHEETCONNECTOR'):
|
|
local([(8,-3),(4,0),(8,3)],fill_='none');local([(4,-3),(0,0),(4,3)],fill_='none')
|
|
elif style in (0,1,2,4):
|
|
local([(0,0),(10,0)],fill_='none')
|
|
if style==0:
|
|
c=transform_point(Point(x=10,y=0),pos,rot);ellipse(c.x,c.y,2.5,2.5)
|
|
elif style==1: local([(6,-3),(10,0),(6,3)],fill_='none')
|
|
elif style==2: local([(10,-7),(10,7)],fill_='none')
|
|
elif style==4:
|
|
for dx,span in [(10,7),(13,4),(16,1)]: local([(dx,-span),(dx,span)],fill_='none')
|
|
else: warn('power_style',f'Unsupported power port style {style}; marker omitted rather than substituted',record)
|
|
if flag(p,'SHOWNETNAME'):
|
|
offset = 20 if style==4 else 14 if flag(p,'ISCROSSSHEETCONNECTOR') else 12
|
|
at=transform_point(Point(x=offset,y=0),pos,rot)
|
|
orient=integer(p,'ORIENTATION')
|
|
text(p.get('TEXT',''),at,angle=0,anchor=('start','middle','end','middle')[orient%4],baseline=('middle','alphabetic','middle','hanging')[orient%4])
|
|
elif t==18:
|
|
style=integer(p,'STYLE'); w=coordinate(p,'WIDTH'); h=coordinate(p,'HEIGHT') or 10
|
|
# Style 0..3 horizontal and 4..7 vertical; bit0 left/top, bit1 right/bottom.
|
|
tip=min(h/2,w/3); left=bool(style&1); right=bool(style&2)
|
|
pts=[(0,0),(tip if left else 0,-h/2),(w-tip if right else w,-h/2),(w,0),(w-tip if right else w,h/2),(tip if left else 0,h/2)]
|
|
local(pts,rotation=90 if style>=4 else 0,closed=True,fill_=color(p.get('AREACOLOR','8454143')))
|
|
at=transform_point(Point(x=w/2,y=0),pos,90 if style>=4 else 0)
|
|
text(p.get('NAME',''),at,angle=-90 if style>=4 else 0,anchor='middle',baseline='middle')
|
|
if p.get('HARNESSTYPE'): warn('harness','Harness semantics are not resolved',record)
|
|
elif t==16:
|
|
parent=records.get((record.stream,integer(p,'OWNERINDEX',-1)))
|
|
if parent and integer(parent.properties,'RECORD')==15:
|
|
q=parent.properties; origin=point(q);side=integer(p,'SIDE');distance=number(p,'DISTANCEFROMTOP')*10+number(p,'DISTANCEFROMTOP_FRAC1')/100000
|
|
at=Point(x=origin.x+(coordinate(q,'XSIZE') if side==1 else distance if side>=2 else 0),y=origin.y+(coordinate(q,'YSIZE') if side==3 else distance if side<2 else 0))
|
|
text(p.get('NAME',''),at,anchor='end' if side==1 else 'start',baseline='middle')
|
|
path([Point(x=at.x-2,y=at.y),Point(x=at.x+2,y=at.y)],fill_='none')
|
|
warn('sheet_entry_shape','Sheet entry location and name are rendered; entry arrow style is unsupported',record)
|
|
else: warn('orphan_sheet_entry','Sheet entry owner not found',record)
|
|
return output
|