summaryrefslogtreecommitdiff
path: root/cad/src/files/mmp/files_mmp.py
blob: e256bd956ce427870d382b1d946675c35e4c2301 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
# Copyright 2004-2009 Nanorex, Inc.  See LICENSE file for details. 
"""
files_mmp.py -- reading MMP files

See also: files_mmp_writing.py, files_mmp_registration.py

@author: Josh, others
@version: $Id$
@copyright: 2004-2009 Nanorex, Inc.  See LICENSE file for details.

History:

bruce 050414 pulled this out of fileIO.py, cvs rev. 1.97
(of which it was the major part),
since I am splitting that into separate modules for each file format.

bruce 080304 split out the mmp writing part into its own file,
files_mmp_writing.py. That also contains the definition of
MMP_FORMAT_VERSION_TO_WRITE and the history of its values
and refers to the documentation for when to change it.
[Later that stuff was moved to mmpformat_versions.py.]

bruce 080304 split out the registration code, to avoid import cycles,
since anything should be able to import that, but the main reading code
(remaining in this file) needs to import a variety of model classes
for constructing them (since not everything uses the registration scheme
or should have to).
"""

import re, time

import foundation.env as env
from utilities import debug_flags

from model.chem import Atom
from model.jigs import AtomSet
from model.jigs import Anchor
from model.jigs import Stat
from model.jigs import Thermo
from model.jigs_motors import RotaryMotor
from model.jigs_motors import LinearMotor
from model.jigs_planes import GridPlane
from analysis.ESP.ESPImage import ESPImage
from model.jigs_measurements import MeasureAngle
from model.jigs_measurements import MeasureDihedral
from geometry.VQT import V, Q, A
from utilities.Log import redmsg, orangemsg, quote_html
from model.elements import PeriodicTable
from model.elements import Pl5
from model.bonds import bond_atoms
from model.chunk import Chunk
from foundation.Utility import Node
from foundation.Group import Group
from model.NamedView import NamedView # for reading one, and for isinstance

from utilities.debug import print_compact_traceback
from utilities.debug import print_compact_stack

from utilities.constants import gensym
from utilities.constants import SUCCESS, ABORTED, READ_ERROR

from model.bond_constants import find_bond
from model.bond_constants import V_SINGLE
from model.bond_constants import V_DOUBLE
from model.bond_constants import V_TRIPLE
from model.bond_constants import V_AROMATIC
from model.bond_constants import V_GRAPHITE
from model.bond_constants import V_CARBOMERIC

from model.Plane import Plane

from files.mmp.files_mmp_registration import find_registered_parser_class
from files.mmp.mmpformat_versions import parse_mmpformat, mmp_date_newer
from files.mmp.mmp_dispnames import interpret_dispName

# the following imports and the assignment they're used in
# should be replaced by some registration scheme
# (not urgent) [bruce 080115]
from dna.model.DnaGroup import DnaGroup
from dna.model.DnaSegment import DnaSegment
from dna.model.DnaStrand import DnaStrand
from cnt.model.NanotubeSegment import NanotubeSegment

from dna.updater.fix_after_readmmp import will_special_updates_after_readmmp_do_anything

# ==

# KNOWN_INFO_KINDS lists the legal "kinds" of info encodable in info records.
# (Note: an "info kind" is a way of finding which object the info is about;
#  it's not related to the data type of the encoded info.)
#
# This is declared here, and checked in set_info_object,
# to make sure that the known info kinds are centrally listed,
# so that that aspect of the mmp format remains documented in this file.
#
# We also include comments about who & when added a specific info kind.
# (The dates might later be turned into code and used for some form of mmp file
#  version compatibility checking.)
#
# [bruce 071023]

KNOWN_INFO_KINDS = (
    'chunk',       #bruce 050217
    'opengroup',   #bruce 050421
    'leaf',        #bruce 050421
    'atom',        #bruce 050511
    'gamess',      #bruce 050701
    'espimage',    #mark 060108
    'povrayscene', #mark 060613
    'plane',
 )

# ==

# GROUP_CLASSIFICATIONS maps a "group classification identifier"
# (a string containing no whitespace) usable in the group mmp record
# (as of 080115) to a constructor function (typically a subclass of Group)
# whose API is the same as that of Group (when used as a constructor).
#
# THIS MUST BE KEPT CONSISTENT with the class constant assignments
# of _mmp_group_classifications in subclasses of Group.
#
# This assignment and the imports that support it ought to be replaced
# by a registration scheme. Not urgent. Alternatively, we could let these
# values depend on the assy being read into (a good thing to do in principle)
# and ask the assy to map these names to classes). [bruce 080115/080310]

_GROUP_CLASSIFICATIONS = { 
    'DnaGroup'         : DnaGroup,
    'DnaSegment'       : DnaSegment,
    'DnaStrand'        : DnaStrand,
    'Block'            : Group,
        #bruce 080331 changed this from Block -> Group, since Block is
        # deprecated; it should remain in this list indefinitely so reading
        # old mmp files continues to work.
    'NanotubeSegment'  : NanotubeSegment,
 }

# == patterns for reading mmp files

#bruce 050414 comment: these pat constants are not presently used in any other files.

_name_pattern = re.compile(r"\(([^)]*)\)")
    # this has a single pattern group which matches a parenthesized string
    
old_csyspat = re.compile("csys \((.+)\) \((-?\d+\.\d+), (-?\d+\.\d+), (-?\d+\.\d+), (-?\d+\.\d+)\) \((-?\d+\.\d+)\)")
new_csyspat = re.compile("csys \((.+)\) \((-?\d+\.\d+), (-?\d+\.\d+), (-?\d+\.\d+), (-?\d+\.\d+)\) \((-?\d+\.\d+)\) \((-?\d+\.\d+), (-?\d+\.\d+), (-?\d+\.\d+)\) \((-?\d+\.\d+)\)")
namedviewpat = re.compile("namedview \((.+)\) \((-?\d+\.\d+), (-?\d+\.\d+), (-?\d+\.\d+), (-?\d+\.\d+)\) \((-?\d+\.\d+)\) \((-?\d+\.\d+), (-?\d+\.\d+), (-?\d+\.\d+)\) \((-?\d+\.\d+)\)")
datumpat = re.compile("datum \((.+)\) \((\d+), (\d+), (\d+)\) (.*) \((-?\d+\.\d+), (-?\d+\.\d+), (-?\d+\.\d+)\) \((-?\d+\.\d+), (-?\d+\.\d+), (-?\d+\.\d+)\) \((-?\d+\.\d+), (-?\d+\.\d+), (-?\d+\.\d+)\)")
keypat = re.compile("\S+")
## molpat = re.compile("mol \(.*\) (\S\S\S)")
molpat = re.compile("mol \(.*\) (\w+)")
atom1pat = re.compile("atom (\d+) \((\d+)\) \((-?\d+), (-?\d+), (-?\d+)\)")
##atom2pat = re.compile("atom \d+ \(\d+\) \(.*\) (\S\S\S)")
atom2pat = re.compile("atom \d+ \(\d+\) \(.*\) (\w+)") # \w == [a-zA-Z0-9_]

# Old Rotary Motor record format: 
# rmotor (name) (r, g, b) torque speed (cx, cy, cz) (ax, ay, az)
old_rmotpat = re.compile("rmotor \((.+)\) \((\d+), (\d+), (\d+)\) (-?\d+\.\d+) (-?\d+\.\d+) \((-?\d+), (-?\d+), (-?\d+)\) \((-?\d+), (-?\d+), (-?\d+)\)")

# New Rotary Motor record format: 
# rmotor (name) (r, g, b) torque speed (cx, cy, cz) (ax, ay, az) length radius spoke_radius
new_rmotpat = re.compile("rmotor \((.+)\) \((\d+), (\d+), (\d+)\) (-?\d+\.\d+) (-?\d+\.\d+) \((-?\d+), (-?\d+), (-?\d+)\) \((-?\d+), (-?\d+), (-?\d+)\) (-?\d+\.\d+) (-?\d+\.\d+) (-?\d+\.\d+)")

# Old Linear Motor record format: 
# lmotor (name) (r, g, b) force stiffness (cx, cy, cz) (ax, ay, az)
old_lmotpat = re.compile("lmotor \((.+)\) \((\d+), (\d+), (\d+)\) (-?\d+\.\d+) (-?\d+\.\d+) \((-?\d+), (-?\d+), (-?\d+)\) \((-?\d+), (-?\d+), (-?\d+)\)")

# New Linear Motor record format: 
# lmotor (name) (r, g, b) force stiffness (cx, cy, cz) (ax, ay, az) length width spoke_radius
new_lmotpat = re.compile("lmotor \((.+)\) \((\d+), (\d+), (\d+)\) (-?\d+\.\d+) (-?\d+\.\d+) \((-?\d+), (-?\d+), (-?\d+)\) \((-?\d+), (-?\d+), (-?\d+)\) (-?\d+\.\d+) (-?\d+\.\d+) (-?\d+\.\d+)")

#Grid Plane record format:
#gridplane (name) (r, g, b) width height (cx, cy, cz) (w, x, y, z) grid_type line_type x_space y_space (gr, gg, gb) 
gridplane_pat = re.compile("gridplane \((.+)\) \((\d+), (\d+), (\d+)\) (-?\d+\.\d+) (-?\d+\.\d+) \((-?\d+\.\d+), (-?\d+\.\d+), (-?\d+\.\d+)\) \((-?\d+\.\d+), (-?\d+\.\d+), (-?\d+\.\d+), (-?\d+\.\d+)\) (\d+) (\d+) (-?\d+\.\d+) (-?\d+\.\d+) \((\d+), (\d+), (\d+)\)")

#Plane record format:
#plane (name) (r, g, b) width height (cx, cy, cz) (w, x, y, z)
plane_pat = re.compile("plane \((.+)\) \((\d+), (\d+), (\d+)\) (-?\d+\.\d+) (-?\d+\.\d+) \((-?\d+\.\d+), (-?\d+\.\d+), (-?\d+\.\d+)\) \((-?\d+\.\d+), (-?\d+\.\d+), (-?\d+\.\d+), (-?\d+\.\d+)\)")

# ESP Image record format:
# espimage (name) (r, g, b) width height resolution (cx, cy, cz) (w, x, y, z) trans (fr, fg, fb) show_bbox win_offset edge_offset 
## esppat = re.compile("espimage \((.+)\) \((\d+), (\d+), (\d+)\) (-?\d+\.\d+) (-?\d+\.\d+) (\d+) \((-?\d+\.\d+), (-?\d+\.\d+), (-?\d+\.\d+)\) \((-?\d+\.\d+), (-?\d+\.\d+), (-?\d+\.\d+), (-?\d+\.\d+)\) (-?\d+\.\d+) \((\d+), (\d+), (\d+)\) (\d+) (-?\d+\.\d+) (-?\d+\.\d+)")
#bruce 060207 generalize pattern so espwindow is also accepted (to help fix bug 1357); safe forever, but can be removed after A7
esppat = re.compile("[a-z]* \((.+)\) \((\d+), (\d+), (\d+)\) (-?\d+\.\d+) (-?\d+\.\d+) (\d+) \((-?\d+\.\d+), (-?\d+\.\d+), (-?\d+\.\d+)\) \((-?\d+\.\d+), (-?\d+\.\d+), (-?\d+\.\d+), (-?\d+\.\d+)\) (-?\d+\.\d+) \((\d+), (\d+), (\d+)\) (\d+) (-?\d+\.\d+) (-?\d+\.\d+)")

# atomset (name) (r, g, b) atom1 atom2 ... atom25 {up to 25}
atomsetpat = re.compile("atomset \((.+)\) \((\d+), (\d+), (\d+)\)")

# ground (name) (r, g, b) atom1 atom2 ... atom25 {up to 25}
#bruce 060228 generalize pattern so "anchor" is also accepted; see also _read_anchor
groundpat = re.compile("[a-z]* \((.+)\) \((\d+), (\d+), (\d+)\)")

# stat (name) (r, g, b) (temp) first_atom last_atom boxed_atom
statpat = re.compile("stat \((.+)\) \((\d+), (\d+), (\d+)\) \((\d+)\)" )

# thermo (name) (r, g, b) first_atom last_atom boxed_atom
thermopat = re.compile("thermo \((.+)\) \((\d+), (\d+), (\d+)\)" )

# general jig pattern #bruce 050701
jigpat = re.compile("\((.+)\) \((\d+), (\d+), (\d+)\)")

# more readable regexps, wware 051103
# font names NEVER have parentheses in them, ala Postscript
nameRgbFontnameFontsize = ("\((.+)\) " +                    # (name)
                           "\((\d+), (\d+), (\d+)\) " +     # (r, g, b)
                           "\((.+)\) " +                    # (font_name)
                           "(\d+)")                         # font_size
_oneAtompat = " (\d+)"

# mdistance (name) (r, g, b) (font_name) font_size a1 a2
mdistancepat = re.compile("mdistance " + nameRgbFontnameFontsize +
                          _oneAtompat + _oneAtompat)

# mangle (name) (r, g, b) (font_name) font_size a1 a2 a3
manglepat = re.compile("mangle " + nameRgbFontnameFontsize +
                       _oneAtompat + _oneAtompat + _oneAtompat)

# mdihedral (name) (r, g, b) (font_name) font_size a1 a2 a3 a4
mdihedralpat = re.compile("mdihedral " + nameRgbFontnameFontsize +
                          _oneAtompat + _oneAtompat + _oneAtompat + _oneAtompat)

# == reading mmp files

_MMP_FORMAT_VERSION_WE_CAN_READ__MOST_CONSERVATIVE = '050920 required; 080321 preferred'
    # ideally, this should be identical to MMP_FORMAT_VERSION_TO_WRITE,
    # and should just be called _MMP_FORMAT_VERSION_WE_CAN_READ,
    # but sometimes it can temporarily differ (in either direction),
    # so we don't use the same constant.
    #
    # Note: this value is based on all the code that contributes to mmp reading,
    # not only the code in this file. That includes methods on model objects
    # for parsing info records, and registered mmp record parsers.
    # [bruce 080328 new feature and comment]

def _mmp_format_version_we_can_read(): # bruce 080328, revised 080410 (should it be a method?)
    from utilities.GlobalPreferences import debug_pref_read_bonds_compactly
    from utilities.GlobalPreferences import debug_pref_read_new_display_names
    
    if debug_pref_read_bonds_compactly() and debug_pref_read_new_display_names():
        # this is the default, as of 080328, still true 080410 and for upcoming
        # release of NE1 1.0.0; revised to a newer one, 080523, 080529
        res = '080328 required; 080529 preferred' # i.e. MMP_FORMAT_VERSION_TO_WRITE__WITH_COMPACT_BONDS_AND_NEW_DISPLAY_NAMES
    elif debug_pref_read_new_display_names():
        # this is the default which we *write*, as of 080410 and for upcoming
        # release of NE1 1.0.0; revised to a newer one, 080523, 080529
        # note: setting prefs to only read this high is only useful for testing
        res = '080327 required; 080529 preferred' # i.e. MMP_FORMAT_VERSION_TO_WRITE__WITH_NEW_DISPLAY_NAMES
    else:
        # setting prefs to only read this high is only useful for testing
        res = _MMP_FORMAT_VERSION_WE_CAN_READ__MOST_CONSERVATIVE
    return res

# ==

def decode_atom_coordinate(coord_string): #bruce 080521
    """
    Decode an atom coordinate string as used in the atom record
    of an mmp file (in the traditional format as of 080521).
    Return a float, in Angstroms.
    """
    # note: this is inlined in decode_atom_coordinates
    return float(coord_string) / 1000.0 # in Angstroms

def decode_atom_coordinates(xs, ys, zs): #bruce 080521
    """
    Decode three atom coordinate strings as used in the atom record
    of an mmp file (in the traditional format as of 080521).
    Interpret them as x, y, z coordinates respectively.
    Return a Numeric array of three floats, in Angstroms
    (which is the NE1 internal standard for representing a
     model space position).
    """
    # this would be correct (untested):
    ## return A(map( decode_atom_coordinate, [xs, ys, zs]))
    # but it's better to optimize by inlining decode_atom_coordinate:
    return V(float(xs), float(ys), float(zs)) / 1000.0

# ==

class _readmmp_state:
    """
    Hold the state needed by _readmmp between lines;
    and provide some methods to help with reading the lines.
    [See also the classes mmp_interp (another read-helper)
     and writemmp_mapping.]
    """
    #bruce 050405 made this class from most of _readmmp to help generalize it
    # (e.g. for reading sim input files for minimize selection)

    # initial values of instance variables
    # TODO: some or all of these are private -- rename them to indicate that [bruce 071023 comment]
    prevatom = None # the last atom read, if any
    prevcard = None # used in reading atoms and bonds [TODO: doc, make private]
    prevchunk = None # the current Chunk being built, if any [renamed from self.mol, bruce 071023]
    prevmotor = None # the last motor jig read, if any (used by shaft record)
    
    def __init__(self, assy, isInsert):
        self.assy = assy
            #bruce 060117 comment: self.assy is only used to pass to Node constructors (including _MarkerNode),
            # and to set assy.temperature and assy.mmpformat (only done if not isInsert, which looks like only use of isInsert here).
        self.isInsert = isInsert
        #bruce 050405 made the following from old _readmmp localvars, and revised their comments
        self.ndix = {}
        topgroup = Group("__opengroup__", assy, None)
            #bruce 050405 topgroup holds toplevel groups (or other items) as members; replaces old code's grouplist
        self.groupstack = [topgroup]
            #bruce 050405 revised this -- no longer stores names separately, and current group is now at end
            # stack (top at end) to store all unclosed groups
            # (the only group which can accept children, as we read the file, is always self.groupstack[-1];
            #  in the old code this was called opengroup [bruce 050405])
        self.sim_input_badnesses_so_far = {} # helps warn about sim-input files
        self.markers = {} #bruce 050422 for forward_ref records
        self._info_objects = {} #bruce 071017 for info records
            # (replacing attributes of self named by the specific kinds)
        self._registered_parser_objects = {} #bruce 071017
        self.listOfAtomsInFileOrder = []
        return

    def destroy(self):
        self.assy = self.ndix = self.groupstack = self.markers = None
        self.prevatom = None
        self.prevcard = None
        self.prevchunk = None
        self.prevmotor = None
        self.sim_input_badnesses_so_far = None
        self._info_objects = None
        self._registered_parser_objects = None
        self.listOfAtomsInFileOrder = None
        return

    def extract_toplevel_items(self):
        """
        for use only when done: extract the list of toplevel items
        (removing them from our artificial Group if any);
        but don't verify they are Groups or alter them, that's up to the caller.
        """
        for marker in self.markers.values():
            marker.kill() #bruce 050422; semi-guess
        self.markers = None
        if len(self.groupstack) > 1:
            self.warning("mmp file had %d unclosed groups" % (len(self.groupstack) - 1))
        topgroup = self.groupstack[0]
        self.groupstack = "error if you keep reading after this"
        res = topgroup.members[:]
        for m in res:
            topgroup.delmember(m)
        return res

    def warning(self, msg):
        msg = quote_html(msg)
        env.history.message( redmsg( "Warning: " + msg))

    def format_error(self, msg): ###e use this more widely?
        msg = quote_html(msg)
        env.history.message( redmsg( "Warning: mmp format error: " + msg))
            ###e and say what we'll do? review calls; syntax error

    def bug_error(self, msg):
        msg = quote_html(msg)
        env.history.message( redmsg( "Bug: " + msg))
    
    def readmmp_line(self, card):
        """
        returns None, or error msg(#k), or raises exception
        on bugs or maybe some syntax errors
        """
        key_m = keypat.match(card)
        if not key_m:
            # ignore blank lines (does this also ignore some erroneous lines??) #k
            return
        recordname = key_m.group(0)
        # recordname should now be the mmp record type, e.g. "group" or "mol"

        linemethod, errmsg = self._find_linemethod(recordname)

        if errmsg or not linemethod:
            return errmsg

        # if linemethod itself has an exception, best to let the caller handle it
        # (only it knows whether the line passed to us was made up or really in the file)
        return linemethod(card)

    def _find_linemethod(self, recordname):
        """
        [private]
        
        Look for a method for parsing one mmp file line
        which starts with recordname.
        
        @return: the tuple (linemethod, errmsg), where linemethod is a callable
                 which takes one argument (the entire line ### with \n or not??)
                 and returns None or an error message string, and errmsg is
                 None or an error message string from the process of looking
                 for the linemethod.
        """
        errmsg = None # will be changed below if an error message is needed
        linemethod = None # will be changed below when an mmp-line parser method is found
        
        # first look for a registered parser for this recordname
        
        parser = self._find_registered_parser_object(recordname)
        if parser:
            linemethod = parser.read_record
                # probably this is never None, but this code supports it being None
            if linemethod:
                return linemethod, errmsg

        # if there was no registered record parser, look for a built-in method to do it,
        # as a specially-named method of self, based on the recordname
        # (this is safe, since the prefix means arbitrary input can't find unintended methods)
        methodname = "_read_" + recordname # e.g. _read_group, _read_mol, ...
        try:
            linemethod = getattr(self, methodname, None)
        except:
            # I don't know if an exception can happen (e.g. from non-identifier chars in recordname),
            # and if it can, whether it would always be an AttributeError.
            ###e TODO: print_compact_traceback until i know
            linemethod = None
            errmsg = "syntax error or bug" ###e improve message
        else:
            if linemethod is None:
                # unrecognized mmp recordname -- not an error,
                # but [bruce 050217 new debug feature]
                # print a debug-only warning except for a comment line
                # (TODO: maybe only do this the first time we see it?)
                if debug_flags.atom_debug and recordname != '#':
                    print "atom_debug: fyi: unrecognized mmp record type ignored (not an error): %r" % recordname
            pass

        return linemethod, errmsg

    def _find_registered_parser_object(self, recordname):
        """
        Return an instance of the registered record-parser class for this recordname
        (cached for reuse while reading the one mmp file self will be used for),
        or None if no parser was registered for this recordname.
        """
        try:
            return self._registered_parser_objects[recordname]
        except KeyError:
            clas = find_registered_parser_class(recordname)
                # just one global registry, for now
            if clas:
                instance = clas(self, recordname)
            else:
                instance = None
                    # cache None too -- the query might be repeated
                    # just as much as if we had a parser for it
            self._registered_parser_objects[recordname] = instance
            return instance
        pass

    def get_name(self, card, default):
        """
        Get the object name from an mmp record line
        which represents the name in the usual way
        (as a parenthesized string immediately after the recordname),
        but don't decode it (see decode_name for that).

        @return: name
        @rtype: string

        @see: get_decoded_name_and_rest
        """
        # note: code also used in get_decoded_name_and_rest
        x = _name_pattern.search(card)
        if x:
            return x.group(1)
        print "warning: mmp record without a valid name field: %r" % (card,) #bruce 071019
        return gensym(default)
            # Note: I'm not sure it's safe/good to pass an assy argument
            # to this gensym, and I also think this probably never happens,
            # so it's best to be safe and not pass one. [bruce 080407 comment]
    
    def get_decoded_name_and_rest(self, card, default = None): #bruce 080115
        """
        Get the object name from an mmp record line
        which represents the name in the usual way
        (as an encoded parenthesized string immediately after the recordname).
        Return the tuple ( decoded name, stripped rest of record),
        or ( default, "" ) if the record line has the wrong format.
        
        @param card: the entire mmp record
        @type card: string
        
        @param default: what to return for the name (or pass to gensym
                        if a string) if the record line
                        has the wrong format; default value is None
                        (typically an error in later stages of the caller)
        @type default: anything, usually string or None
        
        @return: ( decoded name, stripped rest of record )
        @rtype: tuple of (string, string)

        @see: get_name
        """
        # note: copies some code from get_name
        x = _name_pattern.search(card)
        if x:
            # match succeeded
            name = self.decode_name( x.group(1) )
            rest = card.split(')', 1)[1]
            rest = rest.strip()
        else:
            # format error
            print "warning: mmp record without a valid name field: %r" % (card,) #bruce 071019
            if type(default) == type(""):
                name = gensym(default)
                    # Note: I'm not sure it's safe/good to pass an assy argument
                    # to this gensym, so I won't. [bruce 080407 comment]
            else:
                name = default
            rest = ""
        return ( name, rest)
    
    def decode_name(self, name): #bruce 050618 part of fixing part of bug 474
        """
        Invert the transformation done by the writer's encode_name method.
        """
        name = name.replace("%28",'(') # most of these replacements can be done in any order...
        name = name.replace("%29",')')
        name = name.replace("%25",'%') # ... but this one must be done last.
        return name

    # the remaining methods are parsers for specific records (soon to be split out
    # and registered -- bruce 071017), mixed with helper functions for their use
    
    def _read_group(self, card): # group: begins any kind of Group
        """
        Read the mmp record which indicates the beginning of a Group object
        (for Group or any of its specialized subclasses).

        Subsequent mmp records (including nested groups)
        will be read as members of this group,
        until a matching egroup record is read.
        
        @see: self._read_egroup
        """
        #bruce 080115 generalized this to use or save group classifications
        name, rest = self.get_decoded_name_and_rest(card, "Grp")
        assert name is not None
        old_opengroup = self.groupstack[-1]
        constructor = Group
        extra_classifications = []
        for classification in rest.split(): # from general to specific
            if classification in _GROUP_CLASSIFICATIONS:
                constructor = _GROUP_CLASSIFICATIONS[ classification ]
                    # assume this will always write out a classification
                    # sufficient to regenerate it; if that's ever not true
                    # we should save classification into extra_classifications
                    # here
                extra_classifications = []
            else:
                extra_classifications.append( classification )
            continue # use the last one we recognize; save and rewrite the extra ones
        new_opengroup = constructor(name, self.assy, old_opengroup)
            # this includes addchild of new group to old_opengroup (so don't call self.addmember)
        if extra_classifications:
            # make sure they can get written out again
            new_opengroup.set_extra_classifications( extra_classifications )
        self.groupstack.append(new_opengroup)

    def _read_egroup(self, card): # egroup: ends any kind of Group
        """
        Read the mmp record which indicates the end of a Group object
        (for Group or any of its specialized subclasses).
        
        @see: self._read_group
        """        
        name = self.get_name(card, "Grp")
        assert name is not None
        name = self.decode_name(name)
        if len(self.groupstack) == 1:
            return "egroup %r when no groups remain unclosed" % (name,)
        curgroup = self.groupstack.pop()
        curname = curgroup.name
        if name != curname:
            # note, unlike old code we've already popped a group; shouldn't matter [bruce 050405]
            return "mismatched group records: egroup %r tried to match group %r" % (name, curname)
        return None # success
    
    def _read_mol(self, card): # mol: start a Chunk
        name = self.get_name(card, "Mole")
        name = self.decode_name(name)
        mol = Chunk(self.assy,  name)
        self.prevchunk = mol
            # so its atoms, etc, can find it (might not be needed if they'd search for it) [bruce 050405 comment]
            # now that I removed _addMolecule, this is less often reset to None,
            # so we'd detect more errors if they did search for it [bruce 050405]
        disp = molpat.match(card)
        if disp:
            mol.setDisplayStyle(interpret_dispName(disp.group(1), atom = False)) #bruce 080324 revised
        self.addmember(mol) #bruce 050405; removes need for _addMolecule

    def _read_atom(self, card):
        m = atom1pat.match(card)
        if not m:
            print card
        n = int(m.group(1))
        try:
            element = PeriodicTable.getElement(int(m.group(2)))
            sym = element.symbol
        except:
            # catch unsupported element error [bruce 080115] [untested?]
            # todo: improve getElement so we can narrow down exception type,
            # or turn this into a special return value; or perhaps better,
            # permit creating an atom of an unknown element
            # (by transparently extending getElement to create one)
            sym = "C"
            errmsg = "unsupported element in this mmp line; using %s: %s" % (sym, card,)
            self.format_error(errmsg)
        ## xyz = A(map(float, [m.group(3), m.group(4), m.group(5)])) / 1000.0
        xyz = decode_atom_coordinates( m.group(3), m.group(4), m.group(5) ) #bruce 080521
        if self.prevchunk is None:
            #bruce 050405 new feature for reading new bare sim-input mmp files
            self.guess_sim_input('missing_group_or_chunk')
            self.prevchunk = Chunk(self.assy,  "sim chunk")
            self.addmember(self.prevchunk)
        a = Atom(sym, xyz, self.prevchunk) # sets default atomtype for the element [behavior of that was revised by bruce 050707]
        self.listOfAtomsInFileOrder.append(a)
        a.unset_atomtype() # let it guess atomtype later from the bonds read from subsequent mmp records [bruce 050707]
        disp = atom2pat.match(card)
        if disp:
            a.setDisplayStyle(interpret_dispName(disp.group(1))) #bruce 080324 revised
        self.ndix[n] = a
        self.prevatom = a
        self.prevcard = card
        return

    def _read_bond1(self, card):
        return self.read_bond_record(card, V_SINGLE)
        
    def _read_bond2(self, card):
        return self.read_bond_record(card, V_DOUBLE)
        
    def _read_bond3(self, card):
        return self.read_bond_record(card, V_TRIPLE)
        
    def _read_bonda(self, card):
        return self.read_bond_record(card, V_AROMATIC)
        
    def _read_bondg(self, card):
        return self.read_bond_record(card, V_GRAPHITE)
        
    def _read_bondc(self, card): #bruce 050920 added this
        return self.read_bond_record(card, V_CARBOMERIC)

    def read_bond_record(self, card, valence):
        list1 = map(int, re.findall("\d+", card[5:])) # note: this assumes all bond mmp-record-names are the same length, 5 chars.
        try:
            for a in map((lambda n: self.ndix[n]), list1):
                bond_atoms( self.prevatom, a, valence, no_corrections = True) # bruce 050502 revised this
        except KeyError:
            print "error in MMP file: atom ", self.prevcard
            print card
            #e better error action, like some exception?

    def _read_bond_direction(self, card): #bruce 070415
        atomcodes = card.strip().split()[1:] # note: these are strings, but self.ndix needs ints
        assert len(atomcodes) >= 2
        atoms = map((lambda nstr: self.ndix[int(nstr)]), atomcodes)
        for atom1, atom2 in zip(atoms[:-1], atoms[1:]):
            bond = find_bond(atom1, atom2)
            bond.set_bond_direction_from(atom1, 1)
        return

    def _read_bond_chain(self, card): #bruce 080328
        from utilities.GlobalPreferences import debug_pref_read_bonds_compactly
        if not debug_pref_read_bonds_compactly():
            summary_format = "Error: input file contains [N] bond_chain record(s) we can't read"
            env.history.deferred_summary_message( redmsg(summary_format))
            return
        fields = card.strip().split()[1:]
        assert len(fields) >= 2 # beginning and ending atom code
            # (ignore extra fields for upwards compatibility)
        # note: following code is similar in two methods
        atoms_in_range = self.atoms_in_range(fields[0], fields[1])
        if len(atoms_in_range) > 1: # not sure if this test is required for correctness
            for atom1, atom2 in zip( atoms_in_range[:-1], atoms_in_range[1:] ):
                bond_atoms( atom1, atom2, V_SINGLE, no_corrections = True)
            pass
        return

    def _read_directional_bond_chain(self, card): #bruce 080328
        from utilities.GlobalPreferences import debug_pref_read_bonds_compactly
        if not debug_pref_read_bonds_compactly():
            summary_format = "Error: input file contains [N] directional_bond_chain record(s) we can't read"
            env.history.deferred_summary_message( redmsg(summary_format))
            return
        fields = card.strip().split()[1:]
        assert len(fields) >= 3 # beginning and ending atom code, and bond direction
            # (ignore extra fields for upwards compatibility;
            #  optional 4th field might be sequence info [nim], default 'X')
        # note: following code is similar in two methods
        atoms_in_range = self.atoms_in_range(fields[0], fields[1])
        bond_dir = int(fields[2])
        assert bond_dir in (1, -1)
        if len(atoms_in_range) > 1: # not sure if this test is required for correctness
            for atom1, atom2 in zip( atoms_in_range[:-1], atoms_in_range[1:] ):
                bond = bond_atoms( atom1, atom2, V_SINGLE, no_corrections = True)
                bond.set_bond_direction_from(atom1, bond_dir)
            pass
        if len(fields) >= 4:
            # store optional dna base sequence info
            # (allowed to be shorter than needed, but not longer;
            #  X or missing letter is not stored, so it won't override
            #  an earlier per-atom info record specifying the dnaBaseName)
            sequence = fields[3]
            assert sequence.isalpha()
            pointer = 0 # to next unused character within sequence
            for atom in atoms_in_range:
                if pointer >= len(sequence):
                    break
                if atom.element.role == 'strand' and not atom.element is Pl5:
                    # atom is a strand_sugar
                    letter = sequence[pointer]
                    pointer += 1
                    if letter != 'X':
                        atom.setDnaBaseName(letter)
                continue
            if pointer < len(sequence):
                assert 0, "extra sequence info: only %d of %d chars were assigned from %r" % \
                       (pointer, len(sequence), card)
            pass    
        return

    def _read_dna_rung_bonds(self, card): #bruce 080328
        from utilities.GlobalPreferences import debug_pref_read_bonds_compactly
        if not debug_pref_read_bonds_compactly():
            summary_format = "Error: input file contains [N] dna_rung_bonds record(s) we can't read"
            env.history.deferred_summary_message( redmsg(summary_format))
            return
        fields = card.strip().split()[1:]
        assert len(fields) >= 4 # beginning and ending atom codes, for chunk1 and chunk2
            # (ignore extra fields for upwards compatibility)
        atoms_in_range1 = self.atoms_in_range(fields[0], fields[1])
        atoms_in_range2 = self.atoms_in_range(fields[2], fields[3])
        def ok(atom):
            assert isinstance(atom, Atom)
            return atom.element.role == 'axis' or \
                   (atom.element.role == 'strand' and not atom.element is Pl5)
        atoms1 = filter(ok, atoms_in_range1)
        atoms2 = filter(ok, atoms_in_range2)
        assert len(atoms1) == len(atoms2), \
               "qualifying atom counts %d and %d don't match in %r" % \
               (len(atoms1), len(atoms2), card)
        for atom1, atom2 in zip(atoms1, atoms2):
            bond_atoms( atom1, atom2, V_SINGLE, no_corrections = True)
        return

    def atoms_in_range(self, start, end): #bruce 080328
        """
        Return all the atoms whose atomcodes in self are between start and end,
        inclusive. Start and end can be equal, meaning return one atom.
        """
        start = int(start)
        end = int(end)
        assert 1 <= start <= end
        res = [self.ndix[code] for code in range(start, end+1)]
            # that will fail if any atom in that range wasn't yet read
        return res
    
    # == jig reading methods.

    # Note that there are at least three different ways various jigs handle
    # reading their atom list: in a separate shaft record which occurs after
    # their main mmp record, whose atoms are passed to jig.setShaft;
    # or in the same record, passed to the constructor;
    # or in the same record, but passed to setProps after the jig is made.
    # This ought to be cleaned up sometime.
    # See also self.read_new_jig, which is the beginning of a partial cleanup.
    # [bruce 080227 comment]
    
    # Read the MMP record for a Rotary Motor as either:
    # rmotor (name) (r, g, b) torque speed (cx, cy, cz) (ax, ay, az) length, radius, spoke_radius
    # rmotor (name) (r, g, b) torque speed (cx, cy, cz) (ax, ay, az)
    # (note: the atoms are read separately from a subsequent shaft record)
    def _read_rmotor(self, card):
        m = new_rmotpat.match(card) # Try to read card with new format
        if not m:
            m = old_rmotpat.match(card) # If that didn't work, read card with old format
        ngroups = len(m.groups()) # ngroups = number of fields found (12 = old, 15 = new)
        name = m.group(1)
        name = self.decode_name(name)
        col = map(lambda (x): int(x) / 255.0,
                  [m.group(2), m.group(3), m.group(4)] )
        torq = float(m.group(5))
        sped = float(m.group(6))
        cxyz = A(map(float, [m.group(7), m.group(8), m.group(9)])) / 1000.0
        axyz = A(map(float, [m.group(10), m.group(11), m.group(12)])) / 1000.0
        if ngroups == 15: # if we have 15 fields, we have the length, radius and spoke radius.
            length = float(m.group(13))
            radius = float(m.group(14))
            sradius = float(m.group(15))
        else: # if not, set the default values for length, radius and spoke radius.
            length = 10.0
            radius = 2.0
            sradius = 0.5
        motor = RotaryMotor(self.assy)
        props = name, col, torq, sped, cxyz, axyz, length, radius, sradius
        motor.setProps(props)
        self.addmotor(motor)

    def addmotor(self, motor): #bruce 050405 split this out
        self.addmember(motor)
        self.prevmotor = motor # might not be needed if we just looked for it when we need it [bruce 050405 comment]

    def _read_shaft(self, card):
        list1 = map(int, re.findall("\d+", card[6:]))
        list1 = map((lambda n: self.ndix[n]), list1)
        self.prevmotor.setShaft(list1)
          
    # Read the MMP record for a Linear Motor as:
    # lmotor (name) (r, g, b) force stiffness (cx, cy, cz) (ax, ay, az) length, width, spoke_radius
    # lmotor (name) (r, g, b) force stiffness (cx, cy, cz) (ax, ay, az)
    # (note: the atoms are read separately from a subsequent shaft record)
    def _read_lmotor(self, card):
        m = new_lmotpat.match(card) # Try to read card with new format
        if not m:
            m = old_lmotpat.match(card) # If that didn't work, read card with old format
        ngroups = len(m.groups()) # ngroups = number of fields found (12 = old, 15 = new)
        name = m.group(1)
        name = self.decode_name(name)
        col = map(lambda (x): int(x) / 255.0,
                  [m.group(2), m.group(3), m.group(4)] )
        force = float(m.group(5))
        stiffness = float(m.group(6))
        cxyz = A(map(float, [m.group(7), m.group(8), m.group(9)])) / 1000.0
        axyz = A(map(float, [m.group(10), m.group(11), m.group(12)])) / 1000.0
        if ngroups == 15: # if we have 15 fields, we have the length, width and spoke radius.
            length = float(m.group(13))
            width = float(m.group(14))
            sradius = float(m.group(15))
        else: # if not, set the default values for length, width and spoke radius.
            length = 10.0
            width = 2.0
            sradius = 0.5
        motor = LinearMotor(self.assy)
        props = name, col, force, stiffness, cxyz, axyz, length, width, sradius
        motor.setProps(props)
        self.addmotor(motor)

    def _read_gridplane(self, card):
        """
        Read the MMP record for a Grid Plane jig as:
        
        gridplane (name) (r, g, b) width height (cx, cy, cz) (w, x, y, z) grid_type line_type x_space y_space (gr, gg, gb) 
        """
        m = gridplane_pat.match(card)
        name = m.group(1)
        name = self.decode_name(name)
        border_color = map(lambda (x): int(x) / 255.0, [m.group(2), m.group(3), m.group(4)])
        width = float(m.group(5)); height = float(m.group(6)); 
        center = A(map(float, [m.group(7), m.group(8), m.group(9)]))
        quat = A(map(float, [m.group(10), m.group(11), m.group(12), m.group(13)]))
        grid_type = int(m.group(14)); line_type = int(m.group(15)); x_space = float(m.group(16)); y_space = float(m.group(17))
        grid_color = map(lambda (x): int(x) / 255.0, [m.group(18), m.group(19), m.group(20)])
        
        gridPlane = GridPlane(self.assy, [], READ_FROM_MMP = True)
        gridPlane.setProps(name, border_color, width, height, center, quat, grid_type, \
                           line_type, x_space, y_space, grid_color)
        self.addmember(gridPlane)
    
    #Read mmp record for a Reference Plane
    def _read_plane(self, card):
        """
        Read the MMP record for a Reference Plane as:

        plane (name) (r, g, b) width height (cx, cy, cz) (w, x, y, z) 
        """
        m = plane_pat.match(card)
        name = m.group(1)
        name = self.decode_name(name)
        #border_color = color of the border for front side of the reference plane. 
        #user can't set it for now. -- ninad 20070104
        border_color = map(lambda (x): int(x) / 255.0, [m.group(2), m.group(3), m.group(4)])
        width = float(m.group(5)); height = float(m.group(6)); 
        center = A(map(float, [m.group(7), m.group(8), m.group(9)]))
        quat = A(map(float, [m.group(10), m.group(11), m.group(12), m.group(13)]))
        
        #@@HACK: Plane.setProps() accepts a tuple that must also contain values 
        #for the grid related attrs such as gridColor, gridLineType etc. 
        #But as of 2008-06-25 those (new) attrs are set using self.set_info_object 
        #(see Plane.readmmp_info_plane_setitem)  because they are a part of 
        #'info' record (which allows upward and backword compatibility for reading
        #mmp files of different versions.) There is a spacial (old) code 
        # to handle those info records. To satisfy that code as well as the 
        #Plane.setProps() API method, we do the following -- 1. We pass 'None' 
        #for the items, in the  'props' tuple that are a part of info record 
        #2. Note that the info record will be read afterwords in this method
        #3. Plane.setProps takes extra precaution to check if the passed 
        #parameter is None (and set its attrs only when that param is not None)
        # --  Ninad 2008-06-25
        gridColor = None        
        gridLineType = None
        gridXSpacing = None
        gridYSpacing = None
        originLocation = None
        displayLabelStyle = None
        
        plane = Plane(self.assy.w, READ_FROM_MMP = True)
        props = (name, border_color, width, height, center, quat,  
                 gridColor, gridLineType, gridXSpacing, gridYSpacing, 
                 originLocation, displayLabelStyle)
        plane.setProps(props)
        self.addmember(plane)
        
        #This sets the Plane attrs such as gridColor, gridLineType etc. 
        self.set_info_object('plane', plane)

    # Read the MMP record for a Atom Set as:
    # atomset (name) atom1 atom2 ... atom_n {no limit}

    def _read_atomset(self, card):
        m = atomsetpat.match(card)
        name = m.group(1)
        name = self.decode_name(name)
        col = map(lambda (x): int(x) / 255.0,
                  [m.group(2), m.group(3), m.group(4)] )

        # Read in the list of atoms
        card = card[card.index(")") + 1:] # skip past the color field
        list1 = map(int, re.findall("\d+", card[card.index(")") + 1:]))
        list1 = map((lambda n: self.ndix[n]), list1)
        
        atomset = AtomSet(self.assy, list1) # create atom set and set props
        atomset.name = name
        atomset.color = col
        self.addmember(atomset)
        
    def _read_espimage(self, card):
        """
        Read the MMP record for an ESP Image jig as:

        espimage (name) (r, g, b) width height resolution (cx, cy, cz) 
        (w, x, y, z) trans (fr, fg, fb) show_bbox win_offset edge_offset.
        """
        m = esppat.match(card)
        name = m.group(1)
        name = self.decode_name(name)
        border_color = map(lambda (x): int(x) / 255.0, [m.group(2), m.group(3), m.group(4)])
        width = float(m.group(5)); height = float(m.group(6)); resolution = int(m.group(7))
        center = A(map(float, [m.group(8), m.group(9), m.group(10)]))
        quat = A(map(float, [m.group(11), m.group(12), m.group(13), m.group(14)]))
        trans = float(m.group(15))
        fill_color = map(lambda (x): int(x) / 255.0, [m.group(16), m.group(17), m.group(18)])
        show_bbox = int(m.group(19))
        win_offset = float(m.group(20)); edge_offset = float(m.group(21))
        
        espImage = ESPImage(self.assy, [], READ_FROM_MMP = True)
        espImage.setProps(name, border_color, width, height, resolution, center, quat, trans, fill_color, show_bbox, win_offset, edge_offset)
        self.addmember(espImage)

        # for interpreting "info espimage" records:
        self.set_info_object('espimage', espImage)
        return

    _read_espwindow = _read_espimage
        #bruce 060207 help fix bug 1357 (read older mmprecord for ESP Image, for compatibility with older bug report attachments)
        # (the fix also required a change to esppat)
        # (this can be removed after A7 is released, but for now it's convenient to have it so old bug reports remain useful)
    
    # Read the MMP record for a Ground (Anchor) as:
    # ground (name) (r, g, b) atom1 atom2 ... atom25 {up to 25}

    def _read_ground(self, card): # see also _read_anchor
        m = groundpat.match(card)
        name = m.group(1)
        name = self.decode_name(name)
        col = map(lambda (x): int(x) / 255.0,
                  [m.group(2), m.group(3), m.group(4)] )

        # Read in the list of atoms
        card = card[card.index(")") + 1 :] # skip past the color field
        list1 = map(int, re.findall("\d+", card[card.index(")") + 1 :]))
        list1 = map((lambda n: self.ndix[n]), list1)
        
        gr = Anchor(self.assy, list1) # create ground and set props
        gr.name = name
        gr.color = col
        self.addmember(gr)

    _read_anchor = _read_ground #bruce 060228 (part of making anchor work when reading future mmp files, before prerelease snapshots)

    # Read the MMP record for a MeasureDistance, wware 051103
    # mdistance (name) (r, g, b) (font_name) font_size a1 a2
    # no longer modeled on motor, wware 051103
    def _read_mdistance(self, card):
        from model.jigs_measurements import MeasureDistance
        m = mdistancepat.match(card) # Try to read card
        assert len(m.groups()) == 8
        name = m.group(1)
        name = self.decode_name(name)
        col = map(lambda (x): int(x) / 255.0,
                  [m.group(2), m.group(3), m.group(4)] )
        font_name = m.group(5)
        font_size = int(m.group(6))
        atomlist = map(int, [m.group(7), m.group(8)])
        lst = map(lambda n: self.ndix[n], atomlist)
        mdist = MeasureDistance(self.assy, [ ])
        mdist.setProps(name, col, font_name, font_size, lst)
        self.addmember(mdist)

    # Read the MMP record for a MeasureAngle, wware 051103
    # mangle (name) (r, g, b) (font_name) font_size a1 a2 a3
    # no longer modeled on motor, wware 051103
    def _read_mangle(self, card):
        m = manglepat.match(card) # Try to read card
        assert len(m.groups()) == 9
        name = m.group(1)
        name = self.decode_name(name)
        col = map(lambda (x): int(x) / 255.0,
                  [m.group(2), m.group(3), m.group(4)] )
        font_name = m.group(5)
        font_size = int(m.group(6))
        atomlist = map(int, [m.group(7), m.group(8), m.group(9)])
        lst = map(lambda n: self.ndix[n], atomlist)
        mang = MeasureAngle(self.assy, [ ])
        mang.setProps(name, col, font_name, font_size, lst)
        self.addmember(mang)

    # Read the MMP record for a MeasureDistance, wware 051103
    # mdihedral (name) (r, g, b) (font_name) font_size a1 a2 a3 a4
    # no longer modeled on motor, wware 051103
    def _read_mdihedral(self, card):
        m = mdihedralpat.match(card) # Try to read card
        assert len(m.groups()) == 10
        name = m.group(1)
        name = self.decode_name(name)
        col = map(lambda (x): int(x) / 255.0,
                  [m.group(2), m.group(3), m.group(4)] )
        font_name = m.group(5)
        font_size = int(m.group(6))
        atomlist = map(int, [m.group(7), m.group(8), m.group(9), m.group(10)])
        lst = map(lambda n: self.ndix[n], atomlist)
        mdih = MeasureDihedral(self.assy, [ ])
        mdih.setProps(name, col, font_name, font_size, lst)
        self.addmember(mdih)

    def read_new_jig(self, card, constructor): #bruce 050701
        """
        Helper method to read any sort of sufficiently new jig from an mmp file
        and add it to self.assy using self.addmember.
        
        Args are:
        card - the mmp file line.
        constructor - function that takes assy and atomlist and makes a new jig, without putting up any dialog.
        """
        # this method will give one place to fix things in the future (for new jig types),
        # like the max number of atoms per jig.
        recordname, rest = card.split(None, 1)
        del recordname
        card = rest
        
        m = jigpat.match(card)
        name = m.group(1)
        name = self.decode_name(name)
        col = map(lambda (x): int(x) / 255.0,
                  [m.group(2), m.group(3), m.group(4)] )

        # Read in the list of atoms
        # [max number of atoms used to be limited by max mmp-line length
        #  of 511 bytes; I think that limit was removed long ago
        #  but this should be verified (in cad and sim readers)
        #  [bruce 080227 comment]]
        card = card[card.index(")") + 1:] # skip past the color field
        list1 = map(int, re.findall("\d+", card[card.index(")") + 1:]))
        list1 = map((lambda n: self.ndix[n]), list1)
        
        jig = constructor(self.assy, list1) # create jig and set some properties -- constructor must not put up a dialog
        jig.name = name
        jig.color = col
            # (other properties, if any, should be specified later in the file by some kind of "info" records)
        self.addmember(jig)
        return jig
        
    # Read the MMP record for a Thermostat as:
    # stat (name) (r, g, b) (temp) first_atom last_atom box_atom
    
    def _read_stat(self, card):
        m = statpat.match(card)
        name = m.group(1)
        name = self.decode_name(name)
        col = map(lambda (x): int(x) / 255.0,
                  [m.group(2), m.group(3), m.group(4)] )
        temp = m.group(5)

        # Read in the list of atoms
        card = card[card.index(")") + 1:] # skip past the color field
        card = card[card.index(")") + 1:] # skip past the temp field
        list1 = map(int, re.findall("\d+", card[card.index(")") + 1:]))
        
        # We want "list1" to contain only the 3rd item, so let's remove 
        # first_atom (1st item) and last_atom (2nd item) in list1.
        # They will get regenerated in the Thermo constructor.  
        # Mark 050129
        if len(list1) > 2:
            del list1[0:2]
        
        # Now remove everything else from list1 except for the boxed_atom.
        # This would happen if we loaded an old part with more than 3 atoms listed.
        if len(list1) > 1:
            del list1[1:]
            msg = "a thermostat record was found (" + name + ") in the part which contained extra atoms.  They will be ignored."
            self.warning(msg)
            
        list1 = map((lambda n: self.ndix[n]), list1)

        sr = Stat(self.assy, list1) # create stat and set props
        sr.name = name
        sr.color = col
        sr.temp = temp
        self.addmember(sr)

    # Read the MMP record for a Thermometer as:
    # thermo (name) (r, g, b) first_atom last_atom box_atom
            
    def _read_thermo(self, card):
        m = thermopat.match(card)
        name = m.group(1)
        name = self.decode_name(name)
        col = map(lambda (x): int(x) / 255.0,
                  [m.group(2), m.group(3), m.group(4)] )

        # Read in the list of atoms
        card = card[card.index(")") + 1:] # skip past the color field
        list1 = map(int, re.findall("\d+", card[card.index(")") + 1:]))
        
        # We want "list1" to contain only the 3rd item, so let's remove 
        # first_atom (1st item) and last_atom (2nd item) in list1.
        # They will get regenerated in the Thermo constructor.  
        # Mark 050129
        if len(list1) > 2:
            del list1[0:2]
        
        # Now remove everything else from list1 except for the boxed_atom.
        # This would happen if we loaded an old part with more than 3 atoms listed.
        if len(list1) > 1:
            del list1[1:]
            msg = "a thermometer record was found in the part which contained extra atoms.  They will be ignored."
            self.warning(msg)
            
        list1 = map((lambda n: self.ndix[n]), list1)

        sr = Thermo(self.assy, list1) # create stat and set props
        sr.name = name
        sr.color = col
        self.addmember(sr)
    
    def _read_namedview(self, card):
        """
        Read the MMP record for a I{namedview} as:

        namedview (name) (quat.w, quat.x, quat.y, quat.z) (scale) (pov.x, pov.y, pov.z) (zoom factor)
        
        @note: Currently, "namedview" records are treated as an alias for the
        "csys" record. The writer NamedView.writemmp() will switch to writing
        "namedview" records (instead of "csys") soon.
        Mark 2008-02-07.
        """
        #bruce 050418 revising this to not have side effects on assy.
        # Instead, caller can do that by scanning the group these are read into.
        # This means we can now ignore the isInsert flag and always return
        # these records. Finally, I'll return them all, not just the ones with
        # special names we recognize (the prior code only called self.addmember
        # if the namedview name was HomeView or LastView); caller can detect 
        # those special names when it needs to.       
        m = namedviewpat.match(card)      
        name = m.group(1)
        name = self.decode_name(name)
        wxyz = A(map(float, [m.group(2), m.group(3), m.group(4), m.group(5)]))
        scale = float(m.group(6))
        pov = A(map(float, [m.group(7), m.group(8), m.group(9)]))
        zoomFactor = float(m.group(10))
        namedView = NamedView(self.assy, name, scale, pov, zoomFactor, wxyz)
        self.addmember(namedView) 
            # regardless of name; no side effects on assy (yet) for any name,
            # though later code will recognize the names HomeView and LastView
            # and treat them specially.
            # (050421 extension: also some related names, for Part views)
                
    def _read_csys(self, card): # csys -- really a named view.
        """
        Read the MMP record for a I{csys} as:
        
        csys (name) (quat.w, quat.x, quat.y, quat.z) (scale) (pov.x, pov.y, pov.z) (zoom factor)
        
        @note: Currently, "namedview" records are treated as an alias for the
        "csys" record. The writer NamedView.writemmp() will switch to writing
        "namedview" records (instead of "csys") soon.
        Mark 2008-02-07.
        """
        #bruce 050418 revising this to not have side effects on assy.
        # Instead, caller can do that by scanning the group these are read into.
        # This means we can now ignore the isInsert flag and always return
        # these records. Finally, I'll return them all, not just the ones with
        # special names we recognize (the prior code only called self.addmember
        # if the csys name was HomeView or LastView); caller can detect those
        # special names when it needs to.
        ## if not self.isInsert: #Skip this record if inserting
        ###Huaicai 1/27/05, new file format with home view 
        ### and last view information        
        m = new_csyspat.match(card)
        if m:        
            name = m.group(1)
            name = self.decode_name(name)
            wxyz = A(map(float, [m.group(2), m.group(3),
                                 m.group(4), m.group(5)] ))
            scale = float(m.group(6))
            pov = A(map(float, [m.group(7), m.group(8), m.group(9)]))
            zoomFactor = float(m.group(10))
            namedView = NamedView(self.assy, name, scale, pov, zoomFactor, wxyz)
            self.addmember(namedView) 
                # regardless of name; no side effects on assy (yet) for any
                # name, though later code will recognize the names HomeView and 
                # LastView and treat them specially
                # (050421 extension: also some related names, for Part views)
        else:
            m = old_csyspat.match(card)
            if m:
                name = m.group(1)
                name = self.decode_name(name)
                wxyz = A(map(float, [m.group(2), m.group(3),
                                     m.group(4), m.group(5)] ))
                scale = float(m.group(6))
                homeView = NamedView(self.assy, "OldVersion", scale, V(0,0,0), 1.0, wxyz)
                    #bruce 050417 comment
                    # (about Huaicai's preexisting code, some of which I moved into this file 050418):
                    # this name "OldVersion" is detected in fix_assy_and_glpane_views_after_readmmp
                    # (called from MWsemantics.fileOpen, one of our callers)
                    # and changed to "HomeView", also triggering other side effects on glpane at that time.
                lastView = NamedView(self.assy, "LastView", scale, V(0,0,0),
                                     1.0, A([0.0, 1.0, 0.0, 0.0]) )
                self.addmember(homeView)
                self.addmember(lastView)
            else:
                print "bad format in csys record, ignored:", card
        return

    def _read_datum(self, card): # datum -- Datum object -- old version deprecated by bruce 050417
        pass # don't warn about an unrecognized mmp record, even when atom_debug

    def addmember(self, thing): #bruce 050405 split this out
        self.groupstack[-1].addchild(thing)
        
    def _read_waals(self, card): # waals -- van der Waals Interactions
        pass # code was wrong -- to be implemented later
        
    def _read_kelvin(self, card): # kelvin -- Temperature in Kelvin (simulation parameter)
        if not self.isInsert: # Skip this record if inserting
            m = re.match("kelvin (\d+)", card)
            n = int(m.group(1))
            self.assy.temperature = n
            
    def _read_mmpformat(self, card): # mmpformat -- MMP File Format. Mark 050130
        # revised by bruce 080328
        m = re.match("mmpformat (.*)", card)
        mmpformat = m.group(1)
        if not self.isInsert: # Skip this side effect if inserting
            self.assy.mmpformat = mmpformat
        # warn if format might be too new, or if we can't understand
        # mmpformat record at all [new feature, bruce 080328]
        okjunk, can_read_required, can_read_preferred = parse_mmpformat( _mmp_format_version_we_can_read() )
        assert okjunk
        msg = ""
        wrapper = None
        try:
            ok, required, preferred = parse_mmpformat(mmpformat)
        except:
            print_compact_traceback("exception or syntax error while parsing [%s]: " % card.strip())
            msg = "Error: bug or syntax error in mmpformat record, file corrupt or too new: [%s]" % \
                      quote_html(card.strip())
            wrapper = redmsg
        else:
            if not ok:
                msg = "Warning: can't parse mmpformat record, file might be too new: [%s]" % \
                      quote_html(card.strip())
                wrapper = redmsg # intentional, not a typo: a red "Warning"
            elif mmp_date_newer(required, can_read_required):
                msg = "Warning: mmpformat requires newer reading code; essential data might be unreadable: [%s]" % \
                      quote_html(card.strip())
                wrapper = redmsg
            elif mmp_date_newer(preferred, can_read_preferred):
                msg = "Note: mmpformat prefers newer reading code for some data: [%s]" % \
                      quote_html(card.strip())
                wrapper = orangemsg
            pass
        if wrapper:
            msg = wrapper(msg)
        if msg:
            env.history.message(msg)
        return

    def _read_end1(self, card): # end1 -- End of main tree
        pass

    def _read_end(self, card): # end -- end of file
        pass
    
    def _read_info(self, card):
        #bruce 050217 new mmp record, for optional info about
        # various types of objects which occur earlier in the file
        # (what I mean by "optional" is that it's never an error for the
        #  specified type of thing or type of info to not be recognized,
        #  as can happen when a new file is read by older code)
        
        # Find current chunk -- how we do this depends on details of
        # the other mmp-record readers in this big if/elif statement,
        # and is likely to need changing sometime. It's self.prevchunk.
        # Now make dict of all current items that info record might refer to.
        currents = dict(
            chunk = self.prevchunk,
            opengroup = self.groupstack[-1], #bruce 050421
            leaf = ([None] + self.groupstack[-1].members)[-1], #bruce 050421
            atom = self.prevatom, #bruce 050511
        )
        currents.update( self._info_objects) #bruce 071017 
        interp = mmp_interp(self.ndix, self.markers) #e could optim by using the same object each time [like 'self']
        readmmp_info(card, currents, interp) # has side effect on object referred to by card
        return

    def set_info_object(self, kind, model_component): #bruce 071017
        if kind not in KNOWN_INFO_KINDS:
            # for motivation, see comment next to definition of KNOWN_INFO_KINDS
            # [bruce 071023]
            print_compact_stack( "warning: unrecognized info kind, %r: " % (kind,) )
        self._info_objects[kind] = model_component
        return
    
    def _read_forward_ref(self, card):
        """
        forward_ref (%s) ...
        """
        # add a marker which can be used later to insert the target node in the right place,
        # and also remember the marker here in the mapping (so we can offer that service) ###doc better
        lp_id_rp = card.split()[1]
        assert lp_id_rp[0] + lp_id_rp[-1] == "()"
        ref_id = lp_id_rp[1:-1]
        marker = _MarkerNode(self.assy, ref_id) # note: we remove this if not used, so its node type might not matter much.
        self.addmember(marker)
        self.markers[ref_id] = marker

    def guess_sim_input(self, type): #bruce 050405
        """
        Caller finds (and will correct) weird structure which makes us guess
        this is a sim input file of the specified type;
        warn user if you have not already given the same warning
        (normally only one such warning should appear, so warn about that as well).
        """
        # once we see how this is used, we'll revise it to be more like a "state machine"
        # knowing the expected behavior for the various types of files.
        bad_to_worse = ['no_shelf', 'one_part', 'missing_group_or_chunk'] # order is not yet used
        badness = bad_to_worse.index(type)
        if badness not in self.sim_input_badnesses_so_far:
            self.sim_input_badnesses_so_far[badness] = type
            if type == 'missing_group_or_chunk' or type == 'one_part':
                # (this message is a guess, since erroneous files could give rise to this too)
                # (#e To narrow down the cmd that wrote it, we'd need more info than type or
                #  even file comment, from old files -- sorry; maybe we should add an mmp record for
                #  the command that made the file! Will it be bad that file contents are nondet (eg for md5)?
                #  Probably not, since they were until recently anyway, due to dict item arb order.)
                msg = "mmp file probably written by Adjust or Minimize or Simulate -- " \
                      "lacks original file's chunk/group structure and display modes; " \
                      "unreadable by pre-Alpha5 versions unless resaved." #e revise version name
            elif type == 'no_shelf':
                # (this might not happen at all for files written by Alpha5 and beyond)
                # comment from old code's fix_grouplist:
                #bruce 050217 upward-compatible reader extension (needs no mmpformat-record change):
                # permit missing 3rd group, so we can read mmp files written as input for the simulator
                # (and since there is no good reason not to!)
                msg = "this mmp file was written as input for the simulator, and contains no clipboard items" #e add required version
            else:
                msg = "bug in guess_sim_input: missing message for %r" % type
            self.warning( msg)
            # normally only one of these warnings will occur, so we also ought to warn if that is not what happens...
            if len(self.sim_input_badnesses_so_far) > 1:
                self.format_error("the prior warnings should not appear together for the same file")
        return
    
    pass # end of class _readmmp_state

# helper for forward_ref

class _MarkerNode(Node):
    def __init__(self, assy, ref_id):
        name = ref_id
        Node.__init__(self, assy, name) # will passing no assy be legal? I doubt it... so don't bother trying.
        return
    pass

# helpers for _read_info method:

class mmp_interp: #bruce 050217; revised docstrings 050422
    """
    helps translate object refs in mmp file to their objects, while reading the file
    [compare to class writemmp_mapping, which helps turn objs to their refs while writing the file]
    [but also compare to class _readmmp_state... maybe this should be the same object as that. ###k]
    [also has decode methods, and some external code makes one of these just to use those (which is a kluge).]
    """
    # make these helper functions available as if they were methods,
    # to permit clients to avoid import cycles
    # which would occur if they imported them directly from this file
    decode_atom_coordinate = staticmethod( decode_atom_coordinate)
    decode_atom_coordinates = staticmethod( decode_atom_coordinates)
    
    def __init__(self, ndix, markers):
        self.ndix = ndix # maps atom numbers to atoms (??)
        self.markers = markers
    def atom(self, atnum):
        """
        map atnum string to atom, while reading mmp file
        (raises KeyError if that atom-number isn't recognized,
         which is an mmp file format error)
        """
        return self.ndix[int(atnum)]
    def move_forwarded_node( self, node, val):
        """
        find marker based on val, put node after it, and then del the marker
        """
        try:
            marker = self.markers.pop(val) # val is a string; should be ok since we also read it as string from forward_ref record
        except KeyError:
            assert 0, "mmp format error: no forward_ref was written for this forwarded node" ###@@@ improve errmsg
        marker.addsibling(node)
        marker.kill()
    def decode_int(self, val): #bruce 050701; should be used more widely
        """
        helper method for parsing info records; returns an int or None;
        warns of unrecognized values only if ATOM_DEBUG is set
        """
        try:
            assert val.isdigit() or (val[0] == '-' and val[1:].isdigit())
            return int(val)
        except:
            # several kinds of exception are possible here, which are not errors
            if debug_flags.atom_debug:
                print "atom_debug: fyi: some info record wants an int val but got this non-int (not an error): " + repr(val)
                # btw, the reason it's not an error is that the mmp file format might be extended to permit it, in that info record.
            return None
        pass
    def decode_bool(self, val): #bruce 050701; should be used more widely
        """
        helper method for parsing info records; returns True or False or None;
        warns of unrecognized values only if ATOM_DEBUG is set
        """
        val = val.lower()
        if val in ['0', 'no', 'false']:
            return False
        if val in ['1', 'yes', 'true']:
            return True
        if debug_flags.atom_debug:
            print "atom_debug: fyi: some info record wants a boolean val but got this instead (not an error): " + repr(val)
        return None
    pass # end of class mmp_interp

def mmp_interp_just_for_decode_methods(): #bruce 050704
    """
    Return an mmp_interp object usable only for its decode methods (kluge)
    """
    return mmp_interp("not used", "not used")

def readmmp_info( card, currents, interp ): #bruce 050217; revised 050421, 050511
    """
    Handle an info record 'card' being read from an mmp file;
    currents should be a dict from thingtypes to the current things of those types,
    for all thingtypes which info records can give info about
    (including 'chunk', 'opengroup', 'leaf', 'atom');
    interp should be an mmp_interp object #doc.

    The side effect of this function, when given "info <type> <name> = <val>",
    is to tell the current thing of type <type> (that is, the last one read from this file)
    that its optional info <name> has value <val>,
    using a standard info-accepting method on that thing.
    <type> should be a "word";
    <name> should be one or more "words"
    (it's supplied as a python list of strings to the info-accepting method);
    <val> can be (for now) any string with no newlines,
    and no whitespace at the ends; its permissible syntax might be further restricted later.
    """
    #e interface will need expanding when info can be given about non-current things too
    what, val = card.split('=', 1)
    key = "info"
    what = what[len(key):]
    what = what.strip() # e.g. "chunk xxx" for info of type xxx about the current chunk
    val = val.strip()
    what = what.split() # e.g. ["chunk", "xxx"], always 2 or more words
    type = what[0] # as of 050511 this can be 'chunk' or 'opengroup' or 'leaf' or 'atom'
    name = what[1:] # list of words (typically contains exactly one word, an attribute-name)
    thing = currents.get(type)
    if thing: # can be false if type not recognized, or if current one was None
        # record info about the current thing of type <type>
        try:
            meth = getattr(thing, "readmmp_info_%s_setitem" % type) # should be safe regardless of the value of 'type'
        except AttributeError:
            if debug_flags.atom_debug:
                print "atom_debug: fyi: object %r doesn't accept \"info %s\" keys (like %r); ignoring it (not an error)" \
                      % (thing, type, name)
        else:
            try:
                meth( name, val, interp )
            except:
                print_compact_traceback("internal error in %r interpreting %r, ignored: " % (thing, card) )
    elif debug_flags.atom_debug:
        print "atom_debug: fyi: no object found for \"info %s\"; ignoring info record (not an error)" % (type,)
    return

# ==

_readmmp_aborted = False

_reference_to_readmmp_abort_function = None #bruce 080606 precaution

def _readmmp(assy, filename, isInsert = False, showProgressDialog = False): 
    """
    Read an mmp file, print errors and warnings to history,
    modify assy in various ways (a bad design, see comment in insertmmp)
    (but don't actually add file contents to assy -- let caller do that if and
    where it prefers), and return (as part of a larger tuple described below)
    either None (after an error for which caller should store no file contents
    at all) or a list of 3 Groups, which caller should treat as having roles
    "viewdata", "tree", "shelf", regardless of how many toplevel items were
    in the file, or of whether they were groups.
    (We handle normal mmp files with exactly those 3 groups, old sim-input
    files with only the first two, and newer sim-input files for Parts 
    (one group) or for minimize selection (maybe no groups at all). And most 
    other weird kinds of mmp files someone might create.)

    @warning: the optional arguments are sometimes passed positionally.
    
    @param assy: the assembly the file contents are being added into
    @type  assy: assembly.assembly
    
    @param filename: where the data will be read from
    @type  filename: string
    
    @param isInsert: if True, the file contents are being added to an
                     existing assembly, otherwise the file contents are being
                     used to initialize a new assembly.
    @type  isInsert: boolean
    
    @param showProgressDialog: if True, display a progress dialog while reading
                               a file. Default is False.
    @type  showProgressDialog: boolean

    @return: the tuple (ok, grouplist or None, listOfAtomsInFileOrder), where
             ok is one of the string constants named (in utilities.constants)
             SUCCESS, ABORTED, or READ_ERROR. (If ok is not SUCCESS, grouplist
             will be None and listOfAtomsInFileOrder will be [], but callers
             will be cleaner if they don't rely on this.)
    @rtype: (string, list, list)
    """
    #bruce 050405 revised code & docstring
    #ericm 080409 revised return value to contain listOfAtomsInFileOrder
    #bruce 080502 documented return value; fixed it when file is empty
    
    state = _readmmp_state( assy, isInsert)
    
    # The following code is experimental. It reads an mmp file that is contained
    # within a ZIP file. To test, create a zipfile (i.e. "part.zip") which
    # contains an MMP file named "main.mmp", then rename "part.zip" to 
    # "part.mmp". Set the constant READ_MAINMMP_FROM_ZIPFILE = True,
    # then run NE1 and open "part.mmp" using "File > Open...". 
    # Mark 2008-02-03
    READ_MAINMMP_FROM_ZIPFILE = False # Don't commit with True.
    
    if READ_MAINMMP_FROM_ZIPFILE:
        # Experimental. Read "main.mmp", a standard mmp file contained within 
        # a zipfile opened via "File > Open...".
        from zipfile import ZipFile
        _zipfile = ZipFile(filename, 'r')
        _bytes = _zipfile.read("main.mmp")
        lines = _bytes.splitlines()
    else:
        # The normal way to read an MMP file.
        try:
            lines = open(filename,"rU").readlines()
            # 'U' in filemode is for universal newline support
        except:
            return READ_ERROR, None, []
    
    # Commented this out since the assy.filename should be (and is) set by 
    # another caller based on success.
    #if not isInsert:
    #    assy.filename = filename ###e would it be better to do this at the end, and not at all if we fail?

    global _readmmp_aborted
    global _reference_to_readmmp_abort_function

    _readmmp_aborted = False #bruce 080606 bugfix or precaution
    
    # Create and display a Progress dialog while reading the MMP file. 
    # One issue with this implem is that QProgressDialog always displays 
    # a "Cancel" button, which is not hooked up. I think this is OK for now,
    # but later we should either hook it up or create our own progress
    # dialog that doesn't include a "Cancel" button. --mark 2007-12-06
    if showProgressDialog:
        kluge_main_assy = env.mainwindow().assy
            # see comment about kluge_main_assy elsewhere in this file
            # [bruce 080319]
        assert not kluge_main_assy.assy_valid #bruce 080117
        _progressValue = 0
        _progressFinishValue = len(lines)
        win = env.mainwindow()
        win.progressDialog.setLabelText("Reading file...")
        win.progressDialog.setRange(0, _progressFinishValue)
        _progressDialogDisplayed = False
        _timerStart = time.time()
        
        def abort_readmmp():
            """
            This slot is called when the user aborts opening a large
            MMP file by pressing the "Cancel" button in the progress dialog.
            """
            try:
                print "cancelled reading file"
                global _readmmp_aborted
                _readmmp_aborted = True
                win.disconnect(win.progressDialog, SIGNAL("canceled()"), abort_readmmp)
                    # review: why no NameError for this abort_readmmp?
                    # guess: it's a legal read-only reference into the
                    # outer function's scope. But it's also possible that we have
                    # an exception and don't see it, so I'm adding some prints to
                    # find out, and try/except. These can remain since they are
                    # harmless. [bruce 080606]
                print " (returning from abort_readmmp)"
            except:
                print_compact_traceback("exception in abort_readmmp ignored: ")
            return
        
        from PyQt4.Qt import SIGNAL
        win.connect(win.progressDialog, SIGNAL("canceled()"), abort_readmmp)

        _reference_to_readmmp_abort_function = abort_readmmp
            # make sure abort_readmmp doesn't get deallocated before use
            # [bruce 080606 precaution]

        pass
    
    for card in lines:
        if _readmmp_aborted: # User aborted while reading the MMP file.
            _readmmp_aborted = False # (precaution, not really needed, since not
                # sufficient to replace the reset earlier in this function)
            return ABORTED, None, []
        try:
            errmsg = state.readmmp_line( card) # None or an error message
        except:
            # note: the following two error messages are similar but not identical
            errmsg = "bug while reading this mmp line: %s" % (card,) #e include line number; note, two lines might be identical
            print_compact_traceback("bug while reading this mmp line:\n  %s\n" % (card,) )
        #e assert errmsg is None or a string
        if errmsg:
            ###e general history msg for stopping early on error
            ###e special return value then??
            break
        
        if showProgressDialog: # Update the progress dialog.
            _progressValue += 1
            if _progressValue >= _progressFinishValue:
                win.progressDialog.setLabelText("Building model...")
            elif _progressDialogDisplayed:
                win.progressDialog.setValue(_progressValue)
                    # WARNING: this can directly call glpane.paintGL!
                    # So can the other 2 calls here of progressDialog.setValue.
                    # To prevent bugs or slowdowns from drawing incomplete
                    # models or from trying to run updaters (or take undo
                    # checkpoints?) before drawing them, the GLPane now checks
                    # kluge_main_assy.assy_valid to prevent redrawing when this happens.
                    # (Does ThumbView also need this fix?? ### REVIEW)
                    # [bruce 080117 comment / bugfix]                    
            else:
                _timerDuration = time.time() - _timerStart
                if _timerDuration > 0.25: 
                    # Display progress dialog after 0.25 seconds
                    win.progressDialog.setValue(_progressValue)
                    _progressDialogDisplayed = True
        
    grouplist = state.extract_toplevel_items() # for a normal mmp file this has 3 Groups, whose roles are viewdata, tree, shelf

    # now fix up sim input files and other nonstandardly-structured files;
    # use these extra groups if necessary, else discard them:
    viewdata = Group("Fake View Data", assy, None) # name is never used or stored
    shelf = Group("Clipboard", assy, None) # name might not matter since caller resets it
    
    for g in grouplist:
        if not g.is_group(): # might happen for files that ought to be 'one_part', too, I think, if clipboard item was not grouped
            state.guess_sim_input('missing_group_or_chunk') # normally same warning already went out for the missing chunk 
            tree = Group("tree", assy, None, grouplist)
            grouplist = [ viewdata, tree, shelf ]
            break
    if len(grouplist) == 0:
        state.format_error("nothing in file")
        return SUCCESS, None, []
            ### REVIEW: this is really a file format error;
            # what return code is best? need a new one?
            # guess: READ_ERROR would be best; needs analysis.
            # [bruce 080606 Q]
    elif len(grouplist) == 1:
        state.guess_sim_input('one_part')
            # note: 'one_part' gives same warning as 'missing_group_or_chunk' as of 050406
        tree = Group("tree", assy, None, grouplist) #bruce 050406 removed [0] to fix bug in last night's new code
        grouplist = [ viewdata, tree, shelf ]
    elif len(grouplist) == 2:
        state.guess_sim_input('no_shelf')
        grouplist.append( shelf)
    elif len(grouplist) > 3:
        state.format_error("more than 3 toplevel groups -- treating them all as in the main part")
            #bruce 050405 change; old code discarded all the data
        tree = Group("tree", assy, None, grouplist)
        grouplist = [ viewdata, tree, shelf ]
    else:
        pass # nothing was wrong!
    assert len(grouplist) == 3

    listOfAtomsInFileOrder = state.listOfAtomsInFileOrder
    
    state.destroy() # not before now, since it keeps track of which warnings we already emitted
    
    if showProgressDialog: # Make the progress dialog go away.
        win.progressDialog.setValue(_progressFinishValue)

    _reference_to_readmmp_abort_function = None
    
    return SUCCESS, grouplist, listOfAtomsInFileOrder # from _readmmp

def readmmp(assy,
            filename,
            isInsert = False,
            showProgressDialog = False,
            returnListOfAtoms = False):
    """
    Read an mmp file to create a new model (including a new
    Clipboard).  Returns a tuple described below, which
    contains a grouplist tuple of (viewdata, tree, shelf). If
    isInsert is False (the default), assy will be modified to include
    the new items. (If caller wants assy to be marked as "not modified"
    afterwards, it's up to caller to handle this, e.g. using
    assy.reset_changed().)

    @note: mmp stands for Molecular Machine Part.

    @param assy: the assembly object which the file contents are being added to
    @type  assy: instance of assembly.Assembly
    
    @param filename: where the data will be read from
    @type  filename: string
    
    @param isInsert: if True, the file contents are being added to an
                     existing assembly, otherwise the file contents are being
                     used to initialize a new assembly. When this is true,
                     some behavior is not done, e.g. calling assy.update_parts
                     and all the updaters it normally calls. Doing that
                     (or tolerating not doing it) is up to the caller.
    @type  isInsert: boolean
    
    @param showProgressDialog: if True, display a progress dialog while reading
                               a file. Default is False.
    @type  showProgressDialog: boolean

    @param returnListOfAtoms: if True, return value contains a list of all
                              atoms in the file, in the order they
                              appeared.  If False (the default),
                              return value contains the group list.
                              See return value doc for details.
    @type  returnListOfAtoms: boolean
    
    @return: the tuple (ok, grouplist) or (ok, listOfAtoms)
             (depending on the returnListOfAtoms option)
             where ok is one of the following named string constants
             defined in utilities.constants:
             - SUCCESS
             - ABORTED
             - READ_ERROR
    @rtype:  (string, atom list) or (string, grouplist or None)
    """
    # todo: This interface needs revising and clarifying. Ideally, it should take only
    # a filename as parameter, and return a single data structure (of the same
    # class which we use for "the contents of a model file", more or less)
    # representing the contents of that file, and a success code.
    # (Whether it's practical for the data not to point to "its assy" is
    #  questionable, but can be thought of as an independent issue,
    #  except perhaps for performance considerations for Insert.)
    # Maybe the listOfAtoms should be accessible from the data structure
    # even if it's not identical to "all the atoms ultimately in that
    # structure"; if that's not wise, then returning it separately is ok.
    # [comment probably by EricM; revised/extended by bruce 080606]

    # TODO: clean up return value format to return a tuple of three values,
    # always in the same format (ok, grouplist, listOfAtoms)
    # (just like _readmmp does now). [bruce 080606 suggestion]

    kluge_main_assy = env.mainwindow().assy
        # use this instead of assy to fix logic bug in use of assy_valid flag
        # (explained where it's used in master_model_updater)
        # which would be a potential bug during partlib mmpread
        # [bruce 080319]
    assert kluge_main_assy.assy_valid
    kluge_main_assy.assy_valid = False # disable updaters during _readmmp
        # [bruce 080117/080124, revised 080319]
    try:
        ok, grouplist, listOfAtomsInFileOrder = _readmmp(assy,
                                                         filename,
                                                         isInsert,
                                                         showProgressDialog)
            # warning: can show a dialog, which can cause paintGL calls.
    finally:
        kluge_main_assy.assy_valid = True
    if (not isInsert):
        # NOTE: we want to call this even if ok != SUCCESS,
        # since it detects grouplist is None in that case
        # and has side effects on assy which might be required
        # (needs review to see if they are really required).
        # [bruce 080606 comment]
        _reset_grouplist(assy, grouplist)
            # note: handles grouplist is None (though not very well)
            # note: runs all updaters when done, and sets per-part viewdata
    if (returnListOfAtoms):
        return ok, listOfAtomsInFileOrder
    return ok, grouplist
    
def _reset_grouplist(assy, grouplist):
    """
    [private]

    Stick a new just-read grouplist into assy, within readmmp.

    If grouplist is None, indicating file had bad format,
    do some but not all of the usual side effects.
    [appropriateness of behavior for grouplist is None is unreviewed]

    Otherwise grouplist must be a list of exactly 3 Groups
    (though this is not fully checked here),
    which we treat as viewdata, tree, shelf.

    Changed viewdata behavior 050418:
    We used to assume (if necessary) that viewdata contains Csys records
    which, when parsed during _readmmp, had side effects of storing themselves in assy
    (which was a bad design).
    Now we scan it and perform those side effects ourselves.
    """
    #bruce 050302 split this out of readmmp;
    # it should be entirely rewritten and become an assy method
    #bruce 050418: revising this for assy/part split
    if grouplist is None:
        # do most of what old code did (most of which probably shouldn't be done,
        # but this needs more careful review (especially in case old code has
        # already messed things up by clearing assy), and merging with callers,
        # which don't even check for any kind of error return from readmmp),
        # except (as of 050418) don't do any side effects from viewdata.
        # Note: this is intentionally duplicated with code from the other case,
        # since the plan is to clean up each case independently.
        assy.shelf.name = "Clipboard"
        assy.shelf.open = False
        assy.root = Group("ROOT", assy, None, [assy.tree, assy.shelf])
        assy.kluge_patch_toplevel_groups()
        assy.update_parts()
        return
    viewdata, tree, shelf = grouplist
        # don't yet store these in any Part, since those will all be replaced
        # with new ones by update_parts, below!
    assy.tree = tree
    assy.shelf = shelf
        # below, we'll scan viewdata for Csys records to store into mainpart
    assy.shelf.name = "Clipboard"
    if not assy.shelf.open_specified_by_mmp_file: #bruce 050421 added condition
        assy.shelf.open = False
    assy.root = Group("ROOT", assy, None, [assy.tree, assy.shelf])
    assy.kluge_patch_toplevel_groups()
    
    assy.update_parts( do_special_updates_after_readmmp = True)
        #bruce 080319 added do_special_updates_after_readmmp = True
        # (note: we don't test will_special_updates_after_readmmp_do_anything()
        #  since we have to call this even if they won't.)
        #
        # Note: by the time this is called, our callers as of 080319
        # will have restored kluge_main_assy.assy_valid = True,
        # so updaters run by update_parts (such as dna updater)
        # will *not* be disabled. [bruce 080319 comment]
        #
        #bruce 050309 for assy/part split;
        # 080117 added do_post_event_updates = False;
        # 080124 removed that option (and revised when caller restores
        # kluge_main_assy.assy_valid) to fix recent bug in which all newly read
        # files are recorded as modified; at the time I thought that option was
        # needed for safety, like the disabling of updaters during paintGL is,
        # but a closer analysis of the following code shows that it's not.
        # NOTE: ideally the dna updater would mark the assy as modified even
        # during readmmp, if it had to do "irreversible changes" to fix a pre-
        # updater mmp file containing dna-related objects. To implement that
        # without restoring the just-fixed bug would require the dna updater
        # to know and record the difference in status of the different kinds
        # of updates it does; it would also require some new scheme in this
        # code for having that affect the ultimate value of assy._modified
        # (presently determined by code in our callers). Neither is trivial,
        # both are doable -- not yet clear if it's worth the trouble.
        # [bruce comment 080124]
    
    # Now the parts exist, so it's safe to store the viewdata into the mainpart;
    # this imitates what the pre-050418 code did when the csys records were parsed;
    # note that not all mmp files have anything at all in viewdata
    # (e.g. some sim-input files don't).
    mainpart = assy.tree.part
    for m in viewdata.members:
        if isinstance(m, NamedView):
            if m.name == "HomeView" or m.name == "OldVersion":
                    # "OldVersion" will be changed to "HomeView" later... see comment elsewhere
                mainpart.homeView = m
            elif m.name == "LastView":
                mainpart.lastView = m
            elif m.name.startswith("HomeView"):
                _maybe_set_partview(assy, m, "HomeView", 'homeView')
            elif m.name.startswith("LastView"):
                _maybe_set_partview(assy, m, "LastView", 'lastView')
    return

def _maybe_set_partview( assy, namedView, nameprefix, namedViewattr): #bruce 050421; docstring added 050602
    """
    [private helper function for _reset_grouplist]

    If namedView.name == nameprefix plus a decimal number, store namedView as the attr named namedViewattr
    of the .part of the clipboard item indexed by that number
    (starting from 1, using purely positional indices for clipboard items).
    """
    partnodes = assy.shelf.members
    for i in range(len(partnodes)): #e inefficient if there are a huge number of shelf items...
        if namedView.name == nameprefix + "%d" % (i+1):
            part = partnodes[i].part
            setattr(part, namedViewattr, namedView)
            break
    return

def insertmmp(assy, filename):
    """
    Read an mmp file and insert its main part into the existing model.
    Discards other info from the file it reads, like the clipboard.

    @note: does not emit a history message about its result.
           The caller must do that if desired.

    @return: success_code, which is one of these named string constants
             defined in utilities.constants:
             - SUCCESS
             - ABORTED
             - READ_ERROR
    """
    #bruce 050405 revised to fix one or more assembly/part bugs, I hope
    #bruce 080606 revised to return success code (but didn't yet fix
    # callers to make use of it)
    
    # Note: this is a normal user operation, so there is no need
    # to refrain from setting assy's modified flag.
    kluge_main_assy = env.mainwindow().assy
        # use this instead of assy to fix logic bug in use of assy_valid flag
        # (explained where it's used in master_model_updater)
        # which would be a potential bug during partlib mmpread
        # [bruce 080319]
    assert kluge_main_assy.assy_valid
    kluge_main_assy.assy_valid = False # disable updaters during insert [bruce 080117]
    ok = READ_ERROR
    try:
        ok, grouplist, listOfAtomsInFileOrder = _readmmp(assy, 
                                                         filename, 
                                                         isInsert = True,
                                                         showProgressDialog = True)
        del listOfAtomsInFileOrder        
        
            # isInsert = True prevents most side effects on assy;
            # a better design would be to let the caller do them (or not)
        if ok == SUCCESS and grouplist:
            #bruce 080606 added ok == SUCCESS condition (precaution or cleanup)
            ### TODO: NEEDS ERROR MESSAGE OTHERWISE
            viewdata, mainpart, shelf = grouplist
            del viewdata
            ## not yet (see below): del shelf
            assy.addnode( mainpart) #bruce 060604
    ##        assy.part.ensure_toplevel_group()
    ##        assy.part.topnode.addchild( mainpart )
            #bruce 050425 to fix bug 563:
            # Inserted mainpart might contain jigs whose atoms were in clipboard of inserted file.
            #   Internally, right now, those atoms exist, in legitimate chunks in assy
            # (with a chain of dads going up to 'shelf' (the localvar above), which has no dad),
            # and have not been killed. Bug 563 concerns these jigs being inserted with no provision
            # for their noninserted atoms. It's not obvious what's best to do in this case, but a safe
            # simple solution seems to be to pretend to insert and then delete the shelf we just read,
            # thus officially killing those atoms, and removing them from those jigs, with whatever
            # effects that might have (e.g. removing those jigs if all their atoms go away).
            #   (When we add history messages for jigs which die from losing all atoms,
            # those should probably differ in this case and in the usual case,
            # but those are NIM for now.)
            #   I presume it's ok to kill these atoms without first inserting them into any Part...
            # at least, it seems unlikely to mess up any specific Part, since they're not now in one.
            #e in future -- set up special history-message behavior for jigs killed by this:
            
            shelf.kill()
        

            #e in future -- end of that special history-message behavior

            # run special updaters for readmmp, but (as optim for usual case)
            # only if necessary, since otherwise the normal updater run after
            # we return (which happens since this is a normal user op)
            # will be sufficient. For big files this might be a significant
            # optim (in spite of incremental nature of updater) (guess).
            #
            # note: for readmmp this is done in _reset_grouplist, in a call
            # of update_parts which is always required.
            # [bruce 080319]
            if will_special_updates_after_readmmp_do_anything(assy):
                assy.update_parts( do_special_updates_after_readmmp = True)
            pass
        pass
    finally:
        kluge_main_assy.assy_valid = True
    return ok

def fix_assy_and_glpane_views_after_readmmp( assy, glpane):
    """
    #doc; does gl_update but callers should not rely on that
    """
    #bruce 050418 moved this code (written by Huaicai) out of MWsemantics.fileOpen
    # (my guess is it should mostly be done by readmmp itself);
    # here is Huaicai's comment about it:
    # Huaicai 12/14/04, set the initial orientation to the file's home view orientation 
    # when open a file; set the home view scale = current fit-in-view scale
    #bruce 050418 change this for assembly/part split (per-part Csys attributes)
    mainpart = assy.tree.part
    assert assy.part is mainpart # necessary for glpane view funcs to refer to it (or was at one time)
    if mainpart.homeView.name == "OldVersion": ## old version of mmp file
        mainpart.homeView.name = "HomeView"
        glpane.set_part(mainpart) # also sets view, but maybe not fully correctly in this case ###k
        glpane.quat = Q( mainpart.homeView.quat) # might be redundant with above
        glpane.setViewFitToWindow()
    else:    
        glpane.set_part(mainpart)
        ## done by that: glpane._setInitialViewFromPart( mainpart)
    return

# end