@netless/app-presentation 0.1.10 → 0.1.12

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.
@@ -1883,10 +1883,79 @@ var NetlessAppPresentation = (function (exports) {
1883
1883
  }
1884
1884
  };
1885
1885
 
1886
+ // src/camera-options.ts
1887
+ function getCameraScaleRange(fitScale, maxCameraScale, disableCameraTransform) {
1888
+ return {
1889
+ minScale: fitScale,
1890
+ maxScale: fitScale * (disableCameraTransform ? 1 : maxCameraScale)
1891
+ };
1892
+ }
1893
+ function shouldDisableDeviceCameraTransform(options) {
1894
+ return Boolean(options.disableCameraTransform || options.disableDeviceCameraTransform);
1895
+ }
1896
+
1897
+ // src/camera-reference.ts
1898
+ function isValidSize(size) {
1899
+ return Boolean(
1900
+ size && Number.isFinite(size.width) && Number.isFinite(size.height) && size.width > 0 && size.height > 0
1901
+ );
1902
+ }
1903
+ function isValidSharedViewport(viewport) {
1904
+ return Boolean(
1905
+ viewport && Number.isFinite(viewport.originX) && Number.isFinite(viewport.originY) && isValidSize(viewport)
1906
+ );
1907
+ }
1908
+ function getCameraReferenceSize(originSize, pageSize) {
1909
+ return isValidSize(originSize) ? originSize : pageSize;
1910
+ }
1911
+ function fitPageSizeToOrigin(pageSize, originSize) {
1912
+ if (!isValidSize(pageSize) || !isValidSize(originSize))
1913
+ return __spreadValues({}, pageSize);
1914
+ const ratio = Math.min(
1915
+ originSize.width / pageSize.width,
1916
+ originSize.height / pageSize.height
1917
+ );
1918
+ if (!(Number.isFinite(ratio) && ratio > 0))
1919
+ return __spreadValues({}, pageSize);
1920
+ return {
1921
+ width: pageSize.width * ratio,
1922
+ height: pageSize.height * ratio
1923
+ };
1924
+ }
1925
+ function getFitScale(viewSize, referenceSize) {
1926
+ if (!isValidSize(viewSize) || !isValidSize(referenceSize))
1927
+ return;
1928
+ const scale = Math.min(
1929
+ viewSize.width / referenceSize.width,
1930
+ viewSize.height / referenceSize.height
1931
+ );
1932
+ return Number.isFinite(scale) && scale > 0 ? scale : void 0;
1933
+ }
1934
+ function cameraToSharedViewport(camera, viewSize, referenceSize) {
1935
+ const fitScale = getFitScale(viewSize, referenceSize);
1936
+ if (fitScale === void 0 || !Number.isFinite(camera.centerX) || !Number.isFinite(camera.centerY) || !Number.isFinite(camera.scale) || camera.scale <= 0) {
1937
+ return;
1938
+ }
1939
+ const normalizedScale = camera.scale / fitScale;
1940
+ const width = referenceSize.width / normalizedScale;
1941
+ const height = referenceSize.height / normalizedScale;
1942
+ return {
1943
+ originX: camera.centerX - width / 2,
1944
+ originY: camera.centerY - height / 2,
1945
+ width,
1946
+ height
1947
+ };
1948
+ }
1949
+
1886
1950
  // src/app-presentation.ts
1887
1951
  var import_debounce2 = __toESM(require_debounce());
1888
1952
  var emptySceneName = "$$empty$$";
1889
- var ppt2page = (ppt, name) => ppt ? { width: ppt.width, height: ppt.height, src: ppt.src, thumbnail: ppt.previewURL, name } : null;
1953
+ var ppt2page = (ppt, name, originSize) => {
1954
+ if (!ppt)
1955
+ return null;
1956
+ const size = fitPageSizeToOrigin(ppt, originSize);
1957
+ return __spreadProps(__spreadValues({}, size), { src: ppt.src, thumbnail: ppt.previewURL, name });
1958
+ };
1890
1959
  var createLogger = (room) => {
1891
1960
  if (room && room.logger) {
1892
1961
  return (...args) => room.logger.info(...args);
@@ -1894,6 +1963,55 @@ var NetlessAppPresentation = (function (exports) {
1894
1963
  return (...args) => console.log(...args);
1895
1964
  }
1896
1965
  };
1966
+ var createDiagnosticLogger = (context) => {
1967
+ var _a;
1968
+ const createAppLogger = context.createLogger;
1969
+ if (typeof createAppLogger === "function") {
1970
+ return createAppLogger.call(context, "camera", { debounceTime: 300, maxWaitTime: 2e3 });
1971
+ }
1972
+ const roomLogger = (_a = context.getRoom()) == null ? void 0 : _a.logger;
1973
+ const prefix = `[Presentation][${context.appId}][camera]`;
1974
+ const emit = (level, event, ...data) => {
1975
+ try {
1976
+ const printer = roomLogger == null ? void 0 : roomLogger[level];
1977
+ if (typeof printer === "function")
1978
+ printer.call(roomLogger, `${prefix}[${event}]`, ...data);
1979
+ } catch (e) {
1980
+ }
1981
+ };
1982
+ const debouncedByEvent = /* @__PURE__ */ new Map();
1983
+ const debouncedInfo = (event, payload) => {
1984
+ let emitDebounced = debouncedByEvent.get(event);
1985
+ if (!emitDebounced) {
1986
+ emitDebounced = (0, import_debounce2.default)(
1987
+ (nextPayload) => emit("info", event, nextPayload),
1988
+ 300,
1989
+ { maxWait: 2e3 }
1990
+ );
1991
+ debouncedByEvent.set(event, emitDebounced);
1992
+ }
1993
+ emitDebounced(payload);
1994
+ };
1995
+ return {
1996
+ info: (event, payload) => emit("info", event, payload),
1997
+ warn: (event, payload) => emit("warn", event, payload),
1998
+ error: (event, error, payload) => emit("error", event, error, payload),
1999
+ debouncedInfo,
2000
+ flush: () => debouncedByEvent.forEach((logger) => logger.flush())
2001
+ };
2002
+ };
2003
+ var safeResourceLocation = (value) => {
2004
+ if (value.startsWith("data:"))
2005
+ return "data:[omitted]";
2006
+ if (value.startsWith("blob:"))
2007
+ return "blob:[omitted]";
2008
+ try {
2009
+ const url = new URL(value);
2010
+ return `${url.origin}${url.pathname}`;
2011
+ } catch (e) {
2012
+ return value.split("?")[0];
2013
+ }
2014
+ };
1897
2015
  var scenesEqual = (scenes1, scenes2) => {
1898
2016
  if (!scenes1 || !scenes2) {
1899
2017
  return false;
@@ -1910,25 +2028,35 @@ var NetlessAppPresentation = (function (exports) {
1910
2028
  kind: "Presentation",
1911
2029
  setup(context) {
1912
2030
  var _a, _b, _c;
2031
+ const diagnosticLogger = createDiagnosticLogger(context);
1913
2032
  const view = context.getView();
1914
2033
  if (!view)
1915
2034
  throw new Error("[Presentation]: no whiteboard view, make sure you have added options.scenePath in addApp()");
1916
- const pages = (_a = context.getScenes()) == null ? void 0 : _a.map(({ ppt, name }) => ppt2page(ppt, name)).filter(Boolean);
2035
+ const options = context.getAppOptions() || {};
2036
+ const room = context.getRoom();
2037
+ const log = options.log || createLogger(room);
2038
+ const roomLogger = room == null ? void 0 : room.logger;
2039
+ const warn = (...data) => (roomLogger == null ? void 0 : roomLogger.warn) ? roomLogger.warn(...data) : log(...data);
2040
+ const configuredOriginSize = context.storage.state.originSize;
2041
+ const originSize = isValidSize(configuredOriginSize) ? { width: configuredOriginSize.width, height: configuredOriginSize.height } : void 0;
2042
+ if (configuredOriginSize != null && !originSize) {
2043
+ warn(`[Presentation] originSize should contain finite positive width and height, got ${JSON.stringify(configuredOriginSize)}`);
2044
+ }
2045
+ const pages = (_a = context.getScenes()) == null ? void 0 : _a.map(({ ppt, name }) => ppt2page(ppt, name, originSize)).filter(Boolean);
1917
2046
  if (!pages || pages.length === 0)
1918
2047
  throw new Error("[Presentation]: empty scenes, make sure you have added options.scenes in addApp()");
1919
2048
  if (pages[0].src.startsWith("ppt"))
1920
2049
  throw new Error("[Presentation]: legacy dynamic PPT is unsupported, please use the projector converter and @netless/slide to render it");
1921
2050
  const scenePath = context.getInitScenePath();
1922
- const options = context.getAppOptions() || {};
1923
2051
  let maxCameraScale = (_b = options.maxCameraScale) != null ? _b : 3;
1924
2052
  if (!(Number.isFinite(maxCameraScale) && maxCameraScale > 0)) {
1925
- console.warn(`[Presentation] maxCameraScale should be a positive number, got ${options.maxCameraScale}`);
2053
+ warn(`[Presentation] maxCameraScale should be a positive number, got ${options.maxCameraScale}`);
1926
2054
  maxCameraScale = 3;
1927
2055
  }
1928
- const log = options.log || createLogger(context.getRoom());
1929
2056
  log(`[Presentation] new ${context.appId}`);
1930
2057
  const dispose2 = disposableStore();
1931
2058
  dispose2.add(() => log(`[Presentation] dispose ${context.appId}`));
2059
+ dispose2.add(() => diagnosticLogger.flush());
1932
2060
  const view$$ = context.createStorage("view", { uid: "", originX: 0, originY: 0, width: 0, height: 0 });
1933
2061
  const _addScenePathListener = (name, listener) => {
1934
2062
  const windowManger = context.manager.windowManger;
@@ -1962,58 +2090,15 @@ var NetlessAppPresentation = (function (exports) {
1962
2090
  dispose2.add(() => {
1963
2091
  pageIndex$.dispose();
1964
2092
  });
1965
- if (context.isAddApp) {
1966
- if (pages.length > 100)
1967
- console.warn(`[Presentation]: too many pages (${pages.length}), may cause performance issues`);
1968
- let redirectResolve = void 0;
1969
- const room2 = context.getRoom();
1970
- if (room2 && room2.isWritable) {
1971
- const scenes = room2.entireScenes()[scenePath];
1972
- if (pageIndex$.value < 0 || pageIndex$.value >= pages.length) {
1973
- throw new Error(`[Presentation] Invalid page index: ${pageIndex$.value}, scenes length: ${pages.length}`);
1974
- }
1975
- new Promise((resolve) => {
1976
- const { name, ppt } = scenes[pageIndex$.value];
1977
- redirectResolve = resolve;
1978
- const _scenes = pages.map((p, index) => {
1979
- var _a2;
1980
- return {
1981
- name: (_a2 = p.name) != null ? _a2 : String(index + 1),
1982
- ppt: { width: p.width, height: p.height, src: p.src }
1983
- };
1984
- });
1985
- if (!scenesEqual(scenes, _scenes)) {
1986
- room2.removeScenes(scenePath);
1987
- room2.putScenes(scenePath, _scenes);
1988
- }
1989
- if (name === _scenes[pageIndex$.value].name && !ppt) {
1990
- context.addPage({ scene: { name: emptySceneName } }).then(() => {
1991
- log(`[Presentation] setup setScenePath ${scenePath}/${emptySceneName}`);
1992
- context.setScenePath(`${scenePath}/${emptySceneName}`).then(() => {
1993
- redirectResolve && redirectResolve(true);
1994
- });
1995
- });
1996
- } else {
1997
- redirectResolve && redirectResolve(false);
1998
- }
1999
- }).then(async (bol) => {
2000
- await syncPage(pageIndex$.value, room2.logger);
2001
- if (bol) {
2002
- log(`[Presentation] setup removeScenes ${scenePath}/${emptySceneName}`);
2003
- room2.removeScenes(`${scenePath}/${emptySceneName}`);
2004
- }
2005
- });
2006
- }
2007
- }
2008
2093
  const me = ((_c = context.getRoom()) == null ? void 0 : _c.uid) || context.getDisplayer().observerId + "";
2009
2094
  let throttleSyncView = 0;
2010
2095
  const syncPage = async (index, logger) => {
2011
2096
  var _a2;
2012
2097
  if (!context.getIsWritable())
2013
- return;
2098
+ return false;
2014
2099
  const scenes = context.getDisplayer().entireScenes()[scenePath];
2015
2100
  if (!scenes)
2016
- return;
2101
+ return false;
2017
2102
  const p = pages[index];
2018
2103
  const name = (_a2 = p.name) != null ? _a2 : String(index + 1);
2019
2104
  if (!scenes.some((scene) => scene.name === name)) {
@@ -2023,57 +2108,149 @@ var NetlessAppPresentation = (function (exports) {
2023
2108
  logger.info(`[Presentation] syncPage ${scenePath}/${name}`);
2024
2109
  }
2025
2110
  await context.setScenePath(`${scenePath}/${name}`);
2111
+ return true;
2026
2112
  };
2027
- const jumpPage = (index) => {
2028
- var _a2;
2113
+ const prepareScenes = async () => {
2114
+ if (!context.isAddApp)
2115
+ return;
2116
+ if (pages.length > 100)
2117
+ warn(`[Presentation]: too many pages (${pages.length}), may cause performance issues`);
2118
+ if (!room || !room.isWritable)
2119
+ return;
2120
+ if (pageIndex$.value < 0 || pageIndex$.value >= pages.length) {
2121
+ throw new Error(`[Presentation] Invalid page index: ${pageIndex$.value}, scenes length: ${pages.length}`);
2122
+ }
2123
+ const scenes = room.entireScenes()[scenePath];
2124
+ if (!scenes || !scenes[pageIndex$.value]) {
2125
+ throw new Error(`[Presentation]: no initial scene found at ${scenePath}, page index: ${pageIndex$.value}`);
2126
+ }
2127
+ const { name, ppt } = scenes[pageIndex$.value];
2128
+ const nextScenes = pages.map((page, index) => {
2129
+ var _a2;
2130
+ return {
2131
+ name: (_a2 = page.name) != null ? _a2 : String(index + 1),
2132
+ ppt: { width: page.width, height: page.height, src: page.src }
2133
+ };
2134
+ });
2135
+ if (!scenesEqual(scenes, nextScenes)) {
2136
+ room.removeScenes(scenePath);
2137
+ room.putScenes(scenePath, nextScenes);
2138
+ }
2139
+ const shouldRedirect = name === nextScenes[pageIndex$.value].name && !ppt;
2140
+ if (shouldRedirect) {
2141
+ await context.addPage({ scene: { name: emptySceneName } });
2142
+ log(`[Presentation] setup setScenePath ${scenePath}/${emptySceneName}`);
2143
+ await context.setScenePath(`${scenePath}/${emptySceneName}`);
2144
+ }
2145
+ await syncPage(pageIndex$.value, room.logger);
2146
+ if (shouldRedirect) {
2147
+ log(`[Presentation] setup removeScenes ${scenePath}/${emptySceneName}`);
2148
+ room.removeScenes(`${scenePath}/${emptySceneName}`);
2149
+ }
2150
+ };
2151
+ const prepareScenesPromise = prepareScenes();
2152
+ const canJumpPage = (index) => {
2029
2153
  if (!context.getIsWritable()) {
2030
- console.warn("[Presentation]: no permission, make sure you have test room.isWritable");
2154
+ warn("[Presentation]: no permission, make sure you have test room.isWritable");
2031
2155
  return false;
2032
2156
  }
2033
2157
  if (!(0 <= index && index < pages.length)) {
2034
- console.warn(`[Presentation]: page ${index + 1} out of bounds [1, ${pages.length}]`);
2158
+ warn(`[Presentation]: page ${index + 1} out of bounds [1, ${pages.length}]`);
2035
2159
  return false;
2036
2160
  }
2037
2161
  const scenes = context.getDisplayer().entireScenes()[scenePath];
2038
2162
  if (!scenes) {
2039
- console.warn(`[Presentation]: no scenes found at ${scenePath}, make sure you have added options.scenePath in addApp()`);
2163
+ warn(`[Presentation]: no scenes found at ${scenePath}, make sure you have added options.scenePath in addApp()`);
2040
2164
  return false;
2041
2165
  }
2042
- const p = pages[index];
2043
- const name = (_a2 = p.name) != null ? _a2 : String(index + 1);
2044
- if (!scenes.some((scene) => scene.name === name)) {
2045
- context.addPage({ scene: { name, ppt: { width: p.width, height: p.height, src: p.src } } });
2046
- }
2047
- syncPage(index);
2166
+ return true;
2167
+ };
2168
+ const jumpPage = (index) => {
2169
+ if (!canJumpPage(index))
2170
+ return false;
2171
+ void syncPage(index).catch((error) => {
2172
+ warn("[Presentation]: failed to sync page", error);
2173
+ diagnosticLogger.error("jumpPage.failed", error, {
2174
+ index,
2175
+ pageIndex: pageIndex$.value,
2176
+ focusScenePath: view.focusScenePath
2177
+ });
2178
+ });
2048
2179
  return true;
2049
2180
  };
2050
2181
  const prevPage = () => jumpPage(pageIndex$.value - 1);
2051
2182
  const nextPage = () => jumpPage(pageIndex$.value + 1);
2183
+ const jumpPageAsync = async (index) => {
2184
+ if (!canJumpPage(index))
2185
+ return false;
2186
+ try {
2187
+ return await syncPage(index);
2188
+ } catch (error) {
2189
+ diagnosticLogger.error("jumpPageAsync.failed", error, {
2190
+ index,
2191
+ pageIndex: pageIndex$.value,
2192
+ focusScenePath: view.focusScenePath
2193
+ });
2194
+ throw error;
2195
+ }
2196
+ };
2197
+ const prevPageAsync = () => jumpPageAsync(pageIndex$.value - 1);
2198
+ const nextPageAsync = () => jumpPageAsync(pageIndex$.value + 1);
2052
2199
  const pageState = () => ({ index: pageIndex$.value, length: pages.length });
2053
2200
  const scaleDocsToFit = () => {
2054
- const { width, height } = app.page() || {};
2055
- if (width && height) {
2201
+ const page = app.page();
2202
+ if (page && isValidSize(page)) {
2203
+ const referenceSize = getCameraReferenceSize(originSize, page);
2204
+ if (originSize) {
2205
+ const fitScale = getFitScale(view.size, referenceSize);
2206
+ if (!fitScale)
2207
+ return;
2208
+ const { minScale, maxScale } = getCameraScaleRange(
2209
+ fitScale,
2210
+ maxCameraScale,
2211
+ options.disableCameraTransform
2212
+ );
2213
+ view.setCameraBound({
2214
+ damping: 1,
2215
+ maxContentMode: () => maxScale,
2216
+ minContentMode: () => minScale,
2217
+ centerX: 0,
2218
+ centerY: 0,
2219
+ width: page.width,
2220
+ height: page.height
2221
+ });
2222
+ if (isValidSharedViewport(view$$.state)) {
2223
+ syncViewFromRemote(true);
2224
+ return;
2225
+ }
2226
+ }
2056
2227
  view.moveCameraToContain({
2057
- originX: -width / 2,
2058
- originY: -height / 2,
2059
- width,
2060
- height,
2228
+ originX: -referenceSize.width / 2,
2229
+ originY: -referenceSize.height / 2,
2230
+ width: referenceSize.width,
2231
+ height: referenceSize.height,
2061
2232
  animationMode: "immediately"
2062
2233
  });
2063
- const maxScale = view.camera.scale * (options.disableCameraTransform ? 1 : maxCameraScale);
2064
- const minScale = view.camera.scale;
2065
- view.setCameraBound({
2066
- damping: 1,
2067
- maxContentMode: () => maxScale,
2068
- minContentMode: () => minScale,
2069
- centerX: 0,
2070
- centerY: 0,
2071
- width,
2072
- height
2073
- });
2234
+ if (!originSize) {
2235
+ const { minScale, maxScale } = getCameraScaleRange(
2236
+ view.camera.scale,
2237
+ maxCameraScale,
2238
+ options.disableCameraTransform
2239
+ );
2240
+ view.setCameraBound({
2241
+ damping: 1,
2242
+ maxContentMode: () => maxScale,
2243
+ minContentMode: () => minScale,
2244
+ centerX: 0,
2245
+ centerY: 0,
2246
+ width: page.width,
2247
+ height: page.height
2248
+ });
2249
+ }
2074
2250
  syncViewFromRemote(true);
2075
2251
  }
2076
2252
  };
2253
+ let pendingMoveCameraRequest;
2077
2254
  const syncView = () => {
2078
2255
  if (context.getIsWritable()) {
2079
2256
  if (options.debounceSync) {
@@ -2082,18 +2259,31 @@ var NetlessAppPresentation = (function (exports) {
2082
2259
  }
2083
2260
  if (throttleSyncView > 0)
2084
2261
  return;
2085
- const { width, height } = app.page() || {};
2086
- if (width && height) {
2262
+ const page = app.page();
2263
+ if (page && isValidSize(page)) {
2087
2264
  throttleSyncView = setTimeout(() => {
2088
2265
  throttleSyncView = 0;
2089
- const { camera, size } = view;
2090
- const fixedW = Math.min(size.width, size.height * width / height);
2091
- const fixedH = Math.min(size.height, size.width * height / width);
2092
- const w = fixedW / camera.scale;
2093
- const h = fixedH / camera.scale;
2094
- const x = camera.centerX - w / 2;
2095
- const y = camera.centerY - h / 2;
2096
- view$$.setState({ uid: me, originX: x, originY: y, width: w, height: h });
2266
+ try {
2267
+ const { camera, size } = view;
2268
+ const referenceSize = getCameraReferenceSize(originSize, page);
2269
+ const viewport = cameraToSharedViewport(camera, size, referenceSize);
2270
+ if (viewport)
2271
+ view$$.setState(__spreadValues({ uid: me }, viewport));
2272
+ if (pendingMoveCameraRequest) {
2273
+ diagnosticLogger.debouncedInfo(
2274
+ "moveCamera",
2275
+ getCameraDiagnosticState("moveCamera", pendingMoveCameraRequest)
2276
+ );
2277
+ pendingMoveCameraRequest = void 0;
2278
+ }
2279
+ } catch (error) {
2280
+ diagnosticLogger.error(
2281
+ "syncView.failed",
2282
+ error,
2283
+ getCameraDiagnosticState("moveCamera", pendingMoveCameraRequest)
2284
+ );
2285
+ pendingMoveCameraRequest = void 0;
2286
+ }
2097
2287
  }, 50);
2098
2288
  }
2099
2289
  }
@@ -2101,6 +2291,7 @@ var NetlessAppPresentation = (function (exports) {
2101
2291
  dispose2.add(() => {
2102
2292
  clearTimeout(throttleSyncView);
2103
2293
  throttleSyncView = 0;
2294
+ pendingMoveCameraRequest = void 0;
2104
2295
  });
2105
2296
  const syncViewFromRemote = (force = false, animate = false) => {
2106
2297
  const { uid, originX, originY, width, height } = view$$.state;
@@ -2121,13 +2312,63 @@ var NetlessAppPresentation = (function (exports) {
2121
2312
  };
2122
2313
  const box = context.getBox();
2123
2314
  const app = dispose2.add(createPresentation(box, pages, jumpPage, pageIndex$, view, options.thumbnail, options.useClipView));
2124
- app.contentDOM.dataset.appPresentationVersion = "0.1.10";
2315
+ app.contentDOM.dataset.appPresentationVersion = "0.1.12";
2125
2316
  app.scaleDocsToFit = scaleDocsToFit;
2126
2317
  app.log = log;
2318
+ app.warn = warn;
2319
+ const getCameraDiagnosticState = (reason, requestedCamera) => {
2320
+ const page = app.page();
2321
+ const pageSize = page && isValidSize(page) ? { width: page.width, height: page.height } : void 0;
2322
+ const referenceSize = pageSize ? getCameraReferenceSize(originSize, pageSize) : void 0;
2323
+ const viewSize = { width: view.size.width, height: view.size.height };
2324
+ const viewCamera = {
2325
+ centerX: view.camera.centerX,
2326
+ centerY: view.camera.centerY,
2327
+ scale: view.camera.scale
2328
+ };
2329
+ const sharedViewport = __spreadValues({}, view$$.state);
2330
+ const originScale = referenceSize ? getFitScale(viewSize, referenceSize) : void 0;
2331
+ const normalizedScale = originScale && originScale > 0 ? viewCamera.scale / originScale : void 0;
2332
+ const sharedScaleX = referenceSize && sharedViewport.width > 0 ? referenceSize.width / sharedViewport.width : void 0;
2333
+ const sharedScaleY = referenceSize && sharedViewport.height > 0 ? referenceSize.height / sharedViewport.height : void 0;
2334
+ return {
2335
+ reason,
2336
+ requestedCamera,
2337
+ storageOriginSize: context.storage.state.originSize,
2338
+ sharedViewport,
2339
+ pageSize,
2340
+ referenceSize,
2341
+ viewSize,
2342
+ viewCamera,
2343
+ originScale,
2344
+ normalizedScale,
2345
+ sharedScaleX,
2346
+ sharedScaleY,
2347
+ focusScenePath: view.focusScenePath,
2348
+ isWritable: context.getIsWritable()
2349
+ };
2350
+ };
2351
+ let didReportInitializedCamera = false;
2352
+ const reportInitializedCamera = (source) => {
2353
+ if (didReportInitializedCamera || !isValidSize(view.size) || !isValidSize(app.page()))
2354
+ return;
2355
+ didReportInitializedCamera = true;
2356
+ diagnosticLogger.info("initialize", __spreadValues({
2357
+ source
2358
+ }, getCameraDiagnosticState("initialize")));
2359
+ };
2360
+ if (originSize) {
2361
+ let previousPageIndex = pageIndex$.value;
2362
+ dispose2.add(pageIndex$.subscribe((nextPageIndex) => {
2363
+ if (nextPageIndex === previousPageIndex)
2364
+ return;
2365
+ previousPageIndex = nextPageIndex;
2366
+ scaleDocsToFit();
2367
+ }));
2368
+ }
2127
2369
  if (options.justDocsViewReadonly) {
2128
2370
  app.setDocsViewReadonly(true);
2129
2371
  }
2130
- const room = context.getRoom();
2131
2372
  const goToPageByClick = () => {
2132
2373
  var _a2, _b2, _c2, _d;
2133
2374
  const currentApplianceName = (_d = (_c2 = (_b2 = (_a2 = context.getRoom()) == null ? void 0 : _a2.state) == null ? void 0 : _b2.memberState) == null ? void 0 : _c2.currentApplianceName) != null ? _d : "";
@@ -2142,13 +2383,17 @@ var NetlessAppPresentation = (function (exports) {
2142
2383
  });
2143
2384
  }
2144
2385
  context.mountView(app.whiteboardDOM);
2145
- if (options.disableCameraTransform) {
2386
+ if (shouldDisableDeviceCameraTransform(options)) {
2146
2387
  view.disableCameraTransform = true;
2147
2388
  }
2148
2389
  scaleDocsToFit();
2149
2390
  dispose2.make(() => {
2150
- view.callbacks.on("onSizeUpdated", scaleDocsToFit);
2151
- return () => view.callbacks.off("onSizeUpdated", scaleDocsToFit);
2391
+ const onSizeUpdated = () => {
2392
+ scaleDocsToFit();
2393
+ reportInitializedCamera("onSizeUpdated");
2394
+ };
2395
+ view.callbacks.on("onSizeUpdated", onSizeUpdated);
2396
+ return () => view.callbacks.off("onSizeUpdated", onSizeUpdated);
2152
2397
  });
2153
2398
  if (options.viewport && context.isAddApp && app.page()) {
2154
2399
  const page = app.page();
@@ -2167,20 +2412,20 @@ var NetlessAppPresentation = (function (exports) {
2167
2412
  return () => view.callbacks.off("onCameraUpdatedByDevice", syncView);
2168
2413
  });
2169
2414
  syncViewFromRemote(true);
2415
+ reportInitializedCamera("setup");
2170
2416
  dispose2.add(context.emitter.on("writableChange", (isWritable) => {
2171
2417
  app.setReadonly(!isWritable);
2172
2418
  }));
2173
2419
  const getOriginScale = () => {
2174
- const { size } = view;
2175
- const { width, height } = getPageSize();
2176
- return Math.min(size.height / height, size.width / width);
2420
+ const page = app.page();
2421
+ if (!page || !isValidSize(page))
2422
+ return 0;
2423
+ return getFitScale(view.size, getCameraReferenceSize(originSize, page)) || 0;
2177
2424
  };
2178
2425
  const getScale = () => {
2179
2426
  return view.camera.scale;
2180
2427
  };
2181
- const screenshotCurrentPageAsync = async (_context, _width, _height) => {
2182
- var _a2, _b2;
2183
- (_b2 = (_a2 = room == null ? void 0 : room.calibrationTimestamp) == null ? void 0 : _a2.toString()) != null ? _b2 : Date.now().toString();
2428
+ const screenshotCurrentPage = async (_context, _width, _height) => {
2184
2429
  const currentPage = pages[pageIndex$.value];
2185
2430
  if (!currentPage) {
2186
2431
  throw new Error("[Presentation]: current page not found");
@@ -2190,8 +2435,11 @@ var NetlessAppPresentation = (function (exports) {
2190
2435
  img.width = width;
2191
2436
  img.height = height;
2192
2437
  img.crossOrigin = "Anonymous";
2193
- await new Promise((resolve) => {
2194
- img.onload = resolve;
2438
+ await new Promise((resolve, reject) => {
2439
+ img.onload = () => resolve();
2440
+ img.onerror = () => reject(new Error(
2441
+ `[Presentation]: failed to load screenshot page image: ${safeResourceLocation(src)}`
2442
+ ));
2195
2443
  img.src = src;
2196
2444
  });
2197
2445
  _context.drawImage(img, 0, 0, width, height, 0, 0, _width || width, _height || height);
@@ -2214,6 +2462,18 @@ var NetlessAppPresentation = (function (exports) {
2214
2462
  });
2215
2463
  }
2216
2464
  };
2465
+ const screenshotCurrentPageAsync = async (_context, _width, _height) => {
2466
+ try {
2467
+ await screenshotCurrentPage(_context, _width, _height);
2468
+ } catch (error) {
2469
+ diagnosticLogger.error("screenshotCurrentPage.failed", error, {
2470
+ pageIndex: pageIndex$.value,
2471
+ outputSize: { width: _width, height: _height },
2472
+ camera: getCameraDiagnosticState("initialize")
2473
+ });
2474
+ throw error;
2475
+ }
2476
+ };
2217
2477
  let scrollbar;
2218
2478
  if (options.useScrollbar) {
2219
2479
  dispose2.make(() => {
@@ -2229,14 +2489,28 @@ var NetlessAppPresentation = (function (exports) {
2229
2489
  });
2230
2490
  }
2231
2491
  const moveCamera = (camera) => {
2232
- if (context.getIsWritable() && scrollbar) {
2233
- if (!scrollbar) {
2234
- throw new Error("[Presentation]: moveCamera must be called when appOptions: useScrollbar is true");
2492
+ try {
2493
+ if (!context.getIsWritable()) {
2494
+ throw new Error("[Presentation]: moveCamera must be called in writable room");
2235
2495
  }
2236
- scrollbar.moveCamera(camera);
2237
- return;
2496
+ pendingMoveCameraRequest = __spreadValues({}, camera);
2497
+ if (scrollbar) {
2498
+ scrollbar.moveCamera(camera);
2499
+ return;
2500
+ }
2501
+ view.moveCamera(__spreadProps(__spreadValues({}, camera), {
2502
+ animationMode: "immediately"
2503
+ }));
2504
+ syncView();
2505
+ } catch (error) {
2506
+ pendingMoveCameraRequest = void 0;
2507
+ diagnosticLogger.error(
2508
+ "moveCamera.failed",
2509
+ error,
2510
+ getCameraDiagnosticState("moveCamera", camera)
2511
+ );
2512
+ throw error;
2238
2513
  }
2239
- throw new Error("[Presentation]: moveCamera must be called in writable room");
2240
2514
  };
2241
2515
  context.emitter.on("destroy", () => dispose2());
2242
2516
  const reportProgress = (progress, result) => {
@@ -2251,8 +2525,11 @@ var NetlessAppPresentation = (function (exports) {
2251
2525
  } catch (e) {
2252
2526
  }
2253
2527
  const data = await fetch(url);
2254
- if (!data.ok)
2255
- throw new Error(`[Presentation]: failed to fetch ${url} - ${await data.text()}`);
2528
+ if (!data.ok) {
2529
+ throw new Error(
2530
+ `[Presentation]: failed to fetch ${safeResourceLocation(url)}, status: ${data.status} ${data.statusText}`
2531
+ );
2532
+ }
2256
2533
  const blob = await data.blob();
2257
2534
  const reader = new FileReader();
2258
2535
  return new Promise((resolve, reject) => {
@@ -2261,7 +2538,7 @@ var NetlessAppPresentation = (function (exports) {
2261
2538
  reader.readAsDataURL(blob);
2262
2539
  });
2263
2540
  };
2264
- const toPdf = async () => {
2541
+ const toPdfInternal = async () => {
2265
2542
  var _a2;
2266
2543
  const MAX = 1920;
2267
2544
  const firstPage = pages[0];
@@ -2276,6 +2553,9 @@ var NetlessAppPresentation = (function (exports) {
2276
2553
  pdfWidth = Math.floor(width * pdfHeight / height);
2277
2554
  }
2278
2555
  const scenes = context.getDisplayer().entireScenes()[scenePath];
2556
+ if (!scenes) {
2557
+ throw new Error(`[Presentation]: no scenes found while exporting PDF: ${scenePath}`);
2558
+ }
2279
2559
  const stage_canvas = document.createElement("canvas");
2280
2560
  stage_canvas.width = pdfWidth;
2281
2561
  stage_canvas.height = pdfHeight;
@@ -2297,11 +2577,14 @@ var NetlessAppPresentation = (function (exports) {
2297
2577
  const { width: width2, height: height2, src } = p;
2298
2578
  const url = await base64url(src);
2299
2579
  const img = document.createElement("img");
2300
- await new Promise((resolve) => {
2301
- img.onload = resolve;
2580
+ await new Promise((resolve, reject) => {
2581
+ img.onload = () => resolve();
2582
+ img.onerror = () => reject(new Error(
2583
+ `[Presentation]: failed to load PDF page image, page index: ${index}`
2584
+ ));
2302
2585
  img.src = url;
2303
2586
  });
2304
- stage.drawImage(img, 0, 0);
2587
+ stage.drawImage(img, 0, 0, width2, height2);
2305
2588
  wb.clearRect(0, 0, pdfWidth, pdfHeight);
2306
2589
  const name = (_a2 = p.name) != null ? _a2 : String(index + 1);
2307
2590
  if (scenes.some((scene) => scene.name == name)) {
@@ -2328,7 +2611,7 @@ var NetlessAppPresentation = (function (exports) {
2328
2611
  });
2329
2612
  stage.drawImage(wb_img, 0, 0, pdfWidth, pdfHeight);
2330
2613
  } catch (err) {
2331
- console.warn(err);
2614
+ warn(err);
2332
2615
  }
2333
2616
  }
2334
2617
  const output = stage_canvas.toDataURL("image/jpeg", 0.6);
@@ -2344,10 +2627,22 @@ var NetlessAppPresentation = (function (exports) {
2344
2627
  const title = box.title;
2345
2628
  return reportProgress(100, { pdf: data, title });
2346
2629
  };
2630
+ const toPdf = async () => {
2631
+ try {
2632
+ return await toPdfInternal();
2633
+ } catch (error) {
2634
+ diagnosticLogger.error("toPdf.failed", error, {
2635
+ pageIndex: pageIndex$.value,
2636
+ pageCount: pages.length,
2637
+ focusScenePath: view.focusScenePath
2638
+ });
2639
+ throw error;
2640
+ }
2641
+ };
2347
2642
  dispose2.add(listen(window, "message", (ev) => {
2348
2643
  if (ev.data && ev.data.type == "@netless/_request_save_pdf_" && ev.data.appId == context.appId) {
2349
2644
  toPdf().catch((err) => {
2350
- console.warn(err);
2645
+ warn(err);
2351
2646
  reportProgress(100, null);
2352
2647
  });
2353
2648
  }
@@ -2362,7 +2657,7 @@ var NetlessAppPresentation = (function (exports) {
2362
2657
  scrollbar.setReadonly(bol);
2363
2658
  }
2364
2659
  };
2365
- const controller = { app, view, context, jumpPage, prevPage, nextPage, pageState, toPdf, log, setDocsViewReadonly, setReadonly, moveCamera, getOriginScale, getScale, getPageSize, screenshotCurrentPageAsync };
2660
+ const controller = { app, view, context, jumpPage, prevPage, nextPage, jumpPageAsync, prevPageAsync, nextPageAsync, pageState, toPdf, log, setDocsViewReadonly, setReadonly, moveCamera, getOriginScale, getScale, getPageSize, screenshotCurrentPageAsync };
2366
2661
  dispose2.add(listen(window, "message", (ev) => {
2367
2662
  if (ev.data === "@netless/_presentation_") {
2368
2663
  if (typeof window !== "undefined")
@@ -2374,7 +2669,7 @@ var NetlessAppPresentation = (function (exports) {
2374
2669
  console.log(controller);
2375
2670
  }
2376
2671
  }));
2377
- return controller;
2672
+ return prepareScenesPromise.then(() => controller);
2378
2673
  }
2379
2674
  };
2380
2675
  var AppPresentation = class extends Presentation {
@@ -2386,6 +2681,7 @@ var NetlessAppPresentation = (function (exports) {
2386
2681
  super.updateImage();
2387
2682
  }
2388
2683
  onNewPageIndex(index, origin) {
2684
+ var _a;
2389
2685
  if (origin === "keydown" && this.box && !this.box.focus)
2390
2686
  return;
2391
2687
  if (this.log)
@@ -2393,7 +2689,7 @@ var NetlessAppPresentation = (function (exports) {
2393
2689
  if (0 <= index && index < this.pages.length) {
2394
2690
  this.jumpPage(index);
2395
2691
  } else {
2396
- console.warn(`[Presentation]: page index ${index} out of bounds [0, ${this.pages.length - 1}]`);
2692
+ (_a = this.warn) == null ? void 0 : _a.call(this, `[Presentation]: page index ${index} out of bounds [0, ${this.pages.length - 1}]`);
2397
2693
  }
2398
2694
  }
2399
2695
  };
@@ -2440,7 +2736,7 @@ var NetlessAppPresentation = (function (exports) {
2440
2736
  };
2441
2737
 
2442
2738
  // src/index.ts
2443
- var version = "0.1.10";
2739
+ var version = "0.1.12";
2444
2740
 
2445
2741
  exports.NetlessAppPresentation = NetlessAppPresentation;
2446
2742
  exports.Presentation = Presentation;