geometry_objects.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661
  1. '''
  2. Geometry objects
  3. ** By Cro-Ki l@b, 2017 **
  4. '''
  5. from math import sqrt
  6. import math
  7. class BoundingRect(tuple):
  8. """ Bounding rectangle defined by a top-left (xmin, ymin) point
  9. and a bottom-right (xmax, ymax) point """
  10. def __new__(self, xmin, ymin, xmax, ymax):
  11. return tuple.__new__(self, (xmin, ymin, xmax, ymax))
  12. @classmethod
  13. def from_(cls, *args):
  14. BaseGeometry.assertCoordinates(*args)
  15. xs, ys = zip(*list(args))
  16. xs, ys = sorted(list(xs)), sorted(list(ys))
  17. return cls(xs[0], ys[0], xs[-1], ys[-1])
  18. @property
  19. def xmin(self):
  20. return self[0]
  21. @property
  22. def ymin(self):
  23. return self[1]
  24. @property
  25. def xmax(self):
  26. return self[2]
  27. @property
  28. def ymax(self):
  29. return self[3]
  30. def __contains__(self, key):
  31. BaseGeometry.assertCoordinates(key)
  32. return self.xmin <= key[0] <= self.xmax and self.ymin <= key[1] <= self.ymax
  33. @property
  34. def topleft(self):
  35. return (self.xmin, self.ymin)
  36. @property
  37. def bottomright(self):
  38. return (self.xmax, self.ymax)
  39. @property
  40. def width(self):
  41. return self.xmax - self.xmin + 1
  42. @property
  43. def height(self):
  44. return self.ymax - self.ymin + 1
  45. class IBoundingRect(BoundingRect):
  46. """ Infinite bounding rectangle
  47. >> '(x, y) in IBoundingRect()' is always True"""
  48. def __new__(self):
  49. return BoundingRect.__new__(self, -math.inf, -math.inf, math.inf, math.inf)
  50. class BaseGeometry:
  51. """ Base class for geometry classes
  52. ! Should be overriden """
  53. ANGLES = (1, 2, 3)
  54. def __repr__(self):
  55. return "<{} object>".format(self.__class__.__name__)
  56. @classmethod
  57. def instance(cls):
  58. return cls()
  59. @staticmethod
  60. def assertCoordinates(*args):
  61. """ raise a ValueError if the args are not (x, y) iterables, where x and y are integers
  62. usage:
  63. self.assertCoordinates((x1, y1), (x2, y2), ...)
  64. """
  65. try:
  66. if all([isinstance(i, int) for x, y in args for i in (x, y)]):
  67. return
  68. except (TypeError, ValueError):
  69. pass
  70. raise ValueError("'{}' is not a valid (x, y) coordinates iterable".format(args))
  71. @staticmethod
  72. def _assertPositiveInt(value, strict=False):
  73. """ raise a ValueError if the 'value' is not a dimension,
  74. i.e. a (strictly) positive integer """
  75. if not isinstance(value, int) or not ((value > 0) or (not strict and value >= 0)):
  76. raise ValueError("Expected: strictly positive integer(given: '{}')".format(value))
  77. @staticmethod
  78. def _assertValidAngle(value):
  79. """ raise a ValueError if the 'value' is not a valid angle """
  80. if not value in BaseGeometry.ANGLES:
  81. raise ValueError("angle has to be a value from BaseGeometry.ANGLES (given: {})".format(value))
  82. @staticmethod
  83. def _bounding_rect(*args):
  84. """ return the bounding rectangle of the from (x, y) coordinates """
  85. return BoundingRect.from_(*args)
  86. @staticmethod
  87. def graphicsitem(x, y, scale=120):
  88. """ returns the list of the points which compose the (x, y) cell """
  89. raise NotImplementedError("this method is abstract and should be reimplemented in subclasses")
  90. # geometrical algorithms
  91. @classmethod
  92. def neighbors(cls, x, y, br=IBoundingRect()):
  93. """ returns a list of the neighbors of (x, y) """
  94. raise NotImplementedError("this method is abstract and should be reimplemented in subclasses")
  95. @classmethod
  96. def line(cls, x1, y1, x2, y2, br=IBoundingRect()):
  97. raise NotImplementedError("this method is abstract and should be reimplemented in subclasses")
  98. @classmethod
  99. def line3d(cls, x1, y1, z1, x2, y2, z2, br=IBoundingRect()):
  100. """ returns a line from (x1 ,y1, z1) to (x2, y2, z2)
  101. as a list of (x, y, z) coordinates """
  102. cls.assertCoordinates((z1, z2))
  103. hoLine = cls.line(x1, y1, x2, y2)
  104. if z1 == z2:
  105. return [(x, y, z1) for x, y in hoLine]
  106. else:
  107. ligneZ = SquareGeometry.line(0, z1, (len(hoLine) - 1), z2)
  108. return [(hoLine[d][0], hoLine[d][1], z) for d, z in ligneZ]
  109. @classmethod
  110. def zone(cls, x, y, radius, br=IBoundingRect()):
  111. """ returns the list of the coordinates of the cells in a zone around (x, y)
  112. """
  113. cls.assertCoordinates((x, y))
  114. cls._assertPositiveInt(radius)
  115. buffer = frozenset([(x, y)])
  116. for _ in range(0, radius):
  117. current = buffer
  118. for x, y in current:
  119. buffer |= frozenset(cls.neighbors(x, y))
  120. return list(buffer)
  121. @classmethod
  122. def triangle(cls, xa, ya, xh, yh, iAngle, br=IBoundingRect()):
  123. """ return the list of the (x, y) coordinates in a triangle
  124. with (xa, ya) apex and (xh, yh) middle of the base """
  125. raise NotImplementedError("this method is abstract and should be reimplemented in subclasses")
  126. @classmethod
  127. def triangle3d(self, xa, ya, za, xh, yh, zh, iAngle, br=IBoundingRect()):
  128. """Returns a list of (x, y, z) coordinates in a 3d-cone
  129. A is the top of the cone, H if the center of the base
  130. WARNING: result is a dictionary of the form {(x, y): (-z, +z)}
  131. This is for performance reason and because on a 2d grid, you generally don't need a complete list of z coordinates
  132. as you don't want to display them: you just want to know if an altitude is inside a range.
  133. That could change in later version
  134. """
  135. raise NotImplementedError("this method is abstract and should be reimplemented in subclasses")
  136. @classmethod
  137. def rectangle(cls, x1, y1, x2, y2, br=IBoundingRect()):
  138. """return a list of cells in a rectangle between (X1, Y1), (X2, Y2)"""
  139. xmin, ymin, xmax, ymax = cls._bounding_rect((x1, y1), (x2, y2))
  140. return [(x, y) for x in range(xmin, xmax + 1) for y in range(ymin, ymax + 1)]
  141. @classmethod
  142. def hollow_rectangle(cls, x1, y1, x2, y2, br=IBoundingRect()):
  143. """return a list of cells composing the sides of the rectangle between (X1, Y1), (X2, Y2)"""
  144. xmin, ymin, xmax, ymax = cls._bounding_rect((x1, y1), (x2, y2))
  145. if (xmin, ymin) == (xmax, ymax):
  146. return [(xmin, ymin)]
  147. return [(x, ymin) for x in range(xmin, xmax)] + \
  148. [(xmax, y) for y in range(ymin, ymax)] + \
  149. [(x, ymax) for x in range(xmax, xmin, -1)] + \
  150. [(xmin, y) for y in range(ymax, ymin, -1)]
  151. @classmethod
  152. def rotate(cls, center, coordinates, rotations, br=IBoundingRect()):
  153. """ return the 'coordinates' list of (x, y) coordinates
  154. after a rotation of 'rotations' times around the (x, y) center """
  155. raise NotImplementedError("this method is abstract and should be reimplemented in subclasses")
  156. class SquareGeometry(BaseGeometry):
  157. """ Geometry on square grids """
  158. _nodiags = False
  159. @staticmethod
  160. def graphicsitem(x, y, scale=120):
  161. """ reimplemented from BaseGeometry.graphicsitem """
  162. return [
  163. (x * scale, y * scale), \
  164. ((x + 1) * scale, y * scale), \
  165. ((x + 1) * scale, (y + 1) * scale), \
  166. (x * scale, (y + 1) * scale)
  167. ]
  168. @classmethod
  169. def set_no_diagonals(cls, active):
  170. """ if nodiags is set to True, the neighbors method
  171. won't return the diagonals cells """
  172. cls._nodiags = active
  173. @classmethod
  174. def neighbors(cls, x, y, br=IBoundingRect()):
  175. """ reimplemented from BaseGeometry._neighbors """
  176. cls.assertCoordinates((x, y))
  177. if cls._nodiags:
  178. return [(x, y - 1), \
  179. (x - 1, y), (x + 1, y) , \
  180. (x, y + 1)]
  181. else:
  182. return [(x - 1, y - 1), (x, y - 1), (x + 1, y - 1), \
  183. (x - 1, y), (x + 1, y) , \
  184. (x - 1, y + 1), (x, y + 1), (x + 1, y + 1)]
  185. @classmethod
  186. def line(cls, x1, y1, x2, y2, br=IBoundingRect()):
  187. """ reimplemented from BaseGeometry.line
  188. Implementation of bresenham's algorithm
  189. """
  190. cls.assertCoordinates((x1, y1), (x2, y2))
  191. result = []
  192. if (x1, y1) == (x2, y2):
  193. return [(x1, y1)]
  194. # DIAGONAL SYMETRY
  195. V = (abs(y2 - y1) > abs(x2 - x1))
  196. if V: y1, x1, y2, x2 = x1, y1, x2, y2
  197. # VERTICAL SYMETRY
  198. reversed_sym = (x1 > x2)
  199. if reversed_sym:
  200. x2, y2, x1, y1 = x1, y1, x2, y2
  201. DX = x2 - x1 ; DY = y2 - y1
  202. offset = 0.0
  203. step = 1 if DY > 0 else -1
  204. alpha = (abs(DY) / DX)
  205. y = y1
  206. for x in range(x1, x2 + 1):
  207. coord = (y, x) if V else (x, y)
  208. result.append(coord)
  209. offset += alpha
  210. if offset > 0.5:
  211. y += step
  212. offset -= 1.0
  213. if reversed_sym:
  214. result.reverse()
  215. return result
  216. @classmethod
  217. def triangle(cls, xa, ya, xh, yh, iAngle, br=IBoundingRect()):
  218. """ reimplemented from BaseGeometry.triangle """
  219. cls.assertCoordinates((xa, ya), (xh, yh))
  220. cls._assertValidAngle(iAngle)
  221. if (xa, ya) == (xh, yh):
  222. return [(xa, ya)]
  223. result = []
  224. # direction vector
  225. dx_dir, dy_dir = xh - xa, yh - ya
  226. # normal vector
  227. dx_n, dy_n = -dy_dir, dx_dir
  228. # B and C positions
  229. k = 1 / (iAngle * sqrt(3))
  230. xb, yb = xh + (k * dx_n), yh + (k * dy_n)
  231. xc, yc = xh + (-k * dx_n), yh + (-k * dy_n)
  232. xb, yb = round(xb), round(yb)
  233. xc, yc = round(xc), round(yc)
  234. # sides:
  235. lines = [(xa, ya, xb, yb), (xb, yb, xc, yc), (xc, yc, xa, ya)]
  236. # base (lower slope)
  237. x1, y1, x2, y2 = min(lines, key=lambda x: (abs ((x[3] - x[1]) / (x[2] - x[0])) if x[2] != x[0] else 10 ** 10))
  238. base = cls.line(x1, y1, x2, y2)
  239. y_base = y1
  240. lines.remove((x1, y1, x2, y2))
  241. # 'hat' (2 other sides)
  242. hat = []
  243. y_top = None
  244. for x1, y1, x2, y2 in lines:
  245. if y_top == None:
  246. y_top = y2
  247. hat.extend(cls.line(x1, y1, x2, y2))
  248. # sense (1 if top is under base, -1 if not)
  249. sense = 1 if y_top > y_base else -1
  250. # rove over y values from base to hat
  251. for x, y in base:
  252. while not (x, y) in hat:
  253. result.append((x, y))
  254. y += sense
  255. result.extend(hat)
  256. return result
  257. @classmethod
  258. def triangle3d(cls, xa, ya, za, xh, yh, zh, iAngle, br=IBoundingRect()):
  259. """ reimplemented from BaseGeometry.triangle3d """
  260. cls.assertCoordinates((za, zh))
  261. flat_triangle = cls.triangle(xa, ya, xh, yh, iAngle)
  262. result = {}
  263. k = 1 / (iAngle * sqrt(3))
  264. length = max(abs(xh - xa), abs(yh - ya))
  265. vertical_line = cls.line(0, za, length, zh)
  266. # build a dict with X key and value is a list of Z values
  267. vertical_line_dict = {d:[] for d, z in vertical_line}
  268. for d, z in vertical_line:
  269. vertical_line_dict[d].append(z)
  270. # this is approximative: height is update according to the manhattan distance to center
  271. for x, y in flat_triangle:
  272. distance = int(max(abs(x - xa), abs(y - ya)))
  273. try:
  274. z_list = vertical_line_dict[ distance ]
  275. except KeyError:
  276. distance = length
  277. z_list = vertical_line_dict[ distance ]
  278. dh = int(k * distance) + 1 if distance > 0 else 0
  279. result[ (x, y) ] = ((min(z_list) - dh) , (max(z_list) + dh))
  280. return result
  281. @classmethod
  282. def rotate(cls, center, coordinates, rotations, br=IBoundingRect()):
  283. """ reimplemented from BaseGeometry.rotate """
  284. cls.assertCoordinates(center, *coordinates)
  285. if coordinates == [center] or rotations % 4 == 0:
  286. return coordinates
  287. x0, y0 = center
  288. result = []
  289. for x, y in coordinates:
  290. dx, dy = x - x0, y - y0
  291. for _ in range(rotations):
  292. dx, dy = dy, -dx
  293. xr, yr = dx + x0, dy + y0
  294. result.append((xr, yr))
  295. return result
  296. class HexGeometry(BaseGeometry):
  297. """ Base class for hexagonal grids classes
  298. This class should be overridden """
  299. @staticmethod
  300. def cv_cube_off(xu, yu, zu):
  301. """convert cubic coordinates (xu, yu, zu) in standards coordinates (x, y) [offset]"""
  302. y = int(xu + (zu - (zu & 1)) / 2)
  303. x = zu
  304. return (x, y)
  305. @staticmethod
  306. def cv_off_cube(x, y):
  307. """converts standards coordinates (x, y) [offset] in cubic coordinates (xu, yu, zu)"""
  308. zu = x
  309. xu = int(y - (x - (x & 1)) / 2)
  310. yu = int(-xu - zu)
  311. return (xu, yu, zu)
  312. # > unused
  313. @staticmethod
  314. def cube_round(x, y, z):
  315. """returns the nearest cell (in cubic coords)
  316. x, y, z can be floating numbers, no problem."""
  317. rx, ry, rz = round(x), round(y), round(z)
  318. x_diff, y_diff, z_diff = abs(rx - x), abs(ry - y), abs(rz - z)
  319. if x_diff > y_diff and x_diff > z_diff:
  320. rx = -ry - rz
  321. elif y_diff > z_diff:
  322. ry = -rx - rz
  323. else:
  324. rz = -rx - ry
  325. return (rx, ry, rz)
  326. # > unused
  327. @staticmethod
  328. def hex_distance_cube(xa, ya, za, xb, yb, zb):
  329. """returns the manhattan distance between the two cells"""
  330. return max(abs(xa - xb), abs(ya - yb), abs(za - zb))
  331. # > unused
  332. @staticmethod
  333. def distance_off(xa, ya, xb, yb):
  334. """ distance between A and B (offset coordinates)"""
  335. # 10 times quicker if no conversion...
  336. xua, yua, zua = HexGeometry.cv_off_cube(xa, ya)
  337. xub, yub, zub = HexGeometry.cv_off_cube(xb, yb)
  338. return max(abs(xua - xub), abs(yua - yub), abs(zua - zub))
  339. class FHexGeometry(HexGeometry):
  340. """ Flat-hexagonal grid object """
  341. @staticmethod
  342. def graphicsitem(x, y, scale=120):
  343. """ reimplemented from BaseGeometry.graphicsitem """
  344. if x % 2 != 0:
  345. y += 0.5
  346. return [
  347. (((x * 0.866) + 0.2886) * scale , y * scale), \
  348. (((x * 0.866) + 0.866) * scale , y * scale), \
  349. (((x * 0.866) + 1.1547) * scale , (y + 0.5) * scale), \
  350. (((x * 0.866) + 0.866) * scale , (y + 1) * scale), \
  351. (((x * 0.866) + 0.2886) * scale , (y + 1) * scale), \
  352. ((x * 0.866) * scale , (y + 0.5) * scale)
  353. ]
  354. @classmethod
  355. def neighbors(cls, x, y, br=IBoundingRect()):
  356. if x % 2 == 0:
  357. return [(x, y - 1), (x + 1, y - 1), (x + 1, y), (x, y + 1), (x - 1, y), (x - 1, y - 1)]
  358. else:
  359. return [(x, y - 1), (x + 1, y), (x + 1, y + 1), (x, y + 1), (x - 1, y + 1), (x - 1, y)]
  360. @classmethod
  361. def line(cls, x1, y1, x2, y2, br=IBoundingRect()):
  362. """ reimplemented from BaseGeometry.line
  363. Implementation of bresenham's algorithm """
  364. cls.assertCoordinates((x1, y1), (x2, y2))
  365. if (x1, y1) == (x2, y2):
  366. return [(x1, y1)]
  367. reversed_sym = (x1 > x2)
  368. if reversed_sym:
  369. x1, x2 = x2, x1
  370. y1, y2 = y2, y1
  371. if abs(x2 - x1) < (2 * abs((y2 - y1)) + abs(x2 % 2) - abs(x1 % 1)):
  372. # vertical quadrants
  373. # unit is half the width: u = 0.5773
  374. # half-height is then 0.8860u, or sqrt(3)/2
  375. direction = 1 if y2 > y1 else -1
  376. dx = 1.5 * (x2 - x1)
  377. dy = direction * (y2 - y1)
  378. if (x1 + x2) % 2 == 1:
  379. if x1 % 2 == 0:
  380. dy += direction * 0.5
  381. else:
  382. dy -= direction * 0.5
  383. k = dx / (dy * sqrt(3))
  384. pas = 0.5 * sqrt(3)
  385. result = []
  386. offset = 0.0
  387. pos = (x1, y1)
  388. result.append(pos)
  389. while pos != (x2, y2):
  390. offset += (k * pas)
  391. if offset <= 0.5:
  392. x, y = pos
  393. pos = x, y + direction
  394. result.append(pos)
  395. offset += (k * pas)
  396. else:
  397. x, y = pos
  398. if (x % 2 == 0 and direction == 1) or (x % 2 == 1 and direction == -1):
  399. pos = x + 1, y
  400. else:
  401. pos = x + 1, y + direction
  402. result.append(pos)
  403. offset -= 1.5
  404. # in case of error in the algorithm, we should avoid infinite loop:
  405. if direction * pos[1] > direction * y2:
  406. result = []
  407. break
  408. else:
  409. # horizontal quadrants
  410. dx = x2 - x1 ; dy = y2 - y1
  411. if (x1 + x2) % 2 == 1:
  412. dy += 0.5 if x1 % 2 == 0 else -0.5
  413. k = dy / dx
  414. pas = 1
  415. result = []
  416. d = 0.0
  417. pos = (x1, y1)
  418. result.append(pos)
  419. while pos != (x2, y2):
  420. d += k * pas
  421. if d > 0:
  422. x, y = pos
  423. if x % 2 == 0:
  424. pos = x + 1, y
  425. else:
  426. pos = x + 1, y + 1
  427. result.append(pos)
  428. d -= 0.5
  429. else:
  430. x, y = pos
  431. if x % 2 == 0:
  432. pos = x + 1, y - 1
  433. else:
  434. pos = x + 1, y
  435. result.append(pos)
  436. d += 0.5
  437. # in case of error in the algorithm, we should avoid infinite loop:
  438. if pos[0] > x2:
  439. result = []
  440. break
  441. if reversed_sym:
  442. result.reverse()
  443. return result
  444. @classmethod
  445. def triangle(cls, xa, ya, xh, yh, iAngle, br=IBoundingRect()):
  446. """ reimplemented from BaseGeometry.triangle """
  447. cls.assertCoordinates((xa, ya), (xh, yh))
  448. cls._assertValidAngle(iAngle)
  449. if (xa, ya) == (xh, yh):
  450. return [(xa, ya)]
  451. result = []
  452. # convert to cubic coodinates (see 'cube_coords' lib)
  453. xua, yua, _ = cls.cv_off_cube(xa, ya)
  454. xuh, yuh, zuh = cls.cv_off_cube(xh, yh)
  455. # direction vector
  456. dx_dir, dy_dir = xuh - xua, yuh - yua
  457. # normal vector
  458. dx_n, dy_n = -(2 * dy_dir + dx_dir), (2 * dx_dir + dy_dir)
  459. dz_n = (-dx_n - dy_n)
  460. # B and C positions
  461. k = 1 / (iAngle * sqrt(3))
  462. xub, yub, zub = xuh + (k * dx_n), yuh + (k * dy_n), zuh + (k * dz_n)
  463. xuc, yuc, zuc = xuh + (-k * dx_n), yuh + (-k * dy_n), zuh + (-k * dz_n)
  464. xub, yub, zub = cls.cube_round(xub, yub, zub)
  465. xuc, yuc, zuc = cls.cube_round(xuc, yuc, zuc)
  466. xb, yb = cls.cv_cube_off(xub, yub, zub)
  467. xc, yc = cls.cv_cube_off(xuc, yuc, zuc)
  468. # sides
  469. segments = [(xa, ya, xb, yb), (xb, yb, xc, yc), (xc, yc, xa, ya)]
  470. # base (lower slope)
  471. x1, y1, x2, y2 = min(segments, key=lambda x: (abs ((x[3] - x[1]) / (x[2] - x[0])) if x[2] != x[0] else 10 ** 10))
  472. base = cls.line(x1, y1, x2, y2)
  473. y_base = y1
  474. segments.remove((x1, y1, x2, y2))
  475. # 'hat' (the 2 other sides)
  476. chapeau = []
  477. y_sommet = None
  478. for x1, y1, x2, y2 in segments:
  479. if y_sommet == None:
  480. y_sommet = y2
  481. chapeau.extend(cls.line(x1, y1, x2, y2))
  482. # sense (1 if top is under base, -1 if not)
  483. sens = 1 if y_sommet > y_base else -1
  484. # rove over y values from base to hat
  485. for x, y in base:
  486. while not (x, y) in chapeau:
  487. result.append((x, y))
  488. y += sens
  489. result.extend(chapeau)
  490. return result
  491. @classmethod
  492. def triangle3d(cls, xa, ya, za, xh, yh, zh, iAngle, br=IBoundingRect()):
  493. """ reimplemented from BaseGeometry.triangle3d """
  494. cls.assertCoordinates((za, zh))
  495. flat_triangle = cls.triangle(xa, ya, xh, yh, iAngle)
  496. result = {}
  497. k = 1 / (iAngle * sqrt(3))
  498. # use cubic coordinates
  499. xua, yua, zua = cls.cv_off_cube(xa, ya)
  500. xuh, yuh, zuh = cls.cv_off_cube(xh, yh)
  501. length = max(abs(xuh - xua), abs(yuh - yua), abs(zuh - zua))
  502. vertical_line = SquareGeometry.line(0, za, length, zh)
  503. # build a dict with X key and value is a list of Z values
  504. vertical_line_dict = {d:[] for d, z in vertical_line}
  505. for d, z in vertical_line:
  506. vertical_line_dict[d].append(z)
  507. # this is approximative: height is update according to the manhattan distance to center
  508. for x, y in flat_triangle:
  509. xu, yu, zu = cls.cv_off_cube(x, y)
  510. distance = int(max(abs(xu - xua), abs(yu - yua), abs(zu - zua)))
  511. try:
  512. z_list = vertical_line_dict[ distance ]
  513. except KeyError:
  514. distance = length
  515. z_list = vertical_line_dict[ distance ]
  516. dh = int(k * distance) + 1 if distance > 0 else 0
  517. result[ (x, y) ] = ((min(z_list) - dh) , (max(z_list) + dh))
  518. return result
  519. @classmethod
  520. def rotate(cls, center, coordinates, rotations, br=IBoundingRect()):
  521. """ reimplemented from BaseGeometry.rotate """
  522. cls.assertCoordinates(center, *coordinates)
  523. if coordinates == [center] or rotations % 6 == 0:
  524. return coordinates
  525. x0, y0 = center
  526. xu0, yu0, zu0 = cls.cv_off_cube(x0, y0)
  527. result = []
  528. for x, y in coordinates:
  529. xu, yu, zu = cls.cv_off_cube(x, y)
  530. dxu, dyu, dzu = xu - xu0, yu - yu0, zu - zu0
  531. for _ in range(rotations):
  532. dxu, dyu, dzu = -dzu, -dxu, -dyu
  533. xru, yru, zru = dxu + xu0, dyu + yu0, dzu + zu0
  534. xr, yr = cls.cv_cube_off(xru, yru, zru)
  535. result.append((xr, yr))
  536. return result