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.
- package/README.md +2 -0
- package/lib/abstract-3d.d.ts +6 -1
- package/lib/abstract-3d.d.ts.map +1 -1
- package/lib/abstract-3d.js +24 -2
- package/lib/abstract-3d.js.map +1 -1
- package/lib/renderers/react/react.d.ts.map +1 -1
- package/lib/renderers/react/react.js +9 -3
- package/lib/renderers/react/react.js.map +1 -1
- package/lib/renderers/svg/index.d.ts +1 -1
- package/lib/renderers/svg/index.d.ts.map +1 -1
- package/lib/renderers/svg/svg-geometries/shared.d.ts +14 -2
- package/lib/renderers/svg/svg-geometries/shared.d.ts.map +1 -1
- package/lib/renderers/svg/svg-geometries/shared.js +15 -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 +41 -30
- 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 +3 -3
- 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 +18 -6
- package/lib/renderers/svg/svg-geometries/svg-cylinder.js.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-image.d.ts.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-image.js +3 -2
- package/lib/renderers/svg/svg-geometries/svg-image.js.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-line.d.ts.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-line.js +3 -1
- package/lib/renderers/svg/svg-geometries/svg-line.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 +4 -2
- 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 +4 -2
- 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 +4 -2
- package/lib/renderers/svg/svg-geometries/svg-shape.js.map +1 -1
- package/lib/renderers/svg/svg-geometries/svg-text.js +1 -1
- package/lib/renderers/svg/svg-geometries/svg-text.js.map +1 -1
- package/lib/renderers/svg/svg-order.d.ts +9 -0
- package/lib/renderers/svg/svg-order.d.ts.map +1 -0
- package/lib/renderers/svg/svg-order.js +148 -0
- package/lib/renderers/svg/svg-order.js.map +1 -0
- package/lib/renderers/svg/svg.d.ts.map +1 -1
- package/lib/renderers/svg/svg.js +13 -9
- package/lib/renderers/svg/svg.js.map +1 -1
- package/lib/renderers/three/build/build.d.ts +2 -0
- package/lib/renderers/three/build/build.d.ts.map +1 -1
- package/lib/renderers/three/build/build.js +1 -1
- package/lib/renderers/three/build/build.js.map +1 -1
- package/lib/renderers/three/build/materials.d.ts +5 -1
- package/lib/renderers/three/build/materials.d.ts.map +1 -1
- package/lib/renderers/three/build/materials.js +53 -17
- package/lib/renderers/three/build/materials.js.map +1 -1
- package/lib/renderers/three/environment/studio.d.ts +3 -2
- package/lib/renderers/three/environment/studio.d.ts.map +1 -1
- package/lib/renderers/three/environment/studio.js +4 -3
- package/lib/renderers/three/environment/studio.js.map +1 -1
- package/lib/renderers/three/geometry/geometry.d.ts.map +1 -1
- package/lib/renderers/three/geometry/geometry.js +11 -5
- package/lib/renderers/three/geometry/geometry.js.map +1 -1
- package/lib/renderers/three/shadow/key-light.js +1 -1
- package/lib/renderers/three/viewer/options.d.ts +7 -1
- package/lib/renderers/three/viewer/options.d.ts.map +1 -1
- package/lib/renderers/three/viewer/options.js +1 -0
- package/lib/renderers/three/viewer/options.js.map +1 -1
- package/lib/renderers/three/viewer/viewer.d.ts +5 -0
- package/lib/renderers/three/viewer/viewer.d.ts.map +1 -1
- package/lib/renderers/three/viewer/viewer.js +54 -11
- package/lib/renderers/three/viewer/viewer.js.map +1 -1
- package/package.json +1 -1
- package/src/abstract-3d.ts +30 -2
- package/src/renderers/react/react.tsx +14 -4
- package/src/renderers/svg/index.ts +1 -1
- package/src/renderers/svg/svg-geometries/shared.ts +26 -3
- package/src/renderers/svg/svg-geometries/svg-box.ts +42 -36
- package/src/renderers/svg/svg-geometries/svg-cone.ts +17 -3
- package/src/renderers/svg/svg-geometries/svg-cylinder.ts +25 -6
- package/src/renderers/svg/svg-geometries/svg-image.ts +3 -2
- package/src/renderers/svg/svg-geometries/svg-line.ts +3 -1
- package/src/renderers/svg/svg-geometries/svg-plane.ts +9 -2
- package/src/renderers/svg/svg-geometries/svg-polygon.ts +9 -2
- package/src/renderers/svg/svg-geometries/svg-shape.ts +9 -2
- package/src/renderers/svg/svg-geometries/svg-text.ts +1 -1
- package/src/renderers/svg/svg-order.test.ts +35 -0
- package/src/renderers/svg/svg-order.ts +172 -0
- package/src/renderers/svg/svg.ts +13 -9
- package/src/renderers/three/build/build.ts +3 -1
- package/src/renderers/three/build/materials.ts +57 -17
- package/src/renderers/three/environment/studio.ts +4 -3
- package/src/renderers/three/geometry/geometry.test.ts +17 -5
- package/src/renderers/three/geometry/geometry.ts +12 -5
- package/src/renderers/three/shadow/key-light.ts +1 -1
- package/src/renderers/three/viewer/options.ts +8 -1
- 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
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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(
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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 [
|
|
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
|
+
}
|
package/src/renderers/svg/svg.ts
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
109
|
-
const visibleViews = calculateVisibleViews(opts.view, scene.rotation_deprecated);
|
|
112
|
+
const visibleViews = calculateVisibleViews(view, scene.rotation_deprecated);
|
|
110
113
|
|
|
111
|
-
|
|
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
|
}
|