jwwkit 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jwwkit/__init__.py +14 -0
- jwwkit/__main__.py +4 -0
- jwwkit/_commands/__init__.py +1 -0
- jwwkit/_commands/_integrity.py +55 -0
- jwwkit/_commands/check_installation.py +484 -0
- jwwkit/_commands/compare_drawings.py +29 -0
- jwwkit/_commands/inspect_jwf.py +24 -0
- jwwkit/_commands/recognize_planar_tategu.py +20 -0
- jwwkit/_commands/summarize_drawing.py +16 -0
- jwwkit/_comparison_composites.py +179 -0
- jwwkit/_edit_result_core.py +175 -0
- jwwkit/_ezjww_adapter.py +12 -0
- jwwkit/_geometry_engine.py +204 -0
- jwwkit/_geometry_math.py +93 -0
- jwwkit/_normalization_inspection.py +83 -0
- jwwkit/_replacement_contract.py +161 -0
- jwwkit/_resources.py +23 -0
- jwwkit/_spatial_distance.py +95 -0
- jwwkit/_spatial_query.py +34 -0
- jwwkit/_tategu_features.py +201 -0
- jwwkit/_tategu_geometry.py +108 -0
- jwwkit/alignment.py +85 -0
- jwwkit/cad_model.py +5 -0
- jwwkit/cli.py +40 -0
- jwwkit/drawing_comparison.py +430 -0
- jwwkit/edit_result.py +136 -0
- jwwkit/geometry_view.py +141 -0
- jwwkit/guides/AGENTS.md +47 -0
- jwwkit/guides/docs/api_list.md +1944 -0
- jwwkit/guides/docs/common_edit_result_coverage.md +40 -0
- jwwkit/guides/docs/drawing_comparison.md +79 -0
- jwwkit/guides/docs/edit_results.md +59 -0
- jwwkit/guides/docs/geometry_view.md +64 -0
- jwwkit/guides/docs/jwf_key_catalog.md +244 -0
- jwwkit/guides/docs/jwf_readonly.md +76 -0
- jwwkit/guides/docs/plan_api_coverage.md +75 -0
- jwwkit/guides/docs/read_view.md +89 -0
- jwwkit/guides/docs/reading_to_editing.md +100 -0
- jwwkit/guides/docs/schema_normalization.md +64 -0
- jwwkit/guides/docs/search_queries.md +95 -0
- jwwkit/guides/docs/spatial_search.md +77 -0
- jwwkit/guides/docs/tategu_recognition.md +40 -0
- jwwkit/guides/docs/text_equivalence.md +42 -0
- jwwkit/guides/docs/usage_guide.md +604 -0
- jwwkit/guides/docs/workflow_settings.md +95 -0
- jwwkit/hatch_geometry.py +77 -0
- jwwkit/jwf.py +202 -0
- jwwkit/legacy_document.py +2 -0
- jwwkit/line_document.py +14 -0
- jwwkit/model/__init__.py +1 -0
- jwwkit/model/base.py +15 -0
- jwwkit/model/block_definition.py +32 -0
- jwwkit/model/block_names.py +41 -0
- jwwkit/model/block_operations.py +154 -0
- jwwkit/model/block_reference.py +39 -0
- jwwkit/model/circle.py +44 -0
- jwwkit/model/circular_solid.py +72 -0
- jwwkit/model/common_entities.py +34 -0
- jwwkit/model/dimension.py +49 -0
- jwwkit/model/display_state.py +46 -0
- jwwkit/model/document.py +585 -0
- jwwkit/model/editing.py +270 -0
- jwwkit/model/group_audit.py +91 -0
- jwwkit/model/group_import.py +249 -0
- jwwkit/model/line.py +31 -0
- jwwkit/model/point.py +41 -0
- jwwkit/model/preflight.py +120 -0
- jwwkit/model/preset_rigid.py +42 -0
- jwwkit/model/query.py +270 -0
- jwwkit/model/query_fields.py +189 -0
- jwwkit/model/query_regex_worker.py +20 -0
- jwwkit/model/query_text.py +116 -0
- jwwkit/model/rigid_audit.py +66 -0
- jwwkit/model/solid.py +51 -0
- jwwkit/model/spatial.py +99 -0
- jwwkit/model/summary.py +93 -0
- jwwkit/model/text.py +49 -0
- jwwkit/model/text_metrics.py +78 -0
- jwwkit/model/transformations.py +176 -0
- jwwkit/normalization.py +287 -0
- jwwkit/read_view.py +298 -0
- jwwkit/reader/__init__.py +1 -0
- jwwkit/reader/archive.py +299 -0
- jwwkit/reader/images.py +48 -0
- jwwkit/reader/jws.py +175 -0
- jwwkit/reader/jws_placement.py +78 -0
- jwwkit/reader/sxf.py +51 -0
- jwwkit/reader/template_names.py +22 -0
- jwwkit/reader/text_presets.py +23 -0
- jwwkit/record_reader.py +2 -0
- jwwkit/replacement_workflow.py +39 -0
- jwwkit/resources/README.md +7 -0
- jwwkit/resources/empty_template.jww +0 -0
- jwwkit/resources/halfwidth_kana_composition_v1.json +35 -0
- jwwkit/resources/header.json +3 -0
- jwwkit/resources/jwf_schema.json +925 -0
- jwwkit/resources/jww_workflow.example.toml +14 -0
- jwwkit/resources/text_equivalence_v1.json +1779 -0
- jwwkit/safe_edit.py +454 -0
- jwwkit/spatial_search.py +144 -0
- jwwkit/tategu_recognition.py +181 -0
- jwwkit/workflow_settings.py +140 -0
- jwwkit/writer/__init__.py +1 -0
- jwwkit/writer/header.py +104 -0
- jwwkit/writer/legacy_document.py +62 -0
- jwwkit/writer/mfc.py +110 -0
- jwwkit/writer/preserved.py +265 -0
- jwwkit/writer/strings.py +19 -0
- jwwkit/writer/template_constants.py +9 -0
- jwwkit/writer/template_document.py +390 -0
- jwwkit-0.2.0.dist-info/METADATA +174 -0
- jwwkit-0.2.0.dist-info/RECORD +117 -0
- jwwkit-0.2.0.dist-info/WHEEL +5 -0
- jwwkit-0.2.0.dist-info/entry_points.txt +2 -0
- jwwkit-0.2.0.dist-info/licenses/LICENSE +21 -0
- jwwkit-0.2.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +51 -0
- jwwkit-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Read-only planar candidates. No answer/reference input is accepted."""
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from jwwkit.tategu_recognition import recognize_planar_tategu
|
|
5
|
+
|
|
6
|
+
def add_arguments(parser):
|
|
7
|
+
parser.add_argument('--source',required=True,type=Path)
|
|
8
|
+
parser.add_argument('--tolerances',required=True,type=Path)
|
|
9
|
+
parser.add_argument('--output',required=True,type=Path)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run(args):
|
|
13
|
+
if args.output.exists():raise FileExistsError(args.output)
|
|
14
|
+
if args.output.resolve() in (args.source.resolve(),args.tolerances.resolve()):
|
|
15
|
+
raise ValueError('Output conflicts with input')
|
|
16
|
+
result=recognize_planar_tategu(args.source,tolerances=json.loads(args.tolerances.read_text(encoding='utf-8')))
|
|
17
|
+
with args.output.open('x',encoding='utf-8')as stream:
|
|
18
|
+
json.dump(result,stream,ensure_ascii=False,allow_nan=False,indent=2)
|
|
19
|
+
print(json.dumps({'candidates':len(result['recognized_candidates']),'complete':result['complete']}))
|
|
20
|
+
return 0 if result['complete']else 2
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Write a JWW JSON summary without modifying the input or overwriting output."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from jwwkit import CadDocument
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def add_arguments(parser):
|
|
8
|
+
parser.add_argument("--source", type=Path, required=True)
|
|
9
|
+
parser.add_argument("--output", type=Path, required=True)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run(args):
|
|
13
|
+
if args.output.exists():
|
|
14
|
+
raise FileExistsError(args.output)
|
|
15
|
+
result = CadDocument.open(args.source).write_summary(args.output)
|
|
16
|
+
print(f"entities={result['entity_count']}, definitions={len(result['block_definitions'])}, output={args.output}")
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Internal read-only compound comparison. No writer/transform permissions added."""
|
|
2
|
+
import copy
|
|
3
|
+
import math
|
|
4
|
+
from collections import defaultdict,Counter
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def enrich(doc,row,scale,transform,point,children,stack):
|
|
8
|
+
from jwwkit.drawing_comparison import _finite,_json
|
|
9
|
+
g=row['geometry_paper_mm'];kind=row['type'];base=row['attributes'];angle=transform[0]
|
|
10
|
+
if kind=='DIMENSION':
|
|
11
|
+
if g['sxf_mode']!=0:raise ValueError('Dimension mode other than observed mode0 is not semantically verified')
|
|
12
|
+
records=[g['line'],g['text'],*g['aux_lines'],*g['aux_points']]
|
|
13
|
+
paths=['line','text',*[f'aux_lines[{i}]' for i in range(len(g['aux_lines']))],*[f'aux_points[{i}]' for i in range(len(g['aux_points']))]]
|
|
14
|
+
row['children']=children(records,scale)
|
|
15
|
+
for path,child in zip(paths,row['children']):child['child_path']=path
|
|
16
|
+
row['active_child_paths']=['line','text']
|
|
17
|
+
if any(c['status']!='supported' for c in row['children']):raise ValueError('Unsupported dimension child: '+str([(c['child_path'],c.get('reason')) for c in row['children'] if c['status']!='supported']))
|
|
18
|
+
anchor=row['children'][0]['anchor']
|
|
19
|
+
return [anchor],[],[],anchor,dict(sxf_mode=g['sxf_mode']),None
|
|
20
|
+
if kind=='BLOCK':
|
|
21
|
+
style=base['pen_style']
|
|
22
|
+
if style not in (0,1,2,3,64,65,66,67) or base['pen_width']!=0 or base['flag']!=0:
|
|
23
|
+
raise ValueError('Unverified block reference layer-priority attributes')
|
|
24
|
+
number=g['def_number']
|
|
25
|
+
if number in stack:raise ValueError('Cyclic block definition dependency')
|
|
26
|
+
if len(stack)>=64:raise ValueError('Block dependency depth exceeds 64')
|
|
27
|
+
definitions=[d for d in doc._source_records['block_defs'] if d['number']==number]
|
|
28
|
+
if len(definitions)!=1:raise ValueError('Missing or duplicate block definition number')
|
|
29
|
+
definition=definitions[0]
|
|
30
|
+
# Definition coordinates are paper-mm local coordinates, independent
|
|
31
|
+
# of each child's saved layer-group scale. Structure is not flattened.
|
|
32
|
+
row['children']=children(definition['entities'],1.,(0.,0.,0.),(*stack,number))
|
|
33
|
+
for i,child in enumerate(row['children']):child['child_path']=f'definition.entities[{i}]'
|
|
34
|
+
if any(c['status']!='supported' for c in row['children']):raise ValueError('Unsupported definition child: '+str([(c['child_path'],c.get('reason')) for c in row['children'] if c['status']!='supported']))
|
|
35
|
+
row['definition_storage']=copy.deepcopy(definition)
|
|
36
|
+
row['definition_identity']=dict(number=number,name=definition['name'])
|
|
37
|
+
pos=point(g['ref_x'],g['ref_y']);rotation=(_finite(g['rotation'],'block rotation')+math.radians(angle))%(2*math.pi)
|
|
38
|
+
sx,sy=(_finite(g[k],k) for k in ('scale_x','scale_y'))
|
|
39
|
+
if sx==0 or sy==0:raise ValueError('Degenerate zero block scale')
|
|
40
|
+
attrs=dict(layer_priority=bool(style&64),saved_scale_x=sx,saved_scale_y=sy,
|
|
41
|
+
definition=dict(name=definition['name'],base=copy.deepcopy(definition['base'])))
|
|
42
|
+
return [pos],[sx*scale,sy*scale],[(rotation,2*math.pi)],pos,attrs,None
|
|
43
|
+
if kind=='SOLID':
|
|
44
|
+
pts=[point(g[f'point{i}_x'],g[f'point{i}_y']) for i in (1,3,2,4)]
|
|
45
|
+
# Native perimeter slot order is 1,3,2,4. Keep the raw slots in JSON.
|
|
46
|
+
clean=[]
|
|
47
|
+
for p in pts:
|
|
48
|
+
if not clean or p!=clean[-1]:clean.append(p)
|
|
49
|
+
if len(clean)>1 and clean[0]==clean[-1]:clean.pop()
|
|
50
|
+
pts=min((seq[n:]+seq[:n] for seq in (clean,list(reversed(clean))) for n in range(len(clean))),key=_json)
|
|
51
|
+
anchor=[sum(p[i] for p in pts)/len(pts) for i in (0,1)]
|
|
52
|
+
return pts,[],[],anchor,dict(explicit_color=g['color']),None
|
|
53
|
+
if kind!='CIRCLE_SOLID':raise ValueError('Unknown compound type')
|
|
54
|
+
from jwwkit.model.circular_solid import validate_circular_solid
|
|
55
|
+
validate_circular_solid(row['raw_record'])
|
|
56
|
+
center=point(g['center_x'],g['center_y']);radius=_finite(g['radius'],'radius')*scale
|
|
57
|
+
flat=_finite(g['flatness'],'flatness');tilt=_finite(g['tilt_angle'],'tilt')+math.radians(angle)
|
|
58
|
+
a,b=max(radius,radius*flat),min(radius,radius*flat)
|
|
59
|
+
orientation=0. if a==b else (tilt+(math.pi/2 if flat>1 else 0))%math.pi
|
|
60
|
+
pts=[center];lengths=[a,b];angles=[(orientation,math.pi)]
|
|
61
|
+
attrs=dict(explicit_color=g['color'],fill_representation='ring' if base['pen_style']==105 else g['solid_mode'])
|
|
62
|
+
if base['pen_style']==105:lengths.append(g['solid_mode']*scale)
|
|
63
|
+
elif g['solid_mode']==5.:
|
|
64
|
+
start,sweep=g['start_angle'],g['arc_angle']
|
|
65
|
+
def on(t):
|
|
66
|
+
x,y=radius*math.cos(t),radius*flat*math.sin(t)
|
|
67
|
+
return [center[0]+math.cos(tilt)*x-math.sin(tilt)*y,center[1]+math.sin(tilt)*x+math.cos(tilt)*y]
|
|
68
|
+
pts+=sorted([on(start),on(start+sweep)])+[on(start+sweep/2)]
|
|
69
|
+
angles.append((abs(sweep),0))
|
|
70
|
+
return pts,lengths,angles,center,attrs,None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def semantic_key(row):
|
|
74
|
+
"""Canonical semantic multiset; never include archive numbers/dates/order."""
|
|
75
|
+
from jwwkit.drawing_comparison import _json
|
|
76
|
+
children=row.get('children',[])
|
|
77
|
+
if row['type']=='DIMENSION':
|
|
78
|
+
# mode0 auxiliary positions are unused, but their attributes matter.
|
|
79
|
+
nested=[[c['child_path'],semantic_key(c) if c['child_path'] in row['active_child_paths'] else [c['type'],c['comparison_attributes'],c['content']]] for c in children]
|
|
80
|
+
else:nested=sorted([semantic_key(c) for c in children],key=_json)
|
|
81
|
+
return [row['type'],row['comparison_geometry'],row['comparison_attributes'],row['content'],nested]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def child_correspondence(a,b,pt,at):
|
|
85
|
+
from jwwkit.drawing_comparison import _geometry,_signature,_json
|
|
86
|
+
left,right=a.get('children',[]),b.get('children',[])
|
|
87
|
+
if len(left)!=len(right):return None
|
|
88
|
+
if a['type']=='DIMENSION':
|
|
89
|
+
pairs=[]
|
|
90
|
+
for i,(x,y) in enumerate(zip(left,right)):
|
|
91
|
+
if x['child_path']!=y['child_path']:return None
|
|
92
|
+
err=_geometry(x,y,pt,at) if x['child_path'] in a['active_child_paths'] else dict(unused_mode0_geometry=True)
|
|
93
|
+
if err is None:return None
|
|
94
|
+
pairs.append(dict(left=i,right=i,errors=err))
|
|
95
|
+
return dict(child_pairs=pairs,nested_ambiguous=any(p['errors'].get('nested_ambiguous',False) for p in pairs))
|
|
96
|
+
la=set(range(len(left)));rb=set(range(len(right)));pairs=[]
|
|
97
|
+
# Exact children first, including duplicates as a multiset, then geometric
|
|
98
|
+
# candidates. This prevents order/number changes from inventing differences.
|
|
99
|
+
ga,gb=defaultdict(list),defaultdict(list)
|
|
100
|
+
for i,r in enumerate(left):ga[_signature(r)].append(i)
|
|
101
|
+
for j,r in enumerate(right):gb[_signature(r)].append(j)
|
|
102
|
+
for key in sorted(ga.keys()&gb.keys()):
|
|
103
|
+
for i,j in zip(ga[key],gb[key]):pairs.append(dict(left=i,right=j,errors=_geometry(left[i],right[j],pt,at)));la.remove(i);rb.remove(j)
|
|
104
|
+
if not la:return dict(child_pairs=pairs,nested_ambiguous=False)
|
|
105
|
+
edges={}
|
|
106
|
+
if len(la)*len(rb)>200000:raise ValueError('Definition child candidate limit exceeded')
|
|
107
|
+
for i in sorted(la):
|
|
108
|
+
for j in sorted(rb):
|
|
109
|
+
err=_geometry(left[i],right[j],pt,at)
|
|
110
|
+
if err is not None:edges[i,j]=err
|
|
111
|
+
ca=Counter(i for i,j in edges);cb=Counter(j for i,j in edges)
|
|
112
|
+
if any(i not in ca for i in la) or any(j not in cb for j in rb):return None
|
|
113
|
+
ambiguous=any(ca[i]!=1 for i in la) or any(cb[j]!=1 for j in rb)
|
|
114
|
+
if ambiguous:return dict(child_pairs=pairs,nested_ambiguous=True)
|
|
115
|
+
for (i,j),err in sorted(edges.items()):pairs.append(dict(left=i,right=j,errors=err))
|
|
116
|
+
return dict(child_pairs=pairs,nested_ambiguous=any(p['errors'].get('nested_ambiguous',False) for p in pairs))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def changes(a,b,errors,size_tolerance,path=''):
|
|
120
|
+
"""Compound members remain children of one counted root. Keep child paths."""
|
|
121
|
+
from jwwkit.drawing_comparison import _differences,_geometry
|
|
122
|
+
attrs=_differences(a['comparison_attributes'],b['comparison_attributes'],path,size_tolerance)
|
|
123
|
+
contents=[];storage=[]
|
|
124
|
+
if a['content']!=b['content']:contents.append(dict(field=path+'.content',before=a['content'],after=b['content']))
|
|
125
|
+
if a['type']=='BLOCK':
|
|
126
|
+
storage=_differences(a['definition_storage'],b['definition_storage'],path+'.definition_storage')
|
|
127
|
+
for p in (errors or {}).get('child_pairs',[]):
|
|
128
|
+
i,j=p['left'],p['right'];x,y=a['children'][i],b['children'][j]
|
|
129
|
+
child_path=path+'.'+x['child_path']
|
|
130
|
+
ac,cc,sc=changes(x,y,p['errors'],size_tolerance,child_path)
|
|
131
|
+
attrs+=ac;contents+=cc;storage+=sc
|
|
132
|
+
return attrs,contents,storage
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def definitions_report(left,right,pt,at,st):
|
|
136
|
+
"""Selected transitive definitions, independently of archive reference IDs."""
|
|
137
|
+
from jwwkit.drawing_comparison import _json,_differences
|
|
138
|
+
def collect(rows):
|
|
139
|
+
found={}
|
|
140
|
+
def visit(r):
|
|
141
|
+
if r['status']!='supported':return
|
|
142
|
+
if r['type']=='BLOCK':found.setdefault(r['definition_identity']['number'],r)
|
|
143
|
+
for c in r.get('children',[]):visit(c)
|
|
144
|
+
for r in rows:visit(r)
|
|
145
|
+
return found
|
|
146
|
+
aa,bb=collect(left),collect(right);la=set(aa);rb=set(bb);pairs=[];ambiguous=[]
|
|
147
|
+
def key(r):return _json([r['comparison_attributes']['definition'],sorted([semantic_key(c) for c in r['children']],key=_json)])
|
|
148
|
+
ga,gb=defaultdict(list),defaultdict(list)
|
|
149
|
+
for n,r in aa.items():ga[key(r)].append(n)
|
|
150
|
+
for n,r in bb.items():gb[key(r)].append(n)
|
|
151
|
+
for k in sorted(ga.keys()&gb.keys()):
|
|
152
|
+
for i,j in zip(sorted(ga[k]),sorted(gb[k])):pairs.append((i,j));la.remove(i);rb.remove(j)
|
|
153
|
+
# A unique saved name allows reporting a changed definition; it never
|
|
154
|
+
# makes changed geometry equal. Duplicate names remain explicit holds.
|
|
155
|
+
names_a,names_b=defaultdict(list),defaultdict(list)
|
|
156
|
+
for i in sorted(la):names_a[aa[i]['definition_identity']['name']].append(i)
|
|
157
|
+
for j in sorted(rb):names_b[bb[j]['definition_identity']['name']].append(j)
|
|
158
|
+
for name in sorted(names_a.keys()&names_b.keys()):
|
|
159
|
+
ids,jds=names_a[name],names_b[name]
|
|
160
|
+
if len(ids)==len(jds)==1:pairs.append((ids[0],jds[0]))
|
|
161
|
+
else:ambiguous.append(dict(name=name,left_numbers=ids,right_numbers=jds))
|
|
162
|
+
la.difference_update(ids);rb.difference_update(jds)
|
|
163
|
+
rows=[]
|
|
164
|
+
for i,j in sorted(pairs):
|
|
165
|
+
a,b=aa[i],bb[j];err=child_correspondence(a,b,pt,at)
|
|
166
|
+
is_ambiguous=bool(err and err.get('nested_ambiguous'))
|
|
167
|
+
if is_ambiguous:ambiguous.append(dict(left_numbers=[i],right_numbers=[j],reason='non_unique_definition_children'))
|
|
168
|
+
attrs=_differences(a['comparison_attributes']['definition'],b['comparison_attributes']['definition'],'.definition',st)
|
|
169
|
+
contents=[]
|
|
170
|
+
if err and not is_ambiguous:
|
|
171
|
+
for p in err['child_pairs']:
|
|
172
|
+
ac,cc,_=changes(a['children'][p['left']],b['children'][p['right']],p['errors'],st,'.definition.entities['+str(p['left'])+']')
|
|
173
|
+
attrs+=ac;contents+=cc
|
|
174
|
+
rows.append(dict(left_number=i,right_number=j,number_changed=i!=j,name_before=a['definition_identity']['name'],name_after=b['definition_identity']['name'],
|
|
175
|
+
geometry_equal=err is not None and not is_ambiguous,attribute_changes=attrs,content_changes=contents,
|
|
176
|
+
semantic_equal=err is not None and not is_ambiguous and not attrs and not contents,
|
|
177
|
+
storage_differences=_differences(a['definition_storage'],b['definition_storage']),
|
|
178
|
+
before=a['definition_storage'],after=b['definition_storage']))
|
|
179
|
+
return dict(matches=rows,left_only=sorted(la),right_only=sorted(rb),ambiguous=ambiguous)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Private atomic adapter for the 31 approved CadDocument methods only."""
|
|
2
|
+
import copy,hashlib,inspect,json
|
|
3
|
+
from dataclasses import is_dataclass,asdict
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from jwwkit.cad_model import CadDocument
|
|
7
|
+
from jwwkit.model.common_entities import CadEntity,CadBlockDefinition
|
|
8
|
+
|
|
9
|
+
NAMES=('delete_entity','delete_entities','move_entities','move_entity','transform_entities','copy_entities',
|
|
10
|
+
'align_two_points','rotate_entities','reflect_entities','scale_entities','set_entity_attributes','set_text',
|
|
11
|
+
'recalculate_text_endpoint','set_group_scale','add_line','add_text','add_circle','add_point','add_solid',
|
|
12
|
+
'add_circle_solid','set_group_state','set_layer_state','set_layer_name','set_group_name','add_block_definition',
|
|
13
|
+
'add_arc','set_dimension_text','place_block','replace_entities_with_block','set_block_layer_priority','import_group')
|
|
14
|
+
METHODS={name:getattr(CadDocument,name)for name in NAMES}
|
|
15
|
+
ADDITIONS={'add_line','add_text','add_circle','add_arc','add_point','add_solid','add_circle_solid','place_block'}
|
|
16
|
+
RESOURCES={'set_group_scale':'group','set_group_name':'group','set_group_state':'group',
|
|
17
|
+
'set_layer_name':'layer','set_layer_state':'layer','add_block_definition':'definition'}
|
|
18
|
+
|
|
19
|
+
def canonical(v):return json.dumps(v,ensure_ascii=True,sort_keys=True,separators=(',',':'),allow_nan=False)
|
|
20
|
+
def digest(v):return hashlib.sha256(canonical(v).encode('ascii')).hexdigest()
|
|
21
|
+
def detached(v):return json.loads(canonical(v))
|
|
22
|
+
def equal(a,b):return canonical(a)==canonical(b)
|
|
23
|
+
|
|
24
|
+
def copying(name,parameters):
|
|
25
|
+
return name=='copy_entities'or name=='transform_entities'and parameters.get('options',{}).get('copy_entities',False)or name in ('rotate_entities','reflect_entities','scale_entities')and parameters.get('options',{}).get('copy',False)
|
|
26
|
+
|
|
27
|
+
def options(limit,mode):
|
|
28
|
+
if type(limit)is not int or not 0<=limit<=1000 or mode not in ('details','summary','count'):
|
|
29
|
+
raise ValueError('limit must be integer 0..1000; mode details/summary/count')
|
|
30
|
+
|
|
31
|
+
def capture(cad):
|
|
32
|
+
from jwwkit.safe_edit import snapshot
|
|
33
|
+
from jwwkit.model.group_import import definition_records
|
|
34
|
+
s=snapshot(cad)
|
|
35
|
+
state=dict(entities=s['entities'],groups=s['groups'],definitions=list(definition_records(cad).values()),
|
|
36
|
+
metadata=cad.metadata_settings,version=cad.version,origin_sha256=hashlib.sha256(cad._source_bytes).hexdigest())
|
|
37
|
+
return detached(state)
|
|
38
|
+
|
|
39
|
+
def _rows(before,after,pairs):
|
|
40
|
+
changes=[];paired_old=set();paired_new=set();counts=dict(added=0,modified=0,deleted=0,rejected=0,unchanged=0)
|
|
41
|
+
for i,j in pairs:
|
|
42
|
+
paired_old.add(i);paired_new.add(j)
|
|
43
|
+
if equal(before['entities'][i],after['entities'][j]):counts['unchanged']+=1
|
|
44
|
+
else:
|
|
45
|
+
counts['modified']+=1;changes.append(dict(resource_type='entity',change='modified',before_index=i,after_index=j,before=before['entities'][i],after=after['entities'][j]))
|
|
46
|
+
for i,e in enumerate(before['entities']):
|
|
47
|
+
if i not in paired_old:counts['deleted']+=1;changes.append(dict(resource_type='entity',change='deleted',before_index=i,after_index=None,before=e,after=None))
|
|
48
|
+
for j,e in enumerate(after['entities']):
|
|
49
|
+
if j not in paired_new:counts['added']+=1;changes.append(dict(resource_type='entity',change='added',before_index=None,after_index=j,before=None,after=e))
|
|
50
|
+
resources=dict(definitions=0,groups=0,layers=0)
|
|
51
|
+
old={x['number']:x for x in before['definitions']};new={x['number']:x for x in after['definitions']}
|
|
52
|
+
for number in sorted(old.keys()|new.keys()):
|
|
53
|
+
a=old.get(number);b=new.get(number)
|
|
54
|
+
if not equal(a,b):resources['definitions']+=1;changes.append(dict(resource_type='definition',number=number,change='added'if a is None else 'deleted'if b is None else 'modified',before=a,after=b))
|
|
55
|
+
for g,(a,b)in enumerate(zip(before['groups'],after['groups'])):
|
|
56
|
+
ga={k:a[k]for k in ('name','scale','state')};gb={k:b[k]for k in ga}
|
|
57
|
+
if not equal(ga,gb):resources['groups']+=1;changes.append(dict(resource_type='group',layer_group=g,change='modified',before=ga,after=gb))
|
|
58
|
+
for l in range(16):
|
|
59
|
+
la=dict(name=a['layers'][l],state=a['layer_states'][l]);lb=dict(name=b['layers'][l],state=b['layer_states'][l])
|
|
60
|
+
if not equal(la,lb):resources['layers']+=1;changes.append(dict(resource_type='layer',layer_group=g,layer=l,change='modified',before=la,after=lb))
|
|
61
|
+
return counts,resources,changes
|
|
62
|
+
|
|
63
|
+
def net_rows(before,after):
|
|
64
|
+
old=[x['source_index']for x in before['entities']]
|
|
65
|
+
if None in old or len(set(old))!=len(old):raise RuntimeError('Net plan result needs fresh unique source indices')
|
|
66
|
+
current={x['source_index']:j for j,x in enumerate(after['entities'])if x['source_index']is not None}
|
|
67
|
+
if len(current)!=sum(x['source_index']is not None for x in after['entities'])or not current.keys()<=set(old):raise RuntimeError('Invalid original lineage after execution')
|
|
68
|
+
return _rows(before,after,[(i,current[k])for i,k in enumerate(old)if k in current])
|
|
69
|
+
|
|
70
|
+
def result(operation,status,phase,before=None,after=None):
|
|
71
|
+
return dict(schema_version=1,operation=operation,status=status,phase=phase,
|
|
72
|
+
counts=dict(added=0,modified=0,deleted=0,rejected=0,unchanged=len(before['entities'])if before else 0),
|
|
73
|
+
resource_counts=dict(definitions=0,groups=0,layers=0),changes=[],rejections=[],resource_rejections=[],warnings=[],warning_count=0,
|
|
74
|
+
rejected_operation_count=0,rejected_addition_count=0,before_model_sha256=digest(before)if before else None,
|
|
75
|
+
after_model_sha256=digest(after or before)if before else None,approval_plan_sha256=None,
|
|
76
|
+
detail_page=dict(total=0,returned=0,truncated=False,has_more=False,continuation_supported=False),operation_data=None)
|
|
77
|
+
|
|
78
|
+
def bounded(items,limit,mode):
|
|
79
|
+
output=[];size=0
|
|
80
|
+
if mode=='details':
|
|
81
|
+
for row in items[:limit]:
|
|
82
|
+
amount=len(canonical(row).encode('ascii'))
|
|
83
|
+
if size+amount>1048576:break
|
|
84
|
+
output.append(detached(row));size+=amount
|
|
85
|
+
return output,dict(total=len(items),returned=len(output),truncated=len(output)<len(items),has_more=len(output)<len(items),continuation_supported=False,max_bytes=1048576)
|
|
86
|
+
|
|
87
|
+
def present(value,limit=100,mode='details'):
|
|
88
|
+
options(limit,mode);v=copy.deepcopy(value);v['changes'],v['detail_page']=bounded(v['changes'],limit,mode)
|
|
89
|
+
v['error_summary']=None
|
|
90
|
+
if v['rejections']:
|
|
91
|
+
first=v['rejections'][0];message=first.get('message','')
|
|
92
|
+
v['error_summary']=dict(code=first.get('code'),exception_type=first.get('exception_type'),message=message[:4096],message_truncated=len(message)>4096)
|
|
93
|
+
v['warning_codes']=sorted({x['code']for x in v['warnings']})
|
|
94
|
+
for key in ('warnings','rejections','resource_rejections'):
|
|
95
|
+
rows=v[key];v[key],page=bounded(rows,limit,mode);v[key+'_details']=page
|
|
96
|
+
if mode!='details':v['operation_data']=None
|
|
97
|
+
return v
|
|
98
|
+
|
|
99
|
+
def data(value):
|
|
100
|
+
from jwwkit.safe_edit import entity_view
|
|
101
|
+
if isinstance(value,CadEntity):return dict(entity_view(value),source_index=value.source_index)
|
|
102
|
+
if isinstance(value,CadBlockDefinition):return dict(number=value.number,name=value.name,attributes=dict(value.attributes),entities=[data(e)for e in value.entities])
|
|
103
|
+
from jwwkit.model.block_operations import RecordChild
|
|
104
|
+
if isinstance(value,RecordChild):return dict(record=data(value.value))
|
|
105
|
+
if is_dataclass(value):return dict(model_type=type(value).__name__,values=data(asdict(value)))
|
|
106
|
+
if isinstance(value,Path):return str(value)
|
|
107
|
+
if isinstance(value,(list,tuple)):return [data(x)for x in value]
|
|
108
|
+
if isinstance(value,dict):return {str(k):data(v)for k,v in value.items()}
|
|
109
|
+
if value is None or type(value)in (str,int,float,bool):return value
|
|
110
|
+
raise TypeError('Unrepresentable operation return type: '+type(value).__name__)
|
|
111
|
+
|
|
112
|
+
class Rejected(Exception):
|
|
113
|
+
def __init__(self,record):self.record=record;super().__init__(record['rejections'][0]['message'])
|
|
114
|
+
|
|
115
|
+
class Failed(Exception):
|
|
116
|
+
def __init__(self,record):self.record=record;super().__init__(record['rejections'][0]['message'])
|
|
117
|
+
|
|
118
|
+
def refusal(name,before,bound,exc,code='EDIT_REFUSED'):
|
|
119
|
+
r=result(name,'rejected','memory',before);r['rejected_operation_count']=1
|
|
120
|
+
args=bound.arguments if bound else {};targets=[args['index']]if 'index'in args else args.get('indices')
|
|
121
|
+
valid=isinstance(targets,(list,tuple))and all(type(i)is int and 0<=i<len(before['entities'])for i in targets)and len(set(targets))==len(targets)
|
|
122
|
+
r['counts']['rejected']=len(targets)if valid else 0 if name in ADDITIONS or name in RESOURCES else None
|
|
123
|
+
r['rejected_addition_count']=1 if name in ADDITIONS else len(targets)if copying(name,args)and valid else None if name=='import_group'else 0
|
|
124
|
+
if name in RESOURCES:
|
|
125
|
+
try:request={k:data(v)for k,v in args.items()if k!='self'}
|
|
126
|
+
except TypeError:request=dict(unrepresentable_input=True)
|
|
127
|
+
r['resource_rejections']=[dict(resource_type=RESOURCES[name],request=request)]
|
|
128
|
+
r['rejections']=[dict(code=code,exception_type=type(exc).__name__,message=str(exc),targets=targets if valid else None)]
|
|
129
|
+
return r
|
|
130
|
+
|
|
131
|
+
def apply_method(cad,name,/,*args,**kwargs):
|
|
132
|
+
"""Private: called only inside the approved plan engine or its dry run."""
|
|
133
|
+
before=capture(cad);bound=None
|
|
134
|
+
if type(cad)is not CadDocument or name not in METHODS:raise Rejected(refusal(name,before,None,ValueError('PLAN_OPERATION_UNSUPPORTED'),'PLAN_OPERATION_UNSUPPORTED'))
|
|
135
|
+
staged=copy.deepcopy(cad);args=copy.deepcopy(args);kwargs=copy.deepcopy(kwargs)
|
|
136
|
+
try:
|
|
137
|
+
try:
|
|
138
|
+
bound=inspect.signature(METHODS[name]).bind(staged,*args,**kwargs)
|
|
139
|
+
bound=SimpleNamespace(arguments=copy.deepcopy({k:v for k,v in bound.arguments.items()if k!='self'}))
|
|
140
|
+
except TypeError as exc:raise Rejected(refusal(name,before,None,exc,'INVALID_ARGUMENTS'))from exc
|
|
141
|
+
try:
|
|
142
|
+
returned=METHODS[name](staged,*args,**kwargs)
|
|
143
|
+
if name=='align_two_points'and not returned['passed']:raise ValueError('; '.join(returned['reasons']))
|
|
144
|
+
except (ValueError,TypeError,IndexError)as exc:raise Rejected(refusal(name,before,bound,exc))from exc
|
|
145
|
+
try:
|
|
146
|
+
from jwwkit.writer.preserved import _document_bytes
|
|
147
|
+
_document_bytes(staged)
|
|
148
|
+
except (ValueError,TypeError,IndexError)as exc:raise Rejected(refusal(name,before,bound,exc,'SAVE_PREFLIGHT_REFUSED'))from exc
|
|
149
|
+
after=capture(staged);n=len(cad.entities);m=len(staged.entities);deleted=set()
|
|
150
|
+
if name=='delete_entity':deleted={bound.arguments['index']}
|
|
151
|
+
elif name in ('delete_entities','replace_entities_with_block'):
|
|
152
|
+
deleted=set(bound.arguments['indices'])
|
|
153
|
+
if name=='replace_entities_with_block':deleted.remove(min(deleted))
|
|
154
|
+
pairs=[(i,j)for j,i in enumerate(i for i in range(n)if i not in deleted)]
|
|
155
|
+
expected=n+1 if name in ADDITIONS else n+len(bound.arguments['indices'])if copying(name,bound.arguments) else n+returned['added_count']if name=='import_group'else n-len(deleted)
|
|
156
|
+
if m!=expected:raise RuntimeError('Unexpected root population after method')
|
|
157
|
+
counts,resources,changes=_rows(before,after,pairs)
|
|
158
|
+
r=result(name,'applied'if changes else 'unchanged','memory',before,after)
|
|
159
|
+
r.update(counts=counts,resource_counts=resources,changes=changes,operation_data=data(returned))
|
|
160
|
+
if name=='set_group_scale'and resources['groups']:r['warnings'].append(dict(code='GROUP_SCALE_PAPER_FIXED',message='Paper coordinates remain fixed; real coordinates change with group scale.'))
|
|
161
|
+
if name=='import_group'and bound.arguments.get('settings_policy')=='destination':r['warnings'].append(dict(code='DESTINATION_RENDERING_SETTINGS',message='Imported attribute codes retain their values; appearance follows destination settings.'))
|
|
162
|
+
if name=='replace_entities_with_block'and any(before['entities'][i]['record']['attributes']['flag']&2 for i in bound.arguments['indices']):r['warnings'].append(dict(code='BLOCK_FLAG2_NORMALIZATION',message='Only observed definition-child flag2 normalization follows Jw_cad control.'))
|
|
163
|
+
r['warning_count']=len(r['warnings'])
|
|
164
|
+
# Preserve identities of surviving unchanged roots for existing aliases.
|
|
165
|
+
roots=list(staged.entities)
|
|
166
|
+
for i,j in pairs:
|
|
167
|
+
if equal(before['entities'][i],after['entities'][j]):roots[j]=cad.entities[i]
|
|
168
|
+
staged.entities=tuple(roots)
|
|
169
|
+
completed=(r,returned)
|
|
170
|
+
cad.__dict__=staged.__dict__
|
|
171
|
+
return completed
|
|
172
|
+
except Rejected:raise
|
|
173
|
+
except Exception as exc:
|
|
174
|
+
r=result(name,'failed','memory',before);r['rejections']=[dict(code='EDIT_EXECUTION_FAILED',exception_type=type(exc).__name__,message=str(exc))]
|
|
175
|
+
raise Failed(r)from exc
|
jwwkit/_ezjww_adapter.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Internal boundary for the required ezjww 0.2.13 reader.
|
|
2
|
+
|
|
3
|
+
This is a transparent adapter, not a new parser or validation policy.
|
|
4
|
+
See docs/ezjww_adapter_contract.md for the consumed result contract.
|
|
5
|
+
Keep the import eager: making the dependency optional belongs to a later design.
|
|
6
|
+
"""
|
|
7
|
+
import ezjww as _ezjww
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def read_native_document(path):
|
|
11
|
+
"""Delegate once; preserve the native result object and exception unchanged."""
|
|
12
|
+
return _ezjww.read_document(str(path))
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Bounded logical geometry of qualified JWW storage, not rendered font/stroke ink."""
|
|
2
|
+
import math
|
|
3
|
+
from jwwkit._geometry_math import (TAU,point,compose,block_matrix,bounds,union,arc_point,arc_bounds,polygon)
|
|
4
|
+
from jwwkit.model.text_metrics import text_endpoint
|
|
5
|
+
from jwwkit.model.circular_solid import validate_circular_solid
|
|
6
|
+
|
|
7
|
+
FIELDS=('bbox','segments','contours','center','representative_points','text_frame','glyph_bbox')
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def item(status,value=None,reason=None,method=None):
|
|
11
|
+
return dict(status=status,value=value,reason=reason,method=method)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def exact(value,method):return item('exact',value,method=method)
|
|
15
|
+
def unknown(reason):return item('undetermined',reason=str(reason))
|
|
16
|
+
def na(reason='No unique applicable quantity'):return item('not_applicable',reason=reason)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def blank():return {k:item('not_requested') for k in FIELDS}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class GeometryLimit(ValueError):pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Engine:
|
|
26
|
+
def __init__(self,definitions,presets,fields,text_mode,tolerance,limits,*,real_per_unit=None):
|
|
27
|
+
self.definitions=definitions;self.presets=presets;self.fields=fields;self.text_mode=text_mode
|
|
28
|
+
self.tolerance=tolerance;self.limits=limits;self.nodes=0;self.segment_count=0
|
|
29
|
+
self.real_per_unit=real_per_unit
|
|
30
|
+
|
|
31
|
+
def segments(self,segments,approximate=False):
|
|
32
|
+
self.segment_count+=len(segments)
|
|
33
|
+
if self.segment_count>self.limits['max_segments']:return unknown('Segment limit exceeded; no silently truncated geometry')
|
|
34
|
+
return item('approximate' if approximate else 'exact',segments,method='bounded_sagitta' if approximate else 'stored_line_segments')
|
|
35
|
+
|
|
36
|
+
def curve_segments(self,center,radius,start,sweep,closed=False):
|
|
37
|
+
if self.tolerance is None:return unknown('Explicit curve_tolerance is required for curve line approximation')
|
|
38
|
+
step=min(math.pi,4*math.asin(math.sqrt(min(1.,self.tolerance/(2*radius)))))
|
|
39
|
+
if step<=0:return unknown('Requested curve tolerance is below representable resolution')
|
|
40
|
+
n=max(4 if closed else 1,math.ceil(abs(sweep)/step))
|
|
41
|
+
if n+self.segment_count>self.limits['max_segments']:return unknown('Segment limit exceeded; analytic contour and bbox remain available')
|
|
42
|
+
pts=[arc_point(center,radius,start+sweep*i/n) for i in range(n+1)]
|
|
43
|
+
if closed:pts[-1]=pts[0]
|
|
44
|
+
result=self.segments([dict(start=a,end=b)for a,b in zip(pts,pts[1:])],True)
|
|
45
|
+
result['max_deviation']=2*radius*math.sin(abs(sweep)/(4*n))**2
|
|
46
|
+
result['requested_max_deviation']=self.tolerance
|
|
47
|
+
return result
|
|
48
|
+
|
|
49
|
+
def text_frame(self,r,matrix,similarity):
|
|
50
|
+
try:
|
|
51
|
+
if not similarity:raise ValueError('Nonuniform block text-frame rendering is unverified')
|
|
52
|
+
base=r['base']
|
|
53
|
+
if base['flag']!=0 or base['pen_width']!=0:raise ValueError('Special/flagged/dimension text frame is unverified')
|
|
54
|
+
if type(r['text_type'])is not int or not 0<=r['text_type']<=10:raise ValueError('Unverified text preset')
|
|
55
|
+
sizes={k:r[k]for k in ('size_x','size_y','spacing')}
|
|
56
|
+
if r['text_type']:
|
|
57
|
+
if self.presets.get('status')!='exact':raise ValueError('Text preset table unverified')
|
|
58
|
+
preset=self.presets['values'][r['text_type']-1]
|
|
59
|
+
# Ordinary frames require stored and declared sizes to agree;
|
|
60
|
+
# do not silently resolve contradictory source representations.
|
|
61
|
+
if any(r[k]!=preset[k]for k in sizes):raise ValueError('Stored/preset text sizes disagree')
|
|
62
|
+
expected=text_endpoint((r['start_x'],r['start_y']),r['content'],**sizes,
|
|
63
|
+
angle=r['angle'],font_name=r['font_name'],alignment=base['pen_style'])
|
|
64
|
+
saved=(r['end_x'],r['end_y'])
|
|
65
|
+
# Numerical verification only, not an application/drawing tolerance.
|
|
66
|
+
magnitude=max(1.,abs(r['start_x']),abs(r['start_y']),*map(abs,saved),*map(abs,expected))
|
|
67
|
+
length=math.dist((r['start_x'],r['start_y']),expected)
|
|
68
|
+
numeric_guard=max(64*math.ulp(magnitude),1e-9*length)
|
|
69
|
+
real_scale=math.hypot(matrix[0],matrix[2])*self.real_per_unit if self.real_per_unit is not None else None
|
|
70
|
+
if real_scale is None or not math.isfinite(real_scale)or real_scale<=0:
|
|
71
|
+
raise ValueError('Real-coordinate conversion for text roundoff guard is unverified')
|
|
72
|
+
real_guard=numeric_guard*real_scale
|
|
73
|
+
if not math.isfinite(real_guard)or real_guard>=1e-4:
|
|
74
|
+
raise ValueError('Text roundoff guard reaches the real-mm safety cap')
|
|
75
|
+
if math.dist(saved,expected)>numeric_guard:raise ValueError('Saved endpoint does not match the verified ordinary-text rule')
|
|
76
|
+
angle=math.radians(r['angle']);h=r['size_y'];nx,ny=-h*math.sin(angle),h*math.cos(angle)
|
|
77
|
+
corners=[point(matrix,r['start_x'],r['start_y']),point(matrix,*saved),
|
|
78
|
+
point(matrix,saved[0]+nx,saved[1]+ny),point(matrix,r['start_x']+nx,r['start_y']+ny)]
|
|
79
|
+
return exact(dict(kind='text_frame',corners=corners,bbox=bounds(corners),
|
|
80
|
+
is_glyph_outline=False,baseline=[corners[0],corners[1]],
|
|
81
|
+
endpoint_residual_paper_mm=math.dist(saved,expected),numeric_guard_paper_mm=numeric_guard,
|
|
82
|
+
numeric_guard_real_mm=real_guard,numeric_guard_rule='ordinary-endpoint-roundoff-2'),
|
|
83
|
+
'saved_start_end_height_angle_with_verified_ordinary_endpoint')
|
|
84
|
+
except (ValueError,KeyError,TypeError,OverflowError)as exc:return unknown(exc)
|
|
85
|
+
|
|
86
|
+
def build(self,r,matrix,similarity=True,stack=(),depth=0,path=()):
|
|
87
|
+
self.nodes+=1
|
|
88
|
+
if self.nodes>self.limits['max_children']or depth>self.limits['max_depth']:raise GeometryLimit('Geometry expansion/depth limit exceeded')
|
|
89
|
+
out=blank();out.update(kind=r['type'].lower(),child_path=list(path),parts=[],partial_bbox=None)
|
|
90
|
+
for field in ('text_frame','glyph_bbox'):out[field]=na()
|
|
91
|
+
try:
|
|
92
|
+
kind=r['type']
|
|
93
|
+
if kind in ('BLOCK','DIMENSION'):
|
|
94
|
+
out['center']=na('Composite has no unique geometric center');out['text_frame']=na('Inspect text parts for their individual frames')
|
|
95
|
+
parts=[]
|
|
96
|
+
if kind=='BLOCK':
|
|
97
|
+
base=r['base']
|
|
98
|
+
if base['pen_style']not in (0,1,2,3,64,65,66,67)or base['flag']!=0 or base['pen_width']!=0:raise ValueError('Unverified block reference attributes')
|
|
99
|
+
number=r['def_number']
|
|
100
|
+
if number in stack:raise ValueError('Cyclic block definition')
|
|
101
|
+
if number not in self.definitions:raise ValueError('Missing block definition')
|
|
102
|
+
local,uniform=block_matrix(r);child_matrix=compose(matrix,local)
|
|
103
|
+
parts=[self.build(child,child_matrix,similarity and uniform,stack+(number,),depth+1,path+('definition',i)) for i,child in enumerate(self.definitions[number]['entities'])]
|
|
104
|
+
anchor=point(matrix,r['ref_x'],r['ref_y'])
|
|
105
|
+
out['representative_points']=exact([dict(point=anchor,point_kind='block_insertion',on_geometry=None)],'stored_block_anchor')
|
|
106
|
+
else:
|
|
107
|
+
if r['sxf_mode']!=0:raise ValueError('Only verified mode0 dimension geometry is supported')
|
|
108
|
+
# mode0 auxiliaries retain their storage but are not drawn.
|
|
109
|
+
parts=[self.build(r[key],matrix,similarity,stack,depth+1,path+(key,)) for key in ('line','text')]
|
|
110
|
+
out['ignored_storage_parts']=['aux_lines','aux_points']
|
|
111
|
+
out['representative_points']=exact([dict(point=point(matrix,r['line']['start_x'],r['line']['start_y']),point_kind='dimension_line_start',on_geometry=True)],'active_dimension_line')
|
|
112
|
+
out['parts']=parts
|
|
113
|
+
if not parts:raise ValueError('Empty composite has no geometry')
|
|
114
|
+
boxes=[p['bbox']['value']for p in parts if p['bbox']['status']=='exact']
|
|
115
|
+
out['partial_bbox']=union(boxes)if boxes else None
|
|
116
|
+
out['bbox']=exact(union(boxes),'complete_child_union')if len(boxes)==len(parts)else unknown('A child bbox is undetermined; partial_bbox is not the whole extent')
|
|
117
|
+
for field in ('contours','segments'):
|
|
118
|
+
statuses=[p[field]['status']for p in parts]
|
|
119
|
+
if all(s in ('exact','approximate','not_applicable')for s in statuses):
|
|
120
|
+
values=[x for p in parts for x in (p[field]['value']or [])]
|
|
121
|
+
out[field]=item('approximate'if 'approximate'in statuses else 'exact',values,method='complete_child_union')
|
|
122
|
+
else:out[field]=unknown('A child '+field+' is undetermined')
|
|
123
|
+
return out
|
|
124
|
+
if kind=='LINE':
|
|
125
|
+
a,b=(point(matrix,r[p+'_x'],r[p+'_y'])for p in ('start','end'))
|
|
126
|
+
out['bbox']=exact(bounds([a,b]),'segment_endpoints')
|
|
127
|
+
out['center']=exact(dict(point=[a[i]/2+b[i]/2 for i in (0,1)],center_kind='segment_midpoint'),'segment_midpoint')
|
|
128
|
+
out['segments']=self.segments([dict(start=a,end=b)])
|
|
129
|
+
out['contours']=exact([dict(kind='polyline',points=[a,b],closed=False,role='open')],'stored_line')
|
|
130
|
+
out['representative_points']=exact([dict(point=p,point_kind=k,on_geometry=True)for p,k in ((a,'start'),(b,'end'),(out['center']['value']['point'],'midpoint'))],'segment_points')
|
|
131
|
+
elif kind=='POINT':
|
|
132
|
+
p=point(matrix,r['x'],r['y']);out['bbox']=exact(bounds([p]),'geometric_point_not_marker_glyph')
|
|
133
|
+
out['center']=exact(dict(point=p,center_kind='point'),'stored_point')
|
|
134
|
+
out['segments']=na('A point has no line segment');out['contours']=exact([dict(kind='point',point=p)],'stored_point')
|
|
135
|
+
out['representative_points']=exact([dict(point=p,point_kind='point',on_geometry=True)],'stored_point')
|
|
136
|
+
out['marker_glyph']=unknown('Point marker glyph/stroke extent is outside geometric-point scope')
|
|
137
|
+
elif kind=='TEXT':
|
|
138
|
+
frame=self.text_frame(r,matrix,similarity);out['text_frame']=frame
|
|
139
|
+
out['glyph_bbox']=unknown('Rendered font glyph outline is not inferred from stored text frame')
|
|
140
|
+
a,b=point(matrix,r['start_x'],r['start_y']),point(matrix,r['end_x'],r['end_y'])
|
|
141
|
+
out['representative_points']=exact([dict(point=p,point_kind=k,on_geometry=None)for p,k in ((a,'text_start'),(b,'text_end'))],'stored_text_anchors_not_glyph_points')
|
|
142
|
+
if self.text_mode=='frame'and frame['status']=='exact':
|
|
143
|
+
pts=frame['value']['corners'];out['bbox']=exact(bounds(pts),'text_frame_not_glyph')
|
|
144
|
+
out['contours']=exact([dict(kind='polyline',points=pts,closed=True,role='text_frame')],'logical_text_frame')
|
|
145
|
+
out['segments']=self.segments([dict(start=a,end=b)for a,b in zip(pts,pts[1:]+pts[:1])])
|
|
146
|
+
out['center']=exact(dict(point=[sum(p[i]/4 for p in pts)for i in (0,1)],center_kind='text_frame_center_not_glyph'),'logical_text_frame')
|
|
147
|
+
else:
|
|
148
|
+
reason=frame['reason']if self.text_mode=='frame'else 'Glyph geometry is undetermined; choose text_mode=frame for the separate logical frame'
|
|
149
|
+
for field in ('bbox','contours','segments','center'):out[field]=unknown(reason)
|
|
150
|
+
elif kind=='SOLID':
|
|
151
|
+
pts,centroid,orientation=polygon([point(matrix,r[f'point{i}_x'],r[f'point{i}_y'])for i in (1,3,2,4)])
|
|
152
|
+
out['bbox']=exact(bounds(pts),'qualified_perimeter_vertices')
|
|
153
|
+
out['contours']=exact([dict(kind='polyline',points=pts,closed=True,role='outer',orientation=orientation,filled=True)],'simple_solid_polygon')
|
|
154
|
+
out['segments']=self.segments([dict(start=a,end=b)for a,b in zip(pts,pts[1:]+pts[:1])])
|
|
155
|
+
out['center']=exact(dict(point=centroid,center_kind='area_centroid'),'signed_polygon_area')
|
|
156
|
+
out['representative_points']=exact([dict(point=p,point_kind='vertex',on_geometry=True)for p in pts],'polygon_vertices')
|
|
157
|
+
elif kind in ('CIRCLE','ARC','CIRCLE_SOLID'):
|
|
158
|
+
center=point(matrix,r['center_x'],r['center_y'])
|
|
159
|
+
out['center']=exact(dict(point=center,center_kind='supporting_circle_center'),'stored_center')
|
|
160
|
+
out['representative_points']=exact([dict(point=center,point_kind='circle_center',on_geometry=None if kind=='CIRCLE_SOLID'else False)],'stored_center')
|
|
161
|
+
if r['flatness']!=1 or not similarity:raise ValueError('Ellipse/nonuniform circular transform is undetermined; center alone is known')
|
|
162
|
+
if kind=='CIRCLE_SOLID':validate_circular_solid(r)
|
|
163
|
+
scale=math.hypot(matrix[0],matrix[2]);radius=r['radius']*scale
|
|
164
|
+
if not math.isfinite(radius)or radius<=0:raise ValueError('Positive finite radius required')
|
|
165
|
+
full=kind=='CIRCLE'or kind=='CIRCLE_SOLID'and (r['base']['pen_style']==105 or r['solid_mode']==100.)
|
|
166
|
+
sweep=TAU if full else r['arc_angle']
|
|
167
|
+
if not math.isfinite(sweep)or not 0<abs(sweep)<=TAU:raise ValueError('Unverified circular sweep')
|
|
168
|
+
angle=r['start_angle']+r['tilt_angle'];direction=point(matrix,r['center_x']+math.cos(angle),r['center_y']+math.sin(angle))
|
|
169
|
+
start=math.atan2(direction[1]-center[1],direction[0]-center[0])
|
|
170
|
+
if matrix[0]*matrix[3]-matrix[1]*matrix[2]<0:sweep=-sweep
|
|
171
|
+
if full:start=0.;sweep=TAU
|
|
172
|
+
contours=[dict(kind='circle'if full else 'arc',center=center,radius=radius,start_angle=start,sweep_angle=sweep,closed=full,role='outer'if kind=='CIRCLE_SOLID'else 'curve')]
|
|
173
|
+
out['bbox']=exact(arc_bounds(center,radius,start,sweep),'analytic_circle_or_arc_extrema')
|
|
174
|
+
center_in_shape=kind=='CIRCLE_SOLID'and r['base']['pen_style']!=105 and (full or abs(sweep)>=math.pi)
|
|
175
|
+
out['representative_points']=exact([dict(point=center,point_kind='circle_center',on_geometry=center_in_shape),dict(point=arc_point(center,radius,start+sweep/2),point_kind='curve_midpoint',on_geometry=True)],'analytic_curve_points')
|
|
176
|
+
if kind=='CIRCLE_SOLID':
|
|
177
|
+
contours[0]['filled']=True
|
|
178
|
+
if r['base']['pen_style']==105:
|
|
179
|
+
inner=r['solid_mode']*scale
|
|
180
|
+
contours.append(dict(kind='circle',center=center,radius=inner,start_angle=0.,sweep_angle=-TAU,closed=True,role='hole'))
|
|
181
|
+
elif not full:
|
|
182
|
+
a,b=arc_point(center,radius,start),arc_point(center,radius,start+sweep)
|
|
183
|
+
contours.append(dict(kind='polyline',points=[b,a],closed=False,role='chord'))
|
|
184
|
+
out['contours']=exact(contours,'analytic_curve_and_qualified_fill_boundary')
|
|
185
|
+
if 'segments'in self.fields:
|
|
186
|
+
seg=self.curve_segments(center,radius,start,sweep,full)
|
|
187
|
+
if seg['status']=='approximate'and kind=='CIRCLE_SOLID':
|
|
188
|
+
if r['base']['pen_style']==105:
|
|
189
|
+
second=self.curve_segments(center,inner,0.,-TAU,True)
|
|
190
|
+
if second['status']=='approximate':seg['value']+=second['value'];seg['max_deviation']=max(seg['max_deviation'],second['max_deviation'])
|
|
191
|
+
else:seg=unknown(second['reason'])
|
|
192
|
+
elif not full:
|
|
193
|
+
closing=self.segments([dict(start=arc_point(center,radius,start+sweep),end=arc_point(center,radius,start))])
|
|
194
|
+
if closing['status']=='exact':seg['value']+=closing['value']
|
|
195
|
+
else:seg=unknown(closing['reason'])
|
|
196
|
+
out['segments']=seg
|
|
197
|
+
else:raise ValueError('Geometry kind is not verified: '+str(kind))
|
|
198
|
+
except (ValueError,KeyError,TypeError,OverflowError)as exc:
|
|
199
|
+
if isinstance(exc,GeometryLimit):raise
|
|
200
|
+
for field in ('bbox','segments','contours'):
|
|
201
|
+
if out[field]['status']=='not_requested':out[field]=unknown(exc)
|
|
202
|
+
if out['center']['status']=='not_requested':out['center']=unknown(exc)
|
|
203
|
+
if out['representative_points']['status']=='not_requested':out['representative_points']=unknown(exc)
|
|
204
|
+
return out
|