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,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
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)
|