ebta-workflow-prod 4.1.2 → 4.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/activitypack.json +1 -1
- package/build/main.js +6 -6
- package/package.json +3 -2
- package/src/activities/SimplifyGeometry.ts +239 -0
- package/src/elements/AttributeFilter.tsx +5 -5
- package/src/elements/CsColorPicker.tsx +5 -2
- package/src/elements/PanovideoPlayer.tsx +225 -0
- package/src/elements/Timeline.tsx +25 -22
- package/src/elements/modules/AttributeFilterEasyComp.tsx +12 -2
- package/src/index.ts +4 -0
- package/src/styles/mantine-scoped.css +3 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ebta-workflow-prod",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "4.1.
|
|
4
|
+
"version": "4.1.4",
|
|
5
5
|
"description": "",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"author": "",
|
|
@@ -32,8 +32,8 @@
|
|
|
32
32
|
"@fseehawer/react-circular-slider": "^3.2.7",
|
|
33
33
|
"@mantine/core": "^7.16.0",
|
|
34
34
|
"@mantine/hooks": "^9.5.1",
|
|
35
|
-
"@mui/icons-material": "^5.17.1",
|
|
36
35
|
"@mui/base": "5.0.0-beta.70",
|
|
36
|
+
"@mui/icons-material": "^5.17.1",
|
|
37
37
|
"@mui/material": "^5.17.1",
|
|
38
38
|
"@mui/x-charts": "^7.6.2",
|
|
39
39
|
"@turf/turf": "^6.5.0",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"axios": "^1.7.7",
|
|
45
45
|
"dayjs": "^1.11.23",
|
|
46
46
|
"html2canvas": "^1.4.1",
|
|
47
|
+
"marzipano": "^0.10.2",
|
|
47
48
|
"pannellum-react-update": "^1.0.0",
|
|
48
49
|
"pdf-lib": "^1.17.1",
|
|
49
50
|
"qrcode": "^1.5.4",
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import type { IActivityHandler } from "@vertigis/workflow";
|
|
2
|
+
|
|
3
|
+
import Polygon from "@arcgis/core/geometry/Polygon";
|
|
4
|
+
import SpatialReference from "@arcgis/core/geometry/SpatialReference";
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/** An interface that defines the inputs of the activity. */
|
|
8
|
+
interface SimplifyGeometryInputs {
|
|
9
|
+
/**
|
|
10
|
+
* @displayName Geometry
|
|
11
|
+
* @description The geometry to be simplified.
|
|
12
|
+
* @required
|
|
13
|
+
*/
|
|
14
|
+
geometry: Polygon;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @displayName Vertex Limit
|
|
18
|
+
* @description The maximum number of vertices allowed in the simplified geometry.
|
|
19
|
+
*/
|
|
20
|
+
vertlimit?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** An interface that defines the outputs of the activity. */
|
|
24
|
+
interface SimplifyGeometryOutputs {
|
|
25
|
+
/**
|
|
26
|
+
* @description The result of the activity.
|
|
27
|
+
*/
|
|
28
|
+
result: Polygon;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @displayName SimplifyGeometry
|
|
33
|
+
* @category Custom Activities
|
|
34
|
+
* @description Simplify geometry - covering the whole geometry and increase minimum extra area, under required vertex amount.
|
|
35
|
+
*/
|
|
36
|
+
export default class SimplifyGeometryActivity implements IActivityHandler {
|
|
37
|
+
/** Perform the execution logic of the activity. */
|
|
38
|
+
|
|
39
|
+
execute(inputs: SimplifyGeometryInputs): SimplifyGeometryOutputs {
|
|
40
|
+
|
|
41
|
+
// Clone the whole result so we never touch the original $permitFeature.result
|
|
42
|
+
let simplifyresult = inputs.geometry;
|
|
43
|
+
let limit = inputs.vertlimit || 30;
|
|
44
|
+
|
|
45
|
+
let ring = simplifyresult.rings[0];
|
|
46
|
+
|
|
47
|
+
if (ring.length > limit) {
|
|
48
|
+
ring = coveringSimplify(ring, limit);
|
|
49
|
+
|
|
50
|
+
simplifyresult.rings[0] = ring;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { result: simplifyresult };
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
// ---- geometry helpers ----
|
|
59
|
+
function cross(o, a, b) {
|
|
60
|
+
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
|
|
61
|
+
}
|
|
62
|
+
function triangleArea(a, b, c) {
|
|
63
|
+
return Math.abs(cross(a, b, c)) / 2;
|
|
64
|
+
}
|
|
65
|
+
function signedArea(points) {
|
|
66
|
+
var area = 0, n = points.length;
|
|
67
|
+
for (var i = 0; i < n; i++) {
|
|
68
|
+
var p1 = points[i], p2 = points[(i + 1) % n];
|
|
69
|
+
area += p1[0] * p2[1] - p2[0] * p1[1];
|
|
70
|
+
}
|
|
71
|
+
return area / 2;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// orientation: >0 CCW turn, <0 CW turn, 0 collinear
|
|
75
|
+
function orient(a, b, c) {
|
|
76
|
+
var v = (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
|
|
77
|
+
if (v > 1e-9) return 1;
|
|
78
|
+
if (v < -1e-9) return -1;
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// true only for a PROPER crossing (interior intersection), not shared endpoints/touching
|
|
83
|
+
function properlyIntersects(p1, p2, p3, p4) {
|
|
84
|
+
var d1 = orient(p3, p4, p1);
|
|
85
|
+
var d2 = orient(p3, p4, p2);
|
|
86
|
+
var d3 = orient(p1, p2, p3);
|
|
87
|
+
var d4 = orient(p1, p2, p4);
|
|
88
|
+
return (d1 !== d2 && d1 !== 0 && d2 !== 0) && (d3 !== d4 && d3 !== 0 && d4 !== 0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// does chording out vertex i (prev->next) cross any OTHER edge of
|
|
92
|
+
// the polygon? Required check — without it, concave removal can
|
|
93
|
+
// fold the polygon over itself into a self-intersecting bowtie on
|
|
94
|
+
// complex shapes (forks, interchanges, multiple concavities).
|
|
95
|
+
function chordIsSafe(pts, i) {
|
|
96
|
+
var n = pts.length;
|
|
97
|
+
var prev = pts[(i - 1 + n) % n];
|
|
98
|
+
var next = pts[(i + 1) % n];
|
|
99
|
+
|
|
100
|
+
for (var k = 0; k < n; k++) {
|
|
101
|
+
var a = pts[k];
|
|
102
|
+
var b = pts[(k + 1) % n];
|
|
103
|
+
if (k === (i - 1 + n) % n || k === i) continue; // the two edges being replaced
|
|
104
|
+
if (properlyIntersects(prev, next, a, b)) return false;
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Remove reflex (concave) vertices, cheapest-area first, but only
|
|
110
|
+
// when the resulting chord doesn't self-intersect the polygon.
|
|
111
|
+
// Chording a safe concave notch always ADDS area outside the
|
|
112
|
+
// original shape, so coverage is preserved automatically.
|
|
113
|
+
function removeReflexVertices(points, maxVertices) {
|
|
114
|
+
var pts = points.slice();
|
|
115
|
+
var orientSign = signedArea(pts) >= 0 ? 1 : -1;
|
|
116
|
+
|
|
117
|
+
var progress = true;
|
|
118
|
+
while (pts.length > maxVertices && progress) {
|
|
119
|
+
progress = false;
|
|
120
|
+
var n = pts.length;
|
|
121
|
+
var candidates = [];
|
|
122
|
+
|
|
123
|
+
for (var i = 0; i < n; i++) {
|
|
124
|
+
var prev = pts[(i - 1 + n) % n];
|
|
125
|
+
var curr = pts[i];
|
|
126
|
+
var next = pts[(i + 1) % n];
|
|
127
|
+
var c = cross(prev, curr, next);
|
|
128
|
+
var isReflexOrFlat = c * orientSign <= 1e-9;
|
|
129
|
+
if (isReflexOrFlat) {
|
|
130
|
+
candidates.push({ idx: i, area: triangleArea(prev, curr, next) });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
candidates.sort(function (a, b) { return a.area - b.area; });
|
|
135
|
+
|
|
136
|
+
for (var ci = 0; ci < candidates.length; ci++) {
|
|
137
|
+
var idx = candidates[ci].idx;
|
|
138
|
+
if (chordIsSafe(pts, idx)) {
|
|
139
|
+
pts.splice(idx, 1);
|
|
140
|
+
progress = true;
|
|
141
|
+
break; // recompute fresh each removal since indices shift
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return pts;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Infinite-line intersection of (p1,p2) and (p3,p4).
|
|
150
|
+
function lineIntersection(p1, p2, p3, p4) {
|
|
151
|
+
var x1 = p1[0], y1 = p1[1], x2 = p2[0], y2 = p2[1];
|
|
152
|
+
var x3 = p3[0], y3 = p3[1], x4 = p4[0], y4 = p4[1];
|
|
153
|
+
var denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
|
|
154
|
+
if (Math.abs(denom) < 1e-9) return [(x2 + x3) / 2, (y2 + y3) / 2];
|
|
155
|
+
var pxNum = (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4);
|
|
156
|
+
var pyNum = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4);
|
|
157
|
+
return [pxNum / denom, pyNum / denom];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Standard monotone-chain convex hull — guaranteed simple and
|
|
161
|
+
// guaranteed to cover any input point set, regardless of how tangled
|
|
162
|
+
// the remaining shape is. Used as a safe fallback.
|
|
163
|
+
function convexHull(points) {
|
|
164
|
+
var sorted = points.slice().sort(function (a, b) { return a[0] === b[0] ? a[1] - b[1] : a[0] - b[0]; });
|
|
165
|
+
var pts = [];
|
|
166
|
+
for (var i = 0; i < sorted.length; i++) {
|
|
167
|
+
var p = sorted[i], prev = pts[pts.length - 1];
|
|
168
|
+
if (!prev || prev[0] !== p[0] || prev[1] !== p[1]) pts.push(p);
|
|
169
|
+
}
|
|
170
|
+
var n = pts.length;
|
|
171
|
+
if (n < 3) return pts.slice();
|
|
172
|
+
var lower = [];
|
|
173
|
+
for (var i2 = 0; i2 < n; i2++) {
|
|
174
|
+
while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], pts[i2]) <= 0) lower.pop();
|
|
175
|
+
lower.push(pts[i2]);
|
|
176
|
+
}
|
|
177
|
+
var upper = [];
|
|
178
|
+
for (var i3 = n - 1; i3 >= 0; i3--) {
|
|
179
|
+
while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], pts[i3]) <= 0) upper.pop();
|
|
180
|
+
upper.push(pts[i3]);
|
|
181
|
+
}
|
|
182
|
+
lower.pop(); upper.pop();
|
|
183
|
+
return lower.concat(upper);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Reduce an already-convex polygon to maxVertices by selecting
|
|
187
|
+
// maxVertices of its edges (evenly spaced) as infinite support lines
|
|
188
|
+
// and intersecting consecutive ones. Always contains the input.
|
|
189
|
+
function reduceConvexBySupportLines(points, maxVertices) {
|
|
190
|
+
var n = points.length;
|
|
191
|
+
if (n <= maxVertices) return points.slice();
|
|
192
|
+
var chosen = [];
|
|
193
|
+
for (var k = 0; k < maxVertices; k++) chosen.push(Math.floor((k * n) / maxVertices));
|
|
194
|
+
var newPts = [];
|
|
195
|
+
for (var k2 = 0; k2 < maxVertices; k2++) {
|
|
196
|
+
var iPrevEdge = chosen[(k2 - 1 + maxVertices) % maxVertices];
|
|
197
|
+
var iCurrEdge = chosen[k2];
|
|
198
|
+
var a1 = points[iPrevEdge], a2 = points[(iPrevEdge + 1) % n];
|
|
199
|
+
var b1 = points[iCurrEdge], b2 = points[(iCurrEdge + 1) % n];
|
|
200
|
+
newPts.push(lineIntersection(a1, a2, b1, b2));
|
|
201
|
+
}
|
|
202
|
+
return newPts;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Simplify a ring to at most maxVertices points, guaranteeing the
|
|
206
|
+
// result is both SIMPLE (no self-intersections) and always covers
|
|
207
|
+
// (contains) the original shape.
|
|
208
|
+
function coveringSimplify(ring, maxVertices) {
|
|
209
|
+
var first = ring[0], last = ring[ring.length - 1];
|
|
210
|
+
var isClosed = first[0] === last[0] && first[1] === last[1];
|
|
211
|
+
var open = isClosed ? ring.slice(0, ring.length - 1) : ring.slice();
|
|
212
|
+
var budgetOpen = isClosed ? maxVertices - 1 : maxVertices;
|
|
213
|
+
|
|
214
|
+
if (open.length <= budgetOpen) return ring.slice();
|
|
215
|
+
|
|
216
|
+
var afterReflex = removeReflexVertices(open, budgetOpen);
|
|
217
|
+
|
|
218
|
+
var result;
|
|
219
|
+
if (afterReflex.length > budgetOpen) {
|
|
220
|
+
// Couldn't safely get all the way there via reflex removal
|
|
221
|
+
// (or the remainder is genuinely convex) — fall back to the
|
|
222
|
+
// convex hull, which is always simple and always covers.
|
|
223
|
+
var hull = convexHull(afterReflex);
|
|
224
|
+
result = hull.length > budgetOpen
|
|
225
|
+
? reduceConvexBySupportLines(hull, budgetOpen)
|
|
226
|
+
: hull;
|
|
227
|
+
} else {
|
|
228
|
+
result = afterReflex;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return isClosed ? result.concat([result[0]]) : result;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
result: simplifyresult
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -1310,7 +1310,7 @@ function AttributeFilter(props: AttributeFilterProps): React.ReactElement {
|
|
|
1310
1310
|
setProperty("filteredValues" as any, filterrangeResult)
|
|
1311
1311
|
};
|
|
1312
1312
|
|
|
1313
|
-
const normalizeNumber = (value: number) => Number(value.toFixed(
|
|
1313
|
+
const normalizeNumber = (value: number) => Number(value.toFixed(1));
|
|
1314
1314
|
|
|
1315
1315
|
const maxStringLength = 28
|
|
1316
1316
|
function itemText(item: fieldValue, type?: string) {
|
|
@@ -1419,8 +1419,8 @@ function AttributeFilter(props: AttributeFilterProps): React.ReactElement {
|
|
|
1419
1419
|
ISnumberAttribute && (attribute.uniqValue.length > 6) ?
|
|
1420
1420
|
//Display String search or number Range , don't display if value amount is limited
|
|
1421
1421
|
<Box display="flex" alignItems="center">
|
|
1422
|
-
<CustomNumberInput aria-label="min value input" placeholder={`Min (${attributeMin} ~ ${attributeMax})`} min={attributeMin - 1} max={attributeMax + 1}
|
|
1423
|
-
value={minValue == -Infinity ? undefined : minValue} step={0.1}
|
|
1422
|
+
<CustomNumberInput aria-label="min value input" placeholder={`Min (${normalizeNumber(attributeMin)} ~ ${normalizeNumber(attributeMax)})`} min={attributeMin - 1} max={attributeMax + 1}
|
|
1423
|
+
value={minValue == -Infinity ? undefined : normalizeNumber(minValue)} step={0.1}
|
|
1424
1424
|
onChange={(event, val) => {
|
|
1425
1425
|
const normalizedValue = typeof val === 'number' ? normalizeNumber(val) : val;
|
|
1426
1426
|
if (((normalizedValue || normalizedValue == 0) && (normalizedValue <= maxValue))) { setProperty("minValue" as any, normalizedValue) };
|
|
@@ -1437,8 +1437,8 @@ function AttributeFilter(props: AttributeFilterProps): React.ReactElement {
|
|
|
1437
1437
|
}
|
|
1438
1438
|
}}
|
|
1439
1439
|
/>
|
|
1440
|
-
<CustomNumberInput aria-label="max value input" placeholder={`Max (${attributeMin} ~ ${attributeMax})`} min={attributeMin - 1} max={attributeMax + 1}
|
|
1441
|
-
value={maxValue == Infinity ? undefined : maxValue} step={0.1}
|
|
1440
|
+
<CustomNumberInput aria-label="max value input" placeholder={`Max (${normalizeNumber(attributeMin)} ~ ${normalizeNumber(attributeMax)})`} min={attributeMin - 1} max={attributeMax + 1}
|
|
1441
|
+
value={maxValue == Infinity ? undefined : normalizeNumber(maxValue)} step={0.1}
|
|
1442
1442
|
onChange={(_event, val) => {
|
|
1443
1443
|
const normalizedValue = typeof val === 'number' ? normalizeNumber(val) : val;
|
|
1444
1444
|
if ((normalizedValue || normalizedValue == 0) && (normalizedValue >= minValue)) { setProperty("maxValue" as any, normalizedValue) };
|
|
@@ -4,8 +4,9 @@ import { FormElementProps, FormElementRegistration } from "@vertigis/workflow";
|
|
|
4
4
|
import { ColorInput, ColorPicker } from '@mantine/core';
|
|
5
5
|
type ColorFormat = "hex" | "rgba" | "rgb" | "hsl" | "hsla";
|
|
6
6
|
|
|
7
|
-
import "@mantine/core/styles.css";
|
|
8
|
-
|
|
7
|
+
//import "@mantine/core/styles.css";
|
|
8
|
+
//Causing shrink
|
|
9
|
+
import "../styles/mantine-scoped.css";
|
|
9
10
|
import Color from "@arcgis/core/Color";
|
|
10
11
|
|
|
11
12
|
|
|
@@ -79,6 +80,7 @@ function CsColorPickerV2(props: CsColorPickerV2Props): React.ReactElement {
|
|
|
79
80
|
};
|
|
80
81
|
|
|
81
82
|
return (
|
|
83
|
+
<div className="vertigis-mantine-scope">
|
|
82
84
|
<MantineProvider withStaticClasses withGlobalClasses classNamesPrefix="ebta-mantime">
|
|
83
85
|
{componentType === "ColorInput" ? (
|
|
84
86
|
<ColorInput
|
|
@@ -105,6 +107,7 @@ function CsColorPickerV2(props: CsColorPickerV2Props): React.ReactElement {
|
|
|
105
107
|
/>
|
|
106
108
|
)}
|
|
107
109
|
</MantineProvider>
|
|
110
|
+
</div>
|
|
108
111
|
);
|
|
109
112
|
}
|
|
110
113
|
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { FormElementProps, FormElementRegistration } from "@vertigis/workflow";
|
|
3
|
+
import Marzipano from "marzipano";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The generic type argument provided to `FormElementProps<T>` controls the type
|
|
8
|
+
* of `props.value` and `props.setValue(value)`. If your element doesn't need a
|
|
9
|
+
* `value`, you can omit the type argument.
|
|
10
|
+
*
|
|
11
|
+
* You can also declare additional public properties of your element by adding
|
|
12
|
+
* properties to this interface. The properties will be managed by the Workflow
|
|
13
|
+
* runtime, and passed in as `props`. You can update the properties by using
|
|
14
|
+
* `props.setProperty(key, value)`.
|
|
15
|
+
*/
|
|
16
|
+
interface PanovideoPlayerProps extends FormElementProps<string> {}
|
|
17
|
+
|
|
18
|
+
class VideoAsset extends (Marzipano as any).util.Emitter {
|
|
19
|
+
private _element: HTMLVideoElement;
|
|
20
|
+
|
|
21
|
+
constructor(videoElement: HTMLVideoElement) {
|
|
22
|
+
super();
|
|
23
|
+
this._element = videoElement;
|
|
24
|
+
// Marzipano polls isDynamic()/emits('change') to know when to redraw.
|
|
25
|
+
this._element.addEventListener("timeupdate", () => {
|
|
26
|
+
this.emit("change");
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
element(): HTMLVideoElement {
|
|
31
|
+
return this._element;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
width(): number {
|
|
35
|
+
return this._element.videoWidth;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
height(): number {
|
|
39
|
+
return this._element.videoHeight;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
isDynamic(): boolean {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A Workflow element built using React.
|
|
49
|
+
* @displayName PanovideoPlayer
|
|
50
|
+
* @description Marzipano library panoramic video player
|
|
51
|
+
* @param props The props that will be provided by the Workflow runtime.
|
|
52
|
+
*/
|
|
53
|
+
function PanovideoPlayer(props: PanovideoPlayerProps): React.ReactElement {
|
|
54
|
+
const { setValue, value } = props;
|
|
55
|
+
|
|
56
|
+
const containerRef = React.useRef<HTMLDivElement | null>(null);
|
|
57
|
+
const videoElRef = React.useRef<HTMLVideoElement | null>(null);
|
|
58
|
+
const viewerRef = React.useRef<any>(null);
|
|
59
|
+
const [isPlaying, setIsPlaying] = React.useState(false);
|
|
60
|
+
const [error, setError] = React.useState<string | null>(null);
|
|
61
|
+
|
|
62
|
+
// (Re)builds the Marzipano viewer whenever the video URL (props.value) changes.
|
|
63
|
+
React.useEffect(() => {
|
|
64
|
+
if (!value || !containerRef.current) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
setError(null);
|
|
69
|
+
|
|
70
|
+
// Create a hidden video element to drive the panorama. We don't
|
|
71
|
+
// attach it to the DOM directly - Marzipano reads frames from it
|
|
72
|
+
// and draws them into its own WebGL canvas.
|
|
73
|
+
const videoEl = document.createElement("video");
|
|
74
|
+
videoEl.crossOrigin = "anonymous";
|
|
75
|
+
videoEl.loop = true;
|
|
76
|
+
videoEl.muted = true; // required for autoplay in most browsers
|
|
77
|
+
videoEl.playsInline = true;
|
|
78
|
+
videoEl.src = value;
|
|
79
|
+
videoElRef.current = videoEl;
|
|
80
|
+
|
|
81
|
+
let viewer: any;
|
|
82
|
+
let cancelled = false;
|
|
83
|
+
|
|
84
|
+
const onLoadedMetadata = () => {
|
|
85
|
+
if (cancelled || !containerRef.current) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
viewer = new (Marzipano as any).Viewer(containerRef.current);
|
|
91
|
+
viewerRef.current = viewer;
|
|
92
|
+
|
|
93
|
+
const asset = new VideoAsset(videoEl);
|
|
94
|
+
const source = (Marzipano as any).SingleAssetSource.fromAsset(asset);
|
|
95
|
+
|
|
96
|
+
const geometry = new (Marzipano as any).EquirectGeometry([
|
|
97
|
+
{ width: videoEl.videoWidth || 1920 },
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
const limiter = (Marzipano as any).RectilinearView.limit.traditional(
|
|
101
|
+
1024,
|
|
102
|
+
(100 * Math.PI) / 180
|
|
103
|
+
);
|
|
104
|
+
const view = new (Marzipano as any).RectilinearView(
|
|
105
|
+
{ yaw: 0, pitch: 0, fov: Math.PI / 2 },
|
|
106
|
+
limiter
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const scene = viewer.createScene({
|
|
110
|
+
source,
|
|
111
|
+
geometry,
|
|
112
|
+
view,
|
|
113
|
+
pinFirstLevel: true,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
scene.switchTo();
|
|
117
|
+
|
|
118
|
+
videoEl
|
|
119
|
+
.play()
|
|
120
|
+
.then(() => setIsPlaying(true))
|
|
121
|
+
.catch(() => {
|
|
122
|
+
// Autoplay was blocked - user needs to tap play.
|
|
123
|
+
setIsPlaying(false);
|
|
124
|
+
});
|
|
125
|
+
} catch (e) {
|
|
126
|
+
setError(
|
|
127
|
+
e instanceof Error ? e.message : "Failed to initialize panoramic video."
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const onVideoError = () => {
|
|
133
|
+
if (!cancelled) {
|
|
134
|
+
setError("Unable to load video from the provided URL.");
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
videoEl.addEventListener("loadedmetadata", onLoadedMetadata);
|
|
139
|
+
videoEl.addEventListener("error", onVideoError);
|
|
140
|
+
|
|
141
|
+
return () => {
|
|
142
|
+
cancelled = true;
|
|
143
|
+
videoEl.removeEventListener("loadedmetadata", onLoadedMetadata);
|
|
144
|
+
videoEl.removeEventListener("error", onVideoError);
|
|
145
|
+
videoEl.pause();
|
|
146
|
+
videoEl.removeAttribute("src");
|
|
147
|
+
videoEl.load();
|
|
148
|
+
|
|
149
|
+
if (viewerRef.current) {
|
|
150
|
+
try {
|
|
151
|
+
// Not all Marzipano versions expose destroy(); guard defensively.
|
|
152
|
+
viewerRef.current.destroy?.();
|
|
153
|
+
} catch {
|
|
154
|
+
// no-op
|
|
155
|
+
}
|
|
156
|
+
viewerRef.current = null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (containerRef.current) {
|
|
160
|
+
containerRef.current.innerHTML = "";
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}, [value]);
|
|
164
|
+
|
|
165
|
+
const togglePlayback = () => {
|
|
166
|
+
const videoEl = videoElRef.current;
|
|
167
|
+
if (!videoEl) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (videoEl.paused) {
|
|
171
|
+
videoEl.play().then(() => setIsPlaying(true));
|
|
172
|
+
} else {
|
|
173
|
+
videoEl.pause();
|
|
174
|
+
setIsPlaying(false);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
|
|
180
|
+
<input
|
|
181
|
+
type="text"
|
|
182
|
+
placeholder="Enter 360° equirectangular video URL"
|
|
183
|
+
onChange={(event) => setValue(event.currentTarget.value)}
|
|
184
|
+
value={value ?? ""}
|
|
185
|
+
style={{ width: "100%" }}
|
|
186
|
+
/>
|
|
187
|
+
|
|
188
|
+
{error && (
|
|
189
|
+
<div style={{ color: "red", fontSize: 12 }}>{error}</div>
|
|
190
|
+
)}
|
|
191
|
+
|
|
192
|
+
{value && (
|
|
193
|
+
<div style={{ position: "relative" }}>
|
|
194
|
+
<div
|
|
195
|
+
ref={containerRef}
|
|
196
|
+
style={{
|
|
197
|
+
width: "100%",
|
|
198
|
+
height: 400,
|
|
199
|
+
background: "#000",
|
|
200
|
+
}}
|
|
201
|
+
/>
|
|
202
|
+
<button
|
|
203
|
+
type="button"
|
|
204
|
+
onClick={togglePlayback}
|
|
205
|
+
style={{
|
|
206
|
+
position: "absolute",
|
|
207
|
+
bottom: 8,
|
|
208
|
+
left: 8,
|
|
209
|
+
}}
|
|
210
|
+
>
|
|
211
|
+
{isPlaying ? "Pause" : "Play"}
|
|
212
|
+
</button>
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const PanovideoPlayerElementRegistration: FormElementRegistration<PanovideoPlayerProps> = {
|
|
220
|
+
component: PanovideoPlayer,
|
|
221
|
+
getInitialProperties: () => ({ value: "" }),
|
|
222
|
+
id: "PanovideoPlayer",
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export default PanovideoPlayerElementRegistration;
|
|
@@ -2,7 +2,7 @@ import * as React from "react";
|
|
|
2
2
|
import { FormElementProps, FormElementRegistration } from "@vertigis/workflow";
|
|
3
3
|
import { Timeline as MainTimeLine, Text } from '@mantine/core';
|
|
4
4
|
import { MantineProvider } from '@mantine/core';
|
|
5
|
-
|
|
5
|
+
import "../styles/mantine-scoped.css";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* The generic type argument provided to `FormElementProps<T>` controls the type
|
|
@@ -38,30 +38,33 @@ interface TimelineProps extends FormElementProps<string> {
|
|
|
38
38
|
* @param props The props that will be provided by the Workflow runtime.
|
|
39
39
|
*/
|
|
40
40
|
function Timeline(props: TimelineProps): React.ReactElement {
|
|
41
|
-
const { itemDetails,iconSize,lineWidth,active,color,reverseActive } = props;
|
|
41
|
+
const { itemDetails, iconSize, lineWidth, active, color, reverseActive } = props;
|
|
42
42
|
iconSize * 0.6
|
|
43
43
|
return (
|
|
44
|
-
<
|
|
45
|
-
<
|
|
44
|
+
<div className="vertigis-mantine-scope">
|
|
45
|
+
<MantineProvider>
|
|
46
|
+
<MainTimeLine active={active} bulletSize={iconSize} lineWidth={lineWidth} color={color} reverseActive={reverseActive}>
|
|
47
|
+
|
|
48
|
+
{itemDetails.map((item, index) => {
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
|
|
52
|
+
<MainTimeLine.Item
|
|
53
|
+
key={index}
|
|
54
|
+
title={item.title}
|
|
55
|
+
lineVariant={item.lineVariant}
|
|
56
|
+
>
|
|
57
|
+
|
|
58
|
+
{item.description ? <Text c="dimmed" size="sm">{item.description}</Text> : undefined}
|
|
59
|
+
{item.time ? <Text size="xs" mt={4}>{item.time}</Text> : undefined}
|
|
46
60
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
<MainTimeLine.Item
|
|
52
|
-
key={index}
|
|
53
|
-
title={item.title}
|
|
54
|
-
lineVariant={item.lineVariant}
|
|
55
|
-
>
|
|
56
|
-
|
|
57
|
-
{item.description ? <Text c="dimmed" size="sm">{item.description}</Text> : undefined}
|
|
58
|
-
{item.time ? <Text size="xs" mt={4}>{item.time}</Text> : undefined}
|
|
59
|
-
|
|
60
|
-
</MainTimeLine.Item>
|
|
61
|
-
)})}
|
|
62
|
-
</MainTimeLine>
|
|
61
|
+
</MainTimeLine.Item>
|
|
62
|
+
)
|
|
63
|
+
})}
|
|
64
|
+
</MainTimeLine>
|
|
63
65
|
|
|
64
|
-
|
|
66
|
+
</MantineProvider>
|
|
67
|
+
</div>
|
|
65
68
|
);
|
|
66
69
|
}
|
|
67
70
|
|
|
@@ -72,7 +75,7 @@ const TimelineElementRegistration: FormElementRegistration<TimelineProps> = {
|
|
|
72
75
|
lineWidth: 1,
|
|
73
76
|
iconSize: 40,
|
|
74
77
|
active: 2,
|
|
75
|
-
color:'var(--buttonIcon)',
|
|
78
|
+
color: 'var(--buttonIcon)',
|
|
76
79
|
reverseActive: true,
|
|
77
80
|
itemDetails: [
|
|
78
81
|
{
|
|
@@ -114,6 +114,16 @@ const StyledButton = styled('button')(
|
|
|
114
114
|
`,
|
|
115
115
|
)
|
|
116
116
|
|
|
117
|
+
const StyledIncrementButton = styled(StyledButton)({
|
|
118
|
+
gridColumn: 2,
|
|
119
|
+
gridRow: 1,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const StyledDecrementButton = styled(StyledButton)({
|
|
123
|
+
gridColumn: 2,
|
|
124
|
+
gridRow: 2,
|
|
125
|
+
});
|
|
126
|
+
|
|
117
127
|
|
|
118
128
|
|
|
119
129
|
|
|
@@ -151,8 +161,8 @@ const CustomNumberInput = React.forwardRef(function CustomNumberInput(
|
|
|
151
161
|
slots={{
|
|
152
162
|
root: StyledInputRoot,
|
|
153
163
|
input: StyledInputElement,
|
|
154
|
-
incrementButton:
|
|
155
|
-
decrementButton:
|
|
164
|
+
incrementButton: StyledIncrementButton,
|
|
165
|
+
decrementButton: StyledDecrementButton,
|
|
156
166
|
}}
|
|
157
167
|
slotProps={{
|
|
158
168
|
incrementButton: {
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ export { default as HTMLextractorActivity } from "./activities/HTMLextractor";
|
|
|
18
18
|
export { default as AttributeFilterElementRegistration } from "./elements/AttributeFilter"
|
|
19
19
|
export { default as ItemSearcher_V2DEVRegistration } from "./elements/ItemSearcher_v2";
|
|
20
20
|
export { default as Viewer360Registration } from "./elements/Viewer360";
|
|
21
|
+
export { default as PanovideoPlayerRegistration} from "./elements/PanovideoPlayer";
|
|
21
22
|
export { default as TableRadioGroupElementRegistration } from "./elements/TableRadioGoup";
|
|
22
23
|
export { MuiSingleToggleButtonGroupElementRegistration, MuiMultipleToggleButtonGroupElementRegistration } from "./elements/ToggleButtonGroup";
|
|
23
24
|
|
|
@@ -40,3 +41,6 @@ export { default as DrapGeometryActivity } from "./activities/DrapGeometry";
|
|
|
40
41
|
export { default as SampleActivity } from "./activities/Sample";
|
|
41
42
|
export { default as SampleInputRegistration } from "./elements/SampleInput";
|
|
42
43
|
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
export { default as SimplifyGeometryActivity } from "./activities/SimplifyGeometry";
|