diff --git a/docs/plugins/markers.md b/docs/plugins/markers.md index f717e96b4..610a0d9a7 100644 --- a/docs/plugins/markers.md +++ b/docs/plugins/markers.md @@ -122,9 +122,9 @@ const viewer = new Viewer({ // polygon marker id: 'polygon', polygon: [ - [6.2208, 0.0906], [0.0443, 0.1028], [0.2322, 0.0849], [0.4531, 0.0387], + [6.2208, 0.0906], [0.0443, 0.1028], [0.2322, 0.0849], [0.4531, 0.0387], [0.5022, -0.0056], [0.4587, -0.0396], [0.252, -0.0453], [0.0434, -0.0575], - [6.1302, -0.0623], [6.0094, -0.0169], [6.0471, 0.032], [6.2208, 0.0906], + [6.1302, -0.0623], [6.0094, -0.0169], [6.0471, 0.0320], [6.2208, 0.0906], ], svgStyle: { fill: 'rgba(200, 0, 0, 0.2)', @@ -247,43 +247,53 @@ You can try markers live in [the playground](../playground.md). One, and only one, of these options is required for each marker. -| Name | Type | Description | -| ---------------- | ---------------------------------------- | ----------------------------------------------------------------------------- | -| `image` | `string` | Path to an image file. Requires `width` and `height` to be defined. | -| `imageLayer` | `string` | Path to an image file. | -| `videoLayer` | `string` | Path to a video file. | -| `html` | `string` | HTML content of the marker. It is recommended to define `width` and `height`. | -| `element` | `HTMLElement` | Existing DOM element. | -| `square` | `integer` | Size of the square. | -| `rect` | `integer[2]`
`{width:int,height:int}` | Size of the rectangle. | -| `circle` | `integer` | Radius of the circle. | -| `ellipse` | `integer[2]`
`{rx:int,ry:int}` | Radiuses of the ellipse. | -| `path` | `string` | Definition of the path (0,0 will be placed at the defined `position`). | -| `polygon` | `double[2][]`
`string[2][]` | Array of points defining the polygon in spherical coordinates. | -| `polygonPixels` | `integer[2][]` | Same as `polygon` but in pixel coordinates on the panorama image. | -| `polyline` | `double[2][]`
`string[2][]` | Same as `polygon` but generates a polyline. | -| `polylinePixels` | `integer[2][]` | Same as `polygonPixels` but generates a polyline. | - -**Examples :** +#### `image` + +- type: `string` + +Path to an image file. Requires `size` to be defined. + +```js{3} +{ + id: 'marker-1', + image: 'pin-red.png', + position: { yaw: 0, pitch: 0 }, + size: { width: 32, height: 32 }, +} +``` + +#### `imageLayer` + +- type: `string` + +Path to an image file. + +::: tip "Layers" positionning +There is two ways to position `imageLayer` and `videoLayer` markers: + +- `position` (one value) + `size` + `anchor` (optional) + `orientation` (optional) +- `position` with four values defining the corners of the image/video + +[Check the demo](../demos/markers/layers.md) +::: + +```js{3} +{ + id: 'marker-1', + imageLayer: 'pin-red.png', + position: { yaw: 0, pitch: 0 }, + size: { width: 32, height: 32 }, +} -```js { - image: 'pin-red.png', - imageLayer: 'pin-blue.png', - videoLayer: 'intro.mp4', - html: 'Click here', - element: document.querySelector('#my-marker'), - square: 10, - rect: [10, 5], - rect: {width: 10, height: 5}, - circle: 10, - ellipse: [10, 5], - ellipse: {rx: 10, ry: 5}, - path: 'M 0 0 L 60 60 L 60 0 L 0 60 L 0 0', - polygon: [[0.2, 0.4], [0.9, 1.1], [1.5, 0.7]], - polygonPixels: [[100, 200], [150, 300], [300, 200]], - polyline: [[0.2, 0.4], [0.9, 1.1]], - polylinePixels: [[100, 200], [150, 300]], + id: 'marker-2', + imageLayer: 'pin-red.png', + position: [ + { yaw: -0.2, pitch: 0.2 }, + { yaw: 0.2, pitch: 0.2 }, + { yaw: 0.2, pitch: -0.2 }, + { yaw: -0.2, pitch: -0.2 }, + ], } ``` @@ -292,6 +302,50 @@ Both allows to display an image but the difference is in the rendering technique And `image` marker is rendered flat above the viewer but and `imageLayer` is rendered inside the panorama itself, this allows for more natural movements and scaling. ::: +#### `videoLayer` + +- type: `string` + +Path to a video file. It is positionned exactly like `imageLayer`. It can be used with the [`chromaKey`](#chromakey) option. + +```js{3} +{ + id: 'marker-1', + videoLayer: 'intro.mp4', + position: { yaw: 0, pitch: 0 }, + size: { width: 600, height: 400 }, +} +``` + +#### `html` + +- type: `string` + +HTML content of the marker. It is recommended to define th `size`. + +```js{3} +{ + id: 'marker-1', + html: 'Click here', + position: { yaw: 0, pitch: 0 }, + size: { width: 100, height: 30 }, +} +``` + +#### `element` + +- type: `HTMLElement` + +Existing DOM element. + +```js{3} +{ + id: 'marker-1', + element: document.querySelector('#my-marker'), + position: { yaw: 0, pitch: 0 }, +} +``` + ::: tip Custom element markers The `element` marker accepts [Web Components](https://developer.mozilla.org/docs/Web/API/Web_components/Using_custom_elements). If your component has an `updateMarker()` method it will be called by the plugin on each render with a bunch of properties: @@ -305,14 +359,149 @@ If your component has an `updateMarker()` method it will be called by the plugin [Check the demo](../demos/markers/custom-element.md) ::: -::: tip "Layers" positionning -There is two ways to position `imageLayer` and `videoLayer` markers: +#### `polygon` -- `position` (one value) + `size` + `anchor` (optional) + `orientation` (optional) -- `position` with four values defining the corners of the image/video +- type: `number[2][] | number[2][][] | string[2][] | string[2][][]` + +Array of points defining the polygon in spherical coordinates (degrees or radians). +The polygon can have one or more holes by defined them in a nested array (the syntax is [similar to GeoJSON](https://geojson.readthedocs.io/en/latest/#polygon)). + +```js{3,8-11} +{ + id: 'marker-1', + polygon: [[0.2, 0.4], [0.9, 1.1], [1.5, 0.7]]; +} + +{ + id: 'marker-2', + polygon: [ + [[0.2, 0.4], [0.9, 1.1], [1.5, 0.7]], + [[0.3, 0.5], [1.4, 0.8], [0.8, 1.0]], // holes coordinates must be in reverse order + ], +} +``` + +#### `polygonPixels` + +- type: `number[2][] | number[2][][]` + +Same as `polygon` but in pixel coordinates on the panorama image. + +```js{3} +{ + id: 'marker-1', + polygonPixels: [[100, 200], [150, 300], [300, 200]], +} +``` + +#### `polyline` + +- type: `number[2][] | string[2][]` + +Same as `polygon` but generates a polyline. + +```js{3} +{ + id: 'marker-1', + polyline: [[0.2, 0.4], [0.9, 1.1]], +} +``` + +#### `polylinePixels` + +- type: `number[2][] | string[2][]` + +Same as `polygonPixels` but generates a polyline. + +```js{3} +{ + id: 'marker-1', + polylinePixels: [[100, 200], [150, 300]], +} +``` + +#### `square` + +- type: `integer` + +Size of the square. + +```js{3} +{ + id: 'marker-1', + square: 10, + position: { yaw: 0, pitch: 0 }, +} +``` + +#### `rect` + +- type: `integer[2] | { width: integer, height: integer }` + +Size of the rectangle. + +```js{3,9} +{ + id: 'marker-1', + rect: [10, 5], + position: { yaw: 0, pitch: 0 }, +} + +{ + id: 'marker-2', + rect: { width: 10, height: 5 }, + position: { yaw: 0, pitch: 0 }, +} +``` + +#### `circle` + +- type: `integer` + +Radius of the circle. + +```js{3} +{ + id: 'marker-1', + circle: 10, + position: { yaw: 0, pitch: 0 }, +} +``` + +#### `ellipse` + +- type: `integer[2] | { rx: integer, ry: integer }`; + +Radiuses of the ellipse. + +```js{3,9} +{ + id: 'marker-1', + ellipse: [10, 5], + position: { yaw: 0, pitch: 0 }, +} + +{ + id: 'marker-2', + ellipse: { rx: 10, ry: 5 }, + position: { yaw: 0, pitch: 0 }, +} +``` + +#### `path` + +- type: `string` + +Definition of the path (0,0 will be placed at the defined `position`). + +```js{3} +{ + id: 'marker-1', + path: 'M0,0 L60,60 L60,0 L0,60 L0,0', + position: { yaw: 0, pitch: 0 }, +} +``` -[Check the demo](../demos/markers/layers.md) -::: ### Options diff --git a/examples/plugin-markers.html b/examples/plugin-markers.html index c74a42063..93765b552 100644 --- a/examples/plugin-markers.html +++ b/examples/plugin-markers.html @@ -200,7 +200,7 @@

Lorem ipsum

polygon: [ [6.0848, 0.1806], [0.1022, 0.3166], [0.6333, 0.3836], [1.2396, 0.5112], [1.7922, 0.5544], [2.7635, 0.7853], [3.3688, 0.4723], [3.6902, 0.6161], - [4.2981, 0.6942], [4.6605, 0.6618], [5.4694, 0.188], [5.7125, 0.1302], + [4.2981, 0.6942], [4.6605, 0.6618], [5.4694, 0.1880], [5.7125, 0.1302], ], hideList: true, }); @@ -210,8 +210,13 @@

Lorem ipsum

id: 'polygon-1', // prettier-ignore polygonPixels: [ - [2941, 1413], [3042, 1402], [3041, 1555], [2854, 1559], - [2739, 1516], [2775, 1469], [2941, 1413], + [ + [2941, 1413], [3042, 1402], [3041, 1555], [2854, 1559], + [2739, 1516], [2775, 1469], [2941, 1413], + ], + [ + [2900, 1450], [2900, 1500], [2950, 1500], [2950, 1450], + ], ], svgStyle: { fill: 'rgba(255,0,0,0.2)', @@ -219,7 +224,7 @@

Lorem ipsum

strokeWidth: '2px', }, tooltip: { - content: 'Simple polygon', + content: 'Polygon with hole', position: 'bottom right', }, }); @@ -248,8 +253,8 @@

Lorem ipsum

id: 'polyline', // prettier-ignore polylinePixels: [ - 2478, 1635, 2184, 1747, 1674, 1953, 1166, 1852, - 709, 1669, 301, 1519, 94, 1399, 34, 1356, + [2478, 1635], [2184, 1747], [1674, 1953], [1166, 1852], + [709, 1669], [301, 1519], [94, 1399], [34, 1356], ], svgStyle: { stroke: 'rgba(80, 150, 50, 0.8)', diff --git a/packages/markers-plugin/src/markers/Marker3D.ts b/packages/markers-plugin/src/markers/Marker3D.ts index b14ff5258..65042eb83 100644 --- a/packages/markers-plugin/src/markers/Marker3D.ts +++ b/packages/markers-plugin/src/markers/Marker3D.ts @@ -190,6 +190,10 @@ export class Marker3D extends Marker { } }, { once: true }); + if (video.autoplay) { + video.play(); + } + this.definition = this.config.videoLayer; } else { material.alpha = this.config.opacity; diff --git a/packages/markers-plugin/src/markers/MarkerPolygon.ts b/packages/markers-plugin/src/markers/MarkerPolygon.ts index 242e9668a..fc8619e00 100644 --- a/packages/markers-plugin/src/markers/MarkerPolygon.ts +++ b/packages/markers-plugin/src/markers/MarkerPolygon.ts @@ -11,12 +11,15 @@ import { AbstractDomMarker } from './AbstractDomMarker'; * @internal */ export class MarkerPolygon extends AbstractDomMarker { + + private positions3D: Vector3[][]; + constructor(viewer: Viewer, plugin: MarkersPlugin, config: MarkerConfig) { super(viewer, plugin, config); } override createElement(): void { - this.element = document.createElementNS(SVG_NS, this.isPolygon ? 'polygon' : 'polyline'); + this.element = document.createElementNS(SVG_NS, 'path'); this.element[MARKER_DATA] = this; } @@ -45,16 +48,34 @@ export class MarkerPolygon extends AbstractDomMarker { return this.type === MarkerType.polyline || this.type === MarkerType.polylinePixels; } + private get coords(): [number, number][][] { + return this.definition; + } + override render(): Point { - const positions = this.__getPolyPositions(); - const isVisible = positions.length > (this.isPolygon ? 2 : 1); + const positions = this.__getAllPolyPositions(); + const isVisible = positions[0].length > (this.isPolygon ? 2 : 1); if (isVisible) { const position = this.viewer.dataHelper.sphericalCoordsToViewerCoords(this.state.position); - const points = positions.map((pos) => pos.x - position.x + ',' + (pos.y - position.y)).join(' '); + const points = positions + .filter((innerPos, i) => { + return innerPos.length > 0 && (this.isPolygon || i === 0); + }) + .map((innerPos) => { + let innerPoints = 'M'; + innerPoints += innerPos + .map((pos) => `${pos.x - position.x},${pos.y - position.y}`) + .join('L'); + if (this.isPolygon) { + innerPoints += 'Z'; + } + return innerPoints; + }) + .join(' '); - this.domElement.setAttributeNS(null, 'points', points); + this.domElement.setAttributeNS(null, 'd', points); this.domElement.setAttributeNS(null, 'transform', `translate(${position.x} ${position.y})`); return position; @@ -89,7 +110,7 @@ export class MarkerPolygon extends AbstractDomMarker { } // fold arrays: [1,2,3,4] => [[1,2],[3,4]] - const actualPoly: any = this.config[this.type]; + let actualPoly: any = this.config[this.type]; if (!Array.isArray(actualPoly[0])) { for (let i = 0; i < actualPoly.length; i++) { // @ts-ignore @@ -97,29 +118,47 @@ export class MarkerPolygon extends AbstractDomMarker { } } + if (!Array.isArray(actualPoly[0][0])) { + actualPoly = [actualPoly]; + } + // convert texture coordinates to spherical coordinates if (this.isPixels) { - this.definition = (actualPoly as Array<[number, number]>).map((coord) => { - const sphericalCoords = this.viewer.dataHelper.textureCoordsToSphericalCoords({ - textureX: coord[0], - textureY: coord[1], + this.definition = (actualPoly as [number, number][][]).map((coords) => { + return coords.map((coord) => { + const sphericalCoords = this.viewer.dataHelper.textureCoordsToSphericalCoords({ + textureX: coord[0], + textureY: coord[1], + }); + return [sphericalCoords.yaw, sphericalCoords.pitch]; }); - return [sphericalCoords.yaw, sphericalCoords.pitch]; }); } // clean angles else { - this.definition = (actualPoly as Array<[number | string, number | string]>).map((coord) => { - return [utils.parseAngle(coord[0]), utils.parseAngle(coord[1], true)]; + this.definition = (actualPoly as [number, number][][] | [string, string][][]).map((coords) => { + return coords.map((coord) => { + return [utils.parseAngle(coord[0]), utils.parseAngle(coord[1], true)]; + }); }); } - const centroid = this.isPolygon ? getPolygonCenter(this.definition) : getPolylineCenter(this.definition); + const centroid = this.isPolygon ? getPolygonCenter(this.coords[0]) : getPolylineCenter(this.coords[0]); this.state.position = { yaw: centroid[0], pitch: centroid[1] }; // compute x/y/z positions - this.state.positions3D = (this.definition as Array<[number, number]>).map((coord) => { - return this.viewer.dataHelper.sphericalCoordsToVector3({ yaw: coord[0], pitch: coord[1] }); + this.positions3D = this.coords.map((coords) => { + return coords.map((coord) => { + return this.viewer.dataHelper.sphericalCoordsToVector3({ yaw: coord[0], pitch: coord[1] }); + }); + }); + + this.state.positions3D = this.positions3D[0]; + } + + private __getAllPolyPositions(): Point[][] { + return this.positions3D.map(positions => { + return this.__getPolyPositions(positions); }); } @@ -127,11 +166,11 @@ export class MarkerPolygon extends AbstractDomMarker { * Computes viewer coordinates of each point of a polygon/polyline
* It handles points behind the camera by creating intermediary points suitable for the projector */ - private __getPolyPositions(): Point[] { - const nbVectors = this.state.positions3D.length; + private __getPolyPositions(positions: Vector3[]): Point[] { + const nbVectors = positions.length; // compute if each vector is visible - const positions3D = this.state.positions3D.map((vector) => { + const positions3D = positions.map((vector) => { return { vector: vector, visible: vector.dot(this.viewer.state.direction) > 0, diff --git a/packages/markers-plugin/src/model.ts b/packages/markers-plugin/src/model.ts index e869f8561..2ca8a9d0f 100644 --- a/packages/markers-plugin/src/model.ts +++ b/packages/markers-plugin/src/model.ts @@ -64,12 +64,18 @@ export type MarkerConfig = { * Array of points defining the polygon in spherical coordinates */ // eslint-disable-next-line @typescript-eslint/array-type - polygon?: [number, number][] | [string, string][] | number[] | string[]; + polygon?: + | [number, number][] + | [number, number][][] + | [string, string][] + | [string, string][][]; /** * Array of points defining the polygon in pixel coordinates on the panorama image */ // eslint-disable-next-line @typescript-eslint/array-type - polygonPixels?: [number, number][] | number[]; + polygonPixels?: + | [number, number][] + | [number, number][][]; /** * Array of points defining the polyline in spherical coordinates */