Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: mapLayer web worker crashes if no property data provded. #1900

Merged
merged 7 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 102 additions & 119 deletions typescript/packages/subsurface-viewer/src/layers/map/mapLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,70 @@ type Frame = {
};

export type Params = [
meshData: Float32Array,
propertiesData: Float32Array,
meshData: Float32Array | null,
propertiesData: Float32Array | null,
isMesh: boolean,
frame: Frame,
smoothShading: boolean,
gridLines: boolean,
];

async function loadURLData(url: string): Promise<Float32Array | null> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't these function be shared and used in every place where data arrays are loaded ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, they should be but it's more preferable to refactor in a dedicated PR rather than in this bug fixing one.

let res: Float32Array | null = null;
const response = await fetch(url);
if (!response.ok) {
console.error("Could not load ", url);
}
const blob = await response.blob();
const contentType = response.headers.get("content-type");
const isPng = contentType === "image/png";
if (isPng) {
// Load as Png with abolute float values.
res = await new Promise((resolve) => {
const fileReader = new FileReader();
fileReader.readAsArrayBuffer(blob);
fileReader.onload = () => {
const arrayBuffer = fileReader.result;
const imgData = png.decode(arrayBuffer as ArrayBuffer);
const data = imgData.data; // array of int's

const n = data.length;
const buffer = new ArrayBuffer(n);
const view = new DataView(buffer);
for (let i = 0; i < n; i++) {
view.setUint8(i, data[i]);
}

const floatArray = new Float32Array(buffer);
resolve(floatArray);
};
});
} else {
// Load as binary array of floats.
const buffer = await blob.arrayBuffer();
res = new Float32Array(buffer);
}
return res;
}

async function loadFloat32Data(
data: string | number[] | Float32Array
): Promise<Float32Array | null> {
if (!data) {
return null;
}
if (ArrayBuffer.isView(data)) {
// Input data is typed array.
return data;
} else if (Array.isArray(data)) {
// Input data is native javascript array.
return new Float32Array(data);
} else {
// Input data is an URL.
return await loadURLData(data);
}
}

/**
* Will load data for the mesh and the properties. Both of which may be given as arrays (javascript or typed)
* or as a URL to the data in binary format.
Expand All @@ -63,116 +119,18 @@ async function loadMeshAndProperties(
// Keep
//const t0 = performance.now();

const isMesh = typeof meshData !== "undefined";
const isProperties = typeof propertiesData !== "undefined";

if (!isMesh && !isProperties) {
console.error("Error. One or both of texture and mesh must be given!");
}

if (isMesh && !isProperties) {
propertiesData = meshData;
}

//-- PROPERTIES. --
let properties: Float32Array;
if (ArrayBuffer.isView(propertiesData)) {
// Input data is typed array.
properties = propertiesData; // Note no copy. Make sure input data is not altered.
} else if (Array.isArray(propertiesData)) {
// Input data is native javascript array.
properties = new Float32Array(propertiesData);
} else {
// Input data is an URL.
const response = await fetch(propertiesData);
if (!response.ok) {
console.error("Could not load ", propertiesData);
}
const mesh = await loadFloat32Data(meshData);
const properties = await loadFloat32Data(propertiesData);

const blob = await response.blob();
const contentType = response.headers.get("content-type");
const isPng = contentType === "image/png";
if (isPng) {
// Load as Png with abolute float values.
properties = await new Promise((resolve) => {
const fileReader = new FileReader();
fileReader.readAsArrayBuffer(blob);
fileReader.onload = () => {
const arrayBuffer = fileReader.result;
const imgData = png.decode(arrayBuffer as ArrayBuffer);
const data = imgData.data; // array of int's

const n = data.length;
const buffer = new ArrayBuffer(n);
const view = new DataView(buffer);
for (let i = 0; i < n; i++) {
view.setUint8(i, data[i]);
}

const floatArray = new Float32Array(buffer);
resolve(floatArray);
};
});
} else {
// Load as binary array of floats.
const buffer = await blob.arrayBuffer();
properties = new Float32Array(buffer);
}
}

//-- MESH --
let mesh: Float32Array = new Float32Array();
if (isMesh) {
if (ArrayBuffer.isView(meshData)) {
// Input data is typed array.
mesh = meshData; // Note no copy. Make sure data is never altered.
} else if (Array.isArray(meshData)) {
// Input data is native javascript array.
mesh = new Float32Array(meshData);
} else {
// Input data is an URL.
const response_mesh = await fetch(meshData);
if (!response_mesh.ok) {
console.error("Could not load mesh");
}

const blob_mesh = await response_mesh.blob();
const contentType_mesh = response_mesh.headers.get("content-type");
const isPng_mesh = contentType_mesh === "image/png";
if (isPng_mesh) {
// Load as Png with abolute float values.
mesh = await new Promise((resolve) => {
const fileReader = new FileReader();
fileReader.readAsArrayBuffer(blob_mesh);
fileReader.onload = () => {
const arrayBuffer = fileReader.result;
const imgData = png.decode(arrayBuffer as ArrayBuffer);
const data = imgData.data; // array of int's

const n = data.length;
const buffer = new ArrayBuffer(n);
const view = new DataView(buffer);
for (let i = 0; i < n; i++) {
view.setUint8(i, data[i]);
}

const floatArray = new Float32Array(buffer);
resolve(floatArray);
};
});
} else {
// Load as binary array of floats.
const buffer = await blob_mesh.arrayBuffer();
mesh = new Float32Array(buffer);
}
}
}
// if (!isMesh && !isProperties) {
// console.error("Error. One or both of texture and mesh must be given!");
// }

// Keep this.
// const t1 = performance.now();
// console.debug(`Task loading took ${(t1 - t0) * 0.001} seconds.`);

return Promise.all([isMesh, mesh, properties]);
return Promise.all([mesh, properties]);
}

export interface MapLayerProps extends ExtendedLayerProps {
Expand Down Expand Up @@ -324,7 +282,7 @@ export default class MapLayer<

const p = loadMeshAndProperties(meshData, propertiesData);

p.then(([isMesh, meshData, propertiesData]) => {
p.then(([meshData, propertiesData]) => {
// Using inline web worker for calculating the triangle mesh from
// loaded input data so not to halt the GUI thread.
const blob = new Blob(
Expand All @@ -334,19 +292,15 @@ export default class MapLayer<
const url = URL.createObjectURL(blob);
const webWorker = new Worker(url);

const webworkerParams: Params = [
const webworkerParams = this.getWebworkerParams(
meshData,
propertiesData,
isMesh,
this.props.frame,
this.props.smoothShading,
this.props.gridLines,
];

webWorker.postMessage(webworkerParams, [
meshData.buffer,
propertiesData.buffer,
]); // transferable objects to avoid copying.
propertiesData
);

webWorker.postMessage(
webworkerParams.params,
webworkerParams?.transferrables
);
webWorker.onmessage = (e) => {
const [
positions,
Expand Down Expand Up @@ -520,6 +474,35 @@ export default class MapLayer<
);
return [layer];
}

private getWebworkerParams(
meshData: Float32Array | null,
propertiesData: Float32Array | null
): { params: Params; transferrables?: Transferable[] } {
if (!meshData && !propertiesData) {
throw new Error(
"Either mesh or properties or the both must be defined"
);
}

const params: Params = [
meshData,
propertiesData,
!!meshData,
this.props.frame,
this.props.smoothShading,
this.props.gridLines,
];
const transferrables = [
meshData?.buffer,
propertiesData?.buffer,
].filter((item) => !!item) as ArrayBuffer[];

if (transferrables.length > 0) {
return { params, transferrables };
}
return { params };
}
}

MapLayer.layerName = "MapLayer";
Expand Down
17 changes: 13 additions & 4 deletions typescript/packages/subsurface-viewer/src/layers/map/webworker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@ import type { Params } from "./mapLayer";
* that is used by WebGl. Using indice, lines and triangles share common vertices to save memory.
*/
export function makeFullMesh(e: { data: Params }) {
const [meshData, propertiesData, isMesh, frame, smoothShading, gridLines] =
e.data;
const [
inputMeshData,
inputPropertiesData,
isMesh,
frame,
smoothShading,
gridLines,
] = e.data;

// Keep
//const t0 = performance.now();
Expand Down Expand Up @@ -139,6 +145,9 @@ export function makeFullMesh(e: { data: Params }) {
return mean;
}

const meshData = inputMeshData as Float32Array;
const propertiesData = inputPropertiesData ?? meshData;

// non mesh grids use z = 0 (see below)
const meshZValueRange = isMesh ? getFloat32ArrayMinMax(meshData) : [0, 0];
const propertyValueRange = getFloat32ArrayMinMax(propertiesData);
Expand Down Expand Up @@ -533,7 +542,7 @@ export function makeFullMesh(e: { data: Params }) {
number[],
number[],
];
const webworkerParams: returnType = [
const webworkerReturnData: returnType = [
positions,
normals,
triangleIndices,
Expand All @@ -544,7 +553,7 @@ export function makeFullMesh(e: { data: Params }) {
];

postMessage(
webworkerParams,
webworkerReturnData,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
[
Expand Down
Loading