abstract-3d 4.0.0-beta.8 → 4.0.0

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 (96) hide show
  1. package/README.md +2 -0
  2. package/lib/abstract-3d.d.ts +6 -1
  3. package/lib/abstract-3d.d.ts.map +1 -1
  4. package/lib/abstract-3d.js +24 -2
  5. package/lib/abstract-3d.js.map +1 -1
  6. package/lib/renderers/react/react.d.ts.map +1 -1
  7. package/lib/renderers/react/react.js +9 -3
  8. package/lib/renderers/react/react.js.map +1 -1
  9. package/lib/renderers/svg/index.d.ts +1 -1
  10. package/lib/renderers/svg/index.d.ts.map +1 -1
  11. package/lib/renderers/svg/svg-geometries/shared.d.ts +14 -2
  12. package/lib/renderers/svg/svg-geometries/shared.d.ts.map +1 -1
  13. package/lib/renderers/svg/svg-geometries/shared.js +15 -1
  14. package/lib/renderers/svg/svg-geometries/shared.js.map +1 -1
  15. package/lib/renderers/svg/svg-geometries/svg-box.d.ts.map +1 -1
  16. package/lib/renderers/svg/svg-geometries/svg-box.js +41 -30
  17. package/lib/renderers/svg/svg-geometries/svg-box.js.map +1 -1
  18. package/lib/renderers/svg/svg-geometries/svg-cone.d.ts.map +1 -1
  19. package/lib/renderers/svg/svg-geometries/svg-cone.js +3 -3
  20. package/lib/renderers/svg/svg-geometries/svg-cone.js.map +1 -1
  21. package/lib/renderers/svg/svg-geometries/svg-cylinder.d.ts.map +1 -1
  22. package/lib/renderers/svg/svg-geometries/svg-cylinder.js +18 -6
  23. package/lib/renderers/svg/svg-geometries/svg-cylinder.js.map +1 -1
  24. package/lib/renderers/svg/svg-geometries/svg-image.d.ts.map +1 -1
  25. package/lib/renderers/svg/svg-geometries/svg-image.js +3 -2
  26. package/lib/renderers/svg/svg-geometries/svg-image.js.map +1 -1
  27. package/lib/renderers/svg/svg-geometries/svg-line.d.ts.map +1 -1
  28. package/lib/renderers/svg/svg-geometries/svg-line.js +3 -1
  29. package/lib/renderers/svg/svg-geometries/svg-line.js.map +1 -1
  30. package/lib/renderers/svg/svg-geometries/svg-plane.d.ts.map +1 -1
  31. package/lib/renderers/svg/svg-geometries/svg-plane.js +4 -2
  32. package/lib/renderers/svg/svg-geometries/svg-plane.js.map +1 -1
  33. package/lib/renderers/svg/svg-geometries/svg-polygon.d.ts.map +1 -1
  34. package/lib/renderers/svg/svg-geometries/svg-polygon.js +4 -2
  35. package/lib/renderers/svg/svg-geometries/svg-polygon.js.map +1 -1
  36. package/lib/renderers/svg/svg-geometries/svg-shape.d.ts.map +1 -1
  37. package/lib/renderers/svg/svg-geometries/svg-shape.js +4 -2
  38. package/lib/renderers/svg/svg-geometries/svg-shape.js.map +1 -1
  39. package/lib/renderers/svg/svg-geometries/svg-text.js +1 -1
  40. package/lib/renderers/svg/svg-geometries/svg-text.js.map +1 -1
  41. package/lib/renderers/svg/svg-order.d.ts +9 -0
  42. package/lib/renderers/svg/svg-order.d.ts.map +1 -0
  43. package/lib/renderers/svg/svg-order.js +148 -0
  44. package/lib/renderers/svg/svg-order.js.map +1 -0
  45. package/lib/renderers/svg/svg.d.ts.map +1 -1
  46. package/lib/renderers/svg/svg.js +13 -9
  47. package/lib/renderers/svg/svg.js.map +1 -1
  48. package/lib/renderers/three/build/build.d.ts +2 -0
  49. package/lib/renderers/three/build/build.d.ts.map +1 -1
  50. package/lib/renderers/three/build/build.js +1 -1
  51. package/lib/renderers/three/build/build.js.map +1 -1
  52. package/lib/renderers/three/build/materials.d.ts +5 -1
  53. package/lib/renderers/three/build/materials.d.ts.map +1 -1
  54. package/lib/renderers/three/build/materials.js +53 -17
  55. package/lib/renderers/three/build/materials.js.map +1 -1
  56. package/lib/renderers/three/environment/studio.d.ts +3 -2
  57. package/lib/renderers/three/environment/studio.d.ts.map +1 -1
  58. package/lib/renderers/three/environment/studio.js +4 -3
  59. package/lib/renderers/three/environment/studio.js.map +1 -1
  60. package/lib/renderers/three/geometry/geometry.d.ts.map +1 -1
  61. package/lib/renderers/three/geometry/geometry.js +11 -5
  62. package/lib/renderers/three/geometry/geometry.js.map +1 -1
  63. package/lib/renderers/three/shadow/key-light.js +1 -1
  64. package/lib/renderers/three/viewer/options.d.ts +7 -1
  65. package/lib/renderers/three/viewer/options.d.ts.map +1 -1
  66. package/lib/renderers/three/viewer/options.js +1 -0
  67. package/lib/renderers/three/viewer/options.js.map +1 -1
  68. package/lib/renderers/three/viewer/viewer.d.ts +5 -0
  69. package/lib/renderers/three/viewer/viewer.d.ts.map +1 -1
  70. package/lib/renderers/three/viewer/viewer.js +54 -11
  71. package/lib/renderers/three/viewer/viewer.js.map +1 -1
  72. package/package.json +1 -1
  73. package/src/abstract-3d.ts +30 -2
  74. package/src/renderers/react/react.tsx +14 -4
  75. package/src/renderers/svg/index.ts +1 -1
  76. package/src/renderers/svg/svg-geometries/shared.ts +26 -3
  77. package/src/renderers/svg/svg-geometries/svg-box.ts +42 -36
  78. package/src/renderers/svg/svg-geometries/svg-cone.ts +17 -3
  79. package/src/renderers/svg/svg-geometries/svg-cylinder.ts +25 -6
  80. package/src/renderers/svg/svg-geometries/svg-image.ts +3 -2
  81. package/src/renderers/svg/svg-geometries/svg-line.ts +3 -1
  82. package/src/renderers/svg/svg-geometries/svg-plane.ts +9 -2
  83. package/src/renderers/svg/svg-geometries/svg-polygon.ts +9 -2
  84. package/src/renderers/svg/svg-geometries/svg-shape.ts +9 -2
  85. package/src/renderers/svg/svg-geometries/svg-text.ts +1 -1
  86. package/src/renderers/svg/svg-order.test.ts +35 -0
  87. package/src/renderers/svg/svg-order.ts +172 -0
  88. package/src/renderers/svg/svg.ts +13 -9
  89. package/src/renderers/three/build/build.ts +3 -1
  90. package/src/renderers/three/build/materials.ts +57 -17
  91. package/src/renderers/three/environment/studio.ts +4 -3
  92. package/src/renderers/three/geometry/geometry.test.ts +17 -5
  93. package/src/renderers/three/geometry/geometry.ts +12 -5
  94. package/src/renderers/three/shadow/key-light.ts +1 -1
  95. package/src/renderers/three/viewer/options.ts +8 -1
  96. package/src/renderers/three/viewer/viewer.ts +57 -10
@@ -3,7 +3,7 @@ import { vec3Scale, vec3TransRot, vec3RotCombine, vec3Zero, vec3, vec3ZMean } fr
3
3
  import { rgbGrayScale } from "../../../utils.js";
4
4
  import { svgPolygon } from "../svg-encoding.js";
5
5
  import type { zOrderElement, SvgOptions } from "./shared.js";
6
- import { gray, black, zElem } from "./shared.js";
6
+ import { gray, black, zElem, getArea, isOpaque } from "./shared.js";
7
7
 
8
8
  export function box(
9
9
  b: Box,
@@ -16,46 +16,52 @@ export function box(
16
16
  const half = vec3Scale(b.size, 0.5);
17
17
  const pos = vec3TransRot(b.pos, parentPos, parentRot);
18
18
  const rot = vec3RotCombine(parentRot, b.rot ?? vec3Zero);
19
- const vec3tr = (p: Vec3): Vec3 => vec3TransRot(p, pos, rot);
20
-
21
- const v1 = vec3tr(vec3(-half.x, -half.y, half.z));
22
- const v2 = vec3tr(vec3(half.x, -half.y, half.z));
23
- const v3 = vec3tr(vec3(half.x, half.y, half.z));
24
- const v4 = vec3tr(vec3(-half.x, half.y, half.z));
25
- const v5 = vec3tr(vec3(-half.x, -half.y, -half.z));
26
- const v6 = vec3tr(vec3(half.x, -half.y, -half.z));
27
- const v7 = vec3tr(vec3(half.x, half.y, -half.z));
28
- const v8 = vec3tr(vec3(-half.x, half.y, -half.z));
29
-
30
- const frontMean = vec3ZMean(v1, v2, v3, v4);
31
- const backMean = vec3ZMean(v5, v6, v7, v8);
32
- const [frontBackPoints, frontBackMean] =
33
- frontMean > backMean
34
- ? [[point(v1.x, v1.y), point(v2.x, v2.y), point(v3.x, v3.y), point(v4.x, v4.y)], frontMean]
35
- : [[point(v5.x, v5.y), point(v6.x, v6.y), point(v7.x, v7.y), point(v8.x, v8.y)], backMean];
36
- const topMean = vec3ZMean(v8, v7, v3, v4);
37
- const botMean = vec3ZMean(v5, v6, v2, v1);
38
- const [topBotPoints, topBotMean] =
39
- topMean > botMean
40
- ? [[point(v8.x, v8.y), point(v7.x, v7.y), point(v3.x, v3.y), point(v4.x, v4.y)], topMean]
41
- : [[point(v5.x, v5.y), point(v6.x, v6.y), point(v2.x, v2.y), point(v1.x, v1.y)], botMean];
42
-
43
- const rightMean = vec3ZMean(v6, v2, v3, v7);
44
- const leftMean = vec3ZMean(v5, v1, v4, v8);
45
- const [rightLeftPoints, rightLeftMean] =
46
- rightMean > leftMean
47
- ? [[point(v6.x, v6.y), point(v2.x, v2.y), point(v3.x, v3.y), point(v7.x, v7.y)], rightMean]
48
- : [[point(v5.x, v5.y), point(v1.x, v1.y), point(v4.x, v4.y), point(v8.x, v8.y)], leftMean];
19
+ const corners = unitCorners.map((c) => vec3TransRot(vec3(c.x * half.x, c.y * half.y, c.z * half.z), pos, rot));
49
20
 
50
21
  const color = material.normal;
51
22
  const opacity = material.opacity ?? 1.0;
52
23
  const [strokeColor, fill, strokeUse] = opts.only_stroke
53
24
  ? [opts.gray_scale ? gray : color, opts.background, opts.stroke_thickness]
54
25
  : [black, opts.gray_scale ? rgbGrayScale(color) : color, 0];
26
+ const opaque = isOpaque(fill, opacity);
55
27
 
56
- return [
57
- zElem(svgPolygon(rot, frontBackPoints, fill, opacity, strokeColor, strokeUse, b.holes, parentPos), frontBackMean),
58
- zElem(svgPolygon(rot, topBotPoints, fill, opacity, strokeColor, strokeUse), topBotMean),
59
- zElem(svgPolygon(rot, rightLeftPoints, fill, opacity, strokeColor, strokeUse), rightLeftMean),
60
- ];
28
+ return faces.flatMap(([one, other], i) => {
29
+ const polygon = (one.reduce((z, c) => z + corners[c]!.z, 0) > other.reduce((z, c) => z + corners[c]!.z, 0) ? one : other).map((c) => corners[c]!);
30
+ const points = polygon.map((c) => point(c.x, c.y));
31
+ // Seen edge on, a face is only a line along the edges of the box's other faces
32
+ if (Math.abs(getArea(points)) < minArea) {
33
+ return [];
34
+ }
35
+ // Only the front and back have holes, which show what's behind them
36
+ const holes = i === 0 ? b.holes : undefined;
37
+ return [zElem(svgPolygon(rot, points, fill, opacity, strokeColor, strokeUse, holes), vec3ZMean(...polygon), polygon, opaque && !holes?.length)];
38
+ });
61
39
  }
40
+
41
+ const unitCorners = [
42
+ vec3(-1, -1, 1),
43
+ vec3(1, -1, 1),
44
+ vec3(1, 1, 1),
45
+ vec3(-1, 1, 1),
46
+ vec3(-1, -1, -1),
47
+ vec3(1, -1, -1),
48
+ vec3(1, 1, -1),
49
+ vec3(-1, 1, -1),
50
+ ];
51
+ /** Pairs of opposite faces by their corners, front and back, top and bottom, right and left, of which the one toward the viewer is drawn */
52
+ const faces: ReadonlyArray<readonly [ReadonlyArray<number>, ReadonlyArray<number>]> = [
53
+ [
54
+ [0, 1, 2, 3],
55
+ [4, 5, 6, 7],
56
+ ],
57
+ [
58
+ [7, 6, 2, 3],
59
+ [4, 5, 1, 0],
60
+ ],
61
+ [
62
+ [5, 1, 2, 6],
63
+ [4, 0, 3, 7],
64
+ ],
65
+ ];
66
+ /** Of a face on screen, in the scene's units squared, below which it's seen edge on */
67
+ const minArea = 1e-6;
@@ -3,7 +3,7 @@ import { vec3, vec3TransRot, vec3RotCombine, vec3Zero, vec3ZMean, equals } from
3
3
  import { rgbGrayScale } from "../../../utils.js";
4
4
  import { svgCircle, svgPolygon } from "../svg-encoding.js";
5
5
  import type { zOrderElement, SvgOptions } from "./shared.js";
6
- import { gray, stBW, zElem, transparent } from "./shared.js";
6
+ import { gray, isOpaque, stBW, zElem, transparent } from "./shared.js";
7
7
 
8
8
  export function cone(
9
9
  c: Cone,
@@ -37,7 +37,14 @@ export function cone(
37
37
  if (i !== 0) {
38
38
  const prevBot = botVec3Array[i - 1]!;
39
39
  const points = [point(currBot.x, currBot.y), point(prevBot.x, prevBot.y), point(topPos.x, topPos.y), point(topPos.x, topPos.y)];
40
- zOrderComponents.push(zElem(svgPolygon(rot, points, fill, opacity, stroke, stBW), vec3ZMean(currBot, prevBot, topPos)));
40
+ zOrderComponents.push(
41
+ zElem(
42
+ svgPolygon(rot, points, fill, opacity, stroke, stBW),
43
+ vec3ZMean(currBot, prevBot, topPos),
44
+ [currBot, prevBot, topPos],
45
+ isOpaque(fill, opacity)
46
+ )
47
+ );
41
48
  }
42
49
  currentAngle += angleStep;
43
50
  }
@@ -46,7 +53,14 @@ export function cone(
46
53
  const cylTop = vec3tr(vec3(0, half.y, 0));
47
54
  const cylBottom = vec3tr(vec3(0, -half.y, 0));
48
55
  if (equals(cylTop.x, cylBottom.x, 0.1) && equals(cylTop.y, cylBottom.y, 0.1)) {
49
- zOrderComponents.push(zElem(svgCircle(c.radius, rot, point(cylBottom.x, cylBottom.y), fill, opacity, stroke, stBW), cylBottom.z));
56
+ zOrderComponents.push(
57
+ zElem(
58
+ svgCircle(c.radius, rot, point(cylBottom.x, cylBottom.y), fill, opacity, stroke, stBW),
59
+ cylBottom.z,
60
+ botVec3Array,
61
+ isOpaque(fill, opacity)
62
+ )
63
+ );
50
64
  }
51
65
 
52
66
  return zOrderComponents;
@@ -3,7 +3,7 @@ import { vec3, vec3TransRot, vec3RotCombine, vec3Zero, vec3ZMean, equals } from
3
3
  import { rgbGrayScale } from "../../../utils.js";
4
4
  import { svgCircle, svgPolygon } from "../svg-encoding.js";
5
5
  import type { SvgOptions, zOrderElement } from "./shared.js";
6
- import { gray, stBW, transparent, zElem } from "./shared.js";
6
+ import { getArea, gray, isOpaque, stBW, transparent, zElem } from "./shared.js";
7
7
 
8
8
  export function cylinder(
9
9
  c: Cylinder,
@@ -24,6 +24,9 @@ export function cylinder(
24
24
  const [stroke, fill] = opts.only_stroke
25
25
  ? [opts.gray_scale ? gray : color, opts.background]
26
26
  : [transparent, opts.gray_scale ? rgbGrayScale(color) : color];
27
+ const opaque = isOpaque(fill, opacity);
28
+ // A closed opaque cylinder's facets turned away are hidden by those toward the viewer and its near end
29
+ const cullBack = opaque && !c.open;
27
30
  const zOrderComponents = Array<zOrderElement>();
28
31
 
29
32
  const sides = 8;
@@ -39,23 +42,39 @@ export function cylinder(
39
42
  const currTop = vec3tr(vec3(sin * radiusEnd, half, cos * radiusEnd));
40
43
  botVec3Array.push(currBot);
41
44
  topVec3Array.push(currTop);
42
- if (i !== 0) {
45
+ const mid = currentAngle - angleStep / 2;
46
+ if (i !== 0 && !(cullBack && vec3TransRot(vec3(Math.sin(mid), 0, Math.cos(mid)), vec3Zero, rot).z < -1e-6)) {
43
47
  const prevBot = botVec3Array[i - 1]!;
44
48
  const prevTop = topVec3Array[i - 1]!;
45
49
 
46
50
  const points = [point(currBot.x, currBot.y), point(prevBot.x, prevBot.y), point(prevTop.x, prevTop.y), point(currTop.x, currTop.y)];
47
- zOrderComponents.push(zElem(svgPolygon(rot, points, fill, opacity, stroke, stBW), vec3ZMean(currBot, prevBot, currTop, prevTop)));
51
+ const polygon = [currBot, prevBot, prevTop, currTop];
52
+ zOrderComponents.push(zElem(svgPolygon(rot, points, fill, opacity, stroke, stBW), vec3ZMean(...polygon), polygon, opaque));
48
53
  }
49
54
  currentAngle += angleStep;
50
55
  }
51
56
  if (!c.open) {
52
- // Add circle if direcly facing camera
57
+ // The near end: a circle when directly facing the camera, else seen at an angle, its ring of facet corners
53
58
  const circleTop = vec3tr(vec3(0, half, 0));
54
59
  const circleBottom = vec3tr(vec3(0, -half, 0));
60
+ const [circlePos, circleRadius, ring] =
61
+ circleTop.z > circleBottom.z ? [circleTop, radiusEnd, topVec3Array] : [circleBottom, c.radius, botVec3Array];
55
62
  if (equals(circleTop.x, circleBottom.x, 0.1) && equals(circleTop.y, circleBottom.y, 0.1)) {
56
- const [circlePos, circleRadius] = circleTop.z > circleBottom.z ? [circleTop, radiusEnd] : [circleBottom, c.radius];
57
63
  const holes = radiusEnd === c.radius ? c.holes : undefined;
58
- zOrderComponents.push(zElem(svgCircle(circleRadius, rot, point(circlePos.x, circlePos.y), fill, opacity, stroke, stBW, holes), circlePos.z));
64
+ zOrderComponents.push(
65
+ zElem(
66
+ svgCircle(circleRadius, rot, point(circlePos.x, circlePos.y), fill, opacity, stroke, stBW, holes),
67
+ circlePos.z,
68
+ ring,
69
+ opaque && !holes?.length
70
+ )
71
+ );
72
+ } else {
73
+ const points = ring.map((p) => point(p.x, p.y));
74
+ // Not seen edge on, when it's only a line along the facets' edges
75
+ 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
+ }
59
78
  }
60
79
  }
61
80
  return zOrderComponents;
@@ -21,6 +21,7 @@ export function image(
21
21
 
22
22
  const v2 = vec3tr(half.x, -half.y);
23
23
  const v4 = vec3tr(-half.x, half.y);
24
+ const corners = [vec3tr(-half.x, -half.y), v2, vec3tr(half.x, half.y), v4];
24
25
 
25
26
  switch (i.image.type) {
26
27
  case "AbstractImage": {
@@ -30,7 +31,7 @@ export function image(
30
31
  };
31
32
  const svg = createSVG(i.image.image, opts);
32
33
  const img = svgImage(point(v4.x, v4.y), i.size, rot, { type: "svg", svg }, opts.imageBg ? opts.background : undefined, scale);
33
- return [zElem(img, (v2.z + v4.z) / 2)];
34
+ return [zElem(img, (v2.z + v4.z) / 2, corners)];
34
35
  }
35
36
  case "Url": {
36
37
  const imageData = opts.imageDataByUrl[i.image.url];
@@ -45,7 +46,7 @@ export function image(
45
46
 
46
47
  if (image) {
47
48
  const img = svgImage(point(v4.x, v4.y), i.size, rot, image, opts.imageBg ? opts.background : undefined);
48
- return [zElem(img, (v2.z + v4.z) / 2)];
49
+ return [zElem(img, (v2.z + v4.z) / 2, corners)];
49
50
  }
50
51
  return [];
51
52
  }
@@ -15,5 +15,7 @@ export function line(
15
15
  ): ReadonlyArray<zOrderElement> {
16
16
  const v1 = vec3TransRot(l.start, parentPos, parentRot);
17
17
  const v2 = vec3TransRot(l.end, parentPos, parentRot);
18
- return [zElem(svgLine(point(v1.x, v1.y), point(v2.x, v2.y), opts.gray_scale ? rgbGrayScale(fill) : fill, l.thickness), vec3ZMean(v1, v2))];
18
+ return [
19
+ zElem(svgLine(point(v1.x, v1.y), point(v2.x, v2.y), opts.gray_scale ? rgbGrayScale(fill) : fill, l.thickness), vec3ZMean(v1, v2), [v1, v2]),
20
+ ];
19
21
  }
@@ -3,7 +3,7 @@ import { vec2Scale, vec3TransRot, vec3RotCombine, vec3Zero, vec3, vec3ZMean } fr
3
3
  import { rgbGrayScale } from "../../../utils.js";
4
4
  import { svgPolygon } from "../svg-encoding.js";
5
5
  import type { zOrderElement, SvgOptions } from "./shared.js";
6
- import { gray, black, zElem } from "./shared.js";
6
+ import { gray, black, isOpaque, zElem } from "./shared.js";
7
7
 
8
8
  export function plane(
9
9
  p: Plane,
@@ -29,5 +29,12 @@ 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
- return [zElem(svgPolygon(rot, points, fill, material.opacity ?? 1.0, strokeColor, strokeThickness, holes), vec3ZMean(v1, v2, v3, v4))];
32
+ return [
33
+ zElem(
34
+ svgPolygon(rot, points, fill, material.opacity ?? 1.0, strokeColor, strokeThickness, holes),
35
+ vec3ZMean(v1, v2, v3, v4),
36
+ [v1, v2, v3, v4],
37
+ isOpaque(fill, material.opacity ?? 1.0) && !holes?.length
38
+ ),
39
+ ];
33
40
  }
@@ -3,7 +3,7 @@ import { vec3ZMean, vec3TransRot, vec3RotCombine, vec3Zero } from "../../../abst
3
3
  import { rgbGrayScale } from "../../../utils.js";
4
4
  import { svgPolygon } from "../svg-encoding.js";
5
5
  import type { zOrderElement, SvgOptions } from "./shared.js";
6
- import { gray, zElem, transparent } from "./shared.js";
6
+ import { gray, isOpaque, zElem, transparent } from "./shared.js";
7
7
 
8
8
  export function polygon(
9
9
  p: Polygon,
@@ -21,5 +21,12 @@ 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
- return [zElem(svgPolygon(rot, points, fill, material.opacity ?? 1.0, strokeColor, strokeThickness), vec3ZMean(...rotatedPoints))];
24
+ 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
+ ),
31
+ ];
25
32
  }
@@ -3,7 +3,7 @@ import { vec3ZMean, vec3TransRot, vec3RotCombine, vec3, vec3Zero } from "../../.
3
3
  import { rgbGrayScale } from "../../../utils.js";
4
4
  import { svgPolygon } from "../svg-encoding.js";
5
5
  import type { zOrderElement, SvgOptions } from "./shared.js";
6
- import { gray, zElem, transparent } from "./shared.js";
6
+ import { gray, isOpaque, zElem, transparent } from "./shared.js";
7
7
 
8
8
  export function shape(
9
9
  s: Shape,
@@ -21,5 +21,12 @@ 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
- return [zElem(svgPolygon(rot, points, fill, material.opacity ?? 1.0, strokeColor, 0), vec3ZMean(...rotatedPoints))];
24
+ return [
25
+ zElem(
26
+ svgPolygon(rot, points, fill, material.opacity ?? 1.0, strokeColor, 0),
27
+ vec3ZMean(...rotatedPoints),
28
+ rotatedPoints,
29
+ isOpaque(fill, material.opacity ?? 1.0) && !s.holes?.length
30
+ ),
31
+ ];
25
32
  }
@@ -32,7 +32,7 @@ export function text(
32
32
  let posY = strings.length === 1 ? 0 : (fontSize * strings.length - fontSize) / 2;
33
33
  for (const s of strings) {
34
34
  const matrix = svgTrsMatrix(point(pos.x, pos.y + posY), rot);
35
- texts.push(zElem(svgText(s, matrix, fill, opts.font, fontSize), pos.z));
35
+ texts.push(zElem(svgText(s, matrix, fill, opts.font, fontSize), pos.z, [pos]));
36
36
  posY -= fontSize * 1.2;
37
37
  }
38
38
 
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { Vec3 } from "../../abstract-3d.js";
3
+ import { vec3 } from "../../abstract-3d.js";
4
+ import { zElem } from "./svg-geometries/shared.js";
5
+ import { orderElements } from "./svg-order.js";
6
+
7
+ const quad = (element: string, x0: number, x1: number, z: (x: number) => number): ReturnType<typeof zElem> => {
8
+ 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);
10
+ };
11
+ const order = (...elements: ReadonlyArray<ReturnType<typeof zElem>>): ReadonlyArray<string> => orderElements(elements).map((e) => e.element);
12
+
13
+ describe("orderElements", () => {
14
+ it("draws a part in front of a long tilted face after it, though the face's mean depth is nearer", () => {
15
+ // The face runs from z -10 at x -10 to z 10 at x 10, mean 0; the part at z -5 is in front of it where they overlap, at x -9 to -8
16
+ const face = quad("face", -10, 10, (x) => x);
17
+ const part = quad("part", -9, -8, () => -5);
18
+ expect(order(part, face)).toEqual(["face", "part"]);
19
+ });
20
+
21
+ it("keeps the mean depth order of elements that don't overlap on screen, or have no points", () => {
22
+ expect(
23
+ order(
24
+ quad("near", 0, 1, () => 1),
25
+ quad("far", 5, 6, () => -1)
26
+ )
27
+ ).toEqual(["far", "near"]);
28
+ expect(order(zElem("text", 2), zElem("line", -2))).toEqual(["line", "text"]);
29
+ });
30
+
31
+ it("draws every element once, also when they overlap in a cycle", () => {
32
+ const elements = [quad("a", 0, 2, (x) => x), quad("b", 1, 3, (x) => 2 - x), quad("c", -1, 4, () => 1), zElem("text", 0)];
33
+ expect([...order(...elements)].sort()).toEqual(["a", "b", "c", "text"]);
34
+ });
35
+ });
@@ -0,0 +1,172 @@
1
+ import type { Vec3 } from "../../abstract-3d.js";
2
+ import type { zOrderElement } from "./svg-geometries/shared.js";
3
+
4
+ /**
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
9
+ */
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);
19
+ }
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
+ }
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> = [];
50
+ 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
+ }
65
+ }
66
+ }
67
+ return ordered;
68
+ }
69
+
70
+ /** Distances below this, in the scene's units, are equal */
71
+ const eps = 1e-3;
72
+
73
+ type Item = {
74
+ 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 };
81
+ /** The winding on screen of an opaque convex polygon, 1 or -1, by which it can hide others; 0 for others */
82
+ readonly occluder: number;
83
+ };
84
+
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]);
102
+ }
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 };
118
+ }
119
+
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;
124
+ }
125
+ if (b.max.z <= a.min.z + eps) {
126
+ return 1;
127
+ }
128
+ if (!overlap(a, b)) {
129
+ return 0;
130
+ }
131
+ const aToB = b.plane ? getSide(a.points, b.plane) : 0;
132
+ if (aToB !== 0) {
133
+ return aToB;
134
+ }
135
+ return a.plane ? -getSide(b.points, a.plane) : 0;
136
+ }
137
+
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;
146
+ }
147
+
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));
151
+ }
152
+
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;
159
+ }
160
+
161
+ /** 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
+ );
172
+ }
@@ -29,6 +29,7 @@ import { plane } from "./svg-geometries/svg-plane.js";
29
29
  import { polygon } from "./svg-geometries/svg-polygon.js";
30
30
  import { shape } from "./svg-geometries/svg-shape.js";
31
31
  import { text } from "./svg-geometries/svg-text.js";
32
+ import { orderElements } from "./svg-order.js";
32
33
 
33
34
  export type SvgScene = {
34
35
  readonly scene: Scene;
@@ -89,26 +90,29 @@ function renderInternal(
89
90
  rotation: options?.rotation ?? 0,
90
91
  imageBg: options?.imageBg ?? false,
91
92
  };
92
- const cameraRot = rotationForCameraPos(opts.view);
93
+ // Isometric: the front turned 45° to show the side, and tilted by arctan(1/√2) to show the top, so the three axes are foreshortened alike
94
+ const isometric = opts.view === "TopLeft" || opts.view === "TopRight";
95
+ const view = isometric ? "front" : opts.view;
96
+ const isometricRot = isometric
97
+ ? vec3RotCombine(vec3(Math.atan(1 / Math.SQRT2), 0, 0), vec3(0, ((opts.view === "TopRight" ? -1 : 1) * Math.PI) / 4, 0))
98
+ : vec3Zero;
99
+ const cameraRot = vec3RotCombine(isometricRot, rotationForCameraPos(view));
93
100
  const viewRot = opts.rotation ? vec3RotCombine(vec3(0, 0, (opts.rotation * Math.PI) / 180), cameraRot) : cameraRot;
94
101
  const unitRot = vec3RotCombine(viewRot, scene.rotation_deprecated ?? vec3Zero);
95
102
 
96
- const unitCenter = vec3Rot(scene.center_deprecated ?? vec3Zero, vec3Zero, rotationForCameraPos(opts.view));
103
+ const unitCenter = vec3Rot(scene.center_deprecated ?? vec3Zero, vec3Zero, cameraRot);
97
104
  const [unitSize] = sizeBoundsForCameraPos(scene.size_deprecated, unitCenter, unitRot);
98
105
  const svgSize = vec2(unitSize.x + 1.5 * opts.stroke_thickness, unitSize.y + 1.5 * opts.stroke_thickness);
99
106
  const svgCenter = vec2(offset.x + opts.stroke_thickness * 0.75, offset.y + opts.stroke_thickness * 0.75);
100
107
  const point = (x: number, y: number): Vec2 => vec2(svgCenter.x + x, svgCenter.y - y);
101
108
  const unitCenterFlipped = vec3Flip(unitCenter);
102
109
 
103
- const elements = Array<zOrderElement>();
104
- for (const g of scene.groups) {
105
- elements.push(...svgGroup(g, unitCenterFlipped, unitRot, point, opts));
106
- }
110
+ const elements = [...orderElements(scene.groups.flatMap((g) => svgGroup(g, unitCenterFlipped, unitRot, point, opts)))];
107
111
  const dimOpts: SvgOptions = { ...opts, only_stroke: false, gray_scale: false };
108
- elements.sort((a, b) => a.zOrder - b.zOrder);
109
- const visibleViews = calculateVisibleViews(opts.view, scene.rotation_deprecated);
112
+ const visibleViews = calculateVisibleViews(view, scene.rotation_deprecated);
110
113
 
111
- for (const dim of scene.dimensions_deprecated?.dimensions ?? []) {
114
+ // Dimensions are drawn for the flat views
115
+ for (const dim of isometric ? [] : (scene.dimensions_deprecated?.dimensions ?? [])) {
112
116
  const d = dimensionConvertToTypeMesh(dim, scene.rotation_deprecated ?? vec3Zero, scene.dimensions_deprecated?.material);
113
117
  if (isViewVisible(d.views[0], visibleViews)) {
114
118
  const pos = vec3TransRot(d.pos, unitCenterFlipped, unitRot);
@@ -45,6 +45,8 @@ export type BuildOptions = {
45
45
  readonly materialStateImages?: Record<string, string>;
46
46
  readonly useAlphaTest?: boolean;
47
47
  readonly onTextureLoaded?: () => void;
48
+ /** Lit by the studio, whose reflections metals show; true by default */
49
+ readonly studio?: boolean;
48
50
  };
49
51
 
50
52
  export type BuiltScene = {
@@ -233,7 +235,7 @@ function createPart(
233
235
  });
234
236
  if (first.role === "solid") {
235
237
  const c = getMaterialClass(first, targetStates[first.target]!);
236
- const part = createSolidPart(entries, createClassMaterial(c));
238
+ const part = createSolidPart(entries, createClassMaterial(c, options.studio !== false));
237
239
  Object.assign(part.object, { castShadow: c.castShadow, receiveShadow: c.lit });
238
240
  return part;
239
241
  }