├── .gitignore ├── assemblies ├── library │ ├── models │ │ ├── seedcase │ │ │ ├── .python-version │ │ │ ├── LICENSE.txt │ │ │ ├── seeed-top.stl │ │ │ ├── seeed-bottom.stl │ │ │ └── attribution.txt │ │ ├── corne │ │ │ ├── LICENSE.txt │ │ │ ├── attribution.txt │ │ │ ├── corne_blecover.stl │ │ │ ├── corne_chocolate_with_ble_L (1).stl │ │ │ └── corne_chocolate_with_ble_R (1).stl │ │ └── batholder │ │ │ └── Cockroach_75mm_450mah.stl │ ├── util │ │ └── ifItFitsIsits.js │ ├── DTSPCB.js │ ├── xiao_esp32c3.js │ └── radio_enclosure.js ├── SeeedCase.js ├── SleevedTripleAssembly.js ├── TripleSteppedPrism.js └── 4axisArray.js ├── scene.js ├── .gitmodules ├── viewer ├── model_def.man ├── js_bindings.h ├── CMakeLists.txt ├── scene.js ├── main.cpp └── js_bindings.cpp ├── run.sh ├── README.md ├── CMakeLists.txt └── API.md /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /assemblies/library/models/seedcase/.python-version: -------------------------------------------------------------------------------- 1 | 3.10.11 2 | -------------------------------------------------------------------------------- /assemblies/library/models/corne/LICENSE.txt: -------------------------------------------------------------------------------- 1 | https://creativecommons.org/licenses/by-nc-sa/4.0/ -------------------------------------------------------------------------------- /assemblies/library/models/seedcase/LICENSE.txt: -------------------------------------------------------------------------------- 1 | https://creativecommons.org/licenses/by/4.0/ -------------------------------------------------------------------------------- /assemblies/library/models/corne/attribution.txt: -------------------------------------------------------------------------------- 1 | https://www.thingiverse.com/thing:4549765/files -------------------------------------------------------------------------------- /assemblies/library/models/corne/corne_blecover.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yacineMTB/dingcad/HEAD/assemblies/library/models/corne/corne_blecover.stl -------------------------------------------------------------------------------- /assemblies/library/models/seedcase/seeed-top.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yacineMTB/dingcad/HEAD/assemblies/library/models/seedcase/seeed-top.stl -------------------------------------------------------------------------------- /scene.js: -------------------------------------------------------------------------------- 1 | import buildSleevedTripleAssembly from './assemblies/SleevedTripleAssembly.js'; 2 | 3 | export const scene = buildSleevedTripleAssembly(); 4 | -------------------------------------------------------------------------------- /assemblies/library/models/seedcase/seeed-bottom.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yacineMTB/dingcad/HEAD/assemblies/library/models/seedcase/seeed-bottom.stl -------------------------------------------------------------------------------- /assemblies/library/models/batholder/Cockroach_75mm_450mah.stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yacineMTB/dingcad/HEAD/assemblies/library/models/batholder/Cockroach_75mm_450mah.stl -------------------------------------------------------------------------------- /assemblies/library/models/corne/corne_chocolate_with_ble_L (1).stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yacineMTB/dingcad/HEAD/assemblies/library/models/corne/corne_chocolate_with_ble_L (1).stl -------------------------------------------------------------------------------- /assemblies/library/models/corne/corne_chocolate_with_ble_R (1).stl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yacineMTB/dingcad/HEAD/assemblies/library/models/corne/corne_chocolate_with_ble_R (1).stl -------------------------------------------------------------------------------- /assemblies/library/models/seedcase/attribution.txt: -------------------------------------------------------------------------------- 1 | https://www.printables.com/model/1275829-seeed-xiao-esp32-s3-case/files 2 | made by 3 | https://www.printables.com/@RobertATSDuct_791555 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/manifold"] 2 | path = vendor/manifold 3 | url = https://github.com/elalish/manifold 4 | [submodule "vendor/quickjs"] 5 | path = vendor/quickjs 6 | url = https://github.com/quickjs-ng/quickjs 7 | -------------------------------------------------------------------------------- /viewer/model_def.man: -------------------------------------------------------------------------------- 1 | manifold::Manifold cube = manifold::Manifold::Cube({2.0, 2.0, 2.0}, true); 2 | manifold::Manifold sphere = manifold::Manifold::Sphere(1.5, 0); 3 | manifold::Manifold result = cube + sphere.Translate({0.0, 1.0, 0.0}); 4 | -------------------------------------------------------------------------------- /viewer/js_bindings.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | extern "C" { 4 | #include "quickjs.h" 5 | } 6 | 7 | #include 8 | 9 | namespace manifold { 10 | class Manifold; 11 | } 12 | 13 | void EnsureManifoldClass(JSRuntime *runtime); 14 | void RegisterBindings(JSContext *ctx); 15 | std::shared_ptr GetManifoldHandle(JSContext *ctx, 16 | JSValueConst value); 17 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 5 | BUILD_DIR="$ROOT_DIR/build" 6 | 7 | cmake -S "$ROOT_DIR" -B "$BUILD_DIR" 8 | cmake --build "$BUILD_DIR" --target dingcad_viewer 9 | 10 | VIEWER_BIN="$BUILD_DIR/viewer/dingcad_viewer" 11 | 12 | if [[ ! -x "$VIEWER_BIN" ]]; 13 | then 14 | echo "viewer executable not found at $VIEWER_BIN" >&2 15 | exit 1 16 | fi 17 | 18 | "$VIEWER_BIN" "$@" 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Dingcad 2 | 3 | Dingcad is a live reloading program that is a replacement for openscad. Becuase openscad kind of really sucks. Try ./run.sh and then updating scene.js 4 | 5 | This is dingcad. Dependencies: raylib, manifoldcad, and quickjs. Ask an LLM how to set up raylib on your system. For the quickjs and manifoldcad; you can 6 | 7 | ``` 8 | git submodule update --init --recursive 9 | ``` 10 | 11 | This repository is mostly autonomously written by an LLM that I've lazily prompted while watching youtube and hanging out with my family. 12 | 13 | There are no docs. Just read the code. 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /assemblies/SeeedCase.js: -------------------------------------------------------------------------------- 1 | const ensureLoadMeshAvailable = () => { 2 | if (typeof loadMesh !== 'function') { 3 | throw new Error('loadMesh binding is not available; rebuild the viewer with mesh import support.'); 4 | } 5 | }; 6 | 7 | const centerOnOrigin = (manifold) => { 8 | const bounds = boundingBox(manifold); 9 | const cx = (bounds.min[0] + bounds.max[0]) / 2; 10 | const cy = (bounds.min[1] + bounds.max[1]) / 2; 11 | const cz = (bounds.min[2] + bounds.max[2]) / 2; 12 | return translate(manifold, [-cx, -cy, -cz]); 13 | }; 14 | 15 | export const buildSeeedCase = () => { 16 | ensureLoadMeshAvailable(); 17 | const topShell = loadMesh('assemblies/library/models/seedcase/seeed-top.stl', true); 18 | const bottomShell = loadMesh('assemblies/library/models/seedcase/seeed-bottom.stl', true); 19 | const assembled = union(topShell, bottomShell); 20 | return centerOnOrigin(assembled); 21 | }; 22 | 23 | export default buildSeeedCase; 24 | -------------------------------------------------------------------------------- /viewer/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | find_package(raylib 4.0 REQUIRED) 2 | 3 | add_library(dingcad_quickjs STATIC 4 | ${PROJECT_SOURCE_DIR}/vendor/quickjs/quickjs.c 5 | ${PROJECT_SOURCE_DIR}/vendor/quickjs/quickjs-libc.c 6 | ${PROJECT_SOURCE_DIR}/vendor/quickjs/cutils.c 7 | ${PROJECT_SOURCE_DIR}/vendor/quickjs/libunicode.c 8 | ${PROJECT_SOURCE_DIR}/vendor/quickjs/libregexp.c 9 | ${PROJECT_SOURCE_DIR}/vendor/quickjs/dtoa.c 10 | ) 11 | target_include_directories(dingcad_quickjs PUBLIC ${PROJECT_SOURCE_DIR}/vendor/quickjs) 12 | set_target_properties(dingcad_quickjs PROPERTIES LINKER_LANGUAGE C) 13 | 14 | add_executable(dingcad_viewer 15 | main.cpp 16 | js_bindings.cpp 17 | ) 18 | 19 | target_include_directories(dingcad_viewer 20 | PRIVATE 21 | ${PROJECT_SOURCE_DIR}/vendor/manifold/include 22 | ${PROJECT_SOURCE_DIR}/vendor/quickjs 23 | ) 24 | 25 | target_link_libraries(dingcad_viewer 26 | PRIVATE 27 | manifold 28 | raylib 29 | dingcad_quickjs 30 | ) 31 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.18) 2 | project(dingcad LANGUAGES C CXX) 3 | 4 | set(CMAKE_CXX_STANDARD 17) 5 | set(CMAKE_C_STANDARD 11) 6 | set(CMAKE_POSITION_INDEPENDENT_CODE ON) 7 | 8 | if(NOT DEFINED TBB_DIR) 9 | if(EXISTS "/opt/homebrew/opt/tbb/lib/cmake/TBB") 10 | set(TBB_DIR "/opt/homebrew/opt/tbb/lib/cmake/TBB" CACHE PATH "" FORCE) 11 | elseif(EXISTS "/opt/homebrew/Cellar/tbb/2022.2.0/lib/cmake/TBB") 12 | set(TBB_DIR "/opt/homebrew/Cellar/tbb/2022.2.0/lib/cmake/TBB" CACHE PATH "" FORCE) 13 | endif() 14 | endif() 15 | 16 | list(APPEND CMAKE_PREFIX_PATH 17 | /opt/homebrew/opt/tbb 18 | /opt/homebrew/Cellar/tbb/2022.2.0 19 | ) 20 | 21 | set(MANIFOLD_TEST OFF CACHE BOOL "" FORCE) 22 | set(MANIFOLD_PYBIND OFF CACHE BOOL "" FORCE) 23 | set(MANIFOLD_JSBIND OFF CACHE BOOL "" FORCE) 24 | set(MANIFOLD_CROSS_SECTION OFF CACHE BOOL "" FORCE) 25 | set(MANIFOLD_EXPORT ON CACHE BOOL "" FORCE) 26 | set(MANIFOLD_PAR ON CACHE BOOL "" FORCE) 27 | 28 | add_subdirectory(vendor/manifold EXCLUDE_FROM_ALL) 29 | add_subdirectory(viewer) 30 | -------------------------------------------------------------------------------- /assemblies/SleevedTripleAssembly.js: -------------------------------------------------------------------------------- 1 | import buildTripleSteppedAssembly from './TripleSteppedPrism.js'; 2 | 3 | const SLEEVE_CONFIG = { 4 | width: 8, 5 | height: 19, 6 | length: 8, 7 | wall: 0.6, 8 | rotation: [0, 0, 0], 9 | translation: [-13, 0, 4], 10 | }; 11 | 12 | const SLEEVE_CONFIG_2 = { 13 | width: 6.7 + 1.1, 14 | height: 15.5 + 1.1, 15 | length: 8, 16 | wall: 0.6, 17 | rotation: [0, 0, 0], 18 | translation: [-13 - 7.4, 0, 4], 19 | }; 20 | 21 | const buildSleeve = ({ 22 | width, 23 | height, 24 | length, 25 | wall, 26 | rotation = [0, 0, 0], 27 | translation = [0, 0, 0], 28 | }) => { 29 | const outerSize = [width, height, length]; 30 | const innerSize = [width - 2 * wall, height - 2 * wall, length]; 31 | if (innerSize[0] <= 0 || innerSize[1] <= 0 || length <= 0) { 32 | throw new Error('Sleeve dimensions invalid: wall thickness too large or length non-positive.'); 33 | } 34 | 35 | const outer = cube({ size: outerSize, center: true }); 36 | const inner = cube({ size: innerSize, center: true }); 37 | const sleeveBody = difference(outer, inner); 38 | return translate(rotate(sleeveBody, rotation), translation); 39 | }; 40 | 41 | export const buildSleevedTripleAssembly = () => { 42 | const baseScene = buildTripleSteppedAssembly(); 43 | const sleeve = buildSleeve(SLEEVE_CONFIG); 44 | const sleeve2 = buildSleeve(SLEEVE_CONFIG_2); 45 | return union(baseScene, sleeve, sleeve2); 46 | }; 47 | 48 | export default buildSleevedTripleAssembly; 49 | -------------------------------------------------------------------------------- /assemblies/library/util/ifItFitsIsits.js: -------------------------------------------------------------------------------- 1 | export function ifItFitsIsitsBox(manifold, padding = 2) { 2 | const bounds = boundingBox(manifold); 3 | 4 | const [minX, minY, minZ] = bounds.min; 5 | const [maxX, maxY, maxZ] = bounds.max; 6 | 7 | const extentX = maxX - minX; 8 | const extentY = maxY - minY; 9 | const extentZ = maxZ - minZ; 10 | 11 | const centerShiftX = -((minX + maxX) / 2); 12 | const centerShiftY = -((minY + maxY) / 2); 13 | 14 | const boxSize = [ 15 | extentX + padding, 16 | extentY + padding, 17 | extentZ + padding, 18 | ]; 19 | 20 | const padHalf = padding / 2; 21 | 22 | const centeredManifold = translate(manifold, [centerShiftX, centerShiftY, 0]); 23 | 24 | const box = translate( 25 | cube({ 26 | size: boxSize, 27 | center: false, 28 | }), 29 | [minX + centerShiftX - padHalf, minY + centerShiftY - padHalf, minZ] 30 | ); 31 | 32 | return difference(box, centeredManifold); 33 | } 34 | 35 | export function ifItFitsIsits(manifold, scaleFactor = 1.03) { 36 | if (scaleFactor <= 1) { 37 | throw new Error('scaleFactor must be greater than 1 to create clearance.'); 38 | } 39 | 40 | const bounds = boundingBox(manifold); 41 | 42 | const centerX = (bounds.min[0] + bounds.max[0]) / 2; 43 | const centerY = (bounds.min[1] + bounds.max[1]) / 2; 44 | const baseZ = bounds.min[2]; 45 | 46 | const toOrigin = [-centerX, -centerY, -baseZ]; 47 | const fromOrigin = [centerX, centerY, baseZ]; 48 | 49 | const centered = translate(manifold, toOrigin); 50 | const scaled = scale(centered, [scaleFactor, scaleFactor, scaleFactor]); 51 | 52 | const cavity = difference(scaled, centered); 53 | 54 | return translate(cavity, fromOrigin); 55 | } 56 | 57 | export default ifItFitsIsits; 58 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # DingCAD JavaScript API Cheat Sheet 2 | 3 | - cube{options: {size:[x,y,z], center:bool}} 4 | - sphere{options: {radius:number}} 5 | - cylinder{options: {height:number, radius:number, radiusTop:number, center:bool}} 6 | - tetrahedron{} 7 | - compose{...manifolds | manifolds[]} 8 | - decompose{manifold} 9 | - union{...manifolds} 10 | - difference{...manifolds} 11 | - intersection{...manifolds} 12 | - boolean{a,b,op:"add"|"subtract"|"intersect"} 13 | - batchBoolean{op:"add"|"subtract"|"intersect", manifolds[] | ...manifolds} 14 | - translate{manifold,[dx,dy,dz]} 15 | - scale{manifold, factor|[sx,sy,sz]} 16 | - rotate{manifold,[rx,ry,rz]} 17 | - mirror{manifold,[nx,ny,nz]} 18 | - transform{manifold,[m00,m01,m02,m03,...,m22,m23]} 19 | - trimByPlane{manifold,[nx,ny,nz],offset} 20 | - hull{...manifolds | manifolds[]} 21 | - hullPoints{[[x,y,z],...]} 22 | - compose polygons as [[x,y],...] loops grouped like [loop0, loop1,...] 23 | - extrude{polygons, options:{height:number, divisions?:int, twistDegrees?:number, scaleTop?:number|[sx,sy]}} 24 | - revolve{polygons, options?:{segments?:int, degrees?:number}} 25 | - slice{manifold, height?:number} 26 | - project{manifold} 27 | - levelSet{options:{sdf(point:[x,y,z])=>number, bounds:{min:[x,y,z], max:[x,y,z]}, edgeLength:number, level?:number, tolerance?:number}} 28 | - loadMesh{path:string, forceCleanup?:bool} 29 | - setTolerance{manifold, tolerance} 30 | - getTolerance{manifold} 31 | - simplify{manifold, tolerance?} 32 | - refine{manifold, iterations} 33 | - refineToLength{manifold, length} 34 | - refineToTolerance{manifold, tolerance} 35 | - smoothByNormals{manifold, normalIdx} 36 | - smoothOut{manifold, minSharpAngle?, minSmoothness?} 37 | - calculateNormals{manifold, normalIdx, minSharpAngle?} 38 | - calculateCurvature{manifold, gaussianIdx, meanIdx} 39 | - asOriginal{manifold} 40 | - originalId{manifold} 41 | - reserveIds{count} 42 | - surfaceArea{manifold} 43 | - volume{manifold} 44 | - boundingBox{manifold} // returns {min:[x,y,z], max:[x,y,z]} 45 | - minGap{manifoldA, manifoldB, searchLength} 46 | - isEmpty{manifold} 47 | - status{manifold} 48 | - numTriangles{manifold} 49 | - numVertices{manifold} 50 | - numEdges{manifold} 51 | - numProperties{manifold} 52 | - numPropertyVertices{manifold} 53 | - genus{manifold} 54 | - decompose polygons back to JS with slice/project return [[x,y],...] loops 55 | 56 | Assign your final solid to `scene` to render, e.g. `scene = cube({...});`. 57 | -------------------------------------------------------------------------------- /assemblies/library/DTSPCB.js: -------------------------------------------------------------------------------- 1 | const pcbWidth = 21; 2 | const pcbHeight = 15; 3 | const pcbThickness = 2.6; 4 | 5 | const componentSize = 8.8; 6 | const componentTotalHeight = 8.1; 7 | const smallComponentSize = 5.1; 8 | const smallComponentTotalHeight = 6.2; 9 | const connectorWidth = 6.0; 10 | const connectorDepth = 8.0; 11 | const connectorTotalHeight = 2.6; 12 | const connectorGapX = 0.4; 13 | 14 | export function buildDTSPCBBoard() { 15 | return cube({ size: [pcbWidth, pcbHeight, pcbThickness], center: false }); 16 | } 17 | 18 | export function buildDTSPCB() { 19 | const pcb = cube({ size: [pcbWidth, pcbHeight, pcbThickness], center: false }); 20 | 21 | const componentHeight = componentTotalHeight - pcbThickness; // keep total stack height at 8.1mm 22 | const componentOffsetX = pcbWidth - 3.7 - componentSize; 23 | const componentOffsetY = pcbHeight - 3 - componentSize; 24 | 25 | // Align the component so its top-right corner is inset 3.7mm on X and 3mm on Y from the PCB corner. 26 | const component = translate( 27 | cube({ size: [componentSize, componentSize, componentHeight], center: false }), 28 | [componentOffsetX, componentOffsetY, pcbThickness] 29 | ); 30 | 31 | const smallComponentHeight = smallComponentTotalHeight - pcbThickness; // total height from PCB bottom is 6.2mm 32 | const smallGapX = 1.4; 33 | const smallOverlapX = 0.2; // extend to overlap the larger block slightly 34 | const smallComponentLength = smallComponentSize + smallGapX + smallOverlapX; 35 | const smallOffsetX = componentOffsetX - smallGapX - smallComponentSize; 36 | const marginY = (pcbHeight - smallComponentSize) / 2; // ~5mm clearance from top and bottom 37 | 38 | const smallComponent = translate( 39 | cube({ size: [smallComponentLength, smallComponentSize, smallComponentHeight], center: false }), 40 | [smallOffsetX, marginY, pcbThickness] 41 | ); 42 | 43 | const connectorHeight = connectorTotalHeight - pcbThickness; // keeps top flush with connectorTotalHeight 44 | const connectorOffsetX = Math.min(componentOffsetX + componentSize + connectorGapX, pcbWidth - connectorWidth); 45 | const connectorOffsetY = componentOffsetY + componentSize / 2 - connectorDepth / 2; 46 | 47 | const connector = translate( 48 | cube({ size: [connectorWidth, connectorDepth, connectorHeight], center: false }), 49 | [connectorOffsetX, connectorOffsetY, pcbThickness] 50 | ); 51 | 52 | return union(pcb, component, smallComponent, connector); 53 | } 54 | 55 | export default buildDTSPCB; 56 | -------------------------------------------------------------------------------- /assemblies/library/xiao_esp32c3.js: -------------------------------------------------------------------------------- 1 | const PCB_L = 21.0; 2 | const PCB_W = 17.5; 3 | const PCB_T = 0.8; 4 | 5 | const MH_D = 2.0; 6 | const MH_X = 11.4 / 2; 7 | const MH_Y = 11.4 / 2; 8 | 9 | const USB_W = 8.0; 10 | const USB_D = 6.0; 11 | const USB_H = 3.0; 12 | const USB_Z = PCB_T; 13 | 14 | const SHIELD_L = 12.0; 15 | const SHIELD_W = 10.0; 16 | const SHIELD_H = 1.4; 17 | 18 | const ANTENNA_L = 8.5; 19 | const ANTENNA_W = 3.0; 20 | const ANTENNA_H = 0.4; 21 | 22 | const LED_L = 1.4; 23 | const LED_W = 1.8; 24 | const LED_H = 0.5; 25 | 26 | const CASTELLATION_COUNT = 11; 27 | const CASTELLATION_PITCH = 2.0; 28 | const CASTELLATION_WIDTH = 1.0; 29 | const CASTELLATION_DEPTH = 1.4; 30 | 31 | export function buildXiaoEsp32C3() { 32 | const pcb = translate( 33 | cube({ size: [PCB_L, PCB_W, PCB_T], center: true }), 34 | [0, 0, PCB_T / 2] 35 | ); 36 | 37 | const holeHeight = PCB_T + 0.4; 38 | const hole = (x, y) => 39 | translate( 40 | cylinder({ height: holeHeight, radius: MH_D / 2, center: true }), 41 | [x, y, PCB_T / 2] 42 | ); 43 | 44 | const holes = union( 45 | hole(MH_X, MH_Y), 46 | hole(MH_X, -MH_Y), 47 | hole(-MH_X, MH_Y), 48 | hole(-MH_X, -MH_Y) 49 | ); 50 | 51 | const castellations = (() => { 52 | const notches = []; 53 | const xStart = -((CASTELLATION_COUNT - 1) / 2) * CASTELLATION_PITCH; 54 | for (let i = 0; i < CASTELLATION_COUNT; ++i) { 55 | const x = xStart + i * CASTELLATION_PITCH; 56 | const makeNotch = sign => 57 | translate( 58 | cube({ 59 | size: [CASTELLATION_WIDTH, CASTELLATION_DEPTH, PCB_T + 0.2], 60 | center: true, 61 | }), 62 | [x, sign * (PCB_W / 2 - CASTELLATION_DEPTH / 2 + 0.05), PCB_T / 2] 63 | ); 64 | notches.push(makeNotch(1)); 65 | notches.push(makeNotch(-1)); 66 | } 67 | return union(...notches); 68 | })(); 69 | 70 | const boardProfile = difference(pcb, union(holes, castellations)); 71 | 72 | const usb = translate( 73 | cube({ size: [USB_D, USB_W, USB_H], center: true }), 74 | [PCB_L / 2 + USB_D / 2 - 0.1, 0, USB_Z + USB_H / 2] 75 | ); 76 | 77 | const shield = translate( 78 | cube({ size: [SHIELD_L, SHIELD_W, SHIELD_H], center: true }), 79 | [-2.0, 0, PCB_T + SHIELD_H / 2] 80 | ); 81 | 82 | const antenna = translate( 83 | cube({ size: [ANTENNA_L, ANTENNA_W, ANTENNA_H], center: true }), 84 | [PCB_L / 2 - ANTENNA_L / 2 - 1.0, 0, PCB_T + ANTENNA_H / 2] 85 | ); 86 | 87 | const statusLed = translate( 88 | cube({ size: [LED_L, LED_W, LED_H], center: true }), 89 | [-PCB_L / 2 + LED_L, -4.0, PCB_T + LED_H / 2] 90 | ); 91 | 92 | return union(boardProfile, usb, shield, antenna, statusLed); 93 | } 94 | 95 | export default buildXiaoEsp32C3; 96 | 97 | export function importXiaoEsp32C3({ path = './models/seedespc3.dxf', forceCleanup = true } = {}) { 98 | if (typeof loadMesh !== 'function') { 99 | throw new Error('loadMesh binding is not available; rebuild the viewer with mesh import support.'); 100 | } 101 | return loadMesh(path, forceCleanup); 102 | } 103 | 104 | -------------------------------------------------------------------------------- /assemblies/library/radio_enclosure.js: -------------------------------------------------------------------------------- 1 | // the back case of a radio; with cylinders that allow for m2 bolts to go through to the end. the box has rounded corners on the outside, making it look spic and span 2 | // basically, a box to hide my PCB slop 3 | 4 | const plateWidth = 62; 5 | const plateHeight = 42; 6 | const plateThickness = 3; 7 | const cornerRadius = 3; 8 | 9 | const wallHeight = 30; 10 | const wallThickness = 2; 11 | 12 | const holeDiameter = 2.2; // M2 clearance 13 | const bossDiameter = 6; 14 | const bossHeight = wallHeight; 15 | 16 | const holeRadius = holeDiameter / 2; 17 | const bossRadius = bossDiameter / 2; 18 | 19 | const holeSpacingX = 54; 20 | const holeSpacingY = 32; 21 | 22 | function roundedRectSolid(width, height, thickness, radius, options = {}) { 23 | const { center = true } = options; 24 | const clampedRadius = Math.min(Math.max(radius, 0), width / 2, height / 2); 25 | if (clampedRadius === 0) { 26 | return cube({ size: [width, height, thickness], center }); 27 | } 28 | const offsetX = width / 2 - clampedRadius; 29 | const offsetY = height / 2 - clampedRadius; 30 | const corner = cylinder({ height: thickness, radius: clampedRadius, center: true }); 31 | const corners = [ 32 | [-offsetX, -offsetY], 33 | [offsetX, -offsetY], 34 | [-offsetX, offsetY], 35 | [offsetX, offsetY], 36 | ].map(([x, y]) => translate(corner, [x, y, 0])); 37 | const solid = hull(...corners); 38 | return center ? solid : translate(solid, [0, 0, thickness / 2]); 39 | } 40 | 41 | export function buildRadioEnclosure() { 42 | const base = roundedRectSolid(plateWidth, plateHeight, plateThickness, cornerRadius); 43 | 44 | const wallOuter = translate( 45 | roundedRectSolid(plateWidth, plateHeight, wallHeight, cornerRadius), 46 | [0, 0, plateThickness / 2 + wallHeight / 2] 47 | ); 48 | 49 | const innerWidth = Math.max(plateWidth - 2 * wallThickness, plateWidth * 0.1); 50 | const innerHeight = Math.max(plateHeight - 2 * wallThickness, plateHeight * 0.1); 51 | const innerRadius = Math.max(cornerRadius - wallThickness, 0); 52 | 53 | const wallInner = translate( 54 | roundedRectSolid(innerWidth, innerHeight, wallHeight, innerRadius), 55 | [0, 0, plateThickness / 2 + wallHeight / 2] 56 | ); 57 | 58 | const walls = difference(wallOuter, wallInner); 59 | 60 | const holeCenters = [ 61 | [-holeSpacingX / 2, -holeSpacingY / 2], 62 | [holeSpacingX / 2, -holeSpacingY / 2], 63 | [-holeSpacingX / 2, holeSpacingY / 2], 64 | [holeSpacingX / 2, holeSpacingY / 2], 65 | ]; 66 | 67 | const bossSolid = cylinder({ height: bossHeight, radius: bossRadius, center: true }); 68 | const baseTop = plateThickness / 2; 69 | const bosses = holeCenters.map(([x, y]) => 70 | translate(bossSolid, [x, y, baseTop + bossHeight / 2]) 71 | ); 72 | 73 | const body = union(base, walls, ...bosses); 74 | 75 | const bodyTop = baseTop + Math.max(wallHeight, bossHeight); 76 | const bodyBottom = -plateThickness / 2; 77 | const holeHeight = bodyTop - bodyBottom + 4; 78 | const holeMid = (bodyTop + bodyBottom) / 2; 79 | const throughHoleSolid = cylinder({ height: holeHeight, radius: holeRadius, center: true }); 80 | const throughHoles = holeCenters.map(([x, y]) => translate(throughHoleSolid, [x, y, holeMid])); 81 | 82 | const holeCutout = union(...throughHoles); 83 | return difference(body, holeCutout); 84 | } 85 | 86 | export default buildRadioEnclosure; 87 | -------------------------------------------------------------------------------- /assemblies/TripleSteppedPrism.js: -------------------------------------------------------------------------------- 1 | import buildDTSPCB from './library/DTSPCB.js'; 2 | 3 | const centerOnXY = (manifold) => { 4 | const bounds = boundingBox(manifold); 5 | const centerX = (bounds.min[0] + bounds.max[0]) / 2; 6 | const centerY = (bounds.min[1] + bounds.max[1]) / 2; 7 | return translate(manifold, [-centerX, -centerY, 0]); 8 | }; 9 | 10 | const roundedRectPrism = (width, height, depth, radius) => { 11 | const clampedRadius = Math.min(Math.max(radius, 0), width / 2, height / 2); 12 | if (clampedRadius === 0) { 13 | return cube({ size: [width, height, depth], center: false }); 14 | } 15 | const corner = cylinder({ height: depth, radius: clampedRadius, center: true }); 16 | const offsets = [ 17 | [-width / 2 + clampedRadius, -height / 2 + clampedRadius], 18 | [width / 2 - clampedRadius, -height / 2 + clampedRadius], 19 | [-width / 2 + clampedRadius, height / 2 - clampedRadius], 20 | [width / 2 - clampedRadius, height / 2 - clampedRadius], 21 | ]; 22 | const corners = offsets.map(([x, y]) => translate(corner, [x, y, 0])); 23 | const roundedCenter = hull(...corners); 24 | return translate(roundedCenter, [width / 2, height / 2, depth / 2]); 25 | }; 26 | 27 | const buildSteppedPrismScene = () => { 28 | const pcbWidth = 16; 29 | const pcbHeight = 13; 30 | const pcbThickness = 3.8; 31 | const topSectionBaseWidth = 6; 32 | const bottomSectionBaseWidth = 12; 33 | const widthScale = pcbWidth / (topSectionBaseWidth + bottomSectionBaseWidth); 34 | const topSectionWidth = topSectionBaseWidth * widthScale; 35 | const bottomSectionWidth = bottomSectionBaseWidth * widthScale; 36 | const bottomSectionThicknessFactor = 2; 37 | const behindScale = 1.35; 38 | 39 | const frontCornerRadius = 0; 40 | const pcbLeft = roundedRectPrism(topSectionWidth, pcbHeight, pcbThickness, frontCornerRadius); 41 | const pcbRight = translate( 42 | roundedRectPrism(bottomSectionWidth, pcbHeight, pcbThickness * bottomSectionThicknessFactor, frontCornerRadius), 43 | [topSectionWidth, 0, 0] 44 | ); 45 | const behindSize = [pcbWidth * behindScale, pcbHeight * behindScale, pcbThickness * (behindScale + 0.1)]; 46 | const behindDelta = [ 47 | behindSize[0] - pcbWidth, 48 | behindSize[1] - pcbHeight, 49 | behindSize[2] - pcbThickness, 50 | ]; 51 | const behindCornerRadius = 0; 52 | const pcbBehind = translate( 53 | roundedRectPrism(behindSize[0], behindSize[1], behindSize[2], behindCornerRadius), 54 | [-behindDelta[0] / 2, -behindDelta[1] / 2, -2 - behindDelta[2] / 2] 55 | ); 56 | 57 | const centeredLeft = centerOnXY(pcbLeft); 58 | const centeredRight = centerOnXY(pcbRight); 59 | const centeredBehind = centerOnXY(pcbBehind); 60 | const centeredModule = centerOnXY(buildDTSPCB()); 61 | 62 | const rotatedModule = rotate(centeredModule, [0, 90, 0]); 63 | const rotatedLeft = rotate(centeredLeft, [0, 90, 0]); 64 | const rotatedRight = rotate(centeredRight, [0, 90, 0]); 65 | const rotatedBehind = rotate(centeredBehind, [0, 90, 0]); 66 | const offsetLeft = translate(rotatedLeft, [1, 0, -1]); 67 | const offsetRight = translate(rotatedRight, [0.5, 0, -2.5]); 68 | const offsetBehind = translate(rotatedBehind, [3, 0, 1]); 69 | 70 | const leftCut = difference(offsetLeft, rotatedModule); 71 | const rightCut = difference(offsetRight, rotatedModule); 72 | const backCut = difference(offsetBehind, rotatedModule); 73 | const combined = union(leftCut, rightCut, backCut); 74 | 75 | const bounds = boundingBox(combined); 76 | return translate(combined, [ 77 | -(bounds.min[0] + bounds.max[0]) / 2, 78 | -(bounds.min[1] + bounds.max[1]) / 2, 79 | -bounds.min[2], 80 | ]); 81 | }; 82 | 83 | const buildTripleSteppedPrismScene = () => { 84 | const base = buildSteppedPrismScene(); 85 | const radius = 11.5; 86 | const baseAngles = [0, 90, 180]; 87 | const anglesDeg = baseAngles.map((angle) => angle - 90); 88 | 89 | const modules = anglesDeg.map((angle) => { 90 | const angleRad = (angle * Math.PI) / 180; 91 | const rotated = rotate(base, [0, 0, angle]); 92 | const offset = [Math.cos(angleRad) * radius, Math.sin(angleRad) * radius, 0]; 93 | return translate(rotated, offset); 94 | }); 95 | 96 | const trio = union(...modules); 97 | const stackedBase = rotate(base, [0, -90, 180]); 98 | const stackedBounds = boundingBox(stackedBase); 99 | const baseBounds = boundingBox(base); 100 | const stackSpacing = 4; 101 | const stackedOffsetX = -3; 102 | const stackedDrop = 5; 103 | const stacked = translate(stackedBase, [ 104 | -(stackedBounds.min[0] + stackedBounds.max[0]) / 2 + stackedOffsetX, 105 | -(stackedBounds.min[1] + stackedBounds.max[1]) / 2, 106 | baseBounds.max[2] + stackSpacing - stackedBounds.min[2] - stackedDrop, 107 | ]); 108 | 109 | const quartet = union(trio, stacked); 110 | const trioBounds = boundingBox(quartet); 111 | return translate(quartet, [ 112 | -(trioBounds.min[0] + trioBounds.max[0]) / 2, 113 | -(trioBounds.min[1] + trioBounds.max[1]) / 2, 114 | -trioBounds.min[2], 115 | ]); 116 | }; 117 | 118 | export const buildTripleSteppedAssembly = () => { 119 | const baseScene = buildTripleSteppedPrismScene(); 120 | const sceneScale = 1.01; 121 | return scale(baseScene, sceneScale); 122 | }; 123 | 124 | export default buildTripleSteppedAssembly; 125 | -------------------------------------------------------------------------------- /assemblies/4axisArray.js: -------------------------------------------------------------------------------- 1 | import buildDTSPCB from './library/DTSPCB.js'; 2 | 3 | const roundedBox = (width, depth, height, radius) => { 4 | const clamped = Math.min(Math.max(radius, 0), width / 2, depth / 2); 5 | if (clamped === 0) { 6 | return cube({ size: [width, depth, height], center: true }); 7 | } 8 | const corner = cylinder({ height, radius: clamped, center: true }); 9 | const offsetX = width / 2 - clamped; 10 | const offsetY = depth / 2 - clamped; 11 | const corners = [ 12 | [-offsetX, -offsetY], 13 | [offsetX, -offsetY], 14 | [-offsetX, offsetY], 15 | [offsetX, offsetY], 16 | ].map(([x, y]) => translate(corner, [x, y, 0])); 17 | return hull(...corners); 18 | }; 19 | 20 | const centerOnFloor = (manifold) => { 21 | const bounds = boundingBox(manifold); 22 | const centerX = (bounds.min[0] + bounds.max[0]) / 2; 23 | const centerY = (bounds.min[1] + bounds.max[1]) / 2; 24 | return translate(manifold, [-centerX, -centerY, -bounds.min[2]]); 25 | }; 26 | 27 | const rawModule = buildDTSPCB(); 28 | const esp32MeshPath = '~/Downloads/models/seedespc3.stl'; 29 | const rawEsp32Mesh = loadMesh(esp32MeshPath, true); 30 | 31 | // Connector dimensions match the DTS PCB layout so we can clear the shell wall. 32 | const pcbWidth = 21; 33 | const pcbHeight = 15; 34 | const pcbThickness = 1.6; 35 | const componentSize = 8.8; 36 | const connectorWidth = 6.0; 37 | const connectorDepth = 8.0; 38 | const connectorTotalHeight = 2.6; 39 | const connectorGapX = 0.4; 40 | 41 | const rawModuleBounds = boundingBox(rawModule); 42 | const moduleCenterX = (rawModuleBounds.min[0] + rawModuleBounds.max[0]) / 2; 43 | const moduleCenterY = (rawModuleBounds.min[1] + rawModuleBounds.max[1]) / 2; 44 | 45 | const componentOffsetX = pcbWidth - 3.7 - componentSize; 46 | const componentOffsetY = pcbHeight - 3 - componentSize; 47 | const connectorOffsetX = Math.min(componentOffsetX + componentSize + connectorGapX, pcbWidth - connectorWidth); 48 | const connectorOffsetY = componentOffsetY + componentSize / 2 - connectorDepth / 2; 49 | const connectorHeight = connectorTotalHeight - pcbThickness; 50 | 51 | const centeredConnector = [ 52 | connectorOffsetX + connectorWidth / 2 - moduleCenterX, 53 | connectorOffsetY + connectorDepth / 2 - moduleCenterY, 54 | pcbThickness + connectorHeight / 2, 55 | ]; 56 | 57 | // Stand the board on its width so the long edge becomes vertical, then center on the floor plane. 58 | const baseModule = centerOnFloor(rotate(rawModule, [0, 90, 0])); 59 | 60 | const baseBounds = boundingBox(baseModule); 61 | const halfDepth = (baseBounds.max[0] - baseBounds.min[0]) / 2; 62 | const spacing = 8; 63 | 64 | const angles = [ 90, 180, 270]; 65 | const harnessModules = angles.map((angle) => { 66 | const oriented = rotate(baseModule, [0, 0, angle]); 67 | const radians = (angle * Math.PI) / 180; 68 | const offset = [ 69 | Math.cos(radians) * (halfDepth + spacing), 70 | Math.sin(radians) * (halfDepth + spacing), 71 | 0, 72 | ]; 73 | return translate(oriented, offset); 74 | }); 75 | 76 | 77 | const size = 28.3; 78 | const pillarSize = [size, size, size-.1]; 79 | const outerCornerRadius = 3; 80 | const centeredPillar = roundedBox(pillarSize[0], pillarSize[1], pillarSize[2], outerCornerRadius); 81 | const pillarBounds = boundingBox(centeredPillar); 82 | const pillarFloorAligned = translate(centeredPillar, [0, 0, -pillarBounds.min[2]]); 83 | 84 | const wallThickness = 5.; 85 | const innerSize = size - wallThickness * 2; 86 | const innerPillar = cube({ size: [21, 21, 22], center: true }); 87 | const innerBounds = boundingBox(innerPillar); 88 | const innerFloorAligned = translate(innerPillar, [0, 0, -innerBounds.min[2]]); 89 | 90 | const hollowPillar = difference(pillarFloorAligned, innerFloorAligned); 91 | 92 | const sixthModuleOffset = [0, 0, 22]; 93 | const sixthModule = translate(centerOnFloor(rawModule), sixthModuleOffset); 94 | const esp32Mesh = translate( 95 | rotate(centerOnFloor(rawEsp32Mesh), [0, 90, 0]), 96 | [ 97 | sixthModuleOffset[0] + 10, 98 | sixthModuleOffset[1], 99 | sixthModuleOffset[2] - 10, 100 | ], 101 | ); 102 | const scaledModules = union(...harnessModules, sixthModule); 103 | const harnessShell = scale(difference(hollowPillar, scaledModules), 1.035); 104 | 105 | const connectorInsideClearance = 3.0; 106 | const connectorOutsideExtension = 5.0; 107 | const connectorLateralClearance = 8; 108 | const connectorVerticalClearance = 4; 109 | 110 | const connectorHoleLength = connectorWidth + connectorInsideClearance + connectorOutsideExtension; 111 | const connectorHoleDepth = connectorDepth + connectorLateralClearance; 112 | const connectorHoleHeight = connectorHeight + connectorVerticalClearance; 113 | 114 | const connectorHoleCenter = [ 115 | centeredConnector[0] + (connectorOutsideExtension - connectorInsideClearance) / 2 + sixthModuleOffset[0], 116 | centeredConnector[1] + sixthModuleOffset[1], 117 | centeredConnector[2] + sixthModuleOffset[2] - 3, 118 | ]; 119 | 120 | const connectorHole = translate( 121 | cube({ 122 | size: [connectorHoleLength, connectorHoleDepth, connectorHoleHeight], 123 | center: true, 124 | }), 125 | connectorHoleCenter, 126 | ); 127 | 128 | 129 | const fronthole = translate( 130 | cube({size: [5, 20, 33], center: true}), 131 | [10, 0, 10] 132 | ); 133 | 134 | const harnessWithConnector = difference(harnessShell, connectorHole, fronthole); 135 | 136 | // export const scene = harnessWithConnector; 137 | export const scene = harnessWithConnector; 138 | -------------------------------------------------------------------------------- /viewer/scene.js: -------------------------------------------------------------------------------- 1 | const baseShapes = []; 2 | 3 | function failMarker() { 4 | const pillar = cube({ size: [0.6, 1.6, 0.6], center: true }); 5 | return translate(pillar, [0, 0.8, 0]); 6 | } 7 | 8 | function test(name, builder) { 9 | try { 10 | const result = builder(); 11 | if (!result) throw new Error(`${name} returned null`); 12 | baseShapes.push(result); 13 | } catch (error) { 14 | baseShapes.push(failMarker()); 15 | } 16 | } 17 | 18 | test("cube", () => cube({ size: [1, 1, 1], center: true })); 19 | 20 | test("sphere", () => sphere({ radius: 0.7 })); 21 | 22 | test("cylinder", () => cylinder({ height: 1.2, radius: 0.35, center: true })); 23 | 24 | test("tetrahedron", () => tetrahedron()); 25 | 26 | test("union", () => union( 27 | cube({ size: [0.8, 0.8, 0.8], center: true }), 28 | translate(sphere({ radius: 0.45 }), [0.4, 0.2, 0]) 29 | )); 30 | 31 | test("difference", () => difference( 32 | cube({ size: [1, 1, 1], center: true }), 33 | translate(sphere({ radius: 0.6 }), [0.2, 0.2, 0]) 34 | )); 35 | 36 | test("intersection", () => intersection( 37 | sphere({ radius: 0.7 }), 38 | translate(cube({ size: [1.1, 1.1, 1.1], center: true }), [0.2, 0, 0]) 39 | )); 40 | 41 | test("translate", () => translate( 42 | cube({ size: [0.6, 0.6, 0.6], center: true }), 43 | [0.4, 0.3, 0] 44 | )); 45 | 46 | test("scale", () => scale(sphere({ radius: 0.6 }), 0.6)); 47 | 48 | test("rotate", () => rotate( 49 | cylinder({ height: 1.0, radius: 0.3, center: true }), 50 | [45, 0, 30] 51 | )); 52 | 53 | test("mirror", () => mirror( 54 | translate(cube({ size: [0.4, 0.8, 0.4], center: true }), [0.4, 0.3, 0]), 55 | [1, 0, 0] 56 | )); 57 | 58 | test("transform", () => transform( 59 | cube({ size: [0.6, 0.6, 0.6], center: true }), 60 | [1, 0, 0, 0.3, 61 | 0, 1, 0, 0.2, 62 | 0, 0, 1, 0.1] 63 | )); 64 | 65 | test("compose", () => compose([ 66 | translate(cube({ size: [0.5, 0.5, 0.5], center: true }), [-0.35, 0, 0]), 67 | translate(sphere({ radius: 0.35 }), [0.4, 0, 0]) 68 | ])); 69 | 70 | test("decompose", () => { 71 | const combo = compose([ 72 | translate(cube({ size: [0.4, 0.4, 0.4], center: true }), [-0.35, 0, 0]), 73 | translate(cube({ size: [0.4, 0.4, 0.4], center: true }), [0.45, 0, 0]) 74 | ]); 75 | const parts = decompose(combo); 76 | return compose(parts); 77 | }); 78 | 79 | test("boolean", () => boolean( 80 | cube({ size: [0.9, 0.9, 0.9], center: true }), 81 | translate(sphere({ radius: 0.55 }), [0.35, 0, 0]), 82 | "difference" 83 | )); 84 | 85 | test("batchBoolean", () => batchBoolean("add", [ 86 | translate(cube({ size: [0.4, 0.4, 0.4], center: true }), [-0.45, 0, 0]), 87 | cube({ size: [0.4, 0.4, 0.4], center: true }), 88 | translate(cube({ size: [0.4, 0.4, 0.4], center: true }), [0.45, 0, 0]) 89 | ])); 90 | 91 | test("hull", () => hull( 92 | translate(cube({ size: [0.3, 0.7, 0.3], center: true }), [-0.45, 0, 0]), 93 | translate(sphere({ radius: 0.4 }), [0.6, 0.25, 0]) 94 | )); 95 | 96 | test("hullPoints", () => hullPoints([ 97 | [-0.4, -0.4, -0.4], 98 | [0.7, -0.3, -0.1], 99 | [-0.2, 0.8, 0.2], 100 | [0.1, 0, 0.9] 101 | ])); 102 | 103 | test("trimByPlane", () => trimByPlane( 104 | cube({ size: [1, 1, 1], center: true }), 105 | [0, 1, 0], 106 | 0 107 | )); 108 | 109 | test("tolerance", () => { 110 | const base = cube({ size: [1, 1, 1], center: true }); 111 | const adjusted = setTolerance(base, 0.002); 112 | const tol = getTolerance(adjusted); 113 | return translate(scale(adjusted, 0.7), [0, tol * 120, 0]); 114 | }); 115 | 116 | test("simplify", () => { 117 | const noisy = union( 118 | sphere({ radius: 0.6 }), 119 | translate(cube({ size: [0.2, 0.2, 1.2], center: true }), [0, 0.6, 0]) 120 | ); 121 | return simplify(noisy, 0.05); 122 | }); 123 | 124 | test("refine", () => refine(sphere({ radius: 0.55 }), 1)); 125 | 126 | test("refineToLength", () => refineToLength( 127 | cylinder({ height: 1.0, radius: 0.4, center: true }), 128 | 0.3 129 | )); 130 | 131 | test("refineToTolerance", () => refineToTolerance( 132 | cube({ size: [0.9, 0.9, 0.9], center: true }), 133 | 0.05 134 | )); 135 | 136 | test("extrude", () => { 137 | const outline = [ 138 | [[-0.4, -0.2], [0, 0.35], [0.4, -0.2]] 139 | ]; 140 | return extrude(outline, { 141 | height: 0.8, 142 | divisions: 6, 143 | twistDegrees: 15, 144 | scaleTop: [0.6, 0.6] 145 | }); 146 | }); 147 | 148 | test("revolve", () => { 149 | const profile = [ 150 | [[0, 0], [0.4, 0], [0.4, 0.6], [0, 0.6]] 151 | ]; 152 | return revolve(profile, { segments: 32, degrees: 270 }); 153 | }); 154 | 155 | test("slice", () => { 156 | const shape = union( 157 | cube({ size: [1, 1, 1], center: true }), 158 | translate(sphere({ radius: 0.5 }), [0, 0.6, 0]) 159 | ); 160 | const loops = slice(shape, 0.2); 161 | return extrude(loops, { height: 0.2 }); 162 | }); 163 | 164 | test("project", () => { 165 | const shape = rotate( 166 | cylinder({ height: 1.2, radius: 0.3, center: true }), 167 | [90, 0, 0] 168 | ); 169 | const loops = project(shape); 170 | return extrude(loops, { height: 0.2 }); 171 | }); 172 | 173 | test("levelSet", () => levelSet({ 174 | sdf: (p) => Math.sqrt(p[0] * p[0] + p[1] * p[1] + p[2] * p[2]) - 0.5 + 0.1 * Math.sin(3 * p[0]), 175 | bounds: { min: [-0.7, -0.7, -0.7], max: [0.7, 0.7, 0.7] }, 176 | edgeLength: 0.4, 177 | tolerance: 0.02 178 | })); 179 | 180 | test("minGap", () => { 181 | const a = cube({ size: [0.5, 0.5, 0.5], center: true }); 182 | const b = translate(a, [1.2, 0, 0]); 183 | const gap = minGap(a, b, 2.0); 184 | return translate(union(a, b), [gap * 0.1, 0, 0]); 185 | }); 186 | 187 | test("properties", () => { 188 | const base = sphere({ radius: 0.6 }); 189 | const withNormals = calculateNormals(base, 0, 40); 190 | const withCurvature = calculateCurvature(withNormals, -1, 0); 191 | const smoothed = smoothByNormals(withNormals, 0); 192 | const relaxed = smoothOut(smoothed, 30, 0.2); 193 | const propCount = numProperties(withCurvature); 194 | const propVerts = numPropertyVertices(withCurvature); 195 | const asOrig = asOriginal(relaxed); 196 | const id = originalId(asOrig); 197 | const nextId = reserveIds(2); 198 | const factor = propCount > 0 && propVerts > 0 ? 0.75 : 0.35; 199 | return translate(scale(asOrig, factor), [0, (id + nextId) * 0.02, 0]); 200 | }); 201 | 202 | test("metrics", () => { 203 | const core = difference( 204 | sphere({ radius: 0.7 }), 205 | cube({ size: [0.6, 0.6, 0.6], center: true }) 206 | ); 207 | const accent = translate(cylinder({ height: 0.8, radius: 0.2, center: true }), [0, 0.6, 0]); 208 | const shape = union(core, accent); 209 | const area = surfaceArea(shape); 210 | const vol = volume(shape); 211 | const box = boundingBox(shape); 212 | const tris = numTriangles(shape); 213 | const verts = numVertices(shape); 214 | const edges = numEdges(shape); 215 | const g = genus(shape); 216 | const stat = status(shape); 217 | const emptyShape = difference( 218 | cube({ size: [0.5, 0.5, 0.5], center: true }), 219 | cube({ size: [0.5, 0.5, 0.5], center: true }) 220 | ); 221 | const empty = isEmpty(emptyShape); 222 | const scaleFactor = empty ? 0.3 : Math.min(0.3 + area / 50, 0.9); 223 | const density = vol > 0 ? area / vol : 1; 224 | const combinatorics = tris + verts + edges; 225 | const lift = (box.max[1] - box.min[1]) * 0.1 + g * 0.05 + (stat === "NoError" ? 0 : 0.2) + density * 0.05 + combinatorics * 0.00005; 226 | return translate(scale(shape, scaleFactor), [0, lift, 0]); 227 | }); 228 | 229 | const maxShapes = 16; 230 | const selected = baseShapes.slice(0, maxShapes); 231 | const spacing = 3.0; 232 | 233 | if (selected.length === 0) { 234 | scene = cube({ size: [1, 1, 1], center: true }); 235 | } else { 236 | const placed = selected.map((shape, index) => 237 | translate(shape, [index * spacing, 0, 0]) 238 | ); 239 | scene = compose(placed); 240 | } 241 | -------------------------------------------------------------------------------- /viewer/main.cpp: -------------------------------------------------------------------------------- 1 | #include "raylib.h" 2 | #include "raymath.h" 3 | #include "rlgl.h" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | extern "C" { 26 | #include "quickjs.h" 27 | } 28 | 29 | #include "manifold/manifold.h" 30 | #include "manifold/polygon.h" 31 | #include "js_bindings.h" 32 | 33 | namespace { 34 | const Color kBaseColor = {210, 210, 220, 255}; 35 | const char *kBrandText = "dingcad"; 36 | constexpr float kBrandFontSize = 28.0f; 37 | constexpr float kSceneScale = 0.1f; // convert mm scene units to renderer units 38 | 39 | // GLSL 330 core (desktop). Uses raylib's default attribute/uniform names. 40 | const char* kOutlineVS = R"glsl( 41 | #version 330 42 | 43 | in vec3 vertexPosition; 44 | in vec3 vertexNormal; 45 | 46 | uniform mat4 mvp; 47 | uniform float outline; // world-units thickness 48 | 49 | void main() 50 | { 51 | // Expand along the vertex normal in model space. This is robust as long as 52 | // your model transform has no non-uniform scale (true in your code). 53 | vec3 pos = vertexPosition + normalize(vertexNormal) * outline; 54 | gl_Position = mvp * vec4(pos, 1.0); 55 | } 56 | )glsl"; 57 | 58 | const char* kOutlineFS = R"glsl( 59 | #version 330 60 | 61 | out vec4 finalColor; 62 | uniform vec4 outlineColor; 63 | 64 | void main() 65 | { 66 | // Keep only back-faces for a clean silhouette. 67 | if (gl_FrontFacing) discard; 68 | finalColor = outlineColor; 69 | } 70 | )glsl"; 71 | 72 | // Toon (cel) shading — lit 3D pass 73 | const char* kToonVS = R"glsl( 74 | #version 330 75 | in vec3 vertexPosition; 76 | in vec3 vertexNormal; 77 | uniform mat4 mvp; 78 | uniform mat4 matModel; 79 | uniform mat4 matView; 80 | out vec3 vNvs; 81 | out vec3 vVdir; // view dir in view space 82 | void main() { 83 | vec4 wpos = matModel * vec4(vertexPosition, 1.0); 84 | vec3 nvs = mat3(matView) * mat3(matModel) * vertexNormal; 85 | vNvs = normalize(nvs); 86 | vec3 vpos = (matView * wpos).xyz; 87 | vVdir = normalize(-vpos); 88 | gl_Position = mvp * vec4(vertexPosition, 1.0); 89 | } 90 | )glsl"; 91 | 92 | const char* kToonFS = R"glsl( 93 | #version 330 94 | in vec3 vNvs; 95 | in vec3 vVdir; 96 | out vec4 finalColor; 97 | 98 | uniform vec3 lightDirVS; // normalized, in view space 99 | uniform vec4 baseColor; // your kBaseColor normalized [0..1] 100 | uniform int toonSteps; // e.g. 3 or 4 101 | uniform float ambient; // e.g. 0.3 102 | uniform float diffuseWeight; // e.g. 0.7 103 | uniform float rimWeight; // e.g. 0.25 104 | uniform float specWeight; // e.g. 0.15 105 | uniform float specShininess; // e.g. 32.0 106 | 107 | float quantize(float x, int steps){ 108 | float s = max(1, steps-1); 109 | return floor(clamp(x,0.0,1.0)*s + 1e-4)/s; 110 | } 111 | 112 | void main() { 113 | vec3 n = normalize(vNvs); 114 | vec3 l = normalize(lightDirVS); 115 | vec3 v = normalize(vVdir); 116 | 117 | float ndl = max(0.0, dot(n,l)); 118 | float cel = quantize(ndl, toonSteps); 119 | 120 | // crisp rim 121 | float rim = pow(1.0 - max(0.0, dot(n, v)), 1.5); 122 | 123 | // hard-edged spec 124 | float spec = pow(max(0.0, dot(reflect(-l, n), v)), specShininess); 125 | spec = step(0.5, spec) * specWeight; 126 | 127 | float shade = clamp(ambient + diffuseWeight*cel + rimWeight*rim + spec, 0.0, 1.0); 128 | finalColor = vec4(baseColor.rgb * shade, 1.0); 129 | } 130 | )glsl"; 131 | 132 | // Normal+Depth G-buffer — for screen-space edges 133 | const char* kNormalDepthVS = R"glsl( 134 | #version 330 135 | in vec3 vertexPosition; 136 | in vec3 vertexNormal; 137 | uniform mat4 mvp; 138 | uniform mat4 matModel; 139 | uniform mat4 matView; 140 | out vec3 nVS; 141 | out float depthLin; 142 | void main() { 143 | vec4 wpos = matModel * vec4(vertexPosition, 1.0); 144 | vec3 vpos = (matView * wpos).xyz; 145 | nVS = normalize(mat3(matView) * mat3(matModel) * vertexNormal); 146 | depthLin = -vpos.z; // linear view-space depth 147 | gl_Position = mvp * vec4(vertexPosition, 1.0); 148 | } 149 | )glsl"; 150 | 151 | const char* kNormalDepthFS = R"glsl( 152 | #version 330 153 | in vec3 nVS; 154 | in float depthLin; 155 | out vec4 outColor; 156 | uniform float zNear; 157 | uniform float zFar; 158 | void main() { 159 | float d = clamp((depthLin - zNear) / (zFar - zNear), 0.0, 1.0); 160 | outColor = vec4(nVS*0.5 + 0.5, d); // RGB: normal, A: linear depth 161 | } 162 | )glsl"; 163 | 164 | // Fullscreen composite — ink from normal/depth discontinuities 165 | const char* kEdgeQuadVS = R"glsl( 166 | #version 330 167 | in vec3 vertexPosition; 168 | in vec2 vertexTexCoord; 169 | uniform mat4 mvp; 170 | out vec2 uv; 171 | void main() { 172 | uv = vertexTexCoord; 173 | gl_Position = mvp * vec4(vertexPosition, 1.0); 174 | } 175 | )glsl"; 176 | 177 | const char* kEdgeFS = R"glsl( 178 | #version 330 179 | in vec2 uv; 180 | out vec4 finalColor; 181 | 182 | uniform sampler2D texture0; // color from toon pass 183 | uniform sampler2D normDepthTex; // RG: normal, A: depth from ND pass 184 | uniform vec2 texel; // 1/width, 1/height 185 | 186 | uniform float normalThreshold; // e.g. 0.25 187 | uniform float depthThreshold; // e.g. 0.002 188 | uniform float edgeIntensity; // e.g. 1.0 189 | uniform vec4 inkColor; // usually black 190 | 191 | vec3 decodeN(vec3 c){ return normalize(c*2.0 - 1.0); } 192 | 193 | void main(){ 194 | vec4 col = texture(texture0, uv); 195 | vec4 nd = texture(normDepthTex, uv); 196 | vec3 n = decodeN(nd.rgb); 197 | float d = nd.a; 198 | 199 | const vec2 offs[8] = vec2[](vec2(-1,-1), vec2(0,-1), vec2(1,-1), 200 | vec2(-1, 0), vec2(1, 0), 201 | vec2(-1, 1), vec2(0, 1), vec2(1, 1)); 202 | float maxNDiff = 0.0; 203 | float maxDDiff = 0.0; 204 | for (int i=0;i<8;i++){ 205 | vec4 ndn = texture(normDepthTex, uv + offs[i]*texel); 206 | maxNDiff = max(maxNDiff, length(n - decodeN(ndn.rgb))); 207 | maxDDiff = max(maxDDiff, abs(d - ndn.a)); 208 | } 209 | 210 | float eN = smoothstep(normalThreshold, normalThreshold*2.5, maxNDiff); 211 | float eD = smoothstep(depthThreshold, depthThreshold*6.0, maxDDiff); 212 | float edge = clamp(max(eN, eD)*edgeIntensity, 0.0, 1.0); 213 | 214 | vec3 inked = mix(col.rgb, inkColor.rgb, edge); 215 | finalColor = vec4(inked, col.a); 216 | } 217 | )glsl"; 218 | 219 | struct Vec3f { 220 | float x; 221 | float y; 222 | float z; 223 | }; 224 | 225 | struct ModuleLoaderData { 226 | std::filesystem::path baseDir; 227 | std::set dependencies; 228 | }; 229 | 230 | ModuleLoaderData g_module_loader_data; 231 | 232 | struct WatchedFile { 233 | std::optional timestamp; 234 | }; 235 | 236 | Vec3f FetchVertex(const manifold::MeshGL &mesh, uint32_t index) { 237 | const size_t offset = static_cast(index) * mesh.numProp; 238 | return { 239 | static_cast(mesh.vertProperties[offset + 0]), 240 | static_cast(mesh.vertProperties[offset + 1]), 241 | static_cast(mesh.vertProperties[offset + 2]) 242 | }; 243 | } 244 | 245 | Vec3f Subtract(const Vec3f &a, const Vec3f &b) { 246 | return {a.x - b.x, a.y - b.y, a.z - b.z}; 247 | } 248 | 249 | Vec3f Cross(const Vec3f &a, const Vec3f &b) { 250 | return {a.y * b.z - a.z * b.y, 251 | a.z * b.x - a.x * b.z, 252 | a.x * b.y - a.y * b.x}; 253 | } 254 | 255 | Vec3f Normalize(const Vec3f &v) { 256 | const float lenSq = v.x * v.x + v.y * v.y + v.z * v.z; 257 | if (lenSq <= 0.0f) return {0.0f, 0.0f, 0.0f}; 258 | const float invLen = 1.0f / std::sqrt(lenSq); 259 | return {v.x * invLen, v.y * invLen, v.z * invLen}; 260 | } 261 | 262 | bool WriteMeshAsBinaryStl(const manifold::MeshGL &mesh, 263 | const std::filesystem::path &path, 264 | std::string &error) { 265 | const uint32_t triCount = static_cast(mesh.NumTri()); 266 | if (triCount == 0) { 267 | error = "Export failed: mesh is empty"; 268 | return false; 269 | } 270 | 271 | std::ofstream out(path, std::ios::binary); 272 | if (!out) { 273 | error = "Export failed: cannot open " + path.string(); 274 | return false; 275 | } 276 | 277 | std::array header{}; 278 | constexpr const char kHeader[] = "dingcad export"; 279 | std::memcpy(header.data(), kHeader, std::min(header.size(), std::strlen(kHeader))); 280 | out.write(header.data(), header.size()); 281 | out.write(reinterpret_cast(&triCount), sizeof(uint32_t)); 282 | 283 | for (uint32_t tri = 0; tri < triCount; ++tri) { 284 | const uint32_t i0 = mesh.triVerts[tri * 3 + 0]; 285 | const uint32_t i1 = mesh.triVerts[tri * 3 + 1]; 286 | const uint32_t i2 = mesh.triVerts[tri * 3 + 2]; 287 | 288 | const Vec3f v0 = FetchVertex(mesh, i0); 289 | const Vec3f v1 = FetchVertex(mesh, i1); 290 | const Vec3f v2 = FetchVertex(mesh, i2); 291 | 292 | const Vec3f normal = Normalize(Cross(Subtract(v1, v0), Subtract(v2, v0))); 293 | 294 | out.write(reinterpret_cast(&normal), sizeof(Vec3f)); 295 | out.write(reinterpret_cast(&v0), sizeof(Vec3f)); 296 | out.write(reinterpret_cast(&v1), sizeof(Vec3f)); 297 | out.write(reinterpret_cast(&v2), sizeof(Vec3f)); 298 | const uint16_t attr = 0; 299 | out.write(reinterpret_cast(&attr), sizeof(uint16_t)); 300 | } 301 | 302 | if (!out) { 303 | error = "Export failed: write error"; 304 | return false; 305 | } 306 | 307 | return true; 308 | } 309 | 310 | void DestroyModel(Model &model) { 311 | if (model.meshes != nullptr || model.materials != nullptr) { 312 | UnloadModel(model); 313 | } 314 | model = Model{}; 315 | } 316 | 317 | Model CreateRaylibModelFrom(const manifold::MeshGL &meshGL) { 318 | Model model = {0}; 319 | const int vertexCount = meshGL.NumVert(); 320 | const int triangleCount = meshGL.NumTri(); 321 | 322 | if (vertexCount <= 0 || triangleCount <= 0) { 323 | return model; 324 | } 325 | 326 | const int stride = meshGL.numProp; 327 | std::vector positions(vertexCount); 328 | for (int v = 0; v < vertexCount; ++v) { 329 | const int base = v * stride; 330 | // Convert from the scene's Z-up coordinates to raylib's Y-up system. 331 | const float cadX = meshGL.vertProperties[base + 0] * kSceneScale; 332 | const float cadY = meshGL.vertProperties[base + 1] * kSceneScale; 333 | const float cadZ = meshGL.vertProperties[base + 2] * kSceneScale; 334 | positions[v] = {cadX, cadZ, -cadY}; 335 | } 336 | 337 | std::vector accum(vertexCount, {0.0f, 0.0f, 0.0f}); 338 | for (int tri = 0; tri < triangleCount; ++tri) { 339 | const int i0 = meshGL.triVerts[tri * 3 + 0]; 340 | const int i1 = meshGL.triVerts[tri * 3 + 1]; 341 | const int i2 = meshGL.triVerts[tri * 3 + 2]; 342 | 343 | const Vector3 p0 = positions[i0]; 344 | const Vector3 p1 = positions[i1]; 345 | const Vector3 p2 = positions[i2]; 346 | 347 | const Vector3 u = {p1.x - p0.x, p1.y - p0.y, p1.z - p0.z}; 348 | const Vector3 v = {p2.x - p0.x, p2.y - p0.y, p2.z - p0.z}; 349 | const Vector3 n = {u.y * v.z - u.z * v.y, u.z * v.x - u.x * v.z, 350 | u.x * v.y - u.y * v.x}; 351 | 352 | accum[i0].x += n.x; 353 | accum[i0].y += n.y; 354 | accum[i0].z += n.z; 355 | accum[i1].x += n.x; 356 | accum[i1].y += n.y; 357 | accum[i1].z += n.z; 358 | accum[i2].x += n.x; 359 | accum[i2].y += n.y; 360 | accum[i2].z += n.z; 361 | } 362 | 363 | std::vector normals(vertexCount); 364 | std::vector colors(vertexCount); 365 | const Vector3 lightDir = Vector3Normalize({0.45f, 0.85f, 0.35f}); 366 | for (int v = 0; v < vertexCount; ++v) { 367 | const Vector3 n = accum[v]; 368 | const float length = std::sqrt(n.x * n.x + n.y * n.y + n.z * n.z); 369 | 370 | Vector3 normal = {0.0f, 1.0f, 0.0f}; 371 | if (length > 0.0f) { 372 | normal = {n.x / length, n.y / length, n.z / length}; 373 | } 374 | normals[v] = normal; 375 | 376 | float intensity = Vector3DotProduct(normal, lightDir); 377 | intensity = Clamp(intensity, 0.0f, 1.0f); 378 | constexpr int toonSteps = 3; 379 | int level = static_cast(std::floor(intensity * toonSteps)); 380 | if (level >= toonSteps) level = toonSteps - 1; 381 | const float toon = (toonSteps > 1) 382 | ? static_cast(level) / 383 | static_cast(toonSteps - 1) 384 | : intensity; 385 | const float ambient = 0.3f; 386 | const float diffuse = 0.7f; 387 | float finalIntensity = Clamp(ambient + diffuse * toon, 0.0f, 1.0f); 388 | 389 | const Color base = kBaseColor; 390 | Color color = {0}; 391 | color.r = static_cast( 392 | Clamp(base.r * finalIntensity, 0.0f, 255.0f)); 393 | color.g = static_cast( 394 | Clamp(base.g * finalIntensity, 0.0f, 255.0f)); 395 | color.b = static_cast( 396 | Clamp(base.b * finalIntensity, 0.0f, 255.0f)); 397 | color.a = base.a; 398 | colors[v] = color; 399 | } 400 | 401 | constexpr int kMaxVerticesPerMesh = std::numeric_limits::max(); 402 | std::vector remap(vertexCount, 0); 403 | std::vector remapMarker(vertexCount, 0); 404 | int chunkToken = 1; 405 | 406 | std::vector meshes; 407 | meshes.reserve( 408 | static_cast(triangleCount) / kMaxVerticesPerMesh + 1); 409 | 410 | int triIndex = 0; 411 | while (triIndex < triangleCount) { 412 | const int currentToken = chunkToken++; 413 | int chunkVertexCount = 0; 414 | std::vector chunkPositions; 415 | std::vector chunkNormals; 416 | std::vector chunkColors; 417 | std::vector chunkIndices; 418 | 419 | chunkPositions.reserve(std::min(kMaxVerticesPerMesh, vertexCount)); 420 | chunkNormals.reserve(std::min(kMaxVerticesPerMesh, vertexCount)); 421 | chunkColors.reserve(std::min(kMaxVerticesPerMesh, vertexCount)); 422 | chunkIndices.reserve(std::min(kMaxVerticesPerMesh, vertexCount) * 3); 423 | 424 | while (triIndex < triangleCount) { 425 | const int indices[3] = { 426 | static_cast(meshGL.triVerts[triIndex * 3 + 0]), 427 | static_cast(meshGL.triVerts[triIndex * 3 + 1]), 428 | static_cast(meshGL.triVerts[triIndex * 3 + 2])}; 429 | 430 | int needed = 0; 431 | for (int j = 0; j < 3; ++j) { 432 | if (remapMarker[indices[j]] != currentToken) { 433 | ++needed; 434 | } 435 | } 436 | 437 | if (chunkVertexCount + needed > kMaxVerticesPerMesh) { 438 | break; 439 | } 440 | 441 | for (int j = 0; j < 3; ++j) { 442 | const int original = indices[j]; 443 | if (remapMarker[original] != currentToken) { 444 | remapMarker[original] = currentToken; 445 | remap[original] = chunkVertexCount++; 446 | chunkPositions.push_back(positions[original]); 447 | chunkNormals.push_back(normals[original]); 448 | chunkColors.push_back(colors[original]); 449 | } 450 | chunkIndices.push_back(static_cast(remap[original])); 451 | } 452 | ++triIndex; 453 | } 454 | 455 | Mesh chunkMesh = {0}; 456 | chunkMesh.vertexCount = chunkVertexCount; 457 | chunkMesh.triangleCount = static_cast(chunkIndices.size() / 3); 458 | chunkMesh.vertices = static_cast( 459 | MemAlloc(chunkVertexCount * 3 * sizeof(float))); 460 | chunkMesh.normals = static_cast( 461 | MemAlloc(chunkVertexCount * 3 * sizeof(float))); 462 | chunkMesh.colors = static_cast( 463 | MemAlloc(chunkVertexCount * 4 * sizeof(unsigned char))); 464 | chunkMesh.indices = static_cast( 465 | MemAlloc(chunkIndices.size() * sizeof(unsigned short))); 466 | chunkMesh.texcoords = nullptr; 467 | chunkMesh.texcoords2 = nullptr; 468 | chunkMesh.tangents = nullptr; 469 | 470 | for (int v = 0; v < chunkVertexCount; ++v) { 471 | const Vector3 &pos = chunkPositions[v]; 472 | chunkMesh.vertices[v * 3 + 0] = pos.x; 473 | chunkMesh.vertices[v * 3 + 1] = pos.y; 474 | chunkMesh.vertices[v * 3 + 2] = pos.z; 475 | 476 | const Vector3 &normal = chunkNormals[v]; 477 | chunkMesh.normals[v * 3 + 0] = normal.x; 478 | chunkMesh.normals[v * 3 + 1] = normal.y; 479 | chunkMesh.normals[v * 3 + 2] = normal.z; 480 | 481 | const Color color = chunkColors[v]; 482 | chunkMesh.colors[v * 4 + 0] = color.r; 483 | chunkMesh.colors[v * 4 + 1] = color.g; 484 | chunkMesh.colors[v * 4 + 2] = color.b; 485 | chunkMesh.colors[v * 4 + 3] = color.a; 486 | } 487 | 488 | std::memcpy(chunkMesh.indices, chunkIndices.data(), 489 | chunkIndices.size() * sizeof(unsigned short)); 490 | UploadMesh(&chunkMesh, false); 491 | meshes.push_back(chunkMesh); 492 | } 493 | 494 | if (meshes.empty()) { 495 | return model; 496 | } 497 | 498 | model.transform = MatrixIdentity(); 499 | model.meshCount = static_cast(meshes.size()); 500 | model.meshes = static_cast( 501 | MemAlloc(model.meshCount * sizeof(Mesh))); 502 | for (int i = 0; i < model.meshCount; ++i) { 503 | model.meshes[i] = meshes[i]; 504 | } 505 | model.materialCount = 1; 506 | model.materials = static_cast(MemAlloc(sizeof(Material))); 507 | model.materials[0] = LoadMaterialDefault(); 508 | model.meshMaterial = static_cast( 509 | MemAlloc(model.meshCount * sizeof(int))); 510 | for (int i = 0; i < model.meshCount; ++i) { 511 | model.meshMaterial[i] = 0; 512 | } 513 | 514 | return model; 515 | } 516 | 517 | void DrawAxes(float length) { 518 | const float shaftRadius = std::max(length * 0.02f, 0.01f); 519 | const float headLength = std::min(length * 0.2f, length * 0.75f); 520 | const float headRadius = shaftRadius * 2.5f; 521 | 522 | auto drawAxis = [&](Vector3 direction, Color color) { 523 | const Vector3 origin = {0.0f, 0.0f, 0.0f}; 524 | const float shaftLength = std::max(length - headLength, 0.0f); 525 | const Vector3 shaftEnd = Vector3Scale(direction, shaftLength); 526 | const Vector3 axisEnd = Vector3Scale(direction, length); 527 | 528 | if (shaftLength > 0.0f) { 529 | DrawCylinderEx(origin, shaftEnd, shaftRadius, shaftRadius, 12, Fade(color, 0.65f)); 530 | } 531 | DrawCylinderEx(shaftEnd, axisEnd, headRadius, 0.0f, 16, color); 532 | }; 533 | 534 | drawAxis({1.0f, 0.0f, 0.0f}, RED); // +X 535 | drawAxis({0.0f, 1.0f, 0.0f}, GREEN); // +Y 536 | drawAxis({0.0f, 0.0f, 1.0f}, BLUE); // +Z 537 | 538 | DrawSphereEx({0.0f, 0.0f, 0.0f}, shaftRadius * 1.2f, 12, 12, LIGHTGRAY); 539 | } 540 | 541 | void DrawXZGrid(int halfLines, float spacing, Color color) { 542 | for (int i = -halfLines; i <= halfLines; ++i) { 543 | const float offset = static_cast(i) * spacing; 544 | DrawLine3D({offset, 0.0f, -halfLines * spacing}, 545 | {offset, 0.0f, halfLines * spacing}, color); 546 | DrawLine3D({-halfLines * spacing, 0.0f, offset}, 547 | {halfLines * spacing, 0.0f, offset}, color); 548 | } 549 | } 550 | 551 | std::optional FindDefaultScene() { 552 | auto cwdCandidate = std::filesystem::current_path() / "scene.js"; 553 | if (std::filesystem::exists(cwdCandidate)) return cwdCandidate; 554 | if (const char *home = std::getenv("HOME")) { 555 | std::filesystem::path homeCandidate = std::filesystem::path(home) / "scene.js"; 556 | if (std::filesystem::exists(homeCandidate)) return homeCandidate; 557 | } 558 | return std::nullopt; 559 | } 560 | 561 | std::optional ReadTextFile(const std::filesystem::path &path) { 562 | std::ifstream file(path); 563 | if (!file) return std::nullopt; 564 | std::ostringstream ss; 565 | ss << file.rdbuf(); 566 | return ss.str(); 567 | } 568 | 569 | JSModuleDef *FilesystemModuleLoader(JSContext *ctx, const char *module_name, void *opaque) { 570 | auto *data = static_cast(opaque); 571 | std::filesystem::path resolved(module_name); 572 | if (resolved.is_relative()) { 573 | const std::filesystem::path base = data && !data->baseDir.empty() 574 | ? data->baseDir 575 | : std::filesystem::current_path(); 576 | resolved = base / resolved; 577 | } 578 | resolved = std::filesystem::absolute(resolved).lexically_normal(); 579 | 580 | if (data) { 581 | data->baseDir = resolved.parent_path(); 582 | data->dependencies.insert(resolved); 583 | } 584 | 585 | auto source = ReadTextFile(resolved); 586 | if (!source) { 587 | JS_ThrowReferenceError(ctx, "Unable to load module '%s'", resolved.string().c_str()); 588 | return nullptr; 589 | } 590 | 591 | const std::string moduleName = resolved.string(); 592 | JSValue funcVal = JS_Eval(ctx, source->c_str(), source->size(), moduleName.c_str(), 593 | JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY); 594 | if (JS_IsException(funcVal)) { 595 | return nullptr; 596 | } 597 | 598 | auto *module = static_cast(JS_VALUE_GET_PTR(funcVal)); 599 | JS_FreeValue(ctx, funcVal); 600 | return module; 601 | } 602 | 603 | struct LoadResult { 604 | bool success = false; 605 | std::shared_ptr manifold; 606 | std::string message; 607 | std::vector dependencies; 608 | }; 609 | 610 | LoadResult LoadSceneFromFile(JSRuntime *runtime, const std::filesystem::path &path) { 611 | LoadResult result; 612 | const auto absolutePath = std::filesystem::absolute(path); 613 | if (!std::filesystem::exists(absolutePath)) { 614 | result.message = "Scene file not found: " + absolutePath.string(); 615 | return result; 616 | } 617 | g_module_loader_data.baseDir = absolutePath.parent_path(); 618 | g_module_loader_data.dependencies.clear(); 619 | g_module_loader_data.dependencies.insert(absolutePath); 620 | auto sourceOpt = ReadTextFile(absolutePath); 621 | if (!sourceOpt) { 622 | result.message = "Unable to read scene file: " + absolutePath.string(); 623 | result.dependencies.assign(g_module_loader_data.dependencies.begin(), 624 | g_module_loader_data.dependencies.end()); 625 | return result; 626 | } 627 | JSContext *ctx = JS_NewContext(runtime); 628 | RegisterBindings(ctx); 629 | 630 | auto captureException = [&]() { 631 | JSValue exc = JS_GetException(ctx); 632 | JSValue stack = JS_GetPropertyStr(ctx, exc, "stack"); 633 | const char *stackStr = JS_ToCString(ctx, JS_IsUndefined(stack) ? exc : stack); 634 | result.message = stackStr ? stackStr : "JavaScript error"; 635 | JS_FreeCString(ctx, stackStr); 636 | JS_FreeValue(ctx, stack); 637 | JS_FreeValue(ctx, exc); 638 | }; 639 | auto assignDependencies = [&]() { 640 | result.dependencies.assign(g_module_loader_data.dependencies.begin(), 641 | g_module_loader_data.dependencies.end()); 642 | }; 643 | 644 | JSValue moduleFunc = JS_Eval(ctx, sourceOpt->c_str(), sourceOpt->size(), absolutePath.string().c_str(), 645 | JS_EVAL_TYPE_MODULE | JS_EVAL_FLAG_COMPILE_ONLY); 646 | if (JS_IsException(moduleFunc)) { 647 | captureException(); 648 | assignDependencies(); 649 | JS_FreeContext(ctx); 650 | return result; 651 | } 652 | 653 | if (JS_ResolveModule(ctx, moduleFunc) < 0) { 654 | captureException(); 655 | JS_FreeValue(ctx, moduleFunc); 656 | assignDependencies(); 657 | JS_FreeContext(ctx); 658 | return result; 659 | } 660 | 661 | auto *module = static_cast(JS_VALUE_GET_PTR(moduleFunc)); 662 | JSValue evalResult = JS_EvalFunction(ctx, moduleFunc); 663 | if (JS_IsException(evalResult)) { 664 | captureException(); 665 | assignDependencies(); 666 | JS_FreeContext(ctx); 667 | return result; 668 | } 669 | JS_FreeValue(ctx, evalResult); 670 | 671 | JSValue moduleNamespace = JS_GetModuleNamespace(ctx, module); 672 | if (JS_IsException(moduleNamespace)) { 673 | captureException(); 674 | assignDependencies(); 675 | JS_FreeContext(ctx); 676 | return result; 677 | } 678 | 679 | JSValue sceneVal = JS_GetPropertyStr(ctx, moduleNamespace, "scene"); 680 | if (JS_IsException(sceneVal)) { 681 | JS_FreeValue(ctx, moduleNamespace); 682 | captureException(); 683 | assignDependencies(); 684 | JS_FreeContext(ctx); 685 | return result; 686 | } 687 | JS_FreeValue(ctx, moduleNamespace); 688 | 689 | if (JS_IsUndefined(sceneVal)) { 690 | JS_FreeValue(ctx, sceneVal); 691 | JS_FreeContext(ctx); 692 | result.message = "Scene module must export 'scene'"; 693 | assignDependencies(); 694 | return result; 695 | } 696 | 697 | auto sceneHandle = GetManifoldHandle(ctx, sceneVal); 698 | if (!sceneHandle) { 699 | JS_FreeValue(ctx, sceneVal); 700 | JS_FreeContext(ctx); 701 | result.message = "Exported 'scene' is not a manifold"; 702 | assignDependencies(); 703 | return result; 704 | } 705 | 706 | result.manifold = sceneHandle; 707 | result.success = true; 708 | result.message = "Loaded " + absolutePath.string(); 709 | assignDependencies(); 710 | JS_FreeValue(ctx, sceneVal); 711 | JS_FreeContext(ctx); 712 | return result; 713 | } 714 | 715 | bool ReplaceScene(Model &model, 716 | const std::shared_ptr &scene) { 717 | if (!scene) return false; 718 | Model newModel = CreateRaylibModelFrom(scene->GetMeshGL()); 719 | DestroyModel(model); 720 | model = newModel; 721 | return true; 722 | } 723 | 724 | } // namespace 725 | 726 | int main() { 727 | SetConfigFlags(FLAG_MSAA_4X_HINT | FLAG_WINDOW_RESIZABLE); 728 | InitWindow(1280, 720, "dingcad"); 729 | SetTargetFPS(60); 730 | 731 | Font brandingFont = GetFontDefault(); 732 | bool brandingFontCustom = false; 733 | const std::filesystem::path consolasPath("/System/Library/Fonts/Supplemental/Consolas.ttf"); 734 | if (std::filesystem::exists(consolasPath)) { 735 | brandingFont = LoadFontEx(consolasPath.string().c_str(), static_cast(kBrandFontSize), nullptr, 0); 736 | brandingFontCustom = true; 737 | } 738 | 739 | Camera3D camera = {0}; 740 | camera.position = {4.0f, 4.0f, 4.0f}; 741 | camera.target = {0.0f, 0.5f, 0.0f}; 742 | camera.up = {0.0f, 1.0f, 0.0f}; 743 | camera.fovy = 45.0f; 744 | camera.projection = CAMERA_PERSPECTIVE; 745 | 746 | float orbitDistance = Vector3Distance(camera.position, camera.target); 747 | float orbitYaw = atan2f(camera.position.x - camera.target.x, 748 | camera.position.z - camera.target.z); 749 | float orbitPitch = asinf((camera.position.y - camera.target.y) / orbitDistance); 750 | const Vector3 initialTarget = camera.target; 751 | const float initialDistance = orbitDistance; 752 | const float initialYaw = orbitYaw; 753 | const float initialPitch = orbitPitch; 754 | 755 | JSRuntime *runtime = JS_NewRuntime(); 756 | EnsureManifoldClass(runtime); 757 | JS_SetModuleLoaderFunc(runtime, nullptr, FilesystemModuleLoader, &g_module_loader_data); 758 | 759 | std::shared_ptr scene = nullptr; 760 | std::string statusMessage; 761 | std::filesystem::path scriptPath; 762 | std::unordered_map watchedFiles; 763 | auto defaultScript = FindDefaultScene(); 764 | auto reportStatus = [&](const std::string &message) { 765 | statusMessage = message; 766 | TraceLog(LOG_INFO, "%s", statusMessage.c_str()); 767 | std::cout << statusMessage << std::endl; 768 | }; 769 | auto setWatchedFiles = [&](const std::vector &deps) { 770 | std::unordered_map updated; 771 | for (const auto &dep : deps) { 772 | WatchedFile entry; 773 | std::error_code ec; 774 | auto ts = std::filesystem::last_write_time(dep, ec); 775 | if (!ec) { 776 | entry.timestamp = ts; 777 | } 778 | updated.emplace(dep, entry); 779 | } 780 | watchedFiles = std::move(updated); 781 | }; 782 | if (defaultScript) { 783 | scriptPath = std::filesystem::absolute(*defaultScript); 784 | auto load = LoadSceneFromFile(runtime, scriptPath); 785 | if (load.success) { 786 | scene = load.manifold; 787 | reportStatus(load.message); 788 | } else { 789 | reportStatus(load.message); 790 | } 791 | if (!load.dependencies.empty()) { 792 | setWatchedFiles(load.dependencies); 793 | } 794 | } 795 | if (!scene) { 796 | manifold::Manifold cube = manifold::Manifold::Cube({2.0, 2.0, 2.0}, true); 797 | manifold::Manifold sphere = manifold::Manifold::Sphere(1.2, 0); 798 | manifold::Manifold combo = cube + sphere.Translate({0.0, 0.8, 0.0}); 799 | scene = std::make_shared(combo); 800 | if (statusMessage.empty()) { 801 | reportStatus("No scene.js found. Using built-in sample."); 802 | } 803 | } 804 | 805 | Model model = CreateRaylibModelFrom(scene->GetMeshGL()); 806 | 807 | Shader outlineShader = LoadShaderFromMemory(kOutlineVS, kOutlineFS); 808 | Shader toonShader = LoadShaderFromMemory(kToonVS, kToonFS); 809 | Shader normalDepthShader = LoadShaderFromMemory(kNormalDepthVS, kNormalDepthFS); 810 | Shader edgeShader = LoadShaderFromMemory(kEdgeQuadVS, kEdgeFS); 811 | 812 | if (outlineShader.id == 0 || toonShader.id == 0 || normalDepthShader.id == 0 || edgeShader.id == 0) { 813 | TraceLog(LOG_ERROR, "Failed to load one or more shaders."); 814 | DestroyModel(model); 815 | if (brandingFontCustom) { 816 | UnloadFont(brandingFont); 817 | } 818 | JS_FreeRuntime(runtime); 819 | CloseWindow(); 820 | return 1; 821 | } 822 | 823 | // Outline uniforms/material 824 | const int locOutline = GetShaderLocation(outlineShader, "outline"); 825 | const int locOutlineColor = GetShaderLocation(outlineShader, "outlineColor"); 826 | Material outlineMat = LoadMaterialDefault(); 827 | outlineMat.shader = outlineShader; 828 | 829 | auto setOutlineUniforms = [&](float worldThickness, Color color) { 830 | float c[4] = { 831 | color.r / 255.0f, 832 | color.g / 255.0f, 833 | color.b / 255.0f, 834 | color.a / 255.0f}; 835 | SetShaderValue(outlineMat.shader, locOutline, &worldThickness, SHADER_UNIFORM_FLOAT); 836 | SetShaderValue(outlineMat.shader, locOutlineColor, c, SHADER_UNIFORM_VEC4); 837 | }; 838 | 839 | // Toon shader uniforms/material 840 | const int locLightDirVS = GetShaderLocation(toonShader, "lightDirVS"); 841 | const int locBaseColor = GetShaderLocation(toonShader, "baseColor"); 842 | const int locToonSteps = GetShaderLocation(toonShader, "toonSteps"); 843 | const int locAmbient = GetShaderLocation(toonShader, "ambient"); 844 | const int locDiffuseWeight = GetShaderLocation(toonShader, "diffuseWeight"); 845 | const int locRimWeight = GetShaderLocation(toonShader, "rimWeight"); 846 | const int locSpecWeight = GetShaderLocation(toonShader, "specWeight"); 847 | const int locSpecShininess = GetShaderLocation(toonShader, "specShininess"); 848 | Material toonMat = LoadMaterialDefault(); 849 | toonMat.shader = toonShader; 850 | 851 | // Normal/depth shader uniforms/material 852 | const int locNear = GetShaderLocation(normalDepthShader, "zNear"); 853 | const int locFar = GetShaderLocation(normalDepthShader, "zFar"); 854 | Material normalDepthMat = LoadMaterialDefault(); 855 | normalDepthMat.shader = normalDepthShader; 856 | 857 | // Edge composite uniforms 858 | const int locNormDepthTexture = GetShaderLocation(edgeShader, "normDepthTex"); 859 | const int locTexel = GetShaderLocation(edgeShader, "texel"); 860 | const int locNormalThreshold = GetShaderLocation(edgeShader, "normalThreshold"); 861 | const int locDepthThreshold = GetShaderLocation(edgeShader, "depthThreshold"); 862 | const int locEdgeIntensity = GetShaderLocation(edgeShader, "edgeIntensity"); 863 | const int locInkColor = GetShaderLocation(edgeShader, "inkColor"); 864 | 865 | // Static toon lighting configuration 866 | const Vector3 lightDirWS = Vector3Normalize({0.45f, 0.85f, 0.35f}); 867 | const float baseCol[4] = { 868 | kBaseColor.r / 255.0f, 869 | kBaseColor.g / 255.0f, 870 | kBaseColor.b / 255.0f, 871 | 1.0f}; 872 | SetShaderValue(toonShader, locBaseColor, baseCol, SHADER_UNIFORM_VEC4); 873 | int toonSteps = 4; 874 | SetShaderValue(toonShader, locToonSteps, &toonSteps, SHADER_UNIFORM_INT); 875 | float ambient = 0.35f; 876 | SetShaderValue(toonShader, locAmbient, &ambient, SHADER_UNIFORM_FLOAT); 877 | float diffuseWeight = 0.75f; 878 | SetShaderValue(toonShader, locDiffuseWeight, &diffuseWeight, SHADER_UNIFORM_FLOAT); 879 | float rimWeight = 0.25f; 880 | SetShaderValue(toonShader, locRimWeight, &rimWeight, SHADER_UNIFORM_FLOAT); 881 | float specWeight = 0.12f; 882 | SetShaderValue(toonShader, locSpecWeight, &specWeight, SHADER_UNIFORM_FLOAT); 883 | float specShininess = 32.0f; 884 | SetShaderValue(toonShader, locSpecShininess, &specShininess, SHADER_UNIFORM_FLOAT); 885 | 886 | float normalThreshold = 0.25f; 887 | float depthThreshold = 0.002f; 888 | float edgeIntensity = 1.0f; 889 | SetShaderValue(edgeShader, locNormalThreshold, &normalThreshold, SHADER_UNIFORM_FLOAT); 890 | SetShaderValue(edgeShader, locDepthThreshold, &depthThreshold, SHADER_UNIFORM_FLOAT); 891 | SetShaderValue(edgeShader, locEdgeIntensity, &edgeIntensity, SHADER_UNIFORM_FLOAT); 892 | const Color outlineColor = BLACK; 893 | const float inkColor[4] = { 894 | outlineColor.r / 255.0f, 895 | outlineColor.g / 255.0f, 896 | outlineColor.b / 255.0f, 897 | 1.0f}; 898 | SetShaderValue(edgeShader, locInkColor, inkColor, SHADER_UNIFORM_VEC4); 899 | 900 | auto makeRenderTargets = [&]() { 901 | const int width = std::max(GetScreenWidth(), 1); 902 | const int height = std::max(GetScreenHeight(), 1); 903 | RenderTexture2D color = LoadRenderTexture(width, height); 904 | RenderTexture2D normDepth = LoadRenderTexture(width, height); 905 | return std::make_pair(color, normDepth); 906 | }; 907 | 908 | auto [rtColor, rtNormalDepth] = makeRenderTargets(); 909 | SetShaderValueTexture(edgeShader, locNormDepthTexture, rtNormalDepth.texture); 910 | const float initialTexel[2] = { 911 | 1.0f / static_cast(rtNormalDepth.texture.width), 912 | 1.0f / static_cast(rtNormalDepth.texture.height)}; 913 | SetShaderValue(edgeShader, locTexel, initialTexel, SHADER_UNIFORM_VEC2); 914 | 915 | int prevScreenWidth = GetScreenWidth(); 916 | int prevScreenHeight = GetScreenHeight(); 917 | const float zNear = 0.01f; 918 | const float zFar = 1000.0f; 919 | 920 | while (!WindowShouldClose()) { 921 | const Vector2 mouseDelta = GetMouseDelta(); 922 | 923 | auto reloadScene = [&]() { 924 | auto load = LoadSceneFromFile(runtime, scriptPath); 925 | if (load.success) { 926 | scene = load.manifold; 927 | ReplaceScene(model, scene); 928 | reportStatus(load.message); 929 | } else { 930 | reportStatus(load.message); 931 | } 932 | if (!load.dependencies.empty()) { 933 | setWatchedFiles(load.dependencies); 934 | } 935 | }; 936 | 937 | if (!scriptPath.empty()) { 938 | bool changed = false; 939 | for (const auto &entry : watchedFiles) { 940 | std::error_code ec; 941 | auto currentTs = std::filesystem::last_write_time(entry.first, ec); 942 | if (ec) { 943 | if (entry.second.timestamp.has_value()) { 944 | changed = true; 945 | break; 946 | } 947 | } else if (!entry.second.timestamp.has_value() || 948 | currentTs != *entry.second.timestamp) { 949 | changed = true; 950 | break; 951 | } 952 | } 953 | if (changed) { 954 | reloadScene(); 955 | } 956 | } 957 | 958 | if (IsKeyPressed(KEY_R) && !scriptPath.empty()) { 959 | reloadScene(); 960 | } 961 | 962 | static bool prevPDown = false; 963 | bool exportRequested = false; 964 | 965 | for (int key = GetKeyPressed(); key != 0; key = GetKeyPressed()) { 966 | TraceLog(LOG_INFO, "Key pressed: %d", key); 967 | std::cout << "Key pressed: " << key << std::endl; 968 | if (key == KEY_P) { 969 | exportRequested = true; 970 | } 971 | } 972 | 973 | for (int ch = GetCharPressed(); ch != 0; ch = GetCharPressed()) { 974 | TraceLog(LOG_INFO, "Char pressed: %d", ch); 975 | std::cout << "Char pressed: " << ch << std::endl; 976 | if (ch == 'p' || ch == 'P') { 977 | exportRequested = true; 978 | } 979 | } 980 | 981 | const bool pDown = IsKeyDown(KEY_P); 982 | if (pDown && !prevPDown) { 983 | TraceLog(LOG_INFO, "P key down edge detected"); 984 | std::cout << "P key down edge detected" << std::endl; 985 | exportRequested = true; 986 | } 987 | prevPDown = pDown; 988 | 989 | if (!exportRequested && IsKeyPressed(KEY_P)) { 990 | exportRequested = true; 991 | } 992 | 993 | if (exportRequested) { 994 | TraceLog(LOG_INFO, "Export trigger detected"); 995 | std::cout << "Export trigger detected" << std::endl; 996 | if (scene) { 997 | std::filesystem::path downloads; 998 | if (const char *home = std::getenv("HOME")) { 999 | downloads = std::filesystem::path(home) / "Downloads"; 1000 | } else { 1001 | downloads = std::filesystem::current_path(); 1002 | } 1003 | 1004 | std::error_code dirErr; 1005 | std::filesystem::create_directories(downloads, dirErr); 1006 | if (dirErr && !std::filesystem::exists(downloads)) { 1007 | reportStatus("Export failed: cannot access " + downloads.string()); 1008 | } else { 1009 | std::filesystem::path savePath = downloads / "ding.stl"; 1010 | std::string error; 1011 | const bool ok = WriteMeshAsBinaryStl(scene->GetMeshGL(), savePath, error); 1012 | TraceLog(LOG_INFO, "Export path: %s", savePath.string().c_str()); 1013 | if (ok) { 1014 | reportStatus("Saved " + savePath.string()); 1015 | } else { 1016 | reportStatus(error); 1017 | } 1018 | } 1019 | } else { 1020 | reportStatus("No scene loaded to export"); 1021 | } 1022 | } 1023 | 1024 | if (IsMouseButtonDown(MOUSE_BUTTON_LEFT)) { 1025 | orbitYaw -= mouseDelta.x * 0.01f; 1026 | orbitPitch += mouseDelta.y * 0.01f; 1027 | const float limit = DEG2RAD * 89.0f; 1028 | orbitPitch = Clamp(orbitPitch, -limit, limit); 1029 | } 1030 | 1031 | const float wheel = GetMouseWheelMove(); 1032 | if (wheel != 0.0f) { 1033 | orbitDistance *= (1.0f - wheel * 0.1f); 1034 | orbitDistance = Clamp(orbitDistance, 1.0f, 50.0f); 1035 | } 1036 | 1037 | const Vector3 forward = Vector3Normalize(Vector3Subtract(camera.target, camera.position)); 1038 | const Vector3 worldUp = {0.0f, 1.0f, 0.0f}; 1039 | const Vector3 right = Vector3Normalize(Vector3CrossProduct(worldUp, forward)); 1040 | const Vector3 camUp = Vector3CrossProduct(forward, right); 1041 | 1042 | if (IsMouseButtonDown(MOUSE_BUTTON_RIGHT)) { 1043 | camera.target = Vector3Add(camera.target, 1044 | Vector3Scale(right, mouseDelta.x * 0.01f * orbitDistance)); 1045 | camera.target = Vector3Add(camera.target, 1046 | Vector3Scale(camUp, -mouseDelta.y * 0.01f * orbitDistance)); 1047 | } 1048 | 1049 | if (IsKeyPressed(KEY_SPACE)) { 1050 | camera.target = initialTarget; 1051 | orbitDistance = initialDistance; 1052 | orbitYaw = initialYaw; 1053 | orbitPitch = initialPitch; 1054 | } 1055 | 1056 | const float moveSpeed = 0.05f * orbitDistance; 1057 | if (IsKeyDown(KEY_W)) camera.target = Vector3Add(camera.target, Vector3Scale(forward, moveSpeed)); 1058 | if (IsKeyDown(KEY_S)) camera.target = Vector3Add(camera.target, Vector3Scale(forward, -moveSpeed)); 1059 | if (IsKeyDown(KEY_A)) camera.target = Vector3Add(camera.target, Vector3Scale(right, -moveSpeed)); 1060 | if (IsKeyDown(KEY_D)) camera.target = Vector3Add(camera.target, Vector3Scale(right, moveSpeed)); 1061 | if (IsKeyDown(KEY_Q)) camera.target = Vector3Add(camera.target, Vector3Scale(worldUp, -moveSpeed)); 1062 | if (IsKeyDown(KEY_E)) camera.target = Vector3Add(camera.target, Vector3Scale(worldUp, moveSpeed)); 1063 | 1064 | const Vector3 offsets = { 1065 | orbitDistance * cosf(orbitPitch) * sinf(orbitYaw), 1066 | orbitDistance * sinf(orbitPitch), 1067 | orbitDistance * cosf(orbitPitch) * cosf(orbitYaw)}; 1068 | camera.position = Vector3Add(camera.target, offsets); 1069 | camera.up = worldUp; 1070 | 1071 | const int screenWidth = std::max(GetScreenWidth(), 1); 1072 | const int screenHeight = std::max(GetScreenHeight(), 1); 1073 | if (screenWidth != prevScreenWidth || screenHeight != prevScreenHeight) { 1074 | UnloadRenderTexture(rtColor); 1075 | UnloadRenderTexture(rtNormalDepth); 1076 | auto resizedTargets = makeRenderTargets(); 1077 | rtColor = resizedTargets.first; 1078 | rtNormalDepth = resizedTargets.second; 1079 | SetShaderValueTexture(edgeShader, locNormDepthTexture, rtNormalDepth.texture); 1080 | const float texel[2] = { 1081 | 1.0f / static_cast(rtNormalDepth.texture.width), 1082 | 1.0f / static_cast(rtNormalDepth.texture.height)}; 1083 | SetShaderValue(edgeShader, locTexel, texel, SHADER_UNIFORM_VEC2); 1084 | prevScreenWidth = screenWidth; 1085 | prevScreenHeight = screenHeight; 1086 | } 1087 | 1088 | Matrix view = GetCameraMatrix(camera); 1089 | Vector3 lightDirVS = { 1090 | view.m0 * lightDirWS.x + view.m4 * lightDirWS.y + view.m8 * lightDirWS.z, 1091 | view.m1 * lightDirWS.x + view.m5 * lightDirWS.y + view.m9 * lightDirWS.z, 1092 | view.m2 * lightDirWS.x + view.m6 * lightDirWS.y + view.m10 * lightDirWS.z}; 1093 | lightDirVS = Vector3Normalize(lightDirVS); 1094 | SetShaderValue(toonShader, locLightDirVS, &lightDirVS.x, SHADER_UNIFORM_VEC3); 1095 | 1096 | float outlineThickness = 0.0f; 1097 | { 1098 | const float pixels = 2.0f; 1099 | const float distance = Vector3Distance(camera.position, camera.target); 1100 | const float screenHeightF = static_cast(screenHeight); 1101 | const float worldPerPixel = (screenHeightF > 0.0f) 1102 | ? 2.0f * tanf(DEG2RAD * camera.fovy * 0.5f) * distance / screenHeightF 1103 | : 0.0f; 1104 | outlineThickness = pixels * worldPerPixel; 1105 | } 1106 | setOutlineUniforms(outlineThickness, outlineColor); 1107 | 1108 | SetShaderValue(normalDepthShader, locNear, &zNear, SHADER_UNIFORM_FLOAT); 1109 | SetShaderValue(normalDepthShader, locFar, &zFar, SHADER_UNIFORM_FLOAT); 1110 | 1111 | BeginTextureMode(rtColor); 1112 | ClearBackground(RAYWHITE); 1113 | BeginMode3D(camera); 1114 | DrawXZGrid(40, 0.5f, Fade(LIGHTGRAY, 0.4f)); 1115 | DrawAxes(2.0f); 1116 | 1117 | rlDisableBackfaceCulling(); 1118 | for (int i = 0; i < model.meshCount; ++i) { 1119 | DrawMesh(model.meshes[i], outlineMat, model.transform); 1120 | } 1121 | rlEnableBackfaceCulling(); 1122 | 1123 | for (int i = 0; i < model.meshCount; ++i) { 1124 | DrawMesh(model.meshes[i], toonMat, model.transform); 1125 | } 1126 | EndMode3D(); 1127 | EndTextureMode(); 1128 | 1129 | BeginTextureMode(rtNormalDepth); 1130 | ClearBackground({127, 127, 255, 0}); 1131 | BeginMode3D(camera); 1132 | for (int i = 0; i < model.meshCount; ++i) { 1133 | DrawMesh(model.meshes[i], normalDepthMat, model.transform); 1134 | } 1135 | EndMode3D(); 1136 | EndTextureMode(); 1137 | 1138 | BeginDrawing(); 1139 | ClearBackground(RAYWHITE); 1140 | 1141 | const float texel[2] = { 1142 | 1.0f / static_cast(rtNormalDepth.texture.width), 1143 | 1.0f / static_cast(rtNormalDepth.texture.height)}; 1144 | SetShaderValue(edgeShader, locTexel, texel, SHADER_UNIFORM_VEC2); 1145 | 1146 | BeginShaderMode(edgeShader); 1147 | const Rectangle srcRect = {0.0f, 0.0f, static_cast(rtColor.texture.width), 1148 | -static_cast(rtColor.texture.height)}; 1149 | DrawTextureRec(rtColor.texture, srcRect, {0.0f, 0.0f}, WHITE); 1150 | EndShaderMode(); 1151 | 1152 | const float margin = 20.0f; 1153 | const Vector2 textSize = MeasureTextEx(brandingFont, kBrandText, kBrandFontSize, 0.0f); 1154 | const Vector2 brandPos = { 1155 | static_cast(GetScreenWidth()) - textSize.x - margin, 1156 | margin}; 1157 | DrawTextEx(brandingFont, kBrandText, brandPos, kBrandFontSize, 0.0f, DARKGRAY); 1158 | 1159 | if (!statusMessage.empty()) { 1160 | constexpr float statusFontSize = 18.0f; 1161 | const Vector2 statusPos = {margin, margin}; 1162 | DrawTextEx(brandingFont, statusMessage.c_str(), statusPos, statusFontSize, 0.0f, DARKGRAY); 1163 | } 1164 | 1165 | EndDrawing(); 1166 | } 1167 | 1168 | UnloadRenderTexture(rtColor); 1169 | UnloadRenderTexture(rtNormalDepth); 1170 | UnloadMaterial(toonMat); 1171 | UnloadMaterial(normalDepthMat); 1172 | UnloadMaterial(outlineMat); // also releases the shader 1173 | UnloadShader(edgeShader); 1174 | DestroyModel(model); 1175 | if (brandingFontCustom) { 1176 | UnloadFont(brandingFont); 1177 | } 1178 | JS_FreeRuntime(runtime); 1179 | CloseWindow(); 1180 | 1181 | return 0; 1182 | } 1183 | -------------------------------------------------------------------------------- /viewer/js_bindings.cpp: -------------------------------------------------------------------------------- 1 | #include "js_bindings.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "manifold/manifold.h" 15 | #include "manifold/polygon.h" 16 | #include "manifold/meshIO.h" 17 | namespace { 18 | 19 | void PrintLoadMeshError(const std::string &message) { 20 | const char esc = 0x1B; 21 | std::fprintf(stderr, "%c[31m%s%c[0m\n", esc, message.c_str(), esc); 22 | std::fflush(stderr); 23 | } 24 | 25 | 26 | struct JsManifold { 27 | std::shared_ptr handle; 28 | }; 29 | 30 | JSClassID g_manifoldClassId; 31 | 32 | void JsManifoldFinalizer(JSRuntime *rt, JSValue val) { 33 | (void)rt; 34 | auto *wrapper = static_cast(JS_GetOpaque(val, g_manifoldClassId)); 35 | delete wrapper; 36 | } 37 | 38 | void EnsureManifoldClassInternal(JSRuntime *runtime) { 39 | static bool idInitialised = false; 40 | static bool classRegistered = false; 41 | if (!idInitialised) { 42 | JS_NewClassID(runtime, &g_manifoldClassId); 43 | idInitialised = true; 44 | } 45 | if (!classRegistered) { 46 | JSClassDef def{}; 47 | def.class_name = "Manifold"; 48 | def.finalizer = JsManifoldFinalizer; 49 | JS_NewClass(runtime, g_manifoldClassId, &def); 50 | classRegistered = true; 51 | } 52 | } 53 | 54 | JSValue WrapManifold(JSContext *ctx, std::shared_ptr manifold) { 55 | JSValue obj = JS_NewObjectClass(ctx, g_manifoldClassId); 56 | if (JS_IsException(obj)) return obj; 57 | auto *wrapper = new JsManifold{std::move(manifold)}; 58 | JS_SetOpaque(obj, wrapper); 59 | return obj; 60 | } 61 | 62 | JsManifold *GetJsManifold(JSContext *ctx, JSValueConst value) { 63 | return static_cast(JS_GetOpaque2(ctx, value, g_manifoldClassId)); 64 | } 65 | 66 | std::shared_ptr GetManifoldHandleInternal(JSContext *ctx, 67 | JSValueConst value) { 68 | JsManifold *jsManifold = GetJsManifold(ctx, value); 69 | if (!jsManifold) return nullptr; 70 | return jsManifold->handle; 71 | } 72 | 73 | bool GetVec3(JSContext *ctx, JSValueConst value, std::array &out) { 74 | if (!JS_IsArray(value)) { 75 | JS_ThrowTypeError(ctx, "expected array of three numbers"); 76 | return false; 77 | } 78 | for (uint32_t i = 0; i < 3; ++i) { 79 | JSValue element = JS_GetPropertyUint32(ctx, value, i); 80 | if (JS_IsUndefined(element)) { 81 | JS_FreeValue(ctx, element); 82 | JS_ThrowTypeError(ctx, "vector requires three entries"); 83 | return false; 84 | } 85 | if (JS_ToFloat64(ctx, &out[i], element) < 0) { 86 | JS_FreeValue(ctx, element); 87 | return false; 88 | } 89 | JS_FreeValue(ctx, element); 90 | } 91 | return true; 92 | } 93 | 94 | bool GetVec2(JSContext *ctx, JSValueConst value, std::array &out) { 95 | if (!JS_IsArray(value)) { 96 | JS_ThrowTypeError(ctx, "expected array of two numbers"); 97 | return false; 98 | } 99 | for (uint32_t i = 0; i < 2; ++i) { 100 | JSValue element = JS_GetPropertyUint32(ctx, value, i); 101 | if (JS_IsUndefined(element)) { 102 | JS_FreeValue(ctx, element); 103 | JS_ThrowTypeError(ctx, "vector requires two entries"); 104 | return false; 105 | } 106 | if (JS_ToFloat64(ctx, &out[i], element) < 0) { 107 | JS_FreeValue(ctx, element); 108 | return false; 109 | } 110 | JS_FreeValue(ctx, element); 111 | } 112 | return true; 113 | } 114 | 115 | bool GetMat3x4(JSContext *ctx, JSValueConst value, manifold::mat3x4 &out) { 116 | if (!JS_IsArray(value)) { 117 | JS_ThrowTypeError(ctx, "transform expects array of 12 numbers"); 118 | return false; 119 | } 120 | std::array entries{}; 121 | for (uint32_t i = 0; i < 12; ++i) { 122 | JSValue element = JS_GetPropertyUint32(ctx, value, i); 123 | if (JS_IsUndefined(element)) { 124 | JS_FreeValue(ctx, element); 125 | JS_ThrowTypeError(ctx, "transform array requires 12 entries"); 126 | return false; 127 | } 128 | if (JS_ToFloat64(ctx, &entries[i], element) < 0) { 129 | JS_FreeValue(ctx, element); 130 | return false; 131 | } 132 | JS_FreeValue(ctx, element); 133 | } 134 | for (int row = 0; row < 3; ++row) { 135 | for (int col = 0; col < 4; ++col) { 136 | out[row][col] = entries[row * 4 + col]; 137 | } 138 | } 139 | return true; 140 | } 141 | 142 | JSValue Vec3ToJs(JSContext *ctx, const manifold::vec3 &v) { 143 | JSValue arr = JS_NewArray(ctx); 144 | JS_SetPropertyUint32(ctx, arr, 0, JS_NewFloat64(ctx, v.x)); 145 | JS_SetPropertyUint32(ctx, arr, 1, JS_NewFloat64(ctx, v.y)); 146 | JS_SetPropertyUint32(ctx, arr, 2, JS_NewFloat64(ctx, v.z)); 147 | return arr; 148 | } 149 | 150 | JSValue Vec2ToJs(JSContext *ctx, const manifold::vec2 &v) { 151 | JSValue arr = JS_NewArray(ctx); 152 | JS_SetPropertyUint32(ctx, arr, 0, JS_NewFloat64(ctx, v.x)); 153 | JS_SetPropertyUint32(ctx, arr, 1, JS_NewFloat64(ctx, v.y)); 154 | return arr; 155 | } 156 | 157 | bool JsValueToPolygons(JSContext *ctx, JSValueConst value, 158 | manifold::Polygons &out) { 159 | if (!JS_IsArray(value)) { 160 | JS_ThrowTypeError(ctx, "polygons must be an array of loops"); 161 | return false; 162 | } 163 | JSValue lengthVal = JS_GetPropertyStr(ctx, value, "length"); 164 | uint32_t numLoops = 0; 165 | if (JS_ToUint32(ctx, &numLoops, lengthVal) < 0) { 166 | JS_FreeValue(ctx, lengthVal); 167 | return false; 168 | } 169 | JS_FreeValue(ctx, lengthVal); 170 | manifold::Polygons result; 171 | result.reserve(numLoops); 172 | for (uint32_t i = 0; i < numLoops; ++i) { 173 | JSValue loopVal = JS_GetPropertyUint32(ctx, value, i); 174 | if (JS_IsException(loopVal)) return false; 175 | if (!JS_IsArray(loopVal)) { 176 | JS_FreeValue(ctx, loopVal); 177 | JS_ThrowTypeError(ctx, "each loop must be an array of [x,y] points"); 178 | return false; 179 | } 180 | JSValue loopLenVal = JS_GetPropertyStr(ctx, loopVal, "length"); 181 | uint32_t loopLen = 0; 182 | if (JS_ToUint32(ctx, &loopLen, loopLenVal) < 0) { 183 | JS_FreeValue(ctx, loopLenVal); 184 | JS_FreeValue(ctx, loopVal); 185 | return false; 186 | } 187 | JS_FreeValue(ctx, loopLenVal); 188 | manifold::SimplePolygon loop; 189 | loop.reserve(loopLen); 190 | for (uint32_t j = 0; j < loopLen; ++j) { 191 | JSValue pointVal = JS_GetPropertyUint32(ctx, loopVal, j); 192 | if (JS_IsException(pointVal)) { 193 | JS_FreeValue(ctx, loopVal); 194 | return false; 195 | } 196 | std::array point{}; 197 | bool ok = GetVec2(ctx, pointVal, point); 198 | JS_FreeValue(ctx, pointVal); 199 | if (!ok) { 200 | JS_FreeValue(ctx, loopVal); 201 | return false; 202 | } 203 | manifold::vec2 pt{point[0], point[1]}; 204 | loop.push_back(pt); 205 | } 206 | JS_FreeValue(ctx, loopVal); 207 | result.push_back(loop); 208 | } 209 | out = std::move(result); 210 | return true; 211 | } 212 | 213 | JSValue PolygonsToJs(JSContext *ctx, const manifold::Polygons &polys) { 214 | JSValue arr = JS_NewArray(ctx); 215 | uint32_t loopIdx = 0; 216 | for (const auto &loop : polys) { 217 | JSValue loopArr = JS_NewArray(ctx); 218 | uint32_t pointIdx = 0; 219 | for (const auto &pt : loop) { 220 | JS_SetPropertyUint32(ctx, loopArr, pointIdx++, Vec2ToJs(ctx, pt)); 221 | } 222 | JS_SetPropertyUint32(ctx, arr, loopIdx++, loopArr); 223 | } 224 | return arr; 225 | } 226 | 227 | bool CollectManifoldArgs(JSContext *ctx, int argc, JSValueConst *argv, 228 | std::vector &out) { 229 | if (argc == 0) { 230 | JS_ThrowTypeError(ctx, "expected at least one manifold"); 231 | return false; 232 | } 233 | if (argc == 1 && JS_IsArray(argv[0])) { 234 | JSValue arr = argv[0]; 235 | JSValue lengthVal = JS_GetPropertyStr(ctx, arr, "length"); 236 | uint32_t len = 0; 237 | if (JS_ToUint32(ctx, &len, lengthVal) < 0) { 238 | JS_FreeValue(ctx, lengthVal); 239 | return false; 240 | } 241 | JS_FreeValue(ctx, lengthVal); 242 | out.reserve(len); 243 | for (uint32_t i = 0; i < len; ++i) { 244 | JSValue itemVal = JS_GetPropertyUint32(ctx, arr, i); 245 | if (JS_IsException(itemVal)) return false; 246 | JsManifold *jsManifold = GetJsManifold(ctx, itemVal); 247 | JS_FreeValue(ctx, itemVal); 248 | if (!jsManifold) return false; 249 | out.push_back(*jsManifold->handle); 250 | } 251 | return true; 252 | } 253 | out.reserve(argc); 254 | for (int i = 0; i < argc; ++i) { 255 | JsManifold *jsManifold = GetJsManifold(ctx, argv[i]); 256 | if (!jsManifold) return false; 257 | out.push_back(*jsManifold->handle); 258 | } 259 | return true; 260 | } 261 | 262 | JSValue ManifoldVectorToJsArray(JSContext *ctx, 263 | std::vector manifolds) { 264 | JSValue arr = JS_NewArray(ctx); 265 | uint32_t idx = 0; 266 | for (auto &mf : manifolds) { 267 | auto wrapped = std::make_shared(std::move(mf)); 268 | JS_SetPropertyUint32(ctx, arr, idx++, WrapManifold(ctx, std::move(wrapped))); 269 | } 270 | return arr; 271 | } 272 | 273 | bool JsArrayToVec3List(JSContext *ctx, JSValueConst value, 274 | std::vector &out) { 275 | if (!JS_IsArray(value)) { 276 | JS_ThrowTypeError(ctx, "expected array of [x,y,z] points"); 277 | return false; 278 | } 279 | JSValue lengthVal = JS_GetPropertyStr(ctx, value, "length"); 280 | uint32_t length = 0; 281 | if (JS_ToUint32(ctx, &length, lengthVal) < 0) { 282 | JS_FreeValue(ctx, lengthVal); 283 | return false; 284 | } 285 | JS_FreeValue(ctx, lengthVal); 286 | std::vector result; 287 | result.reserve(length); 288 | for (uint32_t i = 0; i < length; ++i) { 289 | JSValue pointVal = JS_GetPropertyUint32(ctx, value, i); 290 | if (JS_IsException(pointVal)) return false; 291 | std::array coords{}; 292 | bool ok = GetVec3(ctx, pointVal, coords); 293 | JS_FreeValue(ctx, pointVal); 294 | if (!ok) return false; 295 | manifold::vec3 vec{coords[0], coords[1], coords[2]}; 296 | result.push_back(vec); 297 | } 298 | out = std::move(result); 299 | return true; 300 | } 301 | 302 | bool GetOpType(JSContext *ctx, JSValueConst value, manifold::OpType &out) { 303 | if (JS_IsString(value)) { 304 | const char *opStr = JS_ToCString(ctx, value); 305 | if (!opStr) return false; 306 | std::string opLower(opStr); 307 | JS_FreeCString(ctx, opStr); 308 | for (auto &c : opLower) { 309 | c = static_cast(std::tolower(static_cast(c))); 310 | } 311 | if (opLower == "add" || opLower == "union") { 312 | out = manifold::OpType::Add; 313 | return true; 314 | } 315 | if (opLower == "subtract" || opLower == "difference") { 316 | out = manifold::OpType::Subtract; 317 | return true; 318 | } 319 | if (opLower == "intersect" || opLower == "intersection") { 320 | out = manifold::OpType::Intersect; 321 | return true; 322 | } 323 | JS_ThrowTypeError(ctx, "unknown boolean op"); 324 | return false; 325 | } 326 | if (JS_IsNumber(value)) { 327 | int32_t idx = 0; 328 | if (JS_ToInt32(ctx, &idx, value) < 0) return false; 329 | switch (idx) { 330 | case 0: 331 | out = manifold::OpType::Add; 332 | return true; 333 | case 1: 334 | out = manifold::OpType::Subtract; 335 | return true; 336 | case 2: 337 | out = manifold::OpType::Intersect; 338 | return true; 339 | default: 340 | JS_ThrowRangeError(ctx, "boolean op index must be 0,1,2"); 341 | return false; 342 | } 343 | } 344 | JS_ThrowTypeError(ctx, "op must be string or number"); 345 | return false; 346 | } 347 | 348 | const char *ErrorToString(manifold::Manifold::Error err) { 349 | switch (err) { 350 | case manifold::Manifold::Error::NoError: 351 | return "NoError"; 352 | case manifold::Manifold::Error::NonFiniteVertex: 353 | return "NonFiniteVertex"; 354 | case manifold::Manifold::Error::NotManifold: 355 | return "NotManifold"; 356 | case manifold::Manifold::Error::VertexOutOfBounds: 357 | return "VertexOutOfBounds"; 358 | case manifold::Manifold::Error::PropertiesWrongLength: 359 | return "PropertiesWrongLength"; 360 | case manifold::Manifold::Error::MissingPositionProperties: 361 | return "MissingPositionProperties"; 362 | case manifold::Manifold::Error::MergeVectorsDifferentLengths: 363 | return "MergeVectorsDifferentLengths"; 364 | case manifold::Manifold::Error::MergeIndexOutOfBounds: 365 | return "MergeIndexOutOfBounds"; 366 | case manifold::Manifold::Error::TransformWrongLength: 367 | return "TransformWrongLength"; 368 | case manifold::Manifold::Error::RunIndexWrongLength: 369 | return "RunIndexWrongLength"; 370 | case manifold::Manifold::Error::FaceIDWrongLength: 371 | return "FaceIDWrongLength"; 372 | case manifold::Manifold::Error::InvalidConstruction: 373 | return "InvalidConstruction"; 374 | case manifold::Manifold::Error::ResultTooLarge: 375 | return "ResultTooLarge"; 376 | } 377 | return "Unknown"; 378 | } 379 | 380 | bool GetBox(JSContext *ctx, JSValueConst value, manifold::Box &out) { 381 | if (!JS_IsObject(value)) { 382 | JS_ThrowTypeError(ctx, "bounds must be an object with min/max"); 383 | return false; 384 | } 385 | JSValue minVal = JS_GetPropertyStr(ctx, value, "min"); 386 | JSValue maxVal = JS_GetPropertyStr(ctx, value, "max"); 387 | if (JS_IsUndefined(minVal) || JS_IsUndefined(maxVal)) { 388 | JS_FreeValue(ctx, minVal); 389 | JS_FreeValue(ctx, maxVal); 390 | JS_ThrowTypeError(ctx, "bounds requires min and max arrays"); 391 | return false; 392 | } 393 | std::array minArr{}; 394 | std::array maxArr{}; 395 | bool okMin = GetVec3(ctx, minVal, minArr); 396 | bool okMax = okMin && GetVec3(ctx, maxVal, maxArr); 397 | JS_FreeValue(ctx, minVal); 398 | JS_FreeValue(ctx, maxVal); 399 | if (!okMax) return false; 400 | out.min = manifold::vec3{minArr[0], minArr[1], minArr[2]}; 401 | out.max = manifold::vec3{maxArr[0], maxArr[1], maxArr[2]}; 402 | return true; 403 | } 404 | 405 | JSValue JsCube(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 406 | double sx = 1.0, sy = 1.0, sz = 1.0; 407 | bool center = false; 408 | if (argc >= 1 && JS_IsObject(argv[0])) { 409 | JSValue sizeVal = JS_GetPropertyStr(ctx, argv[0], "size"); 410 | if (!JS_IsUndefined(sizeVal)) { 411 | std::array size{}; 412 | if (!GetVec3(ctx, sizeVal, size)) { 413 | JS_FreeValue(ctx, sizeVal); 414 | return JS_EXCEPTION; 415 | } 416 | sx = size[0]; 417 | sy = size[1]; 418 | sz = size[2]; 419 | } 420 | JS_FreeValue(ctx, sizeVal); 421 | JSValue centerVal = JS_GetPropertyStr(ctx, argv[0], "center"); 422 | if (!JS_IsUndefined(centerVal)) { 423 | int c = JS_ToBool(ctx, centerVal); 424 | if (c < 0) { 425 | JS_FreeValue(ctx, centerVal); 426 | return JS_EXCEPTION; 427 | } 428 | center = c == 1; 429 | } 430 | JS_FreeValue(ctx, centerVal); 431 | } 432 | auto manifold = std::make_shared(manifold::Manifold::Cube({sx, sy, sz}, center)); 433 | return WrapManifold(ctx, std::move(manifold)); 434 | } 435 | 436 | JSValue JsSphere(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 437 | double radius = 1.0; 438 | if (argc >= 1 && JS_IsObject(argv[0])) { 439 | JSValue radiusVal = JS_GetPropertyStr(ctx, argv[0], "radius"); 440 | if (!JS_IsUndefined(radiusVal)) { 441 | if (JS_ToFloat64(ctx, &radius, radiusVal) < 0) { 442 | JS_FreeValue(ctx, radiusVal); 443 | return JS_EXCEPTION; 444 | } 445 | } 446 | JS_FreeValue(ctx, radiusVal); 447 | } 448 | auto manifold = std::make_shared(manifold::Manifold::Sphere(radius, 0)); 449 | return WrapManifold(ctx, std::move(manifold)); 450 | } 451 | 452 | JSValue JsCylinder(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 453 | double height = 1.0; 454 | double radius = 0.5; 455 | double radiusTop = -1.0; 456 | bool center = false; 457 | if (argc >= 1 && JS_IsObject(argv[0])) { 458 | JSValue heightVal = JS_GetPropertyStr(ctx, argv[0], "height"); 459 | if (!JS_IsUndefined(heightVal)) { 460 | if (JS_ToFloat64(ctx, &height, heightVal) < 0) { 461 | JS_FreeValue(ctx, heightVal); 462 | return JS_EXCEPTION; 463 | } 464 | } 465 | JS_FreeValue(ctx, heightVal); 466 | 467 | JSValue radiusVal = JS_GetPropertyStr(ctx, argv[0], "radius"); 468 | if (!JS_IsUndefined(radiusVal)) { 469 | if (JS_ToFloat64(ctx, &radius, radiusVal) < 0) { 470 | JS_FreeValue(ctx, radiusVal); 471 | return JS_EXCEPTION; 472 | } 473 | } 474 | JS_FreeValue(ctx, radiusVal); 475 | 476 | JSValue radiusTopVal = JS_GetPropertyStr(ctx, argv[0], "radiusTop"); 477 | if (!JS_IsUndefined(radiusTopVal)) { 478 | if (JS_ToFloat64(ctx, &radiusTop, radiusTopVal) < 0) { 479 | JS_FreeValue(ctx, radiusTopVal); 480 | return JS_EXCEPTION; 481 | } 482 | } 483 | JS_FreeValue(ctx, radiusTopVal); 484 | 485 | JSValue centerVal = JS_GetPropertyStr(ctx, argv[0], "center"); 486 | if (!JS_IsUndefined(centerVal)) { 487 | int c = JS_ToBool(ctx, centerVal); 488 | if (c < 0) { 489 | JS_FreeValue(ctx, centerVal); 490 | return JS_EXCEPTION; 491 | } 492 | center = c == 1; 493 | } 494 | JS_FreeValue(ctx, centerVal); 495 | } 496 | double radiusHigh = (radiusTop < 0.0) ? radius : radiusTop; 497 | auto manifold = std::make_shared( 498 | manifold::Manifold::Cylinder(height, radius, radiusHigh, 0, center)); 499 | return WrapManifold(ctx, std::move(manifold)); 500 | } 501 | 502 | JSValue JsBoolean(JSContext *ctx, int argc, JSValueConst *argv, 503 | manifold::OpType op) { 504 | if (argc < 2) { 505 | return JS_ThrowTypeError(ctx, "boolean operation requires at least two manifolds"); 506 | } 507 | JsManifold *base = GetJsManifold(ctx, argv[0]); 508 | if (!base) return JS_EXCEPTION; 509 | std::shared_ptr result = 510 | std::make_shared(*base->handle); 511 | for (int i = 1; i < argc; ++i) { 512 | JsManifold *next = GetJsManifold(ctx, argv[i]); 513 | if (!next) return JS_EXCEPTION; 514 | switch (op) { 515 | case manifold::OpType::Add: 516 | result = std::make_shared(*result + *next->handle); 517 | break; 518 | case manifold::OpType::Subtract: 519 | result = std::make_shared(*result - *next->handle); 520 | break; 521 | case manifold::OpType::Intersect: 522 | result = std::make_shared(*result ^ *next->handle); 523 | break; 524 | } 525 | } 526 | return WrapManifold(ctx, std::move(result)); 527 | } 528 | 529 | JSValue JsUnion(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 530 | return JsBoolean(ctx, argc, argv, manifold::OpType::Add); 531 | } 532 | 533 | JSValue JsDifference(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 534 | return JsBoolean(ctx, argc, argv, manifold::OpType::Subtract); 535 | } 536 | 537 | JSValue JsIntersection(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { 538 | return JsBoolean(ctx, argc, argv, manifold::OpType::Intersect); 539 | } 540 | 541 | JSValue JsTranslate(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 542 | if (argc < 2) { 543 | return JS_ThrowTypeError(ctx, "translate expects (manifold, [x,y,z])"); 544 | } 545 | JsManifold *target = GetJsManifold(ctx, argv[0]); 546 | if (!target) return JS_EXCEPTION; 547 | std::array offset{}; 548 | if (!GetVec3(ctx, argv[1], offset)) return JS_EXCEPTION; 549 | auto manifold = std::make_shared( 550 | target->handle->Translate({offset[0], offset[1], offset[2]})); 551 | return WrapManifold(ctx, std::move(manifold)); 552 | } 553 | 554 | JSValue JsScale(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 555 | if (argc < 2) { 556 | return JS_ThrowTypeError(ctx, "scale expects (manifold, factor)"); 557 | } 558 | JsManifold *target = GetJsManifold(ctx, argv[0]); 559 | if (!target) return JS_EXCEPTION; 560 | manifold::vec3 scaleVec{1.0, 1.0, 1.0}; 561 | if (JS_IsNumber(argv[1])) { 562 | double s = 1.0; 563 | if (JS_ToFloat64(ctx, &s, argv[1]) < 0) return JS_EXCEPTION; 564 | scaleVec = {s, s, s}; 565 | } else { 566 | std::array factors{}; 567 | if (!GetVec3(ctx, argv[1], factors)) return JS_EXCEPTION; 568 | scaleVec = {factors[0], factors[1], factors[2]}; 569 | } 570 | auto manifold = std::make_shared(target->handle->Scale(scaleVec)); 571 | return WrapManifold(ctx, std::move(manifold)); 572 | } 573 | 574 | JSValue JsRotate(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 575 | if (argc < 2) { 576 | return JS_ThrowTypeError(ctx, "rotate expects (manifold, [x,y,z] degrees)"); 577 | } 578 | JsManifold *target = GetJsManifold(ctx, argv[0]); 579 | if (!target) return JS_EXCEPTION; 580 | std::array angles{}; 581 | if (!GetVec3(ctx, argv[1], angles)) return JS_EXCEPTION; 582 | auto manifold = std::make_shared( 583 | target->handle->Rotate(angles[0], angles[1], angles[2])); 584 | return WrapManifold(ctx, std::move(manifold)); 585 | } 586 | 587 | JSValue JsTetrahedron(JSContext *ctx, JSValueConst, int, JSValueConst *) { 588 | auto manifold = std::make_shared( 589 | manifold::Manifold::Tetrahedron()); 590 | return WrapManifold(ctx, std::move(manifold)); 591 | } 592 | 593 | JSValue JsCompose(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 594 | std::vector parts; 595 | if (!CollectManifoldArgs(ctx, argc, argv, parts)) return JS_EXCEPTION; 596 | if (parts.empty()) return JS_EXCEPTION; 597 | auto manifold = std::make_shared( 598 | manifold::Manifold::Compose(parts)); 599 | return WrapManifold(ctx, std::move(manifold)); 600 | } 601 | 602 | JSValue JsDecompose(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 603 | if (argc < 1) { 604 | return JS_ThrowTypeError(ctx, "decompose expects a manifold"); 605 | } 606 | JsManifold *target = GetJsManifold(ctx, argv[0]); 607 | if (!target) return JS_EXCEPTION; 608 | auto manifolds = target->handle->Decompose(); 609 | return ManifoldVectorToJsArray(ctx, std::move(manifolds)); 610 | } 611 | 612 | JSValue JsMirror(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 613 | if (argc < 2) { 614 | return JS_ThrowTypeError(ctx, "mirror expects (manifold, [x,y,z])"); 615 | } 616 | JsManifold *target = GetJsManifold(ctx, argv[0]); 617 | if (!target) return JS_EXCEPTION; 618 | std::array normal{}; 619 | if (!GetVec3(ctx, argv[1], normal)) return JS_EXCEPTION; 620 | manifold::vec3 plane{normal[0], normal[1], normal[2]}; 621 | auto manifold = std::make_shared( 622 | target->handle->Mirror(plane)); 623 | return WrapManifold(ctx, std::move(manifold)); 624 | } 625 | 626 | JSValue JsTransform(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 627 | if (argc < 2) { 628 | return JS_ThrowTypeError(ctx, "transform expects (manifold, mat3x4)"); 629 | } 630 | JsManifold *target = GetJsManifold(ctx, argv[0]); 631 | if (!target) return JS_EXCEPTION; 632 | manifold::mat3x4 matrix{}; 633 | if (!GetMat3x4(ctx, argv[1], matrix)) return JS_EXCEPTION; 634 | auto manifold = std::make_shared(target->handle->Transform(matrix)); 635 | return WrapManifold(ctx, std::move(manifold)); 636 | } 637 | 638 | JSValue JsSetTolerance(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 639 | if (argc < 2) { 640 | return JS_ThrowTypeError(ctx, "setTolerance expects (manifold, tolerance)"); 641 | } 642 | JsManifold *target = GetJsManifold(ctx, argv[0]); 643 | if (!target) return JS_EXCEPTION; 644 | double tol = 0.0; 645 | if (JS_ToFloat64(ctx, &tol, argv[1]) < 0) return JS_EXCEPTION; 646 | auto manifold = std::make_shared(target->handle->SetTolerance(tol)); 647 | return WrapManifold(ctx, std::move(manifold)); 648 | } 649 | 650 | JSValue JsSimplify(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 651 | if (argc < 1) { 652 | return JS_ThrowTypeError(ctx, "simplify expects (manifold, tolerance?)"); 653 | } 654 | JsManifold *target = GetJsManifold(ctx, argv[0]); 655 | if (!target) return JS_EXCEPTION; 656 | double tol = 0.0; 657 | if (argc >= 2 && !JS_IsUndefined(argv[1])) { 658 | if (JS_ToFloat64(ctx, &tol, argv[1]) < 0) return JS_EXCEPTION; 659 | } 660 | auto manifold = std::make_shared(target->handle->Simplify(tol)); 661 | return WrapManifold(ctx, std::move(manifold)); 662 | } 663 | 664 | JSValue JsRefine(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 665 | if (argc < 2) { 666 | return JS_ThrowTypeError(ctx, "refine expects (manifold, iterations)"); 667 | } 668 | JsManifold *target = GetJsManifold(ctx, argv[0]); 669 | if (!target) return JS_EXCEPTION; 670 | int32_t iterations = 0; 671 | if (JS_ToInt32(ctx, &iterations, argv[1]) < 0) return JS_EXCEPTION; 672 | auto manifold = std::make_shared(target->handle->Refine(iterations)); 673 | return WrapManifold(ctx, std::move(manifold)); 674 | } 675 | 676 | JSValue JsRefineToLength(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 677 | if (argc < 2) { 678 | return JS_ThrowTypeError(ctx, "refineToLength expects (manifold, length)"); 679 | } 680 | JsManifold *target = GetJsManifold(ctx, argv[0]); 681 | if (!target) return JS_EXCEPTION; 682 | double length = 0.0; 683 | if (JS_ToFloat64(ctx, &length, argv[1]) < 0) return JS_EXCEPTION; 684 | auto manifold = std::make_shared(target->handle->RefineToLength(length)); 685 | return WrapManifold(ctx, std::move(manifold)); 686 | } 687 | 688 | JSValue JsRefineToTolerance(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 689 | if (argc < 2) { 690 | return JS_ThrowTypeError(ctx, "refineToTolerance expects (manifold, tolerance)"); 691 | } 692 | JsManifold *target = GetJsManifold(ctx, argv[0]); 693 | if (!target) return JS_EXCEPTION; 694 | double tol = 0.0; 695 | if (JS_ToFloat64(ctx, &tol, argv[1]) < 0) return JS_EXCEPTION; 696 | auto manifold = std::make_shared(target->handle->RefineToTolerance(tol)); 697 | return WrapManifold(ctx, std::move(manifold)); 698 | } 699 | 700 | JSValue JsHull(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 701 | std::vector parts; 702 | if (!CollectManifoldArgs(ctx, argc, argv, parts)) return JS_EXCEPTION; 703 | if (parts.empty()) return JS_EXCEPTION; 704 | auto manifold = std::make_shared( 705 | manifold::Manifold::Hull(parts)); 706 | return WrapManifold(ctx, std::move(manifold)); 707 | } 708 | 709 | JSValue JsHullPoints(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 710 | if (argc < 1) { 711 | return JS_ThrowTypeError(ctx, "hullPoints expects array of [x,y,z]"); 712 | } 713 | std::vector pts; 714 | if (!JsArrayToVec3List(ctx, argv[0], pts)) return JS_EXCEPTION; 715 | auto manifold = std::make_shared( 716 | manifold::Manifold::Hull(pts)); 717 | return WrapManifold(ctx, std::move(manifold)); 718 | } 719 | 720 | JSValue JsTrimByPlane(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 721 | if (argc < 3) { 722 | return JS_ThrowTypeError(ctx, "trimByPlane expects (manifold, [nx,ny,nz], offset)"); 723 | } 724 | JsManifold *target = GetJsManifold(ctx, argv[0]); 725 | if (!target) return JS_EXCEPTION; 726 | std::array normal{}; 727 | if (!GetVec3(ctx, argv[1], normal)) return JS_EXCEPTION; 728 | double offset = 0.0; 729 | if (JS_ToFloat64(ctx, &offset, argv[2]) < 0) return JS_EXCEPTION; 730 | manifold::vec3 n{normal[0], normal[1], normal[2]}; 731 | auto manifold = std::make_shared( 732 | target->handle->TrimByPlane(n, offset)); 733 | return WrapManifold(ctx, std::move(manifold)); 734 | } 735 | 736 | JSValue JsSurfaceArea(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 737 | if (argc < 1) { 738 | return JS_ThrowTypeError(ctx, "surfaceArea expects a manifold"); 739 | } 740 | JsManifold *target = GetJsManifold(ctx, argv[0]); 741 | if (!target) return JS_EXCEPTION; 742 | return JS_NewFloat64(ctx, target->handle->SurfaceArea()); 743 | } 744 | 745 | JSValue JsVolume(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 746 | if (argc < 1) { 747 | return JS_ThrowTypeError(ctx, "volume expects a manifold"); 748 | } 749 | JsManifold *target = GetJsManifold(ctx, argv[0]); 750 | if (!target) return JS_EXCEPTION; 751 | return JS_NewFloat64(ctx, target->handle->Volume()); 752 | } 753 | 754 | JSValue JsBoundingBox(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 755 | if (argc < 1) { 756 | return JS_ThrowTypeError(ctx, "boundingBox expects a manifold"); 757 | } 758 | JsManifold *target = GetJsManifold(ctx, argv[0]); 759 | if (!target) return JS_EXCEPTION; 760 | manifold::Box box = target->handle->BoundingBox(); 761 | JSValue obj = JS_NewObject(ctx); 762 | JS_SetPropertyStr(ctx, obj, "min", Vec3ToJs(ctx, box.min)); 763 | JS_SetPropertyStr(ctx, obj, "max", Vec3ToJs(ctx, box.max)); 764 | return obj; 765 | } 766 | 767 | JSValue JsNumTriangles(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 768 | if (argc < 1) { 769 | return JS_ThrowTypeError(ctx, "numTriangles expects a manifold"); 770 | } 771 | JsManifold *target = GetJsManifold(ctx, argv[0]); 772 | if (!target) return JS_EXCEPTION; 773 | return JS_NewInt64(ctx, static_cast(target->handle->NumTri())); 774 | } 775 | 776 | JSValue JsNumVertices(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 777 | if (argc < 1) { 778 | return JS_ThrowTypeError(ctx, "numVertices expects a manifold"); 779 | } 780 | JsManifold *target = GetJsManifold(ctx, argv[0]); 781 | if (!target) return JS_EXCEPTION; 782 | return JS_NewInt64(ctx, static_cast(target->handle->NumVert())); 783 | } 784 | 785 | JSValue JsNumEdges(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 786 | if (argc < 1) { 787 | return JS_ThrowTypeError(ctx, "numEdges expects a manifold"); 788 | } 789 | JsManifold *target = GetJsManifold(ctx, argv[0]); 790 | if (!target) return JS_EXCEPTION; 791 | return JS_NewInt64(ctx, static_cast(target->handle->NumEdge())); 792 | } 793 | 794 | JSValue JsGenus(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 795 | if (argc < 1) { 796 | return JS_ThrowTypeError(ctx, "genus expects a manifold"); 797 | } 798 | JsManifold *target = GetJsManifold(ctx, argv[0]); 799 | if (!target) return JS_EXCEPTION; 800 | return JS_NewInt32(ctx, target->handle->Genus()); 801 | } 802 | 803 | JSValue JsGetTolerance(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 804 | if (argc < 1) { 805 | return JS_ThrowTypeError(ctx, "getTolerance expects a manifold"); 806 | } 807 | JsManifold *target = GetJsManifold(ctx, argv[0]); 808 | if (!target) return JS_EXCEPTION; 809 | return JS_NewFloat64(ctx, target->handle->GetTolerance()); 810 | } 811 | 812 | JSValue JsIsEmpty(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 813 | if (argc < 1) { 814 | return JS_ThrowTypeError(ctx, "isEmpty expects a manifold"); 815 | } 816 | JsManifold *target = GetJsManifold(ctx, argv[0]); 817 | if (!target) return JS_EXCEPTION; 818 | return JS_NewBool(ctx, target->handle->IsEmpty()); 819 | } 820 | 821 | JSValue JsStatus(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 822 | if (argc < 1) { 823 | return JS_ThrowTypeError(ctx, "status expects a manifold"); 824 | } 825 | JsManifold *target = GetJsManifold(ctx, argv[0]); 826 | if (!target) return JS_EXCEPTION; 827 | const char *err = ErrorToString(target->handle->Status()); 828 | return JS_NewString(ctx, err); 829 | } 830 | 831 | JSValue JsSlice(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 832 | if (argc < 1) { 833 | return JS_ThrowTypeError(ctx, "slice expects (manifold, height?)"); 834 | } 835 | JsManifold *target = GetJsManifold(ctx, argv[0]); 836 | if (!target) return JS_EXCEPTION; 837 | double height = 0.0; 838 | if (argc >= 2 && !JS_IsUndefined(argv[1])) { 839 | if (JS_ToFloat64(ctx, &height, argv[1]) < 0) return JS_EXCEPTION; 840 | } 841 | manifold::Polygons polys = target->handle->Slice(height); 842 | return PolygonsToJs(ctx, polys); 843 | } 844 | 845 | JSValue JsProject(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 846 | if (argc < 1) { 847 | return JS_ThrowTypeError(ctx, "project expects a manifold"); 848 | } 849 | JsManifold *target = GetJsManifold(ctx, argv[0]); 850 | if (!target) return JS_EXCEPTION; 851 | manifold::Polygons polys = target->handle->Project(); 852 | return PolygonsToJs(ctx, polys); 853 | } 854 | 855 | JSValue JsExtrude(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 856 | if (argc < 2) { 857 | return JS_ThrowTypeError(ctx, "extrude expects (polygons, options)"); 858 | } 859 | manifold::Polygons polys; 860 | if (!JsValueToPolygons(ctx, argv[0], polys)) return JS_EXCEPTION; 861 | if (!JS_IsObject(argv[1])) { 862 | return JS_ThrowTypeError(ctx, "extrude options must be an object"); 863 | } 864 | JSValue opts = argv[1]; 865 | double height = 1.0; 866 | int32_t divisions = 0; 867 | double twist = 0.0; 868 | manifold::vec2 scaleTop{1.0, 1.0}; 869 | 870 | JSValue heightVal = JS_GetPropertyStr(ctx, opts, "height"); 871 | if (!JS_IsUndefined(heightVal)) { 872 | if (JS_ToFloat64(ctx, &height, heightVal) < 0) { 873 | JS_FreeValue(ctx, heightVal); 874 | return JS_EXCEPTION; 875 | } 876 | } 877 | JS_FreeValue(ctx, heightVal); 878 | 879 | JSValue divVal = JS_GetPropertyStr(ctx, opts, "divisions"); 880 | if (!JS_IsUndefined(divVal)) { 881 | if (JS_ToInt32(ctx, &divisions, divVal) < 0) { 882 | JS_FreeValue(ctx, divVal); 883 | return JS_EXCEPTION; 884 | } 885 | } 886 | JS_FreeValue(ctx, divVal); 887 | 888 | JSValue twistVal = JS_GetPropertyStr(ctx, opts, "twistDegrees"); 889 | if (!JS_IsUndefined(twistVal)) { 890 | if (JS_ToFloat64(ctx, &twist, twistVal) < 0) { 891 | JS_FreeValue(ctx, twistVal); 892 | return JS_EXCEPTION; 893 | } 894 | } 895 | JS_FreeValue(ctx, twistVal); 896 | 897 | JSValue scaleVal = JS_GetPropertyStr(ctx, opts, "scaleTop"); 898 | if (!JS_IsUndefined(scaleVal)) { 899 | if (JS_IsNumber(scaleVal)) { 900 | double s = 1.0; 901 | if (JS_ToFloat64(ctx, &s, scaleVal) < 0) { 902 | JS_FreeValue(ctx, scaleVal); 903 | return JS_EXCEPTION; 904 | } 905 | scaleTop = manifold::vec2{s, s}; 906 | } else { 907 | std::array factors{}; 908 | if (!GetVec2(ctx, scaleVal, factors)) { 909 | JS_FreeValue(ctx, scaleVal); 910 | return JS_EXCEPTION; 911 | } 912 | scaleTop = manifold::vec2{factors[0], factors[1]}; 913 | } 914 | } 915 | JS_FreeValue(ctx, scaleVal); 916 | 917 | auto manifold = std::make_shared( 918 | manifold::Manifold::Extrude(polys, height, divisions, twist, scaleTop)); 919 | return WrapManifold(ctx, std::move(manifold)); 920 | } 921 | 922 | JSValue JsRevolve(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 923 | if (argc < 1) { 924 | return JS_ThrowTypeError(ctx, "revolve expects (polygons, options?)"); 925 | } 926 | manifold::Polygons polys; 927 | if (!JsValueToPolygons(ctx, argv[0], polys)) return JS_EXCEPTION; 928 | int32_t segments = 0; 929 | double degrees = 360.0; 930 | if (argc >= 2 && JS_IsObject(argv[1])) { 931 | JSValue opts = argv[1]; 932 | JSValue segVal = JS_GetPropertyStr(ctx, opts, "segments"); 933 | if (!JS_IsUndefined(segVal)) { 934 | if (JS_ToInt32(ctx, &segments, segVal) < 0) { 935 | JS_FreeValue(ctx, segVal); 936 | return JS_EXCEPTION; 937 | } 938 | } 939 | JS_FreeValue(ctx, segVal); 940 | JSValue degVal = JS_GetPropertyStr(ctx, opts, "degrees"); 941 | if (!JS_IsUndefined(degVal)) { 942 | if (JS_ToFloat64(ctx, °rees, degVal) < 0) { 943 | JS_FreeValue(ctx, degVal); 944 | return JS_EXCEPTION; 945 | } 946 | } 947 | JS_FreeValue(ctx, degVal); 948 | } 949 | auto manifold = std::make_shared( 950 | manifold::Manifold::Revolve(polys, segments, degrees)); 951 | return WrapManifold(ctx, std::move(manifold)); 952 | } 953 | 954 | JSValue JsBatchBoolean(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 955 | if (argc < 2) { 956 | return JS_ThrowTypeError(ctx, "batchBoolean expects (op, manifolds)"); 957 | } 958 | manifold::OpType op; 959 | if (!GetOpType(ctx, argv[0], op)) return JS_EXCEPTION; 960 | std::vector parts; 961 | if (JS_IsArray(argv[1])) { 962 | if (!CollectManifoldArgs(ctx, 1, &argv[1], parts)) return JS_EXCEPTION; 963 | } else { 964 | if (!CollectManifoldArgs(ctx, argc - 1, argv + 1, parts)) return JS_EXCEPTION; 965 | } 966 | if (parts.empty()) { 967 | JS_ThrowTypeError(ctx, "batchBoolean requires manifolds"); 968 | return JS_EXCEPTION; 969 | } 970 | auto manifold = std::make_shared( 971 | manifold::Manifold::BatchBoolean(parts, op)); 972 | return WrapManifold(ctx, std::move(manifold)); 973 | } 974 | 975 | JSValue JsBooleanOp(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 976 | if (argc < 3) { 977 | return JS_ThrowTypeError(ctx, "boolean expects (manifoldA, manifoldB, op)"); 978 | } 979 | JsManifold *base = GetJsManifold(ctx, argv[0]); 980 | if (!base) return JS_EXCEPTION; 981 | JsManifold *other = GetJsManifold(ctx, argv[1]); 982 | if (!other) return JS_EXCEPTION; 983 | manifold::OpType op; 984 | if (!GetOpType(ctx, argv[2], op)) return JS_EXCEPTION; 985 | auto manifold = std::make_shared( 986 | base->handle->Boolean(*other->handle, op)); 987 | return WrapManifold(ctx, std::move(manifold)); 988 | } 989 | 990 | JSValue JsLoadMesh(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 991 | if (argc < 1) { 992 | return JS_ThrowTypeError(ctx, "loadMesh expects (path[, forceCleanup])"); 993 | } 994 | const char *pathStr = JS_ToCString(ctx, argv[0]); 995 | if (!pathStr) return JS_EXCEPTION; 996 | std::string path(pathStr); 997 | JS_FreeCString(ctx, pathStr); 998 | 999 | std::filesystem::path fsPath; 1000 | if (!path.empty() && path[0] == '~') { 1001 | const char *home = std::getenv("HOME"); 1002 | if (!home) { 1003 | const std::string msg = "loadMesh: HOME is not set; cannot resolve '~'"; 1004 | PrintLoadMeshError(msg); 1005 | return JS_ThrowInternalError(ctx, "%s", msg.c_str()); 1006 | } 1007 | std::filesystem::path homePath(home); 1008 | if (path.size() == 1) { 1009 | fsPath = homePath; 1010 | } else if (path[1] == '/') { 1011 | fsPath = homePath / path.substr(2); 1012 | } else { 1013 | fsPath = homePath / path.substr(1); 1014 | } 1015 | } else { 1016 | fsPath = std::filesystem::path(path); 1017 | } 1018 | 1019 | std::error_code ec; 1020 | if (!fsPath.is_absolute()) { 1021 | auto absPath = std::filesystem::absolute(fsPath, ec); 1022 | if (ec) { 1023 | const std::string msg = "loadMesh: unable to resolve path '" + path + "'"; 1024 | PrintLoadMeshError(msg); 1025 | return JS_ThrowInternalError(ctx, "loadMesh: unable to resolve path '%s'", path.c_str()); 1026 | } 1027 | fsPath = absPath; 1028 | } 1029 | const std::string resolvedPath = fsPath.string(); 1030 | 1031 | if (!std::filesystem::exists(fsPath, ec) || ec) { 1032 | const std::string msg = "loadMesh: file not found '" + resolvedPath + "' (expected in ~/Downloads/models)"; 1033 | PrintLoadMeshError(msg); 1034 | return JS_ThrowInternalError(ctx, "loadMesh: file not found '%s'", resolvedPath.c_str()); 1035 | } 1036 | if (!std::filesystem::is_regular_file(fsPath, ec) || ec) { 1037 | const std::string msg = "loadMesh: not a regular file '" + resolvedPath + "'"; 1038 | PrintLoadMeshError(msg); 1039 | return JS_ThrowInternalError(ctx, "loadMesh: not a regular file '%s'", resolvedPath.c_str()); 1040 | } 1041 | 1042 | bool forceCleanup = false; 1043 | if (argc >= 2 && !JS_IsUndefined(argv[1])) { 1044 | int flag = JS_ToBool(ctx, argv[1]); 1045 | if (flag < 0) return JS_EXCEPTION; 1046 | forceCleanup = flag == 1; 1047 | } 1048 | 1049 | try { 1050 | manifold::MeshGL mesh = manifold::ImportMesh(fsPath.string(), forceCleanup); 1051 | if (mesh.NumTri() == 0 || mesh.NumVert() == 0) { 1052 | const std::string msg = "loadMesh: imported mesh is empty for '" + resolvedPath + "'"; 1053 | PrintLoadMeshError(msg); 1054 | return JS_ThrowInternalError(ctx, "loadMesh: imported mesh is empty"); 1055 | } 1056 | manifold::Manifold manifold(mesh); 1057 | auto handle = std::make_shared(std::move(manifold)); 1058 | return WrapManifold(ctx, std::move(handle)); 1059 | } catch (const std::exception &e) { 1060 | const std::string msg = std::string("loadMesh failed: ") + e.what(); 1061 | PrintLoadMeshError(msg); 1062 | return JS_ThrowInternalError(ctx, "loadMesh failed: %s", e.what()); 1063 | } 1064 | } 1065 | 1066 | 1067 | 1068 | 1069 | JSValue JsLevelSet(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 1070 | if (argc < 1 || !JS_IsObject(argv[0])) { 1071 | return JS_ThrowTypeError(ctx, "levelSet expects options object"); 1072 | } 1073 | JSValue opts = argv[0]; 1074 | JSValue sdfVal = JS_GetPropertyStr(ctx, opts, "sdf"); 1075 | if (!JS_IsFunction(ctx, sdfVal)) { 1076 | JS_FreeValue(ctx, sdfVal); 1077 | return JS_ThrowTypeError(ctx, "levelSet requires sdf function"); 1078 | } 1079 | JSValue boundsVal = JS_GetPropertyStr(ctx, opts, "bounds"); 1080 | if (JS_IsUndefined(boundsVal)) { 1081 | JS_FreeValue(ctx, sdfVal); 1082 | return JS_ThrowTypeError(ctx, "levelSet requires bounds"); 1083 | } 1084 | manifold::Box bounds; 1085 | if (!GetBox(ctx, boundsVal, bounds)) { 1086 | JS_FreeValue(ctx, sdfVal); 1087 | JS_FreeValue(ctx, boundsVal); 1088 | return JS_EXCEPTION; 1089 | } 1090 | JS_FreeValue(ctx, boundsVal); 1091 | 1092 | JSValue edgeVal = JS_GetPropertyStr(ctx, opts, "edgeLength"); 1093 | if (JS_IsUndefined(edgeVal)) { 1094 | JS_FreeValue(ctx, sdfVal); 1095 | return JS_ThrowTypeError(ctx, "levelSet requires edgeLength"); 1096 | } 1097 | double edgeLength = 0.0; 1098 | if (JS_ToFloat64(ctx, &edgeLength, edgeVal) < 0) { 1099 | JS_FreeValue(ctx, sdfVal); 1100 | JS_FreeValue(ctx, edgeVal); 1101 | return JS_EXCEPTION; 1102 | } 1103 | JS_FreeValue(ctx, edgeVal); 1104 | 1105 | double level = 0.0; 1106 | double tolerance = -1.0; 1107 | bool canParallel = false; 1108 | 1109 | JSValue levelVal = JS_GetPropertyStr(ctx, opts, "level"); 1110 | if (!JS_IsUndefined(levelVal)) { 1111 | if (JS_ToFloat64(ctx, &level, levelVal) < 0) { 1112 | JS_FreeValue(ctx, sdfVal); 1113 | JS_FreeValue(ctx, levelVal); 1114 | return JS_EXCEPTION; 1115 | } 1116 | } 1117 | JS_FreeValue(ctx, levelVal); 1118 | 1119 | JSValue tolVal = JS_GetPropertyStr(ctx, opts, "tolerance"); 1120 | if (!JS_IsUndefined(tolVal)) { 1121 | if (JS_ToFloat64(ctx, &tolerance, tolVal) < 0) { 1122 | JS_FreeValue(ctx, sdfVal); 1123 | JS_FreeValue(ctx, tolVal); 1124 | return JS_EXCEPTION; 1125 | } 1126 | } 1127 | JS_FreeValue(ctx, tolVal); 1128 | 1129 | JSValue parallelVal = JS_GetPropertyStr(ctx, opts, "canParallel"); 1130 | if (!JS_IsUndefined(parallelVal)) { 1131 | int p = JS_ToBool(ctx, parallelVal); 1132 | if (p < 0) { 1133 | JS_FreeValue(ctx, sdfVal); 1134 | JS_FreeValue(ctx, parallelVal); 1135 | return JS_EXCEPTION; 1136 | } 1137 | canParallel = p == 1; 1138 | } 1139 | JS_FreeValue(ctx, parallelVal); 1140 | 1141 | if (canParallel) { 1142 | JS_FreeValue(ctx, sdfVal); 1143 | return JS_ThrowTypeError(ctx, 1144 | "levelSet canParallel must be false when using JS SDF"); 1145 | } 1146 | 1147 | JSValue sdfFunc = JS_DupValue(ctx, sdfVal); 1148 | JS_FreeValue(ctx, sdfVal); 1149 | 1150 | bool errorOccurred = false; 1151 | std::string errorMessage; 1152 | auto sdf = [ctx, sdfFunc, &errorOccurred, &errorMessage](manifold::vec3 p) { 1153 | if (errorOccurred) return 0.0; 1154 | JSValue point = JS_NewArray(ctx); 1155 | JS_SetPropertyUint32(ctx, point, 0, JS_NewFloat64(ctx, p.x)); 1156 | JS_SetPropertyUint32(ctx, point, 1, JS_NewFloat64(ctx, p.y)); 1157 | JS_SetPropertyUint32(ctx, point, 2, JS_NewFloat64(ctx, p.z)); 1158 | JSValueConst args[1] = {point}; 1159 | JSValue result = JS_Call(ctx, sdfFunc, JS_UNDEFINED, 1, args); 1160 | JS_FreeValue(ctx, point); 1161 | if (JS_IsException(result)) { 1162 | JSValue exc = JS_GetException(ctx); 1163 | JSValue stack = JS_GetPropertyStr(ctx, exc, "stack"); 1164 | const char *msg = JS_ToCString(ctx, JS_IsUndefined(stack) ? exc : stack); 1165 | errorOccurred = true; 1166 | errorMessage = msg ? msg : "levelSet SDF threw"; 1167 | if (msg) JS_FreeCString(ctx, msg); 1168 | JS_FreeValue(ctx, stack); 1169 | JS_FreeValue(ctx, exc); 1170 | return 0.0; 1171 | } 1172 | double value = 0.0; 1173 | if (JS_ToFloat64(ctx, &value, result) < 0) { 1174 | errorOccurred = true; 1175 | errorMessage = "levelSet SDF must return number"; 1176 | JS_FreeValue(ctx, result); 1177 | return 0.0; 1178 | } 1179 | JS_FreeValue(ctx, result); 1180 | return value; 1181 | }; 1182 | 1183 | auto manifoldPtr = std::make_shared( 1184 | manifold::Manifold::LevelSet(sdf, bounds, edgeLength, level, tolerance, 1185 | false)); 1186 | JS_FreeValue(ctx, sdfFunc); 1187 | if (errorOccurred) { 1188 | return JS_ThrowInternalError(ctx, "%s", errorMessage.c_str()); 1189 | } 1190 | return WrapManifold(ctx, std::move(manifoldPtr)); 1191 | } 1192 | 1193 | JSValue JsAsOriginal(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 1194 | if (argc < 1) { 1195 | return JS_ThrowTypeError(ctx, "asOriginal expects a manifold"); 1196 | } 1197 | JsManifold *target = GetJsManifold(ctx, argv[0]); 1198 | if (!target) return JS_EXCEPTION; 1199 | auto manifold = std::make_shared(target->handle->AsOriginal()); 1200 | return WrapManifold(ctx, std::move(manifold)); 1201 | } 1202 | 1203 | JSValue JsOriginalId(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 1204 | if (argc < 1) { 1205 | return JS_ThrowTypeError(ctx, "originalId expects a manifold"); 1206 | } 1207 | JsManifold *target = GetJsManifold(ctx, argv[0]); 1208 | if (!target) return JS_EXCEPTION; 1209 | return JS_NewUint32(ctx, target->handle->OriginalID()); 1210 | } 1211 | 1212 | JSValue JsReserveIds(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 1213 | if (argc < 1) { 1214 | return JS_ThrowTypeError(ctx, "reserveIds expects count"); 1215 | } 1216 | uint32_t count = 0; 1217 | if (JS_ToUint32(ctx, &count, argv[0]) < 0) return JS_EXCEPTION; 1218 | uint32_t base = manifold::Manifold::ReserveIDs(count); 1219 | return JS_NewUint32(ctx, base); 1220 | } 1221 | 1222 | JSValue JsNumProperties(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 1223 | if (argc < 1) { 1224 | return JS_ThrowTypeError(ctx, "numProperties expects a manifold"); 1225 | } 1226 | JsManifold *target = GetJsManifold(ctx, argv[0]); 1227 | if (!target) return JS_EXCEPTION; 1228 | return JS_NewInt64(ctx, static_cast(target->handle->NumProp())); 1229 | } 1230 | 1231 | JSValue JsNumPropertyVertices(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 1232 | if (argc < 1) { 1233 | return JS_ThrowTypeError(ctx, "numPropertyVertices expects a manifold"); 1234 | } 1235 | JsManifold *target = GetJsManifold(ctx, argv[0]); 1236 | if (!target) return JS_EXCEPTION; 1237 | return JS_NewInt64(ctx, static_cast(target->handle->NumPropVert())); 1238 | } 1239 | 1240 | JSValue JsCalculateNormals(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 1241 | if (argc < 2) { 1242 | return JS_ThrowTypeError(ctx, "calculateNormals expects (manifold, normalIdx, minSharpAngle?)"); 1243 | } 1244 | JsManifold *target = GetJsManifold(ctx, argv[0]); 1245 | if (!target) return JS_EXCEPTION; 1246 | int32_t normalIdx = 0; 1247 | if (JS_ToInt32(ctx, &normalIdx, argv[1]) < 0) return JS_EXCEPTION; 1248 | double minSharp = 60.0; 1249 | if (argc >= 3 && !JS_IsUndefined(argv[2])) { 1250 | if (JS_ToFloat64(ctx, &minSharp, argv[2]) < 0) return JS_EXCEPTION; 1251 | } 1252 | auto manifold = std::make_shared( 1253 | target->handle->CalculateNormals(normalIdx, minSharp)); 1254 | return WrapManifold(ctx, std::move(manifold)); 1255 | } 1256 | 1257 | JSValue JsCalculateCurvature(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 1258 | if (argc < 3) { 1259 | return JS_ThrowTypeError(ctx, "calculateCurvature expects (manifold, gaussianIdx, meanIdx)"); 1260 | } 1261 | JsManifold *target = GetJsManifold(ctx, argv[0]); 1262 | if (!target) return JS_EXCEPTION; 1263 | int32_t gaussianIdx = 0; 1264 | int32_t meanIdx = 0; 1265 | if (JS_ToInt32(ctx, &gaussianIdx, argv[1]) < 0) return JS_EXCEPTION; 1266 | if (JS_ToInt32(ctx, &meanIdx, argv[2]) < 0) return JS_EXCEPTION; 1267 | auto manifold = std::make_shared( 1268 | target->handle->CalculateCurvature(gaussianIdx, meanIdx)); 1269 | return WrapManifold(ctx, std::move(manifold)); 1270 | } 1271 | 1272 | JSValue JsSmoothByNormals(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 1273 | if (argc < 2) { 1274 | return JS_ThrowTypeError(ctx, "smoothByNormals expects (manifold, normalIdx)"); 1275 | } 1276 | JsManifold *target = GetJsManifold(ctx, argv[0]); 1277 | if (!target) return JS_EXCEPTION; 1278 | int32_t normalIdx = 0; 1279 | if (JS_ToInt32(ctx, &normalIdx, argv[1]) < 0) return JS_EXCEPTION; 1280 | auto manifold = std::make_shared( 1281 | target->handle->SmoothByNormals(normalIdx)); 1282 | return WrapManifold(ctx, std::move(manifold)); 1283 | } 1284 | 1285 | JSValue JsSmoothOut(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 1286 | if (argc < 1) { 1287 | return JS_ThrowTypeError(ctx, "smoothOut expects (manifold, minSharpAngle?, minSmoothness?)"); 1288 | } 1289 | JsManifold *target = GetJsManifold(ctx, argv[0]); 1290 | if (!target) return JS_EXCEPTION; 1291 | double minSharp = 60.0; 1292 | double minSmooth = 0.0; 1293 | if (argc >= 2 && !JS_IsUndefined(argv[1])) { 1294 | if (JS_ToFloat64(ctx, &minSharp, argv[1]) < 0) return JS_EXCEPTION; 1295 | } 1296 | if (argc >= 3 && !JS_IsUndefined(argv[2])) { 1297 | if (JS_ToFloat64(ctx, &minSmooth, argv[2]) < 0) return JS_EXCEPTION; 1298 | } 1299 | auto manifold = std::make_shared( 1300 | target->handle->SmoothOut(minSharp, minSmooth)); 1301 | return WrapManifold(ctx, std::move(manifold)); 1302 | } 1303 | 1304 | JSValue JsMinGap(JSContext *ctx, JSValueConst, int argc, JSValueConst *argv) { 1305 | if (argc < 3) { 1306 | return JS_ThrowTypeError(ctx, "minGap expects (manifoldA, manifoldB, searchLength)"); 1307 | } 1308 | JsManifold *a = GetJsManifold(ctx, argv[0]); 1309 | if (!a) return JS_EXCEPTION; 1310 | JsManifold *b = GetJsManifold(ctx, argv[1]); 1311 | if (!b) return JS_EXCEPTION; 1312 | double searchLength = 0.0; 1313 | if (JS_ToFloat64(ctx, &searchLength, argv[2]) < 0) return JS_EXCEPTION; 1314 | return JS_NewFloat64(ctx, a->handle->MinGap(*b->handle, searchLength)); 1315 | } 1316 | 1317 | void RegisterBindingsInternal(JSContext *ctx) { 1318 | JSValue global = JS_GetGlobalObject(ctx); 1319 | JS_SetPropertyStr(ctx, global, "cube", JS_NewCFunction(ctx, JsCube, "cube", 1)); 1320 | JS_SetPropertyStr(ctx, global, "sphere", JS_NewCFunction(ctx, JsSphere, "sphere", 1)); 1321 | JS_SetPropertyStr(ctx, global, "cylinder", JS_NewCFunction(ctx, JsCylinder, "cylinder", 1)); 1322 | JS_SetPropertyStr(ctx, global, "union", JS_NewCFunction(ctx, JsUnion, "union", 1)); 1323 | JS_SetPropertyStr(ctx, global, "difference", JS_NewCFunction(ctx, JsDifference, "difference", 1)); 1324 | JS_SetPropertyStr(ctx, global, "intersection", JS_NewCFunction(ctx, JsIntersection, "intersection", 1)); 1325 | JS_SetPropertyStr(ctx, global, "translate", JS_NewCFunction(ctx, JsTranslate, "translate", 2)); 1326 | JS_SetPropertyStr(ctx, global, "scale", JS_NewCFunction(ctx, JsScale, "scale", 2)); 1327 | JS_SetPropertyStr(ctx, global, "rotate", JS_NewCFunction(ctx, JsRotate, "rotate", 2)); 1328 | JS_SetPropertyStr(ctx, global, "tetrahedron", 1329 | JS_NewCFunction(ctx, JsTetrahedron, "tetrahedron", 0)); 1330 | JS_SetPropertyStr(ctx, global, "compose", 1331 | JS_NewCFunction(ctx, JsCompose, "compose", 1)); 1332 | JS_SetPropertyStr(ctx, global, "decompose", 1333 | JS_NewCFunction(ctx, JsDecompose, "decompose", 1)); 1334 | JS_SetPropertyStr(ctx, global, "mirror", 1335 | JS_NewCFunction(ctx, JsMirror, "mirror", 2)); 1336 | JS_SetPropertyStr(ctx, global, "transform", 1337 | JS_NewCFunction(ctx, JsTransform, "transform", 2)); 1338 | JS_SetPropertyStr(ctx, global, "setTolerance", 1339 | JS_NewCFunction(ctx, JsSetTolerance, "setTolerance", 2)); 1340 | JS_SetPropertyStr(ctx, global, "simplify", 1341 | JS_NewCFunction(ctx, JsSimplify, "simplify", 2)); 1342 | JS_SetPropertyStr(ctx, global, "refine", 1343 | JS_NewCFunction(ctx, JsRefine, "refine", 2)); 1344 | JS_SetPropertyStr(ctx, global, "refineToLength", 1345 | JS_NewCFunction(ctx, JsRefineToLength, "refineToLength", 2)); 1346 | JS_SetPropertyStr(ctx, global, "refineToTolerance", 1347 | JS_NewCFunction(ctx, JsRefineToTolerance, "refineToTolerance", 2)); 1348 | JS_SetPropertyStr(ctx, global, "hull", 1349 | JS_NewCFunction(ctx, JsHull, "hull", 1)); 1350 | JS_SetPropertyStr(ctx, global, "hullPoints", 1351 | JS_NewCFunction(ctx, JsHullPoints, "hullPoints", 1)); 1352 | JS_SetPropertyStr(ctx, global, "trimByPlane", 1353 | JS_NewCFunction(ctx, JsTrimByPlane, "trimByPlane", 3)); 1354 | JS_SetPropertyStr(ctx, global, "surfaceArea", 1355 | JS_NewCFunction(ctx, JsSurfaceArea, "surfaceArea", 1)); 1356 | JS_SetPropertyStr(ctx, global, "volume", 1357 | JS_NewCFunction(ctx, JsVolume, "volume", 1)); 1358 | JS_SetPropertyStr(ctx, global, "boundingBox", 1359 | JS_NewCFunction(ctx, JsBoundingBox, "boundingBox", 1)); 1360 | JS_SetPropertyStr(ctx, global, "numTriangles", 1361 | JS_NewCFunction(ctx, JsNumTriangles, "numTriangles", 1)); 1362 | JS_SetPropertyStr(ctx, global, "numVertices", 1363 | JS_NewCFunction(ctx, JsNumVertices, "numVertices", 1)); 1364 | JS_SetPropertyStr(ctx, global, "numEdges", 1365 | JS_NewCFunction(ctx, JsNumEdges, "numEdges", 1)); 1366 | JS_SetPropertyStr(ctx, global, "genus", 1367 | JS_NewCFunction(ctx, JsGenus, "genus", 1)); 1368 | JS_SetPropertyStr(ctx, global, "getTolerance", 1369 | JS_NewCFunction(ctx, JsGetTolerance, "getTolerance", 1)); 1370 | JS_SetPropertyStr(ctx, global, "isEmpty", 1371 | JS_NewCFunction(ctx, JsIsEmpty, "isEmpty", 1)); 1372 | JS_SetPropertyStr(ctx, global, "status", 1373 | JS_NewCFunction(ctx, JsStatus, "status", 1)); 1374 | JS_SetPropertyStr(ctx, global, "slice", 1375 | JS_NewCFunction(ctx, JsSlice, "slice", 2)); 1376 | JS_SetPropertyStr(ctx, global, "project", 1377 | JS_NewCFunction(ctx, JsProject, "project", 1)); 1378 | JS_SetPropertyStr(ctx, global, "extrude", 1379 | JS_NewCFunction(ctx, JsExtrude, "extrude", 2)); 1380 | JS_SetPropertyStr(ctx, global, "revolve", 1381 | JS_NewCFunction(ctx, JsRevolve, "revolve", 2)); 1382 | JS_SetPropertyStr(ctx, global, "boolean", 1383 | JS_NewCFunction(ctx, JsBooleanOp, "boolean", 3)); 1384 | JS_SetPropertyStr(ctx, global, "batchBoolean", 1385 | JS_NewCFunction(ctx, JsBatchBoolean, "batchBoolean", 2)); 1386 | JS_SetPropertyStr(ctx, global, "levelSet", 1387 | JS_NewCFunction(ctx, JsLevelSet, "levelSet", 1)); 1388 | JS_SetPropertyStr(ctx, global, "loadMesh", 1389 | JS_NewCFunction(ctx, JsLoadMesh, "loadMesh", 2)); 1390 | JS_SetPropertyStr(ctx, global, "asOriginal", 1391 | JS_NewCFunction(ctx, JsAsOriginal, "asOriginal", 1)); 1392 | JS_SetPropertyStr(ctx, global, "originalId", 1393 | JS_NewCFunction(ctx, JsOriginalId, "originalId", 1)); 1394 | JS_SetPropertyStr(ctx, global, "reserveIds", 1395 | JS_NewCFunction(ctx, JsReserveIds, "reserveIds", 1)); 1396 | JS_SetPropertyStr(ctx, global, "numProperties", 1397 | JS_NewCFunction(ctx, JsNumProperties, "numProperties", 1)); 1398 | JS_SetPropertyStr(ctx, global, "numPropertyVertices", 1399 | JS_NewCFunction(ctx, JsNumPropertyVertices, 1400 | "numPropertyVertices", 1)); 1401 | JS_SetPropertyStr(ctx, global, "calculateNormals", 1402 | JS_NewCFunction(ctx, JsCalculateNormals, 1403 | "calculateNormals", 3)); 1404 | JS_SetPropertyStr(ctx, global, "calculateCurvature", 1405 | JS_NewCFunction(ctx, JsCalculateCurvature, 1406 | "calculateCurvature", 3)); 1407 | JS_SetPropertyStr(ctx, global, "smoothByNormals", 1408 | JS_NewCFunction(ctx, JsSmoothByNormals, 1409 | "smoothByNormals", 2)); 1410 | JS_SetPropertyStr(ctx, global, "smoothOut", 1411 | JS_NewCFunction(ctx, JsSmoothOut, "smoothOut", 3)); 1412 | JS_SetPropertyStr(ctx, global, "minGap", 1413 | JS_NewCFunction(ctx, JsMinGap, "minGap", 3)); 1414 | JS_FreeValue(ctx, global); 1415 | } 1416 | 1417 | } // namespace 1418 | 1419 | void EnsureManifoldClass(JSRuntime *runtime) { 1420 | EnsureManifoldClassInternal(runtime); 1421 | } 1422 | 1423 | void RegisterBindings(JSContext *ctx) { 1424 | RegisterBindingsInternal(ctx); 1425 | } 1426 | 1427 | std::shared_ptr GetManifoldHandle(JSContext *ctx, 1428 | JSValueConst value) { 1429 | return GetManifoldHandleInternal(ctx, value); 1430 | } --------------------------------------------------------------------------------