diff --git a/examples/deckgl_annotation.py b/examples/deckgl_annotation.py new file mode 100644 index 0000000000..5fce312e4f --- /dev/null +++ b/examples/deckgl_annotation.py @@ -0,0 +1,121 @@ +import numpy as np +import xtgeo +import dash +import webviz_subsurface_components as wsc + +from utils.xtgeo_surface_to_float32 import get_surface_float32 +from utils.xtgeo_polygons_to_json import xtgeo_polygons_to_polylines_geojson + +# Import a depth surface and a property surface using xtgeo +depth_surface = xtgeo.surface_from_file("examples/example-data/topvolantis_depth.gri") +property_surface = xtgeo.surface_from_file( + "examples/example-data/topvolantis_seismic_attribute.gri" +) +polygons = xtgeo.polygons_from_file( + "examples/example-data/topvolantis_faultpolygons.pol" +) + +app = dash.Dash(__name__) + +app.layout = wsc.SubsurfaceViewerDashWrapper( + id="deckgl-map", + layers=[ + { + "@@type": "AxesLayer", + "id": "axes-layer", + "bounds": [ + depth_surface.xmin, + depth_surface.ymin, + -np.nanmax(depth_surface.values), + depth_surface.xmax, + depth_surface.ymax, + np.nanmin(depth_surface.values), + ], + }, + { + "@@type": "MapLayer", + "id": "mesh-layer", + "meshUrl": "/map/mesh", + "frame": { + "origin": [depth_surface.xori, depth_surface.yori], + "count": [depth_surface.ncol, depth_surface.nrow], + "increment": [depth_surface.xinc, depth_surface.yinc], + "rotDeg": depth_surface.rotation, + }, + "contours": [0, 20], + "isContoursDepth": True, + "gridLines": False, + "material": True, + "colorMapName": "Physics", + "name": "mesh", + }, + { + "@@type": "MapLayer", + "id": "mesh-and-property-layer", + "meshUrl": "/map/mesh", + "propertiesUrl": "/map/property", + "frame": { + "origin": [depth_surface.xori, depth_surface.yori], + "count": [depth_surface.ncol, depth_surface.nrow], + "increment": [depth_surface.xinc, depth_surface.yinc], + "rotDeg": depth_surface.rotation, + }, + "isContoursDepth": True, + "gridLines": False, + "material": True, + "colorMapName": "Seismic", + "name": "mesh", + }, + { + "@@type": "FaultPolygonsLayer", + "id": "fault-layer", + "data": "/faults/faults.json", + "refine": False, + }, + ], + views={ + "layout": [1, 2], + "showLabel": True, + "viewports": [ + { + "id": "view_1", + "show3D": False, + "name": "Depth surface", + "layerIds": ["axes-layer", "mesh-layer"], + "isSync": True, + }, + { + "id": "view_2", + "show3D": False, + "name": "Property mapped on depth surface", + "layerIds": [ + "fault-layer", + "axes-layer", + "mesh-and-property-layer", + ], + "isSync": True, + }, + ], + }, + children=[ + wsc.ViewAnnotation(id="view_1", children=[wsc.ViewFooter(children="Hugin")]), + wsc.ViewAnnotation(id="view_2", children=[wsc.ViewFooter(children="sdfsfd")]), + ], +) + + +@app.server.route("/map/") +def send_map(map_name: str): + if map_name == "mesh": + return get_surface_float32(depth_surface) + if map_name == "property": + return get_surface_float32(property_surface) + + +@app.server.route("/faults/faults.json") +def send_faults(): + return xtgeo_polygons_to_polylines_geojson(polygons, xy_only=True) + + +if __name__ == "__main__": + app.run_server(debug=True) diff --git a/react/package-lock.json b/react/package-lock.json index 17179819ad..3a9e57a940 100644 --- a/react/package-lock.json +++ b/react/package-lock.json @@ -32,6 +32,7 @@ "deck.gl": "^8.8.11", "deep-equal": "^2.0.5", "fast-json-patch": "^3.0.0-1", + "gl-matrix": "^3.4.3", "jsonschema": "^1.4.0", "jsverify": "^0.8.4", "leaflet": "^1.6.0", @@ -17417,30 +17418,6 @@ "node-int64": "^0.4.0" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -19705,6 +19682,30 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/cypress/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/cypress/node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -57221,16 +57222,6 @@ "node-int64": "^0.4.0" } }, - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -58972,6 +58963,16 @@ "color-convert": "^2.0.1" } }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", diff --git a/react/package.json b/react/package.json index ede8732f95..bae67500e2 100644 --- a/react/package.json +++ b/react/package.json @@ -77,6 +77,7 @@ "deck.gl": "^8.8.11", "deep-equal": "^2.0.5", "fast-json-patch": "^3.0.0-1", + "gl-matrix": "^3.4.3", "jsonschema": "^1.4.0", "jsverify": "^0.8.4", "leaflet": "^1.6.0", diff --git a/react/src/custom.d.ts b/react/src/custom.d.ts index 7c3b2d327c..6c1e46dee1 100644 --- a/react/src/custom.d.ts +++ b/react/src/custom.d.ts @@ -4,6 +4,7 @@ declare module "*.svg" { const src: string; export default src; } +declare module "*.png"; declare module "addon-redux/withRedux"; declare module "addon-redux/enhancer"; declare module "@emerson-eps/color-tables"; diff --git a/react/src/lib/components/DeckGLMap/SubsurfaceViewerDashWrapper.stories.tsx b/react/src/lib/components/DeckGLMap/SubsurfaceViewerDashWrapper.stories.tsx deleted file mode 100644 index f715aa21c4..0000000000 --- a/react/src/lib/components/DeckGLMap/SubsurfaceViewerDashWrapper.stories.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from "react"; -import { ComponentStory, ComponentMeta } from "@storybook/react"; -import { ViewFooter, SubsurfaceViewerDashWrapper } from "../.."; - -export default { - component: SubsurfaceViewerDashWrapper, - title: "DeckGLMap / SubsurfaceViewerDashWrapper", -} as ComponentMeta; - -const mapLayer = { - "@@type": "MapLayer", - id: "hugin", - meshUrl: "hugin_depth_25_m.float32", - frame: { - origin: [432150, 6475800], - count: [291, 229], - increment: [25, 25], - rotDeg: 0, - }, - propertiesUrl: "kh_netmap_25_m.float32", - contours: [0, 100], - material: false, -}; - -const DashWrapperTemplate: ComponentStory< - typeof SubsurfaceViewerDashWrapper -> = (args) => ; - -export const DashWrapperViewAnnotation = DashWrapperTemplate.bind({}); - -DashWrapperViewAnnotation.args = { - id: "dash_annotation", - layers: [ - mapLayer, - { - ...mapLayer, - id: "kh_netmap", - propertiesUrl: "hugin_depth_25_m.float32", - }, - ], - views: { - layout: [1, 2], - showLabel: true, - viewports: [ - { - id: "view_1", - layerIds: ["hugin"], - }, - { - id: "view_2", - layerIds: ["kh_netmap"], - }, - ], - }, - annotation: { - view_1: Hugin, - view_2: kH Netmap, - }, -}; diff --git a/react/src/lib/components/DeckGLMap/SubsurfaceViewerDashWrapper.tsx b/react/src/lib/components/DeckGLMap/SubsurfaceViewerDashWrapper.tsx index 4eea1555df..20530f1d09 100644 --- a/react/src/lib/components/DeckGLMap/SubsurfaceViewerDashWrapper.tsx +++ b/react/src/lib/components/DeckGLMap/SubsurfaceViewerDashWrapper.tsx @@ -65,21 +65,18 @@ export interface SubsurfaceViewerDashWrapperProps { cameraPosition?: ViewStateType | undefined; children?: React.ReactNode; - - /** - * A mapping associating annotation components to view ids. - * Example: {"view_1": } - */ - annotation?: Record; } -function mapAnnotation(annotation: Record) { - return Object.entries(annotation).map(([viewId, annotation]) => ( - // @ts-expect-error This is demonstrated to work with js, but with ts it gives error - - {annotation} - - )); +function mapAnnotation(annotationContainers: React.ReactNode) { + return React.Children.map(annotationContainers, (annotationContainer) => { + const viewId = (annotationContainer as React.ReactElement).key; + return ( + // @ts-expect-error This is demonstrated to work with js, but with ts it gives error + + {annotationContainer} + + ); + }); } const SubsurfaceViewerDashWrapper: React.FC< @@ -105,7 +102,6 @@ const SubsurfaceViewerDashWrapper: React.FC< triggerHome, triggerResetMultipleWells, children, - annotation = {}, }: SubsurfaceViewerDashWrapperProps) => { const mapArgs: DeckGLMapProps = { id: id, @@ -127,10 +123,9 @@ const SubsurfaceViewerDashWrapper: React.FC< getCameraPosition: getCameraPosition, triggerHome: triggerHome, triggerResetMultipleWells: triggerResetMultipleWells, - children: children, }; - return {mapAnnotation(annotation)}; + return {mapAnnotation(children)}; }; SubsurfaceViewerDashWrapper.defaultProps = { @@ -265,11 +260,7 @@ SubsurfaceViewerDashWrapper.propTypes = { */ onMouseEvent: PropTypes.func, - /** - * A mapping associating annotation components to view ids. - * Example: {"view_1": } - */ - annotation: PropTypes.any, + children: PropTypes.any, }; export default SubsurfaceViewerDashWrapper; diff --git a/react/src/lib/components/DeckGLMap/components/Map.tsx b/react/src/lib/components/DeckGLMap/components/Map.tsx index 9ec16a9473..ab3c27aaa7 100644 --- a/react/src/lib/components/DeckGLMap/components/Map.tsx +++ b/react/src/lib/components/DeckGLMap/components/Map.tsx @@ -335,10 +335,9 @@ const Map: React.FC = ({ ); // Local help function. - function calcDefaultViewStates() { + function calcDefaultViewStates(input?: ViewportType[]) { // If "bounds" or "cameraPosition" is not defined "viewState" will be // calculated based on the union of the reported bounding boxes from each layer. - const union_of_reported_bboxes = addBoundingBoxes( reportedBoundingBoxAcc, reportedBoundingBox @@ -363,8 +362,9 @@ const Map: React.FC = ({ let tempViewStates: Record = {}; const isBoundsDefined = typeof bounds !== "undefined"; + const updatedViewProps = input ? input : viewsProps; tempViewStates = Object.fromEntries( - viewsProps.map((item, index) => [ + updatedViewProps.map((item, index) => [ item.id, isBoundsDefined ? getViewState( @@ -549,13 +549,17 @@ const Map: React.FC = ({ }, [scaleZDown]); useEffect(() => { - setViewsProps( - getViews( - views, - scaleUpFunction, - scaleDownFunction - ) as ViewportType[] - ); + const viewProps = getViews( + views, + scaleUpFunction, + scaleDownFunction + ) as ViewportType[]; + + setViewsProps(viewProps); + + if (!bounds) { + calcDefaultViewStates(viewProps); + } }, [views]); useEffect(() => { @@ -892,6 +896,7 @@ const Map: React.FC = ({ }, [viewStates] ); + if (!deckGLViews || isEmpty(deckGLViews) || isEmpty(deckGLLayers)) return null; return ( diff --git a/react/src/lib/components/DeckGLMap/components/ViewAnnotation.tsx b/react/src/lib/components/DeckGLMap/components/ViewAnnotation.tsx new file mode 100644 index 0000000000..e9599809d2 --- /dev/null +++ b/react/src/lib/components/DeckGLMap/components/ViewAnnotation.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import PropTypes from "prop-types"; + +interface ViewAnnotationProps { + id: string; + children?: React.ReactNode; +} + +export const ViewAnnotation: React.FC = ({ children }) => { + return <> {children} ; +}; + +ViewAnnotation.propTypes = { + id: PropTypes.string.isRequired, + children: PropTypes.any, +}; diff --git a/react/src/lib/components/DeckGLMap/layers/axes2d/axes-fragment.glsl.js b/react/src/lib/components/DeckGLMap/layers/axes2d/axes-fragment.glsl.js new file mode 100644 index 0000000000..1659e11665 --- /dev/null +++ b/react/src/lib/components/DeckGLMap/layers/axes2d/axes-fragment.glsl.js @@ -0,0 +1,17 @@ +export default `\ +#version 300 es +#define SHADER_NAME graph-layer-fragment-shader + +precision highp float; + +out vec4 fragColor; + +uniform sampler2D fontTexture; + +in vec2 _vTexCoord; + +void main(void) { + vec4 color = texture(fontTexture, _vTexCoord); + fragColor = color; +} +`; diff --git a/react/src/lib/components/DeckGLMap/layers/axes2d/axes-fragment.glsl.ts b/react/src/lib/components/DeckGLMap/layers/axes2d/axes-fragment.glsl.ts deleted file mode 100644 index f98449a287..0000000000 --- a/react/src/lib/components/DeckGLMap/layers/axes2d/axes-fragment.glsl.ts +++ /dev/null @@ -1,15 +0,0 @@ -const fragmentShader = `#version 300 es -#define SHADER_NAME graph-layer-fragment-shader - -precision highp float; - -out vec4 fragColor; - -uniform vec4 uColor; - -void main(void) { - fragColor = uColor; -} -`; - -export default fragmentShader; diff --git a/react/src/lib/components/DeckGLMap/layers/axes2d/axes-vertex.glsl.js b/react/src/lib/components/DeckGLMap/layers/axes2d/axes-vertex.glsl.js new file mode 100644 index 0000000000..8fcf699961 --- /dev/null +++ b/react/src/lib/components/DeckGLMap/layers/axes2d/axes-vertex.glsl.js @@ -0,0 +1,20 @@ +export default `\ +#version 300 es +#define SHADER_NAME graph-layer-axis-vertex-shader + +precision highp float; + +in vec3 positions; + +in vec2 vTexCoord; +out vec2 _vTexCoord; + +uniform mat4 projectionMatrix; + +void main(void) { + _vTexCoord = vTexCoord; + + vec3 position_commonspace = positions; // These positions are in view space. + gl_Position = projectionMatrix * vec4(position_commonspace, 1.0); // From viewspace to clip +} +`; diff --git a/react/src/lib/components/DeckGLMap/layers/axes2d/axes2DLayer.stories.tsx b/react/src/lib/components/DeckGLMap/layers/axes2d/axes2DLayer.stories.tsx index 36e215d60d..9ebc5029cd 100644 --- a/react/src/lib/components/DeckGLMap/layers/axes2d/axes2DLayer.stories.tsx +++ b/react/src/lib/components/DeckGLMap/layers/axes2d/axes2DLayer.stories.tsx @@ -8,7 +8,7 @@ export default { } as ComponentMeta; const layerProps = { - marginH: 80, // Horizontal margin (in pixels) + marginH: 100, // Horizontal margin (in pixels) marginV: 40, // Vertical margin (in pixels) }; @@ -31,6 +31,12 @@ const meshMapLayerPng = { colorMapName: "Physics", }; +const axes_hugin = { + "@@type": "AxesLayer", + id: "axes-layer2", + bounds: [432150, 6475800, -3500, 439400, 6481500, 0], +}; + const axes2D = { "@@type": "Axes2DLayer", id: "axes-layer2D", @@ -43,7 +49,7 @@ export const Base: ComponentStory = (args) => { Base.args = { id: "map", - layers: [axes2D, meshMapLayerPng], + layers: [axes_hugin, meshMapLayerPng, axes2D], bounds: [432150, 6475800, 439400, 6481500], views: { @@ -51,6 +57,7 @@ Base.args = { viewports: [ { id: "view_1", + zoom: -3.5, show3D: false, }, ], diff --git a/react/src/lib/components/DeckGLMap/layers/axes2d/axes2DLayer.ts b/react/src/lib/components/DeckGLMap/layers/axes2d/axes2DLayer.ts index cb3dbb86dc..3091309c7c 100644 --- a/react/src/lib/components/DeckGLMap/layers/axes2d/axes2DLayer.ts +++ b/react/src/lib/components/DeckGLMap/layers/axes2d/axes2DLayer.ts @@ -1,134 +1,265 @@ import { - COORDINATE_SYSTEM, - Color, - CompositeLayer, + Layer, Viewport, - UpdateParameters, - LayersList, + LayerContext, + project, OrthographicViewport, + COORDINATE_SYSTEM, } from "@deck.gl/core/typed"; -import BoxLayer from "./boxLayer"; -import { Position3D, ExtendedLayerProps } from "../utils/layerTools"; -import { layersDefaultProps } from "../layersDefaultProps"; -import { TextLayer } from "@deck.gl/layers/typed"; -import { Vector2 } from "@math.gl/core"; +import GL from "@luma.gl/constants"; +import { Model, Geometry } from "@luma.gl/engine"; +import labelsVertexShader from "./axes-vertex.glsl"; +import labelsFragmentShader from "./axes-fragment.glsl"; +import lineVertexShader from "./line-vertex.glsl"; +import lineFragmentShader from "./line-fragment.glsl"; +import { ExtendedLayerProps, Position3D } from "../utils/layerTools"; +import { load } from "@loaders.gl/core"; +import { Texture2D } from "@luma.gl/webgl"; +import { ImageLoader } from "@loaders.gl/images"; +import { vec4, mat4 } from "gl-matrix"; +import { Color } from "@deck.gl/core/typed"; +import fontAtlasPng from "./font-atlas.png"; + +const DEFAULT_TEXTURE_PARAMETERS = { + [GL.TEXTURE_MIN_FILTER]: GL.LINEAR_MIPMAP_LINEAR, + [GL.TEXTURE_MAG_FILTER]: GL.LINEAR, + [GL.TEXTURE_WRAP_S]: GL.CLAMP_TO_EDGE, + [GL.TEXTURE_WRAP_T]: GL.CLAMP_TO_EDGE, +}; + +enum TEXT_ANCHOR { + start = 0, + middle = 1, + end = 2, +} + +enum ALIGNMENT_BASELINE { + top = 1, + center = 0, + bottom = -1, +} + +type LabelData = { + label: string; + pos: Position3D; // tick line start + anchor?: TEXT_ANCHOR; + aligment?: ALIGNMENT_BASELINE; + //font_size: number; KEEP. Fixed size for now. +}; + +type LabelsData = LabelData[]; export interface Axes2DLayerProps extends ExtendedLayerProps { + marginH: number; + marginV: number; labelColor?: Color; labelFontSize?: number; fontFamily?: string; axisColor?: Color; - marginH: number; - marginV: number; } -type TextLayerData = { - label: string; - from: Position3D; // tick line start - to: Position3D; // tick line end - size: number; // font size +const defaultProps = { + name: "Axes2D", + id: "axes-2d-layer", + coordinateSystem: COORDINATE_SYSTEM.CARTESIAN, }; -export default class Axes2DLayer extends CompositeLayer< - Axes2DLayerProps -> { - shouldUpdateState({ - props, - oldProps, - context, - changeFlags, - }: UpdateParameters): boolean { - return ( - super.shouldUpdateState({ - props, - oldProps, - context, - changeFlags, - }) || changeFlags.viewportChanged - ); - } +// FONT ATLAS +const font_width = 86; +const yh = 97; +const fontInfo = { + letterHeight: 92, + spaceWidth: 0, + spacing: -1, + textureWidth: 1714, + textureHeight: 200, + glyphInfos: { + A: { x: 0, y: 0, width: font_width }, + B: { x: font_width, y: 0, width: font_width }, + C: { x: 2 * font_width, y: 0, width: font_width }, + D: { x: 3 * font_width, y: 0, width: font_width }, + E: { x: 4 * font_width, y: 0, width: font_width }, + F: { x: 5 * font_width, y: 0, width: font_width }, + G: { x: 6 * font_width, y: 0, width: font_width }, + H: { x: 7 * font_width, y: 0, width: font_width }, + I: { x: 8 * font_width, y: 0, width: font_width }, + J: { x: 9 * font_width, y: 0, width: font_width }, + K: { x: 10 * font_width, y: 0, width: font_width }, + L: { x: 11 * font_width, y: 0, width: font_width }, + M: { x: 12 * font_width, y: 0, width: font_width }, + N: { x: 13 * font_width, y: 0, width: font_width }, + O: { x: 14 * font_width, y: 0, width: font_width }, + P: { x: 15 * font_width, y: 0, width: font_width }, + Q: { x: 16 * font_width, y: 0, width: font_width }, + R: { x: 17 * font_width, y: 0, width: font_width }, + S: { x: 18 * font_width, y: 0, width: font_width }, + T: { x: 19 * font_width, y: 0, width: font_width }, + + U: { x: 0, y: yh, width: font_width }, + V: { x: font_width, y: yh, width: font_width }, + W: { x: 2 * font_width, y: yh, width: font_width }, + X: { x: 3 * font_width, y: yh, width: font_width }, + Y: { x: 4 * font_width, y: yh, width: font_width }, + Z: { x: 5 * font_width, y: yh, width: font_width }, + 0: { x: 6 * font_width, y: yh, width: font_width }, + 1: { x: 7 * font_width, y: yh, width: font_width }, + 2: { x: 8 * font_width, y: yh, width: font_width }, + 3: { x: 9 * font_width, y: yh, width: font_width }, + 4: { x: 10 * font_width, y: yh, width: font_width }, + 5: { x: 11 * font_width, y: yh, width: font_width }, + 6: { x: 12 * font_width, y: yh, width: font_width }, + 7: { x: 13 * font_width, y: yh, width: font_width }, + 8: { x: 14 * font_width, y: yh, width: font_width }, + 9: { x: 15 * font_width, y: yh, width: font_width }, + "+": { x: 16 * font_width, y: yh, width: font_width }, + "-": { x: 17 * font_width, y: yh, width: font_width }, + ".": { x: 18 * font_width, y: yh, width: font_width }, + ",": { x: 19 * font_width, y: yh, width: font_width }, + }, +}; - getAnchor(d: TextLayerData): string { - const is_xaxis = d.from[1] !== d.to[1]; - if (is_xaxis) { - return "middle"; - } +export default class Axes2DLayer extends Layer> { + initializeState(context: LayerContext): void { + const { gl } = context; - const screen_from = this.context.viewport.project(d.from); - const screen_to = this.context.viewport.project(d.to); - const is_labels = d.label !== "X" && d.label !== "Y" && d.label !== "Z"; // labels on axis or XYZ annotations - if (is_labels) { - if (screen_from[0] < screen_to[0]) { - return "start"; - } - } + const promise = load(fontAtlasPng, ImageLoader, { + image: { type: "data" }, // Will load as ImageData. + }); - return "end"; + promise.then((data: ImageData) => { + const fontTexture = new Texture2D(gl, { + width: data.width, + height: data.height, + format: GL.RGB, + data, + parameters: DEFAULT_TEXTURE_PARAMETERS, + }); + + this.setState({ + fontTexture, + // Insert a dummy model initially. + model: new Model(gl, { + id: "dummy", + vs: lineVertexShader, + fs: lineFragmentShader, + }), + }); + }); } - getLabelPosition(d: TextLayerData): Position3D { - const is_labels = d.label !== "X" && d.label !== "Y" && d.label !== "Z"; // labels on axis or XYZ annotations - if (is_labels) { - const tick_vec = [d.to[0] - d.from[0], d.to[1] - d.from[1]]; - if (d.to[2] && d.from[2]) tick_vec.push(d.to[2] - d.from[2]); + makeLabelsData(tick_lines: number[], tick_labels: string[]): LabelsData { + const labels: LabelsData = []; + for (let i = 0; i < tick_lines.length / 6; i++) { + const from = [ + tick_lines[6 * i + 0], + tick_lines[6 * i + 1], + tick_lines[6 * i + 2], + ]; + const to = [ + tick_lines[6 * i + 3], + tick_lines[6 * i + 4], + tick_lines[6 * i + 5], + ]; + const label = tick_labels[i]; + + const tick_vec = [ + to[0] - from[0], + to[1] - from[1], + to[2] - from[2], + ]; const s = 0.5; - return [ - d.to[0] + s * tick_vec[0], - d.to[1] + s * tick_vec[1], - d.to[2] + s * tick_vec[2], + const pos: Position3D = [ + to[0] + s * tick_vec[0], + to[1] + s * tick_vec[1], + to[2] + s * tick_vec[2], ]; - } else { - // XYZ axis annotaion. - return d.to; - } - } - getBaseLine(d: TextLayerData): string { - const is_x_annotaion = d.label === "X"; - if (is_x_annotaion) { - return "center"; + let anchor = TEXT_ANCHOR.end; + let aligment = ALIGNMENT_BASELINE.center; + const is_xaxis = from[1] !== to[1]; + if (is_xaxis) { + anchor = TEXT_ANCHOR.middle; + aligment = ALIGNMENT_BASELINE.top; + } else { + const screen_from = this.context.viewport.project(from); + const screen_to = this.context.viewport.project(to); + + if (screen_from[0] < screen_to[0]) { + anchor = TEXT_ANCHOR.start; + } + } + + labels.push({ label, pos, anchor, aligment }); } - const is_xaxis_label = d.from[1] !== d.to[1]; - return is_xaxis_label ? "top" : "center"; + return labels; // as Text3DLayerData; } - renderLayers(): LayersList { + draw({ + moduleParameters, + uniforms, + context, + }: { + moduleParameters: unknown; + uniforms: unknown; + context: LayerContext; + }): void { const is_orthographic = this.context.viewport.constructor === OrthographicViewport; - - if (!is_orthographic) { - return []; + if ( + typeof this.state["fontTexture"] === "undefined" || + !is_orthographic + ) { + return; } - // pixels2world: factor to convert a length from pixels to world space. - const npixels = 100; - const p1 = [0, 0]; - const p2 = [npixels, 0]; + const { gl } = context; + + super.draw({ moduleParameters, uniforms, context }); // For some reason this is neccessary. - const p1_unproj = this.context.viewport.unproject(p1); - const p2_unproj = this.context.viewport.unproject(p2); + const { projectionMatrix } = this.context.viewport; - const v1 = new Vector2(p1_unproj[0], p1_unproj[1]); - const v2 = new Vector2(p2_unproj[0], p2_unproj[1]); - const d = v1.distance(v2); + //gl.disable(gl.DEPTH_TEST); KEEP for now. - const pixels2world = d / npixels; + const { label_models, line_model } = this._getModels(gl); + + const fontTexture = this.state["fontTexture"]; + for (const model of label_models) { + model.setUniforms({ projectionMatrix, fontTexture }).draw(); + } - const mh = this.props.marginH * pixels2world; - const mv = this.props.marginV * pixels2world; + line_model.draw(); + + //gl.enable(gl.DEPTH_TEST); + } + + _getModels(gl: WebGLRenderingContext): { + label_models: Model[]; + line_model: Model; + } { + // MAKE MODEL FOR THE AXES LINES (tick marks and axes). + + // Margins. + const m = 100; // Length in pixels + const world_from = this.context.viewport.unproject([0, 0, 0]); + const world_to = this.context.viewport.unproject([0, m, 0]); + const v = [ + world_from[0] - world_to[0], + world_from[1] - world_to[1], + world_from[2] - world_to[2], + ]; - const xMarginLeft = mh; - const xMarginRight = mh; - const yMarginTop = mv; - const yMarginBottom = mv; + const pixel2world = Math.sqrt(v[0] * v[0] + v[1] * v[1]) / 100; + + const mh = this.props.marginH * pixel2world; + const mv = this.props.marginV * pixel2world; const vpBounds = this.context.viewport.getBounds(); - const xMin = vpBounds[0] + xMarginLeft; - const xMax = vpBounds[2] + xMarginRight; // Note: "+" so that the axis extends outside viewport - const yMin = vpBounds[1] + yMarginBottom; - const yMax = vpBounds[3] + yMarginTop; // Note: "+" so that the axis extends outside viewport + const xMin = vpBounds[0] + mh; + const xMax = vpBounds[2] + mh; // Note: "+" so that the axis extends outside viewport + const yMin = vpBounds[1] + mv; + const yMax = vpBounds[3] + mv; // Note: "+" so that the axis extends outside viewport const bounds = [xMin, yMin, xMax, yMax] as [ number, @@ -137,55 +268,200 @@ export default class Axes2DLayer extends CompositeLayer< number ]; - const box_lines = GetBoxLines(bounds); + const axes_lines = GetBoxLines(bounds); const [tick_lines, tick_labels] = GetTickLines( bounds, this.context.viewport ); - const textlayerData = maketextLayerData( - tick_lines, - tick_labels, - this.props.labelFontSize - ); + const labels = this.makeLabelsData(tick_lines, tick_labels); + + const lines = [...axes_lines, ...tick_lines]; + + const line_model = new Model(gl, { + id: `${this.props.id}-lines`, + vs: lineVertexShader, + fs: lineFragmentShader, + uniforms: { uColor: [0, 0, 0, 1] }, + geometry: new Geometry({ + drawMode: GL.LINES, + attributes: { + positions: new Float32Array(lines), + }, + vertexCount: lines.length / 3, + }), + + modules: [project], + isInstanced: false, + }); - const lines = [...box_lines, ...tick_lines]; + //-- MAKE MODEL FOR THE LABEL TEXT'S -- + const { viewMatrix } = this.context.viewport; - const box_layer = new BoxLayer( - this.getSubLayerProps({ - lines, - coordinateSystem: COORDINATE_SYSTEM.CARTESIAN, - color: this.props.axisColor || [0, 0, 0, 255], - }) - ); + const label_models: Model[] = []; - const text_layer = new TextLayer( - this.getSubLayerProps({ - fontFamily: this.props.fontFamily ?? "Monaco, monospace", - data: textlayerData, - id: "text-layer", - pickable: true, - getPosition: (d: TextLayerData) => this.getLabelPosition(d), - getText: (d: TextLayerData) => d.label, - sizeUnits: "pixels", - getSize: (d: TextLayerData) => d.size, - getAngle: 0, - getTextAnchor: (d: TextLayerData) => this.getAnchor(d), - getAlignmentBaseline: (d: TextLayerData) => this.getBaseLine(d), - coordinateSystem: COORDINATE_SYSTEM.CARTESIAN, - getColor: this.props.labelColor || [0, 0, 0, 255], - }) - ); + for (const item of labels) { + const x = item.pos[0]; + const y = item.pos[1]; + const z = item.pos[2]; + const label = item.label; + const anchor = item.anchor ?? TEXT_ANCHOR.start; + const aligment_baseline = + item.aligment ?? ALIGNMENT_BASELINE.center; + + if (label === "") { + continue; + } - return [box_layer, text_layer]; + const pos_w = vec4.fromValues(x, y, z, 1); // pos world + const pos_v = vec4.transformMat4( + vec4.create(), + pos_w, + mat4.fromValues( + viewMatrix[0], + viewMatrix[1], + viewMatrix[2], + viewMatrix[3], + viewMatrix[4], + viewMatrix[5], + viewMatrix[6], + viewMatrix[7], + viewMatrix[8], + viewMatrix[9], + viewMatrix[10], + viewMatrix[11], + viewMatrix[12], + viewMatrix[13], + viewMatrix[14], + viewMatrix[15] + ) + ); // world to view axes. vec4.create() + const pos_view = [pos_v[0], pos_v[1], pos_v[2]]; + + const pixelScale = 8; + + const len = label.length; + const numVertices = len * 6; + const positions = new Float32Array(numVertices * 3); + const texcoords = new Float32Array(numVertices * 2); + const maxX = fontInfo.textureWidth; + const maxY = fontInfo.textureHeight; + let offset = 0; + let offsetTexture = 0; + + let x1 = 0; + if (anchor === TEXT_ANCHOR.end) { + x1 = -len; + } else if (anchor === TEXT_ANCHOR.middle) { + x1 = -len / 2; + } + + let y_aligment_offset = 0; + if (aligment_baseline === ALIGNMENT_BASELINE.center) { + y_aligment_offset = 0.5 * pixelScale; + } else if (aligment_baseline === ALIGNMENT_BASELINE.top) { + y_aligment_offset = 1 * pixelScale; + } + + for (let ii = 0; ii < len; ++ii) { + const letter = label[ii]; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const glyphInfo = fontInfo.glyphInfos[letter]; + if (glyphInfo) { + // Unit square. + const x2 = x1 + 1; + const u1 = glyphInfo.x / maxX; + const v1 = (glyphInfo.y + fontInfo.letterHeight - 1) / maxY; + const u2 = (glyphInfo.x + glyphInfo.width - 1) / maxX; + const v2 = glyphInfo.y / maxY; + + const h = 1; + const x = pos_view[0]; + const y = pos_view[1] - y_aligment_offset; + const z = pos_view[2]; + + // 6 vertices per letter + // t1 + positions[offset + 0] = x + x1 * pixelScale; + positions[offset + 1] = y + 0 * pixelScale; + positions[offset + 2] = z; // Note: may make these vertices 2D. + texcoords[offsetTexture + 0] = u1; + texcoords[offsetTexture + 1] = v1; + + positions[offset + 3] = x + x2 * pixelScale; + positions[offset + 4] = y + 0 * pixelScale; + positions[offset + 5] = z; + texcoords[offsetTexture + 2] = u2; + texcoords[offsetTexture + 3] = v1; + + positions[offset + 6] = x + x1 * pixelScale; + positions[offset + 7] = y + h * pixelScale; + positions[offset + 8] = z; + texcoords[offsetTexture + 4] = u1; + texcoords[offsetTexture + 5] = v2; + + // t2 + positions[offset + 9] = x + x1 * pixelScale; + positions[offset + 10] = y + h * pixelScale; + positions[offset + 11] = z; + texcoords[offsetTexture + 6] = u1; + texcoords[offsetTexture + 7] = v2; + + positions[offset + 12] = x + x2 * pixelScale; + positions[offset + 13] = y + 0 * pixelScale; + positions[offset + 14] = z; + texcoords[offsetTexture + 8] = u2; + texcoords[offsetTexture + 9] = v1; + + positions[offset + 15] = x + x2 * pixelScale; + positions[offset + 16] = y + h * pixelScale; + positions[offset + 17] = z; + texcoords[offsetTexture + 10] = u2; + texcoords[offsetTexture + 11] = v2; + + x1 += 1; + offset += 18; + offsetTexture += 12; + } else { + // we don't have this character so just advance + x1 += 1; + } + } + + const id = `${this.props.id}-${label}`; + const model = new Model(gl, { + id, + vs: labelsVertexShader, + fs: labelsFragmentShader, + geometry: new Geometry({ + drawMode: GL.TRIANGLES, + attributes: { + positions, + vTexCoord: { + value: texcoords, + size: 2, + }, + }, + vertexCount: positions.length / 3, + }), + + modules: [project], + isInstanced: false, + }); + + label_models.push(model); + } + + return { label_models, line_model }; } } Axes2DLayer.layerName = "Axes2DLayer"; -Axes2DLayer.defaultProps = layersDefaultProps["Axes2DLayer"] as Axes2DLayer; +Axes2DLayer.defaultProps = defaultProps; -//-- Local functions. ------------------------------------------------- +//-- Local help functions. ------------------------------------------------- function LineLengthInPixels( p0: Position3D, @@ -204,36 +480,6 @@ function LineLengthInPixels( return L; } -function maketextLayerData( - tick_lines: number[], - tick_labels: string[], - labelFontSize?: number -): [TextLayerData] { - const data = []; - for (let i = 0; i < tick_lines.length / 6; i++) { - const from = [ - tick_lines[6 * i + 0], - tick_lines[6 * i + 1], - tick_lines[6 * i + 2], - ]; - const to = [ - tick_lines[6 * i + 3], - tick_lines[6 * i + 4], - tick_lines[6 * i + 5], - ]; - const label = tick_labels[i]; - - data.push({ - label: label, - from: from, - to: to, - size: labelFontSize ?? 11, - }); - } - - return data as [TextLayerData]; -} - function GetTicks( min: number, max: number, @@ -362,7 +608,7 @@ function GetTickLines( // Y axis labels. const Ly = LineLengthInPixels( - [x_min, y_min, 0], // XXX fjern z dependency... + [x_min, y_min, 0], [x_min, y_max, 0], viewport ); @@ -374,7 +620,7 @@ function GetTickLines( tick_labels.push(label); const x_tick = x_min; - const z_tick = 0; // XXX fjern z dependency... + const z_tick = 0; // tick line start lines.push(x_tick, tick, z_tick); @@ -437,7 +683,6 @@ function GetBoxLines(bounds: [number, number, number, number]): number[] { // ADD LINES OF BOUNDING BOX. const lines = [ - // TOP x_min, y_min, z_min, diff --git a/react/src/lib/components/DeckGLMap/layers/axes2d/boxLayer.ts b/react/src/lib/components/DeckGLMap/layers/axes2d/boxLayer.ts deleted file mode 100644 index 412516065d..0000000000 --- a/react/src/lib/components/DeckGLMap/layers/axes2d/boxLayer.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { - COORDINATE_SYSTEM, - Color, - Layer, - project, - UpdateParameters, -} from "@deck.gl/core/typed"; -import GL from "@luma.gl/constants"; -import { Model, Geometry } from "@luma.gl/engine"; -import fragmentShader from "./axes-fragment.glsl"; -import gridVertex from "./grid-vertex.glsl"; -import { DeckGLLayerContext } from "../../components/Map"; -import { ExtendedLayerProps } from "../utils/layerTools"; - -export interface BoxLayerProps extends ExtendedLayerProps { - lines: [number]; // from pt , to pt. - color: Color; -} - -const defaultProps = { - name: "Box", - id: "box-layer", - coordinateSystem: COORDINATE_SYSTEM.CARTESIAN, - lines: [], - color: [0, 0, 0, 1], -}; - -export default class BoxLayer extends Layer> { - initializeState(context: DeckGLLayerContext): void { - const { gl } = context; - this.setState(this._getModels(gl)); - } - - shouldUpdateState(): boolean { - return true; - } - - updateState({ context }: UpdateParameters): void { - const { gl } = context; - this.setState(this._getModels(gl)); - } - - //eslint-disable-next-line - _getModels(gl: any) { - const color = this.props.color.map((x) => (x ?? 0) / 255); - const grids = new Model(gl, { - id: `${this.props.id}-grids`, - vs: gridVertex, - fs: fragmentShader, - uniforms: { uColor: color }, - geometry: new Geometry({ - drawMode: GL.LINES, - attributes: { - positions: new Float32Array(this.props.lines), - }, - vertexCount: this.props.lines.length / 3, - }), - modules: [project], - isInstanced: false, // This only works when set to false. - }); - - return { - model: grids, - models: [grids].filter(Boolean), - modelsByName: { grids }, - }; - } -} - -BoxLayer.layerName = "BoxLayer"; -BoxLayer.defaultProps = defaultProps; diff --git a/react/src/lib/components/DeckGLMap/layers/axes2d/font-atlas.png b/react/src/lib/components/DeckGLMap/layers/axes2d/font-atlas.png new file mode 100644 index 0000000000..cb186142c8 Binary files /dev/null and b/react/src/lib/components/DeckGLMap/layers/axes2d/font-atlas.png differ diff --git a/react/src/lib/components/DeckGLMap/layers/axes2d/line-fragment.glsl.js b/react/src/lib/components/DeckGLMap/layers/axes2d/line-fragment.glsl.js new file mode 100644 index 0000000000..de742dfd63 --- /dev/null +++ b/react/src/lib/components/DeckGLMap/layers/axes2d/line-fragment.glsl.js @@ -0,0 +1,14 @@ +export default `\ +#version 300 es +#define SHADER_NAME axes2d-layer-fragment-shader + +precision highp float; + +uniform vec4 uColor; + +out vec4 fragColor; + +void main(void) { + fragColor = uColor; +} +`; diff --git a/react/src/lib/components/DeckGLMap/layers/axes2d/grid-vertex.glsl.ts b/react/src/lib/components/DeckGLMap/layers/axes2d/line-vertex.glsl.js similarity index 59% rename from react/src/lib/components/DeckGLMap/layers/axes2d/grid-vertex.glsl.ts rename to react/src/lib/components/DeckGLMap/layers/axes2d/line-vertex.glsl.js index 41768e71e5..6732880765 100644 --- a/react/src/lib/components/DeckGLMap/layers/axes2d/grid-vertex.glsl.ts +++ b/react/src/lib/components/DeckGLMap/layers/axes2d/line-vertex.glsl.js @@ -1,14 +1,13 @@ -const gridVertex = `#version 300 es -#define SHADER_NAME graph-layer-axis-vertex-shader +export default `\ +#version 300 es +#define SHADER_NAME axes2d-layer-vertex-shader precision highp float; -in vec3 positions; +in vec3 positions; void main(void) { vec3 position_commonspace = project_position(positions); gl_Position = project_common_position_to_clipspace(vec4(position_commonspace, 0.0)); } `; - -export default gridVertex; diff --git a/react/src/lib/index.js b/react/src/lib/index.js index 7dbfd3fd90..f6a2a700bc 100644 --- a/react/src/lib/index.js +++ b/react/src/lib/index.js @@ -30,6 +30,7 @@ import { WellsPickInfo } from "./components/DeckGLMap/layers/wells/wellsLayer"; import TerrainMapPickInfo from "./components/DeckGLMap/layers/terrain/terrainMapLayer"; import { FeatureCollection } from "@nebula.gl/edit-modes"; import { ViewFooter } from "./components/DeckGLMap/components/ViewFooter"; +import { ViewAnnotation } from "./components/DeckGLMap/components/ViewAnnotation"; export { HistoryMatch, @@ -58,4 +59,5 @@ export { ViewFooter, View, SubsurfaceViewerDashWrapper, + ViewAnnotation, }; diff --git a/react/tsconfig.dash.json b/react/tsconfig.dash.json index aae266b6cc..2211064a68 100644 --- a/react/tsconfig.dash.json +++ b/react/tsconfig.dash.json @@ -4,5 +4,8 @@ "rootDir": "src/lib" }, "include": [], - "files": ["src/lib/components/DeckGLMap/components/ViewFooter.tsx"] + "files": [ + "src/lib/components/DeckGLMap/components/ViewFooter.tsx", + "src/lib/components/DeckGLMap/components/ViewAnnotation.tsx" + ] }