Compare commits
2 Commits
main
...
windows-na
| Author | SHA1 | Date |
|---|---|---|
|
|
54f2b93aca | |
|
|
8e01eff876 |
|
|
@ -3,6 +3,7 @@ build/ffi/
|
|||
*.o
|
||||
wavs/
|
||||
*.a
|
||||
build/
|
||||
target/
|
||||
examples/vids/
|
||||
assets/old/
|
||||
|
|
|
|||
|
|
@ -19,3 +19,6 @@ tokio = { version = "1", features = ["rt", "rt-multi-thread"] }
|
|||
libc = "0.2"
|
||||
tiny-skia = "0.11"
|
||||
roxmltree = "0.20"
|
||||
|
||||
[target.'cfg(windows)'.build-dependencies]
|
||||
winresource = "0.1"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
fn main() {
|
||||
#[cfg(target_os = "windows")]
|
||||
embed_windows_icon();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn embed_windows_icon() {
|
||||
let manifest = std::path::PathBuf::from(std::env::var_os("CARGO_MANIFEST_DIR").unwrap());
|
||||
let repo_root = manifest.parent().unwrap().parent().unwrap();
|
||||
let icon = repo_root.join("build").join("assets").join("femm.ico");
|
||||
println!("cargo:rerun-if-changed={}", icon.display());
|
||||
if !icon.is_file() {
|
||||
println!("cargo:warning=femm.ico not found at {} - skipping icon embed (run scripts/windows/build.ps1 to generate)", icon.display());
|
||||
return;
|
||||
}
|
||||
let mut res = winresource::WindowsResource::new();
|
||||
res.set_icon(icon.to_str().expect("non-utf8 icon path"));
|
||||
res.compile().expect("winresource compile failed");
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
//! iced shell entry point for the FEMM 4.2 port.
|
||||
|
||||
#![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")]
|
||||
|
||||
mod doc_canvas;
|
||||
mod export;
|
||||
mod kinematic;
|
||||
|
|
@ -380,7 +382,7 @@ impl App {
|
|||
.pick_file();
|
||||
let Some(path) = picked else { return Task::none(); };
|
||||
match svg_io::import_svg(&path) {
|
||||
Ok(doc) => {
|
||||
Ok((doc, warnings)) => {
|
||||
self.doc = doc;
|
||||
self.mesh = None;
|
||||
self.solution = None;
|
||||
|
|
@ -388,8 +390,9 @@ impl App {
|
|||
self.simulation = None;
|
||||
self.source_label = path.file_name().and_then(|s| s.to_str()).unwrap_or("?").to_string();
|
||||
self.source_path = Some(path.clone());
|
||||
let suffix = if warnings.is_empty() { String::new() } else { format!(" ({})", warnings.join("; ")) };
|
||||
self.status = format!(
|
||||
"imported {}: {} nodes, {} segs, {} arcs, {} labels",
|
||||
"imported {}: {} nodes, {} segs, {} arcs, {} labels{suffix}",
|
||||
path.display(),
|
||||
self.doc.nodes.len(),
|
||||
self.doc.segments.len(),
|
||||
|
|
@ -2065,7 +2068,7 @@ fn compute_frame(base: FemmDoc, tracks: Vec<kinematic::Track>, frame_idx: i64, d
|
|||
title: String::from("frame save .pbc failed"),
|
||||
body: format!("{pbc_path:?}\n\n{e}"),
|
||||
})?;
|
||||
let tri_out = std::process::Command::new(&triangle)
|
||||
let tri_out = quiet_command(&triangle)
|
||||
.args([
|
||||
"-p", "-P", "-e", "-A", "-a", "-z", "-Q", "-I",
|
||||
&format!("-q{min_angle}"),
|
||||
|
|
@ -2104,7 +2107,7 @@ fn compute_frame(base: FemmDoc, tracks: Vec<kinematic::Track>, frame_idx: i64, d
|
|||
title: String::from("femm-mag-solve binary missing"),
|
||||
body: String::from("run `cargo xtask install`"),
|
||||
})?;
|
||||
let solver_out = std::process::Command::new(&helper)
|
||||
let solver_out = quiet_command(&helper)
|
||||
.arg(&stem_str)
|
||||
.output()
|
||||
.map_err(|e| ErrorReport {
|
||||
|
|
@ -2438,7 +2441,7 @@ fn run_mesh(doc: &FemmDoc) -> Result<Mesh, ErrorReport> {
|
|||
|
||||
let triangle = locate_triangle().ok_or_else(|| ErrorReport {
|
||||
title: String::from("triangle binary missing"),
|
||||
body: String::from("expected triangle next to femm.app/Contents/MacOS/triangle, or under build/triangle/triangle, or at $FEMM_TRIANGLE - run scripts/macos/build_triangle.sh"),
|
||||
body: String::from("run `cargo xtask install` (or set $FEMM_TRIANGLE to the triangle executable)"),
|
||||
})?;
|
||||
|
||||
let stem_str = stem.to_str().ok_or_else(|| ErrorReport {
|
||||
|
|
@ -2446,7 +2449,7 @@ fn run_mesh(doc: &FemmDoc) -> Result<Mesh, ErrorReport> {
|
|||
body: format!("{stem:?}"),
|
||||
})?;
|
||||
let min_angle = if doc.min_angle > 0.0 { doc.min_angle } else { MIN_ANGLE_DEG };
|
||||
let output = std::process::Command::new(&triangle)
|
||||
let output = quiet_command(&triangle)
|
||||
.args([
|
||||
"-p", "-P", "-e", "-A", "-a", "-z", "-Q", "-I",
|
||||
&format!("-q{min_angle}"),
|
||||
|
|
@ -2483,10 +2486,10 @@ fn run_solve(stem: &Path) -> Result<(), ErrorReport> {
|
|||
|
||||
let helper = locate_mag_solver().ok_or_else(|| ErrorReport {
|
||||
title: String::from("femm-mag-solve binary missing"),
|
||||
body: String::from("expected femm-mag-solve next to femm.app/Contents/MacOS/, or under target/release/, or at $FEMM_MAG_SOLVE - run `cargo xtask install`"),
|
||||
body: String::from("run `cargo xtask install` (or set $FEMM_MAG_SOLVE to the femm-mag-solve executable)"),
|
||||
})?;
|
||||
|
||||
let output = std::process::Command::new(&helper)
|
||||
let output = quiet_command(&helper)
|
||||
.arg(stem_str)
|
||||
.output()
|
||||
.map_err(|e| ErrorReport {
|
||||
|
|
@ -2544,16 +2547,24 @@ fn unix_signal(status: &std::process::ExitStatus) -> Option<i32> {
|
|||
#[cfg(not(unix))]
|
||||
fn unix_signal(_status: &std::process::ExitStatus) -> Option<i32> { None }
|
||||
|
||||
/// resolves the femm-mag-solve helper path from the bundled .app, the dev target dir, or $FEMM_MAG_SOLVE.
|
||||
fn quiet_command(program: &std::path::Path) -> std::process::Command {
|
||||
let mut cmd = std::process::Command::new(program);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||
cmd.creation_flags(CREATE_NO_WINDOW);
|
||||
}
|
||||
cmd
|
||||
}
|
||||
|
||||
/// resolves the femm-mag-solve helper path from the bundled app, the dev target dir, or $FEMM_MAG_SOLVE.
|
||||
fn locate_mag_solver() -> Option<std::path::PathBuf> {
|
||||
let exe_name = format!("femm-mag-solve{}", std::env::consts::EXE_SUFFIX);
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(parent) = exe.parent() {
|
||||
let bundled = parent.join("femm-mag-solve");
|
||||
let bundled = parent.join(&exe_name);
|
||||
if bundled.is_file() { return Some(bundled); }
|
||||
let dev = parent.join("femm-mag-solve");
|
||||
if let Ok(canon) = dev.canonicalize() {
|
||||
if canon.is_file() { return Some(canon); }
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(env_path) = std::env::var("FEMM_MAG_SOLVE") {
|
||||
|
|
@ -2574,15 +2585,16 @@ fn panic_payload_text(payload: &Box<dyn std::any::Any + Send>) -> String {
|
|||
String::from("panic payload was not a string")
|
||||
}
|
||||
|
||||
/// resolves the Triangle binary path from the bundled .app, the dev build dir, or $FEMM_TRIANGLE.
|
||||
/// resolves the Triangle binary path from the bundled app, the dev build dir, or $FEMM_TRIANGLE.
|
||||
fn locate_triangle() -> Option<std::path::PathBuf> {
|
||||
let exe_name = format!("triangle{}", std::env::consts::EXE_SUFFIX);
|
||||
if let Ok(exe) = std::env::current_exe() {
|
||||
if let Some(parent) = exe.parent() {
|
||||
let bundled = parent.join("triangle");
|
||||
let bundled = parent.join(&exe_name);
|
||||
if bundled.is_file() { return Some(bundled); }
|
||||
let dev = parent.join("../../build/triangle/triangle");
|
||||
if let Ok(canon) = dev.canonicalize() {
|
||||
if canon.is_file() { return Some(canon); }
|
||||
for ancestor in parent.ancestors().take(6) {
|
||||
let dev = ancestor.join("build").join("triangle").join(&exe_name);
|
||||
if dev.is_file() { return Some(dev); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,58 +9,42 @@ use femm_doc_mag::{
|
|||
|
||||
const NODE_MERGE_TOL: f64 = 1e-3;
|
||||
|
||||
/// produces a fresh FemmDoc from the SVG at path, prompting (via rfd) to convert any non-circular ellipse into a circle on ambiguous radii.
|
||||
pub fn import_svg(path: &Path) -> Result<FemmDoc, String> {
|
||||
/// produces a fresh FemmDoc from the SVG at path, returning any non-fatal warnings (bezier approximation, missing materials, etc.) alongside the doc.
|
||||
pub fn import_svg(path: &Path) -> Result<(FemmDoc, Vec<String>), String> {
|
||||
let raw = std::fs::read_to_string(path).map_err(|e| format!("read svg: {e}"))?;
|
||||
parse_svg_text(&raw, true)
|
||||
parse_svg_text_with_warnings(&raw)
|
||||
}
|
||||
|
||||
/// parses an SVG text buffer into a FemmDoc; if prompt_for_ellipse is true and any non-circular ellipse is found, asks the user (rfd dialog) whether to convert.
|
||||
pub fn parse_svg_text(src: &str, prompt_for_ellipse: bool) -> Result<FemmDoc, String> {
|
||||
/// parses an SVG text buffer into a FemmDoc; non-circular ellipses are silently converted to circles (average radius). the `_prompt_for_ellipse` argument is accepted for backwards compatibility but ignored.
|
||||
pub fn parse_svg_text(src: &str, _prompt_for_ellipse: bool) -> Result<FemmDoc, String> {
|
||||
let (doc, _) = parse_svg_text_with_warnings(src)?;
|
||||
Ok(doc)
|
||||
}
|
||||
|
||||
/// like parse_svg_text but also returns a list of human-readable warnings the caller can surface (bezier approximation, shapes missing material, etc.).
|
||||
pub fn parse_svg_text_with_warnings(src: &str) -> Result<(FemmDoc, Vec<String>), String> {
|
||||
let xml = Document::parse(src).map_err(|e| format!("svg parse: {e}"))?;
|
||||
let mut builder = Builder::new();
|
||||
let mut warnings = Warnings::default();
|
||||
walk(xml.root_element(), Mat3::identity(), &mut builder, &mut warnings)?;
|
||||
|
||||
let convert_ellipses = if warnings.has_ellipses() && prompt_for_ellipse {
|
||||
let r = rfd::MessageDialog::new()
|
||||
.set_title("Non-circular ellipses detected")
|
||||
.set_description(&format!(
|
||||
"Found {} non-circular ellipse(s). Converting to circles (average radius) keeps the mesh clean and avoids the polygon-approximation penalty. Convert?",
|
||||
warnings.ellipse_count
|
||||
))
|
||||
.set_buttons(rfd::MessageButtons::YesNo)
|
||||
.show();
|
||||
matches!(r, rfd::MessageDialogResult::Yes)
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
let mut messages = Vec::new();
|
||||
if warnings.has_ellipses() {
|
||||
messages.push(format!("converted {} non-circular ellipse(s) to circles", warnings.ellipse_count));
|
||||
}
|
||||
if !warnings.bezier_paths.is_empty() {
|
||||
rfd::MessageDialog::new()
|
||||
.set_title("Bezier curves approximated")
|
||||
.set_description(&format!(
|
||||
"{} bezier curve(s) approximated with straight-line polylines. For smooth curves, replace with circles or arc paths.",
|
||||
warnings.bezier_paths.len(),
|
||||
))
|
||||
.set_buttons(rfd::MessageButtons::Ok)
|
||||
.show();
|
||||
messages.push(format!("approximated {} bezier curve(s) with polylines", warnings.bezier_paths.len()));
|
||||
}
|
||||
|
||||
if !warnings.no_material_shapes.is_empty() {
|
||||
let ids = warnings.no_material_shapes.join(", ");
|
||||
rfd::MessageDialog::new()
|
||||
.set_title("Shapes missing material")
|
||||
.set_description(&format!(
|
||||
"{} shape(s) have a recognized class (Block/Segment/Toroid/Disc) but no material name. Add a <title> child like <title>Steel</title> so the importer can label the region. Affected: {ids}",
|
||||
warnings.no_material_shapes.len(),
|
||||
))
|
||||
.set_buttons(rfd::MessageButtons::Ok)
|
||||
.show();
|
||||
messages.push(format!(
|
||||
"{} shape(s) missing material (add <title>MaterialName</title>): {}",
|
||||
warnings.no_material_shapes.len(),
|
||||
warnings.no_material_shapes.join(", "),
|
||||
));
|
||||
}
|
||||
|
||||
let doc = builder.finalize(convert_ellipses);
|
||||
Ok(doc)
|
||||
let doc = builder.finalize(true);
|
||||
Ok((doc, messages))
|
||||
}
|
||||
|
||||
/// emits an SVG containing every node (as `<circle>`), segment (as `<line>`), arc (as `<path>`), and block label (as `<g><circle><text>`); writes y-flipped so it renders right-side-up in any viewer.
|
||||
|
|
|
|||
|
|
@ -13,20 +13,34 @@ fn main() {
|
|||
// re-run on changes to any FFI header, build script, or solver source dir.
|
||||
println!("cargo:rerun-if-changed={}", ffi_dir.display());
|
||||
println!("cargo:rerun-if-changed={}", repo_root.join("scripts/macos/build_ffi.sh").display());
|
||||
println!("cargo:rerun-if-changed={}", repo_root.join("scripts/windows/build_ffi.ps1").display());
|
||||
println!("cargo:rerun-if-changed={}", repo_root.join("scripts/windows/_toolchain.ps1").display());
|
||||
for engine in ["fkn", "belasolv", "csolv", "hsolv", "liblua", "compat"] {
|
||||
println!("cargo:rerun-if-changed={}", repo_root.join(engine).display());
|
||||
}
|
||||
|
||||
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
|
||||
let script = match target_os.as_str() {
|
||||
"macos" => repo_root.join("scripts/macos/build_ffi.sh"),
|
||||
let target_env = env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default();
|
||||
let status = match target_os.as_str() {
|
||||
"macos" => Command::new("bash")
|
||||
.arg(repo_root.join("scripts/macos/build_ffi.sh"))
|
||||
.status()
|
||||
.expect("failed to invoke bash for engine build script"),
|
||||
"windows" => {
|
||||
let script = repo_root.join("scripts/windows/build_ffi.ps1");
|
||||
Command::new("pwsh")
|
||||
.args([
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
])
|
||||
.arg(&script)
|
||||
.status()
|
||||
.expect("failed to invoke pwsh for engine build script")
|
||||
}
|
||||
other => panic!("femm-sys: no engine build script wired up for target_os={other}"),
|
||||
};
|
||||
|
||||
let status = Command::new("bash")
|
||||
.arg(&script)
|
||||
.status()
|
||||
.expect("failed to invoke bash for engine build script");
|
||||
if !status.success() {
|
||||
panic!("engine build script exited with {status}");
|
||||
}
|
||||
|
|
@ -35,7 +49,13 @@ fn main() {
|
|||
for lib in ["femm_mag", "femm_elec", "femm_heat", "femm_curr"] {
|
||||
println!("cargo:rustc-link-lib=static={lib}");
|
||||
}
|
||||
println!("cargo:rustc-link-lib=dylib=c++");
|
||||
match (target_os.as_str(), target_env.as_str()) {
|
||||
("macos", _) => println!("cargo:rustc-link-lib=dylib=c++"),
|
||||
("windows", "gnu") => println!("cargo:rustc-link-lib=dylib=stdc++"),
|
||||
("windows", "gnullvm") => println!("cargo:rustc-link-lib=dylib=c++"),
|
||||
("windows", "msvc") => { /* MSVC links the C++ runtime automatically */ }
|
||||
_ => println!("cargo:rustc-link-lib=dylib=stdc++"),
|
||||
}
|
||||
|
||||
let bindings = bindgen::Builder::default()
|
||||
.header(manifest.join("wrapper.h").to_str().unwrap())
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
|
||||
#include <limits.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
|
||||
|
||||
|
|
@ -58,7 +59,7 @@ typedef unsigned long lint32; /* unsigned int with at least 32 bits */
|
|||
** conversion of pointer to int (for hashing only)
|
||||
** (the shift removes bits that are usually 0 because of alignment)
|
||||
*/
|
||||
#define IntPoint(p) (((unsigned long)(p)) >> 3)
|
||||
#define IntPoint(p) (((uintptr_t)(p)) >> 3)
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
# Toolchain detection. Picks a backend appropriate for the host arch:
|
||||
# x64 host -> MSVC (x64) or MinGW (x86_64), Rust target = host default or x86_64-pc-windows-gnu
|
||||
# ARM64 host -> CLANGARM64 (msys2) or MinGW (x86_64, emulated, warned), Rust target = aarch64-pc-windows-gnullvm
|
||||
# Dot-source this file: . "$PSScriptRoot\_toolchain.ps1"
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Get-HostArch {
|
||||
if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64' -or $env:PROCESSOR_ARCHITEW6432 -eq 'ARM64') { return 'arm64' }
|
||||
return 'x64'
|
||||
}
|
||||
|
||||
function Initialize-VsEnv {
|
||||
param([string]$VcVars)
|
||||
$tmp = New-TemporaryFile
|
||||
try {
|
||||
cmd /c "`"$VcVars`" x64 >NUL && set" | Out-File -FilePath $tmp -Encoding ascii
|
||||
foreach ($line in Get-Content $tmp) {
|
||||
if ($line -match '^([^=]+)=(.*)$') {
|
||||
Set-Item -Path "env:$($Matches[1])" -Value $Matches[2]
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Remove-Item $tmp -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
function Find-VsInstall {
|
||||
$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
|
||||
if (-not (Test-Path $vswhere)) {
|
||||
$vswhere = Join-Path $env:ProgramFiles 'Microsoft Visual Studio\Installer\vswhere.exe'
|
||||
}
|
||||
if (-not (Test-Path $vswhere)) { return $null }
|
||||
|
||||
$install = & $vswhere -latest -products * `
|
||||
-requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 `
|
||||
-property installationPath 2>$null
|
||||
if (-not $install) { return $null }
|
||||
|
||||
$vcvars = Join-Path $install 'VC\Auxiliary\Build\vcvars64.bat'
|
||||
if (Test-Path $vcvars) { return $vcvars }
|
||||
return $null
|
||||
}
|
||||
|
||||
function Find-MingwBin {
|
||||
if ($env:MINGW_PREFIX -and (Test-Path (Join-Path $env:MINGW_PREFIX 'g++.exe'))) {
|
||||
return $env:MINGW_PREFIX
|
||||
}
|
||||
$gxx = Get-Command g++.exe -ErrorAction SilentlyContinue
|
||||
if ($gxx) { return (Split-Path $gxx.Source -Parent) }
|
||||
foreach ($p in @(
|
||||
'C:\msys64\mingw64\bin',
|
||||
'C:\msys64\ucrt64\bin',
|
||||
'C:\mingw64\bin',
|
||||
'C:\ProgramData\chocolatey\lib\mingw\tools\install\mingw64\bin'
|
||||
)) {
|
||||
if (Test-Path (Join-Path $p 'g++.exe')) { return $p }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Find-ClangArm64Bin {
|
||||
if ($env:CLANGARM64_PREFIX -and (Test-Path (Join-Path $env:CLANGARM64_PREFIX 'clang++.exe'))) {
|
||||
return $env:CLANGARM64_PREFIX
|
||||
}
|
||||
foreach ($p in @('C:\msys64\clangarm64\bin')) {
|
||||
if (Test-Path (Join-Path $p 'clang++.exe')) { return $p }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-Toolchain {
|
||||
$hostArch = Get-HostArch
|
||||
|
||||
if ($hostArch -eq 'arm64') {
|
||||
$clangBin = Find-ClangArm64Bin
|
||||
if ($clangBin) {
|
||||
return [pscustomobject]@{
|
||||
Kind = 'clangarm64'
|
||||
Ready = $true
|
||||
Bin = $clangBin
|
||||
CargoTarget = 'aarch64-pc-windows-gnullvm'
|
||||
}
|
||||
}
|
||||
$mingw = Find-MingwBin
|
||||
if ($mingw) {
|
||||
Write-Warning "ARM64 host but only x86_64 MinGW found; resulting binaries will run under emulation and may crash at startup. Install msys2 CLANGARM64 (pacman -S mingw-w64-clang-aarch64-toolchain)."
|
||||
return [pscustomobject]@{ Kind = 'mingw'; Ready = $true; MingwBin = $mingw; CargoTarget = 'x86_64-pc-windows-gnu' }
|
||||
}
|
||||
throw "No ARM64 C++ toolchain found. Install msys2 CLANGARM64 (pacman -S mingw-w64-clang-aarch64-toolchain) or Visual Studio ARM64 C++ tools."
|
||||
}
|
||||
|
||||
if (Get-Command cl.exe -ErrorAction SilentlyContinue) {
|
||||
return [pscustomobject]@{ Kind = 'msvc'; Ready = $true; VcVars = $null; CargoTarget = '' }
|
||||
}
|
||||
$vcvars = Find-VsInstall
|
||||
if ($vcvars) {
|
||||
return [pscustomobject]@{ Kind = 'msvc'; Ready = $false; VcVars = $vcvars; CargoTarget = '' }
|
||||
}
|
||||
$mingw = Find-MingwBin
|
||||
if ($mingw) {
|
||||
return [pscustomobject]@{ Kind = 'mingw'; Ready = $true; MingwBin = $mingw; CargoTarget = 'x86_64-pc-windows-gnu' }
|
||||
}
|
||||
throw "No C++ toolchain found. Install Visual Studio Build Tools (with the C++ workload) or MinGW-w64."
|
||||
}
|
||||
|
||||
function Initialize-Toolchain {
|
||||
param([pscustomobject]$Toolchain)
|
||||
if ($Toolchain.Kind -eq 'msvc' -and -not $Toolchain.Ready) {
|
||||
Write-Host "Importing MSVC environment from $($Toolchain.VcVars)"
|
||||
Initialize-VsEnv -VcVars $Toolchain.VcVars
|
||||
}
|
||||
if ($Toolchain.Kind -eq 'mingw' -and $Toolchain.MingwBin -and ($env:PATH -notlike "*$($Toolchain.MingwBin)*")) {
|
||||
$env:PATH = "$($Toolchain.MingwBin);$env:PATH"
|
||||
}
|
||||
if ($Toolchain.Kind -eq 'clangarm64' -and $Toolchain.Bin -and ($env:PATH -notlike "*$($Toolchain.Bin)*")) {
|
||||
$env:PATH = "$($Toolchain.Bin);$env:PATH"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
#requires -Version 7
|
||||
# Top-level Windows release build. Mirrors scripts/macos/build.sh.
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$Root = Resolve-Path (Join-Path $PSScriptRoot '..\..')
|
||||
Set-Location $Root
|
||||
|
||||
if (-not $IsWindows) {
|
||||
Write-Error "wrong platform: $($PSVersionTable.OS) - use cargo xtask build"
|
||||
exit 1
|
||||
}
|
||||
|
||||
. (Join-Path $PSScriptRoot '_toolchain.ps1')
|
||||
$tc = Get-Toolchain
|
||||
Initialize-Toolchain $tc
|
||||
Write-Host "toolchain: $($tc.Kind)"
|
||||
|
||||
$env:PROFILE = 'release'
|
||||
|
||||
$Build = Join-Path $Root 'build'
|
||||
$BinDir = Join-Path $Build 'bin\femm'
|
||||
|
||||
Write-Host 'Building Triangle...'
|
||||
& (Join-Path $PSScriptRoot 'build_triangle.ps1')
|
||||
|
||||
$tri = Join-Path $Root 'build\triangle\triangle.exe'
|
||||
if (-not (Test-Path $tri)) { Write-Error "triangle binary missing at $tri"; exit 1 }
|
||||
|
||||
# femm-app/build.rs embeds this into femm.exe, so it must exist before cargo build.
|
||||
$svg = Join-Path $Root 'assets\femm.svg'
|
||||
$icoOut = Join-Path $Build 'assets\femm.ico'
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path $icoOut) | Out-Null
|
||||
if (Test-Path $svg) {
|
||||
$magick = Get-Command magick.exe -ErrorAction SilentlyContinue
|
||||
if ($magick) {
|
||||
& $magick.Source -background none $svg -define icon:auto-resize=16,32,48,64,128,256 $icoOut
|
||||
if ($LASTEXITCODE -ne 0) { Write-Warning "magick failed to build $icoOut ($LASTEXITCODE)" }
|
||||
} else {
|
||||
Write-Warning 'magick not found - femm.exe will be built without an embedded icon'
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host 'Building Rust workspace (release)...'
|
||||
$cargoArgs = @('build','--release','-p','femm-app','-p','femm-mag-solve')
|
||||
if ($tc.CargoTarget) {
|
||||
$cargoArgs += @('--target',$tc.CargoTarget)
|
||||
}
|
||||
& cargo @cargoArgs
|
||||
if ($LASTEXITCODE -ne 0) { throw "cargo build failed ($LASTEXITCODE)" }
|
||||
|
||||
$targetDir = if ($tc.CargoTarget) {
|
||||
Join-Path $Root "target\$($tc.CargoTarget)\release"
|
||||
} else {
|
||||
Join-Path $Root 'target\release'
|
||||
}
|
||||
$bin = Join-Path $targetDir 'femm.exe'
|
||||
$solve = Join-Path $targetDir 'femm-mag-solve.exe'
|
||||
foreach ($p in @($bin, $solve)) {
|
||||
if (-not (Test-Path $p)) { Write-Error "missing build output: $p"; exit 1 }
|
||||
}
|
||||
|
||||
if (Test-Path $BinDir) { Remove-Item $BinDir -Recurse -Force }
|
||||
New-Item -ItemType Directory -Force -Path $BinDir | Out-Null
|
||||
|
||||
Copy-Item $bin (Join-Path $BinDir 'femm.exe')
|
||||
Copy-Item $solve (Join-Path $BinDir 'femm-mag-solve.exe')
|
||||
Copy-Item $tri (Join-Path $BinDir 'triangle.exe')
|
||||
|
||||
if (Test-Path $icoOut) {
|
||||
Copy-Item $icoOut (Join-Path $BinDir 'femm.ico')
|
||||
}
|
||||
|
||||
Write-Host "Built: $BinDir"
|
||||
|
|
@ -0,0 +1,227 @@
|
|||
#requires -Version 7
|
||||
# Per-engine static archives for mag/elec/heat/curr.
|
||||
# MSVC: cl /c + lib /OUT (no inter-engine symbol localization yet).
|
||||
# MinGW: g++ -c + ld -r + objcopy --keep-global-symbols + ar — mirrors scripts/macos/build_ffi.sh.
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$Root = Resolve-Path (Join-Path $PSScriptRoot '..\..')
|
||||
$Build = if ($env:BUILD) { $env:BUILD } else { Join-Path $Root 'build\ffi' }
|
||||
|
||||
. (Join-Path $PSScriptRoot '_toolchain.ps1')
|
||||
$tc = Get-Toolchain
|
||||
Initialize-Toolchain $tc
|
||||
|
||||
$buildProfile = if ($env:PROFILE) { $env:PROFILE } else { 'release' }
|
||||
|
||||
foreach ($d in @('fkn','liblua','belasolv','csolv','hsolv','ffi','exports')) {
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $Build $d) | Out-Null
|
||||
}
|
||||
|
||||
function Compile-Many {
|
||||
param(
|
||||
[string[]]$Includes,
|
||||
[string]$OutDir,
|
||||
[string[]]$Sources
|
||||
)
|
||||
foreach ($src in $Sources) {
|
||||
$base = [IO.Path]::GetFileNameWithoutExtension($src)
|
||||
if ($tc.Kind -eq 'msvc') {
|
||||
$obj = Join-Path $OutDir "$base.obj"
|
||||
$cxxflags = if ($buildProfile -eq 'release') {
|
||||
@('/nologo','/std:c++17','/EHs-c-','/GR-','/O2','/DNDEBUG','/W0','/D_CRT_SECURE_NO_WARNINGS')
|
||||
} else {
|
||||
@('/nologo','/std:c++17','/EHs-c-','/GR-','/Od','/Zi','/W0','/D_CRT_SECURE_NO_WARNINGS')
|
||||
}
|
||||
$incFlags = $Includes | ForEach-Object { "/I$_" }
|
||||
& cl.exe @cxxflags @incFlags /c $src "/Fo:$obj" | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "cl.exe failed on $src ($LASTEXITCODE)" }
|
||||
} else {
|
||||
$obj = Join-Path $OutDir "$base.o"
|
||||
$cxxflags = if ($buildProfile -eq 'release') {
|
||||
@('-std=c++17','-fno-exceptions','-fno-rtti','-fpermissive','-O3','-DNDEBUG','-w')
|
||||
} else {
|
||||
@('-std=c++17','-fno-exceptions','-fno-rtti','-fpermissive','-O0','-g','-w')
|
||||
}
|
||||
$incFlags = $Includes | ForEach-Object { "-I$_" }
|
||||
$cxx = if ($tc.Kind -eq 'clangarm64') { Join-Path $tc.Bin 'clang++.exe' } else { 'g++.exe' }
|
||||
& $cxx @cxxflags @incFlags -c $src -o $obj
|
||||
if ($LASTEXITCODE -ne 0) { throw "$cxx failed on $src ($LASTEXITCODE)" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# liblua — compiled against fkn's complex.h
|
||||
Compile-Many `
|
||||
-Includes @((Join-Path $Root 'fkn'), (Join-Path $Root 'liblua'), (Join-Path $Root 'compat')) `
|
||||
-OutDir (Join-Path $Build 'liblua') `
|
||||
-Sources @(
|
||||
(Join-Path $Root 'liblua\lapi.cpp'),
|
||||
(Join-Path $Root 'liblua\lauxlib.cpp'),
|
||||
(Join-Path $Root 'liblua\lbaselib.cpp'),
|
||||
(Join-Path $Root 'liblua\lcode.cpp'),
|
||||
(Join-Path $Root 'liblua\ldblib.cpp'),
|
||||
(Join-Path $Root 'liblua\ldebug.cpp'),
|
||||
(Join-Path $Root 'liblua\ldo.cpp'),
|
||||
(Join-Path $Root 'liblua\lfunc.cpp'),
|
||||
(Join-Path $Root 'liblua\lgc.cpp'),
|
||||
(Join-Path $Root 'liblua\liolib.cpp'),
|
||||
(Join-Path $Root 'liblua\llex.cpp'),
|
||||
(Join-Path $Root 'liblua\lmathlib.cpp'),
|
||||
(Join-Path $Root 'liblua\lmem.cpp'),
|
||||
(Join-Path $Root 'liblua\lobject.cpp'),
|
||||
(Join-Path $Root 'liblua\lparser.cpp'),
|
||||
(Join-Path $Root 'liblua\lstate.cpp'),
|
||||
(Join-Path $Root 'liblua\lstring.cpp'),
|
||||
(Join-Path $Root 'liblua\lstrlib.cpp'),
|
||||
(Join-Path $Root 'liblua\ltable.cpp'),
|
||||
(Join-Path $Root 'liblua\ltests.cpp'),
|
||||
(Join-Path $Root 'liblua\ltm.cpp'),
|
||||
(Join-Path $Root 'liblua\lundump.cpp'),
|
||||
(Join-Path $Root 'liblua\lvm.cpp'),
|
||||
(Join-Path $Root 'liblua\lzio.cpp')
|
||||
)
|
||||
|
||||
Compile-Many `
|
||||
-Includes @((Join-Path $Root 'fkn'), (Join-Path $Root 'compat')) `
|
||||
-OutDir (Join-Path $Build 'fkn') `
|
||||
-Sources @(
|
||||
(Join-Path $Root 'fkn\complex.cpp'),
|
||||
(Join-Path $Root 'fkn\cspars.cpp'),
|
||||
(Join-Path $Root 'fkn\cuthill.cpp'),
|
||||
(Join-Path $Root 'fkn\femmedoccore.cpp'),
|
||||
(Join-Path $Root 'fkn\fullmatrix.cpp'),
|
||||
(Join-Path $Root 'fkn\matprop.cpp'),
|
||||
(Join-Path $Root 'fkn\prob1big.cpp'),
|
||||
(Join-Path $Root 'fkn\prob2big.cpp'),
|
||||
(Join-Path $Root 'fkn\prob3big.cpp'),
|
||||
(Join-Path $Root 'fkn\prob4big.cpp'),
|
||||
(Join-Path $Root 'fkn\spars.cpp')
|
||||
)
|
||||
|
||||
Compile-Many `
|
||||
-Includes @((Join-Path $Root 'belasolv'), (Join-Path $Root 'compat')) `
|
||||
-OutDir (Join-Path $Build 'belasolv') `
|
||||
-Sources @(
|
||||
(Join-Path $Root 'belasolv\cuthill.cpp'),
|
||||
(Join-Path $Root 'belasolv\femmedoccore.cpp'),
|
||||
(Join-Path $Root 'belasolv\prob1big.cpp'),
|
||||
(Join-Path $Root 'belasolv\spars.cpp')
|
||||
)
|
||||
|
||||
Compile-Many `
|
||||
-Includes @((Join-Path $Root 'csolv'), (Join-Path $Root 'compat')) `
|
||||
-OutDir (Join-Path $Build 'csolv') `
|
||||
-Sources @(
|
||||
(Join-Path $Root 'csolv\complex.cpp'),
|
||||
(Join-Path $Root 'csolv\cspars.cpp'),
|
||||
(Join-Path $Root 'csolv\CUTHILL.CPP'),
|
||||
(Join-Path $Root 'csolv\femmedoccore.cpp'),
|
||||
(Join-Path $Root 'csolv\PROB1BIG.CPP')
|
||||
)
|
||||
|
||||
Compile-Many `
|
||||
-Includes @((Join-Path $Root 'hsolv'), (Join-Path $Root 'compat')) `
|
||||
-OutDir (Join-Path $Build 'hsolv') `
|
||||
-Sources @(
|
||||
(Join-Path $Root 'hsolv\complex.cpp'),
|
||||
(Join-Path $Root 'hsolv\CUTHILL.CPP'),
|
||||
(Join-Path $Root 'hsolv\hsolvdoc.cpp'),
|
||||
(Join-Path $Root 'hsolv\prob1big.cpp'),
|
||||
(Join-Path $Root 'hsolv\SPARS.CPP')
|
||||
)
|
||||
|
||||
# ffi shims — one TU per engine, plus the liblua complex-op shim.
|
||||
Compile-Many -Includes @((Join-Path $Root 'fkn'), (Join-Path $Root 'compat')) -OutDir (Join-Path $Build 'ffi') -Sources @((Join-Path $Root 'ffi\femm_mag.cpp'))
|
||||
Compile-Many -Includes @((Join-Path $Root 'belasolv'), (Join-Path $Root 'compat')) -OutDir (Join-Path $Build 'ffi') -Sources @((Join-Path $Root 'ffi\femm_elec.cpp'))
|
||||
Compile-Many -Includes @((Join-Path $Root 'hsolv'), (Join-Path $Root 'compat')) -OutDir (Join-Path $Build 'ffi') -Sources @((Join-Path $Root 'ffi\femm_heat.cpp'))
|
||||
Compile-Many -Includes @((Join-Path $Root 'csolv'), (Join-Path $Root 'compat')) -OutDir (Join-Path $Build 'ffi') -Sources @((Join-Path $Root 'ffi\femm_curr.cpp'))
|
||||
Compile-Many -Includes @((Join-Path $Root 'liblua'), (Join-Path $Root 'compat')) -OutDir (Join-Path $Build 'ffi') -Sources @((Join-Path $Root 'ffi\femm_lua_complex_ops.cpp'))
|
||||
|
||||
if ($tc.Kind -eq 'msvc') {
|
||||
# MSVC: bundle objects per engine via lib.exe. Note: inter-engine internal-symbol collisions
|
||||
# (cuthill / femmedoccore / prob1big / spars all repeat across engines) are not localized here —
|
||||
# if the Rust link step trips LNK2005, we'll need to merge to DLLs or namespace the C++ sources.
|
||||
function Pack-Lib {
|
||||
param([string]$Name, [string[]]$Objs)
|
||||
$out = Join-Path $Build "$Name.lib"
|
||||
if (Test-Path $out) { Remove-Item $out -Force }
|
||||
& lib.exe /NOLOGO "/OUT:$out" @Objs | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "lib.exe failed for $Name ($LASTEXITCODE)" }
|
||||
}
|
||||
Pack-Lib 'femm_mag' ((Get-ChildItem (Join-Path $Build 'fkn\*.obj') | ForEach-Object FullName) +
|
||||
(Get-ChildItem (Join-Path $Build 'liblua\*.obj') | ForEach-Object FullName) +
|
||||
@((Join-Path $Build 'ffi\femm_mag.obj'),
|
||||
(Join-Path $Build 'ffi\femm_lua_complex_ops.obj')))
|
||||
Pack-Lib 'femm_elec' ((Get-ChildItem (Join-Path $Build 'belasolv\*.obj') | ForEach-Object FullName) +
|
||||
@((Join-Path $Build 'ffi\femm_elec.obj')))
|
||||
Pack-Lib 'femm_heat' ((Get-ChildItem (Join-Path $Build 'hsolv\*.obj') | ForEach-Object FullName) +
|
||||
@((Join-Path $Build 'ffi\femm_heat.obj')))
|
||||
Pack-Lib 'femm_curr' ((Get-ChildItem (Join-Path $Build 'csolv\*.obj') | ForEach-Object FullName) +
|
||||
@((Join-Path $Build 'ffi\femm_curr.obj')))
|
||||
|
||||
Write-Host 'built:'
|
||||
Get-ChildItem (Join-Path $Build 'femm_*.lib') | ForEach-Object { Write-Host " $($_.FullName) ($([math]::Round($_.Length/1KB)) KB)" }
|
||||
} else {
|
||||
$binDir = if ($tc.Kind -eq 'clangarm64') { $tc.Bin } else { $tc.MingwBin }
|
||||
$nmExe = Join-Path $binDir 'nm.exe'
|
||||
$arExe = Join-Path $binDir 'ar.exe'
|
||||
$objcopyExe = if ($tc.Kind -eq 'clangarm64') {
|
||||
Join-Path $binDir 'llvm-objcopy.exe'
|
||||
} else {
|
||||
Join-Path $binDir 'objcopy.exe'
|
||||
}
|
||||
foreach ($p in @($nmExe, $arExe, $objcopyExe)) {
|
||||
if (-not (Test-Path $p)) { throw "missing binutil: $p" }
|
||||
}
|
||||
|
||||
function Pack-Engine {
|
||||
param([string]$Prefix, [string[]]$Objs)
|
||||
|
||||
$publicRx = "^femm_${Prefix}_"
|
||||
$renameSet = [System.Collections.Generic.HashSet[string]]::new()
|
||||
foreach ($obj in $Objs) {
|
||||
$lines = & $nmExe --defined-only -g $obj
|
||||
if ($LASTEXITCODE -ne 0) { throw "nm failed on $obj" }
|
||||
foreach ($line in $lines) {
|
||||
if ($line -match '^\s*(?:[0-9a-fA-F]+\s+)?([A-Z])\s+(\S+)\s*$') {
|
||||
$sym = $Matches[2]
|
||||
if ($sym -notmatch $publicRx) { $null = $renameSet.Add($sym) }
|
||||
}
|
||||
}
|
||||
}
|
||||
$renameFile = Join-Path $Build "exports\${Prefix}_rename.txt"
|
||||
(($renameSet | Sort-Object | ForEach-Object { "$_ __femm_${Prefix}__$_" }) -join "`n") | Set-Content -Path $renameFile -Encoding ascii
|
||||
|
||||
$engineDir = Join-Path $Build "engines\$Prefix"
|
||||
if (Test-Path $engineDir) { Remove-Item $engineDir -Recurse -Force }
|
||||
New-Item -ItemType Directory -Force -Path $engineDir | Out-Null
|
||||
$renamedObjs = foreach ($obj in $Objs) {
|
||||
$base = [IO.Path]::GetFileName($obj)
|
||||
$newPath = Join-Path $engineDir "${Prefix}_${base}"
|
||||
Copy-Item $obj $newPath -Force
|
||||
& $objcopyExe "--redefine-syms=$renameFile" $newPath $newPath
|
||||
if ($LASTEXITCODE -ne 0) { throw "objcopy failed on $newPath ($LASTEXITCODE)" }
|
||||
$newPath
|
||||
}
|
||||
|
||||
$archive = Join-Path $Build "libfemm_${Prefix}.a"
|
||||
if (Test-Path $archive) { Remove-Item $archive -Force }
|
||||
& $arExe rcs $archive @renamedObjs
|
||||
if ($LASTEXITCODE -ne 0) { throw "ar failed for $Prefix ($LASTEXITCODE)" }
|
||||
}
|
||||
|
||||
Pack-Engine 'mag' ((Get-ChildItem (Join-Path $Build 'fkn\*.o') | ForEach-Object FullName) +
|
||||
(Get-ChildItem (Join-Path $Build 'liblua\*.o') | ForEach-Object FullName) +
|
||||
@((Join-Path $Build 'ffi\femm_mag.o'),
|
||||
(Join-Path $Build 'ffi\femm_lua_complex_ops.o')))
|
||||
Pack-Engine 'elec' ((Get-ChildItem (Join-Path $Build 'belasolv\*.o') | ForEach-Object FullName) +
|
||||
@((Join-Path $Build 'ffi\femm_elec.o')))
|
||||
Pack-Engine 'heat' ((Get-ChildItem (Join-Path $Build 'hsolv\*.o') | ForEach-Object FullName) +
|
||||
@((Join-Path $Build 'ffi\femm_heat.o')))
|
||||
Pack-Engine 'curr' ((Get-ChildItem (Join-Path $Build 'csolv\*.o') | ForEach-Object FullName) +
|
||||
@((Join-Path $Build 'ffi\femm_curr.o')))
|
||||
|
||||
Write-Host 'built:'
|
||||
Get-ChildItem (Join-Path $Build 'libfemm_*.a') | ForEach-Object { Write-Host " $($_.FullName) ($([math]::Round($_.Length/1KB)) KB)" }
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
#requires -Version 7
|
||||
# Builds the Triangle mesher into build\triangle\triangle.exe.
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$Root = Resolve-Path (Join-Path $PSScriptRoot '..\..')
|
||||
$Build = if ($env:BUILD) { $env:BUILD } else { Join-Path $Root 'build\triangle' }
|
||||
New-Item -ItemType Directory -Force -Path $Build | Out-Null
|
||||
|
||||
. (Join-Path $PSScriptRoot '_toolchain.ps1')
|
||||
$tc = Get-Toolchain
|
||||
Initialize-Toolchain $tc
|
||||
|
||||
$buildProfile = if ($env:PROFILE) { $env:PROFILE } else { 'release' }
|
||||
$src = Join-Path $Root 'triangle\triangle.c'
|
||||
$out = Join-Path $Build 'triangle.exe'
|
||||
|
||||
if ($tc.Kind -eq 'msvc') {
|
||||
$cflags = if ($buildProfile -eq 'release') { @('/nologo','/O2','/DNDEBUG','/W0') } else { @('/nologo','/Od','/Zi','/W0') }
|
||||
& cl.exe @cflags $src "/Fe:$out" "/Fo:$Build\" /link /SUBSYSTEM:CONSOLE
|
||||
if ($LASTEXITCODE -ne 0) { throw "cl.exe failed building triangle ($LASTEXITCODE)" }
|
||||
} elseif ($tc.Kind -eq 'clangarm64') {
|
||||
$cflags = if ($buildProfile -eq 'release') { @('-std=gnu89','-O3','-DNDEBUG','-w') } else { @('-std=gnu89','-O0','-g','-w') }
|
||||
& (Join-Path $tc.Bin 'clang.exe') @cflags -o $out $src -lm
|
||||
if ($LASTEXITCODE -ne 0) { throw "clang failed building triangle ($LASTEXITCODE)" }
|
||||
} else {
|
||||
$cflags = if ($buildProfile -eq 'release') { @('-std=gnu89','-O3','-DNDEBUG','-w') } else { @('-std=gnu89','-O0','-g','-w') }
|
||||
& gcc.exe @cflags -o $out $src -lm
|
||||
if ($LASTEXITCODE -ne 0) { throw "gcc failed building triangle ($LASTEXITCODE)" }
|
||||
}
|
||||
|
||||
Write-Host "built: $out"
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
#requires -Version 7
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$Root = Resolve-Path (Join-Path $PSScriptRoot '..\..')
|
||||
Set-Location $Root
|
||||
|
||||
if (-not $IsWindows) {
|
||||
Write-Error "wrong platform: $($PSVersionTable.OS) - use cargo xtask debug"
|
||||
exit 1
|
||||
}
|
||||
|
||||
. (Join-Path $PSScriptRoot '_toolchain.ps1')
|
||||
$tc = Get-Toolchain
|
||||
Initialize-Toolchain $tc
|
||||
|
||||
$env:RUST_BACKTRACE = '1'
|
||||
|
||||
$cargoArgs = @('run','-p','femm-app')
|
||||
if ($tc.CargoTarget) {
|
||||
$cargoArgs += @('--target',$tc.CargoTarget)
|
||||
}
|
||||
$cargoArgs += '--'
|
||||
$cargoArgs += $args
|
||||
|
||||
& cargo @cargoArgs
|
||||
exit $LASTEXITCODE
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
#requires -Version 7
|
||||
$ErrorActionPreference = 'Stop'
|
||||
& (Join-Path $PSScriptRoot 'build_ffi.ps1') @args
|
||||
exit $LASTEXITCODE
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
#requires -Version 7
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$Root = Resolve-Path (Join-Path $PSScriptRoot '..\..')
|
||||
Set-Location $Root
|
||||
|
||||
if (-not $IsWindows) {
|
||||
Write-Error "wrong platform: $($PSVersionTable.OS) - use cargo xtask install"
|
||||
exit 1
|
||||
}
|
||||
|
||||
& (Join-Path $PSScriptRoot 'build.ps1')
|
||||
if ($LASTEXITCODE -ne 0) { throw "build.ps1 failed ($LASTEXITCODE)" }
|
||||
|
||||
$Dest = if ($env:FEMM_INSTALL_DIR) { $env:FEMM_INSTALL_DIR } else { Join-Path $env:LOCALAPPDATA 'Programs\femm' }
|
||||
$Src = Join-Path $Root 'build\bin\femm'
|
||||
|
||||
# Stop any running instance so the copy doesn't trip on a file lock.
|
||||
Get-Process -Name 'femm' -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
Write-Host "Stopping running femm.exe (pid $($_.Id))..."
|
||||
try { $_ | Stop-Process -Force } catch {}
|
||||
}
|
||||
Start-Sleep -Milliseconds 200
|
||||
|
||||
if (Test-Path $Dest) { Remove-Item $Dest -Recurse -Force }
|
||||
New-Item -ItemType Directory -Force -Path $Dest | Out-Null
|
||||
Copy-Item -Path (Join-Path $Src '*') -Destination $Dest -Recurse -Force
|
||||
|
||||
Write-Host "Installed: $Dest"
|
||||
|
|
@ -346,6 +346,7 @@
|
|||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#ifndef NO_TIMER
|
||||
|
|
@ -831,7 +832,7 @@ struct behavior {
|
|||
/* extracting an orientation (in the range 0 to 2) and a pointer to the */
|
||||
/* beginning of a triangle. The encode() routine compresses a pointer to a */
|
||||
/* triangle and an orientation into a single pointer. My assumptions that */
|
||||
/* triangles are four-byte-aligned and that the `unsigned long' type is */
|
||||
/* triangles are four-byte-aligned and that the `uintptr_t' type is */
|
||||
/* long enough to hold a pointer are two of the few kludges in this program.*/
|
||||
/* */
|
||||
/* Subsegments are manipulated similarly. A pointer to a subsegment */
|
||||
|
|
@ -942,16 +943,16 @@ int minus1mod3[3] = {2, 0, 1};
|
|||
/* extracted from the two least significant bits of the pointer. */
|
||||
|
||||
#define decode(ptr, otri) \
|
||||
(otri).orient = (int) ((unsigned long) (ptr) & (unsigned long) 3l); \
|
||||
(otri).orient = (int) ((uintptr_t) (ptr) & (uintptr_t) 3l); \
|
||||
(otri).tri = (triangle *) \
|
||||
((unsigned long) (ptr) ^ (unsigned long) (otri).orient)
|
||||
((uintptr_t) (ptr) ^ (uintptr_t) (otri).orient)
|
||||
|
||||
/* encode() compresses an oriented triangle into a single pointer. It */
|
||||
/* relies on the assumption that all triangles are aligned to four-byte */
|
||||
/* boundaries, so the two least significant bits of (otri).tri are zero. */
|
||||
|
||||
#define encode(otri) \
|
||||
(triangle) ((unsigned long) (otri).tri | (unsigned long) (otri).orient)
|
||||
(triangle) ((uintptr_t) (otri).tri | (uintptr_t) (otri).orient)
|
||||
|
||||
/* The following handle manipulation primitives are all described by Guibas */
|
||||
/* and Stolfi. However, Guibas and Stolfi use an edge-based data */
|
||||
|
|
@ -1115,16 +1116,16 @@ int minus1mod3[3] = {2, 0, 1};
|
|||
|
||||
#define infect(otri) \
|
||||
(otri).tri[6] = (triangle) \
|
||||
((unsigned long) (otri).tri[6] | (unsigned long) 2l)
|
||||
((uintptr_t) (otri).tri[6] | (uintptr_t) 2l)
|
||||
|
||||
#define uninfect(otri) \
|
||||
(otri).tri[6] = (triangle) \
|
||||
((unsigned long) (otri).tri[6] & ~ (unsigned long) 2l)
|
||||
((uintptr_t) (otri).tri[6] & ~ (uintptr_t) 2l)
|
||||
|
||||
/* Test a triangle for viral infection. */
|
||||
|
||||
#define infected(otri) \
|
||||
(((unsigned long) (otri).tri[6] & (unsigned long) 2l) != 0l)
|
||||
(((uintptr_t) (otri).tri[6] & (uintptr_t) 2l) != 0l)
|
||||
|
||||
/* Check or set a triangle's attributes. */
|
||||
|
||||
|
|
@ -1162,16 +1163,16 @@ int minus1mod3[3] = {2, 0, 1};
|
|||
/* are masked out to produce the real pointer. */
|
||||
|
||||
#define sdecode(sptr, osub) \
|
||||
(osub).ssorient = (int) ((unsigned long) (sptr) & (unsigned long) 1l); \
|
||||
(osub).ssorient = (int) ((uintptr_t) (sptr) & (uintptr_t) 1l); \
|
||||
(osub).ss = (subseg *) \
|
||||
((unsigned long) (sptr) & ~ (unsigned long) 3l)
|
||||
((uintptr_t) (sptr) & ~ (uintptr_t) 3l)
|
||||
|
||||
/* sencode() compresses an oriented subsegment into a single pointer. It */
|
||||
/* relies on the assumption that all subsegments are aligned to two-byte */
|
||||
/* boundaries, so the least significant bit of (osub).ss is zero. */
|
||||
|
||||
#define sencode(osub) \
|
||||
(subseg) ((unsigned long) (osub).ss | (unsigned long) (osub).ssorient)
|
||||
(subseg) ((uintptr_t) (osub).ss | (uintptr_t) (osub).ssorient)
|
||||
|
||||
/* ssym() toggles the orientation of a subsegment. */
|
||||
|
||||
|
|
@ -1908,27 +1909,27 @@ struct otri *t;
|
|||
struct osub printsh;
|
||||
vertex printvertex;
|
||||
|
||||
printf("triangle x%lx with orientation %d:\n", (unsigned long) t->tri,
|
||||
printf("triangle x%lx with orientation %d:\n", (uintptr_t) t->tri,
|
||||
t->orient);
|
||||
decode(t->tri[0], printtri);
|
||||
if (printtri.tri == m->dummytri) {
|
||||
printf(" [0] = Outer space\n");
|
||||
} else {
|
||||
printf(" [0] = x%lx %d\n", (unsigned long) printtri.tri,
|
||||
printf(" [0] = x%lx %d\n", (uintptr_t) printtri.tri,
|
||||
printtri.orient);
|
||||
}
|
||||
decode(t->tri[1], printtri);
|
||||
if (printtri.tri == m->dummytri) {
|
||||
printf(" [1] = Outer space\n");
|
||||
} else {
|
||||
printf(" [1] = x%lx %d\n", (unsigned long) printtri.tri,
|
||||
printf(" [1] = x%lx %d\n", (uintptr_t) printtri.tri,
|
||||
printtri.orient);
|
||||
}
|
||||
decode(t->tri[2], printtri);
|
||||
if (printtri.tri == m->dummytri) {
|
||||
printf(" [2] = Outer space\n");
|
||||
} else {
|
||||
printf(" [2] = x%lx %d\n", (unsigned long) printtri.tri,
|
||||
printf(" [2] = x%lx %d\n", (uintptr_t) printtri.tri,
|
||||
printtri.orient);
|
||||
}
|
||||
|
||||
|
|
@ -1937,37 +1938,37 @@ struct otri *t;
|
|||
printf(" Origin[%d] = NULL\n", (t->orient + 1) % 3 + 3);
|
||||
else
|
||||
printf(" Origin[%d] = x%lx (%.12g, %.12g)\n",
|
||||
(t->orient + 1) % 3 + 3, (unsigned long) printvertex,
|
||||
(t->orient + 1) % 3 + 3, (uintptr_t) printvertex,
|
||||
printvertex[0], printvertex[1]);
|
||||
dest(*t, printvertex);
|
||||
if (printvertex == (vertex) NULL)
|
||||
printf(" Dest [%d] = NULL\n", (t->orient + 2) % 3 + 3);
|
||||
else
|
||||
printf(" Dest [%d] = x%lx (%.12g, %.12g)\n",
|
||||
(t->orient + 2) % 3 + 3, (unsigned long) printvertex,
|
||||
(t->orient + 2) % 3 + 3, (uintptr_t) printvertex,
|
||||
printvertex[0], printvertex[1]);
|
||||
apex(*t, printvertex);
|
||||
if (printvertex == (vertex) NULL)
|
||||
printf(" Apex [%d] = NULL\n", t->orient + 3);
|
||||
else
|
||||
printf(" Apex [%d] = x%lx (%.12g, %.12g)\n",
|
||||
t->orient + 3, (unsigned long) printvertex,
|
||||
t->orient + 3, (uintptr_t) printvertex,
|
||||
printvertex[0], printvertex[1]);
|
||||
|
||||
if (b->usesegments) {
|
||||
sdecode(t->tri[6], printsh);
|
||||
if (printsh.ss != m->dummysub) {
|
||||
printf(" [6] = x%lx %d\n", (unsigned long) printsh.ss,
|
||||
printf(" [6] = x%lx %d\n", (uintptr_t) printsh.ss,
|
||||
printsh.ssorient);
|
||||
}
|
||||
sdecode(t->tri[7], printsh);
|
||||
if (printsh.ss != m->dummysub) {
|
||||
printf(" [7] = x%lx %d\n", (unsigned long) printsh.ss,
|
||||
printf(" [7] = x%lx %d\n", (uintptr_t) printsh.ss,
|
||||
printsh.ssorient);
|
||||
}
|
||||
sdecode(t->tri[8], printsh);
|
||||
if (printsh.ss != m->dummysub) {
|
||||
printf(" [8] = x%lx %d\n", (unsigned long) printsh.ss,
|
||||
printf(" [8] = x%lx %d\n", (uintptr_t) printsh.ss,
|
||||
printsh.ssorient);
|
||||
}
|
||||
}
|
||||
|
|
@ -2003,19 +2004,19 @@ struct osub *s;
|
|||
vertex printvertex;
|
||||
|
||||
printf("subsegment x%lx with orientation %d and mark %d:\n",
|
||||
(unsigned long) s->ss, s->ssorient, mark(*s));
|
||||
(uintptr_t) s->ss, s->ssorient, mark(*s));
|
||||
sdecode(s->ss[0], printsh);
|
||||
if (printsh.ss == m->dummysub) {
|
||||
printf(" [0] = No subsegment\n");
|
||||
} else {
|
||||
printf(" [0] = x%lx %d\n", (unsigned long) printsh.ss,
|
||||
printf(" [0] = x%lx %d\n", (uintptr_t) printsh.ss,
|
||||
printsh.ssorient);
|
||||
}
|
||||
sdecode(s->ss[1], printsh);
|
||||
if (printsh.ss == m->dummysub) {
|
||||
printf(" [1] = No subsegment\n");
|
||||
} else {
|
||||
printf(" [1] = x%lx %d\n", (unsigned long) printsh.ss,
|
||||
printf(" [1] = x%lx %d\n", (uintptr_t) printsh.ss,
|
||||
printsh.ssorient);
|
||||
}
|
||||
|
||||
|
|
@ -2024,28 +2025,28 @@ struct osub *s;
|
|||
printf(" Origin[%d] = NULL\n", 2 + s->ssorient);
|
||||
else
|
||||
printf(" Origin[%d] = x%lx (%.12g, %.12g)\n",
|
||||
2 + s->ssorient, (unsigned long) printvertex,
|
||||
2 + s->ssorient, (uintptr_t) printvertex,
|
||||
printvertex[0], printvertex[1]);
|
||||
sdest(*s, printvertex);
|
||||
if (printvertex == (vertex) NULL)
|
||||
printf(" Dest [%d] = NULL\n", 3 - s->ssorient);
|
||||
else
|
||||
printf(" Dest [%d] = x%lx (%.12g, %.12g)\n",
|
||||
3 - s->ssorient, (unsigned long) printvertex,
|
||||
3 - s->ssorient, (uintptr_t) printvertex,
|
||||
printvertex[0], printvertex[1]);
|
||||
|
||||
decode(s->ss[6], printtri);
|
||||
if (printtri.tri == m->dummytri) {
|
||||
printf(" [6] = Outer space\n");
|
||||
} else {
|
||||
printf(" [6] = x%lx %d\n", (unsigned long) printtri.tri,
|
||||
printf(" [6] = x%lx %d\n", (uintptr_t) printtri.tri,
|
||||
printtri.orient);
|
||||
}
|
||||
decode(s->ss[7], printtri);
|
||||
if (printtri.tri == m->dummytri) {
|
||||
printf(" [7] = Outer space\n");
|
||||
} else {
|
||||
printf(" [7] = x%lx %d\n", (unsigned long) printtri.tri,
|
||||
printf(" [7] = x%lx %d\n", (uintptr_t) printtri.tri,
|
||||
printtri.orient);
|
||||
}
|
||||
|
||||
|
|
@ -2054,14 +2055,14 @@ struct osub *s;
|
|||
printf(" Segment origin[%d] = NULL\n", 4 + s->ssorient);
|
||||
else
|
||||
printf(" Segment origin[%d] = x%lx (%.12g, %.12g)\n",
|
||||
4 + s->ssorient, (unsigned long) printvertex,
|
||||
4 + s->ssorient, (uintptr_t) printvertex,
|
||||
printvertex[0], printvertex[1]);
|
||||
segdest(*s, printvertex);
|
||||
if (printvertex == (vertex) NULL)
|
||||
printf(" Segment dest [%d] = NULL\n", 5 - s->ssorient);
|
||||
else
|
||||
printf(" Segment dest [%d] = x%lx (%.12g, %.12g)\n",
|
||||
5 - s->ssorient, (unsigned long) printvertex,
|
||||
5 - s->ssorient, (uintptr_t) printvertex,
|
||||
printvertex[0], printvertex[1]);
|
||||
}
|
||||
|
||||
|
|
@ -2124,7 +2125,7 @@ struct memorypool *pool;
|
|||
#endif /* not ANSI_DECLARATORS */
|
||||
|
||||
{
|
||||
unsigned long alignptr;
|
||||
uintptr_t alignptr;
|
||||
|
||||
pool->items = 0;
|
||||
pool->maxitems = 0;
|
||||
|
|
@ -2132,11 +2133,11 @@ struct memorypool *pool;
|
|||
/* Set the currently active block. */
|
||||
pool->nowblock = pool->firstblock;
|
||||
/* Find the first item in the pool. Increment by the size of (VOID *). */
|
||||
alignptr = (unsigned long) (pool->nowblock + 1);
|
||||
alignptr = (uintptr_t) (pool->nowblock + 1);
|
||||
/* Align the item on an `alignbytes'-byte boundary. */
|
||||
pool->nextitem = (VOID *)
|
||||
(alignptr + (unsigned long) pool->alignbytes -
|
||||
(alignptr % (unsigned long) pool->alignbytes));
|
||||
(alignptr + (uintptr_t) pool->alignbytes -
|
||||
(alignptr % (uintptr_t) pool->alignbytes));
|
||||
/* There are lots of unallocated items left in this block. */
|
||||
pool->unallocateditems = pool->itemsfirstblock;
|
||||
/* The stack of deallocated items is empty. */
|
||||
|
|
@ -2241,7 +2242,7 @@ struct memorypool *pool;
|
|||
{
|
||||
VOID *newitem;
|
||||
VOID **newblock;
|
||||
unsigned long alignptr;
|
||||
uintptr_t alignptr;
|
||||
|
||||
/* First check the linked list of dead items. If the list is not */
|
||||
/* empty, allocate an item from the list rather than a fresh one. */
|
||||
|
|
@ -2266,11 +2267,11 @@ struct memorypool *pool;
|
|||
pool->nowblock = (VOID **) *(pool->nowblock);
|
||||
/* Find the first item in the block. */
|
||||
/* Increment by the size of (VOID *). */
|
||||
alignptr = (unsigned long) (pool->nowblock + 1);
|
||||
alignptr = (uintptr_t) (pool->nowblock + 1);
|
||||
/* Align the item on an `alignbytes'-byte boundary. */
|
||||
pool->nextitem = (VOID *)
|
||||
(alignptr + (unsigned long) pool->alignbytes -
|
||||
(alignptr % (unsigned long) pool->alignbytes));
|
||||
(alignptr + (uintptr_t) pool->alignbytes -
|
||||
(alignptr % (uintptr_t) pool->alignbytes));
|
||||
/* There are lots of unallocated items left in this block. */
|
||||
pool->unallocateditems = pool->itemsperblock;
|
||||
}
|
||||
|
|
@ -2325,16 +2326,16 @@ struct memorypool *pool;
|
|||
#endif /* not ANSI_DECLARATORS */
|
||||
|
||||
{
|
||||
unsigned long alignptr;
|
||||
uintptr_t alignptr;
|
||||
|
||||
/* Begin the traversal in the first block. */
|
||||
pool->pathblock = pool->firstblock;
|
||||
/* Find the first item in the block. Increment by the size of (VOID *). */
|
||||
alignptr = (unsigned long) (pool->pathblock + 1);
|
||||
alignptr = (uintptr_t) (pool->pathblock + 1);
|
||||
/* Align with item on an `alignbytes'-byte boundary. */
|
||||
pool->pathitem = (VOID *)
|
||||
(alignptr + (unsigned long) pool->alignbytes -
|
||||
(alignptr % (unsigned long) pool->alignbytes));
|
||||
(alignptr + (uintptr_t) pool->alignbytes -
|
||||
(alignptr % (uintptr_t) pool->alignbytes));
|
||||
/* Set the number of items left in the current block. */
|
||||
pool->pathitemsleft = pool->itemsfirstblock;
|
||||
}
|
||||
|
|
@ -2362,7 +2363,7 @@ struct memorypool *pool;
|
|||
|
||||
{
|
||||
VOID *newitem;
|
||||
unsigned long alignptr;
|
||||
uintptr_t alignptr;
|
||||
|
||||
/* Stop upon exhausting the list of items. */
|
||||
if (pool->pathitem == pool->nextitem) {
|
||||
|
|
@ -2374,11 +2375,11 @@ struct memorypool *pool;
|
|||
/* Find the next block. */
|
||||
pool->pathblock = (VOID **) *(pool->pathblock);
|
||||
/* Find the first item in the block. Increment by the size of (VOID *). */
|
||||
alignptr = (unsigned long) (pool->pathblock + 1);
|
||||
alignptr = (uintptr_t) (pool->pathblock + 1);
|
||||
/* Align with item on an `alignbytes'-byte boundary. */
|
||||
pool->pathitem = (VOID *)
|
||||
(alignptr + (unsigned long) pool->alignbytes -
|
||||
(alignptr % (unsigned long) pool->alignbytes));
|
||||
(alignptr + (uintptr_t) pool->alignbytes -
|
||||
(alignptr % (uintptr_t) pool->alignbytes));
|
||||
/* Set the number of items left in the current block. */
|
||||
pool->pathitemsleft = pool->itemsperblock;
|
||||
}
|
||||
|
|
@ -2430,16 +2431,16 @@ int subsegbytes;
|
|||
#endif /* not ANSI_DECLARATORS */
|
||||
|
||||
{
|
||||
unsigned long alignptr;
|
||||
uintptr_t alignptr;
|
||||
|
||||
/* Set up `dummytri', the `triangle' that occupies "outer space." */
|
||||
m->dummytribase = (triangle *) trimalloc(trianglebytes +
|
||||
m->triangles.alignbytes);
|
||||
/* Align `dummytri' on a `triangles.alignbytes'-byte boundary. */
|
||||
alignptr = (unsigned long) m->dummytribase;
|
||||
alignptr = (uintptr_t) m->dummytribase;
|
||||
m->dummytri = (triangle *)
|
||||
(alignptr + (unsigned long) m->triangles.alignbytes -
|
||||
(alignptr % (unsigned long) m->triangles.alignbytes));
|
||||
(alignptr + (uintptr_t) m->triangles.alignbytes -
|
||||
(alignptr % (uintptr_t) m->triangles.alignbytes));
|
||||
/* Initialize the three adjoining triangles to be "outer space." These */
|
||||
/* will eventually be changed by various bonding operations, but their */
|
||||
/* values don't really matter, as long as they can legally be */
|
||||
|
|
@ -2459,10 +2460,10 @@ int subsegbytes;
|
|||
m->dummysubbase = (subseg *) trimalloc(subsegbytes +
|
||||
m->subsegs.alignbytes);
|
||||
/* Align `dummysub' on a `subsegs.alignbytes'-byte boundary. */
|
||||
alignptr = (unsigned long) m->dummysubbase;
|
||||
alignptr = (uintptr_t) m->dummysubbase;
|
||||
m->dummysub = (subseg *)
|
||||
(alignptr + (unsigned long) m->subsegs.alignbytes -
|
||||
(alignptr % (unsigned long) m->subsegs.alignbytes));
|
||||
(alignptr + (uintptr_t) m->subsegs.alignbytes -
|
||||
(alignptr % (uintptr_t) m->subsegs.alignbytes));
|
||||
/* Initialize the two adjoining subsegments to be the omnipresent */
|
||||
/* subsegment. These will eventually be changed by various bonding */
|
||||
/* operations, but their values don't really matter, as long as they */
|
||||
|
|
@ -2819,7 +2820,7 @@ int number;
|
|||
{
|
||||
VOID **getblock;
|
||||
char *foundvertex;
|
||||
unsigned long alignptr;
|
||||
uintptr_t alignptr;
|
||||
int current;
|
||||
|
||||
getblock = m->vertices.firstblock;
|
||||
|
|
@ -2836,9 +2837,9 @@ int number;
|
|||
}
|
||||
|
||||
/* Now find the right vertex. */
|
||||
alignptr = (unsigned long) (getblock + 1);
|
||||
foundvertex = (char *) (alignptr + (unsigned long) m->vertices.alignbytes -
|
||||
(alignptr % (unsigned long) m->vertices.alignbytes));
|
||||
alignptr = (uintptr_t) (getblock + 1);
|
||||
foundvertex = (char *) (alignptr + (uintptr_t) m->vertices.alignbytes -
|
||||
(alignptr % (uintptr_t) m->vertices.alignbytes));
|
||||
return (vertex) (foundvertex + m->vertices.itembytes * (number - current));
|
||||
}
|
||||
|
||||
|
|
@ -5882,7 +5883,7 @@ struct otri *searchtri;
|
|||
char *firsttri;
|
||||
struct otri sampletri;
|
||||
vertex torg, tdest;
|
||||
unsigned long alignptr;
|
||||
uintptr_t alignptr;
|
||||
REAL searchdist, dist;
|
||||
REAL ahead;
|
||||
long samplesperblock, totalsamplesleft, samplesleft;
|
||||
|
|
@ -5954,11 +5955,11 @@ struct otri *searchtri;
|
|||
population = totalpopulation;
|
||||
}
|
||||
/* Find a pointer to the first triangle in the block. */
|
||||
alignptr = (unsigned long) (sampleblock + 1);
|
||||
alignptr = (uintptr_t) (sampleblock + 1);
|
||||
firsttri = (char *) (alignptr +
|
||||
(unsigned long) m->triangles.alignbytes -
|
||||
(uintptr_t) m->triangles.alignbytes -
|
||||
(alignptr %
|
||||
(unsigned long) m->triangles.alignbytes));
|
||||
(uintptr_t) m->triangles.alignbytes));
|
||||
|
||||
/* Choose `samplesleft' randomly sampled triangles in this block. */
|
||||
do {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ fn main() -> ExitCode {
|
|||
"windows" => (
|
||||
repo_root.join(format!("scripts/windows/{action}.ps1")),
|
||||
vec![
|
||||
"powershell",
|
||||
"pwsh",
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
|
|
|
|||
Loading…
Reference in New Issue