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
|
@@ -1,172 +1,294 @@
|
|
|
1
|
-
import type { Vec3 } from "../../abstract-3d.js";
|
|
1
|
+
import type { Vec2, Vec3 } from "../../abstract-3d.js";
|
|
2
|
+
import { vec3Zero } from "../../abstract-3d.js";
|
|
3
|
+
import { svgEdges, svgPolygon } from "./svg-encoding.js";
|
|
2
4
|
import type { zOrderElement } from "./svg-geometries/shared.js";
|
|
5
|
+
import { isOpaque } from "./svg-geometries/shared.js";
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
|
-
* Elements in drawing order, back to front, by
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* entirely inside an opaque convex polygon
|
|
8
|
+
* Elements in drawing order, back to front, by a BSP tree of their planes: of each node, what's behind its plane is drawn first, then what's in it,
|
|
9
|
+
* then what's in front of it. An element on both sides of a plane is split by it when it's a plain polygon, its pieces stroked only along its own
|
|
10
|
+
* edges, else put on the side of its center, and drawn whole again where nothing drawn between its pieces overlaps it on screen. Elements without
|
|
11
|
+
* points are drawn last, by mean depth. Elements entirely inside an opaque convex polygon drawn after them are left out, as they can't be seen
|
|
9
12
|
*/
|
|
10
|
-
export function orderElements(elements: ReadonlyArray<zOrderElement
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
swept.push(i);
|
|
13
|
+
export function orderElements(elements: ReadonlyArray<zOrderElement>, point: (x: number, y: number) => Vec2): ReadonlyArray<zOrderElement> {
|
|
14
|
+
const pieces: Array<Piece> = [];
|
|
15
|
+
const pointless: Array<zOrderElement> = [];
|
|
16
|
+
for (const element of elements) {
|
|
17
|
+
if (element.polygon?.length) {
|
|
18
|
+
pieces.push({ element, points: element.polygon, plane: getPlane(element.polygon) });
|
|
19
|
+
} else {
|
|
20
|
+
pointless.push(element);
|
|
19
21
|
}
|
|
20
|
-
});
|
|
21
|
-
swept.sort((a, b) => items[a]!.min.x - items[b]!.min.x);
|
|
22
|
-
byDepth.sort((a, b) => elements[a]!.zOrder - elements[b]!.zOrder);
|
|
23
|
-
|
|
24
|
-
// Sweep along x: each item is compared with the active ones whose bounds still overlap it on screen, dropping the others in the same pass
|
|
25
|
-
const behind: Array<Array<number>> = [];
|
|
26
|
-
const hidden = new Uint8Array(elements.length);
|
|
27
|
-
const active: Array<number> = [];
|
|
28
|
-
for (const i of swept) {
|
|
29
|
-
const a = items[i]!;
|
|
30
|
-
active.length = active.reduce((kept, j) => {
|
|
31
|
-
const b = items[j]!;
|
|
32
|
-
if (b.max.x <= a.min.x + eps) {
|
|
33
|
-
return kept;
|
|
34
|
-
}
|
|
35
|
-
active[kept] = j;
|
|
36
|
-
const order = b.min.y < a.max.y - eps && a.min.y < b.max.y - eps ? getOrder(a, b) : 0;
|
|
37
|
-
if (order !== 0) {
|
|
38
|
-
const [back, front] = order < 0 ? [i, j] : [j, i];
|
|
39
|
-
(behind[front] ??= []).push(back);
|
|
40
|
-
hidden[back] ||= covers(items[front]!, items[back]!) ? 1 : 0;
|
|
41
|
-
}
|
|
42
|
-
return kept + 1;
|
|
43
|
-
}, 0);
|
|
44
|
-
active.push(i);
|
|
45
22
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const state =
|
|
49
|
-
|
|
23
|
+
const drawn: Drawn = { pieces: [], items: [], spans: new Map() };
|
|
24
|
+
draw(pieces, drawn);
|
|
25
|
+
const state = getHidden(drawn.items);
|
|
26
|
+
mergeSplit(drawn, state);
|
|
50
27
|
const ordered: Array<zOrderElement> = [];
|
|
51
|
-
for (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (state[i] === 0) {
|
|
56
|
-
state[i] = 1;
|
|
57
|
-
stack.push(i);
|
|
58
|
-
behind[i]?.forEach((j) => state[j] === 0 && stack.push(j));
|
|
59
|
-
} else if (state[i] === 1) {
|
|
60
|
-
state[i] = 2;
|
|
61
|
-
if (!hidden[i]) {
|
|
62
|
-
ordered.push(elements[i]!);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
28
|
+
for (let i = 0; i < drawn.pieces.length; i++) {
|
|
29
|
+
const p = drawn.pieces[i]!;
|
|
30
|
+
if (state[i] !== skip) {
|
|
31
|
+
ordered.push(state[i] === whole || !p.edges ? p.element : createPieceElement(p, point));
|
|
65
32
|
}
|
|
66
33
|
}
|
|
67
|
-
return ordered;
|
|
34
|
+
return [...ordered, ...pointless.sort((a, b) => a.zOrder - b.zOrder)];
|
|
68
35
|
}
|
|
69
36
|
|
|
70
37
|
/** Distances below this, in the scene's units, are equal */
|
|
71
38
|
const eps = 1e-3;
|
|
39
|
+
/** The planes tried at a node, spread over its pieces, of which the one splitting the fewest is taken */
|
|
40
|
+
const candidates = 8;
|
|
41
|
+
/** A drawn piece's state: drawn as it is, left out, or drawn as its whole element */
|
|
42
|
+
const [skip, whole] = [1, 2];
|
|
72
43
|
|
|
44
|
+
/** A plane oriented toward the viewer, at +z */
|
|
45
|
+
type Plane = { readonly normal: Vec3; readonly d: number };
|
|
46
|
+
/** An element, or a piece of it with whether the edge from each point to the next is the element's own, not a cut */
|
|
47
|
+
type Piece = {
|
|
48
|
+
readonly element: zOrderElement;
|
|
49
|
+
readonly points: ReadonlyArray<Vec3>;
|
|
50
|
+
readonly plane?: Plane;
|
|
51
|
+
readonly edges?: ReadonlyArray<boolean>;
|
|
52
|
+
};
|
|
73
53
|
type Item = {
|
|
74
54
|
readonly points: ReadonlyArray<Vec3>;
|
|
75
|
-
readonly min:
|
|
76
|
-
readonly max:
|
|
77
|
-
/** The screen's normals of the polygon's edges, which separate it from polygons it doesn't overlap */
|
|
78
|
-
readonly axes: ReadonlyArray<readonly [number, number]>;
|
|
79
|
-
/** Its plane, oriented toward the viewer, unless it's a line or a point, or seen edge on */
|
|
80
|
-
readonly plane?: { readonly normal: Vec3; readonly d: number };
|
|
55
|
+
readonly min: Vec2;
|
|
56
|
+
readonly max: Vec2;
|
|
81
57
|
/** The winding on screen of an opaque convex polygon, 1 or -1, by which it can hide others; 0 for others */
|
|
82
58
|
readonly occluder: number;
|
|
83
59
|
};
|
|
60
|
+
/** Pieces in drawing order, their items, and of each split element the first and last of its pieces */
|
|
61
|
+
type Drawn = {
|
|
62
|
+
readonly pieces: Array<Piece>;
|
|
63
|
+
readonly items: Array<Item>;
|
|
64
|
+
readonly spans: Map<zOrderElement, { readonly first: number; readonly last: number }>;
|
|
65
|
+
};
|
|
84
66
|
|
|
85
|
-
/**
|
|
86
|
-
function
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
normal.y += (p.z - q.z) * (p.x + q.x);
|
|
99
|
-
normal.z += (p.x - q.x) * (p.y + q.y);
|
|
100
|
-
if (Math.abs(q.x - p.x) + Math.abs(q.y - p.y) > eps) {
|
|
101
|
-
axes.push([q.y - p.y, p.x - q.x]);
|
|
67
|
+
/** The tree walked as it's built, with a stack instead of recursion, as it's as deep as a scene's planes can be many: back, then in, then front */
|
|
68
|
+
function draw(pieces: ReadonlyArray<Piece>, drawn: Drawn): void {
|
|
69
|
+
const stack: Array<{ readonly pieces: ReadonlyArray<Piece>; readonly inPlane?: boolean }> = [{ pieces }];
|
|
70
|
+
while (stack.length > 0) {
|
|
71
|
+
const task = stack.pop()!;
|
|
72
|
+
const plane = task.inPlane ? undefined : getSplitter(task.pieces);
|
|
73
|
+
if (plane) {
|
|
74
|
+
const [back, inPlane, front]: [Array<Piece>, Array<Piece>, Array<Piece>] = [[], [], []];
|
|
75
|
+
for (const p of task.pieces) {
|
|
76
|
+
partition(p, plane, back, inPlane, front);
|
|
77
|
+
}
|
|
78
|
+
stack.push({ pieces: front }, { pieces: inPlane, inPlane: true }, { pieces: back });
|
|
79
|
+
continue;
|
|
102
80
|
}
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
81
|
+
// In a plane, as given; else a leaf, whose pieces have no plane, by mean depth
|
|
82
|
+
for (const p of task.inPlane ? task.pieces : [...task.pieces].sort((a, b) => a.element.zOrder - b.element.zOrder)) {
|
|
83
|
+
if (p.edges) {
|
|
84
|
+
drawn.spans.set(p.element, { first: drawn.spans.get(p.element)?.first ?? drawn.pieces.length, last: drawn.pieces.length });
|
|
85
|
+
}
|
|
86
|
+
drawn.pieces.push(p);
|
|
87
|
+
drawn.items.push(createItem(p.points, !!p.element.opaque));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Of the candidates' planes, the one that the fewest pieces are on both sides of */
|
|
93
|
+
function getSplitter(pieces: ReadonlyArray<Piece>): Plane | undefined {
|
|
94
|
+
const step = Math.max(1, Math.floor(pieces.length / candidates));
|
|
95
|
+
let best: Plane | undefined = undefined;
|
|
96
|
+
let fewest = Infinity;
|
|
97
|
+
for (let i = 0; i < pieces.length && fewest > 0;) {
|
|
98
|
+
const plane = pieces[i]!.plane;
|
|
99
|
+
if (plane) {
|
|
100
|
+
let splits = 0;
|
|
101
|
+
for (const p of pieces) {
|
|
102
|
+
if (p.plane !== plane && getSide(p.points, plane) === 2 && ++splits >= fewest) {
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
[best, fewest] = splits < fewest ? [plane, splits] : [best, fewest];
|
|
107
|
+
}
|
|
108
|
+
i += plane ? step : 1;
|
|
109
|
+
}
|
|
110
|
+
return best;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Behind, in or in front of the plane; split by it if on both sides and a plain polygon, else on the side of its center */
|
|
114
|
+
function partition(piece: Piece, plane: Plane, back: Array<Piece>, inPlane: Array<Piece>, front: Array<Piece>): void {
|
|
115
|
+
// An element's pieces share its plane, which they're in, though its points may be off it by more than eps
|
|
116
|
+
const side = piece.plane === plane ? 0 : getSide(piece.points, plane);
|
|
117
|
+
if (side === 2 && piece.element.style && piece.points.length >= 3) {
|
|
118
|
+
split(piece, plane, back, front);
|
|
119
|
+
} else if (side === 2) {
|
|
120
|
+
let sum = 0;
|
|
121
|
+
for (const p of piece.points) {
|
|
122
|
+
sum += getDistance(p, plane);
|
|
123
|
+
}
|
|
124
|
+
(sum < 0 ? back : front).push(piece);
|
|
125
|
+
} else {
|
|
126
|
+
(side === 0 ? inPlane : side < 0 ? back : front).push(piece);
|
|
127
|
+
}
|
|
118
128
|
}
|
|
119
129
|
|
|
120
|
-
/**
|
|
121
|
-
function
|
|
122
|
-
|
|
123
|
-
|
|
130
|
+
/** Sutherland–Hodgman against the plane, both ways; an edge of a piece along the cut isn't the element's own */
|
|
131
|
+
function split(piece: Piece, plane: Plane, back: Array<Piece>, front: Array<Piece>): void {
|
|
132
|
+
const { points, edges } = piece;
|
|
133
|
+
const [b, f] = [
|
|
134
|
+
{ points: Array<Vec3>(), edges: Array<boolean>() },
|
|
135
|
+
{ points: Array<Vec3>(), edges: Array<boolean>() },
|
|
136
|
+
];
|
|
137
|
+
const first = getDistance(points[0]!, plane);
|
|
138
|
+
let dp = first;
|
|
139
|
+
for (let i = 0; i < points.length; i++) {
|
|
140
|
+
const [p, q] = [points[i]!, points[(i + 1) % points.length]!];
|
|
141
|
+
const dq = i + 1 < points.length ? getDistance(q, plane) : first;
|
|
142
|
+
const own = edges?.[i] ?? true;
|
|
143
|
+
const crosses = (dp < -eps && dq > eps) || (dp > eps && dq < -eps);
|
|
144
|
+
if (dp <= eps) {
|
|
145
|
+
b.points.push(p);
|
|
146
|
+
b.edges.push(own && (crosses || dq <= eps));
|
|
147
|
+
}
|
|
148
|
+
if (dp >= -eps) {
|
|
149
|
+
f.points.push(p);
|
|
150
|
+
f.edges.push(own && (crosses || dq >= -eps));
|
|
151
|
+
}
|
|
152
|
+
if (crosses) {
|
|
153
|
+
const t = dp / (dp - dq);
|
|
154
|
+
const r = { x: p.x + (q.x - p.x) * t, y: p.y + (q.y - p.y) * t, z: p.z + (q.z - p.z) * t };
|
|
155
|
+
b.points.push(r);
|
|
156
|
+
b.edges.push(own && dq < 0);
|
|
157
|
+
f.points.push(r);
|
|
158
|
+
f.edges.push(own && dq > 0);
|
|
159
|
+
}
|
|
160
|
+
dp = dq;
|
|
124
161
|
}
|
|
125
|
-
if (b.
|
|
126
|
-
|
|
162
|
+
if (b.points.length >= 3) {
|
|
163
|
+
back.push({ ...piece, ...b });
|
|
127
164
|
}
|
|
128
|
-
if (
|
|
129
|
-
|
|
165
|
+
if (f.points.length >= 3) {
|
|
166
|
+
front.push({ ...piece, ...f });
|
|
130
167
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function createPieceElement({ element, points, edges = [] }: Piece, point: (x: number, y: number) => Vec2): zOrderElement {
|
|
171
|
+
const { fill, opacity, stroke, strokeWidth } = element.style!;
|
|
172
|
+
const screen = points.map((p) => point(p.x, p.y));
|
|
173
|
+
const outline = strokeWidth > 0 && isOpaque(stroke, 1) && edges.includes(true) ? svgEdges(screen, edges, stroke, strokeWidth) : "";
|
|
174
|
+
return { ...element, element: svgPolygon(vec3Zero, screen, fill, opacity, stroke, 0) + outline, polygon: points };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** By Newell's method, unless they're a line or a point, or seen edge on */
|
|
178
|
+
function getPlane(points: ReadonlyArray<Vec3>): Plane | undefined {
|
|
179
|
+
let [x, y, z] = [0, 0, 0];
|
|
180
|
+
for (let i = 0; i < points.length; i++) {
|
|
181
|
+
const [p, q] = [points[i]!, points[(i + 1) % points.length]!];
|
|
182
|
+
[x, y, z] = [x + (p.y - q.y) * (p.z + q.z), y + (p.z - q.z) * (p.x + q.x), z + (p.x - q.x) * (p.y + q.y)];
|
|
134
183
|
}
|
|
135
|
-
|
|
184
|
+
const length = Math.hypot(x, y, z);
|
|
185
|
+
if (points.length < 3 || length < eps || Math.abs(z) < 1e-6 * length) {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
const s = Math.sign(z) / length;
|
|
189
|
+
const n = { x: x * s, y: y * s, z: z * s };
|
|
190
|
+
return { normal: n, d: n.x * points[0]!.x + n.y * points[0]!.y + n.z * points[0]!.z };
|
|
136
191
|
}
|
|
137
192
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
193
|
+
const getDistance = (p: Vec3, { normal: n, d }: Plane): number => n.x * p.x + n.y * p.y + n.z * p.z - d;
|
|
194
|
+
|
|
195
|
+
/** -1 if the points are all behind the plane, 1 if all in front of it, 0 if all in it, 2 if on both sides */
|
|
196
|
+
function getSide(points: ReadonlyArray<Vec3>, plane: Plane): number {
|
|
197
|
+
let [min, max] = [Infinity, -Infinity];
|
|
198
|
+
for (const p of points) {
|
|
199
|
+
const distance = getDistance(p, plane);
|
|
200
|
+
min = Math.min(min, distance);
|
|
201
|
+
max = Math.max(max, distance);
|
|
202
|
+
}
|
|
203
|
+
return min >= -eps ? (max > eps ? 1 : 0) : max <= eps ? -1 : 2;
|
|
146
204
|
}
|
|
147
205
|
|
|
148
|
-
/**
|
|
149
|
-
function
|
|
150
|
-
|
|
206
|
+
/** Of items drawn back to front, `skip` for those inside an opaque convex polygon drawn after them; a sweep along x over their bounds on screen */
|
|
207
|
+
function getHidden(items: ReadonlyArray<Item>): Uint8Array {
|
|
208
|
+
const state = new Uint8Array(items.length);
|
|
209
|
+
const active: Array<number> = [];
|
|
210
|
+
for (const i of [...items.keys()].sort((a, b) => items[a]!.min.x - items[b]!.min.x)) {
|
|
211
|
+
const a = items[i]!;
|
|
212
|
+
// Those whose bounds end before this one's start are dropped in place
|
|
213
|
+
let kept = 0;
|
|
214
|
+
for (const j of active) {
|
|
215
|
+
const b = items[j]!;
|
|
216
|
+
if (b.max.x > a.min.x + eps) {
|
|
217
|
+
active[kept++] = j;
|
|
218
|
+
// Along x they overlap, by the sweep, which a vertical line's zero width wouldn't by bounds
|
|
219
|
+
const [back, front] = [Math.min(i, j), Math.max(i, j)];
|
|
220
|
+
state[back] ||= a.min.y < b.max.y - eps && b.min.y < a.max.y - eps && covers(items[front]!, items[back]!) ? skip : 0;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
active.length = kept;
|
|
224
|
+
active.push(i);
|
|
225
|
+
}
|
|
226
|
+
return state;
|
|
151
227
|
}
|
|
152
228
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
229
|
+
/**
|
|
230
|
+
* A split element whose pieces have nothing else drawn between them that overlaps it on screen is drawn whole where its last piece is, its other
|
|
231
|
+
* pieces skipped, unless they're all hidden
|
|
232
|
+
*/
|
|
233
|
+
function mergeSplit({ pieces, items, spans }: Drawn, state: Uint8Array): void {
|
|
234
|
+
for (const [element, { first, last }] of spans) {
|
|
235
|
+
const bounds = createItem(element.polygon!, false);
|
|
236
|
+
let [blocked, visible] = [false, false];
|
|
237
|
+
for (let k = first; k <= last && !blocked; k++) {
|
|
238
|
+
const own = pieces[k]!.element === element;
|
|
239
|
+
visible ||= own && state[k] !== skip;
|
|
240
|
+
blocked = !own && overlaps(items[k]!, bounds);
|
|
241
|
+
}
|
|
242
|
+
if (!blocked) {
|
|
243
|
+
for (let k = first; k <= last; k++) {
|
|
244
|
+
state[k] = pieces[k]!.element === element ? skip : state[k]!;
|
|
245
|
+
}
|
|
246
|
+
state[last] = visible ? whole : skip;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** In one pass over the points: their bounds on screen, and whether they're a convex polygon */
|
|
252
|
+
function createItem(points: ReadonlyArray<Vec3>, opaque: boolean): Item {
|
|
253
|
+
const [min, max] = [
|
|
254
|
+
{ x: Infinity, y: Infinity },
|
|
255
|
+
{ x: -Infinity, y: -Infinity },
|
|
256
|
+
];
|
|
257
|
+
// Twice the signed area, and the smallest and largest turn at a corner on screen, the same sign throughout for a convex polygon
|
|
258
|
+
let [area, minTurn, maxTurn] = [0, 0, 0];
|
|
259
|
+
for (let i = 0; i < points.length; i++) {
|
|
260
|
+
const o = points[(i + points.length - 1) % points.length]!;
|
|
261
|
+
const p = points[i]!;
|
|
262
|
+
const q = points[(i + 1) % points.length]!;
|
|
263
|
+
min.x = Math.min(min.x, p.x);
|
|
264
|
+
min.y = Math.min(min.y, p.y);
|
|
265
|
+
max.x = Math.max(max.x, p.x);
|
|
266
|
+
max.y = Math.max(max.y, p.y);
|
|
267
|
+
const turn = (p.x - o.x) * (q.y - p.y) - (p.y - o.y) * (q.x - p.x);
|
|
268
|
+
area += p.x * q.y - q.x * p.y;
|
|
269
|
+
minTurn = Math.min(minTurn, turn);
|
|
270
|
+
maxTurn = Math.max(maxTurn, turn);
|
|
271
|
+
}
|
|
272
|
+
const convex = points.length >= 3 && (minTurn >= -eps || maxTurn <= eps) && Math.abs(area) > eps;
|
|
273
|
+
return { points, min, max, occluder: opaque && convex ? Math.sign(area) : 0 };
|
|
159
274
|
}
|
|
160
275
|
|
|
276
|
+
const overlaps = (a: Pick<Item, "min" | "max">, b: Pick<Item, "min" | "max">): boolean =>
|
|
277
|
+
a.min.x < b.max.x - eps && b.min.x < a.max.x - eps && a.min.y < b.max.y - eps && b.min.y < a.max.y - eps;
|
|
278
|
+
|
|
161
279
|
/** Whether the front's opaque convex polygon contains all of the back's points on screen, so the back can't be seen; not for a single point, e.g. text */
|
|
162
|
-
function covers(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
280
|
+
function covers({ points, occluder }: Item, back: Item): boolean {
|
|
281
|
+
if (occluder === 0 || back.points.length < 2) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
for (let i = 0; i < points.length; i++) {
|
|
285
|
+
const [p, q] = [points[i]!, points[(i + 1) % points.length]!];
|
|
286
|
+
const tolerance = -eps * Math.hypot(q.x - p.x, q.y - p.y);
|
|
287
|
+
for (const r of back.points) {
|
|
288
|
+
if (occluder * ((q.x - p.x) * (r.y - p.y) - (q.y - p.y) * (r.x - p.x)) < tolerance) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return true;
|
|
172
294
|
}
|
package/src/renderers/svg/svg.ts
CHANGED
|
@@ -93,13 +93,13 @@ function renderInternal(
|
|
|
93
93
|
imageBg: options?.imageBg ?? false,
|
|
94
94
|
className: options?.className ?? "",
|
|
95
95
|
};
|
|
96
|
-
//
|
|
97
|
-
const
|
|
98
|
-
const view =
|
|
99
|
-
const
|
|
100
|
-
? vec3RotCombine(vec3(
|
|
96
|
+
// A three-quarter view: the front turned 30° to show the side, and tilted 20° to show the top, the front foremost
|
|
97
|
+
const angled = opts.view === "TopLeft" || opts.view === "TopRight";
|
|
98
|
+
const view = angled ? "front" : opts.view;
|
|
99
|
+
const angledRot = angled
|
|
100
|
+
? vec3RotCombine(vec3((20 * Math.PI) / 180, 0, 0), vec3(0, ((opts.view === "TopRight" ? -1 : 1) * 30 * Math.PI) / 180, 0))
|
|
101
101
|
: vec3Zero;
|
|
102
|
-
const cameraRot = vec3RotCombine(
|
|
102
|
+
const cameraRot = vec3RotCombine(angledRot, rotationForCameraPos(view));
|
|
103
103
|
const viewRot = opts.rotation ? vec3RotCombine(vec3(0, 0, (opts.rotation * Math.PI) / 180), cameraRot) : cameraRot;
|
|
104
104
|
const unitRot = vec3RotCombine(viewRot, scene.rotation_deprecated ?? vec3Zero);
|
|
105
105
|
|
|
@@ -110,12 +110,17 @@ function renderInternal(
|
|
|
110
110
|
const point = (x: number, y: number): Vec2 => vec2(svgCenter.x + x, svgCenter.y - y);
|
|
111
111
|
const unitCenterFlipped = vec3Flip(unitCenter);
|
|
112
112
|
|
|
113
|
-
const elements = [
|
|
113
|
+
const elements = [
|
|
114
|
+
...orderElements(
|
|
115
|
+
scene.groups.flatMap((g) => svgGroup(g, unitCenterFlipped, unitRot, point, opts)),
|
|
116
|
+
point
|
|
117
|
+
),
|
|
118
|
+
];
|
|
114
119
|
const dimOpts: SvgOptions = { ...opts, only_stroke: false, gray_scale: false };
|
|
115
120
|
const visibleViews = calculateVisibleViews(view, scene.rotation_deprecated);
|
|
116
121
|
|
|
117
122
|
// Dimensions are drawn for the flat views
|
|
118
|
-
for (const dim of
|
|
123
|
+
for (const dim of angled ? [] : (scene.dimensions_deprecated?.dimensions ?? [])) {
|
|
119
124
|
const d = dimensionConvertToTypeMesh(dim, scene.rotation_deprecated ?? vec3Zero, scene.dimensions_deprecated?.material);
|
|
120
125
|
if (isViewVisible(d.views[0], visibleViews)) {
|
|
121
126
|
const pos = vec3TransRot(d.pos, unitCenterFlipped, unitRot);
|
|
@@ -11,9 +11,9 @@ const direction = new Vector3(-1, 2, 1.2).normalize();
|
|
|
11
11
|
const intensity = 1.2;
|
|
12
12
|
const mapSize = 1024;
|
|
13
13
|
/** Of the key light kept out of its shadows, so faces under an overhang, e.g. a recessed base frame, aren't black */
|
|
14
|
-
const shadowIntensity = 0.
|
|
14
|
+
const shadowIntensity = 0.55;
|
|
15
15
|
/** The penumbra's reach, in shadow map texels */
|
|
16
|
-
const penumbra =
|
|
16
|
+
const penumbra = 8;
|
|
17
17
|
|
|
18
18
|
/** The light and its target, to add to the scene, with or without shadows; undefined for a scene without solids */
|
|
19
19
|
export function createKeyLight(built: BuiltScene, shadows: boolean): Group | undefined {
|
|
@@ -37,8 +37,8 @@ const shadowLayer = 1;
|
|
|
37
37
|
/** Of the texture's longer side */
|
|
38
38
|
const resolution = 512;
|
|
39
39
|
/** The blur's reach, of the solids' height */
|
|
40
|
-
const softness = 0.
|
|
41
|
-
const opacity = 0.
|
|
40
|
+
const softness = 0.16;
|
|
41
|
+
const opacity = 0.9;
|
|
42
42
|
/** A pool of light on the floor the shadow lies in, so it shows on a dark background, which a shadow alone can't darken; on white it's invisible */
|
|
43
43
|
const floorLight = 0.06;
|
|
44
44
|
/** The pool's reach beyond the solids, of their height */
|