@opsydyn/effect-three 0.3.0 → 0.6.4
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/CHANGELOG.md +56 -0
- package/README.md +15 -1
- package/dist/{index-8cf8t6by.js → index-re3rrfwd.js} +2935 -104
- package/dist/index-re3rrfwd.js.map +56 -0
- package/dist/index-ttwy0znv.js +145 -0
- package/dist/index-ttwy0znv.js.map +12 -0
- package/dist/index.js +557 -147
- package/dist/index.js.map +8 -6
- package/dist/kernel/bodies.d.ts +20 -1
- package/dist/kernel/bodies.d.ts.map +1 -1
- package/dist/kernel/boxSelect.d.ts +22 -0
- package/dist/kernel/boxSelect.d.ts.map +1 -0
- package/dist/kernel/build.d.ts +165 -7
- package/dist/kernel/build.d.ts.map +1 -1
- package/dist/kernel/cameraShake.d.ts.map +1 -1
- package/dist/kernel/describeScene.d.ts +50 -0
- package/dist/kernel/describeScene.d.ts.map +1 -0
- package/dist/kernel/description.d.ts +223 -1
- package/dist/kernel/description.d.ts.map +1 -1
- package/dist/kernel/diff.d.ts +15 -0
- package/dist/kernel/diff.d.ts.map +1 -1
- package/dist/kernel/equality.d.ts +11 -1
- package/dist/kernel/equality.d.ts.map +1 -1
- package/dist/kernel/gizmo.d.ts +69 -0
- package/dist/kernel/gizmo.d.ts.map +1 -0
- package/dist/kernel/history.d.ts +61 -0
- package/dist/kernel/history.d.ts.map +1 -0
- package/dist/kernel/index.d.ts +19 -6
- package/dist/kernel/index.d.ts.map +1 -1
- package/dist/kernel/index.js +124 -3
- package/dist/kernel/index.js.map +1 -1
- package/dist/kernel/inspect.d.ts +85 -0
- package/dist/kernel/inspect.d.ts.map +1 -0
- package/dist/kernel/lineGeometry.d.ts +25 -0
- package/dist/kernel/lineGeometry.d.ts.map +1 -0
- package/dist/kernel/lockstep.d.ts +114 -0
- package/dist/kernel/lockstep.d.ts.map +1 -0
- package/dist/kernel/loop.d.ts.map +1 -1
- package/dist/kernel/materials.d.ts.map +1 -1
- package/dist/kernel/paintOrder.d.ts +53 -0
- package/dist/kernel/paintOrder.d.ts.map +1 -0
- package/dist/kernel/physics.d.ts +39 -0
- package/dist/kernel/physics.d.ts.map +1 -1
- package/dist/kernel/pivot.d.ts +54 -0
- package/dist/kernel/pivot.d.ts.map +1 -0
- package/dist/kernel/pointBuffers.d.ts +17 -0
- package/dist/kernel/pointBuffers.d.ts.map +1 -0
- package/dist/kernel/pointer.d.ts +31 -1
- package/dist/kernel/pointer.d.ts.map +1 -1
- package/dist/kernel/preview.d.ts +32 -0
- package/dist/kernel/preview.d.ts.map +1 -0
- package/dist/kernel/renderKit.d.ts +21 -1
- package/dist/kernel/renderKit.d.ts.map +1 -1
- package/dist/kernel/renderer.d.ts +6 -0
- package/dist/kernel/renderer.d.ts.map +1 -1
- package/dist/kernel/sceneChanges.d.ts +87 -0
- package/dist/kernel/sceneChanges.d.ts.map +1 -0
- package/dist/kernel/sceneGraph.d.ts +9 -1
- package/dist/kernel/sceneGraph.d.ts.map +1 -1
- package/dist/kernel/sceneIndex.d.ts +5 -0
- package/dist/kernel/sceneIndex.d.ts.map +1 -1
- package/dist/kernel/seeded.d.ts +9 -0
- package/dist/kernel/seeded.d.ts.map +1 -0
- package/dist/kernel/shots.d.ts +26 -0
- package/dist/kernel/shots.d.ts.map +1 -0
- package/dist/kernel/tape.d.ts +38 -0
- package/dist/kernel/tape.d.ts.map +1 -0
- package/dist/kernel/timeline.d.ts +60 -0
- package/dist/kernel/timeline.d.ts.map +1 -0
- package/dist/kernel/trail.d.ts +31 -0
- package/dist/kernel/trail.d.ts.map +1 -0
- package/dist/kernel/viewportGizmo.d.ts +25 -0
- package/dist/kernel/viewportGizmo.d.ts.map +1 -0
- package/dist/runtime/Inspector.d.ts +20 -0
- package/dist/runtime/Inspector.d.ts.map +1 -0
- package/dist/runtime/SceneFile.d.ts +462 -0
- package/dist/runtime/SceneFile.d.ts.map +1 -0
- package/dist/runtime/SceneSchema.d.ts +404 -0
- package/dist/runtime/SceneSchema.d.ts.map +1 -1
- package/dist/runtime/errors.d.ts +9 -0
- package/dist/runtime/errors.d.ts.map +1 -1
- package/dist/runtime/index.d.ts +3 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/webgl/index.d.ts.map +1 -1
- package/dist/webgl/index.js +228 -3
- package/dist/webgl/index.js.map +6 -4
- package/dist/webgl/line.d.ts +4 -0
- package/dist/webgl/line.d.ts.map +1 -0
- package/dist/webgl/points.d.ts +4 -0
- package/dist/webgl/points.d.ts.map +1 -0
- package/dist/webgpu/index.d.ts.map +1 -1
- package/dist/webgpu/index.js +206 -26
- package/dist/webgpu/index.js.map +6 -4
- package/dist/webgpu/line.d.ts +4 -0
- package/dist/webgpu/line.d.ts.map +1 -0
- package/dist/webgpu/points.d.ts +4 -0
- package/dist/webgpu/points.d.ts.map +1 -0
- package/package.json +4 -2
- package/src/kernel/bodies.ts +299 -17
- package/src/kernel/boxSelect.ts +73 -0
- package/src/kernel/build.ts +374 -1
- package/src/kernel/cameraShake.ts +3 -13
- package/src/kernel/describeScene.ts +224 -0
- package/src/kernel/description.ts +243 -0
- package/src/kernel/diff.ts +39 -0
- package/src/kernel/equality.ts +123 -0
- package/src/kernel/gizmo.ts +478 -0
- package/src/kernel/history.ts +118 -0
- package/src/kernel/index.ts +110 -1
- package/src/kernel/inspect.ts +214 -0
- package/src/kernel/lineGeometry.ts +75 -0
- package/src/kernel/lockstep.ts +237 -0
- package/src/kernel/loop.ts +7 -0
- package/src/kernel/materials.ts +3 -0
- package/src/kernel/paintOrder.ts +274 -0
- package/src/kernel/physics.ts +86 -1
- package/src/kernel/pivot.ts +418 -0
- package/src/kernel/pointBuffers.ts +115 -0
- package/src/kernel/pointer.ts +98 -17
- package/src/kernel/preview.ts +467 -0
- package/src/kernel/renderKit.ts +23 -1
- package/src/kernel/renderer.ts +6 -0
- package/src/kernel/sceneChanges.ts +265 -0
- package/src/kernel/sceneGraph.ts +354 -11
- package/src/kernel/sceneIndex.ts +8 -0
- package/src/kernel/seeded.ts +18 -0
- package/src/kernel/shots.ts +167 -0
- package/src/kernel/tape.ts +124 -0
- package/src/kernel/timeline.ts +93 -0
- package/src/kernel/trail.ts +170 -0
- package/src/kernel/viewportGizmo.ts +194 -0
- package/src/runtime/Inspector.ts +43 -0
- package/src/runtime/SceneFile.ts +46 -0
- package/src/runtime/SceneSchema.ts +319 -0
- package/src/runtime/errors.ts +6 -0
- package/src/runtime/index.ts +3 -1
- package/src/webgl/index.ts +4 -0
- package/src/webgl/line.ts +66 -0
- package/src/webgl/points.ts +205 -0
- package/src/webgpu/index.ts +4 -0
- package/src/webgpu/line.ts +67 -0
- package/src/webgpu/points.ts +193 -0
- package/dist/index-8cf8t6by.js.map +0 -41
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A still of a scene as SVG text, drawn without a browser or a GPU: for previews made on a
|
|
3
|
+
* server, in a build or in a test. The description is applied to a scene graph as it is
|
|
4
|
+
* for the renderer, posed at a moment of scene time, projected with three's `Projector`
|
|
5
|
+
* and painted as flat-shaded paths, as three's `SVGRenderer` does
|
|
6
|
+
* (`examples/jsm/renderers/SVGRenderer.js` at 0.186.1), but in the order a binary space
|
|
7
|
+
* partition gives (`paintOrder.ts`) rather than by depth, so that faces meeting (a floor
|
|
8
|
+
* and what stands on it) are painted in the right order. Each face is lit by the diffuse
|
|
9
|
+
* terms of three's shaders (`lights_pars_begin` and `lights_physical_pars_fragment` at
|
|
10
|
+
* 0.186.1): ambient and hemisphere light, then directional, point and spot light by the
|
|
11
|
+
* face's normal with their distance and cone falloff, over π (`BRDF_Lambert`).
|
|
12
|
+
*
|
|
13
|
+
* It is a preview, not the picture: no textures, shadows, specular highlights, fog,
|
|
14
|
+
* environment lighting, tone mapping, post-processing, views, HUD or render-kit parts
|
|
15
|
+
* (sky, shader grids, wide lines), and no labels. Borrowed models and text are drawn when
|
|
16
|
+
* `store` lends their objects and fonts. Skinned models stand in their rest pose.
|
|
17
|
+
*
|
|
18
|
+
* @module
|
|
19
|
+
*/
|
|
20
|
+
import {
|
|
21
|
+
Box2,
|
|
22
|
+
Color,
|
|
23
|
+
type ColorSpace,
|
|
24
|
+
DirectionalLight,
|
|
25
|
+
HemisphereLight,
|
|
26
|
+
InstancedMesh,
|
|
27
|
+
LinearSRGBColorSpace,
|
|
28
|
+
LineBasicMaterial,
|
|
29
|
+
LineDashedMaterial,
|
|
30
|
+
type Material,
|
|
31
|
+
Matrix3,
|
|
32
|
+
Matrix4,
|
|
33
|
+
Mesh,
|
|
34
|
+
MeshBasicMaterial,
|
|
35
|
+
MeshLambertMaterial,
|
|
36
|
+
MeshNormalMaterial,
|
|
37
|
+
MeshPhongMaterial,
|
|
38
|
+
MeshStandardMaterial,
|
|
39
|
+
MeshToonMaterial,
|
|
40
|
+
Light,
|
|
41
|
+
Line,
|
|
42
|
+
type Object3D,
|
|
43
|
+
PointLight,
|
|
44
|
+
Points,
|
|
45
|
+
Scene,
|
|
46
|
+
Sprite,
|
|
47
|
+
PointsMaterial,
|
|
48
|
+
SpotLight,
|
|
49
|
+
SpriteMaterial,
|
|
50
|
+
SRGBColorSpace,
|
|
51
|
+
Vector2,
|
|
52
|
+
Vector3,
|
|
53
|
+
AmbientLight,
|
|
54
|
+
} from 'three';
|
|
55
|
+
import {
|
|
56
|
+
Projector,
|
|
57
|
+
RenderableFace,
|
|
58
|
+
RenderableLine,
|
|
59
|
+
RenderableSprite,
|
|
60
|
+
} from 'three/addons/renderers/Projector.js';
|
|
61
|
+
|
|
62
|
+
import type { Rgb, SceneDescription } from './description.ts';
|
|
63
|
+
import {
|
|
64
|
+
type Facet,
|
|
65
|
+
type Mark,
|
|
66
|
+
paintOrder,
|
|
67
|
+
type Plane,
|
|
68
|
+
split,
|
|
69
|
+
type Unit,
|
|
70
|
+
unitOf,
|
|
71
|
+
} from './paintOrder.ts';
|
|
72
|
+
import type { ResourceStore } from './resources.ts';
|
|
73
|
+
import { createSceneGraph, type SceneGraphOptions } from './sceneGraph.ts';
|
|
74
|
+
import type { DescriptionError } from './sceneIndex.ts';
|
|
75
|
+
|
|
76
|
+
export type PreviewOptions = {
|
|
77
|
+
/** The picture's size in pixels; the camera takes its aspect ratio. */
|
|
78
|
+
readonly width: number;
|
|
79
|
+
readonly height: number;
|
|
80
|
+
/** The moment of scene time shown, in seconds: behaviours and clips as they are then. */
|
|
81
|
+
readonly at?: number;
|
|
82
|
+
/** A colour behind the scene; transparent without one. */
|
|
83
|
+
readonly background?: Rgb | null;
|
|
84
|
+
/** Lends what borrowed nodes, maps by id and text name; nothing is lent without one. */
|
|
85
|
+
readonly store?: ResourceStore;
|
|
86
|
+
/** Behaviours nodes may name, beyond the built-in ones, as for the scene graph. */
|
|
87
|
+
readonly behaviours?: SceneGraphOptions['behaviours'];
|
|
88
|
+
/** Digits after the decimal point in coordinates. */
|
|
89
|
+
readonly precision?: number;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export type PreviewResult =
|
|
93
|
+
| { readonly _tag: 'Preview'; readonly svg: string }
|
|
94
|
+
| { readonly _tag: 'Invalid'; readonly error: DescriptionError };
|
|
95
|
+
|
|
96
|
+
/** Pixels each edge of a face is widened by, as `SVGRenderer`'s `overdraw`. */
|
|
97
|
+
const overdraw = 0.5;
|
|
98
|
+
|
|
99
|
+
const black = new Color(0, 0, 0);
|
|
100
|
+
|
|
101
|
+
/** Diffuse irradiance on a face from every light, before `BRDF_Lambert`'s 1 / π. */
|
|
102
|
+
const irradianceAt = (
|
|
103
|
+
lights: ReadonlyArray<Light>,
|
|
104
|
+
position: Vector3,
|
|
105
|
+
normal: Vector3,
|
|
106
|
+
into: Color,
|
|
107
|
+
): Color => {
|
|
108
|
+
into.copy(black);
|
|
109
|
+
const lightPosition = new Vector3();
|
|
110
|
+
const toLight = new Vector3();
|
|
111
|
+
const direction = new Vector3();
|
|
112
|
+
for (const light of lights) {
|
|
113
|
+
const color = light.color.clone().multiplyScalar(light.intensity);
|
|
114
|
+
if (light instanceof AmbientLight) {
|
|
115
|
+
into.add(color);
|
|
116
|
+
} else if (light instanceof HemisphereLight) {
|
|
117
|
+
lightPosition.setFromMatrixPosition(light.matrixWorld).normalize();
|
|
118
|
+
const weight = 0.5 * normal.dot(lightPosition) + 0.5;
|
|
119
|
+
into.add(light.groundColor.clone().lerp(light.color, weight).multiplyScalar(light.intensity));
|
|
120
|
+
} else if (light instanceof DirectionalLight) {
|
|
121
|
+
lightPosition.setFromMatrixPosition(light.matrixWorld);
|
|
122
|
+
direction.setFromMatrixPosition(light.target.matrixWorld);
|
|
123
|
+
toLight.subVectors(lightPosition, direction).normalize();
|
|
124
|
+
into.add(color.multiplyScalar(Math.max(normal.dot(toLight), 0)));
|
|
125
|
+
} else if (light instanceof PointLight || light instanceof SpotLight) {
|
|
126
|
+
lightPosition.setFromMatrixPosition(light.matrixWorld);
|
|
127
|
+
toLight.subVectors(lightPosition, position);
|
|
128
|
+
const distance = toLight.length();
|
|
129
|
+
toLight.normalize();
|
|
130
|
+
const cone = light instanceof SpotLight ? spotAttenuation(light, toLight) : 1;
|
|
131
|
+
const falloff = distanceAttenuation(distance, light.distance, light.decay);
|
|
132
|
+
into.add(color.multiplyScalar(Math.max(normal.dot(toLight), 0) * cone * falloff));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return into;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/** three's `getDistanceAttenuation`. */
|
|
139
|
+
const distanceAttenuation = (distance: number, cutoff: number, decay: number): number => {
|
|
140
|
+
const falloff = 1 / Math.max(distance ** decay, 0.01);
|
|
141
|
+
return cutoff > 0
|
|
142
|
+
? falloff * Math.min(Math.max(1 - (distance / cutoff) ** 4, 0), 1) ** 2
|
|
143
|
+
: falloff;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const smoothstep = (low: number, high: number, x: number): number => {
|
|
147
|
+
const t = Math.min(Math.max((x - low) / (high - low), 0), 1);
|
|
148
|
+
return t * t * (3 - 2 * t);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/** three's `getSpotAttenuation`, with the cone and penumbra cosines `WebGLLights` gives it. */
|
|
152
|
+
const spotAttenuation = (light: SpotLight, toLight: Vector3): number => {
|
|
153
|
+
const direction = new Vector3()
|
|
154
|
+
.setFromMatrixPosition(light.matrixWorld)
|
|
155
|
+
.sub(new Vector3().setFromMatrixPosition(light.target.matrixWorld))
|
|
156
|
+
.normalize();
|
|
157
|
+
return smoothstep(
|
|
158
|
+
Math.cos(light.angle),
|
|
159
|
+
Math.cos(light.angle * (1 - light.penumbra)),
|
|
160
|
+
direction.dot(toLight),
|
|
161
|
+
);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
type Painter = {
|
|
165
|
+
readonly lights: ReadonlyArray<Light>;
|
|
166
|
+
readonly normalView: Matrix3;
|
|
167
|
+
readonly colorSpace: ColorSpace;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* A face's colour, as `SVGRenderer.renderFace3` with three's physical diffuse term, lit
|
|
172
|
+
* from its object's `centre` so that the faces of one flat side share a colour; `undefined`
|
|
173
|
+
* for a material it does not draw. A metal is lit as if matte: without the environment it
|
|
174
|
+
* reflects, three would draw it nearly black.
|
|
175
|
+
*/
|
|
176
|
+
const faceColor = (
|
|
177
|
+
painter: Painter,
|
|
178
|
+
face: RenderableFace,
|
|
179
|
+
material: Material,
|
|
180
|
+
centre: Vector3,
|
|
181
|
+
): Color | undefined => {
|
|
182
|
+
if (material instanceof MeshBasicMaterial) {
|
|
183
|
+
const color = material.color.clone();
|
|
184
|
+
return material.vertexColors ? color.multiply(face.color) : color;
|
|
185
|
+
}
|
|
186
|
+
if (material instanceof MeshNormalMaterial) {
|
|
187
|
+
const normal = face.normalModel.clone().applyMatrix3(painter.normalView).normalize();
|
|
188
|
+
return new Color(normal.x, normal.y, normal.z).multiplyScalar(0.5).addScalar(0.5);
|
|
189
|
+
}
|
|
190
|
+
if (
|
|
191
|
+
material instanceof MeshLambertMaterial ||
|
|
192
|
+
material instanceof MeshPhongMaterial ||
|
|
193
|
+
material instanceof MeshStandardMaterial ||
|
|
194
|
+
material instanceof MeshToonMaterial
|
|
195
|
+
) {
|
|
196
|
+
const diffuse = material.color.clone();
|
|
197
|
+
if (material.vertexColors) diffuse.multiply(face.color);
|
|
198
|
+
return irradianceAt(painter.lights, centre, face.normalModel, new Color())
|
|
199
|
+
.multiply(diffuse)
|
|
200
|
+
.multiplyScalar(1 / Math.PI)
|
|
201
|
+
.add(material.emissive.clone().multiplyScalar(material.emissiveIntensity));
|
|
202
|
+
}
|
|
203
|
+
return undefined;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/** A colour as SVG writes it, clamped to what a display shows. */
|
|
207
|
+
const styleOf = (color: Color, colorSpace: ColorSpace): string => {
|
|
208
|
+
const shown = new Color(
|
|
209
|
+
Math.min(Math.max(color.r, 0), 1),
|
|
210
|
+
Math.min(Math.max(color.g, 0), 1),
|
|
211
|
+
Math.min(Math.max(color.b, 0), 1),
|
|
212
|
+
);
|
|
213
|
+
return `#${shown.getHexString(colorSpace)}`;
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/** What a material's opacity is worth: three blends only a transparent material. */
|
|
217
|
+
const opacityOf = (material: Material): number => (material.transparent ? material.opacity : 1);
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Stands in for what an object draws, posed where it is in the world, for `Projector` to
|
|
221
|
+
* project alone: each instance of an instanced mesh as a mesh of its own (`Projector` draws
|
|
222
|
+
* an instanced mesh's geometry once), its colour copied into its material.
|
|
223
|
+
*/
|
|
224
|
+
const standInsFor = (
|
|
225
|
+
object: Object3D,
|
|
226
|
+
): ReadonlyArray<{ readonly standIn: Object3D; readonly tinted?: Material }> => {
|
|
227
|
+
if (!(object instanceof InstancedMesh)) {
|
|
228
|
+
const standIn = object.clone(false);
|
|
229
|
+
standIn.matrix.copy(object.matrixWorld);
|
|
230
|
+
return [{ standIn }];
|
|
231
|
+
}
|
|
232
|
+
const material = Array.isArray(object.material) ? object.material[0] : object.material;
|
|
233
|
+
if (material === undefined) return [];
|
|
234
|
+
const matrix = new Matrix4();
|
|
235
|
+
const standIns: Array<{ readonly standIn: Object3D; readonly tinted?: Material }> = [];
|
|
236
|
+
for (let i = 0; i < object.count; i++) {
|
|
237
|
+
object.getMatrixAt(i, matrix);
|
|
238
|
+
const tinted = object.instanceColor === null ? undefined : material.clone();
|
|
239
|
+
if (tinted !== undefined && 'color' in tinted && tinted.color instanceof Color) {
|
|
240
|
+
tinted.color.multiply(object.getColorAt(i, new Color()));
|
|
241
|
+
}
|
|
242
|
+
const standIn = new Mesh(object.geometry, tinted ?? material);
|
|
243
|
+
standIn.matrix.multiplyMatrices(object.matrixWorld, matrix);
|
|
244
|
+
standIn.renderOrder = object.renderOrder;
|
|
245
|
+
standIns.push(tinted === undefined ? { standIn } : { standIn, tinted });
|
|
246
|
+
}
|
|
247
|
+
return standIns;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const number =
|
|
251
|
+
(precision: number) =>
|
|
252
|
+
(value: number): string =>
|
|
253
|
+
String(Number(value.toFixed(precision)));
|
|
254
|
+
|
|
255
|
+
/** A shape's paint, and for a mark its outline on the picture. */
|
|
256
|
+
type Item = { readonly paint: string; readonly d: string };
|
|
257
|
+
|
|
258
|
+
/** Widens each edge of a polygon on the picture by `overdraw` pixels, as `SVGRenderer.expand`. */
|
|
259
|
+
const widened = (points: ReadonlyArray<Vector2>): ReadonlyArray<Vector2> => {
|
|
260
|
+
const out = points.map((point) => point.clone());
|
|
261
|
+
for (const [i, from] of out.entries()) {
|
|
262
|
+
const to = out[(i + 1) % out.length];
|
|
263
|
+
const length = to === undefined ? 0 : from.distanceTo(to);
|
|
264
|
+
if (to === undefined || length === 0) continue;
|
|
265
|
+
const step = new Vector2().subVectors(to, from).multiplyScalar(overdraw / length);
|
|
266
|
+
to.add(step);
|
|
267
|
+
from.sub(step);
|
|
268
|
+
}
|
|
269
|
+
return out;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* A face's paint. An opaque one is also outlined in its colour, a little (the width is set
|
|
274
|
+
* once, on the group of every path), so that the pieces a face is cut into show no seams.
|
|
275
|
+
*/
|
|
276
|
+
const facetPaint = (fill: string, opacity: number): string =>
|
|
277
|
+
opacity < 1
|
|
278
|
+
? `fill="${fill}" fill-opacity="${String(Number(opacity.toFixed(2)))}"`
|
|
279
|
+
: `fill="${fill}" stroke="${fill}"`;
|
|
280
|
+
|
|
281
|
+
/** Where an object's bounds are centred in the world. */
|
|
282
|
+
const centreOf = (object: Object3D): Vector3 => {
|
|
283
|
+
if (!(object instanceof Mesh)) return new Vector3().setFromMatrixPosition(object.matrixWorld);
|
|
284
|
+
const geometry = object.geometry;
|
|
285
|
+
if (geometry.boundingSphere === null) geometry.computeBoundingSphere();
|
|
286
|
+
return (geometry.boundingSphere?.center.clone() ?? new Vector3()).applyMatrix4(
|
|
287
|
+
object.matrixWorld,
|
|
288
|
+
);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const isDrawn = (object: Object3D): boolean =>
|
|
292
|
+
object instanceof Mesh ||
|
|
293
|
+
object instanceof Line ||
|
|
294
|
+
object instanceof Points ||
|
|
295
|
+
object instanceof Sprite;
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Draws `description` as SVG text `width` by `height` pixels, at `at` seconds of scene time,
|
|
299
|
+
* or says why the description is invalid.
|
|
300
|
+
*/
|
|
301
|
+
export const previewSvg = (
|
|
302
|
+
description: SceneDescription,
|
|
303
|
+
options: PreviewOptions,
|
|
304
|
+
): PreviewResult => {
|
|
305
|
+
const { width, height } = options;
|
|
306
|
+
const graph = createSceneGraph({
|
|
307
|
+
...(options.store === undefined ? {} : { store: options.store }),
|
|
308
|
+
...(options.behaviours === undefined ? {} : { behaviours: options.behaviours }),
|
|
309
|
+
});
|
|
310
|
+
const applied = graph.reconcile(description);
|
|
311
|
+
if (applied._tag === 'Invalid') {
|
|
312
|
+
graph.dispose();
|
|
313
|
+
return applied;
|
|
314
|
+
}
|
|
315
|
+
graph.resize(width, height);
|
|
316
|
+
graph.tick(options.at ?? 0);
|
|
317
|
+
const { scene, camera } = graph;
|
|
318
|
+
scene.updateMatrixWorld();
|
|
319
|
+
camera.updateMatrixWorld();
|
|
320
|
+
|
|
321
|
+
const lights: Array<Light> = [];
|
|
322
|
+
const drawn: Array<Object3D> = [];
|
|
323
|
+
scene.traverseVisible((object) => {
|
|
324
|
+
if (object instanceof Light) lights.push(object);
|
|
325
|
+
else if (isDrawn(object)) drawn.push(object);
|
|
326
|
+
});
|
|
327
|
+
const colorSpace =
|
|
328
|
+
graph.renderSettings().colorSpace === 'linear' ? LinearSRGBColorSpace : SRGBColorSpace;
|
|
329
|
+
const painter: Painter = {
|
|
330
|
+
lights,
|
|
331
|
+
normalView: new Matrix3().getNormalMatrix(camera.matrixWorldInverse),
|
|
332
|
+
colorSpace,
|
|
333
|
+
};
|
|
334
|
+
const halfWidth = width / 2;
|
|
335
|
+
const halfHeight = height / 2;
|
|
336
|
+
const at = number(options.precision ?? 1);
|
|
337
|
+
const eye = new Vector3().setFromMatrixPosition(camera.matrixWorld);
|
|
338
|
+
const ahead = camera.getWorldDirection(new Vector3());
|
|
339
|
+
// Only what is past the near plane is drawn: the rest would project behind the eye.
|
|
340
|
+
const near: Plane = { normal: ahead, constant: ahead.dot(eye) + camera.near };
|
|
341
|
+
const xy = (x: number, y: number): string => `${at(x * halfWidth)},${at(-y * halfHeight)}`;
|
|
342
|
+
|
|
343
|
+
/** A face as lit, the part of it past the near plane; none for a material not drawn. */
|
|
344
|
+
function facetOf(
|
|
345
|
+
face: RenderableFace,
|
|
346
|
+
material: Material,
|
|
347
|
+
centre: Vector3,
|
|
348
|
+
opacity: number,
|
|
349
|
+
): Facet<Item> | undefined {
|
|
350
|
+
const color = faceColor(painter, face, material, centre);
|
|
351
|
+
if (color === undefined) return undefined;
|
|
352
|
+
const points = [face.v1, face.v2, face.v3].map((vertex) => vertex.positionWorld.clone());
|
|
353
|
+
const paint = facetPaint(styleOf(color, colorSpace), opacity);
|
|
354
|
+
return split(near, { _tag: 'Facet', points, item: { paint, d: '' } }).front;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Each object stands in alone, so that what it draws is known as one unit. What is not
|
|
358
|
+
// depth-tested (gizmos drawn over everything) is painted after the rest, by render
|
|
359
|
+
// order, as three draws it.
|
|
360
|
+
const projector = new Projector();
|
|
361
|
+
const holder = new Scene();
|
|
362
|
+
const units: Array<Unit<Item>> = [];
|
|
363
|
+
const over: Array<{ readonly renderOrder: number; readonly item: Facet<Item> | Mark<Item> }> = [];
|
|
364
|
+
const tinted: Array<Material> = [];
|
|
365
|
+
for (const object of drawn) {
|
|
366
|
+
for (const { standIn, tinted: copy } of standInsFor(object)) {
|
|
367
|
+
if (copy !== undefined) tinted.push(copy);
|
|
368
|
+
standIn.matrixAutoUpdate = false;
|
|
369
|
+
holder.add(standIn);
|
|
370
|
+
const projected = projector.projectScene(holder, camera, false, true);
|
|
371
|
+
holder.remove(standIn);
|
|
372
|
+
const centre = centreOf(standIn);
|
|
373
|
+
const facets: Array<Facet<Item>> = [];
|
|
374
|
+
const marks: Array<Mark<Item>> = [];
|
|
375
|
+
const keep = (material: Material, item: Facet<Item> | Mark<Item>): void => {
|
|
376
|
+
if (!material.depthTest) over.push({ renderOrder: standIn.renderOrder, item });
|
|
377
|
+
else if (item._tag === 'Facet') facets.push(item);
|
|
378
|
+
else marks.push(item);
|
|
379
|
+
};
|
|
380
|
+
for (const element of projected.elements) {
|
|
381
|
+
const material: Material | null = element.material;
|
|
382
|
+
if (material === null || material.opacity === 0) continue;
|
|
383
|
+
const opacity = opacityOf(material);
|
|
384
|
+
const faded = opacity < 1 ? ` fill-opacity="${at(opacity)}"` : '';
|
|
385
|
+
if (element instanceof RenderableFace) {
|
|
386
|
+
const seen = facetOf(element, material, centre, opacity);
|
|
387
|
+
if (seen === undefined) continue;
|
|
388
|
+
keep(material, seen);
|
|
389
|
+
} else if (element instanceof RenderableLine && material instanceof LineBasicMaterial) {
|
|
390
|
+
const a = element.v1.positionScreen;
|
|
391
|
+
const b = element.v2.positionScreen;
|
|
392
|
+
const dashes =
|
|
393
|
+
material instanceof LineDashedMaterial
|
|
394
|
+
? ` stroke-dasharray="${at(material.dashSize)},${at(material.gapSize)}"`
|
|
395
|
+
: '';
|
|
396
|
+
const stroked = opacity < 1 ? ` stroke-opacity="${at(opacity)}"` : '';
|
|
397
|
+
keep(material, {
|
|
398
|
+
_tag: 'Mark',
|
|
399
|
+
at: element.v1.positionWorld.clone().lerp(element.v2.positionWorld, 0.5),
|
|
400
|
+
item: {
|
|
401
|
+
paint: `fill="none" stroke="${styleOf(material.color, colorSpace)}"${stroked} stroke-width="${at(material.linewidth)}" stroke-linecap="${material.linecap}"${dashes}`,
|
|
402
|
+
d: `M${xy(a.x, a.y)}L${xy(b.x, b.y)}`,
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
} else if (
|
|
406
|
+
element instanceof RenderableSprite &&
|
|
407
|
+
(material instanceof SpriteMaterial || material instanceof PointsMaterial)
|
|
408
|
+
) {
|
|
409
|
+
const size = material instanceof PointsMaterial ? material.size : 1;
|
|
410
|
+
const w = element.scale.x * halfWidth * size;
|
|
411
|
+
const h = element.scale.y * halfHeight * size;
|
|
412
|
+
const x = element.x * halfWidth;
|
|
413
|
+
const y = -element.y * halfHeight;
|
|
414
|
+
keep(material, {
|
|
415
|
+
_tag: 'Mark',
|
|
416
|
+
at: new Vector3(element.x, element.y, element.z).unproject(camera),
|
|
417
|
+
item: {
|
|
418
|
+
paint: `fill="${styleOf(material.color, colorSpace)}"${faded}`,
|
|
419
|
+
d: `M${at(x - w / 2)},${at(y - h / 2)}h${at(w)}v${at(h)}h${at(-w)}z`,
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (facets.length + marks.length > 0) units.push(unitOf(facets, marks, eye));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
for (const material of tinted) material.dispose();
|
|
428
|
+
graph.dispose();
|
|
429
|
+
|
|
430
|
+
const ordered = [
|
|
431
|
+
...paintOrder(units, eye),
|
|
432
|
+
...over.toSorted((a, b) => a.renderOrder - b.renderOrder).map(({ item }) => item),
|
|
433
|
+
];
|
|
434
|
+
|
|
435
|
+
// Consecutive shapes of one paint share a path, as `SVGRenderer.addPath` does.
|
|
436
|
+
const clip = new Box2(new Vector2(-halfWidth, -halfHeight), new Vector2(halfWidth, halfHeight));
|
|
437
|
+
const toPicture = (point: Vector3): Vector2 => {
|
|
438
|
+
const projectedPoint = point.clone().project(camera);
|
|
439
|
+
return new Vector2(projectedPoint.x * halfWidth, -projectedPoint.y * halfHeight);
|
|
440
|
+
};
|
|
441
|
+
/** A shape's outline on the picture, or none when it falls outside it. */
|
|
442
|
+
function outlineOf(shape: Facet<Item> | Mark<Item>): string | undefined {
|
|
443
|
+
if (shape._tag === 'Mark') return shape.item.d;
|
|
444
|
+
const corners = widened(shape.points.map(toPicture));
|
|
445
|
+
return new Box2().setFromPoints([...corners]).intersectsBox(clip)
|
|
446
|
+
? `M${corners.map((corner) => `${at(corner.x)},${at(corner.y)}`).join('L')}z`
|
|
447
|
+
: undefined;
|
|
448
|
+
}
|
|
449
|
+
const groups: Array<{ readonly paint: string; readonly outlines: Array<string> }> = [];
|
|
450
|
+
for (const shape of ordered) {
|
|
451
|
+
const outline = outlineOf(shape);
|
|
452
|
+
const last = groups.at(-1);
|
|
453
|
+
if (outline === undefined) continue;
|
|
454
|
+
if (last?.paint === shape.item.paint) last.outlines.push(outline);
|
|
455
|
+
else groups.push({ paint: shape.item.paint, outlines: [outline] });
|
|
456
|
+
}
|
|
457
|
+
const paths = groups.map(({ paint, outlines }) => `<path ${paint} d="${outlines.join('')}"/>`);
|
|
458
|
+
|
|
459
|
+
const background =
|
|
460
|
+
options.background === undefined || options.background === null
|
|
461
|
+
? ''
|
|
462
|
+
: `<rect x="${at(-halfWidth)}" y="${at(-halfHeight)}" width="${at(width)}" height="${at(height)}" fill="${styleOf(new Color(options.background), colorSpace)}"/>`;
|
|
463
|
+
return {
|
|
464
|
+
_tag: 'Preview',
|
|
465
|
+
svg: `<svg xmlns="http://www.w3.org/2000/svg" width="${at(width)}" height="${at(height)}" viewBox="${at(-halfWidth)} ${at(-halfHeight)} ${at(width)} ${at(height)}">${background}<g stroke-width="0.6" stroke-linejoin="round">${paths.join('')}</g></svg>`,
|
|
466
|
+
};
|
|
467
|
+
};
|
package/src/kernel/renderKit.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { Object3D, Scene } from 'three';
|
|
11
11
|
|
|
12
|
-
import type { ContactShadows, ShaderGridNode, Sky } from './description.ts';
|
|
12
|
+
import type { ContactShadows, LineNode, PointsNode, ShaderGridNode, Sky } from './description.ts';
|
|
13
13
|
import type { SceneRenderer } from './renderer.ts';
|
|
14
14
|
|
|
15
15
|
/** A sky as a kit draws it: the object the graph adds to its scene, never picked. */
|
|
@@ -26,6 +26,24 @@ export type HostedShaderGrid = {
|
|
|
26
26
|
readonly dispose: () => void;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
/** A line as a kit draws it (three's fat lines), inside the node's wrapper. */
|
|
30
|
+
export type HostedLine = {
|
|
31
|
+
readonly object: Object3D;
|
|
32
|
+
/** Applies new points or a new look; the wrapper carries the transform. */
|
|
33
|
+
readonly update: (line: LineNode) => void;
|
|
34
|
+
readonly dispose: () => void;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Points as a kit draws them, inside the node's wrapper. */
|
|
38
|
+
export type HostedPoints = {
|
|
39
|
+
readonly object: Object3D;
|
|
40
|
+
/** Applies a new look; the wrapper carries the transform. */
|
|
41
|
+
readonly update: (points: PointsNode) => void;
|
|
42
|
+
/** Moves sparkles and twinkles stars to scene time `seconds`. */
|
|
43
|
+
readonly tick: (seconds: number) => void;
|
|
44
|
+
readonly dispose: () => void;
|
|
45
|
+
};
|
|
46
|
+
|
|
29
47
|
/** The contact shadow passes for one scene, drawn by the frame loop before each frame. */
|
|
30
48
|
export type ContactShadowPasses = {
|
|
31
49
|
/** Draws the scene's contact shadows, if it has any, before the frame is drawn. */
|
|
@@ -50,5 +68,9 @@ export type RenderKit = {
|
|
|
50
68
|
readonly shading: 'webgl' | 'tsl';
|
|
51
69
|
readonly sky: () => HostedSky;
|
|
52
70
|
readonly shaderGrid: (node: ShaderGridNode) => HostedShaderGrid;
|
|
71
|
+
/** Draws a line; a kit without lines leaves them empty. */
|
|
72
|
+
readonly line?: (node: LineNode) => HostedLine;
|
|
73
|
+
/** Draws points; a kit without points leaves them empty. */
|
|
74
|
+
readonly points?: (node: PointsNode) => HostedPoints;
|
|
53
75
|
readonly contactShadows: (graph: ShadowedScene, renderer: SceneRenderer) => ContactShadowPasses;
|
|
54
76
|
};
|
package/src/kernel/renderer.ts
CHANGED
|
@@ -43,6 +43,12 @@ export type SceneRenderer = {
|
|
|
43
43
|
* otherwise across the whole surface.
|
|
44
44
|
*/
|
|
45
45
|
readonly render: (scene: Scene, camera: Camera, area?: RenderArea, target?: RenderTarget) => void;
|
|
46
|
+
/**
|
|
47
|
+
* Draws `scene` over what the surface already shows, across the whole of it: keeping
|
|
48
|
+
* the colour, clearing only depth (a HUD, as Drei's `Hud` draws). Without it, a HUD is
|
|
49
|
+
* not drawn. Not while presenting immersively.
|
|
50
|
+
*/
|
|
51
|
+
readonly renderOver?: (scene: Scene, camera: Camera) => void;
|
|
46
52
|
/** Creates an offscreen target of `width` × `height` pixels to render into. */
|
|
47
53
|
readonly createTarget: (width: number, height: number) => RenderTarget;
|
|
48
54
|
/**
|