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,201 @@
1
+ """Geometric hypotheses: radial swing, folded double edges and framed parallel rails.
2
+
3
+ No DAT templates, nominal dimension list, labels, curve-group IDs or reference
4
+ regions are used. Constants below are dimensionless feature guards, not a
5
+ substitute for the explicitly supplied metric tolerances.
6
+ """
7
+ import math
8
+ from jwwkit._tategu_geometry import dot,sub,cross,distance,arc_point,point_segment,box
9
+
10
+
11
+ def axis(line):
12
+ v=sub(line['b'],line['a']);length=math.hypot(*v);u=(v[0]/length,v[1]/length)
13
+ if u[1]<0 or abs(u[1])<1e-12 and u[0]<0:u=(-u[0],-u[1])
14
+ return u
15
+
16
+
17
+ def parallel_vectors(a,b,eps):
18
+ length=max(math.hypot(*a),math.hypot(*b))
19
+ return length>0 and math.atan2(abs(cross(a,b)),abs(dot(a,b)))<=math.atan(eps/length)
20
+
21
+
22
+ def project(lines,u,eps,reference_length=1.):
23
+ v=(-u[1],u[0]);parallel=[];normal=[];diagonal=[]
24
+ for r in lines:
25
+ a,b=r['a'],r['b'];d=sub(b,a)
26
+ p0,p1=dot(a,u),dot(b,u);q0,q1=dot(a,v),dot(b,v)
27
+ item=dict(record=r,lo=min(p0,p1),hi=max(p0,p1),q=(q0+q1)/2,
28
+ qlo=min(q0,q1),qhi=max(q0,q1),p=(p0+p1)/2)
29
+ reference=(u[0]*reference_length,u[1]*reference_length)
30
+ if parallel_vectors(d,reference,eps):parallel.append(item)
31
+ elif parallel_vectors(d,(-reference[1],reference[0]),eps):normal.append(item)
32
+ else:diagonal.append(item)
33
+ return parallel,normal,diagonal
34
+
35
+
36
+ def point(u,p,q):return (u[0]*p-u[1]*q,u[1]*p+u[0]*q)
37
+
38
+
39
+ def support(rows,u,lo,hi,qlo,qhi,margin,always=()):
40
+ ids=set(always)
41
+ for r in rows:
42
+ if r['kind']!='LINE':continue
43
+ values=[(dot(p,u),dot(p,(-u[1],u[0])))for p in (r['a'],r['b'])]
44
+ if all(lo-margin<=p<=hi+margin and qlo-margin<=q<=qhi+margin for p,q in values):ids.add(r['index'])
45
+ return sorted(ids)
46
+
47
+
48
+ def hypothesis(kind,width,endpoints,ids,evidence,*,direction=None,ambiguities=(),method):
49
+ return dict(type_candidates=[kind],clear_width_mm=width,
50
+ position_real_mm=[(endpoints[0][i]+endpoints[1][i])/2 for i in (0,1)],
51
+ opening_endpoints_real_mm=[list(p)for p in endpoints],element_indices=sorted(ids),
52
+ opening_direction=direction,evidence=evidence,ambiguities=list(ambiguities),measurement_method=method)
53
+
54
+
55
+ def swing(rows,eps,width_eps):
56
+ arcs=[r for r in rows if r['kind']=='ARC'];lines=[r for r in rows if r['kind']=='LINE'];out=[]
57
+ for arc in arcs:
58
+ verified=[];fallback=[]
59
+ h=arc['center'];radius=arc['radius']
60
+ ends=[arc_point(arc,arc['start']),arc_point(arc,arc['start']+arc['sweep'])]
61
+ for n,leafend in enumerate(ends):
62
+ leaves=[r for r in lines if min(distance(r['a'],h)+distance(r['b'],leafend),distance(r['b'],h)+distance(r['a'],leafend))<=2*eps]
63
+ if not leaves:continue
64
+ closed=ends[1-n];u=((closed[0]-h[0])/radius,(closed[1]-h[1])/radius)
65
+ parallel,caps,_=project(lines,u,eps,radius);origin=dot(h,u);qhinge=dot(h,(-u[1],u[0]))
66
+ left=[c for c in caps if abs(c['p']-origin)<=width_eps and c['record']['length']<radius/2]
67
+ right=[c for c in caps if abs(c['p']-(origin+radius))<=width_eps and c['record']['length']<radius/2]
68
+ for a in left:
69
+ for b in right:
70
+ if abs(a['qlo']-b['qlo'])>eps or abs(a['qhi']-b['qhi'])>eps:continue
71
+ if not a['qlo']-eps<=qhinge<=a['qhi']+eps:continue
72
+ measured=b['p']-a['p'];depth=a['qhi']-a['qlo']
73
+ if measured<=eps or depth<=eps or abs(measured-radius)>width_eps:continue
74
+ qcenter=(a['qlo']+a['qhi'])/2
75
+ endpoints=[point(u,a['p'],qcenter),point(u,b['p'],qcenter)]
76
+ leaf=leaves[0];leaf_u=((leafend[0]-h[0])/radius,(leafend[1]-h[1])/radius)
77
+ direction=dict(hinge_real_mm=list(h),closed_ray_degrees=math.degrees(math.atan2(u[1],u[0]))%360,
78
+ leaf_ray_degrees=math.degrees(math.atan2(leaf_u[1],leaf_u[0]))%360,
79
+ signed_rotation_degrees=math.degrees(math.atan2(cross(u,leaf_u),dot(u,leaf_u))),
80
+ handedness=None,inside_outside=None,reason='建物の内外基準がないため右勝手/内開き等へ読み替えない')
81
+ leafq=dot(leafend,(-u[1],u[0]));qmin=min(a['qlo'],leafq);qmax=max(a['qhi'],leafq)
82
+ ids=support(rows,u,a['p'],b['p'],qmin,qmax,depth+eps,
83
+ always=[arc['index'],leaf['index'],a['record']['index'],b['record']['index']])
84
+ verified.append(hypothesis('開戸',measured,endpoints,ids,
85
+ ['円弧の中心から端点へ伸びる扉線を確認','閉鎖側の両端に同じ見込の枠内面線を確認',
86
+ '幅は枠内面の投影間隔。半径/全体外形だけでは決めない'],
87
+ direction=direction,method='paired_jamb_faces_with_radial_swing'))
88
+ if not verified:
89
+ fallback.append(hypothesis('開戸',None,[h,closed],[arc['index'],leaves[0]['index']],
90
+ ['円弧中心と端点を結ぶ扉線は確認できる'],
91
+ direction=dict(hinge_real_mm=list(h),handedness=None,inside_outside=None),
92
+ ambiguities=['枠内面が両端で確定しないため半径を内法に読み替えない'],
93
+ method='radial_swing_unverified_opening'))
94
+ # A closed-position line can also join an arc endpoint. An unverified
95
+ # reverse interpretation adds no evidence once the two jamb faces
96
+ # establish the opening for this same arc. Keep all verified variants.
97
+ out.extend(verified or fallback)
98
+ return out
99
+
100
+
101
+ def folded(rows,eps):
102
+ lines=[r for r in rows if r['kind']=='LINE'];out=[]
103
+ for base in lines:
104
+ u=axis(base);length=base['length'];pa,pb=sorted((dot(base['a'],u),dot(base['b'],u)))
105
+ parallel,normal,diagonal=project(lines,u,eps,length)
106
+ long_diagonal=[d for d in diagonal if .1*length<=d['record']['length']<=.9*length]
107
+ if len(long_diagonal)<4:continue
108
+ endpoint_groups=[]
109
+ for endpoint in (base['a'],base['b']):
110
+ near=[d for d in long_diagonal if min(distance(endpoint,d['record']['a']),distance(endpoint,d['record']['b']))<=eps]
111
+ if not near:break
112
+ p=dot(endpoint,u)
113
+ local=[d for d in long_diagonal if min(abs(d['lo']-p),abs(d['hi']-p))<=length*.25]
114
+ doubled=False
115
+ for i,a in enumerate(local):
116
+ for b in local[:i]:
117
+ av=sub(a['record']['b'],a['record']['a']);bv=sub(b['record']['b'],b['record']['a'])
118
+ if parallel_vectors(av,bv,eps):
119
+ separation=point_segment(a['record']['a'],b['record']['a'],b['record']['b'])
120
+ if eps<separation<min(a['record']['length'],b['record']['length'])*.25:doubled=True
121
+ if not doubled:break
122
+ endpoint_groups.append(local)
123
+ if len(endpoint_groups)!=2:continue
124
+ if {d['record']['index']for d in endpoint_groups[0]}&{d['record']['index']for d in endpoint_groups[1]}:continue
125
+ q=dot(base['a'],(-u[1],u[0]));diagonal_ids={d['record']['index']for part in endpoint_groups for d in part}
126
+ local=[base]+[d['record']for part in endpoint_groups for d in part]
127
+ bb=box(local);ids=[r['index']for r in rows if r['bbox'][0]>=bb[0]-eps and r['bbox'][1]>=bb[1]-eps and r['bbox'][2]<=bb[2]+eps and r['bbox'][3]<=bb[3]+eps]
128
+ ids.extend(n['record']['index']for n in normal if n['record']['length']<=length*.25 and pa-eps<=n['p']<=pb+eps and n['qlo']-eps<=q<=n['qhi']+eps)
129
+ ids=sorted(set(ids))
130
+ # Baseline joins both folded double-edge assemblies; it is not a bounding-box dimension.
131
+ out.append(hypothesis('折れ戸',length,[point(u,pa,q),point(u,pb,q)],ids,
132
+ ['直線の開口基準線の両端に、斜めの二重線の組を確認','両端の折れた戸先形状と基準線の接続を確認',
133
+ '幅は両端取付位置を結ぶ基準線長。全体外形幅ではない'],
134
+ direction=dict(axis_degrees=math.degrees(math.atan2(u[1],u[0]))%180,
135
+ side_of_fold='positive'if sum(d['q']-q for d in long_diagonal)>0 else 'negative',
136
+ handedness=None,inside_outside=None),method='baseline_between_folded_leaf_assemblies'))
137
+ return out
138
+
139
+
140
+ def framed(rows,eps):
141
+ lines=[r for r in rows if r['kind']=='LINE'];axes=[];out=[]
142
+ for r in sorted(lines,key=lambda x:-x['length']):
143
+ u=axis(r)
144
+ if not any(parallel_vectors(sub(r['b'],r['a']),(other[0]*length,other[1]*length),eps) for other,length in axes):axes.append((u,r['length']))
145
+ for u,reference_length in axes:
146
+ parallel,normal,_=project(lines,u,eps,reference_length);frames=[];intervals=[]
147
+ for line in parallel:
148
+ match=next((part for part in intervals if abs(part[0]['lo']-line['lo'])<=eps and abs(part[0]['hi']-line['hi'])<=eps),None)
149
+ if match is None:intervals.append([line])
150
+ else:match.append(line)
151
+ for part in intervals:
152
+ if len(part)<3:continue
153
+ lo,hi=part[0]['lo'],part[0]['hi'];qlow=min(p['q']for p in part);qhigh=max(p['q']for p in part)
154
+ if hi-lo<=eps or not eps<qhigh-qlow<(hi-lo)*.5:continue
155
+ caps=[[n for n in normal if abs(n['p']-p)<=eps and sum(n['qlo']-eps<=line['q']<=n['qhi']+eps for line in part)>=2]for p in (lo,hi)]
156
+ if all(caps):frames.append(dict(lo=lo,hi=hi,qlo=qlow,qhi=qhigh,source='continuous_rails_and_inner_jamb_faces',ids=[c[0]['record']['index']for c in caps]))
157
+ # Opposed H/box jamb profiles: the facing ends of the short parallel arms
158
+ # define the clear opening; the outside endpoints are not used as width.
159
+ brackets=[]
160
+ for part in intervals:
161
+ if len(part)<2:continue
162
+ for i,a in enumerate(part):
163
+ for b in part[:i]:
164
+ qlow,qhigh=sorted((a['q'],b['q']));thick=a['hi']-a['lo'];depth=qhigh-qlow
165
+ if depth<=eps or thick<=eps or thick>depth*2:continue
166
+ crosses=[n for n in normal if a['lo']-eps<=n['p']<=a['hi']+eps and n['qlo']<=qlow+eps and n['qhi']>=qhigh-eps]
167
+ if crosses:brackets.append(dict(lo=a['lo'],hi=a['hi'],qlo=qlow,qhi=qhigh,ids=[a['record']['index'],b['record']['index'],crosses[0]['record']['index']]))
168
+ for i,a in enumerate(brackets):
169
+ if len(brackets)>128:raise ValueError('Jamb-profile hypothesis limit exceeded (128)')
170
+ for b in brackets[:i]:
171
+ if abs(a['qlo']-b['qlo'])>eps or abs(a['qhi']-b['qhi'])>eps:continue
172
+ left,right=sorted((a,b),key=lambda x:x['lo']);lo,hi=left['hi'],right['lo'];depth=a['qhi']-a['qlo']
173
+ if hi-lo<=2*depth:continue
174
+ frames.append(dict(lo=lo,hi=hi,qlo=a['qlo'],qhi=a['qhi'],source='inward_ends_of_opposed_jamb_profiles',ids=left['ids']+right['ids']))
175
+ for frame in frames:
176
+ lo,hi=frame['lo'],frame['hi'];width=hi-lo;depth=frame['qhi']-frame['qlo'];qcenter=(frame['qlo']+frame['qhi'])/2
177
+ interior=[p for p in parallel if p['lo']>=lo-depth-eps and p['hi']<=hi+depth+eps and frame['qlo']-eps<=p['q']<=frame['qhi']+eps]
178
+ partial=[p for p in interior if .25*width<=p['hi']-p['lo']<=.9*width]
179
+ offset_pair=[]
180
+ for a in partial:
181
+ for b in partial:
182
+ if a is b or abs(a['q']-b['q'])<=eps:continue
183
+ if abs(a['lo']-lo)>depth+eps or abs(b['hi']-hi)>depth+eps:continue
184
+ if abs(a['hi']-b['lo'])>depth+eps:continue
185
+ middle=(a['hi']+b['lo'])/2
186
+ bridges=[n for n in normal if abs(n['p']-middle)<=depth+eps and n['qlo']<=min(a['q'],b['q'])+eps and n['qhi']>=max(a['q'],b['q'])-eps]
187
+ if bridges:offset_pair=[a,b];break
188
+ if offset_pair:break
189
+ full=[p for p in interior if abs(p['lo']-lo)<=eps and abs(p['hi']-hi)<=eps]
190
+ if offset_pair:
191
+ kind='引き違い窓';evidence=['両端の枠内面/内向き端から開口区間を測定','異なる見込位置の左右の戸線と中央の重なり/接続を確認']
192
+ elif len(full)>=3:
193
+ kind='フィックス';evidence=['左右の枠内面に接する連続した平行線を3本以上確認','開口全体を連続する線があり、左右にずれた戸線の組は確認されない']
194
+ else:continue
195
+ ids=support(rows,u,lo,hi,frame['qlo'],frame['qhi'],2*depth+eps,always=frame['ids'])
196
+ out.append(hypothesis(kind,width,[point(u,lo,qcenter),point(u,hi,qcenter)],ids,
197
+ evidence+['内法は枠内側の間隔。全体外形・水切り等の出を幅に含めない'],
198
+ direction=dict(axis_degrees=math.degrees(math.atan2(u[1],u[0]))%180,
199
+ motion='axis_parallel'if kind=='引き違い窓'else 'fixed',handedness=None,inside_outside=None),
200
+ method=frame['source']))
201
+ return out
@@ -0,0 +1,108 @@
1
+ """Real-mm geometry primitives for read-only planar-symbol analysis."""
2
+ import math
3
+
4
+ TAU=2*math.pi
5
+
6
+
7
+ def dot(a,b):return a[0]*b[0]+a[1]*b[1]
8
+ def sub(a,b):return (a[0]-b[0],a[1]-b[1])
9
+ def cross(a,b):return a[0]*b[1]-a[1]*b[0]
10
+ def distance(a,b):return math.hypot(a[0]-b[0],a[1]-b[1])
11
+
12
+
13
+ def arc_point(r,angle):return (r['center'][0]+r['radius']*math.cos(angle),r['center'][1]+r['radius']*math.sin(angle))
14
+
15
+
16
+ def on_arc(r,point,eps):
17
+ if abs(distance(r['center'],point)-r['radius'])>eps:return False
18
+ if r['kind']=='CIRCLE':return True
19
+ a=math.atan2(point[1]-r['center'][1],point[0]-r['center'][0]);s=r['start'];sweep=r['sweep']
20
+ if sweep<0:s+=sweep;sweep=-sweep
21
+ return (a-s)%TAU<=sweep+eps/r['radius'] or (s-a)%TAU<=eps/r['radius']
22
+
23
+
24
+ def bounds(r):
25
+ if r['kind']=='LINE':points=[r['a'],r['b']]
26
+ else:
27
+ angles=[r['start'],r['start']+r['sweep']]
28
+ angles.extend(a for a in (0,math.pi/2,math.pi,3*math.pi/2) if on_arc(r,arc_point(r,a),1e-10))
29
+ points=[arc_point(r,a) for a in angles]
30
+ return (min(p[0]for p in points),min(p[1]for p in points),max(p[0]for p in points),max(p[1]for p in points))
31
+
32
+
33
+ def box(rows):
34
+ boxes=[r['bbox'] for r in rows]
35
+ return (min(b[0]for b in boxes),min(b[1]for b in boxes),max(b[2]for b in boxes),max(b[3]for b in boxes))
36
+
37
+
38
+ def point_segment(p,a,b):
39
+ v=sub(b,a);length2=dot(v,v)
40
+ if not length2:return distance(p,a)
41
+ t=max(0.,min(1.,dot(sub(p,a),v)/length2))
42
+ return distance(p,(a[0]+t*v[0],a[1]+t*v[1]))
43
+
44
+
45
+ def contacts(a,b,eps):
46
+ ba,bb=a['bbox'],b['bbox']
47
+ if ba[2]+eps<bb[0]or bb[2]+eps<ba[0]or ba[3]+eps<bb[1]or bb[3]+eps<ba[1]:return False
48
+ if a['kind']=='LINE'and b['kind']=='LINE':
49
+ p,r=a['a'],sub(a['b'],a['a']);q,s=b['a'],sub(b['b'],b['a']);den=cross(r,s)
50
+ if den:
51
+ t=cross(sub(q,p),s)/den;u=cross(sub(q,p),r)/den
52
+ if 0<=t<=1 and 0<=u<=1:return True
53
+ return min(point_segment(a['a'],b['a'],b['b']),point_segment(a['b'],b['a'],b['b']),
54
+ point_segment(b['a'],a['a'],a['b']),point_segment(b['b'],a['a'],a['b']))<=eps
55
+ if b['kind']=='LINE':a,b=b,a
56
+ if a['kind']=='LINE':
57
+ p=a['a'];v=sub(a['b'],p);f=sub(p,b['center']);aa=dot(v,v)
58
+ if not aa:return on_arc(b,p,eps)
59
+ if on_arc(b,a['a'],eps)or on_arc(b,a['b'],eps):return True
60
+ if b['kind']=='ARC' and any(point_segment(arc_point(b,t),a['a'],a['b'])<=eps for t in (b['start'],b['start']+b['sweep'])):return True
61
+ bb=2*dot(f,v);cc=dot(f,f)-b['radius']**2;discriminant=bb*bb-4*aa*cc
62
+ if discriminant<0:return False
63
+ for t in ((-bb-math.sqrt(discriminant))/(2*aa),(-bb+math.sqrt(discriminant))/(2*aa)):
64
+ if 0<=t<=1 and on_arc(b,(p[0]+t*v[0],p[1]+t*v[1]),eps):return True
65
+ return False
66
+ d=distance(a['center'],b['center'])
67
+ if d<=eps and abs(a['radius']-b['radius'])<=eps:return any(on_arc(b,arc_point(a,t),eps)for t in (a['start'],a['start']+a['sweep']))
68
+ if d==0 or d>a['radius']+b['radius']+eps or d<abs(a['radius']-b['radius'])-eps:return False
69
+ x=(a['radius']**2-b['radius']**2+d*d)/(2*d);h2=a['radius']**2-x*x
70
+ if h2<0:return False
71
+ u=((b['center'][0]-a['center'][0])/d,(b['center'][1]-a['center'][1])/d)
72
+ for sign in (-1,1):
73
+ point=(a['center'][0]+x*u[0]-sign*math.sqrt(h2)*u[1],a['center'][1]+x*u[1]+sign*math.sqrt(h2)*u[0])
74
+ if on_arc(a,point,eps)and on_arc(b,point,eps):return True
75
+ return False
76
+
77
+
78
+ def components(rows,eps,max_pairs):
79
+ n=len(rows)
80
+ if n*(n-1)//2>max_pairs:raise ValueError('Planar recognition contact-pair limit exceeded')
81
+ parent=list(range(n))
82
+ def root(i):
83
+ while parent[i]!=i:parent[i]=parent[parent[i]];i=parent[i]
84
+ return i
85
+ for i in range(n):
86
+ for j in range(i):
87
+ if contacts(rows[i],rows[j],eps):parent[root(i)]=root(j)
88
+ groups={}
89
+ for i,r in enumerate(rows):groups.setdefault(root(i),[]).append(r)
90
+ return list(groups.values())
91
+
92
+
93
+ def real_record(entity,index,scale,marked):
94
+ g=entity.geometry;kind=entity.entity_type
95
+ r=dict(index=index,kind=kind,marked=marked,curve_group=entity.attributes['group'])
96
+ if kind=='LINE':
97
+ r.update(a=(g['start_x']*scale,g['start_y']*scale),b=(g['end_x']*scale,g['end_y']*scale))
98
+ if not all(math.isfinite(v)for p in (r['a'],r['b'])for v in p):raise ValueError('Nonfinite line')
99
+ r['length']=distance(r['a'],r['b'])
100
+ if not r['length']:raise ValueError('Zero-length line')
101
+ else:
102
+ if g['flatness']!=1:raise ValueError('Elliptical symbols are outside the first planar recognizer')
103
+ r.update(center=(g['center_x']*scale,g['center_y']*scale),radius=g['radius']*scale,
104
+ start=g['start_angle']+g['tilt_angle'],sweep=TAU if kind=='CIRCLE'else g['arc_angle'])
105
+ if not all(math.isfinite(v)for v in (*r['center'],r['radius'],r['start'],r['sweep']))or r['radius']<=0 or not 0<abs(r['sweep'])<=TAU:
106
+ raise ValueError('Unverified circular symbol geometry')
107
+ r['bbox']=bounds(r)
108
+ return r
jwwkit/alignment.py ADDED
@@ -0,0 +1,85 @@
1
+ """Two-point alignment in real millimetres; no automatic tolerance or scaling."""
2
+ import math
3
+
4
+ __all__ = ['solve_two_point_alignment']
5
+
6
+
7
+ def _number(value, label):
8
+ if type(value) not in (int, float) or not math.isfinite(value):
9
+ raise ValueError(label + ' must be finite numeric')
10
+ return float(value)
11
+
12
+
13
+ def _points(values, label):
14
+ if not isinstance(values, (tuple, list)) or len(values) != 2:
15
+ raise ValueError(label + ' requires two points')
16
+ result = []
17
+ for p in values:
18
+ if not isinstance(p, (tuple, list)) or len(p) != 2:
19
+ raise ValueError(label + ' requires XY pairs')
20
+ result.append(tuple(_number(x, label) for x in p))
21
+ return result
22
+
23
+
24
+ def solve_two_point_alignment(source_points, target_points, *, distance_tolerance,
25
+ angle_tolerance, angle_units='rad', allow_scale=False):
26
+ """Return fit, both angle units and tolerance verdict without editing.
27
+
28
+ Distance tolerance is real mm. Angle tolerance bounds the absolute rotation
29
+ between the directed pairs, not a post-fit residual. Scaling is opt-in and
30
+ never bypasses either original-pair tolerance. Coincident pairs are refused.
31
+ """
32
+ src = _points(source_points, 'source_points')
33
+ dst = _points(target_points, 'target_points')
34
+ dt = _number(distance_tolerance, 'distance_tolerance')
35
+ at = _number(angle_tolerance, 'angle_tolerance')
36
+ if angle_units not in ('rad', 'deg'):
37
+ raise ValueError('angle_units must be rad or deg')
38
+ if type(allow_scale) is not bool:
39
+ raise ValueError('allow_scale must be bool')
40
+ if dt < 0 or at < 0 or at > (math.pi if angle_units == 'rad' else 180):
41
+ raise ValueError('Invalid tolerance range')
42
+ ar = at if angle_units == 'rad' else math.radians(at)
43
+ u = tuple(b-a for a,b in zip(src[0],src[1]))
44
+ v = tuple(b-a for a,b in zip(dst[0],dst[1]))
45
+ ls, lt = math.hypot(*u), math.hypot(*v)
46
+ if not all(math.isfinite(x) for x in (*u,*v,ls,lt)):
47
+ raise ValueError('Pair span overflow')
48
+ result = dict(passed=False, reasons=[], source_points_real_mm=src,
49
+ target_points_real_mm=dst, source_distance_mm=ls, target_distance_mm=lt,
50
+ distance_difference_mm=lt-ls, distance_tolerance_mm=dt,
51
+ angle_input_units=angle_units, angle_tolerance_input=at,
52
+ angle_tolerance_rad=ar, angle_tolerance_deg=math.degrees(ar),
53
+ allow_scale=allow_scale, coordinate_units='real', transform=None)
54
+ if ls == 0 or lt == 0:
55
+ result['reasons'] = ['Coincident reference points']
56
+ return result
57
+ result.update(angle_tolerance_endpoint_displacement_mm=(2*math.sin(ar/2))*ls,
58
+ angle_tolerance_lateral_mm=ls*abs(math.sin(ar)),
59
+ angular_offset_baseline_mm=ls,
60
+ reference_angle_tolerance_rad=dt/ls,
61
+ reference_angle_tolerance_deg=math.degrees(dt/ls))
62
+ if not all(math.isfinite(result[k]) for k in ('angle_tolerance_endpoint_displacement_mm','angle_tolerance_lateral_mm','reference_angle_tolerance_rad','reference_angle_tolerance_deg')):
63
+ raise ValueError('Angular tolerance reference amount overflow')
64
+ ux,uy = u[0]/ls,u[1]/ls
65
+ vx,vy = v[0]/lt,v[1]/lt
66
+ angle = math.atan2(ux*vy-uy*vx,ux*vx+uy*vy)
67
+ ratio = lt/ls
68
+ if not math.isfinite(ratio) or ratio <= 0:
69
+ raise ValueError('Scale ratio cannot be represented')
70
+ scale = ratio if allow_scale else 1.
71
+ shift = tuple(b-a for a,b in zip(src[0],dst[0]))
72
+ if not all(math.isfinite(x) for x in shift):
73
+ raise ValueError('Translation overflow')
74
+ result.update(angle_difference_rad=angle, angle_difference_deg=math.degrees(angle),
75
+ estimated_scale=ratio, applied_scale=scale,
76
+ endpoint_residuals_mm=[0., abs(lt-scale*ls)])
77
+ if abs(lt-ls) > dt:
78
+ result['reasons'].append('Distance difference exceeds tolerance')
79
+ if abs(angle) > ar:
80
+ result['reasons'].append('Angle difference exceeds tolerance')
81
+ result['passed'] = not result['reasons']
82
+ if result['passed']:
83
+ result['transform'] = dict(base_point=src[0], translation=shift,
84
+ rotation=angle, scale_x=scale, scale_y=scale, units='real')
85
+ return result
jwwkit/cad_model.py ADDED
@@ -0,0 +1,5 @@
1
+ """編集用公開APIの互換窓口。実寸換算・既存図面編集はmodel.documentで管理する。"""
2
+ from jwwkit.model.common_entities import CadEntity, CadBlockDefinition, model_entity
3
+ from jwwkit.model.document import CadDocument
4
+
5
+ from jwwkit.reader.jws import JwsDocument
jwwkit/cli.py ADDED
@@ -0,0 +1,40 @@
1
+ """Offline command line entry point."""
2
+ import argparse
3
+ import json
4
+
5
+ from jwwkit import guide_paths
6
+ from jwwkit._commands import (
7
+ check_installation, compare_drawings, inspect_jwf, recognize_planar_tategu, summarize_drawing,
8
+ )
9
+
10
+
11
+ def _check(_args):
12
+ result = check_installation.check(
13
+ compare_command="compare",
14
+ inspect_jwf_command="inspect-jwf",
15
+ summarize_command="summarize",
16
+ )
17
+ print(json.dumps(result, ensure_ascii=False, indent=2))
18
+
19
+
20
+ def _guide(_args):
21
+ for path in guide_paths().values():
22
+ print(path)
23
+
24
+
25
+ def main(argv=None):
26
+ parser = argparse.ArgumentParser(prog="jwwkit", description=__doc__)
27
+ commands = parser.add_subparsers(dest="command", required=True)
28
+ commands.add_parser("check", help="Check the installed package with synthetic drawings").set_defaults(run=_check)
29
+ commands.add_parser("guide", help="Print the installed AGENTS.md and usage guide paths").set_defaults(run=_guide)
30
+ for name, module in (
31
+ ("summarize", summarize_drawing),
32
+ ("compare", compare_drawings),
33
+ ("inspect-jwf", inspect_jwf),
34
+ ("recognize-tategu", recognize_planar_tategu),
35
+ ):
36
+ command = commands.add_parser(name, help=module.__doc__, description=module.__doc__)
37
+ module.add_arguments(command)
38
+ command.set_defaults(run=module.run)
39
+ args = parser.parse_args(argv)
40
+ return args.run(args)