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