@retor/react-native 0.3.6 → 0.4.0

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/dist/index.d.mts CHANGED
@@ -43,6 +43,15 @@ interface InitPayload {
43
43
  interface LineProgressPayload {
44
44
  progress: number;
45
45
  closestTagId: string | null;
46
+ /** Point on the line at current progress (project-local space). */
47
+ targetPosition?: {
48
+ x: number;
49
+ y: number;
50
+ z: number;
51
+ } | null;
52
+ /** Distance along the line from the start.
53
+ * In meters if GPS is configured on the project, else project-local units. */
54
+ distanceFromStart?: number | null;
46
55
  }
47
56
  interface ViewerHandle {
48
57
  openLine: (lineId: string) => void;
@@ -55,29 +64,39 @@ interface ViewerHandle {
55
64
  interface NoteSubmitPayload {
56
65
  text: string;
57
66
  isPrivate: boolean;
67
+ /** The tag that was active when the + button was pressed. */
58
68
  tagId: string | null;
59
69
  lineId: string | null;
70
+ /**
71
+ * Point on the line at the scroll position when the + button was pressed,
72
+ * in project-local space. This is where the note visually anchors.
73
+ */
60
74
  position: {
61
75
  x: number;
62
76
  y: number;
63
77
  z: number;
64
78
  } | null;
79
+ /** Scroll progress along the line (0..1) when the + was pressed. */
80
+ progress: number;
81
+ /** Distance from the start of the line when the + was pressed.
82
+ * In meters if GPS is configured on the project, else project-local units. */
83
+ distanceFromStart: number | null;
65
84
  }
85
+ /**
86
+ * Stable data + commands. Updates only when meaningful things change
87
+ * (line opens/closes, notes loaded, project changes). Does NOT update per frame.
88
+ */
66
89
  interface RetorBridgeContextValue {
67
90
  project: RetorProject | null;
68
91
  lines: RetorLine[];
69
92
  activeLineId: string | null;
70
93
  activeLine: RetorLine | null;
71
- closestTagId: string | null;
72
- progress: number;
73
- isPlaying: boolean;
74
94
  isAddNoteOpen: boolean;
75
95
  addNoteTagId: string | null;
76
96
  controls: ViewerHandle;
77
97
  openAddNote: (tagId?: string) => void;
78
98
  closeAddNote: () => void;
79
99
  submitNote: (text: string, isPrivate?: boolean) => void;
80
- onNoteSubmit?: (payload: NoteSubmitPayload) => void;
81
100
  }
82
101
  declare function useRetorBridge(): RetorBridgeContextValue;
83
102
  /** Returns the array of all lines in the current project. */
@@ -91,11 +110,18 @@ declare function useProject(): RetorProject | null;
91
110
  declare function useActiveLine(): RetorLine | null;
92
111
  /**
93
112
  * Returns scroll progress (0..1) and the id of the closest tag along the
94
- * currently active line. Both are null when no line is open.
113
+ * currently active line. Components using this re-render on every frame
114
+ * during camera scroll — only call from leaves (tag items, progress rings).
95
115
  */
96
116
  declare function useLineProgress(): {
97
117
  progress: number;
98
118
  closestTagId: string | null;
119
+ targetPosition: {
120
+ x: number;
121
+ y: number;
122
+ z: number;
123
+ } | null;
124
+ distanceFromStart: number | null;
99
125
  };
100
126
  /**
101
127
  * Returns the autoplay state and controls.
package/dist/index.d.ts CHANGED
@@ -43,6 +43,15 @@ interface InitPayload {
43
43
  interface LineProgressPayload {
44
44
  progress: number;
45
45
  closestTagId: string | null;
46
+ /** Point on the line at current progress (project-local space). */
47
+ targetPosition?: {
48
+ x: number;
49
+ y: number;
50
+ z: number;
51
+ } | null;
52
+ /** Distance along the line from the start.
53
+ * In meters if GPS is configured on the project, else project-local units. */
54
+ distanceFromStart?: number | null;
46
55
  }
47
56
  interface ViewerHandle {
48
57
  openLine: (lineId: string) => void;
@@ -55,29 +64,39 @@ interface ViewerHandle {
55
64
  interface NoteSubmitPayload {
56
65
  text: string;
57
66
  isPrivate: boolean;
67
+ /** The tag that was active when the + button was pressed. */
58
68
  tagId: string | null;
59
69
  lineId: string | null;
70
+ /**
71
+ * Point on the line at the scroll position when the + button was pressed,
72
+ * in project-local space. This is where the note visually anchors.
73
+ */
60
74
  position: {
61
75
  x: number;
62
76
  y: number;
63
77
  z: number;
64
78
  } | null;
79
+ /** Scroll progress along the line (0..1) when the + was pressed. */
80
+ progress: number;
81
+ /** Distance from the start of the line when the + was pressed.
82
+ * In meters if GPS is configured on the project, else project-local units. */
83
+ distanceFromStart: number | null;
65
84
  }
85
+ /**
86
+ * Stable data + commands. Updates only when meaningful things change
87
+ * (line opens/closes, notes loaded, project changes). Does NOT update per frame.
88
+ */
66
89
  interface RetorBridgeContextValue {
67
90
  project: RetorProject | null;
68
91
  lines: RetorLine[];
69
92
  activeLineId: string | null;
70
93
  activeLine: RetorLine | null;
71
- closestTagId: string | null;
72
- progress: number;
73
- isPlaying: boolean;
74
94
  isAddNoteOpen: boolean;
75
95
  addNoteTagId: string | null;
76
96
  controls: ViewerHandle;
77
97
  openAddNote: (tagId?: string) => void;
78
98
  closeAddNote: () => void;
79
99
  submitNote: (text: string, isPrivate?: boolean) => void;
80
- onNoteSubmit?: (payload: NoteSubmitPayload) => void;
81
100
  }
82
101
  declare function useRetorBridge(): RetorBridgeContextValue;
83
102
  /** Returns the array of all lines in the current project. */
@@ -91,11 +110,18 @@ declare function useProject(): RetorProject | null;
91
110
  declare function useActiveLine(): RetorLine | null;
92
111
  /**
93
112
  * Returns scroll progress (0..1) and the id of the closest tag along the
94
- * currently active line. Both are null when no line is open.
113
+ * currently active line. Components using this re-render on every frame
114
+ * during camera scroll — only call from leaves (tag items, progress rings).
95
115
  */
96
116
  declare function useLineProgress(): {
97
117
  progress: number;
98
118
  closestTagId: string | null;
119
+ targetPosition: {
120
+ x: number;
121
+ y: number;
122
+ z: number;
123
+ } | null;
124
+ distanceFromStart: number | null;
99
125
  };
100
126
  /**
101
127
  * Returns the autoplay state and controls.
package/dist/index.js CHANGED
@@ -54,6 +54,13 @@ module.exports = __toCommonJS(index_exports);
54
54
  // src/context.tsx
55
55
  var import_react = __toESM(require("react"));
56
56
  var RetorBridgeContext = (0, import_react.createContext)(null);
57
+ var RetorProgressContext = (0, import_react.createContext)({
58
+ progress: 0,
59
+ closestTagId: null,
60
+ targetPosition: null,
61
+ distanceFromStart: null,
62
+ isPlaying: false
63
+ });
57
64
  function noop() {
58
65
  }
59
66
  var noopHandle = {
@@ -68,9 +75,6 @@ var fallback = {
68
75
  lines: [],
69
76
  activeLineId: null,
70
77
  activeLine: null,
71
- closestTagId: null,
72
- progress: 0,
73
- isPlaying: false,
74
78
  isAddNoteOpen: false,
75
79
  addNoteTagId: null,
76
80
  controls: noopHandle,
@@ -81,6 +85,9 @@ var fallback = {
81
85
  function useRetorBridge() {
82
86
  return (0, import_react.useContext)(RetorBridgeContext) ?? fallback;
83
87
  }
88
+ function useRetorProgress() {
89
+ return (0, import_react.useContext)(RetorProgressContext);
90
+ }
84
91
  function useLines() {
85
92
  return useRetorBridge().lines;
86
93
  }
@@ -91,11 +98,15 @@ function useActiveLine() {
91
98
  return useRetorBridge().activeLine;
92
99
  }
93
100
  function useLineProgress() {
94
- const { progress, closestTagId } = useRetorBridge();
95
- return (0, import_react.useMemo)(() => ({ progress, closestTagId }), [progress, closestTagId]);
101
+ const { progress, closestTagId, targetPosition, distanceFromStart } = useRetorProgress();
102
+ return (0, import_react.useMemo)(
103
+ () => ({ progress, closestTagId, targetPosition, distanceFromStart }),
104
+ [progress, closestTagId, targetPosition, distanceFromStart]
105
+ );
96
106
  }
97
107
  function useAutoplay() {
98
- const { isPlaying, controls } = useRetorBridge();
108
+ const { isPlaying } = useRetorProgress();
109
+ const { controls } = useRetorBridge();
99
110
  return (0, import_react.useMemo)(() => ({
100
111
  isPlaying,
101
112
  toggle: () => controls.toggleAutoplay(),
@@ -116,6 +127,9 @@ function useAddNote() {
116
127
  function RetorBridgeProvider({ value, children }) {
117
128
  return /* @__PURE__ */ import_react.default.createElement(RetorBridgeContext.Provider, { value }, children);
118
129
  }
130
+ function RetorProgressProvider({ value, children }) {
131
+ return /* @__PURE__ */ import_react.default.createElement(RetorProgressContext.Provider, { value }, children);
132
+ }
119
133
 
120
134
  // src/Viewer.tsx
121
135
  var import_react2 = __toESM(require("react"));
@@ -194,6 +208,8 @@ var Viewer = (0, import_react2.forwardRef)(function Viewer2({ projectId, id = "d
194
208
  const [activeLineId, setActiveLineId] = (0, import_react2.useState)(null);
195
209
  const [closestTagId, setClosestTagId] = (0, import_react2.useState)(null);
196
210
  const [progress, setProgress] = (0, import_react2.useState)(0);
211
+ const [targetPosition, setTargetPosition] = (0, import_react2.useState)(null);
212
+ const [distanceFromStart, setDistanceFromStart] = (0, import_react2.useState)(null);
197
213
  const [isPlaying, setIsPlaying] = (0, import_react2.useState)(false);
198
214
  const [isAddNoteOpen, setIsAddNoteOpen] = (0, import_react2.useState)(false);
199
215
  const [addNoteTagId, setAddNoteTagId] = (0, import_react2.useState)(null);
@@ -235,8 +251,11 @@ var Viewer = (0, import_react2.forwardRef)(function Viewer2({ projectId, id = "d
235
251
  const closestTagIdRef = (0, import_react2.useRef)(closestTagId);
236
252
  const addNoteTagIdRef = (0, import_react2.useRef)(addNoteTagId);
237
253
  const activeLineIdRef = (0, import_react2.useRef)(activeLineId);
238
- const linesRef = (0, import_react2.useRef)(lines);
239
254
  const onNoteSubmitRef = (0, import_react2.useRef)(onNoteSubmit);
255
+ const noteSnapshotRef = (0, import_react2.useRef)({ progress: 0, targetPosition: null, distanceFromStart: null });
256
+ const progressRef = (0, import_react2.useRef)(progress);
257
+ const targetPositionRef = (0, import_react2.useRef)(targetPosition);
258
+ const distanceFromStartRef = (0, import_react2.useRef)(distanceFromStart);
240
259
  (0, import_react2.useEffect)(() => {
241
260
  closestTagIdRef.current = closestTagId;
242
261
  }, [closestTagId]);
@@ -246,14 +265,25 @@ var Viewer = (0, import_react2.forwardRef)(function Viewer2({ projectId, id = "d
246
265
  (0, import_react2.useEffect)(() => {
247
266
  activeLineIdRef.current = activeLineId;
248
267
  }, [activeLineId]);
249
- (0, import_react2.useEffect)(() => {
250
- linesRef.current = lines;
251
- }, [lines]);
252
268
  (0, import_react2.useEffect)(() => {
253
269
  onNoteSubmitRef.current = onNoteSubmit;
254
270
  }, [onNoteSubmit]);
271
+ (0, import_react2.useEffect)(() => {
272
+ progressRef.current = progress;
273
+ }, [progress]);
274
+ (0, import_react2.useEffect)(() => {
275
+ targetPositionRef.current = targetPosition;
276
+ }, [targetPosition]);
277
+ (0, import_react2.useEffect)(() => {
278
+ distanceFromStartRef.current = distanceFromStart;
279
+ }, [distanceFromStart]);
255
280
  const openAddNote = (0, import_react2.useCallback)((tagId) => {
256
281
  setAddNoteTagId(tagId ?? closestTagIdRef.current ?? null);
282
+ noteSnapshotRef.current = {
283
+ progress: progressRef.current,
284
+ targetPosition: targetPositionRef.current,
285
+ distanceFromStart: distanceFromStartRef.current
286
+ };
257
287
  setIsAddNoteOpen(true);
258
288
  }, []);
259
289
  const closeAddNote = (0, import_react2.useCallback)(() => {
@@ -262,13 +292,15 @@ var Viewer = (0, import_react2.forwardRef)(function Viewer2({ projectId, id = "d
262
292
  const submitNote = (0, import_react2.useCallback)((text, isPrivate = true) => {
263
293
  const tagId = addNoteTagIdRef.current;
264
294
  const lineId = activeLineIdRef.current;
265
- const tag = linesRef.current.find((l) => l._id === lineId)?.tags.find((t) => t._id === tagId);
295
+ const snap = noteSnapshotRef.current;
266
296
  const payload = {
267
297
  text,
268
298
  isPrivate,
269
299
  tagId,
270
300
  lineId,
271
- position: tag?.position ?? null
301
+ position: snap.targetPosition,
302
+ progress: snap.progress,
303
+ distanceFromStart: snap.distanceFromStart
272
304
  };
273
305
  onNoteSubmitRef.current?.(payload);
274
306
  setIsAddNoteOpen(false);
@@ -309,6 +341,8 @@ var Viewer = (0, import_react2.forwardRef)(function Viewer2({ projectId, id = "d
309
341
  const payload = data.payload;
310
342
  setProgress(payload.progress);
311
343
  setClosestTagId(payload.closestTagId);
344
+ setTargetPosition(payload.targetPosition ?? null);
345
+ setDistanceFromStart(payload.distanceFromStart ?? null);
312
346
  onLineProgress?.(payload);
313
347
  break;
314
348
  }
@@ -333,20 +367,20 @@ var Viewer = (0, import_react2.forwardRef)(function Viewer2({ projectId, id = "d
333
367
  lines,
334
368
  activeLineId,
335
369
  activeLine,
336
- closestTagId,
337
- progress,
338
- isPlaying,
339
370
  isAddNoteOpen,
340
371
  addNoteTagId,
341
372
  controls,
342
373
  openAddNote,
343
374
  closeAddNote,
344
- submitNote,
345
- onNoteSubmit
375
+ submitNote
346
376
  }),
347
- [project, lines, activeLineId, activeLine, closestTagId, progress, isPlaying, isAddNoteOpen, addNoteTagId, controls, openAddNote, closeAddNote, submitNote, onNoteSubmit]
377
+ [project, lines, activeLineId, activeLine, isAddNoteOpen, addNoteTagId, controls, openAddNote, closeAddNote, submitNote]
378
+ );
379
+ const progressCtx = (0, import_react2.useMemo)(
380
+ () => ({ progress, closestTagId, targetPosition, distanceFromStart, isPlaying }),
381
+ [progress, closestTagId, targetPosition, distanceFromStart, isPlaying]
348
382
  );
349
- return /* @__PURE__ */ import_react2.default.createElement(RetorBridgeProvider, { value: ctxValue }, /* @__PURE__ */ import_react2.default.createElement(import_react_native.View, { style: [styles.root, style] }, /* @__PURE__ */ import_react2.default.createElement(
383
+ return /* @__PURE__ */ import_react2.default.createElement(RetorBridgeProvider, { value: ctxValue }, /* @__PURE__ */ import_react2.default.createElement(RetorProgressProvider, { value: progressCtx }, /* @__PURE__ */ import_react2.default.createElement(import_react_native.View, { style: [styles.root, style] }, /* @__PURE__ */ import_react2.default.createElement(
350
384
  import_react_native_webview.WebView,
351
385
  {
352
386
  ref: webviewRef,
@@ -359,7 +393,7 @@ var Viewer = (0, import_react2.forwardRef)(function Viewer2({ projectId, id = "d
359
393
  allowsInlineMediaPlayback: true,
360
394
  mediaPlaybackRequiresUserAction: false
361
395
  }
362
- ), children));
396
+ ), children)));
363
397
  });
364
398
  var styles = import_react_native.StyleSheet.create({
365
399
  root: { flex: 1, position: "relative" },
@@ -593,7 +627,8 @@ function DefaultHeader({
593
627
  return /* @__PURE__ */ import_react6.default.createElement(import_react_native5.View, { style: styles3.header }, /* @__PURE__ */ import_react6.default.createElement(import_react_native5.View, { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ import_react6.default.createElement(import_react_native5.Text, { style: styles3.title, numberOfLines: 1 }, line.name), line.subtitle && /* @__PURE__ */ import_react6.default.createElement(import_react_native5.Text, { style: styles3.subtitle, numberOfLines: 1 }, line.subtitle), line.description && /* @__PURE__ */ import_react6.default.createElement(import_react_native5.Text, { style: styles3.description, numberOfLines: minimized ? 2 : 4 }, line.description)), /* @__PURE__ */ import_react6.default.createElement(import_react_native5.View, { style: styles3.headerActions }, /* @__PURE__ */ import_react6.default.createElement(AutoplayButton, null), /* @__PURE__ */ import_react6.default.createElement(import_react_native5.Pressable, { style: styles3.iconBtn, onPress: onToggleMinimize }, minimized ? /* @__PURE__ */ import_react6.default.createElement(import_lucide_react_native2.ArrowUp, { size: 14, color: "rgba(255,255,255,0.6)" }) : /* @__PURE__ */ import_react6.default.createElement(import_lucide_react_native2.ArrowDown, { size: 14, color: "rgba(255,255,255,0.6)" }))));
594
628
  }
595
629
  function AutoplayButton() {
596
- const { isPlaying, progress, controls } = useRetorBridge();
630
+ const { controls } = useRetorBridge();
631
+ const { isPlaying, progress } = useRetorProgress();
597
632
  const r = 12.5;
598
633
  const c = 2 * Math.PI * r;
599
634
  return /* @__PURE__ */ import_react6.default.createElement(import_react_native5.Pressable, { style: styles3.iconBtn, onPress: () => controls.toggleAutoplay() }, /* @__PURE__ */ import_react6.default.createElement(import_react_native_svg.default, { width: 28, height: 28, style: import_react_native5.StyleSheet.absoluteFill }, /* @__PURE__ */ import_react6.default.createElement(
@@ -613,7 +648,8 @@ function AutoplayButton() {
613
648
  )), isPlaying ? /* @__PURE__ */ import_react6.default.createElement(import_lucide_react_native2.Pause, { size: 11, color: "white", fill: "white" }) : /* @__PURE__ */ import_react6.default.createElement(import_lucide_react_native2.Play, { size: 11, color: "white", fill: "white", style: { marginLeft: 1 } }));
614
649
  }
615
650
  function LineTagList({ children, listHeader }) {
616
- const { activeLine, closestTagId } = useRetorBridge();
651
+ const { activeLine } = useRetorBridge();
652
+ const { closestTagId } = useRetorProgress();
617
653
  const scrollRef = (0, import_react6.useRef)(null);
618
654
  const offsetsRef = (0, import_react6.useRef)(/* @__PURE__ */ new Map());
619
655
  const tags = (0, import_react6.useMemo)(
@@ -623,20 +659,17 @@ function LineTagList({ children, listHeader }) {
623
659
  (0, import_react6.useEffect)(() => {
624
660
  offsetsRef.current = /* @__PURE__ */ new Map();
625
661
  }, [activeLine?._id]);
662
+ const lastScrolledIdRef = (0, import_react6.useRef)(null);
626
663
  (0, import_react6.useEffect)(() => {
627
- if (!closestTagId) return;
628
- let attempts = 0;
629
- const tick = () => {
664
+ if (!closestTagId || closestTagId === lastScrolledIdRef.current) return;
665
+ const t = setTimeout(() => {
630
666
  const y = offsetsRef.current.get(closestTagId);
631
667
  if (y != null) {
632
668
  scrollRef.current?.scrollTo?.({ y, animated: true });
633
- return;
669
+ lastScrolledIdRef.current = closestTagId;
634
670
  }
635
- attempts += 1;
636
- if (attempts < 10) raf = requestAnimationFrame(tick);
637
- };
638
- let raf = requestAnimationFrame(tick);
639
- return () => cancelAnimationFrame(raf);
671
+ }, 300);
672
+ return () => clearTimeout(t);
640
673
  }, [closestTagId]);
641
674
  if (!activeLine) return null;
642
675
  const handleItemLayout = (id, e) => {
package/dist/index.mjs CHANGED
@@ -1,6 +1,13 @@
1
1
  // src/context.tsx
2
2
  import React, { createContext, useContext, useMemo } from "react";
3
3
  var RetorBridgeContext = createContext(null);
4
+ var RetorProgressContext = createContext({
5
+ progress: 0,
6
+ closestTagId: null,
7
+ targetPosition: null,
8
+ distanceFromStart: null,
9
+ isPlaying: false
10
+ });
4
11
  function noop() {
5
12
  }
6
13
  var noopHandle = {
@@ -15,9 +22,6 @@ var fallback = {
15
22
  lines: [],
16
23
  activeLineId: null,
17
24
  activeLine: null,
18
- closestTagId: null,
19
- progress: 0,
20
- isPlaying: false,
21
25
  isAddNoteOpen: false,
22
26
  addNoteTagId: null,
23
27
  controls: noopHandle,
@@ -28,6 +32,9 @@ var fallback = {
28
32
  function useRetorBridge() {
29
33
  return useContext(RetorBridgeContext) ?? fallback;
30
34
  }
35
+ function useRetorProgress() {
36
+ return useContext(RetorProgressContext);
37
+ }
31
38
  function useLines() {
32
39
  return useRetorBridge().lines;
33
40
  }
@@ -38,11 +45,15 @@ function useActiveLine() {
38
45
  return useRetorBridge().activeLine;
39
46
  }
40
47
  function useLineProgress() {
41
- const { progress, closestTagId } = useRetorBridge();
42
- return useMemo(() => ({ progress, closestTagId }), [progress, closestTagId]);
48
+ const { progress, closestTagId, targetPosition, distanceFromStart } = useRetorProgress();
49
+ return useMemo(
50
+ () => ({ progress, closestTagId, targetPosition, distanceFromStart }),
51
+ [progress, closestTagId, targetPosition, distanceFromStart]
52
+ );
43
53
  }
44
54
  function useAutoplay() {
45
- const { isPlaying, controls } = useRetorBridge();
55
+ const { isPlaying } = useRetorProgress();
56
+ const { controls } = useRetorBridge();
46
57
  return useMemo(() => ({
47
58
  isPlaying,
48
59
  toggle: () => controls.toggleAutoplay(),
@@ -63,6 +74,9 @@ function useAddNote() {
63
74
  function RetorBridgeProvider({ value, children }) {
64
75
  return /* @__PURE__ */ React.createElement(RetorBridgeContext.Provider, { value }, children);
65
76
  }
77
+ function RetorProgressProvider({ value, children }) {
78
+ return /* @__PURE__ */ React.createElement(RetorProgressContext.Provider, { value }, children);
79
+ }
66
80
 
67
81
  // src/Viewer.tsx
68
82
  import React2, {
@@ -150,6 +164,8 @@ var Viewer = forwardRef(function Viewer2({ projectId, id = "default", baseUrl =
150
164
  const [activeLineId, setActiveLineId] = useState(null);
151
165
  const [closestTagId, setClosestTagId] = useState(null);
152
166
  const [progress, setProgress] = useState(0);
167
+ const [targetPosition, setTargetPosition] = useState(null);
168
+ const [distanceFromStart, setDistanceFromStart] = useState(null);
153
169
  const [isPlaying, setIsPlaying] = useState(false);
154
170
  const [isAddNoteOpen, setIsAddNoteOpen] = useState(false);
155
171
  const [addNoteTagId, setAddNoteTagId] = useState(null);
@@ -191,8 +207,11 @@ var Viewer = forwardRef(function Viewer2({ projectId, id = "default", baseUrl =
191
207
  const closestTagIdRef = useRef(closestTagId);
192
208
  const addNoteTagIdRef = useRef(addNoteTagId);
193
209
  const activeLineIdRef = useRef(activeLineId);
194
- const linesRef = useRef(lines);
195
210
  const onNoteSubmitRef = useRef(onNoteSubmit);
211
+ const noteSnapshotRef = useRef({ progress: 0, targetPosition: null, distanceFromStart: null });
212
+ const progressRef = useRef(progress);
213
+ const targetPositionRef = useRef(targetPosition);
214
+ const distanceFromStartRef = useRef(distanceFromStart);
196
215
  useEffect(() => {
197
216
  closestTagIdRef.current = closestTagId;
198
217
  }, [closestTagId]);
@@ -202,14 +221,25 @@ var Viewer = forwardRef(function Viewer2({ projectId, id = "default", baseUrl =
202
221
  useEffect(() => {
203
222
  activeLineIdRef.current = activeLineId;
204
223
  }, [activeLineId]);
205
- useEffect(() => {
206
- linesRef.current = lines;
207
- }, [lines]);
208
224
  useEffect(() => {
209
225
  onNoteSubmitRef.current = onNoteSubmit;
210
226
  }, [onNoteSubmit]);
227
+ useEffect(() => {
228
+ progressRef.current = progress;
229
+ }, [progress]);
230
+ useEffect(() => {
231
+ targetPositionRef.current = targetPosition;
232
+ }, [targetPosition]);
233
+ useEffect(() => {
234
+ distanceFromStartRef.current = distanceFromStart;
235
+ }, [distanceFromStart]);
211
236
  const openAddNote = useCallback((tagId) => {
212
237
  setAddNoteTagId(tagId ?? closestTagIdRef.current ?? null);
238
+ noteSnapshotRef.current = {
239
+ progress: progressRef.current,
240
+ targetPosition: targetPositionRef.current,
241
+ distanceFromStart: distanceFromStartRef.current
242
+ };
213
243
  setIsAddNoteOpen(true);
214
244
  }, []);
215
245
  const closeAddNote = useCallback(() => {
@@ -218,13 +248,15 @@ var Viewer = forwardRef(function Viewer2({ projectId, id = "default", baseUrl =
218
248
  const submitNote = useCallback((text, isPrivate = true) => {
219
249
  const tagId = addNoteTagIdRef.current;
220
250
  const lineId = activeLineIdRef.current;
221
- const tag = linesRef.current.find((l) => l._id === lineId)?.tags.find((t) => t._id === tagId);
251
+ const snap = noteSnapshotRef.current;
222
252
  const payload = {
223
253
  text,
224
254
  isPrivate,
225
255
  tagId,
226
256
  lineId,
227
- position: tag?.position ?? null
257
+ position: snap.targetPosition,
258
+ progress: snap.progress,
259
+ distanceFromStart: snap.distanceFromStart
228
260
  };
229
261
  onNoteSubmitRef.current?.(payload);
230
262
  setIsAddNoteOpen(false);
@@ -265,6 +297,8 @@ var Viewer = forwardRef(function Viewer2({ projectId, id = "default", baseUrl =
265
297
  const payload = data.payload;
266
298
  setProgress(payload.progress);
267
299
  setClosestTagId(payload.closestTagId);
300
+ setTargetPosition(payload.targetPosition ?? null);
301
+ setDistanceFromStart(payload.distanceFromStart ?? null);
268
302
  onLineProgress?.(payload);
269
303
  break;
270
304
  }
@@ -289,20 +323,20 @@ var Viewer = forwardRef(function Viewer2({ projectId, id = "default", baseUrl =
289
323
  lines,
290
324
  activeLineId,
291
325
  activeLine,
292
- closestTagId,
293
- progress,
294
- isPlaying,
295
326
  isAddNoteOpen,
296
327
  addNoteTagId,
297
328
  controls,
298
329
  openAddNote,
299
330
  closeAddNote,
300
- submitNote,
301
- onNoteSubmit
331
+ submitNote
302
332
  }),
303
- [project, lines, activeLineId, activeLine, closestTagId, progress, isPlaying, isAddNoteOpen, addNoteTagId, controls, openAddNote, closeAddNote, submitNote, onNoteSubmit]
333
+ [project, lines, activeLineId, activeLine, isAddNoteOpen, addNoteTagId, controls, openAddNote, closeAddNote, submitNote]
334
+ );
335
+ const progressCtx = useMemo2(
336
+ () => ({ progress, closestTagId, targetPosition, distanceFromStart, isPlaying }),
337
+ [progress, closestTagId, targetPosition, distanceFromStart, isPlaying]
304
338
  );
305
- return /* @__PURE__ */ React2.createElement(RetorBridgeProvider, { value: ctxValue }, /* @__PURE__ */ React2.createElement(View, { style: [styles.root, style] }, /* @__PURE__ */ React2.createElement(
339
+ return /* @__PURE__ */ React2.createElement(RetorBridgeProvider, { value: ctxValue }, /* @__PURE__ */ React2.createElement(RetorProgressProvider, { value: progressCtx }, /* @__PURE__ */ React2.createElement(View, { style: [styles.root, style] }, /* @__PURE__ */ React2.createElement(
306
340
  WebView,
307
341
  {
308
342
  ref: webviewRef,
@@ -315,7 +349,7 @@ var Viewer = forwardRef(function Viewer2({ projectId, id = "default", baseUrl =
315
349
  allowsInlineMediaPlayback: true,
316
350
  mediaPlaybackRequiresUserAction: false
317
351
  }
318
- ), children));
352
+ ), children)));
319
353
  });
320
354
  var styles = StyleSheet.create({
321
355
  root: { flex: 1, position: "relative" },
@@ -554,7 +588,8 @@ function DefaultHeader({
554
588
  return /* @__PURE__ */ React6.createElement(View5, { style: styles3.header }, /* @__PURE__ */ React6.createElement(View5, { style: { flex: 1, minWidth: 0 } }, /* @__PURE__ */ React6.createElement(Text2, { style: styles3.title, numberOfLines: 1 }, line.name), line.subtitle && /* @__PURE__ */ React6.createElement(Text2, { style: styles3.subtitle, numberOfLines: 1 }, line.subtitle), line.description && /* @__PURE__ */ React6.createElement(Text2, { style: styles3.description, numberOfLines: minimized ? 2 : 4 }, line.description)), /* @__PURE__ */ React6.createElement(View5, { style: styles3.headerActions }, /* @__PURE__ */ React6.createElement(AutoplayButton, null), /* @__PURE__ */ React6.createElement(Pressable2, { style: styles3.iconBtn, onPress: onToggleMinimize }, minimized ? /* @__PURE__ */ React6.createElement(ArrowUp2, { size: 14, color: "rgba(255,255,255,0.6)" }) : /* @__PURE__ */ React6.createElement(ArrowDown2, { size: 14, color: "rgba(255,255,255,0.6)" }))));
555
589
  }
556
590
  function AutoplayButton() {
557
- const { isPlaying, progress, controls } = useRetorBridge();
591
+ const { controls } = useRetorBridge();
592
+ const { isPlaying, progress } = useRetorProgress();
558
593
  const r = 12.5;
559
594
  const c = 2 * Math.PI * r;
560
595
  return /* @__PURE__ */ React6.createElement(Pressable2, { style: styles3.iconBtn, onPress: () => controls.toggleAutoplay() }, /* @__PURE__ */ React6.createElement(Svg, { width: 28, height: 28, style: StyleSheet5.absoluteFill }, /* @__PURE__ */ React6.createElement(
@@ -574,7 +609,8 @@ function AutoplayButton() {
574
609
  )), isPlaying ? /* @__PURE__ */ React6.createElement(Pause, { size: 11, color: "white", fill: "white" }) : /* @__PURE__ */ React6.createElement(Play, { size: 11, color: "white", fill: "white", style: { marginLeft: 1 } }));
575
610
  }
576
611
  function LineTagList({ children, listHeader }) {
577
- const { activeLine, closestTagId } = useRetorBridge();
612
+ const { activeLine } = useRetorBridge();
613
+ const { closestTagId } = useRetorProgress();
578
614
  const scrollRef = useRef3(null);
579
615
  const offsetsRef = useRef3(/* @__PURE__ */ new Map());
580
616
  const tags = useMemo4(
@@ -584,20 +620,17 @@ function LineTagList({ children, listHeader }) {
584
620
  useEffect3(() => {
585
621
  offsetsRef.current = /* @__PURE__ */ new Map();
586
622
  }, [activeLine?._id]);
623
+ const lastScrolledIdRef = useRef3(null);
587
624
  useEffect3(() => {
588
- if (!closestTagId) return;
589
- let attempts = 0;
590
- const tick = () => {
625
+ if (!closestTagId || closestTagId === lastScrolledIdRef.current) return;
626
+ const t = setTimeout(() => {
591
627
  const y = offsetsRef.current.get(closestTagId);
592
628
  if (y != null) {
593
629
  scrollRef.current?.scrollTo?.({ y, animated: true });
594
- return;
630
+ lastScrolledIdRef.current = closestTagId;
595
631
  }
596
- attempts += 1;
597
- if (attempts < 10) raf = requestAnimationFrame(tick);
598
- };
599
- let raf = requestAnimationFrame(tick);
600
- return () => cancelAnimationFrame(raf);
632
+ }, 300);
633
+ return () => clearTimeout(t);
601
634
  }, [closestTagId]);
602
635
  if (!activeLine) return null;
603
636
  const handleItemLayout = (id, e) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retor/react-native",
3
- "version": "0.3.6",
3
+ "version": "0.4.0",
4
4
  "description": "React Native SDK for embedding Retor 3D experiences",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",