pyforge3d 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. forge3d/__init__.py +105 -0
  2. forge3d/app.py +219 -0
  3. forge3d/backend.py +118 -0
  4. forge3d/camera.py +235 -0
  5. forge3d/collision/__init__.py +20 -0
  6. forge3d/collision/detection.py +739 -0
  7. forge3d/collision/epa.py +217 -0
  8. forge3d/collision/gjk.py +266 -0
  9. forge3d/collision/heightfield.py +196 -0
  10. forge3d/collision/layers.py +38 -0
  11. forge3d/constraints/__init__.py +35 -0
  12. forge3d/constraints/base.py +132 -0
  13. forge3d/constraints/joints.py +635 -0
  14. forge3d/contact/__init__.py +1 -0
  15. forge3d/contact/solver.py +341 -0
  16. forge3d/dynamics/__init__.py +24 -0
  17. forge3d/dynamics/aba.py +108 -0
  18. forge3d/dynamics/crba.py +73 -0
  19. forge3d/dynamics/model.py +97 -0
  20. forge3d/dynamics/rnea.py +260 -0
  21. forge3d/errors.py +98 -0
  22. forge3d/events.py +267 -0
  23. forge3d/facade.py +1032 -0
  24. forge3d/input.py +243 -0
  25. forge3d/io/__init__.py +10 -0
  26. forge3d/io/mesh_data.py +206 -0
  27. forge3d/io/obj_loader.py +203 -0
  28. forge3d/io/world_snapshot.py +284 -0
  29. forge3d/logging.py +35 -0
  30. forge3d/math/__init__.py +59 -0
  31. forge3d/math/inertia.py +60 -0
  32. forge3d/math/quaternion.py +141 -0
  33. forge3d/math/se3.py +162 -0
  34. forge3d/math/spatial.py +139 -0
  35. forge3d/model/__init__.py +14 -0
  36. forge3d/model/kinematics.py +134 -0
  37. forge3d/model/robot_config.py +138 -0
  38. forge3d/model/urdf_loader.py +194 -0
  39. forge3d/py.typed +0 -0
  40. forge3d/recorder.py +204 -0
  41. forge3d/render/__init__.py +1 -0
  42. forge3d/render/base.py +36 -0
  43. forge3d/render/hq/__init__.py +1 -0
  44. forge3d/render/hq/raytracer.py +297 -0
  45. forge3d/render/hq/renderer.py +72 -0
  46. forge3d/render/hq/scene.py +138 -0
  47. forge3d/render/realtime/__init__.py +5 -0
  48. forge3d/render/realtime/context.py +119 -0
  49. forge3d/render/realtime/meshes.py +254 -0
  50. forge3d/render/realtime/renderer.py +565 -0
  51. forge3d/render/realtime/shaders.py +193 -0
  52. forge3d/render/snapshot.py +91 -0
  53. forge3d/robot/__init__.py +38 -0
  54. forge3d/robot/presets.py +82 -0
  55. forge3d/robot/robot.py +162 -0
  56. forge3d/sim/__init__.py +1 -0
  57. forge3d/sim/domain_rand.py +113 -0
  58. forge3d/sim/jax_batch.py +191 -0
  59. forge3d/sim/world.py +626 -0
  60. forge3d/viewer.py +235 -0
  61. pyforge3d-1.0.0.dist-info/METADATA +566 -0
  62. pyforge3d-1.0.0.dist-info/RECORD +64 -0
  63. pyforge3d-1.0.0.dist-info/WHEEL +4 -0
  64. pyforge3d-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,38 @@
1
+ """Collision layer and mask bit-field constants.
2
+
3
+ Usage::
4
+
5
+ box.collision_layer = forge3d.CollisionLayer.DEFAULT
6
+ box.collision_mask = forge3d.CollisionLayer.DEFAULT | forge3d.CollisionLayer.PLAYER
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+
12
+ class CollisionLayer:
13
+ """Named bit-field constants for collision layers.
14
+
15
+ Collision rule: bodies A and B collide if and only if:
16
+ ``(A.collision_layer & B.collision_mask) != 0``
17
+ and
18
+ ``(B.collision_layer & A.collision_mask) != 0``
19
+
20
+ The default is layer 0x0001 with mask 0xFFFF (collide with everything).
21
+ """
22
+
23
+ DEFAULT = 1 << 0 # 0x0001 — default layer for all bodies
24
+ PLAYER = 1 << 1 # 0x0002
25
+ ENEMY = 1 << 2 # 0x0004
26
+ BULLET = 1 << 3 # 0x0008
27
+ TRIGGER = 1 << 4 # 0x0010
28
+ TERRAIN = 1 << 5 # 0x0020
29
+ DEBRIS = 1 << 6 # 0x0040
30
+ SENSOR = 1 << 7 # 0x0080
31
+
32
+ NONE = 0
33
+ ALL = 0xFFFF # collide with all 16 standard layers
34
+
35
+
36
+ def layers_collide(layer_a: int, mask_a: int, layer_b: int, mask_b: int) -> bool:
37
+ """Return True if body a and body b should collide."""
38
+ return bool((layer_a & mask_b) != 0 and (layer_b & mask_a) != 0)
@@ -0,0 +1,35 @@
1
+ """forge3d constraint / joint system.
2
+
3
+ All constraints use velocity-level Sequential Impulse with
4
+ Baumgarte position-error stabilization.
5
+
6
+ Supported joint types
7
+ ---------------------
8
+ FixedJoint — rigid weld (0 DOF)
9
+ BallJoint — ball-and-socket (3 rotational DOF)
10
+ HingeJoint — revolute (1 rotational DOF) with optional limits + motor
11
+ PrismaticJoint — slider (1 translational DOF) with optional limits + motor
12
+ DistanceJoint — maintain distance between two anchor points
13
+ SpringJoint — spring-damper force element (not a hard constraint)
14
+ """
15
+
16
+ from forge3d.constraints.base import Constraint, JointHandle
17
+ from forge3d.constraints.joints import (
18
+ BallJoint,
19
+ DistanceJoint,
20
+ FixedJoint,
21
+ HingeJoint,
22
+ PrismaticJoint,
23
+ SpringJoint,
24
+ )
25
+
26
+ __all__ = [
27
+ "Constraint",
28
+ "JointHandle",
29
+ "FixedJoint",
30
+ "BallJoint",
31
+ "HingeJoint",
32
+ "PrismaticJoint",
33
+ "DistanceJoint",
34
+ "SpringJoint",
35
+ ]
@@ -0,0 +1,132 @@
1
+ """Base classes for the constraint / joint system."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from abc import ABC, abstractmethod
6
+ from dataclasses import dataclass, field
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ import numpy as np
10
+
11
+ if TYPE_CHECKING:
12
+ from forge3d.sim.world import _Body
13
+
14
+ # Baumgarte stabilization — position error fed back as velocity bias
15
+ BAUMGARTE_BETA = 0.05 # conservative: avoids instability while still correcting drift
16
+ BAUMGARTE_SLOP = 1e-3 # 1 mm dead-zone
17
+
18
+
19
+ @dataclass
20
+ class JointHandle:
21
+ """Opaque handle returned by World.add_joint().
22
+
23
+ Keep it to later call world.remove_joint(handle).
24
+ """
25
+
26
+ joint_id: int
27
+ joint_type: str
28
+
29
+
30
+ class Constraint(ABC):
31
+ """Abstract base for all velocity-level constraints.
32
+
33
+ Sub-classes must implement ``apply(bodies, dt)``, which modifies
34
+ the body list in-place (replace immutable _Body with updated copies).
35
+ """
36
+
37
+ joint_id: int = field(default=-1)
38
+
39
+ @abstractmethod
40
+ def apply(self, bodies: list[Any], id_to_idx: dict[int, int], dt: float) -> None:
41
+ """Apply constraint impulses to the body list.
42
+
43
+ Args:
44
+ bodies: Flat list of ``_Body`` objects (may be mutated via replace).
45
+ id_to_idx: Map from body_id to list index.
46
+ dt: Physics time-step in seconds.
47
+ """
48
+
49
+ # ── Helper: compute world-frame inertia inverse ───────────────────────────
50
+
51
+ @staticmethod
52
+ def _I_world_inv(b: Any) -> np.ndarray:
53
+ """World-frame inverse inertia (3×3).
54
+
55
+ For point masses or static bodies returns zero matrix.
56
+ """
57
+ if b.static or b.inertia_inv_local is None:
58
+ return np.zeros((3, 3))
59
+ R = _quat_to_rot(b.quat)
60
+ I_inv_local = b.inertia_inv_local # diagonal (3×3)
61
+ return R @ I_inv_local @ R.T
62
+
63
+ @staticmethod
64
+ def _effective_mass_ball(
65
+ b_a: Any, r_a: np.ndarray, b_b: Any, r_b: np.ndarray
66
+ ) -> np.ndarray:
67
+ """Effective mass matrix K (3×3) for a point-to-point constraint.
68
+
69
+ K = K_a + K_b where K_x = 1/m_x * I + [r_x×] * I_x_world_inv * [r_x×]ᵀ
70
+ """
71
+ def _k(b: Any, r: np.ndarray) -> np.ndarray:
72
+ if b.static:
73
+ return np.zeros((3, 3))
74
+ m_inv = 1.0 / b.mass if b.mass > 0 else 0.0
75
+ I_inv = Constraint._I_world_inv(b)
76
+ rx = _skew(r)
77
+ return m_inv * np.eye(3) + rx @ I_inv @ rx.T
78
+
79
+ return _k(b_a, r_a) + _k(b_b, r_b)
80
+
81
+ @staticmethod
82
+ def _apply_impulse_pair(
83
+ bodies: list[Any],
84
+ id_to_idx: dict[int, int],
85
+ id_a: int,
86
+ id_b: int,
87
+ impulse: np.ndarray,
88
+ r_a: np.ndarray,
89
+ r_b: np.ndarray,
90
+ ) -> None:
91
+ """Apply linear impulse J and angular reactions to body pair."""
92
+ from dataclasses import replace
93
+
94
+ if id_a >= 0:
95
+ idx_a = id_to_idx[id_a]
96
+ ba = bodies[idx_a]
97
+ if not ba.static and ba.mass > 0:
98
+ dv = impulse / ba.mass
99
+ I_inv = Constraint._I_world_inv(ba)
100
+ dw = I_inv @ np.cross(r_a, impulse)
101
+ bodies[idx_a] = replace(ba, vel=ba.vel + dv, omega=ba.omega + dw)
102
+
103
+ if id_b >= 0:
104
+ idx_b = id_to_idx[id_b]
105
+ bb = bodies[idx_b]
106
+ if not bb.static and bb.mass > 0:
107
+ dv = -impulse / bb.mass
108
+ I_inv = Constraint._I_world_inv(bb)
109
+ dw = I_inv @ np.cross(r_b, -impulse)
110
+ bodies[idx_b] = replace(bb, vel=bb.vel + dv, omega=bb.omega + dw)
111
+
112
+
113
+ # ── Utility functions ─────────────────────────────────────────────────────────
114
+
115
+
116
+ def _quat_to_rot(q: np.ndarray) -> np.ndarray:
117
+ """Quaternion [w,x,y,z] → 3×3 rotation matrix."""
118
+ w, x, y, z = q
119
+ return np.array([
120
+ [1 - 2*(y*y + z*z), 2*(x*y - w*z), 2*(x*z + w*y)],
121
+ [2*(x*y + w*z), 1 - 2*(x*x + z*z), 2*(y*z - w*x)],
122
+ [2*(x*z - w*y), 2*(y*z + w*x), 1 - 2*(x*x + y*y)],
123
+ ])
124
+
125
+
126
+ def _skew(v: np.ndarray) -> np.ndarray:
127
+ """Skew-symmetric cross-product matrix."""
128
+ return np.array([
129
+ [ 0.0, -v[2], v[1]],
130
+ [ v[2], 0.0, -v[0]],
131
+ [-v[1], v[0], 0.0],
132
+ ])