This is a big one, even though I've tried to keep a few things out of it.
Goal
The goal is to make it more straightforward for us internally but also for consumers of @h5web/lib to implement and use custom buffer geometries (like maybe thick/dashed lines that don't change while zooming). Attn @PeterC-DLS, as this might be of interest to you for Davidia.
Custom buffer geometries are useful to avoid blowing up the bundle and increasing maintenance cost by adding three-stdlib and drei as (peer) dependencies to @h5web/lib and consumer applications. Moreover, Three's/drei's built-in geometry code can be:
quite obscure, poorly documented, and difficult to debug;
inefficient - e.g. by looping through the data a second time to create an index buffer ... and even a third time inside arrayNeedsUint32;
easily misused, especially in a React environment - e.g. recreating/updating on every render by forgetting to memoise things properly; forgetting to set needsUpdate, etc.
Implementation
The main entry point is a new hook called useGeometry, which accepts:
a buffer geometry class that inherits H5WebGeometry (see below);
the number of items to iterate over when updating the geometry (called dataLength);
a bunch of parameters that are required to prepare/update the various buffer attributes in the geometry (abscissas, values, scales, interpolators, etc.
H5WebGeometry is an abstract class that inherits BufferGeometry and specifies two methods: prepare and update.
Whenever the number of items to iterate over changes, useGeometry creates a new instance of the given H5WebGeometry sub-class. The sub-class's constructor typically takes care of initialising the buffer attributes.
Whenever any of the parameters used by the geometry change, useGeometry updates the geometry instance as follows: first, it calls the prepare method to store the updated parameters in the geometry instance; then it starts a for loop that calls the update method dataLength times. It is then up to the update method to update the content of the various buffer attributes in the geometry.
Justification
With this approach geometries are fully independent from one another. We no longer try to over-optimise things by sharing buffer attributes across geometries or by coupling lots of unrelated computations inside the same loop, which led to code that was difficult to read and impossible to abstract for re-use.
Sure, we end up with more buffers in memory in some cases (like when displaying both the line and points with DataCurve/LineVis), but we gain in code readability and make the geometry code much easier to refactor in the future (for instance to fix the subtle glitches when drawing lines that include points with non-finite coordinates, which are moved to (0, 0, CAMERA_FAR)).
My initial idea was to implement a useGeometries hook that allowed updating multiple geometries within a single loop. However, this required having a small loop (to iterate over the geometries) inside a big loop (to iterate over the data), and I realised that this had no theoretical performance benefit over doing the big loop multiple times in a row (i.e. via multiple, consecutive calls to useGeometry).
Moreover, useGeometry (rather than useGeometries) allows having re-usable components like Line, Glyphs and ErrorBars that each create/update their own geometries independently from one another. As a result, DataCurve is now just a dumb component that helps reduce duplication in LineVis.
In this PR
The new useGeometry hook and H5WebGeometry abstract class (documented in Utilities.mdx).
3 new re-usable components and their custom geometries to turn DataCurve into a dumb component:
Line, LineGeometry
Glyphs, GlyphsGeometry
ErrorBars, ErroBarsGeometry, ErrorCapsGeometry
3 new story files to document the new components
2 additional custom geometries used to refactor other components:
ScatterPointsGeometry (used in ScatterPoints)
SurfaceMeshGeometry (used in SurfaceMesh)
2 new utility functions to create buffer attributes: createBufferAttribute and createIndex (documented in Utilities.mdx).
Naming decisions
I wasn't sure about the naming of the geometries and the components in which they are used, because:
geometries aren't necessarily coupled to a specific Three object (e.g. SurfaceMeshGeometry is passed to both <points> and <mesh>);
geometries aren't necessarily coupled to a specific material (e.g. GlyphsGeometry would work find with <points>'s default material, PointsMaterial, instead of GlyphMaterial).
As a result, I decided to use the names of the components in which they are used - i.e. SurfaceMesh, ScatterPoints, Line, etc. Was this a good decision?
If it was, are the names of the new components, Line, Glyphs, ErrorBars, appropriate?
Line/LineGeometry kind of follows Three's naming (<line>, <lineBasicMaterial>), but obviously conflicts with it ... and somewhat with SvgLine too. It's perhaps too generic.
Glyphs/GlyphsGeometry are named after the existing GlyphMaterial, but as mentioned, the geometry itself would work perfectly fine with PointsMaterial and others (for instance, I used it in SurfaceMesh at some point to display the points when debugging).
I wondered about using the names LineCurve/LineCurveGeometry and GlyphCurve/GlyphCurveGeometry, for instance.
This is a big one, even though I've tried to keep a few things out of it.
Goal
The goal is to make it more straightforward for us internally but also for consumers of
@h5web/lib
to implement and use custom buffer geometries (like maybe thick/dashed lines that don't change while zooming). Attn @PeterC-DLS, as this might be of interest to you for Davidia.Custom buffer geometries are useful to avoid blowing up the bundle and increasing maintenance cost by adding
three-stdlib
anddrei
as (peer) dependencies to@h5web/lib
and consumer applications. Moreover, Three's/drei's built-in geometry code can be:index
buffer ... and even a third time insidearrayNeedsUint32
;needsUpdate
, etc.Implementation
The main entry point is a new hook called
useGeometry
, which accepts:H5WebGeometry
(see below);dataLength
);H5WebGeometry
is an abstract class that inheritsBufferGeometry
and specifies two methods:prepare
andupdate
.useGeometry
creates a new instance of the givenH5WebGeometry
sub-class. The sub-class's constructor typically takes care of initialising the buffer attributes.useGeometry
updates the geometry instance as follows: first, it calls theprepare
method to store the updated parameters in the geometry instance; then it starts afor
loop that calls theupdate
methoddataLength
times. It is then up to theupdate
method to update the content of the various buffer attributes in the geometry.Justification
With this approach geometries are fully independent from one another. We no longer try to over-optimise things by sharing buffer attributes across geometries or by coupling lots of unrelated computations inside the same loop, which led to code that was difficult to read and impossible to abstract for re-use.
Sure, we end up with more buffers in memory in some cases (like when displaying both the line and points with
DataCurve
/LineVis
), but we gain in code readability and make the geometry code much easier to refactor in the future (for instance to fix the subtle glitches when drawing lines that include points with non-finite coordinates, which are moved to(0, 0, CAMERA_FAR)
).My initial idea was to implement a
useGeometries
hook that allowed updating multiple geometries within a single loop. However, this required having a small loop (to iterate over the geometries) inside a big loop (to iterate over the data), and I realised that this had no theoretical performance benefit over doing the big loop multiple times in a row (i.e. via multiple, consecutive calls touseGeometry
).Moreover,
useGeometry
(rather thanuseGeometries
) allows having re-usable components likeLine
,Glyphs
andErrorBars
that each create/update their own geometries independently from one another. As a result,DataCurve
is now just a dumb component that helps reduce duplication inLineVis
.In this PR
useGeometry
hook andH5WebGeometry
abstract class (documented inUtilities.mdx
).DataCurve
into a dumb component:Line
,LineGeometry
Glyphs
,GlyphsGeometry
ErrorBars
,ErroBarsGeometry
,ErrorCapsGeometry
ScatterPointsGeometry
(used inScatterPoints
)SurfaceMeshGeometry
(used inSurfaceMesh
)createBufferAttribute
andcreateIndex
(documented inUtilities.mdx
).Naming decisions
I wasn't sure about the naming of the geometries and the components in which they are used, because:
SurfaceMeshGeometry
is passed to both<points>
and<mesh>
);GlyphsGeometry
would work find with<points>
's default material,PointsMaterial
, instead ofGlyphMaterial
).As a result, I decided to use the names of the components in which they are used - i.e.
SurfaceMesh
,ScatterPoints
,Line
, etc. Was this a good decision?If it was, are the names of the new components,
Line
,Glyphs
,ErrorBars
, appropriate?Line
/LineGeometry
kind of follows Three's naming (<line>
,<lineBasicMaterial>
), but obviously conflicts with it ... and somewhat withSvgLine
too. It's perhaps too generic.Glyphs
/GlyphsGeometry
are named after the existingGlyphMaterial
, but as mentioned, the geometry itself would work perfectly fine withPointsMaterial
and others (for instance, I used it inSurfaceMesh
at some point to display the points when debugging).I wondered about using the names
LineCurve
/LineCurveGeometry
andGlyphCurve
/GlyphCurveGeometry
, for instance.