@netless/app-presentation 0.1.10 → 0.1.11

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,80 @@ 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 ORIGIN_SIZE_COORDINATE_VERSION = 2;
1955
+ var ppt2page = (ppt, name, originSize) => {
1956
+ if (!ppt)
1957
+ return null;
1958
+ const size = fitPageSizeToOrigin(ppt, originSize);
1959
+ return __spreadProps(__spreadValues({}, size), { src: ppt.src, thumbnail: ppt.previewURL, name });
1960
+ };
1891
1961
  var createLogger = (room) => {
1892
1962
  if (room && room.logger) {
1893
1963
  return (...args) => room.logger.info(...args);
@@ -1895,6 +1965,55 @@ var createLogger = (room) => {
1895
1965
  return (...args) => console.log(...args);
1896
1966
  }
1897
1967
  };
1968
+ var createDiagnosticLogger = (context) => {
1969
+ var _a;
1970
+ const createAppLogger = context.createLogger;
1971
+ if (typeof createAppLogger === "function") {
1972
+ return createAppLogger.call(context, "camera", { debounceTime: 300, maxWaitTime: 2e3 });
1973
+ }
1974
+ const roomLogger = (_a = context.getRoom()) == null ? void 0 : _a.logger;
1975
+ const prefix = `[Presentation][${context.appId}][camera]`;
1976
+ const emit = (level, event, ...data) => {
1977
+ try {
1978
+ const printer = roomLogger == null ? void 0 : roomLogger[level];
1979
+ if (typeof printer === "function")
1980
+ printer.call(roomLogger, `${prefix}[${event}]`, ...data);
1981
+ } catch (e) {
1982
+ }
1983
+ };
1984
+ const debouncedByEvent = /* @__PURE__ */ new Map();
1985
+ const debouncedInfo = (event, payload) => {
1986
+ let emitDebounced = debouncedByEvent.get(event);
1987
+ if (!emitDebounced) {
1988
+ emitDebounced = (0, import_debounce2.default)(
1989
+ (nextPayload) => emit("info", event, nextPayload),
1990
+ 300,
1991
+ { maxWait: 2e3 }
1992
+ );
1993
+ debouncedByEvent.set(event, emitDebounced);
1994
+ }
1995
+ emitDebounced(payload);
1996
+ };
1997
+ return {
1998
+ info: (event, payload) => emit("info", event, payload),
1999
+ warn: (event, payload) => emit("warn", event, payload),
2000
+ error: (event, error, payload) => emit("error", event, error, payload),
2001
+ debouncedInfo,
2002
+ flush: () => debouncedByEvent.forEach((logger) => logger.flush())
2003
+ };
2004
+ };
2005
+ var safeResourceLocation = (value) => {
2006
+ if (value.startsWith("data:"))
2007
+ return "data:[omitted]";
2008
+ if (value.startsWith("blob:"))
2009
+ return "blob:[omitted]";
2010
+ try {
2011
+ const url = new URL(value);
2012
+ return `${url.origin}${url.pathname}`;
2013
+ } catch (e) {
2014
+ return value.split("?")[0];
2015
+ }
2016
+ };
1898
2017
  var scenesEqual = (scenes1, scenes2) => {
1899
2018
  if (!scenes1 || !scenes2) {
1900
2019
  return false;
@@ -1911,25 +2030,43 @@ var NetlessAppPresentation = {
1911
2030
  kind: "Presentation",
1912
2031
  setup(context) {
1913
2032
  var _a, _b, _c;
2033
+ const diagnosticLogger = createDiagnosticLogger(context);
1914
2034
  const view = context.getView();
1915
2035
  if (!view)
1916
2036
  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);
2037
+ const options = context.getAppOptions() || {};
2038
+ const room = context.getRoom();
2039
+ const log = options.log || createLogger(room);
2040
+ const roomLogger = room == null ? void 0 : room.logger;
2041
+ const warn = (...data) => (roomLogger == null ? void 0 : roomLogger.warn) ? roomLogger.warn(...data) : log(...data);
2042
+ const configuredOriginSize = context.storage.state.originSize;
2043
+ const originSize = isValidSize(configuredOriginSize) ? { width: configuredOriginSize.width, height: configuredOriginSize.height } : void 0;
2044
+ if (configuredOriginSize != null && !originSize) {
2045
+ warn(`[Presentation] originSize should contain finite positive width and height, got ${JSON.stringify(configuredOriginSize)}`);
2046
+ }
2047
+ const useOriginSizeCoordinates = Boolean(
2048
+ originSize && (context.isAddApp || context.storage.state._originSizeCoordinateVersion === ORIGIN_SIZE_COORDINATE_VERSION)
2049
+ );
2050
+ if (originSize && context.isAddApp && context.getIsWritable()) {
2051
+ context.storage.setState({
2052
+ _originSizeCoordinateVersion: ORIGIN_SIZE_COORDINATE_VERSION
2053
+ });
2054
+ }
2055
+ const pages = (_a = context.getScenes()) == null ? void 0 : _a.map(({ ppt, name }) => ppt2page(ppt, name, useOriginSizeCoordinates ? originSize : void 0)).filter(Boolean);
1918
2056
  if (!pages || pages.length === 0)
1919
2057
  throw new Error("[Presentation]: empty scenes, make sure you have added options.scenes in addApp()");
1920
2058
  if (pages[0].src.startsWith("ppt"))
1921
2059
  throw new Error("[Presentation]: legacy dynamic PPT is unsupported, please use the projector converter and @netless/slide to render it");
1922
2060
  const scenePath = context.getInitScenePath();
1923
- const options = context.getAppOptions() || {};
1924
2061
  let maxCameraScale = (_b = options.maxCameraScale) != null ? _b : 3;
1925
2062
  if (!(Number.isFinite(maxCameraScale) && maxCameraScale > 0)) {
1926
- console.warn(`[Presentation] maxCameraScale should be a positive number, got ${options.maxCameraScale}`);
2063
+ warn(`[Presentation] maxCameraScale should be a positive number, got ${options.maxCameraScale}`);
1927
2064
  maxCameraScale = 3;
1928
2065
  }
1929
- const log = options.log || createLogger(context.getRoom());
1930
2066
  log(`[Presentation] new ${context.appId}`);
1931
2067
  const dispose2 = disposableStore();
1932
2068
  dispose2.add(() => log(`[Presentation] dispose ${context.appId}`));
2069
+ dispose2.add(() => diagnosticLogger.flush());
1933
2070
  const view$$ = context.createStorage("view", { uid: "", originX: 0, originY: 0, width: 0, height: 0 });
1934
2071
  const _addScenePathListener = (name, listener) => {
1935
2072
  const windowManger = context.manager.windowManger;
@@ -1963,58 +2100,15 @@ var NetlessAppPresentation = {
1963
2100
  dispose2.add(() => {
1964
2101
  pageIndex$.dispose();
1965
2102
  });
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
2103
  const me = ((_c = context.getRoom()) == null ? void 0 : _c.uid) || context.getDisplayer().observerId + "";
2010
2104
  let throttleSyncView = 0;
2011
2105
  const syncPage = async (index, logger) => {
2012
2106
  var _a2;
2013
2107
  if (!context.getIsWritable())
2014
- return;
2108
+ return false;
2015
2109
  const scenes = context.getDisplayer().entireScenes()[scenePath];
2016
2110
  if (!scenes)
2017
- return;
2111
+ return false;
2018
2112
  const p = pages[index];
2019
2113
  const name = (_a2 = p.name) != null ? _a2 : String(index + 1);
2020
2114
  if (!scenes.some((scene) => scene.name === name)) {
@@ -2024,57 +2118,149 @@ var NetlessAppPresentation = {
2024
2118
  logger.info(`[Presentation] syncPage ${scenePath}/${name}`);
2025
2119
  }
2026
2120
  await context.setScenePath(`${scenePath}/${name}`);
2121
+ return true;
2027
2122
  };
2028
- const jumpPage = (index) => {
2029
- var _a2;
2123
+ const prepareScenes = async () => {
2124
+ if (!context.isAddApp)
2125
+ return;
2126
+ if (pages.length > 100)
2127
+ warn(`[Presentation]: too many pages (${pages.length}), may cause performance issues`);
2128
+ if (!room || !room.isWritable)
2129
+ return;
2130
+ if (pageIndex$.value < 0 || pageIndex$.value >= pages.length) {
2131
+ throw new Error(`[Presentation] Invalid page index: ${pageIndex$.value}, scenes length: ${pages.length}`);
2132
+ }
2133
+ const scenes = room.entireScenes()[scenePath];
2134
+ if (!scenes || !scenes[pageIndex$.value]) {
2135
+ throw new Error(`[Presentation]: no initial scene found at ${scenePath}, page index: ${pageIndex$.value}`);
2136
+ }
2137
+ const { name, ppt } = scenes[pageIndex$.value];
2138
+ const nextScenes = pages.map((page, index) => {
2139
+ var _a2;
2140
+ return {
2141
+ name: (_a2 = page.name) != null ? _a2 : String(index + 1),
2142
+ ppt: { width: page.width, height: page.height, src: page.src }
2143
+ };
2144
+ });
2145
+ if (!scenesEqual(scenes, nextScenes)) {
2146
+ room.removeScenes(scenePath);
2147
+ room.putScenes(scenePath, nextScenes);
2148
+ }
2149
+ const shouldRedirect = name === nextScenes[pageIndex$.value].name && !ppt;
2150
+ if (shouldRedirect) {
2151
+ await context.addPage({ scene: { name: emptySceneName } });
2152
+ log(`[Presentation] setup setScenePath ${scenePath}/${emptySceneName}`);
2153
+ await context.setScenePath(`${scenePath}/${emptySceneName}`);
2154
+ }
2155
+ await syncPage(pageIndex$.value, room.logger);
2156
+ if (shouldRedirect) {
2157
+ log(`[Presentation] setup removeScenes ${scenePath}/${emptySceneName}`);
2158
+ room.removeScenes(`${scenePath}/${emptySceneName}`);
2159
+ }
2160
+ };
2161
+ const prepareScenesPromise = prepareScenes();
2162
+ const canJumpPage = (index) => {
2030
2163
  if (!context.getIsWritable()) {
2031
- console.warn("[Presentation]: no permission, make sure you have test room.isWritable");
2164
+ warn("[Presentation]: no permission, make sure you have test room.isWritable");
2032
2165
  return false;
2033
2166
  }
2034
2167
  if (!(0 <= index && index < pages.length)) {
2035
- console.warn(`[Presentation]: page ${index + 1} out of bounds [1, ${pages.length}]`);
2168
+ warn(`[Presentation]: page ${index + 1} out of bounds [1, ${pages.length}]`);
2036
2169
  return false;
2037
2170
  }
2038
2171
  const scenes = context.getDisplayer().entireScenes()[scenePath];
2039
2172
  if (!scenes) {
2040
- console.warn(`[Presentation]: no scenes found at ${scenePath}, make sure you have added options.scenePath in addApp()`);
2173
+ warn(`[Presentation]: no scenes found at ${scenePath}, make sure you have added options.scenePath in addApp()`);
2041
2174
  return false;
2042
2175
  }
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);
2176
+ return true;
2177
+ };
2178
+ const jumpPage = (index) => {
2179
+ if (!canJumpPage(index))
2180
+ return false;
2181
+ void syncPage(index).catch((error) => {
2182
+ warn("[Presentation]: failed to sync page", error);
2183
+ diagnosticLogger.error("jumpPage.failed", error, {
2184
+ index,
2185
+ pageIndex: pageIndex$.value,
2186
+ focusScenePath: view.focusScenePath
2187
+ });
2188
+ });
2049
2189
  return true;
2050
2190
  };
2051
2191
  const prevPage = () => jumpPage(pageIndex$.value - 1);
2052
2192
  const nextPage = () => jumpPage(pageIndex$.value + 1);
2193
+ const jumpPageAsync = async (index) => {
2194
+ if (!canJumpPage(index))
2195
+ return false;
2196
+ try {
2197
+ return await syncPage(index);
2198
+ } catch (error) {
2199
+ diagnosticLogger.error("jumpPageAsync.failed", error, {
2200
+ index,
2201
+ pageIndex: pageIndex$.value,
2202
+ focusScenePath: view.focusScenePath
2203
+ });
2204
+ throw error;
2205
+ }
2206
+ };
2207
+ const prevPageAsync = () => jumpPageAsync(pageIndex$.value - 1);
2208
+ const nextPageAsync = () => jumpPageAsync(pageIndex$.value + 1);
2053
2209
  const pageState = () => ({ index: pageIndex$.value, length: pages.length });
2054
2210
  const scaleDocsToFit = () => {
2055
- const { width, height } = app.page() || {};
2056
- if (width && height) {
2211
+ const page = app.page();
2212
+ if (page && isValidSize(page)) {
2213
+ const referenceSize = getCameraReferenceSize(originSize, page);
2214
+ if (originSize) {
2215
+ const fitScale = getFitScale(view.size, referenceSize);
2216
+ if (!fitScale)
2217
+ return;
2218
+ const { minScale, maxScale } = getCameraScaleRange(
2219
+ fitScale,
2220
+ maxCameraScale,
2221
+ options.disableCameraTransform
2222
+ );
2223
+ view.setCameraBound({
2224
+ damping: 1,
2225
+ maxContentMode: () => maxScale,
2226
+ minContentMode: () => minScale,
2227
+ centerX: 0,
2228
+ centerY: 0,
2229
+ width: page.width,
2230
+ height: page.height
2231
+ });
2232
+ if (isValidSharedViewport(view$$.state)) {
2233
+ syncViewFromRemote(true);
2234
+ return;
2235
+ }
2236
+ }
2057
2237
  view.moveCameraToContain({
2058
- originX: -width / 2,
2059
- originY: -height / 2,
2060
- width,
2061
- height,
2238
+ originX: -referenceSize.width / 2,
2239
+ originY: -referenceSize.height / 2,
2240
+ width: referenceSize.width,
2241
+ height: referenceSize.height,
2062
2242
  animationMode: "immediately"
2063
2243
  });
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
- });
2244
+ if (!originSize) {
2245
+ const { minScale, maxScale } = getCameraScaleRange(
2246
+ view.camera.scale,
2247
+ maxCameraScale,
2248
+ options.disableCameraTransform
2249
+ );
2250
+ view.setCameraBound({
2251
+ damping: 1,
2252
+ maxContentMode: () => maxScale,
2253
+ minContentMode: () => minScale,
2254
+ centerX: 0,
2255
+ centerY: 0,
2256
+ width: page.width,
2257
+ height: page.height
2258
+ });
2259
+ }
2075
2260
  syncViewFromRemote(true);
2076
2261
  }
2077
2262
  };
2263
+ let pendingMoveCameraRequest;
2078
2264
  const syncView = () => {
2079
2265
  if (context.getIsWritable()) {
2080
2266
  if (options.debounceSync) {
@@ -2083,18 +2269,31 @@ var NetlessAppPresentation = {
2083
2269
  }
2084
2270
  if (throttleSyncView > 0)
2085
2271
  return;
2086
- const { width, height } = app.page() || {};
2087
- if (width && height) {
2272
+ const page = app.page();
2273
+ if (page && isValidSize(page)) {
2088
2274
  throttleSyncView = setTimeout(() => {
2089
2275
  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 });
2276
+ try {
2277
+ const { camera, size } = view;
2278
+ const referenceSize = getCameraReferenceSize(originSize, page);
2279
+ const viewport = cameraToSharedViewport(camera, size, referenceSize);
2280
+ if (viewport)
2281
+ view$$.setState(__spreadValues({ uid: me }, viewport));
2282
+ if (pendingMoveCameraRequest) {
2283
+ diagnosticLogger.debouncedInfo(
2284
+ "moveCamera",
2285
+ getCameraDiagnosticState("moveCamera", pendingMoveCameraRequest)
2286
+ );
2287
+ pendingMoveCameraRequest = void 0;
2288
+ }
2289
+ } catch (error) {
2290
+ diagnosticLogger.error(
2291
+ "syncView.failed",
2292
+ error,
2293
+ getCameraDiagnosticState("moveCamera", pendingMoveCameraRequest)
2294
+ );
2295
+ pendingMoveCameraRequest = void 0;
2296
+ }
2098
2297
  }, 50);
2099
2298
  }
2100
2299
  }
@@ -2102,6 +2301,7 @@ var NetlessAppPresentation = {
2102
2301
  dispose2.add(() => {
2103
2302
  clearTimeout(throttleSyncView);
2104
2303
  throttleSyncView = 0;
2304
+ pendingMoveCameraRequest = void 0;
2105
2305
  });
2106
2306
  const syncViewFromRemote = (force = false, animate = false) => {
2107
2307
  const { uid, originX, originY, width, height } = view$$.state;
@@ -2122,13 +2322,63 @@ var NetlessAppPresentation = {
2122
2322
  };
2123
2323
  const box = context.getBox();
2124
2324
  const app = dispose2.add(createPresentation(box, pages, jumpPage, pageIndex$, view, options.thumbnail, options.useClipView));
2125
- app.contentDOM.dataset.appPresentationVersion = "0.1.10";
2325
+ app.contentDOM.dataset.appPresentationVersion = "0.1.11";
2126
2326
  app.scaleDocsToFit = scaleDocsToFit;
2127
2327
  app.log = log;
2328
+ app.warn = warn;
2329
+ const getCameraDiagnosticState = (reason, requestedCamera) => {
2330
+ const page = app.page();
2331
+ const pageSize = page && isValidSize(page) ? { width: page.width, height: page.height } : void 0;
2332
+ const referenceSize = pageSize ? getCameraReferenceSize(originSize, pageSize) : void 0;
2333
+ const viewSize = { width: view.size.width, height: view.size.height };
2334
+ const viewCamera = {
2335
+ centerX: view.camera.centerX,
2336
+ centerY: view.camera.centerY,
2337
+ scale: view.camera.scale
2338
+ };
2339
+ const sharedViewport = __spreadValues({}, view$$.state);
2340
+ const originScale = referenceSize ? getFitScale(viewSize, referenceSize) : void 0;
2341
+ const normalizedScale = originScale && originScale > 0 ? viewCamera.scale / originScale : void 0;
2342
+ const sharedScaleX = referenceSize && sharedViewport.width > 0 ? referenceSize.width / sharedViewport.width : void 0;
2343
+ const sharedScaleY = referenceSize && sharedViewport.height > 0 ? referenceSize.height / sharedViewport.height : void 0;
2344
+ return {
2345
+ reason,
2346
+ requestedCamera,
2347
+ storageOriginSize: context.storage.state.originSize,
2348
+ sharedViewport,
2349
+ pageSize,
2350
+ referenceSize,
2351
+ viewSize,
2352
+ viewCamera,
2353
+ originScale,
2354
+ normalizedScale,
2355
+ sharedScaleX,
2356
+ sharedScaleY,
2357
+ focusScenePath: view.focusScenePath,
2358
+ isWritable: context.getIsWritable()
2359
+ };
2360
+ };
2361
+ let didReportInitializedCamera = false;
2362
+ const reportInitializedCamera = (source) => {
2363
+ if (didReportInitializedCamera || !isValidSize(view.size) || !isValidSize(app.page()))
2364
+ return;
2365
+ didReportInitializedCamera = true;
2366
+ diagnosticLogger.info("initialize", __spreadValues({
2367
+ source
2368
+ }, getCameraDiagnosticState("initialize")));
2369
+ };
2370
+ if (originSize) {
2371
+ let previousPageIndex = pageIndex$.value;
2372
+ dispose2.add(pageIndex$.subscribe((nextPageIndex) => {
2373
+ if (nextPageIndex === previousPageIndex)
2374
+ return;
2375
+ previousPageIndex = nextPageIndex;
2376
+ scaleDocsToFit();
2377
+ }));
2378
+ }
2128
2379
  if (options.justDocsViewReadonly) {
2129
2380
  app.setDocsViewReadonly(true);
2130
2381
  }
2131
- const room = context.getRoom();
2132
2382
  const goToPageByClick = () => {
2133
2383
  var _a2, _b2, _c2, _d;
2134
2384
  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 +2393,17 @@ var NetlessAppPresentation = {
2143
2393
  });
2144
2394
  }
2145
2395
  context.mountView(app.whiteboardDOM);
2146
- if (options.disableCameraTransform) {
2396
+ if (shouldDisableDeviceCameraTransform(options)) {
2147
2397
  view.disableCameraTransform = true;
2148
2398
  }
2149
2399
  scaleDocsToFit();
2150
2400
  dispose2.make(() => {
2151
- view.callbacks.on("onSizeUpdated", scaleDocsToFit);
2152
- return () => view.callbacks.off("onSizeUpdated", scaleDocsToFit);
2401
+ const onSizeUpdated = () => {
2402
+ scaleDocsToFit();
2403
+ reportInitializedCamera("onSizeUpdated");
2404
+ };
2405
+ view.callbacks.on("onSizeUpdated", onSizeUpdated);
2406
+ return () => view.callbacks.off("onSizeUpdated", onSizeUpdated);
2153
2407
  });
2154
2408
  if (options.viewport && context.isAddApp && app.page()) {
2155
2409
  const page = app.page();
@@ -2168,20 +2422,20 @@ var NetlessAppPresentation = {
2168
2422
  return () => view.callbacks.off("onCameraUpdatedByDevice", syncView);
2169
2423
  });
2170
2424
  syncViewFromRemote(true);
2425
+ reportInitializedCamera("setup");
2171
2426
  dispose2.add(context.emitter.on("writableChange", (isWritable) => {
2172
2427
  app.setReadonly(!isWritable);
2173
2428
  }));
2174
2429
  const getOriginScale = () => {
2175
- const { size } = view;
2176
- const { width, height } = getPageSize();
2177
- return Math.min(size.height / height, size.width / width);
2430
+ const page = app.page();
2431
+ if (!page || !isValidSize(page))
2432
+ return 0;
2433
+ return getFitScale(view.size, getCameraReferenceSize(originSize, page)) || 0;
2178
2434
  };
2179
2435
  const getScale = () => {
2180
2436
  return view.camera.scale;
2181
2437
  };
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();
2438
+ const screenshotCurrentPage = async (_context, _width, _height) => {
2185
2439
  const currentPage = pages[pageIndex$.value];
2186
2440
  if (!currentPage) {
2187
2441
  throw new Error("[Presentation]: current page not found");
@@ -2191,8 +2445,11 @@ var NetlessAppPresentation = {
2191
2445
  img.width = width;
2192
2446
  img.height = height;
2193
2447
  img.crossOrigin = "Anonymous";
2194
- await new Promise((resolve) => {
2195
- img.onload = resolve;
2448
+ await new Promise((resolve, reject) => {
2449
+ img.onload = () => resolve();
2450
+ img.onerror = () => reject(new Error(
2451
+ `[Presentation]: failed to load screenshot page image: ${safeResourceLocation(src)}`
2452
+ ));
2196
2453
  img.src = src;
2197
2454
  });
2198
2455
  _context.drawImage(img, 0, 0, width, height, 0, 0, _width || width, _height || height);
@@ -2215,6 +2472,18 @@ var NetlessAppPresentation = {
2215
2472
  });
2216
2473
  }
2217
2474
  };
2475
+ const screenshotCurrentPageAsync = async (_context, _width, _height) => {
2476
+ try {
2477
+ await screenshotCurrentPage(_context, _width, _height);
2478
+ } catch (error) {
2479
+ diagnosticLogger.error("screenshotCurrentPage.failed", error, {
2480
+ pageIndex: pageIndex$.value,
2481
+ outputSize: { width: _width, height: _height },
2482
+ camera: getCameraDiagnosticState("initialize")
2483
+ });
2484
+ throw error;
2485
+ }
2486
+ };
2218
2487
  let scrollbar;
2219
2488
  if (options.useScrollbar) {
2220
2489
  dispose2.make(() => {
@@ -2230,14 +2499,28 @@ var NetlessAppPresentation = {
2230
2499
  });
2231
2500
  }
2232
2501
  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");
2502
+ try {
2503
+ if (!context.getIsWritable()) {
2504
+ throw new Error("[Presentation]: moveCamera must be called in writable room");
2236
2505
  }
2237
- scrollbar.moveCamera(camera);
2238
- return;
2506
+ pendingMoveCameraRequest = __spreadValues({}, camera);
2507
+ if (scrollbar) {
2508
+ scrollbar.moveCamera(camera);
2509
+ return;
2510
+ }
2511
+ view.moveCamera(__spreadProps(__spreadValues({}, camera), {
2512
+ animationMode: "immediately"
2513
+ }));
2514
+ syncView();
2515
+ } catch (error) {
2516
+ pendingMoveCameraRequest = void 0;
2517
+ diagnosticLogger.error(
2518
+ "moveCamera.failed",
2519
+ error,
2520
+ getCameraDiagnosticState("moveCamera", camera)
2521
+ );
2522
+ throw error;
2239
2523
  }
2240
- throw new Error("[Presentation]: moveCamera must be called in writable room");
2241
2524
  };
2242
2525
  context.emitter.on("destroy", () => dispose2());
2243
2526
  const reportProgress = (progress, result) => {
@@ -2252,8 +2535,11 @@ var NetlessAppPresentation = {
2252
2535
  } catch (e) {
2253
2536
  }
2254
2537
  const data = await fetch(url);
2255
- if (!data.ok)
2256
- throw new Error(`[Presentation]: failed to fetch ${url} - ${await data.text()}`);
2538
+ if (!data.ok) {
2539
+ throw new Error(
2540
+ `[Presentation]: failed to fetch ${safeResourceLocation(url)}, status: ${data.status} ${data.statusText}`
2541
+ );
2542
+ }
2257
2543
  const blob = await data.blob();
2258
2544
  const reader = new FileReader();
2259
2545
  return new Promise((resolve, reject) => {
@@ -2262,7 +2548,7 @@ var NetlessAppPresentation = {
2262
2548
  reader.readAsDataURL(blob);
2263
2549
  });
2264
2550
  };
2265
- const toPdf = async () => {
2551
+ const toPdfInternal = async () => {
2266
2552
  var _a2;
2267
2553
  const MAX = 1920;
2268
2554
  const firstPage = pages[0];
@@ -2277,6 +2563,9 @@ var NetlessAppPresentation = {
2277
2563
  pdfWidth = Math.floor(width * pdfHeight / height);
2278
2564
  }
2279
2565
  const scenes = context.getDisplayer().entireScenes()[scenePath];
2566
+ if (!scenes) {
2567
+ throw new Error(`[Presentation]: no scenes found while exporting PDF: ${scenePath}`);
2568
+ }
2280
2569
  const stage_canvas = document.createElement("canvas");
2281
2570
  stage_canvas.width = pdfWidth;
2282
2571
  stage_canvas.height = pdfHeight;
@@ -2298,11 +2587,14 @@ var NetlessAppPresentation = {
2298
2587
  const { width: width2, height: height2, src } = p;
2299
2588
  const url = await base64url(src);
2300
2589
  const img = document.createElement("img");
2301
- await new Promise((resolve) => {
2302
- img.onload = resolve;
2590
+ await new Promise((resolve, reject) => {
2591
+ img.onload = () => resolve();
2592
+ img.onerror = () => reject(new Error(
2593
+ `[Presentation]: failed to load PDF page image, page index: ${index}`
2594
+ ));
2303
2595
  img.src = url;
2304
2596
  });
2305
- stage.drawImage(img, 0, 0);
2597
+ stage.drawImage(img, 0, 0, width2, height2);
2306
2598
  wb.clearRect(0, 0, pdfWidth, pdfHeight);
2307
2599
  const name = (_a2 = p.name) != null ? _a2 : String(index + 1);
2308
2600
  if (scenes.some((scene) => scene.name == name)) {
@@ -2329,7 +2621,7 @@ var NetlessAppPresentation = {
2329
2621
  });
2330
2622
  stage.drawImage(wb_img, 0, 0, pdfWidth, pdfHeight);
2331
2623
  } catch (err) {
2332
- console.warn(err);
2624
+ warn(err);
2333
2625
  }
2334
2626
  }
2335
2627
  const output = stage_canvas.toDataURL("image/jpeg", 0.6);
@@ -2345,10 +2637,22 @@ var NetlessAppPresentation = {
2345
2637
  const title = box.title;
2346
2638
  return reportProgress(100, { pdf: data, title });
2347
2639
  };
2640
+ const toPdf = async () => {
2641
+ try {
2642
+ return await toPdfInternal();
2643
+ } catch (error) {
2644
+ diagnosticLogger.error("toPdf.failed", error, {
2645
+ pageIndex: pageIndex$.value,
2646
+ pageCount: pages.length,
2647
+ focusScenePath: view.focusScenePath
2648
+ });
2649
+ throw error;
2650
+ }
2651
+ };
2348
2652
  dispose2.add(listen(window, "message", (ev) => {
2349
2653
  if (ev.data && ev.data.type == "@netless/_request_save_pdf_" && ev.data.appId == context.appId) {
2350
2654
  toPdf().catch((err) => {
2351
- console.warn(err);
2655
+ warn(err);
2352
2656
  reportProgress(100, null);
2353
2657
  });
2354
2658
  }
@@ -2363,7 +2667,7 @@ var NetlessAppPresentation = {
2363
2667
  scrollbar.setReadonly(bol);
2364
2668
  }
2365
2669
  };
2366
- const controller = { app, view, context, jumpPage, prevPage, nextPage, pageState, toPdf, log, setDocsViewReadonly, setReadonly, moveCamera, getOriginScale, getScale, getPageSize, screenshotCurrentPageAsync };
2670
+ const controller = { app, view, context, jumpPage, prevPage, nextPage, jumpPageAsync, prevPageAsync, nextPageAsync, pageState, toPdf, log, setDocsViewReadonly, setReadonly, moveCamera, getOriginScale, getScale, getPageSize, screenshotCurrentPageAsync };
2367
2671
  dispose2.add(listen(window, "message", (ev) => {
2368
2672
  if (ev.data === "@netless/_presentation_") {
2369
2673
  if (typeof window !== "undefined")
@@ -2375,7 +2679,7 @@ var NetlessAppPresentation = {
2375
2679
  console.log(controller);
2376
2680
  }
2377
2681
  }));
2378
- return controller;
2682
+ return prepareScenesPromise.then(() => controller);
2379
2683
  }
2380
2684
  };
2381
2685
  var AppPresentation = class extends Presentation {
@@ -2387,6 +2691,7 @@ var AppPresentation = class extends Presentation {
2387
2691
  super.updateImage();
2388
2692
  }
2389
2693
  onNewPageIndex(index, origin) {
2694
+ var _a;
2390
2695
  if (origin === "keydown" && this.box && !this.box.focus)
2391
2696
  return;
2392
2697
  if (this.log)
@@ -2394,7 +2699,7 @@ var AppPresentation = class extends Presentation {
2394
2699
  if (0 <= index && index < this.pages.length) {
2395
2700
  this.jumpPage(index);
2396
2701
  } else {
2397
- console.warn(`[Presentation]: page index ${index} out of bounds [0, ${this.pages.length - 1}]`);
2702
+ (_a = this.warn) == null ? void 0 : _a.call(this, `[Presentation]: page index ${index} out of bounds [0, ${this.pages.length - 1}]`);
2398
2703
  }
2399
2704
  }
2400
2705
  };
@@ -2441,7 +2746,7 @@ var install = (register, options = {}) => {
2441
2746
  };
2442
2747
 
2443
2748
  // src/index.ts
2444
- var version = "0.1.10";
2749
+ var version = "0.1.11";
2445
2750
 
2446
2751
  exports.NetlessAppPresentation = NetlessAppPresentation;
2447
2752
  exports.Presentation = Presentation;