-
Notifications
You must be signed in to change notification settings - Fork 5
/
pyurcad.py
2825 lines (2530 loc) · 113 KB
/
pyurcad.py
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
"""
PyurCad is Pure CAD in the sense that it uses only Python3 and the standard
libraries that come with it. It is a rewrite of cadvas without the use of
Python MegaWidgets.
"""
import math
import os
import pickle
import pprint
import tkinter as tk
from tkinter import filedialog
from tkinter import messagebox
import entities
import geometryhelpers as gh
import tkrpncalc
import txtdialog
from zooming import Zooming
import matrix
GEOMCOLOR = 'white' # color of geometry entities
CONSTRCOLOR = 'magenta' # color of construction entities
TEXTCOLOR = 'white' # color of text entities
DIMCOLOR = 'red' # color of dimension entities
RUBBERCOLOR = 'yellow' # color of (temporary) rubber elements
TOOLBARCOLS = 2 # number of columns of toolbar buttons
class PyurCad(tk.Tk):
tool_bar_function_names = {'noop': "No Current Operation",
'hvcl': "Horizontal & Vertical Construction Line",
'hcl': "Horizontal Construction Line",
'vcl': "Vertical Construction Line",
'cl2p': "Construction Line by 2 Points",
'acl': "Angled Construction Line",
'clrefang': "Construction Line by Ref Angle",
'abcl': "Angle Bisector Construction Line",
'lbcl': "Linear Bisector Consturction Line",
'parcl': "Parallel Construction Line",
'perpcl': "Perpendicular Construction Line",
'cltan1': "Construction Line Tangent to Circle",
'cltan2': "Construction Line Tangent to 2 Circles",
'ccirc': "Construction Circle",
'cc3p': "Construction Circle by 3 Points",
'cccirc': "Concentric Construction Circle",
'line': "Line",
'poly': "Poly Line",
'rect': "Rectangle",
'circ': "Circle",
'arcc2p': "Arc by Center, Start, End Point",
'arc3p': "Arc by 3 Points",
'slot': "Slot by 2 Points & Width",
'split': "Split Line",
'join': "Join 2 Lines",
'fillet': "Fillet 2 Adjacent Lines",
'translate': "Translate Geometry (&/or Text)",
'rotate': "Rotate Geometry"}
tool_bar_functions = ('noop', 'hvcl', 'hcl', 'vcl', 'cl2p', 'acl', 'clrefang',
'abcl', 'lbcl', 'parcl', 'perpcl', 'cltan1', 'cltan2',
'ccirc', 'cc3p', 'cccirc', 'line', 'poly', 'rect',
'circ', 'arcc2p', 'arc3p', 'slot', 'split', 'join',
'fillet', 'translate', 'rotate')
selected_tool_bar_function = tool_bar_functions[0]
catchCntr = False
catch_pnt = None # ID of (temporary) catch point
catch_radius = 5 # radius of catch region
catch_pnt_size = 5 # size of displayed catch point
rubber = None # ID of (temporary) rubber element
rtext = None # ID of (temporary) rubber text
sel_boxID = None # ID of (temporary) selection box
op = '' # current CAD operation (create or modify)
op_stack = []
text_entry_enable = 0
text = ''
curr = {} # all entities in curr dwg {k=handle: v=entity}
prev = {}
allow_list = 0 # enable/disable item selection in list mode
sel_mode = '' # selection mode for screen picks
float_stack = [] # float values (unitless)
pt_stack = [] # points, in ECS (mm) units
obj_stack = [] # canvas items picked from the screen
sel_box_crnr = None # first corner of selection box, if any
undo_stack = [] # list of dicts of sets of entities
redo_stack = [] # data popped off undo_stack
filename = None # name of file currently loaded (or saved as)
dimgap = 10 # extension line gap (in canvas units)
textsize = 10 # default text size
textstyle = 'Calibri' # default text style
TEXTCOLOR = TEXTCOLOR
CONSTR_DASH = 2 # dash size for construction lines & circles
modified_text_object = None
cl_list = [] # list of all cline coords (so they don't get lost)
shift_key_advice = ' (Use SHIFT key to select center of element)'
unit_dict = {'mm': 1.0,
'inches': 25.4,
'feet': 304.8}
units = 'mm'
unitscale = unit_dict[units]
calculator = None # reference to a Toplevel window
txtdialog = None # reference to a Toplevel window
popup = None
msg = "Left-Click a tool button to start. Middle-Click on screen to end."
# =======================================================================
# Functions for converting between canvas CS and engineering CS
# =======================================================================
def ep2cp(self, pt):
"""Convert pt from ECS to CCS."""
return self.canvas.world2canvas(pt[0], -pt[1])
def cp2ep(self, pt):
"""Convert pt from CCS to ECS."""
x, y = self.canvas.canvas2world(pt[0], pt[1])
return (x, -y)
# =======================================================================
# File, View, Units and Measure commands
# =======================================================================
def printps(self):
openfile = None
ftypes = [('PostScript file', '*.ps'),
('All files', '*')]
openfile = filedialog.asksaveasfilename(filetypes=ftypes)
if openfile:
outfile = os.path.abspath(openfile)
self.ipostscript(outfile)
def ipostscript(self, file='drawing.ps'):
ps = self.canvas.postscript()
ps = ps.replace('1.000 1.000 1.000 setrgbcolor',
'0.000 0.000 0.000 setrgbcolor')
fd = open(file, 'w')
fd.write(ps)
fd.close()
def fileOpen(self):
openfile = None
ftypes = [('CADvas dwg', '*.pkl'),
('All files', '*')]
openfile = filedialog.askopenfilename(filetypes=ftypes,
defaultextension='.pkl')
if openfile:
infile = os.path.abspath(openfile)
self.load(infile)
def fileImport(self):
openfile = None
ftypes = [('DXF format', '*.dxf'),
('All files', '*')]
openfile = filedialog.askopenfilename(filetypes=ftypes,
defaultextension='.dxf')
if openfile:
infile = os.path.abspath(openfile)
self.load(infile)
def fileSave(self):
openfile = self.filename
if openfile:
outfile = os.path.abspath(openfile)
self.save(outfile)
else:
self.fileSaveas()
def fileSaveas(self):
ftypes = [('CADvas dwg', '*.pkl'),
('All files', '*')]
openfile = filedialog.asksaveasfilename(filetypes=ftypes,
defaultextension='.pkl')
if openfile:
self.filename = openfile
outfile = os.path.abspath(openfile)
self.save(outfile)
def fileExport(self):
ftypes = [('DXF format', '*.dxf'),
('All files', '*')]
openfile = filedialog.asksaveasfilename(filetypes=ftypes,
defaultextension='.dxf')
if openfile:
outfile = os.path.abspath(openfile)
self.save(outfile)
def save(self, file):
drawlist = []
for entity in self.curr.values():
drawlist.append({entity.type: entity.get_attribs()})
fext = os.path.splitext(file)[-1]
if fext == '.dxf':
import dxf
dxf.native2dxf(drawlist, file)
elif fext == '.pkl':
with open(file, 'wb') as f:
pickle.dump(drawlist, f)
self.filename = file
elif not fext:
print("Please type entire filename, including extension.")
else:
print("Save files of type {fext} not supported.")
def load(self, file):
"""Load CAD data from file.
Data is saved/loaded as a list of dicts, one dict for each
drawing entity, {key=entity_type: val=entity_attribs} """
fext = os.path.splitext(file)[-1]
if fext == '.dxf':
import dxf
drawlist = dxf.dxf2native(file)
elif fext == '.pkl':
with open(file, 'rb') as f:
drawlist = pickle.load(f)
self.filename = file
else:
print("Load files of type {fext} not supported.")
for ent_dict in drawlist:
if 'cl' in ent_dict:
attribs = ent_dict['cl']
e = entities.CL(attribs)
self.cline_gen(e.coords) # This method takes coords
elif 'cc' in ent_dict:
attribs = ent_dict['cc']
e = entities.CC(attribs)
self.ccirc_gen(e)
elif 'gl' in ent_dict:
attribs = ent_dict['gl']
e = entities.GL(attribs)
self.gline_gen(e)
elif 'gc' in ent_dict:
attribs = ent_dict['gc']
e = entities.GC(attribs)
self.gcirc_gen(e)
elif 'ga' in ent_dict:
attribs = ent_dict['ga']
e = entities.GA(attribs)
self.garc_gen(e)
elif 'dl' in ent_dict:
attribs = ent_dict['dl']
e = entities.DL(attribs)
self.dim_gen(e)
elif 'tx' in ent_dict:
attribs = ent_dict['tx']
print(attribs)
e = entities.TX(attribs)
self.text_gen(e)
self.view_fit()
self.save_delta() # undo/redo thing
def close(self):
self.quit()
def view_fit(self):
bbox = self.canvas.bbox('g', 'd', 't')
if bbox:
xsize, ysize = bbox[2]-bbox[0], bbox[3]-bbox[1]
xc, yc = (bbox[2]+bbox[0])/2, (bbox[3]+bbox[1])/2
w, h = self.canvas.winfo_width(), self.canvas.winfo_height()
self.canvas.move_can(w/2-xc, h/2-yc)
wm, hm = .9 * w, .9 * h
xscale, yscale = wm/float(xsize), hm/float(ysize)
if xscale > yscale:
scale = yscale
else:
scale = xscale
self.canvas.scale(w/2, h/2, scale, scale)
self.regen()
def regen(self, event=None):
self.regen_all_cl()
self.regen_all_dims()
self.regen_all_text()
def set_units(self, units):
if units in self.unit_dict.keys():
self.units = units
self.unitscale = self.unit_dict.get(units)
self.unitsDisplay.configure(text="Units: %s" % self.units)
self.regen_all_dims()
def meas_dist(self, obj=None):
"""Measure distance between 2 points."""
self.op = 'meas_dist'
if not self.pt_stack:
self.update_message_bar('Pick 1st point for distance measurement.')
self.set_sel_mode('pnt')
elif len(self.pt_stack) == 1:
self.update_message_bar('Pick 2nd point for distance measurement.')
elif len(self.pt_stack) > 1:
p2 = self.pt_stack.pop()
p1 = self.pt_stack.pop()
dist = gh.p2p_dist(p1, p2)/self.unitscale
self.update_message_bar('%s %s' % (dist, self.units))
self.launch_calc()
self.calculator.putx(dist)
def itemcoords(self, obj=None):
"""Print coordinates (in ECS) of selected element."""
if not self.obj_stack:
self.update_message_bar('Pick element from drawing.')
self.set_sel_mode('items')
elif self.obj_stack:
elem = self.obj_stack.pop()
if 'g' in self.canvas.gettags(elem):
x1, y1, x2, y2 = self.canvas.coords(elem)
print(self.cp2ep((x1, y1)), self.cp2ep((x2, y2)))
else:
print("This works only for 'geometry type' elements")
def itemlength(self, obj=None):
"""Print length (in current units) of selected line, circle, or arc."""
if not self.obj_stack:
self.update_message_bar('Pick element from drawing.')
self.set_sel_mode('items')
elif self.obj_stack:
elem = None
length = 0
for item in self.obj_stack.pop():
if 'g' in self.canvas.gettags(item):
elem = self.curr[item]
if elem.type == 'gl':
p1, p2 = elem.coords
length = gh.p2p_dist(p1, p2) / self.unitscale
elif elem.type == 'gc':
length = math.pi*2*elem.coords[1]/self.unitscale
elif elem.type == 'cc':
length = math.pi*2*elem.coords[1]/self.unitscale
elif elem.type == 'ga':
pc, r, a0, a1 = elem.coords
ang = float(self.canvas.itemcget(item, 'extent'))
length = math.pi*r*ang/180/self.unitscale
if length:
self.launch_calc()
self.calculator.putx(length)
def launch_calc(self):
if not self.calculator:
self.calculator = tkrpncalc.Calculator(self)
#self.calculator.grab_set()
self.calculator.geometry('+800+50')
def on_close_menu_clicked(self):
self.close_window()
def close_window(self):
if messagebox.askokcancel("Quit", "Do you really want to quit?"):
self.destroy()
def on_about_menu_clicked(self, event=None):
messagebox.showinfo(
"About", "PYurCAD (pureCAD)\n Doug Blanding\n [email protected]")
# =======================================================================
# Debug Tools
# =======================================================================
def show_curr(self):
pprint.pprint(self.curr)
self.end()
def show_prev(self):
pprint.pprint(self.prev)
self.end()
def show_undo(self):
pprint.pprint(self.undo_stack)
self.end()
def show_redo(self):
pprint.pprint(self.redo_stack)
self.end()
def show_zoomscale(self):
zoom_scale = self.canvas.scl.x
print(zoom_scale)
self.end()
def show_calc(self):
print(self.calculator)
self.end()
def show_dir_self(self):
pprint.pprint(dir(self))
self.end()
def draw_line(self):
self.current_item = self.canvas.create_line(
self.start_x, self.start_y, self.end_x, self.end_y,
fill=self.fill, width=self.width, arrow=self.arrow, dash=self.dash)
def draw_workplane(self):
start_x, start_y = self.ep2cp((-100, -100))
end_x, end_y = self.ep2cp((400, 400))
self.wp = self.canvas.create_rectangle(
start_x, start_y, end_x, end_y, outline='#d5ffd5', fill=None, width=20)
# =======================================================================
# Construction
# construction lines (clines) are "infinite" length lines
# described by the equation: ax + by + c = 0
# they are defined by coefficients: (a, b, c)
#
# circles are defined by coordinates: (pc, r)
# =======================================================================
def cline_gen(self, cline, rubber=0, regen=False):
'''Generate clines from coords (a,b,c) in ECS (mm) values.'''
# extend clines 500 canvas units beyond edge of canvas
w, h = self.canvas.winfo_width(), self.canvas.winfo_height()
toplft = self.cp2ep((-500, -500))
botrgt = self.cp2ep((w+500, h+500))
trimbox = (toplft[0], toplft[1], botrgt[0], botrgt[1])
endpts = gh.cline_box_intrsctn(cline, trimbox)
if len(endpts) == 2:
p1 = self.ep2cp(endpts[0])
p2 = self.ep2cp(endpts[1])
if rubber:
if self.rubber:
self.canvas.coords(self.rubber, p1[0], p1[1], p2[0], p2[1])
else:
self.rubber = self.canvas.create_line(p1[0], p1[1],
p2[0], p2[1],
fill=CONSTRCOLOR,
tags='r',
dash=self.CONSTR_DASH)
else:
if self.rubber:
self.canvas.delete(self.rubber)
self.rubber = None
handle = self.canvas.create_line(p1[0], p1[1], p2[0], p2[1],
fill=CONSTRCOLOR, tags='c',
dash=self.CONSTR_DASH)
self.canvas.tag_lower(handle)
attribs = (cline, CONSTRCOLOR)
e = entities.CL(attribs)
self.curr[handle] = e
if not regen:
self.cl_list.append(cline)
def regen_all_cl(self, event=None):
"""Delete existing clines, remove them from self.curr, and regenerate
This needs to be done after pan or zoom because the "infinite" length
clines are not really infinite, they just hang off the edge a bit. So
when zooming out, new clines need to be generated so they extend over
the full canvas. Also, when zooming in, some clines are completely off
the canvas, so we need a way to keep them from getting lost."""
cl_keylist = [k for k, v in self.curr.items() if v.type == 'cl']
for handle in cl_keylist:
self.canvas.delete(handle)
del self.curr[handle]
for cline in self.cl_list:
self.cline_gen(cline, regen=True)
def hcl(self, pnt=None):
"""Create horizontal construction line from one point or y value."""
message = 'Pick a pt or enter a value'
message += self.shift_key_advice
self.update_message_bar(message)
proceed = 0
if self.pt_stack:
p = self.pt_stack.pop()
proceed = 1
elif self.float_stack:
y = self.float_stack.pop()*self.unitscale
p = (0, y)
proceed = 1
elif pnt:
p = self.cp2ep(pnt)
cline = gh.angled_cline(p, 0)
self.cline_gen(cline, rubber=1)
if proceed:
cline = gh.angled_cline(p, 0)
self.cline_gen(cline)
def vcl(self, pnt=None):
"""Create vertical construction line from one point or x value."""
message = 'Pick a pt or enter a value'
message += self.shift_key_advice
self.update_message_bar(message)
proceed = 0
if self.pt_stack:
p = self.pt_stack.pop()
proceed = 1
elif self.float_stack:
x = self.float_stack.pop()*self.unitscale
p = (x, 0)
proceed = 1
elif pnt:
p = self.cp2ep(pnt)
cline = gh.angled_cline(p, 90)
self.cline_gen(cline, rubber=1)
if proceed:
cline = gh.angled_cline(p, 90)
self.cline_gen(cline)
def hvcl(self, pnt=None):
"""Create a horizontal & vertical construction line pair at a point."""
message = 'Pick a pt or enter coords x,y'
message += self.shift_key_advice
self.update_message_bar(message)
if self.pt_stack:
p = self.pt_stack.pop()
self.cline_gen(gh.angled_cline(p, 0))
self.cline_gen(gh.angled_cline(p, 90))
def cl2p(self, pnt=None):
"""Create construction line thru 2 points."""
if not self.pt_stack:
message = 'Pick 1st point or enter coords'
message += self.shift_key_advice
self.update_message_bar(message)
elif len(self.pt_stack) == 1:
message = 'Pick 2nd point or enter coords'
message += self.shift_key_advice
self.update_message_bar(message)
if pnt:
p0 = self.pt_stack[0]
p1 = self.cp2ep(pnt)
ang = gh.p2p_angle(p0, p1)
cline = gh.angled_cline(p0, ang)
self.cline_gen(cline, rubber=1)
elif len(self.pt_stack) > 1:
p1 = self.pt_stack.pop()
p0 = self.pt_stack.pop()
cline = gh.cnvrt_2pts_to_coef(p0, p1)
self.cline_gen(cline)
def acl(self, pnt=None):
"""Create construction line thru a point, at a specified angle."""
if not self.pt_stack:
message = 'Pick a pt for angled construction line or enter coords'
message += self.shift_key_advice
self.update_message_bar(message)
elif self.pt_stack and self.float_stack:
p0 = self.pt_stack[0]
ang = self.float_stack.pop()
cline = gh.angled_cline(p0, ang)
self.cline_gen(cline)
elif len(self.pt_stack) > 1:
p0 = self.pt_stack[0]
p1 = self.pt_stack.pop()
cline = gh.cnvrt_2pts_to_coef(p0, p1)
self.cline_gen(cline)
elif self.pt_stack and not self.float_stack:
message = 'Specify 2nd point or enter angle in degrees'
message += self.shift_key_advice
self.update_message_bar(message)
if pnt:
p0 = self.pt_stack[0]
p1 = self.cp2ep(pnt)
ang = gh.p2p_angle(p0, p1)
cline = gh.angled_cline(p0, ang)
self.cline_gen(cline, rubber=1)
def clrefang(self, p3=None):
"""Create a construction line at an angle relative to a reference."""
if not self.pt_stack:
message = 'Specify a pt for new construction line'
message += self.shift_key_advice
self.update_message_bar(message)
elif not self.float_stack:
self.update_message_bar('Enter offset angle in degrees')
elif len(self.pt_stack) == 1:
message = 'Pick first point on reference line'
message += self.shift_key_advice
self.update_message_bar(message)
elif len(self.pt_stack) == 2:
message = 'Pick second point on reference line'
message += self.shift_key_advice
self.update_message_bar(message)
elif len(self.pt_stack) == 3:
p3 = self.pt_stack.pop()
p2 = self.pt_stack.pop()
p1 = self.pt_stack.pop()
baseangle = gh.p2p_angle(p2, p3)
angoffset = self.float_stack.pop()
ang = baseangle + angoffset
cline = gh.angled_cline(p1, ang)
self.cline_gen(cline)
def abcl(self, pnt=None):
"""Create an angular bisector construction line."""
if not self.float_stack and not self.pt_stack:
message = 'Enter bisector factor (Default=.5) or specify vertex'
message += self.shift_key_advice
self.update_message_bar(message)
elif not self.pt_stack:
message = 'Specify vertex point'
message += self.shift_key_advice
self.update_message_bar(message)
elif len(self.pt_stack) == 1:
self.update_message_bar('Specify point on base line')
elif len(self.pt_stack) == 2:
self.update_message_bar('Specify second point')
if pnt:
f = .5
if self.float_stack:
f = self.float_stack[-1]
p2 = self.cp2ep(pnt)
p1 = self.pt_stack[-1]
p0 = self.pt_stack[-2]
cline = gh.ang_bisector(p0, p1, p2, f)
self.cline_gen(cline, rubber=1)
elif len(self.pt_stack) == 3:
f = .5
if self.float_stack:
f = self.float_stack[-1]
p2 = self.pt_stack.pop()
p1 = self.pt_stack.pop()
p0 = self.pt_stack.pop()
cline = gh.ang_bisector(p0, p1, p2, f)
self.cline_gen(cline)
def lbcl(self, pnt=None):
"""Create a linear bisector construction line."""
if not self.pt_stack and not self.float_stack:
message = 'Enter bisector factor (Default=.5) or specify first point'
message += self.shift_key_advice
self.update_message_bar(message)
elif not self.pt_stack:
message = 'Specify first point'
message += self.shift_key_advice
self.update_message_bar(message)
elif len(self.pt_stack) == 1:
message = 'Specify second point'
message += self.shift_key_advice
self.update_message_bar(message)
if pnt:
f = .5
if self.float_stack:
f = self.float_stack[-1]
p2 = self.cp2ep(pnt)
p1 = self.pt_stack[-1]
p0 = gh.midpoint(p1, p2, f)
baseline = gh.cnvrt_2pts_to_coef(p1, p2)
newline = gh.perp_line(baseline, p0)
self.cline_gen(newline, rubber=1)
elif len(self.pt_stack) == 2:
f = .5
if self.float_stack:
f = self.float_stack[-1]
p2 = self.pt_stack.pop()
p1 = self.pt_stack.pop()
p0 = gh.midpoint(p1, p2, f)
baseline = gh.cnvrt_2pts_to_coef(p1, p2)
newline = gh.perp_line(baseline, p0)
self.cline_gen(newline)
def parcl(self, pnt=None):
"""Create parallel clines in one of two modes:
1) At a specified offset distance from selected straight element, or
2) Parallel to a selected straight element through a selected point."""
if not self.obj_stack and not self.float_stack:
self.update_message_bar(
'Pick a straight element or enter an offset distance')
self.set_sel_mode('items')
elif self.float_stack: # mode 1
if not self.obj_stack:
self.set_sel_mode('items')
self.update_message_bar(
'Pick a straight element to be parallel to')
elif not self.pt_stack:
self.set_sel_mode('pnt')
self.update_message_bar('Pick on (+) side of line')
else:
obj = self.obj_stack.pop()
p = self.pt_stack.pop()
item = obj[0]
baseline = (0, 0, 0)
if self.canvas.type(item) == 'line':
if 'c' in self.canvas.gettags(item):
baseline = self.curr[item].coords
elif 'g' in self.canvas.gettags(item):
p1, p2 = self.curr[item].coords
baseline = gh.cnvrt_2pts_to_coef(p1, p2)
d = self.float_stack[-1]*self.unitscale
cline1, cline2 = gh.para_lines(baseline, d)
p1 = gh.proj_pt_on_line(cline1, p)
p2 = gh.proj_pt_on_line(cline2, p)
d1 = gh.p2p_dist(p1, p)
d2 = gh.p2p_dist(p2, p)
if d1 < d2:
self.cline_gen(cline1)
else:
self.cline_gen(cline2)
elif self.obj_stack: # mode 2
obj = self.obj_stack[-1]
if not obj:
return
item = obj[0]
baseline = (0, 0, 0)
if self.canvas.type(item) == 'line':
if 'c' in self.canvas.gettags(item):
baseline = self.curr[item].coords
elif 'g' in self.canvas.gettags(item):
p1, p2 = self.curr[item].coords
baseline = gh.cnvrt_2pts_to_coef(p1, p2)
if not self.pt_stack:
self.set_sel_mode('pnt')
message = 'Select point for new parallel line'
message += self.shift_key_advice
self.update_message_bar(message)
if pnt:
p = self.cp2ep(pnt)
parline = gh.para_line(baseline, p)
self.cline_gen(parline, rubber=1)
else:
p = self.pt_stack.pop()
newline = gh.para_line(baseline, p)
self.cline_gen(newline)
def perpcl(self, pnt=None):
"""Create a perpendicular cline through a selected point."""
if not self.obj_stack:
self.update_message_bar('Pick line to be perpendicular to')
self.set_sel_mode('items')
else:
message = 'Select point for perpendicular construction'
message += self.shift_key_advice
self.update_message_bar(message)
self.set_sel_mode('pnt')
obj = self.obj_stack[0]
if not obj:
return
item = obj[0]
baseline = (0, 0, 0)
if self.canvas.type(item) == 'line':
if 'c' in self.canvas.gettags(item):
baseline = self.curr[item].coords
elif 'g' in self.canvas.gettags(item):
p1, p2 = self.curr[item].coords
baseline = gh.cnvrt_2pts_to_coef(p1, p2)
if self.pt_stack:
p = self.pt_stack.pop()
newline = gh.perp_line(baseline, p)
self.cline_gen(newline)
self.obj_stack.pop()
elif pnt:
p = self.cp2ep(pnt)
newline = gh.perp_line(baseline, p)
self.cline_gen(newline, rubber=1)
def cltan1(self, p1=None):
'''Create a construction line through a point, tangent to a circle.'''
if not self.obj_stack:
self.update_message_bar('Pick circle')
self.set_sel_mode('items')
elif self.obj_stack and not self.pt_stack:
self.update_message_bar('specify point')
self.set_sel_mode('pnt')
elif self.obj_stack and self.pt_stack:
item = self.obj_stack.pop()[0]
p = self.pt_stack.pop()
circ = None
if self.curr[item].type in ('gc', 'cc'):
circ = self.curr[item].coords
if circ:
p1, p2 = gh.line_tan_to_circ(circ, p)
cline1 = gh.cnvrt_2pts_to_coef(p1, p)
cline2 = gh.cnvrt_2pts_to_coef(p2, p)
self.cline_gen(cline1)
self.cline_gen(cline2)
def cltan2(self, p1=None):
'''Create a construction line tangent to 2 circles.'''
if not self.obj_stack:
self.update_message_bar('Pick first circle')
self.set_sel_mode('items')
elif len(self.obj_stack) == 1:
self.update_message_bar('Pick 2nd circle')
elif len(self.obj_stack) == 2:
item1 = self.obj_stack.pop()[0]
item2 = self.obj_stack.pop()[0]
circ1 = circ2 = None
if self.curr[item1].type in ('gc', 'cc'):
circ1 = self.curr[item1].coords
if self.curr[item2].type in ('gc', 'cc'):
circ2 = self.curr[item2].coords
if circ1 and circ2:
p1, p2 = gh.line_tan_to_2circs(circ1, circ2)
cline = gh.cnvrt_2pts_to_coef(p1, p2)
self.cline_gen(cline)
def ccirc_gen(self, cc, tag='c'):
"""Create constr circle from a CC object. Save to self.curr."""
coords, color = cc.get_attribs()
handle = self.circ_draw(coords, color, tag=tag)
self.curr[handle] = cc
self.canvas.tag_lower(handle)
def ccirc(self, p1=None):
'''Create a construction circle from center point and
perimeter point or radius.'''
self.circ(p1=p1, constr=1)
def cccirc(self, p1=None):
'''Create a construction circle concentric to an existing circle,
at a "relative" radius.'''
if not self.obj_stack:
self.set_sel_mode('items')
self.update_message_bar('Select existing circle')
elif self.obj_stack and not (self.float_stack or self.pt_stack):
item = self.obj_stack[0][0]
self.coords = None
if self.curr[item].type in ('cc', 'gc'):
self.coords = self.curr[item].coords
self.set_sel_mode('pnt')
self.update_message_bar(
'Enter relative radius or specify point on new circle')
if self.coords and p1:
pc, r0 = self.coords
ep = self.cp2ep(p1)
r = gh.p2p_dist(pc, ep)
self.circ_builder((pc, r), rubber=1)
elif self.coords and self.float_stack:
pc, r0 = self.coords
self.obj_stack.pop()
r = self.float_stack.pop()*self.unitscale + r0
self.circ_builder((pc, r), constr=1)
elif self.coords and self.pt_stack:
pc, r0 = self.coords
self.obj_stack.pop()
p = self.pt_stack.pop()
r = gh.p2p_dist(pc, p)
self.circ_builder((pc, r), constr=1)
def cc3p(self, p3=None):
"""Create a constr circle from 3 pts on circle."""
if not self.pt_stack:
self.update_message_bar('Pick first point on circle')
elif len(self.pt_stack) == 1:
self.update_message_bar('Pick second point on circle')
elif len(self.pt_stack) == 2:
self.update_message_bar('Pick third point on circle')
if p3:
p3 = self.cp2ep(p3)
p2 = self.pt_stack[1]
p1 = self.pt_stack[0]
tup = gh.cr_from_3p(p1, p2, p3)
if tup:
pc, r = tup
self.circ_builder((pc, r,), rubber=1)
elif len(self.pt_stack) == 3:
p3 = self.pt_stack.pop()
p2 = self.pt_stack.pop()
p1 = self.pt_stack.pop()
pc, r = gh.cr_from_3p(p1, p2, p3)
self.circ_builder((pc, r), constr=1)
# =======================================================================
# Geometry
# geometry line parameters are stored in GL objects.
# geometry lines are finite length segments between 2 pts: p1, p2
# lines are defined by coordinates: (p1, p2)
#
# =======================================================================
def line_draw(self, coords, color, arrow=None, tag='g'):
"""Create and display line segment between two pts. Return ID.
This == a low level method that accesses the canvas directly &
returns tkid. The caller can save to self.curr if needed."""
p1, p2 = coords
xa, ya = self.ep2cp(p1)
xb, yb = self.ep2cp(p2)
tkid = self.canvas.create_line(xa, ya, xb, yb,
fill=color, tags=tag, arrow=arrow)
return tkid
def gline_gen(self, gl):
"""Create line segment from gl object. Store {ID: obj} in self.curr.
This provides access to line_gen using a gl object."""
coords, color = gl.get_attribs()
tkid = self.line_draw(coords, color)
self.curr[tkid] = gl
def line(self, p1=None):
'''Create line segment between 2 points. Enable 'rubber line' mode'''
rc = RUBBERCOLOR
if not self.pt_stack:
message = 'Pick start point of line or enter coords'
message += self.shift_key_advice
self.update_message_bar(message)
elif len(self.pt_stack) > 1:
p2 = self.pt_stack.pop()
p1 = self.pt_stack.pop()
coords = (p1, p2)
attribs = (coords, GEOMCOLOR)
e = entities.GL(attribs)
self.gline_gen(e)
if self.rubber:
self.canvas.delete(self.rubber)
self.rubber = None
if self.rtext:
self.canvas.delete(self.rtext)
self.rtext = None
elif self.pt_stack and p1:
p0 = self.pt_stack[-1]
x, y = self.ep2cp(p0) # fixed first point (canvas coords)
xr, yr = p1 # rubber point (canvas coords)
x0, y0 = p0 # fixed first point (ECS)
x1, y1 = self.cp2ep(p1) # rubber point (ECS)
strcoords = "(%1.3f, %1.3f)" % ((x1-x0)/self.unitscale,
(y1-y0)/self.unitscale)
if self.rubber:
self.canvas.coords(self.rubber, x, y, xr, yr)
else:
self.rubber = self.canvas.create_line(x, y, xr, yr,
fill=rc, tags='r')
if self.rtext:
self.canvas.delete(self.rtext)
self.rtext = self.canvas.create_text(xr+20, yr-20,
text=strcoords,
fill=TEXTCOLOR)
self.update_message_bar('Specify end point of line')
def poly(self, p1=None):
'''Create chain of line segments, enabling 'rubber line' mode.'''
if not self.pt_stack:
self.poly_start_pt = None
message = 'Pick start point or enter coords'
message += self.shift_key_advice
self.update_message_bar(message)
elif len(self.pt_stack) > 1:
lastpt = self.pt_stack[-1]
self.line() # This will pop 2 points off stack
if not gh.same_pt_p(self.poly_start_pt, lastpt):
self.pt_stack.append(lastpt)
elif self.pt_stack and p1:
if not self.poly_start_pt:
self.poly_start_pt = self.pt_stack[-1]
self.line(p1) # This will generate rubber line
self.update_message_bar('Pick next point or enter coords')
def rect(self, p2=None):
'''Generate a rectangle from 2 diagonally opposite corners.'''
rc = RUBBERCOLOR
if not self.pt_stack:
self.update_message_bar(
'Pick first corner of rectangle or enter coords')
elif len(self.pt_stack) == 1 and p2:
self.update_message_bar(
'Pick opposite corner of rectangle or enter coords')
p1 = self.pt_stack[0]
x1, y1 = self.ep2cp(p1)
x2, y2 = p2
if self.rubber:
self.canvas.coords(self.rubber, x1, y1, x2, y2)
else:
self.rubber = self.canvas.create_rectangle(x1, y1, x2, y2,
outline=rc,
tags='r')
elif len(self.pt_stack) > 1:
x2, y2 = self.pt_stack.pop()
x1, y1 = self.pt_stack.pop()
a = (x1, y1)
b = (x2, y1)
c = (x2, y2)
d = (x1, y2)
sides = ((a, b), (b, c), (c, d), (d, a))
for p in sides:
coords = (p[0], p[1])
attribs = (coords, GEOMCOLOR)
e = entities.GL(attribs)
self.gline_gen(e)
if self.rubber:
self.canvas.delete(self.rubber)
self.rubber = None
# =======================================================================