CSG¶
Constructive Solid Geometry boolean operations on 3D shapes.
▶ Run in browserTags: 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())