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