UNPKG

@equinor/esv-intersection

Version:

Intersection component package with testing and automatic documentation.

1,233 lines (1,105 loc) 35.6 kB
import { max } from 'd3-array'; import { scaleLinear, ScaleLinear } from 'd3-scale'; import { Assets, Graphics, groupD8, Point, Rectangle, MeshRope, Texture, } from 'pixi.js'; import { DashLine } from '../vendor/pixi-dashed-line'; import { LayerOptions, PixiLayer, PixiRenderApplication } from '.'; import { DEFAULT_TEXTURE_SIZE, EXAGGERATED_DIAMETER, HOLE_OUTLINE, SCREEN_OUTLINE, } from '../constants'; import { assertNever, Casing, CasingOptions, Cement, CementOptions, CementPlugOptions, CementSqueeze, CementSqueezeOptions, foldCompletion, HoleOptions, HoleSize, isCementSqueeze, PAndA, SchematicData, ScreenOptions, TubingOptions, Screen, Tubing, CompletionSymbol, isPAndASymbol, isCementPlug, CementPlug, PAndASymbol, InternalLayerOptions, defaultHoleOptions, defaultCasingOptions, defaultCementOptions, defaultCementSqueezeOptions, defaultCementPlugOptions, defaultScreenOptions, defaultTubingOptions, defaultInternalLayerOptions, Perforation, PerforationOptions, defaultPerforationOptions, Completion, OutlineClosure, hasPacking, hasFracLines, hasSpikes, } from './schematicInterfaces'; import { CasingRenderObject, createCementTexture, createComplexRopeSegmentsForCement, createComplexRopeSegmentsForCementSqueeze, createComplexRopeSegmentsForCementPlug, createHoleBaseTexture, createScreenTexture, createTubingTexture, createTubularRenderingObject, prepareCasingRenderObject, createCementPlugTexture, createComplexRopeSegmentsForPerforation, createPerforationPackingTexture, PerforationShape, createCementSqueezeTexture, createPerforationFracLineTexture, createPerforationSpikeTexture, } from '../datautils/schematicShapeGenerator'; import { OnUpdateEvent, OnRescaleEvent, OnUnmountEvent } from '../interfaces'; import { convertColor } from '../utils/color'; import { createNormals, offsetPoint, offsetPoints } from '../utils/vectorUtils'; import { ComplexRope, ComplexRopeSegment, } from './CustomDisplayObjects/ComplexRope'; import { FixedWidthSimpleRope } from './CustomDisplayObjects/FixedWidthSimpleRope'; import { UniformTextureStretchRope } from './CustomDisplayObjects/UniformTextureStretchRope'; interface ScalingFactors { height: number; zFactor: number; yScale: ScaleLinear<number, number, never>; } interface SymbolRenderObject { pathPoints: Point[]; referenceDiameter: number; symbolKey: string; } interface CementRenderObject { kind: 'cement'; segments: ComplexRopeSegment[]; casingIds: string[]; zIndex?: number; } interface CementSqueezeRenderObject { kind: 'cementSqueeze'; segments: ComplexRopeSegment[]; casingIds: string[]; zIndex?: number; } type InterlacedRenderObjects = | CasingRenderObject | CementRenderObject | CementSqueezeRenderObject; const foldInterlacedRenderObjects = <T>( fCasing: (obj: CasingRenderObject) => T, fCement: (obj: CementRenderObject) => T, fCementSqueeze: (obj: CementSqueezeRenderObject) => T, ) => (renderObject: InterlacedRenderObjects): T => { switch (renderObject.kind) { case 'casing': return fCasing(renderObject); case 'cement': return fCement(renderObject); case 'cementSqueeze': return fCementSqueeze(renderObject); default: return assertNever(renderObject); } }; export interface SchematicLayerOptions< T extends SchematicData, > extends LayerOptions<T> { exaggerationFactor?: number; internalLayerOptions?: InternalLayerOptions; holeOptions?: HoleOptions; casingOptions?: CasingOptions; cementOptions?: CementOptions; cementSqueezeOptions?: CementSqueezeOptions; screenOptions?: ScreenOptions; tubingOptions?: TubingOptions; cementPlugOptions?: CementPlugOptions; perforationOptions?: PerforationOptions; } const defaultSchematicLayerOptions = ( layerId: string, ): SchematicLayerOptions<SchematicData> => ({ exaggerationFactor: 2, internalLayerOptions: defaultInternalLayerOptions(layerId), holeOptions: defaultHoleOptions, casingOptions: defaultCasingOptions, cementOptions: defaultCementOptions, cementSqueezeOptions: defaultCementSqueezeOptions, screenOptions: defaultScreenOptions, tubingOptions: defaultTubingOptions, cementPlugOptions: defaultCementPlugOptions, perforationOptions: defaultPerforationOptions, }); type InternalLayerVisibility = { [K in keyof InternalLayerOptions]: boolean }; export class SchematicLayer<T extends SchematicData> extends PixiLayer<T> { private internalLayerVisibility: InternalLayerVisibility = { holeLayerId: true, casingLayerId: true, completionLayerId: true, cementLayerId: true, pAndALayerId: true, perforationLayerId: true, }; private cementTextureCache: Texture | null = null; private cementSqueezeTextureCache: Texture | null = null; private cementPlugTextureCache: Texture | null = null; private holeTextureCache: Texture | null = null; private screenTextureCache: Texture | null = null; private tubingTextureCache: Texture | null = null; private textureSymbolCacheArray: { [key: string]: Texture } | null = null; protected scalingFactors: ScalingFactors = { height: 600, zFactor: 1, yScale: scaleLinear(), }; constructor( ctx: PixiRenderApplication, id?: string, options?: SchematicLayerOptions<T>, ) { super(ctx, id, options); this.options = <SchematicLayerOptions<T>>{ ...this.options, ...defaultSchematicLayerOptions(this.id), ...options, }; } public override onUnmount(event?: OnUnmountEvent): void { super.onUnmount(event); this.cementTextureCache = null; this.cementSqueezeTextureCache = null; this.holeTextureCache = null; this.screenTextureCache = null; this.tubingTextureCache = null; this.textureSymbolCacheArray = null; } public override async onUpdate(event: OnUpdateEvent<T>): Promise<void> { super.onUpdate(event); this.clearLayer(); await this.preRender(); this.render(); } public override async onRescale(event: OnRescaleEvent): Promise<void> { const shouldRecalculate = this.scalingFactors.zFactor !== event.zFactor; this.scalingFactors = { height: event.height, zFactor: event.zFactor, yScale: event.yScale, }; super.optionsRescale(event); const yRatio = this.yRatio(); const flippedX = event.xBounds[0] > event.xBounds[1]; const flippedY = event.yBounds[0] > event.yBounds[1]; this.setContainerPosition(event.xScale(0), event.yScale(0)); this.setContainerScale( event.xRatio * (flippedX ? -1 : 1), yRatio * (flippedY ? -1 : 1), ); if (shouldRecalculate) { this.clearLayer(); await this.preRender(); } this.render(); } public override async setVisibility(isVisible: boolean, layerId: string) { if (layerId === this.id) { super.setVisibility(isVisible, layerId); return; } const { internalLayerOptions } = this.options as SchematicLayerOptions<T>; const entries = internalLayerOptions ? Object.entries(internalLayerOptions) : []; const entryFound = entries.find( ([_key, id]: [string, string]) => id === layerId, ); const keyFound = entryFound?.[0]; if (keyFound) { this.internalLayerVisibility[keyFound as keyof InternalLayerVisibility] = isVisible; this.clearLayer(); await this.preRender(); this.render(); } } public override getInternalLayerIds(): string[] { const { internalLayerOptions } = this.options as SchematicLayerOptions<T>; return internalLayerOptions ? Object.values(internalLayerOptions) : []; } /** * Calculate yRatio without zFactor * TODO consider to move this into ZoomPanHandler */ protected yRatio(): number { const domain = this.scalingFactors.yScale.domain() as [number, number]; const ySpan = domain[1] - domain[0]; const baseYSpan = ySpan * this.scalingFactors.zFactor; const baseDomain: [number, number] = [domain[0], domain[0] + baseYSpan]; return Math.abs( this.scalingFactors.height / (baseDomain[1] - baseDomain[0]), ); } protected getZFactorScaledPathForPoints = ( start: number, end: number, ): Point[] => { const y = (y: number): number => y * this.scalingFactors.zFactor; const path = this.referenceSystem?.getCurtainPath(start, end, true) ?? []; return path.map(p => new Point(p.point[0], y(p.point[1]!))); }; protected drawBigPolygon = (coords: Point[], color = 0x000000) => { const polygon = new Graphics(); polygon.poly(coords); polygon.fill(color); this.addChild(polygon); }; protected drawRope(path: Point[], texture: Texture, tint?: number): void { if (path.length === 0) { return undefined; } const rope: MeshRope = new MeshRope({ texture, points: path, textureScale: 1, }); rope.tint = tint || rope.tint; this.addChild(rope); } /** * * @param leftPath Points for line on left side * @param rightPath Points for line on right side * @param lineColor Color of line * @param lineWidth Width of line * @param outlineClosure If line should be drawn at top and/or bottom of the paths * @param lineAlignment alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outer). */ protected drawOutline( leftPath: Point[], rightPath: Point[], lineColor: number, lineWidth = 1, outlineClosure: OutlineClosure = 'None', lineAlignment = 1, ): void { const leftPathReverse = leftPath.map<Point>(d => d.clone()).reverse(); const startPointRight = rightPath[0]!; const startPointLeft = leftPathReverse[0]!; const line = new Graphics(); line.moveTo(startPointRight.x, startPointRight.y); rightPath.forEach((p: Point) => line.lineTo(p.x, p.y)); if (outlineClosure === 'None' || outlineClosure === 'Top') { line.moveTo(startPointLeft.x, startPointLeft.y); } leftPathReverse.forEach((p: Point) => line.lineTo(p.x, p.y)); if (outlineClosure === 'TopAndBottom' || outlineClosure === 'Top') { line.lineTo(startPointRight.x, startPointRight.y); } line.stroke({ width: lineWidth, color: lineColor, alignment: lineAlignment, }); this.addChild(line); } /** * Uses a dashed outline on one side to represent casing window * The casing window should be visualized at the upper side of the wellbore path * @param leftPath Points for line on left side * @param pointPath Points for line on right side * @param lineColor Color of line * @param lineWidth Width of line * @param lineAlignment alignment of the line to draw, (0 = inner, 0.5 = middle, 1 = outer). */ protected drawCasingWindowOutline( leftPath: Point[], rightPath: Point[], { lineColor, windowOptions }: CasingOptions, lineWidth = 1, ): void { // Correct the dashed path. Should always be displayed on the upper side of the wellbore path. const flippedPaths = !!this.referenceSystem?.options?.calculateDisplacementFromBottom; const [linePath, dashedPath] = flippedPaths ? [leftPath, rightPath] : [rightPath, leftPath]; const [dashedAlignment, solidAlignment] = flippedPaths ? [1, 0] : [0, 1]; const graphics = new Graphics(); const startPointLinePath = linePath[0]!; graphics.moveTo(startPointLinePath.x, startPointLinePath.y); linePath.forEach((p: Point) => graphics.lineTo(p.x, p.y)); graphics.setStrokeStyle({ width: lineWidth, color: convertColor(lineColor), alignment: solidAlignment, }); const dashedLine = new DashLine(graphics, { dash: [windowOptions.dashLength, windowOptions.spaceLength], color: convertColor(windowOptions.dashColor), width: lineWidth, alignment: dashedAlignment, }); const startPointDashedPath = dashedPath[0]!; dashedLine.moveTo(startPointDashedPath.x, startPointDashedPath.y); dashedPath.forEach((currentPoint: Point) => { dashedLine.lineTo(currentPoint.x, currentPoint.y); }); this.addChild(graphics); } private perforationRopeAndTextureReferences: { rope: ComplexRope; texture: Texture; }[] = []; public async preRender(): Promise<void> { if (!this.data || !this.referenceSystem) { return; } const { exaggerationFactor = 1 } = this.options as SchematicLayerOptions<T>; const { holeSizes, casings, cements, completion, symbols, pAndA, perforations, } = this.data; await this.updateSymbolCache(symbols); holeSizes.sort((a: HoleSize, b: HoleSize) => b.diameter - a.diameter); const maxHoleDiameter = holeSizes.length > 0 ? (max(holeSizes, d => d.diameter) ?? 0) * exaggerationFactor : EXAGGERATED_DIAMETER * exaggerationFactor; if (this.internalLayerVisibility.holeLayerId) { holeSizes.forEach((hole: HoleSize) => this.drawHoleSize(maxHoleDiameter, hole), ); } casings.sort((a: Casing, b: Casing) => b.diameter - a.diameter); const casingRenderObjects: CasingRenderObject[] = casings.map( (casing: Casing) => this.createCasingRenderObject(casing), ); const cementShapes: CementRenderObject[] = cements.map( (cement: Cement): CementRenderObject => ({ kind: 'cement', segments: createComplexRopeSegmentsForCement( cement, casings, completion, holeSizes, exaggerationFactor, this.getZFactorScaledPathForPoints, ), casingIds: (cement.referenceIds || []).filter(id => id), }), ); const [cementSqueezes, remainingPAndA] = pAndA.reduce< [CementSqueeze[], Exclude<PAndA, CementSqueeze>[]] >( ([squeezes, remaining], current: PAndA) => isCementSqueeze(current) ? [[current, ...squeezes], remaining] : [squeezes, [current, ...remaining]], [[], []], ); const cementSqueezesShape: CementSqueezeRenderObject[] = cementSqueezes.map( squeeze => ({ kind: 'cementSqueeze', segments: this.createCementSqueezeShape( squeeze, casings, completion, holeSizes, ), casingIds: squeeze.referenceIds, }), ); this.sortCementAndCasingRenderObjects( casingRenderObjects, cementShapes, cementSqueezesShape, ).forEach( foldInterlacedRenderObjects( (casingRO: CasingRenderObject) => { if (this.internalLayerVisibility.casingLayerId) { this.drawCasing(casingRO); if (casingRO.hasShoe) { this.drawShoe(casingRO.bottom, casingRO.referenceRadius); } } }, (cementRO: CementRenderObject) => { if (this.internalLayerVisibility.cementLayerId) { const texture = this.getCementTexture(); texture && this.drawComplexRope(cementRO.segments, texture); } }, (cementSqueezesRO: CementSqueezeRenderObject) => { if (this.internalLayerVisibility.pAndALayerId) { const texture = this.getCementSqueezeTexture(); texture && this.drawComplexRope(cementSqueezesRO.segments, texture); } }, ), ); this.perforationRopeAndTextureReferences.forEach(({ rope, texture }) => { if (!rope.destroyed) { rope.destroy({ children: true, texture: true, textureSource: true, }); } if (texture) { texture.destroy(true); } }); this.perforationRopeAndTextureReferences = []; if (this.internalLayerVisibility.perforationLayerId) { const { perforationOptions } = this.options as SchematicLayerOptions<T>; const packings = perforations.filter(hasPacking); const fracLines = perforations.filter(hasFracLines); const spikes = perforations.filter(hasSpikes); packings.forEach(perforation => { const perfShapes = this.createPerforationShape( perforation, casings, holeSizes, ); const perfShapesByDiameter: { [key: number]: ComplexRopeSegment[] } = perfShapes.reduce( (dict: { [key: number]: ComplexRopeSegment[] }, ps) => { if (!dict[ps.diameter]) { dict[ps.diameter] = []; } dict[ps.diameter] = [...(dict[ps.diameter] ?? []), ps]; return dict; }, {}, ); Object.values(perfShapesByDiameter).forEach( perfShapesWithSameDiameter => { const texture = createPerforationPackingTexture( perforation, perfShapesWithSameDiameter[0]!, perforationOptions!, ); const rope = this.drawComplexRope( perfShapesWithSameDiameter, texture, ); rope && this.perforationRopeAndTextureReferences.push({ rope, texture }); }, ); }); spikes.forEach(perforation => { const perfShapes = this.createPerforationShape( perforation, casings, holeSizes, ); const thiccPerfShapes = perfShapes.map(ps => ({ ...ps, diameter: ps.diameter * 3, })); const perfShapesByDiameter: { [key: number]: ComplexRopeSegment[] } = thiccPerfShapes.reduce( (dict: { [key: number]: ComplexRopeSegment[] }, ps) => { if (!dict[ps.diameter]) { dict[ps.diameter] = []; } dict[ps.diameter] = [...(dict[ps.diameter] ?? []), ps]; return dict; }, {}, ); Object.values(perfShapesByDiameter).forEach( perfShapesWithSameDiameter => { perfShapesWithSameDiameter.forEach(perfShape => { const texture = createPerforationSpikeTexture( perforation, perforations, perfShape, perforationOptions!, ); const rope = this.drawComplexRope([perfShape], texture); rope && this.perforationRopeAndTextureReferences.push({ rope, texture, }); }); }, ); }); fracLines.forEach(perforation => { const perfShapes = this.createPerforationShape( perforation, casings, holeSizes, ); const thiccPerfShapes = perfShapes.map(ps => ({ ...ps, diameter: ps.diameter * 3, })); const perfShapesByDiameter: { [key: number]: ComplexRopeSegment[] } = thiccPerfShapes.reduce( (dict: { [key: number]: ComplexRopeSegment[] }, ps) => { if (!dict[ps.diameter]) { dict[ps.diameter] = []; } dict[ps.diameter] = [...(dict[ps.diameter] ?? []), ps]; return dict; }, {}, ); Object.values(perfShapesByDiameter).forEach( perfShapesWithSameDiameter => { perfShapesWithSameDiameter.forEach(perfShape => { const texture = createPerforationFracLineTexture( perforation, perfShape, perforationOptions!, ); const rope = this.drawComplexRope([perfShape], texture); rope && this.perforationRopeAndTextureReferences.push({ rope, texture, }); }); }, ); }); } if (this.internalLayerVisibility.completionLayerId) { completion.forEach( foldCompletion( (obj: Screen) => this.drawScreen(obj), (obj: Tubing) => this.drawTubing(obj), (obj: CompletionSymbol) => { const symbolRenderObject = this.prepareSymbolRenderObject(obj); this.drawSymbolComponent(symbolRenderObject); }, ), ); } if (this.internalLayerVisibility.pAndALayerId) { remainingPAndA.forEach(obj => { if (isPAndASymbol(obj)) { const symbolRenderObject = this.prepareSymbolRenderObject(obj); this.drawSymbolComponent(symbolRenderObject); } if (isCementPlug(obj)) { this.drawCementPlug(obj, casings, completion, holeSizes); } }); } } private async updateSymbolCache(symbols: { [key: string]: string }) { if (!this.textureSymbolCacheArray) { this.textureSymbolCacheArray = {}; } if (!symbols) { return; } const existingKeys = Object.keys(this.textureSymbolCacheArray); const promises = Object.entries(symbols).map( async ([key, symbol]: [string, string]) => { if (!existingKeys.includes(key) && this.textureSymbolCacheArray) { this.textureSymbolCacheArray[key] = await Assets.load(symbol); } }, ); await Promise.all(promises); } private drawCementPlug( cementPlug: CementPlug, casings: Casing[], completion: Completion[], holes: HoleSize[], ) { const { exaggerationFactor = 1, cementPlugOptions } = this .options as SchematicLayerOptions<T>; const cementPlugSegments = createComplexRopeSegmentsForCementPlug( cementPlug, casings, completion, holes, exaggerationFactor, this.getZFactorScaledPathForPoints, ); cementPlugOptions && this.drawComplexRope( cementPlugSegments, this.getCementPlugTexture(cementPlugOptions), ); const { rightPath, leftPath } = cementPlugSegments.reduce<{ rightPath: Point[]; leftPath: Point[]; }>( (acc, current) => { const { leftPath, rightPath } = createTubularRenderingObject( current.diameter / 2, current.points, ); return { rightPath: [...acc.rightPath, ...rightPath], leftPath: [...acc.leftPath, ...leftPath], }; }, { rightPath: [], leftPath: [] }, ); this.drawOutline( leftPath, rightPath, convertColor('black'), 0.25, 'TopAndBottom', ); } private createCasingRenderObject(casing: Casing): CasingRenderObject { const { exaggerationFactor = 1 } = this.options as SchematicLayerOptions<T>; return prepareCasingRenderObject( exaggerationFactor, casing, this.getZFactorScaledPathForPoints, ); } private getCementPlugTexture(cementPlugOptions: CementPlugOptions): Texture { if (!this.cementPlugTextureCache) { this.cementPlugTextureCache = createCementPlugTexture(cementPlugOptions); } return this.cementPlugTextureCache; } private prepareSymbolRenderObject = ( component: CompletionSymbol | PAndASymbol, ): SymbolRenderObject => { const { exaggerationFactor = 1 } = this.options as SchematicLayerOptions<T>; const exaggeratedDiameter = component.diameter * exaggerationFactor; const pathPoints = this.getZFactorScaledPathForPoints( component.start, component.end, ); return { pathPoints, referenceDiameter: exaggeratedDiameter, symbolKey: component.symbolKey, }; }; private drawSymbolComponent = ({ pathPoints, referenceDiameter, symbolKey, }: SymbolRenderObject): void => { const texture = this.getSymbolTexture(symbolKey, referenceDiameter); // The rope renders fine in CANVAS/fallback mode this.drawSVGRope(pathPoints, texture!); }; private drawSVGRope(path: Point[], texture: Texture): void { if (path.length === 0) { return undefined; } const rope: UniformTextureStretchRope = new UniformTextureStretchRope( texture, path, ); this.addChild(rope); } private getSymbolTexture( symbolKey: string, diameter: number, ): Texture | undefined { const baseTexture = this.textureSymbolCacheArray?.[symbolKey]?.source; return baseTexture ? new Texture({ source: baseTexture, orig: new Rectangle(0, 0, 0, diameter), rotate: groupD8.MAIN_DIAGONAL, }) : undefined; } private drawHoleSize = ( maxHoleDiameter: number, holeObject: HoleSize, ): void => { if (holeObject == null) { return; } const pathPoints = this.getZFactorScaledPathForPoints( holeObject.start, holeObject.end, ); if (pathPoints.length === 0) { return; } const { exaggerationFactor = 1, holeOptions } = this .options as SchematicLayerOptions<T>; const exaggeratedDiameter = holeObject.diameter * exaggerationFactor; const { rightPath, leftPath } = createTubularRenderingObject( exaggeratedDiameter / 2, pathPoints, ); const texture = this.getHoleTexture( holeOptions!, exaggeratedDiameter, maxHoleDiameter, ); this.drawHoleRope(pathPoints, texture, maxHoleDiameter); this.drawOutline( leftPath, rightPath, convertColor(holeOptions!.lineColor), HOLE_OUTLINE * exaggerationFactor, 'TopAndBottom', 0, ); }; private drawHoleRope( path: Point[], texture: Texture, maxHoleDiameter: number, ): void { if (path.length === 0) { return undefined; } const rope: MeshRope = new MeshRope({ texture, points: path, textureScale: maxHoleDiameter / DEFAULT_TEXTURE_SIZE, }); this.addChild(rope); } private getHoleTexture( holeOptions: HoleOptions, diameter: number, maxHoleDiameter: number, ): Texture { const size = DEFAULT_TEXTURE_SIZE; const height = size; const width = size; const textureDiameter = (diameter / maxHoleDiameter) * size; if (!this.holeTextureCache) { this.holeTextureCache = createHoleBaseTexture(holeOptions, width, height); } const baseTexture = this.holeTextureCache.source; const sidePadding = (height - textureDiameter) / 2; const frame = new Rectangle(0, sidePadding, width, textureDiameter); const texture = new Texture({ source: baseTexture, frame, }); return texture; } /** * The rendering order of these components needs to be aligned * @param casingRenderObjects * @param cementRenderObject * @param cementSqueezes * @returns ordered rendering list */ private sortCementAndCasingRenderObjects( casingRenderObjects: CasingRenderObject[], cementRenderObject: CementRenderObject[], cementSqueezes: CementSqueezeRenderObject[], ): InterlacedRenderObjects[] { type InterlaceReducerAcc = { result: InterlacedRenderObjects[]; remainingCement: CementRenderObject[]; remainingCementSqueezes: CementSqueezeRenderObject[]; }; let zIndex = 0; const { result } = casingRenderObjects.reduce( ( acc: InterlaceReducerAcc, casingRenderObject: CasingRenderObject, ): InterlaceReducerAcc => { const foundCementShape = acc.remainingCement.find(cement => cement.casingIds.includes(casingRenderObject.id), ); const foundCementSqueezes = acc.remainingCementSqueezes.filter( squeeze => squeeze.casingIds.includes(casingRenderObject.id), ); if (foundCementShape) { foundCementShape.zIndex = zIndex++; } foundCementSqueezes.forEach(item => (item.zIndex = zIndex++)); casingRenderObject.zIndex = zIndex++; return { result: [ ...acc.result, foundCementShape!, casingRenderObject, ...foundCementSqueezes, ], remainingCement: acc.remainingCement.filter( c => c !== foundCementShape, ), remainingCementSqueezes: acc.remainingCementSqueezes.filter( squeeze => !foundCementSqueezes.includes(squeeze), ), }; }, { result: [], remainingCement: cementRenderObject, remainingCementSqueezes: cementSqueezes, }, ); return result .filter((item): item is InterlacedRenderObjects => item != null) .sort((a, b) => a.zIndex! - b.zIndex!); } /** * * @param intervals * @param texture * optionally fetch the exaggerationFactor from a different options prop * options.perforationOptions for example * @param getExaggerationFactor * @returns */ private drawComplexRope( intervals: ComplexRopeSegment[], texture: Texture, ): ComplexRope | undefined { if (intervals.length === 0) { return undefined; } const rope = new ComplexRope(texture, intervals); this.addChild(rope); return rope; } private static getOutlineClosureType = ( index: number, maxIndex: number, ): OutlineClosure => { if (index === 0) { if (index === maxIndex) { return 'TopAndBottom'; } return 'Top'; } if (index === maxIndex) { return 'Bottom'; } return 'None'; }; private drawCasing = (casingRenderObject: CasingRenderObject): void => { const { casingOptions } = this.options as SchematicLayerOptions<T>; const casingSolidColorNumber = convertColor(casingOptions!.solidColor); const casingLineColorNumber = convertColor(casingOptions!.lineColor); casingRenderObject.sections.forEach((section, index, list) => { const outlineClosureType = SchematicLayer.getOutlineClosureType( index, list.length - 1, ); const texture = this.createCasingTexture( casingRenderObject.referenceDiameter, ); this.drawRope(section.pathPoints, texture, casingSolidColorNumber); if (section.kind === 'casing-window') { this.drawCasingWindowOutline( section.leftPath, section.rightPath, casingOptions!, casingRenderObject.casingWallWidth, ); } else { this.drawOutline( section.leftPath, section.rightPath, casingLineColorNumber, casingRenderObject.casingWallWidth, outlineClosureType, ); } }); }; private createCasingTexture(diameter: number): Texture { const textureWidthPO2 = 16; return new Texture({ source: Texture.WHITE.source, orig: new Rectangle(0, 0, textureWidthPO2, diameter), }); } private drawShoe(casingEnd: number, casingRadius: number): void { const { exaggerationFactor = 1, casingOptions } = this .options as SchematicLayerOptions<T>; const shoeWidth = casingOptions!.shoeSize.width * exaggerationFactor; const shoeLength = casingOptions!.shoeSize.length * exaggerationFactor; const shoeCoords = this.generateShoe( casingEnd, casingRadius, shoeLength, shoeWidth, ); const shoeCoords2 = this.generateShoe( casingEnd, casingRadius, shoeLength, -shoeWidth, ); this.drawBigPolygon(shoeCoords2); this.drawBigPolygon(shoeCoords); } private generateShoe = ( casingEnd: number, casingRadius: number, length: number, width: number, ): Point[] => { const start = casingEnd - length; const end = casingEnd; const points = this.getZFactorScaledPathForPoints(start, end); const normal = createNormals(points); const shoeEdge: Point[] = offsetPoints( points, normal, casingRadius * (width < 0 ? -1 : 1), ); const shoeTipPoint = points[points.length - 1]!; const shoeTipNormal = normal[normal.length - 1]!; const shoeTip: Point = offsetPoint( shoeTipPoint, shoeTipNormal, width + casingRadius * (width < 0 ? -1 : 1), ); return [...shoeEdge, shoeTip]; }; private createCementSqueezeShape = ( squeeze: CementSqueeze, casings: Casing[], completion: Completion[], holes: HoleSize[], ): ComplexRopeSegment[] => { const { exaggerationFactor = 1 } = this.options as SchematicLayerOptions<T>; return createComplexRopeSegmentsForCementSqueeze( squeeze, casings, completion, holes, exaggerationFactor, this.getZFactorScaledPathForPoints, ); }; private getCementTexture(): Texture | null { if (!this.cementTextureCache) { const { cementOptions } = this.options as SchematicLayerOptions<T>; cementOptions && (this.cementTextureCache = createCementTexture(cementOptions)); } return this.cementTextureCache; } private createPerforationShape = ( perforation: Perforation, casings: Casing[], holes: HoleSize[], ): PerforationShape[] => { const { exaggerationFactor = 1 } = this.options as SchematicLayerOptions<T>; return createComplexRopeSegmentsForPerforation( perforation, casings, holes, exaggerationFactor, this.getZFactorScaledPathForPoints, ); }; private getCementSqueezeTexture(): Texture | null { if (!this.cementSqueezeTextureCache) { const { cementSqueezeOptions } = this.options as SchematicLayerOptions<T>; cementSqueezeOptions && (this.cementSqueezeTextureCache = createCementSqueezeTexture(cementSqueezeOptions)); } return this.cementSqueezeTextureCache; } private drawScreen({ start, end, diameter }: Screen): void { const { exaggerationFactor = 1, screenOptions } = this .options as SchematicLayerOptions<T>; const exaggeratedDiameter = exaggerationFactor * diameter; const pathPoints = this.getZFactorScaledPathForPoints(start, end); const { leftPath, rightPath } = createTubularRenderingObject( exaggeratedDiameter / 2, pathPoints, ); const texture = this.getScreenTexture(); if (texture) { this.drawCompletionRope(pathPoints, texture, exaggeratedDiameter); this.drawOutline( leftPath, rightPath, convertColor(screenOptions!.lineColor), SCREEN_OUTLINE * exaggerationFactor, 'TopAndBottom', ); } } private drawTubing({ diameter, start, end }: Tubing): void { const { exaggerationFactor = 1, tubingOptions } = this .options as SchematicLayerOptions<T>; const exaggeratedDiameter = exaggerationFactor * diameter; const pathPoints = this.getZFactorScaledPathForPoints(start, end); const texture = this.getTubingTexture(tubingOptions!); this.drawCompletionRope(pathPoints, texture, exaggeratedDiameter); } private getTubingTexture(tubingOptions: TubingOptions): Texture { if (!this.tubingTextureCache) { this.tubingTextureCache = createTubingTexture(tubingOptions); } return this.tubingTextureCache; } private getScreenTexture(): Texture | null { if (!this.screenTextureCache) { const { screenOptions } = this.options as SchematicLayerOptions<T>; screenOptions && (this.screenTextureCache = createScreenTexture(screenOptions)); } return this.screenTextureCache; } private drawCompletionRope( path: Point[], texture: Texture, diameter: number, ): void { if (path.length === 0) { return; } const rope: FixedWidthSimpleRope = new FixedWidthSimpleRope( texture, path, diameter, ); this.addChild(rope); } }