From d38f1d7febe378678c0de911ca867445d2c939bc Mon Sep 17 00:00:00 2001 From: Harsh panwar Date: Fri, 26 Sep 2025 18:45:28 +0530 Subject: [PATCH 1/4] Add support requestAnimationFrame --- examples/dragrotate/DragRotate.tsx | 118 ++++++++++++++++------------- 1 file changed, 64 insertions(+), 54 deletions(-) diff --git a/examples/dragrotate/DragRotate.tsx b/examples/dragrotate/DragRotate.tsx index 0f8db86..4880e42 100644 --- a/examples/dragrotate/DragRotate.tsx +++ b/examples/dragrotate/DragRotate.tsx @@ -1,90 +1,100 @@ -import { useEffect, useRef, useState } from "react" -import { renderScene } from "../../lib" -import type { Scene } from "../../lib/types" +import { useEffect, useRef, useState } from "react"; +import { renderScene } from "../../lib"; +import type { Scene } from "../../lib/types"; -type Opt = Parameters[1] +type Opt = Parameters[1]; interface Props { - scene: Scene - opt?: Opt + scene: Scene; + opt?: Opt; } export default function DragRotate({ scene, opt }: Props) { - const containerRef = useRef(null) + const containerRef = useRef(null); // camera / drag state - const yaw = useRef(0.6) - const pitch = useRef(0.3) - const radius = 30 - const dragging = useRef(false) - const last = useRef({ x: 0, y: 0 }) + const yaw = useRef(0.6); + const pitch = useRef(0.3); + const radius = 30; + const dragging = useRef(false); + const last = useRef({ x: 0, y: 0 }); // viewport size – updated on every window resize - const [size, setSize] = useState({ width: 0, height: 0 }) + const [size, setSize] = useState({ width: 0, height: 0 }); - const [svg, setSvg] = useState("") + const [svg, setSvg] = useState(""); + // Throttle redraws using requestAnimationFrame + const redrawPending = useRef(false); - // helper to (re-)render const redraw = async () => { - if (!size.width || !size.height) return // size unknown yet - const dim = Math.min(size.width, size.height) // keep square aspect + if (!size.width || !size.height) return; + const dim = Math.min(size.width, size.height); const camPos = { x: radius * Math.cos(pitch.current) * Math.cos(yaw.current), y: radius * Math.sin(pitch.current), z: radius * Math.cos(pitch.current) * Math.sin(yaw.current), - } + }; const svgText = await renderScene( { ...scene, camera: { ...scene.camera, position: camPos } }, - { ...opt, width: dim, height: dim }, - ) - setSvg(svgText.replace(/<\?xml[^>]*\?>\s*/g, "")) - } + { ...opt, width: dim, height: dim } + ); + setSvg(svgText.replace(/<\?xml[^>]*\?>\s*/g, "")); + redrawPending.current = false; + }; + + // Throttled redraw trigger + const requestRedraw = () => { + if (!redrawPending.current) { + redrawPending.current = true; + window.requestAnimationFrame(() => redraw()); + } + }; /* initial render + event handling */ useEffect(() => { - redraw() + redraw(); const md = (e: MouseEvent) => { - if (!containerRef.current?.contains(e.target as Node)) return - dragging.current = true - last.current = { x: e.clientX, y: e.clientY } - } + if (!containerRef.current?.contains(e.target as Node)) return; + dragging.current = true; + last.current = { x: e.clientX, y: e.clientY }; + }; const mm = (e: MouseEvent) => { - if (!dragging.current) return - const dx = e.clientX - last.current.x - const dy = e.clientY - last.current.y - last.current = { x: e.clientX, y: e.clientY } - yaw.current += dx * 0.01 - pitch.current += dy * 0.01 - const lim = Math.PI / 2 - 0.01 - if (pitch.current > lim) pitch.current = lim - if (pitch.current < -lim) pitch.current = -lim - redraw() - } + if (!dragging.current) return; + const dx = e.clientX - last.current.x; + const dy = e.clientY - last.current.y; + last.current = { x: e.clientX, y: e.clientY }; + yaw.current += dx * 0.01; + pitch.current += dy * 0.01; + const lim = Math.PI / 2 - 0.01; + if (pitch.current > lim) pitch.current = lim; + if (pitch.current < -lim) pitch.current = -lim; + requestRedraw(); + }; const mu = () => { - dragging.current = false - } + dragging.current = false; + }; - window.addEventListener("mousedown", md) - window.addEventListener("mousemove", mm) - window.addEventListener("mouseup", mu) + window.addEventListener("mousedown", md); + window.addEventListener("mousemove", mm); + window.addEventListener("mouseup", mu); return () => { - window.removeEventListener("mousedown", md) - window.removeEventListener("mousemove", mm) - window.removeEventListener("mouseup", mu) - } + window.removeEventListener("mousedown", md); + window.removeEventListener("mousemove", mm); + window.removeEventListener("mouseup", mu); + }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [scene, opt, size]) // also when window size changes + }, [scene, opt, size]); // also when window size changes // track window-resize to keep `size` current useEffect(() => { const update = () => - setSize({ width: window.innerWidth, height: window.innerHeight }) - update() // initialise once mounted - window.addEventListener("resize", update) - return () => window.removeEventListener("resize", update) - }, []) + setSize({ width: window.innerWidth, height: window.innerHeight }); + update(); // initialise once mounted + window.addEventListener("resize", update); + return () => window.removeEventListener("resize", update); + }, []); return (
- ) + ); } From 08a6c27358623b592d8ebe3a59cc30dbe3323221 Mon Sep 17 00:00:00 2001 From: Harsh panwar Date: Fri, 26 Sep 2025 19:03:28 +0530 Subject: [PATCH 2/4] fix typo --- examples/dragrotate/DragRotate.tsx | 114 ++++++++++++++--------------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/examples/dragrotate/DragRotate.tsx b/examples/dragrotate/DragRotate.tsx index 4880e42..9d55ce2 100644 --- a/examples/dragrotate/DragRotate.tsx +++ b/examples/dragrotate/DragRotate.tsx @@ -1,100 +1,100 @@ -import { useEffect, useRef, useState } from "react"; -import { renderScene } from "../../lib"; -import type { Scene } from "../../lib/types"; +import { useEffect, useRef, useState } from "react" +import { renderScene } from "../../lib" +import type { Scene } from "../../lib/types" -type Opt = Parameters[1]; +type Opt = Parameters[1] interface Props { - scene: Scene; - opt?: Opt; + scene: Scene + opt?: Opt } export default function DragRotate({ scene, opt }: Props) { - const containerRef = useRef(null); + const containerRef = useRef(null) // camera / drag state - const yaw = useRef(0.6); - const pitch = useRef(0.3); - const radius = 30; - const dragging = useRef(false); - const last = useRef({ x: 0, y: 0 }); + const yaw = useRef(0.6) + const pitch = useRef(0.3) + const radius = 30 + const dragging = useRef(false) + const last = useRef({ x: 0, y: 0 }) // viewport size – updated on every window resize - const [size, setSize] = useState({ width: 0, height: 0 }); + const [size, setSize] = useState({ width: 0, height: 0 }) - const [svg, setSvg] = useState(""); + const [svg, setSvg] = useState("") // Throttle redraws using requestAnimationFrame - const redrawPending = useRef(false); + const redrawPending = useRef(false) const redraw = async () => { - if (!size.width || !size.height) return; - const dim = Math.min(size.width, size.height); + if (!size.width || !size.height) return + const dim = Math.min(size.width, size.height) const camPos = { x: radius * Math.cos(pitch.current) * Math.cos(yaw.current), y: radius * Math.sin(pitch.current), z: radius * Math.cos(pitch.current) * Math.sin(yaw.current), - }; + } const svgText = await renderScene( { ...scene, camera: { ...scene.camera, position: camPos } }, { ...opt, width: dim, height: dim } - ); - setSvg(svgText.replace(/<\?xml[^>]*\?>\s*/g, "")); - redrawPending.current = false; - }; + ) + setSvg(svgText.replace(/<\?xml[^>]*\?>\s*/g, "")) + redrawPending.current = false + } // Throttled redraw trigger const requestRedraw = () => { if (!redrawPending.current) { - redrawPending.current = true; - window.requestAnimationFrame(() => redraw()); + redrawPending.current = true + window.requestAnimationFrame(() => redraw()) } - }; + } /* initial render + event handling */ useEffect(() => { - redraw(); + redraw() const md = (e: MouseEvent) => { - if (!containerRef.current?.contains(e.target as Node)) return; - dragging.current = true; - last.current = { x: e.clientX, y: e.clientY }; - }; + if (!containerRef.current?.contains(e.target as Node)) return + dragging.current = true + last.current = { x: e.clientX, y: e.clientY } + } const mm = (e: MouseEvent) => { - if (!dragging.current) return; - const dx = e.clientX - last.current.x; - const dy = e.clientY - last.current.y; - last.current = { x: e.clientX, y: e.clientY }; - yaw.current += dx * 0.01; - pitch.current += dy * 0.01; - const lim = Math.PI / 2 - 0.01; - if (pitch.current > lim) pitch.current = lim; - if (pitch.current < -lim) pitch.current = -lim; - requestRedraw(); - }; + if (!dragging.current) return + const dx = e.clientX - last.current.x + const dy = e.clientY - last.current.y + last.current = { x: e.clientX, y: e.clientY } + yaw.current += dx * 0.01 + pitch.current += dy * 0.01 + const lim = Math.PI / 2 - 0.01 + if (pitch.current > lim) pitch.current = lim + if (pitch.current < -lim) pitch.current = -lim + requestRedraw() + } const mu = () => { - dragging.current = false; - }; + dragging.current = false + } - window.addEventListener("mousedown", md); - window.addEventListener("mousemove", mm); - window.addEventListener("mouseup", mu); + window.addEventListener("mousedown", md) + window.addEventListener("mousemove", mm) + window.addEventListener("mouseup", mu) return () => { - window.removeEventListener("mousedown", md); - window.removeEventListener("mousemove", mm); - window.removeEventListener("mouseup", mu); - }; + window.removeEventListener("mousedown", md) + window.removeEventListener("mousemove", mm) + window.removeEventListener("mouseup", mu) + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [scene, opt, size]); // also when window size changes + }, [scene, opt, size]) // also when window size changes // track window-resize to keep `size` current useEffect(() => { const update = () => - setSize({ width: window.innerWidth, height: window.innerHeight }); - update(); // initialise once mounted - window.addEventListener("resize", update); - return () => window.removeEventListener("resize", update); - }, []); + setSize({ width: window.innerWidth, height: window.innerHeight }) + update() // initialise once mounted + window.addEventListener("resize", update) + return () => window.removeEventListener("resize", update) + }, []) return (
- ); + ) } From b238cd9e0461fa6dd0dfa83aa097b4db8a744515 Mon Sep 17 00:00:00 2001 From: Harsh panwar Date: Fri, 26 Sep 2025 19:05:30 +0530 Subject: [PATCH 3/4] fix format --- examples/dragrotate/DragRotate.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/dragrotate/DragRotate.tsx b/examples/dragrotate/DragRotate.tsx index 9d55ce2..f0480d5 100644 --- a/examples/dragrotate/DragRotate.tsx +++ b/examples/dragrotate/DragRotate.tsx @@ -39,6 +39,7 @@ export default function DragRotate({ scene, opt }: Props) { { ...scene, camera: { ...scene.camera, position: camPos } }, { ...opt, width: dim, height: dim } ) + setSvg(svgText.replace(/<\?xml[^>]*\?>\s*/g, "")) redrawPending.current = false } From d1d7f61e26abba7732e6242242c17f241f9dce3c Mon Sep 17 00:00:00 2001 From: Harsh panwar Date: Sat, 27 Sep 2025 11:29:29 +0530 Subject: [PATCH 4/4] Replace BSP sort with Z sort for faster rendering --- lib/render-elements.ts | 134 +++-------------------------------------- 1 file changed, 8 insertions(+), 126 deletions(-) diff --git a/lib/render-elements.ts b/lib/render-elements.ts index 2e0542d..4c0866d 100644 --- a/lib/render-elements.ts +++ b/lib/render-elements.ts @@ -433,134 +433,16 @@ export async function buildRenderElements( } } - // BSP sort faces before merging with other elements - function sortFacesBSP( - polys: Face[], - W: number, - H: number, - focal: number, - ): Face[] { - const EPS = 1e-6 - type Node = { - face: Face - normal: Point3 - point: Point3 - front: Node | null - back: Node | null - } - - function build(list: Face[]): Node | null { - if (!list.length) return null - const face = list[0]! - const p0 = face.cam[0]! - const p1 = face.cam[1]! - const p2 = face.cam[2]! - const normal = cross(sub(p1, p0), sub(p2, p0)) - const front: Face[] = [] - const back: Face[] = [] - - for (let k = 1; k < list.length; k++) { - const f = list[k]! - // classify each vertex - let pos = 0, - neg = 0 - const d: number[] = [] - for (const v of f.cam) { - const dist = dot(normal, sub(v!, p0)) - d.push(dist) - if (dist > EPS) pos++ - else if (dist < -EPS) neg++ - } - if (!pos && !neg) { - front.push(f) // coplanar – draw after splitter - } else if (!pos) back.push(f) - else if (!neg) front.push(f) - else { - // split polygon by plane - const fFrontCam: Point3[] = [] - const fBackCam: Point3[] = [] - const fFront2D: Proj[] = [] - const fBack2D: Proj[] = [] - - for (let i = 0; i < f.cam.length; i++) { - const j = (i + 1) % f.cam.length - const aCam = f.cam[i]! - const bCam = f.cam[j]! - const a2D = f.pts[i]! - const b2D = f.pts[j]! - const da = d[i]! - const db = d[j]! - - const push = ( - arrCam: Point3[], - arr2D: Proj[], - cCam: Point3, - c2D: Proj, - ) => { - arrCam.push(cCam) - arr2D.push(c2D) - } - - if (da >= -EPS) push(fFrontCam, fFront2D, aCam!, a2D!) - if (da <= EPS) push(fBackCam, fBack2D, aCam!, a2D!) - - if ((da > 0 && db < 0) || (da < 0 && db > 0)) { - const t = da / (da - db) - const interCam = { - x: aCam.x + (bCam.x - aCam.x) * t, - y: aCam.y + (bCam.y - aCam.y) * t, - z: aCam.z + (bCam.z - aCam.z) * t, - } - const inter2D = proj(interCam, W, H, focal)! - push(fFrontCam, fFront2D, interCam, inter2D) - push(fBackCam, fBack2D, interCam, inter2D) - } - } - - const mk = (cam: Point3[], pts: Proj[]): Face | null => { - if (cam.length < 3) return null - const nf: Face = { cam, pts, fill: f!.fill, stroke: false } - const img = faceToImg.get(f) - if (img) faceToImg.set(nf, img) - return nf - } - const f1 = mk(fFrontCam, fFront2D) - const f2 = mk(fBackCam, fBack2D) - if (f1) front.push(f1) - if (f2) back.push(f2) - } - } - - return { - face, - normal, - point: p0, - front: build(front), - back: build(back), - } - } - - function traverse(node: Node | null, out: Face[]) { - if (!node) return - const cameraSide = dot(node.normal, scale(node.point, -1)) - if (cameraSide >= 0) { - traverse(node.back, out) - out.push(node.face) - traverse(node.front, out) - } else { - traverse(node.front, out) - out.push(node.face) - traverse(node.back, out) - } - } - - const root = build(polys) - const ordered: Face[] = [] - traverse(root, ordered) - return ordered + function sortFacesZ(faces: Face[]): Face[] { + // Sort by average Z (camera space) + return faces.slice().sort((a, b) => { + const za = a.cam.reduce((sum, v) => sum + v.z, 0) / a.cam.length + const zb = b.cam.reduce((sum, v) => sum + v.z, 0) / b.cam.length + return zb - za // farthest first + }) } - const orderedFaces = sortFacesBSP(faces, W, H, focal) + const orderedFaces = sortFacesZ(faces) const elements: RenderElement[] = [] for (const f of orderedFaces) {