CSG

Constructive Solid Geometry boolean operations on 3D shapes.

▶ Run in browser

Tags: 3d

Demonstrates:

  • CSG Union: combining two shapes into one solid

  • CSG Subtract: punching a hole through a shape

  • CSG Intersect: keeping only the overlapping volume

  • CSGCombiner3D generating meshes from boolean operations

  • Camera orbit with arrow keys

Controls: Left / Right - Orbit camera horizontally Up / Down - Orbit camera vertically Escape - Quit

Run: uv run python examples/features/3d/csg.py

Source

  1"""CSG: Constructive Solid Geometry boolean operations on 3D shapes.
  2
  3# /// simvx
  4# web = { width = 1280, height = 720 }
  5# ///
  6
  7Demonstrates:
  8  - CSG Union: combining two shapes into one solid
  9  - CSG Subtract: punching a hole through a shape
 10  - CSG Intersect: keeping only the overlapping volume
 11  - CSGCombiner3D generating meshes from boolean operations
 12  - Camera orbit with arrow keys
 13
 14Controls:
 15    Left / Right  - Orbit camera horizontally
 16    Up / Down     - Orbit camera vertically
 17    Escape        - Quit
 18
 19Run: uv run python examples/features/3d/csg.py
 20"""
 21
 22
 23import math
 24
 25from simvx.core import (
 26    Camera3D,
 27    CSGBox3D,
 28    CSGCombiner3D,
 29    CSGOperation,
 30    CSGSphere3D,
 31    DirectionalLight3D,
 32    Input,
 33    InputMap,
 34    Key,
 35    Material,
 36    MeshInstance3D,
 37    Node,
 38    Property,
 39    Text2D,
 40    Vec3,
 41)
 42from simvx.graphics import App
 43
 44
 45class CSGDemo(Node):
 46    """Three side-by-side CSG boolean operation results with orbit camera."""
 47
 48    orbit_speed = Property(60.0, range=(10, 120))
 49    cam_distance = Property(12.0, range=(5, 30))
 50
 51    def on_ready(self):
 52        # Input actions (registered here so web export picks them up;
 53        # module-level / __main__ registrations are skipped by WebApp).
 54        InputMap.add_action("orbit_left", [Key.LEFT])
 55        InputMap.add_action("orbit_right", [Key.RIGHT])
 56        InputMap.add_action("orbit_up", [Key.UP])
 57        InputMap.add_action("orbit_down", [Key.DOWN])
 58        InputMap.add_action("quit", [Key.ESCAPE])
 59
 60        # Camera
 61        self._yaw = 30.0
 62        self._pitch = 25.0
 63        self._cam = self.add_child(Camera3D(name="Camera", fov=55, near=0.1, far=100.0))
 64
 65        # Lighting
 66        sun = self.add_child(DirectionalLight3D(name="Sun", intensity=1.2))
 67        sun.look_at((-1.0, -2.0, -1.0))
 68        fill = self.add_child(DirectionalLight3D(name="Fill", intensity=0.4, colour=(0.5, 0.6, 1.0)))
 69        fill.look_at((1.0, -1.0, 2.0))
 70
 71        # Build the three CSG demos
 72        spacing = 5.0
 73        self._build_union(position=Vec3(-spacing, 0, 0))
 74        self._build_subtract(position=Vec3(0, 0, 0))
 75        self._build_intersect(position=Vec3(spacing, 0, 0))
 76
 77        # Labels
 78        self.add_child(Text2D(name="Title", text="CSG Boolean Operations", x=10, y=10, font_scale=1.5))
 79        self.add_child(Text2D(name="Controls", text="Arrow keys: orbit camera | Esc: quit", x=10, y=690, font_scale=1.1))
 80        self.add_child(Text2D(name="LblUnion", text="UNION", x=200, y=50, font_scale=1.3))
 81        self.add_child(Text2D(name="LblSubtract", text="SUBTRACT", x=540, y=50, font_scale=1.3))
 82        self.add_child(Text2D(name="LblIntersect", text="INTERSECT", x=900, y=50, font_scale=1.3))
 83
 84        self._update_camera()
 85
 86    def _build_union(self, position: Vec3):
 87        """Box + Sphere combined (both UNION)."""
 88        combiner = CSGCombiner3D()
 89        box = CSGBox3D(size=Vec3(2, 2, 2))
 90        box.operation = CSGOperation.UNION
 91        combiner.add_child(box)
 92        sphere = CSGSphere3D(radius=1.3, rings=20, sectors=20)
 93        sphere.operation = CSGOperation.UNION
 94        combiner.add_child(sphere)
 95        mesh = combiner.mesh
 96        mat = Material(colour=(0.2, 0.6, 0.9, 1.0), roughness=0.4, metallic=0.2)
 97        mi = MeshInstance3D(name="Union", mesh=mesh, material=mat, position=position)
 98        self.add_child(mi)
 99
100    def _build_subtract(self, position: Vec3):
101        """Box with sphere hole punched through it."""
102        combiner = CSGCombiner3D()
103        box = CSGBox3D(size=Vec3(2, 2, 2))
104        box.operation = CSGOperation.UNION
105        combiner.add_child(box)
106        sphere = CSGSphere3D(radius=1.3, rings=20, sectors=20)
107        sphere.operation = CSGOperation.SUBTRACT
108        combiner.add_child(sphere)
109        mesh = combiner.mesh
110        mat = Material(colour=(0.9, 0.3, 0.2, 1.0), roughness=0.35, metallic=0.3)
111        mi = MeshInstance3D(name="Subtract", mesh=mesh, material=mat, position=position)
112        self.add_child(mi)
113
114    def _build_intersect(self, position: Vec3):
115        """Only the volume where box and sphere overlap."""
116        combiner = CSGCombiner3D()
117        box = CSGBox3D(size=Vec3(2, 2, 2))
118        box.operation = CSGOperation.UNION
119        combiner.add_child(box)
120        sphere = CSGSphere3D(radius=1.3, rings=20, sectors=20)
121        sphere.operation = CSGOperation.INTERSECT
122        combiner.add_child(sphere)
123        mesh = combiner.mesh
124        mat = Material(colour=(0.2, 0.8, 0.3, 1.0), roughness=0.3, metallic=0.4)
125        mi = MeshInstance3D(name="Intersect", mesh=mesh, material=mat, position=position)
126        self.add_child(mi)
127
128    def _update_camera(self):
129        yaw_rad = math.radians(self._yaw)
130        pitch_rad = math.radians(self._pitch)
131        cp = math.cos(pitch_rad)
132        x = self.cam_distance * cp * math.sin(yaw_rad)
133        y = self.cam_distance * math.sin(pitch_rad)
134        z = self.cam_distance * cp * math.cos(yaw_rad)
135        self._cam.position = Vec3(x, y, z)
136        self._cam.look_at(Vec3(0, 0, 0))
137
138    def on_process(self, dt):
139        if Input.is_action_just_pressed("quit"):
140            self.app.quit()
141            return
142        if Input.is_action_pressed("orbit_left"):
143            self._yaw += self.orbit_speed * dt
144        if Input.is_action_pressed("orbit_right"):
145            self._yaw -= self.orbit_speed * dt
146        if Input.is_action_pressed("orbit_up"):
147            self._pitch = min(80.0, self._pitch + self.orbit_speed * dt)
148        if Input.is_action_pressed("orbit_down"):
149            self._pitch = max(-10.0, self._pitch - self.orbit_speed * dt)
150        self._update_camera()
151
152
153if __name__ == "__main__":
154
155    App(title="CSG Boolean Operations", width=1280, height=720).run(CSGDemo())