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
+
+
+ Background Line (20px)
+
+
+
+ Variable Offset Line
+
+
+
+
+
+
+
+
+
+
+
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();
+});