@terraware/web-components 5.0.3 → 5.0.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/components/VirtualWalkthrough/AnnotationPanel.d.ts.map +1 -1
- package/components/VirtualWalkthrough/AnnotationPanel.js +24 -10
- package/components/VirtualWalkthrough/vr-annotation-panel-layout.d.ts +65 -0
- package/components/VirtualWalkthrough/vr-annotation-panel-layout.d.ts.map +1 -1
- package/components/VirtualWalkthrough/vr-annotation-panel-layout.js +99 -1
- package/components/VirtualWalkthrough/vr-annotation-panel.d.ts +40 -7
- package/components/VirtualWalkthrough/vr-annotation-panel.d.ts.map +1 -1
- package/components/VirtualWalkthrough/vr-annotation-panel.js +265 -104
- package/components/VirtualWalkthrough/vr-annotation-scroll.d.ts +23 -0
- package/components/VirtualWalkthrough/vr-annotation-scroll.d.ts.map +1 -0
- package/components/VirtualWalkthrough/vr-annotation-scroll.js +33 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnnotationPanel.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/AnnotationPanel.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,UAAU,oBAAoB;IAC5B,UAAU,EAAE,eAAe,GAAG,IAAI,CAAC;IAInC,eAAe,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACjE,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;
|
|
1
|
+
{"version":3,"file":"AnnotationPanel.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/AnnotationPanel.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEpD,UAAU,oBAAoB;IAC5B,UAAU,EAAE,eAAe,GAAG,IAAI,CAAC;IAInC,eAAe,CAAC,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACjE,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAiED,QAAA,MAAM,eAAe,GAAI,0CAA0C,oBAAoB,mDAsItF,CAAC;AAEF,eAAe,eAAe,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useRef, useState, useEffect, useLayoutEffect } from 'react';
|
|
2
|
-
import { useTheme } from '@mui/material';
|
|
2
|
+
import { useTheme, Box } from '@mui/material';
|
|
3
3
|
import PhotosCarousel from '../PhotosCarousel/index.js';
|
|
4
4
|
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
5
5
|
|
|
@@ -36,17 +36,33 @@ const PANEL_STYLE = {
|
|
|
36
36
|
display: 'flex',
|
|
37
37
|
flexDirection: 'column'
|
|
38
38
|
};
|
|
39
|
-
const
|
|
39
|
+
const CAROUSEL_SX = {
|
|
40
40
|
width: 'min(60vw, 640px)',
|
|
41
41
|
borderRadius: '12px 12px 0 0',
|
|
42
42
|
overflow: 'hidden',
|
|
43
|
-
flexShrink: 0
|
|
43
|
+
flexShrink: 0,
|
|
44
|
+
'& .photos-carousel-image': {
|
|
45
|
+
height: 'min(400px, 40vh)'
|
|
46
|
+
},
|
|
47
|
+
'& .photos-carousel-container': {
|
|
48
|
+
minHeight: 'min(200px, 20vh)'
|
|
49
|
+
}
|
|
44
50
|
};
|
|
45
51
|
const TEXT_BLOCK_STYLE = {
|
|
46
52
|
padding: '24px',
|
|
47
53
|
display: 'flex',
|
|
48
54
|
flexDirection: 'column',
|
|
49
|
-
gap: 8
|
|
55
|
+
gap: 8,
|
|
56
|
+
minHeight: 0
|
|
57
|
+
};
|
|
58
|
+
const BODY_STYLE = {
|
|
59
|
+
fontSize: 16,
|
|
60
|
+
lineHeight: '24px',
|
|
61
|
+
margin: 0,
|
|
62
|
+
minHeight: 0,
|
|
63
|
+
overflowY: 'auto',
|
|
64
|
+
overscrollBehavior: 'contain',
|
|
65
|
+
scrollbarWidth: 'thin'
|
|
50
66
|
};
|
|
51
67
|
const AnnotationPanel = ({
|
|
52
68
|
annotation,
|
|
@@ -149,8 +165,8 @@ const AnnotationPanel = ({
|
|
|
149
165
|
ref: panelRef,
|
|
150
166
|
"data-testid": "annotation-panel",
|
|
151
167
|
style: panelStyle,
|
|
152
|
-
children: [annotation.imageUrls && annotation.imageUrls.length > 0 && /*#__PURE__*/jsx(
|
|
153
|
-
|
|
168
|
+
children: [annotation.imageUrls && annotation.imageUrls.length > 0 && /*#__PURE__*/jsx(Box, {
|
|
169
|
+
sx: CAROUSEL_SX,
|
|
154
170
|
children: /*#__PURE__*/jsx(PhotosCarousel, {
|
|
155
171
|
photos: annotation.imageUrls.map(url => ({
|
|
156
172
|
url,
|
|
@@ -187,10 +203,8 @@ const AnnotationPanel = ({
|
|
|
187
203
|
}), annotation.bodyText && /*#__PURE__*/jsx("p", {
|
|
188
204
|
"data-testid": "annotation-panel-description",
|
|
189
205
|
style: {
|
|
190
|
-
|
|
191
|
-
color: theme.palette.TwClrTxt
|
|
192
|
-
lineHeight: '24px',
|
|
193
|
-
margin: 0
|
|
206
|
+
...BODY_STYLE,
|
|
207
|
+
color: theme.palette.TwClrTxt
|
|
194
208
|
},
|
|
195
209
|
children: annotation.bodyText
|
|
196
210
|
})]
|
|
@@ -3,4 +3,69 @@
|
|
|
3
3
|
* each candidate line. A single word wider than `maxWidth` still gets its own line.
|
|
4
4
|
*/
|
|
5
5
|
export declare const wrapText: (text: string, maxWidth: number, measure: (segment: string) => number) => string[];
|
|
6
|
+
export declare const PANEL_CANVAS_WIDTH = 1024;
|
|
7
|
+
/** Tallest the panel can grow; also the canvas the content is painted into. */
|
|
8
|
+
export declare const PANEL_MAX_HEIGHT = 768;
|
|
9
|
+
/** Shortest the panel can shrink, so a bare title still reads as a panel rather than a sliver. */
|
|
10
|
+
export declare const PANEL_MIN_HEIGHT = 200;
|
|
11
|
+
export declare const PANEL_PAD_X = 48;
|
|
12
|
+
export declare const PANEL_PAD_Y = 40;
|
|
13
|
+
export declare const IMAGE_BAND_HEIGHT = 360;
|
|
14
|
+
export declare const CHIP_HEIGHT = 44;
|
|
15
|
+
export declare const TITLE_LINE_HEIGHT = 56;
|
|
16
|
+
export declare const BODY_LINE_HEIGHT = 40;
|
|
17
|
+
/**
|
|
18
|
+
* Tallest canvas the body text is rasterised into. The body scrolls as a window onto this canvas,
|
|
19
|
+
* so it bounds how much text an annotation can show — far more than the panel itself holds.
|
|
20
|
+
*/
|
|
21
|
+
export declare const TEXT_CANVAS_MAX_HEIGHT = 2048;
|
|
22
|
+
export declare const PANEL_FONTS: {
|
|
23
|
+
readonly label: "600 28px sans-serif";
|
|
24
|
+
readonly title: "700 48px sans-serif";
|
|
25
|
+
readonly body: "400 32px sans-serif";
|
|
26
|
+
};
|
|
27
|
+
export type PanelTextStyle = keyof typeof PANEL_FONTS;
|
|
28
|
+
export interface PanelLayoutInput {
|
|
29
|
+
title: string;
|
|
30
|
+
label?: string;
|
|
31
|
+
bodyText?: string;
|
|
32
|
+
imageCount: number;
|
|
33
|
+
contentWidth: number;
|
|
34
|
+
measure: (text: string, style: PanelTextStyle) => number;
|
|
35
|
+
}
|
|
36
|
+
export interface PanelLayout {
|
|
37
|
+
/** Painted height of the panel, between the min and max bounds. */
|
|
38
|
+
height: number;
|
|
39
|
+
image: {
|
|
40
|
+
y: number;
|
|
41
|
+
height: number;
|
|
42
|
+
} | null;
|
|
43
|
+
dotsY: number | null;
|
|
44
|
+
chip: {
|
|
45
|
+
y: number;
|
|
46
|
+
width: number;
|
|
47
|
+
height: number;
|
|
48
|
+
} | null;
|
|
49
|
+
title: {
|
|
50
|
+
y: number;
|
|
51
|
+
lines: string[];
|
|
52
|
+
};
|
|
53
|
+
body: {
|
|
54
|
+
/** Top of the body viewport, in panel coordinates. */
|
|
55
|
+
y: number;
|
|
56
|
+
lines: string[];
|
|
57
|
+
/** Height of all the lines together, which is also the text canvas height. */
|
|
58
|
+
contentHeight: number;
|
|
59
|
+
/** Height of the visible window onto that canvas. */
|
|
60
|
+
viewportHeight: number;
|
|
61
|
+
scrollable: boolean;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Lays the panel's blocks out top-to-bottom and reports the height they need, so the quad
|
|
66
|
+
* can be sized to the content instead of a fixed aspect. Everything above the body is a header
|
|
67
|
+
* that has to fit within `PANEL_MAX_HEIGHT`; the body instead scrolls within whatever height
|
|
68
|
+
* the header leaves it.
|
|
69
|
+
*/
|
|
70
|
+
export declare const layoutPanel: ({ title, label, bodyText, imageCount, contentWidth, measure, }: PanelLayoutInput) => PanelLayout;
|
|
6
71
|
//# sourceMappingURL=vr-annotation-panel-layout.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vr-annotation-panel-layout.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/vr-annotation-panel-layout.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,QAAQ,GAAI,MAAM,MAAM,EAAE,UAAU,MAAM,EAAE,SAAS,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,KAAG,MAAM,EAoBrG,CAAC"}
|
|
1
|
+
{"version":3,"file":"vr-annotation-panel-layout.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/vr-annotation-panel-layout.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,QAAQ,GAAI,MAAM,MAAM,EAAE,UAAU,MAAM,EAAE,SAAS,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,KAAG,MAAM,EAoBrG,CAAC;AAEF,eAAO,MAAM,kBAAkB,OAAO,CAAC;AACvC,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB,MAAM,CAAC;AACpC,kGAAkG;AAClG,eAAO,MAAM,gBAAgB,MAAM,CAAC;AAEpC,eAAO,MAAM,WAAW,KAAK,CAAC;AAC9B,eAAO,MAAM,WAAW,KAAK,CAAC;AAE9B,eAAO,MAAM,iBAAiB,MAAM,CAAC;AAKrC,eAAO,MAAM,WAAW,KAAK,CAAC;AAI9B,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAEpC,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAEnC;;;GAGG;AACH,eAAO,MAAM,sBAAsB,OAAO,CAAC;AAE3C,eAAO,MAAM,WAAW;;;;CAId,CAAC;AAEX,MAAM,MAAM,cAAc,GAAG,MAAM,OAAO,WAAW,CAAC;AAEtD,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,KAAK,MAAM,CAAC;CAC1D;AAED,MAAM,WAAW,WAAW;IAC1B,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC5C,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC1D,KAAK,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACtC,IAAI,EAAE;QACJ,sDAAsD;QACtD,CAAC,EAAE,MAAM,CAAC;QACV,KAAK,EAAE,MAAM,EAAE,CAAC;QAChB,8EAA8E;QAC9E,aAAa,EAAE,MAAM,CAAC;QACtB,qDAAqD;QACrD,cAAc,EAAE,MAAM,CAAC;QACvB,UAAU,EAAE,OAAO,CAAC;KACrB,CAAC;CACH;AASD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,gEAOzB,gBAAgB,KAAG,WA8CrB,CAAC"}
|
|
@@ -20,5 +20,103 @@ const wrapText = (text, maxWidth, measure) => {
|
|
|
20
20
|
}
|
|
21
21
|
return lines;
|
|
22
22
|
};
|
|
23
|
+
const PANEL_CANVAS_WIDTH = 1024;
|
|
24
|
+
/** Tallest the panel can grow; also the canvas the content is painted into. */
|
|
25
|
+
const PANEL_MAX_HEIGHT = 768;
|
|
26
|
+
/** Shortest the panel can shrink, so a bare title still reads as a panel rather than a sliver. */
|
|
27
|
+
const PANEL_MIN_HEIGHT = 200;
|
|
28
|
+
const PANEL_PAD_X = 48;
|
|
29
|
+
const PANEL_PAD_Y = 40;
|
|
30
|
+
const IMAGE_BAND_HEIGHT = 360;
|
|
31
|
+
const IMAGE_GAP = 16;
|
|
32
|
+
const DOTS_BLOCK_HEIGHT = 32;
|
|
33
|
+
const DOTS_CENTER_OFFSET = 4;
|
|
34
|
+
const CHIP_HEIGHT = 44;
|
|
35
|
+
const CHIP_PAD_X = 16;
|
|
36
|
+
const CHIP_GAP = 24;
|
|
37
|
+
const TITLE_LINE_HEIGHT = 56;
|
|
38
|
+
const TITLE_GAP = 16;
|
|
39
|
+
const BODY_LINE_HEIGHT = 40;
|
|
23
40
|
|
|
24
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Tallest canvas the body text is rasterised into. The body scrolls as a window onto this canvas,
|
|
43
|
+
* so it bounds how much text an annotation can show — far more than the panel itself holds.
|
|
44
|
+
*/
|
|
45
|
+
const TEXT_CANVAS_MAX_HEIGHT = 2048;
|
|
46
|
+
const PANEL_FONTS = {
|
|
47
|
+
label: '600 28px sans-serif',
|
|
48
|
+
title: '700 48px sans-serif',
|
|
49
|
+
body: '400 32px sans-serif'
|
|
50
|
+
};
|
|
51
|
+
/** Keeps the lines whose full line box fits above the bottom padding. */
|
|
52
|
+
const fitLines = (lines, y, lineHeight) => {
|
|
53
|
+
const room = Math.max(0, Math.floor((PANEL_MAX_HEIGHT - PANEL_PAD_Y - y) / lineHeight));
|
|
54
|
+
return lines.slice(0, room);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Lays the panel's blocks out top-to-bottom and reports the height they need, so the quad
|
|
59
|
+
* can be sized to the content instead of a fixed aspect. Everything above the body is a header
|
|
60
|
+
* that has to fit within `PANEL_MAX_HEIGHT`; the body instead scrolls within whatever height
|
|
61
|
+
* the header leaves it.
|
|
62
|
+
*/
|
|
63
|
+
const layoutPanel = ({
|
|
64
|
+
title,
|
|
65
|
+
label,
|
|
66
|
+
bodyText,
|
|
67
|
+
imageCount,
|
|
68
|
+
contentWidth,
|
|
69
|
+
measure
|
|
70
|
+
}) => {
|
|
71
|
+
let y = PANEL_PAD_Y;
|
|
72
|
+
const image = imageCount > 0 ? {
|
|
73
|
+
y,
|
|
74
|
+
height: IMAGE_BAND_HEIGHT
|
|
75
|
+
} : null;
|
|
76
|
+
let dotsY = null;
|
|
77
|
+
if (image) {
|
|
78
|
+
y += IMAGE_BAND_HEIGHT + IMAGE_GAP;
|
|
79
|
+
if (imageCount > 1) {
|
|
80
|
+
dotsY = y + DOTS_CENTER_OFFSET;
|
|
81
|
+
y += DOTS_BLOCK_HEIGHT;
|
|
82
|
+
}
|
|
83
|
+
y += IMAGE_GAP;
|
|
84
|
+
}
|
|
85
|
+
const chip = label ? {
|
|
86
|
+
y,
|
|
87
|
+
width: measure(label, 'label') + CHIP_PAD_X * 2,
|
|
88
|
+
height: CHIP_HEIGHT
|
|
89
|
+
} : null;
|
|
90
|
+
if (chip) {
|
|
91
|
+
y += CHIP_HEIGHT + CHIP_GAP;
|
|
92
|
+
}
|
|
93
|
+
const titleLines = wrapText(title, contentWidth, segment => measure(segment, 'title'));
|
|
94
|
+
const fittedTitle = fitLines(titleLines, y, TITLE_LINE_HEIGHT);
|
|
95
|
+
const titleY = y;
|
|
96
|
+
y += fittedTitle.length * TITLE_LINE_HEIGHT + TITLE_GAP;
|
|
97
|
+
const bodyLines = bodyText ? wrapText(bodyText, contentWidth, segment => measure(segment, 'body')) : [];
|
|
98
|
+
const fittedBody = bodyLines.slice(0, Math.floor(TEXT_CANVAS_MAX_HEIGHT / BODY_LINE_HEIGHT));
|
|
99
|
+
const bodyY = y;
|
|
100
|
+
const contentHeight = fittedBody.length * BODY_LINE_HEIGHT;
|
|
101
|
+
const viewportHeight = Math.min(contentHeight, Math.max(0, PANEL_MAX_HEIGHT - PANEL_PAD_Y - bodyY));
|
|
102
|
+
const height = Math.max(PANEL_MIN_HEIGHT, Math.min(PANEL_MAX_HEIGHT, bodyY + viewportHeight + PANEL_PAD_Y));
|
|
103
|
+
return {
|
|
104
|
+
height,
|
|
105
|
+
image,
|
|
106
|
+
dotsY,
|
|
107
|
+
chip,
|
|
108
|
+
title: {
|
|
109
|
+
y: titleY,
|
|
110
|
+
lines: fittedTitle
|
|
111
|
+
},
|
|
112
|
+
body: {
|
|
113
|
+
y: bodyY,
|
|
114
|
+
lines: fittedBody,
|
|
115
|
+
contentHeight,
|
|
116
|
+
viewportHeight,
|
|
117
|
+
scrollable: viewportHeight > 0 && contentHeight > viewportHeight
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export { BODY_LINE_HEIGHT, CHIP_HEIGHT, IMAGE_BAND_HEIGHT, PANEL_CANVAS_WIDTH, PANEL_FONTS, PANEL_MAX_HEIGHT, PANEL_MIN_HEIGHT, PANEL_PAD_X, PANEL_PAD_Y, TEXT_CANVAS_MAX_HEIGHT, TITLE_LINE_HEIGHT, layoutPanel, wrapText };
|
|
@@ -6,13 +6,28 @@ export declare class VrAnnotationPanel extends Script {
|
|
|
6
6
|
bodyText?: string;
|
|
7
7
|
imageUrls?: string[];
|
|
8
8
|
annotationIndex: number;
|
|
9
|
-
private
|
|
10
|
-
private
|
|
11
|
-
private
|
|
12
|
-
private
|
|
9
|
+
private _chromeMaterial?;
|
|
10
|
+
private _chromeTexture?;
|
|
11
|
+
private _chromeMesh?;
|
|
12
|
+
private _chromeCanvas?;
|
|
13
|
+
private _textMaterial?;
|
|
14
|
+
private _textTexture?;
|
|
15
|
+
private _textMesh?;
|
|
16
|
+
private _textCanvas?;
|
|
17
|
+
private _textInstance?;
|
|
18
|
+
private _thumbMaterial?;
|
|
19
|
+
private _thumbMesh?;
|
|
20
|
+
private _thumbInstance?;
|
|
13
21
|
private _drawnSignature;
|
|
14
22
|
private _currentImage;
|
|
15
23
|
private _loadToken;
|
|
24
|
+
private _loadedImage?;
|
|
25
|
+
private _layout?;
|
|
26
|
+
private _scrollY;
|
|
27
|
+
private _maxScroll;
|
|
28
|
+
/** Height (canvas px) the chrome occupies; the rest of its canvas is unused. */
|
|
29
|
+
private _canvasHeight;
|
|
30
|
+
private _panelHeight;
|
|
16
31
|
private _headRotation;
|
|
17
32
|
private _anchor;
|
|
18
33
|
private _scratchDir;
|
|
@@ -22,17 +37,35 @@ export declare class VrAnnotationPanel extends Script {
|
|
|
22
37
|
private _scratchScale;
|
|
23
38
|
private _onSelect;
|
|
24
39
|
initialize(): void;
|
|
25
|
-
private
|
|
40
|
+
private _createTexture;
|
|
41
|
+
private _createTexturedMaterial;
|
|
42
|
+
private _createQuadMesh;
|
|
43
|
+
/** Rewrites the quad's four corners and the slice of its texture they map to. */
|
|
44
|
+
private _writeQuad;
|
|
45
|
+
/** Converts a y in panel canvas coordinates to the entity's local space. */
|
|
46
|
+
private _localY;
|
|
47
|
+
private _writeChromeQuad;
|
|
48
|
+
/** Maps the visible window of the (much taller) text texture onto the body quad. */
|
|
49
|
+
private _writeTextQuad;
|
|
50
|
+
private _writeThumbQuad;
|
|
26
51
|
private _roundRect;
|
|
27
52
|
private _drawArrow;
|
|
28
53
|
private _drawDots;
|
|
29
|
-
|
|
54
|
+
/** Re-measures the content, resizes the panel, and repaints both canvases. */
|
|
55
|
+
private _rebuild;
|
|
56
|
+
private _drawChrome;
|
|
57
|
+
/**
|
|
58
|
+
* Paints every body line onto its own canvas, tall enough to hold all of them. Scrolling then
|
|
59
|
+
* only moves the quad's UV window over this texture, so no repaint or upload happens per frame.
|
|
60
|
+
*/
|
|
61
|
+
private _drawText;
|
|
30
62
|
private _setImage;
|
|
31
63
|
private _loadCurrentImage;
|
|
32
64
|
private _rayQuadHit;
|
|
33
65
|
/** True when the ray points at the panel quad (used to suppress the empty-select dismiss). */
|
|
34
66
|
rayHitsPanel(origin: Vec3, direction: Vec3): boolean;
|
|
35
|
-
update(): void;
|
|
67
|
+
update(dt: number): void;
|
|
68
|
+
private _scroll;
|
|
36
69
|
private _trackHead;
|
|
37
70
|
private _teardown;
|
|
38
71
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vr-annotation-panel.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/vr-annotation-panel.ts"],"names":[],"mappings":"AAAA,OAAO,EAUL,MAAM,EAGN,IAAI,EAEL,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"vr-annotation-panel.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/vr-annotation-panel.ts"],"names":[],"mappings":"AAAA,OAAO,EAUL,MAAM,EAGN,IAAI,EAEL,MAAM,YAAY,CAAC;AA2CpB,qBAAa,iBAAkB,SAAQ,MAAM;IAC3C,MAAM,CAAC,UAAU,SAAuB;IAGxC,KAAK,SAAM;IACX,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,eAAe,SAAM;IAErB,OAAO,CAAC,eAAe,CAAC,CAAmB;IAC3C,OAAO,CAAC,cAAc,CAAC,CAAU;IACjC,OAAO,CAAC,WAAW,CAAC,CAAO;IAC3B,OAAO,CAAC,aAAa,CAAC,CAAoB;IAE1C,OAAO,CAAC,aAAa,CAAC,CAAmB;IACzC,OAAO,CAAC,YAAY,CAAC,CAAU;IAC/B,OAAO,CAAC,SAAS,CAAC,CAAO;IACzB,OAAO,CAAC,WAAW,CAAC,CAAoB;IACxC,OAAO,CAAC,aAAa,CAAC,CAAe;IAErC,OAAO,CAAC,cAAc,CAAC,CAAmB;IAC1C,OAAO,CAAC,UAAU,CAAC,CAAO;IAC1B,OAAO,CAAC,cAAc,CAAC,CAAe;IAEtC,OAAO,CAAC,eAAe,CAAuB;IAC9C,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,YAAY,CAAC,CAAmB;IACxC,OAAO,CAAC,OAAO,CAAC,CAAc;IAC9B,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,UAAU,CAAK;IACvB,gFAAgF;IAChF,OAAO,CAAC,aAAa,CAAoB;IACzC,OAAO,CAAC,YAAY,CAA8B;IAElD,OAAO,CAAC,aAAa,CAAc;IACnC,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,QAAQ,CAAc;IAC9B,OAAO,CAAC,UAAU,CAAc;IAChC,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,aAAa,CAAc;IAEnC,OAAO,CAAC,SAAS,CAwBf;IAEF,UAAU;IA0CV,OAAO,CAAC,cAAc;IAgBtB,OAAO,CAAC,uBAAuB;IAgB/B,OAAO,CAAC,eAAe;IASvB,iFAAiF;IACjF,OAAO,CAAC,UAAU;IAelB,4EAA4E;IAC5E,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,gBAAgB;IASxB,oFAAoF;IACpF,OAAO,CAAC,cAAc;IAiBtB,OAAO,CAAC,eAAe;IA0BvB,OAAO,CAAC,UAAU;IAUlB,OAAO,CAAC,UAAU;IAkBlB,OAAO,CAAC,SAAS;IAWjB,8EAA8E;IAC9E,OAAO,CAAC,QAAQ;IAuChB,OAAO,CAAC,WAAW;IAoEnB;;;OAGG;IACH,OAAO,CAAC,SAAS;IAmCjB,OAAO,CAAC,SAAS;IAOjB,OAAO,CAAC,iBAAiB;IAmBzB,OAAO,CAAC,WAAW;IAmBnB,8FAA8F;IAC9F,YAAY,CAAC,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,GAAG,OAAO;IAIpD,MAAM,CAAC,EAAE,EAAE,MAAM;IAkCjB,OAAO,CAAC,OAAO;IAsBf,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,SAAS;CAsBlB"}
|
|
@@ -1,19 +1,22 @@
|
|
|
1
|
-
import { Vec3, Script, Quat,
|
|
2
|
-
import {
|
|
1
|
+
import { Vec3, Script, Quat, StandardMaterial, MeshInstance, Color, BLEND_NORMAL, CULLFACE_NONE, LAYERID_IMMEDIATE, Texture, FILTER_LINEAR, ADDRESS_CLAMP_TO_EDGE, Mesh } from 'playcanvas';
|
|
2
|
+
import { PANEL_MAX_HEIGHT, PANEL_CANVAS_WIDTH, layoutPanel, PANEL_PAD_X, PANEL_FONTS, TITLE_LINE_HEIGHT, BODY_LINE_HEIGHT } from './vr-annotation-panel-layout.js';
|
|
3
|
+
import { readScrollAxis, nextScrollY } from './vr-annotation-scroll.js';
|
|
3
4
|
import { rayQuadHit } from './xr-ray-quad.js';
|
|
4
5
|
|
|
5
|
-
const CANVAS_WIDTH = 1024;
|
|
6
|
-
const CANVAS_HEIGHT = 768;
|
|
7
6
|
const PANEL_WIDTH = 7.5;
|
|
8
|
-
const PANEL_HEIGHT = PANEL_WIDTH * CANVAS_HEIGHT / CANVAS_WIDTH;
|
|
9
7
|
|
|
10
8
|
/** Gap (m) between the top of the hotspot and the bottom edge of the panel. */
|
|
11
9
|
const PANEL_GAP = 0.75;
|
|
10
|
+
const PX_TO_M = PANEL_WIDTH / PANEL_CANVAS_WIDTH;
|
|
12
11
|
|
|
13
|
-
/**
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
const
|
|
12
|
+
/** Local +z faces the viewer, so the overlay quads sit just in front of the chrome quad. Their
|
|
13
|
+
* draw order would otherwise depend on how the transparent sort breaks a tie between coplanar
|
|
14
|
+
* quads, all of which have depth testing off. */
|
|
15
|
+
const TEXT_Z = 0.001;
|
|
16
|
+
const THUMB_Z = 0.002;
|
|
17
|
+
const SCROLLBAR_WIDTH = 10;
|
|
18
|
+
const SCROLLBAR_INSET = 18;
|
|
19
|
+
const SCROLLBAR_MIN_HEIGHT = 48;
|
|
17
20
|
|
|
18
21
|
/** Local +z (the quad's front face) — used to derive the panel's yaw toward the viewer. */
|
|
19
22
|
const FORWARD_Z = new Vec3(0, 0, 1);
|
|
@@ -22,10 +25,6 @@ const RIGHT_AXIS = new Vec3(1, 0, 0);
|
|
|
22
25
|
const UP_AXIS = new Vec3(0, 1, 0);
|
|
23
26
|
const RAD_TO_DEG = 180 / Math.PI;
|
|
24
27
|
|
|
25
|
-
/** Fixed image band (canvas px) so arrows/dots keep a stable position while images load. */
|
|
26
|
-
const IMAGE_Y = 40;
|
|
27
|
-
const IMAGE_H = 360;
|
|
28
|
-
|
|
29
28
|
/** Width (canvas px) of the left/right arrow hit + draw zones over the image band. */
|
|
30
29
|
const ARROW_ZONE_W = 170;
|
|
31
30
|
class VrAnnotationPanel extends Script {
|
|
@@ -37,6 +36,11 @@ class VrAnnotationPanel extends Script {
|
|
|
37
36
|
_drawnSignature = null;
|
|
38
37
|
_currentImage = 0;
|
|
39
38
|
_loadToken = 0;
|
|
39
|
+
_scrollY = 0;
|
|
40
|
+
_maxScroll = 0;
|
|
41
|
+
/** Height (canvas px) the chrome occupies; the rest of its canvas is unused. */
|
|
42
|
+
_canvasHeight = PANEL_MAX_HEIGHT;
|
|
43
|
+
_panelHeight = PANEL_MAX_HEIGHT * PX_TO_M;
|
|
40
44
|
_headRotation = new Quat();
|
|
41
45
|
_anchor = new Vec3();
|
|
42
46
|
_scratchDir = new Vec3();
|
|
@@ -49,73 +53,136 @@ class VrAnnotationPanel extends Script {
|
|
|
49
53
|
return;
|
|
50
54
|
}
|
|
51
55
|
const images = this.imageUrls ?? [];
|
|
52
|
-
|
|
56
|
+
const band = this._layout?.image;
|
|
57
|
+
if (images.length <= 1 || !band) {
|
|
53
58
|
return;
|
|
54
59
|
}
|
|
55
60
|
const hit = this._rayQuadHit(inputSource.getOrigin(), inputSource.getDirection());
|
|
56
61
|
if (!hit) {
|
|
57
62
|
return;
|
|
58
63
|
}
|
|
59
|
-
const cx = (hit.u + 1) / 2 *
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
const cx = (hit.u + 1) / 2 * PANEL_CANVAS_WIDTH;
|
|
65
|
+
// The image sits on the chrome quad, which doesn't scroll, so this needs no scroll offset.
|
|
66
|
+
const cy = (1 - hit.v) / 2 * this._canvasHeight;
|
|
67
|
+
if (cy < band.y || cy > band.y + band.height) {
|
|
62
68
|
return;
|
|
63
69
|
}
|
|
64
70
|
if (cx <= ARROW_ZONE_W) {
|
|
65
71
|
this._setImage((this._currentImage - 1 + images.length) % images.length);
|
|
66
|
-
} else if (cx >=
|
|
72
|
+
} else if (cx >= PANEL_CANVAS_WIDTH - ARROW_ZONE_W) {
|
|
67
73
|
this._setImage((this._currentImage + 1) % images.length);
|
|
68
74
|
}
|
|
69
75
|
};
|
|
70
76
|
initialize() {
|
|
71
|
-
this.
|
|
72
|
-
this.
|
|
73
|
-
this.
|
|
74
|
-
this.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
this._chromeCanvas = document.createElement('canvas');
|
|
78
|
+
this._chromeCanvas.width = PANEL_CANVAS_WIDTH;
|
|
79
|
+
this._chromeCanvas.height = PANEL_MAX_HEIGHT;
|
|
80
|
+
this._chromeTexture = this._createTexture('vr-annotation-panel', this._chromeCanvas);
|
|
81
|
+
this._chromeMaterial = this._createTexturedMaterial(this._chromeTexture);
|
|
82
|
+
this._chromeMesh = this._createQuadMesh();
|
|
83
|
+
|
|
84
|
+
// Sized once the first layout runs; until then it has nothing to show.
|
|
85
|
+
this._textMesh = this._createQuadMesh();
|
|
86
|
+
this._textMaterial = new StandardMaterial();
|
|
87
|
+
this._textInstance = new MeshInstance(this._textMesh, this._textMaterial);
|
|
88
|
+
this._textInstance.visible = false;
|
|
89
|
+
this._thumbMaterial = new StandardMaterial();
|
|
90
|
+
this._thumbMaterial.useLighting = false;
|
|
91
|
+
this._thumbMaterial.emissive = new Color(0, 0, 0);
|
|
92
|
+
this._thumbMaterial.opacity = 0.3;
|
|
93
|
+
this._thumbMaterial.blendType = BLEND_NORMAL;
|
|
94
|
+
this._thumbMaterial.depthTest = false;
|
|
95
|
+
this._thumbMaterial.depthWrite = false;
|
|
96
|
+
this._thumbMaterial.cull = CULLFACE_NONE;
|
|
97
|
+
this._thumbMaterial.update();
|
|
98
|
+
this._thumbMesh = this._createQuadMesh();
|
|
99
|
+
this._thumbInstance = new MeshInstance(this._thumbMesh, this._thumbMaterial);
|
|
100
|
+
this._thumbInstance.visible = false;
|
|
101
|
+
this.entity.addComponent('render', {
|
|
102
|
+
meshInstances: [new MeshInstance(this._chromeMesh, this._chromeMaterial), this._textInstance, this._thumbInstance],
|
|
103
|
+
layers: [LAYERID_IMMEDIATE]
|
|
104
|
+
});
|
|
105
|
+
this.app.xr?.input?.on('select', this._onSelect);
|
|
106
|
+
// Script removal fires a 'destroy' event rather than calling a destroy() method, so the
|
|
107
|
+
// teardown has to be registered as a listener or the handler and GPU resources outlive us.
|
|
108
|
+
this.once('destroy', () => this._teardown());
|
|
109
|
+
}
|
|
110
|
+
_createTexture(name, source) {
|
|
111
|
+
const texture = new Texture(this.app.graphicsDevice, {
|
|
112
|
+
name,
|
|
113
|
+
width: source.width,
|
|
114
|
+
height: source.height,
|
|
78
115
|
addressU: ADDRESS_CLAMP_TO_EDGE,
|
|
79
116
|
addressV: ADDRESS_CLAMP_TO_EDGE,
|
|
80
117
|
minFilter: FILTER_LINEAR,
|
|
81
118
|
magFilter: FILTER_LINEAR,
|
|
82
119
|
mipmaps: true
|
|
83
120
|
});
|
|
84
|
-
|
|
121
|
+
texture.setSource(source);
|
|
122
|
+
return texture;
|
|
123
|
+
}
|
|
124
|
+
_createTexturedMaterial(texture) {
|
|
85
125
|
const material = new StandardMaterial();
|
|
86
126
|
material.useLighting = false;
|
|
87
127
|
material.emissive = new Color(1, 1, 1);
|
|
88
|
-
material.emissiveMap =
|
|
89
|
-
material.opacityMap =
|
|
128
|
+
material.emissiveMap = texture;
|
|
129
|
+
material.opacityMap = texture;
|
|
90
130
|
material.opacityMapChannel = 'a';
|
|
91
131
|
material.blendType = BLEND_NORMAL;
|
|
92
132
|
material.depthTest = false;
|
|
93
133
|
material.depthWrite = false;
|
|
94
134
|
material.cull = CULLFACE_NONE;
|
|
95
135
|
material.update();
|
|
96
|
-
|
|
97
|
-
this._mesh = this._createMesh();
|
|
98
|
-
const meshInstance = new MeshInstance(this._mesh, material);
|
|
99
|
-
this.entity.addComponent('render', {
|
|
100
|
-
meshInstances: [meshInstance],
|
|
101
|
-
layers: [LAYERID_IMMEDIATE]
|
|
102
|
-
});
|
|
103
|
-
this.app.xr?.input?.on('select', this._onSelect);
|
|
104
|
-
// Script removal fires a 'destroy' event rather than calling a destroy() method, so the
|
|
105
|
-
// teardown has to be registered as a listener or the handler and GPU resources outlive us.
|
|
106
|
-
this.once('destroy', () => this._teardown());
|
|
136
|
+
return material;
|
|
107
137
|
}
|
|
108
|
-
|
|
109
|
-
const hw = PANEL_WIDTH / 2;
|
|
110
|
-
const hh = PANEL_HEIGHT / 2;
|
|
138
|
+
_createQuadMesh() {
|
|
111
139
|
const mesh = new Mesh(this.app.graphicsDevice);
|
|
112
|
-
mesh.setPositions([-hw, -hh, 0, hw, -hh, 0, hw, hh, 0, -hw, hh, 0]);
|
|
113
140
|
mesh.setNormals([0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1]);
|
|
114
|
-
mesh.setUvs(0, [0, 1, 1, 1, 1, 0, 0, 0]);
|
|
115
141
|
mesh.setIndices([0, 1, 2, 0, 2, 3]);
|
|
116
|
-
|
|
142
|
+
this._writeQuad(mesh, -PANEL_WIDTH / 2, PANEL_WIDTH / 2, 0, 0, 0, 0, 1);
|
|
117
143
|
return mesh;
|
|
118
144
|
}
|
|
145
|
+
|
|
146
|
+
/** Rewrites the quad's four corners and the slice of its texture they map to. */
|
|
147
|
+
_writeQuad(mesh, left, right, top, bottom, z, vTop, vBottom) {
|
|
148
|
+
mesh.setPositions([left, bottom, z, right, bottom, z, right, top, z, left, top, z]);
|
|
149
|
+
mesh.setUvs(0, [0, vBottom, 1, vBottom, 1, vTop, 0, vTop]);
|
|
150
|
+
mesh.update();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Converts a y in panel canvas coordinates to the entity's local space. */
|
|
154
|
+
_localY(canvasY) {
|
|
155
|
+
return this._panelHeight / 2 - canvasY * PX_TO_M;
|
|
156
|
+
}
|
|
157
|
+
_writeChromeQuad() {
|
|
158
|
+
if (!this._chromeMesh) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const hw = PANEL_WIDTH / 2;
|
|
162
|
+
const hh = this._panelHeight / 2;
|
|
163
|
+
this._writeQuad(this._chromeMesh, -hw, hw, hh, -hh, 0, 0, this._canvasHeight / PANEL_MAX_HEIGHT);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Maps the visible window of the (much taller) text texture onto the body quad. */
|
|
167
|
+
_writeTextQuad() {
|
|
168
|
+
const body = this._layout?.body;
|
|
169
|
+
if (!this._textMesh || !this._textInstance || !body || body.contentHeight === 0) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
this._writeQuad(this._textMesh, -PANEL_WIDTH / 2, PANEL_WIDTH / 2, this._localY(body.y), this._localY(body.y + body.viewportHeight), TEXT_Z, this._scrollY / body.contentHeight, (this._scrollY + body.viewportHeight) / body.contentHeight);
|
|
173
|
+
}
|
|
174
|
+
_writeThumbQuad() {
|
|
175
|
+
const body = this._layout?.body;
|
|
176
|
+
if (!this._thumbMesh || !body?.scrollable) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// The minimum keeps the thumb grabbable on very long text, but never past the track itself.
|
|
180
|
+
const height = Math.min(body.viewportHeight, Math.max(SCROLLBAR_MIN_HEIGHT, body.viewportHeight * body.viewportHeight / body.contentHeight));
|
|
181
|
+
const travel = body.viewportHeight - height;
|
|
182
|
+
const top = body.y + (this._maxScroll > 0 ? this._scrollY / this._maxScroll * travel : 0);
|
|
183
|
+
const right = PANEL_CANVAS_WIDTH - SCROLLBAR_INSET;
|
|
184
|
+
this._writeQuad(this._thumbMesh, (right - SCROLLBAR_WIDTH) * PX_TO_M - PANEL_WIDTH / 2, right * PX_TO_M - PANEL_WIDTH / 2, this._localY(top), this._localY(top + height), THUMB_Z, 0, 1);
|
|
185
|
+
}
|
|
119
186
|
_roundRect(ctx, x, y, w, h, r) {
|
|
120
187
|
ctx.beginPath();
|
|
121
188
|
ctx.moveTo(x + r, y);
|
|
@@ -143,7 +210,7 @@ class VrAnnotationPanel extends Script {
|
|
|
143
210
|
}
|
|
144
211
|
_drawDots(ctx, count, dotY) {
|
|
145
212
|
const gap = 26;
|
|
146
|
-
const startX =
|
|
213
|
+
const startX = PANEL_CANVAS_WIDTH / 2 - (count - 1) * gap / 2;
|
|
147
214
|
for (let i = 0; i < count; i++) {
|
|
148
215
|
ctx.beginPath();
|
|
149
216
|
ctx.arc(startX + i * gap, dotY, 7, 0, Math.PI * 2);
|
|
@@ -151,83 +218,144 @@ class VrAnnotationPanel extends Script {
|
|
|
151
218
|
ctx.fill();
|
|
152
219
|
}
|
|
153
220
|
}
|
|
154
|
-
|
|
155
|
-
|
|
221
|
+
|
|
222
|
+
/** Re-measures the content, resizes the panel, and repaints both canvases. */
|
|
223
|
+
_rebuild() {
|
|
224
|
+
const ctx = this._chromeCanvas?.getContext('2d');
|
|
156
225
|
if (!ctx) {
|
|
157
226
|
return;
|
|
158
227
|
}
|
|
159
|
-
|
|
228
|
+
const images = this.imageUrls ?? [];
|
|
229
|
+
this._layout = layoutPanel({
|
|
230
|
+
title: this.title,
|
|
231
|
+
label: this.label,
|
|
232
|
+
bodyText: this.bodyText,
|
|
233
|
+
imageCount: images.length,
|
|
234
|
+
contentWidth: PANEL_CANVAS_WIDTH - PANEL_PAD_X * 2,
|
|
235
|
+
measure: (text, style) => {
|
|
236
|
+
ctx.font = PANEL_FONTS[style];
|
|
237
|
+
return ctx.measureText(text).width;
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
const {
|
|
241
|
+
body
|
|
242
|
+
} = this._layout;
|
|
243
|
+
this._maxScroll = body.contentHeight - body.viewportHeight;
|
|
244
|
+
this._canvasHeight = this._layout.height;
|
|
245
|
+
this._panelHeight = this._layout.height * PX_TO_M;
|
|
246
|
+
this._drawChrome();
|
|
247
|
+
this._drawText();
|
|
248
|
+
this._writeChromeQuad();
|
|
249
|
+
this._writeTextQuad();
|
|
250
|
+
this._writeThumbQuad();
|
|
251
|
+
if (this._textInstance) {
|
|
252
|
+
this._textInstance.visible = body.contentHeight > 0;
|
|
253
|
+
}
|
|
254
|
+
if (this._thumbInstance) {
|
|
255
|
+
this._thumbInstance.visible = body.scrollable;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
_drawChrome() {
|
|
259
|
+
const ctx = this._chromeCanvas?.getContext('2d');
|
|
260
|
+
const layout = this._layout;
|
|
261
|
+
if (!ctx || !layout) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const contentWidth = PANEL_CANVAS_WIDTH - PANEL_PAD_X * 2;
|
|
265
|
+
const images = this.imageUrls ?? [];
|
|
266
|
+
ctx.clearRect(0, 0, PANEL_CANVAS_WIDTH, PANEL_MAX_HEIGHT);
|
|
160
267
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.96)';
|
|
161
|
-
this._roundRect(ctx, 0, 0,
|
|
268
|
+
this._roundRect(ctx, 0, 0, PANEL_CANVAS_WIDTH, layout.height, 32);
|
|
162
269
|
ctx.fill();
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
270
|
+
if (layout.image) {
|
|
271
|
+
const {
|
|
272
|
+
y: bandY,
|
|
273
|
+
height: bandH
|
|
274
|
+
} = layout.image;
|
|
167
275
|
ctx.save();
|
|
168
|
-
this._roundRect(ctx,
|
|
276
|
+
this._roundRect(ctx, PANEL_PAD_X, bandY, contentWidth, bandH, 16);
|
|
169
277
|
ctx.clip();
|
|
278
|
+
const image = this._loadedImage;
|
|
170
279
|
if (image) {
|
|
171
|
-
const scale = Math.max(contentWidth / image.width,
|
|
280
|
+
const scale = Math.max(contentWidth / image.width, bandH / image.height);
|
|
172
281
|
const dw = image.width * scale;
|
|
173
282
|
const dh = image.height * scale;
|
|
174
|
-
ctx.drawImage(image,
|
|
283
|
+
ctx.drawImage(image, PANEL_PAD_X + (contentWidth - dw) / 2, bandY + (bandH - dh) / 2, dw, dh);
|
|
175
284
|
} else {
|
|
176
285
|
ctx.fillStyle = 'rgba(0, 0, 0, 0.06)';
|
|
177
|
-
ctx.fillRect(
|
|
286
|
+
ctx.fillRect(PANEL_PAD_X, bandY, contentWidth, bandH);
|
|
178
287
|
}
|
|
179
288
|
ctx.restore();
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const bandCenterY = IMAGE_Y + IMAGE_H / 2;
|
|
289
|
+
if (layout.dotsY !== null) {
|
|
290
|
+
const bandCenterY = bandY + bandH / 2;
|
|
183
291
|
this._drawArrow(ctx, ARROW_ZONE_W / 2, bandCenterY, true);
|
|
184
|
-
this._drawArrow(ctx,
|
|
185
|
-
this._drawDots(ctx, images.length,
|
|
186
|
-
y += 32;
|
|
292
|
+
this._drawArrow(ctx, PANEL_CANVAS_WIDTH - ARROW_ZONE_W / 2, bandCenterY, false);
|
|
293
|
+
this._drawDots(ctx, images.length, layout.dotsY);
|
|
187
294
|
}
|
|
188
|
-
y += 16;
|
|
189
295
|
}
|
|
190
|
-
if (this.label) {
|
|
191
|
-
ctx.font =
|
|
296
|
+
if (layout.chip && this.label) {
|
|
297
|
+
ctx.font = PANEL_FONTS.label;
|
|
192
298
|
ctx.textBaseline = 'middle';
|
|
193
|
-
const chipW = ctx.measureText(this.label).width + 32;
|
|
194
|
-
const chipH = 44;
|
|
195
299
|
ctx.fillStyle = 'rgba(44, 134, 88, 0.95)';
|
|
196
|
-
this._roundRect(ctx,
|
|
300
|
+
this._roundRect(ctx, PANEL_PAD_X, layout.chip.y, layout.chip.width, layout.chip.height, 12);
|
|
197
301
|
ctx.fill();
|
|
198
302
|
ctx.fillStyle = '#ffffff';
|
|
199
|
-
ctx.fillText(this.label,
|
|
200
|
-
y += chipH + 24;
|
|
303
|
+
ctx.fillText(this.label, PANEL_PAD_X + 16, layout.chip.y + layout.chip.height / 2);
|
|
201
304
|
}
|
|
202
305
|
ctx.textBaseline = 'top';
|
|
203
306
|
ctx.fillStyle = '#000000';
|
|
204
|
-
ctx.font =
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
y
|
|
307
|
+
ctx.font = PANEL_FONTS.title;
|
|
308
|
+
layout.title.lines.forEach((line, index) => {
|
|
309
|
+
ctx.fillText(line, PANEL_PAD_X, layout.title.y + index * TITLE_LINE_HEIGHT);
|
|
310
|
+
});
|
|
311
|
+
if (layout.body.scrollable) {
|
|
312
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.08)';
|
|
313
|
+
const trackX = PANEL_CANVAS_WIDTH - SCROLLBAR_INSET - SCROLLBAR_WIDTH;
|
|
314
|
+
this._roundRect(ctx, trackX, layout.body.y, SCROLLBAR_WIDTH, layout.body.viewportHeight, SCROLLBAR_WIDTH / 2);
|
|
315
|
+
ctx.fill();
|
|
212
316
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
317
|
+
this._chromeTexture?.upload();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Paints every body line onto its own canvas, tall enough to hold all of them. Scrolling then
|
|
322
|
+
* only moves the quad's UV window over this texture, so no repaint or upload happens per frame.
|
|
323
|
+
*/
|
|
324
|
+
_drawText() {
|
|
325
|
+
const body = this._layout?.body;
|
|
326
|
+
if (!body) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const height = Math.max(1, body.contentHeight);
|
|
330
|
+
if (!this._textCanvas || this._textCanvas.height !== height) {
|
|
331
|
+
this._textCanvas = document.createElement('canvas');
|
|
332
|
+
this._textCanvas.width = PANEL_CANVAS_WIDTH;
|
|
333
|
+
this._textCanvas.height = height;
|
|
334
|
+
this._textTexture?.destroy();
|
|
335
|
+
this._textTexture = this._createTexture('vr-annotation-panel-text', this._textCanvas);
|
|
336
|
+
this._textMaterial?.destroy();
|
|
337
|
+
this._textMaterial = this._createTexturedMaterial(this._textTexture);
|
|
338
|
+
if (this._textInstance) {
|
|
339
|
+
this._textInstance.material = this._textMaterial;
|
|
224
340
|
}
|
|
225
341
|
}
|
|
226
|
-
this.
|
|
342
|
+
const ctx = this._textCanvas.getContext('2d');
|
|
343
|
+
if (!ctx) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
ctx.clearRect(0, 0, PANEL_CANVAS_WIDTH, height);
|
|
347
|
+
ctx.textBaseline = 'top';
|
|
348
|
+
ctx.font = PANEL_FONTS.body;
|
|
349
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.85)';
|
|
350
|
+
body.lines.forEach((line, index) => {
|
|
351
|
+
ctx.fillText(line, PANEL_PAD_X, index * BODY_LINE_HEIGHT);
|
|
352
|
+
});
|
|
353
|
+
this._textTexture?.upload();
|
|
227
354
|
}
|
|
228
355
|
_setImage(index) {
|
|
229
356
|
this._currentImage = index;
|
|
230
|
-
this.
|
|
357
|
+
this._loadedImage = undefined;
|
|
358
|
+
this._drawChrome();
|
|
231
359
|
this._loadCurrentImage();
|
|
232
360
|
}
|
|
233
361
|
_loadCurrentImage() {
|
|
@@ -241,7 +369,8 @@ class VrAnnotationPanel extends Script {
|
|
|
241
369
|
img.onload = () => {
|
|
242
370
|
// Ignore a stale load that a newer navigation has superseded.
|
|
243
371
|
if (token === this._loadToken) {
|
|
244
|
-
this.
|
|
372
|
+
this._loadedImage = img;
|
|
373
|
+
this._drawChrome();
|
|
245
374
|
}
|
|
246
375
|
};
|
|
247
376
|
// onerror (e.g. a host without CORS headers): keep the placeholder + text already drawn.
|
|
@@ -253,7 +382,7 @@ class VrAnnotationPanel extends Script {
|
|
|
253
382
|
rotation.transformVector(UP_AXIS, this._axisUp);
|
|
254
383
|
this.entity.getWorldTransform().getScale(this._scratchScale);
|
|
255
384
|
const halfWidth = PANEL_WIDTH / 2 * this._scratchScale.x;
|
|
256
|
-
const halfHeight =
|
|
385
|
+
const halfHeight = this._panelHeight / 2 * this._scratchScale.y;
|
|
257
386
|
return rayQuadHit(origin, direction, this.entity.getPosition(), this._axisRight, this._axisUp, halfWidth, halfHeight);
|
|
258
387
|
}
|
|
259
388
|
|
|
@@ -261,7 +390,7 @@ class VrAnnotationPanel extends Script {
|
|
|
261
390
|
rayHitsPanel(origin, direction) {
|
|
262
391
|
return this._rayQuadHit(origin, direction) !== null;
|
|
263
392
|
}
|
|
264
|
-
update() {
|
|
393
|
+
update(dt) {
|
|
265
394
|
if (this.app.xr?.active !== true) {
|
|
266
395
|
return;
|
|
267
396
|
}
|
|
@@ -269,16 +398,19 @@ class VrAnnotationPanel extends Script {
|
|
|
269
398
|
if (signature !== this._drawnSignature) {
|
|
270
399
|
this._drawnSignature = signature;
|
|
271
400
|
this._currentImage = 0;
|
|
272
|
-
this.
|
|
401
|
+
this._scrollY = 0;
|
|
402
|
+
this._loadedImage = undefined;
|
|
403
|
+
this._rebuild();
|
|
273
404
|
this._loadCurrentImage();
|
|
274
405
|
}
|
|
406
|
+
this._scroll(dt);
|
|
275
407
|
this._trackHead();
|
|
276
408
|
const anchor = this.app.root.findByName(`annotation-${this.annotationIndex}`);
|
|
277
409
|
if (!anchor) {
|
|
278
410
|
return;
|
|
279
411
|
}
|
|
280
412
|
this._anchor.copy(anchor.getPosition());
|
|
281
|
-
this.entity.setPosition(this._anchor.x
|
|
413
|
+
this.entity.setPosition(this._anchor.x, this._anchor.y + this._panelHeight / 2 + PANEL_GAP, this._anchor.z);
|
|
282
414
|
|
|
283
415
|
// Billboard on yaw only: face the viewer horizontally but stay upright and level, so
|
|
284
416
|
// tilting/rolling the headset does not tilt the panel. Derived from the head's forward
|
|
@@ -288,6 +420,25 @@ class VrAnnotationPanel extends Script {
|
|
|
288
420
|
this._yawQuat.setFromEulerAngles(0, yaw, 0);
|
|
289
421
|
this.entity.setRotation(this._yawQuat);
|
|
290
422
|
}
|
|
423
|
+
_scroll(dt) {
|
|
424
|
+
if (this._maxScroll <= 0) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
let axis = 0;
|
|
428
|
+
for (const inputSource of this.app.xr?.input?.inputSources ?? []) {
|
|
429
|
+
const sourceAxis = readScrollAxis(inputSource);
|
|
430
|
+
if (Math.abs(sourceAxis) > Math.abs(axis)) {
|
|
431
|
+
axis = sourceAxis;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
const scrollY = nextScrollY(this._scrollY, axis, dt, this._maxScroll);
|
|
435
|
+
if (scrollY === this._scrollY) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
this._scrollY = scrollY;
|
|
439
|
+
this._writeTextQuad();
|
|
440
|
+
this._writeThumbQuad();
|
|
441
|
+
}
|
|
291
442
|
_trackHead() {
|
|
292
443
|
const views = this.app.xr?.views?.list;
|
|
293
444
|
if (!views || views.length === 0) {
|
|
@@ -300,12 +451,22 @@ class VrAnnotationPanel extends Script {
|
|
|
300
451
|
if (this.entity.render) {
|
|
301
452
|
this.entity.render.meshInstances = [];
|
|
302
453
|
}
|
|
303
|
-
this.
|
|
304
|
-
this.
|
|
305
|
-
this.
|
|
306
|
-
this.
|
|
307
|
-
this.
|
|
308
|
-
this.
|
|
454
|
+
this._chromeMesh?.destroy();
|
|
455
|
+
this._textMesh?.destroy();
|
|
456
|
+
this._thumbMesh?.destroy();
|
|
457
|
+
this._chromeMaterial?.destroy();
|
|
458
|
+
this._textMaterial?.destroy();
|
|
459
|
+
this._thumbMaterial?.destroy();
|
|
460
|
+
this._chromeTexture?.destroy();
|
|
461
|
+
this._textTexture?.destroy();
|
|
462
|
+
this._chromeMesh = undefined;
|
|
463
|
+
this._textMesh = undefined;
|
|
464
|
+
this._thumbMesh = undefined;
|
|
465
|
+
this._chromeMaterial = undefined;
|
|
466
|
+
this._textMaterial = undefined;
|
|
467
|
+
this._thumbMaterial = undefined;
|
|
468
|
+
this._chromeTexture = undefined;
|
|
469
|
+
this._textTexture = undefined;
|
|
309
470
|
}
|
|
310
471
|
}
|
|
311
472
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Deflection the stick must pass before the text moves, so a resting thumb doesn't drift it. */
|
|
2
|
+
export declare const SCROLL_DEADZONE = 0.15;
|
|
3
|
+
/** Canvas px per second at full deflection. */
|
|
4
|
+
export declare const SCROLL_SPEED = 1200;
|
|
5
|
+
interface ThumbstickSource {
|
|
6
|
+
handedness: string;
|
|
7
|
+
gamepad?: {
|
|
8
|
+
axes: ArrayLike<number>;
|
|
9
|
+
} | null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Thumbstick Y for a source that can scroll the panel. Only the right hand scrolls: the left
|
|
13
|
+
* stick drives locomotion, and right-stick Y is free because snap-vertical is turned off.
|
|
14
|
+
* Hand-tracked sources report a gamepad with no axes, which would read as NaN.
|
|
15
|
+
*/
|
|
16
|
+
export declare const readScrollAxis: (inputSource: ThumbstickSource) => number;
|
|
17
|
+
/**
|
|
18
|
+
* Advances the scroll position for one frame of stick deflection, ramping from a standstill at
|
|
19
|
+
* the deadzone edge. A forward push reports negative Y and moves further into the text.
|
|
20
|
+
*/
|
|
21
|
+
export declare const nextScrollY: (scrollY: number, axisY: number, dt: number, maxScroll: number) => number;
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=vr-annotation-scroll.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vr-annotation-scroll.d.ts","sourceRoot":"","sources":["../../../src/components/VirtualWalkthrough/vr-annotation-scroll.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,eAAO,MAAM,eAAe,OAAO,CAAC;AAEpC,+CAA+C;AAC/C,eAAO,MAAM,YAAY,OAAO,CAAC;AAEjC,UAAU,gBAAgB;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,SAAS,CAAC,MAAM,CAAC,CAAA;KAAE,GAAG,IAAI,CAAC;CAC9C;AAED;;;;GAIG;AACH,eAAO,MAAM,cAAc,GAAI,aAAa,gBAAgB,KAAG,MAO9D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,WAAW,GAAI,SAAS,MAAM,EAAE,OAAO,MAAM,EAAE,IAAI,MAAM,EAAE,WAAW,MAAM,KAAG,MAU3F,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Deflection the stick must pass before the text moves, so a resting thumb doesn't drift it. */
|
|
2
|
+
const SCROLL_DEADZONE = 0.15;
|
|
3
|
+
|
|
4
|
+
/** Canvas px per second at full deflection. */
|
|
5
|
+
const SCROLL_SPEED = 1200;
|
|
6
|
+
/**
|
|
7
|
+
* Thumbstick Y for a source that can scroll the panel. Only the right hand scrolls: the left
|
|
8
|
+
* stick drives locomotion, and right-stick Y is free because snap-vertical is turned off.
|
|
9
|
+
* Hand-tracked sources report a gamepad with no axes, which would read as NaN.
|
|
10
|
+
*/
|
|
11
|
+
const readScrollAxis = inputSource => {
|
|
12
|
+
const axes = inputSource.gamepad?.axes;
|
|
13
|
+
if (inputSource.handedness !== 'right' || !axes || axes.length < 4) {
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
return axes[3];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Advances the scroll position for one frame of stick deflection, ramping from a standstill at
|
|
21
|
+
* the deadzone edge. A forward push reports negative Y and moves further into the text.
|
|
22
|
+
*/
|
|
23
|
+
const nextScrollY = (scrollY, axisY, dt, maxScroll) => {
|
|
24
|
+
const magnitude = Math.abs(axisY);
|
|
25
|
+
if (magnitude <= SCROLL_DEADZONE) {
|
|
26
|
+
return Math.min(Math.max(scrollY, 0), maxScroll);
|
|
27
|
+
}
|
|
28
|
+
const ramped = (magnitude - SCROLL_DEADZONE) / (1 - SCROLL_DEADZONE);
|
|
29
|
+
const delta = -Math.sign(axisY) * ramped * SCROLL_SPEED * dt;
|
|
30
|
+
return Math.min(Math.max(scrollY + delta, 0), maxScroll);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export { SCROLL_DEADZONE, SCROLL_SPEED, nextScrollY, readScrollAxis };
|