CSG

Play Demo

CSG (Constructive Solid Geometry) Demo — Boolean operations on 3D shapes.

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 packages/graphics/examples/3d_csg.py

Source Code

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