abstract-3d 4.0.5 → 4.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/lib/renderers/svg/svg-encoding.d.ts +2 -0
  2. package/lib/renderers/svg/svg-encoding.d.ts.map +1 -1
  3. package/lib/renderers/svg/svg-encoding.js +12 -0
  4. package/lib/renderers/svg/svg-encoding.js.map +1 -1
  5. package/lib/renderers/svg/svg-geometries/shared.d.ts +11 -4
  6. package/lib/renderers/svg/svg-geometries/shared.d.ts.map +1 -1
  7. package/lib/renderers/svg/svg-geometries/shared.js +2 -1
  8. package/lib/renderers/svg/svg-geometries/shared.js.map +1 -1
  9. package/lib/renderers/svg/svg-geometries/svg-box.d.ts.map +1 -1
  10. package/lib/renderers/svg/svg-geometries/svg-box.js +4 -1
  11. package/lib/renderers/svg/svg-geometries/svg-box.js.map +1 -1
  12. package/lib/renderers/svg/svg-geometries/svg-cone.d.ts.map +1 -1
  13. package/lib/renderers/svg/svg-geometries/svg-cone.js +1 -1
  14. package/lib/renderers/svg/svg-geometries/svg-cone.js.map +1 -1
  15. package/lib/renderers/svg/svg-geometries/svg-cylinder.d.ts.map +1 -1
  16. package/lib/renderers/svg/svg-geometries/svg-cylinder.js +3 -2
  17. package/lib/renderers/svg/svg-geometries/svg-cylinder.js.map +1 -1
  18. package/lib/renderers/svg/svg-geometries/svg-plane.d.ts.map +1 -1
  19. package/lib/renderers/svg/svg-geometries/svg-plane.js +3 -1
  20. package/lib/renderers/svg/svg-geometries/svg-plane.js.map +1 -1
  21. package/lib/renderers/svg/svg-geometries/svg-polygon.d.ts.map +1 -1
  22. package/lib/renderers/svg/svg-geometries/svg-polygon.js +7 -1
  23. package/lib/renderers/svg/svg-geometries/svg-polygon.js.map +1 -1
  24. package/lib/renderers/svg/svg-geometries/svg-shape.d.ts.map +1 -1
  25. package/lib/renderers/svg/svg-geometries/svg-shape.js +2 -1
  26. package/lib/renderers/svg/svg-geometries/svg-shape.js.map +1 -1
  27. package/lib/renderers/svg/svg-order.d.ts +6 -5
  28. package/lib/renderers/svg/svg-order.d.ts.map +1 -1
  29. package/lib/renderers/svg/svg-order.js +233 -124
  30. package/lib/renderers/svg/svg-order.js.map +1 -1
  31. package/lib/renderers/svg/svg.js +10 -8
  32. package/lib/renderers/svg/svg.js.map +1 -1
  33. package/package.json +3 -3
  34. package/src/renderers/svg/svg-encoding.ts +13 -0
  35. package/src/renderers/svg/svg-geometries/shared.ts +13 -5
  36. package/src/renderers/svg/svg-geometries/svg-box.ts +10 -1
  37. package/src/renderers/svg/svg-geometries/svg-cone.ts +2 -1
  38. package/src/renderers/svg/svg-geometries/svg-cylinder.ts +3 -2
  39. package/src/renderers/svg/svg-geometries/svg-plane.ts +5 -2
  40. package/src/renderers/svg/svg-geometries/svg-polygon.ts +7 -6
  41. package/src/renderers/svg/svg-geometries/svg-shape.ts +4 -2
  42. package/src/renderers/svg/svg-order.test.ts +61 -6
  43. package/src/renderers/svg/svg-order.ts +257 -135
  44. package/src/renderers/svg/svg.ts +13 -8
  45. package/lib/renderers/ifc/ifc.test.d.ts +0 -2
  46. package/lib/renderers/ifc/ifc.test.d.ts.map +0 -1
  47. package/lib/renderers/ifc/ifc.test.js +0 -380
  48. package/lib/renderers/ifc/ifc.test.js.map +0 -1
  49. package/lib/renderers/react/react-camera.d.ts +0 -71
  50. package/lib/renderers/react/react-camera.d.ts.map +0 -1
  51. package/lib/renderers/react/react-camera.js +0 -201
  52. package/lib/renderers/react/react-camera.js.map +0 -1
  53. package/lib/renderers/react/react-dimension.d.ts +0 -27
  54. package/lib/renderers/react/react-dimension.d.ts.map +0 -1
  55. package/lib/renderers/react/react-dimension.js +0 -44
  56. package/lib/renderers/react/react-dimension.js.map +0 -1
  57. package/lib/renderers/react/react-group.d.ts +0 -29
  58. package/lib/renderers/react/react-group.d.ts.map +0 -1
  59. package/lib/renderers/react/react-group.js +0 -82
  60. package/lib/renderers/react/react-group.js.map +0 -1
  61. package/lib/renderers/react/react-image-material.d.ts +0 -30
  62. package/lib/renderers/react/react-image-material.d.ts.map +0 -1
  63. package/lib/renderers/react/react-image-material.js +0 -81
  64. package/lib/renderers/react/react-image-material.js.map +0 -1
  65. package/lib/renderers/react/react-material.d.ts +0 -19
  66. package/lib/renderers/react/react-material.d.ts.map +0 -1
  67. package/lib/renderers/react/react-material.js +0 -39
  68. package/lib/renderers/react/react-material.js.map +0 -1
  69. package/lib/renderers/react/react-mesh.d.ts +0 -11
  70. package/lib/renderers/react/react-mesh.d.ts.map +0 -1
  71. package/lib/renderers/react/react-mesh.js +0 -302
  72. package/lib/renderers/react/react-mesh.js.map +0 -1
  73. package/lib/renderers/react/react-relation.d.ts +0 -10
  74. package/lib/renderers/react/react-relation.d.ts.map +0 -1
  75. package/lib/renderers/react/react-relation.js +0 -31
  76. package/lib/renderers/react/react-relation.js.map +0 -1
  77. package/lib/renderers/react/react-scene.d.ts +0 -25
  78. package/lib/renderers/react/react-scene.d.ts.map +0 -1
  79. package/lib/renderers/react/react-scene.js +0 -13
  80. package/lib/renderers/react/react-scene.js.map +0 -1
  81. package/lib/renderers/react/react-types.d.ts +0 -7
  82. package/lib/renderers/react/react-types.d.ts.map +0 -1
  83. package/lib/renderers/react/react-types.js +0 -3
  84. package/lib/renderers/react/react-types.js.map +0 -1
  85. package/lib/renderers/three/appearance/appearance.test.d.ts +0 -2
  86. package/lib/renderers/three/appearance/appearance.test.d.ts.map +0 -1
  87. package/lib/renderers/three/appearance/appearance.test.js +0 -97
  88. package/lib/renderers/three/appearance/appearance.test.js.map +0 -1
  89. package/lib/renderers/three/appearance.d.ts +0 -41
  90. package/lib/renderers/three/appearance.d.ts.map +0 -1
  91. package/lib/renderers/three/appearance.js +0 -100
  92. package/lib/renderers/three/appearance.js.map +0 -1
  93. package/lib/renderers/three/build/build.test.d.ts +0 -2
  94. package/lib/renderers/three/build/build.test.d.ts.map +0 -1
  95. package/lib/renderers/three/build/build.test.js +0 -120
  96. package/lib/renderers/three/build/build.test.js.map +0 -1
  97. package/lib/renderers/three/build.d.ts +0 -50
  98. package/lib/renderers/three/build.d.ts.map +0 -1
  99. package/lib/renderers/three/build.js +0 -210
  100. package/lib/renderers/three/build.js.map +0 -1
  101. package/lib/renderers/three/camera/camera.test.d.ts +0 -2
  102. package/lib/renderers/three/camera/camera.test.d.ts.map +0 -1
  103. package/lib/renderers/three/camera/camera.test.js +0 -46
  104. package/lib/renderers/three/camera/camera.test.js.map +0 -1
  105. package/lib/renderers/three/camera.d.ts +0 -63
  106. package/lib/renderers/three/camera.d.ts.map +0 -1
  107. package/lib/renderers/three/camera.js +0 -85
  108. package/lib/renderers/three/camera.js.map +0 -1
  109. package/lib/renderers/three/compile/compile.test.d.ts +0 -2
  110. package/lib/renderers/three/compile/compile.test.d.ts.map +0 -1
  111. package/lib/renderers/three/compile/compile.test.js +0 -93
  112. package/lib/renderers/three/compile/compile.test.js.map +0 -1
  113. package/lib/renderers/three/compile.d.ts +0 -58
  114. package/lib/renderers/three/compile.d.ts.map +0 -1
  115. package/lib/renderers/three/compile.js +0 -89
  116. package/lib/renderers/three/compile.js.map +0 -1
  117. package/lib/renderers/three/controls.d.ts +0 -27
  118. package/lib/renderers/three/controls.d.ts.map +0 -1
  119. package/lib/renderers/three/controls.js +0 -115
  120. package/lib/renderers/three/controls.js.map +0 -1
  121. package/lib/renderers/three/geometry/geometry.test.d.ts +0 -2
  122. package/lib/renderers/three/geometry/geometry.test.d.ts.map +0 -1
  123. package/lib/renderers/three/geometry/geometry.test.js +0 -80
  124. package/lib/renderers/three/geometry/geometry.test.js.map +0 -1
  125. package/lib/renderers/three/geometry.d.ts +0 -20
  126. package/lib/renderers/three/geometry.d.ts.map +0 -1
  127. package/lib/renderers/three/geometry.js +0 -164
  128. package/lib/renderers/three/geometry.js.map +0 -1
  129. package/lib/renderers/three/gizmo.d.ts +0 -49
  130. package/lib/renderers/three/gizmo.d.ts.map +0 -1
  131. package/lib/renderers/three/gizmo.js +0 -172
  132. package/lib/renderers/three/gizmo.js.map +0 -1
  133. package/lib/renderers/three/input.d.ts +0 -25
  134. package/lib/renderers/three/input.d.ts.map +0 -1
  135. package/lib/renderers/three/input.js +0 -71
  136. package/lib/renderers/three/input.js.map +0 -1
  137. package/lib/renderers/three/lines.d.ts +0 -16
  138. package/lib/renderers/three/lines.d.ts.map +0 -1
  139. package/lib/renderers/three/lines.js +0 -63
  140. package/lib/renderers/three/lines.js.map +0 -1
  141. package/lib/renderers/three/materials.d.ts +0 -15
  142. package/lib/renderers/three/materials.d.ts.map +0 -1
  143. package/lib/renderers/three/materials.js +0 -70
  144. package/lib/renderers/three/materials.js.map +0 -1
  145. package/lib/renderers/three/options.d.ts +0 -68
  146. package/lib/renderers/three/options.d.ts.map +0 -1
  147. package/lib/renderers/three/options.js +0 -64
  148. package/lib/renderers/three/options.js.map +0 -1
  149. package/lib/renderers/three/overlays.d.ts +0 -25
  150. package/lib/renderers/three/overlays.d.ts.map +0 -1
  151. package/lib/renderers/three/overlays.js +0 -57
  152. package/lib/renderers/three/overlays.js.map +0 -1
  153. package/lib/renderers/three/quality/quality.test.d.ts +0 -2
  154. package/lib/renderers/three/quality/quality.test.d.ts.map +0 -1
  155. package/lib/renderers/three/quality/quality.test.js +0 -23
  156. package/lib/renderers/three/quality/quality.test.js.map +0 -1
  157. package/lib/renderers/three/quality.d.ts +0 -24
  158. package/lib/renderers/three/quality.d.ts.map +0 -1
  159. package/lib/renderers/three/quality.js +0 -67
  160. package/lib/renderers/three/quality.js.map +0 -1
  161. package/lib/renderers/three/text/text.test.d.ts +0 -2
  162. package/lib/renderers/three/text/text.test.d.ts.map +0 -1
  163. package/lib/renderers/three/text/text.test.js +0 -70
  164. package/lib/renderers/three/text/text.test.js.map +0 -1
  165. package/lib/renderers/three/text.d.ts +0 -53
  166. package/lib/renderers/three/text.d.ts.map +0 -1
  167. package/lib/renderers/three/text.js +0 -151
  168. package/lib/renderers/three/text.js.map +0 -1
  169. package/lib/renderers/three/viewer/options.test.d.ts +0 -2
  170. package/lib/renderers/three/viewer/options.test.d.ts.map +0 -1
  171. package/lib/renderers/three/viewer/options.test.js +0 -38
  172. package/lib/renderers/three/viewer/options.test.js.map +0 -1
  173. package/lib/renderers/three/viewer.d.ts +0 -38
  174. package/lib/renderers/three/viewer.d.ts.map +0 -1
  175. package/lib/renderers/three/viewer.js +0 -435
  176. package/lib/renderers/three/viewer.js.map +0 -1
@@ -42,7 +42,8 @@ export function cone(
42
42
  svgPolygon(rot, points, fill, opacity, stroke, stBW),
43
43
  vec3ZMean(currBot, prevBot, topPos),
44
44
  [currBot, prevBot, topPos],
45
- isOpaque(fill, opacity)
45
+ isOpaque(fill, opacity),
46
+ { fill, opacity, stroke, strokeWidth: stBW }
46
47
  )
47
48
  );
48
49
  }
@@ -25,6 +25,7 @@ export function cylinder(
25
25
  ? [opts.gray_scale ? gray : color, opts.background]
26
26
  : [transparent, opts.gray_scale ? rgbGrayScale(color) : color];
27
27
  const opaque = isOpaque(fill, opacity);
28
+ const style = { fill, opacity, stroke, strokeWidth: stBW };
28
29
  // A closed opaque cylinder's facets turned away are hidden by those toward the viewer and its near end
29
30
  const cullBack = opaque && !c.open;
30
31
  const zOrderComponents = Array<zOrderElement>();
@@ -49,7 +50,7 @@ export function cylinder(
49
50
 
50
51
  const points = [point(currBot.x, currBot.y), point(prevBot.x, prevBot.y), point(prevTop.x, prevTop.y), point(currTop.x, currTop.y)];
51
52
  const polygon = [currBot, prevBot, prevTop, currTop];
52
- zOrderComponents.push(zElem(svgPolygon(rot, points, fill, opacity, stroke, stBW), vec3ZMean(...polygon), polygon, opaque));
53
+ zOrderComponents.push(zElem(svgPolygon(rot, points, fill, opacity, stroke, stBW), vec3ZMean(...polygon), polygon, opaque, style));
53
54
  }
54
55
  currentAngle += angleStep;
55
56
  }
@@ -73,7 +74,7 @@ export function cylinder(
73
74
  const points = ring.map((p) => point(p.x, p.y));
74
75
  // Not seen edge on, when it's only a line along the facets' edges
75
76
  if (Math.abs(getArea(points)) >= 1e-6) {
76
- zOrderComponents.push(zElem(svgPolygon(rot, points, fill, opacity, stroke, stBW), circlePos.z, ring, opaque && !c.holes?.length));
77
+ zOrderComponents.push(zElem(svgPolygon(rot, points, fill, opacity, stroke, stBW), circlePos.z, ring, opaque && !c.holes?.length, style));
77
78
  }
78
79
  }
79
80
  }
@@ -29,12 +29,15 @@ export function plane(
29
29
  const [strokeColor, fill, strokeThickness] = opts.only_stroke
30
30
  ? [opts.gray_scale ? gray : material.normal, opts.background, opts.stroke_thickness]
31
31
  : [black, opts.gray_scale ? rgbGrayScale(material.normal) : material.normal, 0];
32
+ const opacity = material.opacity ?? 1.0;
33
+ const plain = !holes?.length;
32
34
  return [
33
35
  zElem(
34
- svgPolygon(rot, points, fill, material.opacity ?? 1.0, strokeColor, strokeThickness, holes),
36
+ svgPolygon(rot, points, fill, opacity, strokeColor, strokeThickness, holes),
35
37
  vec3ZMean(v1, v2, v3, v4),
36
38
  [v1, v2, v3, v4],
37
- isOpaque(fill, material.opacity ?? 1.0) && !holes?.length
39
+ isOpaque(fill, opacity) && plain,
40
+ plain ? { fill, opacity, stroke: strokeColor, strokeWidth: strokeThickness } : undefined
38
41
  ),
39
42
  ];
40
43
  }
@@ -21,12 +21,13 @@ export function polygon(
21
21
  const [strokeColor, fill, strokeThickness] = opts.only_stroke
22
22
  ? [opts.gray_scale ? gray : color, opts.background, opts.stroke_thickness]
23
23
  : [transparent, opts.gray_scale ? rgbGrayScale(color) : color, 0];
24
+ const opacity = material.opacity ?? 1.0;
24
25
  return [
25
- zElem(
26
- svgPolygon(rot, points, fill, material.opacity ?? 1.0, strokeColor, strokeThickness),
27
- vec3ZMean(...rotatedPoints),
28
- rotatedPoints,
29
- isOpaque(fill, material.opacity ?? 1.0)
30
- ),
26
+ zElem(svgPolygon(rot, points, fill, opacity, strokeColor, strokeThickness), vec3ZMean(...rotatedPoints), rotatedPoints, isOpaque(fill, opacity), {
27
+ fill,
28
+ opacity,
29
+ stroke: strokeColor,
30
+ strokeWidth: strokeThickness,
31
+ }),
31
32
  ];
32
33
  }
@@ -21,12 +21,14 @@ export function shape(
21
21
  const [strokeColor, fill] = opts.only_stroke
22
22
  ? [opts.gray_scale ? gray : color, opts.background]
23
23
  : [transparent, opts.gray_scale ? rgbGrayScale(color) : color];
24
+ const opacity = material.opacity ?? 1.0;
24
25
  return [
25
26
  zElem(
26
- svgPolygon(rot, points, fill, material.opacity ?? 1.0, strokeColor, 0),
27
+ svgPolygon(rot, points, fill, opacity, strokeColor, 0),
27
28
  vec3ZMean(...rotatedPoints),
28
29
  rotatedPoints,
29
- isOpaque(fill, material.opacity ?? 1.0) && !s.holes?.length
30
+ isOpaque(fill, opacity) && !s.holes?.length,
31
+ { fill, opacity, stroke: strokeColor, strokeWidth: 0 }
30
32
  ),
31
33
  ];
32
34
  }
@@ -1,14 +1,26 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import type { Vec3 } from "../../abstract-3d.js";
3
- import { vec3 } from "../../abstract-3d.js";
2
+ import type { Vec2, Vec3 } from "../../abstract-3d.js";
3
+ import { vec2, vec3 } from "../../abstract-3d.js";
4
+ import type { PolygonStyle, zOrderElement } from "./svg-geometries/shared.js";
4
5
  import { zElem } from "./svg-geometries/shared.js";
5
6
  import { orderElements } from "./svg-order.js";
6
7
 
7
- const quad = (element: string, x0: number, x1: number, z: (x: number) => number): ReturnType<typeof zElem> => {
8
+ const point = (x: number, y: number): Vec2 => vec2(x, -y);
9
+ const quad = (element: string, x0: number, x1: number, z: (x: number) => number, opaque?: boolean, style?: PolygonStyle): zOrderElement => {
8
10
  const points: ReadonlyArray<Vec3> = [vec3(x0, -1, z(x0)), vec3(x1, -1, z(x1)), vec3(x1, 1, z(x1)), vec3(x0, 1, z(x0))];
9
- return zElem(element, points.reduce((sum, p) => sum + p.z, 0) / points.length, points);
11
+ return zElem(element, points.reduce((sum, p) => sum + p.z, 0) / points.length, points, opaque, style);
10
12
  };
11
- const order = (...elements: ReadonlyArray<ReturnType<typeof zElem>>): ReadonlyArray<string> => orderElements(elements).map((e) => e.element);
13
+ const style = (fill: string, strokeWidth = 0): PolygonStyle => ({ fill, opacity: 1, stroke: "rgb(0,0,0)", strokeWidth });
14
+ const order = (...elements: ReadonlyArray<zOrderElement>): ReadonlyArray<string> => orderElements(elements, point).map((e) => e.element);
15
+ /** The fill of the last drawn element whose polygon contains the point on screen */
16
+ const topAt = (ordered: ReadonlyArray<zOrderElement>, x: number, y: number): string | undefined =>
17
+ ordered
18
+ .filter(({ polygon = [] }) => {
19
+ const xs = polygon.map((p) => p.x);
20
+ const ys = polygon.map((p) => p.y);
21
+ return Math.min(...xs) < x && x < Math.max(...xs) && Math.min(...ys) < y && y < Math.max(...ys);
22
+ })
23
+ .at(-1)?.style?.fill;
12
24
 
13
25
  describe("orderElements", () => {
14
26
  it("draws a part in front of a long tilted face after it, though the face's mean depth is nearer", () => {
@@ -28,8 +40,51 @@ describe("orderElements", () => {
28
40
  expect(order(zElem("text", 2), zElem("line", -2))).toEqual(["line", "text"]);
29
41
  });
30
42
 
31
- it("draws every element once, also when they overlap in a cycle", () => {
43
+ it("draws every element once when it can't be split, also when they overlap in a cycle", () => {
32
44
  const elements = [quad("a", 0, 2, (x) => x), quad("b", 1, 3, (x) => 2 - x), quad("c", -1, 4, () => 1), zElem("text", 0)];
33
45
  expect([...order(...elements)].sort()).toEqual(["a", "b", "c", "text"]);
34
46
  });
47
+
48
+ it("splits faces that cut through each other, each drawn in front where it's nearer", () => {
49
+ // Crossing at x 0: a is nearer at x > 0, b at x < 0
50
+ const ordered = orderElements([quad("a", -1, 1, (x) => x, true, style("a")), quad("b", -1, 1, (x) => -x, true, style("b"))], point);
51
+ expect([topAt(ordered, 0.5, 0), topAt(ordered, -0.5, 0)]).toEqual(["a", "b"]);
52
+ });
53
+
54
+ it("strokes a split face's pieces along its own edges, not the cut", () => {
55
+ const ordered = orderElements([quad("a", -1, 1, (x) => x, true, style("a", 2)), quad("b", -1, 1, (x) => -x, true, style("b", 2))], point);
56
+ const pieces = ordered.filter((e) => e.element.includes("<path"));
57
+ expect(pieces.length).toBeGreaterThan(0);
58
+ // A piece of a: its polygon unstroked, its outline three of its four edges, without the cut at x 0 from y -1 to 1
59
+ pieces.forEach((e) => expect(e.element).toMatch(/^<polygon[^>]*stroke-width="0"[^>]*\/><path[^>]* d="M[^"]*L[^"]*L[^"]*L[^"]*"/));
60
+ pieces.forEach((e) => expect(e.element).not.toMatch(/0,-1L0,1|0,1L0,-1/));
61
+ });
62
+
63
+ it("draws a face split by a plane whole, when nothing drawn between its pieces overlaps it", () => {
64
+ // Each is on both sides of the other's plane, so one is split, but b is above a on screen
65
+ const b = zElem("b", 0, [vec3(-1, 5, 1), vec3(1, 5, -1), vec3(1, 6, -1), vec3(-1, 6, 1)], true, style("b"));
66
+ expect(
67
+ [
68
+ ...order(
69
+ quad("a", -1, 1, (x) => x, true, style("a")),
70
+ b
71
+ ),
72
+ ].sort()
73
+ ).toEqual(["a", "b"]);
74
+ });
75
+
76
+ it("leaves out what an opaque face in front covers", () => {
77
+ expect(
78
+ order(
79
+ quad("small", -1, 1, () => 0),
80
+ quad("big", -2, 2, () => 1, true)
81
+ )
82
+ ).toEqual(["big"]);
83
+ expect(
84
+ order(
85
+ quad("small", -1, 1, () => 0),
86
+ quad("glass", -2, 2, () => 1)
87
+ )
88
+ ).toEqual(["small", "glass"]);
89
+ });
35
90
  });
@@ -1,172 +1,294 @@
1
- import type { Vec3 } from "../../abstract-3d.js";
1
+ import type { Vec2, Vec3 } from "../../abstract-3d.js";
2
+ import { vec3Zero } from "../../abstract-3d.js";
3
+ import { svgEdges, svgPolygon } from "./svg-encoding.js";
2
4
  import type { zOrderElement } from "./svg-geometries/shared.js";
5
+ import { isOpaque } from "./svg-geometries/shared.js";
3
6
 
4
7
  /**
5
- * Elements in drawing order, back to front, by Newell's ordering without splitting: of two elements that overlap on screen, one entirely behind the
6
- * other's plane, or the other entirely in front of its plane, is drawn first. Elements are drawn by mean depth otherwise, each after those it must be
7
- * drawn after, depth first; elements overlapping in a cycle, which only splitting them could resolve, are drawn where the cycle is met. Elements
8
- * entirely inside an opaque convex polygon in front of them are left out, as they can't be seen
8
+ * Elements in drawing order, back to front, by a BSP tree of their planes: of each node, what's behind its plane is drawn first, then what's in it,
9
+ * then what's in front of it. An element on both sides of a plane is split by it when it's a plain polygon, its pieces stroked only along its own
10
+ * edges, else put on the side of its center, and drawn whole again where nothing drawn between its pieces overlaps it on screen. Elements without
11
+ * points are drawn last, by mean depth. Elements entirely inside an opaque convex polygon drawn after them are left out, as they can't be seen
9
12
  */
10
- export function orderElements(elements: ReadonlyArray<zOrderElement>): ReadonlyArray<zOrderElement> {
11
- const items: Array<Item | undefined> = [];
12
- const swept: Array<number> = [];
13
- const byDepth: Array<number> = [];
14
- elements.forEach((e, i) => {
15
- items.push(e.polygon && createItem(e.polygon, !!e.opaque));
16
- byDepth.push(i);
17
- if (e.polygon) {
18
- swept.push(i);
13
+ export function orderElements(elements: ReadonlyArray<zOrderElement>, point: (x: number, y: number) => Vec2): ReadonlyArray<zOrderElement> {
14
+ const pieces: Array<Piece> = [];
15
+ const pointless: Array<zOrderElement> = [];
16
+ for (const element of elements) {
17
+ if (element.polygon?.length) {
18
+ pieces.push({ element, points: element.polygon, plane: getPlane(element.polygon) });
19
+ } else {
20
+ pointless.push(element);
19
21
  }
20
- });
21
- swept.sort((a, b) => items[a]!.min.x - items[b]!.min.x);
22
- byDepth.sort((a, b) => elements[a]!.zOrder - elements[b]!.zOrder);
23
-
24
- // Sweep along x: each item is compared with the active ones whose bounds still overlap it on screen, dropping the others in the same pass
25
- const behind: Array<Array<number>> = [];
26
- const hidden = new Uint8Array(elements.length);
27
- const active: Array<number> = [];
28
- for (const i of swept) {
29
- const a = items[i]!;
30
- active.length = active.reduce((kept, j) => {
31
- const b = items[j]!;
32
- if (b.max.x <= a.min.x + eps) {
33
- return kept;
34
- }
35
- active[kept] = j;
36
- const order = b.min.y < a.max.y - eps && a.min.y < b.max.y - eps ? getOrder(a, b) : 0;
37
- if (order !== 0) {
38
- const [back, front] = order < 0 ? [i, j] : [j, i];
39
- (behind[front] ??= []).push(back);
40
- hidden[back] ||= covers(items[front]!, items[back]!) ? 1 : 0;
41
- }
42
- return kept + 1;
43
- }, 0);
44
- active.push(i);
45
22
  }
46
-
47
- // Depth first: an element is drawn when it's met on the stack again, after the elements behind it; 1 while it waits, 2 once drawn
48
- const state = new Uint8Array(elements.length);
49
- const stack: Array<number> = [];
23
+ const drawn: Drawn = { pieces: [], items: [], spans: new Map() };
24
+ draw(pieces, drawn);
25
+ const state = getHidden(drawn.items);
26
+ mergeSplit(drawn, state);
50
27
  const ordered: Array<zOrderElement> = [];
51
- for (const start of byDepth) {
52
- stack.push(start);
53
- while (stack.length > 0) {
54
- const i = stack.pop()!;
55
- if (state[i] === 0) {
56
- state[i] = 1;
57
- stack.push(i);
58
- behind[i]?.forEach((j) => state[j] === 0 && stack.push(j));
59
- } else if (state[i] === 1) {
60
- state[i] = 2;
61
- if (!hidden[i]) {
62
- ordered.push(elements[i]!);
63
- }
64
- }
28
+ for (let i = 0; i < drawn.pieces.length; i++) {
29
+ const p = drawn.pieces[i]!;
30
+ if (state[i] !== skip) {
31
+ ordered.push(state[i] === whole || !p.edges ? p.element : createPieceElement(p, point));
65
32
  }
66
33
  }
67
- return ordered;
34
+ return [...ordered, ...pointless.sort((a, b) => a.zOrder - b.zOrder)];
68
35
  }
69
36
 
70
37
  /** Distances below this, in the scene's units, are equal */
71
38
  const eps = 1e-3;
39
+ /** The planes tried at a node, spread over its pieces, of which the one splitting the fewest is taken */
40
+ const candidates = 8;
41
+ /** A drawn piece's state: drawn as it is, left out, or drawn as its whole element */
42
+ const [skip, whole] = [1, 2];
72
43
 
44
+ /** A plane oriented toward the viewer, at +z */
45
+ type Plane = { readonly normal: Vec3; readonly d: number };
46
+ /** An element, or a piece of it with whether the edge from each point to the next is the element's own, not a cut */
47
+ type Piece = {
48
+ readonly element: zOrderElement;
49
+ readonly points: ReadonlyArray<Vec3>;
50
+ readonly plane?: Plane;
51
+ readonly edges?: ReadonlyArray<boolean>;
52
+ };
73
53
  type Item = {
74
54
  readonly points: ReadonlyArray<Vec3>;
75
- readonly min: Vec3;
76
- readonly max: Vec3;
77
- /** The screen's normals of the polygon's edges, which separate it from polygons it doesn't overlap */
78
- readonly axes: ReadonlyArray<readonly [number, number]>;
79
- /** Its plane, oriented toward the viewer, unless it's a line or a point, or seen edge on */
80
- readonly plane?: { readonly normal: Vec3; readonly d: number };
55
+ readonly min: Vec2;
56
+ readonly max: Vec2;
81
57
  /** The winding on screen of an opaque convex polygon, 1 or -1, by which it can hide others; 0 for others */
82
58
  readonly occluder: number;
83
59
  };
60
+ /** Pieces in drawing order, their items, and of each split element the first and last of its pieces */
61
+ type Drawn = {
62
+ readonly pieces: Array<Piece>;
63
+ readonly items: Array<Item>;
64
+ readonly spans: Map<zOrderElement, { readonly first: number; readonly last: number }>;
65
+ };
84
66
 
85
- /** In one pass over the points: their bounds, edge normals on screen, plane by Newell's method, and whether they're a convex polygon */
86
- function createItem(points: ReadonlyArray<Vec3>, opaque: boolean): Item {
87
- const min = { x: Infinity, y: Infinity, z: Infinity };
88
- const max = { x: -Infinity, y: -Infinity, z: -Infinity };
89
- const normal = { x: 0, y: 0, z: 0 };
90
- // The smallest and largest turn at a corner on screen, the same sign throughout for a convex polygon
91
- const turns = { min: 0, max: 0 };
92
- const axes: Array<readonly [number, number]> = [];
93
- points.forEach((p, i) => {
94
- Object.assign(min, { x: Math.min(min.x, p.x), y: Math.min(min.y, p.y), z: Math.min(min.z, p.z) });
95
- Object.assign(max, { x: Math.max(max.x, p.x), y: Math.max(max.y, p.y), z: Math.max(max.z, p.z) });
96
- const q = points[(i + 1) % points.length]!;
97
- normal.x += (p.y - q.y) * (p.z + q.z);
98
- normal.y += (p.z - q.z) * (p.x + q.x);
99
- normal.z += (p.x - q.x) * (p.y + q.y);
100
- if (Math.abs(q.x - p.x) + Math.abs(q.y - p.y) > eps) {
101
- axes.push([q.y - p.y, p.x - q.x]);
67
+ /** The tree walked as it's built, with a stack instead of recursion, as it's as deep as a scene's planes can be many: back, then in, then front */
68
+ function draw(pieces: ReadonlyArray<Piece>, drawn: Drawn): void {
69
+ const stack: Array<{ readonly pieces: ReadonlyArray<Piece>; readonly inPlane?: boolean }> = [{ pieces }];
70
+ while (stack.length > 0) {
71
+ const task = stack.pop()!;
72
+ const plane = task.inPlane ? undefined : getSplitter(task.pieces);
73
+ if (plane) {
74
+ const [back, inPlane, front]: [Array<Piece>, Array<Piece>, Array<Piece>] = [[], [], []];
75
+ for (const p of task.pieces) {
76
+ partition(p, plane, back, inPlane, front);
77
+ }
78
+ stack.push({ pieces: front }, { pieces: inPlane, inPlane: true }, { pieces: back });
79
+ continue;
102
80
  }
103
- const o = points[(i + points.length - 1) % points.length]!;
104
- const turn = (p.x - o.x) * (q.y - p.y) - (p.y - o.y) * (q.x - p.x);
105
- Object.assign(turns, { min: Math.min(turns.min, turn), max: Math.max(turns.max, turn) });
106
- });
107
- const convex = points.length >= 3 && (turns.min >= -eps || turns.max <= eps);
108
- const occluder = opaque && convex ? Math.sign(normal.z) : 0;
109
- const length = Math.hypot(normal.x, normal.y, normal.z);
110
- if (points.length < 3 || length < eps || Math.abs(normal.z) < 1e-6 * length) {
111
- return { points, min, max, axes, occluder: 0 };
112
- }
113
- // Toward the viewer, at +z
114
- const s = Math.sign(normal.z) / length;
115
- const n = { x: normal.x * s, y: normal.y * s, z: normal.z * s };
116
- const p0 = points[0]!;
117
- return { points, min, max, axes, plane: { normal: n, d: n.x * p0.x + n.y * p0.y + n.z * p0.z }, occluder };
81
+ // In a plane, as given; else a leaf, whose pieces have no plane, by mean depth
82
+ for (const p of task.inPlane ? task.pieces : [...task.pieces].sort((a, b) => a.element.zOrder - b.element.zOrder)) {
83
+ if (p.edges) {
84
+ drawn.spans.set(p.element, { first: drawn.spans.get(p.element)?.first ?? drawn.pieces.length, last: drawn.pieces.length });
85
+ }
86
+ drawn.pieces.push(p);
87
+ drawn.items.push(createItem(p.points, !!p.element.opaque));
88
+ }
89
+ }
90
+ }
91
+
92
+ /** Of the candidates' planes, the one that the fewest pieces are on both sides of */
93
+ function getSplitter(pieces: ReadonlyArray<Piece>): Plane | undefined {
94
+ const step = Math.max(1, Math.floor(pieces.length / candidates));
95
+ let best: Plane | undefined = undefined;
96
+ let fewest = Infinity;
97
+ for (let i = 0; i < pieces.length && fewest > 0;) {
98
+ const plane = pieces[i]!.plane;
99
+ if (plane) {
100
+ let splits = 0;
101
+ for (const p of pieces) {
102
+ if (p.plane !== plane && getSide(p.points, plane) === 2 && ++splits >= fewest) {
103
+ break;
104
+ }
105
+ }
106
+ [best, fewest] = splits < fewest ? [plane, splits] : [best, fewest];
107
+ }
108
+ i += plane ? step : 1;
109
+ }
110
+ return best;
111
+ }
112
+
113
+ /** Behind, in or in front of the plane; split by it if on both sides and a plain polygon, else on the side of its center */
114
+ function partition(piece: Piece, plane: Plane, back: Array<Piece>, inPlane: Array<Piece>, front: Array<Piece>): void {
115
+ // An element's pieces share its plane, which they're in, though its points may be off it by more than eps
116
+ const side = piece.plane === plane ? 0 : getSide(piece.points, plane);
117
+ if (side === 2 && piece.element.style && piece.points.length >= 3) {
118
+ split(piece, plane, back, front);
119
+ } else if (side === 2) {
120
+ let sum = 0;
121
+ for (const p of piece.points) {
122
+ sum += getDistance(p, plane);
123
+ }
124
+ (sum < 0 ? back : front).push(piece);
125
+ } else {
126
+ (side === 0 ? inPlane : side < 0 ? back : front).push(piece);
127
+ }
118
128
  }
119
129
 
120
- /** -1 if `a` is drawn before `b`, 1 if after, 0 if they don't overlap or nothing decides */
121
- function getOrder(a: Item, b: Item): number {
122
- if (a.max.z <= b.min.z + eps) {
123
- return -1;
130
+ /** Sutherland–Hodgman against the plane, both ways; an edge of a piece along the cut isn't the element's own */
131
+ function split(piece: Piece, plane: Plane, back: Array<Piece>, front: Array<Piece>): void {
132
+ const { points, edges } = piece;
133
+ const [b, f] = [
134
+ { points: Array<Vec3>(), edges: Array<boolean>() },
135
+ { points: Array<Vec3>(), edges: Array<boolean>() },
136
+ ];
137
+ const first = getDistance(points[0]!, plane);
138
+ let dp = first;
139
+ for (let i = 0; i < points.length; i++) {
140
+ const [p, q] = [points[i]!, points[(i + 1) % points.length]!];
141
+ const dq = i + 1 < points.length ? getDistance(q, plane) : first;
142
+ const own = edges?.[i] ?? true;
143
+ const crosses = (dp < -eps && dq > eps) || (dp > eps && dq < -eps);
144
+ if (dp <= eps) {
145
+ b.points.push(p);
146
+ b.edges.push(own && (crosses || dq <= eps));
147
+ }
148
+ if (dp >= -eps) {
149
+ f.points.push(p);
150
+ f.edges.push(own && (crosses || dq >= -eps));
151
+ }
152
+ if (crosses) {
153
+ const t = dp / (dp - dq);
154
+ const r = { x: p.x + (q.x - p.x) * t, y: p.y + (q.y - p.y) * t, z: p.z + (q.z - p.z) * t };
155
+ b.points.push(r);
156
+ b.edges.push(own && dq < 0);
157
+ f.points.push(r);
158
+ f.edges.push(own && dq > 0);
159
+ }
160
+ dp = dq;
124
161
  }
125
- if (b.max.z <= a.min.z + eps) {
126
- return 1;
162
+ if (b.points.length >= 3) {
163
+ back.push({ ...piece, ...b });
127
164
  }
128
- if (!overlap(a, b)) {
129
- return 0;
165
+ if (f.points.length >= 3) {
166
+ front.push({ ...piece, ...f });
130
167
  }
131
- const aToB = b.plane ? getSide(a.points, b.plane) : 0;
132
- if (aToB !== 0) {
133
- return aToB;
168
+ }
169
+
170
+ function createPieceElement({ element, points, edges = [] }: Piece, point: (x: number, y: number) => Vec2): zOrderElement {
171
+ const { fill, opacity, stroke, strokeWidth } = element.style!;
172
+ const screen = points.map((p) => point(p.x, p.y));
173
+ const outline = strokeWidth > 0 && isOpaque(stroke, 1) && edges.includes(true) ? svgEdges(screen, edges, stroke, strokeWidth) : "";
174
+ return { ...element, element: svgPolygon(vec3Zero, screen, fill, opacity, stroke, 0) + outline, polygon: points };
175
+ }
176
+
177
+ /** By Newell's method, unless they're a line or a point, or seen edge on */
178
+ function getPlane(points: ReadonlyArray<Vec3>): Plane | undefined {
179
+ let [x, y, z] = [0, 0, 0];
180
+ for (let i = 0; i < points.length; i++) {
181
+ const [p, q] = [points[i]!, points[(i + 1) % points.length]!];
182
+ [x, y, z] = [x + (p.y - q.y) * (p.z + q.z), y + (p.z - q.z) * (p.x + q.x), z + (p.x - q.x) * (p.y + q.y)];
134
183
  }
135
- return a.plane ? -getSide(b.points, a.plane) : 0;
184
+ const length = Math.hypot(x, y, z);
185
+ if (points.length < 3 || length < eps || Math.abs(z) < 1e-6 * length) {
186
+ return undefined;
187
+ }
188
+ const s = Math.sign(z) / length;
189
+ const n = { x: x * s, y: y * s, z: z * s };
190
+ return { normal: n, d: n.x * points[0]!.x + n.y * points[0]!.y + n.z * points[0]!.z };
136
191
  }
137
192
 
138
- /** -1 if the points are all behind the plane, 1 if all in front of it, 0 if on both sides or in it */
139
- function getSide(points: ReadonlyArray<Vec3>, { normal: n, d }: NonNullable<Item["plane"]>): number {
140
- const range = { min: Infinity, max: -Infinity };
141
- points.forEach((p) => {
142
- const distance = n.x * p.x + n.y * p.y + n.z * p.z - d;
143
- Object.assign(range, { min: Math.min(range.min, distance), max: Math.max(range.max, distance) });
144
- });
145
- return range.max <= eps && range.min < -eps ? -1 : range.min >= -eps && range.max > eps ? 1 : 0;
193
+ const getDistance = (p: Vec3, { normal: n, d }: Plane): number => n.x * p.x + n.y * p.y + n.z * p.z - d;
194
+
195
+ /** -1 if the points are all behind the plane, 1 if all in front of it, 0 if all in it, 2 if on both sides */
196
+ function getSide(points: ReadonlyArray<Vec3>, plane: Plane): number {
197
+ let [min, max] = [Infinity, -Infinity];
198
+ for (const p of points) {
199
+ const distance = getDistance(p, plane);
200
+ min = Math.min(min, distance);
201
+ max = Math.max(max, distance);
202
+ }
203
+ return min >= -eps ? (max > eps ? 1 : 0) : max <= eps ? -1 : 2;
146
204
  }
147
205
 
148
- /** Whether the polygons overlap on screen by more than touching: no edge normal of either separates them */
149
- function overlap(a: Item, b: Item): boolean {
150
- return a.axes.every(([x, y]) => !separates(a, b, x, y)) && b.axes.every(([x, y]) => !separates(a, b, x, y));
206
+ /** Of items drawn back to front, `skip` for those inside an opaque convex polygon drawn after them; a sweep along x over their bounds on screen */
207
+ function getHidden(items: ReadonlyArray<Item>): Uint8Array {
208
+ const state = new Uint8Array(items.length);
209
+ const active: Array<number> = [];
210
+ for (const i of [...items.keys()].sort((a, b) => items[a]!.min.x - items[b]!.min.x)) {
211
+ const a = items[i]!;
212
+ // Those whose bounds end before this one's start are dropped in place
213
+ let kept = 0;
214
+ for (const j of active) {
215
+ const b = items[j]!;
216
+ if (b.max.x > a.min.x + eps) {
217
+ active[kept++] = j;
218
+ // Along x they overlap, by the sweep, which a vertical line's zero width wouldn't by bounds
219
+ const [back, front] = [Math.min(i, j), Math.max(i, j)];
220
+ state[back] ||= a.min.y < b.max.y - eps && b.min.y < a.max.y - eps && covers(items[front]!, items[back]!) ? skip : 0;
221
+ }
222
+ }
223
+ active.length = kept;
224
+ active.push(i);
225
+ }
226
+ return state;
151
227
  }
152
228
 
153
- function separates(a: Item, b: Item, x: number, y: number): boolean {
154
- const range = { aMin: Infinity, aMax: -Infinity, bMin: Infinity, bMax: -Infinity };
155
- a.points.forEach((p) => Object.assign(range, { aMin: Math.min(range.aMin, p.x * x + p.y * y), aMax: Math.max(range.aMax, p.x * x + p.y * y) }));
156
- b.points.forEach((p) => Object.assign(range, { bMin: Math.min(range.bMin, p.x * x + p.y * y), bMax: Math.max(range.bMax, p.x * x + p.y * y) }));
157
- const tolerance = eps * Math.hypot(x, y);
158
- return range.aMax <= range.bMin + tolerance || range.bMax <= range.aMin + tolerance;
229
+ /**
230
+ * A split element whose pieces have nothing else drawn between them that overlaps it on screen is drawn whole where its last piece is, its other
231
+ * pieces skipped, unless they're all hidden
232
+ */
233
+ function mergeSplit({ pieces, items, spans }: Drawn, state: Uint8Array): void {
234
+ for (const [element, { first, last }] of spans) {
235
+ const bounds = createItem(element.polygon!, false);
236
+ let [blocked, visible] = [false, false];
237
+ for (let k = first; k <= last && !blocked; k++) {
238
+ const own = pieces[k]!.element === element;
239
+ visible ||= own && state[k] !== skip;
240
+ blocked = !own && overlaps(items[k]!, bounds);
241
+ }
242
+ if (!blocked) {
243
+ for (let k = first; k <= last; k++) {
244
+ state[k] = pieces[k]!.element === element ? skip : state[k]!;
245
+ }
246
+ state[last] = visible ? whole : skip;
247
+ }
248
+ }
249
+ }
250
+
251
+ /** In one pass over the points: their bounds on screen, and whether they're a convex polygon */
252
+ function createItem(points: ReadonlyArray<Vec3>, opaque: boolean): Item {
253
+ const [min, max] = [
254
+ { x: Infinity, y: Infinity },
255
+ { x: -Infinity, y: -Infinity },
256
+ ];
257
+ // Twice the signed area, and the smallest and largest turn at a corner on screen, the same sign throughout for a convex polygon
258
+ let [area, minTurn, maxTurn] = [0, 0, 0];
259
+ for (let i = 0; i < points.length; i++) {
260
+ const o = points[(i + points.length - 1) % points.length]!;
261
+ const p = points[i]!;
262
+ const q = points[(i + 1) % points.length]!;
263
+ min.x = Math.min(min.x, p.x);
264
+ min.y = Math.min(min.y, p.y);
265
+ max.x = Math.max(max.x, p.x);
266
+ max.y = Math.max(max.y, p.y);
267
+ const turn = (p.x - o.x) * (q.y - p.y) - (p.y - o.y) * (q.x - p.x);
268
+ area += p.x * q.y - q.x * p.y;
269
+ minTurn = Math.min(minTurn, turn);
270
+ maxTurn = Math.max(maxTurn, turn);
271
+ }
272
+ const convex = points.length >= 3 && (minTurn >= -eps || maxTurn <= eps) && Math.abs(area) > eps;
273
+ return { points, min, max, occluder: opaque && convex ? Math.sign(area) : 0 };
159
274
  }
160
275
 
276
+ const overlaps = (a: Pick<Item, "min" | "max">, b: Pick<Item, "min" | "max">): boolean =>
277
+ a.min.x < b.max.x - eps && b.min.x < a.max.x - eps && a.min.y < b.max.y - eps && b.min.y < a.max.y - eps;
278
+
161
279
  /** Whether the front's opaque convex polygon contains all of the back's points on screen, so the back can't be seen; not for a single point, e.g. text */
162
- function covers(front: Item, back: Item): boolean {
163
- const { points, occluder } = front;
164
- return (
165
- occluder !== 0 &&
166
- back.points.length >= 2 &&
167
- points.every((p, i) => {
168
- const q = points[(i + 1) % points.length]!;
169
- return back.points.every((r) => occluder * ((q.x - p.x) * (r.y - p.y) - (q.y - p.y) * (r.x - p.x)) >= -eps * Math.hypot(q.x - p.x, q.y - p.y));
170
- })
171
- );
280
+ function covers({ points, occluder }: Item, back: Item): boolean {
281
+ if (occluder === 0 || back.points.length < 2) {
282
+ return false;
283
+ }
284
+ for (let i = 0; i < points.length; i++) {
285
+ const [p, q] = [points[i]!, points[(i + 1) % points.length]!];
286
+ const tolerance = -eps * Math.hypot(q.x - p.x, q.y - p.y);
287
+ for (const r of back.points) {
288
+ if (occluder * ((q.x - p.x) * (r.y - p.y) - (q.y - p.y) * (r.x - p.x)) < tolerance) {
289
+ return false;
290
+ }
291
+ }
292
+ }
293
+ return true;
172
294
  }