renderer.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. const puppeteer = require("puppeteer-core");
  2. const CHROME_DEFAULT =
  3. "C:/Program Files/Google/Chrome/Application/chrome.exe";
  4. /**
  5. * 无头渲染:打开门户三维路由,等待 viewer,点"日照分析"挂载组件,
  6. * 自己加载实景三维场景(倾斜摄影) + DSM 高程,再调 __sunlightRun({geojson,date})
  7. * (内部自动 设置范围(采样高程) → 采光率分析 → 导出),返回结果图。
  8. */
  9. async function renderScene(opts) {
  10. const {
  11. url,
  12. route = "/checkmodel",
  13. token = "",
  14. range = null,
  15. date = "",
  16. terrainUrl = "",
  17. sceneUrl = "",
  18. iserverHost = "192.168.60.2:8090",
  19. chromePath = CHROME_DEFAULT,
  20. } = opts;
  21. function evalTimeout(page, fn, ...args) {
  22. return Promise.race([
  23. page.evaluate(fn, ...args),
  24. new Promise((_, rej) =>
  25. setTimeout(() => rej(new Error("evaluate 超时")), 300000)
  26. ),
  27. ]);
  28. }
  29. const browser = await puppeteer.launch({
  30. executablePath: chromePath,
  31. headless: "new",
  32. args: [
  33. "--no-sandbox",
  34. "--disable-setuid-sandbox",
  35. "--enable-gpu",
  36. "--ignore-gpu-blocklist",
  37. "--window-size=1600,900",
  38. ],
  39. });
  40. try {
  41. const page = await browser.newPage();
  42. await page.setViewport({ width: 1600, height: 900 });
  43. // 转发无头页面日志
  44. page.on("console", (m) => console.log("[页]", m.type(), m.text()));
  45. page.on("pageerror", (e) => console.log("[页ERROR]", String(e && e.message || e)));
  46. page.on("dialog", (d) => d.accept());
  47. if (token) {
  48. await page.setCookie({
  49. name: "Admin-Token",
  50. value: token,
  51. domain: new URL(url).hostname,
  52. path: "/",
  53. });
  54. }
  55. await page.goto(url + route, {
  56. waitUntil: "domcontentloaded",
  57. timeout: 120000,
  58. });
  59. await page.waitForFunction(
  60. "window.viewer && window.viewer.scene && window.viewer.scene.canvas",
  61. { timeout: 120000 }
  62. );
  63. try {
  64. await page.waitForFunction(
  65. "window.viewer.scene.canvas.width > 0 && window.viewer.scene.canvas.height > 0",
  66. { timeout: 60000 }
  67. );
  68. } catch (e) {
  69. /* 画布 0 尺寸则走兜底截图 */
  70. }
  71. await new Promise((r) => setTimeout(r, 8000));
  72. // 打开日照分析(挂载 SunlightAnalysis,注入 __sunlightRun)
  73. let hasHook = false;
  74. try {
  75. await page.evaluate(() => {
  76. const btn = Array.from(document.querySelectorAll("button")).find(
  77. (b) => (b.innerText || "").trim() === "日照分析"
  78. );
  79. if (btn) btn.click();
  80. });
  81. await page.waitForFunction(
  82. "typeof window.__sunlightRun === 'function'",
  83. { timeout: 30000 }
  84. );
  85. hasHook = true;
  86. } catch (e) {
  87. console.warn("open 日照分析 error", e.message);
  88. }
  89. // 自己加载实景三维场景(倾斜摄影)
  90. if (sceneUrl) {
  91. const resolved = sceneUrl.replace("{s}", iserverHost);
  92. console.log("[renderer] 加载实景三维场景:", resolved);
  93. try {
  94. await evalTimeout(page, async (u) => {
  95. if (typeof window.viewer === "undefined") return;
  96. try {
  97. await window.viewer.scene.open(u);
  98. console.log("实景场景 open 完成");
  99. } catch (e) {
  100. console.warn("scene.open error", e);
  101. }
  102. }, resolved);
  103. } catch (e) {
  104. console.warn("[renderer] 打开场景异常", e.message);
  105. }
  106. try {
  107. await page.waitForFunction(
  108. "window.viewer.scene.layers && window.viewer.scene.layers.layerQueue.length > 0",
  109. { timeout: 90000 }
  110. );
  111. console.log("[renderer] 实景图层已就绪, layerQueue=",
  112. await page.evaluate(() => window.viewer.scene.layers.layerQueue.length)
  113. );
  114. } catch (e) {
  115. console.warn("[renderer] 等待实景图层超时", e.message);
  116. }
  117. await new Promise((r) => setTimeout(r, 5000));
  118. }
  119. // 自己加载 DSM 高程
  120. if (terrainUrl) {
  121. console.log("[renderer] 加载 DSM 高程:", terrainUrl.slice(0, 90));
  122. try {
  123. await evalTimeout(page, async (u) => {
  124. if (typeof Cesium === "undefined" || !window.viewer) return;
  125. window.viewer.scene.terrainProvider =
  126. new Cesium.EllipsoidTerrainProvider({});
  127. const tp = new Cesium.CesiumTerrainProvider({
  128. url: u,
  129. isSct: true,
  130. requestVertexNormals: true,
  131. });
  132. try {
  133. tp.visible = true;
  134. } catch (e) {}
  135. window.viewer.terrainProvider = tp;
  136. try {
  137. await tp.readyPromise;
  138. } catch (e) {
  139. console.warn("terrain ready error", e);
  140. }
  141. }, terrainUrl);
  142. console.log("[renderer] 地形已加载");
  143. } catch (e) {
  144. console.warn("[renderer] 地形加载失败", e.message);
  145. }
  146. await new Promise((r) => setTimeout(r, 3000));
  147. }
  148. if (range && hasHook) {
  149. // 调 __sunlightRun({geojson,date}):内部自动 设置范围(采样高程)→采光率分析→导出→返回图片
  150. const dataUrl = await evalTimeout(
  151. page,
  152. (args) =>
  153. window.__sunlightRun({
  154. geojson: args.range,
  155. date: args.date || undefined,
  156. }),
  157. { range, date: date || "" }
  158. );
  159. return {
  160. ok: true,
  161. jpegBase64: dataUrl.split(",")[1],
  162. note: "sunlight-export",
  163. };
  164. }
  165. // 兜底:截取当前三维场景
  166. const shot = await page.screenshot({
  167. encoding: "base64",
  168. type: "jpeg",
  169. quality: 90,
  170. });
  171. return {
  172. ok: true,
  173. jpegBase64: shot,
  174. note: hasHook ? "scene-capture(hook-ok-no-range)" : "scene-capture(no-hook)",
  175. };
  176. } catch (e) {
  177. return { ok: false, error: String((e && e.message) || e) };
  178. } finally {
  179. await browser.close().catch(() => {});
  180. }
  181. }
  182. module.exports = { renderScene };