Skip to content

Commit cfce282

Browse files
committed
fix(billboards): Scale SVGs in texture atlas to match billboard display size
1 parent debd74e commit cfce282

File tree

8 files changed

+143
-39
lines changed

8 files changed

+143
-39
lines changed

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#### Fixes :wrench:
88

99
- Billboards using `imageSubRegion` now render as expected. [#12585](https://github.com/CesiumGS/cesium/issues/12585)
10+
- Improved scaling of SVGs in billboards [#TODO](https://github.com/CesiumGS/cesium/issues/TODO)
1011

1112
#### Additions :tada:
1213

CONTRIBUTORS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to Cesiu
105105
- [Daniel Zhong](https://github.com/danielzhong)
106106
- [Mark Schlosser](https://github.com/markschlosseratbentley)
107107
- [Adam Larkeryd](https://github.com/alarkbentley)
108+
- [Don McCurdy](https://github.com/donmccurdy)
108109
- [Flightradar24 AB](https://www.flightradar24.com)
109110
- [Aleksei Kalmykov](https://github.com/kalmykov)
110111
- [BIT Systems](http://www.caci.com/bit-systems)

packages/engine/Source/DataSources/BillboardGraphics.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import createPropertyDescriptor from "./createPropertyDescriptor.js";
1010
* Initialization options for the BillboardGraphics constructor
1111
*
1212
* @property {Property | boolean} [show=true] A boolean Property specifying the visibility of the billboard.
13-
* @property {Property | string | HTMLCanvasElement} [image] A Property specifying the Image, URI, or Canvas to use for the billboard.
13+
* @property {Property | string | HTMLImageElement | HTMLCanvasElement} [image] A Property specifying the Image, URI, or Canvas to use for the billboard.
1414
* @property {Property | number} [scale=1.0] A numeric Property specifying the scale to apply to the image size.
1515
* @property {Property | Cartesian2} [pixelOffset=Cartesian2.ZERO] A {@link Cartesian2} Property specifying the pixel offset.
1616
* @property {Property | Cartesian3} [eyeOffset=Cartesian3.ZERO] A {@link Cartesian3} Property specifying the eye offset.

packages/engine/Source/DataSources/BillboardVisualizer.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,6 @@ BillboardVisualizer.prototype.update = function (time) {
131131
}
132132

133133
billboard.show = show;
134-
if (item.textureValue !== textureValue) {
135-
billboard.image = textureValue;
136-
item.textureValue = textureValue;
137-
}
138134
billboard.position = position;
139135
billboard.color = Property.getValueOrDefault(
140136
billboardGraphics._color,
@@ -227,6 +223,13 @@ BillboardVisualizer.prototype.update = function (time) {
227223
defaultSplitDirection,
228224
);
229225

226+
// Apply .image last, so any necessary property values are available
227+
// in Billboard#_computeImageTextureSize before adding to the atlas.
228+
if (item.textureValue !== textureValue) {
229+
billboard.image = textureValue;
230+
item.textureValue = textureValue;
231+
}
232+
230233
const subRegion = Property.getValueOrUndefined(
231234
billboardGraphics._imageSubRegion,
232235
time,

packages/engine/Source/Renderer/TextureAtlas.js

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -648,9 +648,11 @@ async function resolveImage(image, id) {
648648
* @param {string} id An identifier to detect whether the image already exists in the atlas.
649649
* @param {HTMLImageElement|HTMLCanvasElement|string|Resource|Promise|TextureAtlas.CreateImageCallback} image An image or canvas to add to the texture atlas,
650650
* or a URL to an Image, or a Promise for an image, or a function that creates an image.
651+
* @param {number} width A number specifying the width of the texture. If undefined, the image width will be used.
652+
* @param {number} height A number specifying the height of the texture. If undefined, the image height will be used.
651653
* @returns {Promise<number>} A Promise that resolves to the image region index, or -1 if resources are in the process of being destroyed.
652654
*/
653-
TextureAtlas.prototype.addImage = function (id, image) {
655+
TextureAtlas.prototype.addImage = function (id, image, width, height) {
654656
//>>includeStart('debug', pragmas.debug);
655657
Check.typeOf.string("id", id);
656658
Check.defined("image", image);
@@ -671,17 +673,24 @@ TextureAtlas.prototype.addImage = function (id, image) {
671673
this._indexById.set(id, index);
672674

673675
const resolveAndAddImage = async () => {
674-
image = await resolveImage(image, id);
676+
const resolvedImage = await resolveImage(image, id);
675677
//>>includeStart('debug', pragmas.debug);
676-
Check.defined("image", image);
678+
Check.defined("image", resolvedImage);
677679
//>>includeEnd('debug');
678680

679-
if (this.isDestroyed() || !defined(image)) {
681+
if (this.isDestroyed() || !defined(resolvedImage)) {
680682
this._indexPromiseById.delete(id);
681683
return -1;
682684
}
683685

684-
const imageIndex = await this._addImage(index, image);
686+
if (defined(width)) {
687+
resolvedImage.width = width;
688+
}
689+
if (defined(height)) {
690+
resolvedImage.height = height;
691+
}
692+
693+
const imageIndex = await this._addImage(index, resolvedImage);
685694
this._indexPromiseById.delete(id);
686695
return imageIndex;
687696
};

packages/engine/Source/Scene/Billboard.js

Lines changed: 76 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import SceneMode from "./SceneMode.js";
2323
import SceneTransforms from "./SceneTransforms.js";
2424
import VerticalOrigin from "./VerticalOrigin.js";
2525
import SplitDirection from "./SplitDirection.js";
26+
import getExtensionFromUri from "../Core/getExtensionFromUri.js";
27+
import isDataUri from "../Core/isDataUri.js";
2628

2729
/**
2830
* @typedef {object} Billboard.ConstructorOptions
@@ -32,7 +34,7 @@ import SplitDirection from "./SplitDirection.js";
3234
* @property {Cartesian3} position The cartesian position of the billboard.
3335
* @property {*} [id] A user-defined object to return when the billboard is picked with {@link Scene#pick}.
3436
* @property {boolean} [show=true] Determines if this billboard will be shown.
35-
* @property {string | HTMLCanvasElement} [image] A loaded HTMLImageElement, ImageData, or a url to an image to use for the billboard.
37+
* @property {string | HTMLImageElement | HTMLCanvasElement} [image] A loaded HTMLImageElement, ImageData, or a url to an image to use for the billboard.
3638
* @property {number} [scale=1.0] A number specifying the uniform scale that is multiplied with the billboard's image size in pixels.
3739
* @property {Cartesian2} [pixelOffset=Cartesian2.ZERO] A {@link Cartesian2} Specifying the pixel offset in screen space from the origin of this billboard.
3840
* @property {Cartesian3} [eyeOffset=Cartesian3.ZERO] A {@link Cartesian3} Specifying the 3D Cartesian offset applied to this billboard in eye coordinates.
@@ -190,28 +192,26 @@ function Billboard(options, billboardCollection) {
190192

191193
this._imageTexture = new BillboardTexture(billboardCollection);
192194

195+
this._imageId = options.imageId;
196+
this._imageWidth = undefined;
197+
this._imageHeight = undefined;
193198
this._labelDimensions = undefined;
194199
this._labelHorizontalOrigin = undefined;
195200
this._labelTranslate = undefined;
196201

197202
const image = options.image;
198-
let imageId = options.imageId;
199203
if (defined(image)) {
200-
if (!defined(imageId)) {
201-
if (typeof image === "string") {
202-
imageId = image;
203-
} else if (defined(image.src)) {
204-
imageId = image.src;
205-
} else {
206-
imageId = createGuid();
207-
}
208-
}
209-
210-
this._imageTexture.loadImage(imageId, image);
204+
this._computeImageTextureProperties(options.imageId, image);
205+
this._imageTexture.loadImage(
206+
this._imageId,
207+
image,
208+
this._imageWidth,
209+
this._imageHeight,
210+
);
211211
}
212212

213213
if (defined(options.imageSubRegion)) {
214-
this._imageTexture.addImageSubRegion(imageId, options.imageSubRegion);
214+
this._imageTexture.addImageSubRegion(this._imageId, options.imageSubRegion);
215215
}
216216

217217
this._actualClampedPosition = undefined;
@@ -945,18 +945,13 @@ Object.defineProperties(Billboard.prototype, {
945945
return;
946946
}
947947

948-
let id;
949-
if (typeof value === "string") {
950-
id = value;
951-
} else if (value instanceof Resource) {
952-
id = value._url;
953-
} else if (defined(value.src)) {
954-
id = value.src;
955-
} else {
956-
id = createGuid();
957-
}
958-
959-
this._imageTexture.loadImage(id, value);
948+
this._computeImageTextureProperties(undefined, value);
949+
this._imageTexture.loadImage(
950+
this._imageId,
951+
value,
952+
this._imageWidth,
953+
this._imageHeight,
954+
);
960955
},
961956
},
962957

@@ -1250,7 +1245,13 @@ Billboard.prototype.setImage = function (id, image) {
12501245
Check.defined("image", image);
12511246
//>>includeEnd('debug');
12521247

1253-
this._imageTexture.loadImage(id, image);
1248+
this._computeImageTextureProperties(id, image);
1249+
this._imageTexture.loadImage(
1250+
this._imageId,
1251+
image,
1252+
this._imageWidth,
1253+
this._imageHeight,
1254+
);
12541255
};
12551256

12561257
/**
@@ -1266,6 +1267,54 @@ Billboard.prototype.setImageTexture = function (billboardTexture) {
12661267
BillboardTexture.clone(billboardTexture, this._imageTexture);
12671268
};
12681269

1270+
/** Arbitrary limit on allocated SVG size, in pixels. Raster images use image resolution. */
1271+
const SVG_MAX_SIZE_PX = 512;
1272+
1273+
/**
1274+
* Computes billboard texture ID, width, and height. For raster images, width and height are left
1275+
* undefined, defaulting to image resolution. For SVG, use billboard pixel width and height.
1276+
* @param {string | undefined} id The id of the image.
1277+
* @param {string | HTMLImageElement | HTMLCanvasElement | undefined} image A loaded HTMLImageElement, ImageData, or a url to an image to use for the billboard.
1278+
* @private
1279+
*/
1280+
Billboard.prototype._computeImageTextureProperties = function (id, image) {
1281+
this._imageWidth = undefined;
1282+
this._imageHeight = undefined;
1283+
1284+
if (!defined(image)) {
1285+
this._imageId = createGuid();
1286+
return;
1287+
}
1288+
1289+
let imageUri;
1290+
if (typeof image === "string") {
1291+
imageUri = image;
1292+
} else if (image instanceof Resource) {
1293+
imageUri = image._url;
1294+
} else if (defined(image.src)) {
1295+
imageUri = image.src;
1296+
}
1297+
1298+
this._imageId = id ?? imageUri ?? createGuid();
1299+
1300+
const hasSizeInPixels =
1301+
defined(this._width) && defined(this._height) && !this._sizeInMeters;
1302+
1303+
if (hasSizeInPixels && isSvgUri(imageUri)) {
1304+
this._imageWidth = Math.min(this._width, SVG_MAX_SIZE_PX);
1305+
this._imageHeight = Math.min(this._height, SVG_MAX_SIZE_PX);
1306+
}
1307+
};
1308+
1309+
function isSvgUri(uri) {
1310+
if (!defined(uri)) {
1311+
return false;
1312+
}
1313+
return isDataUri(uri)
1314+
? uri.startsWith("data:image/svg+xml")
1315+
: getExtensionFromUri(uri) === "svg";
1316+
}
1317+
12691318
/**
12701319
* Uses a sub-region of the image with the given id as the image for this billboard,
12711320
* measured in pixels from the bottom-left.

packages/engine/Source/Scene/BillboardTexture.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,15 @@ BillboardTexture.prototype.unload = async function () {
160160
* @param {string} id An identifier to detect whether the image already exists in the atlas.
161161
* @param {HTMLImageElement|HTMLCanvasElement|string|Resource|Promise|TextureAtlas.CreateImageCallback} image An image or canvas to add to the texture atlas,
162162
* or a URL to an Image, or a Promise for an image, or a function that creates an image.
163+
* @param {number} width A number specifying the width of the texture. If undefined, the image width will be used.
164+
* @param {number} height A number specifying the height of the texture. If undefined, the image height will be used.
163165
*/
164-
BillboardTexture.prototype.loadImage = async function (id, image) {
166+
BillboardTexture.prototype.loadImage = async function (
167+
id,
168+
image,
169+
width,
170+
height,
171+
) {
165172
if (this._id === id) {
166173
// This image has already been loaded
167174
return;
@@ -192,7 +199,7 @@ BillboardTexture.prototype.loadImage = async function (id, image) {
192199
let index;
193200
const atlas = this._billboardCollection.textureAtlas;
194201
try {
195-
index = await atlas.addImage(id, image);
202+
index = await atlas.addImage(id, image, width, height);
196203
} catch (error) {
197204
// There was an error loading the image
198205
billboardTexture._loadState = BillboardLoadState.ERROR;

packages/engine/Specs/Scene/BillboardCollectionSpec.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,40 @@ describe("Scene/BillboardCollection", function () {
347347
expect(b.image.length).toBe(guidLength);
348348
});
349349

350+
it("image property setter computes SVG width and height", function () {
351+
const b = billboards.add();
352+
353+
// Don't override image size if billboard size is unspecified.
354+
b.image = "icon.svg";
355+
expect(b.image).toBe("icon.svg");
356+
expect(b._imageWidth).toBeUndefined();
357+
expect(b._imageHeight).toBeUndefined();
358+
359+
// Don't override image size for raster images.
360+
b.width = b.height = 50;
361+
b.image = "icon.png";
362+
expect(b._imageWidth).toBeUndefined();
363+
expect(b._imageHeight).toBeUndefined();
364+
365+
// Override image size with billboard size for SVGs.
366+
b.width = b.height = 50;
367+
b.image = "icon.svg";
368+
expect(b._imageWidth).toBe(50);
369+
expect(b._imageHeight).toBe(50);
370+
371+
// Limit billboard-derived SVG size to 512px.
372+
b.width = b.height = 10000;
373+
b.image = "icon-lg.svg";
374+
expect(b._imageWidth).toBe(512);
375+
expect(b._imageHeight).toBe(512);
376+
377+
// Don't override image size if billboard is not sized in pixels.
378+
b.sizeInMeters = true;
379+
b.image = "icon.svg";
380+
expect(b._imageWidth).toBeUndefined();
381+
expect(b._imageHeight).toBeUndefined();
382+
});
383+
350384
it("is not destroyed", function () {
351385
expect(billboards.isDestroyed()).toEqual(false);
352386
});

0 commit comments

Comments
 (0)