
痛点场景还原假设我要做一个SSS全等判定的演示给定三边长度AB4, BC3, AC5在坐标系中画出这个三角形然后改变边长观察三角形是否唯一确定。如果纯手动操作我会这样写from manim import * import numpy as np class PainfulSSSDemo(Scene): def construct(self): # 手动指定顶点坐标 —— 怎么知道这三个坐标能满足边长条件 A np.array([0, 0, 0]) # 固定 A 在原点 B np.array([4, 0, 0]) # 固定 B 在 (4,0)这样 AB4 # C 的位置需要同时满足 AC5, BC3 # 只能手动解方程x²y²25, (x-4)²y²9 # 手算得 x4, y3于是 C(4,3) —— 但这是直角三角形特例 # 换一组边长又要重新手算而且每次都要判断镜像解 C np.array([4, 3, 0]) triangle Polygon(A, B, C) self.add(triangle)核心痛点给 A、B 定好位置后C 的坐标必须同时满足 AC 和 BC 的距离约束手算就是解二元二次方程组换个边长就要重算一次。SAS和ASA更麻烦涉及角度条件需要把“∠A60∘”转化为向量点积方程手算极易出错。方程组通常有两组解镜像三角形需要判断哪个是合理的朝向。手动算出来的坐标是近似值动画中顶点位置不够精准。这些计算本质上就是几何约束求解完全可以交给SymPy自动完成。2. SymPy 解决方案介绍SymPy可以将几何条件转化为代数方程然后自动求解顶点坐标。2.1 SSS 全等已知三边求顶点已知 A(0,0), B(c,0)求 C(x,y) 满足 ACb, BCa。import sympy as sp x, y sp.symbols(x y, realTrue) a, b, c 3, 5, 4 # BC, AC, AB # 距离约束转化为方程 eq1 (x - 0)**2 (y - 0)**2 - b**2 # AC 5 eq2 (x - c)**2 (y - 0)**2 - a**2 # BC 3 solutions sp.solve([eq1, eq2], (x, y)) # 输出两组解[(4, -3), (4, 3)] —— 对应镜像三角形对于SAS和ASA只需把角度条件用向量点积或余弦定理表示同样构建方程组求解。2.2 SAS 全等已知两边和夹角求第三顶点已知 ABc, ACb, $ \angle A\theta A 在原点 B 在 (c,0) C 满足到 A 的距离为 b 、到 B $ 的距离用余弦定理求import sympy as sp x, y sp.symbols(x y, realTrue) b, c, theta 4, 5, sp.rad(60) # AC4, AB5, ∠A60° # C 到 A 的距离 eq1 x**2 y**2 - b**2 # 用余弦定理BC² AB² AC² - 2·AB·AC·cosθ bc_sq c**2 b**2 - 2*c*b*sp.cos(theta) eq2 (x - c)**2 y**2 - bc_sq solutions sp.solve([eq1, eq2], (x, y))2.3 筛选合理解SymPy会返回两组解关于 AB 所在直线对称通过指定 y 的正负号可以筛选# 筛选 y 0 的解取上方三角形 valid_solution [sol for sol in solutions if sol[1] 0][0]这样就能得到唯一确定的三角形顶点坐标完美支撑全等判定定理的可视化。3. Manim 联动实战下面是一个完整的动画场景用ValueTracker控制边长动态展示SSS全等下三角形的唯一确定性。from manim import * import sympy as sp import numpy as np class SSSCongruenceDemo(Scene): def construct(self): # 固定两个顶点 A np.array([-1, 0, 0]) B np.array([1, 0, 0]) # 可调边长 a_tracker ValueTracker(2) # BC b_tracker ValueTracker(2) # AC # 用 always_redraw 动态更新三角形两个镜像三角形 triangles always_redraw( lambda: self.get_triangles( A, B, a_tracker.get_value(), b_tracker.get_value() ) ) self.add(triangles) # 顶点标签 labels always_redraw( lambda: self.get_labels(A, B, a_tracker.get_value(), b_tracker.get_value()) ) self.add(labels) # 边长标注 side_labels always_redraw( lambda: self.get_side_labels( A, B, a_tracker.get_value(), b_tracker.get_value() ) ) self.add(side_labels) # 动画改变 BC 和 AC 的长度 self.play(a_tracker.animate.set_value(3), run_time2) self.play(b_tracker.animate.set_value(1), run_time2) self.play( a_tracker.animate.set_value(2), b_tracker.animate.set_value(2), run_time2 ) self.wait() def solve_vertex_C(self, A, B, a, b): 用 SymPy 求解顶点 C返回两个镜像点的 np.array 坐标列表 x, y sp.symbols(x y, realTrue) # AB 的长度 c np.linalg.norm(B - A) # 距离约束方程 eq1 (x - 0) ** 2 (y - 0) ** 2 - b**2 # 以 A 为原点 eq2 (x - c) ** 2 (y - 0) ** 2 - a**2 # 以 B 为原点 solutions sp.solve([eq1, eq2], (x, y), dictTrue) if not solutions: return [] # 转换到实际坐标系 AB_vec B - A x_axis AB_vec / c y_axis np.array([-x_axis[1], x_axis[0], 0]) C_points [] for sol in solutions: sol_x float(sp.N(sol[x])) sol_y float(sp.N(sol[y])) # 局部坐标转全局坐标 C A sol_x * x_axis sol_y * y_axis C_points.append(C) return C_points def get_triangles(self, A, B, a, b): C_points self.solve_vertex_C(A, B, a, b) if not C_points: return VGroup() # 无法构成三角形时返回空 triangles VGroup() colors [BLUE, GREEN] for i, C in enumerate(C_points): triangle Polygon( A, B, C, colorcolors[i % 2], fill_opacity0.3, stroke_width2 ) triangles.add(triangle) return triangles def get_labels(self, A, B, a, b): C_points self.solve_vertex_C(A, B, a, b) if not C_points: return VGroup() label_A MathTex(A, colorWHITE, font_size28).next_to(A, DL, buff0.15) label_B MathTex(B, colorWHITE, font_size28).next_to(B, DR, buff0.15) labels VGroup(label_A, label_B) for i, C in enumerate(C_points): direction UP if C[1] A[1] else DOWN label_C MathTex(fC_{i1}, colorWHITE, font_size28).next_to( C, direction, buff0.15 ) labels.add(label_C) return labels def get_side_labels(self, A, B, a, b): C_points self.solve_vertex_C(A, B, a, b) if not C_points: return VGroup() c np.linalg.norm(B - A) labels VGroup() # AB 边长标注共用 label_AB MathTex(f{c:.1f}, font_size24, colorYELLOW).move_to( (A B) / 2 DOWN * 0.3 ) labels.add(label_AB) # 每个三角形的 AC 和 BC 边长标注 for i, C in enumerate(C_points): direction LEFT if C[0] (A[0] B[0]) / 2 else RIGHT label_AC MathTex(f{b:.1f}, font_size24, colorRED).move_to( (A C) / 2 direction * 0.3 ) label_BC MathTex(f{a:.1f}, font_size24, colorRED).move_to( (B C) / 2 (-direction) * 0.3 ) labels.add(label_AC, label_BC) return labels