Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/browser-manifold.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Test Browser Manifold Runtime

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- run: bun install
- run: bunx playwright install chromium --with-deps
- run: bun run test:browser-manifold
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@
"vercel-build": "bun run build-storybook",
"format": "biome format . --write",
"format:check": "biome format .",
"test:node-bundle": "node ./scripts/load-bundle-in-node.js"
"test:node-bundle": "node ./scripts/load-bundle-in-node.js",
"test:browser-manifold": "node ./scripts/test-browser-manifold.mjs"
},
"dependencies": {
"@jscad/regl-renderer": "^2.6.12",
"@jscad/stl-serializer": "^2.1.20",
"@tscircuit/manifold-2d": "https://jscdn.tscircuit.com/@tscircuit/manifold-2d/0.0.4.tgz",
"circuit-json": "^0.0.446",
"circuit-to-canvas": "^0.0.111",
"react-hot-toast": "^2.6.0",
Expand All @@ -47,6 +49,7 @@
"@biomejs/biome": "^2.1.4",
"@chromatic-com/storybook": "^1.9.0",
"@jscad/modeling": "^2.12.5",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@storybook/blocks": "9.0.0-alpha.17",
"@storybook/react-vite": "^9.1.5",
"@tscircuit/alphabet": "^0.0.25",
Expand All @@ -56,15 +59,15 @@
"@types/react": "19",
"@types/react-dom": "19",
"@types/three": "^0.165.0",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@vitejs/plugin-react": "^4.3.4",
"bun-match-svg": "^0.0.9",
"bun-types": "1.2.1",
"debug": "^4.4.0",
"esbuild": "^0.28.1",
"jscad-electronics": "^0.0.138",
"jscad-planner": "^0.0.13",
"jsdom": "^26.0.0",
"manifold-3d": "^3.2.1",
"playwright": "^1.61.1",
"react-use-gesture": "^9.1.3",
"semver": "^7.7.0",
"strip-ansi": "^7.1.0",
Expand Down
75 changes: 75 additions & 0 deletions scripts/test-browser-manifold.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import assert from "node:assert/strict"
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
import { createServer } from "node:http"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { build } from "esbuild"
import { chromium } from "playwright"

const testDir = await mkdtemp(join(tmpdir(), "3d-viewer-manifold-browser-"))
const entryPath = join(testDir, "entry.ts")
const bundlePath = join(testDir, "bundle.js")

await writeFile(
entryPath,
`
import { loadManifoldRuntime } from ${JSON.stringify(
new URL("../src/utils/manifold/load-manifold-runtime.ts", import.meta.url)
.pathname,
)}

try {
const module = await loadManifoldRuntime()
const cube = module.Manifold.cube([2, 3, 4])
const triangles = cube.numTri()
cube.delete()
document.body.dataset.result = triangles > 0 ? "ok" : "empty"
} catch (error) {
document.body.dataset.result = "error"
document.body.dataset.message = String(error?.stack ?? error)
}
`,
)

await build({
entryPoints: [entryPath],
outfile: bundlePath,
bundle: true,
external: ["node:module"],
format: "esm",
platform: "browser",
})

const browserBundle = await readFile(bundlePath, "utf8")
assert.equal(browserBundle.includes("manifold-3d"), false)

const server = createServer((request, response) => {
if (request.url === "/bundle.js") {
response.setHeader("Content-Type", "text/javascript")
response.end(browserBundle)
return
}
response.setHeader("Content-Type", "text/html")
response.end('<body><script type="module" src="/bundle.js"></script></body>')
})

await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve))
const address = server.address()
assert(address && typeof address === "object")

let browser
try {
browser = await chromium.launch({ headless: true })
const page = await browser.newPage()
await page.goto(`http://127.0.0.1:${address.port}`)
await page.waitForFunction(() => document.body.dataset.result)
const result = await page.locator("body").getAttribute("data-result")
const message = await page.locator("body").getAttribute("data-message")
assert.equal(result, "ok", message ?? "Manifold browser runtime failed")
} finally {
await browser?.close()
await new Promise((resolve, reject) =>
server.close((error) => (error ? reject(error) : resolve())),
)
await rm(testDir, { recursive: true, force: true })
}
80 changes: 9 additions & 71 deletions src/CadViewerManifold.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { su } from "@tscircuit/circuit-json-util"
import type { AnyCircuitElement, CadComponent } from "circuit-json"
import type { ManifoldToplevel } from "manifold-3d"
import type React from "react"
import { useEffect, useMemo, useState } from "react"
import * as THREE from "three"
Expand All @@ -16,16 +15,9 @@ import { createTextureMeshes } from "./textures"
import { Error3d } from "./three-components/Error3d"
import { ThreeErrorBoundary } from "./three-components/ThreeErrorBoundary"
import { createGeometryMeshes } from "./utils/manifold/create-three-geometry-meshes"
import { loadManifoldRuntime } from "./utils/manifold/load-manifold-runtime"
import { addFauxBoardIfNeeded } from "./utils/preprocess-circuit-json"

declare global {
interface Window {
ManifoldModule: any
MANIFOLD?: any
MANIFOLD_MODULE?: any
}
}

export const BoardMeshes = ({
geometryMeshes,
textureMeshes,
Expand Down Expand Up @@ -135,8 +127,6 @@ type CadViewerManifoldProps = {
| { circuitJson?: never; children: React.ReactNode }
)

const MANIFOLD_CDN_BASE_URL = "https://cdn.jsdelivr.net/npm/manifold-3d@3.2.1"

const CadViewerManifold: React.FC<CadViewerManifoldProps> = ({
circuitJson: circuitJsonProp,
autoRotateDisabled,
Expand All @@ -160,79 +150,27 @@ const CadViewerManifold: React.FC<CadViewerManifoldProps> = ({
const { shadowsEnabled } = useRenderingMode()

useEffect(() => {
if (
window.ManifoldModule &&
typeof window.ManifoldModule === "object" &&
window.ManifoldModule.setup
) {
setManifoldJSModule(window.ManifoldModule)
return
}
let cancelled = false

const initManifold = async (ManifoldModule: any) => {
const initManifold = async () => {
try {
const loadedModule: ManifoldToplevel = await ManifoldModule()
loadedModule.setup()
window.ManifoldModule = loadedModule
const loadedModule = await loadManifoldRuntime()

if (cancelled) return
setManifoldJSModule(loadedModule)
} catch (error) {
if (cancelled) return
console.error("Failed to initialize Manifold:", error)
setManifoldLoadingError(
`Failed to initialize Manifold: ${error instanceof Error ? error.message : "Unknown error"}`,
)
}
}

const existingManifold =
window.ManifoldModule ?? window.MANIFOLD ?? window.MANIFOLD_MODULE
if (existingManifold) {
window.ManifoldModule = existingManifold
initManifold(window.ManifoldModule)
return
}

const eventName = "manifoldLoaded"
const handleLoad = () => {
const loadedManifold =
window.ManifoldModule ?? window.MANIFOLD ?? window.MANIFOLD_MODULE
if (loadedManifold) {
window.ManifoldModule = loadedManifold
initManifold(window.ManifoldModule)
} else {
const errText = "ManifoldModule not found on window after script load."
console.error(errText)
setManifoldLoadingError(errText)
}
}

window.addEventListener(eventName, handleLoad, { once: true })

const script = document.createElement("script")
script.type = "module"
script.innerHTML = `
try {
const { default: ManifoldModule } = await import('${MANIFOLD_CDN_BASE_URL}/manifold.js');
window.ManifoldModule = ManifoldModule;
} catch (e) {
console.error('Error importing manifold in dynamic script:', e);
} finally {
window.dispatchEvent(new CustomEvent('${eventName}'));
}
`.trim()

const scriptError = (err: any) => {
const errText = "Failed to load Manifold loader script."
console.error(errText, err)
setManifoldLoadingError(errText)
window.removeEventListener(eventName, handleLoad)
}

script.addEventListener("error", scriptError)
document.body.appendChild(script)
void initManifold()

return () => {
window.removeEventListener(eventName, handleLoad)
script.removeEventListener("error", scriptError)
cancelled = true
}
}, [])

Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useManifoldBoardBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { su } from "@tscircuit/circuit-json-util"
import type { AnyCircuitElement, PcbBoard, PcbPanel } from "circuit-json"
import type { ManifoldToplevel } from "manifold-3d"
import type { ManifoldToplevel } from "@tscircuit/manifold-2d"
import { useEffect, useMemo, useRef, useState } from "react"
import type { LayerVisibilityState } from "src/contexts/LayerVisibilityContext"
import {
Expand Down
2 changes: 1 addition & 1 deletion src/utils/manifold-mesh-to-three-geometry.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as THREE from "three"
import type { Mesh } from "manifold-3d"
import type { Mesh } from "@tscircuit/manifold-2d"

export function manifoldMeshToThreeGeometry(
manifoldMesh: Mesh,
Expand Down
2 changes: 1 addition & 1 deletion src/utils/manifold/create-manifold-board.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type {
ManifoldToplevel,
CrossSection as ManifoldCrossSection,
} from "manifold-3d"
} from "@tscircuit/manifold-2d"
import type { PcbBoard } from "circuit-json"

const arePointsClockwise = (points: Array<[number, number]>): boolean => {
Expand Down
34 changes: 34 additions & 0 deletions src/utils/manifold/load-manifold-runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {
getManifoldModule,
type ManifoldToplevel,
} from "@tscircuit/manifold-2d"

declare global {
interface Window {
ManifoldModule: any
MANIFOLD?: any
MANIFOLD_MODULE?: any
}
}

export const loadManifoldRuntime = async (): Promise<ManifoldToplevel> => {
const existingManifold =
window.ManifoldModule ?? window.MANIFOLD ?? window.MANIFOLD_MODULE
let loadedModule: ManifoldToplevel

if (
existingManifold &&
typeof existingManifold === "object" &&
existingManifold.setup
) {
loadedModule = existingManifold
} else if (existingManifold) {
loadedModule = await existingManifold()
loadedModule.setup()
} else {
loadedModule = await getManifoldModule()
}

window.ManifoldModule = loadedModule
return loadedModule
}
2 changes: 1 addition & 1 deletion src/utils/manifold/process-cutouts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ManifoldToplevel, CrossSection } from "manifold-3d"
import type { CrossSection, ManifoldToplevel } from "@tscircuit/manifold-2d"
import type { AnyCircuitElement, PcbCutout } from "circuit-json"
import { su } from "@tscircuit/circuit-json-util"
import { SMOOTH_CIRCLE_SEGMENTS } from "../../geoms/constants"
Expand Down
2 changes: 1 addition & 1 deletion src/utils/manifold/process-non-plated-holes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { su } from "@tscircuit/circuit-json-util"
import type { AnyCircuitElement } from "circuit-json"
import type { ManifoldToplevel } from "manifold-3d"
import type { ManifoldToplevel } from "@tscircuit/manifold-2d"
import { SMOOTH_CIRCLE_SEGMENTS } from "../../geoms/constants"
import { createCircleHoleDrill } from "../hole-geoms"
import { createRoundedRectPrism } from "../pad-geoms"
Expand Down
2 changes: 1 addition & 1 deletion src/utils/manifold/process-plated-holes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { su } from "@tscircuit/circuit-json-util"
import type { AnyCircuitElement, PcbPlatedHole } from "circuit-json"
import type { Manifold, ManifoldToplevel } from "manifold-3d"
import type { Manifold, ManifoldToplevel } from "@tscircuit/manifold-2d"
import * as THREE from "three"
import {
colors as defaultColors,
Expand Down
2 changes: 1 addition & 1 deletion src/utils/manifold/process-vias.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ManifoldToplevel } from "manifold-3d"
import type { ManifoldToplevel } from "@tscircuit/manifold-2d"
import type { AnyCircuitElement, PcbVia } from "circuit-json"
import { su } from "@tscircuit/circuit-json-util"
import * as THREE from "three"
Expand Down
Loading