@equinor/esv-intersection
Version:
Intersection component package with testing and automatic documentation.
1,465 lines (1,275 loc) • 40 kB
text/typescript
import { CanvasSource, groupD8, Point, Rectangle, Texture } from 'pixi.js';
import { DEFAULT_TEXTURE_SIZE } from '../constants';
import {
Casing,
CasingWindow,
Cement,
CementOptions,
CementPlug,
CementPlugOptions,
CementSqueeze,
CementSqueezeOptions,
Completion,
HoleOptions,
HoleSize,
ScreenOptions,
TubingOptions,
Perforation,
PerforationOptions,
foldPerforationSubKind,
intersect,
isSubKindCasedHoleFracPack,
isSubkindCasedHoleGravelPack,
PerforationSubKind,
isSubKindCasedHoleFracturation,
} from '../layers/schematicInterfaces';
import { ComplexRopeSegment } from '../layers/CustomDisplayObjects/ComplexRope';
import { createNormals, offsetPoints } from '../utils/vectorUtils';
export type PerforationShape = ComplexRopeSegment;
export interface TubularRenderingObject {
leftPath: Point[];
rightPath: Point[];
}
export interface CasingRenderObject {
id: string;
kind: 'casing';
referenceDiameter: number;
referenceRadius: number;
casingWallWidth: number;
hasShoe: boolean;
bottom: number;
zIndex?: number;
sections: {
kind: 'casing' | 'casing-window';
leftPath: Point[];
rightPath: Point[];
pathPoints: Point[];
}[];
}
export const getEndLines = (
rightPath: [Point, Point, ...Point[]],
leftPath: [Point, Point, ...Point[]],
): {
top: [Point, Point];
bottom: [Point, Point];
} => {
return {
top: [rightPath[0], leftPath[0]],
bottom: [
rightPath[rightPath.length - 1] as Point,
leftPath[leftPath.length - 1] as Point,
],
};
};
export const overlaps = (
top1: number,
bottom1: number,
top2: number,
bottom2: number,
): boolean => top1 <= bottom2 && top2 <= bottom1;
export const strictlyOverlaps = (
top1: number,
bottom1: number,
top2: number,
bottom2: number,
): boolean => top1 < bottom2 && top2 < bottom1;
export const uniq = <T>(arr: T[]): T[] => Array.from<T>(new Set(arr));
const findIntersectingItems = (
start: number,
end: number,
otherStrings: (Casing | Completion)[],
holes: HoleSize[],
): {
overlappingHoles: HoleSize[];
overlappingOuterStrings: (Casing | Completion)[];
} => {
const overlappingHoles = holes.filter((hole: HoleSize) =>
overlaps(start, end, hole.start, hole.end),
);
const overlappingOuterStrings = otherStrings.filter(
(casing: Casing | Completion) =>
overlaps(start, end, casing.start, casing.end),
);
return {
overlappingHoles,
overlappingOuterStrings,
};
};
export const getUniqueDiameterChangeDepths = (
[intervalStart, intervalEnd]: [number, number],
diameterIntervals: { start: number; end: number }[],
): number[] => {
const epsilon = 0.0001;
const diameterChangeDepths = diameterIntervals.flatMap(
(
d, // to find diameter right before/after object
) => [d.start - epsilon, d.start, d.end, d.end + epsilon],
);
const trimmedChangedDepths = diameterChangeDepths.filter(
d => d >= intervalStart && d <= intervalEnd,
); // trim
trimmedChangedDepths.push(intervalStart);
trimmedChangedDepths.push(intervalEnd);
const uniqDepths = uniq(trimmedChangedDepths);
return uniqDepths.sort((a: number, b: number) => a - b);
};
const getInnerStringDiameter = (stringType: Casing | Completion): number =>
stringType.kind === 'casing' ? stringType.innerDiameter : stringType.diameter;
export const findCementOuterDiameterAtDepth = (
attachedStrings: (Casing | Completion)[],
nonAttachedStrings: (Casing | Completion)[],
holes: HoleSize[],
depth: number,
): number => {
const defaultCementWidth = 100; // Default to flow cement outside to show error in data
const attachedStringAtDepth = attachedStrings.find(
(casingOrCompletion: Casing | Completion) =>
casingOrCompletion.start <= depth && casingOrCompletion.end >= depth,
);
const attachedOuterDiameter = attachedStringAtDepth
? attachedStringAtDepth.diameter
: 0;
const outerCasingAtDepth = nonAttachedStrings
.filter(
(casingOrCompletion: Casing | Completion) =>
getInnerStringDiameter(casingOrCompletion) > attachedOuterDiameter,
)
.sort(
(a: Casing | Completion, b: Casing | Completion) =>
getInnerStringDiameter(a) - getInnerStringDiameter(b),
) // ascending
.find(casing => casing.start <= depth && casing.end >= depth);
const holeAtDepth = holes.find(
(hole: HoleSize) =>
hole.start <= depth &&
hole.end >= depth &&
hole.diameter > attachedOuterDiameter,
);
if (outerCasingAtDepth) {
return getInnerStringDiameter(outerCasingAtDepth);
}
if (holeAtDepth) {
return holeAtDepth.diameter;
}
return defaultCementWidth;
};
export const findPerforationOuterDiameterAtDepth = (
nonAttachedStrings: (Casing | Completion)[],
holes: HoleSize[],
depth: number,
perforationSubKind: PerforationSubKind,
): number => {
const defaultPerforationWidth = 100; // Default to flow perforation outside to show error in data
const outerCasingAtDepth = nonAttachedStrings
.sort(
(a: Casing | Completion, b: Casing | Completion) =>
b.diameter - a.diameter,
) // descending
.find(casing => casing.start <= depth && casing.end >= depth);
const holeAtDepth = holes.find(
(hole: HoleSize) => hole.start <= depth && hole.end >= depth,
);
if (
outerCasingAtDepth &&
perforationSubKind !== 'Open hole frac pack' &&
perforationSubKind !== 'Open hole gravel pack'
) {
return getInnerStringDiameter(outerCasingAtDepth);
}
if (holeAtDepth) {
return holeAtDepth.diameter;
}
return defaultPerforationWidth;
};
export const findCementPlugInnerDiameterAtDepth = (
attachedStrings: (Casing | Completion)[],
nonAttachedStrings: (Casing | Completion)[],
holes: HoleSize[],
depth: number,
): number => {
// Default to flow cement outside to show error in data
const defaultCementWidth = 100;
const attachedStringAtDepth = attachedStrings
.sort(
(a: Casing | Completion, b: Casing | Completion) =>
getInnerStringDiameter(a) - getInnerStringDiameter(b),
) // ascending
.find(
casingOrCompletion =>
casingOrCompletion.start <= depth && casingOrCompletion.end >= depth,
);
if (attachedStringAtDepth) {
return getInnerStringDiameter(attachedStringAtDepth);
}
// Start from an attached diameter
const minimumDiameter = attachedStrings.length
? Math.min(...attachedStrings.map(c => getInnerStringDiameter(c)))
: 0;
const nonAttachedStringAtDepth = nonAttachedStrings
.sort(
(a: Casing | Completion, b: Casing | Completion) =>
getInnerStringDiameter(a) - getInnerStringDiameter(b),
) // ascending
.find(
(casingOrCompletion: Casing | Completion) =>
casingOrCompletion.start <= depth &&
casingOrCompletion.end >= depth &&
minimumDiameter <= getInnerStringDiameter(casingOrCompletion),
);
if (nonAttachedStringAtDepth) {
return getInnerStringDiameter(nonAttachedStringAtDepth);
}
const holeAtDepth = holes.find(
hole => hole.start <= depth && hole.end >= depth && hole.diameter,
);
if (holeAtDepth) {
return holeAtDepth.diameter;
}
return defaultCementWidth;
};
export const createComplexRopeSegmentsForCement = (
cement: Cement,
casings: Casing[],
completion: Completion[],
holes: HoleSize[],
exaggerationFactor: number,
getPoints: (start: number, end: number) => Point[],
): ComplexRopeSegment[] => {
const { attachedStrings, nonAttachedStrings } = splitByReferencedStrings(
cement.referenceIds,
casings,
completion,
);
if (attachedStrings.length === 0) {
throw new Error(
`Invalid cement data, can't find referenced casing/completion string for cement with id '${cement.id}'`,
);
}
attachedStrings.sort((a, b) => a.end - b.end); // ascending
const bottomOfCement = attachedStrings[attachedStrings.length - 1]!.end;
const { overlappingOuterStrings, overlappingHoles } = findIntersectingItems(
cement.toc,
bottomOfCement,
nonAttachedStrings,
holes,
);
const outerDiameterIntervals = [
...overlappingOuterStrings,
...overlappingHoles,
].map(d => ({
start: d.start,
end: d.end,
}));
const changeDepths = getUniqueDiameterChangeDepths(
[cement.toc, bottomOfCement],
outerDiameterIntervals,
);
const diameterIntervals = changeDepths.flatMap(
(depth: number, index: number, list: number[]) => {
if (index === list.length - 1) {
return [];
}
const nextDepth = list[index + 1]!;
const diameterAtChangeDepth = findCementOuterDiameterAtDepth(
attachedStrings,
overlappingOuterStrings,
overlappingHoles,
depth,
);
return [
{
top: depth,
bottom: nextDepth,
diameter: diameterAtChangeDepth * exaggerationFactor,
},
];
},
);
const ropeSegments = diameterIntervals.map(interval => ({
diameter: interval.diameter,
points: getPoints(interval.top, interval.bottom),
}));
return ropeSegments;
};
const splitByReferencedStrings = (
referenceIds: string[],
casings: Casing[],
completion: Completion[],
): {
attachedStrings: (Casing | Completion)[];
nonAttachedStrings: (Casing | Completion)[];
} =>
[...casings, ...completion].reduce(
(acc, current) => {
if (referenceIds.includes(current.id)) {
return { ...acc, attachedStrings: [...acc.attachedStrings, current] };
}
return {
...acc,
nonAttachedStrings: [...acc.nonAttachedStrings, current],
};
},
{
attachedStrings: [] as (Casing | Completion)[],
nonAttachedStrings: [] as (Casing | Completion)[],
},
);
export const createComplexRopeSegmentsForCementSqueeze = (
squeeze: CementSqueeze,
casings: Casing[],
completion: Completion[],
holes: HoleSize[],
exaggerationFactor: number,
getPoints: (start: number, end: number) => Point[],
): ComplexRopeSegment[] => {
const { attachedStrings, nonAttachedStrings } = splitByReferencedStrings(
squeeze.referenceIds,
casings,
completion,
);
if (attachedStrings.length === 0) {
throw new Error(
`Invalid cement squeeze data, can't find referenced casing/completion for squeeze with id '${squeeze.id}'`,
);
}
const { overlappingOuterStrings, overlappingHoles } = findIntersectingItems(
squeeze.start,
squeeze.end,
nonAttachedStrings,
holes,
);
const outerDiameterIntervals = [
...overlappingOuterStrings,
...overlappingHoles,
].map(d => ({
start: d.start,
end: d.end,
}));
const changeDepths = getUniqueDiameterChangeDepths(
[squeeze.start, squeeze.end],
outerDiameterIntervals,
);
const diameterIntervals = changeDepths.flatMap((depth, index, list) => {
if (index === list.length - 1) {
return [];
}
const nextDepth = list[index + 1]!;
const diameterAtDepth = findCementOuterDiameterAtDepth(
attachedStrings,
overlappingOuterStrings,
overlappingHoles,
depth,
);
return [
{
top: depth,
bottom: nextDepth,
diameter: diameterAtDepth * exaggerationFactor,
},
];
});
const ropeSegments = diameterIntervals.map(interval => ({
diameter: interval.diameter,
points: getPoints(interval.top, interval.bottom),
}));
return ropeSegments;
};
export const createComplexRopeSegmentsForCementPlug = (
plug: CementPlug,
casings: Casing[],
completion: Completion[],
holes: HoleSize[],
exaggerationFactor: number,
getPoints: (start: number, end: number) => Point[],
): ComplexRopeSegment[] => {
const { attachedStrings, nonAttachedStrings } = splitByReferencedStrings(
plug.referenceIds,
casings,
completion,
);
const { overlappingHoles, overlappingOuterStrings } = findIntersectingItems(
plug.start,
plug.end,
nonAttachedStrings,
holes,
);
const innerDiameterIntervals = [
...attachedStrings,
...overlappingHoles,
...overlappingOuterStrings,
].map(d => ({
start: d.start,
end: d.end,
}));
const changeDepths = getUniqueDiameterChangeDepths(
[plug.start, plug.end],
innerDiameterIntervals,
);
const diameterIntervals = changeDepths.flatMap((depth, index, list) => {
if (index === list.length - 1) {
return [];
}
const nextDepth = list[index + 1]!;
const diameterAtDepth = findCementPlugInnerDiameterAtDepth(
attachedStrings,
overlappingOuterStrings,
overlappingHoles,
depth,
);
return [
{
top: depth,
bottom: nextDepth,
diameter: diameterAtDepth * exaggerationFactor,
},
];
});
const ropeSegments = diameterIntervals.map(interval => ({
diameter: interval.diameter,
points: getPoints(interval.top, interval.bottom),
}));
return ropeSegments;
};
const createGradientFill = (
canvas: HTMLCanvasElement,
canvasCtx: CanvasRenderingContext2D,
firstColor: string,
secondColor: string,
startPctOffset: number,
): CanvasGradient => {
const halfWayPct = 0.5;
const gradient = canvasCtx.createLinearGradient(0, 0, 0, canvas.height);
gradient.addColorStop(0, firstColor);
gradient.addColorStop(halfWayPct - startPctOffset, secondColor);
gradient.addColorStop(halfWayPct + startPctOffset, secondColor);
gradient.addColorStop(1, firstColor);
return gradient;
};
export const createHoleBaseTexture = (
{ firstColor, secondColor }: HoleOptions,
width: number,
height: number,
): Texture => {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const canvasCtx = canvas.getContext('2d');
if (canvasCtx == null) {
throw Error('Could not get canvas context!');
}
canvasCtx.fillStyle = createGradientFill(
canvas,
canvasCtx,
firstColor,
secondColor,
0,
);
canvasCtx.fillRect(0, 0, canvas.width, canvas.height);
return Texture.from(canvas);
};
export const createScreenTexture = ({
scalingFactor,
}: ScreenOptions): Texture => {
const canvas = document.createElement('canvas');
const size = DEFAULT_TEXTURE_SIZE * scalingFactor;
canvas.width = size;
canvas.height = size;
const canvasCtx = canvas.getContext('2d');
if (canvasCtx == null) {
throw Error('Could not get canvas context!');
}
canvasCtx.fillStyle = 'white';
canvasCtx.fillRect(0, 0, canvas.width, canvas.height);
const baseLineWidth = size / 10;
canvasCtx.strokeStyle = '#AAAAAA';
canvasCtx.lineWidth = baseLineWidth;
canvasCtx.beginPath();
const distanceBetweenLines = size / 3;
for (let i = -canvas.width; i < canvas.width; i++) {
canvasCtx.moveTo(-canvas.width + distanceBetweenLines * i, -canvas.height);
canvasCtx.lineTo(
canvas.width + distanceBetweenLines * i,
canvas.height * 2,
);
}
canvasCtx.stroke();
return Texture.from(canvas);
};
export const createTubingTexture = ({
innerColor,
outerColor,
scalingFactor,
}: TubingOptions): Texture => {
const size = DEFAULT_TEXTURE_SIZE * scalingFactor;
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const canvasCtx = canvas.getContext('2d');
if (canvasCtx == null) {
throw Error('Could not get canvas context!');
}
const gradient = canvasCtx.createLinearGradient(0, 0, 0, size);
const innerColorStart = 0.3;
const innerColorEnd = 0.7;
gradient.addColorStop(0, outerColor);
gradient.addColorStop(innerColorStart, innerColor);
gradient.addColorStop(innerColorEnd, innerColor);
gradient.addColorStop(1, outerColor);
canvasCtx.fillStyle = gradient;
canvasCtx.fillRect(0, 0, canvas.width, canvas.height);
return Texture.from(canvas);
};
export const createCementTexture = ({
firstColor,
secondColor,
scalingFactor,
}: CementOptions): Texture => {
const canvas = document.createElement('canvas');
const size = DEFAULT_TEXTURE_SIZE * scalingFactor;
const lineWidth = scalingFactor;
canvas.width = size;
canvas.height = size;
const canvasCtx = canvas.getContext('2d');
if (canvasCtx == null) {
throw Error('Could not get canvas context!');
}
canvasCtx.fillStyle = firstColor;
canvasCtx.fillRect(0, 0, canvas.width, canvas.height);
canvasCtx.lineWidth = lineWidth;
canvasCtx.fillStyle = secondColor;
canvasCtx.beginPath();
const distanceBetweenLines = size / 12;
for (let i = -canvas.width; i < canvas.width; i++) {
canvasCtx.moveTo(-canvas.width + distanceBetweenLines * i, -canvas.height);
canvasCtx.lineTo(canvas.width + distanceBetweenLines * i, canvas.height);
}
canvasCtx.stroke();
return Texture.from(canvas);
};
export const createCementPlugTexture = ({
firstColor,
secondColor,
scalingFactor,
}: CementPlugOptions): Texture => {
const canvas = document.createElement('canvas');
const size = DEFAULT_TEXTURE_SIZE * scalingFactor;
canvas.width = size;
canvas.height = size;
const canvasCtx = canvas.getContext('2d');
if (canvasCtx == null) {
throw Error('Could not get canvas context!');
}
canvasCtx.fillStyle = firstColor;
canvasCtx.fillRect(0, 0, canvas.width, canvas.height);
canvasCtx.lineWidth = scalingFactor;
canvasCtx.strokeStyle = secondColor;
canvasCtx.beginPath();
canvasCtx.setLineDash([20, 10]);
const distanceBetweenLines = size / 12;
for (let i = -canvas.width; i < canvas.width; i++) {
canvasCtx.moveTo(-canvas.width + distanceBetweenLines * i, -canvas.height);
canvasCtx.lineTo(
canvas.width + distanceBetweenLines * i,
canvas.height * 2,
);
}
canvasCtx.stroke();
return Texture.from(canvas);
};
export const createCementSqueezeTexture = ({
firstColor,
secondColor,
scalingFactor,
}: CementSqueezeOptions): Texture => {
const canvas = document.createElement('canvas');
const size = DEFAULT_TEXTURE_SIZE * scalingFactor;
const lineWidth = scalingFactor;
canvas.width = size;
canvas.height = size;
const canvasCtx = canvas.getContext('2d');
if (canvasCtx == null) {
throw Error('Could not get canvas context!');
}
canvasCtx.lineWidth = lineWidth;
canvasCtx.fillStyle = firstColor;
canvasCtx.strokeStyle = secondColor;
canvasCtx.fillRect(0, 0, canvas.width, canvas.height);
canvasCtx.beginPath();
canvasCtx.setLineDash([20, 10]);
const distanceBetweenLines = size / 12;
for (let i = -canvas.width; i < canvas.width; i++) {
canvasCtx.moveTo(-canvas.width + distanceBetweenLines * i, -canvas.height);
canvasCtx.lineTo(
canvas.width + distanceBetweenLines * i,
canvas.height * 2,
);
}
canvasCtx.stroke();
return Texture.from(canvas);
};
export const createTubularRenderingObject = (
radius: number,
pathPoints: Point[],
): TubularRenderingObject => {
const normals = createNormals(pathPoints);
const rightPath = offsetPoints(pathPoints, normals, radius);
const leftPath = offsetPoints(pathPoints, normals, -radius);
return { leftPath, rightPath };
};
export type CasingInterval = {
kind: 'casing' | 'casing-window';
start: number;
end: number;
};
const createCasingInterval = (start: number, end: number): CasingInterval => ({
kind: 'casing',
start,
end,
});
const createCasingWindowInterval = (
start: number,
end: number,
): CasingInterval => ({ kind: 'casing-window', start, end });
export const getCasingIntervalsWithWindows = (
casing: Casing,
): CasingInterval[] => {
const result = (casing.windows || [])
.filter((cw: CasingWindow) =>
strictlyOverlaps(casing.start, casing.end, cw.start, cw.end),
)
.reduce<{ intervals: CasingInterval[]; lastBottom: number }>(
(
{ intervals, lastBottom },
currentWindow: CasingWindow,
index: number,
list: CasingWindow[],
) => {
const startCasingInterval: CasingInterval | null =
// last bottom before current start?
lastBottom < currentWindow.start
? createCasingInterval(lastBottom, currentWindow.start)
: null;
const updatedLastBottom = startCasingInterval
? startCasingInterval.end
: lastBottom;
const windowStart = Math.max(updatedLastBottom, currentWindow.start);
const windowEnd = Math.min(casing.end, currentWindow.end);
const windowInterval: CasingInterval = createCasingWindowInterval(
windowStart,
windowEnd,
);
const nextLastBottom = windowEnd;
const isLastWindow = index === list.length - 1;
const endCasingInterval: CasingInterval | null =
isLastWindow &&
// still room for a casing interval?
nextLastBottom < casing.end
? createCasingInterval(nextLastBottom, casing.end)
: null;
const newIntervals: CasingInterval[] = [
startCasingInterval,
windowInterval,
endCasingInterval,
].filter((i): i is CasingInterval => i != null);
return {
intervals: [...intervals, ...newIntervals],
lastBottom: nextLastBottom,
};
},
{ intervals: [], lastBottom: casing.start },
);
if (!result.intervals.length) {
return [createCasingInterval(casing.start, casing.end)];
}
return result.intervals;
};
export const prepareCasingRenderObject = (
exaggerationFactor: number,
casing: Casing,
getPathPoints: (start: number, end: number) => Point[],
): CasingRenderObject => {
const exaggeratedDiameter = casing.diameter * exaggerationFactor;
const exaggeratedRadius = exaggeratedDiameter / 2;
const exaggeratedInnerDiameter = casing.innerDiameter * exaggerationFactor;
const exaggeratedInnerRadius = exaggeratedInnerDiameter / 2;
const casingWallWidth = exaggeratedRadius - exaggeratedInnerRadius;
const sections = getCasingIntervalsWithWindows(casing).map(
(casingInterval: CasingInterval) => {
const pathPoints = getPathPoints(
casingInterval.start,
casingInterval.end,
);
const { leftPath, rightPath } = createTubularRenderingObject(
exaggeratedRadius,
pathPoints,
);
return { kind: casingInterval.kind, leftPath, rightPath, pathPoints };
},
);
return {
kind: 'casing',
id: casing.id,
referenceDiameter: exaggeratedDiameter,
referenceRadius: exaggeratedRadius,
sections,
casingWallWidth,
hasShoe: casing.hasShoe,
bottom: casing.end,
};
};
export const createComplexRopeSegmentsForPerforation = (
perforation: Perforation,
casings: Casing[],
holes: HoleSize[],
exaggerationFactor: number,
getPoints: (start: number, end: number) => Point[],
): ComplexRopeSegment[] => {
const { overlappingOuterStrings, overlappingHoles } = findIntersectingItems(
perforation.start,
perforation.end,
casings,
holes,
);
const outerDiameterIntervals = [
...overlappingOuterStrings,
...overlappingHoles,
].map(d => ({
start: d.start,
end: d.end,
}));
const changeDepths = getUniqueDiameterChangeDepths(
[perforation.start, perforation.end],
outerDiameterIntervals,
);
const diameterIntervals = changeDepths.flatMap((depth, index, list) => {
if (index === list.length - 1) {
return [];
}
const nextDepth = list[index + 1]!;
const diameterAtDepth = findPerforationOuterDiameterAtDepth(
overlappingOuterStrings,
overlappingHoles,
depth,
perforation.subKind,
);
return [
{
top: depth,
bottom: nextDepth,
diameter: diameterAtDepth * exaggerationFactor,
},
];
});
const ropeSegments = diameterIntervals.map(interval => {
const points = getPoints(interval.top, interval.bottom);
const diameter = interval.diameter;
return {
diameter,
points,
};
});
return ropeSegments;
};
const drawPacking = (
canvas: HTMLCanvasElement,
ctx: CanvasRenderingContext2D,
perforationOptions: PerforationOptions,
) => {
const { packingOpacity, yellow } = perforationOptions;
ctx.fillStyle = yellow;
ctx.strokeStyle = yellow;
const xy: [number, number] = [0, 0];
const wh: [number, number] = [canvas.width, canvas.height];
ctx.save();
ctx.globalAlpha = packingOpacity;
ctx.fillRect(...xy, ...wh);
ctx.restore();
};
const drawFracLines = (
canvas: HTMLCanvasElement,
ctx: CanvasRenderingContext2D,
extendedPerfShapeDiameter: number,
perforationOptions: PerforationOptions,
startAt: 'diameter' | 'spike',
) => {
const { fracLineCurve } = perforationOptions;
const amountOfSpikes = 10;
const spikeWidth = canvas.width / amountOfSpikes;
const diameter =
(extendedPerfShapeDiameter / 3) * perforationOptions.scalingFactor;
const fracLineLength = diameter / 4;
const spikeLength = diameter / 2;
const offsetX = 0;
const offsetY = startAt === 'diameter' ? 0 : spikeLength;
ctx.globalAlpha = perforationOptions.packingOpacity;
const fracLines = () => {
for (let i = -1; i < amountOfSpikes; i++) {
const bottom: [number, number] = [
i * spikeWidth + offsetX + spikeWidth / 2,
canvas.height / 2 - fracLineLength - offsetY - fracLineLength,
];
ctx.beginPath();
const start: [number, number] = [...bottom];
const controlPoint1: [number, number] = [
bottom[0] - fracLineCurve * 2,
bottom[1] - fracLineLength / 4,
];
const middle: [number, number] = [
bottom[0],
bottom[1] - fracLineLength / 2,
];
const controlPoint2: [number, number] = [
bottom[0] + fracLineCurve * 2,
bottom[1] - fracLineLength / 2 - fracLineLength / 4,
];
const end: [number, number] = [bottom[0], bottom[1] - fracLineLength];
ctx.bezierCurveTo(...start, ...controlPoint1, ...middle);
ctx.bezierCurveTo(...middle, ...controlPoint2, ...end);
ctx.stroke();
}
for (let i = -1; i < amountOfSpikes; i++) {
const bottom: [number, number] = [
i * spikeWidth + spikeWidth + offsetX + spikeWidth / 2,
canvas.height / 2 + diameter / 2 + offsetY,
];
ctx.beginPath();
const start: [number, number] = [...bottom];
const controlPoint1: [number, number] = [
bottom[0] - fracLineCurve * 2,
bottom[1] + fracLineLength / 4,
];
const middle: [number, number] = [
bottom[0],
bottom[1] + fracLineLength / 2,
];
const controlPoint2: [number, number] = [
bottom[0] + fracLineCurve * 2,
bottom[1] + fracLineLength / 2 + fracLineLength / 4,
];
const end: [number, number] = [bottom[0], bottom[1] + fracLineLength];
ctx.bezierCurveTo(...start, ...controlPoint1, ...middle);
ctx.bezierCurveTo(...middle, ...controlPoint2, ...end);
ctx.stroke();
}
};
ctx.strokeStyle = perforationOptions.yellow;
ctx.lineWidth = 6;
ctx.save();
fracLines();
ctx.restore();
ctx.lineWidth = 1;
ctx.strokeStyle = perforationOptions.outline;
fracLines();
ctx.closePath();
};
const drawSpikes = (
canvas: HTMLCanvasElement,
ctx: CanvasRenderingContext2D,
extendedPerfShapeDiameter: number,
perforationOptions: PerforationOptions,
) => {
const amountOfSpikes = 4;
const spikeWidth = canvas.width / amountOfSpikes;
ctx.strokeStyle = perforationOptions.outline;
const diameter =
(extendedPerfShapeDiameter / 3) * perforationOptions.scalingFactor;
ctx.lineWidth = 1;
const spikeLength = diameter / 2;
// left spikes
for (let i = 0; i <= amountOfSpikes; i++) {
const left: [number, number] = [
i * spikeWidth,
canvas.height / 2 - diameter / 2,
];
const bottom: [number, number] = [
left[0] - spikeWidth / 2,
left[1] - spikeLength,
];
const right: [number, number] = [left[0] - spikeWidth, left[1]];
ctx.beginPath();
ctx.moveTo(...left);
ctx.lineTo(...bottom);
ctx.lineTo(...right);
ctx.fill();
ctx.lineWidth = 1;
ctx.stroke();
}
// right spikes
for (let i = 0; i <= amountOfSpikes; i++) {
const left: [number, number] = [
i * spikeWidth,
canvas.height / 2 + diameter / 2,
];
const bottom: [number, number] = [
left[0] - spikeWidth / 2,
left[1] + spikeLength,
];
const right: [number, number] = [left[0] - spikeWidth, left[1]];
ctx.beginPath();
ctx.moveTo(...left);
ctx.lineTo(...bottom);
ctx.lineTo(...right);
ctx.fill();
ctx.lineWidth = 1;
ctx.stroke();
}
ctx.closePath();
};
// for visual debugging
// if this shoes up, something is wrong
const errorTexture = (
errorMessage = 'Error!',
existingContext?: {
canvas: HTMLCanvasElement;
canvasCtx: CanvasRenderingContext2D;
},
) => {
console.error(`${errorMessage}`);
const canvas = existingContext?.canvas || document.createElement('canvas');
const size = DEFAULT_TEXTURE_SIZE;
canvas.width = size / 2;
canvas.height = size;
const canvasCtx = existingContext?.canvasCtx || canvas.getContext('2d');
const xy: [number, number] = [0, 0];
const wh: [number, number] = [canvas.width, canvas.height];
if (canvasCtx == null) {
throw Error('Could not get canvas context!');
}
canvasCtx.fillStyle = '#ff00ff';
canvasCtx.fillRect(...xy, ...wh);
const texture = new Texture({
source: new CanvasSource({
resource: canvas,
wrapMode: 'clamp-to-edge',
}),
orig: new Rectangle(0, 0, canvas.width, canvas.height),
rotate: groupD8.MIRROR_HORIZONTAL,
});
return texture;
};
const createPerforationCanvas = (
perfShape: ComplexRopeSegment,
options: PerforationOptions,
): { canvas: HTMLCanvasElement; ctx: CanvasRenderingContext2D } => {
const canvas = document.createElement('canvas');
const perfShapeDiameter = perfShape.diameter;
const size = perfShapeDiameter * options.scalingFactor;
canvas.width = size / 2;
canvas.height = size;
const ctx = canvas.getContext('2d');
if (ctx == null) {
throw Error('Could not get canvas context!');
}
return { canvas, ctx };
};
const createPerforationTexture = (canvas: HTMLCanvasElement) => {
const texture = new Texture({
source: new CanvasSource({
resource: canvas,
}),
orig: new Rectangle(0, 0, canvas.width, canvas.height),
rotate: groupD8.MIRROR_HORIZONTAL,
});
return texture;
};
const compareIntersectingPerforationsBy =
(targetPerf: Perforation, comparedPerforations: Perforation[]) =>
(compareFunc: (comparedPerf: Perforation) => boolean) =>
comparedPerforations.some(
perf => compareFunc(perf) && intersect(targetPerf, perf),
);
/**
* @Perforation
* If a perforation does not overlap with another perforations of type with gravel,
* the perforation spikes are either red when open or grey when closed.
* Open and closed refers to two fields on a perforation item referencing runs.
*
* If a perforation overlaps with another perforation of type with gravel and the perforation is open,
* the perforation spikes should be yellow. If closed the perforation remains grey.
*
* Cased Hole Frac Pack:
* Makes perforations of type "Perforation" yellow if overlapping and perforation are open.
* If a perforation of type "perforation" is overlapping, the fracturation lines extends from the tip of the perforation spikes into formation.
*
* Cased Hole Gravel Pack:
* Yellow gravel. Makes perforations of type "Perforation" yellow if overlapping and perforation are open.
*
* Cased Hole Fracturation:
* Makes perforations of type "Perforation" yellow if overlapping and perforation are open.
*/
const createSubkindPerforationTexture = {
packing: () => errorTexture(),
fracLines: () => errorTexture(),
spikes: (
perforation: Perforation,
perfShape: ComplexRopeSegment,
otherPerforations: Perforation[],
perforationOptions: PerforationOptions,
): Texture => {
const { canvas, ctx } = createPerforationCanvas(
perfShape,
perforationOptions,
);
const compareBy = compareIntersectingPerforationsBy(
perforation,
otherPerforations,
);
const intersectionsWithCasedHoleGravel: boolean = compareBy(
isSubkindCasedHoleGravelPack,
);
const intersectsWithCasedHoleFracturation: boolean = compareBy(
isSubKindCasedHoleFracturation,
);
const intersectionsWithCasedHoleFracPack: boolean = compareBy(
isSubKindCasedHoleFracPack,
);
const intersectsWithPerforation =
intersectionsWithCasedHoleGravel ||
intersectsWithCasedHoleFracturation ||
intersectionsWithCasedHoleFracPack;
const openPerforationSpikeColor = intersectsWithPerforation
? perforationOptions.yellow
: perforationOptions.red;
ctx.globalAlpha = perforationOptions.packingOpacity;
if (perforation.isOpen) {
ctx.fillStyle = openPerforationSpikeColor;
ctx.strokeStyle = openPerforationSpikeColor;
} else {
ctx.fillStyle = perforationOptions.grey;
ctx.strokeStyle = perforationOptions.grey;
}
drawSpikes(canvas, ctx, perfShape.diameter, perforationOptions);
if (intersectionsWithCasedHoleFracPack) {
drawFracLines(
canvas,
ctx,
perfShape.diameter,
perforationOptions,
'spike',
);
}
return createPerforationTexture(canvas);
},
};
/**
* @Cased_hole_fracturation
* Yellow fracturation lines from casing OD into formation
*/
const createSubkindCasedHoleFracturationTexture = {
packing: () => errorTexture(),
fracLines: (
perfShape: ComplexRopeSegment,
perforationOptions: PerforationOptions,
): Texture => {
const { canvas, ctx } = createPerforationCanvas(
perfShape,
perforationOptions,
);
drawFracLines(
canvas,
ctx,
perfShape.diameter,
perforationOptions,
'diameter',
);
return createPerforationTexture(canvas);
},
spikes: () => errorTexture(),
};
/**
* @Cased_hole_frac_pack
* Yellow gravel and fracturation lines.
* Makes perforations of type "Perforation" yellow if overlapping and perforation are open.
* If no perforation of type "perforation" are overlapping, there are no fracturation lines and no spikes.
* If a perforation of type "perforation" is overlapping, the fracturation lines extends from the tip of the perforation spikes into formation.
*/
const createSubkindCasedHoleFracPackTexture = {
packing: (
perfShape: ComplexRopeSegment,
perforationOptions: PerforationOptions,
): Texture => {
const { canvas, ctx } = createPerforationCanvas(
perfShape,
perforationOptions,
);
drawPacking(canvas, ctx, perforationOptions);
return createPerforationTexture(canvas);
},
fracLines: (
perfShape: ComplexRopeSegment,
perforationOptions: PerforationOptions,
) => {
const { canvas } = createPerforationCanvas(perfShape, perforationOptions);
return createPerforationTexture(canvas);
},
spikes: () => errorTexture(),
};
/**
* @Cased_hole_gravel_pack
* Yellow gravel. Makes perforations of type "Perforation" yellow if overlapping and perforation are open.
*/
const createSubkindCasedHoleGravelPackTexture = {
packing: (
perfShape: ComplexRopeSegment,
perforationOptions: PerforationOptions,
): Texture => {
const { canvas, ctx } = createPerforationCanvas(
perfShape,
perforationOptions,
);
drawPacking(canvas, ctx, perforationOptions);
return createPerforationTexture(canvas);
},
fracLines: () => errorTexture(),
spikes: () => errorTexture(),
};
/**
* @Open_hole_gravel_pack
* Yellow gravel
*/
const createSubkindOpenHoleGravelPackTexture = {
packing: (
perfShape: ComplexRopeSegment,
perforationOptions: PerforationOptions,
) => {
const { canvas, ctx } = createPerforationCanvas(
perfShape,
perforationOptions,
);
drawPacking(canvas, ctx, perforationOptions);
return createPerforationTexture(canvas);
},
fracLines: () => errorTexture(),
spikes: () => errorTexture(),
};
/**
* @Open_hole_frac_pack
* Yellow gravel. Yellow frac lines from hole OD into formation
*/
const createSubkindOpenHoleFracPackTexture = {
packing: (
_perforation: Perforation,
perfShape: ComplexRopeSegment,
perforationOptions: PerforationOptions,
) => {
const { canvas, ctx } = createPerforationCanvas(
perfShape,
perforationOptions,
);
drawPacking(canvas, ctx, perforationOptions);
return createPerforationTexture(canvas);
},
fracLines: (
perfShape: ComplexRopeSegment,
perforationOptions: PerforationOptions,
): Texture => {
const { canvas, ctx } = createPerforationCanvas(
perfShape,
perforationOptions,
);
drawFracLines(
canvas,
ctx,
perfShape.diameter,
perforationOptions,
'diameter',
);
return createPerforationTexture(canvas);
},
spikes: () => errorTexture(),
};
export const createPerforationPackingTexture = (
perforation: Perforation,
perfShape: ComplexRopeSegment,
perforationOptions: PerforationOptions,
): Texture => {
return foldPerforationSubKind(
{
Perforation: () => createSubkindPerforationTexture.packing(),
CasedHoleFracturation: () =>
createSubkindCasedHoleFracPackTexture.packing(
perfShape,
perforationOptions,
),
CasedHoleFracPack: () =>
createSubkindCasedHoleFracPackTexture.packing(
perfShape,
perforationOptions,
),
OpenHoleGravelPack: () =>
createSubkindOpenHoleGravelPackTexture.packing(
perfShape,
perforationOptions,
),
OpenHoleFracPack: () =>
createSubkindOpenHoleFracPackTexture.packing(
perforation,
perfShape,
perforationOptions,
),
CasedHoleGravelPack: () =>
createSubkindCasedHoleGravelPackTexture.packing(
perfShape,
perforationOptions,
),
},
perforation.subKind,
);
};
export const createPerforationFracLineTexture = (
perforation: Perforation,
perfShape: ComplexRopeSegment,
perforationOptions: PerforationOptions,
): Texture => {
return foldPerforationSubKind(
{
Perforation: () => createSubkindPerforationTexture.fracLines(),
OpenHoleGravelPack: () =>
createSubkindOpenHoleGravelPackTexture.fracLines(),
OpenHoleFracPack: () =>
createSubkindOpenHoleFracPackTexture.fracLines(
perfShape,
perforationOptions,
),
CasedHoleFracturation: () =>
createSubkindCasedHoleFracturationTexture.fracLines(
perfShape,
perforationOptions,
),
CasedHoleGravelPack: () =>
createSubkindCasedHoleGravelPackTexture.fracLines(),
CasedHoleFracPack: () =>
createSubkindCasedHoleFracPackTexture.fracLines(
perfShape,
perforationOptions,
),
},
perforation.subKind,
);
};
export const createPerforationSpikeTexture = (
perforation: Perforation,
otherPerforations: Perforation[],
perfShape: ComplexRopeSegment,
perforationOptions: PerforationOptions,
): Texture => {
return foldPerforationSubKind(
{
Perforation: () =>
createSubkindPerforationTexture.spikes(
perforation,
perfShape,
otherPerforations,
perforationOptions,
),
OpenHoleGravelPack: () => createSubkindOpenHoleGravelPackTexture.spikes(),
OpenHoleFracPack: () => createSubkindOpenHoleFracPackTexture.spikes(),
CasedHoleFracturation: () =>
createSubkindCasedHoleFracturationTexture.spikes(),
CasedHoleGravelPack: () =>
createSubkindCasedHoleGravelPackTexture.spikes(),
CasedHoleFracPack: () => createSubkindCasedHoleFracPackTexture.spikes(),
},
perforation.subKind,
);
};