summaryrefslogtreecommitdiff
path: root/cad/src/exprs/scratch/test_animation_mode.py
blob: df702163b740667c34f6cd0a90f774fd27e4d204 (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
"""
test_animation_mode.py -- scratch code for animation loop, connectWithState, exprs drawing.

@author: Bruce
@version: $Id$

To run this code:

1. shell commands to make a symbolic link
(or you can copy it if you don't want to recommit your edits):

% cd ~/Nanorex/Modes
% ln -s /Nanorex/trunk/cad/src/exprs/scratch/test_animation_mode.py .  # or whatever this file's absolute pathname is

2. rerun NE1 (needed?)

3. debug menu -> custom modes -> test_animation_mode

History: this was mostly written long ago in my spare time,
just for fun. I put it in cvs since it's sometimes useful now
as a testbed.

Bugs:

updated 070831

- Needs fix for exit to remove PM tab, like in test_commands

- Performance is worse in NE1 PyOpenGL than in the PyOpenGL I have on the iMac G5
  (the same thing is true for testmode)

- refers to a missing image file on my desktop

(out of date below this)

test_animation_mode bugs as of 060219 night:
- loop stops; should be checkbox
- arrow keys are not camera relative; they're player-rel but player never turns; really they're world-rel (which is wrong)
- camera never tries to swing behind player
- camera dynamics are not very good (should get there faster, stop sooner, maybe have inertia)
- stuff is not in MT or mmp file
- stuff is not very interesting yet

and a couple more noted on g4:
- antialiasing
- a few more
"""

from widgets.prefs_widgets import Preferences_StateRef, Preferences_StateRef_double, ObjAttr_StateRef
from foundation.changes import Formula

## from test_animation_mode_PM import test_animation_mode_PM
from prototype.test_command_PMs import ExampleCommand1_PM
from PM.PM_GroupBox import PM_GroupBox
from PM.PM_DoubleSpinBox import PM_DoubleSpinBox
from PM.PM_PushButton import PM_PushButton
from PM.PM_CheckBox import PM_CheckBox

from command_support.Command import Command
from command_support.GraphicsMode import GraphicsMode

from utilities.debug import print_compact_traceback, register_debug_menu_command
import time, math

from foundation.state_utils import copy_val

from utilities.constants import green, red, white, pink, black, brown, gray # other colors below
from math import pi
from graphics.drawing.CS_draw_primitives import drawline
from graphics.drawing.drawers import drawbrick
from graphics.drawing.CS_draw_primitives import drawsphere
from graphics.drawing.CS_draw_primitives import drawcylinder
from OpenGL.GL import GL_LIGHTING, glDisable, glEnable
from geometry.VQT import cross, proj2sphere, V, norm, Q, vlen

from foundation.Utility import Node
import foundation.env as env

DX = V(1,0,0)
DY = V(0,1,0)
DZ = V(0,0,1)

from PyQt4.Qt import Qt, QCursor
# for Qt.Key_Left, etc

shiftButton = Qt.ShiftModifier
controlButton = Qt.ControlModifier

# from experiment, 070813, intel macbook pro
_key_control = 16777250
_key_command = 16777249


# for points of interest in this file, search for:
# - class test_animation_mode

###TODO:

# works in exprs/test.py: Image("/Users/bruce/Desktop/IMG_0560 clouds1.png")
# see test_commands.py...

# WARNING: if you have test_animation_mode.py OR test_animation_mode.pyc in cad/src, that one gets imported instead of one in ~/Nanorex/Modes.

# see also test_animation_mode-outtakes.py [lots of obs stuff moved there, 060219]

class Sketch3D_entity: pass
class Sketch3D_Sphere(Sketch3D_entity):
    # see class Sphere in exprs.Rect -- that ought to be enough info for a complete one!
    pass

##from exprs.staterefs import PrefsKey_StateRef
##
##def Preferences_StateRef_double( prefs_key, defaultValue = 0.0): #UNTESTED
##    ### TODO: cache this instance somehow (based on our args), as we IMPLEM the way to make it -- see test_commands for that
##    expr = PrefsKey_StateRef( prefs_key, defaultValue)
##    return find_or_make_expr_instance( expr, cache_key = ('PrefsState', prefs_key), assert_same = (defaultValue,) )
##
##### IMPLEM find_or_make_expr_instance, and make it fast when expr already exists (but do make it check assert_same, for now)



CANNON_HEIGHT_PREFS_KEY = "a9.2 devel/test_animation_mode/cannon height"
CANNON_HEIGHT_DEFAULT_VALUE = 7.5

def cannon_height(): # make a method?? why?
    return env.prefs.get( CANNON_HEIGHT_PREFS_KEY, CANNON_HEIGHT_DEFAULT_VALUE)

def set_cannon_height(val):
    env.prefs[CANNON_HEIGHT_PREFS_KEY] = val

CANNON_OSCILLATES_PREFS_KEY = "a9.2 devel/test_animation_mode/cannon oscillates"
CANNON_OSCILLATES_DEFAULT_VALUE = True

def cannon_oscillates():
    return env.prefs.get( CANNON_OSCILLATES_PREFS_KEY, CANNON_OSCILLATES_DEFAULT_VALUE)

SILLY_TEST = False ####


TESTING_KLUGES = False # temporary

class test_animation_mode_PM(ExampleCommand1_PM):
    """
    [does not use GBC; at least Done & Cancel should work]
    """
    title = "test_animation_mode PM"

    def __init__(self, command): #bruce 080909/080910, remove win arg
        ExampleCommand1_PM.__init__(self, command)

    def _addGroupBoxes(self):
        """
        Add the groupboxes for this Property Manager.
        """
        self.pmGroupBox1 = \
            PM_GroupBox( self,
                         title           =  "test_animation_mode globals",
                         )
        self._loadGroupBox1(self.pmGroupBox1)
        self.pmGroupBox2 = \
            PM_GroupBox( self,
                         title           =  "commands",
                         )
        self._loadGroupBox2(self.pmGroupBox2)
        return

    _sMaxCannonHeight = 20

    def _loadGroupBox1(self, pmGroupBox):
        """
        Load widgets into groupbox 1 (passed as pmGroupBox)
        """
##        elementComboBoxItems  =  self._sElementSymbolList
##        self.elementComboBox  =  \
##            PM_ComboBox( pmGroupBox,
##                         label         =  "Elements :",
##                         choices       =  elementComboBoxItems,
##                         index         =  0,
##                         setAsDefault  =  True,
##                         spanWidth     =  False )

        ### change to a control for cannon height, dflt 7.5.
        # have to figure out what state that is, how to share it and track it.
        # use expr state to store it?? change class cannon into an expr?
        # but then when we adjust this does it affect all cannons or only a current one? either is possibly desirable...
        # and if both can work, it means cannon instances have formulas for their height,
        # some referring to default from class or maker...

        self.cannonHeightSpinbox  =  \
            PM_DoubleSpinBox( pmGroupBox,
                              label         =  "cannon height:", #e try ending that label with " :" rather than ":", too
                              value         =  CANNON_HEIGHT_DEFAULT_VALUE,
                              # guess: default value or initial value (guess they can't be distinguished -- bug -- yes, doc confirms)
                              setAsDefault  =  True,
                              minimum       =  3,
                              maximum       =  self._sMaxCannonHeight,
                              singleStep    =  0.25,
                              decimals      =  self._sCoordinateDecimals,
                              suffix        =  ' ' + self._sCoordinateUnits )
        ### REVIEW: when we make the connection, where does the initial value come from if they differ?
        # best guess answer: PM_spec above specifies default value within PM (if any); existing state specifies current value.
        self.cannonHeightSpinbox.connectWithState(
            Preferences_StateRef_double( CANNON_HEIGHT_PREFS_KEY, CANNON_HEIGHT_DEFAULT_VALUE )
            )

        self.cannonWidthSpinbox  =  \
            PM_DoubleSpinBox( pmGroupBox,
                              label         =  "cannon width:",
                              value         =  2.0,
                              setAsDefault  =  True,
                              minimum       =  0.1,
                              maximum       =  15.0,
                              singleStep    =  0.1,
                              decimals      =  self._sCoordinateDecimals,
                              suffix        =  ' ' + self._sCoordinateUnits )
        self.cannonWidthSpinbox.connectWithState(
            ObjAttr_StateRef( self.command, 'cannonWidth') # first real test of ObjAttr_StateRef.
                # test results: pm change -> tracked state change (mode attr), with redraw being triggered: works.
                # other direction (mode change to cannonWidth, in cmd_Start -> pm change) -- fails! ###BUG
                # note: the prefs state also was not tested in that direction! now it is, in fire command, and it works,
                # but this, also tested there, still fails. ... What could be different in these two cases?
            # Is there an API mismatch in the lval we get for this, with get_value not doing usage tracking?
            )

        self.cannonOscillatesCheckbox = \
            PM_CheckBox(pmGroupBox,
                        text       = 'oscillate cannon during loop' ,
                        ## value = CANNON_OSCILLATES_DEFAULT_VALUE ### BUG: not working as default value for restore defaults
                        # in fact, worse bug -- TypeError: __init__() got an unexpected keyword argument 'value'
                        )
        self.cannonOscillatesCheckbox.setDefaultValue(CANNON_OSCILLATES_DEFAULT_VALUE) #bruce extension to its API
        self.cannonOscillatesCheckbox.connectWithState( ### UNTESTED in direction mode->PM, tho i think code is tested in user prefs
            Preferences_StateRef( CANNON_OSCILLATES_PREFS_KEY, CANNON_OSCILLATES_DEFAULT_VALUE ) )
        return

    def _loadGroupBox2(self, pmGroupBox):
        self.startButton = \
            PM_PushButton( pmGroupBox,
                           label     = "",
                           text      = "Start",
                           spanWidth = False ) ###BUG: button spans PM width, in spite of this setting
        self.startButton.setAction( self.cmd_Start, cmdname = "Start")

        self.stopButton = \
            PM_PushButton( pmGroupBox,
                           label     = "",
                           text      = "Stop",
                           spanWidth = False )
        self.stopButton.setAction( self.cmd_Stop, cmdname = "Stop")

        #e also let in_loop be shown, or control whether they're disabled... note it can be changed by external effects
        # so we need to track it and control their disabled state, using a formula that gets checked sufficiently often...
        # connect the expr or formula for self.command._in_loop to the update function...

        ## whenever_state_changes( ObjAttr_StateRef( self.command, '_in_loop'), self.update_GroupBox2 ) ###IMPLEM 2 things

        ## or just do this?? call_often(update_GroupBox2) -- a few times per sec, and explicit calls on the buttons... seems worse...

        self._keepme = Formula( self.update_GroupBox2, self.update_GroupBox2 ) ### hmm.....

        return

    def update_GroupBox2(self, junk = None):
        in_loop = self.command._in_loop ### or should this be passed in?? probably a better habit is if it's not...
            # note we can't track it from running this, since that attr is not directly usage tracked.
            # maybe we should change it so it is? then just call this once and say "call whenever what it used changes"...
            ##### TODO / REVIEW
        self.startButton.setEnabled( not in_loop) ### TODO: make something like setEnabledFormula for this... pass it a function??
        self.stopButton.setEnabled( in_loop)
        return

    def cmd_Start(self):
        self.command.cmd_Start()

    def cmd_Stop(self):
        self.command.cmd_Stop()

    def _addWhatsThisText(self):
        """
        What's This text for some of the widgets in the Property Manager
        """
        self.cannonHeightSpinbox.setWhatsThis("cannon height")
        return

    pass # test_animation_mode_PM


"""
File "/Nanorex/Working/cad/src/platform.py", line 92, in ascii
  return filter_key( self._qt_event.ascii() ) #k (does filter_key matter here?)
AttributeError: ascii
"""
#unfixed bug in arrow key bindings -- due to event.ascii ### BUG: not available in Qt4 [070811]

keynames = {}
for keyname in filter(lambda s: s.startswith('Key'), dir(Qt)):
    keynames[getattr(Qt, keyname)] = keyname

def keyname(key):#070812
    try:
        return keynames[keyname]
    except KeyError:
        return "<key %r>" % (key,)
    pass

pink1 = (0.8, 0.4, 0.4)
yellow = (0.8, 0.7, 0.0)
yellow2 = (0.8, 0.7, 0.2)
ygreen = (0.4, 0.85, 0.1)
blue = (0.0, 0.2, 0.9)
orange = (1.0, 0.35, 0.05)
orange = (1.0, 0.3, 0.00)
pumpkin = (0.9, 0.4, 0.0)
purple = (0.7, 0.0, 0.7) #060218 bugfix

def light(color, whiteness = 0.25):
    return mix(color, white, whiteness)

def mix(color1, color2, amount2):
    amount2 = V(amount2, amount2, amount2) #e optional?
    amount1 = V(1, 1, 1) - amount2
    return color1 * amount1 + color2 * amount2

lgreen = light(green)
lblue = light(blue)
lred = light(red)

colorkeys = dict(R = light(red),
                 G = light(green),
                 B = light(blue),
                 P = light(purple),
                 W = light(black, 0.8),
                 K = light(black),
                 Y = yellow,
                 O = orange,
            )

# and these, once we move them there:

####@@@@ assume Node inherits _S_State_Mixin
### that and other _S_ mixins can define writemmp, which classes can override if they want to use trad methods,
# calling utils in it to write info if desired (worry - don't Groups do that for them in some cases??),
# or they can not define it or tell it to call that mixin version, and then it just uses decls.
# also we can support binary mmp from them, as well as undo of course.
# undo policy decls can override what you'd guess from state decls. ###refile this

S_DATA = 'S_DATA'
# S_DATA, S_CHILD, S_CHILDREN, S_PARENT, S_PARENTS, S_REF, S_REFS, S_CACHE, S_JUNK, S_OWNED_BY, ...

class _S_Data_Mixin: pass #e stub #e refile in state_utils

class _S_ImmutableData_Mixin(_S_Data_Mixin):
    """
    For efficiency, inheritors of this mixin promise that all their declared data
    is immutable, so it can be shared by all copies, and so they themselves can be
    copied as themselves. (Most of them deepcopy the data passed into them, to protect
    themselves from callers who might pass shared data.)
    """
    def _s_deepcopy(self, copyfunc): ##k API [not presently called as of 081229, AFAIK]
        #e maybe someday we'll inherit this from (say) _S_ImmutableData_Mixin
        return self
    def _s_copy_for_shallow_mod(self): #e likely to be renamed, maybe ...private_mod
        """
        Private method for main class -- copy self, sharing data,
        in anticipation that the copy will be privately modified
        and then returned as a new immutable data object.
        """
        assert 0, "nim" #e but should be easily done using _s_initargs, or perhaps the set of _s_attr decls
    pass

# see also comments about _s_initargs API, below

# ==

def do_what_MainWindowUI_should_do(win):
    pass

_superclass = Command
# see also _superclass_for_GM

# new stuff 060218

# local copy for debugging & customization, original by josh in VQT.py
class myTrackball:
    """
    A trackball object. The current transformation matrix
    can be retrieved using the "matrix" attribute.
    """
    def __init__(self, wide, high):
        """
        Create a Trackball object.
        "size" is the radius of the inner trackball
        sphere.
        """
        self.w2=wide/2.0
        self.h2=high/2.0
        self.scale = 1.1 / min(wide/2.0, high/2.0)
        self.quat = Q(1,0,0,0)
        self.oldmouse = None

    def rescale(self, wide, high):
        self.w2=wide/2.0
        self.h2=high/2.0
        self.scale = 1.1 / min(wide/2.0, high/2.0)

    def start(self, px, py):
        self.oldmouse=proj2sphere((px-self.w2)*self.scale,
                                  (self.h2-py)*self.scale)

    def update(self, px, py, uq=None):
        newmouse = proj2sphere((px-self.w2)*self.scale,
                               (self.h2-py)*self.scale)
        if self.oldmouse and not uq:
            quat = Q(self.oldmouse, newmouse)
        elif self.oldmouse and uq:
            quat =  uq + Q(self.oldmouse, newmouse) - uq
        else:
            quat = Q(1,0,0,0)
        self.oldmouse = newmouse
        ## print "trackball quat would rotate V(0,0,1) to",quat.rot(V(0,0,1))
        showquat("trackball quat", quat, where = V(-3,0,0))

        ## drawline(white, self.origin, endpoint)
        return quat
    pass

def hacktrack(glpane):
    print "hacking trackball, permanently"
    glpane.trackball = myTrackball(10, 10)
    glpane.trackball.rescale(glpane.width, glpane.height)

quats_to_show = {}

def showquat(label, quat, where = V(0,0,0)):
    quats_to_show[label] = (Q(quat), + where)

def draw_debug_quats(glpane):
    for quat, where in quats_to_show.values():
        drawquat(glpane, quat, where)

def drawquat(glpane, quat, where):
    # not yet good enough: need to correct for screensize and scale, put it in fixed place/size on screen.
    # more importantly, what I *need* to show is accumulation of several small quats... not sure how.
    # or maybe, the *history* of this one? as it is, latest value might be "0" and so i don't see the transient one.
    oldquat = glpane.quat
    newquat = oldquat + quat
    def trans(screenpos):
        return glpane.pov + oldquat.unrot(screenpos)
    # show how the x,y,z axes would be moved.
    p0 = trans(where)
    for vec, color in [ (V(1,0,0), red), (V(0,1,0), green), (V(0,0,1), blue) ]:
        vecnow = oldquat.rot(vec)
        vecthen = newquat.rot(vec)
        p1 = trans(where + vecnow)
        p2 = trans(where + vecthen)
        drawline(color, p0, p1)
        drawline(color, p1, p2)
    return

# i want a trackball which is constrained to keep the Y axis in the up/out plane.
# this cmd works, but interface is a pain... at least needs button, but really needs to be a trackball option for this mode.
# well, it can be, since we intercept the trackballing events!
def novertigo_cmd(widget):
    glpane = widget
    novertigo(glpane)
    return

def novertigo(glpane):
    """
    Rotate the view so that the Y axis V(0,1,0) points up, plus maybe a bit in or out.
    """
    xjunk, y, z = Y = glpane.quat.rot(V(0,1,0))
    # we'll use a projection of this into the y,z plane, but fix it to be within our range; note, y,z could be (0,0)
    k = 2 # determines max angle from vertical
    ## print "Y =",Y
    ## printvec("Y",Y)
    if y < k * abs(z):
        y = k * abs(z)
    if y == 0 and z == 0:
        y, z = 1, 0
    proj = norm(V(0, y, z))
    ##printvec("proj",proj)
    ## print proj
    # proj is where we want Y to be
    incrquat = Q(Y, proj)
    showquat("incrquat",incrquat, V(3,0,0))
    glpane.quat += incrquat
##    q2 = glpane.quat + Q(Y, proj)
##    glpane.snapToView(q2, glpane.scale, glpane.pov, glpane.zoomFactor)
    return

def printvec(label, vec):
    print label, "(%03f, %03f, %03f)" % (vec[0], vec[1], vec[2]) # why does it print with 6 digits? Y (-0.003402, 0.696759, 0.717297)

register_debug_menu_command("novertigo", novertigo_cmd) # will only work in glpane debug menu


def hack_standard_repaint_0(glpane, prefunc):
    """
    replace glpane.standard_repaint_0 with one which calls prefunc
    and then calls the class version of standard_repaint_0
    """
    def imposter(glpane = glpane, prefunc = prefunc):
        prefunc()
        glpane.__class__.standard_repaint_0(glpane)
        return
    glpane.standard_repaint_0 = imposter
    return

# ==

class CyberTextNode(Node):
    """
    Hold some text which can be edited and displayed, and a few properties to control when/where/how to display it.
    Also try to let it exist in cyberspace, i.e. when it's changed, store it there, and when we're reshown, reload it from there.
    For initial test, cyberspace means a file whose basename we contain and whose dirname is hardcoded.
    Or a prefs db entry, or something like that.
    - Hmm... how does user make one? Mt cmenu.
    - How does user edit text? Ideally, select node, then text is shown in editable text field.
    This field is only there when something has text to edit (not nec. us).
    Optional cool feature: if multiple nodes selected, show all of them (editably) in that text field (longer, scrollable).
    - Where do we put that text field? Better make one... no good place at the moment.
    Do we have *any* code to grab for that?
    - history widget
    - debug text edit
    - glpane text, with our own key bindings for editing -- sounds harder, not sure it really is (need focus to follow selected node)
    Related nE-1 desired features:
    - edit props for any node, in a pane added below it in MT when it's selected -- this is almost identical to what we want here
    - text-containing node, for notes, or display in 3dview -- also almost identical
    - the cyberspace part is the only weird part
    So, what to do?
    - have a text widget at MT bottom, hide/show on demand
    - be able to tie it to edit any textual lvalue; some owner takes it over by providing that lval's API, later releases it,
      and new owners can also kick out old ones
    - this node, when selected, takes it over unless it's locked, or always if some cmenu command is used or if it's unlocked
    - this locking is controlled by a cmenu on the text widget, or a checkbox, or equivalent

    """

# see Macintosh HD:Nanorex:code, work notes:060401 code snippets
# QTextEdit(win.mt)

def find_or_make_textpane(win):
    try:
        return win.__textpane
    except:
        pass
    try:
        vs = win.vsplitter2
    except:
        return None
    # make it only once
    te = QTextEdit(vs) #e use subclass with key bindings; maybe put it in a 1-pixel frame
    vs.setSizes([vs.height() - 100, 100]) #k assumes our new widget is only the 2nd shown thing of two things (1st being win.mt)
    te.show()
    win.__textpane = te
    return te

"""
set mtree_in_a_vsplitter in MWsemantics (edit it before startup) (debug_pref?)
from __main__ import foo as win
import test_animation_mode
print test_animation_mode.find_or_make_textpane(win)
"""

# ==

class DebugNode(Node):
    def __init__(self, stuff, name = None):
        assy = env.mainwindow().assy #k wrongheaded??
        Node.__init__(self, assy, name)
        self.stuff = stuff # exprs for what to draw (list of objects)
    _s_attr_stuff = S_DATA
    def draw(self, glpane, dispdef):
        if self.picked: # only draw when picked! (good?)
            draw_stuff(self.stuff, glpane)
    def writemmp(self, mapping):
        """
        Write this Node to an mmp file, as controlled by mapping,
        which should be an instance of writemmp_mapping.
        """
        line = "# nim: mmp record for %r" % self.__class__.__name__
        mapping.write(line + '\n')
        # no warning; this happens all the time as we make undo checkpoints
        return
    def __CM_upgrade_my_code(self): # experiment
        """
        replace self with an updated version using the latest code, for self *and* self's data!
        """
        name = self.__class__.__name__
        print "name is",name # "DebugNode"
        print self.__class__.__module__ # "test_animation_mode"
        #e rest is nim; like copy_val but treats instances differently, maps them through an upgrader
    pass

def draw_stuff(stuff, glpane):
    if type(stuff) in (type(()), type([])):
        for thing in stuff:
            draw_stuff(thing, glpane)
    else:
        try:
            stuff.draw(glpane)
        except:
            print_compact_traceback("draw_stuff skipping %r: " % (stuff,))
    return

# ==

class DrawableStuff(_S_Data_Mixin):
    def __init__(self, *args, **kws):
        self.args = copy_val(args)
        self.kws = copy_val(kws)
        self.process_args()
    _s_attr_args = S_DATA
    _s_attr_kws = S_DATA
    def _s_initargs(self):
        #####k API -- in this case we're providing full info,
        # but didn't the _um_ version get to leave out some attrs, and instead handle them with diffs?? ###@@@
        # we want to keep it simple, but we know there are some objects that require that approach...
        # maybe let them define a *different* method and leave this one undefined?? or have a different retval format for that case??
        return self.args, self.kws
    def process_args(self):
        # callers can add arg asserts if they want; we guarantee __init__ will call this, so it can store args in attrs,
        # which _S_Data_Mixin will effectively treat as S_CACHE by default
        pass
    pass

class makeline(DrawableStuff):
    def process_args(self):
        self.color, self.pos1, self.pos2 = self.args[0:3]
    def draw(self, glpane):
        drawline(self.color, self.pos1, self.pos2)
    pass

class makedot(DrawableStuff):
    def process_args(self):
        self.color, self.pos = self.args[0:2]
    def draw(self, glpane):
        #e stub, need to choose radius based on pixel size, and make a circle not sphere
        detailLevel = 2
        radius = 0.1 ### total stub and guess
        drawsphere(self.color, self.pos, radius, detailLevel)
        pass #####@@@@@
    pass

class makevecs(DrawableStuff):
    def process_args(self):
        self.origin, self.vecs, self.colors = self.args[0:3]
        self.vecs_colors = zip(self.vecs, self.colors)
    def draw(self, glpane):
        for vec, color in self.vecs_colors:
            drawline(color, self.origin, self.origin + vec)
        return
    pass

# ==

class attrholder:
    def __init__(self, **options):
        self.__dict__.update(options)
    pass

DEFAULT_DIRECTIONS = attrholder(away = - DZ, towards = DZ, up = DY, down = - DY, left = - DX, right = DX)
    # maybe attrnames should differ?

def interpret_arrow_key( key, space = None): ###TODO: pass space if nec.
    """
    return None for a non-3d-arrow-key, or a direction vector for one, taken from space or DEFAULT_DIRECTIONS
    """
    if not space:
        space = DEFAULT_DIRECTIONS
    if key == Qt.Key_Up: # 4115:# up means in = lineofsight = away from user
        return space.away
    elif key == Qt.Key_Down: # 4117: # down means out = towards user
        return space.towards
    elif key == Qt.Key_Left: # 4114: # left, right mean themselves
        return space.left
    elif key == Qt.Key_Right: # 4116:
        return space.right
    elif key == Qt.Key_PageUp: # 4118: # pageup means up
        return space.up
    elif key == Qt.Key_PageDown: # 4119: # pagedown means down
        return space.down
    return None

# ==

class shelvable_graphic:
    # some default state attrs (kluge, should be in a graphical subclass)
    pos = V(0,0,0)
    dir = V(1,0,0)
    size = V(1,1,1)
    color = pink1
    dead = False # set self.dead to True in instances, to cause destroy() and culling on next redraw
        # ok to set it during a command (then it prevents redraw of self) or during redraw (culls upon return); see guy.draw for implem
    def __init__(self, space, dict1 = {}):
        self.__dict__.update(dict1) # do first so we don't overwrite the following ones
        self.space = space # note: this is the mode, a test_animation_mode instance [assume it is a Command, not a GraphicsMode]
        self.glpane = space.glpane
        self.stuff = {} # kids, i guess [need a way of removing old ones]
        self.creation_time = space.simtime
    def changed(self):#050116 [long after shelvable_graphic stub created; note that it never worked yet]
        global_changed[self] = self ###k does self count as a legit key, or not?
    def save(self, storage, key):
        storage[key] = self.state()
    def state(self):
        res = {}
        for k in self.__dict__.keys(): # not dir(self), that includes class methods...
            if not (k.startswith('_') or k in ['space','stuff']): ###@@@ see also "snaps.py" which I started writing...
                #k don't exclude stuff... need pickle methods to turn objs into refs to them or their snapshot then.
                res[k] = self.__dict__[k]
        return res
    def contents(self):
        """
        other objs whose state is sometimes considered part of ours, but not in self.state()
        """
        return self.stuff.values()
    def load(self, storage, key):
        """
        change our state to match what's in the storage
        """
        stored = storage[key]
        self.__dict__.update(stored) # dangerous!
    def destroy(self):
        ## print "destroying", self # works
        pass
    def move(self, vec):
        self.pos = self.pos + vec
    pass # pickleable, mainly

##class brick(shelvable_graphic): # this is not right yet
##    "a brick, in standard orientation, std size, brighter if keyfocus is on it; has specified shelf-key for state"
##    def draw(self, pos = V(0,0,0), dir = V(1,0,0), size = V(1,1,1), color = pink1):
##        # drawbrick(pink1, self.origin + V(2.5, 0, 0), self.right, 2.0, 4.0, 6.0) - uses current GL context (that's ok)
##        drawbrick(color, pos, dir, size[0], size[1], size[2])
##    pass

class wireguy(shelvable_graphic): # used for square tiles in the "racetrack"
    moves = False # to change this, let it be in the dict arg when we create this
    def draw(self, glpane):
        ## drawwirecube(self.color, self.pos, 1.02 / 2.0)# no place for self.dir; last number is not what i hoped...
        pos = self.pos
        if self.moves:
            pos = pos + DY * (self.space.simtime - self.creation_time) # 070813  [071010 glpane .mode -> self.space == a Command]
        drawbrick( self.color, pos - V(0, 0.6, 0), self.dir, 1.0, 1.0, 0.05) ## 0.98, 0.98, 0.05)
    pass

TOOFAR = 100 # some things disappear when this far away -- to test this, use 6 so things disappear within sight -- works

CANNONBALL_SPEED = 21
BIRD_SPEED = 11 # not yet used

class cannonball(shelvable_graphic):
    motion = V(0,0,0) # caller may want to use CANNONBALL_SPEED when setting this

    position = None
    velocity = None
    last_update_time = None
    acceleration = -10 * DY * 3
    def update_incr(self):
        if self.last_update_time is None:
            self.last_update_time = self.creation_time
        if self.position is None:
            self.position = V(0,0,0) + self.pos ###k
        if self.velocity is None:
            self.velocity = V(0,0,0) + self.motion ###k
        now = self.space.simtime
        delta_time = now - self.last_update_time
        # if too large, do the following in several steps, or use a quadratic formula
        self.position += (delta_time * self.velocity)
        self.velocity += (delta_time * self.acceleration)
        self.last_update_time = now
        ## print "dt = %r, so set position = %r, velocity %r" % (delta_time, self.position, self.velocity)
        return
    def draw(self, glpane):
        self.update_incr() # now self.position should be correct; self.pos is starting position
##        age = (self.space.simtime - self.creation_time)
##        displacement = self.motion * age
        displacement = self.position - self.pos
        if vlen(displacement) > TOOFAR:
            self.dead = True # causes delayed destroy/cull
        else:
            pos = self.position
            drawsphere( blue, pos, 1, 2)
        return
    pass

class cannon(shelvable_graphic): # one of these is self.cannon in test_animation_mode; created with mode = that mode

    direction = norm(DY - DX) #070821

    def basepos(self):
        mode = self.space
        ## pos = mode.origin
        pos = self.pos # changed by .move()
        return pos + V(mode.brickpos, 0, 0) - DY * 9

    def draw(self): # needs glpane arg if ever occurs in object list
        mode = self.space # note: uses mode.brickpos for position, updated separately -- should bring that in here

        ## drawwirecube(purple, mode.origin, 5.0)
        ## drawwirecube(gray, mode.origin, 6.5)
        ## if 0: drawbrick(yellow, mode.origin, mode.right, 2.0, 4.0, 6.0)

        basepos = self.basepos()
        drawbrick(pink1, basepos + 1.0 * self.direction, mode.right, 2.0, 2.0, 1.0) # wrong orientation
        detailLevel = 2
        drawsphere(red, basepos + 2.5 * self.direction, 2, detailLevel) ###@@@
        radius = mode.cannonWidth / 2.0 # note: this is usage-tracked!
        drawcylinder(lgreen,
                     basepos + 2.5 * self.direction,
                     basepos + cannon_height() * self.direction,
                     radius, True)
        # with red, its lighting is pretty poor; pink1 looks nice

    def fire(self): # bound to command key
        toppos = self.basepos() + cannon_height() * self.direction
            # note: cannon_height() is usage tracked, but needn't be in this context, but that needs to be ok without any reason
            # to mention it here, since the same issue affects all usage tracked variables used in commands.
        new = cannonball(self.space, dict(pos = toppos, motion = self.direction * CANNONBALL_SPEED))
        self.space.appendnew(new)
        if SILLY_TEST:
            self.grow_cannon()
        return

    def grow_cannon(self):
        set_cannon_height(cannon_height()+1) # test of whether pm gets updated -- it does!
        self.space.cannonWidth += 0.5 # ditto - in this case it doesn't! bug [fixed now]

    pass # class cannon

class guy(shelvable_graphic):
    """
    a combo of object list and a specific thing ###FIX
    """
    def draw(self, glpane):
        # this main guy is the one that needs to be transparent, so you can see what's he's making under him!
        # and, the thing he might make also needs to be transparent but there. and, a key should put it down for you... and unput it.
        # and, remove any other stuff in same pos!
        pos = self.pos ## + self.space.now # 070813 experiment -- works [since then, glpane .mode -> self.space, but i don't want to move this guy (the 3d cursor)
        drawbrick(self.color, pos, self.dir, 1, 1, 1) ## 2.0, 4.0, 6.0) # uses current GL context
        glDisable(GL_LIGHTING)
        drawbrick(gray, pos * V(1,0,1) - V(0, 6, 0), self.dir, 1, 1, 0.02) # brick dims for this dir are x, inout, z
        glEnable(GL_LIGHTING)
        drawline(light(black, 0.2), pos, pos * V(1,0,1) - V(0, 6, 0))
        #e and a line between them
        deads = []
        for thing in self.stuff.itervalues():
            #e move to superclass? ... well, they need to notice our shadow hitting them! be transparent? fog?
            # i bet transparent is not super hard to do... note that for cubes (or any convex solids)
            # it's easy to know back to front order...
            if thing.dead:
                # (thing.dead was set during a command -- don't draw it)
                deads.append(thing)
            else:
                thing.draw(glpane) # arg?
                if thing.dead: # (thing.dead was set during draw method itself, which may or may not have returned early)
                    deads.append(thing)
        for thing in deads:
            thing.destroy()
            del self.stuff[id(thing)]
        return
    def keyPressEvent(self, event):
        key = event.key()
        ## asc = event.ascii()  ### BUG: not available in Qt4 [070811]
        ## but = event.stateAfter()  ### BUG: not available in Qt4 [070811]
        ## but = event.state()  ### BUG: not available in Qt4 [070811]
        but = -1

        self.shift = but & shiftButton # not set correctly at the moment; used to control whether to makeonehere
        self.control = but & controlButton # not yet used
        try:
            chrkey = chr(key)
        except:
            chrkey = None
        ##print key, asc, but, chrkey
        if key == Qt.Key_Return: # 4100: # Return
            key = ord('\n')
            ## asc = key
##        if key == ord('\n') or 32 <= key <= 126:
##            # use asc, so 'a' vs 'A' is correct
##            self.text = str_insert(self.text, self.curpos, chr(asc))
##            self.curpos += 1
##            ## didn't work: return 1 ###@@@ incrkluge test; not correct btw!
##        elif key == 'rdel': # names like that are stubs
##            self.text = self.text[:self.curpos] + self.text[self.curpos+1:]
##        elif key in [4099, 'del']: # Delete on Mac; unable to bind the "del" key!
##            self.text = self.text[:self.curpos-1] + self.text[self.curpos:]
##            self.curpos -= 1
        ## print "data:", asc, key, chrkey, self.colorkeys # asc = 114, key = 82, chr(key) = 'R'

        arrowdir = interpret_arrow_key(key)

        if chrkey in colorkeys:
            self.color = colorkeys[chrkey]
        ### 060402 new arrow keys [not done., disabled]
##        elif key == 4114:
##            # left
##            self.space.o.left
        # end of 060402 new arrow keys
        #e also arrow keys... should these be model relative as here?
        # [this is in class guy, but would make more sense in class test_animation_mode]
        elif arrowdir is not None:
            if not self.space._in_loop:
                self.move( arrowdir)
            else:
                self.space.cannon.move( arrowdir)
        elif chrkey == ',': # '<'
            self.space.rotleft(0.05) # far away stuff goes left, that much of 360deg ... around what center?
        elif chrkey == '.': # '>'
            self.space.rotleft(-0.05) ###@@@ subr is wrong at the moment
        else:
            print "test_animation_mode received key:", keyname(event.key()) ## "(%r)" % event.key()
        self.save()
    def save(self):
        pass # save the state! just store our dict at a key... but turn values from objs to refs... ask the objs for those. ###@@@
    def move(self, delta):
        """
        overrides superclass move; compatible but does more
        """
        if self.shift:
            self.makeonehere() # should be in the space, not the guy itself! ###@@@
        self.lastmotion = delta
        # this is superclass.move: ###FIX, call it
        pos = self.pos = self.pos + delta
        ##print "new pos and proj", pos, pos * V(1,0,1) - V(0,6,0)
        # caller does redraw, no need to tell it to here
    def makeonehere(self):
        d1 = dict(self.__dict__)
        if self.space._in_loop: ## FIX: could simplify since self.space == mode [did it now, glpane .mode -> space]
            ## d1['moves'] = True # sets new.moves = True
            d1['motion'] = DY * 3 # new.motion
            new = cannonball(self.space, d1)
        else:
            new = wireguy(self.space, d1) # note: it inherits copies of all our attributes! such as pos, color.
        #e remove any existing stuff in the same place! let's store them in a dict by place, not this list... ###@@@
        self.space.appendnew(new)
        ## self.stuff.append(new)
    pass # end of class guy

# ==

'editToolbar', 'fileToolbar', 'helpToolbar',
'modifyToolbar', 'molecularDispToolbar',

annoyers = [##'editToolbar', 'fileToolbar', 'helpToolbar', 'modifyToolbar',
            ##'molecularDispToolbar', 'selectToolbar', 'simToolbar',
            ## 'toolsToolbar',
            ##'viewToolbar',
    ] # all have been renamed in Qt4


# code copied from test_commands.py:
# these imports are not needed in a minimal example like ExampleCommand1;
# to make that clear, we put them down here instead of at the top of the file
from graphics.drawing.CS_draw_primitives import drawline
from utilities.constants import red, green
##from exprs.ExprsConstants import PIXELS
from exprs.images import Image
##from exprs.Overlay import Overlay
from exprs.instance_helpers import get_glpane_InstanceHolder
from exprs.Rect import Rect # needed for Image size option and/or for testing
from exprs.Boxed import Boxed
from exprs.draggable import DraggablyBoxed
from exprs.instance_helpers import InstanceMacro
from exprs.attr_decl_macros import State

from exprs.TextRect import TextRect#k
class TextState(InstanceMacro):#e rename?
    text = State(str, "initial text", doc = "text")#k
    _value = TextRect(text) #k value #e need size?s
    pass

# ==============================================================================

_superclass_for_GM = GraphicsMode
# print "_superclass = %r, _superclass_for_GM = %r" % (_superclass, _superclass_for_GM)####

class test_animation_mode_GM( _superclass_for_GM ):

    def leftDown(self, event):
        pass

    def leftDrag(self, event):
        pass

    def leftUp(self, event):
        pass

    def middleDrag(self, event):
        glpane = self.glpane
##        q1 = Q(glpane.quat)
        _superclass_for_GM.middleDrag(self, event)
        self.command.modelstate += 1
##        q2 = Q(glpane.quat)
        novertigo(glpane)
##        q3 = Q(glpane.quat)
##        print "nv", q1, q2, q3
        return

    def pre_repaint(self):
        """
        This is called early enough in paintGL to have a chance
        (unlike Draw) to update the point of view.
        """
        if self.isCurrentGraphicsMode() and self.command._in_loop:
            ## self.glpane.quat += Q(V(0,0,1), norm(V(0, 0.01, 0.99)))
            ## now = time.time() - self._loop_start_time
            now = self.command.simtime
            self.now = now # not used??
            if cannon_oscillates():
                self.command.brickpos = 2.5 + 0.7 * math.sin(now * 2 * pi)
            else:
                self.command.brickpos = 2.5
            self.set_cov()
        return

    def set_cov(self):#060218... doesn't yet work very well. need to check if 6*self.glpane.scale is correct...
            # not that i know what would fail if not.
        return ######
        try:
            space = self.command # since this method is in the GraphicsMode
            glpane = self.glpane
            cov = - glpane.pov # center of view, ie what camera is looking at
            guy_offset = space.guy.pos - cov
            # camera wants to rotate, but can't do this super-quickly, and it might have inertia, not sure...
            # kluge, test: set cov to some fraction of the distance
            newcov = + cov
            newcov += guy_offset * 0.2
            # now figure out new pos of camera (maintains distance), then new angle... what is old pos of camera?
            eyeball = (glpane.quat).unrot(V(0, 0, 6 * glpane.scale)) - glpane.pov # code copied from selectMode; the 6 might be wrong
                # took out - from -glpane.quat since it looks wrong... this drastically improved it, still not perfect.
                # but it turned out that was wrong! There was some other bug (what was it? I forget).
                # And th - was needed. I prefer to put in unrot instead.
            _debug_oldeye = + eyeball
            desireddist = 6*glpane.scale
            nowdist = vlen(eyeball - newcov) #k not used? should it be?
            # now move it partway towards guy... what about elevation? should we track its shadow instead?
            # ignore for now and see what happens? or just use fixed ratio?
            desired_elev = 2 * glpane.scale ## guess
            desired_h_dist = math.sqrt( desireddist * desireddist - desired_elev * desired_elev)
##            print "desireddist = %s, desired_elev = %s, desired_h_dist = %s" % (desireddist, desired_elev, desired_h_dist)
            # figure out coords for use in setting camera position from this
            dx, dy, dz = eyeball - newcov
            # x, z is a ratio to maintain
            eyedir = norm(V(dx, 0, dz)) * desired_h_dist # dir on ground only
            eyedir += desired_elev * V(0,1,0)
            # eyedir is ideal position rel to newcov, but eyeball should only move partway towards it... perfect this later (inertia?)
            eyeball += ((eyedir + newcov) - eyeball) * 0.2
            # now we know where eyeball is... it should look towards newcov... so real cov is partly there
            realcov = eyeball + norm(newcov - eyeball) * desireddist
##            print "desired-eyeball to desired-cov dist is", vlen(eyeball - realcov)
            # the quat is one which would look from eyeball to realcov, but keeping y in that vertical plane.
            # hmm... wish I could just use gluLookAt.
            #   Q(x, y, z) where x, y, and z are three orthonormal vectors
            #   is the quaternion that rotates the standard axes into that reference frame.
            # Standard axes: eyeball from cov is positive z...
            wantz = norm(eyeball - realcov)
            Y = V(0,1,0)
            wantx = norm(cross(Y, wantz))
            wanty = cross(wantz, wantx)
##            ## print "w", wantx, wanty, wantz
            realquat = - Q(wantx, wanty, wantz) # added '-' since should be correct...
##            print "X_AXIS =? realquat.rot(wantx)", realquat.rot(wantx)
##            print "Y_AXIS =? realquat.rot(wanty)", realquat.rot(wanty)
##            print "Z_AXIS =? realquat.rot(wantz)", realquat.rot(wantz)
            glpane.pov = - realcov
            glpane.quat = realquat ###@@@ see if - helps... makes elev 0 and makes it unstable eventually... but should be correct!
            #e store attrs for debug drawing
    # DON'T ZAP THIS, it might be very useful for improving the camera follower algorithm
##            # want to show (maybe elsewhere in diff coords): old eyeball, old guy, new guy, new place to lookat,
##            # new eyeball, actual cov... and some lines between them, blobs at them, etc; and store this for later back/fwd browsing
##            # (hmm, use ericm's little movie format? or smth similar, with back/fwd buttons for replay, or node per frame in MT,
##            #  visible when selected)
##            oldeye = _debug_oldeye
##            oldcov = cov
##            newguy = + space.guy.pos
##            guylookat = + newcov
##            debug_stuff = [makeline(white, oldeye, oldcov), makeline(gray, oldcov, newguy), makedot(gray, guylookat),
##                           makeline(orange, oldeye, eyeball), makedot(green, eyeball),
##                           makedot(blue, realcov),
##                           makevecs(realcov, [wantx, wanty, wantz], [red, green, blue]),
##                                ]
##            dn = DebugNode(debug_stuff, name = "debug%d" % env.redraw_counter)
##            space.placenode(dn) #e maybe ought to put them in a group, one per loop? not sure, might be more inconvenient than useful.
##            print "tried to set eyeball to be at", eyeball, "and cov at", realcov
##            apparenteyeball = (glpane.quat).unrot(V(0, 0, 6*glpane.scale)) - glpane.pov ### will unrot fix it? yes!
##            print "recomputed eyeball from formula is", apparenteyeball, "and also", realcov + glpane.out * desireddist
##            # 2nd formula says we got it right. first formula must be wrong (also in earlier use above).
##            # if we assume that glpane.eyeball() is nonsense (too early for gluUnProject??)
##            # then the data makes sense. Could the selectMode formula source have been right all along? #####@@@@@
        except:
            print_compact_traceback("math error: ")
            space.cmd_Stop()
            pass
        return

    def Draw_model(self):
        """
        """
        _superclass_for_GM.Draw_model(self)
        glpane = self.glpane
        glpane.assy.draw(glpane)

    def Draw_other(self):
        """
        """
        _superclass_for_GM.Draw_other(self)

        glpane = self.glpane

##        # can we use smth lke mousepoints to print model coords of eyeball?
##        print "glpane says eyeball is now at", glpane.eyeball(), "and cov at", - glpane.pov, " ." ####@@@@

        origin = self.command.origin
        endpoint = origin + self.command.right * 10.0
        drawline(white, origin, endpoint)

        self.command.cannon.draw()

        ## thing.draw(glpane, endpoint)
        self.command.guy.draw(glpane)
        ## draw_debug_quats(glpane)
        self.command._expr_instance.draw() #070813 - works, but resizer highlight doesn't work, didn't investigate why not ###BUG
        return

    def keyPressEvent(self, event):
        ## ascii = event.ascii() ### BUG: not available in Qt4 [070811]
        key = event.key()
##        if ascii == ' ': # doesn't work
##            self.cmd_Stop()
        if key == 32 or key == ord('S'): # note: 32 doesn't work, gets caught at some earlier stage and makes an orientation window
                ### note: ord('s') does not work... 83 is presumably the ascii code of 'S', not 's'
            self.command.cmd_Stop()
        ## if not self.what_has_our_focus: # can this be a single floor tile? or only the entire floor?
            # note that the focus is like the "character" in a game...camera should follow it through 3d space...
            # and it might have a dir, not just be an object... it might be a guy *near* the object.
            # so this var might be "what's near our guy" or "what's under our guy".
            # yeah, i'm sure it should be a guy (who might be over empty space, might move continuously).
            # otoh, this guy might have a finger poiinting at a thing, which is what gets our key events!
            # so we feed key to him, he feeds it out his feet or finger.
            # why not do "depmode" but with a guy walking around the mol? ... but how to draw the guy? not easy!
            # the keyfocus concept is that it's invisible, or a highlight state of the thing near it! or a simple line...
        elif key == _key_command or key == ord('F'): # F repeats, command doesn't
            self.command.cannon.fire()
        elif key == ord('G'):
            self.command.cannon.grow_cannon() # useful for testing PM update
        else:
            self.command.guy.keyPressEvent(event) ## wrong anyway : or thing.keyPressEvent(event)

        ## self.incrkluge = thing.keyPressEvent(event)
        self.redraw()

    def keyReleaseEvent(self, event):
        pass ## thing.keyReleaseEvent(event)

    def redraw(self):
        #e could be optimized if nothing but typing, and no DEL, occurred!
        self.command.modelstate += 1
        ##bruce 050528 zapped this: self.glpane.paintGL() - do it this newer way now:
        self.glpane.gl_update()

    def update_cursor_for_no_MB(self):
        """
        [part of GraphicsMode subclass API]
        """
        self.o.setCursor(QCursor(Qt.ArrowCursor))

    pass # end of class test_animation_mode_GM

# ========

from exprs.ExprsMeta import ExprsMeta
from exprs.StatePlace import StatePlace
from exprs.IorE_guest_mixin import IorE_guest_mixin # REVIEW: can we use State_preMixin here?

class test_animation_mode(_superclass, IorE_guest_mixin): # list of supers might need object if it doesn't have IorE_guest_mixin; see also __init__
    __metaclass__ = ExprsMeta # this seems to cause no harm.
        # Will it let us define State in here (if we generalize the implem)??
        # probably not just yet, but we'll try it and see what errors we get.
    transient_state = StatePlace('transient') # see if this makes State work... it's not enough --
        # it is a formula with a compute method, and Exprs.py:273 asserts self._e_is_instance before
        # proceeding with that. I predict self would need a lot of IorE to work here...
    _e_is_instance = True # I predict this just gets to another exception... if it's even correct --
        # it might be wrong to say it in the class itself!!! anyway it didn't complain about that
        # but i was right, it then asked for _i_env_ipath_for_formula_at_index...
        # so there is a set of things we need, to be able to support formulae like that one for transient_state,
        # and i bet IorE is mostly that set of things, but also with support for things we don't need or even want,
        # like child instances, call == customize, etc.
        #
        # So there are different ways to go: ### DECIDE:
        # - revise State to work without transient_state [probably desirable in the long run, anyway]
        # - make a more standalone way of defining transient_state
        # - split IorE into the part we want here and the rest (the part we want here might be a post-mixin)
        # - revise IorE to be safe to inherit here (as a post-mixin I guess), ie make it ask us what __call__ should do.
        #
        # Let's see what happens if we just inherit IorE. maybe it already works due to _e_is_instance,
        # and if not, maybe I can override __call__ or remove __call__ alone from what I inherit.

    # class constants needed by mode API
    backgroundColor = 103/256.0, 124/256.0, 53/256.0
    commandName = 'test_animation_mode' # must be same as module basename, for 'custom mode' to work
    featurename = "Prototype: Example Animation Mode"
    from utilities.constants import CL_ENVIRONMENT_PROVIDING
    command_level = CL_ENVIRONMENT_PROVIDING

    # other class constants
    PM_class = test_animation_mode_PM

    GraphicsMode_class = test_animation_mode_GM

    # tracked state (specially defined instance variables)
    # (none yet, but we want to put _in_loop here, at least)
    # can we say: _in_loop = State(boolean, False) ?
    # Or would that overload State too much, or use a too-generic word? ###REVIEW

    ## _in_loop = False ###TODO: unset this when we're suspended due to some temp commands -- but not all??
    _in_loop = State(bool, False)
    cannonWidth = State(float, 2.0) ## add , _e_debug = True to see debug prints about some accesses to this state

    # initial values of instance variables
    now = 0.0 #070813 [still used? maybe simtime has replaced it?]
    brickpos = 2.5
    # in _superclass anyMode: propMgr = None

    _please_exit_loop = False
    _loop_start_time = 0
    simtime = 0.0 # this is a constant between loops, and grows with real time during loops. only changed in cmd_Start. never reset.

    def __init__(self, commandSequencer):
        """
        create an expr instance, to draw in addition to the model
        """
        # code copied from test_commands.py
        super(test_animation_mode, self).__init__(commandSequencer) # that only calls some mode's init method, not IorE.__init__,
            # so (for now) call that separately
        glpane = commandSequencer.assy.glpane
        IorE_guest_mixin.__init__(self, glpane)
        if 0:
            # expr from test_commands - works except for resizer highlighting
            ## expr1 = Rect(4, 1, green)
            expr1 = TextState()
            expr2 = DraggablyBoxed(expr1, resizable = True)
                ###BUG: resizing is projecting mouseray in the wrong way, when plane is tilted!
                # I vaguely recall that the Boxed resizable option was only coded for use in 2D widgets,
                # whereas some other constrained drag code is correct for 3D but not yet directly usable in Boxed.
                # So this is just an example interactive expr, not the best way to do resizing in 3D. (Though it could be fixed.)
            expr = expr2
        if 0:
            expr = Rect() # works
        if 1:
            expr = Image("/Users/bruce/Desktop/IMG_0560 clouds g5 2.png", size = Rect(), two_sided = True)

        # note: this code is similar to _expr_instance_for_imagename in confirmation_corner.py
        ih = get_glpane_InstanceHolder(glpane)
        index = (id(self),) # WARNING: needs to be unique, we're sharing this InstanceHolder with everything else in NE1
        self._expr_instance = ih.Instance( expr, index, skip_expr_compare = True)

        ## in Draw: add         self._expr_instance.draw()

        if TESTING_KLUGES:
            self._clear_command_state() ###### FOR TESTING
            print "_clear_command_state in init, testing kluge"####

        return

    def _clear_command_state(self):
        """
        [private, not part of command API]
        """
        self.cmd_Stop()
        if TESTING_KLUGES:
            print "KLUGE FOR TESTING: set cannonWidth in cmd_Stop"
            self.cannonWidth = 2.0 ########### DOES IT FIX THE BUG? THEN ZAP.
        return

    # ==

    def command_will_exit(self):
        self._clear_command_state()
        _superclass.command_will_exit(self)
        return

    def command_enter_PM(self):
        if not self.propMgr:
            self.propMgr = self.PM_class(self) # [080910 change]
        return

    def _command_enter_effects(self):
        print
        print "entering test_animation_mode again", time.asctime()
##        self.assy = self.w.assy # [AttributeError: can't set attribute -- property?]
        hacktrack(self.glpane)
        hack_standard_repaint_0(self.glpane, self.graphicsMode.pre_repaint)
            # KLUGE -- this ought to be part of an Enter_GraphicsMode method...
            # there was something like that in one of those Pan/Rotate/Zoom classes too...
            # need to find those and decide when to call a method like that.
        self.glpane.pov = V(0, 0, 0)
        self.glpane.quat = Q(1,0,0,0) + Q(V(1,0,0),10.0 * pi/180)
        print "self.glpane.scale =", self.glpane.scale # 10 -- or 10.0?
        self.glpane.scale = 20.0 #### 070813 # note: using 20 (int not float) may have caused AssertionError:
            ## in GLPane.py 3473 in typecheckViewArgs
            ## assert isinstance(s2, float)

        print "self.glpane.scale changed to", self.glpane.scale
        self.right = V(1,0,0) ## self.glpane.right
        self.up = V(0,1,0)
        self.left = - self.right
        self.down = - self.up
        self.away = V(0,0,-1)
        self.towards = - self.away
        self.origin = - self.glpane.pov ###k replace with V(0,0,0)
        self.guy = guy(self)
        self.cannon = cannon(self)

        ##self.glbufstates = [0, 0] # 0 = unknown, number = last drawn model state number
        self.modelstate = 1
        # set perspective view -- no need, just do it in user prefs
        return

    def command_entered(self):
        _superclass.command_entered(self)
        self._command_enter_effects()
        return

    def command_enter_misc_actions(self): #bruce 080909 guess
        self.hidethese = hidethese = []
        for tbname in annoyers:
            try:
                tb = getattr(self.w, tbname)
                if tb.isVisible(): # someone might make one not visible by default
                    tb.hide()
                    hidethese.append(tb) # only if hiding it was needed and claims it worked
            except:
                print_compact_traceback("hmm %s: " % tbname) # someone might rename one of them
        return

    def command_exit_misc_actions(self): #bruce 080909 guess
        for tb in self.hidethese:
            tb.show()
        return

# not used now, but keep (was used to append DebugNodes):
##    def placenode(self, node):
##        "place newly made node somewhere in the MT"
##        # modified from Part.place_new_jig
##        part = self.assy.part
##        part.ensure_toplevel_group()
##        part.topnode.addchild(node) # order? later ones come below earlier ones, which is good.
##        self.w.mt.mt_update()
##        return

    def appendnew(self, new): ##FIX: stores object list (really a dict) in self.guy
        stuff = self.guy.stuff
        ## stuff.append(new) # it's a list
        stuff[id(new)] = new # it's a dict
        ## print "appendnew: %d things" % len(stuff) # test culling of old things -- works
        return

    def rotleft(self, amount):
        """
        [#doc is in caller]
        """
        # from Q doc: Q(V(x,y,z), theta) is what you probably want.
        #060218 bugfix: left -> down
        self.glpane.quat += Q(self.down, amount * 2 * pi) # does something, but not yet what i want. need to transform to my model...###@@@
        ## self.redraw() - do in caller

    def makeMenus(self):
        _superclass.makeMenus(self)
        self.Menu_spec = [
            ('loop', self.cmd_Start),
         ]
        return

    def cmd_Start(self): # renamed from myloop
        # WARNING: this does not return until the loop stops; it does recursive event processing
        # (including going into temporary subcommand modes, or even going entirely into other modes)
        # while it runs. If we wish some subcommands to suspect the simtime updates and redraws this
        # does, they should somehow set a flag here which affects this loop, since they have no
        # direct way to exit this loop immediately.
        if SILLY_TEST:
            self.cannonWidth = 5.0 - self.cannonWidth # test whether pm gets updated -- it doesn't (bug)
        if self._in_loop:
            print "cmd_Start: already in loop, ignoring"
            #e future: increase the time remaining
            return
        print "cmd_Start: starting loop"
        glpane = self.glpane
        starttime = self._loop_start_time = time.time()
        start_simtime = self.simtime
        safety_timeout = 600.0 # 10 minutes -- not really needed as long as 's' works to stop the loop
        self._please_exit_loop = False
        self._in_loop = True
        # if the menu stays up all this time, we'll have to instead set a flag to make this happen later, or so...
        # anyway, it doesn't.
        min_frame_time = 0.1 ### 0.02
        while not self._please_exit_loop and time.time() < starttime + safety_timeout:
            ## glpane.quat += Q(glpane.up, glpane.out) # works
            self.simtime = start_simtime + (time.time() - starttime)
            glpane.gl_update_duration()
                # This processes events (including keypresses etc, which mode should record),
                # and then does a redraw (which should update the state vars first),
                # times the redraw (and the other event processing) in glpane._repaint_start_time, glpane._repaint_end_time,
                # and sets glpane._repaint_duration =  max(MIN_REPAINT_TIME, duration), where MIN_REPAINT_TIME = 0.01.
                #
                # If any of those events exit this mode, that will happen right away, but our _clear_command_state method
                # will run cmd_Stop so that this loop will exit ASAP. To make this exit faster,
                # we also test _please_exit_loop just below. (REVIEW: do we need to set such a flag to be tested
                # inside gl_update_duration itself??)
            if not self._please_exit_loop: #### TODO: also test whether app is exiting, in this and every other internal event loop
                duration = glpane._repaint_end_time - glpane._repaint_start_time
                # this is actual duration of state-update-calcs and redraw. if too fast, slow down!
                if duration < min_frame_time:
                    time.sleep(min_frame_time - duration)
            continue
        ## self.simtime = start_simtime + (time.time() - starttime) -- no, might cause glitch on next redraw
        self._in_loop = False
        return

    def cmd_Stop(self):
        if self._in_loop:
            print "cmd_Stop: exiting loop"
            self._please_exit_loop = True
        else:
            print "cmd_Stop: not in loop, ignoring" #e show this msg in PM somewhere?
        return

    pass # end of class test_animation_mode

# DebugNode

# end