From e04bfe3d5d29ebaaf602a978c72197454292d487 Mon Sep 17 00:00:00 2001 From: nilscb Date: Mon, 16 Jan 2023 09:46:59 +0100 Subject: [PATCH] Fixes: "Axes2D flickering #1341" (#1379) * Fixes: "Axes2D flickering #1341" * Added package-lock.json * In progress * Made font atlas png image inline. * Lint fix. * Code comment fix. Use webpack to load image. * Removed unused package * Fixed ts condition. * Fixed some ts type conditions. Co-authored-by: Havard Bjerke --- react/package-lock.json | 69 +-- react/package.json | 1 + react/src/custom.d.ts | 1 + .../layers/axes2d/axes-fragment.glsl.js | 17 + .../layers/axes2d/axes-fragment.glsl.ts | 15 - .../layers/axes2d/axes-vertex.glsl.js | 20 + .../layers/axes2d/axes2DLayer.stories.tsx | 11 +- .../DeckGLMap/layers/axes2d/axes2DLayer.ts | 565 +++++++++++++----- .../DeckGLMap/layers/axes2d/boxLayer.ts | 71 --- .../DeckGLMap/layers/axes2d/font-atlas.png | Bin 0 -> 25391 bytes .../layers/axes2d/line-fragment.glsl.js | 14 + ...rid-vertex.glsl.ts => line-vertex.glsl.js} | 9 +- 12 files changed, 506 insertions(+), 287 deletions(-) create mode 100644 react/src/lib/components/DeckGLMap/layers/axes2d/axes-fragment.glsl.js delete mode 100644 react/src/lib/components/DeckGLMap/layers/axes2d/axes-fragment.glsl.ts create mode 100644 react/src/lib/components/DeckGLMap/layers/axes2d/axes-vertex.glsl.js delete mode 100644 react/src/lib/components/DeckGLMap/layers/axes2d/boxLayer.ts create mode 100644 react/src/lib/components/DeckGLMap/layers/axes2d/font-atlas.png create mode 100644 react/src/lib/components/DeckGLMap/layers/axes2d/line-fragment.glsl.js rename react/src/lib/components/DeckGLMap/layers/axes2d/{grid-vertex.glsl.ts => line-vertex.glsl.js} (59%) diff --git a/react/package-lock.json b/react/package-lock.json index 17179819a..3a9e57a94 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 ede8732f9..bae67500e 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 7c3b2d327..6c1e46dee 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/layers/axes2d/axes-fragment.glsl.js b/react/src/lib/components/DeckGLMap/layers/axes2d/axes-fragment.glsl.js new file mode 100644 index 000000000..1659e1166 --- /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 f98449a28..000000000 --- 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 000000000..8fcf69996 --- /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 36e215d60..9ebc5029c 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 cb3dbb86d..3091309c7 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 412516065..000000000 --- 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 0000000000000000000000000000000000000000..cb186142c8f0335e6765abb2ab0bc24681e7b76e GIT binary patch literal 25391 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