Download cadshim.py from HelloSun/WFreeCAD: direct link, hf CLI and curl.
- Browser
- Download file 27.1 kB
-
https://huggingface.co/spaces/HelloSun/WFreeCAD/resolve/main/cadshim.py
- Command line
-
hf download hf://spaces/HelloSun/WFreeCAD/cadshim.py
-
curl -L -o cadshim.py https://huggingface.co/spaces/HelloSun/WFreeCAD/resolve/main/cadshim.py
27.1 kB
| """A tiny, browser-friendly subset of the FreeCAD Python API. | |
| The shim does not perform real boundary-representation modelling. It records | |
| the operations that a macro asks for and emits a small JSON scene graph which | |
| the Three.js front-end can build and display. It is intentionally forgiving | |
| so that simple Part-based macros run without a FreeCAD installation. | |
| """ | |
| import copy | |
| import json | |
| import math | |
| import sys | |
| import traceback | |
| import types | |
| PALETTE = [ | |
| (0.94, 0.68, 0.32), | |
| (0.40, 0.64, 0.56), | |
| (0.94, 0.49, 0.37), | |
| (0.40, 0.50, 0.66), | |
| (0.83, 0.78, 0.66), | |
| (0.60, 0.47, 0.70), | |
| ] | |
| # -------------------------------------------------------------------------- | |
| # small vector / quaternion helpers | |
| # -------------------------------------------------------------------------- | |
| def _num(value, default=0.0): | |
| try: | |
| return float(value) | |
| except Exception: | |
| return default | |
| class Vector: | |
| __slots__ = ("x", "y", "z") | |
| def __init__(self, x=0.0, y=0.0, z=0.0): | |
| if isinstance(x, Vector): | |
| self.x, self.y, self.z = x.x, x.y, x.z | |
| elif isinstance(x, (tuple, list)): | |
| values = list(x) + [0.0, 0.0, 0.0] | |
| self.x, self.y, self.z = _num(values[0]), _num(values[1]), _num(values[2]) | |
| else: | |
| self.x, self.y, self.z = _num(x), _num(y), _num(z) | |
| def __iter__(self): | |
| yield self.x | |
| yield self.y | |
| yield self.z | |
| def __getitem__(self, index): | |
| return (self.x, self.y, self.z)[index] | |
| def __len__(self): | |
| return 3 | |
| def __add__(self, other): | |
| other = Vector(other) | |
| return Vector(self.x + other.x, self.y + other.y, self.z + other.z) | |
| def __sub__(self, other): | |
| other = Vector(other) | |
| return Vector(self.x - other.x, self.y - other.y, self.z - other.z) | |
| def __mul__(self, other): | |
| if isinstance(other, (Vector, tuple, list)): | |
| return self.dot(Vector(other)) | |
| return Vector(self.x * other, self.y * other, self.z * other) | |
| __rmul__ = __mul__ | |
| def __truediv__(self, other): | |
| return Vector(self.x / other, self.y / other, self.z / other) | |
| def __neg__(self): | |
| return Vector(-self.x, -self.y, -self.z) | |
| def __repr__(self): | |
| return "Vector (%g, %g, %g)" % (self.x, self.y, self.z) | |
| def clone(self): | |
| return Vector(self) | |
| copy = clone | |
| def Length(self): | |
| return math.sqrt(self.x * self.x + self.y * self.y + self.z * self.z) | |
| def normalize(self): | |
| length = self.Length() | |
| if length > 1e-12: | |
| self.x /= length | |
| self.y /= length | |
| self.z /= length | |
| return self | |
| normalized = normalize | |
| def dot(self, other): | |
| other = Vector(other) | |
| return self.x * other.x + self.y * other.y + self.z * other.z | |
| def cross(self, other): | |
| other = Vector(other) | |
| return Vector( | |
| self.y * other.z - self.z * other.y, | |
| self.z * other.x - self.x * other.z, | |
| self.x * other.y - self.y * other.x, | |
| ) | |
| def to_list(self): | |
| return [self.x, self.y, self.z] | |
| def _qmul(a, b): | |
| x1, y1, z1, w1 = a | |
| x2, y2, z2, w2 = b | |
| return ( | |
| w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2, | |
| w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2, | |
| w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2, | |
| w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2, | |
| ) | |
| def _qaxis(axis, angle_deg): | |
| axis = Vector(axis) | |
| length = axis.Length() | |
| if length < 1e-12: | |
| return (0.0, 0.0, 0.0, 1.0) | |
| axis = axis / length | |
| half = math.radians(_num(angle_deg)) * 0.5 | |
| sine = math.sin(half) | |
| return (axis.x * sine, axis.y * sine, axis.z * sine, math.cos(half)) | |
| def _qnorm(q): | |
| length = math.sqrt(sum(v * v for v in q)) | |
| if length < 1e-12: | |
| return (0.0, 0.0, 0.0, 1.0) | |
| return tuple(v / length for v in q) | |
| def _qconj(q): | |
| return (-q[0], -q[1], -q[2], q[3]) | |
| def _qrotate(q, v): | |
| v = Vector(v) | |
| r = _qmul(_qmul(q, (v.x, v.y, v.z, 0.0)), _qconj(q)) | |
| return Vector(r[0], r[1], r[2]) | |
| def _qmatrix(q): | |
| x, y, z, w = _qnorm(q) | |
| xx, yy, zz = x * x, y * y, z * z | |
| xy, xz, yz = x * y, x * z, y * z | |
| wx, wy, wz = w * x, w * y, w * z | |
| return [ | |
| [1 - 2 * (yy + zz), 2 * (xy - wz), 2 * (xz + wy)], | |
| [2 * (xy + wz), 1 - 2 * (xx + zz), 2 * (yz - wx)], | |
| [2 * (xz - wy), 2 * (yz + wx), 1 - 2 * (xx + yy)], | |
| ] | |
| def _matmul(a, b): | |
| return [[sum(a[r][k] * b[k][c] for k in range(4)) for c in range(4)] for r in range(4)] | |
| def _mat_identity(): | |
| return [ | |
| [1.0, 0.0, 0.0, 0.0], | |
| [0.0, 1.0, 0.0, 0.0], | |
| [0.0, 0.0, 1.0, 0.0], | |
| [0.0, 0.0, 0.0, 1.0], | |
| ] | |
| def _mat_translate(v): | |
| m = _mat_identity() | |
| m[0][3], m[1][3], m[2][3] = v.x, v.y, v.z | |
| return m | |
| def _mat_rotate(q): | |
| r = _qmatrix(q) | |
| return [ | |
| [r[0][0], r[0][1], r[0][2], 0.0], | |
| [r[1][0], r[1][1], r[1][2], 0.0], | |
| [r[2][0], r[2][1], r[2][2], 0.0], | |
| [0.0, 0.0, 0.0, 1.0], | |
| ] | |
| def _mat_to_column_major(m): | |
| return [m[r][c] for c in range(4) for r in range(4)] | |
| def _mat_is_identity(m): | |
| for r in range(4): | |
| for c in range(4): | |
| expected = 1.0 if r == c else 0.0 | |
| if abs(m[r][c] - expected) > 1e-9: | |
| return False | |
| return True | |
| class Rotation: | |
| def __init__(self, *args): | |
| if not args: | |
| self.q = (0.0, 0.0, 0.0, 1.0) | |
| elif len(args) == 1 and isinstance(args[0], Rotation): | |
| self.q = tuple(args[0].q) | |
| elif len(args) == 1 and isinstance(args[0], (tuple, list)) and len(args[0]) == 4: | |
| self.q = _qnorm(tuple(_num(v) for v in args[0])) | |
| elif len(args) == 2: | |
| self.q = _qaxis(Vector(args[0]), args[1]) | |
| elif len(args) == 3: | |
| yaw = _qaxis((0.0, 0.0, 1.0), args[0]) | |
| pitch = _qaxis((0.0, 1.0, 0.0), args[1]) | |
| roll = _qaxis((1.0, 0.0, 0.0), args[2]) | |
| self.q = _qnorm(_qmul(_qmul(yaw, pitch), roll)) | |
| elif len(args) == 4: | |
| self.q = _qnorm(tuple(_num(v) for v in args)) | |
| else: | |
| self.q = (0.0, 0.0, 0.0, 1.0) | |
| def __repr__(self): | |
| return "Rotation (%g, %g, %g, %g)" % self.q | |
| def multiply(self, other): | |
| return Rotation(_qmul(self.q, Rotation(other).q)) | |
| def __mul__(self, other): | |
| if isinstance(other, Placement): | |
| return Placement(other.Base, self).multiply(other.Rotation) | |
| return self.multiply(other) | |
| def toMatrix(self): | |
| return _qmatrix(self.q) | |
| def Q(self): | |
| return self.q | |
| def Angle(self): | |
| w = max(-1.0, min(1.0, self.q[3])) | |
| return math.degrees(2.0 * math.acos(w)) | |
| def Axis(self): | |
| x, y, z, w = self.q | |
| s = math.sqrt(max(0.0, 1.0 - w * w)) | |
| if s < 1e-9: | |
| return Vector(0.0, 0.0, 1.0) | |
| return Vector(x / s, y / s, z / s) | |
| def copy(self): | |
| return Rotation(self) | |
| class Placement: | |
| def __init__(self, *args): | |
| self.Base = Vector() | |
| self.Rotation = Rotation() | |
| if not args: | |
| return | |
| if len(args) == 1 and isinstance(args[0], Placement): | |
| self.Base = Vector(args[0].Base) | |
| self.Rotation = Rotation(args[0].Rotation) | |
| elif len(args) == 1 and isinstance(args[0], (Vector, tuple, list)): | |
| self.Base = Vector(args[0]) | |
| elif len(args) >= 2: | |
| self.Base = Vector(args[0]) | |
| self.Rotation = Rotation(args[1]) | |
| def __repr__(self): | |
| return "Placement [Pos=%s, Rot=%s]" % (self.Base, self.Rotation) | |
| def copy(self): | |
| return Placement(self) | |
| def multiply(self, other): | |
| other = Placement(other) | |
| q = _qmul(self.Rotation.q, other.Rotation.q) | |
| base = self.Base + _qrotate(self.Rotation.q, other.Base) | |
| return Placement(base, Rotation(q)) | |
| def __mul__(self, other): | |
| if isinstance(other, Vector): | |
| return self.Base + _qrotate(self.Rotation.q, other) | |
| if isinstance(other, Placement): | |
| return self.multiply(other) | |
| if isinstance(other, Shape): | |
| shape = other.copy() | |
| shape.Placement = self * shape.Placement | |
| return shape | |
| raise TypeError("unsupported Placement multiplication") | |
| def __rmul__(self, other): | |
| if isinstance(other, Vector): | |
| return self.Base + _qrotate(self.Rotation.q, other) | |
| return NotImplemented | |
| def toMatrix(self): | |
| return _mat_to_column_major(_matmul(_mat_translate(self.Base), _mat_rotate(self.Rotation.q))) | |
| class Matrix: | |
| def __init__(self, *args): | |
| self._m = _mat_identity() | |
| if len(args) == 16: | |
| values = [_num(v) for v in args] | |
| self._m = [[values[c * 4 + r] for c in range(4)] for r in range(4)] | |
| elif len(args) == 1 and isinstance(args[0], Matrix): | |
| self._m = copy.deepcopy(args[0]._m) | |
| def move(self, vector): | |
| self._m = _matmul(_mat_translate(Vector(vector)), self._m) | |
| def multiply(self, other): | |
| self._m = _matmul(self._m, other._m) | |
| def A11(self): | |
| return self._m[0][0] | |
| def to_list(self): | |
| return _mat_to_column_major(self._m) | |
| # -------------------------------------------------------------------------- | |
| # shape tree | |
| # -------------------------------------------------------------------------- | |
| class Shape: | |
| def __init__(self, node=None, profile=None, placed=False): | |
| self._node = node | |
| self._profile = profile | |
| self._placement = Placement() | |
| def __repr__(self): | |
| return "<Shape>" | |
| def Placement(self): | |
| return self._placement | |
| def Placement(self, value): | |
| self._placement = Placement(value) | |
| def ShapeType(self): | |
| return "Solid" | |
| def copy(self): | |
| shape = Shape(copy.deepcopy(self._node), copy.deepcopy(self._profile)) | |
| shape._placement = Placement(self._placement) | |
| return shape | |
| def render_node(self): | |
| node = copy.deepcopy(self._node) | |
| matrix = self._placement.toMatrix() | |
| if not _mat_is_identity(_matmul(_mat_translate(self._placement.Base), _mat_rotate(self._placement.Rotation.q))): | |
| node = {"op": "transform", "m": matrix, "child": node} | |
| return node | |
| def translate(self, vector, *args): | |
| self._placement = Placement(Vector(vector)) * self._placement | |
| return self | |
| def rotate(self, center, axis, angle, *args): | |
| center = Vector(center) | |
| rot = Placement(Vector(0, 0, 0), Rotation(axis, angle)) | |
| self._placement = ( | |
| Placement(center) * rot * Placement(-center) * self._placement | |
| ) | |
| return self | |
| def scale(self, *args): | |
| return self | |
| def cut(self, tool): | |
| return _boolean("cut", self, tool) | |
| def fuse(self, tool): | |
| return _boolean("fuse", self, tool) | |
| def common(self, tool): | |
| return _boolean("common", self, tool) | |
| def multiFuse(self, tools): | |
| return _multi_boolean("fuse", [self] + list(tools)) | |
| def multiCommon(self, tools): | |
| return _multi_boolean("common", [self] + list(tools)) | |
| def extrude(self, vector): | |
| if self._profile is None: | |
| raise ValueError("extrude() needs a Face or closed wire") | |
| node = {"op": "extrude", "profile": self._profile, "vector": Vector(vector).to_list()} | |
| matrix = self._placement.toMatrix() | |
| if not _mat_is_identity( | |
| _matmul(_mat_translate(self._placement.Base), _mat_rotate(self._placement.Rotation.q)) | |
| ): | |
| node = {"op": "transform", "m": matrix, "child": node} | |
| return Shape(node) | |
| def removeSplitter(self): | |
| return self | |
| def isClosed(self): | |
| return self._profile is not None | |
| def _boolean(kind, a, b): | |
| if not isinstance(a, Shape) or not isinstance(b, Shape): | |
| raise TypeError("boolean operations need Shape arguments") | |
| node = {"op": "boolean", "kind": kind, "a": a.render_node(), "b": b.render_node()} | |
| return Shape(node) | |
| def _multi_boolean(kind, shapes): | |
| shapes = [s for s in shapes if isinstance(s, Shape)] | |
| if not shapes: | |
| raise ValueError("no shapes supplied") | |
| if len(shapes) == 1: | |
| return shapes[0].copy() | |
| node = {"op": "group", "children": [s.render_node() for s in shapes]} | |
| return Shape(node) | |
| def _points2d(points): | |
| result = [] | |
| for point in points: | |
| p = Vector(point) | |
| result.append([p.x, p.y]) | |
| return result | |
| def _make_face(profile, placement=None): | |
| face = Shape({"op": "face", "profile": profile}, profile=profile) | |
| if placement is not None: | |
| face._placement = Placement(placement) | |
| return face | |
| # -------------------------------------------------------------------------- | |
| # Part module | |
| # -------------------------------------------------------------------------- | |
| def _make_box(length, width, height, pnt=None, dir=None): | |
| return Shape( | |
| { | |
| "op": "box", | |
| "l": _num(length), | |
| "w": _num(width), | |
| "h": _num(height), | |
| "pnt": Vector(pnt).to_list() if pnt is not None else [0, 0, 0], | |
| "dir": Vector(dir).to_list() if dir is not None else [0, 0, 1], | |
| } | |
| ) | |
| def _make_cylinder(radius, height, pnt=None, dir=None, angle=360): | |
| return Shape( | |
| { | |
| "op": "cylinder", | |
| "r": _num(radius), | |
| "h": _num(height), | |
| "pnt": Vector(pnt).to_list() if pnt is not None else [0, 0, 0], | |
| "dir": Vector(dir).to_list() if dir is not None else [0, 0, 1], | |
| "angle": _num(angle, 360), | |
| } | |
| ) | |
| def _make_sphere(radius, pnt=None, *args): | |
| return Shape( | |
| { | |
| "op": "sphere", | |
| "r": _num(radius), | |
| "pnt": Vector(pnt).to_list() if pnt is not None else [0, 0, 0], | |
| } | |
| ) | |
| def _make_cone(radius1, radius2, height, pnt=None, dir=None, angle=360): | |
| return Shape( | |
| { | |
| "op": "cone", | |
| "r1": _num(radius1), | |
| "r2": _num(radius2), | |
| "h": _num(height), | |
| "pnt": Vector(pnt).to_list() if pnt is not None else [0, 0, 0], | |
| "dir": Vector(dir).to_list() if dir is not None else [0, 0, 1], | |
| "angle": _num(angle, 360), | |
| } | |
| ) | |
| def _make_torus(radius1, radius2, pnt=None, dir=None, *args): | |
| return Shape( | |
| { | |
| "op": "torus", | |
| "r1": _num(radius1), | |
| "r2": _num(radius2), | |
| "pnt": Vector(pnt).to_list() if pnt is not None else [0, 0, 0], | |
| "dir": Vector(dir).to_list() if dir is not None else [0, 0, 1], | |
| } | |
| ) | |
| def _make_polygon(points, closed=False): | |
| profile = _points2d(points) | |
| return Shape({"op": "wire", "profile": profile, "closed": bool(closed)}, profile=profile) | |
| def _make_line(point1, point2): | |
| return _make_polygon([point1, point2], False) | |
| def _make_face_from(value): | |
| if isinstance(value, Shape): | |
| if value._profile is None: | |
| raise ValueError("Part.Face() needs a closed wire") | |
| return _make_face(copy.deepcopy(value._profile), value._placement) | |
| profile = _points2d(value) | |
| if len(profile) > 2 and profile[0] != profile[-1]: | |
| profile.append(profile[0]) | |
| return _make_face(profile) | |
| def _make_compound(shapes): | |
| shapes = [s for s in shapes if isinstance(s, Shape)] | |
| return _multi_boolean("fuse", shapes) | |
| def _show(shape, name=None): | |
| document = _active_document() | |
| if document is None: | |
| document = newDocument("Unnamed") | |
| return document._show(shape, name) | |
| # -------------------------------------------------------------------------- | |
| # document | |
| # -------------------------------------------------------------------------- | |
| class ViewObject: | |
| def __init__(self, obj): | |
| self.obj = obj | |
| self.ShapeColor = None | |
| self.LineColor = None | |
| self.Visibility = True | |
| self.Transparency = 0 | |
| self.DisplayMode = "Flat Lines" | |
| def update(self): | |
| pass | |
| class DocObject: | |
| def __init__(self, document, type_id, name): | |
| object.__setattr__(self, "Document", document) | |
| object.__setattr__(self, "Name", name) | |
| object.__setattr__(self, "Label", name) | |
| object.__setattr__(self, "TypeId", type_id) | |
| object.__setattr__(self, "_shape", None) | |
| object.__setattr__(self, "_feature_shape", None) | |
| object.__setattr__(self, "_dirty", True) | |
| object.__setattr__(self, "ViewObject", ViewObject(self)) | |
| object.__setattr__(self, "_props", self._defaults(type_id)) | |
| if "Placement" not in self._props: | |
| self._props["Placement"] = Placement() | |
| def _defaults(type_id): | |
| if type_id == "Part::Box": | |
| return {"Length": 10.0, "Width": 10.0, "Height": 10.0, "Placement": Placement()} | |
| if type_id == "Part::Cylinder": | |
| return {"Radius": 2.0, "Height": 10.0, "Angle": 360.0, "Placement": Placement()} | |
| if type_id == "Part::Sphere": | |
| return {"Radius": 5.0, "Placement": Placement()} | |
| if type_id == "Part::Cone": | |
| return { | |
| "Radius1": 2.0, | |
| "Radius2": 4.0, | |
| "Height": 10.0, | |
| "Angle": 360.0, | |
| "Placement": Placement(), | |
| } | |
| if type_id == "Part::Torus": | |
| return {"Radius1": 10.0, "Radius2": 2.0, "Placement": Placement()} | |
| if type_id in ("Part::Cut", "Part::Fuse"): | |
| return {"Base": None, "Tool": None, "Placement": Placement()} | |
| if type_id in ("Part::MultiFuse", "Part::MultiCommon"): | |
| return {"Shapes": [], "Placement": Placement()} | |
| return {"Placement": Placement()} | |
| def __getattr__(self, name): | |
| props = object.__getattribute__(self, "_props") | |
| if name in props: | |
| return props[name] | |
| raise AttributeError("'%s' object has no attribute '%s'" % (self.TypeId, name)) | |
| def __setattr__(self, name, value): | |
| if name in ("Document", "Name", "Label", "TypeId", "ViewObject"): | |
| object.__setattr__(self, name, value) | |
| elif name == "Shape": | |
| object.__setattr__(self, "_feature_shape", value) | |
| object.__setattr__(self, "_shape", value) | |
| object.__setattr__(self, "_dirty", False) | |
| elif name.startswith("_"): | |
| object.__setattr__(self, name, value) | |
| else: | |
| self._props[name] = value | |
| object.__setattr__(self, "_dirty", True) | |
| def Shape(self): | |
| if self._dirty or self._shape is None: | |
| self._shape = self._compute() | |
| object.__setattr__(self, "_dirty", False) | |
| return self._shape | |
| def Shape(self, value): | |
| object.__setattr__(self, "_feature_shape", value) | |
| object.__setattr__(self, "_shape", value) | |
| object.__setattr__(self, "_dirty", False) | |
| def _compute(self): | |
| type_id = self.TypeId | |
| placement = self._props.get("Placement", Placement()) | |
| if type_id == "Part::Feature": | |
| return self._feature_shape | |
| if type_id == "Part::Box": | |
| shape = _make_box(self.Length, self.Width, self.Height) | |
| elif type_id == "Part::Cylinder": | |
| shape = _make_cylinder(self.Radius, self.Height, angle=self.Angle) | |
| elif type_id == "Part::Sphere": | |
| shape = _make_sphere(self.Radius) | |
| elif type_id == "Part::Cone": | |
| shape = _make_cone(self.Radius1, self.Radius2, self.Height, angle=self.Angle) | |
| elif type_id == "Part::Torus": | |
| shape = _make_torus(self.Radius1, self.Radius2) | |
| elif type_id == "Part::Cut": | |
| if self.Base is None or self.Tool is None: | |
| raise ValueError("%s needs Base and Tool" % self.Name) | |
| shape = self.Base.Shape.copy().cut(self.Tool.Shape) | |
| elif type_id == "Part::Fuse": | |
| if self.Base is None or self.Tool is None: | |
| raise ValueError("%s needs Base and Tool" % self.Name) | |
| shape = self.Base.Shape.copy().fuse(self.Tool.Shape) | |
| elif type_id == "Part::MultiFuse": | |
| shape = _multi_boolean("fuse", [s.Shape for s in self.Shapes]) | |
| elif type_id == "Part::MultiCommon": | |
| shape = _multi_boolean("common", [s.Shape for s in self.Shapes]) | |
| else: | |
| return self._feature_shape | |
| if type_id != "Part::Feature": | |
| shape.Placement = placement | |
| return shape | |
| class Document: | |
| def __init__(self, name="Unnamed"): | |
| self.Name = name | |
| self.Label = name | |
| self._objects = [] | |
| self._counter = 0 | |
| self._errors = [] | |
| def Objects(self): | |
| return list(self._objects) | |
| def addObject(self, type_id, name=None): | |
| return self._add(type_id, name) | |
| def _add(self, type_id, name=None): | |
| if name is None: | |
| name = type_id.split("::")[-1] + "%03d" % self._counter | |
| base_name = name | |
| while self.getObject(name) is not None: | |
| self._counter += 1 | |
| name = base_name + "%03d" % self._counter | |
| obj = DocObject(self, type_id, name) | |
| self._objects.append(obj) | |
| self._counter += 1 | |
| return obj | |
| def _show(self, shape, name=None): | |
| if not isinstance(shape, Shape): | |
| raise TypeError("Part.show() needs a Shape") | |
| obj = self._add("Part::Feature", name or "Shape") | |
| obj.Shape = shape.copy() | |
| return obj | |
| def recompute(self): | |
| self._errors = [] | |
| for obj in list(self._objects): | |
| try: | |
| _ = obj.Shape | |
| except Exception as error: | |
| self._errors.append("%s: %s" % (obj.Name, error)) | |
| return self | |
| def getObject(self, name): | |
| for obj in self._objects: | |
| if obj.Name == name: | |
| return obj | |
| return None | |
| def removeObject(self, name): | |
| obj = self.getObject(name) | |
| if obj is not None: | |
| self._objects.remove(obj) | |
| def __repr__(self): | |
| return "<Document %s>" % self.Name | |
| # -------------------------------------------------------------------------- | |
| # module installation | |
| # -------------------------------------------------------------------------- | |
| _freecad = None | |
| _part = None | |
| _documents = {} | |
| _active = None | |
| _log = [] | |
| def _log_message(kind): | |
| def inner(*args, **kwargs): | |
| text = " ".join(str(a) for a in args) | |
| _log.append("%s: %s" % (kind, text)) | |
| return inner | |
| def newDocument(name="Unnamed", *args, **kwargs): | |
| global _active | |
| document = Document(name) | |
| if name in _documents: | |
| index = 1 | |
| while "%s%03d" % (name, index) in _documents: | |
| index += 1 | |
| document.Name = "%s%03d" % (name, index) | |
| document.Label = document.Name | |
| _documents[document.Name] = document | |
| _active = document | |
| if _freecad is not None: | |
| _freecad.ActiveDocument = document | |
| return document | |
| def activeDocument(): | |
| return _active | |
| def _active_document(): | |
| return _active | |
| def closeDocument(name): | |
| global _active | |
| document = _documents.pop(name, None) | |
| if _active is document: | |
| _active = None | |
| if _freecad is not None: | |
| _freecad.ActiveDocument = None | |
| return document | |
| def listDocuments(): | |
| return dict(_documents) | |
| def _get_color(obj, index): | |
| color = getattr(obj.ViewObject, "ShapeColor", None) | |
| opacity = 1.0 - (max(0.0, min(100.0, _num(getattr(obj.ViewObject, "Transparency", 0)))) / 100.0) | |
| if color is None: | |
| rgb = PALETTE[index % len(PALETTE)] | |
| elif isinstance(color, str): | |
| value = color.strip() | |
| if value.startswith("#") and len(value) >= 7: | |
| rgb = ( | |
| int(value[1:3], 16) / 255.0, | |
| int(value[3:5], 16) / 255.0, | |
| int(value[5:7], 16) / 255.0, | |
| ) | |
| else: | |
| rgb = PALETTE[index % len(PALETTE)] | |
| else: | |
| values = list(color) | |
| values += [0.0, 0.0, 0.0] | |
| rgb = (_num(values[0], 0.8), _num(values[1], 0.8), _num(values[2], 0.8)) | |
| return [max(0.0, min(1.0, float(c))) for c in rgb], opacity | |
| def build_scene(): | |
| document = _active | |
| if document is None: | |
| return {"objects": [], "errors": [], "log": list(_log)} | |
| document.recompute() | |
| objects = [] | |
| for obj in document.Objects: | |
| if getattr(obj.ViewObject, "Visibility", True) is False: | |
| continue | |
| try: | |
| shape = obj.Shape | |
| except Exception: | |
| continue | |
| if shape is None: | |
| continue | |
| color, opacity = _get_color(obj, len(objects)) | |
| objects.append( | |
| { | |
| "name": obj.Name, | |
| "color": color, | |
| "opacity": opacity, | |
| "node": shape.render_node(), | |
| } | |
| ) | |
| return {"objects": objects, "errors": list(document._errors), "log": list(_log)} | |
| class Runtime: | |
| def reset(self): | |
| global _active, _log | |
| _documents.clear() | |
| _active = None | |
| _log = [] | |
| if _freecad is not None: | |
| _freecad.ActiveDocument = None | |
| def run(self, code): | |
| self.reset() | |
| try: | |
| namespace = {"__name__": "__main__", "__file__": "<wfreecad>"} | |
| exec(compile(code, "<wfreecad>", "exec"), namespace, namespace) | |
| scene = build_scene() | |
| return {"ok": True, "scene": scene} | |
| except Exception: | |
| return {"ok": False, "error": traceback.format_exc(limit=8), "log": list(_log)} | |
| _runtime = Runtime() | |
| def install(): | |
| global _freecad, _part | |
| if _freecad is not None: | |
| return | |
| _freecad = types.ModuleType("FreeCAD") | |
| _freecad.Vector = Vector | |
| _freecad.Rotation = Rotation | |
| _freecad.Placement = Placement | |
| _freecad.Matrix = Matrix | |
| _freecad.newDocument = newDocument | |
| _freecad.activeDocument = activeDocument | |
| _freecad.closeDocument = closeDocument | |
| _freecad.listDocuments = listDocuments | |
| _freecad.ActiveDocument = None | |
| _freecad.Console = types.SimpleNamespace( | |
| PrintMessage=_log_message("Message"), | |
| PrintWarning=_log_message("Warning"), | |
| PrintError=_log_message("Error"), | |
| ) | |
| sys.modules["FreeCAD"] = _freecad | |
| _part = types.ModuleType("Part") | |
| _part.makeBox = _make_box | |
| _part.makeCylinder = _make_cylinder | |
| _part.makeSphere = _make_sphere | |
| _part.makeCone = _make_cone | |
| _part.makeTorus = _make_torus | |
| _part.makePolygon = _make_polygon | |
| _part.makeLine = _make_line | |
| _part.makeCompound = _make_compound | |
| _part.Face = _make_face_from | |
| _part.Wire = lambda *args, **kwargs: _make_polygon(args[0] if args else [], True) | |
| _part.show = _show | |
| _part.Shape = Shape | |
| sys.modules["Part"] = _part | |
| try: | |
| sys.modules["FreeCADGui"] = types.ModuleType("FreeCADGui") | |
| except Exception: | |
| pass | |
| install() | |
| __wfcad_runtime = _runtime | |