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.
Files changed (117) hide show
  1. jwwkit/__init__.py +14 -0
  2. jwwkit/__main__.py +4 -0
  3. jwwkit/_commands/__init__.py +1 -0
  4. jwwkit/_commands/_integrity.py +55 -0
  5. jwwkit/_commands/check_installation.py +484 -0
  6. jwwkit/_commands/compare_drawings.py +29 -0
  7. jwwkit/_commands/inspect_jwf.py +24 -0
  8. jwwkit/_commands/recognize_planar_tategu.py +20 -0
  9. jwwkit/_commands/summarize_drawing.py +16 -0
  10. jwwkit/_comparison_composites.py +179 -0
  11. jwwkit/_edit_result_core.py +175 -0
  12. jwwkit/_ezjww_adapter.py +12 -0
  13. jwwkit/_geometry_engine.py +204 -0
  14. jwwkit/_geometry_math.py +93 -0
  15. jwwkit/_normalization_inspection.py +83 -0
  16. jwwkit/_replacement_contract.py +161 -0
  17. jwwkit/_resources.py +23 -0
  18. jwwkit/_spatial_distance.py +95 -0
  19. jwwkit/_spatial_query.py +34 -0
  20. jwwkit/_tategu_features.py +201 -0
  21. jwwkit/_tategu_geometry.py +108 -0
  22. jwwkit/alignment.py +85 -0
  23. jwwkit/cad_model.py +5 -0
  24. jwwkit/cli.py +40 -0
  25. jwwkit/drawing_comparison.py +430 -0
  26. jwwkit/edit_result.py +136 -0
  27. jwwkit/geometry_view.py +141 -0
  28. jwwkit/guides/AGENTS.md +47 -0
  29. jwwkit/guides/docs/api_list.md +1944 -0
  30. jwwkit/guides/docs/common_edit_result_coverage.md +40 -0
  31. jwwkit/guides/docs/drawing_comparison.md +79 -0
  32. jwwkit/guides/docs/edit_results.md +59 -0
  33. jwwkit/guides/docs/geometry_view.md +64 -0
  34. jwwkit/guides/docs/jwf_key_catalog.md +244 -0
  35. jwwkit/guides/docs/jwf_readonly.md +76 -0
  36. jwwkit/guides/docs/plan_api_coverage.md +75 -0
  37. jwwkit/guides/docs/read_view.md +89 -0
  38. jwwkit/guides/docs/reading_to_editing.md +100 -0
  39. jwwkit/guides/docs/schema_normalization.md +64 -0
  40. jwwkit/guides/docs/search_queries.md +95 -0
  41. jwwkit/guides/docs/spatial_search.md +77 -0
  42. jwwkit/guides/docs/tategu_recognition.md +40 -0
  43. jwwkit/guides/docs/text_equivalence.md +42 -0
  44. jwwkit/guides/docs/usage_guide.md +604 -0
  45. jwwkit/guides/docs/workflow_settings.md +95 -0
  46. jwwkit/hatch_geometry.py +77 -0
  47. jwwkit/jwf.py +202 -0
  48. jwwkit/legacy_document.py +2 -0
  49. jwwkit/line_document.py +14 -0
  50. jwwkit/model/__init__.py +1 -0
  51. jwwkit/model/base.py +15 -0
  52. jwwkit/model/block_definition.py +32 -0
  53. jwwkit/model/block_names.py +41 -0
  54. jwwkit/model/block_operations.py +154 -0
  55. jwwkit/model/block_reference.py +39 -0
  56. jwwkit/model/circle.py +44 -0
  57. jwwkit/model/circular_solid.py +72 -0
  58. jwwkit/model/common_entities.py +34 -0
  59. jwwkit/model/dimension.py +49 -0
  60. jwwkit/model/display_state.py +46 -0
  61. jwwkit/model/document.py +585 -0
  62. jwwkit/model/editing.py +270 -0
  63. jwwkit/model/group_audit.py +91 -0
  64. jwwkit/model/group_import.py +249 -0
  65. jwwkit/model/line.py +31 -0
  66. jwwkit/model/point.py +41 -0
  67. jwwkit/model/preflight.py +120 -0
  68. jwwkit/model/preset_rigid.py +42 -0
  69. jwwkit/model/query.py +270 -0
  70. jwwkit/model/query_fields.py +189 -0
  71. jwwkit/model/query_regex_worker.py +20 -0
  72. jwwkit/model/query_text.py +116 -0
  73. jwwkit/model/rigid_audit.py +66 -0
  74. jwwkit/model/solid.py +51 -0
  75. jwwkit/model/spatial.py +99 -0
  76. jwwkit/model/summary.py +93 -0
  77. jwwkit/model/text.py +49 -0
  78. jwwkit/model/text_metrics.py +78 -0
  79. jwwkit/model/transformations.py +176 -0
  80. jwwkit/normalization.py +287 -0
  81. jwwkit/read_view.py +298 -0
  82. jwwkit/reader/__init__.py +1 -0
  83. jwwkit/reader/archive.py +299 -0
  84. jwwkit/reader/images.py +48 -0
  85. jwwkit/reader/jws.py +175 -0
  86. jwwkit/reader/jws_placement.py +78 -0
  87. jwwkit/reader/sxf.py +51 -0
  88. jwwkit/reader/template_names.py +22 -0
  89. jwwkit/reader/text_presets.py +23 -0
  90. jwwkit/record_reader.py +2 -0
  91. jwwkit/replacement_workflow.py +39 -0
  92. jwwkit/resources/README.md +7 -0
  93. jwwkit/resources/empty_template.jww +0 -0
  94. jwwkit/resources/halfwidth_kana_composition_v1.json +35 -0
  95. jwwkit/resources/header.json +3 -0
  96. jwwkit/resources/jwf_schema.json +925 -0
  97. jwwkit/resources/jww_workflow.example.toml +14 -0
  98. jwwkit/resources/text_equivalence_v1.json +1779 -0
  99. jwwkit/safe_edit.py +454 -0
  100. jwwkit/spatial_search.py +144 -0
  101. jwwkit/tategu_recognition.py +181 -0
  102. jwwkit/workflow_settings.py +140 -0
  103. jwwkit/writer/__init__.py +1 -0
  104. jwwkit/writer/header.py +104 -0
  105. jwwkit/writer/legacy_document.py +62 -0
  106. jwwkit/writer/mfc.py +110 -0
  107. jwwkit/writer/preserved.py +265 -0
  108. jwwkit/writer/strings.py +19 -0
  109. jwwkit/writer/template_constants.py +9 -0
  110. jwwkit/writer/template_document.py +390 -0
  111. jwwkit-0.2.0.dist-info/METADATA +174 -0
  112. jwwkit-0.2.0.dist-info/RECORD +117 -0
  113. jwwkit-0.2.0.dist-info/WHEEL +5 -0
  114. jwwkit-0.2.0.dist-info/entry_points.txt +2 -0
  115. jwwkit-0.2.0.dist-info/licenses/LICENSE +21 -0
  116. jwwkit-0.2.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +51 -0
  117. 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
@@ -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