mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 17:58:20 +00:00
web: scaffold IfcViewerWeb (Emscripten clear-color renderer)
First Emscripten target. main_web.cpp brings up a wgpu instance against
a <canvas id="viewer-canvas">, requests adapter+device asynchronously
via the standard webgpu.h callback chain, configures the surface, and
clears to the BonsaiViewer slate background on each RAF tick. No
sidecar load, no pipelines, no scene state yet — the goal is to end-
to-end verify the build + canvas + wgpu plumbing.
src/ifcviewer-web/ is a separate CMake root (not a subdir under the
desktop cmake/CMakeLists.txt) so the web build doesn't have to opt out
of Qt / OpenCASCADE / IfcGeom find_packages it can't satisfy. It adds
src/ifcviewer EXCLUDE_FROM_ALL and consumes only IfcViewerCore.
src/ifcviewer/CMakeLists.txt now gates the wgpu-native fetch + the
Qt-using IfcViewer target + install commands behind NOT EMSCRIPTEN.
The wgpu_native link target still resolves under Emscripten as an
INTERFACE library that activates --use-port=emdawnwebgpu (Dawn's
webgpu.h, replaces the legacy -sUSE_WEBGPU=1).
Build:
source path/to/emsdk_env.sh
emcmake cmake -S src/ifcviewer-web -B build-web -G Ninja
ninja -C build-web
python3 -m http.server --directory build-web 8080
# open http://localhost:8080/IfcViewerWeb.html in a WebGPU-capable
# browser (Chrome 113+, Edge 113+).
Phase B step 3 of #45.
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
################################################################################
|
||||
# #
|
||||
# This file is part of IfcOpenShell. #
|
||||
# #
|
||||
# IfcOpenShell is free software: you can redistribute it and/or modify #
|
||||
# it under the terms of the Lesser GNU General Public License as published by #
|
||||
# the Free Software Foundation, either version 3.0 of the License, or #
|
||||
# (at your option) any later version. #
|
||||
# #
|
||||
# IfcOpenShell is distributed in the hope that it will be useful, #
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
|
||||
# Lesser GNU General Public License for more details. #
|
||||
# #
|
||||
# You should have received a copy of the Lesser GNU General Public License #
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>. #
|
||||
# #
|
||||
################################################################################
|
||||
|
||||
# Web (Emscripten + WebGPU) build root.
|
||||
#
|
||||
# This is a SEPARATE root from cmake/CMakeLists.txt — the desktop CMake
|
||||
# pulls in Qt, OpenCASCADE, IfcGeom, CGAL, Boost, none of which exist or
|
||||
# make sense for wasm. The web build only needs IfcViewerCore (the
|
||||
# Qt-free, OpenCASCADE-free static lib carved out of src/ifcviewer)
|
||||
# plus this main_web.cpp scaffold.
|
||||
#
|
||||
# Usage:
|
||||
# source /path/to/emsdk_env.sh
|
||||
# emcmake cmake -S src/ifcviewer-web -B build-web
|
||||
# ninja -C build-web
|
||||
# python3 -m http.server --directory build-web 8080
|
||||
# # open http://localhost:8080/IfcViewerWeb.html
|
||||
#
|
||||
# Why a separate root and not a sub-add under cmake/CMakeLists.txt:
|
||||
# making every find_package() conditional on (NOT EMSCRIPTEN) would
|
||||
# pollute the desktop build with web-only branches it never exercises.
|
||||
# Better to keep the web path's machinery local to this directory.
|
||||
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
project(IfcViewerWeb LANGUAGES CXX)
|
||||
|
||||
if(NOT EMSCRIPTEN)
|
||||
message(FATAL_ERROR
|
||||
"src/ifcviewer-web requires the Emscripten toolchain. "
|
||||
"Re-run via `emcmake cmake ...`.")
|
||||
endif()
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# Allow find_package to look at the host system for header-only deps
|
||||
# (Eigen3 is the only one we need). Emscripten's CMake toolchain
|
||||
# defaults this to ONLY, which restricts searches to the emcc sysroot
|
||||
# and blocks the system Eigen3Config.cmake. Eigen is pure-header so
|
||||
# pulling its include path from the host is completely safe.
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH)
|
||||
|
||||
# --- IfcViewerCore: portable runtime subset ----------------------------------
|
||||
#
|
||||
# Build IfcViewerCore by adding src/ifcviewer as a subdir. The Qt /
|
||||
# IfcGeom / OpenCASCADE-coupled IfcViewer target inside that CMakeLists
|
||||
# will fail under emcc (find_package(Qt6) etc. won't resolve), so the
|
||||
# subdir's full build can't be reached here. We add it with EXCLUDE_FROM_ALL
|
||||
# so only the targets we explicitly reference (IfcViewerCore) are built.
|
||||
# Eigen and wgpu_native (the latter becomes a no-op interface under
|
||||
# EMSCRIPTEN — see ifcviewer/CMakeLists.txt) get pulled in transitively.
|
||||
#
|
||||
# TODO(web): when src/ifcviewer/CMakeLists.txt is taught to skip the
|
||||
# Qt-using IfcViewer target under EMSCRIPTEN, we can drop EXCLUDE_FROM_ALL.
|
||||
set(IFCVIEWER_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../ifcviewer")
|
||||
add_subdirectory(${IFCVIEWER_DIR} ifcviewer EXCLUDE_FROM_ALL)
|
||||
|
||||
# --- The web executable ------------------------------------------------------
|
||||
|
||||
add_executable(IfcViewerWeb main_web.cpp)
|
||||
target_link_libraries(IfcViewerWeb PRIVATE IfcViewerCore)
|
||||
target_link_options(IfcViewerWeb PRIVATE
|
||||
# Asyncify lets us await wgpu's RequestAdapter/RequestDevice/MapAsync
|
||||
# as if they were synchronous. Adds ~30-50% wasm size and a small
|
||||
# per-await runtime cost; acceptable for the spike, revisit when
|
||||
# we ship.
|
||||
"-sASYNCIFY"
|
||||
# Streaming + chunked geometry want a heap that can grow as buffers
|
||||
# arrive. 256 MB initial, 2 GB ceiling (matches the wasm32 pointer
|
||||
# cap; --shared64 / MEMORY64 would lift this later if we need it).
|
||||
"-sALLOW_MEMORY_GROWTH=1"
|
||||
"-sINITIAL_MEMORY=268435456" # 256 MB
|
||||
"-sMAXIMUM_MEMORY=2147483648" # 2 GB
|
||||
# FETCH lets emscripten_fetch issue HTTP Range requests for the
|
||||
# sidecar byte-range loader. Not used yet by the scaffold but
|
||||
# needed by the upcoming #27 web streaming I/O backend.
|
||||
"-sFETCH=1"
|
||||
# Shell template wraps the JS output in our canvas page.
|
||||
"--shell-file=${CMAKE_CURRENT_SOURCE_DIR}/shell.html"
|
||||
)
|
||||
set_target_properties(IfcViewerWeb PROPERTIES SUFFIX ".html")
|
||||
@@ -0,0 +1,197 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
// Phase-B-step-3 scaffold for the web target. Brings up a wgpu instance
|
||||
// against a #canvas via the emdawnwebgpu port, requests adapter+device
|
||||
// asynchronously, configures the surface, and renders a clear color on
|
||||
// each requestAnimationFrame tick. No sidecar load, no pipelines, no
|
||||
// scene state yet — the goal of this commit is "something renders in a
|
||||
// browser tab" so the build + canvas + wgpu plumbing is end-to-end
|
||||
// verified before we wire in IfcViewerCore.
|
||||
|
||||
#include <emscripten/emscripten.h>
|
||||
#include <emscripten/html5.h>
|
||||
|
||||
#include <webgpu/webgpu.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace {
|
||||
|
||||
// Async-arrived handles. Populated by the requestAdapter/requestDevice
|
||||
// callback chain in startup(); render() short-circuits until they're
|
||||
// all set. Keeps the path single-threaded — the JS event loop drives
|
||||
// progress between callbacks.
|
||||
WGPUInstance g_instance = nullptr;
|
||||
WGPUAdapter g_adapter = nullptr;
|
||||
WGPUDevice g_device = nullptr;
|
||||
WGPUQueue g_queue = nullptr;
|
||||
WGPUSurface g_surface = nullptr;
|
||||
WGPUTextureFormat g_surface_format = WGPUTextureFormat_Undefined;
|
||||
bool g_main_loop_started = false;
|
||||
|
||||
// Canvas dimensions in CSS pixels. emscripten reports the canvas size in
|
||||
// CSS pixels but the GPU surface wants device pixels; we keep things at
|
||||
// 1× DPR for the scaffold and re-derive on resize once we wire input.
|
||||
int g_width = 1280;
|
||||
int g_height = 800;
|
||||
|
||||
void frame() {
|
||||
if (!g_surface || !g_device) return;
|
||||
|
||||
WGPUSurfaceTexture st = {};
|
||||
wgpuSurfaceGetCurrentTexture(g_surface, &st);
|
||||
if (st.status != WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal &&
|
||||
st.status != WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal) {
|
||||
// Lost / outdated / OOM / device-lost — log once and bail out
|
||||
// of this frame so we don't queue work against a torn surface.
|
||||
static int s_complained = 0;
|
||||
if (s_complained++ < 4) {
|
||||
std::fprintf(stderr,
|
||||
"[viewer-web] surface texture status %d; skipping frame\n",
|
||||
int(st.status));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
WGPUTextureView view = wgpuTextureCreateView(st.texture, nullptr);
|
||||
|
||||
WGPURenderPassColorAttachment ca = {};
|
||||
ca.view = view;
|
||||
ca.loadOp = WGPULoadOp_Clear;
|
||||
ca.storeOp = WGPUStoreOp_Store;
|
||||
ca.clearValue = {0.18, 0.21, 0.28, 1.0}; // BonsaiViewer slate background
|
||||
ca.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
|
||||
|
||||
WGPURenderPassDescriptor rp_desc = {};
|
||||
rp_desc.colorAttachmentCount = 1;
|
||||
rp_desc.colorAttachments = &ca;
|
||||
|
||||
WGPUCommandEncoderDescriptor enc_desc = {};
|
||||
WGPUCommandEncoder enc = wgpuDeviceCreateCommandEncoder(g_device, &enc_desc);
|
||||
WGPURenderPassEncoder rp = wgpuCommandEncoderBeginRenderPass(enc, &rp_desc);
|
||||
wgpuRenderPassEncoderEnd(rp);
|
||||
wgpuRenderPassEncoderRelease(rp);
|
||||
|
||||
WGPUCommandBufferDescriptor cb_desc = {};
|
||||
WGPUCommandBuffer cb = wgpuCommandEncoderFinish(enc, &cb_desc);
|
||||
wgpuQueueSubmit(g_queue, 1, &cb);
|
||||
|
||||
wgpuCommandBufferRelease(cb);
|
||||
wgpuCommandEncoderRelease(enc);
|
||||
wgpuTextureViewRelease(view);
|
||||
wgpuTextureRelease(st.texture);
|
||||
}
|
||||
|
||||
void configure_surface() {
|
||||
WGPUSurfaceCapabilities caps = {};
|
||||
if (wgpuSurfaceGetCapabilities(g_surface, g_adapter, &caps) != WGPUStatus_Success
|
||||
|| caps.formatCount == 0) {
|
||||
std::fprintf(stderr, "[viewer-web] surface has no formats\n");
|
||||
return;
|
||||
}
|
||||
g_surface_format = caps.formats[0];
|
||||
wgpuSurfaceCapabilitiesFreeMembers(caps);
|
||||
|
||||
WGPUSurfaceConfiguration cfg = {};
|
||||
cfg.device = g_device;
|
||||
cfg.format = g_surface_format;
|
||||
cfg.usage = WGPUTextureUsage_RenderAttachment;
|
||||
cfg.width = uint32_t(g_width);
|
||||
cfg.height = uint32_t(g_height);
|
||||
cfg.alphaMode = WGPUCompositeAlphaMode_Auto;
|
||||
cfg.presentMode = WGPUPresentMode_Fifo;
|
||||
wgpuSurfaceConfigure(g_surface, &cfg);
|
||||
|
||||
if (!g_main_loop_started) {
|
||||
emscripten_set_main_loop(frame, 0, /*simulate_infinite=*/0);
|
||||
g_main_loop_started = true;
|
||||
std::fprintf(stderr,
|
||||
"[viewer-web] surface configured (%dx%d format=%d); RAF loop started\n",
|
||||
g_width, g_height, int(g_surface_format));
|
||||
}
|
||||
}
|
||||
|
||||
void on_device_ready(WGPURequestDeviceStatus status, WGPUDevice device,
|
||||
WGPUStringView message, void* /*ud1*/, void* /*ud2*/) {
|
||||
if (status != WGPURequestDeviceStatus_Success || !device) {
|
||||
std::fprintf(stderr, "[viewer-web] requestDevice failed: %.*s\n",
|
||||
int(message.length), message.data ? message.data : "");
|
||||
return;
|
||||
}
|
||||
g_device = device;
|
||||
g_queue = wgpuDeviceGetQueue(device);
|
||||
|
||||
WGPUEmscriptenSurfaceSourceCanvasHTMLSelector canvas = {};
|
||||
canvas.chain.sType = WGPUSType_EmscriptenSurfaceSourceCanvasHTMLSelector;
|
||||
static const char* kSelector = "#viewer-canvas";
|
||||
canvas.selector.data = kSelector;
|
||||
canvas.selector.length = std::strlen(kSelector);
|
||||
|
||||
WGPUSurfaceDescriptor sd = {};
|
||||
sd.nextInChain = &canvas.chain;
|
||||
g_surface = wgpuInstanceCreateSurface(g_instance, &sd);
|
||||
if (!g_surface) {
|
||||
std::fprintf(stderr,
|
||||
"[viewer-web] wgpuInstanceCreateSurface returned null "
|
||||
"(canvas '#viewer-canvas' missing?)\n");
|
||||
return;
|
||||
}
|
||||
configure_surface();
|
||||
}
|
||||
|
||||
void on_adapter_ready(WGPURequestAdapterStatus status, WGPUAdapter adapter,
|
||||
WGPUStringView message, void* /*ud1*/, void* /*ud2*/) {
|
||||
if (status != WGPURequestAdapterStatus_Success || !adapter) {
|
||||
std::fprintf(stderr, "[viewer-web] requestAdapter failed: %.*s\n",
|
||||
int(message.length), message.data ? message.data : "");
|
||||
return;
|
||||
}
|
||||
g_adapter = adapter;
|
||||
|
||||
WGPUDeviceDescriptor dd = {};
|
||||
WGPURequestDeviceCallbackInfo cb = {};
|
||||
cb.mode = WGPUCallbackMode_AllowSpontaneous;
|
||||
cb.callback = on_device_ready;
|
||||
wgpuAdapterRequestDevice(adapter, &dd, cb);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
WGPUInstanceDescriptor desc = {};
|
||||
g_instance = wgpuCreateInstance(&desc);
|
||||
if (!g_instance) {
|
||||
std::fprintf(stderr, "[viewer-web] wgpuCreateInstance returned null\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
WGPURequestAdapterOptions opts = {};
|
||||
WGPURequestAdapterCallbackInfo cb = {};
|
||||
cb.mode = WGPUCallbackMode_AllowSpontaneous;
|
||||
cb.callback = on_adapter_ready;
|
||||
wgpuInstanceRequestAdapter(g_instance, &opts, cb);
|
||||
|
||||
// main() returns; the browser keeps the JS event loop running so
|
||||
// the async adapter/device callbacks above land naturally and the
|
||||
// main loop kicks off from configure_surface().
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>IfcViewer (web)</title>
|
||||
<style>
|
||||
html, body { margin: 0; height: 100%; background: #0f1117; color: #c8ccd6;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
|
||||
#viewer-canvas { display: block; width: 100vw; height: 100vh; outline: none;
|
||||
background: #1a1d24; }
|
||||
#status { position: fixed; top: 8px; left: 12px; font-size: 12px;
|
||||
background: rgba(20,22,28,.72); padding: 4px 8px; border-radius: 4px;
|
||||
pointer-events: none; }
|
||||
#status.error { background: rgba(120,30,30,.85); color: #fff; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="viewer-canvas" width="1280" height="800"></canvas>
|
||||
<div id="status">Starting…</div>
|
||||
<script>
|
||||
// Emscripten Module hook: route stderr to the status overlay so any
|
||||
// wgpu init failure (no WebGPU, canvas missing, etc.) is visible
|
||||
// without opening devtools.
|
||||
var statusEl = document.getElementById('status');
|
||||
var Module = {
|
||||
canvas: document.getElementById('viewer-canvas'),
|
||||
print: function(t) { console.log(t); },
|
||||
printErr: function(t) {
|
||||
console.warn(t);
|
||||
// First non-empty stderr line replaces the "Starting…" tag; subsequent
|
||||
// lines append. Anything containing "fail" / "error" flips us into
|
||||
// the red error chip.
|
||||
statusEl.textContent = t;
|
||||
if (/fail|error|null/i.test(t)) statusEl.classList.add('error');
|
||||
},
|
||||
onRuntimeInitialized: function() {
|
||||
statusEl.textContent = 'wasm loaded — waiting for WebGPU';
|
||||
}
|
||||
};
|
||||
if (!navigator.gpu) {
|
||||
statusEl.textContent = 'navigator.gpu is missing — open in a browser with WebGPU enabled';
|
||||
statusEl.classList.add('error');
|
||||
}
|
||||
</script>
|
||||
<!-- IfcViewerWeb.js is emitted alongside this shell by the emcc build;
|
||||
`--shell-file` injects this HTML around it. -->
|
||||
{{{ SCRIPT }}}
|
||||
</body>
|
||||
</html>
|
||||
@@ -19,15 +19,26 @@
|
||||
|
||||
message("Running CMakeLists.txt in /src/ifcviewer")
|
||||
|
||||
set(QT_VERSION 6 CACHE STRING "Qt version")
|
||||
find_package(Qt${QT_VERSION} COMPONENTS Core Gui REQUIRED PATHS ${QT_DIR})
|
||||
# Qt is desktop-only. Under Emscripten this directory is reached via
|
||||
# src/ifcviewer-web/CMakeLists.txt which only consumes IfcViewerCore;
|
||||
# the IfcViewer (Qt-using) target below is skipped entirely.
|
||||
if(NOT EMSCRIPTEN)
|
||||
set(QT_VERSION 6 CACHE STRING "Qt version")
|
||||
find_package(Qt${QT_VERSION} COMPONENTS Core Gui REQUIRED PATHS ${QT_DIR})
|
||||
endif()
|
||||
|
||||
# Eigen3 — used by Federation matrices, ViewportWindow's instance compose,
|
||||
# the per-model coordinate-operation matrices stored on ModelGpuData, and
|
||||
# everywhere a 4x4 transform shows up.
|
||||
# everywhere a 4x4 transform shows up. Header-only, works under Emscripten.
|
||||
find_package(Eigen3 REQUIRED)
|
||||
|
||||
# wgpu-native — fetched as a pre-built binary release from upstream.
|
||||
# Under Emscripten this whole block is skipped; the web build links
|
||||
# against Dawn's webgpu.h via the emdawnwebgpu port instead. The
|
||||
# `wgpu_native` link target is created as an INTERFACE in that branch
|
||||
# (see the end of this block) so consumers' target_link_libraries lines
|
||||
# work uniformly.
|
||||
if(NOT EMSCRIPTEN)
|
||||
# Pin the version with WGPU_NATIVE_VERSION; bump to pull a newer release.
|
||||
set(WGPU_NATIVE_VERSION "v29.0.0.0" CACHE STRING "wgpu-native release tag")
|
||||
|
||||
@@ -105,6 +116,17 @@ endif()
|
||||
set(WGPU_NATIVE_LIB_DIR "${wgpu_native_SOURCE_DIR}/lib" CACHE INTERNAL
|
||||
"Directory containing the wgpu-native shared library")
|
||||
|
||||
else() # EMSCRIPTEN — Dawn's webgpu.h via the emdawnwebgpu port instead.
|
||||
# Marker interface so downstream `target_link_libraries(... wgpu_native)`
|
||||
# lines work uniformly across desktop and web. Activating the
|
||||
# emdawnwebgpu port both at compile and link via INTERFACE so any
|
||||
# TU that #includes <webgpu/webgpu.h> picks up the port's vendored
|
||||
# header without each target having to re-declare the flag.
|
||||
add_library(wgpu_native INTERFACE)
|
||||
target_compile_options(wgpu_native INTERFACE "--use-port=emdawnwebgpu")
|
||||
target_link_options(wgpu_native INTERFACE "--use-port=emdawnwebgpu")
|
||||
endif()
|
||||
|
||||
# IfcViewerCore: the Qt-free, OpenCASCADE-free runtime subset. Buffer
|
||||
# pool, chunk planner, instance composition, sidecar cache, streaming
|
||||
# I/O, LOD builder, vertex quantisation — everything needed to load a
|
||||
@@ -155,7 +177,9 @@ endif()
|
||||
install(TARGETS IfcViewerCore EXPORT ${IFCOPENSHELL_EXPORT_TARGETS})
|
||||
|
||||
# IfcViewer: the Qt + IfcGeom + OpenCASCADE shell — everything in this
|
||||
# directory NOT already pulled into IfcViewerCore above.
|
||||
# directory NOT already pulled into IfcViewerCore above. Desktop-only;
|
||||
# Emscripten builds consume IfcViewerCore directly via src/ifcviewer-web.
|
||||
if(NOT EMSCRIPTEN)
|
||||
file(GLOB _ifcviewer_all_cpp ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp)
|
||||
file(GLOB _ifcviewer_all_h ${CMAKE_CURRENT_SOURCE_DIR}/*.h)
|
||||
set(IFCVIEWER_FILES ${_ifcviewer_all_cpp} ${_ifcviewer_all_h})
|
||||
@@ -248,6 +272,8 @@ else()
|
||||
install(FILES "${wgpu_native_SOURCE_DIR}/lib/${_wgpu_lib}" DESTINATION lib)
|
||||
endif()
|
||||
|
||||
if(BUILD_BONSAIVIEWER_TESTS)
|
||||
endif() # NOT EMSCRIPTEN
|
||||
|
||||
if(BUILD_BONSAIVIEWER_TESTS AND NOT EMSCRIPTEN)
|
||||
add_subdirectory(tests)
|
||||
endif()
|
||||
|
||||
Reference in New Issue
Block a user