This repository has been archived by the owner on Oct 2, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 11
/
tikz.lua
1772 lines (1573 loc) · 58.5 KB
/
tikz.lua
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
----------------------------------------------------------------------
-- tikz export ipelet
----------------------------------------------------------------------
--[[
Copyright (C) 2016 Joseph Rabinoff
ipe2tikz is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free
Software Foundation; either version 3 of the License, or (at your option)
any later version.
ipe2tikz is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
details.
You should have received a copy of the GNU General Public License along with
ipe2tikz; if not, you can find it at "http://www.gnu.org/copyleft/gpl.html",
or write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA
02139, USA.
--]]
label = "TikZ export"
methods = {
{ label="Export to File", run=run },
{ label="Export to Text Object", run=run }
}
about = "Export readable TikZ code"
shortcuts.ipelet_1_tikz = "Alt+T"
shortcuts.ipelet_2_tikz = "Ctrl+Shift+T"
-- Globals
write = _G.io.write
indent_amt = " "
indent = ""
--------------------------------------------------------------------------------
-- Utility
--------------------------------------------------------------------------------
function concat_pairs(t, sep, order)
local ret = ""
local first = true
for i,key in ipairs(order) do
if t[key] then
if not first then
ret = ret .. sep
else
first = false
end
ret = ret .. key .. "=" .. t[key]
end
end
return ret
end
-- Round a number to idp decimal places
function round(num, idp)
local mult = 10^(idp or 4)
return math.floor(num * mult + 0.5) / mult
end
-- Convert a number to a string, omitting trailing zeros after the decimal, and
-- rounding.
function sround(num, idp)
idp = idp or 4
num = round(num, idp)
local neg = (num < 0)
if neg then
num = -num
neg = "-"
else
neg = ""
end
local intpart = math.floor(num)
local fracpart = num - intpart
local intstr = string.format("%d", intpart)
local fracstr
if fracpart == 0 then
return neg .. intstr
end
while idp > 0 do
fracstr = string.format("%."..idp.."f", fracpart)
if string.sub(fracstr, -1) ~= "0" then
-- get rid of leading zero
return neg .. intstr .. string.sub(fracstr, 2)
end
idp = idp - 1
end
end
-- Convert a Vector to a string, after rounding
function svec(vec, idp)
return "(" .. sround(vec.x, idp) .. ", " .. sround(vec.y, idp) .. ")"
end
-- Check if the linear part of a matrix is the identity, after rounding
function is_almost_identity(matrix)
local a, c, b, d = table.unpack(matrix:coeff())
return (round(a) == 1 and round(b) == 0 and round(c) == 0 and round(d) == 1)
end
-- One should be able to say "if tonumber(str) then ... end", but it seems to be
-- bugged on my system, because tonumber(str) returns 0 for an invalid number.
-- So I have to use a regex.
function is_number(str)
return (string.find(str, "^[-+]?%d*%.?%d*$") ~= nil)
end
-- Calculate a bounding box for selected, visible objects
function bounding_box(model)
local page = model:page()
local box = ipe.Rect()
for i,obj,sel,layer in page:objects() do
if page:visible(model.vno, i) and sel then box:add(page:bbox(i)) end
end
return box
end
--------------------------------------------------------------------------------
-- Translate attributes into options
--------------------------------------------------------------------------------
-- Read a value which is one of:
-- 1) a string containing a number
-- 2) a string
-- 3) the string "undefined" or the string "normal"
-- 4) nil
-- In case (1), add "key=value" to options. In case (2), add prepend .. "value"
-- to options. In cases (3) and (4), don't do anything.
function number_option(value, key, options, prepend)
if not value or value == "undefined" or value == "normal" then
-- do nothing
elseif is_number(value) then
table.insert(options, key .. "=" .. value)
else
table.insert(options, (prepend or "") .. value)
end
end
-- Read a value which is one of:
-- 1) a string
-- 2) a table with keys r, g, b
-- 3) the string "undefined"
-- 4) nil
-- In case (1), add "key=..prepend..value" to options, unless key_optional is
-- true, in which case, just add "..prepend..value". In case (2), "rgb
-- color={key=r g b}" is added. In cases (3) and (4), nothing happens.
function color_option(value, key, options, prepend, key_optional)
-- Sometimes white and black don't get stored as symbols
if _G.type(value) == "table" then
if round(value.r) == 1 and
round(value.g) == 1 and
round(value.b) == 1 then
value = "white"
elseif round(value.r) == 0 and
round(value.g) == 0 and
round(value.b) == 0 then
value = "black"
end
end
if not value or value == "undefined" then
-- do nothing
elseif _G.type(value) == "string" then
if key_optional then
key = ""
else
key = key .. "="
end
table.insert(options, key .. (prepend or "") .. value)
elseif _G.type(value) == "table" then
table.insert(options, string.format(
"rgb color={%s=%s %s %s}", key,
sround(value.r), sround(value.g), sround(value.b)))
else
print("Illegal color value:" .. tostring(value))
end
end
-- Pass a string as an option. If value is "undefined" or "normal" or nil,
-- don't do anything. Otherwise, add "key=..prepend..value", unless key is nil,
-- in which case just add "prepend..value".
function string_option(value, key, options, prepend)
if not value or value == "undefined" or value == "normal" then
-- do nothing
elseif key then
table.insert(options, key .. "=" .. (prepend or "") .. value)
else
table.insert(options, (prepend or "") .. value)
end
end
-- Turn an arrow name and size into an arrow spec, using the following
-- procedure.
-- * "shape" is parsed as "Arrow[options]", and is normally returned directly.
-- * If "size" is not "normal", it is interpreted as a number_option() and
-- appended to the arrow options.
-- * If Arrow is "To", it is replaced by ">".
-- * If Arrow is "Bar", it is replaced by "|".
-- * "Arrow" can also have the form "Arrow1[options2] Arrow2[options2] ...", in
-- which case it is also returned like this (with "To" being replaced by
-- ">"). The "size" option goes in brackets at the beginning the arrow spec.
-- * If Forward is false, ">" is replaced by "<".
--
-- In ipe the arrows inherit the line join and cap style from the path, and in
-- TikZ they don't. This follows TikZ's behavior.
function arrow_spec(shapes, size, forward)
local ret = ""
_, _, shapes = string.find(shapes, "arrow/([^(]+)%(")
local sizeopt = {}
number_option(size, "scale", sizeopt, "ipe arrow ")
if #sizeopt > 0 then
sizeopt = sizeopt[1]
else
sizeopt = nil
end
local arrows = {}
local function parse_arrow(shape)
local arrow = nil
local options = nil
_, _, arrow, options = string.find(shape, "^(%S+)%[(%S+)%]$")
if not arrow then
_, _, arrow = string.find(shape, "^(%S+)$")
end
if arrow == "To" or arrow == "normal" then
if forward then arrow = ">" else arrow = "<" end
elseif arrow == "Bar" then
arrow = "|"
elseif params.stylesheets then
arrow = "ipe " .. arrow
end
if forward then
table.insert(arrows, {arrow=arrow, options=options})
else -- reverse order
table.insert(arrows, 1, {arrow=arrow, options=options})
end
return ""
end
string.gsub(shapes .. " ", "(%S+)%s", parse_arrow)
local multiple = (#arrows > 1)
if multiple and sizeopt then
ret = "[" .. sizeopt .. "]"
end
for _,arrow in ipairs(arrows) do
if sizeopt and not multiple then
if arrow.options then
arrow.options = arrow.options .. "," .. sizeopt
else
arrow.options = sizeopt
end
end
ret = ret .. arrow.arrow
if arrow.options then
ret = ret .. "[" .. arrow.options .. "]"
end
end
if string.find(ret, "%[") then
return "{" .. ret .. "}"
else
return ret
end
end
-- Turns a PDF-style dash specification "[1 2 3 4] 5" into "dash pattern" and
-- "dash phase" options
function dash_options(dash)
local ret = ""
local dashstr, phase
local dashes = {}
_, _, dashstr, phase = string.find(dash, "%[(.*)%]%s*(%S+)")
local onedash
_, _, onedash = string.find(dashstr, "^%s*(%S+)%s*$")
if onedash then
table.insert(dashes, string.format("on %sbp off %sbp", onedash, onedash))
else
local function add_dash(on, off)
table.insert(dashes, string.format("on %sbp off %sbp", on, off))
end
string.gsub(dashstr .. " ", "(%S+)%s+(%S+)%s+", add_dash)
end
ret = "dash pattern=" .. table.concat(dashes, " ")
if tonumber(phase) ~= 0 then
ret = ret .. ", dash phase=" .. phase .. "bp"
end
return ret
end
--------------------------------------------------------------------------------
-- Decompose matrix and turn into options
--------------------------------------------------------------------------------
-- Decompose the linear part of a matrix into scale then rotate, or rotate then
-- scale, if possible. If not, just return the matrix.
function mat_decompose(matrix)
local a, c, b, d = table.unpack(matrix:coeff())
local norm1, norm2
local ret = {}
if is_almost_identity(matrix) then
ret["type"] = "identity"
return ret
end
-- Check just scale
if round(b) == 0 and round(c) == 0 then
ret["type"] = "scale"
if round(a) == round(d) then
table.insert(ret, {type="scale", scale=a})
else
table.insert(ret, {type="xyscale", xscale=a, yscale=d})
end
return ret
end
-- Check just rotate
norm1 = math.sqrt(a^2 + c^2)
norm2 = math.sqrt(b^2 + d^2)
if round(norm1) == 1 and round(norm2) == 1
and round(a) == round(d)
and round(b) == round(-c) then
ret["type"] = "rotate"
table.insert(ret, {type="rotate",
angle=math.atan2(-b,a)*180/math.pi})
return ret
end
-- Check scale then rotate
if round(a/norm1) == round(d/norm2)
and round(c/norm1) == round(-b/norm2) then
ret["type"] = "scale,rotate"
if round(norm1) == round(norm2) then
table.insert(ret, {type="scale", scale=norm1})
else
table.insert(ret, {type="xyscale",
xscale=norm1, yscale=norm2})
end
table.insert(ret,
{type="rotate",
angle=math.atan2(-b/norm2,a/norm1)*180/math.pi})
return ret
elseif round(a/norm1) == round(-d/norm2)
and round(c/norm1) == round(b/norm2) then
ret["type"] = "scale,rotate"
table.insert(ret, {type="xyscale",
xscale=norm1, yscale=-norm2})
table.insert(ret,
{type="rotate",
angle=math.atan2(b/norm2,a/norm1)*180/math.pi})
return ret
end
-- Check rotate then scale
norm1 = math.sqrt(a^2 + b^2)
norm2 = math.sqrt(c^2 + d^2)
if round(a/norm1) == round(d/norm2)
and round(b/norm1) == round(-c/norm2) then
ret["type"] = "rotate,scale"
table.insert(ret, {type="rotate",
angle=math.atan2(-b/norm1,a/norm1)*180/math.pi})
if round(norm1) == round(norm2) then
table.insert(ret, {type="scale", scale=norm1})
else
table.insert(ret, {type="xyscale",
xscale=norm1, yscale=norm2})
end
return ret
elseif round(a/norm1) == round(-d/norm2)
and round(b/norm1) == round(c/norm2) then
ret["type"] = "rotate,scale"
table.insert(ret, {type="rotate",
angle=math.atan2(-b/norm1,a/norm1)*180/math.pi})
table.insert(ret, {type="xyscale",
xscale=norm1, yscale=-norm2})
return ret
end
-- Not easily decomposable
ret["type"] = "matrix"
table.insert(ret, {type="matrix", a=a, b=b, c=c, d=d})
return ret
end
-- Turn the linear part of a transformation matrix into TikZ transform options.
-- Use rotate and scale if possible; otherwise use a general cm= option.
function matrix_to_options(matrix, options)
local decomposed = mat_decompose(matrix)
-- Go backward: inner transformations in TikZ are evaluated first
for i = #decomposed,1,-1 do
local tform = decomposed[i]
if tform.type == "scale" then
table.insert(options, "scale=" .. sround(tform.scale))
elseif tform.type == "xyscale" then
if round(tform.xscale) ~= 1 then
table.insert(options, "xscale=" .. sround(tform.xscale))
end
if round(tform.yscale) ~= 1 then
table.insert(options, "yscale=" .. sround(tform.yscale))
end
elseif tform.type == "rotate" then
table.insert(options, "rotate=" .. sround(tform.angle))
else
table.insert(options, "cm={" .. sround(tform.a) .. ","
.. sround(tform.c) .. "," .. sround(tform.b)
.. "," .. sround(tform.d) .. ",(0,0)}")
end
end
end
-- Turns the linear part of a transform matrix into TikZ transform options for
-- an ellipse / arc. If possible, specify the transformation with (x,y)radius
-- and rotate. Otherwise use the general cm= option.
function matrix_to_ellipse_options(matrix, options)
local decomposed = mat_decompose(matrix)
if #decomposed > 0 and decomposed[1].type == "rotate" then
-- Rotating a circle just changes the start and end angles (for arcs)
if options["start angle"] then
options["start angle"] = options["start angle"] + decomposed[1].angle
end
if options["end angle"] then
options["end angle"] = options["end angle"] + decomposed[1].angle
end
table.remove(decomposed, 1)
if decomposed.type == "rotate" then
decomposed.type = "identity"
elseif decomposed.type == "rotate,scale" then
decomposed.type = "scale"
end
end
if decomposed.type == "identity" then
options.radius = 1
elseif decomposed.type == "scale"
or decomposed.type == "scale,rotate" then
local tform = decomposed[1]
if tform.type == "xyscale" then
options["x radius"] = sround(tform.xscale)
options["y radius"] = sround(tform.yscale)
if decomposed.type == "scale,rotate" then
tform = decomposed[2]
options["rotate"] = sround(tform.angle)
end
else
options["radius"] = sround(tform.scale)
-- Rotating a circle only changes the start and end angles (for arcs)
if decomposed.type == "scale,rotate" then
if options["start angle"] then
options["start angle"]
= options["start angle"] + decomposed[2].angle
end
if options["end angle"] then
options["end angle"]
= options["end angle"] + decomposed[2].angle
end
end
end
else -- decomposed.type == "matrix"
local tform = decomposed[1]
options["radius"] = 1
options["cm"]
= "{" .. sround(tform.a) .. "," .. sround(tform.c)
.. "," .. sround(tform.b) .. "," .. sround(tform.d) .. ",(0,0)}"
end
-- End angle has to be greater than start angle in TikZ; otherwise the arc
-- goes backwards.
if options["start angle"] and options["end angle"] then
while options["end angle"] < options["start angle"] do
options["end angle"] = options["end angle"] + 360
end
end
if options["start angle"] then
options["start angle"] = sround(options["start angle"])
end
if options["end angle"] then
options["end angle"] = sround(options["end angle"])
end
end
--------------------------------------------------------------------------------
-- Export mark
--------------------------------------------------------------------------------
-- Marks/references are the simplest objects to export. They translate to TikZ
-- pic's, which are defined in TikZ, not here. They are unaffected by any
-- transformation matrix. The attributes stroke/draw and fill are passed to the
-- pic options. The symbolsize is passed as an option as well, using "ipe mark
-- *" and "ipe mark scale" options, which set the scaling *inside* the pic, and
-- can be overridden (unlike the scale= key, which just updates the current
-- transformation matrix).
--
-- Relevant attributes: stroke, fill, symbolsize, markshape
function export_mark(model, obj, matrix)
local options = {}
-- The "transformations" attribute has no effect on marks other than
-- translation
local pos = matrix*obj:position()
local markshape = obj:get("markshape")
local actions, drawing, filling
_, _, markshape, actions = string.find(markshape, "mark/([^(]+)%(([^)]+)%)")
markshape = "ipe " .. markshape
drawing = (string.find(actions, "s") ~= nil)
filling = (string.find(actions, "f") ~= nil)
number_option(obj:get("symbolsize"), "ipe mark scale",
options, "ipe mark ")
-- draw and fill
if drawing then
if obj:get("stroke") ~= "black" then
color_option(obj:get("stroke"), "draw", options, nil, not filling)
end
end
if filling then
color_option(obj:get("fill"), "fill", options, nil, not drawing)
end
write(indent .. "\\pic")
if #options > 0 then
write("[" .. table.concat(options, ", ") .. "]")
end
write("\n" .. indent .. indent_amt)
write(" at " .. svec(pos) .. " {" .. markshape .. "};\n")
end
--------------------------------------------------------------------------------
-- Export group
--------------------------------------------------------------------------------
-- Group objects translate into TikZ scopes. Elements in the group are
-- recursively exported, with a larger indentation. The clipping shape becomes
-- a \clip path. Group objects do not have style options, so the only options
-- to the scope are transformations.
--
-- Unlike with path objects, the group object's transformation matrix is
-- preserved and set at the beginning of the scope -- I couldn't think of a
-- natural origin in this case. In particular, translations are not absorbed
-- into the element objects' transformation matrices.
--
-- Relevant attributes: transformations
function export_group(model, obj, matrix)
local options = {}
local v = matrix:translation()
if round(v.x) ~= 0 or round(v.y) ~= 0 then
table.insert(options, "shift={" .. svec(v) .. "}")
end
matrix_to_options(matrix, options)
write(indent .. "\\begin{scope}")
if #options > 0 then
write("[" .. table.concat(options, ", ") .. "]")
end
write("\n")
local old_indent = indent
indent = indent_amt .. indent
local clip = obj:clip()
if clip then
export_path(clip, "clip", ipe.Matrix())
end
for i,element in ipairs(obj:elements()) do
export_object(model, element, ipe.Vector(0,0))
end
indent = old_indent
write(indent .. "\\end{scope}\n")
end
--------------------------------------------------------------------------------
-- Export text
--------------------------------------------------------------------------------
-- Text objects are exported as \node-s in the "ipe node" style, which should
-- specify a rectangular node with zero inner sep and outer sep, anchored at
-- base west (ipe's default), with the natural width and height. Minipage text
-- objects are wrapped in a minipage environment with the width specified by the
-- width attribute (which is ignored otherwise). The horizontal and vertical
-- alignment attributes specify the anchor for the node.
--
-- Relevant attributes: stroke, textsize, opacity, textstyle, minipage, width,
-- horizontalalignment, verticalalignment
function export_text(model, obj, matrix)
local sheets = model.doc:sheets()
local text = obj:text()
local minipage = obj:get("minipage")
local anchor
local ha = obj:get("horizontalalignment")
local va = obj:get("verticalalignment")
if minipage then ha = "left" end
if ha == "left" then
if va == "bottom" then
anchor = "south west"
elseif va == "baseline" then
anchor = "base west"
elseif va == "vcenter" then
anchor = "west"
elseif va == "top" then
anchor = "north west"
end
elseif ha == "hcenter" then
if va == "bottom" then
anchor = "south"
elseif va == "baseline" then
anchor = "base"
elseif va == "vcenter" then
anchor = "center"
elseif va == "top" then
anchor = "north"
end
elseif ha == "right" then
if va == "bottom" then
anchor = "south east"
elseif va == "baseline" then
anchor = "base east"
elseif va == "vcenter" then
anchor = "east"
elseif va == "top" then
anchor = "north east"
end
end
local options = {}
table.insert(options, "ipe node")
-- Handle coordinate transformations
local pos = matrix*obj:position()
-- This attribute is handled differently for text objects
local trans = obj:get("transformations")
if trans == "translations" then
matrix = ipe.Matrix() -- already translated
elseif trans == "rigid" then
local x, y = table.unpack(matrix:coeff())
local angle = ipe.Vector(x, y):angle()
matrix = ipe.Translation(pos) * ipe.Rotation(angle)
end
matrix_to_options(matrix, options)
-- anchor
if anchor ~= "base west" then
table.insert(options, "anchor=" .. anchor)
end
-- text size
local textsizesym = obj:get("textsize")
local textsize
local setsize = ""
local normalstretchnum = sheets:find("textstretch", "normal")
if textsizesym ~= "normal" then
if is_number(textsizesym) then
if minipage then
-- Need to set font *inside* the minipage
setsize = string.format(
"\\fontsize{%s}{%sbp}\\selectfont",
textsizesym, textsizesym*1.2)
else
table.insert(options, "font size pt=" .. textsizesym
.. "/" .. (textsizesym*1.2))
end
-- In this case, use no text stretch
if normalstretchnum ~= 1 then
table.insert(options, "ipe node stretch=1")
normalstretchnum = 1 -- hack to skip \ipeminipagewidth
end
else
textsize = sheets:find("textsize", textsizesym)
if minipage then
-- Need to set font *inside* the minipage
setsize = textsize
else
table.insert(options, "font=" .. textsize)
end
end
end
-- textstretch
-- This uses the same key as textsize, if textsize is a symbol. But we won't
-- bother adding a scaling factor option unless the textstretch is different
-- from the normal textstretch (which might be ~= 1)
local textstretchnum = normalstretchnum
if not is_number(textsizesym) then
local res
res, textstretchnum
= _G.pcall(function()
return sheets:find("textstretch", textsizesym) end)
if not res then
textstretchnum = normalstretchnum
end
end
if textstretchnum ~= normalstretchnum then
table.insert(options, "ipe stretch " .. textsizesym)
end
-- textstyle
local textstyle
local val
if minipage then
textstyle = obj:get("textstyle")
val = sheets:find("textstyle", textstyle)
else
local res
-- 7.2.6 onwards: textstyle for labels is "labelstyle"
res, textstyle = _G.pcall(function() return obj:get("labelstyle") end)
if res then
val = sheets:find("labelstyle", textstyle)
else
textstyle = obj:get("textstyle")
val = sheets:find("textstyle", textstyle)
end
end
-- val is a \0-separated pair of strings
local beginstr, endstr
_, _, beginstr, endstr = string.find(val, "^(.*)\0(.*)$")
if beginstr ~= "" or endstr ~= "" then
if minipage then
text = beginstr .. "\n" .. text .. "\n" .. endstr
else
text = beginstr .. text .. endstr
end
end
-- color
if obj:get("stroke") ~= "black" then
color_option(obj:get("stroke"), "text", options)
end
-- opacity is always a symbolic name in ipe
local opacity = obj:get("opacity")
local prepend = nil
if params.stylesheets then prepend = "ipe opacity " end
opacity = string.gsub(opacity, "%%", "") -- strip %
if opacity ~= "opaque" then
string_option(opacity, nil, options, prepend)
end
write(indent .. "\\node")
if #options > 0 then
write("[" .. table.concat(options, ", ") .. "]")
end
write("\n" .. indent .. indent_amt .. " at " .. svec(pos, 3) .. " ")
if minipage then
-- Add minipage environment, and indent everything. The indentation looks
-- nice, but it'll mess up a verbatim environment or something. Oh well.
local old_indent = indent
indent = indent .. indent_amt .. indent_amt .. " "
write("{\n")
-- The \kern0pt is to cancel out \ignorespaces in the \begin{minipage},
-- which seems to be what happens when ipe runs LaTeX.
if textstretchnum ~= 1 then
write(indent .. string.format(
"\\ipestretchwidth{%sbp}\n", sround(obj:get("width"))))
write(indent .. "\\begin{minipage}{\\ipeminipagewidth}"
.. setsize .. "\\kern0pt\n")
else
write(indent .. "\\begin{minipage}{"
.. sround(obj:get("width")) .. "bp}"
.. setsize .. "\\kern0pt\n")
end
string.gsub(text .. "\n", "([^\n]*)\n",
function (c) write(indent .. indent_amt .. c .. "\n") end)
write(indent .. "\\end{minipage}\n")
indent = old_indent
write(indent .. indent_amt .. " }")
else
write("{" .. text .. "}")
end
write(";\n")
end
--------------------------------------------------------------------------------
-- Export paths
--------------------------------------------------------------------------------
function export_curve(subpath, matrix, options)
local ret = ""
local translate_origin = not is_almost_identity(matrix)
local first_point
-- Recognize rectangles
if #subpath == 3 and subpath.closed
and subpath[1].type == "segment"
and subpath[2].type == "segment"
and subpath[3].type == "segment"
then
local pt1 = subpath[1][1]
local pt2 = subpath[1][2]
local pt3 = subpath[2][2]
local pt4 = subpath[3][2]
if (round(pt1.x) == round(pt2.x)
and round(pt1.y) == round(pt4.y)
and round(pt3.y) == round(pt2.y)
and round(pt3.x) == round(pt4.x)) or
(round(pt1.x) == round(pt4.x)
and round(pt1.y) == round(pt2.y)
and round(pt3.y) == round(pt4.y)
and round(pt3.x) == round(pt2.x))
then -- It's a rectangle
first_point = matrix*pt1
if translate_origin then
matrix = ipe.Translation(-pt1)
ret = ret .. "\n" .. indent .. "(0, 0)"
else
ret = ret .. "\n" .. indent .. svec(matrix*pt1)
end
ret = ret .. " rectangle " .. svec(matrix*pt3)
subpath = {}
end
end
for i,segment in ipairs(subpath) do
if segment.type == "segment" then
if i == 1 then
first_point = matrix*segment[1]
if translate_origin then
matrix = ipe.Translation(-segment[1])
ret = ret .. "\n" .. indent .. "(0, 0)"
else
ret = ret .. "\n" .. indent .. svec(matrix*segment[1])
end
end
ret = ret .. "\n" .. indent .. " -- " .. svec(matrix*segment[2])
elseif segment.type == "spline" or
segment.type == "oldspline" then
-- TikZ doesn't know from uniform cubic B-splines, so convert to
-- regular splines first. (This is what happens internally in ipe
-- before converting to PDF.)
local beziers = ipe.splineToBeziers(
segment, false, segment.type == "oldspline")
for j,bezier in ipairs(beziers) do
if i == 1 and j == 1 then
first_point = matrix*bezier[1]
if translate_origin then
matrix = ipe.Translation(-bezier[1])
ret = ret .. "\n" .. indent .. "(0, 0)"
else
ret = ret .. "\n" .. indent .. svec(matrix*bezier[1])
end
end
ret = ret .. "\n" .. indent .. " .. controls "
.. svec(matrix*bezier[2])
.. " and " .. svec(matrix*bezier[3])
.. " .. " .. svec(matrix*bezier[4])
end
elseif segment.type == "arc" then
-- An arc in ipe is specified by an ellipse, which is defined by a
-- transformation matrix applied to the unit circle, and a start/end
-- angle, which are applied to the unit circle, before transformation.
-- This code extracts the rotation and scale components of the
-- transformation (if possible), and uses them to set the "x radius"
-- and "y radius" options of the arc.
local arc_options = {}
local alpha, beta = segment.arc:angles()
arc_options["start angle"] = alpha*180/math.pi
arc_options["end angle"] = beta*180/math.pi
local arc_matrix = segment.arc:matrix()
local beginp, endp = segment.arc:endpoints()
if i == 1 then
first_point = matrix*beginp
if translate_origin then
matrix = ipe.Translation(-beginp)
ret = ret .. "\n" .. indent .. "(0, 0)"
else
ret = ret .. "\n" .. indent .. svec(matrix*beginp)
end
end
matrix_to_ellipse_options(arc_matrix, arc_options)
subpath_options = concat_pairs(arc_options, ", ", {"rotate", "cm"})
ret = ret .. "\n" .. indent
if subpath_options ~= "" then
ret = ret .. " { [" .. subpath_options .. "]"
end
ret = ret .. " arc[" ..
concat_pairs(arc_options, ", ",
{"start angle", "end angle", "x radius",
"y radius", "radius"}) .. "]"
if subpath_options ~= "" then
ret = ret .. " }"
end
end
end
if subpath.closed then
-- Use "--" (path-to) in all cases: for arcs and splines, the end point is
-- on top of the first point anyway.
ret = ret .. "\n" .. indent .. " -- cycle"
end
if translate_origin then
table.insert(options, 1, "shift={" .. svec(first_point,3) .. "}")
end
return ret, matrix
end
function export_ellipse(subpath, matrix, options)
local ret = ""
local translate_origin = not is_almost_identity(matrix)
local ellipse_matrix = subpath[1]
local first_point = matrix*ellipse_matrix:translation()
if translate_origin then
matrix = ipe.Translation(-ellipse_matrix:translation())
ret = ret .. "\n" .. indent .. "(0, 0)"
table.insert(options, 1, "shift={" .. svec(first_point,3) .. "}")
else
ret = ret .. "\n" .. indent .. svec(first_point)
end
local ellipse_options = {}
matrix_to_ellipse_options(ellipse_matrix, ellipse_options)
local op_type
if ellipse_options.radius then
op_type = "circle"
else
op_type = "ellipse"
end
ret = ret .. " " .. op_type .. "["
.. concat_pairs(ellipse_options, ", ",
{"x radius", "y radius", "radius", "rotate", "cm"})
.. "]"
return ret, matrix
end
function export_closedspline(subpath, matrix, options)
local ret = ""
local translate_origin = not is_almost_identity(matrix)
-- TikZ doesn't know from uniform cubic B-splines, so convert to
-- regular splines first. (This is what happens internally in ipe
-- before converting to PDF.)
local beziers = ipe.splineToBeziers(subpath, true)
for i,bezier in ipairs(beziers) do
if i == 1 then
first_point = matrix*bezier[1]
if translate_origin then
matrix = ipe.Translation(-bezier[1])
ret = ret .. "\n" .. indent .. "(0, 0)"
else
ret = ret .. "\n" .. indent .. svec(matrix*bezier[1])
end
end
ret = ret .. "\n" .. indent .. " .. controls "
.. svec(matrix*bezier[2])
.. " and " .. svec(matrix*bezier[3])
.. " .. "
if i == #beziers then
ret = ret .. "cycle"
else
ret = ret .. svec(matrix*bezier[4])