uvcalc_lightmap.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698
  1. # ##### BEGIN GPL LICENSE BLOCK #####
  2. #
  3. # This program is free software; you can redistribute it and/or
  4. # modify it under the terms of the GNU General Public License
  5. # as published by the Free Software Foundation; either version 2
  6. # of the License, or (at your option) any later version.
  7. #
  8. # This program is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU General Public License for more details.
  12. #
  13. # You should have received a copy of the GNU General Public License
  14. # along with this program; if not, write to the Free Software Foundation,
  15. # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  16. #
  17. # ##### END GPL LICENSE BLOCK #####
  18. # <pep8 compliant>
  19. import bpy
  20. from bpy.types import Operator
  21. import mathutils
  22. class prettyface:
  23. __slots__ = (
  24. "uv",
  25. "width",
  26. "height",
  27. "children",
  28. "xoff",
  29. "yoff",
  30. "has_parent",
  31. "rot",
  32. )
  33. def __init__(self, data):
  34. self.has_parent = False
  35. self.rot = False # only used for triangles
  36. self.xoff = 0
  37. self.yoff = 0
  38. if type(data) == list: # list of data
  39. self.uv = None
  40. # join the data
  41. if len(data) == 2:
  42. # 2 vertical blocks
  43. data[1].xoff = data[0].width
  44. self.width = data[0].width * 2
  45. self.height = data[0].height
  46. elif len(data) == 4:
  47. # 4 blocks all the same size
  48. d = data[0].width # dimension x/y are the same
  49. data[1].xoff += d
  50. data[2].yoff += d
  51. data[3].xoff += d
  52. data[3].yoff += d
  53. self.width = self.height = d * 2
  54. # else:
  55. # print(len(data), data)
  56. # raise "Error"
  57. for pf in data:
  58. pf.has_parent = True
  59. self.children = data
  60. elif type(data) == tuple:
  61. # 2 blender faces
  62. # f, (len_min, len_mid, len_max)
  63. self.uv = data
  64. _f1, lens1, lens1ord = data[0]
  65. if data[1]:
  66. _f2, lens2, lens2ord = data[1]
  67. self.width = (lens1[lens1ord[0]] + lens2[lens2ord[0]]) / 2.0
  68. self.height = (lens1[lens1ord[1]] + lens2[lens2ord[1]]) / 2.0
  69. else: # 1 tri :/
  70. self.width = lens1[0]
  71. self.height = lens1[1]
  72. self.children = []
  73. else: # blender face
  74. uv_layer = data.id_data.uv_layers.active.data
  75. self.uv = [uv_layer[i].uv for i in data.loop_indices]
  76. # cos = [v.co for v in data]
  77. cos = [data.id_data.vertices[v].co for v in data.vertices] # XXX25
  78. if len(self.uv) == 4:
  79. self.width = ((cos[0] - cos[1]).length + (cos[2] - cos[3]).length) / 2.0
  80. self.height = ((cos[1] - cos[2]).length + (cos[0] - cos[3]).length) / 2.0
  81. else:
  82. # ngon, note:
  83. # for ngons to calculate the width/height we need to do the
  84. # whole projection, unlike other faces
  85. # we store normalized UV's in the faces coords to avoid
  86. # calculating the projection and rotating it twice.
  87. no = data.normal
  88. r = no.rotation_difference(mathutils.Vector((0.0, 0.0, 1.0)))
  89. cos_2d = [(r @ co).xy for co in cos]
  90. # print(cos_2d)
  91. angle = mathutils.geometry.box_fit_2d(cos_2d)
  92. mat = mathutils.Matrix.Rotation(angle, 2)
  93. cos_2d = [(mat @ co) for co in cos_2d]
  94. xs = [co.x for co in cos_2d]
  95. ys = [co.y for co in cos_2d]
  96. xmin = min(xs)
  97. ymin = min(ys)
  98. xmax = max(xs)
  99. ymax = max(ys)
  100. xspan = xmax - xmin
  101. yspan = ymax - ymin
  102. self.width = xspan
  103. self.height = yspan
  104. # ngons work different, we store projected result
  105. # in UV's to avoid having to re-project later.
  106. for i, co in enumerate(cos_2d):
  107. self.uv[i][:] = ((co.x - xmin) / xspan,
  108. (co.y - ymin) / yspan)
  109. self.children = []
  110. def spin(self):
  111. if self.uv and len(self.uv) == 4:
  112. self.uv = self.uv[1], self.uv[2], self.uv[3], self.uv[0]
  113. self.width, self.height = self.height, self.width
  114. self.xoff, self.yoff = self.yoff, self.xoff # not needed?
  115. self.rot = not self.rot # only for tri pairs and ngons.
  116. # print("spinning")
  117. for pf in self.children:
  118. pf.spin()
  119. def place(self, xoff, yoff, xfac, yfac, margin_w, margin_h):
  120. from math import pi
  121. xoff += self.xoff
  122. yoff += self.yoff
  123. for pf in self.children:
  124. pf.place(xoff, yoff, xfac, yfac, margin_w, margin_h)
  125. uv = self.uv
  126. if not uv:
  127. return
  128. x1 = xoff
  129. y1 = yoff
  130. x2 = xoff + self.width
  131. y2 = yoff + self.height
  132. # Scale the values
  133. x1 = x1 / xfac + margin_w
  134. x2 = x2 / xfac - margin_w
  135. y1 = y1 / yfac + margin_h
  136. y2 = y2 / yfac - margin_h
  137. # 2 Tri pairs
  138. if len(uv) == 2:
  139. # match the order of angle sizes of the 3d verts with the UV angles and rotate.
  140. def get_tri_angles(v1, v2, v3):
  141. a1 = (v2 - v1).angle(v3 - v1, pi)
  142. a2 = (v1 - v2).angle(v3 - v2, pi)
  143. a3 = pi - (a1 + a2) # a3= (v2 - v3).angle(v1 - v3)
  144. return [(a1, 0), (a2, 1), (a3, 2)]
  145. def set_uv(f, p1, p2, p3):
  146. # cos =
  147. #v1 = cos[0]-cos[1]
  148. #v2 = cos[1]-cos[2]
  149. #v3 = cos[2]-cos[0]
  150. # angles_co = get_tri_angles(*[v.co for v in f])
  151. angles_co = get_tri_angles(*[f.id_data.vertices[v].co for v in f.vertices]) # XXX25
  152. angles_co.sort()
  153. I = [i for a, i in angles_co]
  154. uv_layer = f.id_data.uv_layers.active.data
  155. fuv = [uv_layer[i].uv for i in f.loop_indices]
  156. if self.rot:
  157. fuv[I[2]][:] = p1
  158. fuv[I[1]][:] = p2
  159. fuv[I[0]][:] = p3
  160. else:
  161. fuv[I[2]][:] = p1
  162. fuv[I[0]][:] = p2
  163. fuv[I[1]][:] = p3
  164. f = uv[0][0]
  165. set_uv(f, (x1, y1), (x1, y2 - margin_h), (x2 - margin_w, y1))
  166. if uv[1]:
  167. f = uv[1][0]
  168. set_uv(f, (x2, y2), (x2, y1 + margin_h), (x1 + margin_w, y2))
  169. else: # 1 QUAD
  170. if len(uv) == 4:
  171. uv[1][:] = x1, y1
  172. uv[2][:] = x1, y2
  173. uv[3][:] = x2, y2
  174. uv[0][:] = x2, y1
  175. else:
  176. # NGon
  177. xspan = x2 - x1
  178. yspan = y2 - y1
  179. for uvco in uv:
  180. x, y = uvco
  181. uvco[:] = ((x1 + (x * xspan)),
  182. (y1 + (y * yspan)))
  183. def __hash__(self):
  184. # None unique hash
  185. return self.width, self.height
  186. def lightmap_uvpack(
  187. meshes,
  188. PREF_SEL_ONLY=True,
  189. PREF_NEW_UVLAYER=False,
  190. PREF_PACK_IN_ONE=False,
  191. PREF_APPLY_IMAGE=False,
  192. PREF_IMG_PX_SIZE=512,
  193. PREF_BOX_DIV=8,
  194. PREF_MARGIN_DIV=512,
  195. ):
  196. """
  197. BOX_DIV if the maximum division of the UV map that
  198. a box may be consolidated into.
  199. Basically, a lower value will be slower but waist less space
  200. and a higher value will have more clumpy boxes but more wasted space
  201. """
  202. import time
  203. from math import sqrt
  204. if not meshes:
  205. return
  206. t = time.time()
  207. if PREF_PACK_IN_ONE:
  208. if PREF_APPLY_IMAGE:
  209. image = bpy.data.images.new(name="lightmap", width=PREF_IMG_PX_SIZE, height=PREF_IMG_PX_SIZE, alpha=False)
  210. face_groups = [[]]
  211. else:
  212. face_groups = []
  213. for me in meshes:
  214. if PREF_SEL_ONLY:
  215. faces = [f for f in me.polygons if f.select]
  216. else:
  217. faces = me.polygons[:]
  218. if PREF_PACK_IN_ONE:
  219. face_groups[0].extend(faces)
  220. else:
  221. face_groups.append(faces)
  222. if PREF_NEW_UVLAYER:
  223. me.uv_layers.new()
  224. # Add face UV if it does not exist.
  225. # All new faces are selected.
  226. if not me.uv_layers:
  227. me.uv_layers.new()
  228. for face_sel in face_groups:
  229. print("\nStarting unwrap")
  230. if not face_sel:
  231. continue
  232. pretty_faces = [prettyface(f) for f in face_sel if f.loop_total >= 4]
  233. # Do we have any triangles?
  234. if len(pretty_faces) != len(face_sel):
  235. # Now add triangles, not so simple because we need to pair them up.
  236. def trylens(f):
  237. # f must be a tri
  238. # cos = [v.co for v in f]
  239. cos = [f.id_data.vertices[v].co for v in f.vertices] # XXX25
  240. lens = [(cos[0] - cos[1]).length, (cos[1] - cos[2]).length, (cos[2] - cos[0]).length]
  241. lens_min = lens.index(min(lens))
  242. lens_max = lens.index(max(lens))
  243. for i in range(3):
  244. if i != lens_min and i != lens_max:
  245. lens_mid = i
  246. break
  247. lens_order = lens_min, lens_mid, lens_max
  248. return f, lens, lens_order
  249. tri_lengths = [trylens(f) for f in face_sel if f.loop_total == 3]
  250. del trylens
  251. def trilensdiff(t1, t2):
  252. return (abs(t1[1][t1[2][0]] - t2[1][t2[2][0]]) +
  253. abs(t1[1][t1[2][1]] - t2[1][t2[2][1]]) +
  254. abs(t1[1][t1[2][2]] - t2[1][t2[2][2]]))
  255. while tri_lengths:
  256. tri1 = tri_lengths.pop()
  257. if not tri_lengths:
  258. pretty_faces.append(prettyface((tri1, None)))
  259. break
  260. best_tri_index = -1
  261. best_tri_diff = 100000000.0
  262. for i, tri2 in enumerate(tri_lengths):
  263. diff = trilensdiff(tri1, tri2)
  264. if diff < best_tri_diff:
  265. best_tri_index = i
  266. best_tri_diff = diff
  267. pretty_faces.append(prettyface((tri1, tri_lengths.pop(best_tri_index))))
  268. # Get the min, max and total areas
  269. max_area = 0.0
  270. min_area = 100000000.0
  271. tot_area = 0
  272. for f in face_sel:
  273. area = f.area
  274. if area > max_area:
  275. max_area = area
  276. if area < min_area:
  277. min_area = area
  278. tot_area += area
  279. max_len = sqrt(max_area)
  280. min_len = sqrt(min_area)
  281. side_len = sqrt(tot_area)
  282. # Build widths
  283. curr_len = max_len
  284. print("\tGenerating lengths...", end="")
  285. lengths = []
  286. while curr_len > min_len:
  287. lengths.append(curr_len)
  288. curr_len = curr_len / 2.0
  289. # Don't allow boxes smaller then the margin
  290. # since we contract on the margin, boxes that are smaller will create errors
  291. # print(curr_len, side_len/MARGIN_DIV)
  292. if curr_len / 4.0 < side_len / PREF_MARGIN_DIV:
  293. break
  294. if not lengths:
  295. lengths.append(curr_len)
  296. # convert into ints
  297. lengths_to_ints = {}
  298. l_int = 1
  299. for l in reversed(lengths):
  300. lengths_to_ints[l] = l_int
  301. l_int *= 2
  302. lengths_to_ints = list(lengths_to_ints.items())
  303. lengths_to_ints.sort()
  304. print("done")
  305. # apply quantized values.
  306. for pf in pretty_faces:
  307. w = pf.width
  308. h = pf.height
  309. bestw_diff = 1000000000.0
  310. besth_diff = 1000000000.0
  311. new_w = 0.0
  312. new_h = 0.0
  313. for l, i in lengths_to_ints:
  314. d = abs(l - w)
  315. if d < bestw_diff:
  316. bestw_diff = d
  317. new_w = i # assign the int version
  318. d = abs(l - h)
  319. if d < besth_diff:
  320. besth_diff = d
  321. new_h = i # ditto
  322. pf.width = new_w
  323. pf.height = new_h
  324. if new_w > new_h:
  325. pf.spin()
  326. print("...done")
  327. # Since the boxes are sized in powers of 2, we can neatly group them into bigger squares
  328. # this is done hierarchically, so that we may avoid running the pack function
  329. # on many thousands of boxes, (under 1k is best) because it would get slow.
  330. # Using an off and even dict us useful because they are packed differently
  331. # where w/h are the same, their packed in groups of 4
  332. # where they are different they are packed in pairs
  333. #
  334. # After this is done an external pack func is done that packs the whole group.
  335. print("\tConsolidating Boxes...", end="")
  336. even_dict = {} # w/h are the same, the key is an int (w)
  337. odd_dict = {} # w/h are different, the key is the (w,h)
  338. for pf in pretty_faces:
  339. w, h = pf.width, pf.height
  340. if w == h:
  341. even_dict.setdefault(w, []).append(pf)
  342. else:
  343. odd_dict.setdefault((w, h), []).append(pf)
  344. # Count the number of boxes consolidated, only used for stats.
  345. c = 0
  346. # This is tricky. the total area of all packed boxes, then sqrt() that to get an estimated size
  347. # this is used then converted into out INT space so we can compare it with
  348. # the ints assigned to the boxes size
  349. # and divided by BOX_DIV, basically if BOX_DIV is 8
  350. # ...then the maximum box consolidation (recursive grouping) will have a max width & height
  351. # ...1/8th of the UV size.
  352. # ...limiting this is needed or you end up with bug unused texture spaces
  353. # ...however if its too high, box-packing is way too slow for high poly meshes.
  354. float_to_int_factor = lengths_to_ints[0][0]
  355. if float_to_int_factor > 0:
  356. max_int_dimension = int(((side_len / float_to_int_factor)) / PREF_BOX_DIV)
  357. ok = True
  358. else:
  359. max_int_dimension = 0.0 # won't be used
  360. ok = False
  361. # RECURSIVE pretty face grouping
  362. while ok:
  363. ok = False
  364. # Tall boxes in groups of 2
  365. for d, boxes in list(odd_dict.items()):
  366. if d[1] < max_int_dimension:
  367. # boxes.sort(key=lambda a: len(a.children))
  368. while len(boxes) >= 2:
  369. # print("foo", len(boxes))
  370. ok = True
  371. c += 1
  372. pf_parent = prettyface([boxes.pop(), boxes.pop()])
  373. pretty_faces.append(pf_parent)
  374. w, h = pf_parent.width, pf_parent.height
  375. assert(w <= h)
  376. if w == h:
  377. even_dict.setdefault(w, []).append(pf_parent)
  378. else:
  379. odd_dict.setdefault((w, h), []).append(pf_parent)
  380. # Even boxes in groups of 4
  381. for d, boxes in list(even_dict.items()):
  382. if d < max_int_dimension:
  383. boxes.sort(key=lambda a: len(a.children))
  384. while len(boxes) >= 4:
  385. # print("bar", len(boxes))
  386. ok = True
  387. c += 1
  388. pf_parent = prettyface([boxes.pop(), boxes.pop(), boxes.pop(), boxes.pop()])
  389. pretty_faces.append(pf_parent)
  390. w = pf_parent.width # width and weight are the same
  391. even_dict.setdefault(w, []).append(pf_parent)
  392. del even_dict
  393. del odd_dict
  394. # orig = len(pretty_faces)
  395. pretty_faces = [pf for pf in pretty_faces if not pf.has_parent]
  396. # spin every second pretty-face
  397. # if there all vertical you get less efficiently used texture space
  398. i = len(pretty_faces)
  399. d = 0
  400. while i:
  401. i -= 1
  402. pf = pretty_faces[i]
  403. if pf.width != pf.height:
  404. d += 1
  405. if d % 2: # only pack every second
  406. pf.spin()
  407. # pass
  408. print("Consolidated", c, "boxes, done")
  409. # print("done", orig, len(pretty_faces))
  410. # boxes2Pack.append([islandIdx, w,h])
  411. print("\tPacking Boxes", len(pretty_faces), end="...")
  412. boxes2Pack = [[0.0, 0.0, pf.width, pf.height, i] for i, pf in enumerate(pretty_faces)]
  413. packWidth, packHeight = mathutils.geometry.box_pack_2d(boxes2Pack)
  414. # print(packWidth, packHeight)
  415. packWidth = float(packWidth)
  416. packHeight = float(packHeight)
  417. margin_w = ((packWidth) / PREF_MARGIN_DIV) / packWidth
  418. margin_h = ((packHeight) / PREF_MARGIN_DIV) / packHeight
  419. # print(margin_w, margin_h)
  420. print("done")
  421. # Apply the boxes back to the UV coords.
  422. print("\twriting back UVs", end="")
  423. for i, box in enumerate(boxes2Pack):
  424. pretty_faces[i].place(box[0], box[1], packWidth, packHeight, margin_w, margin_h)
  425. # pf.place(box[1][1], box[1][2], packWidth, packHeight, margin_w, margin_h)
  426. print("done")
  427. if PREF_APPLY_IMAGE:
  428. pass
  429. # removed with texface
  430. '''
  431. if not PREF_PACK_IN_ONE:
  432. image = bpy.data.images.new(name="lightmap",
  433. width=PREF_IMG_PX_SIZE,
  434. height=PREF_IMG_PX_SIZE,
  435. )
  436. for f in face_sel:
  437. f.image = image
  438. '''
  439. for me in meshes:
  440. me.update()
  441. print("finished all %.2f " % (time.time() - t))
  442. def unwrap(operator, context, **kwargs):
  443. # switch to object mode
  444. is_editmode = context.object and context.object.mode == 'EDIT'
  445. if is_editmode:
  446. bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
  447. # define list of meshes
  448. meshes = list({me for obj in context.selected_objects if obj.type == 'MESH' for me in (obj.data,) if me.polygons and me.library is None})
  449. if not meshes:
  450. operator.report({'ERROR'}, "No mesh object")
  451. return {'CANCELLED'}
  452. lightmap_uvpack(meshes, **kwargs)
  453. # switch back to edit mode
  454. if is_editmode:
  455. bpy.ops.object.mode_set(mode='EDIT', toggle=False)
  456. return {'FINISHED'}
  457. from bpy.props import BoolProperty, FloatProperty, IntProperty
  458. class LightMapPack(Operator):
  459. """Pack each faces UV's into the UV bounds"""
  460. bl_idname = "uv.lightmap_pack"
  461. bl_label = "Lightmap Pack"
  462. # Disable REGISTER flag for now because this operator might create new
  463. # images. This leads to non-proper operator redo because current undo
  464. # stack is local for edit mode and can not remove images created by this
  465. # operator.
  466. # Proper solution would be to make undo stack aware of such things,
  467. # but for now just disable redo. Keep undo here so unwanted changes to uv
  468. # coords might be undone.
  469. # This fixes infinite image creation reported there [#30968] (sergey)
  470. bl_options = {'UNDO'}
  471. PREF_CONTEXT: bpy.props.EnumProperty(
  472. name="Selection",
  473. items=(
  474. ('SEL_FACES', "Selected Faces", "Space all UVs evenly"),
  475. ('ALL_FACES', "All Faces", "Average space UVs edge length of each loop"),
  476. ),
  477. )
  478. # Image & UVs...
  479. PREF_PACK_IN_ONE: BoolProperty(
  480. name="Share Tex Space",
  481. description=(
  482. "Objects Share texture space, map all objects "
  483. "into 1 uvmap"
  484. ),
  485. default=True,
  486. )
  487. PREF_NEW_UVLAYER: BoolProperty(
  488. name="New UV Map",
  489. description="Create a new UV map for every mesh packed",
  490. default=False,
  491. )
  492. PREF_APPLY_IMAGE: BoolProperty(
  493. name="New Image",
  494. description=(
  495. "Assign new images for every mesh (only one if "
  496. "shared tex space enabled)"
  497. ),
  498. default=False,
  499. )
  500. PREF_IMG_PX_SIZE: IntProperty(
  501. name="Image Size",
  502. description="Width and Height for the new image",
  503. min=64, max=5000,
  504. default=512,
  505. )
  506. # UV Packing...
  507. PREF_BOX_DIV: IntProperty(
  508. name="Pack Quality",
  509. description="Pre Packing before the complex boxpack",
  510. min=1, max=48,
  511. default=12,
  512. )
  513. PREF_MARGIN_DIV: FloatProperty(
  514. name="Margin",
  515. description="Size of the margin as a division of the UV",
  516. min=0.001, max=1.0,
  517. default=0.1,
  518. )
  519. def draw(self, context):
  520. layout = self.layout
  521. layout.use_property_split = True
  522. is_editmode = context.active_object.mode == 'EDIT'
  523. if is_editmode:
  524. layout.prop(self, "PREF_CONTEXT")
  525. layout.prop(self, "PREF_PACK_IN_ONE")
  526. layout.prop(self, "PREF_NEW_UVLAYER")
  527. layout.prop(self, "PREF_APPLY_IMAGE")
  528. layout.prop(self, "PREF_IMG_PX_SIZE")
  529. layout.prop(self, "PREF_BOX_DIV")
  530. layout.prop(self, "PREF_MARGIN_DIV")
  531. @classmethod
  532. def poll(cls, context):
  533. ob = context.active_object
  534. return ob and ob.type == 'MESH'
  535. def execute(self, context):
  536. kwargs = self.as_keywords()
  537. PREF_CONTEXT = kwargs.pop("PREF_CONTEXT")
  538. is_editmode = context.active_object.mode == 'EDIT'
  539. if not is_editmode:
  540. kwargs["PREF_SEL_ONLY"] = False
  541. elif PREF_CONTEXT == 'SEL_FACES':
  542. kwargs["PREF_SEL_ONLY"] = True
  543. elif PREF_CONTEXT == 'ALL_FACES':
  544. kwargs["PREF_SEL_ONLY"] = False
  545. else:
  546. raise Exception("invalid context")
  547. kwargs["PREF_MARGIN_DIV"] = int(1.0 / (kwargs["PREF_MARGIN_DIV"] / 100.0))
  548. return unwrap(self, context, **kwargs)
  549. def invoke(self, context, _event):
  550. wm = context.window_manager
  551. return wm.invoke_props_dialog(self)
  552. classes = (
  553. LightMapPack,
  554. )