-
Notifications
You must be signed in to change notification settings - Fork 0
/
addon_loader.lua
722 lines (554 loc) · 20.9 KB
/
addon_loader.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
-- Lua (Keep this comment, this is an indication for editor's 'run' command)
--------------------------------
-- Global Paths ---
--------------------------------
-- root
local project_root = gom.get_environment_value("PROJECT_ROOT")
--------------------------------
-- Utils ---
--------------------------------
function to_table(it)
local t = {}
for x in it do
table.insert(t, x)
end
return t
end
local is_windows = package.config:sub(1,1) == '\\'
print("Operating system is Windows: " .. tostring(is_windows))
-- Get unix executable recursively in a directory using shell command line
function get_unix_executables(path)
local str, err, code = os.execute('for i in $(find ' .. path .. '); do if [ -x "$i" ] && [ -f "$i" ]; then echo "$i"; fi done > exe_list.out')
if str then
-- Get stdout result of cmd in redirected file
local f = io.open("exe_list.out", "r")
local data = f:read("*all")
f:close()
-- Cleanup file
FileSystem.delete_file("exe_list.out")
local files = to_table(string.split(data, '\n'))
local filtered_files = {}
for i, file in pairs(files) do
local file_ext = FileSystem.extension(file)
if file_ext == "exe" or file_ext == "" then
table.insert(filtered_files, file)
end
end
return filtered_files
elseif code == 1 then
return false
else
error("Error checking file permissions: " .. err)
end
end
-- Recursively search in directory files that match with the pattern
function search(dir, pattern)
local files = {}
for i, path in pairs(FileSystem.get_files(dir)) do
local file = FileSystem.base_name(path,false)
if string.match(file, pattern) ~= nil then
table.insert(files, path)
end
end
for i, path in pairs(FileSystem.get_subdirectories(dir)) do
local rec_files = search(path, pattern)
for _, f in pairs(rec_files) do
table.insert(files, f)
end
end
return files
end
-- Remove some characters that graphite doesn't support
function string.clean(str)
return str:gsub("%-", "_"):gsub("% ", "_"):gsub("%/", "_"):gsub("%.", "_")
end
-- Check whether a string is empty or not
function string.empty(str)
return str == nil or str == ""
end
-- Join strings with a given character
function string.join(lines, c)
if not lines then
return nil
end
local s = ""
for line in lines do
s = s .. line .. c
end
return string.sub(s, 0, string.len(s) - string.len(c))
end
-- Count number of element in a table
function table_count(t)
local count = 0
for _ in pairs(t) do
count = count + 1
end
return count
end
-- Concat two tables
function concat_table(t1, t2)
local t = {}
for _, v in pairs(t1) do
table.insert(t, v)
end
for _, v in pairs(t2) do
table.insert(t, v)
end
return t
end
--------------------------------
-- Graphite utils ---
--------------------------------
-- Get attribute data of a mesh
function get_attributes_data(object)
local str_attrs = string.split(object.attributes, ';')
S = scene_graph.find_or_create_object('OGF::MeshGrob', object.name)
E = S.I.Editor
local attr = {}
for str_attr in str_attrs do
local primitive, name = table.unpack(to_table(string.split(str_attr, '.')))
local attr_data = E.find_attribute(str_attr)
attr[str_attr] = {name = name, primitive = primitive, dim = attr_data.dimension, type = attr_data.element_meta_type.name}
end
return attr
end
-- Check whether the parameter type belong to an attribute type (vertices, facets, edges...)
function is_param_is_type_attribute(param_type)
return (string.starts_with(param_type, 'vertices')
or string.starts_with(param_type, 'facets')
or string.starts_with(param_type, 'edges')
or string.starts_with(param_type, 'cells')
or string.starts_with(param_type, 'facet_corners'))
end
-- Extract attribute name long name (e.g: vertices.my_attr -> my_attr)
function get_attribute_shortname(attr_name)
return to_table(string.split(attr_name, '.'))[2]
end
--------------------------------
-- Global ---
--------------------------------
--------------------------------
-- Serialization/Format ---
--------------------------------
-- Get structured parameters object by parsing lines
function parameters_from_lines(lines)
local parameters = {}
for line in lines do
-- skip comments
if string.sub(line, 0, 1) ~= '#' then
-- get chunks
chunks = string.split(line, ';')
local t = {}
for chunk in chunks do
local kv = to_table(string.split(chunk, '='))
t[kv[1]] = kv[2]
end
table.insert(parameters, t)
end
end
return parameters
end
-- Check arg value is well formed and well typed
function check_arg(param, val)
local success = true
-- If parameter is of attribute type
if is_param_is_type_attribute(param.type) then
local actual_attrs_data = get_attributes_data(scene_graph.current())
-- Check attribute existence
if actual_attrs_data[val] == nil then
print("Attribute " .. val .. " doesn't exists.")
success = false
else
local actual_attr_data = actual_attrs_data[val]
local actual_param_type = actual_attr_data['primitive'] .. "." .. actual_attr_data['type'] .. "." .. actual_attr_data['dim']
-- Bruno have renammed vertices.bool type to vertices.OGF::Numeric::uint8 for example between two versions of graphite...
-- so I have to check the new name and old name to be sure it works
local actual_param_type_renamed = actual_attr_data['primitive'] .. "." .. t_attr_reverse_map[actual_attr_data['type']] .. "." .. actual_attr_data['dim']
-- Check attribute type consistency between expected and actual
if not (param.type == actual_param_type or param.type == actual_param_type_renamed) then
print(
"Parameter '".. param.name ..
"' expect an attribute of type '" .. param.type ..
"', but attribute '" .. val ..
"' of type '" .. actual_param_type .. "' was given."
)
success = false
end
end
end
return success
end
-- Check arg values are well formed and well typed
function check_args(params, args)
for _, param in pairs(params) do
local clean_param_name = string.clean(param.name)
if not check_arg(param, args[clean_param_name]) then
return false
end
end
return true
end
function map_param(input_path, output_model_path, param, val)
local str_val = ""
if val == nil then
str_val = ""
end
-- Set automatically special parameters
if param.type == 'input' then
str_val = input_path
elseif param.name == 'result_path' then
str_val = output_model_path
elseif param.name == 'run_from' then
str_val = "graphite"
-- Attribute parameters
elseif is_param_is_type_attribute(param.type) then
str_val = get_attribute_shortname(val)
else
-- Set value or default value
if val ~= nil then
str_val = tostring(val)
else
str_val = param.value
end
end
return param.name .. "=" .. str_val
end
-- format parameters into a string key1=value1 key2=value2 ...
function format_args(input_path, output_model_path, params, args)
local str = ""
for _, param in pairs(params) do
local clean_param_name = string.clean(param.name)
str = str.." "..map_param(input_path, output_model_path, param, args[clean_param_name])
end
return str
end
function load_outputs(sandbox_dir)
-- TODO load lua file eventually
-- TODO load a file to do some action (add or replace to scene graph for example)
-- Load model outputs
local obj_models = search(sandbox_dir, ".*%.obj")
local geogram_models = search(sandbox_dir, ".*%.geogram")
local mesh_models = search(sandbox_dir, ".*%.mesh")
local models = concat_table(obj_models, geogram_models)
local models = concat_table(models, mesh_models)
local prev_current_object = scene_graph.current_object
for _, model in pairs(models) do
print('Load: '..model)
-- scene_graph.delete_current_object()
scene_graph.load_object(model)
end
scene_graph.current_object = prev_current_object
end
-- Remove sandbox dir if empty
function cleanup_sandbox(sandbox_dir)
local entries = FileSystem.get_directory_entries(sandbox_dir)
if table_count(entries) == 0 then
FileSystem.delete_directory(sandbox_dir)
print("Sandbox '" .. sandbox_dir .. "' is empty, cleanup...")
end
end
-- Execute program
function exec_addon(addon)
-- Curryfied
local exec = function(args)
print('args='..tostring(args))
-- Check whether a model selected
if addon.is_mesh_expected and scene_graph.current() == nil then
print('No object selected.')
return
end
local object = scene_graph.current()
-- Get plugin to execute
local plug_name = args['method']
print("Add-on: "..addon.name)
-- Check arguments
if not check_args(addon.parameters, args) then
print("Abort add-on call.")
return
end
-- Create a sandbox
-- Get document root
-- TODO replace by tmp dir
local project_root = FileSystem.documents_directory()
local sandbox_dir = project_root .. "/" .. "sandbox_" .. os.clock()
FileSystem.create_directory(sandbox_dir)
print("Sandbox dir created: "..sandbox_dir)
-- Save & Copy current model (in order to keep last changes that occurred to the model !)
-- TODO UUID here !
local input_model_path = ""
if addon.is_mesh_expected then
local file_extension = FileSystem.extension(object.filename)
print(file_extension)
input_model_path = sandbox_dir .. '/' .. object.name .. "_" .. os.clock() .. "." .. file_extension
if not object.save(input_model_path) then
print('An error occurred when transfering the current model to add-on.')
return
end
end
local output_model_path = sandbox_dir .. "/output"
-- exec bin in sandbox
-- local wd = FileSystem.get_current_working_directory()
-- FileSystem.set_current_working_directory(sandbox_dir)
-- Create output directory
FileSystem.create_directory(output_model_path)
local str_args = format_args(input_model_path, output_model_path, addon.parameters, args)
local cmd = addon.path .. " " .. str_args
print('call: ' .. cmd)
-- Run command
os.execute(cmd)
-- Reset working dir
-- FileSystem.set_current_working_directory(wd)
-- Load models found into sandbox
print('Load outputs...')
load_outputs(output_model_path)
-- Clean up if empty
cleanup_sandbox(sandbox_dir)
end
return exec
end
-- map table of types to gom types
t_map = {
double = gom.meta_types.double,
float = gom.meta_types.float,
int = gom.meta_types.int,
bool = gom.meta_types.bool,
string = gom.meta_types.std.string,
file = gom.meta_types.OGF.FileName,
input = gom.meta_types.OGF.FileName,
}
t_attr_map = {
double = 'OGF::Numeric::float64',
float = 'OGF::Numeric::float64',
int = 'OGF::Numeric::int32',
uint = 'OGF::Numeric::uint32',
bool = 'OGF::Numeric::uint8',
}
t_attr_reverse_map = {}
t_attr_reverse_map['OGF::Numeric::float64'] = 'double'
t_attr_reverse_map['OGF::Numeric::int32'] = 'int'
t_attr_reverse_map['OGF::Numeric::uint32'] = 'uint'
t_attr_reverse_map['OGF::Numeric::uint8'] = 'bool'
-- Note: for facet corners, type are returned by graphite in regular form 'int', 'double' instead of 'OGF::Numeric::int32', 'OGF::Numeric::float64'
-- I don't know why this is different between facet_corners attributes and other attributes, should ask to Bruno L.
-- That's why I added this mapping below
t_attr_reverse_map['double'] = 'double'
t_attr_reverse_map['int'] = 'int'
t_attr_reverse_map['uint'] = 'uint'
t_attr_reverse_map['bool'] = 'bool'
function draw_addon_menu(addon)
local menu_path = string.sub(FileSystem.dir_name(addon.path), #add_ons_directory + 1)
-- Choose the menu to add the add-on
-- If add-on expect a mesh as input it goes to MeshGrob menu, else to SceneGraph menu
-- Contrary to SceneGraph menu, MeshGrob menu is only visible when a mesh is loaded
local mclass = nil
if addon.is_mesh_expected then
mclass = mclass_mesh_grob_command
else
mclass = mclass_scene_graph_command
end
local parameters = addon.parameters
-- And another command, also created in the 'Foobars' submenu
m = mclass.add_slot(addon.name, exec_addon(addon))
-- Add add-on help as tooltip text
if not string.empty(addon.help) then
m.create_custom_attribute('help', addon.help)
end
for _, param in pairs(parameters) do
local clean_param_name = string.clean(param.name)
-- Map string param type to gom type
local param_type = t_map[param.type]
if param_type == nil then
param_type = gom.meta_types.std.string
end
-- Doesn't display special parameters that will be automatically filled by graphite !
-- - parameters with 'input' type
-- - parameter of type 'system'
if param.type ~= 'input' and param.type_of_param ~= 'system' then
if param.value ~= "undefined" then
m.add_arg(clean_param_name, param_type, param.value)
else
m.add_arg(clean_param_name, param_type)
end
-- Add description as tooltip text
m.create_arg_custom_attribute(clean_param_name, 'help', param.description)
-- # Attribute management !
-- If parameter type is an attribute type, add attribute combobox to UI
if is_param_is_type_attribute(param.type) then
m.create_arg_custom_attribute(clean_param_name, 'handler','combo_box')
-- Filter by attribute type / primitive
primitive, type, dim = table.unpack(to_table(string.split(param.type, '.')))
m.create_arg_custom_attribute(clean_param_name, 'values', '$grob.list_attributes("' .. primitive .. '","' .. t_attr_map[type] .. '","' .. tostring(dim) .. '")')
end
-- # Enum management !
-- Possible values is set ! So should display a combo box with all choices
if (not (string.empty(param.possible_values) or param.possible_values == 'undefined')) then
local values = param.possible_values:gsub(",", ";")
m.create_arg_custom_attribute(clean_param_name, 'handler','combo_box')
m.create_arg_custom_attribute(clean_param_name, 'values', values)
end
end
end
m.create_custom_attribute('menu','/Add-ons' .. menu_path)
return m
end
function draw_addons_menus(addons)
for _, addon in ipairs(addons) do
draw_addon_menu(addon)
end
end
-- We are going to create a subclass of OGF::MeshGrobCommands,
-- let us first get the metaclass associated with OGF::MeshGrobCommands
mclass_mesh_grob_superclass = gom.meta_types.OGF.MeshGrobCommands
-- Create our subclass, that we name OGF::MeshGrobCustomCommands
-- By default, our commands will land in a new menu 'Custom'
-- (name your class OGF::MeshGrobZorglubCommands if you want a 'Zorglub'
-- menu, or use custom attributes, see below).
mclass_mesh_grob_command = mclass_mesh_grob_superclass.create_subclass('OGF::LuaGrobCustomCommands')
-- Create a constructor for our new class.
-- For Commands classes, we just create the default constructor
-- (one can also create constructors with arguments, but we do not need that here)
mclass_mesh_grob_command.add_constructor()
mclass_scene_graph_command_superclass = gom.meta_types.OGF.SceneGraphCommands
mclass_scene_graph_command = mclass_scene_graph_command_superclass.create_subclass('OGF::SceneGraphExternalCommands')
mclass_scene_graph_command.add_constructor()
--------------------------------
-- Draw menus ---
--------------------------------
-- Make our new Commands visible from MeshGrob
scene_graph.register_grob_commands(gom.meta_types.OGF.MeshGrob, mclass_mesh_grob_command)
local addon_loader_file = project_root .. "/addon_loader.txt"
function load_addon_directory()
if not FileSystem.is_file(addon_loader_file) then
return project_root
end
local f = io.open(addon_loader_file, "r")
local data = f:read("*all")
f:close()
return data
end
function save_addon_directory(directory)
add_ons_directory = directory
local f = io.open(addon_loader_file, "w")
f:write(directory)
f:close()
end
add_ons_directory = load_addon_directory()
print("addons directory: " .. add_ons_directory)
function search_addons(directory)
local exe_files = search(directory, ".*%.exe")
local addon_files = search(directory, ".*%.addon")
local addons = concat_table(exe_files, addon_files)
-- Sort by alphabetical order
table.sort(addons, function(a, b) return FileSystem.base_name(a, true) < FileSystem.base_name(b, true) end)
return addons
end
function search_params_files(directory)
return search(directory, ".*%.params")
end
function search_help_files(directory)
return search(directory, ".*%.help")
end
-- Detect if the data is formatted as param file data
function is_param_file_data(data)
local lines = io.lines(param_file)
for line in lines do
if string.starts_with(line, "name=") then
return true
end
end
return false
end
function scan_directory(directory)
-- Search for addons programs
local addons = search_addons(directory)
for _, addon in pairs(addons) do
local param_file = addon .. ".params"
local help_file = addon .. ".help"
-- Call program to get its parameters
-- Call program to get its help string
os.execute(addon .. " --show-params > " .. param_file)
os.execute(addon .. " -h > " .. help_file)
-- Check params file
local f = io.open(param_file, "r")
local data = f:read("*all")
f:close()
-- No content ? Not an addon !
if string.empty(data) then
FileSystem.delete_file(param_file)
FileSystem.delete_file(help_file)
end
end
end
function sync()
if not FileSystem.is_directory(add_ons_directory) then
print(add_ons_directory .. " is not a directory. Add-on loader expect to load add-ons from a directory.")
return
end
local param_files = search_params_files(add_ons_directory)
local help_files = search_help_files(add_ons_directory)
for _, param_file in pairs(param_files) do
FileSystem.delete_file(param_file)
end
for _, help_file in pairs(help_files) do
FileSystem.delete_file(help_file)
end
scan_directory(add_ons_directory)
local addons = load_addons(add_ons_directory)
draw_addons_menus(addons)
end
function load_addons(directory)
local param_files = search_params_files(directory)
local addons = {}
for _, param_file in pairs(param_files) do
-- read
local lines = io.lines(param_file)
local parameters = parameters_from_lines(lines)
-- Extract addon name
local addon_name = FileSystem.base_name(param_file, true)
local clean_addon_name = string.clean(addon_name)
-- Search for an input parameter
local is_mesh_expected = false
for k, p in pairs(parameters) do
if p.type == 'input' then
is_mesh_expected = true
end
end
-- Create a new addon object
local addon = {
name = clean_addon_name,
path = param_file:gsub(".params", ""),
parameters = parameters,
help = "",
is_mesh_expected = is_mesh_expected
}
-- local lines = io.lines(help_file)
-- local help = string.join(lines, '\n')
-- Keep plugin object in a associative map
table.insert(addons, addon)
-- addons[addon.name] = addon
end
-- Sort alphabetically
table.sort(addons, function(a, b) return a.name < b.name end)
return addons
end
-- Make our new commands visible from MeshGrob
scene_graph.register_grob_commands(gom.meta_types.OGF.SceneGraph, mclass_scene_graph_command)
-- Add menus to manage external plugins
-- Add plugin menu
local m_add_plugin = mclass_scene_graph_command.add_slot("Choose_add_ons_directory", function(args)
save_addon_directory(args.add_ons_directory)
sync()
main.stop()
end)
-- Add menu to add addons directory
m_add_plugin.add_arg("add_ons_directory", gom.meta_types.OGF.FileName, add_ons_directory)
m_add_plugin.create_custom_attribute('menu','/Add-ons/Manage add ons')
-- Add menu to sync addons
m_clean_plugin = mclass_scene_graph_command.add_slot("Syncronize_and_Quit", function()
sync()
main.stop()
end)
m_clean_plugin.create_custom_attribute('menu','/Add-ons/Manage add ons')
-- Load addons
local addons = load_addons(add_ons_directory)
draw_addons_menus(addons)