CSG¶
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())