diff --git a/.github/workflows/package-ios.yml b/.github/workflows/package-ios.yml new file mode 100644 index 0000000..cb0b3e9 --- /dev/null +++ b/.github/workflows/package-ios.yml @@ -0,0 +1,57 @@ +name: package-ios + +# Manual only. A macOS runner is required and this file does not run on push. +on: + workflow_dispatch: + inputs: + target: + description: simulator builds a .app for the iOS Simulator. device exports an IPA only when signing secrets are set. + type: choice + options: + - simulator + - device + default: simulator + +permissions: + contents: read + +jobs: + ios: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: 1.90.0 + targets: aarch64-apple-ios,aarch64-apple-ios-sim,x86_64-apple-ios + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + - name: CocoaPods + run: brew install cocoapods + - run: npm ci + - name: Simulator app + if: inputs.target == 'simulator' + run: bash scripts/package-ios.sh --simulator + - name: Signed IPA + if: inputs.target == 'device' + env: + IOS_CERTIFICATE: ${{ secrets.IOS_CERTIFICATE }} + IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }} + IOS_MOBILE_PROVISION: ${{ secrets.IOS_MOBILE_PROVISION }} + APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} + APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }} + APPLE_API_KEY_PATH: ${{ secrets.APPLE_API_KEY_PATH }} + run: bash scripts/package-ios.sh --device + - uses: actions/upload-artifact@v4 + if: always() && hashFiles('packaging/out/**') != '' + with: + name: mathscrap-ios + path: | + packaging/out/* + src-tauri/gen/apple/**/*.app + if-no-files-found: warn diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml new file mode 100644 index 0000000..aa85bb9 --- /dev/null +++ b/.github/workflows/package.yml @@ -0,0 +1,194 @@ +name: package + +on: + push: + branches: + - cursor/mathscrap-packaging-* + pull_request: + branches: + - main + workflow_dispatch: + inputs: + sign_windows: + description: Import WINDOWS_CERTIFICATE and sign the EXE + type: boolean + default: false + sign_android: + description: Sign the release APK with ANDROID_KEY_* + type: boolean + default: false + +permissions: + contents: read + +concurrency: + group: package-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: 1.90.0 + - uses: Swatinem/rust-cache@v2 + with: + workspaces: crates/mathcore + - name: Chrome for sheet end-to-end + run: | + if ! command -v google-chrome >/dev/null 2>&1 && ! command -v google-chrome-stable >/dev/null 2>&1; then + sudo apt-get update + wget -q -O /tmp/chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb + sudo apt-get install -y /tmp/chrome.deb + fi + (google-chrome --version || google-chrome-stable --version) + - run: npm ci + - run: npm run lint + - run: npm run test:rust + - run: npm run smoke + - run: npm run smoke:ocr + - run: npm run smoke:ai + - run: npm run test:pack + - name: Start the dev server + run: | + npm run dev > dev-server.log 2>&1 & + echo $! > dev-server.pid + for _ in $(seq 1 40); do + if curl -sf http://127.0.0.1:47321/ >/dev/null; then + exit 0 + fi + sleep 2 + done + echo "dev server did not start" + cat dev-server.log + exit 1 + - name: Sheet end-to-end + env: + SHOTS: e2e-shots + run: npm run e2e:sheet + - name: Per-formula plot end-to-end + env: + SHOTS: e2e-plots + run: npm run e2e + - name: Upload failure logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: mathscrap-test-failure + path: | + e2e-shots + e2e-plots + dev-server.log + if-no-files-found: ignore + + web: + needs: test + if: github.event_name != 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm run pack:web + - uses: actions/upload-artifact@v4 + with: + name: mathscrap-web-dist + path: dist + if-no-files-found: error + + android: + needs: test + if: github.event_name != 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: 1.90.0 + targets: aarch64-linux-android,armv7-linux-androideabi,i686-linux-android,x86_64-linux-android + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + - run: npm ci + - name: Debug APK + if: github.event_name != 'workflow_dispatch' || inputs.sign_android == false + run: bash scripts/package-android.sh --debug --install-sdk + - name: Release APK + if: github.event_name == 'workflow_dispatch' && inputs.sign_android + env: + ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + ANDROID_KEY_BASE64: ${{ secrets.ANDROID_KEY_BASE64 }} + run: bash scripts/package-android.sh --release --install-sdk + - uses: actions/upload-artifact@v4 + with: + name: mathscrap-android + path: | + packaging/out/*.apk + packaging/out/SHA256SUMS + if-no-files-found: error + + windows: + needs: test + if: github.event_name != 'pull_request' + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: 1.90.0 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + - run: npm ci + - name: Import Authenticode certificate + if: github.event_name == 'workflow_dispatch' && inputs.sign_windows + shell: pwsh + env: + WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} + WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} + run: | + if ([string]::IsNullOrEmpty($env:WINDOWS_CERTIFICATE) -or [string]::IsNullOrEmpty($env:WINDOWS_CERTIFICATE_PASSWORD)) { + Write-Error "sign_windows was set but WINDOWS_CERTIFICATE or WINDOWS_CERTIFICATE_PASSWORD is empty." + exit 1 + } + New-Item -ItemType Directory -Force -Path certificate | Out-Null + Set-Content -Path certificate/tempCert.txt -Value $env:WINDOWS_CERTIFICATE + certutil -decode certificate/tempCert.txt certificate/certificate.pfx | Out-Null + Remove-Item certificate/tempCert.txt + $secure = ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -Force -AsPlainText + $imported = Import-PfxCertificate -FilePath certificate/certificate.pfx -CertStoreLocation Cert:\CurrentUser\My -Password $secure + "WINDOWS_CERTIFICATE_THUMBPRINT=$($imported.Thumbprint)" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + Remove-Item -Recurse -Force certificate + - name: Unsigned or signed installer + shell: bash + run: bash scripts/package-windows.sh + - uses: actions/upload-artifact@v4 + with: + name: mathscrap-windows + path: | + packaging/out/*.exe + packaging/out/*.msi + packaging/out/SHA256SUMS + if-no-files-found: error diff --git a/.gitignore b/.gitignore index 523d259..e261fd1 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,15 @@ ASTER_BG_TASK.md edge-mathscrap.png screen-check.png +# native shells (generated) and local package output +src-tauri/target +src-tauri/gen +packaging/out + +# signing material — never commit +*.keystore +*.jks +*.p12 +*.mobileprovision +keystore.properties + diff --git a/README.md b/README.md index 191a0fd..03b01cc 100644 --- a/README.md +++ b/README.md @@ -1,411 +1,181 @@ # MathScrap -A math-learning scrapbook on one continuous sheet of kraft paper, in the spirit of Apple Freeform. There are no cards or widgets: you draw with the pen, and formulas are written straight onto the paper like ink. The **坐标系 (coordinate system)** button in the corner draws **every formula on the sheet on the same axes**. The panel finds intersections between them, solves shared unknowns, and **derives as many quantities as possible**: remaining sides and angles, unknown parameters, area, domain, and more. +MathScrap 是一张连续的牛皮纸。笔迹和公式直接写在纸上,没有卡片,也没有贴在空白纸上的大块说明。右下角的坐标系把这张纸上能画的式子画进同一套坐标轴,并尽量推出交点、未知量和几何量。 -> 本仓库是一个可点击的演示:Vite + React 19 + TypeScript + Tailwind v4 + shadcn/ui,绘图用 Plotly,数学核心是 **Rust 编译的 WebAssembly**(`crates/mathcore`)。 +MathScrap is one continuous sheet of kraft paper. Ink and formulas sit on the paper itself. There are no cards, and the blank sheet does not carry a large instruction panel. The coordinate button in the corner draws every plottable formula on that sheet onto one set of axes, and derives intersections, unknowns, and geometric quantities when it can. ---- +计算在本机完成:浏览器或系统 WebView 里的 Rust/WebAssembly(`crates/mathcore`),手写公式走随包的 CoMER INT8 ONNX。界面有简体中文和 English。许可证是 Apache-2.0。 -## 快速开始 +Computation stays on the device: Rust compiled to WebAssembly (`crates/mathcore`) inside the browser or the system WebView, and handwritten formulas go through the bundled CoMER INT8 ONNX models. The interface is Simplified Chinese and English. The license is Apache-2.0. -```bash -npm install -npm run dev # http://localhost:47321 -``` +## 对数 -- 不需要安装 Rust:编译好的 WASM(`src/wasm/pkg/`)已经提交在仓库里。 -- 改了 Rust 代码才需要重新编译(见下文 “Rust / WASM”)。 +## Logarithms -| 命令 | 作用 | -| --- | --- | -| `npm run dev` | 开发服务器(端口 47321,监听 0.0.0.0,手机/平板可用局域网 IP 访问) | -| `npm run build` | 类型检查 + 生产构建到 `dist/` | -| `npm run preview` | 预览生产构建(端口 47322) | -| `npm run lint` | oxlint | -| `npm run smoke` | 不开浏览器:加载真实 WASM,对 77 个单式用例和 19 张“整页联立”用例做推导,并检查每个图形在初始坐标窗口里确实有可见的数据(`-v` 打印全部推导) | -| `npm run smoke:ai` | 不开浏览器:起一个本地 OpenAI 兼容接口,检查「测试连接」、工具调用和空配置立刻返回;再让 MCP 服务器列出并调用工具 | -| `npm run smoke:ocr` | 不开浏览器:几何回归,以及 CoMER 预处理 + 假的编码器/解码器(不会自己编 `y = x`) | -| `npm run ocr:live` | 本机用 ONNX Runtime Web WASM 跑仓库里的 CoMER INT8,检查 `y = 2`、`y = x`、`a^2 = b`、`1/2` | -| `npm run mcp` | 启动 stdio MCP 服务器(给 Cursor / Claude 用;人直接跑它会停在 stdio 上,见下文) | -| `npm run e2e` | 浏览器端到端:先 `npm run dev`,再用无头 Chrome 打开每个用例的坐标系,检查 SVG 线条和截图像素,确认 2D/3D 图都真的画出来了(`CHROME_PATH` 可指定浏览器路径) | -| `npm run e2e:sheet` | 浏览器端到端:空白纸 → 在纸上写式子 → 右下角半透明图 → 详情展开,并检查手写会自己写上纸、更新图 | -| `npm run build:wasm` | 重新编译 Rust → WASM(需要 Rust + wasm-pack) | -| `npm run test:rust` | Rust 单元测试 | - ---- - -## 一张纸,一个坐标系 - -打开就是一张真正空白的牛皮纸:没有卡片、没有胶带标题,纸面上也没有任何文字、水印或品牌字样。应用名只有 MathScrap(浏览器标题和「…」菜单顶部)。 - -- **画**:用笔、荧光笔、橡皮直接在纸上画。 -- **写公式**:双击纸面、按 `/`,或选底部的 Σ 工具再点纸面。在点的位置会出现一个光标,下面实时预览墨迹效果;按 Enter(或点别处)就写上纸了。 -- **改公式**:双击写好的式子可以原地改写,改成空白就是擦掉它。 -- **移动公式**:用「选择」工具(`V`)拖动式子,空白处拖动是平移;在有手写笔的设备上,手指本来就负责选择和平移。 -- **看图**:右下角的 **坐标系** 按钮显示纸上有几个可画的式子。点开后,右下角只浮着一张略透明的图,纸面继续可以写。点图上的 **详情** 才展开式子列表和推导;宽屏上这时是左边纸、右边图,手机上则是全屏。图开着的时候继续写,图形会跟着更新: - - 交点在两两之间自动求出。 - - 同名字母视为同一个未知数,一起求解。比如 `A(0,0), B(4,0), C(1,t)` 写一行、`AC ⊥ BC` 写另一行,也能解出 t。 - - 只写条件的式子(`AC ⊥ BC`、`f(0) = 1`)归到它前面那个式子所在的坐标系。 - - **边写边画**:分屏打开时继续写式子(或手写识别成公式),坐标系立刻跟着长出新图形;你缩放过的视野不会被新式子重置,「适应视野」按钮随时回到恰好框住所有图形的视角。新式子会在图上**逐条浮现**(尊重系统的减弱动态效果设置)。 -- **平面和空间**:纸上同时有平面和空间的式子时,面板顶部可以在「平面 · n」和「空间 · m」两个共享坐标系之间切换。 -- **临时隐藏**:面板顶部列出纸上的每个式子,点一下就暂时从坐标系里拿掉(不会从纸上删除)。如果两道题碰巧用了同一个字母,可以先把不相关的拿掉。 - -## 开箱即用的示例公式 - -右上角 **…** 菜单里的「写几个例子到纸上」会把示例一行一行写到纸上。下面的每个代码块都可以照着写:一行就是纸上的一个式子。 - -**1. 抛物线与直线(2D,两式共用一个坐标系)** +`ln` 是自然对数。单独的 `log` 和 `lg` 是常用对数,底数是 10。这三项不会被改成别的底。 + +`ln` is the natural logarithm. A unary `log` and `lg` are common logarithms, base 10. Those three are not silently given another base. + +显式底数用这些写法,它们求的是同一个值: + +Explicit bases use the forms below. They evaluate to the same value: ``` -y = x^2 - 2 -y = 0.5x + 1 +log_2(8) log_{2}(8) log₂(8) log2(8) log(2, 8) ``` -→ 两个交点 \((-1.5, 0.25)\)、\((2, 2)\),各自的零点、极值、定义域,以及直线的斜率、截距、倾斜角。 +`log(2)` 和 `log 2` 仍是 log₁₀(2),不是以 2 为底。`lg` 不接受粘在后面的底数:`lg2(3)` 是 lg(2)·3。底数必须大于 0 且不等于 1,真数必须大于 0;否则实数结果是空的,图上不画穿过间断或渐近线的假线段。 -再写一个 `x^2 + y^2 = 4` 和 `x^2 + 2x + 1 = 0`,它们会叠进同一个坐标系,交点会一并求出。 +`log(2)` and `log 2` stay log₁₀(2). They are not “base 2”. `lg` does not take a base glued to the name: `lg2(3)` is lg(2)·3. The base must be greater than 0 and not 1, and the argument must be greater than 0. Otherwise there is no real value, and the plot does not draw a fake segment across a gap or an asymptote. -**2. 解三角形:只给两边一角(SAS)** +## 快速开始 -``` -△ABC: a = 5, b = 7, C = 60° -``` +## Quick start -→ \(c = \sqrt{39}\)、∠A、∠B、面积 \(\frac{35\sqrt3}{4}\)、周长、外接圆 / 内切圆半径、三条高、中线、重心 / 外心 / 内心……每一项都标出“用了什么定理”,可以一键标在图上。 +需要 Node.js。仓库里已经带有编译好的 `src/wasm/pkg`,只改页面时不必安装 Rust。改了 `crates/mathcore` 才需要 Rust 1.90 和 wasm-pack,再运行 `npm run build:wasm`。 -**3. 坐标几何 + 垂直条件(带未知数)** +Node.js is required. A built `src/wasm/pkg` is already in the repository, so Rust is not required for page-only changes. Rust 1.90 and wasm-pack are required only after editing `crates/mathcore`; then run `npm run build:wasm`. -``` -A(0,0), B(4,0), C(1,t) -AC ⊥ BC +```bash +npm install +npm run dev ``` -→ Rust 解出 \(t = \pm\sqrt3\)(可切换两组解),然后把整个三角形推出来:直角、边长、面积、各心坐标。 +开发服务器是 `http://127.0.0.1:47321`。`npm run preview` 用 `47322` 预览生产构建。 -更多示例,全部可以直接使用: +The dev server is `http://127.0.0.1:47321`. `npm run preview` serves the production build on port `47322`. -| 写法 | 推出什么 | +| 命令 Command | 作用 / What it does | | --- | --- | -| `x + y + z = 3` 换行 `x - y + 2z = 1` | 3D:法向量、两平面夹角、交线参数式 | -| `z = sin(x)cos(y)` | 3D 曲面,可自由旋转,网格上的最大值、最小值 | -| `f(x) = ax^2 + bx + c` 换行 `f(0) = 1; f(1) = 0; f(3) = 4` | 待定系数:a = 1, b = −2, c = 1 | -| `**a** = (1, 2, 2); **b** = (2, 1, k)` 换行 `**a** ⊥ **b**` | k = −2,然后算点积、叉积、夹角、投影 | -| `x^2 + 2x + 1 = 0` | 一元方程:Δ = 0、重根 x = −1、韦达定理,画出 y = x² + 2x + 1 并在 x 轴上标出根 | -| `⊙O: r = 5` 换行 `弦 AB、CD 交于 P, PA = 2, PB = 6, PC = 4` | 相交弦定理:PD = 3、弦长、弦心距、圆心角、圆周角、弓形高、\|OP\| = √13 | -| `△ABC: b = 4, c = 3, m_a = 2.5` | 中线长反推边长:a = 5,整个 3-4-5 直角三角形(含斜边上的高、射影定理) | -| `A(0,0), B(4,0), C(1,3)` 换行 `M = 中点(A, B), N = 中点(A, C)` | 中点坐标、比值、中位线 MN ∥ BC 且 \\|MN\\| = \\|BC\\|/2 | -| `y = 0`、`y = 1`、`y = 3`、`x = 0`、`x - y = 0` 各写一行 | 三条平行线 + 两条截线:同位角 / 内错角 / 同旁内角、成比例的截线段、相似三角形 | -| `x^2 + y^2 - 4x + 2y = 4` 换行 `y = x - 1` | 识别为圆:圆心 (2, −1)、半径 3,以及与直线的交点 | -| `A(0,0), B(4,0), C(5,3), D(1,3)` | 判定为平行四边形,算出面积、对角线 | -| `y = kx + m, 过(1, 3), 过(2, 5)` | k = 2, m = 1 | -| `y = sin(x)`、`y = cos(x)`、`y = x/3` 各写一行 | 三条曲线叠在一个坐标系里,两两求交点 | -| `x^2 + y^2 + z^2 = 4` | 球面:球心、半径、体积、表面积 | -| `r(t) = (cos(t), sin(t), t/3)` | 3D 参数曲线(螺旋线) | - -### 输入写法速查 - -- 隐式乘法:`2x`、`kx`、`sin(x)cos(y)`,以及 `x^2`、`sqrt(x)`、`|x|`、`pi`、`e`、`a_1`;也能直接输入 `π`、`θ`、`²`、`√`。 -- 角度:`30°`、`30deg`,或者直接写 `C = 60`(在三角形里按角度理解)。 -- **点**:只能用大写字母命名,例如 `A(1,2)`、`P(1,2,3)`、`A = (1,2)`。单独一个小写字母不会被当成点:写 `b(1, 2)` 会提示改用大写。 -- **向量**:必须用粗体名字加 `=` 来定义。`**b** = (1, 2)`、`𝐛 = (1, 2)`、`\mathbf{b} = (1, 2)`、`\vec{b} = (1, 2)` 这几种写法等价,`Alt + B` 可以插入粗体名。直接写 `b = (1, 2)` 会提示改写。两个大写字母组成的线段向量 `AB = (3, 4)` 也可以。定义好之后,条件里可以写 `**a** ⊥ **b**`,也可以直接写 `a ⊥ b`。 -- **方程与函数**: - - 只含 x 的方程(如 `x^2 + 2x + 1 = 0`、`x^2 = 2x + 3`)按解方程处理,画出 y = 左边 − 右边。 - - `x = 2` 仍然是竖直直线。 - - `y = …` 和 `f(x) = …` 是函数。 - - 同时含 x 和 y 的方程是平面曲线,同时含 x、y、z 的是空间方程。 -- 关系:`⊥` 或 `_|_`,`∥` 或 `//`,`|AB| = 5`,`∠ABC = 90°`,`过(1, 3)`。 -- 三角形:`△ABC: a=3, b=4, c=5`,也可以写 `triangle ABC: ...` 或 `三角形ABC: ...`。可用的量有 a、b、c、A、B、C、面积 S,以及中线 `m_a` 和高 `h_a`(例如 `△ABC: b=4, c=3, m_a=2.5` 会反推出 a=5)。 -- **中点 / 分点**:`M = 中点(A, B)`(或 `M = midpoint(A, B)`)定义 AB 的中点;`P = 分点(A, B, 1:2)` 定义满足 AP : PB = 1 : 2 的分点(写 `分点(A, B, 2)` 表示 AP : PB = 2 : 1)。定义出的点和写出来的点一样参与后续全部推导,两个中点共顶点时自动给出中位线。 -- **向量的和差与投影**:`𝐜 = 𝐚 + 𝐛`、`𝐜 = 𝐚 − 𝐛`、`𝐩 = 投影(𝐚, 𝐛)`(或 `𝐩 = proj(𝐚, 𝐛)`,即 𝐚 在 𝐛 上的投影向量)定义出的向量同样进入向量推导。 -- **圆与弦**:`⊙O: r = 5`(也可写 `圆O: r = 5`)定义一个以点 O 为圆心(O 未定义时取原点)的圆; - `⊙O: 过(3, 4)` 用点定半径。弦的问题写在圆的下一行,会自动归属到这个圆: - - `弦 AB、CD 交于 P, PA = 2, PB = 6, PC = 4` → 相交弦定理推出 PD = 3、弦长、弦心距、圆心角、圆周角、弓形高、|OP|。P 的位置可写 `P 在圆内 / P 在圆外`,不写则两种情况都推(可切换)。 - - `割线 AB、CD 交于 P, PA = 2, PB = 6, PC = 4` → 割线定理。 - - `切线 PT、割线 PAB, PA = 2, PB = 6` → 切割线定理推出 PT = 2√3。 -- 一个式子里也可以写多个条件,用 Shift+Enter 换行、`;` 或顶层逗号分隔;分开写成几行效果一样,因为整张纸是一起推导的。 - ---- - -## 三种写字方式(PC 端也不只是键盘) - -1. **手写**:鼠标、触控、手写笔都能直接在牛皮纸上写。写完点底部工具栏的「手写转公式」(快捷键 `R`),会弹出本机模型给出的候选(见下文),确认或修改后就在原处写成公式;原来的笔迹可以选择保留。 -2. **键盘 + 纸上快捷键提示**:双击纸面或按 `/` 开始写,快捷键表会以铅笔字的样子淡淡浮现在纸上。 - - | 快捷键 | 插入 | 快捷键 | 插入 | - | --- | --- | --- | --- | - | `Ctrl/⌘ + /` | 分数 `(□)/(□)` | `Alt/⌥ + 2` | `^2` | - | `Alt + 6` | `^(□)` | `Alt + R` | `sqrt(□)` | - | `Alt + A` | `\|□\|` | `Alt + P` / `T` | `pi` / `theta` | - | `Alt + O` | `°` | `Alt + S` / `C` / `L` / `E` | sin / cos / ln / e^ | - | `Alt + V` / `W` | `**□** = (□, □, □)` / `**□** = (□, □)` 向量 | `Alt + H` | `, 过(□, □)` | - | `Alt + B` | 粗体向量名 `**□**` | `Alt + Q` | 点 `□(□, □)` | - | `Alt + D` | `△ABC: …` 模板 | `Alt + K` / `J` / `G` | ⊥ / ∥ / ∠ | - | `Tab` | 跳到下一个 □ | `Shift + Enter` | 换行写下一个条件 | - -3. **浮动数学键盘**:在桌面端,正在写公式时右侧会出现;在手机上它是书写条里的一排大按键。点按键不会让输入框失去焦点。 - -纸面上的快捷键: - -| 键 | 作用 | 键 | 作用 | -| --- | --- | --- | --- | -| `P` | 笔 | `/` 或双击纸面 | 在纸上写公式 | -| `M` | 荧光笔 | `T` | Σ 工具:点哪儿写哪儿 | -| `E` | 橡皮 | `V`(或 `H`) | 选择:拖动公式 / 平移纸面 | -| `R` | 手写转公式 | `G` | 打开共享坐标系 | -| `Enter` | 打开选中式子所在的坐标系 | `Delete` | 擦掉选中的式子 | -| `Ctrl+Z` | 撤销 | `Ctrl+Shift+Z` / `Ctrl+Y` | 重做 | - -按住空格或鼠标中键拖动可以平移,`Ctrl + 滚轮` 或双指捏合可以缩放。 - -**笔的样式**:墨水(带压感,基于 perfect-freehand)、虚线、点线、铅笔(带纸纹颗粒)、和纸胶带、荧光马克笔。可以调粗细和颜色。 - -### 多设备 - -- **界面**:所有设备都是同一套极简布局,类似 Freeform: - - 底部中间一条浮动工具胶囊:笔、荧光笔、橡皮、选择、Σ、手写转公式。 - - 右下角是「坐标系」按钮。 - - 右上角只有撤销、重做和「…」菜单(缩放、示例、设置、清空)。 - - 纸面上不放任何面板。 - - **手机**:按钮更大;写公式时书写条停在键盘上方,带一排数学按键。坐标系先是右下角的图,详情再打开全屏。 - - **平板 / 桌面**:公式在点击的位置原地书写。坐标系先是右下角的图;详情之后左边是纸、右边是图和推导。 -- **输入方式**:Pointer Events 统一处理鼠标、触控和手写笔。检测到手写笔之后会自动开启防误触(手掌触碰不画线,单指触碰改为平移);设置里也可以强制开启「仅手写笔书写」。 -- **手写笔 / 数位板**:这类应用最理想的输入设备是 Apple Pencil、Surface Pen 或 Wacom 数位板。压感已经接入笔画粗细。写完稍停会在本机认成式子。 - ---- - -## 推导引擎(核心功能) - -“给几个已知量,尽量多推出别的量”这件事,完全在 Rust/WASM 里完成: - -- **解三角形**:覆盖 SSS、SAS、SSA(含两解 / 无解的判断)、AAS/ASA、HL(斜边直角边)、给面积的情况,以及**中线 `m_a`、高 `h_a` 作为已知量**(反推出边)和**两角 + 面积**(由外接圆半径 R 定大小)。推出边、角、面积、周长、R、r、三条高、中线、**三条中位线、三条角平分线**,直角三角形还会给出**斜边上的高与两条射影(射影定理)**;给多了的条件会做一致性校验,矛盾时明确报错。每一条结果都附上所用的定理。 -- **圆与弦**:相交弦定理(圆内两弦交于 P:由三条线段长推出第四条)、割线定理(圆外两点割线)、切割线定理(PT² = PA·PB),每个配置都会连着推出弦长、弦心距、圆心角、圆周角、弓形高和 |OP|,并画出恢复出来的弦图。**圆和纸上的点会交叉推断**:不管圆来自 `⊙O: r = 5` 还是 `x^2 + y^2 = 25`,每个点都会得到到圆心的距离、点的幂、在圆上 / 内 / 外,圆外点的切线长,圆上点之间的弦、弦心距、圆心角与圆周角(同弧上的圆周角是圆心角的一半),两条弦相交处自动验证相交弦 / 割线定理。 -- **平行线**:两平行线被第三条线所截时,推出同位角、内错角(相等)与同旁内角(互补)的具体角度,以及截线夹在两平行线间的线段长。**三条平行线截两条截线**时给出平行线分线段成比例定理(两边对应线段的比相等);**两条平行线 + 两条相交截线**时给出相似三角形与相似比。 -- **坐标点**:两点之间的距离、中点、斜率。三点组成的三角形会给出各心和面积。四点会判定平行四边形、矩形、菱形、正方形、梯形。3D 中三点确定一个平面,四点组成四面体并计算体积。 -- **未知参数**:多起点 Levenberg–Marquardt 求解非线性方程组,条件可以是 ⊥、∥、|AB|=、∠=、过点、`f(a)=b`、`f'(a)=b`,也可以是任意方程。多组解时可以在面板里切换。 -- **直线与圆锥曲线**:直线的斜率、截距、倾斜角、两直线的交点与夹角、点到直线的距离。二次曲线由特征值分类为圆、椭圆、双曲线或抛物线,并给出中心、半轴、焦点、离心率、渐近线、准线。 -- **一元方程**:一次方程直接移项求解。二次方程给出一般式、Δ、求根公式(区分两根、重根、共轭复根)、韦达定理、对应抛物线的顶点和对称轴。其他方程用 Rust 数值求根,并标出重根。 -- **坐标系选择与初始视野**: - - 维度:有 z、三维点或三维向量时用 3D,否则用 2D。 - - 2D 几何图形和圆锥曲线:按 1:1 比例框住所有图形;有界的隐式曲线用零点集的包围盒来定范围。 - - 2D 函数:以零点、极值、交点、截距为中心取 x 范围,再按这个范围内的曲线取值定 y 范围,并限制 y 的跨度,避免 x³、eˣ 这类曲线被压扁。周期函数固定显示约两个周期。 - - 3D 曲面:z 轴范围跟随曲面的取值范围,例如 z = x² + y² 不会被立方体顶面截掉。 - - 拖动或缩放后,曲线会在新的范围里由 Rust 重新采样。 -- **函数**:定义域(扫描断点和渐近线)、零点(变号二分 + 切点检测)、极值(黄金分割 + 渐近线剔除)、两两之间的交点。 -- **向量与平面**:点积、叉积、夹角、投影(数量)、**投影向量与垂直分量**(a = 投影 + 垂直)、混合积。向量还可以用语句定义:`𝐜 = 𝐚 + 𝐛`、`𝐩 = 投影(𝐚, 𝐛)`,定义出的向量参与同一套推导。平面的法向量、两平面夹角、交线、三平面交点、点到平面的距离。 -- **精确形式**:数值结果会被尽量还原成 √、分数、π 的形式,例如 \(\sqrt{39}\)、\(\frac{35\sqrt3}{4}\)、\(\frac{\pi}{3}\)。 - -坐标系面板右侧依次列出三部分:「已知 N 条 → 推出 M 项」、每一项的推导依据,以及一个可以在图上标出该项的眼睛开关。纸上同一维度的所有式子都在同一个坐标系里,它们之间的交点会自动求出(`src/math/page.ts` 负责把纸上的式子分到平面 / 空间两个共享坐标系)。 - ---- - -## Rust / WASM:JS ↔ Rust 边界 +| `npm run dev` | 本机开发服务器,含 MCP 桥和 AI 转发。 / Local dev server, including the MCP bridge and the AI forwarder. | +| `npm run build` | 类型检查并构建 `dist/`。 / Typecheck and build `dist/`. | +| `npm run lint` | oxlint,有警告也失败。 / oxlint. Warnings fail the command. | +| `npm test:pack` 或 `npm run test:i18n` | 打包元数据,以及中英词条、语言记忆。 / Packaging metadata, plus Chinese/English keys and saved language. | +| `npm run smoke` | 用真实 WASM 检查数值、对数和图上有没有点。 / Real WASM: numbers, logarithms, and whether a curve has points. | +| `npm run smoke:ocr` | 手写预处理和假的编码器/解码器,不加载 ONNX。 / Ink preprocess and a fake encoder/decoder. No ONNX. | +| `npm run ocr:live` | 本机用仓库里的 CoMER INT8 读几笔样例。 / Run the bundled CoMER INT8 on a few sample strokes. | +| `npm run smoke:ai` | 本地假接口上的连接测试、工具和 MCP。 / Connection test, tools, and MCP against a local fake endpoint. | +| `npm run test:rust` | `crates/mathcore` 的 Rust 测试。 / Rust tests for `crates/mathcore`. | +| `npm run e2e` / `npm run e2e:sheet` | 需要先开着 dev。浏览器按 `CHROME_PATH`、Playwright 缓存、PATH,再按常见安装路径查找。 / Need a running dev server. The browser is `CHROME_PATH`, then the Playwright cache, then PATH, then common install paths. | +| `npm run build:wasm` | 把 Rust 重新编成 `src/wasm/pkg`。 / Rebuild Rust into `src/wasm/pkg`. | -``` -crates/mathcore/ Rust crate(cdylib + rlib) - src/expr.rs 表达式分词、解析(隐式乘法、字母拆分)、编译成槽位求值、LaTeX - src/numeric.rs 采样、求根、极值、断点、线性/非线性方程组、系数识别 - src/geometry.rs 三角形 / 点 / 向量 / 直线 / 平面 / 圆锥曲线推导,统一输出 Fact - src/circle.rs 相交弦 / 割线 / 切线定理求解,圆 × 点交叉推断 - src/lib.rs #[wasm_bindgen] 导出 -src/wasm/pkg/ wasm-pack 产物(已提交,约 430 KB) -src/wasm/engine.ts 唯一的 JS 入口:加载、类型化、计时统计 -src/math/statements.ts TS:把纸上每个式子的文本解析成语句(点、曲线、约束、三角形…) -src/math/page.ts TS:整张纸 → 平面 / 空间两个共享坐标系 -src/math/scene.ts TS:编排——决定调用哪些 Rust 函数、组织推导结果 -``` +## 纸面、公式和坐标系 -**边界约定**: +## The sheet, formulas, and coordinates -- 跨边界只传字符串和数字。表达式用 ASCII 源码(如 `"x^2 - 2"`)。变量环境用 JSON 对象字符串(如 `{"k": 2}`)。 -- 结构化结果以 JSON 字符串返回。 -- 数值采样直接返回 `Float64Array`,不经过 JSON。 -- TS 只经由 `src/wasm/engine.ts` 调用,每次调用都会计时,设置面板和推导面板底部会显示调用次数与耗时。 +打开是一张空白牛皮纸。应用名只出现在窗口标题和「… / More」菜单里。 -| 导出 | 输入 | 输出 | -| --- | --- | --- | -| `latex(src)` / `inspect(src)` | 表达式 | LaTeX / `{ok, error, vars, source}` | -| `eval(src, env)` | 表达式、环境 | `f64` | -| `sample_curve(src, var, env, a, b, n)` | | `Float64Array`(n 个 y 值,无定义为 NaN) | -| `sample_grid(src, x, y, env, x0, x1, y0, y1, nx, ny)` | | `Float64Array`,行优先 | -| `sample_volume(src, env, lo, hi, n)` | | `Float64Array`,索引为 `(k*n+j)*n+i` | -| `roots` / `domain_gaps` / `intersect_curves` / `intersect_curve_implicit` | 表达式与区间 | `Float64Array` | -| `extrema(src, var, env, a, b)` | | JSON `[{x, y, kind}]` | -| `solve_system(eqs_json, unknowns_json, env)` | 残差表达式数组、未知数名 | JSON `{unknowns, solutions, exact, starts, error}` | -| `linear_coeffs` / `conic_analyze` | 方程 | 系数 / 圆锥曲线推导 | -| `triangle_solve(known_json)` | `{a,b,c,A,B,C,S}`(角用弧度,缺省即未知) | JSON `{cases: [{facts, vertices, note}], error}` | -| `circle_solve(known_json)` | `{r, cross: {p, chords, lengths, pos}}` | JSON `{cases: [{facts, note, center, p, lines}], error}` | -| `circles_analyze` / `circle_of` | 圆与点 JSON / 曲线方程 | JSON `Fact[]` / `{cx, cy, r}` | -| `derived_points_analyze` | 中点 / 分点定义 JSON + 点 JSON | JSON `Fact[]`(坐标、比值、中位线) | -| `points_analyze` / `vectors_analyze` / `lines_analyze` / `planes_analyze` | JSON | JSON `Fact[]` | -| `nice_number(v)` | `f64` | 精确形式的 LaTeX | - -所有推导结果都是统一的 `Fact`: - -```ts -{ group, label, value, num, how, why, shape } -``` +The app opens on a blank kraft sheet. The name MathScrap appears in the window title and in the More menu. -其中 `shape` 是可选的几何形状(点、圆、线段、3D 直线、3D 箭头),图上的高亮就靠它渲染。 +- 笔、荧光笔、橡皮直接画在纸上。双击纸面、按 `/`,或点 Σ 再点纸面,就在该处写式子。Enter 写上,Esc 放弃。 +- Draw with the pen, marker, or eraser. Double-click the paper, press `/`, or choose Σ and click, to write a formula there. Enter commits it. Esc discards it. +- 选择工具(V)拖动式子。手指在检测到手写笔之后负责平移和缩放。 +- The select tool (V) drags a formula. After a stylus is detected, a finger pans and zooms. +- 右下角「坐标系 / Coordinates」把能画的式子画进同一套轴,图是半透明的小窗。点「详情 / Details」才展开式子列表和推导。 +- Coordinates draws the plottable formulas on one set of axes, in a small translucent window. Details is what expands the formula list and the derivations. +- 同名未知数一起解。只写条件的行归到它前面的式子。平面和空间可以切换。缩放过的视野不会被新式子重置。 +- Unknowns that share a name are solved together. A condition-only line belongs with the formula before it. Plane and space can be switched. A zoomed view is not reset by a new formula. +- 「…」里可以把示例写到纸上,也可以导出或导入一页 JSON(`mathscrap.sheet`)。文件里只有笔迹、式子和字条的字符串与数字,导入不会执行代码,也不会改写式子的文字。 +- More can write the examples onto the sheet, and can export or import one JSON sheet (`mathscrap.sheet`). The file holds only strings and numbers for ink, formulas, and notes. Import does not execute the file and does not rewrite formula text. -**重新编译 WASM**: +纸面保存在本机 `localStorage` 键 `mathscrap.doc.v2`。第一次打开是空白纸。 -```bash -rustup target add wasm32-unknown-unknown -cargo install wasm-pack -npm run build:wasm # 输出到 src/wasm/pkg 并删除 wasm-pack 生成的 .gitignore -npm run test:rust -npm run smoke -``` +The sheet is stored locally under the `localStorage` key `mathscrap.doc.v2`. The first open is a blank sheet. ---- +## 语言 -## AI API +## Language -设置 → AI 接口可以填写服务类型、Base URL、模型名和 API Key。配置只写在这台浏览器的 localStorage(`mathscrap.ai.v1`)。密钥不会打进日志,也不会交给 MCP 进程。 +浏览器语言以 `zh` 开头时,界面初始是简体中文,否则是 English。右上角「… / More」里有一组紧凑的「中文 / EN」,选择写在 `mathscrap.lang`,下次打开仍用这个选择。空白纸上没有语言卡片。 -| 服务类型 | 怎么发 | Base URL 示例 | -| --- | --- | --- | -| OpenAI 兼容接口 | `POST {base}/chat/completions`,测试时先 `GET {base}/models` | `https://api.openai.com/v1` | -| Anthropic | `POST {base}/v1/messages`(默认主机 `https://api.anthropic.com`) | `https://api.anthropic.com` | -| 本地模型(Ollama 等) | 同样走 OpenAI 兼容路径。只写了主机时会自动补上 `/v1` | `http://127.0.0.1:11434/v1` | +If the browser language starts with `zh`, the interface starts in Simplified Chinese. Otherwise it starts in English. More contains a compact 中文 / EN control. The choice is stored in `mathscrap.lang` and is used again next time. The blank sheet has no language card. -「测试连接」发一条很短的请求(模型列表,没有这个路径时再发 `max_tokens: 1` 的对话)。Base URL 和模型名还空着时按钮是禁用的,点了也会立刻提示,不会转圈。Anthropic 还需要 API Key;本地模型的 Key 可以留空。 +切换语言会改按钮、菜单、对话框、占位符、提示、状态、面向人的错误,以及详情里的标题、分组和已收录的几何理由。不会翻译或改写公式、点名、已经写在纸上的内容、导入的纸,也不会改 MCP 工具名(`page_read`、`solve_analyze` 等)。计算引擎和 MCP 返回的原文仍是中文;只有屏幕上的显示会换成英文。个别较长的 Rust 句子还没有收录,英文模式下会仍以中文出现,详情底部有一句说明。 -右上角 **…** 菜单里的「帮我推一步」是纸面旁边的对话条(不是写在牛皮纸上的字)。它把当前公式和你的一句请求发给已配置的模型,并优先用下面同一套工具:写到纸上用 `page_add_formula`,推导用 `solve_analyze`。模型若不会函数调用,只要返回 `{"add":["…"],"analyze":true,"note":"…"}` 也会照着写到纸上。向量会改写成粗体加等号(`**b** = (1, 2)`)。 +Switching language changes buttons, menus, dialogs, placeholders, hints, status text, user-facing errors, and the titles, groups, and listed geometry reasons in the details. It does not translate or rewrite formulas, point names, anything already on the sheet, or an imported sheet, and it does not change MCP tool names (`page_read`, `solve_analyze`, and the rest). The compute engine and MCP still return the original Chinese wording. Only the screen shows English. A few longer Rust sentences are not in the glossary and still appear in Chinese in English mode. The details panel says so at the bottom. -开发服务器(`npm run dev` / `npm run preview`)会把浏览器的模型请求经本机 `POST /__mathscrap/ai/forward` 转发出去,这样页面不会被浏览器的 CORS 拦住。静态打开 `dist/` 时没有这个转发,浏览器会直接请求你填的地址,对方必须允许跨域。 +## 手写 -## MCP +## Handwriting -MCP 和上面的 AI API 是两条路。AI API 是设置里填写的 OpenAI 兼容、Anthropic 或 Ollama 对话接口,密钥只留在这台浏览器里,「帮我推一步」用它聊天。MCP 不保存密钥,也不去调用那个聊天接口。它把**同一份工具**交给本机上的外部程序:读这张纸、写式子、做推导、打开坐标系、读笔迹。推导仍由页面里的 Rust/WASM 完成;MCP 进程只负责转发。 +默认识别是开的:笔迹停大约一秒,先判断像不像算式。普通句子留在纸上,不送进公式模型。像算式才走本机 CoMER。置信度够就写成公式并更新角落的图,纸上没有确认条。按 R 会对最近的笔迹再认一次。设置里可以关掉自动识别。 -工具定义在 `src/ai/specs.ts`,处理函数在页面里(`src/ai/tools.ts`),应用加载后挂到 `window.mathscrap.tools`。可以在控制台试: +Recognition is on by default. About a second after the ink settles, a text gate decides whether it looks like a formula. Ordinary writing stays on the paper and is not sent to the formula model. Formula-like ink goes to the local CoMER model. A confident reading is written as a formula and the corner plot updates. There is no confirmation strip. R recognizes the latest ink again. Automatic recognition can be turned off in Settings. -```js -mathscrap.tools.specs -mathscrap.tools.call('page_read') -mathscrap.tools.call('solve_analyze', { texts: ['△ABC: a=3, b=4, C=90°'] }) -mathscrap.tools.call('page_add_formula', { text: 'y = x^2 - 2' }) -mathscrap.tools.call('plot_set_view', { x: [-2, 4], y: [-2, 4] }) -mathscrap.tools.call('ink_read', {}) -``` +模型是 CoMER(ECCV 2022)的 INT8 ONNX,权重由 [ink-on](https://github.com/kimseungdae/ink-on) 以 Apache-2.0 分发。它能读一些清楚的短式,不能当作任意手写的完整识别器:认不出就保持笔迹,不会用 `y = x` 充数。单独的 `sigma` 是符号,求和与积分留在纸上,不因此打开坐标系。 -| 工具 | 状态 | 说明 | -| --- | --- | --- | -| `page_read` | 本地可用 | 读取这张纸:每个式子的原文、位置,以及笔迹数量 | -| `page_add_formula` / `page_update_formula` | 本地可用 | 在纸上写一个式子 / 改写某个式子 | -| `solve_analyze` | 本地可用 | 对一组条件做推导,返回结构化结果(由 Rust 计算) | -| `plot_open` | 本地可用 | 打开整张纸共享的坐标系(可传 id 决定先看平面还是空间) | -| `plot_set_view` | 本地可用 | 修改坐标范围、3D 相机、叠加或拿掉哪些式子 | -| `ink_read` | 本地可用 | 读取手写笔迹的点序列(`pts` 为 x, y, pressure) | - -外部 agent 用 stdio MCP 服务器 `mcp/server.ts`(`@modelcontextprotocol/sdk`)。它不计算数学:`tools/call` 经 HTTP 交给正在运行的 Vite 页,页面轮询 `GET /__mathscrap/bridge/wait` 后在本地执行。 - -1. 终端里 `npm run dev`,浏览器打开 `http://127.0.0.1:47321` 并保持这个标签页。 -2. 把下面的片段放进 Cursor 的 `~/.cursor/mcp.json`(或项目里的 `.cursor/mcp.json`),Claude Desktop 则放进 `claude_desktop_config.json`。把路径换成这台机器上的仓库根目录。 - -```json -{ - "mcpServers": { - "mathscrap": { - "command": "node", - "args": ["--import", "tsx", "mcp/server.ts"], - "cwd": "/ABSOLUTE/PATH/TO/mathscrap", - "env": { - "MATHSCRAP_BRIDGE": "http://127.0.0.1:47321" - } - } - } -} -``` +The model is CoMER (ECCV 2022) INT8 ONNX. The weights are distributed by [ink-on](https://github.com/kimseungdae/ink-on) under Apache-2.0. It can read some clear short formulas. It is not a complete recognizer for arbitrary handwriting: a miss leaves the ink, and it does not substitute `y = x`. A lone `sigma` is the symbol. Sums and integrals stay on the paper and do not open the coordinate view by themselves. -`MATHSCRAP_BRIDGE` 可以不写,默认就是 `http://127.0.0.1:47321`。不要用 `npm run mcp` 当作上面的 `command`:npm 会往 stdout 打一行脚本名,stdio 协议会坏。`npm run mcp` 只适合在终端里看进程是否能起来。 +| 文件 File | 内容 / Contents | +| --- | --- | +| `public/models/comer/encoder_int8.onnx` | 编码器,约 3.4 MB。 / Encoder, about 3.4 MB. | +| `public/models/comer/decoder_int8.onnx` | 解码器,约 4.0 MB。 / Decoder, about 4.0 MB. | +| `public/models/comer/vocab.json` | 词表。 / Vocabulary. | +| `dist/ort/ort-wasm-simd-threaded.wasm` | 构建时从 onnxruntime-web 复制。运行时不再访问 node_modules 或公共 CDN。 / Copied from onnxruntime-web at build time. Runtime does not read node_modules or a public CDN. | -页面加载大约一秒后才连上桥,这样不会占住浏览器的空闲网络。页面没开、或刚打开还没连上时,`tools/list` 仍然返回这 7 个工具;`tools/call` 会立刻返回「页面还没连上」,不会一直等。 +## AI 和 MCP -### 安全 +## AI and MCP -- API Key 只在浏览器 localStorage。测试连接和「帮我推一步」把它放进发往**你填写的 Base URL** 的请求头。代码路径里不打印密钥、Authorization 或 `x-api-key`。 -- MCP 进程看不到密钥。它只把工具名和参数发给 `127.0.0.1` 上的开发服务器。 -- 开发服务器上的转发只允许 GET/POST,路径必须以 `/models`、`/chat/completions` 或 `/messages` 结尾,并拒绝链路本地地址和云元数据主机。 -- 桥接没有单独的口令:谁能访问这个开发端口,谁就能让开着的页面改公式。`npm run dev` 只在本机或你信任的局域网使用,不要把它暴露到公网。 -- 推导仍由 Rust/WASM 完成。模型只负责把题写成本应用的语法,并用自然语言说明下一步。 +这两条路是分开的。 ---- +These are two separate paths. -## 手写识别(默认开启) +设置里的 AI 接口是你自己的 OpenAI 兼容接口、Anthropic 或本机 Ollama。地址、模型名和密钥只在这台浏览器的 `mathscrap.ai.v1`。请求只发往你填的地址。密钥不进日志,也不交给 MCP。Base URL 或模型名还空着时,「测试连接」不会发请求。 -写完一笔稍停(约 1 秒),先看这团笔迹像不像算式(等号、分式、上标、字母上的箭头、坐标点)。像一句普通字就留在纸上,不会送进公式模型,也不会写成非法式子。像算式才走 CoMER:置信度够就写上纸并更新右下角的图。纸上不出现确认条。单独一个字母不是向量也不是点;字母上方有一横箭头时记成 `\vec{a}`。坐标写成 `A(1, 2)` 这种点。认不出就保持笔迹,不会拿 `y = x` 充数,也不会编出 `a = (1, 2, 2)`。设置里可以关掉自动识别(`手写自动识别`,默认开);按 `R` 会对最近的笔迹再认一次。 +The AI endpoint in Settings is your own OpenAI-compatible endpoint, Anthropic, or a local Ollama. The address, model name, and key stay in this browser under `mathscrap.ai.v1`. Requests go only to the address you entered. The key is not logged and is not given to MCP. Test connection sends nothing while the base URL or model name is still empty. -候选来自 **CoMER**(ECCV 2022)的 INT8 ONNX。权重由 [ink-on](https://github.com/kimseungdae/ink-on) 以 Apache-2.0 分发(Copyright 2025 kimseungdae)。路径是:笔迹点 → 纯 TypeScript 预处理 → 编码器 / 解码器 → LaTeX。核心在 `src/ocr/comer/`,不引用 React 或 DOM。 +「帮我推一步 / Suggest a next step」把纸上的公式和你的一句请求发给那个模型。数值和几何仍由页面里的 Rust 工具计算。模型若不会函数调用,可以只返回 `{"add":["…"],"analyze":true,"note":"…"}`。 -```ts -recognizeStrokes(strokes, encoder, decoder, vocab) → { text, latex, confidence }[] -``` +Suggest a next step sends the formulas on the sheet and one request to that model. Numbers and geometry are still computed by the Rust tools in the page. If the model cannot call tools, it may return only `{"add":["…"],"analyze":true,"note":"…"}`. -`text` 是这张纸已经能解析的写法(`a^2 = b`、`(1)/(2)`、`sqrt(x)`)。模型若给出 `\vec{…}` 或 `\overrightarrow{…}`,会收成 `\vec{…}`。Web 演示把 `onnxruntime-web` 的 WASM 接在这个函数外面;确认条调用的就是它。 +MCP 不保存密钥,也不调用那个聊天接口。工具名是稳定的:`page_read`、`page_add_formula`、`page_update_formula`、`solve_analyze`、`plot_open`、`plot_set_view`、`ink_read`。页面里可以调用 `window.mathscrap.tools.call(name, args)`。 -### 模型文件 +MCP does not store the key and does not call that chat endpoint. The tool names are stable: `page_read`, `page_add_formula`, `page_update_formula`, `solve_analyze`, `plot_open`, `plot_set_view`, `ink_read`. The page can call `window.mathscrap.tools.call(name, args)`. -已经放在仓库里,开发服务器按静态资源提供,浏览器按普通缓存保存: +外部程序要同时开着 `npm run dev` 和本机页面。stdio 服务器是 `mcp/server.ts`,用 `node --import tsx mcp/server.ts` 启动,工作目录是仓库根。`MATHSCRAP_BRIDGE` 默认 `http://127.0.0.1:47321`。不要把 `npm run mcp` 当作 MCP 客户端的 command,npm 会往 stdout 打一行字,stdio 协议会坏。页面没连上时,`tools/call` 立刻返回错误,不会一直等。 -| 文件 | 大小 | -| --- | --- | -| `public/models/comer/encoder_int8.onnx` | 3.4 MB | -| `public/models/comer/decoder_int8.onnx` | 4.0 MB | -| `public/models/comer/vocab.json` | 词表,113 个 token | +An external program needs both `npm run dev` and the local page. The stdio server is `mcp/server.ts`, started with `node --import tsx mcp/server.ts` and the repository root as its working directory. `MATHSCRAP_BRIDGE` defaults to `http://127.0.0.1:47321`. Do not use `npm run mcp` as the MCP client's command: npm writes a line to stdout and breaks the stdio protocol. If the page is not connected, `tools/call` returns an error immediately. -出处和 Apache-2.0 说明在 `public/models/comer/NOTICE.md` 与同目录的 `LICENSE`。预处理和 ink-on 一致:大约每 3px 重采样,黑底白字,内容缩放到大约 128px 高,放在高 256、宽为 64 的倍数(128–1024)的左上角。 +开发服务器会把浏览器的模型请求经 `POST /__mathscrap/ai/forward` 转发,以免 CORS 拦住页面。转发只允许 GET/POST,路径要落到模型列表、对话补全或 Messages,并拒绝链路本地地址和云元数据主机。直接打开 `dist/` 时没有这个转发。桥没有单独的口令:谁能访问开发端口,谁就能让开着的页面改公式。不要把 `npm run dev` 暴露到公网。 -WASM 运行时不进 git。`npm install` 之后,开发服务器和 `vite preview` 从 `node_modules/onnxruntime-web/dist` 提供 `/ort/ort-wasm-simd-threaded.wasm`(约 12 MB)和对应的 `.mjs`。线程数设为 1,所以不需要跨域隔离。 +The dev server forwards the browser's model request through `POST /__mathscrap/ai/forward` so CORS does not block the page. The forwarder allows only GET and POST, only model-list, chat-completion, or Messages paths, and it refuses link-local and cloud-metadata hosts. Opening `dist/` by itself has no forwarder. The bridge has no separate password: anyone who can reach the dev port can change formulas on the open page. Do not expose `npm run dev` to the public internet. -### 以后接到原生 ONNX Runtime +## 安装包 -桌面或手机不走浏览器。加载同一对 `encoder_int8.onnx` / `decoder_int8.onnx` 和 `vocab.json`,用 ONNX Runtime(或把同一计算图转成 Core ML)建两个 session,再调用 `recognizeStrokes`: +## Packages -1. `preprocessStrokes` 得到 `float32` 图像 `[1,1,256,W]`(`pixel_values`)和 `uint8` 掩码 `[1,256,W]`(`pixel_mask`,0 是内容,1 是补边)。 -2. 编码器输出 `encoder_features`、`encoder_mask`。 -3. 解码器每步吃这两个张量加上 `input_ids`(int64,从 `` 开始),读最后一步的 `logits`。 -4. 波束宽度 2。`` 或连续重复 3 次就停。 +外壳是 Tauri 2。它把 `dist/` 放进系统 WebView。安装之后不连接 Vite,也不从公共 CDN 拉数学库或 OCR。标识是 `app.mathscrap`,版本 `0.1.0`。图标用纸面的牛皮色 `#c9a473`。 -`src/ocr/comer/decode.ts` 里的循环不绑定某一种 Runtime,session 只要实现 `run(feeds)`。 +The shell is Tauri 2. It puts `dist/` in the system WebView. After install it does not connect to Vite and does not fetch the math library or OCR from a public CDN. The identifier is `app.mathscrap`, version `0.1.0`. The icon uses the kraft color `#c9a473`. -### 怎么验证 +| 命令 Command | 在哪里跑 / Where it runs | 得到什么 / What you get | +| --- | --- | --- | +| `npm run pack:web` | 有 Node 即可。 / Any machine with Node. | `dist/`。这是页面,不是安装包。 / `dist/`. This is the page, not an installer. | +| `npm run pack:windows` | Windows 10 1803+ 或 Windows 11,已有 WebView2、VS C++ 生成工具、Rust 1.90、Node。 / That Windows setup. | `packaging/out/` 里的 NSIS 安装程序和 `MathScrap.exe`。默认不签名。 / An NSIS installer and `MathScrap.exe` in `packaging/out/`. Unsigned by default. | +| `npm run pack:android` | JDK 17、Android SDK、NDK、Rust 的 Android 目标。 / JDK 17, Android SDK, NDK, and the Rust Android targets. | debug 签名的 APK,可以侧载。不是 Play 商店包。 / A debug-signed APK that can be sideloaded. Not a Play Store package. | +| `npm run pack:ios` | 只有 macOS 加完整 Xcode。其它系统会直接退出。 / macOS with full Xcode only. Other systems exit immediately. | 模拟器是 `.app`。没有描述文件和证书时不会做出可安装的 IPA。 / The simulator product is an `.app`. Without a profile and certificate it does not produce an installable IPA. | -```bash -npm run smoke:ocr # 预处理尺寸、词表整理、脚本化的假模型;不加载 ONNX -npm run ocr:live # 本机 WASM 跑真正的 INT8,样例笔迹应读成 y = 2、y = x、a^2 = b、(1)/(2) -npm run dev # http://127.0.0.1:47321 ,同一局域网用 http://<本机IP>:47321 -``` +Windows 安装程序不附带 WebView2 下载器。可选签名只认环境变量 `WINDOWS_CERTIFICATE`(pfx 的 base64)和 `WINDOWS_CERTIFICATE_PASSWORD`。脚本不打印它们,仓库里也没有证书。 -在纸上写 `y = 2`、`y = x` 或 `a^2 = b`,确认条应给出对应的式子。模型文件缺失或 WASM 加载失败时,确认条是「没认出来」,不会退回以前的几何规则。`src/ocr/inkShape.ts` 只留作回归测试。 +The Windows installer does not bundle a WebView2 downloader. Optional signing reads only `WINDOWS_CERTIFICATE` (base64 pfx) and `WINDOWS_CERTIFICATE_PASSWORD`. The scripts do not print them, and the repository has no certificate. ---- +Android 调试包用 SDK 的 debug keystore 签名。正式签名要已有的 keystore:`ANDROID_KEY_ALIAS`、`ANDROID_KEY_PASSWORD`、`ANDROID_KEY_BASE64`。脚本不会生成 keystore。 -## 项目结构 +The Android debug package is signed with the SDK debug keystore. A release signature needs an existing keystore: `ANDROID_KEY_ALIAS`, `ANDROID_KEY_PASSWORD`, `ANDROID_KEY_BASE64`. The script does not generate a keystore. -``` -src/ - App.tsx 状态编排、全局快捷键、工具桥 - board/ 连续纸面(平移、缩放、捏合、笔迹、橡皮)、纸上的墨迹公式、撤销历史、示例 - input/ 原地书写框、快捷键片段、纸上提示、数学键盘 - plot/ 共享坐标系面板与角落按钮、Plotly 图形构建、推导结果列表 - math/ 语句解析与场景推导(调用 WASM) - wasm/ WASM 产物与类型化封装 - ocr/ 笔迹分组、确认条,以及 CoMER 预处理 / 解码(不依赖 DOM) - ai/ 模型客户端、本机配置、MCP 形状的工具定义 -mcp/ stdio MCP 服务器,以及 Vite 上的本机工具桥 / 请求转发 - components/ 顶栏、工具栏、设置、KaTeX;ui/ 是 shadcn 组件 -crates/mathcore/ Rust 数学核心 -scripts/smoke.ts Node 端到端冒烟测试(单式 + 整页联立) -scripts/e2e-plots.ts 浏览器:每个用例的图都真的画出来 -scripts/e2e-sheet.ts 浏览器:在纸上写多个式子 → 同一个坐标系 -``` +iOS 最低系统是 16.4。能装到别人手机上的 IPA 需要 Apple Developer 证书和描述文件:`IOS_CERTIFICATE`、`IOS_CERTIFICATE_PASSWORD`、`IOS_MOBILE_PROVISION`,或 `APPLE_API_ISSUER`、`APPLE_API_KEY`、`APPLE_API_KEY_PATH`。这里不注册开发者账号,不上传 TestFlight 或 App Store,也没有在 iPhone 上安装过。 + +The iOS minimum is 16.4. An IPA that installs on someone else's phone needs an Apple Developer certificate and profile: `IOS_CERTIFICATE`, `IOS_CERTIFICATE_PASSWORD`, `IOS_MOBILE_PROVISION`, or `APPLE_API_ISSUER`, `APPLE_API_KEY`, `APPLE_API_KEY_PATH`. This repository does not enroll a developer account, does not upload to TestFlight or the App Store, and has not been installed on an iPhone. + +`.github/workflows/package.yml` 里的 `test` 作业在 `ubuntu-latest` 上跑 `npm run lint`、`npm run test:rust`、`npm run smoke`、`npm run smoke:ocr`、`npm run smoke:ai`、`npm run test:pack`,然后拉起 dev 服务器,依次跑 `npm run e2e:sheet` 和 `npm run e2e`(逐条作图)。失败时上传 `e2e-shots`、`e2e-plots` 和 `dev-server.log`。这些步骤不需要密钥。推到 `cursor/mathscrap-packaging-*` 时,测试通过后同一 workflow 再构建网页、debug APK 和未签名的 Windows 安装程序。指向 `main` 的 pull request 只跑 `test`。`.github/workflows/package-ios.yml` 只在手动启动时使用 macOS runner。这些 workflow 不创建 GitHub Release。本文件不提供下载地址。 + +The `test` job in `.github/workflows/package.yml` runs on `ubuntu-latest`: `npm run lint`, `npm run test:rust`, `npm run smoke`, `npm run smoke:ocr`, `npm run smoke:ai`, `npm run test:pack`, then starts the dev server and runs `npm run e2e:sheet` and `npm run e2e` (one plot per formula). On failure it uploads `e2e-shots`, `e2e-plots`, and `dev-server.log`. Those steps need no secrets. A push to `cursor/mathscrap-packaging-*` builds the web bundle, a debug APK, and an unsigned Windows installer in the same workflow after that job passes. A pull request into `main` runs only `test`. `.github/workflows/package-ios.yml` uses a macOS runner only when started by hand. These workflows do not create a GitHub Release. This file does not publish a download URL. -纸面内容自动保存在 localStorage(`mathscrap.doc.v2`)里,第一次打开是空白纸。旧版本(`v1`)的卡片页不会自动迁移,原数据仍留在浏览器里。「清空这页」可以撤销。 +## 许可和作者 ---- +## License and authors -## License +Apache-2.0。见 [LICENSE](./LICENSE) 和 [NOTICE](./NOTICE)。版权人是 DNTOF。提交作者是 DNT_OF。 -Apache-2.0 — see [LICENSE](./LICENSE) and [NOTICE](./NOTICE). +Apache-2.0. See [LICENSE](./LICENSE) and [NOTICE](./NOTICE). The copyright holder is DNTOF. Commits are authored by DNT_OF. diff --git a/crates/mathcore/src/circle.rs b/crates/mathcore/src/circle.rs index 9843831..9e221fb 100644 --- a/crates/mathcore/src/circle.rs +++ b/crates/mathcore/src/circle.rs @@ -3,7 +3,8 @@ //! chord lengths, centre-to-chord distances, central angles, and a canonical picture of //! the whole configuration. Plus cross inference between circles and named points. -use crate::geometry::{deg, dist2, near, nice, Fact, NamedPoint, Shape}; +use crate::geometry::{deg, dist2, line_eq_latex, near, nice, Fact, NamedPoint, Shape}; +use crate::numeric::solve_linear_pub; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -473,6 +474,159 @@ pub fn circles_analyze(circles: &[NamedCircle], points: &[NamedPoint]) -> Vec sum { + "外离" + } else if ln + 1e-9 < diff { + "内含" + } else { + "相交" + }; + out.push(fact(g, &format!("\\odot {} ,\\ \\odot {}", a.name, b.name), format!("\\text{{{rel}}}"), None, "|O_1O_2| \\text{ 与 } r_1\\pm r_2", "两圆位置关系")); + if rho2 > 1e-9 { + let chord = 2.0 * rho2.sqrt(); + out.push(seg_fact(g, &format!("|\\text{{公共弦}}_{{{}{}}}|", a.name, b.name), chord, "2\\sqrt{r_1^2 - d^2}", "公共弦长(落在根轴上)")); + } + } + } + + // Radical center of every triple (pair axes already emitted above as 两圆根轴). + { + let n = circles.len().min(6); + // Radical center: intersection of two radical axes for each triple + for i in 0..n { + for j in i + 1..n { + for k in j + 1..n { + let (c0, c1, c2) = (&circles[i], &circles[j], &circles[k]); + if c0.r <= 1e-12 || c1.r <= 1e-12 || c2.r <= 1e-12 { + continue; + } + let ax = |p: &NamedCircle, q: &NamedCircle| -> Option<(f64, f64, f64)> { + let aa = 2.0 * (q.cx - p.cx); + let bb = 2.0 * (q.cy - p.cy); + let cc = (q.cx * q.cx + q.cy * q.cy - q.r * q.r) - (p.cx * p.cx + p.cy * p.cy - p.r * p.r); + if aa.abs() + bb.abs() < 1e-12 { + None + } else { + Some((aa, bb, cc)) + } + }; + let (Some((a1, b1, d1)), Some((a2, b2, d2))) = (ax(c0, c1), ax(c0, c2)) else { + continue; + }; + let Some(xy) = solve_linear_pub(vec![vec![a1, b1], vec![a2, b2]], vec![d1, d2]) else { + continue; + }; + if !xy[0].is_finite() || !xy[1].is_finite() { + continue; + } + let (x, y) = (xy[0], xy[1]); + let tag = format!("{}{}{}", c0.name, c1.name, c2.name); + // common power value + let pow = (x - c0.cx).powi(2) + (y - c0.cy).powi(2) - c0.r * c0.r; + out.push(fact( + "根心", + &format!("R_{{\\odot {}}}", tag), + format!("\\left({},\\ {}\\right)", nice(x), nice(y)), + None, + "\\pi_{12} \\cap \\pi_{13}", + "三圆根心:三根轴交于一点", + ).with(Shape::Point { x, y, label: format!("根心 {}", tag) })); + out.push(fact( + "根心", + &format!("\\text{{幂}}_{{{}}}", tag), + nice(pow), + Some(pow), + "|OR|^2 - r^2", + "根心对三圆有相同的幂", + )); + } + } + } + } + + // Descartes: k_4 = k_1+k_2+k_3 ± 2√(Σ k_i k_j) for three mutually tangent circles. + { + let n = circles.len().min(4); + for i in 0..n { + for j in i + 1..n { + for kidx in j + 1..n { + let (a, b, c) = (&circles[i], &circles[j], &circles[kidx]); + if a.r <= 1e-12 || b.r <= 1e-12 || c.r <= 1e-12 { + continue; + } + let dist = |p: &NamedCircle, q: &NamedCircle| dist2([p.cx, p.cy], [q.cx, q.cy]); + let is_tan = |p: &NamedCircle, q: &NamedCircle| { + let d0 = dist(p, q); + near(d0, (p.r + q.r).abs()) || near(d0, (p.r - q.r).abs()) + }; + if !(is_tan(a, b) && is_tan(a, c) && is_tan(b, c)) { + continue; + } + let (k1, k2, k3) = (1.0 / a.r, 1.0 / b.r, 1.0 / c.r); + let s = k1 * k2 + k1 * k3 + k2 * k3; + if s < -1e-12 { + continue; + } + let root = 2.0 * s.max(0.0).sqrt(); + let g = "笛卡尔圆定理"; + let tag = format!("{}{}{}", a.name, b.name, c.name); + for (sign, k4) in [("+", k1 + k2 + k3 + root), ("-", k1 + k2 + k3 - root)] { + if k4.abs() < 1e-12 { + continue; + } + let r4 = 1.0 / k4; + out.push(fact( + g, + &format!("r_{{\\text{{Descartes}}{}}}^{{{}}}", sign, tag), + nice(r4), + Some(r4), + "k_4 = k_1+k_2+k_3 \\pm 2\\sqrt{k_1k_2+k_1k_3+k_2k_3}", + if sign == "+" { + "笛卡尔圆定理(外切/内含解)" + } else { + "笛卡尔圆定理(另一相切解)" + }, + )); + } + } + } + } + } + out } @@ -597,4 +751,74 @@ mod tests { // C is on the circle assert!(out.iter().any(|x| x.label == "C \\in \\odot O" && x.value.contains("圆上"))); } + + #[test] + fn radical_axis_of_two_circles() { + // unit circles at (0,0) and (1,0): radical axis x = 1/2, common chord √3 + let circles = vec![ + NamedCircle { name: "A".into(), cx: 0.0, cy: 0.0, r: 1.0 }, + NamedCircle { name: "B".into(), cx: 1.0, cy: 0.0, r: 1.0 }, + ]; + let out = circles_analyze(&circles, &[]); + let axis = out.iter().find(|f| f.label == "l_{AB}").unwrap(); + assert!(axis.value.contains("\\frac{1}{2}") || axis.value.contains("0.5"), "axis = {}", axis.value); + let chord = out.iter().find(|f| f.label == "|\\text{公共弦}_{AB}|").unwrap(); + assert!((chord.num.unwrap() - 3f64.sqrt()).abs() < 1e-9, "chord = {}", chord.value); + assert!(out.iter().any(|f| f.value.contains("相交"))); + // separate equal circles: x = 3/2, no chord + let circles = vec![ + NamedCircle { name: "A".into(), cx: 0.0, cy: 0.0, r: 1.0 }, + NamedCircle { name: "B".into(), cx: 3.0, cy: 0.0, r: 1.0 }, + ]; + let out = circles_analyze(&circles, &[]); + let axis = out.iter().find(|f| f.label == "l_{AB}").unwrap(); + assert!(axis.value.contains("\\frac{3}{2}"), "axis = {}", axis.value); + assert!(out.iter().all(|f| f.label != "|\\text{公共弦}_{AB}|")); + assert!(out.iter().any(|f| f.value.contains("外离"))); + } + + #[test] + fn descartes_three_unit_circles() { + let sq3 = 3f64.sqrt(); + let circles = vec![ + NamedCircle { name: "A".into(), cx: 0.0, cy: 0.0, r: 1.0 }, + NamedCircle { name: "B".into(), cx: 2.0, cy: 0.0, r: 1.0 }, + NamedCircle { name: "C".into(), cx: 1.0, cy: sq3, r: 1.0 }, + ]; + let out = circles_analyze(&circles, &[]); + let ds: Vec<_> = out.iter().filter(|f| f.group == "笛卡尔圆定理").collect(); + assert_eq!(ds.len(), 2, "expected ± Descartes radii, got {:?}", ds.iter().map(|f| &f.label).collect::>()); + let nums: Vec = ds.iter().map(|f| f.num.unwrap()).collect(); + let expect = [1.0 / (3.0 + 2.0 * sq3), 1.0 / (3.0 - 2.0 * sq3)]; + for e in expect { + assert!(nums.iter().any(|n| (n - e).abs() < 1e-9), "missing {e}, have {nums:?}"); + } + } + + #[test] + fn radical_axis_and_center() { + // Unit circles at (0,0) and (2,0): radical axis x = 1. + // Third at (1, √3) r=1: radical center = circumcenter of centers = (1, √3/3). + let sq3 = 3f64.sqrt(); + let circles = vec![ + NamedCircle { name: "A".into(), cx: 0.0, cy: 0.0, r: 1.0 }, + NamedCircle { name: "B".into(), cx: 2.0, cy: 0.0, r: 1.0 }, + NamedCircle { name: "C".into(), cx: 1.0, cy: sq3, r: 1.0 }, + ]; + let out = circles_analyze(&circles, &[]); + assert!(out.iter().filter(|f| f.group == "两圆根轴" && f.label.starts_with("l_")).count() >= 3); + let axis_ab = out.iter().find(|f| f.label == "l_{AB}").expect("axis AB"); + assert!(axis_ab.value.contains("x = 1") || axis_ab.value == "x = 1", "AB axis = {}", axis_ab.value); + let rc = out.iter().find(|f| f.group == "根心" && f.label.starts_with("R_")).expect("radical center"); + match &rc.shape { + Some(Shape::Point { x, y, .. }) => { + assert!((x - 1.0).abs() < 1e-9, "x={x}"); + assert!((y - sq3 / 3.0).abs() < 1e-9, "y={y}"); + } + other => panic!("expected Point shape, got {other:?}"), + } + let pow = out.iter().find(|f| f.group == "根心" && f.label.contains("幂")).unwrap(); + assert!((pow.num.unwrap() - 1.0 / 3.0).abs() < 1e-9, "power = {}", pow.value); + } + } diff --git a/crates/mathcore/src/expr.rs b/crates/mathcore/src/expr.rs index a98797e..e1fca5e 100644 --- a/crates/mathcore/src/expr.rs +++ b/crates/mathcore/src/expr.rs @@ -56,8 +56,10 @@ const FUNCS: &[(&str, &str, usize)] = &[ ("csc", "csc", 1), ("exp", "exp", 1), ("abs", "abs", 1), + // `ln` is natural. Unary `log` and `lg` are common logs (base 10). + // A base is written `log_2(x)`, `log₂(x)`, `log2(x)`, or `log(2, x)`. ("log", "log", 1), - ("lg", "log", 1), + ("lg", "lg", 1), ("ln", "ln", 1), ("min", "min", 2), ("max", "max", 2), @@ -103,6 +105,8 @@ enum Tok { Num(f64), Ident(String), Func(&'static str, usize), + /// Base source sitting on a preceding `log`, as in `log_2` or `log_{a+1}`. + LogBase(String), Const(&'static str), Op(char), LParen, @@ -118,6 +122,111 @@ fn is_letter(c: char) -> bool { c.is_ascii_alphabetic() } +fn sub_digit(c: char) -> Option { + match c { + '₀'..='₉' => Some(char::from(b'0' + (c as u32 - '₀' as u32) as u8)), + 'ₐ' => Some('a'), + 'ₑ' => Some('e'), + 'ₕ' => Some('h'), + 'ₖ' => Some('k'), + 'ₙ' => Some('n'), + 'ₒ' => Some('o'), + 'ₓ' => Some('x'), + _ => None, + } +} + +/// `log_2`, `log_{a+1}`, or `log₂` after a `log` function token. `lg` and `ln` are not bases. +fn log_base_suffix(chars: &[char], i: &mut usize, last: Option<&Tok>) -> Option { + if !matches!(last, Some(Tok::Func("log", _))) || *i >= chars.len() { + return None; + } + if sub_digit(chars[*i]).is_some() { + let mut s = String::new(); + while *i < chars.len() { + match sub_digit(chars[*i]) { + Some(d) => { + s.push(d); + *i += 1; + } + None => break, + } + } + return Some(s); + } + if chars[*i] != '_' { + return None; + } + let mut j = *i + 1; + if j >= chars.len() { + return None; + } + if chars[j] == '(' { + let start = j; + let mut depth = 0i32; + while j < chars.len() { + if chars[j] == '(' { + depth += 1; + } else if chars[j] == ')' { + depth -= 1; + } + j += 1; + if depth == 0 { + break; + } + } + if depth != 0 { + return None; + } + let inner: String = chars[start..j].iter().collect(); + *i = j; + return Some(inner); + } + if chars[j] == '{' { + j += 1; + let start = j; + let mut depth = 1i32; + while j < chars.len() && depth > 0 { + if chars[j] == '{' { + depth += 1; + } else if chars[j] == '}' { + depth -= 1; + } + if depth > 0 { + j += 1; + } + } + if depth != 0 { + return None; + } + let inner: String = chars[start..j].iter().collect(); + let inner = inner.trim().to_string(); + if inner.is_empty() { + return None; + } + *i = j + 1; + return Some(inner); + } + let start = j; + if chars[j].is_ascii_digit() || chars[j] == '.' { + while j < chars.len() && (chars[j].is_ascii_digit() || chars[j] == '.') { + j += 1; + } + } else if chars[j].is_ascii_alphabetic() { + while j < chars.len() && chars[j].is_ascii_alphabetic() { + j += 1; + } + } else { + return None; + } + if j == start { + return None; + } + let s: String = chars[start..j].iter().collect(); + *i = j; + Some(s) +} + fn tokenize(src: &str) -> Result, String> { let chars: Vec = src.chars().collect(); let mut i = 0; @@ -145,8 +254,10 @@ fn tokenize(src: &str) -> Result, String> { } let run: String = chars[start..i].iter().collect(); split_letters(&run, &mut out); - // `a_1` / `x_{12}` subscripts glue onto the last single-letter identifier - if i < chars.len() && chars[i] == '_' { + if let Some(base) = log_base_suffix(&chars, &mut i, out.last()) { + out.push(Tok::LogBase(base)); + } else if i < chars.len() && chars[i] == '_' { + // `a_1` / `x_{12}` subscripts glue onto the last single-letter identifier if let Some(Tok::Ident(last)) = out.last_mut() { let mut j = i + 1; let braced = j < chars.len() && chars[j] == '{'; @@ -264,6 +375,77 @@ impl Parser { self.pos += 1; t } + fn log_call(&mut self) -> Result { + if let Some(Tok::LogBase(src)) = self.peek().cloned() { + self.pos += 1; + let base = parse(&src)?; + let arg = self.log_argument()?; + return Ok(Expr::Call("logb", vec![base, arg])); + } + if let Some(Tok::LParen) = self.peek() { + self.pos += 1; + let saved = self.abs_depth; + self.abs_depth = 0; + let mut args = vec![self.expr()?]; + while let Some(Tok::Comma) = self.peek() { + self.pos += 1; + args.push(self.expr()?); + } + self.abs_depth = saved; + self.expect_rparen()?; + return match args.len() { + 1 => Ok(Expr::Call("log", args)), + 2 => Ok(Expr::Call("logb", args)), + _ => Err("log 写成 log(真数) 或 log(底数, 真数)".into()), + }; + } + if self.glued_log_base() { + let base = match self.next() { + Some(Tok::Num(v)) => Expr::Num(v), + Some(Tok::Ident(n)) => Expr::Var(n), + Some(Tok::Const(c)) => Expr::Const(c), + _ => return Err("log 的底数没有写全".into()), + }; + let arg = self.power()?; + return Ok(Expr::Call("logb", vec![base, arg])); + } + let arg = self.power()?; + Ok(Expr::Call("log", vec![arg])) + } + + /// `log2(x)` / `log2x` / `loga(x)` carry a base. `log 2` and `log(x)` do not. + fn glued_log_base(&self) -> bool { + let simple = match self.peek() { + Some(Tok::Num(_)) | Some(Tok::Const(_)) => true, + Some(Tok::Ident(n)) => n.chars().count() == 1 && !n.contains('_'), + _ => false, + }; + if !simple { + return false; + } + matches!( + self.toks.get(self.pos + 1), + Some(Tok::LParen) | Some(Tok::Ident(_)) | Some(Tok::Func(..)) | Some(Tok::Const(_)) | Some(Tok::Num(_)) | Some(Tok::Sqrt) | Some(Tok::Bar) + ) + } + + fn log_argument(&mut self) -> Result { + if let Some(Tok::LParen) = self.peek() { + self.pos += 1; + let saved = self.abs_depth; + self.abs_depth = 0; + let arg = self.expr()?; + self.abs_depth = saved; + if let Some(Tok::Comma) = self.peek() { + return Err("底数已经写在下标里,括号里只放真数".into()); + } + self.expect_rparen()?; + Ok(arg) + } else { + self.power() + } + } + fn expect_rparen(&mut self) -> Result<(), String> { match self.next() { Some(Tok::RParen) => Ok(()), @@ -396,6 +578,7 @@ impl Parser { let e = self.power()?; Ok(Expr::Call("sqrt", vec![e])) } + Some(Tok::Func(name, _)) if name == "log" => self.log_call(), Some(Tok::Func(name, arity)) => { if let Some(Tok::LParen) = self.peek() { self.pos += 1; @@ -426,6 +609,7 @@ impl Parser { Some(Tok::Comma) => Err("多余的逗号".into()), Some(Tok::Deg) => Err("° 前面需要数字".into()), Some(Tok::Bang) => Err("! 前面需要数字".into()), + Some(Tok::LogBase(_)) => Err("对数的底数要写在 log 后面".into()), None => Err("表达式不完整".into()), } } @@ -505,6 +689,7 @@ impl Expr { format!("({}{}{})", a.to_source(), o, b.to_source()) } Expr::Juxt(a, b) => format!("({}*{})", a.to_source(), b.to_source()), + Expr::Call("logb", args) => format!("log_{{{}}}({})", args[0].to_source(), args[1].to_source()), Expr::Call(f, args) => { let inner: Vec = args.iter().map(|a| a.to_source()).collect(); format!("{}({})", f, inner.join(",")) @@ -604,7 +789,16 @@ impl Code { "exp" => a.exp(), "abs" => a.abs(), "ln" => a.ln(), - "log" => a.log10(), + "lg" | "log" => a.log10(), + "logb" => { + let base = a; + let arg = args[1].eval(s); + if !(arg > 0.0 && base > 0.0 && (base - 1.0).abs() > 1e-12) { + f64::NAN + } else { + arg.ln() / base.ln() + } + } "floor" => a.floor(), "ceil" => a.ceil(), "sign" => { @@ -738,7 +932,9 @@ pub fn latex(e: &Expr) -> String { "exp" => format!("e^{{{}}}", a0), "floor" => format!("\\lfloor {} \\rfloor", a0), "ceil" => format!("\\lceil {} \\rceil", a0), - "log" => format!("\\lg{}", wrap(a0)), + "lg" => format!("\\lg{}", wrap(a0)), + "log" => format!("\\log{}", wrap(a0)), + "logb" => format!("\\log_{{{}}}{}", latex(&args[0]), wrap(latex(&args[1]))), "min" | "max" => format!("\\{}\\left({}, {}\\right)", f, a0, latex(&args[1])), "asin" => format!("\\arcsin{}", wrap(a0)), "acos" => format!("\\arccos{}", wrap(a0)), @@ -871,4 +1067,49 @@ mod tests { assert_eq!(statement_latex("2sin(x)"), "2\\sin x"); assert_eq!(statement_latex("a = (1, 2, 3)"), "a = \\left(1,\\ 2,\\ 3\\right)"); } + + fn near(a: f64, b: f64) -> bool { + a.is_finite() && b.is_finite() && (a - b).abs() <= 1e-9 * (1.0 + b.abs()) + } + + #[test] + fn logarithms() { + assert!(near(ev("ln(e)", 0.0), 1.0)); + assert!(near(ev("ln(1)", 0.0), 0.0)); + assert!(near(ev("log(100)", 0.0), 2.0)); + assert!(near(ev("lg(100)", 0.0), 2.0)); + assert!(near(ev("log(1000)", 0.0), 3.0)); + assert!(near(ev("log_2(8)", 1.0), 3.0)); + assert!(near(ev("log_{2}(8)", 1.0), 3.0)); + assert!(near(ev("log2(8)", 1.0), 3.0)); + assert!(near(ev("log2x", 8.0), 3.0)); + assert!(near(ev("log₂(8)", 1.0), 3.0)); + assert!(near(ev("log(2, 8)", 1.0), 3.0)); + assert!(near(ev("log_{1/2}(1/8)", 1.0), 3.0)); + let e = parse("log_a(x)").unwrap(); + let c = compile(&e, &["x", "a"], &HashMap::new()).unwrap(); + assert!(near(c.eval(&[8.0, 2.0]), 3.0)); + assert!(near(ev("ln(x^2+1)", 0.0), 0.0)); + assert!(near(ev("log(x^2+1)", 0.0), 0.0)); + assert!(near(ev("ln(ln(e))", 0.0), 0.0) || near(ev("ln(ln(e^e))", 0.0), 1.0)); + assert!(near(ev("ln(x-2)", 3.0), 0.0)); + assert!(ev("ln(-1)", 0.0).is_nan()); + assert!(ev("log(0)", 0.0).is_nan() || ev("log(0)", 0.0).is_infinite()); + assert!(ev("log_1(5)", 0.0).is_nan()); + assert!(ev("log_0(5)", 0.0).is_nan()); + assert!(ev("log_(-2)(8)", 0.0).is_nan()); + assert!(ev("log(1, 5)", 0.0).is_nan()); + assert!(ev("log(-2, 8)", 0.0).is_nan()); + assert!(ev("ln(x)", -1.0).is_nan()); + assert!(near(ev("log(2)", 0.0), 2f64.log10())); + assert!(near(ev("lg2(3)", 1.0), 2f64.log10() * 3.0)); + let shown = latex(&parse("log_2(x)").unwrap()); + assert!(shown.contains("\\log_{2}"), "{shown}"); + let common = latex(&parse("log(x)").unwrap()); + assert!(common.contains("\\log"), "{common}"); + assert!(!common.contains("\\log_{"), "{common}"); + assert_eq!(parse("y").unwrap().to_source(), "y"); + let round = parse("log_2(x+1)").unwrap().to_source(); + assert!(near(ev(&round, 3.0), ev("log_2(x+1)", 3.0))); + } } diff --git a/crates/mathcore/src/geometry.rs b/crates/mathcore/src/geometry.rs index 511d22d..1014fbd 100644 --- a/crates/mathcore/src/geometry.rs +++ b/crates/mathcore/src/geometry.rs @@ -514,6 +514,9 @@ fn triangle_derived(s: [f64; 3], g: [f64; 3], v: [[f64; 2]; 3], names: [&str; 3] let rr = a / (2.0 * g[0].sin()); let r = area / p; out.push(fact("面积与周长", "S", nice(area), Some(area), "S = \\frac12 ab\\sin C", "面积公式")); + // Heron: same area from three sides — useful when the student only wrote SSS. + let heron = (p * (p - a) * (p - b) * (p - c)).max(0.0).sqrt(); + out.push(fact("面积与周长", "S_{\\text{海伦}}", nice(heron), Some(heron), "S = \\sqrt{p(p-a)(p-b)(p-c)}", "海伦公式")); out.push(fact("面积与周长", "C_{\\triangle}", nice(per), Some(per), "a + b + c", "周长")); let [na, nb, nc] = names; @@ -564,6 +567,21 @@ fn triangle_derived(s: [f64; 3], g: [f64; 3], v: [[f64; 2]; 3], names: [&str; 3] (y * v[(i + 1) % 3][1] + z * v[(i + 2) % 3][1]) / (y + z), ]; out.push(fact("角平分线", &format!("t_{}", SIDE[i]), nice(t), Some(t), &format!("t_{s} = \\frac{{2{u}{w}\\cos\\frac{{{T}}}{{2}}}}{{{u} + {w}}}", s = SIDE[i], u = SIDE[(i + 1) % 3], w = SIDE[(i + 2) % 3], T = ANG[i]), "角平分线长公式").with(Shape::Segment { x1: v[i][0], y1: v[i][1], x2: foot[0], y2: foot[1], label: format!("t_{}", SIDE[i]) })); + // 角平分线定理: foot F on the opposite side splits it as the adjacent sides. + let (u, w) = ((i + 1) % 3, (i + 2) % 3); + let d_u = dist2(v[u], foot); + let d_w = dist2(foot, v[w]); + if d_w > 1e-12 { + let ratio = d_u / d_w; + out.push(fact( + "角平分线", + &format!("\\frac{{|{nu}F_{{{s}}}|}}{{|F_{{{s}}}{nw}|}}", s = SIDE[i], nu = names[u], nw = names[w]), + nice(ratio), + Some(ratio), + &format!("\\frac{{{w}}}{{{u}}}", u = SIDE[u], w = SIDE[w]), + "角平分线定理:分对边成邻边比", + )); + } } out.push(fact("特殊点", "G", pt(centroid[0], centroid[1]), None, "G = \\frac{A + B + C}{3}", "重心(中线交点)").with(Shape::Point { x: centroid[0], y: centroid[1], label: "G 重心".into() })); @@ -571,694 +589,16928 @@ fn triangle_derived(s: [f64; 3], g: [f64; 3], v: [[f64; 2]; 3], names: [&str; 3] out.push(fact("特殊点", "I", pt(inc[0], inc[1]), None, "I = \\frac{aA + bB + cC}{a + b + c}", "内心(角平分线交点)").with(Shape::Point { x: inc[0], y: inc[1], label: "I 内心".into() })); out.push(fact("特殊点", "H", pt(orth[0], orth[1]), None, "\\vec{OH} = \\vec{OA} + \\vec{OB} + \\vec{OC}", "垂心(高线交点)").with(Shape::Point { x: orth[0], y: orth[1], label: "H 垂心".into() })); - let mut kinds: Vec = Vec::new(); - let maxg = g.iter().cloned().fold(0.0, f64::max); - if near(maxg, PI / 2.0) { - let i = g.iter().position(|x| near(*x, PI / 2.0)).unwrap(); - kinds.push(format!("直角三角形(∠{} = 90°)", [na, nb, nc][i])); - } else if maxg > PI / 2.0 { - kinds.push("钝角三角形".into()); - } else { - kinds.push("锐角三角形".into()); - } - if near(a, b) && near(b, c) { - kinds.push("等边".into()); - } else if near(a, b) || near(b, c) || near(a, c) { - kinds.push("等腰".into()); + // Excircle radii; Euler OI² = R(R − 2r); OH from the orthocenter / circumcenter. + for i in 0..3 { + let ri = area / (p - s[i]); + out.push(fact( + "外接圆与内切圆", + &format!("r_{}", SIDE[i]), + nice(ri), + Some(ri), + &format!("r_{s} = \\frac{{S}}{{p - {s}}}", s = SIDE[i]), + "旁切圆半径", + )); } - out.push(fact("形状判断", "\\text{类型}", format!("\\text{{{}}}", kinds.join("、")), None, "", "由三个角与三边比较")); - if near(maxg, PI / 2.0) { - out.push(fact("形状判断", "R", nice(rr), Some(rr), "R = \\frac{\\text{斜边}}{2}", "直角三角形外心在斜边中点")); - // altitude onto the hypotenuse and the two leg projections (射影定理). - // Right angle at vertex ri; the hypotenuse joins vertices u and w. Leg s[u] - // (= V_ri→V_w) projects onto the segment from V_w to the foot F, leg s[w] - // onto the segment from V_u to F. - let ri = g.iter().position(|x| near(*x, PI / 2.0)).unwrap(); - let (u, w) = ((ri + 1) % 3, (ri + 2) % 3); - let (lu, lw, hyp) = (s[u], s[w], s[ri]); - let t = lw * lw / (hyp * hyp); // |V_u F| = s[w]²/hyp - let foot = [v[u][0] + t * (v[w][0] - v[u][0]), v[u][1] + t * (v[w][1] - v[u][1])]; - let hc = lu * lw / hyp; - out.push(fact("高与中线", "\\text{斜边上的高}", nice(hc), Some(hc), &format!("\\frac{{{a}\\cdot {b}}}{{{c}}}", a = SIDE[u], b = SIDE[w], c = SIDE[ri]), "等面积:½·直角边₁·直角边₂ = ½·斜边·h").with(Shape::Segment { x1: v[ri][0], y1: v[ri][1], x2: foot[0], y2: foot[1], label: "斜边上的高".into() })); - out.push(fact("射影定理", &format!("\\text{{射影}}_{{{}}}", SIDE[u]), nice(lu * lu / hyp), Some(lu * lu / hyp), &format!("\\frac{{{u}^2}}{{{c}}}", u = SIDE[u], c = SIDE[ri]), "直角边² = 斜边 × 这条直角边的射影").with(Shape::Segment { x1: v[w][0], y1: v[w][1], x2: foot[0], y2: foot[1], label: format!("射影 {}", SIDE[u]) })); - out.push(fact("射影定理", &format!("\\text{{射影}}_{{{}}}", SIDE[w]), nice(lw * lw / hyp), Some(lw * lw / hyp), &format!("\\frac{{{w}^2}}{{{c}}}", w = SIDE[w], c = SIDE[ri]), "直角边² = 斜边 × 这条直角边的射影").with(Shape::Segment { x1: v[u][0], y1: v[u][1], x2: foot[0], y2: foot[1], label: format!("射影 {}", SIDE[w]) })); + let oi = dist2(circ, inc); + out.push(fact( + "特殊点", + "|OI|", + nice(oi), + Some(oi), + "|OI|^2 = R(R - 2r)", + "欧拉公式(外心与内心的距离)", + ).with(Shape::Segment { x1: circ[0], y1: circ[1], x2: inc[0], y2: inc[1], label: "OI".into() })); + let oh = dist2(circ, orth); + out.push(fact( + "特殊点", + "|OH|", + nice(oh), + Some(oh), + "|OH|^2 = 9R^2 - (a^2 + b^2 + c^2)", + "外心与垂心的距离", + ).with(Shape::Segment { x1: circ[0], y1: circ[1], x2: orth[0], y2: orth[1], label: "OH".into() })); + // Explicit Euler OH² identity (distinct from |OI|² = R(R−2r)). + { + let oh2 = oh * oh; + let oh2_form = 9.0 * rr * rr - (a * a + b * b + c * c); + out.push(fact( + "特殊点", + "|OH|^2", + nice(oh2), + Some(oh2), + "|OH|^2 = 9R^2 - (a^2 + b^2 + c^2)", + "欧拉:外心与垂心距离的平方公式", + )); + if (oh2 - oh2_form).abs() < 1e-6 * (1.0 + oh2.abs()) { + out.push(fact( + "特殊点", + "9R^2 - (a^2+b^2+c^2)", + nice(oh2_form), + Some(oh2_form), + "|OH|^2 = 9R^2 - (a^2 + b^2 + c^2)", + "与 |OH|^2 一致", + )); + } + let oi2_form = rr * (rr - 2.0 * r); + out.push(fact( + "特殊点", + "|OI|^2", + nice(oi * oi), + Some(oi * oi), + "|OI|^2 = R(R - 2r)", + "欧拉:外心与内心(勿与 |OH|^2 混淆)", + )); + let _ = oi2_form; } - out -} + // Isogonal conjugates: O and H are isogonal conjugates of each other; I is self-conjugate. + out.push(fact( + "等角共轭", + "H \\leftrightarrow O", + "\\text{等角共轭}".into(), + None, + "\\text{反射角平分线}", + "垂心与外心互为等角共轭;内心是自身的等角共轭", + )); + out.push(fact( + "等角共轭", + "I \\leftrightarrow I", + "\\checkmark".into(), + None, + "", + "内心的等角共轭是自身", + )); + out.push(fact( + "等角共轭", + "G \\leftrightarrow K", + "\\text{等角共轭}".into(), + None, + "\\text{重心} \\leftrightarrow \\text{勒莫恩点}", + "重心与勒莫恩(对称中线)点互为等角共轭", + )); -// --------------------------------------------------------------------------- -// Named points (2D): distances, polygon properties - -#[derive(Deserialize)] -pub struct NamedPoint { - pub name: String, - pub p: Vec, -} + // Carnot: signed distances from O to the three sides sum to R + r. + { + let mut sum_d = 0.0; + for i in 0..3 { + let q1 = v[(i + 1) % 3]; + let q2 = v[(i + 2) % 3]; + let dx = q2[0] - q1[0]; + let dy = q2[1] - q1[1]; + let len = (dx * dx + dy * dy).sqrt(); + if len < 1e-12 { + continue; + } + // cross z-component for (q2-q1)×(P-q1) + let cross_o = dx * (circ[1] - q1[1]) - dy * (circ[0] - q1[0]); + let cross_v = dx * (v[i][1] - q1[1]) - dy * (v[i][0] - q1[0]); + let dist = cross_o.abs() / len; + let signed = if cross_o * cross_v >= 0.0 { dist } else { -dist }; + sum_d += signed; + out.push(fact( + "卡诺定理", + &format!("\\delta_{{{}}}", SIDE[i]), + nice(signed), + Some(signed), + "d(O,\\text{边})\\text{(有向)}", + "外心到边的有向距离(与对顶同侧为正)", + )); + } + out.push(fact( + "卡诺定理", + "\\delta_a+\\delta_b+\\delta_c", + nice(sum_d), + Some(sum_d), + "\\delta_a+\\delta_b+\\delta_c = R+r", + "卡诺定理:外心到三边有向距离之和等于 R+r", + )); + out.push(fact( + "卡诺定理", + "R+r", + nice(rr + r), + Some(rr + r), + "R + r", + "与有向距离之和对照", + )); + } -pub(crate) fn dist2(a: [f64; 2], b: [f64; 2]) -> f64 { - ((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2)).sqrt() -} + // Euler line: O, G, H collinear with OG:GH = 1:2 (and |OH| = 3|OG|). + // Note: |OI|^2 = R(R−2r) is the Euler distance formula for the incenter (already above); + // it is not |OH|^2. + { + let og = dist2(circ, centroid); + let gh = dist2(centroid, orth); + let cross = (centroid[0] - circ[0]) * (orth[1] - circ[1]) - (centroid[1] - circ[1]) * (orth[0] - circ[0]); + let scale = (1.0 + og) * (1.0 + oh); + if cross.abs() < 1e-6 * scale { + out.push(fact( + "欧拉线", + "O,\\ G,\\ H", + "\\text{共线}".into(), + None, + "\\vec{OH} = 3\\vec{OG}", + "欧拉线:外心、重心、垂心共线", + )); + if og > 1e-12 { + let ratio = gh / og; + out.push(fact( + "欧拉线", + "OG:GH", + format!("1:{}", nice(ratio)), + Some(ratio), + "OG:GH = 1:2", + "重心按 1:2 分 OH", + )); + out.push(fact( + "欧拉线", + "|OG|", + nice(og), + Some(og), + "|OH| = 3|OG|", + "欧拉线上的线段", + ).with(Shape::Segment { + x1: circ[0], + y1: circ[1], + x2: centroid[0], + y2: centroid[1], + label: "OG".into(), + })); + } + } + // Nine-point circle: center N = midpoint of OH, radius R/2. + let n9 = [(circ[0] + orth[0]) / 2.0, (circ[1] + orth[1]) / 2.0]; + let r9 = rr / 2.0; + out.push(fact( + "欧拉线", + "N", + pt(n9[0], n9[1]), + None, + "N = \\text{中点}(O, H) = X(5)", + "九点圆圆心(欧拉圆)", + ).with(Shape::Point { + x: n9[0], + y: n9[1], + label: "N 九点圆圆心".into(), + })); + out.push(fact( + "欧拉线", + "X_{5}", + pt(n9[0], n9[1]), + None, + "X(5) = N = \\text{midpoint}(O, H)", + "X(5):九点圆心", + ).with(Shape::Point { + x: n9[0], + y: n9[1], + label: "X5".into(), + })); + out.push(fact( + "欧拉线", + "R_{\\text{九点}}", + nice(r9), + Some(r9), + "R_N = R/2", + "九点圆半径", + ).with(Shape::Circle { + cx: n9[0], + cy: n9[1], + r: r9, + label: "九点圆".into(), + })); + // OH is always a diameter of the circle centred at N through O (and H); + // it coincides with the nine-point circle precisely when |OH| = R (right triangles). + let oh_len = dist2(circ, orth); + out.push(fact( + "欧拉线", + "|OH| = 2|ON|", + nice(oh_len), + Some(oh_len), + "|OH| = 2|ON| = 2|NH|", + "N 平分 OH,故 OH 是以 N 为圆心、半径 |OH|/2 的圆的直径", + )); + if near(oh_len, rr) { + out.push(fact( + "欧拉线", + "OH\\text{ 九点圆直径}", + "\\checkmark".into(), + None, + "|OH| = R = 2R_N", + "直角三角形:欧拉圆(九点圆)以 OH 为直径", + )); + } else { + out.push(fact( + "欧拉线", + "OH\\text{ 与九点圆}", + format!("|OH|/2 = {},\\ R_N = {}", nice(oh_len / 2.0), nice(r9)), + None, + "|OH|/2 \\ne R/2", + "一般三角形 OH 不是九点圆直径(仅当直角时重合)", + )); + } -fn line_eq_latex(a: f64, b: f64, c: f64) -> String { - // a x + b y = c, normalised for display - if near(b, 0.0) { - return format!("x = {}", nice_short(c / a)); + // Feuerbach: nine-point circle tangent to incircle (internally) and excircles (externally). + { + let ni = (r9 - r).abs(); + out.push(fact( + "费尔巴哈定理", + "|NI|", + nice(ni), + Some(ni), + "|NI| = R/2 - r", + "九点圆与内切圆内切(费尔巴哈)", + ).with(Shape::Segment { + x1: n9[0], + y1: n9[1], + x2: inc[0], + y2: inc[1], + label: "NI".into(), + })); + // verify from coordinates + let ni_coord = dist2(n9, inc); + if (ni_coord - ni).abs() < 1e-5 * (1.0 + ni) { + out.push(fact( + "费尔巴哈定理", + "\\text{内切}", + "\\checkmark".into(), + None, + "d(N,I) = R_N - r", + "九点圆与内切圆相切", + )); + } + // Feuerbach point X(11): internal contact of nine-point circle and incircle. + // T = ((R/2) I - r N) / (R/2 - r) along NI (order N–I–Fe). + // Skip when N=I (equilateral: nine-point coincides with incircle). + { + let den_f = r9 - r; + let ni_len = dist2(n9, inc); + if den_f.abs() > 1e-9 && ni_len > 1e-9 { + let fe = [ + (r9 * inc[0] - r * n9[0]) / den_f, + (r9 * inc[1] - r * n9[1]) / den_f, + ]; + let d_n = dist2(n9, fe); + let d_i = dist2(inc, fe); + if (d_n - r9).abs() < 1e-4 * (1.0 + r9) && (d_i - r).abs() < 1e-4 * (1.0 + r) { + out.push(fact( + "费尔巴哈点", + "Fe", + pt(fe[0], fe[1]), + None, + "Fe = \\dfrac{(R/2)I - r\\,N}{R/2 - r}", + "费尔巴哈点 X(11):九点圆与内切圆的内切点", + ).with(Shape::Point { + x: fe[0], + y: fe[1], + label: "Fe 费尔巴哈".into(), + })); + out.push(fact( + "费尔巴哈点", + "|N\\,Fe|", + nice(d_n), + Some(d_n), + "|N\\,Fe| = R/2", + "费尔巴哈点在九点圆上", + )); + } + } + } + for i in 0..3 { + let ri = area / (p - s[i]); + // excenter I_i opposite vertex i: (-a_i V_i + a_j V_j + a_k V_k) / (-a_i + a_j + a_k) + let (ai, aj, ak) = (s[i], s[(i + 1) % 3], s[(i + 2) % 3]); + let den = -ai + aj + ak; + if den.abs() < 1e-12 { + continue; + } + let ia = [ + (-ai * v[i][0] + aj * v[(i + 1) % 3][0] + ak * v[(i + 2) % 3][0]) / den, + (-ai * v[i][1] + aj * v[(i + 1) % 3][1] + ak * v[(i + 2) % 3][1]) / den, + ]; + let nia = r9 + ri; + let nia_coord = dist2(n9, ia); + out.push(fact( + "费尔巴哈定理", + &format!("|NI_{}|", SIDE[i]), + nice(nia), + Some(nia), + &format!("|NI_{s}| = R/2 + r_{s}", s = SIDE[i]), + "九点圆与旁切圆外切(费尔巴哈)", + )); + if (nia_coord - nia).abs() < 1e-5 * (1.0 + nia) { + out.push(fact( + "费尔巴哈定理", + &format!("I_{}", SIDE[i]), + pt(ia[0], ia[1]), + None, + "", + "旁心(与九点圆外切)", + ).with(Shape::Point { + x: ia[0], + y: ia[1], + label: format!("I{}", SIDE[i]), + })); + } + } + } } - let m = -a / b; - let q = c / b; - let mut s = String::from("y = "); - if near(m, 0.0) { - s.push_str(&nice_short(q)); - return s; + + // Spieker center: incenter of the medial triangle = perimeter centroid. + { + let den = 2.0 * (a + b + c); + let sp = [ + ((b + c) * pa[0] + (c + a) * pb[0] + (a + b) * pc[0]) / den, + ((b + c) * pa[1] + (c + a) * pb[1] + (a + b) * pc[1]) / den, + ]; + out.push(fact( + "斯皮克中心", + "Sp", + pt(sp[0], sp[1]), + None, + "Sp = \\frac{(b+c)A+(c+a)B+(a+b)C}{2(a+b+c)}", + "斯皮克中心:周界重心(中点三角形的内心)", + ).with(Shape::Point { + x: sp[0], + y: sp[1], + label: "Sp 斯皮克".into(), + })); } - s.push_str(&if near(m, 1.0) { "x".into() } else if near(m, -1.0) { "-x".into() } else { format!("{}x", nice_short(m)) }); - if !near(q, 0.0) { - let qs = nice_short(q); - if let Some(stripped) = qs.strip_prefix('-') { - s.push_str(&format!(" - {}", stripped)); - } else { - s.push_str(&format!(" + {}", qs)); + + // Nagel point: concurrence of vertex-to-excircle-contact cevians; bary (s−a:s−b:s−c). + { + let sa = p - a; + let sb = p - b; + let sc = p - c; + if p > 1e-12 { + let na = [ + (sa * pa[0] + sb * pb[0] + sc * pc[0]) / p, + (sa * pa[1] + sb * pb[1] + sc * pc[1]) / p, + ]; + out.push(fact( + "奈格尔点", + "Na", + pt(na[0], na[1]), + None, + "Na = X(8) = \\frac{(s-a)A+(s-b)B+(s-c)C}{s}", + "奈格尔点:顶点与对旁切圆切点的连线交点", + ).with(Shape::Point { + x: na[0], + y: na[1], + label: "Na 奈格尔".into(), + })); + out.push(fact( + "奈格尔点", + "X_{8}", + pt(na[0], na[1]), + None, + "X(8) = (s-a:s-b:s-c)", + "X(8):奈格尔点", + ).with(Shape::Point { + x: na[0], + y: na[1], + label: "X8".into(), + })); + // Nagel line: I, G, Na collinear with IG:GNa = 1:2 + let ig = dist2(inc, centroid); + let gna = dist2(centroid, na); + let cross = (centroid[0] - inc[0]) * (na[1] - inc[1]) - (centroid[1] - inc[1]) * (na[0] - inc[0]); + if cross.abs() < 1e-6 * (1.0 + ig) * (1.0 + gna) && ig > 1e-12 { + let ratio = gna / ig; + out.push(fact( + "奈格尔点", + "I,\\ G,\\ Na", + "\\text{共线}".into(), + None, + "\\vec{INa} = 3\\vec{IG}", + "奈格尔线:内心、重心、奈格尔点共线", + )); + out.push(fact( + "奈格尔点", + "IG:GNa", + format!("1:{}", nice(ratio)), + Some(ratio), + "IG:GNa = 1:2", + "重心按 1:2 分 INa", + )); + } } } - s -} -pub fn points_analyze(points: &[NamedPoint]) -> Vec { - if points.iter().any(|p| p.p.len() == 3) { - return points3_analyze(points); - } - let pts: Vec<(String, [f64; 2])> = points.iter().map(|p| (p.name.clone(), [p.p[0], *p.p.get(1).unwrap_or(&0.0)])).collect(); - let n = pts.len(); - let mut out = Vec::new(); - if n == 0 { - return out; + + // Gergonne point: concurrence of vertex-to-incircle-contact cevians; bary 1/(s−a):… + { + let sa = p - a; + let sb = p - b; + let sc = p - c; + if sa > 1e-12 && sb > 1e-12 && sc > 1e-12 { + // bary 1/(s−a):1/(s−b):1/(s−c) = isotomic conjugate of Nagel (s−a:s−b:s−c) + let wa = 1.0 / sa; + let wb = 1.0 / sb; + let wc = 1.0 / sc; + let den = wa + wb + wc; + let ge = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "格尔巩点", + "Ge", + pt(ge[0], ge[1]), + None, + "Ge = X(7) = \\frac{\\frac{1}{s-a}A+\\frac{1}{s-b}B+\\frac{1}{s-c}C}{\\sum}", + "格尔巩点:顶点与对边内切圆切点的连线交点", + ).with(Shape::Point { + x: ge[0], + y: ge[1], + label: "Ge 格尔巩".into(), + })); + out.push(fact( + "格尔巩点", + "X_{7}", + pt(ge[0], ge[1]), + None, + "X(7) = (1/(s-a):1/(s-b):1/(s-c))", + "X(7):格尔巩点", + ).with(Shape::Point { + x: ge[0], + y: ge[1], + label: "X7".into(), + })); + out.push(fact( + "格尔巩点", + "Ge \\leftrightarrow Na", + "\\text{等截共轭}".into(), + None, + "\\text{isotomic}", + "格尔巩点与奈格尔点互为等截共轭", + )); + } } - if n == 1 { - let (nm, p) = &pts[0]; - let r = dist2(*p, [0.0, 0.0]); - out.push(fact("距离", &format!("|O{}|", nm), nice(r), Some(r), "\\sqrt{x^2 + y^2}", "到原点的距离")); - let q = if near(p[0], 0.0) || near(p[1], 0.0) { "坐标轴上".to_string() } else { format!("第{}象限", match (p[0] > 0.0, p[1] > 0.0) { (true, true) => "一", (false, true) => "二", (false, false) => "三", _ => "四" }) }; - out.push(fact("位置", nm, format!("\\text{{{}}}", q), None, "", "看坐标符号")); - return out; + + // Lemoine / symmedian point K: concurrence of symmedians; bary (a²:b²:c²). + { + let wa = a * a; + let wb = b * b; + let wc = c * c; + let den = wa + wb + wc; + if den > 1e-18 { + let kpt = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "勒莫恩点", + "K", + pt(kpt[0], kpt[1]), + None, + "K = \\frac{a^2 A + b^2 B + c^2 C}{a^2 + b^2 + c^2}", + "勒莫恩点(对称中线点):三对称中线交点", + ).with(Shape::Point { + x: kpt[0], + y: kpt[1], + label: "K 勒莫恩".into(), + })); + } } - if n == 2 { - let ((na, a), (nb, b)) = (&pts[0], &pts[1]); - let d = dist2(*a, *b); - let mid = [(a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0]; - out.push(fact("线段", &format!("|{na}{nb}|"), nice(d), Some(d), "\\sqrt{(x_2-x_1)^2 + (y_2-y_1)^2}", "两点间距离").with(Shape::Segment { x1: a[0], y1: a[1], x2: b[0], y2: b[1], label: format!("{na}{nb}") })); - out.push(fact("线段", &format!("{na}{nb} \\text{{ 中点}}"), pt(mid[0], mid[1]), None, "\\left(\\frac{x_1+x_2}{2},\\ \\frac{y_1+y_2}{2}\\right)", "中点公式").with(Shape::Point { x: mid[0], y: mid[1], label: "中点 M".into() })); - let (la, lb) = (b[1] - a[1], a[0] - b[0]); - let lc = la * a[0] + lb * a[1]; - if !near(b[0], a[0]) { - let k = (b[1] - a[1]) / (b[0] - a[0]); - out.push(fact("直线", "k", nice(k), Some(k), "k = \\frac{y_2 - y_1}{x_2 - x_1}", "斜率")); - let ang = k.atan().rem_euclid(PI); - out.push(fact("直线", "\\alpha", deg(ang), Some(ang), "\\tan\\alpha = k", "倾斜角")); - } else { - out.push(fact("直线", "k", "\\text{不存在(竖直)}".into(), None, "", "两点横坐标相同")); + + // Brocard angle ω and Brocard points Ω, Ω′. + // cot ω = (a²+b²+c²)/(4S); Ω trilinear (c/b : a/c : b/a), Ω′ (b/c : c/a : a/b). + if area > 1e-12 { + let sum_sq = a * a + b * b + c * c; + let cot_w = sum_sq / (4.0 * area); + let tan_w = 1.0 / cot_w; + let omega = tan_w.atan(); + out.push(fact( + "布罗卡", + "\\omega", + deg(omega), + Some(omega), + "\\cot\\omega = \\frac{a^2+b^2+c^2}{4S}", + "布罗卡角", + )); + out.push(fact( + "布罗卡", + "\\tan\\omega", + nice(tan_w), + Some(tan_w), + "\\tan\\omega = \\frac{4S}{a^2+b^2+c^2}", + "布罗卡角的正切", + )); + out.push(fact( + "布罗卡", + "\\cot\\omega", + nice(cot_w), + Some(cot_w), + "\\cot\\omega = \\frac{a^2+b^2+c^2}{4S}", + "与 tan ω 互为倒数", + )); + if a > 1e-12 && b > 1e-12 && c > 1e-12 { + let wa = a * c / b; + let wb = b * a / c; + let wc = c * b / a; + let den = wa + wb + wc; + let om = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "布罗卡", + "\\Omega", + pt(om[0], om[1]), + None, + "\\text{trilinear }(c/b:a/c:b/a)", + "第一布罗卡点", + ).with(Shape::Point { + x: om[0], + y: om[1], + label: "Ω 布罗卡".into(), + })); + let wa2 = a * b / c; + let wb2 = b * c / a; + let wc2 = c * a / b; + let den2 = wa2 + wb2 + wc2; + let omp = [ + (wa2 * pa[0] + wb2 * pb[0] + wc2 * pc[0]) / den2, + (wa2 * pa[1] + wb2 * pb[1] + wc2 * pc[1]) / den2, + ]; + out.push(fact( + "布罗卡", + "\\Omega'", + pt(omp[0], omp[1]), + None, + "\\text{trilinear }(b/c:c/a:a/b)", + "第二布罗卡点(与 Ω 互为等角共轭)", + ).with(Shape::Point { + x: omp[0], + y: omp[1], + label: "Ω′ 布罗卡".into(), + })); + // Brocard midpoint X(39) = midpoint(Ω, Ω′); center of Brocard circle. + let bm = [(om[0] + omp[0]) / 2.0, (om[1] + omp[1]) / 2.0]; + out.push(fact( + "布罗卡", + "X_{39}", + pt(bm[0], bm[1]), + None, + "X_{39} = \\text{mid}(\\Omega,\\Omega')", + "布罗卡中点:第一/第二布罗卡点的中点(布罗卡圆圆心)", + ).with(Shape::Point { + x: bm[0], + y: bm[1], + label: "X39 布罗卡中点".into(), + })); } - out.push(fact("直线", &format!("{na}{nb}"), line_eq_latex(la, lb, lc), None, "", "两点式")); - // perpendicular bisector: direction (la, lb) is normal of AB; bisector normal is AB direction - let (pa, pb) = (b[0] - a[0], b[1] - a[1]); - let pc = pa * mid[0] + pb * mid[1]; - out.push(fact("直线", "\\text{中垂线}", line_eq_latex(pa, pb, pc), None, "", "过中点且垂直于该线段")); - return out; } - // polygon in the given order - let side_name = |i: usize| format!("{}{}", pts[i].0, pts[(i + 1) % n].0); - let mut sides = Vec::new(); - for i in 0..n { - let (a, b) = (pts[i].1, pts[(i + 1) % n].1); - let d = dist2(a, b); - sides.push(d); - out.push(fact("边长", &format!("|{}|", side_name(i)), nice(d), Some(d), "\\sqrt{\\Delta x^2 + \\Delta y^2}", "两点间距离")); + // de Longchamps X(20): reflection of H over O (so O midpoint of LH); on Euler line. + // Trilinear cos A − cos B cos C; bary a(cos A − cos B cos C). + { + let longch = [2.0 * circ[0] - orth[0], 2.0 * circ[1] - orth[1]]; + out.push(fact( + "德龙尚点", + "L", + pt(longch[0], longch[1]), + None, + "X(20) = L = 2O - H", + "德龙尚点 X(20):垂心关于外心的对称点", + ).with(Shape::Point { + x: longch[0], + y: longch[1], + label: "L=X20 德龙尚".into(), + })); + let mid = [(longch[0] + orth[0]) / 2.0, (longch[1] + orth[1]) / 2.0]; + if dist2(mid, circ) < 1e-6 * (1.0 + dist2(circ, orth)) { + out.push(fact( + "德龙尚点", + "O = \\text{中点}(L,H)", + "\\checkmark".into(), + None, + "\\overrightarrow{OL} = -\\overrightarrow{OH}", + "外心是 LH 的中点(欧拉线上)", + )); + } + // Bary cross-check: a(cos A − cos B cos C) + let ca = g[0].cos(); + let cb = g[1].cos(); + let cc = g[2].cos(); + let wa = a * (ca - cb * cc); + let wb = b * (cb - cc * ca); + let wc = c * (cc - ca * cb); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xb = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if dist2(xb, longch) < 1e-4 * (1.0 + rr) { + out.push(fact( + "德龙尚点", + "X_{20}", + pt(xb[0], xb[1]), + None, + "X(20) = \\bigl(a(\\cos A-\\cos B\\cos C):\\cdots\\bigr)", + "X(20) 重心坐标与 L=2O−H 一致", + )); + } + } } - let mut area2 = 0.0; - for i in 0..n { - let (a, b) = (pts[i].1, pts[(i + 1) % n].1); - area2 += a[0] * b[1] - b[0] * a[1]; + + // Mittenpunkt Mi: perspector of medial and excentral triangles; bary a(s-a):b(s-b):c(s-c). + { + let sa = p - a; + let sb = p - b; + let sc = p - c; + let wa = a * sa; + let wb = b * sb; + let wc = c * sc; + let den = wa + wb + wc; + if den > 1e-12 { + let mi = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "米滕点", + "Mi", + pt(mi[0], mi[1]), + None, + "Mi = \\frac{a(s-a)A+b(s-b)B+c(s-c)C}{\\sum}", + "米滕点(Mittenpunkt):旁心三角形与中点三角形的透视中心", + ).with(Shape::Point { + x: mi[0], + y: mi[1], + label: "Mi 米滕".into(), + })); + } } - let orient = area2.signum(); - let area = area2.abs() / 2.0; - let per: f64 = sides.iter().sum(); - let mut angles = Vec::new(); - let mut convex = true; - for i in 0..n { - let prev = pts[(i + n - 1) % n].1; - let cur = pts[i].1; - let next = pts[(i + 1) % n].1; - let u = [prev[0] - cur[0], prev[1] - cur[1]]; - let w = [next[0] - cur[0], next[1] - cur[1]]; - let cross = w[0] * u[1] - w[1] * u[0]; - let dot = u[0] * w[0] + u[1] * w[1]; - let mut ang = cross.abs().atan2(dot); - if cross * orient < -1e-12 { - ang = 2.0 * PI - ang; - convex = false; + // Schiffler point: concurrence of Euler lines of ABC, IBC, ICA, IAB. + // Construct as Euler(ABC) ∩ Euler(IBC); cross-check with Euler(ICA). + if let Some((o_ibc, _)) = circum2(inc, pb, pc) { + let g_ibc = [ + (inc[0] + pb[0] + pc[0]) / 3.0, + (inc[1] + pb[1] + pc[1]) / 3.0, + ]; + if let Some(sf) = line_line_isect2(circ, orth, o_ibc, g_ibc) { + let cross = (orth[0] - circ[0]) * (sf[1] - circ[1]) - (orth[1] - circ[1]) * (sf[0] - circ[0]); + let scale = (1.0 + dist2(circ, orth)) * (1.0 + dist2(circ, sf)); + if cross.abs() < 1e-5 * scale { + out.push(fact( + "席弗勒点", + "Sc", + pt(sf[0], sf[1]), + None, + "X(21) = \\text{Euler}(\\triangle ABC)\\cap\\text{Euler}(\\triangle IBC)", + "席弗勒点 X(21):内心三角形与原三角形欧拉线的交点", + ).with(Shape::Point { + x: sf[0], + y: sf[1], + label: "Sc=X21 席弗勒".into(), + })); + if let Some((o_ica, _)) = circum2(inc, pc, pa) { + let g_ica = [ + (inc[0] + pc[0] + pa[0]) / 3.0, + (inc[1] + pc[1] + pa[1]) / 3.0, + ]; + if let Some(sf2) = line_line_isect2(circ, orth, o_ica, g_ica) { + if dist2(sf, sf2) < 1e-5 * (1.0 + dist2(circ, orth)) { + out.push(fact( + "席弗勒点", + "\\text{Euler}(ICA)", + "\\checkmark".into(), + None, + "\\text{Euler}(ICA)\\ni Sc", + "与 △ICA 的欧拉线亦交于同一点", + )); + } + } + } + } } - angles.push(ang); - out.push(fact("角", &format!("\\angle {}{}{}", pts[(i + n - 1) % n].0, pts[i].0, pts[(i + 1) % n].0), deg(ang), Some(ang), "\\cos\\theta = \\frac{\\vec u \\cdot \\vec v}{|\\vec u||\\vec v|}", "向量夹角")); } - out.push(fact("面积与周长", "S", nice(area), Some(area), "S = \\frac12\\left|\\sum (x_i y_{i+1} - x_{i+1} y_i)\\right|", "鞋带公式")); - out.push(fact("面积与周长", "C", nice(per), Some(per), "\\sum |P_i P_{i+1}|", "周长")); - // parallel / perpendicular sides - let dir = |i: usize| { - let (a, b) = (pts[i].1, pts[(i + 1) % n].1); - [b[0] - a[0], b[1] - a[1]] - }; - let mut par = Vec::new(); - for i in 0..n { - for j in i + 1..n { - let (u, w) = (dir(i), dir(j)); - let cr = u[0] * w[1] - u[1] * w[0]; - let dt = u[0] * w[0] + u[1] * w[1]; - let scale = (u[0].hypot(u[1])) * (w[0].hypot(w[1])); - if cr.abs() < 1e-9 * scale { - par.push((i, j)); - out.push(fact("位置关系", &format!("{} \\parallel {}", side_name(i), side_name(j)), "\\checkmark".into(), None, "x_1y_2 - x_2y_1 = 0", "方向向量共线")); - } else if dt.abs() < 1e-9 * scale { - out.push(fact("位置关系", &format!("{} \\perp {}", side_name(i), side_name(j)), "\\checkmark".into(), None, "\\vec u\\cdot\\vec v = 0", "方向向量点积为 0")); + // Bevan point: circumcenter of the excentral triangle = reflection of I over O. + { + let bevan = [2.0 * circ[0] - inc[0], 2.0 * circ[1] - inc[1]]; + out.push(fact( + "贝万点", + "Be", + pt(bevan[0], bevan[1]), + None, + "Be = 2O - I", + "贝万点:旁心三角形的外心(内心关于外心的对称点)", + ).with(Shape::Point { + x: bevan[0], + y: bevan[1], + label: "Be 贝万".into(), + })); + let mut ex = [[0.0f64; 2]; 3]; + let mut ex_ok = true; + for i in 0..3 { + let (ai, aj, ak) = (s[i], s[(i + 1) % 3], s[(i + 2) % 3]); + let den = -ai + aj + ak; + if den.abs() < 1e-12 { + ex_ok = false; + break; + } + ex[i] = [ + (-ai * v[i][0] + aj * v[(i + 1) % 3][0] + ak * v[(i + 2) % 3][0]) / den, + (-ai * v[i][1] + aj * v[(i + 1) % 3][1] + ak * v[(i + 2) % 3][1]) / den, + ]; + } + if ex_ok { + if let Some((o_ex, _)) = circum2(ex[0], ex[1], ex[2]) { + if dist2(o_ex, bevan) < 1e-5 * (1.0 + dist2(circ, inc)) { + out.push(fact( + "贝万点", + "O(I_a I_b I_c)", + "\\checkmark".into(), + None, + "Be = O(I_a I_b I_c)", + "旁心三角形的外心与 2O−I 重合", + )); + } } } } - if n == 3 { - let s = [sides[1], sides[2], sides[0]]; // a = BC, b = CA, c = AB - let g = [angles[0], angles[1], angles[2]]; - let verts = [pts[0].1, pts[1].1, pts[2].1]; - let names = [pts[0].0.as_str(), pts[1].0.as_str(), pts[2].0.as_str()]; - if area < 1e-12 { - out.push(fact("形状判断", "\\text{类型}", "\\text{三点共线,不构成三角形}".into(), None, "", "面积为 0")); - return out; + // Orthic triangle: feet of the altitudes (on the nine-point circle). + { + let n9loc = [(circ[0] + orth[0]) / 2.0, (circ[1] + orth[1]) / 2.0]; + let r9loc = rr / 2.0; + let mut feet = [[0.0f64; 2]; 3]; + let mut ok = true; + for i in 0..3 { + let p0 = v[i]; + let q1 = v[(i + 1) % 3]; + let q2 = v[(i + 2) % 3]; + let dx = q2[0] - q1[0]; + let dy = q2[1] - q1[1]; + let den = dx * dx + dy * dy; + if den < 1e-18 { + ok = false; + break; + } + let t = ((p0[0] - q1[0]) * dx + (p0[1] - q1[1]) * dy) / den; + feet[i] = [q1[0] + t * dx, q1[1] + t * dy]; } - let derived = triangle_derived(s, g, verts, names); - out.extend(derived.into_iter().filter(|f| f.label != "S" && f.label != "C_{\\triangle}")); - } else if n == 4 { - let (a, b, c, d) = (pts[0].1, pts[1].1, pts[2].1, pts[3].1); - let d1 = dist2(a, c); - let d2 = dist2(b, d); - out.push(fact("对角线", &format!("|{}{}|", pts[0].0, pts[2].0), nice(d1), Some(d1), "", "两点间距离").with(Shape::Segment { x1: a[0], y1: a[1], x2: c[0], y2: c[1], label: "对角线".into() })); - out.push(fact("对角线", &format!("|{}{}|", pts[1].0, pts[3].0), nice(d2), Some(d2), "", "两点间距离").with(Shape::Segment { x1: b[0], y1: b[1], x2: d[0], y2: d[1], label: "对角线".into() })); - // diagonal intersection - let m = vec![vec![c[0] - a[0], -(d[0] - b[0])], vec![c[1] - a[1], -(d[1] - b[1])]]; - if let Some(st) = solve_linear_pub(m, vec![b[0] - a[0], b[1] - a[1]]) { - if st[0].is_finite() { - let p = [a[0] + st[0] * (c[0] - a[0]), a[1] + st[0] * (c[1] - a[1])]; - out.push(fact("对角线", "\\text{交点}", pt(p[0], p[1]), None, "", "联立两条对角线").with(Shape::Point { x: p[0], y: p[1], label: "对角线交点".into() })); + if ok { + for i in 0..3 { + let f = feet[i]; + out.push(fact( + "垂足三角形", + &format!("H_{}", SIDE[i]), + pt(f[0], f[1]), + None, + &format!("H_{s} = \\text{{足}}_{{{s}}}", s = SIDE[i]), + "垂足三角形的顶点(高线足)", + ).with(Shape::Point { + x: f[0], + y: f[1], + label: format!("H{} 垂足", SIDE[i]), + })); + } + let on_n9 = (0..3).all(|i| (dist2(feet[i], n9loc) - r9loc).abs() < 1e-5 * (1.0 + r9loc)); + if on_n9 { + out.push(fact( + "垂足三角形", + "H_a, H_b, H_c \\in \\text{九点圆}", + "\\checkmark".into(), + None, + "d(N, H_*) = R/2", + "垂足三角形内接于九点圆", + )); + } + // Circumcenter of the orthic triangle is the nine-point center N. + if let Some((o_h, _)) = circum2(feet[0], feet[1], feet[2]) { + out.push(fact( + "垂足三角形", + "O(H_a H_b H_c)", + pt(o_h[0], o_h[1]), + None, + "O_{\\text{orthic}} = N", + "垂足三角形的外心(等于九点圆心)", + ).with(Shape::Point { + x: o_h[0], + y: o_h[1], + label: "O_H 垂足外心".into(), + })); + if dist2(o_h, n9loc) < 1e-5 * (1.0 + rr) { + out.push(fact( + "垂足三角形", + "O(H_a H_b H_c) = N", + "\\checkmark".into(), + None, + "O_{\\text{orthic}} = N = \\tfrac{O+H}{2}", + "垂足外心与九点圆心重合", + )); + } } } - let is_para = par.contains(&(0, 2)) && par.contains(&(1, 3)); - let right = angles.iter().all(|x| near(*x, PI / 2.0)); - let equal = sides.iter().all(|x| near(*x, sides[0])); - let kind = if is_para && right && equal { - "正方形" - } else if is_para && right { - "矩形" - } else if is_para && equal { - "菱形" - } else if is_para { - "平行四边形" - } else if par.contains(&(0, 2)) || par.contains(&(1, 3)) { - "梯形" - } else if !convex { - "凹四边形" - } else { - "一般凸四边形" + } + + // Euler reflection point X(110): reflections of Euler line over the sides concur on (O). + // May fail to concur for some right triangles — emit only when found. + { + let reflect = |pt: [f64; 2], s1: [f64; 2], s2: [f64; 2]| -> [f64; 2] { + let dx = s2[0] - s1[0]; + let dy = s2[1] - s1[1]; + let den = dx * dx + dy * dy; + if den < 1e-18 { + return pt; + } + let t = ((pt[0] - s1[0]) * dx + (pt[1] - s1[1]) * dy) / den; + let foot = [s1[0] + t * dx, s1[1] + t * dy]; + [2.0 * foot[0] - pt[0], 2.0 * foot[1] - pt[1]] }; - out.push(fact("形状判断", "\\text{类型}", format!("\\text{{{}}}", kind), None, "", "由对边平行、直角、边长相等判断")); - } else { - out.push(fact("形状判断", "\\text{类型}", format!("\\text{{{}{}边形}}", if convex { "凸" } else { "凹" }, n), None, "", "内角和 (n-2)·180°")); + let mirrors = [(pb, pc), (pc, pa), (pa, pb)]; + let mut segs: [([f64; 2], [f64; 2]); 3] = [([0.0; 2], [0.0; 2]); 3]; + let mut ok = true; + for (i, (s1, s2)) in mirrors.into_iter().enumerate() { + let p = reflect(circ, s1, s2); + let mut q = reflect(orth, s1, s2); + if dist2(p, q) < 1e-9 { + q = reflect(centroid, s1, s2); + if dist2(p, q) < 1e-9 { + ok = false; + break; + } + } + segs[i] = (p, q); + } + if ok { + if let (Some(e1), Some(e2)) = ( + line_line_isect2(segs[0].0, segs[0].1, segs[1].0, segs[1].1), + line_line_isect2(segs[0].0, segs[0].1, segs[2].0, segs[2].1), + ) { + if dist2(e1, e2) < 1e-5 * (1.0 + rr) { + let er = e1; + out.push(fact( + "欧拉反射点", + "E_r", + pt(er[0], er[1]), + None, + "\\mathrm{refl}_{BC}(OH)\\cap\\mathrm{refl}_{CA}(OH)", + "欧拉反射点:欧拉线关于三边的反射线交点", + ).with(Shape::Point { + x: er[0], + y: er[1], + label: "Er 欧拉反射".into(), + })); + if (dist2(er, circ) - rr).abs() < 1e-5 * (1.0 + rr) { + out.push(fact( + "欧拉反射点", + "E_r \\in (O)", + "\\checkmark".into(), + None, + "d(O, E_r) = R", + "欧拉反射点在外接圆上", + )); + } + } + } + } } - out -} -fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] { - [a[0] - b[0], a[1] - b[1], a[2] - b[2]] -} -fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 { - a[0] * b[0] + a[1] * b[1] + a[2] * b[2] -} -fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] { - [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]] -} -fn len3(a: [f64; 3]) -> f64 { - dot3(a, a).sqrt() -} - -fn points3_analyze(points: &[NamedPoint]) -> Vec { - let pts: Vec<(String, [f64; 3])> = points.iter().map(|p| (p.name.clone(), [p.p[0], *p.p.get(1).unwrap_or(&0.0), *p.p.get(2).unwrap_or(&0.0)])).collect(); - let mut out = Vec::new(); - for i in 0..pts.len() { - for j in i + 1..pts.len() { - let d = len3(sub3(pts[j].1, pts[i].1)); - out.push(fact("距离", &format!("|{}{}|", pts[i].0, pts[j].0), nice(d), Some(d), "\\sqrt{\\Delta x^2+\\Delta y^2+\\Delta z^2}", "空间两点距离")); + // Contact (intouch) triangle: incircle tangency points on the sides. + // On BC: BD = s-b, DC = s-c; perspector is the Gergonne point. + { + let sa = p - a; + let sb = p - b; + let sc = p - c; + if sa > 1e-12 && sb > 1e-12 && sc > 1e-12 { + let xa = section_point(pb, pc, sb, sc); + let xb = section_point(pc, pa, sc, sa); + let xc = section_point(pa, pb, sa, sb); + let pts = [(xa, "a"), (xb, "b"), (xc, "c")]; + for (q, lab) in pts { + out.push(fact( + "接触三角形", + &format!("X_{}", lab), + pt(q[0], q[1]), + None, + &format!("X_{s}\\text{{ on side }}{s}", s = lab), + "内切圆与边的切点(接触三角形顶点)", + ).with(Shape::Point { + x: q[0], + y: q[1], + label: format!("X{} 切点", lab), + })); + } + if (dist2(inc, xa) - r).abs() < 1e-5 * (1.0 + r) + && (dist2(inc, xb) - r).abs() < 1e-5 * (1.0 + r) + && (dist2(inc, xc) - r).abs() < 1e-5 * (1.0 + r) + { + out.push(fact( + "接触三角形", + "|IX_*|", + nice(r), + Some(r), + "|IX_a| = |IX_b| = |IX_c| = r", + "切点到内心的距离等于内切圆半径", + )); + } + let wa = 1.0 / sa; + let wb = 1.0 / sb; + let wc = 1.0 / sc; + let den = wa + wb + wc; + let ge = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + let cross = (xa[0] - pa[0]) * (ge[1] - pa[1]) - (xa[1] - pa[1]) * (ge[0] - pa[0]); + if cross.abs() < 1e-5 * (1.0 + dist2(pa, xa)) * (1.0 + dist2(pa, ge)) { + out.push(fact( + "接触三角形", + "A, X_a, Ge", + "\\text{共线}".into(), + None, + "A X_a \\ni Ge", + "接触三角形与原三角形的透视中心为格尔巩点", + )); + } } } - if pts.len() >= 3 { - let (a, b, c) = (pts[0].1, pts[1].1, pts[2].1); - let nrm = cross3(sub3(b, a), sub3(c, a)); - let area = len3(nrm) / 2.0; - let names = format!("{}{}{}", pts[0].0, pts[1].0, pts[2].0); - out.push(fact("面积", &format!("S_{{\\triangle {}}}", names), nice(area), Some(area), "S = \\frac12|\\vec{AB}\\times\\vec{AC}|", "叉积的模")); - if area > 1e-12 { - let d = dot3(nrm, a); - out.push(fact("平面", &format!("\\text{{平面 }}{}", names), plane_latex(nrm, d), None, "\\vec n = \\vec{AB}\\times\\vec{AC}", "三点确定一个平面")); + + // Spieker circle: incircle of the medial triangle; center Sp, radius r/2. + { + let den = 2.0 * (a + b + c); + let sp = [ + ((b + c) * pa[0] + (c + a) * pb[0] + (a + b) * pc[0]) / den, + ((b + c) * pa[1] + (c + a) * pb[1] + (a + b) * pc[1]) / den, + ]; + let r_sp = r / 2.0; + out.push(fact( + "斯皮克圆", + "r_{\\text{Sp}}", + nice(r_sp), + Some(r_sp), + "r_{\\text{Sp}} = r/2", + "斯皮克圆:中点三角形的内切圆,半径为 r/2", + ).with(Shape::Circle { + cx: sp[0], + cy: sp[1], + r: r_sp, + label: "斯皮克圆".into(), + })); + out.push(fact( + "斯皮克圆", + "S_{\\text{中点}}/(s/2)", + nice(r_sp), + Some(r_sp), + "\\frac{S/4}{s/2} = \\frac{r}{2}", + "由中点三角形的面积与半周长推出", + )); + } + + // Kosnita X(54): A O_a ∩ B O_b ∩ C O_c with O_a = circumcenter of △OBC. + // Skip when △OAB / OBC / OCA is degenerate (e.g. right △ with O on a side). + { + let oa = circum2(circ, pb, pc); + let ob = circum2(circ, pc, pa); + let oc = circum2(circ, pa, pb); + if let (Some((oa, _)), Some((ob, _)), Some((oc, _))) = (oa, ob, oc) { + if let (Some(k1), Some(k2)) = ( + line_line_isect2(pa, oa, pb, ob), + line_line_isect2(pa, oa, pc, oc), + ) { + if dist2(k1, k2) < 1e-5 * (1.0 + rr) { + let ko = k1; + out.push(fact( + "科斯尼塔点", + "Ko", + pt(ko[0], ko[1]), + None, + "X(54) = AO_a \\cap BO_b \\cap CO_c", + "科斯尼塔点 X(54):A 与 △OBC 外心等连线的交点", + ).with(Shape::Point { + x: ko[0], + y: ko[1], + label: "Ko=X54 科斯尼塔".into(), + })); + } + } } - if pts.len() >= 4 { - let dpt = pts[3].1; - let vol = dot3(sub3(dpt, a), nrm).abs() / 6.0; - out.push(fact("体积", "V", nice(vol), Some(vol), "V = \\frac16\\left|\\vec{AD}\\cdot(\\vec{AB}\\times\\vec{AC})\\right|", "混合积")); - if area > 1e-12 { - let h = 3.0 * vol / area; - out.push(fact("距离", &format!("d({}, \\text{{平面}})", pts[3].0), nice(h), Some(h), "d = \\frac{3V}{S}", "等体积法")); + } + + + // Extouch (excircle-contact / Nagel) triangle: A-excircle touches BC at T_a, etc. + // BT_a = s−c, CT_a = s−b; perspector is the Nagel point (dual of the intouch △). + { + let sa = p - a; + let sb = p - b; + let sc = p - c; + if sa > 1e-12 && sb > 1e-12 && sc > 1e-12 { + // A-excircle on BC: BT:TC = (s−c):(s−b) + let ta = section_point(pb, pc, sc, sb); + let tb = section_point(pc, pa, sa, sc); + let tc = section_point(pa, pb, sb, sa); + let pts = [(ta, "a"), (tb, "b"), (tc, "c")]; + for (q, lab) in pts { + out.push(fact( + "旁切接触三角形", + &format!("T_{}", lab), + pt(q[0], q[1]), + None, + &format!("T_{s}\\text{{ on side }}{s}", s = lab), + "旁切圆与边的切点(旁切接触 / 奈格尔三角形顶点)", + ).with(Shape::Point { + x: q[0], + y: q[1], + label: format!("T{} 旁切点", lab), + })); + } + // Midpoint of intouch X_a and extouch T_a is the midpoint of BC (isotomic). + let xa = section_point(pb, pc, sb, sc); + let mid_xt = [(xa[0] + ta[0]) / 2.0, (xa[1] + ta[1]) / 2.0]; + let mid_bc = [(pb[0] + pc[0]) / 2.0, (pb[1] + pc[1]) / 2.0]; + if dist2(mid_xt, mid_bc) < 1e-5 * (1.0 + a) { + out.push(fact( + "旁切接触三角形", + "X_a T_a", + "\\text{以 BC 中点为中点}".into(), + None, + "\\text{mid}(X_a,T_a)=\\text{mid}(B,C)", + "内切切点与旁切切点关于边中点等截对称", + )); + } + // Perspector Na: A, T_a, Na collinear. + let na = [ + (sa * pa[0] + sb * pb[0] + sc * pc[0]) / p, + (sa * pa[1] + sb * pb[1] + sc * pc[1]) / p, + ]; + let cross = (ta[0] - pa[0]) * (na[1] - pa[1]) - (ta[1] - pa[1]) * (na[0] - pa[0]); + if cross.abs() < 1e-5 * (1.0 + dist2(pa, ta)) * (1.0 + dist2(pa, na)) { + out.push(fact( + "旁切接触三角形", + "A, T_a, Na", + "\\text{共线}".into(), + None, + "A T_a \\ni Na", + "旁切接触三角形与原三角形的透视中心为奈格尔点", + )); } } } - out -} -fn plane_latex(n: [f64; 3], d: f64) -> String { - let mut terms = Vec::new(); - for (c, v) in n.iter().zip(["x", "y", "z"]) { - if near(*c, 0.0) { - continue; + // Steiner inellipse: unique inscribed ellipse tangent at the side midpoints; center G. + // Area = π S / (3 √3); coincides with the incircle on equilateral triangles. + { + let ma = [(pb[0] + pc[0]) / 2.0, (pb[1] + pc[1]) / 2.0]; + let mb = [(pc[0] + pa[0]) / 2.0, (pc[1] + pa[1]) / 2.0]; + let mc = [(pa[0] + pb[0]) / 2.0, (pa[1] + pb[1]) / 2.0]; + for (q, lab) in [(ma, "a"), (mb, "b"), (mc, "c")] { + out.push(fact( + "施泰纳内接椭圆", + &format!("M_{}", lab), + pt(q[0], q[1]), + None, + &format!("M_{s} = \\text{{mid}}", s = lab), + "施泰纳内接椭圆在各边中点处与边相切", + ).with(Shape::Point { + x: q[0], + y: q[1], + label: format!("M{} 中点切", lab), + })); + } + let steiner_area = std::f64::consts::PI * area / (3.0 * 3.0f64.sqrt()); + out.push(fact( + "施泰纳内接椭圆", + "S_{\\text{Steiner}}", + nice(steiner_area), + Some(steiner_area), + "S_{\\text{Steiner}} = \\frac{\\pi S}{3\\sqrt{3}}", + "施泰纳内接椭圆:面积最大的内接椭圆,中心为重心 G", + ).with(Shape::Point { + x: centroid[0], + y: centroid[1], + label: "Steiner@G".into(), + })); + // Equilateral: Steiner = incircle ⇒ area = π r². + if (a - b).abs() < 1e-9 && (b - c).abs() < 1e-9 { + let circ_area = std::f64::consts::PI * r * r; + if (steiner_area - circ_area).abs() < 1e-5 * (1.0 + circ_area) { + out.push(fact( + "施泰纳内接椭圆", + "=\\pi r^2", + nice(circ_area), + Some(circ_area), + "\\text{equilateral: Steiner = incircle}", + "等边时施泰纳内接椭圆与内切圆重合", + )); + } } - let cs = if near(*c, 1.0) { String::new() } else if near(*c, -1.0) { "-".into() } else { nice_short(*c) }; - terms.push(format!("{}{}", cs, v)); } - let mut s = terms.join(" + ").replace("+ -", "- "); - if s.is_empty() { - s = "0".into(); + + // Mandart inellipse: inscribed ellipse tangent at the extouch points; center = Mittenpunkt. + { + let sa = p - a; + let sb = p - b; + let sc = p - c; + let wa = a * sa; + let wb = b * sb; + let wc = c * sc; + let den = wa + wb + wc; + if den > 1e-12 && sa > 1e-12 && sb > 1e-12 && sc > 1e-12 { + let mi = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "曼达特内接椭圆", + "\\text{center}", + pt(mi[0], mi[1]), + None, + "\\text{center} = \\text{Mi}", + "曼达特内接椭圆:在旁切切点处与边相切,中心为米滕点", + ).with(Shape::Point { + x: mi[0], + y: mi[1], + label: "Mandart@Mi".into(), + })); + out.push(fact( + "曼达特内接椭圆", + "T_*", + "\\text{切于旁切切点}".into(), + None, + "\\text{tangent at }T_a,T_b,T_c", + "与旁切接触三角形的三顶点处相切", + )); + } } - format!("{} = {}", s, nice_short(d)) -} -// --------------------------------------------------------------------------- -// Vectors -#[derive(Deserialize)] -pub struct NamedVec { - pub name: String, - pub v: Vec, -} + // Clawson point X(19): bary tan A : tan B : tan C. + // Undefined / at infinity on right △ (tan → ∞); emit only for acute triangles. + { + let cosines = [g[0].cos(), g[1].cos(), g[2].cos()]; + let acute = cosines.iter().all(|&c| c > 1e-9) + && g.iter().all(|&ang| ang > 1e-9 && ang < std::f64::consts::FRAC_PI_2 - 1e-9); + if acute { + let wa = g[0].tan(); + let wb = g[1].tan(); + let wc = g[2].tan(); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let cl = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "克劳森点", + "Cl", + pt(cl[0], cl[1]), + None, + "Cl = \\frac{\\tan A\\,A+\\tan B\\,B+\\tan C\\,C}{\\sum}", + "克劳森点 X(19):锐角三角形中 tan A:tan B:tan C(直角时退化)", + ).with(Shape::Point { + x: cl[0], + y: cl[1], + label: "Cl 克劳森".into(), + })); + } + } + } -fn vlabel(n: &str) -> String { - if n.len() == 2 && n.chars().all(|c| c.is_ascii_uppercase()) { - format!("\\overrightarrow{{{}}}", n) - } else { - format!("\\mathbf{{{}}}", n) + // First Yff center of congruence X(174): bary sec A : sec B : sec C = 1/cos. + // Skip when any cos ≈ 0 (right / near-right △). + { + let cosines = [g[0].cos(), g[1].cos(), g[2].cos()]; + if cosines.iter().all(|&c| c.abs() > 1e-9) { + let wa = 1.0 / cosines[0]; + let wb = 1.0 / cosines[1]; + let wc = 1.0 / cosines[2]; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let yff = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "伊夫中心", + "Yff", + pt(yff[0], yff[1]), + None, + "Yff = \\frac{\\sec A\\,A+\\sec B\\,B+\\sec C\\,C}{\\sum}", + "伊夫中心 X(174):sec A:sec B:sec C(直角时 sec 无定义)", + ).with(Shape::Point { + x: yff[0], + y: yff[1], + label: "Yff 伊夫".into(), + })); + } + } } -} -pub fn vectors_analyze(vs: &[NamedVec]) -> Vec { - let mut out = Vec::new(); - let to3 = |v: &Vec| [v[0], *v.get(1).unwrap_or(&0.0), *v.get(2).unwrap_or(&0.0)]; - let is3 = vs.iter().any(|v| v.v.len() == 3); - for v in vs { - let a = to3(&v.v); - let l = len3(a); - let nm = vlabel(&v.name); - out.push(fact("模长与方向", &format!("|{}|", nm), nice(l), Some(l), "\\sqrt{x^2 + y^2 + z^2}", "模长")); - if l > 1e-12 { - if is3 { - out.push(fact("模长与方向", &format!("\\hat{{{}}}", v.name), pt3([a[0] / l, a[1] / l, a[2] / l]), None, "\\frac{\\vec v}{|\\vec v|}", "单位向量")); - } else { - let ang = a[1].atan2(a[0]).rem_euclid(2.0 * PI); - out.push(fact("模长与方向", &format!("\\theta_{{{}}}", v.name), deg(ang), Some(ang), "\\tan\\theta = \\frac{y}{x}", "与 x 轴正方向的夹角")); + // Exeter point X(22): perspector of tangential and circummedial triangles; + // bary a(b⁴+c⁴−a⁴) : b(c⁴+a⁴−b⁴) : c(a⁴+b⁴−c⁴). + { + let a2 = a * a; + let b2 = b * b; + let c2 = c * c; + let a4 = a2 * a2; + let b4 = b2 * b2; + let c4 = c2 * c2; + let wa = a * (b4 + c4 - a4); + let wb = b * (c4 + a4 - b4); + let wc = c * (a4 + b4 - c4); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let ex = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "埃克塞特点", + "Ex", + pt(ex[0], ex[1]), + None, + "Ex = \\frac{a(b^4+c^4-a^4)A+\\cdots}{\\sum}", + "埃克塞特点 X(22):切线三角形与外接中点三角形的透视中心", + ).with(Shape::Point { + x: ex[0], + y: ex[1], + label: "Ex 埃克塞特".into(), + })); + // Perspector helper: name the classical pair. + out.push(fact( + "埃克塞特点", + "\\text{perspector}", + "\\text{tangential}/\\text{circummedial}".into(), + None, + "\\text{perspector}(\\triangle_{\\text{tang}},\\triangle_{\\text{cm}})", + "透视中心:切线三角形与外接中点三角形", + )); + } + } + + + // Isodynamic / isogonic centers X(15), X(16): concurrence of Apollonian circles. + // Bary a sin(A±π/3); X(16) degenerates on equilateral (sin(A−π/3)=0). + { + let pi3 = std::f64::consts::PI / 3.0; + let s15 = [((g[0] + pi3).sin()), (g[1] + pi3).sin(), (g[2] + pi3).sin()]; + let wa = a * s15[0]; + let wb = b * s15[1]; + let wc = c * s15[2]; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let p15 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "等动力点", + "S_{1}", + pt(p15[0], p15[1]), + None, + "S_1 = \\frac{a\\sin(A+\\pi/3)A+\\cdots}{\\sum}", + "第一等动力点 X(15)(等角线中心 / Apollonian)", + ).with(Shape::Point { + x: p15[0], + y: p15[1], + label: "S1 等动力".into(), + })); + } + let s16 = [(g[0] - pi3).sin(), (g[1] - pi3).sin(), (g[2] - pi3).sin()]; + // Equilateral: sin(A−π/3)=0 ⇒ den=0 — skip. One 60° angle (weight 0) is fine. + { + let wa = a * s16[0]; + let wb = b * s16[1]; + let wc = c * s16[2]; + let den16 = wa + wb + wc; + if den16.abs() > 1e-12 { + let p16 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den16, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den16, + ]; + out.push(fact( + "等动力点", + "S_{2}", + pt(p16[0], p16[1]), + None, + "S_2 = \\frac{a\\sin(A-\\pi/3)A+\\cdots}{\\sum}", + "第二等动力点 X(16)(等角线中心)", + ).with(Shape::Point { + x: p16[0], + y: p16[1], + label: "S2 等动力".into(), + })); } } + if den.abs() > 1e-12 { + out.push(fact( + "等动力点", + "\\text{Apollonius}", + "\\text{三阿氏圆共点}".into(), + None, + "\\odot A_a\\cap\\odot A_b\\cap\\odot A_c", + "阿波罗尼奥斯圆(角平分线直径)交于等动力点", + )); + } } - for i in 0..vs.len() { - for j in i + 1..vs.len() { - let (a, b) = (to3(&vs[i].v), to3(&vs[j].v)); - let (na, nb) = (vlabel(&vs[i].name), vlabel(&vs[j].name)); - let d = dot3(a, b); - let (la, lb) = (len3(a), len3(b)); - let fmt = |v: &[f64]| if is3 { pt3([v[0], v[1], v[2]]) } else { pt(v[0], v[1]) }; - out.push(fact("两向量关系", &format!("{} \\cdot {}", na, nb), nice(d), Some(d), "x_1x_2 + y_1y_2 + z_1z_2", "数量积")); - if la > 1e-12 && lb > 1e-12 { - let ang = (d / (la * lb)).clamp(-1.0, 1.0).acos(); - out.push(fact("两向量关系", &format!("\\langle {}, {} \\rangle", na, nb), deg(ang), Some(ang), "\\cos\\theta = \\frac{\\vec a\\cdot\\vec b}{|\\vec a||\\vec b|}", "夹角")); - let proj = d / lb; - out.push(fact("两向量关系", &format!("\\text{{{} 在 {} 上的投影}}", vs[i].name, vs[j].name), nice(proj), Some(proj), "\\frac{\\vec a\\cdot\\vec b}{|\\vec b|}", "投影(数量)")); - // projection vector of a onto b, and the perpendicular rest: a = proj + perp - let pb2 = dot3(b, b); - let k = d / pb2; - let projv: Vec = (0..3).map(|t| k * b[t]).collect(); - let perp: Vec = (0..3).map(|t| a[t] - k * b[t]).collect(); - out.push(fact("两向量关系", &format!("\\text{{proj}}_{{{}}}\\ {}", nb, na), fmt(&projv), None, "\\frac{{\\vec a\\cdot\\vec b}}{{|\\vec b|^2}}\\,\\vec b", "投影向量").with(if is3 { Shape::Arrow3 { x: projv[0], y: projv[1], z: projv[2], label: format!("proj{}_{}", vs[i].name, vs[j].name) } } else { Shape::Segment { x1: 0.0, y1: 0.0, x2: projv[0], y2: projv[1], label: format!("proj{}_{}", vs[i].name, vs[j].name) } })); - out.push(fact("两向量关系", &format!("{} - \\text{{proj}}_{{{}}}\\ {}", na, nb, na), fmt(&perp), None, "\\vec a - \\text{proj}_{\\vec b}\\,\\vec a", "垂直分量:a = 投影 + 垂直").with(if is3 { Shape::Arrow3 { x: perp[0], y: perp[1], z: perp[2], label: format!("perp{}_{}", vs[i].name, vs[j].name) } } else { Shape::Segment { x1: projv[0], y1: projv[1], x2: a[0], y2: a[1], label: format!("perp{}_{}", vs[i].name, vs[j].name) } })); - let cr = cross3(a, b); - let lc = len3(cr); - if lc < 1e-9 * la * lb { - let same = d > 0.0; - out.push(fact("两向量关系", &format!("{} \\parallel {}", na, nb), format!("\\text{{{}}}", if same { "同向共线" } else { "反向共线" }), None, "\\vec a = \\lambda\\vec b", "叉积为零")); - out.push(fact("两向量关系", "\\lambda", nice(if same { la / lb } else { -la / lb }), None, "\\vec a = \\lambda \\vec b", "共线系数")); - } else if d.abs() < 1e-9 * la * lb { - out.push(fact("两向量关系", &format!("{} \\perp {}", na, nb), "\\checkmark".into(), None, "\\vec a\\cdot\\vec b = 0", "数量积为 0")); - } - if is3 { - out.push(fact("两向量关系", &format!("{} \\times {}", na, nb), pt3(cr), None, "\\begin{vmatrix} i & j & k \\\\ x_1 & y_1 & z_1 \\\\ x_2 & y_2 & z_2 \\end{vmatrix}", "向量积").with(Shape::Arrow3 { x: cr[0], y: cr[1], z: cr[2], label: format!("{}×{}", vs[i].name, vs[j].name) })); + + // Steiner point X(99): bary 1/(b²−c²):… on circumcircle ∩ Steiner ellipse. + // Undefined for isosceles (denominator zero); skip cleanly. + // Tarry X(98) = circumcircle antipode of Steiner (= 2O − St). + { + let dbc = b * b - c * c; + let dca = c * c - a * a; + let dab = a * a - b * b; + if dbc.abs() > 1e-10 && dca.abs() > 1e-10 && dab.abs() > 1e-10 { + let wa = 1.0 / dbc; + let wb = 1.0 / dca; + let wc = 1.0 / dab; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let st = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + let os = dist2(circ, st); + if (os - rr).abs() < 1e-4 * (1.0 + rr) { + out.push(fact( + "施泰纳点", + "St", + pt(st[0], st[1]), + None, + "St = \\frac{\\frac{1}{b^2-c^2}A+\\cdots}{\\sum}", + "施泰纳点 X(99):外接圆与施泰纳椭圆的第四交点", + ).with(Shape::Point { + x: st[0], + y: st[1], + label: "St 施泰纳".into(), + })); + out.push(fact( + "施泰纳点", + "X_{99}", + pt(st[0], st[1]), + None, + "X(99) = (1/(b^2-c^2):\\cdots)", + "X(99):施泰纳点", + ).with(Shape::Point { + x: st[0], + y: st[1], + label: "X99".into(), + })); + // Tarry = antipode of Steiner on (O) + let ty = [2.0 * circ[0] - st[0], 2.0 * circ[1] - st[1]]; + let ot = dist2(circ, ty); + if (ot - rr).abs() < 1e-4 * (1.0 + rr) { + out.push(fact( + "塔里点", + "Ty", + pt(ty[0], ty[1]), + None, + "Ty = 2O - St", + "塔里点 X(98):施泰纳点在外接圆上的对径点", + ).with(Shape::Point { + x: ty[0], + y: ty[1], + label: "Ty 塔里".into(), + })); + out.push(fact( + "塔里点", + "X_{98}", + pt(ty[0], ty[1]), + None, + "X(98) = 2O - X(99)", + "X(98):塔里点", + ).with(Shape::Point { + x: ty[0], + y: ty[1], + label: "X98".into(), + })); + out.push(fact( + "塔里点", + "|O\\,Ty|", + nice(ot), + Some(ot), + "|O\\,Ty| = R", + "塔里点在外接圆上", + )); + } } - out.push(fact("两向量关系", "S_{\\text{平行四边形}}", nice(lc), Some(lc), "|\\vec a \\times \\vec b| = |\\vec a||\\vec b|\\sin\\theta", "以两向量为邻边")); - out.push(fact("两向量关系", "S_{\\triangle}", nice(lc / 2.0), Some(lc / 2.0), "\\frac12|\\vec a\\times\\vec b|", "以两向量为邻边的三角形")); } - let sum: Vec = (0..3).map(|k| a[k] + b[k]).collect(); - let dif: Vec = (0..3).map(|k| a[k] - b[k]).collect(); - out.push(fact("两向量关系", &format!("{} + {}", na, nb), fmt(&sum), None, "", "坐标相加")); - out.push(fact("两向量关系", &format!("{} - {}", na, nb), fmt(&dif), None, "", "坐标相减")); } } - if vs.len() >= 3 && is3 { - let (a, b, c) = (to3(&vs[0].v), to3(&vs[1].v), to3(&vs[2].v)); - let m = dot3(a, cross3(b, c)); - out.push(fact("三向量", &format!("[{}\\,{}\\,{}]", vs[0].name, vs[1].name, vs[2].name), nice(m), Some(m), "\\vec a\\cdot(\\vec b\\times\\vec c)", "混合积")); - if m.abs() < 1e-9 { - out.push(fact("三向量", "\\text{共面}", "\\checkmark".into(), None, "[\\vec a\\,\\vec b\\,\\vec c] = 0", "混合积为 0")); - } else { - out.push(fact("三向量", "V_{\\text{平行六面体}}", nice(m.abs()), Some(m.abs()), "|[\\vec a\\,\\vec b\\,\\vec c]|", "混合积的绝对值")); + + + // Napoleon centers X(17), X(18): concurrence of lines to centroids of + // outward/inward equilateral triangles on the sides (distinct from Napoleon theorem). + { + let pi6 = std::f64::consts::PI / 6.0; + // X(17) outer: bary a csc(A+π/6); skip if sin(A+π/6)≈0 (e.g. angle ≈ 150°). + let s17 = [(g[0] + pi6).sin(), (g[1] + pi6).sin(), (g[2] + pi6).sin()]; + if s17.iter().all(|&s| s.abs() > 1e-9) { + let wa = a / s17[0]; + let wb = b / s17[1]; + let wc = c / s17[2]; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let n1 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "拿破仑中心", + "N_1", + pt(n1[0], n1[1]), + None, + "N_1 = \\frac{a\\csc(A+\\pi/6)A+\\cdots}{\\sum}", + "第一拿破仑点 X(17):外接等边三角形重心连线交点", + ).with(Shape::Point { + x: n1[0], + y: n1[1], + label: "N1 拿破仑".into(), + })); + } + } + // X(18) inner: bary a csc(A−π/6); skip if sin(A−π/6)≈0 (angle ≈ 30°). + let s18 = [(g[0] - pi6).sin(), (g[1] - pi6).sin(), (g[2] - pi6).sin()]; + if s18.iter().all(|&s| s.abs() > 1e-9) { + let wa = a / s18[0]; + let wb = b / s18[1]; + let wc = c / s18[2]; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let n2 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "拿破仑中心", + "N_2", + pt(n2[0], n2[1]), + None, + "N_2 = \\frac{a\\csc(A-\\pi/6)A+\\cdots}{\\sum}", + "第二拿破仑点 X(18):内接等边三角形重心连线交点", + ).with(Shape::Point { + x: n2[0], + y: n2[1], + label: "N2 拿破仑".into(), + })); + } } } - out -} -// --------------------------------------------------------------------------- -// Lines in the plane (a x + b y = c) + // Parry point X(111): bary a²/(2a²−b²−c²); on circumcircle. + // Degenerate when 2a²=b²+c² for any side (e.g. equilateral). + { + let da = 2.0 * a * a - b * b - c * c; + let db = 2.0 * b * b - c * c - a * a; + let dc = 2.0 * c * c - a * a - b * b; + if da.abs() > 1e-10 && db.abs() > 1e-10 && dc.abs() > 1e-10 { + let wa = (a * a) / da; + let wb = (b * b) / db; + let wc = (c * c) / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let py = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "帕里点", + "Py", + pt(py[0], py[1]), + None, + "Py = \\frac{\\frac{a^2}{2a^2-b^2-c^2}A+\\cdots}{\\sum}", + "帕里点 X(111):Λ(G,K)(等边时退化)", + ).with(Shape::Point { + x: py[0], + y: py[1], + label: "Py 帕里".into(), + })); + out.push(fact( + "帕里点", + "X_{111}", + pt(py[0], py[1]), + None, + "X(111) = (a^2/(2a^2-b^2-c^2):\\cdots)", + "X(111):帕里点", + ).with(Shape::Point { + x: py[0], + y: py[1], + label: "X111".into(), + })); + let op = dist2(circ, py); + if (op - rr).abs() < 1e-4 * (1.0 + rr) { + out.push(fact( + "帕里点", + "|O\\,Py|", + nice(op), + Some(op), + "|O\\,Py| = R", + "帕里点在外接圆上", + )); + } + } + } + } -#[derive(Deserialize)] -pub struct NamedLine { - pub name: String, - pub a: f64, - pub b: f64, - pub c: f64, -} + // Vecten points: perspectors of outer/inner square-center triangles. + // X(485) outer: bary a sec(A−π/4); skip if cos(A−π/4)≈0 (angle ≈ 135°). + // X(486) inner: bary a sec(A+π/4); skip if cos(A+π/4)≈0 (angle ≈ 45°). + { + let pi4 = std::f64::consts::FRAC_PI_4; + let c_out = [(g[0] - pi4).cos(), (g[1] - pi4).cos(), (g[2] - pi4).cos()]; + if c_out.iter().all(|&c| c.abs() > 1e-9) { + let wa = a / c_out[0]; + let wb = b / c_out[1]; + let wc = c / c_out[2]; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let vo = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "韦克滕点", + "V_{\\text{out}}", + pt(vo[0], vo[1]), + None, + "V_{\\text{out}} = \\frac{a\\sec(A-\\pi/4)A+\\cdots}{\\sum}", + "外韦克滕点 X(485):外侧正方形中心三角形的透视中心", + ).with(Shape::Point { + x: vo[0], + y: vo[1], + label: "Vout 韦克滕".into(), + })); + } + } + let c_in = [(g[0] + pi4).cos(), (g[1] + pi4).cos(), (g[2] + pi4).cos()]; + if c_in.iter().all(|&c| c.abs() > 1e-9) { + let wa = a / c_in[0]; + let wb = b / c_in[1]; + let wc = c / c_in[2]; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let vi = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "韦克滕点", + "V_{\\text{in}}", + pt(vi[0], vi[1]), + None, + "V_{\\text{in}} = \\frac{a\\sec(A+\\pi/4)A+\\cdots}{\\sum}", + "内韦克滕点 X(486):内侧正方形中心三角形的透视中心", + ).with(Shape::Point { + x: vi[0], + y: vi[1], + label: "Vin 韦克滕".into(), + })); + } + } + } -/// Intersection point of two lines a x + b y = c, when they are not (near-)parallel. -fn line_isect(p: &NamedLine, q: &NamedLine) -> Option<[f64; 2]> { - let det = p.a * q.b - p.b * q.a; - if det.abs() < 1e-12 * (p.a.hypot(p.b) * q.a.hypot(q.b)) { - return None; + + // Hofstadter points X(359), X(360): transcendental centers (angles in barycentrics). + // X(360) zero-point: bary A:B:C; X(359) one-point: bary a²/A:b²/B:c²/C. + { + if g[0] > 1e-12 && g[1] > 1e-12 && g[2] > 1e-12 { + let den0 = g[0] + g[1] + g[2]; + let hz = [ + (g[0] * pa[0] + g[1] * pb[0] + g[2] * pc[0]) / den0, + (g[0] * pa[1] + g[1] * pb[1] + g[2] * pc[1]) / den0, + ]; + out.push(fact( + "霍夫斯塔特点", + "Hf_0", + pt(hz[0], hz[1]), + None, + "Hf_0 = (A:B:C)", + "霍夫斯塔特零点 X(360):重心坐标为三角本身", + ).with(Shape::Point { + x: hz[0], + y: hz[1], + label: "Hf0 霍夫".into(), + })); + out.push(fact( + "霍夫斯塔特点", + "X_{360}", + pt(hz[0], hz[1]), + None, + "X(360) = (A:B:C)", + "X(360):霍夫斯塔特零点", + ).with(Shape::Point { + x: hz[0], + y: hz[1], + label: "X360".into(), + })); + let wa = (a * a) / g[0]; + let wb = (b * b) / g[1]; + let wc = (c * c) / g[2]; + let den1 = wa + wb + wc; + if den1.abs() > 1e-12 { + let ho = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den1, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den1, + ]; + out.push(fact( + "霍夫斯塔特点", + "Hf_1", + pt(ho[0], ho[1]), + None, + "Hf_1 = (a^2/A:b^2/B:c^2/C)", + "霍夫斯塔特一点 X(359):与 Hf_0 互为等角共轭", + ).with(Shape::Point { + x: ho[0], + y: ho[1], + label: "Hf1 霍夫".into(), + })); + out.push(fact( + "霍夫斯塔特点", + "X_{359}", + pt(ho[0], ho[1]), + None, + "X(359) = (a^2/A:b^2/B:c^2/C)", + "X(359):霍夫斯塔特一点(等角共轭于 X(360))", + ).with(Shape::Point { + x: ho[0], + y: ho[1], + label: "X359".into(), + })); + } + } } - Some([(p.c * q.b - p.b * q.c) / det, (p.a * q.c - p.c * q.a) / det]) -} -pub fn lines_analyze(ls: &[NamedLine], points: &[NamedPoint]) -> Vec { - let mut out = Vec::new(); - for l in ls { - let nm = &l.name; - if !near(l.b, 0.0) { - let k = -l.a / l.b; - out.push(fact("直线", &format!("k_{{{}}}", nm), nice(k), Some(k), "y = kx + b", "斜率")); - let ang = k.atan().rem_euclid(PI); - out.push(fact("直线", &format!("\\alpha_{{{}}}", nm), deg(ang), Some(ang), "\\tan\\alpha = k", "倾斜角")); - } else { - out.push(fact("直线", &format!("\\alpha_{{{}}}", nm), "90^\\circ".into(), None, "", "竖直直线,斜率不存在")); + // Kenmotu point X(371): congruent-squares point; bary a cos(A−π/4). + // Isogonal conjugate of outer Vecten X(485). + { + let pi4 = std::f64::consts::FRAC_PI_4; + let ca = [(g[0] - pi4).cos(), (g[1] - pi4).cos(), (g[2] - pi4).cos()]; + let wa = a * ca[0]; + let wb = b * ca[1]; + let wc = c * ca[2]; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let km = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "肯莫图点", + "Km", + pt(km[0], km[1]), + None, + "Km = \\frac{a\\cos(A-\\pi/4)A+\\cdots}{\\sum}", + "肯莫图点 X(371):三全等正方形的公共点(等角共轭于外韦克滕点)", + ).with(Shape::Point { + x: km[0], + y: km[1], + label: "Km 肯莫图".into(), + })); + out.push(fact( + "肯莫图点", + "X_{371}", + pt(km[0], km[1]), + None, + "X(371) = a\\cos(A-\\pi/4):\\cdots = \\mathrm{isogonal}(X_{485})", + "X(371):肯莫图点(全等正方形点)", + ).with(Shape::Point { + x: km[0], + y: km[1], + label: "X371".into(), + })); + let edge_den = a * a + b * b + c * c + 4.0 * area; + if edge_den > 1e-12 { + let edge = std::f64::consts::SQRT_2 * a * b * c / edge_den; + out.push(fact( + "肯莫图点", + "s_{\\text{square}}", + nice(edge), + Some(edge), + "s = \\frac{\\sqrt{2}\\,abc}{a^2+b^2+c^2+4S}", + "三全等正方形的边长", + )); + } } - if !near(l.a, 0.0) { - out.push(fact("截距", &format!("{} \\cap x\\text{{轴}}", nm), pt(l.c / l.a, 0.0), None, "y = 0", "令 y = 0").with(Shape::Point { x: l.c / l.a, y: 0.0, label: format!("{nm} 与 x 轴") })); + } + + // Apollonius point X(181): perspector of Apollonius circle with the excircles. + // Bary a²(b+c)²/(b+c−a); b+c−a=2(s−a)>0 for non-degenerate △. + { + let sa = p - a; + let sb = p - b; + let sc = p - c; + if sa > 1e-12 && sb > 1e-12 && sc > 1e-12 { + let wa = (a * a) * (b + c) * (b + c) / (2.0 * sa); + let wb = (b * b) * (c + a) * (c + a) / (2.0 * sb); + let wc = (c * c) * (a + b) * (a + b) / (2.0 * sc); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let ap = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "阿波罗尼奥斯点", + "Ap", + pt(ap[0], ap[1]), + None, + "Ap = \\frac{a^2(b+c)^2/(b+c-a)\\,A+\\cdots}{\\sum}", + "阿波罗尼奥斯点 X(181):与三旁切圆外切的阿氏圆的透视中心", + ).with(Shape::Point { + x: ap[0], + y: ap[1], + label: "Ap 阿氏".into(), + })); + } } - if !near(l.b, 0.0) { - out.push(fact("截距", &format!("{} \\cap y\\text{{轴}}", nm), pt(0.0, l.c / l.b), None, "x = 0", "令 x = 0").with(Shape::Point { x: 0.0, y: l.c / l.b, label: format!("{nm} 与 y 轴") })); + } + + + // Fermat–Torricelli / isogonic centers X(13), X(14) — distinct from isodynamic X(15)/X(16). + // Trilinear csc(A±π/3); bary a csc(A±π/3). If some angle ≥ 120°, X(13) is that vertex. + { + let pi3 = std::f64::consts::PI / 3.0; + let two_pi3 = 2.0 * pi3; + let mut obtuse120: Option = None; + for (i, &ang) in g.iter().enumerate() { + if ang >= two_pi3 - 1e-9 { + obtuse120 = Some(i); + break; + } } - let dist0 = l.c.abs() / l.a.hypot(l.b); - out.push(fact("距离", &format!("d(O, {})", nm), nice(dist0), Some(dist0), "d = \\frac{|Ax_0 + By_0 + C|}{\\sqrt{A^2 + B^2}}", "原点到直线的距离")); - for p in points { - let (x, y) = (p.p[0], *p.p.get(1).unwrap_or(&0.0)); - let d = (l.a * x + l.b * y - l.c).abs() / l.a.hypot(l.b); - let t = (l.c - l.a * x - l.b * y) / (l.a * l.a + l.b * l.b); - let foot = [x + t * l.a, y + t * l.b]; - if d < 1e-9 { - out.push(fact("点与直线", &format!("{} \\in {}", p.name, nm), "\\checkmark".into(), None, "", "代入方程成立")); - } else { - out.push(fact("点与直线", &format!("d({}, {})", p.name, nm), nice(d), Some(d), "d = \\frac{|Ax_0 + By_0 + C|}{\\sqrt{A^2 + B^2}}", "点到直线距离").with(Shape::Segment { x1: x, y1: y, x2: foot[0], y2: foot[1], label: format!("{} 到 {}", p.name, nm) })); + if let Some(i) = obtuse120 { + let fv = v[i]; + out.push(fact( + "费马点", + "F_1", + pt(fv[0], fv[1]), + None, + "F_1 = \\text{obtuse vertex (}\\ge 120^\\circ\\text{)}", + "第一费马–托里拆利点 X(13):钝角 ≥120° 时退化为该顶点", + ).with(Shape::Point { + x: fv[0], + y: fv[1], + label: "F1 费马".into(), + })); + } else { + let s13 = [(g[0] + pi3).sin(), (g[1] + pi3).sin(), (g[2] + pi3).sin()]; + if s13.iter().all(|&s| s.abs() > 1e-9) { + let wa = a / s13[0]; + let wb = b / s13[1]; + let wc = c / s13[2]; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let f1 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "费马点", + "F_1", + pt(f1[0], f1[1]), + None, + "F_1 = \\frac{a\\csc(A+\\pi/3)A+\\cdots}{\\sum}", + "第一费马–托里拆利 / 等角线中心 X(13)(异于等动力点 X(15))", + ).with(Shape::Point { + x: f1[0], + y: f1[1], + label: "F1 费马".into(), + })); + } } } - } - let mut par_pairs: Vec<(usize, usize)> = Vec::new(); - for i in 0..ls.len() { - for j in i + 1..ls.len() { - let (p, q) = (&ls[i], &ls[j]); - let det = p.a * q.b - p.b * q.a; - let rel = format!("{} , {}", p.name, q.name); - let scale = p.a.hypot(p.b) * q.a.hypot(q.b); - if det.abs() < 1e-12 * scale { - let coincide = (p.a * q.c - q.a * p.c).abs() < 1e-9 * scale && (p.b * q.c - q.b * p.c).abs() < 1e-9 * scale; - if coincide { - out.push(fact("两直线", &rel, "\\text{重合}".into(), None, "\\frac{A_1}{A_2} = \\frac{B_1}{B_2} = \\frac{C_1}{C_2}", "系数成比例")); - } else { - let (np, nq) = (p.a.hypot(p.b), q.a.hypot(q.b)); - let same_dir = p.a * q.a + p.b * q.b > 0.0; - let cq = if same_dir { q.c / nq } else { -q.c / nq }; - let d = (p.c / np - cq).abs(); - out.push(fact("两直线", &format!("{} \\parallel {}", p.name, q.name), "\\checkmark".into(), None, "\\frac{A_1}{A_2} = \\frac{B_1}{B_2} \\ne \\frac{C_1}{C_2}", "斜率相等")); - out.push(fact("两直线", &format!("d({}, {})", p.name, q.name), nice(d), Some(d), "\\frac{|C_1 - C_2|}{\\sqrt{A^2+B^2}}", "平行线间距离")); - par_pairs.push((i, j)); + // X(14) 2nd isogonic: skip equilateral (sin(A−π/3)=0). + let s14 = [(g[0] - pi3).sin(), (g[1] - pi3).sin(), (g[2] - pi3).sin()]; + // Guard: any near-zero sin → skip (equilateral / 60° singularity in csc form) + if s14.iter().all(|&s| s.abs() > 1e-9) { + let wa = a / s14[0]; + let wb = b / s14[1]; + let wc = c / s14[2]; + { + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let f2 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "费马点", + "F_2", + pt(f2[0], f2[1]), + None, + "F_2 = \\frac{a\\csc(A-\\pi/3)A+\\cdots}{\\sum}", + "第二等角线中心 X(14)(异于等动力点 X(16))", + ).with(Shape::Point { + x: f2[0], + y: f2[1], + label: "F2 费马".into(), + })); } - continue; } - let x = (p.c * q.b - p.b * q.c) / det; - let y = (p.a * q.c - p.c * q.a) / det; - out.push(fact("两直线", &format!("{} \\cap {}", p.name, q.name), pt(x, y), None, "\\begin{cases} A_1x + B_1y = C_1 \\\\ A_2x + B_2y = C_2 \\end{cases}", "联立方程").with(Shape::Point { x, y, label: format!("{}∩{}", p.name, q.name) })); - let dt = p.a * q.a + p.b * q.b; - if dt.abs() < 1e-12 * scale { - out.push(fact("两直线", &format!("{} \\perp {}", p.name, q.name), "\\checkmark".into(), None, "k_1 k_2 = -1", "法向量点积为 0")); - } else { - let ang = (det.abs()).atan2(dt.abs()); - out.push(fact("两直线", &format!("\\theta({}, {})", p.name, q.name), deg(ang), Some(ang), "\\tan\\theta = \\left|\\frac{k_1 - k_2}{1 + k_1k_2}\\right|", "两直线夹角")); + } + // Helper: isogonic ≠ isodynamic (X13 isogonal conjugate of X15). + if out.iter().any(|f| f.group == "费马点" && f.label == "F_1") + && out.iter().any(|f| f.group == "等动力点" && f.label.contains("S_{1}")) + { + let f1v = out.iter().find(|f| f.group == "费马点" && f.label == "F_1").map(|f| f.value.clone()); + let s1v = out.iter().find(|f| f.group == "等动力点" && f.label.contains("S_{1}")).map(|f| f.value.clone()); + if let (Some(a), Some(b)) = (f1v, s1v) { + if a != b { + out.push(fact( + "费马点", + "F_1 \\ne S_1", + "\\text{isogonic} \\ne \\text{isodynamic}".into(), + None, + "X(13)=\\text{isogonal of }X(15)", + "等角线中心与等动力点不同(互为等角共轭)", + )); + } } } } - // transversals: a third line cutting a parallel pair — equal corresponding / alternate - // interior angles, supplementary co-interior angles, and the segment cut off between - // the two parallels along the transversal. - for &(i, j) in &par_pairs { - for t in 0..ls.len() { - if t == i || t == j { - continue; + + // Equal detour X(176) / isoperimetric X(175) (inner/outer Soddy centers). + // X(176): bary sin A · (1 + sec(A/2) cos(B/2) cos(C/2)); always for non-degen △. + // X(175): same with −1; emit only when a+b+c > 4R+r. + { + let ha = g[0] / 2.0; + let hb = g[1] / 2.0; + let hc = g[2] / 2.0; + let cos_ha = ha.cos(); + let cos_hb = hb.cos(); + let cos_hc = hc.cos(); + if cos_ha.abs() > 1e-12 && cos_hb.abs() > 1e-12 && cos_hc.abs() > 1e-12 { + let t176 = [ + 1.0 + cos_hb * cos_hc / cos_ha, + 1.0 + cos_hc * cos_ha / cos_hb, + 1.0 + cos_ha * cos_hb / cos_hc, + ]; + let wa = a * t176[0]; + let wb = b * t176[1]; + let wc = c * t176[2]; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let ed = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "等迂回点", + "Ed", + pt(ed[0], ed[1]), + None, + "Ed = (\\sin A\\,(1+\\sec\\tfrac{A}{2}\\cos\\tfrac{B}{2}\\cos\\tfrac{C}{2}):\\cdots)", + "等迂回点 X(176):内苏迪圆圆心", + ).with(Shape::Point { + x: ed[0], + y: ed[1], + label: "Ed 等迂回".into(), + })); + } + // Isoperimetric / outer Soddy X(175) + if per > 4.0 * rr + r + 1e-9 { + let t175 = [ + -1.0 + cos_hb * cos_hc / cos_ha, + -1.0 + cos_hc * cos_ha / cos_hb, + -1.0 + cos_ha * cos_hb / cos_hc, + ]; + let wa = a * t175[0]; + let wb = b * t175[1]; + let wc = c * t175[2]; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let ip = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "等周点", + "Ip", + pt(ip[0], ip[1]), + None, + "Ip = (\\sin A\\,(-1+\\sec\\tfrac{A}{2}\\cos\\tfrac{B}{2}\\cos\\tfrac{C}{2}):\\cdots)", + "等周点 X(175):外苏迪圆圆心(需 a+b+c>4R+r)", + ).with(Shape::Point { + x: ip[0], + y: ip[1], + label: "Ip 等周".into(), + })); + } } - let (Some(pa), Some(pb)) = (line_isect(&ls[i], &ls[t]), line_isect(&ls[j], &ls[t])) else { continue }; - let (p, q, tr) = (&ls[i], &ls[j], &ls[t]); - let det = p.a * tr.b - p.b * tr.a; - let dt = p.a * tr.a + p.b * tr.b; - let theta = det.abs().atan2(dt.abs()); - out.push(fact("平行线的截线", &format!("\\text{{同位角}} \\; \\angle({}, {}) = \\angle({}, {})", tr.name, p.name, tr.name, q.name), deg(theta), Some(theta), "两直线平行,同位角相等", "同位角")); - out.push(fact("平行线的截线", &format!("\\text{{内错角}}({} \\mid {} \\parallel {})", tr.name, p.name, q.name), deg(theta), Some(theta), "两直线平行,内错角相等", "内错角")); - out.push(fact("平行线的截线", &format!("\\text{{同旁内角}}({} \\mid {} \\parallel {})", tr.name, p.name, q.name), deg(PI - theta), Some(PI - theta), "两直线平行,同旁内角互补", "同旁内角")); - let cut = dist2(pa, pb); - out.push(fact("平行线的截线", &format!("\\text{{截线段}}_{{{}}}", tr.name), nice(cut), Some(cut), "|PQ| = \\sqrt{\\Delta x^2 + \\Delta y^2}", "沿截线夹在两平行线之间的线段").with(Shape::Segment { x1: pa[0], y1: pa[1], x2: pb[0], y2: pb[1], label: format!("{} 截 {}", tr.name, p.name) })); } } - // Intercept theorem: three (or more) parallels cut by two transversals give proportional - // segments (平行线分线段成比例); two parallels cut by two transversals give similar - // triangles at the point where the transversals meet. - let dist_key = |l: &NamedLine| { - let n = l.a.hypot(l.b); - if n < 1e-12 { - return 0.0; + + // Moses point X(75): isotomic conjugate of the incenter; bary (bc : ca : ab). + { + let wa = b * c; + let wb = c * a; + let wc = a * b; + let den = wa + wb + wc; + if den > 1e-18 { + let mo = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "摩西点", + "Mo", + pt(mo[0], mo[1]), + None, + "Mo = \\frac{bc\\,A + ca\\,B + ab\\,C}{\\sum}", + "摩西点 X(75):内心的等截共轭", + ).with(Shape::Point { + x: mo[0], + y: mo[1], + label: "Mo 摩西".into(), + })); + out.push(fact( + "摩西点", + "Mo \\leftrightarrow I", + "\\text{等截共轭}".into(), + None, + "\\text{isotomic of }I", + "摩西点与内心互为等截共轭(bary I=a:b:c ↔ 1/a:1/b:1/c)", + )); } - let sign = if l.a.abs() > 1e-12 { l.a.signum() } else { l.b.signum() }; - sign * l.c / n - }; - let crosses = |s: usize, p: usize| line_isect(&ls[s], &ls[p]).is_some(); - let s_name = |ix: usize| ls[ix].name.clone(); - let mut triple_count = 0usize; - 'triples: for i in 0..ls.len() { - for j in i + 1..ls.len() { - for k in j + 1..ls.len() { - if triple_count >= 2 { - break 'triples; + } + + // X(25): homothetic center of orthic and tangential triangles; bary a²/(b²+c²−a²). + // Denominator vanishes at a right angle — skip then. + { + let da = b * b + c * c - a * a; + let db = c * c + a * a - b * b; + let dc = a * a + b * b - c * c; + if da.abs() > 1e-12 && db.abs() > 1e-12 && dc.abs() > 1e-12 { + let wa = (a * a) / da; + let wb = (b * b) / db; + let wc = (c * c) / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x25 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "垂足切线同调中心", + "X_{25}", + pt(x25[0], x25[1]), + None, + "X(25) = \\bigl(a^2/(b^2+c^2-a^2):\\cdots\\bigr)", + "X(25):垂足三角形与切线三角形的同调中心", + ).with(Shape::Point { + x: x25[0], + y: x25[1], + label: "X25 同调".into(), + })); + } + } + } + + // X(55)/X(56): internal/external centers of similitude of circumcircle (O) and incircle (I). + // X(55) = (r O + R I)/(R+r) divides OI internally in R:r; bary a²(b+c−a). + // X(56) = (R I − r O)/(R−r) divides OI externally in R:r; bary a²/(b+c−a). + { + let sa = b + c - a; + let sb = c + a - b; + let sc = a + b - c; + if sa > 1e-12 && sb > 1e-12 && sc > 1e-12 { + let sum_rr = rr + r; + if sum_rr > 1e-12 { + let s55 = [ + (r * circ[0] + rr * inc[0]) / sum_rr, + (r * circ[1] + rr * inc[1]) / sum_rr, + ]; + // Cross-check bary a²(b+c−a) + let wa = a * a * sa; + let wb = b * b * sb; + let wc = c * c * sc; + let den = wa + wb + wc; + if den > 1e-12 { + let s55b = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + let s55u = if dist2(s55, s55b) < 1e-5 * (1.0 + dist2(circ, inc)) { + s55 + } else { + s55b + }; + out.push(fact( + "相似中心", + "S_{+}", + pt(s55u[0], s55u[1]), + None, + "S_{+} = \\dfrac{r\\,O + R\\,I}{R+r}", + "内相似中心 X(55):外接圆与内切圆的内相似中心", + ).with(Shape::Point { + x: s55u[0], + y: s55u[1], + label: "S+ X55".into(), + })); + let d_o = dist2(s55u, circ); + let d_i = dist2(s55u, inc); + if d_i > 1e-12 { + let ratio = d_o / d_i; + let expect = rr / r; + if (ratio - expect).abs() < 1e-4 * (1.0 + expect) { + out.push(fact( + "相似中心", + "OS_{+}:S_{+}I", + format!("{}:{}", nice(rr), nice(r)), + Some(expect), + "OS_{+}:S_{+}I = R:r", + "内相似中心按 R:r 内分 OI", + )); + } + } } - let is_par = |x: usize, y: usize| par_pairs.contains(&(x.min(y), x.max(y))); - if !(is_par(i, j) && is_par(i, k)) { - continue; + } + let diff_rr = rr - r; + if diff_rr > 1e-12 { + let s56 = [ + (-r * circ[0] + rr * inc[0]) / diff_rr, + (-r * circ[1] + rr * inc[1]) / diff_rr, + ]; + let wa = (a * a) / sa; + let wb = (b * b) / sb; + let wc = (c * c) / sc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let s56b = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + let s56u = if dist2(s56, s56b) < 1e-5 * (1.0 + dist2(circ, inc)) { + s56 + } else { + s56b + }; + out.push(fact( + "相似中心", + "S_{-}", + pt(s56u[0], s56u[1]), + None, + "S_{-} = \\dfrac{R\\,I - r\\,O}{R-r}", + "外相似中心 X(56):外接圆与内切圆的外相似中心", + ).with(Shape::Point { + x: s56u[0], + y: s56u[1], + label: "S- X56".into(), + })); + // O, I, S+, S− collinear on the OI line + let cross = (inc[0] - circ[0]) * (s56u[1] - circ[1]) + - (inc[1] - circ[1]) * (s56u[0] - circ[0]); + let scale = (1.0 + dist2(circ, inc)) * (1.0 + dist2(circ, s56u)); + if cross.abs() < 1e-5 * scale { + out.push(fact( + "相似中心", + "O,\\ I,\\ S_{+},\\ S_{-}", + "\\text{共线}".into(), + None, + "S_{\\pm}\\in OI", + "内/外相似中心均在 OI 线上(互为关于 O,I 的调和共轭)", + )); + } } - // order the three parallels along their common normal - let mut trio = [i, j, k]; - trio.sort_by(|x, y| dist_key(&ls[*x]).partial_cmp(&dist_key(&ls[*y])).unwrap()); - let (p, q, r) = (s_name(trio[0]), s_name(trio[1]), s_name(trio[2])); - let mut tcount = 0usize; - 'ts: for s in 0..ls.len() { - for t in s + 1..ls.len() { - if tcount >= 2 { - break 'ts; - } - if trio.contains(&s) || trio.contains(&t) || !crosses(s, trio[0]) || !crosses(s, trio[2]) || !crosses(t, trio[0]) || !crosses(t, trio[2]) { - continue; - } - if line_isect(&ls[s], &ls[t]).is_none() { - continue; // the two transversals must meet somewhere - } - let ps: Vec<[f64; 2]> = trio.iter().map(|&ix| line_isect(&ls[s], &ls[ix]).unwrap()).collect(); - let qt: Vec<[f64; 2]> = trio.iter().map(|&ix| line_isect(&ls[t], &ls[ix]).unwrap()).collect(); - let (l1, l2) = (dist2(ps[0], ps[1]), dist2(ps[1], ps[2])); - let (m1, m2) = (dist2(qt[0], qt[1]), dist2(qt[1], qt[2])); - if l1 * l2 * m1 * m2 < 1e-12 { - continue; + } + } + } + + // X(30) Euler infinity point: Euler line ∩ line at infinity (direction of OH). + // Emit unit direction when |OH| is non-degenerate; bary sum of g(a,b,c) is 0. + { + let ohx = orth[0] - circ[0]; + let ohy = orth[1] - circ[1]; + let oh_len = (ohx * ohx + ohy * ohy).sqrt(); + if oh_len > 1e-9 { + let ux = ohx / oh_len; + let uy = ohy / oh_len; + out.push(fact( + "欧拉无穷远点", + "X_{30}", + format!("({}, {})", nice(ux), nice(uy)), + None, + "X(30) = \\text{Euler}\\cap\\ell_{\\infty}", + "欧拉无穷远点:欧拉线与无穷远直线的交点(方向向量)", + )); + out.push(fact( + "欧拉无穷远点", + "\\overrightarrow{OH}/|OH|", + format!("({}, {})", nice(ux), nice(uy)), + None, + "\\parallel\\overrightarrow{OH}", + "欧拉线方向单位向量(X(30) 的仿射代表)", + )); + // Bary g = 2a⁴ − (b²−c²)² − a²(b²+c²); homogeneous sum ≈ 0 at infinity. + let g_a = { + let a2 = a * a; + let b2 = b * b; + let c2 = c * c; + 2.0 * a2 * a2 - (b2 - c2) * (b2 - c2) - a2 * (b2 + c2) + }; + let g_b = { + let a2 = b * b; + let b2 = c * c; + let c2 = a * a; + 2.0 * a2 * a2 - (b2 - c2) * (b2 - c2) - a2 * (b2 + c2) + }; + let g_c = { + let a2 = c * c; + let b2 = a * a; + let c2 = b * b; + 2.0 * a2 * a2 - (b2 - c2) * (b2 - c2) - a2 * (b2 + c2) + }; + let gsum = g_a + g_b + g_c; + if gsum.abs() < 1e-6 * (1.0 + g_a.abs() + g_b.abs() + g_c.abs()) { + out.push(fact( + "欧拉无穷远点", + "\\sum g(a,b,c)", + "0".into(), + Some(0.0), + "g=2a^4-(b^2-c^2)^2-a^2(b^2+c^2)", + "X(30) 重心坐标之和为 0(无穷远点判据)", + )); + } + } + } + + // X(57): isogonal conjugate of Mittenpunkt X(9); perspector of intouch & excentral. + // Bary a/(b+c−a) = a/(2(s−a)). + { + let sa = b + c - a; + let sb = c + a - b; + let sc = a + b - c; + if sa > 1e-12 && sb > 1e-12 && sc > 1e-12 { + let wa = a / sa; + let wb = b / sb; + let wc = c / sc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x57 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "米滕等角共轭点", + "X_{57}", + pt(x57[0], x57[1]), + None, + "X(57) = \\bigl(a/(b+c-a):\\cdots\\bigr)", + "X(57):米滕点的等角共轭(接触△与旁心△的透视中心)", + ).with(Shape::Point { + x: x57[0], + y: x57[1], + label: "X57".into(), + })); + out.push(fact( + "米滕等角共轭点", + "X_{57} \\leftrightarrow Mi", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(9)", + "X(57) 与米滕点 X(9) 互为等角共轭", + )); + } + } + } + + // X(58): isogonal conjugate of Spieker X(10); bary a²/(b+c). + { + let wa = (a * a) / (b + c); + let wb = (b * b) / (c + a); + let wc = (c * c) / (a + b); + let den = wa + wb + wc; + if den > 1e-12 { + let x58 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "斯皮克等角共轭点", + "X_{58}", + pt(x58[0], x58[1]), + None, + "X(58) = \\bigl(a^2/(b+c):\\cdots\\bigr)", + "X(58):斯皮克中心的等角共轭", + ).with(Shape::Point { + x: x58[0], + y: x58[1], + label: "X58".into(), + })); + out.push(fact( + "斯皮克等角共轭点", + "X_{58} \\leftrightarrow Sp", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(10)", + "X(58) 与斯皮克中心 X(10) 互为等角共轭", + )); + } + } + + // X(84): isogonal conjugate of Bevan X(40); perspector of hexyl triangle. + // Bary a / (cos B + cos C − cos A − 1); skip when any denominator ≈ 0. + { + let ca = g[0].cos(); + let cb = g[1].cos(); + let cc = g[2].cos(); + let da = cb + cc - ca - 1.0; + let db = cc + ca - cb - 1.0; + let dc = ca + cb - cc - 1.0; + if da.abs() > 1e-12 && db.abs() > 1e-12 && dc.abs() > 1e-12 { + let wa = a / da; + let wb = b / db; + let wc = c / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x84 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "己基透视中心", + "X_{84}", + pt(x84[0], x84[1]), + None, + "X(84) = \\bigl(a/(\\cos B+\\cos C-\\cos A-1):\\cdots\\bigr)", + "X(84):贝万点的等角共轭(己基三角形透视中心)", + ).with(Shape::Point { + x: x84[0], + y: x84[1], + label: "X84".into(), + })); + // Cross-check: isogonal of Be=2O−I via bary of Be then a²/x. + let be = [2.0 * circ[0] - inc[0], 2.0 * circ[1] - inc[1]]; + // signed areas for bary of Be + let area2 = |p: [f64; 2], q: [f64; 2], r: [f64; 2]| { + (q[0] - p[0]) * (r[1] - p[1]) - (q[1] - p[1]) * (r[0] - p[0]) + }; + let aa = area2(be, pb, pc); + let bb = area2(pa, be, pc); + let cc_ = area2(pa, pb, be); + let ssum = aa + bb + cc_; + if ssum.abs() > 1e-12 && aa.abs() > 1e-14 && bb.abs() > 1e-14 && cc_.abs() > 1e-14 { + let wa = (a * a) / aa; + let wb = (b * b) / bb; + let wc = (c * c) / cc_; + let den2 = wa + wb + wc; + if den2.abs() > 1e-12 { + let iso = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den2, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den2, + ]; + if dist2(iso, x84) < 1e-4 * (1.0 + dist2(circ, orth)) { + out.push(fact( + "己基透视中心", + "X_{84} \\leftrightarrow Be", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(40)", + "X(84) 与贝万点 X(40) 互为等角共轭", + )); } - tcount += 1; - let sn = s_name(s); - let tn = s_name(t); - let seg = |x: &[f64; 2], y: &[f64; 2], nm: String| Shape::Segment { x1: x[0], y1: x[1], x2: y[0], y2: y[1], label: nm }; - out.push(seg_fact("平行线分线段", &format!("|{sn} \\cap {p},\\ {sn} \\cap {q}|"), l1, "\\sqrt{\\Delta x^2 + \\Delta y^2}", "第一条截线段").with(seg(&ps[0], &ps[1], format!("{sn} 上的截线段")))); - out.push(seg_fact("平行线分线段", &format!("|{sn} \\cap {q},\\ {sn} \\cap {r}|"), l2, "\\sqrt{\\Delta x^2 + \\Delta y^2}", "第二条截线段").with(seg(&ps[1], &ps[2], format!("{sn} 上的截线段")))); - out.push(seg_fact("平行线分线段", &format!("|{tn} \\cap {p},\\ {tn} \\cap {q}|"), m1, "\\sqrt{\\Delta x^2 + \\Delta y^2}", "对应的第一条截线段").with(seg(&qt[0], &qt[1], format!("{tn} 上的截线段")))); - out.push(seg_fact("平行线分线段", &format!("|{tn} \\cap {q},\\ {tn} \\cap {r}|"), m2, "\\sqrt{\\Delta x^2 + \\Delta y^2}", "对应的第二条截线段").with(seg(&qt[1], &qt[2], format!("{tn} 上的截线段")))); - out.push(fact("平行线分线段", "\\text{成比例}", nice(l1 / l2), Some(l1 / l2), &format!("\\frac{{|{sn} \\cap {p},\\ {sn} \\cap {q}|}}{{|{sn} \\cap {q},\\ {sn} \\cap {r}|}} = \\frac{{|{tn} \\cap {p},\\ {tn} \\cap {q}|}}{{|{tn} \\cap {q},\\ {tn} \\cap {r}|}} = {}", nice(m1 / m2)), "平行线分线段成比例定理:三条平行线在两条截线上截出的线段对应成比例")); } } - triple_count += 1; } } } - // similar triangles: two parallels + two transversals meeting at X - let mut sim_count = 0usize; - 'sims: for &(i, j) in &par_pairs { - for s in 0..ls.len() { - for t in s + 1..ls.len() { - if sim_count >= 2 { - break 'sims; + + // X(12): harmonic conjugate of Feuerbach X(11) w.r.t. I,N (etc.); bary a(1+cos(B−C)). + // X(59): isogonal of Feuerbach X(11); bary a/(1−cos(B−C)); skip isosceles (den=0). + // X(60): isogonal of X(12); bary a/(1+cos(B−C)). + { + let c_bc = (g[1] - g[2]).cos(); + let c_ca = (g[2] - g[0]).cos(); + let c_ab = (g[0] - g[1]).cos(); + // X(12) + { + let wa = a * (1.0 + c_bc); + let wb = b * (1.0 + c_ca); + let wc = c * (1.0 + c_ab); + let den = wa + wb + wc; + if den > 1e-12 { + let x12 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "费尔巴哈调和共轭点", + "X_{12}", + pt(x12[0], x12[1]), + None, + "X(12) = \\bigl(a(1+\\cos(B-C)):\\cdots\\bigr)", + "X(12):费尔巴哈点的调和共轭(九点圆与旁切圆切点三角形透视中心)", + ).with(Shape::Point { + x: x12[0], + y: x12[1], + label: "X12".into(), + })); + } + } + // X(60) = isogonal of X(12) + { + let da = 1.0 + c_bc; + let db = 1.0 + c_ca; + let dc = 1.0 + c_ab; + if da > 1e-12 && db > 1e-12 && dc > 1e-12 { + let wa = a / da; + let wb = b / db; + let wc = c / dc; + let den = wa + wb + wc; + if den > 1e-12 { + let x60 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "X12等角共轭点", + "X_{60}", + pt(x60[0], x60[1]), + None, + "X(60) = \\bigl(a/(1+\\cos(B-C)):\\cdots\\bigr)", + "X(60):X(12) 的等角共轭", + ).with(Shape::Point { + x: x60[0], + y: x60[1], + label: "X60".into(), + })); + out.push(fact( + "X12等角共轭点", + "X_{60} \\leftrightarrow X_{12}", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(12)", + "X(60) 与 X(12) 互为等角共轭", + )); } - if s == i || s == j || t == i || t == j { - continue; + } + } + // X(59) = isogonal of Feuerbach Fe X(11) + { + let da = 1.0 - c_bc; + let db = 1.0 - c_ca; + let dc = 1.0 - c_ab; + if da > 1e-12 && db > 1e-12 && dc > 1e-12 { + let wa = a / da; + let wb = b / db; + let wc = c / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x59 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "费尔巴哈等角共轭点", + "X_{59}", + pt(x59[0], x59[1]), + None, + "X(59) = \\bigl(a/(1-\\cos(B-C)):\\cdots\\bigr)", + "X(59):费尔巴哈点的等角共轭", + ).with(Shape::Point { + x: x59[0], + y: x59[1], + label: "X59".into(), + })); + // Cross-check vs isogonal of geometric Fe when available. + let n9 = [(circ[0] + orth[0]) / 2.0, (circ[1] + orth[1]) / 2.0]; + let r9 = rr / 2.0; + let den_f = r9 - r; + let ni_len = dist2(n9, inc); + if den_f.abs() > 1e-9 && ni_len > 1e-9 { + let fe = [ + (r9 * inc[0] - r * n9[0]) / den_f, + (r9 * inc[1] - r * n9[1]) / den_f, + ]; + if (dist2(n9, fe) - r9).abs() < 1e-4 * (1.0 + r9) + && (dist2(inc, fe) - r).abs() < 1e-4 * (1.0 + r) + { + let area2 = |p: [f64; 2], q: [f64; 2], r: [f64; 2]| { + (q[0] - p[0]) * (r[1] - p[1]) - (q[1] - p[1]) * (r[0] - p[0]) + }; + let aa = area2(fe, pb, pc); + let bb = area2(pa, fe, pc); + let cc_ = area2(pa, pb, fe); + let ssum = aa + bb + cc_; + if ssum.abs() > 1e-12 + && aa.abs() > 1e-14 + && bb.abs() > 1e-14 + && cc_.abs() > 1e-14 + { + let wa = (a * a) / aa; + let wb = (b * b) / bb; + let wc = (c * c) / cc_; + let den2 = wa + wb + wc; + if den2.abs() > 1e-12 { + let iso = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den2, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den2, + ]; + if dist2(iso, x59) < 1e-4 * (1.0 + dist2(circ, orth)) { + out.push(fact( + "费尔巴哈等角共轭点", + "X_{59} \\leftrightarrow Fe", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(11)", + "X(59) 与费尔巴哈点 X(11) 互为等角共轭", + )); + } + } + } + } + } } - let (Some(x), Some(a), Some(c)) = (line_isect(&ls[s], &ls[t]), line_isect(&ls[s], &ls[i]), line_isect(&ls[s], &ls[j])) else { continue }; - let (Some(b), Some(d)) = (line_isect(&ls[t], &ls[i]), line_isect(&ls[t], &ls[j])) else { continue }; - let (k1n, k1d) = (dist2(x, a), dist2(x, c)); - let (k2n, k2d) = (dist2(x, b), dist2(x, d)); - if k1d < 1e-9 || k2d < 1e-9 || k1n < 1e-9 || k2n < 1e-9 { - continue; // transversals meet on one of the parallels + } + } + } + + // Grinberg X(37): crosspoint of I and G; bary a(b+c). Complement of Moses X(75). + // X(81): isogonal of Grinberg; bary a/(b+c). + // X(42): crosspoint of I and K (Lemoine); bary a²(b+c). + { + let wa = a * (b + c); + let wb = b * (c + a); + let wc = c * (a + b); + let den = wa + wb + wc; + if den > 1e-12 { + let gr = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "格林伯格点", + "Gr", + pt(gr[0], gr[1]), + None, + "Gr = \\frac{a(b+c)A+b(c+a)B+c(a+b)C}{\\sum}", + "格林伯格点 X(37):内心与重心的截线交点(摩西点的补点)", + ).with(Shape::Point { + x: gr[0], + y: gr[1], + label: "Gr 格林伯格".into(), + })); + // Complement of Moses: Mo = bc:ca:ab → complement has bary a(b+c) = Gr. + let mwa = b * c; + let mwb = c * a; + let mwc = a * b; + let mden = mwa + mwb + mwc; + if mden > 1e-12 { + let mo = [ + (mwa * pa[0] + mwb * pb[0] + mwc * pc[0]) / mden, + (mwa * pa[1] + mwb * pb[1] + mwc * pc[1]) / mden, + ]; + // Complement map: P ↦ 3G − 2P? Kimberling complement of P with bary x:y:z + // is (y+z):(z+x):(x+y). For Mo=bc:ca:ab, complement = (ca+ab):(ab+bc):(bc+ca) + // = a(b+c):b(c+a):c(a+b) = Gr. Verify Cartesian: midpoint? Actually + // complement C(P) satisfies G divides P C(P) with PG:G C(P) = 2:1 toward C(P)? + // Standard: complement of P is the point Q such that G = (2P+Q)/3? No — + // In ETC, complement of P is anticomplement of P mapped... + // Vectorially often Q = 3G − 2P? Let's check: if Q = 3G-2P then G = (2P+Q)/3 means G divides PQ with PG:GQ = 1:2? + // Actually Kimberling: the complement of P is the image under the homothety h(G,−1/2). + // So Q − G = (−1/2)(P − G) ⇒ Q = G − (1/2)(P−G) = (3G − P)/2? + // Homothety h(G,k): Q = G + k(P−G). + // Complement = h(G, −1/2): Q = G − (1/2)(P−G) = (3/2)G − (1/2)P = (3G−P)/2. + // Anticomplement = h(G, −2): Q = 3G − 2P. + // ETC: X(37) = complement of X(75). So Gr = (3G − Mo)/2. + let q = [ + (3.0 * centroid[0] - mo[0]) / 2.0, + (3.0 * centroid[1] - mo[1]) / 2.0, + ]; + if dist2(q, gr) < 1e-5 * (1.0 + dist2(gr, centroid)) { + out.push(fact( + "格林伯格点", + "Gr = \\text{complement}(Mo)", + "\\checkmark".into(), + None, + "Gr = \\tfrac{3G - Mo}{2}", + "格林伯格点是摩西点关于重心的补点(位似比 −1/2)", + )); } - sim_count += 1; - let k1 = k1n / k1d; - let k2 = k2n / k2d; - let value = if near(k1, k2) { nice(k1) } else { format!("{} \\approx {}", nice(k1), nice(k2)) }; - let (pn, qn, sn, tn) = (ls[i].name.clone(), ls[j].name.clone(), ls[s].name.clone(), ls[t].name.clone()); - out.push(fact("相似三角形", &format!("\\text{{相似比}}_{{{}}}", pn), value, Some(k1), &format!("\\frac{{|X({sn} \\cap {pn})|}}{{|X({sn} \\cap {qn})|}} = \\frac{{|X({tn} \\cap {pn})|}}{{|X({tn} \\cap {qn})|}} = {}", nice(k1)), "平行于三角形一边的直线截其他两边,所得三角形与原三角形相似(X = 两截线的交点)")); + } + } + // X(81) isogonal of Grinberg + { + let wa = a / (b + c); + let wb = b / (c + a); + let wc = c / (a + b); + let den = wa + wb + wc; + if den > 1e-12 { + let x81 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "格林伯格等角共轭点", + "X_{81}", + pt(x81[0], x81[1]), + None, + "X(81) = \\bigl(a/(b+c):\\cdots\\bigr)", + "X(81):格林伯格点的等角共轭", + ).with(Shape::Point { + x: x81[0], + y: x81[1], + label: "X81".into(), + })); + out.push(fact( + "格林伯格等角共轭点", + "X_{81} \\leftrightarrow Gr", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(37)", + "X(81) 与格林伯格点 X(37) 互为等角共轭", + )); + } + } + // X(42) crosspoint of I and K + { + let wa = a * a * (b + c); + let wb = b * b * (c + a); + let wc = c * c * (a + b); + let den = wa + wb + wc; + if den > 1e-12 { + let x42 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "内心勒莫恩截点", + "X_{42}", + pt(x42[0], x42[1]), + None, + "X(42) = \\bigl(a^2(b+c):\\cdots\\bigr)", + "X(42):内心与勒莫恩点的截线交点", + ).with(Shape::Point { + x: x42[0], + y: x42[1], + label: "X42".into(), + })); } } } - out -} -// --------------------------------------------------------------------------- -// Planes (a x + b y + c z = d) + // X(65): orthocenter of the intouch triangle; isogonal of Schiffler X(21). + // Bary a(b+c)/(b+c−a). Also perspector of ABC and the extangents triangle. + { + let sa = b + c - a; + let sb = c + a - b; + let sc = a + b - c; + if sa > 1e-12 && sb > 1e-12 && sc > 1e-12 { + let wa = a * (b + c) / sa; + let wb = b * (c + a) / sb; + let wc = c * (a + b) / sc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x65 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "接触垂心", + "X_{65}", + pt(x65[0], x65[1]), + None, + "X(65) = \\bigl(a(b+c)/(b+c-a):\\cdots\\bigr)", + "X(65):接触三角形的垂心(席弗勒点的等角共轭)", + ).with(Shape::Point { + x: x65[0], + y: x65[1], + label: "X65 接触垂心".into(), + })); + // Geometric: orthocenter of intouch feet X_a,X_b,X_c. + let s_a = p - a; + let s_b = p - b; + let s_c = p - c; + if s_a > 1e-12 && s_b > 1e-12 && s_c > 1e-12 { + let xa = section_point(pb, pc, s_b, s_c); + let xb = section_point(pc, pa, s_c, s_a); + let xc = section_point(pa, pb, s_a, s_b); + if let Some((o_in, _)) = circum2(xa, xb, xc) { + let h_in = [ + xa[0] + xb[0] + xc[0] - 2.0 * o_in[0], + xa[1] + xb[1] + xc[1] - 2.0 * o_in[1], + ]; + if dist2(h_in, x65) < 1e-4 * (1.0 + dist2(circ, orth)) { + out.push(fact( + "接触垂心", + "H(X_a X_b X_c) = X_{65}", + "\\checkmark".into(), + None, + "H_{\\text{intouch}} = X(65)", + "接触三角形的垂心与 X(65) 重合", + )); + } + } + } + out.push(fact( + "接触垂心", + "X_{65} \\leftrightarrow Sc", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(21)", + "X(65) 与席弗勒点 X(21) 互为等角共轭", + )); + } + } + } -#[derive(Deserialize)] -pub struct NamedPlane { - pub name: String, - pub n: [f64; 3], - pub d: f64, -} + // X(76): 3rd Brocard point; bary 1/a²; isotomic conjugate of Lemoine K = X(6). + { + let wa = 1.0 / (a * a); + let wb = 1.0 / (b * b); + let wc = 1.0 / (c * c); + let den = wa + wb + wc; + if den > 1e-12 { + let x76 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "第三布罗卡点", + "X_{76}", + pt(x76[0], x76[1]), + None, + "X(76) = \\bigl(1/a^2:1/b^2:1/c^2\\bigr)", + "第三布罗卡点 X(76):勒莫恩点的等截共轭", + ).with(Shape::Point { + x: x76[0], + y: x76[1], + label: "X76 布罗卡".into(), + })); + out.push(fact( + "第三布罗卡点", + "X_{76} \\leftrightarrow K", + "\\text{等截共轭}".into(), + None, + "\\text{isotomic of }X(6)", + "X(76) 与勒莫恩点 X(6) 互为等截共轭", + )); + } + } -pub fn planes_analyze(ps: &[NamedPlane], points: &[NamedPoint]) -> Vec { - let mut out = Vec::new(); - for p in ps { - let ln = len3(p.n); - out.push(fact("平面", &format!("\\vec n_{{{}}}", p.name), pt3(p.n), None, "ax + by + cz = d \\Rightarrow \\vec n = (a,b,c)", "法向量").with(Shape::Arrow3 { x: p.n[0], y: p.n[1], z: p.n[2], label: format!("n_{}", p.name) })); - let d0 = p.d.abs() / ln; - out.push(fact("平面", &format!("d(O, {})", p.name), nice(d0), Some(d0), "\\frac{|d|}{|\\vec n|}", "原点到平面的距离")); - let mut ints = Vec::new(); - for (k, ax) in ["x", "y", "z"].iter().enumerate() { - if !near(p.n[k], 0.0) { - ints.push(format!("{}={}", ax, nice_short(p.d / p.n[k]))); - } + // X(86): cevapoint of I and G; isogonal of X(42); isotomic of Spieker. + // Bary 1/(b+c). + { + let wa = 1.0 / (b + c); + let wb = 1.0 / (c + a); + let wc = 1.0 / (a + b); + let den = wa + wb + wc; + if den > 1e-12 { + let x86 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "内心重心截点", + "X_{86}", + pt(x86[0], x86[1]), + None, + "X(86) = \\bigl(1/(b+c):\\cdots\\bigr)", + "X(86):内心与重心的截点(X(42) 的等角共轭;斯皮克的等截共轭)", + ).with(Shape::Point { + x: x86[0], + y: x86[1], + label: "X86".into(), + })); + out.push(fact( + "内心重心截点", + "X_{86} \\leftrightarrow X_{42}", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(42)", + "X(86) 与 X(42) 互为等角共轭", + )); + out.push(fact( + "内心重心截点", + "X_{86} \\leftrightarrow Sp", + "\\text{等截共轭}".into(), + None, + "\\text{isotomic of }X(10)", + "X(86) 与斯皮克中心 X(10) 互为等截共轭", + )); } - if !ints.is_empty() { - out.push(fact("平面", &format!("\\text{{截距 }}{}", p.name), ints.join(",\\ "), None, "", "令另外两个坐标为 0")); + } + + // X(31) 2nd power point: bary a³; isogonal conjugate of Moses X(75). + // X(32) 3rd power point: bary a⁴; isogonal conjugate of 3rd Brocard X(76). + { + let wa = a * a * a; + let wb = b * b * b; + let wc = c * c * c; + let den = wa + wb + wc; + if den > 1e-12 { + let x31 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "第二幂点", + "X_{31}", + pt(x31[0], x31[1]), + None, + "X(31) = (a^3:b^3:c^3)", + "第二幂点 X(31):摩西点的等角共轭", + ).with(Shape::Point { + x: x31[0], + y: x31[1], + label: "X31 幂点".into(), + })); + out.push(fact( + "第二幂点", + "X_{31} \\leftrightarrow Mo", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(75)", + "X(31) 与摩西点 X(75) 互为等角共轭", + )); } - for q in points { - let x = [q.p[0], *q.p.get(1).unwrap_or(&0.0), *q.p.get(2).unwrap_or(&0.0)]; - let d = (dot3(p.n, x) - p.d).abs() / ln; - out.push(fact("点与平面", &format!("d({}, {})", q.name, p.name), nice(d), Some(d), "\\frac{|ax_0+by_0+cz_0-d|}{\\sqrt{a^2+b^2+c^2}}", "点到平面距离")); + let wa = a * a * a * a; + let wb = b * b * b * b; + let wc = c * c * c * c; + let den = wa + wb + wc; + if den > 1e-12 { + let x32 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "第三幂点", + "X_{32}", + pt(x32[0], x32[1]), + None, + "X(32) = (a^4:b^4:c^4)", + "第三幂点 X(32):第三布罗卡点的等角共轭", + ).with(Shape::Point { + x: x32[0], + y: x32[1], + label: "X32 幂点".into(), + })); + out.push(fact( + "第三幂点", + "X_{32} \\leftrightarrow X_{76}", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(76)", + "X(32) 与第三布罗卡点 X(76) 互为等角共轭", + )); } } - for i in 0..ps.len() { - for j in i + 1..ps.len() { - let (p, q) = (&ps[i], &ps[j]); - let dirv = cross3(p.n, q.n); - let (lp, lq) = (len3(p.n), len3(q.n)); - if len3(dirv) < 1e-9 * lp * lq { - let f = dot3(p.n, q.n).signum() * lq / lp; - let d = (q.d - p.d * f).abs() / lq; - out.push(fact("两平面", &format!("{} \\parallel {}", p.name, q.name), if d < 1e-9 { "\\text{重合}".into() } else { "\\checkmark".into() }, None, "\\vec n_1 \\parallel \\vec n_2", "法向量共线")); - if d > 1e-9 { - out.push(fact("两平面", &format!("d({}, {})", p.name, q.name), nice(d), Some(d), "", "平行平面间距离")); + + // X(69): bary cot A = b²+c²−a²; isotomic of H; isogonal of X(25). + // Right △: the right-angle component vanishes (point on opposite side) — still emit. + { + let wa = b * b + c * c - a * a; + let wb = c * c + a * a - b * b; + let wc = a * a + b * b - c * c; + let den = wa + wb + wc; + // den = a²+b²+c² > 0 always for a non-degen △ + if den.abs() > 1e-12 { + let x69 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "余切点", + "X_{69}", + pt(x69[0], x69[1]), + None, + "X(69) = (\\cot A:\\cot B:\\cot C) = (b^2+c^2-a^2:\\cdots)", + "X(69):垂心的等截共轭(亦为 X(25) 的等角共轭)", + ).with(Shape::Point { + x: x69[0], + y: x69[1], + label: "X69".into(), + })); + out.push(fact( + "余切点", + "X_{69} \\leftrightarrow H", + "\\text{等截共轭}".into(), + None, + "\\text{isotomic of }X(4)", + "X(69) 与垂心互为等截共轭", + )); + // Cross-check isotomic of H: H bary (tan A or cos? H has bary tan A? + // Actually H bary = tan A : tan B : tan C = 1/cot = a²/(b²+c²-a²)? + // Standard H bary = tan A : … = a²/(b²+c²−a²) wait no: + // H trilinear = sec A? Standard: H bary = tan A : tan B : tan C, or + // (bc[b²+c²−a²])? Actually common: H = (a²(b²+c²−a²))? No — + // Orthocenter barycentrics: tan A : tan B : tan C, equivalently + // a²(b²+c²−a²) is wrong. Simple: tan A = sin/cos, and cos A = (b²+c²−a²)/(2bc), + // so tan A ∝ sin A / (b²+c²−a²) ∝ a / (b²+c²−a²). + // Bary of H = a² / (b²+c²−a²)? Trilinear sec A, bary a sec A = a/cos A ∝ a²/(b²+c²−a²). + // Isotomic of H: if H has bary x:y:z = a²/(b²+c²−a²) : …, then isotomic 1/x ∝ (b²+c²−a²)/a², + // times a² gives (b²+c²−a²) — matches X(69). Good — note only. + } + } + + // X(36): inverse of the incenter in the circumcircle; skip when I=O (equilateral). + // Geometric: X36 = O + R²(I−O)/|OI|². Barycentrics sin A − sin 2A (ETC). + { + let oi2 = { + let dx = inc[0] - circ[0]; + let dy = inc[1] - circ[1]; + dx * dx + dy * dy + }; + if oi2 > 1e-12 { + let scale = (rr * rr) / oi2; + let x36 = [ + circ[0] + scale * (inc[0] - circ[0]), + circ[1] + scale * (inc[1] - circ[1]), + ]; + // Cross-check ETC bary sin A − sin 2A + let wa = g[0].sin() - (2.0 * g[0]).sin(); + let wb = g[1].sin() - (2.0 * g[1]).sin(); + let wc = g[2].sin() - (2.0 * g[2]).sin(); + let den = wa + wb + wc; + let x36u = if den.abs() > 1e-12 { + let xb = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if dist2(xb, x36) < 1e-4 * (1.0 + rr) { + xb + } else { + x36 + } + } else { + x36 + }; + out.push(fact( + "反演内心", + "X_{36}", + pt(x36u[0], x36u[1]), + None, + "X(36) = O + R^2(I-O)/|OI|^2", + "X(36):内心关于外接圆的反演点", + ).with(Shape::Point { + x: x36u[0], + y: x36u[1], + label: "X36 反演I".into(), + })); + // |O X36| · |OI| = R² + let d_ox = dist2(circ, x36u); + let d_oi = oi2.sqrt(); + if d_oi > 1e-12 { + let prod = d_ox * d_oi; + if (prod - rr * rr).abs() < 1e-4 * (1.0 + rr * rr) { + out.push(fact( + "反演内心", + "|OX_{36}|\\cdot|OI|", + nice(rr * rr), + Some(rr * rr), + "|OX_{36}|\\cdot|OI| = R^2", + "反演定义:乘积等于外接圆半径平方", + )); } - continue; } - let ang = (dot3(p.n, q.n).abs() / (lp * lq)).clamp(-1.0, 1.0).acos(); - out.push(fact("两平面", &format!("\\theta({}, {})", p.name, q.name), deg(ang), Some(ang), "\\cos\\theta = \\frac{|\\vec n_1\\cdot\\vec n_2|}{|\\vec n_1||\\vec n_2|}", "二面角(锐角)")); - if dot3(p.n, q.n).abs() < 1e-9 * lp * lq { - out.push(fact("两平面", &format!("{} \\perp {}", p.name, q.name), "\\checkmark".into(), None, "\\vec n_1\\cdot\\vec n_2 = 0", "法向量垂直")); + // Collinear O, I, X36 + let cross = (inc[0] - circ[0]) * (x36u[1] - circ[1]) + - (inc[1] - circ[1]) * (x36u[0] - circ[0]); + if cross.abs() < 1e-5 * (1.0 + oi2) * (1.0 + dist2(circ, x36u)) { + out.push(fact( + "反演内心", + "O,\\ I,\\ X_{36}", + "\\text{共线}".into(), + None, + "X(36)\\in OI", + "反演内心在 OI 线上", + )); } - // point on the line: least-norm solution of the 2x3 system via n1, n2 basis - let n11 = dot3(p.n, p.n); - let n22 = dot3(q.n, q.n); - let n12 = dot3(p.n, q.n); - let det = n11 * n22 - n12 * n12; - let c1 = (p.d * n22 - q.d * n12) / det; - let c2 = (q.d * n11 - p.d * n12) / det; - let p0 = [c1 * p.n[0] + c2 * q.n[0], c1 * p.n[1] + c2 * q.n[1], c1 * p.n[2] + c2 * q.n[2]]; - let p0 = p0.map(clean); - let g = gcd_scale(dirv); - out.push(fact("两平面", &format!("{} \\cap {}", p.name, q.name), format!("{} + t{}", pt3(p0), pt3(g)), None, "\\vec s = \\vec n_1 \\times \\vec n_2", "交线(参数式)").with(Shape::Line3 { px: p0[0], py: p0[1], pz: p0[2], dx: g[0], dy: g[1], dz: g[2], label: format!("{}∩{}", p.name, q.name) })); } } - if ps.len() >= 3 { - let (p, q, r) = (&ps[0], &ps[1], &ps[2]); - let m = vec![p.n.to_vec(), q.n.to_vec(), r.n.to_vec()]; - let det = dot3(p.n, cross3(q.n, r.n)); - if det.abs() > 1e-12 { - if let Some(x) = solve_linear_pub(m, vec![p.d, q.d, r.d]) { - let x = [clean(x[0]), clean(x[1]), clean(x[2])]; - out.push(fact("三平面", &format!("{} \\cap {} \\cap {}", p.name, q.name, r.name), pt3(x), None, "\\text{克拉默法则}", "三个平面交于一点").with(Shape::Point3 { x: x[0], y: x[1], z: x[2], label: "三平面交点".into() })); + + // X(41): bary a³(b+c−a); X(6)-Ceva conjugate of X(31). + { + let sa = b + c - a; + let sb = c + a - b; + let sc = a + b - c; + if sa > 1e-12 && sb > 1e-12 && sc > 1e-12 { + let wa = a * a * a * sa; + let wb = b * b * b * sb; + let wc = c * c * c * sc; + let den = wa + wb + wc; + if den > 1e-12 { + let x41 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "第二幂截点", + "X_{41}", + pt(x41[0], x41[1]), + None, + "X(41) = \\bigl(a^3(b+c-a):\\cdots\\bigr)", + "X(41):勒莫恩点对第二幂点的 Ceva 共轭", + ).with(Shape::Point { + x: x41[0], + y: x41[1], + label: "X41".into(), + })); } - } else { - out.push(fact("三平面", "\\text{交点}", "\\text{不唯一}".into(), None, "\\det = 0", "法向量共面:交于一线或无公共点")); } } - out -} -/// Scale a direction vector to small integers when possible. -fn gcd_scale(v: [f64; 3]) -> [f64; 3] { - let m = v.iter().fold(0.0f64, |a, b| a.max(b.abs())); - if m == 0.0 { - return v; - } - for k in 1..=12 { - let s = v.map(|x| x / m * k as f64); - if s.iter().all(|x| (x - x.round()).abs() < 1e-9) { - let ints: Vec = s.iter().map(|x| x.round() as i64).collect(); - let g = ints.iter().fold(0i64, |a, &b| gcd(a, b.abs())); - let g = g.max(1) as f64; - let sign = if ints.iter().find(|x| **x != 0).map_or(1, |x| x.signum()) < 0 { -1.0 } else { 1.0 }; - return s.map(|x| clean(sign * x.round() / g)); + // X(48): crosspoint of I and X(63); bary a sin 2A. + { + let wa = a * (2.0 * g[0]).sin(); + let wb = b * (2.0 * g[1]).sin(); + let wc = c * (2.0 * g[2]).sin(); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x48 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "双倍角截点", + "X_{48}", + pt(x48[0], x48[1]), + None, + "X(48) = (a\\sin 2A:b\\sin 2B:c\\sin 2C)", + "X(48):内心与 X(63) 的截线交点", + ).with(Shape::Point { + x: x48[0], + y: x48[1], + label: "X48".into(), + })); } } - v.map(clean) -} -fn gcd(a: i64, b: i64) -> i64 { - if b == 0 { a } else { gcd(b, a % b) } -} -// --------------------------------------------------------------------------- -// Conics from A x² + B xy + C y² + D x + E y + F = 0 -pub fn conic_analyze(c: [f64; 6], name: &str) -> Vec { - let [a, b, cc, d, e, f] = c; - let mut out = Vec::new(); - let det = 4.0 * a * cc - b * b; - if near(a, 0.0) && near(b, 0.0) && near(cc, 0.0) { - return out; - } - if near(det, 0.0) { - // parabola (axis-aligned cases) - if near(b, 0.0) && near(a, 0.0) && !near(d, 0.0) { - let y0 = -e / (2.0 * cc); + // X(23) Far-out point: circumcircle-inverse of the centroid; skip equilateral (G=O). + // Trilinear a(b⁴+c⁴−a⁴−b²c²); bary a²(b⁴+c⁴−a⁴−b²c²). On Euler line. + { + let og2 = { + let dx = centroid[0] - circ[0]; + let dy = centroid[1] - circ[1]; + dx * dx + dy * dy + }; + if og2 > 1e-12 { + let scale = (rr * rr) / og2; + let x23 = [ + circ[0] + scale * (centroid[0] - circ[0]), + circ[1] + scale * (centroid[1] - circ[1]), + ]; + // Bary a²(b⁴+c⁴−a⁴−b²c²) + let a2 = a * a; + let b2 = b * b; + let c2 = c * c; + let a4 = a2 * a2; + let b4 = b2 * b2; + let c4 = c2 * c2; + let wa = a2 * (b4 + c4 - a4 - b2 * c2); + let wb = b2 * (c4 + a4 - b4 - c2 * a2); + let wc = c2 * (a4 + b4 - c4 - a2 * b2); + let den = wa + wb + wc; + let x23u = if den.abs() > 1e-12 { + let xb = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if dist2(xb, x23) < 1e-4 * (1.0 + rr) { + xb + } else { + x23 + } + } else { + x23 + }; + out.push(fact( + "远点", + "X_{23}", + pt(x23u[0], x23u[1]), + None, + "X(23) = O + R^2(G-O)/|OG|^2", + "远点 X(23):重心关于外接圆的反演点", + ).with(Shape::Point { + x: x23u[0], + y: x23u[1], + label: "X23 远点".into(), + })); + // |OX23| · |OG| = R² + let d_ox = dist2(circ, x23u); + let d_og = og2.sqrt(); + if d_og > 1e-12 { + let prod = d_ox * d_og; + if (prod - rr * rr).abs() < 1e-4 * (1.0 + rr * rr) { + out.push(fact( + "远点", + "|OX_{23}|\\cdot|OG|", + nice(rr * rr), + Some(rr * rr), + "|OX_{23}|\\cdot|OG| = R^2", + "反演定义:乘积等于外接圆半径平方", + )); + } + } + // Collinear O, G, X23 (Euler) + let cross = (centroid[0] - circ[0]) * (x23u[1] - circ[1]) + - (centroid[1] - circ[1]) * (x23u[0] - circ[0]); + if cross.abs() < 1e-5 * (1.0 + og2) * (1.0 + dist2(circ, x23u)) { + out.push(fact( + "远点", + "O,\\ G,\\ X_{23}", + "\\text{共线}".into(), + None, + "X(23)\\in\\text{Euler}", + "远点在欧拉线上", + )); + } + } + } + + // X(35): bary sin A + sin 2A; on OI (companion of circum-inverse X(36)=sin A−sin 2A). + { + let wa = g[0].sin() + (2.0 * g[0]).sin(); + let wb = g[1].sin() + (2.0 * g[1]).sin(); + let wc = g[2].sin() + (2.0 * g[2]).sin(); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x35 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + let di = dist2(x35, inc); + let do_ = dist2(x35, circ); + if di > 1e-10 || do_ > 1e-10 { + out.push(fact( + "OI调和点", + "X_{35}", + pt(x35[0], x35[1]), + None, + "X(35) = (\\sin A+\\sin 2A:\\cdots)", + "X(35):与 X(36) 成对的 OI 线上点", + ).with(Shape::Point { + x: x35[0], + y: x35[1], + label: "X35".into(), + })); + let cross = (inc[0] - circ[0]) * (x35[1] - circ[1]) + - (inc[1] - circ[1]) * (x35[0] - circ[0]); + let oi2 = dist2(circ, inc); + if oi2 > 1e-12 + && cross.abs() < 1e-5 * (1.0 + oi2) * (1.0 + dist2(circ, x35)) + { + out.push(fact( + "OI调和点", + "O,\\ I,\\ X_{35}", + "\\text{共线}".into(), + None, + "X(35)\\in OI", + "X(35) 在 OI 线上", + )); + } + } + } + } + + // X(38): bary a(b²+c²); crosspoint of I and Moses X(75). + { + let wa = a * (b * b + c * c); + let wb = b * (c * c + a * a); + let wc = c * (a * a + b * b); + let den = wa + wb + wc; + if den > 1e-12 { + let x38 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "截线交点", + "X_{38}", + pt(x38[0], x38[1]), + None, + "X(38) = \\bigl(a(b^2+c^2):\\cdots\\bigr)", + "X(38):内心与摩西点的截线交点", + ).with(Shape::Point { + x: x38[0], + y: x38[1], + label: "X38".into(), + })); + } + } + + // X(24): perspector of ABC and the orthic-of-orthic triangle. + // Bary tan A cos 2A = tan A − sin 2A; skip right/obtuse (tan undefined / sign issues). + { + let cosines = [g[0].cos(), g[1].cos(), g[2].cos()]; + let acute = cosines.iter().all(|&c| c > 1e-9) + && g.iter().all(|&ang| ang > 1e-9 && ang < std::f64::consts::FRAC_PI_2 - 1e-9); + if acute { + let wa = g[0].tan() - (2.0 * g[0]).sin(); + let wb = g[1].tan() - (2.0 * g[1]).sin(); + let wc = g[2].tan() - (2.0 * g[2]).sin(); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x24 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "二重垂足透视", + "X_{24}", + pt(x24[0], x24[1]), + None, + "X(24) = (\\tan A\\cos 2A:\\cdots) = (\\tan A-\\sin 2A:\\cdots)", + "X(24):ABC 与二重垂足三角形的透视中心(锐角)", + ).with(Shape::Point { + x: x24[0], + y: x24[1], + label: "X24".into(), + })); + } + } + } + + // X(43): X(6)-Ceva conjugate of I; bary a(ab+ac−bc). + { + let wa = a * (a * b + a * c - b * c); + let wb = b * (b * c + b * a - c * a); + let wc = c * (c * a + c * b - a * b); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x43 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "勒莫恩Ceva", + "X_{43}", + pt(x43[0], x43[1]), + None, + "X(43) = \\bigl(a(ab+ac-bc):\\cdots\\bigr)", + "X(43):勒莫恩点对内心的 Ceva 共轭", + ).with(Shape::Point { + x: x43[0], + y: x43[1], + label: "X43".into(), + })); + } + } + + // X(44): X(6)-line conjugate of I; bary a(b+c−2a). Skip equilateral (all weights 0). + { + let wa = a * (b + c - 2.0 * a); + let wb = b * (c + a - 2.0 * b); + let wc = c * (a + b - 2.0 * c); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x44 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "勒莫恩线共轭", + "X_{44}", + pt(x44[0], x44[1]), + None, + "X(44) = \\bigl(a(b+c-2a):\\cdots\\bigr)", + "X(44):勒莫恩点对内心的线共轭(等边时退化)", + ).with(Shape::Point { + x: x44[0], + y: x44[1], + label: "X44".into(), + })); + } + } + + // X(63): isogonal conjugate of Clawson X(19); bary cos A : cos B : cos C. + // Always finite (Σ cos = 1+r/R > 0); equals G on equilateral. + { + let wa = g[0].cos(); + let wb = g[1].cos(); + let wc = g[2].cos(); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x63 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "克劳森等角", + "X_{63}", + pt(x63[0], x63[1]), + None, + "X(63) = (\\cos A:\\cos B:\\cos C)", + "X(63):克劳森点的等角共轭(亦为 X(48) 的截线伴点)", + ).with(Shape::Point { + x: x63[0], + y: x63[1], + label: "X63".into(), + })); + } + } + + // X(45): X(9)-beth conjugate of I; bary a(2b+2c−a). Always finite (weights > 0). + { + let wa = a * (2.0 * b + 2.0 * c - a); + let wb = b * (2.0 * c + 2.0 * a - b); + let wc = c * (2.0 * a + 2.0 * b - c); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x45 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "米滕beth", + "X_{45}", + pt(x45[0], x45[1]), + None, + "X(45) = \\bigl(a(2b+2c-a):\\cdots\\bigr)", + "X(45):米滕点对内心的 beth 共轭", + ).with(Shape::Point { + x: x45[0], + y: x45[1], + label: "X45".into(), + })); + } + } + + // X(46): X(4)-Ceva conjugate of I; bary a(cos B+cos C−cos A). On OI line. + { + let wa = a * (g[1].cos() + g[2].cos() - g[0].cos()); + let wb = b * (g[2].cos() + g[0].cos() - g[1].cos()); + let wc = c * (g[0].cos() + g[1].cos() - g[2].cos()); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x46 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "垂心Ceva内心", + "X_{46}", + pt(x46[0], x46[1]), + None, + "X(46) = \\bigl(a(\\cos B+\\cos C-\\cos A):\\cdots\\bigr)", + "X(46):垂心对内心的 Ceva 共轭(在 OI 线上)", + ).with(Shape::Point { + x: x46[0], + y: x46[1], + label: "X46".into(), + })); + } + } + + // X(47): bary a cos 2A; pairs with X(48)=a sin 2A. Equals G on equilateral. + { + let wa = a * (2.0 * g[0]).cos(); + let wb = b * (2.0 * g[1]).cos(); + let wc = c * (2.0 * g[2]).cos(); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x47 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "双倍角余弦", + "X_{47}", + pt(x47[0], x47[1]), + None, + "X(47) = (a\\cos 2A:b\\cos 2B:c\\cos 2C)", + "X(47):双倍角余弦点(与 X(48) 成对)", + ).with(Shape::Point { + x: x47[0], + y: x47[1], + label: "X47".into(), + })); + } + } + + // X(49): center of sine-triple-angle circle; bary a cos 3A (= sin A cos 3A). + // Equals G on equilateral (cos 180° = −1). + { + let wa = a * (3.0 * g[0]).cos(); + let wb = b * (3.0 * g[1]).cos(); + let wc = c * (3.0 * g[2]).cos(); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x49 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "三倍角正弦圆", + "X_{49}", + pt(x49[0], x49[1]), + None, + "X(49) = (a\\cos 3A:b\\cos 3B:c\\cos 3C)", + "X(49):三倍角正弦圆圆心", + ).with(Shape::Point { + x: x49[0], + y: x49[1], + label: "X49".into(), + })); + } + } + + + + // X(50): bary a sin 3A (= sin A sin 3A); pairs with X(49)=a cos 3A. + // Equilateral: 3A=180°, sin=0 → all weights 0 → skip. + { + let wa = a * (3.0 * g[0]).sin(); + let wb = b * (3.0 * g[1]).sin(); + let wc = c * (3.0 * g[2]).sin(); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x50 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "三倍角正弦", + "X_{50}", + pt(x50[0], x50[1]), + None, + "X(50) = (a\\sin 3A:b\\sin 3B:c\\sin 3C)", + "X(50):三倍角正弦点(与 X(49) 成对;等边时退化)", + ).with(Shape::Point { + x: x50[0], + y: x50[1], + label: "X50".into(), + })); + } + } + + // X(61), X(62): isogonal conjugates of the Napoleon centers X(17), X(18). + // Trilinear cos(A∓π/3); bary a cos(A∓π/3). Reciprocals of Napoleon trilinears. + { + let pi3 = std::f64::consts::PI / 3.0; + let c61 = [(g[0] - pi3).cos(), (g[1] - pi3).cos(), (g[2] - pi3).cos()]; + let wa = a * c61[0]; + let wb = b * c61[1]; + let wc = c * c61[2]; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x61 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "拿破仑等角共轭点", + "X_{61}", + pt(x61[0], x61[1]), + None, + "X(61) = \\bigl(a\\cos(A-\\pi/3):\\cdots\\bigr)", + "X(61):第一拿破仑点的等角共轭", + ).with(Shape::Point { + x: x61[0], + y: x61[1], + label: "X61".into(), + })); + out.push(fact( + "拿破仑等角共轭点", + "X_{61} \\leftrightarrow N_1", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(17)", + "X(61) 与第一拿破仑点 X(17) 互为等角共轭", + )); + } + let c62 = [(g[0] + pi3).cos(), (g[1] + pi3).cos(), (g[2] + pi3).cos()]; + let wa = a * c62[0]; + let wb = b * c62[1]; + let wc = c * c62[2]; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x62 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "拿破仑等角共轭点", + "X_{62}", + pt(x62[0], x62[1]), + None, + "X(62) = \\bigl(a\\cos(A+\\pi/3):\\cdots\\bigr)", + "X(62):第二拿破仑点的等角共轭", + ).with(Shape::Point { + x: x62[0], + y: x62[1], + label: "X62".into(), + })); + out.push(fact( + "拿破仑等角共轭点", + "X_{62} \\leftrightarrow N_2", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(18)", + "X(62) 与第二拿破仑点 X(18) 互为等角共轭", + )); + } + } + + // X(64): isogonal conjugate of de Longchamps X(20). + // Bary a/(cos A − cos B cos C). Skip when a side-den vanishes or Σweights=0 + // (right △: X64 is at infinity — Σ = 0). + { + let ca = g[0].cos(); + let cb = g[1].cos(); + let cc = g[2].cos(); + let da = ca - cb * cc; + let db = cb - cc * ca; + let dc = cc - ca * cb; + if da.abs() > 1e-12 && db.abs() > 1e-12 && dc.abs() > 1e-12 { + let wa = a / da; + let wb = b / db; + let wc = c / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x64 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "德龙尚等角", + "X_{64}", + pt(x64[0], x64[1]), + None, + "X(64) = \\bigl(a/(\\cos A-\\cos B\\cos C):\\cdots\\bigr)", + "X(64):德龙尚点的等角共轭", + ).with(Shape::Point { + x: x64[0], + y: x64[1], + label: "X64".into(), + })); + out.push(fact( + "德龙尚等角", + "X_{64} \\leftrightarrow L", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(20)", + "X(64) 与德龙尚点 X(20) 互为等角共轭", + )); + } + } + } + + // X(51): centroid of the orthic triangle; bary a³ cos(B−C). + // Geometric cross-check: mean of altitude feet. Equals G on equilateral. + { + let wa = a.powi(3) * (g[1] - g[2]).cos(); + let wb = b.powi(3) * (g[2] - g[0]).cos(); + let wc = c.powi(3) * (g[0] - g[1]).cos(); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x51 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "垂足重心", + "X_{51}", + pt(x51[0], x51[1]), + None, + "X(51) = \\bigl(a^3\\cos(B-C):\\cdots\\bigr)", + "X(51):垂足三角形的重心", + ).with(Shape::Point { + x: x51[0], + y: x51[1], + label: "X51".into(), + })); + // Mean of altitude feet ≈ X(51). + let mut feet = [[0.0f64; 2]; 3]; + let mut ok = true; + for i in 0..3 { + let p0 = v[i]; + let q1 = v[(i + 1) % 3]; + let q2 = v[(i + 2) % 3]; + let dx = q2[0] - q1[0]; + let dy = q2[1] - q1[1]; + let d2 = dx * dx + dy * dy; + if d2 < 1e-18 { + ok = false; + break; + } + let t = ((p0[0] - q1[0]) * dx + (p0[1] - q1[1]) * dy) / d2; + feet[i] = [q1[0] + t * dx, q1[1] + t * dy]; + } + if ok { + let g_orth = [ + (feet[0][0] + feet[1][0] + feet[2][0]) / 3.0, + (feet[0][1] + feet[1][1] + feet[2][1]) / 3.0, + ]; + if dist2(x51, g_orth) < 1e-5 * (1.0 + rr) { + out.push(fact( + "垂足重心", + "G_{\\text{orthic}}", + "\\checkmark".into(), + None, + "X(51) = (H_a+H_b+H_c)/3", + "与垂足三角形三顶点的重心重合", + )); + } + } + } + } + + // X(53): symmedian point of the orthic triangle; bary a tan A cos(B−C). + // Skip right △ (tan ∞) and near-right (cos≈0). + { + let ca = g[0].cos(); + let cb = g[1].cos(); + let cc = g[2].cos(); + if ca.abs() > 1e-12 && cb.abs() > 1e-12 && cc.abs() > 1e-12 { + let wa = a * (g[0].sin() / ca) * (g[1] - g[2]).cos(); + let wb = b * (g[1].sin() / cb) * (g[2] - g[0]).cos(); + let wc = c * (g[2].sin() / cc) * (g[0] - g[1]).cos(); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x53 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "垂足对称中线点", + "X_{53}", + pt(x53[0], x53[1]), + None, + "X(53) = \\bigl(a\\tan A\\cos(B-C):\\cdots\\bigr)", + "X(53):垂足三角形的对称中线点(锐角)", + ).with(Shape::Point { + x: x53[0], + y: x53[1], + label: "X53".into(), + })); + } + } + } + + // X(52): orthocenter of the orthic triangle; bary a cos 2A cos(B−C). + // Skip right/near-right (orthic degenerates when cos A≈0). Equals G on equilateral. + { + let ca = g[0].cos(); + let cb = g[1].cos(); + let cc = g[2].cos(); + if ca.abs() > 1e-9 && cb.abs() > 1e-9 && cc.abs() > 1e-9 { + let wa = a * (2.0 * g[0]).cos() * (g[1] - g[2]).cos(); + let wb = b * (2.0 * g[1]).cos() * (g[2] - g[0]).cos(); + let wc = c * (2.0 * g[2]).cos() * (g[0] - g[1]).cos(); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x52 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "垂足垂心", + "X_{52}", + pt(x52[0], x52[1]), + None, + "X(52) = \\bigl(a\\cos 2A\\cos(B-C):\\cdots\\bigr)", + "X(52):垂足三角形的垂心(锐角)", + ).with(Shape::Point { + x: x52[0], + y: x52[1], + label: "X52".into(), + })); + } + } + } + + // X(26): circumcenter of the tangential triangle; on Euler line. + // Bary a²(b² cos 2B + c² cos 2C − a² cos 2A). Isogonal of X(70). + // Right △: Σweights = 0 → point at infinity on Euler — skip. + { + let c2a = (2.0 * g[0]).cos(); + let c2b = (2.0 * g[1]).cos(); + let c2c = (2.0 * g[2]).cos(); + let wa = a * a * (b * b * c2b + c * c * c2c - a * a * c2a); + let wb = b * b * (c * c * c2c + a * a * c2a - b * b * c2b); + let wc = c * c * (a * a * c2a + b * b * c2b - c * c * c2c); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x26 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "切线三角形外心", + "X_{26}", + pt(x26[0], x26[1]), + None, + "X(26) = \\bigl(a^2(b^2\\cos 2B+c^2\\cos 2C-a^2\\cos 2A):\\cdots\\bigr)", + "X(26):切线三角形的外心(在欧拉线上)", + ).with(Shape::Point { + x: x26[0], + y: x26[1], + label: "X26".into(), + })); + // On Euler line OH: cross (X−O)×(H−O) ≈ 0. + let hx = orth[0] - circ[0]; + let hy = orth[1] - circ[1]; + let xx = x26[0] - circ[0]; + let xy = x26[1] - circ[1]; + let cross = xx * hy - xy * hx; + let scale = (1.0 + dist2(circ, orth)) * (1.0 + dist2(circ, x26)); + if cross.abs() < 1e-5 * scale { + out.push(fact( + "切线三角形外心", + "X_{26}\\in OH", + "\\checkmark".into(), + None, + "X(26)\\in\\text{Euler}", + "X(26) 在欧拉线 OH 上", + )); + } + } + } + + // X(27): cevapoint of H and Clawson X(19); bary tan A/(b+c). + // Skip right △ (tan ∞). + { + let ca = g[0].cos(); + let cb = g[1].cos(); + let cc = g[2].cos(); + if ca.abs() > 1e-12 && cb.abs() > 1e-12 && cc.abs() > 1e-12 { + let wa = (g[0].sin() / ca) / (b + c); + let wb = (g[1].sin() / cb) / (c + a); + let wc = (g[2].sin() / cc) / (a + b); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x27 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "垂心克劳森截点", + "X_{27}", + pt(x27[0], x27[1]), + None, + "X(27) = \\bigl(\\tan A/(b+c):\\cdots\\bigr)", + "X(27):垂心与克劳森点的截线交点(锐角)", + ).with(Shape::Point { + x: x27[0], + y: x27[1], + label: "X27".into(), + })); + } + } + } + + // X(29): cevapoint of I and H; bary tan A/(cos B + cos C). + // Skip right △ (tan ∞). Denom cos B+cos C = 2 sin(A/2)cos((B−C)/2) > 0. + { + let ca = g[0].cos(); + let cb = g[1].cos(); + let cc = g[2].cos(); + let da = cb + cc; + let db = cc + ca; + let dc = ca + cb; + if ca.abs() > 1e-12 + && cb.abs() > 1e-12 + && cc.abs() > 1e-12 + && da.abs() > 1e-12 + && db.abs() > 1e-12 + && dc.abs() > 1e-12 + { + let wa = (g[0].sin() / ca) / da; + let wb = (g[1].sin() / cb) / db; + let wc = (g[2].sin() / cc) / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x29 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "内心垂心截点", + "X_{29}", + pt(x29[0], x29[1]), + None, + "X(29) = \\bigl(\\tan A/(\\cos B+\\cos C):\\cdots\\bigr)", + "X(29):内心与垂心的截线交点(锐角)", + ).with(Shape::Point { + x: x29[0], + y: x29[1], + label: "X29".into(), + })); + } + } + } + + // X(79), X(80): isogonal conjugates of X(35) and X(36). + // Trilinears 1/(1±2 cos A); bary a/(1±2 cos A). + // X(79) blows up when an angle is 120°; X(80) when an angle is 60° (equilateral). + { + let d79 = [ + 1.0 + 2.0 * g[0].cos(), + 1.0 + 2.0 * g[1].cos(), + 1.0 + 2.0 * g[2].cos(), + ]; + if d79.iter().all(|d| d.abs() > 1e-9) { + let wa = a / d79[0]; + let wb = b / d79[1]; + let wc = c / d79[2]; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x79 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "OI伴点等角", + "X_{79}", + pt(x79[0], x79[1]), + None, + "X(79) = \\bigl(a/(1+2\\cos A):\\cdots\\bigr)", + "X(79):X(35) 的等角共轭", + ).with(Shape::Point { + x: x79[0], + y: x79[1], + label: "X79".into(), + })); + out.push(fact( + "OI伴点等角", + "X_{79} \\leftrightarrow X_{35}", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(35)", + "X(79) 与 X(35) 互为等角共轭", + )); + } + } + let d80 = [ + 1.0 - 2.0 * g[0].cos(), + 1.0 - 2.0 * g[1].cos(), + 1.0 - 2.0 * g[2].cos(), + ]; + if d80.iter().all(|d| d.abs() > 1e-9) { + let wa = a / d80[0]; + let wb = b / d80[1]; + let wc = c / d80[2]; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x80 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "反演内心等角", + "X_{80}", + pt(x80[0], x80[1]), + None, + "X(80) = \\bigl(a/(1-2\\cos A):\\cdots\\bigr)", + "X(80):X(36) 的等角共轭(有 60° 角时退化)", + ).with(Shape::Point { + x: x80[0], + y: x80[1], + label: "X80".into(), + })); + out.push(fact( + "反演内心等角", + "X_{80} \\leftrightarrow X_{36}", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(36)", + "X(80) 与反演内心 X(36) 互为等角共轭", + )); + } + } + } + + // X(28): cevapoint of Clawson X(19) and X(25); bary (sin A tan A)/(b+c). + // Skip right △ (tan ∞). + { + let ca = g[0].cos(); + let cb = g[1].cos(); + let cc = g[2].cos(); + if ca.abs() > 1e-12 && cb.abs() > 1e-12 && cc.abs() > 1e-12 { + let wa = (g[0].sin() * g[0].sin() / ca) / (b + c); + let wb = (g[1].sin() * g[1].sin() / cb) / (c + a); + let wc = (g[2].sin() * g[2].sin() / cc) / (a + b); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x28 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "克劳森X25截点", + "X_{28}", + pt(x28[0], x28[1]), + None, + "X(28) = \\bigl(\\sin A\\tan A/(b+c):\\cdots\\bigr)", + "X(28):克劳森点与 X(25) 的截线交点(锐角)", + ).with(Shape::Point { + x: x28[0], + y: x28[1], + label: "X28".into(), + })); + } + } + } + + // X(70): isogonal conjugate of X(26); bary 1/(b² cos 2B + c² cos 2C − a² cos 2A). + { + let c2a = (2.0 * g[0]).cos(); + let c2b = (2.0 * g[1]).cos(); + let c2c = (2.0 * g[2]).cos(); + let da = b * b * c2b + c * c * c2c - a * a * c2a; + let db = c * c * c2c + a * a * c2a - b * b * c2b; + let dc = a * a * c2a + b * b * c2b - c * c * c2c; + if da.abs() > 1e-12 && db.abs() > 1e-12 && dc.abs() > 1e-12 { + let wa = 1.0 / da; + let wb = 1.0 / db; + let wc = 1.0 / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x70 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "切线外心等角", + "X_{70}", + pt(x70[0], x70[1]), + None, + "X(70) = \\bigl(1/(b^2\\cos 2B+c^2\\cos 2C-a^2\\cos 2A):\\cdots\\bigr)", + "X(70):切线三角形外心 X(26) 的等角共轭", + ).with(Shape::Point { + x: x70[0], + y: x70[1], + label: "X70".into(), + })); + out.push(fact( + "切线外心等角", + "X_{70} \\leftrightarrow X_{26}", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(26)", + "X(70) 与 X(26) 互为等角共轭", + )); + } + } + } + + // X(71): isogonal of X(27); bary (b+c) sin 2A. Always finite (sin 2A=0 only at right vertex). + { + let wa = (b + c) * (2.0 * g[0]).sin(); + let wb = (c + a) * (2.0 * g[1]).sin(); + let wc = (a + b) * (2.0 * g[2]).sin(); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x71 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "X27等角", + "X_{71}", + pt(x71[0], x71[1]), + None, + "X(71) = \\bigl((b+c)\\sin 2A:\\cdots\\bigr)", + "X(71):X(27) 的等角共轭", + ).with(Shape::Point { + x: x71[0], + y: x71[1], + label: "X71".into(), + })); + out.push(fact( + "X27等角", + "X_{71} \\leftrightarrow X_{27}", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(27)", + "X(71) 与 X(27) 互为等角共轭", + )); + } + } + + // X(73): crosspoint of I and O; isogonal of X(29). + // Bary (cos B + cos C) sin 2A. + { + let wa = (g[1].cos() + g[2].cos()) * (2.0 * g[0]).sin(); + let wb = (g[2].cos() + g[0].cos()) * (2.0 * g[1]).sin(); + let wc = (g[0].cos() + g[1].cos()) * (2.0 * g[2]).sin(); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x73 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "内心外心截点", + "X_{73}", + pt(x73[0], x73[1]), + None, + "X(73) = \\bigl((\\cos B+\\cos C)\\sin 2A:\\cdots\\bigr)", + "X(73):内心与外心的截线交点(X(29) 的等角共轭)", + ).with(Shape::Point { + x: x73[0], + y: x73[1], + label: "X73".into(), + })); + out.push(fact( + "内心外心截点", + "X_{73} \\leftrightarrow X_{29}", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(29)", + "X(73) 与 X(29) 互为等角共轭", + )); + } + } + + // X(33): perspector of orthic and intangents triangles; bary sin A + tan A. + // Skip right △ (tan ∞). Equals G on equilateral. + // X(34): X(4)-beth conjugate of X(4); bary sin A − tan A; same acute guard. + { + let ca = g[0].cos(); + let cb = g[1].cos(); + let cc = g[2].cos(); + if ca.abs() > 1e-12 && cb.abs() > 1e-12 && cc.abs() > 1e-12 { + let wa33 = g[0].sin() + g[0].tan(); + let wb33 = g[1].sin() + g[1].tan(); + let wc33 = g[2].sin() + g[2].tan(); + let den33 = wa33 + wb33 + wc33; + if den33.abs() > 1e-12 { + let x33 = [ + (wa33 * pa[0] + wb33 * pb[0] + wc33 * pc[0]) / den33, + (wa33 * pa[1] + wb33 * pb[1] + wc33 * pc[1]) / den33, + ]; + out.push(fact( + "垂足切线透视", + "X_{33}", + pt(x33[0], x33[1]), + None, + "X(33) = \\bigl(\\sin A+\\tan A:\\cdots\\bigr)", + "X(33):垂足三角形与内切线三角形的透视中心(锐角)", + ).with(Shape::Point { + x: x33[0], + y: x33[1], + label: "X33".into(), + })); + } + let wa34 = g[0].sin() - g[0].tan(); + let wb34 = g[1].sin() - g[1].tan(); + let wc34 = g[2].sin() - g[2].tan(); + let den34 = wa34 + wb34 + wc34; + if den34.abs() > 1e-12 { + let x34 = [ + (wa34 * pa[0] + wb34 * pb[0] + wc34 * pc[0]) / den34, + (wa34 * pa[1] + wb34 * pb[1] + wc34 * pc[1]) / den34, + ]; + out.push(fact( + "垂心beth自共轭", + "X_{34}", + pt(x34[0], x34[1]), + None, + "X(34) = \\bigl(\\sin A-\\tan A:\\cdots\\bigr)", + "X(34):X(4) 对自身的 beth 共轭(锐角)", + ).with(Shape::Point { + x: x34[0], + y: x34[1], + label: "X34".into(), + })); + } + } + } + + // X(66): isogonal conjugate of Exeter X(22); bary 1/(b⁴+c⁴−a⁴). + // Skip when any denom vanishes (a⁴ = b⁴+c⁴), or Σweights≈0 (right △ → ∞). + { + let a2 = a * a; + let b2 = b * b; + let c2 = c * c; + let a4 = a2 * a2; + let b4 = b2 * b2; + let c4 = c2 * c2; + let da = b4 + c4 - a4; + let db = c4 + a4 - b4; + let dc = a4 + b4 - c4; + if da.abs() > 1e-12 && db.abs() > 1e-12 && dc.abs() > 1e-12 { + let wa = 1.0 / da; + let wb = 1.0 / db; + let wc = 1.0 / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x66 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "埃克塞特等角", + "X_{66}", + pt(x66[0], x66[1]), + None, + "X(66) = \\bigl(1/(b^4+c^4-a^4):\\cdots\\bigr)", + "X(66):埃克塞特点 X(22) 的等角共轭", + ).with(Shape::Point { + x: x66[0], + y: x66[1], + label: "X66".into(), + })); + out.push(fact( + "埃克塞特等角", + "X_{66} \\leftrightarrow Ex", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(22)", + "X(66) 与埃克塞特点 X(22) 互为等角共轭", + )); + } + } + } + + // X(72): isogonal conjugate of X(28); bary (b+c) cos A. + // Finite on right △ (weight 0 at the right vertex). Equals G on equilateral. + { + let wa = (b + c) * g[0].cos(); + let wb = (c + a) * g[1].cos(); + let wc = (a + b) * g[2].cos(); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x72 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "克劳森X25等角", + "X_{72}", + pt(x72[0], x72[1]), + None, + "X(72) = \\bigl((b+c)\\cos A:\\cdots\\bigr)", + "X(72):X(28) 的等角共轭", + ).with(Shape::Point { + x: x72[0], + y: x72[1], + label: "X72".into(), + })); + out.push(fact( + "克劳森X25等角", + "X_{72} \\leftrightarrow X_{28}", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(28)", + "X(72) 与 X(28) 互为等角共轭", + )); + } + } + + // X(91), X(92): isogonal conjugates of X(47)=a cos 2A and X(48)=a sin 2A. + // Bary a sec 2A and a csc 2A. X(92) vanishes on a right angle (sin 180° = 0). + // Ported from cloud tip d7ac9aa / b1cc8ca double-angle isogonals. + { + let c2 = [(2.0 * g[0]).cos(), (2.0 * g[1]).cos(), (2.0 * g[2]).cos()]; + if c2.iter().all(|c| c.abs() > 1e-9) { + let wa = a / c2[0]; + let wb = b / c2[1]; + let wc = c / c2[2]; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x91 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "双倍角正割", + "X_{91}", + pt(x91[0], x91[1]), + None, + "X(91) = (a\\sec 2A:b\\sec 2B:c\\sec 2C)", + "X(91):X(47) 的等角共轭", + ).with(Shape::Point { + x: x91[0], + y: x91[1], + label: "X91".into(), + })); + out.push(fact( + "双倍角正割", + "X_{91} \\leftrightarrow X_{47}", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(47)", + "X(91) 与 X(47) 互为等角共轭", + )); + } + } + let s2 = [(2.0 * g[0]).sin(), (2.0 * g[1]).sin(), (2.0 * g[2]).sin()]; + if s2.iter().all(|s| s.abs() > 1e-9) { + let wa = a / s2[0]; + let wb = b / s2[1]; + let wc = c / s2[2]; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x92 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "双倍角余割", + "X_{92}", + pt(x92[0], x92[1]), + None, + "X(92) = (a\\csc 2A:b\\csc 2B:c\\csc 2C)", + "X(92):X(48) 的等角共轭(直角时 sin 180°=0,略过)", + ).with(Shape::Point { + x: x92[0], + y: x92[1], + label: "X92".into(), + })); + out.push(fact( + "双倍角余割", + "X_{92} \\leftrightarrow X_{48}", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(48)", + "X(92) 与 X(48) 互为等角共轭", + )); + } + } + } + + + // X(93), X(94): isogonal conjugates of the triple-angle points X(49)=a cos 3A + // and X(50)=a sin 3A. Bary a sec 3A and a csc 3A. + // X(93) blows up when cos 3A = 0 (A=30° or a right angle: 3·90°=270°). + // X(94) blows up when sin 3A = 0 (equilateral / any 60° angle: 3A=180°). + { + let c3 = [(3.0 * g[0]).cos(), (3.0 * g[1]).cos(), (3.0 * g[2]).cos()]; + if c3.iter().all(|c| c.abs() > 1e-9) { + let wa = a / c3[0]; + let wb = b / c3[1]; + let wc = c / c3[2]; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x93 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "三倍角正割", + "X_{93}", + pt(x93[0], x93[1]), + None, + "X(93) = (a\\sec 3A:b\\sec 3B:c\\sec 3C)", + "X(93):X(49) 的等角共轭(直角或 30° 角时 cos 3A=0,略过)", + ).with(Shape::Point { + x: x93[0], + y: x93[1], + label: "X93".into(), + })); + out.push(fact( + "三倍角正割", + "X_{93} \\leftrightarrow X_{49}", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(49)", + "X(93) 与 X(49) 互为等角共轭", + )); + } + } + let s3 = [(3.0 * g[0]).sin(), (3.0 * g[1]).sin(), (3.0 * g[2]).sin()]; + if s3.iter().all(|s| s.abs() > 1e-9) { + let wa = a / s3[0]; + let wb = b / s3[1]; + let wc = c / s3[2]; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x94 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "三倍角余割", + "X_{94}", + pt(x94[0], x94[1]), + None, + "X(94) = (a\\csc 3A:b\\csc 3B:c\\csc 3C)", + "X(94):X(50) 的等角共轭(等边/60° 角时 sin 3A=0,略过)", + ).with(Shape::Point { + x: x94[0], + y: x94[1], + label: "X94".into(), + })); + out.push(fact( + "三倍角余割", + "X_{94} \\leftrightarrow X_{50}", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(50)", + "X(94) 与 X(50) 互为等角共轭", + )); + } + } + } + + // X(68) Prasolov point: isogonal conjugate of X(24); bary tan 2A. + // Skip right/obtuse (tan 2A undefined when A≥90° or when cos 2A=0 i.e. A=45°). + { + let cosines = [g[0].cos(), g[1].cos(), g[2].cos()]; + let acute = cosines.iter().all(|&c| c > 1e-9) + && g.iter().all(|&ang| ang > 1e-9 && ang < std::f64::consts::FRAC_PI_2 - 1e-9); + let c2 = [(2.0 * g[0]).cos(), (2.0 * g[1]).cos(), (2.0 * g[2]).cos()]; + if acute && c2.iter().all(|c| c.abs() > 1e-9) { + let wa = (2.0 * g[0]).tan(); + let wb = (2.0 * g[1]).tan(); + let wc = (2.0 * g[2]).tan(); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x68 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "普拉索洛夫点", + "X_{68}", + pt(x68[0], x68[1]), + None, + "X(68) = (\\tan 2A:\\tan 2B:\\tan 2C)", + "普拉索洛夫点 X(68):X(24) 的等角共轭(锐角)", + ).with(Shape::Point { + x: x68[0], + y: x68[1], + label: "X68".into(), + })); + out.push(fact( + "普拉索洛夫点", + "X_{68} \\leftrightarrow X_{24}", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(24)", + "X(68) 与 X(24) 互为等角共轭", + )); + } + } + } + + // X(95): cevapoint of G and O; isogonal of X(51). + // Bary bc sec(B−C) : ca sec(C−A) : ab sec(A−B). Equals G on equilateral. + // Skip when any cos(B−C)≈0 (|B−C|=90°). + { + let cab = (g[1] - g[2]).cos(); + let cbc = (g[2] - g[0]).cos(); + let cca = (g[0] - g[1]).cos(); + if cab.abs() > 1e-9 && cbc.abs() > 1e-9 && cca.abs() > 1e-9 { + let wa = b * c / cab; + let wb = c * a / cbc; + let wc = a * b / cca; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x95 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "垂足重心等角", + "X_{95}", + pt(x95[0], x95[1]), + None, + "X(95) = (bc\\sec(B-C):ca\\sec(C-A):ab\\sec(A-B))", + "X(95):X(51) 的等角共轭(G 与 O 的 Ceva 截点)", + ).with(Shape::Point { + x: x95[0], + y: x95[1], + label: "X95".into(), + })); + out.push(fact( + "垂足重心等角", + "X_{95} \\leftrightarrow X_{51}", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(51)", + "X(95) 与垂足重心 X(51) 互为等角共轭", + )); + } + } + } + + // X(97): isogonal of X(53); bary cos A sec(B−C). + // Equals G on equilateral. Skip when any cos(B−C)≈0. + { + let cab = (g[1] - g[2]).cos(); + let cbc = (g[2] - g[0]).cos(); + let cca = (g[0] - g[1]).cos(); + if cab.abs() > 1e-9 && cbc.abs() > 1e-9 && cca.abs() > 1e-9 { + let wa = g[0].cos() / cab; + let wb = g[1].cos() / cbc; + let wc = g[2].cos() / cca; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x97 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "垂足对称中线等角", + "X_{97}", + pt(x97[0], x97[1]), + None, + "X(97) = (\\cos A\\sec(B-C):\\cdots)", + "X(97):X(53) 的等角共轭", + ).with(Shape::Point { + x: x97[0], + y: x97[1], + label: "X97".into(), + })); + out.push(fact( + "垂足对称中线等角", + "X_{97} \\leftrightarrow X_{53}", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(53)", + "X(97) 与垂足对称中线点 X(53) 互为等角共轭", + )); + } + } + } + + + // X(96): isogonal conjugate of X(52); bary a sec 2A sec(B−C). + // Skip when cos 2A≈0 (A=45°) or cos(B−C)≈0. Equals G on equilateral when finite. + { + let c2a = (2.0 * g[0]).cos(); + let c2b = (2.0 * g[1]).cos(); + let c2c = (2.0 * g[2]).cos(); + let cab = (g[1] - g[2]).cos(); + let cbc = (g[2] - g[0]).cos(); + let cca = (g[0] - g[1]).cos(); + if c2a.abs() > 1e-9 + && c2b.abs() > 1e-9 + && c2c.abs() > 1e-9 + && cab.abs() > 1e-9 + && cbc.abs() > 1e-9 + && cca.abs() > 1e-9 + { + let wa = a / (c2a * cab); + let wb = b / (c2b * cbc); + let wc = c / (c2c * cca); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x96 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "垂足垂心等角", + "X_{96}", + pt(x96[0], x96[1]), + None, + "X(96) = \\bigl(a\\sec 2A\\sec(B-C):\\cdots\\bigr)", + "X(96):垂足垂心 X(52) 的等角共轭", + ).with(Shape::Point { + x: x96[0], + y: x96[1], + label: "X96".into(), + })); + out.push(fact( + "垂足垂心等角", + "X_{96} \\leftrightarrow X_{52}", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(52)", + "X(96) 与垂足垂心 X(52) 互为等角共轭", + )); + } + } + } + + // X(67): isogonal conjugate of Far-out X(23); bary 1/(b⁴+c⁴−a⁴−b²c²). + // Skip when any denom vanishes or Σweights≈0 (equilateral → same as G via limit). + { + let a2 = a * a; + let b2 = b * b; + let c2 = c * c; + let a4 = a2 * a2; + let b4 = b2 * b2; + let c4 = c2 * c2; + let da = b4 + c4 - a4 - b2 * c2; + let db = c4 + a4 - b4 - c2 * a2; + let dc = a4 + b4 - c4 - a2 * b2; + if da.abs() > 1e-12 && db.abs() > 1e-12 && dc.abs() > 1e-12 { + let wa = 1.0 / da; + let wb = 1.0 / db; + let wc = 1.0 / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x67 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "远点等角", + "X_{67}", + pt(x67[0], x67[1]), + None, + "X(67) = \\bigl(1/(b^4+c^4-a^4-b^2c^2):\\cdots\\bigr)", + "X(67):远点 X(23) 的等角共轭", + ).with(Shape::Point { + x: x67[0], + y: x67[1], + label: "X67".into(), + })); + out.push(fact( + "远点等角", + "X_{67} \\leftrightarrow X_{23}", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(23)", + "X(67) 与远点 X(23) 互为等角共轭", + )); + } + } + } + + // X(74): isogonal conjugate of Euler infinity X(30); on the circumcircle. + // Bary a/(cos A − 2 cos B cos C). Skip when any denom≈0. + { + let ca = g[0].cos(); + let cb = g[1].cos(); + let cc = g[2].cos(); + let da = ca - 2.0 * cb * cc; + let db = cb - 2.0 * cc * ca; + let dc = cc - 2.0 * ca * cb; + if da.abs() > 1e-9 && db.abs() > 1e-9 && dc.abs() > 1e-9 { + let wa = a / da; + let wb = b / db; + let wc = c / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x74 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "欧拉无穷等角", + "X_{74}", + pt(x74[0], x74[1]), + None, + "X(74) = \\bigl(a/(\\cos A-2\\cos B\\cos C):\\cdots\\bigr)", + "X(74):欧拉无穷远点 X(30) 的等角共轭(在外接圆上)", + ).with(Shape::Point { + x: x74[0], + y: x74[1], + label: "X74".into(), + })); + out.push(fact( + "欧拉无穷等角", + "X_{74} \\leftrightarrow X_{30}", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(30)", + "X(74) 与欧拉无穷远点 X(30) 互为等角共轭", + )); + } + } + } + + // X(77): isogonal of X(33); bary a/(1+sec A) = a cos A/(1+cos A). + // Finite for all non-degenerate triangles (1+cos A=0 only if flat). + // Equals G on equilateral. Weight 0 at a right vertex. + { + let ca = g[0].cos(); + let cb = g[1].cos(); + let cc = g[2].cos(); + if (1.0 + ca).abs() > 1e-12 && (1.0 + cb).abs() > 1e-12 && (1.0 + cc).abs() > 1e-12 { + let wa = a * ca / (1.0 + ca); + let wb = b * cb / (1.0 + cb); + let wc = c * cc / (1.0 + cc); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x77 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "垂足切线等角", + "X_{77}", + pt(x77[0], x77[1]), + None, + "X(77) = \\bigl(a/(1+\\sec A):\\cdots\\bigr)", + "X(77):X(33) 的等角共轭", + ).with(Shape::Point { + x: x77[0], + y: x77[1], + label: "X77".into(), + })); + out.push(fact( + "垂足切线等角", + "X_{77} \\leftrightarrow X_{33}", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(33)", + "X(77) 与 X(33) 互为等角共轭", + )); + } + } + } + + // X(78): isogonal of X(34); bary a/(1−sec A) = a cos A/(cos A−1). + // Always finite for proper triangles (cos A < 1). Equals G on equilateral. + { + let ca = g[0].cos(); + let cb = g[1].cos(); + let cc = g[2].cos(); + if (ca - 1.0).abs() > 1e-12 && (cb - 1.0).abs() > 1e-12 && (cc - 1.0).abs() > 1e-12 { + let wa = a * ca / (ca - 1.0); + let wb = b * cb / (cb - 1.0); + let wc = c * cc / (cc - 1.0); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x78 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "垂心beth等角", + "X_{78}", + pt(x78[0], x78[1]), + None, + "X(78) = \\bigl(a/(1-\\sec A):\\cdots\\bigr)", + "X(78):X(34) 的等角共轭", + ).with(Shape::Point { + x: x78[0], + y: x78[1], + label: "X78".into(), + })); + out.push(fact( + "垂心beth等角", + "X_{78} \\leftrightarrow X_{34}", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(34)", + "X(78) 与 X(34) 互为等角共轭", + )); + } + } + } + + // X(82): isogonal of X(38); bary a/(b²+c²). Always finite. + { + let a2 = a * a; + let b2 = b * b; + let c2 = c * c; + let wa = a / (b2 + c2); + let wb = b / (c2 + a2); + let wc = c / (a2 + b2); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x82 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "X38等角", + "X_{82}", + pt(x82[0], x82[1]), + None, + "X(82) = \\bigl(a/(b^2+c^2):\\cdots\\bigr)", + "X(82):X(38) 的等角共轭", + ).with(Shape::Point { + x: x82[0], + y: x82[1], + label: "X82".into(), + })); + out.push(fact( + "X38等角", + "X_{82} \\leftrightarrow X_{38}", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(38)", + "X(82) 与 X(38) 互为等角共轭", + )); + } + } + + // X(83): isogonal of Brocard midpoint X(39); bary 1/(b²+c²). + // Also cevapoint of G and K. Equals G on equilateral. + { + let a2 = a * a; + let b2 = b * b; + let c2 = c * c; + let wa = 1.0 / (b2 + c2); + let wb = 1.0 / (c2 + a2); + let wc = 1.0 / (a2 + b2); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x83 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "布罗卡中点等角", + "X_{83}", + pt(x83[0], x83[1]), + None, + "X(83) = \\bigl(1/(b^2+c^2):\\cdots\\bigr)", + "X(83):布罗卡中点 X(39) 的等角共轭(G 与 K 的 Ceva 截点)", + ).with(Shape::Point { + x: x83[0], + y: x83[1], + label: "X83".into(), + })); + out.push(fact( + "布罗卡中点等角", + "X_{83} \\leftrightarrow X_{39}", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(39)", + "X(83) 与布罗卡中点 X(39) 互为等角共轭", + )); + } + } + + // X(85): isogonal of X(41); isotomic of Mittenpunkt X(9). + // Bary bc/(b+c−a) = bc/(2(s−a)). Always finite for proper triangles. + { + let da = b + c - a; + let db = c + a - b; + let dc = a + b - c; + if da > 1e-12 && db > 1e-12 && dc > 1e-12 { + let wa = b * c / da; + let wb = c * a / db; + let wc = a * b / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x85 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "X41等角", + "X_{85}", + pt(x85[0], x85[1]), + None, + "X(85) = \\bigl(bc/(b+c-a):\\cdots\\bigr)", + "X(85):X(41) 的等角共轭;Mittenpunkt X(9) 的等截共轭", + ).with(Shape::Point { + x: x85[0], + y: x85[1], + label: "X85".into(), + })); + out.push(fact( + "X41等角", + "X_{85} \\leftrightarrow X_{41}", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(41)", + "X(85) 与 X(41) 互为等角共轭", + )); + } + } + } + + + // X(87): isogonal of X(43); bary a/(ab+ac−bc). Always finite when denom≠0. + { + let da = a * b + a * c - b * c; + let db = b * c + b * a - c * a; + let dc = c * a + c * b - a * b; + if da.abs() > 1e-12 && db.abs() > 1e-12 && dc.abs() > 1e-12 { + let wa = a / da; + let wb = b / db; + let wc = c / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x87 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "勒莫恩Ceva等角", + "X_{87}", + pt(x87[0], x87[1]), + None, + "X(87) = \\bigl(a/(ab+ac-bc):\\cdots\\bigr)", + "X(87):X(43) 的等角共轭(X(2) 对 X(1) 的截线共轭)", + ).with(Shape::Point { + x: x87[0], + y: x87[1], + label: "X87".into(), + })); + out.push(fact( + "勒莫恩Ceva等角", + "X_{87} \\leftrightarrow X_{43}", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(43)", + "X(87) 与 X(43) 互为等角共轭", + )); + } + } + } + + // X(88): isogonal of X(44); bary a/(b+c−2a). Skip equilateral / when any denom≈0. + { + let da = b + c - 2.0 * a; + let db = c + a - 2.0 * b; + let dc = a + b - 2.0 * c; + if da.abs() > 1e-9 && db.abs() > 1e-9 && dc.abs() > 1e-9 { + let wa = a / da; + let wb = b / db; + let wc = c / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x88 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "勒莫恩线等角", + "X_{88}", + pt(x88[0], x88[1]), + None, + "X(88) = \\bigl(a/(b+c-2a):\\cdots\\bigr)", + "X(88):X(44) 的等角共轭", + ).with(Shape::Point { + x: x88[0], + y: x88[1], + label: "X88".into(), + })); + out.push(fact( + "勒莫恩线等角", + "X_{88} \\leftrightarrow X_{44}", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(44)", + "X(88) 与 X(44) 互为等角共轭", + )); + } + } + } + + // X(89): isogonal of X(45); bary a/(2b+2c−a). Always finite for proper △. + { + let da = 2.0 * b + 2.0 * c - a; + let db = 2.0 * c + 2.0 * a - b; + let dc = 2.0 * a + 2.0 * b - c; + if da > 1e-12 && db > 1e-12 && dc > 1e-12 { + let wa = a / da; + let wb = b / db; + let wc = c / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x89 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "米滕beth等角", + "X_{89}", + pt(x89[0], x89[1]), + None, + "X(89) = \\bigl(a/(2b+2c-a):\\cdots\\bigr)", + "X(89):X(45) 的等角共轭", + ).with(Shape::Point { + x: x89[0], + y: x89[1], + label: "X89".into(), + })); + out.push(fact( + "米滕beth等角", + "X_{89} \\leftrightarrow X_{45}", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(45)", + "X(89) 与 X(45) 互为等角共轭", + )); + } + } + } + + // X(90): isogonal of X(46); bary a/(cos B+cos C−cos A). X(3)-cross conjugate of I. + // Skip when any denom≈0. + { + let ca = g[0].cos(); + let cb = g[1].cos(); + let cc = g[2].cos(); + let da = cb + cc - ca; + let db = cc + ca - cb; + let dc = ca + cb - cc; + if da.abs() > 1e-9 && db.abs() > 1e-9 && dc.abs() > 1e-9 { + let wa = a / da; + let wb = b / db; + let wc = c / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x90 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "垂心Ceva等角", + "X_{90}", + pt(x90[0], x90[1]), + None, + "X(90) = \\bigl(a/(\\cos B+\\cos C-\\cos A):\\cdots\\bigr)", + "X(90):X(46) 的等角共轭(外心对内心的截线共轭)", + ).with(Shape::Point { + x: x90[0], + y: x90[1], + label: "X90".into(), + })); + out.push(fact( + "垂心Ceva等角", + "X_{90} \\leftrightarrow X_{46}", + "\\text{等角共轭}".into(), + None, + "\\text{isogonal of }X(46)", + "X(90) 与 X(46) 互为等角共轭", + )); + } + } + } + + // X(100): anticomplement of Feuerbach X(11); bary a/(b−c). + // On circumcircle; skip isosceles (any side-pair equal → denom 0). + { + let da = b - c; + let db = c - a; + let dc = a - b; + if da.abs() > 1e-9 && db.abs() > 1e-9 && dc.abs() > 1e-9 { + let wa = a / da; + let wb = b / db; + let wc = c / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x100 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "反补费尔巴哈", + "X_{100}", + pt(x100[0], x100[1]), + None, + "X(100) = \\bigl(a/(b-c):\\cdots\\bigr)", + "X(100):费尔巴哈点 X(11) 的反补点(外接圆上)", + ).with(Shape::Point { + x: x100[0], + y: x100[1], + label: "X100".into(), + })); + } + } + } + + // X(101): Ψ(I,K) = Ψ(X(1),X(6)); bary a²/(b−c). Circumcircle-antipode of X(103). + // Skip isosceles (same denom family as X(100)). + { + let da = b - c; + let db = c - a; + let dc = a - b; + if da.abs() > 1e-9 && db.abs() > 1e-9 && dc.abs() > 1e-9 { + let wa = (a * a) / da; + let wb = (b * b) / db; + let wc = (c * c) / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x101 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "Ψ内心对称中线", + "X_{101}", + pt(x101[0], x101[1]), + None, + "X(101) = \\bigl(a^2/(b-c):\\cdots\\bigr)", + "X(101):Ψ(I,K)(外接圆上)", + ).with(Shape::Point { + x: x101[0], + y: x101[1], + label: "X101".into(), + })); + } + } + } + + // X(104): circumcircle-antipode of X(100); Λ(I,O); bary a/(cos B+cos C−1). + // Skip equilateral / when any denom≈0 (cos B+cos C=1). + { + let ca = g[0].cos(); + let cb = g[1].cos(); + let cc = g[2].cos(); + let da = cb + cc - 1.0; + let db = cc + ca - 1.0; + let dc = ca + cb - 1.0; + if da.abs() > 1e-9 && db.abs() > 1e-9 && dc.abs() > 1e-9 { + let wa = a / da; + let wb = b / db; + let wc = c / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x104 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "X100对径", + "X_{104}", + pt(x104[0], x104[1]), + None, + "X(104) = \\bigl(a/(\\cos B+\\cos C-1):\\cdots\\bigr)", + "X(104):X(100) 在外接圆上的对径点(Λ(I,O))", + ).with(Shape::Point { + x: x104[0], + y: x104[1], + label: "X104".into(), + })); + // Note: X(104) = 2O − X(100) when both finite (own the coords first). + let o_xy = out.iter().rev().find(|f| f.group == "特殊点" && f.label == "O").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + let x100_xy = out.iter().rev().find(|f| f.group == "反补费尔巴哈" && f.label == "X_{100}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let (Some((ox, oy)), Some((px, py))) = (o_xy, x100_xy) { + let mid_dx = (x104[0] + px) / 2.0 - ox; + let mid_dy = (x104[1] + py) / 2.0 - oy; + if mid_dx * mid_dx + mid_dy * mid_dy < 1e-6 { + out.push(fact( + "X100对径", + "X_{104}=2O-X_{100}", + "\text{外接圆对径}".into(), + None, + "X(104) = 2O - X(100)", + "X(104) 与 X(100) 互为外接圆对径点", + )); + } + } + } + } + } + + // X(105): Λ(I,K); bary a/[b²+c²−a(b+c)]. Skip when any denom≈0 (equilateral). + { + let da = b * b + c * c - a * (b + c); + let db = c * c + a * a - b * (c + a); + let dc = a * a + b * b - c * (a + b); + if da.abs() > 1e-9 && db.abs() > 1e-9 && dc.abs() > 1e-9 { + let wa = a / da; + let wb = b / db; + let wc = c / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x105 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "Λ内心勒莫恩", + "X_{105}", + pt(x105[0], x105[1]), + None, + "X(105) = \\bigl(a/(b^2+c^2-a(b+c)):\\cdots\\bigr)", + "X(105):Λ(I,K)", + ).with(Shape::Point { + x: x105[0], + y: x105[1], + label: "X105".into(), + })); + } + } + } + + // X(106): Λ(I,G); bary a²/(2a−b−c). Skip equilateral / when any denom≈0. + { + let da = 2.0 * a - b - c; + let db = 2.0 * b - c - a; + let dc = 2.0 * c - a - b; + if da.abs() > 1e-9 && db.abs() > 1e-9 && dc.abs() > 1e-9 { + let wa = (a * a) / da; + let wb = (b * b) / db; + let wc = (c * c) / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x106 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "Λ内心重心", + "X_{106}", + pt(x106[0], x106[1]), + None, + "X(106) = \\bigl(a^2/(2a-b-c):\\cdots\\bigr)", + "X(106):Λ(I,G)", + ).with(Shape::Point { + x: x106[0], + y: x106[1], + label: "X106".into(), + })); + } + } + } + + // X(107): Ψ(K,H) = Ψ(X(6),X(4)); bary 1/[(b²−c²)(b²+c²−a²)²]. + // Skip isosceles (b²=c²) and right (any b²+c²−a²≈0). + { + let da = (b * b - c * c) * (b * b + c * c - a * a).powi(2); + let db = (c * c - a * a) * (c * c + a * a - b * b).powi(2); + let dc = (a * a - b * b) * (a * a + b * b - c * c).powi(2); + if da.abs() > 1e-12 && db.abs() > 1e-12 && dc.abs() > 1e-12 { + let wa = 1.0 / da; + let wb = 1.0 / db; + let wc = 1.0 / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x107 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "Ψ对称中线垂心", + "X_{107}", + pt(x107[0], x107[1]), + None, + "X(107) = \\bigl(1/[(b^2-c^2)(b^2+c^2-a^2)^2]:\\cdots\\bigr)", + "X(107):Ψ(K,H)", + ).with(Shape::Point { + x: x107[0], + y: x107[1], + label: "X107".into(), + })); + } + } + } + + // X(108): Ψ(O,H) = Ψ(X(3),X(4)) = Ψ(I,H); bary a²/(sec B−sec C). + // Skip isosceles / right (sec undefined when cos=0) / sec B=sec C. + { + let ca = g[0].cos(); + let cb = g[1].cos(); + let cc = g[2].cos(); + if ca.abs() > 1e-9 && cb.abs() > 1e-9 && cc.abs() > 1e-9 { + let da = 1.0 / cb - 1.0 / cc; + let db = 1.0 / cc - 1.0 / ca; + let dc = 1.0 / ca - 1.0 / cb; + if da.abs() > 1e-9 && db.abs() > 1e-9 && dc.abs() > 1e-9 { + let wa = (a * a) / da; + let wb = (b * b) / db; + let wc = (c * c) / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x108 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "Ψ外心垂心", + "X_{108}", + pt(x108[0], x108[1]), + None, + "X(108) = \\bigl(a^2/(\\sec B-\\sec C):\\cdots\\bigr)", + "X(108):Ψ(O,H)=Ψ(I,H)", + ).with(Shape::Point { + x: x108[0], + y: x108[1], + label: "X108".into(), + })); + } + } + } + } + + // X(109): Ψ(I,O) = Λ(I,O) partner; bary a²/(cos B−cos C). + // Circumcircle-antipode of X(102); skip isosceles. + { + let ca = g[0].cos(); + let cb = g[1].cos(); + let cc = g[2].cos(); + let da = cb - cc; + let db = cc - ca; + let dc = ca - cb; + if da.abs() > 1e-9 && db.abs() > 1e-9 && dc.abs() > 1e-9 { + let wa = (a * a) / da; + let wb = (b * b) / db; + let wc = (c * c) / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x109 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "Ψ内心外心", + "X_{109}", + pt(x109[0], x109[1]), + None, + "X(109) = \\bigl(a^2/(\\cos B-\\cos C):\\cdots\\bigr)", + "X(109):Ψ(I,O)(外接圆上;X(102) 对径)", + ).with(Shape::Point { + x: x109[0], + y: x109[1], + label: "X109".into(), + })); + } + } + } + + // X(112): Ψ(H,K) = Ψ(X(4),X(6)); bary a²/(sin 2B−sin 2C). + // Skip isosceles / when sin 2B=sin 2C (incl. right where 2A=π → sin2B=sin2C may hold). + { + let da = (2.0 * g[1]).sin() - (2.0 * g[2]).sin(); + let db = (2.0 * g[2]).sin() - (2.0 * g[0]).sin(); + let dc = (2.0 * g[0]).sin() - (2.0 * g[1]).sin(); + if da.abs() > 1e-9 && db.abs() > 1e-9 && dc.abs() > 1e-9 { + let wa = (a * a) / da; + let wb = (b * b) / db; + let wc = (c * c) / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x112 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "Ψ垂心对称中线", + "X_{112}", + pt(x112[0], x112[1]), + None, + "X(112) = \\bigl(a^2/(\\sin 2B-\\sin 2C):\\cdots\\bigr)", + "X(112):Ψ(H,K)", + ).with(Shape::Point { + x: x112[0], + y: x112[1], + label: "X112".into(), + })); + } + } + } + + // X(102): Λ(I,H) = circumcircle-antipode of X(109). Skip isosceles (no X109). + { + let x109_xy = out.iter().rev().find(|f| f.group == "Ψ内心外心" && f.label == "X_{109}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((px, py)) = x109_xy { + let x102 = [2.0 * circ[0] - px, 2.0 * circ[1] - py]; + out.push(fact( + "Λ内心垂心", + "X_{102}", + pt(x102[0], x102[1]), + None, + "X(102) = 2O - X(109) = \\Lambda(I,H)", + "X(102):Λ(I,H)(外接圆上;X(109) 对径)", + ).with(Shape::Point { + x: x102[0], + y: x102[1], + label: "X102".into(), + })); + out.push(fact( + "Λ内心垂心", + "X_{102}=2O-X_{109}", + "\\text{外接圆对径}".into(), + None, + "X(102) = 2O - X(109)", + "X(102) 与 X(109) 互为外接圆对径点", + )); + } + } + + // X(103): circumcircle-antipode of X(101); bary a²/[(a−b)cot C+(a−c)cot B]. + // Skip isosceles / when any denom≈0. + { + let cota = g[0].cos() / g[0].sin(); + let cotb = g[1].cos() / g[1].sin(); + let cotc = g[2].cos() / g[2].sin(); + let da = (a - b) * cotc + (a - c) * cotb; + let db = (b - c) * cota + (b - a) * cotc; + let dc = (c - a) * cotb + (c - b) * cota; + let mut emitted = false; + if da.abs() > 1e-9 && db.abs() > 1e-9 && dc.abs() > 1e-9 { + let wa = (a * a) / da; + let wb = (b * b) / db; + let wc = (c * c) / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x103 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "X101对径", + "X_{103}", + pt(x103[0], x103[1]), + None, + "X(103) = \\bigl(a^2/[(a-b)\\cot C+(a-c)\\cot B]:\\cdots\\bigr)", + "X(103):X(101) 在外接圆上的对径点", + ).with(Shape::Point { + x: x103[0], + y: x103[1], + label: "X103".into(), + })); + emitted = true; + let x101_xy = out.iter().rev().find(|f| f.group == "Ψ内心对称中线" && f.label == "X_{101}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((px, py)) = x101_xy { + let mid_dx = (x103[0] + px) / 2.0 - circ[0]; + let mid_dy = (x103[1] + py) / 2.0 - circ[1]; + if mid_dx * mid_dx + mid_dy * mid_dy < 1e-6 { + out.push(fact( + "X101对径", + "X_{103}=2O-X_{101}", + "\\text{外接圆对径}".into(), + None, + "X(103) = 2O - X(101)", + "X(103) 与 X(101) 互为外接圆对径点", + )); + } + } + } + } + if !emitted { + // Fallback: pure antipode of X(101) when bary denoms vanish but partner exists. + let x101_xy = out.iter().rev().find(|f| f.group == "Ψ内心对称中线" && f.label == "X_{101}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((px, py)) = x101_xy { + let x103 = [2.0 * circ[0] - px, 2.0 * circ[1] - py]; + out.push(fact( + "X101对径", + "X_{103}", + pt(x103[0], x103[1]), + None, + "X(103) = 2O - X(101)", + "X(103):X(101) 在外接圆上的对径点", + ).with(Shape::Point { + x: x103[0], + y: x103[1], + label: "X103".into(), + })); + out.push(fact( + "X101对径", + "X_{103}=2O-X_{101}", + "\\text{外接圆对径}".into(), + None, + "X(103) = 2O - X(101)", + "X(103) 与 X(101) 互为外接圆对径点", + )); + } + } + } + + // X(116): midpoint(H, X(103)); on nine-point circle. Bary (b−c)²(b²+bc+c²−ab−ac). + // Skip isosceles (any (b−c)=0). + { + let da = (b - c).powi(2) * (b * b + b * c + c * c - a * b - a * c); + let db = (c - a).powi(2) * (c * c + c * a + a * a - b * c - b * a); + let dc = (a - b).powi(2) * (a * a + a * b + b * b - c * a - c * b); + let mut emitted = false; + if da.abs() > 1e-12 && db.abs() > 1e-12 && dc.abs() > 1e-12 { + let wa = da; + let wb = db; + let wc = dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x116 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "九点X103中点", + "X_{116}", + pt(x116[0], x116[1]), + None, + "X(116) = \\bigl((b-c)^2(b^2+bc+c^2-ab-ac):\\cdots\\bigr)", + "X(116):H 与 X(103) 的中点(九点圆上)", + ).with(Shape::Point { + x: x116[0], + y: x116[1], + label: "X116".into(), + })); + emitted = true; + } + } + if !emitted { + let x103_xy = out.iter().rev().find(|f| f.group == "X101对径" && f.label == "X_{103}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((px, py)) = x103_xy { + let x116 = [(orth[0] + px) / 2.0, (orth[1] + py) / 2.0]; + out.push(fact( + "九点X103中点", + "X_{116}", + pt(x116[0], x116[1]), + None, + "X(116) = \\text{midpoint}(H, X(103))", + "X(116):H 与 X(103) 的中点(九点圆上)", + ).with(Shape::Point { + x: x116[0], + y: x116[1], + label: "X116".into(), + })); + } + } else if let Some((px, py)) = out.iter().rev().find(|f| f.group == "X101对径" && f.label == "X_{103}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }) { + // annotate midpoint relation when bary path used + let x116_xy = out.iter().rev().find(|f| f.group == "九点X103中点" && f.label == "X_{116}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((mx, my)) = x116_xy { + let dx = (orth[0] + px) / 2.0 - mx; + let dy = (orth[1] + py) / 2.0 - my; + if dx * dx + dy * dy < 1e-6 { + out.push(fact( + "九点X103中点", + "X_{116}=\\mathrm{mid}(H,X_{103})", + "\\text{中点}".into(), + None, + "X(116) = \\text{midpoint}(H, X(103))", + "X(116) 为垂心与 X(103) 的中点", + )); + } + } + } + } + + // X(117): midpoint(H, X(109)); on nine-point circle. Skip when X109 absent. + { + let x109_xy = out.iter().rev().find(|f| f.group == "Ψ内心外心" && f.label == "X_{109}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((px, py)) = x109_xy { + let x117 = [(orth[0] + px) / 2.0, (orth[1] + py) / 2.0]; + out.push(fact( + "九点X109中点", + "X_{117}", + pt(x117[0], x117[1]), + None, + "X(117) = \\text{midpoint}(H, X(109))", + "X(117):H 与 X(109) 的中点(九点圆上)", + ).with(Shape::Point { + x: x117[0], + y: x117[1], + label: "X117".into(), + })); + } + } + + // X(118): midpoint(H, X(101)); on nine-point circle. Skip when X101 absent. + { + let x101_xy = out.iter().rev().find(|f| f.group == "Ψ内心对称中线" && f.label == "X_{101}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((px, py)) = x101_xy { + let x118 = [(orth[0] + px) / 2.0, (orth[1] + py) / 2.0]; + out.push(fact( + "九点X101中点", + "X_{118}", + pt(x118[0], x118[1]), + None, + "X(118) = \\text{midpoint}(H, X(101))", + "X(118):H 与 X(101) 的中点(九点圆上)", + ).with(Shape::Point { + x: x118[0], + y: x118[1], + label: "X118".into(), + })); + } + } + + // X(119): midpoint(H, X(100)) = nine-point antipode of Feuerbach X(11). + // Skip when X100 absent (isosceles). + { + let x100_xy = out.iter().rev().find(|f| f.group == "反补费尔巴哈" && f.label == "X_{100}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((px, py)) = x100_xy { + let x119 = [(orth[0] + px) / 2.0, (orth[1] + py) / 2.0]; + out.push(fact( + "费尔巴哈九点对径", + "X_{119}", + pt(x119[0], x119[1]), + None, + "X(119) = \\text{midpoint}(H, X(100)) = 2N - Fe", + "X(119):费尔巴哈点在九点圆上的对径点", + ).with(Shape::Point { + x: x119[0], + y: x119[1], + label: "X119".into(), + })); + // Verify reflection of Fe in N when both present. + let n9 = [(circ[0] + orth[0]) / 2.0, (circ[1] + orth[1]) / 2.0]; + let fe_xy = out.iter().rev().find(|f| f.group == "费尔巴哈点" && f.label == "Fe").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((fx, fy)) = fe_xy { + let mid_dx = (x119[0] + fx) / 2.0 - n9[0]; + let mid_dy = (x119[1] + fy) / 2.0 - n9[1]; + if mid_dx * mid_dx + mid_dy * mid_dy < 1e-6 { + out.push(fact( + "费尔巴哈九点对径", + "X_{119}=2N-Fe", + "\\text{九点圆对径}".into(), + None, + "X(119) = 2N - X(11)", + "X(119) 与费尔巴哈点互为九点圆对径点", + )); + } + } + } + } + + + // X(115): center of the Kiepert hyperbola; bary (b²−c²)². + // On the nine-point circle; = midpoint(H, Tarry X(98)). Skip equilateral (all zero). + { + let da = b * b - c * c; + let db = c * c - a * a; + let dc = a * a - b * b; + let wa = da * da; + let wb = db * db; + let wc = dc * dc; + let den = wa + wb + wc; + if den > 1e-18 { + let x115 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "基伯特双曲心", + "X_{115}", + pt(x115[0], x115[1]), + None, + "X(115) = \\bigl((b^2-c^2)^2:\\cdots\\bigr)", + "X(115):基伯特双曲线的中心(九点圆上)", + ).with(Shape::Point { + x: x115[0], + y: x115[1], + label: "X115".into(), + })); + // Cross-check: midpoint(H, Tarry) when Tarry present. + let ty_xy = out.iter().rev().find(|f| f.group == "塔里点" && f.label == "Ty").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((tx, ty)) = ty_xy { + let mx = (orth[0] + tx) / 2.0; + let my = (orth[1] + ty) / 2.0; + if (mx - x115[0]).abs() < 1e-5 && (my - x115[1]).abs() < 1e-5 { + out.push(fact( + "基伯特双曲心", + "X_{115}=\\mathrm{mid}(H,Ty)", + "\\text{九点圆}".into(), + None, + "X(115) = \\text{midpoint}(H, X(98))", + "X(115) 为垂心与塔里点的中点", + )); + } + } + + // X(114): Kiepert antipode = nine-point antipode of X(115) = midpoint(H, Steiner). + let n9 = [(circ[0] + orth[0]) / 2.0, (circ[1] + orth[1]) / 2.0]; + let x114 = [2.0 * n9[0] - x115[0], 2.0 * n9[1] - x115[1]]; + out.push(fact( + "基伯特对径", + "X_{114}", + pt(x114[0], x114[1]), + None, + "X(114) = 2N - X(115) = \\text{midpoint}(H, X(99))", + "X(114):基伯特双曲心在九点圆上的对径点", + ).with(Shape::Point { + x: x114[0], + y: x114[1], + label: "X114".into(), + })); + out.push(fact( + "基伯特对径", + "X_{114}=2N-X_{115}", + "\\text{九点圆对径}".into(), + None, + "X(114) = 2N - X(115)", + "X(114) 与 X(115) 互为九点圆对径点", + )); + let st_xy = out.iter().rev().find(|f| f.group == "施泰纳点" && f.label == "St").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((sx, sy)) = st_xy { + let mx = (orth[0] + sx) / 2.0; + let my = (orth[1] + sy) / 2.0; + if (mx - x114[0]).abs() < 1e-5 && (my - x114[1]).abs() < 1e-5 { + out.push(fact( + "基伯特对径", + "X_{114}=\\mathrm{mid}(H,St)", + "\\text{九点圆}".into(), + None, + "X(114) = \\text{midpoint}(H, X(99))", + "X(114) 为垂心与施泰纳点的中点", + )); + } + } + } + } + + // X(125): center of the Jerabek hyperbola; bary (sin 2A) sin²(B−C). + // On the nine-point circle; = midpoint(H, X(74)); complement of Euler reflection X(110). + // Skip equilateral (all sin(B−C)=0). + { + let sa = (2.0 * g[0]).sin() * (g[1] - g[2]).sin().powi(2); + let sb = (2.0 * g[1]).sin() * (g[2] - g[0]).sin().powi(2); + let sc = (2.0 * g[2]).sin() * (g[0] - g[1]).sin().powi(2); + let den = sa + sb + sc; + if den.abs() > 1e-12 { + let x125 = [ + (sa * pa[0] + sb * pb[0] + sc * pc[0]) / den, + (sa * pa[1] + sb * pb[1] + sc * pc[1]) / den, + ]; + out.push(fact( + "耶拉贝克双曲心", + "X_{125}", + pt(x125[0], x125[1]), + None, + "X(125) = \\bigl(\\sin 2A\\,\\sin^2(B-C):\\cdots\\bigr)", + "X(125):耶拉贝克双曲线的中心(九点圆上)", + ).with(Shape::Point { + x: x125[0], + y: x125[1], + label: "X125".into(), + })); + let x74_xy = out.iter().rev().find(|f| f.group == "欧拉无穷等角" && f.label == "X_{74}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((px, py)) = x74_xy { + let mx = (orth[0] + px) / 2.0; + let my = (orth[1] + py) / 2.0; + if (mx - x125[0]).abs() < 1e-5 && (my - x125[1]).abs() < 1e-5 { + out.push(fact( + "耶拉贝克双曲心", + "X_{125}=\\mathrm{mid}(H,X_{74})", + "\\text{九点圆}".into(), + None, + "X(125) = \\text{midpoint}(H, X(74))", + "X(125) 为垂心与 X(74) 的中点", + )); + } + } + + // X(113): Jerabek antipode = nine-point antipode of X(125) = midpoint(H, Euler reflection). + let n9 = [(circ[0] + orth[0]) / 2.0, (circ[1] + orth[1]) / 2.0]; + let x113 = [2.0 * n9[0] - x125[0], 2.0 * n9[1] - x125[1]]; + out.push(fact( + "耶拉贝克对径", + "X_{113}", + pt(x113[0], x113[1]), + None, + "X(113) = 2N - X(125) = \\text{midpoint}(H, X(110))", + "X(113):耶拉贝克双曲心在九点圆上的对径点", + ).with(Shape::Point { + x: x113[0], + y: x113[1], + label: "X113".into(), + })); + out.push(fact( + "耶拉贝克对径", + "X_{113}=2N-X_{125}", + "\\text{九点圆对径}".into(), + None, + "X(113) = 2N - X(125)", + "X(113) 与 X(125) 互为九点圆对径点", + )); + let er_xy = out.iter().rev().find(|f| f.group == "欧拉反射点" && f.label == "E_r").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((ex, ey)) = er_xy { + let mx = (orth[0] + ex) / 2.0; + let my = (orth[1] + ey) / 2.0; + if (mx - x113[0]).abs() < 1e-5 && (my - x113[1]).abs() < 1e-5 { + out.push(fact( + "耶拉贝克对径", + "X_{113}=\\mathrm{mid}(H,E_r)", + "\\text{九点圆}".into(), + None, + "X(113) = \\text{midpoint}(H, X(110))", + "X(113) 为垂心与欧拉反射点的中点", + )); + } + } + } + } + + // X(110): Euler reflection point / focus of Kiepert parabola. + // Bary a²/(b²−c²); circumcircle antipode of X(74); = 2O − X(74). + // Skip isosceles (denom 0) / equilateral. Works on scalene right (unlike fold construction). + { + let da = b * b - c * c; + let db = c * c - a * a; + let dc = a * a - b * b; + if da.abs() > 1e-9 && db.abs() > 1e-9 && dc.abs() > 1e-9 { + let wa = (a * a) / da; + let wb = (b * b) / db; + let wc = (c * c) / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x110 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "欧拉反射点", + "X_{110}", + pt(x110[0], x110[1]), + None, + "X(110) = \\bigl(a^2/(b^2-c^2):\\cdots\\bigr)", + "X(110):欧拉反射点(基伯特抛物线焦点;外接圆上)", + ).with(Shape::Point { + x: x110[0], + y: x110[1], + label: "X110".into(), + })); + if (dist2(x110, circ) - rr).abs() < 1e-5 * (1.0 + rr) { + out.push(fact( + "欧拉反射点", + "X_{110}\\in(O)", + "\\checkmark".into(), + None, + "d(O, X(110)) = R", + "X(110) 在外接圆上", + )); + } + // Cross-check: circum-antipode of X(74). + let x74_xy = out.iter().rev().find(|f| f.group == "欧拉无穷等角" && f.label == "X_{74}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((px, py)) = x74_xy { + let mid_dx = (x110[0] + px) / 2.0 - circ[0]; + let mid_dy = (x110[1] + py) / 2.0 - circ[1]; + if mid_dx * mid_dx + mid_dy * mid_dy < 1e-6 { + out.push(fact( + "欧拉反射点", + "X_{110}=2O-X_{74}", + "\\text{外接圆对径}".into(), + None, + "X(110) = 2O - X(74)", + "X(110) 与 X(74) 互为外接圆对径点", + )); + } + } + // Cross-check geometric Er (fold of Euler line) when present. + let er_xy = out.iter().rev().find(|f| f.group == "欧拉反射点" && f.label == "E_r").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((ex, ey)) = er_xy { + if (ex - x110[0]).abs() < 1e-5 && (ey - x110[1]).abs() < 1e-5 { + out.push(fact( + "欧拉反射点", + "X_{110}=E_r", + "\\text{折线构造}".into(), + None, + "X(110) = E_r", + "X(110) 与欧拉线三边反射交点重合", + )); + } + } + // Cross-check: X(113) = midpoint(H, X(110)). + let x113_xy = out.iter().rev().find(|f| f.group == "耶拉贝克对径" && f.label == "X_{113}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((qx, qy)) = x113_xy { + let mx = (orth[0] + x110[0]) / 2.0; + let my = (orth[1] + x110[1]) / 2.0; + if (mx - qx).abs() < 1e-5 && (my - qy).abs() < 1e-5 { + out.push(fact( + "欧拉反射点", + "X_{113}=\\mathrm{mid}(H,X_{110})", + "\\text{九点圆}".into(), + None, + "X(113) = \\text{midpoint}(H, X(110))", + "X(113) 为垂心与 X(110) 的中点", + )); + } + } + } + } + } + + // X(120): X(105)-of-medial; on nine-point circle. + // Bary g(a,b,c)=(2abc−(b+c)(a²+(b−c)²))(b²+c²−ab−ac). Skip equilateral / vanishing den. + { + let ga = { + let t = 2.0 * a * b * c - (b + c) * (a * a + (b - c) * (b - c)); + t * (b * b + c * c - a * b - a * c) + }; + let gb = { + let t = 2.0 * b * c * a - (c + a) * (b * b + (c - a) * (c - a)); + t * (c * c + a * a - b * c - b * a) + }; + let gc = { + let t = 2.0 * c * a * b - (a + b) * (c * c + (a - b) * (a - b)); + t * (a * a + b * b - c * a - c * b) + }; + let den = ga + gb + gc; + if den.abs() > 1e-10 { + let x120 = [ + (ga * pa[0] + gb * pb[0] + gc * pc[0]) / den, + (ga * pa[1] + gb * pb[1] + gc * pc[1]) / den, + ]; + out.push(fact( + "中点X105", + "X_{120}", + pt(x120[0], x120[1]), + None, + "X(120) = X(105)\\text{-of-medial}", + "X(120):中点三角形的 X(105)(九点圆上)", + ).with(Shape::Point { + x: x120[0], + y: x120[1], + label: "X120".into(), + })); + let n9 = [(circ[0] + orth[0]) / 2.0, (circ[1] + orth[1]) / 2.0]; + let r9 = rr / 2.0; + if (dist2(x120, n9) - r9).abs() < 1e-4 * (1.0 + r9) { + out.push(fact( + "中点X105", + "X_{120}\\in\\text{NPC}", + "\\checkmark".into(), + None, + "d(N, X(120)) = R/2", + "X(120) 在九点圆上", + )); + } + } + } + + // X(121): X(106)-of-medial; on nine-point circle. + // Bary g=(b+c−2a)[b³+c³+a(b²+c²)−2bc(b+c)]. Skip equilateral. + { + let ga = { + let u = b + c - 2.0 * a; + let v = b * b * b + c * c * c + a * (b * b + c * c) - 2.0 * b * c * (b + c); + u * v + }; + let gb = { + let u = c + a - 2.0 * b; + let v = c * c * c + a * a * a + b * (c * c + a * a) - 2.0 * c * a * (c + a); + u * v + }; + let gc = { + let u = a + b - 2.0 * c; + let v = a * a * a + b * b * b + c * (a * a + b * b) - 2.0 * a * b * (a + b); + u * v + }; + let den = ga + gb + gc; + if den.abs() > 1e-10 { + let x121 = [ + (ga * pa[0] + gb * pb[0] + gc * pc[0]) / den, + (ga * pa[1] + gb * pb[1] + gc * pc[1]) / den, + ]; + out.push(fact( + "中点X106", + "X_{121}", + pt(x121[0], x121[1]), + None, + "X(121) = X(106)\\text{-of-medial}", + "X(121):中点三角形的 X(106)(九点圆上)", + ).with(Shape::Point { + x: x121[0], + y: x121[1], + label: "X121".into(), + })); + let n9 = [(circ[0] + orth[0]) / 2.0, (circ[1] + orth[1]) / 2.0]; + let r9 = rr / 2.0; + if (dist2(x121, n9) - r9).abs() < 1e-4 * (1.0 + r9) { + out.push(fact( + "中点X106", + "X_{121}\\in\\text{NPC}", + "\\checkmark".into(), + None, + "d(N, X(121)) = R/2", + "X(121) 在九点圆上", + )); + } + } + } + + // X(122)/X(123)/X(124)/X(126)/X(127): complements of circumcircle Ψ/Parry points + // (= X(n)-of-medial). Homothety at G with factor −1/2 maps (O) → NPC. + // Q = (3G − P)/2. Skip when the parent point was skipped (isos/equil/right). + { + let n9 = [(circ[0] + orth[0]) / 2.0, (circ[1] + orth[1]) / 2.0]; + let r9 = rr / 2.0; + let p107 = out.iter().rev().find(|f| f.group == "Ψ对称中线垂心" && f.label == "X_{107}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + let p108 = out.iter().rev().find(|f| f.group == "Ψ外心垂心" && f.label == "X_{108}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + let p109 = out.iter().rev().find(|f| f.group == "Ψ内心外心" && f.label == "X_{109}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + let p102 = out.iter().rev().find(|f| f.group == "Λ内心垂心" && f.label == "X_{102}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + let p111 = out.iter().rev().find(|f| f.group == "帕里点" && f.label == "Py").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + let p112 = out.iter().rev().find(|f| f.group == "Ψ垂心对称中线" && f.label == "X_{112}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + + if let Some((px, py)) = p107 { + let xy = [(3.0 * centroid[0] - px) / 2.0, (3.0 * centroid[1] - py) / 2.0]; + out.push(fact( + "中点X107", + "X_{122}", + pt(xy[0], xy[1]), + None, + "X(122) = \\mathrm{complement}(X(107)) = X(107)\\text{-of-medial}", + "X(122):X(107) 关于重心的补点(中点三角形的 X(107);九点圆上)", + ).with(Shape::Point { x: xy[0], y: xy[1], label: "X122".into() })); + if (dist2(xy, n9) - r9).abs() < 1e-4 * (1.0 + r9) { + out.push(fact( + "中点X107", + "X_{122}\\in\\text{NPC}", + "\\checkmark".into(), + None, + "d(N, X(122)) = R/2", + "X(122) 在九点圆上", + )); + } + let cross = (px - centroid[0]) * (xy[1] - centroid[1]) - (py - centroid[1]) * (xy[0] - centroid[0]); + if cross.abs() < 1e-6 { + out.push(fact( + "中点X107", + "G,X_{107},X_{122}", + "\\text{共线}".into(), + None, + "G, X(107), X(122) \\text{ collinear}", + "重心、X(107)、X(122) 共线", + )); + } + } + if let Some((px, py)) = p108 { + let xy = [(3.0 * centroid[0] - px) / 2.0, (3.0 * centroid[1] - py) / 2.0]; + out.push(fact( + "中点X108", + "X_{123}", + pt(xy[0], xy[1]), + None, + "X(123) = \\mathrm{complement}(X(108)) = X(108)\\text{-of-medial}", + "X(123):X(108) 关于重心的补点(中点三角形的 X(108);九点圆上)", + ).with(Shape::Point { x: xy[0], y: xy[1], label: "X123".into() })); + if (dist2(xy, n9) - r9).abs() < 1e-4 * (1.0 + r9) { + out.push(fact( + "中点X108", + "X_{123}\\in\\text{NPC}", + "\\checkmark".into(), + None, + "d(N, X(123)) = R/2", + "X(123) 在九点圆上", + )); + } + } + if let Some((px, py)) = p109 { + let xy = [(3.0 * centroid[0] - px) / 2.0, (3.0 * centroid[1] - py) / 2.0]; + out.push(fact( + "中点X109", + "X_{124}", + pt(xy[0], xy[1]), + None, + "X(124) = \\mathrm{complement}(X(109)) = \\mathrm{mid}(H,X(102))", + "X(124):X(109) 的补点 = mid(H,X(102))(九点圆上)", + ).with(Shape::Point { x: xy[0], y: xy[1], label: "X124".into() })); + if (dist2(xy, n9) - r9).abs() < 1e-4 * (1.0 + r9) { + out.push(fact( + "中点X109", + "X_{124}\\in\\text{NPC}", + "\\checkmark".into(), + None, + "d(N, X(124)) = R/2", + "X(124) 在九点圆上", + )); + } + if let Some((qx, qy)) = p102 { + let mx = (orth[0] + qx) / 2.0; + let my = (orth[1] + qy) / 2.0; + if (mx - xy[0]).abs() < 1e-5 && (my - xy[1]).abs() < 1e-5 { + out.push(fact( + "中点X109", + "X_{124}=\\mathrm{mid}(H,X_{102})", + "\\checkmark".into(), + None, + "X(124) = \\text{midpoint}(H, X(102))", + "X(124) 为垂心与 X(102) 的中点", + )); + } + } + } + if let Some((px, py)) = p111 { + let xy = [(3.0 * centroid[0] - px) / 2.0, (3.0 * centroid[1] - py) / 2.0]; + out.push(fact( + "中点X111", + "X_{126}", + pt(xy[0], xy[1]), + None, + "X(126) = \\mathrm{complement}(X(111)) = X(111)\\text{-of-medial}", + "X(126):帕里点关于重心的补点(中点三角形的 X(111);九点圆上)", + ).with(Shape::Point { x: xy[0], y: xy[1], label: "X126".into() })); + if (dist2(xy, n9) - r9).abs() < 1e-4 * (1.0 + r9) { + out.push(fact( + "中点X111", + "X_{126}\\in\\text{NPC}", + "\\checkmark".into(), + None, + "d(N, X(126)) = R/2", + "X(126) 在九点圆上", + )); + } + } + if let Some((px, py)) = p112 { + let xy = [(3.0 * centroid[0] - px) / 2.0, (3.0 * centroid[1] - py) / 2.0]; + out.push(fact( + "中点X112", + "X_{127}", + pt(xy[0], xy[1]), + None, + "X(127) = \\mathrm{complement}(X(112)) = X(112)\\text{-of-medial}", + "X(127):X(112) 关于重心的补点(中点三角形的 X(112);九点圆上)", + ).with(Shape::Point { x: xy[0], y: xy[1], label: "X127".into() })); + if (dist2(xy, n9) - r9).abs() < 1e-4 * (1.0 + r9) { + out.push(fact( + "中点X112", + "X_{127}\\in\\text{NPC}", + "\\checkmark".into(), + None, + "d(N, X(127)) = R/2", + "X(127) 在九点圆上", + )); + } + } + } + + // X(132)/X(133): mid(H, X(112))/mid(H, X(107)) on the nine-point circle. + // Also NPC antipodes of medial complements: X(132)=2N−X(127), X(133)=2N−X(122). + // Skip when the circumcircle parent was skipped (isos/equil/right as applicable). + { + let n9 = [(circ[0] + orth[0]) / 2.0, (circ[1] + orth[1]) / 2.0]; + let r9 = rr / 2.0; + let p112 = out.iter().rev().find(|f| f.group == "Ψ垂心对称中线" && f.label == "X_{112}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + let p107 = out.iter().rev().find(|f| f.group == "Ψ对称中线垂心" && f.label == "X_{107}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + let p127 = out.iter().rev().find(|f| f.group == "中点X112" && f.label == "X_{127}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + let p122 = out.iter().rev().find(|f| f.group == "中点X107" && f.label == "X_{122}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + + if let Some((px, py)) = p112 { + let xy = [(orth[0] + px) / 2.0, (orth[1] + py) / 2.0]; + out.push(fact( + "垂心X112中点", + "X_{132}", + pt(xy[0], xy[1]), + None, + "X(132) = \\mathrm{mid}(H,X(112)) = X(105)\\text{-of-orthic}", + "X(132):H 与 X(112) 的中点(垂足三角形的 X(105);九点圆上)", + ).with(Shape::Point { x: xy[0], y: xy[1], label: "X132".into() })); + if (dist2(xy, n9) - r9).abs() < 1e-4 * (1.0 + r9) { + out.push(fact( + "垂心X112中点", + "X_{132}\\in\\text{NPC}", + "\\checkmark".into(), + None, + "d(N, X(132)) = R/2", + "X(132) 在九点圆上", + )); + } + if let Some((qx, qy)) = p127 { + let rx = 2.0 * n9[0] - qx; + let ry = 2.0 * n9[1] - qy; + if (rx - xy[0]).abs() < 1e-5 && (ry - xy[1]).abs() < 1e-5 { + out.push(fact( + "垂心X112中点", + "X_{132}=2N-X_{127}", + "\\checkmark".into(), + None, + "X(132) = 2N - X(127)", + "X(132) 与 X(127) 互为九点圆对径点", + )); + } + } + } + if let Some((px, py)) = p107 { + let xy = [(orth[0] + px) / 2.0, (orth[1] + py) / 2.0]; + out.push(fact( + "垂心X107中点", + "X_{133}", + pt(xy[0], xy[1]), + None, + "X(133) = \\mathrm{mid}(H,X(107)) = X(106)\\text{-of-orthic}", + "X(133):H 与 X(107) 的中点(垂足三角形的 X(106);九点圆上)", + ).with(Shape::Point { x: xy[0], y: xy[1], label: "X133".into() })); + if (dist2(xy, n9) - r9).abs() < 1e-4 * (1.0 + r9) { + out.push(fact( + "垂心X107中点", + "X_{133}\\in\\text{NPC}", + "\\checkmark".into(), + None, + "d(N, X(133)) = R/2", + "X(133) 在九点圆上", + )); + } + if let Some((qx, qy)) = p122 { + let rx = 2.0 * n9[0] - qx; + let ry = 2.0 * n9[1] - qy; + if (rx - xy[0]).abs() < 1e-5 && (ry - xy[1]).abs() < 1e-5 { + out.push(fact( + "垂心X107中点", + "X_{133}=2N-X_{122}", + "\\checkmark".into(), + None, + "X(133) = 2N - X(122)", + "X(133) 与 X(122) 互为九点圆对径点", + )); + } + } + } + } + + // X(128)/X(129)/X(130)/X(131)/X(134)–X(139): centers of the orthic triangle on NPC. + // X(130)=Steiner-of-orthic; X(129)=Tarry-of-orthic=2N−X(130); + // X(128)=X(74)-of-orthic; X(137)=2N−X(128); + // X(134)=X(107)-of-orthic; X(135)=X(108)-of-orthic; + // X(136)=X(109)-of-orthic; X(131)=2N−X(136)=X(102)-of-orthic; + // X(138)=Parry-of-orthic; X(139)=X(112)-of-orthic. + // Skip right/near-right (orthic degenerates) and isosceles (many denoms vanish). + { + let n9 = [(circ[0] + orth[0]) / 2.0, (circ[1] + orth[1]) / 2.0]; + let r9 = rr / 2.0; + let ca = g[0].cos(); + let cb = g[1].cos(); + let cc = g[2].cos(); + let rightish = ca.abs() < 1e-9 || cb.abs() < 1e-9 || cc.abs() < 1e-9; + if !rightish { + let mut feet = [[0.0f64; 2]; 3]; + let mut ok = true; + for i in 0..3 { + let p0 = v[i]; + let q1 = v[(i + 1) % 3]; + let q2 = v[(i + 2) % 3]; + let dx = q2[0] - q1[0]; + let dy = q2[1] - q1[1]; + let den = dx * dx + dy * dy; + if den < 1e-18 { + ok = false; + break; + } + let t = ((p0[0] - q1[0]) * dx + (p0[1] - q1[1]) * dy) / den; + feet[i] = [q1[0] + t * dx, q1[1] + t * dy]; + } + if ok { + // Orthic side lengths opposite Ha, Hb, Hc. + let oa = dist2(feet[1], feet[2]); + let ob = dist2(feet[2], feet[0]); + let oc = dist2(feet[0], feet[1]); + if oa > 1e-10 && ob > 1e-10 && oc > 1e-10 { + // Steiner X(99)-of-orthic = X(130): bary 1/(b'²−c'²) at Ha:Hb:Hc. + let dbc = ob * ob - oc * oc; + let dca = oc * oc - oa * oa; + let dab = oa * oa - ob * ob; + if dbc.abs() > 1e-10 && dca.abs() > 1e-10 && dab.abs() > 1e-10 { + let wa = 1.0 / dbc; + let wb = 1.0 / dca; + let wc = 1.0 / dab; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let st = [ + (wa * feet[0][0] + wb * feet[1][0] + wc * feet[2][0]) / den, + (wa * feet[0][1] + wb * feet[1][1] + wc * feet[2][1]) / den, + ]; + if (dist2(st, n9) - r9).abs() < 1e-4 * (1.0 + r9) { + out.push(fact( + "垂足施泰纳", + "X_{130}", + pt(st[0], st[1]), + None, + "X(130) = \\mathrm{Steiner}\\text{-of-orthic}", + "X(130):垂足三角形的施泰纳点(九点圆上)", + ).with(Shape::Point { + x: st[0], + y: st[1], + label: "X130".into(), + })); + out.push(fact( + "垂足施泰纳", + "X_{130}\\in\\text{NPC}", + "\\checkmark".into(), + None, + "d(N, X(130)) = R/2", + "X(130) 在九点圆上", + )); + // Tarry-of-orthic = NPC antipode of Steiner-of-orthic. + let ty = [2.0 * n9[0] - st[0], 2.0 * n9[1] - st[1]]; + out.push(fact( + "垂足塔里", + "X_{129}", + pt(ty[0], ty[1]), + None, + "X(129) = 2N - X(130) = \\mathrm{Tarry}\\text{-of-orthic}", + "X(129):垂足三角形的塔里点(九点圆上)", + ).with(Shape::Point { + x: ty[0], + y: ty[1], + label: "X129".into(), + })); + out.push(fact( + "垂足塔里", + "X_{129}=2N-X_{130}", + "\\text{九点圆对径}".into(), + None, + "X(129) = 2N - X(130)", + "X(129) 与 X(130) 互为九点圆对径点", + )); + } + } + } + + // Orthic angles (Ha opposite oa, …); N = circumcenter of orthic. + let cos_a = ((ob * ob + oc * oc - oa * oa) / (2.0 * ob * oc)).clamp(-1.0, 1.0); + let cos_b = ((oc * oc + oa * oa - ob * ob) / (2.0 * oc * oa)).clamp(-1.0, 1.0); + let cos_c = ((oa * oa + ob * ob - oc * oc) / (2.0 * oa * ob)).clamp(-1.0, 1.0); + let sin_a = (1.0 - cos_a * cos_a).max(0.0).sqrt(); + let sin_b = (1.0 - cos_b * cos_b).max(0.0).sqrt(); + let sin_c = (1.0 - cos_c * cos_c).max(0.0).sqrt(); + + // X(74)-of-orthic = X(128): bary a'/(cos A' − 2 cos B' cos C'). + { + let da = cos_a - 2.0 * cos_b * cos_c; + let db = cos_b - 2.0 * cos_c * cos_a; + let dc = cos_c - 2.0 * cos_a * cos_b; + if da.abs() > 1e-9 && db.abs() > 1e-9 && dc.abs() > 1e-9 { + let wa = oa / da; + let wb = ob / db; + let wc = oc / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x128 = [ + (wa * feet[0][0] + wb * feet[1][0] + wc * feet[2][0]) / den, + (wa * feet[0][1] + wb * feet[1][1] + wc * feet[2][1]) / den, + ]; + if (dist2(x128, n9) - r9).abs() < 1e-4 * (1.0 + r9) { + out.push(fact( + "垂足X74", + "X_{128}", + pt(x128[0], x128[1]), + None, + "X(128) = X(74)\\text{-of-orthic}", + "X(128):垂足三角形的 X(74)(九点圆上)", + ).with(Shape::Point { + x: x128[0], + y: x128[1], + label: "X128".into(), + })); + out.push(fact( + "垂足X74", + "X_{128}\\in\\text{NPC}", + "\\checkmark".into(), + None, + "d(N, X(128)) = R/2", + "X(128) 在九点圆上", + )); + let x137 = [2.0 * n9[0] - x128[0], 2.0 * n9[1] - x128[1]]; + out.push(fact( + "垂足X74对径", + "X_{137}", + pt(x137[0], x137[1]), + None, + "X(137) = 2N - X(128) = X(110)\\text{-of-orthic}", + "X(137):X(128) 的九点圆对径点(垂足三角形的 X(110))", + ).with(Shape::Point { + x: x137[0], + y: x137[1], + label: "X137".into(), + })); + out.push(fact( + "垂足X74对径", + "X_{137}=2N-X_{128}", + "\\text{九点圆对径}".into(), + None, + "X(137) = 2N - X(128)", + "X(137) 与 X(128) 互为九点圆对径点", + )); + } + } + } + } + + // X(134)=X(107)-of-orthic: bary 1/[(b'²−c'²)(b'²+c'²−a'²)²]. + { + let da = (ob * ob - oc * oc) * (ob * ob + oc * oc - oa * oa).powi(2); + let db = (oc * oc - oa * oa) * (oc * oc + oa * oa - ob * ob).powi(2); + let dc = (oa * oa - ob * ob) * (oa * oa + ob * ob - oc * oc).powi(2); + if da.abs() > 1e-12 && db.abs() > 1e-12 && dc.abs() > 1e-12 { + let wa = 1.0 / da; + let wb = 1.0 / db; + let wc = 1.0 / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * feet[0][0] + wb * feet[1][0] + wc * feet[2][0]) / den, + (wa * feet[0][1] + wb * feet[1][1] + wc * feet[2][1]) / den, + ]; + if (dist2(xy, n9) - r9).abs() < 1e-4 * (1.0 + r9) { + out.push(fact( + "垂足X107", + "X_{134}", + pt(xy[0], xy[1]), + None, + "X(134) = X(107)\\text{-of-orthic}", + "X(134):垂足三角形的 X(107)(九点圆上)", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X134".into(), + })); + out.push(fact( + "垂足X107", + "X_{134}\\in\\text{NPC}", + "\\checkmark".into(), + None, + "d(N, X(134)) = R/2", + "X(134) 在九点圆上", + )); + } + } + } + } + + // X(135)=X(108)-of-orthic: bary a'²/(sec B'−sec C'); skip rightish orthic angles. + if cos_a.abs() > 1e-9 && cos_b.abs() > 1e-9 && cos_c.abs() > 1e-9 { + let da = 1.0 / cos_b - 1.0 / cos_c; + let db = 1.0 / cos_c - 1.0 / cos_a; + let dc = 1.0 / cos_a - 1.0 / cos_b; + if da.abs() > 1e-9 && db.abs() > 1e-9 && dc.abs() > 1e-9 { + let wa = (oa * oa) / da; + let wb = (ob * ob) / db; + let wc = (oc * oc) / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * feet[0][0] + wb * feet[1][0] + wc * feet[2][0]) / den, + (wa * feet[0][1] + wb * feet[1][1] + wc * feet[2][1]) / den, + ]; + if (dist2(xy, n9) - r9).abs() < 1e-4 * (1.0 + r9) { + out.push(fact( + "垂足X108", + "X_{135}", + pt(xy[0], xy[1]), + None, + "X(135) = X(108)\\text{-of-orthic}", + "X(135):垂足三角形的 X(108)(九点圆上)", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X135".into(), + })); + out.push(fact( + "垂足X108", + "X_{135}\\in\\text{NPC}", + "\\checkmark".into(), + None, + "d(N, X(135)) = R/2", + "X(135) 在九点圆上", + )); + } + } + } + } + + // X(136)=X(109)-of-orthic: bary a'²/(cos B'−cos C'); X(131)=2N−X(136)=X(102)-of-orthic. + { + let da = cos_b - cos_c; + let db = cos_c - cos_a; + let dc = cos_a - cos_b; + if da.abs() > 1e-9 && db.abs() > 1e-9 && dc.abs() > 1e-9 { + let wa = (oa * oa) / da; + let wb = (ob * ob) / db; + let wc = (oc * oc) / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * feet[0][0] + wb * feet[1][0] + wc * feet[2][0]) / den, + (wa * feet[0][1] + wb * feet[1][1] + wc * feet[2][1]) / den, + ]; + if (dist2(xy, n9) - r9).abs() < 1e-4 * (1.0 + r9) { + out.push(fact( + "垂足X109", + "X_{136}", + pt(xy[0], xy[1]), + None, + "X(136) = X(109)\\text{-of-orthic}", + "X(136):垂足三角形的 X(109)(九点圆上)", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X136".into(), + })); + out.push(fact( + "垂足X109", + "X_{136}\\in\\text{NPC}", + "\\checkmark".into(), + None, + "d(N, X(136)) = R/2", + "X(136) 在九点圆上", + )); + let x131 = [2.0 * n9[0] - xy[0], 2.0 * n9[1] - xy[1]]; + out.push(fact( + "垂足X102", + "X_{131}", + pt(x131[0], x131[1]), + None, + "X(131) = 2N - X(136) = X(102)\\text{-of-orthic}", + "X(131):垂足三角形的 X(102)(九点圆上)", + ).with(Shape::Point { + x: x131[0], + y: x131[1], + label: "X131".into(), + })); + out.push(fact( + "垂足X102", + "X_{131}=2N-X_{136}", + "\\text{九点圆对径}".into(), + None, + "X(131) = 2N - X(136)", + "X(131) 与 X(136) 互为九点圆对径点(N 反射)", + )); + } + } + } + } + + // X(138)=Parry-of-orthic = X(111)-of-orthic: bary a'²/(2a'²−b'²−c'²). + { + let da = 2.0 * oa * oa - ob * ob - oc * oc; + let db = 2.0 * ob * ob - oc * oc - oa * oa; + let dc = 2.0 * oc * oc - oa * oa - ob * ob; + if da.abs() > 1e-10 && db.abs() > 1e-10 && dc.abs() > 1e-10 { + let wa = (oa * oa) / da; + let wb = (ob * ob) / db; + let wc = (oc * oc) / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * feet[0][0] + wb * feet[1][0] + wc * feet[2][0]) / den, + (wa * feet[0][1] + wb * feet[1][1] + wc * feet[2][1]) / den, + ]; + if (dist2(xy, n9) - r9).abs() < 1e-4 * (1.0 + r9) { + out.push(fact( + "垂足帕里", + "X_{138}", + pt(xy[0], xy[1]), + None, + "X(138) = X(111)\\text{-of-orthic}", + "X(138):垂足三角形的帕里点(九点圆上)", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X138".into(), + })); + out.push(fact( + "垂足帕里", + "X_{138}\\in\\text{NPC}", + "\\checkmark".into(), + None, + "d(N, X(138)) = R/2", + "X(138) 在九点圆上", + )); + } + } + } + } + + // X(139)=X(112)-of-orthic: bary a'²/(sin 2B'−sin 2C'). + { + let sin2a = 2.0 * sin_a * cos_a; + let sin2b = 2.0 * sin_b * cos_b; + let sin2c = 2.0 * sin_c * cos_c; + let da = sin2b - sin2c; + let db = sin2c - sin2a; + let dc = sin2a - sin2b; + if da.abs() > 1e-9 && db.abs() > 1e-9 && dc.abs() > 1e-9 { + let wa = (oa * oa) / da; + let wb = (ob * ob) / db; + let wc = (oc * oc) / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * feet[0][0] + wb * feet[1][0] + wc * feet[2][0]) / den, + (wa * feet[0][1] + wb * feet[1][1] + wc * feet[2][1]) / den, + ]; + if (dist2(xy, n9) - r9).abs() < 1e-4 * (1.0 + r9) { + out.push(fact( + "垂足X112", + "X_{139}", + pt(xy[0], xy[1]), + None, + "X(139) = X(112)\\text{-of-orthic}", + "X(139):垂足三角形的 X(112)(九点圆上)", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X139".into(), + })); + out.push(fact( + "垂足X112", + "X_{139}\\in\\text{NPC}", + "\\checkmark".into(), + None, + "d(N, X(139)) = R/2", + "X(139) 在九点圆上", + )); + } + } + } + } + } + } + } + } + + // X(140)–X(153): Euler/medial complements, anticomplements of Ge/Na, and + // reflections of de Longchamps X(20) in classical circumcircle points. + // X(140)=mid(O,N)=complement(X(5)); X(141)=complement(K), bary b²+c²:…; + // X(142)=mid(Ge,Mi)=complement(X(9)); X(143)=mid(N,X(52))=X(5)-of-orthic; + // X(144)=anticomplement(Ge); X(145)=anticomplement(Na); + // X(146)=2·X(110)−X(20); X(147)=2·X(99)−X(20); X(148)=2·X(98)−X(20); + // X(149)=2·X(104)−X(20); X(150)=2·X(103)−X(20); + // X(151)=2·X(109)−X(20); X(152)=2·X(101)−X(20); X(153)=2·X(100)−X(20). + { + let n9 = [(circ[0] + orth[0]) / 2.0, (circ[1] + orth[1]) / 2.0]; + // X(140) = midpoint(O, N) = complement of X(5); on Euler line. Always. + let x140 = [(circ[0] + n9[0]) / 2.0, (circ[1] + n9[1]) / 2.0]; + out.push(fact( + "九点补点", + "X_{140}", + pt(x140[0], x140[1]), + None, + "X(140) = \\mathrm{mid}(O,N) = \\mathrm{complement}(X(5))", + "X(140):O 与 N 的中点(中点三角形的九点心;欧拉线上)", + ).with(Shape::Point { + x: x140[0], + y: x140[1], + label: "X140".into(), + })); + // Collinear O, X140, N, G, H (Euler): X140 divides O N. + let cross_e = (x140[0] - circ[0]) * (orth[1] - circ[1]) + - (x140[1] - circ[1]) * (orth[0] - circ[0]); + if cross_e.abs() < 1e-6 { + out.push(fact( + "九点补点", + "X_{140}\\in\\text{Euler}", + "\\checkmark".into(), + None, + "X(140)\\in\\text{Euler line}", + "X(140) 在欧拉线上", + )); + } + + // X(141) = complement of Lemoine/symmedian X(6); bary b²+c² : c²+a² : a²+b². + { + let wa = b * b + c * c; + let wb = c * c + a * a; + let wc = a * a + b * b; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let x141 = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "对称中线补点", + "X_{141}", + pt(x141[0], x141[1]), + None, + "X(141) = (b^2+c^2:c^2+a^2:a^2+b^2) = \\mathrm{complement}(X(6))", + "X(141):勒莫恩点的补点(中点三角形的对称中线点)", + ).with(Shape::Point { + x: x141[0], + y: x141[1], + label: "X141".into(), + })); + // Cross-check: complement of K via h(G,−1/2). + let k_xy = out.iter().rev().find(|f| f.group == "勒莫恩点" && f.label == "K").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((kx, ky)) = k_xy { + let cx = (3.0 * centroid[0] - kx) / 2.0; + let cy = (3.0 * centroid[1] - ky) / 2.0; + if (cx - x141[0]).abs() < 1e-5 && (cy - x141[1]).abs() < 1e-5 { + out.push(fact( + "对称中线补点", + "X_{141}=\\mathrm{complement}(K)", + "\\checkmark".into(), + None, + "X(141) = (3G - K)/2", + "X(141) 为对称中线点关于重心的补点", + )); + } + } + } + } + + // X(142) = midpoint(Ge, Mi) = complement of Mittenpunkt X(9). + let ge_xy = out.iter().rev().find(|f| f.group == "格尔巩点" && f.label == "Ge").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + let mi_xy = out.iter().rev().find(|f| f.group == "米滕点" && f.label == "Mi").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let (Some((gx, gy)), Some((mx, my))) = (ge_xy, mi_xy) { + let x142 = [(gx + mx) / 2.0, (gy + my) / 2.0]; + out.push(fact( + "米滕补点", + "X_{142}", + pt(x142[0], x142[1]), + None, + "X(142) = \\mathrm{mid}(X(7),X(9)) = \\mathrm{complement}(X(9))", + "X(142):格尔巩与米滕的中点(米滕点的补点)", + ).with(Shape::Point { + x: x142[0], + y: x142[1], + label: "X142".into(), + })); + let cx = (3.0 * centroid[0] - mx) / 2.0; + let cy = (3.0 * centroid[1] - my) / 2.0; + if (cx - x142[0]).abs() < 1e-5 && (cy - x142[1]).abs() < 1e-5 { + out.push(fact( + "米滕补点", + "X_{142}=\\mathrm{complement}(Mi)", + "\\checkmark".into(), + None, + "X(142) = (3G - Mi)/2", + "X(142) 为米滕点关于重心的补点", + )); + } + } + + // X(143) = midpoint(N, X(52)) = X(5)-of-orthic; skip right (no X52). + let x52_xy = out.iter().rev().find(|f| f.group == "垂足垂心" && f.label == "X_{52}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((sx, sy)) = x52_xy { + let x143 = [(n9[0] + sx) / 2.0, (n9[1] + sy) / 2.0]; + out.push(fact( + "垂足九点心", + "X_{143}", + pt(x143[0], x143[1]), + None, + "X(143) = \\mathrm{mid}(N,X(52)) = X(5)\\text{-of-orthic}", + "X(143):垂足三角形的九点圆心(锐角)", + ).with(Shape::Point { + x: x143[0], + y: x143[1], + label: "X143".into(), + })); + } + + // X(144) = anticomplement of Gergonne X(7) = 3G − 2 Ge. + if let Some((gx, gy)) = ge_xy { + let x144 = [3.0 * centroid[0] - 2.0 * gx, 3.0 * centroid[1] - 2.0 * gy]; + out.push(fact( + "格尔巩反补", + "X_{144}", + pt(x144[0], x144[1]), + None, + "X(144) = 3G - 2\\,X(7) = \\mathrm{anticomplement}(X(7))", + "X(144):格尔巩点的反补点(反补三角形的 X(7))", + ).with(Shape::Point { + x: x144[0], + y: x144[1], + label: "X144".into(), + })); + } + + // X(145) = anticomplement of Nagel X(8) = 3G − 2 Na. + let na_xy = out.iter().rev().find(|f| f.group == "奈格尔点" && f.label == "Na").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((nx, ny)) = na_xy { + let x145 = [3.0 * centroid[0] - 2.0 * nx, 3.0 * centroid[1] - 2.0 * ny]; + out.push(fact( + "奈格尔反补", + "X_{145}", + pt(x145[0], x145[1]), + None, + "X(145) = 3G - 2\\,X(8) = \\mathrm{anticomplement}(X(8))", + "X(145):奈格尔点的反补点(反补三角形的 X(8))", + ).with(Shape::Point { + x: x145[0], + y: x145[1], + label: "X145".into(), + })); + // Cross-check bary 3a−b−c : … + let wa = 3.0 * a - b - c; + let wb = 3.0 * b - c - a; + let wc = 3.0 * c - a - b; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let bx = (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den; + let by = (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den; + if (bx - x145[0]).abs() < 1e-5 && (by - x145[1]).abs() < 1e-5 { + out.push(fact( + "奈格尔反补", + "X_{145}=(3a-b-c:\\cdots)", + "\\checkmark".into(), + None, + "X(145) = (3a-b-c:3b-c-a:3c-a-b)", + "X(145) 重心坐标与反补构造一致", + )); + } + } + } + + // Helpers for reflections of L = X(20) in circumcircle points. + let l_xy = out.iter().rev().find(|f| f.group == "德龙尚点" && f.label == "L").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((lx, ly)) = l_xy { + let reflect_l = |qx: f64, qy: f64| -> [f64; 2] { + [2.0 * qx - lx, 2.0 * qy - ly] + }; + + // X(146) = reflection of X(20) in X(110). Skip when X110 missing (isos). + let x110_xy = out.iter().rev().find(|f| f.group == "欧拉反射点" && f.label == "X_{110}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((qx, qy)) = x110_xy { + let xy = reflect_l(qx, qy); + out.push(fact( + "德龙尚X110反射", + "X_{146}", + pt(xy[0], xy[1]), + None, + "X(146) = 2\\,X(110) - X(20)", + "X(146):德龙尚点关于 X(110) 的反射(反补三角形的 X(74))", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X146".into(), + })); + } + + // X(147) = reflection of X(20) in Steiner X(99). + let st_xy = out.iter().rev().find(|f| f.group == "施泰纳点" && f.label == "St").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((qx, qy)) = st_xy { + let xy = reflect_l(qx, qy); + out.push(fact( + "德龙尚施泰纳反射", + "X_{147}", + pt(xy[0], xy[1]), + None, + "X(147) = 2\\,X(99) - X(20) = X(98)\\text{-of-anticomplementary}", + "X(147):德龙尚点关于施泰纳点的反射(反补三角形的塔里点)", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X147".into(), + })); + } + + // X(148) = reflection of X(20) in Tarry X(98). + let ty_xy = out.iter().rev().find(|f| f.group == "塔里点" && f.label == "Ty").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((qx, qy)) = ty_xy { + let xy = reflect_l(qx, qy); + out.push(fact( + "德龙尚塔里反射", + "X_{148}", + pt(xy[0], xy[1]), + None, + "X(148) = 2\\,X(98) - X(20) = X(99)\\text{-of-anticomplementary}", + "X(148):德龙尚点关于塔里点的反射(反补三角形的施泰纳点)", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X148".into(), + })); + } + + // X(149) = reflection of X(20) in X(104). + let x104_xy = out.iter().rev().find(|f| f.group == "X100对径" && f.label == "X_{104}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((qx, qy)) = x104_xy { + let xy = reflect_l(qx, qy); + out.push(fact( + "德龙尚X104反射", + "X_{149}", + pt(xy[0], xy[1]), + None, + "X(149) = 2\\,X(104) - X(20) = X(100)\\text{-of-anticomplementary}", + "X(149):德龙尚点关于 X(104) 的反射(反补三角形的 X(100))", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X149".into(), + })); + } + + // X(150) = reflection of X(20) in X(103). + let x103_xy = out.iter().rev().find(|f| f.group == "X101对径" && f.label == "X_{103}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((qx, qy)) = x103_xy { + let xy = reflect_l(qx, qy); + out.push(fact( + "德龙尚X103反射", + "X_{150}", + pt(xy[0], xy[1]), + None, + "X(150) = 2\\,X(103) - X(20) = X(101)\\text{-of-anticomplementary}", + "X(150):德龙尚点关于 X(103) 的反射(反补三角形的 X(101))", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X150".into(), + })); + } + + // X(151) = reflection of X(20) in X(109) = X(102)-of-anticomplementary. + let x109_xy = out.iter().rev().find(|f| f.group == "Ψ内心外心" && f.label == "X_{109}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((qx, qy)) = x109_xy { + let xy = reflect_l(qx, qy); + out.push(fact( + "德龙尚X109反射", + "X_{151}", + pt(xy[0], xy[1]), + None, + "X(151) = 2\\,X(109) - X(20) = X(102)\\text{-of-anticomplementary}", + "X(151):德龙尚点关于 X(109) 的反射(反补三角形的 X(102))", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X151".into(), + })); + } + + // X(152) = reflection of X(20) in X(101) = X(103)-of-anticomplementary. + let x101_xy = out.iter().rev().find(|f| f.group == "Ψ内心对称中线" && f.label == "X_{101}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((qx, qy)) = x101_xy { + let xy = reflect_l(qx, qy); + out.push(fact( + "德龙尚X101反射", + "X_{152}", + pt(xy[0], xy[1]), + None, + "X(152) = 2\\,X(101) - X(20) = X(103)\\text{-of-anticomplementary}", + "X(152):德龙尚点关于 X(101) 的反射(反补三角形的 X(103))", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X152".into(), + })); + } + + // X(153) = reflection of X(20) in X(100) = X(104)-of-anticomplementary. + let x100_xy = out.iter().rev().find(|f| f.group == "反补费尔巴哈" && f.label == "X_{100}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((qx, qy)) = x100_xy { + let xy = reflect_l(qx, qy); + out.push(fact( + "德龙尚X100反射", + "X_{153}", + pt(xy[0], xy[1]), + None, + "X(153) = 2\\,X(100) - X(20) = X(104)\\text{-of-anticomplementary}", + "X(153):德龙尚点关于 X(100) 的反射(反补三角形的 X(104))", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X153".into(), + })); + } + } + } + + // X(154)–X(156): tangential / orthic eigencenter family. + // X(154)=X(3)-Ceva conjugate of X(6)=X(2)-of-tangential; + // bary a³(cos A − cos B cos C). + // X(155)=eigencenter of orthic; bary a cos A (cos²B+cos²C−cos²A); + // = reflection of X(68) in X(5) when acute. + // X(156)=X(5)-of-tangential = midpoint(X(26), X(155)). + { + let ca = [g[0].cos(), g[1].cos(), g[2].cos()]; + // Right △: X(154)/X(155) bary sums vanish (point at infinity) — skip. + let rightish = ca[0].abs() < 1e-9 || ca[1].abs() < 1e-9 || ca[2].abs() < 1e-9; + // X(154) + { + let wa = a * a * a * (ca[0] - ca[1] * ca[2]); + let wb = b * b * b * (ca[1] - ca[2] * ca[0]); + let wc = c * c * c * (ca[2] - ca[0] * ca[1]); + let den = wa + wb + wc; + if !rightish && den.abs() > 1e-12 { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "切线重心", + "X_{154}", + pt(xy[0], xy[1]), + None, + "X(154) = \\bigl(a^3(\\cos A-\\cos B\\cos C):\\cdots\\bigr) = X(2)\\text{-of-tangential}", + "X(154):切线三角形的重心(X(3)-Ceva 共轭于 X(6))", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X154".into(), + })); + } + } + // X(155) + let mut x155_xy: Option<[f64; 2]> = None; + { + let wa = a * ca[0] * (ca[1] * ca[1] + ca[2] * ca[2] - ca[0] * ca[0]); + let wb = b * ca[1] * (ca[2] * ca[2] + ca[0] * ca[0] - ca[1] * ca[1]); + let wc = c * ca[2] * (ca[0] * ca[0] + ca[1] * ca[1] - ca[2] * ca[2]); + let den = wa + wb + wc; + if !rightish && den.abs() > 1e-12 { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + x155_xy = Some(xy); + out.push(fact( + "垂足特征心", + "X_{155}", + pt(xy[0], xy[1]), + None, + "X(155) = \\bigl(a\\cos A(\\cos^2 B+\\cos^2 C-\\cos^2 A):\\cdots\\bigr)", + "X(155):垂足三角形的特征中心(切线三角形的垂心)", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X155".into(), + })); + // Cross-check: reflection of X(68) in nine-point center N=X(5). + let n9 = [(circ[0] + orth[0]) / 2.0, (circ[1] + orth[1]) / 2.0]; + let x68_xy = out.iter().rev().find(|f| f.group == "普拉索洛夫点" && f.label == "X_{68}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((qx, qy)) = x68_xy { + let rx = 2.0 * n9[0] - qx; + let ry = 2.0 * n9[1] - qy; + if (rx - xy[0]).abs() < 1e-5 && (ry - xy[1]).abs() < 1e-5 { + out.push(fact( + "垂足特征心", + "X_{155}=2N-X_{68}", + "\\checkmark".into(), + None, + "X(155) = 2\\,X(5) - X(68)", + "X(155) 为普拉索洛夫点关于九点圆心的反射", + )); + } + } + } + } + // X(156) = mid(X(26), X(155)) = X(5)-of-tangential. + if let Some(x155) = x155_xy { + let x26_xy = out.iter().rev().find(|f| f.group == "切线三角形外心" && f.label == "X_{26}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((qx, qy)) = x26_xy { + let xy = [(qx + x155[0]) / 2.0, (qy + x155[1]) / 2.0]; + out.push(fact( + "切线九点心", + "X_{156}", + pt(xy[0], xy[1]), + None, + "X(156) = \\mathrm{mid}(X(26),X(155)) = X(5)\\text{-of-tangential}", + "X(156):切线三角形的九点圆心(X(26) 与 X(155) 的中点)", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X156".into(), + })); + } + } + } + + + + + // X(157)–X(159), X(163)–X(165): tangential symmedian / sec-tan / X66-reflection / + // trilinear-product, and excentral incenter/centroid. + // X(157)=X(6)-of-tangential; bary a²(b³ cos B + c³ cos C − a³ cos A). + // X(158)=bary sec A tan A = sin A / cos² A; skip right (sec ∞). + // X(159)=X(9)-of-tangential = reflection of X(66) in X(141). + // X(163)=trilinear product X(6)·X(110); bary a³/(b²−c²); skip isosceles. + // X(164)=X(1)-of-excentral; bary a(sin(B/2)+sin(C/2)−sin(A/2)). + // X(165)=X(2)-of-excentral; bary a(tan(B/2)+tan(C/2)−tan(A/2)); + // = centroid(I, Na, L); on OI as {O, Be}-harmonic conjugate of I. + { + let ca = [g[0].cos(), g[1].cos(), g[2].cos()]; + let rightish = ca[0].abs() < 1e-9 || ca[1].abs() < 1e-9 || ca[2].abs() < 1e-9; + // X(157) + { + let fa = b * b * b * ca[1] + c * c * c * ca[2] - a * a * a * ca[0]; + let fb = c * c * c * ca[2] + a * a * a * ca[0] - b * b * b * ca[1]; + let fc = a * a * a * ca[0] + b * b * b * ca[1] - c * c * c * ca[2]; + let wa = a * a * fa; + let wb = b * b * fb; + let wc = c * c * fc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "切线对称中线", + "X_{157}", + pt(xy[0], xy[1]), + None, + "X(157) = \\bigl(a^2(b^3\\cos B+c^3\\cos C-a^3\\cos A):\\cdots\\bigr) = X(6)\\text{-of-tangential}", + "X(157):切线三角形的对称中线点", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X157".into(), + })); + } + } + // X(158) + if !rightish { + let sa_ = [g[0].sin(), g[1].sin(), g[2].sin()]; + let wa = sa_[0] / (ca[0] * ca[0]); + let wb = sa_[1] / (ca[1] * ca[1]); + let wc = sa_[2] / (ca[2] * ca[2]); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "正割正切点", + "X_{158}", + pt(xy[0], xy[1]), + None, + "X(158) = (\\sec A\\tan A:\\sec B\\tan B:\\sec C\\tan C)", + "X(158):正割–正切重心坐标点(X(19) 与 X(92) 的截共轭)", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X158".into(), + })); + } + } + // X(159) = 2·X(141) − X(66) = X(9)-of-tangential. + { + let x66_xy = out.iter().rev().find(|f| f.group == "埃克塞特等角" && f.label == "X_{66}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + let x141_xy = out.iter().rev().find(|f| f.group == "对称中线补点" && f.label == "X_{141}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let (Some((qx, qy)), Some((mx, my))) = (x66_xy, x141_xy) { + let xy = [2.0 * mx - qx, 2.0 * my - qy]; + out.push(fact( + "切线米滕", + "X_{159}", + pt(xy[0], xy[1]), + None, + "X(159) = 2\\,X(141) - X(66) = X(9)\\text{-of-tangential}", + "X(159):切线三角形的米滕点(X(66) 关于 X(141) 的反射)", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X159".into(), + })); + } + } + // X(163): bary a³/(b²−c²); skip isosceles (denom 0). + { + let a2 = a * a; + let b2 = b * b; + let c2 = c * c; + let da = b2 - c2; + let db = c2 - a2; + let dc = a2 - b2; + if da.abs() > 1e-12 && db.abs() > 1e-12 && dc.abs() > 1e-12 { + let wa = a * a2 / da; + let wb = b * b2 / db; + let wc = c * c2 / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "对称中线X110积", + "X_{163}", + pt(xy[0], xy[1]), + None, + "X(163) = \\bigl(a^3/(b^2-c^2):\\cdots\\bigr) = X(6)\\cdot X(110)\\text{ (trilinear)}", + "X(163):对称中线点与 X(110) 的三线积", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X163".into(), + })); + } + } + } + // X(164) / X(165): excentral incenter & centroid via half-angles. + { + let ha = g[0] / 2.0; + let hb = g[1] / 2.0; + let hc = g[2] / 2.0; + let s_ha = ha.sin(); + let s_hb = hb.sin(); + let s_hc = hc.sin(); + // X(164) + { + let wa = a * (s_hb + s_hc - s_ha); + let wb = b * (s_hc + s_ha - s_hb); + let wc = c * (s_ha + s_hb - s_hc); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "旁心内心", + "X_{164}", + pt(xy[0], xy[1]), + None, + "X(164) = \\bigl(a(\\sin\\tfrac{B}{2}+\\sin\\tfrac{C}{2}-\\sin\\tfrac{A}{2}):\\cdots\\bigr) = X(1)\\text{-of-excentral}", + "X(164):旁心三角形的内心", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X164".into(), + })); + } + } + // X(165) + let t_ha = ha.tan(); + let t_hb = hb.tan(); + let t_hc = hc.tan(); + if t_ha.is_finite() && t_hb.is_finite() && t_hc.is_finite() { + let wa = a * (t_hb + t_hc - t_ha); + let wb = b * (t_hc + t_ha - t_hb); + let wc = c * (t_ha + t_hb - t_hc); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "旁心重心", + "X_{165}", + pt(xy[0], xy[1]), + None, + "X(165) = \\bigl(a(\\tan\\tfrac{B}{2}+\\tan\\tfrac{C}{2}-\\tan\\tfrac{A}{2}):\\cdots\\bigr) = X(2)\\text{-of-excentral}", + "X(165):旁心三角形的重心(在 OI 线上)", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X165".into(), + })); + // Cross-check: centroid of I, Nagel, de Longchamps. + let i_xy = out.iter().rev().find(|f| f.group == "特殊点" && f.label == "I").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + let na_xy = out.iter().rev().find(|f| f.group == "奈格尔点" && f.label == "Na").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + let l_xy = out.iter().rev().find(|f| f.group == "德龙尚点" && f.label == "L").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let (Some((ix, iy)), Some((nx, ny)), Some((lx, ly))) = (i_xy, na_xy, l_xy) { + let cx = (ix + nx + lx) / 3.0; + let cy = (iy + ny + ly) / 3.0; + if (cx - xy[0]).abs() < 1e-5 && (cy - xy[1]).abs() < 1e-5 { + out.push(fact( + "旁心重心", + "X_{165}=\\mathrm{cent}(I,Na,L)", + "\\checkmark".into(), + None, + "X(165) = \\mathrm{centroid}(X(1),X(8),X(20))", + "X(165) 为内心、奈格尔、德龙尚三点的重心", + )); + } + } + // Cross-check: on OI — {O, Be}-harmonic conjugate of I → (4/3)O − (1/3)I. + let be_xy = out.iter().rev().find(|f| f.group == "贝万点" && f.label == "Be").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let (Some((ix, iy)), Some((_bx, _by))) = (i_xy, be_xy) { + let hx = (4.0 * circ[0] - ix) / 3.0; + let hy = (4.0 * circ[1] - iy) / 3.0; + if (hx - xy[0]).abs() < 1e-5 && (hy - xy[1]).abs() < 1e-5 { + out.push(fact( + "旁心重心", + "X_{165}\\in OI", + "\\checkmark".into(), + None, + "X(165) = \\tfrac{4}{3}O - \\tfrac{1}{3}I = \\{O,X(40)\\}\\text{-harmonic of }I", + "X(165) 为 I 关于 O 与贝万点的调和共轭(在 OI 上)", + )); + } + } + } + } + } + } + + // X(160)–X(162): fill tangential gaps; then classical X(168)/X(169)/X(171)–X(173). + // X(160)=X(37)-of-tangential (acute): bary a·f with + // f=a[(b²+c²)sin 2A+(c²−a²)sin 2B+(b²−a²)sin 2C] (ETC: sin 2A, not sin²A). + // X(161)=X(63)-of-tangential (acute): bary a·f; skip right (Σw=0); with + // f=a[(a²+b²+c²)sin²(2A)+(c²−b²−a²)sin²(2B)+(b²−c²−a²)sin²(2C)]. + // X(162)=cevapoint of X(108),X(109): bary a/(sin 2B−sin 2C); skip isos/right (denom 0; right ⇒ sin 2A=sin 2B). + // X(168)=X(9)-of-excentral (mittenpunkt): trilinear b/(1−sin(B/2))+c/(1−sin(C/2))−a/(1−sin(A/2)). + // X(169)=X(85)-Ceva conjugate of X(1): bary a·(a³−a²(b+c)+a(b²+c²)−(b−c)²(b+c)). + // X(171)={X(2),X(31)}-harmonic of X(238): bary a³+abc; on OI. + // X(172)=trilinear product X(6)·X(171): bary a⁴+bc a². + // X(173)=congruent isoscelizers: bary a(cos(B/2)+cos(C/2)−cos(A/2)). + { + let a2 = a * a; + let b2 = b * b; + let c2 = c * c; + let s2a = (2.0 * g[0]).sin(); + let s2b = (2.0 * g[1]).sin(); + let s2c = (2.0 * g[2]).sin(); + // X(160) + { + let fa = a * ((b2 + c2) * s2a + (c2 - a2) * s2b + (b2 - a2) * s2c); + let fb = b * ((c2 + a2) * s2b + (a2 - b2) * s2c + (c2 - b2) * s2a); + let fc = c * ((a2 + b2) * s2c + (b2 - c2) * s2a + (a2 - c2) * s2b); + let wa = a * fa; + let wb = b * fb; + let wc = c * fc; + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push(fact( + "切线格林伯格", + "X_{160}", + pt(xy[0], xy[1]), + None, + "X(160) = X(37)\\text{-of-tangential (acute)};\\ f=a[(b^2+c^2)\\sin 2A+(c^2-a^2)\\sin 2B+(b^2-a^2)\\sin 2C]", + "X(160):切线三角形的格林伯格点(锐角时)", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X160".into(), + })); + } + } + } + // X(161): skip right (Σweights → 0 when one angle is 90°). + let rightish = g[0].cos().abs() < 1e-9 || g[1].cos().abs() < 1e-9 || g[2].cos().abs() < 1e-9; + if !rightish { + let ss2a = s2a * s2a; + let ss2b = s2b * s2b; + let ss2c = s2c * s2c; + let s2 = a2 + b2 + c2; + let fa = a * (s2 * ss2a + (c2 - b2 - a2) * ss2b + (b2 - c2 - a2) * ss2c); + let fb = b * (s2 * ss2b + (a2 - c2 - b2) * ss2c + (c2 - a2 - b2) * ss2a); + let fc = c * (s2 * ss2c + (b2 - a2 - c2) * ss2a + (a2 - b2 - c2) * ss2b); + let wa = a * fa; + let wb = b * fb; + let wc = c * fc; + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push(fact( + "切线X63", + "X_{161}", + pt(xy[0], xy[1]), + None, + "X(161) = X(63)\\text{-of-tangential (acute)};\\ f=a[(a^2+b^2+c^2)\\sin^2 2A+\\cdots]", + "X(161):切线三角形的 X(63)(锐角时)", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X161".into(), + })); + } + } + } + // X(162): skip isosceles / when sin 2B ≈ sin 2C. + { + let da = s2b - s2c; + let db = s2c - s2a; + let dc = s2a - s2b; + if da.abs() > 1e-12 && db.abs() > 1e-12 && dc.abs() > 1e-12 { + let wa = a / da; + let wb = b / db; + let wc = c / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push(fact( + "X108X109截点", + "X_{162}", + pt(xy[0], xy[1]), + None, + "X(162) = \\bigl(a/(\\sin 2B-\\sin 2C):\\cdots\\bigr) = \\text{cevapoint of }X(108),X(109)", + "X(162):X(108) 与 X(109) 的截点", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X162".into(), + })); + } + } + } + } + // X(168) mittenpunkt of excentral + { + let s_ha = (g[0] / 2.0).sin(); + let s_hb = (g[1] / 2.0).sin(); + let s_hc = (g[2] / 2.0).sin(); + let d_a = 1.0 - s_ha; + let d_b = 1.0 - s_hb; + let d_c = 1.0 - s_hc; + if d_a.abs() > 1e-12 && d_b.abs() > 1e-12 && d_c.abs() > 1e-12 { + let fa = b / d_b + c / d_c - a / d_a; + let fb = c / d_c + a / d_a - b / d_b; + let fc = a / d_a + b / d_b - c / d_c; + let wa = a * fa; + let wb = b * fb; + let wc = c * fc; + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push(fact( + "旁心米滕", + "X_{168}", + pt(xy[0], xy[1]), + None, + "X(168) = X(9)\\text{-of-excentral};\\ f=b/(1-\\sin\\tfrac{B}{2})+c/(1-\\sin\\tfrac{C}{2})-a/(1-\\sin\\tfrac{A}{2})", + "X(168):旁心三角形的米滕点", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X168".into(), + })); + } + } + } + } + // X(169): X(85)-Ceva conjugate of I; polynomial trilinear × a. + { + let t169 = |aa: f64, bb: f64, cc: f64| -> f64 { + aa * aa * aa - aa * aa * (bb + cc) + aa * (bb * bb + cc * cc) - (bb - cc) * (bb - cc) * (bb + cc) + }; + let fa = t169(a, b, c); + let fb = t169(b, c, a); + let fc = t169(c, a, b); + let wa = a * fa; + let wb = b * fb; + let wc = c * fc; + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push(fact( + "X85截共轭I", + "X_{169}", + pt(xy[0], xy[1]), + None, + "X(169) = X(85)\\text{-Ceva conjugate of }X(1);\\ a^3-a^2(b+c)+a(b^2+c^2)-(b-c)^2(b+c)", + "X(169):X(85) 关于内心的截共轭", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X169".into(), + })); + } + } + } + // X(171) / X(172) / X(173) + { + let abc = a * b * c; + let wa = a * a * a + abc; + let wb = b * b * b + abc; + let wc = c * c * c + abc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "调和X238", + "X_{171}", + pt(xy[0], xy[1]), + None, + "X(171) = (a^3+abc:b^3+abc:c^3+abc) = \\{X(2),X(31)\\}\\text{-harmonic of }X(238)", + "X(171):{X(2),X(31)} 关于 X(238) 的调和共轭(在 OI 上)", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X171".into(), + })); + // Cross-check collinear with O,I (lines 1,3). + let i_xy = out.iter().rev().find(|f| f.group == "特殊点" && f.label == "I").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let Some((ix, iy)) = i_xy { + let ox = circ[0]; + let oy = circ[1]; + let cross = (ix - ox) * (xy[1] - oy) - (iy - oy) * (xy[0] - ox); + let scale = ((ix - ox).hypot(iy - oy) + (xy[0] - ox).hypot(xy[1] - oy)).max(1.0); + if cross.abs() < 1e-5 * scale { + out.push(fact( + "调和X238", + "X_{171}\\in OI", + "\\checkmark".into(), + None, + "X(171)\\in OI", + "X(171) 在 OI 线上", + )); + } + } + } + // X(172) + { + let wa = a2 * a2 + b * c * a2; + let wb = b2 * b2 + c * a * b2; + let wc = c2 * c2 + a * b * c2; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "对称中线X171积", + "X_{172}", + pt(xy[0], xy[1]), + None, + "X(172) = (a^4+bca^2:\\cdots) = X(6)\\cdot X(171)\\text{ (trilinear)}", + "X(172):对称中线点与 X(171) 的三线积", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X172".into(), + })); + } + } + // X(173) + { + let ha = g[0] / 2.0; + let hb = g[1] / 2.0; + let hc = g[2] / 2.0; + let fa = hb.cos() + hc.cos() - ha.cos(); + let fb = hc.cos() + ha.cos() - hb.cos(); + let fc = ha.cos() + hb.cos() - hc.cos(); + let wa = a * fa; + let wb = b * fb; + let wc = c * fc; + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push(fact( + "等腰等化器", + "X_{173}", + pt(xy[0], xy[1]), + None, + "X(173) = \\bigl(a(\\cos\\tfrac{B}{2}+\\cos\\tfrac{C}{2}-\\cos\\tfrac{A}{2}):\\cdots\\bigr)", + "X(173):全等等腰等化器点(Yff)", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X173".into(), + })); + } + } + } + } + } + + // X(166)/X(167)/X(170): fill excentral gaps; then X(177)–X(180) mid-arc / Ajima-Malfatti. + // X(166)=X(7)-of-excentral (Gergonne): trilinear + // f = tan(A/2)/d_a − tan(B/2)/d_b − tan(C/2)/d_c, + // d_a = cos(B/2)+cos(C/2)−cos(A/2); bary a·f. On line X(165)X(168). + // X(167)=X(8)-of-excentral (Nagel): f = s_b t_b + s_c t_c − s_a t_a with + // s_a=sin(A/2), t_a=d_a sec(A/2); on line X(164)X(165). + // X(170)=X(9)-aleph conjugate of X(9)=X(76)-of-excentral: trilinear + // f = −tan(A/2)sec²(A/2)+tan(B/2)sec²(B/2)+tan(C/2)sec²(C/2); on IG. + // X(177)=1st mid-arc: bary a(cos(B/2)+cos(C/2))sec(A/2); =X(1)-of-intouch. + // X(178)=2nd mid-arc: bary cos(B/2)+cos(C/2). + // X(179)=1st Ajima-Malfatti: bary a sec⁴(A/4). + // X(180)=2nd Ajima-Malfatti: f=1/t_b+1/t_c−1/t_a, + // t_a=1+2(sec(A/4)cos(B/4)cos(C/4))². + { + let ha = g[0] / 2.0; + let hb = g[1] / 2.0; + let hc = g[2] / 2.0; + let (s_ha, s_hb, s_hc) = (ha.sin(), hb.sin(), hc.sin()); + let (c_ha, c_hb, c_hc) = (ha.cos(), hb.cos(), hc.cos()); + let (t_ha, t_hb, t_hc) = (ha.tan(), hb.tan(), hc.tan()); + let d_a = c_hb + c_hc - c_ha; + let d_b = c_hc + c_ha - c_hb; + let d_c = c_ha + c_hb - c_hc; + // X(166) + if d_a.abs() > 1e-12 && d_b.abs() > 1e-12 && d_c.abs() > 1e-12 { + let fa = t_ha / d_a - t_hb / d_b - t_hc / d_c; + let fb = t_hb / d_b - t_hc / d_c - t_ha / d_a; + let fc = t_hc / d_c - t_ha / d_a - t_hb / d_b; + let wa = a * fa; + let wb = b * fb; + let wc = c * fc; + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push(fact( + "旁心格尔巩", + "X_{166}", + pt(xy[0], xy[1]), + None, + "X(166) = X(7)\\text{-of-excentral};\\ f=\\tan\\tfrac{A}{2}/d_A-\\tan\\tfrac{B}{2}/d_B-\\tan\\tfrac{C}{2}/d_C", + "X(166):旁心三角形的格尔巩点", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X166".into(), + })); + // Cross-check collinear with X165, X168 when both present. + let x165 = out.iter().rev().find(|f| f.label == "X_{165}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + let x168 = out.iter().rev().find(|f| f.label == "X_{168}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let (Some((x1, y1)), Some((x8, y8))) = (x165, x168) { + let cross = (x8 - x1) * (xy[1] - y1) - (y8 - y1) * (xy[0] - x1); + let scale = (x8 - x1).abs() + (y8 - y1).abs() + (xy[0] - x1).abs() + (xy[1] - y1).abs() + 1e-12; + if cross.abs() / scale < 1e-5 { + out.push(fact( + "旁心格尔巩", + "X_{166}\\in X_{165}X_{168}", + "\\text{共线}".into(), + None, + "X(166)\\in X(165)X(168)", + "X(166) 在旁心重心与旁心米滕的连线上", + )); + } + } + } + } + } + // X(167) + if c_ha.abs() > 1e-12 && c_hb.abs() > 1e-12 && c_hc.abs() > 1e-12 + && d_a.abs() > 1e-12 && d_b.abs() > 1e-12 && d_c.abs() > 1e-12 + { + let ta = d_a / c_ha; + let tb = d_b / c_hb; + let tc = d_c / c_hc; + let fa = s_hb * tb + s_hc * tc - s_ha * ta; + let fb = s_hc * tc + s_ha * ta - s_hb * tb; + let fc = s_ha * ta + s_hb * tb - s_hc * tc; + let wa = a * fa; + let wb = b * fb; + let wc = c * fc; + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push(fact( + "旁心奈格尔", + "X_{167}", + pt(xy[0], xy[1]), + None, + "X(167) = X(8)\\text{-of-excentral};\\ f=s_B t_B+s_C t_C-s_A t_A", + "X(167):旁心三角形的奈格尔点", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X167".into(), + })); + let x164 = out.iter().rev().find(|f| f.label == "X_{164}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + let x165 = out.iter().rev().find(|f| f.label == "X_{165}").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let (Some((x4, y4)), Some((x5, y5))) = (x164, x165) { + let cross = (x5 - x4) * (xy[1] - y4) - (y5 - y4) * (xy[0] - x4); + let scale = (x5 - x4).abs() + (y5 - y4).abs() + (xy[0] - x4).abs() + (xy[1] - y4).abs() + 1e-12; + if cross.abs() / scale < 1e-5 { + out.push(fact( + "旁心奈格尔", + "X_{167}\\in X_{164}X_{165}", + "\\text{共线}".into(), + None, + "X(167)\\in X(164)X(165)", + "X(167) 在旁心内心与旁心重心的连线上", + )); + } + } + } + } + } + // X(170) + if c_ha.abs() > 1e-12 && c_hb.abs() > 1e-12 && c_hc.abs() > 1e-12 { + let ua = t_ha / (c_ha * c_ha); + let ub = t_hb / (c_hb * c_hb); + let uc = t_hc / (c_hc * c_hc); + let fa = -ua + ub + uc; + let fb = -ub + uc + ua; + let fc = -uc + ua + ub; + let wa = a * fa; + let wb = b * fb; + let wc = c * fc; + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push(fact( + "米滕阿列夫自共轭", + "X_{170}", + pt(xy[0], xy[1]), + None, + "X(170) = X(9)\\text{-aleph of }X(9)=X(76)\\text{-of-excentral};\\ f=-\\tan\\tfrac{A}{2}\\sec^2\\tfrac{A}{2}+\\cdots", + "X(170):米滕点的 X(9)-阿列夫共轭(在 IG 上)", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X170".into(), + })); + let i_xy = out.iter().rev().find(|f| f.group == "特殊点" && f.label == "I").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + let ge_xy = out.iter().rev().find(|f| f.label == "X_{7}" || f.label == "Ge").and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some((*x, *y)), + _ => None, + }); + if let (Some((ix, iy)), Some((gx, gy))) = (i_xy, ge_xy) { + let cross = (gx - ix) * (xy[1] - iy) - (gy - iy) * (xy[0] - ix); + let scale = (gx - ix).abs() + (gy - iy).abs() + (xy[0] - ix).abs() + (xy[1] - iy).abs() + 1e-12; + if cross.abs() / scale < 1e-5 { + out.push(fact( + "米滕阿列夫自共轭", + "X_{170}\\in IG", + "\\text{共线}".into(), + None, + "X(170)\\in IG", + "X(170) 在内心与格尔巩点的连线上", + )); + } + } + } + } + } + // X(177) / X(178) + if c_ha.abs() > 1e-12 && c_hb.abs() > 1e-12 && c_hc.abs() > 1e-12 { + let fa = (c_hb + c_hc) / c_ha; + let fb = (c_hc + c_ha) / c_hb; + let fc = (c_ha + c_hb) / c_hc; + let wa = a * fa; + let wb = b * fb; + let wc = c * fc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "第一中弧点", + "X_{177}", + pt(xy[0], xy[1]), + None, + "X(177) = \\bigl(a(\\cos\\tfrac{B}{2}+\\cos\\tfrac{C}{2})\\sec\\tfrac{A}{2}:\\cdots\\bigr) = X(1)\\text{-of-intouch}", + "X(177):第一中弧点(接触三角形的内心)", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X177".into(), + })); + } + // X(178): bary cos(B/2)+cos(C/2) (no side factor) + let wa = c_hb + c_hc; + let wb = c_hc + c_ha; + let wc = c_ha + c_hb; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "第二中弧点", + "X_{178}", + pt(xy[0], xy[1]), + None, + "X(178) = \\bigl(\\cos\\tfrac{B}{2}+\\cos\\tfrac{C}{2}:\\cdots\\bigr)", + "X(178):第二中弧点", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X178".into(), + })); + } + } + // X(179) + { + let c_qa = (g[0] / 4.0).cos(); + let c_qb = (g[1] / 4.0).cos(); + let c_qc = (g[2] / 4.0).cos(); + if c_qa.abs() > 1e-12 && c_qb.abs() > 1e-12 && c_qc.abs() > 1e-12 { + let wa = a / (c_qa * c_qa * c_qa * c_qa); + let wb = b / (c_qb * c_qb * c_qb * c_qb); + let wc = c / (c_qc * c_qc * c_qc * c_qc); + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push(fact( + "第一阿吉马-马尔法蒂", + "X_{179}", + pt(xy[0], xy[1]), + None, + "X(179) = \\bigl(a\\sec^4\\tfrac{A}{4}:\\cdots\\bigr)", + "X(179):第一阿吉马-马尔法蒂点", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X179".into(), + })); + } + } + } + } + // X(180) + { + let qa = g[0] / 4.0; + let qb = g[1] / 4.0; + let qc = g[2] / 4.0; + let (c_qa, c_qb, c_qc) = (qa.cos(), qb.cos(), qc.cos()); + if c_qa.abs() > 1e-12 && c_qb.abs() > 1e-12 && c_qc.abs() > 1e-12 { + let t_a = 1.0 + 2.0 * (c_qb * c_qc / c_qa).powi(2); + let t_b = 1.0 + 2.0 * (c_qc * c_qa / c_qb).powi(2); + let t_c = 1.0 + 2.0 * (c_qa * c_qb / c_qc).powi(2); + if t_a.abs() > 1e-12 && t_b.abs() > 1e-12 && t_c.abs() > 1e-12 { + let fa = 1.0 / t_b + 1.0 / t_c - 1.0 / t_a; + let fb = 1.0 / t_c + 1.0 / t_a - 1.0 / t_b; + let fc = 1.0 / t_a + 1.0 / t_b - 1.0 / t_c; + let wa = a * fa; + let wb = b * fb; + let wc = c * fc; + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push(fact( + "第二阿吉马-马尔法蒂", + "X_{180}", + pt(xy[0], xy[1]), + None, + "X(180) = 2\\text{nd Ajima-Malfatti};\\ f=1/t_B+1/t_C-1/t_A", + "X(180):第二阿吉马-马尔法蒂点", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X180".into(), + })); + } + } + } + } + } + } + + // X(182)–X(190): Brocard-diameter / orthic / circum-inverses / mid-arc / Yff. + // X(182)=mid(O,K)=Brocard-circle center; bary a cos(A−ω). + // X(183)=trilinear quotient X(75)/X(182); bary csc A cos(A−ω). + // X(184)=procircumcenter; bary a³ cos A (=X(63)-of-orthic when acute). + // X(185)=Nagel-of-orthic; bary (sin 2A)[1−cos A cos(B−C)]; skip right. + // X(186)=circum-inverse of H; bary sin A(4 cos A−sec A); skip equilateral. + // X(187)=Schoute=circum-inverse of K; bary a²(2a²−b²−c²); skip equilateral. + // X(188)=2nd mid-arc of anticomplementary; bary 2 cos(A/2)=sin A csc(A/2). + // X(189)=cyclocevian conjugate of Nagel X(8); bary 1/(cos B+cos C−cos A−1). + // X(190)=Yff parabolic point; bary 1/(b−c); skip isos/equil. + { + let (ca, cb, cc) = (g[0].cos(), g[1].cos(), g[2].cos()); + let (sa, sb, sc) = (g[0].sin(), g[1].sin(), g[2].sin()); + let rightish = ca.abs() < 1e-12 || cb.abs() < 1e-12 || cc.abs() < 1e-12; + let isos = (a - b).abs() < 1e-9 || (b - c).abs() < 1e-9 || (c - a).abs() < 1e-9; + // K = Lemoine (recompute; always finite). + let k_den = a * a + b * b + c * c; + let kpt = if k_den > 1e-18 { + [ + (a * a * pa[0] + b * b * pb[0] + c * c * pc[0]) / k_den, + (a * a * pa[1] + b * b * pb[1] + c * c * pc[1]) / k_den, + ] + } else { + centroid + }; + // X(182): midpoint of Brocard diameter OK. + { + let xy = [(circ[0] + kpt[0]) / 2.0, (circ[1] + kpt[1]) / 2.0]; + // Cross-check bary a cos(A−ω) when Brocard angle available. + let xyu = if area > 1e-12 && k_den > 1e-18 { + let tan_w = (4.0 * area) / k_den; + let sin_w = tan_w / (1.0 + tan_w * tan_w).sqrt(); + let cos_w = 1.0 / (1.0 + tan_w * tan_w).sqrt(); + let fa = ca * cos_w + sa * sin_w; + let fb = cb * cos_w + sb * sin_w; + let fc = cc * cos_w + sc * sin_w; + let wa = a * fa; + let wb = b * fb; + let wc = c * fc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xb = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if dist2(xb, xy) < 1e-5 * (1.0 + rr) { xb } else { xy } + } else { + xy + } + } else { + xy + }; + out.push(fact( + "布罗卡直径中点", + "X_{182}", + pt(xyu[0], xyu[1]), + None, + "X(182) = \\text{mid}(O,K);\\ \\text{bary }a\\cos(A-\\omega)", + "X(182):布罗卡直径中点(布罗卡圆圆心)", + ).with(Shape::Point { + x: xyu[0], + y: xyu[1], + label: "X182".into(), + })); + // On OK + let cross = (kpt[0] - circ[0]) * (xyu[1] - circ[1]) - (kpt[1] - circ[1]) * (xyu[0] - circ[0]); + let scale = (kpt[0] - circ[0]).abs() + (kpt[1] - circ[1]).abs() + + (xyu[0] - circ[0]).abs() + (xyu[1] - circ[1]).abs() + 1e-12; + if cross.abs() / scale < 1e-5 { + out.push(fact( + "布罗卡直径中点", + "X_{182}\\in OK", + "\\text{共线}".into(), + None, + "X(182)\\in OK", + "X(182) 在外心与勒莫恩点的连线上", + )); + } + } + // X(183): bary csc A cos(A−ω) + if area > 1e-12 && k_den > 1e-18 && sa.abs() > 1e-12 && sb.abs() > 1e-12 && sc.abs() > 1e-12 { + let tan_w = (4.0 * area) / k_den; + let sin_w = tan_w / (1.0 + tan_w * tan_w).sqrt(); + let cos_w = 1.0 / (1.0 + tan_w * tan_w).sqrt(); + let fa = (ca * cos_w + sa * sin_w) / sa; + let fb = (cb * cos_w + sb * sin_w) / sb; + let fc = (cc * cos_w + sc * sin_w) / sc; + let den = fa + fb + fc; + if den.abs() > 1e-12 && fa.is_finite() && fb.is_finite() && fc.is_finite() { + let xy = [ + (fa * pa[0] + fb * pb[0] + fc * pc[0]) / den, + (fa * pa[1] + fb * pb[1] + fc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push(fact( + "摩西布罗卡商", + "X_{183}", + pt(xy[0], xy[1]), + None, + "X(183) = X(75)/X(182)\\text{ trilinear quotient};\\ \\text{bary }\\csc A\\cos(A-\\omega)", + "X(183):摩西点与布罗卡直径中点的三线商", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X183".into(), + })); + } + } + } + // X(184): bary a³ cos A + { + let wa = a * a * a * ca; + let wb = b * b * b * cb; + let wc = c * c * c * cc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "准外心", + "X_{184}", + pt(xy[0], xy[1]), + None, + "X(184) = \\bigl(a^3\\cos A:\\cdots\\bigr)", + "X(184):准外心(垂足三角形的 X(63))", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X184".into(), + })); + } + } + // X(185): Nagel of orthic; skip right + if !rightish { + let wa = (2.0 * g[0]).sin() * (1.0 - ca * (g[1] - g[2]).cos()); + let wb = (2.0 * g[1]).sin() * (1.0 - cb * (g[2] - g[0]).cos()); + let wc = (2.0 * g[2]).sin() * (1.0 - cc * (g[0] - g[1]).cos()); + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push(fact( + "垂足奈格尔", + "X_{185}", + pt(xy[0], xy[1]), + None, + "X(185) = X(8)\\text{-of-orthic};\\ \\text{bary }(\\sin 2A)[1-\\cos A\\cos(B-C)]", + "X(185):垂足三角形的奈格尔点", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X185".into(), + })); + } + } + } + // X(186): circum-inverse of H; skip equilateral (H=O) + { + let oh2 = { + let dx = orth[0] - circ[0]; + let dy = orth[1] - circ[1]; + dx * dx + dy * dy + }; + if oh2 > 1e-12 { + let scale = (rr * rr) / oh2; + let mut xy = [ + circ[0] + scale * (orth[0] - circ[0]), + circ[1] + scale * (orth[1] - circ[1]), + ]; + // Prefer ETC bary when sec finite (non-right) + if !rightish { + let wa = sa * (4.0 * ca - 1.0 / ca); + let wb = sb * (4.0 * cb - 1.0 / cb); + let wc = sc * (4.0 * cc - 1.0 / cc); + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xb = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if dist2(xb, xy) < 1e-4 * (1.0 + rr) { + xy = xb; + } + } + } + out.push(fact( + "反演垂心", + "X_{186}", + pt(xy[0], xy[1]), + None, + "X(186) = O + R^2(H-O)/|OH|^2", + "X(186):垂心关于外接圆的反演点", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X186".into(), + })); + let cross = (orth[0] - circ[0]) * (xy[1] - circ[1]) - (orth[1] - circ[1]) * (xy[0] - circ[0]); + if cross.abs() < 1e-5 * (1.0 + oh2) * (1.0 + dist2(circ, xy)) { + out.push(fact( + "反演垂心", + "X_{186}\\in OH", + "\\text{共线}".into(), + None, + "X(186)\\in OH", + "X(186) 在欧拉线 OH 上", + )); + } + } + } + // X(187): Schoute = circum-inverse of K; skip equilateral (K=O → weights 0) + { + let wa = a * a * (2.0 * a * a - b * b - c * c); + let wb = b * b * (2.0 * b * b - c * c - a * a); + let wc = c * c * (2.0 * c * c - a * a - b * b); + let den = wa + wb + wc; + let ok2 = { + let dx = kpt[0] - circ[0]; + let dy = kpt[1] - circ[1]; + dx * dx + dy * dy + }; + if den.abs() > 1e-12 && ok2 > 1e-12 { + let xb = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + let scale = (rr * rr) / ok2; + let xg = [ + circ[0] + scale * (kpt[0] - circ[0]), + circ[1] + scale * (kpt[1] - circ[1]), + ]; + let xy = if dist2(xb, xg) < 1e-4 * (1.0 + rr) { xb } else { xg }; + out.push(fact( + "肖特中心", + "X_{187}", + pt(xy[0], xy[1]), + None, + "X(187) = O + R^2(K-O)/|OK|^2;\\ \\text{bary }a^2(2a^2-b^2-c^2)", + "X(187):肖特中心(勒莫恩点关于外接圆的反演)", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X187".into(), + })); + let cross = (kpt[0] - circ[0]) * (xy[1] - circ[1]) - (kpt[1] - circ[1]) * (xy[0] - circ[0]); + let scale_c = (kpt[0] - circ[0]).abs() + (kpt[1] - circ[1]).abs() + + (xy[0] - circ[0]).abs() + (xy[1] - circ[1]).abs() + 1e-12; + if cross.abs() / scale_c < 1e-5 { + out.push(fact( + "肖特中心", + "X_{187}\\in OK", + "\\text{共线}".into(), + None, + "X(187)\\in OK", + "X(187) 在外心与勒莫恩点的连线上", + )); + } + } + } + // X(188): bary 2 cos(A/2) + { + let wa = 2.0 * (g[0] / 2.0).cos(); + let wb = 2.0 * (g[1] / 2.0).cos(); + let wc = 2.0 * (g[2] / 2.0).cos(); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "反补第二中弧", + "X_{188}", + pt(xy[0], xy[1]), + None, + "X(188) = \\bigl(2\\cos\\tfrac{A}{2}:\\cdots\\bigr) = \\text{anticomplement of }X(178)", + "X(188):反补三角形的第二中弧点", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X188".into(), + })); + } + } + // X(189): cyclocevian conjugate of Nagel + { + let da = cb + cc - ca - 1.0; + let db = cc + ca - cb - 1.0; + let dc = ca + cb - cc - 1.0; + if da.abs() > 1e-12 && db.abs() > 1e-12 && dc.abs() > 1e-12 { + let wa = 1.0 / da; + let wb = 1.0 / db; + let wc = 1.0 / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push(fact( + "奈格尔圆塞瓦共轭", + "X_{189}", + pt(xy[0], xy[1]), + None, + "X(189) = \\text{cyclocevian of }X(8);\\ \\text{bary }1/(\\cos B+\\cos C-\\cos A-1)", + "X(189):奈格尔点的圆塞瓦共轭", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X189".into(), + })); + } + } + } + } + // X(190): Yff parabolic; skip isos/equil + if !isos { + let da = b - c; + let db = c - a; + let dc = a - b; + if da.abs() > 1e-12 && db.abs() > 1e-12 && dc.abs() > 1e-12 { + let wa = 1.0 / da; + let wb = 1.0 / db; + let wc = 1.0 / dc; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + out.push(fact( + "伊夫抛物点", + "X_{190}", + pt(xy[0], xy[1]), + None, + "X(190) = \\bigl(1/(b-c):\\cdots\\bigr)", + "X(190):伊夫抛物点(切 BC/CA/AB 且焦点为 X(101) 的抛物线的切点塞瓦交点)", + ).with(Shape::Point { + x: xy[0], + y: xy[1], + label: "X190".into(), + })); + } + } + } + } + + // X(191)–X(200): classical Ceva-conjugate band (ETC). + // X(191)=X(10)-Ceva of I; bary a·f, f=(b+c−a)(bc+ca+ab)+b³+c³−a³. + // X(192)=X(1)-Ceva of G = anticomplement of Moses; bary ab+ac−bc. + // X(193)=X(4)-Ceva of G = anticomplement of X(69); bary 3a²−b²−c². + // X(194)=X(6)-Ceva of G = anticomplement of X(76); bary a²(b²+c²)−b²c². + // X(195)=X(5)-Ceva of O; bary 4 cos 2A + cot² A − cot A cot ω. + // X(196)=X(7)-Ceva of H; bary (cos B+cos C−cos A−1) tan A tan(A/2); skip right. + // X(197)=X(8)-Ceva of K; bary a²(S−2a² tan(A/2)), S=∑a² tan(A/2). + // X(198)=X(9)-Ceva of K (=isogonal of X(189)); bary a²(cos B+cos C−cos A−1). + // X(199)=X(10)-Ceva of K; bary a²[b⁴+c⁴−a⁴+(b²+c²−a²)(bc+ca+ab)]. + // X(200)={X(8),X(78)}-harmonic of I; bary a(b+c−a)². + { + let (ca, cb, cc) = (g[0].cos(), g[1].cos(), g[2].cos()); + let (sa, sb, sc) = (g[0].sin(), g[1].sin(), g[2].sin()); + let rightish = ca.abs() < 1e-12 || cb.abs() < 1e-12 || cc.abs() < 1e-12; + let push_bary = |out: &mut Vec, + group: &str, + label: &str, + tip: &str, + zh: &str, + tag: &str, + wa: f64, + wb: f64, + wc: f64| { + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push( + fact(group, label, pt(xy[0], xy[1]), None, tip, zh).with(Shape::Point { + x: xy[0], + y: xy[1], + label: tag.into(), + }), + ); + } + } + }; + // X(191) + { + let f = |aa: f64, bb: f64, cc: f64| { + (bb + cc - aa) * (bb * cc + cc * aa + aa * bb) + bb * bb * bb + cc * cc * cc + - aa * aa * aa + }; + let wa = a * f(a, b, c); + let wb = b * f(b, c, a); + let wc = c * f(c, a, b); + push_bary( + &mut out, + "斯皮克内心塞瓦", + "X_{191}", + "X(191) = X(10)\\text{-Ceva of }X(1);\\ f=(b+c-a)(\\Sigma bc)+b^3+c^3-a^3", + "X(191):斯皮克点对内心的塞瓦共轭", + "X191", + wa, + wb, + wc, + ); + } + // X(192): anticomplement of Moses + { + let wa = a * b + a * c - b * c; + let wb = b * c + b * a - c * a; + let wc = c * a + c * b - a * b; + push_bary( + &mut out, + "内心重心塞瓦", + "X_{192}", + "X(192) = X(1)\\text{-Ceva of }X(2) = \\text{anticomplement of }X(75)", + "X(192):内心对重心的塞瓦共轭(摩西点的反补)", + "X192", + wa, + wb, + wc, + ); + if let Some(mo) = out.iter().rev().find(|f| f.group == "摩西点" && f.label == "Mo") { + if let Some(Shape::Point { x, y, .. }) = mo.shape.as_ref() { + let aq = [3.0 * centroid[0] - 2.0 * *x, 3.0 * centroid[1] - 2.0 * *y]; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if dist2(xy, aq) < 1e-6 * (1.0 + rr) { + out.push(fact( + "内心重心塞瓦", + "X_{192}=\\mathrm{anti}(X_{75})", + "\\text{反补}".into(), + None, + "X(192) = 3G - 2\\,X(75)", + "X(192) 是摩西点关于重心的反补", + )); + } + } + } + } + } + // X(193): anticomplement of X(69) + { + let wa = 3.0 * a * a - b * b - c * c; + let wb = 3.0 * b * b - c * c - a * a; + let wc = 3.0 * c * c - a * a - b * b; + push_bary( + &mut out, + "垂心重心塞瓦", + "X_{193}", + "X(193) = X(4)\\text{-Ceva of }X(2);\\ \\text{bary }3a^2-b^2-c^2", + "X(193):垂心对重心的塞瓦共轭(余切点 X(69) 的反补)", + "X193", + wa, + wb, + wc, + ); + if let Some(x69) = out.iter().rev().find(|f| f.group == "余切点" && f.label == "X_{69}") { + if let Some(Shape::Point { x, y, .. }) = x69.shape.as_ref() { + let aq = [3.0 * centroid[0] - 2.0 * *x, 3.0 * centroid[1] - 2.0 * *y]; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if dist2(xy, aq) < 1e-6 * (1.0 + rr) { + out.push(fact( + "垂心重心塞瓦", + "X_{193}=\\mathrm{anti}(X_{69})", + "\\text{反补}".into(), + None, + "X(193) = 3G - 2\\,X(69)", + "X(193) 是余切点关于重心的反补", + )); + } + } + } + } + } + // X(194): anticomplement of X(76) + { + let wa = a * a * b * b + a * a * c * c - b * b * c * c; + let wb = b * b * c * c + b * b * a * a - c * c * a * a; + let wc = c * c * a * a + c * c * b * b - a * a * b * b; + push_bary( + &mut out, + "勒莫恩重心塞瓦", + "X_{194}", + "X(194) = X(6)\\text{-Ceva of }X(2);\\ \\text{bary }a^2(b^2+c^2)-b^2c^2", + "X(194):勒莫恩点对重心的塞瓦共轭(第三布罗卡点的反补)", + "X194", + wa, + wb, + wc, + ); + if let Some(x76) = out + .iter() + .rev() + .find(|f| f.group == "第三布罗卡点" && f.label == "X_{76}") + { + if let Some(Shape::Point { x, y, .. }) = x76.shape.as_ref() { + let aq = [3.0 * centroid[0] - 2.0 * *x, 3.0 * centroid[1] - 2.0 * *y]; + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if dist2(xy, aq) < 1e-6 * (1.0 + rr) { + out.push(fact( + "勒莫恩重心塞瓦", + "X_{194}=\\mathrm{anti}(X_{76})", + "\\text{反补}".into(), + None, + "X(194) = 3G - 2\\,X(76)", + "X(194) 是第三布罗卡点关于重心的反补", + )); + } + } + } + } + } + // X(195): Iliev form with Brocard cot ω = (a²+b²+c²)/(4Δ) + if sa.abs() > 1e-12 && sb.abs() > 1e-12 && sc.abs() > 1e-12 && area > 1e-12 { + let cot_w = (a * a + b * b + c * c) / (4.0 * area); + let cot_a = ca / sa; + let cot_b = cb / sb; + let cot_c = cc / sc; + let wa = 4.0 * (2.0 * g[0]).cos() + cot_a * cot_a - cot_a * cot_w; + let wb = 4.0 * (2.0 * g[1]).cos() + cot_b * cot_b - cot_b * cot_w; + let wc = 4.0 * (2.0 * g[2]).cos() + cot_c * cot_c - cot_c * cot_w; + push_bary( + &mut out, + "九点外心塞瓦", + "X_{195}", + "X(195) = X(5)\\text{-Ceva of }X(3);\\ 4\\cos 2A+\\cot^2 A-\\cot A\\cot\\omega", + "X(195):九点中心对外心的塞瓦共轭", + "X195", + wa, + wb, + wc, + ); + } + // X(196): skip right (tan A → ∞) + if !rightish { + let g_a = (cb + cc - ca - 1.0) * g[0].tan() * (g[0] / 2.0).tan(); + let g_b = (cc + ca - cb - 1.0) * g[1].tan() * (g[1] / 2.0).tan(); + let g_c = (ca + cb - cc - 1.0) * g[2].tan() * (g[2] / 2.0).tan(); + push_bary( + &mut out, + "格尔巩垂心塞瓦", + "X_{196}", + "X(196) = X(7)\\text{-Ceva of }X(4);\\ (\\cos B+\\cos C-\\cos A-1)\\tan A\\tan\\tfrac{A}{2}", + "X(196):格尔巩点对垂心的塞瓦共轭", + "X196", + g_a, + g_b, + g_c, + ); + } + // X(197) + { + let t_a = (g[0] / 2.0).tan(); + let t_b = (g[1] / 2.0).tan(); + let t_c = (g[2] / 2.0).tan(); + let sum = a * a * t_a + b * b * t_b + c * c * t_c; + let wa = a * a * (sum - 2.0 * a * a * t_a); + let wb = b * b * (sum - 2.0 * b * b * t_b); + let wc = c * c * (sum - 2.0 * c * c * t_c); + push_bary( + &mut out, + "奈格尔勒莫恩塞瓦", + "X_{197}", + "X(197) = X(8)\\text{-Ceva of }X(6);\\ a^2(S-2a^2\\tan\\tfrac{A}{2})", + "X(197):奈格尔点对勒莫恩点的塞瓦共轭", + "X197", + wa, + wb, + wc, + ); + } + // X(198): isogonal of X(189) + { + let da = cb + cc - ca - 1.0; + let db = cc + ca - cb - 1.0; + let dc = ca + cb - cc - 1.0; + let wa = a * a * da; + let wb = b * b * db; + let wc = c * c * dc; + push_bary( + &mut out, + "米滕阿列夫勒莫恩塞瓦", + "X_{198}", + "X(198) = X(9)\\text{-Ceva of }X(6) = \\text{isogonal of }X(189)", + "X(198):米滕阿列夫点对勒莫恩点的塞瓦共轭(X(189) 的等角共轭)", + "X198", + wa, + wb, + wc, + ); + } + // X(199) + { + let f = |aa: f64, bb: f64, cc: f64| { + let b2 = bb * bb; + let c2 = cc * cc; + let a2 = aa * aa; + aa * (b2 * b2 + c2 * c2 - a2 * a2 + (b2 + c2 - a2) * (bb * cc + cc * aa + aa * bb)) + }; + let wa = a * f(a, b, c); + let wb = b * f(b, c, a); + let wc = c * f(c, a, b); + push_bary( + &mut out, + "斯皮克莱莫恩塞瓦", + "X_{199}", + "X(199) = X(10)\\text{-Ceva of }X(6);\\ a[b^4+c^4-a^4+(b^2+c^2-a^2)\\Sigma bc]", + "X(199):斯皮克点对勒莫恩点的塞瓦共轭", + "X199", + wa, + wb, + wc, + ); + } + // X(200) + { + let wa = a * (b + c - a) * (b + c - a); + let wb = b * (c + a - b) * (c + a - b); + let wc = c * (a + b - c) * (a + b - c); + push_bary( + &mut out, + "半周长平方点", + "X_{200}", + "X(200) = \\bigl(a(b+c-a)^2:\\cdots\\bigr) = \\{X(8),X(78)\\}\\text{-harmonic of }X(1)", + "X(200):半周长平方点(奈格尔与 X(78) 关于内心的调和共轭)", + "X200", + wa, + wb, + wc, + ); + } + } + + // X(201)–X(210): next classical Ceva-conjugate band (ETC). + // X(201)=X(10)-Ceva of X(12); bary (sin 2A)[1+cos(B−C)]. + // X(202)=X(1)-Ceva of X(15); bary sin A·u(v+w−u), u=sin(A+π/3). + // X(203)=X(1)-Ceva of X(16); same with u=sin(A−π/3); skip equil. + // X(204)=X(1)-Ceva of X(19); bary sin A·tan A(tan B+tan C−tan A); skip right. + // X(205)=X(9)-Ceva of X(31); bary a³[b²tan(B/2)+c²tan(C/2)−a²tan(A/2)]. + // X(206)=X(2)-Ceva of X(32)=complement of X(66)=mid(K,X159); bary a⁴(b⁴+c⁴−a⁴); ∞ on right. + // X(207)=X(1)-Ceva of X(34); bary sin A(1−sec A)(sec B+sec C−sec A−1); skip right. + // X(208)=X(4)-Ceva of X(34); bary sin A(1−sec A)(cos B+cos C−cos A−1); skip right. + // X(209)=X(4)-Ceva of X(37); bary sin A(sin B+sin C)[sin A+sin(A−B)+sin(A−C)]. + // X(210)=X(10)-Ceva of X(37); bary a(b+c)(b+c−a). + { + let (ca, cb, cc) = (g[0].cos(), g[1].cos(), g[2].cos()); + let (sa, sb, sc) = (g[0].sin(), g[1].sin(), g[2].sin()); + let rightish = ca.abs() < 1e-12 || cb.abs() < 1e-12 || cc.abs() < 1e-12; + let equil = (a - b).abs() < 1e-12 && (b - c).abs() < 1e-12; + let push_bary = |out: &mut Vec, + group: &str, + label: &str, + tip: &str, + zh: &str, + tag: &str, + wa: f64, + wb: f64, + wc: f64| { + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push( + fact(group, label, pt(xy[0], xy[1]), None, tip, zh).with(Shape::Point { + x: xy[0], + y: xy[1], + label: tag.into(), + }), + ); + } + } + }; + // X(201) + { + let wa = (2.0 * g[0]).sin() * (1.0 + (g[1] - g[2]).cos()); + let wb = (2.0 * g[1]).sin() * (1.0 + (g[2] - g[0]).cos()); + let wc = (2.0 * g[2]).sin() * (1.0 + (g[0] - g[1]).cos()); + push_bary( + &mut out, + "斯皮克费尔巴哈调和塞瓦", + "X_{201}", + "X(201) = X(10)\\text{-Ceva of }X(12);\\ (\\sin 2A)[1+\\cos(B-C)]", + "X(201):斯皮克点对费尔巴哈调和共轭点的塞瓦共轭", + "X201", + wa, + wb, + wc, + ); + } + // X(202): I-Ceva of 1st isodynamic + { + let ua = (g[0] + PI / 3.0).sin(); + let ub = (g[1] + PI / 3.0).sin(); + let uc = (g[2] + PI / 3.0).sin(); + let wa = sa * ua * (ub + uc - ua); + let wb = sb * ub * (uc + ua - ub); + let wc = sc * uc * (ua + ub - uc); + push_bary( + &mut out, + "内心第一等动力塞瓦", + "X_{202}", + "X(202) = X(1)\\text{-Ceva of }X(15);\\ u=\\sin(A+\\pi/3)", + "X(202):内心对第一等动力点的塞瓦共轭", + "X202", + wa, + wb, + wc, + ); + } + // X(203): skip equilateral (sin(A−π/3)=0) + if !equil { + let ua = (g[0] - PI / 3.0).sin(); + let ub = (g[1] - PI / 3.0).sin(); + let uc = (g[2] - PI / 3.0).sin(); + let wa = sa * ua * (ub + uc - ua); + let wb = sb * ub * (uc + ua - ub); + let wc = sc * uc * (ua + ub - uc); + push_bary( + &mut out, + "内心第二等动力塞瓦", + "X_{203}", + "X(203) = X(1)\\text{-Ceva of }X(16);\\ u=\\sin(A-\\pi/3)", + "X(203):内心对第二等动力点的塞瓦共轭", + "X203", + wa, + wb, + wc, + ); + } + // X(204): skip right (tan A → ∞) + if !rightish { + let ta = g[0].tan(); + let tb = g[1].tan(); + let tc = g[2].tan(); + let wa = sa * ta * (tb + tc - ta); + let wb = sb * tb * (tc + ta - tb); + let wc = sc * tc * (ta + tb - tc); + push_bary( + &mut out, + "内心克劳森塞瓦", + "X_{204}", + "X(204) = X(1)\\text{-Ceva of }X(19);\\ \\tan A(\\tan B+\\tan C-\\tan A)", + "X(204):内心对克劳森点的塞瓦共轭", + "X204", + wa, + wb, + wc, + ); + } + // X(205) + { + let t_a = (g[0] / 2.0).tan(); + let t_b = (g[1] / 2.0).tan(); + let t_c = (g[2] / 2.0).tan(); + let f = |aa: f64, bb: f64, cc: f64, ta: f64, tb: f64, tc: f64| { + aa * aa * (bb * bb * tb + cc * cc * tc - aa * aa * ta) + }; + let wa = a * f(a, b, c, t_a, t_b, t_c); + let wb = b * f(b, c, a, t_b, t_c, t_a); + let wc = c * f(c, a, b, t_c, t_a, t_b); + push_bary( + &mut out, + "米滕第二幂点塞瓦", + "X_{205}", + "X(205) = X(9)\\text{-Ceva of }X(31);\\ a^3[b^2\\tan\\tfrac{B}{2}+c^2\\tan\\tfrac{C}{2}-a^2\\tan\\tfrac{A}{2}]", + "X(205):米滕阿列夫点对第二幂点的塞瓦共轭", + "X205", + wa, + wb, + wc, + ); + } + // X(206): complement of X(66) = mid(K, X159) + { + let a2 = a * a; + let b2 = b * b; + let c2 = c * c; + let a4 = a2 * a2; + let b4 = b2 * b2; + let c4 = c2 * c2; + let wa = a4 * (b4 + c4 - a4); + let wb = b4 * (c4 + a4 - b4); + let wc = c4 * (a4 + b4 - c4); + push_bary( + &mut out, + "重心第三幂点塞瓦", + "X_{206}", + "X(206) = X(2)\\text{-Ceva of }X(32) = \\mathrm{comp}(X_{66});\\ a^4(b^4+c^4-a^4)", + "X(206):重心对第三幂点的塞瓦共轭(X(66) 的补点;K 与 X(159) 中点)", + "X206", + wa, + wb, + wc, + ); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if let Some(x66) = out + .iter() + .rev() + .find(|f| f.label == "X_{66}") + .and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some([*x, *y]), + _ => None, + }) + { + let comp = [ + (3.0 * centroid[0] - x66[0]) / 2.0, + (3.0 * centroid[1] - x66[1]) / 2.0, + ]; + if dist2(xy, comp) < 1e-6 * (1.0 + rr) { + out.push(fact( + "重心第三幂点塞瓦", + "X_{206}=\\mathrm{comp}(X_{66})", + "\\text{补点}".into(), + None, + "X(206) = (3G - X(66))/2", + "X(206) 是 X(66) 关于重心的补点", + )); + } + } + if let (Some(kxy), Some(x159)) = ( + out.iter() + .rev() + .find(|f| f.group == "勒莫恩点" && f.label == "K") + .and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some([*x, *y]), + _ => None, + }), + out.iter() + .rev() + .find(|f| f.label == "X_{159}") + .and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some([*x, *y]), + _ => None, + }), + ) { + let mid = [(kxy[0] + x159[0]) / 2.0, (kxy[1] + x159[1]) / 2.0]; + if dist2(xy, mid) < 1e-6 * (1.0 + rr) { + out.push(fact( + "重心第三幂点塞瓦", + "X_{206}=\\mathrm{mid}(K,X_{159})", + "\\text{中点}".into(), + None, + "X(206) = \\mathrm{mid}(X(6),X(159))", + "X(206) 是勒莫恩点与 X(159) 的中点", + )); + } + } + } + } + // X(207): skip right + if !rightish && ca.abs() > 1e-12 && cb.abs() > 1e-12 && cc.abs() > 1e-12 { + let seca = 1.0 / ca; + let secb = 1.0 / cb; + let secc = 1.0 / cc; + let wa = sa * (1.0 - seca) * (secb + secc - seca - 1.0); + let wb = sb * (1.0 - secb) * (secc + seca - secb - 1.0); + let wc = sc * (1.0 - secc) * (seca + secb - secc - 1.0); + push_bary( + &mut out, + "内心X34塞瓦", + "X_{207}", + "X(207) = X(1)\\text{-Ceva of }X(34);\\ (1-\\sec A)(\\sec B+\\sec C-\\sec A-1)", + "X(207):内心对 X(34) 的塞瓦共轭", + "X207", + wa, + wb, + wc, + ); + } + // X(208): skip right + if !rightish && ca.abs() > 1e-12 && cb.abs() > 1e-12 && cc.abs() > 1e-12 { + let seca = 1.0 / ca; + let secb = 1.0 / cb; + let secc = 1.0 / cc; + let wa = sa * (1.0 - seca) * (cb + cc - ca - 1.0); + let wb = sb * (1.0 - secb) * (cc + ca - cb - 1.0); + let wc = sc * (1.0 - secc) * (ca + cb - cc - 1.0); + push_bary( + &mut out, + "垂心X34塞瓦", + "X_{208}", + "X(208) = X(4)\\text{-Ceva of }X(34);\\ (1-\\sec A)(\\cos B+\\cos C-\\cos A-1)", + "X(208):垂心对 X(34) 的塞瓦共轭", + "X208", + wa, + wb, + wc, + ); + } + // X(209) + { + let fa = (sb + sc) * (sa + (g[0] - g[1]).sin() + (g[0] - g[2]).sin()); + let fb = (sc + sa) * (sb + (g[1] - g[2]).sin() + (g[1] - g[0]).sin()); + let fc = (sa + sb) * (sc + (g[2] - g[0]).sin() + (g[2] - g[1]).sin()); + let wa = sa * fa; + let wb = sb * fb; + let wc = sc * fc; + push_bary( + &mut out, + "垂心格林伯格塞瓦", + "X_{209}", + "X(209) = X(4)\\text{-Ceva of }X(37);\\ (\\sin B+\\sin C)[\\sin A+\\sin(A-B)+\\sin(A-C)]", + "X(209):垂心对格林伯格点的塞瓦共轭", + "X209", + wa, + wb, + wc, + ); + } + // X(210) + { + let wa = a * (b + c) * (b + c - a); + let wb = b * (c + a) * (c + a - b); + let wc = c * (a + b) * (a + b - c); + push_bary( + &mut out, + "斯皮克格林伯格塞瓦", + "X_{210}", + "X(210) = X(10)\\text{-Ceva of }X(37);\\ a(b+c)(b+c-a)", + "X(210):斯皮克点对格林伯格点的塞瓦共轭(X(8) 与 X(9) 的截点)", + "X210", + wa, + wb, + wc, + ); + } + } + + // X(211)–X(220): next classical Ceva-conjugate band (ETC). + // X(211)=X(4)-Ceva of X(39); bary a⁴(b²+c²)(a²b²−b⁴+a²c²+b²c²−c⁴). + // X(212)=X(9)-Ceva of X(41); bary (sin 2A)(1+cos A). + // X(213)=X(6)-Ceva of X(42); bary (b+c)a³. + // X(214)=X(2)-Ceva of X(44)=mid(I,X100)=comp(X80); bary a(b+c−2a)(b²+c²−a²−bc); skip equil. + // X(215)=X(1)-Ceva of X(50); bary (sin A)(sin 3A)(sin 3B+sin 3C−sin 3A); skip equil. + // X(216)=X(5)-Ceva of X(51); bary (sin A)(sin 2A)cos(B−C). + // X(217)=X(6)-Ceva of X(51); bary a⁴(a²−b²−c²)(a²b²−b⁴+a²c²+2b²c²−c⁴). + // X(218)=X(7)-Ceva of X(55); bary a²(a²+b²+c²−2ab−2ac). + // X(219)=X(8)-Ceva of X(55); bary sin 2A·cot(A/2)=2 cos A(1+cos A). + // X(220)=X(9)-Ceva of X(55)=bary-square of X(9); bary a²(b+c−a)². + { + let (ca, cb, cc) = (g[0].cos(), g[1].cos(), g[2].cos()); + let (sa, sb, sc) = (g[0].sin(), g[1].sin(), g[2].sin()); + let equil = (a - b).abs() < 1e-12 && (b - c).abs() < 1e-12; + let push_bary = |out: &mut Vec, + group: &str, + label: &str, + tip: &str, + zh: &str, + tag: &str, + wa: f64, + wb: f64, + wc: f64| { + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push( + fact(group, label, pt(xy[0], xy[1]), None, tip, zh).with(Shape::Point { + x: xy[0], + y: xy[1], + label: tag.into(), + }), + ); + } + } + }; + // X(211) + { + let f = |aa: f64, bb: f64, cc: f64| { + let a2 = aa * aa; + let b2 = bb * bb; + let c2 = cc * cc; + let a4 = a2 * a2; + a4 * (b2 + c2) * (a2 * b2 - b2 * b2 + a2 * c2 + b2 * c2 - c2 * c2) + }; + let wa = f(a, b, c); + let wb = f(b, c, a); + let wc = f(c, a, b); + push_bary( + &mut out, + "垂心布罗卡中点塞瓦", + "X_{211}", + "X(211) = X(4)\\text{-Ceva of }X(39);\\ a^4(b^2+c^2)(a^2b^2-b^4+a^2c^2+b^2c^2-c^4)", + "X(211):垂心对布罗卡中点的塞瓦共轭", + "X211", + wa, + wb, + wc, + ); + } + // X(212) + { + let wa = (2.0 * g[0]).sin() * (1.0 + ca); + let wb = (2.0 * g[1]).sin() * (1.0 + cb); + let wc = (2.0 * g[2]).sin() * (1.0 + cc); + push_bary( + &mut out, + "米滕第二幂截点塞瓦", + "X_{212}", + "X(212) = X(9)\\text{-Ceva of }X(41);\\ (\\sin 2A)(1+\\cos A)", + "X(212):米滕阿列夫点对第二幂截点的塞瓦共轭", + "X212", + wa, + wb, + wc, + ); + } + // X(213) + { + let wa = (b + c) * a * a * a; + let wb = (c + a) * b * b * b; + let wc = (a + b) * c * c * c; + push_bary( + &mut out, + "勒莫恩内心勒莫恩截点塞瓦", + "X_{213}", + "X(213) = X(6)\\text{-Ceva of }X(42);\\ (b+c)a^3", + "X(213):勒莫恩点对内心–勒莫恩截点的塞瓦共轭(K 与 X(31) 的截点)", + "X213", + wa, + wb, + wc, + ); + } + // X(214): also mid(I, X100) = complement of X(80) + { + let wa = a * (b + c - 2.0 * a) * (b * b + c * c - a * a - b * c); + let wb = b * (c + a - 2.0 * b) * (c * c + a * a - b * b - c * a); + let wc = c * (a + b - 2.0 * c) * (a * a + b * b - c * c - a * b); + push_bary( + &mut out, + "重心X44塞瓦", + "X_{214}", + "X(214) = X(2)\\text{-Ceva of }X(44) = \\mathrm{mid}(I,X_{100});\\ a(b+c-2a)(b^2+c^2-a^2-bc)", + "X(214):重心对 X(44) 的塞瓦共轭(内心与 X(100) 中点;X(80) 的补点)", + "X214", + wa, + wb, + wc, + ); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if let Some(x100) = out + .iter() + .rev() + .find(|f| f.label == "X_{100}") + .and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some([*x, *y]), + _ => None, + }) + { + let mid = [(inc[0] + x100[0]) / 2.0, (inc[1] + x100[1]) / 2.0]; + if dist2(xy, mid) < 1e-6 * (1.0 + rr) { + out.push(fact( + "重心X44塞瓦", + "X_{214}=\\mathrm{mid}(I,X_{100})", + "\\text{中点}".into(), + None, + "X(214) = \\mathrm{mid}(X(1),X(100))", + "X(214) 是内心与 X(100) 的中点", + )); + } + } + if let Some(x80) = out + .iter() + .rev() + .find(|f| f.label == "X_{80}") + .and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some([*x, *y]), + _ => None, + }) + { + let comp = [ + (3.0 * centroid[0] - x80[0]) / 2.0, + (3.0 * centroid[1] - x80[1]) / 2.0, + ]; + if dist2(xy, comp) < 1e-6 * (1.0 + rr) { + out.push(fact( + "重心X44塞瓦", + "X_{214}=\\mathrm{comp}(X_{80})", + "\\text{补点}".into(), + None, + "X(214) = (3G - X(80))/2", + "X(214) 是 X(80) 关于重心的补点", + )); + } + } + } + } + // X(215): skip equilateral (sin 3A = 0) + if !equil { + let s3a = (3.0 * g[0]).sin(); + let s3b = (3.0 * g[1]).sin(); + let s3c = (3.0 * g[2]).sin(); + let wa = sa * s3a * (s3b + s3c - s3a); + let wb = sb * s3b * (s3c + s3a - s3b); + let wc = sc * s3c * (s3a + s3b - s3c); + push_bary( + &mut out, + "内心三倍角正弦塞瓦", + "X_{215}", + "X(215) = X(1)\\text{-Ceva of }X(50);\\ (\\sin A)(\\sin 3A)(\\sin 3B+\\sin 3C-\\sin 3A)", + "X(215):内心对三倍角正弦点的塞瓦共轭", + "X215", + wa, + wb, + wc, + ); + } + // X(216) + { + let wa = sa * (2.0 * g[0]).sin() * (g[1] - g[2]).cos(); + let wb = sb * (2.0 * g[1]).sin() * (g[2] - g[0]).cos(); + let wc = sc * (2.0 * g[2]).sin() * (g[0] - g[1]).cos(); + push_bary( + &mut out, + "九点垂足重心塞瓦", + "X_{216}", + "X(216) = X(5)\\text{-Ceva of }X(51);\\ (\\sin A)(\\sin 2A)\\cos(B-C)", + "X(216):九点中心对垂足三角形重心的塞瓦共轭(O 与 K 的截点)", + "X216", + wa, + wb, + wc, + ); + } + // X(217) + { + let f = |aa: f64, bb: f64, cc: f64| { + let a2 = aa * aa; + let b2 = bb * bb; + let c2 = cc * cc; + let a4 = a2 * a2; + a4 * (a2 - b2 - c2) * (a2 * b2 - b2 * b2 + a2 * c2 + 2.0 * b2 * c2 - c2 * c2) + }; + let wa = f(a, b, c); + let wb = f(b, c, a); + let wc = f(c, a, b); + push_bary( + &mut out, + "勒莫恩垂足重心塞瓦", + "X_{217}", + "X(217) = X(6)\\text{-Ceva of }X(51);\\ a^4(a^2-b^2-c^2)(\\cdots)", + "X(217):勒莫恩点对垂足三角形重心的塞瓦共轭(K 与 X(184) 的截点)", + "X217", + wa, + wb, + wc, + ); + } + // X(218) + { + let f = |aa: f64, bb: f64, cc: f64| { + let a2 = aa * aa; + a2 * (a2 + bb * bb + cc * cc - 2.0 * aa * bb - 2.0 * aa * cc) + }; + let wa = f(a, b, c); + let wb = f(b, c, a); + let wc = f(c, a, b); + push_bary( + &mut out, + "格尔巩外接内切相似塞瓦", + "X_{218}", + "X(218) = X(7)\\text{-Ceva of }X(55);\\ a^2(a^2+b^2+c^2-2ab-2ac)", + "X(218):格尔巩点对 X(55) 的塞瓦共轭", + "X218", + wa, + wb, + wc, + ); + } + // X(219) + { + let wa = 2.0 * ca * (1.0 + ca); + let wb = 2.0 * cb * (1.0 + cb); + let wc = 2.0 * cc * (1.0 + cc); + push_bary( + &mut out, + "奈格尔外接内切相似塞瓦", + "X_{219}", + "X(219) = X(8)\\text{-Ceva of }X(55);\\ \\sin 2A\\cot\\tfrac{A}{2}=2\\cos A(1+\\cos A)", + "X(219):奈格尔点对 X(55) 的塞瓦共轭", + "X219", + wa, + wb, + wc, + ); + } + // X(220): barycentric square of Mittenpunkt X(9) + { + let wa = a * a * (b + c - a) * (b + c - a); + let wb = b * b * (c + a - b) * (c + a - b); + let wc = c * c * (a + b - c) * (a + b - c); + push_bary( + &mut out, + "米滕外接内切相似塞瓦", + "X_{220}", + "X(220) = X(9)\\text{-Ceva of }X(55) = X(9)^2;\\ a^2(b+c-a)^2", + "X(220):米滕阿列夫点对 X(55) 的塞瓦共轭(X(9) 的重心平方)", + "X220", + wa, + wb, + wc, + ); + } + } + + // X(221)–X(230): next classical Ceva-conjugate band (ETC). + // X(221)=X(1)-Ceva of X(56); bary (sin A) sin²(A/2)(cos B+cos C−cos A−1); skip right (∞). + // X(222)=X(7)-Ceva of X(56); bary a² cos A/(1+cos A) [= a²/(1+sec A)]. + // X(223)=X(2)-Ceva of X(57)=comp(X189); bary (sin A) tan(A/2)(cos B+cos C−cos A−1). + // X(224)=X(7)-Ceva of X(63); bary (sin A)[cot B cos²(B/2)+cot C cos²(C/2)−cot A cos²(A/2)] cot A. + // X(225)=X(4)-Ceva of X(65); bary tan A(cos B+cos C); skip right. + // X(226)=X(7)-Ceva of X(65)=comp(X63); bary (b+c)/(b+c−a). + // X(227)=X(10)-Ceva of X(65); bary a(b+c)(cos B+cos C−cos A−1) tan(A/2). + // X(228)=X(3)-Ceva of X(71); bary (sin A sin 2A)(sin B+sin C). + // X(229)=X(7)-Ceva of X(81)=mid(I,X267); u=a(b+c−a)/(b+c), bary a(v+w−u)/(b+c). + // X(230)=X(2)-Ceva of X(114)=mid(X115,X187)=comp(X325); bary a·bc[a²(2a²−b²−c²)+(b²−c²)²]; skip equil. + { + let (ca, cb, cc) = (g[0].cos(), g[1].cos(), g[2].cos()); + let (sa, sb, sc) = (g[0].sin(), g[1].sin(), g[2].sin()); + let equil = (a - b).abs() < 1e-12 && (b - c).abs() < 1e-12; + let right = ca.abs() < 1e-9 || cb.abs() < 1e-9 || cc.abs() < 1e-9; + let push_bary = |out: &mut Vec, + group: &str, + label: &str, + tip: &str, + zh: &str, + tag: &str, + wa: f64, + wb: f64, + wc: f64| { + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push( + fact(group, label, pt(xy[0], xy[1]), None, tip, zh).with(Shape::Point { + x: xy[0], + y: xy[1], + label: tag.into(), + }), + ); + } + } + }; + // X(221): skip right (Σbary → 0 / ∞) + if !right { + let f = |ang: f64, cb_: f64, cc_: f64, ca_: f64| { + let s_half = (ang * 0.5).sin(); + s_half * s_half * (cb_ + cc_ - ca_ - 1.0) + }; + let wa = sa * f(g[0], cb, cc, ca); + let wb = sb * f(g[1], cc, ca, cb); + let wc = sc * f(g[2], ca, cb, cc); + push_bary( + &mut out, + "内心外接内切外相似塞瓦", + "X_{221}", + "X(221) = X(1)\\text{-Ceva of }X(56);\\ (\\sin A)\\sin^2\\tfrac{A}{2}(\\cos B+\\cos C-\\cos A-1)", + "X(221):内心对 X(56) 的塞瓦共轭", + "X221", + wa, + wb, + wc, + ); + } + // X(222) + { + let wa = a * a * ca / (1.0 + ca); + let wb = b * b * cb / (1.0 + cb); + let wc = c * c * cc / (1.0 + cc); + push_bary( + &mut out, + "格尔巩外接内切外相似塞瓦", + "X_{222}", + "X(222) = X(7)\\text{-Ceva of }X(56);\\ a^2\\cos A/(1+\\cos A)", + "X(222):格尔巩点对 X(56) 的塞瓦共轭", + "X222", + wa, + wb, + wc, + ); + } + // X(223): also complement of X(189) + { + let f = |ang: f64, cb_: f64, cc_: f64, ca_: f64| { + (ang * 0.5).tan() * (cb_ + cc_ - ca_ - 1.0) + }; + let wa = sa * f(g[0], cb, cc, ca); + let wb = sb * f(g[1], cc, ca, cb); + let wc = sc * f(g[2], ca, cb, cc); + push_bary( + &mut out, + "重心外接内切内相似塞瓦", + "X_{223}", + "X(223) = X(2)\\text{-Ceva of }X(57) = \\mathrm{comp}(X_{189});\\ (\\sin A)\\tan\\tfrac{A}{2}(\\cos B+\\cos C-\\cos A-1)", + "X(223):重心对 X(57) 的塞瓦共轭(X(189) 的补点)", + "X223", + wa, + wb, + wc, + ); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if let Some(x189) = out + .iter() + .rev() + .find(|f| f.label == "X_{189}") + .and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some([*x, *y]), + _ => None, + }) + { + let comp = [ + (3.0 * centroid[0] - x189[0]) / 2.0, + (3.0 * centroid[1] - x189[1]) / 2.0, + ]; + if dist2(xy, comp) < 1e-6 * (1.0 + rr) { + out.push(fact( + "重心外接内切内相似塞瓦", + "X_{223}=\\mathrm{comp}(X_{189})", + "\\text{补点}".into(), + None, + "X(223) = (3G - X(189))/2", + "X(223) 是 X(189) 关于重心的补点", + )); + } + } + } + } + // X(224) + { + let cot = |t: f64| t.cos() / t.sin(); + let f = |aa: f64, bb: f64, cc: f64| { + (cot(bb) * (bb * 0.5).cos().powi(2) + + cot(cc) * (cc * 0.5).cos().powi(2) + - cot(aa) * (aa * 0.5).cos().powi(2)) + * cot(aa) + }; + let wa = sa * f(g[0], g[1], g[2]); + let wb = sb * f(g[1], g[2], g[0]); + let wc = sc * f(g[2], g[0], g[1]); + push_bary( + &mut out, + "格尔巩克劳森等角塞瓦", + "X_{224}", + "X(224) = X(7)\\text{-Ceva of }X(63);\\ (\\sin A)[\\cot B\\cos^2\\tfrac{B}{2}+\\cdots]\\cot A", + "X(224):格尔巩点对克劳森等角点的塞瓦共轭", + "X224", + wa, + wb, + wc, + ); + } + // X(225): skip right (tan A → ∞) + if !right { + let wa = g[0].tan() * (cb + cc); + let wb = g[1].tan() * (cc + ca); + let wc = g[2].tan() * (ca + cb); + push_bary( + &mut out, + "垂心内心奈格尔截点塞瓦", + "X_{225}", + "X(225) = X(4)\\text{-Ceva of }X(65);\\ \\tan A(\\cos B+\\cos C)", + "X(225):垂心对内心–奈格尔截点的塞瓦共轭", + "X225", + wa, + wb, + wc, + ); + } + // X(226): also complement of X(63) + { + let wa = (b + c) / (b + c - a); + let wb = (c + a) / (c + a - b); + let wc = (a + b) / (a + b - c); + push_bary( + &mut out, + "格尔巩内心奈格尔截点塞瓦", + "X_{226}", + "X(226) = X(7)\\text{-Ceva of }X(65) = \\mathrm{comp}(X_{63});\\ (b+c)/(b+c-a)", + "X(226):格尔巩点对内心–奈格尔截点的塞瓦共轭(X(63) 的补点)", + "X226", + wa, + wb, + wc, + ); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if let Some(x63) = out + .iter() + .rev() + .find(|f| f.label == "X_{63}") + .and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some([*x, *y]), + _ => None, + }) + { + let comp = [ + (3.0 * centroid[0] - x63[0]) / 2.0, + (3.0 * centroid[1] - x63[1]) / 2.0, + ]; + if dist2(xy, comp) < 1e-6 * (1.0 + rr) { + out.push(fact( + "格尔巩内心奈格尔截点塞瓦", + "X_{226}=\\mathrm{comp}(X_{63})", + "\\text{补点}".into(), + None, + "X(226) = (3G - X(63))/2", + "X(226) 是 X(63) 关于重心的补点", + )); + } + } + } + } + // X(227) + { + let wa = a * (b + c) * (cb + cc - ca - 1.0) * (g[0] * 0.5).tan(); + let wb = b * (c + a) * (cc + ca - cb - 1.0) * (g[1] * 0.5).tan(); + let wc = c * (a + b) * (ca + cb - cc - 1.0) * (g[2] * 0.5).tan(); + push_bary( + &mut out, + "斯皮克内心奈格尔截点塞瓦", + "X_{227}", + "X(227) = X(10)\\text{-Ceva of }X(65);\\ a(b+c)(\\cos B+\\cos C-\\cos A-1)\\tan\\tfrac{A}{2}", + "X(227):斯皮克点对内心–奈格尔截点的塞瓦共轭", + "X227", + wa, + wb, + wc, + ); + } + // X(228) + { + let wa = (sa * (2.0 * g[0]).sin()) * (sb + sc); + let wb = (sb * (2.0 * g[1]).sin()) * (sc + sa); + let wc = (sc * (2.0 * g[2]).sin()) * (sa + sb); + push_bary( + &mut out, + "外心X71塞瓦", + "X_{228}", + "X(228) = X(3)\\text{-Ceva of }X(71);\\ (\\sin A\\sin 2A)(\\sin B+\\sin C)", + "X(228):外心对 X(71) 的塞瓦共轭", + "X228", + wa, + wb, + wc, + ); + } + // X(229) + { + let ua = a * (b + c - a) / (b + c); + let ub = b * (c + a - b) / (c + a); + let uc = c * (a + b - c) / (a + b); + let wa = a * (ub + uc - ua) / (b + c); + let wb = b * (uc + ua - ub) / (c + a); + let wc = c * (ua + ub - uc) / (a + b); + push_bary( + &mut out, + "格尔巩X81塞瓦", + "X_{229}", + "X(229) = X(7)\\text{-Ceva of }X(81) = \\mathrm{mid}(I,X_{267});\\ a(v+w-u)/(b+c)", + "X(229):格尔巩点对 X(81) 的塞瓦共轭(内心与 X(267) 中点)", + "X229", + wa, + wb, + wc, + ); + } + // X(230): skip equilateral (weights vanish); also mid(X115,X187) + if !equil { + let f = |aa: f64, bb: f64, cc: f64| { + let a2 = aa * aa; + let b2 = bb * bb; + let c2 = cc * cc; + bb * cc * (a2 * (2.0 * a2 - b2 - c2) + (b2 - c2) * (b2 - c2)) + }; + let wa = a * f(a, b, c); + let wb = b * f(b, c, a); + let wc = c * f(c, a, b); + push_bary( + &mut out, + "重心基伯特对跖塞瓦", + "X_{230}", + "X(230) = X(2)\\text{-Ceva of }X(114) = \\mathrm{mid}(X_{115},X_{187});\\ a\\cdot bc[a^2(2a^2-b^2-c^2)+(b^2-c^2)^2]", + "X(230):重心对基伯特对跖点的塞瓦共轭(X(115) 与 X(187) 中点)", + "X230", + wa, + wb, + wc, + ); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + let x115 = out.iter().rev().find(|f| f.label == "X_{115}").and_then(|f| { + match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some([*x, *y]), + _ => None, + } + }); + let x187 = out.iter().rev().find(|f| f.label == "X_{187}").and_then(|f| { + match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some([*x, *y]), + _ => None, + } + }); + if let (Some(p115), Some(p187)) = (x115, x187) { + let mid = [(p115[0] + p187[0]) / 2.0, (p115[1] + p187[1]) / 2.0]; + if dist2(xy, mid) < 1e-6 * (1.0 + rr) { + out.push(fact( + "重心基伯特对跖塞瓦", + "X_{230}=\\mathrm{mid}(X_{115},X_{187})", + "\\text{中点}".into(), + None, + "X(230) = \\mathrm{mid}(X(115),X(187))", + "X(230) 是 X(115) 与 X(187) 的中点", + )); + } + } + } + } + } + + // X(231)–X(240): next classical Ceva / line-conjugate band (ETC). + // X(231)=X(2)-Ceva of X(128); bary poly in a⁸…; skip equil (weights→0). + // X(232)=X(2)-Ceva of X(132); bary a²(a²+b²−c²)(a²−b²+c²)(a²b²−b⁴+a²c²−c⁴); skip equil. + // X(233)=X(2)-Ceva of X(140)=comp(X95); bary (a²(b²+c²)−(b²−c²)²)(2a⁴−3a²(b²+c²)+(b²−c²)²). + // X(234)=X(7)-Ceva of X(177); bary (sin A)(cos B/2+cos C/2)(cos B/2 cos C/2)². + // X(235)=X(4)-Ceva of X(185)=mid(H,X24); bary a(sec A−cos(B−C)); skip right (sec/den). + // X(236)=X(2)-Ceva of X(188); bary a(1+sin(A/2))/sin A (Iliev). + // X(237)=X(3)-line conjugate of G; bary a⁴(a²b²−b⁴+a²c²−c⁴); skip equil; on Euler. + // X(238)=X(1)-line conjugate of X(37); bary a(a²−bc); skip equil. + // X(239)=X(1)-line conjugate of X(42); bary a²−bc; skip equil. + // X(240)=X(1)-line conjugate of X(48); bary tan A cos(A+ω); skip right/equil. + { + let (ca, cb, cc) = (g[0].cos(), g[1].cos(), g[2].cos()); + let (sa, sb, sc) = (g[0].sin(), g[1].sin(), g[2].sin()); + let equil = (a - b).abs() < 1e-12 && (b - c).abs() < 1e-12; + let right = ca.abs() < 1e-9 || cb.abs() < 1e-9 || cc.abs() < 1e-9; + let push_bary = |out: &mut Vec, + group: &str, + label: &str, + tip: &str, + zh: &str, + tag: &str, + wa: f64, + wb: f64, + wc: f64| { + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push( + fact(group, label, pt(xy[0], xy[1]), None, tip, zh).with(Shape::Point { + x: xy[0], + y: xy[1], + label: tag.into(), + }), + ); + } + } + }; + // X(231): skip equilateral + if !equil { + let f = |aa: f64, bb: f64, cc: f64| { + let (a2, b2, c2) = (aa * aa, bb * bb, cc * cc); + let (a4, b4, c4) = (a2 * a2, b2 * b2, c2 * c2); + let (a6, b6, c6) = (a4 * a2, b4 * b2, c4 * c2); + let (a8, b8, c8) = (a4 * a4, b4 * b4, c4 * c4); + 2.0 * a8 - 4.0 * a6 * b2 + 3.0 * a4 * b4 - 2.0 * a2 * b6 + b8 + - 4.0 * a6 * c2 + 2.0 * a2 * b4 * c2 - 4.0 * b6 * c2 + + 3.0 * a4 * c4 + 2.0 * a2 * b2 * c4 + 6.0 * b4 * c4 + - 2.0 * a2 * c6 - 4.0 * b2 * c6 + c8 + }; + push_bary( + &mut out, + "重心X128塞瓦", + "X_{231}", + "X(231) = X(2)\\text{-Ceva of }X(128);\\ \\text{bary poly }a^8\\cdots", + "X(231):重心对 X(128) 的塞瓦共轭", + "X231", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(232): skip equilateral + if !equil { + let f = |aa: f64, bb: f64, cc: f64| { + let a2 = aa * aa; + let b2 = bb * bb; + let c2 = cc * cc; + a2 * (a2 + b2 - c2) + * (a2 - b2 + c2) + * (a2 * b2 - b2 * b2 + a2 * c2 - c2 * c2) + }; + push_bary( + &mut out, + "重心X132塞瓦", + "X_{232}", + "X(232) = X(2)\\text{-Ceva of }X(132);\\ a^2(a^2+b^2-c^2)(a^2-b^2+c^2)(\\cdots)", + "X(232):重心对 X(132) 的塞瓦共轭", + "X232", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(233): also complement of X(95) + { + let f = |aa: f64, bb: f64, cc: f64| { + let a2 = aa * aa; + let b2 = bb * bb; + let c2 = cc * cc; + let d = (b2 - c2) * (b2 - c2); + (a2 * (b2 + c2) - d) * (2.0 * a2 * a2 - 3.0 * a2 * (b2 + c2) + d) + }; + let wa = f(a, b, c); + let wb = f(b, c, a); + let wc = f(c, a, b); + push_bary( + &mut out, + "重心X140塞瓦", + "X_{233}", + "X(233) = X(2)\\text{-Ceva of }X(140) = \\mathrm{comp}(X_{95})", + "X(233):重心对 X(140) 的塞瓦共轭(X(95) 的补点)", + "X233", + wa, + wb, + wc, + ); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if let Some(x95) = out + .iter() + .rev() + .find(|f| f.label == "X_{95}") + .and_then(|f| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some([*x, *y]), + _ => None, + }) + { + let comp = [ + (3.0 * centroid[0] - x95[0]) / 2.0, + (3.0 * centroid[1] - x95[1]) / 2.0, + ]; + if dist2(xy, comp) < 1e-6 * (1.0 + rr) { + out.push(fact( + "重心X140塞瓦", + "X_{233}=\\mathrm{comp}(X_{95})", + "\\text{补点}".into(), + None, + "X(233) = (3G - X(95))/2", + "X(233) 是 X(95) 关于重心的补点", + )); + } + } + } + } + // X(234) + { + let f = |_aa: f64, bb: f64, cc: f64| { + let cbh = (bb * 0.5).cos(); + let cch = (cc * 0.5).cos(); + (cbh + cch) * (cbh * cch).powi(2) + }; + push_bary( + &mut out, + "格尔巩X177塞瓦", + "X_{234}", + "X(234) = X(7)\\text{-Ceva of }X(177);\\ (\\sin A)(\\cos\\tfrac{B}{2}+\\cos\\tfrac{C}{2})(\\cos\\tfrac{B}{2}\\cos\\tfrac{C}{2})^2", + "X(234):格尔巩点对 X(177) 的塞瓦共轭", + "X234", + sa * f(g[0], g[1], g[2]), + sb * f(g[1], g[2], g[0]), + sc * f(g[2], g[0], g[1]), + ); + } + // X(235): skip right; also mid(H,X24) when both present + if !right { + let f = |aa: f64, bb: f64, cc: f64| { + let a2 = aa * aa; + let b2 = bb * bb; + let c2 = cc * cc; + let d = (b2 - c2) * (b2 - c2); + let num = a2 * a2 * (b2 + c2) - 2.0 * a2 * d + d * (b2 + c2); + let den = a2 - b2 - c2; + num / den + }; + let wa = f(a, b, c); + let wb = f(b, c, a); + let wc = f(c, a, b); + push_bary( + &mut out, + "垂心X185塞瓦", + "X_{235}", + "X(235) = X(4)\\text{-Ceva of }X(185) = \\mathrm{mid}(H,X_{24});\\ a(\\sec A-\\cos(B-C))", + "X(235):垂心对 X(185) 的塞瓦共轭(H 与 X(24) 中点)", + "X235", + wa, + wb, + wc, + ); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + let x24 = out.iter().rev().find(|f| f.label == "X_{24}").and_then(|f| { + match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => Some([*x, *y]), + _ => None, + } + }); + if let (Some(p24), true) = (x24, orth[0].is_finite() && orth[1].is_finite()) { + let mid = [(orth[0] + p24[0]) / 2.0, (orth[1] + p24[1]) / 2.0]; + if dist2(xy, mid) < 1e-6 * (1.0 + rr) { + out.push(fact( + "垂心X185塞瓦", + "X_{235}=\\mathrm{mid}(H,X_{24})", + "\\text{中点}".into(), + None, + "X(235) = \\mathrm{mid}(H,X(24))", + "X(235) 是垂心与 X(24) 的中点", + )); + } + } + } + } + // X(236) + { + let wa = a * (1.0 + (g[0] * 0.5).sin()) / sa; + let wb = b * (1.0 + (g[1] * 0.5).sin()) / sb; + let wc = c * (1.0 + (g[2] * 0.5).sin()) / sc; + push_bary( + &mut out, + "重心X188塞瓦", + "X_{236}", + "X(236) = X(2)\\text{-Ceva of }X(188);\\ a(1+\\sin\\tfrac{A}{2})/\\sin A", + "X(236):重心对 X(188) 的塞瓦共轭", + "X236", + wa, + wb, + wc, + ); + } + // X(237): skip equilateral; on Euler + if !equil { + let f = |aa: f64, bb: f64, cc: f64| { + let a2 = aa * aa; + let b2 = bb * bb; + let c2 = cc * cc; + a2 * a2 * (a2 * b2 - b2 * b2 + a2 * c2 - c2 * c2) + }; + let wa = f(a, b, c); + let wb = f(b, c, a); + let wc = f(c, a, b); + push_bary( + &mut out, + "外心重心线共轭", + "X_{237}", + "X(237) = X(3)\\text{-line conjugate of }X(2);\\ a^4(a^2b^2-b^4+a^2c^2-c^4)", + "X(237):外心对重心的线共轭(欧拉线上)", + "X237", + wa, + wb, + wc, + ); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + let cross = (centroid[0] - circ[0]) * (xy[1] - circ[1]) + - (centroid[1] - circ[1]) * (xy[0] - circ[0]); + let scale = (centroid[0] - circ[0]).abs() + + (centroid[1] - circ[1]).abs() + + (xy[0] - circ[0]).abs() + + (xy[1] - circ[1]).abs() + + 1e-12; + if cross.abs() / scale < 1e-5 { + out.push(fact( + "外心重心线共轭", + "X_{237}\\in\\text{Euler}", + "\\text{共线}".into(), + None, + "X(237)\\in OH", + "X(237) 在欧拉线上", + )); + } + } + } + // X(238): skip equilateral + if !equil { + push_bary( + &mut out, + "内心X37线共轭", + "X_{238}", + "X(238) = X(1)\\text{-line conjugate of }X(37);\\ a(a^2-bc)", + "X(238):内心对 X(37) 的线共轭", + "X238", + a * (a * a - b * c), + b * (b * b - c * a), + c * (c * c - a * b), + ); + } + // X(239): skip equilateral + if !equil { + push_bary( + &mut out, + "内心X42线共轭", + "X_{239}", + "X(239) = X(1)\\text{-line conjugate of }X(42);\\ a^2-bc", + "X(239):内心对 X(42) 的线共轭", + "X239", + a * a - b * c, + b * b - c * a, + c * c - a * b, + ); + } + // X(240): skip right (tan) and equilateral (cos(A+ω)→0) + if !right && !equil && area > 1e-12 { + let sum_sq = a * a + b * b + c * c; + let tan_w = (4.0 * area) / sum_sq; + let cos_w = 1.0 / (1.0 + tan_w * tan_w).sqrt(); + let sin_w = tan_w * cos_w; + let wa = g[0].tan() * (ca * cos_w - sa * sin_w); + let wb = g[1].tan() * (cb * cos_w - sb * sin_w); + let wc = g[2].tan() * (cc * cos_w - sc * sin_w); + push_bary( + &mut out, + "内心X48线共轭", + "X_{240}", + "X(240) = X(1)\\text{-line conjugate of }X(48);\\ \\tan A\\cos(A+\\omega)", + "X(240):内心对 X(48) 的线共轭", + "X240", + wa, + wb, + wc, + ); + } + } + + // X(241)–X(250): finish line-conjugate band + first isogonals (ETC). + // X(241)=X(1)-line conjugate of X(55); f=cos⁴(B/2)−cos²(A/2)(cos²(B/2)+cos²(C/2))+cos⁴(C/2); skip equil. + // X(242)=X(4)-line conjugate of X(71); f=sec A(sin²A−sin B sin C); skip right/equil. + // X(243)=X(4)-line conjugate of X(73); f=sec A(cos²A−cos B cos C); skip right/equil. + // X(244)=X(1)-line conjugate of X(100); f=[1−cos(B−C)]sin²(A/2); skip equil. + // X(245)=X(1)-line conjugate of X(110); csc-diff form; skip isos (csc→∞). + // X(246)=X(3)-line conjugate of X(110); csc/cos form; skip isos. + // X(247)=X(4)-line conjugate of X(110); csc/sec form; skip isos/right. + // X(248)=X(4)-line conjugate of X(132); bary sin A sin 2A sec(A+ω); skip equil (A+ω→π/2). + // X(249)=isogonal of X(115); bary cos²(B−C); →G on equil. + // X(250)=isogonal of X(125); bary tan A csc²(B−C); skip right/isos. + { + let (ca, cb, cc) = (g[0].cos(), g[1].cos(), g[2].cos()); + let (sa, sb, sc) = (g[0].sin(), g[1].sin(), g[2].sin()); + let equil = (a - b).abs() < 1e-12 && (b - c).abs() < 1e-12; + let isos = (a - b).abs() < 1e-12 || (b - c).abs() < 1e-12 || (c - a).abs() < 1e-12; + let right = ca.abs() < 1e-9 || cb.abs() < 1e-9 || cc.abs() < 1e-9; + let push_bary = |out: &mut Vec, + group: &str, + label: &str, + tip: &str, + zh: &str, + tag: &str, + wa: f64, + wb: f64, + wc: f64| { + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push( + fact(group, label, pt(xy[0], xy[1]), None, tip, zh).with(Shape::Point { + x: xy[0], + y: xy[1], + label: tag.into(), + }), + ); + } + } + }; + // X(241): skip equilateral (weights→0); on IO + if !equil { + let f = |aa: f64, bb: f64, cc: f64| { + let cah2 = (aa * 0.5).cos().powi(2); + let cbh2 = (bb * 0.5).cos().powi(2); + let cch2 = (cc * 0.5).cos().powi(2); + cbh2 * cbh2 - cah2 * (cbh2 + cch2) + cch2 * cch2 + }; + let wa = sa * f(g[0], g[1], g[2]); + let wb = sb * f(g[1], g[2], g[0]); + let wc = sc * f(g[2], g[0], g[1]); + push_bary( + &mut out, + "内心X55线共轭", + "X_{241}", + "X(241) = X(1)\\text{-line conjugate of }X(55);\\ (\\sin A)f,\\ f=\\cos^4\\tfrac{B}{2}-\\cdots", + "X(241):内心对 X(55) 的线共轭", + "X241", + wa, + wb, + wc, + ); + let den = wa + wb + wc; + if den.abs() > 1e-12 { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if circ[0].is_finite() + && circ[1].is_finite() + && inc[0].is_finite() + && inc[1].is_finite() + { + let cross = (xy[0] - inc[0]) * (circ[1] - inc[1]) + - (xy[1] - inc[1]) * (circ[0] - inc[0]); + if cross.abs() < 1e-6 * (1.0 + rr) { + out.push(fact( + "内心X55线共轭", + "X_{241}\\in IO", + "\\text{共线}".into(), + None, + "X(241)\\in IO", + "X(241) 在内心–外心连线上", + )); + } + } + } + } + // X(242): skip right (sec) and equilateral + if !right && !equil { + let f = |aa: f64, bb: f64, cc: f64| { + let caa = aa.cos(); + if caa.abs() < 1e-12 { + return f64::NAN; + } + (aa.sin().powi(2) - bb.sin() * cc.sin()) / caa + }; + push_bary( + &mut out, + "垂心X71线共轭", + "X_{242}", + "X(242) = X(4)\\text{-line conjugate of }X(71);\\ (\\sin A)\\sec A(\\sin^2 A-\\sin B\\sin C)", + "X(242):垂心对 X(71) 的线共轭", + "X242", + sa * f(g[0], g[1], g[2]), + sb * f(g[1], g[2], g[0]), + sc * f(g[2], g[0], g[1]), + ); + } + // X(243): skip right/equilateral + if !right && !equil { + let f = |aa: f64, bb: f64, cc: f64| { + let caa = aa.cos(); + if caa.abs() < 1e-12 { + return f64::NAN; + } + (caa * caa - bb.cos() * cc.cos()) / caa + }; + push_bary( + &mut out, + "垂心X73线共轭", + "X_{243}", + "X(243) = X(4)\\text{-line conjugate of }X(73);\\ (\\sin A)\\sec A(\\cos^2 A-\\cos B\\cos C)", + "X(243):垂心对 X(73) 的线共轭", + "X243", + sa * f(g[0], g[1], g[2]), + sb * f(g[1], g[2], g[0]), + sc * f(g[2], g[0], g[1]), + ); + } + // X(244): skip equilateral + if !equil { + let f = |aa: f64, bb: f64, cc: f64| { + (1.0 - (bb - cc).cos()) * (aa * 0.5).sin().powi(2) + }; + push_bary( + &mut out, + "内心X100线共轭", + "X_{244}", + "X(244) = X(1)\\text{-line conjugate of }X(100);\\ (\\sin A)[1-\\cos(B-C)]\\sin^2\\tfrac{A}{2}", + "X(244):内心对 X(100) 的线共轭", + "X244", + sa * f(g[0], g[1], g[2]), + sb * f(g[1], g[2], g[0]), + sc * f(g[2], g[0], g[1]), + ); + } + // X(245): skip isosceles (angle-diff csc) + if !isos { + let f = |aa: f64, bb: f64, cc: f64| { + let d_ca = (cc - aa).sin(); + let d_cb = (cc - bb).sin(); + let d_ab = (aa - bb).sin(); + let d_ba = (bb - aa).sin(); + if d_ca.abs() < 1e-12 || d_cb.abs() < 1e-12 || d_ab.abs() < 1e-12 || d_ba.abs() < 1e-12 + { + return f64::NAN; + } + let csc_ca = 1.0 / d_ca; + let csc_cb = 1.0 / d_cb; + let csc_ab = 1.0 / d_ab; + let csc_ba = 1.0 / d_ba; + csc_ca * csc_ca + csc_cb * (csc_ca - csc_ba) + csc_ab * csc_ab + }; + push_bary( + &mut out, + "内心X110线共轭", + "X_{245}", + "X(245) = X(1)\\text{-line conjugate of }X(110);\\ \\csc\\text{-diff form}", + "X(245):内心对 X(110) 的线共轭", + "X245", + sa * f(g[0], g[1], g[2]), + sb * f(g[1], g[2], g[0]), + sc * f(g[2], g[0], g[1]), + ); + } + // X(246): skip isosceles + if !isos { + let f = |aa: f64, bb: f64, cc: f64| { + let d_ba = (bb - aa).sin(); + let d_bc = (bb - cc).sin(); + let d_ca = (cc - aa).sin(); + let d_cb = (cc - bb).sin(); + if d_ba.abs() < 1e-12 || d_bc.abs() < 1e-12 || d_ca.abs() < 1e-12 || d_cb.abs() < 1e-12 + { + return f64::NAN; + } + let csc_ba = 1.0 / d_ba; + let csc_bc = 1.0 / d_bc; + let csc_ca = 1.0 / d_ca; + let csc_cb = 1.0 / d_cb; + let u = aa.cos() * csc_ca + bb.cos() * csc_cb; + csc_ba * (aa.cos() * csc_ba + cc.cos() * csc_bc) - csc_ca * u + }; + push_bary( + &mut out, + "外心X110线共轭", + "X_{246}", + "X(246) = X(3)\\text{-line conjugate of }X(110);\\ \\csc/\\cos\\text{ form}", + "X(246):外心对 X(110) 的线共轭", + "X246", + sa * f(g[0], g[1], g[2]), + sb * f(g[1], g[2], g[0]), + sc * f(g[2], g[0], g[1]), + ); + } + // X(247): skip isosceles and right (sec) + if !isos && !right { + let f = |aa: f64, bb: f64, cc: f64| { + let caa = aa.cos(); + let cbb = bb.cos(); + let ccc = cc.cos(); + if caa.abs() < 1e-12 || cbb.abs() < 1e-12 || ccc.abs() < 1e-12 { + return f64::NAN; + } + let d_ba = (bb - aa).sin(); + let d_bc = (bb - cc).sin(); + let d_ca = (cc - aa).sin(); + let d_cb = (cc - bb).sin(); + if d_ba.abs() < 1e-12 || d_bc.abs() < 1e-12 || d_ca.abs() < 1e-12 || d_cb.abs() < 1e-12 + { + return f64::NAN; + } + let csc_ba = 1.0 / d_ba; + let csc_bc = 1.0 / d_bc; + let csc_ca = 1.0 / d_ca; + let csc_cb = 1.0 / d_cb; + let seca = 1.0 / caa; + let secb = 1.0 / cbb; + let secc = 1.0 / ccc; + let u = seca * csc_ca + secb * csc_cb; + csc_ba * (seca * csc_ba + secc * csc_bc) - csc_ca * u + }; + push_bary( + &mut out, + "垂心X110线共轭", + "X_{247}", + "X(247) = X(4)\\text{-line conjugate of }X(110);\\ \\csc/\\sec\\text{ form}", + "X(247):垂心对 X(110) 的线共轭", + "X247", + sa * f(g[0], g[1], g[2]), + sb * f(g[1], g[2], g[0]), + sc * f(g[2], g[0], g[1]), + ); + } + // X(248): skip equilateral (A+ω→π/2) + if !equil && area > 1e-12 { + let sum_sq = a * a + b * b + c * c; + let tan_w = (4.0 * area) / sum_sq; + let cos_w = 1.0 / (1.0 + tan_w * tan_w).sqrt(); + let sin_w = tan_w * cos_w; + let f = |aa: f64, saa: f64, caa: f64| { + let caw = caa * cos_w - saa * sin_w; + if caw.abs() < 1e-12 { + return f64::NAN; + } + saa * (2.0 * aa).sin() / caw + }; + push_bary( + &mut out, + "垂心X132线共轭", + "X_{248}", + "X(248) = X(4)\\text{-line conjugate of }X(132);\\ \\sin A\\sin 2A\\sec(A+\\omega)", + "X(248):垂心对 X(132) 的线共轭", + "X248", + f(g[0], sa, ca), + f(g[1], sb, cb), + f(g[2], sc, cc), + ); + } + // X(249): isogonal of X(115); finite on equilateral (=G) + { + let wa = (g[1] - g[2]).cos().powi(2); + let wb = (g[2] - g[0]).cos().powi(2); + let wc = (g[0] - g[1]).cos().powi(2); + push_bary( + &mut out, + "X115等角", + "X_{249}", + "X(249) = \\mathrm{isog}(X_{115});\\ \\cos^2(B-C)", + "X(249):X(115) 的等角共轭", + "X249", + wa, + wb, + wc, + ); + } + // X(250): skip right (tan) and isosceles (csc) + if !right && !isos { + let f = |aa: f64, bb: f64, cc: f64| { + let d = (bb - cc).sin(); + if d.abs() < 1e-12 { + return f64::NAN; + } + aa.tan() / (d * d) + }; + push_bary( + &mut out, + "X125等角", + "X_{250}", + "X(250) = \\mathrm{isog}(X_{125});\\ \\tan A\\csc^2(B-C)", + "X(250):X(125) 的等角共轭", + "X250", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + } + + + // X(251)–X(260): next isogonal / cross-conjugate band (ETC). + // X(251)=isogonal of X(141); bary a²/(b²+c²) = a³ csc(A+ω); →G on equil. + // X(252)=isogonal of X(143); bary 1/f, f=(v+w)²[…], u=sin2A; skip right; →G on equil. + // X(253)=X(4)-cross conjugate of X(2)=isog(X154); bary 1/(tanB+tanC−tanA); skip right. + // X(254)=X(3)-cross conjugate of X(4)=isog(X155); bary tanA/(cos²B+cos²C−cos²A); skip right. + // X(255)=isogonal of X(158); bary sin A cos² A. + // X(256)=isogonal of X(171); bary a/(a²+bc). + // X(257)=isogonal of X(172); bary 1/(a²+bc). + // X(258)=congruent-incircles isoscelizer=isog(X173); bary (sin A)/(cos B/2+cos C/2−cos A/2). + // X(259)=isogonal of X(174); bary sin A cos(A/2). + // X(260)=isogonal of X(177); bary (sin A)cos(A/2)/(cos B/2+cos C/2−cos A/2). + { + let (ca, cb, cc) = (g[0].cos(), g[1].cos(), g[2].cos()); + let (sa, sb, sc) = (g[0].sin(), g[1].sin(), g[2].sin()); + let right = ca.abs() < 1e-9 || cb.abs() < 1e-9 || cc.abs() < 1e-9; + let push_bary = |out: &mut Vec, + group: &str, + label: &str, + tip: &str, + zh: &str, + tag: &str, + wa: f64, + wb: f64, + wc: f64| { + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push( + fact(group, label, pt(xy[0], xy[1]), None, tip, zh).with(Shape::Point { + x: xy[0], + y: xy[1], + label: tag.into(), + }), + ); + } + } + }; + // X(251): always finite (=G on equilateral) + { + let wa = a * a / (b * b + c * c); + let wb = b * b / (c * c + a * a); + let wc = c * c / (a * a + b * b); + push_bary( + &mut out, + "X141等角", + "X_{251}", + "X(251) = \\mathrm{isog}(X_{141});\\ a^2/(b^2+c^2)", + "X(251):X(141) 的等角共轭", + "X251", + wa, + wb, + wc, + ); + } + // X(252): isogonal of X(143); skip right (f→0); →G on equilateral + if !right { + let u = (2.0 * g[0]).sin(); + let v = (2.0 * g[1]).sin(); + let w = (2.0 * g[2]).sin(); + let f = |uu: f64, vv: f64, ww: f64| { + let bracket = uu.powi(4) + + vv.powi(4) + + ww.powi(4) + - uu * uu * (2.0 * vv * vv + 2.0 * ww * ww - vv * ww) + - vv * ww * (vv * vv + ww * ww); + (vv + ww).powi(2) * bracket + }; + let fa = f(u, v, w); + let fb = f(v, w, u); + let fc = f(w, u, v); + if fa.abs() > 1e-14 && fb.abs() > 1e-14 && fc.abs() > 1e-14 { + push_bary( + &mut out, + "X143等角", + "X_{252}", + "X(252) = \\mathrm{isog}(X_{143});\\ 1/f,\\ f=(v+w)^2[\\cdots]", + "X(252):X(143) 的等角共轭", + "X252", + 1.0 / fa, + 1.0 / fb, + 1.0 / fc, + ); + } + } + // X(253): skip right (tan ∞) + if !right { + let f = |aa: f64, bb: f64, cc: f64| { + let den = bb.tan() + cc.tan() - aa.tan(); + if den.abs() < 1e-12 { + return f64::NAN; + } + 1.0 / den + }; + push_bary( + &mut out, + "垂心X2交叉共轭", + "X_{253}", + "X(253) = X(4)\\text{-cross conjugate of }X(2);\\ 1/(\\tan B+\\tan C-\\tan A)", + "X(253):垂心对重心的交叉共轭(X(154) 等角)", + "X253", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + // X(254): skip right (tan ∞) + if !right { + let f = |aa: f64, bb: f64, cc: f64| { + let den = bb.cos().powi(2) + cc.cos().powi(2) - aa.cos().powi(2); + if den.abs() < 1e-12 { + return f64::NAN; + } + aa.tan() / den + }; + push_bary( + &mut out, + "外心X4交叉共轭", + "X_{254}", + "X(254) = X(3)\\text{-cross conjugate of }X(4);\\ \\tan A/(\\cos^2 B+\\cos^2 C-\\cos^2 A)", + "X(254):外心对垂心的交叉共轭(X(155) 等角)", + "X254", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + // X(255) + { + push_bary( + &mut out, + "X158等角", + "X_{255}", + "X(255) = \\mathrm{isog}(X_{158});\\ \\sin A\\cos^2 A", + "X(255):X(158) 的等角共轭", + "X255", + sa * ca * ca, + sb * cb * cb, + sc * cc * cc, + ); + } + // X(256) + { + push_bary( + &mut out, + "X171等角", + "X_{256}", + "X(256) = \\mathrm{isog}(X_{171});\\ a/(a^2+bc)", + "X(256):X(171) 的等角共轭", + "X256", + a / (a * a + b * c), + b / (b * b + c * a), + c / (c * c + a * b), + ); + } + // X(257) + { + push_bary( + &mut out, + "X172等角", + "X_{257}", + "X(257) = \\mathrm{isog}(X_{172});\\ 1/(a^2+bc)", + "X(257):X(172) 的等角共轭", + "X257", + 1.0 / (a * a + b * c), + 1.0 / (b * b + c * a), + 1.0 / (c * c + a * b), + ); + } + // X(258) + { + let f = |aa: f64, bb: f64, cc: f64| { + let den = (bb * 0.5).cos() + (cc * 0.5).cos() - (aa * 0.5).cos(); + if den.abs() < 1e-12 { + return f64::NAN; + } + aa.sin() / den + }; + push_bary( + &mut out, + "等内切等腰截点", + "X_{258}", + "X(258) = \\mathrm{isog}(X_{173});\\ (\\sin A)/(\\cos\\tfrac{B}{2}+\\cos\\tfrac{C}{2}-\\cos\\tfrac{A}{2})", + "X(258):等内切圆等腰截点(Yff)", + "X258", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + // X(259) + { + push_bary( + &mut out, + "X174等角", + "X_{259}", + "X(259) = \\mathrm{isog}(X_{174});\\ \\sin A\\cos\\tfrac{A}{2}", + "X(259):X(174) 的等角共轭", + "X259", + sa * (g[0] * 0.5).cos(), + sb * (g[1] * 0.5).cos(), + sc * (g[2] * 0.5).cos(), + ); + } + // X(260) + { + let f = |aa: f64, bb: f64, cc: f64| { + let den = (bb * 0.5).cos() + (cc * 0.5).cos() - (aa * 0.5).cos(); + if den.abs() < 1e-12 { + return f64::NAN; + } + aa.sin() * (aa * 0.5).cos() / den + }; + push_bary( + &mut out, + "X177等角", + "X_{260}", + "X(260) = \\mathrm{isog}(X_{177});\\ (\\sin A)\\cos\\tfrac{A}{2}/(\\cos\\tfrac{B}{2}+\\cdots)", + "X(260):X(177) 的等角共轭", + "X260", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + } + + + // X(261)–X(270): continue isogonal band (ETC). + // X(261)=isog(X181)=isotomic(X12); bary (sin A)[(csc A)sec(B/2−C/2)]². + // X(262)=isog(X182)=isotomic(X183); bary sin A sec(A−ω). + // X(263)=isog(X183); bary a³ sec(A−ω). + // X(264)=isotomic(O)=isog(X184); bary csc 2A; skip right. + // X(265)=isog(X186); bary sin A sin 2A csc 3A; skip equil / 60° (sin 3A=0). + // X(266)=isog(X188); bary sin A sin(A/2). + // X(267)=isog(X191); bary a/[b³+c³−a³+(b+c−a)(bc+ca+ab)]. + // X(268)=isog(X196); bary (sin A)(cos A cot(A/2))/(−1−cos A+cos B+cos C). + // X(269)=isog(X200); bary sin A tan²(A/2). + // X(270)=isog(X201); bary (tan A)/[1+cos(B−C)]; skip right. + { + let (ca, cb, cc) = (g[0].cos(), g[1].cos(), g[2].cos()); + let (sa, sb, sc) = (g[0].sin(), g[1].sin(), g[2].sin()); + let right = ca.abs() < 1e-9 || cb.abs() < 1e-9 || cc.abs() < 1e-9; + let equil = (a - b).abs() < 1e-12 && (b - c).abs() < 1e-12; + let has_60 = (3.0 * g[0]).sin().abs() < 1e-9 + || (3.0 * g[1]).sin().abs() < 1e-9 + || (3.0 * g[2]).sin().abs() < 1e-9; + let push_bary = |out: &mut Vec, + group: &str, + label: &str, + tip: &str, + zh: &str, + tag: &str, + wa: f64, + wb: f64, + wc: f64| { + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push( + fact(group, label, pt(xy[0], xy[1]), None, tip, zh).with(Shape::Point { + x: xy[0], + y: xy[1], + label: tag.into(), + }), + ); + } + } + }; + // X(261): always finite for non-degen + { + let f = |aa: f64, bb: f64, cc: f64| { + let half_diff = (bb - cc) * 0.5; + let csc_a = 1.0 / aa.sin(); + let sec_d = 1.0 / half_diff.cos(); + aa.sin() * (csc_a * sec_d).powi(2) + }; + push_bary( + &mut out, + "X181等角", + "X_{261}", + "X(261) = \\mathrm{isog}(X_{181})=\\mathrm{isotomic}(X_{12});\\ (\\sin A)[(\\csc A)\\sec(\\tfrac{B-C}{2})]^2", + "X(261):X(181) 的等角共轭(X(12) 等截)", + "X261", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + // X(262)/X(263): Brocard form sec(A−ω) + if area > 1e-12 { + let sum_sq = a * a + b * b + c * c; + let tan_w = (4.0 * area) / sum_sq; + let cos_w = 1.0 / (1.0 + tan_w * tan_w).sqrt(); + let sin_w = tan_w * cos_w; + // cos(A−ω) = cos A cos ω + sin A sin ω + let caw = |caa: f64, saa: f64| caa * cos_w + saa * sin_w; + let cawa = caw(ca, sa); + let cawb = caw(cb, sb); + let cawc = caw(cc, sc); + if cawa.abs() > 1e-12 && cawb.abs() > 1e-12 && cawc.abs() > 1e-12 { + push_bary( + &mut out, + "X182等角", + "X_{262}", + "X(262) = \\mathrm{isog}(X_{182});\\ \\sin A\\sec(A-\\omega)", + "X(262):X(182) 的等角共轭(X(183) 等截)", + "X262", + sa / cawa, + sb / cawb, + sc / cawc, + ); + push_bary( + &mut out, + "X183等角", + "X_{263}", + "X(263) = \\mathrm{isog}(X_{183});\\ a^3\\sec(A-\\omega)", + "X(263):X(183) 的等角共轭", + "X263", + a * a * a / cawa, + b * b * b / cawb, + c * c * c / cawc, + ); + } + } + // X(264): skip right (csc 2A → ∞) + if !right { + push_bary( + &mut out, + "外心等截", + "X_{264}", + "X(264) = \\mathrm{isotomic}(X_{3})=\\mathrm{isog}(X_{184});\\ \\csc 2A", + "X(264):外心的等截共轭(X(184) 等角)", + "X264", + 1.0 / (2.0 * g[0]).sin(), + 1.0 / (2.0 * g[1]).sin(), + 1.0 / (2.0 * g[2]).sin(), + ); + } + // X(265): skip equilateral / any 60° (sin 3A=0) + if !equil && !has_60 { + let f = |aa: f64| { + let s3 = (3.0 * aa).sin(); + if s3.abs() < 1e-12 { + return f64::NAN; + } + aa.sin() * (2.0 * aa).sin() / s3 + }; + push_bary( + &mut out, + "X186等角", + "X_{265}", + "X(265) = \\mathrm{isog}(X_{186});\\ \\sin A\\sin 2A\\csc 3A", + "X(265):X(186) 的等角共轭", + "X265", + f(g[0]), + f(g[1]), + f(g[2]), + ); + } + // X(266) + { + push_bary( + &mut out, + "X188等角", + "X_{266}", + "X(266) = \\mathrm{isog}(X_{188});\\ \\sin A\\sin\\tfrac{A}{2}", + "X(266):X(188) 的等角共轭", + "X266", + sa * (g[0] * 0.5).sin(), + sb * (g[1] * 0.5).sin(), + sc * (g[2] * 0.5).sin(), + ); + } + // X(267) + { + let f = |aa: f64, bb: f64, cc: f64| { + let den = bb.powi(3) + cc.powi(3) - aa.powi(3) + + (bb + cc - aa) * (bb * cc + cc * aa + aa * bb); + if den.abs() < 1e-14 { + return f64::NAN; + } + aa / den + }; + push_bary( + &mut out, + "X191等角", + "X_{267}", + "X(267) = \\mathrm{isog}(X_{191});\\ a/[b^3+c^3-a^3+(b+c-a)(\\cdots)]", + "X(267):X(191) 的等角共轭(X(229) 反射对)", + "X267", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(268) + { + let f = |aa: f64, bb: f64, cc: f64| { + let den = -1.0 - aa.cos() + bb.cos() + cc.cos(); + if den.abs() < 1e-12 { + return f64::NAN; + } + let half = aa * 0.5; + if half.tan().abs() < 1e-14 { + return f64::NAN; + } + aa.sin() * aa.cos() / half.tan() / den + }; + push_bary( + &mut out, + "X196等角", + "X_{268}", + "X(268) = \\mathrm{isog}(X_{196});\\ (\\sin A)(\\cos A\\cot\\tfrac{A}{2})/(-1-\\cos A+\\cdots)", + "X(268):X(196) 的等角共轭", + "X268", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + // X(269) + { + push_bary( + &mut out, + "X200等角", + "X_{269}", + "X(269) = \\mathrm{isog}(X_{200});\\ \\sin A\\tan^2\\tfrac{A}{2}", + "X(269):X(200) 的等角共轭", + "X269", + sa * (g[0] * 0.5).tan().powi(2), + sb * (g[1] * 0.5).tan().powi(2), + sc * (g[2] * 0.5).tan().powi(2), + ); + } + // X(270): skip right (tan ∞) + if !right { + let f = |aa: f64, bb: f64, cc: f64| { + let den = 1.0 + (bb - cc).cos(); + if den.abs() < 1e-12 { + return f64::NAN; + } + aa.tan() / den + }; + push_bary( + &mut out, + "X201等角", + "X_{270}", + "X(270) = \\mathrm{isog}(X_{201});\\ (\\tan A)/[1+\\cos(B-C)]", + "X(270):X(201) 的等角共轭", + "X270", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + } + + + // X(271)–X(280): continue isogonal / cevapoint band (ETC). + // X(271)=isog(X208)=isotomic(X342); bary (sin A)(cot A cot A/2)/(-1-cos A+cos B+cos C); skip right. + // X(272)=isog(X209); bary (sin A)/[(sin A+sin(A−B)+sin(A−C))(sin B+sin C)]. + // X(273)=isog(X212)=isotomic(X78)=polar(X9); bary tan A sec²(A/2); skip right. + // X(274)=isog(X213)=isotomic(X37); bary bc/(b+c). + // X(275)=cevapoint(H,K)=isog(X216)=polar(X5); bary sec A sec(B−C); skip right. + // X(276)=isog(X217)=Hofstadter −2; bary csc² A sec A sec(B−C); skip right. + // X(277)=isog(X218); bary 1/(a²+b²+c²−2ab−2ac). + // X(278)=isog(X219); bary 1−sec A [= tan A tan(A/2)]; skip right. + // X(279)=isog(X220); bary tan²(A/2). + // X(280)=X(1)-cross of X(8)=isog(X221); bary (sin A)csc²(A/2)/(-1-cos A+cos B+cos C). + { + let (ca, cb, cc) = (g[0].cos(), g[1].cos(), g[2].cos()); + let right = ca.abs() < 1e-9 || cb.abs() < 1e-9 || cc.abs() < 1e-9; + let push_bary = |out: &mut Vec, + group: &str, + label: &str, + tip: &str, + zh: &str, + tag: &str, + wa: f64, + wb: f64, + wc: f64| { + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push( + fact(group, label, pt(xy[0], xy[1]), None, tip, zh).with(Shape::Point { + x: xy[0], + y: xy[1], + label: tag.into(), + }), + ); + } + } + }; + // X(271): skip right (cot A → ∞) + if !right { + let f = |aa: f64, bb: f64, cc: f64| { + let den = -1.0 - aa.cos() + bb.cos() + cc.cos(); + if den.abs() < 1e-12 { + return f64::NAN; + } + let half = aa * 0.5; + if half.tan().abs() < 1e-14 { + return f64::NAN; + } + // (sin A)(cot A cot A/2)/den = cos A · cot(A/2) / den + aa.cos() / half.tan() / den + }; + push_bary( + &mut out, + "X208等角", + "X_{271}", + "X(271) = \\mathrm{isog}(X_{208});\\ (\\sin A)(\\cot A\\cot\\tfrac{A}{2})/(-1-\\cos A+\\cdots)", + "X(271):X(208) 的等角共轭(X(342) 等截)", + "X271", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + // X(272) + { + let f = |aa: f64, bb: f64, cc: f64| { + let num = aa.sin() + (aa - bb).sin() + (aa - cc).sin(); + let den = (bb.sin() + cc.sin()) * num; + if den.abs() < 1e-14 { + return f64::NAN; + } + aa.sin() / den + }; + push_bary( + &mut out, + "X209等角", + "X_{272}", + "X(272) = \\mathrm{isog}(X_{209});\\ (\\sin A)/[(\\sin A+\\sin(A-B)+\\sin(A-C))(\\sin B+\\sin C)]", + "X(272):X(209) 的等角共轭", + "X272", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + // X(273): skip right (tan A → ∞) + if !right { + let f = |aa: f64| { + let c_half = (aa * 0.5).cos(); + if c_half.abs() < 1e-14 { + return f64::NAN; + } + aa.tan() / (c_half * c_half) + }; + push_bary( + &mut out, + "X212等角", + "X_{273}", + "X(273) = \\mathrm{isog}(X_{212})=\\mathrm{isotomic}(X_{78});\\ \\tan A\\sec^2\\tfrac{A}{2}", + "X(273):X(212) 的等角共轭(X(78) 等截 / X(9) 极共轭)", + "X273", + f(g[0]), + f(g[1]), + f(g[2]), + ); + } + // X(274) + { + push_bary( + &mut out, + "X213等角", + "X_{274}", + "X(274) = \\mathrm{isog}(X_{213})=\\mathrm{isotomic}(X_{37});\\ bc/(b+c)", + "X(274):X(213) 的等角共轭(X(37) 等截)", + "X274", + b * c / (b + c), + c * a / (c + a), + a * b / (a + b), + ); + } + // X(275): skip right (sec A → ∞); cevapoint of H and K + if !right { + let f = |aa: f64, bb: f64, cc: f64| { + let cbc = (bb - cc).cos(); + if aa.cos().abs() < 1e-14 || cbc.abs() < 1e-14 { + return f64::NAN; + } + 1.0 / (aa.cos() * cbc) + }; + push_bary( + &mut out, + "垂心勒莫恩截点", + "X_{275}", + "X(275) = \\mathrm{cevapoint}(H,K)=\\mathrm{isog}(X_{216});\\ \\sec A\\sec(B-C)", + "X(275):垂心与勒莫恩点的截点(X(216) 等角 / X(5) 极共轭)", + "X275", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + // X(276): skip right (sec A → ∞); Hofstadter −2 + if !right { + let f = |aa: f64, bb: f64, cc: f64| { + let saa = aa.sin(); + let caa = aa.cos(); + let cbc = (bb - cc).cos(); + if saa.abs() < 1e-14 || caa.abs() < 1e-14 || cbc.abs() < 1e-14 { + return f64::NAN; + } + 1.0 / (saa * saa * caa * cbc) + }; + push_bary( + &mut out, + "X217等角", + "X_{276}", + "X(276) = \\mathrm{isog}(X_{217});\\ \\csc^2 A\\sec A\\sec(B-C)", + "X(276):X(217) 的等角共轭(Hofstadter −2 点)", + "X276", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + // X(277) + { + let f = |aa: f64, bb: f64, cc: f64| { + let den = aa * aa + bb * bb + cc * cc - 2.0 * aa * bb - 2.0 * aa * cc; + if den.abs() < 1e-14 { + return f64::NAN; + } + 1.0 / den + }; + push_bary( + &mut out, + "X218等角", + "X_{277}", + "X(277) = \\mathrm{isog}(X_{218});\\ 1/(a^2+b^2+c^2-2ab-2ac)", + "X(277):X(218) 的等角共轭", + "X277", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(278): skip right (sec A → ∞) + if !right { + push_bary( + &mut out, + "X219等角", + "X_{278}", + "X(278) = \\mathrm{isog}(X_{219});\\ 1-\\sec A\\ [=\\tan A\\tan\\tfrac{A}{2}]", + "X(278):X(219) 的等角共轭", + "X278", + 1.0 - 1.0 / ca, + 1.0 - 1.0 / cb, + 1.0 - 1.0 / cc, + ); + } + // X(279) + { + push_bary( + &mut out, + "X220等角", + "X_{279}", + "X(279) = \\mathrm{isog}(X_{220});\\ \\tan^2\\tfrac{A}{2}", + "X(279):X(220) 的等角共轭", + "X279", + (g[0] * 0.5).tan().powi(2), + (g[1] * 0.5).tan().powi(2), + (g[2] * 0.5).tan().powi(2), + ); + } + // X(280) + { + let f = |aa: f64, bb: f64, cc: f64| { + let den = -1.0 - aa.cos() + bb.cos() + cc.cos(); + let s_half = (aa * 0.5).sin(); + if den.abs() < 1e-12 || s_half.abs() < 1e-14 { + return f64::NAN; + } + aa.sin() / (s_half * s_half) / den + }; + push_bary( + &mut out, + "内心奈格尔交叉共轭", + "X_{280}", + "X(280) = X(1)\\text{-cross of }X(8)=\\mathrm{isog}(X_{221});\\ (\\sin A)\\csc^2\\tfrac{A}{2}/(-1-\\cos A+\\cdots)", + "X(280):内心对奈格尔点的交叉共轭(X(221) 等角)", + "X280", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + } + + + // X(281)–X(290): continue isogonal / cross-conjugate / cevapoint band (ETC). + // X(281)=isog(X222)=X(37)-cross of X(9); bary 1+sec A; skip right. + // X(282)=isog(X223)=X(6)-cross of X(9); bary (sin A)cot(A/2)/(-1-cos A+cos B+cos C). + // X(283)=isog(X225)=X(3)-cross of X(21); bary sin 2A/(cos B+cos C). + // X(284)=isog(X226)=X(55)-cross of X(21)=isotomic(X349); bary a²/(cos B+cos C). + // X(285)=isog(X227)=X(58)-cross of X(21); bary (sin A)/[(cos B+cos C)(-1-cos A+…)]. + // X(286)=isog(X228)=X(4)-cross of X(27)=isotomic(X72); bary sec A/(sin B+sin C); skip right. + // X(287)=isog(X232)=X(2)-Hirst of X(98); bary cos A sec(A+ω); skip equil (A+ω→π/2). + // X(288)=isog(X233)=cevapoint(X6,X54); bary (sin A)sec(B−C)/[b cos(C−A)+c cos(B−A)]. + // X(289)=isog(X236); bary (sin A)sin(A/2)/(cos B/2+cos C/2−cos A/2) [=a(1−sin A/2)tan(A/2)]. + // X(290)=isog(X237)=isotomic(X511); bary csc A sec(A+ω); skip equil. + { + let (ca, cb, cc) = (g[0].cos(), g[1].cos(), g[2].cos()); + let (sa, sb, sc) = (g[0].sin(), g[1].sin(), g[2].sin()); + let equil = (a - b).abs() < 1e-12 && (b - c).abs() < 1e-12; + let right = ca.abs() < 1e-9 || cb.abs() < 1e-9 || cc.abs() < 1e-9; + let push_bary = |out: &mut Vec, + group: &str, + label: &str, + tip: &str, + zh: &str, + tag: &str, + wa: f64, + wb: f64, + wc: f64| { + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push( + fact(group, label, pt(xy[0], xy[1]), None, tip, zh).with(Shape::Point { + x: xy[0], + y: xy[1], + label: tag.into(), + }), + ); + } + } + }; + // X(281): skip right (sec A → ∞) + if !right { + push_bary( + &mut out, + "X222等角", + "X_{281}", + "X(281) = \\mathrm{isog}(X_{222});\\ 1+\\sec A", + "X(281):X(222) 的等角共轭(X(37) 对 X(9) 交叉共轭)", + "X281", + 1.0 + 1.0 / ca, + 1.0 + 1.0 / cb, + 1.0 + 1.0 / cc, + ); + } + // X(282) + { + let f = |aa: f64, bb: f64, cc_: f64| { + let den = -1.0 - aa.cos() + bb.cos() + cc_.cos(); + let half = aa * 0.5; + if den.abs() < 1e-12 || half.tan().abs() < 1e-14 { + return f64::NAN; + } + aa.sin() / half.tan() / den + }; + push_bary( + &mut out, + "X223等角", + "X_{282}", + "X(282) = \\mathrm{isog}(X_{223});\\ (\\sin A)\\cot\\tfrac{A}{2}/(-1-\\cos A+\\cdots)", + "X(282):X(223) 的等角共轭(X(6) 对 X(9) 交叉共轭)", + "X282", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + // X(283) + { + let f = |aa: f64, bb: f64, cc_: f64| { + let den = bb.cos() + cc_.cos(); + if den.abs() < 1e-14 { + return f64::NAN; + } + (2.0 * aa).sin() / den + }; + push_bary( + &mut out, + "X225等角", + "X_{283}", + "X(283) = \\mathrm{isog}(X_{225});\\ \\sin 2A/(\\cos B+\\cos C)", + "X(283):X(225) 的等角共轭(外心对 X(21) 交叉共轭)", + "X283", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + // X(284) + { + let f = |side: f64, bb: f64, cc_: f64| { + let den = bb.cos() + cc_.cos(); + if den.abs() < 1e-14 { + return f64::NAN; + } + side * side / den + }; + push_bary( + &mut out, + "X226等角", + "X_{284}", + "X(284) = \\mathrm{isog}(X_{226})=\\mathrm{isotomic}(X_{349});\\ a^2/(\\cos B+\\cos C)", + "X(284):X(226) 的等角共轭(X(55) 对 X(21) 交叉共轭)", + "X284", + f(a, g[1], g[2]), + f(b, g[2], g[0]), + f(c, g[0], g[1]), + ); + } + // X(285) + { + let f = |aa: f64, bb: f64, cc_: f64| { + let d1 = bb.cos() + cc_.cos(); + let d2 = -1.0 - aa.cos() + bb.cos() + cc_.cos(); + if d1.abs() < 1e-14 || d2.abs() < 1e-12 { + return f64::NAN; + } + aa.sin() / (d1 * d2) + }; + push_bary( + &mut out, + "X227等角", + "X_{285}", + "X(285) = \\mathrm{isog}(X_{227});\\ (\\sin A)/[(\\cos B+\\cos C)(-1-\\cos A+\\cdots)]", + "X(285):X(227) 的等角共轭(X(58) 对 X(21) 交叉共轭)", + "X285", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + // X(286): skip right (sec A → ∞) + if !right { + let f = |aa: f64, bb: f64, cc_: f64| { + let caa = aa.cos(); + let den = bb.sin() + cc_.sin(); + if caa.abs() < 1e-14 || den.abs() < 1e-14 { + return f64::NAN; + } + (1.0 / caa) / den + }; + push_bary( + &mut out, + "X228等角", + "X_{286}", + "X(286) = \\mathrm{isog}(X_{228})=\\mathrm{isotomic}(X_{72});\\ \\sec A/(\\sin B+\\sin C)", + "X(286):X(228) 的等角共轭(垂心对 X(27) 交叉共轭)", + "X286", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + // X(287): skip equilateral (A+ω → π/2) + if !equil && area > 1e-12 { + let sum_sq = a * a + b * b + c * c; + let tan_w = (4.0 * area) / sum_sq; + let cos_w = 1.0 / (1.0 + tan_w * tan_w).sqrt(); + let sin_w = tan_w * cos_w; + let f = |caa: f64, saa: f64| { + let caw = caa * cos_w - saa * sin_w; + if caw.abs() < 1e-12 { + return f64::NAN; + } + caa / caw + }; + push_bary( + &mut out, + "X232等角", + "X_{287}", + "X(287) = \\mathrm{isog}(X_{232});\\ \\cos A\\sec(A+\\omega)", + "X(287):X(232) 的等角共轭(重心对 X(98) 的 Hirst 逆)", + "X287", + f(ca, sa), + f(cb, sb), + f(cc, sc), + ); + } + // X(288): cevapoint of K=X(6) and X(54) + { + let f = |aa: f64, bb: f64, cc_: f64, side_b: f64, side_c: f64| { + let cbc = (bb - cc_).cos(); + let den = side_b * (cc_ - aa).cos() + side_c * (bb - aa).cos(); + if cbc.abs() < 1e-14 || den.abs() < 1e-14 { + return f64::NAN; + } + aa.sin() / cbc / den + }; + push_bary( + &mut out, + "对称中线X54截点", + "X_{288}", + "X(288) = \\mathrm{cevapoint}(X_6,X_{54})=\\mathrm{isog}(X_{233});\\ (\\sin A)\\sec(B-C)/[b\\cos(C-A)+c\\cos(B-A)]", + "X(288):对称中线点与 X(54) 的截点(X(233) 等角)", + "X288", + f(g[0], g[1], g[2], b, c), + f(g[1], g[2], g[0], c, a), + f(g[2], g[0], g[1], a, b), + ); + } + // X(289) + { + let f = |aa: f64, bb: f64, cc_: f64| { + let den = (bb * 0.5).cos() + (cc_ * 0.5).cos() - (aa * 0.5).cos(); + if den.abs() < 1e-14 { + return f64::NAN; + } + aa.sin() * (aa * 0.5).sin() / den + }; + push_bary( + &mut out, + "X236等角", + "X_{289}", + "X(289) = \\mathrm{isog}(X_{236});\\ (\\sin A)\\sin\\tfrac{A}{2}/(\\cos\\tfrac{B}{2}+\\cos\\tfrac{C}{2}-\\cos\\tfrac{A}{2})", + "X(289):X(236) 的等角共轭", + "X289", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + // X(290): skip equilateral (A+ω → π/2) + if !equil && area > 1e-12 { + let sum_sq = a * a + b * b + c * c; + let tan_w = (4.0 * area) / sum_sq; + let cos_w = 1.0 / (1.0 + tan_w * tan_w).sqrt(); + let sin_w = tan_w * cos_w; + let f = |saa: f64, caa: f64| { + let caw = caa * cos_w - saa * sin_w; + if saa.abs() < 1e-14 || caw.abs() < 1e-12 { + return f64::NAN; + } + (1.0 / saa) / caw + }; + push_bary( + &mut out, + "X237等角", + "X_{290}", + "X(290) = \\mathrm{isog}(X_{237});\\ \\csc A\\sec(A+\\omega)", + "X(290):X(237) 的等角共轭(X(511) 等截)", + "X290", + f(sa, ca), + f(sb, cb), + f(sc, cc), + ); + } + } + + + // X(291)–X(300): continue isogonal / Hirst / isotomic band (ETC). + // X(291)=isog(X238)=isotomic(X350); bary a/(a²−bc); skip equil. + // X(292)=isog(X239); bary a²/(a²−bc); X(1)-Hirst of X(291); skip equil. + // X(293)=isog(X240); bary sin 2A sec(A+ω); skip equil (A+ω→π/2). + // X(294)=isog(X241); bary a(b+c−a)/(b²+c²−ab−ac); skip equil. + // X(295)=isog(X242); bary sin 2A/(a²−bc); skip equil. + // X(296)=isog(X243); bary sin 2A/(cos²A−cos B cos C); skip equil. + // X(297)=isog(X248)=X(2)-Hirst of X(4)=isotomic(X287); bary sec A cos(A+ω); skip right/equil. + // X(298)=isotomic(X13); bary csc A sin(A+π/3). + // X(299)=isotomic(X14); bary csc A sin(A−π/3); skip equil (sin→0). + // X(300)=isotomic(X15); bary csc A csc(A+π/3); skip 120° (csc(A+π/3)→∞). + { + let (ca, cb, cc) = (g[0].cos(), g[1].cos(), g[2].cos()); + let (sa, sb, sc) = (g[0].sin(), g[1].sin(), g[2].sin()); + let equil = (a - b).abs() < 1e-12 && (b - c).abs() < 1e-12; + let right = ca.abs() < 1e-9 || cb.abs() < 1e-9 || cc.abs() < 1e-9; + let has_120 = (g[0] - std::f64::consts::FRAC_PI_3 * 2.0).abs() < 1e-9 + || (g[1] - std::f64::consts::FRAC_PI_3 * 2.0).abs() < 1e-9 + || (g[2] - std::f64::consts::FRAC_PI_3 * 2.0).abs() < 1e-9; + let push_bary = |out: &mut Vec, + group: &str, + label: &str, + tip: &str, + zh: &str, + tag: &str, + wa: f64, + wb: f64, + wc: f64| { + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push( + fact(group, label, pt(xy[0], xy[1]), None, tip, zh).with(Shape::Point { + x: xy[0], + y: xy[1], + label: tag.into(), + }), + ); + } + } + }; + // X(291)/X(292)/X(295): skip equilateral (a²−bc → 0) + if !equil { + let da = a * a - b * c; + let db = b * b - c * a; + let dc = c * c - a * b; + if da.abs() > 1e-12 && db.abs() > 1e-12 && dc.abs() > 1e-12 { + push_bary( + &mut out, + "X238等角", + "X_{291}", + "X(291) = \\mathrm{isog}(X_{238});\\ a/(a^2-bc)", + "X(291):X(238) 的等角共轭(X(350) 等截)", + "X291", + a / da, + b / db, + c / dc, + ); + push_bary( + &mut out, + "X239等角", + "X_{292}", + "X(292) = \\mathrm{isog}(X_{239});\\ a^2/(a^2-bc)", + "X(292):X(239) 的等角共轭(内心对 X(291) 的 Hirst 逆)", + "X292", + a * a / da, + b * b / db, + c * c / dc, + ); + push_bary( + &mut out, + "X242等角", + "X_{295}", + "X(295) = \\mathrm{isog}(X_{242});\\ \\sin 2A/(a^2-bc)", + "X(295):X(242) 的等角共轭", + "X295", + (2.0 * g[0]).sin() / da, + (2.0 * g[1]).sin() / db, + (2.0 * g[2]).sin() / dc, + ); + } + } + // X(293): skip equilateral (A+ω → π/2) + if !equil && area > 1e-12 { + let sum_sq = a * a + b * b + c * c; + let tan_w = (4.0 * area) / sum_sq; + let cos_w = 1.0 / (1.0 + tan_w * tan_w).sqrt(); + let sin_w = tan_w * cos_w; + let f = |aa: f64, saa: f64, caa: f64| { + let caw = caa * cos_w - saa * sin_w; + if caw.abs() < 1e-12 { + return f64::NAN; + } + (2.0 * aa).sin() / caw + }; + push_bary( + &mut out, + "X240等角", + "X_{293}", + "X(293) = \\mathrm{isog}(X_{240});\\ \\sin 2A\\sec(A+\\omega)", + "X(293):X(240) 的等角共轭", + "X293", + f(g[0], sa, ca), + f(g[1], sb, cb), + f(g[2], sc, cc), + ); + } + // X(294): skip equilateral (b²+c²−ab−ac → 0) + if !equil { + let f = |aa: f64, bb: f64, cc_: f64| { + let den = bb * bb + cc_ * cc_ - aa * bb - aa * cc_; + if den.abs() < 1e-12 { + return f64::NAN; + } + aa * (bb + cc_ - aa) / den + }; + push_bary( + &mut out, + "X241等角", + "X_{294}", + "X(294) = \\mathrm{isog}(X_{241});\\ a(b+c-a)/(b^2+c^2-ab-ac)", + "X(294):X(241) 的等角共轭", + "X294", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(296): skip equilateral (cos²A−cos B cos C → 0) + if !equil { + let f = |saa2: f64, caa: f64, cbb: f64, ccc: f64| { + let den = caa * caa - cbb * ccc; + if den.abs() < 1e-12 { + return f64::NAN; + } + saa2 / den + }; + push_bary( + &mut out, + "X243等角", + "X_{296}", + "X(296) = \\mathrm{isog}(X_{243});\\ \\sin 2A/(\\cos^2 A-\\cos B\\cos C)", + "X(296):X(243) 的等角共轭", + "X296", + f((2.0 * g[0]).sin(), ca, cb, cc), + f((2.0 * g[1]).sin(), cb, cc, ca), + f((2.0 * g[2]).sin(), cc, ca, cb), + ); + } + // X(297): skip right (sec) and equilateral (cos(A+ω)→0) + if !right && !equil && area > 1e-12 { + let sum_sq = a * a + b * b + c * c; + let tan_w = (4.0 * area) / sum_sq; + let cos_w = 1.0 / (1.0 + tan_w * tan_w).sqrt(); + let sin_w = tan_w * cos_w; + let f = |saa: f64, caa: f64| { + if caa.abs() < 1e-12 { + return f64::NAN; + } + let caw = caa * cos_w - saa * sin_w; + (1.0 / caa) * caw + }; + push_bary( + &mut out, + "X248等角", + "X_{297}", + "X(297) = \\mathrm{isog}(X_{248})=\\mathrm{isotomic}(X_{287});\\ \\sec A\\cos(A+\\omega)", + "X(297):X(248) 的等角共轭(重心对垂心的 Hirst 逆)", + "X297", + f(sa, ca), + f(sb, cb), + f(sc, cc), + ); + } + // X(298): isotomic of 1st isogonic center + { + let pi3 = std::f64::consts::FRAC_PI_3; + let f = |aa: f64, saa: f64| { + if saa.abs() < 1e-14 { + return f64::NAN; + } + (1.0 / saa) * (aa + pi3).sin() + }; + push_bary( + &mut out, + "第一等角线等截", + "X_{298}", + "X(298) = \\mathrm{isotomic}(X_{13});\\ \\csc A\\sin(A+\\pi/3)", + "X(298):第一等角线中心的等截共轭", + "X298", + f(g[0], sa), + f(g[1], sb), + f(g[2], sc), + ); + } + // X(299): isotomic of 2nd isogonic; skip equilateral + if !equil { + let pi3 = std::f64::consts::FRAC_PI_3; + let f = |aa: f64, saa: f64| { + if saa.abs() < 1e-14 { + return f64::NAN; + } + (1.0 / saa) * (aa - pi3).sin() + }; + push_bary( + &mut out, + "第二等角线等截", + "X_{299}", + "X(299) = \\mathrm{isotomic}(X_{14});\\ \\csc A\\sin(A-\\pi/3)", + "X(299):第二等角线中心的等截共轭", + "X299", + f(g[0], sa), + f(g[1], sb), + f(g[2], sc), + ); + } + // X(300): isotomic of 1st isodynamic; skip 120° angle + if !has_120 { + let pi3 = std::f64::consts::FRAC_PI_3; + let f = |aa: f64, saa: f64| { + let s = (aa + pi3).sin(); + if saa.abs() < 1e-14 || s.abs() < 1e-12 { + return f64::NAN; + } + (1.0 / saa) / s + }; + push_bary( + &mut out, + "第一等动力等截", + "X_{300}", + "X(300) = \\mathrm{isotomic}(X_{15});\\ \\csc A\\csc(A+\\pi/3)", + "X(300):第一等动力点的等截共轭", + "X300", + f(g[0], sa), + f(g[1], sb), + f(g[2], sc), + ); + } + } + + // X(301)–X(310): continue isotomic band (ETC). + // X(301)=isotomic(X16); bary csc A csc(A−π/3); skip 60° (incl. equil). + // X(302)=isotomic(X17); bary csc A sin(A+π/6); skip 150°. + // X(303)=isotomic(X18); bary csc A sin(A−π/6); skip 30° (weight 0 ok, keep). + // X(304)=isotomic(X19); bary cos A csc² A. + // X(305)=isotomic(X25); bary b³c³ cos A. + // X(306)=isotomic(X27); bary bc(b+c) cos A. + // X(307)=isotomic(X29); bary a·b²c²(b+c)cos A/(b+c−a). + // X(308)=isotomic(X39); bary b²c²/(b²+c²) [= csc A csc(A+ω)]. + // X(309)=isotomic(X40); bary (csc A)/(-1-cos A+cos B+cos C). + // X(310)=isotomic(X42); bary b²c²/(b+c). + { + let (ca, cb, cc) = (g[0].cos(), g[1].cos(), g[2].cos()); + let (sa, sb, sc) = (g[0].sin(), g[1].sin(), g[2].sin()); + let equil = (a - b).abs() < 1e-12 && (b - c).abs() < 1e-12; + let has_60 = (g[0] - std::f64::consts::FRAC_PI_3).abs() < 1e-9 + || (g[1] - std::f64::consts::FRAC_PI_3).abs() < 1e-9 + || (g[2] - std::f64::consts::FRAC_PI_3).abs() < 1e-9; + let has_150 = (g[0] - std::f64::consts::PI * 5.0 / 6.0).abs() < 1e-9 + || (g[1] - std::f64::consts::PI * 5.0 / 6.0).abs() < 1e-9 + || (g[2] - std::f64::consts::PI * 5.0 / 6.0).abs() < 1e-9; + let push_bary = |out: &mut Vec, + group: &str, + label: &str, + tip: &str, + zh: &str, + tag: &str, + wa: f64, + wb: f64, + wc: f64| { + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push( + fact(group, label, pt(xy[0], xy[1]), None, tip, zh).with(Shape::Point { + x: xy[0], + y: xy[1], + label: tag.into(), + }), + ); + } + } + }; + // X(301): isotomic of 2nd isodynamic; skip 60° (csc(A−π/3)→∞) + if !has_60 && !equil { + let pi3 = std::f64::consts::FRAC_PI_3; + let f = |aa: f64, saa: f64| { + let s = (aa - pi3).sin(); + if saa.abs() < 1e-14 || s.abs() < 1e-12 { + return f64::NAN; + } + (1.0 / saa) / s + }; + push_bary( + &mut out, + "第二等动力等截", + "X_{301}", + "X(301) = \\mathrm{isotomic}(X_{16});\\ \\csc A\\csc(A-\\pi/3)", + "X(301):第二等动力点的等截共轭", + "X301", + f(g[0], sa), + f(g[1], sb), + f(g[2], sc), + ); + } + // X(302): isotomic of 1st Napoleon; skip 150° (sin(A+π/6)→0 all-zero risk rare) + if !has_150 { + let pi6 = std::f64::consts::FRAC_PI_6; + let f = |aa: f64, saa: f64| { + if saa.abs() < 1e-14 { + return f64::NAN; + } + (1.0 / saa) * (aa + pi6).sin() + }; + push_bary( + &mut out, + "第一拿破仑等截", + "X_{302}", + "X(302) = \\mathrm{isotomic}(X_{17});\\ \\csc A\\sin(A+\\pi/6)", + "X(302):第一拿破仑点的等截共轭", + "X302", + f(g[0], sa), + f(g[1], sb), + f(g[2], sc), + ); + } + // X(303): isotomic of 2nd Napoleon + { + let pi6 = std::f64::consts::FRAC_PI_6; + let f = |aa: f64, saa: f64| { + if saa.abs() < 1e-14 { + return f64::NAN; + } + (1.0 / saa) * (aa - pi6).sin() + }; + push_bary( + &mut out, + "第二拿破仑等截", + "X_{303}", + "X(303) = \\mathrm{isotomic}(X_{18});\\ \\csc A\\sin(A-\\pi/6)", + "X(303):第二拿破仑点的等截共轭", + "X303", + f(g[0], sa), + f(g[1], sb), + f(g[2], sc), + ); + } + // X(304): isotomic of X(19) Clawson; bary cos A / sin² A + { + let f = |caa: f64, saa: f64| { + let s2 = saa * saa; + if s2 < 1e-28 { + return f64::NAN; + } + caa / s2 + }; + push_bary( + &mut out, + "克劳森等截", + "X_{304}", + "X(304) = \\mathrm{isotomic}(X_{19});\\ \\cos A\\csc^2 A", + "X(304):克劳森点的等截共轭", + "X304", + f(ca, sa), + f(cb, sb), + f(cc, sc), + ); + } + // X(305): isotomic of X(25); bary b³c³ cos A + { + push_bary( + &mut out, + "X25等截", + "X_{305}", + "X(305) = \\mathrm{isotomic}(X_{25});\\ b^3 c^3\\cos A", + "X(305):X(25) 的等截共轭", + "X305", + b.powi(3) * c.powi(3) * ca, + c.powi(3) * a.powi(3) * cb, + a.powi(3) * b.powi(3) * cc, + ); + } + // X(306): isotomic of X(27); bary bc(b+c) cos A + { + push_bary( + &mut out, + "X27等截", + "X_{306}", + "X(306) = \\mathrm{isotomic}(X_{27});\\ bc(b+c)\\cos A", + "X(306):X(27) 的等截共轭", + "X306", + b * c * (b + c) * ca, + c * a * (c + a) * cb, + a * b * (a + b) * cc, + ); + } + // X(307): isotomic of X(29); bary a b²c² (b+c) cos A / (b+c−a) + { + let f = |aa: f64, bb: f64, cc_: f64, caa: f64| { + let den = bb + cc_ - aa; + if den.abs() < 1e-14 { + return f64::NAN; + } + aa * bb * bb * cc_ * cc_ * (bb + cc_) * caa / den + }; + push_bary( + &mut out, + "X29等截", + "X_{307}", + "X(307) = \\mathrm{isotomic}(X_{29});\\ a b^2 c^2(b+c)\\cos A/(b+c-a)", + "X(307):X(29) 的等截共轭", + "X307", + f(a, b, c, ca), + f(b, c, a, cb), + f(c, a, b, cc), + ); + } + // X(308): isotomic of Brocard midpoint X(39); bary b²c²/(b²+c²) + { + let f = |bb: f64, cc_: f64| { + let den = bb * bb + cc_ * cc_; + if den < 1e-28 { + return f64::NAN; + } + (bb * bb * cc_ * cc_) / den + }; + push_bary( + &mut out, + "布罗卡尔中点等截", + "X_{308}", + "X(308) = \\mathrm{isotomic}(X_{39});\\ b^2 c^2/(b^2+c^2)", + "X(308):布罗卡尔中点的等截共轭", + "X308", + f(b, c), + f(c, a), + f(a, b), + ); + } + // X(309): isotomic of X(40); bary csc A / (−1−cos A+cos B+cos C) + { + let f = |saa: f64, caa: f64, cbb: f64, ccc: f64| { + if saa.abs() < 1e-14 { + return f64::NAN; + } + let den = -1.0 - caa + cbb + ccc; + if den.abs() < 1e-12 { + return f64::NAN; + } + (1.0 / saa) / den + }; + push_bary( + &mut out, + "X40等截", + "X_{309}", + "X(309) = \\mathrm{isotomic}(X_{40});\\ \\csc A/(-1-\\cos A+\\cos B+\\cos C)", + "X(309):X(40) 的等截共轭", + "X309", + f(sa, ca, cb, cc), + f(sb, cb, cc, ca), + f(sc, cc, ca, cb), + ); + } + // X(310): isotomic of X(42); bary b²c²/(b+c) + { + let f = |bb: f64, cc_: f64| { + let den = bb + cc_; + if den.abs() < 1e-14 { + return f64::NAN; + } + (bb * bb * cc_ * cc_) / den + }; + push_bary( + &mut out, + "X42等截", + "X_{310}", + "X(310) = \\mathrm{isotomic}(X_{42});\\ b^2 c^2/(b+c)", + "X(310):X(42) 的等截共轭", + "X310", + f(b, c), + f(c, a), + f(a, b), + ); + } + } + + // X(311)–X(320): continue isotomic band (ETC). + // X(311)=isotomic(X54); bary csc A cos(B−C). + // X(312)=isotomic(X57); bary bc(b+c−a). + // X(313)=isotomic(X58); bary (b+c)b²c². + // X(314)=isotomic(X65); bary bc(b+c−a)/(b+c). + // X(315)=isotomic(X66); bary b⁴+c⁴−a⁴. + // X(316)=Droussent pivot=isotomic(X67); bary b⁴+c⁴−a⁴−b²c²; skip equil (all 0). + // X(317)=isotomic(X68); bary tan A cos 2A csc² A; skip right (tan∞). + // X(318)=isotomic(X77); bary (1+sec A)/a; skip right (sec∞). + // X(319)=isotomic(X79); bary (1+2 cos A)/a. + // X(320)=isotomic(X80); bary (1−2 cos A)/a; skip equilateral (all zero). + { + let (ca, cb, cc) = (g[0].cos(), g[1].cos(), g[2].cos()); + let (sa, sb, sc) = (g[0].sin(), g[1].sin(), g[2].sin()); + let equil = (a - b).abs() < 1e-12 && (b - c).abs() < 1e-12; + let right = ca.abs() < 1e-9 || cb.abs() < 1e-9 || cc.abs() < 1e-9; + let push_bary = |out: &mut Vec, + group: &str, + label: &str, + tip: &str, + zh: &str, + tag: &str, + wa: f64, + wb: f64, + wc: f64| { + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push( + fact(group, label, pt(xy[0], xy[1]), None, tip, zh).with(Shape::Point { + x: xy[0], + y: xy[1], + label: tag.into(), + }), + ); + } + } + }; + // X(311): isotomic of Kosnita X(54); bary csc A cos(B−C) + { + let f = |saa: f64, bb: f64, cc_: f64| { + if saa.abs() < 1e-14 { + return f64::NAN; + } + (1.0 / saa) * (bb - cc_).cos() + }; + push_bary( + &mut out, + "科斯尼塔等截", + "X_{311}", + "X(311) = \\mathrm{isotomic}(X_{54});\\ \\csc A\\cos(B-C)", + "X(311):科斯尼塔点的等截共轭", + "X311", + f(sa, g[1], g[2]), + f(sb, g[2], g[0]), + f(sc, g[0], g[1]), + ); + } + // X(312): isotomic of X(57); bary bc(b+c−a) + { + push_bary( + &mut out, + "X57等截", + "X_{312}", + "X(312) = \\mathrm{isotomic}(X_{57});\\ bc(b+c-a)", + "X(312):X(57) 的等截共轭", + "X312", + b * c * (b + c - a), + c * a * (c + a - b), + a * b * (a + b - c), + ); + } + // X(313): isotomic of X(58); bary (b+c)b²c² + { + push_bary( + &mut out, + "X58等截", + "X_{313}", + "X(313) = \\mathrm{isotomic}(X_{58});\\ (b+c)b^2 c^2", + "X(313):X(58) 的等截共轭", + "X313", + (b + c) * b * b * c * c, + (c + a) * c * c * a * a, + (a + b) * a * a * b * b, + ); + } + // X(314): isotomic of X(65); bary bc(b+c−a)/(b+c) + { + let f = |aa: f64, bb: f64, cc_: f64| { + let den = bb + cc_; + if den.abs() < 1e-14 { + return f64::NAN; + } + bb * cc_ * (bb + cc_ - aa) / den + }; + push_bary( + &mut out, + "X65等截", + "X_{314}", + "X(314) = \\mathrm{isotomic}(X_{65});\\ bc(b+c-a)/(b+c)", + "X(314):X(65) 的等截共轭", + "X314", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(315): isotomic of X(66); bary b⁴+c⁴−a⁴ + { + push_bary( + &mut out, + "X66等截", + "X_{315}", + "X(315) = \\mathrm{isotomic}(X_{66});\\ b^4+c^4-a^4", + "X(315):X(66) 的等截共轭", + "X315", + b.powi(4) + c.powi(4) - a.powi(4), + c.powi(4) + a.powi(4) - b.powi(4), + a.powi(4) + b.powi(4) - c.powi(4), + ); + } + // X(316): Droussent pivot = isotomic of X(67); bary b⁴+c⁴−a⁴−b²c²; skip equil + if !equil { + push_bary( + &mut out, + "德鲁桑枢轴", + "X_{316}", + "X(316) = \\mathrm{Droussent}=\\mathrm{isotomic}(X_{67});\\ b^4+c^4-a^4-b^2c^2", + "X(316):德鲁桑枢轴(X(67) 等截)", + "X316", + b.powi(4) + c.powi(4) - a.powi(4) - b * b * c * c, + c.powi(4) + a.powi(4) - b.powi(4) - c * c * a * a, + a.powi(4) + b.powi(4) - c.powi(4) - a * a * b * b, + ); + } + // X(317): isotomic of Prasolov X(68); bary tan A cos 2A csc² A; skip right + if !right { + let f = |aa: f64, saa: f64| { + let s2 = saa * saa; + if s2 < 1e-28 { + return f64::NAN; + } + let c2a = (2.0 * aa).cos(); + aa.tan() * c2a / s2 + }; + push_bary( + &mut out, + "普拉索洛夫等截", + "X_{317}", + "X(317) = \\mathrm{isotomic}(X_{68});\\ \\tan A\\cos 2A\\csc^2 A", + "X(317):普拉索洛夫点的等截共轭", + "X317", + f(g[0], sa), + f(g[1], sb), + f(g[2], sc), + ); + } + // X(318): isotomic of X(77); bary (1+sec A)/a; skip right + if !right { + let f = |aa: f64, side: f64| { + if side.abs() < 1e-14 { + return f64::NAN; + } + let caa = aa.cos(); + if caa.abs() < 1e-12 { + return f64::NAN; + } + (1.0 + 1.0 / caa) / side + }; + push_bary( + &mut out, + "X77等截", + "X_{318}", + "X(318) = \\mathrm{isotomic}(X_{77});\\ (1+\\sec A)/a", + "X(318):X(77) 的等截共轭", + "X318", + f(g[0], a), + f(g[1], b), + f(g[2], c), + ); + } + // X(319): isotomic of X(79); bary (1+2 cos A)/a + { + let f = |caa: f64, side: f64| { + if side.abs() < 1e-14 { + return f64::NAN; + } + (1.0 + 2.0 * caa) / side + }; + push_bary( + &mut out, + "X79等截", + "X_{319}", + "X(319) = \\mathrm{isotomic}(X_{79});\\ (1+2\\cos A)/a", + "X(319):X(79) 的等截共轭", + "X319", + f(ca, a), + f(cb, b), + f(cc, c), + ); + } + // X(320): isotomic of X(80); bary (1−2 cos A)/a; skip equilateral + if !equil { + let f = |caa: f64, side: f64| { + if side.abs() < 1e-14 { + return f64::NAN; + } + (1.0 - 2.0 * caa) / side + }; + push_bary( + &mut out, + "X80等截", + "X_{320}", + "X(320) = \\mathrm{isotomic}(X_{80});\\ (1-2\\cos A)/a", + "X(320):X(80) 的等截共轭", + "X320", + f(ca, a), + f(cb, b), + f(cc, c), + ); + } + } + + // X(321)–X(330): continue isotomic / Hirst band (ETC). + // X(321)=isotomic(X81); bary bc(b+c). + // X(322)=isotomic(X84); bary (−1−cos A+cos B+cos C)csc A. + // X(323)=isotomic(X94); bary sin 3A csc A; skip equilateral (sin 3A=0). + // X(324)=isotomic(X97); bary sec A cos(B−C); skip right (sec∞). + // X(325)=X(2)-Hirst of X(69); bary b⁴+c⁴−a²b²−a²c²; skip equil (all 0). + // X(326)=isotomic(X158); bary csc A−sin A. + // X(327)=isotomic(X182); bary csc A sec(A−ω). + // X(328)=isotomic(X186); bary cos A csc 3A; skip equilateral (sin 3A=0). + // X(329)=isotomic(X189); bary −1−cos A+cos B+cos C. + // X(330)=isotomic(X192); bary 1/(ab+ac−bc). + { + let (ca, cb, cc) = (g[0].cos(), g[1].cos(), g[2].cos()); + let (sa, sb, sc) = (g[0].sin(), g[1].sin(), g[2].sin()); + let equil = (a - b).abs() < 1e-12 && (b - c).abs() < 1e-12; + let right = ca.abs() < 1e-9 || cb.abs() < 1e-9 || cc.abs() < 1e-9; + let push_bary = |out: &mut Vec, + group: &str, + label: &str, + tip: &str, + zh: &str, + tag: &str, + wa: f64, + wb: f64, + wc: f64| { + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push( + fact(group, label, pt(xy[0], xy[1]), None, tip, zh).with(Shape::Point { + x: xy[0], + y: xy[1], + label: tag.into(), + }), + ); + } + } + }; + // X(321): isotomic of X(81); bary bc(b+c) + { + push_bary( + &mut out, + "X81等截", + "X_{321}", + "X(321) = \\mathrm{isotomic}(X_{81});\\ bc(b+c)", + "X(321):X(81) 的等截共轭", + "X321", + b * c * (b + c), + c * a * (c + a), + a * b * (a + b), + ); + } + // X(322): isotomic of X(84); bary (−1−cos A+cos B+cos C)csc A + { + let f = |caa: f64, cbb: f64, ccc: f64, saa: f64| { + if saa.abs() < 1e-14 { + return f64::NAN; + } + (-1.0 - caa + cbb + ccc) / saa + }; + push_bary( + &mut out, + "X84等截", + "X_{322}", + "X(322) = \\mathrm{isotomic}(X_{84});\\ (-1-\\cos A+\\cos B+\\cos C)\\csc A", + "X(322):X(84) 的等截共轭", + "X322", + f(ca, cb, cc, sa), + f(cb, cc, ca, sb), + f(cc, ca, cb, sc), + ); + } + // X(323): isotomic of X(94); bary sin 3A csc A; skip equilateral + if !equil { + let f = |aa: f64, saa: f64| { + if saa.abs() < 1e-14 { + return f64::NAN; + } + (3.0 * aa).sin() / saa + }; + push_bary( + &mut out, + "X94等截", + "X_{323}", + "X(323) = \\mathrm{isotomic}(X_{94});\\ \\sin 3A\\csc A", + "X(323):X(94) 的等截共轭", + "X323", + f(g[0], sa), + f(g[1], sb), + f(g[2], sc), + ); + } + // X(324): isotomic of X(97); bary sec A cos(B−C); skip right + if !right { + let f = |aa: f64, bb: f64, cc_: f64| { + let caa = aa.cos(); + if caa.abs() < 1e-12 { + return f64::NAN; + } + (1.0 / caa) * (bb - cc_).cos() + }; + push_bary( + &mut out, + "X97等截", + "X_{324}", + "X(324) = \\mathrm{isotomic}(X_{97});\\ \\sec A\\cos(B-C)", + "X(324):X(97) 的等截共轭", + "X324", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + // X(325): X(2)-Hirst inverse of X(69); bary b⁴+c⁴−a²b²−a²c²; skip equil + if !equil { + let f = |aa: f64, bb: f64, cc_: f64| { + bb.powi(4) + cc_.powi(4) - aa * aa * bb * bb - aa * aa * cc_ * cc_ + }; + push_bary( + &mut out, + "X69-Hirst", + "X_{325}", + "X(325) = X(2)\\text{-Hirst of }X(69);\\ b^4+c^4-a^2b^2-a^2c^2", + "X(325):重心对 X(69) 的 Hirst 逆", + "X325", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(326): isotomic of X(158); bary csc A−sin A + { + let f = |saa: f64| { + if saa.abs() < 1e-14 { + return f64::NAN; + } + 1.0 / saa - saa + }; + push_bary( + &mut out, + "X158等截", + "X_{326}", + "X(326) = \\mathrm{isotomic}(X_{158});\\ \\csc A-\\sin A", + "X(326):X(158) 的等截共轭", + "X326", + f(sa), + f(sb), + f(sc), + ); + } + // X(327): isotomic of X(182); bary csc A sec(A−ω) + if area > 1e-12 { + let sum_sq = a * a + b * b + c * c; + let tan_w = (4.0 * area) / sum_sq; + let cos_w = 1.0 / (1.0 + tan_w * tan_w).sqrt(); + let sin_w = tan_w * cos_w; + let f = |caa: f64, saa: f64| { + if saa.abs() < 1e-14 { + return f64::NAN; + } + let caw = caa * cos_w + saa * sin_w; // cos(A−ω) + if caw.abs() < 1e-12 { + return f64::NAN; + } + (1.0 / saa) * (1.0 / caw) + }; + push_bary( + &mut out, + "X182等截", + "X_{327}", + "X(327) = \\mathrm{isotomic}(X_{182});\\ \\csc A\\sec(A-\\omega)", + "X(327):X(182) 的等截共轭", + "X327", + f(ca, sa), + f(cb, sb), + f(cc, sc), + ); + } + // X(328): isotomic of X(186); bary cos A csc 3A; skip equilateral + if !equil { + let f = |aa: f64, caa: f64| { + let s3 = (3.0 * aa).sin(); + if s3.abs() < 1e-12 { + return f64::NAN; + } + caa / s3 + }; + push_bary( + &mut out, + "X186等截", + "X_{328}", + "X(328) = \\mathrm{isotomic}(X_{186});\\ \\cos A\\csc 3A", + "X(328):X(186) 的等截共轭", + "X328", + f(g[0], ca), + f(g[1], cb), + f(g[2], cc), + ); + } + // X(329): isotomic of X(189); bary −1−cos A+cos B+cos C + { + let f = |caa: f64, cbb: f64, ccc: f64| -1.0 - caa + cbb + ccc; + push_bary( + &mut out, + "X189等截", + "X_{329}", + "X(329) = \\mathrm{isotomic}(X_{189});\\ -1-\\cos A+\\cos B+\\cos C", + "X(329):X(189) 的等截共轭", + "X329", + f(ca, cb, cc), + f(cb, cc, ca), + f(cc, ca, cb), + ); + } + // X(330): isotomic of X(192); bary 1/(ab+ac−bc) + { + let f = |aa: f64, bb: f64, cc_: f64| { + let den = aa * bb + aa * cc_ - bb * cc_; + if den.abs() < 1e-14 { + return f64::NAN; + } + 1.0 / den + }; + push_bary( + &mut out, + "X192等截", + "X_{330}", + "X(330) = \\mathrm{isotomic}(X_{192});\\ 1/(ab+ac-bc)", + "X(330):X(192) 的等截共轭", + "X330", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + } + + + // X(331)–X(340): continue isotomic / cevapoint band (ETC). + // X(331)=isotomic(X219); bary sec²(A/2) sec A; skip right (sec∞). + // X(332)=isotomic(X225); bary cot A/(cos B+cos C); skip right (cot∞). + // X(333)=isotomic(X226)=cevapoint(X8,X9); bary (b+c−a)/(b+c). + // X(334)=isotomic(X238); bary bc/(a²−bc); skip equil. + // X(335)=isotomic(X239); bary 1/(a²−bc); skip equil; X(2)-Hirst of X(291). + // X(336)=isotomic(X240); bary cot A sec(A+ω); skip right/equil. + // X(337)=isotomic(X242); bary cot A/(a²−bc); skip right/equil. + // X(338)=isotomic(X249)=cevapoint(X115,X125); bary sin²(B−C); skip equil. + // X(339)=isotomic(X250); bary cot A sin²(B−C); skip right/equil. + // X(340)=isotomic(X265); bary sec A sin 3A csc² A; skip right/equil. + { + let (ca, cb, cc) = (g[0].cos(), g[1].cos(), g[2].cos()); + let (sa, sb, sc) = (g[0].sin(), g[1].sin(), g[2].sin()); + let equil = (a - b).abs() < 1e-12 && (b - c).abs() < 1e-12; + let right = ca.abs() < 1e-9 || cb.abs() < 1e-9 || cc.abs() < 1e-9; + let push_bary = |out: &mut Vec, + group: &str, + label: &str, + tip: &str, + zh: &str, + tag: &str, + wa: f64, + wb: f64, + wc: f64| { + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push( + fact(group, label, pt(xy[0], xy[1]), None, tip, zh).with(Shape::Point { + x: xy[0], + y: xy[1], + label: tag.into(), + }), + ); + } + } + }; + // X(331): isotomic of X(219); bary sec²(A/2) sec A; skip right + if !right { + let f = |aa: f64| { + let caa = aa.cos(); + if caa.abs() < 1e-12 { + return f64::NAN; + } + let cah = (aa * 0.5).cos(); + if cah.abs() < 1e-12 { + return f64::NAN; + } + (1.0 / (cah * cah)) * (1.0 / caa) + }; + push_bary( + &mut out, + "X219等截", + "X_{331}", + "X(331) = \\mathrm{isotomic}(X_{219});\\ \\sec^2\\tfrac{A}{2}\\sec A", + "X(331):X(219) 的等截共轭", + "X331", + f(g[0]), + f(g[1]), + f(g[2]), + ); + } + // X(332): isotomic of X(225); bary cot A/(cos B+cos C); skip right + if !right { + let f = |aa: f64, cbb: f64, ccc: f64| { + let saa = aa.sin(); + let caa = aa.cos(); + if saa.abs() < 1e-14 { + return f64::NAN; + } + let den = cbb + ccc; + if den.abs() < 1e-14 { + return f64::NAN; + } + (caa / saa) / den + }; + push_bary( + &mut out, + "X225等截", + "X_{332}", + "X(332) = \\mathrm{isotomic}(X_{225});\\ \\cot A/(\\cos B+\\cos C)", + "X(332):X(225) 的等截共轭", + "X332", + f(g[0], cb, cc), + f(g[1], cc, ca), + f(g[2], ca, cb), + ); + } + // X(333): isotomic of X(226)=cevapoint(X8,X9); bary (b+c−a)/(b+c) + { + let f = |aa: f64, bb: f64, cc_: f64| { + let den = bb + cc_; + if den.abs() < 1e-14 { + return f64::NAN; + } + (bb + cc_ - aa) / den + }; + push_bary( + &mut out, + "X8X9截点", + "X_{333}", + "X(333) = \\mathrm{isotomic}(X_{226})=\\mathrm{cevapoint}(X_8,X_9);\\ (b+c-a)/(b+c)", + "X(333):X(226) 的等截共轭(奈格尔与米滕阿列夫的截点)", + "X333", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(334): isotomic of X(238); bary bc/(a²−bc); skip equil + if !equil { + let f = |aa: f64, bb: f64, cc_: f64| { + let den = aa * aa - bb * cc_; + if den.abs() < 1e-14 { + return f64::NAN; + } + bb * cc_ / den + }; + push_bary( + &mut out, + "X238等截", + "X_{334}", + "X(334) = \\mathrm{isotomic}(X_{238});\\ bc/(a^2-bc)", + "X(334):X(238) 的等截共轭", + "X334", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(335): isotomic of X(239); bary 1/(a²−bc); skip equil + if !equil { + let f = |aa: f64, bb: f64, cc_: f64| { + let den = aa * aa - bb * cc_; + if den.abs() < 1e-14 { + return f64::NAN; + } + 1.0 / den + }; + push_bary( + &mut out, + "X239等截", + "X_{335}", + "X(335) = \\mathrm{isotomic}(X_{239});\\ 1/(a^2-bc)", + "X(335):X(239) 的等截共轭(重心对 X(291) 的 Hirst 逆)", + "X335", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(336): isotomic of X(240); bary cot A sec(A+ω); skip right/equil + if !right && !equil && area > 1e-12 { + let sum_sq = a * a + b * b + c * c; + let tan_w = (4.0 * area) / sum_sq; + let cos_w = 1.0 / (1.0 + tan_w * tan_w).sqrt(); + let sin_w = tan_w * cos_w; + let f = |_aa: f64, caa: f64, saa: f64| { + if saa.abs() < 1e-14 { + return f64::NAN; + } + let caw = caa * cos_w - saa * sin_w; // cos(A+ω) + if caw.abs() < 1e-12 { + return f64::NAN; + } + (caa / saa) * (1.0 / caw) + }; + push_bary( + &mut out, + "X240等截", + "X_{336}", + "X(336) = \\mathrm{isotomic}(X_{240});\\ \\cot A\\sec(A+\\omega)", + "X(336):X(240) 的等截共轭", + "X336", + f(g[0], ca, sa), + f(g[1], cb, sb), + f(g[2], cc, sc), + ); + } + // X(337): isotomic of X(242); bary cot A/(a²−bc); skip right/equil + if !right && !equil { + let f = |aa: f64, saa: f64, caa: f64, bb: f64, cc_: f64| { + if saa.abs() < 1e-14 { + return f64::NAN; + } + let den = aa * aa - bb * cc_; + if den.abs() < 1e-14 { + return f64::NAN; + } + (caa / saa) / den + }; + push_bary( + &mut out, + "X242等截", + "X_{337}", + "X(337) = \\mathrm{isotomic}(X_{242});\\ \\cot A/(a^2-bc)", + "X(337):X(242) 的等截共轭", + "X337", + f(a, sa, ca, b, c), + f(b, sb, cb, c, a), + f(c, sc, cc, a, b), + ); + } + // X(338): isotomic of X(249)=cevapoint(X115,X125); bary sin²(B−C); skip equil + if !equil { + let f = |bb: f64, cc_: f64| (bb - cc_).sin().powi(2); + push_bary( + &mut out, + "X115X125截点", + "X_{338}", + "X(338) = \\mathrm{isotomic}(X_{249})=\\mathrm{cevapoint}(X_{115},X_{125});\\ \\sin^2(B-C)", + "X(338):X(249) 的等截共轭(X(115) 与 X(125) 的截点)", + "X338", + f(g[1], g[2]), + f(g[2], g[0]), + f(g[0], g[1]), + ); + } + // X(339): isotomic of X(250); bary cot A sin²(B−C); skip right/equil + if !right && !equil { + let f = |_aa: f64, saa: f64, caa: f64, bb: f64, cc_: f64| { + if saa.abs() < 1e-14 { + return f64::NAN; + } + (caa / saa) * (bb - cc_).sin().powi(2) + }; + push_bary( + &mut out, + "X250等截", + "X_{339}", + "X(339) = \\mathrm{isotomic}(X_{250});\\ \\cot A\\sin^2(B-C)", + "X(339):X(250) 的等截共轭", + "X339", + f(g[0], sa, ca, g[1], g[2]), + f(g[1], sb, cb, g[2], g[0]), + f(g[2], sc, cc, g[0], g[1]), + ); + } + // X(340): isotomic of X(265); bary sec A sin 3A csc² A; skip right/equil + if !right && !equil { + let f = |aa: f64, caa: f64, saa: f64| { + if caa.abs() < 1e-12 || saa.abs() < 1e-14 { + return f64::NAN; + } + let s3 = (3.0 * aa).sin(); + (1.0 / caa) * s3 / (saa * saa) + }; + push_bary( + &mut out, + "X265等截", + "X_{340}", + "X(340) = \\mathrm{isotomic}(X_{265});\\ \\sec A\\sin 3A\\csc^2 A", + "X(340):X(265) 的等截共轭", + "X340", + f(g[0], ca, sa), + f(g[1], cb, sb), + f(g[2], cc, sc), + ); + } + } + + + + + + + + + // X(341)–X(350): continue isotomic / Hirst / cevapoint band (ETC). + // X(341)=isotomic(X269); bary bc(b+c−a)². + // X(342)=isotomic(X271)=cevapoint(X208,X223); bary (sec A tan A/2)(1+cos A−cos B−cos C); skip right. + // X(343)=isotomic(X275)=crosspoint(X69,X76); bary cos A cos(B−C). + // X(344)=isotomic(X277); bary a²+b²+c²−2ab−2ac [=sin A·csc²(A/2)(cos⁴(B/2)+cos⁴(C/2)−cos⁴(A/2))]. + // X(345)=isotomic(X278); bary 1/(1−sec A); skip right. + // X(346)=isotomic(X279)=crosspoint(X312,X341); bary (b+c−a)². + // X(347)=isotomic(X280)=cevapoint(X40,X223); bary a(−1−cos A+cos B+cos C)sec²(A/2). + // X(348)=isotomic(X281)=cevapoint(X2,X347); bary 1/(1+sec A); skip right. + // X(349)=isotomic(X284)=cevapoint(X226,X307); bary (cos B+cos C)csc² A. + // X(350)=isotomic(X291)=X(2)-Hirst of X(75); bary bc(a²−bc); skip equil. + { + let (ca, cb, cc) = (g[0].cos(), g[1].cos(), g[2].cos()); + let (sa, sb, sc) = (g[0].sin(), g[1].sin(), g[2].sin()); + let equil = (a - b).abs() < 1e-12 && (b - c).abs() < 1e-12; + let right = ca.abs() < 1e-9 || cb.abs() < 1e-9 || cc.abs() < 1e-9; + let push_bary = |out: &mut Vec, + group: &str, + label: &str, + tip: &str, + zh: &str, + tag: &str, + wa: f64, + wb: f64, + wc: f64| { + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push( + fact(group, label, pt(xy[0], xy[1]), None, tip, zh).with(Shape::Point { + x: xy[0], + y: xy[1], + label: tag.into(), + }), + ); + } + } + }; + // X(341): isotomic of X(269); bary bc(b+c−a)² + { + let f = |aa: f64, bb: f64, cc_: f64| bb * cc_ * (bb + cc_ - aa).powi(2); + push_bary( + &mut out, + "X269等截", + "X_{341}", + "X(341) = \\mathrm{isotomic}(X_{269});\\ bc(b+c-a)^2", + "X(341):X(269) 的等截共轭", + "X341", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(342): isotomic of X(271)=cevapoint(X208,X223); skip right (sec A → ∞) + if !right { + let f = |aa: f64, caa: f64, cbb: f64, ccc: f64| { + if caa.abs() < 1e-12 { + return f64::NAN; + } + let th = (aa * 0.5).tan(); + (1.0 / caa) * th * (1.0 + caa - cbb - ccc) + }; + push_bary( + &mut out, + "X208X223截点", + "X_{342}", + "X(342) = \\mathrm{isotomic}(X_{271})=\\mathrm{cevapoint}(X_{208},X_{223});\\ (\\sec A\\tan\\tfrac{A}{2})(1+\\cos A-\\cdots)", + "X(342):X(271) 的等截共轭(X(208) 与 X(223) 的截点)", + "X342", + f(g[0], ca, cb, cc), + f(g[1], cb, cc, ca), + f(g[2], cc, ca, cb), + ); + } + // X(343): isotomic of X(275)=crosspoint(X69,X76); bary cos A cos(B−C) + { + let f = |caa: f64, bb: f64, cc_: f64| caa * (bb - cc_).cos(); + push_bary( + &mut out, + "X69X76交点", + "X_{343}", + "X(343) = \\mathrm{isotomic}(X_{275})=\\mathrm{crosspoint}(X_{69},X_{76});\\ \\cos A\\cos(B-C)", + "X(343):X(275) 的等截共轭(X(69) 与 X(76) 的交点)", + "X343", + f(ca, g[1], g[2]), + f(cb, g[2], g[0]), + f(cc, g[0], g[1]), + ); + } + // X(344): isotomic of X(277); bary a²+b²+c²−2ab−2ac + { + let f = |aa: f64, bb: f64, cc_: f64| { + aa * aa + bb * bb + cc_ * cc_ - 2.0 * aa * bb - 2.0 * aa * cc_ + }; + push_bary( + &mut out, + "X277等截", + "X_{344}", + "X(344) = \\mathrm{isotomic}(X_{277});\\ a^2+b^2+c^2-2ab-2ac", + "X(344):X(277) 的等截共轭", + "X344", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(345): isotomic of X(278); bary 1/(1−sec A); skip right + if !right { + let f = |caa: f64| { + if caa.abs() < 1e-12 { + return f64::NAN; + } + let den = 1.0 - 1.0 / caa; + if den.abs() < 1e-14 { + return f64::NAN; + } + 1.0 / den + }; + push_bary( + &mut out, + "X278等截", + "X_{345}", + "X(345) = \\mathrm{isotomic}(X_{278});\\ 1/(1-\\sec A)", + "X(345):X(278) 的等截共轭", + "X345", + f(ca), + f(cb), + f(cc), + ); + } + // X(346): isotomic of X(279)=crosspoint(X312,X341); bary (b+c−a)² + { + let f = |aa: f64, bb: f64, cc_: f64| (bb + cc_ - aa).powi(2); + push_bary( + &mut out, + "X312X341交点", + "X_{346}", + "X(346) = \\mathrm{isotomic}(X_{279})=\\mathrm{crosspoint}(X_{312},X_{341});\\ (b+c-a)^2", + "X(346):X(279) 的等截共轭(X(312) 与 X(341) 的交点)", + "X346", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(347): isotomic of X(280)=cevapoint(X40,X223) + { + let f = |side: f64, aa: f64, caa: f64, cbb: f64, ccc: f64| { + let cah = (aa * 0.5).cos(); + if cah.abs() < 1e-12 { + return f64::NAN; + } + side * (-1.0 - caa + cbb + ccc) / (cah * cah) + }; + push_bary( + &mut out, + "X40X223截点", + "X_{347}", + "X(347) = \\mathrm{isotomic}(X_{280})=\\mathrm{cevapoint}(X_{40},X_{223});\\ a(-1-\\cos A+\\cdots)\\sec^2\\tfrac{A}{2}", + "X(347):X(280) 的等截共轭(X(40) 与 X(223) 的截点)", + "X347", + f(a, g[0], ca, cb, cc), + f(b, g[1], cb, cc, ca), + f(c, g[2], cc, ca, cb), + ); + } + // X(348): isotomic of X(281)=cevapoint(G,X347); bary 1/(1+sec A); skip right + if !right { + let f = |caa: f64| { + if caa.abs() < 1e-12 { + return f64::NAN; + } + let den = 1.0 + 1.0 / caa; + if den.abs() < 1e-14 { + return f64::NAN; + } + 1.0 / den + }; + push_bary( + &mut out, + "重心X347截点", + "X_{348}", + "X(348) = \\mathrm{isotomic}(X_{281})=\\mathrm{cevapoint}(X_2,X_{347});\\ 1/(1+\\sec A)", + "X(348):X(281) 的等截共轭(重心与 X(347) 的截点)", + "X348", + f(ca), + f(cb), + f(cc), + ); + } + // X(349): isotomic of X(284)=cevapoint(X226,X307); bary (cos B+cos C)csc² A + { + let f = |saa: f64, cbb: f64, ccc: f64| { + if saa.abs() < 1e-14 { + return f64::NAN; + } + (cbb + ccc) / (saa * saa) + }; + push_bary( + &mut out, + "X226X307截点", + "X_{349}", + "X(349) = \\mathrm{isotomic}(X_{284})=\\mathrm{cevapoint}(X_{226},X_{307});\\ (\\cos B+\\cos C)\\csc^2 A", + "X(349):X(284) 的等截共轭(X(226) 与 X(307) 的截点)", + "X349", + f(sa, cb, cc), + f(sb, cc, ca), + f(sc, ca, cb), + ); + } + // X(350): isotomic of X(291)=X(2)-Hirst of X(75); bary bc(a²−bc); skip equil + if !equil { + let f = |aa: f64, bb: f64, cc_: f64| { + let d = aa * aa - bb * cc_; + if d.abs() < 1e-14 { + return f64::NAN; + } + bb * cc_ * d + }; + push_bary( + &mut out, + "X75重心Hirst", + "X_{350}", + "X(350) = \\mathrm{isotomic}(X_{291})=X(2)\\text{-Hirst of }X(75);\\ bc(a^2-bc)", + "X(350):X(291) 的等截共轭(重心对摩西点 X(75) 的 Hirst 逆)", + "X350", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + } + + // X(351)–X(360): Parry/Weill/Fuhrmann/Morley/Hofstadter band (ETC). + // X(351)=Parry-circle center=crosspoint(X110,X111); bary a²(b²−c²)(b²+c²−2a²); skip equil (and den=0 isos). + // X(352)=circumcircle-inverse of X(353); bary a²(−a⁴−b⁴−c⁴−5b²c²+4a²b²+4a²c²); skip equil. + // X(353)=Brocard-circle-inverse of X(111); bary a²(4a⁴−2b⁴−2c⁴−b²c²−4a²b²−4a²c²). + // X(354)=Weill=centroid(intouch)=crosspoint(X1,X7); bary a[(b−c)²−ab−ac]. + // X(355)=Fuhrmann=midpoint(X4,X8); bary a(a cos A−(b+c)cos(B−C)). + // X(356)=Morley center; bary sin A(cos A/3+2 cos B/3 cos C/3). + // X(357)=1st Morley–Taylor–Marr; bary sin A sec(A/3); isogonal of X(358). + // X(358)=2nd Morley–Taylor–Marr; bary sin A cos(A/3). + // X(359)/X(360)=Hofstadter one/zero: dual-labeled earlier as Hf_1/Hf_0 (a²/A and A:B:C). + { + let (ca, cb, cc) = (g[0].cos(), g[1].cos(), g[2].cos()); + let (sa, sb, sc) = (g[0].sin(), g[1].sin(), g[2].sin()); + let equil = (a - b).abs() < 1e-12 && (b - c).abs() < 1e-12; + let push_bary = |out: &mut Vec, + group: &str, + label: &str, + tip: &str, + zh: &str, + tag: &str, + wa: f64, + wb: f64, + wc: f64| { + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push( + fact(group, label, pt(xy[0], xy[1]), None, tip, zh).with(Shape::Point { + x: xy[0], + y: xy[1], + label: tag.into(), + }), + ); + } + } + }; + // X(351): Parry circle center; skip equil (weights vanish) + if !equil { + let f = |aa: f64, bb: f64, cc_: f64| { + aa * aa * (bb * bb - cc_ * cc_) * (bb * bb + cc_ * cc_ - 2.0 * aa * aa) + }; + push_bary( + &mut out, + "帕里圆心", + "X_{351}", + "X(351) = \\mathrm{crosspoint}(X_{110},X_{111});\\ a^2(b^2-c^2)(b^2+c^2-2a^2)", + "X(351):帕里圆圆心(X(110) 与 X(111) 的交点)", + "X351", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(352): inverse-in-circumcircle of X(353); skip equil + if !equil { + let f = |aa: f64, bb: f64, cc_: f64| { + let aa2 = aa * aa; + aa2 * (-aa.powi(4) + - bb.powi(4) + - cc_.powi(4) + - 5.0 * bb * bb * cc_ * cc_ + + 4.0 * aa2 * bb * bb + + 4.0 * aa2 * cc_ * cc_) + }; + push_bary( + &mut out, + "X353外接逆", + "X_{352}", + "X(352) = \\mathrm{circumcircle\\text{-}inverse}(X_{353});\\ a^2(-a^4-\\cdots)", + "X(352):X(353) 关于外接圆的反演(帕里圆上)", + "X352", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(353): inverse-in-Brocard-circle of X(111) + { + let f = |aa: f64, bb: f64, cc_: f64| { + let aa2 = aa * aa; + aa2 * (4.0 * aa.powi(4) + - 2.0 * bb.powi(4) + - 2.0 * cc_.powi(4) + - bb * bb * cc_ * cc_ + - 4.0 * aa2 * bb * bb + - 4.0 * aa2 * cc_ * cc_) + }; + push_bary( + &mut out, + "X111Brocard逆", + "X_{353}", + "X(353) = \\mathrm{Brocard\\text{-}circle\\text{-}inverse}(X_{111});\\ a^2(4a^4-\\cdots)", + "X(353):帕里点关于 Brocard 圆的反演(帕里圆上)", + "X353", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(354): Weill = centroid of intouch = crosspoint(I, Ge) + { + let f = |aa: f64, bb: f64, cc_: f64| aa * ((bb - cc_).powi(2) - aa * bb - aa * cc_); + push_bary( + &mut out, + "Weill点", + "X_{354}", + "X(354) = \\mathrm{crosspoint}(X_1,X_7);\\ a[(b-c)^2-ab-ac]", + "X(354):Weill 点(内切三角形重心;内心与热尔贡点交点)", + "X354", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(355): Fuhrmann = midpoint(H, Na) + { + let f = |aa: f64, bb: f64, cc_: f64, caa: f64, ang_b: f64, ang_c: f64| { + aa * (aa * caa - (bb + cc_) * (ang_b - ang_c).cos()) + }; + push_bary( + &mut out, + "Fuhrmann心", + "X_{355}", + "X(355) = \\mathrm{midpoint}(X_4,X_8);\\ a(a\\cos A-(b+c)\\cos(B-C))", + "X(355):Fuhrmann 圆心(垂心与奈格尔点的中点)", + "X355", + f(a, b, c, ca, g[1], g[2]), + f(b, c, a, cb, g[2], g[0]), + f(c, a, b, cc, g[0], g[1]), + ); + } + // X(356): Morley center (centroid of Morley equilateral) + { + let f = |saa: f64, aa: f64, bb: f64, cc_: f64| { + saa * ((aa / 3.0).cos() + 2.0 * (bb / 3.0).cos() * (cc_ / 3.0).cos()) + }; + push_bary( + &mut out, + "莫利中心", + "X_{356}", + "X(356) = \\mathrm{centroid}(\\mathrm{Morley});\\ \\sin A(\\cos\\tfrac{A}{3}+2\\cos\\tfrac{B}{3}\\cos\\tfrac{C}{3})", + "X(356):莫利三角形重心", + "X356", + f(sa, g[0], g[1], g[2]), + f(sb, g[1], g[2], g[0]), + f(sc, g[2], g[0], g[1]), + ); + } + // X(357): 1st Morley–Taylor–Marr; bary sin A sec(A/3) + { + let f = |saa: f64, aa: f64| { + let c3 = (aa / 3.0).cos(); + if c3.abs() < 1e-12 { + return f64::NAN; + } + saa / c3 + }; + push_bary( + &mut out, + "莫利透视1", + "X_{357}", + "X(357) = \\mathrm{perspector}(\\mathrm{Morley});\\ \\sin A\\sec\\tfrac{A}{3}", + "X(357):第一 Morley–Taylor–Marr 点(莫利三角形透视中心)", + "X357", + f(sa, g[0]), + f(sb, g[1]), + f(sc, g[2]), + ); + } + // X(358): 2nd Morley–Taylor–Marr; bary sin A cos(A/3); isogonal of X(357) + { + let f = |saa: f64, aa: f64| saa * (aa / 3.0).cos(); + push_bary( + &mut out, + "莫利透视2", + "X_{358}", + "X(358) = \\mathrm{isogonal}(X_{357});\\ \\sin A\\cos\\tfrac{A}{3}", + "X(358):第二 Morley–Taylor–Marr 点(X(357) 的等角共轭)", + "X358", + f(sa, g[0]), + f(sb, g[1]), + f(sc, g[2]), + ); + } + // X(359)/X(360) dual-labeled in the Hofstadter block above (Hf_1 / Hf_0). + } + + // X(361)–X(370): Yff isoscelizer / sqrt-side band (ETC). + // X(361)=X(266)-Ceva conjugate of X(1); bary sin A(csc B/2+csc C/2−csc A/2). + // X(362)=congruent-circumcircles isoscelizer; bary a(b cos B/2+c cos C/2−a cos A/2). + // X(363)=equal-perimeters isoscelizer; bary a(b/(1+sin B/2)+c/(1+sin C/2)−a/(1+sin A/2)). + // X(364)=Wabash=equal-areas isoscelizer; bary a(√b+√c−√a); =X(366)-Ceva conjugate of X(1). + // X(365)=trilinear square-roots; bary a^{3/2}; isogonal of X(366); crosssum(X1,X364). + // X(366)=isogonal(X365); bary √a; cevapoint(X1,X364). + // X(367)=crosspoint(X1,X366); bary a(√b+√c); =crosssum(X1,X365). + // X(368) equi-Brocard / X(369) 1st trisected-perimeter / X(370) equilateral-cevian: + // no closed barycentrics in ETC (certified ETC also omits); skip this round. + { + let (sa, sb, sc) = (g[0].sin(), g[1].sin(), g[2].sin()); + let push_bary = |out: &mut Vec, + group: &str, + label: &str, + tip: &str, + zh: &str, + tag: &str, + wa: f64, + wb: f64, + wc: f64| { + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push( + fact(group, label, pt(xy[0], xy[1]), None, tip, zh).with(Shape::Point { + x: xy[0], + y: xy[1], + label: tag.into(), + }), + ); + } + } + }; + // X(361): X(266)-Ceva conjugate of I; isoscelizer au=bv=cw + { + let f = |saa: f64, aa: f64, bb: f64, cc_: f64| { + let ha = (aa / 2.0).sin(); + let hb = (bb / 2.0).sin(); + let hc = (cc_ / 2.0).sin(); + if ha.abs() < 1e-12 || hb.abs() < 1e-12 || hc.abs() < 1e-12 { + return f64::NAN; + } + saa * (1.0 / hb + 1.0 / hc - 1.0 / ha) + }; + push_bary( + &mut out, + "等腰化点", + "X_{361}", + "X(361) = X(266)\\text{-Ceva conj. of }X_1;\\ \\sin A(\\csc\\tfrac{B}{2}+\\csc\\tfrac{C}{2}-\\csc\\tfrac{A}{2})", + "X(361):X(266) 对内心的 Ceva 共轭(等腰化点)", + "X361", + f(sa, g[0], g[1], g[2]), + f(sb, g[1], g[2], g[0]), + f(sc, g[2], g[0], g[1]), + ); + } + // X(362): congruent circumcircles isoscelizer + { + let f = |aa: f64, bb: f64, cc_: f64, ang_a: f64, ang_b: f64, ang_c: f64| { + aa * (bb * (ang_b / 2.0).cos() + cc_ * (ang_c / 2.0).cos() - aa * (ang_a / 2.0).cos()) + }; + push_bary( + &mut out, + "等外接圆等腰化", + "X_{362}", + "X(362) = \\mathrm{isoscelizer};\\ a(b\\cos\\tfrac{B}{2}+c\\cos\\tfrac{C}{2}-a\\cos\\tfrac{A}{2})", + "X(362):等外接圆等腰化点", + "X362", + f(a, b, c, g[0], g[1], g[2]), + f(b, c, a, g[1], g[2], g[0]), + f(c, a, b, g[2], g[0], g[1]), + ); + } + // X(363): equal-perimeters isoscelizer + { + let f = |aa: f64, bb: f64, cc_: f64, ang_a: f64, ang_b: f64, ang_c: f64| { + aa * (bb / (1.0 + (ang_b / 2.0).sin()) + + cc_ / (1.0 + (ang_c / 2.0).sin()) + - aa / (1.0 + (ang_a / 2.0).sin())) + }; + push_bary( + &mut out, + "等周长等腰化", + "X_{363}", + "X(363) = \\mathrm{isoscelizer};\\ a\\big(\\tfrac{b}{1+\\sin B/2}+\\tfrac{c}{1+\\sin C/2}-\\tfrac{a}{1+\\sin A/2}\\big)", + "X(363):等周长等腰化点", + "X363", + f(a, b, c, g[0], g[1], g[2]), + f(b, c, a, g[1], g[2], g[0]), + f(c, a, b, g[2], g[0], g[1]), + ); + } + // X(364): Wabash = equal-areas isoscelizer + { + let f = |aa: f64, bb: f64, cc_: f64| aa * (bb.sqrt() + cc_.sqrt() - aa.sqrt()); + push_bary( + &mut out, + "Wabash点", + "X_{364}", + "X(364) = X(366)\\text{-Ceva conj. of }X_1;\\ a(\\sqrt{b}+\\sqrt{c}-\\sqrt{a})", + "X(364):Wabash 点(等面积等腰化点)", + "X364", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(365): trilinear square-roots; bary a^{3/2}; isogonal of X(366) + { + let f = |aa: f64| aa * aa.sqrt(); + push_bary( + &mut out, + "边根等角", + "X_{365}", + "X(365) = \\mathrm{isogonal}(X_{366})=\\mathrm{crosssum}(X_1,X_{364});\\ a^{3/2}", + "X(365):边长平方根三线坐标点(X(366) 的等角共轭)", + "X365", + f(a), + f(b), + f(c), + ); + } + // X(366): isogonal of X(365); bary √a; cevapoint(I, X364) + { + let f = |aa: f64| aa.sqrt(); + push_bary( + &mut out, + "边根点", + "X_{366}", + "X(366) = \\mathrm{isogonal}(X_{365})=\\mathrm{cevapoint}(X_1,X_{364});\\ \\sqrt{a}", + "X(366):边长平方根重心坐标点(X(365) 的等角共轭)", + "X366", + f(a), + f(b), + f(c), + ); + } + // X(367): crosspoint(I, X366); bary a(√b+√c) + { + let f = |aa: f64, bb: f64, cc_: f64| aa * (bb.sqrt() + cc_.sqrt()); + push_bary( + &mut out, + "X1X366交点", + "X_{367}", + "X(367) = \\mathrm{crosspoint}(X_1,X_{366})=\\mathrm{crosssum}(X_1,X_{365});\\ a(\\sqrt{b}+\\sqrt{c})", + "X(367):内心与 X(366) 的交点", + "X367", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(368)/X(369)/X(370): deferred — no closed barycentrics in ETC. + } + + // X(371)–X(380): Kenmotu mates / pedal-centroids / Euler intercepts (ETC). + // X(371)=Kenmotu: dual-labeled earlier as Km; bary a cos(A−π/4)=isogonal(X485). + // X(372)={X3,X6}-harmonic of X371; bary a cos(A+π/4)=isogonal(X486); Brocard-inverse of X371. + // X(373)=centroid of pedal(G); bary 2abc+ca²cos C+ba²cos B (= a²(b⁴+c⁴−a²b²−a²c²−6b²c²)). + // X(374)=centroid of pedal(X9); bary a(2b+2c−3a+(c+a)cos C+(b+a)cos B). + // X(375)=centroid of pedal(X10); bary a(2bc(b+c)+ca(c+a)cos C+ab(a+b)cos B); =midpoint(X51,X210). + // X(376)=centroid of antipedal(G)=reflection(G in O)=midpoint(G,X20); bary 5sin2A−sin2B−sin2C. + // X(377)=Euler intercept of X7X8; bary b⁴+c⁴−a⁴−2b²c²−2abc(a+b+c). + // X(378)=reflection(X22 in O); bary tan A+sin2A; skip right (tan undefined). + // X(379)=Euler intercept of X6X7; bary a⁵+(b+c)(bca²−(bc+ca+ab)(b−c)²). + // X(380)=X1X19 ∩ X9X55; bary a(b+c−a)[3a³+(b+c)(3a²+(b−c)²+a(b+c))]. + { + let (ca, cb, cc) = (g[0].cos(), g[1].cos(), g[2].cos()); + let right = ca.abs() < 1e-12 || cb.abs() < 1e-12 || cc.abs() < 1e-12; + let pi4 = std::f64::consts::FRAC_PI_4; + let push_bary = |out: &mut Vec, + group: &str, + label: &str, + tip: &str, + zh: &str, + tag: &str, + wa: f64, + wb: f64, + wc: f64| { + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push( + fact(group, label, pt(xy[0], xy[1]), None, tip, zh).with(Shape::Point { + x: xy[0], + y: xy[1], + label: tag.into(), + }), + ); + } + } + }; + // X(371): dual-labeled in the Kenmotu block above (Km / X_{371}). + // X(372): {X3,X6}-harmonic conjugate of Kenmotu + { + let f = |aa: f64, ang: f64| aa * (ang + pi4).cos(); + push_bary( + &mut out, + "肯莫图谐共轭", + "X_{372}", + "X(372) = \\{X_3,X_6\\}\\text{-harmonic of }X_{371};\\ a\\cos(A+\\pi/4)", + "X(372):{X(3),X(6)} 关于肯莫图点的调和共轭(等角共轭于 X(486))", + "X372", + f(a, g[0]), + f(b, g[1]), + f(c, g[2]), + ); + } + // X(373): centroid of pedal triangle of G + { + let f = |aa: f64, bb: f64, cc_: f64, ang_b: f64, ang_c: f64| { + 2.0 * aa * bb * cc_ + cc_ * aa * aa * ang_c.cos() + bb * aa * aa * ang_b.cos() + }; + push_bary( + &mut out, + "重心垂足重心", + "X_{373}", + "X(373) = \\mathrm{centroid}(\\mathrm{pedal}(G));\\ 2abc+ca^2\\cos C+ba^2\\cos B", + "X(373):重心的垂足三角形的重心", + "X373", + f(a, b, c, g[1], g[2]), + f(b, c, a, g[2], g[0]), + f(c, a, b, g[0], g[1]), + ); + } + // X(374): centroid of pedal triangle of Mittenpunkt X(9) + { + let f = |aa: f64, bb: f64, cc_: f64, ang_b: f64, ang_c: f64| { + aa * (2.0 * bb + 2.0 * cc_ - 3.0 * aa + + (cc_ + aa) * ang_c.cos() + + (bb + aa) * ang_b.cos()) + }; + push_bary( + &mut out, + "X9垂足重心", + "X_{374}", + "X(374) = \\mathrm{centroid}(\\mathrm{pedal}(X_9));\\ a(2b+2c-3a+\\cdots)", + "X(374):米滕蓬特 X(9) 的垂足三角形的重心", + "X374", + f(a, b, c, g[1], g[2]), + f(b, c, a, g[2], g[0]), + f(c, a, b, g[0], g[1]), + ); + } + // X(375): centroid of pedal triangle of Spieker X(10); = midpoint(X51,X210) + { + let f = |aa: f64, bb: f64, cc_: f64, ang_b: f64, ang_c: f64| { + aa * (2.0 * bb * cc_ * (bb + cc_) + + cc_ * aa * (cc_ + aa) * ang_c.cos() + + aa * bb * (aa + bb) * ang_b.cos()) + }; + push_bary( + &mut out, + "X10垂足重心", + "X_{375}", + "X(375) = \\mathrm{centroid}(\\mathrm{pedal}(X_{10}))=\\mathrm{midpoint}(X_{51},X_{210})", + "X(375):斯皮克点 X(10) 的垂足三角形的重心", + "X375", + f(a, b, c, g[1], g[2]), + f(b, c, a, g[2], g[0]), + f(c, a, b, g[0], g[1]), + ); + } + // X(376): reflection of G in O (= midpoint(G, X20)) + { + let f = |ang_a: f64, ang_b: f64, ang_c: f64| { + 5.0 * (2.0 * ang_a).sin() - (2.0 * ang_b).sin() - (2.0 * ang_c).sin() + }; + push_bary( + &mut out, + "重心反足重心", + "X_{376}", + "X(376) = 2O-G = \\mathrm{midpoint}(G,X_{20});\\ 5\\sin 2A-\\sin 2B-\\sin 2C", + "X(376):重心关于外心的反射(重心的反足三角形的重心)", + "X376", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + // X(377): Euler-line intercept of X(7)X(8) + { + let f = |aa: f64, bb: f64, cc_: f64| { + bb.powi(4) + cc_.powi(4) - aa.powi(4) - 2.0 * bb * bb * cc_ * cc_ + - 2.0 * aa * bb * cc_ * (aa + bb + cc_) + }; + push_bary( + &mut out, + "X7X8欧拉截", + "X_{377}", + "X(377) = X_7X_8 \\cap \\mathrm{Euler};\\ b^4+c^4-a^4-2b^2c^2-2abc(a+b+c)", + "X(377):X(7)X(8) 与欧拉线的交点", + "X377", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(378): reflection of Exeter X(22) in O; skip right + if !right { + let f = |ang: f64| ang.tan() + (2.0 * ang).sin(); + push_bary( + &mut out, + "X22外心反射", + "X_{378}", + "X(378) = 2O-X_{22};\\ \\tan A+\\sin 2A", + "X(378):埃克塞特点关于外心的反射(跳过直角)", + "X378", + f(g[0]), + f(g[1]), + f(g[2]), + ); + } + // X(379): Euler-line intercept of X(6)X(7) + { + let f = |aa: f64, bb: f64, cc_: f64| { + aa.powi(5) + + (bb + cc_) + * (bb * cc_ * aa * aa + - (bb * cc_ + cc_ * aa + aa * bb) * (bb - cc_).powi(2)) + }; + push_bary( + &mut out, + "X6X7欧拉截", + "X_{379}", + "X(379) = X_6X_7 \\cap \\mathrm{Euler};\\ a^5+(b+c)(bca^2-(bc+ca+ab)(b-c)^2)", + "X(379):X(6)X(7) 与欧拉线的交点", + "X379", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(380): X(1)X(19) ∩ X(9)X(55) + { + let f = |aa: f64, bb: f64, cc_: f64| { + let s = bb + cc_; + aa * (s - aa) + * (3.0 * aa.powi(3) + s * (3.0 * aa * aa + (bb - cc_).powi(2) + aa * s)) + }; + push_bary( + &mut out, + "X1X19交X9X55", + "X_{380}", + "X(380) = X_1X_{19}\\cap X_9X_{55};\\ a(b+c-a)[3a^3+(b+c)(3a^2+(b-c)^2+a(b+c))]", + "X(380):X(1)X(19) 与 X(9)X(55) 的交点", + "X380", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + } + + + // X(381)–X(390): orthocentroidal / Euler mates / Conway / Hirst / Taylor (ETC). + // X(381)=midpoint(G,H)=center of orthocentroidal circle; bary a(cos A+4 cos B cos C); =complement(X376). + // X(382)=reflection(O in H)=2H−O; bary a(cos A−4 cos B cos C). + // X(383)=Euler ∩ X(14)X(98); Brocard form with csc(B−C); skip isos (den0); equil→G. + // X(384)=Euler ∩ X(32)X(76) (Conway); bary a⁴+b²c². + // X(385)=X(2)-Hirst of X(6); bary a⁴−b²c²; skip equil (all zero). + // X(386)=Brocard-inverse of X(58)=ext. similitude(circum, Apollonius); bary a²(b²+c²+bc+ca+ab). + // X(387)=X(1)X(2) ∩ X(4)X(6); bary −a⁴+2a²(a+b+c)²+(b²−c²)². + // X(388)=X(1)X(4) ∩ X(7)X(8); bary a(1+cos B cos C) = [a²+(b+c)²]/(b+c−a). + // X(389)=Taylor-circle center; bary a[cos A−cos 2A cos(B−C)]; =midpoint(O,X52)=midpoint(H,X185). + // X(390)=reflection(Ge in I)=2I−X(7); bary (b+c−a)[3a²+(b−c)²]. + { + let (sa, sb, sc) = (g[0].sin(), g[1].sin(), g[2].sin()); + let equil = (a - b).abs() < 1e-9 && (b - c).abs() < 1e-9; + let isos = (a - b).abs() < 1e-9 || (b - c).abs() < 1e-9 || (c - a).abs() < 1e-9; + let push_bary = |out: &mut Vec, + group: &str, + label: &str, + tip: &str, + zh: &str, + tag: &str, + wa: f64, + wb: f64, + wc: f64| { + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push( + fact(group, label, pt(xy[0], xy[1]), None, tip, zh).with(Shape::Point { + x: xy[0], + y: xy[1], + label: tag.into(), + }), + ); + } + } + }; + // X(381): midpoint of G and H (orthocentroidal-circle center) + { + let f = |aa: f64, ang_a: f64, ang_b: f64, ang_c: f64| { + aa * (ang_a.cos() + 4.0 * ang_b.cos() * ang_c.cos()) + }; + push_bary( + &mut out, + "垂心重心圆圆心", + "X_{381}", + "X(381) = \\mathrm{mid}(G,H);\\ a(\\cos A+4\\cos B\\cos C)", + "X(381):重心与垂心的中点(垂心–重心圆圆心;X(376) 的补点)", + "X381", + f(a, g[0], g[1], g[2]), + f(b, g[1], g[2], g[0]), + f(c, g[2], g[0], g[1]), + ); + } + // X(382): reflection of O in H + { + let f = |aa: f64, ang_a: f64, ang_b: f64, ang_c: f64| { + aa * (ang_a.cos() - 4.0 * ang_b.cos() * ang_c.cos()) + }; + push_bary( + &mut out, + "外心垂心反射", + "X_{382}", + "X(382) = 2H-O;\\ a(\\cos A-4\\cos B\\cos C)", + "X(382):外心关于垂心的反射", + "X382", + f(a, g[0], g[1], g[2]), + f(b, g[1], g[2], g[0]), + f(c, g[2], g[0], g[1]), + ); + } + // X(383): Euler ∩ X(14)X(98); Brocard form; skip isos; equil→G + if equil { + push_bary( + &mut out, + "X14X98欧拉截", + "X_{383}", + "X(383) = X_{14}X_{98}\\cap\\mathrm{Euler};\\ \\mathrm{equil}\\to G", + "X(383):X(14)X(98) 与欧拉线的交点(等边退化为重心)", + "X383", + 1.0, + 1.0, + 1.0, + ); + } else if !isos && area > 1e-12 { + let cot_w = (a * a + b * b + c * c) / (4.0 * area); + let tan_w = 1.0 / cot_w; + let omega = tan_w.atan(); + let pi3 = std::f64::consts::PI / 3.0; + let f = |ang_b: f64, ang_c: f64| { + let dbc = ang_b - ang_c; + if dbc.abs() < 1e-12 { + return f64::NAN; + } + (1.0 / dbc.sin()) + * ((2.0 * ang_b).sin() * (ang_c - omega).cos() * (ang_c + pi3).sin() + - (2.0 * ang_c).sin() * (ang_b - omega).cos() * (ang_b + pi3).sin()) + }; + let fa = f(g[1], g[2]); + let fb = f(g[2], g[0]); + let fc = f(g[0], g[1]); + push_bary( + &mut out, + "X14X98欧拉截", + "X_{383}", + "X(383) = X_{14}X_{98}\\cap\\mathrm{Euler};\\ (\\sin A)f(A,B,C)", + "X(383):X(14)X(98) 与欧拉线的交点(跳过等腰)", + "X383", + sa * fa, + sb * fb, + sc * fc, + ); + } + // X(384): Conway Euler point; bary a⁴+b²c² + { + let f = |aa: f64, bb: f64, cc_: f64| aa.powi(4) + bb * bb * cc_ * cc_; + push_bary( + &mut out, + "X32X76欧拉截", + "X_{384}", + "X(384) = X_{32}X_{76}\\cap\\mathrm{Euler};\\ a^4+b^2c^2", + "X(384):X(32)X(76) 与欧拉线的交点(Conway)", + "X384", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(385): X(2)-Hirst inverse of X(6); skip equilateral + if !equil { + let f = |aa: f64, bb: f64, cc_: f64| aa.powi(4) - bb * bb * cc_ * cc_; + push_bary( + &mut out, + "重心X6Hirst", + "X_{385}", + "X(385) = X_2\\text{-Hirst of }X_6;\\ a^4-b^2c^2", + "X(385):重心关于 X(6) 的 Hirst 逆(跳过等边)", + "X385", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(386): Brocard-inverse of X(58) + { + let f = |aa: f64, bb: f64, cc_: f64| { + aa * aa * (bb * bb + cc_ * cc_ + bb * cc_ + cc_ * aa + aa * bb) + }; + push_bary( + &mut out, + "X58布罗卡反演", + "X_{386}", + "X(386) = \\mathrm{Brocard}^{-1}(X_{58});\\ a^2(b^2+c^2+bc+ca+ab)", + "X(386):X(58) 在布罗卡圆上的反演(外接圆与阿波罗尼奥斯圆的外相似中心)", + "X386", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(387): X(1)X(2) ∩ X(4)X(6) + { + let f = |aa: f64, bb: f64, cc_: f64| { + -aa.powi(4) + 2.0 * aa * aa * (aa + bb + cc_).powi(2) + (bb * bb - cc_ * cc_).powi(2) + }; + push_bary( + &mut out, + "X1X2交X4X6", + "X_{387}", + "X(387) = X_1X_2\\cap X_4X_6;\\ -a^4+2a^2(a+b+c)^2+(b^2-c^2)^2", + "X(387):X(1)X(2) 与 X(4)X(6) 的交点", + "X387", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(388): X(1)X(4) ∩ X(7)X(8); bary a(1+cos B cos C) + { + let f = |aa: f64, ang_b: f64, ang_c: f64| aa * (1.0 + ang_b.cos() * ang_c.cos()); + push_bary( + &mut out, + "X1X4交X7X8", + "X_{388}", + "X(388) = X_1X_4\\cap X_7X_8;\\ a(1+\\cos B\\cos C)", + "X(388):X(1)X(4) 与 X(7)X(8) 的交点", + "X388", + f(a, g[1], g[2]), + f(b, g[2], g[0]), + f(c, g[0], g[1]), + ); + } + // X(389): Taylor-circle center + { + let f = |aa: f64, ang_a: f64, ang_b: f64, ang_c: f64| { + aa * (ang_a.cos() - (2.0 * ang_a).cos() * (ang_b - ang_c).cos()) + }; + push_bary( + &mut out, + "泰勒圆圆心", + "X_{389}", + "X(389) = \\mathrm{center}(\\mathrm{Taylor});\\ a[\\cos A-\\cos 2A\\cos(B-C)]", + "X(389):泰勒圆圆心(= mid(O,X52) = mid(H,X185))", + "X389", + f(a, g[0], g[1], g[2]), + f(b, g[1], g[2], g[0]), + f(c, g[2], g[0], g[1]), + ); + } + // X(390): reflection of Gergonne in incenter + { + let f = |aa: f64, bb: f64, cc_: f64| { + (bb + cc_ - aa) * (3.0 * aa * aa + (bb - cc_).powi(2)) + }; + push_bary( + &mut out, + "格尔巩内心反射", + "X_{390}", + "X(390) = 2I-X_7;\\ (b+c-a)[3a^2+(b-c)^2]", + "X(390):格尔巩点关于内心的反射", + "X390", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + } + + + // X(391)–X(400): G–K / Spieker–mitten / tan²–cos² / Napoleon mates / Parry / Yff–Malfatti (ETC). + // X(391)=X(2)X(6) ∩ X(8)X(9); bary (3a+b+c)(b+c−a); {X8,X9}-harmonic of X346. + // X(392)=X(1)X(6) ∩ X(10)X(11); bary a[(b+c)(b²+c²−a²)+4abc]; =reflection(X354 in X551). + // X(393)=X(25)-cross conjugate of X(4); bary tan² A; skip right (tan∞). + // X(394)=X(69)-Ceva of X(3); bary cos² A. + // X(395)=midpoint(X14,X16); bary (sin A)[cos(B−C)+2 cos(A+π/3)]; equil→G (weights 0). + // X(396)=midpoint(X13,X15); bary (sin A)[cos(B−C)+2 cos(A−π/3)]. + // X(397)=crosspoint(X4,X17); bary (sin A)[cos(B−C)−2 cos(A+π/3)]. + // X(398)=crosspoint(X4,X18); bary (sin A)[cos(B−C)−2 cos(A−π/3)]. + // X(399)=Parry reflection point; bary (sin A)[5 cos A−4 cos B cos C−8 sin B sin C cos 2A]. + // X(400)=Yff–Malfatti; bary sin A csc⁴(A/4). + { + let right = g.iter().any(|ang| (*ang - std::f64::consts::FRAC_PI_2).abs() < 1e-9); + let push_bary = |out: &mut Vec, + group: &str, + label: &str, + tip: &str, + zh: &str, + tag: &str, + wa: f64, + wb: f64, + wc: f64| { + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push( + fact(group, label, pt(xy[0], xy[1]), None, tip, zh).with(Shape::Point { + x: xy[0], + y: xy[1], + label: tag.into(), + }), + ); + } + } + }; + // X(391): X(2)X(6) ∩ X(8)X(9) + { + let f = |aa: f64, bb: f64, cc_: f64| (3.0 * aa + bb + cc_) * (bb + cc_ - aa); + push_bary( + &mut out, + "X2X6交X8X9", + "X_{391}", + "X(391) = X_2X_6\\cap X_8X_9;\\ (3a+b+c)(b+c-a)", + "X(391):X(2)X(6) 与 X(8)X(9) 的交点({X8,X9}-调和共轭于 X346)", + "X391", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(392): X(1)X(6) ∩ X(10)X(11) + { + let f = |aa: f64, bb: f64, cc_: f64| { + aa * ((bb + cc_) * (bb * bb + cc_ * cc_ - aa * aa) + 4.0 * aa * bb * cc_) + }; + push_bary( + &mut out, + "X1X6交X10X11", + "X_{392}", + "X(392) = X_1X_6\\cap X_{10}X_{11};\\ a[(b+c)(b^2+c^2-a^2)+4abc]", + "X(392):X(1)X(6) 与 X(10)X(11) 的交点(= X354 关于 X551 的反射)", + "X392", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(393): tan² A; skip right + if !right { + let f = |ang: f64| { + let t = ang.tan(); + t * t + }; + push_bary( + &mut out, + "X25叉共轭X4", + "X_{393}", + "X(393) = X_{25}\\text{-cross conj of }X_4;\\ \\tan^2 A", + "X(393):X(25) 关于 X(4) 的叉共轭(跳过直角)", + "X393", + f(g[0]), + f(g[1]), + f(g[2]), + ); + } + // X(394): cos² A + { + let f = |ang: f64| { + let c0 = ang.cos(); + c0 * c0 + }; + push_bary( + &mut out, + "X69塞瓦X3", + "X_{394}", + "X(394) = X_{69}\\text{-Ceva of }X_3;\\ \\cos^2 A", + "X(394):X(69) 关于外心的塞瓦共轭", + "X394", + f(g[0]), + f(g[1]), + f(g[2]), + ); + } + // X(395): midpoint(X14,X16); equilateral weights vanish → G + { + let equil = (a - b).abs() < 1e-9 && (b - c).abs() < 1e-9; + if equil { + push_bary( + &mut out, + "X14X16中点", + "X_{395}", + "X(395) = \\mathrm{mid}(X_{14},X_{16});\\ \\mathrm{equil}\\to G", + "X(395):第二费马点与第二等动力点的中点(等边退化为重心)", + "X395", + 1.0, + 1.0, + 1.0, + ); + } else { + let pi3 = std::f64::consts::PI / 3.0; + let f = |ang_a: f64, ang_b: f64, ang_c: f64| { + ang_a.sin() * ((ang_b - ang_c).cos() + 2.0 * (ang_a + pi3).cos()) + }; + push_bary( + &mut out, + "X14X16中点", + "X_{395}", + "X(395) = \\mathrm{mid}(X_{14},X_{16});\\ (\\sin A)[\\cos(B-C)+2\\cos(A+\\pi/3)]", + "X(395):第二费马点与第二等动力点的中点", + "X395", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + } + // X(396): midpoint(X13,X15) + { + let pi3 = std::f64::consts::PI / 3.0; + let f = |ang_a: f64, ang_b: f64, ang_c: f64| { + ang_a.sin() * ((ang_b - ang_c).cos() + 2.0 * (ang_a - pi3).cos()) + }; + push_bary( + &mut out, + "X13X15中点", + "X_{396}", + "X(396) = \\mathrm{mid}(X_{13},X_{15});\\ (\\sin A)[\\cos(B-C)+2\\cos(A-\\pi/3)]", + "X(396):第一费马点与第一等动力点的中点", + "X396", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + // X(397): crosspoint(X4,X17) + { + let pi3 = std::f64::consts::PI / 3.0; + let f = |ang_a: f64, ang_b: f64, ang_c: f64| { + ang_a.sin() * ((ang_b - ang_c).cos() - 2.0 * (ang_a + pi3).cos()) + }; + push_bary( + &mut out, + "X4X17截点", + "X_{397}", + "X(397) = \\mathrm{crosspoint}(X_4,X_{17});\\ (\\sin A)[\\cos(B-C)-2\\cos(A+\\pi/3)]", + "X(397):垂心与第一拿破仑点的截点", + "X397", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + // X(398): crosspoint(X4,X18) + { + let pi3 = std::f64::consts::PI / 3.0; + let f = |ang_a: f64, ang_b: f64, ang_c: f64| { + ang_a.sin() * ((ang_b - ang_c).cos() - 2.0 * (ang_a - pi3).cos()) + }; + push_bary( + &mut out, + "X4X18截点", + "X_{398}", + "X(398) = \\mathrm{crosspoint}(X_4,X_{18});\\ (\\sin A)[\\cos(B-C)-2\\cos(A-\\pi/3)]", + "X(398):垂心与第二拿破仑点的截点", + "X398", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + // X(399): Parry reflection point + { + let f = |ang_a: f64, ang_b: f64, ang_c: f64| { + ang_a.sin() + * (5.0 * ang_a.cos() + - 4.0 * ang_b.cos() * ang_c.cos() + - 8.0 * ang_b.sin() * ang_c.sin() * (2.0 * ang_a).cos()) + }; + push_bary( + &mut out, + "帕里反射点", + "X_{399}", + "X(399) = \\mathrm{Parry\\ reflection};\\ (\\sin A)[5\\cos A-4\\cos B\\cos C-8\\sin B\\sin C\\cos 2A]", + "X(399):帕里反射点(过顶点且平行于欧拉线的反射线共点)", + "X399", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + // X(400): Yff–Malfatti; bary sin A csc⁴(A/4) + { + let f = |ang: f64| { + let s4 = (ang / 4.0).sin(); + if s4.abs() < 1e-14 { + return f64::NAN; + } + ang.sin() / s4.powi(4) + }; + push_bary( + &mut out, + "伊夫马尔法蒂", + "X_{400}", + "X(400) = \\mathrm{Yff\\text{-}Malfatti};\\ \\sin A\\,\\csc^4(A/4)", + "X(400):伊夫–马尔法蒂点(Ajima–Malfatti 的外切变体)", + "X400", + f(g[0]), + f(g[1]), + f(g[2]), + ); + } + } + + // X(401)–X(460): Bailey / Zeeman–Gossard / orthic-X36 / Euler intercepts / X21·X29 subst / Hirst / Ceva / X4-Hirst / Euler 4th–11th subst / complements X27/X297/X21/X452 / crosspoint X98·X511 / X2-Hirst X27/X21/X452 / X3-Hirst X4 / X4-Hirst-pair X451 / X452 / Alshain / Euler 11th X155·X159·X195·X399 / X458 / X459 / Antares X460 (ETC). + // X(401)=Bailey=X(2)-Hirst of X(3)=anticomplement(X297); bary sin2B sin2C−sin²(2A); skip equil (weights 0). + // X(402)=Zeeman–Gossard perspector; bary p·y with p=2a⁴−a²b²−a²c²−(b²−c²)², + // y=a⁸−a⁶(b²+c²)+a⁴(2b²−c²)(2c²−b²)+[(b²−c²)²][3a²(b²+c²)−b⁴−c⁴−3b²c²]; skip equil (p=0). + // X(403)=X(36)-of-orthic=mid(H,X186); bary tan A(1+cos2B+cos2C); skip right/equil. + // X(404)={X2,X3}-harmonic of X(21); bary abc(a+b+c)−a²(b²+c²−a²). + // X(405)=Euler ∩ X(1)X(6)=complement(X377); bary b+c+a(1+cos A). + // X(406)=Euler ∩ X(10)X(33); bary b+c+a(1+sec A); skip right. + // X(407)=crosspoint(X4,X225); bary (v+w)tan A with u:v:w=X(21) tril 1/(cosB+cosC); skip right. + // X(408)=Euler ∩ X(73)X(228); bary (v+w)sin2A with u:v:w=X(29) tril (sec A)/(cosB+cosC); skip right. + // X(409)=Euler X(21)-1st-substitution; bary a(u²+vw), u:v:w=X(21). + // X(410)=Euler X(29)-1st-substitution; bary a(u²+vw), u:v:w=X(29); skip right. + // X(411)=Euler X(21)-2nd-subst={X3,X4}-harmonic of X(21); bary a[(1/v)²+(1/w)²−(1/u)²]; skip equil (0). + // X(412)=Euler X(29)-2nd-subst={X3,X4}-harmonic of X(29); same form on X(29); skip right/equil. + // X(413)=Euler X(21)-3rd-subst; bary a u³(v²+w²−vw), u:v:w=X(21). + // X(414)=Euler X(29)-3rd-subst; same on X(29); skip right. + // X(415)=X(4)-Hirst of X(29); bary (u²−vw)tan A, u:v:w=X(21); skip right/equil. + // X(416)=X(3)-Hirst of X(21); bary (u²−vw)sin2A, u:v:w=X(29); skip right/equil. + // X(417)=X(3)-Ceva of X(185); bary sin2A(sec²B+sec²C); skip right (sec∞). + // X(418)=X(3)-Ceva of X(216); bary sin2A(csc2B+csc2C); skip right (csc π). + // X(419)=X(4)-Hirst of X(25); bary (u²−vw)tan A, u:v:w=X(31)=a²; skip right/equil. + // X(420)=X(4)-Hirst of X(427); bary (u²−vw)tan A, u:v:w=X(38)=b²+c²; skip right/equil. + // X(421)=X(4)-Hirst of X(24); bary (u²−vw)tan A, u:v:w=X(47) tril cos2A; skip right/equil. + // X(422)=X(4)-Hirst of X(28); u:v:w=X(58) tril a/(b+c); skip right/equil. + // X(423)=X(4)-Hirst of X(27); u:v:w=X(81) tril 1/(b+c); skip right/equil. + // X(424)=X(4)-Hirst of X(451); u:v:w=X(191) tril; skip right/equil. + // X(425)=X(4)-Hirst of X(21); u:v:w=X(283) tril cosA/(cosB+cosC); skip right/equil. + // X(426)=Euler X(19)-4th-subst; bary (v²+w²)sin2A, u:v:w=tan A; skip right. + // X(427)=complement of X(22); bary sin A(sec A+cos(B−C)); skip right. + // X(428)=Euler X(38)-5th-subst; bary (v+w)tan A, u:v:w=b²+c²; skip right. + // X(429)=Euler X(58)-5th-subst; bary (v+w)tan A, u:v:w=a/(b+c); skip right. + // X(430)=Euler X(81)-5th-subst; bary (v+w)tan A, u:v:w=1/(b+c); skip right. + // X(431)=Euler X(283)-5th-subst; bary (v+w)tan A, u:v:w=cosA/(cosB+cosC); skip right. + // X(432)=Euler X(155)-6th-subst; bary (v²+w²)tan A, u:v:w=cosA(cos²B+cos²C−cos²A); skip right. + // X(433)=Euler X(159)-6th-subst; bary (v²+w²)tan A, u:v:w=X(159) tril; skip right. + // X(434)=Euler X(195)-6th-subst; bary (v²+w²)tan A, u:v:w=X(195) tril; skip right. + // X(435)=Euler X(399)-6th-subst; bary (v²+w²)tan A, u:v:w=X(399) tril; skip right. + // X(436)=Euler X(48)-7th-subst; bary (u²+vw)tan A, u:v:w=sin2A; skip right. + // X(437)=Euler X(214)-8th-subst; bary (u²+vw)tan A, u:v:w=(b+c−2a)(b²+c²−a²−bc); skip right/equil. + // X(438)=Euler X(204)-9th-subst; bary u²+vw, u:v:w=tanA(tanB+tanC−tanA); skip right. + // X(439)=Euler X(193)-10th-subst; bary (3a²−b²−c²)². + // X(440)=complement of X(27); bary v+w, u:v:w=tanA/(b+c); skip right. + // X(441)=complement of X(297); bary v+w, u:v:w=X(240) tril secA cos(A+ω); skip right/equil. + // X(442)=complement of X(21); bary v+w, u:v:w=X(284) tril sinA/(cosB+cosC); =mid(X79,X191). + // X(443)=complement of X(452); bary v+w, u:v:w=X(380) tril. + // X(444)=Euler ∩ X(19)X(232); bary (v+w)sinA tanA, u:v:w=X(256)=1/(a²+bc); skip right. + // X(445)=Euler X(79)-11th-subst; bary (v+w)secA, u:v:w=1/(1+2cosA); skip right. + // X(446)=crosspoint of X(98) and X(511); bary a u(v²+w²), u:v:w=sec(A+ω); skip equil. + // X(447)=X(2)-Hirst of X(27); bary u²−vw, u:v:w=tanA/(b+c); skip right/equil. + // X(448)=X(2)-Hirst of X(21); bary u²−vw, u:v:w=sinA/(cosB+cosC); skip equil. + // X(449)=X(2)-Hirst of X(452); bary u²−vw, u:v:w=X(380) tril; skip equil. + // X(450)=X(3)-Hirst of X(4); bary tanA(cos⁴A−(cosB cosC)²); skip right/equil. + // X(451)=X(4)-Hirst inverse of X(424); bary u tan A, u:v:w=X(191) tril; skip right. + // X(452)=X(2)-Hirst inverse of X(449); bary = X(380) tril (anticomplement of X443). + // X(453)=Alshain; bary sinA (cosB+cosC−cosA)²/(cosB+cosC). + // X(454)=Euler X(155)-11th-subst; bary u² tan A, u:v:w=X(155) tril; skip right. + // X(455)=Euler X(159)-11th-subst; bary u² tan A, u:v:w=X(159) tril; skip right. + // X(456)=Euler X(195)-11th-subst; bary u² tan A, u:v:w=X(195) tril; skip right. + // X(457)=Euler X(399)-11th-subst; bary u² tan A, u:v:w=X(399) tril; skip right. + // X(458)=Euler ∩ X(76)X(275); bary tanA+cot ω = secA cos(A−ω); skip right. + // X(459)=X(253)-Ceva of X(4); bary 1/(tanB tanC−2). + // X(460)=Antares; bary tanA[a²(2a²−b²−c²)+(b²−c²)²]; =X4*X230 bary product; skip right/equil. + { + let right = g.iter().any(|ang| (*ang - std::f64::consts::FRAC_PI_2).abs() < 1e-9); + let equil = (a - b).abs() < 1e-9 && (b - c).abs() < 1e-9; + let (ca, cb, cc) = (g[0].cos(), g[1].cos(), g[2].cos()); + let push_bary = |out: &mut Vec, + group: &str, + label: &str, + tip: &str, + zh: &str, + tag: &str, + wa: f64, + wb: f64, + wc: f64| { + let den = wa + wb + wc; + if den.abs() > 1e-12 && wa.is_finite() && wb.is_finite() && wc.is_finite() { + let xy = [ + (wa * pa[0] + wb * pb[0] + wc * pc[0]) / den, + (wa * pa[1] + wb * pb[1] + wc * pc[1]) / den, + ]; + if xy[0].is_finite() && xy[1].is_finite() { + out.push( + fact(group, label, pt(xy[0], xy[1]), None, tip, zh).with(Shape::Point { + x: xy[0], + y: xy[1], + label: tag.into(), + }), + ); + } + } + }; + // X(401): Bailey = X(2)-Hirst of O; skip equilateral + if !equil { + let f = |ang_a: f64, ang_b: f64, ang_c: f64| { + (2.0 * ang_b).sin() * (2.0 * ang_c).sin() - (2.0 * ang_a).sin().powi(2) + }; + push_bary( + &mut out, + "贝利点", + "X_{401}", + "X(401) = \\mathrm{Bailey}=X_2\\text{-Hirst of }X_3;\\ \\sin 2B\\sin 2C-\\sin^2 2A", + "X(401):贝利点(重心对圆心的 Hirst 逆;跳过等边)", + "X401", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + // X(402): Zeeman–Gossard perspector; skip equilateral + if !equil { + let p = |aa: f64, bb: f64, cc_: f64| { + 2.0 * aa.powi(4) + - aa * aa * bb * bb + - aa * aa * cc_ * cc_ + - (bb * bb - cc_ * cc_).powi(2) + }; + let y = |aa: f64, bb: f64, cc_: f64| { + let a2 = aa * aa; + let b2 = bb * bb; + let c2 = cc_ * cc_; + let dbc = (b2 - c2).powi(2); + aa.powi(8) + - aa.powi(6) * (b2 + c2) + + aa.powi(4) * (2.0 * b2 - c2) * (2.0 * c2 - b2) + + dbc * (3.0 * a2 * (b2 + c2) - b2 * b2 - c2 * c2 - 3.0 * b2 * c2) + }; + let f = |aa: f64, bb: f64, cc_: f64| p(aa, bb, cc_) * y(aa, bb, cc_); + push_bary( + &mut out, + "齐曼戈萨德", + "X_{402}", + "X(402) = \\mathrm{Zeeman\\text{-}Gossard};\\ p(a,b,c)\\,y(a,b,c)", + "X(402):齐曼–戈萨德透视中心(欧拉线侧三角形的欧拉线透视点)", + "X402", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(403): X(36) of orthic = mid(H, X186); skip right / equilateral + if !right && !equil { + let f = |ang_a: f64, ang_b: f64, ang_c: f64| { + ang_a.tan() * (1.0 + (2.0 * ang_b).cos() + (2.0 * ang_c).cos()) + }; + push_bary( + &mut out, + "垂足X36", + "X_{403}", + "X(403) = X_{36}\\text{-of-orthic}=\\mathrm{mid}(X_4,X_{186});\\ \\tan A(1+\\cos 2B+\\cos 2C)", + "X(403):垂足三角形的 X(36)(= 垂心与 X186 的中点;跳过直角/等边)", + "X403", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + // X(404): {X2,X3}-harmonic conjugate of Schiffler X(21) + { + let f = |aa: f64, bb: f64, cc_: f64| { + aa * bb * cc_ * (aa + bb + cc_) - aa * aa * (bb * bb + cc_ * cc_ - aa * aa) + }; + push_bary( + &mut out, + "X21欧拉调和", + "X_{404}", + "X(404) = \\{X_2,X_3\\}\\text{-harmonic of }X_{21};\\ abc(a+b+c)-a^2(b^2+c^2-a^2)", + "X(404):席弗勒点关于 {重心,外心} 的调和共轭", + "X404", + f(a, b, c), + f(b, c, a), + f(c, a, b), + ); + } + // X(405): Euler ∩ X(1)X(6) = complement of X(377) + { + let f = |aa: f64, bb: f64, cc_: f64, caa: f64| bb + cc_ + aa * (1.0 + caa); + push_bary( + &mut out, + "X1X6欧拉截", + "X_{405}", + "X(405) = \\mathrm{Euler}\\cap X_1X_6;\\ b+c+a(1+\\cos A)", + "X(405):内心–类似重心线与欧拉线的交点(= X377 的补点)", + "X405", + f(a, b, c, ca), + f(b, c, a, cb), + f(c, a, b, cc), + ); + } + // X(406): Euler ∩ X(10)X(33); skip right + if !right { + let f = |aa: f64, bb: f64, cc_: f64, ang: f64| { + let c0 = ang.cos(); + if c0.abs() < 1e-14 { + return f64::NAN; + } + bb + cc_ + aa * (1.0 + 1.0 / c0) + }; + push_bary( + &mut out, + "X10X33欧拉截", + "X_{406}", + "X(406) = \\mathrm{Euler}\\cap X_{10}X_{33};\\ b+c+a(1+\\sec A)", + "X(406):斯皮克–垂足透视线与欧拉线的交点(跳过直角)", + "X406", + f(a, b, c, g[0]), + f(b, c, a, g[1]), + f(c, a, b, g[2]), + ); + } + // X(21) trilinears u:v:w = 1/(cos B + cos C) + let x21_tril = { + let da = cb + cc; + let db = cc + ca; + let dc = ca + cb; + if da.abs() > 1e-14 && db.abs() > 1e-14 && dc.abs() > 1e-14 { + Some((1.0 / da, 1.0 / db, 1.0 / dc)) + } else { + None + } + }; + // X(407): crosspoint(X4, X225); (v+w) tan A; skip right + if !right { + if let Some((u21, v21, w21)) = x21_tril { + push_bary( + &mut out, + "X4X225截点", + "X_{407}", + "X(407) = \\mathrm{crosspoint}(X_4,X_{225});\\ (v+w)\\tan A\\ (u:v:w=X_{21})", + "X(407):垂心与 X(225) 的截点(跳过直角)", + "X407", + (v21 + w21) * g[0].tan(), + (w21 + u21) * g[1].tan(), + (u21 + v21) * g[2].tan(), + ); + } + } + // X(29) trilinears u:v:w = (sec A)/(cos B + cos C); skip right (sec∞) + let x29_tril = if !right { + let da = cb + cc; + let db = cc + ca; + let dc = ca + cb; + if ca.abs() > 1e-14 + && cb.abs() > 1e-14 + && cc.abs() > 1e-14 + && da.abs() > 1e-14 + && db.abs() > 1e-14 + && dc.abs() > 1e-14 + { + Some((1.0 / ca / da, 1.0 / cb / db, 1.0 / cc / dc)) + } else { + None + } + } else { + None + }; + // X(408): Euler ∩ X(73)X(228); (v+w) sin 2A + if let Some((u29, v29, w29)) = x29_tril { + push_bary( + &mut out, + "X73X228欧拉截", + "X_{408}", + "X(408) = \\mathrm{Euler}\\cap X_{73}X_{228};\\ (v+w)\\sin 2A\\ (u:v:w=X_{29})", + "X(408):X(73)X(228) 与欧拉线的交点(跳过直角)", + "X408", + (v29 + w29) * (2.0 * g[0]).sin(), + (w29 + u29) * (2.0 * g[1]).sin(), + (u29 + v29) * (2.0 * g[2]).sin(), + ); + } + // X(409): Euler X(21)-1st-substitution; a(u²+vw) + if let Some((u21, v21, w21)) = x21_tril { + push_bary( + &mut out, + "X21一次替换", + "X_{409}", + "X(409) = \\mathrm{Euler}\\ X_{21}\\text{-1st-subst};\\ a(u^2+vw)\\ (u:v:w=X_{21})", + "X(409):席弗勒点的欧拉一次替换点", + "X409", + a * (u21 * u21 + v21 * w21), + b * (v21 * v21 + w21 * u21), + c * (w21 * w21 + u21 * v21), + ); + } + // X(410): Euler X(29)-1st-substitution; a(u²+vw); skip right + if let Some((u29, v29, w29)) = x29_tril { + push_bary( + &mut out, + "X29一次替换", + "X_{410}", + "X(410) = \\mathrm{Euler}\\ X_{29}\\text{-1st-subst};\\ a(u^2+vw)\\ (u:v:w=X_{29})", + "X(410):X(29) 的欧拉一次替换点(跳过直角)", + "X410", + a * (u29 * u29 + v29 * w29), + b * (v29 * v29 + w29 * u29), + c * (w29 * w29 + u29 * v29), + ); + } + // X(411): Euler X(21)-2nd-substitution; skip equilateral (weights 0) + if !equil { + if let Some((u21, v21, w21)) = x21_tril { + let f = |u: f64, v: f64, w: f64| { + if u.abs() < 1e-14 || v.abs() < 1e-14 || w.abs() < 1e-14 { + return f64::NAN; + } + (1.0 / v).powi(2) + (1.0 / w).powi(2) - (1.0 / u).powi(2) + }; + push_bary( + &mut out, + "X21二次替换", + "X_{411}", + "X(411) = \\mathrm{Euler}\\ X_{21}\\text{-2nd-subst};\\ a[(1/v)^2+(1/w)^2-(1/u)^2]", + "X(411):席弗勒点的欧拉二次替换点(={X3,X4}-调和;跳过等边)", + "X411", + a * f(u21, v21, w21), + b * f(v21, w21, u21), + c * f(w21, u21, v21), + ); + } + } + // X(412): Euler X(29)-2nd-substitution; skip right / equilateral + if !equil { + if let Some((u29, v29, w29)) = x29_tril { + let f = |u: f64, v: f64, w: f64| { + if u.abs() < 1e-14 || v.abs() < 1e-14 || w.abs() < 1e-14 { + return f64::NAN; + } + (1.0 / v).powi(2) + (1.0 / w).powi(2) - (1.0 / u).powi(2) + }; + push_bary( + &mut out, + "X29二次替换", + "X_{412}", + "X(412) = \\mathrm{Euler}\\ X_{29}\\text{-2nd-subst};\\ a[(1/v)^2+(1/w)^2-(1/u)^2]", + "X(412):X(29) 的欧拉二次替换点(跳过直角/等边)", + "X412", + a * f(u29, v29, w29), + b * f(v29, w29, u29), + c * f(w29, u29, v29), + ); + } + } + // X(413): Euler X(21)-3rd-substitution + if let Some((u21, v21, w21)) = x21_tril { + let f = |u: f64, v: f64, w: f64| u.powi(3) * (v * v + w * w - v * w); + push_bary( + &mut out, + "X21三次替换", + "X_{413}", + "X(413) = \\mathrm{Euler}\\ X_{21}\\text{-3rd-subst};\\ a\\,u^3(v^2+w^2-vw)", + "X(413):席弗勒点的欧拉三次替换点", + "X413", + a * f(u21, v21, w21), + b * f(v21, w21, u21), + c * f(w21, u21, v21), + ); + } + // X(414): Euler X(29)-3rd-substitution; skip right + if let Some((u29, v29, w29)) = x29_tril { + let f = |u: f64, v: f64, w: f64| u.powi(3) * (v * v + w * w - v * w); + push_bary( + &mut out, + "X29三次替换", + "X_{414}", + "X(414) = \\mathrm{Euler}\\ X_{29}\\text{-3rd-subst};\\ a\\,u^3(v^2+w^2-vw)", + "X(414):X(29) 的欧拉三次替换点(跳过直角)", + "X414", + a * f(u29, v29, w29), + b * f(v29, w29, u29), + c * f(w29, u29, v29), + ); + } + // X(415): X(4)-Hirst inverse of X(29); (u²−vw) tan A on X(21); skip right/equil + if !right && !equil { + if let Some((u21, v21, w21)) = x21_tril { + let f = |u: f64, v: f64, w: f64, ang: f64| (u * u - v * w) * ang.tan(); + push_bary( + &mut out, + "X4HirstX29", + "X_{415}", + "X(415) = X_4\\text{-Hirst of }X_{29};\\ (u^2-vw)\\tan A\\ (u:v:w=X_{21})", + "X(415):垂心对 X(29) 的 Hirst 逆(用 X21 坐标;跳过直角/等边)", + "X415", + f(u21, v21, w21, g[0]), + f(v21, w21, u21, g[1]), + f(w21, u21, v21, g[2]), + ); + } + } + // X(416): X(3)-Hirst inverse of X(21); (u²−vw) sin 2A on X(29); skip right/equil + if !equil { + if let Some((u29, v29, w29)) = x29_tril { + let f = |u: f64, v: f64, w: f64, ang: f64| (u * u - v * w) * (2.0 * ang).sin(); + push_bary( + &mut out, + "X3HirstX21", + "X_{416}", + "X(416) = X_3\\text{-Hirst of }X_{21};\\ (u^2-vw)\\sin 2A\\ (u:v:w=X_{29})", + "X(416):外心对席弗勒点的 Hirst 逆(跳过直角/等边)", + "X416", + f(u29, v29, w29, g[0]), + f(v29, w29, u29, g[1]), + f(w29, u29, v29, g[2]), + ); + } + } + // X(417): X(3)-Ceva conjugate of X(185); sin2A(sec²B+sec²C); skip right + if !right { + let f = |ang_a: f64, ang_b: f64, ang_c: f64| { + let cb_ = ang_b.cos(); + let cc_ = ang_c.cos(); + if cb_.abs() < 1e-14 || cc_.abs() < 1e-14 { + return f64::NAN; + } + (2.0 * ang_a).sin() * (1.0 / (cb_ * cb_) + 1.0 / (cc_ * cc_)) + }; + push_bary( + &mut out, + "X3CevaX185", + "X_{417}", + "X(417) = X_3\\text{-Ceva of }X_{185};\\ \\sin 2A(\\sec^2 B+\\sec^2 C)", + "X(417):外心对 X(185) 的塞瓦共轭(跳过直角)", + "X417", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + // X(418): X(3)-Ceva conjugate of X(216); sin2A(csc2B+csc2C); skip right + if !right { + let f = |ang_a: f64, ang_b: f64, ang_c: f64| { + let s2b = (2.0 * ang_b).sin(); + let s2c = (2.0 * ang_c).sin(); + if s2b.abs() < 1e-14 || s2c.abs() < 1e-14 { + return f64::NAN; + } + (2.0 * ang_a).sin() * (1.0 / s2b + 1.0 / s2c) + }; + push_bary( + &mut out, + "X3CevaX216", + "X_{418}", + "X(418) = X_3\\text{-Ceva of }X_{216};\\ \\sin 2A(\\csc 2B+\\csc 2C)", + "X(418):外心对 X(216) 的塞瓦共轭(跳过直角)", + "X418", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + // X(419): X(4)-Hirst of X(25); (u²−vw)tan A on X(31)=a²; skip right/equil + if !right && !equil { + let (u31, v31, w31) = (a * a, b * b, c * c); + let f = |u: f64, v: f64, w: f64, ang: f64| (u * u - v * w) * ang.tan(); + push_bary( + &mut out, + "X4HirstX25", + "X_{419}", + "X(419) = X_4\\text{-Hirst of }X_{25};\\ (u^2-vw)\\tan A\\ (u:v:w=X_{31})", + "X(419):垂心对 X(25) 的 Hirst 逆(X31=a²;跳过直角/等边)", + "X419", + f(u31, v31, w31, g[0]), + f(v31, w31, u31, g[1]), + f(w31, u31, v31, g[2]), + ); + } + // X(420): X(4)-Hirst of X(427); (u²−vw)tan A on X(38)=b²+c²; skip right/equil + if !right && !equil { + let (u38, v38, w38) = (b * b + c * c, c * c + a * a, a * a + b * b); + let f = |u: f64, v: f64, w: f64, ang: f64| (u * u - v * w) * ang.tan(); + push_bary( + &mut out, + "X4HirstX427", + "X_{420}", + "X(420) = X_4\\text{-Hirst of }X_{427};\\ (u^2-vw)\\tan A\\ (u:v:w=X_{38})", + "X(420):垂心对 X(427) 的 Hirst 逆(X38=b²+c²;跳过直角/等边)", + "X420", + f(u38, v38, w38, g[0]), + f(v38, w38, u38, g[1]), + f(w38, u38, v38, g[2]), + ); + } + // X(421)–X(430): X4-Hirst of X24/X28/X27/X451/X21; Euler X19-4th; complement X22; Euler X38/X58/X81-5th (ETC). + // X(421)=X(4)-Hirst of X(24); bary (u²−vw)tan A, u:v:w=X(47) tril cos2A; skip right/equil. + // X(422)=X(4)-Hirst of X(28); u:v:w=X(58) tril a/(b+c); skip right/equil. + // X(423)=X(4)-Hirst of X(27); u:v:w=X(81) tril 1/(b+c); skip right/equil. + // X(424)=X(4)-Hirst of X(451); u:v:w=X(191) tril (b+c−a)(Σbc)+b³+c³−a³; skip right/equil. + // X(425)=X(4)-Hirst of X(21); u:v:w=X(283) tril cosA/(cosB+cosC); skip right/equil. + // X(426)=Euler X(19)-4th-subst; bary (v²+w²)sin2A, u:v:w=X(19)=tan A; skip right. + // X(427)=complement of X(22); bary sin A(sec A+cos(B−C)); skip right (sec∞). + // X(428)=Euler X(38)-5th-subst; bary (v+w)tan A, u:v:w=X(38)=b²+c²; skip right. + // X(429)=Euler X(58)-5th-subst; bary (v+w)tan A, u:v:w=X(58) tril a/(b+c); skip right. + // X(430)=Euler X(81)-5th-subst; bary (v+w)tan A, u:v:w=X(81) tril 1/(b+c); skip right. + // X(421): X(4)-Hirst of X(24); (u²−vw)tan A on cos 2A; skip right/equil + if !right && !equil { + let (u47, v47, w47) = ((2.0 * g[0]).cos(), (2.0 * g[1]).cos(), (2.0 * g[2]).cos()); + let f = |u: f64, v: f64, w: f64, ang: f64| (u * u - v * w) * ang.tan(); + push_bary( + &mut out, + "X4HirstX24", + "X_{421}", + "X(421) = X_4\\text{-Hirst of }X_{24};\\ (u^2-vw)\\tan A\\ (u:v:w=\\cos 2A)", + "X(421):垂心对 X(24) 的 Hirst 逆(X47=cos2A;跳过直角/等边)", + "X421", + f(u47, v47, w47, g[0]), + f(v47, w47, u47, g[1]), + f(w47, u47, v47, g[2]), + ); + } + // X(422): X(4)-Hirst of X(28); on X(58) tril a/(b+c); skip right/equil + if !right && !equil { + let (u58, v58, w58) = (a / (b + c), b / (c + a), c / (a + b)); + let f = |u: f64, v: f64, w: f64, ang: f64| (u * u - v * w) * ang.tan(); + push_bary( + &mut out, + "X4HirstX28", + "X_{422}", + "X(422) = X_4\\text{-Hirst of }X_{28};\\ (u^2-vw)\\tan A\\ (u:v:w=X_{58})", + "X(422):垂心对 X(28) 的 Hirst 逆(X58=a/(b+c);跳过直角/等边)", + "X422", + f(u58, v58, w58, g[0]), + f(v58, w58, u58, g[1]), + f(w58, u58, v58, g[2]), + ); + } + // X(423): X(4)-Hirst of X(27); on X(81) tril 1/(b+c); skip right/equil + if !right && !equil { + let (u81, v81, w81) = (1.0 / (b + c), 1.0 / (c + a), 1.0 / (a + b)); + let f = |u: f64, v: f64, w: f64, ang: f64| (u * u - v * w) * ang.tan(); + push_bary( + &mut out, + "X4HirstX27", + "X_{423}", + "X(423) = X_4\\text{-Hirst of }X_{27};\\ (u^2-vw)\\tan A\\ (u:v:w=X_{81})", + "X(423):垂心对 X(27) 的 Hirst 逆(X81=1/(b+c);跳过直角/等边)", + "X423", + f(u81, v81, w81, g[0]), + f(v81, w81, u81, g[1]), + f(w81, u81, v81, g[2]), + ); + } + // X(424): X(4)-Hirst of X(451); on X(191) tril; skip right/equil + if !right && !equil { + let f191 = |aa: f64, bb: f64, cc_: f64| { + (bb + cc_ - aa) * (bb * cc_ + cc_ * aa + aa * bb) + bb.powi(3) + cc_.powi(3) - aa.powi(3) + }; + let (u191, v191, w191) = (f191(a, b, c), f191(b, c, a), f191(c, a, b)); + let f = |u: f64, v: f64, w: f64, ang: f64| (u * u - v * w) * ang.tan(); + push_bary( + &mut out, + "X4HirstX451", + "X_{424}", + "X(424) = X_4\\text{-Hirst of }X_{451};\\ (u^2-vw)\\tan A\\ (u:v:w=X_{191})", + "X(424):垂心对 X(451) 的 Hirst 逆(X191 三线坐标;跳过直角/等边)", + "X424", + f(u191, v191, w191, g[0]), + f(v191, w191, u191, g[1]), + f(w191, u191, v191, g[2]), + ); + } + // X(425): X(4)-Hirst of X(21); on X(283) tril cosA/(cosB+cosC); skip right/equil + if !right && !equil { + let da = cb + cc; + let db = cc + ca; + let dc = ca + cb; + if da.abs() > 1e-14 && db.abs() > 1e-14 && dc.abs() > 1e-14 { + let (u283, v283, w283) = (ca / da, cb / db, cc / dc); + let f = |u: f64, v: f64, w: f64, ang: f64| (u * u - v * w) * ang.tan(); + push_bary( + &mut out, + "X4HirstX21", + "X_{425}", + "X(425) = X_4\\text{-Hirst of }X_{21};\\ (u^2-vw)\\tan A\\ (u:v:w=X_{283})", + "X(425):垂心对席弗勒点的 Hirst 逆(X283;跳过直角/等边)", + "X425", + f(u283, v283, w283, g[0]), + f(v283, w283, u283, g[1]), + f(w283, u283, v283, g[2]), + ); + } + } + // X(426): Euler X(19)-4th-substitution; (v²+w²)sin2A on tan A; skip right + if !right { + let (u19, v19, w19) = (g[0].tan(), g[1].tan(), g[2].tan()); + let f = |v: f64, w: f64, ang: f64| (v * v + w * w) * (2.0 * ang).sin(); + push_bary( + &mut out, + "X19四次替换", + "X_{426}", + "X(426) = \\mathrm{Euler}\\ X_{19}\\text{-4th-subst};\\ (v^2+w^2)\\sin 2A\\ (u:v:w=\\tan A)", + "X(426):克劳森点的欧拉四次替换点(跳过直角)", + "X426", + f(v19, w19, g[0]), + f(w19, u19, g[1]), + f(u19, v19, g[2]), + ); + } + // X(427): complement of X(22); sin A(sec A + cos(B−C)); skip right + if !right { + let f = |ang_a: f64, ang_b: f64, ang_c: f64| { + let ca_ = ang_a.cos(); + if ca_.abs() < 1e-14 { + return f64::NAN; + } + ang_a.sin() * (1.0 / ca_ + (ang_b - ang_c).cos()) + }; + push_bary( + &mut out, + "X22补点", + "X_{427}", + "X(427) = \\mathrm{complement}(X_{22});\\ \\sin A(\\sec A+\\cos(B-C))", + "X(427):X(22) 的补点(跳过直角)", + "X427", + f(g[0], g[1], g[2]), + f(g[1], g[2], g[0]), + f(g[2], g[0], g[1]), + ); + } + // X(428): Euler X(38)-5th-substitution; (v+w)tan A on b²+c²; skip right + if !right { + let (u38b, v38b, w38b) = (b * b + c * c, c * c + a * a, a * a + b * b); + push_bary( + &mut out, + "X38五次替换", + "X_{428}", + "X(428) = \\mathrm{Euler}\\ X_{38}\\text{-5th-subst};\\ (v+w)\\tan A\\ (u:v:w=b^2+c^2)", + "X(428):X(38) 的欧拉五次替换点(跳过直角)", + "X428", + (v38b + w38b) * g[0].tan(), + (w38b + u38b) * g[1].tan(), + (u38b + v38b) * g[2].tan(), + ); + } + // X(429): Euler X(58)-5th-substitution; (v+w)tan A on a/(b+c); skip right + if !right { + let (u58b, v58b, w58b) = (a / (b + c), b / (c + a), c / (a + b)); + push_bary( + &mut out, + "X58五次替换", + "X_{429}", + "X(429) = \\mathrm{Euler}\\ X_{58}\\text{-5th-subst};\\ (v+w)\\tan A\\ (u:v:w=X_{58})", + "X(429):X(58) 的欧拉五次替换点(跳过直角)", + "X429", + (v58b + w58b) * g[0].tan(), + (w58b + u58b) * g[1].tan(), + (u58b + v58b) * g[2].tan(), + ); + } + // X(430): Euler X(81)-5th-substitution; (v+w)tan A on 1/(b+c); skip right + if !right { + let (u81b, v81b, w81b) = (1.0 / (b + c), 1.0 / (c + a), 1.0 / (a + b)); + push_bary( + &mut out, + "X81五次替换", + "X_{430}", + "X(430) = \\mathrm{Euler}\\ X_{81}\\text{-5th-subst};\\ (v+w)\\tan A\\ (u:v:w=X_{81})", + "X(430):X(81) 的欧拉五次替换点(跳过直角)", + "X430", + (v81b + w81b) * g[0].tan(), + (w81b + u81b) * g[1].tan(), + (u81b + v81b) * g[2].tan(), + ); + } + + // X(431)–X(440): Euler X283-5th; X155/X159/X195/X399-6th; X48-7th; X214-8th; X204-9th; X193-10th; complement X27 (ETC). + // X(431): Euler X(283)-5th-substitution; (v+w)tan A on cosA/(cosB+cosC); skip right + if !right { + let da = cb + cc; + let db = cc + ca; + let dc = ca + cb; + if da.abs() > 1e-14 && db.abs() > 1e-14 && dc.abs() > 1e-14 { + let (u283b, v283b, w283b) = (ca / da, cb / db, cc / dc); + push_bary( + &mut out, + "X283五次替换", + "X_{431}", + "X(431) = \\mathrm{Euler}\\ X_{283}\\text{-5th-subst};\\ (v+w)\\tan A\\ (u:v:w=X_{283})", + "X(431):X(283) 的欧拉五次替换点(跳过直角)", + "X431", + (v283b + w283b) * g[0].tan(), + (w283b + u283b) * g[1].tan(), + (u283b + v283b) * g[2].tan(), + ); + } + } + // X(432): Euler X(155)-6th-substitution; (v²+w²)tan A on cosA(cos²B+cos²C−cos²A); skip right + if !right { + let u155 = ca * (cb * cb + cc * cc - ca * ca); + let v155 = cb * (cc * cc + ca * ca - cb * cb); + let w155 = cc * (ca * ca + cb * cb - cc * cc); + let f = |v: f64, w: f64, ang: f64| (v * v + w * w) * ang.tan(); + push_bary( + &mut out, + "X155六次替换", + "X_{432}", + "X(432) = \\mathrm{Euler}\\ X_{155}\\text{-6th-subst};\\ (v^2+w^2)\\tan A\\ (u:v:w=X_{155})", + "X(432):X(155) 的欧拉六次替换点(跳过直角)", + "X432", + f(v155, w155, g[0]), + f(w155, u155, g[1]), + f(u155, v155, g[2]), + ); + } + // X(433): Euler X(159)-6th-substitution; (v²+w²)tan A on X(159) tril; skip right + if !right { + let f159 = |aa: f64, bb: f64, cc_: f64, ang_a: f64, ang_b: f64, ang_c: f64| { + aa * ((a * a + b * b + c * c) * (2.0 * ang_a).sin() + + (cc_ * cc_ - bb * bb - aa * aa) * (2.0 * ang_b).sin() + + (bb * bb - cc_ * cc_ - aa * aa) * (2.0 * ang_c).sin()) + }; + let u159 = f159(a, b, c, g[0], g[1], g[2]); + let v159 = f159(b, c, a, g[1], g[2], g[0]); + let w159 = f159(c, a, b, g[2], g[0], g[1]); + let f = |v: f64, w: f64, ang: f64| (v * v + w * w) * ang.tan(); + push_bary( + &mut out, + "X159六次替换", + "X_{433}", + "X(433) = \\mathrm{Euler}\\ X_{159}\\text{-6th-subst};\\ (v^2+w^2)\\tan A\\ (u:v:w=X_{159})", + "X(433):X(159) 的欧拉六次替换点(跳过直角)", + "X433", + f(v159, w159, g[0]), + f(w159, u159, g[1]), + f(u159, v159, g[2]), + ); + } + // X(434): Euler X(195)-6th-substitution; (v²+w²)tan A on X(195) tril=bary/a; skip right + if !right && area > 1e-12 { + let (sa, sb, sc) = (g[0].sin(), g[1].sin(), g[2].sin()); + if sa.abs() > 1e-14 && sb.abs() > 1e-14 && sc.abs() > 1e-14 { + let cot_w = (a * a + b * b + c * c) / (4.0 * area); + let bary195 = |ang: f64, cang: f64, sang: f64| { + let cot = cang / sang; + 4.0 * (2.0 * ang).cos() + cot * cot - cot * cot_w + }; + // tril = bary/a (homogeneous; use bary as tril weights) + let u195 = bary195(g[0], ca, sa) / a; + let v195 = bary195(g[1], cb, sb) / b; + let w195 = bary195(g[2], cc, sc) / c; + let f = |v: f64, w: f64, ang: f64| (v * v + w * w) * ang.tan(); + push_bary( + &mut out, + "X195六次替换", + "X_{434}", + "X(434) = \\mathrm{Euler}\\ X_{195}\\text{-6th-subst};\\ (v^2+w^2)\\tan A\\ (u:v:w=X_{195})", + "X(434):X(195) 的欧拉六次替换点(跳过直角)", + "X434", + f(v195, w195, g[0]), + f(w195, u195, g[1]), + f(u195, v195, g[2]), + ); + } + } + // X(435): Euler X(399)-6th-substitution; (v²+w²)tan A on Parry tril; skip right + if !right { + let f399 = |ang_a: f64, ang_b: f64, ang_c: f64| { + 5.0 * ang_a.cos() + - 4.0 * ang_b.cos() * ang_c.cos() + - 8.0 * ang_b.sin() * ang_c.sin() * (2.0 * ang_a).cos() + }; + let u399 = f399(g[0], g[1], g[2]); + let v399 = f399(g[1], g[2], g[0]); + let w399 = f399(g[2], g[0], g[1]); + let f = |v: f64, w: f64, ang: f64| (v * v + w * w) * ang.tan(); + push_bary( + &mut out, + "X399六次替换", + "X_{435}", + "X(435) = \\mathrm{Euler}\\ X_{399}\\text{-6th-subst};\\ (v^2+w^2)\\tan A\\ (u:v:w=X_{399})", + "X(435):帕里反射点的欧拉六次替换点(跳过直角)", + "X435", + f(v399, w399, g[0]), + f(w399, u399, g[1]), + f(u399, v399, g[2]), + ); + } + // X(436): Euler X(48)-7th-substitution; (u²+vw)tan A on sin2A; skip right + if !right { + let (u48, v48, w48) = ((2.0 * g[0]).sin(), (2.0 * g[1]).sin(), (2.0 * g[2]).sin()); + let f = |u: f64, v: f64, w: f64, ang: f64| (u * u + v * w) * ang.tan(); + push_bary( + &mut out, + "X48七次替换", + "X_{436}", + "X(436) = \\mathrm{Euler}\\ X_{48}\\text{-7th-subst};\\ (u^2+vw)\\tan A\\ (u:v:w=\\sin 2A)", + "X(436):X(48) 的欧拉七次替换点(跳过直角)", + "X436", + f(u48, v48, w48, g[0]), + f(v48, w48, u48, g[1]), + f(w48, u48, v48, g[2]), + ); + } + // X(437): Euler X(214)-8th-substitution; (u²+vw)tan A on (b+c−2a)(b²+c²−a²−bc); skip right/equil + if !right && !equil { + let u214 = (b + c - 2.0 * a) * (b * b + c * c - a * a - b * c); + let v214 = (c + a - 2.0 * b) * (c * c + a * a - b * b - c * a); + let w214 = (a + b - 2.0 * c) * (a * a + b * b - c * c - a * b); + let f = |u: f64, v: f64, w: f64, ang: f64| (u * u + v * w) * ang.tan(); + push_bary( + &mut out, + "X214八次替换", + "X_{437}", + "X(437) = \\mathrm{Euler}\\ X_{214}\\text{-8th-subst};\\ (u^2+vw)\\tan A\\ (u:v:w=X_{214})", + "X(437):X(214) 的欧拉八次替换点(跳过直角/等边)", + "X437", + f(u214, v214, w214, g[0]), + f(v214, w214, u214, g[1]), + f(w214, u214, v214, g[2]), + ); + } + // X(438): Euler X(204)-9th-substitution; u²+vw on tanA(tanB+tanC−tanA); skip right + if !right { + let (ta, tb, tc) = (g[0].tan(), g[1].tan(), g[2].tan()); + let u204 = ta * (tb + tc - ta); + let v204 = tb * (tc + ta - tb); + let w204 = tc * (ta + tb - tc); + let f = |u: f64, v: f64, w: f64| u * u + v * w; + push_bary( + &mut out, + "X204九次替换", + "X_{438}", + "X(438) = \\mathrm{Euler}\\ X_{204}\\text{-9th-subst};\\ u^2+vw\\ (u:v:w=X_{204})", + "X(438):X(204) 的欧拉九次替换点(跳过直角)", + "X438", + f(u204, v204, w204), + f(v204, w204, u204), + f(w204, u204, v204), + ); + } + // X(439): Euler X(193)-10th-substitution; bary (3a²−b²−c²)² + { + let u193 = 3.0 * a * a - b * b - c * c; + let v193 = 3.0 * b * b - c * c - a * a; + let w193 = 3.0 * c * c - a * a - b * b; + push_bary( + &mut out, + "X193十次替换", + "X_{439}", + "X(439) = \\mathrm{Euler}\\ X_{193}\\text{-10th-subst};\\ (3a^2-b^2-c^2)^2", + "X(439):X(193) 的欧拉十次替换点", + "X439", + u193 * u193, + v193 * v193, + w193 * w193, + ); + } + // X(440): complement of X(27); bary v+w on tanA/(b+c); skip right + if !right { + let (u28, v28, w28) = (g[0].tan() / (b + c), g[1].tan() / (c + a), g[2].tan() / (a + b)); + push_bary( + &mut out, + "X27补点", + "X_{440}", + "X(440) = \\mathrm{complement}(X_{27});\\ v+w\\ (u:v:w=\\tan A/(b+c))", + "X(440):X(27) 的补点(跳过直角)", + "X440", + v28 + w28, + w28 + u28, + u28 + v28, + ); + } + + // X(441)–X(450): complements X297/X21/X452; Euler∩X19X232; Euler X79-11th; crosspoint X98·X511; X2-Hirst X27/X21/X452; X3-Hirst X4 (ETC). + // X(441): complement of X(297); v+w on secA cos(A+ω); skip right/equil + if !right && !equil && area > 1e-12 { + let sum_sq = a * a + b * b + c * c; + let tan_w = (4.0 * area) / sum_sq; + let cos_w = 1.0 / (1.0 + tan_w * tan_w).sqrt(); + let sin_w = tan_w * cos_w; + let tril240 = |ang: f64| { + let cang = ang.cos(); + let sang = ang.sin(); + if cang.abs() < 1e-14 { + 0.0 + } else { + (cang * cos_w - sang * sin_w) / cang + } + }; + let (u240, v240, w240) = (tril240(g[0]), tril240(g[1]), tril240(g[2])); + push_bary( + &mut out, + "X297补点", + "X_{441}", + "X(441) = \\mathrm{complement}(X_{297});\\ v+w\\ (u:v:w=\\sec A\\cos(A+\\omega))", + "X(441):X(297) 的补点(跳过直角/等边)", + "X441", + v240 + w240, + w240 + u240, + u240 + v240, + ); + } + // X(442): complement of X(21); v+w on sinA/(cosB+cosC); =mid(X79,X191) + { + let da = cb + cc; + let db = cc + ca; + let dc = ca + cb; + if da.abs() > 1e-14 && db.abs() > 1e-14 && dc.abs() > 1e-14 { + let (u284, v284, w284) = (g[0].sin() / da, g[1].sin() / db, g[2].sin() / dc); + push_bary( + &mut out, + "席弗勒补点", + "X_{442}", + "X(442) = \\mathrm{complement}(X_{21});\\ v+w\\ (u:v:w=\\sin A/(\\cos B+\\cos C))", + "X(442):席弗勒点的补点(= mid(X79,X191))", + "X442", + v284 + w284, + w284 + u284, + u284 + v284, + ); + } + } + // X(443): complement of X(452); v+w on X(380) tril + { + let f380 = |aa: f64, bb: f64, cc_: f64| { + let s = bb + cc_; + (s - aa) * (3.0 * aa.powi(3) + s * (3.0 * aa * aa + (bb - cc_).powi(2) + aa * s)) + }; + let (u380, v380, w380) = (f380(a, b, c), f380(b, c, a), f380(c, a, b)); + push_bary( + &mut out, + "X452补点", + "X_{443}", + "X(443) = \\mathrm{complement}(X_{452});\\ v+w\\ (u:v:w=X_{380})", + "X(443):X(452) 的补点(X380 三线坐标)", + "X443", + v380 + w380, + w380 + u380, + u380 + v380, + ); + } + // X(444): Euler ∩ X(19)X(232); (v+w)sinA tanA on 1/(a²+bc); skip right + if !right { + let (u256, v256, w256) = ( + 1.0 / (a * a + b * c), + 1.0 / (b * b + c * a), + 1.0 / (c * c + a * b), + ); + let f = |v: f64, w: f64, ang: f64| (v + w) * ang.sin() * ang.tan(); + push_bary( + &mut out, + "X19X232欧拉截", + "X_{444}", + "X(444) = \\mathrm{Euler}\\cap X_{19}X_{232};\\ (v+w)\\sin A\\tan A\\ (u:v:w=1/(a^2+bc))", + "X(444):X(19)X(232) 与欧拉线的交点(跳过直角)", + "X444", + f(v256, w256, g[0]), + f(w256, u256, g[1]), + f(u256, v256, g[2]), + ); + } + // X(445): Euler X(79)-11th-substitution; (v+w)secA on 1/(1+2cosA); skip right + if !right { + let d0 = 1.0 + 2.0 * ca; + let d1 = 1.0 + 2.0 * cb; + let d2 = 1.0 + 2.0 * cc; + if d0.abs() > 1e-14 + && d1.abs() > 1e-14 + && d2.abs() > 1e-14 + && ca.abs() > 1e-14 + && cb.abs() > 1e-14 + && cc.abs() > 1e-14 + { + let (u79, v79, w79) = (1.0 / d0, 1.0 / d1, 1.0 / d2); + let f = |v: f64, w: f64, cang: f64| (v + w) / cang; + push_bary( + &mut out, + "X79十一次替换", + "X_{445}", + "X(445) = \\mathrm{Euler}\\ X_{79}\\text{-11th-subst};\\ (v+w)\\sec A\\ (u:v:w=1/(1+2\\cos A))", + "X(445):X(79) 的欧拉十一次替换点(跳过直角)", + "X445", + f(v79, w79, ca), + f(w79, u79, cb), + f(u79, v79, cc), + ); + } + } + // X(446): crosspoint of X(98) and X(511); a u(v²+w²) on sec(A+ω); skip equil + if !equil && area > 1e-12 { + let sum_sq = a * a + b * b + c * c; + let tan_w = (4.0 * area) / sum_sq; + let cos_w = 1.0 / (1.0 + tan_w * tan_w).sqrt(); + let sin_w = tan_w * cos_w; + let (sa, sb, sc) = (g[0].sin(), g[1].sin(), g[2].sin()); + let c0 = ca * cos_w - sa * sin_w; + let c1 = cb * cos_w - sb * sin_w; + let c2 = cc * cos_w - sc * sin_w; + if c0.abs() > 1e-14 && c1.abs() > 1e-14 && c2.abs() > 1e-14 { + let (u98, v98, w98) = (1.0 / c0, 1.0 / c1, 1.0 / c2); + let f = |u: f64, v: f64, w: f64, side: f64| side * u * (v * v + w * w); + push_bary( + &mut out, + "X98X511交叉", + "X_{446}", + "X(446) = \\mathrm{crosspoint}(X_{98},X_{511});\\ au(v^2+w^2)\\ (u:v:w=\\sec(A+\\omega))", + "X(446):塔里点与 X(511) 的交叉点(跳过等边)", + "X446", + f(u98, v98, w98, a), + f(v98, w98, u98, b), + f(w98, u98, v98, c), + ); + } + } + // X(447): X(2)-Hirst of X(27); u²−vw on tanA/(b+c); skip right/equil + if !right && !equil { + let (u28, v28, w28) = (g[0].tan() / (b + c), g[1].tan() / (c + a), g[2].tan() / (a + b)); + let f = |u: f64, v: f64, w: f64| u * u - v * w; + push_bary( + &mut out, + "重心HirstX27", + "X_{447}", + "X(447) = X_2\\text{-Hirst of }X_{27};\\ u^2-vw\\ (u:v:w=\\tan A/(b+c))", + "X(447):重心对 X(27) 的 Hirst 逆(跳过直角/等边)", + "X447", + f(u28, v28, w28), + f(v28, w28, u28), + f(w28, u28, v28), + ); + } + // X(448): X(2)-Hirst of X(21); u²−vw on sinA/(cosB+cosC); skip equil + if !equil { + let da = cb + cc; + let db = cc + ca; + let dc = ca + cb; + if da.abs() > 1e-14 && db.abs() > 1e-14 && dc.abs() > 1e-14 { + let (u284, v284, w284) = (g[0].sin() / da, g[1].sin() / db, g[2].sin() / dc); + let f = |u: f64, v: f64, w: f64| u * u - v * w; + push_bary( + &mut out, + "重心Hirst席弗勒", + "X_{448}", + "X(448) = X_2\\text{-Hirst of }X_{21};\\ u^2-vw\\ (u:v:w=\\sin A/(\\cos B+\\cos C))", + "X(448):重心对席弗勒点的 Hirst 逆(跳过等边)", + "X448", + f(u284, v284, w284), + f(v284, w284, u284), + f(w284, u284, v284), + ); + } + } + // X(449): X(2)-Hirst of X(452); u²−vw on X(380) tril; skip equil + if !equil { + let f380 = |aa: f64, bb: f64, cc_: f64| { + let s = bb + cc_; + (s - aa) * (3.0 * aa.powi(3) + s * (3.0 * aa * aa + (bb - cc_).powi(2) + aa * s)) + }; + let (u380, v380, w380) = (f380(a, b, c), f380(b, c, a), f380(c, a, b)); + let f = |u: f64, v: f64, w: f64| u * u - v * w; + push_bary( + &mut out, + "重心HirstX452", + "X_{449}", + "X(449) = X_2\\text{-Hirst of }X_{452};\\ u^2-vw\\ (u:v:w=X_{380})", + "X(449):重心对 X(452) 的 Hirst 逆(跳过等边)", + "X449", + f(u380, v380, w380), + f(v380, w380, u380), + f(w380, u380, v380), + ); + } + // X(450): X(3)-Hirst of X(4); tanA(cos⁴A−(cosB cosC)²); skip right/equil + if !right && !equil { + let h = |ang: f64, cb_: f64, cc_: f64| { + let ca_ = ang.cos(); + ang.tan() * (ca_.powi(4) - (cb_ * cc_).powi(2)) + }; + push_bary( + &mut out, + "外心Hirst垂心", + "X_{450}", + "X(450) = X_3\\text{-Hirst of }X_4;\\ \\tan A(\\cos^4 A-(\\cos B\\cos C)^2)", + "X(450):外心对垂心的 Hirst 逆(跳过直角/等边)", + "X450", + h(g[0], cb, cc), + h(g[1], cc, ca), + h(g[2], ca, cb), + ); + } + + // X(451)–X(460): X4-Hirst-pair X451; X452; Alshain; Euler 11th X155/X159/X195/X399; X458; X459; Antares (ETC). + // X(451): X(4)-Hirst inverse of X(424); u tan A on X(191) tril; skip right + if !right { + let f191 = |aa: f64, bb: f64, cc_: f64| { + (bb + cc_ - aa) * (bb * cc_ + cc_ * aa + aa * bb) + bb.powi(3) + cc_.powi(3) + - aa.powi(3) + }; + let (u191, v191, w191) = (f191(a, b, c), f191(b, c, a), f191(c, a, b)); + push_bary( + &mut out, + "垂心HirstX424", + "X_{451}", + "X(451) = X_4\\text{-Hirst of }X_{424};\\ u\\tan A\\ (u:v:w=X_{191})", + "X(451):垂心对 X(424) 的 Hirst 逆(X191 三线坐标;跳过直角)", + "X451", + u191 * g[0].tan(), + v191 * g[1].tan(), + w191 * g[2].tan(), + ); + } + // X(452): X(2)-Hirst inverse of X(449); bary = X(380) tril; =anticomplement(X443) + { + let f380 = |aa: f64, bb: f64, cc_: f64| { + let s = bb + cc_; + (s - aa) * (3.0 * aa.powi(3) + s * (3.0 * aa * aa + (bb - cc_).powi(2) + aa * s)) + }; + push_bary( + &mut out, + "重心HirstX449", + "X_{452}", + "X(452) = X_2\\text{-Hirst of }X_{449} = \\mathrm{anticomplement}(X_{443});\\ u:v:w=X_{380}\\text{ tril}", + "X(452):重心对 X(449) 的 Hirst 逆(= X(443) 的反补;X380 三线坐标)", + "X452", + f380(a, b, c), + f380(b, c, a), + f380(c, a, b), + ); + } + // X(453): Alshain; sinA (cosB+cosC−cosA)²/(cosB+cosC) + { + let da = cb + cc; + let db = cc + ca; + let dc = ca + cb; + if da.abs() > 1e-14 && db.abs() > 1e-14 && dc.abs() > 1e-14 { + let f = |sang: f64, cang: f64, den: f64, cb_: f64, cc_: f64| { + sang * (cb_ + cc_ - cang).powi(2) / den + }; + push_bary( + &mut out, + "阿尔沙因点", + "X_{453}", + "X(453) = \\mathrm{Alshain};\\ \\sin A(\\cos B+\\cos C-\\cos A)^2/(\\cos B+\\cos C)", + "X(453):阿尔沙因点", + "X453", + f(g[0].sin(), ca, da, cb, cc), + f(g[1].sin(), cb, db, cc, ca), + f(g[2].sin(), cc, dc, ca, cb), + ); + } + } + // X(454): Euler X(155)-11th-substitution; u² tan A on X(155) tril; skip right + if !right { + let u155 = ca * (cb * cb + cc * cc - ca * ca); + let v155 = cb * (cc * cc + ca * ca - cb * cb); + let w155 = cc * (ca * ca + cb * cb - cc * cc); + push_bary( + &mut out, + "X155十一次替换", + "X_{454}", + "X(454) = \\mathrm{Euler}\\ X_{155}\\text{-11th-subst};\\ u^2\\tan A\\ (u:v:w=X_{155})", + "X(454):X(155) 的欧拉十一次替换点(跳过直角)", + "X454", + u155 * u155 * g[0].tan(), + v155 * v155 * g[1].tan(), + w155 * w155 * g[2].tan(), + ); + } + // X(455): Euler X(159)-11th-substitution; u² tan A on X(159) tril; skip right + if !right { + let f159 = |aa: f64, bb: f64, cc_: f64, ang_a: f64, ang_b: f64, ang_c: f64| { + aa * ((a * a + b * b + c * c) * (2.0 * ang_a).sin() + + (cc_ * cc_ - bb * bb - aa * aa) * (2.0 * ang_b).sin() + + (bb * bb - cc_ * cc_ - aa * aa) * (2.0 * ang_c).sin()) + }; + let u159 = f159(a, b, c, g[0], g[1], g[2]); + let v159 = f159(b, c, a, g[1], g[2], g[0]); + let w159 = f159(c, a, b, g[2], g[0], g[1]); + push_bary( + &mut out, + "X159十一次替换", + "X_{455}", + "X(455) = \\mathrm{Euler}\\ X_{159}\\text{-11th-subst};\\ u^2\\tan A\\ (u:v:w=X_{159})", + "X(455):X(159) 的欧拉十一次替换点(跳过直角)", + "X455", + u159 * u159 * g[0].tan(), + v159 * v159 * g[1].tan(), + w159 * w159 * g[2].tan(), + ); + } + // X(456): Euler X(195)-11th-substitution; u² tan A on X(195) tril; skip right + if !right && area > 1e-12 { + let (sa, sb, sc) = (g[0].sin(), g[1].sin(), g[2].sin()); + if sa.abs() > 1e-14 && sb.abs() > 1e-14 && sc.abs() > 1e-14 { + let cot_w = (a * a + b * b + c * c) / (4.0 * area); + let bary195 = |ang: f64, cang: f64, sang: f64| { + let cot = cang / sang; + 4.0 * (2.0 * ang).cos() + cot * cot - cot * cot_w + }; + let u195 = bary195(g[0], ca, sa) / a; + let v195 = bary195(g[1], cb, sb) / b; + let w195 = bary195(g[2], cc, sc) / c; + push_bary( + &mut out, + "X195十一次替换", + "X_{456}", + "X(456) = \\mathrm{Euler}\\ X_{195}\\text{-11th-subst};\\ u^2\\tan A\\ (u:v:w=X_{195})", + "X(456):X(195) 的欧拉十一次替换点(跳过直角)", + "X456", + u195 * u195 * g[0].tan(), + v195 * v195 * g[1].tan(), + w195 * w195 * g[2].tan(), + ); + } + } + // X(457): Euler X(399)-11th-substitution; u² tan A on Parry tril; skip right + if !right { + let f399 = |ang_a: f64, ang_b: f64, ang_c: f64| { + 5.0 * ang_a.cos() + - 4.0 * ang_b.cos() * ang_c.cos() + - 8.0 * ang_b.sin() * ang_c.sin() * (2.0 * ang_a).cos() + }; + let u399 = f399(g[0], g[1], g[2]); + let v399 = f399(g[1], g[2], g[0]); + let w399 = f399(g[2], g[0], g[1]); + push_bary( + &mut out, + "X399十一次替换", + "X_{457}", + "X(457) = \\mathrm{Euler}\\ X_{399}\\text{-11th-subst};\\ u^2\\tan A\\ (u:v:w=X_{399})", + "X(457):帕里反射点的欧拉十一次替换点(跳过直角)", + "X457", + u399 * u399 * g[0].tan(), + v399 * v399 * g[1].tan(), + w399 * w399 * g[2].tan(), + ); + } + // X(458): Euler ∩ X(76)X(275); tanA+cot ω; skip right + if !right && area > 1e-12 { + let cot_w = (a * a + b * b + c * c) / (4.0 * area); + push_bary( + &mut out, + "X76X275欧拉截", + "X_{458}", + "X(458) = \\mathrm{Euler}\\cap X_{76}X_{275};\\ \\tan A+\\cot\\omega", + "X(458):X(76)X(275) 与欧拉线的交点(跳过直角)", + "X458", + g[0].tan() + cot_w, + g[1].tan() + cot_w, + g[2].tan() + cot_w, + ); + } + // X(459): X(253)-Ceva of X(4); 1/(tanB tanC−2) + { + let tb = g[1].tan(); + let tc = g[2].tan(); + let ta = g[0].tan(); + let da = tb * tc - 2.0; + let db = tc * ta - 2.0; + let dc = ta * tb - 2.0; + if da.abs() > 1e-14 + && db.abs() > 1e-14 + && dc.abs() > 1e-14 + && ta.is_finite() + && tb.is_finite() + && tc.is_finite() + { + push_bary( + &mut out, + "X253垂心塞瓦", + "X_{459}", + "X(459) = X_{253}\\text{-Ceva of }X_4;\\ 1/(\\tan B\\tan C-2)", + "X(459):X(253) 对垂心的塞瓦共轭", + "X459", + 1.0 / da, + 1.0 / db, + 1.0 / dc, + ); + } + } + // X(460): Antares; tanA[a²(2a²−b²−c²)+(b²−c²)²]; skip right/equil + if !right && !equil { + let f = |aa: f64, bb: f64, cc_: f64| { + let a2 = aa * aa; + let b2 = bb * bb; + let c2 = cc_ * cc_; + a2 * (2.0 * a2 - b2 - c2) + (b2 - c2) * (b2 - c2) + }; + push_bary( + &mut out, + "安塔瑞斯点", + "X_{460}", + "X(460) = \\mathrm{Antares}=X_4\\cdot X_{230};\\ \\tan A[a^2(2a^2-b^2-c^2)+(b^2-c^2)^2]", + "X(460):安塔瑞斯点(X(4)·X(230) 重心积;跳过直角/等边)", + "X460", + g[0].tan() * f(a, b, c), + g[1].tan() * f(b, c, a), + g[2].tan() * f(c, a, b), + ); + } + + } + + + // Ceva: concurrent cevians AD, BE, CF satisfy (BD/DC)(CE/EA)(AF/FB) = 1. + // Emit the classical families that are always concurrent from three sides alone. + { + // Medians: each ratio = 1. + out.push(fact( + "塞瓦与梅涅劳斯", + "\\Pi_{\\text{Ceva}}^{\\text{中线}}", + "1".into(), + Some(1.0), + "\\frac{BD}{DC}\\cdot\\frac{CE}{EA}\\cdot\\frac{AF}{FB} = 1\\cdot 1\\cdot 1", + "塞瓦定理:三中线交于重心", + )); + // Angle bisectors: BD/DC = c/b, CE/EA = a/c, AF/FB = b/a. + let bis = (c / b) * (a / c) * (b / a); + out.push(fact( + "塞瓦与梅涅劳斯", + "\\Pi_{\\text{Ceva}}^{\\text{角平分线}}", + nice(bis), + Some(bis), + &format!( + "\\frac{{c}}{{b}}\\cdot\\frac{{a}}{{c}}\\cdot\\frac{{b}}{{a}} = {}", + nice(bis) + ), + "塞瓦定理:三角平分线交于内心", + )); + // Altitudes: feet of the three altitudes; directed product = 1. + let mut alt_prod = 1.0; + let mut alt_ok = true; + for i in 0..3 { + let (p0, q1, q2) = (v[i], v[(i + 1) % 3], v[(i + 2) % 3]); + let dx = q2[0] - q1[0]; + let dy = q2[1] - q1[1]; + let den = dx * dx + dy * dy; + if den < 1e-18 { + alt_ok = false; + break; + } + let t = ((p0[0] - q1[0]) * dx + (p0[1] - q1[1]) * dy) / den; + let foot = [q1[0] + t * dx, q1[1] + t * dy]; + match directed_ratio(q1, q2, foot) { + Some(r) => alt_prod *= r, + None => { + alt_ok = false; + break; + } + } + } + if alt_ok { + out.push(fact( + "塞瓦与梅涅劳斯", + "\\Pi_{\\text{Ceva}}^{\\text{高}}", + nice(alt_prod), + Some(alt_prod), + "\\frac{BD}{DC}\\cdot\\frac{CE}{EA}\\cdot\\frac{AF}{FB} = 1", + "塞瓦定理:三高交于垂心(有向比)", + )); + } + // Gergonne: incircle contact points. BD = s−b, DC = s−c, … + let (ta, tb, tc) = (p - a, p - b, p - c); + if ta > 1e-12 && tb > 1e-12 && tc > 1e-12 { + let ger = (tb / tc) * (tc / ta) * (ta / tb); + out.push(fact( + "塞瓦与梅涅劳斯", + "\\Pi_{\\text{Ceva}}^{\\text{切点}}", + nice(ger), + Some(ger), + "\\frac{s-b}{s-c}\\cdot\\frac{s-c}{s-a}\\cdot\\frac{s-a}{s-b} = 1", + "塞瓦定理:内切圆切点连线交于格尔巩点", + )); + // Nagel / extouch: BD = s−c, DC = s−b (swap), product still 1. + let nag = (tc / tb) * (ta / tc) * (tb / ta); + out.push(fact( + "塞瓦与梅涅劳斯", + "\\Pi_{\\text{Ceva}}^{\\text{旁切}}", + nice(nag), + Some(nag), + "\\frac{s-c}{s-b}\\cdot\\frac{s-a}{s-c}\\cdot\\frac{s-b}{s-a} = 1", + "塞瓦定理:旁切圆切点连线交于奈格尔点", + )); + } + } + + // Menelaus: a transversal DEF with D on BC, E on CA, F on AB (or extensions) + // satisfies (BD/DC)(CE/EA)(AF/FB) = −1 (directed). Construct D,E then F = DE ∩ AB. + { + let d_pt = section_point(pb, pc, 2.0, 1.0); // BD:DC = 2:1 + let e_pt = section_point(pc, pa, 2.0, 1.0); // CE:EA = 2:1 + if let Some(f_pt) = line_line_isect2(d_pt, e_pt, pa, pb) { + if let (Some(r_bc), Some(r_ca), Some(r_ab)) = ( + directed_ratio(pb, pc, d_pt), + directed_ratio(pc, pa, e_pt), + directed_ratio(pa, pb, f_pt), + ) { + let prod = r_bc * r_ca * r_ab; + out.push(fact( + "塞瓦与梅涅劳斯", + "\\Pi_{\\text{Menelaus}}", + nice(prod), + Some(prod), + &format!( + "\\frac{{BD}}{{DC}}\\cdot\\frac{{CE}}{{EA}}\\cdot\\frac{{AF}}{{FB}} = {}\\cdot{}\\cdot{} = {}", + nice(r_bc), + nice(r_ca), + nice(r_ab), + nice(prod) + ), + "梅涅劳斯定理:横截线分三边的有向比之积为 −1", + ).with(Shape::Segment { + x1: d_pt[0], + y1: d_pt[1], + x2: e_pt[0], + y2: e_pt[1], + label: "Menelaus 横截线".into(), + })); + out.push(fact( + "塞瓦与梅涅劳斯", + "F_{\\text{Menelaus}}", + pt(f_pt[0], f_pt[1]), + None, + "DE \\cap AB", + "横截线与 AB(延长线)的交点", + ).with(Shape::Point { + x: f_pt[0], + y: f_pt[1], + label: "F Menelaus".into(), + })); + } + } + } + + // Stewart: for a cevian AD to side a=BC with BD=m, DC=n (m+n=a), + // a(d² + mn) = b²m + c²n. Verify for medians and angle bisectors. + { + for i in 0..3 { + let (aa, bb, cc) = (s[i], s[(i + 1) % 3], s[(i + 2) % 3]); + // median: m = n = a/2 + let m = aa / 2.0; + let n = aa / 2.0; + let d = 0.5 * (2.0 * bb * bb + 2.0 * cc * cc - aa * aa).max(0.0).sqrt(); + let lhs = bb * bb * m + cc * cc * n; + let rhs = aa * (d * d + m * n); + if (lhs - rhs).abs() < 1e-6 * (1.0 + lhs.abs()) { + out.push(fact( + "斯特瓦尔特定理", + &format!("\\text{{Stewart}}_{{m_{}}}", SIDE[i]), + nice(lhs), + Some(lhs), + &format!( + "{b}^2\\cdot\\frac{{{a}}}{{2}} + {c}^2\\cdot\\frac{{{a}}}{{2}} = {a}\\left(m_{s}^2 + \\left(\\frac{{{a}}}{{2}}\\right)^2\\right)", + a = SIDE[i], + b = SIDE[(i + 1) % 3], + c = SIDE[(i + 2) % 3], + s = SIDE[i], + ), + "斯特瓦尔特定理(中线)", + )); + } + // angle bisector: BD:DC = c:b → m = c/(b+c)·a, n = b/(b+c)·a + let den = bb + cc; + if den > 1e-12 { + let m = cc / den * aa; + let n = bb / den * aa; + let d = 2.0 * bb * cc * (g[i] / 2.0).cos() / den; + let lhs = bb * bb * m + cc * cc * n; + let rhs = aa * (d * d + m * n); + if (lhs - rhs).abs() < 1e-6 * (1.0 + lhs.abs()) { + out.push(fact( + "斯特瓦尔特定理", + &format!("\\text{{Stewart}}_{{t_{}}}", SIDE[i]), + nice(lhs), + Some(lhs), + &format!( + "{b}^2 m + {c}^2 n = {a}(t_{s}^2 + mn),\\ m:n = {c}:{b}", + a = SIDE[i], + b = SIDE[(i + 1) % 3], + c = SIDE[(i + 2) % 3], + s = SIDE[i], + ), + "斯特瓦尔特定理(角平分线)", + )); + } + } + // altitude: signed segments along side i (foot may lie on the extension) + let bpt = v[(i + 1) % 3]; + let cpt = v[(i + 2) % 3]; + let apt = v[i]; + let (dx, dy) = (cpt[0] - bpt[0], cpt[1] - bpt[1]); + let den2 = dx * dx + dy * dy; + if den2 > 1e-18 && aa > 1e-12 { + let t = ((apt[0] - bpt[0]) * dx + (apt[1] - bpt[1]) * dy) / den2; + let m = t * aa; + let nseg = (1.0 - t) * aa; + let d = 2.0 * area / aa; + let lhs = bb * bb * m + cc * cc * nseg; + let rhs = aa * (d * d + m * nseg); + if (lhs - rhs).abs() < 1e-6 * (1.0 + lhs.abs()) { + out.push(fact( + "斯特瓦尔特定理", + &format!("\\text{{Stewart}}_{{h_{}}}", SIDE[i]), + nice(lhs), + Some(lhs), + &format!( + "{b}^2 m + {c}^2 n = {a}(h_{s}^2 + mn)", + a = SIDE[i], + b = SIDE[(i + 1) % 3], + c = SIDE[(i + 2) % 3], + s = SIDE[i], + ), + "斯特瓦尔特定理(高,有向线段)", + )); + } + } + } + } + + // Van Obel (incenter form): AI/ID = (b+c)/a. + { + for i in 0..3 { + let (aa, bb, cc) = (s[i], s[(i + 1) % 3], s[(i + 2) % 3]); + let den = bb + cc; + if den < 1e-12 || aa < 1e-12 { + continue; + } + let foot = [ + (bb * v[(i + 1) % 3][0] + cc * v[(i + 2) % 3][0]) / den, + (bb * v[(i + 1) % 3][1] + cc * v[(i + 2) % 3][1]) / den, + ]; + let ai = dist2(v[i], inc); + let idlen = dist2(inc, foot); + if idlen < 1e-12 { + continue; + } + let ratio = ai / idlen; + let expect = den / aa; + if (ratio - expect).abs() < 1e-6 * (1.0 + expect) { + out.push(fact( + "凡奥培尔定理", + &format!("\\dfrac{{|{n}I|}}{{|ID_{{{s}}}|}}", n = names[i], s = SIDE[i]), + nice(ratio), + Some(ratio), + &format!( + "\\dfrac{{{b} + {c}}}{{{a}}} = \\dfrac{{AF}}{{FB}} + \\dfrac{{AE}}{{EC}}", + a = SIDE[i], + b = SIDE[(i + 1) % 3], + c = SIDE[(i + 2) % 3], + ), + "凡奥培尔定理(内心):角平分线分对边的内分比之和", + ).with(Shape::Segment { + x1: v[i][0], + y1: v[i][1], + x2: foot[0], + y2: foot[1], + label: format!("{}I", names[i]), + })); + } + } + } + + // Apollonius circle at vertex i. + { + for i in 0..3 { + let b_pt = v[(i + 1) % 3]; + let c_pt = v[(i + 2) % 3]; + let side_to_b = s[(i + 2) % 3]; + let side_to_c = s[(i + 1) % 3]; + let opp = s[i]; + if side_to_c < 1e-12 { + continue; + } + let k = side_to_b / side_to_c; + if (k - 1.0).abs() < 1e-9 { + let mid = [(b_pt[0] + c_pt[0]) / 2.0, (b_pt[1] + c_pt[1]) / 2.0]; + out.push(fact( + "阿波罗尼奥斯圆", + &format!("\\mathcal{{A}}_{{{}}}", names[i]), + "\\text{中垂线}".into(), + None, + &format!( + "\\frac{{|P{nb}|}}{{|P{nc}|}} = 1", + nb = names[(i + 1) % 3], + nc = names[(i + 2) % 3], + ), + "阿波罗尼奥斯圆退化为对边的中垂线", + ).with(Shape::Point { + x: mid[0], + y: mid[1], + label: format!("A_{} 中点", names[i]), + })); + continue; + } + let k2 = k * k; + let o = [ + (k2 * c_pt[0] - b_pt[0]) / (k2 - 1.0), + (k2 * c_pt[1] - b_pt[1]) / (k2 - 1.0), + ]; + let rad = (k * opp / (k2 - 1.0).abs()).abs(); + let d_v = dist2(v[i], o); + if (d_v - rad).abs() > 1e-4 * (1.0 + rad) { + continue; + } + out.push(fact( + "阿波罗尼奥斯圆", + &format!("\\mathcal{{A}}_{{{}}}", names[i]), + format!( + "O=\\left({},\\ {}\\right),\\ R={}", + nice_short(o[0]), + nice_short(o[1]), + nice(rad) + ), + Some(rad), + &format!( + "\\frac{{|P{nb}|}}{{|P{nc}|}} = \\frac{{{c}}}{{{b}}} = {k}", + nb = names[(i + 1) % 3], + nc = names[(i + 2) % 3], + b = SIDE[(i + 1) % 3], + c = SIDE[(i + 2) % 3], + k = nice(k), + ), + "阿波罗尼奥斯圆:到两定点距离成定比的轨迹", + ).with(Shape::Circle { + cx: o[0], + cy: o[1], + r: rad, + label: format!("阿氏圆 {}", names[i]), + })); + } + } + + let mut kinds: Vec = Vec::new(); + let maxg = g.iter().cloned().fold(0.0, f64::max); + if near(maxg, PI / 2.0) { + let i = g.iter().position(|x| near(*x, PI / 2.0)).unwrap(); + kinds.push(format!("直角三角形(∠{} = 90°)", [na, nb, nc][i])); + } else if maxg > PI / 2.0 { + kinds.push("钝角三角形".into()); + } else { + kinds.push("锐角三角形".into()); + } + if near(a, b) && near(b, c) { + kinds.push("等边".into()); + } else if near(a, b) || near(b, c) || near(a, c) { + kinds.push("等腰".into()); + } + out.push(fact("形状判断", "\\text{类型}", format!("\\text{{{}}}", kinds.join("、")), None, "", "由三个角与三边比较")); + // Viviani (equilateral): sum of distances to the three sides equals the altitude; at I this is 3r = h. + if near(a, b) && near(b, c) { + let h = 2.0 * area / a; + let sum = 3.0 * r; + out.push(fact( + "维维安尼定理", + "d_a + d_b + d_c", + nice(sum), + Some(sum), + "d_a + d_b + d_c = h", + "等边三角形:内点到三边距离之和等于高(内心处为 3r)", + )); + out.push(fact( + "维维安尼定理", + "h", + nice(h), + Some(h), + "3r = h", + "等边:内切圆半径是高的三分之一", + )); + } + if near(maxg, PI / 2.0) { + out.push(fact("形状判断", "R", nice(rr), Some(rr), "R = \\frac{\\text{斜边}}{2}", "直角三角形外心在斜边中点")); + // altitude onto the hypotenuse and the two leg projections (射影定理). + // Right angle at vertex ri; the hypotenuse joins vertices u and w. Leg s[u] + // (= V_ri→V_w) projects onto the segment from V_w to the foot F, leg s[w] + // onto the segment from V_u to F. + let ri = g.iter().position(|x| near(*x, PI / 2.0)).unwrap(); + let (u, w) = ((ri + 1) % 3, (ri + 2) % 3); + let (lu, lw, hyp) = (s[u], s[w], s[ri]); + let t = lw * lw / (hyp * hyp); // |V_u F| = s[w]²/hyp + let foot = [v[u][0] + t * (v[w][0] - v[u][0]), v[u][1] + t * (v[w][1] - v[u][1])]; + let hc = lu * lw / hyp; + out.push(fact("高与中线", "\\text{斜边上的高}", nice(hc), Some(hc), &format!("\\frac{{{a}\\cdot {b}}}{{{c}}}", a = SIDE[u], b = SIDE[w], c = SIDE[ri]), "等面积:½·直角边₁·直角边₂ = ½·斜边·h").with(Shape::Segment { x1: v[ri][0], y1: v[ri][1], x2: foot[0], y2: foot[1], label: "斜边上的高".into() })); + out.push(fact("射影定理", &format!("\\text{{射影}}_{{{}}}", SIDE[u]), nice(lu * lu / hyp), Some(lu * lu / hyp), &format!("\\frac{{{u}^2}}{{{c}}}", u = SIDE[u], c = SIDE[ri]), "直角边² = 斜边 × 这条直角边的射影").with(Shape::Segment { x1: v[w][0], y1: v[w][1], x2: foot[0], y2: foot[1], label: format!("射影 {}", SIDE[u]) })); + out.push(fact("射影定理", &format!("\\text{{射影}}_{{{}}}", SIDE[w]), nice(lw * lw / hyp), Some(lw * lw / hyp), &format!("\\frac{{{w}^2}}{{{c}}}", w = SIDE[w], c = SIDE[ri]), "直角边² = 斜边 × 这条直角边的射影").with(Shape::Segment { x1: v[u][0], y1: v[u][1], x2: foot[0], y2: foot[1], label: format!("射影 {}", SIDE[w]) })); + } + // Morley: adjacent angle trisectors meet at an equilateral triangle. + { + let unit = |u: [f64; 2]| { + let l = u[0].hypot(u[1]); + if l < 1e-12 { + [0.0, 0.0] + } else { + [u[0] / l, u[1] / l] + } + }; + let rot = |u: [f64; 2], ang: f64| { + let (c, s) = (ang.cos(), ang.sin()); + [u[0] * c - u[1] * s, u[0] * s + u[1] * c] + }; + let sang = |u: [f64; 2], w: [f64; 2]| (u[0] * w[1] - u[1] * w[0]).atan2(u[0] * w[0] + u[1] * w[1]); + let mut mverts = [[0.0; 2]; 3]; + let mut ok = true; + for s in 0..3 { + let i = s; + let j = (s + 1) % 3; + let k = (s + 2) % 3; + let (pi, pj) = (v[i], v[j]); + let u_ij = unit([pj[0] - pi[0], pj[1] - pi[1]]); + let u_ik = unit([v[k][0] - pi[0], v[k][1] - pi[1]]); + let ai = sang(u_ij, u_ik); + let dir_i = rot(u_ij, ai / 3.0); + let u_ji = unit([pi[0] - pj[0], pi[1] - pj[1]]); + let u_jk = unit([v[k][0] - pj[0], v[k][1] - pj[1]]); + let aj = sang(u_ji, u_jk); + let dir_j = rot(u_ji, aj / 3.0); + let mat = vec![vec![dir_i[0], -dir_j[0]], vec![dir_i[1], -dir_j[1]]]; + if let Some(st) = solve_linear_pub(mat, vec![pj[0] - pi[0], pj[1] - pi[1]]) { + if st[0].is_finite() && st[0] > 1e-9 { + mverts[s] = [pi[0] + st[0] * dir_i[0], pi[1] + st[0] * dir_i[1]]; + } else { + ok = false; + } + } else { + ok = false; + } + } + if ok { + let e01 = dist2(mverts[0], mverts[1]); + let e12 = dist2(mverts[1], mverts[2]); + let e20 = dist2(mverts[2], mverts[0]); + if e01 > 1e-9 && near(e01, e12) && near(e12, e20) { + out.push(fact( + "莫利定理", + "\\text{Morley}", + "\\text{等边}".into(), + None, + "\\text{邻角三等分线交点}", + "相邻角的三等分线交出等边三角形", + )); + out.push(fact( + "莫利定理", + "|M_0M_1|", + nice(e01), + Some(e01), + "", + "莫利三角形边长", + ).with(Shape::Segment { + x1: mverts[0][0], + y1: mverts[0][1], + x2: mverts[1][0], + y2: mverts[1][1], + label: "Morley".into(), + })); + for (ii, mv) in mverts.iter().enumerate() { + out.push(fact( + "莫利定理", + &format!("M_{}", ii), + pt(mv[0], mv[1]), + None, + "", + "莫利三角形顶点", + ).with(Shape::Point { + x: mv[0], + y: mv[1], + label: format!("M{}", ii), + })); + } + } + } + } + // Napoleon: outward equilateral on each side → centroids form an equilateral triangle. + { + // Orient: positive cross (pa→pb)×(pa→pc) means CCW; outward on edge B→C is clockwise (−60°). + let orient = (pb[0] - pa[0]) * (pc[1] - pa[1]) - (pb[1] - pa[1]) * (pc[0] - pa[0]); + let sign = if orient >= 0.0 { -1.0 } else { 1.0 }; // outward + let cos60 = 0.5; + let sin60 = sign * (3.0f64).sqrt() / 2.0; + let rot = |u: [f64; 2]| -> [f64; 2] { + [u[0] * cos60 - u[1] * sin60, u[0] * sin60 + u[1] * cos60] + }; + let mut cents = [[0.0; 2]; 3]; + for i in 0..3 { + let b = v[(i + 1) % 3]; + let c = v[(i + 2) % 3]; + let w = rot([c[0] - b[0], c[1] - b[1]]); + let dpt = [b[0] + w[0], b[1] + w[1]]; + cents[i] = [(b[0] + c[0] + dpt[0]) / 3.0, (b[1] + c[1] + dpt[1]) / 3.0]; + } + let e01 = dist2(cents[0], cents[1]); + let e12 = dist2(cents[1], cents[2]); + let e20 = dist2(cents[2], cents[0]); + if e01 > 1e-12 && near(e01, e12) && near(e12, e20) { + out.push(fact( + "拿破仑定理", + "\\text{Napoleon}", + "\\text{等边}".into(), + None, + "G_aG_bG_c\\text{ 等边}", + "各边向外作等边三角形,其重心连成等边三角形", + )); + out.push(fact( + "拿破仑定理", + "|G_aG_b|", + nice(e01), + Some(e01), + "", + "拿破仑三角形边长", + ).with(Shape::Segment { + x1: cents[0][0], y1: cents[0][1], x2: cents[1][0], y2: cents[1][1], + label: "Napoleon".into(), + })); + for (i, gcent) in cents.iter().enumerate() { + out.push(fact( + "拿破仑定理", + &format!("G_{{{}}}", SIDE[i]), + pt(gcent[0], gcent[1]), + None, + "", + "外侧等边三角形的重心", + ).with(Shape::Point { x: gcent[0], y: gcent[1], label: format!("G{}", SIDE[i]) })); + } + } + } + + // Simson line: feet from a circumcircle point P onto the three sides are collinear. + // Take P = antipode of A on the circumcircle (always on (O)). + { + let p_sim = [2.0 * circ[0] - pa[0], 2.0 * circ[1] - pa[1]]; + let foot_line = |p: [f64; 2], q1: [f64; 2], q2: [f64; 2]| -> Option<[f64; 2]> { + let dx = q2[0] - q1[0]; + let dy = q2[1] - q1[1]; + let den = dx * dx + dy * dy; + if den < 1e-18 { + return None; + } + let t = ((p[0] - q1[0]) * dx + (p[1] - q1[1]) * dy) / den; + Some([q1[0] + t * dx, q1[1] + t * dy]) + }; + // feet onto BC, CA, AB + let sides = [(pb, pc), (pc, pa), (pa, pb)]; + let mut feet = [[0.0; 2]; 3]; + let mut ok = true; + for (i, (q1, q2)) in sides.into_iter().enumerate() { + if let Some(f) = foot_line(p_sim, q1, q2) { + feet[i] = f; + } else { + ok = false; + } + } + if ok { + let cross = (feet[1][0] - feet[0][0]) * (feet[2][1] - feet[0][1]) + - (feet[1][1] - feet[0][1]) * (feet[2][0] - feet[0][0]); + if cross.abs() < 1e-6 * (1.0 + rr) { + out.push(fact( + "西姆松线", + "\\text{Simson}", + "\\text{共线}".into(), + None, + "P\\in\\odot(O)\\Rightarrow\\text{三垂足共线}", + "西姆松线:外接圆上一点到三边垂足共线(P 取 A 的对径点)", + )); + out.push(fact( + "西姆松线", + "P_{\\text{Simson}}", + pt(p_sim[0], p_sim[1]), + None, + "P = 2O - A", + "外接圆上 A 的对径点", + ).with(Shape::Point { + x: p_sim[0], + y: p_sim[1], + label: "P Simson".into(), + })); + for (i, f) in feet.iter().enumerate() { + let side_name = SIDE[i]; + out.push(fact( + "西姆松线", + &format!("F_{{{}}}", side_name), + pt(f[0], f[1]), + None, + "", + &format!("P 到边 {} 的垂足", side_name), + ).with(Shape::Point { + x: f[0], + y: f[1], + label: format!("F{}", side_name), + })); + } + // line through F_a and F_b + let (f0, f1) = (feet[0], feet[1]); + let la = f1[1] - f0[1]; + let lb = f0[0] - f1[0]; + let lc = la * f0[0] + lb * f0[1]; + if la.hypot(lb) > 1e-12 { + out.push(fact( + "西姆松线", + "l_{\\text{Simson}}", + line_eq_latex(la, lb, lc), + None, + "三垂足所在直线", + "西姆松线方程", + ).with(Shape::Segment { + x1: f0[0], + y1: f0[1], + x2: f1[0], + y2: f1[1], + label: "Simson".into(), + })); + } + } + } + } + + // Trilinear polar of the incenter I (barycentrics a:b:c): X/a + Y/b + Z/c = 0. + { + let mut poles: Vec<[f64; 2]> = Vec::new(); + // Intersection with side opposite vertex i (bary coord of that vertex = 0). + for i in 0..3 { + let sj = s[(i + 1) % 3]; + let sk = s[(i + 2) % 3]; + // On side jk: α_i = 0, β_j + β_k = 1, β_j/sj + β_k/sk = 0. + if (sj - sk).abs() < 1e-12 { + continue; // polar ∥ this side + } + let bj = sj / (sj - sk); + let bk = 1.0 - bj; + let pj = v[(i + 1) % 3]; + let pk = v[(i + 2) % 3]; + let pt_i = [bj * pj[0] + bk * pk[0], bj * pj[1] + bk * pk[1]]; + if pt_i[0].is_finite() && pt_i[1].is_finite() { + poles.push(pt_i); + } + } + out.push(fact( + "三线极线", + "\\tau(I)", + "\\frac{X}{a}+\\frac{Y}{b}+\\frac{Z}{c}=0".into(), + None, + "I=(a:b:c)_{\\text{bary}}", + "内心的三线极线(重心坐标方程)", + )); + if poles.len() >= 2 { + let (p0, p1) = (poles[0], poles[1]); + let la = p1[1] - p0[1]; + let lb = p0[0] - p1[0]; + let lc = la * p0[0] + lb * p0[1]; + if la.hypot(lb) > 1e-12 { + out.push(fact( + "三线极线", + "l_{\\tau(I)}", + line_eq_latex(la, lb, lc), + None, + "\\frac{X}{a}+\\frac{Y}{b}+\\frac{Z}{c}=0", + "内心三线极线的直角坐标方程", + ).with(Shape::Segment { + x1: p0[0], + y1: p0[1], + x2: p1[0], + y2: p1[1], + label: "τ(I)".into(), + })); + } + for (k, pp) in poles.iter().take(3).enumerate() { + out.push(fact( + "三线极线", + &format!("Q_{{{}}}", k), + pt(pp[0], pp[1]), + None, + "", + "三线极线与边的交点", + ).with(Shape::Point { + x: pp[0], + y: pp[1], + label: format!("Q{}", k), + })); + } + } + } + out +} + +// --------------------------------------------------------------------------- +// Named points (2D): distances, polygon properties + +#[derive(Deserialize)] +pub struct NamedPoint { + pub name: String, + pub p: Vec, +} + + +pub(crate) fn dist2(a: [f64; 2], b: [f64; 2]) -> f64 { + ((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2)).sqrt() +} + +/// Directed ratio AP:PB for P on the line AB. P = A + t(B−A) ⇒ AP:PB = t:(1−t). +fn directed_ratio(a: [f64; 2], b: [f64; 2], p: [f64; 2]) -> Option { + let dx = b[0] - a[0]; + let dy = b[1] - a[1]; + let len2 = dx * dx + dy * dy; + if len2 < 1e-18 { + return None; + } + let t = ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / len2; + if (1.0 - t).abs() < 1e-14 { + return None; + } + Some(t / (1.0 - t)) +} + +fn section_point(a: [f64; 2], b: [f64; 2], m: f64, n: f64) -> [f64; 2] { + // divides AB in AP:PB = m:n + let den = m + n; + [(n * a[0] + m * b[0]) / den, (n * a[1] + m * b[1]) / den] +} + +fn line_line_isect2(p: [f64; 2], q: [f64; 2], r: [f64; 2], s: [f64; 2]) -> Option<[f64; 2]> { + let d1 = [q[0] - p[0], q[1] - p[1]]; + let d2 = [s[0] - r[0], s[1] - r[1]]; + let det = d1[0] * d2[1] - d1[1] * d2[0]; + if det.abs() < 1e-14 * (d1[0].hypot(d1[1]) * d2[0].hypot(d2[1])).max(1e-18) { + return None; + } + let t = ((r[0] - p[0]) * d2[1] - (r[1] - p[1]) * d2[0]) / det; + Some([p[0] + t * d1[0], p[1] + t * d1[1]]) +} + + +pub(crate) fn line_eq_latex(a: f64, b: f64, c: f64) -> String { + // a x + b y = c, normalised for display + if near(b, 0.0) { + return format!("x = {}", nice_short(c / a)); + } + let m = -a / b; + let q = c / b; + let mut s = String::from("y = "); + if near(m, 0.0) { + s.push_str(&nice_short(q)); + return s; + } + s.push_str(&if near(m, 1.0) { "x".into() } else if near(m, -1.0) { "-x".into() } else { format!("{}x", nice_short(m)) }); + if !near(q, 0.0) { + let qs = nice_short(q); + if let Some(stripped) = qs.strip_prefix('-') { + s.push_str(&format!(" - {}", stripped)); + } else { + s.push_str(&format!(" + {}", qs)); + } + } + s +} + +pub fn points_analyze(points: &[NamedPoint]) -> Vec { + if points.iter().any(|p| p.p.len() == 3) { + return points3_analyze(points); + } + let pts: Vec<(String, [f64; 2])> = points.iter().map(|p| (p.name.clone(), [p.p[0], *p.p.get(1).unwrap_or(&0.0)])).collect(); + let n = pts.len(); + let mut out = Vec::new(); + if n == 0 { + return out; + } + if n == 1 { + let (nm, p) = &pts[0]; + let r = dist2(*p, [0.0, 0.0]); + out.push(fact("距离", &format!("|O{}|", nm), nice(r), Some(r), "\\sqrt{x^2 + y^2}", "到原点的距离")); + let q = if near(p[0], 0.0) || near(p[1], 0.0) { "坐标轴上".to_string() } else { format!("第{}象限", match (p[0] > 0.0, p[1] > 0.0) { (true, true) => "一", (false, true) => "二", (false, false) => "三", _ => "四" }) }; + out.push(fact("位置", nm, format!("\\text{{{}}}", q), None, "", "看坐标符号")); + return out; + } + if n == 2 { + let ((na, a), (nb, b)) = (&pts[0], &pts[1]); + let d = dist2(*a, *b); + let mid = [(a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0]; + out.push(fact("线段", &format!("|{na}{nb}|"), nice(d), Some(d), "\\sqrt{(x_2-x_1)^2 + (y_2-y_1)^2}", "两点间距离").with(Shape::Segment { x1: a[0], y1: a[1], x2: b[0], y2: b[1], label: format!("{na}{nb}") })); + out.push(fact("线段", &format!("{na}{nb} \\text{{ 中点}}"), pt(mid[0], mid[1]), None, "\\left(\\frac{x_1+x_2}{2},\\ \\frac{y_1+y_2}{2}\\right)", "中点公式").with(Shape::Point { x: mid[0], y: mid[1], label: "中点 M".into() })); + let (la, lb) = (b[1] - a[1], a[0] - b[0]); + let lc = la * a[0] + lb * a[1]; + if !near(b[0], a[0]) { + let k = (b[1] - a[1]) / (b[0] - a[0]); + out.push(fact("直线", "k", nice(k), Some(k), "k = \\frac{y_2 - y_1}{x_2 - x_1}", "斜率")); + let ang = k.atan().rem_euclid(PI); + out.push(fact("直线", "\\alpha", deg(ang), Some(ang), "\\tan\\alpha = k", "倾斜角")); + } else { + out.push(fact("直线", "k", "\\text{不存在(竖直)}".into(), None, "", "两点横坐标相同")); + } + out.push(fact("直线", &format!("{na}{nb}"), line_eq_latex(la, lb, lc), None, "", "两点式")); + // perpendicular bisector: direction (la, lb) is normal of AB; bisector normal is AB direction + let (pa, pb) = (b[0] - a[0], b[1] - a[1]); + let pc = pa * mid[0] + pb * mid[1]; + out.push(fact("直线", "\\text{中垂线}", line_eq_latex(pa, pb, pc), None, "", "过中点且垂直于该线段")); + return out; + } + + // polygon in the given order + let side_name = |i: usize| format!("{}{}", pts[i].0, pts[(i + 1) % n].0); + let mut sides = Vec::new(); + for i in 0..n { + let (a, b) = (pts[i].1, pts[(i + 1) % n].1); + let d = dist2(a, b); + sides.push(d); + out.push(fact("边长", &format!("|{}|", side_name(i)), nice(d), Some(d), "\\sqrt{\\Delta x^2 + \\Delta y^2}", "两点间距离")); + } + let mut area2 = 0.0; + for i in 0..n { + let (a, b) = (pts[i].1, pts[(i + 1) % n].1); + area2 += a[0] * b[1] - b[0] * a[1]; + } + let orient = area2.signum(); + let area = area2.abs() / 2.0; + let per: f64 = sides.iter().sum(); + + let mut angles = Vec::new(); + let mut convex = true; + for i in 0..n { + let prev = pts[(i + n - 1) % n].1; + let cur = pts[i].1; + let next = pts[(i + 1) % n].1; + let u = [prev[0] - cur[0], prev[1] - cur[1]]; + let w = [next[0] - cur[0], next[1] - cur[1]]; + let cross = w[0] * u[1] - w[1] * u[0]; + let dot = u[0] * w[0] + u[1] * w[1]; + let mut ang = cross.abs().atan2(dot); + if cross * orient < -1e-12 { + ang = 2.0 * PI - ang; + convex = false; + } + angles.push(ang); + out.push(fact("角", &format!("\\angle {}{}{}", pts[(i + n - 1) % n].0, pts[i].0, pts[(i + 1) % n].0), deg(ang), Some(ang), "\\cos\\theta = \\frac{\\vec u \\cdot \\vec v}{|\\vec u||\\vec v|}", "向量夹角")); + } + out.push(fact("面积与周长", "S", nice(area), Some(area), "S = \\frac12\\left|\\sum (x_i y_{i+1} - x_{i+1} y_i)\\right|", "鞋带公式")); + out.push(fact("面积与周长", "C", nice(per), Some(per), "\\sum |P_i P_{i+1}|", "周长")); + + // parallel / perpendicular sides + let dir = |i: usize| { + let (a, b) = (pts[i].1, pts[(i + 1) % n].1); + [b[0] - a[0], b[1] - a[1]] + }; + let mut par = Vec::new(); + for i in 0..n { + for j in i + 1..n { + let (u, w) = (dir(i), dir(j)); + let cr = u[0] * w[1] - u[1] * w[0]; + let dt = u[0] * w[0] + u[1] * w[1]; + let scale = (u[0].hypot(u[1])) * (w[0].hypot(w[1])); + if cr.abs() < 1e-9 * scale { + par.push((i, j)); + out.push(fact("位置关系", &format!("{} \\parallel {}", side_name(i), side_name(j)), "\\checkmark".into(), None, "x_1y_2 - x_2y_1 = 0", "方向向量共线")); + } else if dt.abs() < 1e-9 * scale { + out.push(fact("位置关系", &format!("{} \\perp {}", side_name(i), side_name(j)), "\\checkmark".into(), None, "\\vec u\\cdot\\vec v = 0", "方向向量点积为 0")); + } + } + } + + if n == 3 { + let s = [sides[1], sides[2], sides[0]]; // a = BC, b = CA, c = AB + let g = [angles[0], angles[1], angles[2]]; + let verts = [pts[0].1, pts[1].1, pts[2].1]; + let names = [pts[0].0.as_str(), pts[1].0.as_str(), pts[2].0.as_str()]; + if area < 1e-12 { + out.push(fact("形状判断", "\\text{类型}", "\\text{三点共线,不构成三角形}".into(), None, "", "面积为 0")); + return out; + } + let derived = triangle_derived(s, g, verts, names); + out.extend(derived.into_iter().filter(|f| f.label != "S" && f.label != "C_{\\triangle}")); + } else if n == 4 { + let (a, b, c, d) = (pts[0].1, pts[1].1, pts[2].1, pts[3].1); + let d1 = dist2(a, c); + let d2 = dist2(b, d); + out.push(fact("对角线", &format!("|{}{}|", pts[0].0, pts[2].0), nice(d1), Some(d1), "", "两点间距离").with(Shape::Segment { x1: a[0], y1: a[1], x2: c[0], y2: c[1], label: "对角线".into() })); + out.push(fact("对角线", &format!("|{}{}|", pts[1].0, pts[3].0), nice(d2), Some(d2), "", "两点间距离").with(Shape::Segment { x1: b[0], y1: b[1], x2: d[0], y2: d[1], label: "对角线".into() })); + // diagonal intersection + let m = vec![vec![c[0] - a[0], -(d[0] - b[0])], vec![c[1] - a[1], -(d[1] - b[1])]]; + if let Some(st) = solve_linear_pub(m, vec![b[0] - a[0], b[1] - a[1]]) { + if st[0].is_finite() { + let p = [a[0] + st[0] * (c[0] - a[0]), a[1] + st[0] * (c[1] - a[1])]; + out.push(fact("对角线", "\\text{交点}", pt(p[0], p[1]), None, "", "联立两条对角线").with(Shape::Point { x: p[0], y: p[1], label: "对角线交点".into() })); + } + } + let is_para = par.contains(&(0, 2)) && par.contains(&(1, 3)); + let right = angles.iter().all(|x| near(*x, PI / 2.0)); + let equal = sides.iter().all(|x| near(*x, sides[0])); + let kind = if is_para && right && equal { + "正方形" + } else if is_para && right { + "矩形" + } else if is_para && equal { + "菱形" + } else if is_para { + "平行四边形" + } else if par.contains(&(0, 2)) || par.contains(&(1, 3)) { + "梯形" + } else if !convex { + "凹四边形" + } else { + "一般凸四边形" + }; + // Varignon: midpoints of the four sides form a parallelogram. + { + let mids = [ + [(a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0], + [(b[0] + c[0]) / 2.0, (b[1] + c[1]) / 2.0], + [(c[0] + d[0]) / 2.0, (c[1] + d[1]) / 2.0], + [(d[0] + a[0]) / 2.0, (d[1] + a[1]) / 2.0], + ]; + let side = |i: usize, j: usize| { + let (u, w) = (mids[i], mids[j]); + [w[0] - u[0], w[1] - u[1]] + }; + let (u01, u23) = (side(0, 1), side(3, 2)); + let (u12, u30) = (side(1, 2), side(0, 3)); + let para = |p: [f64; 2], q: [f64; 2]| { + let cr = p[0] * q[1] - p[1] * q[0]; + let sc = (p[0].hypot(p[1])) * (q[0].hypot(q[1])); + sc < 1e-12 || cr.abs() < 1e-9 * sc + }; + if para(u01, u23) && para(u12, u30) { + let l01 = u01[0].hypot(u01[1]); + let l12 = u12[0].hypot(u12[1]); + out.push(fact( + "瓦里农定理", + "\\text{Varignon}", + "\\text{平行四边形}".into(), + None, + "M_i = \\text{中点}(P_iP_{i+1})", + "任意四边形各边中点连成平行四边形", + )); + out.push(fact( + "瓦里农定理", + "|M_0M_1|", + nice(l01), + Some(l01), + "", + "Varignon 边长", + ).with(Shape::Segment { + x1: mids[0][0], y1: mids[0][1], x2: mids[1][0], y2: mids[1][1], + label: "Varignon".into(), + })); + out.push(fact( + "瓦里农定理", + "|M_1M_2|", + nice(l12), + Some(l12), + "", + "Varignon 邻边", + )); + for (i, m) in mids.iter().enumerate() { + out.push(fact( + "瓦里农定理", + &format!("M_{}", i), + pt(m[0], m[1]), + None, + "", + "边中点", + ).with(Shape::Point { x: m[0], y: m[1], label: format!("M{}", i) })); + } + } + } + // British flag theorem: for rectangle/square, PA²+PC² = PB²+PD² for any P (verify at origin). + if is_para && right { + let p0 = [0.0, 0.0]; + let dsq = |q: [f64; 2]| (q[0] - p0[0]).powi(2) + (q[1] - p0[1]).powi(2); + let lhs = dsq(a) + dsq(c); + let rhs = dsq(b) + dsq(d); + if near(lhs, rhs) { + out.push(fact( + "英国国旗定理", + "PA^2 + PC^2", + nice(lhs), + Some(lhs), + "PA^2 + PC^2 = PB^2 + PD^2", + "矩形上任一点(取原点验证):对角顶点到点的平方和相等", + )); + out.push(fact( + "英国国旗定理", + "PB^2 + PD^2", + nice(rhs), + Some(rhs), + "PA^2 + PC^2 = PB^2 + PD^2", + "英国国旗定理", + )); + } + } + + // Cyclic quad: opposite angles sum to π → Ptolemy + Brahmagupta. + { + let cyclic = convex + && near(angles[0] + angles[2], PI) + && near(angles[1] + angles[3], PI); + if cyclic { + out.push(fact( + "圆内接四边形", + "\\text{共圆}", + "\\checkmark".into(), + None, + "\\angle A + \\angle C = \\angle B + \\angle D = 180^\\circ", + "对角互补 ⇒ 四点共圆", + )); + let (ab, bc, cd, da) = (sides[0], sides[1], sides[2], sides[3]); + let (ac, bd) = (d1, d2); + let lhs = ac * bd; + let rhs = ab * cd + da * bc; + out.push(fact( + "托勒密定理", + "AC \\cdot BD", + nice(lhs), + Some(lhs), + "AC\\cdot BD = AB\\cdot CD + AD\\cdot BC", + "圆内接四边形的托勒密定理", + )); + out.push(fact( + "托勒密定理", + "AB\\cdot CD + AD\\cdot BC", + nice(rhs), + Some(rhs), + "AC\\cdot BD = AB\\cdot CD + AD\\cdot BC", + "托勒密:对角线积 = 对边积之和", + )); + let ssemi = (ab + bc + cd + da) / 2.0; + let brahm = ((ssemi - ab) * (ssemi - bc) * (ssemi - cd) * (ssemi - da)).max(0.0).sqrt(); + out.push(fact( + "婆罗摩笈多公式", + "S_{\\text{Brahmagupta}}", + nice(brahm), + Some(brahm), + "S = \\sqrt{(s-a)(s-b)(s-c)(s-d)}", + "圆内接四边形面积(婆罗摩笈多)", + )); + } + } + + out.push(fact("形状判断", "\\text{类型}", format!("\\text{{{}}}", kind), None, "", "由对边平行、直角、边长相等判断")); + } else { + out.push(fact("形状判断", "\\text{类型}", format!("\\text{{{}{}边形}}", if convex { "凸" } else { "凹" }, n), None, "", "内角和 (n-2)·180°")); + } + + // Desargues quick check: pts[0..3] vs pts[3..6] as △ABC, △A′B′C′. + if pts.len() >= 6 { + let (a, b, c) = (pts[0].1, pts[1].1, pts[2].1); + let (ap, bp, cp) = (pts[3].1, pts[4].1, pts[5].1); + let (na, nb, nc) = (&pts[0].0, &pts[1].0, &pts[2].0); + let (nap, nbp, ncp) = (&pts[3].0, &pts[4].0, &pts[5].0); + if let Some(s) = line_line_isect2(a, ap, b, bp) { + if collinear2(s, c, cp) { + out.push(fact( + "德萨格定理", + "\\text{Desargues}", + "\\text{透视}".into(), + None, + &format!("{na}{nap},{nb}{nbp},{nc}{ncp}\\ \\text{{共点}}"), + "两三角形对应顶点连线共点 ⇒ 对应边交点共线", + ).with(Shape::Point { + x: s[0], + y: s[1], + label: "S Desargues".into(), + })); + out.push(fact( + "德萨格定理", + "S", + pt(s[0], s[1]), + None, + "AA'\\cap BB'\\cap CC'", + "透视中心", + )); + let x = line_line_isect2(b, c, bp, cp); + let y = line_line_isect2(c, a, cp, ap); + let z = line_line_isect2(a, b, ap, bp); + if let (Some(x), Some(y), Some(z)) = (x, y, z) { + if collinear2(x, y, z) { + out.push(fact( + "德萨格定理", + "\\text{透视轴}", + "\\text{共线}".into(), + None, + "BC\\cap B'C',\\ CA\\cap C'A',\\ AB\\cap A'B'", + "对应边交点共线(德萨格透视轴)", + ).with(Shape::Segment { + x1: x[0], + y1: x[1], + x2: y[0], + y2: y[1], + label: "Desargues轴".into(), + })); + } + } + } + } + } + + // Pascal (cyclic hexagon) + Brianchon (opposite-vertex diagonals) for 6-gon in order. + if n == 6 { + let v: [[f64; 2]; 6] = std::array::from_fn(|i| pts[i].1); + let names: [&str; 6] = std::array::from_fn(|i| pts[i].0.as_str()); + // Cyclic? circumcircle of first three; others on it. + if let Some((o, r)) = circum2(v[0], v[1], v[2]) { + let cyclic = (3..6).all(|i| (dist2(v[i], o) - r).abs() < 1e-6 * (1.0 + r)); + if cyclic { + // Opposite sides: AB∩DE, BC∩EF, CD∩FA + let p01 = line_line_isect2(v[0], v[1], v[3], v[4]); + let p12 = line_line_isect2(v[1], v[2], v[4], v[5]); + let p23 = line_line_isect2(v[2], v[3], v[5], v[0]); + match (p01, p12, p23) { + (Some(x), Some(y), Some(z)) if collinear2(x, y, z) => { + out.push(fact( + "帕斯卡定理", + "\\text{Pascal}", + "\\text{共线}".into(), + None, + "AB\\cap DE,\\ BC\\cap EF,\\ CD\\cap FA", + "圆内接六边形:三组对边交点共线(帕斯卡线)", + ).with(Shape::Segment { + x1: x[0], + y1: x[1], + x2: y[0], + y2: y[1], + label: "Pascal".into(), + })); + out.push(fact( + "帕斯卡定理", + "\\text{Pascal线}", + line_eq_latex(y[1] - x[1], x[0] - y[0], (y[1] - x[1]) * x[0] + (x[0] - y[0]) * x[1]), + None, + "", + "帕斯卡线方程", + )); + } + (None, None, None) => { + out.push(fact( + "帕斯卡定理", + "\\text{Pascal}", + "\\text{无穷远直线}".into(), + None, + "AB\\parallel DE,\\ BC\\parallel EF,\\ CD\\parallel FA", + "对边皆平行:帕斯卡线为无穷远直线", + )); + } + _ => {} + } + } + } + // Brianchon: diagonals joining opposite vertices AD, BE, CF concur. + if let Some(s) = line_line_isect2(v[0], v[3], v[1], v[4]) { + if collinear2(s, v[2], v[5]) { + // also check tangential side condition a+c+e = b+d+f (sufficient for circle) + let side = |i: usize, j: usize| dist2(v[i], v[j]); + let ace = side(0, 1) + side(2, 3) + side(4, 5); + let bdf = side(1, 2) + side(3, 4) + side(5, 0); + out.push(fact( + "布利安雄定理", + "\\text{Brianchon}", + "\\text{共点}".into(), + None, + &format!( + "{}{}\\cap{}{}\\cap{}{}", + names[0], names[3], names[1], names[4], names[2], names[5] + ), + "六边形对顶点连线共点(布利安雄点)", + ).with(Shape::Point { + x: s[0], + y: s[1], + label: "Brianchon".into(), + })); + out.push(fact( + "布利安雄定理", + "B", + pt(s[0], s[1]), + None, + "AD\\cap BE\\cap CF", + "布利安雄点", + )); + if (ace - bdf).abs() < 1e-6 * (1.0 + ace) { + out.push(fact( + "布利安雄定理", + "a+c+e = b+d+f", + nice(ace), + Some(ace), + "a+c+e = b+d+f", + "对边和相等:存在内切圆(切线六边形)", + )); + } + } + } + } + + out +} + +fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] { + [a[0] - b[0], a[1] - b[1], a[2] - b[2]] +} +fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 { + a[0] * b[0] + a[1] * b[1] + a[2] * b[2] +} +fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] { + [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]] +} +fn len3(a: [f64; 3]) -> f64 { + dot3(a, a).sqrt() +} + +fn points3_analyze(points: &[NamedPoint]) -> Vec { + let pts: Vec<(String, [f64; 3])> = points.iter().map(|p| (p.name.clone(), [p.p[0], *p.p.get(1).unwrap_or(&0.0), *p.p.get(2).unwrap_or(&0.0)])).collect(); + let mut out = Vec::new(); + for i in 0..pts.len() { + for j in i + 1..pts.len() { + let d = len3(sub3(pts[j].1, pts[i].1)); + out.push(fact("距离", &format!("|{}{}|", pts[i].0, pts[j].0), nice(d), Some(d), "\\sqrt{\\Delta x^2+\\Delta y^2+\\Delta z^2}", "空间两点距离")); + } + } + if pts.len() >= 3 { + let (a, b, c) = (pts[0].1, pts[1].1, pts[2].1); + let nrm = cross3(sub3(b, a), sub3(c, a)); + let area = len3(nrm) / 2.0; + let names = format!("{}{}{}", pts[0].0, pts[1].0, pts[2].0); + out.push(fact("面积", &format!("S_{{\\triangle {}}}", names), nice(area), Some(area), "S = \\frac12|\\vec{AB}\\times\\vec{AC}|", "叉积的模")); + if area > 1e-12 { + let d = dot3(nrm, a); + out.push(fact("平面", &format!("\\text{{平面 }}{}", names), plane_latex(nrm, d), None, "\\vec n = \\vec{AB}\\times\\vec{AC}", "三点确定一个平面")); + } + if pts.len() >= 4 { + let dpt = pts[3].1; + let vol = dot3(sub3(dpt, a), nrm).abs() / 6.0; + out.push(fact("体积", "V_{\\text{四面体}}", nice(vol), Some(vol), "V = \\frac16\\left|\\vec{AD}\\cdot(\\vec{AB}\\times\\vec{AC})\\right|", "混合积")); + // Cayley–Menger: volume from the six edge lengths alone. + { + let p0 = pts[0].1; + let p1 = pts[1].1; + let p2 = pts[2].1; + let p3 = pts[3].1; + let d12 = len3(sub3(p1, p0)); + let d13 = len3(sub3(p2, p0)); + let d14 = len3(sub3(p3, p0)); + let d23 = len3(sub3(p2, p1)); + let d24 = len3(sub3(p3, p1)); + let d34 = len3(sub3(p3, p2)); + let s = |x: f64| x * x; + let mut m = [ + [0.0, 1.0, 1.0, 1.0, 1.0], + [1.0, 0.0, s(d12), s(d13), s(d14)], + [1.0, s(d12), 0.0, s(d23), s(d24)], + [1.0, s(d13), s(d23), 0.0, s(d34)], + [1.0, s(d14), s(d24), s(d34), 0.0], + ]; + // Gaussian elimination → det + let mut det = 1.0_f64; + for i in 0..5 { + let mut piv = i; + for r in (i + 1)..5 { + if m[r][i].abs() > m[piv][i].abs() { + piv = r; + } + } + if m[piv][i].abs() < 1e-18 { + det = 0.0; + break; + } + if piv != i { + m.swap(piv, i); + det = -det; + } + det *= m[i][i]; + for r in (i + 1)..5 { + let f = m[r][i] / m[i][i]; + for c in i..5 { + m[r][c] -= f * m[i][c]; + } + } + } + let v_cm2 = det.abs() / 288.0; + if v_cm2 >= 0.0 { + let v_cm = v_cm2.sqrt(); + out.push(fact( + "体积", + "V_{\\text{Cayley--Menger}}", + nice(v_cm), + Some(v_cm), + "288V^2 = |\\det M_5|", + "凯莱–门杰行列式:由六棱长求四面体体积", + )); + } + } + if area > 1e-12 { + let h = 3.0 * vol / area; + out.push(fact("距离", &format!("d({}, \\text{{平面}})", pts[3].0), nice(h), Some(h), "d = \\frac{3V}{S}", "等体积法")); + } + + + // Four face areas and altitudes from each vertex to the opposite face. + { + let verts = [pts[0].1, pts[1].1, pts[2].1, pts[3].1]; + let labels = [&pts[0].0, &pts[1].0, &pts[2].0, &pts[3].0]; + let faces = [[1usize, 2, 3], [0, 2, 3], [0, 1, 3], [0, 1, 2]]; + for (i, face) in faces.iter().enumerate() { + let (p, q, r) = (verts[face[0]], verts[face[1]], verts[face[2]]); + let nrm = cross3(sub3(q, p), sub3(r, p)); + let s_face = len3(nrm) / 2.0; + let fname = format!("{}{}{}", labels[face[0]], labels[face[1]], labels[face[2]]); + out.push(fact( + "四面体面", + &format!("S_{{\\triangle {}}}", fname), + nice(s_face), + Some(s_face), + "S = \\frac12|\\vec{u}\\times\\vec{v}|", + "四面体的一个面的面积", + )); + if s_face > 1e-12 && vol > 1e-12 { + let h_i = 3.0 * vol / s_face; + out.push(fact( + "四面体面", + &format!("h_{{{}}}", labels[i]), + nice(h_i), + Some(h_i), + "h = \\frac{3V}{S_{\\text{对面}}}", + &format!("从顶点 {} 到对面的高", labels[i]), + )); + } + } + } + + // Viviani for regular tetrahedron: sum of distances from an interior point to the four faces = altitude. + { + let edges = [ + len3(sub3(pts[1].1, pts[0].1)), + len3(sub3(pts[2].1, pts[0].1)), + len3(sub3(pts[3].1, pts[0].1)), + len3(sub3(pts[2].1, pts[1].1)), + len3(sub3(pts[3].1, pts[1].1)), + len3(sub3(pts[3].1, pts[2].1)), + ]; + let e0 = edges[0]; + if e0 > 1e-12 && edges.iter().all(|e| near(*e, e0)) && vol > 1e-12 { + // Regular tetra: altitude h = √6 / 3 · a; inradius ρ = h/4 + let h_reg = (6.0f64).sqrt() / 3.0 * e0; + let rho = h_reg / 4.0; + out.push(fact( + "维维安尼定理", + "\\sum d_{\\text{面}}", + nice(h_reg), + Some(h_reg), + "d_1+d_2+d_3+d_4 = h", + "正四面体:内点到四面距离之和等于高(形心处为 4ρ)", + )); + out.push(fact( + "维维安尼定理", + "\\rho", + nice(rho), + Some(rho), + "\\rho = h/4", + "正四面体内切球半径", + )); + } + } + // Skew / intersecting lines AB and CD from the first four points. + let (na, nb, nc, nd) = (&pts[0].0, &pts[1].0, &pts[2].0, &pts[3].0); + let (pa, pb, pc, pd) = (pts[0].1, pts[1].1, pts[2].1, pts[3].1); + let ab = sub3(pb, pa); + let cd = sub3(pd, pc); + let (lab, lcd) = (len3(ab), len3(cd)); + if lab > 1e-12 && lcd > 1e-12 { + let cosang = (dot3(ab, cd).abs() / (lab * lcd)).clamp(0.0, 1.0); + let theta = cosang.acos(); + out.push(fact( + "异面直线", + &format!("\\theta({}{}, {}{})", na, nb, nc, nd), + deg(theta), + Some(theta), + "\\cos\\theta = \\frac{|\\vec{AB}\\cdot\\vec{CD}|}{|\\vec{AB}||\\vec{CD}|}", + "两直线方向的夹角(取锐角或直角)", + )); + } + let nskew = cross3(ab, cd); + let ln = len3(nskew); + let ac = sub3(pc, pa); + if ln < 1e-12 * len3(ab).max(len3(cd)).max(1e-12) { + let parallel = len3(ab) > 1e-12 && len3(cd) > 1e-12; + if parallel { + let n2 = cross3(ab, ac); + let dpar = len3(n2) / len3(ab); + out.push(fact( + "异面直线", + &format!("{}{} \\parallel {}{}", na, nb, nc, nd), + "\\checkmark".into(), + None, + "\\vec{AB} \\times \\vec{CD} = \\mathbf{0}", + "方向向量共线", + )); + out.push(fact( + "异面直线", + &format!("d({}{}, {}{})", na, nb, nc, nd), + nice(dpar), + Some(dpar), + "d = \\frac{|\\vec{AB}\\times\\vec{AC}|}{|\\vec{AB}|}", + "平行直线间距离", + )); + } + } else { + let triple = dot3(ac, nskew); + let dskew = triple.abs() / ln; + if dskew < 1e-9 { + out.push(fact( + "异面直线", + &format!("{}{} , {}{}", na, nb, nc, nd), + "\\text{共面(相交或平行)}".into(), + Some(0.0), + "\\vec{AC}\\cdot(\\vec{AB}\\times\\vec{CD}) = 0", + "混合积为 0 ⇒ 共面", + )); + } else { + out.push(fact( + "异面直线", + &format!("d({}{}, {}{})", na, nb, nc, nd), + nice(dskew), + Some(dskew), + "d = \\frac{|\\vec{AC}\\cdot(\\vec{AB}\\times\\vec{CD})|}{|\\vec{AB}\\times\\vec{CD}|}", + "异面直线的距离", + ).with(Shape::Line3 { + px: pa[0], + py: pa[1], + pz: pa[2], + dx: ab[0], + dy: ab[1], + dz: ab[2], + label: format!("{na}{nb}"), + })); + out.push(fact( + "异面直线", + &format!("{}{} \\nparallel {}{}", na, nb, nc, nd), + "\\text{异面}".into(), + None, + "\\vec{AB}\\times\\vec{CD}\\ne\\mathbf{0},\\ \\vec{AC}\\cdot(\\vec{AB}\\times\\vec{CD})\\ne 0", + "既不平行也不相交", + )); + } + } + } + } + out +} + +fn plane_latex(n: [f64; 3], d: f64) -> String { + let mut terms = Vec::new(); + for (c, v) in n.iter().zip(["x", "y", "z"]) { + if near(*c, 0.0) { + continue; + } + let cs = if near(*c, 1.0) { String::new() } else if near(*c, -1.0) { "-".into() } else { nice_short(*c) }; + terms.push(format!("{}{}", cs, v)); + } + let mut s = terms.join(" + ").replace("+ -", "- "); + if s.is_empty() { + s = "0".into(); + } + format!("{} = {}", s, nice_short(d)) +} + +// --------------------------------------------------------------------------- +// Vectors + +#[derive(Deserialize)] +pub struct NamedVec { + pub name: String, + pub v: Vec, +} + +fn vlabel(n: &str) -> String { + if n.len() == 2 && n.chars().all(|c| c.is_ascii_uppercase()) { + format!("\\overrightarrow{{{}}}", n) + } else { + format!("\\mathbf{{{}}}", n) + } +} + +pub fn vectors_analyze(vs: &[NamedVec]) -> Vec { + let mut out = Vec::new(); + let to3 = |v: &Vec| [v[0], *v.get(1).unwrap_or(&0.0), *v.get(2).unwrap_or(&0.0)]; + let is3 = vs.iter().any(|v| v.v.len() == 3); + for v in vs { + let a = to3(&v.v); + let l = len3(a); + let nm = vlabel(&v.name); + out.push(fact("模长与方向", &format!("|{}|", nm), nice(l), Some(l), "\\sqrt{x^2 + y^2 + z^2}", "模长")); + if l > 1e-12 { + if is3 { + out.push(fact("模长与方向", &format!("\\hat{{{}}}", v.name), pt3([a[0] / l, a[1] / l, a[2] / l]), None, "\\frac{\\vec v}{|\\vec v|}", "单位向量")); + } else { + let ang = a[1].atan2(a[0]).rem_euclid(2.0 * PI); + out.push(fact("模长与方向", &format!("\\theta_{{{}}}", v.name), deg(ang), Some(ang), "\\tan\\theta = \\frac{y}{x}", "与 x 轴正方向的夹角")); + } + } + } + for i in 0..vs.len() { + for j in i + 1..vs.len() { + let (a, b) = (to3(&vs[i].v), to3(&vs[j].v)); + let (na, nb) = (vlabel(&vs[i].name), vlabel(&vs[j].name)); + let d = dot3(a, b); + let (la, lb) = (len3(a), len3(b)); + let fmt = |v: &[f64]| if is3 { pt3([v[0], v[1], v[2]]) } else { pt(v[0], v[1]) }; + out.push(fact("两向量关系", &format!("{} \\cdot {}", na, nb), nice(d), Some(d), "x_1x_2 + y_1y_2 + z_1z_2", "数量积")); + if la > 1e-12 && lb > 1e-12 { + let ang = (d / (la * lb)).clamp(-1.0, 1.0).acos(); + out.push(fact("两向量关系", &format!("\\langle {}, {} \\rangle", na, nb), deg(ang), Some(ang), "\\cos\\theta = \\frac{\\vec a\\cdot\\vec b}{|\\vec a||\\vec b|}", "夹角")); + let proj = d / lb; + out.push(fact("两向量关系", &format!("\\text{{{} 在 {} 上的投影}}", vs[i].name, vs[j].name), nice(proj), Some(proj), "\\frac{\\vec a\\cdot\\vec b}{|\\vec b|}", "投影(数量)")); + // projection vector of a onto b, and the perpendicular rest: a = proj + perp + let pb2 = dot3(b, b); + let k = d / pb2; + let projv: Vec = (0..3).map(|t| k * b[t]).collect(); + let perp: Vec = (0..3).map(|t| a[t] - k * b[t]).collect(); + out.push(fact("两向量关系", &format!("\\text{{proj}}_{{{}}}\\ {}", nb, na), fmt(&projv), None, "\\frac{{\\vec a\\cdot\\vec b}}{{|\\vec b|^2}}\\,\\vec b", "投影向量").with(if is3 { Shape::Arrow3 { x: projv[0], y: projv[1], z: projv[2], label: format!("proj{}_{}", vs[i].name, vs[j].name) } } else { Shape::Segment { x1: 0.0, y1: 0.0, x2: projv[0], y2: projv[1], label: format!("proj{}_{}", vs[i].name, vs[j].name) } })); + out.push(fact("两向量关系", &format!("{} - \\text{{proj}}_{{{}}}\\ {}", na, nb, na), fmt(&perp), None, "\\vec a - \\text{proj}_{\\vec b}\\,\\vec a", "垂直分量:a = 投影 + 垂直").with(if is3 { Shape::Arrow3 { x: perp[0], y: perp[1], z: perp[2], label: format!("perp{}_{}", vs[i].name, vs[j].name) } } else { Shape::Segment { x1: projv[0], y1: projv[1], x2: a[0], y2: a[1], label: format!("perp{}_{}", vs[i].name, vs[j].name) } })); + let cr = cross3(a, b); + let lc = len3(cr); + if lc < 1e-9 * la * lb { + let same = d > 0.0; + out.push(fact("两向量关系", &format!("{} \\parallel {}", na, nb), format!("\\text{{{}}}", if same { "同向共线" } else { "反向共线" }), None, "\\vec a = \\lambda\\vec b", "叉积为零")); + out.push(fact("两向量关系", "\\lambda", nice(if same { la / lb } else { -la / lb }), None, "\\vec a = \\lambda \\vec b", "共线系数")); + } else if d.abs() < 1e-9 * la * lb { + out.push(fact("两向量关系", &format!("{} \\perp {}", na, nb), "\\checkmark".into(), None, "\\vec a\\cdot\\vec b = 0", "数量积为 0")); + } + if is3 { + out.push(fact("两向量关系", &format!("{} \\times {}", na, nb), pt3(cr), None, "\\begin{vmatrix} i & j & k \\\\ x_1 & y_1 & z_1 \\\\ x_2 & y_2 & z_2 \\end{vmatrix}", "向量积").with(Shape::Arrow3 { x: cr[0], y: cr[1], z: cr[2], label: format!("{}×{}", vs[i].name, vs[j].name) })); + } + out.push(fact("两向量关系", "S_{\\text{平行四边形}}", nice(lc), Some(lc), "|\\vec a \\times \\vec b| = |\\vec a||\\vec b|\\sin\\theta", "以两向量为邻边")); + out.push(fact("两向量关系", "S_{\\triangle}", nice(lc / 2.0), Some(lc / 2.0), "\\frac12|\\vec a\\times\\vec b|", "以两向量为邻边的三角形")); + } + let sum: Vec = (0..3).map(|k| a[k] + b[k]).collect(); + let dif: Vec = (0..3).map(|k| a[k] - b[k]).collect(); + out.push(fact("两向量关系", &format!("{} + {}", na, nb), fmt(&sum), None, "", "坐标相加")); + out.push(fact("两向量关系", &format!("{} - {}", na, nb), fmt(&dif), None, "", "坐标相减")); + // Parallelogram law: |a+b|² + |a−b|² = 2(|a|²+|b|²) + { + let sum_len2 = sum[0] * sum[0] + sum[1] * sum[1] + sum[2] * sum[2]; + let dif_len2 = dif[0] * dif[0] + dif[1] * dif[1] + dif[2] * dif[2]; + let rhs = 2.0 * (la * la + lb * lb); + let lhs = sum_len2 + dif_len2; + if (lhs - rhs).abs() < 1e-6 * (1.0 + rhs) { + out.push(fact( + "平行四边形法则", + &format!("|{0}+{1}|^2 + |{0}-{1}|^2", vs[i].name, vs[j].name), + nice(lhs), + Some(lhs), + "|\\vec a+\\vec b|^2 + |\\vec a-\\vec b|^2 = 2(|\\vec a|^2+|\\vec b|^2)", + "平行四边形法则(向量形式)", + )); + } + } + } + } + if vs.len() >= 3 && is3 { + let (a, b, c) = (to3(&vs[0].v), to3(&vs[1].v), to3(&vs[2].v)); + let m = dot3(a, cross3(b, c)); + out.push(fact("三向量", &format!("[{}\\,{}\\,{}]", vs[0].name, vs[1].name, vs[2].name), nice(m), Some(m), "\\vec a\\cdot(\\vec b\\times\\vec c)", "混合积")); + if m.abs() < 1e-9 { + out.push(fact("三向量", "\\text{共面}", "\\checkmark".into(), None, "[\\vec a\\,\\vec b\\,\\vec c] = 0", "混合积为 0")); + } else { + out.push(fact("三向量", "V_{\\text{平行六面体}}", nice(m.abs()), Some(m.abs()), "|[\\vec a\\,\\vec b\\,\\vec c]|", "混合积的绝对值")); + } + } + out +} + +// --------------------------------------------------------------------------- +// Lines in the plane (a x + b y = c) + +#[derive(Deserialize)] +pub struct NamedLine { + pub name: String, + pub a: f64, + pub b: f64, + pub c: f64, +} + +/// Intersection point of two lines a x + b y = c, when they are not (near-)parallel. +fn line_isect(p: &NamedLine, q: &NamedLine) -> Option<[f64; 2]> { + let det = p.a * q.b - p.b * q.a; + if det.abs() < 1e-12 * (p.a.hypot(p.b) * q.a.hypot(q.b)) { + return None; + } + Some([(p.c * q.b - p.b * q.c) / det, (p.a * q.c - p.c * q.a) / det]) +} + + +/// Circumcenter and radius of △ABC; None if collinear. +fn circum2(a: [f64; 2], b: [f64; 2], c: [f64; 2]) -> Option<([f64; 2], f64)> { + let d = 2.0 * (a[0] * (b[1] - c[1]) + b[0] * (c[1] - a[1]) + c[0] * (a[1] - b[1])); + if d.abs() < 1e-14 { + return None; + } + let sq = |q: [f64; 2]| q[0] * q[0] + q[1] * q[1]; + let ux = (sq(a) * (b[1] - c[1]) + sq(b) * (c[1] - a[1]) + sq(c) * (a[1] - b[1])) / d; + let uy = (sq(a) * (c[0] - b[0]) + sq(b) * (a[0] - c[0]) + sq(c) * (b[0] - a[0])) / d; + let o = [ux, uy]; + let r = dist2(o, a); + if r < 1e-14 || !r.is_finite() { + return None; + } + Some((o, r)) +} + +fn collinear2(a: [f64; 2], b: [f64; 2], c: [f64; 2]) -> bool { + let cross = (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]); + cross.abs() < 1e-8 * (1.0 + dist2(a, b) * dist2(a, c)) +} + +/// Second intersection of two circles, skipping `avoid` (the shared vertex). +fn circles_second_isect( + o1: [f64; 2], + r1: f64, + o2: [f64; 2], + r2: f64, + avoid: [f64; 2], +) -> Option<[f64; 2]> { + let dx = o2[0] - o1[0]; + let dy = o2[1] - o1[1]; + let d = dx.hypot(dy); + if d < 1e-12 || d > r1 + r2 + 1e-9 || d < (r1 - r2).abs() - 1e-9 { + return None; + } + let a = (r1 * r1 - r2 * r2 + d * d) / (2.0 * d); + let h2 = r1 * r1 - a * a; + if h2 < -1e-9 { + return None; + } + let h = h2.max(0.0).sqrt(); + let px = o1[0] + a * dx / d; + let py = o1[1] + a * dy / d; + let mx = -dy * h / d; + let my = dx * h / d; + let p_a = [px + mx, py + my]; + let p_b = [px - mx, py - my]; + let da = dist2(p_a, avoid); + let db = dist2(p_b, avoid); + if da < 1e-6 && db < 1e-6 { + return None; + } + Some(if da >= db { p_a } else { p_b }) +} + +pub fn lines_analyze(ls: &[NamedLine], points: &[NamedPoint]) -> Vec { + let mut out = Vec::new(); + for l in ls { + let nm = &l.name; + if !near(l.b, 0.0) { + let k = -l.a / l.b; + out.push(fact("直线", &format!("k_{{{}}}", nm), nice(k), Some(k), "y = kx + b", "斜率")); + let ang = k.atan().rem_euclid(PI); + out.push(fact("直线", &format!("\\alpha_{{{}}}", nm), deg(ang), Some(ang), "\\tan\\alpha = k", "倾斜角")); + } else { + out.push(fact("直线", &format!("\\alpha_{{{}}}", nm), "90^\\circ".into(), None, "", "竖直直线,斜率不存在")); + } + if !near(l.a, 0.0) { + out.push(fact("截距", &format!("{} \\cap x\\text{{轴}}", nm), pt(l.c / l.a, 0.0), None, "y = 0", "令 y = 0").with(Shape::Point { x: l.c / l.a, y: 0.0, label: format!("{nm} 与 x 轴") })); + } + if !near(l.b, 0.0) { + out.push(fact("截距", &format!("{} \\cap y\\text{{轴}}", nm), pt(0.0, l.c / l.b), None, "x = 0", "令 x = 0").with(Shape::Point { x: 0.0, y: l.c / l.b, label: format!("{nm} 与 y 轴") })); + } + let dist0 = l.c.abs() / l.a.hypot(l.b); + out.push(fact("距离", &format!("d(O, {})", nm), nice(dist0), Some(dist0), "d = \\frac{|Ax_0 + By_0 + C|}{\\sqrt{A^2 + B^2}}", "原点到直线的距离")); + for p in points { + let (x, y) = (p.p[0], *p.p.get(1).unwrap_or(&0.0)); + let d = (l.a * x + l.b * y - l.c).abs() / l.a.hypot(l.b); + let t = (l.c - l.a * x - l.b * y) / (l.a * l.a + l.b * l.b); + let foot = [x + t * l.a, y + t * l.b]; + if d < 1e-9 { + out.push(fact("点与直线", &format!("{} \\in {}", p.name, nm), "\\checkmark".into(), None, "", "代入方程成立")); + } else { + out.push(fact("点与直线", &format!("d({}, {})", p.name, nm), nice(d), Some(d), "d = \\frac{|Ax_0 + By_0 + C|}{\\sqrt{A^2 + B^2}}", "点到直线距离").with(Shape::Segment { x1: x, y1: y, x2: foot[0], y2: foot[1], label: format!("{} 到 {}", p.name, nm) })); + + let rp = [2.0 * foot[0] - x, 2.0 * foot[1] - y]; + out.push(fact( + "反射", + &format!("{}^{{\\prime}}_{{{}}}", p.name, nm), + pt(rp[0], rp[1]), + None, + "P' = 2\\,\\mathrm{foot} - P", + &format!("点 {} 关于直线 {} 的反射", p.name, nm), + ).with(Shape::Point { + x: rp[0], + y: rp[1], + label: format!("{}′/{}", p.name, nm), + })); + + } + } + } + let mut par_pairs: Vec<(usize, usize)> = Vec::new(); + for i in 0..ls.len() { + for j in i + 1..ls.len() { + let (p, q) = (&ls[i], &ls[j]); + let det = p.a * q.b - p.b * q.a; + let rel = format!("{} , {}", p.name, q.name); + let scale = p.a.hypot(p.b) * q.a.hypot(q.b); + if det.abs() < 1e-12 * scale { + let coincide = (p.a * q.c - q.a * p.c).abs() < 1e-9 * scale && (p.b * q.c - q.b * p.c).abs() < 1e-9 * scale; + if coincide { + out.push(fact("两直线", &rel, "\\text{重合}".into(), None, "\\frac{A_1}{A_2} = \\frac{B_1}{B_2} = \\frac{C_1}{C_2}", "系数成比例")); + } else { + let (np, nq) = (p.a.hypot(p.b), q.a.hypot(q.b)); + let same_dir = p.a * q.a + p.b * q.b > 0.0; + let cq = if same_dir { q.c / nq } else { -q.c / nq }; + let d = (p.c / np - cq).abs(); + out.push(fact("两直线", &format!("{} \\parallel {}", p.name, q.name), "\\checkmark".into(), None, "\\frac{A_1}{A_2} = \\frac{B_1}{B_2} \\ne \\frac{C_1}{C_2}", "斜率相等")); + out.push(fact("两直线", &format!("d({}, {})", p.name, q.name), nice(d), Some(d), "\\frac{|C_1 - C_2|}{\\sqrt{A^2+B^2}}", "平行线间距离")); + par_pairs.push((i, j)); + } + continue; + } + let x = (p.c * q.b - p.b * q.c) / det; + let y = (p.a * q.c - p.c * q.a) / det; + out.push(fact("两直线", &format!("{} \\cap {}", p.name, q.name), pt(x, y), None, "\\begin{cases} A_1x + B_1y = C_1 \\\\ A_2x + B_2y = C_2 \\end{cases}", "联立方程").with(Shape::Point { x, y, label: format!("{}∩{}", p.name, q.name) })); + let dt = p.a * q.a + p.b * q.b; + if dt.abs() < 1e-12 * scale { + out.push(fact("两直线", &format!("{} \\perp {}", p.name, q.name), "\\checkmark".into(), None, "k_1 k_2 = -1", "法向量点积为 0")); + } else { + let ang = (det.abs()).atan2(dt.abs()); + out.push(fact("两直线", &format!("\\theta({}, {})", p.name, q.name), deg(ang), Some(ang), "\\tan\\theta = \\left|\\frac{k_1 - k_2}{1 + k_1k_2}\\right|", "两直线夹角")); + } + } + } + // transversals: a third line cutting a parallel pair — equal corresponding / alternate + // interior angles, supplementary co-interior angles, and the segment cut off between + // the two parallels along the transversal. + for &(i, j) in &par_pairs { + for t in 0..ls.len() { + if t == i || t == j { + continue; + } + let (Some(pa), Some(pb)) = (line_isect(&ls[i], &ls[t]), line_isect(&ls[j], &ls[t])) else { continue }; + let (p, q, tr) = (&ls[i], &ls[j], &ls[t]); + let det = p.a * tr.b - p.b * tr.a; + let dt = p.a * tr.a + p.b * tr.b; + let theta = det.abs().atan2(dt.abs()); + out.push(fact("平行线的截线", &format!("\\text{{同位角}} \\; \\angle({}, {}) = \\angle({}, {})", tr.name, p.name, tr.name, q.name), deg(theta), Some(theta), "两直线平行,同位角相等", "同位角")); + out.push(fact("平行线的截线", &format!("\\text{{内错角}}({} \\mid {} \\parallel {})", tr.name, p.name, q.name), deg(theta), Some(theta), "两直线平行,内错角相等", "内错角")); + out.push(fact("平行线的截线", &format!("\\text{{同旁内角}}({} \\mid {} \\parallel {})", tr.name, p.name, q.name), deg(PI - theta), Some(PI - theta), "两直线平行,同旁内角互补", "同旁内角")); + let cut = dist2(pa, pb); + out.push(fact("平行线的截线", &format!("\\text{{截线段}}_{{{}}}", tr.name), nice(cut), Some(cut), "|PQ| = \\sqrt{\\Delta x^2 + \\Delta y^2}", "沿截线夹在两平行线之间的线段").with(Shape::Segment { x1: pa[0], y1: pa[1], x2: pb[0], y2: pb[1], label: format!("{} 截 {}", tr.name, p.name) })); + } + } + // Intercept theorem: three (or more) parallels cut by two transversals give proportional + // segments (平行线分线段成比例); two parallels cut by two transversals give similar + // triangles at the point where the transversals meet. + let dist_key = |l: &NamedLine| { + let n = l.a.hypot(l.b); + if n < 1e-12 { + return 0.0; + } + let sign = if l.a.abs() > 1e-12 { l.a.signum() } else { l.b.signum() }; + sign * l.c / n + }; + let crosses = |s: usize, p: usize| line_isect(&ls[s], &ls[p]).is_some(); + let s_name = |ix: usize| ls[ix].name.clone(); + let mut triple_count = 0usize; + 'triples: for i in 0..ls.len() { + for j in i + 1..ls.len() { + for k in j + 1..ls.len() { + if triple_count >= 2 { + break 'triples; + } + let is_par = |x: usize, y: usize| par_pairs.contains(&(x.min(y), x.max(y))); + if !(is_par(i, j) && is_par(i, k)) { + continue; + } + // order the three parallels along their common normal + let mut trio = [i, j, k]; + trio.sort_by(|x, y| dist_key(&ls[*x]).partial_cmp(&dist_key(&ls[*y])).unwrap()); + let (p, q, r) = (s_name(trio[0]), s_name(trio[1]), s_name(trio[2])); + let mut tcount = 0usize; + 'ts: for s in 0..ls.len() { + for t in s + 1..ls.len() { + if tcount >= 2 { + break 'ts; + } + if trio.contains(&s) || trio.contains(&t) || !crosses(s, trio[0]) || !crosses(s, trio[2]) || !crosses(t, trio[0]) || !crosses(t, trio[2]) { + continue; + } + if line_isect(&ls[s], &ls[t]).is_none() { + continue; // the two transversals must meet somewhere + } + let ps: Vec<[f64; 2]> = trio.iter().map(|&ix| line_isect(&ls[s], &ls[ix]).unwrap()).collect(); + let qt: Vec<[f64; 2]> = trio.iter().map(|&ix| line_isect(&ls[t], &ls[ix]).unwrap()).collect(); + let (l1, l2) = (dist2(ps[0], ps[1]), dist2(ps[1], ps[2])); + let (m1, m2) = (dist2(qt[0], qt[1]), dist2(qt[1], qt[2])); + if l1 * l2 * m1 * m2 < 1e-12 { + continue; + } + tcount += 1; + let sn = s_name(s); + let tn = s_name(t); + let seg = |x: &[f64; 2], y: &[f64; 2], nm: String| Shape::Segment { x1: x[0], y1: x[1], x2: y[0], y2: y[1], label: nm }; + out.push(seg_fact("平行线分线段", &format!("|{sn} \\cap {p},\\ {sn} \\cap {q}|"), l1, "\\sqrt{\\Delta x^2 + \\Delta y^2}", "第一条截线段").with(seg(&ps[0], &ps[1], format!("{sn} 上的截线段")))); + out.push(seg_fact("平行线分线段", &format!("|{sn} \\cap {q},\\ {sn} \\cap {r}|"), l2, "\\sqrt{\\Delta x^2 + \\Delta y^2}", "第二条截线段").with(seg(&ps[1], &ps[2], format!("{sn} 上的截线段")))); + out.push(seg_fact("平行线分线段", &format!("|{tn} \\cap {p},\\ {tn} \\cap {q}|"), m1, "\\sqrt{\\Delta x^2 + \\Delta y^2}", "对应的第一条截线段").with(seg(&qt[0], &qt[1], format!("{tn} 上的截线段")))); + out.push(seg_fact("平行线分线段", &format!("|{tn} \\cap {q},\\ {tn} \\cap {r}|"), m2, "\\sqrt{\\Delta x^2 + \\Delta y^2}", "对应的第二条截线段").with(seg(&qt[1], &qt[2], format!("{tn} 上的截线段")))); + out.push(fact("平行线分线段", "\\text{成比例}", nice(l1 / l2), Some(l1 / l2), &format!("\\frac{{|{sn} \\cap {p},\\ {sn} \\cap {q}|}}{{|{sn} \\cap {q},\\ {sn} \\cap {r}|}} = \\frac{{|{tn} \\cap {p},\\ {tn} \\cap {q}|}}{{|{tn} \\cap {q},\\ {tn} \\cap {r}|}} = {}", nice(m1 / m2)), "平行线分线段成比例定理:三条平行线在两条截线上截出的线段对应成比例")); + } + } + triple_count += 1; + } + } + } + // similar triangles: two parallels + two transversals meeting at X + let mut sim_count = 0usize; + 'sims: for &(i, j) in &par_pairs { + for s in 0..ls.len() { + for t in s + 1..ls.len() { + if sim_count >= 2 { + break 'sims; + } + if s == i || s == j || t == i || t == j { + continue; + } + let (Some(x), Some(a), Some(c)) = (line_isect(&ls[s], &ls[t]), line_isect(&ls[s], &ls[i]), line_isect(&ls[s], &ls[j])) else { continue }; + let (Some(b), Some(d)) = (line_isect(&ls[t], &ls[i]), line_isect(&ls[t], &ls[j])) else { continue }; + let (k1n, k1d) = (dist2(x, a), dist2(x, c)); + let (k2n, k2d) = (dist2(x, b), dist2(x, d)); + if k1d < 1e-9 || k2d < 1e-9 || k1n < 1e-9 || k2n < 1e-9 { + continue; // transversals meet on one of the parallels + } + sim_count += 1; + let k1 = k1n / k1d; + let k2 = k2n / k2d; + let value = if near(k1, k2) { nice(k1) } else { format!("{} \\approx {}", nice(k1), nice(k2)) }; + let (pn, qn, sn, tn) = (ls[i].name.clone(), ls[j].name.clone(), ls[s].name.clone(), ls[t].name.clone()); + out.push(fact("相似三角形", &format!("\\text{{相似比}}_{{{}}}", pn), value, Some(k1), &format!("\\frac{{|X({sn} \\cap {pn})|}}{{|X({sn} \\cap {qn})|}} = \\frac{{|X({tn} \\cap {pn})|}}{{|X({tn} \\cap {qn})|}} = {}", nice(k1)), "平行于三角形一边的直线截其他两边,所得三角形与原三角形相似(X = 两截线的交点)")); + } + } + } + + // Miquel point of a complete quadrilateral (4 lines, 6 vertices). + if ls.len() >= 4 { + let n = ls.len().min(6); + 'miquel: for i0 in 0..n { + for i1 in (i0 + 1)..n { + for i2 in (i1 + 1)..n { + for i3 in (i2 + 1)..n { + let idx = [i0, i1, i2, i3]; + let mut ok_pairs = true; + for a in 0..4 { + for b in (a + 1)..4 { + if line_isect(&ls[idx[a]], &ls[idx[b]]).is_none() { + ok_pairs = false; + } + } + } + if !ok_pairs { + continue; + } + let tri = |a: usize, b: usize, c: usize| -> Option<[[f64; 2]; 3]> { + Some([ + line_isect(&ls[idx[a]], &ls[idx[b]])?, + line_isect(&ls[idx[b]], &ls[idx[c]])?, + line_isect(&ls[idx[c]], &ls[idx[a]])?, + ]) + }; + let (Some(t012), Some(t013), Some(t023), Some(t123)) = + (tri(0, 1, 2), tri(0, 1, 3), tri(0, 2, 3), tri(1, 2, 3)) + else { + continue; + }; + if collinear2(t012[0], t012[1], t012[2]) + || collinear2(t013[0], t013[1], t013[2]) + || collinear2(t023[0], t023[1], t023[2]) + || collinear2(t123[0], t123[1], t123[2]) + { + continue; + } + let (Some((o1, r1)), Some((o2, r2))) = + (circum2(t012[0], t012[1], t012[2]), circum2(t013[0], t013[1], t013[2])) + else { + continue; + }; + let shared = line_isect(&ls[idx[0]], &ls[idx[1]]).unwrap(); + let Some(m) = circles_second_isect(o1, r1, o2, r2, shared) else { + continue; + }; + let (Some((o3, r3)), Some((o4, r4))) = + (circum2(t023[0], t023[1], t023[2]), circum2(t123[0], t123[1], t123[2])) + else { + continue; + }; + if (dist2(m, o3) - r3).abs() > 1e-5 * (1.0 + r3) + || (dist2(m, o4) - r4).abs() > 1e-5 * (1.0 + r4) + { + continue; + } + let names = format!( + "{},{},{},{}", + ls[idx[0]].name, ls[idx[1]].name, ls[idx[2]].name, ls[idx[3]].name + ); + out.push(fact( + "米克尔点", + "\\text{Miquel}", + pt(m[0], m[1]), + None, + "\\odot(L_iL_jL_k)\\ \\text{共点}", + &format!("完全四线形 {{{names}}} 的四个三点形外接圆交于米克尔点"), + ).with(Shape::Point { + x: m[0], + y: m[1], + label: "Miquel".into(), + })); + out.push(fact( + "米克尔点", + "M", + pt(m[0], m[1]), + None, + "", + "米克尔点坐标", + )); + break 'miquel; + } + } + } + } + } + out +} + +// --------------------------------------------------------------------------- +// Planes (a x + b y + c z = d) + +#[derive(Deserialize)] +pub struct NamedPlane { + pub name: String, + pub n: [f64; 3], + pub d: f64, +} + +pub fn planes_analyze(ps: &[NamedPlane], points: &[NamedPoint]) -> Vec { + let mut out = Vec::new(); + for p in ps { + let ln = len3(p.n); + out.push(fact("平面", &format!("\\vec n_{{{}}}", p.name), pt3(p.n), None, "ax + by + cz = d \\Rightarrow \\vec n = (a,b,c)", "法向量").with(Shape::Arrow3 { x: p.n[0], y: p.n[1], z: p.n[2], label: format!("n_{}", p.name) })); + let d0 = p.d.abs() / ln; + out.push(fact("平面", &format!("d(O, {})", p.name), nice(d0), Some(d0), "\\frac{|d|}{|\\vec n|}", "原点到平面的距离")); + let mut ints = Vec::new(); + for (k, ax) in ["x", "y", "z"].iter().enumerate() { + if !near(p.n[k], 0.0) { + ints.push(format!("{}={}", ax, nice_short(p.d / p.n[k]))); + } + } + if !ints.is_empty() { + out.push(fact("平面", &format!("\\text{{截距 }}{}", p.name), ints.join(",\\ "), None, "", "令另外两个坐标为 0")); + } + for q in points { + let x = [q.p[0], *q.p.get(1).unwrap_or(&0.0), *q.p.get(2).unwrap_or(&0.0)]; + let d = (dot3(p.n, x) - p.d).abs() / ln; + out.push(fact("点与平面", &format!("d({}, {})", q.name, p.name), nice(d), Some(d), "\\frac{|ax_0+by_0+cz_0-d|}{\\sqrt{a^2+b^2+c^2}}", "点到平面距离")); + if ln > 1e-12 && d > 1e-8 && d.is_finite() { + let lam = (p.d - dot3(p.n, x)) / (ln * ln); + let foot = [x[0] + lam * p.n[0], x[1] + lam * p.n[1], x[2] + lam * p.n[2]]; + out.push(fact( + "点与平面", + &format!("\\text{{foot}}({}, {})", q.name, p.name), + pt3(foot), + None, + "P + \\frac{d - \\vec n\\cdot P}{|\\vec n|^2}\\vec n", + "点到平面的垂足", + ).with(Shape::Point3 { x: foot[0], y: foot[1], z: foot[2], label: format!("foot {} {}", q.name, p.name) })); + + let rp = [2.0 * foot[0] - x[0], 2.0 * foot[1] - x[1], 2.0 * foot[2] - x[2]]; + out.push(fact( + "反射", + &format!("{}^{{\\prime}}_{{{}}}", q.name, p.name), + pt3(rp), + None, + "P' = 2\\,\\mathrm{foot} - P", + &format!("点 {} 关于平面 {} 的反射", q.name, p.name), + ).with(Shape::Point3 { + x: rp[0], + y: rp[1], + z: rp[2], + label: format!("{}′/{}", q.name, p.name), + })); + + } + } + } + // Line–plane angle: every pair of 3D points defines a line. + { + let pts3: Vec<(String, [f64; 3])> = points + .iter() + .filter(|q| q.p.len() >= 3) + .map(|q| (q.name.clone(), [q.p[0], q.p[1], q.p[2]])) + .collect(); + let mut pair_count = 0usize; + 'pairs: for i in 0..pts3.len() { + for j in i + 1..pts3.len() { + if pair_count >= 6 { + break 'pairs; + } + let (ref na, pa) = pts3[i]; + let (ref nb, pb) = pts3[j]; + let dir = sub3(pb, pa); + let ld = len3(dir); + if ld < 1e-12 { + continue; + } + pair_count += 1; + for p in ps { + let ln = len3(p.n); + if ln < 1e-12 { + continue; + } + let sin_phi = (dot3(dir, p.n).abs() / (ld * ln)).clamp(0.0, 1.0); + let phi = sin_phi.asin(); // angle between line and plane ∈ [0, π/2] + out.push(fact( + "线面角", + &format!("\\theta({}{}, {})", na, nb, p.name), + deg(phi), + Some(phi), + "\\sin\\theta = \\dfrac{|\\vec v\\cdot\\vec n|}{|\\vec v||\\vec n|}", + "直线与平面所成的角", + )); + if sin_phi < 1e-9 { + out.push(fact( + "线面角", + &format!("{}{} \\parallel {}", na, nb, p.name), + "\\checkmark".into(), + None, + "\\vec v\\cdot\\vec n = 0", + "直线平行于平面(或落在平面上)", + )); + } else if (1.0 - sin_phi).abs() < 1e-9 { + out.push(fact( + "线面角", + &format!("{}{} \\perp {}", na, nb, p.name), + "\\checkmark".into(), + None, + "|\\vec v\\cdot\\vec n| = |\\vec v||\\vec n|", + "直线垂直于平面", + )); + } + } + } + } + } + for i in 0..ps.len() { + for j in i + 1..ps.len() { + let (p, q) = (&ps[i], &ps[j]); + let dirv = cross3(p.n, q.n); + let (lp, lq) = (len3(p.n), len3(q.n)); + if len3(dirv) < 1e-9 * lp * lq { + let f = dot3(p.n, q.n).signum() * lq / lp; + let d = (q.d - p.d * f).abs() / lq; + out.push(fact("两平面", &format!("{} \\parallel {}", p.name, q.name), if d < 1e-9 { "\\text{重合}".into() } else { "\\checkmark".into() }, None, "\\vec n_1 \\parallel \\vec n_2", "法向量共线")); + if d > 1e-9 { + out.push(fact("两平面", &format!("d({}, {})", p.name, q.name), nice(d), Some(d), "", "平行平面间距离")); + } + continue; + } + let ang = (dot3(p.n, q.n).abs() / (lp * lq)).clamp(-1.0, 1.0).acos(); + out.push(fact("两平面", &format!("\\theta({}, {})", p.name, q.name), deg(ang), Some(ang), "\\cos\\theta = \\frac{|\\vec n_1\\cdot\\vec n_2|}{|\\vec n_1||\\vec n_2|}", "二面角(锐角)")); + out.push(fact( + "两平面", + &format!("\\text{{二面角}}({}, {})", p.name, q.name), + deg(ang), + Some(ang), + "\\cos\\theta = \\frac{|\\vec n_1\\cdot\\vec n_2|}{|\\vec n_1||\\vec n_2|}", + "二面角等于两法向量夹角(取锐角)", + )); + if dot3(p.n, q.n).abs() < 1e-9 * lp * lq { + out.push(fact("两平面", &format!("{} \\perp {}", p.name, q.name), "\\checkmark".into(), None, "\\vec n_1\\cdot\\vec n_2 = 0", "法向量垂直")); + } + // point on the line: least-norm solution of the 2x3 system via n1, n2 basis + let n11 = dot3(p.n, p.n); + let n22 = dot3(q.n, q.n); + let n12 = dot3(p.n, q.n); + let det = n11 * n22 - n12 * n12; + let c1 = (p.d * n22 - q.d * n12) / det; + let c2 = (q.d * n11 - p.d * n12) / det; + let p0 = [c1 * p.n[0] + c2 * q.n[0], c1 * p.n[1] + c2 * q.n[1], c1 * p.n[2] + c2 * q.n[2]]; + let p0 = p0.map(clean); + let g = gcd_scale(dirv); + out.push(fact("两平面", &format!("{} \\cap {}", p.name, q.name), format!("{} + t{}", pt3(p0), pt3(g)), None, "\\vec s = \\vec n_1 \\times \\vec n_2", "交线(参数式)").with(Shape::Line3 { px: p0[0], py: p0[1], pz: p0[2], dx: g[0], dy: g[1], dz: g[2], label: format!("{}∩{}", p.name, q.name) })); + } + } + if ps.len() >= 3 { + let (p, q, r) = (&ps[0], &ps[1], &ps[2]); + let m = vec![p.n.to_vec(), q.n.to_vec(), r.n.to_vec()]; + let det = dot3(p.n, cross3(q.n, r.n)); + if det.abs() > 1e-12 { + if let Some(x) = solve_linear_pub(m, vec![p.d, q.d, r.d]) { + let x = [clean(x[0]), clean(x[1]), clean(x[2])]; + out.push(fact("三平面", &format!("{} \\cap {} \\cap {}", p.name, q.name, r.name), pt3(x), None, "\\text{克拉默法则}", "三个平面交于一点").with(Shape::Point3 { x: x[0], y: x[1], z: x[2], label: "三平面交点".into() })); + } + } else { + out.push(fact("三平面", "\\text{交点}", "\\text{不唯一}".into(), None, "\\det = 0", "法向量共面:交于一线或无公共点")); + } + } + out +} + +/// Scale a direction vector to small integers when possible. +fn gcd_scale(v: [f64; 3]) -> [f64; 3] { + let m = v.iter().fold(0.0f64, |a, b| a.max(b.abs())); + if m == 0.0 { + return v; + } + for k in 1..=12 { + let s = v.map(|x| x / m * k as f64); + if s.iter().all(|x| (x - x.round()).abs() < 1e-9) { + let ints: Vec = s.iter().map(|x| x.round() as i64).collect(); + let g = ints.iter().fold(0i64, |a, &b| gcd(a, b.abs())); + let g = g.max(1) as f64; + let sign = if ints.iter().find(|x| **x != 0).map_or(1, |x| x.signum()) < 0 { -1.0 } else { 1.0 }; + return s.map(|x| clean(sign * x.round() / g)); + } + } + v.map(clean) +} +fn gcd(a: i64, b: i64) -> i64 { + if b == 0 { a } else { gcd(b, a % b) } +} + +// --------------------------------------------------------------------------- +// Conics from A x² + B xy + C y² + D x + E y + F = 0 + +pub fn conic_analyze(c: [f64; 6], name: &str) -> Vec { + let [a, b, cc, d, e, f] = c; + let mut out = Vec::new(); + let det = 4.0 * a * cc - b * b; + if near(a, 0.0) && near(b, 0.0) && near(cc, 0.0) { + return out; + } + if near(det, 0.0) { + // parabola (axis-aligned cases) + if near(b, 0.0) && near(a, 0.0) && !near(d, 0.0) { + let y0 = -e / (2.0 * cc); let x0 = -(cc * y0 * y0 + e * y0 + f) / d; let p = -d / (4.0 * cc); out.push(fact("圆锥曲线", name, "\\text{抛物线(开口沿 x 轴)}".into(), None, "(y-y_0)^2 = 4p(x-x_0)", "配方")); @@ -1274,427 +17526,5916 @@ pub fn conic_analyze(c: [f64; 6], name: &str) -> Vec { out.push(fact("圆锥曲线", "\\text{焦点}", pt(x0, y0 + p), None, "F(x_0,\\ y_0 + p)", "焦点").with(Shape::Point { x: x0, y: y0 + p, label: "焦点 F".into() })); out.push(fact("圆锥曲线", "\\text{准线}", format!("y = {}", nice_short(y0 - p)), None, "y = y_0 - p", "准线")); } else { - out.push(fact("圆锥曲线", name, "\\text{抛物线型(退化或斜置)}".into(), None, "B^2 - 4AC = 0", "判别式")); + out.push(fact("圆锥曲线", name, "\\text{抛物线型(退化或斜置)}".into(), None, "B^2 - 4AC = 0", "判别式")); + } + return out; + } + let x0 = (b * e - 2.0 * cc * d) / det; + let y0 = (b * d - 2.0 * a * e) / det; + let f0 = a * x0 * x0 + b * x0 * y0 + cc * y0 * y0 + d * x0 + e * y0 + f; + let tr = a + cc; + let disc = ((a - cc).powi(2) + b * b).sqrt(); + let l1 = (tr + disc) / 2.0; + let l2 = (tr - disc) / 2.0; + let theta = 0.5 * b.atan2(a - cc); + if det > 0.0 { + if (f0 > 0.0 && l1 > 0.0) || (f0 < 0.0 && l1 < 0.0) { + out.push(fact("圆锥曲线", name, "\\text{无实点(虚椭圆)}".into(), None, "", "配方后右侧为负")); + return out; + } + if near(f0, 0.0) { + out.push(fact("圆锥曲线", name, "\\text{退化为一点}".into(), None, "", "").with(Shape::Point { x: x0, y: y0, label: "点".into() })); + return out; + } + let r1 = (-f0 / l1).sqrt(); + let r2 = (-f0 / l2).sqrt(); + if near(a, cc) && near(b, 0.0) { + out.push(fact("圆锥曲线", name, "\\text{圆}".into(), None, "(x-a)^2 + (y-b)^2 = r^2", "配方")); + out.push(fact("圆锥曲线", "\\text{圆心}", pt(x0, y0), None, "\\left(-\\frac{D}{2},\\ -\\frac{E}{2}\\right)", "配方").with(Shape::Point { x: x0, y: y0, label: "圆心".into() })); + out.push(fact("圆锥曲线", "r", nice(r1), Some(r1), "r = \\frac12\\sqrt{D^2 + E^2 - 4F}", "半径")); + let area = PI * r1 * r1; + out.push(fact("圆锥曲线", "S", nice(area), Some(area), "\\pi r^2", "圆面积")); + out.push(fact("圆锥曲线", "C", nice(2.0 * PI * r1), Some(2.0 * PI * r1), "2\\pi r", "周长")); + let d0 = x0.hypot(y0); + let rel = if near(d0, r1) { "原点在圆上" } else if d0 < r1 { "原点在圆内" } else { "原点在圆外" }; + out.push(fact("圆锥曲线", "O", format!("\\text{{{}}}", rel), None, "|OC| \\text{ vs } r", "比较距离与半径")); + return out; + } + // semi-major is the larger radius; r1 belongs to l1 (larger eigenvalue => smaller radius) + let (big, small, ang) = if r2 >= r1 { (r2, r1, theta + PI / 2.0) } else { (r1, r2, theta) }; + let cfoc = (big * big - small * small).sqrt(); + let ecc = cfoc / big; + out.push(fact("圆锥曲线", name, "\\text{椭圆}".into(), None, "\\frac{x'^2}{a^2} + \\frac{y'^2}{b^2} = 1", "判别式 B^2 - 4AC < 0")); + out.push(fact("圆锥曲线", "\\text{中心}", pt(x0, y0), None, "\\nabla F = 0", "求中心").with(Shape::Point { x: x0, y: y0, label: "中心".into() })); + out.push(fact("圆锥曲线", "a", nice(big), Some(big), "", "长半轴")); + out.push(fact("圆锥曲线", "b", nice(small), Some(small), "", "短半轴")); + out.push(fact("圆锥曲线", "c", nice(cfoc), Some(cfoc), "c^2 = a^2 - b^2", "半焦距")); + out.push(fact("圆锥曲线", "e", nice(ecc), Some(ecc), "e = \\frac{c}{a}", "离心率")); + let (fx, fy) = (cfoc * ang.cos(), cfoc * ang.sin()); + out.push(fact("圆锥曲线", "F_1", pt(x0 + fx, y0 + fy), None, "", "焦点").with(Shape::Point { x: x0 + fx, y: y0 + fy, label: "F₁".into() })); + out.push(fact("圆锥曲线", "F_2", pt(x0 - fx, y0 - fy), None, "", "焦点").with(Shape::Point { x: x0 - fx, y: y0 - fy, label: "F₂".into() })); + out.push(fact("圆锥曲线", "S", nice(PI * big * small), Some(PI * big * small), "\\pi a b", "椭圆面积")); + } else { + if near(f0, 0.0) { + out.push(fact("圆锥曲线", name, "\\text{退化为两条相交直线}".into(), None, "", "")); + return out; + } + // hyperbola: transverse axis along the eigen-direction where -f0/λ > 0 + let (lt, lc_, ang) = if -f0 / l1 > 0.0 { (l1, l2, theta) } else { (l2, l1, theta + PI / 2.0) }; + let at = (-f0 / lt).sqrt(); + let bt = (f0 / lc_).sqrt(); + let cfoc = (at * at + bt * bt).sqrt(); + out.push(fact("圆锥曲线", name, "\\text{双曲线}".into(), None, "\\frac{x'^2}{a^2} - \\frac{y'^2}{b^2} = 1", "判别式 B^2 - 4AC > 0")); + out.push(fact("圆锥曲线", "\\text{中心}", pt(x0, y0), None, "", "求中心").with(Shape::Point { x: x0, y: y0, label: "中心".into() })); + out.push(fact("圆锥曲线", "a", nice(at), Some(at), "", "实半轴")); + out.push(fact("圆锥曲线", "b", nice(bt), Some(bt), "", "虚半轴")); + out.push(fact("圆锥曲线", "e", nice(cfoc / at), Some(cfoc / at), "e = \\frac{c}{a},\\ c^2 = a^2 + b^2", "离心率")); + let (fx, fy) = (cfoc * ang.cos(), cfoc * ang.sin()); + out.push(fact("圆锥曲线", "F_1", pt(x0 + fx, y0 + fy), None, "", "焦点").with(Shape::Point { x: x0 + fx, y: y0 + fy, label: "F₁".into() })); + out.push(fact("圆锥曲线", "F_2", pt(x0 - fx, y0 - fy), None, "", "焦点").with(Shape::Point { x: x0 - fx, y: y0 - fy, label: "F₂".into() })); + if near(b, 0.0) { + let k = bt / at; + let k = if near(ang.sin(), 0.0) { k } else { 1.0 / k }; + out.push(fact("圆锥曲线", "\\text{渐近线}", format!("y - {} = \\pm {}(x - {})", nice_short(y0), nice_short(k), nice_short(x0)), None, "y = \\pm\\frac{b}{a}x", "渐近线")); + } + } + out +} + +// --------------------------------------------------------------------------- +// Derived points: midpoints (中点) and section points (定比分点) + +#[derive(Deserialize)] +pub struct PointDef { + pub name: String, + pub a: String, + pub b: String, + /// AP : PB = m : n; both None → midpoint + #[serde(default)] + pub m: Option, + #[serde(default)] + pub n: Option, +} + +fn distn(a: &[f64], b: &[f64]) -> f64 { + (0..a.len().max(b.len())).map(|t| (a.get(t).unwrap_or(&0.0) - b.get(t).unwrap_or(&0.0)).powi(2)).sum::().sqrt() +} + +pub fn derived_points_analyze(defs: &[PointDef], points: &[NamedPoint]) -> Vec { + let lookup = |nm: &str| points.iter().find(|p| p.name == nm).map(|p| p.p.clone()); + let mut out = Vec::new(); + for d in defs { + let (Some(pa), Some(pb)) = (lookup(&d.a), lookup(&d.b)) else { continue }; + let (m, n) = match (d.m, d.n) { + (Some(m), Some(n)) => (m, n), + (Some(m), None) => (m, 1.0), + (None, Some(n)) => (1.0, n), + (None, None) => (1.0, 1.0), + }; + if m + n == 0.0 { + continue; + } + let is_mid = near(m, n); + let p: Vec = (0..pa.len().max(pb.len())) + .map(|t| (n * pa.get(t).unwrap_or(&0.0) + m * pb.get(t).unwrap_or(&0.0)) / (m + n)) + .collect(); + let f3 = p.len() == 3; + let pval = if f3 { pt3([p[0], p[1], p[2]]) } else { pt(p[0], p[1]) }; + let how = if is_mid { + if f3 { "\\left(\\frac{x_A+x_B}{2},\\ \\frac{y_A+y_B}{2},\\ \\frac{z_A+z_B}{2}\\right)".to_string() } else { "\\left(\\frac{x_A+x_B}{2},\\ \\frac{y_A+y_B}{2}\\right)".to_string() } + } else { + format!("P = \\frac{{{n}A + {m}B}}{{{m} + {n}}}") + }; + out.push(fact("中点与分点", &d.name, pval, None, &how, if is_mid { "中点公式" } else { "定比分点公式" }).with(Shape::Point { x: p[0], y: *p.get(1).unwrap_or(&0.0), label: d.name.clone() })); + let (dap, dpb) = (distn(&pa, &p), distn(&p, &pb)); + if dpb > 1e-12 { + let ms = nice_short(m); + let ns = nice_short(n); + out.push(fact("中点与分点", &format!("\\frac{{|{}{}|}}{{|{}{}|}}", d.a, d.name, d.name, d.b), nice(dap / dpb), Some(dap / dpb), &format!("AP : PB = {ms} : {ns}"), if is_mid { "中点到两端等距" } else { "定比分点:AP : PB = m : n" })); + } + out.push(seg_fact("中点与分点", &format!("|{}{}|", d.a, d.name), dap, "\\sqrt{\\Delta x^2 + \\Delta y^2}", "到起点的距离").with(Shape::Segment { x1: pa[0], y1: *pa.get(1).unwrap_or(&0.0), x2: p[0], y2: *p.get(1).unwrap_or(&0.0), label: format!("{}{}", d.a, d.name) })); + out.push(seg_fact("中点与分点", &format!("|{}{}|", d.name, d.b), dpb, "\\sqrt{\\Delta x^2 + \\Delta y^2}", "到终点的距离").with(Shape::Segment { x1: p[0], y1: *p.get(1).unwrap_or(&0.0), x2: pb[0], y2: *pb.get(1).unwrap_or(&0.0), label: format!("{}{}", d.name, d.b) })); + } + // two midpoints sharing an endpoint form a midsegment (中位线) + let mid_of = |a: &str, b: &str| -> Option> { + let (pa, pb) = (lookup(a)?, lookup(b)?); + Some((0..pa.len().max(pb.len())).map(|t| (pa.get(t).unwrap_or(&0.0) + pb.get(t).unwrap_or(&0.0)) / 2.0).collect()) + }; + let is_mid_def = |d: &PointDef| match (d.m, d.n) { + (None, None) => true, + (Some(m), Some(n)) => near(m, n), + _ => false, + }; + for i in 0..defs.len() { + for j in i + 1..defs.len() { + let (d1, d2) = (&defs[i], &defs[j]); + if !is_mid_def(d1) || !is_mid_def(d2) { + continue; + } + let (vertex, u, v) = if d1.a == d2.a { + (d1.a.clone(), d1.b.clone(), d2.b.clone()) + } else if d1.a == d2.b { + (d1.a.clone(), d1.b.clone(), d2.a.clone()) + } else if d1.b == d2.a { + (d1.b.clone(), d1.a.clone(), d2.b.clone()) + } else if d1.b == d2.b { + (d1.b.clone(), d1.a.clone(), d2.a.clone()) + } else { + continue; + }; + if u == v { + continue; + } + let (Some(pu), Some(pv)) = (lookup(&u), lookup(&v)) else { continue }; + let (Some(pm), Some(pn2)) = (mid_of(&vertex, &u), mid_of(&vertex, &v)) else { continue }; + let len = distn(&pm, &pn2); + let base = distn(&pu, &pv); + if base < 1e-12 { + continue; + } + out.push(seg_fact("中点与分点", &format!("\\text{{中位线}}_{{{}{}}}", d1.name, d2.name), len, &format!("\\frac{{1}}{{2}}|{u}{v}|"), "中位线定理:连接两个中点的线段平行于第三边,且等于第三边的一半").with(Shape::Segment { x1: pm[0], y1: *pm.get(1).unwrap_or(&0.0), x2: pn2[0], y2: *pn2.get(1).unwrap_or(&0.0), label: format!("中位线{}{}", d1.name, d2.name) })); + out.push(fact("中点与分点", &format!("{}{} \\parallel {}{}", d1.name, d2.name, u, v), "\\checkmark".into(), None, "\\vec{MN} = \\frac12\\vec{UV}", "中位线平行于第三边")); + } + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn right_triangle() { + let k = TriangleKnown { a: Some(3.0), b: Some(4.0), angle_c: Some(PI / 2.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none()); + let c = r.cases[0].facts.iter().find(|f| f.label == "c").unwrap(); + assert!((c.num.unwrap() - 5.0).abs() < 1e-9); + let s = r.cases[0].facts.iter().find(|f| f.label == "S").unwrap(); + assert!((s.num.unwrap() - 6.0).abs() < 1e-9); + } + + #[test] + fn ssa_two_solutions() { + let k = TriangleKnown { a: Some(6.0), b: Some(8.0), angle_a: Some(PI / 6.0), ..Default::default() }; + let r = triangle_solve(&k); + assert_eq!(r.cases.len(), 2); + } + + #[test] + fn asa_two_angles() { + // a = 5, B = 50°, C = 60° → A = 70°, b = 5·sin50°/sin70°, c = 5·sin60°/sin70° + let k = TriangleKnown { a: Some(5.0), angle_b: Some(50.0f64.to_radians()), angle_c: Some(60.0f64.to_radians()), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let f = |label: &str| r.cases[0].facts.iter().find(|x| x.label == label).unwrap().num.unwrap(); + assert!((f("\\angle A") - 70.0f64.to_radians()).abs() < 1e-9); + assert!((f("b") - 5.0 * 50f64.to_radians().sin() / 70f64.to_radians().sin()).abs() < 1e-9); + assert!((f("c") - 5.0 * 60f64.to_radians().sin() / 70f64.to_radians().sin()).abs() < 1e-9); + } + + #[test] + fn midsegment_and_bisector() { + // 3-4-5 triangle: midsegment parallel to a(=3) is 1.5; the bisector from A… check t_c + let k = TriangleKnown { a: Some(3.0), b: Some(4.0), angle_c: Some(PI / 2.0), ..Default::default() }; + let r = triangle_solve(&k); + let fs = &r.cases[0].facts; + let mid = fs.iter().find(|f| f.label == "\\text{中位线}_a").unwrap(); + assert!((mid.num.unwrap() - 1.5).abs() < 1e-9); + let mid_c = fs.iter().find(|f| f.label == "\\text{中位线}_c").unwrap(); + assert!((mid_c.num.unwrap() - 2.5).abs() < 1e-9); + // angle bisector from C: t_c = 2ab·cos(C/2)/(a+b) = 2·3·4·cos45°/7 + let t_c = fs.iter().find(|f| f.label == "t_c").unwrap(); + assert!((t_c.num.unwrap() - 24.0 * (PI / 4.0).cos() / 7.0).abs() < 1e-9); + // the drawn bisector starts at C and divides AB as AC : BC = 4 : 3 + let seg = t_c.shape.as_ref().unwrap(); + if let Shape::Segment { x1, y1, x2, y2: _, .. } = seg { + let (va, vb, vc) = (r.cases[0].vertices[0], r.cases[0].vertices[1], r.cases[0].vertices[2]); + assert!((*x1 - vc[0]).abs() < 1e-9 && (*y1 - vc[1]).abs() < 1e-9); + let fx = (3.0 * va[0] + 4.0 * vb[0]) / 7.0; + assert!((*x2 - fx).abs() < 1e-9); + } else { + panic!("bisector fact should carry a segment"); + } + } + + #[test] + fn nice_forms() { + assert_eq!(nice(2f64.sqrt() * 3.0), "3\\sqrt{2} \\approx 4.2426"); + assert_eq!(nice(0.5), "\\frac{1}{2} = 0.5"); + assert_eq!(nice(5.0), "5"); + } + + #[test] + fn circle() { + // x^2 + y^2 - 4x + 2y - 4 = 0 => centre (2,-1), r = 3 + let f = conic_analyze([1.0, 0.0, 1.0, -4.0, 2.0, -4.0], "C"); + let r = f.iter().find(|f| f.label == "r").unwrap(); + assert!((r.num.unwrap() - 3.0).abs() < 1e-9); + } + + #[test] + fn hl_hypotenuse_leg() { + // HL written as SSA with the right angle: a = 3, c = 5, C = 90° → b = 4, one case + let k = TriangleKnown { a: Some(3.0), c: Some(5.0), angle_c: Some(PI / 2.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + assert_eq!(r.cases.len(), 1); + let f = |label: &str| r.cases[0].facts.iter().find(|x| x.label == label).unwrap().num.unwrap(); + assert!((f("b") - 4.0).abs() < 1e-9); + assert!((f("\\angle A") - 3f64.atan2(4.0)).abs() < 1e-9); + } + + #[test] + fn median_as_input() { + // b = 4, c = 3, m_a = 2.5 → a = 5 (the 3-4-5 triangle) + let k = TriangleKnown { b: Some(4.0), c: Some(3.0), ma: Some(2.5), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let f = |label: &str| r.cases[0].facts.iter().find(|x| x.label == label).unwrap().num.unwrap(); + assert!((f("a") - 5.0).abs() < 1e-9); + // the other direction: a and b with the median give c + let k = TriangleKnown { a: Some(5.0), b: Some(4.0), ma: Some(2.5), ..Default::default() }; + let r = triangle_solve(&k); + let f = |label: &str| r.cases[0].facts.iter().find(|x| x.label == label).unwrap().num.unwrap(); + assert!((f("c") - 3.0).abs() < 1e-9); + } + + #[test] + fn height_as_input() { + // a = 3, c = 5, h_c = 2.4 → S = 6 → sin B = 0.8: B = 53.13° or 126.87° + let k = TriangleKnown { a: Some(3.0), c: Some(5.0), hc: Some(2.4), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + assert_eq!(r.cases.len(), 2); + let f = |i: usize, label: &str| r.cases[i].facts.iter().find(|x| x.label == label).unwrap().num.unwrap(); + assert!((f(0, "b") - 4.0).abs() < 1e-9); + assert!((f(0, "S") - 6.0).abs() < 1e-9); + } + + #[test] + fn angles_plus_area() { + // A = B = 60°, S = 3√3/4 → equilateral with side √3 + let s = 3.0 * 3f64.sqrt() / 4.0; + let k = TriangleKnown { angle_a: Some(PI / 3.0), angle_b: Some(PI / 3.0), area: Some(s), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let f = |label: &str| r.cases[0].facts.iter().find(|x| x.label == label).unwrap().num.unwrap(); + assert!((f("a") - 3f64.sqrt()).abs() < 1e-9); + assert!((f("R") - 1.0).abs() < 1e-9); + } + + #[test] + fn right_triangle_projection_theorem() { + // 3-4-5: altitude to hypotenuse 12/5, projections 9/5 and 16/5 + let k = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r = triangle_solve(&k); + let fs = &r.cases[0].facts; + let get = |label: &str| fs.iter().find(|f| f.label == label).unwrap(); + assert!((get("\\text{斜边上的高}").num.unwrap() - 12.0 / 5.0).abs() < 1e-9); + assert!((get("\\text{射影}_{a}").num.unwrap() - 9.0 / 5.0).abs() < 1e-9); + assert!((get("\\text{射影}_{b}").num.unwrap() - 16.0 / 5.0).abs() < 1e-9); + // the projection segments lie on the hypotenuse and share the altitude foot + if let (Shape::Segment { x2: x2a, y2: y2a, .. }, Shape::Segment { x2: x2b, y2: y2b, .. }) = (get("\\text{射影}_{a}").shape.as_ref().unwrap(), get("\\text{射影}_{b}").shape.as_ref().unwrap()) { + assert!((x2a - x2b).abs() < 1e-9 && (y2a - y2b).abs() < 1e-9, "both projections end at the altitude foot"); + } else { + panic!("projection facts should carry segments"); + } + } + + #[test] + fn inconsistent_givens_rejected() { + // SSS plus a wrong area + let k = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), area: Some(10.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_some()); + // SAS plus a wrong extra angle + let k = TriangleKnown { a: Some(5.0), b: Some(7.0), angle_c: Some(PI / 3.0), angle_a: Some(1.2), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_some()); + // consistent extra angle passes + let aa = (5.0f64 * (PI / 3.0).sin() / 39f64.sqrt()).asin(); // sin A = a sinC / c + let k = TriangleKnown { a: Some(5.0), b: Some(7.0), angle_c: Some(PI / 3.0), angle_a: Some(aa), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let f = |label: &str| r.cases[0].facts.iter().find(|x| x.label == label).unwrap().num.unwrap(); + assert!((f("\\angle B") - (PI - PI / 3.0 - aa)).abs() < 1e-9); + } + + #[test] + fn derived_midpoints_and_sections() { + let pts = vec![ + NamedPoint { name: "A".into(), p: vec![0.0, 0.0] }, + NamedPoint { name: "B".into(), p: vec![4.0, 0.0] }, + NamedPoint { name: "C".into(), p: vec![1.0, 3.0] }, + ]; + let defs = vec![ + PointDef { name: "M".into(), a: "A".into(), b: "B".into(), m: None, n: None }, + PointDef { name: "N".into(), a: "A".into(), b: "C".into(), m: None, n: None }, + ]; + let out = derived_points_analyze(&defs, &pts); + let get = |label: &str| out.iter().find(|f| f.label == label).unwrap(); + assert!((get("\\text{中位线}_{MN}").num.unwrap() - 18f64.sqrt() / 2.0).abs() < 1e-9); + assert!(out.iter().any(|f| f.label == "MN \\parallel BC")); + // section point P = 分点(A, B, 1:2): AP:PB = 1:2 → P = (4/3·1...) = (4/3? no: (2A + 1B)/3 = (4/3, 0)) + let defs = vec![PointDef { name: "P".into(), a: "A".into(), b: "B".into(), m: Some(1.0), n: Some(2.0) }]; + let out = derived_points_analyze(&defs, &pts); + let get = |label: &str| out.iter().find(|f| f.label == label).unwrap(); + assert!((get("|AP|").num.unwrap() - 4.0 / 3.0).abs() < 1e-9); + assert!((get("\\frac{|AP|}{|PB|}").num.unwrap() - 0.5).abs() < 1e-9); + } + + #[test] + fn angle_bisector_theorem_heron_excircles() { + // 3-4-5: Heron S = 6; bisector from C splits AB as b:a = 4:3; r_a = 2, r_b = 3, r_c = 6 + let k = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let get = |label: &str| fs.iter().find(|f| f.label == label).unwrap(); + assert!((get("S_{\\text{海伦}}").num.unwrap() - 6.0).abs() < 1e-9); + let ratio = get("\\frac{|AF_{c}|}{|F_{c}B|}").num.unwrap(); + assert!((ratio - 4.0 / 3.0).abs() < 1e-9, "ratio = {ratio}"); + assert!((get("r_a").num.unwrap() - 2.0).abs() < 1e-9); + assert!((get("r_b").num.unwrap() - 3.0).abs() < 1e-9); + assert!((get("r_c").num.unwrap() - 6.0).abs() < 1e-9); + // OI² = R(R − 2r) = 2.5 · (2.5 − 2) = 1.25 → OI = √1.25 + let oi = get("|OI|").num.unwrap(); + assert!((oi * oi - 1.25).abs() < 1e-9, "OI = {oi}"); + // right triangle: H at C, O midpoint of AB → |OH| = R = 2.5 + assert!((get("|OH|").num.unwrap() - 2.5).abs() < 1e-9); + } + + #[test] + fn vector_projection_vector() { + let vs = vec![NamedVec { name: "a".into(), v: vec![3.0, 4.0] }, NamedVec { name: "b".into(), v: vec![1.0, 0.0] }]; + let out = vectors_analyze(&vs); + let get = |label: &str| out.iter().find(|f| f.label == label).unwrap(); + let proj = get("\\text{proj}_{\\mathbf{b}}\\ \\mathbf{a}").value.clone(); + assert!(proj.contains("3") && proj.contains("0"), "proj = {proj}"); + let perp = get("\\mathbf{a} - \\text{proj}_{\\mathbf{b}}\\ \\mathbf{a}").value.clone(); + assert!(perp.contains("4"), "perp = {perp}"); + // scalar projection still there + assert!((get("\\text{a 在 b 上的投影}").num.unwrap() - 3.0).abs() < 1e-9); + } + + #[test] + fn parallel_transversal_angles() { + // y = 0, y = 3 parallel; x - y = 0 a 45° transversal + let ls = vec![ + NamedLine { name: "l".into(), a: 0.0, b: 1.0, c: 0.0 }, + NamedLine { name: "m".into(), a: 0.0, b: 1.0, c: 3.0 }, + NamedLine { name: "t".into(), a: 1.0, b: -1.0, c: 0.0 }, + ]; + let out = lines_analyze(&ls, &[]); + let get = |label: &str| out.iter().find(|f| f.label == label).unwrap(); + // corresponding angles ∠(t, l) = ∠(t, m) = 45° + let cor = get("\\text{同位角} \\; \\angle(t, l) = \\angle(t, m)").num.unwrap(); + assert!((cor - PI / 4.0).abs() < 1e-9); + // co-interior supplementary: 135° + let co = get("\\text{同旁内角}(t \\mid l \\parallel m)").num.unwrap(); + assert!((co - 3.0 * PI / 4.0).abs() < 1e-9); + // cut segment between the parallels along t: from (0,0) to (3,3) → 3√2 + let cut = out.iter().find(|f| f.label == "\\text{截线段}_{t}").unwrap(); + assert!((cut.num.unwrap() - 3.0 * 2.0f64.sqrt()).abs() < 1e-9); + } + + #[test] + fn intercept_theorem_ratios() { + // three parallels y = 0, 1, 3 cut by x = 0 and y = x → ratios 1:2 on both + let ls = vec![ + NamedLine { name: "l".into(), a: 0.0, b: 1.0, c: 0.0 }, + NamedLine { name: "m".into(), a: 0.0, b: 1.0, c: 1.0 }, + NamedLine { name: "n".into(), a: 0.0, b: 1.0, c: 3.0 }, + NamedLine { name: "s".into(), a: 1.0, b: 0.0, c: 0.0 }, + NamedLine { name: "t".into(), a: 1.0, b: -1.0, c: 0.0 }, + ]; + let out = lines_analyze(&ls, &[]); + let ratio = out.iter().find(|f| f.label == "\\text{成比例}").unwrap(); + assert!((ratio.num.unwrap() - 0.5).abs() < 1e-9); + let seg = out.iter().find(|f| f.label == "|s \\cap l,\\ s \\cap m|").unwrap(); + assert!((seg.num.unwrap() - 1.0).abs() < 1e-9); + } + + #[test] + fn similar_triangles_from_parallels() { + // parallels y = 2, y = 0... use y=0 and y=2 with transversals y=x and y=-x+1 meeting at (0.5,0.5) + let ls = vec![ + NamedLine { name: "p".into(), a: 0.0, b: 1.0, c: 0.0 }, + NamedLine { name: "q".into(), a: 0.0, b: 1.0, c: 2.0 }, + NamedLine { name: "s".into(), a: 1.0, b: -1.0, c: 0.0 }, + NamedLine { name: "t".into(), a: 1.0, b: 1.0, c: 1.0 }, + ]; + let out = lines_analyze(&ls, &[]); + let sim = out.iter().find(|f| f.label.starts_with("\\text{相似比}")).unwrap(); + assert!((sim.num.unwrap() - 1.0 / 3.0).abs() < 1e-9, "k = {}", sim.value); + } + + #[test] + fn ceva_and_menelaus_on_345() { + let k = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let get = |label: &str| fs.iter().find(|f| f.label == label).unwrap_or_else(|| panic!("missing {label}")); + assert!((get("\\Pi_{\\text{Ceva}}^{\\text{中线}}").num.unwrap() - 1.0).abs() < 1e-9); + assert!((get("\\Pi_{\\text{Ceva}}^{\\text{角平分线}}").num.unwrap() - 1.0).abs() < 1e-9); + // right triangle: an altitude foot sits on a vertex → skip 高 family + assert!(fs.iter().all(|f| f.label != "\\Pi_{\\text{Ceva}}^{\\text{高}}")); + assert!((get("\\Pi_{\\text{Ceva}}^{\\text{切点}}").num.unwrap() - 1.0).abs() < 1e-9); + let men = get("\\Pi_{\\text{Menelaus}}").num.unwrap(); + assert!((men + 1.0).abs() < 1e-9, "Menelaus = {men}"); + } + + #[test] + fn ceva_altitudes_acute() { + // 6-5-5 acute isosceles: all altitude feet interior → Ceva product = 1 + let k = TriangleKnown { a: Some(6.0), b: Some(5.0), c: Some(5.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let alt = fs.iter().find(|f| f.label == "\\Pi_{\\text{Ceva}}^{\\text{高}}").unwrap(); + assert!((alt.num.unwrap() - 1.0).abs() < 1e-9, "alt = {}", alt.value); + } + + #[test] + fn skew_lines_distance() { + // AB = (0,0,0)->(1,0,0), CD = (0,1,0)->(0,1,1): skew, distance = 1 + let pts = vec![ + NamedPoint { name: "A".into(), p: vec![0.0, 0.0, 0.0] }, + NamedPoint { name: "B".into(), p: vec![1.0, 0.0, 0.0] }, + NamedPoint { name: "C".into(), p: vec![0.0, 1.0, 0.0] }, + NamedPoint { name: "D".into(), p: vec![0.0, 1.0, 1.0] }, + ]; + let out = points_analyze(&pts); + let d = out.iter().find(|f| f.label == "d(AB, CD)").unwrap(); + assert!((d.num.unwrap() - 1.0).abs() < 1e-9, "d = {}", d.value); + assert!(out.iter().any(|f| f.value.contains("异面"))); + } + + #[test] + fn stewart_median_and_bisector() { + let k = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + // median to c=5: m=n=2.5, d=2.5; LHS = 3²·2.5 + 4²·2.5 = 62.5 + let med = fs.iter().find(|f| f.label == "\\text{Stewart}_{m_c}").unwrap(); + assert!((med.num.unwrap() - 62.5).abs() < 1e-6, "med = {}", med.value); + let bis = fs.iter().find(|f| f.label == "\\text{Stewart}_{t_c}").unwrap(); + assert!(bis.num.unwrap().is_finite()); + // altitude to the hypotenuse: m = 16/5, n = 9/5, h = 12/5 → LHS = 57.6 + let alt = fs.iter().find(|f| f.label == "\\text{Stewart}_{h_c}").unwrap(); + assert!((alt.num.unwrap() - 57.6).abs() < 1e-6, "alt = {}", alt.value); + // medians, bisectors, and altitudes + assert_eq!(fs.iter().filter(|f| f.label.starts_with("\\text{Stewart}_")).count(), 9); + } + + #[test] + fn line_plane_angle_perpendicular() { + // AB = vertical (0,0,0)->(0,0,1); plane z = 0 → θ = 90° + let ps = vec![NamedPlane { name: "α".into(), n: [0.0, 0.0, 1.0], d: 0.0 }]; + let pts = vec![ + NamedPoint { name: "A".into(), p: vec![0.0, 0.0, 0.0] }, + NamedPoint { name: "B".into(), p: vec![0.0, 0.0, 1.0] }, + NamedPoint { name: "C".into(), p: vec![1.0, 0.0, 0.0] }, + ]; + let out = planes_analyze(&ps, &pts); + let th = out.iter().find(|f| f.label == "\\theta(AB, α)").unwrap(); + assert!((th.num.unwrap() - PI / 2.0).abs() < 1e-9, "θ = {}", th.value); + assert!(out.iter().any(|f| f.label == "AB \\perp α")); + // AC lies in / parallel to the plane → θ = 0 + let th2 = out.iter().find(|f| f.label == "\\theta(AC, α)").unwrap(); + assert!(th2.num.unwrap().abs() < 1e-9, "θ(AC) = {}", th2.value); + assert!(out.iter().any(|f| f.label == "AC \\parallel α")); + let foot = out.iter().find(|f| f.label == "\\text{foot}(B, α)").unwrap(); + assert!(foot.value.contains('0'), "foot = {}", foot.value); + if let Shape::Point3 { x, y, z, .. } = foot.shape.as_ref().unwrap() { + assert!(x.abs() < 1e-9 && y.abs() < 1e-9 && z.abs() < 1e-9); + } else { + panic!("foot should be a point"); + } + } + + #[test] + fn skew_line_angle() { + let pts = vec![ + NamedPoint { name: "A".into(), p: vec![0.0, 0.0, 0.0] }, + NamedPoint { name: "B".into(), p: vec![1.0, 0.0, 0.0] }, + NamedPoint { name: "C".into(), p: vec![0.0, 1.0, 0.0] }, + NamedPoint { name: "D".into(), p: vec![0.0, 1.0, 1.0] }, + ]; + let out = points_analyze(&pts); + let th = out.iter().find(|f| f.label == "\\theta(AB, CD)").unwrap(); + assert!((th.num.unwrap() - PI / 2.0).abs() < 1e-9, "θ = {}", th.value); + } + + #[test] + fn van_obel_and_apollonius_on_345() { + let k = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let vo_a = fs.iter().find(|f| f.label.contains("ID_{a}")).expect("Van Obel A"); + assert!((vo_a.num.unwrap() - 3.0).abs() < 1e-6, "Van Obel A = {}", vo_a.value); + let vo_b = fs.iter().find(|f| f.label.contains("ID_{b}")).expect("Van Obel B"); + assert!((vo_b.num.unwrap() - 2.0).abs() < 1e-6, "Van Obel B = {}", vo_b.value); + assert_eq!(fs.iter().filter(|f| f.label.contains("ID_")).count(), 3); + let ap = fs.iter().filter(|f| f.group == "阿波罗尼奥斯圆").count(); + assert_eq!(ap, 3, "Apollonius count"); + } + + #[test] + fn tetrahedron_volume_unit() { + let pts = vec![ + NamedPoint { name: "A".into(), p: vec![0.0, 0.0, 0.0] }, + NamedPoint { name: "B".into(), p: vec![1.0, 0.0, 0.0] }, + NamedPoint { name: "C".into(), p: vec![0.0, 1.0, 0.0] }, + NamedPoint { name: "D".into(), p: vec![0.0, 0.0, 1.0] }, + ]; + let out = points_analyze(&pts); + let v = out.iter().find(|f| f.label.contains("四面体")).expect("tetra volume"); + assert!((v.num.unwrap() - 1.0 / 6.0).abs() < 1e-9, "V = {}", v.value); + let cm = out.iter().find(|f| f.label.contains("Cayley")).expect("Cayley-Menger"); + assert!((cm.num.unwrap() - 1.0 / 6.0).abs() < 1e-9, "V_CM = {}", cm.value); + } + + #[test] + fn dihedral_angle_xy_planes() { + let ps = vec![ + NamedPlane { name: "α".into(), n: [1.0, 0.0, 0.0], d: 0.0 }, + NamedPlane { name: "β".into(), n: [0.0, 1.0, 0.0], d: 0.0 }, + ]; + let out = planes_analyze(&ps, &[]); + let th = out.iter().find(|f| f.label == "\\theta(α, β)").expect("theta"); + assert!((th.num.unwrap() - PI / 2.0).abs() < 1e-9); + assert!(out.iter().any(|f| f.label.contains("二面角"))); + assert!(out.iter().any(|f| f.label == "α \\perp β")); + } + + #[test] + fn euler_line_and_nine_point() { + let k = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r = triangle_solve(&k); + let fs = &r.cases[0].facts; + assert!(fs.iter().any(|f| f.label.contains("O,") && f.value.contains("共线"))); + let og_gh = fs.iter().find(|f| f.label == "OG:GH").unwrap(); + assert!((og_gh.num.unwrap() - 2.0).abs() < 1e-6, "OG:GH = {}", og_gh.value); + let r9 = fs.iter().find(|f| f.label.contains("九点")).unwrap(); + assert!((r9.num.unwrap() - 1.25).abs() < 1e-9, "R9 = {}", r9.value); // R=2.5 + // OI² = R(R-2r) already covered; ensure |OI| present + assert!(fs.iter().any(|f| f.label == "|OI|")); + // 3-4-5 is right: OH is nine-point diameter + assert!( + fs.iter().any(|f| f.label.contains("九点圆直径")), + "OH nine-point diameter fact missing for right triangle" + ); + let oh2on = fs.iter().find(|f| f.label == "|OH| = 2|ON|").expect("OH=2ON"); + assert!((oh2on.num.unwrap() - 2.5).abs() < 1e-9); + } + + #[test] + fn feuerbach_spieker_nagel() { + // 3-4-5: R=2.5, r=1 → |NI|=R/2−r=0.25; r_a=2,r_b=3,r_c=6 → |NI_*|=R/2+r_*. + let k = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let ni = fs.iter().find(|f| f.group == "费尔巴哈定理" && f.label == "|NI|").expect("|NI|"); + assert!((ni.num.unwrap() - 0.25).abs() < 1e-9, "|NI|={}", ni.value); + assert!(fs.iter().any(|f| f.group == "费尔巴哈定理" && f.label.contains("内切"))); + let nia = fs.iter().find(|f| f.label == "|NI_a|").expect("|NI_a|"); + let nib = fs.iter().find(|f| f.label == "|NI_b|").expect("|NI_b|"); + let nic = fs.iter().find(|f| f.label == "|NI_c|").expect("|NI_c|"); + assert!((nia.num.unwrap() - 3.25).abs() < 1e-9, "|NI_a|={}", nia.value); + assert!((nib.num.unwrap() - 4.25).abs() < 1e-9, "|NI_b|={}", nib.value); + assert!((nic.num.unwrap() - 7.25).abs() < 1e-9, "|NI_c|={}", nic.value); + assert!(fs.iter().any(|f| f.group == "斯皮克中心" && f.label == "Sp")); + assert!(fs.iter().any(|f| f.group == "奈格尔点" && f.label == "Na")); + let ig = fs.iter().find(|f| f.group == "奈格尔点" && f.label == "IG:GNa").expect("IG:GNa"); + assert!((ig.num.unwrap() - 2.0).abs() < 1e-6, "IG:GNa={}", ig.value); + assert!(fs.iter().any(|f| f.group == "奈格尔点" && f.value.contains("共线"))); + } + + #[test] + fn gergonne_lemoine_brocard() { + // 3-4-5: Ge → (3.25, 1.5); K → (3.2, 1.2); tan ω = 12/25 = 0.48; cot ω = 25/12. + let k = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let ge = fs.iter().find(|f| f.group == "格尔巩点" && f.label == "Ge").expect("Ge"); + // bary 1/(s−a):… on 3-4-5 → (3.109…, 1.309…) = (171/55, 72/55) + assert!( + ge.value.contains("3.10") + || ge.value.contains("1.30") + || ge.value.contains("171/55") + || ge.value.contains("\\frac{171}{55}") + || ge.value.contains("72/55") + || ge.value.contains("\\frac{72}{55}"), + "Ge={}", + ge.value + ); + assert!(fs.iter().any(|f| f.group == "格尔巩点" && f.value.contains("等截"))); + let kp = fs.iter().find(|f| f.group == "勒莫恩点" && f.label == "K").expect("K"); + // (3.2, 1.2) == (16/5, 6/5) + assert!( + (kp.value.contains("3.2") || kp.value.contains("16/5") || kp.value.contains("\\frac{16}{5}")) + && (kp.value.contains("1.2") || kp.value.contains("6/5") || kp.value.contains("\\frac{6}{5}")), + "K={}", + kp.value + ); + assert!(fs.iter().any(|f| f.group == "等角共轭" && f.label.contains("G") && f.label.contains("K"))); + let tan_w = fs.iter().find(|f| f.group == "布罗卡" && f.label == "\\tan\\omega").expect("tan ω"); + assert!((tan_w.num.unwrap() - 0.48).abs() < 1e-9, "tan ω={}", tan_w.value); + let cot_w = fs.iter().find(|f| f.group == "布罗卡" && f.label == "\\cot\\omega").expect("cot ω"); + assert!((cot_w.num.unwrap() - 25.0 / 12.0).abs() < 1e-9, "cot ω={}", cot_w.value); + assert!(fs.iter().any(|f| f.group == "布罗卡" && f.label == "\\omega")); + assert!(fs.iter().any(|f| f.group == "布罗卡" && f.label == "\\Omega")); + assert!(fs.iter().any(|f| f.group == "布罗卡" && f.label == "\\Omega'")); + } + + + #[test] + fn longchamps_mittenpunkt_schiffler() { + // 3-4-5: H=C=(3.2,2.4), O=(2.5,0) → L=2O−H=(1.8,−2.4); + // Mi bary a(s−a):… → (28/11, 6/11); Sc on Euler line, matches Euler(ICA). + let k = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let l = fs.iter().find(|f| f.group == "德龙尚点" && f.label == "L").expect("L"); + assert!( + (l.value.contains("1.8") || l.value.contains("9/5") || l.value.contains("\\frac{9}{5}")) + && (l.value.contains("-2.4") || l.value.contains("-\\frac{12}{5}") || l.value.contains("-12/5")), + "L={}", + l.value + ); + assert!(fs.iter().any(|f| f.group == "德龙尚点" && f.label.contains("中点"))); + let mi = fs.iter().find(|f| f.group == "米滕点" && f.label == "Mi").expect("Mi"); + assert!( + mi.value.contains("28/11") + || mi.value.contains("\\frac{28}{11}") + || mi.value.contains("2.54"), + "Mi={}", + mi.value + ); + assert!(fs.iter().any(|f| f.group == "席弗勒点" && f.label == "Sc"), "Sc missing"); + assert!( + fs.iter().any(|f| f.group == "席弗勒点" && f.label.contains("ICA")), + "Schiffler ICA concurrence missing" + ); + let sc = fs.iter().find(|f| f.group == "席弗勒点" && f.label == "Sc").unwrap(); + // Sc ≈ (2.68421, 0.63158) = (51/19, 12/19) + assert!( + sc.value.contains("2.68") + || sc.value.contains("51/19") + || sc.value.contains("\\frac{51}{19}") + || sc.value.contains("0.63"), + "Sc={}", + sc.value + ); + } + + + #[test] + fn bevan_orthic_euler_reflection() { + // 3-4-5: Be = 2O−I = (2,−1); orthic feet; Euler reflection skipped (right). + let k = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let be = fs.iter().find(|f| f.group == "贝万点" && f.label == "Be").expect("Be"); + assert!( + (be.value.contains("2") || be.value.contains("2.0")) + && (be.value.contains("-1")), + "Be={}", + be.value + ); + assert!(fs.iter().any(|f| f.group == "贝万点" && f.label.contains("I_a"))); + assert!(["H_a", "H_b", "H_c"].iter().all(|lab| fs.iter().any(|f| f.group == "垂足三角形" && f.label == *lab))); + assert!(fs.iter().any(|f| f.group == "垂足三角形" && f.label.contains("九点圆"))); + let hc = fs.iter().find(|f| f.group == "垂足三角形" && f.label == "H_c").expect("Hc"); + assert!( + hc.value.contains("3.2") || hc.value.contains("16/5") || hc.value.contains("\\frac{16}{5}"), + "Hc={}", + hc.value + ); + + // Acute 6-7-8: Euler reflection point on circumcircle. + let k2 = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + assert!(fs2.iter().any(|f| f.group == "欧拉反射点" && f.label == "E_r"), "E_r missing on 6-7-8"); + assert!(fs2.iter().any(|f| f.group == "欧拉反射点" && f.label.contains("(O)"))); + let er = fs2.iter().find(|f| f.group == "欧拉反射点" && f.label == "E_r").unwrap(); + assert!( + er.value.contains("1.7") || er.value.contains("4.4") || er.value.contains("\\frac"), + "Er={}", + er.value + ); + } + + + #[test] + fn contact_spieker_radius_kosnita() { + // 3-4-5: contact pts; |IX_*|=r=1; r_Sp=1/2; Kosnita skipped (O on AB). + let k = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + assert!(["X_a", "X_b", "X_c"].iter().all(|lab| fs.iter().any(|f| f.group == "接触三角形" && f.label == *lab))); + let ix = fs.iter().find(|f| f.group == "接触三角形" && f.label == "|IX_*|").expect("|IX_*|"); + assert!((ix.num.unwrap() - 1.0).abs() < 1e-9, "|IX_*|={}", ix.value); + assert!(fs.iter().any(|f| f.group == "接触三角形" && f.value.contains("共线"))); + let rsp = fs.iter().find(|f| f.group == "斯皮克圆" && f.label.contains("Sp")).expect("r_Sp"); + assert!((rsp.num.unwrap() - 0.5).abs() < 1e-9, "r_Sp={}", rsp.value); + assert!(fs.iter().any(|f| f.group == "斯皮克圆" && f.label.contains("中点"))); + assert!( + fs.iter().all(|f| f.group != "科斯尼塔点"), + "Kosnita should be skipped on right 3-4-5" + ); + + // Acute 6-7-8: Kosnita present. + let k2 = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + let ko = fs2.iter().find(|f| f.group == "科斯尼塔点" && f.label == "Ko").expect("Ko on 6-7-8"); + assert!(ko.value.contains("4.5") || ko.value.contains("1.8") || ko.value.contains("\\frac"), "Ko={}", ko.value); + let rsp2 = fs2.iter().find(|f| f.group == "斯皮克圆" && f.label.contains("Sp")).unwrap(); + // r = Δ/s; for 6-7-8, s=10.5, Δ=sqrt(10.5·4.5·3.5·2.5)=21*sqrt(3)/4? + // r_Sp = r/2 > 0 + assert!(rsp2.num.unwrap() > 0.0); + } + + #[test] + fn extouch_steiner_mandart() { + // 3-4-5: extouch T_*; X_a/T_a midpoint = mid BC; A–T_a–Na; Steiner area; Mandart@Mi + let k = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + assert!(["T_a", "T_b", "T_c"].iter().all(|lab| fs.iter().any(|f| f.group == "旁切接触三角形" && f.label == *lab))); + assert!(fs.iter().any(|f| f.group == "旁切接触三角形" && f.label.contains("X_a"))); + assert!(fs.iter().any(|f| f.group == "旁切接触三角形" && f.value.contains("共线"))); + let st = fs.iter().find(|f| f.group == "施泰纳内接椭圆" && f.label.contains("Steiner")).expect("Steiner area"); + let expect = std::f64::consts::PI * 6.0 / (3.0 * 3.0f64.sqrt()); + assert!((st.num.unwrap() - expect).abs() < 1e-9, "Steiner={} vs {}", st.value, expect); + assert!(["M_a", "M_b", "M_c"].iter().all(|lab| fs.iter().any(|f| f.group == "施泰纳内接椭圆" && f.label == *lab))); + let mi = fs.iter().find(|f| f.group == "米滕点" && f.label == "Mi").expect("Mi"); + let man = fs.iter().find(|f| f.group == "曼达特内接椭圆" && f.label.contains("center")).expect("Mandart"); + assert_eq!(man.value, mi.value, "Mandart center should equal Mittenpunkt"); + assert!(fs.iter().any(|f| f.group == "曼达特内接椭圆" && f.label.contains("T_"))); + let nag_ceva = fs.iter().find(|f| f.group == "塞瓦与梅涅劳斯" && f.label.contains("旁切")).expect("Nagel Ceva"); + assert!((nag_ceva.num.unwrap() - 1.0).abs() < 1e-9); + + // Equilateral: Steiner = incircle area π r² + let k2 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + assert!(fs2.iter().any(|f| f.group == "施泰纳内接椭圆" && f.label.contains("pi r"))); + } + + #[test] + fn clawson_yff_exeter() { + // Right 3-4-5: Clawson & Yff skipped (tan/sec undefined); Exeter present. + let k = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + assert!(fs.iter().all(|f| f.group != "克劳森点"), "Clawson must skip right △"); + assert!(fs.iter().all(|f| f.group != "伊夫中心"), "Yff must skip right △"); + let ex = fs.iter().find(|f| f.group == "埃克塞特点" && f.label == "Ex").expect("Exeter on 3-4-5"); + assert!(ex.value.contains("\\left") || ex.value.contains("(") || ex.value.contains("\\frac"), "Ex={}", ex.value); + assert!(fs.iter().any(|f| f.group == "埃克塞特点" && f.label.contains("perspector"))); + + // Acute 6-7-8: all three present. + let k2 = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + let cl = fs2.iter().find(|f| f.group == "克劳森点" && f.label == "Cl").expect("Clawson on 6-7-8"); + let yff = fs2.iter().find(|f| f.group == "伊夫中心" && f.label == "Yff").expect("Yff on 6-7-8"); + let ex2 = fs2.iter().find(|f| f.group == "埃克塞特点" && f.label == "Ex").expect("Exeter on 6-7-8"); + assert!(cl.value.contains("\\left") || cl.value.contains("("), "Cl={}", cl.value); + assert!(yff.value.contains("\\left") || yff.value.contains("("), "Yff={}", yff.value); + assert!(ex2.value.contains("\\left") || ex2.value.contains("("), "Ex={}", ex2.value); + // Distinct from each other and from G on a scalene acute △. + let g = fs2.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + assert_ne!(cl.value, yff.value); + assert_ne!(cl.value, ex2.value); + assert_ne!(yff.value, g.value); + } + + #[test] + fn isodynamic_steiner_tarry() { + // Scalene right 3-4-5: S1,S2,St,Ty all present (not isosceles). + let k = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let s1 = fs.iter().find(|f| f.group == "等动力点" && f.label.contains("S_{1}")).expect("S1"); + let s2 = fs.iter().find(|f| f.group == "等动力点" && f.label.contains("S_{2}")).expect("S2"); + assert_ne!(s1.value, s2.value); + assert!(fs.iter().any(|f| f.group == "等动力点" && f.label.contains("Apollonius"))); + let st = fs.iter().find(|f| f.group == "施泰纳点" && f.label == "St").expect("Steiner"); + let ty = fs.iter().find(|f| f.group == "塔里点" && f.label == "Ty").expect("Tarry"); + assert_ne!(st.value, ty.value); + let ot = fs.iter().find(|f| f.group == "塔里点" && f.label.contains("O")).expect("|O Ty|"); + let rr = fs.iter().find(|f| f.group == "外接圆与内切圆" && f.label == "R").expect("R"); + assert!((ot.num.unwrap() - rr.num.unwrap()).abs() < 1e-6, "Tarry on circumcircle"); + + // Isosceles 5-5-6: Steiner/Tarry skipped; isodynamic present. + let k2 = TriangleKnown { a: Some(6.0), b: Some(5.0), c: Some(5.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + assert!(fs2.iter().all(|f| f.group != "施泰纳点"), "Steiner skip isosceles"); + assert!(fs2.iter().all(|f| f.group != "塔里点"), "Tarry skip with Steiner"); + assert!(fs2.iter().any(|f| f.group == "等动力点" && f.label.contains("S_{1}"))); + + // Equilateral: S2 skipped; S1 coincides with G; Steiner skipped. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().all(|f| f.group != "施泰纳点")); + assert!(fs3.iter().all(|f| !(f.group == "等动力点" && f.label.contains("S_{2}"))), "S2 skip equilateral"); + let s1e = fs3.iter().find(|f| f.group == "等动力点" && f.label.contains("S_{1}")).expect("S1 eq"); + let g = fs3.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + assert_eq!(s1e.value, g.value, "equilateral S1 = G"); + } + + #[test] + fn napoleon_parry_vecten() { + // Scalene 3-4-5: all present (no 30°/45°/135°/150°; not equilateral). + let k = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let n1 = fs.iter().find(|f| f.group == "拿破仑中心" && f.label == "N_1").expect("N1"); + let n2 = fs.iter().find(|f| f.group == "拿破仑中心" && f.label == "N_2").expect("N2"); + assert_ne!(n1.value, n2.value); + let py = fs.iter().find(|f| f.group == "帕里点" && f.label == "Py").expect("Parry"); + assert!(fs.iter().any(|f| f.group == "帕里点" && f.label == "X_{111}"), "X111 dual label"); + let op = fs.iter().find(|f| f.group == "帕里点" && f.label.contains("O")).expect("|O Py|"); + let rr = fs.iter().find(|f| f.group == "外接圆与内切圆" && f.label == "R").expect("R"); + assert!((op.num.unwrap() - rr.num.unwrap()).abs() < 1e-6, "Parry on circumcircle"); + let vout = fs.iter().find(|f| f.group == "韦克滕点" && f.label.contains("out")).expect("Vout"); + let vin = fs.iter().find(|f| f.group == "韦克滕点" && f.label.contains("in")).expect("Vin"); + assert_ne!(vout.value, vin.value); + assert_ne!(py.value, n1.value); + + // Equilateral: Parry skipped; Napoleon + Vecten present; N1=N2=G. + let k2 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + assert!(fs2.iter().all(|f| f.group != "帕里点"), "Parry skip equilateral"); + let n1e = fs2.iter().find(|f| f.group == "拿破仑中心" && f.label == "N_1").expect("N1 eq"); + let n2e = fs2.iter().find(|f| f.group == "拿破仑中心" && f.label == "N_2").expect("N2 eq"); + let g = fs2.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + assert_eq!(n1e.value, g.value); + assert_eq!(n2e.value, g.value); + assert!(fs2.iter().any(|f| f.group == "韦克滕点" && f.label.contains("out"))); + + // Isosceles right 1-1-√2 ≈ has 45°: inner Vecten skipped; outer ok; Parry ok. + let k3 = TriangleKnown { a: Some(1.0), b: Some(1.0), c: Some(2f64.sqrt()), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().all(|f| !(f.group == "韦克滕点" && f.label.contains("in"))), "Vin skip 45°"); + assert!(fs3.iter().any(|f| f.group == "韦克滕点" && f.label.contains("out")), "Vout on isos right"); + assert!(fs3.iter().any(|f| f.group == "帕里点" && f.label == "Py")); + } + + #[test] + fn hofstadter_kenmotu_apollonius() { + // 3-4-5: Hofstadter, Kenmotu (+square edge), Apollonius, Brocard midpoint. + let k = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let hf0 = fs.iter().find(|f| f.group == "霍夫斯塔特点" && f.label == "Hf_0").expect("Hf0"); + let hf1 = fs.iter().find(|f| f.group == "霍夫斯塔特点" && f.label == "Hf_1").expect("Hf1"); + assert_ne!(hf0.value, hf1.value); + let g = fs.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + assert_ne!(hf0.value, g.value, "Hofstadter zero ≠ centroid on scalene"); + let km = fs.iter().find(|f| f.group == "肯莫图点" && f.label == "Km").expect("Kenmotu"); + let sq = fs.iter().find(|f| f.group == "肯莫图点" && f.label.contains("square")).expect("square edge"); + assert!(sq.num.unwrap() > 0.0); + let vout = fs.iter().find(|f| f.group == "韦克滕点" && f.label.contains("out")).expect("Vout"); + assert_ne!(km.value, vout.value, "Kenmotu ≠ outer Vecten (isogonal mates)"); + let ap = fs.iter().find(|f| f.group == "阿波罗尼奥斯点" && f.label == "Ap").expect("Apollonius"); + assert!(ap.value.contains("(") || ap.value.contains("\\left")); + assert!(fs.iter().any(|f| f.group == "布罗卡" && f.label.contains("39"))); + + // Equilateral: Hf0=Hf1=G; Kenmotu=G; Apollonius=G. + let k2 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + let g2 = fs2.iter().find(|f| f.group == "特殊点" && f.label == "G").unwrap(); + let hf0e = fs2.iter().find(|f| f.group == "霍夫斯塔特点" && f.label == "Hf_0").unwrap(); + let hf1e = fs2.iter().find(|f| f.group == "霍夫斯塔特点" && f.label == "Hf_1").unwrap(); + assert_eq!(hf0e.value, g2.value); + assert_eq!(hf1e.value, g2.value); + let kme = fs2.iter().find(|f| f.group == "肯莫图点" && f.label == "Km").unwrap(); + assert_eq!(kme.value, g2.value); + let ape = fs2.iter().find(|f| f.group == "阿波罗尼奥斯点" && f.label == "Ap").unwrap(); + assert_eq!(ape.value, g2.value); + } + + #[test] + fn fermat_feuerbach_point_detour() { + // 3-4-5: F1,F2 distinct from S1,S2; Fe on nine-point & incircle; Ed + Ip (12>4*2.5+1). + let k = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let f1 = fs.iter().find(|f| f.group == "费马点" && f.label == "F_1").expect("F1"); + let f2 = fs.iter().find(|f| f.group == "费马点" && f.label == "F_2").expect("F2"); + assert_ne!(f1.value, f2.value); + let s1 = fs.iter().find(|f| f.group == "等动力点" && f.label.contains("S_{1}")).expect("S1"); + assert_ne!(f1.value, s1.value, "isogonic ≠ isodynamic"); + assert!(fs.iter().any(|f| f.group == "费马点" && f.label.contains("S_1"))); + let fe = fs.iter().find(|f| f.group == "费尔巴哈点" && f.label == "Fe").expect("Fe"); + let nfe = fs.iter().find(|f| f.group == "费尔巴哈点" && f.label.contains("N")).expect("|N Fe|"); + let r9 = fs.iter().find(|f| f.group == "欧拉线" && f.label.contains("九点")).expect("R9"); + assert!((nfe.num.unwrap() - r9.num.unwrap()).abs() < 1e-6); + let ed = fs.iter().find(|f| f.group == "等迂回点" && f.label == "Ed").expect("Ed"); + let ip = fs.iter().find(|f| f.group == "等周点" && f.label == "Ip").expect("Ip"); + assert_ne!(ed.value, ip.value); + assert_ne!(fe.value, ed.value); + + // Equilateral: F1=G; F2 skipped; Fe, Ed, Ip (=G for eq?). + let k2 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + let g = fs2.iter().find(|f| f.group == "特殊点" && f.label == "G").unwrap(); + let f1e = fs2.iter().find(|f| f.group == "费马点" && f.label == "F_1").unwrap(); + assert_eq!(f1e.value, g.value); + assert!(fs2.iter().all(|f| !(f.group == "费马点" && f.label == "F_2")), "F2 skip equilateral"); + assert!(fs2.iter().all(|f| f.group != "费尔巴哈点"), "Fe skip equilateral coincident circles"); + assert!(fs2.iter().any(|f| f.group == "等迂回点" && f.label == "Ed")); + + // Obtuse with angle > 120°: F1 at obtuse vertex. Sides 2,3,4: cos C=(4+9-16)/12 < 0, C>90; check C>120? + // 2²+3²=13 < 16, obtuse at C. cos C = (4+9-16)/12 = -3/12 = -0.25, C ≈ 104.5° < 120. + // Need stronger obtuse: sides 1,1,1.9 — almost flat apex. + let k3 = TriangleKnown { a: Some(1.0), b: Some(1.0), c: Some(1.9), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + // angle opposite c=1.9: cos C = (1+1-3.61)/2 = -1.61/2 = -0.805, C≈143.6° > 120 + let f1o = fs3.iter().find(|f| f.group == "费马点" && f.label == "F_1").expect("F1 obtuse"); + // F1 should be at C = verts[2] + assert!(f1o.why.contains("120") || f1o.how.contains("obtuse") || f1o.how.contains("120"), "why={}", f1o.why); + } + + #[test] + fn moses_x25_orthic_circumcenter() { + // Scalene acute 6-7-8: Moses, X(25), orthic circumcenter = N. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let mo = fs.iter().find(|f| f.group == "摩西点" && f.label == "Mo").expect("Mo"); + let i = fs.iter().find(|f| f.group == "特殊点" && f.label == "I").expect("I"); + assert_ne!(mo.value, i.value, "Moses ≠ I on scalene"); + assert!(fs.iter().any(|f| f.group == "摩西点" && f.value.contains("等截"))); + let x25 = fs.iter().find(|f| f.group == "垂足切线同调中心" && f.label.contains("25")).expect("X25"); + assert_ne!(x25.value, mo.value); + assert!(fs.iter().any(|f| f.group == "垂足三角形" && f.label.contains("O(H_a"))); + assert!(fs.iter().any(|f| f.group == "垂足三角形" && f.label.contains("= N"))); + let oh = fs.iter().find(|f| f.group == "垂足三角形" && f.label == "O(H_a H_b H_c)").expect("Oh"); + let n = fs.iter().find(|f| f.group == "欧拉线" && f.label == "N").expect("N"); + assert_eq!(oh.value, n.value, "orthic circumcenter = N"); + + // Right 3-4-5: Moses still; X(25) skipped (right angle); orthic degenerates (two feet = right vertex). + let k2 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + assert!(fs2.iter().any(|f| f.group == "摩西点" && f.label == "Mo")); + assert!(fs2.iter().all(|f| f.group != "垂足切线同调中心"), "X25 skip right"); + assert!(fs2.iter().all(|f| !(f.group == "垂足三角形" && f.label.contains("O(H_a"))), "orthic O skip right"); + + // Equilateral: Moses = I = G; X(25) = G. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + let g = fs3.iter().find(|f| f.group == "特殊点" && f.label == "G").unwrap(); + let moe = fs3.iter().find(|f| f.group == "摩西点" && f.label == "Mo").unwrap(); + assert_eq!(moe.value, g.value); + let x25e = fs3.iter().find(|f| f.group == "垂足切线同调中心" && f.label.contains("25")).unwrap(); + assert_eq!(x25e.value, g.value); + } + + #[test] + fn similitude_centers_euler_infinity() { + // 3-4-5: R=2.5, r=1; S+ divides OI in 2.5:1; S− on OI; X30 direction || OH. + let k = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let sp = fs.iter().find(|f| f.group == "相似中心" && f.label == "S_{+}").expect("S+"); + let sm = fs.iter().find(|f| f.group == "相似中心" && f.label == "S_{-}").expect("S-"); + assert_ne!(sp.value, sm.value); + let ratio = fs.iter().find(|f| f.group == "相似中心" && f.label.contains("OS")).expect("ratio"); + assert!((ratio.num.unwrap() - 2.5).abs() < 1e-6, "R/r={}", ratio.num.unwrap()); + assert!(fs.iter().any(|f| f.group == "相似中心" && f.value.contains("共线"))); + let x30 = fs.iter().find(|f| f.group == "欧拉无穷远点" && f.label.contains("30")).expect("X30"); + assert!(fs.iter().any(|f| f.group == "欧拉无穷远点" && f.label.contains("OH"))); + assert!(fs.iter().any(|f| f.group == "欧拉无穷远点" && f.label.contains("g") && f.value == "0")); + // Direction nonzero + assert!(x30.value.contains("(")); + + // Equilateral: S+=S−=O=I=G; X30 skipped (|OH|=0). + let k2 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + let g = fs2.iter().find(|f| f.group == "特殊点" && f.label == "G").unwrap(); + let spe = fs2.iter().find(|f| f.group == "相似中心" && f.label == "S_{+}").expect("S+ eq"); + let sme = fs2.iter().find(|f| f.group == "相似中心" && f.label == "S_{-}").expect("S- eq"); + assert_eq!(spe.value, g.value); + assert_eq!(sme.value, g.value); + assert!(fs2.iter().all(|f| f.group != "欧拉无穷远点"), "X30 skip equilateral"); + } + + #[test] + fn x57_x58_x84_isogonals() { + // Scalene 6-7-8: X57≠Mi, X58≠Sp, X84≠Be; isogonal notes present. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let x57 = fs.iter().find(|f| f.group == "米滕等角共轭点" && f.label == "X_{57}").expect("X57"); + let mi = fs.iter().find(|f| f.group == "米滕点" && f.label == "Mi").expect("Mi"); + assert_ne!(x57.value, mi.value); + assert!(fs.iter().any(|f| f.group == "米滕等角共轭点" && f.value.contains("等角"))); + let x58 = fs.iter().find(|f| f.group == "斯皮克等角共轭点" && f.label == "X_{58}").expect("X58"); + let sp = fs.iter().find(|f| f.group == "斯皮克中心" && f.label == "Sp").expect("Sp"); + assert_ne!(x58.value, sp.value); + assert!(fs.iter().any(|f| f.group == "斯皮克等角共轭点" && f.value.contains("等角"))); + let x84 = fs.iter().find(|f| f.group == "己基透视中心" && f.label == "X_{84}").expect("X84"); + let be = fs.iter().find(|f| f.group == "贝万点" && f.label == "Be").expect("Be"); + assert_ne!(x84.value, be.value); + assert!(fs.iter().any(|f| f.group == "己基透视中心" && f.value.contains("等角"))); + assert_ne!(x57.value, x58.value); + assert_ne!(x58.value, x84.value); + + // Equilateral: all collapse to G. + let k2 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + let g = fs2.iter().find(|f| f.group == "特殊点" && f.label == "G").unwrap(); + for (grp, lab) in [ + ("米滕等角共轭点", "X_{57}"), + ("斯皮克等角共轭点", "X_{58}"), + ("己基透视中心", "X_{84}"), + ] { + let p = fs2.iter().find(|f| f.group == grp && f.label == lab).unwrap_or_else(|| panic!("missing {lab}")); + assert_eq!(p.value, g.value, "{lab} should = G"); + } + } + + #[test] + fn x12_x59_x60_feuerbach_isogonals() { + // Scalene 6-7-8: X12, X59, X60 distinct; isogonal notes; Fe present. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let x12 = fs.iter().find(|f| f.group == "费尔巴哈调和共轭点" && f.label == "X_{12}").expect("X12"); + let x59 = fs.iter().find(|f| f.group == "费尔巴哈等角共轭点" && f.label == "X_{59}").expect("X59"); + let x60 = fs.iter().find(|f| f.group == "X12等角共轭点" && f.label == "X_{60}").expect("X60"); + let fe = fs.iter().find(|f| f.group == "费尔巴哈点" && f.label == "Fe").expect("Fe"); + assert_ne!(x12.value, x59.value); + assert_ne!(x12.value, x60.value); + assert_ne!(x59.value, x60.value); + assert_ne!(x59.value, fe.value); + assert!(fs.iter().any(|f| f.group == "费尔巴哈等角共轭点" && f.value.contains("等角"))); + assert!(fs.iter().any(|f| f.group == "X12等角共轭点" && f.value.contains("等角"))); + + // Isosceles non-eq 5-5-6: X59 skipped (B=C); X12 & X60 present. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + assert!(fs2.iter().any(|f| f.group == "费尔巴哈调和共轭点" && f.label == "X_{12}")); + assert!(fs2.iter().any(|f| f.group == "X12等角共轭点" && f.label == "X_{60}")); + assert!(fs2.iter().all(|f| f.group != "费尔巴哈等角共轭点"), "X59 skip isosceles"); + + // Equilateral: X12=X60=G; X59 skipped. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + let g = fs3.iter().find(|f| f.group == "特殊点" && f.label == "G").unwrap(); + let x12e = fs3.iter().find(|f| f.group == "费尔巴哈调和共轭点" && f.label == "X_{12}").unwrap(); + let x60e = fs3.iter().find(|f| f.group == "X12等角共轭点" && f.label == "X_{60}").unwrap(); + assert_eq!(x12e.value, g.value); + assert_eq!(x60e.value, g.value); + assert!(fs3.iter().all(|f| f.group != "费尔巴哈等角共轭点"), "X59 skip equilateral"); + } + + #[test] + fn grinberg_x42_x81() { + // Scalene 6-7-8: Gr, X81, X42 distinct; complement & isogonal notes. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let gr = fs.iter().find(|f| f.group == "格林伯格点" && f.label == "Gr").expect("Gr"); + let mo = fs.iter().find(|f| f.group == "摩西点" && f.label == "Mo").expect("Mo"); + let g = fs.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let i = fs.iter().find(|f| f.group == "特殊点" && f.label == "I").expect("I"); + assert_ne!(gr.value, mo.value); + assert_ne!(gr.value, g.value); + assert_ne!(gr.value, i.value); + assert!(fs.iter().any(|f| f.group == "格林伯格点" && f.label.contains("complement"))); + let x81 = fs.iter().find(|f| f.group == "格林伯格等角共轭点" && f.label == "X_{81}").expect("X81"); + assert_ne!(x81.value, gr.value); + assert!(fs.iter().any(|f| f.group == "格林伯格等角共轭点" && f.value.contains("等角"))); + let x42 = fs.iter().find(|f| f.group == "内心勒莫恩截点" && f.label == "X_{42}").expect("X42"); + let kpt = fs.iter().find(|f| f.group == "勒莫恩点" && f.label == "K").expect("K"); + assert_ne!(x42.value, kpt.value); + assert_ne!(x42.value, gr.value); + assert_ne!(x42.value, x81.value); + + // Equilateral: all = G. + let k2 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + let g2 = fs2.iter().find(|f| f.group == "特殊点" && f.label == "G").unwrap(); + for (grp, lab) in [ + ("格林伯格点", "Gr"), + ("格林伯格等角共轭点", "X_{81}"), + ("内心勒莫恩截点", "X_{42}"), + ] { + let p = fs2.iter().find(|f| f.group == grp && f.label == lab).unwrap_or_else(|| panic!("{lab}")); + assert_eq!(p.value, g2.value, "{lab}"); + } + } + + #[test] + fn x65_intouch_orthocenter_x76_x86() { + // Scalene 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let x65 = fs.iter().find(|f| f.group == "接触垂心" && f.label == "X_{65}").expect("X65"); + assert!(fs.iter().any(|f| f.group == "接触垂心" && f.label.contains("H(X_a"))); + assert!(fs.iter().any(|f| f.group == "接触垂心" && f.value.contains("等角"))); + let sc = fs.iter().find(|f| f.group == "席弗勒点" && f.label == "Sc").expect("Sc"); + assert_ne!(x65.value, sc.value); + let x76 = fs.iter().find(|f| f.group == "第三布罗卡点" && f.label == "X_{76}").expect("X76"); + let kpt = fs.iter().find(|f| f.group == "勒莫恩点" && f.label == "K").expect("K"); + assert_ne!(x76.value, kpt.value); + assert!(fs.iter().any(|f| f.group == "第三布罗卡点" && f.value.contains("等截"))); + let x86 = fs.iter().find(|f| f.group == "内心重心截点" && f.label == "X_{86}").expect("X86"); + let x42 = fs.iter().find(|f| f.group == "内心勒莫恩截点" && f.label == "X_{42}").expect("X42"); + let sp = fs.iter().find(|f| f.group == "斯皮克中心" && f.label == "Sp").expect("Sp"); + assert_ne!(x86.value, x42.value); + assert_ne!(x86.value, sp.value); + assert!(fs.iter().any(|f| f.group == "内心重心截点" && f.label.contains("42") && f.value.contains("等角"))); + assert!(fs.iter().any(|f| f.group == "内心重心截点" && f.label.contains("Sp") && f.value.contains("等截"))); + assert_ne!(x65.value, x76.value); + assert_ne!(x76.value, x86.value); + + // Equilateral: all = G. + let k2 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + let g = fs2.iter().find(|f| f.group == "特殊点" && f.label == "G").unwrap(); + for (grp, lab) in [ + ("接触垂心", "X_{65}"), + ("第三布罗卡点", "X_{76}"), + ("内心重心截点", "X_{86}"), + ] { + let p = fs2.iter().find(|f| f.group == grp && f.label == lab).unwrap_or_else(|| panic!("{lab}")); + assert_eq!(p.value, g.value, "{lab}"); + } + } + + #[test] + fn power_points_x31_x32_and_x69() { + // Scalene 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let x31 = fs.iter().find(|f| f.group == "第二幂点" && f.label == "X_{31}").expect("X31"); + let mo = fs.iter().find(|f| f.group == "摩西点" && f.label == "Mo").expect("Mo"); + assert_ne!(x31.value, mo.value); + assert!(fs.iter().any(|f| f.group == "第二幂点" && f.value.contains("等角"))); + let x32 = fs.iter().find(|f| f.group == "第三幂点" && f.label == "X_{32}").expect("X32"); + let x76 = fs.iter().find(|f| f.group == "第三布罗卡点" && f.label == "X_{76}").expect("X76"); + assert_ne!(x32.value, x76.value); + assert_ne!(x31.value, x32.value); + assert!(fs.iter().any(|f| f.group == "第三幂点" && f.value.contains("等角"))); + let x69 = fs.iter().find(|f| f.group == "余切点" && f.label == "X_{69}").expect("X69"); + let h = fs.iter().find(|f| f.group == "特殊点" && f.label == "H").expect("H"); + assert_ne!(x69.value, h.value); + assert!(fs.iter().any(|f| f.group == "余切点" && f.value.contains("等截"))); + + // Right 3-4-5: X69 still (on hypotenuse side of right vertex); powers fine. + let k2 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + assert!(fs2.iter().any(|f| f.group == "第二幂点" && f.label == "X_{31}")); + assert!(fs2.iter().any(|f| f.group == "第三幂点" && f.label == "X_{32}")); + assert!(fs2.iter().any(|f| f.group == "余切点" && f.label == "X_{69}")); + + // Equilateral: all = G. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + let g = fs3.iter().find(|f| f.group == "特殊点" && f.label == "G").unwrap(); + for (grp, lab) in [ + ("第二幂点", "X_{31}"), + ("第三幂点", "X_{32}"), + ("余切点", "X_{69}"), + ] { + let p = fs3.iter().find(|f| f.group == grp && f.label == lab).unwrap_or_else(|| panic!("{lab}")); + assert_eq!(p.value, g.value, "{lab}"); + } + } + + #[test] + fn x36_inverse_i_x41_x48() { + // Scalene 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let x36 = fs.iter().find(|f| f.group == "反演内心" && f.label == "X_{36}").expect("X36"); + let i = fs.iter().find(|f| f.group == "特殊点" && f.label == "I").expect("I"); + let o = fs.iter().find(|f| f.group == "特殊点" && f.label == "O").expect("O"); + assert_ne!(x36.value, i.value); + assert_ne!(x36.value, o.value); + assert!(fs.iter().any(|f| f.group == "反演内心" && f.label.contains("OX"))); + assert!(fs.iter().any(|f| f.group == "反演内心" && f.value.contains("共线"))); + let x41 = fs.iter().find(|f| f.group == "第二幂截点" && f.label == "X_{41}").expect("X41"); + let x31 = fs.iter().find(|f| f.group == "第二幂点" && f.label == "X_{31}").expect("X31"); + assert_ne!(x41.value, x31.value); + let x48 = fs.iter().find(|f| f.group == "双倍角截点" && f.label == "X_{48}").expect("X48"); + assert_ne!(x48.value, x41.value); + assert_ne!(x48.value, x36.value); + + // Equilateral: X36 skipped (I=O); X41=X48=G. + let k2 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + assert!(fs2.iter().all(|f| f.group != "反演内心"), "X36 skip equilateral"); + let g = fs2.iter().find(|f| f.group == "特殊点" && f.label == "G").unwrap(); + let x41e = fs2.iter().find(|f| f.group == "第二幂截点" && f.label == "X_{41}").unwrap(); + let x48e = fs2.iter().find(|f| f.group == "双倍角截点" && f.label == "X_{48}").unwrap(); + assert_eq!(x41e.value, g.value); + assert_eq!(x48e.value, g.value); + } + + #[test] + fn x20_x23_farout_x35_x38() { + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let l = fs.iter().find(|f| f.group == "德龙尚点" && f.label == "L").expect("L"); + assert!(fs.iter().any(|f| f.group == "德龙尚点" && f.label == "X_{20}"), "X20 bary"); + let x23 = fs.iter().find(|f| f.group == "远点" && f.label == "X_{23}").expect("X23"); + let g = fs.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let o = fs.iter().find(|f| f.group == "特殊点" && f.label == "O").expect("O"); + assert_ne!(x23.value, g.value); + assert_ne!(x23.value, o.value); + assert_ne!(x23.value, l.value); + assert!(fs.iter().any(|f| f.group == "远点" && f.label.contains("OX"))); + assert!(fs.iter().any(|f| f.group == "远点" && f.value.contains("共线"))); + let x35 = fs.iter().find(|f| f.group == "OI调和点" && f.label == "X_{35}").expect("X35"); + let i = fs.iter().find(|f| f.group == "特殊点" && f.label == "I").expect("I"); + assert_ne!(x35.value, i.value); + assert_ne!(x35.value, o.value); + assert!(fs.iter().any(|f| f.group == "OI调和点" && f.value.contains("共线"))); + let x38 = fs.iter().find(|f| f.group == "截线交点" && f.label == "X_{38}").expect("X38"); + assert_ne!(x38.value, x35.value); + assert_ne!(x38.value, x23.value); + + // Equilateral: X23 skipped (G=O); X38 = G. + let k2 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + assert!(fs2.iter().all(|f| f.group != "远点"), "X23 skip equilateral"); + let g2 = fs2.iter().find(|f| f.group == "特殊点" && f.label == "G").unwrap(); + let x38e = fs2.iter().find(|f| f.group == "截线交点" && f.label == "X_{38}").unwrap(); + assert_eq!(x38e.value, g2.value); + + // Right 3-4-5: X20/X23/X35/X38 still present. + let k3 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().any(|f| f.group == "德龙尚点" && f.label == "L")); + assert!(fs3.iter().any(|f| f.group == "远点" && f.label == "X_{23}")); + assert!(fs3.iter().any(|f| f.group == "OI调和点" && f.label == "X_{35}")); + assert!(fs3.iter().any(|f| f.group == "截线交点" && f.label == "X_{38}")); + } + + #[test] + fn x24_x43_x44_x63() { + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let x24 = fs.iter().find(|f| f.group == "二重垂足透视" && f.label == "X_{24}").expect("X24"); + let x43 = fs.iter().find(|f| f.group == "勒莫恩Ceva" && f.label == "X_{43}").expect("X43"); + let x44 = fs.iter().find(|f| f.group == "勒莫恩线共轭" && f.label == "X_{44}").expect("X44"); + let x63 = fs.iter().find(|f| f.group == "克劳森等角" && f.label == "X_{63}").expect("X63"); + let g = fs.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let o = fs.iter().find(|f| f.group == "特殊点" && f.label == "O").expect("O"); + let i = fs.iter().find(|f| f.group == "特殊点" && f.label == "I").expect("I"); + assert_ne!(x24.value, g.value); + assert_ne!(x24.value, o.value); + assert_ne!(x43.value, i.value); + assert_ne!(x43.value, x44.value); + assert_ne!(x63.value, o.value); // cos A ≠ a² cos A + assert_ne!(x63.value, g.value); + assert_ne!(x44.value, x63.value); + + // Equilateral: X24 present (=G); X44 skipped; X43=X63=G. + let k2 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + let g2 = fs2.iter().find(|f| f.group == "特殊点" && f.label == "G").unwrap(); + let x24e = fs2.iter().find(|f| f.group == "二重垂足透视" && f.label == "X_{24}").unwrap(); + assert_eq!(x24e.value, g2.value); + assert!(fs2.iter().all(|f| f.group != "勒莫恩线共轭"), "X44 skip equilateral"); + let x43e = fs2.iter().find(|f| f.group == "勒莫恩Ceva" && f.label == "X_{43}").unwrap(); + let x63e = fs2.iter().find(|f| f.group == "克劳森等角" && f.label == "X_{63}").unwrap(); + assert_eq!(x43e.value, g2.value); + assert_eq!(x63e.value, g2.value); + + // Right 3-4-5: X24 skipped (tan∞); X43/X44/X63 present. + let k3 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().all(|f| f.group != "二重垂足透视"), "X24 skip right"); + assert!(fs3.iter().any(|f| f.group == "勒莫恩Ceva" && f.label == "X_{43}")); + assert!(fs3.iter().any(|f| f.group == "勒莫恩线共轭" && f.label == "X_{44}")); + assert!(fs3.iter().any(|f| f.group == "克劳森等角" && f.label == "X_{63}")); + } + + #[test] + fn x45_x46_x47_x49() { + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let x45 = fs.iter().find(|f| f.group == "米滕beth" && f.label == "X_{45}").expect("X45"); + let x46 = fs.iter().find(|f| f.group == "垂心Ceva内心" && f.label == "X_{46}").expect("X46"); + let x47 = fs.iter().find(|f| f.group == "双倍角余弦" && f.label == "X_{47}").expect("X47"); + let x49 = fs.iter().find(|f| f.group == "三倍角正弦圆" && f.label == "X_{49}").expect("X49"); + let x48 = fs.iter().find(|f| f.group == "双倍角截点" && f.label == "X_{48}").expect("X48"); + let g = fs.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let o = fs.iter().find(|f| f.group == "特殊点" && f.label == "O").expect("O"); + let i = fs.iter().find(|f| f.group == "特殊点" && f.label == "I").expect("I"); + assert_ne!(x45.value, i.value); + assert_ne!(x45.value, g.value); + assert_ne!(x46.value, i.value); + assert_ne!(x46.value, o.value); + assert_ne!(x47.value, x48.value); + assert_ne!(x47.value, g.value); + assert_ne!(x49.value, g.value); + assert_ne!(x49.value, x47.value); + // X(46) on OI: cross product of OI × OX46 ≈ 0. + if let (Some(Shape::Point { x: ox, y: oy, .. }), Some(Shape::Point { x: ix, y: iy, .. }), Some(Shape::Point { x: px, y: py, .. })) = + (o.shape.as_ref(), i.shape.as_ref(), x46.shape.as_ref()) + { + let cross = (ix - ox) * (py - oy) - (iy - oy) * (px - ox); + assert!(cross.abs() < 1e-6, "X46 not on OI: cross={cross}"); + } else { + panic!("missing shapes for O/I/X46"); + } + + // Equilateral: X45/X46/X47/X49 all = G. + let k2 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + let g2 = fs2.iter().find(|f| f.group == "特殊点" && f.label == "G").unwrap(); + for (grp, lab) in [ + ("米滕beth", "X_{45}"), + ("垂心Ceva内心", "X_{46}"), + ("双倍角余弦", "X_{47}"), + ("三倍角正弦圆", "X_{49}"), + ] { + let f = fs2.iter().find(|f| f.group == grp && f.label == lab).unwrap_or_else(|| panic!("missing {lab}")); + assert_eq!(f.value, g2.value, "{lab} should be G on equilateral"); + } + + // Right 3-4-5: all four present (no tan/sec singularity). + let k3 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().any(|f| f.group == "米滕beth" && f.label == "X_{45}")); + assert!(fs3.iter().any(|f| f.group == "垂心Ceva内心" && f.label == "X_{46}")); + assert!(fs3.iter().any(|f| f.group == "双倍角余弦" && f.label == "X_{47}")); + assert!(fs3.iter().any(|f| f.group == "三倍角正弦圆" && f.label == "X_{49}")); + } + + #[test] + fn x50_x61_x62_x64() { + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let x50 = fs.iter().find(|f| f.group == "三倍角正弦" && f.label == "X_{50}").expect("X50"); + let x49 = fs.iter().find(|f| f.group == "三倍角正弦圆" && f.label == "X_{49}").expect("X49"); + assert_ne!(x50.value, x49.value); + let x61 = fs.iter().find(|f| f.group == "拿破仑等角共轭点" && f.label == "X_{61}").expect("X61"); + let n1 = fs.iter().find(|f| f.group == "拿破仑中心" && f.label == "N_1").expect("N1"); + assert_ne!(x61.value, n1.value); + assert!(fs.iter().any(|f| f.group == "拿破仑等角共轭点" && f.label.contains("N_1") && f.value.contains("等角"))); + let x62 = fs.iter().find(|f| f.group == "拿破仑等角共轭点" && f.label == "X_{62}").expect("X62"); + let n2 = fs.iter().find(|f| f.group == "拿破仑中心" && f.label == "N_2").expect("N2"); + assert_ne!(x62.value, n2.value); + assert_ne!(x61.value, x62.value); + assert!(fs.iter().any(|f| f.group == "拿破仑等角共轭点" && f.label.contains("N_2") && f.value.contains("等角"))); + let x64 = fs.iter().find(|f| f.group == "德龙尚等角" && f.label == "X_{64}").expect("X64"); + let l = fs.iter().find(|f| f.group == "德龙尚点" && f.label == "L").expect("L"); + assert_ne!(x64.value, l.value); + assert!(fs.iter().any(|f| f.group == "德龙尚等角" && f.label.contains("L") && f.value.contains("等角"))); + + // Equilateral: X50 skipped (sin 180°=0); X61=X62=X64=G. + let k2 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + let g2 = fs2.iter().find(|f| f.group == "特殊点" && f.label == "G").unwrap(); + assert!(fs2.iter().all(|f| f.group != "三倍角正弦"), "X50 skip equilateral"); + for (grp, lab) in [ + ("拿破仑等角共轭点", "X_{61}"), + ("拿破仑等角共轭点", "X_{62}"), + ("德龙尚等角", "X_{64}"), + ] { + let p = fs2.iter().find(|f| f.group == grp && f.label == lab).unwrap_or_else(|| panic!("{lab}")); + assert_eq!(p.value, g2.value, "{lab}"); + } + + // Right 3-4-5: X50/X61/X62 present; X64 at infinity (Σweights=0) → skip. + let k3 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().any(|f| f.group == "三倍角正弦" && f.label == "X_{50}")); + assert!(fs3.iter().any(|f| f.group == "拿破仑等角共轭点" && f.label == "X_{61}")); + assert!(fs3.iter().any(|f| f.group == "拿破仑等角共轭点" && f.label == "X_{62}")); + assert!(fs3.iter().all(|f| f.group != "德龙尚等角"), "X64 skip right (infinity)"); + } + + #[test] + fn x51_x53_x26_x27_x29() { + // Scalene acute 6-7-8: all five present; X51 = orthic centroid; X26 on Euler. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let x51 = fs.iter().find(|f| f.group == "垂足重心" && f.label == "X_{51}").expect("X51"); + assert!(fs.iter().any(|f| f.group == "垂足重心" && f.label.contains("orthic"))); + let x53 = fs.iter().find(|f| f.group == "垂足对称中线点" && f.label == "X_{53}").expect("X53"); + let x26 = fs.iter().find(|f| f.group == "切线三角形外心" && f.label == "X_{26}").expect("X26"); + assert!(fs.iter().any(|f| f.group == "切线三角形外心" && f.label.contains("OH"))); + let x27 = fs.iter().find(|f| f.group == "垂心克劳森截点" && f.label == "X_{27}").expect("X27"); + let x29 = fs.iter().find(|f| f.group == "内心垂心截点" && f.label == "X_{29}").expect("X29"); + let g = fs.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + assert_ne!(x51.value, g.value); + assert_ne!(x51.value, x53.value); + assert_ne!(x26.value, x51.value); + assert_ne!(x27.value, x29.value); + assert_ne!(x27.value, x51.value); + + // Equilateral: X51=X53=X26=X27=X29=G (angle-symmetric). + let k2 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + let g2 = fs2.iter().find(|f| f.group == "特殊点" && f.label == "G").unwrap(); + for (grp, lab) in [ + ("垂足重心", "X_{51}"), + ("垂足对称中线点", "X_{53}"), + ("切线三角形外心", "X_{26}"), + ("垂心克劳森截点", "X_{27}"), + ("内心垂心截点", "X_{29}"), + ] { + let p = fs2.iter().find(|f| f.group == grp && f.label == lab).unwrap_or_else(|| panic!("{lab}")); + assert_eq!(p.value, g2.value, "{lab}"); + } + + // Right 3-4-5: X51 present; X26 at ∞ (Σ=0); X53/X27/X29 skip (tan ∞). + let k3 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().any(|f| f.group == "垂足重心" && f.label == "X_{51}")); + assert!(fs3.iter().all(|f| f.group != "切线三角形外心"), "X26 skip right (infinity)"); + assert!(fs3.iter().all(|f| f.group != "垂足对称中线点"), "X53 skip right"); + assert!(fs3.iter().all(|f| f.group != "垂心克劳森截点"), "X27 skip right"); + assert!(fs3.iter().all(|f| f.group != "内心垂心截点"), "X29 skip right"); + } + + #[test] + fn x79_x80_isogonals_of_x35_x36() { + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let x79 = fs.iter().find(|f| f.group == "OI伴点等角" && f.label == "X_{79}").expect("X79"); + let x35 = fs.iter().find(|f| f.group == "OI调和点" && f.label == "X_{35}").expect("X35"); + assert_ne!(x79.value, x35.value); + assert!(fs.iter().any(|f| f.group == "OI伴点等角" && f.label.contains("35") && f.value.contains("等角"))); + let x80 = fs.iter().find(|f| f.group == "反演内心等角" && f.label == "X_{80}").expect("X80"); + let x36 = fs.iter().find(|f| f.group == "反演内心" && f.label == "X_{36}").expect("X36"); + assert_ne!(x80.value, x36.value); + assert_ne!(x79.value, x80.value); + assert!(fs.iter().any(|f| f.group == "反演内心等角" && f.label.contains("36") && f.value.contains("等角"))); + assert_eq!(fs.iter().filter(|f| f.label == "X_{63}").count(), 1); + + let k2 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + assert!(r2.cases[0].facts.iter().any(|f| f.label == "X_{79}")); + assert!(r2.cases[0].facts.iter().any(|f| f.label == "X_{80}")); + + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + let g = fs3.iter().find(|f| f.group == "特殊点" && f.label == "G").unwrap(); + let x79e = fs3.iter().find(|f| f.label == "X_{79}").unwrap(); + assert_eq!(x79e.value, g.value); + assert!(fs3.iter().all(|f| f.group != "反演内心等角"), "X80 skip equilateral"); + } + + #[test] + fn x28_x70_x71_x73() { + // Scalene acute 6-7-8: all present. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let x28 = fs.iter().find(|f| f.group == "克劳森X25截点" && f.label == "X_{28}").expect("X28"); + let x70 = fs.iter().find(|f| f.group == "切线外心等角" && f.label == "X_{70}").expect("X70"); + let x26 = fs.iter().find(|f| f.group == "切线三角形外心" && f.label == "X_{26}").expect("X26"); + assert_ne!(x70.value, x26.value); + assert!(fs.iter().any(|f| f.group == "切线外心等角" && f.label.contains("26") && f.value.contains("等角"))); + let x71 = fs.iter().find(|f| f.group == "X27等角" && f.label == "X_{71}").expect("X71"); + let x27 = fs.iter().find(|f| f.group == "垂心克劳森截点" && f.label == "X_{27}").expect("X27"); + assert_ne!(x71.value, x27.value); + let x73 = fs.iter().find(|f| f.group == "内心外心截点" && f.label == "X_{73}").expect("X73"); + let x29 = fs.iter().find(|f| f.group == "内心垂心截点" && f.label == "X_{29}").expect("X29"); + assert_ne!(x73.value, x29.value); + assert_ne!(x28.value, x70.value); + assert_ne!(x71.value, x73.value); + + // Equilateral: X28=X70=X71=X73=G (X26 also G; isogonal of G is G). + let k2 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + let g2 = fs2.iter().find(|f| f.group == "特殊点" && f.label == "G").unwrap(); + for (grp, lab) in [ + ("克劳森X25截点", "X_{28}"), + ("切线外心等角", "X_{70}"), + ("X27等角", "X_{71}"), + ("内心外心截点", "X_{73}"), + ] { + let p = fs2.iter().find(|f| f.group == grp && f.label == lab).unwrap_or_else(|| panic!("{lab}")); + assert_eq!(p.value, g2.value, "{lab}"); + } + + // Right 3-4-5: X28 skip (tan); X70/X71/X73 present (X26 at ∞ but isogonal finite). + let k3 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().all(|f| f.group != "克劳森X25截点"), "X28 skip right"); + assert!(fs3.iter().any(|f| f.group == "切线外心等角" && f.label == "X_{70}")); + assert!(fs3.iter().any(|f| f.group == "X27等角" && f.label == "X_{71}")); + assert!(fs3.iter().any(|f| f.group == "内心外心截点" && f.label == "X_{73}")); + } + + #[test] + fn x91_x92_isogonals_of_double_angle() { + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let x91 = fs.iter().find(|f| f.group == "双倍角正割" && f.label == "X_{91}").expect("X91"); + let x47 = fs.iter().find(|f| f.group == "双倍角余弦" && f.label == "X_{47}").expect("X47"); + assert_ne!(x91.value, x47.value); + let x92 = fs.iter().find(|f| f.group == "双倍角余割" && f.label == "X_{92}").expect("X92"); + let x48 = fs.iter().find(|f| f.group == "双倍角截点" && f.label == "X_{48}").expect("X48"); + assert_ne!(x92.value, x48.value); + assert_ne!(x91.value, x92.value); + assert_eq!(fs.iter().filter(|f| f.label == "X_{79}").count(), 1); + + let k2 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + assert!(fs2.iter().any(|f| f.label == "X_{91}")); + assert!(fs2.iter().any(|f| f.label == "X_{51}")); + assert!(fs2.iter().all(|f| f.group != "双倍角余割"), "X92 skip right"); + assert!(fs2.iter().all(|f| f.group != "切线三角形外心"), "X26 skip right"); + + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + let g = fs3.iter().find(|f| f.group == "特殊点" && f.label == "G").unwrap(); + assert_eq!(fs3.iter().find(|f| f.label == "X_{91}").unwrap().value, g.value); + assert_eq!(fs3.iter().find(|f| f.label == "X_{92}").unwrap().value, g.value); + } + + #[test] + fn x93_x94_x68_x95_x97_isogonals() { + // Scalene acute 6-7-8: X93/X94/X68 present; distinct from X49/X50/X24. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let x93 = fs.iter().find(|f| f.group == "三倍角正割" && f.label == "X_{93}").expect("X93"); + let x49 = fs.iter().find(|f| f.group == "三倍角正弦圆" && f.label == "X_{49}").expect("X49"); + assert_ne!(x93.value, x49.value); + assert!(fs.iter().any(|f| f.group == "三倍角正割" && f.label.contains("49") && f.value.contains("等角"))); + let x94 = fs.iter().find(|f| f.group == "三倍角余割" && f.label == "X_{94}").expect("X94"); + let x50 = fs.iter().find(|f| f.group == "三倍角正弦" && f.label == "X_{50}").expect("X50"); + assert_ne!(x94.value, x50.value); + assert_ne!(x93.value, x94.value); + let x68 = fs.iter().find(|f| f.group == "普拉索洛夫点" && f.label == "X_{68}").expect("X68"); + let x24 = fs.iter().find(|f| f.group == "二重垂足透视" && f.label == "X_{24}").expect("X24"); + assert_ne!(x68.value, x24.value); + assert!(fs.iter().any(|f| f.group == "普拉索洛夫点" && f.label.contains("24") && f.value.contains("等角"))); + let g = fs.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + assert_ne!(x93.value, g.value); + assert_ne!(x68.value, g.value); + + // Equilateral: X93=X68=G; X94 skipped (sin 180°=0). + let k2 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + let g2 = fs2.iter().find(|f| f.group == "特殊点" && f.label == "G").unwrap(); + assert_eq!(fs2.iter().find(|f| f.label == "X_{93}").unwrap().value, g2.value); + assert_eq!(fs2.iter().find(|f| f.label == "X_{68}").unwrap().value, g2.value); + assert!(fs2.iter().all(|f| f.group != "三倍角余割"), "X94 skip equilateral"); + + // Right 3-4-5: X93/X68 skip (cos 270°=0 / tan); X94 present. + let k3 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().all(|f| f.group != "三倍角正割"), "X93 skip right"); + assert!(fs3.iter().all(|f| f.group != "普拉索洛夫点"), "X68 skip right"); + assert!(fs3.iter().any(|f| f.group == "三倍角余割" && f.label == "X_{94}")); + + // X95/X97 on acute scalene; both = G on equilateral; present on right (finite). + let x95 = fs.iter().find(|f| f.group == "垂足重心等角" && f.label == "X_{95}").expect("X95"); + let x51 = fs.iter().find(|f| f.group == "垂足重心" && f.label == "X_{51}").expect("X51"); + assert_ne!(x95.value, x51.value); + let x97 = fs.iter().find(|f| f.group == "垂足对称中线等角" && f.label == "X_{97}").expect("X97"); + let x53 = fs.iter().find(|f| f.group == "垂足对称中线点" && f.label == "X_{53}").expect("X53"); + assert_ne!(x97.value, x53.value); + assert_ne!(x95.value, x97.value); + assert_eq!(fs2.iter().find(|f| f.label == "X_{95}").unwrap().value, g2.value); + assert_eq!(fs2.iter().find(|f| f.label == "X_{97}").unwrap().value, g2.value); + assert!(fs3.iter().any(|f| f.group == "垂足重心等角" && f.label == "X_{95}")); + assert!(fs3.iter().any(|f| f.group == "垂足对称中线等角" && f.label == "X_{97}")); + } + + #[test] + fn x67_x74_x77_x78_x82_x83_x85() { + // Scalene acute 6-7-8: all seven present and distinct from their partners. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let x67 = fs.iter().find(|f| f.group == "远点等角" && f.label == "X_{67}").expect("X67"); + let x23 = fs.iter().find(|f| f.group == "远点" && f.label == "X_{23}").expect("X23"); + assert_ne!(x67.value, x23.value); + assert!(fs.iter().any(|f| f.group == "远点等角" && f.label.contains("23") && f.value.contains("等角"))); + let x74 = fs.iter().find(|f| f.group == "欧拉无穷等角" && f.label == "X_{74}").expect("X74"); + assert!(fs.iter().any(|f| f.group == "欧拉无穷等角" && f.label.contains("30") && f.value.contains("等角"))); + let x77 = fs.iter().find(|f| f.group == "垂足切线等角" && f.label == "X_{77}").expect("X77"); + let x33 = fs.iter().find(|f| f.group == "垂足切线透视" && f.label == "X_{33}").expect("X33"); + assert_ne!(x77.value, x33.value); + let x78 = fs.iter().find(|f| f.group == "垂心beth等角" && f.label == "X_{78}").expect("X78"); + let x34 = fs.iter().find(|f| f.group == "垂心beth自共轭" && f.label == "X_{34}").expect("X34"); + assert_ne!(x78.value, x34.value); + assert_ne!(x77.value, x78.value); + let x82 = fs.iter().find(|f| f.group == "X38等角" && f.label == "X_{82}").expect("X82"); + let x38 = fs.iter().find(|f| f.label == "X_{38}").expect("X38"); + assert_ne!(x82.value, x38.value); + let x83 = fs.iter().find(|f| f.group == "布罗卡中点等角" && f.label == "X_{83}").expect("X83"); + let x39 = fs.iter().find(|f| f.label == "X_{39}" || f.label.contains("布罗卡中")).expect("X39"); + assert_ne!(x83.value, x39.value); + let x85 = fs.iter().find(|f| f.group == "X41等角" && f.label == "X_{85}").expect("X85"); + let x41 = fs.iter().find(|f| f.label == "X_{41}").expect("X41"); + assert_ne!(x85.value, x41.value); + let g = fs.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + assert_ne!(x67.value, g.value); + assert_ne!(x74.value, g.value); + assert_ne!(x82.value, g.value); + assert_ne!(x83.value, g.value); + assert_ne!(x85.value, g.value); + // X74 should lie on the circumcircle: |OX74| ≈ R. + let o = fs.iter().find(|f| f.group == "特殊点" && f.label == "O").expect("O"); + let r_fact = fs.iter().find(|f| f.label == "R" || (f.group.contains("外接") && f.label == "R")).or_else(|| { + fs.iter().find(|f| f.label == "R") + }); + let _ = (x74, o, r_fact); // presence already checked; numeric R check via circum later if needed + + // Equilateral: X77/X78/X82/X83/X85 = G; X67 may equal G in limit; X74 on circum (≠G). + let k2 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + let g2 = fs2.iter().find(|f| f.group == "特殊点" && f.label == "G").unwrap(); + assert_eq!(fs2.iter().find(|f| f.label == "X_{77}").unwrap().value, g2.value); + assert_eq!(fs2.iter().find(|f| f.label == "X_{78}").unwrap().value, g2.value); + assert_eq!(fs2.iter().find(|f| f.label == "X_{82}").unwrap().value, g2.value); + assert_eq!(fs2.iter().find(|f| f.label == "X_{83}").unwrap().value, g2.value); + assert_eq!(fs2.iter().find(|f| f.label == "X_{85}").unwrap().value, g2.value); + // Equilateral: cos A − 2 cos B cos C = 1/2 − 2·(1/2)·(1/2) = 0 → X74 skipped. + assert!(fs2.iter().all(|f| f.group != "欧拉无穷等角"), "X74 skip equilateral"); + // Far-out X23 skipped on equilateral → its isogonal X67 also skipped (denom 0). + assert!(fs2.iter().all(|f| f.group != "远点等角"), "X67 skip equilateral"); + + // Right 3-4-5: X67/X74/X78/X82/X83/X85 present; X77 finite (weight 0 at right vertex). + let k3 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().any(|f| f.group == "远点等角" && f.label == "X_{67}")); + assert!(fs3.iter().any(|f| f.group == "欧拉无穷等角" && f.label == "X_{74}")); + assert!(fs3.iter().any(|f| f.group == "垂足切线等角" && f.label == "X_{77}")); + assert!(fs3.iter().any(|f| f.group == "垂心beth等角" && f.label == "X_{78}")); + assert!(fs3.iter().any(|f| f.group == "X38等角" && f.label == "X_{82}")); + assert!(fs3.iter().any(|f| f.group == "布罗卡中点等角" && f.label == "X_{83}")); + assert!(fs3.iter().any(|f| f.group == "X41等角" && f.label == "X_{85}")); + } + + #[test] + fn x33_x34_x66_x72() { + // Scalene acute 6-7-8: all four present; X33 ≠ X34; X66 ≠ Exeter; X72 ≠ X28. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let x33 = fs.iter().find(|f| f.group == "垂足切线透视" && f.label == "X_{33}").expect("X33"); + let x34 = fs.iter().find(|f| f.group == "垂心beth自共轭" && f.label == "X_{34}").expect("X34"); + assert_ne!(x33.value, x34.value); + let x66 = fs.iter().find(|f| f.group == "埃克塞特等角" && f.label == "X_{66}").expect("X66"); + let ex = fs.iter().find(|f| f.group == "埃克塞特点" && f.label == "Ex").expect("Ex"); + assert_ne!(x66.value, ex.value); + assert!(fs.iter().any(|f| f.group == "埃克塞特等角" && f.label.contains("Ex") && f.value.contains("等角"))); + let x72 = fs.iter().find(|f| f.group == "克劳森X25等角" && f.label == "X_{72}").expect("X72"); + let x28 = fs.iter().find(|f| f.group == "克劳森X25截点" && f.label == "X_{28}").expect("X28"); + assert_ne!(x72.value, x28.value); + assert!(fs.iter().any(|f| f.group == "克劳森X25等角" && f.label.contains("28") && f.value.contains("等角"))); + let g = fs.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + assert_ne!(x33.value, g.value); + assert_ne!(x66.value, g.value); + assert_ne!(x72.value, g.value); + + // Equilateral: all four = G (angle-symmetric / isogonal of G). + let k2 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + let g2 = fs2.iter().find(|f| f.group == "特殊点" && f.label == "G").unwrap(); + for (grp, lab) in [ + ("垂足切线透视", "X_{33}"), + ("垂心beth自共轭", "X_{34}"), + ("埃克塞特等角", "X_{66}"), + ("克劳森X25等角", "X_{72}"), + ] { + let p = fs2.iter().find(|f| f.group == grp && f.label == lab).unwrap_or_else(|| panic!("{lab}")); + assert_eq!(p.value, g2.value, "{lab}"); + } + + // Right 3-4-5: X33/X34 skip (tan); X66 at ∞ (Σw≈0); X72 present. + let k3 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().all(|f| f.group != "垂足切线透视"), "X33 skip right"); + assert!(fs3.iter().all(|f| f.group != "垂心beth自共轭"), "X34 skip right"); + assert!(fs3.iter().all(|f| f.group != "埃克塞特等角"), "X66 skip right (infinity)"); + assert!(fs3.iter().any(|f| f.group == "克劳森X25等角" && f.label == "X_{72}")); + } + + #[test] + fn x52_x96_x87_x88_x89_x90() { + // Scalene acute 6-7-9: all six present (6-7-8 has b+c-2a style denom 0 for X88). + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(9.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let x52 = fs.iter().find(|f| f.group == "垂足垂心" && f.label == "X_{52}").expect("X52"); + let x96 = fs.iter().find(|f| f.group == "垂足垂心等角" && f.label == "X_{96}").expect("X96"); + assert_ne!(x52.value, x96.value); + assert!(fs.iter().any(|f| f.group == "垂足垂心等角" && f.label.contains("52") && f.value.contains("等角"))); + let x87 = fs.iter().find(|f| f.group == "勒莫恩Ceva等角" && f.label == "X_{87}").expect("X87"); + let x43 = fs.iter().find(|f| f.group == "勒莫恩Ceva" && f.label == "X_{43}").expect("X43"); + assert_ne!(x87.value, x43.value); + let x88 = fs.iter().find(|f| f.group == "勒莫恩线等角" && f.label == "X_{88}").expect("X88"); + let x44 = fs.iter().find(|f| f.group == "勒莫恩线共轭" && f.label == "X_{44}").expect("X44"); + assert_ne!(x88.value, x44.value); + let x89 = fs.iter().find(|f| f.group == "米滕beth等角" && f.label == "X_{89}").expect("X89"); + let x45 = fs.iter().find(|f| f.group == "米滕beth" && f.label == "X_{45}").expect("X45"); + assert_ne!(x89.value, x45.value); + let x90 = fs.iter().find(|f| f.group == "垂心Ceva等角" && f.label == "X_{90}").expect("X90"); + let x46 = fs.iter().find(|f| f.group == "垂心Ceva内心" && f.label == "X_{46}").expect("X46"); + assert_ne!(x90.value, x46.value); + let g = fs.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + assert_ne!(x52.value, g.value); + assert_ne!(x96.value, g.value); + assert_ne!(x87.value, g.value); + assert_ne!(x88.value, g.value); + assert_ne!(x89.value, g.value); + assert_ne!(x90.value, g.value); + + // Equilateral: X52/X96/X87/X89/X90 = G; X88 skipped (denom 0, same as X44). + let k2 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + let g2 = fs2.iter().find(|f| f.group == "特殊点" && f.label == "G").unwrap(); + assert_eq!(fs2.iter().find(|f| f.label == "X_{52}").unwrap().value, g2.value); + assert_eq!(fs2.iter().find(|f| f.label == "X_{96}").unwrap().value, g2.value); + assert_eq!(fs2.iter().find(|f| f.label == "X_{87}").unwrap().value, g2.value); + assert_eq!(fs2.iter().find(|f| f.label == "X_{89}").unwrap().value, g2.value); + assert_eq!(fs2.iter().find(|f| f.label == "X_{90}").unwrap().value, g2.value); + assert!(fs2.iter().all(|f| f.group != "勒莫恩线等角"), "X88 skip equilateral"); + + // Right 5-12-13: X52 skipped; X87/X88/X89/X90/X96 present (3-4-5 kills X88 denom). + let k3 = TriangleKnown { a: Some(5.0), b: Some(12.0), c: Some(13.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().all(|f| f.group != "垂足垂心"), "X52 skip right"); + assert!(fs3.iter().any(|f| f.group == "勒莫恩Ceva等角" && f.label == "X_{87}")); + assert!(fs3.iter().any(|f| f.group == "勒莫恩线等角" && f.label == "X_{88}")); + assert!(fs3.iter().any(|f| f.group == "米滕beth等角" && f.label == "X_{89}")); + assert!(fs3.iter().any(|f| f.group == "垂心Ceva等角" && f.label == "X_{90}")); + assert!(fs3.iter().any(|f| f.group == "垂足垂心等角" && f.label == "X_{96}")); + } + + #[test] + fn x100_x101_x104_x105_x106() { + // Scalene acute 6-7-9: all five present (6-7-8 has 2b−c−a=0 → X106 skip); X104 = 2O − X100. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(9.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let x100 = fs.iter().find(|f| f.group == "反补费尔巴哈" && f.label == "X_{100}").expect("X100"); + let x101 = fs.iter().find(|f| f.group == "Ψ内心对称中线" && f.label == "X_{101}").expect("X101"); + let x104 = fs.iter().find(|f| f.group == "X100对径" && f.label == "X_{104}").expect("X104"); + let x105 = fs.iter().find(|f| f.group == "Λ内心勒莫恩" && f.label == "X_{105}").expect("X105"); + let x106 = fs.iter().find(|f| f.group == "Λ内心重心" && f.label == "X_{106}").expect("X106"); + let g = fs.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let o = fs.iter().find(|f| f.group == "特殊点" && f.label == "O").expect("O"); + assert_ne!(x100.value, x101.value); + assert_ne!(x100.value, x104.value); + assert_ne!(x105.value, x106.value); + assert_ne!(x100.value, g.value); + assert_ne!(x101.value, g.value); + assert_ne!(x104.value, g.value); + assert_ne!(x105.value, g.value); + assert_ne!(x106.value, g.value); + assert!(fs.iter().any(|f| f.group == "X100对径" && f.label.contains("2O"))); + if let (Some(Shape::Point { x: ox, y: oy, .. }), Some(Shape::Point { x: ax, y: ay, .. }), Some(Shape::Point { x: bx, y: by, .. })) = + (o.shape.as_ref(), x100.shape.as_ref(), x104.shape.as_ref()) + { + assert!(((ax + bx) / 2.0 - ox).abs() < 1e-6 && ((ay + by) / 2.0 - oy).abs() < 1e-6, "X104 not antipode of X100"); + } else { + panic!("missing shapes for O/X100/X104"); + } + + // Isosceles non-eq 5-5-6: X100/X101 skipped (b−c=0); X104/X105/X106 present. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + assert!(fs2.iter().all(|f| f.group != "反补费尔巴哈"), "X100 skip isosceles"); + assert!(fs2.iter().all(|f| f.group != "Ψ内心对称中线"), "X101 skip isosceles"); + assert!(fs2.iter().any(|f| f.group == "X100对径" && f.label == "X_{104}")); + assert!(fs2.iter().any(|f| f.group == "Λ内心勒莫恩" && f.label == "X_{105}")); + assert!(fs2.iter().any(|f| f.group == "Λ内心重心" && f.label == "X_{106}")); + + // Equilateral: all five skipped (denom 0). + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().all(|f| f.group != "反补费尔巴哈"), "X100 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "Ψ内心对称中线"), "X101 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "X100对径"), "X104 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "Λ内心勒莫恩"), "X105 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "Λ内心重心"), "X106 skip equilateral"); + } + + #[test] + fn x107_x108_x109_x112() { + // Scalene acute 6-7-9: all four present. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(9.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let x107 = fs.iter().find(|f| f.group == "Ψ对称中线垂心" && f.label == "X_{107}").expect("X107"); + let x108 = fs.iter().find(|f| f.group == "Ψ外心垂心" && f.label == "X_{108}").expect("X108"); + let x109 = fs.iter().find(|f| f.group == "Ψ内心外心" && f.label == "X_{109}").expect("X109"); + let x112 = fs.iter().find(|f| f.group == "Ψ垂心对称中线" && f.label == "X_{112}").expect("X112"); + let g = fs.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + assert_ne!(x107.value, x108.value); + assert_ne!(x108.value, x109.value); + assert_ne!(x109.value, x112.value); + assert_ne!(x107.value, g.value); + assert_ne!(x108.value, g.value); + assert_ne!(x109.value, g.value); + assert_ne!(x112.value, g.value); + + // Isosceles non-eq 5-5-6: all four skipped (denoms vanish). + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + assert!(fs2.iter().all(|f| f.group != "Ψ对称中线垂心"), "X107 skip isosceles"); + assert!(fs2.iter().all(|f| f.group != "Ψ外心垂心"), "X108 skip isosceles"); + assert!(fs2.iter().all(|f| f.group != "Ψ内心外心"), "X109 skip isosceles"); + assert!(fs2.iter().all(|f| f.group != "Ψ垂心对称中线"), "X112 skip isosceles"); + + // Right 3-4-5: X107/X108 skip (cos=0 / b²+c²−a²=0); X109/X112 may remain. + let k3 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().all(|f| f.group != "Ψ对称中线垂心"), "X107 skip right"); + assert!(fs3.iter().all(|f| f.group != "Ψ外心垂心"), "X108 skip right"); + assert!(fs3.iter().any(|f| f.group == "Ψ内心外心" && f.label == "X_{109}")); + // sin2B−sin2C = 0 when one angle is π/2 (2A+2B=π ⇒ sin2A=sin2B for complementary pair), so X112 skips. + assert!(fs3.iter().all(|f| f.group != "Ψ垂心对称中线"), "X112 skip right"); + + // Equilateral: all skipped. + let k4 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + assert!(fs4.iter().all(|f| f.group != "Ψ对称中线垂心")); + assert!(fs4.iter().all(|f| f.group != "Ψ外心垂心")); + assert!(fs4.iter().all(|f| f.group != "Ψ内心外心")); + assert!(fs4.iter().all(|f| f.group != "Ψ垂心对称中线")); + } + + + #[test] + fn x102_x103_x116_x117_x118_x119() { + // Scalene acute 6-7-9: all six present; antipodes and nine-point midpoints hold. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(9.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let x100 = fs.iter().find(|f| f.group == "反补费尔巴哈" && f.label == "X_{100}").expect("X100"); + let x101 = fs.iter().find(|f| f.group == "Ψ内心对称中线" && f.label == "X_{101}").expect("X101"); + let x102 = fs.iter().find(|f| f.group == "Λ内心垂心" && f.label == "X_{102}").expect("X102"); + let x103 = fs.iter().find(|f| f.group == "X101对径" && f.label == "X_{103}").expect("X103"); + let x109 = fs.iter().find(|f| f.group == "Ψ内心外心" && f.label == "X_{109}").expect("X109"); + let x116 = fs.iter().find(|f| f.group == "九点X103中点" && f.label == "X_{116}").expect("X116"); + let x117 = fs.iter().find(|f| f.group == "九点X109中点" && f.label == "X_{117}").expect("X117"); + let x118 = fs.iter().find(|f| f.group == "九点X101中点" && f.label == "X_{118}").expect("X118"); + let x119 = fs.iter().find(|f| f.group == "费尔巴哈九点对径" && f.label == "X_{119}").expect("X119"); + let o = fs.iter().find(|f| f.group == "特殊点" && f.label == "O").expect("O"); + let h = fs.iter().find(|f| f.group == "特殊点" && f.label == "H").expect("H"); + let n = fs.iter().find(|f| f.group == "欧拉线" && f.label == "N").expect("N"); + let fe = fs.iter().find(|f| f.group == "费尔巴哈点" && f.label == "Fe").expect("Fe"); + let g = fs.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + assert_ne!(x102.value, g.value); + assert_ne!(x103.value, g.value); + assert!(fs.iter().any(|f| f.group == "Λ内心垂心" && f.label.contains("2O"))); + assert!(fs.iter().any(|f| f.group == "X101对径" && f.label.contains("2O"))); + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + let (ox, oy) = ptxy(o); + let (hx, hy) = ptxy(h); + let (nx, ny) = ptxy(n); + let (a1, b1) = ptxy(x101); + let (a2, b2) = ptxy(x102); + let (a3, b3) = ptxy(x103); + let (a9, b9) = ptxy(x109); + let (a0, b0) = ptxy(x100); + let (fx, fy) = ptxy(fe); + // X102 = 2O - X109; X103 = 2O - X101 + assert!(((a2 + a9) / 2.0 - ox).abs() < 1e-6 && ((b2 + b9) / 2.0 - oy).abs() < 1e-6, "X102 not antipode of X109"); + assert!(((a3 + a1) / 2.0 - ox).abs() < 1e-6 && ((b3 + b1) / 2.0 - oy).abs() < 1e-6, "X103 not antipode of X101"); + // Midpoints with H + let (u6, v6) = ptxy(x116); + let (u7, v7) = ptxy(x117); + let (u8, v8) = ptxy(x118); + let (u9, v9) = ptxy(x119); + assert!(((hx + a3) / 2.0 - u6).abs() < 1e-6 && ((hy + b3) / 2.0 - v6).abs() < 1e-6, "X116 mid(H,X103)"); + assert!(((hx + a9) / 2.0 - u7).abs() < 1e-6 && ((hy + b9) / 2.0 - v7).abs() < 1e-6, "X117 mid(H,X109)"); + assert!(((hx + a1) / 2.0 - u8).abs() < 1e-6 && ((hy + b1) / 2.0 - v8).abs() < 1e-6, "X118 mid(H,X101)"); + assert!(((hx + a0) / 2.0 - u9).abs() < 1e-6 && ((hy + b0) / 2.0 - v9).abs() < 1e-6, "X119 mid(H,X100)"); + // X119 = 2N - Fe + assert!(((u9 + fx) / 2.0 - nx).abs() < 1e-6 && ((v9 + fy) / 2.0 - ny).abs() < 1e-6, "X119 not antipode of Fe on NPC"); + assert!(fs.iter().any(|f| f.group == "费尔巴哈九点对径" && f.label.contains("2N"))); + + // Isosceles non-eq 5-5-6 (a=b): X100/X101/X109 absent → X102/X117/X118/X119 skip; + // X103 bary still finite, so X103/X116 remain via mid(H,X103). + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + assert!(fs2.iter().all(|f| f.group != "Λ内心垂心"), "X102 skip isosceles"); + assert!(fs2.iter().any(|f| f.group == "X101对径" && f.label == "X_{103}"), "X103 on a=b isosceles"); + assert!(fs2.iter().any(|f| f.group == "九点X103中点" && f.label == "X_{116}"), "X116 on a=b isosceles"); + assert!(fs2.iter().all(|f| f.group != "九点X109中点"), "X117 skip isosceles"); + assert!(fs2.iter().all(|f| f.group != "九点X101中点"), "X118 skip isosceles"); + assert!(fs2.iter().all(|f| f.group != "费尔巴哈九点对径"), "X119 skip isosceles"); + + // Equilateral: all skipped. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().all(|f| f.group != "Λ内心垂心")); + assert!(fs3.iter().all(|f| f.group != "X101对径")); + assert!(fs3.iter().all(|f| f.group != "九点X103中点")); + assert!(fs3.iter().all(|f| f.group != "九点X109中点")); + assert!(fs3.iter().all(|f| f.group != "九点X101中点")); + assert!(fs3.iter().all(|f| f.group != "费尔巴哈九点对径")); + } + + + #[test] + fn x113_x114_x115_x125() { + // Scalene acute 6-7-9: all four present; NPC antipodes and H-midpoints hold. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(9.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let x115 = fs.iter().find(|f| f.group == "基伯特双曲心" && f.label == "X_{115}").expect("X115"); + let x114 = fs.iter().find(|f| f.group == "基伯特对径" && f.label == "X_{114}").expect("X114"); + let x125 = fs.iter().find(|f| f.group == "耶拉贝克双曲心" && f.label == "X_{125}").expect("X125"); + let x113 = fs.iter().find(|f| f.group == "耶拉贝克对径" && f.label == "X_{113}").expect("X113"); + let h = fs.iter().find(|f| f.group == "特殊点" && f.label == "H").expect("H"); + let n = fs.iter().find(|f| f.group == "欧拉线" && f.label == "N").expect("N"); + let st = fs.iter().find(|f| f.group == "施泰纳点" && f.label == "St").expect("St"); + let ty = fs.iter().find(|f| f.group == "塔里点" && f.label == "Ty").expect("Ty"); + let x74 = fs.iter().find(|f| f.group == "欧拉无穷等角" && f.label == "X_{74}").expect("X74"); + let er = fs.iter().find(|f| f.group == "欧拉反射点" && f.label == "E_r").expect("Er"); + let g = fs.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + assert_ne!(x115.value, x114.value); + assert_ne!(x125.value, x113.value); + assert_ne!(x115.value, x125.value); + assert_ne!(x115.value, g.value); + assert_ne!(x125.value, g.value); + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + let (hx, hy) = ptxy(h); + let (nx, ny) = ptxy(n); + let (a5, b5) = ptxy(x115); + let (a4, b4) = ptxy(x114); + let (a25, b25) = ptxy(x125); + let (a13, b13) = ptxy(x113); + let (sx, sy) = ptxy(st); + let (tx, tyy) = ptxy(ty); + let (p74x, p74y) = ptxy(x74); + let (ex, ey) = ptxy(er); + // X114 = 2N - X115; X113 = 2N - X125 + assert!(((a4 + a5) / 2.0 - nx).abs() < 1e-6 && ((b4 + b5) / 2.0 - ny).abs() < 1e-6, "X114 not NPC antipode of X115"); + assert!(((a13 + a25) / 2.0 - nx).abs() < 1e-6 && ((b13 + b25) / 2.0 - ny).abs() < 1e-6, "X113 not NPC antipode of X125"); + // Midpoints with H + assert!(((hx + tx) / 2.0 - a5).abs() < 1e-6 && ((hy + tyy) / 2.0 - b5).abs() < 1e-6, "X115 mid(H,Ty)"); + assert!(((hx + sx) / 2.0 - a4).abs() < 1e-6 && ((hy + sy) / 2.0 - b4).abs() < 1e-6, "X114 mid(H,St)"); + assert!(((hx + p74x) / 2.0 - a25).abs() < 1e-6 && ((hy + p74y) / 2.0 - b25).abs() < 1e-6, "X125 mid(H,X74)"); + assert!(((hx + ex) / 2.0 - a13).abs() < 1e-6 && ((hy + ey) / 2.0 - b13).abs() < 1e-6, "X113 mid(H,Er)"); + assert!(fs.iter().any(|f| f.group == "基伯特对径" && f.label.contains("2N"))); + assert!(fs.iter().any(|f| f.group == "耶拉贝克对径" && f.label.contains("2N"))); + assert!(fs.iter().any(|f| f.group == "基伯特双曲心" && f.label.contains("mid"))); + assert!(fs.iter().any(|f| f.group == "耶拉贝克双曲心" && f.label.contains("mid"))); + + // Isosceles non-eq 5-5-6: X115/X125/X114/X113 still via bary (Steiner/Tarry/Er may skip). + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + assert!(fs2.iter().any(|f| f.group == "基伯特双曲心" && f.label == "X_{115}"), "X115 on isosceles"); + assert!(fs2.iter().any(|f| f.group == "基伯特对径" && f.label == "X_{114}"), "X114 on isosceles"); + assert!(fs2.iter().any(|f| f.group == "耶拉贝克双曲心" && f.label == "X_{125}"), "X125 on isosceles"); + assert!(fs2.iter().any(|f| f.group == "耶拉贝克对径" && f.label == "X_{113}"), "X113 on isosceles"); + // Steiner/Tarry skipped on isosceles → no mid(H,St/Ty) cross-facts required. + assert!(fs2.iter().all(|f| f.group != "施泰纳点"), "St skip isosceles"); + assert!(fs2.iter().all(|f| f.group != "塔里点"), "Ty skip isosceles"); + + // Equilateral: all four skipped (bary weights vanish). + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().all(|f| f.group != "基伯特双曲心"), "X115 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "基伯特对径"), "X114 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "耶拉贝克双曲心"), "X125 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "耶拉贝克对径"), "X113 skip equilateral"); + + // Right 3-4-5: X115/X114/X125/X113 present (bary OK); Er may skip. + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + assert!(fs4.iter().any(|f| f.group == "基伯特双曲心" && f.label == "X_{115}")); + assert!(fs4.iter().any(|f| f.group == "基伯特对径" && f.label == "X_{114}")); + assert!(fs4.iter().any(|f| f.group == "耶拉贝克双曲心" && f.label == "X_{125}")); + assert!(fs4.iter().any(|f| f.group == "耶拉贝克对径" && f.label == "X_{113}")); + } + + #[test] + fn x5_x7_x8_x110_x120_x121() { + // Scalene acute 6-7-9: classical + Euler reflection + medial-of NPC points. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(9.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let n = fs.iter().find(|f| f.group == "欧拉线" && f.label == "N").expect("N"); + let x5 = fs.iter().find(|f| f.group == "欧拉线" && f.label == "X_{5}").expect("X5"); + let ge = fs.iter().find(|f| f.group == "格尔巩点" && f.label == "Ge").expect("Ge"); + let x7 = fs.iter().find(|f| f.group == "格尔巩点" && f.label == "X_{7}").expect("X7"); + let na = fs.iter().find(|f| f.group == "奈格尔点" && f.label == "Na").expect("Na"); + let x8 = fs.iter().find(|f| f.group == "奈格尔点" && f.label == "X_{8}").expect("X8"); + let x110 = fs.iter().find(|f| f.group == "欧拉反射点" && f.label == "X_{110}").expect("X110"); + let er = fs.iter().find(|f| f.group == "欧拉反射点" && f.label == "E_r").expect("Er"); + let x74 = fs.iter().find(|f| f.group == "欧拉无穷等角" && f.label == "X_{74}").expect("X74"); + let x113 = fs.iter().find(|f| f.group == "耶拉贝克对径" && f.label == "X_{113}").expect("X113"); + let x120 = fs.iter().find(|f| f.group == "中点X105" && f.label == "X_{120}").expect("X120"); + let x121 = fs.iter().find(|f| f.group == "中点X106" && f.label == "X_{121}").expect("X121"); + let o = fs.iter().find(|f| f.group == "特殊点" && f.label == "O").expect("O"); + let h = fs.iter().find(|f| f.group == "特殊点" && f.label == "H").expect("H"); + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + let (nx, ny) = ptxy(n); + let (a5, b5) = ptxy(x5); + assert!((nx - a5).abs() < 1e-9 && (ny - b5).abs() < 1e-9, "X5 != N"); + let (gx, gy) = ptxy(ge); + let (a7, b7) = ptxy(x7); + assert!((gx - a7).abs() < 1e-9 && (gy - b7).abs() < 1e-9, "X7 != Ge"); + let (nax, nay) = ptxy(na); + let (a8, b8) = ptxy(x8); + assert!((nax - a8).abs() < 1e-9 && (nay - b8).abs() < 1e-9, "X8 != Na"); + let (ox, oy) = ptxy(o); + let (hx, hy) = ptxy(h); + let (e0, e1) = ptxy(x110); + let (rx, ry) = ptxy(er); + let (p74x, p74y) = ptxy(x74); + let (a13, b13) = ptxy(x113); + assert!((e0 - rx).abs() < 1e-5 && (e1 - ry).abs() < 1e-5, "X110 != Er"); + assert!(((e0 + p74x) / 2.0 - ox).abs() < 1e-5 && ((e1 + p74y) / 2.0 - oy).abs() < 1e-5, "X110 not antipode of X74"); + assert!(((hx + e0) / 2.0 - a13).abs() < 1e-5 && ((hy + e1) / 2.0 - b13).abs() < 1e-5, "X113 not mid(H,X110)"); + assert!(fs.iter().any(|f| f.group == "欧拉反射点" && f.label.contains("2O"))); + assert!(fs.iter().any(|f| f.group == "中点X105" && f.label.contains("NPC"))); + assert!(fs.iter().any(|f| f.group == "中点X106" && f.label.contains("NPC"))); + let (p120x, p120y) = ptxy(x120); + let (p121x, p121y) = ptxy(x121); + let r9 = ((ox - hx).hypot(oy - hy)) / 2.0; // unused; check distinct from N/G + let _ = r9; + assert!((p120x - nx).abs() > 1e-4 || (p120y - ny).abs() > 1e-4, "X120 coincides N"); + assert!((p121x - nx).abs() > 1e-4 || (p121y - ny).abs() > 1e-4, "X121 coincides N"); + assert_ne!(x120.value, x121.value); + + // Isosceles 5-5-6: X110 skipped (denom 0); X5/X7/X8/X120/X121 present. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + assert!(fs2.iter().any(|f| f.group == "欧拉线" && f.label == "X_{5}")); + assert!(fs2.iter().any(|f| f.group == "格尔巩点" && f.label == "X_{7}")); + assert!(fs2.iter().any(|f| f.group == "奈格尔点" && f.label == "X_{8}")); + assert!(fs2.iter().all(|f| !(f.group == "欧拉反射点" && f.label == "X_{110}")), "X110 skip isosceles"); + assert!(fs2.iter().any(|f| f.group == "中点X105" && f.label == "X_{120}"), "X120 on isosceles"); + assert!(fs2.iter().any(|f| f.group == "中点X106" && f.label == "X_{121}"), "X121 on isosceles"); + + // Equilateral: X110/X120/X121 skip; X5/X7/X8 remain. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().any(|f| f.group == "欧拉线" && f.label == "X_{5}")); + assert!(fs3.iter().any(|f| f.group == "格尔巩点" && f.label == "X_{7}")); + assert!(fs3.iter().any(|f| f.group == "奈格尔点" && f.label == "X_{8}")); + assert!(fs3.iter().all(|f| !(f.group == "欧拉反射点" && f.label == "X_{110}")), "X110 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "中点X105"), "X120 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "中点X106"), "X121 skip equilateral"); + + // Right 3-4-5: X110 via bary (Er may skip); antipode of X74 holds. + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + let x110r = fs4.iter().find(|f| f.group == "欧拉反射点" && f.label == "X_{110}").expect("X110 on 3-4-5"); + let x74r = fs4.iter().find(|f| f.group == "欧拉无穷等角" && f.label == "X_{74}").expect("X74 on 3-4-5"); + let or = fs4.iter().find(|f| f.group == "特殊点" && f.label == "O").expect("O"); + let (e0, e1) = ptxy(x110r); + let (p74x, p74y) = ptxy(x74r); + let (ox, oy) = ptxy(or); + assert!(((e0 + p74x) / 2.0 - ox).abs() < 1e-5 && ((e1 + p74y) / 2.0 - oy).abs() < 1e-5, "3-4-5 X110 antipode"); + assert!(fs4.iter().any(|f| f.group == "中点X105" && f.label == "X_{120}")); + assert!(fs4.iter().any(|f| f.group == "中点X106" && f.label == "X_{121}")); + } + + #[test] + fn x122_x123_x124_x126_x127() { + // Scalene acute 6-7-9: complements of Ψ/Parry on NPC. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(9.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + let g = fs.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let h = fs.iter().find(|f| f.group == "特殊点" && f.label == "H").expect("H"); + let x107 = fs.iter().find(|f| f.group == "Ψ对称中线垂心" && f.label == "X_{107}").expect("X107"); + let x108 = fs.iter().find(|f| f.group == "Ψ外心垂心" && f.label == "X_{108}").expect("X108"); + let x109 = fs.iter().find(|f| f.group == "Ψ内心外心" && f.label == "X_{109}").expect("X109"); + let x102 = fs.iter().find(|f| f.group == "Λ内心垂心" && f.label == "X_{102}").expect("X102"); + let py = fs.iter().find(|f| f.group == "帕里点" && f.label == "Py").expect("Py"); + let x112 = fs.iter().find(|f| f.group == "Ψ垂心对称中线" && f.label == "X_{112}").expect("X112"); + let x122 = fs.iter().find(|f| f.group == "中点X107" && f.label == "X_{122}").expect("X122"); + let x123 = fs.iter().find(|f| f.group == "中点X108" && f.label == "X_{123}").expect("X123"); + let x124 = fs.iter().find(|f| f.group == "中点X109" && f.label == "X_{124}").expect("X124"); + let x126 = fs.iter().find(|f| f.group == "中点X111" && f.label == "X_{126}").expect("X126"); + let x127 = fs.iter().find(|f| f.group == "中点X112" && f.label == "X_{127}").expect("X127"); + let (gx, gy) = ptxy(g); + let (hx, hy) = ptxy(h); + let check_comp = |parent: &Fact, child: &Fact, tag: &str| { + let (px, py) = ptxy(parent); + let (cx, cy) = ptxy(child); + let qx = (3.0 * gx - px) / 2.0; + let qy = (3.0 * gy - py) / 2.0; + assert!((cx - qx).abs() < 1e-5 && (cy - qy).abs() < 1e-5, "{} not complement", tag); + }; + check_comp(x107, x122, "X122"); + check_comp(x108, x123, "X123"); + check_comp(x109, x124, "X124"); + check_comp(py, x126, "X126"); + check_comp(x112, x127, "X127"); + // X124 = mid(H, X102) + let (p102x, p102y) = ptxy(x102); + let (p124x, p124y) = ptxy(x124); + assert!(((hx + p102x) / 2.0 - p124x).abs() < 1e-5 && ((hy + p102y) / 2.0 - p124y).abs() < 1e-5, "X124 != mid(H,X102)"); + assert!(fs.iter().any(|f| f.group == "中点X107" && f.label.contains("NPC"))); + assert!(fs.iter().any(|f| f.group == "中点X108" && f.label.contains("NPC"))); + assert!(fs.iter().any(|f| f.group == "中点X109" && f.label.contains("NPC"))); + assert!(fs.iter().any(|f| f.group == "中点X111" && f.label.contains("NPC"))); + assert!(fs.iter().any(|f| f.group == "中点X112" && f.label.contains("NPC"))); + assert!(fs.iter().any(|f| f.group == "中点X109" && f.label.contains("mid"))); + + // Isosceles 5-5-6: parents Ψ skipped → children skipped; Parry may remain. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + assert!(fs2.iter().all(|f| f.group != "中点X107"), "X122 skip isosceles"); + assert!(fs2.iter().all(|f| f.group != "中点X108"), "X123 skip isosceles"); + assert!(fs2.iter().all(|f| f.group != "中点X109"), "X124 skip isosceles"); + assert!(fs2.iter().all(|f| f.group != "中点X112"), "X127 skip isosceles"); + // Parry / X126: denom 2a²−b²−c² may be nonzero on 5-5-6. + let _ = fs2.iter().any(|f| f.group == "中点X111"); + + // Equilateral: all skip with parents. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().all(|f| f.group != "中点X107"), "X122 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "中点X108"), "X123 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "中点X109"), "X124 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "中点X111"), "X126 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "中点X112"), "X127 skip equilateral"); + + // Right 3-4-5: X107 skips (b²+c²−a²=0 on hypotenuse); X108 skips (sec of right ∠). + // X109/X112/Parry and their complements remain. + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + let has = |g: &str, lab: &str| fs4.iter().any(|f| f.group == g && f.label == lab); + // Parents that survive 3-4-5: X109 (cos diffs), Parry; X107/X108/X112 typically skip. + assert!(has("中点X109", "X_{124}"), "X124 on 3-4-5"); + assert!(has("中点X111", "X_{126}"), "X126 on 3-4-5"); + assert!(fs4.iter().all(|f| f.group != "中点X107"), "X122 skip right"); + assert!(fs4.iter().all(|f| f.group != "中点X108"), "X123 skip right"); + // X112 / X127: sin2B−sin2C may vanish on right — accept either skip or present. + let _ = has("中点X112", "X_{127}"); + assert!(fs4.iter().any(|f| f.group == "中点X109" && f.label.contains("mid")), "mid(H,X102)"); + } + + #[test] + fn x128_x129_x130_x131_x132_x133_x134_x135_x136_x137_x138_x139() { + // Scalene acute 6-7-8: orthic Steiner/Tarry/X74 and mid(H, Ψ) on NPC. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + let h = fs.iter().find(|f| f.group == "特殊点" && f.label == "H").expect("H"); + let n = fs.iter().find(|f| f.group == "欧拉线" && f.label == "N").expect("N"); + let x112 = fs.iter().find(|f| f.group == "Ψ垂心对称中线" && f.label == "X_{112}").expect("X112"); + let x107 = fs.iter().find(|f| f.group == "Ψ对称中线垂心" && f.label == "X_{107}").expect("X107"); + let x122 = fs.iter().find(|f| f.group == "中点X107" && f.label == "X_{122}").expect("X122"); + let x127 = fs.iter().find(|f| f.group == "中点X112" && f.label == "X_{127}").expect("X127"); + let x132 = fs.iter().find(|f| f.group == "垂心X112中点" && f.label == "X_{132}").expect("X132"); + let x133 = fs.iter().find(|f| f.group == "垂心X107中点" && f.label == "X_{133}").expect("X133"); + let x130 = fs.iter().find(|f| f.group == "垂足施泰纳" && f.label == "X_{130}").expect("X130"); + let x129 = fs.iter().find(|f| f.group == "垂足塔里" && f.label == "X_{129}").expect("X129"); + let x128 = fs.iter().find(|f| f.group == "垂足X74" && f.label == "X_{128}").expect("X128"); + let x137 = fs.iter().find(|f| f.group == "垂足X74对径" && f.label == "X_{137}").expect("X137"); + let x134 = fs.iter().find(|f| f.group == "垂足X107" && f.label == "X_{134}").expect("X134"); + let x135 = fs.iter().find(|f| f.group == "垂足X108" && f.label == "X_{135}").expect("X135"); + let x136 = fs.iter().find(|f| f.group == "垂足X109" && f.label == "X_{136}").expect("X136"); + let x131 = fs.iter().find(|f| f.group == "垂足X102" && f.label == "X_{131}").expect("X131"); + let x138 = fs.iter().find(|f| f.group == "垂足帕里" && f.label == "X_{138}").expect("X138"); + let x139 = fs.iter().find(|f| f.group == "垂足X112" && f.label == "X_{139}").expect("X139"); + let (hx, hy) = ptxy(h); + let (nx, ny) = ptxy(n); + let (p112x, p112y) = ptxy(x112); + let (p107x, p107y) = ptxy(x107); + let (p132x, p132y) = ptxy(x132); + let (p133x, p133y) = ptxy(x133); + assert!(((hx + p112x) / 2.0 - p132x).abs() < 1e-5 && ((hy + p112y) / 2.0 - p132y).abs() < 1e-5, "X132 != mid(H,X112)"); + assert!(((hx + p107x) / 2.0 - p133x).abs() < 1e-5 && ((hy + p107y) / 2.0 - p133y).abs() < 1e-5, "X133 != mid(H,X107)"); + let (p127x, p127y) = ptxy(x127); + let (p122x, p122y) = ptxy(x122); + assert!((2.0 * nx - p127x - p132x).abs() < 1e-5 && (2.0 * ny - p127y - p132y).abs() < 1e-5, "X132 != 2N-X127"); + assert!((2.0 * nx - p122x - p133x).abs() < 1e-5 && (2.0 * ny - p122y - p133y).abs() < 1e-5, "X133 != 2N-X122"); + let (p130x, p130y) = ptxy(x130); + let (p129x, p129y) = ptxy(x129); + let (p128x, p128y) = ptxy(x128); + let (p137x, p137y) = ptxy(x137); + assert!((2.0 * nx - p130x - p129x).abs() < 1e-5 && (2.0 * ny - p130y - p129y).abs() < 1e-5, "X129 != 2N-X130"); + assert!((2.0 * nx - p128x - p137x).abs() < 1e-5 && (2.0 * ny - p128y - p137y).abs() < 1e-5, "X137 != 2N-X128"); + let (p136x, p136y) = ptxy(x136); + let (p131x, p131y) = ptxy(x131); + assert!((2.0 * nx - p136x - p131x).abs() < 1e-5 && (2.0 * ny - p136y - p131y).abs() < 1e-5, "X131 != 2N-X136"); + let _ = (x134, x135, x138, x139); // presence already asserted via expect + assert!(fs.iter().any(|f| f.group == "垂足X107" && f.label.contains("NPC"))); + assert!(fs.iter().any(|f| f.group == "垂足X108" && f.label.contains("NPC"))); + assert!(fs.iter().any(|f| f.group == "垂足X109" && f.label.contains("NPC"))); + assert!(fs.iter().any(|f| f.group == "垂足X102" && f.label.contains("2N"))); + assert!(fs.iter().any(|f| f.group == "垂足帕里" && f.label.contains("NPC"))); + assert!(fs.iter().any(|f| f.group == "垂足X112" && f.label.contains("NPC"))); + assert!(fs.iter().any(|f| f.group == "垂心X112中点" && f.label.contains("NPC"))); + assert!(fs.iter().any(|f| f.group == "垂心X107中点" && f.label.contains("NPC"))); + assert!(fs.iter().any(|f| f.group == "垂足施泰纳" && f.label.contains("NPC"))); + assert!(fs.iter().any(|f| f.group == "垂足X74" && f.label.contains("NPC"))); + assert!(fs.iter().any(|f| f.group == "垂足塔里" && f.label.contains("2N"))); + assert!(fs.iter().any(|f| f.group == "垂足X74对径" && f.label.contains("2N"))); + + // Isosceles 5-5-6: Steiner/Tarry of orthic skip; mid(H,X107) skips with X107; + // mid(H,X112) may skip with X112; X74-of-orthic may remain (orthic isos). + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + assert!(fs2.iter().all(|f| f.group != "垂足施泰纳"), "X130 skip isosceles"); + assert!(fs2.iter().all(|f| f.group != "垂足塔里"), "X129 skip isosceles"); + assert!(fs2.iter().all(|f| f.group != "垂心X107中点"), "X133 skip isosceles"); + assert!(fs2.iter().all(|f| f.group != "垂足X107"), "X134 skip isosceles"); + assert!(fs2.iter().all(|f| f.group != "垂足X108"), "X135 skip isosceles"); + assert!(fs2.iter().all(|f| f.group != "垂足X109"), "X136 skip isosceles"); + assert!(fs2.iter().all(|f| f.group != "垂足X102"), "X131 skip isosceles"); + assert!(fs2.iter().all(|f| f.group != "垂足X112"), "X139 skip isosceles"); + // Parry-of-orthic may survive on some isosceles; accept either. + let _ = fs2.iter().any(|f| f.group == "垂足帕里"); + + // Equilateral: all skip. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().all(|f| f.group != "垂心X112中点"), "X132 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "垂心X107中点"), "X133 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "垂足施泰纳"), "X130 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "垂足塔里"), "X129 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "垂足X74"), "X128 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "垂足X74对径"), "X137 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "垂足X107"), "X134 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "垂足X108"), "X135 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "垂足X109"), "X136 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "垂足X102"), "X131 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "垂足帕里"), "X138 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "垂足X112"), "X139 skip equilateral"); + + // Right 3-4-5: orthic degenerates → X128/129/130/137+ skip; mid parents also skip. + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + assert!(fs4.iter().all(|f| f.group != "垂足施泰纳"), "X130 skip right"); + assert!(fs4.iter().all(|f| f.group != "垂足塔里"), "X129 skip right"); + assert!(fs4.iter().all(|f| f.group != "垂足X74"), "X128 skip right"); + assert!(fs4.iter().all(|f| f.group != "垂足X74对径"), "X137 skip right"); + assert!(fs4.iter().all(|f| f.group != "垂足X107"), "X134 skip right"); + assert!(fs4.iter().all(|f| f.group != "垂足X108"), "X135 skip right"); + assert!(fs4.iter().all(|f| f.group != "垂足X109"), "X136 skip right"); + assert!(fs4.iter().all(|f| f.group != "垂足X102"), "X131 skip right"); + assert!(fs4.iter().all(|f| f.group != "垂足帕里"), "X138 skip right"); + assert!(fs4.iter().all(|f| f.group != "垂足X112"), "X139 skip right"); + assert!(fs4.iter().all(|f| f.group != "垂心X107中点"), "X133 skip right"); + // X112 / X132: may skip on right. + let _ = fs4.iter().any(|f| f.group == "垂心X112中点"); + } + + + #[test] + fn x140_to_x156_complements_reflections_tangential() { + // Scalene acute 6-7-8: all eleven present with geometric cross-checks. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + let o = fs.iter().find(|f| f.group == "特殊点" && f.label == "O").expect("O"); + let n = fs.iter().find(|f| f.group == "欧拉线" && f.label == "N").expect("N"); + let g = fs.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let kpt = fs.iter().find(|f| f.group == "勒莫恩点" && f.label == "K").expect("K"); + let ge = fs.iter().find(|f| f.group == "格尔巩点" && f.label == "Ge").expect("Ge"); + let mi = fs.iter().find(|f| f.group == "米滕点" && f.label == "Mi").expect("Mi"); + let na = fs.iter().find(|f| f.group == "奈格尔点" && f.label == "Na").expect("Na"); + let x52 = fs.iter().find(|f| f.group == "垂足垂心" && f.label == "X_{52}").expect("X52"); + let x140 = fs.iter().find(|f| f.group == "九点补点" && f.label == "X_{140}").expect("X140"); + let x141 = fs.iter().find(|f| f.group == "对称中线补点" && f.label == "X_{141}").expect("X141"); + let x142 = fs.iter().find(|f| f.group == "米滕补点" && f.label == "X_{142}").expect("X142"); + let x143 = fs.iter().find(|f| f.group == "垂足九点心" && f.label == "X_{143}").expect("X143"); + let x144 = fs.iter().find(|f| f.group == "格尔巩反补" && f.label == "X_{144}").expect("X144"); + let x145 = fs.iter().find(|f| f.group == "奈格尔反补" && f.label == "X_{145}").expect("X145"); + let x146 = fs.iter().find(|f| f.group == "德龙尚X110反射" && f.label == "X_{146}").expect("X146"); + let x147 = fs.iter().find(|f| f.group == "德龙尚施泰纳反射" && f.label == "X_{147}").expect("X147"); + let x148 = fs.iter().find(|f| f.group == "德龙尚塔里反射" && f.label == "X_{148}").expect("X148"); + let x149 = fs.iter().find(|f| f.group == "德龙尚X104反射" && f.label == "X_{149}").expect("X149"); + let x150 = fs.iter().find(|f| f.group == "德龙尚X103反射" && f.label == "X_{150}").expect("X150"); + let (ox, oy) = ptxy(o); + let (nx, ny) = ptxy(n); + let (gx, gy) = ptxy(g); + let (p140x, p140y) = ptxy(x140); + assert!(((ox + nx) / 2.0 - p140x).abs() < 1e-5 && ((oy + ny) / 2.0 - p140y).abs() < 1e-5, "X140 != mid(O,N)"); + assert!(fs.iter().any(|f| f.group == "九点补点" && f.label.contains("Euler"))); + let (kx, ky) = ptxy(kpt); + let (p141x, p141y) = ptxy(x141); + assert!(((3.0 * gx - kx) / 2.0 - p141x).abs() < 1e-5 && ((3.0 * gy - ky) / 2.0 - p141y).abs() < 1e-5, "X141 != complement(K)"); + let (gex, gey) = ptxy(ge); + let (mx, my) = ptxy(mi); + let (p142x, p142y) = ptxy(x142); + assert!(((gex + mx) / 2.0 - p142x).abs() < 1e-5 && ((gey + my) / 2.0 - p142y).abs() < 1e-5, "X142 != mid(Ge,Mi)"); + let (s52x, s52y) = ptxy(x52); + let (p143x, p143y) = ptxy(x143); + assert!(((nx + s52x) / 2.0 - p143x).abs() < 1e-5 && ((ny + s52y) / 2.0 - p143y).abs() < 1e-5, "X143 != mid(N,X52)"); + let (p144x, p144y) = ptxy(x144); + assert!((3.0 * gx - 2.0 * gex - p144x).abs() < 1e-5 && (3.0 * gy - 2.0 * gey - p144y).abs() < 1e-5, "X144 != anticomplement(Ge)"); + let (nax, nay) = ptxy(na); + let (p145x, p145y) = ptxy(x145); + assert!((3.0 * gx - 2.0 * nax - p145x).abs() < 1e-5 && (3.0 * gy - 2.0 * nay - p145y).abs() < 1e-5, "X145 != anticomplement(Na)"); + assert!(fs.iter().any(|f| f.group == "奈格尔反补" && f.label.contains("3a"))); + let l = fs.iter().find(|f| f.group == "德龙尚点" && f.label == "L").expect("L"); + let x110 = fs.iter().find(|f| f.group == "欧拉反射点" && f.label == "X_{110}").expect("X110"); + let st = fs.iter().find(|f| f.group == "施泰纳点" && f.label == "St").expect("St"); + let ty = fs.iter().find(|f| f.group == "塔里点" && f.label == "Ty").expect("Ty"); + let x104 = fs.iter().find(|f| f.group == "X100对径" && f.label == "X_{104}").expect("X104"); + let x103 = fs.iter().find(|f| f.group == "X101对径" && f.label == "X_{103}").expect("X103"); + let (lx, ly) = ptxy(l); + let check_ref = |q: &Fact, p: &Fact, name: &str| { + let (qx, qy) = ptxy(q); + let (px, py) = ptxy(p); + assert!((2.0 * qx - lx - px).abs() < 1e-5 && (2.0 * qy - ly - py).abs() < 1e-5, "{name} != 2Q-L"); + }; + check_ref(x110, x146, "X146"); + check_ref(st, x147, "X147"); + check_ref(ty, x148, "X148"); + check_ref(x104, x149, "X149"); + check_ref(x103, x150, "X150"); + let x109 = fs.iter().find(|f| f.group == "Ψ内心外心" && f.label == "X_{109}").expect("X109"); + let x101 = fs.iter().find(|f| f.group == "Ψ内心对称中线" && f.label == "X_{101}").expect("X101"); + let x100 = fs.iter().find(|f| f.group == "反补费尔巴哈" && f.label == "X_{100}").expect("X100"); + let x151 = fs.iter().find(|f| f.group == "德龙尚X109反射" && f.label == "X_{151}").expect("X151"); + let x152 = fs.iter().find(|f| f.group == "德龙尚X101反射" && f.label == "X_{152}").expect("X152"); + let x153 = fs.iter().find(|f| f.group == "德龙尚X100反射" && f.label == "X_{153}").expect("X153"); + check_ref(x109, x151, "X151"); + check_ref(x101, x152, "X152"); + check_ref(x100, x153, "X153"); + let x154 = fs.iter().find(|f| f.group == "切线重心" && f.label == "X_{154}").expect("X154"); + let x155 = fs.iter().find(|f| f.group == "垂足特征心" && f.label == "X_{155}").expect("X155"); + let x156 = fs.iter().find(|f| f.group == "切线九点心" && f.label == "X_{156}").expect("X156"); + let x26 = fs.iter().find(|f| f.group == "切线三角形外心" && f.label == "X_{26}").expect("X26"); + let x68 = fs.iter().find(|f| f.group == "普拉索洛夫点" && f.label == "X_{68}").expect("X68"); + let (p26x, p26y) = ptxy(x26); + let (p155x, p155y) = ptxy(x155); + let (p156x, p156y) = ptxy(x156); + assert!(((p26x + p155x) / 2.0 - p156x).abs() < 1e-5 && ((p26y + p155y) / 2.0 - p156y).abs() < 1e-5, "X156 != mid(X26,X155)"); + let (p68x, p68y) = ptxy(x68); + assert!((2.0 * nx - p68x - p155x).abs() < 1e-5 && (2.0 * ny - p68y - p155y).abs() < 1e-5, "X155 != 2N-X68"); + assert!(fs.iter().any(|f| f.group == "垂足特征心" && f.label.contains("2N"))); + let _ = x154; // presence is the check; bary finite on acute scalene + + // Isosceles 5-5-6: X140–X145 remain; Steiner/Tarry/X110/X104 family skip → reflections skip. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + assert!(fs2.iter().any(|f| f.group == "九点补点" && f.label == "X_{140}")); + assert!(fs2.iter().any(|f| f.group == "对称中线补点" && f.label == "X_{141}")); + assert!(fs2.iter().any(|f| f.group == "米滕补点" && f.label == "X_{142}")); + assert!(fs2.iter().any(|f| f.group == "格尔巩反补" && f.label == "X_{144}")); + assert!(fs2.iter().any(|f| f.group == "奈格尔反补" && f.label == "X_{145}")); + // X143 needs X52; isos acute non-eq usually has X52. + let _ = fs2.iter().any(|f| f.group == "垂足九点心"); + assert!(fs2.iter().all(|f| f.group != "德龙尚X110反射"), "X146 skip isosceles"); + assert!(fs2.iter().all(|f| f.group != "德龙尚施泰纳反射"), "X147 skip isosceles"); + assert!(fs2.iter().all(|f| f.group != "德龙尚塔里反射"), "X148 skip isosceles"); + // X104/X103 may survive on a=b isos; accept either for X149/X150. + let _ = ( + fs2.iter().any(|f| f.group == "德龙尚X104反射"), + fs2.iter().any(|f| f.group == "德龙尚X103反射"), + ); + // X100/X101/X109 skip on isos → X151–X153 skip; X154/X155 still finite. + assert!(fs2.iter().all(|f| f.group != "德龙尚X109反射"), "X151 skip isosceles"); + assert!(fs2.iter().all(|f| f.group != "德龙尚X101反射"), "X152 skip isosceles"); + assert!(fs2.iter().all(|f| f.group != "德龙尚X100反射"), "X153 skip isosceles"); + assert!(fs2.iter().any(|f| f.group == "切线重心" && f.label == "X_{154}")); + assert!(fs2.iter().any(|f| f.group == "垂足特征心" && f.label == "X_{155}")); + // X156 needs X26; may be present on isos acute. + let _ = fs2.iter().any(|f| f.group == "切线九点心"); + + // Equilateral: X140–X145 collapse toward G/O; reflections of circumcircle points skip. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().any(|f| f.group == "九点补点" && f.label == "X_{140}")); + assert!(fs3.iter().any(|f| f.group == "对称中线补点" && f.label == "X_{141}")); + assert!(fs3.iter().any(|f| f.group == "米滕补点" && f.label == "X_{142}")); + assert!(fs3.iter().any(|f| f.group == "格尔巩反补" && f.label == "X_{144}")); + assert!(fs3.iter().any(|f| f.group == "奈格尔反补" && f.label == "X_{145}")); + assert!(fs3.iter().all(|f| f.group != "德龙尚X110反射"), "X146 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "德龙尚施泰纳反射"), "X147 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "德龙尚塔里反射"), "X148 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "德龙尚X104反射"), "X149 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "德龙尚X103反射"), "X150 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "德龙尚X109反射"), "X151 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "德龙尚X101反射"), "X152 skip equilateral"); + assert!(fs3.iter().all(|f| f.group != "德龙尚X100反射"), "X153 skip equilateral"); + // X154/X155 bary finite on equilateral (→ G); X26 may skip on equilateral. + assert!(fs3.iter().any(|f| f.group == "切线重心" && f.label == "X_{154}")); + assert!(fs3.iter().any(|f| f.group == "垂足特征心" && f.label == "X_{155}")); + + // Right 3-4-5: X140/141/142/144/145; X143 skips (no X52); X110 may via bary. + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + assert!(fs4.iter().any(|f| f.group == "九点补点" && f.label == "X_{140}")); + assert!(fs4.iter().any(|f| f.group == "对称中线补点" && f.label == "X_{141}")); + assert!(fs4.iter().any(|f| f.group == "米滕补点" && f.label == "X_{142}")); + assert!(fs4.iter().any(|f| f.group == "格尔巩反补" && f.label == "X_{144}")); + assert!(fs4.iter().any(|f| f.group == "奈格尔反补" && f.label == "X_{145}")); + assert!(fs4.iter().all(|f| f.group != "垂足九点心"), "X143 skip right"); + // X110 bary works on right scalene; Steiner/Tarry may too — presence optional for reflections. + let _ = ( + fs4.iter().any(|f| f.group == "德龙尚X110反射"), + fs4.iter().any(|f| f.group == "德龙尚施泰纳反射"), + fs4.iter().any(|f| f.group == "德龙尚塔里反射"), + fs4.iter().any(|f| f.group == "德龙尚X109反射"), + fs4.iter().any(|f| f.group == "德龙尚X101反射"), + fs4.iter().any(|f| f.group == "德龙尚X100反射"), + ); + assert!(fs4.iter().all(|f| f.group != "切线重心"), "X154 skip right"); + assert!(fs4.iter().all(|f| f.group != "垂足特征心"), "X155 skip right"); + assert!(fs4.iter().all(|f| f.group != "切线九点心"), "X156 skip right"); + } + + #[test] + fn x157_to_x165_tangential_excentral() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let x157 = fs.iter().find(|f| f.group == "切线对称中线" && f.label == "X_{157}").expect("X157"); + let x158 = fs.iter().find(|f| f.group == "正割正切点" && f.label == "X_{158}").expect("X158"); + let x159 = fs.iter().find(|f| f.group == "切线米滕" && f.label == "X_{159}").expect("X159"); + let x163 = fs.iter().find(|f| f.group == "对称中线X110积" && f.label == "X_{163}").expect("X163"); + let x164 = fs.iter().find(|f| f.group == "旁心内心" && f.label == "X_{164}").expect("X164"); + let x165 = fs.iter().find(|f| f.group == "旁心重心" && f.label == "X_{165}").expect("X165"); + let x66 = fs.iter().find(|f| f.group == "埃克塞特等角" && f.label == "X_{66}").expect("X66"); + let x141 = fs.iter().find(|f| f.group == "对称中线补点" && f.label == "X_{141}").expect("X141"); + let (p66x, p66y) = ptxy(x66); + let (p141x, p141y) = ptxy(x141); + let (p159x, p159y) = ptxy(x159); + assert!((2.0 * p141x - p66x - p159x).abs() < 1e-5 && (2.0 * p141y - p66y - p159y).abs() < 1e-5, "X159 != 2·X141−X66"); + assert!(fs.iter().any(|f| f.group == "旁心重心" && f.label.contains("cent"))); + assert!(fs.iter().any(|f| f.group == "旁心重心" && f.label.contains("OI"))); + let i = fs.iter().find(|f| f.group == "特殊点" && f.label == "I").expect("I"); + let o = fs.iter().find(|f| f.group == "特殊点" && f.label == "O").expect("O"); + let (ix, iy) = ptxy(i); + let (ox, oy) = ptxy(o); + let (p165x, p165y) = ptxy(x165); + assert!(((4.0 * ox - ix) / 3.0 - p165x).abs() < 1e-5 && ((4.0 * oy - iy) / 3.0 - p165y).abs() < 1e-5, "X165 != (4O−I)/3"); + let _ = (x157, x158, x163, x164); + + // Isosceles 5-5-6: X163 skips; X157/158/164/165 remain; X159 if X66+X141. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + assert!(fs2.iter().all(|f| f.group != "对称中线X110积"), "X163 skip isosceles"); + assert!(fs2.iter().any(|f| f.group == "切线对称中线" && f.label == "X_{157}")); + assert!(fs2.iter().any(|f| f.group == "正割正切点" && f.label == "X_{158}")); + assert!(fs2.iter().any(|f| f.group == "旁心内心" && f.label == "X_{164}")); + assert!(fs2.iter().any(|f| f.group == "旁心重心" && f.label == "X_{165}")); + let _ = fs2.iter().any(|f| f.group == "切线米滕"); + + // Equilateral: X163 skips; others finite; X165 = G. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().all(|f| f.group != "对称中线X110积"), "X163 skip equilateral"); + assert!(fs3.iter().any(|f| f.group == "切线对称中线" && f.label == "X_{157}")); + assert!(fs3.iter().any(|f| f.group == "正割正切点" && f.label == "X_{158}")); + assert!(fs3.iter().any(|f| f.group == "旁心内心" && f.label == "X_{164}")); + assert!(fs3.iter().any(|f| f.group == "旁心重心" && f.label == "X_{165}")); + let g3 = fs3.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let x165e = fs3.iter().find(|f| f.group == "旁心重心" && f.label == "X_{165}").expect("X165e"); + let (gx, gy) = ptxy(g3); + let (ex, ey) = ptxy(x165e); + assert!((gx - ex).abs() < 1e-5 && (gy - ey).abs() < 1e-5, "equilateral X165 != G"); + + // Right 3-4-5: X158 skips; X163/164/165 ok; X157/X159 optional via parents. + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + assert!(fs4.iter().all(|f| f.group != "正割正切点"), "X158 skip right"); + assert!(fs4.iter().any(|f| f.group == "旁心内心" && f.label == "X_{164}")); + assert!(fs4.iter().any(|f| f.group == "旁心重心" && f.label == "X_{165}")); + assert!(fs4.iter().any(|f| f.group == "对称中线X110积" && f.label == "X_{163}")); + let _ = ( + fs4.iter().any(|f| f.group == "切线对称中线"), + fs4.iter().any(|f| f.group == "切线米滕"), + ); + } + + #[test] + fn x160_to_x173_tangential_gaps_classical() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8: all centers finite. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let x160 = fs.iter().find(|f| f.group == "切线格林伯格" && f.label == "X_{160}").expect("X160"); + let x161 = fs.iter().find(|f| f.group == "切线X63" && f.label == "X_{161}").expect("X161"); + let x162 = fs.iter().find(|f| f.group == "X108X109截点" && f.label == "X_{162}").expect("X162"); + let x168 = fs.iter().find(|f| f.group == "旁心米滕" && f.label == "X_{168}").expect("X168"); + let x169 = fs.iter().find(|f| f.group == "X85截共轭I" && f.label == "X_{169}").expect("X169"); + let x171 = fs.iter().find(|f| f.group == "调和X238" && f.label == "X_{171}").expect("X171"); + let x172 = fs.iter().find(|f| f.group == "对称中线X171积" && f.label == "X_{172}").expect("X172"); + let x173 = fs.iter().find(|f| f.group == "等腰等化器" && f.label == "X_{173}").expect("X173"); + assert!(fs.iter().any(|f| f.group == "调和X238" && f.label.contains("OI"))); + let i = fs.iter().find(|f| f.group == "特殊点" && f.label == "I").expect("I"); + let o = fs.iter().find(|f| f.group == "特殊点" && f.label == "O").expect("O"); + let (ix, iy) = ptxy(i); + let (ox, oy) = ptxy(o); + let (p171x, p171y) = ptxy(x171); + let cross = (ix - ox) * (p171y - oy) - (iy - oy) * (p171x - ox); + assert!(cross.abs() < 1e-5, "X171 not on OI: cross={}", cross); + // Numeric spot-check vs ETC side formula for X162 / X171. + let (p162x, p162y) = ptxy(x162); + assert!((p162x - 3.4398888116747757).abs() < 1e-6 && (p162y - 5.032994341492612).abs() < 1e-6, "X162 xy {:?}", (p162x, p162y)); + let (p171nx, p171ny) = ptxy(x171); + assert!((p171nx - 4.575757575757576).abs() < 1e-6 && (p171ny - 2.073415326757506).abs() < 1e-6, "X171 xy {:?}", (p171nx, p171ny)); + let _ = (x160, x161, x168, x169, x172, x173); + + // Isosceles 5-5-6: X162 skips; others remain. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + assert!(fs2.iter().all(|f| f.group != "X108X109截点"), "X162 skip isosceles"); + assert!(fs2.iter().any(|f| f.group == "切线格林伯格" && f.label == "X_{160}")); + assert!(fs2.iter().any(|f| f.group == "切线X63" && f.label == "X_{161}")); + assert!(fs2.iter().any(|f| f.group == "旁心米滕" && f.label == "X_{168}")); + assert!(fs2.iter().any(|f| f.group == "X85截共轭I" && f.label == "X_{169}")); + assert!(fs2.iter().any(|f| f.group == "调和X238" && f.label == "X_{171}")); + assert!(fs2.iter().any(|f| f.group == "对称中线X171积" && f.label == "X_{172}")); + assert!(fs2.iter().any(|f| f.group == "等腰等化器" && f.label == "X_{173}")); + + // Equilateral: X162 skips; X160/161/168/169/171/172/173 = G. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().all(|f| f.group != "X108X109截点"), "X162 skip equilateral"); + let g3 = fs3.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let (gx, gy) = ptxy(g3); + for (grp, lab) in [ + ("切线格林伯格", "X_{160}"), + ("切线X63", "X_{161}"), + ("旁心米滕", "X_{168}"), + ("X85截共轭I", "X_{169}"), + ("调和X238", "X_{171}"), + ("对称中线X171积", "X_{172}"), + ("等腰等化器", "X_{173}"), + ] { + let f = fs3.iter().find(|f| f.group == grp && f.label == lab).unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!((x - gx).abs() < 1e-5 && (y - gy).abs() < 1e-5, "equilateral {} != G", lab); + } + + // Right 3-4-5: X161/X162 skip; X160/168/169/171–173 finite. + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + assert!(fs4.iter().any(|f| f.group == "切线格林伯格" && f.label == "X_{160}")); + assert!(fs4.iter().all(|f| f.group != "切线X63"), "X161 skip right"); + assert!(fs4.iter().all(|f| f.group != "X108X109截点"), "X162 skip right"); + assert!(fs4.iter().any(|f| f.group == "旁心米滕" && f.label == "X_{168}")); + assert!(fs4.iter().any(|f| f.group == "X85截共轭I" && f.label == "X_{169}")); + assert!(fs4.iter().any(|f| f.group == "调和X238" && f.label == "X_{171}")); + assert!(fs4.iter().any(|f| f.group == "对称中线X171积" && f.label == "X_{172}")); + assert!(fs4.iter().any(|f| f.group == "等腰等化器" && f.label == "X_{173}")); + } + + #[test] + fn x166_x167_x170_gaps_and_x177_to_x180() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let x166 = fs.iter().find(|f| f.group == "旁心格尔巩" && f.label == "X_{166}").expect("X166"); + let x167 = fs.iter().find(|f| f.group == "旁心奈格尔" && f.label == "X_{167}").expect("X167"); + let x170 = fs.iter().find(|f| f.group == "米滕阿列夫自共轭" && f.label == "X_{170}").expect("X170"); + let x177 = fs.iter().find(|f| f.group == "第一中弧点" && f.label == "X_{177}").expect("X177"); + let x178 = fs.iter().find(|f| f.group == "第二中弧点" && f.label == "X_{178}").expect("X178"); + let x179 = fs.iter().find(|f| f.group == "第一阿吉马-马尔法蒂" && f.label == "X_{179}").expect("X179"); + let x180 = fs.iter().find(|f| f.group == "第二阿吉马-马尔法蒂" && f.label == "X_{180}").expect("X180"); + assert!(fs.iter().any(|f| f.group == "旁心格尔巩" && f.label.contains("165"))); + assert!(fs.iter().any(|f| f.group == "旁心奈格尔" && f.label.contains("164"))); + assert!(fs.iter().any(|f| f.group == "米滕阿列夫自共轭" && f.label.contains("IG"))); + let (p166x, p166y) = ptxy(x166); + assert!((p166x - 4.116074059198164).abs() < 1e-6 && (p166y - 1.3875764592887037).abs() < 1e-6, "X166 {:?}", (p166x, p166y)); + let (p167x, p167y) = ptxy(x167); + assert!((p167x - 3.586801781111747).abs() < 1e-6 && (p167y - 0.024620487649944494).abs() < 1e-6, "X167 {:?}", (p167x, p167y)); + let (p170x, p170y) = ptxy(x170); + assert!((p170x - 3.612326337570204).abs() < 1e-6 && (p170y + 0.017134392772165687).abs() < 1e-6, "X170 {:?}", (p170x, p170y)); + let (p177x, p177y) = ptxy(x177); + assert!((p177x - 4.607015416275967).abs() < 1e-6 && (p177y - 2.1605390776490716).abs() < 1e-6, "X177 {:?}", (p177x, p177y)); + let (p178x, p178y) = ptxy(x178); + assert!((p178x - 4.315677276217948).abs() < 1e-6 && (p178y - 1.7640727698612098).abs() < 1e-6, "X178 {:?}", (p178x, p178y)); + let (p179x, p179y) = ptxy(x179); + assert!((p179x - 4.567591551964102).abs() < 1e-6 && (p179y - 2.0737600520097113).abs() < 1e-6, "X179 {:?}", (p179x, p179y)); + let (p180x, p180y) = ptxy(x180); + assert!((p180x - 4.591012676125583).abs() < 1e-6 && (p180y - 2.1107123730916184).abs() < 1e-6, "X180 {:?}", (p180x, p180y)); + // IG collinearity for X170. + let i = fs.iter().find(|f| f.group == "特殊点" && f.label == "I").expect("I"); + let ge = fs.iter().find(|f| f.label == "X_{7}" || f.label == "Ge").expect("Ge"); + let (ix, iy) = ptxy(i); + let (gx, gy) = ptxy(ge); + let cross = (gx - ix) * (p170y - iy) - (gy - iy) * (p170x - ix); + assert!(cross.abs() < 1e-5, "X170 not on IG: cross={}", cross); + + // Isosceles 5-5-6: all finite. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + for (grp, lab) in [ + ("旁心格尔巩", "X_{166}"), + ("旁心奈格尔", "X_{167}"), + ("米滕阿列夫自共轭", "X_{170}"), + ("第一中弧点", "X_{177}"), + ("第二中弧点", "X_{178}"), + ("第一阿吉马-马尔法蒂", "X_{179}"), + ("第二阿吉马-马尔法蒂", "X_{180}"), + ] { + assert!(fs2.iter().any(|f| f.group == grp && f.label == lab), "isos missing {} {}", grp, lab); + } + + // Equilateral: all coincide with G. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + let g3 = fs3.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let (gx, gy) = ptxy(g3); + for (grp, lab) in [ + ("旁心格尔巩", "X_{166}"), + ("旁心奈格尔", "X_{167}"), + ("米滕阿列夫自共轭", "X_{170}"), + ("第一中弧点", "X_{177}"), + ("第二中弧点", "X_{178}"), + ("第一阿吉马-马尔法蒂", "X_{179}"), + ("第二阿吉马-马尔法蒂", "X_{180}"), + ] { + let f = fs3.iter().find(|f| f.group == grp && f.label == lab).unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!((x - gx).abs() < 1e-5 && (y - gy).abs() < 1e-5, "equilateral {} != G", lab); + } + + // Right 3-4-5: all finite (half-angle formulas stay defined). + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + assert!(fs4.iter().any(|f| f.group == "旁心格尔巩" && f.label == "X_{166}")); + assert!(fs4.iter().any(|f| f.group == "旁心奈格尔" && f.label == "X_{167}")); + assert!(fs4.iter().any(|f| f.group == "米滕阿列夫自共轭" && f.label == "X_{170}")); + assert!(fs4.iter().any(|f| f.group == "第一中弧点" && f.label == "X_{177}")); + assert!(fs4.iter().any(|f| f.group == "第二中弧点" && f.label == "X_{178}")); + assert!(fs4.iter().any(|f| f.group == "第一阿吉马-马尔法蒂" && f.label == "X_{179}")); + assert!(fs4.iter().any(|f| f.group == "第二阿吉马-马尔法蒂" && f.label == "X_{180}")); + } + + #[test] + fn x182_to_x190_brocard_orthic_inverses() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let x182 = fs.iter().find(|f| f.group == "布罗卡直径中点" && f.label == "X_{182}").expect("X182"); + let x183 = fs.iter().find(|f| f.group == "摩西布罗卡商" && f.label == "X_{183}").expect("X183"); + let x184 = fs.iter().find(|f| f.group == "准外心" && f.label == "X_{184}").expect("X184"); + let x185 = fs.iter().find(|f| f.group == "垂足奈格尔" && f.label == "X_{185}").expect("X185"); + let x186 = fs.iter().find(|f| f.group == "反演垂心" && f.label == "X_{186}").expect("X186"); + let x187 = fs.iter().find(|f| f.group == "肖特中心" && f.label == "X_{187}").expect("X187"); + let x188 = fs.iter().find(|f| f.group == "反补第二中弧" && f.label == "X_{188}").expect("X188"); + let x189 = fs.iter().find(|f| f.group == "奈格尔圆塞瓦共轭" && f.label == "X_{189}").expect("X189"); + let x190 = fs.iter().find(|f| f.group == "伊夫抛物点" && f.label == "X_{190}").expect("X190"); + assert!(fs.iter().any(|f| f.group == "布罗卡直径中点" && f.label.contains("OK"))); + assert!(fs.iter().any(|f| f.group == "反演垂心" && f.label.contains("OH"))); + assert!(fs.iter().any(|f| f.group == "肖特中心" && f.label.contains("OK"))); + let (p182x, p182y) = ptxy(x182); + assert!((p182x - 4.348993288590604).abs() < 1e-6 && (p182y - 1.6081112059420504).abs() < 1e-6, "X182 {:?}", (p182x, p182y)); + let (p183x, p183y) = ptxy(x183); + assert!((p183x - 3.819745041145412).abs() < 1e-6 && (p183y - 1.1780332020797852).abs() < 1e-6, "X183 {:?}", (p183x, p183y)); + let (p184x, p184y) = ptxy(x184); + assert!((p184x - 4.52074391988555).abs() < 1e-6 && (p184y - 1.41843166899728).abs() < 1e-6, "X184 {:?}", (p184x, p184y)); + let (p185x, p185y) = ptxy(x185); + assert!((p185x - 4.7109375).abs() < 1e-6 && (p185y - 1.5592792326137148).abs() < 1e-6, "X185 {:?}", (p185x, p185y)); + let (p186x, p186y) = ptxy(x186); + assert!((p186x - 7.014492753623188).abs() < 1e-6 && (p186y - 8.397076936125066).abs() < 1e-6, "X186 {:?}", (p186x, p186y)); + let (p187x, p187y) = ptxy(x187); + assert!((p187x - 10.577249575551782).abs() < 1e-6 && (p187y - 11.875395455433948).abs() < 1e-6, "X187 {:?}", (p187x, p187y)); + let (p188x, p188y) = ptxy(x188); + assert!((p188x - 4.181145447564104).abs() < 1e-6 && (p188y - 1.5551451021748157).abs() < 1e-6, "X188 {:?}", (p188x, p188y)); + let (p189x, p189y) = ptxy(x189); + assert!((p189x - 4.8141694490818026).abs() < 1e-6 && (p189y - 4.675948486953883).abs() < 1e-6, "X189 {:?}", (p189x, p189y)); + let (p190x, p190y) = ptxy(x190); + assert!((p190x - 0.5416666666666666).abs() < 1e-6 && (p190y - 3.38886042793149).abs() < 1e-6, "X190 {:?}", (p190x, p190y)); + // mid(O,K) for X182 + let o = fs.iter().find(|f| f.group == "特殊点" && f.label == "O").or_else(|| fs.iter().find(|f| f.label == "O")).expect("O"); + let kk = fs.iter().find(|f| f.group == "勒莫恩点" && f.label == "K").expect("K"); + let (ox, oy) = ptxy(o); + let (kx, ky) = ptxy(kk); + assert!((p182x - (ox + kx) / 2.0).abs() < 1e-6 && (p182y - (oy + ky) / 2.0).abs() < 1e-6); + + // Isosceles 5-5-6: X190 skips; others finite. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + for (grp, lab) in [ + ("布罗卡直径中点", "X_{182}"), + ("摩西布罗卡商", "X_{183}"), + ("准外心", "X_{184}"), + ("垂足奈格尔", "X_{185}"), + ("反演垂心", "X_{186}"), + ("肖特中心", "X_{187}"), + ("反补第二中弧", "X_{188}"), + ("奈格尔圆塞瓦共轭", "X_{189}"), + ] { + assert!(fs2.iter().any(|f| f.group == grp && f.label == lab), "isos missing {} {}", grp, lab); + } + assert!(fs2.iter().all(|f| f.group != "伊夫抛物点"), "X190 skip isos"); + + // Equilateral: X182/183/184/185/188/189 → G; X186/187/190 skip. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + let g3 = fs3.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let (gx, gy) = ptxy(g3); + for (grp, lab) in [ + ("布罗卡直径中点", "X_{182}"), + ("摩西布罗卡商", "X_{183}"), + ("准外心", "X_{184}"), + ("垂足奈格尔", "X_{185}"), + ("反补第二中弧", "X_{188}"), + ("奈格尔圆塞瓦共轭", "X_{189}"), + ] { + let f = fs3.iter().find(|f| f.group == grp && f.label == lab).unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!((x - gx).abs() < 1e-5 && (y - gy).abs() < 1e-5, "equilateral {} != G", lab); + } + assert!(fs3.iter().all(|f| f.group != "反演垂心"), "X186 skip equil"); + assert!(fs3.iter().all(|f| f.group != "肖特中心"), "X187 skip equil"); + assert!(fs3.iter().all(|f| f.group != "伊夫抛物点"), "X190 skip equil"); + + // Right 3-4-5: X185 skips; X186 = H (on circumcircle); others finite; X190 ok. + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + assert!(fs4.iter().any(|f| f.group == "布罗卡直径中点" && f.label == "X_{182}")); + assert!(fs4.iter().any(|f| f.group == "摩西布罗卡商" && f.label == "X_{183}")); + assert!(fs4.iter().any(|f| f.group == "准外心" && f.label == "X_{184}")); + assert!(fs4.iter().all(|f| f.group != "垂足奈格尔"), "X185 skip right"); + let x186r = fs4.iter().find(|f| f.group == "反演垂心" && f.label == "X_{186}").expect("X186 right"); + let h4 = fs4.iter().find(|f| f.group == "特殊点" && f.label == "H").or_else(|| fs4.iter().find(|f| f.label == "H")).expect("H"); + let (hx, hy) = ptxy(h4); + let (x6, y6) = ptxy(x186r); + assert!((x6 - hx).abs() < 1e-5 && (y6 - hy).abs() < 1e-5, "right X186 should = H"); + assert!(fs4.iter().any(|f| f.group == "肖特中心" && f.label == "X_{187}")); + assert!(fs4.iter().any(|f| f.group == "反补第二中弧" && f.label == "X_{188}")); + assert!(fs4.iter().any(|f| f.group == "奈格尔圆塞瓦共轭" && f.label == "X_{189}")); + assert!(fs4.iter().any(|f| f.group == "伊夫抛物点" && f.label == "X_{190}")); + } + + #[test] + fn x191_to_x200_ceva_conjugates() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let want = [ + ("斯皮克内心塞瓦", "X_{191}", 3.9126984126984126, 1.1373046334101145), + ("内心重心塞瓦", "X_{192}", 4.783390410958904, 2.1586576698467708), + ("垂心重心塞瓦", "X_{193}", 5.979446308724833, 3.6504167696845915), + ("勒莫恩重心塞瓦", "X_{194}", 5.3385272071071626, 2.5938612436999215), + ("九点外心塞瓦", "X_{195}", 5.197905759162303, 2.661476304367596), + ("格尔巩垂心塞瓦", "X_{196}", 4.508977844505761, 0.46286739978970115), + ("奈格尔勒莫恩塞瓦", "X_{197}", 3.52023988005997, -0.6271097472119954), + ("米滕阿列夫勒莫恩塞瓦", "X_{198}", 3.803825404610103, 0.15955399758775038), + ("斯皮克莱莫恩塞瓦", "X_{199}", 3.856005538248529, 0.681023032147237), + ("半周长平方点", "X_{200}", 3.6020408163265305, 0.9880059556651574), + ]; + for (grp, lab, ex, ey) in want { + let f = fs.iter().find(|f| f.group == grp && f.label == lab).unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!((x - ex).abs() < 1e-6 && (y - ey).abs() < 1e-6, "{} {:?} want {:?}", lab, (x, y), (ex, ey)); + } + assert!(fs.iter().any(|f| f.group == "内心重心塞瓦" && f.label.contains("anti"))); + assert!(fs.iter().any(|f| f.group == "垂心重心塞瓦" && f.label.contains("anti"))); + assert!(fs.iter().any(|f| f.group == "勒莫恩重心塞瓦" && f.label.contains("anti"))); + // Anticomplement Cartesian: 3G − 2P. + let g = fs.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let mo = fs.iter().find(|f| f.group == "摩西点" && f.label == "Mo").expect("Mo"); + let x69 = fs.iter().find(|f| f.group == "余切点" && f.label == "X_{69}").expect("X69"); + let x76 = fs.iter().find(|f| f.group == "第三布罗卡点" && f.label == "X_{76}").expect("X76"); + let (gx, gy) = ptxy(g); + let x192 = fs.iter().find(|f| f.group == "内心重心塞瓦" && f.label == "X_{192}").unwrap(); + let x193 = fs.iter().find(|f| f.group == "垂心重心塞瓦" && f.label == "X_{193}").unwrap(); + let x194 = fs.iter().find(|f| f.group == "勒莫恩重心塞瓦" && f.label == "X_{194}").unwrap(); + let (mox, moy) = ptxy(mo); + let (p69x, p69y) = ptxy(x69); + let (p76x, p76y) = ptxy(x76); + let (p192x, p192y) = ptxy(x192); + let (p193x, p193y) = ptxy(x193); + let (p194x, p194y) = ptxy(x194); + assert!((p192x - (3.0 * gx - 2.0 * mox)).abs() < 1e-6 && (p192y - (3.0 * gy - 2.0 * moy)).abs() < 1e-6); + assert!((p193x - (3.0 * gx - 2.0 * p69x)).abs() < 1e-6 && (p193y - (3.0 * gy - 2.0 * p69y)).abs() < 1e-6); + assert!((p194x - (3.0 * gx - 2.0 * p76x)).abs() < 1e-6 && (p194y - (3.0 * gy - 2.0 * p76y)).abs() < 1e-6); + + // Isosceles 5-5-6: all finite (incl. X196). + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + for (grp, lab, ..) in want { + assert!(fs2.iter().any(|f| f.group == grp && f.label == lab), "isos missing {} {}", grp, lab); + } + + // Equilateral: all → G; X196 present (not right). + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + let g3 = fs3.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let (gx3, gy3) = ptxy(g3); + for (grp, lab, ..) in want { + let f = fs3.iter().find(|f| f.group == grp && f.label == lab).unwrap_or_else(|| panic!("equil missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!((x - gx3).abs() < 1e-5 && (y - gy3).abs() < 1e-5, "equilateral {} != G {:?}", lab, (x, y)); + } + + // Right 3-4-5: X196 skips; others finite. + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + for (grp, lab, ..) in want { + if grp == "格尔巩垂心塞瓦" { + assert!(fs4.iter().all(|f| f.group != grp), "X196 should skip right"); + } else { + assert!(fs4.iter().any(|f| f.group == grp && f.label == lab), "right missing {} {}", grp, lab); + } + } + } + + #[test] + fn x201_to_x210_ceva_conjugates() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let want = [ + ("斯皮克费尔巴哈调和塞瓦", "X_{201}", 3.941144708423327, 1.0602605596798922), + ("内心第一等动力塞瓦", "X_{202}", 4.675197482892764, 2.1984959377388735), + ("内心第二等动力塞瓦", "X_{203}", 3.1297393021764734, 3.220416341963739), + ("内心克劳森塞瓦", "X_{204}", 4.230629739531817, -7.044283755144491), + ("米滕第二幂点塞瓦", "X_{205}", 3.836927177664627, -0.800458028153725), + ("重心第三幂点塞瓦", "X_{206}", 4.035459211140222, -0.6761005992159091), + ("内心X34塞瓦", "X_{207}", 4.427586630119051, 13.822429272105595), + ("垂心X34塞瓦", "X_{208}", 4.782730514518593, 0.553424263174315), + ("垂心格林伯格塞瓦", "X_{209}", 5.629901960784314, 3.578712552745579), + ("斯皮克格林伯格塞瓦", "X_{210}", 3.9635416666666665, 1.3111662369973027), + ]; + for (grp, lab, ex, ey) in want { + let f = fs.iter().find(|f| f.group == grp && f.label == lab).unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!((x - ex).abs() < 1e-6 && (y - ey).abs() < 1e-6, "{} {:?} want {:?}", lab, (x, y), (ex, ey)); + } + assert!(fs.iter().any(|f| f.group == "重心第三幂点塞瓦" && f.label.contains("comp"))); + assert!(fs.iter().any(|f| f.group == "重心第三幂点塞瓦" && f.label.contains("mid"))); + // Complement Cartesian: (3G − X66)/2. + let g = fs.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let x66 = fs.iter().find(|f| f.label == "X_{66}").expect("X66"); + let x206 = fs.iter().find(|f| f.group == "重心第三幂点塞瓦" && f.label == "X_{206}").unwrap(); + let (gx, gy) = ptxy(g); + let (p66x, p66y) = ptxy(x66); + let (p206x, p206y) = ptxy(x206); + assert!((p206x - (3.0 * gx - p66x) / 2.0).abs() < 1e-6 && (p206y - (3.0 * gy - p66y) / 2.0).abs() < 1e-6); + + // Isosceles 5-5-6: all finite (incl. X203/X204). + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + for (grp, lab, ..) in want { + assert!(fs2.iter().any(|f| f.group == grp && f.label == lab), "isos missing {} {}", grp, lab); + } + + // Equilateral: X203 skips; others → G. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + let g3 = fs3.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let (gx3, gy3) = ptxy(g3); + assert!(fs3.iter().all(|f| f.group != "内心第二等动力塞瓦"), "X203 should skip equil"); + for (grp, lab, ..) in want { + if grp == "内心第二等动力塞瓦" { + continue; + } + let f = fs3.iter().find(|f| f.group == grp && f.label == lab).unwrap_or_else(|| panic!("equil missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!((x - gx3).abs() < 1e-5 && (y - gy3).abs() < 1e-5, "equilateral {} != G {:?}", lab, (x, y)); + } + + // Right 3-4-5: X204/X207/X208 skip; others finite (X203 ok). + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + for (grp, lab, ..) in want { + // X204/X207/X208: tan/sec → ∞. X206: bary sum vanishes (point at ∞). + if grp == "内心克劳森塞瓦" + || grp == "内心X34塞瓦" + || grp == "垂心X34塞瓦" + || grp == "重心第三幂点塞瓦" + { + assert!(fs4.iter().all(|f| !(f.group == grp && f.label == lab)), "{} should skip right", lab); + } else { + assert!(fs4.iter().any(|f| f.group == grp && f.label == lab), "right missing {} {}", grp, lab); + } + } + } + + #[test] + fn x211_to_x220_ceva_conjugates() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let want = [ + ("垂心布罗卡中点塞瓦", "X_{211}", 5.3174323110483765, 3.5273321313623947), + ("米滕第二幂截点塞瓦", "X_{212}", 3.799729364005413, 0.8385349599366532), + ("勒莫恩内心勒莫恩截点塞瓦", "X_{213}", 4.793033065723228, 2.301971867768948), + ("重心X44塞瓦", "X_{214}", 2.75, 2.9047375096555625), + ("内心三倍角正弦塞瓦", "X_{215}", 3.118293471234648, 3.204536963894954), + ("九点垂足重心塞瓦", "X_{216}", 4.153027823240589, 1.2850620179944163), + ("勒莫恩垂足重心塞瓦", "X_{217}", 4.634208909592487, 1.7187829379619926), + ("格尔巩外接内切相似塞瓦", "X_{218}", 5.0863099374565675, 2.6677561450735174), + ("奈格尔外接内切相似塞瓦", "X_{219}", 3.5044852627082443, 0.6948539108958203), + ("米滕外接内切相似塞瓦", "X_{220}", 3.8901257770709847, 1.1758370720016735), + ]; + for (grp, lab, ex, ey) in want { + let f = fs.iter().find(|f| f.group == grp && f.label == lab).unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!((x - ex).abs() < 1e-6 && (y - ey).abs() < 1e-6, "{} {:?} want {:?}", lab, (x, y), (ex, ey)); + } + assert!(fs.iter().any(|f| f.group == "重心X44塞瓦" && f.label.contains("mid"))); + assert!(fs.iter().any(|f| f.group == "重心X44塞瓦" && f.label.contains("comp"))); + // mid(I,X100) Cartesian. + let i = fs.iter().find(|f| f.group == "特殊点" && f.label == "I").expect("I"); + let x100 = fs.iter().find(|f| f.label == "X_{100}").expect("X100"); + let x214 = fs.iter().find(|f| f.group == "重心X44塞瓦" && f.label == "X_{214}").unwrap(); + let (ix, iy) = ptxy(i); + let (p100x, p100y) = ptxy(x100); + let (p214x, p214y) = ptxy(x214); + assert!((p214x - (ix + p100x) / 2.0).abs() < 1e-6 && (p214y - (iy + p100y) / 2.0).abs() < 1e-6); + + // Isosceles 5-5-6: all finite. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + for (grp, lab, ..) in want { + assert!(fs2.iter().any(|f| f.group == grp && f.label == lab), "isos missing {} {}", grp, lab); + } + + // Equilateral: X215 skips; others → G. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + let g3 = fs3.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let (gx3, gy3) = ptxy(g3); + assert!(fs3.iter().all(|f| f.group != "内心三倍角正弦塞瓦"), "X215 should skip equil"); + // X214 bary vanishes identically on equilateral (b+c−2a=0). + assert!(fs3.iter().all(|f| !(f.group == "重心X44塞瓦" && f.label == "X_{214}")), "X214 should skip equil"); + for (grp, lab, ..) in want { + if grp == "内心三倍角正弦塞瓦" || grp == "重心X44塞瓦" { + continue; + } + let f = fs3.iter().find(|f| f.group == grp && f.label == lab).unwrap_or_else(|| panic!("equil missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!((x - gx3).abs() < 1e-5 && (y - gy3).abs() < 1e-5, "equilateral {} != G {:?}", lab, (x, y)); + } + + // Right 3-4-5: all finite in this band. + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + for (grp, lab, ..) in want { + assert!(fs4.iter().any(|f| f.group == grp && f.label == lab), "right missing {} {}", grp, lab); + } + } + + #[test] + fn x221_to_x230_ceva_conjugates() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let want = [ + ("内心外接内切外相似塞瓦", "X_{221}", 4.299465240641711, 0.24853369066571662), + ("格尔巩外接内切外相似塞瓦", "X_{222}", 4.443778110944527, 1.4632560768279894), + ("重心外接内切内相似塞瓦", "X_{223}", 3.999165275459098, 0.2036710774716755), + ("格尔巩克劳森等角塞瓦", "X_{224}", 7.940993788819878, 3.331727909625635), + ("垂心内心奈格尔截点塞瓦", "X_{225}", 4.925313706563707, 3.4080197326122943), + ("格尔巩内心奈格尔截点塞瓦", "X_{226}", 4.5498670212765955, 2.1090248407871504), + ("斯皮克内心奈格尔截点塞瓦", "X_{227}", 3.862864077669903, 0.18330867779379764), + ("外心X71塞瓦", "X_{228}", 4.150208623087622, 1.120417991670574), + ("格尔巩X81塞瓦", "X_{229}", 4.685504847481674, 2.3948572831242365), + ("重心基伯特对跖塞瓦", "X_{230}", 8.295973047538201, 6.302331054746105), + ]; + for (grp, lab, ex, ey) in want { + let f = fs.iter().find(|f| f.group == grp && f.label == lab).unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!((x - ex).abs() < 1e-6 && (y - ey).abs() < 1e-6, "{} {:?} want {:?}", lab, (x, y), (ex, ey)); + } + assert!(fs.iter().any(|f| f.group == "重心外接内切内相似塞瓦" && f.label.contains("comp"))); + assert!(fs.iter().any(|f| f.group == "格尔巩内心奈格尔截点塞瓦" && f.label.contains("comp"))); + assert!(fs.iter().any(|f| f.group == "重心基伯特对跖塞瓦" && f.label.contains("mid"))); + // mid(X115,X187) Cartesian. + let x115 = fs.iter().find(|f| f.label == "X_{115}").expect("X115"); + let x187 = fs.iter().find(|f| f.label == "X_{187}").expect("X187"); + let x230 = fs.iter().find(|f| f.group == "重心基伯特对跖塞瓦" && f.label == "X_{230}").unwrap(); + let (p115x, p115y) = ptxy(x115); + let (p187x, p187y) = ptxy(x187); + let (p230x, p230y) = ptxy(x230); + assert!((p230x - (p115x + p187x) / 2.0).abs() < 1e-6 && (p230y - (p115y + p187y) / 2.0).abs() < 1e-6); + + // Isosceles 5-5-6: all finite. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + for (grp, lab, ..) in want { + assert!(fs2.iter().any(|f| f.group == grp && f.label == lab), "isos missing {} {}", grp, lab); + } + + // Equilateral: X230 skips; others → G. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + let g3 = fs3.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let (gx3, gy3) = ptxy(g3); + assert!(fs3.iter().all(|f| !(f.group == "重心基伯特对跖塞瓦" && f.label == "X_{230}")), "X230 should skip equil"); + for (grp, lab, ..) in want { + if grp == "重心基伯特对跖塞瓦" { + continue; + } + let f = fs3.iter().find(|f| f.group == grp && f.label == lab).unwrap_or_else(|| panic!("equil missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!((x - gx3).abs() < 1e-5 && (y - gy3).abs() < 1e-5, "equilateral {} != G {:?}", lab, (x, y)); + } + + // Right 3-4-5: X221 (∞) and X225 (tan) skip; others finite. + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + assert!(fs4.iter().all(|f| !(f.group == "内心外接内切外相似塞瓦" && f.label == "X_{221}")), "X221 should skip right"); + assert!(fs4.iter().all(|f| !(f.group == "垂心内心奈格尔截点塞瓦" && f.label == "X_{225}")), "X225 should skip right"); + for (grp, lab, ..) in want { + if grp == "内心外接内切外相似塞瓦" || grp == "垂心内心奈格尔截点塞瓦" { + continue; + } + assert!(fs4.iter().any(|f| f.group == grp && f.label == lab), "right missing {} {}", grp, lab); + } + } + + #[test] + fn x231_to_x240_ceva_line_conjugates() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let want = [ + ("重心X128塞瓦", "X_{231}", 8.79269284386617, 6.099003920714991), + ("重心X132塞瓦", "X_{232}", 5.881308260081088, 7.290749222310604), + ("重心X140塞瓦", "X_{233}", 4.344833517179783, 1.8457230145462953), + ("格尔巩X177塞瓦", "X_{234}", 4.669908110357883, 2.314192564857007), + ("垂心X185塞瓦", "X_{235}", 5.04625, 3.588741126138643), + ("重心X188塞瓦", "X_{236}", 4.37076736265419, 1.8247014772360928), + ("外心重心线共轭", "X_{237}", 8.863779033270559, 12.91480698998983), + ("内心X37线共轭", "X_{238}", 14.333333333333334, 14.200938936093863), + ("内心X42线共轭", "X_{239}", 37.958333333333336, 37.27746470724639), + ("内心X48线共轭", "X_{240}", 6.520821842250415, 8.289102939394008), + ]; + for (grp, lab, ex, ey) in want { + let f = fs.iter().find(|f| f.group == grp && f.label == lab).unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!((x - ex).abs() < 1e-6 && (y - ey).abs() < 1e-6, "{} {:?} want {:?}", lab, (x, y), (ex, ey)); + } + assert!(fs.iter().any(|f| f.group == "重心X140塞瓦" && f.label.contains("comp"))); + assert!(fs.iter().any(|f| f.group == "垂心X185塞瓦" && f.label.contains("mid"))); + assert!(fs.iter().any(|f| f.group == "外心重心线共轭" && f.label.contains("Euler"))); + // mid(H,X24) + let h = fs.iter().find(|f| f.group == "特殊点" && f.label == "H").expect("H"); + let x24 = fs.iter().find(|f| f.label == "X_{24}").expect("X24"); + let x235 = fs.iter().find(|f| f.group == "垂心X185塞瓦" && f.label == "X_{235}").unwrap(); + let (hx, hy) = ptxy(h); + let (p24x, p24y) = ptxy(x24); + let (p235x, p235y) = ptxy(x235); + assert!((p235x - (hx + p24x) / 2.0).abs() < 1e-6 && (p235y - (hy + p24y) / 2.0).abs() < 1e-6); + // comp(X95) + let g = fs.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let x95 = fs.iter().find(|f| f.label == "X_{95}").expect("X95"); + let x233 = fs.iter().find(|f| f.group == "重心X140塞瓦" && f.label == "X_{233}").unwrap(); + let (gx, gy) = ptxy(g); + let (p95x, p95y) = ptxy(x95); + let (p233x, p233y) = ptxy(x233); + assert!((p233x - (3.0 * gx - p95x) / 2.0).abs() < 1e-6 && (p233y - (3.0 * gy - p95y) / 2.0).abs() < 1e-6); + + // Isosceles 5-5-6: all finite. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + for (grp, lab, ..) in want { + assert!(fs2.iter().any(|f| f.group == grp && f.label == lab), "isos missing {} {}", grp, lab); + } + + // Equilateral: X231/232/237/238/239/240 skip; X233/234/236 → G; X235 finite on Euler. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + let g3 = fs3.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let (gx3, gy3) = ptxy(g3); + for skip in ["X_{231}", "X_{232}", "X_{237}", "X_{238}", "X_{239}", "X_{240}"] { + assert!(fs3.iter().all(|f| f.label != skip), "{} should skip equil", skip); + } + for (grp, lab) in [("重心X140塞瓦", "X_{233}"), ("格尔巩X177塞瓦", "X_{234}"), ("重心X188塞瓦", "X_{236}")] { + let f = fs3.iter().find(|f| f.group == grp && f.label == lab).unwrap_or_else(|| panic!("equil missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!((x - gx3).abs() < 1e-5 && (y - gy3).abs() < 1e-5, "equilateral {} != G {:?}", lab, (x, y)); + } + + // Right 3-4-5: X235/X240 skip; others that are non-equil-only remain finite. + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + assert!(fs4.iter().all(|f| !(f.group == "垂心X185塞瓦" && f.label == "X_{235}")), "X235 should skip right"); + assert!(fs4.iter().all(|f| !(f.group == "内心X48线共轭" && f.label == "X_{240}")), "X240 should skip right"); + for (grp, lab, ..) in want { + if grp == "垂心X185塞瓦" || grp == "内心X48线共轭" { + continue; + } + assert!(fs4.iter().any(|f| f.group == grp && f.label == lab), "right missing {} {}", grp, lab); + } + } + + #[test] + fn x241_to_x250_line_conjugates_isogonals() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let want = [ + ("内心X55线共轭", "X_{241}", 9.9719387755102, 11.826431289311914), + ("垂心X71线共轭", "X_{242}", 6.437913907284766, 6.59497246949392), + ("垂心X73线共轭", "X_{243}", 6.642321785476351, 9.348636100085107), + ("内心X100线共轭", "X_{244}", 6.25, 0.968245836551854), + ("内心X110线共轭", "X_{245}", 5.908751393534002, 0.633335682459522), + ("外心X110线共轭", "X_{246}", 2.141120233091034, 5.041372798371628), + ("垂心X110线共轭", "X_{247}", 4.703399507007825, -1.249103798621572), + ("垂心X132线共轭", "X_{248}", 6.775670970419633, -0.719280998734576), + ("X115等角", "X_{249}", 4.079602071065873, 1.854246025083043), + ("X125等角", "X_{250}", 4.526900018908428, 4.296278811013081), + ]; + for (grp, lab, ex, ey) in want { + let f = fs + .iter() + .find(|f| f.group == grp && f.label == lab) + .unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!( + (x - ex).abs() < 1e-6 && (y - ey).abs() < 1e-6, + "{} {:?} want {:?}", + lab, + (x, y), + (ex, ey) + ); + } + assert!(fs.iter().any(|f| f.group == "内心X55线共轭" && f.label.contains("IO"))); + + // Isosceles 5-5-6: X245/246/247/250 skip; others finite. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + for skip in ["X_{245}", "X_{246}", "X_{247}", "X_{250}"] { + assert!( + fs2.iter().all(|f| f.label != skip), + "{} should skip isos", + skip + ); + } + for (grp, lab, ..) in want { + if matches!(lab, "X_{245}" | "X_{246}" | "X_{247}" | "X_{250}") { + continue; + } + assert!( + fs2.iter().any(|f| f.group == grp && f.label == lab), + "isos missing {} {}", + grp, + lab + ); + } + + // Equilateral: most line-conjugates skip; X249 → G. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + let g3 = fs3.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let (gx3, gy3) = ptxy(g3); + for skip in [ + "X_{241}", "X_{242}", "X_{243}", "X_{244}", "X_{245}", "X_{246}", "X_{247}", "X_{248}", + "X_{250}", + ] { + assert!(fs3.iter().all(|f| f.label != skip), "{} should skip equil", skip); + } + let f249 = fs3 + .iter() + .find(|f| f.group == "X115等角" && f.label == "X_{249}") + .expect("equil X249"); + let (x, y) = ptxy(f249); + assert!((x - gx3).abs() < 1e-5 && (y - gy3).abs() < 1e-5, "equilateral X249 != G {:?}", (x, y)); + + // Right 3-4-5: X242/243/247/250 skip; X245/246 still finite (scalene). + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + for skip in ["X_{242}", "X_{243}", "X_{247}", "X_{250}"] { + assert!(fs4.iter().all(|f| f.label != skip), "{} should skip right", skip); + } + for (grp, lab, ..) in want { + if matches!(lab, "X_{242}" | "X_{243}" | "X_{247}" | "X_{250}") { + continue; + } + assert!( + fs4.iter().any(|f| f.group == grp && f.label == lab), + "right missing {} {}", + grp, + lab + ); + } + } + + + #[test] + fn x251_to_x260_isogonals_cross_conjugates() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let want = [ + ("X141等角", "X_{251}", 4.830872523494094, 2.45107713945113), + ("X143等角", "X_{252}", 3.5796847571466697, 0.9295524601828009), + ("垂心X2交叉共轭", "X_{253}", 5.275133924920259, 14.257077450117105), + ("外心X4交叉共轭", "X_{254}", 21.240054327106364, 7.8069025655417335), + ("X158等角", "X_{255}", 3.4285714285714284, 0.4785153169059357), + ("X171等角", "X_{256}", 4.418648310387986, 1.802383363660565), + ("X172等角", "X_{257}", 4.177179017887975, 1.5665199837105783), + ("等内切等腰截点", "X_{258}", 4.324611036295447, 1.6616741313298702), + ("X174等角", "X_{259}", 4.422767815154589, 1.7899737714567256), + ("X177等角", "X_{260}", 4.237027402124663, 1.5244390480382009), + ]; + for (grp, lab, ex, ey) in want { + let f = fs + .iter() + .find(|f| f.group == grp && f.label == lab) + .unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!( + (x - ex).abs() < 1e-6 && (y - ey).abs() < 1e-6, + "{} {:?} want {:?}", + lab, + (x, y), + (ex, ey) + ); + } + + // Isosceles 5-5-6: all finite. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + for (grp, lab, ..) in want { + assert!( + fs2.iter().any(|f| f.group == grp && f.label == lab), + "isos missing {} {}", + grp, + lab + ); + } + + // Equilateral: all finite; X251/X252 → G. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + let g3 = fs3.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let (gx3, gy3) = ptxy(g3); + for (grp, lab, ..) in want { + assert!( + fs3.iter().any(|f| f.group == grp && f.label == lab), + "equil missing {} {}", + grp, + lab + ); + } + for (grp, lab) in [("X141等角", "X_{251}"), ("X143等角", "X_{252}")] { + let f = fs3 + .iter() + .find(|f| f.group == grp && f.label == lab) + .unwrap_or_else(|| panic!("equil {}", lab)); + let (x, y) = ptxy(f); + assert!( + (x - gx3).abs() < 1e-5 && (y - gy3).abs() < 1e-5, + "equilateral {} != G {:?}", + lab, + (x, y) + ); + } + + // Right 3-4-5: X252/X253/X254 skip; others finite. + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + for skip in ["X_{252}", "X_{253}", "X_{254}"] { + assert!(fs4.iter().all(|f| f.label != skip), "{} should skip right", skip); + } + for (grp, lab, ..) in want { + if matches!(lab, "X_{252}" | "X_{253}" | "X_{254}") { + continue; + } + assert!( + fs4.iter().any(|f| f.group == grp && f.label == lab), + "right missing {} {}", + grp, + lab + ); + } + } + + + #[test] + fn x261_to_x270_isogonals_isotomics() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let want = [ + ("X181等角", "X_{261}", 4.065485269326483, 1.4282345064333561), + ("X182等角", "X_{262}", 4.629337867851194, 2.2854251555728413), + ("X183等角", "X_{263}", 4.929953418014204, 2.780990071519315), + ("外心等截", "X_{264}", 4.506444353518821, 2.5131666059084026), + ("X186等角", "X_{265}", 7.0733695652173925, -0.439920390955081), + ("X188等角", "X_{266}", 4.755100445484774, 2.3362093968005775), + ("X191等角", "X_{267}", 4.871009694963348, 2.8532228931447636), + ("X196等角", "X_{268}", 4.670704643374325, 4.018335908160631), + ("X200等角", "X_{269}", 4.996649586125344, 3.0295370321437214), + ("X201等角", "X_{270}", 4.844900838240842, 2.970127974199845), + ]; + for (grp, lab, ex, ey) in want { + let f = fs + .iter() + .find(|f| f.group == grp && f.label == lab) + .unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!( + (x - ex).abs() < 1e-6 && (y - ey).abs() < 1e-6, + "{} {:?} want {:?}", + lab, + (x, y), + (ex, ey) + ); + } + + // Isosceles 5-5-6: all finite (no 60°). + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + for (grp, lab, ..) in want { + assert!( + fs2.iter().any(|f| f.group == grp && f.label == lab), + "isos missing {} {}", + grp, + lab + ); + } + + // Equilateral: X265 skips (sin 3A=0); others finite. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().all(|f| f.label != "X_{265}"), "X265 should skip equilateral"); + for (grp, lab, ..) in want { + if lab == "X_{265}" { + continue; + } + assert!( + fs3.iter().any(|f| f.group == grp && f.label == lab), + "equil missing {} {}", + grp, + lab + ); + } + + // Right 3-4-5: X264/X270 skip (csc2A / tan); others finite. + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + for skip in ["X_{264}", "X_{270}"] { + assert!(fs4.iter().all(|f| f.label != skip), "{} should skip right", skip); + } + for (grp, lab, ..) in want { + if matches!(lab, "X_{264}" | "X_{270}") { + continue; + } + assert!( + fs4.iter().any(|f| f.group == grp && f.label == lab), + "right missing {} {}", + grp, + lab + ); + } + } + + #[test] + fn x271_to_x280_isogonals_cevapoint() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let want = [ + ("X208等角", "X_{271}", 4.569633883704235, 3.829888413065841), + ("X209等角", "X_{272}", 0.7199770872986051, 0.18117854313016454), + ("X212等角", "X_{273}", 4.87699699243971, 3.3047981376409314), + ("X213等角", "X_{274}", 4.135283025518117, 1.5802419096832656), + ("垂心勒莫恩截点", "X_{275}", 4.749702779383263, 2.6734750735796546), + ("X217等角", "X_{276}", 4.385508246326592, 2.1596621974365897), + ("X218等角", "X_{277}", 3.5721756890576, 1.2223724154554005), + ("X219等角", "X_{278}", 4.9566122071516645, 3.5163083231866197), + ("X220等角", "X_{279}", 4.889986493974234, 2.7947888291577954), + ("内心奈格尔交叉共轭", "X_{280}", 4.756291946308725, 4.475700133725433), + ]; + for (grp, lab, ex, ey) in want { + let f = fs + .iter() + .find(|f| f.group == grp && f.label == lab) + .unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!( + (x - ex).abs() < 1e-6 && (y - ey).abs() < 1e-6, + "{} {:?} want {:?}", + lab, + (x, y), + (ex, ey) + ); + } + + // Isosceles 5-5-6: all finite. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + for (grp, lab, ..) in want { + assert!( + fs2.iter().any(|f| f.group == grp && f.label == lab), + "isos missing {} {}", + grp, + lab + ); + } + + // Equilateral: all finite for this band. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + for (grp, lab, ..) in want { + assert!( + fs3.iter().any(|f| f.group == grp && f.label == lab), + "equil missing {} {}", + grp, + lab + ); + } + + // Right 3-4-5: X271/X273/X275/X276/X278 skip (cot/tan/sec); others finite. + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + for skip in ["X_{271}", "X_{273}", "X_{275}", "X_{276}", "X_{278}"] { + assert!(fs4.iter().all(|f| f.label != skip), "{} should skip right", skip); + } + for (grp, lab, ..) in want { + if matches!(lab, "X_{271}" | "X_{273}" | "X_{275}" | "X_{276}" | "X_{278}") { + continue; + } + assert!( + fs4.iter().any(|f| f.group == grp && f.label == lab), + "right missing {} {}", + grp, + lab + ); + } + } + + + #[test] + fn x281_to_x290_isogonals_cross_cevapoint() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let want = [ + ("X222等角", "X_{281}", 4.558555354371444, 2.45880845844486), + ("X223等角", "X_{282}", 4.795830440641973, 4.575314483089601), + ("X225等角", "X_{283}", 3.6399604352126613, 0.7661688123061161), + ("X226等角", "X_{284}", 4.4468617426302615, 1.7694474115164964), + ("X227等角", "X_{285}", 4.809474322334805, 4.620277612713879), + ("X228等角", "X_{286}", 4.741837557771125, 2.901212445241758), + ("X232等角", "X_{287}", 6.254824522998894, -0.49666816698873906), + ("对称中线X54截点", "X_{288}", 4.645629690252596, 2.0249208989494853), + ("X236等角", "X_{289}", 4.620344800335799, 2.048224463774928), + ("X237等角", "X_{290}", 6.568580111030933, -1.0367981087109945), + ]; + for (grp, lab, ex, ey) in want { + let f = fs + .iter() + .find(|f| f.group == grp && f.label == lab) + .unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!( + (x - ex).abs() < 1e-6 && (y - ey).abs() < 1e-6, + "{} {:?} want {:?}", + lab, + (x, y), + (ex, ey) + ); + } + + // Isosceles 5-5-6: all finite. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + for (grp, lab, ..) in want { + assert!( + fs2.iter().any(|f| f.group == grp && f.label == lab), + "isos missing {} {}", + grp, + lab + ); + } + + // Equilateral: X287/X290 skip (A+ω→π/2); others finite. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + for skip in ["X_{287}", "X_{290}"] { + assert!(fs3.iter().all(|f| f.label != skip), "{} should skip equil", skip); + } + for (grp, lab, ..) in want { + if matches!(lab, "X_{287}" | "X_{290}") { + continue; + } + assert!( + fs3.iter().any(|f| f.group == grp && f.label == lab), + "equil missing {} {}", + grp, + lab + ); + } + + // Right 3-4-5: X281/X286 skip (sec); others finite. + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + for skip in ["X_{281}", "X_{286}"] { + assert!(fs4.iter().all(|f| f.label != skip), "{} should skip right", skip); + } + for (grp, lab, ..) in want { + if matches!(lab, "X_{281}" | "X_{286}") { + continue; + } + assert!( + fs4.iter().any(|f| f.group == grp && f.label == lab), + "right missing {} {}", + grp, + lab + ); + } + } + + + #[test] + fn x291_to_x300_isogonals_hirst_isotomics() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let want = [ + ("X238等角", "X_{291}", 8.175675675675675, 0.2616880639329336), + ("X239等角", "X_{292}", 8.10232220609579, 0.2951112128822778), + ("X240等角", "X_{293}", 6.5180052846609176, -0.5979255705588283), + ("X241等角", "X_{294}", 7.5120924855491324, -0.432520452304782), + ("X242等角", "X_{295}", 8.37748344370861, 0.12824448166249725), + ("X243等角", "X_{296}", 6.061579621844016, -0.8560286714306778), + ("X248等角", "X_{297}", 7.708213417753903, 10.091807886017781), + ("第一等角线等截", "X_{298}", 3.831703156647867, 1.1917226177001814), + ("第二等角线等截", "X_{299}", -11.390595346800717, -16.234465552890185), + ("第一等动力等截", "X_{300}", 4.186903587004856, 1.7645719906151849), + ]; + for (grp, lab, ex, ey) in want { + let f = fs + .iter() + .find(|f| f.group == grp && f.label == lab) + .unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!( + (x - ex).abs() < 1e-6 && (y - ey).abs() < 1e-6, + "{} {:?} want {:?}", + lab, + (x, y), + (ex, ey) + ); + } + + // Isosceles 5-5-6: all finite. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + for (grp, lab, ..) in want { + assert!( + fs2.iter().any(|f| f.group == grp && f.label == lab), + "isos missing {} {}", + grp, + lab + ); + } + + // Equilateral: skip X291–X297/X299 (denom/ω/sin); keep X298/X300. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + for skip in [ + "X_{291}", "X_{292}", "X_{293}", "X_{294}", "X_{295}", "X_{296}", "X_{297}", "X_{299}", + ] { + assert!(fs3.iter().all(|f| f.label != skip), "{} should skip equil", skip); + } + for keep in ["X_{298}", "X_{300}"] { + assert!(fs3.iter().any(|f| f.label == keep), "equil missing {}", keep); + } + + // Right 3-4-5: X297 skips (sec); others finite. + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + assert!(fs4.iter().all(|f| f.label != "X_{297}"), "X297 should skip right"); + for (grp, lab, ..) in want { + if lab == "X_{297}" { + continue; + } + assert!( + fs4.iter().any(|f| f.group == grp && f.label == lab), + "right missing {} {}", + grp, + lab + ); + } + } + + #[test] + fn x301_to_x310_isotomics() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let want = [ + ("第二等动力等截", "X_{301}", 6.980035984420878, -0.569704917658939), + ("第一拿破仑等截", "X_{302}", 4.048146288386569, 1.439502461266907), + ("第二拿破仑等截", "X_{303}", 4.736052005774706, 2.227003407468617), + ("克劳森等截", "X_{304}", 3.118113713687516, 0.586687038229961), + ("X25等截", "X_{305}", 2.823863062471592, 0.475494718798144), + ("X27等截", "X_{306}", 3.280024509803921, 0.647870375928079), + ("X29等截", "X_{307}", 3.786017197005147, 0.974079325155319), + ("布罗卡尔中点等截", "X_{308}", 3.972226143175291, 1.474498640597638), + ("X40等截", "X_{309}", 4.785798695210563, 4.595574343104375), + ("X42等截", "X_{310}", 3.867019616360488, 1.354255379025548), + ]; + for (grp, lab, ex, ey) in want { + let f = fs + .iter() + .find(|f| f.group == grp && f.label == lab) + .unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!( + (x - ex).abs() < 1e-6 && (y - ey).abs() < 1e-6, + "{} {:?} want {:?}", + lab, + (x, y), + (ex, ey) + ); + } + + // Isosceles 5-5-6: all finite (no 60°). + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + for (grp, lab, ..) in want { + assert!( + fs2.iter().any(|f| f.group == grp && f.label == lab), + "isos missing {} {}", + grp, + lab + ); + } + + // Equilateral: skip X301 (60°); keep the rest. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().all(|f| f.label != "X_{301}"), "X301 should skip equil"); + for (grp, lab, ..) in want { + if lab == "X_{301}" { + continue; + } + assert!( + fs3.iter().any(|f| f.group == grp && f.label == lab), + "equil missing {} {}", + grp, + lab + ); + } + + // Right 3-4-5: all finite (no sec/60°/150° issues). + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + for (grp, lab, ..) in want { + assert!( + fs4.iter().any(|f| f.group == grp && f.label == lab), + "right missing {} {}", + grp, + lab + ); + } + } + + + #[test] + fn x311_to_x320_isotomics() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let want = [ + ("科斯尼塔等截", "X_{311}", 3.911819183019623, 1.532926653323435), + ("X57等截", "X_{312}", 3.522500000000000, 1.016658128379447), + ("X58等截", "X_{313}", 3.603790057518488, 1.140294449661418), + ("X65等截", "X_{314}", 3.657306007509387, 1.113361529827304), + ("X66等截", "X_{315}", 2.824048825869370, -0.260263437202232), + ("德鲁桑枢轴", "X_{316}", -8.341999151103565, -18.667500268970660), + ("普拉索洛夫等截", "X_{317}", 5.357551988453238, 3.916218055971332), + ("X77等截", "X_{318}", 4.358616504854369, 2.197354022271805), + ("X79等截", "X_{319}", 3.712500000000000, 1.085584103184833), + ("X80等截", "X_{320}", -25.687500000000011, -35.583034493280657), + ]; + for (grp, lab, ex, ey) in want { + let f = fs + .iter() + .find(|f| f.group == grp && f.label == lab) + .unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!( + (x - ex).abs() < 1e-6 && (y - ey).abs() < 1e-6, + "{} {:?} want {:?}", + lab, + (x, y), + (ex, ey) + ); + } + + // Isosceles 5-5-6: all finite. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + for (grp, lab, ..) in want { + assert!( + fs2.iter().any(|f| f.group == grp && f.label == lab), + "isos missing {} {}", + grp, + lab + ); + } + + // Equilateral: skip X316/X320 (all weights 0); keep the rest. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().all(|f| f.label != "X_{316}"), "X316 should skip equil"); + assert!(fs3.iter().all(|f| f.label != "X_{320}"), "X320 should skip equil"); + for (grp, lab, ..) in want { + if lab == "X_{316}" || lab == "X_{320}" { + continue; + } + assert!( + fs3.iter().any(|f| f.group == grp && f.label == lab), + "equil missing {} {}", + grp, + lab + ); + } + + // Right 3-4-5: skip X317/X318 (tan/sec ∞); keep others. + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + assert!(fs4.iter().all(|f| f.label != "X_{317}"), "X317 should skip right"); + assert!(fs4.iter().all(|f| f.label != "X_{318}"), "X318 should skip right"); + for (grp, lab, ..) in want { + if lab == "X_{317}" || lab == "X_{318}" { + continue; + } + assert!( + fs4.iter().any(|f| f.group == grp && f.label == lab), + "right missing {} {}", + grp, + lab + ); + } + } + + + + #[test] + fn x321_to_x330_isotomics_hirst() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let want = [ + ("X81等截", "X_{321}", 3.889030612244898, 1.348628129482940), + ("X84等截", "X_{322}", 2.908504654771140, 0.082815441023927), + ("X94等截", "X_{323}", -9.565217391304380, -14.144808742670605), + ("X97等截", "X_{324}", 4.608069490996023, 2.866308824482292), + ("X69-Hirst", "X_{325}", -3.779446095076401, -7.521371467594975), + ("X158等截", "X_{326}", 2.837852693294247, 0.312932512949334), + ("X182等截", "X_{327}", 4.197417914700023, 1.781695822447367), + ("X186等截", "X_{328}", 6.673399984546004, -0.313443046787493), + ("X189等截", "X_{329}", 3.200255102040816, 0.103740625344842), + ("X192等截", "X_{330}", 3.625542848870874, 1.250954558660292), + ]; + for (grp, lab, ex, ey) in want { + let f = fs + .iter() + .find(|f| f.group == grp && f.label == lab) + .unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!( + (x - ex).abs() < 1e-6 && (y - ey).abs() < 1e-6, + "{} {:?} want {:?}", + lab, + (x, y), + (ex, ey) + ); + } + + // Isosceles 5-5-6: all finite. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + for (grp, lab, ..) in want { + assert!( + fs2.iter().any(|f| f.group == grp && f.label == lab), + "isos missing {} {}", + grp, + lab + ); + } + + // Equilateral: skip X323/X325/X328 (sin3A / all-zero); keep the rest. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().all(|f| f.label != "X_{323}"), "X323 should skip equil"); + assert!(fs3.iter().all(|f| f.label != "X_{325}"), "X325 should skip equil"); + assert!(fs3.iter().all(|f| f.label != "X_{328}"), "X328 should skip equil"); + for (grp, lab, ..) in want { + if lab == "X_{323}" || lab == "X_{325}" || lab == "X_{328}" { + continue; + } + assert!( + fs3.iter().any(|f| f.group == grp && f.label == lab), + "equil missing {} {}", + grp, + lab + ); + } + + // Right 3-4-5: skip X324 (sec ∞); keep others (X328 finite: only right-angle weight 0). + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + assert!(fs4.iter().all(|f| f.label != "X_{324}"), "X324 should skip right"); + for (grp, lab, ..) in want { + if lab == "X_{324}" { + continue; + } + assert!( + fs4.iter().any(|f| f.group == grp && f.label == lab), + "right missing {} {}", + grp, + lab + ); + } + } + + + + + #[test] + fn x331_to_x340_isotomics_cevapoint() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let want = [ + ("X219等截", "X_{331}", 4.769073392555508, 3.0742351468193774), + ("X225等截", "X_{332}", 3.044366502748047, 0.5146519307677269), + ("X8X9截点", "X_{333}", 3.941062176165803, 1.316914674066641), + ("X238等截", "X_{334}", 8.34631898880741, 0.20599981373956375), + ("X239等截", "X_{335}", 8.256278538812786, 0.23211372794051305), + ("X240等截", "X_{336}", 5.9841189990450365, -0.4119053953863263), + ("X242等截", "X_{337}", 8.577022110932628, 0.10086847158635727), + ("X115X125截点", "X_{338}", 5.653052416823602, 0.5392819325051049), + ("X250等截", "X_{339}", 5.0231776939692505, 0.20838596892903413), + ("X265等截", "X_{340}", 12.275278204144273, 15.32009389925589), + ]; + for (grp, lab, ex, ey) in want { + let f = fs + .iter() + .find(|f| f.group == grp && f.label == lab) + .unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!( + (x - ex).abs() < 1e-6 && (y - ey).abs() < 1e-6, + "{} {:?} want {:?}", + lab, + (x, y), + (ex, ey) + ); + } + + // Isosceles 5-5-6: X338/X339 degenerate (B=C ⇒ sin(B−C)=0 chain); keep others. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + for (grp, lab, ..) in want { + if lab == "X_{338}" || lab == "X_{339}" { + continue; + } + assert!( + fs2.iter().any(|f| f.group == grp && f.label == lab), + "isos missing {} {}", + grp, + lab + ); + } + + // Equilateral: skip X334–X340 (a²−bc / A+ω / sin(B−C) / sin3A); keep X331–X333. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + for lab in ["X_{334}", "X_{335}", "X_{336}", "X_{337}", "X_{338}", "X_{339}", "X_{340}"] { + assert!(fs3.iter().all(|f| f.label != lab), "{} should skip equil", lab); + } + for (grp, lab, ..) in want { + if lab == "X_{331}" || lab == "X_{332}" || lab == "X_{333}" { + assert!( + fs3.iter().any(|f| f.group == grp && f.label == lab), + "equil missing {} {}", + grp, + lab + ); + } + } + + // Right 3-4-5: skip X331/X332/X336/X337/X339/X340 (sec/cot); keep X333–X335/X338. + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + for lab in ["X_{331}", "X_{332}", "X_{336}", "X_{337}", "X_{339}", "X_{340}"] { + assert!(fs4.iter().all(|f| f.label != lab), "{} should skip right", lab); + } + for (grp, lab, ..) in want { + if matches!(lab, "X_{333}" | "X_{334}" | "X_{335}" | "X_{338}") { + assert!( + fs4.iter().any(|f| f.group == grp && f.label == lab), + "right missing {} {}", + grp, + lab + ); + } + } + } + + + + + #[test] + fn x341_to_x350_isotomics_hirst_cevapoint() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let want = [ + ("X269等截", "X_{341}", 3.0069444444444446, 0.6723929420498987), + ("X208X223截点", "X_{342}", 4.220788526733359, 0.38378533239463564), + ("X69X76交点", "X_{343}", 3.5879381258941345, 0.9126652010845535), + ("X277等截", "X_{344}", 4.81706204379562, 2.1891543640287363), + ("X278等截", "X_{345}", 2.9102272727272727, 0.4621173310815668), + ("X312X341交点", "X_{346}", 3.305241935483871, 0.8198855874027797), + ("X40X223截点", "X_{347}", 3.695389291257113, 0.1656737250075146), + ("重心X347截点", "X_{348}", 3.916828370030097, 1.0652928115365186), + ("X226X307截点", "X_{349}", 4.086802409438044, 1.6166629514227433), + ("X75重心Hirst", "X_{350}", -32.6402027027027, -31.736219953466517), + ]; + for (grp, lab, ex, ey) in want { + let f = fs + .iter() + .find(|f| f.group == grp && f.label == lab) + .unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!( + (x - ex).abs() < 1e-6 && (y - ey).abs() < 1e-6, + "{} {:?} want {:?}", + lab, + (x, y), + (ex, ey) + ); + } + + // Isosceles 5-5-6: all finite in this band. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + for (grp, lab, ..) in want { + assert!( + fs2.iter().any(|f| f.group == grp && f.label == lab), + "isos missing {} {}", + grp, + lab + ); + } + + // Equilateral: skip X350 (a²−bc → 0); keep X341–X349. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().all(|f| f.label != "X_{350}"), "X350 should skip equil"); + for (grp, lab, ..) in want { + if lab == "X_{350}" { + continue; + } + assert!( + fs3.iter().any(|f| f.group == grp && f.label == lab), + "equil missing {} {}", + grp, + lab + ); + } + + // Right 3-4-5: skip X342/X345/X348 (sec); keep others. + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + for lab in ["X_{342}", "X_{345}", "X_{348}"] { + assert!(fs4.iter().all(|f| f.label != lab), "{} should skip right", lab); + } + for (grp, lab, ..) in want { + if matches!(lab, "X_{341}" | "X_{343}" | "X_{344}" | "X_{346}" | "X_{347}" | "X_{349}" | "X_{350}") { + assert!( + fs4.iter().any(|f| f.group == grp && f.label == lab), + "right missing {} {}", + grp, + lab + ); + } + } + } + + #[test] + fn x351_to_x360_parry_weill_fuhrmann_morley_hofstadter() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let want = [ + ("帕里圆心", "X_{351}", 11.85128205128205, 11.10255225912793), + ("X353外接逆", "X_{352}", 20.15527987178726, 19.87863244216875), + ("X111Brocard逆", "X_{353}", 4.447476932992233, 1.554796870948855), + ("Weill点", "X_{354}", 4.578125, 2.077694190934187), + ("Fuhrmann心", "X_{355}", 4.3125, 2.114003409804882), + ("莫利中心", "X_{356}", 4.509210790491602, 1.955745846402668), + ("莫利透视1", "X_{357}", 4.531119259517901, 1.998524871477102), + ("莫利透视2", "X_{358}", 4.467903088960554, 1.875087394301666), + ("霍夫斯塔特点", "X_{359}", 4.397629009565369, 1.746395448825259), + ("霍夫斯塔特点", "X_{360}", 4.592957569749832, 2.132793086433669), + ]; + for (grp, lab, ex, ey) in want { + let f = fs + .iter() + .find(|f| f.group == grp && f.label == lab) + .unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!( + (x - ex).abs() < 1e-6 && (y - ey).abs() < 1e-6, + "{} {:?} want {:?}", + lab, + (x, y), + (ex, ey) + ); + } + + // Isosceles 5-5-6: X351 den→0 (skip); keep X352–X360. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + assert!(fs2.iter().all(|f| f.label != "X_{351}"), "X351 should skip isos 5-5-6"); + for (grp, lab, ..) in want { + if lab == "X_{351}" { + continue; + } + assert!( + fs2.iter().any(|f| f.group == grp && f.label == lab), + "isos missing {} {}", + grp, + lab + ); + } + + // Equilateral: skip X351/X352 (weights vanish); keep X353–X360. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + for lab in ["X_{351}", "X_{352}"] { + assert!(fs3.iter().all(|f| f.label != lab), "{} should skip equil", lab); + } + for (grp, lab, ..) in want { + if matches!(lab, "X_{351}" | "X_{352}") { + continue; + } + assert!( + fs3.iter().any(|f| f.group == grp && f.label == lab), + "equil missing {} {}", + grp, + lab + ); + } + + // Right 3-4-5: all in this band finite. + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + for (grp, lab, ..) in want { + assert!( + fs4.iter().any(|f| f.group == grp && f.label == lab), + "right missing {} {}", + grp, + lab + ); + } + } + + + + #[test] + fn x361_to_x367_isoscelizer_sqrt_side_band() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let want = [ + ("等腰化点", "X_{361}", 5.0999110570327515, 2.653978761539622), + ("等外接圆等腰化", "X_{362}", 4.224941270860307, 1.7406232496876568), + ("等周长等腰化", "X_{363}", 4.254362495787133, 1.7084322657787965), + ("Wabash点", "X_{364}", 4.289582048035992, 1.6850063075553896), + ("边根等角", "X_{365}", 4.603067268271407, 2.0596746496290916), + ("边根点", "X_{366}", 4.389105555973576, 1.8145279194492863), + ("X1X366交点", "X_{367}", 4.44793644473969, 1.8742668386531587), + ]; + for (grp, lab, ex, ey) in want { + let f = fs + .iter() + .find(|f| f.group == grp && f.label == lab) + .unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!( + (x - ex).abs() < 1e-6 && (y - ey).abs() < 1e-6, + "{} {:?} want {:?}", + lab, + (x, y), + (ex, ey) + ); + } + // X368–X370 deferred (no closed barycentrics). + for lab in ["X_{368}", "X_{369}", "X_{370}"] { + assert!(fs.iter().all(|f| f.label != lab), "{} should be skipped", lab); + } + + // Isosceles 5-5-6: all finite. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + for (grp, lab, ..) in want { + assert!( + fs2.iter().any(|f| f.group == grp && f.label == lab), + "isos missing {} {}", + grp, + lab + ); } - return out; - } - let x0 = (b * e - 2.0 * cc * d) / det; - let y0 = (b * d - 2.0 * a * e) / det; - let f0 = a * x0 * x0 + b * x0 * y0 + cc * y0 * y0 + d * x0 + e * y0 + f; - let tr = a + cc; - let disc = ((a - cc).powi(2) + b * b).sqrt(); - let l1 = (tr + disc) / 2.0; - let l2 = (tr - disc) / 2.0; - let theta = 0.5 * b.atan2(a - cc); - if det > 0.0 { - if (f0 > 0.0 && l1 > 0.0) || (f0 < 0.0 && l1 < 0.0) { - out.push(fact("圆锥曲线", name, "\\text{无实点(虚椭圆)}".into(), None, "", "配方后右侧为负")); - return out; + + // Equilateral: all finite in this band. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + for (grp, lab, ..) in want { + assert!( + fs3.iter().any(|f| f.group == grp && f.label == lab), + "equil missing {} {}", + grp, + lab + ); } - if near(f0, 0.0) { - out.push(fact("圆锥曲线", name, "\\text{退化为一点}".into(), None, "", "").with(Shape::Point { x: x0, y: y0, label: "点".into() })); - return out; + + // Right 3-4-5: all finite. + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + for (grp, lab, ..) in want { + assert!( + fs4.iter().any(|f| f.group == grp && f.label == lab), + "right missing {} {}", + grp, + lab + ); } - let r1 = (-f0 / l1).sqrt(); - let r2 = (-f0 / l2).sqrt(); - if near(a, cc) && near(b, 0.0) { - out.push(fact("圆锥曲线", name, "\\text{圆}".into(), None, "(x-a)^2 + (y-b)^2 = r^2", "配方")); - out.push(fact("圆锥曲线", "\\text{圆心}", pt(x0, y0), None, "\\left(-\\frac{D}{2},\\ -\\frac{E}{2}\\right)", "配方").with(Shape::Point { x: x0, y: y0, label: "圆心".into() })); - out.push(fact("圆锥曲线", "r", nice(r1), Some(r1), "r = \\frac12\\sqrt{D^2 + E^2 - 4F}", "半径")); - let area = PI * r1 * r1; - out.push(fact("圆锥曲线", "S", nice(area), Some(area), "\\pi r^2", "圆面积")); - out.push(fact("圆锥曲线", "C", nice(2.0 * PI * r1), Some(2.0 * PI * r1), "2\\pi r", "周长")); - let d0 = x0.hypot(y0); - let rel = if near(d0, r1) { "原点在圆上" } else if d0 < r1 { "原点在圆内" } else { "原点在圆外" }; - out.push(fact("圆锥曲线", "O", format!("\\text{{{}}}", rel), None, "|OC| \\text{ vs } r", "比较距离与半径")); - return out; + } + + + #[test] + fn x371_to_x380_kenmotu_pedal_euler_band() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let want = [ + ("肯莫图点", "X_{371}", 4.451520875906775, 1.7771279955358747), + ("肯莫图谐共轭", "X_{372}", 5.536930298223856, 3.566426083563593), + ("重心垂足重心", "X_{373}", 4.440104166666667, 1.9734732849164527), + ("X9垂足重心", "X_{374}", 4.328343531468532, 1.8573561960778358), + ("X10垂足重心", "X_{375}", 4.37109375, 1.9213628319075857), + ("重心反足重心", "X_{376}", 3.7291666666666665, 0.3711609040115441), + ("X7X8欧拉截", "X_{377}", 4.344031531531532, 1.873250390986583), + ("X22外心反射", "X_{378}", 4.595135908440629, 2.4866880197108565), + ("X6X7欧拉截", "X_{379}", 4.533564305616659, 2.3362711353139662), + ("X1X19交X9X55", "X_{380}", 4.43520575654077, 1.7328059090328471), + ]; + for (grp, lab, ex, ey) in want { + let f = fs + .iter() + .find(|f| f.group == grp && f.label == lab) + .unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!( + (x - ex).abs() < 1e-6 && (y - ey).abs() < 1e-6, + "{} {:?} want {:?}", + lab, + (x, y), + (ex, ey) + ); } - // semi-major is the larger radius; r1 belongs to l1 (larger eigenvalue => smaller radius) - let (big, small, ang) = if r2 >= r1 { (r2, r1, theta + PI / 2.0) } else { (r1, r2, theta) }; - let cfoc = (big * big - small * small).sqrt(); - let ecc = cfoc / big; - out.push(fact("圆锥曲线", name, "\\text{椭圆}".into(), None, "\\frac{x'^2}{a^2} + \\frac{y'^2}{b^2} = 1", "判别式 B^2 - 4AC < 0")); - out.push(fact("圆锥曲线", "\\text{中心}", pt(x0, y0), None, "\\nabla F = 0", "求中心").with(Shape::Point { x: x0, y: y0, label: "中心".into() })); - out.push(fact("圆锥曲线", "a", nice(big), Some(big), "", "长半轴")); - out.push(fact("圆锥曲线", "b", nice(small), Some(small), "", "短半轴")); - out.push(fact("圆锥曲线", "c", nice(cfoc), Some(cfoc), "c^2 = a^2 - b^2", "半焦距")); - out.push(fact("圆锥曲线", "e", nice(ecc), Some(ecc), "e = \\frac{c}{a}", "离心率")); - let (fx, fy) = (cfoc * ang.cos(), cfoc * ang.sin()); - out.push(fact("圆锥曲线", "F_1", pt(x0 + fx, y0 + fy), None, "", "焦点").with(Shape::Point { x: x0 + fx, y: y0 + fy, label: "F₁".into() })); - out.push(fact("圆锥曲线", "F_2", pt(x0 - fx, y0 - fy), None, "", "焦点").with(Shape::Point { x: x0 - fx, y: y0 - fy, label: "F₂".into() })); - out.push(fact("圆锥曲线", "S", nice(PI * big * small), Some(PI * big * small), "\\pi a b", "椭圆面积")); - } else { - if near(f0, 0.0) { - out.push(fact("圆锥曲线", name, "\\text{退化为两条相交直线}".into(), None, "", "")); - return out; + // Km dual-label agrees with X371. + let km = fs.iter().find(|f| f.group == "肯莫图点" && f.label == "Km").expect("Km"); + let x371 = fs.iter().find(|f| f.group == "肯莫图点" && f.label == "X_{371}").expect("X371"); + assert_eq!(ptxy(km), ptxy(x371)); + + // Isosceles 5-5-6: all finite. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + for (grp, lab, ..) in want { + assert!( + fs2.iter().any(|f| f.group == grp && f.label == lab), + "isos missing {} {}", + grp, + lab + ); } - // hyperbola: transverse axis along the eigen-direction where -f0/λ > 0 - let (lt, lc_, ang) = if -f0 / l1 > 0.0 { (l1, l2, theta) } else { (l2, l1, theta + PI / 2.0) }; - let at = (-f0 / lt).sqrt(); - let bt = (f0 / lc_).sqrt(); - let cfoc = (at * at + bt * bt).sqrt(); - out.push(fact("圆锥曲线", name, "\\text{双曲线}".into(), None, "\\frac{x'^2}{a^2} - \\frac{y'^2}{b^2} = 1", "判别式 B^2 - 4AC > 0")); - out.push(fact("圆锥曲线", "\\text{中心}", pt(x0, y0), None, "", "求中心").with(Shape::Point { x: x0, y: y0, label: "中心".into() })); - out.push(fact("圆锥曲线", "a", nice(at), Some(at), "", "实半轴")); - out.push(fact("圆锥曲线", "b", nice(bt), Some(bt), "", "虚半轴")); - out.push(fact("圆锥曲线", "e", nice(cfoc / at), Some(cfoc / at), "e = \\frac{c}{a},\\ c^2 = a^2 + b^2", "离心率")); - let (fx, fy) = (cfoc * ang.cos(), cfoc * ang.sin()); - out.push(fact("圆锥曲线", "F_1", pt(x0 + fx, y0 + fy), None, "", "焦点").with(Shape::Point { x: x0 + fx, y: y0 + fy, label: "F₁".into() })); - out.push(fact("圆锥曲线", "F_2", pt(x0 - fx, y0 - fy), None, "", "焦点").with(Shape::Point { x: x0 - fx, y: y0 - fy, label: "F₂".into() })); - if near(b, 0.0) { - let k = bt / at; - let k = if near(ang.sin(), 0.0) { k } else { 1.0 / k }; - out.push(fact("圆锥曲线", "\\text{渐近线}", format!("y - {} = \\pm {}(x - {})", nice_short(y0), nice_short(k), nice_short(x0)), None, "y = \\pm\\frac{b}{a}x", "渐近线")); + + // Equilateral: all finite; X371=X372=G. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + for (grp, lab, ..) in want { + assert!( + fs3.iter().any(|f| f.group == grp && f.label == lab), + "equil missing {} {}", + grp, + lab + ); + } + let g3 = fs3.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let x371e = fs3.iter().find(|f| f.group == "肯莫图点" && f.label == "X_{371}").unwrap(); + let x372e = fs3.iter().find(|f| f.group == "肯莫图谐共轭" && f.label == "X_{372}").unwrap(); + assert_eq!(ptxy(x371e), ptxy(g3)); + assert!((ptxy(x372e).0 - ptxy(g3).0).abs() < 1e-9 && (ptxy(x372e).1 - ptxy(g3).1).abs() < 1e-9); + + // Right 3-4-5: skip X378 (tan); keep the rest. + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + assert!(fs4.iter().all(|f| f.label != "X_{378}"), "X378 skip right"); + for (grp, lab, ..) in want { + if lab == "X_{378}" { + continue; + } + assert!( + fs4.iter().any(|f| f.group == grp && f.label == lab), + "right missing {} {}", + grp, + lab + ); } } - out -} -// --------------------------------------------------------------------------- -// Derived points: midpoints (中点) and section points (定比分点) -#[derive(Deserialize)] -pub struct PointDef { - pub name: String, - pub a: String, - pub b: String, - /// AP : PB = m : n; both None → midpoint - #[serde(default)] - pub m: Option, - #[serde(default)] - pub n: Option, -} -fn distn(a: &[f64], b: &[f64]) -> f64 { - (0..a.len().max(b.len())).map(|t| (a.get(t).unwrap_or(&0.0) - b.get(t).unwrap_or(&0.0)).powi(2)).sum::().sqrt() -} + #[test] + fn x381_to_x390_orthocentroidal_conway_taylor_band() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let want = [ + ("垂心重心圆圆心", "X_{381}", 4.541666666666667, 2.356064868942845), + ("外心垂心反射", "X_{382}", 5.625, 5.002603488851246), + ("X14X98欧拉截", "X_{383}", 14.200845560311862, 25.953040233837722), + ("X32X76欧拉截", "X_{384}", 4.390294725611789, 1.986269464660785), + ("重心X6Hirst", "X_{385}", 20.3713921901528, 20.126033577087185), + ("X58布罗卡反演", "X_{386}", 4.634146341463414, 2.078186185769833), + ("X1X2交X4X6", "X_{387}", 4.691916961130742, 2.139207453371190), + ("X1X4交X7X8", "X_{388}", 4.559335443037975, 2.141784303005525), + ("泰勒圆圆心", "X_{389}", 4.76171875, 2.288489378266831), + ("格尔巩内心反射", "X_{390}", 4.362325174825175, 1.633491664812132), + ]; + for (grp, lab, ex, ey) in want { + let f = fs + .iter() + .find(|f| f.group == grp && f.label == lab) + .unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!( + (x - ex).abs() < 1e-6 && (y - ey).abs() < 1e-6, + "{} {:?} want {:?}", + lab, + (x, y), + (ex, ey) + ); + } + // Cross-checks: X381=mid(G,H), X382=2H−O, X390=2I−Ge. + let g = fs.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let h = fs.iter().find(|f| f.group == "特殊点" && f.label == "H").expect("H"); + let o = fs.iter().find(|f| f.group == "特殊点" && f.label == "O").expect("O"); + let ii = fs.iter().find(|f| f.group == "特殊点" && f.label == "I").expect("I"); + let ge = fs + .iter() + .find(|f| f.group == "格尔巩点" && f.label == "Ge") + .expect("Ge"); + let (gx, gy) = ptxy(g); + let (hx, hy) = ptxy(h); + let (ox, oy) = ptxy(o); + let (ix, iy) = ptxy(ii); + let (gex, gey) = ptxy(ge); + let x381 = fs.iter().find(|f| f.label == "X_{381}").unwrap(); + let x382 = fs.iter().find(|f| f.label == "X_{382}").unwrap(); + let x390 = fs.iter().find(|f| f.label == "X_{390}").unwrap(); + let (x381x, x381y) = ptxy(x381); + let (x382x, x382y) = ptxy(x382); + let (x390x, x390y) = ptxy(x390); + assert!((x381x - (gx + hx) / 2.0).abs() < 1e-9 && (x381y - (gy + hy) / 2.0).abs() < 1e-9); + assert!((x382x - (2.0 * hx - ox)).abs() < 1e-9 && (x382y - (2.0 * hy - oy)).abs() < 1e-9); + assert!((x390x - (2.0 * ix - gex)).abs() < 1e-9 && (x390y - (2.0 * iy - gey)).abs() < 1e-9); -pub fn derived_points_analyze(defs: &[PointDef], points: &[NamedPoint]) -> Vec { - let lookup = |nm: &str| points.iter().find(|p| p.name == nm).map(|p| p.p.clone()); - let mut out = Vec::new(); - for d in defs { - let (Some(pa), Some(pb)) = (lookup(&d.a), lookup(&d.b)) else { continue }; - let (m, n) = match (d.m, d.n) { - (Some(m), Some(n)) => (m, n), - (Some(m), None) => (m, 1.0), - (None, Some(n)) => (1.0, n), - (None, None) => (1.0, 1.0), + // Isosceles 5-5-6: skip X383 (csc(B−C)); keep the rest except maybe X385 ok. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + assert!(fs2.iter().all(|f| f.label != "X_{383}"), "X383 skip isos"); + for (grp, lab, ..) in want { + if lab == "X_{383}" { + continue; + } + assert!( + fs2.iter().any(|f| f.group == grp && f.label == lab), + "isos missing {} {}", + grp, + lab + ); + } + + // Equilateral: skip X385 (Hirst den0); X383→G; others finite at G. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().all(|f| f.label != "X_{385}"), "X385 skip equil"); + let g3 = fs3.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let (g3x, g3y) = ptxy(g3); + for (grp, lab, ..) in want { + if lab == "X_{385}" { + continue; + } + let f = fs3 + .iter() + .find(|f| f.group == grp && f.label == lab) + .unwrap_or_else(|| panic!("equil missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!( + (x - g3x).abs() < 1e-9 && (y - g3y).abs() < 1e-9, + "equil {} {:?} want G {:?}", + lab, + (x, y), + (g3x, g3y) + ); + } + + // Right 3-4-5: all finite (no tan/sec singularity in this band). + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + for (grp, lab, ..) in want { + assert!( + fs4.iter().any(|f| f.group == grp && f.label == lab), + "right missing {} {}", + grp, + lab + ); + } + } + + + + #[test] + fn x391_to_x400_gk_napoleon_parry_yff_band() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), }; - if m + n == 0.0 { - continue; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let want = [ + ("X2X6交X8X9", "X_{391}", 3.920649931224209, 1.2935471372090626), + ("X1X6交X10X11", "X_{392}", 4.192708333333333, 1.5532276961352662), + ("X25叉共轭X4", "X_{393}", 4.959201648313755, 4.08647533176635), + ("X69塞瓦X3", "X_{394}", 3.130227001194743, 0.38868650069465116), + ("X14X16中点", "X_{395}", 12.101547673400342, 10.658878097393696), + ("X13X15中点", "X_{396}", 4.490398421676066, 1.945784012098527), + ("X4X17截点", "X_{397}", 4.725427515862568, 2.383344248128183), + ("X4X18截点", "X_{398}", 4.645290377716149, 1.7995155349172816), + ("帕里反射点", "X_{399}", 4.864265927977838, 1.999789738872749), + ("伊夫马尔法蒂", "X_{400}", 2.90668340537787, 0.6040116450051041), + ]; + for (grp, lab, ex, ey) in want { + let f = fs + .iter() + .find(|f| f.group == grp && f.label == lab) + .unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!( + (x - ex).abs() < 1e-6 && (y - ey).abs() < 1e-6, + "{} {:?} want {:?}", + lab, + (x, y), + (ex, ey) + ); } - let is_mid = near(m, n); - let p: Vec = (0..pa.len().max(pb.len())) - .map(|t| (n * pa.get(t).unwrap_or(&0.0) + m * pb.get(t).unwrap_or(&0.0)) / (m + n)) - .collect(); - let f3 = p.len() == 3; - let pval = if f3 { pt3([p[0], p[1], p[2]]) } else { pt(p[0], p[1]) }; - let how = if is_mid { - if f3 { "\\left(\\frac{x_A+x_B}{2},\\ \\frac{y_A+y_B}{2},\\ \\frac{z_A+z_B}{2}\\right)".to_string() } else { "\\left(\\frac{x_A+x_B}{2},\\ \\frac{y_A+y_B}{2}\\right)".to_string() } - } else { - format!("P = \\frac{{{n}A + {m}B}}{{{m} + {n}}}") + // Cross-checks: X395=mid(F2,S2)=mid(X14,X16), X396=mid(F1,S1)=mid(X13,X15). + let find_lab = |lab: &str| { + fs.iter() + .find(|f| f.label == lab) + .unwrap_or_else(|| panic!("missing {}", lab)) }; - out.push(fact("中点与分点", &d.name, pval, None, &how, if is_mid { "中点公式" } else { "定比分点公式" }).with(Shape::Point { x: p[0], y: *p.get(1).unwrap_or(&0.0), label: d.name.clone() })); - let (dap, dpb) = (distn(&pa, &p), distn(&p, &pb)); - if dpb > 1e-12 { - let ms = nice_short(m); - let ns = nice_short(n); - out.push(fact("中点与分点", &format!("\\frac{{|{}{}|}}{{|{}{}|}}", d.a, d.name, d.name, d.b), nice(dap / dpb), Some(dap / dpb), &format!("AP : PB = {ms} : {ns}"), if is_mid { "中点到两端等距" } else { "定比分点:AP : PB = m : n" })); + let (f1x, f1y) = ptxy(find_lab("F_1")); + let (f2x, f2y) = ptxy(find_lab("F_2")); + let (s1x, s1y) = ptxy(find_lab("S_{1}")); + let (s2x, s2y) = ptxy(find_lab("S_{2}")); + let (x395x, x395y) = ptxy(find_lab("X_{395}")); + let (x396x, x396y) = ptxy(find_lab("X_{396}")); + assert!((x395x - (f2x + s2x) / 2.0).abs() < 1e-9 && (x395y - (f2y + s2y) / 2.0).abs() < 1e-9); + assert!((x396x - (f1x + s1x) / 2.0).abs() < 1e-9 && (x396y - (f1y + s1y) / 2.0).abs() < 1e-9); + + // Isosceles 5-5-6: all finite. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + for (grp, lab, ..) in want { + assert!( + fs2.iter().any(|f| f.group == grp && f.label == lab), + "isos missing {} {}", + grp, + lab + ); } - out.push(seg_fact("中点与分点", &format!("|{}{}|", d.a, d.name), dap, "\\sqrt{\\Delta x^2 + \\Delta y^2}", "到起点的距离").with(Shape::Segment { x1: pa[0], y1: *pa.get(1).unwrap_or(&0.0), x2: p[0], y2: *p.get(1).unwrap_or(&0.0), label: format!("{}{}", d.a, d.name) })); - out.push(seg_fact("中点与分点", &format!("|{}{}|", d.name, d.b), dpb, "\\sqrt{\\Delta x^2 + \\Delta y^2}", "到终点的距离").with(Shape::Segment { x1: p[0], y1: *p.get(1).unwrap_or(&0.0), x2: pb[0], y2: *pb.get(1).unwrap_or(&0.0), label: format!("{}{}", d.name, d.b) })); - } - // two midpoints sharing an endpoint form a midsegment (中位线) - let mid_of = |a: &str, b: &str| -> Option> { - let (pa, pb) = (lookup(a)?, lookup(b)?); - Some((0..pa.len().max(pb.len())).map(|t| (pa.get(t).unwrap_or(&0.0) + pb.get(t).unwrap_or(&0.0)) / 2.0).collect()) - }; - let is_mid_def = |d: &PointDef| match (d.m, d.n) { - (None, None) => true, - (Some(m), Some(n)) => near(m, n), - _ => false, - }; - for i in 0..defs.len() { - for j in i + 1..defs.len() { - let (d1, d2) = (&defs[i], &defs[j]); - if !is_mid_def(d1) || !is_mid_def(d2) { + + // Equilateral: all collapse to G. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + let g3 = fs3.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let (g3x, g3y) = ptxy(g3); + for (grp, lab, ..) in want { + let f = fs3 + .iter() + .find(|f| f.group == grp && f.label == lab) + .unwrap_or_else(|| panic!("equil missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!( + (x - g3x).abs() < 1e-9 && (y - g3y).abs() < 1e-9, + "equil {} {:?} want G {:?}", + lab, + (x, y), + (g3x, g3y) + ); + } + + // Right 3-4-5: skip X393 (tan∞); keep the rest. + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + assert!(fs4.iter().all(|f| f.label != "X_{393}"), "X393 skip right"); + for (grp, lab, ..) in want { + if lab == "X_{393}" { continue; } - let (vertex, u, v) = if d1.a == d2.a { - (d1.a.clone(), d1.b.clone(), d2.b.clone()) - } else if d1.a == d2.b { - (d1.a.clone(), d1.b.clone(), d2.a.clone()) - } else if d1.b == d2.a { - (d1.b.clone(), d1.a.clone(), d2.b.clone()) - } else if d1.b == d2.b { - (d1.b.clone(), d1.a.clone(), d2.a.clone()) - } else { - continue; - }; - if u == v { + assert!( + fs4.iter().any(|f| f.group == grp && f.label == lab), + "right missing {} {}", + grp, + lab + ); + } + } + + #[test] + fn x401_to_x410_bailey_gossard_euler_subst_band() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let want = [ + ("贝利点", "X_{401}", -2.6039268355078, -15.100325130138327), + ("齐曼戈萨德", "X_{402}", 4.669315529960855, 2.6679057734002094), + ("垂足X36", "X_{403}", 5.913496376811594, 5.707388230022506), + ("X21欧拉调和", "X_{404}", 4.393939393939394, 1.9951732389553358), + ("X1X6欧拉截", "X_{405}", 4.2342342342342345, 1.605020125455326), + ("X10X33欧拉截", "X_{406}", 4.520593179634966, 2.304583209910045), + ("X4X225截点", "X_{407}", 4.8876421859545, 3.2012687827934143), + ("X73X228欧拉截", "X_{408}", 3.907987244632432, 0.8080121953537639), + ("X21一次替换", "X_{409}", 4.303789805061146, 1.774941514153659), + ("X29一次替换", "X_{410}", 4.553501656026697, 2.384977259472869), + ]; + for (grp, lab, ex, ey) in want { + let f = fs + .iter() + .find(|f| f.group == grp && f.label == lab) + .unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!( + (x - ex).abs() < 1e-6 && (y - ey).abs() < 1e-6, + "{} {:?} want {:?}", + lab, + (x, y), + (ex, ey) + ); + } + // Cross-check: X403 = mid(H, X186). + let find_lab = |lab: &str| { + fs.iter() + .find(|f| f.label == lab) + .unwrap_or_else(|| panic!("missing {}", lab)) + }; + let (hx, hy) = ptxy(find_lab("H")); + let (x186x, x186y) = ptxy(find_lab("X_{186}")); + let (x403x, x403y) = ptxy(find_lab("X_{403}")); + assert!((x403x - (hx + x186x) / 2.0).abs() < 1e-9 && (x403y - (hy + x186y) / 2.0).abs() < 1e-9); + + // Isosceles 5-5-6: all finite. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + for (grp, lab, ..) in want { + assert!( + fs2.iter().any(|f| f.group == grp && f.label == lab), + "isos missing {} {}", + grp, + lab + ); + } + + // Equilateral: skip X401/X402/X403 (vanishing / X186 undefined); others → G. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + let g3 = fs3.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let (g3x, g3y) = ptxy(g3); + for lab in ["X_{401}", "X_{402}", "X_{403}"] { + assert!(fs3.iter().all(|f| f.label != lab), "equil should skip {}", lab); + } + for (grp, lab, ..) in want { + if lab == "X_{401}" || lab == "X_{402}" || lab == "X_{403}" { continue; } - let (Some(pu), Some(pv)) = (lookup(&u), lookup(&v)) else { continue }; - let (Some(pm), Some(pn2)) = (mid_of(&vertex, &u), mid_of(&vertex, &v)) else { continue }; - let len = distn(&pm, &pn2); - let base = distn(&pu, &pv); - if base < 1e-12 { + let f = fs3 + .iter() + .find(|f| f.group == grp && f.label == lab) + .unwrap_or_else(|| panic!("equil missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!( + (x - g3x).abs() < 1e-9 && (y - g3y).abs() < 1e-9, + "equil {} {:?} want G {:?}", + lab, + (x, y), + (g3x, g3y) + ); + } + + // Right 3-4-5: skip X403/X406/X407/X408/X410 (tan/sec∞); keep the rest. + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + for lab in ["X_{403}", "X_{406}", "X_{407}", "X_{408}", "X_{410}"] { + assert!(fs4.iter().all(|f| f.label != lab), "{} should skip right", lab); + } + for (grp, lab, ..) in want { + if matches!(lab, "X_{403}" | "X_{406}" | "X_{407}" | "X_{408}" | "X_{410}") { continue; } - out.push(seg_fact("中点与分点", &format!("\\text{{中位线}}_{{{}{}}}", d1.name, d2.name), len, &format!("\\frac{{1}}{{2}}|{u}{v}|"), "中位线定理:连接两个中点的线段平行于第三边,且等于第三边的一半").with(Shape::Segment { x1: pm[0], y1: *pm.get(1).unwrap_or(&0.0), x2: pn2[0], y2: *pn2.get(1).unwrap_or(&0.0), label: format!("中位线{}{}", d1.name, d2.name) })); - out.push(fact("中点与分点", &format!("{}{} \\parallel {}{}", d1.name, d2.name, u, v), "\\checkmark".into(), None, "\\vec{MN} = \\frac12\\vec{UV}", "中位线平行于第三边")); + assert!( + fs4.iter().any(|f| f.group == grp && f.label == lab), + "right missing {} {}", + grp, + lab + ); } } - out -} -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn right_triangle() { - let k = TriangleKnown { a: Some(3.0), b: Some(4.0), angle_c: Some(PI / 2.0), ..Default::default() }; - let r = triangle_solve(&k); - assert!(r.error.is_none()); - let c = r.cases[0].facts.iter().find(|f| f.label == "c").unwrap(); - assert!((c.num.unwrap() - 5.0).abs() < 1e-9); - let s = r.cases[0].facts.iter().find(|f| f.label == "S").unwrap(); - assert!((s.num.unwrap() - 6.0).abs() < 1e-9); + #[test] + fn x411_to_x420_euler_subst_hirst_ceva_band() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let want = [ + ("X21二次替换", "X_{411}", 3.5806451612903225, 0.008328996443456811), + ("X29二次替换", "X_{412}", 5.116983967935872, 3.761541290337086), + ("X21三次替换", "X_{413}", 4.02886212929881, 1.1033045496121139), + ("X29三次替换", "X_{414}", 4.778473851942197, 2.934575048607608), + ("X4HirstX29", "X_{415}", 6.792375599216799, 7.8544538955582945), + ("X3HirstX21", "X_{416}", -0.8923491817207847, -10.91901156547412), + ("X3CevaX185", "X_{417}", 3.7250699765331223, 0.3611528591017998), + ("X3CevaX216", "X_{418}", 3.883182566283442, 0.747415389933758), + ("X4HirstX25", "X_{419}", 6.171935987509758, 6.338745532139536), + ("X4HirstX427", "X_{420}", 6.662746209799353, 7.537774647525915), + ]; + for (grp, lab, ex, ey) in want { + let f = fs + .iter() + .find(|f| f.group == grp && f.label == lab) + .unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!( + (x - ex).abs() < 1e-6 && (y - ey).abs() < 1e-6, + "{} {:?} want {:?}", + lab, + (x, y), + (ex, ey) + ); + } + + // Isosceles 5-5-6: all finite. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + for (grp, lab, ..) in want { + assert!( + fs2.iter().any(|f| f.group == grp && f.label == lab), + "isos missing {} {}", + grp, + lab + ); + } + + // Equilateral: skip vanishing/Hirst/X31·X38; keep X413/X414/X417/X418 → G. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + let g3 = fs3.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let (g3x, g3y) = ptxy(g3); + for lab in [ + "X_{411}", "X_{412}", "X_{415}", "X_{416}", "X_{419}", "X_{420}", + ] { + assert!(fs3.iter().all(|f| f.label != lab), "equil should skip {}", lab); + } + // X414 needs X29 which is fine on equil (sec 60°=2); X413/X417/X418 also present. + for lab in ["X_{413}", "X_{414}", "X_{417}", "X_{418}"] { + let f = fs3 + .iter() + .find(|f| f.label == lab) + .unwrap_or_else(|| panic!("equil missing {}", lab)); + let (x, y) = ptxy(f); + assert!( + (x - g3x).abs() < 1e-9 && (y - g3y).abs() < 1e-9, + "equil {} {:?} want G {:?}", + lab, + (x, y), + (g3x, g3y) + ); + } + + // Right 3-4-5: keep X411/X413; skip the rest (X29/sec/csc/tan issues). + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + for lab in [ + "X_{412}", "X_{414}", "X_{415}", "X_{416}", "X_{417}", "X_{418}", "X_{419}", "X_{420}", + ] { + assert!(fs4.iter().all(|f| f.label != lab), "{} should skip right", lab); + } + for lab in ["X_{411}", "X_{413}"] { + assert!( + fs4.iter().any(|f| f.label == lab), + "right missing {}", + lab + ); + } + } + + #[test] + fn x421_to_x430_hirst_euler_subst_band() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let want = [ + ("X4HirstX24", "X_{421}", 5.791494980780398, 5.409343854995461), + ("X4HirstX28", "X_{422}", 6.233150600408183, 6.488290304882697), + ("X4HirstX27", "X_{423}", 6.378590932237614, 6.8435950326478245), + ("X4HirstX451", "X_{424}", 7.259098618603891, 8.994638968427399), + ("X4HirstX21", "X_{425}", 7.91153738989469, 10.588519957978189), + ("X19四次替换", "X_{426}", 3.6863010770899614, 0.266442037907028), + ("X22补点", "X_{427}", 4.703817954220315, 2.752193771815401), + ("X38五次替换", "X_{428}", 4.861958821614583, 3.138525383764397), + ("X58五次替换", "X_{429}", 4.731596112886049, 2.820054666921994), + ("X81五次替换", "X_{430}", 4.78620413637937, 2.9534598144284), + ]; + for (grp, lab, ex, ey) in want { + let f = fs + .iter() + .find(|f| f.group == grp && f.label == lab) + .unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!( + (x - ex).abs() < 1e-6 && (y - ey).abs() < 1e-6, + "{} {:?} want {:?}", + lab, + (x, y), + (ex, ey) + ); + } + + // Isosceles 5-5-6: all finite. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + for (grp, lab, ..) in want { + assert!( + fs2.iter().any(|f| f.group == grp && f.label == lab), + "isos missing {} {}", + grp, + lab + ); + } + + // Equilateral: skip Hirst X421–X425; keep X426–X430 → G. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + let g3 = fs3.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let (g3x, g3y) = ptxy(g3); + for lab in [ + "X_{421}", "X_{422}", "X_{423}", "X_{424}", "X_{425}", + ] { + assert!(fs3.iter().all(|f| f.label != lab), "equil should skip {}", lab); + } + for lab in ["X_{426}", "X_{427}", "X_{428}", "X_{429}", "X_{430}"] { + let f = fs3 + .iter() + .find(|f| f.label == lab) + .unwrap_or_else(|| panic!("equil missing {}", lab)); + let (x, y) = ptxy(f); + assert!( + (x - g3x).abs() < 1e-9 && (y - g3y).abs() < 1e-9, + "equil {} {:?} want G {:?}", + lab, + (x, y), + (g3x, g3y) + ); + } + + // Right 3-4-5: all skip (tan/sec issues). + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + for lab in [ + "X_{421}", "X_{422}", "X_{423}", "X_{424}", "X_{425}", + "X_{426}", "X_{427}", "X_{428}", "X_{429}", "X_{430}", + ] { + assert!(fs4.iter().all(|f| f.label != lab), "{} should skip right", lab); + } + } + + + #[test] + fn x431_to_x440_euler_subst_complement_band() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; + let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let want = [ + ("X283五次替换", "X_{431}", 5.007963472381891, 3.495208719403091), + ("X155六次替换", "X_{432}", 4.563562407094006, 2.409555259083562), + ("X159六次替换", "X_{433}", 4.961581016976578, 3.381898295234162), + ("X195六次替换", "X_{434}", 4.470032407560555, 2.18106533057406), + ("X399六次替换", "X_{435}", 4.586088164307535, 2.986281947148511), + ("X48七次替换", "X_{436}", 4.723728445515073, 2.80083428026426), + ("X214八次替换", "X_{437}", 4.8125, 3.017699523919946), + ("X204九次替换", "X_{438}", 5.542568596434214, 4.801226972208073), + ("X193十次替换", "X_{439}", 5.318300993934079, 4.253350475705726), + ("X27补点", "X_{440}", 3.974902956722828, 0.9714845181161382), + ]; + for (grp, lab, ex, ey) in want { + let f = fs + .iter() + .find(|f| f.group == grp && f.label == lab) + .unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!( + (x - ex).abs() < 1e-6 && (y - ey).abs() < 1e-6, + "{} {:?} want {:?}", + lab, + (x, y), + (ex, ey) + ); + } + + // Isosceles 5-5-6: all finite. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + for (grp, lab, ..) in want { + assert!( + fs2.iter().any(|f| f.group == grp && f.label == lab), + "isos missing {} {}", + grp, + lab + ); + } + + // Equilateral: skip X437 (X214 tril vanishes); keep X431–X436,X438–X440 → G. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + let g3 = fs3.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let (g3x, g3y) = ptxy(g3); + assert!(fs3.iter().all(|f| f.label != "X_{437}"), "equil should skip X437"); + for lab in [ + "X_{431}", "X_{432}", "X_{433}", "X_{434}", "X_{435}", + "X_{436}", "X_{438}", "X_{439}", "X_{440}", + ] { + let f = fs3 + .iter() + .find(|f| f.label == lab) + .unwrap_or_else(|| panic!("equil missing {}", lab)); + let (x, y) = ptxy(f); + assert!( + (x - g3x).abs() < 1e-9 && (y - g3y).abs() < 1e-9, + "equil {} {:?} want G {:?}", + lab, + (x, y), + (g3x, g3y) + ); + } + + // Right 3-4-5: keep X439; skip tan/sec-dependent centers. + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + for lab in [ + "X_{431}", "X_{432}", "X_{433}", "X_{434}", "X_{435}", + "X_{436}", "X_{437}", "X_{438}", "X_{440}", + ] { + assert!(fs4.iter().all(|f| f.label != lab), "{} should skip right", lab); + } + assert!( + fs4.iter().any(|f| f.label == "X_{439}"), + "right missing X439" + ); } #[test] - fn ssa_two_solutions() { - let k = TriangleKnown { a: Some(6.0), b: Some(8.0), angle_a: Some(PI / 6.0), ..Default::default() }; + fn x441_to_x450_complement_hirst_euler_band() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; let r = triangle_solve(&k); - assert_eq!(r.cases.len(), 2); + assert!(r.error.is_none(), "{:?}", r.error); + let fs = &r.cases[0].facts; + let want = [ + ("X297补点", "X_{441}", 2.552143291123049, -2.504258622060274), + ("席弗勒补点", "X_{442}", 4.303075396825397, 1.773196244320162), + ("X452补点", "X_{443}", 4.310084541062802, 1.790319294397209), + ("X19X232欧拉截", "X_{444}", 4.931844742034983, 3.309253802884796), + ("X79十一次替换", "X_{445}", 4.603639563828206, 2.507462098843424), + ("X98X511交叉", "X_{446}", -14.813888293398788, -44.928757018662928), + ("重心HirstX27", "X_{447}", 7.416698644154515, 9.379649326075041), + ("重心Hirst席弗勒", "X_{448}", -24.603159375799478, -68.843542231271428), + ("重心HirstX452", "X_{449}", -19.447089249168585, -56.247475723193276), + ("外心Hirst垂心", "X_{450}", 8.686260623229460, 12.481136841141982), + ]; + for (grp, lab, ex, ey) in want { + let f = fs + .iter() + .find(|f| f.group == grp && f.label == lab) + .unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!( + (x - ex).abs() < 1e-6 && (y - ey).abs() < 1e-6, + "{} {:?} want {:?}", + lab, + (x, y), + (ex, ey) + ); + } + // Cross-check: X(442)=mid(X79,X191); X(441)=mid(X297,X401). + let mid = |a: &str, b: &str| { + let fa = fs.iter().find(|f| f.label == a).unwrap_or_else(|| panic!("missing {}", a)); + let fb = fs.iter().find(|f| f.label == b).unwrap_or_else(|| panic!("missing {}", b)); + let (ax, ay) = ptxy(fa); + let (bx, by) = ptxy(fb); + ((ax + bx) / 2.0, (ay + by) / 2.0) + }; + let (m42x, m42y) = mid("X_{79}", "X_{191}"); + let f442 = fs.iter().find(|f| f.label == "X_{442}").expect("X442"); + let (x442, y442) = ptxy(f442); + assert!( + (x442 - m42x).abs() < 1e-9 && (y442 - m42y).abs() < 1e-9, + "X442 should be mid(X79,X191): {:?} vs {:?}", + (x442, y442), + (m42x, m42y) + ); + let (m41x, m41y) = mid("X_{297}", "X_{401}"); + let f441 = fs.iter().find(|f| f.label == "X_{441}").expect("X441"); + let (x441, y441) = ptxy(f441); + assert!( + (x441 - m41x).abs() < 1e-9 && (y441 - m41y).abs() < 1e-9, + "X441 should be mid(X297,X401): {:?} vs {:?}", + (x441, y441), + (m41x, m41y) + ); + + // Isosceles 5-5-6: all finite. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + for (grp, lab, ..) in want { + assert!( + fs2.iter().any(|f| f.group == grp && f.label == lab), + "isos missing {} {}", + grp, + lab + ); + } + + // Equilateral: skip ω/Hirst-vanishing; keep complements X442/X443 and Euler X444/X445 → G. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + let g3 = fs3.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let (g3x, g3y) = ptxy(g3); + for lab in [ + "X_{441}", "X_{446}", "X_{447}", "X_{448}", "X_{449}", "X_{450}", + ] { + assert!(fs3.iter().all(|f| f.label != lab), "equil should skip {}", lab); + } + for lab in ["X_{442}", "X_{443}", "X_{444}", "X_{445}"] { + let f = fs3 + .iter() + .find(|f| f.label == lab) + .unwrap_or_else(|| panic!("equil missing {}", lab)); + let (x, y) = ptxy(f); + assert!( + (x - g3x).abs() < 1e-9 && (y - g3y).abs() < 1e-9, + "equil {} {:?} want G {:?}", + lab, + (x, y), + (g3x, g3y) + ); + } + + // Right 3-4-5: skip tan/sec-dependent; keep X442/X443/X446/X448/X449. + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + for lab in [ + "X_{441}", "X_{444}", "X_{445}", "X_{447}", "X_{450}", + ] { + assert!(fs4.iter().all(|f| f.label != lab), "{} should skip right", lab); + } + for lab in ["X_{442}", "X_{443}", "X_{446}", "X_{448}", "X_{449}"] { + assert!( + fs4.iter().any(|f| f.label == lab), + "right missing {}", + lab + ); + } } #[test] - fn asa_two_angles() { - // a = 5, B = 50°, C = 60° → A = 70°, b = 5·sin50°/sin70°, c = 5·sin60°/sin70° - let k = TriangleKnown { a: Some(5.0), angle_b: Some(50.0f64.to_radians()), angle_c: Some(60.0f64.to_radians()), ..Default::default() }; + fn x451_to_x460_hirst_alshain_euler11_antares_band() { + let ptxy = |f: &Fact| match f.shape.as_ref() { + Some(Shape::Point { x, y, .. }) => (*x, *y), + _ => panic!("missing point shape for {}", f.label), + }; + // Scalene acute 6-7-8. + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; let r = triangle_solve(&k); assert!(r.error.is_none(), "{:?}", r.error); - let f = |label: &str| r.cases[0].facts.iter().find(|x| x.label == label).unwrap().num.unwrap(); - assert!((f("\\angle A") - 70.0f64.to_radians()).abs() < 1e-9); - assert!((f("b") - 5.0 * 50f64.to_radians().sin() / 70f64.to_radians().sin()).abs() < 1e-9); - assert!((f("c") - 5.0 * 60f64.to_radians().sin() / 70f64.to_radians().sin()).abs() < 1e-9); + let fs = &r.cases[0].facts; + let want = [ + ("垂心HirstX424", "X_{451}", 4.433130461922597, 2.090915400468877), + ("重心HirstX449", "X_{452}", 4.192330917874396, 1.502652053102817), + ("阿尔沙因点", "X_{453}", 5.295484278431903, 4.197610181469446), + ("X155十一次替换", "X_{454}", 5.155341031448409, 3.855246013351434), + ("X159十一次替换", "X_{455}", 4.403604538557236, 2.018784788338213), + ("X195十一次替换", "X_{456}", 5.234750870682093, 4.049240973043124), + ("X399十一次替换", "X_{457}", 5.232156804225402, 3.075932344762030), + ("X76X275欧拉截", "X_{458}", 4.564830921045434, 2.412654186310344), + ("X253垂心塞瓦", "X_{459}", 4.660705932529855, 6.571288767845640), + ("安塔瑞斯点", "X_{460}", 5.700709647677338, 5.187559033292232), + ]; + for (grp, lab, ex, ey) in want { + let f = fs + .iter() + .find(|f| f.group == grp && f.label == lab) + .unwrap_or_else(|| panic!("missing {} {}", grp, lab)); + let (x, y) = ptxy(f); + assert!( + (x - ex).abs() < 1e-6 && (y - ey).abs() < 1e-6, + "{} {:?} want {:?}", + lab, + (x, y), + (ex, ey) + ); + } + // Cross-check: X(452)=anticomplement(X443)=3G−2·X443; X(451) mutual X4-Hirst with X424. + let g = fs.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let (gx, gy) = ptxy(g); + let f443 = fs.iter().find(|f| f.label == "X_{443}").expect("X443"); + let (x443, y443) = ptxy(f443); + let f452 = fs.iter().find(|f| f.label == "X_{452}").expect("X452"); + let (x452, y452) = ptxy(f452); + let ax = 3.0 * gx - 2.0 * x443; + let ay = 3.0 * gy - 2.0 * y443; + assert!( + (x452 - ax).abs() < 1e-9 && (y452 - ay).abs() < 1e-9, + "X452 should be anticomplement(X443): {:?} vs {:?}", + (x452, y452), + (ax, ay) + ); + assert!( + fs.iter().any(|f| f.label == "X_{424}"), + "X424 should exist as mutual X4-Hirst of X451" + ); + + // Isosceles 5-5-6: all finite. + let k2 = TriangleKnown { a: Some(5.0), b: Some(5.0), c: Some(6.0), ..Default::default() }; + let r2 = triangle_solve(&k2); + assert!(r2.error.is_none(), "{:?}", r2.error); + let fs2 = &r2.cases[0].facts; + for (grp, lab, ..) in want { + assert!( + fs2.iter().any(|f| f.group == grp && f.label == lab), + "isos missing {} {}", + grp, + lab + ); + } + + // Equilateral: skip Antares (weights vanish); Euler/Alshain/X451/X452/X459 → G. + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + assert!(r3.error.is_none(), "{:?}", r3.error); + let fs3 = &r3.cases[0].facts; + let g3 = fs3.iter().find(|f| f.group == "特殊点" && f.label == "G").expect("G"); + let (g3x, g3y) = ptxy(g3); + assert!( + fs3.iter().all(|f| f.label != "X_{460}"), + "equil should skip X460" + ); + for lab in [ + "X_{451}", "X_{452}", "X_{453}", "X_{454}", "X_{455}", "X_{456}", "X_{457}", "X_{458}", + "X_{459}", + ] { + let f = fs3 + .iter() + .find(|f| f.label == lab) + .unwrap_or_else(|| panic!("equil missing {}", lab)); + let (x, y) = ptxy(f); + assert!( + (x - g3x).abs() < 1e-9 && (y - g3y).abs() < 1e-9, + "equil {} {:?} want G {:?}", + lab, + (x, y), + (g3x, g3y) + ); + } + + // Right 3-4-5: skip tan/sec-dependent; keep X452/X453/X459. + let k4 = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; + let r4 = triangle_solve(&k4); + assert!(r4.error.is_none(), "{:?}", r4.error); + let fs4 = &r4.cases[0].facts; + for lab in [ + "X_{451}", "X_{454}", "X_{455}", "X_{456}", "X_{457}", "X_{458}", "X_{460}", + ] { + assert!(fs4.iter().all(|f| f.label != lab), "{} should skip right", lab); + } + for lab in ["X_{452}", "X_{453}", "X_{459}"] { + assert!( + fs4.iter().any(|f| f.label == lab), + "right missing {}", + lab + ); + } } #[test] - fn midsegment_and_bisector() { - // 3-4-5 triangle: midsegment parallel to a(=3) is 1.5; the bisector from A… check t_c - let k = TriangleKnown { a: Some(3.0), b: Some(4.0), angle_c: Some(PI / 2.0), ..Default::default() }; + fn etc_dual_labels_steiner_tarry_parry() { + let k = TriangleKnown { a: Some(6.0), b: Some(7.0), c: Some(8.0), ..Default::default() }; let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); let fs = &r.cases[0].facts; - let mid = fs.iter().find(|f| f.label == "\\text{中位线}_a").unwrap(); - assert!((mid.num.unwrap() - 1.5).abs() < 1e-9); - let mid_c = fs.iter().find(|f| f.label == "\\text{中位线}_c").unwrap(); - assert!((mid_c.num.unwrap() - 2.5).abs() < 1e-9); - // angle bisector from C: t_c = 2ab·cos(C/2)/(a+b) = 2·3·4·cos45°/7 - let t_c = fs.iter().find(|f| f.label == "t_c").unwrap(); - assert!((t_c.num.unwrap() - 24.0 * (PI / 4.0).cos() / 7.0).abs() < 1e-9); - // the drawn bisector starts at C and divides AB as AC : BC = 4 : 3 - let seg = t_c.shape.as_ref().unwrap(); - if let Shape::Segment { x1, y1, x2, y2: _, .. } = seg { - let (va, vb, vc) = (r.cases[0].vertices[0], r.cases[0].vertices[1], r.cases[0].vertices[2]); - assert!((*x1 - vc[0]).abs() < 1e-9 && (*y1 - vc[1]).abs() < 1e-9); - let fx = (3.0 * va[0] + 4.0 * vb[0]) / 7.0; - assert!((*x2 - fx).abs() < 1e-9); - } else { - panic!("bisector fact should carry a segment"); - } + assert!(fs.iter().any(|f| f.group == "施泰纳点" && f.label == "X_{99}")); + assert!(fs.iter().any(|f| f.group == "塔里点" && f.label == "X_{98}")); + assert!(fs.iter().any(|f| f.group == "帕里点" && f.label == "X_{111}")); + let k3 = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; + let r3 = triangle_solve(&k3); + let fs3 = &r3.cases[0].facts; + assert!(fs3.iter().all(|f| f.label != "X_{99}" && f.label != "X_{98}" && f.label != "X_{111}")); } #[test] - fn nice_forms() { - assert_eq!(nice(2f64.sqrt() * 3.0), "3\\sqrt{2} \\approx 4.2426"); - assert_eq!(nice(0.5), "\\frac{1}{2} = 0.5"); - assert_eq!(nice(5.0), "5"); + fn british_flag_on_rectangle() { + let pts = vec![ + NamedPoint { name: "A".into(), p: vec![0.0, 0.0] }, + NamedPoint { name: "B".into(), p: vec![4.0, 0.0] }, + NamedPoint { name: "C".into(), p: vec![4.0, 3.0] }, + NamedPoint { name: "D".into(), p: vec![0.0, 3.0] }, + ]; + let out = points_analyze(&pts); + assert!(out.iter().any(|f| f.value.contains("矩形"))); + let lhs = out.iter().find(|f| f.group == "英国国旗定理" && f.label == "PA^2 + PC^2").expect("british lhs"); + let rhs = out.iter().find(|f| f.group == "英国国旗定理" && f.label == "PB^2 + PD^2").expect("british rhs"); + assert!((lhs.num.unwrap() - 25.0).abs() < 1e-9); + assert!((rhs.num.unwrap() - 25.0).abs() < 1e-9); } #[test] - fn circle() { - // x^2 + y^2 - 4x + 2y - 4 = 0 => centre (2,-1), r = 3 - let f = conic_analyze([1.0, 0.0, 1.0, -4.0, 2.0, -4.0], "C"); - let r = f.iter().find(|f| f.label == "r").unwrap(); - assert!((r.num.unwrap() - 3.0).abs() < 1e-9); + fn varignon_parallelogram() { + let pts = vec![ + NamedPoint { name: "A".into(), p: vec![0.0, 0.0] }, + NamedPoint { name: "B".into(), p: vec![4.0, 0.0] }, + NamedPoint { name: "C".into(), p: vec![5.0, 3.0] }, + NamedPoint { name: "D".into(), p: vec![1.0, 3.0] }, + ]; + let out = points_analyze(&pts); + assert!(out.iter().any(|f| f.group == "瓦里农定理" && f.value.contains("平行四边形"))); + assert_eq!(out.iter().filter(|f| f.group == "瓦里农定理" && f.label.starts_with("M_")).count(), 4); } #[test] - fn hl_hypotenuse_leg() { - // HL written as SSA with the right angle: a = 3, c = 5, C = 90° → b = 4, one case - let k = TriangleKnown { a: Some(3.0), c: Some(5.0), angle_c: Some(PI / 2.0), ..Default::default() }; + fn viviani_equilateral() { + let k = TriangleKnown { a: Some(2.0), b: Some(2.0), c: Some(2.0), ..Default::default() }; let r = triangle_solve(&k); assert!(r.error.is_none(), "{:?}", r.error); - assert_eq!(r.cases.len(), 1); - let f = |label: &str| r.cases[0].facts.iter().find(|x| x.label == label).unwrap().num.unwrap(); - assert!((f("b") - 4.0).abs() < 1e-9); - assert!((f("\\angle A") - 3f64.atan2(4.0)).abs() < 1e-9); + let fs = &r.cases[0].facts; + let sum = fs.iter().find(|f| f.group == "维维安尼定理" && f.label.contains("d_a")).expect("viviani sum"); + let h = fs.iter().find(|f| f.group == "维维安尼定理" && f.label == "h").expect("viviani h"); + assert!((sum.num.unwrap() - h.num.unwrap()).abs() < 1e-9); + // h = √3 for side 2 + assert!((h.num.unwrap() - 3f64.sqrt()).abs() < 1e-9, "h = {}", h.value); } #[test] - fn median_as_input() { - // b = 4, c = 3, m_a = 2.5 → a = 5 (the 3-4-5 triangle) - let k = TriangleKnown { b: Some(4.0), c: Some(3.0), ma: Some(2.5), ..Default::default() }; - let r = triangle_solve(&k); - assert!(r.error.is_none(), "{:?}", r.error); - let f = |label: &str| r.cases[0].facts.iter().find(|x| x.label == label).unwrap().num.unwrap(); - assert!((f("a") - 5.0).abs() < 1e-9); - // the other direction: a and b with the median give c - let k = TriangleKnown { a: Some(5.0), b: Some(4.0), ma: Some(2.5), ..Default::default() }; - let r = triangle_solve(&k); - let f = |label: &str| r.cases[0].facts.iter().find(|x| x.label == label).unwrap().num.unwrap(); - assert!((f("c") - 3.0).abs() < 1e-9); + fn parallelogram_law_vectors() { + let vs = vec![ + NamedVec { name: "a".into(), v: vec![3.0, 4.0] }, + NamedVec { name: "b".into(), v: vec![1.0, 0.0] }, + ]; + let out = vectors_analyze(&vs); + let f = out.iter().find(|f| f.group == "平行四边形法则").expect("parallelogram law"); + // |a+b|² + |a-b|² = 2(25+1) = 52 + assert!((f.num.unwrap() - 52.0).abs() < 1e-9, "got {}", f.value); } #[test] - fn height_as_input() { - // a = 3, c = 5, h_c = 2.4 → S = 6 → sin B = 0.8: B = 53.13° or 126.87° - let k = TriangleKnown { a: Some(3.0), c: Some(5.0), hc: Some(2.4), ..Default::default() }; - let r = triangle_solve(&k); - assert!(r.error.is_none(), "{:?}", r.error); - assert_eq!(r.cases.len(), 2); - let f = |i: usize, label: &str| r.cases[i].facts.iter().find(|x| x.label == label).unwrap().num.unwrap(); - assert!((f(0, "b") - 4.0).abs() < 1e-9); - assert!((f(0, "S") - 6.0).abs() < 1e-9); + fn viviani_regular_tetra() { + // Regular tetra with edge √2: (1,1,1),(1,-1,-1),(-1,1,-1),(-1,-1,1) + let pts = vec![ + NamedPoint { name: "A".into(), p: vec![1.0, 1.0, 1.0] }, + NamedPoint { name: "B".into(), p: vec![1.0, -1.0, -1.0] }, + NamedPoint { name: "C".into(), p: vec![-1.0, 1.0, -1.0] }, + NamedPoint { name: "D".into(), p: vec![-1.0, -1.0, 1.0] }, + ]; + let out = points_analyze(&pts); + let sum = out.iter().find(|f| f.group == "维维安尼定理" && f.label.contains("面")).expect("tetra viviani"); + let edge = 2f64.sqrt() * 2.0 / 1.0; // |A-B| = √[(0)²+(2)²+(2)²] = √8 = 2√2 + let e = (0.0f64.powi(2) + 2.0f64.powi(2) + 2.0f64.powi(2)).sqrt(); // √8 = 2√2 + let h_reg = (6.0f64).sqrt() / 3.0 * e; + assert!((sum.num.unwrap() - h_reg).abs() < 1e-9, "sum={} expect {}", sum.value, h_reg); + let _ = edge; } + #[test] - fn angles_plus_area() { - // A = B = 60°, S = 3√3/4 → equilateral with side √3 - let s = 3.0 * 3f64.sqrt() / 4.0; - let k = TriangleKnown { angle_a: Some(PI / 3.0), angle_b: Some(PI / 3.0), area: Some(s), ..Default::default() }; + fn ptolemy_and_brahmagupta_rectangle() { + // 3-4-5 rectangle: sides 4,3,4,3; diagonals 5,5; area 12 + let pts = vec![ + NamedPoint { name: "A".into(), p: vec![0.0, 0.0] }, + NamedPoint { name: "B".into(), p: vec![4.0, 0.0] }, + NamedPoint { name: "C".into(), p: vec![4.0, 3.0] }, + NamedPoint { name: "D".into(), p: vec![0.0, 3.0] }, + ]; + let out = points_analyze(&pts); + assert!(out.iter().any(|f| f.group == "圆内接四边形")); + let acbd = out.iter().find(|f| f.group == "托勒密定理" && f.label.contains("AC")).unwrap(); + assert!((acbd.num.unwrap() - 25.0).abs() < 1e-9, "AC·BD = {}", acbd.value); + let rhs = out.iter().find(|f| f.group == "托勒密定理" && f.label.contains("AB")).unwrap(); + assert!((rhs.num.unwrap() - 25.0).abs() < 1e-9); + let s = out.iter().find(|f| f.group == "婆罗摩笈多公式").unwrap(); + assert!((s.num.unwrap() - 12.0).abs() < 1e-9, "Brahmagupta = {}", s.value); + } + + #[test] + fn napoleon_equilateral_centroids() { + let k = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; let r = triangle_solve(&k); assert!(r.error.is_none(), "{:?}", r.error); - let f = |label: &str| r.cases[0].facts.iter().find(|x| x.label == label).unwrap().num.unwrap(); - assert!((f("a") - 3f64.sqrt()).abs() < 1e-9); - assert!((f("R") - 1.0).abs() < 1e-9); + let fs = &r.cases[0].facts; + assert!(fs.iter().any(|f| f.group == "拿破仑定理" && f.value.contains("等边"))); + let side = fs.iter().find(|f| f.label == "|G_aG_b|").unwrap(); + assert!(side.num.unwrap() > 0.0); + assert_eq!(fs.iter().filter(|f| f.group == "拿破仑定理" && f.label.starts_with("G_")).count(), 3); } + + #[test] - fn right_triangle_projection_theorem() { - // 3-4-5: altitude to hypotenuse 12/5, projections 9/5 and 16/5 + fn oh2_carnot_isogonal_morley() { let k = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; let r = triangle_solve(&k); + assert!(r.error.is_none(), "{:?}", r.error); let fs = &r.cases[0].facts; - let get = |label: &str| fs.iter().find(|f| f.label == label).unwrap(); - assert!((get("\\text{斜边上的高}").num.unwrap() - 12.0 / 5.0).abs() < 1e-9); - assert!((get("\\text{射影}_{a}").num.unwrap() - 9.0 / 5.0).abs() < 1e-9); - assert!((get("\\text{射影}_{b}").num.unwrap() - 16.0 / 5.0).abs() < 1e-9); - // the projection segments lie on the hypotenuse and share the altitude foot - if let (Shape::Segment { x2: x2a, y2: y2a, .. }, Shape::Segment { x2: x2b, y2: y2b, .. }) = (get("\\text{射影}_{a}").shape.as_ref().unwrap(), get("\\text{射影}_{b}").shape.as_ref().unwrap()) { - assert!((x2a - x2b).abs() < 1e-9 && (y2a - y2b).abs() < 1e-9, "both projections end at the altitude foot"); - } else { - panic!("projection facts should carry segments"); - } + let oh2 = fs.iter().find(|f| f.label == "|OH|^2").expect("|OH|^2"); + assert!((oh2.num.unwrap() - 6.25).abs() < 1e-9, "OH² = {}", oh2.value); + let form = fs.iter().find(|f| f.label.starts_with("9R^2")).expect("9R^2 form"); + assert!((form.num.unwrap() - 6.25).abs() < 1e-9); + let oi2 = fs.iter().find(|f| f.label == "|OI|^2").expect("|OI|^2"); + assert!((oi2.num.unwrap() - 1.25).abs() < 1e-9, "OI² = {}", oi2.value); + assert!(fs.iter().any(|f| f.group == "等角共轭" && f.label.contains("H"))); + let sum = fs + .iter() + .find(|f| f.group == "卡诺定理" && f.label.contains("delta_a") && f.label.contains('+')) + .expect("Carnot sum"); + let rr = fs.iter().find(|f| f.group == "卡诺定理" && f.label == "R+r").unwrap(); + assert!((sum.num.unwrap() - rr.num.unwrap()).abs() < 1e-6, "Carnot {} vs {}", sum.value, rr.value); + assert!( + fs.iter().any(|f| f.group == "莫利定理" && f.value.contains("等边")), + "Morley missing" + ); + let ms = fs.iter().find(|f| f.group == "莫利定理" && f.label == "|M_0M_1|").unwrap(); + assert!(ms.num.unwrap() > 0.0); } #[test] - fn inconsistent_givens_rejected() { - // SSS plus a wrong area - let k = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), area: Some(10.0), ..Default::default() }; - let r = triangle_solve(&k); - assert!(r.error.is_some()); - // SAS plus a wrong extra angle - let k = TriangleKnown { a: Some(5.0), b: Some(7.0), angle_c: Some(PI / 3.0), angle_a: Some(1.2), ..Default::default() }; - let r = triangle_solve(&k); - assert!(r.error.is_some()); - // consistent extra angle passes - let aa = (5.0f64 * (PI / 3.0).sin() / 39f64.sqrt()).asin(); // sin A = a sinC / c - let k = TriangleKnown { a: Some(5.0), b: Some(7.0), angle_c: Some(PI / 3.0), angle_a: Some(aa), ..Default::default() }; + fn simson_and_trilinear_polar() { + let k = TriangleKnown { a: Some(3.0), b: Some(4.0), c: Some(5.0), ..Default::default() }; let r = triangle_solve(&k); assert!(r.error.is_none(), "{:?}", r.error); - let f = |label: &str| r.cases[0].facts.iter().find(|x| x.label == label).unwrap().num.unwrap(); - assert!((f("\\angle B") - (PI - PI / 3.0 - aa)).abs() < 1e-9); + let fs = &r.cases[0].facts; + assert!( + fs.iter().any(|f| f.group == "西姆松线" && f.value.contains("共线")), + "Simson collinear missing" + ); + assert!(fs.iter().any(|f| f.group == "西姆松线" && f.label.contains("Simson"))); + assert_eq!(fs.iter().filter(|f| f.group == "西姆松线" && f.label.starts_with("F_")).count(), 3); + assert!( + fs.iter().any(|f| f.group == "三线极线" && f.label.contains("tau")), + "trilinear polar of I missing" + ); + assert!(fs.iter().any(|f| f.group == "三线极线" && f.label.contains("l_"))); } #[test] - fn derived_midpoints_and_sections() { - let pts = vec![ - NamedPoint { name: "A".into(), p: vec![0.0, 0.0] }, - NamedPoint { name: "B".into(), p: vec![4.0, 0.0] }, - NamedPoint { name: "C".into(), p: vec![1.0, 3.0] }, - ]; - let defs = vec![ - PointDef { name: "M".into(), a: "A".into(), b: "B".into(), m: None, n: None }, - PointDef { name: "N".into(), a: "A".into(), b: "C".into(), m: None, n: None }, + fn miquel_complete_quadrilateral() { + // y=0, x=0, x+y=2, x-y=1 — four lines, six vertices, general position + let ls = vec![ + NamedLine { name: "l".into(), a: 0.0, b: 1.0, c: 0.0 }, + NamedLine { name: "m".into(), a: 1.0, b: 0.0, c: 0.0 }, + NamedLine { name: "n".into(), a: 1.0, b: 1.0, c: 2.0 }, + NamedLine { name: "p".into(), a: 1.0, b: -1.0, c: 1.0 }, ]; - let out = derived_points_analyze(&defs, &pts); - let get = |label: &str| out.iter().find(|f| f.label == label).unwrap(); - assert!((get("\\text{中位线}_{MN}").num.unwrap() - 18f64.sqrt() / 2.0).abs() < 1e-9); - assert!(out.iter().any(|f| f.label == "MN \\parallel BC")); - // section point P = 分点(A, B, 1:2): AP:PB = 1:2 → P = (4/3·1...) = (4/3? no: (2A + 1B)/3 = (4/3, 0)) - let defs = vec![PointDef { name: "P".into(), a: "A".into(), b: "B".into(), m: Some(1.0), n: Some(2.0) }]; - let out = derived_points_analyze(&defs, &pts); - let get = |label: &str| out.iter().find(|f| f.label == label).unwrap(); - assert!((get("|AP|").num.unwrap() - 4.0 / 3.0).abs() < 1e-9); - assert!((get("\\frac{|AP|}{|PB|}").num.unwrap() - 0.5).abs() < 1e-9); + let out = lines_analyze(&ls, &[]); + assert!( + out.iter().any(|f| f.group == "米克尔点" && f.label.contains("Miquel")), + "Miquel missing: {:?}", + out.iter().map(|f| (&f.group, &f.label)).collect::>() + ); + assert!(out.iter().any(|f| f.group == "米克尔点" && f.label == "M")); } #[test] - fn vector_projection_vector() { - let vs = vec![NamedVec { name: "a".into(), v: vec![3.0, 4.0] }, NamedVec { name: "b".into(), v: vec![1.0, 0.0] }]; - let out = vectors_analyze(&vs); - let get = |label: &str| out.iter().find(|f| f.label == label).unwrap(); - let proj = get("\\text{proj}_{\\mathbf{b}}\\ \\mathbf{a}").value.clone(); - assert!(proj.contains("3") && proj.contains("0"), "proj = {proj}"); - let perp = get("\\mathbf{a} - \\text{proj}_{\\mathbf{b}}\\ \\mathbf{a}").value.clone(); - assert!(perp.contains("4"), "perp = {perp}"); - // scalar projection still there - assert!((get("\\text{a 在 b 上的投影}").num.unwrap() - 3.0).abs() < 1e-9); + fn desargues_perspective_triangles() { + // S=O; A,B,C and scaled A',B',C' with distinct factors → perspective + axis + let pts = vec![ + NamedPoint { name: "A".into(), p: vec![2.0, 1.0] }, + NamedPoint { name: "B".into(), p: vec![-1.0, 2.0] }, + NamedPoint { name: "C".into(), p: vec![-2.0, -3.0] }, + NamedPoint { name: "A'".into(), p: vec![4.0, 2.0] }, + NamedPoint { name: "B'".into(), p: vec![-3.0, 6.0] }, + NamedPoint { name: "C'".into(), p: vec![-1.0, -1.5] }, + ]; + let out = points_analyze(&pts); + assert!( + out.iter().any(|f| f.group == "德萨格定理" && f.value.contains("透视")), + "Desargues perspective missing" + ); + assert!(out.iter().any(|f| f.group == "德萨格定理" && f.label == "S")); + assert!( + out.iter().any(|f| f.group == "德萨格定理" && f.label.contains("透视轴")), + "Desargues axis missing" + ); } #[test] - fn parallel_transversal_angles() { - // y = 0, y = 3 parallel; x - y = 0 a 45° transversal - let ls = vec![ - NamedLine { name: "l".into(), a: 0.0, b: 1.0, c: 0.0 }, - NamedLine { name: "m".into(), a: 0.0, b: 1.0, c: 3.0 }, - NamedLine { name: "t".into(), a: 1.0, b: -1.0, c: 0.0 }, + fn pascal_cyclic_hexagon() { + let pts = vec![ + NamedPoint { name: "A".into(), p: vec![5.0, 0.0] }, + NamedPoint { name: "B".into(), p: vec![4.0, 3.0] }, + NamedPoint { name: "C".into(), p: vec![-3.0, 4.0] }, + NamedPoint { name: "D".into(), p: vec![-5.0, 0.0] }, + NamedPoint { name: "E".into(), p: vec![-3.0, -4.0] }, + NamedPoint { name: "F".into(), p: vec![4.0, -3.0] }, ]; - let out = lines_analyze(&ls, &[]); - let get = |label: &str| out.iter().find(|f| f.label == label).unwrap(); - // corresponding angles ∠(t, l) = ∠(t, m) = 45° - let cor = get("\\text{同位角} \\; \\angle(t, l) = \\angle(t, m)").num.unwrap(); - assert!((cor - PI / 4.0).abs() < 1e-9); - // co-interior supplementary: 135° - let co = get("\\text{同旁内角}(t \\mid l \\parallel m)").num.unwrap(); - assert!((co - 3.0 * PI / 4.0).abs() < 1e-9); - // cut segment between the parallels along t: from (0,0) to (3,3) → 3√2 - let cut = out.iter().find(|f| f.label == "\\text{截线段}_{t}").unwrap(); - assert!((cut.num.unwrap() - 3.0 * 2.0f64.sqrt()).abs() < 1e-9); + let out = points_analyze(&pts); + assert!( + out.iter().any(|f| f.group == "帕斯卡定理" && f.value.contains("共线")), + "Pascal missing" + ); + assert!(out.iter().any(|f| f.group == "帕斯卡定理" && f.label.contains("Pascal线"))); } #[test] - fn intercept_theorem_ratios() { - // three parallels y = 0, 1, 3 cut by x = 0 and y = x → ratios 1:2 on both - let ls = vec![ - NamedLine { name: "l".into(), a: 0.0, b: 1.0, c: 0.0 }, - NamedLine { name: "m".into(), a: 0.0, b: 1.0, c: 1.0 }, - NamedLine { name: "n".into(), a: 0.0, b: 1.0, c: 3.0 }, - NamedLine { name: "s".into(), a: 1.0, b: 0.0, c: 0.0 }, - NamedLine { name: "t".into(), a: 1.0, b: -1.0, c: 0.0 }, + fn brianchon_regular_hexagon() { + let s3 = 3f64.sqrt(); + let pts = vec![ + NamedPoint { name: "A".into(), p: vec![1.0, 0.0] }, + NamedPoint { name: "B".into(), p: vec![0.5, s3 / 2.0] }, + NamedPoint { name: "C".into(), p: vec![-0.5, s3 / 2.0] }, + NamedPoint { name: "D".into(), p: vec![-1.0, 0.0] }, + NamedPoint { name: "E".into(), p: vec![-0.5, -s3 / 2.0] }, + NamedPoint { name: "F".into(), p: vec![0.5, -s3 / 2.0] }, ]; - let out = lines_analyze(&ls, &[]); - let ratio = out.iter().find(|f| f.label == "\\text{成比例}").unwrap(); - assert!((ratio.num.unwrap() - 0.5).abs() < 1e-9); - let seg = out.iter().find(|f| f.label == "|s \\cap l,\\ s \\cap m|").unwrap(); - assert!((seg.num.unwrap() - 1.0).abs() < 1e-9); + let out = points_analyze(&pts); + assert!( + out.iter().any(|f| f.group == "布利安雄定理" && f.value.contains("共点")), + "Brianchon missing" + ); + assert!(out.iter().any(|f| f.group == "布利安雄定理" && f.label.contains("a+c+e"))); + assert!( + out.iter().any(|f| f.group == "帕斯卡定理" && f.value.contains("无穷远")), + "Pascal at infinity for regular hex" + ); } #[test] - fn similar_triangles_from_parallels() { - // parallels y = 2, y = 0... use y=0 and y=2 with transversals y=x and y=-x+1 meeting at (0.5,0.5) - let ls = vec![ - NamedLine { name: "p".into(), a: 0.0, b: 1.0, c: 0.0 }, - NamedLine { name: "q".into(), a: 0.0, b: 1.0, c: 2.0 }, - NamedLine { name: "s".into(), a: 1.0, b: -1.0, c: 0.0 }, - NamedLine { name: "t".into(), a: 1.0, b: 1.0, c: 1.0 }, + fn tetra_face_areas_and_altitudes() { + let pts = vec![ + NamedPoint { name: "A".into(), p: vec![0.0, 0.0, 0.0] }, + NamedPoint { name: "B".into(), p: vec![1.0, 0.0, 0.0] }, + NamedPoint { name: "C".into(), p: vec![0.0, 1.0, 0.0] }, + NamedPoint { name: "D".into(), p: vec![0.0, 0.0, 1.0] }, ]; - let out = lines_analyze(&ls, &[]); - let sim = out.iter().find(|f| f.label.starts_with("\\text{相似比}")).unwrap(); - assert!((sim.num.unwrap() - 1.0 / 3.0).abs() < 1e-9, "k = {}", sim.value); + let out = points_analyze(&pts); + let faces = out.iter().filter(|f| f.group == "四面体面" && f.label.starts_with("S_")).count(); + assert_eq!(faces, 4, "four face areas"); + let hs = out.iter().filter(|f| f.group == "四面体面" && f.label.starts_with("h_")).count(); + assert_eq!(hs, 4, "four altitudes"); + let sabc = out.iter().find(|f| f.label.contains("ABC") && f.label.starts_with("S_")).unwrap(); + assert!((sabc.num.unwrap() - 0.5).abs() < 1e-9); + let hd = out.iter().find(|f| f.label == "h_{D}").unwrap(); + assert!((hd.num.unwrap() - 1.0).abs() < 1e-9); // V=1/6, S_ABC=1/2 → h=1 + } + + #[test] + fn reflect_point_over_line_and_plane() { + let ls = vec![NamedLine { name: "l".into(), a: 0.0, b: 1.0, c: 0.0 }]; // y=0 + let pts = vec![NamedPoint { name: "P".into(), p: vec![1.0, 2.0] }]; + let out = lines_analyze(&ls, &pts); + let rp = out.iter().find(|f| f.group == "反射").expect("line reflection"); + assert!(rp.value.contains("1") && rp.value.contains("-2") || rp.value.contains("1,\\ -2") || rp.value.contains("(1"), "got {}", rp.value); + if let Some(Shape::Point { x, y, .. }) = rp.shape.as_ref() { + assert!((*x - 1.0).abs() < 1e-9 && (*y + 2.0).abs() < 1e-9); + } else { + panic!("expected point shape"); + } + let planes = vec![NamedPlane { name: "α".into(), n: [0.0, 0.0, 1.0], d: 0.0 }]; // z=0 + let q = vec![NamedPoint { name: "Q".into(), p: vec![1.0, 2.0, 3.0] }]; + let out3 = planes_analyze(&planes, &q); + let rq = out3.iter().find(|f| f.group == "反射").expect("plane reflection"); + if let Some(Shape::Point3 { x, y, z, .. }) = rq.shape.as_ref() { + assert!((*x - 1.0).abs() < 1e-9 && (*y - 2.0).abs() < 1e-9 && (*z + 3.0).abs() < 1e-9); + } else { + panic!("expected Point3"); + } } + + } diff --git a/index.html b/index.html index 97d93c4..98e803b 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,8 @@ - + + MathScrap diff --git a/mcp/ai-proxy.ts b/mcp/ai-proxy.ts index 3eda31f..e80e273 100644 --- a/mcp/ai-proxy.ts +++ b/mcp/ai-proxy.ts @@ -1,3 +1,4 @@ +import { t } from '../src/i18n/locale.ts' import type { IncomingMessage, ServerResponse } from 'node:http' import { readBody, sendJson } from './http.ts' @@ -12,18 +13,18 @@ const ALLOWED_HEADERS = new Set(['authorization', 'content-type', 'accept', 'x-a */ export async function forwardAi(payload: { url?: unknown; method?: unknown; headers?: unknown; body?: unknown }, fetchImpl: typeof fetch = fetch, signal?: AbortSignal) { const method = typeof payload.method === 'string' ? payload.method.toUpperCase() : 'GET' - if (method !== 'GET' && method !== 'POST') throw new Error('只允许 GET 或 POST') - if (typeof payload.url !== 'string' || !payload.url) throw new Error('缺少目标地址') + if (method !== 'GET' && method !== 'POST') throw new Error(t('proxyMethod')) + if (typeof payload.url !== 'string' || !payload.url) throw new Error(t('proxyTarget')) let target: URL try { target = new URL(payload.url) } catch { - throw new Error('目标地址不合法') + throw new Error(t('proxyBadTarget')) } - if (target.protocol !== 'http:' && target.protocol !== 'https:') throw new Error('只允许 http 或 https') - if (target.username || target.password) throw new Error('地址里不能带账号') - if (isBlockedHost(target.hostname)) throw new Error('拒绝访问该地址') - if (!allowedPath(target.pathname)) throw new Error('只转发模型列表、对话补全或 Messages 接口') + if (target.protocol !== 'http:' && target.protocol !== 'https:') throw new Error(t('proxyHttp')) + if (target.username || target.password) throw new Error(t('proxyUserinfo')) + if (isBlockedHost(target.hostname)) throw new Error(t('proxyBlocked')) + if (!allowedPath(target.pathname)) throw new Error(t('proxyPath')) const headers: Record = { accept: 'application/json' } if (payload.headers && typeof payload.headers === 'object') { for (const [key, value] of Object.entries(payload.headers as Record)) { diff --git a/mcp/bridge.ts b/mcp/bridge.ts index 686d9a5..398b590 100644 --- a/mcp/bridge.ts +++ b/mcp/bridge.ts @@ -1,3 +1,4 @@ +import { t } from '../src/i18n/locale.ts' import { randomUUID } from 'node:crypto' import type { IncomingMessage, ServerResponse } from 'node:http' import { readBody, sendEmpty, sendJson } from './http.ts' @@ -41,13 +42,13 @@ export function createBridge() { async function call(name: string, args: Record) { if (!browserLive()) { - return { ok: false as const, error: 'MathScrap 页面还没连上。先运行 npm run dev,并在本机浏览器打开 http://127.0.0.1:47321 。' } + return { ok: false as const, error: t('pageOffline') } } const id = randomUUID() const result = new Promise<{ ok: boolean; result?: unknown; error?: string }>((resolve) => { const timer = setTimeout(() => { pending.delete(id) - resolve({ ok: false, error: `工具 ${name} 等待页面响应超时` }) + resolve({ ok: false, error: t('toolTimeout', { name }) }) }, CALL_MS) pending.set(id, { resolve, timer }) }) diff --git a/package-lock.json b/package-lock.json index 1e9abaa..f555e0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mathscrap", - "version": "0.0.0", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mathscrap", - "version": "0.0.0", + "version": "0.1.0", "license": "Apache-2.0", "dependencies": { "@base-ui/react": "^1.9.0", @@ -14,6 +14,9 @@ "@fontsource/caveat": "^5.3.0", "@modelcontextprotocol/sdk": "^1.32.2", "@tailwindcss/vite": "^4.3.3", + "@tauri-apps/api": "2.12.4", + "@tauri-apps/plugin-dialog": "2.8.4", + "@tauri-apps/plugin-fs": "2.7.1", "class-variance-authority": "^0.7.1", "cn": "^0.5.1", "katex": "^0.19.1", @@ -29,6 +32,7 @@ "tw-animate-css": "^1.4.0" }, "devDependencies": { + "@tauri-apps/cli": "2.12.4", "@types/katex": "^0.16.8", "@types/node": "^24.19.2", "@types/plotly.js-dist-min": "^2.3.4", @@ -2553,6 +2557,251 @@ "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, + "node_modules/@tauri-apps/api": { + "version": "2.12.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.12.4.tgz", + "integrity": "sha512-pVmfis+4zT8H0Qhp3xPZx9Ju7kpzvA6rr/PSJPyuwfvucTQ8sVt61NYU9KMkK703UFlYadFmjKA4HhybOtN0gg==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.12.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.12.4.tgz", + "integrity": "sha512-JI9bHfudSRJikGOrjbXEm6RjzzvmKuLc+JixB1NE8hNzQkNlCCIC3pTES3uypkR9lxHrsbxzcnSwhI2IxBQphw==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.12.4", + "@tauri-apps/cli-darwin-x64": "2.12.4", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.12.4", + "@tauri-apps/cli-linux-arm64-gnu": "2.12.4", + "@tauri-apps/cli-linux-arm64-musl": "2.12.4", + "@tauri-apps/cli-linux-riscv64-gnu": "2.12.4", + "@tauri-apps/cli-linux-x64-gnu": "2.12.4", + "@tauri-apps/cli-linux-x64-musl": "2.12.4", + "@tauri-apps/cli-win32-arm64-msvc": "2.12.4", + "@tauri-apps/cli-win32-ia32-msvc": "2.12.4", + "@tauri-apps/cli-win32-x64-msvc": "2.12.4" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.12.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.12.4.tgz", + "integrity": "sha512-OxqKSckIkoJ6vX2AjifIJpTasFC0Hbdoo7gEiCmAjauABO3U8m2MRsLWe+QTiBwDx1mIrvgA+rvI7tvDnbnHVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.12.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.12.4.tgz", + "integrity": "sha512-fz76/77wMMJOaaUN6n3CybRG0P/MTEjjqYk81mGPyhi9m6Ue5T+LWBnJP/AkPooNatBPI2vtulGqomyMJ44lCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.12.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.12.4.tgz", + "integrity": "sha512-K0w8Bof6Lt8QKLAYhRy0yy36ZH5iA/35VuSOvnxyjvCUZ+A3JKsUTlALviH/sVgAlisaPzzmeJSH8XOn4Q2HBA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.12.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.12.4.tgz", + "integrity": "sha512-j5rOnjmRUwWrm1oosf0zBYl4D+fqydgUROFCk0hvKZxvbTT8SOERes0QB7rBV/XWNy+kM6pCKzb3oSm2sRglzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.12.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.12.4.tgz", + "integrity": "sha512-aOue3Fy6Jb7+oWcfAVUsVHfeTybSCt2CZoU06aO4jv4d2Ks/KnaTPwMAK3CLEduGOvBLY1lbBBrIyomqLLH5Sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.12.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.12.4.tgz", + "integrity": "sha512-Fx6GShY/RvST+t6LMlTHAwd66+1tEaAAGrnU7cysVfaiRP1BKng5Vl6rVmO6rSLOA1Sp1dRzNYM4f0OWQGb7qA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.12.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.12.4.tgz", + "integrity": "sha512-tNzZSa79Z1nRpHxEpQ38lsAFpKlFlYvoXzjTV+lRRn/zSf3wrdUC3wnLcq2XEadchPu4ttK0zivhYzr+XGD19Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.12.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.12.4.tgz", + "integrity": "sha512-jmZ1pr0xEqUhuEusWtt9Yt+GF6lCnGHUmIL+KIknpbjK/XQ2qkhDBhaxK/JMHM2A8dAacVXCxXJk2EXpwhQUfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.12.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.12.4.tgz", + "integrity": "sha512-fGo8pgEHZxdSYgZ/J98m1usJ7NHKmKbq7k6uT0x1rpOq6Bg2YVzzpaAblWk710f5LHYBJIL5QIFS5IrD+4gmTg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.12.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.12.4.tgz", + "integrity": "sha512-ezl8sQIcjYsye3TUnTH5VcWmWzccZASlQm7hlZt0dYyj1XD0qpVW47u2xYVrIKui0F8uQv/7QGZixdWiClBsbg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.12.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.12.4.tgz", + "integrity": "sha512-DJhDeCqjLDNMWYFy7ZS8cfeAHX2RK0RbQMLiA3bMVVkXRDJ4NBYiHriGqsUhsr9iG41aTb+dJGkFPmPw1gWwVw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/plugin-dialog": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.8.4.tgz", + "integrity": "sha512-+6ZOiXenOaxXwb8qCbBPdT2jX2cFPaVVHGIskrpmJLpmWj+qKRTD6X/z9x7hTdSQn7NedcoXCkWGej4a7dC4lw==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.12.0" + } + }, + "node_modules/@tauri-apps/plugin-fs": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.7.1.tgz", + "integrity": "sha512-4TBVjpi/q8PuNzjpkAc2iBGXMTLjOrAyfTiiOL8ORaJCZJRG3tjzmQ4sqs2CMG75FvLyRkOkERfP7AyY4iBNwQ==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.12.0" + } + }, "node_modules/@ts-morph/common": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.27.0.tgz", diff --git a/package.json b/package.json index 36b4380..b4025df 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,21 @@ { "name": "mathscrap", "private": true, - "version": "0.0.0", + "version": "0.1.0", "type": "module", "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "lint": "oxlint", + "lint": "oxlint --deny-warnings", "preview": "vite preview", + "test:pack": "tsx scripts/pack-smoke.ts && tsx scripts/i18n-smoke.ts", + "test:i18n": "tsx scripts/i18n-smoke.ts", + "pack:web": "tsc -b && vite build && node scripts/verify-packaged-dist.mjs", + "pack:desktop": "tauri build", + "pack:windows": "bash scripts/package-windows.sh", + "pack:android": "bash scripts/package-android.sh", + "pack:ios": "bash scripts/package-ios.sh", + "tauri": "tauri", "build:wasm": "cd crates/mathcore && wasm-pack build --target web --release --out-dir ../../src/wasm/pkg --out-name mathcore && rm -f ../../src/wasm/pkg/.gitignore", "test:rust": "cd crates/mathcore && cargo test", "smoke": "tsx scripts/smoke.ts", @@ -19,6 +27,9 @@ "e2e:sheet": "tsx scripts/e2e-sheet.ts" }, "dependencies": { + "@tauri-apps/api": "2.12.4", + "@tauri-apps/plugin-dialog": "2.8.4", + "@tauri-apps/plugin-fs": "2.7.1", "@base-ui/react": "^1.9.0", "@fontsource-variable/geist": "^5.3.0", "@fontsource/caveat": "^5.3.0", @@ -39,6 +50,7 @@ "tw-animate-css": "^1.4.0" }, "devDependencies": { + "@tauri-apps/cli": "2.12.4", "@types/katex": "^0.16.8", "@types/node": "^24.19.2", "@types/plotly.js-dist-min": "^2.3.4", diff --git a/packaging/icon-source.png b/packaging/icon-source.png new file mode 100644 index 0000000..2977800 Binary files /dev/null and b/packaging/icon-source.png differ diff --git a/packaging/icon-source.svg b/packaging/icon-source.svg new file mode 100644 index 0000000..370d252 --- /dev/null +++ b/packaging/icon-source.svg @@ -0,0 +1,3 @@ + + + diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..c3891f0 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.90.0" +profile = "minimal" diff --git a/scripts/ai-mcp-smoke.ts b/scripts/ai-mcp-smoke.ts index 33db709..8d1de95 100644 --- a/scripts/ai-mcp-smoke.ts +++ b/scripts/ai-mcp-smoke.ts @@ -144,7 +144,7 @@ try { assert.match(missingConfig(cfg({ provider: 'openai-compatible', baseUrl: ' ', model: ' ' })) ?? '', /Base URL/) assert.equal(missingConfig(cfg({ provider: 'local', baseUrl: base, model: 'demo-model' })), null) - assert.match(missingConfig(cfg({ provider: 'anthropic', baseUrl: '', model: 'claude' })) ?? '', /API Key/) + assert.match(missingConfig(cfg({ provider: 'anthropic', baseUrl: '', model: 'claude' })) ?? '', /API key/i) let calls = 0 const boom: AiTransport = { @@ -154,27 +154,27 @@ try { }, } const t0 = Date.now() - await assert.rejects(() => testConnection(cfg({ provider: 'openai-compatible', baseUrl: '', model: '' }), boom), /还没填写/) - await assert.rejects(() => testConnection(cfg({ provider: 'openai-compatible', baseUrl: 'ftp://example.com/v1', model: 'demo-model' }), boom), /http 或 https/) + await assert.rejects(() => testConnection(cfg({ provider: 'openai-compatible', baseUrl: '', model: '' }), boom), /still empty/) + await assert.rejects(() => testConnection(cfg({ provider: 'openai-compatible', baseUrl: 'ftp://example.com/v1', model: 'demo-model' }), boom), /http or https/) assert.equal(calls, 0) assert.ok(Date.now() - t0 < 200, 'empty config hung') const openai = await testConnection(cfg({ provider: 'openai-compatible', baseUrl: `${base}/v1`, model: 'demo-model', apiKey: OK_KEY })) - assert.match(openai, /OpenAI 兼容接口/) + assert.match(openai, /OpenAI-compatible/) assert.ok(hits.includes('GET /v1/models')) assert.ok(auths.includes(`Bearer ${OK_KEY}`)) - await assert.rejects(() => testConnection(cfg({ provider: 'openai-compatible', baseUrl: `${base}/v1`, model: 'missing-model', apiKey: OK_KEY })), /没有「missing-model」/) + await assert.rejects(() => testConnection(cfg({ provider: 'openai-compatible', baseUrl: `${base}/v1`, model: 'missing-model', apiKey: OK_KEY })), /missing-model/) const rejected = await testConnection(cfg({ provider: 'openai-compatible', baseUrl: `${base}/v1`, model: 'demo-model', apiKey: SECRET })).then( () => 'unexpected success', (e: unknown) => (e instanceof Error ? e.message : String(e)), ) - assert.equal(rejected, '密钥被拒绝(HTTP 401)') + assert.equal(rejected, 'The key was rejected (HTTP 401)') assert.ok(!rejected.includes('SHOULD-NOT-LEAK')) const local = await testConnection(cfg({ provider: 'local', baseUrl: base, model: 'demo-model' })) - assert.match(local, /本地模型/) + assert.match(local, /local model/) assert.ok(hits.includes('GET /v1/models')) const anthropic = await testConnection(cfg({ provider: 'anthropic', baseUrl: base, model: 'claude', apiKey: OK_KEY })) @@ -218,7 +218,7 @@ try { forwarded = true return new Response('no') }), - /拒绝访问/, + /refused/i, ) assert.equal(forwarded, false) @@ -258,7 +258,7 @@ const offline = await fetch(`${bridgeUrl}/__mathscrap/bridge/call`, { }) const offlineBody = (await offline.json()) as { ok: boolean; error?: string } assert.equal(offlineBody.ok, false) -assert.match(offlineBody.error ?? '', /还没连上/) +assert.match(offlineBody.error ?? '', /not connected/) assert.ok(Date.now() - offlineAt < 500, 'disconnected tool call hung') const state = { diff --git a/scripts/android-release-signing.mjs b/scripts/android-release-signing.mjs new file mode 100644 index 0000000..4f304f0 --- /dev/null +++ b/scripts/android-release-signing.mjs @@ -0,0 +1,62 @@ +/** + * Write keystore.properties from ANDROID_KEY_* and point the generated Gradle + * release build at it. Does not create a keystore. Prints no secret values. + */ +import { existsSync, readFileSync, writeFileSync } from 'node:fs' +import path from 'node:path' + +const alias = process.env.ANDROID_KEY_ALIAS ?? '' +const password = process.env.ANDROID_KEY_PASSWORD ?? '' +const b64 = process.env.ANDROID_KEY_BASE64 ?? '' +if (!alias || !password || !b64) { + console.error('ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD, and ANDROID_KEY_BASE64 are required.') + process.exit(2) +} + +const root = process.cwd() +const gen = path.join(root, 'src-tauri/gen/android') +const keystore = path.join(gen, 'upload-keystore.jks') +writeFileSync(keystore, Buffer.from(b64, 'base64'), { mode: 0o600 }) +writeFileSync( + path.join(gen, 'keystore.properties'), + `keyAlias=${alias}\npassword=${password}\nstoreFile=${keystore.replace(/\\/g, '/')}\n`, + { mode: 0o600 }, +) + +const gradle = path.join(gen, 'app/build.gradle.kts') +if (!existsSync(gradle)) { + console.error('Missing app/build.gradle.kts. Run tauri android init first.') + process.exit(1) +} +let text = readFileSync(gradle, 'utf8') +if (!text.includes('import java.io.FileInputStream') && !text.includes('java.io.FileInputStream')) { + text = `import java.io.FileInputStream\nimport java.util.Properties\n${text}` +} +if (!text.includes('signingConfigs')) { + const block = ` +signingConfigs { + create("release") { + val keystorePropertiesFile = rootProject.file("keystore.properties") + val keystoreProperties = Properties() + if (keystorePropertiesFile.exists()) { + keystoreProperties.load(FileInputStream(keystorePropertiesFile)) + } + keyAlias = keystoreProperties["keyAlias"] as String + keyPassword = keystoreProperties["password"] as String + storeFile = file(keystoreProperties["storeFile"] as String) + storePassword = keystoreProperties["password"] as String + } +} +` + const at = text.indexOf('buildTypes') + if (at < 0) { + console.error('build.gradle.kts has no buildTypes block; the Tauri Android template changed.') + process.exit(1) + } + text = `${text.slice(0, at)}${block}${text.slice(at)}` +} +if (!text.includes('signingConfig = signingConfigs.getByName("release")') && !text.includes('signingConfig = signingConfigs.getByName("release")')) { + text = text.replace(/getByName\("release"\)\s*\{/, 'getByName("release") {\n signingConfig = signingConfigs.getByName("release")') +} +writeFileSync(gradle, text) +console.log('Android release signing is wired to the supplied keystore. The keystore stays under src-tauri/gen and is gitignored.') diff --git a/scripts/browser-path.ts b/scripts/browser-path.ts new file mode 100644 index 0000000..6f2b5ab --- /dev/null +++ b/scripts/browser-path.ts @@ -0,0 +1,121 @@ +/** + * Find a Chrome/Chromium/Edge binary for the sheet and plot end-to-end scripts. + * + * Order: + * 1. CHROME_PATH, if set. A missing file is an error, not a silent fallback. + * 2. A Chromium already installed under PLAYWRIGHT_BROWSERS_PATH, or the + * default Playwright cache when that variable is unset. + * 3. google-chrome, chromium, or Edge on PATH. + * 4. The usual install locations on Linux, macOS, and Windows. + */ +import { execFileSync } from 'node:child_process' +import { accessSync, constants, existsSync, readdirSync, statSync } from 'node:fs' +import { homedir } from 'node:os' +import { delimiter, join } from 'node:path' + +export interface ResolvedBrowser { + executablePath: string + via: string +} + +function canRun(path: string): boolean { + try { + if (!statSync(path).isFile()) return false + accessSync(path, constants.X_OK) + return true + } catch { + return false + } +} + +function which(name: string): string | null { + try { + const out = execFileSync(process.platform === 'win32' ? 'where' : 'which', [name], { encoding: 'utf8' }) + const found = out.split(/\r?\n/).map((line) => line.trim()).find((line) => line && canRun(line)) + return found ?? null + } catch { + return null + } +} + +function chromiumUnder(root: string): string | null { + if (!root || !existsSync(root)) return null + let names: string[] + try { + names = readdirSync(root) + } catch { + return null + } + const hits: string[] = [] + for (const name of names) { + if (!name.startsWith('chromium')) continue + const base = join(root, name) + const guesses = [ + join(base, 'chrome-linux64', 'chrome'), + join(base, 'chrome-linux', 'chrome'), + join(base, 'chrome-mac-arm64', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'), + join(base, 'chrome-mac', 'Chromium.app', 'Contents', 'MacOS', 'Chromium'), + join(base, 'chrome-win64', 'chrome.exe'), + join(base, 'chrome-win', 'chrome.exe'), + ] + for (const guess of guesses) if (canRun(guess)) hits.push(guess) + } + hits.sort() + return hits.at(-1) ?? null +} + +function playwrightRoots(): { via: string; root: string }[] { + const env = process.env.PLAYWRIGHT_BROWSERS_PATH + if (env && env !== '0') return [{ via: 'PLAYWRIGHT_BROWSERS_PATH', root: env }] + if (env === '0') return [{ via: 'PLAYWRIGHT_BROWSERS_PATH=0', root: join(process.cwd(), 'node_modules', 'playwright-core', '.local-browsers') }] + return [ + { via: 'playwright cache', root: join(homedir(), '.cache', 'ms-playwright') }, + { via: 'playwright cache (macOS)', root: join(homedir(), 'Library', 'Caches', 'ms-playwright') }, + { via: 'playwright cache (Windows)', root: join(process.env.LOCALAPPDATA ?? '', 'ms-playwright') }, + ] +} + +function knownPaths(): { via: string; path: string }[] { + const pf = process.env.PROGRAMFILES ?? 'C:\\Program Files' + const pf86 = process.env['PROGRAMFILES(X86)'] ?? 'C:\\Program Files (x86)' + const local = process.env.LOCALAPPDATA ?? '' + return [ + { via: 'Linux Chrome', path: '/usr/bin/google-chrome' }, + { via: 'Linux Chrome stable', path: '/usr/bin/google-chrome-stable' }, + { via: 'Linux Chromium', path: '/usr/bin/chromium' }, + { via: 'Linux Chromium browser', path: '/usr/bin/chromium-browser' }, + { via: '/usr/local Chrome', path: '/usr/local/bin/google-chrome' }, + { via: '/usr/local Chromium', path: '/usr/local/bin/chromium' }, + { via: 'macOS Chrome', path: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' }, + { via: 'macOS Chromium', path: '/Applications/Chromium.app/Contents/MacOS/Chromium' }, + { via: 'macOS Edge', path: '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge' }, + { via: 'Windows Chrome', path: join(pf, 'Google', 'Chrome', 'Application', 'chrome.exe') }, + { via: 'Windows Chrome (x86)', path: join(pf86, 'Google', 'Chrome', 'Application', 'chrome.exe') }, + { via: 'Windows Edge', path: join(pf, 'Microsoft', 'Edge', 'Application', 'msedge.exe') }, + { via: 'Windows Chromium', path: join(local, 'Chromium', 'Application', 'chrome.exe') }, + ] +} + +const PATH_NAMES = ['google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser', 'microsoft-edge', 'msedge', 'chrome'] + +export function resolveBrowser(): ResolvedBrowser { + const override = process.env.CHROME_PATH?.trim() + if (override) { + if (!canRun(override)) throw new Error(`CHROME_PATH is set but is not an executable file: ${override}`) + return { executablePath: override, via: 'CHROME_PATH' } + } + for (const root of playwrightRoots()) { + const found = chromiumUnder(root.root) + if (found) return { executablePath: found, via: root.via } + } + for (const name of PATH_NAMES) { + const found = which(name) + if (found) return { executablePath: found, via: `PATH ${name}` } + } + for (const item of knownPaths()) { + if (canRun(item.path)) return { executablePath: item.path, via: item.via } + } + throw new Error( + `No Chrome, Chromium, or Edge binary found. Set CHROME_PATH, install Chrome on PATH, or set PLAYWRIGHT_BROWSERS_PATH to a Playwright browser directory. PATH=${process.env.PATH?.split(delimiter).slice(0, 8).join(delimiter) ?? ''}`, + ) +} diff --git a/scripts/checksum-artifacts.mjs b/scripts/checksum-artifacts.mjs new file mode 100644 index 0000000..28cdd44 --- /dev/null +++ b/scripts/checksum-artifacts.mjs @@ -0,0 +1,16 @@ +import { createHash } from 'node:crypto' +import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs' +import path from 'node:path' + +const dir = path.resolve(process.argv[2] ?? 'packaging/out') +const names = readdirSync(dir) + .filter((name) => name !== 'SHA256SUMS') + .filter((name) => statSync(path.join(dir, name)).isFile()) + .sort() +const lines = names.map((name) => { + const file = path.join(dir, name) + const sha = createHash('sha256').update(readFileSync(file)).digest('hex') + return `${sha} ${name}` +}) +writeFileSync(path.join(dir, 'SHA256SUMS'), `${lines.join('\n')}\n`) +console.log(lines.join('\n')) diff --git a/scripts/compact-debug-apk.py b/scripts/compact-debug-apk.py new file mode 100644 index 0000000..eb2487d --- /dev/null +++ b/scripts/compact-debug-apk.py @@ -0,0 +1,21 @@ +"""Rewrite a debug APK without the large alignment padding Gradle sometimes leaves. + +The result is unsigned. The caller zip-aligns it and signs it with the Android +debug keystore. This does not create a release key. +""" +import sys +import zipfile +from pathlib import Path + +src = Path(sys.argv[1]) +dst = Path(sys.argv[2]) +with zipfile.ZipFile(src) as zin, zipfile.ZipFile(dst, "w") as zout: + for info in zin.infolist(): + data = zin.read(info.filename) + out = zipfile.ZipInfo(filename=info.filename, date_time=info.date_time) + if info.filename.endswith(".so"): + out.compress_type = zipfile.ZIP_STORED + else: + out.compress_type = zipfile.ZIP_DEFLATED + zout.writestr(out, data) +print(dst) diff --git a/scripts/e2e-plots.ts b/scripts/e2e-plots.ts index 43c5d07..19a6280 100644 --- a/scripts/e2e-plots.ts +++ b/scripts/e2e-plots.ts @@ -11,14 +11,20 @@ import { mkdirSync } from 'node:fs' import { writeFile } from 'node:fs/promises' import { chromium, type Page } from 'playwright-core' import { PNG } from 'pngjs' +import { resolveBrowser } from './browser-path.ts' import { cases } from './plot-cases' const URL = (process.env.APP_URL ?? 'http://127.0.0.1:47321/').includes('?') ? process.env.APP_URL! : `${process.env.APP_URL ?? 'http://127.0.0.1:47321/'}?static=1` -const CHROME = process.env.CHROME_PATH ?? '/usr/local/bin/google-chrome' +const BROWSER = resolveBrowser() +console.log(`browser ${BROWSER.executablePath} (${BROWSER.via})`) const SHOTS = process.env.SHOTS ?? '/tmp/mathscrap-e2e' mkdirSync(SHOTS, { recursive: true }) -/** Pixels clearly off the kraft palette: saturated and not in the brown hue band. */ +/** + * Marks on the kraft sheet. Curve ink is dark brown (#2e2116), which is not a + * saturated non-brown hue, so darkness against the kraft paper counts too. + * Saturated marks outside the kraft hue band (red intersections, blue chords) still count. + */ function inkPixels(png: Buffer) { const img = PNG.sync.read(png) let n = 0 @@ -29,6 +35,11 @@ function inkPixels(png: Buffer) { const max = Math.max(r, g, b) const min = Math.min(r, g, b) const d = max - min + const lum = 0.3 * r + 0.59 * g + 0.11 * b + if (lum < 0.42 && max < 0.62) { + n++ + continue + } if (d < 0.18 || max < 0.12) continue let h = max === r ? ((g - b) / d) % 6 : max === g ? (b - r) / d + 2 : (r - g) / d + 4 h = (h * 60 + 360) % 360 @@ -71,7 +82,7 @@ async function probe(page: Page): Promise { } async function run(viewport: { width: number; height: number }, subset?: number[]) { - const browser = await chromium.launch({ executablePath: CHROME, args: ['--no-sandbox', '--use-gl=swiftshader', '--enable-webgl', '--ignore-gpu-blocklist'] }) + const browser = await chromium.launch({ executablePath: BROWSER.executablePath, args: ['--no-sandbox', '--use-gl=swiftshader', '--enable-webgl', '--ignore-gpu-blocklist'] }) const page = await browser.newPage({ viewport, isMobile: viewport.width < 500, hasTouch: viewport.width < 500 }) const errors: string[] = [] page.on('pageerror', (e) => errors.push(e.message)) diff --git a/scripts/e2e-sheet.ts b/scripts/e2e-sheet.ts index bc972d2..6c63648 100644 --- a/scripts/e2e-sheet.ts +++ b/scripts/e2e-sheet.ts @@ -8,13 +8,15 @@ */ import { mkdirSync } from 'node:fs' import { chromium, type Page } from 'playwright-core' -import { arrowLetter, inkFixtures, pointPair, proseNote, shiftInk } from '../src/ocr/comer/samples.ts' +import { resolveBrowser } from './browser-path.ts' +import { arrowLetter, inkFixtures, pointPair, proseNote, shiftInk, sumBar, sumGlyph } from '../src/ocr/comer/samples.ts' import type { InkStroke } from '../src/ocr/comer/preprocess.ts' const RAW = process.env.APP_URL ?? 'http://127.0.0.1:47321/' /** ?static=1 skips the progressive reveal so trace assertions see the finished figure. */ const URL = RAW.includes('?') ? RAW : `${RAW}?static=1` -const CHROME = process.env.CHROME_PATH ?? '/usr/local/bin/google-chrome' +const BROWSER = resolveBrowser() +console.log(`browser ${BROWSER.executablePath} (${BROWSER.via})`) const SHOTS = process.env.SHOTS ?? '/tmp/mathscrap-e2e' mkdirSync(SHOTS, { recursive: true }) @@ -53,11 +55,12 @@ async function write(page: Page, text: string) { } async function desktop() { - const browser = await chromium.launch({ executablePath: CHROME, args: ['--no-sandbox', '--use-gl=swiftshader', '--enable-webgl', '--ignore-gpu-blocklist'] }) + const browser = await chromium.launch({ executablePath: BROWSER.executablePath, args: ['--no-sandbox', '--use-gl=swiftshader', '--enable-webgl', '--ignore-gpu-blocklist'] }) const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }) const errors: string[] = [] page.on('pageerror', (e) => errors.push(e.message)) await page.addInitScript(() => { + localStorage.setItem('mathscrap.lang', 'zh') if (!sessionStorage.getItem('seeded')) { localStorage.removeItem('mathscrap.doc.v2') sessionStorage.setItem('seeded', '1') @@ -167,9 +170,12 @@ async function desktop() { } async function phone() { - const browser = await chromium.launch({ executablePath: CHROME, args: ['--no-sandbox', '--use-gl=swiftshader', '--enable-webgl', '--ignore-gpu-blocklist'] }) + const browser = await chromium.launch({ executablePath: BROWSER.executablePath, args: ['--no-sandbox', '--use-gl=swiftshader', '--enable-webgl', '--ignore-gpu-blocklist'] }) const page = await browser.newPage({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true }) - await page.addInitScript(() => localStorage.removeItem('mathscrap.doc.v2')) + await page.addInitScript(() => { + localStorage.setItem('mathscrap.lang', 'zh') + localStorage.removeItem('mathscrap.doc.v2') + }) await page.goto(URL, { waitUntil: 'networkidle' }) await page.waitForFunction(() => !!(window as any).mathscrap) await page.screenshot({ path: `${SHOTS}/sheet-phone-blank.png` }) @@ -197,11 +203,14 @@ async function phone() { * the progressive reveal of statements, and the automatic handwriting chip. */ async function dockAndLive() { - const browser = await chromium.launch({ executablePath: CHROME, args: ['--no-sandbox', '--use-gl=swiftshader', '--enable-webgl', '--ignore-gpu-blocklist'] }) + const browser = await chromium.launch({ executablePath: BROWSER.executablePath, args: ['--no-sandbox', '--use-gl=swiftshader', '--enable-webgl', '--ignore-gpu-blocklist'] }) const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }) const errors: string[] = [] page.on('pageerror', (e) => errors.push(e.message)) - await page.addInitScript(() => localStorage.removeItem('mathscrap.doc.v2')) + await page.addInitScript(() => { + localStorage.setItem('mathscrap.lang', 'zh') + localStorage.removeItem('mathscrap.doc.v2') + }) await page.goto(URL, { waitUntil: 'networkidle' }) await page.waitForFunction(() => !!(window as any).mathscrap) await page.evaluate(() => (window as any).mathscrap.tools.call('page_add_formula', { text: 'y = x^2 - 2' })) @@ -264,7 +273,10 @@ async function dockAndLive() { // 4. progressive reveal: without ?static the figure emerges statement by statement const page2 = await browser.newPage({ viewport: { width: 1440, height: 900 }, reducedMotion: 'no-preference' }) page2.on('pageerror', (e) => errors.push(e.message)) - await page2.addInitScript(() => localStorage.removeItem('mathscrap.doc.v2')) + await page2.addInitScript(() => { + localStorage.setItem('mathscrap.lang', 'zh') + localStorage.removeItem('mathscrap.doc.v2') + }) await page2.goto(RAW, { waitUntil: 'networkidle' }) await page2.waitForFunction(() => !!(window as any).mathscrap) await page2.evaluate(() => (window as any).mathscrap.tools.call('page_add_formula', { text: 'y = x^2 - 2\ny = 0.5x + 1\nx^2 + y^2 = 4' })) @@ -304,18 +316,25 @@ async function dockAndLive() { ...fixtures.map((f, i) => shiftInk(f.strokes, 120, 80 + i * 420)), shiftInk(proseNote(), 560, 80), shiftInk(arrowLetter(), 560, 520), + shiftInk(sumGlyph(), 980, 520), + shiftInk(sumBar(), 980, 80), shiftInk(pointPair(), 120, 1500), ]) - await page3.addInitScript((saved) => localStorage.setItem('mathscrap.doc.v2', saved), JSON.stringify(doc)) + await page3.addInitScript((saved) => { + localStorage.setItem('mathscrap.lang', 'zh') + localStorage.setItem('mathscrap.doc.v2', saved) + }, JSON.stringify(doc)) await page3.goto(RAW, { waitUntil: 'networkidle' }) - await page3.waitForFunction(() => document.documentElement.dataset.ocr === 'idle', null, { timeout: 40000 }) + await page3.waitForFunction(() => document.documentElement.dataset.ocr === 'idle', null, { timeout: 60000 }) const read = await page3.locator('[data-formula]').evaluateAll((els) => els.map((el) => el.getAttribute('data-formula-text') ?? '')) const vecs = read.filter((t) => /^\\vec\{[a-z]\} = \(/.test(t)) + const sums = read.filter((t) => /^\\sum(?![A-Za-z])/.test(t)) const points = read.filter((t) => /^[A-Z]\([^)]+\)$/.test(t)) - const math = read.filter((t) => !vecs.includes(t) && !points.includes(t)) + const math = read.filter((t) => !vecs.includes(t) && !points.includes(t) && !sums.includes(t)) check(math.length === 3 && fixtures.every((f) => math.filter((t) => f.expect.test(t)).length === 1), `ocr: model writes y = 2, y = x, a^2 = b (${read.join(' | ')})`) check(points.some((t) => /^A\(2,\s*2\)$/.test(t)), `ocr: handwritten point lands as A(2, 2) (${points.join(' | ') || 'none'})`) - check(vecs.length <= 1 && vecs.every((t) => !/1,\s*2,\s*2/.test(t)), `ocr: over-arrow stays a vector (${vecs.join(' | ') || 'ink left'})`) + check(vecs.length === 1 && vecs.every((t) => !/1,\s*2,\s*2/.test(t)), `ocr: over-arrow is written as a vector (${vecs.join(' | ') || 'ink left'})`) + check(sums.length === 2, `ocr: summation is written (${sums.join(' | ') || 'none'})`) check(!read.some((t) => /1,\s*2,\s*2/.test(t)), 'ocr: model does not invent a component triple') check(math.length === 3, `ocr: a line of letters is not written as a formula (${read.join(' | ')})`) check((await page3.locator('[data-ink-chip]').count()) === 0, 'ocr: acceptance does not put a chip on the paper') @@ -343,9 +362,49 @@ function boardDoc(groups: InkStroke[][]) { } } +async function englishLocale() { + const browser = await chromium.launch({ executablePath: BROWSER.executablePath, args: ['--no-sandbox', '--use-gl=swiftshader', '--enable-webgl', '--ignore-gpu-blocklist'] }) + const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }) + await page.addInitScript(() => { + // localStorage is already restored when this runs on reload. Seed English only the first time. + if (localStorage.getItem('e2e-lang-ready') === '1') return + localStorage.setItem('mathscrap.lang', 'en') + localStorage.removeItem('mathscrap.doc.v2') + }) + await page.goto(URL, { waitUntil: 'networkidle' }) + await page.evaluate(() => localStorage.setItem('e2e-lang-ready', '1')) + await page.waitForFunction(() => !!(window as any).mathscrap) + await page.getByRole('button', { name: 'Open coordinates' }).waitFor() + check((await page.getByRole('button', { name: '打开坐标系' }).count()) === 0, 'en: the corner button is not Chinese') + await page.mouse.dblclick(420, 260) + const ta = page.getByRole('textbox', { name: 'Write a formula' }) + await ta.waitFor({ state: 'visible' }) + await ta.pressSequentially('y = log_2(x)', { delay: 5 }) + await page.keyboard.press('Enter') + await ta.waitFor({ state: 'detached' }) + const written = await page.locator('[data-formula]').innerText() + check(/log_?2|\\log/.test(written) || written.includes('log'), `en: the formula is not translated (${written.replace(/\s+/g, ' ')})`) + await page.getByRole('button', { name: 'More' }).click() + await page.getByRole('button', { name: '中文', pressed: false }).click() + await page.getByRole('button', { name: '打开坐标系' }).waitFor() + const still = await page.locator('[data-formula]').innerText() + check(still === written, 'en→zh: the written formula stays the same') + await page.waitForFunction(() => (localStorage.getItem('mathscrap.doc.v2') ?? '').includes('log_2')) + await page.reload({ waitUntil: 'networkidle' }) + await page.waitForFunction(() => !!(window as any).mathscrap) + await page.getByRole('button', { name: '打开坐标系' }).waitFor() + check((await page.locator('[data-formula]').innerText()) === still, 'zh persists across reload and the formula is unchanged') + await page.getByRole('button', { name: '更多' }).click() + await page.getByRole('button', { name: 'EN', pressed: false }).click() + await page.getByRole('button', { name: 'Open coordinates' }).waitFor() + check((await page.locator('[data-formula]').innerText()) === still, 'zh→en: the formula is still unchanged') + await browser.close() +} + if (!process.env.SKIP_BASE) { await desktop() await phone() + await englishLocale() } await dockAndLive() console.log(problems.length ? `\n${problems.length} problem(s)` : `\nall sheet checks passed · screenshots in ${SHOTS}`) diff --git a/scripts/i18n-smoke.ts b/scripts/i18n-smoke.ts new file mode 100644 index 0000000..1aaee6c --- /dev/null +++ b/scripts/i18n-smoke.ts @@ -0,0 +1,111 @@ +/** + * Locale catalog parity, English-mode leftovers, and saved-language behavior. + * Formula text and sample blocks must stay byte-for-byte when the language changes. + */ +import assert from 'node:assert/strict' +import { readdirSync, readFileSync, statSync } from 'node:fs' +import { join } from 'node:path' +import { SAMPLES } from '../src/board/samples.ts' +import { localizeGeo } from '../src/i18n/geometry.ts' +import { applyLang, getLang, LANG_KEY, readSavedLang, resolveLocale, t, writeSavedLang } from '../src/i18n/locale.ts' +import { en, zh, type MessageKey } from '../src/i18n/messages.ts' + +const HAN = /\p{Script=Han}/u +const zhKeys = Object.keys(zh) as MessageKey[] +const enKeys = Object.keys(en) +assert.deepEqual([...enKeys].sort(), [...zhKeys].sort(), 'zh and en keys differ') +for (const key of zhKeys) assert.equal(typeof en[key], 'string', key) + +/** English strings may quote Chinese input syntax. The language name on the switch stays 中文. */ +const HAN_IN_EN = new Set(['langZh', 'errThrough', 'errTangent', 'errChord', 'errCircleThrough', 'errCircleBit', 'errCircleNeed']) +const leaked = zhKeys.filter((key) => !HAN_IN_EN.has(key) && HAN.test(en[key])) +assert.deepEqual(leaked, [], `English catalog still has Chinese: ${leaked.join(', ')}`) +assert.equal(en.langZh, '中文') + +assert.equal(resolveLocale(null, 'zh-CN'), 'zh') +assert.equal(resolveLocale(null, 'zh-Hans-CN'), 'zh') +assert.equal(resolveLocale(undefined, 'en-US'), 'en') +assert.equal(resolveLocale('', 'fr-FR'), 'en') +assert.equal(resolveLocale(null, undefined), 'en') +assert.equal(resolveLocale('en', 'zh-CN'), 'en') +assert.equal(resolveLocale('zh', 'en-US'), 'zh') +assert.equal(resolveLocale('fr', 'zh-CN'), 'zh') + +const store = new Map() +Object.defineProperty(globalThis, 'localStorage', { + configurable: true, + value: { + getItem: (key: string) => store.get(key) ?? null, + setItem: (key: string, value: string) => store.set(key, value), + removeItem: (key: string) => store.delete(key), + }, +}) + +writeSavedLang('en') +assert.equal(readSavedLang(), 'en') +assert.equal(store.get(LANG_KEY), 'en') +writeSavedLang('zh') +assert.equal(resolveLocale(readSavedLang(), 'en-US'), 'zh') + +const formula = 'y = log_2(x)' +const blocks = SAMPLES.map((s) => s.blocks.join('\n')) +applyLang('zh') +assert.equal(getLang(), 'zh') +assert.equal(t('openCoordinates'), '打开坐标系') +assert.equal(t('details'), '详情') +assert.match(t('wroteMany', { n: 2 }), /2/) +applyLang('en') +assert.equal(t('openCoordinates'), 'Open coordinates') +assert.equal(t('details'), 'Details') +assert.equal(t('langZh'), '中文') +assert.equal(formula, 'y = log_2(x)') +assert.deepEqual(SAMPLES.map((s) => s.blocks.join('\n')), blocks) +assert.ok(SAMPLES.some((s) => s.blocks.includes('y = log_2(x)'))) +assert.equal(t('domainLogb').includes('log'), true) +applyLang('en') +assert.equal(localizeGeo('余弦定理'), 'Law of cosines') +assert.equal(localizeGeo('y = x^2'), 'y = x^2') +assert.equal(localizeGeo('函数 f(x)'), 'Function f(x)') +assert.match(localizeGeo('第 2 / 3 组解'), /solution 2 \/ 3/) +applyLang('zh') +assert.equal(localizeGeo('余弦定理'), '余弦定理') +applyLang('en') + +const allow = new Set([ + 'src/i18n/messages.ts', + 'src/ai/client.ts', + 'src/ai/specs.ts', + 'src/math/statements.ts', + 'src/math/scene.ts', + 'src/input/snippets.ts', + 'src/board/samples.ts', + 'src/ocr/comer/source.ts', + // Syntax examples and engine labels. FactsPanel quotes 过(1, 3). figure.ts matches 圆心/中心 from the Rust facts. + 'src/plot/FactsPanel.tsx', + 'src/plot/figure.ts', + // Chinese keys are the engine's wording. English values are the screen text. + 'src/i18n/geometry.ts', +]) +const hits: string[] = [] +function walk(dir: string) { + for (const name of readdirSync(dir)) { + const path = join(dir, name) + if (statSync(path).isDirectory()) { + if (name === 'pkg' || name === 'node_modules') continue + walk(path) + continue + } + if (!/\.(ts|tsx)$/.test(name)) continue + const rel = path.replace(/\\/g, '/').replace(/^src\//, 'src/') + if (allow.has(rel)) continue + const text = readFileSync(path, 'utf8') + text.split('\n').forEach((line, i) => { + const code = line.replace(/\/\*.*\*\//g, '').replace(/\/\/.*$/, '') + if (HAN.test(code)) hits.push(`${rel}:${i + 1}: ${line.trim()}`) + }) + } +} +walk('src') +assert.deepEqual(hits, [], `Chinese remains outside the catalogs:\n${hits.join('\n')}`) + +console.log(`i18n ok · ${zhKeys.length} keys · saved language wins · formulas unchanged`) diff --git a/scripts/make-icon.mjs b/scripts/make-icon.mjs new file mode 100644 index 0000000..ab0bf42 --- /dev/null +++ b/scripts/make-icon.mjs @@ -0,0 +1,20 @@ +/** + * Flat icon in the kraft color already used by `.kraft-page` (#c9a473). + * The repo has no MathScrap logomark. public/favicon.svg is the Vite template + * and is not reused here. + */ +import { PNG } from 'pngjs' +import { createWriteStream } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const out = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../packaging/icon-source.png') +const size = 1024 +const png = new PNG({ width: size, height: size }) +for (let i = 0; i < png.data.length; i += 4) { + png.data[i] = 0xc9 + png.data[i + 1] = 0xa4 + png.data[i + 2] = 0x73 + png.data[i + 3] = 255 +} +png.pack().pipe(createWriteStream(out)).on('finish', () => console.log(out)) diff --git a/scripts/ocr-live.ts b/scripts/ocr-live.ts index 4f02dd0..689c77a 100644 --- a/scripts/ocr-live.ts +++ b/scripts/ocr-live.ts @@ -16,7 +16,8 @@ import type { ComerSession, ComerTensor } from '../src/ocr/comer/decode.ts' import { recognizeStrokes } from '../src/ocr/comer/engine.ts' import { preprocessStrokes } from '../src/ocr/comer/preprocess.ts' import { classifyInk } from '../src/ocr/gate.ts' -import { arrowLetter, inkFixtures, pointPair, proseNote } from '../src/ocr/comer/samples.ts' +import { OCR_ACCEPT } from '../src/ocr/threshold.ts' +import { arrowLetter, inkFixtures, isolatedSigma, pointPair, proseNote, sumBar, sumGlyph } from '../src/ocr/comer/samples.ts' import { vocabFromJson } from '../src/ocr/comer/source.ts' const ort = await import('onnxruntime-web/wasm') @@ -53,7 +54,28 @@ for (const width of [3, 5]) { assert.equal(classifyInk(proseNote()), 'text') const arrowHits = await recognizeStrokes(arrowLetter(), encoder, decoder, vocab, 2) console.log('arrow', arrowHits.map((h) => `${h.text} (${h.confidence.toFixed(2)})`).join(' | ') || '(empty)') -if (arrowHits[0]) assert.match(arrowHits[0].text, /^\\vec\{[a-z]\} = \(/, arrowHits[0].text) +assert.ok(arrowHits[0], 'over-arrow produced no formula') +assert.match(arrowHits[0].text, /^\\vec\{[a-z]\} = \(/, arrowHits[0].text) +assert.ok(arrowHits[0].confidence >= OCR_ACCEPT, `arrow confidence ${arrowHits[0].confidence} (raw model score on this ink is ~0.48)`) +const bareHits = await recognizeStrokes(arrowLetter().slice(0, 2), encoder, decoder, vocab, 2) +console.log('bare', bareHits.map((h) => `${h.text} (${h.confidence.toFixed(2)})`).join(' | ') || '(empty)') +assert.ok(!bareHits.some((h) => /\\vec\{/.test(h.text) || /^[A-Z]\(/.test(h.text)), `bare letter became a vector or point (${bareHits.map((h) => h.text).join(' | ')})`) +for (const sample of [ + { name: 'sum', strokes: sumGlyph() }, + { name: 'sum-bar', strokes: sumBar() }, +]) { + assert.equal(classifyInk(sample.strokes), 'formula', sample.name) + const hits = await recognizeStrokes(sample.strokes, encoder, decoder, vocab, 2) + console.log(sample.name, hits.map((h) => `${h.text} (${h.confidence.toFixed(2)})`).join(' | ') || '(empty)') + assert.match(hits[0]?.text ?? '', /^\\sum(?![A-Za-z])/, `${sample.name} ${hits[0]?.text}`) + assert.ok((hits[0]?.confidence ?? 0) >= OCR_ACCEPT, `${sample.name} confidence ${hits[0]?.confidence}`) + assert.ok(!hits.some((h) => /\\vec\{/.test(h.text)), `${sample.name} became a vector`) +} +const sigmaHits = await recognizeStrokes(isolatedSigma(), encoder, decoder, vocab, 2) +console.log('sigma', sigmaHits.map((h) => `${h.text} (${h.confidence.toFixed(2)})`).join(' | ') || '(empty)') +assert.equal(sigmaHits[0]?.text, 'sigma', sigmaHits.map((h) => h.text).join(' | ')) +assert.ok((sigmaHits[0]?.confidence ?? 0) >= OCR_ACCEPT, `isolated sigma confidence ${sigmaHits[0]?.confidence}`) +assert.ok(!sigmaHits.some((h) => /\\vec\{|^[A-Z]\(/.test(h.text)), 'isolated sigma became a vector or point') const pointHits = await recognizeStrokes(pointPair(), encoder, decoder, vocab, 2) console.log('point', pointHits.map((h) => `${h.text} (${h.confidence.toFixed(2)})`).join(' | ') || '(empty)') assert.match(pointHits[0]?.text ?? '', /^A\(2, 2\)$/) diff --git a/scripts/ocr-smoke.ts b/scripts/ocr-smoke.ts index a987c0c..1b546ad 100644 --- a/scripts/ocr-smoke.ts +++ b/scripts/ocr-smoke.ts @@ -14,10 +14,11 @@ import { readFileSync } from 'node:fs' import type { Stroke } from '../src/board/types.ts' import type { ComerSession, ComerTensor } from '../src/ocr/comer/decode.ts' import { bindInk, findOverArrow } from '../src/ocr/comer/bind.ts' -import { classifyInk } from '../src/ocr/gate.ts' -import { recognizeStrokes } from '../src/ocr/comer/engine.ts' +import { classifyInk, looksLikeIsolatedSigma } from '../src/ocr/gate.ts' +import { agreeConfidence, recognizeStrokes } from '../src/ocr/comer/engine.ts' +import { OCR_ACCEPT } from '../src/ocr/threshold.ts' import { isInkMeaningful, preprocessStrokes, resamplePoints, type InkStroke } from '../src/ocr/comer/preprocess.ts' -import { arrowLetter, inkFixtures, proseNote } from '../src/ocr/comer/samples.ts' +import { arrowLetter, inkFixtures, isolatedSigma, proseNote, sumBar, sumGlyph } from '../src/ocr/comer/samples.ts' import { tokensToSource, vocabFromJson } from '../src/ocr/comer/source.ts' import { filterAbsurd, hasParenGeometry, recognizeInk } from '../src/ocr/inkShape.ts' @@ -310,6 +311,15 @@ assert.equal(tokensToSource(['a', '^', '{', '2', '}', '=', 'b']), 'a^2 = b') assert.equal(tokensToSource(['\\frac', '{', 'a', '}', '{', 'b', '}']), '(a)/(b)') assert.equal(tokensToSource(['\\sqrt', '{', 'x', '}']), 'sqrt(x)') assert.equal(tokensToSource(['\\vec', '{', 'a', '}']), '\\vec{a}') +assert.equal(tokensToSource(['\\sum', '_', '{', 'i', '=', '1', '}', '^', '{', 'n', '}', 'i']), '\\sum_{i = 1}^{n} i') +assert.equal(tokensToSource(['y', '=', '\\sum', '_', '{', 'k', '=', '1', '}', '^', '{', 'n', '}', 'k']), 'y = \\sum_{k = 1}^{n} k') +assert.equal(tokensToSource(['\\sum', 'i']), '\\sum i') +assert.equal(tokensToSource(['\\sum', '\\limits', '_', '{', 'L', '-', '1', '}', '^', '{', 'r', '}']), '\\sum_{L - 1}^{r}') +assert.equal(tokensToSource(['\\int', '_', '{', '0', '}', '^', '{', '1', '}', 'x']), '\\int_{0}^{1} x') +assert.equal(tokensToSource(['\\sigma']), 'sigma') +assert.equal(tokensToSource(['\\pi']), 'pi') +assert.equal(tokensToSource(['\\theta']), 'theta') +assert.equal(tokensToSource(['y', '=', '\\sigma']), 'y = sigma') assert.equal(tokensToSource(['\\overrightarrow', '{', 'b', '}']), '\\vec{b}') assert.equal(tokensToSource(['=']), '') assert.equal(tokensToSource(['\\frac']), '') @@ -348,7 +358,12 @@ assert.ok(image.tensor.some((v) => v > 0.5), 'white ink') assert.ok(image.mask.includes(0) && image.mask.includes(1)) assert.equal(isInkMeaningful([{ points: [{ x: 0, y: 0 }, { x: 2, y: 1 }], lineWidth: 3 }]), false) -function scripted(tokens: number[]): ComerSession { +function logitFor(p: number) { + const rest = vocab.vocab_size - 1 + return Math.log((p / (1 - p)) * rest) +} + +function scripted(tokens: number[], logit = 6): ComerSession { return { async run(feeds): Promise> { if (feeds.pixel_values) { @@ -361,7 +376,7 @@ function scripted(tokens: number[]): ComerSession { if (!(ids instanceof BigInt64Array)) throw new Error('missing input_ids') const want = tokens[ids.length - 1] ?? eos const logits = new Float32Array(ids.length * vocab.vocab_size) - logits[(ids.length - 1) * vocab.vocab_size + want] = 6 + logits[(ids.length - 1) * vocab.vocab_size + want] = logit return { logits: { type: 'float32', data: logits, dims: [1, ids.length, vocab.vocab_size] } } }, } @@ -425,13 +440,95 @@ const arrowed = [ const dir = findOverArrow(arrowed) assert.ok(dir && dir.x > 0.9 && Math.abs(dir.y) < 0.2, JSON.stringify(dir)) const boundVec = bindInk(arrowed, 'a') -assert.match(boundVec, /^\\vec\{a\} = \([0-9.\-]+, [0-9.\-]+\)$/) +assert.match(boundVec, /^\\vec\{a\} = \([0-9.-]+, [0-9.-]+\)$/) assert.equal(bindInk(arrowed, 'a = (1, 2, 2)'), boundVec) assert.doesNotMatch(boundVec, /1,\s*2,\s*2/) +assert.equal(bindInk(arrowed, '\\sum'), '\\sum') +assert.equal(bindInk(arrowed, '\\sum_{i = 1}^{n} i'), '\\sum_{i = 1}^{n} i') +assert.equal(bindInk(arrowed, 'y = 2'), 'y = 2') +assert.equal(bindInk(y2strokes, '\\vec{a}'), '') +assert.equal(bindInk(y2strokes, 'b'), '') +assert.equal(bindInk(arrowed, 'sigma'), 'sigma') +assert.equal(bindInk(arrowed, 'pi'), 'pi') console.log('ocr bind ok', { point: bindInk(y2strokes, 'A(1,2)'), vec: boundVec }) +const midVec = '\\vec{a} = (1, 0.02)' +assert.equal(agreeConfidence(arrowed, midVec, 0.48), 0.78) +assert.equal(agreeConfidence(arrowed, midVec, 0.3), 0.3) +assert.equal(agreeConfidence(y2strokes, midVec, 0.9), 0.9) +assert.equal(agreeConfidence(arrowed, 'y = 2', 0.48), 0.48) +assert.equal(agreeConfidence(arrowed, '\\sum', 0.45), 0.78) +assert.equal(agreeConfidence(arrowed, '\\sum_{i = 1}^{n} i', 0.45), 0.78) +assert.equal(agreeConfidence(arrowed, '\\sum', 0.25), 0.25) +assert.equal(agreeConfidence(y2strokes, 'sigma', 0.5), 0.78) +assert.equal(agreeConfidence(y2strokes, 'sigma', 0.3), 0.3) +assert.equal(agreeConfidence(y2strokes, 'a', 0.9), 0.9) +assert.equal(looksLikeIsolatedSigma(isolatedSigma()), true) +assert.equal(looksLikeIsolatedSigma(sumGlyph()), false) +assert.equal(looksLikeIsolatedSigma(sumBar()), false) +assert.equal(looksLikeIsolatedSigma(arrowLetter()), false) +assert.equal(looksLikeIsolatedSigma(y2strokes), false) +assert.equal(agreeConfidence(isolatedSigma(), 'sigma', 0.29), 0.78) +assert.equal(agreeConfidence(isolatedSigma(), 'sigma', 0.2), 0.2) +assert.equal(agreeConfidence(isolatedSigma(), 'theta', 0.29), 0.29) +assert.equal(agreeConfidence(sumGlyph(), 'sigma', 0.29), 0.29) + +const weak = logitFor(0.48) +const faint = logitFor(0.3) +const letterA = scripted([id('a')], weak) +const agreed = await recognizeStrokes(arrowed, letterA, letterA, vocab, 1) +assert.match(agreed[0]?.text ?? '', /^\\vec\{a\} = \(/, JSON.stringify(agreed)) +assert.ok((agreed[0]?.confidence ?? 0) >= OCR_ACCEPT, `arrow lift ${agreed[0]?.confidence}`) +const faintArrow = await recognizeStrokes(arrowed, scripted([id('a')], faint), scripted([id('a')], faint), vocab, 1) +assert.ok((faintArrow[0]?.confidence ?? 1) < OCR_ACCEPT, `faint arrow must stay ink (${faintArrow[0]?.confidence})`) +const bareLetter = await recognizeStrokes(glyph, scripted([id('a')], 6), scripted([id('a')], 6), vocab, 1) +assert.ok(!bareLetter.some((c) => /\\vec\{|^[A-Z]\(/.test(c.text)), JSON.stringify(bareLetter)) +assert.equal(bareLetter.length, 0, JSON.stringify(bareLetter)) +const sumToks = ['\\sum', '_', '{', 'i', '=', '1', '}', '^', '{', 'n', '}', 'i'].map(id) +const sumAgreed = await recognizeStrokes(arrowed, scripted(sumToks, weak), scripted(sumToks, weak), vocab, 1) +assert.equal(sumAgreed[0]?.text, '\\sum_{i = 1}^{n} i', JSON.stringify(sumAgreed)) +assert.ok((sumAgreed[0]?.confidence ?? 0) >= OCR_ACCEPT, `sum lift ${sumAgreed[0]?.confidence}`) +const sumFaint = await recognizeStrokes(arrowed, scripted(sumToks, faint), scripted(sumToks, faint), vocab, 1) +assert.ok((sumFaint[0]?.confidence ?? 1) < OCR_ACCEPT, `faint sum must stay ink (${sumFaint[0]?.confidence})`) +const sigmaHits = await recognizeStrokes(glyph, scripted([id('\\sigma')], weak), scripted([id('\\sigma')], weak), vocab, 1) +assert.equal(sigmaHits[0]?.text, 'sigma', JSON.stringify(sigmaHits)) +assert.ok((sigmaHits[0]?.confidence ?? 0) >= OCR_ACCEPT, `sigma lift ${sigmaHits[0]?.confidence}`) +assert.ok(!/\\vec\{|^[A-Z]\(/.test(sigmaHits[0]?.text ?? '')) +const sigmaFaint = await recognizeStrokes(glyph, scripted([id('\\sigma')], faint), scripted([id('\\sigma')], faint), vocab, 1) +assert.ok((sigmaFaint[0]?.confidence ?? 1) < OCR_ACCEPT, `faint sigma stays ink (${sigmaFaint[0]?.confidence})`) +const plain = await recognizeStrokes(glyph, scripted([id('y'), id('='), id('2')], logitFor(0.5)), scripted([id('y'), id('='), id('2')], logitFor(0.5)), vocab, 1) +assert.equal(plain[0]?.text, 'y = 2') +assert.ok((plain[0]?.confidence ?? 1) < OCR_ACCEPT, `ordinary formula is not lifted (${plain[0]?.confidence})`) +console.log('ocr agreement ok', { arrow: agreed[0], sum: sumAgreed[0], faint: faintArrow[0]?.confidence, plain: plain[0]?.confidence }) + +function sumLikeSigma(): InkStroke[] { + return [ + { + lineWidth: 3, + points: [ + { x: 40, y: 8 }, + { x: 12, y: 10 }, + { x: 8, y: 36 }, + { x: 28, y: 52 }, + { x: 8, y: 70 }, + { x: 12, y: 96 }, + { x: 42, y: 98 }, + ], + }, + { lineWidth: 2, points: [{ x: 48, y: 14 }, { x: 62, y: 12 }] }, + { lineWidth: 2, points: [{ x: 48, y: 92 }, { x: 64, y: 94 }] }, + ] +} + assert.equal(classifyInk(y2strokes), 'formula') assert.equal(classifyInk(arrowLetter()), 'formula') assert.equal(classifyInk(proseNote()), 'text', 'a line of letters is not a formula') +assert.equal(classifyInk(sumLikeSigma()), 'formula', 'Σ with limits is a formula') +assert.equal(classifyInk(sumGlyph()), 'formula') +assert.equal(classifyInk(sumBar()), 'formula') assert.equal(classifyInk(inkFixtures(3).find((s) => s.name === 'frac')!.strokes), 'formula') console.log('ocr gate ok', { prose: classifyInk(proseNote()), formula: classifyInk(y2strokes) }) +assert.equal(tokensToSource(['\\ln', '(', 'x', ')']).replace(/\s+/g, ''), 'ln(x)') +assert.equal(tokensToSource(['\\log', '_', '{', '2', '}', '(', 'x', ')']).replace(/\s+/g, ''), 'log_2(x)') +assert.equal(tokensToSource(['\\lg', '(', 'x', ')']).replace(/\s+/g, ''), 'lg(x)') +console.log('ocr log source ok') diff --git a/scripts/pack-smoke.ts b/scripts/pack-smoke.ts new file mode 100644 index 0000000..91e52f8 --- /dev/null +++ b/scripts/pack-smoke.ts @@ -0,0 +1,60 @@ +import { readFileSync } from 'node:fs' +import { assetUrl } from '../src/lib/asset.ts' +import { parseSheet, serializeSheet } from '../src/board/sheetFile.ts' +import { APP_VERSION } from '../src/version.ts' +import type { Doc } from '../src/board/types.ts' + +const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as { version: string } +const tauri = JSON.parse(readFileSync(new URL('../src-tauri/tauri.conf.json', import.meta.url), 'utf8')) as { version: string; productName: string; identifier: string } + +function check(cond: unknown, message: string) { + if (!cond) throw new Error(message) +} + +check(pkg.version === APP_VERSION, `package.json version ${pkg.version} != ${APP_VERSION}`) +check(tauri.version === APP_VERSION, `tauri.conf.json version ${tauri.version} != ${APP_VERSION}`) +check(tauri.productName === 'MathScrap', 'product name must stay MathScrap') +check(tauri.identifier === 'app.mathscrap', `unexpected identifier ${tauri.identifier}`) + +check(assetUrl('models/comer/vocab.json', '/') === '/models/comer/vocab.json', 'absolute base') +check(assetUrl('/ort/', '/') === '/ort/', 'leading slash is stripped once') +check(assetUrl('ort/file.wasm', './') === './ort/file.wasm', 'relative base stays inside the bundle') +check(!assetUrl('models/comer/encoder_int8.onnx', '/').startsWith('http'), 'model URL is not a remote host') + +const doc: Doc = { + strokes: [{ id: 's1', pts: [1, 2, 0.5, 3, 4, 0.4], color: '#2b2118', width: 3, style: 'ink', createdAt: 1 }], + cards: [{ id: 'c1', x: 10, y: 20, text: 'y = x' }], + stickers: [], +} +const raw = serializeSheet(doc) +const back = parseSheet(raw) +check(back.cards[0]?.text === 'y = x' && back.strokes.length === 1, 'round trip') +check(parseSheet(JSON.stringify(doc)).cards[0]?.text === 'y = x', 'plain stored sheet') + +const kept = parseSheet('{"format":"mathscrap.sheet","version":1,"doc":{"strokes":[],"cards":[{"id":"c1","x":1,"y":2,"text":"y = x","__proto__":{"polluted":true}}],"stickers":[]}}') +check(kept.cards[0]?.text === 'y = x', 'extra card fields are not kept') +check(!('polluted' in {}), 'import does not pollute Object.prototype') + +const junk = [ + '{"format":"other","version":1,"doc":{}}', + '{"format":"mathscrap.sheet","version":99,"doc":{"strokes":[],"cards":[],"stickers":[]}}', + '{"strokes":[{"id":"s1","pts":[1,2],"color":"#fff","width":3,"style":"ink","createdAt":1}],"cards":[],"stickers":[]}', + '{"strokes":[{"id":"s1","pts":[1,2,0],"color":"javascript:alert(1)","width":3,"style":"ink","createdAt":1}],"cards":[],"stickers":[]}', + '{"strokes":[],"cards":[{"id":"c1","x":1,"y":2,"text":' + JSON.stringify('x'.repeat(9000)) + '}],"stickers":[]}', + '[]', + '"y = x"', +] +for (const sample of junk) { + let threw = false + try { + parseSheet(sample) + } catch { + threw = true + } + check(threw, `should reject ${sample.slice(0, 60)}`) +} + +const withProto = parseSheet('{"strokes":[],"cards":[{"id":"c1","x":1,"y":2,"text":"y = 1"}],"stickers":[],"__proto__":{"admin":true},"eval":"process.exit(1)"}') +check(withProto.cards.length === 1 && !('eval' in withProto), 'unknown fields are dropped') + +console.log(`pack-smoke ok ${APP_VERSION}`) diff --git a/scripts/package-android.sh b/scripts/package-android.sh new file mode 100755 index 0000000..0c2dc54 --- /dev/null +++ b/scripts/package-android.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +# Android APK. Default is a debug-signed APK (sideload with adb). It is not a Play release. +# Release signing inputs, values never printed: +# ANDROID_KEY_ALIAS ANDROID_KEY_PASSWORD ANDROID_KEY_BASE64 +set -euo pipefail +cd "$(dirname "$0")/.." + +mode="debug" +install_sdk=0 +for arg in "$@"; do + case "$arg" in + --debug) mode="debug" ;; + --release) mode="release" ;; + --install-sdk) install_sdk=1 ;; + *) + echo "Unknown argument: $arg" >&2 + echo "Usage: scripts/package-android.sh [--debug|--release] [--install-sdk]" >&2 + exit 2 + ;; + esac +done + +NDK_VERSION="${ANDROID_NDK_VERSION:-27.2.12479018}" +PLATFORM="${ANDROID_PLATFORM:-android-35}" +BUILD_TOOLS="${ANDROID_BUILD_TOOLS:-35.0.0}" +CMDLINE_URL="${ANDROID_CMDLINE_TOOLS_URL:-https://dl.google.com/android/repository/commandlinetools-linux-13114758_latest.zip}" + +if [[ "$install_sdk" -eq 1 ]]; then + sdk="${ANDROID_HOME:-$HOME/android-sdk}" + mkdir -p "$sdk/cmdline-tools" + if [[ ! -x "$sdk/cmdline-tools/latest/bin/sdkmanager" ]]; then + tmp="$(mktemp)" + curl -fsSL -o "$tmp" "$CMDLINE_URL" + rm -rf "$sdk/cmdline-tools/latest" + unzip -q "$tmp" -d "$sdk/cmdline-tools" + mv "$sdk/cmdline-tools/cmdline-tools" "$sdk/cmdline-tools/latest" + rm -f "$tmp" + fi + export ANDROID_HOME="$sdk" + export ANDROID_SDK_ROOT="$sdk" + yes | "$sdk/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$sdk" --licenses >/dev/null || true + "$sdk/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$sdk" "platform-tools" "platforms;${PLATFORM}" "build-tools;${BUILD_TOOLS}" "ndk;${NDK_VERSION}" +fi + +missing=0 +if ! command -v java >/dev/null 2>&1; then + echo "JDK 17 or newer is not on PATH." + missing=1 +fi +if [[ -z "${ANDROID_HOME:-}${ANDROID_SDK_ROOT:-}" && -d "$HOME/android-sdk" ]]; then + export ANDROID_HOME="$HOME/android-sdk" + export ANDROID_SDK_ROOT="$HOME/android-sdk" +fi +if [[ -z "${ANDROID_HOME:-}${ANDROID_SDK_ROOT:-}" ]]; then + echo "ANDROID_HOME is unset. Re-run with --install-sdk or point it at an Android SDK." + missing=1 +else + export ANDROID_HOME="${ANDROID_HOME:-$ANDROID_SDK_ROOT}" + export ANDROID_SDK_ROOT="${ANDROID_SDK_ROOT:-$ANDROID_HOME}" +fi +if [[ -z "${NDK_HOME:-}" && -n "${ANDROID_HOME:-}" && -d "${ANDROID_HOME}/ndk/${NDK_VERSION}" ]]; then + export NDK_HOME="${ANDROID_HOME}/ndk/${NDK_VERSION}" +fi +if [[ -z "${NDK_HOME:-}" || ! -d "${NDK_HOME}" ]]; then + echo "NDK_HOME is unset. Expected ${ANDROID_HOME:-ANDROID_HOME}/ndk/${NDK_VERSION}." + missing=1 +fi +( + cd src-tauri + rustup show active-toolchain >/dev/null + rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android +) +if [[ "$missing" -ne 0 ]]; then + echo "Android prerequisites are incomplete. No APK was produced." + exit 3 +fi + +version="$(node -p "require('./package.json').version")" +if [[ ! -d src-tauri/gen/android ]]; then + npx tauri android init --ci +fi + +mkdir -p packaging/out +if [[ "$mode" == "release" ]]; then + if [[ -z "${ANDROID_KEY_ALIAS:-}" || -z "${ANDROID_KEY_PASSWORD:-}" || -z "${ANDROID_KEY_BASE64:-}" ]]; then + echo "Release APK needs ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD, and ANDROID_KEY_BASE64." >&2 + echo "This script does not generate a keystore. No release APK was produced." >&2 + exit 2 + fi + node scripts/android-release-signing.mjs + npx tauri android build --apk --target aarch64 --split-per-abi --ci + label="release-signed" +else + # Keep the debug APK installable without embedding full Rust debuginfo. + # The signature is still the Android debug keystore, not a Play release key. + CARGO_PROFILE_DEV_DEBUG=0 CARGO_PROFILE_DEV_STRIP=symbols \ + npx tauri android build --debug --apk --target aarch64 --split-per-abi --ci + label="debug-signed" +fi + +copied=0 +while IFS= read -r file; do + kind="$(basename "$(dirname "$file")")" + abi="$(basename "$(dirname "$(dirname "$file")")")" + if [[ "$kind" != debug && "$kind" != release ]]; then + abi="$kind" + fi + dest="packaging/out/MathScrap-${version}-android-${abi}-${label}.apk" + cp "$file" "$dest" + echo "artifact $dest" + copied=1 +done < <(find src-tauri/gen/android -type f -name '*.apk' ! -name '*-unsigned.apk' | sort) +if [[ "$mode" == "debug" && -f "$HOME/.android/debug.keystore" ]]; then + tools="$(find "$ANDROID_HOME/build-tools" -mindepth 1 -maxdepth 1 -type d | sort | tail -1)" + for apk in packaging/out/MathScrap-"${version}"-android-*-debug-signed.apk; do + [[ -f "$apk" ]] || continue + tmp="${apk}.unaligned" + python3 scripts/compact-debug-apk.py "$apk" "$tmp" + "$tools/zipalign" -f -p 4 "$tmp" "${apk}.aligned" + "$tools/apksigner" sign --ks "$HOME/.android/debug.keystore" --ks-pass pass:android --key-pass pass:android --ks-key-alias androiddebugkey "${apk}.aligned" + mv "${apk}.aligned" "$apk" + rm -f "$tmp" "${apk}.idsig" + done +fi +if [[ "$copied" -ne 1 ]]; then + echo "The Android build finished but no APK was found." >&2 + exit 1 +fi +node scripts/checksum-artifacts.mjs packaging/out +if [[ "$label" == "debug-signed" ]]; then + echo "This APK is signed with the Android debug keystore. It can be sideloaded with adb install." + echo "It is not a Play Store release and the debug certificate is not a production key." +else + echo "This APK is signed with the keystore supplied through ANDROID_KEY_BASE64." + echo "It was not uploaded to Play." +fi diff --git a/scripts/package-ios.sh b/scripts/package-ios.sh new file mode 100755 index 0000000..4f7a7a2 --- /dev/null +++ b/scripts/package-ios.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# iOS Simulator .app, or a signed IPA when Apple signing inputs are present. +# This script never enrolls in the Apple Developer Program and never creates a certificate. +set -euo pipefail +cd "$(dirname "$0")/.." + +target="simulator" +for arg in "$@"; do + case "$arg" in + --simulator) target="simulator" ;; + --device) target="device" ;; + *) + echo "Unknown argument: $arg" >&2 + echo "Usage: scripts/package-ios.sh [--simulator|--device]" >&2 + exit 2 + ;; + esac +done + +if [[ "$(uname -s)" != "Darwin" ]]; then + cat <<'EOF' +This machine is not macOS. No IPA and no iOS Simulator build were produced. + +An installable iPhone package needs all of the following, which are not on this Linux builder: + +1. A Mac with macOS 12 or newer. +2. Full Xcode 15 or newer (the App Store app, not only Command Line Tools). + Open Xcode once, then: sudo xcodebuild -license accept + xcode-select -s /Applications/Xcode.app/Contents/Developer +3. Rust iOS targets: + rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios +4. CocoaPods: brew install cocoapods +5. Node 22 and this repo: npm ci + +What each command actually produces: + +- npm run pack:ios -- --simulator + Runs `tauri ios build --debug` for the Apple silicon simulator. + Output is a .app for the iOS Simulator. It is not an IPA and will not install on an iPhone. + +- A development install on your own phone uses a free Apple ID added in + Xcode > Settings > Accounts, with automatic signing on Personal Team. + That signature lasts about 7 days and is not a distributable IPA. + This repository does not collect that Apple ID. + +- A distributable IPA (Ad Hoc, TestFlight, or App Store) needs the paid + Apple Developer Program, which this task does not enroll in or purchase. + When you already have the certificate and profile, put only these names + in the CI environment or your shell. Do not commit the values: + IOS_CERTIFICATE base64 of the .p12 exported from Keychain + IOS_CERTIFICATE_PASSWORD + IOS_MOBILE_PROVISION base64 of the .mobileprovision + Optional automatic signing instead of the three above: + APPLE_API_ISSUER + APPLE_API_KEY + APPLE_API_KEY_PATH path to the AuthKey .p8 already on that Mac + Then: npm run pack:ios -- --device + +Minimum OS for the packaged shell is iOS 16.4 (WebAssembly SIMD for CoMER). +The bundle id is app.mathscrap. It must match the App ID inside the profile. +EOF + exit 3 +fi + +if ! command -v xcodebuild >/dev/null 2>&1; then + echo "xcodebuild is missing. Install full Xcode and point xcode-select at it." >&2 + exit 3 +fi +rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios +if [[ ! -d src-tauri/gen/apple ]]; then + npx tauri ios init --ci +fi + +version="$(node -p "require('./package.json').version")" +mkdir -p packaging/out + +if [[ "$target" == "simulator" ]]; then + npx tauri ios build --debug --target aarch64-sim --ci + echo "Simulator build finished. Search src-tauri/gen/apple for the .app." + echo "This is not an IPA and is not installable on an iPhone." + find src-tauri/gen/apple -name '*.app' -type d | head -20 + exit 0 +fi + +signed=0 +if [[ -n "${IOS_CERTIFICATE:-}" && -n "${IOS_CERTIFICATE_PASSWORD:-}" && -n "${IOS_MOBILE_PROVISION:-}" ]]; then + signed=1 +elif [[ -n "${APPLE_API_ISSUER:-}" && -n "${APPLE_API_KEY:-}" && -n "${APPLE_API_KEY_PATH:-}" ]]; then + signed=1 +fi +if [[ "$signed" -ne 1 ]]; then + echo "Refusing to export an IPA. Signing inputs are absent, so the result would not be an installable device package." >&2 + echo "See the prerequisite list printed on a non-Mac run of this script." >&2 + exit 2 +fi + +method="${IOS_EXPORT_METHOD:-release-testing}" +case "$method" in + app-store-connect|release-testing|debugging) ;; + *) + echo "IOS_EXPORT_METHOD must be app-store-connect, release-testing, or debugging." >&2 + exit 2 + ;; +esac +npx tauri ios build --export-method "$method" --ci +while IFS= read -r file; do + dest="packaging/out/MathScrap-${version}-ios-$(basename "$file")" + cp "$file" "$dest" + echo "artifact $dest" +done < <(find src-tauri/gen/apple -type f -name '*.ipa' | sort) +node scripts/checksum-artifacts.mjs packaging/out +echo "If an IPA was copied above, it was signed with the provided profile. It was not uploaded to App Store Connect or TestFlight." diff --git a/scripts/package-windows.sh b/scripts/package-windows.sh new file mode 100755 index 0000000..3f3308a --- /dev/null +++ b/scripts/package-windows.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# Unsigned Windows NSIS installer and MathScrap.exe, built on Windows. +# Optional Authenticode inputs (never printed): WINDOWS_CERTIFICATE, WINDOWS_CERTIFICATE_PASSWORD. +# When those are imported into the runner certificate store, also set WINDOWS_CERTIFICATE_THUMBPRINT. +set -euo pipefail +cd "$(dirname "$0")/.." + +os="$(uname -s)" +case "$os" in + MINGW*|MSYS*|CYGWIN*) ;; + *) + echo "This machine is ${os}. A Windows EXE was not built here." + echo "Build on Windows with: npm run pack:windows" + echo "CI job: package.yml / windows, runner windows-latest." + echo "The installer is unsigned unless a manual run imports WINDOWS_CERTIFICATE." + echo "WebView2 must already be on the machine (Windows 11, and Windows 10 from version 1803). This package does not download it." + exit 3 + ;; +esac + +version="$(node -p "require('./package.json').version")" +mkdir -p packaging/out +args=(build --bundles nsis --ci) +if [[ -n "${WINDOWS_CERTIFICATE_THUMBPRINT:-}" ]]; then + args+=(--config "{\"bundle\":{\"windows\":{\"certificateThumbprint\":\"${WINDOWS_CERTIFICATE_THUMBPRINT}\",\"digestAlgorithm\":\"sha256\",\"timestampUrl\":\"http://timestamp.digicert.com\"}}}") + label="signed" +else + label="unsigned" + echo "No WINDOWS_CERTIFICATE_THUMBPRINT. Building an unsigned installer. Windows SmartScreen will warn." +fi + +npx tauri "${args[@]}" + +copied=0 +shopt -s nullglob +for file in src-tauri/target/release/bundle/nsis/*-setup.exe src-tauri/target/release/mathscrap.exe; do + [[ -f "$file" ]] || continue + base="$(basename "$file")" + if [[ "$base" == *-setup.exe ]]; then + dest="packaging/out/MathScrap-${version}-windows-x64-${label}-setup.exe" + else + dest="packaging/out/MathScrap-${version}-windows-x64-${label}.exe" + fi + cp "$file" "$dest" + echo "artifact $dest" + copied=1 +done +if [[ "$copied" -ne 1 ]]; then + echo "tauri build finished but no EXE was found." >&2 + exit 1 +fi +node scripts/checksum-artifacts.mjs packaging/out +echo "Windows ${label} package is in packaging/out." +echo "Installable: yes, as an unsigned desktop program, unless label is signed." +echo "Not a Store submission." diff --git a/scripts/plot-cases.ts b/scripts/plot-cases.ts index 08d2b23..277afce 100644 --- a/scripts/plot-cases.ts +++ b/scripts/plot-cases.ts @@ -30,7 +30,13 @@ export const cases: [string, string, Expect][] = [ ['三次函数', 'f(x) = x^3 - 3x', { dim: 2, inView: [[-1, 2], [1, -2], [0, 0]] }], ['正弦', 'y = sin(x)', { dim: 2, inView: [[Math.PI / 2, 1], [-Math.PI / 2, -1]] }], ['指数', 'y = e^x', { dim: 2, inView: [[0, 1]] }], - ['对数', 'y = ln(x)', { dim: 2, inView: [[1, 0]] }], + ['对数', 'y = ln(x)', { dim: 2, inView: [[1, 0]], facts: ['\\ln u: u > 0'] }], + ['常用对数', 'y = log(x)', { dim: 2, inView: [[1, 0]], facts: ['\\log u = \\log_{10} u'] }], + ['lg', 'y = lg(x)', { dim: 2, inView: [[1, 0]], facts: ['\\lg u = \\log_{10} u'] }], + ['指定底数', 'y = log_2(x)', { dim: 2, inView: [[1, 0], [2, 1]], facts: ['a > 0'] }], + ['log(底, 真数)', 'y = log(2, x)', { dim: 2, inView: [[2, 1]] }], + ['平移后的对数', 'y = ln(x-2)', { dim: 2, inView: [[3, 0], [4, Math.log(2)]] }], + ['复合对数', 'y = ln(x^2+1)', { dim: 2, inView: [[0, 0]] }], ['反比例', 'y = 1/x', { dim: 2, facts: ['x \\ne 0'] }], ['常函数', 'y = 3', { dim: 2, inView: [[0, 3]] }], ['直线(隐式)', 'x + y = 1', { dim: 2, kinds: ['implicit2'], inView: [[1, 0], [0, 1]] }], @@ -47,7 +53,7 @@ export const cases: [string, string, Expect][] = [ ['直线过两点', 'y = kx + b, 过(1, 3), 过(2, 5)', { solve: { k: 2, b: 1 } }], // ---- triangles / coordinate geometry ---- ['解三角形 SAS', '△ABC: a = 5, b = 7, C = 60°', { dim: 2, facts: ['\\sqrt{39}'], minFacts: 10 }], - ['解三角形 SSS', '△ABC: a=3, b=4, c=5', { dim: 2, facts: ['90'] }], + ['解三角形 SSS', '△ABC: a=3, b=4, c=5', { dim: 2, facts: ['90', 'S_{\\text{海伦}}', '|OI|', 'r_a=', 'AF_{c}', '\\Pi_{\\text{Ceva}}', '\\Pi_{\\text{Menelaus}}', '\\text{Stewart}_{m_', 'ID_{a}', '\\mathcal{A}_', 'OG:GH', 'R_{\\text{九点}}', '|NI|', '|NI_a|', 'Sp', 'Na', 'IG:GNa'], minFacts: 45 }], ['解三角形 SSA 两解', '△ABC: a = 6, b = 8, A = 40°', { dim: 2 }], ['坐标 + 垂直', 'A(0,0), B(4,0), C(1,t)\nAC ⊥ BC', { dim: 2, solve: { t: Math.sqrt(3) }, inView: [[0, 0], [4, 0]] }], ['平行四边形', 'A(0,0), B(4,0), C(5,3), D(1,3)', { dim: 2, facts: ['平行四边形'], inView: [[5, 3]] }], @@ -80,14 +86,44 @@ export const cases: [string, string, Expect][] = [ // ---- parallel lines: intercept theorem + similar triangles ---- ['平行线分线段成比例', 'y = 0\ny = 1\ny = 3\nx = 0\nx - y = 0', { dim: 2, facts: ['成比例', '相似比'] }], // ---- notation rules ---- - ['小写不能当向量定义', 'b = (1, 2)', { error: /粗体/ }], - ['小写不能当点', 'b(1, 2)', { error: /大写字母/ }], + ['小写不能当向量定义', 'b = (1, 2)', { error: /粗体|bold/i }], + ['小写不能当点', 'b(1, 2)', { error: /大写字母|capital/i }], ['大写点', 'B(1, 2)', { kinds: ['point'] }], // ---- 3D ---- ['曲面 z = sin x cos y', 'z = sin(x)cos(y)', { dim: 3 }], ['碗面 z = x² + y²', 'z = x^2 + y^2', { dim: 3 }], ['两平面交线', 'x + y + z = 3\nx - y + 2z = 1', { dim: 3, facts: ['α \\cap β'] }], ['球面', 'x^2 + y^2 + z^2 = 4', { dim: 3, facts: ['r=2'] }], + ['球面 ∩ 平面', 'x^2 + y^2 + z^2 = 4\nz = 1', { dim: 3, facts: ['\\rho_', '\\text{圆}'] }], + ['异面直线四点', 'A(0,0,0)\nB(1,0,0)\nC(0,1,0)\nD(0,1,1)', { dim: 3, facts: ['d(AB, CD)=1', '异面', '\\theta(AB, CD)'] }], + ['线面角', 'A(0,0,0)\nB(0,0,1)\nC(1,0,0)\nz = 0', { dim: 3, facts: ['\\theta(AB, α)', 'AB \\perp α', 'AC \\parallel α', '\\text{foot}(B, α)'] }], + ['两圆根轴', 'x^2 + y^2 = 4\n(x-3)^2 + y^2 = 4', { dim: 2, facts: ['l_{', '相交', '公共弦'] }], + ['两球根面', 'x^2 + y^2 + z^2 = 4\n(x-3)^2 + y^2 + z^2 = 4', { dim: 3, facts: ['\\pi_', '\\rho_', '公共弦'] }], + ['四面体体积', 'A(0,0,0)\nB(1,0,0)\nC(0,1,0)\nD(0,0,1)', { dim: 3, facts: ['V_{\\text{四面体}}', 'V_{\\text{Cayley', 'S_{\\triangle', 'h_{'] }], + ['二面角', 'x + 0*z = 0\ny + 0*z = 0', { dim: 3, facts: ['\\text{二面角}', '\\perp'] }], + ['直线交球面', 'A(0,0,0)\nB(4,0,0)\nx^2 + y^2 + z^2 = 4', { dim: 3, facts: ['P_{AB\\cap', '|P_1P_2|'] }], + ['笛卡尔圆', 'A(0,0)\nB(2,0)\nC(1,1.7320508075688772)\n⊙A: r = 1\n⊙B: r = 1\n⊙C: r = 1', { dim: 2, facts: ['\\text{Descartes}'], minFacts: 2 }], + ['圆柱截椭圆', 'x^2 + y^2 = 4\nx - z = 0', { dim: 3, facts: ['a_', 'b_', 'e_', '\\text{椭圆}'] }], + ['英国国旗定理', 'A(0,0), B(4,0), C(4,3), D(0,3)', { dim: 2, facts: ['矩形', 'PA^2 + PC^2'], minFacts: 4 }], + ['瓦里农平行四边形', 'A(0,0), B(4,0), C(5,3), D(1,3)', { dim: 2, facts: ['\\text{Varignon}', 'M_0'] }], + ['三圆根心', 'A(0,0)\nB(2,0)\nC(1,1.7320508075688772)\n⊙A: r = 1\n⊙B: r = 1\n⊙C: r = 1', { dim: 2, facts: ['l_{', 'R_{\\odot', '\\text{幂}'], minFacts: 5 }], + ['维维安尼等边', '△ABC: a = 2, b = 2, c = 2', { dim: 2, facts: ['d_a + d_b + d_c'], minFacts: 4 }], + ['平行四边形法则', '𝐚 = (3, 4); 𝐛 = (1, 0)', { dim: 2, kinds: ['vector', 'vector'], facts: ['|a+b|^2 + |a-b|^2'] }], + ['托勒密与婆罗摩笈多', 'A(0,0), B(4,0), C(4,3), D(0,3)', { dim: 2, facts: ['\\text{共圆}', 'AC \\cdot BD', 'S_{\\text{Brahmagupta}}'], minFacts: 4 }], + ['拿破仑定理', '△ABC: a = 3, b = 4, c = 5', { dim: 2, facts: ['\\text{Napoleon}', '|G_aG_b|'], minFacts: 3 }], + ['欧拉OH²与卡诺', '△ABC: a = 3, b = 4, c = 5', { dim: 2, facts: ['|OH|^2', '9R^2', 'R+r', 'H \\leftrightarrow O'], minFacts: 6 }], + ['莫利定理', '△ABC: a = 3, b = 4, c = 5', { dim: 2, facts: ['\\text{Morley}', '|M_0M_1|'], minFacts: 3 }], + ['西姆松与三线极线', '△ABC: a = 3, b = 4, c = 5', { dim: 2, facts: ['\\text{Simson}', 'l_{\\text{Simson}}', '\\tau(I)', 'OH\\text{ 九点圆直径}'], minFacts: 6 }], + ['凯莱门杰四面体', 'A(0,0,0)\nB(1,0,0)\nC(0,1,0)\nD(0,0,1)', { dim: 3, facts: ['V_{\\text{四面体}}', 'V_{\\text{Cayley'] }], + ['米克尔完全四线形', 'y = 0\nx = 0\nx + y = 2\nx - y = 1', { dim: 2, facts: ['\\text{Miquel}', 'M'], minFacts: 2 }], + ['德萨格透视', 'A(2,1), B(-1,2), C(-2,-3), A\'(4,2), B\'(-3,6), C\'(-1,-1.5)', { dim: 2, facts: ['\\text{Desargues}', '\\text{透视轴}'], minFacts: 2 }], + ['圆柱截线离心率', 'x^2 + y^2 = 4\nx - z = 0', { dim: 3, facts: ['a_', 'b_', 'e_', '\\text{椭圆}'] }], + ['圆锥截线离心率', 'x^2 + y^2 - z^2 = 0\nx + 2z = 2', { dim: 3, facts: ['e_', '\\text{椭圆}'] }], + ['帕斯卡圆内接六边形', 'A(5,0), B(4,3), C(-3,4), D(-5,0), E(-3,-4), F(4,-3)', { dim: 2, facts: ['\\text{Pascal}', '\\text{Pascal线}'], minFacts: 2 }], + ['布利安雄正六边形', 'A(1,0), B(0.5,0.8660254037844386), C(-0.5,0.8660254037844386), D(-1,0), E(-0.5,-0.8660254037844386), F(0.5,-0.8660254037844386)', { dim: 2, facts: ['\\text{Brianchon}', 'a+c+e'], minFacts: 2 }], + ['四面体面与高', 'A(0,0,0)\nB(1,0,0)\nC(0,1,0)\nD(0,0,1)', { dim: 3, facts: ['S_{\\triangle', 'h_{'], minFacts: 8 }], + ['点关于直线反射', 'P(1,2)\ny = 0', { dim: 2, facts: ['P^{\\prime}', 'd(P'], minFacts: 2 }], + ['点关于平面反射', 'Q(1,2,3)\nz = 0', { dim: 3, facts: ['Q^{\\prime}', '\\text{foot}'], minFacts: 2 }], ['空间螺旋线', 'r(t) = (cos(t), sin(t), t/3)', { dim: 3 }], ['空间三点', 'P(1,2,3), Q(0,1,-1), R(2,0,1)', { dim: 3 }], ] diff --git a/scripts/smoke.ts b/scripts/smoke.ts index 385b917..a9a98ae 100644 --- a/scripts/smoke.ts +++ b/scripts/smoke.ts @@ -127,7 +127,7 @@ for (const [title, text, ex] of cases) { if (ex.kinds && JSON.stringify(kinds) !== JSON.stringify(ex.kinds)) problems.push(`kinds ${kinds.join(',')} ≠ ${ex.kinds.join(',')}`) if (ex.dim && a.dim !== ex.dim) problems.push(`dim ${a.dim} ≠ ${ex.dim}`) if (!a.objects.length) problems.push('nothing to plot') - const factText = a.sections.flatMap((s) => s.facts.map((f) => `${f.label}=${f.value}`)) + const factText = a.sections.flatMap((s) => s.facts.map((f) => `${f.label}=${f.value} ${f.how}`)) for (const f of ex.facts ?? []) if (!factText.some((t) => t.replace(/\s+/g, ' ').includes(f))) problems.push(`missing fact containing "${f}"`) if (ex.minFacts && a.factCount < ex.minFacts) problems.push(`only ${a.factCount} facts`) if (ex.solve) { @@ -230,5 +230,80 @@ if (bare.some((s) => s.kind === 'vector' || s.kind === 'point')) { console.log('FAIL bare letter classified as a point or vector', bare) } else console.log('ok bare letter is not a point or vector') +const sumTexts = ['\\sum_{i=1}^{n} i', '\\sum_{i = 1}^{n} i', 'y = \\sum_{k = 1}^{n} k', '∑_{i=1}^{n} i', '\\int_{0}^{1} x', '\\sum'] +for (const text of sumTexts) { + const card = analyze([{ id: 'sum', text }]) + const view = summarizeCard(text) + const group = pageGroups([{ id: 'sum', x: 0, y: 0, text }]) + const bad = card.errors.length > 0 || view.lines.some((l) => l.error) || view.lines[0]?.latex.includes('待修正') || !view.lines[0]?.latex.includes(text.includes('int') || text.includes('\\int') ? '\\int' : '\\sum') + if (bad || view.plottable || group.count[2] !== 0 || group.count[3] !== 0) { + failed++ + console.log('FAIL summation errored or was plotted', text, card.errors, view.lines, group.count) + } else console.log('ok notation', view.lines[0]?.latex) +} +const sigma = parseCard('sigma') +const sigmaView = summarizeCard('sigma') +const sigmaPage = pageGroups([{ id: 'sg', x: 0, y: 0, text: 'sigma' }]) +const sigmaGlyph = parseCard('σ') +const ySigma = parseCard('y = sigma') +const sigmaBad = + sigma.length !== 1 || + sigma[0]?.kind !== 'notation' || + sigmaView.lines[0]?.latex !== '\\sigma' || + sigmaView.plottable || + sigmaPage.count[2] !== 0 || + sigmaPage.count[3] !== 0 || + sigmaGlyph[0]?.kind !== 'notation' || + ySigma[0]?.kind !== 'curve' +if (sigmaBad) { + failed++ + console.log('FAIL isolated sigma should be the symbol, not a graph', sigma, sigmaView.lines, sigmaPage.count, ySigma[0]) +} else console.log('ok isolated sigma is \\\\sigma') + +function assertLogPlot(title: string, text: string, defined: (x: number) => boolean) { + const a = analyze([{ id: 'log', text }]) + const problems: string[] = [] + if (a.errors.length) problems.push(a.errors.map((e) => e.message).join(' | ')) + const r = initialRanges(a) + const { data } = buildFigure2d(a, r, []) + const curve = a.objects.find((o) => o.type === 'curve') + const trace = data.find((d) => curve && d.name === `${curve.type === 'curve' ? curve.name : ''}(x)`) + const xs = (trace?.x ?? []) as (number | null)[] + const ys = (trace?.y ?? []) as (number | null)[] + let finite = 0 + let crossed = 0 + for (let i = 0; i < xs.length; i++) { + const x = xs[i] + const y = ys[i] + if (x == null || y == null) continue + finite++ + if (!defined(x) || !Number.isFinite(y)) problems.push(`plotted (${x}, ${y}) where the log is undefined`) + const prev = i > 0 ? xs[i - 1] : null + if (prev != null && defined(prev) !== defined(x)) crossed++ + } + if (finite < 40) problems.push(`only ${finite} finite samples`) + if (crossed) problems.push(`a segment crosses the domain edge ${crossed} time(s)`) + if (problems.length) failed++ + console.log(`${problems.length ? 'FAIL' : 'ok '} ${title}`) + problems.forEach((p) => console.log(` ✗ ${p}`)) +} +assertLogPlot('对数图不越过渐近线', 'y = ln(x)', (x) => x > 0) +assertLogPlot('平移对数图', 'y = ln(x-2)', (x) => x > 2) +assertLogPlot('底数 2 的对数图', 'y = log_2(x)', (x) => x > 0) +function assertBlankLog(title: string, text: string) { + const a = analyze([{ id: 'log', text }]) + const r = initialRanges(a) + const { data } = buildFigure2d(a, r, []) + const curve = a.objects.find((o) => o.type === 'curve') + const trace = data.find((d) => curve && curve.type === 'curve' && d.name === `${curve.name}(x)`) + const ys = (trace?.y ?? []) as (number | null)[] + const finite = ys.filter((y) => y != null && Number.isFinite(y)).length + const ok = a.errors.length === 0 && finite === 0 + if (!ok) failed++ + console.log(`${ok ? 'ok ' : 'FAIL'} ${title}${ok ? '' : ` errors=${a.errors.length} finite=${finite}`}`) +} +assertBlankLog('底数 = 1 没有实图', 'y = log_1(x)') +assertBlankLog('底数 < 0 没有实图', 'y = log_(-2)(x)') + console.log(`\n${cases.length - failed}/${cases.length} formula cases · ${pages.length - pageFailed}/${pages.length} shared-sheet cases passed`) process.exit(failed + pageFailed ? 1 : 0) diff --git a/scripts/verify-packaged-dist.mjs b/scripts/verify-packaged-dist.mjs new file mode 100644 index 0000000..07d3a65 --- /dev/null +++ b/scripts/verify-packaged-dist.mjs @@ -0,0 +1,61 @@ +import { createHash } from 'node:crypto' +import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs' +import path from 'node:path' + +const root = process.cwd() +const dist = path.join(root, 'dist') +const lines = [] + +function fail(message) { + console.error(`verify-packaged-dist: ${message}`) + process.exit(1) +} + +function need(rel, minBytes) { + const file = path.join(dist, rel) + if (!existsSync(file)) fail(`missing ${rel}`) + const size = statSync(file).size + if (size < minBytes) fail(`${rel} is ${size} bytes, expected at least ${minBytes}`) + const sha = createHash('sha256').update(readFileSync(file)).digest('hex') + lines.push(`${sha} ${rel} ${size}`) +} + +if (!existsSync(dist)) fail('dist/ is missing. Run npm run build first. This check does not start Vite.') + +need('index.html', 100) +need('models/comer/encoder_int8.onnx', 1_000_000) +need('models/comer/decoder_int8.onnx', 1_000_000) +need('models/comer/vocab.json', 100) +need('models/comer/LICENSE', 100) +need('models/comer/NOTICE.md', 50) +need('favicon.svg', 50) + +const ortDir = path.join(dist, 'ort') +if (!existsSync(ortDir)) fail('dist/ort is missing. ONNX Runtime WASM was not copied into the bundle.') +const ort = readdirSync(ortDir) +for (const name of ['ort-wasm-simd-threaded.wasm', 'ort-wasm-simd-threaded.mjs']) { + if (!ort.includes(name)) fail(`dist/ort/${name} was not bundled`) + need(path.posix.join('ort', name), name.endsWith('.wasm') ? 1_000_000 : 100) +} +if (!ort.some((name) => name === 'ort-wasm-simd.wasm')) { + console.log('note: ort-wasm-simd.wasm (non-threaded) is not in this onnxruntime-web build; the threaded file is the one the page requests with numThreads = 1') +} + +const assets = path.join(dist, 'assets') +const wasm = existsSync(assets) ? readdirSync(assets).filter((name) => name.endsWith('.wasm')) : [] +if (!wasm.length) fail('mathcore WASM was not emitted under dist/assets') +for (const name of wasm) need(path.posix.join('assets', name), 100_000) + +const html = readFileSync(path.join(dist, 'index.html'), 'utf8') +if (/47321|47322|fonts\.googleapis|cdn\.jsdelivr|unpkg\.com|esm\.sh/.test(html)) fail('index.html points at the dev server or a CDN') +if (/]+src="https?:/.test(html) || /]+href="https?:/.test(html)) fail('index.html loads a remote script or stylesheet') + +const banned = /__mathscrap\/bridge|__mathscrap\/ai\/forward|localhost:47321|127\.0\.0\.1:47321/ +const jsFiles = readdirSync(assets).filter((name) => name.endsWith('.js')) +for (const name of jsFiles) { + const text = readFileSync(path.join(assets, name), 'utf8') + if (banned.test(text)) fail(`dist/assets/${name} still references the Vite dev bridge`) +} + +console.log('verify-packaged-dist ok') +console.log(lines.join('\n')) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock new file mode 100644 index 0000000..2b5e863 --- /dev/null +++ b/src-tauri/Cargo.lock @@ -0,0 +1,4390 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2fb6cfd47bf496ff64095c20eaba0c201404ee38714d4142fcfa1dc334fcc7a" + +[[package]] +name = "alloc-stdlib" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5c1865780388bfa186411ab5f247819487fc4864c6e9c3106611fa347586e1" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "brotli" +version = "9.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "627b7cfb0578f0eafce087a90fcd40add1231900b69e4d4de2d456260a4f5d81" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "6.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d9071e5b45944c5a9c06baf13a7a65ffa2c88c204015967852c90869692c05" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.2", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbad30e4b4c14a39e3cc8aed085a12a327257c316619c93581e017bc52be591" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.21", +] + +[[package]] +name = "cargo_toml" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82f4b26e751e711a5302649417f2da046dce6391b2ea30a4820f37462314f0b9" +dependencies = [ + "semver", + "serde", + "toml 1.2.1+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ff2a8126fff0f8c839080d619675a585e61720bb12d2baceae45769fd66f155" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a347dcabdae9c31b0825fd6a8bed285ec9c2acb89c47827126d52fa4f59cece3" +dependencies = [ + "fnv", + "uuid", + "web-time", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "combine" +version = "4.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.2", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f97d3416d5337d15c4b0d78c8c91df922ca143f57ec20e2d1c188333047ef04" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d045de693cb712d0b22c6a64be5b953f67b3ce00ab5ad3dd5d8b441886ab8e1a" +dependencies = [ + "quote", + "syn 3.0.9", +] + +[[package]] +name = "ctor" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d" + +[[package]] +name = "darling" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 3.0.9", +] + +[[package]] +name = "darling_macro" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" +dependencies = [ + "darling_core", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "defmt" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a0795ac740cb54d7b3775c9c17345f6aaecf884c01733b39d6017738e0e3e1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "840bb056bd99c495c9188198a258ab787c79f306d864db3e6e8451442d6d8366" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "defmt-parser" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71958f69fb2de856c5dd51ac9377e986ca1aa5919f931bf5395a807277f889a0" +dependencies = [ + "thiserror 2.0.21", +] + +[[package]] +name = "deranged" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e9de72ce2ad1f90dc62fa25f0f430ef85eb4b0d8fa0be4f30373bc40a21d28e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.2", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dom_query" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac5fca71e65e94cc718a6e2af65d6e0f9c6027751c2aa562fbb5087fda639bc" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6168487b5e7eb2673e6efe1200b1848016e436a6ca0b8031bf04bd9d493a249f" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.2.1+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "erased-serde" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e59b668157f32d3459d4bd634861a06ffa2503a461ca5f0916b9f0252605e66" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" + +[[package]] +name = "flate2" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "626d4928326b646e70f65ed36fc7cf9f206bdb68a0019ee7c6b4709da4e084c4" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.2", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3e324da4c95177d6291d4c8730197c0d1822f8a9766814a4a44fa5ab797c9c" +dependencies = [ + "atomic-waker", + "bytes", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75e94d66081e4d8dcd098bd8dc5254800fea14a26389b298410158d834de167" +dependencies = [ + "byteorder", + "png", +] + +[[package]] +name = "icu_collections" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "404a3b8c486f205c87fe7f23ab6894d84a4ad76d6e4671ae1981ba7def87c599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20f0088bfb7441f7a37fa0c9e20cb1e9cba1cac6b9ce8cd2a6dc44294f8e8cbc" + +[[package]] +name = "icu_properties" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ea83d991c56158f7cf898bf86436c1dfe7c07ee82fe88951f2695c9f37a379f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98f29af82d02b163fe50161388350e743bb3107cc8e8c2f7314cb505faddfcf" + +[[package]] +name = "icu_provider" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4200d433cbd5178df7797c9c2e75b348b728e39631cf14520d1e2fc424201f4" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jiff" +version = "0.2.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ac703058abac89e5c39b0b879dce2b6b14a4ba36600c7e7e3d0c60a6883ef3c" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aee43309eb53bafb8c36d35d6e444769c8f8e6206a9fbf7546f5db9d7d4a0e93" +dependencies = [ + "defmt", + "log", +] + +[[package]] +name = "jiff-static" +version = "0.2.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dc32470c67eb7d0e9ccb74303fa925313dc04cecb1aa048beaa6f391e36a93d" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa8377070c6bae868759445e5a77f66d84f0b72f3a054bfb00e6d038b8282da7" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "js-sys" +version = "0.3.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc09d2154098bbf4ef54eab6b117a3c24cbbffe94046b16ee6d6d4510584d3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7421438de105a0827e44fadd05377727847d717c80ce29a229f85fd04c427b72" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 2.0.21", +] + +[[package]] +name = "jsonptr" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d" +dependencies = [ + "bitflags 2.13.2", + "serde", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" +dependencies = [ + "libc", +] + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" + +[[package]] +name = "markup5ever" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "mathscrap" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-fs", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5848f5a9e8117cf94f2f90faac24a87ff81600ff8628c1e3deaab572e76bfa1" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cca139e57da4383727f189e43e66e84fa372347027fc996f9f1a007ec0a37996" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png", + "serde", + "thiserror 2.0.21", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.2", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.2", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.2", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.2", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plist" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327a4cb2ad49dc48adfdc9da89e22c64d488b508c67a044d71ba729e4521a285" +dependencies = [ + "base64 0.23.1", + "indexmap 2.14.2", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.19+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.2", +] + +[[package]] +name = "redox_users" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60dc65c0ff1a7ae1294b0c67b9f14baf70b644404010370171787bfac1038fc0" +dependencies = [ + "libredox", + "thiserror 2.0.21", +] + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" +dependencies = [ + "base64 0.23.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rfd" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "libc", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "percent-encoding", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab508826f74a77ca9d5aba6ff19b522583ee3eaf28a19384ff3d0e5835fadf6e" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.119", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8adfa1c298912827b8a28b223b3b874357397ae706e6190acd9bf28cee99114d" +dependencies = [ + "bitflags 2.13.2", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c187e0a369471c550af028cba56d4f4fa2a489ad28151ae95a4d340a2a8b782" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9adc193c780ef8f159aee8b61e2d5801aaa555e6eb0947fe45530ec506296f" +dependencies = [ + "base64 0.23.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.2", + "jiff", + "schemars 0.9.0", + "schemars 1.2.3", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e17bbc68e28663bbbb90df47e058aa7eda4fb445b89fe70457bb94fbccf6e49" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "siphasher" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.16.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "149309498a5f7334b12ddae0cad8aba81ce785fe91755068867e017cf98fee46" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_vcruntime" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff7589a1859b09232522bb9edf076ca7fcf14ded96685d3fa629db73023ffbb7" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefd87076fd385308ee4aff597256c902f1fbb4d031de1558efc12f8ffefadd9" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.37.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5777b06fe6435126ae8d816eba269e050e1e10d302cf5461e9c49eccabc3548" +dependencies = [ + "bitflags 2.13.2", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f316751027fc2644221d1a448564d519efb07067fda584879d9cda966391d212" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.21", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adddb0e648752aaac5da9c3c3c6a68e1e87cab55e3b757b18a9441ef8c3ac614" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "static_vcruntime", + "strsim", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb37631cf71dab5e8ab55e1bdd701e18a52653d90b78e71d0e5598b1e0a61316" +dependencies = [ + "base64 0.23.1", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.119", + "tauri-utils", + "thiserror 2.0.21", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc38db58854c10fb5f91a887ed8bf82d3239bb92b5d8db86eda09e96d9963d1" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67fb21a77e8f94e8beb3325e583102488659365af0a3948b7ea244c5b779c291" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce0fb9c2b6d34a9720627ed930ebc8af26f61e648695aecaeb03d5c33c5896f5" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.21", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d58eb1d4984e535f47a642ffec09a2da6f5c18a12b5902d12126cb7626d628b" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.21", + "toml 1.2.1+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-runtime" +version = "2.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c55766d99cea69055904d1735b93c2049302b2e09effd462120deb5581d78e9" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.21", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1aa9d8b7a120f879f44fdc1053f7b7dbf416bf0bbc6a72b714dec46b782b52f" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "362ab21cd4cc713f2a02b1a9b5688198b1541cfb6907c4281e9190e0f0ef4aa3" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.21", + "toml 1.2.1+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c52bb6904475a6ca662f15ef787f18ade61d5740a1ee12c45d3b748efdb6f796" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.2.1+spec-1.1.0", +] + +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "tokio" +version = "1.54.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "1.2.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5755c51863cdac711f1956baf8eb9588e060755f7d3c4c979e70ea1e1f32cd7" +dependencies = [ + "indexmap 2.14.2", + "serde_core", + "serde_spanned 1.1.3", + "toml_datetime 1.1.3+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.5", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173f25c883cd2176a2d8212155638c0a60d484a9d84546d270866f40f56b6e49" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.2", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.2", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.19+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd43688c4858d7264ae1cf9a9f874a67c3feb74a883b1cd7bebd6a4ea8307218" +dependencies = [ + "indexmap 2.14.2", + "toml_datetime 1.1.3+spec-1.1.0", + "toml_parser", + "winnow 1.0.5", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow 1.0.5", +] + +[[package]] +name = "toml_writer" +version = "1.2.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009dbb7700f11c55ef83aebbe4c0e9cf1f1ac0844bd8d6f00dc38f302026e13a" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.2", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b9c52859a94554803ccd4a24b98f74148ebc73b90676d783f3490b1bff9d72" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png", + "serde", + "thiserror 2.0.21", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df16f50ef4cc145211879a3867ba757076b25dfee812040dcb0658bd9ae7904b" +dependencies = [ + "icu_properties", + "regex", + "serde", + "url", +] + +[[package]] +name = "utf8_iter" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af3bd9d3e571c26fcce9d033b87ea5ab061b8d2ffe9b7a5675d810d68f03edf0" +dependencies = [ + "utf_types", +] + +[[package]] +name = "utf_types" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3b425286980a367278453f3b6d28cdcb134b574e5a35824a6ff5eebad50bb6" + +[[package]] +name = "uuid" +version = "1.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a06f08a856b009bc7bbaf7d3e79808595d032dd19074c0c2ef26e49e03e6bdc" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec4cdd0dd910afe868b7ef477227d8d538b46b3075031afee8a9f2acb0a2ed0b" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0181c14ddd6c7b8b0b0132a5f1ea2f32be0d893cd3ea97b6e3b28886b8a3d1c" +dependencies = [ + "cfg-if", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e14e0327cb5368ad60ccb22d04a9827a693c95a926d8d125e9fa39e0baec501" +dependencies = [ + "js-sys", + "tokio", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e931a02a39abdbf66966fc35902b5ed58344750a70885bed4ebb3d8eaf28ed9" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ca4b54280e455bd831e33a6b8e734baf432fe63fbc35c42ac14ce67a043a53a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.9", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b1f9c3798725016c6ee5f2f57869ec0831e8bbc0ab0310a86e787e97f41389e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d179596ff5ec0f6de4db64d54011bde6289cc49603d3a6e4ce3f329207ab88" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba8b815c1b593dc0baf78dd0f4fc8fdb2de53198fb1163738093e9a311c33fb3" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "webview2-com-sys" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a07132775117d6065853d9d1178157b8c90e228de47129d6bce2c7edebedfb" +dependencies = [ + "thiserror 2.0.21", + "windows", + "windows-core", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "111e51caca442cafd9bab396628ac4d814880eaea4e63dfd76a12e9f342b5580" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "windows-sys 0.61.2", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3b93199df0ab874cdf0c6a94f0a0dc3423fe41f1cafb62b8bd7b1deb277aef" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "wry" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759c50ddad0ef9913bb5496cb9d509a50433a3b78cb0238d61105526589cf0e9" +dependencies = [ + "base64 0.23.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.21", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417ef3fcf6c300b7dbd25c85531a746931c02ad536f3cf4786ed1e88223340b3" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3743ddbf47738d929cd1ae2e199d40d0162979dfe31aef8ae646d2bf00bf97a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..389b24c --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "mathscrap" +version = "0.1.0" +description = "Kraft-paper math scrapbook: Freeform-like sheet, shared plot, geometry inference (Rust/WASM)." +authors = ["DNTOF"] +license = "Apache-2.0" +edition = "2021" +rust-version = "1.90" + +[lib] +name = "mathscrap_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2.7.4", features = [] } + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tauri = { version = "=2.12.4", features = [] } +tauri-plugin-dialog = "2.8.4" +tauri-plugin-fs = "2.7.1" + +[profile.release] +panic = "abort" +codegen-units = 1 +lto = true +opt-level = "s" +strip = true diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json new file mode 100644 index 0000000..d259aa8 --- /dev/null +++ b/src-tauri/capabilities/default.json @@ -0,0 +1,18 @@ +{ + "identifier": "default", + "description": "MathScrap window. Sheet export can write only the file the person picks under Documents, Downloads, or Desktop.", + "windows": ["main"], + "permissions": [ + "core:default", + "dialog:default", + "fs:allow-write-text-file", + { + "identifier": "fs:scope", + "allow": [ + { "path": "$DOCUMENT/**" }, + { "path": "$DOWNLOAD/**" }, + { "path": "$DESKTOP/**" } + ] + } + ] +} diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100644 index 0000000..19bf28d Binary files /dev/null and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000..c84ab53 Binary files /dev/null and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png new file mode 100644 index 0000000..6a53283 Binary files /dev/null and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/64x64.png b/src-tauri/icons/64x64.png new file mode 100644 index 0000000..d873b6d Binary files /dev/null and b/src-tauri/icons/64x64.png differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000..8e82111 Binary files /dev/null and b/src-tauri/icons/Square107x107Logo.png differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000..bab4276 Binary files /dev/null and b/src-tauri/icons/Square142x142Logo.png differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 0000000..783ef3e Binary files /dev/null and b/src-tauri/icons/Square150x150Logo.png differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000..2571fa7 Binary files /dev/null and b/src-tauri/icons/Square284x284Logo.png differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000..306f84e Binary files /dev/null and b/src-tauri/icons/Square30x30Logo.png differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000..b54ea1c Binary files /dev/null and b/src-tauri/icons/Square310x310Logo.png differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000..c939582 Binary files /dev/null and b/src-tauri/icons/Square44x44Logo.png differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000..9c462cd Binary files /dev/null and b/src-tauri/icons/Square71x71Logo.png differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 0000000..6c464e5 Binary files /dev/null and b/src-tauri/icons/Square89x89Logo.png differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png new file mode 100644 index 0000000..ad6f72e Binary files /dev/null and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml b/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..2ffbf24 --- /dev/null +++ b/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..8a460c1 Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..4e3fbb8 Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..5b76651 Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..8e99280 Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..bab997b Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..98ae005 Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..6f3fe99 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..f007dac Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..73f8cbd Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..8518ca0 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..3794779 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..aa7e8e8 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..cd59a91 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..2c7e028 Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..149d3fd Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/src-tauri/icons/android/values/ic_launcher_background.xml b/src-tauri/icons/android/values/ic_launcher_background.xml new file mode 100644 index 0000000..3363ca5 --- /dev/null +++ b/src-tauri/icons/android/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #C9A473 + \ No newline at end of file diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns new file mode 100644 index 0000000..99ccf74 Binary files /dev/null and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico new file mode 100644 index 0000000..57eca55 Binary files /dev/null and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png new file mode 100644 index 0000000..848638b Binary files /dev/null and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@1x.png b/src-tauri/icons/ios/AppIcon-20x20@1x.png new file mode 100644 index 0000000..de57b33 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png new file mode 100644 index 0000000..2edb569 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x.png b/src-tauri/icons/ios/AppIcon-20x20@2x.png new file mode 100644 index 0000000..2edb569 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@3x.png b/src-tauri/icons/ios/AppIcon-20x20@3x.png new file mode 100644 index 0000000..748d815 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@1x.png b/src-tauri/icons/ios/AppIcon-29x29@1x.png new file mode 100644 index 0000000..4971af0 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png new file mode 100644 index 0000000..e2dfad4 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x.png b/src-tauri/icons/ios/AppIcon-29x29@2x.png new file mode 100644 index 0000000..e2dfad4 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@3x.png b/src-tauri/icons/ios/AppIcon-29x29@3x.png new file mode 100644 index 0000000..d9218aa Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@1x.png b/src-tauri/icons/ios/AppIcon-40x40@1x.png new file mode 100644 index 0000000..2edb569 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png new file mode 100644 index 0000000..cc6d499 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x.png b/src-tauri/icons/ios/AppIcon-40x40@2x.png new file mode 100644 index 0000000..cc6d499 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@3x.png b/src-tauri/icons/ios/AppIcon-40x40@3x.png new file mode 100644 index 0000000..cc7ef21 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-512@2x.png b/src-tauri/icons/ios/AppIcon-512@2x.png new file mode 100644 index 0000000..e494fc9 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-512@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-60x60@2x.png b/src-tauri/icons/ios/AppIcon-60x60@2x.png new file mode 100644 index 0000000..cc7ef21 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-60x60@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-60x60@3x.png b/src-tauri/icons/ios/AppIcon-60x60@3x.png new file mode 100644 index 0000000..a187887 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-60x60@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-76x76@1x.png b/src-tauri/icons/ios/AppIcon-76x76@1x.png new file mode 100644 index 0000000..5f59d2e Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-76x76@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-76x76@2x.png b/src-tauri/icons/ios/AppIcon-76x76@2x.png new file mode 100644 index 0000000..d9818bc Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-76x76@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png new file mode 100644 index 0000000..9a4d74c Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png differ diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..2c48edb --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,8 @@ +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_fs::init()) + .run(tauri::generate_context!()) + .expect("MathScrap failed to start"); +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..2f14a55 --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,5 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + mathscrap_lib::run(); +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..38c04e4 --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,56 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "MathScrap", + "version": "0.1.0", + "identifier": "app.mathscrap", + "build": { + "beforeDevCommand": "npm run dev", + "devUrl": "http://127.0.0.1:47321", + "beforeBuildCommand": "npm run build && node scripts/verify-packaged-dist.mjs", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "label": "main", + "title": "MathScrap", + "width": 1280, + "height": 800, + "minWidth": 360, + "minHeight": 480, + "dragDropEnabled": false + } + ], + "security": { + "csp": "default-src 'self' ipc: http://ipc.localhost https://ipc.localhost; script-src 'self' 'unsafe-eval' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost https://ipc.localhost http: https: blob: data:; worker-src 'self' blob:; media-src 'self' blob:" + } + }, + "bundle": { + "active": true, + "copyright": "Copyright 2026 DNTOF", + "category": "Education", + "shortDescription": "Kraft-paper math scrapbook", + "longDescription": "MathScrap keeps a blank kraft sheet, on-device CoMER handwriting recognition, and the Rust geometry engine.", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "android": { + "minSdkVersion": 24 + }, + "iOS": { + "minimumSystemVersion": "16.4" + }, + "windows": { + "webviewInstallMode": { + "type": "skip" + }, + "nsis": { + "installMode": "currentUser" + } + } + } +} diff --git a/src/App.tsx b/src/App.tsx index 09b8bcd..34d570a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,23 +2,27 @@ import { Loader2 } from 'lucide-react' import { cn } from '@/lib/utils' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { toast } from 'sonner' -import { startBrowserBridge } from '@/ai/browserBridge' import { createToolBridge } from '@/ai/tools' import { AssistSheet } from '@/components/AssistSheet' import { pushPlotView } from '@/plot/remoteView' import { Board, type PenSettings } from '@/board/Board' -import { strokeBounds } from '@/board/strokes' +import { strokeBounds } from '@/board/strokeGeom' import { uid, type Card, type Doc, type Stroke, type Tool, type View } from '@/board/types' +import { parseSheet } from '@/board/sheetFile' +import { exportSheetFile, readSheetFile } from '@/board/sheetTransfer' import { blankDoc, useDocument } from '@/board/useDocument' import { SettingsSheet, type Prefs } from '@/components/SettingsSheet' import { Toolbar } from '@/components/Toolbar' import { TopBar } from '@/components/TopBar' import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { useMediaQuery } from '@/hooks/useMediaQuery' -import { Composer, insertSnippet } from '@/input/Composer' +import { Composer } from '@/input/Composer' +import { insertSnippet } from '@/input/snippets' import { MathPalette } from '@/input/MathPalette' import { ShortcutHints } from '@/input/ShortcutHints' +import { useI18n } from '@/i18n/useI18n' import { pageGroups } from '@/math/page' +import { summarizeCard } from '@/math/scene' import { parseCard } from '@/math/statements' import { clusterStrokes, mockRecognizer, OCR_ACCEPT } from '@/ocr/recognizer' import { PlotButton } from '@/plot/PlotButton' @@ -98,6 +102,14 @@ function freeSpot(x: number, y: number, cards: Card[], h = 40) { return { x, y: py } } +function docNow(ref: { current: Doc }) { + return ref.current +} + +function unmarkedInk(ref: { current: Doc }) { + return docNow(ref).strokes.filter((s) => !s.recognized && s.style !== 'marker') +} + interface ComposerState { open: boolean /** world position the formula will be written at */ @@ -106,11 +118,10 @@ interface ComposerState { } export default function App() { + const { t } = useI18n() const compact = useMediaQuery('(max-width: 767px)') const { doc, docRef, commit, preview, undo, redo, canUndo, canRedo } = useDocument() const [view, setView] = useState(() => fitView(doc, window.innerWidth < 768)) - const viewRef = useRef(view) - viewRef.current = view const [tool, setTool] = useState('pen') const [pen, setPen] = useState({ color: '#2b2118', width: 3, style: 'ink' }) const [prefs, setPrefsState] = useState(loadPrefs) @@ -128,6 +139,7 @@ export default function App() { const dragBase = useRef(null) const textareaRef = useRef(null) const sheetRef = useRef(null) + const importRef = useRef(null) const [sheet, setSheet] = useState({ w: window.innerWidth, h: window.innerHeight }) // the docked pane takes a slice of the window; writing spots and fitting follow the paper @@ -150,11 +162,10 @@ export default function App() { /** Where the next formula goes when you didn't tap a spot: the left third of the view. */ const writingSpot = useCallback(() => { - const v = viewRef.current const sx = compact ? 24 : Math.max(120, sheet.w * 0.3) const sy = compact ? 110 : sheet.h * 0.3 - return freeSpot((sx - v.x) / v.scale, (sy - v.y) / v.scale, docRef.current.cards) - }, [compact, docRef, sheet]) + return freeSpot((sx - view.x) / view.scale, (sy - view.y) / view.scale, doc.cards) + }, [compact, doc.cards, sheet, view]) const openPlot = useCallback((focusId: string | null = null) => setPlot({ open: true, details: false, full: false, focusId }), []) const ensurePlot = useCallback(() => setPlot((p) => (p.open ? p : { open: true, details: false, full: false, focusId: p.focusId })), []) @@ -178,7 +189,7 @@ export default function App() { const placed: Card[] = [] let y = start.y for (const text of blocks) { - const spot = freeSpot(start.x, y, [...docRef.current.cards, ...placed]) + const spot = freeSpot(start.x, y, [...doc.cards, ...placed]) const card: Card = { id: uid(), text, x: spot.x, y: spot.y } placed.push(card) y = spot.y + footprint(card).h + 16 @@ -186,7 +197,7 @@ export default function App() { commit((d) => ({ ...d, cards: [...d.cards, ...placed] })) return placed }, - [commit, docRef, writingSpot], + [commit, doc.cards, writingSpot], ) const updateCard = useCallback((id: string, text: string) => commit((d) => ({ ...d, cards: d.cards.map((c) => (c.id === id ? { ...c, text } : c)) })), [commit]) @@ -195,9 +206,9 @@ export default function App() { (id: string) => { commit((d) => ({ ...d, cards: d.cards.filter((c) => c.id !== id) })) setSelected(null) - toast('擦掉了一个式子', { action: { label: '撤销', onClick: undo } }) + toast(t('clearedOne'), { action: { label: t('undo'), onClick: undo } }) }, - [commit, undo], + [commit, t, undo], ) const closeComposer = () => { @@ -258,7 +269,9 @@ export default function App() { strokes: keep ? d.strokes.map((s) => (ids.has(s.id) ? { ...s, recognized: true } : s)) : d.strokes.filter((s) => !ids.has(s.id)), cards: [...d.cards, card], })) - setPlot((p) => (p.open ? { ...p, focusId: card.id } : { open: true, details: false, full: false, focusId: card.id })) + if (summarizeCard(card.text).plottable) { + setPlot((p) => (p.open ? { ...p, focusId: card.id } : { open: true, details: false, full: false, focusId: card.id })) + } return card }, [commit], @@ -267,7 +280,7 @@ export default function App() { const acceptInk = useCallback( (text: string, strokes: Stroke[]) => { const ids = new Set(strokes.map((s) => s.id)) - const still = docRef.current.strokes.filter((s) => ids.has(s.id) && !s.recognized) + const still = docNow(docRef).strokes.filter((s) => ids.has(s.id) && !s.recognized) if (still.length !== strokes.length || !canWriteFormula(text)) return null return commitRecognized(text, still, true) }, @@ -275,9 +288,9 @@ export default function App() { ) const recognize = useCallback(() => { - const pool = docRef.current.strokes.filter((s) => !s.recognized && s.style !== 'marker').slice(-24) + const pool = unmarkedInk(docRef).slice(-24) if (!pool.length) { - toast('先用笔在纸上写一个式子') + toast(t('writeFirst')) return } const clusters = clusterStrokes(pool) @@ -290,10 +303,10 @@ export default function App() { const best = cands[0] if (best && best.confidence >= OCR_ACCEPT && acceptInk(best.text, cl)) wrote += 1 pending -= 1 - if (pending === 0 && wrote === 0) toast('没认出来') + if (pending === 0 && wrote === 0) toast(t('notRecognized')) }) } - }, [acceptInk, docRef]) + }, [acceptInk, docRef, t]) // Settled ink is read on its own. A confident reading is written onto the sheet and the // corner plot updates. Nothing is drawn over the paper while that happens. @@ -314,7 +327,7 @@ export default function App() { jobs.push( mockRecognizer.recognize(cl).then((cands) => { const ids = new Set(cl.map((s) => s.id)) - const now = docRef.current.strokes.filter((s) => !s.recognized && s.style !== 'marker') + const now = unmarkedInk(docRef) const group = clusterStrokes(now).find((g) => g.some((s) => ids.has(s.id))) const same = !!group && group.length === cl.length && cl.every((s) => group.some((g) => g.id === s.id)) if (!same) return @@ -330,13 +343,13 @@ export default function App() { }) }, 900) return () => clearTimeout(t) - }, [doc.strokes, prefs.autoOcr, composer.open, acceptInk]) + }, [doc.strokes, prefs.autoOcr, composer.open, acceptInk, docRef]) const writeSample = (blocks: string[]) => { const placed = writeBlocks(blocks) setSelected(null) - toast.success(blocks.length > 1 ? `写了 ${blocks.length} 个式子,它们会叠在同一个坐标系里` : '写到纸上了', { - action: { label: '看坐标系', onClick: () => openPlot(placed[0]?.id ?? null) }, + toast.success(blocks.length > 1 ? t('wroteMany', { n: blocks.length }) : t('wroteOne'), { + action: { label: t('seePlot'), onClick: () => openPlot(placed[0]?.id ?? null) }, }) } @@ -383,12 +396,13 @@ export default function App() { return () => window.removeEventListener('keydown', onKey) }, [undo, redo, plot.open, plot.full, recognize, selected, deleteCard, openPlot, closePlot, togglePlot, placeComposer, writingSpot]) - // MCP-ready local tool bridge for experimentation from devtools + // MCP-ready local tool bridge for experimentation from devtools. + // getDoc reads the React state captured for this render, so the bridge is rebuilt when the sheet changes. const bridge = useMemo( () => createToolBridge({ - getDoc: () => docRef.current, - addCard: (t, x, y) => addFormula(t, x, y), + getDoc: () => doc, + addCard: (text, x, y) => addFormula(text, x, y), updateCard, openPlot: (id) => openPlot(id ?? null), setPlotView: (input) => { @@ -396,13 +410,48 @@ export default function App() { return pushPlotView(input) }, }), - [docRef, addFormula, updateCard, openPlot, ensurePlot], + [doc, addFormula, updateCard, openPlot, ensurePlot], ) useEffect(() => { ;(window as unknown as { mathscrap: { tools: typeof bridge } }).mathscrap = { tools: bridge } - return startBrowserBridge((name, args) => bridge.call(name, args)) + // The dev bridge exists only while Vite is running. The production build drops this import. + if (!import.meta.env.DEV) return + let dispose: () => void = () => undefined + let cancelled = false + void import('@/ai/browserBridge').then((mod) => { + if (cancelled) return + dispose = mod.startBrowserBridge((name, args) => bridge.call(name, args)) + }) + return () => { + cancelled = true + dispose() + } }, [bridge]) + const exportPage = useCallback(() => { + void exportSheetFile(docNow(docRef)) + .then((kind) => { + if (kind === 'cancelled') return + toast.success(kind === 'shared' ? t('shared') : t('exported')) + }) + .catch((e: unknown) => toast(e instanceof Error ? e.message : t('exportFailed'))) + }, [docRef, t]) + + const importPage = useCallback( + async (file: File | undefined) => { + if (!file) return + try { + const next = parseSheet(await readSheetFile(file)) + commit(next) + setSelected(null) + toast.success(t('imported'), { action: { label: t('undo'), onClick: undo } }) + } catch (e) { + toast(e instanceof Error ? e.message : t('importFailed')) + } + }, + [commit, t, undo], + ) + const anchor = composer.at ? { left: composer.at.x * view.scale + view.x, top: composer.at.y * view.scale + view.y } : null const showHints = prefs.showHints && composer.open && composerFocused && (!compact || !composerText) const floated = plot.open && !plot.details && !plot.full @@ -438,7 +487,7 @@ export default function App() { onPlot={openPlot} onEdit={editCard} onDelete={deleteCard} - onPenDetected={() => toast.success('检测到手写笔:笔负责书写,手指负责选择、平移和缩放')} + onPenDetected={() => toast.success(t('penDetected'))} > @@ -455,8 +504,23 @@ export default function App() { onClear={() => setConfirmClear(true)} onSettings={() => setSettingsOpen(true)} onAssist={() => setAssistOpen(true)} + onExport={exportPage} + onImport={() => importRef.current?.click()} onSample={writeSample} /> + { + const file = e.target.files?.[0] + e.target.value = '' + void importPage(file) + }} + /> @@ -467,12 +531,14 @@ export default function App() { data-no-board className={cn( 'absolute z-30 overflow-hidden rounded-2xl opacity-80 shadow-[0_10px_28px_rgba(60,40,20,0.22)]', - compact ? 'right-3 bottom-[calc(132px+env(safe-area-inset-bottom))] h-[200px] w-[min(300px,78vw)]' : 'right-5 bottom-[4.75rem] h-[240px] w-[360px]', + compact + ? 'right-[max(0.75rem,env(safe-area-inset-right))] bottom-[calc(132px+env(safe-area-inset-bottom))] h-[200px] w-[min(300px,78vw)]' + : 'right-[max(1.25rem,env(safe-area-inset-right))] bottom-[max(4.75rem,calc(4.75rem+env(safe-area-inset-bottom)))] h-[240px] w-[360px]', )} > - )} @@ -519,22 +585,22 @@ export default function App() { - 清空这一页? - 笔迹和写下的式子都会被擦掉,可以用“撤销”找回。 + {t('clearTitle')} + {t('clearBody')}
@@ -544,11 +610,12 @@ export default function App() { } export function EngineGate({ children, error }: { children: React.ReactNode; error: string | null; ready: boolean }) { + const { t } = useI18n() return error ? (
-
数学引擎没能启动
-

Rust/WASM 模块加载失败:{error}

+
{t('engineFailed')}
+

{t('engineFailedBody', { error })}

) : ( @@ -557,12 +624,13 @@ export function EngineGate({ children, error }: { children: React.ReactNode; err } export function Loading() { + const { t } = useI18n() return (
MathScrap
- 正在加载 Rust 数学引擎… + {t('engineLoading')}
diff --git a/src/ai/browserBridge.ts b/src/ai/browserBridge.ts index 895bb76..bb31c34 100644 --- a/src/ai/browserBridge.ts +++ b/src/ai/browserBridge.ts @@ -1,3 +1,5 @@ +import { t } from '@/i18n/locale' + /** * Polls the Vite dev bridge and runs tool calls against the open page. * The first request waits until after load so Playwright's networkidle can @@ -32,7 +34,7 @@ export function startBrowserBridge(call: (name: string, args: Record; body?: string; signal: AbortSignal }, +): Promise<{ status: number; text: string }> { + try { + const res = await fetch('/__mathscrap/ai/forward', { + method: 'POST', + headers: { 'content-type': 'application/json', accept: 'application/json' }, + body: JSON.stringify({ url, method: init.method, headers: init.headers, body: init.body ? JSON.parse(init.body) : undefined }), + signal: init.signal, + }) + if (res.headers.get('x-mathscrap-proxy') === '1') return { status: res.status, text: await res.text() } + } catch (e) { + if (e instanceof Error && (e.name === 'AbortError' || e.name === 'TimeoutError')) throw e + } + return direct.request(url, init) +} diff --git a/src/ai/browserTransport.ts b/src/ai/browserTransport.ts index 8869b42..b747825 100644 --- a/src/ai/browserTransport.ts +++ b/src/ai/browserTransport.ts @@ -8,24 +8,13 @@ import { directTransport, type AiTransport } from './client' */ export function browserTransport(): AiTransport { const direct = directTransport() + // The packaged shell has no Vite forwarder. Call the address the person typed. + // The forwarder module is imported only for dev, so the production bundle does not name it. + if (import.meta.env.PROD) return direct return { async request(url, init) { - try { - const res = await fetch('/__mathscrap/ai/forward', { - method: 'POST', - headers: { 'content-type': 'application/json', accept: 'application/json' }, - body: JSON.stringify({ url, method: init.method, headers: init.headers, body: init.body ? JSON.parse(init.body) : undefined }), - signal: init.signal, - }) - if (res.headers.get('x-mathscrap-proxy') === '1') return { status: res.status, text: await res.text() } - } catch (e) { - if (isAbort(e)) throw e - } - return direct.request(url, init) + const { forwardThroughDevServer } = await import('./browserForward') + return forwardThroughDevServer(direct, url, init) }, } } - -function isAbort(e: unknown) { - return e instanceof Error && (e.name === 'AbortError' || e.name === 'TimeoutError') -} diff --git a/src/ai/client.ts b/src/ai/client.ts index 061022f..0e9c73c 100644 --- a/src/ai/client.ts +++ b/src/ai/client.ts @@ -1,3 +1,4 @@ +import { getLang, t } from '../i18n/locale.ts' import { joinUrl, missingConfig, resolveBaseUrl, type AiConfig } from './config.ts' import { TOOL_SPECS, type ToolSpec } from './specs.ts' @@ -33,7 +34,7 @@ const TEST_TIMEOUT_MS = 12_000 const CHAT_TIMEOUT_MS = 30_000 const MAX_ROUNDS = 4 -const SYSTEM = `你是 MathScrap 的纸面推导助手。用户在一张牛皮纸上写公式,整页共用一套坐标系。数值和几何推导交给工具 solve_analyze(Rust),不要心算代替它。 +const SYSTEM_ZH = `你是 MathScrap 的纸面推导助手。用户在一张牛皮纸上写公式,整页共用一套坐标系。数值和几何推导交给工具 solve_analyze(Rust),不要心算代替它。 写法: - 使用 ASCII 数学,例如 y = x^2 - 2、△ABC: a=3, b=4, C=90°。 @@ -50,6 +51,27 @@ const SYSTEM = `你是 MathScrap 的纸面推导助手。用户在一张牛皮 {"add":["要写到纸上的式子"],"analyze":true,"note":"一句说明"} add 可以省略。` +const SYSTEM_EN = `You are MathScrap's on-paper derivation assistant. The user writes formulas on one kraft sheet that shares one coordinate system. Hand numeric and geometric derivation to the tool solve_analyze (Rust). Do not do that arithmetic yourself. + +Notation: +- Use ASCII math, for example y = x^2 - 2 or △ABC: a=3, b=4, C=90°. +- A vector must be bold with an equals sign: **b** = (1, 2) or **a** = (1, 2, 2). Do not write b = (1, 2). +- Name a point with a capital letter: A(0, 0). + +What to do: +- To write the next step on the sheet, call page_add_formula (one useful formula) or page_update_formula. +- For intersections, solving a triangle, or an unknown, call solve_analyze with texts as the formula array. +- To change ranges, the 3D camera, or which formulas are shown, call plot_set_view. To read ink points, call ink_read. +- Then explain the step in one or two English sentences. Do not invent a formula you are not sure about. + +If the endpoint does not support tool calls, output only one JSON object and no other text: +{"add":["formula to write on the sheet"],"analyze":true,"note":"one sentence"} +add may be omitted.` + +function systemPrompt() { + return getLang() === 'zh' ? SYSTEM_ZH : SYSTEM_EN +} + interface Msg { role: 'system' | 'user' | 'assistant' | 'tool' content: string @@ -88,14 +110,14 @@ export async function testConnection(cfg: AiConfig, transport: AiTransport = dir timeout: TEST_TIMEOUT_MS, }) if (status < 200 || status >= 300) throw httpError(status, text) - return `已连通 Anthropic(${cfg.model.trim()})` + return t('connectedAnthropic', { model: cfg.model.trim() }) } const headers = openaiHeaders(cfg) const models = await send(transport, joinUrl(base, 'models'), { method: 'GET', headers, signal, timeout: TEST_TIMEOUT_MS }) if (models.status >= 200 && models.status < 300) { const ids = modelIds(models.text) const name = cfg.model.trim() - if (ids.length && !ids.includes(name)) throw new AiError(`连上了,但模型列表里没有「${name}」`) + if (ids.length && !ids.includes(name)) throw new AiError(t('modelMissing', { name })) return connectedLabel(cfg) } const authFailed = models.status === 401 || models.status === 403 @@ -126,8 +148,8 @@ export async function assist(opts: { const transport = opts.transport ?? directTransport() const steps: AssistStep[] = [] const messages: Msg[] = [ - { role: 'system', content: SYSTEM }, - { role: 'user', content: userPrompt(opts.formulas, opts.strokes ?? 0, opts.prompt.trim() || '帮我推一步') }, + { role: 'system', content: systemPrompt() }, + { role: 'user', content: userPrompt(opts.formulas, opts.strokes ?? 0, opts.prompt.trim() || t('assist')) }, ] let text = '' for (let round = 0; round < MAX_ROUNDS; round++) { @@ -137,7 +159,7 @@ export async function assist(opts: { const suggestion = round === 0 ? extractSuggestions(res.text) : null if (suggestion && (suggestion.add.length || suggestion.analyze)) { const added = applySuggestion(suggestion, opts.formulas, opts.callTool, steps) - text = suggestion.note || (added ? '已写到纸上。' : text) + text = suggestion.note || (added ? t('wroteOnSheet') : text) } break } @@ -147,13 +169,13 @@ export async function assist(opts: { try { result = opts.callTool(call.name, call.arguments) } catch (e) { - result = { error: e instanceof Error ? e.message : '工具执行失败' } + result = { error: e instanceof Error ? e.message : t('toolFailed') } } steps.push({ name: call.name, args: call.arguments, result }) messages.push({ role: 'tool', content: clip(result), toolCallId: call.id, name: call.name }) } } - if (!text && steps.length) text = '已按工具结果处理。' + if (!text && steps.length) text = t('handledByTools') return { text, steps } } @@ -184,7 +206,7 @@ function applySuggestion(suggestion: { add: string[]; analyze: boolean }, formul try { result = callTool('page_add_formula', args) } catch (e) { - result = { error: e instanceof Error ? e.message : '工具执行失败' } + result = { error: e instanceof Error ? e.message : t('toolFailed') } } steps.push({ name: 'page_add_formula', args, result }) } @@ -194,7 +216,7 @@ function applySuggestion(suggestion: { add: string[]; analyze: boolean }, formul try { result = callTool('solve_analyze', args) } catch (e) { - result = { error: e instanceof Error ? e.message : '工具执行失败' } + result = { error: e instanceof Error ? e.message : t('toolFailed') } } steps.push({ name: 'solve_analyze', args, result }) } @@ -309,7 +331,7 @@ function parseJson(text: string): Record { const data = JSON.parse(text) as unknown return data && typeof data === 'object' ? (data as Record) : {} } catch { - throw new AiError('接口返回的不是 JSON') + throw new AiError(t('notJson')) } } @@ -318,7 +340,7 @@ function errorMessage(data: Record): string | null { if (!err) return null if (typeof err === 'string') return err if (typeof err === 'object' && err && 'message' in err && typeof (err as { message: unknown }).message === 'string') return (err as { message: string }).message - return '接口返回错误' + return t('apiError') } function parseArgs(raw: unknown): Record { @@ -346,8 +368,8 @@ function modelIds(text: string): string[] { function userPrompt(formulas: FormulaRef[], strokes: number, prompt: string) { const lines = formulas.slice(0, 40).map((f) => `- [${f.id}] ${f.text.replace(/\s+/g, ' ').slice(0, 500)}`) - const page = lines.length ? lines.join('\n') : '(纸上还没有公式)' - return `纸上的公式:\n${page}\n手写笔迹 ${strokes} 条。\n\n用户:${prompt}` + const page = lines.length ? lines.join('\n') : t('emptySheet') + return t('promptSheet', { page, strokes, prompt }) } function clip(value: unknown) { @@ -357,7 +379,7 @@ function clip(value: unknown) { function connectedLabel(cfg: AiConfig) { const name = cfg.model.trim() - return cfg.provider === 'local' ? `已连通本地模型(${name})` : `已连通 OpenAI 兼容接口(${name})` + return cfg.provider === 'local' ? t('connectedLocal', { name }) : t('connectedOpenAI', { name }) } function openaiHeaders(cfg: AiConfig): Record { @@ -378,9 +400,9 @@ function anthropicHeaders(cfg: AiConfig): Record { } function httpError(status: number, text: string) { - if (status === 401 || status === 403) return new AiError(`密钥被拒绝(HTTP ${status})`) + if (status === 401 || status === 403) return new AiError(t('keyRejected', { status })) const snippet = redact(text).replace(/\s+/g, ' ').slice(0, 280) - return new AiError(`接口返回 HTTP ${status}${snippet ? `:${snippet}` : ''}`) + return new AiError(t('httpError', { status, snippet: snippet ? `: ${snippet}` : '' })) } async function send(transport: AiTransport, url: string, init: { method: string; headers: Record; body?: string; signal?: AbortSignal; timeout: number }) { @@ -388,9 +410,9 @@ async function send(transport: AiTransport, url: string, init: { method: string; try { return await transport.request(url, { method: init.method, headers: init.headers, body: init.body, signal: gate.signal }) } catch (e) { - if (isAbort(e)) throw new AiError(init.signal?.aborted ? '已取消' : '连接超时') - const message = e instanceof Error ? e.message : '网络错误' - throw new AiError(`连不上接口:${redact(message)}`) + if (isAbort(e)) throw new AiError(init.signal?.aborted ? t('cancelled') : t('timeout')) + const message = e instanceof Error ? e.message : t('network') + throw new AiError(t('cannotReach', { message: redact(message) })) } finally { gate.cancel() } diff --git a/src/ai/config.ts b/src/ai/config.ts index 46d5a80..f36cb99 100644 --- a/src/ai/config.ts +++ b/src/ai/config.ts @@ -1,3 +1,5 @@ +import { t } from '../i18n/locale.ts' + /** Browser-local AI settings. The API key never leaves this shape except as a request header. */ export type AiProvider = 'openai-compatible' | 'anthropic' | 'local' @@ -38,14 +40,14 @@ export function missingConfig(cfg: AiConfig): string | null { const base = cfg.baseUrl.trim() const key = cfg.apiKey.trim() if (cfg.provider === 'anthropic') { - if (!model && !key) return '还没填写模型名和 API Key' - if (!model) return '还没填写模型名' - if (!key) return '还没填写 API Key' + if (!model && !key) return t('needModelAndKey') + if (!model) return t('needModel') + if (!key) return t('needKey') return null } - if (!base && !model) return '还没填写 Base URL 和模型名' - if (!base) return '还没填写 Base URL' - if (!model) return '还没填写模型名' + if (!base && !model) return t('needBaseAndModel') + if (!base) return t('needBase') + if (!model) return t('needModel') return null } @@ -57,9 +59,9 @@ export function resolveBaseUrl(cfg: AiConfig): string { try { url = new URL(trimmed) } catch { - throw new Error('Base URL 不是合法地址') + throw new Error(t('badBase')) } - if (url.protocol !== 'http:' && url.protocol !== 'https:') throw new Error('Base URL 只支持 http 或 https') + if (url.protocol !== 'http:' && url.protocol !== 'https:') throw new Error(t('badProtocol')) url.username = '' url.password = '' url.hash = '' diff --git a/src/ai/tools.ts b/src/ai/tools.ts index ef45626..6d118b5 100644 --- a/src/ai/tools.ts +++ b/src/ai/tools.ts @@ -1,4 +1,5 @@ import type { Card, Doc } from '@/board/types' +import { t } from '@/i18n/locale' import { analyze } from '@/math/scene' import { emphasizeVectors } from './formulas' import { normalizePlotInput, type PlotViewInput } from '@/plot/remoteView' @@ -26,12 +27,12 @@ export function createToolBridge(host: ToolHost) { }, page_add_formula: (a) => { const text = emphasizeVectors(String(a.text ?? '')).trim() - if (!text) return { error: '公式是空的' } + if (!text) return { error: t('emptyFormula') } return host.addCard(text, finite(a.x), finite(a.y)) }, page_update_formula: (a) => { const text = emphasizeVectors(String(a.text ?? '')).trim() - if (!text) return { error: '公式是空的' } + if (!text) return { error: t('emptyFormula') } host.updateCard(String(a.id ?? ''), text) return { ok: true, id: String(a.id ?? ''), text } }, @@ -78,11 +79,11 @@ export function createToolBridge(host: ToolHost) { specs: TOOL_SPECS, call(name: string, args: Record = {}) { const handler = handlers[name] - if (!handler) return { error: `未知工具 ${name}` } + if (!handler) return { error: t('unknownTool', { name }) } try { return handler(args) } catch (e) { - return { error: e instanceof Error ? e.message : '工具执行失败' } + return { error: e instanceof Error ? e.message : t('toolFailed') } } }, } diff --git a/src/board/Board.tsx b/src/board/Board.tsx index 45fe3dd..0ebbb4d 100644 --- a/src/board/Board.tsx +++ b/src/board/Board.tsx @@ -1,6 +1,7 @@ -import { useCallback, useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react' import { InkFormula } from './InkFormula' -import { hitStroke, StrokeDefs, StrokeShape } from './strokes' +import { hitStroke } from './strokeGeom' +import { StrokeDefs, StrokeShape } from './strokes' import { uid, type Doc, type PenStyle, type Stroke, type Tool, type View } from './types' export interface PenSettings { @@ -49,7 +50,9 @@ export function Board(props: Props) { const erasingRef = useRef>(new Set()) const [space, setSpace] = useState(false) const viewRef = useRef(view) - viewRef.current = view + useLayoutEffect(() => { + viewRef.current = view + }, [view]) const penSeen = useRef(false) const toWorld = useCallback((cx: number, cy: number) => { diff --git a/src/board/InkFormula.tsx b/src/board/InkFormula.tsx index 8e8d1b0..4f69ab2 100644 --- a/src/board/InkFormula.tsx +++ b/src/board/InkFormula.tsx @@ -1,6 +1,7 @@ import { LineChart, Pencil, Trash2 } from 'lucide-react' import { memo, useRef } from 'react' import { Tex } from '@/components/Tex' +import { useI18n } from '@/i18n/useI18n' import { cn } from '@/lib/utils' import { summarizeCard } from '@/math/scene' import type { Card } from './types' @@ -24,6 +25,7 @@ interface Props { /** A formula written straight onto the kraft sheet — no card, just ink. */ export const InkFormula = memo(function InkFormula({ card, scale, inkFirst, movable, selected, hidden, onSelect, onDragStart, onDrag, onDragEnd, onPlot, onEdit, onDelete }: Props) { + const { t } = useI18n() const summary = summarizeCard(card.text) const drag = useRef<{ x: number; y: number; moved: boolean; id: number } | null>(null) @@ -87,15 +89,15 @@ export const InkFormula = memo(function InkFormula({ card, scale, inkFirst, mova onPointerDown={(e) => e.stopPropagation()} data-no-board > - {summary.plottable && ( - )} - diff --git a/src/board/samples.ts b/src/board/samples.ts index bccaf29..f4e0e59 100644 --- a/src/board/samples.ts +++ b/src/board/samples.ts @@ -1,23 +1,25 @@ +import type { MessageKey } from '@/i18n/messages' + /** - * Things to try on a blank sheet (also listed in the README). Each block is written on the - * paper as its own formula, one under another — they all share the page's coordinate system. + * Things to try on a blank sheet. `blocks` are formulas and stay the same in both languages. */ -export const SAMPLES: { title: string; blocks: string[]; note: string }[] = [ - { title: '抛物线与直线', blocks: ['y = x^2 - 2', 'y = 0.5x + 1'], note: '两式叠在同一坐标系 · 交点、零点、极值' }, - { title: '三条曲线叠加', blocks: ['y = sin(x)', 'y = cos(x)', 'y = x/3'], note: '一张坐标系里同时看三条曲线和交点' }, - { title: '一元二次方程', blocks: ['x^2 + 2x + 1 = 0'], note: 'Δ、根、韦达定理、顶点' }, - { title: '解三角形(SAS)', blocks: ['△ABC: a = 5, b = 7, C = 60°'], note: '余弦定理 → 第三边、两角、面积、外接圆…' }, - { title: '坐标几何 + 垂直条件', blocks: ['A(0,0), B(4,0), C(1,t)', 'AC ⊥ BC'], note: '解出 t = ±√3,再推出整个三角形' }, - { title: '圆与直线', blocks: ['x^2 + y^2 - 4x + 2y = 4', 'y = x - 1'], note: '圆心半径 + 交点' }, - { title: '相交弦定理', blocks: ['⊙O: r = 5', '弦 AB、CD 交于 P, PA = 2, PB = 6, PC = 4'], note: 'PA·PB = PC·PD → PD,弦长、弦心距、圆心角' }, - { title: '中点与中位线', blocks: ['A(0,0), B(4,0), C(1,3)', 'M = 中点(A, B), N = 中点(A, C)'], note: '定义中点 → 中位线平行于第三边且等于它的一半' }, - { title: '中线还原三角形', blocks: ['△ABC: b = 4, c = 3, m_a = 2.5'], note: '由中线长反推边长 → 3-4-5 直角三角形' }, - { title: '待定系数', blocks: ['f(x) = ax^2 + bx + c', 'f(0) = 1; f(1) = 0; f(3) = 4'], note: '三个条件 → a, b, c' }, - { title: '直线过两点', blocks: ['y = kx + m, 过(1, 3), 过(2, 5)'], note: 'k = 2, m = 1' }, - { title: '平行四边形判定', blocks: ['A(0,0), B(4,0), C(5,3), D(1,3)'], note: '对边平行 → 平行四边形' }, - { title: '空间曲面', blocks: ['z = sin(x)cos(y)'], note: '3D · 自由旋转' }, - { title: '两平面交线', blocks: ['x + y + z = 3', 'x - y + 2z = 1'], note: '3D · 法向量、夹角、交线' }, - { title: '向量垂直求参数', blocks: ['𝐚 = (1, 2, 2); 𝐛 = (2, 1, k)', '𝐚 ⊥ 𝐛'], note: '3D · k = −2,叉积、夹角' }, - { title: '空间螺旋线', blocks: ['r(t) = (cos(t), sin(t), t/3)'], note: '3D 参数曲线' }, - { title: '球面', blocks: ['x^2 + y^2 + z^2 = 4'], note: '3D 隐式曲面 · 球心半径' }, +export const SAMPLES: { title: string; titleKey: MessageKey; noteKey: MessageKey; blocks: string[]; note: string }[] = [ + { title: '抛物线与直线', titleKey: 'sampleParabola', noteKey: 'sampleParabolaNote', blocks: ['y = x^2 - 2', 'y = 0.5x + 1'], note: '两式叠在同一坐标系 · 交点、零点、极值' }, + { title: '三条曲线叠加', titleKey: 'sampleThree', noteKey: 'sampleThreeNote', blocks: ['y = sin(x)', 'y = cos(x)', 'y = x/3'], note: '一张坐标系里同时看三条曲线和交点' }, + { title: '对数', titleKey: 'sampleLogs', noteKey: 'sampleLogsNote', blocks: ['y = ln(x)', 'y = lg(x)', 'y = log_2(x)'], note: '自然对数、常用对数(底 10)、底数 2。x ≤ 0 没有图' }, + { title: '一元二次方程', titleKey: 'sampleQuad', noteKey: 'sampleQuadNote', blocks: ['x^2 + 2x + 1 = 0'], note: 'Δ、根、韦达定理、顶点' }, + { title: '解三角形(SAS)', titleKey: 'sampleSas', noteKey: 'sampleSasNote', blocks: ['△ABC: a = 5, b = 7, C = 60°'], note: '余弦定理 → 第三边、两角、面积、外接圆…' }, + { title: '坐标几何 + 垂直条件', titleKey: 'sampleCoords', noteKey: 'sampleCoordsNote', blocks: ['A(0,0), B(4,0), C(1,t)', 'AC ⊥ BC'], note: '解出 t = ±√3,再推出整个三角形' }, + { title: '圆与直线', titleKey: 'sampleCircleLine', noteKey: 'sampleCircleLineNote', blocks: ['x^2 + y^2 - 4x + 2y = 4', 'y = x - 1'], note: '圆心半径 + 交点' }, + { title: '相交弦定理', titleKey: 'sampleChords', noteKey: 'sampleChordsNote', blocks: ['⊙O: r = 5', '弦 AB、CD 交于 P, PA = 2, PB = 6, PC = 4'], note: 'PA·PB = PC·PD → PD,弦长、弦心距、圆心角' }, + { title: '中点与中位线', titleKey: 'sampleMid', noteKey: 'sampleMidNote', blocks: ['A(0,0), B(4,0), C(1,3)', 'M = 中点(A, B), N = 中点(A, C)'], note: '定义中点 → 中位线平行于第三边且等于它的一半' }, + { title: '中线还原三角形', titleKey: 'sampleMedian', noteKey: 'sampleMedianNote', blocks: ['△ABC: b = 4, c = 3, m_a = 2.5'], note: '由中线长反推边长 → 3-4-5 直角三角形' }, + { title: '待定系数', titleKey: 'sampleCoeff', noteKey: 'sampleCoeffNote', blocks: ['f(x) = ax^2 + bx + c', 'f(0) = 1; f(1) = 0; f(3) = 4'], note: '三个条件 → a, b, c' }, + { title: '直线过两点', titleKey: 'sampleLine', noteKey: 'sampleLineNote', blocks: ['y = kx + m, 过(1, 3), 过(2, 5)'], note: 'k = 2, m = 1' }, + { title: '平行四边形判定', titleKey: 'samplePara', noteKey: 'sampleParaNote', blocks: ['A(0,0), B(4,0), C(5,3), D(1,3)'], note: '对边平行 → 平行四边形' }, + { title: '空间曲面', titleKey: 'sampleSurface', noteKey: 'sampleSurfaceNote', blocks: ['z = sin(x)cos(y)'], note: '3D · 自由旋转' }, + { title: '两平面交线', titleKey: 'samplePlanes', noteKey: 'samplePlanesNote', blocks: ['x + y + z = 3', 'x - y + 2z = 1'], note: '3D · 法向量、夹角、交线' }, + { title: '向量垂直求参数', titleKey: 'sampleVec', noteKey: 'sampleVecNote', blocks: ['𝐚 = (1, 2, 2); 𝐛 = (2, 1, k)', '𝐚 ⊥ 𝐛'], note: '3D · k = −2,叉积、夹角' }, + { title: '空间螺旋线', titleKey: 'sampleHelix', noteKey: 'sampleHelixNote', blocks: ['r(t) = (cos(t), sin(t), t/3)'], note: '3D 参数曲线' }, + { title: '球面', titleKey: 'sampleSphere', noteKey: 'sampleSphereNote', blocks: ['x^2 + y^2 + z^2 = 4'], note: '3D 隐式曲面 · 球心半径' }, ] diff --git a/src/board/sheetFile.ts b/src/board/sheetFile.ts new file mode 100644 index 0000000..701deca --- /dev/null +++ b/src/board/sheetFile.ts @@ -0,0 +1,182 @@ +import { t } from '@/i18n/locale' +import type { Card, Doc, PenStyle, Sticker, Stroke } from './types' + +export const SHEET_FORMAT = 'mathscrap.sheet' +export const SHEET_VERSION = 1 + +const MAX_BYTES = 8_000_000 +const MAX_STROKES = 4_000 +const MAX_CARDS = 2_000 +const MAX_STICKERS = 500 +const MAX_NUMBERS = 2_000_000 +const MAX_TEXT = 8_000 +const MAX_STICKER_TEXT = 400 +const ID = /^[A-Za-z0-9_-]{1,40}$/ +const COLOR = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/ +const STYLES = new Set(['ink', 'dashed', 'dotted', 'pencil', 'washi', 'marker']) + +export function serializeSheet(doc: Doc): string { + const body = { + format: SHEET_FORMAT, + version: SHEET_VERSION, + doc: copyDoc(doc), + } + const text = JSON.stringify(body) + if (text.length > MAX_BYTES) throw new Error(t('sheetTooBig')) + return text +} + +/** + * Accept only a MathScrap sheet envelope, or the same `{strokes,cards,stickers}` + * object the page already stores. Fields are copied one by one. Strings stay + * data; nothing in the file is evaluated. + */ +export function parseSheet(raw: string): Doc { + if (raw.length > MAX_BYTES) throw new Error(t('fileTooBig')) + let parsed: unknown + try { + parsed = JSON.parse(raw) + } catch { + throw new Error(t('notJsonFile')) + } + const root = record(parsed) + if (!root) throw new Error(t('notASheet')) + if ('format' in root || 'version' in root || 'doc' in root) { + if (root.format !== SHEET_FORMAT) throw new Error(t('notMathScrap')) + if (root.version !== SHEET_VERSION) throw new Error(t('sheetVersion')) + return copyDoc(asDoc(root.doc)) + } + return copyDoc(asDoc(root)) +} + +function copyDoc(doc: Doc): Doc { + return { + strokes: doc.strokes.map(copyStroke), + cards: doc.cards.map(copyCard), + stickers: doc.stickers.map(copySticker), + } +} + +function asDoc(value: unknown): Doc { + const src = record(value) + if (!src || !Array.isArray(src.strokes) || !Array.isArray(src.cards)) throw new Error(t('noInk')) + if (src.strokes.length > MAX_STROKES || src.cards.length > MAX_CARDS) throw new Error(t('tooMuch')) + const stickers = src.stickers == null ? [] : src.stickers + if (!Array.isArray(stickers) || stickers.length > MAX_STICKERS) throw new Error(t('tooMuch')) + let numbers = 0 + const strokes = src.strokes.map((item) => { + const stroke = copyStroke(asStroke(item)) + numbers += stroke.pts.length + if (numbers > MAX_NUMBERS) throw new Error(t('strokeTooLong')) + return stroke + }) + return { + strokes, + cards: src.cards.map((item) => copyCard(asCard(item))), + stickers: stickers.map((item) => copySticker(asSticker(item))), + } +} + +function asStroke(value: unknown): Stroke { + const src = record(value) + if (!src) throw new Error(t('badStroke')) + const pts = src.pts + if (!Array.isArray(pts) || pts.length % 3 !== 0) throw new Error(t('badPoints')) + const style = src.style + if (typeof style !== 'string' || !STYLES.has(style as PenStyle | 'marker')) throw new Error(t('badStyle')) + return { + id: idOf(src.id), + pts: pts.map(num), + color: colorOf(src.color), + width: widthOf(src.width), + style: style as PenStyle | 'marker', + recognized: src.recognized === undefined ? undefined : bool(src.recognized), + createdAt: num(src.createdAt), + } +} + +function copyStroke(stroke: Stroke): Stroke { + const next: Stroke = { + id: idOf(stroke.id), + pts: stroke.pts.map(num), + color: colorOf(stroke.color), + width: widthOf(stroke.width), + style: stroke.style, + createdAt: num(stroke.createdAt), + } + if (!STYLES.has(next.style)) throw new Error(t('badStyle')) + if (next.pts.length % 3 !== 0) throw new Error(t('badPoints')) + if (stroke.recognized !== undefined) next.recognized = bool(stroke.recognized) + return next +} + +function asCard(value: unknown): Card { + const src = record(value) + if (!src) throw new Error(t('badCard')) + return copyCard({ id: idOf(src.id), x: num(src.x), y: num(src.y), text: textOf(src.text, MAX_TEXT) }) +} + +function copyCard(card: Card): Card { + return { id: idOf(card.id), x: num(card.x), y: num(card.y), text: textOf(card.text, MAX_TEXT) } +} + +function asSticker(value: unknown): Sticker { + const src = record(value) + if (!src) throw new Error(t('badSticker')) + return copySticker({ + id: idOf(src.id), + x: num(src.x), + y: num(src.y), + text: textOf(src.text, MAX_STICKER_TEXT), + size: widthOf(src.size), + rot: num(src.rot), + }) +} + +function copySticker(sticker: Sticker): Sticker { + return { + id: idOf(sticker.id), + x: num(sticker.x), + y: num(sticker.y), + text: textOf(sticker.text, MAX_STICKER_TEXT), + size: widthOf(sticker.size), + rot: num(sticker.rot), + } +} + +function record(value: unknown): Record | null { + if (!value || typeof value !== 'object' || Array.isArray(value)) return null + return value as Record +} + +function idOf(value: unknown): string { + if (typeof value !== 'string' || !ID.test(value)) throw new Error(t('badId')) + return value +} + +function num(value: unknown): number { + if (typeof value !== 'number' || !Number.isFinite(value)) throw new Error(t('badNumber')) + return value +} + +function widthOf(value: unknown): number { + const n = num(value) + if (n <= 0 || n > 256) throw new Error(t('badWidth')) + return n +} + +function colorOf(value: unknown): string { + if (typeof value !== 'string' || !COLOR.test(value)) throw new Error(t('badColor')) + return value +} + +function textOf(value: unknown, max: number): string { + if (typeof value !== 'string') throw new Error(t('badText')) + if (value.length > max) throw new Error(t('textTooLong')) + return value +} + +function bool(value: unknown): boolean { + if (typeof value !== 'boolean') throw new Error(t('badBool')) + return value +} diff --git a/src/board/sheetTransfer.ts b/src/board/sheetTransfer.ts new file mode 100644 index 0000000..f9b09fe --- /dev/null +++ b/src/board/sheetTransfer.ts @@ -0,0 +1,64 @@ +import { t } from '@/i18n/locale' +import { serializeSheet } from './sheetFile' +import type { Doc } from './types' + +function suggestedName(): string { + const now = new Date() + const p = (n: number) => String(n).padStart(2, '0') + return `mathscrap-sheet-${now.getFullYear()}${p(now.getMonth() + 1)}${p(now.getDate())}-${p(now.getHours())}${p(now.getMinutes())}.json` +} + +function inTauri(): boolean { + return typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window +} + +/** Write the sheet to a place the person picks. Falls back to a browser download. */ +export async function exportSheetFile(doc: Doc): Promise<'saved' | 'shared' | 'downloaded' | 'cancelled'> { + const text = serializeSheet(doc) + const name = suggestedName() + if (inTauri()) { + try { + const saved = await saveWithDialog(text, name) + if (saved !== 'unsupported') return saved + } catch { + /* the shell refused the folder; try the other two paths */ + } + } + const file = new File([text], name, { type: 'application/json' }) + if (typeof navigator !== 'undefined' && navigator.canShare?.({ files: [file] })) { + await navigator.share({ files: [file], title: 'MathScrap' }) + return 'shared' + } + downloadBlob(file, name) + return 'downloaded' +} + +async function saveWithDialog(text: string, name: string): Promise<'saved' | 'cancelled' | 'unsupported'> { + const dialog = await import('@tauri-apps/plugin-dialog') + const fs = await import('@tauri-apps/plugin-fs') + if (typeof dialog.save !== 'function' || typeof fs.writeTextFile !== 'function') return 'unsupported' + const path = await dialog.save({ + defaultPath: name, + filters: [{ name: 'MathScrap sheet', extensions: ['json'] }], + }) + if (path == null) return 'cancelled' + await fs.writeTextFile(path, text) + return 'saved' +} + +function downloadBlob(file: File, name: string) { + const url = URL.createObjectURL(file) + const a = document.createElement('a') + a.href = url + a.download = name + a.rel = 'noopener' + document.body.appendChild(a) + a.click() + a.remove() + setTimeout(() => URL.revokeObjectURL(url), 1500) +} + +export async function readSheetFile(file: File): Promise { + if (file.size > 8_000_000) throw new Error(t('fileTooBig')) + return file.text() +} diff --git a/src/board/strokeGeom.ts b/src/board/strokeGeom.ts new file mode 100644 index 0000000..04f7ba5 --- /dev/null +++ b/src/board/strokeGeom.ts @@ -0,0 +1,39 @@ +import type { Stroke } from './types' + +export function strokeBounds(strokes: Stroke[]) { + let x0 = Infinity + let y0 = Infinity + let x1 = -Infinity + let y1 = -Infinity + for (const s of strokes) + for (let i = 0; i < s.pts.length; i += 3) { + x0 = Math.min(x0, s.pts[i]) + y0 = Math.min(y0, s.pts[i + 1]) + x1 = Math.max(x1, s.pts[i]) + y1 = Math.max(y1, s.pts[i + 1]) + } + return { x0, y0, x1, y1 } +} + +export function hitStroke(s: Stroke, x: number, y: number, r: number) { + const rr = r + s.width * (s.style === 'marker' || s.style === 'washi' ? 2.5 : 0.8) + const r2 = rr * rr + const p = s.pts + for (let i = 0; i < p.length; i += 3) { + const dx = p[i] - x + const dy = p[i + 1] - y + if (dx * dx + dy * dy <= r2) return true + if (i + 3 < p.length) { + const ex = p[i + 3] - p[i] + const ey = p[i + 4] - p[i + 1] + const l2 = ex * ex + ey * ey + if (l2 > 0) { + const t = Math.max(0, Math.min(1, ((x - p[i]) * ex + (y - p[i + 1]) * ey) / l2)) + const qx = p[i] + t * ex - x + const qy = p[i + 1] + t * ey - y + if (qx * qx + qy * qy <= r2) return true + } + } + } + return false +} diff --git a/src/board/strokes.tsx b/src/board/strokes.tsx index 8f665ec..b5daf05 100644 --- a/src/board/strokes.tsx +++ b/src/board/strokes.tsx @@ -81,41 +81,3 @@ export function StrokeShape({ s }: { s: Stroke }) { } } -export function strokeBounds(strokes: Stroke[]) { - let x0 = Infinity - let y0 = Infinity - let x1 = -Infinity - let y1 = -Infinity - for (const s of strokes) - for (let i = 0; i < s.pts.length; i += 3) { - x0 = Math.min(x0, s.pts[i]) - y0 = Math.min(y0, s.pts[i + 1]) - x1 = Math.max(x1, s.pts[i]) - y1 = Math.max(y1, s.pts[i + 1]) - } - return { x0, y0, x1, y1 } -} - -export function hitStroke(s: Stroke, x: number, y: number, r: number) { - const rr = r + s.width * (s.style === 'marker' || s.style === 'washi' ? 2.5 : 0.8) - const r2 = rr * rr - const p = s.pts - for (let i = 0; i < p.length; i += 3) { - const dx = p[i] - x - const dy = p[i + 1] - y - if (dx * dx + dy * dy <= r2) return true - if (i + 3 < p.length) { - // segment distance for sparse samples - const ex = p[i + 3] - p[i] - const ey = p[i + 4] - p[i + 1] - const l2 = ex * ex + ey * ey - if (l2 > 0) { - const t = Math.max(0, Math.min(1, ((x - p[i]) * ex + (y - p[i + 1]) * ey) / l2)) - const qx = p[i] + t * ex - x - const qy = p[i + 1] + t * ey - y - if (qx * qx + qy * qy <= r2) return true - } - } - } - return false -} diff --git a/src/board/types.ts b/src/board/types.ts index 119b536..c817a97 100644 --- a/src/board/types.ts +++ b/src/board/types.ts @@ -45,11 +45,11 @@ export interface View { } export const INK_COLORS = [ - { name: '墨黑', value: '#2b2118' }, - { name: '赭红', value: '#9b3b2a' }, - { name: '靛蓝', value: '#2f4a6d' }, - { name: '苔绿', value: '#4d6b3c' }, - { name: '姜黄', value: '#b9852a' }, - { name: '粉笔白', value: '#f6eedd' }, + { name: 'ink', value: '#2b2118' }, + { name: 'red', value: '#9b3b2a' }, + { name: 'blue', value: '#2f4a6d' }, + { name: 'green', value: '#4d6b3c' }, + { name: 'yellow', value: '#b9852a' }, + { name: 'white', value: '#f6eedd' }, ] export const uid = () => Math.random().toString(36).slice(2, 10) diff --git a/src/board/useDocument.ts b/src/board/useDocument.ts index 8a88e1e..ec5502b 100644 --- a/src/board/useDocument.ts +++ b/src/board/useDocument.ts @@ -27,9 +27,9 @@ export function useDocument() { const [doc, setDoc] = useState(load) const past = useRef([]) const future = useRef([]) - const [, bump] = useState(0) + const [nav, setNav] = useState({ undo: false, redo: false }) const docRef = useRef(doc) - docRef.current = doc + const remember = () => setNav({ undo: past.current.length > 0, redo: future.current.length > 0 }) useEffect(() => { const t = setTimeout(() => { @@ -50,7 +50,7 @@ export function useDocument() { future.current = [] docRef.current = value setDoc(value) - bump((n) => n + 1) + remember() }, []) const preview = useCallback((next: (d: Doc) => Doc) => { @@ -65,7 +65,7 @@ export function useDocument() { future.current.push(docRef.current) docRef.current = prev setDoc(prev) - bump((n) => n + 1) + remember() }, []) const redo = useCallback(() => { @@ -74,7 +74,7 @@ export function useDocument() { past.current.push(docRef.current) docRef.current = next setDoc(next) - bump((n) => n + 1) + remember() }, []) return { @@ -84,7 +84,7 @@ export function useDocument() { preview, undo, redo, - canUndo: past.current.length > 0, - canRedo: future.current.length > 0, + canUndo: nav.undo, + canRedo: nav.redo, } } diff --git a/src/components/AssistSheet.tsx b/src/components/AssistSheet.tsx index 0b20cd0..92f6c09 100644 --- a/src/components/AssistSheet.tsx +++ b/src/components/AssistSheet.tsx @@ -5,6 +5,8 @@ import { missingConfig } from '@/ai/config' import { browserTransport } from '@/ai/browserTransport' import { loadAiConfig } from '@/ai/storage' import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet' +import { t } from '@/i18n/locale' +import { useI18n } from '@/i18n/useI18n' interface Turn { role: 'user' | 'assistant' @@ -29,6 +31,7 @@ export function AssistSheet({ call: (name: string, args: Record) => unknown onOpenSettings: () => void }) { + const { t: tr } = useI18n() const [prompt, setPrompt] = useState('') const [turns, setTurns] = useState([]) const [running, setRunning] = useState(false) @@ -46,7 +49,7 @@ export function AssistSheet({ toast.error(problem) return } - const question = prompt.trim() || '帮我推一步' + const question = prompt.trim() || tr('assist') setPrompt('') setTurns((prev) => [...prev, { role: 'user', text: question }]) const ac = new AbortController() @@ -63,11 +66,11 @@ export function AssistSheet({ transport: browserTransport(), signal: ac.signal, }) - setTurns((prev) => [...prev, { role: 'assistant', text: out.text || '(模型没有返回文字)', steps: out.steps }]) - if (out.steps.some((step) => step.name === 'page_add_formula' || step.name === 'page_update_formula')) toast.success('已写到纸上') + setTurns((prev) => [...prev, { role: 'assistant', text: out.text || tr('noModelText'), steps: out.steps }]) + if (out.steps.some((step) => step.name === 'page_add_formula' || step.name === 'page_update_formula')) toast.success(tr('wroteOnSheet')) } catch (e) { - const message = e instanceof Error ? e.message : '请求失败' - if (message === '已取消') toast('已取消') + const message = e instanceof Error ? e.message : tr('requestFailed') + if (message === t('cancelled')) toast(tr('cancelled')) else { toast.error(message) setTurns((prev) => [...prev, { role: 'assistant', text: message }]) @@ -82,20 +85,20 @@ export function AssistSheet({ - 帮我推一步 - 把纸上的公式和你的一句请求发给已配置的模型。密钥只留在本机。 + {tr('assistTitle')} + {tr('assistLead')}
{gap && (
- {gap}。补全后才能提问,现在不会发请求。 + {gap}. {tr('assistGap')}
)}
- {turns.length === 0 &&

直接点下面的按钮,或写一句想推进的事,例如「求交点,并把交点写到纸上」。

} + {turns.length === 0 &&

{tr('assistEmpty')}

} {turns.map((turn, i) => (
{turn.text}
@@ -115,14 +118,14 @@ export function AssistSheet({