phoria2 1.2.0 → 1.3.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 -3
- package/dist/charts.js +476 -459
- package/dist/charts.js.map +1 -1
- package/dist/core.js +1192 -963
- package/dist/core.js.map +1 -1
- package/dist/edit.js +75 -75
- package/dist/edit.js.map +1 -1
- package/dist/explain.js +1 -1
- package/dist/gltf.js +84 -84
- package/dist/gltf.js.map +1 -1
- package/dist/keyboard.js +188 -0
- package/dist/keyboard.js.map +1 -0
- package/dist/motion.js +35 -35
- package/dist/motion.js.map +1 -1
- package/dist/particles.js +191 -118
- package/dist/particles.js.map +1 -1
- package/dist/passes.js +16 -16
- package/dist/passes.js.map +1 -1
- package/dist/phoria2.js +1 -1
- package/dist/picking.js +30 -14
- package/dist/picking.js.map +1 -1
- package/dist/snapshot.js +1 -1
- package/dist/tooltip.js +1 -1
- package/dist/types/View.d.ts +11 -0
- package/dist/types/area.d.ts +18 -0
- package/dist/types/geometry/Geometry.d.ts +16 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/material/Material.d.ts +15 -0
- package/dist/types/material/Texture.d.ts +18 -2
- package/dist/types/particles/Emitter.d.ts +39 -2
- package/dist/types/scene/drawables.d.ts +23 -2
- package/dist/types/webgl/resources.d.ts +12 -2
- package/dist/types/webgl/shaders.d.ts +7 -2
- package/llms-full.txt +48 -1
- package/package.json +1 -1
- package/schema/scene.schema.json +48 -0
- package/src/View.js +16 -0
- package/src/area.js +131 -0
- package/src/charts/marks.js +29 -2
- package/src/geometry/Geometry.js +16 -1
- package/src/index.js +1 -0
- package/src/material/Material.js +9 -2
- package/src/material/Texture.js +55 -2
- package/src/particles/Emitter.js +121 -10
- package/src/scene/drawables.js +68 -4
- package/src/watch.js +8 -1
- package/src/webgl/WebGLRenderer.js +36 -5
- package/src/webgl/picking.js +22 -1
- package/src/webgl/resources.js +168 -14
- package/src/webgl/shaders.js +110 -11
|
@@ -61,6 +61,28 @@ export type EmitterOptions = NodeOptions & SpawnOptions & {
|
|
|
61
61
|
frustumCulled?: boolean;
|
|
62
62
|
renderOrder?: number;
|
|
63
63
|
onParticle?: ((p: Particle) => boolean | void) | null;
|
|
64
|
+
sort?: boolean;
|
|
65
|
+
} & LookOptions;
|
|
66
|
+
export type LookOptions = {
|
|
67
|
+
/**
|
|
68
|
+
* a multiple of the material's size, or several each particle goes
|
|
69
|
+
* through over its life - [0.5, 3] for smoke that billows out (default 1)
|
|
70
|
+
*/
|
|
71
|
+
size?: number | number[];
|
|
72
|
+
/**
|
|
73
|
+
* each particle's size varies by up to this share either way, 0-1 (default 0)
|
|
74
|
+
*/
|
|
75
|
+
sizeSpread?: number;
|
|
76
|
+
/**
|
|
77
|
+
* 0-1, or several each particle goes through - [0, 1, 0] fades in
|
|
78
|
+
* and out (default 1)
|
|
79
|
+
*/
|
|
80
|
+
opacity?: number | number[];
|
|
81
|
+
/**
|
|
82
|
+
* radians per second: a number turns each particle up to that
|
|
83
|
+
* fast either way, [min, max] a range. Spinning particles start at a random angle (default 0)
|
|
84
|
+
*/
|
|
85
|
+
spin?: number | [number, number];
|
|
64
86
|
};
|
|
65
87
|
/**
|
|
66
88
|
* Particles drawn as points. Add it to a scene and it runs itself.
|
|
@@ -102,11 +124,11 @@ export declare class Emitter extends Points {
|
|
|
102
124
|
/**
|
|
103
125
|
* Change several settings at once - a preset, say. Vectors are copied into the emitter's own,
|
|
104
126
|
* so anything bound to `emitter.gravity` and friends stays bound. Live particles carry on.
|
|
105
|
-
* @param {SpawnOptions & { rate?: number, gravity?: Vec3Input, drag?: number, follow?: Node | null,
|
|
127
|
+
* @param {SpawnOptions & LookOptions & { rate?: number, gravity?: Vec3Input, drag?: number, follow?: Node | null,
|
|
106
128
|
* onParticle?: ((p: Particle) => boolean | void) | null }} options
|
|
107
129
|
* @returns {this}
|
|
108
130
|
*/
|
|
109
|
-
set(options: SpawnOptions & {
|
|
131
|
+
set(options: SpawnOptions & LookOptions & {
|
|
110
132
|
rate?: number;
|
|
111
133
|
gravity?: Vec3Input;
|
|
112
134
|
drag?: number;
|
|
@@ -121,6 +143,21 @@ export declare class Emitter extends Points {
|
|
|
121
143
|
set color(v: ColorInput | ColorInput[]);
|
|
122
144
|
/** @returns {[number, number, number][]} */
|
|
123
145
|
get color(): [number, number, number][];
|
|
146
|
+
/** Each particle's size over its life, as multiples of the material's. @param {number | number[]} v */
|
|
147
|
+
set size(v: number | number[]);
|
|
148
|
+
/** @returns {number[]} */
|
|
149
|
+
get size(): number[];
|
|
150
|
+
/** How much each particle's size varies, 0-1 either way. @param {number} v */
|
|
151
|
+
set sizeSpread(v: number);
|
|
152
|
+
get sizeSpread(): number;
|
|
153
|
+
/** Each particle's opacity over its life. @param {number | number[]} v */
|
|
154
|
+
set opacity(v: number | number[]);
|
|
155
|
+
/** @returns {number[]} */
|
|
156
|
+
get opacity(): number[];
|
|
157
|
+
/** How fast particles turn, radians per second, as a [min, max] range. @param {number | [number, number]} v */
|
|
158
|
+
set spin(v: number | [number, number]);
|
|
159
|
+
/** @returns {[number, number]} */
|
|
160
|
+
get spin(): [number, number];
|
|
124
161
|
/**
|
|
125
162
|
* Birth `count` particles at once - explosions, splashes. Options override the emitter's own
|
|
126
163
|
* spawn settings for this burst only.
|
|
@@ -238,19 +238,40 @@ export declare class Points extends Drawable {
|
|
|
238
238
|
static get OPTIONS(): string[];
|
|
239
239
|
/** A label per point from its row of data - or a list of them, several per point. @type {ReturnType<typeof instanceLabels>} */
|
|
240
240
|
labels: ReturnType<typeof instanceLabels>;
|
|
241
|
-
/**
|
|
241
|
+
/**
|
|
242
|
+
* Draw the points back to front, furthest first, each frame - for soft points that cover what is
|
|
243
|
+
* behind them (smoke, with blending 'normal'), which then layer the right way round. Drawn as squares.
|
|
244
|
+
*/
|
|
245
|
+
sort: boolean;
|
|
246
|
+
/** @param {DrawableOptions & { labels?: InstanceLabels | InstanceLabels[] | null, sort?: boolean }} options */
|
|
242
247
|
constructor(options: DrawableOptions & {
|
|
243
248
|
labels?: InstanceLabels | InstanceLabels[] | null;
|
|
249
|
+
sort?: boolean;
|
|
244
250
|
});
|
|
245
251
|
/** @returns {'points'} */
|
|
246
252
|
get drawMode(): 'points';
|
|
247
253
|
}
|
|
254
|
+
export type LinesOptions = DrawableOptions & {
|
|
255
|
+
arrows?: 'none' | 'start' | 'end' | 'both';
|
|
256
|
+
arrowSize?: number;
|
|
257
|
+
};
|
|
258
|
+
/**
|
|
259
|
+
* @typedef {DrawableOptions & { arrows?: 'none' | 'start' | 'end' | 'both', arrowSize?: number }} LinesOptions
|
|
260
|
+
*/
|
|
248
261
|
/**
|
|
249
262
|
* Line segments: indices (or consecutive vertices) in pairs. Width is in pixels and works on
|
|
250
|
-
* every platform. edges(geometry) makes a wireframe of any geometry.
|
|
263
|
+
* every platform. edges(geometry) makes a wireframe of any geometry. Segments each starting where
|
|
264
|
+
* the one before ended make a chain - a polyline - which dashes run on along and arrowheads finish.
|
|
251
265
|
*/
|
|
252
266
|
export declare class Lines extends Drawable {
|
|
253
267
|
static type: string;
|
|
268
|
+
static get OPTIONS(): string[];
|
|
269
|
+
/** Arrowheads at the ends of each chain: at its 'start', its 'end', 'both' or 'none'. @type {'none' | 'start' | 'end' | 'both'} */
|
|
270
|
+
arrows: 'none' | 'start' | 'end' | 'both';
|
|
271
|
+
/** Arrowhead length in pixels; 0 sizes them from the material's width. */
|
|
272
|
+
arrowSize: number;
|
|
273
|
+
/** @param {LinesOptions} options */
|
|
274
|
+
constructor(options: LinesOptions);
|
|
254
275
|
/** @returns {'lines'} */
|
|
255
276
|
get drawMode(): 'lines';
|
|
256
277
|
}
|
|
@@ -101,8 +101,8 @@ export declare class Resources {
|
|
|
101
101
|
count: number;
|
|
102
102
|
} | null, withTangents?: boolean): number;
|
|
103
103
|
/**
|
|
104
|
-
* Vertex array for Lines: one instance per segment with its two endpoints (and colours)
|
|
105
|
-
* expanded to a screen-space quad in the vertex shader.
|
|
104
|
+
* Vertex array for Lines: one instance per segment with its two endpoints (and colours) and where
|
|
105
|
+
* it is along its chain, expanded to a screen-space quad - and arrowheads - in the vertex shader.
|
|
106
106
|
* @param {Geometry} geometry
|
|
107
107
|
* @returns {MeshBuffers}
|
|
108
108
|
*/
|
|
@@ -119,6 +119,16 @@ export declare class Resources {
|
|
|
119
119
|
* @param {object} owner @param {Scene} scene
|
|
120
120
|
*/
|
|
121
121
|
release(owner: object, scene: Scene): void;
|
|
122
|
+
/**
|
|
123
|
+
* Vertex array for points drawn as squares: one instance a point - its position, colour, size,
|
|
124
|
+
* opacity, angle and index - over the corner strip lines use. Given a model-view matrix, the points
|
|
125
|
+
* go back to front (furthest first) - re-sorted every call, as the camera may have moved; without
|
|
126
|
+
* one, in their own order; with undefined, in whatever order they were last sent (picking, which
|
|
127
|
+
* reads each point's index and doesn't mind).
|
|
128
|
+
* @param {Geometry} geometry @param {ArrayLike<number> | null} [modelView]
|
|
129
|
+
* @returns {MeshBuffers}
|
|
130
|
+
*/
|
|
131
|
+
sprites(geometry: Geometry, modelView?: ArrayLike<number> | null): MeshBuffers;
|
|
122
132
|
/** Forget everything - after a lost context, when every GL object is already gone. */
|
|
123
133
|
reset(): void;
|
|
124
134
|
}
|
|
@@ -8,11 +8,16 @@ export declare const ATTRIBUTES: {
|
|
|
8
8
|
instanceColor: number;
|
|
9
9
|
tangent: number;
|
|
10
10
|
uv2: number;
|
|
11
|
+
pointSize: number;
|
|
12
|
+
pointOpacity: number;
|
|
13
|
+
pointAngle: number;
|
|
14
|
+
pointIndex: number;
|
|
11
15
|
segmentStart: number;
|
|
12
16
|
segmentEnd: number;
|
|
13
17
|
segmentColorStart: number;
|
|
14
18
|
segmentColorEnd: number;
|
|
15
19
|
corner: number;
|
|
20
|
+
segmentInfo: number;
|
|
16
21
|
};
|
|
17
22
|
export declare const CLIP = "\n#ifdef CLIP\nuniform vec4 uClipPlane; // world space, unit normal: cut away where dot(n, p) + d > 0\nvoid clipAway(vec3 p) {\n if (dot(uClipPlane.xyz, p) + uClipPlane.w > 0.0) discard;\n}\n#else\nvoid clipAway(vec3 p) {}\n#endif\n";
|
|
18
23
|
export declare const FOG = "\n#ifdef FOG\nuniform vec3 uFogColor; // linear\nuniform vec2 uFogRange; // linear fog: near, far\nuniform float uFogDensity; // exponential fog\nin float vFogDepth;\nvec3 fogged(vec3 c) {\n#ifdef FOG_LINEAR\n float f = clamp((vFogDepth - uFogRange.x) / max(uFogRange.y - uFogRange.x, 1e-4), 0.0, 1.0);\n#else\n float d = uFogDensity * vFogDepth;\n float f = 1.0 - exp(-d * d);\n#endif\n#ifdef FOG_ADD\n return c * (1.0 - f);\n#elif defined(FOG_MULTIPLY)\n // a filter in the fog fades to white, which leaves what is behind it as it was\n return mix(c, vec3(1.0), f);\n#else\n return mix(c, uFogColor, f);\n#endif\n}\n#else\nvec3 fogged(vec3 c) { return c; }\n#endif\n";
|
|
@@ -22,8 +27,8 @@ export declare const EQUIRECT = "\nvec2 equirectUv(vec3 d) {\n return vec2(atan
|
|
|
22
27
|
export declare const FULLSCREEN_VERTEX = "\nout vec2 vUv;\nvoid main() {\n vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));\n vUv = p;\n gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);\n}\n";
|
|
23
28
|
export declare const TONE_MAP = "\nuniform float uExposure;\nuniform int uToneMapping; // 0 none, 1 aces, 2 neutral\n\nvec3 toneMap(vec3 c) {\n c *= uExposure;\n if (uToneMapping == 1) {\n // ACES filmic, Narkowicz's fit\n c = clamp((c * (2.51 * c + 0.03)) / (c * (2.43 * c + 0.59) + 0.14), 0.0, 1.0);\n } else if (uToneMapping == 2) {\n // Khronos PBR Neutral\n float x = min(c.r, min(c.g, c.b));\n float offset = x < 0.08 ? x - 6.25 * x * x : 0.04;\n c -= offset;\n float peak = max(c.r, max(c.g, c.b));\n if (peak > 0.8) {\n float d = 0.2;\n float newPeak = 1.0 - d * d / (peak + d - 0.8);\n c *= newPeak / peak;\n float g = 1.0 - 1.0 / (0.15 * (peak - newPeak) + 1.0);\n c = mix(c, vec3(newPeak), g);\n }\n }\n return c;\n}\n";
|
|
24
29
|
export declare const MESH_VERTEX = "\nlayout(location = 0) in vec3 position;\nlayout(location = 1) in vec3 normal;\nlayout(location = 2) in vec2 uv;\nlayout(location = 3) in vec3 color;\n#ifdef INSTANCED\nlayout(location = 4) in mat4 instanceMatrix;\nlayout(location = 8) in vec3 instanceColor;\n#endif\n#ifdef ANISOTROPY\nlayout(location = 9) in vec4 tangent;\nout vec4 vTangent;\n#endif\n#ifdef UV2\nlayout(location = 10) in vec2 uv2;\nout vec2 vUv2;\n#endif\n\nuniform mat4 uModel;\nuniform mat3 uNormalMatrix;\nuniform mat4 uViewProjection;\nuniform int uHighlightIndex; // -2 none, -1 the whole node, otherwise the instance\n#ifdef FOG\nuniform mat4 uView;\nout float vFogDepth;\n#endif\n\nout vec3 vWorldPosition;\nout vec3 vNormal;\nout vec2 vUv;\nout vec3 vColor;\nflat out float vHighlight;\n#ifdef TRIPLANAR\n// the mesh's own space, so projected maps stay on it as it moves; and how its normals turn into the world\nout vec3 vObjectPosition;\nout vec3 vObjectNormal;\nout mat3 vObjectToWorld;\n#endif\n#ifdef PICK\nflat out uint vPickElement; // which instance, point or segment, for GPU picking\n#endif\n\n\nvec3 srgbToLinear(vec3 c) {\n return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));\n}\nvec3 linearToSrgb(vec3 c) {\n c = clamp(c, 0.0, 1.0);\n return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));\n}\n\n\n// Matrices times vectors only - a matrix times a matrix per vertex is four times the work, and\n// with 100,000 instances the vertex stage is most of the frame.\nvoid main() {\n vColor = vec3(1.0);\n vHighlight = uHighlightIndex == -1 ? 1.0 : 0.0;\n#ifdef INSTANCED\n vec4 local = instanceMatrix * vec4(position, 1.0);\n // the instance's normal matrix as its cofactor: the inverse transpose up to scale, from three\n // cross products, flipped back for mirroring transforms\n mat3 m = mat3(instanceMatrix);\n mat3 instanceNormal = mat3(cross(m[1], m[2]), cross(m[2], m[0]), cross(m[0], m[1])) * sign(dot(m[0], cross(m[1], m[2])));\n vec3 n = instanceNormal * normal;\n vColor *= instanceColor;\n if (gl_InstanceID == uHighlightIndex) vHighlight = 1.0;\n#else\n vec4 local = vec4(position, 1.0);\n vec3 n = normal;\n#endif\n#ifdef VERTEX_COLORS\n vColor *= srgbToLinear(color);\n#endif\n vec4 world = uModel * local;\n vWorldPosition = world.xyz;\n vNormal = uNormalMatrix * n;\n vUv = uv;\n#ifdef UV2\n vUv2 = uv2;\n#endif\n#ifdef ANISOTROPY\n // a direction along the surface, so it turns as the model does\n#ifdef INSTANCED\n vTangent = vec4(mat3(uModel) * (m * tangent.xyz), tangent.w);\n#else\n vTangent = vec4(mat3(uModel) * tangent.xyz, tangent.w);\n#endif\n#endif\n#ifdef TRIPLANAR\n vObjectPosition = position;\n vObjectNormal = normal;\n#ifdef INSTANCED\n vObjectToWorld = uNormalMatrix * instanceNormal;\n#else\n vObjectToWorld = uNormalMatrix;\n#endif\n#endif\n gl_Position = uViewProjection * world;\n#ifdef FOG\n vFogDepth = -(uView * world).z;\n#endif\n#ifdef PICK\n vPickElement = uint(gl_InstanceID);\n#endif\n}\n";
|
|
25
|
-
export declare const POINTS_VERTEX = "\nlayout(location = 0) in vec3 position;\nlayout(location = 3) in vec3 color;\n\nuniform mat4 uModel;\nuniform mat4 uView;\nuniform mat4 uProjection;\nuniform float uSize; // pixels (with the pixel ratio applied), or world units when attenuated\nuniform float uScale; // projection focal term x half the buffer height: world size -> pixels at depth 1\nuniform int uHighlightIndex; // -2 none, -1 every point, otherwise the point\n\nout vec3 vColor;\nflat out float vHighlight;\n#ifdef PICK\nflat out uint vPickElement; // which instance, point or segment, for GPU picking\n#endif\n#ifdef FOG\nout float vFogDepth;\n#endif\n\n\nvec3 srgbToLinear(vec3 c) {\n return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));\n}\nvec3 linearToSrgb(vec3 c) {\n c = clamp(c, 0.0, 1.0);\n return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));\n}\n\n\nvoid main() {\n vec4 viewPosition = uView * (uModel * vec4(position, 1.0));\n gl_Position = uProjection * viewPosition;\n#ifdef FOG\n vFogDepth = -viewPosition.z;\n#endif\n#ifdef SIZE_ATTENUATION\n // perspective shrinks with distance; an orthographic projection (w row 0, 0, 0, 1) doesn't\n float depth = uProjection[3][3] == 1.0 ? 1.0 : max(-viewPosition.z, 1e-4);\n gl_PointSize = uSize * uScale / depth;\n#else\n gl_PointSize = uSize;\n#endif\n vColor = vec3(1.0);\n#ifdef VERTEX_COLORS\n vColor = srgbToLinear(color);\n#endif\n vHighlight = uHighlightIndex == -1 ||
|
|
26
|
-
export declare const LINES_VERTEX = "\nlayout(location = 0) in vec3 segmentStart;\nlayout(location = 1) in vec3 segmentEnd;\nlayout(location = 2) in vec3 segmentColorStart;\nlayout(location = 3) in vec3 segmentColorEnd;\
|
|
30
|
+
export declare const POINTS_VERTEX = "\nlayout(location = 0) in vec3 position;\nlayout(location = 3) in vec3 color;\n#ifdef SPRITES\n// each point a square: one instance a point, drawn as a strip over the corners (along 0-1, across -1-1)\nlayout(location = 4) in vec2 corner;\nlayout(location = 11) in float pointSize;\nlayout(location = 12) in float pointOpacity;\nlayout(location = 13) in float pointAngle;\nlayout(location = 14) in float pointIndex;\nuniform vec2 uResolution; // drawing buffer size in pixels\nout vec2 vUv;\nout float vOpacity;\n#endif\n\nuniform mat4 uModel;\nuniform mat4 uView;\nuniform mat4 uProjection;\nuniform float uSize; // pixels (with the pixel ratio applied), or world units when attenuated\nuniform float uScale; // projection focal term x half the buffer height: world size -> pixels at depth 1\nuniform int uHighlightIndex; // -2 none, -1 every point, otherwise the point\n\nout vec3 vColor;\nflat out float vHighlight;\n#ifdef PICK\nflat out uint vPickElement; // which instance, point or segment, for GPU picking\n#endif\n#ifdef FOG\nout float vFogDepth;\n#endif\n\n\nvec3 srgbToLinear(vec3 c) {\n return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));\n}\nvec3 linearToSrgb(vec3 c) {\n c = clamp(c, 0.0, 1.0);\n return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));\n}\n\n\nvoid main() {\n vec4 viewPosition = uView * (uModel * vec4(position, 1.0));\n#ifdef SPRITES\n // the square's corner, turned by the point's angle: the texture turns with it\n vec2 c = vec2(corner.x * 2.0 - 1.0, corner.y);\n float s = sin(pointAngle), k = cos(pointAngle);\n vec2 turned = vec2(c.x * k - c.y * s, c.x * s + c.y * k);\n float size = uSize * pointSize;\n#ifdef SIZE_ATTENUATION\n // world units: the square sits in the view, facing the camera\n viewPosition.xy += turned * size * 0.5;\n gl_Position = uProjection * viewPosition;\n#else\n // pixels: the square is laid out on the screen\n gl_Position = uProjection * viewPosition;\n gl_Position.xy += turned * size / uResolution * gl_Position.w;\n#endif\n // the image upright on the square, its top at the top, as a point's is\n vUv = vec2(c.x * 0.5 + 0.5, 0.5 - c.y * 0.5);\n vOpacity = pointOpacity;\n int self = int(pointIndex + 0.5);\n#else\n gl_Position = uProjection * viewPosition;\n int self = gl_VertexID;\n#endif\n#ifdef FOG\n vFogDepth = -viewPosition.z;\n#endif\n#ifdef SPRITES\n#elif defined(SIZE_ATTENUATION)\n // perspective shrinks with distance; an orthographic projection (w row 0, 0, 0, 1) doesn't\n float depth = uProjection[3][3] == 1.0 ? 1.0 : max(-viewPosition.z, 1e-4);\n gl_PointSize = uSize * uScale / depth;\n#else\n gl_PointSize = uSize;\n#endif\n vColor = vec3(1.0);\n#ifdef VERTEX_COLORS\n vColor = srgbToLinear(color);\n#endif\n vHighlight = uHighlightIndex == -1 || self == uHighlightIndex ? 1.0 : 0.0;\n#ifdef PICK\n vPickElement = uint(self);\n#endif\n}\n";
|
|
31
|
+
export declare const LINES_VERTEX = "\nlayout(location = 0) in vec3 segmentStart;\nlayout(location = 1) in vec3 segmentEnd;\nlayout(location = 2) in vec3 segmentColorStart;\nlayout(location = 3) in vec3 segmentColorEnd;\n// the distance along its chain (segments joined end to start) where it starts, in the model's\n// units; 1 if it starts a chain; 1 if it ends one\nlayout(location = 5) in vec3 segmentInfo;\n\nuniform mat4 uModel;\nuniform mat4 uView;\nuniform mat4 uProjection;\nuniform vec2 uResolution; // drawing buffer size in pixels\nuniform float uWidth; // pixels, already scaled by the pixel ratio\nuniform float uNear;\nuniform int uArrows; // arrowheads: 1 at the start of each chain, 2 at its end, 3 at both\nuniform float uArrowSize; // their length in pixels, scaled by the pixel ratio\n\n// six vertices an instance make the segment's quad, as two triangles - x: 0 at its start, 1 at its\n// end; y: -1 or 1 across - and six more its two arrowheads, drawn only when asked for\nconst vec2 QUAD[6] = vec2[6](vec2(0.0, -1.0), vec2(0.0, 1.0), vec2(1.0, -1.0), vec2(1.0, -1.0), vec2(0.0, 1.0), vec2(1.0, 1.0));\n\nout vec3 vColor;\n#ifdef DASHED\nout float vDistance; // along the chain, in the model's units\nflat out float vArrow; // 1 on an arrowhead, which is never dashed\n#endif\n#ifdef PICK\nflat out uint vPickElement; // which instance, point or segment, for GPU picking\n#endif\n#ifdef FOG\nout float vFogDepth;\n#endif\n\n\nvec3 srgbToLinear(vec3 c) {\n return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)), step(0.04045, c));\n}\nvec3 linearToSrgb(vec3 c) {\n c = clamp(c, 0.0, 1.0);\n return mix(c * 12.92, 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, c));\n}\n\n\nvoid main() {\n vec4 a = uView * (uModel * vec4(segmentStart, 1.0));\n vec4 b = uView * (uModel * vec4(segmentEnd, 1.0));\n // clip the segment to the near plane so lines passing behind the camera stay correct\n float clipZ = -uNear;\n if (a.z > clipZ && b.z > clipZ) {\n gl_Position = vec4(2.0, 2.0, 2.0, 1.0); // wholly behind: off screen\n return;\n }\n if (a.z > clipZ) a = mix(a, b, (clipZ - a.z) / (b.z - a.z));\n if (b.z > clipZ) b = mix(b, a, (clipZ - b.z) / (a.z - b.z));\n\n vec4 ca = uProjection * a;\n vec4 cb = uProjection * b;\n vec2 sa = ca.xy / ca.w * uResolution * 0.5;\n vec2 sb = cb.xy / cb.w * uResolution * 0.5;\n vec2 dir = sb - sa;\n float len = length(dir);\n dir = len > 0.0 ? dir / len : vec2(1.0, 0.0);\n vec2 normal = vec2(-dir.y, dir.x);\n\n bool arrowStart = (uArrows & 1) != 0 && segmentInfo.y > 0.5;\n bool arrowEnd = (uArrows & 2) != 0 && segmentInfo.z > 0.5;\n float along, arrow = 0.0;\n vec2 offset;\n if (gl_VertexID < 6) {\n vec2 c = QUAD[gl_VertexID];\n along = c.x;\n // past each end by half the width so joins look continuous - or short of it, under an arrowhead\n float startOut = arrowStart ? -uArrowSize * 0.8 : uWidth * 0.5;\n float endOut = arrowEnd ? -uArrowSize * 0.8 : uWidth * 0.5;\n offset = normal * c.y * uWidth * 0.5 + dir * (along > 0.5 ? endOut : -startOut);\n } else {\n // an arrowhead: its tip on the chain's end, pointing out along the segment\n int which = (gl_VertexID - 6) / 3, k = gl_VertexID - 6 - which * 3;\n if (which == 0 ? !arrowStart : !arrowEnd) {\n gl_Position = vec4(2.0, 2.0, 2.0, 1.0);\n return;\n }\n along = float(which);\n vec2 d = which == 1 ? dir : -dir;\n float wing = uArrowSize * 0.45;\n offset = k == 0 ? vec2(0.0) : -d * uArrowSize + normal * (k == 1 ? wing : -wing);\n arrow = 1.0;\n }\n\n vec4 clip = mix(ca, cb, along);\n#ifdef FOG\n vFogDepth = -mix(a.z, b.z, along);\n#endif\n clip.xy += offset / (uResolution * 0.5) * clip.w;\n gl_Position = clip;\n#ifdef DASHED\n vDistance = segmentInfo.x + length(segmentEnd - segmentStart) * along;\n vArrow = arrow;\n#endif\n#ifdef PICK\n vPickElement = uint(gl_InstanceID);\n#endif\n\n vColor = vec3(1.0);\n#ifdef VERTEX_COLORS\n vColor = srgbToLinear(mix(segmentColorStart, segmentColorEnd, along));\n#endif\n}\n";
|
|
27
32
|
export type ShaderKind = 'mesh' | 'points' | 'lines';
|
|
28
33
|
/**
|
|
29
34
|
* Vertex and fragment source for a kind of drawable, or a pass's own pair, with a set of feature defines.
|
package/llms-full.txt
CHANGED
|
@@ -139,6 +139,8 @@ Methods:
|
|
|
139
139
|
- `on('click' | 'hover', (hit, event) => ...)` - see Interaction. Returns an unsubscribe function.
|
|
140
140
|
- `tooltip(fn)` - set the tooltip function later.
|
|
141
141
|
- `pick(x, y)` / `pick(event)` - the hit under a canvas position; `pickAll` for every hit.
|
|
142
|
+
- `pickArea(area)` - every hit inside a box `{ x, y, width, height }` or a lasso of `[x, y]` points
|
|
143
|
+
(CSS pixels), nearest first - see Interaction.
|
|
142
144
|
- `rayAt(x, y)` / `rayAt(event)` - the world `Ray` through a canvas position, for placing things:
|
|
143
145
|
`ray.at(ray.intersectPlane([0, 1, 0], [0, 0, 0]))` is the point on the floor under the pointer
|
|
144
146
|
(`intersectPlane` returns null when the ray runs along the plane or away from it).
|
|
@@ -222,6 +224,24 @@ new Lines({ geometry: edges(box(2)), material: { color: '#333', width: 2 } })
|
|
|
222
224
|
new Label({ text: 'Rack A1', position: [0, 2, 0], className: 'my-label', place: 'above' })
|
|
223
225
|
```
|
|
224
226
|
|
|
227
|
+
**Lines** dashed and with arrowheads - flow and direction in network diagrams. Dashes are material
|
|
228
|
+
options in the model's units: `dash` (0 is solid), `gap` (0 is the dash's length) and `dashOffset`,
|
|
229
|
+
which moves them along - animate it for traffic. Arrowheads are Lines options: `arrows: 'start' |
|
|
230
|
+
'end' | 'both'` puts them at the ends of each chain (segments each starting where the one before
|
|
231
|
+
ended - a polyline is one chain, separate links are one each), `arrowSize` in pixels (0, the default,
|
|
232
|
+
sizes them from the width). The dashes run on along a chain, and arrowheads are never dashed.
|
|
233
|
+
|
|
234
|
+
```js
|
|
235
|
+
const flow = view.scene.add(new Lines({ geometry: links, arrows: 'end', material: { color: '#4cc9f0', width: 2, dash: 0.3, gap: 0.15 } }))
|
|
236
|
+
view.start((dt) => { flow.material.dashOffset += dt * 0.8 }) // traffic moving along each link
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**Points** each their own size, opacity or angle: give the geometry `sizes` (multiples of the material's
|
|
240
|
+
`size`), `opacities` (0-1) or `angles` (radians, turning the point's `map`), one per vertex. Such points
|
|
241
|
+
are drawn as camera-facing squares, so any size works and they stay whole at the edge of the view.
|
|
242
|
+
`sort: true` draws them furthest first each frame - for soft points with `blending: 'normal'`
|
|
243
|
+
(smoke, cloud) that would otherwise layer wrongly; with additive blending order doesn't matter.
|
|
244
|
+
|
|
225
245
|
**InstancedMesh** - many copies of one geometry in one draw call, for bars, racks, nodes:
|
|
226
246
|
|
|
227
247
|
```js
|
|
@@ -361,7 +381,10 @@ unless its `colorSpace` was set on purpose - `normalMap: await loadTexture('n.pn
|
|
|
361
381
|
|
|
362
382
|
**Canvas and video textures**: a texture's `image` can be a canvas - draw on it, then call
|
|
363
383
|
`texture.needsUpdate()` - or a `<video>`, whose frames upload by themselves while it plays, with
|
|
364
|
-
nothing to call.
|
|
384
|
+
nothing to call. For a big canvas drawn on a little at a time (paint, a map being revealed), say what
|
|
385
|
+
changed - `texture.needsUpdate({ x, y, width, height })`, in the canvas's pixels from its top left -
|
|
386
|
+
and only that rectangle is sent, rather than the whole canvas every frame (several calls before a
|
|
387
|
+
frame add up). `new Texture({ url: 'promo.mp4' })` (or .webm, .ogv, .mov, .m4v) makes the video
|
|
365
388
|
for you, muted and looping so browsers let it start by itself; `texture.image` is that `<video>` -
|
|
366
389
|
pause, seek or unmute it as any other. A browser that blocks autoplay leaves it on its first frame
|
|
367
390
|
until `texture.image.play()` from a click. Video textures make no mipmaps (they would be remade
|
|
@@ -537,6 +560,16 @@ view.on('hover', (hit) => show(hit?.datum)) // fires when the th
|
|
|
537
560
|
- The **nearest surface** under the pointer is the hit - a link drawn over a site's top wins the
|
|
538
561
|
hover there. To let one kind of thing through, give it `pickable: false`; to choose among
|
|
539
562
|
everything under the pointer, `view.pickAll(event)` lists every hit, nearest first.
|
|
563
|
+
- **Selecting an area** - `view.pickArea({ x, y, width, height })` (width and height may be negative,
|
|
564
|
+
as a drag up or left makes them) or `view.pickArea([[x, y], ...])` for a lasso: a hit for each point,
|
|
565
|
+
instance and line segment whose place on screen is inside, and each mesh whose middle is. Places, not
|
|
566
|
+
surfaces: what is hidden behind other things is found too, so a box over a scatter takes that whole
|
|
567
|
+
region. Hidden, unpickable and zero-scale instances are left out. Turn `view.controls.enabled` off
|
|
568
|
+
while the user drags out the area.
|
|
569
|
+
|
|
570
|
+
```js
|
|
571
|
+
const chosen = view.pickArea({ x: 40, y: 30, width: 200, height: 120 }).map((h) => h.datum)
|
|
572
|
+
```
|
|
540
573
|
- **Rays of your own** - line of sight, what's below a dropped object, picking in tests: a
|
|
541
574
|
`Raycaster` finds what any ray hits, on the CPU. Set `raycaster.ray.origin` and `.direction`, or aim
|
|
542
575
|
it with `setFromCamera([x, y], camera)` (-1 to 1, y up); `intersect(nodeOrNodes)` returns hits,
|
|
@@ -807,6 +840,20 @@ second), `follow` (a node whose position is the origin each frame - a trail), `o
|
|
|
807
840
|
default material is small additive points. `burst(count, spawnOptions)`, `clear()`, `set(options)`,
|
|
808
841
|
`count`.
|
|
809
842
|
|
|
843
|
+
How particles look over their lives: `size` (a multiple of the material's size, or a list each particle
|
|
844
|
+
goes through - `[0.5, 3]` billows), `sizeSpread` (each one's size varies by up to this share, 0-1),
|
|
845
|
+
`opacity` (0-1, or a list - `[0, 1, 0]` fades in and out) and `spin` (radians per second: a number
|
|
846
|
+
either way, or `[min, max]`; spinning particles start at a random angle). For smoke, a soft `map`,
|
|
847
|
+
`blending: 'normal'` and `sort: true`:
|
|
848
|
+
|
|
849
|
+
```js
|
|
850
|
+
const smoke = view.scene.add(new Emitter({
|
|
851
|
+
rate: 40, velocity: [0, 1.5, 0], velocitySpread: 0.4, lifetime: [3, 5], color: ['#777', '#333'],
|
|
852
|
+
size: [0.6, 4], sizeSpread: 0.3, opacity: [0, 0.6, 0], spin: 0.5, sort: true,
|
|
853
|
+
material: { map: puff, size: 1, blending: 'normal' },
|
|
854
|
+
}))
|
|
855
|
+
```
|
|
856
|
+
|
|
810
857
|
`Body` moves any node: `node.onUpdate(new Body({ velocity, gravity, drag }).update)`;
|
|
811
858
|
`body.impulse([x, y, z])` kicks it.
|
|
812
859
|
|
package/package.json
CHANGED
package/schema/scene.schema.json
CHANGED
|
@@ -102,6 +102,10 @@
|
|
|
102
102
|
"properties": {
|
|
103
103
|
"type": {
|
|
104
104
|
"const": "points"
|
|
105
|
+
},
|
|
106
|
+
"sort": {
|
|
107
|
+
"type": "boolean",
|
|
108
|
+
"description": "Draw the points furthest first each frame, for soft points that cover what is behind them."
|
|
105
109
|
}
|
|
106
110
|
},
|
|
107
111
|
"unevaluatedProperties": false
|
|
@@ -113,6 +117,15 @@
|
|
|
113
117
|
"properties": {
|
|
114
118
|
"type": {
|
|
115
119
|
"const": "lines"
|
|
120
|
+
},
|
|
121
|
+
"arrows": {
|
|
122
|
+
"enum": ["none", "start", "end", "both"],
|
|
123
|
+
"description": "Arrowheads at the ends of each chain of joined segments."
|
|
124
|
+
},
|
|
125
|
+
"arrowSize": {
|
|
126
|
+
"type": "number",
|
|
127
|
+
"description": "Arrowhead length in pixels; 0 sizes them from the material's width.",
|
|
128
|
+
"minimum": 0
|
|
116
129
|
}
|
|
117
130
|
},
|
|
118
131
|
"unevaluatedProperties": false
|
|
@@ -916,6 +929,20 @@
|
|
|
916
929
|
"description": "Lines: width in pixels.",
|
|
917
930
|
"minimum": 0
|
|
918
931
|
},
|
|
932
|
+
"dash": {
|
|
933
|
+
"type": "number",
|
|
934
|
+
"description": "Lines: dashed, with dashes this long in the model's units; 0 is solid.",
|
|
935
|
+
"minimum": 0
|
|
936
|
+
},
|
|
937
|
+
"gap": {
|
|
938
|
+
"type": "number",
|
|
939
|
+
"description": "Lines: the gaps between dashes; 0 is as long as the dashes.",
|
|
940
|
+
"minimum": 0
|
|
941
|
+
},
|
|
942
|
+
"dashOffset": {
|
|
943
|
+
"type": "number",
|
|
944
|
+
"description": "Lines: moves the dashes along the lines - animate it for flow."
|
|
945
|
+
},
|
|
919
946
|
"alphaTest": {
|
|
920
947
|
"type": "number",
|
|
921
948
|
"description": "Cut-outs: drop pixels with less alpha.",
|
|
@@ -1280,6 +1307,27 @@
|
|
|
1280
1307
|
"type": "number"
|
|
1281
1308
|
}
|
|
1282
1309
|
},
|
|
1310
|
+
"sizes": {
|
|
1311
|
+
"type": "array",
|
|
1312
|
+
"description": "Points: one per vertex, a multiple of the material's size.",
|
|
1313
|
+
"items": {
|
|
1314
|
+
"type": "number"
|
|
1315
|
+
}
|
|
1316
|
+
},
|
|
1317
|
+
"opacities": {
|
|
1318
|
+
"type": "array",
|
|
1319
|
+
"description": "Points: one per vertex, 0-1, times the material's opacity.",
|
|
1320
|
+
"items": {
|
|
1321
|
+
"type": "number"
|
|
1322
|
+
}
|
|
1323
|
+
},
|
|
1324
|
+
"angles": {
|
|
1325
|
+
"type": "array",
|
|
1326
|
+
"description": "Points: one per vertex, radians anticlockwise on screen.",
|
|
1327
|
+
"items": {
|
|
1328
|
+
"type": "number"
|
|
1329
|
+
}
|
|
1330
|
+
},
|
|
1283
1331
|
"indices": {
|
|
1284
1332
|
"type": "array",
|
|
1285
1333
|
"items": {
|
package/src/View.js
CHANGED
|
@@ -4,6 +4,7 @@ import { DRAG_START, OrbitControl } from './OrbitControl.js'
|
|
|
4
4
|
import { cameraPreset, fitPoints, framingScale, presetDirection } from './presets.js'
|
|
5
5
|
import { EASES } from './ease.js'
|
|
6
6
|
import { Raycaster } from './Raycaster.js'
|
|
7
|
+
import { pickArea } from './area.js'
|
|
7
8
|
import { ChangeWatch } from './watch.js'
|
|
8
9
|
import { Box3 } from './value/Box3.js'
|
|
9
10
|
import { Color } from './value/Color.js'
|
|
@@ -803,6 +804,21 @@ export class View {
|
|
|
803
804
|
return new Mat4().multiplyMatrices(camera.projectionMatrix, camera.viewMatrix)
|
|
804
805
|
}
|
|
805
806
|
|
|
807
|
+
/**
|
|
808
|
+
* Everything inside an area of the canvas, nearest first: a box `{ x, y, width, height }` or a
|
|
809
|
+
* lasso of `[x, y]` points, CSS pixels from the canvas's top left. Each point, instance and line
|
|
810
|
+
* segment is its own hit, found by where it is (a mesh by its middle) - hidden behind others or
|
|
811
|
+
* not - so a box dragged over a scatter selects that region of it.
|
|
812
|
+
*
|
|
813
|
+
* const picked = view.pickArea({ x: 40, y: 30, width: 200, height: 120 }).map((h) => h.datum)
|
|
814
|
+
* @param {import('./area.js').PickArea} area
|
|
815
|
+
* @returns {Hit[]}
|
|
816
|
+
*/
|
|
817
|
+
pickArea(area) {
|
|
818
|
+
this.scene.updateWorldTree()
|
|
819
|
+
return pickArea(this.scene, this.camera, this.#viewProjection(), this.#width, this.#height, area)
|
|
820
|
+
}
|
|
821
|
+
|
|
806
822
|
/** Every hit under a canvas position, nearest first. @param {number | MouseEvent} x @param {number} [y] @returns {Hit[]} */
|
|
807
823
|
pickAll(x, y) {
|
|
808
824
|
if (typeof x === 'object') {
|
package/src/area.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Picking an area of the canvas: everything whose place on screen falls inside a box or a lasso drawn
|
|
2
|
+
// over it - each point, instance and line segment on its own, and a mesh by its middle. Positions
|
|
3
|
+
// are tested, not surfaces, so what is hidden behind other things is found too: selecting a region
|
|
4
|
+
// of a scatter takes the whole region, front to back.
|
|
5
|
+
import * as mat4 from './math/mat4.js'
|
|
6
|
+
import { hiddenInstance } from './scene/drawables.js'
|
|
7
|
+
import { Vec3 } from './value/Vec3.js'
|
|
8
|
+
import { describe, fail } from './validate.js'
|
|
9
|
+
|
|
10
|
+
/** @import { Hit } from './Raycaster.js' */
|
|
11
|
+
/** @import { Node } from './scene/Node.js' */
|
|
12
|
+
/** @import { Camera } from './scene/cameras.js' */
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A box on the canvas, CSS pixels from its top left - width and height may be negative, as a drag
|
|
16
|
+
* up or left makes them - or a lasso: the points of a polygon, [x, y] each.
|
|
17
|
+
* @typedef {{ x: number, y: number, width: number, height: number } | [number, number][]} PickArea
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const instanceWorld = new Float32Array(16)
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The polygon an area is, and its bounding box, for the inside test.
|
|
24
|
+
* @param {PickArea} area
|
|
25
|
+
* @returns {{ points: number[], left: number, top: number, right: number, bottom: number }}
|
|
26
|
+
*/
|
|
27
|
+
function outline(area) {
|
|
28
|
+
/** @type {number[]} */
|
|
29
|
+
let points
|
|
30
|
+
if (Array.isArray(area)) {
|
|
31
|
+
if (area.length < 3 || !area.every((p) => Array.isArray(p) && p.length === 2 && p.every(Number.isFinite))) {
|
|
32
|
+
fail('View', `pickArea() takes a box { x, y, width, height } or a lasso of three or more [x, y] points, got ${describe(area)}`)
|
|
33
|
+
}
|
|
34
|
+
points = area.flat()
|
|
35
|
+
} else {
|
|
36
|
+
const { x, y, width, height } = area ?? /** @type {any} */ ({})
|
|
37
|
+
if (![x, y, width, height].every(Number.isFinite)) {
|
|
38
|
+
fail('View', `pickArea() takes a box { x, y, width, height } or a lasso of three or more [x, y] points, got ${describe(area)}`)
|
|
39
|
+
}
|
|
40
|
+
points = [x, y, x + width, y, x + width, y + height, x, y + height]
|
|
41
|
+
}
|
|
42
|
+
let left = Infinity, top = Infinity, right = -Infinity, bottom = -Infinity
|
|
43
|
+
for (let i = 0; i < points.length; i += 2) {
|
|
44
|
+
left = Math.min(left, points[i])
|
|
45
|
+
right = Math.max(right, points[i])
|
|
46
|
+
top = Math.min(top, points[i + 1])
|
|
47
|
+
bottom = Math.max(bottom, points[i + 1])
|
|
48
|
+
}
|
|
49
|
+
return { points, left, top, right, bottom }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Is (x, y) inside the polygon? Even-odd crossings. @param {number[]} p @param {number} x @param {number} y */
|
|
53
|
+
function inside(p, x, y) {
|
|
54
|
+
let into = false
|
|
55
|
+
for (let i = 0, j = p.length - 2; i < p.length; j = i, i += 2) {
|
|
56
|
+
const yi = p[i + 1], yj = p[j + 1]
|
|
57
|
+
if ((yi > y) !== (yj > y) && x < p[i] + (p[j] - p[i]) * (y - yi) / (yj - yi)) into = !into
|
|
58
|
+
}
|
|
59
|
+
return into
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Everything in the scene the camera shows, pickable, whose place falls inside the area - nearest
|
|
64
|
+
* the camera first.
|
|
65
|
+
* @param {Node} scene @param {Camera} camera @param {Float32Array} viewProjection
|
|
66
|
+
* @param {number} width @param {number} height the canvas, CSS pixels
|
|
67
|
+
* @param {PickArea} area
|
|
68
|
+
* @returns {Hit[]}
|
|
69
|
+
*/
|
|
70
|
+
export function pickArea(scene, camera, viewProjection, width, height, area) {
|
|
71
|
+
const shape = outline(area)
|
|
72
|
+
const eye = camera.worldPosition
|
|
73
|
+
const m = viewProjection
|
|
74
|
+
/** @type {Hit[]} */
|
|
75
|
+
const hits = []
|
|
76
|
+
/** World position (x, y, z) inside the area? @param {number} x @param {number} y @param {number} z */
|
|
77
|
+
const within = (x, y, z) => {
|
|
78
|
+
const w = m[3] * x + m[7] * y + m[11] * z + m[15]
|
|
79
|
+
if (w <= 0) return false
|
|
80
|
+
const nz = (m[2] * x + m[6] * y + m[10] * z + m[14]) / w
|
|
81
|
+
if (nz < -1 || nz > 1) return false
|
|
82
|
+
const sx = ((m[0] * x + m[4] * y + m[8] * z + m[12]) / w * 0.5 + 0.5) * width
|
|
83
|
+
const sy = (0.5 - (m[1] * x + m[5] * y + m[9] * z + m[13]) / w * 0.5) * height
|
|
84
|
+
return sx >= shape.left && sx <= shape.right && sy >= shape.top && sy <= shape.bottom && inside(shape.points, sx, sy)
|
|
85
|
+
}
|
|
86
|
+
/** @param {any} node @param {number} x @param {number} y @param {number} z @param {number | null} face @param {number | null} instance */
|
|
87
|
+
const hit = (node, x, y, z, face, instance) => {
|
|
88
|
+
const point = new Vec3(x, y, z)
|
|
89
|
+
hits.push({ node, point, distance: point.distanceTo(eye), normal: null, face, instance, uv: null, datum: node.datumAt(instance ?? face) })
|
|
90
|
+
}
|
|
91
|
+
const p = new Vec3()
|
|
92
|
+
|
|
93
|
+
/** @param {Node} node */
|
|
94
|
+
const visit = (node) => {
|
|
95
|
+
if (!node.shownTo(camera) || !node.pickable) return
|
|
96
|
+
const n = /** @type {any} */ (node)
|
|
97
|
+
if (n.isDrawable && n.geometry.vertexCount) {
|
|
98
|
+
const g = n.geometry, world = n.worldMatrix
|
|
99
|
+
if (n.drawMode === 'points') {
|
|
100
|
+
const pos = g.positions
|
|
101
|
+
for (let i = 0; i < g.drawnVertices; i++) {
|
|
102
|
+
p.set(pos[i * 3], pos[i * 3 + 1], pos[i * 3 + 2]).applyMatrix4(world)
|
|
103
|
+
if (within(p[0], p[1], p[2])) hit(n, p[0], p[1], p[2], i, null)
|
|
104
|
+
}
|
|
105
|
+
} else if (n.drawMode === 'lines') {
|
|
106
|
+
// a segment by its middle
|
|
107
|
+
const pos = g.positions, idx = g.indices
|
|
108
|
+
for (let s = 0; s < Math.floor(g.drawnCount / 2); s++) {
|
|
109
|
+
const a = idx ? idx[s * 2] : s * 2, b = idx ? idx[s * 2 + 1] : s * 2 + 1
|
|
110
|
+
p.set((pos[a * 3] + pos[b * 3]) / 2, (pos[a * 3 + 1] + pos[b * 3 + 1]) / 2, (pos[a * 3 + 2] + pos[b * 3 + 2]) / 2).applyMatrix4(world)
|
|
111
|
+
if (within(p[0], p[1], p[2])) hit(n, p[0], p[1], p[2], s, null)
|
|
112
|
+
}
|
|
113
|
+
} else if (n.instanceMatrices) {
|
|
114
|
+
// each instance by the middle of its copy of the geometry; hidden ones aren't there
|
|
115
|
+
const centre = g.bounds.getCenter()
|
|
116
|
+
for (let i = 0; i < n.count; i++) {
|
|
117
|
+
if (hiddenInstance(n.instanceMatrices, i)) continue
|
|
118
|
+
mat4.multiply(instanceWorld, world, n.instanceMatrices.subarray(i * 16, i * 16 + 16))
|
|
119
|
+
p.copy(centre).applyMatrix4(instanceWorld)
|
|
120
|
+
if (within(p[0], p[1], p[2])) hit(n, p[0], p[1], p[2], null, i)
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
p.copy(g.bounds.getCenter()).applyMatrix4(world)
|
|
124
|
+
if (within(p[0], p[1], p[2])) hit(n, p[0], p[1], p[2], null, null)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (const child of node.children) visit(child)
|
|
128
|
+
}
|
|
129
|
+
visit(scene)
|
|
130
|
+
return hits.sort((a, b) => a.distance - b.distance)
|
|
131
|
+
}
|
package/src/charts/marks.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// lay out and draw, never animate.
|
|
4
4
|
import { Geometry } from '../geometry/Geometry.js'
|
|
5
5
|
import { box as boxGeometry, icosphere } from '../geometry/generators.js'
|
|
6
|
+
import { Texture } from '../material/Texture.js'
|
|
6
7
|
import { InstancedMesh, Lines, Mesh, Points } from '../scene/drawables.js'
|
|
7
8
|
import { fail } from '../validate.js'
|
|
8
9
|
import { distinct } from './scales.js'
|
|
@@ -154,8 +155,10 @@ export function pointMark(ctx) {
|
|
|
154
155
|
/** @type {any} */
|
|
155
156
|
let node = sized
|
|
156
157
|
? new InstancedMesh({ geometry: icosphere({ radius: 1, detail: 1 }), material, count: 64, labels: labelsOf(ctx, 'above') })
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
// dots with a darker rim, so each one reads where they crowd together - drawn furthest first, so
|
|
159
|
+
// each blends over the ones behind it
|
|
160
|
+
: new Points({ geometry: emptyPoints(64), material: { size: ctx.spec.channels.size?.value ?? unit * 1.4, vertexColors: true, map: dot(), depthWrite: false, ...material },
|
|
161
|
+
sort: true, labels: labelsOf(ctx, 'above') })
|
|
159
162
|
if (sized) node.count = 0
|
|
160
163
|
const mark = {
|
|
161
164
|
nodes: [node],
|
|
@@ -205,6 +208,30 @@ export function pointMark(ctx) {
|
|
|
205
208
|
return mark
|
|
206
209
|
}
|
|
207
210
|
|
|
211
|
+
/** @type {Texture | null} */
|
|
212
|
+
let dotTexture = null
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* A dot: a white disc whose outer part darkens to a rim, with a soft edge - pixels of its own, so
|
|
216
|
+
* charts need no canvas to make it. Shared by every scatter.
|
|
217
|
+
*/
|
|
218
|
+
function dot() {
|
|
219
|
+
if (dotTexture) return dotTexture
|
|
220
|
+
const size = 64, data = new Uint8Array(size * size * 4), r = size / 2
|
|
221
|
+
for (let y = 0; y < size; y++) {
|
|
222
|
+
for (let x = 0; x < size; x++) {
|
|
223
|
+
const d = Math.hypot(x + 0.5 - r, y + 0.5 - r) / r
|
|
224
|
+
// the colour full to 0.78 of the radius, then down to 0.62 at the edge; alpha soft over the last pixel and a half
|
|
225
|
+
const shade = d < 0.78 ? 1 : 1 - (d - 0.78) / 0.22 * 0.38
|
|
226
|
+
const alpha = Math.min(1, Math.max(0, (1 - d) * r / 1.5))
|
|
227
|
+
const o = (y * size + x) * 4
|
|
228
|
+
data[o] = data[o + 1] = data[o + 2] = Math.round(shade * 255)
|
|
229
|
+
data[o + 3] = Math.round(alpha * 255)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return (dotTexture = new Texture({ image: { data, width: size, height: size } }))
|
|
233
|
+
}
|
|
234
|
+
|
|
208
235
|
/** @param {number} n */
|
|
209
236
|
function emptyPoints(n) {
|
|
210
237
|
const g = new Geometry({ positions: new Float32Array(n * 3), colors: new Float32Array(n * 3) })
|
package/src/geometry/Geometry.js
CHANGED
|
@@ -9,10 +9,16 @@ import { checkOptions, describe, fail } from '../validate.js'
|
|
|
9
9
|
* @property {ArrayLike<number>} [uvs2] a second uv per vertex, for maps that need every surface on its own
|
|
10
10
|
* patch of texture - baked occlusion and light maps (a texture's uvSet: 1 reads it)
|
|
11
11
|
* @property {ArrayLike<number>} [colors] rgb per vertex, sRGB 0-1 like every colour input
|
|
12
|
+
* @property {ArrayLike<number>} [sizes] points: one per vertex, a multiple of the material's size
|
|
13
|
+
* @property {ArrayLike<number>} [opacities] points: one per vertex, 0-1, multiplying the material's opacity
|
|
14
|
+
* @property {ArrayLike<number>} [angles] points: one per vertex, radians anticlockwise on screen - turns
|
|
15
|
+
* each point's texture
|
|
12
16
|
* @property {ArrayLike<number>} [indices] vertex indices; triangles for meshes, pairs for lines
|
|
13
17
|
*/
|
|
14
18
|
|
|
15
|
-
const OPTIONS = ['positions', 'normals', 'uvs', 'uvs2', 'colors', 'indices']
|
|
19
|
+
const OPTIONS = ['positions', 'normals', 'uvs', 'uvs2', 'colors', 'sizes', 'opacities', 'angles', 'indices']
|
|
20
|
+
// the per-point arrays: Points with any of them draw each point as its own square, sized, faded and turned
|
|
21
|
+
const POINT_ARRAYS = /** @type {const} */ (['sizes', 'opacities', 'angles'])
|
|
16
22
|
|
|
17
23
|
/**
|
|
18
24
|
* Vertex data: flat typed arrays plus an optional index. Generators such as box() and sphere()
|
|
@@ -25,6 +31,9 @@ export class Geometry {
|
|
|
25
31
|
/** @type {Float32Array | null} */ uvs = null
|
|
26
32
|
/** A second UV set, for baked occlusion and light maps. @type {Float32Array | null} */ uvs2 = null
|
|
27
33
|
/** @type {Float32Array | null} */ colors = null
|
|
34
|
+
/** Points: each one's size, as a multiple of the material's. @type {Float32Array | null} */ sizes = null
|
|
35
|
+
/** Points: each one's opacity, 0-1, times the material's. @type {Float32Array | null} */ opacities = null
|
|
36
|
+
/** Points: each one's turn on screen, in radians anticlockwise. @type {Float32Array | null} */ angles = null
|
|
28
37
|
/** @type {Uint16Array | Uint32Array | null} */ indices = null
|
|
29
38
|
|
|
30
39
|
/** @internal Bumped by needsUpdate() so renderers know to re-upload. */
|
|
@@ -62,6 +71,7 @@ export class Geometry {
|
|
|
62
71
|
if (options.uvs) this.uvs = toFloat32('uvs', options.uvs, 2, n)
|
|
63
72
|
if (options.uvs2) this.uvs2 = toFloat32('uvs2', options.uvs2, 2, n)
|
|
64
73
|
if (options.colors) this.colors = toFloat32('colors', options.colors, 3, n)
|
|
74
|
+
for (const name of POINT_ARRAYS) if (options[name]) this[name] = toFloat32(name, options[name], 1, n)
|
|
65
75
|
if (options.indices) {
|
|
66
76
|
// -1: no indices at all refer to no vertex, so an empty geometry can say so
|
|
67
77
|
let max = -1
|
|
@@ -191,6 +201,9 @@ export class Geometry {
|
|
|
191
201
|
uvs: this.uvs ?? undefined,
|
|
192
202
|
uvs2: this.uvs2 ?? undefined,
|
|
193
203
|
colors: this.colors ?? undefined,
|
|
204
|
+
sizes: this.sizes ?? undefined,
|
|
205
|
+
opacities: this.opacities ?? undefined,
|
|
206
|
+
angles: this.angles ?? undefined,
|
|
194
207
|
indices: this.indices ?? undefined,
|
|
195
208
|
})
|
|
196
209
|
g.source = this.source && { ...this.source }
|
|
@@ -218,6 +231,7 @@ export class Geometry {
|
|
|
218
231
|
if (this.uvs) json.uvs = Array.from(this.uvs)
|
|
219
232
|
if (this.uvs2) json.uvs2 = Array.from(this.uvs2)
|
|
220
233
|
if (this.colors) json.colors = Array.from(this.colors)
|
|
234
|
+
for (const name of POINT_ARRAYS) if (this[name]) json[name] = Array.from(/** @type {Float32Array} */ (this[name]))
|
|
221
235
|
if (this.indices) json.indices = Array.from(this.indices)
|
|
222
236
|
return json
|
|
223
237
|
}
|
|
@@ -242,6 +256,7 @@ export class Geometry {
|
|
|
242
256
|
this.uvs = expand(this.uvs, 2)
|
|
243
257
|
this.uvs2 = expand(this.uvs2, 2)
|
|
244
258
|
this.colors = expand(this.colors, 3)
|
|
259
|
+
for (const name of POINT_ARRAYS) this[name] = expand(this[name], 1)
|
|
245
260
|
this.indices = null
|
|
246
261
|
this.source = null
|
|
247
262
|
}
|