abstract-3d 4.0.4 → 4.0.7

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 (49) 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/lib/renderers/three/shadow/key-light.js +2 -2
  34. package/lib/renderers/three/shadow/key-light.js.map +1 -1
  35. package/lib/renderers/three/shadow/shadow.js +2 -2
  36. package/package.json +2 -2
  37. package/src/renderers/svg/svg-encoding.ts +13 -0
  38. package/src/renderers/svg/svg-geometries/shared.ts +13 -5
  39. package/src/renderers/svg/svg-geometries/svg-box.ts +10 -1
  40. package/src/renderers/svg/svg-geometries/svg-cone.ts +2 -1
  41. package/src/renderers/svg/svg-geometries/svg-cylinder.ts +3 -2
  42. package/src/renderers/svg/svg-geometries/svg-plane.ts +5 -2
  43. package/src/renderers/svg/svg-geometries/svg-polygon.ts +7 -6
  44. package/src/renderers/svg/svg-geometries/svg-shape.ts +4 -2
  45. package/src/renderers/svg/svg-order.test.ts +61 -6
  46. package/src/renderers/svg/svg-order.ts +257 -135
  47. package/src/renderers/svg/svg.ts +13 -8
  48. package/src/renderers/three/shadow/key-light.ts +2 -2
  49. package/src/renderers/three/shadow/shadow.ts +2 -2
@@ -2,15 +2,23 @@ import type { Vec2, Vec3, View } from "../../../abstract-3d.js";
2
2
  import { vec3TransRot, vec3Zero } from "../../../abstract-3d.js";
3
3
 
4
4
  /**
5
- * An element and its mean depth, and the points it covers in view space (x right, y up, z toward the viewer), by which it's ordered when given, and
6
- * whether it hides what's behind it
5
+ * An element and its mean depth, and the points it covers in view space (x right, y up, z toward the viewer), by which it's ordered when given,
6
+ * whether it hides what's behind it, and the style of a plain polygon, without holes, by which its pieces are drawn when it's split
7
7
  */
8
- export type zOrderElement = { readonly element: string; readonly zOrder: number; readonly polygon?: ReadonlyArray<Vec3>; readonly opaque?: boolean };
9
- export const zElem = (element: string, zOrder: number, polygon?: ReadonlyArray<Vec3>, opaque?: boolean): zOrderElement => ({
8
+ export type zOrderElement = {
9
+ readonly element: string;
10
+ readonly zOrder: number;
11
+ readonly polygon?: ReadonlyArray<Vec3>;
12
+ readonly opaque?: boolean;
13
+ readonly style?: PolygonStyle;
14
+ };
15
+ export type PolygonStyle = { readonly fill: string; readonly opacity: number; readonly stroke: string; readonly strokeWidth: number };
16
+ export const zElem = (element: string, zOrder: number, polygon?: ReadonlyArray<Vec3>, opaque?: boolean, style?: PolygonStyle): zOrderElement => ({
10
17
  element,
11
18
  zOrder,
12
19
  ...(polygon && { polygon }),
13
20
  ...(opaque && { opaque }),
21
+ ...(style && { style }),
14
22
  });
15
23
 
16
24
  /** Whether a fill hides what's behind it: fully opaque, and not transparent, e.g. a sketch's default background */
@@ -34,7 +42,7 @@ export const black = "rgb(15, 15, 15)";
34
42
 
35
43
  export type ImageDataUri = `data:image/png;base64,${string}` | `data:image/jpeg;base64,${string}` | `data:image/svg+xml,${string}`;
36
44
 
37
- /** A view, or an isometric one from the front's top left or top right, which shows the front, the top and that side, foreshortened alike */
45
+ /** A view, or a three-quarter one from the front's top left or top right, which shows the front foremost, the top and that side */
38
46
  export type SvgView = View | "TopLeft" | "TopRight";
39
47
 
40
48
  export type SvgOptions = {
@@ -34,7 +34,16 @@ export function box(
34
34
  }
35
35
  // Only the front and back have holes, which show what's behind them
36
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)];
37
+ const plain = !holes?.length;
38
+ return [
39
+ zElem(
40
+ svgPolygon(rot, points, fill, opacity, strokeColor, strokeUse, holes),
41
+ vec3ZMean(...polygon),
42
+ polygon,
43
+ opaque && plain,
44
+ plain ? { fill, opacity, stroke: strokeColor, strokeWidth: strokeUse } : undefined
45
+ ),
46
+ ];
38
47
  });
39
48
  }
40
49
 
@@ -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
  });