diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml deleted file mode 100644 index c1c1052ba4d..00000000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,74 +0,0 @@ -# For most projects, this workflow file will not need changing; you simply need -# to commit it to your repository. -# -# You may wish to alter this file to override the set of languages analyzed, -# or to provide custom queries or build logic. -# -# ******** NOTE ******** -# We have attempted to detect the languages in your repository. Please check -# the `language` matrix defined below to confirm you have the correct set of -# supported CodeQL languages. -# -name: "CodeQL" - -on: - push: - branches: [ main ] - pull_request: - # The branches below must be a subset of the branches above - branches: [ main ] - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: [ 'javascript' ] - # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] - # Learn more about CodeQL language support at https://git.io/codeql-language-support - - steps: - - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 - - - if: matrix.language == 'javascript-typescript' - name: Setup Node.js 20.x - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 #v4.4.0 - with: - node-version: 20.x - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f #v3.28.18 - with: - languages: ${{ matrix.language }} - # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. - # Prefix the list here with "+" to use these queries and those in the config file. - # queries: ./path/to/local/query, your-org/your-repo/queries@main - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f #v3.28.18 - - # â„šī¸ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f #v3.28.18 diff --git a/debug/variable-line-offset-demo.html b/debug/variable-line-offset-demo.html new file mode 100644 index 00000000000..e0af53c52a3 --- /dev/null +++ b/debug/variable-line-offset-demo.html @@ -0,0 +1,168 @@ + + + + Variable Line Offset Demo - Multi-Line Rendering + + + + + + + +
+

Variable Line Offset Demo

+

Feature: line-offset with line-progress

+

The red line offset varies from -10px to +10px along its length.

+ +
+

Layer Controls

+
+ + +
+
+ + +
+
+
+ +
+ + + + + + diff --git a/src/data/bucket/line_bucket.ts b/src/data/bucket/line_bucket.ts index 79834775df8..0ff4cf1b693 100644 --- a/src/data/bucket/line_bucket.ts +++ b/src/data/bucket/line_bucket.ts @@ -99,6 +99,7 @@ type GradientTexture = { type LineProgressFeatures = { zOffset: number; variableWidth: number; + variableOffset: number; }; interface Subsegment { @@ -121,6 +122,7 @@ class LineBucket implements Bucket { lineClips: LineClips | null | undefined; zOffsetValue: PossiblyEvaluatedValue; variableWidthValue: PossiblyEvaluatedValue; + variableOffsetValue: PossiblyEvaluatedValue; lineFeature: BucketFeature; e1: number; @@ -455,6 +457,15 @@ class LineBucket implements Bucket { this.variableWidthValue = lineWidth; } + // Check layers for variable offset + for (const layer of this.layers) { + const lineOffset = layer.paint.get('line-offset').value; + if (lineOffset.kind !== 'constant' && lineOffset.isLineProgressConstant === false) { + this.variableOffsetValue = lineOffset; + break; + } + } + if (this.elevationType === 'road') { const vertexOffset = this.layoutVertexArray.length; const added = this.addElevatedRoadFeature(feature, geometry, canonical, elevationFeatures, join, cap, miterLimit, roundLimit); @@ -1019,7 +1030,7 @@ class LineBucket implements Bucket { evaluateLineProgressFeatures(distance: number): LineProgressFeatures | null { assert(distance >= 0); - if (!this.variableWidthValue && this.elevationType !== 'offset') { + if (!this.variableWidthValue && !this.variableOffsetValue && this.elevationType !== 'offset') { return null; } this.evaluationGlobals.lineProgress = 0; @@ -1032,14 +1043,20 @@ class LineBucket implements Bucket { if (this.variableWidthValue && this.variableWidthValue.kind !== 'constant') { variableWidth = this.variableWidthValue.evaluate(this.evaluationGlobals, this.lineFeature) || 0.0; } + + let variableOffset = 0.0; + if (this.variableOffsetValue && this.variableOffsetValue.kind !== 'constant') { + variableOffset = this.variableOffsetValue.evaluate(this.evaluationGlobals, this.lineFeature) || 0.0; + } + if (this.elevationType !== 'offset') { - return {zOffset: 0.0, variableWidth}; + return {zOffset: 0.0, variableWidth, variableOffset}; } if (this.zOffsetValue.kind === 'constant') { - return {zOffset: this.zOffsetValue.value, variableWidth}; + return {zOffset: this.zOffsetValue.value, variableWidth, variableOffset}; } const zOffset: number = this.zOffsetValue.evaluate(this.evaluationGlobals, this.lineFeature) || 0.0; - return {zOffset, variableWidth}; + return {zOffset, variableWidth, variableOffset}; } /** @@ -1181,7 +1198,7 @@ class LineBucket implements Bucket { this.zOffsetVertexArray.emplaceBack( lineProgressFeatures.zOffset, lineProgressFeatures.variableWidth, - lineProgressFeatures.variableWidth + lineProgressFeatures.variableOffset ); } assert(this.zOffsetVertexArray.length === this.layoutVertexArray.length || this.elevationType !== 'offset'); diff --git a/src/render/draw_line.ts b/src/render/draw_line.ts index 2b9457755c2..9581bcab5ef 100644 --- a/src/render/draw_line.ts +++ b/src/render/draw_line.ts @@ -133,6 +133,11 @@ export default function drawLine(painter: Painter, sourceCache: SourceCache, lay definesValues.push("VARIABLE_LINE_WIDTH"); } + const offset = layer.paint.get('line-offset').value; + if (offset.kind !== 'constant' && offset.isLineProgressConstant === false) { + definesValues.push("VARIABLE_LINE_OFFSET"); + } + if (isDraping) { if (painter.emissiveMode === 'dual-source-blending' && !constantEmissiveStrength) { definesValues.push('DUAL_SOURCE_BLENDING'); diff --git a/src/render/program/line_program.ts b/src/render/program/line_program.ts index 21caf90c14c..fb717ce0cb2 100644 --- a/src/render/program/line_program.ts +++ b/src/render/program/line_program.ts @@ -54,7 +54,7 @@ export type LinePatternUniformsType = { ['u_pattern_transition']: Uniform1f; }; -export type LineDefinesType = 'RENDER_LINE_GRADIENT' | 'RENDER_LINE_DASH' | 'RENDER_LINE_TRIM_OFFSET' | 'RENDER_LINE_BORDER' | 'LINE_JOIN_NONE' | 'ELEVATED' | 'VARIABLE_LINE_WIDTH' | 'CROSS_SLOPE_VERTICAL' | 'CROSS_SLOPE_HORIZONTAL' | 'ELEVATION_REFERENCE_SEA' | 'LINE_PATTERN_TRANSITION' | 'USE_MRT1' | 'DUAL_SOURCE_BLENDING'; +export type LineDefinesType = 'RENDER_LINE_GRADIENT' | 'RENDER_LINE_DASH' | 'RENDER_LINE_TRIM_OFFSET' | 'RENDER_LINE_BORDER' | 'LINE_JOIN_NONE' | 'ELEVATED' | 'VARIABLE_LINE_WIDTH' | 'VARIABLE_LINE_OFFSET' | 'CROSS_SLOPE_VERTICAL' | 'CROSS_SLOPE_HORIZONTAL' | 'ELEVATION_REFERENCE_SEA' | 'LINE_PATTERN_TRANSITION' | 'USE_MRT1' | 'DUAL_SOURCE_BLENDING'; const lineUniforms = (context: Context): LineUniformsType => ({ 'u_matrix': new UniformMatrix4f(context), diff --git a/src/shaders/line.vertex.glsl b/src/shaders/line.vertex.glsl index 2a0c8fd2017..e5992cacc66 100644 --- a/src/shaders/line.vertex.glsl +++ b/src/shaders/line.vertex.glsl @@ -12,7 +12,7 @@ in vec2 a_pos_normal; in vec4 a_data; -#if defined(ELEVATED) || defined(ELEVATED_ROADS) || defined(VARIABLE_LINE_WIDTH) +#if defined(ELEVATED) || defined(ELEVATED_ROADS) || defined(VARIABLE_LINE_WIDTH) || defined(VARIABLE_LINE_OFFSET) in vec3 a_z_offset_width; #endif @@ -130,6 +130,10 @@ void main() { offset = -1.0 * offset * u_width_scale; +#ifdef VARIABLE_LINE_OFFSET + offset = -1.0 * a_z_offset_width.z * u_width_scale; +#endif + // these transformations used to be applied in the JS and native code bases. // moved them into the shader for clarity and simplicity. gapwidth = gapwidth / 2.0; @@ -137,7 +141,16 @@ void main() { #ifdef VARIABLE_LINE_WIDTH bool left = normal.y == 1.0; float left_width = a_z_offset_width.y; - float right_width = a_z_offset_width.z; + float right_width = a_z_offset_width.y; + + #ifdef VARIABLE_LINE_OFFSET + // When both features active: use symmetric width, offset from z component + offset = -1.0 * a_z_offset_width.z * u_width_scale; + #else + // Original behavior: z component is right_width for asymmetric lines + right_width = a_z_offset_width.z; + #endif + halfwidth = (u_width_scale * (left ? left_width : right_width)) / 2.0; a_z_offset += left ? side_z_offset : 0.0; v_normal = side_z_offset > 0.0 && left ? vec2(0.0) : v_normal; diff --git a/src/shaders/line_pattern.vertex.glsl b/src/shaders/line_pattern.vertex.glsl index 33c1c5fa8e3..e97a522bddd 100644 --- a/src/shaders/line_pattern.vertex.glsl +++ b/src/shaders/line_pattern.vertex.glsl @@ -12,7 +12,7 @@ in vec2 a_pos_normal; in vec4 a_data; -#if defined(ELEVATED) || defined(ELEVATED_ROADS) +#if defined(ELEVATED) || defined(ELEVATED_ROADS) || defined(VARIABLE_LINE_OFFSET) in vec3 a_z_offset_width; #endif // Includes in order: a_uv_x, a_split_index, a_line_progress @@ -128,6 +128,11 @@ void main() { float halfwidth = (u_width_scale * width) / 2.0; offset = -1.0 * offset * u_width_scale; +#ifdef VARIABLE_LINE_OFFSET + // Variable offset uses the same scaling as regular offset + offset = -1.0 * a_z_offset_width.z * u_width_scale; +#endif + float inset = gapwidth + (gapwidth > 0.0 ? ANTIALIASING : 0.0); float outset = gapwidth + halfwidth * (gapwidth > 0.0 ? 2.0 : 1.0) + (halfwidth == 0.0 ? 0.0 : ANTIALIASING); diff --git a/src/style-spec/reference/v8.json b/src/style-spec/reference/v8.json index ded33e92f13..531f37c8e34 100644 --- a/src/style-spec/reference/v8.json +++ b/src/style-spec/reference/v8.json @@ -5271,7 +5271,7 @@ } }, "line-progress": { - "doc": "Returns the progress along a gradient line. Can only be used in the `line-gradient` and `line-z-offset` properties.", + "doc": "Returns the progress along a gradient line. Can only be used in the `line-gradient`, `line-z-offset`, and `line-offset` properties.", "group": "Feature data", "sdk-support": { "basic functionality": { @@ -8474,7 +8474,8 @@ "zoom", "feature", "feature-state", - "measure-light" + "measure-light", + "line-progress" ] }, "property-type": "data-driven" diff --git a/test/integration/render-tests/line-offset/line-progress-curved/expected.png b/test/integration/render-tests/line-offset/line-progress-curved/expected.png new file mode 100644 index 00000000000..b5492773417 Binary files /dev/null and b/test/integration/render-tests/line-offset/line-progress-curved/expected.png differ diff --git a/test/integration/render-tests/line-offset/line-progress-curved/style.json b/test/integration/render-tests/line-offset/line-progress-curved/style.json new file mode 100644 index 00000000000..edec091c23c --- /dev/null +++ b/test/integration/render-tests/line-offset/line-progress-curved/style.json @@ -0,0 +1,72 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 256, + "height": 256 + } + }, + "zoom": 2, + "sources": { + "line": { + "type": "geojson", + "lineMetrics": true, + "data": { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-15, 15], + [-5, -15], + [5, 15], + [15, -15] + ] + } + } + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "reference-line", + "type": "line", + "source": "line", + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": 15, + "line-color": "#cccccc" + } + }, + { + "id": "variable-offset-line", + "type": "line", + "source": "line", + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": 4, + "line-color": "#ff0000", + "line-offset": [ + "interpolate", + ["linear"], + ["line-progress"], + 0, -8, + 0.5, 0, + 1, 8 + ] + } + } + ] +} diff --git a/test/integration/render-tests/line-offset/line-progress-exponential/expected.png b/test/integration/render-tests/line-offset/line-progress-exponential/expected.png new file mode 100644 index 00000000000..63ac9b6f9c9 Binary files /dev/null and b/test/integration/render-tests/line-offset/line-progress-exponential/expected.png differ diff --git a/test/integration/render-tests/line-offset/line-progress-exponential/style.json b/test/integration/render-tests/line-offset/line-progress-exponential/style.json new file mode 100644 index 00000000000..92ae41cc7c1 --- /dev/null +++ b/test/integration/render-tests/line-offset/line-progress-exponential/style.json @@ -0,0 +1,61 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 256, + "height": 256 + } + }, + "zoom": 2, + "sources": { + "line": { + "type": "geojson", + "lineMetrics": true, + "data": { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-20, 0], + [20, 0] + ] + } + } + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "reference-line", + "type": "line", + "source": "line", + "paint": { + "line-width": 40, + "line-color": "#cccccc" + } + }, + { + "id": "variable-offset-line", + "type": "line", + "source": "line", + "paint": { + "line-width": 4, + "line-color": "#0000ff", + "line-offset": [ + "interpolate", + ["exponential", 2], + ["line-progress"], + 0, -20, + 1, 20 + ] + } + } + ] +} diff --git a/test/integration/render-tests/line-offset/line-progress-with-variable-width/expected.png b/test/integration/render-tests/line-offset/line-progress-with-variable-width/expected.png new file mode 100644 index 00000000000..a3f7b50eb2c Binary files /dev/null and b/test/integration/render-tests/line-offset/line-progress-with-variable-width/expected.png differ diff --git a/test/integration/render-tests/line-offset/line-progress-with-variable-width/style.json b/test/integration/render-tests/line-offset/line-progress-with-variable-width/style.json new file mode 100644 index 00000000000..3ed5456f8cf --- /dev/null +++ b/test/integration/render-tests/line-offset/line-progress-with-variable-width/style.json @@ -0,0 +1,67 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 256, + "height": 256 + } + }, + "zoom": 2, + "sources": { + "line": { + "type": "geojson", + "lineMetrics": true, + "data": { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-20, 0], + [20, 0] + ] + } + } + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "reference-line", + "type": "line", + "source": "line", + "paint": { + "line-width": 40, + "line-color": "#cccccc" + } + }, + { + "id": "variable-line", + "type": "line", + "source": "line", + "paint": { + "line-width": [ + "interpolate", + ["linear"], + ["line-progress"], + 0, 2, + 1, 10 + ], + "line-color": "#00ff00", + "line-offset": [ + "interpolate", + ["linear"], + ["line-progress"], + 0, -20, + 1, 20 + ] + } + } + ] +} diff --git a/test/integration/render-tests/line-offset/line-progress/expected.png b/test/integration/render-tests/line-offset/line-progress/expected.png new file mode 100644 index 00000000000..c46c3bfa43b Binary files /dev/null and b/test/integration/render-tests/line-offset/line-progress/expected.png differ diff --git a/test/integration/render-tests/line-offset/line-progress/style.json b/test/integration/render-tests/line-offset/line-progress/style.json new file mode 100644 index 00000000000..7e85e19edaf --- /dev/null +++ b/test/integration/render-tests/line-offset/line-progress/style.json @@ -0,0 +1,61 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 256, + "height": 256 + } + }, + "zoom": 2, + "sources": { + "line": { + "type": "geojson", + "lineMetrics": true, + "data": { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [ + [-20, 0], + [20, 0] + ] + } + } + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "reference-line", + "type": "line", + "source": "line", + "paint": { + "line-width": 40, + "line-color": "#cccccc" + } + }, + { + "id": "variable-offset-line", + "type": "line", + "source": "line", + "paint": { + "line-width": 4, + "line-color": "#ff0000", + "line-offset": [ + "interpolate", + ["linear"], + ["line-progress"], + 0, -20, + 1, 20 + ] + } + } + ] +} diff --git a/test/unit/data/line_bucket.test.ts b/test/unit/data/line_bucket.test.ts index 60ff1b1f284..8191d7d47e4 100644 --- a/test/unit/data/line_bucket.test.ts +++ b/test/unit/data/line_bucket.test.ts @@ -137,3 +137,99 @@ test('LineBucket segmentation', () => { expect(console.warn).toHaveBeenCalledTimes(1); }); + +test('LineBucket with variable line-offset detects variable offset', () => { + const layer = new LineStyleLayer({ + id: 'test', + type: 'line', + paint: { + 'line-offset': [ + 'interpolate', + ['linear'], + ['line-progress'], + 0, -10, + 1, 10 + ] + } + }); + layer.recalculate({zoom: 14}); + + const bucket = new LineBucket({layers: [layer]}); + + const line = { + type: 2, + properties: { + mapbox_clip_start: 0, + mapbox_clip_end: 1 + } + }; + + // Test that variable offset is detected when layer is configured + const paint = layer.paint; + const lineOffset = paint.get('line-offset').value; + expect(lineOffset.kind).not.toBe('constant'); + expect(lineOffset.isLineProgressConstant).toBe(false); +}); + +test('LineBucket with both variable width and variable offset', () => { + const layer = new LineStyleLayer({ + id: 'test', + type: 'line', + paint: { + 'line-width': [ + 'interpolate', + ['linear'], + ['line-progress'], + 0, 2, + 1, 10 + ], + 'line-offset': [ + 'interpolate', + ['linear'], + ['line-progress'], + 0, -5, + 1, 5 + ] + } + }); + layer.recalculate({zoom: 14}); + + const bucket = new LineBucket({layers: [layer]}); + + // Both variable width and offset should be detected in layer configuration + const paint = layer.paint; + const lineWidth = paint.get('line-width').value; + const lineOffset = paint.get('line-offset').value; + + expect(lineWidth.kind).not.toBe('constant'); + expect(lineWidth.isLineProgressConstant).toBe(false); + expect(lineOffset.kind).not.toBe('constant'); + expect(lineOffset.isLineProgressConstant).toBe(false); +}); + +test('LineBucket with constant offset should not set variableOffsetValue', () => { + const layer = new LineStyleLayer({ + id: 'test', + type: 'line', + paint: { + 'line-offset': 5 + } + }); + layer.recalculate({zoom: 14}); + + const bucket = new LineBucket({layers: [layer]}); + + const line = { + type: 2, + properties: {} + }; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + bucket.addFeature(line, [[ + new Point(0, 0), + new Point(100, 0) + ]]); + + // Constant offset should not trigger variable offset + expect(bucket.variableOffsetValue).toBeFalsy(); +});