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.
- package/lib/renderers/svg/svg-encoding.d.ts +2 -0
- package/lib/renderers/svg/svg-encoding.d.ts.map +1 -1
- package/lib/renderers/svg/svg-encoding.js +12 -0
- package/lib/renderers/svg/svg-encoding.js.map +1 -1
- package/lib/renderers/svg/svg-geometries/shared.d.ts +11 -4
- package/lib/renderers/svg/svg-geometries/shared.d.ts.map +1 -1
- package/lib/renderers/svg/svg-geometries/shared.js +2 -1
- package/lib/renderers/svg/svg-geometries/shared.js.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-box.d.ts.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-box.js +4 -1
- package/lib/renderers/svg/svg-geometries/svg-box.js.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-cone.d.ts.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-cone.js +1 -1
- package/lib/renderers/svg/svg-geometries/svg-cone.js.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-cylinder.d.ts.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-cylinder.js +3 -2
- package/lib/renderers/svg/svg-geometries/svg-cylinder.js.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-plane.d.ts.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-plane.js +3 -1
- package/lib/renderers/svg/svg-geometries/svg-plane.js.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-polygon.d.ts.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-polygon.js +7 -1
- package/lib/renderers/svg/svg-geometries/svg-polygon.js.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-shape.d.ts.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-shape.js +2 -1
- package/lib/renderers/svg/svg-geometries/svg-shape.js.map +1 -1
- package/lib/renderers/svg/svg-order.d.ts +6 -5
- package/lib/renderers/svg/svg-order.d.ts.map +1 -1
- package/lib/renderers/svg/svg-order.js +233 -124
- package/lib/renderers/svg/svg-order.js.map +1 -1
- package/lib/renderers/svg/svg.js +10 -8
- package/lib/renderers/svg/svg.js.map +1 -1
- package/lib/renderers/three/shadow/key-light.js +2 -2
- package/lib/renderers/three/shadow/key-light.js.map +1 -1
- package/lib/renderers/three/shadow/shadow.js +2 -2
- package/package.json +2 -2
- package/src/renderers/svg/svg-encoding.ts +13 -0
- package/src/renderers/svg/svg-geometries/shared.ts +13 -5
- package/src/renderers/svg/svg-geometries/svg-box.ts +10 -1
- package/src/renderers/svg/svg-geometries/svg-cone.ts +2 -1
- package/src/renderers/svg/svg-geometries/svg-cylinder.ts +3 -2
- package/src/renderers/svg/svg-geometries/svg-plane.ts +5 -2
- package/src/renderers/svg/svg-geometries/svg-polygon.ts +7 -6
- package/src/renderers/svg/svg-geometries/svg-shape.ts +4 -2
- package/src/renderers/svg/svg-order.test.ts +61 -6
- package/src/renderers/svg/svg-order.ts +257 -135
- package/src/renderers/svg/svg.ts +13 -8
- package/src/renderers/three/shadow/key-light.ts +2 -2
- 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,
|
|
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 = {
|
|
9
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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,
|
|
27
|
+
svgPolygon(rot, points, fill, opacity, strokeColor, 0),
|
|
27
28
|
vec3ZMean(...rotatedPoints),
|
|
28
29
|
rotatedPoints,
|
|
29
|
-
isOpaque(fill,
|
|
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
|
|
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
|
|
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
|
});
|